写点什么

干货系列:SOFARPC 性能优化实践(上)

  • 2019-08-27
  • 本文字数:5084 字

    阅读完需:约 17 分钟

干货系列:SOFARPC 性能优化实践(上)

< SOFA:Channel/ >,有趣实用的分布式架构频道。

本次主要分享 SOFARPC 在性能上做的一些优化,这个系列会分成上下两部分进行分享,今天是 SOFARPC 性能优化(上),也会对本次分享中的一些结论,提供部分代码 Demo,供大家了解验证。


大家好!


我是来自蚂蚁金服中间件的雷志远,花名碧远,目前在负责 SOFARPC 框架相关工作。


去年的时候,我们和外部的爱好者们一起,做了一个基于 SOFARPC 的源码解析系列,我同事已经发到群里了,大家可以保存,直播之后查看。


今年,基于源码解析的基础,我们来多讲讲实践,如何应用到大家的业务,来帮助大家解决实际问题。在直播过程中有相关的问题想提问,可以在钉钉群互动。

前言

在上一期中,余淮分享了《从蚂蚁金服微服务实践谈起》。介绍了蚂蚁微服务的起源,以及之后服务化,单元化的情况。同时介绍了 SOFAStack 目前开源的情况。最后也分享了一下整个微服务中 SOFARPC 的设计与实现。


本期,我们主要分享 SOFARPC 在性能上做的一些优化。这个系列会分成上下两部分进行分享,今天是 SOFARPC 性能优化(上),也会对本次分享中的一些结论,提供部分代码 Demo,供大家了解验证。


我们先简要介绍一下 SOFARPC 的框架分层。这个在上次的分享中已经进行了介绍。



下层是网络传输层,依次是协议,序列化,服务发现和 Filter 等。


Transport 主要负责数据传输,可以是 Http2Transport,也可以是 BoltTransport,还有可能是其他。


Protocol 层是协议,是 Rest 还是 Bolt ,或者是 Dubbo 。


Serialization 是序列化,对于每种协议,可以是用不同的序列化方式,比如 hessian,pb,json 等。


Filter 是通用的过滤器层,主要是为了留出一些扩展,完成一些其他扩展功能,比如 Tracer 的埋点等。


Router 是路由层,主要是做寻址,这里可能是 Zk,也可能是 LVS,也可能是直连。


Cluster 是客户端集群方式的表示。

自定义通讯协议使用

首先我想介绍一下自定义通讯协议。


在说明自定义通讯协议之前,我先简单介绍一下通讯协议。在 TCP 之上,RPC 框架通常还需要将请求和响应数据进行一定的封装,组装成 Packet,然后发送出去。这样,服务端收到之后,才能正确识别整个 TCP 发过来的字节流中,哪一部分是我们可以进行处理的一个完整单位。反之,客户端收到服务端的 TCP 数据流也是如此。


有了上面的共识之后,我们要回答下面两个问题:


1.为什么要自定义,不使用 Http2/Dubbo/Rest/Grpc?

2.自定义之后,带来了什么好处呢?


Http2 虽然更为通用,但是一方面,出现较晚,迁移转换成本高,并且通用则意味着传输的辅助数据会变多,会有一些额外的信息需要传递或者判断。对于序列化反序列化的控制上,也不是很好扩展操作。


而 Dubbo,协议简单强大。但是一些元信息需要解析,Header 中传输的数据太少,很多都需要依赖 body 中的数据反序列化完成后才能使用,头部的信息太少。



而使用了自研的协议之后,Header 中可自定义传输更多的元信息,序列化方式,Server Fail Fast,服务端线程隔离等也都成为可能。甚至蚂蚁在 ServiceMesh 的场景下,Mesh 本身也能利用 Bolt 的协议,进行部分数据的读取,而不依赖具体的序列化实现。



BOLT 协议图


经过我们的实践,大致来看,目前给我们带来的好处主要有以下的能力:


1.Server Fast 的支持


2.Header 和 Body 的分开序列化


3.Crc 校验的支持


4.版本的支持,预防未来可能出现的更好的设计方案


5.多种序列化方式的支持


6.安全认证,Mesh 路由


如果你要自己设计一个通讯协议。可以考虑使用 BOLT 协议,或者参考进行更好的设计和优化。


关于 SOFABolt 相关的源码解析,也可以通过这个系列来了解。


https://www.sofastack.tech/posts

Netty 性能参数优化

在介绍了自定义通讯协议之后,也就是确定好了怎么封包解包之后,还需要确定传输层的开发。一个 RPC 框架从现在的情况来看,一般不太可能完全基于 JAVA 的 NIO 或者其他 IO 进行直接的开发,主要是一些 NIO 原生的问题和使用难度,而成熟的,目前可选的不多。基本上,大家都会基于 Netty 进行开发,HSF/Dubbo/Motan 等都是这样。


直接使用是比较简单的。在 Netty 的 Bootstrap 的设置中,有一些可选的优化项,有必要跟大家分享一下。

1、SO_REUSEPORT/SO_REUSEADDR - 端口复用(允许多个 socket 监听同一个 IP+端口)

SO_REUSEPORT 支持多个进程或者线程绑定到同一端口,提高服务器的接收链接的并发能力,由内核层面实现对端口数据的分发的负载均衡,在服务器 socket 上没有了锁的竞争。


同时 SO_REUSEADDR 也要打开,这样针对 time-wait 链接 ,可以确保 server 重启成功。在一些服务端启动很快的情况下,可以防止启动失败。

2、TCP_FASTOPEN - 3 次握手时也用来交换数据

三次握手的过程中,当用户首次访问服务端时,发送 syn 包,server 根据客户端 IP 生成 cookie ,并与 syn+ack 一同发回客户端;客户端再次访问服务端时,在 syn 包携带 TCP cookie;如果服务端校验合法,则在用户回复 ack 前就可以直接发送数据;否则按照正常三次握手进行。也就是说,如果客户端中途断开,再建联的时候,会同时发送数据,会有一定的性能提升。


TFO 提高性能的关键是省去了热请求的三次握手,这在小对象传输较多的移动应用场景中,能够极大提升性能。


Netty 中仅在 Epoll 的时候可用 Linux 特性,不能在 Mac/Windows 上使用,SOFARPC 未开启。

3、TCP_NODELAY-关闭 (纳格) Nagle 算法,再小的包也发送,而不是等待

TCP/IP 协议中针对 TCP 默认开启了 Nagle 算法。Nagle 算法通过减少需要传输的数据包个数,来优化网络。但是现在的环境下,网络带宽足够,需要进行关闭。这样,对于传输数据量小的场景,能很好的提高性能,不至于出现数据包等待。

4、SO_KEEPALIVE –开启 TCP 层面的 Keep Alive 能力

这个不多说,开启一下 TCP 层面的 Keep Alive 的能力。

5、WRITE_BUFFER_WATER_MARK 设置

通过 WRITE_BUFFER_WATER_MARK 设置某个连接上可以暂存的最大最小 Buffer 之后,如果该连接的等待发送的数据量大于设置的值时,则 isWritable 会返回不可写。这样,客户端可以不再发送,防止这个量不断的积压,最终可能让客户端挂掉。如果发生这种情况,一般是服务端处理缓慢导致。这个值可以有效的保护客户端。此时数据并没有发送出去。

6、workerGroup

worker 线程数设置 处理器+1,Netty 默认是线程数*2,可以根据自己的压测情况来判断。Boss Group 用于服务端处理建立连接的请求,WorkGroup 用于处理 I/O。为了避免线程上下文切换,只要能满足要求,这个值一般越少越好。

7、ioRadio 设置

EventLoop#ioRatio 的设置(默认 50), 这是 EventLoop 执行 IO 任务和非 IO 任务的一个时间比例上的控制,BOLT 最佳实践是 70,表示 70%的时间在执行 IO 任务。

8、SO_BACKLOG 设置

在 Linux 系统内核中维护了两个队列:syns queue 和 accept queue。第一个是半连接队列,保存收到客户端 syn 之后,进入 syn_recv 状态的这些连接,默认 netty 中是 128,io.netty.util.NetUtil#SOMAXCONN ,然后读取/proc/sys/net/core/somaxconn 来继续确定,之后还有一些系统级别的覆盖逻辑。


在一些场景下,如果客户端远远多余服务端,并发建联,可能不够。这个值也不能太大,否则会无法防止 SYN-Flood 攻击。Bolt 中目前这个值修改成了 1024。通过设置之后,由于自己设置的和系统的取小,所以自己设置的值相当于设置了上限。如果 Linux 系统运维某些设置错误,也能通过代码层面进行避免。


目前我们的 Linux 层面,通常设置的是 128,最终经过计算会设置为 128。

SOFARPC 连接保持

Netty 设置基本 ok,协议也确定之后,连接的保持就比较重要,否则,第一次发送或者每次发送都要走一次建联的过程。虽然有 FAST OPEN 的加持,还是有一些损失。


说到这里, 可能有些同学有疑问:


1.Keep Alive 不够吗?

2.Bolt 的连接管理怎么做的?

3.如何解决初次建联的问题?

4.心跳是单向还是双向?


前面我们说过了,Keep Alive 已经打开了。不过,Keep Alive 还不够,主要是经过很多网络设备之后,Keep Alive 可能失效,另外 Keep Alive 是一个 Linux 层面的设置,有时候整个系统并未打开。这些不可控的因素都会导致我们的连接管理失效。



Keep Alive 图


上面是 Keep Alive 的处理,主要是在没有读写事件一段时间后,进行数据包的发送来保活。


因为我们需要更通用的连接保持方案。连接管理核心的基于 Netty 的 Idle 事件来做。BOLT 的设置为单向心跳,客户端发,服务端收,减少心跳数据在网络上的传输量。有些 RPC 框架会使用双向心跳,同时,BOLT 在连接管理上,也允许一个地址,建立多个连接,这样可以在发送时,最大限度的利用网卡。默认为 1,连接数在满足传输吞吐量的情况下越少越好。


但是这里要注意,如果你的场景是有大量的服务端,那么这个数据不建议进行扩大。因为 tcp 连接会成倍增长,反而带来性能下降。目前蚂蚁这边大部分也多为 1。



RPC 连接管理


在 BOLT 连接管理的基础上,RPC 为了避免第一次用户请求,进行建联并发送的延迟,RPC 还有一个连接管理的线程,会异步的进行连接初始化。这样,当真正的请求发起的时候,连接已经准备好了,可以减少一次建联的耗时对业务的影响。


对于 LVS 和 VIP 的场景下,由于长连接的特性,即使后端有 100 个 IP,对客户端来说,也只能和一个 IP 进行通信,因为这些设备是建联层面的,并非通信层面的。所以对这种情况。,一个 RPC 框架也要考虑支持定时断链和重连。

序列化选择

以上都准备好了之后,序列化方式的选择决定了业务传输对象能够有多小,也决定了在传输之前,序列化和反序列化的时候能有多快或者有多占用 CPU 。



序列化图


蚂蚁这边长期使用 hessian 作为序列化方式,在出现跨语言需求后,同时支持 pb 。如果你还有考虑其他的序列化方式,可以参考附录中的序列化框架性能测试套件来进行选择。


需要注意的是,在 RPC 场景的序列化中,一定要考虑接口变更,字段新增的兼容性。因为一旦一个接口被客户 A 和 B 引用,此时 C 要升级 facade 接口,能否兼容 A 和 B 的情况就很重要。


基于我们自己的情况,在序列化方式的选择上:


1.如果很长时间内,不存在跨语言的情况,hessian 是兼容性和性能的综合考虑


2.如果考虑跨语言,并且对性能要求很高,Pb 可作为跨语言的情况下的选择。


3.在选型时也要考虑序列化框架的社区情况。切勿选择看上去性能高,但是已经不再维护的库,或者用户量非常少的库,一旦出现问题,比较难解决。

IO 线程池批量解包


批量解包图


Netty 提供了一个方便的解码工具类 ByteToMessageDecoder ,如图上半部分所示,这个类具备 accumulate 批量解包能力,可以尽可能的从 socket 里读取字节,然后同步调用 decode 方法,解码出业务对象,并组成一个 List 。最后再循环遍历该 List ,依次提交到 ChannelPipeline 进行处理。改动后,如图下半部分所示,即将提交的内容从单个 command ,改为整个 List 一起提交,如此能减少 pipeline 的执行次数,同时提升吞吐量。这个模式在低并发场景下不明显,但是在高并发场景下对吞吐量有不小的性能提升。


这一段是我改成开关方式的,方便大家理解改动点。


if (batchSwitch) {    ArrayList<Object> ret = new ArrayList<Object>(size);    for (int i = 0; i < size; i++) {        ret.add(out.get(i));    }    ctx.fireChannelRead(ret);}else{    for (int i = 0; i < size; i++) {        ctx.fireChannelRead(out.get(i));    }}
复制代码


我们的 DEMO 提供了一个验证的方式,如果有相关的压测环境,可以参考进行多并发的验证。


DEMO 链接:


https://github.com/leizhiyuan/rpcchannel

客户端 Proxy 的性能优化

作为一个 RPC 框架,最后,我们还有给用户的接口生成代理。目前一般大家都是要用动态代理来做。动态代理的性能有不同,使用上也有一定的差别。各个版本之间,也会有一定的差异。在选择上,需要大家根据实际情况,进行测试验证。


我们自己的测试数据显示 Javassist Bytecode 的方式是除了 Asm 之外,性能最好的。Asm 由于使用写法非常反人类,所以我们目前还是使用的 Javassist Bytecode 的方式。



可优先选择 javassist bytecode,有一定的性能优势,性能测试可以根据自己的情况,使用 JMH 进行测试。测试代码和版本在 DEMO 中提供。

总结

得益于 Java 社区的发展以及前辈们的贡献,目前写一个 RPC 框架并不是很难。但是作为一个 RPC 框架,需要在可维护性的基础上,尽可能提高自身性能,将在实际过程中遇到的一些场景和异常情况进行修复和优化,并进行更好的代码设计和实现。对于性能上的数据,可以多使用 JMH 并结合实际业务场景,进行相应的测试。


本文转载自公众号蚂蚁金服科技(ID:Ant-Techfin)。


原文链接:


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


2019-08-27 17:301805

评论

发布
暂无评论
发现更多内容

macos轻量级思维导图软件 ClickCharts最新激活版

mac大玩家j

思维导图 Mac软件 思维导图软件

张勇卸任阿里云董事长与CEO,该职务将由集团CEO吴泳铭兼任

B Impact

持续部署:提高敏捷加速软件交付(内含教程)

SEAL安全

ci 持续部署 CD 软件交付 企业号9月PK榜

9月23-24日·上海线下·CSM认证周末班【提前报名特惠】“全球金牌课程”CST导师亲授

ShineScrum捷行

DPText-DETR: 基于动态点query的场景文本检测,更高更快更鲁棒 | 京东探索研究院

京东科技开发者

京东云 企业号9月PK榜

第四周ARTS打卡

犇犇

ARTS 打卡计划

Rich Bowen: 无论你在创造什么,最终交付的是信任。

亚马逊云科技 (Amazon Web Services)

开源 亚马逊云科技

ARTS 打卡第四周

请务必优秀

文盘Rust -- 给程序加个日志 | 京东云技术团队

京东科技开发者

京东云 企业号9月PK榜

数据库深分页介绍及优化方案 | 京东云技术团队

京东科技开发者

京东云 企业号9月PK榜

ARTS 打卡第 4 周

atom

蓝易云:提升网站性能:Nginx五种高效负载均衡策略详解!

百度搜索:蓝易云

nginx 云计算 Linux 运维 云服务器

解锁社交媒体的未来:SocialFi 的承诺

区块链软件开发推广运营

交易所开发 数字藏品开发 合约交易所开发 NFT开发 区块链开发DAPP开发

【Y 码力】WAL 与性能

YMatrix 超融合数据库

性能提升 WAL 超融合数据库 故障恢复 YMatrix

采用Excel作为可视化设计器的开源规则引擎 NopRule

canonical

低代码 规则引擎 可视化开发 可逆计算 Nop平台

手机+卫星的科技狂想

脑极体

手机 卫星

破局DevOps|8大北极星指标指引研发效能方向

laofo

DevOps 研发效能 研发度量 效能度量 研发效能度量

数据驱动决策:以经验与洞察引领工作智慧

叶小鍵

当红语言模型利器:深度解析向量数据库技术及其应用

Baihai IDP

人工智能 AI 向量数据库 白海科技 大语言模型

数据通信网络之OSPFv3基础

timerring

数据通信网络

CnosDB 签约京清能源,助力分布式 光伏发电解决监测系统难题

CnosDB

开源 时序数据库 CnosDB 京清能源

元宇宙优势与劣势、机遇与挑战分析-元宇宙解决方案

3DCAT实时渲染

元宇宙解决方案

蓝易云:Tomcat 部署及优化详细教程!

百度搜索:蓝易云

Java tomcat Linux Web

谷沁清益生菌清口含片,守护口腔健康的第一道防线

联营汇聚

Spring 中 Bean 的生命周期

小万哥

Java spring 程序员 springboot SpringCloud

mac电脑数据转换 EasyDataTransform激活最新版

胖墩儿不胖y

数据转换 Mac软件 数据处理软件

区块链项目:白皮书+PPT海报设计,热度视频/MG动画,出海包装/宣发,经济模型设计

区块链软件开发推广运营

数字藏品开发 dapp开发 区块链开发 链游开发 NFT开发

探索GreatADM:如何快速定义监控

GreatSQL

ARTS 打卡第 4 周

Geek_wu

ARTS 打卡计划 左耳朵耗子

干货系列:SOFARPC 性能优化实践(上)_语言 & 开发_碧远_InfoQ精选文章