前言

假設要做同一套會員與訂單 API:有路由、驗證、ORM、資料庫連線、Redis 快取與 JSON 回應,分別用 Go、C#、Java、Node.js、PHP 實作。在功能一樣、部署成容器的前提下,哪一種比較省記憶體?哪一種容易在高流量下卡住?正式環境又要多照顧哪些地方?

我的選型起點是:**重視精簡常駐服務,先評估 Go;重視複雜業務與持續吞吐,評估 C#、Java;以 TypeScript 整合服務,評估 Node.js;快速交付商業 CRUD,評估 Laravel,並把 FPM 與 Octane 分開看。**接下來把這些判斷放進同一個專案比較,說明背後的 CPU、記憶體與執行模型差異。

比較基準:同樣功能、相同資源與相同達標條件

本文比較的是 Linux 容器中的後端 API。JavaScript/TypeScript 都以 Node.js 執行後端來看;C# 使用現代 ASP.NET Core。PHP 則分成傳統 PHP-FPM 與 Laravel Octane,因為兩者的請求生命週期不同。

先定義一套共同的微服務規格,避免拿精簡 JSON endpoint 和完整訂單系統比較:

項目 本文採用的共同條件
業務功能 會員查詢、訂單列表、建立訂單;含權限驗證、欄位驗證與基本商業規則
資料存取 相同 PostgreSQL schema、索引、資料量與交易邊界;ORM 產生的查詢數與資料欄位要對齊
快取與輸出 相同 Redis 策略、TTL 與 JSON 欄位;快取命中、未命中與寫入分開比較
連線預算 每個服務副本的 DB 後端連線總上限先設為 20;多程序與外部連線代理一起計入
執行環境 相同 CPU 架構與容器資源;固定 runtime、框架、套件版本,使用正式環境建置並關閉 debug
效能目標 比較相同成功 RPS 下的 CPU、記憶體與 P95/P99;例如要求 P95 小於 100 ms、錯誤率小於 0.1%

最後一列是假設的驗收條件,可以換成自己專案的目標。P95 表示 95% 的請求耗時落在該數值以內,P99 則用來觀察更尾端的慢請求。

候選堆疊可以選 Gin/Echo + GORM、ASP.NET Core + EF Core、Spring Boot + Hibernate、Fastify/Express + Prisma,以及 Laravel + Eloquent。這些框架的預設工作量不相同,因此「同樣功能」還要包含相同的查詢、驗證與序列化行為。例如,某一組多了 ORM 關聯追蹤或 N+1 查詢,測出來的就包含這些額外工作。

**本文沒有執行這套跨語言實驗。**以下將官方文件確認的執行機制、工程選型判斷,以及用於規劃的假設數字分開呈現;不把未量測的 RAM 範圍與 CPU 倍率寫成 Production 實績。機制與功能依 2026 年 10 月 16 日查閱的文件整理。

同樣專案,資源差異先看這張表

以下是針對上述 CRUD 服務的工程判斷。記憶體描述的是執行模型帶來的壓力來源,CPU 則要連同資料轉換、ORM 與實際業務運算判讀。

堆疊 基礎記憶體與負載成長 同流量 CPU 的主要特性 併發增加時先注意什麼
Go/Gin、Echo 精簡服務通常有較低的基礎開銷;隨在途請求、buffer、連線與 heap 成長 原生編譯;物件配置量、JSON 與 GC 仍會消耗 CPU goroutine 是否無上限、DB pool 等待、配置速率
C#/ASP.NET Core,JIT CLR、JIT、DI 與 ORM 快取有固定開銷;GC 策略影響保留的記憶體 暖機後適合持續吞吐,可在單程序利用多核心 同步阻塞、ThreadPool 排隊、GC 與 ORM tracking
Java/Spring Boot,JVM JVM、類別、框架容器與 ORM 有基礎開銷;heap 之外也要預留空間 暖機後適合持續運算;不能用啟動速度推斷穩態效能 GC、執行緒/virtual threads、連線池與物件配置
Node.js/Fastify、Express V8、模組與應用狀態構成底座;多程序會複製部分固定開銷 非同步 I/O 合適;主要 JS 業務運算集中在各程序的事件迴圈 event loop lag、大型 JSON、同步函式與多程序總 RAM
PHP/Laravel + FPM 共享 OPcache 加上多個 worker;worker 私有記憶體會隨數量增加 請求期間會重新建立框架的請求狀態,包含重複初始化成本 pm.max_children、等待中的 worker、FPM queue
PHP/Laravel Octane 應用常駐於 worker;基礎用量與 worker 數相關 可省掉反覆啟動 Laravel 的部分工作,改善幅度取決於該部分占比 跨請求狀態、worker 回收、所選 server 與套件的 I/O 行為

這可以支持「Go 值得先驗證服務密度,C#、Java 值得驗證持續吞吐」的選型方向。但 Node.js 也能處理高併發 I/O,PHP 也要按執行模式比較。單憑語言,無法推出「同一個 API 必定耗用 1 倍、3 倍或 8 倍 CPU」。

記憶體想先抓數字,可以從配額實驗開始

假設是一個功能精簡、沒有大型本機快取的 API,我會先安排下面這組小規模試跑配額,觀察啟動與低流量行為,再調整。這些數字是本文的規劃範例,不是閒置/高負載 RAM 的測量結果,也不承諾能承接 2,000 RPS。

堆疊 Memory request 起點 Memory limit 起點 初期要確認的條件
Go 128Mi 256Mi 控制結果集、buffer 與在途請求;依實際用量調整
C#/JIT 256Mi 512Mi 觀察啟動尖峰、GC 模式、EF Core 與原生記憶體
Java/JVM 512Mi 1Gi heap 要小於容器上限,另留類別、執行緒與 native memory
Node.js,單程序 256Mi 512Mi V8 heap 之外還有 Buffer、原生套件與其他程序記憶體
Laravel + FPM 512Mi 1Gi 明確限制 worker 數,並計入 OPcache 與 HTTP 入口程序
Laravel Octane 512Mi 1Gi 明確記錄 request/task worker 數與使用的 server

各組 CPU 可先統一使用 CPU request 250m、CPU limit 1 觀察,避免預先替某種語言分配更多 CPU。正式比較還要再跑一輪相同記憶體上限,例如全部 1Gi;上表不同配額下的結果不能直接用來宣告語言效能勝負。

Kubernetes 的 request 主要影響排程與資源分配,並非即時用量;CPU limit 透過節流限制使用,記憶體超限則可能觸發 OOM 終止。把 CPU limit 設得很小,啟動或瞬間運算也可能變慢。Kubernetes 資源管理文件

各語言的差異,實際發生在哪裡?

同一套業務會用到相近的資料結構與外部服務,但執行環境決定了等待、運算與狀態如何被保留。以下逐一對照對這個 CRUD 專案的影響。

Go:適合精簡服務,但 goroutine 和 GC 都有成本

Go 的常見部署方式是編譯後執行,不需要 JIT 暖機。對邊界清楚的 API,這讓部署與啟動流程比較直接,也是我會優先拿它驗證小型常駐服務資源需求的原因。

記憶體方面,編譯器會依逃逸分析決定哪些值能放在 stack,哪些需要進 heap;不能只憑用了 struct 就保證沒有 heap allocation。goroutine 的 stack 可以成長,還會保留請求資料、閉包與 buffer,因此「一萬個 goroutine」不能只用初始 stack 大小推算總 RAM。Go GC Guide

放回這個專案,該優先觀察的是查詢結果是否一次載入太多、JSON 是否產生大量暫存物件,以及 DB 變慢時等待中的請求是否無限制增加。GOMEMLIMIT 可協助調整 runtime 記憶體使用,但屬於軟限制;設得太緊可能讓 GC 花更多 CPU,也不能涵蓋所有外部配置。Go runtime 記憶體限制

C#:持續吞吐是強項,記憶體要連 GC 與 ORM 一起看

ASP.NET Core 可透過非同步 I/O 讓等待 DB 的請求不持續占用工作執行緒,運算也能由多個執行緒利用多核心。對包含驗證、交易與多層商業邏輯的 API,這是值得評估的執行模型;實際效率仍取決於同步阻塞、物件配置和資料存取方式。ASP.NET Core 效能建議

C# 容器的用量包含 runtime、JIT、heap 與應用快取。ASP.NET Core 通常配置 Server GC,但實際模式還受可用處理器與設定影響。可以比較 DOTNET_gcServer=0 的 Workstation GC,觀察記憶體、GC 頻率和延遲的取捨;它不是保證 RAM 減半的開關。ASP.NET Core 記憶體管理

文件中的容器預設 75% 指的是特定條件下的 GC heap hard limit percent,不能解讀成「整個 Pod 到 75% 才開始 GC」。heap 限制也不是程序所有記憶體的上限。.NET GC 設定

Native AOT 可以改善啟動與記憶體需求,但這套基準使用 EF Core,不能直接套用精簡 AOT 範例的成果。截至查閱時,EF Core 官方仍將 NativeAOT 與查詢預編譯列為實驗性功能,並不建議該組合投入正式環境;因此本文先以一般 JIT 部署比較。Native AOT、EF Core NativeAOT 限制

Java:框架底座與暖機要計入,穩態運算也要公平比較

Spring Boot + Hibernate 包含 JVM、類別載入、相依注入與 ORM 初始化,對小型 CRUD 服務可能形成明顯的固定開銷。實際專案可以利用 Spring Boot 的 startup 觀察能力,查出時間花在哪些初始化步驟,而非把全部歸因於 Java 語言。Spring Boot 啟動觀察

暖機完成後,Java 同樣能利用多核心並持續執行業務運算。若採用 JDK 21 起正式提供的 virtual threads,可以降低大量等待型工作的執行緒開銷;這提高的是併發擴展能力,CPU 運算本身不會因此加速,DB 連線上限也不會自動增加。OpenJDK JEP 444

記憶體設定也要區分 Java heap 與整個程序:-Xmx 不包含所有類別 metadata、執行緒與原生配置。將 -Xmx 設成容器 memory limit,會擠壓其他必要空間。Oracle JVM 記憶體診斷

在這套專案中,Java 應與 C#、Go 一起驗證穩態吞吐,同時把啟動與記憶體列為獨立指標。若另測 GraalVM Native Image,要記錄反射、資源存取與 metadata 設定,不能將它與一般 JVM 部署混成同一組結果。GraalVM reachability metadata

Node.js:I/O 併發可以很好,CPU 工作要避開主事件迴圈

Node.js 用少量執行緒處理大量等待型工作,並不是「只能一次處理一個 HTTP 請求」。對 Redis、DB 或外部 API 等待占大部分時間的服務,這個模型很合適。問題出現在主事件迴圈長時間執行同步業務邏輯、大型 JSON 或資料轉換時,其他請求也必須等待。Node.js 事件迴圈指南

如果給單一 Node.js 程序更多 CPU,主要 JavaScript 業務邏輯不會自動平均分到各核心。可使用受控的 worker_threads 池、多程序或多副本處理 CPU 工作,但要計入資料傳遞與額外執行環境的開銷。async 本身也不會把計算搬到背景核心。Worker threads 文件

記憶體則要區分 V8 heap 與整個程序的 RSS。Buffer、原生套件和其他執行緒也占空間,heapUsed 小不代表容器 RAM 小;增加程序時,每個程序的模組、連線與應用狀態也會影響總用量。Node.js memoryUsage

PHP:FPM 和 Octane 的容量模型要分開

PHP-FPM 的 worker 通常常駐,但 Laravel 的請求範圍狀態會在每次請求建立。OPcache 可以共用已編譯的 bytecode,減少重複載入與解析 PHP 程式碼;它不等於把整套已初始化的 Laravel 應用物件留給下一次請求。PHP OPcache

FPM 一個 worker 同時服務一個請求,pm.max_children 決定 pool 的同時處理上限。等待 DB 時也會占住 worker,因此高等待、高併發的 API 可能需要更多 worker,或開始在 FPM queue 排隊。DB 連線是否重建則取決於 driver、持久連線與代理設定;PHP 支援在相同 worker 重用持久連線,不能說每次都必定重新握手。PHP-FPM 設定、PHP 持久連線

Octane 讓 Laravel 啟動後常駐,再接續處理請求,能省去部分反覆初始化成本。但 Swoole、RoadRunner、FrankenPHP 的模型與 I/O 行為不同,不能把「使用 Octane」直接等同「所有 DB 呼叫自動非同步」。正式使用要關注跨請求殘留狀態、舊的 request/container 參照、記憶體成長與 worker 重載。Laravel Octane 文件

因此,這套 CRUD 若已經用 Laravel 快速完成需求,下一步可以先量出框架初始化、SQL 與等待各占多少,再決定調整 FPM、採用 Octane,或拆出特定工作。Octane 改善了請求生命週期,無法保證整個服務追平某個 Node.js 或 Go 實作。

同樣 2,000 RPS,為什麼 CPU、RAM 還是能差很多?

RPS 是每秒到達或完成的請求數,不是同時在處理的請求數。還需要平均耗時、每請求 CPU 時間與排隊情況,才能解釋資源用量。

CPU 看的是每個請求實際用了多少運算時間

下面是純假設算例,沒有指定任何語言屬於哪一列:

1
2
3
4
流量:每秒 2,000 個成功請求

每請求 CPU 0.25 ms:2,000 × 0.00025 = 0.5 個 CPU 核心
每請求 CPU 1.00 ms:2,000 × 0.00100 = 2.0 個 CPU 核心

這是平均 CPU 需求的概念估算,還需要留下尖峰、背景工作與延遲餘裕。若兩組完整服務量出的 CPU 時間真的差四倍,才有依據說「這兩個實作在這個工作負載下差四倍」;不能將倍率套用到所有使用相同語言的專案。

等待則要另外看。假設某個請求依序等待 DB 40 ms、使用 CPU 1 ms,把運算部分加速兩倍,總時間也只從 41 ms 變成 40.5 ms。此時慢 SQL、查詢次數或連線池,通常更值得先處理。

記憶體要看同時保留多少份請求狀態

在穩定狀態下,可以用 Little’s Law 的關係估算平均在途請求數:平均在途數約等於吞吐量乘以平均耗時。假設持續完成 2,000 RPS、平均耗時 50 ms,系統內平均約有 100 個請求;同樣流量若變成 200 ms,就約有 400 個。這裡的時間使用平均值,不能拿 P95 代入。JEP 444 對併發與吞吐的說明

Go、C#、Java 的合適非同步/輕量執行緒模型,以及 Node.js 的非同步 I/O,能降低「每個等待請求都綁住一個完整 OS 執行緒或程序」的需求,但仍須保留請求資料。FPM 則需要考慮其中有多少請求正在占用 worker,有多少已在入口排隊。

FPM 的 RAM 可以先用這個拆法建立預算,再量測校正:

1
2
3
4
5
6
7
8
9
FPM 服務記憶體 ≈
worker 數 × 每個 worker 的私有記憶體
+ 共享區,例如 OPcache
+ master/HTTP 入口與其他開銷
+ 瞬間尖峰餘裕

假設每個 worker 的私有記憶體為 30 MiB:
8 個 worker:240 MiB,再加共享區與其他開銷
32 個 worker:960 MiB,再加共享區與其他開銷

這裡的 30 MiB 是計算用假設,必須換成自己的量測值。直接加總各程序 RSS 可能重複計算共享頁面;比較時應以相同定義的整體容器用量為主,再搭配私有記憶體或比例分攤的 PSS 找原因。Linux RSS/PSS 說明

連線數也要用整個服務副本的總量比較。四個 Node.js 程序各開 20 條 DB 連線,與單一 Go 程序共用 20 條,已不是相同條件。FPM 的持久連線可能分散於不同 worker,也要一起納入預算。

啟動、Docker 映像與維運,怎麼放進同一張比較?

對持續執行的 API,啟動主要影響部署、重新啟動與擴容。要分開記錄「程序開始監聽」、「readiness 通過」、「首次真實查詢完成」及「暖機後穩定達標」;空框架的啟動時間無法直接代表含 ORM 與 Redis 的服務。

堆疊 啟動要觀察的工作 最終映像的主要組成 維運優先監控
Go 設定載入、連線與應用初始化;通常沒有 JIT 階段 執行檔、憑證與所需系統函式庫;有 cgo 時需處理相依性 goroutine、配置速率、GC、timeout 與 pool 等待
C#/JIT runtime、DI、ORM 模型與首次執行的 JIT runtime/自包含產物、應用與套件;AOT 另列 GC、ThreadPool、配置量與查詢
Java/JVM JVM、Spring context、Hibernate 與 JIT 暖機 JRE/runtime image、JAR 與相依套件;Native Image 另列 heap/native memory、GC、執行緒與 pool
Node.js 模組載入、框架/ORM 初始化與 V8 暖機 Node runtime、正式環境相依套件與建置產物 event loop lag、RSS、worker 與 pool
PHP-FPM FPM 啟動、worker 準備、OPcache 及首次 Laravel 請求 PHP runtime、擴充、vendor 與 HTTP 入口;可能分容器 active worker、listen queue、slowlog 與 OPcache
PHP Octane server 與 Laravel worker 初始化 PHP/server、擴充、vendor 與應用 worker RAM 趨勢、回收、重載與狀態殘留

Docker 映像大小很受封裝方式影響。Go 用最小 runtime 映像,Node.js 卻保留整個建置工具鏈,得到的差距就混入了建置策略。所有組別都應使用正式環境產物與 multi-stage build,並統一比較 registry 壓縮傳輸量或本機解壓後大小。Docker multi-stage build

PHP 若拆成 Nginx 與 FPM 兩個容器,報告服務資源時要合計;其他組別若搭配 sidecar 也採同一規則。映像小可以減少傳輸與儲存,但映像大小和執行中的 RAM 是兩個指標。

回到這個 CRUD 專案,我會怎麼選?

選型可以先從團隊和工作負載篩出兩個候選,再用共同條件比較,通常比一開始替所有語言排總名次更有用。

專案最在意的事情 我的評估起點 決策依據
很多邊界清楚的小服務,希望減少固定記憶體開銷 Go 量測每副本基礎用量、負載增量與部署複雜度
訂單、權限、交易規則複雜,而且服務持續運作 C# 或 Java 商業框架、型別工具與團隊能力;另驗證暖機後 CPU 與延遲
前後端都用 TypeScript,API 以整合與 I/O 等待為主 Node.js 開發協作效率,以及事件迴圈有沒有被同步運算占住
需要快速完成後台、管理功能與商業流程 Laravel 現有生態與交付速度;按量測決定 FPM 或 Octane
API 主要時間花在 SQL 或外部服務 先沿用熟悉的堆疊 先修查詢、pool、timeout 與快取,再判斷語言是否構成瓶頸
確認有大量 CPU 業務運算 比較 Go、C#、Java,並評估獨立 worker 以相同演算法與資料量量測;Node/PHP 也需計入 native library 或運算拆分方案

實際驗證只需要先回答四個問題:

  1. **固定資源時誰達標?**例如先統一 1 CPU/1Gi,逐步增加流量,比較成功 RPS、P95/P99 與錯誤率;多核心能力另外用相同較高 CPU 配額比較。
  2. **固定流量時誰用得少?**在所有候選都能達標的流量下,記錄每請求 CPU 時間與相同定義的容器記憶體。
  3. **差距出現在哪條路徑?**分開看快取命中、DB 查詢、寫入交易與 JSON 處理,避免用純 JSON 成績代表完整 CRUD。
  4. **持續執行是否穩定?**觀察 GC、pool 等待、FPM queue、event loop lag 與記憶體趨勢,並檢查部署後的恢復速度。

公開 benchmark 可以幫忙理解候選框架,但也要先對齊測試類型。TechEmpower 區分 JSON、查詢、更新等不同工作,單一 endpoint 的排名不能直接換算成這套會員與訂單系統的資源用量。TechEmpower 測試定義

總結

對同一套 CRUD 微服務,**Go 的評估重點是低固定開銷與服務密度;C#、Java 是持續吞吐和商業框架;Node.js 是 I/O 併發與 TypeScript 協作;Laravel 是交付效率,而 FPM/Octane 決定了不同的容量與維運方式。**實際差距要回到每個請求的運算量、等待時間與保留狀態來解釋。

如果目標是決定容器要配多少 CPU、RAM,最有價值的結果會長這樣:「同一套查詢、快取與商業規則,在指定流量和延遲目標下,這個實作需要多少資源。」有了這個結果,再決定調整配額、worker、查詢或語言,才有明確依據。

延伸閱讀:從 PHP、Go、Node.js 轉學 C# 的概念對照。

封面為 AI 生成的概念插畫。