高效杀死大规模容器
【CSDN 编者按】大家在编写代码时是不是经常会遇到repls卡顿而造成的容器关闭缓慢等问题呢?本文针对这一问题为大家提供了解决方案,希望给大家提供更流畅的体验。
以下为译文:
为了使基于Web浏览器的任何用户都可以在我们的Replit平台(在线编程语言环境)上进行编码,我们的后端基础架构运行在Preemptible VM(经济虚拟机器服务)上。这意味着你运行代码的同时计算机即使关机也毫无问题!
当发生这种情况时,我们努力使repls重新连接的速度加快。尽管我们尽了最大的努力,但是很长一段时间以来,人们一直感觉repls停滞不前。在对Docker源代码进行一些分析和挖掘之后,我们发现并解决了连接速度的问题。我们的会话连接错误率从3%降至0.5%以下,第99个百分位会话启动时间从2分钟缩短至15秒。造成repls卡顿的原因有很多不同,这些原因有:不健康的机器,会引起死锁的资源竞争状态以及容器关闭缓慢等。
本篇文章将重点分享我们如何解决容器缓慢关闭的问题。缓慢的容器关闭影响了几乎所有使用该平台的人,并可能导致长达一分钟的无法访问。也希望本文对大家有所裨益。
Replit架构
在深入解决容器关闭缓慢的问题之前,你需要了解Replit的体系结构。
当你打开一个repl时,浏览器将建立一个与Docker容器(运行在Preemptible VM上的)的websocket连接。每个虚拟机都运行着我们称之为conman的东西,这是容器管理器(container manager)的缩写。
我们必须确保任何时候每个repl只使用一个容器。该容器用于促进多人游戏功能,因此重要的是REPL中的每个用户都连接到同一容器。
当托管这些Docker容器的计算机关闭时,我们必须等待每个容器被销毁,然后才能在其他计算机上再次启动它们。由于我们使用抢占式实例,因此该过程经常发生。
在下方,你可以查看尝试在正在关闭的vm实例上访问repl时的典型流程。
1. 用户打开其repl,后者将打开IDE,并尝试通过WebSocket连接到后端评估服务器;
2. 该请求到达一个负载均衡器,该负载均衡器根据CPU使用情况选择要代理的conman实例;
3. 一个健康,存活的conman得到了请求。Conman注意到,该请求使用的容器位于其他conman,并在此代理该请求;
4. 可悲的是,另一个conman正在关闭并拒绝了该WebSocket连接。
请求将继续失败,直到发生以下任何一种情况:
1. Docker容器已关闭,全局存储中的repl容器条目已删除;
2. Conman完成关闭,无法再访问。在这种情况下,第一个conman将删除旧的repl容器条目并启动新的容器。
缓慢的容器关闭
我们的可抢占式VM在被强行终止之前,有30秒的时间进行彻底关闭。经过调查,我们发现在这30秒之内很少完成关机。这促使我们进一步研究并检测机器关闭程序。
在添加了有关机器关闭的更多日志记录和指标之后,我们可以很明显地发现,调用docker kill花费的时间比预期长得多。docker kill通常在正常操作期间需要花费几毫秒的时间来杀死一个repl容器,但是在实际关闭过程中我们花了20秒钟以上的时间来同时杀死100-200个容器。
Docker提供了两种停止容器的方法:docker stop和docker kill。Docker stop向SIGTERM容器发送信号,并为其提供宽限期以正常关闭。如果容器在宽限期内没有关闭,则向容器发送SIGKILL。我们不关心正常关闭容器,而是希望尽快关闭它,因此我们使用kill命令。docker kill发送SIGKILL,理论上应该立即杀死该容器。出于某种原因,该理论与现实不符,docker kill不应花费秒量级的时间去SIGKILL容器。肯定还有其他事情发生。
为了对此进行深入研究,我们编写了如下所示的脚本,该脚本创建200个Docker容器并确定同时杀死它们所需的时间。
#!/bin/bash
COUNT=200
echo "Starting $COUNT containers..."
for i in $(seq 1 $COUNT); do
printf .
docker run -d --name test-$i nginx > /dev/null 2>&1
done
echo -e "\nKilling $COUNT containers..."
time $(docker kill $(docker container ls -a --filter "name=test" --format "{{.ID}}") > /dev/null 2>&1)
echo -e "\nCleaning up..."
docker rm $(docker container ls -a --filter "name=test" --format "{{.ID}}") > /dev/null 2>&1
使用和生产环境一致的VM(GCE n1-highmem-4实例)运行脚本,可以得到:
Starting200
containers...................................<trimmed>
Killing 200 containers...
real 0m37.732s
user 0m0.135s
sys 0m0.081s
Cleaning up...
这证实了我们的怀疑,即Docker运行时内部正在发生某些事情,这导致关闭速度如此之慢。是时候深入研究Docker本身了...
Docker守护程序具有启用调试日志记录的选项。这些日志使我们可以深入了解dockerd内部发生的情况,并且每个条目都有一个时间戳,因此它可以使你了解时间都花在了哪里。
启用调试日志记录源码:https://docs.docker.com/config/daemon/
启用调试日志记录后,让我们重新运行脚本并查看dockerd的日志。由于我们正在处理200个容器,因此这将输出很多日志消息,因此,我手动选择了感兴趣的部分日志。
2020-12-04T04:30:53.084Z dockerd Calling GET /v1.40/containers/json?all=1&filters=%7B%22name%22%3A%7B%22test%22%3Atrue%7D%7D
2020-12-04T04:30:53.084Z dockerd Calling HEAD /_ping
2020-12-04T04:30:53.468Z dockerd Calling POST /v1.40/containers/33f7bdc9a123/kill?signal=KILL
2020-12-04T04:30:53.468Z dockerd Sending kill signal 9 to container 33f7bdc9a1239a3e1625ddb607a7d39ae00ea9f0fba84fc2cbca239d73c7b85c
2020-12-04T04:30:53.468Z dockerd Calling POST /v1.40/containers/2bfc4bf27ce9/kill?signal=KILL
2020-12-04T04:30:53.468Z dockerd Sending kill signal 9 to container 2bfc4bf27ce93b1cd690d010df329c505d51e0ae3e8d55c888b199ce0585056b
2020-12-04T04:30:53.468Z dockerd Calling POST /v1.40/containers/bef1570e5655/kill?signal=KILL
2020-12-04T04:30:53.468Z dockerd Sending kill signal 9 to container bef1570e5655f902cb262ab4cac4a873a27915639e96fe44a4381df9c11575d0
...
在这里我们可以看到杀死每个容器的请求,这些SIGKILL请求几乎立即发送到每个容器。
这是执行docker kill后约30秒后看到的一些日志条目:
...
2020-12-04T04:31:32.308Z dockerd Releasing addresses for endpoint test-1's interface on network bridge
2020-12-04T04:31:32.308Z dockerd ReleaseAddress(LocalDefault/172.17.0.0/16, 172.17.0.2)
2020-12-04T04:31:32.308Z dockerd Released address PoolID:LocalDefault/172.17.0.0/16, Address:172.17.0.2 Sequence:App: ipam/default/data, ID: LocalDefault/172.17.0.0/16, DBIndex: 0x0, Bits: 65536, Unselected: 65529, Sequence: (0xfa000000, 1)->(0x0, 2046)->(0x1, 1)->end Curr:202
2020-12-04T04:31:32.308Z dockerd Releasing addresses for endpoint test-5's interface on network bridge
2020-12-04T04:31:32.308Z dockerd ReleaseAddress(LocalDefault/172.17.0.0/16, 172.17.0.6)
2020-12-04T04:31:32.308Z dockerd Released address PoolID:LocalDefault/172.17.0.0/16, Address:172.17.0.6 Sequence:App: ipam/default/data, ID: LocalDefault/172.17.0.0/16, DBIndex: 0x0, Bits: 65536, Unselected: 65530, Sequence: (0xda000000, 1)->(0x0, 2046)->(0x1, 1)->end Curr:202
2020-12-04T04:31:32.308Z dockerd Releasing addresses for endpoint test-3's interface on network bridge
2020-12-04T04:31:32.308Z dockerd ReleaseAddress(LocalDefault/172.17.0.0/16, 172.17.0.4)
2020-12-04T04:31:32.308Z dockerd Released address PoolID:LocalDefault/172.17.0.0/16, Address:172.17.0.4 Sequence:App: ipam/default/data, ID: LocalDefault/172.17.0.0/16, DBIndex: 0x0, Bits: 65536, Unselected: 65531, Sequence: (0xd8000000, 1)->(0x0, 2046)->(0x1, 1)->end Curr:202
2020-12-04T04:31:32.308Z dockerd Releasing addresses for endpoint test-2's interface on network bridge
2020-12-04T04:31:32.308Z dockerd ReleaseAddress(LocalDefault/172.17.0.0/16, 172.17.0.3)
2020-12-04T04:31:32.308Z dockerd Released address PoolID:LocalDefault/172.17.0.0/16, Address:172.17.0.3 Sequence:App: ipam/default/data, ID: LocalDefault/172.17.0.0/16, DBIndex: 0x0, Bits: 65536, Unselected: 65532, Sequence: (0xd0000000, 1)->(0x0, 2046)->(0x1, 1)->end Curr:202
我首先查找处理容器终止请求的代码路径。我添加了一些具有不同时间跨度的额外日志消息,最终我发现所有这些时间都花在了哪里:
SIGKILL被发送到容器,然后在响应HTTP请求之前,引擎将等待容器不再运行。
源码:https://github.com/docker/engine/blob/ab373df1 125b6002603456fd7f554ef370389ad9/daemon/kill.go
<-container.Wait(context.Background(), containerpkg.WaitConditionNotRunning)
该container.Wait函数返回一个通道,该通道接收退出代码和来自容器的任何错误。不幸的是,要获得退出代码和错误,必须获取内部容器结构的锁。
源码:https://github.com/docker/engine/blob/ab373df1125b6002603456fd7f554ef370389ad9/container/state.go
...
go func() {
select {
case <-ctx.Done():
// Context timeout or cancellation.
resultC <- StateStatus{
exitCode: -1,
err: ctx.Err(),
}
return
case <-waitStop:
case <-waitRemove:
}
s.Lock() // <-- Time is spent waiting here
result := StateStatus{
exitCode: s.ExitCode(),
err: s.Err(),
}
s.Unlock()
resultC <- result
}()
return resultC
...
手册页源码:https://man7.org/linux/man-pages/man7/pid_ namespaces.7 .html
原文地址:
https://blog.repl.it/killing-containers-at-scale
声明:本文为 CSDN 翻译,转载请注明来源。
☞巨头王炸不断,硬核解读芯片技术路线
☞Babel 陷财务困境,负责人13万年薪遭质疑,Vue.js作者尤雨溪发文力挺
☞9 岁自学编程、24 岁身价涨至数百万美元,与微软一较高低的大佬多厉害?
关注公众号:拾黑(shiheibook)了解更多
[广告]赞助链接:
四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/

随时掌握互联网精彩
- 1 植树就是植未来 7923442
- 2 DeepSeek眼中不会被AI替代的职业 7949242
- 3 海底捞10倍补偿小便事件4109单顾客 7808556
- 4 哪些行业人才火爆就业市场 7732466
- 5 商务部等部门约谈沃尔玛 7603596
- 6 实探大疆总部:21点楼下人头攒动 7527026
- 7 #金秀贤与未成年恋爱是恋童癖嘛# 7429325
- 8 陕西一医院被曝“偷换”新生儿 7357769
- 9 女子离婚提5万家务补偿 法院判25万 7235787
- 10 体重门诊到底能干啥 7102846