Py学习  »  Python

上线一周就重写后端?这家初创团队真的干了:把Python后端全砍掉换成Node

Python开发者 • 1 周前 • 54 次点击  

转自:CSDN(ID:CSDNnews)

作者:yakkomajuri

产品上线才一周,这个初创团队就做了件大胆的事——他们决定彻底重写后端。而且这次不是继续用炙手可热的 Python,而是换成了 Node。

乍听之下,这像是一场典型的“过早优化”,但他们的理由并不只是性能,而是一次对技术生态、团队节奏与长期可扩展性的全面权衡。

来源:https://blog.yakkomajuri.com/blog/python-to-node


我们刚干了一件疯狂的事:在产品上线仅一周后,我们把后端从 Python 完全重写成了 Node。

为什么要这样做?为了可扩展性 没错,就是为了扩展,刚上线一周就这么做了。

从某些角度看,这个时间点其实挺合适的——毕竟当前代码库还小,用户体量也不多。但另一方面,这在很多开发者看来,完全违背了早期创业公司的建议,过往很多人觉得初创公司就是应该把产品先上线、先卖产品,等达到产品市场匹配再考虑扩展。

其实,我们并没有经历过那种用户暴涨、迫使我们快速扩展系统的神奇发布周。通常来说,你选择的技术栈应该能够在很长一段时间内合理扩展,直到你真正需要考虑换框架或用另一种语言重写后端(比如 Rust)的时候。

那么,为什么我们要弃用 Python 而选择 Node?接下来分享我们这么做的具体原因。


Python 的异步真的太糟糕了

我是 Django 的铁粉。在知名的开源产品分析平台 PostHog 项目中,我第一次接触它,从那以后它几乎成了我大部分项目的首选后端框架。Django 能让你快速启动项目,提供优秀的工具和抽象,同时也足够灵活,能按需调整。

所以自然地,当我开始为自己的 Skald(一个用于构建 AI 原生应用的 API 平台,https://github.com/skaldlabs/skald)项目编写后端代码时,也选择了 Django。

问题是,Skald 经常调用大语言模型(LLM)和向量嵌入 API,这意味着我们有大量的网络 I/O,希望是异步的。不仅如此,我们还经常希望并发发送大量请求,比如在为文档的不同片段生成向量嵌入时。

在 Django 中,这些操作很快就变得非常混乱。

我先说明一下,我和同事都没有太多 Python 异步编程经验(我以前主要在 Node 上做异步密集型服务),但我觉得这正是问题的核心:写出稳健高效的 Python 异步代码非常困难,也不直观。你必须深入理解各种底层原理才能做到。

我其实很想花时间真正掌握 Python 异步,但在我们的场景下,你会面临两个问题:

  1. 作为早期创业公司,你会浪费宝贵的时间,而这些时间本该用来上线产品。

  2. 在这个过程中,很容易踩坑,把自己搞得一团糟。

起初,我把问题归咎于自己——脑子里一直在响起“糟糕的程序员!糟糕的程序员!”的声音。虽然更有经验的人会轻松些,但我们发现 Python 异步编程的基础其实也有些不牢固。

与 JavaScript 不同,它从一开始就有事件循环;Go 也创造了 goroutine 的概念(这两种并发模型我都很喜欢,也在生产环境中用过)。而 Python 的异步支持是后来才补上的,这正是困难所在。

有两篇博客很好地解释了这个问题:

  • 《Python has had async for 10 years — why isn’t it more popular?》:https://tonybaloney.github.io/posts/why-isnt-python-async-more-popular.html

  • 《Python concurrency: gevent had it right》:https://harshal.sheth.io/2025/09/12/python-async.html

这两篇内容是在我开始深入研究之前不久发布,非常及时。

从中,我们学到的几点经验:

  • Python 没有原生的异步文件 I/O。

  • Django 仍然没有完全支持异步。ORM 的异步还没做完,colored functions 问题在这里非常明显。理论上可以用 Django 写异步,但官方文档里有太多限制和警告,几乎会吓退任何人。

  • 你到处都得写 sync_to_async 和 async_to_sync。

  • Python 生态中出现了各种模型来改善异步支持,但它们不是原生的,各有局限。例如,aiofiles 提供异步文件操作接口,但底层使用了线程池;Gevent 的 greenlets 很酷,但它实际上是对标准库进行了补丁才能工作。

  • 由于很多 Python 异步支持依赖于语言之上的层,而非原生,你写的异步代码要非常小心,否则会受到运行环境的影响,例如 Gunicorn 的 worker 类型(顺便说一句,从 Gunicorn 文档里很难学到这些)。

总的来说,仅仅想实现一个类似 Promise.all 的功能,同时还要理解它所有的坑,根本不是件简单的事。

面对这个问题,我回到了 PostHog 的代码库看看。

我之前在 PostHog 工作了三年,当时 Django 代码库里完全没有异步,但他们是大公司,现在还加入了 AI 功能,他们肯定已经解决了这些问题!

结果我发现,他们仍然运行的是 WSGI(不是 ASGI),用的是 Gunicorn Gthread workers(最大并发请求通常是 CPU 核心数的 4 倍),所以从异步运行中几乎没有获得多少好处。代码库里有很多工具来让异步正常工作,比如他们自己实现的 async_to_sync。所以我猜,他们处理大量负载的方式可能还是水平扩展

总结一下,Django 没有一个真正优秀的异步解决方案。


那接下来怎么办?

我们基本上得出结论:Django 很快就会成为我们的瓶颈,不只是当负载很大时才会出现问题。

即使用户不多,我们也已经需要开始运行多台机器来避免延迟太高,而且还得写很多笨重、难维护的代码。

当然,我们也可以暂时“做那些不易扩展的事情”,用钱(或者 AWS 额度)解决问题,但感觉不对。而且现在阶段还早,把后端迁移到另一个框架反而会更容易。

这时候,有人可能会建议道:“直接用 FastAPI 吧!”——事实上我们确实考虑过。

FastAPI 支持真正的异步,而且很受欢迎,性能据说不错。如果你想搭配 ORM,可以用 SQLAlchemy,它也支持异步。

迁移到 FastAPI 可能会帮我们省一两天时间(我们的迁移花了 3 天),因为很多代码可以直接复用,无需迁移。但到这个时候,我们对 Python 异步生态整体并不太有信心,而且我们其实已经用 Node 写好了后台 worker 服务,所以我们觉得这是一次机会——干脆全力投入一个生态系统。

于是,我们选择了迁移到 Node。我们花了一点时间挑选框架 + ORM 的组合,最终决定用 Express + MikroORM。

是的,Express 有些老,但经过验证很可靠,而且用起来很熟悉。反正我们迁移的重点就是JS 事件循环


收获与失去

收获:效率

初步基准测试显示,我们的吞吐量提升了大约 3 倍,而且这只是把主要是顺序执行的代码放到异步环境里。现在用 Node 后,我们计划在分块、嵌入、重排序等环节做大量并发处理。这意味着,随着时间推移,这次改动的回报会更大。

失去:Django

失去 Django 让人心痛,而且我们已经发现,在 Express 端需要自己写更多中间件和工具。Node 里也有功能更全面的框架,比如 Adonis,但迁移到一个全新的生态对我们来说工作量太大,所以还是选择了一个最小化的方案。

我最怀念的是 Django 的 ORM,它真的很人性化。虽然在追求极致性能时,ORM 总得小心使用,但 Django ORM 在底层做了很多优化,让你可以在 Python 里写查询而仍有不错性能。我们在把 Django 模型迁移到 MikroORM 实体时,也学到了一些这方面的经验。

收获:MikroORM

MikroORM 是这次迁移中的一个安慰奖。虽然我仍然更喜欢 Django ORM,但不同生态需要不同工具。

以前我从未使用过 MikroORM,但惊喜地发现,它有类似 Django 的懒加载机制,迁移设置感觉比 Prisma 更好,同时 API 也相对人性化(前提是你手动把基础设施搭好)。

总体来说,我们刚开始使用 MikroORM,但目前很高兴在它和原本的 Prisma 之间选择了 MikroORM。

失去:Python 生态

这个其实不用多解释。虽然大多数构建 RAG(Retrieval-Augmented Generation)和智能体的工具都有 Python 和 TypeScript SDK,但 Python 仍然是首选,这里我们只是说 API 封装层。

一旦你想自己深入做 ML,Python 根本无可匹敌。我猜随着我们项目复杂度增加,最终可能还是会有一个 Python 服务,但现在我们还没这个需求。

收获:统一的代码库

我们一直意识到迁移到 Node 意味着我们会有两个 Node 服务,而不是一个 Python 服务加一个 Node 服务。但直到有一天,我们才意识到其实可以把代码库合并,这会非常有帮助。

Node worker 和 Django server 之间有很多重复逻辑,现在我们把 Express server 和后台 worker 统一到了同一个代码库,感觉好太多了。它们都能用同一个 ORM(之前 worker 还得用原生 SQL),还能共享大量工具函数。

收获:更好的测试

这并不是 pytest vs jest 的问题,而是为了确保迁移后所有功能正常,我们写了大量测试。同时做了一些重构,这也是一个很棒的附加收获。


我们是怎么做的?

差不多该总结这篇文章了,但这里简单记录一下实际迁移过程:

  • 我们花了三天完成迁移。

  • 在最后几个环节之前,几乎没用 AI 生成代码——我们觉得理解新架构基础非常重要,特别是新 ORM 的内部运作。基础都搞定后,Claude Code 在生成一些不太重要的接口代码以及扫描代码库问题时帮了大忙。

  • 我们几乎多次想放弃。当时客户不断提出新功能请求,Django 代码里还有一些 bug,让人感觉迁移是在浪费时间,而不是为客户服务。


我们会再做一次吗?

老实说,我们对这个决定非常满意,100% 会再做一次。这不仅在长期会带来回报,实际上现在就已经开始看到好处了。

在这个过程中,我们也学到了很多东西。

如果你想看看实际代码,可以查看以下 PR:

  • skaldlabs/skald#56https://github.com/skaldlabs/skald/pull/56

  • skaldlabs/skald#68https://github.com/skaldlabs/skald/pull/68

Skald 是一个 MIT 授权的 RAG API 平台:https://github.com/skaldlabs/skald

推荐阅读  点击标题可跳转

1、纳德拉亲口承认:微软 GPU 堆成山,却因缺电在仓库吃灰!

2、Meta 被爆按代码行数来裁员

3、吵翻了!CTO 不搞管理反而沉迷写代码改 Bug,还靠这保住百万订单。网友:小公司才敢这么干,大公司早乱了

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/188807