Reddit将评论后端从Python迁移至Go语言
出乎意料的是,Go和Python与数据库层的交互方式存在根本差异。Python通过ORM简化了对Postgres存储的查询和写入操作。Reddit的Go服务未使用ORM,而Python ORM底层某些未知优化机制导致新Go接口上线初期出现数据库压力
背景
在Reddit,我们有四个核心模型支撑着几乎所有用例:评论、账户、帖子和子版块。这些模型原本由一个老旧的Python服务提供支持,其维护责任分散在不同团队之间。到2024年,该Python服务已存在可靠性和性能问题,其所有权和维护工作对所有相关团队都变得日益繁琐。鉴于此,我们决定转向现代化的领域专用Go微服务架构。
2024年下半年,我们率先推进评论模型的全面迁移。Reddit用户热衷在评论区分享观点,因此评论模型成为我们规模最大、写入吞吐量最高的模型,自然成为首轮迁移的理想候选对象。
如何实现?
读取端点的迁移方案通常较为明确:我们采用 tap compare 测试法。该方法能在不影响用户体验的前提下,确保新旧端点返回相同响应。具体操作是:将少量流量导向新接口,获取其响应后,再从新接口调用旧接口,对比并记录两者响应。此时仍向用户返回旧接口响应以确保零影响,同时捕获新接口可能返回差异响应的日志。简单又安全!
另一方面,写入端点的迁移风险则高得多。
原因何在?首先,写入端点几乎总需向数据存储(缓存、数据库等)写入数据。我们需关注多个评论数据存储,且任何核心模型变更都会触发CDC事件。我们对这些变更事件提供100%的交付保障,Reddit其他关键服务依赖这些事件,因此必须确保事件生成过程零间隙、零延迟、零故障。本质上,与读取迁移仅需返回评论数据不同,评论基础设施涉及三个独立的数据存储层,这些都需纳入迁移考量:
- Postgres – 存储全部评论数据的后端数据存储
- Memcached – 缓存层
- Redis – 用于触发CDC事件的事件存储
若在迁移过程中仅进行数据存储的简单比较,而未对数据存储进行特殊处理,可能会导致新实现写入无效数据,而旧实现无法读取这些数据。为安全迁移Reddit最关键的数据,我们不能依赖在生产数据存储中验证tap compare差异。
由于评论ID的唯一键限制,数据存储中不可能出现重复写入。那么如何在不重复提交相同数据的情况下,验证两个实现对数据存储的写入?为正确测试新写入接口,我们搭建了三个专用于tap比较测试的姊妹数据存储,仅由新的Go微服务接口写入。这样就能将生产环境中旧接口写入的数据与姊妹存储中的数据进行比对,同时避免新接口破坏或覆盖生产数据的风险。
验证并行写入的流程如下:
- 将少量流量导向Go微服务
- Go微服务调用旧版Python服务执行生产环境写入
- Go微服务随后独立执行并行写入,与生产数据完全隔离

所有写入操作完成后,我们必须进行验证。我们从三个生产数据存储中读取数据(这些数据由旧版Python服务写入),并与Go微服务中三个姊妹数据存储的写入内容进行比对。
此外,为解决迁移初期遇到的序列化问题(Python服务无法反序列化Go服务写入的数据),我们验证了遗留Python服务中注释CDC事件消费者中的所有数据比对操作。

综上所述,我们迁移了3个写入端点(每个端点分别写入3个不同数据存储),并在2个不同服务间验证数据,最终运行了18个独立的分流比较操作,这些操作需要额外时间进行验证和修复。
结果与改进
我们欣喜地宣布,经过无缝迁移且未对Reddit用户造成任何影响后,所有评论接口现已由全新的Golang微服务提供支持。这标志着一个重要里程碑——评论功能成为首个完全脱离传统单体架构的核心模型!
本项目核心目标是将关键评论读写路径从老旧Python服务迁移至现代Go微服务,同时保持性能与可用性一致。然而从Python到Go的迁移带来了意外收获——迁移后的三个写入接口延迟成功减半。从以下p99延迟图可见成效(旧Python服务端点为绿色,新Go微服务端点为黄色):
创建评论端点

评论更新接口

评论属性递增接口

这些图表的x轴上限为0.1(100毫秒),以便清晰呈现差异,但旧版Python服务偶尔会出现高达15秒的严重延迟峰值。
经验总结
注释指出此次迁移虽成功完成,却为未来核心模型迁移提供了宝贵经验。我们遇到了几个值得关注的问题。
Go与Python的差异
跨语言迁移端点本身比Python内部迁移更具挑战性。理解两种语言的差异,以及如何在Thrift和GRPC层生成相同响应,是项目预期的难点。出乎意料的是,Go和Python与数据库层的交互方式存在根本差异。Python通过ORM简化了对Postgres存储的查询和写入操作。Reddit的Go服务未使用ORM,而Python ORM底层某些未知优化机制导致新Go接口上线初期出现数据库压力。所幸我们及时发现问题并优化了Go端查询逻辑。未来迁移中,我们将确保持续监控数据库查询及资源利用率。
评论更新中的竞争条件
Tap compare是确保新接口无差异的绝佳工具。然而我们在tap compare逻辑中遭遇了“虚假不匹配”问题。耗费大量时间排查后发现,这源于竞争条件。
假设我们正在比较一条将评论正文更新为“hello”的更新调用。该更新调用被路由至新的Go服务。Go服务在关联数据存储中更新评论后,调用Python服务处理实际更新。随后它将Python服务写入生产数据库的内容与Go服务写入关联数据库的内容进行比较。然而此时生产数据库的评论正文已变为“hello again”。这导致我们的tap比较日志出现无法解释的差异!我们意识到问题根源在于:在调用Python服务并执行数据库操作的几毫秒间隙内,被更新的评论内容再次被修改。
这使得确保新旧端点无差异变得极其复杂:差异究竟源于新旧端点实现中的缺陷,还是纯粹的时序冲突?未来我们将对数据库更新进行版本控制,确保仅比较相关模型变更。
测试
此次迁移过程中,大量时间都耗费在手动审查生产环境中的tap compare日志上。面向未来的内核模型迁移,我们决定投入更多时间进行全面的本地测试,再推进tap compare操作,以期尽早发现更多端点和转换差异。这并非意味着评论迁移缺乏充分测试,而是我们将把测试水平提升到全新高度。
每条评论由众多内部元数据字段构成,用于表示评论可能存在的不同状态——这导致评论的呈现方式存在数千种可能组合。我们虽已覆盖常见评论用例的本地测试,但仍依赖tap compare日志来发现小众边缘案例的差异。在未来的内核模型迁移中,我们计划通过使用真实生产数据指导本地测试,深入探索这些边缘案例,甚至在开始生产环境tap compare之前就完成准备工作。
后续计划
Reddit基础设施团队的目标是通过现代化技术栈实现可靠性与高性能,这意味着彻底淘汰旧有的Python单体架构。截至今日,四大核心模型中的评论与账户系统已完成迁移,帖子与子版块系统正在迁移中。不久后所有核心模型都将现代化升级,确保您在r/AmItheAsshole的自我审判和萌猫图片都能更可靠、更快速地送达!
本文文字及图片出自 Modernizing Reddit's Comment Backend Infrastructure
> 令人意外的是Go和Python与数据库层交互方式的根本差异。Python通过ORM简化了对Postgres存储的查询和写入操作。Reddit的Golang服务未使用ORM,而Python ORM底层某些未知的优化机制,导致我们启动新的Go接口时引发了数据库压力。所幸我们及时发现问题,成功优化了Go端的查询逻辑。
几周前我重新评估了Java的Hibernate框架,因为明年产品需要支持两种数据库。最终决定保持代码库不依赖ORM,因为直接调试SQL查询比追踪Hibernate的具体行为更高效。我认为在可预见的未来不会再使用ORM了。
我认为“Python”本身并未采用ORM,更多是开发者选择使用。你可以在Python中直接编写SQL语句,这正是SQLAlchemy Core与SQLAlchemy ORM存在差异的原因。
没错——这正是人们转向NoSQL的主要原因。ORM会带来性能损耗。在Ruby生态中,ActiveRecord与Sequel(更优的ORM)的性能差距就是明证。
如今已有更好工具如sqlc等,兼具ORM的易用性又避免了性能损失。
有时直接用NoSQL更合适
我从这篇帖子真正领悟到的是:从2005年到2024年,Python对Reddit而言始终足够优秀。
> 到2024年,老旧的Python服务已积累了可靠性与性能问题。该服务的维护工作对所有相关团队都变得日益繁琐。因此我们决定转向现代化的领域特定Go微服务架构。
用Python构建后端系统来“扩展”纯属权宜之计,从根本上就不可扩展——Reddit的经历正是明证。他们深知仅靠解释型语言的伪优化无法提升性能,而Go语言重构自然解决了这些问题。
这再次清晰表明:除最小可行产品(MVP)原型阶段外,在2025年仍使用解释型语言编写的后端进行扩展实属不明智。
转向Go这类安全、高性能且成熟的语言,不仅能普遍提升性能,更能正确处理并发问题——这正是Python长期挣扎的领域,尤其在规模化场景下。正因如此,在Reddit案例中,重写前暴露的竞态条件如今变得更加清晰。
Reddit创立于2005年,Go语言发布于2007年。
他们选择了当时可用且成熟的技术,支撑业务实现了二十年增长(日活超1亿+成功IPO)——在我看来这是相当明智的选择。
你知道还有哪个平台是用Python构建的吗?YouTube。
若要打造[特定类型的]市值数十亿美元的企业,Python并非糟糕选择。
Python几乎在所有场景下都不适合用作后端。它甚至不如其他后端框架“易用”——Java Spring和.NET框架同样适用,且针对此场景进行了专门优化。
若想构建易维护的应用,Python因动态类型和工具链缺陷绝非良选;若追求可扩展的高性能应用,Python本身就是致命缺陷;若急需上线网站,Python依然不是最佳选择——其他语言拥有更完善的开箱即用框架。
2005年时,即使PHP也是更优选项,其性能至今仍足以支撑运行。
如今情况更糟。Python确实有适用场景!实验性项目、科学相关领域、“一次性”代码。但应用程序开发在我看来绝非其适用场景。
他们最初用LISP开发,后来重写为Python(而且显然没选用任何“成熟”的Web框架)。
http://www.aaronsw.com/weblog/rewritingreddit
> 无法扩展
规模远超Reddit的Instagram对此持反对意见。
看吧,又有人抛出那个S字开头的词了。根据我的经验,每当有人在Web应用场景中提及“可扩展性”,我总能嗅到危险信号。
目前我们尚不清楚Reddit评论服务具体涉及哪些内容—— 或许 它确实需要大量CPU密集型处理,这种情况下迁移到Golang确实能带来帮助。
但也许它只是简单的“从数据库读取数据,输出JSON”,这种情况下瓶颈永远是数据库,所谓“可扩展性”不过是为工作找借口罢了。
这一举措源于从“传统”系统向“现代”“微服务”的迁移,这表明大量开发者正乐在其中,并受到激励去证明继续领取薪水、享受替换一个运行良好的系统之乐的合理性——而非解决实际的可扩展性瓶颈问题,毕竟这类问题无法通过简单增加硬件等更简便的方式解决。
> 此次迁移从“传统”系统转向“现代”“微服务”的事实表明,大量开发者正乐在其中…
我认为这根本不说明问题。这是他们的新闻稿,当然要这么包装。
Reddit作为访问量最大的网站之一,如今竟也觉得有必要弃用Python。普通用户的网站用Python完全没问题。
你觉得Python在什么规模下会崩溃?
这非常主观。使用Python会对架构产生其他语言无法比拟的影响。
我维护的某个关键服务用Python编写并部署在AWS上,约40个容器可稳定处理1000次/秒请求。但我们遇到了HTTP库问题和系统内部压力。
40个容器处理1k请求/秒,意味着每个容器仅25次请求/秒。你们是否在使用同步线程(即等待IO或网络调用时被阻塞,而CPU实际处于空闲状态)?如果是这样,切换到gevent并用少量容器处理负载可能会更有效。
每秒1000次请求 在处理什么 ?
在我看来这速度实在太低了。
我编写的软件能在单台机器的单核上处理每秒数百万条消息
我们用一台小型Hetzner VPS就达到每秒1000次请求,仅靠Nginx后端的Flask实现 ¯_(ツ)_/¯
没错,25次请求/秒/进程简直慢得离谱。任何语言都能写出这么慢的代码。
但你很少看到针对Ruby后端的类似抱怨,而Ruby的运行速度其实处于相同量级。
当投入更多硬件运行CPU密集型Python部分(而非等待数据库/IO/网络服务的环节——这些用Go语言也无法改变)的成本,开始超过雇佣开发者用新语言重写代码的成本时,就会出现问题。更别提引入新语言带来的技术栈混乱、现有应用的“部落知识”流失等弊端。
现代硬件速度惊人,若等待上述规模化场景出现,可能永远不会发生。重写决策往往基于政治因素而非工程限制,我怀疑此处正是如此。
Reddit级规模。对全球多数场景而言Python已足够。Go语言易用性极高,成为Python之后的自然选择。
我近十年都在强调:Go才是后端技术的未来。
当负责人宣布“方案可行,现在解决性能问题”时,Python的定位就如同现代BASIC语言——易于编写,适合原型开发、脚本编写、库集成及快速迭代。若需高性能与重度数据处理,任何语言都比它更优。PHP、Java,甚至JavaScript都比它强。
例如Python在解码40年前老旧硬盘的RLL/MFM数据时,实时性能就捉襟见肘(https://github.com/raszpl/sigrok-disk)。即便用4GHz处理器,简单循环也突破不了500KB/s:
要优化这段代码,请使用临时变量替代成员查找,以避免缓慢的getattr和setattr调用。即便如此仍无法超越编译型语言——数值计算始终是Python最不擅长的领域。
正因如此,实际使用Python时需付出代价:将数据迁移至原生模块(numpy/pandas/polars)进行所有数值计算,再提取结果。
虽非理想方案,但这已是成熟解决方案。Python在优质数据框库领域表现优异。
所有类变量都已存入__slots__,理论上不应有影响。你的建议很中肯。
这段代码可能仅涉及50-100条x86指令?原生代码运行速度超过100MB/s,而Python 3.14仅能勉强达到300KB/s。Python 3.4(Sigrok硬编码要求)表现更差:
您可尝试https://github.com/raszpl/sigrok-disk/tree/main/benchmarks 如有成功提升速度者,欢迎提交Pull请求。我目前每解码一个RLL硬盘轨道耗时约2秒,已放弃优化。
目前在i7-4790平台解码单轨道的结果如下:
“每条评论由多个内部元数据字段构成,用于表示评论可能存在的不同状态——这导致评论的表示方式存在数千种可能组合”
这种表述乍听令人毛骨悚然,但我确信实际情况应该相当合理。
没错,评论表面看似简单,实则暗藏玄机!例如:评论是纯文本还是富文本?是否包含多媒体?多媒体是照片还是动图?尺寸规格如何?内容类型是什么?再比如在需要Automod审核的子版块,评论会经历哪些状态?是否存在评论奖励机制?类似问题不胜枚举!
这确实引起了我的兴趣——很想了解更多详情。
难以置信我参与编写的Python代码竟能运行这么久!期待看到这个项目,它早就该实现了。
能问个不太相关的问题吗?你们的服务器是否仍全部采用亚利桑那时间?
当年我管理服务器时都用亚利桑那时间,这样日志时间戳计算就简单多了。全年大部分时间它和太平洋时间一致,剩余时间只差一小时。
最近有人问我是否还沿用这个设置,所以现在转问你们。:)
感谢你为Reddit历史做出的贡献,也抱歉我们正逐步拆解它!现在所有服务都已迁移至Kubernetes的Docker化容器中,并统一设置为UTC时区,所幸不再使用亚利桑那时间。
我们能提问吗?
服务上线后看到那些p99延迟图表,想必非常有成就感吧。
您提及语言特有的数据序列化问题很有意思:
确实令人欣喜!我们的基础设施团队对在现代架构中采用Go语言持积极态度。对于这类高RPS服务,Go的并发特性意味着生产环境中部署更少Pod即可实现比Python更高的吞吐量。加之Reddit内部已广泛部署并支持Go,开发门槛更低。基于这些优势,我们实际只考虑了Go方案。
关于Go语言和Python评论更新之间的竞态条件,您是否考虑过让事件存储中每个评论更新事件都拥有唯一的事件ID?这样Go语言无需调用Python,Python可独立订阅事件,并像Go语言那样以完全解耦的方式实时处理评论更新?
我们当前已为事件分配唯一ID,这在事件存储器数据截取比较中有效避免了竞争条件。但仍需数据库版本控制机制——因我们分别对三种数据结构(事件存储器、memcached和数据库)进行数据截取比较,必须确保数据库更新不会滞后。
好奇问一句:为什么手动执行tap比较日志?这似乎是可自动化的任务
我们通过日志聚合器审查了数据接口比较日志。许多出现的异常需要手动排查逻辑和代码变更才能修复,而特定问题修复后便不再产生日志。数据接口比较日志仅记录差异内容,因此一旦所有差异被解决,就无日志可查阅!唯一例外是前述的竞争条件,我们编写了自定义代码进行检测并忽略。