原始项目在这里。原始项目不再维护,只是作为仓库保存历史代码。 此项目参考了nifty项目,虽然该项目不在维护了,有些地方还是值得学习的。
配合spring,实现自动装配出thrift的server
- 使用自定义的注解
- 在spring容器初始化后,获取全部的thrift server实现类。
- 构建服务类统一代理实现的thrift server类,并最终注入到ioc容器中
配合spring,实现自动装配出thrift的客户端
- 使用自定义的注解,标记客户端使用的熔断类
- 使用cglib动态代理,在thrift调用中出现服务熔断,则调用本地实现类
- 手动注册客户端bean
添加对spring cloud的支持。
- 改造eureka客户端的注册流程。在服务端启动后,如果有配置eureka,则在eureka客户端注册的服务信息中添加thrift服务信息,包括服务名称和端口
- 改造eureka客户端的获取服务信息流程。在客户端启动后,如果有配置成从eureka获取服务信息,则从eureka注册中心获取服务的信息并提取其中的metaData,解析metaData获取thrift服务信息。
- 注册监听器,监听eureka客户端的服务列表更新。获取最新的服务列表后更新本地的thrift服务信息
融合server和client。实现server和client并存,赋予微服务服务能力的同时也有客户端功能。
分离客户端和eurake客户端,把纯封装thrift的抽出成client-core,再定义eurake客户端实现eurake-thrift-client。eurake-thrift-client只需提供eurake相关的bean并引入client-core依赖组装而成。分离的目的为了未来更好地扩展出其他版本的服务注册中心的客户端 同理分离服务端
修改客户端处理服务的逻辑。原来的扫描全部的服务并建立代理bean的方式有缺陷。在微服务不需要大部分的服务时,只会消耗系统资源去维护服务列表(包括列表刷新和底层socket的操作)。 因此,现在改成按需建立代理bean,只有被ThriftClient注解注释的才会被认为是需要的服务而创建代理bean。同时被注解注释的类会作为熔断回调类在服务down的时候调用。
修改客户端的socket管理。目前的管理方式没有处理服务端掉线的问题。尝试使用netty做底层通讯框架,方便管理socket和处理各种事件。设置掉线重连机制。
thrift耦合了netty后,底层的实际通讯逻辑经由netty实现。把耦合后的thrift和netty从spring的逻辑中抽离出来,形成新的模块,spring只是负责管理依赖和注入依赖,thrift负责管理rpc的协议和调用,netty负责管理底层的网络通讯
服务端和客户端都采用了springboot starter标准,实现只需引入便能自动装配的功能。
客户端和服务端都采用了Apache开源的thrift RPC框架。使用thrift作为通讯协议。
服务端采用了netty框架作为通讯框架。 客户端也可以采用了netty作为通讯框架。
客户端中,所有的thrift服务的是通过cglib动态代理生成代理对象,并注入到ioc容器中,和mybatis的接口注入原理一样。
客户端和服务端都需要用到spring的ioc容器。因此,免不了和ioc注入打交道,实现自定义的注入逻辑。需要了解ioc注入的生命周期、注入的切入点和触发点。
- 触发条件:客户端使用同一个socket(同一个服务)进行并发地请求
- 问题:服务端netty接收到数据包后进行解码时,发现多个请求的数据糅合在一起,导致无法正常解析。
- 根本原因:thrift的客户端是线程不安全的,当多个请求调用时,底层是共享一个socket。因此在并发的情况下,socket的缓冲区会就会出现写混乱,是比拆包粘包更严重的问题。
- 解决方案:让每个thrift的socket独占一个线程。多个线程对同一个socket的调用,会先放到任务队列中,使用同步机制回去异步执行的结果。
- 触发条件:客户端在更新thrift服务列表时,接收用户的请求并调用被更新的服务
- 问题:thrift服务列表更新时,如果是执行移动服务操作,则有几率响应用户请求的时候,刚好获取列表最后一个实例的同时,该实例被移除,会导致访问越界。
- 根本原因:多线程安全问题
- 解决方案:加读写锁,不过会有一定的响应性能损耗,一定几率会挂起响应。如果不加锁,直接捕抓异常,则一定几率降低服务质量,会触发熔断
- 存在的困难:原始的nifty项目在客户端这一块的处理是按照有序请求的方式处理的。请求在发送后都会阻塞,下个请求需要等待上一个完成了才能进行处理。 所以要求服务端也是有序的响应。目前服务端的逻辑是强制性有序的。目前需要解决有序和无序的兼容。有序是原生的thrift客户端,无序的是netty代理的客户端。
- 解决方案:设计上,netty的channel代理thrift的transport进行读写,每次请求获取客户端都新建一个代理后的transport,其中该transport复用相同ip端口的channel
- 副作用:每次的请求都需要反射生成thrift的客户端,需要额外的开销
- 问题:在多线程的情况下,底层创建channel的逻辑没有加锁,导致高并发的情况下会出现多个相同目标主机的链接的channel。
- 解决方案:加上锁
- 问题:不清除thrift是怎样构造请求的,在获取seqId的时候,不同的请求得到的结果是一样的。因此推测seqId是对于同一个请求而言才有意义。
- 解决方案:再把thrift协议封装一次,每个channel为所发送的thrift请求添加唯一的id,接收的时候按照id进行响应
- 问题:在多个线程同时调用同一个channel发送请求时,会出现某些请求失败,一直得不到服务端的响应。通过抓包排查发现,请求的数据包确实是通过网卡发送出去了,而服务器的网卡也接收到了,但是服务进程却没有接收到数据包。 通过对比单线程循环发送请求的tcp数据包顺序和多线程并发发送的顺序,发现循环发送的情况数据包是按照:发->收->确认的顺序。而多线程的情况下是多个线程的数据包按照先后次序依次发送,并没有等服务端的响应,随后服务端的响应也只是其中某几个请求。 通过对比,分析得出推论:在多线程的情况下,可能是缺少了确认的数据包,导致通讯不完整,只有某些包是有确认数据包的,所以只有某些请求响应了
- 原因:通过进一步分析,判断有可能是tcp粘包拆包的问题造成的。仔细查看下,客户端是做了粘包拆包处理,但是经过分析,服务端并没有。因此怀疑是服务端没有做相应的处理, 添加相应的处理后,果然就可以了。
- 解决方案:客户端和服务端都要添加tcp粘包拆包的处理
- 问题:在多线程访问下,会有一定几率出现内存泄漏的异常
- 关键类的架构图
- client-core
只是核心的部分,实现了扫描客户端并动态代理的功能。细节上着重体现在注入和维护两方面。
- client-eureka
- 结合netty
thrift框架和netty框架结合。需要理解thrift的架构是怎样的,在那个点的功能可以由netty进行代理。 经过粗糙的理解,thrift定义了transport的概念负责底层的socket操作的,从这个点出发,用netty进行代理transport就可以实现结合两个框架
只有重大的逻辑变更才会记录在这里
- 2020-06-24
原来的客户端是使用原生的thrift客户端进行rpc通讯的。当搭配eureka使用的时候,需要对eureka监听到的服务刷新事件进行处理。底层需要维护一套在线可用的transport列表
经过netty化后的客户端,可以减少维护成本,服务的可用只需根据netty的链接状态即可维护,此时的eureka更多的只是作为一个服务信息增量提供的中间件。

