Hi,欢迎来到 卡瓦邦噶!我是 laixintao,现在生活在新加坡。我的工作是 SRE,喜欢在终端完成大部分工作,对各种技术都感兴趣。我从 2013 年开始写这个博客,写的内容很广泛,运维的方法论,编程的思考,工作的感悟,除了技术内容之外,还会分享一些读书感想,旅行游记,电影和音乐等。欢迎留下你的评论。

声明:本博客内容仅代表本人观点,和我的雇主无关。本博客承诺不含有 AI 生成的内容,所有内容未加说明均为博主原创,一经发布自动进入公有领域,本人放弃所有权利。转载无需本人同意。但是依然建议在转载的时候留下本博客的链接,因为这里的很多内容在发布之后会还会不断地继续更新和追加内容。 Not By AI

AoC 2025 通关留念

[剧透警告]

今年又来玩了 Advant of Code1,最近看 C 语言的代码比较多,就用尽量用 C 语言做。

第 9 和和第 10 天最难,尤其是第 10 天,看着挺简单,实际是从来没有听说过的整数线性规划问题2

第 11 天一看要用 dict,放弃 C 了。用 Python 很快就过了,一个搜索加路径缓存即可。但是如何用 C 写出来完全没有思路,最后问 chatGPT,想起来还有邻接表这种巧妙的表示方法。

第 12 天最搞笑,以前是做 part1 顺利,做 part2 的时候时间爆炸。day 12 做 sample 的时候就感觉时间要爆炸了。加了几个剪枝,part1 居然过了。我想完了,part2 肯定是让我找所有的可能的情况,一定会爆炸。结果点开 part——就通关了。今年居然只有 12 天?那距离圣诞节还有 13 天呢,我做啥?

  1. https://adventofcode.com/ ↩︎
  2. 高铭骏 写的:整数线性规划 ↩︎

fuglede 的代码太优雅了:https://github.com/fuglede/adventofcode/tree/master/2025,不愧是做量化的。

 

如何把网络设备从 traceroute 中隐藏

在和朋友一起吃饭的时候,A 提了一个有意思的问题:怎样可以把一个机房内的路由设备从互联网「隐藏」呢?

「隐藏就是没有人可以知道这个设备的 IP 地址,这还不简单,只要禁用 ICMP 就可以了」, B说。

A 说,这样是可以。很多安全团队在实施起来也确实是这么做的,但是这样并不好:机房内所有的 IP 都无法 ping 通了。这样会增加 debug 的难度,得不偿失呀!

C 说,那就依然转发 ICMP 包,但是如果是 TTL=1 的包,就不要回复 ICMP Time Exceeded 了。

B 说,人家要的是「隐藏」,要是像你说的这么做,别人还是知道中间有一个设备的存在,没有完全符合要求。

一个 traceroute 的例子:第7跳直接丢弃 TTL=1 的包,不返回错误1

事实是这样的。假设一个简单的物理拓扑是 A -> B -> C,B 不回复 ICMP Time Exceeded,那么 traceroute 看起来就是 A ? C,可以猜测得到中间有一个路由器,但是已经禁止回复 ICMP Time Exceeded。看起来像下面这样。

C 说,traceroute 的原理是发送 TTL=1, 2, 3, … 的包,不断让路由器回复 ICMP Time Exceeded 信息,来得到每一跳的 IP 地址。要想完全隐藏,只需要:

  • 自己不回复 ICMP Time Exceeded
  • 让下一跳回复,仿佛下一跳就在自己的位置;

这样就可以完全隐藏了。要达到这个目的,只需要:

  • 对于 TTL=1 的包,不是丢弃,而是转发给下一跳,并且 TTL 依然保持为 1,即可。A ---[TTL=1]---> B ---[TTL=1]---> C, 对于客户端的 traceroute,看起来就像:A → C。

这样(理论上)好像确实可行了。三人对这个结论满意了。

后来我把这个讨论记录在了博客上(你现在正在阅读的一个),一位读者马上就发现了问题:可是这样 C 会出现两次吧!

确实是这样,假设在 A -> B -> C 的链路中:

  • TTL = 1 从 A 进入的时候,A 会在 ICMP 中回复自己的 IP;
  • TTL = 2 从 A 进入的时候,B 会直接转发给 C,C 会在 ICMP 中回复自己的 IP;
  • TTL = 3 从 A 进入的时候,B 会 TTL -1 转发给 C,C 会在 ICMP 中回复自己的 IP;

这样 C 就出现了 2 次!

看来,B 必须完全不减 TTL,直接转发,才能隐藏自己。不过这样就有出现环路2的风险了。

  1. 使用 mtr 检查网络问题,以及注意事项 ↩︎
  2. 网络中的环路和防环技术 ↩︎
 

博客维护:升级到 Ubuntu 24.04

这个博客在四年前做了一次迁移1,目前是架设在一台 DigitalOcean 的 VPS 上,前面用 Cloudflare 作 CDN,得益于我选择的这两家公司非常靠谱(但是最近 Cloudflare 的事故2 3让我这个 CF 吹有一些尴尬),自从搭建起来之后,我几乎没有维护过。

最近后台一直提示我 PHP7.4 EOL 了,今天终于打起精神来决定升级一把。

一不做而不休,干脆直接把 4 年半前启动的这台 VPS 全升了吧:

  • Ubuntu 升级到 24.04;
  • Mysql 升级到 8.0;
  • PHP 升级到 8.3;

因为我是专业的 SRE,所以这次升级读者感受不到任何区别。

整体比较顺利,唯一遇到的问题是,我用的 wordpress theme 太老了,一样的代码居然在 PHP8.3 挂了。看提示是函数参数少传了,在 PHP7.4 是 Warning,在 PHP8.4 是直接 Fatal。

本来打算放弃治疗,直接用一个新的 wordpress 官方主题得了,省心。结果一个二〇二五这些主题,都是什么玩意,行距看着都难受。又回来决定修好主题的代码。得亏 ChatGPT,没想到意外地顺利,很快就跑起来了,目前也没发现什么问题。

安全方面上顺便做了一个加强,以前的架构是:域名解析到 Cloudflare,Cloudflare proxy 到我的 Nginx,Nginx 只接受 Cloudflare 的 IP4,其他的一概拒绝。自己以为很安全了,没有人知道我的真实 IP。然后自己一查,居然早已经暴露了。

censys 的查询结果

我也不知道什么时候暴露的。

这次直接用了 cloudflared,原理是,我的服务器 Nginx 只 listen localhost 的端口,我的服务器安装一个 cloudflared,cloudflared 会去主动连接 cloudflare,这样,在 cloudflare 收到请求的时候,会通过 –> cloudflared –> nginx 转发到我的机器上。有点像 FRP5 穿透。如此一来,我的 IP 完全没有暴露在公网上了。过段时间再去搜索一下,看暴露了没有。

欢迎读者留言 ; D

  1. 博客迁移到 Cloudflare ↩︎
  2. Cloudflare outage on November 18, 2025 ↩︎
  3. Cloudflare outage on December 5, 2025 ↩︎
  4. Cloudflare 的 IP range:https://www.cloudflare.com/ips/ ↩︎
  5. https://github.com/fatedier/frp ↩︎
 

Harbor GC 问题

最近的工作比较忙,以至于网络技术的系列文章1许久不更新了。这几天在解决的问题是镜像存储服务 Harbor2,存储的 docker image 太多了。

虽然我之前在博客里面分享了一些 Docker image 构建的技巧3,以及炫耀了构建一个最小的 Redis Docker 镜像才不到 2MiB4,但是无奈,我的博客基本没有人看,所以同事上传的 image 都非常可怕,动辄就上 G,20+ GiB 的都有。现在我们的 Harbor 存储已经是 PiB 级别了。

多余的 image 就删除就好了,问题就在于,删除 image 比较复杂。分成几个步骤:

  1. 删除 image 的 Tag5
  2. 扫描整个数据库,找到没有被任何其他 image 和 tag 引用的 blob;
  3. 删除这些未被引用的 blob;

第二步尤其重要,简单来说,image 是分层的,一层就是一个 blob,一个 image 可以引用多个 blob。比如服务 A 的 Dockerfile 开头是 From: ubuntu:24.04,另一个服务 B 的 Dockerfile 开头也是 From: ubuntu:24.04,那么这两个 image 都是引用了 ubuntu 的 blob。删除服务 A 的 image 的时候,不能把 A 的 blob 都删除,因为这样的话 ubuntu 的 base image 就连带被删除了。所以我们在删除一个 image 的时候,其实并没有释放任何空间,而只是删除了 image 对 blob 的引用。这时候还不知道哪些 blob 是可以释放的,要知道哪些 blob 可以删除,就必须扫描全部的数据库,找到没有任何引用的 blob,才可以删除。难题就在扫全表这里。

这个问题就和编程语言的 GC 问题很像,不过更加简单一些,因为引用只存在于 tag 到 blob,tag 之间和 blob 之间不存在引用,也就没有环的问题。

引用计数

引用计数比较合适这个场景,因为没有环路,所以引用计数到 0 就可以直接删除,不需要扫表找孤零零的环。但是 Harbor 本身没有用这种方案,估计是因为引用记录维护起来比较难,必须准确并且处理好并发,处理不当很容易有数据误删或者出现永久的垃圾。

Mark and Sweep

这是官方的代码采用的方案,基本思路是,扫描所有的 image,对它们引用的 blob 标记为在使用中。扫描完成之后,所有从未被标记过的 blob 直接删除。

问题

如果直接用 Harbor 的 GC 方案,那么运行一次 GC 需要超过一个月的运行时间(不知道具体需要多久,因为从来没有成功跑完过)。之前的负责人设计了一个很聪明的方案,基本思路是,找到系统性能低的瓶颈,然后针对性地处理这些瓶颈。

对于前面的 3 个步骤:

  1. 删除 image 的 Tag:直接用 SQL 从数据库查询出来 image,判断是否需要保留(规则是每一个 image 只保留最近的 3 个版本),如果不需要保留,通过 API 删除;
  2. 扫描整个数据库,找到没有被任何其他 image 和 tag 引用的 blob:这一步因为是 Harbor 代码的 GC 逻辑,比较负载,还是通过 web UI 来触发的;
  3. 删除这些未被引用的 blob:Harbor 本身 sweep 的过程很慢,原因是没有并发,一个一个删除的,改进是直接通过并发删除。

这样,整体运行一次只需要一个月。

目前还是存在很多问题。我接受之后又做了一些改进:

  1. 之前的 PIC 显然是一个脚本大师,所有的工作都是通过 bash,awk,curl 这些工具完成的,每一步都需要人工操作 -> 等待完成 -> 人工操作下一步,比如到 mark and sweep 的这一步,需要人工去页面上触发 GC,然后关注执行的进度,在执行到 sweep 阶段的时候手动结束,开始运行下一步的脚本;我写了一个 300 多行的 Python 脚本,把所有的步骤串起来,这样就有了 crontab 定期执行的条件。
  2. 在第一步删除 image 的时候还是很慢,30s 只能删除一个 image,我们有千万个 image。解决办法是读了 harbor 的代码,发现 blobMgr.CleanupAssociationsForProject 这一步其实是最费时间且多余的,后面执行 GC mark 的时候一定会运行一遍。删除这个逻辑之后只需要 0.1s 就可以删除一个 image;
  3. 最后一步通过 API 删除 S3 上的数据,之前还是脚本用 curl 触发,速度太慢。使用 Python 之后就可以用 connection pool 并发删除了;
  4. 还做了其他的功能,比如支持不同的 project 自定义删除逻辑,「删除最近1年没有 pull 记录的 image」这种。

本质上是用最少的改动自动化原来的 GC 逻辑,目前运行一次的时间是 3 天。已经足够满足需求了,因为不需要人工执行,所以 3天和 3 个小时区别不大。

上一个负责人留下的文档详细记录的 Harbor GC 的逻辑以及改进点,比 Harbor 官方的文档还要详细。有了这些我半重写 GC 的逻辑就简单很多。

在他之前,是另一个负责 Harbor 的同事。阅读代码并找到瓶颈是需要很大的勇气的,且不一定行得通,可能花了很大的力气,最后发现这个事情做起来就只能这么慢。

但是问题还是要解决。所以他那时用了另一个有意思的方案:

  1. 搭建另一套一模一样的 Harbor 集群,复制以前的用户名,权限,project 等数据,但是把 blob 和 image 数据删除;
  2. 搭建一套 Nginx 代理,Nginx 转发逻辑是:
    • 对于 push,转发到新集群;
    • 对于 pull,先 pull 新集群,如果得到 404,就转发到老的集群,这样以前的数据都可以读;
  3. 在 1年之后,完全删除老的集群;

这是一个很有意思的「用运维手段解决技术问题」的例子,在 SRE 的工作中,迫于没有对软件的实现的控制力,我们经常需要用运维手段来解决代码实现上的问题。

  1. 计算机网络实用技术 ↩︎
  2. https://goharbor.io/ ↩︎
  3. Docker 镜像构建的一些技巧 ↩︎
  4. Build 一个最小的 Redis Docker Image ↩︎
  5. https://docs.docker.com/reference/cli/docker/image/tag/ ↩︎
 

大巴

我上初中的时候,村里去县城唯一的公共交通就是一辆大巴车。「大」也说不上,应该叫中巴车。车是邻村一个叫王文义的人买的。他把车停在我们村,每天早上骑摩托车从自己的村子跑到我们村,然后以我们村为起点,一路接上邻村的人,去县城的停车场。从我们村走,可以拉更多的人。走到王文义的村子,顺路接上王文义的老婆,王文义的老婆在车上卖票,王文义开车,一张票 7 块,过年的时候一张票 10 块。

我 12 岁的时候上初中。我们村的学生大部分都去镇上的中学读,我爸重视教育,送我到县城读。每天就是坐这个中巴车去学校,在学校住 12 天,每两个星期回家一次。刚开始每天都想家,住在学校里很不习惯,12 个人一间宿舍,没有办法洗澡,宿舍臭烘烘的。周五下午离开学校,坐在车上,是心情最好的时候,因为从现在开始局距离学校越来越远了。周日的下午在车上,是心情最差的时候,在家收拾好东西,上了车,就没有退路了,只有一条去学校的路。

读高中的学生也是坐这辆车,需要去县城的村民也是坐这辆车,所以每周日下午格外人多。座位上全部坐满,车的走道也是挤满了人, 挤得满满当当,超载了两倍还多。

车比较破,车内的地板是一张铁皮,有的地方还破了小洞。有一次我坐在破洞的旁边,怀着郁闷的心情,透过洞看路上的小石子向后飞去。奔驰的客车,我和路面只有一张铁皮之隔。

后来王文义换了一辆新的宇通牌客车,涂着崭新的绿色油漆,座位也没有污渍,比以前也大了,可以叫做「大巴车」了。

高中生是周日中午回学校,初中生是周一早上。冬天天短,起个大早去坐车的只有我一个初中生。有一个冬天的早晨,格外的冷,我去车站的时候还伸手不见五指。我到车站,等了一会,王文义骑着摩托车来了。还不到发车时间,他打开门让我上车等,然后去发动车子。却发现油箱被冻住了,车发动不起来。他让我在上面坐着,我从玻璃看到他从附近的人家门外扯了一把干草,点了火塞在油箱下面烤,也顺便点了一只烟。看到这我就不淡定了,赶紧下了车,站在他旁边,看着火焰在车底燃烧,想象着发生巨大爆炸,把这几吨重的铁皮炸到天上的情形。过了一会,王文义的烟抽完了,回到驾驶室尝试发动,结果还真发动着了。他下车灭了火,就起步去县城了,车上还是只有我一个人。

3 年之后,我开始读高中,还是每两个周坐他的车上学,回家。生活还是一样的麻木,每天学习超过18个小时。最喜欢周六的中午坐车回家,最讨厌周日下午坐在回县城的车上。每两周的休息时间只有不到一天。

后来我去读大学,就再也没有坐过他的车了。每次回家我坐飞机去机场,然后坐芳姐的车去滨海酒店。父亲会去滨海酒店等我,开车带我回家。

芳姐是我哥介绍给我的,任何时候需要去坐飞机,或者从机场回家,只需要在微信上和芳姐说一声,芳姐安排一辆车点对点送到机场,除了我还会顺路接上其他需要去机场的人,一个人 60,7 座车跑一趟可以赚 360。 这些人常年跑机场,从来没有误过飞机。

只是过年的时候人流量大,活也多,一次司机和我说他三天只睡了6个小时,让我听了有些害怕。

大学快毕业的时候,村里就通了公交车,去县城价格更便宜了,但是为了连接更多的村庄,公交车也绕了,原来需要 40 分钟,现在要至少一个半小时。王文义的车也再也没有出现在我们村里。