压测的时候 QPS 为什么上不去?答案和解析

这个问题实际的原因是客户端的端口不够用了。

为什么端口会不够用呢?因为一个 TCP 连接的标志是四元组:

(src ip, src port, dst ip, dst port)

在这个场景中,代理服务器去连接 Real Server:

  • 代理服务器的 src ip 确定
  • 代理服务器的 src port 是随机指定
  • dst ip 是 Real Server 的 ip
  • dst port 是 Real Server 的 port

所以,能让 TCP 四元组不一样的字段,就只有 src port 了。

那么 Linux 服务器在连接远程服务器的 80 端口的时候,本地端口会用什么呢?答案是随机指定的。但是我们可以设置随机指定的范围。通过 sysctl -w net.ipv4.ip_local_port_range="32768 65535" 命令,可以让 client port 使用 32768 到 65535 之间的值。这样,低于 32768 的端口可以让其他服务 listen。

Local Port 不够用的一些场景

默认的端口就有 3 万个可用,所以大部分的情况下是很够用的。况且,这是在 client ip, dst ip, dst port 都确定的情况下最多可以建 3 万个连接。如果 dst ip 和 dst port 不固定,比如同一个 HTTP 服务在同一个 Server IP listen 了两个端口,那么就是最多 6 万个连接。如果部署多个实例,不同的 IP,那么每一个 IP 都可以是 3 万个连接。这么大的连接数量,一般来说代码性能甚至硬件(网卡)性能会首先到达瓶颈。

什么情况下会遇到端口不够用呢?

一种就是如上所说,一个代理程序去直连另一个真实服务器,两边的 IP 固定了,一边的端口固定了,那么 client 侧端口最多 3 万的话,在 QPS 大的情况下可能会遇到端口不够用的情况。

理论上最多可以有 3 万个并发,为什么在实际的情况中达不到这么高的并发呢?因为在一个 TCP 连接结束之后,这个 client port 并不是马上可以用来创建一个新的 TCP 连接。在 TCP 的状态机中,主动关闭 TCP 连接的一方会进入 TIME_WAIT 状态。需要在这个状态等待 2MSL (Maximum Segment Lifetime,最大报文生存时间,在 Linux 中,默认是 1 分钟的等待时间),然后这个 TCP 连接才会完全释放,client 端口才可以被重新用来建立新的 TCP 连接。

为什么要等呢?原因主要有二:

  1. 最后回复的 ACK 可能丢失了,如果再收到对方发来的 FIN,还可以回复 ACK;
  2. 如果直接建立新的连接,那么属于当前连接的包由于乱序、延迟或者重复,可能会让对方收到,对方可能认为是属于自己的连接的包,造成问题。所以,等待 2MSL 可以确保连接相关的数据包在网络中完全消失;
图来自维基百科

那么这种情况该如何解决呢?

首先可以调整参数,sysctl -w net.ipv4.ip_local_port_range="10000 65535" 就可以有更多的可用端口。

另一种就是用长连接,不那么频繁地建立连接,也就没有反复创建连接的端口问题了。

TIME_WAIT 状态的行为是可以通过参数调整的,通过 sysctl -w net.ipv4.tcp_tw_reuse=1 设置,可以让处于 TIME_WAIT 状态的端口用于创建新的 TCP 连接。(但是可能带来其他问题)

还有一种情况会遇到 local port 不够用,就是 NAT 设备,source IP 可能有很多,但是经过了 NAT,NAT 上的 TCP 连接就都是 NAT 的 IP 了,很容易造成四元组不够用。NAT 上面的问题最好的办法是增加出口 IP。

抓包如何分析?

到这里,首先向读者致歉,在写分析的时候,我发现这个例子其实并不好完全通过抓包来分析解决。因为出问题的时候,客户端角度的包并没有发出来,抓包也就抓不到这个包。所以这个例子选的不合适。

这个例子最好的排查方法是通过客户端侧的网络状态来排查。直接通过 ss -s 命令,可以直接看到处于 timewait 状态的连接。

ss -s 命令查看连接状态

如果很高(占用了可用 local 端口范围的大部分),就说明瓶颈在这里了。

通过 tcp.flags.syn==1 and tcp.dstport == 80 and tcp.srcport == 65531 这个条件来过滤,我们可以查看同一个 local port 建立连接的历史。

连接建立的历史

打开 Delta Time,可以看到这个端口每次复用的时间在 60s 之后了,和 Linux timewait 默认的等待时间一致,也可以判断出来是这种问题。

==计算机网络实用技术 目录==

这篇文章是计算机网络实用技术系列文章中的一篇,这个系列正在连载中,我计划用这个系列的文章来分享一些网络抓包分析的实用技术。这些文章都是总结了我的工作经历中遇到的问题,经过精心构造和编写,每个文件附带抓包文件,通过实战来学习网路分析。

如果本文对您有帮助,欢迎扫博客右侧二维码打赏支持,正是订阅者的支持,让我公开写这个系列成为可能,感谢!

没有链接的目录还没有写完,敬请期待……

  1. 序章
  2. 抓包技术以及技巧
  3. 理解网络的分层模型
  4. 数据是如何路由的
  5. 网络问题排查的思路和技巧
  6. 不可以用路由器?
  7. 网工闯了什么祸?
  8. 网络中的环路和防环技术
  9. 延迟增加了多少?
  10. TCP 延迟分析
  11. 压测的时候 QPS 为什么上不去?
  12. 压测的时候 QPS 为什么上不去?答案和解析
  13. 重新认识 TCP 的握手和挥手
  14. 重新认识 TCP 的握手和挥手:答案和解析
  15. TCP 下载速度为什么这么慢?
  16. TCP 长肥管道性能分析
  17. 请求为什么超时了?
  18. 请求为什么超时了?答案和解析
  19. 后记:学习网络的一点经验分享
与本博客的其他页面不同,本页面使用 署名-非商业性使用-禁止演绎 4.0 国际 协议。
 

数据是如何转发的

在之前的文章中介绍了网络的「分层」概念1,那么这一篇继续科普一下网络的基础,即「转发」这个概念。

所有的转发都是二层转发。

为什么这么说呢?从前面网络的分层中我们已经知道,四层的网络协议是基于一种三层协议的,三层协议的数据包需要二层协议来承载(毕竟,我们不可能直接把三层数据包不经过二层直接传给别人),然后二层协议的数据包通过物理层发出去。物理层就是通信技术的知识了,可以说,到网络工程师这里第一层数字信号就是二层。在我们平时使用抓包工具抓包的时候,看的最外层数据包一般都是二层的包。

从网络设备的工作原理上讲也是这样,一个 IP 包从 src 传到 dst,中间经过了各种各样的网络设备,那么从 src 发出到 dst 收到,中间的网络设备修改了这个包的什么内容,才能一环接一环把它送到目的地呢?

交换机:不会修改任何内容,只查找自己的 mac 地址表转发走;

路由器:会根据 dst IP 查询下一跳应该发给哪一个 IP,但是下一跳的 IP 不会添加到数据包中。路由器会获取到下一跳对应的 MAC 地址,把数据包的 dst MAC 修改成下一跳的 MAC 然后转发出去。可以认为,此路由器出口 IP 和下一跳路由器入口的 IP 是在同一个子网中,所以每跳三层转发都是在同一个子网内的转发,即「所有的转发都是二层转发」。一般来说,路由器会有很多个物理接口,每一个物理接口都有 IP,互相连接的两个路由器的接口是在同一个子网(也有某些特殊的 P2P 网络可以不在同子网,甚至不需要配置 IP就能完成三层路由)。

不通路由的之间的接口是在同一个 LAN 下,所以也是基于二层转发

可以看出来,如果不考虑 NAT 这种会改变 IP 的设备,三层及以上的内容(除了 TTL 会被路由器修改)的内容是基本不会改变的。而二层内容几乎每一次都在改变。

Traceroute 的原理

一个包从源发送到目的地,要经过的网络设备太多,遇到问题怎么排查呢?traceroute 是网络的世界中最常用的一个工具了。

它的原理是:发送 TTL=1 的 ping 包,故意让第 1 跳路由器无法完成转发,第 1 跳路由器只能丢弃这个包,并发送 ICMP time exceed 错误信息回来,这个 ICMP 是告诉我转发的时候出错了,源 IP 是发生因为 TTL 丢包的设备的 IP,目的 IP 就是我,因为错误是要告诉我的。于是我就知道了第 1 跳的 IP(即丢包的 IP)是什么了。如此炮制,继续发送 TTL=2 的包出去,拿到第二跳的地址。知道目的地收到了我的包并且回复 ICMP reply 回来。这样,我就知道了整条链路上所有的设备的 IP,就可以用来定位问题了。当然了,肯定有些设备因为「安全因素」的考虑,配置了不对丢包发回去 ICMP time exceed,这样,这一跳就是空的,我们拿不到它的 IP。

说起来,还有过一次挺有意思的讨论。一次面试的时候,我和人家讨论 traceroute 的原理,对方讲的很好—— 「发送 TTL=1 的包,再发送 TTL=2 的包,到那一跳的时候会因为 TTL 丢包,然后直接发送一个 ICMP 回来」。但是这里的「直接」很有意思,有多直接呢?我问:「直接的意思是,比如第 3 跳会直接发给 src IP 一个 ICMP,还是第三跳转发 ICMP 给第 2 跳,第 2 跳转发给第 1 跳,最后转发到 src IP ?」对方说:「是前者,会直接发给 src IP,因为是要立即告诉 sender 出错了。」

候选人认为的 ICMP 工作方式

但是…… 候选人忽略了一个实际的问题——我们的 sender 和 R3 有物理连接吗?如果有物理连接,那么从发出的时候就不需要经过 R1 和 R2 了!说完之后他也恍然大悟,包是不可能隔空传递的!

什么是流?

谈到四层的时候,我认为流 (flow) 这个概念在四层上强调地不够。四层的数据流,就像小溪一样,源源不断从一个地方流到另一个地方,但是经过的路线总是一样的。

虽然在 IP 层每一跳都可能有多个下一跳设备可以选择,甚至多个设备的 cost 一样。那么在选路的时候,这一跳会根据 hash 算法来选择一个作为下一跳。hash 算法对于属于同一个流的数据包总会得到相同的结果,这样,就可以保证一个流的所有数据包经过的路线是一样的。比如,TCP 用来计算 hash 的 header 有 (src ip, src port, dst ip, dst port),UDP 和 TCP 一样,ICMP 一般是 (src ip, dst ip)。这也取决于设备的配置和实现,不一定非要使用这些字段。比如,使用 (src MAC, dst MAC) 也可以,TCP 的 hash 也可以只使用 (src ip, dst ip)。只要保证一点:同一个流经过的路线是一样的,就可以了。

尽管有多条路线,但是一个 flow 总会保持同一个路线

为什么要这样做呢?为了尽最大努力保证数据包的顺序,让接收到的顺序和发送的顺序是一致的。

但是 TCP 协议不是会保证包的顺序的一致性吗?是的。TCP 尽管可以帮我们纠正顺序,但是这不是免费的,TCP 需要在实现上利用 buffer 将乱序的包临时保存并且重新排列,然后再交给应用层。而且 TCP 协议可能认为收到的包乱序是网络堵塞了,然后会降低发送的速度。所以 IP 层会尽量保证包的到达顺序和发送顺序一致,但是不会完全保证。TCP 则会作为最后的兜底,完全保证顺序的一致性。

  1. 理解网络的分层模型 ↩︎

==计算机网络实用技术 目录==

这篇文章是计算机网络实用技术系列文章中的一篇,这个系列正在连载中,我计划用这个系列的文章来分享一些网络抓包分析的实用技术。这些文章都是总结了我的工作经历中遇到的问题,经过精心构造和编写,每个文件附带抓包文件,通过实战来学习网路分析。

如果本文对您有帮助,欢迎扫博客右侧二维码打赏支持,正是订阅者的支持,让我公开写这个系列成为可能,感谢!

没有链接的目录还没有写完,敬请期待……

  1. 序章
  2. 抓包技术以及技巧
  3. 理解网络的分层模型
  4. 数据是如何路由的
  5. 网络问题排查的思路和技巧
  6. 不可以用路由器?
  7. 网工闯了什么祸?
  8. 网络中的环路和防环技术
  9. 延迟增加了多少?
  10. TCP 延迟分析
  11. 压测的时候 QPS 为什么上不去?
  12. 压测的时候 QPS 为什么上不去?答案和解析
  13. 重新认识 TCP 的握手和挥手
  14. 重新认识 TCP 的握手和挥手:答案和解析
  15. TCP 下载速度为什么这么慢?
  16. TCP 长肥管道性能分析
  17. 请求为什么超时了?
  18. 请求为什么超时了?答案和解析
  19. 后记:学习网络的一点经验分享
与本博客的其他页面不同,本页面使用 署名-非商业性使用-禁止演绎 4.0 国际 协议。
 

压测的时候 QPS 为什么上不去?

小陈最近为团队的 HTTP 服务开发了一个高性能网关,用户的请求会先经过小陈开发的网关,网关对 HTTP Header 做一系列的校验,如果没有问题,会把请求转发给真实服务器(Real Server,简称为 RS)。网关经过测试,性能非常高,请求可以在 2ms 内完成转发。于是小陈很高兴,自己写的代码马上就能在线上服务十几万的真实用户了!现在,距离能够上线就差最后一步——压力测试,能不能支持十几万的用户同时在线,这时候就要拉出来溜溜了。

压力测试的时候发现,单机 QPS 总是在 5 万左右就无法继续提高了,虽说这个性能也够了,但是瓶颈到达的时候,CPU 和 内存的利用率都很低。无法充分利用硬件资源,这在当下「降本增效」的公司运动中解释不了呀!于是小陈撸起袖子,进行一番排查,势必找到瓶颈,榨干硬件资源。

经过查看日志,小陈发现这个网关的日志中有一些连接建立失败 (Failed to establish a new connection) 的情况。为什么连接会建立失败呢?难道是压测环境的网络出了问题?抓包看看就知道了!

tcpdump 的位置

小陈在网关侧进行抓包,得到的抓包文件如下。

对着抓包文件看了半天,小陈还是没有看出来个所以然。所幸,团队里有一个网络专家(就是你!)小陈只好来请求专家的帮助。

请下载上面这个抓包文件,用 Wireshark 打开,解释一下小陈的网关软件为什么会出现 Failed to establish a new connection 的错误?(答案会在下一篇博客揭晓)

==计算机网络实用技术 目录==

这篇文章是计算机网络实用技术系列文章中的一篇,这个系列正在连载中,我计划用这个系列的文章来分享一些网络抓包分析的实用技术。这些文章都是总结了我的工作经历中遇到的问题,经过精心构造和编写,每个文件附带抓包文件,通过实战来学习网路分析。

如果本文对您有帮助,欢迎扫博客右侧二维码打赏支持,正是订阅者的支持,让我公开写这个系列成为可能,感谢!

没有链接的目录还没有写完,敬请期待……

  1. 序章
  2. 抓包技术以及技巧
  3. 理解网络的分层模型
  4. 数据是如何路由的
  5. 网络问题排查的思路和技巧
  6. 不可以用路由器?
  7. 网工闯了什么祸?
  8. 网络中的环路和防环技术
  9. 延迟增加了多少?
  10. TCP 延迟分析
  11. 压测的时候 QPS 为什么上不去?
  12. 压测的时候 QPS 为什么上不去?答案和解析
  13. 重新认识 TCP 的握手和挥手
  14. 重新认识 TCP 的握手和挥手:答案和解析
  15. TCP 下载速度为什么这么慢?
  16. TCP 长肥管道性能分析
  17. 请求为什么超时了?
  18. 请求为什么超时了?答案和解析
  19. 后记:学习网络的一点经验分享
与本博客的其他页面不同,本页面使用 署名-非商业性使用-禁止演绎 4.0 国际 协议。
 

Golang 中的 One-function Interfaces

看到一个 Golang 的模式,用一个 function 来实现一个 interface,function 本身就是 interface 的实现。初次看到看了好久才想明白。在这里记录一下。

以 Golang 内置库中的 server.go1 为例。Handler 的定义如下:

如果我们要定义一个 Handler,需要这么写:

有两个问题:略显啰嗦;距离函数内容最近的 ServeHTTP 是一个 interface 规定的具体的名字,这个函数名字不能变,但是又没有意义,所有的 Handler function 都要写成这个名字。

我们现在写 Golang 显然不是这么写的。我们会这样定义一个 Handler:

为什么我们可以这么写呢?因为源代码中有这样几行2

虽然这里的注释只有短短几行,但是意义深刻。

首先,第一行定义的 type HandlerFunc func(ResponseWriter, *Request) 让我们的 myHanlder 函数变成了一个 type HandlerFunc 类型。

然后,所有的 HandlerFunc 对象都有一个方法,叫做 ServeHTTP,这就实现了 Handler 这个 interface。实现的内容,就是调用对象本身,对象本身是一个函数,所以就是调用这个函数。

综上,所有符合 ServeHTTP(w ResponseWriter, r *Request) 签名的函数都可以转换成 HandlerFunc 对象,(虽然它是函数,但是函数也是对象。)即所有签名如此的函数,都可以是一个 Handler 了。

我们就可以这么写:

那么为什么不直接把 Handler 定义成一个函数呢?

就可以实现一样的效果了。

这是因为,Handler 可以变得很复杂,比如,Golang 的 middleware 本质上就是基于 Handler 的链式调用来实现的。复杂的 Handler 需要维护一些内部的状态,这种情况下,struct 就比 function 好用很多了。比如 httpauth3 这个库,就先初始化成 Handler 再使用。

那如果还是把 Handler 定义成一个 function,三方库规定在使用的时候,先初始化一个三方库定义的对象,然后三方库提供兼容 Handler 的函数,好像能达到一样的效果?

这样的话,多个 middleware 的入参和返回是不一样的对象,就无法串起来了。而如果把 Handler 定义成一个标准库里面的对象,就可以做到:middleware 接收的是一个 Handler,返回的还是一个 Handler4。只要 middleware 是这样的接口,它们就可以串联使用。

还有一个有趣的一点,Golang 里面不光函数可以实现 interface,任何类型都可以5。(Golang 还真是一切皆对象呢。)

  1. https://cs.opensource.google/go/go/+/refs/tags/go1.24.1:src/net/http/server.go;l=88 ↩︎
  2. https://cs.opensource.google/go/go/+/refs/tags/go1.24.1:src/net/http/server.go;l=2290 ↩︎
  3. https://github.com/goji/httpauth?tab=readme-ov-file#nethttp ↩︎
  4. https://github.com/goji/httpauth/blob/master/basic_auth.go#L153 ↩︎
  5. I read it from here: Functions implementing interfaces in go | Karthik Karanth ↩︎
 

Spegel 镜像分发介绍

网络系列文章许久不更新了,因为最近的工作比较忙,有很多其他的问题要解决。上个周花了很多时间研究 P2P,(和 web3 区块链无关的),(和民间 P2P 借贷也无关),(和下载盗版电影也无关,好吧,算是有点关系)。而且着实被「云原生」坑了一把,所以这篇文章介绍一下我们要解决什么问题,Spegel 这个项目是什么,怎么解决问题的。最重要的是…… 我会写一下如何从 0 启动并运行这个项目。读者可能会问,这不是按照项目的文档就能跑起来的吗?不是!这项目是云原生的,它居然没有文档,只有一个 Helm Charts1,命令行的参数看的云里雾里,如果不在 K8S 中运行这个程序,就只能结合它的 Charts 以及源代码来弄懂参数的含义。好在我功力深厚,已经完全掌握命令行的启动方式了,接下来就把它传授给读者。

问题

在部署容器的时候,node 要从 image registry 来 pull image。如果要部署的规模非常大,比如 2000 个 container,那么就要 pull 2000 次,这样对 image registry 造成的压力就非常大。

在规模比较大的软件分发问题中,自然而然会想到 P2P 的方式来分发。如果要下载 1000 次,可以先让 10 个节点下载完成,然后后续 100 个节点从这 10 个节点下载,每一个并发是 10,然后其余节点从这 110 个节点下载。这样,每一个节点的最大并发都不会超过 10,就解决了中心点的性能瓶颈问题。

我们尝试过的 P2P 方案是 Dragonfly2, 这个工具以透明代理的方式工作3,透明代理会首先通过 P2P 查找资源,如果找到,优先从 P2P 网络进行下载。下载完成后,会另外保存一份「缓存」,在后续一段时间内(可配置,默认 6 小时,时间越长,文件生效的时间越长,但是占用的磁盘空间越大),会给其他请求下载的节点来 serve 文件。如果整个 P2P 网络都没有目标文件,那么就会回源下载。而且文件支持分段下载,比如一个文件 1G,就可以并行地从 10 个节点分别下载 100M。

透明代理的设计非常好,不光是镜像文件,像 Python 的 pip 下载 tar 包,apt 下载 deb 文件,部署其他的 binary 文件,都可以通过这个 P2P 网络来加速下载。

但是这个设计也有一些弊端,尤其是在镜像下载的场景。

比如进行故障切换的时候,要对目标 IDC 的服务进行扩容,需要同时扩容 100 个服务,那么这 100 个服务都需要请求到镜像中心,因为这 100 个服务都是不同的镜像,P2P 网络中现在没有缓存。或者对一组服务进行紧急扩容的时候,也会因为缓存中没有镜像而全部请求到镜像中心。

在多个数据中心部署的时候,由于无法事先得知一个 IDC 需要部署哪些服务,就得把所有的镜像都同步到这个 IDC,极其浪费带宽。如果不同步的话,那么每次部署都要跨 IDC 来拉取镜像, 延迟比较大。今天部署新的服务,跨 IDC 拉取镜像部署完成。明天一个机器挂了,容器要部署到另一个机器上去,但这时候 P2P 网络中的缓存已经删除了,所以又需要跨 IDC 拉取镜像部署。

以上的问题,按照 dragonfly 的缓存设计,不太好解决。

可能的方案

这个问题的本质,其实是镜像太大了,假设一个服务的镜像只有 10M4,算了,算 100M 吧,那么这些问题都不是问题,1000 个节点全部去镜像中心拉取,也才 10G,跨 IDC 也都能接受。

但是很多人打的 image 什么都往里放,golang 编译器也放进去,gcc 也放进去。甚至做基础镜像的团队也把很多不用的东西都放进去了。导致 image 最终就是 1G 的正常大小,甚至还有 10G 的5

Image lazy loading

一个很新奇的想法是,既然 image 很多的文件都用不到,那么就只下载用得到的文件,其他的文件等读到的时候再去下载。比如 Nydus6,把 image 进行文件级别的索引和分析,启动的时候只下载必要文件,其他的文件等 access 的时候再通过 P2P 网络下载7

从 containerd 来 serve

既然 image 在其他部署的机器上已经存在了,那能不能直接从其他的机器来下载呢?

通过 containerd 把已经存在的 image 读出来是可能的8。于是我们就想尝试写一个程序:

  • 可以把本地 containerd 的 image 暴露出来;
  • 需要下载 image 的时候,优先从其他的机器上直接下载;
  • 需要做一个服务发现服务让不同的节点知道 image 的 blob 都存在于哪些机器上。

然后就发现了 Spegel 这个项目9,简直和我们想做的事情一模一样!

Spegel 做了上面的三件事:

  • 暴露一个 HTTP 服务可以提供下载本机的 image;
  • 对 containerd 做 image mirror,containerd 想要 pull image 的时候,会被 spegel 代理到去其他机器下载;
  • 如何发现谁有什么镜像呢?服务发现是用的 P2P 网络,但是并没有用 P2P 来存储数据,本质上,只是用 P2P 来做了服务发现。Spegel 会定期把本地的 image 广播到 P2P 网络中,需要 image 的话,也会从 P2P 网络中寻找 Provider。

运行 Spegel

环境准备

首先需要准备一个 containerd 的运行环境。也可以直接安装 docker。

然后需要安装 golang 用来编译项目的 binary,直接安装 golang 的官方文档进行编译即可。

下载项目进行编译。

这里编译的是版本是 v0.0.28,因为最新的版本 v0.0.30 我没成功运行起来,会遇到错误:failed to negotiate security protocol: remote error: tls: bad certificate。有一个 issue 说是因为 IPv4 和 IPv6 dual stack 的问题10,但是尝试关闭 IPv6 也于事无补。

软件的 binary 准备好了,就可以准备运行起来了。

Containerd mirror 配置

Spegel 启动的时候会检查连接的 containerd 是否已经配置自己为 mirror,如果没有配置,会拒绝启动。Containerd registry config path needs to be set for mirror configuration to take effect

首先配置 CRI 的 config path。

然后用 Spegel 提供的配置命令来创建 registry 的 mirror 配置。这个命令只是修改配置文件,你也可以自己修改。

其中:

--containerd-registry-config-path:是要写入配置的目录,因为上面配置中 containerd 也是使用了这个目录,所以在这里 Spegel 就要修改这个目录。

--registries:是对什么域名配置 mirror。

--mirror-registries:Spegel 提供 image mirror 服务的地址。这里写什么地址,接下来启动 Spegel 的时候就使用什么地址。

--resolve-tags=true:标记 Spegel 的地址有 resolve tag 的能力。

这个命令执行过后,会看到多了一个文件:/etc/containerd/certs.d/registry-1.docker.io/hosts.toml,内容是:

启动项目

下一步就可以启动项目了。

启动的命令是:

参数的含义如下:

  • registry 表示启动 registry 服务。spegel binary 只支持两个子命令,另一个就是上面用到的 configuration
  • --mirror-resolve-retries 表示 Spegel 在解析 image 的时候最多重试几次;
  • --mirror-resolve-timeout 表示解析 image 的时候多久会超时;
  • --registry-addr 指定 Spegel 在本地提供 image 下载服务的地址,containerd 会从这个地址来下载镜像,如果失败,就 fallback 到镜像中心;
  • --router-addr Spegel 会 listen 这个地址来接收来自 P2P 网络的请求;
  • --metrics-addr 这个地址可以访问 /metrics 以及 golang 的 pprof 文件;
  • --containerd-sock containerd 的客户端通过这个 socket 文件访问 containerd,本地已经存在的 image 也是通过这种方式访问到的;
  • --containerd-registry-config-path 和上面一样,但是这个值在代码中并没有实际用到;
  • --bootstrap-kind 加入一个 P2P 网络,至少需要认识一个已经存在 P2P 网络中的节点,通过已经存在于 P2P 的节点来加入网络。这里是指定发现节点的方式,是 HTTP;
  • --http-bootstrap-addr Spegel 启动之后会 listen 这个地址,提供 HTTP 服务。只有一个 path /id,访问这个 path 会返回自己的 multiaddr11。即,其他节点可以把本 node 的 --http-bootstrap-addr 来当作 bootstrap http 地址,这个配置是让本节点对其他节点提供服务;
  • --http-bootstrap-peer 指定其他节点的 --http-bootstrap-addr,来加入 P2P 网络;
  • --resolve-latest-tag 是否解析 latest tag,因为 Spegel 本质上是 image 缓存分发,如果 latest 修改了,那么 Spegel 解析到的 latest tag 可能是过时的;
  • --registries 对什么镜像地址进行 mirror;
  • --local-addr 本机的地址,实际上没有 listen,在代码中只是在获取到其他节点地址的时候,用这个配置项来比较是不是自己,过滤掉自己的地址,(用于有 NAT 等的复杂环境);

启动一个节点之后,再去另一个节点修改一下地址相关的参数,启动,就可以得到一个 2 节点的 Spegel 网络了。

使用 crictl 来测试

好像使用 docker 不会走到 contianerd 的 image 下载逻辑,所以我们下载一个 crictl 直接来操作 containerd。

安装 crictl:

在第一个节点下载:

耗时 3m 左右。

然后再去另一个节点执行同样的命令,会看到耗时 20s 左右,提升已经很明显了,而且会极大减少镜像中心的压力。

  1. Spegel 的 Charts:https://github.com/spegel-org/spegel/tree/main/charts/spegel ↩︎
  2. Dragonfly https://d7y.io/ ↩︎
  3. 架构图:https://d7y.io/docs/#architecture ↩︎
  4. 一个 Redis 都几 M 就够了 Build 一个最小的 Redis Docker Image ↩︎
  5. 这就是去年年终总结说过的问题,如果人人都是高级工程师,那么问题就不存在了 ↩︎
  6. 项目主页:https://nydus.dev/,也是 dragonfly 的一个项目 ↩︎
  7. AWS 等云厂商也有类似的技术 Under the hood: Lazy Loading Container Images with Seekable OCI and AWS Fargate | Containers ↩︎
  8. Containerd API:https://pkg.go.dev/github.com/containerd/[email protected]#Client.ContentStore ↩︎
  9. 项目代码:https://github.com/spegel-org/spegel,项目主页:https://spegel.dev/ ↩︎
  10. “could not get peer id” and timeouts since 0.0.29 · Issue #709 · spegel-org/spegel ↩︎
  11. https://docs.libp2p.io/concepts/introduction/overview/ ↩︎