最近一直在找装修公司,自己辛苦买的房子不住进去确实心有不甘呐。。。所以,比赛完了好久才开始写这个比赛总结。写总结的原因是这次比赛还是学到了很多东西。想要总结下。一开始看到有这个比赛的时候我是犹豫之拒绝的。因为想着准备装修,刚换工作根本没有时间。直到另外一个朋友发消息给我说,有这个比赛挺适合我的时候,我才决定报名的。。
目的:借助于 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.5k。。感快优化,把之前偷懒的地方赶上去。。
1. 首先就是 ca http 服务器,之前使用 spring mvc ,赶快换成 netty 的 http。折腾了两晚上总算是搞定了。
2. 学习了 docker 性能分析指令,观察了容器的 cpu 情况。发现 provider 的 cpu的利用率明显不一致。找小伙伴改负载均衡算法。小伙伴第二版实现的是加权轮询算法实现的负载均衡。
3. 结果:
通过各种测试总算是有一点提升,但是进复赛还是很悬呐。。
眼看比赛结束只剩一周了,眼看有可能被淘汰了。下班,闲着没事 jstack 一下,发现线程还是有点多啊。
1. 找所有使用 eventLoop 的地方更改线程数。
2. 结果
增加了 100 多的 tps。
感觉实在没有办法了,周末还有两家装修公司要跑。。不管了,坐等比赛结果。周末走了几家公司之后,周一投入到了新公司给安排接收的新项目中,直到晚上回家闲着没事,有看了下排名。竟然入围了。。。。
比赛结束之后立马关注大佬的分享。对比之后感觉有好多需要学习的点。
2. jdk nio 封装的 epoll 是level-triggered。所以 netty 可以使用 EpollSocketChannel。
3. 使用入站服务端的 eventLoopGroup 为出站客户端预先创建好 channel,这样可以达到复用 eventLoop 的目的。并且此时还有一个伴随的优化点,就是将存储 Map 的数据结构,从 concurrentHashMap 替换为了 ThreadLocal ,因为入站线程和出站线程都是相同的线程,省去一个 concurrentHashMap 可以进一步降低锁的竞争。
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 队列的消息系统。限定只使用基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);
}
一个周末大家周六忙完自己的事情之后聚在一起讨论方案。由于第一赛季队友感觉把 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找。找不就到硬盘找。
又一个周末,装修找项目经理,经过一个小伙伴的家周围,顺便去沟通了下,合了一部分代码。另外一个小伙伴因为老家有事,回家了。。。
周一晚上下班,我们两找个咖啡厅,合代码。。
经过折腾总算是出了一个编译过的版本。提测。。段错误。。。
开始排查。上内存分析神器 valgrand 。经过几个晚上的调试。终于都解决了。接着上,可以半个小时左右,显示程序被杀死。。
距离比赛结束还有一晚上,感觉拿奖是没有希望了。但是突然来了兴趣,想要在进步下。而且今天才知道原来每天可以提测 8 次。。
1. 通过 gstatck 分析发现程序运行的时候好多锁等待。由于时间有限,我只敢改自己写的存储部分。Storage write() 的是排它锁。但是实际上写不同的文件的时候完全可以不锁,于是赶紧实现分段锁存储。实现完分段锁之后已经是夜里12点了。这个时候在提测等待结果肯定是来不及了。于是草草提测了。接着优化。
2. 结果 升到 60 几名了
写分段锁了。读呢能不能优化下呢?感谢 c++11 的atomic和linux pread()可以原子的随机读文件。
1. 于是读开始实现无锁化,经过分析,Storage read()的时候只有读内存的时候才怕和write()冲突,读文件的时候 pread() 可以并发读。
于是从 Storage读的时候先判断偏移量(atomic_offset类型的)如果要读内存则上锁读内存部分。如果不读内存,则直接从文件无锁的读。。
这次代码写完已经半夜2点了。上次的提测应该结束了。看了下,名词上升到 60 几名了。赶紧在提测。因为写的仓促,提测完之后,在源环境上也开始了稳定性测试。就这样到半夜4点多终于出成绩了。
2. 结果: 升到了 55名。
等待头几名大佬答辩分享比赛经验了。