Spring Cloud 是一个优秀的开源微服务解决方案,通常采用 HTTP + json 的 REST 接口对外提供服务,简洁易用部署方便,很多公司也基于 Spring Cloud 作为基础架构去构建自身的微服务架构。但是随着业务规模和用户规模的增长,传统基于的 HTTP 的服务会逐步暴露出一些问题。
首先是性能的问题,随着用户请求量的增长和业务逻辑复杂度的提升,我们会发现微服务的单机性能会成为系统瓶颈。
其次是稳定性问题,当一个服务节点A需要依赖于后端的几个服务的时候,我们会发现当其中一个被依赖的服务发生卡顿,很可能会导致前端的服务节点A产生毛刺甚至无法继续提供服务,而且当问题节点没有能够被及时屏蔽或者恢复的时候,还有可能会导致整个系统雪崩。
TARS如何为SpringCloud提供高性能解决方案
TARS 是腾讯从2008年到今天一直在使用的后台逻辑层的统一应用框架,上述问题在 TARS 框架的发展过程中已经得到了比较好的解决。现在,TARS 通过插件集成到 Spring Cloud 体系中,希望通过输出 TARS 的 RPC 能力针对某些对性能和稳定性要求更高应用的场景提供一种新的解决方案,并且提供了基于 Spring Boot 的开发方式,符合Spring Cloud开发者的使用习惯,可以仅使用较小的开发成本在整个Spring Cloud体系中引入TARS的RPC能力。
将 TARS 结合到 Spring Cloud 中使用,通过 TARS 提供的长连接异步调用和二进制协议可以明显的提升 RPC 调用性能。长连接通过连接复用减少整体的连接数量减少了资源消耗,同时通过二进制协议提升了编解码效率提升了整体的 RPC 性能。
一. 解决SpringCloud传统HTTP网络连接使用率不高的问题
问题:
由于HTTP协议本身是无状态的,所以发起一次请求的时候必须等待上一个请求响应才能再次使用这个连接,就算是采用流水线模式一个连接上的请求也会被之前发起的请求所阻塞,如果要提高并发能力则必须要建立大量的连接,而连接的建立、维持和销毁都会消耗系统资源。
解决:
因为HTTP协议的特性,HTTP的回包是依赖于请求的先后顺序的,必须要按照顺序处理完一个请求再处理下一个请求,如果希望并行的处理请求则只能通过建立新的链接从而产生建连的时间开销以及维护连接需要的CPU和内存资源。
而TARS的协议设计是TARS的私有协议,每个请求会带有一个请求id,通过同一个链接来发送多个请求可以通过id来匹配返回从而避免了线程阻塞,从而降低了硬件资源消耗。
通过上图可以看到,TARS可以在同一个连接上不断的写入请求和接收响应,而客户端通过请求Id来关联每一个请求和对应的响应,从而可以复用连接,避免了资源的浪费,通常情况下一个客户端和一个服务端之间仅使用数个连接就可以满足传输的要求。
二. 解决SpringCloud传统HTTP通讯协议性能低下的问题
问题:
HTTP + json 本身是一种可读性很高的文本协议,因此实际传输的数据包会比二进制协议要大不少,而且文本协议在数据的序列化反序列化效率上相比二进制数据的效率要低很多,所以 HTTP 协议本身的性能就不高。
TARS的数据传输采用的是TARS协议进行编解码,TARS协议是一种二进制协议,相较于常见的JSON等文本协议,二进制协议主要有两个方面的优势:
1. 编解码效率
二进制协议的编解码是按二进制位直接进行编解码的,减少了对不确定的字符串解析的过程,直接从对应的二进制位读取数据,效率相比解析文本协议有非常大的提升。
2. 网络包大小
因为所有的数据都是采用二进制存储,数据按位存储减少了对空间的浪费,使得数据序列化后能减少对空间的占用。
TARS协议采用.tars文件定义接口和数据接口,通过提供的工具可以将数据和接口定义翻译成各种语言的代码实现。
接口的共享只需要提供接口的定义文件,使用者通过定义文件直接生成客户端接口代码即可。这样减少了双方的沟通成本,避免了需要写大量的接口定义文档以及解析JSON所需的对象。
三. 解决SpringCloud传统 HTTP服务基于同步线程模型的性能问题
问题:
传统的 HTTP 服务多是基于同步的线程模型,由于 HTTP 协议本身无状态,所以在协议层面就不支持异步,所以当我们在客户端发起一次 HTTP 调用时主调线程必须挂起等待被调响应请求,这个时候主调线程的资源则被浪费了,因为线程资源是有限的,大量线程被挂起等待白白浪费了主调方的运算资源。
相比于使用HTTP协议的常规方案,TARS首先提供的特性就是异步长连接的RPC调用方式:
发起一个异步调用之后,当前线程并不会被阻塞而是继续执行,当收到服务端响应之后在回调线程池中通过回掉函数来执行结果的处理。这样所有的处理线程都一直处于工作的状态中,而不会挂起导致线程资源的浪费。整体上提升了服务的处理能力。
TARS的异步能力主要是通过两个部分的异步来实现的,首先是网络首发包的异步,TARS的网络层实现采用了Reactor模型,通过nio提供的事件IO实现基于事件的异步网络IO。第二是线程模型的异步,我们从线程模型上来看TARS如何是做到异步调用的:
TARS的主要通过上图的过程来完成异步调用,首先主调线程发起异步调用,主调线程将请求内容加入网络线程池的发送队列中,之后该线程继续执行。网络线程池使用Reactor模型实现,通过nio提供的Selecter实现事件IO,所以所有网络线程均是事件驱动的异步IO,当监听到对应连接的写事件后将请求发送,等待监听到读事件后读取响应并交给回调线程处理响应。这样所有的线程都避免了IO阻塞达到了更高的利用效率。
四. 解决SpringCloud服务端基于同步线程模型的稳定性问题
问题:
微服务的服务端基于同步的线程模型面临的最大的隐患就是线程的IO等待,比如说一个基于同步的线程模型的微服务,依赖后面的3个接口,微服务本身的线程数是50个,那么当后面依赖的3个接口中有一个延时飚高,只需要有50个对问题接口的调用,就足够把整个微服务的进程挂起,因为当前已经没有线程能够对外提供服务了(当然可以设置超时,但是设置超时治标不治本)。同时,由于微服务线程对问题接口的IO等待,会导致微服务的队列中堆积大量的等待时间过长(可能已经超时)的请求,当问题接口恢复后,服务端会消耗资源去处理大量的过期的请求(请求超时,客户端不再等待)导致问题进一步恶化,严重的甚至会导致系统雪崩。
TARS提供了纯异步化编程,和服务端过载保护的能力,在服务端保证收到大量的请求也能够保证服务的正常处理效率,其次因为主调方采用长连接和异步调用,避免了大量新建连接和阻塞带来的资源浪费从而提升了服务的整体稳定性。
此外,如果服务端收到过量的请求往往会导致服务端的线程竞争,让服务端的处理能力低于正常的处理水平,在TARS则通过队列来进行过载保护。我们来看TARS的线程模型:
在网络线程收到请求后,TARS会将请求先加入请求队列,工作线程从请求队列中获取请求进行处理,如果短时间内大量请求到达只会被缓存到请求队列中并不会影响工作线程池的处理能力。如果工作线程池从队列中取到请求发现其已经超时则会直接丢弃请求避免处理无效的请求。
通过上面TARS的解决方案,看看实际的使用场景
场景一.同步调用,性能提升一倍标题
通常可以简单可以简单的改造服务,将本来的HTTP接口改为使用TARS,我们对比一下在同步调用场景下TARS调用和HTTP调用的性能差异,这里规定了服务端线程数为100个线程,服务端的处理都为简单的echo服务。我们对比一下在同一台机器上不同RPC方式、不同的客户端线程数以及不同数据包大小的TPS数据:
可以看出,因为采用了连接复用和二进制的协议,整体的调用效率相比使用HTTP有了非常明显的提升,而且是仅仅在简单优化了一下调用方式的情况下,对业务处理逻辑并没有影响。
场景二.异步调用,避免阻塞,提升性能
假如我们在Spring Cloud中存在这样一个调用关系,A服务需要调用B服务,而B服务需要依赖处理耗时远大于是B服务的C服务。比如在通常的业务场景中,如果API接口需要调用一个订单生成的服务,而订单生成服务只需要生成订单ID计算量相对较小,但是他还需要依赖一个订单写入服务,应为涉及到库存修改、订单写入需要一系列的事务处理,整体耗时远远大于订单ID的生成,所以订单服务大量的线程资源浪费在了等待订单写入服务上。在这种情况下可以使用TARS改造订单服务和写入服务,从而使用异步调用写入服务来提升资源利用率,采用TARS提供的异步RPC能力来进行跟深度的改造:
通常情况下如果使用同步调用,因为B需要等待C服务的响应,需要花上自身处理耗时的数倍来进行等待C服务返回结果,线程被阻塞浪费线程资源。这样的情况我们可以保持A服务不变,提供REST接口,而B服务采用异步调用来进行改造。如下图所示:
我们通过简单的代码来模拟上述过程,加入C服务的逻辑时收到请求后Sleep 10s后返回结果,C服务有10个处理线程,最开始B和C之间采用同步调用,要达到最大的并发效率B服务必须也提供10个线程才能够达到最大的并发效率 TPS 为1。此时我们采用异步调用改造B服务:
此时仅需核数 + 1个线程即可达到最大的处理效率。在通常的业务使用中,如果所有IO均用异步实现,那么只使用核数+1个线程便能达到较高的处理效率,从而避免了同步IO带来的资源浪费。
对上述情况进行测试,我们规定C服务默认采用100个线程,服务的处理过程为Sleep 10s,用以模拟一个耗时比较高的资源服务。B服务为一个依赖资源服务C的普通服务,即收到C的结果即返回,在测试中B服务分别采用同步和异步的方式调用C服务,通过调整线程数记录B服务在不同线程数的情况下能提供的最大吞吐:
因为C服务能提供的最大TPS为10,可以看出使用TARS的异步调用因为避免了阻塞,仅使用较少的线程数便可以达到对资源服务C的充分利用,从而避免了对资源的浪费。
在以上改造中,对外的HTTP接口并不需要改动,可以仅在内部需要提升RPC性能和用到异步调用的地方进行改造即可,可以平滑的按服务逐步升级。而且因为均采用Spring boot实现,只需要修改接口,其余所有业务代码还是使用Spring注入即可。
写在最后
我们通过插件实现了TARS对Eureka服务发现的支持,提供了Spring boot starter包和相关的注解,能够通过符合Spring Cloud开发者习惯的开发方式快速开发服务。通过对服务发现和开发方式对Spring Cloud集成能够让开发者以较小的代价快速的在整个Spring Cloud的环境中快速引入TARS的RPC能力。
欢迎访问TARS项目的Github地址: https://github.com/Tencent/Tars
试用最新的TARS on SpringCloud !