最近大四的课程基本结束了,除了跟进毕业设计,剩下的时间都在看实习机会。广州的初冬不太冷,晚上坐在宿舍电脑前,看着跑了很久的博客,觉得是时候给它做一次彻底的升级了。
这次升级从准备到最终全部跑通,花了我好几个晚上的时间。整个过程像是一场漫长的运维马拉松,虽然没有剧烈的体力消耗,但看着终端里跳动的字符,脑子里的弦一直绷得很紧。
博客之前的后端是 Mix Space v8,前端用的是开源的 Shiro。这次打算把后端直接拉到最新的 v10.0.3,前端也换成了付费版的 Shiroi。买主题的钱是从刚发下来的国家奖学金里出的。在广商读了四年软件工程,拿了两次国奖,绩点维持在 4.02,这些数字在简历上或许能证明我是一个合格的学生,但真正让我觉得踏实的,还是自己真金白银买下喜欢的代码,然后把它部署到服务器上的过程。
我的服务器资源不算宽裕。主力是一台香港的轻量云服务器,原生部署,不用备案,延迟也低;另外还有两台白嫖的 Oracle Cloud,用 Docker 跑着一些辅助服务。资源有限,意味着容错率低,这也为后面的踩坑埋下了伏笔。
动手升级前,我习惯性地连上数据库看一眼数据量,结果这第一眼就让我后背发凉。
图形化界面里,我原本的博客数据库不见了,取而代之的是一个名为 READ_ME_TO_RECOVER_YOUR_DATA 的库。点开一看,里面只有一条记录,大意是我的数据已经被他们打包带走,需要往某个地址打多少比特币才能赎回。
这是真实的勒索攻击,不是课本里的网络安全案例。
我看着屏幕,情绪倒是没有太大波动。大二的时候我写过一个简单的 Shell 脚本,利用 crontab 每天凌晨把 MongoDB 的数据 dump 出来,压缩后传到另一台服务器上。我连上备份服务器,解压了昨天的压缩包,数据都在。恢复数据只花了十分钟,但这十分钟里我想明白了一件事:学校里教数据库,为了方便演示,通常会让大家把 bindIp 改成 0.0.0.0,且不设密码。我当初部署博客时图省事,把这个坏习惯带到了公网上。
事后,我老老实实地修改了 mongod.conf,把监听地址改回了 127.0.0.1,并开启了 authorization: enabled,给所有的库加上了强密码验证。互联网是一片黑暗森林,公网上裸奔的 27017 端口,引来猎人只是时间问题。
解决了数据库的惊吓,我开始处理后端的环境。Mix Space v10 的底层更新了,强制要求 Node.js 的版本在 22 以上。我那台香港服务器装的是 CentOS 8,这是一个已经被官方放弃维护的系统。当我习惯性地敲下 yum install nodejs 时,源里提供的版本还停留在远古时期。
尝试编译安装太耗时间,用 nvm 切换版本在低配机器上又显得有些臃肿。最后我换用了 fnm(Fast Node Manager)。它是用 Rust 写的,没有那么多复杂的依赖,速度很快。配好环境变量后,敲下 fnm use 22,看着终端里返回的 v22.x.x,有种在破旧的老房子里装上了现代家电的错觉。
后端跑起来后,我开始部署前端。Shiroi 的前端打包后部署在 Vercel 上,这也是为了省下自己服务器的带宽。本以为前后端对接只是改个环境变量的事,结果打开域名,页面是一片刺眼的空白。
我按下 F12 打开开发者工具,Network 面板里一片红。CORS,跨域资源共享报错。
这是一个老生常谈的问题,但这次的报错有些奇怪。我明明在 Nginx 的反向代理配置里加上了常规的跨域头,普通的 GET 和 POST 请求按理说应该能过。我盯着控制台的请求详情看了很久,发现由于 Shiroi 引入了 next-intl 来做多语言国际化,前端在发起请求时,会在 Header 里带上一个自定义的 x-lang 字段。
Nginx 默认是非常严格的,它不认识这个自定义头,在处理浏览器发出的 OPTIONS 预检请求时,直接把这个请求拦截了,导致后面的真实请求根本发不出去。找到问题后,解决起来就只有一行代码。我打开 /etc/nginx/nginx.conf,在 Access-Control-Allow-Headers 那一行的末尾,加上了 x-lang。执行 nginx -s reload 后,刷新页面,博客的 UI 终于慢慢加载了出来。
但安稳的日子没过几分钟,SSH 连接突然断了。
我尝试重新连接,终端一直提示 timeout。登录云服务商的控制台一看,CPU 和内存的使用率已经拉成了一条直线。服务器死机了。
这台香港服务器只有 1.7GB 的可用内存。在部署 Mix Space 后端时,我习惯性地使用了 PM2 的 cluster 模式,想着能充分利用多核性能,于是开了 2 个实例。Node.js 本身就是个吃内存的大户,两个实例加上底层的 MongoDB、Nginx,瞬间把这 1.7GB 内存吃干抹净,直接触发了系统的 OOM(Out of Memory)机制,把重要进程杀了个干净,最后连 SSH 守护进程都挂了。
我只能在网页端强制重启服务器。重启后,我第一时间停掉了 PM2,把 ecosystem.config.js 里的 instances 改成了 1,并且把执行模式从 cluster 改回了 fork。单实例运行后,用 htop 观察了半个小时,内存占用稳定在 60% 左右,再也没有出现过崩溃。
很多时候,我们在技术博客和开源文档里看到的“最佳实践”,都是建立在充足的计算资源之上的。对于我们这种手里只有入门级服务器的学生来说,懂得在性能和资源之间做妥协,也是一种必修课。
天快亮的时候,所有的配置都固定了下来。现在的架构非常清晰:用户访问前端,请求先到达 Vercel 的边缘节点;动态数据的请求会被转发到我香港服务器的 Nginx 上;Nginx 处理完跨域和 SSL 卸载后,把流量交给 PM2 守护的单实例 Node.js 后端;后端再与本地经过认证的 MongoDB 进行数据交互。旁边的两台 Oracle 服务器则安静地跑着定时备份和一些监控脚本。
看着跑通的博客,我没有太多激动,只是觉得很平静。
在民办三本读软件工程,学校能给的资源和视野是有上限的。课本上的软件工程讲述的是完美的瀑布模型和敏捷开发,但在实际的个人项目中,你要面对的是勒索病毒、系统过载、环境依赖冲突以及数不清的网络协议细节。我的绩点和国奖,证明了我能把书本上的知识学好,但今晚这一连串的排错过程,才是我四年大学生活里最真实的写照。
运维能力从来不是在课堂上听出来的,也不是看几篇教程就能学会的。它是在服务器宕机、数据丢失、服务起不来的一个个深夜里,被逼着去看日志、查文档、改配置,一行行试错试出来的。好在,现在的这套系统很稳定,它应该能陪我安稳地度过接下来的毕业季。