简介: 未来,中国工商银行将持续致力于 Dubbo 的金融级规模化应用。
作者:颜高飞,微服务领域架构师,主要从事服务发现、高性能网络通信等研发工作,擅长 ZooKeeper、Dubbo、RPC 协议等技术方向。
Dubbo是一款轻量级的开源Java服务框架,是众多企业在建设分布式服务架构时的首选。中国工商银行自2014年开始探索分布式架构转型工作,基于开源Dubbo自主研发建设了分布式服务平台。Dubbo框架在提供方消费方数量较小的服务规模下,运行稳定、性能良好。
随着银行业务线上化、多样化、智能化的需求越来越旺盛,在可预见的未来,会出现一个提供方为数千个、甚至上万个消费方提供服务的场景。在如此高负载量下,若服务端程序设计不够良好,网络服务在处理数以万计的客户端连接时、可能会出现效率低下甚至完全瘫痪的情况,即为C10K问题。那么,基于dubbo的分布式服务平台能否应对复杂的C10K场景?为此,我们搭建了大规模连接环境、模拟服务调用进行了一系列探索和验证。
使用dubbo2.5.9(默认netty版本为3.2.5.Final)版本编写服务提供方和对应的服务消费方。提供方服务方法中无实际业务逻辑、仅sleep 100ms;消费方侧配置服务超时时间为5s,每个消费方启动后每分钟调用1次服务。
准备1台8C16G服务器以容器化方式部署一个服务提供方,准备数百台8C16G服务器以容器化方式部署7000个服务消费方。
启动dubbo监控中心,以监控服务调用情况。
|
操作步骤 |
观察内容 |
验证结果 |
场景1 |
先启动服务提供方,后分批启动消费方 |
调用1小时观察交易情况 |
存在零星交易超时失败。消费方分散在多台服务器上。 |
场景2 |
在服务正常调用一段时间后,重启提供方 |
观察提供方重启后的表现 |
在提供方重启后1-2分钟内存在大量交易超时失败,后逐渐恢复。消费方分散在多台服务器上。 |
验证情况不尽如人意,C10K场景下dubbo服务调用存在超时失败的情况。
如果分布式服务调用耗时长,从服务消费方到服务提供方全链路节点都会长时间占用线程池资源,增加了额外的性能损耗。而当服务调用并发突增时,很容易造成全链路节点堵塞,从而影响其他服务的调用,并进一步造成整个服务集群性能下降甚至整体不可用,导致发生雪崩。服务调用超时问题不可忽视。因此,针对该C10K场景下dubbo服务调用超时失败情况我们进行了详细分析。
根据服务调用交易链路,我们首先怀疑交易超时是因为提供方或消费方自身进程卡顿或网络存在延迟导致的。
因此,我们在存在交易失败的提供方、消费方服务器上开启进程gc日志,多次打印进程jstack,并在宿主机进行网络抓包。
提供方、消费方进程gc时长、gc间隔、内存使用情况、线程堆栈等无明显异常,暂时排除gc触发stop the world导致超时、或线程设计不当导致阻塞而超时等猜想。
针对场景1:提供方稳定运行过程中交易超时。
跟踪网络抓包及提供方、消费方交易日志。消费方发起服务调用请求发起后,在提供方端迅速抓到消费方请求报文,但提供方从收到请求报文到开始处理交易耗时2s+。
同时,观察交易请求响应的数据流。提供方业务方法处理完毕后到向消费方发送回包之间也耗时2s+,此后消费方端迅速收到交易返回报文。但此时交易总耗时已超过5s、超过服务调用超时时间,导致抛出超时异常。
由此,判断导致交易超时的原因不在消费方侧,而在提供方侧。
针对场景2:提供方重启后大量交易超时。
服务调用请求发起后,提供方迅速收到消费方的请求报文,但提供方未正常将交易报文递交给应用层,而是回复了RST报文,该笔交易超时失败。
观察在提供方重启后1-2分钟内出现大量的RST报文。通过部署脚本,在提供方重启后每隔10ms打印established状态的连接数,发现提供方重启后连接数未能迅速恢复到7000,而是经过1-2分钟后连接数才恢复至正常数值。而在此过程中,逐台消费方上查询与提供方的连接状态,均为established,怀疑提供方存在单边连接情况。
我们继续分别分析这两种异常场景。
细化收集提供方的运行状态及性能指标:
根据Dubbo框架的心跳机制,当消费方数量较大时,提供方发送心跳报文、需应答的消费方心跳报文将会很密集。因此,怀疑是心跳密集导致netty线程忙碌,从而影响交易请求的处理,继而导致交易耗时增加。
进一步分析netty worker线程的运行机制,记录每个netty worker线程在处理连接请求、处理写队列、处理selectKeys这三个关键环节的处理耗时。观察到每间隔60s左右(与心跳间隔一致)处理读取数据包较多、耗时较大,期间存在交易耗时增加的情况。同一时间观察网络抓包,提供方收到较多的心跳报文。
因此,确认以上怀疑。心跳密集导致netty worker线程忙碌,从而导致交易耗时增长。
TCP建立连接三次握手的过程中,若全连接队列满,将导致单边连接。
全连接队列大小由系统参数net.core.somaxconn及listen(somaxconn,backlog)的backlog取最小值决定。somaxconn是Linux内核的参数,默认值是128;backlog在创建Socket时设置,dubbo2.5.9中默认backlog值是50。因此,生产环境全连接队列是50。通过ss命令(Socket Statistics)也查得全连接队列大小为50。
观察TCP连接队列情况,证实存在全连接队列溢出的现象。
即:全连接队列容量不足导致大量单边连接产生。因在本验证场景下,订阅提供方的消费方数量过多,当提供方重启后,注册中心向消费方推送提供方上线通知,所有消费方几乎同时与提供方重建连接,导致全连接队列溢出。
单边连接影响范围多为消费方首笔交易,偶发为首笔开始连续失败2-3笔。
建立为单边的连接下,交易非必然失败。三次握手全连接队列满后,若半连接队列空闲,提供方创建定时器向消费方重传syn+ack,重传默认5次,重传间隔以倍数增长,1s..2s..4s..共31s。在重传次数内,若全连接队列恢复空闲,消费方应答ack、连接建立成功。此时交易成功。
在重传次数内,若全连接队列仍然忙碌,新交易到达超时时间后失败。
到达重传次数后,连接被丢弃。此后消费方发送请求,提供方应答RST。后交易到达超时时间失败。
根据Dubbo的服务调用模型,提供方发送RST后,消费方抛出异常Connection reset by peer,后断开与提供方的连接。而消费方无法收到当前交易的响应报文、导致超时异常。同时,消费方定时器每2s检测与提供方连接,若连接异常,发起重连,连接恢复。此后交易正常。
总结以上造成交易超时的原因有两个:
针对以上场景1:如何能降低单个netty worker线程处理心跳的时间,加速IO线程的运行效率?初步设想了如下几种方案:
针对以上场景2:如何规避首笔大量半连接导致的交易失败?设想了如下方案:
基于以上设想,我们从系统层面、dubbo框架层面进行了大量的优化,以提升C10K场景下交易处理效率,提升服务调用的性能容量。
优化内容包括以下方面:
具体涉及优化的框架层如下:
经对各优化内容逐项验证,各措施均有不同程度的提升,效果分别如下:
优化内容 |
优化效果 |
tcp全连接队列扩容 |
提供方重启后交易超时失败现象消除 |
epoll模型调整 |
提供方重启后全连接队列溢出次数明显降低,连接accept速度有所提升 |
心跳绕过序列化 |
提供方在心跳周期无CPU毛刺,CPU峰值降低20% 消费方与提供方之间平均处理时差由27ms降低至3ms 前99%的交易耗时从191ms下降至133ms |
增加Iothreads线程数 |
将默认的iothreads线程数9调整为20后,消费方与提供方之间平均处理时差由27ms降低至14ms 前99%的交易耗时从191ms下降至186ms |
提供方心跳打散 |
从提供方网络抓包分析,心跳数据包的毛刺峰值从1.5万/秒压降至3000/秒 |
消费方心跳打散 |
从提供方网络抓包分析,心跳数据包几乎不再有毛刺峰 |
综合运用以上优化效果最佳。在此1个提供方连接7000个消费方的验证场景下,重启提供方后、长时间运行无交易超时场景。对比优化前后,提供方CPU峰值下降30%,消费方与提供方之间处理时差控制在1ms以内,P99交易耗时从191ms下降至125ms。在提升交易成功率的同时,有效减少了消费方等待时间、降低了服务运行资源占用、提升了系统稳定性。
基于以上验证结果,中国工商银行在分布式服务平台中集成了以上优化内容。截至发文日期,线上已存在应用一个提供方上连接上万个消费方的场景。落地该优化版本后,在提供方版本升级、及长时间运行下均无异常交易超时情况,实际运行效果符合预期。
中国工商银行深度参与Dubbo社区建设,在Dubbo金融级规模化运用的过程中遇到了诸多技术挑战,为满足金融级高敏交易的苛刻运行要求,开展了大规模自主研发,并通过对Dubbo框架的扩展和定制持续提升服务体系的稳定性,以“源于开源、回馈开源”的理念将通用增强能力不断贡献至开源社区。未来,我们将持续致力于Dubbo的金融级规模化应用,协同社区继续提升Dubbo的性能容量和高可用水平,加速金融行业数字化创新和转型及基础核心关键的全面自主可控。
原文链接
本文为阿里云原创内容,未经允许不得转载。