0x00 起因
首先,这是一篇迟到了非常久的 blog,这个性能优化早在今年 6 月份就已经上线,这篇文章将细致的梳理一下这次调优的过程。
调优的对象就是 PTA 使用的评测机,该代码已经开源,原本是 quark 学长维护的版本,而现在在我们团队的 git 仓库当中,分别是 ljudge 和 lrun 。
首先简单讲一下评测机的部署方案:
- 每个评测机处于一个 docker 容器当中
- 每台云主机可以 host 多个 docker 容器
- 每个评测机都是单线程工作的,占用一个核心
- 每台云主机根据自身核心数来决定最大承担 judger 容器的个数
judger 的性能问题,是在一次承担考试的时候发现的。我因为观察到评测队列堆积,所以进行了一次扩容,将云主机上的容器个数调整到了最大值。但不幸的是,从监控来看,评测队列的消化速度几乎没有增加。之后也遇到过几次同样的情况,所以最终决定进行一次性能问题的排查。
0x01 排查
准备一些脚本:
entrypoint.sh
1 |
|
在容器当中 judge 一个最简单的 a+b 的程序,循环 100 次,放进容器当中备用
1 |
|
分别启动 1 ~ 6 个容器,这里的 --entrypoint
就是指定刚才放进容器当中的脚本,测试所需要的时间(脚本写的有点 low 见谅)
在一台 8 核心的机器上,得到了测试结果
1 |
|
很明显可以看出,在一台核心数足够的机器上启用多个 judger,消耗的时间竟然是线性增加的,也就是说,在单机上通过增加 judger 实例数进行的扩容效果几乎没有,多个 judger 之间势必有资源发生了抢占。
0x02 继续排查
那确认了这种部署模式的问题之后,接下去就需要找是什么东西在多个 judger 之间发生了竞争。排查持续了一天,期间使用了各种工具比如 perf,strace 等等,但都没有确定问题所在。但一个偶然的尝试发现了一些问题。
通过观察,可以发现 lrun 进程频繁的 sleep 在 copy_net_ns 这个地方。确认了一下代码,每一次评测都会创建一个新的 network namespace。所以根据表现,大胆猜测 network namespace creation 应该是一个比较耗时而且无法并发的调用。但 judger 确实需要进行网络隔离,所以在这里我尝试绕过这个行为。
因为 judger 同一个时间只会评测一个程序,也就是说同一个时间至多只会存在一个沙盒,所以我可以事先创建好一个已经被隔离开的 network namespace,而让每次评测是将这个 network namespace 设置到当前沙盒上,且不需要考虑多个沙盒的情况。
带着这个思路简单增加了一些代码:
1 | // switch to existed netns for better performance if needed |
如果没有 reusable network namespace,则创建一个新的,如果有并且可用,就将当前沙盒的 network namespace 设置成它。
0x03 验证
准备脚本:
entrypoint-netns.sh
1 |
|
在之前的测试脚本当中加入测试优化过的 judger 的代码
1 |
|
对照测试!得到结果
1 | root@judger-test:~# bash benchmark.sh |
可以看到优化之后,在同一台机器上开核心数以内的 judger 实例,总时长并不会有明显增加,也就是说扩容的目标达到了。
至此这次 judger 的性能优化就算是完成了~