本文将分享一个高可用的池化 Thrift Client 及其源码实现,欢迎阅读源码(Github)并使用,同时欢迎提出宝贵的意见和建议,本人将持续完善。

本文的主要目标读者是对 Thrift 有一定了解并使用的童鞋,如对 Thrift 的基础知识了解不多或者想重温一下基础知识,推荐先阅读本站文章《和 Thrift 的一场美丽邂逅》。

下面进入正题。

为什么我们需要这么一个组件?

我们知道,Thrift 是一个 RPC 框架体系,可以非常方便的进行跨语言 RPC 服务的开发和调用。然而,它并没有提供针对多个 Server 的 Smart Client【1】。比如,你有一个服务 service,分别部署在 116.31.1.1 和 116.31.1.2 两台服务器上,当你需要从 Client 端调用该 service 的某个远程方法的时候,你只能在代码中显式指定使用 116.31.1.1 或者 116.31.1.2 其中的一个。这种情况下,你调用的时候无法预知所指定 IP 对应的服务是否可用,并且当该服务不可用时,无法隐式自动切换到调用另外一个 IP 对应的服务。也就是说,服务的状态对你并不是透明的,并且无法做到服务的负载均衡和高可用。

此外,当你调用远程方法时,每次你都得新建一个连接,当请求量很大时,不断的创建、删除连接所耗费的服务资源是巨大的。

因此,我们需要这么一个组件,使服务状态透明化并底层实现负载均衡和高可用,让你可以专注于业务逻辑的实现,提升工作效率和服务的质量。下面我们就对该组件(ThrifJ)进行详细的剖析。

它到底能做些什么?

特性

  • 链式调用API,简洁直观
  • 完善的默认配置,无需担心调用时配置不全导致抛错
  • 池化连接对象,高效管理连接的生命周期
  • 异常服务自动隔离与恢复
  • 多种可配置的负载均衡策略,支持随机、轮询、权重和哈希
  • 多种可配置的服务级别,并自动根据服务级别进行服务降级

该如何使用它?

目前最新版本为1.0.1(点此关注最新版本的更新),首先在项目中引入 thriftj-1.0.1.jar,或在 Maven 依赖中加入:

<dependency>
<groupId>com.github.cyfonly</groupId>
<artifactId>thriftj</artifactId>
<version>1.0.1</version>
</dependency>

需要注意的是,ThriftJ 基于 slf4j 构建,因此你需要在项目中增加具体日志实现的依赖,比如 log4j 或 logback。

然后在项目中,参照以下这段代码进行调用:

//Thrift server 列表
private static final String servers = "127.0.0.1:10001,127.0.0.1:10002"; //TTransport 验证器
ConnectionValidator validator = new ConnectionValidator() {
@Override
public boolean isValid(TTransport object) {
return object.isOpen();
}
}; //连接对象池配置
GenericKeyedObjectPoolConfig poolConfig = new GenericKeyedObjectPoolConfig(); //failover 策略
FailoverStrategy failoverStrategy = new FailoverStrategy(); //构造 ThriftClient 对象并配置
final ThriftClient thriftClient = new ThriftClient();
thriftClient.servers(servers)
.loadBalance(Constant.LoadBalance.RANDOM)
.connectionValidator(validator)
.poolConfig(poolConfig)
.failoverStrategy(failoverStrategy)
.connTimeout(5)
.backupServers("")
.serviceLevel(Constant.ServiceLevel.NOT_EMPTY)
.start(); //打印从 ThriftClient 获取到的可用服务列表
List<ThriftServer> servers = thriftClient.getAvailableServers();
for(ThriftServer server : servers){
System.out.println(server.getHost() + ":" + server.getPort());
} //服务调用
if(servers.size()>0){
try{
TestThriftJ.Client client = thriftClient.iface(TestThriftJ.Client.class);
QryResult result = client.qryTest(1);
System.out.println("result[code=" + result.code + " msg=" + result.msg + "]");
}catch(Throwable t){
logger.error("-------------exception happen", t);
}
}

友情提示:除 servers 必须配置外,其他配置均为可选(使用默认配置)

它是如何设计并实现的呢?

整体设计

连接池对象工厂及连接对象的管理

基于 commons-pool2 中的 KeyedPooledObjectFactory,以 ThriftServer 为 key,TTransport 为 value 进行实现。关键代码如下:

@Override
public PooledObject<TTransport> makeObject(ThriftServer thriftServer) throws Exception {
TSocket tsocket = new TSocket(thriftServer.getHost(), thriftServer.getPort());
tsocket.setTimeout(timeout);
TFramedTransport transport = new TFramedTransport(tsocket); transport.open();
DefaultPooledObject<TTransport> result = new DefaultPooledObject<TTransport>(transport);
logger.trace("Make new thrift connection: {}:{}", thriftServer.getHost(), thriftServer.getPort()); return result;
} @Override
public boolean validateObject(ThriftServer thriftServer, PooledObject<TTransport> pooledObject) {
boolean isValidate;
try {
if (failoverChecker == null) {
isValidate = pooledObject.getObject().isOpen();
} else {
ConnectionValidator validator = failoverChecker.getConnectionValidator();
isValidate = pooledObject.getObject().isOpen() && (validator == null || validator.isValid(pooledObject.getObject()));
}
} catch (Throwable e) {
logger.warn("Fail to validate tsocket: {}:{}", new Object[]{thriftServer.getHost(), thriftServer.getPort(), e});
isValidate = false;
}
if (failoverChecker != null && !isValidate) {
failoverChecker.getFailoverStrategy().fail(thriftServer);
}
logger.info("ValidateObject isValidate:{}", isValidate); return isValidate;
} @Override
public void destroyObject(ThriftServer thriftServer, PooledObject<TTransport> pooledObject) throws Exception {
TTransport transport = pooledObject.getObject();
if (transport != null) {
transport.close();
logger.trace("Close thrift connection: {}:{}", thriftServer.getHost(), thriftServer.getPort());
}
}

在使用连接对象时,根据用户的自定义连接池配置创建连接池,并实现连接对象的获取、回池、清除以及连接池的关闭操作。关键代码如下:

public DefaultThriftConnectionPool(KeyedPooledObjectFactory<ThriftServer, TTransport> factory, GenericKeyedObjectPoolConfig config) {
connections = new GenericKeyedObjectPool<>(factory, config);
} @Override
public TTransport getConnection(ThriftServer thriftServer) {
try {
return connections.borrowObject(thriftServer);
} catch (Exception e) {
logger.warn("Fail to get connection for {}:{}", new Object[]{thriftServer.getHost(), thriftServer.getPort(), e});
throw new RuntimeException(e);
}
} @Override
public void returnConnection(ThriftServer thriftServer, TTransport transport) {
connections.returnObject(thriftServer, transport);
} @Override
public void returnBrokenConnection(ThriftServer thriftServer, TTransport transport) {
try {
connections.invalidateObject(thriftServer, transport);
} catch (Exception e) {
logger.warn("Fail to invalid object:{},{}", new Object[] { thriftServer, transport, e });
}
} @Override
public void close() {
connections.close();
} @Override
public void clear(ThriftServer thriftServer) {
connections.clear(thriftServer);
}

异常服务自动隔离与恢复

需要实现服务状态的透明化,就必须在底层实现服务的监测、隔离和恢复。在 ThriftJ 中,调用 ThriftClient 时会启动一个线程对服务进行异步监测,用户可以指定检验规则(对应配置为 ConnectionValidator)以及 failover 策略(对应配置为 FailoverStrategy,可以指定失败的次数、失效持续时间和恢复持续时间)。默认情况下,服务验证规则为判断 TTransport 是否处于开启状态,即:

if (this.validator == null) {
  this.validator = new ConnectionValidator() {
    @Override
    public boolean isValid(TTransport object) {
      return object.isOpen();
    }
  };
}

而默认的 failover 策略为

  • 失败次数:10(次),表示通过 ConnectionValidator 检验失败 10 次后才考虑将该服务失效,需要配合失效持续时间一起使用
  • 时效持续时间:1(分钟),表示在一个检验周期内,首次检验失败的时间持续达到该值后才考虑将该服务失效,配合失败次数一起使用
  • 恢复持续时间:1(分钟),表示在判定某服务失效并隔离后,经过该值后将服务重新恢复

以上功能基于 Guava cache 实现,关键代码如下:

/**
* 使用默认 failover 策略
*/
public FailoverStrategy() {
this(DEFAULT_FAIL_COUNT, DEFAULT_FAIL_DURATION, DEFAULT_RECOVER_DURATION);
} /**
* 自定义 failover 策略
* @param failCount 失败次数
* @param failDuration 失效持续时间
* @param recoverDuration 恢复持续时间
*/
public FailoverStrategy(final int failCount, long failDuration, long recoverDuration) {
this.failDuration = failDuration;
this.failedList = CacheBuilder.newBuilder().weakKeys().expireAfterWrite(recoverDuration, TimeUnit.MILLISECONDS).build();
this.failCountMap = CacheBuilder.newBuilder().weakKeys().build(new CacheLoader<T, EvictingQueue<Long>>() {
@Override
public EvictingQueue<Long> load(T key) throws Exception {
return EvictingQueue.create(failCount);
}
});
} public void fail(T object) {
logger.info("Server {}:{} failed.", ((ThriftServer)object).getHost(),((ThriftServer)object).getPort());
boolean addToFail = false;
try {
EvictingQueue<Long> evictingQueue = failCountMap.get(object);
synchronized (evictingQueue) {
evictingQueue.add(System.currentTimeMillis());
if (evictingQueue.remainingCapacity() == 0 && evictingQueue.element() >= (System.currentTimeMillis() - failDuration)) {
addToFail = true;
}
}
} catch (ExecutionException e) {
logger.error("Ops.", e);
}
if (addToFail) {
failedList.put(object, Boolean.TRUE);
logger.info("Server {}:{} failed. Add to fail list.", ((ThriftServer)object).getHost(), ((ThriftServer)object).getPort());
}
} public Set<T> getFailed() {
return failedList.asMap().keySet();
}

负载均衡

ThriftJ 提供了四种可选的负载均衡策略:

  • 随机
  • 轮询
  • 权重
  • 哈希

在用户不显式指定的情况下,默认采用随机算法。具体算法的实现在此就不再进行过多的描述了。

需要注意的是,ThriftJ 严格规范了调用的语义,比如使用哈希策略时,必须要指定 hash key;当使用非哈希的其他策略时,一定不能指定 key,避免造成理解的二义性。

服务级别与服务降级

ThriftJ 提供了多种可配置的服务级别,并根据服务级别进行服务降级处理,其对应关系如下:

  • SERVERS_ONLY:最高级别,仅返回配置的 servers 列表中可用的服务
  • ALL_SERVERS:中等级别,当 servers 列表中的服务全部不可用时,返回 backupServers 列表中的可用服务
  • NOT_EMPTY:最低级别,当 servers 和 backupServers 列表中的服务全部不可用时,返回 servers 列表中的所有服务

其中 ThriftJ 默认使用的服务级别是 NOT_EMPTY。服务降级处理的关键代码如下:

private List<ThriftServer> getAvailableServers(boolean all) {
List<ThriftServer> returnList = new ArrayList<>();
Set<ThriftServer> failedServers = failoverStrategy.getFailed();
for (ThriftServer thriftServer : serverList) {
if (!failedServers.contains(thriftServer))
returnList.add(thriftServer);
}
if (this.serviceLevel == Constant.ServiceLevel.SERVERS_ONLY) {
return returnList;
}
if ((all || returnList.isEmpty()) && !backupServerList.isEmpty()) {
for (ThriftServer thriftServer : backupServerList) {
if (!failedServers.contains(thriftServer))
returnList.add(thriftServer);
}
}
if (this.serviceLevel == Constant.ServiceLevel.ALL_SERVERS) {
return returnList;
}
if(returnList.isEmpty()){
returnList.addAll(serverList);
}
return returnList;
}

我还有话要说

技术的提升源自无私的分享,好的技术或工具分享出来,并不会让自己失去什么,反而可以在大家共同研究和沟通后使之获得更好的完善。不要担心自己写的工具不够好,不要害怕自己的技术不够牛,谁能一步就登天呢?

请热爱你的热爱!

【1】Smart Client:比如 MongoClient,可自动发现集群服务节点、自动故障转移和负载均衡。

高可用的池化 Thrift Client 实现(源码分享)的更多相关文章

  1. Android 全面插件化 RePlugin 流程与源码解析

    转自 Android 全面插件化 RePlugin 流程与源码解析 RePlugin,360开源的全面插件化框架,按照官网说的,其目的是“尽可能多的让模块变成插件”,并在很稳定的前提下,尽可能像开发普 ...

  2. Java并发包源码学习之线程池(一)ThreadPoolExecutor源码分析

    Java中使用线程池技术一般都是使用Executors这个工厂类,它提供了非常简单方法来创建各种类型的线程池: public static ExecutorService newFixedThread ...

  3. 多线程高并发编程(12) -- 阻塞算法实现ArrayBlockingQueue源码分析(1)

    一.前言 前文探究了非阻塞算法的实现ConcurrentLinkedQueue安全队列,也说明了阻塞算法实现的两种方式,使用一把锁(出队和入队同一把锁ArrayBlockingQueue)和两把锁(出 ...

  4. IdentityServer4同时使用多个GrantType进行授权和IdentityModel.Client部分源码解析

    首先,介绍一下问题. 由于项目中用户分了三个角色:管理员.代理.会员.其中,代理又分为一级代理.二级代理等,会员也可以相互之间进行推荐. 将用户表分为了两个,管理员和代理都属于后台,在同一张表,会员单 ...

  5. iOS天气动画、高仿QQ菜单、放京东APP、高仿微信、推送消息等源码

    iOS精选源码 TYCyclePagerView iOS上的一个无限循环轮播图组件 iOS高仿微信完整项目源码 想要更简单的推送消息,看本文就对了 ScrollView嵌套ScrolloView解决方 ...

  6. 多线程高并发编程(11) -- 非阻塞队列ConcurrentLinkedQueue源码分析

    一.背景 要实现对队列的安全访问,有两种方式:阻塞算法和非阻塞算法.阻塞算法的实现是使用一把锁(出队和入队同一把锁ArrayBlockingQueue)和两把锁(出队和入队各一把锁LinkedBloc ...

  7. (三)FastDFS 高可用集群架构学习---Client 接口开发

    一.Python3 与 FastDFS 交互 1.安装 py3fdfs模块 # pip3 install py3Fdfs 2.测试使用 py3Fdfs 与 Fastdfs 集群交互(上传文件) fro ...

  8. java并发编程(四) 线程池 & 任务执行、终止源码分析

    参考文档 线程池任务执行全过程:https://blog.csdn.net/wojiaolinaaa/article/details/51345789 线程池中断:https://www.cnblog ...

  9. 线程池技术之:ThreadPoolExecutor 源码解析

    java中的所说的线程池,一般都是围绕着 ThreadPoolExecutor 来展开的.其他的实现基本都是基于它,或者模仿它的.所以只要理解 ThreadPoolExecutor, 就相当于完全理解 ...

随机推荐

  1. PE文件结构(五岁以下儿童)基地搬迁

    PE文件结构(五岁以下儿童) 參考 书:<加密与解密> 视频:小甲鱼 解密系列 视频 基址重定位 链接器生成一个PE文件时,它会如果程序被装入时使用的默认ImageBase基地址(VC默认 ...

  2. leetcode第38题--Combination Sum

    题目: Given a set of candidate numbers (C) and a target number (T), find all unique combinations in C  ...

  3. mediawiki在windows下的安装

    mediawiki在windows下的安装 对于刚接触wiki的朋友们来说,配置一个服务器环境,安装并运行mediawiki是一件很麻烦的事情,在这里,我尽量用通俗易懂的语言,介绍mw(mediawi ...

  4. Android项目----dispathTouchEvent

    说到dispathTouchEvent,就不得不说一个最贱的屏幕触摸动作触发的一些列Touch事件: ACTION_DOWN->ACTION_MOVE->ACTION_MOVE->A ...

  5. NhibernateProfiler-写个自动破解工具(源码)

    04 2013 档案   [屌丝的逆袭系列]是个人都能破解之终结NhibernateProfiler-写个自动破解工具(源码) 摘要: 破解思路分析及手动破解 增加“附加到进程”功能--功能介绍增加“ ...

  6. php和表单(1)

    先来一段处理表单的html代码(test.html) <form action="index.php" method="post"> name : ...

  7. ASP.NET MVC + 百度富文本编辑器 + EasyUi + EntityFrameWork 制作一个添加新闻功能

    本文将交大伙怎么集成ASP.NET MVC + 百度富文本编辑器 + EasyUi + EntityFrameWork来制作一个新闻系统 先上截图: 添加页面如下: 下面来看代码部分 列表页如下: @ ...

  8. Windows上memcached的使用

    Memcached是什么?Memcached是由Danga Interactive开发的,高性能的,分布式的内存对象缓存系统,用于在动态应用中减少数据库负载,提升访问速度. Memcached能缓存什 ...

  9. Essential C#读书笔记

    Essential C#读书笔记 这是一个多变的时代,一次又一次的浪潮将不同的人推上了巅峰.新的人想搭上这一波,同时老的人也不想死在沙滩上.这些年新的浪潮又一次推开,历史不停地重复上演,那便是移动互联 ...

  10. [转]Hacking the iOS Spotlight

    原文:http://theiostream.tumblr.com/post/36905860826/hacking-the-ios-spotlight 原文:http://theiostream.tu ...