Skip to content

第 2 章:失忆的巨兽 (The Amnesic Behemoth)

2000年4月,纳斯达克泡沫的巅峰。西雅图的空气里都弥漫着烧钱发狂的味道。

雷德蒙德,创世软件 113 号楼。

凭借去年超级碗那一通“向魔鬼献祭一致性”的野蛮操作,西拉斯·霍恩如愿以偿地扫清了政敌,晋升为 Web 事业部的高级总监 (Senior Director)。而李思,也借此稳固了高级软件工程师 (L5) 的位子。但在架构委员会那些穿着定制西装的老派工程师眼里,李思的代码缺乏“企业级的优雅”。

“见鬼的优雅。”

此时的李思正瘫坐在战情室(War Room)的角落里,浑身冷汗。他的太阳穴仿佛被一根生锈的钢钉狠狠凿击,伴随着剧烈的偏头痛,他引以为傲的“代码通感”正向他疯狂报警。

半个月前,为了在千禧年的互联网狂欢中向华尔街讲一个“个性化门户”的新故事,西拉斯强行立项并上线了 “Hello World V2.0”

“仅仅 11 个字符太冷冰冰了!李,我们需要温度!”西拉斯曾在会议室里挥舞着拳头咆哮,“用户需要被记住!我要让这 11 个字符加上用户的真实名字!当他们登录后,页面绝不该只是一句干瘪的词,它必须是对着灵魂的呼唤:Hello World, Simon! 或者 Hello World, Silas! 明白吗?让系统认识他们!”

为了这几字节的“温度”,开发团队选用了当时最“优雅”的官方标准方案:开启了 Web 服务器 (IIS/ASP) 默认的 In-Process Session(进程内会话状态) 功能。

当用户登录后,他们那个只包含几个字母的昵称字符串,直接作为对象(Object),被塞进了 Web 服务器的进程内存里。

在单机测试时,它完美无瑕。但在拥有十台高性能康柏服务器、前端扛着每天数千万次并发的创世软件机房里,这就是在召唤一头失控的怪兽。

“警报!5 号服务器的 inetinfo.exe 进程崩溃了!”运维组长戴夫满头大汗地拍打着控制台,“该死,2 号也挂了!刚刚登入的八万个用户瞬间被强制踢下线!”

“我们的客服热线已经被骂瘫痪了!”西拉斯冲进机房,愤怒地扯开领带,额头青筋暴起,“用户刚打完他们的名字,屏幕一闪又退回了登录页!系统是得了阿尔茨海默症吗?!它为什么连个名字都记不住!”

“不,它没有失忆。”李思痛苦地闭上眼睛,双手指尖死死抵住额头,“它是脑充血快被撑死了。”

在李思的高维通感视界中,那十台昂贵的 Web 服务器根本不是冰冷的机器,而是十头趴在数据流河床上的畸形裸兽。

每一头巨兽的脑袋(进程内存),都在以肉眼可见的速度发生着极其惊悚的肿胀。每当一个新用户访问,巨兽的大脑里就会被强行塞进一块名为 Session 的脂肪。百万个用户的昵称,就是百万块沉重的碎瘤。

最致命的是,底层代码存在极其隐微的内存泄漏 (Memory Leak)。那些早就关掉网页去喝咖啡的用户,他们的名字依然残留在巨兽的大脑里,没有任何垃圾回收机制去销毁它们。

可用内存从 2GB 狂跌到不足 10MB。巨兽的颅骨被撑到了物理极限,发出令人牙酸的开裂声。

就在这时,操作系统(Windows NT)底层最冷酷的防御机制被唤醒了——为了防止整台物理机因耗尽内存而陷入彻底的蓝屏死亡 (BSOD)。

在李思的视界中,一个没有头颅、浑身散发着死气、手持沾血巨斧的刽子手——OOM Killer (Out of Memory 杀手),突然在那头脑部最肿胀的巨兽身旁浮现。

没有警告判断,没有优雅退让。 手起,斧落。

咔嚓!

巨兽那颗装满了十万个用户名字的庞大头颅,被瞬间砍飞! inetinfo.exe 进程被底层系统一斧子劈碎,内存被粗暴、血腥地强行夺回。整台服务器幸存了,但附着在它身上的那十万名在线用户,瞬间失去了所有的状态,如同跌落悬崖般被狠狠踢回了登录界面。

仅仅两秒钟后,看门狗进程 (Watchdog) 盲目地重新拉起服务。巨兽长出了一颗崭新、空荡荡的头颅,然后再次张开大口,绝望地吞噬随之涌来的海量用户名字,等待着下一次被处决。

“加内存!立刻给我加最高规格的内存条!”戴夫对着对讲机咆哮,几名硬件工程师推着装满 4GB 内存的推车准备进行热插拔。

“停下!都把手离开机柜!”李思猛地睁开眼睛,瞳孔中布满血丝,厉声喝止。

“为什么?!内存不够就加,机器就不会死!”戴夫不解地大喊。

“你那是让它死得更惨!”李思一把推开戴夫,抢过键盘,“更大的内存只意味着巨兽能把头撑得更巨大!在 NT 内核里,一旦持有几个 G 碎片的庞大进程被强制砍头,操作系统回收那海量内存页表 (Page Table Reclaim) 的瞬间,会让整台机器陷入彻底的僵死 (Freeze)!时间一长,连心跳探针都会超时,它会把前后整个网络拖进深渊的!”

“如果是内存泄漏,为什么 3 号服务器死得比别人快整整五倍?!”西拉斯急得像热锅上的蚂蚁,“我们的流量明明是均匀分配的!”

李思的双手在键盘上拉出残影,他调出了最顶层那台昂贵的 F5 硬件负载均衡器 (Hardware Load Balancer) 的网络探针数据。一行行 IP 分布在他眼中闪烁。

荒诞的真相浮出水面。

“因为‘黏性会话 (Sticky Session)’彻底失效了。”李思盯着那密密麻麻的同源 IP,声音如同冰窖里的寒风。

在分布式环境下,为了让某台服务器“记住”某个用户,负载均衡器会使用 IP-Hash 策略:同一个 IP 来的请求,死死绑定在同一台后端的机器上。

“西拉斯,”李思转过身,死死盯着这位商业奇才,“你太着迷于纳斯达克的数字,却忘了现在的世界是什么样。全美有将近三千万网民,正在用 AOL (美国在线) 的拨号网络冲浪!”

西拉斯一愣:“那又怎样?”

“AOL 为了节省公网 IPv4 地址,让几百万乃至上千万的用户,在底层共享了区区几十个超级代理出口!在我们的 F5 看来,这根本不是几百万个人——”

李思在通感中指向 3 号服务器的上方,那里已经化作了一个质量无限大的漏斗:“——而是一个拥有几百万次并发敲击的‘黑洞级超级 IP’!”

状态黑洞 (State Black Hole) 带来了极度的流量倾斜。百万个名字被 F5 教条地全部砸向了同一台服务器。3 号服务器瞬间被撑爆,OOM 刽子手砍下它的头。3 号一死,F5 立即将这个黑洞砸向 5 号,5 号紧跟着脑碎死亡。

这就是一场沿着机房排列顺序,疯狂蔓延的连环斩首行动!

“取消 IP 绑定!立刻切换!”西拉斯脸色惨白,惊恐地喊道。

“不能切!如果改成轮询 (Round-Robin),用户的下一次点击会随机飘到另一台空机器上,他们依然会因为找不到名字被当场踢下线!”戴夫抱着头近乎绝望,“只要名字还存在内存里,怎么死都是一盘死局!”

李思没有说话,但他已经推开了战情室通往机房的玻璃门。机房冰冷的空调风吹在他的脸上。

他走向控制台,打开了 IIS 的全局管理器。 他的鼠标悬停在那个名为 Enable Session State(启用会话状态)的优美复选框上。

“西拉斯,”李思的手指扣在鼠标左键上,眼神中透着一种近乎暴戾的冷酷理性,“你现在只有两个选择。要么,让这群系统每隔三分钟被砍头一次,所有网民陪你们一起疯;要么,接受一个哪怕只是单调显示 Hello World,但坚如磐石、永不宕机的系统。”

“你……你想干什么?”西拉斯眼角疯狂抽动。

“我要对你的这些机器,执行脑白质切除手术。”

咔哒。

李思毫不犹豫地按下了左键,去掉了那个勾选。在不到十秒的时间里,他用极其暴力的脚本下发全局覆盖,彻底封杀了服务器的内存状态写入。

在通感的剧烈收缩中,李思仿佛亲手挖出了那十头巨兽滴血的大脑。 那些原本堆积如山的几百万个昵称、状态、寄托着华尔街虚荣的“个性化温度”,在一瞬间被全部蒸发、抛弃。这群庞大的服务器变成了没有记忆、也没有任何羁绊的空壳。

李思反手切进 F5 控制台,将那导致死亡倾斜的“Sticky Session”彻底粉碎,改为了最粗暴流畅的轮询 (Round-Robin)。此时此刻,无论你是来自德州乡下的独立拨号,还是裹挟着百万用户的 AOL 黑洞,请求都像扑克牌般被毫无滞涩地均匀洗进了十台机器里。

奇迹发生了。

失去记忆的巨兽,变得前所未有的轻盈。 OOM 刽子手失去了锁定目标,在空气中逐渐消散。服务器内存占用率笔直地跌落并死死锁在 15%,宛如一条心电图上的平稳直线,再也没有一丝一毫的泄漏波动。

那句极其昂贵却荒诞的 Hello World, [Name],再次变回了原始的、冷冰冰的 11 个字符。

“虽然不再是个性化主页了……但是,雪崩停了。”戴夫不可思议地看着大屏上全部变绿的集群指示灯,喃喃自语。

李思抽出一张纸巾,擦去额角的冷汗,转过身直视西拉斯的眼睛。在那一刻,他说出了那条在未来二十年里,犹如《摩西十诫》般统治所有大厂分布式架构的第一铁律:

“应用服务器 (App Server) 必须是绝对无状态的 (Stateless)。”

李思的目光扫过那十台指示灯平稳闪烁的机柜设备,语气冰冷:“西拉斯,这些机器只是牲口 (Cattle),不是供你寄托情感的宠物 (Pets)。如果你不能在任意一秒钟,毫无顾忌地拔瞎其中一台服务器的电源而不影响任何用户,那么这套架构,从娘胎里就是注定要毁灭的。”

西拉斯掏出手帕疯狂擦汗,哪怕心头在滴血,但他不得不承认,李思再一次用冷酷的物理规律,把创世软件从悬崖边硬生生拉了回来。

“好吧……我妥协。业务逻辑层的内存里不存任何用户状态。”西拉斯咬着牙关,却抛出了一个极其现实的商业质问,“李,但华尔街不会妥协!我们迟早还是得把用户的名字、购物车、他们的登录信息写进这个该死的网页里!如果我们不能把‘状态’存在应用服务器里,那你说,我们该把它们塞到哪里去?!”

李思没有立刻回答。 他的目光越过那些轻盈的 Web 空壳群,投向了机房的最深处。在那里,闪烁着幽暗红光的集中式高端 SAN 阵列与庞大昂贵的关系型数据库主库,正静静潜伏在黑暗的阴影中。

“放到……该放的底座里去。”李思喃喃自语。

但他心里却升起了一股不可名状的寒意。把状态从应用层强行剥离下放,不过是将洪水从这道堤坝,赶向了系统更深、更脆弱也更致命的核心枢纽罢了。

一张名为“TempDB 绞首索”的绞肉机,正在不远处的黑暗中悄然张开獠牙。


章末文档:创世软件架构决策记录 (ADR) & 事故复盘

事故编号: INC-2000-0415 (巨兽失忆/OOM级联雪崩) 决策编号: ADR-002: 应用层状态剥离与无状态化原则 决策者: Simon Li (Senior SDE)

背景 (Context): Hello World V2.0 引入了个性化主页,开发团队启用了 IIS 默认的 In-Process Session State,将用户昵称状态维护在内存中。由于大量 AOL 拨号用户在底层共享少数代理出口,展现为单一超大 IP 来源。F5 负载均衡器的 IP-Hash(黏性会话)策略因此失效,将海量 Session 路由至单台服务器,导致堆内存存在倾斜并发生严重溢出。Windows NT 底层的 OOM Killer 频繁杀死进程执行裁决,引发了集群范围内的业务连环雪崩。

决策 (Decision):

  1. 全局极其严格地禁用 In-Process Session/State,并将应用服务器定义为纯粹的计算透传层。
  2. 彻底废弃 F5 的 Sticky Session (IP-Hash) 路由规则,强制更替为底层负载彻底打散的 Round-Robin (轮询)
  3. 身份验证凭证降级为客户端加密 Cookie,确立前端集群无状态 (Stateless) 第一原则。

后果 (Trade-offs):

  • 优势: 应用服务器被彻底解耦,成为可随时横向扩展 (Scale-out)、可任意宕机重建而不伤及用户会话的“牲口 (Cattle)”。彻底消灭了应用侧由于会话累积导致的 OOM 级联崩溃风险,极大提升可用上限。
  • 劣势: 极其沉重的商业妥协体验。富状态业务发展被迫受阻。若在未来试图恢复并跨节点共享状态(如恢复昵称与偏好显示),系统必须引入集中式的外层并发状态存储(DB/Cache),这将对后端核心层产生极其可怕的热点冲击。

架构师科普:连接过去与现代的系统设计 (Architect's Note)

在本章的千禧年技术背景下,应用状态撑爆内存主要引发的是操作系统(OS)层面的 OOM 猎杀与页表瘫痪。但为了让大家掌握现代大厂在“架构无状态化”上的深层绝望,我们必须引出现代语言微服务(如 Java 的 JVM 或 Go)中应对庞大内存对象时的终极死神——垃圾回收机制 (Garbage Collection, GC)STW (Stop-The-World 全局停顿)

  • 现代微服务集群中的 GC与 STW 噩梦: 如果本章的系统平移到现代微服务架构中,单纯地去为挂掉的容器“加配置/加内存”同样是饮鸩止渴。当你的 JVM 堆内存(Heap)被设置得哪怕高达 64GB,一旦里面塞满了上千万个被遗忘的 Session 对象,当触发 Full GC(全量垃圾回收) 时,JVM 引擎会像执行军法一样强制挂起你所有的网络工作线程,去极其缓慢地扫描那些错综复杂的内存树。这个挂起僵死的过程,就是大厂谈之色变的 Stop-The-World (STW)
  • 为什么盲目加内存死得更惨? 堆内存越大犹如需要打扫的垃圾场越辽阔,彻底扫描的耗时成倍增加。一次针对几十 GB 的大内存 Full GC,可能会让整个提供核心业务的容器面临长达数十秒的彻底假死——不崩溃,但就是不回包。在这数十秒内,前端网关会因为超时而绝望地疯狂发起重试,最终演化成指数级放大的重试风暴 (Retry Storm),用流量把整个活着的集群轰成齑粉。
  • 系统设计的铁律基石: 这也是为什么哪怕到了拥有顶级垃圾回收器(ZGC, C4)的今天,高级架构师们依然如同狂徒般死守“应用层绝对无状态化 (Stateless)”。他们通过将可变状态外置(Externalized State,例如存入极其专业且隔离的 Redis 或 Memcached 矩阵),不仅从根本上抹除了单点流量倾斜引发的 OOM,更让业务层彻底摆脱了应用内庞大对象造成的停顿噩梦。这是将后端集群视为“可随意替换的牲口 (Cattle)” 的物理底座。

只有抛弃了牵挂,架构才能获得横向扩展的终极自由。