第 15 章:刺瞎天眼的基数爆炸 (The Cardinality Blindness)
2013年,春天。雷德蒙德,创世软件 113 号楼,那个见证了无数次系统崩溃的战情室。
随着业务的全球化狂飙,Hello World 已经被拆分成了三百多个微服务。成千上万台服务器在北美和欧洲的跨洋网络中疯狂交互。在这个完全没有中央节点的复杂有向无环图(DAG)里,唯一能让李思和工程师们看清系统全貌的,是一套他们花费巨资自研的“神盾(Aegis)”分布式时序监控系统。
“神盾”就像是一只拥有亿万只眼睛的天眼,死死地盯着所有的服务器。
“看,李。”运维总监戴夫得意地指着一块占据了整面墙壁的巨型四维数据大屏。
屏幕上,无数条曲线实时勾勒着数千个微服务的健康状况。 “神盾每 10 秒钟就会拉取一次全网的监控指标(Metrics)。我们可以随时通过多维度标签(Tags)来切片数据!这比老式的日志分析强太多了。”
如果李思想要知道“过去一小时,部署在纽约机房、版本号为 V14.2 且调用结算接口的 HTTP 500 错误数”,他只需要在控制台组装这几个标签(status=500, region=nyc, version=14.2, api=/checkout),底层的时序数据库(TSDB)就能在一秒钟内拉出一条完美的曲线。
这套维度无穷的打标机制,让排查微服务故障变成了极其优雅的降维打击。
李思喝了一口黑咖啡,满意地点了点头:“有了这种切片能力,我们就不必再像瞎子一样去海量的日志里 grep 关键字了。系统彻底透明了。”
但在这个被数据指标充满的狂欢中,一个比曾经的“数据库锁死”更隐蔽、更致命的系统级“癌变”,正在那套引以为傲的监控天眼底层悄然发生。
灾难的导火索,是一次再普通不过的增长黑客(Growth Hacker)需求。
为了追踪用户的个性化转化率,市场部要求在前端的重定向网关里,加上一个新的极其微小的监控探针。
“市场部想知道,到底是哪个用户分享的链接带来了最多的注册。”负责大数据的工程师在会议上提议,“很简单,我们在网关向‘神盾’发送请求吞吐量(Request TPS)时,多加一个标签:user_id。”
“这样我们在大屏上就能看到:metric="http_request", status=200, user_id="10001"的流量曲线了!完美切片!”
在当时的知识盲区里,没有人觉得多加一个 Tag 是什么大不了的事情。 毕竟,时序数据库(TSDB)以其极高的写入吞吐量闻名,而且只存数字,它应该能处理一切。
当这行带有 user_id 标签的监控代码被推向生产环境时,李思并没有关注。他正在处理底层的另一次数据库重构。
代码上线后的第十五分钟。深夜 11:45。
“嗡——嗡——嗡——!”
战情室爆发出前所未有的凄厉防空警报声。不是某个应用服务宕机,而是那面占据了整面墙壁的巨型监控大屏,突然开始剧烈地闪烁、花屏!
“李!天眼瞎了!”戴夫惊恐的尖叫声划破了夜空。
李思冲进战情室,被眼前的景象震惊了。 那个原本能够在一秒内绘制出全网实时情况的大屏,所有的曲线都卡死在了 15 分钟前。所有的仪表盘全部变成了灰色的 N/A(数据不可用)。
“神盾系统挂了?!它不是有几百台集群做支撑吗?”李思飞速敲击键盘,“我们现在彻底失去了对那三百个微服务的监视!如果是被黑客攻击了,我们连看都看不见!”
“没有被攻击!业务服务活得好好的,CPU 都很空闲!”戴夫满头大汗地调出神盾底层的 TSDB 服务器状态,“但……但是神盾的数据库,内存被彻底撑爆了!疯狂在 OOM(Out of Memory)!”
李思的心脏猛地一沉。通感(Synesthesia)视界瞬间开启。
在李思的脑海中,展现出了一幅令人头皮发麻、头晕目眩的高维灾难图景。
这并不是流量洪峰。神盾底层接收的数据量(单纯的数值写入)甚至没有平时大。 但在时序数据库的引擎核心里,一场比宇宙大爆炸还要恐怖的多维空间膨胀正在发生。
在时序数据库(如后来的 Prometheus)的设计中,一条唯一的时间线(Time Series)是由指标名(Metric Name)和其附带的所有标签键值对(Key-Value Tags)共同组成的倒排索引(Inverted Index)。
在灾难发生前。 如果只有 region=nyc 和 region=la,加上 status=200 和 status=500。 组合起来的可能情况(基数,Cardinality)只有极其可控的 2 x 2 = 4 条时间线。 TSDB 只需要在内存里维护 4 个极为轻量的数据结构的指针,轻松无比。
但是十五分钟前,那个为了市场需求而被塞进去的标签,是一个极度狂野的变量——user_id。
在创世软件,活跃的 user_id 有多大? 三亿!
当这三亿个独一无二的用户 ID 作为标签组合进去时。 在李思的通感世界里,那原本只有 4 条清晰时间线的高速公路,在一瞬间,“轰”地一声巨响,像细胞癌裂般,极其暴力地膨胀分裂成了三亿条平行的高速公路!
每一秒钟,当不同用户发来请求时,数据库必须在内存的倒排索引树里,为这个用户新建一个全新、独绝的内存数据结构(Chunk/Series Object)。
这叫基数爆炸 (High Cardinality Explosion)!
“疯了!全疯了!谁他妈的允许业务开发在这个 HTTP 请求监控里加上 user_id 的监控标签的?!”李思盯着屏幕上直接原地起飞突破单机 128GB 内存上限的占用率曲线,怒吼道。
“是市场部的需求!”戴夫在嘈杂的报警声中扯着嗓子大喊,“只是多传了一个字符串而已,为什么会把几百台内存打挂?!”
“因为这不是一个普通的字符串!这是三亿种组合的乘数!”
李思的眼中闪烁着对滥用架构特性的极度愤怒。 “时序数据库是用来统计宏观维度的(比如机房、错误码),它是聚合(Aggregation)的利器!你们竟然妄图把微观到每一个用户的独立行级数据(Row-level details)作为维度塞进它的索引里!”
“你们这群蠢货硬生生在内存里制造了一个拥有十亿量级基数的高维超立方体!”
这就是监控系统最恐怖的死法。 在三亿条时间线的重压下,神盾底层的几十台 TSDB 服务器,它们为了给每一个可能的用户维持那个微小的写入槽位,耗尽了索引机制。在进行周期性的由于标签变动引发的磁盘压缩(Compaction / GC)时,导致 CPU 和内存全部枯竭宕机。
监控系统倒下了,成为了盲飞的帝国。而帝国真正处理业务的服务器还在正常运转。
如果此刻有物理机房断电,正在盲飞的李思将一无所知。
“切断它!立刻把所有流向神盾系统的数据上报(Scrape)路由在网关处全部掐断!”李思做出了果断而无奈的决定。
他必须先斩断毒源,再去清洗那被污染的内存池。
“那我们就什么也看不见了!”西拉斯紧张地咽了一口唾沫。
“看不见,总比看着报警器一直响,然后在极度恐慌中做出错误的操作要把大!”李思毫不犹豫地按下了阻断按钮。
全场陷入了那种令人绝望的“盲盒时刻”。
在接下来的三个小时里,整个创世软件的 SRE 团队如同在雷区中闭着眼睛行走。他们紧急重启了神盾的时序数据库集群,并极其暴力地在接收端写下了一个正则丢弃规则(Drop Rule),将所有胆敢带有 user_id 或者 session_id 等高散列度标签的数据包,直接在内存里全部抛弃!
当盲飞结束。 那个占据了整面墙壁的巨型监控大屏再次亮起时,时间已经来到了凌晨 3 点。
平滑的、聚合过的宏观曲线再次出现。
“活过来了……”戴夫虚脱地趴在桌子上,“但是,市场部想要的那个‘追踪具体每个用户点击率’的需求,彻底没戏了。”
“去告诉市场部,想要统计微观的用户行为特征,给我去用大数据的批处理系统(Hadoop/Spark),或者存进可以进行海量全表扫描的列式数据仓库(OLAP/ClickHouse)里,明天早上再去看报表!”
李思转过身,在白板上用极粗的红笔写下了四个带有血泪教训的字: 控制基数 (Control Cardinality)。
他看着那些刚刚经历了一场数字癌变的微服务群。
从这一刻起,“监控系统的基数限制”成为了整个分布式云原生架构中的最高禁忌之一。任何胆敢在 Prometheus 等暴露不可控变量(如 URL 全路径、UUID)的开发人员,都会在代码被合并前,遭到架构师最严厉的绞杀。
因为李思比谁都清楚。 微服务的海洋已经深不可测。在这个连天眼都随时可能被刺瞎的沼泽里,人类工程师想要依靠有限的大脑去控制庞大的拓扑,已经处于全面溃败的边缘。
下一次的灾难。 不会是硬件。不会是重试。也不会是监控。 而将是组织(Organization)本身的扭曲,所折射在架构上的恐怖投影——
那是一个庞大、臃肿、集合了所有人贪婪与惰性的“怪物中台 (The Monster Middle-Platform)”。 系统中最肮脏的毛线球(Big Ball of Mud),即将在第 16 章彻底引爆。
架构决策记录 (ADR) & 事故复盘 (Post-Mortem)
文档编号:PM-2013-04-12 事故等级:SEV-2 (全网监控系统宕机,SRE陷入长达 3 小时的盲飞,但核心业务未断) 主导人:李思 (Principal Engineer)
1. 事故现象 (What happened?) 为满足市场对微观数据的实时观测需求,业务团队在全网基础请求监控上报(Metrics Scrape)的标签里。新增了一个名为 user_id 的 Tag。 该发布操作虽然没有直接影响业务微服务,但在 15 分钟内导致底层的“神盾”时序数据库(TSDB)因内存溢出(OOM)群发宕机。导致整个公司的全局可视化监控系统彻底陷入黑暗。
2. 5 Whys 根本原因分析 (Root Cause)
- Why 1:为什么监控系统会内存溢出死亡? 因为底层 TSDB 在维护索引时消耗了极为恐怖的内存资源。
- Why 2:为什么平时不溢出,加上新代码就溢出? 因为
user_id是一个拥有三亿个散列值(活跃用户数)的无边界变量。 - Why 3:为什么无边界变量如此致命? 由于时序数据库依靠时间线(Time Series)进行数据存储,每一条独特的时间线由(指标名 + 所有的变体标签组合)共同构成。强行植入三亿级别的散列键,立刻触发了高基数维度爆炸 (High Cardinality Explosion)。
- Why 4:基数爆炸引发了什么物理动作? 在百万乃至千万条独一无二的时间线上,TSDB 被迫在内存中维护海量的倒排索引节点。在接收写入以及垃圾回收刷盘(Compaction)时,超高散列度的元数据操作直接导致了 CPU 和内存的物理雪崩。
- Why 5:为什么这种需求不合理? 在架构选型上发生了极其严重的错位:错误地将专门用于宏观趋势聚合观测(Observability/Metrics)系统,当成了用于追踪单点明细的在线分析结构(Logs/Events/OLAP)系统来使用。
3. 解决方案与架构决策 (Action Items & ADR)
- 临时止血 (Workaround):从网关层面主动熔断上报网络。清理宕机遇难的 TSDB 内存并重启集群;紧急上架 Relabel(标签重写)规则正则强行丢弃一切包含
user_id的上传负载。 - 架构重构 (Long-term Fix):
- ADR-015:颁布全公司级的《分布式可观测性基数铁律》
- 绝对禁止 (Hard Ban):在所有的 Metrics 类监控探针(如日后的 Prometheus
Counter/Gauge)中,严禁下发任何无边界的离散型动态变量作为 Label(如:user_id,email,session_uuid,full_raw_url)。 - 边界收敛:标签的值必须是极其有限且可枚举的(例如:HTTP方法仅有
GET/POST,状态码约十几种如200/500/404)。 - 异构观测系统分层:如果确实需要查询细粒度的追踪日志(Tracing)以定位某个人,必须采用且只能向独立的、能抗粗大明细数据的分布式日志系统 (ELK Stack) 或 链路追踪系统 (Jaeger/Zipkin) 投放事件(Events),绝不污染用于告警和救命的 Metrics 核心时序大盘。
4. 爆炸半径与代价反思 (Blast Radius & Trade-offs) 虽然本次盲飞只是干死了监控侧而未波及支付主引擎,但在缺乏可见性的三个小时内,系统的脆弱度达到了“上帝掷骰子”的境地。这次事故极大地警醒了后续的云原生工程师:在高度解耦的系统中,监控系统并非拥有无限吸收各种信息强度的吞噬者。必须通过“有限基数”的架构红线自我阉割部分细颗粒数据,以此来保全监控大盘在面对极端大流量攻击时始终屹立不倒。
架构师科普:连接过去与现在的系统设计 (Architect's Note)
1. 踩穿时序数据库大动脉:高基数问题 (High Cardinality) 在没有实操过超大规模监控系统的初级工程师眼里,“只要多存一个字段”似乎不是啥事。但如果这发生在时序数据库(Time-Series Database, 例如现代统治大厂的 Prometheus, 或 InfluxDB)上,就是不折不扣的大灾难。 指标(Metrics)是“把海量请求算成聚合统计图表”的引擎,它的价值是“1秒内告诉整个国家双十一的 QPS 大盘是多少”。所以底层的倒排字典是基于组合建立的。 如果一个小白开发在统计 http_requests_total 时,加了一个 Label 叫作 path,而如果这个 path 包含了用户的独有查询参数(比如 api/v1/user/1024/info),这就硬生生为全网上亿个请求在内存中创造了上亿条独有的 Time Series (极高基数)。而原本健康的可能只是像 api/v1/user/{id}/info 只有一条。 这就是为什么当今任何高阶 SRE 面试中,绝对必考“如何防止和排查 Prometheus 内存暴雪”的原因——万恶之源往往只是开发同事悄悄加了一个带有 UUID 的该死 Tag。
2. 现代云原生的三大支柱与分工 (Three Pillars of Observability) 李思在这场盲飞后,直接将系统的监控诉求强行划出不可逾越的护城河,这也就是今时今日支撑整个 K8s 及现代系统观测理论的核心——可观测性三大支柱:
- Metrics(指标):高频、聚合类宏观数据,标签数必须极短。用来在出事时第一时间“报警并展示粗狂曲线”。不可存详细微观个体(Prometheus的使命)。
- Logs(日志):包含了极度详尽的单个用户或者某次错误打印的内容堆栈。数量庞大,搜索极慢,但在发现报警之后“提供肉眼寻找蛛丝马迹的凶器”(ELK/Loki的使命)。
- Tracing(链路追踪):将拆分后如同无头苍蝇般的五十个微服务的完整一串请求打包记录,绘制出甘特图瀑布。用来“查到底死在了哪条网络线上”(OpenTelemetry/SkyWalking的使命)。 任何妄想用其中一张王牌去篡改另一种结构存储数据的架构,都将被分布式巨大的物理基数反手吞没。