高效杀死大规模容器

【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/bashCOUNT=200echo "Starting $COUNT containers..."for i in $(seq 1 $COUNT); doprintf .docker run -d --name test-$i nginx > /dev/null 2>&1doneecho -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实例)运行脚本,可以得到:
Starting?200?containers...................................<trimmed>Killing?200?containers...real????0m37.732suser????0m0.135ssys?????0m0.081sCleaning 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%7D2020-12-04T04:30:53.084Z????dockerd????Calling?HEAD?/_ping2020-12-04T04:30:53.468Z????dockerd????Calling?POST?/v1.40/containers/33f7bdc9a123/kill?signal=KILL2020-12-04T04:30:53.468Z????dockerd????Sending?kill?signal?9?to?container?33f7bdc9a1239a3e1625ddb607a7d39ae00ea9f0fba84fc2cbca239d73c7b85c2020-12-04T04:30:53.468Z????dockerd????Calling?POST?/v1.40/containers/2bfc4bf27ce9/kill?signal=KILL2020-12-04T04:30:53.468Z????dockerd????Sending?kill?signal?9?to?container?2bfc4bf27ce93b1cd690d010df329c505d51e0ae3e8d55c888b199ce0585056b2020-12-04T04:30:53.468Z????dockerd????Calling?POST?/v1.40/containers/bef1570e5655/kill?signal=KILL2020-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?bridge2020-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:2022020-12-04T04:31:32.308Z????dockerd????Releasing?addresses?for?endpoint?test-5's?interface?on?network?bridge2020-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:2022020-12-04T04:31:32.308Z????dockerd????Releasing?addresses?for?endpoint?test-3's?interface?on?network?bridge2020-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:2022020-12-04T04:31:32.308Z????dockerd????Releasing?addresses?for?endpoint?test-2's?interface?on?network?bridge2020-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 习近平将发表二〇二六年新年贺词 7904141
- 2 2026年国补政策来了 7808738
- 3 东部战区:开火!开火!全部命中! 7712893
- 4 2026年这些民生政策将惠及百姓 7616985
- 5 小学食堂米线过期2.5小时被罚5万 7519709
- 6 解放军喊话驱离台军 原声曝光 7428214
- 7 为博流量直播踩烈士陵墓?绝不姑息 7327605
- 8 每月最高800元!多地发放养老消费券 7238391
- 9 数字人民币升级 1月1日起将计付利息 7141831
- 10 2026年1月1日起 一批新规将施行 7040675











CSDN
