
產品上線才一周,這個初創團隊就做了件大膽的事——他們決定徹底重寫后端。而且這次不是繼續用炙手可熱的 Python,而是換成了 Node。
乍聽之下,這像是一場典型的“過早優化”,但他們的理由并不只是性能,而是一次對技術生態、團隊節奏與長期可擴展性的全面權衡。
來源:https://blog.yakkomajuri.com/blog/python-to-node
作者 | yakkomajuri 責編 | 蘇宓
出品 | CSDN(ID:CSDNnews)
我們剛干了一件瘋狂的事:在產品上線僅一周后,我們把后端從 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 異步,但在我們的場景下,你會面臨兩個問題:
作為早期創業公司,你會浪費寶貴的時間,而這些時間本該用來上線產品。
在這個過程中,很容易踩坑,把自己搞得一團糟。
起初,我把問題歸咎于自己——腦子里一直在響起“糟糕的程序員!糟糕的程序員!”的聲音。雖然更有經驗的人會輕松些,但我們發現 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: https://github.com/skaldlabs/skald/pull/56
skaldlabs/skald: https://github.com/skaldlabs/skald/pull/68
Skald 是一個 MIT 授權的 RAG API 平臺:https://github.com/skaldlabs/skald
特別聲明:以上內容(如有圖片或視頻亦包括在內)為自媒體平臺“網易號”用戶上傳并發布,本平臺僅提供信息存儲服務。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.