为什么140次下载等于零用户,9%的失败率等于没人管
- 2026-11-07 15:16:57
Those jobs aren't flaky, they're broken, and they keep running because nobody looks.
三份复盘,三个不相干的项目,主角都是自己项目里的一个数字。
140次下载、35个提交、9%的失败率。三个数字都真实存在,都被认真记下来过,也都在第一时间给了一个让人安心的答案。
问题在于,这三个数量的都不是它们看起来在量的东西。
一、高兴了十分钟的140次下载
一个开发者往npm发了个小包,不是多大的东西。第二天早上打开统计页,140次下载。
Spoiler: they didn't. Nobody did. It was robots.
他说自己有大概十分钟是真心在想「哇,居然真有人需要这个」。
真实的机制很朴素:每次发一个新版本,registry镜像和安全扫描器会立刻把包抓走,跟有没有人类用过毫无关系。这个量通常是每个版本100到150次。
Usually that's somewhere around 100-150 downloads per version, doesn't matter if a single human ever touched it.
再叠上一层常年存在的底噪:机器人每周把所有包的每个版本都拉一遍;再加上CI每次提交都重装依赖。
于是平台上那个「每周下载量」,实际含义只是这个tarball被服务了多少次。不是用户数,很多时候差得很远。
让他不舒服的地方在于,这个数大家都在看。挑两个库对比的时候看,判断自己项目有没有起色的时候看,写README里那句「被数千个项目使用」的时候也看。
他的处理方式是给自己造一把新尺子,叫humanloads:先砍掉每个新版本在头几天被镜像和扫描器抓出来的尖峰,再减掉机器人每周从每个版本拉走的那个固定底数,剩下的用区间加置信度表示。
他自己把边界写清楚了:这不是「人数」,没有人能数出人数,npm也数不出来,CI和依赖安装仍然混在里面。它只是比原始数字更接近现实。
代价是一整个人肉校准过程,以及一个必须自己维护的估算模型。换来的是一个不太好看、但不会再骗自己的数字。
二、35个提交只存在那台笔记本上
第二个项目不是发个包,是把整套开发环境搬走。
一台M2 Pro的MacBook Pro,16GB内存。日常负载是几个Next和Vite的dev server、跑着Postgres和Redis的Docker、两个编辑器、一个开满标签的浏览器,再加上并排开着的agent终端。
内存压力条长期是黄的,风扇没理由地转起来,磁盘被node_modules、镜像和构建缓存一点点吃掉。
该换的是一台内存更大的Mac。他付不起。所以走了另一条路:机器留着,但不再让它干重活。
选的是一台Hetzner AX41拍卖机:Ryzen 5 3600,6核12线程,64GB内存,两块512GB NVMe做RAID1,Ubuntu 26.04,每月57欧元。
Mac上现在主要跑终端、浏览器和编辑器。仓库、依赖、Docker、dev server、数据库、agent,全部搬到服务器上。
组网用的是Tailscale。服务器在tailnet里叫 hause-dev,ufw默认拒绝所有入站,对外只留Caddy的80和443。SSH、mosh、Samba只认tailscale0网卡,公网上连尝试都碰不到。
Docker用SSH context远程跑,docker ps 和 docker compose up 都在服务器上执行,Mac上那个吃掉几个GB的Linux虚拟机没了。服务器上的代码目录通过Samba挂进Finder,按 smb://hause-dev 连,重启自动挂载。
这条也有边界:网络盘延迟135毫秒,npm install 和 git 这种操作还是老老实实在服务器上跑,共享盘只用来浏览、预览、拖文件。
最惊险的一段是搬仓库。他在 ~/code 里堆了很多项目,有些带着没提交的改动,有些有被gitignore的 .env,还有些分支只存在于本地。
他没有直接信rsync,而是三步走:先连同被忽略但重要的文件一起拷过去,再把两台机器上的每个文件做哈希比对,最后单独找那些只存在于Mac、不在服务器上的提交。
Found commits that existed on the Mac but not on the server, and imported them (there were 35 I would have lost).
35个。如果不是专门去对一遍,这些提交会随着旧笔记本一起消失在抽屉里。
还有一条是主动不搬的:凭证。云账号登录、API key、SSH私钥、AWS和Kubernetes配置、各种CLI的token,一个都没拷。服务器上每个工具都重新登录一次。
这更慢,但服务器只拿到他明确给过的权限,他清楚知道是哪些。
三、99.7%的失败来自5%的作业
第三份复盘是对自家服务的12天全量数据做的一次清算。
作者运营一个按计划调用URL的cron服务,能看到大量不同服务器的外部视角:共享主机、WordPress站、Laravel应用、小VPS、serverless函数。
他统计了2026年10月4日到15日全部计划调用,共12天,排除心跳和自家内部作业,只留「调度器通过HTTP调用你的脚本」这一种。
结果是八成的作业完全正常:79.9%一次都没失败,12.0%的失败率低于1%,2.2%在1%到50%之间。剩下5.9%失败过半,其中4.4%每一次调用都失败。
如果只看基本能用的那批,失败率是0.3%,大约千分之三。
真正说明问题的是下面这个分布。
The 5 % of cron jobs with the most failures cause 99.7 % of all failures.
失败最多的5%的作业,制造了99.7%的失败。所以看「平均失败率」这件事本身就错了:把死作业算进去,0.3%会跳到9%以上。
Those jobs aren't flaky, they're broken, and they keep running because nobody looks.
这些作业不是时好时坏,是坏的,而且一直在跑,因为没人看。
死作业里56%拿的是404。模式几乎一样:站点重建了、换了CMS、插件被删了,cron还指着那个早就不存在的文件。什么都没有崩,调度器只是永远收到「找不到」。
能用的作业里,最常见的原因是脚本自己的500,占5.4%;连接中断3.5%、服务器过载2.7%、空响应2.1%、上游502占1.8%、连接被拒1.6%、TLS失败1.1%、服务器不可达0.6%。
还有一个容易被忽略的形状:67%的失败段只包含一次失败,下一次就好了。他的建议因此分了岔:高频作业不要在第一次失败时告警,等连续两三次;一天只跑一次的作业,第一次失败就该告警,因为下一次要等24小时。
时长也在长尾里。一半的作业平均4.4秒跑完,13%不到1秒,但有11%平均超过30秒,多是导入、导出、备份。这群作业真正的风险是超时:共享主机、PHP的 max_execution_time、反向代理、调度器,任何一层到点就把脚本掐在半路。
还有一类失败连错误码都不给:0.4%的作业拿回的是Cloudflare挑战页,调度器解不了JavaScript或CAPTCHA,于是每一次都失败。
最难查的是免费主机:有些对所有非浏览器请求都返回200,附一个JavaScript cookie校验。调度器看到的是成功,脚本根本没跑。
四、三个数字都在量过程
把三份复盘摆在一起,错法几乎一样。
140次下载量的是tarball被取走的次数,用户数是另一个东西;16GB内存压力量的是当下的拥挤程度,35个未推送的提交是另一个东西;9%的失败率是死作业拉高的平均数,真正该看的是那5%的作业和那56%的404。
它们都有同一个特点:测起来太容易,所以被当成了结论。
下载量是一个接口就有的数,内存压力条是一条常驻的曲线,失败率是把所有作业除一下。而真实的答案,都要多做一步——去砍尖峰、去逐文件哈希、去把死作业从分母里拿掉。
多做的那一步没人替你做,也没人提醒你少做了。数字会照常变好看,报表会照常是绿的。
三份复盘里,没有一份是靠新工具解决的。npm的下载口径没变,MacBook的内存没变,cron的失败定义也没变。变的是他们各自往那个数字底下多挖了一层。
关 于 这 个 号
点上面的名片关注,每周更新AI设计实战与工具实测。
热门文章 · 合集《AI设计学习》
Long Labs给球鞋列了17项清单,五轮之后才把孔数对上