你好呀,我是歪歪。
是这样的,我最近又看到了这篇文章《工商银行分布式服务 C10K 场景解决方案 》。
为什么是又呢?
因为这篇文章最开始发布的时候我就看过了,当时就觉得写得挺好的,宇宙行(工商银行)果然是很叼的样子。
但是看过了也就看过了,当时没去细琢磨。
这次看到的时候,刚好是在下班路上,就仔仔细细的又看了一遍。
嗯,常读常新,还是很有收获的。
所以写篇文章,给大家汇报一下我再次阅读之后的一下收获。
文章提要
我知道很多同学应该都没有看过这篇文章,所以我先放个链接,《工商银行分布式服务 C10K 场景解决方案 》。
先给大家提炼一下文章的内容,但是如果你有时间的话,也可以先去细细的读一下这篇文章,感受一下宇宙行的实力。
文章内容大概是这样的。
在宇宙行的架构中,随着业务的发展,在可预见的未来,会出现一个提供方为数千个、甚至上万个消费方提供服务的场景。
在如此高负载量下,若服务端程序设计不够良好,网络服务在处理数以万计的客户端连接时、可能会出现效率低下甚至完全瘫痪的情况,即为 C10K 问题。
C10K 问题就不展开讲了,网上查一下,非常著名的程序相关问题,只不过该问题已经成为历史了。
而宇宙行的 RPC 框架使用的是 Dubbo,所以他们那篇文章就是基于这个问题去展开的:
基于 Dubbo 的分布式服务平台能否应对复杂的 C10K 场景?
为此,他们搭建了大规模连接环境、模拟服务调用进行了一系列探索和验证。
首先他们使用的 Dubbo 版本是 2.5.9。版本确实有点低,但是银行嘛,懂的都懂,架构升级是能不动就不动,稳当运行才是王道。
在这个版本里面,他们搞了一个服务端,服务端的逻辑就是 sleep 100ms,模拟业务调用,部署在一台 8C16G 的服务器上。
对应的消费方配置服务超时时间为 5s,然后把消费方部署在数百台 8C16G 的服务器上(我滴个乖乖,数百台 8C16G 的服务器,这都是白花花的银子啊,有钱真好),以容器化方式部署 7000 个服务消费方。
每个消费方启动后每分钟调用 1 次服务。
然后他们定制了两个测试的场景:
场景 2 先暂时不说,异常是必然的,因为只有一个提供方嘛,重启期间消费方还在发请求,这必然是要凉的。
但是场景 1 按理来说不应该的啊。
你想,消费方配置的超时时间是 5s,而提供方业务逻辑只处理 100ms。再怎么说时间也是够够的了。
需要额外多说一句的是:本文也只聚焦于场景 1。
但是,朋友们,但是啊。
虽然调用方一分钟发一次请求的频率不高,但是架不住调用方有 7000 个啊,这 7000 个调用方,这就是传说中的突发流量,只是这个“突发”是每分钟一次。
所以,偶现超时也是可以理解的,毕竟服务端处理能力有限,有任务在队列里面稍微等等就超时了。
可以写个小例子示意一下,是这样的:
就是搞个线程池,线程数是 200。然后提交 7000 个任务,每个任务耗时 100ms,用 CountDownLatch 模拟了一下并发,在我的 12 核的机器上运行耗时 3.8s 的样子。
也就是说如果在 Dubbo 的场景下,每一个请求再加上一点点网络传输的时间,一点点框架内部的消耗,这一点点时间再乘以 7000,最后被执行的任务理论上来说,是有可能超过 5s 的。
所以偶现超时是可以理解的。
但是,朋友们,又来但是了啊。
我前面都说的是理论上,然而实践才是检验真理的唯一办法。
看一下宇宙行的验证结果:
首先我们可以看到消费方不论是发起请求还是处理响应都是非常迅速的,但是卡壳就卡在服务方从收到请求到处理请求之间。
经过抓包分析,他们得出结论:导致交易超时的原因不在消费方侧,而在提供方侧。
这个结论其实也很好理解,因为压力都在服务提供方这边,所以阻塞也应该是在它这里。
其实到这里我们基本上就可以确认,肯定是 Dubbo 框架里面的某一些操作导致了耗时的增加。
难的就是定位到,到底是什么操作呢?
宇宙行通过一系列操作,经过缜密的分析,得出了一个结论:
心跳密集导致 netty worker 线程忙碌,从而导致交易耗时增长。
也就是结论中提到的这一点:
有了结论,找到了病灶就好办了,对症下药嘛。
因为前面说过,本文只聚焦于场景一,所以我们看一下对于场景一宇宙行给出的解决方案:
全都是围绕着心跳的优化处理,处理完成后的效果如下:
其中效果最显著的操作是“心跳绕过序列化”。
消费方与提供方之间平均处理时差由 27ms 降低至 3m,提升了 89%。
前 99% 的交易耗时从 191ms 下降至 133ms,提升了 30%。
好了,写到这,就差不多是把那篇文章里面我当时看到的一些东西复述了一遍,没啥大营养。
只是我还记得第一次看到这篇文章的时候,我是这样的:
我觉得挺牛逼的,一个小小的心跳,在 C10K 的场景下竟然演变成了一个性能隐患。
我得去研究一下,顺便宇宙行给出的方案中最重要的是“心跳绕过序列化”,我还得去研究一下 Dubbo 怎么去实现这个功能,研究明白了这玩意就是我的了啊。
但是...
我忘记当时为啥没去看了,但是没关系,我现在想起来了嘛,马上就开始研究。
心跳如何绕过序列化
我是怎么去研究呢?
直接往源码里面冲吗?
是的,就是往源码里面冲。
但是冲之前,我先去 Dubb 的 github 上逛了一圈:
然后在 Pull request 里面先搜索了一下“Heartbeat”,这一搜还搜出不少好东西呢:
我一眼看到这两个 pr 的时候,眼睛都在放光。
好家伙,我本来只是想随便看看,没想到直接定位了我要研究的东西了。
我只需要看看这两个 pr,就知道是怎么实现的“心跳绕过序列化”,这直接就让我少走了很多弯路。
首先看这个:
从这段描述中可以知道,我找到对的地方了。而从他的描述中知道“心跳跳过序列化”,就是用 null 来代替了序列化的这个过程。
同时这个 pr 里面还说明了自己的改造思路:
接着就带大家看一下这一次提交的代码。
怎么看呢?
可以在 git 上看到他对应这次提交的文件:
到源码里面找到对应地方即可,这也是一个去找源码的方法。
我比较熟悉 Dubbo 框架,不看这个 pr 我也大概知道去哪里找对应的代码。但是如果换成另外一个我不熟悉的框架呢?
从它的 git 入手其实是一个很好的角度。
一个翻阅源码的小技巧,送给你。
如果你不了解 Dubbo 框架也没有关系,我们只是聚焦于“心跳是如何跳过序列化”的这一个点。至于心跳是由谁如何在什么时间发起的,这一节暂时不讲。
接着,我们从这个类下手:
org.apache.dubbo.rpc.protocol.dubbo.DubboCodec