微服务|打造企业级微服务开发框架(下)

阅读数:132 2019 年 9 月 29 日 11:51

微服务|打造企业级微服务开发框架(下)

近年来越来越多的企业开始实践微服务,本文分为上下两篇介绍微服务框架 ServiceComb 如何帮助企业应用进行微服务化,实现快速交付,并可靠地运行在云端。今天的下篇介绍 ServiceComb 的通信处理详解。

上篇回顾微服务|打造企业级微服务开发框架(上)

整体介绍

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

微服务|打造企业级微服务开发框架(下)

图 Reactive 模式工作方式

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

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

  • 各种安全加固的组件,只提供了同步工作模式,比如 redis、zookeeper 等等;
  • 一些存量代码工作于同步模式,需要低成本迁移;
  • 开发人员技能不足以控制 Reactive 逻辑。

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

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

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

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

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

微服务|打造企业级微服务开发框架(下)

图 同步模式下的整体线程模型
  • 一个微服务进程中,为 transport 创建了一个独立的 vertx 实例;
  • Eventloop 是 vertx 中的网络、任务线程;
  • 一个 vertx 实例默认的 Eventloop 数为:

2 * Runtime.getRuntime().availableProcessors()

服务消费者端

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

单连接模型

1、最简单的单连接模型

微服务|打造企业级微服务开发框架(下)

图 最简单的单连接模型

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

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

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

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

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

2、优化的单连接模型

微服务|打造企业级微服务开发框架(下)

图 优化的单连接模型

在优化模型中:

  • 每个 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.packageQueueio.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 组数据,可以看到性能有明显提升(不同硬件的测试环境,数据可能差异巨大,不具备比较意义):

微服务|打造企业级微服务开发框架(下)

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

多连接模型

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

微服务|打造企业级微服务开发框架(下)

图 多连接模型
  • 允许配置多个 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()

优化后的性能对比:

微服务|打造企业级微服务开发框架(下)

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

每请求大小为 1KB,可以看到万兆网的带宽接近吃满了,可以充分利用硬件性能。(该测试环境,网卡支持 RSS 特性。)

服务提供者端

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

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

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

1、单线程池 (ThreadPoolExecutor)

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

微服务|打造企业级微服务开发框架(下)

图 单线程池实现方式

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

  • 所有的 Eventloop 向同一个 Blocking Queue 中提交任务;
  • 线程池中所有线程从同一个 Blocking Queue 中抢任务执行;

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

2、多线程池 (ThreadPoolExecutor)

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

微服务|打造企业级微服务开发框架(下)

图 多线程池实现方式
  • Eventloop 线程与线程池建立均衡的绑定关系,降低锁冲突概率;
  • 相当于将线程分组,不同线程从不同 Queue 中抢任务,降低冲突概率。

ServiceComb 默认所有请求使用同一个线程池实例:

复制代码
io.servicecomb.core.executor.FixedThreadExecutor

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

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

3、隔离仓

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

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

微服务|打造企业级微服务开发框架(下)

图 隔离仓

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

4、灵活的线程池策略

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

微服务|打造企业级微服务开发框架(下)

图 ServiceComb 微服务概念模型

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

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

  • 查看配置项 cse.executors.Provider.[schemaId].[operationId] 是否有值;
  • 如果有值,则将值作为 beanId 从 spring 中获取 bean 实例,该实例即是一个 Executor。

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

  • 使用相同的方式,查看配置项 cse.executors.Provider.[schemaId] 是否有值;
  • 使用相同的方式,查看配置项 cse.executors.default 是否有值;
  • 以”cse.executor.groupThreadPool”作为 beanId,获取线程池(系统内置的 FixedThreadExecutor)。

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

  • 实现 java.util.concurrent.Executor 接口
  • 将实现类定义为一个 bean;
  • 在 microservice.yaml 中将线程池与对应的业务进行绑定。

5、线程池模型总结

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

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

写在最后:ServiceComb 除了在华为云微服务引擎商用之外,也于 2017 年 12 月全票通过进入 Apache 孵化器。欢迎感兴趣的读者前往开源社区和我们讨论切磋,希望此文可以给正在进行微服务方案实施的读者们一些启发。

本文转载自公众号华为开发者社区(ID:Huawei_Developer)。

原文链接:

https://mp.weixin.qq.com/s/OCo_1AJb8sV1i9vWdBWvTw

评论

发布