1月27号晚上,一个运行了超过18年的项目——在线代码编辑器JS Bin突然崩溃,维护者Remy Sharp收到了告警邮件。接下来的三天,他经历了一场与时间赛跑的服务器救援行动。用他自己的话总结问题根源:Fucking, everything.
背景:运行在“古董”服务器上的活化石
JS Bin诞生于2008年,是一个老牌的在线代码编辑与分享工具。多年来,它一直运行在维护模式下,只需偶尔处理一些小问题或恶意内容举报。
它的硬件配置堪称“古董”:一台AWS t2.micro实例,单核CPU,仅配备1GB内存。这个配置在今天看来简陋得不可思议,却默默支撑了这个服务近18年。环境就像一间布满灰尘的旧机房,机柜上贴着“2008”的标签,指示灯闪烁着微光,墙上挂着复古科技的海报,桌上堆满了文件和杂物,充满了怀旧与凌乱的氛围。
然而,这一次的故障与以往任何一次都不同。服务器重启后直接锁死,甚至连SSH都无法连接。
第一步:简单重启失效,尝试“物理冷却”
Remy最初尝试了多次重启,但机器每次启动后都会迅速卡死。他判断是有持续性的恶性流量在冲击服务器,简单的重启不足以让攻击源散去。
于是他采取了一个非常规但有效的策略:将服务器彻底关机一小时。目的是让那些疯狂发送请求的客户端(很可能是自动化脚本或爬虫)在长时间得不到响应后主动放弃。一小时后重新开机,SSH连接终于恢复。
定位问题:htop与日志揭示真相
登录服务器后,他首先使用 htop 命令查看系统状态,发现CPU占用率直接飙升至100%,内存使用量也在快速增长。紧接着,他查阅系统日志(syslog),发现了关键线索:Node.js进程因内存溢出(OOM)而崩溃的日志。
问题根源浮出水面:巨大的入站流量压垮了Node服务,导致内存耗尽,进而引发整个系统崩溃。查看CloudWatch监控图表印证了这一点:平时入站流量约为1MB,故障时峰值竟达到了100MB,暴增了100倍。
止血第一招:驯服OOM Killer,保住排查通道
在默认的Linux配置下,当系统内存耗尽时,OOM Killer会随机终止进程以释放内存。这可能导致一个尴尬的局面:SSH守护进程被误杀,使得运维人员无法登录服务器继续排查。
Remy立即修改了系统配置,让OOM Killer的行为变得可预测:
# /etc/sysctl.conf
vm.oom_kill_allocating_task=1
# 然后执行
sudo sysctl -p
这行配置的含义是:让OOM Killer优先终止那个正在申请大量内存的进程(也就是导致问题的Node进程),而不是随机滥杀。这样,即使应用崩溃,至少能保证SSH通道畅通,为后续的运维操作留出余地。
顺手升级:从Node 7跨越到Node 22
在排查过程中,Remy向ChatGPT寻求建议。AI助手建议他升级Node.js版本。这里发生了一个有趣的插曲:Remy并未告知ChatGPT他使用的Node版本,但AI却准确指出了版本过低的问题。原来,他使用的是ChatGPT桌面客户端,而他的终端窗口正好显示着 node -v 命令的输出(v7.0.0),AI通过屏幕“窥视”到了这个信息。
这给他敲响了警钟:在使用AI桌面端时,屏幕上显示的任何敏感信息(例如终端里 cat .env 显示的API密钥)都可能被其读取。一个卡通插画描绘了这一幕:左侧电脑终端显示着node版本和环境变量,右侧一个机器人拿着放大镜,标题写着“AI PEEK-A-BOO! (Oops, be careful!)”。
言归正传,他决定将Node.js从古老的v7.0.0直接升级到v22.x。令人意外的是,升级过程异常顺利,这得益于他在2024年已提前对代码进行了兼容性改造。然而,升级并未解决根本问题,洪水般的流量仍在持续涌入。
止血第二招:Nginx调优,但杯水车薪
面对有限的硬件资源,他尝试对Nginx进行调优,尽可能节省每一分资源:
worker_connections 1024;
worker_processes auto;
keepalive_timeout 10;
keepalive_requests 100;
他还关闭了HTTP/2支持以节省内存。然而,在每秒超过1000次请求的疯狂冲击下,这些优化措施收效甚微。
引入外援:终于启用CloudFlare
此前,Remy一直未使用CloudFlare,担心复杂的配置会引入新问题。但至此他已别无选择。接入CloudFlare的过程相对简单,主要是修改DNS解析记录。
到1月29日深夜,主域名 jsbin.com 终于能在浏览器中访问了。但新的问题接踵而至:其他用户反馈他们仍然无法访问,并且收到一个520错误。
破解CloudFlare 520:TLS版本不匹配的陷阱
520是一个比较特殊的错误码,它不同于常见的“503服务不可用”或“504网关超时”,其含义更接近“CloudFlare向源站发送了请求,但收到了无法理解的响应”。
Remy发现自己能访问,但使用VPN(模拟外部用户)就不行。经过一番波折,他发现问题出在TLS协议版本上。他的Nginx配置中只启用了TLS 1.0到1.2:
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
而CloudFlare的全球边缘网络默认会优先使用更安全、更高效的TLS 1.3与用户建立连接。当CloudFlare(使用TLS 1.3)试图与他的源站(只支持到TLS 1.2)进行握手时,版本不匹配导致握手失败,CloudFlare随即向用户返回520错误。一张示意图清晰地展示了这个过程:左侧是标有TLSv1.3的CloudFlare边缘节点,右侧是标有TLSv1.2的源站服务器,中间是断裂的链条和“520 ERROR”标识。
他尝试在Nginx中启用TLS 1.3,但由于服务器上的Nginx版本过旧,无法加载所需模块。最终的解决方案是:在CloudFlare控制面板中,找到“SSL/TLS” -> “Edge Certificates”设置,直接关闭TLS 1.3。此举虽略显倒退,但立竿见影地解决了主页访问问题。
不过,用于加载静态资源和运行代码的iframe子域名(null.jsbin.com)依然无法访问,这引出了下一个更隐蔽的坑。
自己挖的坑:逻辑冲突的IP白名单配置
在排障过程中,Remy过度依赖了LLM(大型语言模型)的建议,导致配置复杂化且自相矛盾。
他首先根据AI建议,添加了还原用户真实IP的配置:
set_real_ip_from 173.245.48.0/20;
# ... 其他 CloudFlare IP 段
real_ip_header CF-Connecting-IP;
这段配置的作用是:如果请求来自CloudFlare的IP段,则使用请求头 CF-Connecting-IP 中的值来覆盖Nginx的内置变量 $remote_addr,从而获取到终端用户的真实IP。
接着,为了阻挡直接攻击源站的恶意流量,他又添加了一段基于地理信息的访问控制:
geo $is_cloudflare {
default 0;
173.245.48.0/20 1;
# ...
}
if ($is_cloudflare = 0) { return 444; }
问题来了:geo 指令判断的依据是 $remote_addr 变量。然而,在前一段配置中,来自CloudFlare的请求其 $remote_addr 已经被改写成了用户的真实IP,而非CloudFlare的边缘IP。因此,这些合法请求在第二段判断中,$is_cloudflare 的值变成了0,从而被无情地拦截(返回444)。
这就像两个功能模块在“打架”:一个模块(set_real_ip_from)说“我要把来访者的地址改成用户的真实IP”;另一个模块(geo_is_cloudflare)说“我要阻挡所有不是来自CloudFlare IP的访问”。两者组合的结果就是误杀了一大批正常用户。一张卡通对比图形象地展示了这个冲突:左侧蓝色电脑说“我会把remote_addr改成用户真实IP”,右侧橙色怪物说“我会阻挡所有非CloudFlare IP的访问”,中间是爆炸的“VS”,下方是一个困惑的技术人员。
在凌晨三点高压环境下,这种由AI建议堆砌而成的配置错误极易发生。最终,Remy删除了这些复杂的Nginx逻辑,转而使用更底层的防火墙规则来过滤流量。他通过 ufw(Uncomplicated Firewall)和AWS安全组,直接在网络层放行CloudFlare的IP段,并拒绝所有其他对443端口的直接访问:
ufw allow from 173.245.48.0/20 to any port 443
# ... 其他 CloudFlare IP 段
ufw deny 443
至此,所有访问问题才被彻底解决。
复盘:攻击来源与经验教训
启用CloudFlare后,那台1GB内存的“老伙计”瞬间轻松下来。htop显示CPU使用率降至4.6%,内存占用维持在30%,甚至比遭受攻击前的正常时期还要空闲。
那么,这场风暴的源头究竟是什么?CloudFlare的统计数据显示,在24小时内,有超过1000万次请求来自中国香港地区。结合其他地区的流量,总请求量异常巨大。一张网络安全监控图描绘了这一场景:世界地图背景下,密集的请求线条从香港、美国、中国、日本等地涌向中央服务器,线条上标注着爬虫图标,图表底部警示“检测到高级别AI爬虫活动”。
Remy推测这极有可能是大规模的分布式AI爬虫在抓取数据,虽然无法获得确凿证据。
总结:几条用代价换来的经验
- 敬畏老系统,但须掌握应急预案:像Node 7这样稳定运行了十年的环境,无事时切忌乱动。但必须为“万一”做好准备,清楚崩溃后的核心排查路径(日志、监控、资源查看)。
- 防护前置,宜早不宜迟:CloudFlare这类CDN和云原生防护服务的免费层就能抵御大量垃圾流量和简单攻击。不要等到服务被冲垮时才想起部署。
- 善用LLM,但保持清醒:在疲惫和高压下,LLM提供的代码或配置建议可能缺乏整体性和一致性。务必进行交叉验证和逻辑梳理,避免陷入“复制-粘贴-叠加”的陷阱,最终创造出自己都无法理解的复杂系统。
- 理解特定错误码的含义:CloudFlare的520错误通常指向源站与边缘节点间的通信异常(如TLS/SSL握手失败、响应格式不符),而非源站服务完全宕机。精准理解错误码能大幅缩短排障时间。
这场持续三天的救援行动,不仅救活了一个充满历史感的产品,也为维护古老但仍在服役的系统提供了宝贵的实战案例。
参考资料
[1] 一个跑了18年的老项目炸了,花3天把它救回来, 微信公众号:mp.weixin.qq.com/s/uf9_194cYhhpFhKKoT_LrA
版权声明:本文由 云栈社区 整理发布,版权归原作者所有。