标签归档:ByAIGC

[转译] 获得报酬的三种方式

来源:Three Ways to Get Paid – Jason Zweig

作者:Jason Zweig

说明:本文为 AIGC 辅助翻译版本,已进行简单人工校对,具体细节请自行甄别。

获得报酬的三种方式

我的父亲于 1981 年去世。他是一个取之不尽的智慧与幽默之源。我已经记不清他是什么时候告诉我这条由三部分组成的准则了,但我从未忘记。三年前我曾把它发到推特上;不过不断有人希望能在一个固定页面里看到它,所以我把它放在这里。

谋生有三种方式:

  1. 对那些愿意被欺骗的人撒谎,你会发财。

  2. 对那些想听真话的人说真话,你能维持生计。

  3. 对那些愿意被欺骗的人说真话,你会破产。

其余的,都只是注解。

继续阅读

这篇文章最初发表于《华尔街日报》。

延伸阅读

  • Jason Zweig, Your Money and Your Brain
  • Jason Zweig, The Devil’s Financial Dictionary
  • Benjamin Graham, The Intelligent Investor
  • Jason Zweig, The Little Book of Safe Money
  • Saving Investors from Themselves
  • What I Read and Why
  • A Long Chat with Peter L. Bernstein
  • In Memory of My Father: By and For My Father

[转译] 用 100 年求解一个积分

来源:100 years to solve an integral – Lior Sinai

作者:Lior Sinai

说明:本文为 AIGC 辅助翻译版本,已进行简单人工校对,具体细节请自行甄别。

用 100 年求解一个积分

墨卡托地图与正割积分的历史

任何初学微积分的学生都很熟悉正割函数的积分。然而,这个积分曾经是数学史上一个悬而未决的重要问题。1569 年,杰拉杜斯·墨卡托(Gerardus Mercator)为了绘制他著名的地图而首次提出了它。他没能求出精确解,只好采用近似方法。86 年后的 1645 年,这个精确解在没有微积分工具的情况下被偶然发现。此后又过了二十多年,直到 1668 年才出现形式化证明;距离墨卡托首次提出这个问题,已经过去了 99 年。

2021 年 3 月 13 日更新:补充了纳皮尔(Napier)如何计算对数三角函数表的说明。这源于本文在 Hacker News 上讨论时有人提出的一处修正。

2021 年 10 月 10 日更新:大圆航线与恒向线图像现在由使用 Cartopy 的脚本生成。此前这些图像由 Matlab 应用生成。你可以在作者的 GitHub 仓库 中查看新脚本,并输入自己的坐标来生成对应的航线。

正如 SMBC 的这幅漫画所调侃的那样,数学史往往并不那么直线前进。课堂上被例行讲授的定理、公式和记号,在当初也曾是洞见,甚至是偶然发现。本文讲述的正是这样一个公式:正割函数的积分。

我第一次读到这个故事,大约是在十年前。当时我开始对制图学(cartography)产生兴趣:制图学既是地图制作的科学,也是地图制作的艺术。1 这个积分对于墨卡托地图至关重要,因此也影响了许多使用墨卡托投影的在线地图,例如 Apple MapsGoogle Maps

这个故事此前已经被讲述过多次,可参见相关论文与综述资料。不过,这些资料大多是期刊文章,主要面向学术读者。本文希望在更轻松、更有色彩的语境中介绍它,让它更容易被理解。

本文是一篇数学文章。若读者熟悉代数、三角函数、弧度制和基础微积分,会更容易阅读;这些内容通常出现在高中高阶数学课程或大学一年级数学课程中。

大一数学课

大学一年级的数学课上,在学习了一个月的求导之后,我们开始学习它的反问题:积分。求导研究的是如何为曲线找到梯度函数;积分则是在反向追问:给定一个梯度函数,对应的曲线是什么?当时我的老师开始介绍三角函数的积分。他先写下:

[ \int \sin(x) dx = -\cos(x) + c \quad\text{and}\quad \int \cos(x) dx = \sin(x) + c ]

这组关系很容易理解,因为正弦和余弦的导数只差一个符号。只要注意负号即可。接着他推导了正切函数的积分:

[ \int \tan(x) dx = \int \frac{\sin(x)}{\cos(x)}dx = -\ln|\cos(x)| + c ]

好吧,这就有点技巧了。这里并不能一眼看出可以使用链式法则的逆过程;不过,因为函数 $\cos(x)$ 与它的导数 $\sin(x)$ 同时出现,仔细想想也能说得通。然后老师说,下面是正割函数的积分,把它背下来:

[ \int \sec(x) dx = \ln|\sec(x) + \tan(x)| + c ]

等一下,这个结果是从哪里来的?我的老师没有解释。通过求导验证它确实成立并不困难。2 但问题是:最初是谁、又是怎样想到这个形式的?

函数 $\ln|\sec(x)+\tan(x)|$ 的切线斜率为 $\sec(x)$。发现这一事实,花了差不多 100 年。

我认为在这个时刻,大多数像我一样的大一微积分学生,脑中都会闪过以下想法:

  1. 积分比求导难多了。
  2. 某位数学家一定是先通过求导偶然撞见了这个结果。
  3. 反正这东西以后到底能在哪里用?

事实上,第一点是真的;许多学生在考试之后都会作证。第二点是错的:它实际上是一位教师在查看原始数值表时发现的。用这种方式发现一个积分极其罕见,甚至可以猜测它可能是唯一一个以这种方式被发现的积分。在微积分课堂上,原始数字表如此少见,如果你试图靠查数表来求一个积分,大概会被人笑话。至于第三点,对我个人来说仍然是真的。

不过,这并不意味着这个积分没有用。它被用来构造墨卡托地图。也正因为如此,那位教师才会在处理大量数字时,意外地意识到公式的真实形态。

快速回顾:三角函数

正割函数是一个标准三角函数。对于直角三角形中角 $\varphi$ 而言,它被定义为斜边 $c$ 与邻边 $a$ 的比值:

[ \sec(\varphi) = \frac{c}{a} ]

它也是更常用的余弦函数的倒数:

[ \sec(\varphi) = \frac{1}{\cos(\varphi)} ]

若画出从 $-2\pi$(-360°)到 $2\pi$(360°)范围内的正割函数与余弦函数图像,就能直观看到二者的关系。

正割函数的积分可以解释为图像下方的面积。3

制图学入门

地球无法在不产生畸变的情况下投影到平面地图上。多年来,制图师设计了许多不同的地图投影,试图在尽量减少畸变与保留其他性质之间取得平衡。这些投影形态各异。为了理解后文的墨卡托地图,下面先介绍两种最简单的投影。

所有地图投影都可以表示为一组方程:它们把球面坐标转换为平面地图坐标。球面上的坐标是角度 $\varphi$ 与 $\lambda$,分别对应纬线(parallels)和经线(meridians)。平面地图上的坐标则是 $x$ 与 $y$。因此,地图投影就是从 $\varphi, \lambda$ 到 $x, y$ 的变换。

最简单、也是已知最古老的投影之一,是等距圆柱投影(equirectangular projection)。

它的做法是把经线和纬线分别映射为间距固定的竖直直线与水平直线。这会沿纬线方向拉伸物体。它的投影方程为:

[ \begin{align} y &= R\varphi\ x &= R\lambda \end{align} ]

虽然方程很简单,但构造过程并不总是容易想象。可以把球面的一段剥离并压平来理解:沿经度方向把弧线拉成直线。弧线本身在这个过程中长度不变,但这要求沿 $x$ 轴方向拉伸两侧。因此,所有弧线相交的一个点会被拉伸成一条线。

等距圆柱投影的总面积为 $(2\pi R)(\pi R)=2\pi^2 R^2$,而球面的表面积为 $4\pi R^2$。因此,这种投影会使面积产生一个 $\frac{\pi}{2}\approx1.57$ 倍的畸变因子。

另一类投影可以通过从球面向制图平面投射线来得到。兰伯特圆柱投影(Lambert cylindrical projection)就是一个例子。它可以理解为用一个圆柱包住球面,然后通过平行于 $x$ 轴的直线把点投射到圆柱上。

对应的方程是:

[ \begin{align} y &= R\sin(\varphi)\ x &= R\lambda \end{align} ]

对于赤道附近的物体,这种投影产生的畸变很小,例如非洲附近。但靠近两极的物体会因为球面曲率而被压缩;格陵兰岛就是一个很明显的例子。

这种地图有一个有用的性质:它的面积等于球面的表面积。平面地图面积为 $(2\pi R)(2R)=4\pi R^2$,与球面面积相同。因此,虽然物体形状发生了畸变,它们的面积仍然是正确的。格陵兰岛相对于非洲的比例,在这张地图中就得到了准确表示。

墨卡托地图

1569 年,杰拉杜斯·墨卡托希望绘制一幅适合航海的全球地图。他生活在一个横跨大洋航行相当普遍的时代;1492 年,克里斯托弗·哥伦布就是从西班牙一路航行到美洲。前面介绍的那些地图适合作为艺术化印象图或一般用途图,却不适合导航。畸变会妨碍在地图上准确测量距离和方位。在局部尺度上,现实中彼此垂直相交的线(例如道路),在地图上可能会变得相互倾斜。

墨卡托尤其希望制作一张能让恒向线(rhumb lines)表现为直线的地图。恒向线是相对于经线保持恒定方位角的曲线。沿恒向线航行时,导航员只需要在整个旅程中保持罗盘上的同一方位。例如,穿过现代纽约与开普敦的恒向线,在每一点上与经线的夹角都为 48.56°,这对应于从开普敦到纽约的方位角 311°26′18″。

与恒向线相对的是大圆航线(great circle)。大圆是球面上圆心位于球心的圆;沿大圆航行总是更短。在纽约到开普敦这个例子中,大圆距离约为 12550 千米,而沿恒向线约为 12600 千米。借助现代技术,船舶和飞机很容易沿大圆航线行进。

但在墨卡托的时代,这并不容易。因此,水手更倾向于沿恒向线航行:他们宁愿多走一点路抵达目的地,也不愿迷航后走更多冤枉路。

墨卡托的想法是,在南北方向上拉伸一张圆柱投影地图,以保持形状与角度。观察兰伯特投影可以发现,4 每一个纬度所需的拉伸因子都不同:在赤道处不需要拉伸;在 45° 纬线上只需要少量向上拉伸;越接近两极,物体越需要被大幅拉伸,才能抵消原先的压缩。

这个拉伸因子可以这样计算。

首先,墨卡托把地球划分为等间距的经纬网格,间隔分别为 $\delta\varphi$ 与 $\delta\lambda$。沿经线方向,每个小网格的弧长为 $R\delta\varphi$。沿纬线方向,该纬圈半径为 $R\cos(\varphi)$,所以弧长为 $(R\cos(\varphi))\delta\lambda$。于是切线值可以近似为:

[ \tan(\alpha) \approx \frac{R\cos(\varphi)\delta\lambda}{R\delta\varphi} ]

接着,把这个小网格压平成一个矩形,并满足两个要求:

  1. 保持角度不变,即令 $\alpha=\beta$。
  2. 像兰伯特投影那样,把纬线投影到 $x$ 轴方向。这意味着 $\delta x = R\delta\lambda$。

因此,变换关系为:

[ \begin{align} \tan(\alpha) &= \tan(\beta) \ \frac{R\cos(\varphi)\delta\lambda}{R\delta\varphi} &= \frac{\delta x}{\delta y} \ \delta y &= \frac{\delta x}{\delta \lambda} \frac{1}{\cos(\varphi)} \delta \varphi = R \sec(\varphi) \delta \varphi \end{align} ]

从这里再向前一步,就可以把它写成积分。不过,严格意义上的微积分要在墨卡托发表地图之后一个世纪才形成。墨卡托实际做的是意识到:可以把每一点上的拉伸因子逐项相加。第 $n$ 个网格处的拉伸,近似等于它下方网格的拉伸再加上 $R\sec(\varphi)\delta\varphi$。于是可以得到求和式:

[ \begin{align} yn &\approx R \sec(n \cdot \delta \varphi) \delta \varphi + y{n-1} \ &=\sum^{n}_{k=0} R \sec(k \cdot \delta \varphi) \delta \varphi \end{align} ]

通过使用固定的 $\delta\varphi$,墨卡托便能计算出地图上的纬线间距,然后在其上绘制世界地图。现代渲染出的结果,就是我们熟悉的墨卡托地图。

这张地图畸变非常严重。格陵兰岛现在看起来比非洲还大;大圆航线则呈现为奇怪的曲线。但恒向线是直线!只用一把尺子和量角器,就可以计算方位;不需要额外思考,也不需要复杂数学,更不需要在地球仪上使用精巧仪器。用现代的话说,它在水手中大受欢迎。

对于在线地图而言,局部投影非常重要。如果你在城市中查路线,你最关心的是道路看起来是否正确。墨卡托地图被采用,正是因为它能保持道路网格之间的角度。其他地图投影无法满足这个简单要求。一个小问题是比例尺会随纬度变化;但在线地图可以在每个点上轻易计算这种变化。你可以在 Google Maps 中尝试观察:相同缩放级别下,不同纬度的比例尺长度并不相同。赤道附近比例尺可能最小为 5 米;接近两极时,由于被拉伸得更厉害,比例尺可能降到 1 米。不过,当你在地图上缩放到某一个具体地点时,通常不会注意到这一点。

顺便说一句,如果你要在 Google Maps 上查看长距离范围,最好打开“地球视图”(Globe view)。

三角函数表、墨卡托表与对数表

故事在这里出现了意外转折。为了理解这一点,需要先介绍另一段历史:数学表。

今天,袖珍计算器和计算软件太常见,以至于我们已经忘记数学表曾经无处不在。即便到了 20 世纪 90 年代,情况仍然如此。

过去,如果你想计算 $\sec(36^\circ)$,可以画一个大三角形,用尺子实际测量角度和边长,然后手工完成长除法。更常见、更现实的做法,是在三角函数表中查找这个值。这些表中的数字,需要通过近似公式和三角恒等式辛苦计算出来;但对于使用者来说,它们非常简单。

你只需要查表即可;如果想要更高精度,偶尔在相邻数值之间做插值。这些表还有一个额外好处:反查也很方便。例如,如果你在正割表中查找数字 1.23606,会发现它对应 36°。

1599 年,爱德华·赖特(Edward Wright)发表了赤道墨卡托地图方程所需的表格。他采用 $\delta\varphi=1’=\frac{1}{60}1^\circ$。他还给出了墨卡托地图的第一份数学描述;墨卡托本人并没有完整说明这一点。这使其他人更容易制作自己的墨卡托地图。

1614 年,约翰·纳皮尔(John Napier)引入了对数。对数是指数运算的逆运算。用现代记法,以 $b$ 为底的 $x$ 的对数 $y$ 写作:

[ y = \log_b x \quad ; \quad b^y = x ]

纳皮尔的主要动机,是寻找更容易执行乘法和除法的方法。例如,根据指数法则:

[ 2^a \div 2^b = 2^{a-b} ]

因此,除法可以这样完成:

[ 3764 \div 873 = 2^{11.878} \div 2^{9.770} = 2^{2.108} = 4.311 ]

其中 $\log_2(3764)=11.878$,$\log_2(873)=9.770$。

对数同样需要通过近似计算辛苦制表。纳皮尔使用了一种运动学框架来完成这件事。这个想法今天看来可能很奇特,但它与纳皮尔最初如何想象对数有关。5 他的最终表格把数字与其对数以及正弦联系起来。用户可以用这样的表查找对数,并同样享受方便反查的好处。这些表非常受欢迎——显然,过去的人们并不喜欢做乘除法。用加减法替代乘除法,也能降低出错概率,尤其是在连续计算中。

随后,数学家把这类表扩展到了其他三角函数。

1645 年,根据传说,一位名叫亨利·邦德(Henry Bond)的教师注意到一个奇怪现象:赖特的墨卡托表中的数字,与 $\log_e(\tan(\varphi))$ 表中的数字非常相似。二者只是在表格中相差一个因子 2 和一个 45° 的偏移。因此,他本质上猜想:

[ \int_0^{\varphi_1} \sec(\varphi) d\varphi = \ln \left| \tan \left( \frac{\varphi_1}{2} + 45^\circ \right) \right | ]

数学家们注意到了这个断言,却无法证明它。当时微积分仍处于萌芽阶段。1668 年,也就是墨卡托首次绘制地图 99 年后、邦德给出结果 23 年后,詹姆斯·格雷戈里(James Gregory)终于证明了它。不过,这个证明被认为冗长而“令人疲惫”。1670 年,艾萨克·巴罗(Isaac Barrow)通过部分分式积分给出了更简洁的证明。

最后,借助三角恒等式可以证明,下列三个公式完全等价:

[ \int \sec(\varphi) d\varphi = \begin{cases} \ln |\sec(\varphi) + \tan(\varphi) | + c \ \ln \left| \tan \left( \frac{\varphi}{2} + 45^\circ \right) \right | + c\ \frac{1}{2}\ln \left| \frac{1+\sin(\varphi)}{1-\sin(\varphi)} \right| + c \end{cases} ]

结论

这是一篇很长的文章。希望你和我一样觉得这段历史引人入胜。对我来说,真正令人惊叹的是:一个出现在大一考试中的小公式,背后竟然有如此丰富多彩的历史。我确实认为,课堂上应该更多讲述它。已经有人尝试过这种教学方式;在小规模教学实验中,效果不错。

如果你更关心 Google 如何制作地图,强烈建议阅读一位 Google 工程师写的相关博客文章。你能在 Google 的代码中找到正割积分的影子吗?

最后,我还想补充一点。围绕墨卡托地图存在很多争议。它是一种极其常见的投影。我年轻时,墙上就挂过一张墨卡托投影的世界地图。不过,希望你现在已经充分理解:它的主要用途是导航。除此之外,它会不必要地扭曲形状,尤其会使美洲和欧洲看起来比真实情况大得多。人们把这种效果与殖民主义和种族主义联系起来,并非毫无道理。

几十年来,制图师一直批评墨卡托投影被用于并不适合它的场景。6 甚至还有一段 20 世纪 90 年代电视剧中的有趣片段专门调侃这一点。

地图投影有很多种,每一种都有自己的用途。我个人最喜欢的是温克尔三重投影(Winkel Tripel projection)。它是美国国家地理学会的官方地图投影。无论在最终呈现还是数学形式上,它都是形状与尺度之间优雅的折中。另一个广受喜爱的投影是罗宾森投影(Robinson projection)。它采用一种“艺术化方法”设计:不同于其他投影,亚瑟·H·罗宾森(Arthur H. Robinson)并没有使用方程,而是以 5° 为间隔手工确定比例因子。



  1. 如果我没记错,当时我是在 Wikipedia 上漫游时读到这个故事的。途中我进入了正割函数积分页面,那里包含一段非常简短的历史。 

  2. 通过求导验证如下:

    [

    \begin{align}

    &\frac{d}{dx}\ln|\sec(x) + \tan(x)| \

    &= \frac{d}{dx}\ln|z| \quad,\quad z=\sec(x)+\tan(x) \

    &= \frac{1}{z} \frac{dz}{dx} \

    &= \frac{1}{\sec(x) + \tan(x)}(\sec(x)\tan(x) + \sec^2(x)) \

    &= \sec(x)\frac{\tan(x) + \sec(x)}{\sec(x) + \tan(x)} \

    &= \sec(x)

    \end{align}

    ]

    这里用到了链式法则,并假定下列结论已经证明:

    • $\frac{d}{dz}\ln(z)=\frac{1}{z}$

    • $\frac{d}{dx}\tan(x)=\sec^2(x)$

    • $\frac{d}{dx}\sec(x)=\sec(x)\tan(x)$

  3. 面积相对于 $x$ 轴方向的变化率就是函数值 $y$,也就是一条极薄矩形的高度。因此 $\frac{dA}{dx}=y\implies A=\int y\,dx$。 

  4. 作者并不确定墨卡托是否知道这种投影。兰伯特圆柱投影直到 1772 年才由约翰·海因里希·兰伯特正式描述。不过,它是最容易用来直观解释墨卡托投影构造过程的方式,因此原文采用了这种说明。 

  5. 简述纳皮尔的方法:他比较一个沿无限长直线运动的粒子与另一个沿长度为 $R$ 的有限线段运动的粒子。第一个粒子匀速运动,$\frac{dx_1}{dt}=1$;第二个粒子的速度与它在有限线段上剩余的距离成正比,$\frac{dx_2}{dt}=R-x_2$。第二个粒子走过的距离与第一个粒子的距离满足微分方程 $\frac{dx_2}{dx_1}=R-x_2$。其解为:

    [

    \begin{align}

    x1 &= \log{\frac{1}{e}} \left(\frac{R-x_2}{R}\right) \

    &\approx \log_{\left(1-\frac{1}{R}\right)^R}\left(\sin(\theta)\right)

    \end{align}

    ]

    更多信息可参见 Dynamic logarithms。原文最初曾称对数三角函数表出现在普通对数表之后;但由于纳皮尔推导近似公式的方法很特殊,这并不准确。这个问题由 Hacker News 评论指出。 

  6. 原文注:有几个流行的新冠疫情地图追踪器使用了墨卡托投影,令人遗憾。 

【翻译】如何快速给 MacBook 预热

前言

文章来源:z3ugma.github.io

原文标题:Warm up your MacBook

原文发布时间:2019-11-18

原文链接:https://z3ugma.github.io/2019/11/18/warm-up-your-macbook/

本文根据原文内容翻译整理,并补充了适用范围与使用注意事项,译文由 AI 辅助完成。


这是一篇带点玩笑意味的实用短文:如果你刚从冬天室外、冷车厢,或者空调很足的环境里拿出 MacBook,金属机身摸起来冰凉,想让它快一点“热起来”,最直接的方法其实不是等,而是让 CPU 开始工作

如何快速给 MacBook 预热

如果你只是想粗暴地让机器升温,可以打开终端,执行下面这条命令:

yes > /dev/null

这个命令会持续输出 y,再把输出丢进黑洞设备 /dev/null,从而让一个 CPU 核心持续忙碌起来。机器开始算东西,温度自然就会上去,机身也会慢慢暖和。

不过,如果你想要一个更可控的方式,原文更推荐使用 stress 这个小工具。先用 Homebrew 安装:

brew install stress

然后运行:

stress -c 6 -m 2 -t 300

这条命令的意思是:

  • -c 6:启动 6 个 CPU 压力任务;
  • -m 2:再启动 2 个不断做内存分配/释放的任务;
  • -t 300:持续 300 秒,也就是 5 分钟后自动停止。

这样做的好处是,你不用自己记得去停掉进程,时间到了它会自动结束。

如果你觉得这个操作以后还会经常用,原文建议顺手在 shell 配置里加一个别名,例如:

alias warm='stress -c 6 -m 2 -t 300'

以后只要输入:

warm

MacBook 就会开始“自我发热”。

补充说明

原文写于 2019 年,更贴近当时的 Intel MacBook 使用场景。放到今天,有几点值得补充:

1. yes > /dev/null 只会打满一个核心

在多核机器上,这条命令并不会让整台机器瞬间满载,它只是让一个核心一直忙。想要更明显地升温,还是 stress 这类可以并发压测的工具更直接。

2. 不要把“预热”变成“过热”

这类做法本质上是在主动制造负载。短时间内问题不大,但如果你把任务开得太猛、时间拉得太长,风扇噪音、电池消耗、表面温度都会明显上升。放在腿上、被子上或者不通风的地方跑,就更不推荐了。

3. Apple Silicon 时代通常没那么需要

现在的 M 系列 MacBook 在能耗和发热控制上比当年的 Intel 机型好很多。多数情况下,你可能根本不需要专门“预热”它;如果只是嫌机身太凉,降低空调、换个环境,往往比专门压 CPU 更自然。

4. 想停下来就直接中断

如果你是手动运行 yesstress,发现已经够热了,直接按 Ctrl+C 即可结束。不要让它在后台一直跑着忘了关。

小结

这篇原文其实就讲了一件事:想让冷冰冰的 MacBook 快点变暖,最简单的方法就是让 CPU 忙起来。

2019 年看,这是一个略带黑色幽默的 MacBook 小技巧;今天再看,更像是一篇属于 Intel 时代的轻松小品。拿来了解一下无妨,真要使用时,记得适可而止。

主流编程智能体工具横向对比:Claude Code、Codex、OpenCode、CodeWhale、GitHub Copilot

最近编程智能体(Coding Agent)工具更新非常快,不同产品在“自主程度、代码理解深度、集成能力、生态开放性、价格和上手门槛”上差异很大。本文把几个常见工具放在一起做一个实用导向的对比,帮助你按场景选型。

一图速览:核心差异横向对比

说明:以下结论基于当前公开信息与实际使用体验,产品能力迭代很快,建议你最终按自己项目做一轮小规模实测。

工具 主要形态 主要接入模型(默认/主力) 模型开放性(是否可接入其他模型) 自主执行能力 长上下文与大项目理解 生态集成 上手门槛 更适合谁
Claude Code 命令行 Agent Claude 系列(Anthropic) 中:主打 Claude 体验,第三方模型可配能力相对有限 高(可多步执行、自动迭代) 强(复杂代码库推理较稳) 中(CLI 为主) 资深开发者、重度终端用户
Codex(OpenAI) 云端/CLI/IDE 辅助 OpenAI 模型(GPT/Codex 系) 中:以 OpenAI 体系为核心,外部模型接入灵活性一般 高(任务分解与执行积极) 强(跨文件与任务链表现好) 高(OpenAI 生态与 API 完整) 全栈与平台工程团队
OpenCode 开源本地/CLI 方案 可按配置接入多模型(如 OpenAI/Anthropic/DeepSeek 等) 高:强调模型可替换、可自托管与可编排 中高(可编排流程) 中(依赖模型与配置) 中高(可定制) 中高 喜欢自托管与可控性的开发者
CodeWhale(DeepSeek-TUI) 终端交互式 Agent 以 DeepSeek 系列为主 中:主流用法偏 DeepSeek 生态,扩展能力取决于实现版本 中高(终端内效率高) 中高(中文语境表现好) 中(偏 TUI 工作流) 中文开发场景、终端党
GitHub Copilot IDE 插件 + Chat + Agent 默认主力为 GitHub 提供的模型池(含 OpenAI/Anthropic 等) 中高:可在支持范围内切换模型,但不是完全自由接入任意模型 中高(从补全到任务级) 中高(仓库上下文整合成熟) 很高(GitHub/Actions/PR) 大多数团队的默认选择
Cursor AI 原生 IDE 支持多模型(常见如 OpenAI/Anthropic/Gemini 等) 高:模型选择与切换相对灵活 高(Agent 模式成熟) 强(多文件修改体验好) 中高(插件与 MCP 生态增长快) 低中 追求“开箱即用 AI IDE”的个人与小团队
Windsurf(原 Codeium) AI IDE/插件 以自家能力 + 第三方模型组合为主 中高:提供一定模型选择,但开放边界受产品策略限制 中高(强调流式协作) 中高 中高 低中 希望低成本尝试 AI IDE 的用户
Aider 终端 Git 驱动助手 可连接多家 LLM(按 API 配置) 高:模型后端可替换,适合自定义工作流 中(以补丁改动为主) 中高(对仓库改动可控) 中(与 git/workflow 绑定紧) 重视可审计改动、偏 CLI 的开发者

各工具特点与擅长场景

Claude Code

特性

  • 以终端为中心,强调“给目标 -> 自动执行 -> 反馈结果”的 Agent 流程;
  • 在复杂任务中会主动做拆解、验证、回滚式修正;
  • 对大型仓库的语义理解和推理链稳定性较好;
  • 模型生态相对聚焦在 Claude 体系,优势是体验一致,代价是模型侧开放度一般。

擅长的事情

  • 大规模重构、跨模块改动;
  • 需要连续多步执行的工程任务(改代码、跑测试、修复再验证);
  • 适合已深度使用 terminal + git 的工程师。

Codex(OpenAI)

特性

  • 强项在任务规划与执行连续性,尤其是多步骤编码任务;
  • 与 OpenAI 模型和 API 生态联动紧密;
  • 在“解释代码 + 生成实现 + 修复问题”三段式流程表现稳定;
  • 模型生态以 OpenAI 官方体系为核心,开放性更偏“平台内能力完整”。

擅长的事情

  • 业务功能快速原型;
  • 跨语言项目协作;
  • 需要结合 API/自动化平台的团队型工作流。

OpenCode

特性

  • 开源与可定制属性强,可按团队需求编排工作流;
  • 本地化与自托管友好,模型选择更灵活;
  • 可以和现有工具链深度绑定,减少平台锁定;
  • 生态开放性高,通常可接入多家模型与私有推理服务。

擅长的事情

  • 对数据合规、隐私与可控性要求高的团队;
  • 想搭建内部 AI 编程平台的技术团队;
  • 有能力维护 Agent 配置和运行环境的用户。

CodeWhale(DeepSeek-TUI)

特性

  • 终端 TUI 交互自然,适合“边看日志边改代码”的开发节奏;
  • 中文语境下交流和问题理解体验较好;
  • 强调低干扰、轻量化的 CLI 工作方式;
  • 主流使用路径偏 DeepSeek 生态,开放程度取决于具体发行版本和接入实现。

擅长的事情

  • Linux/后端开发与运维混合场景;
  • 需要快速定位问题、生成补丁、立即验证的任务;
  • 已经以 terminal 为主战场的个人开发者。

GitHub Copilot

特性

  • 从“代码补全”发展到“聊天 + 工作区理解 + 任务代理”;
  • 与 GitHub 生态深度融合(仓库、PR、Actions、Issue);
  • 团队推广成本低,覆盖 VS Code、JetBrains 等主流 IDE;
  • 模型侧提供“平台可选集”,对团队治理友好,但不等于完全开放接入任意模型。

擅长的事情

  • 日常编码提效(补全、解释、单测生成);
  • 团队协作场景(代码评审、PR 上下文、规范一致性);
  • 希望快速落地、降低学习成本的组织。

其他值得关注的流行工具

Cursor

  • 代表“AI 原生 IDE”路线,Agent 模式与多文件编辑体验优秀;
  • 模型可选范围相对广,生态开放性在商业 IDE 中较突出;
  • 适合希望把 AI 作为第一工作入口的开发者。

Windsurf(Codeium)

  • 强调低门槛与速度,订阅成本相对友好;
  • 提供一定的多模型能力,但开放边界受产品路线影响;
  • 适合从传统 IDE 迁移到 AI IDE 的过渡人群。

Aider

  • Git 驱动、patch 导向,改动可追踪;
  • 支持按 API 配置接入多个模型提供商,开放性高;
  • 特别适合喜欢“每一步都可审计”的终端开发流程。

关于“生态开放性”的实用判断标准

如果你在意模型选择自由度,建议重点看这三件事:

  • 模型可替换性:是否能无缝切换不同模型供应商;
  • 配置控制权:是否可自定义 API、路由、预算与默认模型策略;
  • 锁定成本:迁移到其他工具时,提示词、工作流和团队习惯是否可复用。

通常来说,开源/CLI 可配置工具的开放性更高;而平台化产品的一体化体验更强,但边界由平台定义。选型时本质是“开放度”与“开箱效率”的权衡。

选型建议(按场景)

  • 个人开发者、追求即开即用:优先看 GitHub Copilot / Cursor;
  • 终端重度用户:优先看 Claude Code / CodeWhale / Aider;
  • 团队规模化协作:优先看 GitHub Copilot / Codex;
  • 注重私有化与可控性:优先看 OpenCode;
  • 中文语境密集沟通:CodeWhale 往往更顺手。

总结

这些工具没有绝对“谁最好”,关键在于你要优化哪一段流程:

  • 你是要更快写出代码?
  • 还是要更稳地完成“需求到上线”的全链路?
  • 或者你更看重合规、私有化与成本控制?

建议方式是:先选 2~3 款做同一任务 A/B 测试(如“实现 + 测试 + 修复”完整闭环),再按实际产出质量、可维护性和团队接受度决定长期方案。

【翻译】这篇博客在 Ubuntu 16.04 上跑了 10 年,我把它迁移到了 FreeBSD

前言

文章来源:CrociDB

原文作者:CrociDB(Antonio Vivace)

原文标题:This blog ran on Ubuntu 16.04 for 10 years. I migrated it to FreeBSD

原文发布时间:2026-05-21

原文链接:https://crocidb.com/post/this-blog-ran-on-ubuntu-16-04-for-10-years-i-migrated-it-to-freebsd/

本文为全文翻译,译文由 AI 辅助整理,若有理解偏差欢迎指正。


这篇博客已经在一台 DigitalOcean VPS 上运行了十多年。那是一台托管在纽约、运行着 Ubuntu 16.04 LTS 的机器。而这个 LTS 版本至少已经有 5 年不再受支持了。是时候做些改变了。经过一番权衡,我把它迁移到了一台 Hetzner 的虚拟机上:配置比我旧的 Ubuntu 机器强得多,价格还不到以前的一半,而且机房就在我所在国家的另一端。不仅如此,我还顺便挑战了一下自己,把整套栈迁到了 FreeBSD。这篇文章会有点长,但如果你愿意读下去,会看到一个关于 FreeBSD JailsBastille,以及一些有意思的网站负载测试结果的故事。

动机

如果你了解 Ubuntu 的发布和支持周期(我自己其实也不算熟),就会知道一旦某个版本结束支持,apt 对应的软件仓库也就没了,你再也拿不到更新。运行这样一套过时系统会带来不少问题,其中最显而易见的一点就是:你的服务器不再那么安全了。互联网上肯定有各种机器人,专门扫描存在漏洞的节点,然后想办法塞进去恶意内容。幸运的是(我想大概算幸运),我这里一直没出过事。反正机器里也没什么特别重要的东西可偷。但我记得很久以前,我还有一个 WordPress 博客,也是跑在一台老旧 VPS 上,结果某天文章正文里突然被插满了可疑的赌场和赌博链接。

我原本就有一台 Hetzner VPS,当作远程开发机来用,可以随时 SSH 上去,考虑到价格,它一直都很可靠。所以我决定先从对比配置开始。下面这台就是原来跑我的博客和其它网站的 droplet:

(原文此处有旧 DigitalOcean 服务器截图)

它有 2GB 内存、1 个 vCPU、50GB 磁盘、每月 2TB 流量,运行的是 Ubuntu 16.04 x64。机房位于纽约,这大概也是它比较贵的原因:我每个月要为它支付 13 美元。

Hetzner 是一家老牌欧洲 IT 公司,在德国有大型数据中心,而我也住在德国。它最便宜的一款服务器,只要 3.56 欧元,配置就已经比我原来那台更好了:

(原文此处有 Hetzner 入门套餐截图)

内存和 CPU 都翻倍,存储稍微少一点,但月流量多了十倍。不过我最后还是选了一台更“豪华”的配置,每月不到 6 欧元:

(原文此处有更高配套餐截图)

对我的这些站点来说,这配置甚至有点过头了,不过为什么不呢?

旧的部署方式

我那套旧环境托管的不只是这个博客,还有几个别的网站。它们都不算热门;在所有站点里,这个博客流量算最高的了,但每个月也不过几千次页面浏览。除了偶尔有几篇文章在 Hacker News 上爆一下,平时根本没多少流量。说到底,这台机器基本就是在提供静态网站服务,没有复杂 CGI,也没有什么自定义后端代码在跑。整套栈很简单:所有内容都通过 nginx/1.10.3 以静态方式提供。我基本上就是在 /etc/nginx/sites-available 里给每个站点放一份配置文件。像静态站点生成器、LaTeX 套件之类的附加程序(比如这个博客就是用 Hugo 生成的)则通过 aptsnap 安装。事实上,我更新博客的流程一直是这样:

  • 在本地写文章
  • 提交并推送到仓库
  • SSH 登录服务器
  • 拉取仓库更新
  • 运行 hugo

这台 VPS 刚开始用的那些年里,我还会拿它做一些测试和编程实验。所以机器里积累了很多已经不再使用的老旧软件,显得很臃肿。但它就是能跑,而且跑得还挺好。

(原文此处有 Linux 4.4 截图)

它甚至好到什么程度呢?我关机前,它的 uptime 达到了 1491 天,也就是差不多连续 4 年没中断过。

为什么选 FreeBSD

说实话,我的主要动机之一就是想亲手折腾点不一样的东西。我最近读了很多、也看了很多关于 BSD 家族的内容,而我之前也曾短暂用过 FreeBSD,所以我觉得这是一个把它放到真实生产场景里试一试的好机会。FreeBSD 常常因为它的一体化设计、安全性以及 Jails 而被人称赞。后面我会详细展开。

我不想装得好像自己一开始就完全知道该怎么做,但当我读到 Jails 时,我立刻就知道自己想怎么搭了。Jails 是一种虚拟化/容器化机制,已经存在于 FreeBSD 里超过 25 年了,比 Docker 诞生还早得多。乍一看,它实现的效果和你对 Docker 容器的预期很像:在系统里面隔离出一个“小系统”,你可以在里面运行一些拿不到宿主机权限的东西。但在我看来,Docker 以及其它容器方案更适合“打包程序”——它们强调短生命周期和不可变;容器内处理的数据来自外部,但容器内部本身最好始终保持一致。而 Jails 更像真正的子系统,几乎可以把它看作迷你虚拟机,只不过底层仍然共享同一个内核。

除此之外,FreeBSD 的文件系统 ZFS 对服务器场景也确实非常好用。如果你来自 Linux 世界,大概听说过 Btrfs,它是近几年越来越多发行版开始采用的新文件系统。它和 ZFS 有一些相似点,比如数据完整性和快照。不过 ZFS 大概比 Btrfs 要成熟得多。如果我能频繁地给系统做快照,就不需要依赖 VPS 提供商的快照或备份服务,而那些通常都要额外付费。

我的想法是:每个网站一个 Jail,里面装上构建这个网站所需的工具(比如博客需要 Hugo),再在 Jail 里运行一个 nginx 实例来提供服务;另外再单独放一个主 Web 服务器 Jail,通过反向代理把外部世界和这些站点连起来。这样一来,哪怕某个 Jail 被攻破了,我也可以把它直接销毁,再建一个新的。后面我会更具体地介绍配置过程。

Hetzner VPS

先给所有喜欢看系统信息的人一点细节:

(原文此处有 fastfetch 截图)

搭建过程

我会快速讲一下自己是如何把这台服务器搭起来的,以及其中最关键的几个点。平时我会记开发日志,所以这次也是边做边记。不过不要把这篇文章当作教程,因为我可能漏掉了一些步骤。更准确地说,这是一篇“在 Hetzner 上用 FreeBSD 搭 Web 服务器是什么感觉”的记录。

安装 FreeBSD

Hetzner 在创建虚拟机时会提供一些镜像,但选择其实非常有限:

(原文此处有 “No BSD” 截图)

不过我在 FreeBSD 官方 YouTube 频道 上看到了一份讲如何在 Hetzner 上安装 FreeBSD 的指南。顺带一提,这个频道非常推荐。

关键在于:Hetzner 其实提供了 FreeBSD 镜像,只是藏得比较深,需要多点几步。它是以 ISO 镜像的形式提供的。所以在创建虚拟机、选择操作系统镜像的时候,你先随便选一个,反正稍后都要把它抹掉。创建完虚拟机后,只需要去控制台里的 ISO Images 标签页,挂载你想要的镜像即可:

(原文此处有 FreeBSD ISO 镜像截图)

我选的是 14.3。然后重启,基本上就按安装器一路走下去。印象里我没有改什么默认选项,当时是跟着官方 FreeBSD 频道里的安装视频一步步做的。很快,系统就装好并跑起来了。

Bastille

前面我已经提了好几次 Jails。但我用的不只是 Jails,还用了 Bastille,这是一个用来管理 Jails 的系统。原因很简单:手动创建 Jail 这件事比我想象中复杂,而且每新建一个 Jail,都有一堆独立步骤需要自己处理。Bastille 把这些都简化了,很多事情只差一条 bastille 命令。比如 bastille list 可以列出系统里的所有 Jail,bastille create 用来创建新 Jail,bastille console 则可以直接打开某个 Jail 的 shell,等等。

安装并启用它(几乎)就像 官方文档 里说的这么简单:

pkg install bastille
sysrc bastille_enable="YES"

当然也有别的 Jail 管理器,我只是选了一个名字最酷的。

整体架构

整套思路是:先有一个运行 Caddy 的 Jail,负责对外提供所有站点,并处理域名和 SSL 证书;然后每个站点再单独有自己的 Jail,里面安装各自构建和服务所需要的软件。这个“服务器 Jail”会把所有流量反向代理到对应的站点 Jail。所以第一步就是配置一个内部虚拟网卡。你可以把这套结构理解成:一组跑在同一个宿主机里的小型虚拟机网络。

配置虚拟网络接口的方式如下:

sudo sysrc cloned_interfaces+="lo1"
sudo sysrc ifconfig_lo1_name="bastille0"
sudo service netif cloneup
sudo sysrc ifconfig_bastille0="inet 10.0.0.1 netmask 255.255.255.0"

这几条命令本质上就是克隆一个回环接口,把它命名为 bastille0,再给它分配网络参数。所有 Jail 只会跑在这张网络接口上。至于 Caddy 所在的 Jail,因为它需要监听外部请求,就必须具备访问外网的能力。为此,我们要用 PF(Packet Filter,也就是 FreeBSD 的 防火墙)配置一条出网规则。

我们需要编辑 /etc/pf.conf

# the main interface `vtnet0` and the virtual one we created `bastille0`
ext_if = "vtnet0"
int_if = "bastille0"
vpn_if = "tailscale1"

# skip internal traffic
set skip on $int_if
set skip on $vpn_if

# allow outbound access to the internet from the jails
nat on $ext_if from 10.0.0.0/24 to any -> ($ext_if)

# redirect HTTP/HTTPS traffic from the web to the server jail (10.0.0.5)
rdr pass on $ext_if proto tcp from any to any port {80, 443} -> 10.0.0.5

# block everything else
block all

# allows outbound traffic from host
pass out quick on $ext_if keep state

正如配置里的注释所写,这段规则主要做了几件事:把 bastille0tailscale1 上的内部流量跳过处理;允许 Jail 和宿主机出网;再把所有 80443(HTTP/HTTPS)流量重定向到内部 IP 10.0.0.5,也就是我们的 Caddy 服务器。

然后启用 PF:

sysrc pf_enable="YES"
service pf start
sysrc gateway_enable="YES"

至此,网络层的基础就完成了。接下来开始创建 Caddy 服务器 Jail。

创建第一个 Jail:Caddy Server

我的旧服务器一直在跑老朋友 nginx。除了早年短暂用过一点 Apache,我人生的大多数时间都在用 nginx。不过 nginx 有个烦人的点,而 Caddy 正好能漂亮地解决:SSL 证书。以前在旧服务器上,我得隔三差五跑一遍 certbot 去续域名证书,而且续签忘掉的次数比我愿意承认的要多。Caddy 会自动处理这些事情。

要创建一个新 Jail,先得为当前要使用的 FreeBSD 版本做 bootstrap,在我的场景里是 14.3-RELEASE

bastille bootstrap 14.3-RELEASE

然后创建真正的 Jail:

bastille create caddy 14.3-RELEASE 10.0.0.5 bastille0
bastille start caddy

这里我们创建了一个名为 caddy 的 Jail,使用 14.3-RELEASE,IP 为 10.0.0.5,挂在 bastille0 接口上。

检查 Jail 状态:

# bastille list
 JID  Name      Boot  Prio  State  Type   IP Address  Published Ports  Release       Tags
 3    caddy     on    99    Up     thin   10.0.0.5    -                14.3-RELEASE  -

这样它就已经跑起来了!要记住,Jails 并不是像 Docker 容器那样追求 ephemeral 的东西。你不会先把容器完全定义好,然后指望它永远都保持一模一样。这里的感觉更接近虚拟机。你随时都可以通过下面的命令进到它的 shell:

# bastille console caddy

[caddy]:
root@caddy:~ #

配置 Caddy Server

既然我们已经在 caddy 这个 Jail 里了,那只需要把 Caddy 装上:

pkg install caddy
sysrc caddy_enable="YES"
service caddy start

Caddy 的所有配置都放在 Jail 里的 /usr/local/etc/caddy/Caddyfile。如果我们想在宿主机上也能直接访问这份配置文件,就需要把宿主机上的一个目录挂载到 Jail 里。甚至还可以用只读模式挂载,这样 Jail 自己就无法修改配置。大概像这样:

bastille mount caddy /usr/local/etc/my-caddy-config /usr/local/etc/caddy nullfs ro 0 0

好了,服务器差不多配完了,但我们还没有一个真正的站点。所以先建一个站点 Jail,然后再回来配置 Caddy。

第一个站点:es.cro.to

疫情期间,巴西总统选择把 Covid-19 当作感冒来对待,没有采取任何紧急封锁政策。他不断强调国家不能停摆,承担不起代价,哪怕有人因此死掉也无所谓。这完全无视科学,甚至无视常识。历史已经证明他有多错。对此我很不爽,于是我想做一点“抗议”。

所以我做了一个小页面 es.cro.to。它在葡萄牙语里字面上是“阴囊”的意思,但更常被用来形容一个人是个 混蛋烂人,或者别的类似意思。我买下了 cro.to 这个域名(现在回头看,这域名其实挺酷的,英文里听起来也不错),然后加了一个 es 子域名。它刚好把这个词按字典式的音节拆开,于是我就在页面里放上了这个词的词义和对应生物的图片:

(原文此处有 escroto 页面截图)

它当然也是跑在我的旧服务器上的,而且在封锁那段时间还获得了不小的流量。我决定先从它开始迁,因为它本质上只是一个带着一张压得很惨照片的单页站点,而且现在也没那么重要了。

这个站点和我其它站一样,也是一个 Git 仓库。所以我准备把所有网站都放在宿主机的 /usr/local/www 下,然后把各个站点对应的目录以只读方式挂载到对应 Jail 里。于是,当我把这个站点放到 /usr/local/www/escroto 后,就开始用 Bastille 创建 Jail。这次我直接用了一个用于 nginx 的 bastille 模板:www/nginx。模板其实就是预装一些软件的小脚本。其实不用也行,但既然有,为什么不用呢?

bastille bootstrap https://github.com/bastillebsd/templates
bastille create escroto 14.3-RELEASE 10.0.0.11 bastille0
bastille template escroto www/nginx

我给它分配的 IP 是 10.0.0.11,等会会用到。此时,这个 Jail 里的 nginx 已经在提供默认页面了。它的站点目录位于 /usr/local/www/nginx(注意,FreeBSD 几乎什么都放在 /usr/local 下)。

接着,我把宿主机上的站点目录挂到 Jail 里:

bastille mount escroto /usr/local/www/escroto /usr/local/www/escroto nullfs ro 0 0

这样一来,我在 Jail 内就能访问 /usr/local/www/escroto 这个站点目录了。因为它是个 Git 仓库,里面带有 .git 之类不该被直接提供出去的文件,所以我写了一个 deploy.sh 脚本,把 /usr/local/www/escroto/* 复制到 /usr/local/www/nginx/*,再删掉 .git 目录:

rm -fr /usr/local/www/nginx/*
cp -R /usr/local/www/escroto/* /usr/local/www/nginx/
rm -fr /usr/local/www/nginx/.git

这样以后每次要部署这个站点的新版本,只需要登录宿主机,更新仓库,然后执行这个脚本:

cd /usr/local/www/escroto
git pull
bastille cmd escroto /root/deploy.sh

后来我发现这个思路挺好,于是也给其它站点都放了一份 /root/deploy.sh

让 Caddy 把域名指向这个 Jail

回到 Caddy 配置:

cro.to {
        redir https://es.cro.to{uri} permanent
}

es.cro.to {
        reverse_proxy 10.0.0.11
}

配置非常简单:主域名重定向到 es.cro.to,再由后者反向代理到 10.0.0.11,也就是 escroto Jail。

配置好 DNS 记录后,这个站点就上线了。

部署这个博客

这个博客本身是用 Hugo 写的,仓库放在 GitHub 上。所以我把它克隆到了 /usr/local/www/blog,然后像 escroto 一样创建了一个新的 Jail:

bastille create blog 14.3-RELEASE 10.0.0.12 bastille0
bastille template blog www/nginx
bastille mount blog /usr/local/www/blog /usr/local/www/blog nullfs ro 0 0

然后在这个 Jail 里安装 Hugo:

bastille pkg blog update
bastille pkg blog install gohugo

部署脚本也很类似:

rm -fr /usr/local/www/nginx/*
cd /usr/local/www/blog
hugo -d /usr/local/www/nginx

在正式把站点迁离旧服务器前,我想先做一些基准测试,所以先把博客绑定到了一个旧域名上:

crocidb.cro.to {
        reverse_proxy 10.0.0.12
}

就这么简单,博客已经在新服务器上跑起来了。

给我的服务器做基准测试

在正式更新 DNS 记录前,我想确认这台新服务器到底能不能扛住流量。理论上看它当然行,但万一我哪里配错了呢?于是我开始琢磨,怎么比较旧服务器上的 crocidb.com 和新服务器上的 crocidb.cro.to

一开始我先想清楚,到底该测什么。测延迟没什么意义,因为我知道,对大多数访问我博客的人来说,新服务器的延迟本来就会稍微更长一些。这个站点的大部分流量通常来自北美,其次是欧洲,再然后是南美。不过那点差距其实不至于影响体验。所以更值得测的是:服务能力和高负载表现。

市面上有一些免费的在线工具,可以做比较通用的测试,比如 GTMetrixPingdomWebPageTest。但老实说,这两台服务器在这些工具里的差异几乎只体现在延迟上,其它方面基本差不多。所以我决定往更深一点挖。

我发现了 wrkhey 这两个 HTTP 压测工具。它们本质上就是对网站发起成千上万的并发请求,然后收集诸如请求延迟、错误响应、每秒传输量等信息。比如,我在另一台 Hetzner VPS 上跑了下面这个 wrk 命令:

wrk -t4 -c100 -d30s --latency https://crocidb.com/

这表示让 wrk4 个线程、维持 100 个并发连接、持续压测 30 秒。这个数字其实也不算特别离谱。结果如下:

Running 30s test @ https://crocidb.com/
  4 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    89.63ms   18.31ms 189.09ms   95.29%
    Req/Sec   211.74     38.44   313.00     87.69%
Requests/sec:    833.41
Transfer/sec:      8.29MB

而对 crocidb.cro.to 测出来是:

Running 30s test @ https://crocidb.cro.to/
  4 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     6.75ms    3.59ms  51.98ms   74.41%
    Req/Sec     3.08k   260.11     4.34k    81.83%
Requests/sec:  12260.10
Transfer/sec:    130.80MB

旧服务器每秒能处理 833 个请求,而新服务器则达到 12,260。平均延迟则是 89ms6ms。但这当然不公平:压测机器和新服务器在同一个数据中心。所以我想,要不试试挂 VPN,从不同地区去测?

我用的是 Proton VPN,于是写了一个小脚本,从一组地点里选一个,连上 VPN,再跑 wrk。结果非常平淡:旧服务器平均大概 300 req/s,新服务器大概 800 req/s。测试地点如下:

LOCATIONS=(
    "US//New York|US - New York"
    "US//Los Angeles|US - Los Angeles"
    "BR//São Paulo|BR - São Paulo"
    "GB//London|GB - London"
    "DE//Frankfurt|DE - Frankfurt"
    "FR//Paris|FR - Paris"
    "SE//Stockholm|SE - Stockholm"
    "CH//Zurich|CH - Zurich"
    "IN//Mumbai|IN - Mumbai"
    "JP//Tokyo|JP - Tokyo"
    "KR//Seoul|KR - Seoul"
    "SG//Singapore|SG - Singapore"
    "AU//Sydney|AU - Sydney"
    "ZA//Johannesburg|ZA - Johannesburg"
    "AR//Buenos Aires|AR - Buenos Aires"
)

于是我放弃了“用终端用户 VPN 视角来测”的思路,转而采用一种更硬核的办法:直接在世界各地的数据中心创建真正的 VPS 节点来跑测试。

Vultr VPS 基准测试

我需要一家不同于 DigitalOcean 和 Hetzner 的 VPS 提供商,以免底层基础设施带来额外优势。除了这两家,我唯一用过的 VPS 服务商就是 Vultr。于是我决定在不同地区各开一台机器跑测试。因为整个过程是手工完成的,所以最后只选了四个地区:伦敦圣保罗硅谷东京。我在这些地区各创建了一台最便宜的 Fedora 虚拟机。

经过几轮测试后,我发现 hey 更符合我的需求。于是我的流程就变成:SSH 登录测试机,执行脚本,然后把结果拷出来。第一个测试点是圣保罗:

dnf install -y tmux vim htop
wget https://storage.googleapis.com/hey-releases/hey_linux_amd64
chmod +x hey_linux_amd64
./hey_linux_amd64 -n 1000000 -c 10000 -t 10 -z 5m -h2 https://crocidb.com/ > crocidb.com.log
./hey_linux_amd64 -n 1000000 -c 10000 -t 10 -z 5m -h2 https://crocidb.cro.to/ > crocidb.cro.to.log

这会下载 hey,然后以一种非常夸张的配置运行:总请求数 1M,并发 10k,超时 10s(也就是任意单个请求超过 10 秒就算失败),总时长 5m

第一次跑的时候,我很快就发现新 FreeBSD 服务器确实有个问题:它很早就开始失败,因为它根本扛不住 1 万并发连接。查了很多资料后,我发现可以用 netstat -Lan 查看 socket 队列大小,而我的队列值全都是 128。继续追下来才发现,原来 kern.ipc.somaxconn 的默认值就是这个数字。于是我把它调高了:

sysctl kern.ipc.somaxconn=16384

差不多十分钟后,日志跑完了,我开始看结果:

(原文此处有圣保罗 VPS 上 hey 输出对比图)

左边是旧服务器,右边是新服务器,差异简直夸张。

虽然两台机器都返回了不少错误,但 FreeBSD 这边至少完成了预期的 1M 请求,而 Ubuntu 那边连 20k 都没撑住!这个差距实在是太大了

基准结果分析

(原文此处有成功率图表)

旧服务器 crocidb.com 只能完成极少一部分请求,这一点真的很惊人。实际上,它只完成了大约 7% 的请求,而 FreeBSD 服务器完成了 94%。东京地区里新服务器的成功率略微低一点,但我觉得还不至于值得担心。

从每秒请求数来看,新服务器最少也有 3 倍提升,最高甚至能到 11 倍。

(原文此处有 Requests per second 图表)

这里的差距看起来更夸张,而我猜测东京那边的延迟可能确实更高一些。

(原文此处有延迟百分位图表)

如果看延迟百分位(例如 p50 表示 50% 的请求都比这个值更快返回),会发现两台服务器呈现出很不一样的曲线形态。新服务器直到大约 90% 之前都保持比较 线性 的增长,所以表现更可预测;而旧服务器的增长曲线则明显更不稳定。

这说明,即使在高负载下,全球范围内 90% 的访问者在尝试加载我博客首页时,仍然能在 3.5 秒 内拿到内容。我觉得这已经相当不错了。

我没有再继续深挖东京地区的问题,不过目前我也没那么担心。看 hey 的请求阶段分解(也就是它会拆分 DNS、连接、等待响应、读取响应等耗时),确实能看出日本方向的流量更慢一些:

(原文此处有请求阶段耗时分解图)

不过这些数据在我看来又有点怪。首先,第二个域名的 DNS 拨号和查询时间低得离谱,也许是因为它是个 CNAME 记录?其次,resp wait(基本可以理解成首字节时间)和 resp read(传输时间)在这里高得有点异常。但这也可能是因为它统计的只是成功请求:也就是说,旧服务器可能一开始响应很快,但很快就把新请求都拒掉了。

结论

我并不认为这个差距是因为我选的这套栈本身就有多神奇。更有可能的原因是:旧 Ubuntu 系统本身就有一些配置问题,再加上我选的 Hetzner VPS 有 4 个 CPU 核,而旧 DigitalOcean 机器只有 1 个,自然能同时处理更多请求。我也不觉得这在实际意义上真的有多重要,因为这些服务器几乎不可能真的遇到这么大的并发量。也许跑一遍 WebPageTest 这样的 Web 基准工具就已经够了。

真正切换

虽然这些基准测试还留下了很多未解答的问题,但我对结果已经很满意了。所以我直接更新了 DNS 记录。现在,这个博客已经正式跑在那台新机器上了。

说到底,经过很多个小时的实验、折腾、构建、推倒重来之后,我意识到:搭一台用 FreeBSD 托管网站的机器,其实并没有我想象中那么复杂。满足我需求的 Web 托管服务其实也有不少,我本来也完全可以用别的方案。或者装一个 Proxmox,用图形化仪表盘来管理容器和系统。甚至也可以试试它在 FreeBSD 世界里的对应物 Sylve。但我还是很喜欢自己这次走过的这条路,因为整个过程让我学到了很多。

其中几个最主要的收获是:

  • 那台 Ubuntu 服务器真的非常皮实。十年来,它一直把我这些站点的负载扛得很好。最后四年甚至一次都没重启过。而这一切并不需要多复杂的配置。
  • 配置 FreeBSD 比我原本想象的容易。我很喜欢把系统配置集中在一个地方管理的感觉,而且它的在线文档也真的非常好。
  • 想自己搭一台托管博客的机器,往往需要大量网络知识,而这些远远超出了一个游戏开发者通常具备的范围。
  • 学习另一套系统真的很有趣。也许下次我会试试 OpenBSD 或 NetBSD。

不过说到底,这一切也没什么意义,因为我的大部分流量现在反正都来自各种 AI 抓取器……

Refs

【翻译】Programming Still Sucks:编程依然很糟糕

前言

文章来源:stvn.sh / Writing

原文作者:Jerome Choo

原文标题:Programming Still Sucks

原文链接:https://www.stvn.sh/writing/programming-still-sucks-fqffhyp

本文为全文翻译,译文由 AI 辅助整理,若有理解偏差欢迎指正。


2026 年 4 月 19 日 · 阅读约 8 分钟

编程依然很糟糕

编程 · 领导力 · AI

抱歉,Peter。

——

我在一个生日派对上。来的人大多数也都在科技行业工作,但这种场合里总会有那么一个“有正经工作的人”。你知道的,那种做实体工作的,真的在造某些人们需要的东西的人。而这个人总会问出某种变体的同一个问题:你不担心 AI 会抢走你的工作吗?我朝四周瞥了一眼,看见几张脸微微转过来,轻轻翻了个白眼,然后又回到他们原本的聊天里。对,又是这个问题。

他们有个外甥在帮人搭 Shopify 商店。他们听不懂那孩子嘴里一半的词,但他说自己麻烦大了,而且科技行业里所有人都一样。他的外甥是不是得去学一门“手艺”?我们是不是都得这样?

如果酒喝得够多,我就会认真回答,因为那时候我已经不在乎别人觉得我说的话有没有意思、是不是真的了。但通常我只会叹口气,说一句:“当然,会有一点。我们大多数人都会。不担心才奇怪吧,对吧?”他们点点头,然后就转去聊个更轻松的话题,比如我们会不会去核平伊朗。

但事实是,在科技行业工作一直都很糟糕,而且从来都不是他们以为的那个样子。

有些人以为,我的工作就是坐在一张干净的桌子前,待在一间角落办公室里,外面是开放式办公区,摆满长桌,上头放着一排排 MacBook 或 ThinkPad。在我的角落办公室里,我制定完美的计划,我那群完美的员工会为我鼓掌喝彩。没有任何东西能逃过我的目光,每一个决定都由我完美地做出,每一分钱、每一分钟都被精确记录。

当掌声散去,我的员工——或者叫下属,或者在我心情好的时候叫“我的团队”——就开始疯狂敲键盘。敲啊敲,敲啊敲。不久之后,完美的软件就诞生了。它从集体流水线上滚下来,像第一个孩子一样,绝不会出错。

但现实根本不是这么回事。是的,我确实也很不爽自己从来没混到一间角落办公室,但我更忙着陷入恐慌,因为我根本不知道自己在干什么,没人知道,而现在整辆车的轮子都已经飞出去了。CEO 说 AI 让他那位朋友 Jared 的团队生产力高得惊人,高到他能裁掉一半人;但他说这话时像是在炫耀,不是在威胁?我不知道,反正我是感到威胁了,不过这大概只是我的焦虑症又犯了。没事,我总能从那几个在厕所里哭的员工那里借一片 Xanax 吧。

想象一下,你接了一份船长的工作。第一天,你骑车来到港口,兴冲冲地准备见你的船员。你注意到船并不在那儿,但那个你之前聊过、情绪特别亢奋的招聘人员 Greg 朝你挥手,跟你保证这不是问题。接着你被绑到一台投石机上,竟然奇迹般地被发射到了船上。上一任船长在 Captainpalooza 2025 上听另一个船长给他解释了内燃机的概念,然后就想“朝着这个方向开始迭代”。于是他把船点着了。他后来被人从船上推了下去,但顺手把使用手册也一起带走了。这本来也不该是问题,偏偏整艘船都是专门为他定制打造的。船上倒是还有帆,可它们没连在桅杆上;而那台半固定在船尾的内燃机,零件还散落在甲板各处。

你走进甲板下方,想搞明白这艘船到底怎么运作,以及你们究竟要去哪儿。但当你沿着楼梯往下层走时,你却莫名其妙地走进了桅杆里?你问一个水手这是怎么回事。他卡顿了一下,然后说:“你说得完全对!我之前的方法有问题,不过这里有一个更好的楼梯实现方案。”接着桅杆啪地一下倒转过来,你又回到了甲板上,回到一开始站着的地方。帆现在也上下颠倒了,而你的“水手”还兴奋地等着你夸他干得多好。

你又去问另一个人:“等等,我们到底是要去哪儿?”太好了,是个人类!没有卡顿,没有热情却没帮助的回答,是真正的人。她已经一周没睡了。她几乎没看你,只是说:“去问领航员。”
“领航员?”你问。
她抬手一指。那个领航员,是一个按下背后按钮就会说“勇往直前,步步高升”的娃娃。

那娃娃着火了。

这就是现在的工作。你站在一艘着火的船上,手里拿着一张地图,试图搞明白我们他妈到底要去哪里,以及我们到底要怎么去。

你知道这艘船。有些人曾经就在一艘一模一样的船上当工程师。有些人曾经就是那个离开的船长。我不是写给生日派对上的那个“有正经工作的人”看的。我是写给你的。

你曾经也是工程师。你记得代码评审原本是用来做什么的。你记得自己也曾是那个新人,第一份 PR 被某个资深工程师撕得粉碎,但对方愿意花时间解释“为什么”。你不是在 2024 年某天早上醒来后,突然决定要废掉这一切的。

真正发生的是:跑道被砍掉了。董事会会议里压根没有出现“价值观”这个词。CFO 手里拿着一张电子表格。CEO 则刚从一次线下管理层 retreat 回来,有人在那里给他看了一个 agent 在十四分钟里写完一整个功能的演示,而他竟然信了——就像人们在想相信某件事的时候,总是特别容易相信那样——然后他告诉董事会,到第二季度之前,他可以把工程团队裁掉 30%。现在,你的工作就是想办法把这件事做成。

你告诉自己,新人不会有事的。他们会适应,会重新学习技能,会找到别的出路。你告诉自己,资深工程师可以吸收掉少掉的人手,agent 会把缺口补上。你告诉自己,下个季度再回头看看。你在那张名单上签了字。你回了家。你比平时多喝了一点。然后你上床睡觉。

你知道的。

你知道,因为你也曾经是那个工程师——那个不得不去收拾上一位被“简单答案”忽悠瘸了的领导留下烂摊子的工程师。你看着 Goodhart 定律一口一口吞掉速度指标、故事点、测试覆盖率;几乎所有非工程背景的人手里拿过、并当成“工作一切顺利”证据的数字,最后都被它吃掉。你知道,当你引入工具的速度快于培养判断力的速度时,DORA 指标早就在告诉你部署稳定性会发生什么。你知道,当那些本来会发现错误的人被挤出去,或者学会了不再发现错误时,代码库会变成什么样。

你知道。可你还是签字了。因为不这么做的代价,可能就是丢掉工作;而工作背后是房贷、学费、签证,以及那个“等局势稳定下来、以后再把事情修好”的你自己。

可“以后”从来不会来。我们都知道。我也签过一张名单。直到现在,我们还在互相指责到底是谁那张名单更糟。

已经没有新人了。2024 年,我们为他们的消失办过一场葬礼。没人来。机器现在在做他们做的事,而且更便宜。当然,新人的价值从来都不在于他们眼下能产出什么,而在于他们将来会成为什么:那个知道“尸体埋在哪儿”的资深工程师。我们为产出做了优化,于是废除了学徒制。再过几年,我们会一脸困惑地问:那些资深工程师都去哪儿了?是我们亲手把他们打死了。没人会记得。

可即便如此……

在你的某处基础设施里,一定有一个 cron job。它在凌晨 3 点运行。从 2016 年起,它就一直在跑。它做着某件关键的事情。你没法准确告诉我它到底做什么,但你知道有一个人肯定知道,而那个人在 2019 年就离职了。文件开头的注释写着:# DO NOT CHANGE!!! Ask Ben。Ben 联系不上了。过去四年里的每一次路线图规划会议上,“modernize legacy cron(现代化改造遗留 cron)”都会作为候选项目出现。它从来没排上优先级。你甚至亲手把它从列表里删掉过两次。

但总有人让它继续跑着。她叫 Sara。你并不知道这件事。

她五十多岁。她没去过 Captainpalooza。她以前在离总部三条街外的一间小办公室上班。去年,为了省钱,有人把那间办公室关掉了。离她最近、同时有桌子和网络连接的地方就是这艘船,所以她现在会自己带午餐,然后顺着舷梯走到甲板下的一间小舱室。船上没人知道她在那里。还记得 Ben 吗?Ben 从 1998 年开始带她,她后来从 Ben 那里接过了这个 cron job。

她知道 Ben 几年前已经去世了。她还参加了他的葬礼。你不知道这件事。

那个任务卡住的时候——而且它经常会卡住——她就轻轻推它一下,它就会再试一次。电话响了。她确认告警。她推一下。这个任务依赖一个早已失落于时间洪流中的模块。嗯,也不算完全失落,因为 Ben 去世后,她在他的办公桌里找到了一根 U 盘,里面正好有一份备份。没有 agent 碰过它。以后也不会有。

她不是这个行业里“最安全”的人。她代表着一种你无法触碰的东西。她就是你那场转型刚刚删除掉的所有组织记忆,装在一个五十五岁的身体里四处走动。她正是从你亲手废掉的那条学徒制管道里成长出来的:Ben、1998 年、那根 U 盘。她就是那条管道。当她死去时,那个能生产出像她这样的人才的系统,也早就已经不存在了。三年前,是你亲手杀掉它的。你根本不可能再招到一个能替代她的人,因为制造她的机器已经被你砸烂了。

她就像那个拿着勺子在魔多地下挖隧道的人。勺子是她的,隧道也是她的。没有别人想要这把勺子,也没有别人想要这条隧道。而等她死了,这个 cron job 也会死,工资发放会停摆,一家有三万名员工的公司将不得不想办法重新给所有人发薪水,而到那时,只会剩下一个答案:去找一个手里有勺子的人。你找不到的。是你自己确保了这一点。

那个 cron job 负责发工资。你并不知道这件事。

生日派对上的那个家伙还在等我回答。现在我已经喝得太多,没法再撒谎了。我告诉他:不是 AI 抢走了我们的工作。是贪婪。是同一种贪婪——那种把工厂迁去孟加拉、让刚果钴矿里继续存在奴工的贪婪——只不过现在换上了一张新面具。告诉你那个外甥,去做点别的吧。做什么都行。那也未必救得了他,但至少他不用假装,摧毁他生活的东西是一台机器人。

除了 Sara。她在甲板下,带着她的 U 盘。他们动不了她,因为他们甚至不知道她在那里。

而我们其余的人,都在甲板上,盯着那些上下颠倒的桅杆,琢磨那边那个娃娃到底是干什么的。

那娃娃着火了。

Refs

【翻译】如果你以为写代码速度是你的问题,那你还有更大的问题

前言

文章来源:Andrew Murphy

原文标题:If You Thought the Speed of Writing Code Was Your Problem You Have Bigger Problems

原文链接:https://andrewmurphy.io/blog/if-you-thought-the-speed-of-writing-code-was-your-problem-you-have-bigger-problems

本文为博主翻译,若有理解偏差欢迎指正。


周二早上,你们 VP of Engineering 站在投影前,兴奋得像刚在 2017 年买到第一枚加密货币。TA 刚从某个大会(或者厂商晚宴)回来,三杯黑皮诺下肚,看完一场 Demo,然后带回了“好消息”:

“我们要给所有团队上线 AI 编码助手。早期数据显示,代码产出提升 40%。这会彻底改变我们的研发速度。”

会议室里就会出现那种经典场面:一半人在点头,另一半人突然对自己的笔记本屏幕产生了浓厚兴趣。资深工程师脸上写着“要不要现在说真话,还是回去更新 LinkedIn”。

但没有人问最关键的问题:

你说的速度,是朝着什么目标在加速?

因为你们刚刚做了一件事:在整个交付系统里,挑中了本来就不慢的一环,然后把它继续加速。你们给“非瓶颈”砸了钱。

而系统论告诉我们,这不仅不会帮到你,甚至会让情况更糟。

Goldratt 会想跟你聊聊

1984 年,Eli Goldratt 写了《The Goal》。这是一本讲制造业的小说,却对软件交付异常适用。

核心思想是约束理论(Theory of Constraints)

  • 每个系统只有一个真正约束(瓶颈);
  • 整体吞吐量由这个瓶颈决定;
  • 在瓶颈解决前,优化别的环节意义不大。

很多人理解到这里就停了。真正可怕的是下一句:

当你优化的不是瓶颈时,你得到的不是“更快系统”,而是“更坏系统”。

很直观:A 工位更快了,但瓶颈 B 速度不变,于是 A 和 B 之间堆起半成品;库存上升,交付周期变长,B 工位被淹没,优先级更混乱,质量也会下降。

你并没有提速,你只是制造了一场交通堵塞,并把它叫做“生产力”。

恐怖现场:当你“3 倍代码产出”后会发生什么

开发者 PR 提交更快了,听起来很好。但评审人数没变,没人去扩容 reviewer。

于是 PR 堆在队列里:一天、两天、一周。作者已经上下文切换去写下一个 AI 加持功能,回头再看第一个 PR 时,连自己都快不认识了。为了赶队列,评审开始“橡皮图章式”通过;CI 跑 45 分钟,偶发失败,重跑通过;发布还需要人工审批,而审批人正在开“关于会议的会议”;功能在 staging 再躺三天,因为没人真正对“尽快上线”负责。

同时,开发者已经又提了两个 PR。队列越来越长,在制品(WIP)爆炸,人人手里都有 6 件“进行中”,但“真正完成”的反而更少。真正衡量价值交付速度的 cycle time 不降反升。

你会得到一个很荒诞的局面:

  • 代码更多;
  • 软件交付更少;
  • 仪表盘显示“生产力 +40%”。

你们建成了一个世界级工厂:特别擅长生产会堆在地上腐烂的库存。

更糟的是,很多 AI 生成代码没有被任何人真正理解。提示词的人不一定真正“写过”它;值班排障的人也不一定懂它。于是系统可出故障面积变大,而能推理系统的人变少。

更多代码,更少理解。这不是生产力提升,这是定时炸弹。

那真正的瓶颈在哪?

沿着价值流走一遍:从“有人提出想法”到“用户真正获得价值”。瓶颈会自己跳出来。

1. 你根本不清楚该做什么

PM 两个月没访谈真实用户;需求是三句 Jira + 一个 Figma;工程师每天要替产品做几十个没人定义的细节决策。大家在猜。

结果是:你可能用 6 周做了一个功能,最后只有 11 个人用,其中 9 个还是内部 QA。

这不是“交付慢”,而是“我们到底在干嘛”。

在这种环境里加速写代码,只会更快把错误功能做完。

瓶颈是“理解问题”,不是“敲键盘速度”。

2. 代码“写完”之后的所有环节

在多数组织里,写代码可能只占 20%,其余 80% 都在排队。

评审、CI、staging、QA、安全审查、产品验收、发布窗口、灰度……代码在各个环节之间静止等待。很多功能代码半天写完,却两个月后才到生产。

你看见过“紧急修复”9 天才上线,就知道瓶颈根本不在编码。

想提速,先看“等待时间”,而不是“编码时间”。

3. 发布信任的恶性循环

测试不稳定、可观测性混乱、灰度流程没人信,团队越来越怕发布。越怕越攒大包,包越大风险越高,风险越高就更怕。

这时再提高代码产出,只会把“恐惧文化”喂得更肥:更多代码、同样恐惧、更大批次、更低发布频率。

4. 上线了,但到底有没有效果?没人知道

功能发布后,没有像样分析、没有用户回访、没人复盘“问题是否被解决”。于是下一个需求继续猜。

你只是更快地重复“做了—发了—耸肩”的循环。

5. 你的日历才是承重墙

有时瓶颈不是技术,而是协作:

  • 等一个决策会议;
  • 三个团队一个月没对齐 API;
  • 某架构师成了所有设计的单点审批;
  • 季度规划流程太重,紧急事项也要排队。

这都是组织问题、人问题、协调问题。

写代码更快,对这些问题的作用是 0

应该做什么(不性感但有效)

  • 画出价值流:把一个功能从想法到上线的每一步写下来,也写下步骤之间“等了多久”。
  • 衡量 cycle time,不是产出量:别再盯代码行数、PR 数、故事点;看从提交到用户拿到价值要多久。
  • 消灭等待态:评审慢就改评审机制;发布卡人工审批就自动化或降低摩擦;决策依赖会议就拆小决策。
  • 少开工,多完工:限制 WIP,3 个真正完成比 10 个进行中更有价值。
  • 听一线团队的:开发者早就知道瓶颈在哪,只是通常没人认真听。

结语

如果真的想加速交付,正确的管理台词不该是“代码产出提升 40%”,而应是:

“我们做了价值流分析,发现功能平均在流程间等待 9 天。接下来我们要把这个时间砍半。”

写代码速度从来不是大多数团队的核心问题。真正的优势不属于“写得最快”的团队,而属于能持续做到这三件事的团队:

  1. 搞清楚该做什么;
  2. 把它做出来;
  3. 快速、稳定地送到用户手里。

修瓶颈。瓶颈不在键盘。

[翻译] 不要让 AI 替你写作

原文:Don’t Let AI Write For You 发布于 2026年3月8日

当你写一篇文档或文章时,你其实是在提出一个问题,然后回答它。例如,一份 PRD 回答的是”我们应该构建什么?”,一份技术规范回答的是”我们应该如何构建?”有时问题更难回答——”我们到底想要实现什么?”而每一次尝试作答,你都在反思自己是否问对了问题。

但现在,当然,我们有了 LLM。我看到越来越多由 LLM 生成的文档、文章和随笔。对此我想发出警示:每一篇由 LLM 生成的文档,都是一次错失的思考机会,也是一次错失的建立信任的机会。

思考

写作的目的不在于”写完”,而在于加深自己的理解,进而加深周围人的理解。当你被要求写点什么时,你的任务是深入那片混沌,带着结构和理解走出来——征服未知

写作的第二层目标是让自己变得更有能力。这就像锻炼身体。每一次在能力边界上多做一个动作,你就会变得更强。这既不舒适,也需要付出努力。

让 LLM 替你写作,就像花钱雇人替你健身。

建立信任

LLM 生成的写作还带来社会层面的影响。当我发给别人一份带着 LLM 气息的文档时,我只是在证明 LLM 能生成一些近似于”别人想听的话”的内容,而并没有展示出我曾真正与这些想法搏斗过。

这会损害我的公信力——作为一个能够主导这份文档所提出的任何项目的人,这种信任至关重要。这实在可惜,因为我本可以借此机会建立自己的公信力。

LLM 生成的写作不仅损害了写作本身的真实性,也损害了其背后思想的真实性。如果文字是自动生成的,那么其中的想法是否也是如此?

在 Oxide 使用 LLM:LLM 作为写作者

LLM 如何融入写作过程

LLM 在研究和校对工作方面很有价值。它们也适合快速记录信息或转录文字(这两者都不是我所说的”写作”,即”写一篇文章”的那种意义)。

它们尤其擅长生成创意。这一用途之所以有效,是因为即使它们生成了 10 个想法而只有一个有用,也毫无损失。你可以取其精华,弃其糟粕。

这些 LLM 将提高软件交付的效率。但要充分发挥其价值,我们需要在运用它们的同时,同步提升自身的思考深度。

Kubernetes kubectl –raw 使用指南

什么是 kubectl –raw?

kubectl --raw 是一个强大的底层工具,允许你直接访问 Kubernetes API Server 的 REST API,绕过 kubectl 的客户端逻辑、准入控制器(Admission Controllers)和 Webhook。

为什么需要 –raw?

标准 kubectl 的请求流程

kubectl 命令
    ↓
客户端验证和处理
    ↓
Admission Controllers
    ↓
Mutating Webhooks (修改请求)
    ↓
Validating Webhooks (验证请求)
    ↓
API Server 存储到 etcd

kubectl –raw 的请求流程

kubectl --raw
    ↓
直接 HTTP 请求到 API Server
    ↓
绕过大部分中间件
    ↓
直接操作 etcd

适用场景

  1. 绕过 Webhook 干扰 – 当 Mutating/Validating Webhook 阻止正常操作时
  2. 调试 API Server – 排查 kubectl 客户端与 API Server 的交互问题
  3. 访问特殊端点 – 访问 metrics、healthz 等非资源端点
  4. 绕过客户端限制 – kubectl 版本不支持某些新特性时
  5. 性能测试 – 直接测试 API Server 响应时间
  6. 修复僵尸资源 – 清理被控制器锁定的资源状态

基本语法

# 基本格式
kubectl get --raw <API-PATH>

# 或在某些版本中
kubectl --raw <API-PATH>

常用操作示例

1. GET 请求 – 查询资源

查看集群级别资源

# 获取所有节点
kubectl get --raw /api/v1/nodes | jq .

# 获取特定节点
kubectl get --raw /api/v1/nodes/node-name | jq .

# 获取节点状态
kubectl get --raw /api/v1/nodes/node-name/status | jq .

# 获取所有命名空间
kubectl get --raw /api/v1/namespaces | jq .

查看命名空间级别资源

# 获取 default 命名空间的所有 Pod
kubectl get --raw /api/v1/namespaces/default/pods | jq .

# 获取特定 Pod
kubectl get --raw /api/v1/namespaces/default/pods/pod-name | jq .

# 获取 Deployment
kubectl get --raw /apis/apps/v1/namespaces/default/deployments/deploy-name | jq .

# 获取 Service
kubectl get --raw /api/v1/namespaces/default/services/svc-name | jq .

查看子资源

# Pod 日志
kubectl get --raw /api/v1/namespaces/default/pods/pod-name/log

# Pod 状态
kubectl get --raw /api/v1/namespaces/default/pods/pod-name/status | jq .

# Service 的 Endpoint
kubectl get --raw /api/v1/namespaces/default/endpoints/service-name | jq .

2. PUT 请求 – 完整更新资源

# 更新节点(先获取,修改,再替换)
kubectl get --raw /api/v1/nodes/node-name > node.json

# 编辑 node.json 文件
vim node.json

# 替换(注意:不同版本语法可能不同)
kubectl replace --raw /api/v1/nodes/node-name -f node.json

# 或使用 kubectl proxy 方式
kubectl proxy --port=8001 &
curl -X PUT \
  -H "Content-Type: application/json" \
  -d @node.json \
  http://localhost:8001/api/v1/nodes/node-name

实战案例:清除节点僵尸条件

# 获取节点当前状态
kubectl get --raw /api/v1/nodes/node-name > /tmp/node.json

# 使用 jq 删除特定条件
jq 'del(.status.conditions[] | select(.type == "EtcdIsVoter"))' \
  /tmp/node.json > /tmp/node-fixed.json

# 更新节点状态
kubectl replace --raw /api/v1/nodes/node-name/status -f /tmp/node-fixed.json

3. POST 请求 – 创建资源

# 创建 Pod
cat > pod.json <<EOF
{
  "apiVersion": "v1",
  "kind": "Pod",
  "metadata": {
    "name": "test-pod",
    "namespace": "default"
  },
  "spec": {
    "containers": [{
      "name": "nginx",
      "image": "nginx:latest"
    }]
  }
}
EOF

kubectl create --raw /api/v1/namespaces/default/pods -f pod.json

4. DELETE 请求 – 删除资源

# 删除 Pod
kubectl delete --raw /api/v1/namespaces/default/pods/pod-name

# 使用 kubectl proxy 方式
kubectl proxy --port=8001 &
curl -X DELETE http://localhost:8001/api/v1/namespaces/default/pods/pod-name

5. PATCH 请求 – 部分更新

# JSON Patch (精确的操作指令)
kubectl patch --raw /api/v1/nodes/node-name \
  --type='json' \
  -p='[
    {"op": "add", "path": "/metadata/labels/new-label", "value": "new-value"},
    {"op": "remove", "path": "/status/conditions/0"}
  ]'

# Strategic Merge Patch (合并式更新)
kubectl patch --raw /api/v1/nodes/node-name \
  --type='merge' \
  -p '{
    "metadata": {
      "labels": {
        "environment": "production"
      }
    }
  }'

# Merge Patch (简单合并)
kubectl patch --raw /api/v1/nodes/node-name \
  --type='merge' \
  -p '{"spec":{"unschedulable":true}}'

API 路径规则

核心 API 组 (Core API Group)

# 格式
/api/v1/<resource-type>                        # 集群级别
/api/v1/namespaces/
<namespace>/<resource-type> # 命名空间级别

# 示例
/api/v1/nodes
/api/v1/nodes/node-name
/api/v1/nodes/node-name/status
/api/v1/namespaces/default/pods
/api/v1/namespaces/default/pods/pod-name
/api/v1/namespaces/default/services

命名 API 组 (Named API Groups)

# 格式
/apis/
<group>/<version>/<resource-type>
/apis/
<group>/<version>/namespaces/<ns>/<resource-type>

# 常用 API 组示例
/apis/apps/v1/deployments                           # Deployment
/apis/apps/v1/namespaces/default/deployments
/apis/batch/v1/cronjobs                             # CronJob
/apis/networking.k8s.io/v1/ingresses                # Ingress
/apis/rbac.authorization.k8s.io/v1/clusterroles     # ClusterRole
/apis/storage.k8s.io/v1/storageclasses              # StorageClass

子资源 (Subresources)

# 状态子资源
/api/v1/nodes/
<name>/status
/apis/apps/v1/namespaces/
<ns>/deployments/<name>/status

# 日志
/api/v1/namespaces/
<ns>/pods/<name>/log
/api/v1/namespaces/
<ns>/pods/<name>/log?container=container-name

# 执行命令
/api/v1/namespaces/
<ns>/pods/<name>/exec

# 端口转发
/api/v1/namespaces/
<ns>/pods/<name>/portforward

# 代理
/api/v1/nodes/
<name>/proxy
/api/v1/namespaces/
<ns>/pods/<name>/proxy
/api/v1/namespaces/
<ns>/services/<name>/proxy

特殊端点

查看 API 资源

# 列出所有 API 版本
kubectl get --raw /apis | jq '.groups[].name'

# 查看特定 API 组
kubectl get --raw /apis/apps/v1 | jq .

# 列出所有可用资源
kubectl get --raw /api/v1 | jq '.resources[].name'

# OpenAPI 规范
kubectl get --raw /openapi/v2 | jq . > openapi.json

集群信息

# 版本信息
kubectl get --raw /version | jq .

# 健康检查
kubectl get --raw /healthz
kubectl get --raw /livez
kubectl get --raw /readyz

# API Server 标志
kubectl get --raw /debug/flags/v

# Metrics
kubectl get --raw /metrics

认证和授权

# 检查当前用户权限
kubectl get --raw /apis/authorization.k8s.io/v1/selfsubjectaccessreviews \
  -X POST \
  -d '{
    "apiVersion": "authorization.k8s.io/v1",
    "kind": "SelfSubjectAccessReview",
    "spec": {
      "resourceAttributes": {
        "namespace": "default",
        "verb": "get",
        "resource": "pods"
      }
    }
  }'

使用 kubectl proxy 的方式

kubectl --raw 不可用或语法复杂时,可以使用 proxy 方式:

# 启动代理
kubectl proxy --port=8001 &

# 使用 curl 访问
curl http://localhost:8001/api/v1/nodes | jq .

# GET 请求
curl http://localhost:8001/api/v1/namespaces/default/pods

# POST 请求
curl -X POST \
  -H "Content-Type: application/json" \
  -d @pod.json \
  http://localhost:8001/api/v1/namespaces/default/pods

# PUT 请求
curl -X PUT \
  -H "Content-Type: application/json" \
  -d @node.json \
  http://localhost:8001/api/v1/nodes/node-name/status

# DELETE 请求
curl -X DELETE \
  http://localhost:8001/api/v1/namespaces/default/pods/pod-name

# 停止代理
pkill -f "kubectl proxy"

实战案例

案例 1: 绕过 Webhook 修改节点标签

# 问题:Mutating Webhook 拦截标签修改
# 解决:直接通过 API 修改

# 1. 获取节点
kubectl get --raw /api/v1/nodes/node-name > node.json

# 2. 使用 jq 添加标签
jq '.metadata.labels["custom-label"] = "custom-value"' node.json > node-updated.json

# 3. 替换节点
kubectl replace --raw /api/v1/nodes/node-name -f node-updated.json

案例 2: 清理僵尸 Finalizer

# 问题:资源因 finalizer 无法删除
# 解决:直接清空 finalizers

# 1. 获取资源
kubectl get --raw /api/v1/namespaces/stuck-namespace > ns.json

# 2. 清空 finalizers
jq '.spec.finalizers = []' ns.json > ns-clean.json

# 3. 更新
kubectl replace --raw /api/v1/namespaces/stuck-namespace/finalize -f ns-clean.json

案例 3: 批量查询资源状态

#!/bin/bash
# 批量检查节点状态

for node in $(kubectl get nodes -o name | cut -d/ -f2); do
  echo "=== Node: $node ==="
  kubectl get --raw /api/v1/nodes/$node/status | \
    jq -r '.status.conditions[] | select(.type=="Ready") | 
    "Status: \(.status), Reason: \(.reason)"'
done

案例 4: 性能测试

#!/bin/bash
# 测试 API Server 响应时间

echo "Testing API Server performance..."
for i in {1..10}; do
  time kubectl get --raw /api/v1/nodes > /dev/null 2>&1
done

案例 5: 导出所有资源

#!/bin/bash
# 导出命名空间的所有资源

NAMESPACE="default"
OUTPUT_DIR="./k8s-backup"
mkdir -p $OUTPUT_DIR

# 导出 Pods
kubectl get --raw /api/v1/namespaces/$NAMESPACE/pods | \
  jq . > $OUTPUT_DIR/pods.json

# 导出 Services
kubectl get --raw /api/v1/namespaces/$NAMESPACE/services | \
  jq . > $OUTPUT_DIR/services.json

# 导出 Deployments
kubectl get --raw /apis/apps/v1/namespaces/$NAMESPACE/deployments | \
  jq . > $OUTPUT_DIR/deployments.json

echo "Backup completed in $OUTPUT_DIR"

注意事项

1. 权限要求

# 需要相应的 RBAC 权限
# 检查权限
kubectl auth can-i get nodes
kubectl auth can-i update nodes

2. resourceVersion 冲突

# 更新时可能遇到冲突
# Error: the object has been modified; please apply your changes to the latest version

# 解决:重新获取最新版本
kubectl get --raw /api/v1/nodes/node-name > node-latest.json
# 重新修改并更新

3. 数据格式验证

# 使用 jq 验证 JSON 格式
cat resource.json | jq . > /dev/null

# 如果有错误会提示

4. 备份重要资源

# 在修改前务必备份
kubectl get --raw /api/v1/nodes/node-name > node-backup-$(date +%Y%m%d).json

5. 只读操作优先

# 先用 GET 查看,确认无误后再 PUT/PATCH
kubectl get --raw /api/v1/nodes/node-name | jq .

版本兼容性

Kubernetes 1.18+

kubectl get --raw /api/v1/nodes
kubectl create --raw /api/v1/namespaces/default/pods -f pod.json
kubectl replace --raw /api/v1/nodes/node-name -f node.json
kubectl patch --raw /api/v1/nodes/node-name --type=merge -p '{...}'
kubectl delete --raw /api/v1/namespaces/default/pods/pod-name

早期版本或不支持时

# 使用 kubectl proxy
kubectl proxy --port=8001 &
curl http://localhost:8001/api/v1/nodes

调试技巧

1. 查看完整请求

# 增加日志级别
kubectl get --raw /api/v1/nodes -v=8

2. 使用 jq 过滤输出

# 只查看节点名称
kubectl get --raw /api/v1/nodes | jq '.items[].metadata.name'

# 查看 Pod 状态
kubectl get --raw /api/v1/namespaces/default/pods | \
  jq '.items[] | {name: .metadata.name, status: .status.phase}'

3. 格式化时间戳

# 转换时间格式
kubectl get --raw /api/v1/nodes/node-name | \
  jq '.metadata.creationTimestamp | fromdate | strftime("%Y-%m-%d %H:%M:%S")'

总结

kubectl --raw 是 Kubernetes 的”瑞士军刀”,提供了:

直接访问 API – 绕过客户端限制
调试工具 – 排查 kubectl 和 API Server 问题
应急修复 – 处理 Webhook 和控制器导致的问题
性能测试 – 直接测试 API Server
学习工具 – 理解 Kubernetes API 结构

⚠️ 使用场景: 作为最后的调试和修复手段
⚠️ 不推荐: 日常操作应使用标准 kubectl 命令
⚠️ 需谨慎: 直接操作可能破坏资源状态

参考资源

磁盘使用分析工具对比:du vs ncdu vs gdu vs dust

Claude Sonnet 4.5 协助编写。

在日常的系统管理和磁盘空间清理工作中,我们经常需要分析磁盘使用情况。本文将对比四个常用的磁盘使用分析工具:传统的 du、经典的交互式工具 ncdu、现代化的 gdudust

工具简介

du (Disk Usage)

du 是 Unix/Linux 系统自带的经典磁盘使用分析工具,已经存在了几十年。它是最基础、最通用的选择。

ncdu (NCurses Disk Usage)

ncdu 是基于 ncurses 库的磁盘使用分析工具,提供了简洁的交互式文本界面。它是最早流行的交互式磁盘分析工具之一。

gdu (Go Disk Usage)

gdu 是用 Go 语言编写的现代化磁盘分析工具,提供了交互式界面和更快的扫描速度。

dust (du + rust = dust)

dust 是用 Rust 编写的磁盘使用分析工具,以更直观的可视化输出为特色。

功能对比

特性 du ncdu gdu dust
交互式界面
扫描速度 中等 中等
可视化输出 基础 中等 强大 优秀
系统自带
内存占用
删除文件功能
编程语言 C C Go Rust
易用性

使用示例

du 基本用法

# 显示当前目录大小
du -sh

# 显示所有子目录大小并排序
du -h --max-depth=1 | sort -hr

# 显示最大的10个目录
du -h | sort -rh | head -10

优点:

  • 系统自带,无需安装
  • 稳定可靠,脚本友好
  • 广泛的兼容性

缺点:

  • 速度较慢
  • 输出不够直观
  • 缺少交互功能

ncdu 基本用法

# 安装
# Debian/Ubuntu
sudo apt install ncdu

# macOS
brew install ncdu

# RHEL/CentOS
sudo yum install ncdu

# 分析当前目录
ncdu

# 分析指定目录
ncdu /path/to/directory

# 扫描时排除某些目录
ncdu --exclude /path/to/exclude

# 导出结果到文件(可在其他机器上查看)
ncdu -o result.json
ncdu -f result.json  # 读取导出的文件

交互式操作:

  • ↑↓j/k: 上下移动
  • Enter: 进入目录
  • : 返回上级目录
  • d: 删除选中的文件/目录
  • g: 显示百分比/图形条
  • n: 按名称排序
  • s: 按大小排序
  • q: 退出

优点:

  • 成熟稳定,广泛使用
  • 交互式界面简洁清晰
  • 可以直接删除文件
  • 支持导出和导入扫描结果
  • 内存占用合理
  • 在大多数发行版仓库中可用

缺点:

  • 扫描速度比 gdu 慢
  • 界面相对传统,不如 gdu 美观
  • 大型目录扫描时需要等待

gdu 基本用法

# 安装
# macOS
brew install gdu

# Linux
curl -L https://github.com/dundee/gdu/releases/latest/download/gdu_linux_amd64.tgz | tar xz
sudo mv gdu /usr/local/bin/

# 分析当前目录
gdu

# 分析指定目录
gdu /path/to/directory

# 非交互模式
gdu -n /path/to/directory

优点:

  • 扫描速度极快
  • 交互式 TUI 界面,可以用键盘导航
  • 可以直接在界面中删除文件
  • 支持彩色输出
  • 可以显示进度条

缺点:

  • 需要单独安装
  • 交互模式在某些脚本场景下不适用

dust 基本用法

# 安装
# macOS
brew install dust

# Linux
cargo install du-dust

# 基本使用
dust

# 分析指定目录
dust /path/to/directory

# 显示更多层级
dust -d 3

# 只显示目录
dust -t

优点:

  • 树状图可视化输出,非常直观
  • 彩色条形图显示占用比例
  • 输出清晰易读
  • 速度较快
  • 默认排序输出

缺点:

  • 需要单独安装
  • 没有交互式界面
  • 相对 du 功能较新,可能有兼容性问题

实际使用场景推荐

选择 du 的场景

  • 在生产服务器上进行快速检查
  • 编写自动化脚本
  • 需要最大兼容性
  • 系统资源受限

选择 ncdu 的场景

  • 需要交互式浏览但服务器上没有 gdu
  • 偏好传统稳定的工具
  • 需要导出扫描结果到其他机器分析
  • 在资源受限的系统上需要交互功能
  • 系统包管理器中已有 ncdu

选择 gdu 的场景

  • 需要深入分析大型目录结构
  • 需要最快的扫描速度
  • 追求现代化的交互体验
  • 在个人工作站上使用
  • 经常处理超大目录

选择 dust 的场景

  • 需要快速浏览目录大小
  • 偏好可视化输出
  • 想要更现代化的工具体验
  • 不需要交互式操作
  • 需要快速生成报告

性能对比

在一个包含 50GB 数据、约 100,000 个文件的目录上测试:

  • du: ~8 秒
  • ncdu: ~5 秒(扫描阶段)
  • gdu: ~2 秒
  • dust: ~4 秒

注:实际性能取决于硬件配置、文件系统类型和文件数量。ncdu 的优势在于扫描后的交互浏览非常流畅。

总结

四个工具各有千秋:

  • du 是经典之选,适合脚本和生产环境
  • ncdu 是稳定可靠的交互式工具,兼具易用性和可用性
  • gdu 是性能之王,提供强大的交互功能和最快速度
  • dust 是可视化专家,输出最为直观

工具演进历史

这四个工具代表了磁盘分析工具的演进过程:

  1. du (1970s): 命令行时代的基础工具
  2. ncdu (2007): 加入交互式界面,提升用户体验
  3. gdu (2020): 现代编程语言带来的性能提升
  4. dust (2018): 注重可视化和用户友好度

我的推荐

对于日常使用,我的建议是:

  1. 保留 du 用于脚本和快速检查
  2. 安装 ncdu 作为通用的交互式工具(服务器友好)
  3. 安装 gdu 用于深入的磁盘分析(个人工作站)
  4. 安装 dust 用于快速浏览和可视化

如果只能选一个额外工具:

  • 服务器环境: 选 ncdu(稳定、轻量、可靠)
  • 个人电脑: 选 gdu(快速、现代、强大)
  • 快速查看: 选 dust(直观、美观、高效)

根据具体需求选择合适的工具,甚至可以将它们组合使用,发挥各自的优势。

参考链接