大家好,这里是debug滴得太多不小心被流感病毒感染一天用掉三包纸巾的Zeee ( ;´-д-`)。希望大家在debug的时候也要注意防护,别让身体也跟着出bug。

没错。你没有看错。Velas Weekly居然又更新了,明明还没到一年(雾)。毕竟咱是叫Velas Weekly,不是Velas Yearly,想啥时候更新都可以。

可是看上去距离上次Velas Weekly也没更新啥呀,怎么又发了一篇?

对,也不对。

因为我花了近两个月时间,修好了一个bug,光是这个bug我觉得就值得专门开一篇文章来写了。

我治好了电波站的“失熵症”

失熵症是什么?

某位不愿意透露姓名的病友 & 前·格拉默铁骑是这样介绍的:

失熵症是一种奇怪的现象。罹患这种病症的人,物理结构会陷入不可逆的慢性解离。这意味着你正在慢慢消失,而这种「消失」,在旁人眼中甚至难以察觉——你依旧能跑、能跳、能和他人交流。一切看起来都那么正常,只不过你总是比别人慢一点点……然后越来越慢,越来越慢,直到自己和整个世界的轮廓都变得模糊不清。

但这位病友描述得过于抽象,让外人无法清楚认知到这是种什么体验,还以为是什么编剧虚构的病症。直到两个月前,电波站,准确来说是新电波站,也不幸患上了它。

不起眼的征兆

一切缘于我偶然在iPhone的Telegram点开了电波站的文章链接,网站没有像往常一样迅速打开,反而好长时间没有响应,然后在Safari内核弹出了这样的报错:FetchEvent.respondWith received an error: TypeError: Load failed.

在不同的设备上复现了几次之后,我发现这个报错只会出现在Safari上,包括macOS、iOS和iPadOS上的Safari都有一定概率会报这个错。但诡异的是,在重启了程序之后,它又暂时地“好”了,不管怎么试图复现,也都不会再报错。

在谷歌上逛了好一圈,似乎定位到是由于17.2版本前的Safari在处理FetchEvent.respondWith()时出错导致的,而我的service worker的fetch事件里也刚好用了这个函数。

想着service worker也不是什么刚需的功能,夜长梦多,就暂且把sw的主功能代码移除了,只留了个空壳在跑。

虽说移除了sw之后看上去没再出这个问题了,但我隐隐觉得事情没那么简单。原因有三:

  1. 据最新一条搜索结果表示,Safari的这个bug在17.2之后貌似就已经修复了,而我Safari的版本是17.5,按理说并不会再报这个错了。
  2. 如果这个报错单纯是因为Safari处理FetchEvent.respondWith()这个函数出错导致的话,按理说我重启博客后端并不会缓解这个问题。
  3. 若是据angular的这条Issue所描述,是Safari的bug导致出错的话,它应该会显示TypeError: Internal error,而非像我这样显示TypeError: Load failed才对。

新的风暴

果然,sw的壮士断腕并没有让电波站就此幸免。三天之后,网站又时不时变得无法打开了,或是说,要花很长时间才能收到服务器的响应。(更诡异的是,Express的API响应是正常的,反而是SvelteKit负责的服务端渲染那块变得非常非常慢,甚至有时需要十几秒才有东西返回。)

翻了翻服务器报告,发现这次的报错变成了由undici报的TypeError: fetch failed,error code显示是'UND_ERR_CONNECT_TIMEOUT'

具体的报错我忘记保存了,但与这个Issue类似。同样的还有这几个Issue:1 2 3 4 5

这下就大条了,毕竟在此之前我甚至不知道undici是个什么东西。查了一下才知道是Node.js上用来处理HTTP请求的模块,在我的项目里面主要是被SvelteKit引用,用来处理服务端fetch请求用的。

旧电波站的HTTP请求都是用axios处理的,怪不得没见过。

翻了一下提到这个问题的Issue,触发的原因也是五花八门,大概可以归结为:

  1. 服务器的网络连接问题(可能由服务器的VPN配置导致)
  2. (老版本的)undici在处理IPv6连接的时候有概率会出错
  3. (老版本的)undici对AWS Lambda的支持不佳(已在昨天发布的Node.js 20.16.0 (LTS) 中修复)
  4. 不知道 🤷

说实话,这也是个玄学报错,毕竟可能导致网络连接的原因五花八门,而且最近我这连接服务器的线路也变得很不稳定,时不时连ssh连接都连不上。

试了一下Issue里的解决办法,甚至为了升级undici的版本,将Node.js升到了22.4.1(毕竟在此之前LTS上的undici模块还是老版本),但都没效。反而因为‘current’版本的Node.js不稳定,出现了一些新的报错,所以又灰溜溜地降回了LTS。

就这样靠隔三差五登上去重启程序又苟了几个星期。终于在某天突然发现了一个华点:

不对,SvelteKit的服务端明明是给同一台服务器上的API发请求,为什么还会出现网络连接问题?

重新翻了一下代码,才发现电波站在生产环境的API请求,不论是在服务端还是在客户端(浏览器)上,通通都是发给电波站的域名https://www.velasx.com,而不是localhost

……

神奇的是,这块的代码逻辑已经存在了7年了(可能是因为axios的鲁棒性太好了才没出问题,或者说出了问题我也不知道)。是那个懒得分别处理服务端和客户端请求的我给今天的自己埋下的坑,甚至在重写电波站的时候居然还没反应过来。

谢谢你,摸鱼侠。

把服务端部分的HTTP请求地址改成了http://localhost:xxxx(xxxx为服务端API具体的端口号),准确来说是http://127.0.0.1:xxxx(原因看这里),就没再出现过这个报错了。

If I Can Stop One Heart From Breaking

当然,故事并没有到此结束。

就像在玩文字冒险游戏的时候,玩家只能在Good End/Bad End线接触到真相的冰山一角,只有打通了所有的Good End之后,才能凑齐打开True End线路的大门的钥匙。但玩家在这条True End线的经历,反而不如之前的Good End线来得美好。

因为现实往往是残酷的。

可能上面的表述有些混乱,让我再在这里复述一下这个bug到底是个什么样子:

  1. 在网站程序刚运行的时候看上去一切正常。但随着时间流逝,初次打开页面/刷新页面(触发SvelteKit的服务端渲染)的响应速度会变得越来越慢。从刚开始的50ms左右,在24小时后变成了1 ~ 2s,在48小时会变成4 ~ 6s,72小时后甚至会到10s以上。
  2. 受影响范围似乎仅局限于文章详情页面。其他页面的响应速度一切正常。但由于Node.js应用的特性,所以只要有任意一个人的请求在渲染文章详情页面时卡住了,那整个网站的服务端就会陷入瘫痪状态,其他人将无法打开网站或进行其他操作,例如留言/跳转页面。
  3. 重启网站程序能够使第1点的状态重置。
  4. 后端的REST API响应速度正常。

如上述所示,虽然在陆陆续续修复了控制台打印出来的一个个错误之后,终于不会再看到什么奇怪的报错了。但更可怕的问题也随之而来——这个bug依旧雷打不动地存在着。

就如《三傻大闹宝莱坞》里“消音器”的《屁之颂歌》所说:

震耳欲聋的屁,令人尊敬。

浓淡相宜的屁,可以忍受。

轻如晨风的屁,非常可怕。

沉默无声的屁,极度致命。

没有什么比一个不会报错却影响深远的bug更吓人的东西了。更何况它的影响并非一下就会暴露,而是在生产环境跑了相当长一段时间之后才会慢慢出问题,排查难度更是飙升。即便逐行核对了文章详情页面部分的代码,也没有发现什么可能会随时间对性能影响加重的代码。

这时候的我,俨然已经万策尽了。

仿佛一个刚刚诞生的人造人少女,还没好好看看世间,却已不幸罹患“失熵症”。而我,作为创造者,却只能眼睁睁看着她,举手投足一点点地在自己面前变得越来越慢、越来越慢,直到被无情的现实残忍撕裂成无数肉眼无法观测到的碎片……

(悲痛地狂笑)你小子...还是...还是一点都没变啊!还是...什么都做不到啊!

 

绝境之中,我跪坐在荒无人烟的旷野上,对着乌云密布的天空喊出了那句话:

帮帮我,ChatGPT先生!

于是我抖擞精神,与ChatGPT深入交流了数十个回合。终于,他逐个字逐个字地在我的电脑屏幕上打出了这句话:

“可能是由SvelteKit的某些配置导致了内存溢出。”

这一下子提醒了我。毕竟在上一篇Velas Weekly里面,我也提到过自己对Svelte的Reactive Statement一知半解,甚至有时候即便它能跑,我也不确定自己的写法是对的。而整个网站程序里面,偏偏文章详情页是对Reactive Statement用得最多的。

在社区逛了一圈之后,这个猜想也得到了证实:滥用Reactive Statement确实可能会导致内存溢出。

于是我恶补了Svelte 4的原理,去掉了许多不必要的Reactive Statement。

不知是不是错觉,感觉这一通优化之后,网站运行起来更快了?

 

但是显然最终boss并没有那么容易攻克。问题依旧存在。

——并不是Svelte的问题,毕竟人家还是很鲁棒的。

不过这样一折腾,倒是给了我一个思路。

我给服务器挂上PM2,发现程序其实没有出现显著的内存溢出问题。反而在运行数天之后,用服务端去渲染文章详情页面会持续将CPU跑满,直到服务端返回响应结果。

慢着……CPU?

真相只有一个!

即便这个逆天bug依旧存在,即便控制台依旧没有任何报错,但此刻,我仿佛已经摸到了真理之门的一角。即便没想到解决办法,但已经不再迷茫。

阿尔冯斯,阿尔冯斯,阿尔冯斯!我一定会来接你的,你等着啊!等着啊!

向着罹患“失熵症”的人造人少女落寞的背影,我像爱德华·艾尔利克一样远远朝着她歇斯底里地喊道。

 

只有文章详情页面有,而其他页面没有的函数 + 在服务端渲染的时候被触发 + 高CPU需求

这三点共同指向了一个东西——Markdown渲染

电波站的Markdown渲染由marked.js负责。它正常运行的情况下速度非常快,输出一篇文档仅为几毫秒。而且它在电波站已经兢兢业业稳定运行近8年了,所以我怀疑了好几轮都没怀疑到它头上。

所以问题出在哪呢?

由于marked.js最近两年像打了鸡血一样,版本号突然飙升。旧电波站因为所用Node.js版本较低,所以marked.js停留在了4.2.1版。而新电波站趁着重写,把所有的依赖都升级到了最新版,marked.js自然也升级到了最新的13.0.0。但在升级之余,我并没有好好翻看marked.js的文档,也没去看它各个大版本的Breaking Change到底有什么,只是改了一下原有的代码就搬过来了,想着“能跑就行”。

却没注意到自己用的其实是marked.js新增的全局渲染器设置:marked.use()

虽说如marked.js的文档所言:

All options will overwrite those previously set, except for the following options which will be merged with the existing framework and can be used to change or extend the functionality of Marked: renderer, tokenizer, hooks, walkTokens, and extensions.

旧的设置选项会被新的所覆盖,但唯独渲染器、插件等设置采取的不是“覆盖”的策略,而是“与现有的框架进行合并”。

由于电波站在文章详情页面与RSS Feed使用了不同的渲染格式,所以在每次输出前要先给渲染器套上相应的设置。

但因为渲染器设置是全局的且不互相覆盖,相当于每次在服务端渲染文章详情或是RSS Feed的时候,服务端就会将一套新的渲染器设置合并到了现有的Marked渲染管线里面。虽然输出的结果看上去并无大碍,但因为这根管线会随着时间流逝变得越来越长长长长长,Markdown渲染的性能负荷与耗时也会随之持续上升。

照此类推,原本“失熵症”的情况其实也出现在了RSS Feed的渲染里面。但由于RSS Feed的渲染结果更多面向的是机器人,它们对响应时间相对不敏感,再加上RSS Feed缓存时间长,所以乍一看也没发现问题。

既然定位到了病根,一切就很好解决了,只需要将设置配合marked.js渲染器一同改成实例化引入就好。

官方文档所示:

import { Marked } from 'marked';
const marked = new Marked([options, extension, ...]);

一切……都结束了。

在改完代码之后,困扰了我整整两个月的bug也随之烟消云散。这头恶龙在它生命的最后,既没有挣扎,也没有无能狂怒,只是慢慢地在我面前,永远地闭上了双眼。它的沉默反而显得我之前的惊吓和迷茫是那么不真实。

在故事的最后,勇士带着恶龙的毒牙归来,他用毒牙碾成的齑粉为药,治愈了人造人少女的“失熵症”。他们又幸福快乐地生活在了一起,仿佛所有病痛都没有发生过。

 

曾经,我以为debug比生活简单得多,毕竟一个bug只会有一种解法,只要找到病根就能对症下药。而生活千变万化,没有人知道明天和意外哪个先来。

如今,我才发现debug和生活是一样的,他们都一样的扑朔迷离。你以为找到了应对的办法,他却总能在别的地方给你当头一棒。但当你已经不抱期望的时候,反而柳暗花明又一村,难题自己却莫名其妙地解决了。

All we need… is a little bit of luck.

看个bug还要听我的人生感悟,见笑了。

其他

除了修这个大bug之外,这两个月电波站还是有其他改进的。

  1. 把正文部分的字号从16px加大到了18px,而其他UI的字体也相应调整了一下。感觉大字号对于长文章来说更易读一点。(绝对不是因为我年纪大了。)
  2. 在服务端加入了Rate Limiter,限制机器人无意义的频繁请求。(因为限制上限设得还挺高的,所以正常访问不会受影响。)
  3. 改进了文章列表页的设计,主要是为了解决从首页点击“查看更多文章”按钮跳转过来的小伙伴找不到刚刚看到哪篇文章的问题。(用黑话说就是:因为之前的文章列表不显示封面图和标签,和首页排版不匹配,容易让用户跳转页面后认知负荷过高。)
  4. 一些bug的小修小补,包括但不限于:分页组件、目录侧边栏和Json-LD在特定情况下显示异常的问题。
  5. 努力修复了文章咕咕咕的问题。

噢,顺带一提,这周电波站友链的猫梨小伙伴带着他重启的新博客回来啦。(好耶!)


感谢你读到了这里。祝你生活愉快,身体健康,不受bug困扰。

我们下篇文章再见吧~

  • 文/Zeee

商业转载请联系站长获得授权;
非商业转载请注明文章来源及链接。