写点什么

运维大规模反向代理的教训

作者:Mitendra Mahto

  • 2025-12-07
    北京
  • 本文字数:5653 字

    阅读完需:约 19 分钟

大小:2.76M时长:16:04
运维大规模反向代理的教训

代理层的关键脆弱性

反向代理是互联网大规模基础设施中默默无闻的核心组件。它们终止传输层安全(Transport Layer Security,TLS)、防御拒绝服务(DoS)攻击、平衡负载、响应缓存,并连接快速演变的服务。无论你称它为负载均衡器、边缘代理、API 网关还是 Kubernetes Ingress 控制器,这一层都是所有流量汇聚的地方,而且我们也得承认,它也是很容易出问题的地方。

 

它的麻烦在于代理很少以整洁、教科书式的方式失败。相反,有时候它们会在优化基准测试中表现出色,但是在真实工作负载下却会崩溃;因为元数据中缺失了一个逗号,导致实时流量悄无声息地中断,它们也会因此而失败。它们还可能因为旨在简化技术栈的抽象变成了隐藏的脆弱点而失败。

 

本文是运行大规模反向代理舰队(fleet)的一系列战争故事的集合。它探讨了适得其反的优化,触发故障的常规变更,以及塑造我们今天设计和运行代理方式的运维教训。

优化陷阱:当优化变得有毒

优化是很诱人的。它们承诺了免费的性能提升,这在基准测试中看起来很棒,通常在小环境中能够很完美地工作。

 

但一旦主机扩展至超过五十个核心,舰队在数百个节点上服务数百万 QPS 时,规则就会发生巨大的变化,一个地方的性能提升可以迅速成为大规模的负担。

Freelist 争用灾难

我们扩展了 Apache Traffic Server(ATS),从一组较小核心的机器转移到现代化的多核心主机。假设很简单:更多的核心应该意味着成比例的吞吐量增加。在传统硬件上,ATS 的 freelist 优化能够按预期工作,减少了堆争用并提高了分配速度。

 

但是,在 64 核主机上,同样的 freelist 设计会适得其反。ATS 依赖于 freelist 访问的单个全局锁。数十个核心同时访问它的话,会导致锁成为热点,造成系统抖动和浪费 CPU 周期。吞吐量没有翻倍,相反尾部延迟增加,总体吞吐量下降。代理花更多时间争夺 freelist 的使用权,而不是服务于流量。

 

起初我们对自己的分析持怀疑态度,我们原本以为 freelist 应该会从中受益。但一旦我们禁用它,吞吐量从大约 2k 请求每秒跳升到大约 6k 请求每秒,提高了 3 倍。

无锁设计的隐藏成本

我们与Read-Copy-Update(RCU)模式进行了斗争,这是一种在内核和高性能用户空间中流行的模式,用于实现快速的无锁读取。它的权衡在于每次写入都需要复制结构,并且只有在所有活动读取器完成后,才能回收原始内存。

 

在大规模情况下,即使存在延迟,不断进行 new/delete 周期的成本也会急剧上升。代理面对着数十万主机。添加或删除一个主机意味着复制大型的结构,在流量高峰期间会导致可观的内存波动。无锁读取很快,但延迟的内存回收成为了降低性能的昂贵代价。令人惊讶的是,切换回简单的基于锁的方法不仅更有效,而且更可预测。

大规模的 DNS 崩溃

使用 HAProxy 时,我们曾经遇到一个故障,揭示了大规模场景下如何暴露出较小规模下可以忽略的数学问题。内置的DNS解析器对某些场景会使用quadratic-time查找(这意味着查找时间与记录或主机的数量的平方成比例增长)。在小型主机数量下,额外的工作是可以忽略的,系统能够顺畅运行。

 

但当我们在更大的舰队上启用这个代理时,成本一下子显现出来。曾经只是一个小问题的事情在数百台主机上变得难以收拾,这导致整个代理舰队出现 CPU 峰值和崩溃。

 

后来,这个bug在上游进行了修复,但教训一直伴随着我们。低效的行为变得危险并不一定需要改变它们的复杂性。有时,规模会使隐藏的成本变得无法忽视。

 

生产教训:在小规模下“工作正常”的代码可能仍然隐藏着O(N²)或更糟的行为。在数百或数千个节点上,这些成本不再是理论上的,而是真正开始破坏生产环境。

平凡的故障:默认值和例行任务的反噬

导致数十亿美元的系统产生崩溃很少是由于冷僻的 zero-days 攻击或深奥的协议错误。它们几乎都是平平无奇的,比如,错位的字符、被遗忘的默认值,或者操作系统功能过于完美地执行其工作。

引发致命灾难的 YAMl 逗号

对于某些路由和策略决策,我们的代理会从远程服务获取运行时元数据。工程师在 UI 中能够编辑这个值,它预期是一个逗号分隔的列表(a,b,c)。有一天,LinkedIn 的一位工程师漏掉了一个逗号,将列表变成了一个单一的畸形令牌。控制服务的验证很少,因此将错误的载荷传递给了下游。我们的代理解析器更严格。当代理拉取更新并尝试将值解释为列表时,它无法对其进行处理并导致了崩溃。

 

因为这些元数据对启动至关重要,所有需要立即重启的实例在获得相同的错误值后都会再次崩溃。更糟糕的是,用户界面本身就位于代理之后,所以我们无法修复列表,直到我们执行了一次带外(out-of-band)恢复。

沉默的杀手:文件描述符和 Watchdogs

基本的操作系统限制可能会变成灾难性的失败。在一次事件中,系统标准化将最大文件描述符(FD)限制重置为一个更低的默认值,这对于大多数应用程序来说是合理的,但对于处理数十万个并发连接的代理来说却远远不够。在高峰流量期间,代理耗尽了文件描述符(FD)。新的连接和正在进行的请求被默默地丢弃或延迟,导致级联故障看起来比实际情况要复杂得多。

 

另一次中断来自于“例行清理”。一位工程师发现了在用户nobody下运行的进程,并假设它们是离散的。许多 Unix 服务(包括我们的代理)故意以nobody身份运行,以减少权限。清理脚本在全舰队范围内杀死了它们,瞬间使网站的很大一部分瘫痪。

 

生产教训:最具破坏性的故障往往并不引人注目。它们来自默认设置、不良输入和每个人都视为理所当然的日常清理任务。我们要始终将远程元数据视为不可信的,验证语义,而不仅仅是语法。缓存并回退到已知的最新正确值。将控制平面与数据平面解耦,并在金丝雀后面分阶段进行更改。如果可能的话,优先选择静态配置而不是动态元数据。在危险行为之前监控资源,并在每个全舰队范围内的行动周围强制执行护栏。

信任但验证:测量热路径

在大流量的基础设施中,缺乏根据的假设是一种毒药。在大规模环境中,最小的函数可以悄悄地消耗不成比例的资源。

不是缓存的缓存头信息

在代理中解析头信息是代价昂贵的操作,我们的许多策略逻辑依赖于检查特定的头信息。为了优化这一点,我们的代码库使用了一个名为 extractHeader 的方法,它被注释为值将会被缓存,并且头信息只解析一次。从表面上看,代码似乎就是这样工作的,有一个 Boolean 标志指示结果是否已经被提取。

 

然而,当我们在大规模环境中分析 CPU 的使用情况时,头信息解析不断作为一个瓶颈出现。这有点出乎意料,函数名称已经承诺会缓存头信息。经过深入研究,我们发现多年来,该函数积累了新的逻辑。在某个地方,headerExtracted 标志被重置了,每次访问头信息时都强制进行完整的重新解析。在单个请求中,同一个头信息可以被重新解析数百次。

 

调试持续了数周,因为方法的名称创造了一种虚假的信任感。它看起来像是缓存,但实际上几乎没有缓存任何东西。

随机数瓶颈

乍看上去,生成随机数像是一个简单的、无状态的计算,是一个微不足道的、可以忽略的操作。实际上,常见的rand()实现依赖于一个全局锁来保护其状态。在低 QPS 下,锁争用是不可见的。但是,多核心机器在持续负载下,那个全局锁就会变成一个热点。请求堆积等待“随机性”,本应是系统中成本最低的操作之一变成了延迟和吞吐量崩溃的根源。

 

我们的解决方案是切换到一个低成本、线程安全的随机数生成器,它专为并发设计,但是充分汲取了教训。即便是我们认为无状态的数学函数也可能存在隐藏同步和争用成本,在大规模环境下引发大问题。

 

生产教训:永远不要假设“简单”的库调用是免费的。在热路径中对它们进行分析,特别是在多核心、高 QPS 工作负载下,隐藏的锁会将微不足道的函数变成瓶颈。

蹩脚的头信息检查

有些问题不是来自错误的代码,而是来自存在隐藏的昂贵副作用的惯用代码。

 

一位开发人员曾经用 Go 编写了一个简单的检查,以验证 HTTP 头信息是否为空:

splitted_headers := strings.Split(header, ":")if len(splitted_headers) > 1 { ... }
复制代码

这是完全惯用的 Go 代码:清晰、可读、安全。在单元测试和小规模运行中,它能够完美地工作。但在生产环境中,面对每秒数千个请求,strings.Split的开销就变得明显了。每次调用都会分配一个新的切片,在热路径中会创建不必要的混乱。CPU 周期消失在分配操作中,延迟悄然上升。

 

解决方案也非常简单,那就是避免分割。通过直接扫描字符,我们消除了分配操作,并将检查变成了一个轻量级的操作。

 

生产教训:惯用代码并不意味着是可以部署到生产环境的代码。在测试中看起来简单或无害的操作在大规模环境中可能会成为隐藏的瓶颈。在热路径中,缺乏根据的假设代价高昂。客观地进行分析,相信数据而不是假设。

异常不是常态:保持公共路径的清洁

在分布式系统中,优雅往往来自于统一,即一个规则涵盖所有情况。但将正常路径和异常合并在一起,将会使系统变得脆弱和缓慢。

哈希键争用

在调试负载均衡器中的争用问题时,我们注意到每个请求都要承担哈希查找的成本,主机更新也会停滞。更令人惊讶的是,哈希表通常只包含一个键。

 

根本原因是一个罕见的部署。一个上游团队将同一个应用程序拆分到多个集群中,每个集群都映射到不同的分片键。为了支持这个案例,代码被泛化了,总是期望一个像这样的结构:

{ app_name => { hash_key1 => host_list1, hash_key2 => host_list2 } }
复制代码

但对于几乎所有的应用程序来讲,只有一个主机列表。基于分片的间接性是一个例外,而不是常态,但它成为了每个人的默认设置。

 

我们将其简化为:

{ app_name => host_list }
复制代码

这消除了不必要的哈希查找,消除了更新争用,并使系统更快、更容易理解。罕见的基于分片的部署会被明确地进行处理,放在了公共路径之外。

异常情况驱动的错误修复

在代理迁移期间,我们最初将大多数设置保留为默认值。迁移开始后不久,我们收到了一个涉及异常长头信息和 cookie 的罕见用例失败的报告。快速修复是非常简单的:提高限制。问题消失了,直到下周再次出现。我们再次提高了限制,然后一次又一次,每次都来自同一个异常。

 

只有当我们进行基准测试时,真正的成本才显现出来:每次提高限制都会增加内存使用量,降低整体吞吐量。通过迎合一个异常值,我们降低了每个人的性能。

 

正确的答案不是让系统适应那个异常。我们撤销了对限制的更改,并要求那个单一用例继续使用旧技术栈。我们这样做了之后,新技术栈立即提供了更高的吞吐量和更低的延迟。团队最终修复了有问题的 cookie,并在后来迁移到了新技术栈。

实验膨胀

在我们的代理中,我们建立了对实验的支持,旨在进行快速的 A/B 测试、功能推出或迁移。该机制非常有效,但需要仔细设置和验证。后来,有人默认扩展了它:添加了工具来为每个服务自动生成实验配置。

 

起初,这似乎是一个胜利:减少了手动工作,更容易启动。实际上,大多数这种实验都是无效的。它们没有按预期工作,但给人的印象它们却是按照预期运行的,这误导了运维人员。调试变得很痛苦,因为失败的实验看起来像是路由问题,而且启动序列经常在额外的复杂性下出现中断。

 

最终,我们回滚了更改,移除了默认的自动生成功能,并要求实验要明确且逐个进行添加。后来,工具更新为自动检查其他来源是否需要这样的实验,并仅在这些场景中添加。这些更新大幅减少了路由规则的数量,节省了关键的 CPU 周期,并保持了网站的稳定。

 

生产教训:永远不要让异常情况决定常态。明确地处理它们,将其隔离在特定路径或层中,而不是污染主线逻辑。看起来像“灵活性”的东西通常只是延迟出现的脆弱性,等待在大规模场景上显现。

为高压下的运维人员而设计

机器运行着系统,但它们的恢复需要人类来执行。代理可能每秒处理数百万请求,但当边缘出现问题时,系统恢复依赖于一个在凌晨 3 点盯着终端的疲惫运维人员。

仪表板变黑的场景

在部分停电期间,我们的整个监控和警报管道都变黑了。仪表盘、追踪 UI 和服务发现控制台都离线了,我们甚至不能从受影响的数据中心失败中脱离出去,因为故障转移的用户界面(UI)和命令行界面(CLI)都依赖于已经降级的服务。拯救我们的是基础指令:sshgrepawknetstat。有了这些肌肉记忆工具,以及最终在故障转移工具中埋藏的手动工具,我们追踪失败的流程,隔离了错误的层,并强制执行故障转移。如果团队失去了对基础的舒适感,或者如果没有那个逃生舱口,我们就会手足无措。

 

我们还痛苦地认识到,可观测性系统永远不能依赖于它们要监控的代理。在某个时候,代理日志能够正确地发送到一个中央平台,但查看这些日志的 UI 和中央可视化服务器本身只能通过代理舰队进行访问。当舰队限于泥潭时,运维人员无法再访问仪表盘。日志仍在流动,但我们无法看到它们。

 

修复方法是在每个节点上保留一个本地日志路径,始终可以使用grepawk等简单的 shell 工具访问它们,即使这意味着冗余。但这保证了无论代理的状态如何,都能看到系统。

负载均衡器的开关迷宫

另一个痛点是我们的负载均衡算法。它试图处理一切:连接错误、预热、垃圾收集(GC)暂停、流量激增,有几十种开关,如阈值、步长、起始权重和衰减率。在纸面上,它看起来很强大,但在实践中,它是混乱的。

 

当因为某种情况出现失败时,运维人员要花费数小时在黑暗中尝试调整各种开关,这有时能解决问题,有时却让问题变得更糟。想象一下凌晨 3 点的紧急呼叫,紧接着是六个小时的猜测性工作。最终,我们放弃了复杂性,转而采用了一个简单的基于时间的预热机制,类似于HAProxy的慢启动。恢复变得可预测、流程化且快速,这是最好的运维结果。

 

生产教训:运维人员不会在完美条件下使用完美的仪表盘进行调试。他们会使用即便其他一切都在宕机时仍然有效的工具进行调试。设计你的边缘层,以便在功能丰富的工具消失时,基本的日志、纯文本、简单命令仍然能给运维人员足够的信息去观察和行动。

结论

反向代理是现代基础设施中最繁忙和最脆弱的点。我们的教训并不是来源于冷僻的协议或尖端算法,而是在规模扩大时才会出现的隐性成本、普通故障和运维人员现状。通过保持公共路径的精简,验证每一个假设,并为处于压力状态的运维人员设计功能,我们可以使得这个关键层既具有弹性又流程化,这是所有生产系统的理想结果。

 

原文链接:

When Reverse Proxies Surprise You: Hard Lessons from Operating at Scale

2025-12-07 16:0910

评论

发布
暂无评论

你应该搞懂的 C 语言头文件路径问题

矜辰所致

C语言 头文件 6 月 优质更文活动

推动开源行业高质量发展|2023开放原子全球开源峰会圆满落幕

开放原子开源基金会

开源 开发原子全球开源峰会 开发原子

大促质量备战之三化战役:“常态化、精细化、一体化” | 京东云技术团队

京东科技开发者

测试 质量 电商大促 企业号 6 月 PK 榜

万物云原生下的服务进化 | 京东云技术团队

京东科技开发者

Java 云原生 镜像 GraalVM 企业号 6 月 PK 榜

北京国际开源社区正式启航

开放原子开源基金会

开源 开放原子全球开源峰会 开放原子 北京国际开源社区

用友承办全国两化融合标委会工业互联网管理标准工作组全体成员大会圆满召开

用友BIP

工业互联网

OpenAI发布ChatGPT函数调用和API更新

楚少AI

openai GPT-4 ChatGPT4 chatgpt api gpt-3.5-turbo-16k

牛逼!Windows竟然也能运行QEMU虚拟机!

吴脑的键客

qemu windows10 windows 11

南宁建宁水务集团财务共享、全面预算项目正式启动

用友BIP

如何用Taro打造敏捷的移动App架构

没有用户名丶

2023 年最适用于工业物联网领域的三款开源 MQTT Broker

EMQ映云科技

物联网 mqtt mqtt broker

用友入选信通院“铸基计划”IPaaS标准贡献单位

用友BIP

数智平台

重庆企业购买堡垒机选择哪家好?理由有哪些?

行云管家

网络安全 堡垒机 重庆

火山引擎DataLeap:一个易用、高效的数据目录,是如何搭建的?

字节跳动数据平台

大数据 数据治理 数据目录 数据研发

《中国电子报》专访简丽荣:“模型热”将引发云计算与数据库行业大变革

酷克数据HashData

深度学习应用篇-元学习[14]:基于优化的元学习-MAML模型、LEO模型、Reptile模型

汀丶人工智能

人工智能 深度学习 元学习 元强化学习 6 月 优质更文活动

软件测试/测试开发丨Python类与对象学习笔记

测试人

Python 程序员 软件测试 测试开发 类与对象

深度学习应用篇-元学习[13]:元学习概念、学习期、工作原理、模型分类等

汀丶人工智能

人工智能 深度学习 元学习 元强化学习 6 月 优质更文活动

如何成功实施一个数据治理项目?实施步骤有哪些?

袋鼠云数栈

数字化转型 数据治理 企业号 6 月 PK 榜

“升级数智底座”中央企业创新发展沙龙在中国科技城(绵阳)举办!

用友BIP

数智底座 数智平台

体验 TDengine 3.0 高性能的第一步,请学会控制建表策略

爱倒腾的程序员

涛思数据 时序数据库 #TDengine

智慧隧道三维可视化管控平台系统

2D3D前端可视化开发

物联网 数字孪生 三维可视化 智慧隧道 智慧公路隧道

推动绿色计算 共迎绿色未来|2023开放原子全球开源峰会绿色基础设施技术分论坛圆满收官

开放原子开源基金会

开源 开放原子全球开源峰会 开放原子 绿色基础设施技术

还在为618电商推送方案烦恼?我们帮你做好了!

HarmonyOS SDK

HMS Core

公有云数据库新趋势,企业降本增效正当时

MatrixOrigin

数据库 分布式 云原生 超融合 HTAP

银行数字化转型研究与敏捷转型探索

L3C老司机

数字化转型 敏捷转型 敏捷组织 银行转型 敏捷探索

打造科学新高地|2023开放原子全球开源峰会科学智能分论坛圆满举行

开放原子开源基金会

开源 开放原子全球开源峰会 开放原子 科学智能

河北国控携手用友探索数智司库,加快建设世界一流

用友BIP

全球司库

Battery Indicator for Mac:Mac笔记本电脑电池电量剩余百分比显示工具

背包客

macos Mac软件 MacBook Pro Battery Mac电量显示软件

运维大规模反向代理的教训_DevOps & 平台工程_InfoQ精选文章