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

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

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/ ↩︎
 

pwru 工具介绍和案例一则

pwru1 是排查 Linux 网络问题最好的工具,全称是 Packet where are you?

它是怎么工作的呢?

eBFP 可以让我们往 kernel 的函数上添加 hook,当这个 kernel 函数执行的时候,我们可以通过 eBPF 定义一些额外的动作,比如把这个函数和它的参数记录下来然后打印出来。

Linux 启动的时候,会生成 /proc/kallsyms,pwru 通过读取这个文件,找到所有和 skb (网络包在内核中的数据结构)相关的函数,然后 hook 这些函数。这样,在这个包在内核栈经过的路径,就可以用 eBPF 追踪到了2 3

通过这种方法,pwru 几乎可以解决在 Linux 上遇到的任何网络不通的问题,因为通过函数路径可以很快确定这个包经过了哪些函数处理,没有走到哪些函数。对照函数查看源代码,就可以知道原因了。

它的安装方法很简单:apt install pwru 即可4

然后通过 pwru icmp and dest host 1.1.1.1 就可以开始追踪了。包的过滤语法和 tcpdump 一样。

案例分享

今天遇到的问题,还是网络在 Linux 上网络不通了,网络结构很简单,就是默认路由到一个 vxlan driver 的 interface,经过这个 interface 封装,然后通过物理 interface 发送出去。这种网络不通的问题最适合用 pwru 定位了,这个工具可以直接告诉我们包在 Linux 网络栈经过的代码路径。

我们用以下命令来抓这个包,pwru --filter-track-skb --all-kmods dst 10.1.1.100,得到的输出如下:

pwru 抓包的输出

可以看到数据包的确被 Drop 了,虽然原因是 SKB_DROP_REASON_NOT_SPECIFIED,但是不要紧,我们可以看到 drop 之前的函数是 vxlan_get_route。然后就可以去查看对应的 Linux 源代码5

从源码中可以确认,这个 vxlan_get_route 函数返回错误的原因有 2 个:

  • 一个是 vxlan 封包之后使用的 dev 和现在一样,那就是出现环路了,会导致一直封包一直循环;
  • 另一个就是没有路由;

按照包的封装过程查看 ip route,发现确认有一条多加的路由导致环路了,删除即可恢复。

如果没有 pwru 的话,就得依靠网络的经验逐个地方检查,然后判断出来是路由表配置问题。但是有了 pwru,就可以顺藤摸瓜得排查到根因。

  1. https://github.com/cilium/pwru ↩︎
  2. https://cleveruptime.com/docs/files/proc-kallsyms ↩︎
  3. https://github.com/cilium/pwru/blob/db786876d10bc104be1a7908e13902c890e548d0/internal/pwru/ksym.go#L45 ↩︎
  4. 这个是我司在内网打包的,公网不能这么安装,用户需要按照 Github 的文档来安装。 ↩︎
  5. 相应的代码地址:https://elixir.bootlin.com/linux/v5.15.178/source/drivers/net/vxlan/vxlan_core.c#L2428 ↩︎
 

一个由 BGP Route Aggregation 引发的问题

上周遇到的一个问题很有意思,后来搜索相关的资料,找到的也比较少,感觉有必要记录一下。

问题的场景很简单:我们有两个路由设备同时发布了 10.81.0.0/16 的网段做 ECMP1,网络一切正常。拓扑如下图。现在,有一个新的 IP,只存在于 Router A 上,所以 Router A 宣告网段 10.81.100.100/32,而 Router B 不宣告。这样,由于在路由表中,/32 的 prefix 比 /16 要长,所以 Router X 在从路由表选路的时候,10.81.100.100 会优先选择去 Router A,而对于其他的 10.81.0.0/16 的网段,会负载均衡到 A 和 B 两台路由器上。

简化的拓扑图

理论上,一切看似合理并且正常。但是 /32 的网段一经宣告,10.81.0.0/16 的网络都挂了。

事后我们得知,在 Router X 上有一条路由聚合配置。但是这条合理的路由聚合,怎么会让整个网段挂掉呢?

BGP 路由聚合

为什么需要路由聚合呢?

Router A 每次宣告一个网段给 Router X,Router X 的 BGP 路由就会多一个。Router B 每次宣告一个网段,X 上也会多一个。可想而知,Router X 上的路由是它的下游的总和。同理,Router X 上游的路由器的路由将会更多。路由的条目越多,对路由器的性能要求就越高。所以,核心路由器要想处理所有的路由条目,就需要性能非常高。性能是有上限的,假设性能再搞也无法处理这么多路由,怎么办呢?我们可以优化另一个变量——路由条目2

如何减少路由条目呢? 考虑下面 3 个网段:

  • 10.81.2.0/24
  • 10.81.3.0/24
  • 10.83.4.4/26

其实都可以汇聚成一个网段:10.81.0.0/16。把这个网段宣告出去,收到的流量可以在 X 这里根据自己的路由表进行转发。

这里产生了一个问题:就是我们宣告了自己没有路由的网段出去,比如我们的路由中并不存在 10.81.5.0/24 这个段,但是被我们的 10.81.0.0/16 宣告了出去。

由此,会产生两个问题。第一个问题,假设其他路由器有到 10.81.5.0/24 的路由,那么会不会走到我们的 10.81.0.0/16 这里来呢?答案是不会的。因为路由表的匹配规则是最长前缀匹配/24 比我们的 /16 优先级更高。

第二个问题更加严重一些,路由的聚合可能导致环路3

考虑下面这个拓扑图,两个路由器都存在路由聚合的配置。

路由聚合导致环路产生的例子

这里的问题是,10.81.4.1 这个 IP 不存在于 A 也不存在于 B,但是由于路由聚合的配置,A 认为在 B 上,B 认为在 A 上,导致在转发的时候会出现环路。虽然 IP 层有 TTL 机制,会让这个包最终被丢弃,但是也会让两个路由器在某些网段的转发上浪费一些计算资源。

如何避免在转发「不存在的网段」的时候出现的环路呢?一个思路是我们精确的控制聚合的配置,不配置出来可能产生环路的聚合,但是这几乎是不可能的。(就像用静态路由配置替代动态路由一样不可能)。

另一个思路是,在 10.81.4.1 这种本地没有路由的包出现的时候,直接「黑洞」掉。方法很简单,就是在每次聚合的时候,创建一条路由,终点是 Null0,即直接丢弃。

具体来说,在上图的 Router A 中,聚合本地的三条路由到 /16,我们应该这么做:

  • 向外宣告路由 10.81.0.0/16,以达到减少路由条目的目的4
  • 在本地插入一条 Null0 的路由,使得本地的路由最终如下。

注意,路由表的顺序没有意义,因为用的是最长前缀匹配。转发包的时候,对于 10.81.1.0/24 这种本地存在的段,因为它们的前缀比 /16 长,所以正常转发;对于不存在的段,比如 10.81.4.1,会命中 10.81.0.0/16 -> Null0 的路由,直接在本地丢弃。这样,就可以阻止环路的产生。由聚合而自动产生的 /16 是一个防环的兜底路由,正常情况下,不应该使用这条路由,如果命中这条路由,说明无法转发的包到达了路由器,直接丢弃即可。

回到本文开头的问题上,为什么宣告一条 /32 会导致整个网段挂掉呢?Null0 不是说只是兜底而已吗?回答这个问题,还要补充一点知识。

BGP 和路由表

路由设备按照路由表(叫做 RIB, Routing Information Base)进行转发(实际上还有一层加速用的 FIB,但是 FIB 的 source of the truth 是 RIB,所以这里先忽略)。RIB 转发的逻辑是最长前缀匹配。

RIB 是怎么生成的呢?一种是静态配置,即静态路由。另一种是动态路由协议。路由协议之间交换路由信息,然后负责动态修改 RIB。在有多条可达路由的时候,怎么决定把哪一条路由写入到 RIB 呢?这就是不同的路由协议来决定的了。比如,BGP 有 13 条选路原则5;OSPF 和 IS-IS 这种协议也有自己的路径选择算法。

路由协议和 RIB 的关系6

这张图比较好,不同的路由协议可以同时运行,不同的路由协议可以根据自己的算法来操作路由表,决定转发路径。

路由的聚合也是路由协议的一部分。像 OSPF, EIGRP, BGP 这些协议,都有关于路由聚合的定义和支持。重申一下:路由聚合是路由协议的 feature,而不是路由表 RIB 的。

这也就是说,路由聚合中产生的 Null0 黑洞条目首先出现在 BGP 中,然后 BGP 根据自己的选路原则,放到路由表中。

回到本文最先开始讨论的问题,现在就可以用上面的知识来解释这个问题了。

首先,Router X 会收到 3 条路由。

到达 Router X,经过聚合之后,在 BGP 里面,会有 4 条路,多出来的一条是聚合产生的 Null0 黑洞路由。

到达 10.81.0.0/16 的路由有 3 个

BGP 会按照自己的选路原则,在 10.81.0.0/16 的 3 条路径中选择一条放到 RIB 中。这 3 条路径中,Null0 这条可是本地路由,Weight 是最高的。所以,Null0 由于其他两条真实存在的路由,进入了 RIB。

show ip route

可以看到路由表中,只有 10.81.100.100 明细路由和 10.81.0.0/16 到 Null0 的黑洞路由,其他两条路由被刷下去了。

到这里,真相就大白了。10.81.100.100 在没有发布的时候,10.81.0.0/16 工作正常。但是一旦发布,10.81.0.0/16 的正常路由就被路由聚合产生的 Null0 给刷下去了。

  1. 数据中心网络高可用技术:ECMP ↩︎
  2. Understand Route Aggregation in BGP – Cisco ↩︎
  3. 网络中的环路和防环技术 ↩︎
  4. 确认了下没有写错,这里的意思是 Tiao Mu De Mu Di,博大精深的中文! ↩︎
  5. Select BGP Best Path Algorithm – Cisco ↩︎
  6. 来源:FIB表与RIB表的区别与联系 – &Yhao – 博客园 ↩︎
 

2024 年的总结

今年依旧是平静而充实的一年。

特别喜欢 Armin1 的这一段话:

Whether it’s working on a project, solving a difficult problem, or even refining soft skills like communication, the act of showing up and putting in the hours is essential. Practice makes perfect, but more so it’s all about progress rather than perfection. Each hour you spend iterating, refining, failing and retrying brings you closer to excellence. It doesn’t always feel that way in the moment but when you look back at what you did before, you will see your progress. And that act of looking back, and seeing how you improved, is immensely rewarding and in turn makes you enjoy your work.

「注重进步,而非完美」。当知道自己每天在朝着自己的目标进步的时候,每天都有一种充实的感觉。

生活上

年初体验了邮轮2,今年旅行去了东京3。然后去了马六甲和吉隆坡,马六甲和吉隆坡的旅行比较随意,说去就去了,路途上也没有特别惊艳的地方,就连博客都没有写。今年去了很多次新山,新马通道支持自助过关了,方便了很多。

工作

今年有意控制自己喜欢创造新的项目的冲动,所以新的项目开启的不多,自己写了一个有意思的玩具,只不过从来没有公开过,属于自娱自乐,除此之外就没有什么新的项目了,Github 空空,只有偶尔维护一些已有的项目。

大部分的精力花在了工作上,处理了非常多的问题,有意思的一些已经纪录在了博客上,所以今年博客产量稍微多一些。

一如即往,今年在公司里也有一些新项目的尝试。比较自豪的是写了一个 IP 信息查询系统。可以查询一个 IP 对应的设备,机器,容器,虚拟机,VIP,等等,集成了 20 多个系统,并且用了并发加载,后端只用了 500 行左右,虽然小,但是用户体验很不错,个人很满意。

也有一些想法,比如经常在讨论某某技术的时候,一些大公司的人会评价说「这种开源的方案不适用我们 XX 公司的规模,规模一大就不行了。」然后发明一种新的技术宣称能解决超大规模的问题。我发现很多时候规模大是有用户滥用的本质问题的。比如监控系统的 metrics,遇到过好几次,低质量的 metrics 占用了 50% 的存储资源,只要用户优化一下4,就可以节省很多资源;还有镜像存储服务,我们的镜像存储服务的压力越来愈大,但是用户的 docker 镜像的优化空间还很大5,甚至很多镜像中存储了很多不需要的内容。去解决技术问题无法从根本解决滥用问题,规模扩大到多大总有一天还是会遇到规模问题,不如从根本解决用户计费问题来的根本。现在认为,即使是内部产品,也要像公有云那样的思路去做,不能提供免费的服务。另一个思路也能解决问题,就是只招聘专家,每一个人都知道自己在做什么,不做出来浪费的事情。

另一个想法是关于运维系统的透明,即我们做操作所使用平台。我越来越喜欢 Jenkins 了,因为它足够透明和简单,当运维操作失败的时候,我只看 Job 的日志就可以了,从来不需要去排查 Jenkins 自己内部的问题。但是最近公司总是冒出来一个又一个的团队,想要用代码开发出来一个运维平台来给 SRE 使用。然后遇到问题总是得去排查平台的代码问题,最终变得很复杂,得明白运维平台的代码是怎么运行的,才能用的好。怀念 Jenkins 和 Ansible 的操作。

业余的爱好

读书方面,印象比较深的是读了莫言的《丰乳肥臀》,非常震撼。读了很多和网络相关的书。读了一本 The Manager’s Path: A Guide for Tech Leaders Navigating Growth and Change,写的很好,也许后面有空单独写一篇博客。

今年也看了很多影视作品,年末上映的《好东西》非常好看;偶然发现 Disenchantment 已经更新完了,之前只看过第一季,所以今年一口气全部看完了,连着看居然有一些审美疲劳;印象比较深的还有《奥本海默》,《沙丘2》(我今年最喜欢的电影了),Shameless 年初跟了一段时间,感觉烂尾了,已经弃剧。

今年玩的令人印象深刻的游戏是《黑神话·悟空》,已经期待了四年,今年终于玩上了。非常好玩,一点也没有让人失望。下半年腾讯宣布 Switch 游戏机服务器停止维护,作为国服勇士,我可以领四个游戏,最喜欢的是超级马里奥兄弟,和欣一块玩,不亦乐乎,已经通关了,现在再玩第二遍,寻找所有的隐藏金币。

欣对星星比较着迷,跟着普及了一下天文学的知识,能看到星星的夜晚都不会浪费掉,会去公园的躺椅上辨认一颗颗星星。

新的一年

  • 打算继续锻炼身体,2024 年锻炼的还是不够多,今年要多花点时间锻炼才行;
  • 打算带爸妈去新加坡和泰国旅行,已经订好酒店了,期待;
  • 继续在网络方面的研究,还有很多想要学习的内容在排队;

其他的年终总结列表:

  1. 2013年
  2. 2014年
  3. 2015年
  4. 2016年
  5. 2017年
  6. 2018年
  7. 2019年
  8. 2020年
  9. 2021年
  10. 2022年
  11. 2023年
  12. 2024年
  1. Armin 写的人生反思:https://lucumr.pocoo.org/2024/12/26/reflecting-on-life/ ↩︎
  2. 去远航 ↩︎
  3. 去伊豆去东京 ↩︎
  4. 程序的 Metrics 优化——Prometheus 文档缺失的一章 ↩︎
  5. Docker 镜像构建的一些技巧 ↩︎