第四届阿里巴巴性能大赛总结

比赛完了好久才开始写这个比赛总结。写总结的原因是这次比赛还是学到了很多东西。想要总结下。

第一赛季:

简介:

目的:借助于 service mesh 的解决方案,让 dubbo 自己提供跨语言的解决方案,来屏蔽不同语言的处理细节,于是乎,dubbo 生态的跨语言 service mesh 解决方案就被命名为了 dubbo mesh。
赛题

实现一个高性能的 Service Mesh Agent 组件,并包含如下一些功能:

  1. 服务注册与发现
  2. 协议转换
  3. 负载均衡

这里写图片描述

简单点说就是实现图中红色部分 (ca)consumer-agent, (pa)provider-agent。
ca 和 pa 通信不限,pa和provider dubbo协议(provider是dubbo提供的服务)。每个 provider 处理能力不一样(负载均衡算法)。其他详细限制请看赛题:
我约了队友出来一起见个面准备开搞这个事情。周末下午经过千山万水大家终于聚在一起,没时间搞代码了,只能一起讨论下题目了。

第一版本

我们讨论出的方案是:
这里写图片描述
方案有了开撸?no,比赛环境是 spring boot + docker 的。。
之前一直想等有时间了,好好学习下 spring boot + spring cloud + docker,但是一直没有学。每个周五晚上一边恶补知识,一边写代码。
环境问题终于解决了,开始全力撸代码吧。
首先我们选择的网络框架为 netty(netty 比较熟悉) 。和etcd通信的就使用了官方例子里的 jetcd-core。

  1. 首先完成的是照着 duubo 的协议实现** dubbo协议**。官方给的 dubbo的协议真的写的太难看了 ,于是找了一个晚上重写,结果花了两个晚上写完。。 就这样 pa 和 provider 通信了。dubbo协议也了解了 _
  2. 终于周五晚上到了,计划是完成 ca 和 pa 通信这步。肯定是要用长连接或者udp,不可能像官方提供的 demo 那样用 http。一开始懒得写代码,ca 直接使用的 dubbo 协议发给 pa,pa 透传给 provider。
  3. 周六最后一块,ca 怎么接受 consumer 发送过来的http部分。。第一版当然是先完成主流程了。直接用了 spring mvc 的http部分。我知道性能不一定好。但是先完成主要流程把。
  4. 周天和小伙伴碰面,完成负载均衡算法。小伙伴和我说 java 环境太复杂了,mvc,boot,cloud 等,不会写了代码了(这也是复赛选择c++一个重要原因)。。。最后我们先随便完成第一个负载均衡算法随机负载均衡
  5. 结果:
    周一晚上一下吧我就把所有代码整理起来了。然后提交了。也跑出了成绩。比官方提供的高了500多的tps。

优化1 负载均衡

正式比赛开始了,看了下大家的成绩,惊掉了下巴,怎么都这么高,和第一差了大约 1.5k。。感快优化,把之前偷懒的地方赶上去。。

  1. 首先就是 ca http 服务器,之前使用 spring mvc ,赶快换成 netty 的 http。折腾了两晚上总算是搞定了。
  2. 学习了 docker 性能分析指令,观察了容器的 cpu 情况。发现 provider 的 cpu的利用率明显不一致。找小伙伴改负载均衡算法。小伙伴第二版实现的是加权轮询负载均衡
  3. 结果:
    通过各种测试总算是有一点提升,但是进复赛还是很悬呐。。

优化2 jvm

  1. jvm 优化jmap 观察内存,没有发现老年代内存有频繁 gc。jstat 观察 old gc 也很正常。jstack 发现问题。线程有 100 多个。原来写代码的时候ca 作为客户端时候使用的 FixChannlePool 链接池每次添加新链接的时候都会 new 一个新的 EventLoop。这还了得,线程切换多费性能啊
  2. 结果:
    完成之后测试了一下,果然性提升了小几百。

优化3 网络

  1. 乘着手热,把之前的坑都写写。之前 ca 到 pa 是 tcp 长连接的透传的方式。猜测可能 ca 收到数据转为 duubo 协议发送给 pa,pa 直接透传给provider 的时候会pa会拆包,倒是 provider 组包之后才能处理,影响了 provider 的处理性能。于是使用 length+cbor编码消息体的自定义协议。
  2. 结果:
    一顿整改之后终于完成了。提测只增加了几十 tps。。。。

优化4 线程数

眼看比赛结束只剩一周了,看看别人的性能都在提升了好多。。慌了。下班,闲着没事 jstack 一下,发现线程还是有点多啊。

  1. 找所有使用 eventLoop 的地方更改线程数。
  2. 结果:
    增加了 100 多的 tps。

优化5 netty

  1. 使用入站服务端的 eventLoopGroup 为出站客户端预先创建好 channel,这样可以达到复用 eventLoop 的目的。并且此时还有一个伴随的优化点,就是将存储 Map 的数据结构,从 concurrentHashMap 替换为了 ThreadLocal ,因为入站线程和出站线程都是相同的线程,省去一个 concurrentHashMap 可以进一步降低锁的竞争。

结束

感觉实在没有办法了,不管了,坐等比赛结果。可能努力过了,都忘了看结果,直到晚上回家闲着没事,有看了下排名。竟然入围了。通知兄弟们别休息了,准备决赛吧。。。
排名

总结

收获:
  1. 首先这个比赛的机会学会了 spring boot ,spring cloud,docker 。现在已经在项目组中推行。
  2. 以前一直觉得 io (网络,硬盘)才是瓶颈,现在知道原来,在极限的优化下,任何代码都可能是瓶颈。
  3. 坚持不懈的精神,看大佬分享的优化方案,就是不停的尝试,哪怕只有一点点提升。
学到的技术:

比赛结束之后立马关注大佬的分享。对比之后感觉有好多需要学习的点。
2. jdk nio 封装的 epoll 是level-triggered。因为比赛使用的环境是 linux 所以 netty 可以使用 EpollSocketChannel。
4. provider 线程数固定为 200 个线程,如果 large-pa 继续分配 3/1+2+3=0.5 即 50% 的请求,很容易出现 provider 线程池饱满的异常,所以调整了加权值为 1:2:2。
5. 关闭 netty 的内存泄露检测。
6. 百度: 对于主动连接(connect)的fd,设置TCP_QUICKACK=0,该往往说明客户端将很快有数据要发送给服务器,所以在三次握手协议中的第三步,客户端会延迟发送ACK,而是直接给服务器发送request数据,并将ACK随request包一同发给服务器。
对于被动接受(accept)的fd,设置了TCP_QUICKACK=0。
这种情况需要先明白一个过程,比如对于一个http协议,三次握手协议结束后,客户端会立即向服务器发送一个request请求,当服务器接收完这个request请求以后,会首先给客户端一个ACK确认告诉客户端已经收到了该数据包,然后当服务器完成了请求,才会再发response。
明白了这个过程,就很容易解释了,服务器端这样设置的目的就是接收完request后先不ACK,而是把这个ACK和接下来的response一同发送给客户端。

第二赛季:

第二赛季一开始感觉在第一赛季耗光了精力,不大想搞了。可以谁能想到小伙伴可能由于自己公司的事情忙的差不多了,主动联系我们开始搞复赛。。队友不放弃,我也不能放弃。。。T_T

赛题

赛题
目标:使用 java 或者 c++ 实现一个 100w 队列(topic)的消息系统。限定只使用基jdk。机器性能4c8g的ECS,限定使用的最大JVM大小为4GB(-Xmx4g)。我猜是为了物联网场景,才百万topic 的。

public abstract class QueueStore {
    abstract void put(String queueName, byte[] message);
    abstract Collection<byte[]> get(String queueName, long offset, long num);
}
  1. 程序校验逻辑
    校验程序分为三个阶段: 1.发送阶段 2.索引校验阶段 3.顺序消费阶段
  2. 程序校验规模说明
    1. 各个阶段线程数在20~30左右
    2. 发送阶段:消息大小在50字节左右,消息条数在20亿条左右,也即发送总数据在100G左右
      3)索引校验阶段:会对所有队列的索引进行随机校验;平均每个队列会校验1~2次;
      4)顺序消费阶段:挑选20%的队列进行全部读取和校验;
      5)发送阶段最大耗时不能超过1800s;索引校验阶段和顺序消费阶段加在一起,最大耗时也不能超过1800s;超时会被判断为评测失败。
  3. 排名规则
    在结果校验100%正确的前提下,按照平均tps从高到低来排名。

一个周末大家周六忙完自己的事情之后聚在一起讨论方案。由于第一赛季队友感觉把 java 都忘的差不多了,加上 jvm 有限制4G内存。第二赛季我们决定使用c++实现。

第一版

因为 c++ 没有分段锁的 map,要自己实现一个 java 的 CouncurrentHashMap。小伙伴测试下来,100w 最好就是 1000*1000来分段。这块我们到最后也是这个策略。
因为内存有限,每个topic 缓存到40条,然后调用存储Storage的 write()获取到一个这快消息的一个索引信息FileBlock。Storage 随机找到一个BlockQueue(启动会初始化多个),判断文件的缓存队列是否大于二级缓存极限,大于则触发 BlockQueue 的 write()。BlockQueue 的write()会入队数据,线程异步写入文件系统

读的话,刚刚好是反过程,Topic 先判断是在缓存还是在 FlieBlock 的list里,读自己的缓存。读FileBlock list里的就去,下级Storage里要。Storage在己的缓存找,找不到就到BlockQueueFile找。找不就到硬盘找。

主要流程图:
主流程

主要类图:
类图

又一个周末,经过一个小伙伴的家周围,顺便去沟通了下,合了一部分代码。另外一个小伙伴因为老家有事,回家了。。。
周一晚上下班,我们两找个咖啡厅,合代码。。
整体思路:

  1. 多用 atomic 避免锁。
  2. 100w 的 topic 每次找到写消息的地方就很费劲,还得考虑并发。使用 分段锁实现 topic 的存。
  3. 批量顺序写磁盘,不然的话根本就读不出来。

优化1:

经过折腾总算是出了一个编译过的版本。提测。。段错误。。。
开始排查。上内存分析神器 valgrand 。经过几个晚上的调试。终于都解决了。接着上,可以半个小时左右,显示程序被杀死。。

优化2:

  1. 分析无果后,买了云服务器,决定在运行的时候分析状态(100G数据谁会用自己的电脑?)。经过分析发现程序运行十几分钟后内存就耗尽了。无奈只能把每个topic缓存的消息由40改为20。BlockQueueFile 的队列缓存改成最多10个。否则入队的时候将阻塞。一顿本地调试之后提交。一个多小时候终于出结果了。
  2. 大约 70几名。

优化3:

距离比赛结束还有一晚上,感觉拿奖是没有希望了。但是突然来了兴趣,想要在进步下。而且今天才知道原来每天可以提测 8 次。。。我以为两次这浪费了这么多机会呀。

  1. 通过 gstatck 分析发现程序运行的时候好多锁等待。。Storage write() 的是互斥锁。但是实际上写不同的文件的时候完全可以不锁,于是赶紧实现分段锁存储。实现完分段锁之后已经是夜里12点了。这个时候在提测等待结果肯定是来不及了。于是草草提测了。接着优化。
  2. 结果 升到 60 几名了

优化4:

写分段锁了。读呢能不能优化下呢?感谢 **c++11 的atomic和linux pread()**可以原子的随机读文件。

  1. 于是读开始实现无锁化,经过分析,Storage read()的时候只有读内存的时候才怕和write()冲突,读文件的时候 pread() 可以并发读。
    于是从 Storage读的时候先判断偏移量(atomic_offset类型的)如果要读内存则上锁读内存部分。如果不读内存,则直接从文件无锁的读。。
    这次代码写完已经半夜2点了。上次的提测应该结束了。看了下,名词上升到 60 几名了。赶紧在提测。因为写的仓促,提测完之后,在源环境上也开始了稳定性测试。就这样到半夜4点多终于出成绩了。
  2. 结果: 升到了 55名。

优化5:

  1. 初赛的时候看他们的经验说是有时候看运气。我到了这个时候也是没有办法了。到第二天10点前还有6个小时。于是定了闹钟2小时一次提测。每次都是把参数改的很激进。可惜并没有出现什么好运。名词定格在了 55。
  2. 结果:无变化。
    最终排名

总结:

收获:
  1. 第二赛季感觉还是挺不错的,毕竟没有学环境学好久。最后一晚的感觉来了,使名词提升了10几名还是挺爽的,好久没有这么酣畅淋漓的写代码了。
  2. 无锁化的性能提升还真是不小的。
  3. linux 底层有些为性能的优化的 api 可以直接用。
学到的技术:

根据大佬的分析我发现了几个点。
最震惊: 的几个香港的并发博士,竟然分析阿里云 cpu 从指令集优化。。。
最难受: 要根据比赛的测试规则,面向测试用例编程。感觉吃亏了,我这个面向产品级各种场景的编程了。。。
golang 并发编程也不错。
Actor 思想也很好。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值