微服务高速通信设计

开卷有益,点击蓝字关注哟

企业应用微服务化的思考

近年来越来越多的企业开始实践微服务,而微服务在企业应用落地的过程,面临着微服务开发框架的选型,无论是自研还是选择第三方框架都不得不考虑的问题包括:

微服务框架是否具备高可靠性,任何时间不能中断业务;

微服务框架是否能够实现高速通信性能,保证业务从单体架构向微服务架构切换时,能满足微服务业务的诉求。

本文从服务管理中心、通信处理两个模块来介绍华为开源微服务框架SeviceComb如何帮助企业应用快速具备高性能的通信能力以及高可靠的服务管理能力。

本篇将详细介绍ServiceComb的通信处理。

整体介绍

ServiceComb的底层通信框架依赖Vert.x.vertx标准工作模式为高性能的reactive模式,其工作方式如下图所示:

图1 reactive模式工作方式

业务逻辑直接在eventloop中执行,整个业务流程中没有线程切换,所有的等待逻辑都是异步的,只要有任务,则不会让线程停下来,充分、有效地利用系统资源。

vertx生态中包含了业界常用各种组件的reactive封装,包括jdbc、zookeeper、各种mq等等。但是reactive模式对业务的要求相当高,业务主流程中不允许有任何的阻塞行为。因此,为了简化上层业务逻辑,方便开发人员的使用,在Vertx之上提供同步模式的开发接口还是必不可少的,例如:

  • 各种安全加固的组件,只提供了同步工作模式,比如redis、zookeeper等等

  • 一些存量代码工作于同步模式,需要低成本迁移

  • 开发人员技能不足以控制reactive逻辑

所以ServiceComb底层基于vertx,但在vertx之上进行了进一步封装,同时支持reactive及同步模式。

工作于Reactive模式时,利用Vertx原生的能力,不必做什么额外的优化,仅需要注意不要在业务代码中阻塞整个进程。

而同步模式则会遭遇各种并发性能问题。,本文描述同步模式下的各种问题以及解决方案。

RESTful流程中,连接由vertx管理,当前没有特别的优化,所以本文中,连接都是指highway流程中的tcp连接。

同步模式下的整体线程模型

图2 同步模式下的整体线程模型

  • 一个微服务进程中,为transport创建了一个独立的vertx实例

  • Eventloop是vertx中的网络、任务线程

  • 一个vertx实例默认的Eventloop数为:2 * Runtime.getRuntime().availableProcessors()

服务消费者端

在服务消费者端,主要需要处理的问题是如何更加高效地把请求推送到服务提供者上去,然后拿到服务提供者的返回信息。所以在这一端我们主要关注“ 如何更高效的发送数据 ”这个话题。

1

单连接模型最简单的单连接模型

图3  最简单的单连接模型

从模型图中,我们可以看到,所有的consumer线程,如果向同一个目标发送数据,必然产生资源竞争,此时实际的处理如下:

Connection.send内部直接调用Vertx的socket.write(buf),是必然加锁互斥的。

这必然导致大量并发时,大多数consumer线程都无法及时地发送自己的数据。

Socket.write内部会调用netty的channel.write,此时会判断出执行线程不是eventloop线程,所以会创建出一个任务并加入到eventloop任务队列中,如果eventloop线程当前在睡眠态,则立即唤醒eventloop线程,异步执行任务。

这导致频繁的任务下发及线程唤醒,无谓地增加cpu占用,降低性能。

优化的单连接模型

图4 优化的单连接模型

在优化模型中:

  • 每个TcpClientConnection额外配备一个CAS消息队列

  • Connection.send不再直接调用vertx的write方法,而是:

    • 所有消息保存到CAS队列中,减少入队竞争

    • 通过原子变量判定,只有入队前CAS队列为空,才向eventloop下发write任务,唤醒eventloop线程

    • 在eventloop中处理write任务时,将多个请求数据包装为composite buffer,批量发送,减少进入os内核的次数,提高tcp发送效率。

代码参见:

https://github.com/ServiceComb/ServiceComb-Java-Chassis/blob/master/foundations/foundation-vertx/src/main/java/io/servicecomb/foundation/vertx/client/tcp/TcpClientConnection.java

io.servicecomb.foundation.vertx.client.tcp.TcpClientConnection.packageQueue io.servicecomb.foundation.vertx.client.tcp.TcpClientConnection.send(AbstractTcpClientPackage,long, TcpResponseCallback)

https://github.com/ServiceComb/ServiceComb-Java-Chassis/blob/master/foundations/foundation-vertx/src/main/java/io/servicecomb/foundation/vertx/tcp/TcpConnection.java

io.servicecomb.foundation.vertx.tcp.TcpConnection.write(ByteBuf) io.servicecomb.foundation.vertx.tcp.TcpConnection.writeInContext()

进行此项优化后,在同一环境下测试2组数据,可以看到性能有明显提升(不同硬件的测试环境,数据可能差异巨大,不具备比较意义):

表1 单连接模型优化前后性能对比

TPS

Latency

(ms)

CPU

TPS

提升比例

Consumer

Producer

(新-旧)/旧

优化前

81986

1.22

290%

290%

77.31%

优化后

145369

0.688

270%

270%

2

多连接模型

在单连接场景下进行相应的优化后,我们发现其实还有更多的优化空间。因为在大多数场景中,实际机器配置足够高,比如多核、万兆网络连接、网卡支持RSS特性等。此时,需要允许一对consumer与producer之间建立多条连接来充分发挥硬件的性能。

图5  多连接模型

允许配置多个eventloop线程

在microservice.yaml中进行以下配置:

cse:   highway:     client:       thread-count: 线程数       server:       thread-count: 线程数

Consumer线程与eventloop线程建立均衡的绑定关系,进一步降低consumer线程的竞争概率。

代码参见:

https://github.com/ServiceComb/ServiceComb-Java-Chassis/blob/master/foundations/foundation-vertx/src/main/java/io/servicecomb/foundation/vertx/client/ClientPoolManager.java

io.servicecomb.foundation.vertx.client.ClientPoolManager.findThreadBindClientPool()

优化后的性能对比:

表2 多连接下线程模型优化前后性能对比

TPS

Latency

(ms)

CPU

TPS

提升比例

Consumer

Producer

(新-旧)/旧

简单单连接*10

543442

0.919

2305%

1766%

72.81%

优化后

939117

0.532

1960%

1758%

每请求大小为1KB,可以看到万兆网的带宽接近吃满了,可以充分利用硬件性能。

(该测试环境,网卡支持RSS特性。)

服务提供者端

不同于服务消费者,服务提供者主要的工作模式就是等待消费者的请求,然后处理后返回应答的信息。所以在这一端,我们更加关注“如何高效的接收和处理数据”这件事情。

同步模式下,业务逻辑和IO逻辑分开,且根据“隔离仓”原则,为了保证整个系统更加稳定和高效地运行,业务逻辑本身也需要在不同隔离的区域内运行。而这些区域,就是线程池。所以构建服务提供者,就需要对线程池进行精细的管理。

下面是针对线程池的各种管理方式。

1

单线程池(ThreadPoolExecutor)

下图为将业务逻辑用单独的线程池实现的方式。在这种方式下,IO仍然采用异步模式,所有接到的请求放入队列中等待处理。在同一个线程池内的线程消费这个队列并进行业务处理。

图6  单线程池实现方式

在这种方式下,有以下瓶颈点:

  • 所有的eventloop向同一个BlockingQueue中提交任务

  • 线程池中所有线程从同一个BlockingQueue中抢任务执行

ServiceComb默认不使用这种线程池。

2

多线程池(ThreadPoolExecutor)

为规避线程池中Queue带来的瓶颈点,我们可以使用一个Executor将多个真正的Executor包起来。

图7  多线程池实现方式

  • Eventloop线程与线程池建立均衡的绑定关系,降低锁冲突概率

  • 相当于将线程分组,不同线程从不同Queue中抢任务,降低冲突概率

ServiceComb默认所有请求使用同一个线程池实例:io.servicecomb.core.executor.FixedThreadExecutor

FixedThreadExecutor内部默认创建2个真正的线程池,每个池中有CPU数目的线程,可以通过配置修改默认值:

servicecomb:   executor:     default:       group: 内部真正线程池的数目       thread-per-group: 每个线程池中的线程数

代码参见:

https://github.com/ServiceComb/ServiceComb-Java-Chassis/blob/master/core/src/main/java/io/servicecomb/core/executor/FixedThreadExecutor.java

3

隔离仓

业务接口的处理速度有快有慢,如果所有的请求统一在同一个Executor中进行处理,则可能每个线程都在处理慢速请求,导致其他请求在Queue中排队。

此时,可以根据业务特征,事先做好规划,将不同的业务处理按照一定的方式进行分组,每个组用不同的线程池,以达到隔离的目的。

图8  隔离仓

隔离仓的实现依托于ServiceComb灵活的线程池策略,具体在下一节进行描述。

4

灵活的线程池策略

ServiceComb微服务的概念模型如下:

图9  ServiceComb微服务概念模型

可以针对这3个层次进行线程池的配置,operation与线程池之间的对应关系,在启动阶段即完成绑定。

operation与线程池之间的绑定按以下逻辑进行:

  1. 查看配置项cse.executors.Provider.[schemaId].[operationId]是否有值:

    1. 如果有值,则将值作为beanId从spring中获取bean实例,该实例即是一个Executor

    2. 如果没有值,则继续尝试下一步

  2. 使用相同的方式,查看配置项cse.executors.Provider.[schemaId]是否有值

  3. 使用相同的方式,查看配置项cse.executors.default是否有值

  4. 以”cse.executor.groupThreadPool”作为beanId,获取线程池(系统内置的FixedThreadExecutor)

代码参见:

https://github.com/ServiceComb/ServiceComb-Java-Chassis/blob/master/core/src/main/java/io/servicecomb/core/executor/ExecutorManager.java

按以上策略,用户如果需要创建自定义的线程池,需要按以下步骤执行:

  1. 实现java.util.concurrent.Executor接口

  2. 将实现类定义为一个bean

  3. 在microservice.yaml中将线程池与对应的业务进行绑定

5

线程池模型总结

如上一节所述,在默认多线程池的基础上,CSE提供了更为灵活的线程池配置。“隔离仓”模式的核心价值是实现不同业务之间的相互隔离,从而让一个业务的故障不要影响其他业务。这一点在CSE中可以通过对线程池的配置实现。例如,可以为不同的operation配置各自独立的线程池。

另外,灵活性也带来了一定的危险性。要避免将线程池配置为前面提到的“单业务线程池”模式,从而为整个系统引入瓶颈点。

Apache ServiceComb自开源以来,秉持将“复杂扔给自己,将极简留给用户”的原则,诸多技术创新被证明对于微服务化行之有效,并先后获得云计算开源大会《2018 OSCAR 尖峰开源技术奖》和云计算标准和应用大会《中国优秀开源项目一等奖》,这些都离不开社区开源开发者们的共同努力,我们在此也非常欢迎各位感兴趣的读者前往开源社区和我们讨论切磋,甚至加入我们,希望此文可以给正在进行微服务方案实施的读者们一些启发。

往期精彩大放送

长按添加小助手,加入我们

点击左下角“阅读原文”,给SeriveComb加个Star吧

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章