工程實作 15 min read

韓國線上寺廟산사為什麼擠爆伺服器?我仿做 3D 禪堂壓測 2000 人,延遲從 4.2 秒降到 0.45 秒

산사是韓國個人開發者 10 月 28 日上線的 3D 線上寺廟,大家化身小沙彌坐在法堂裡合掌、叩拜,不能聊天,開站一天就湧進數萬人擠爆伺服器。我照它的 Three.js + WebSocket 架構仿做一間壓測 2000 人:全擠一間 p95 延遲 4.2 秒、分成 20 間只要 0.45 秒——同房廣播量跟人數平方成正比,分房才擋得住。

韓國線上寺廟산사為什麼擠爆伺服器?我仿做 3D 禪堂壓測 2000 人,延遲從 4.2 秒降到 0.45 秒
本文目錄 · 8
懶人包
  • 韓國個人開發者 10 月 28 日上線的線上寺廟「산사」,讓大家化身小沙彌在 3D 法堂裡合掌、叩拜、轉念珠,不能聊天。開站一天湧進數萬人(各家報導 5 萬到 10 萬),伺服器被擠爆。
  • 座位從 20 個一路加到 1 萬個,最後改成 100 間、每間 100 人才撐住。
  • 我讀了它的前端 bundle:Vite + Three.js 畫 3D,即時同步走 Cloudflare Workers 上的 WebSocket,訊息只有移動、坐下、叩拜、念珠這幾種。
  • 我照這個架構做了繁中版「線上禪堂」,上線在 html.yanchen.app/zen-hall,兩個分頁同房互看、合掌會通知對方,E2E 通過。
  • 同一台電腦壓測 2000 人:全擠一間 p95 延遲 4,164 ms,分成 20 間只要 451 ms。同一房的廣播量跟人數平方成正比,分房是讓它有上限的做法。

這是 Threads 上 @dajak.dev 的一則貼文,內容是韓國最近爆紅的線上寺廟:太多人跑去線上拜佛,連伺服器都被擠爆,作者笑到打了一串ㅋㅋㅋㅋ。配的影片是一整片小沙彌坐在佛像前。

笑完之後我好奇的是工程那一半:一個不能聊天、只有大家坐著的網站,為什麼會擠爆?所以我讀了原站前端,照同樣的架構仿做一間,再用壓測把「擠爆」重現出來。

산사 是什麼?為什麼在韓國爆紅?

산사(山寺)網頁標題是「산사 — 온라인 절」,描述寫著「말없이 같이 앉아 있는 온라인 절」,意思是「不說話、一起坐著的線上寺廟」。進去會變成一個 Q 版小沙彌,可以走到蒲團坐下、合掌、叩拜、轉念珠、聽心經。沒有聊天室。

項目內容來源
開發者個人開發者「메타몽」(Metamong)農民新聞
上線2026 年 10 月 28 日HuffPost Korea
人潮約 1.2 萬人時伺服器出狀況,當晚一天破 10 萬人農民新聞,引開發者 Threads
人潮(另一說)公開一天湧入 5 萬人,伺服器癱瘓HuffPost Korea
容量演進20 席 → 100 → 1,200 → 1 萬席,最後是 100 個頻道、每個 100 人農民新聞、HuffPost Korea
人數各家說法不同,我只能確定是「一天數萬人」這個量級。

為什麼紅?HuffPost Korea 把它放在「戰爭、AI、氣候災難帶來不確定感,人們想找個地方讓心休息一下」的脈絡裡,同一篇也引了 2025 年韓國社會調查:13 歲以上有 38.2% 的人平常會感到孤獨。這個數字是背景,報導沒有直接說它造成산사爆紅。

它的設計本身就說明了爽點:有人陪,但不用社交。同一間房最多坐 100 個陌生人,你能做的只有向對方合掌、叩拜。原貼文作者的解讀比較直接:「韓國人火氣真的太大了,需要找個地方消消氣。」

韓國同一週也有人做了線上聖堂、線上教會(아시아투데이)。線上拜佛在韓國也不是新東西,2000 年代後期就有「사이버 법당」(網路法堂)。

산사 用什麼技術做的?

我把原站的 HTML 和 JS bundle 抓下來讀。以下是 bundle 裡看得到的東西:

層用什麼bundle 裡的證據
建置Vite/assets/index-<hash>.js 加 modulepreload 的打包格式
3DThree.jsGLTFLoader 載入 monk.glb、buddha.glb;大量角色用 InstancedMesh
遠端角色低模版本另有 monk-remote-lod.glb
即時同步Cloudflare Workers 上的 WebSocket連線網址是 wss://sansa-realtime.<帳號>.workers.dev/presence
備援Supabase Realtime只在沒設定 Worker 網址時才用
音訊心經音檔heart-sutra-v1.mp3
同步的訊息種類很少:hello、move{x, z, yaw, pose}、seat、stand、bow、bead、room-switch、ack、sync。沒有文字訊息,因為不能聊天。

後端是不是用 Durable Objects 做分房,前端看不出來,我不下結論。

我做的繁中版:線上禪堂

我照這個架構做了一個單檔版本,叫「線上禪堂」,已經放在 html.yanchen.app/zen-hall。

線上禪堂 3D 畫面:紅色柱子的法堂中央是金色佛像,前方蒲團坐滿小沙彌,底部是合掌、叩拜、念珠、念經、起身五個按鈕
功能做法
3D 場景Three.js 0.169(importmap 從 jsDelivr 載入),法堂、佛像、35 個蒲團、Q 版小沙彌都用基本幾何體拼
操作點地板走路、點蒲團坐下、拖曳轉視角;按鈕有合掌、叩拜、念珠、念經、起身
聲音Web Audio 即時合成木魚和頌缽,不用版權音檔
同步網址加 ?ws=ws://host:8787/presence 就連真的伺服器;不加就用 BroadcastChannel,同一個瀏覽器的分頁互通,再加 26 位模擬香客
線上那個網址是靜態站,沒有接伺服器,所以看到的是「離線示範」:26 位模擬香客加上你自己。

真多人的部分我在本機驗。伺服器是 99 行的 Node.js(ws 套件),兩個分頁連同一台伺服器:

兩個分頁連到同一台 presence server,畫面左上角顯示「清蓮堂 1 號房 · 2/100 人」,畫面中有兩個小沙彌

Playwright E2E 的實際輸出:

offline room label: 清蓮堂 1 號房 · 27/100 人(離線示範)
A label: 清蓮堂 1 號房 · 2/100 人 | A sees peers: 1
B label: 清蓮堂 1 號房 · 2/100 人 | B sees peers: 1
B toast after A palms: "有人向你合掌問候 🙏"
A sees B pose: beads
errors: none
E2E PASS

第一次跑是 FAIL。WebSocket 模式下伺服器發的 id 是數字,前端拿它呼叫 startsWith 直接噴錯;離線模式的 id 是字串,所以單機測不出來。改成先轉字串才過。

線上拜佛為什麼會擠爆伺服器?

關鍵在「同一間房裡,每個人都要看到其他所有人」。

一個人走一步,伺服器要通知房裡其他 N−1 個人。N 個人都在動,伺服器每秒要送出的量就跟 N × N 成正比。人數翻倍,廣播量變四倍。

所以分房的效果不是線性的。同樣 2000 人:

配置每人要收幾人的狀態廣播量(相對值)
全擠一間2,0002000² = 400 萬
20 間 × 100 人10020 × 100² = 20 萬
理論上差 20 倍。산사的容量從 1 萬席改成「100 間 × 100 人」,就是把 N 鎖在 100。

我的伺服器還多做了一件事:不是收到一個動作就轉發一次,而是每 0.1 秒(10Hz)每房只打一包,裡面是這段時間內有動的人。這樣訊息「包數」不隨人數平方成長,但「包裡的內容」還是 N²,分房才是根本解。核心就這一段:

// 每 tick 每房只送一包:這一 tick 內有變動的人 + 事件
setInterval(() => {
  for (const room of rooms.values()) {
    if (!room.dirty.size && !room.events.length) continue;
    const packet = { type: 'tick', states: [...room.dirty.values()], events: room.events };
    for (const { ws } of room.members.values()) if (ws.readyState === 1) send(ws, packet);
    room.dirty = new Map();
    room.events = [];
  }
}, 1000 / TICK_HZ);

分房的邏輯更短:找第一間還沒滿的房,都滿了就開新的。

function pickRoom() {
  for (const [id, r] of rooms) if (r.members.size < ROOM_CAP) return id;
  return rooms.size + 1;
}

壓測:2000 人全擠一間 vs 分 20 間

壓測程式開 N 個 WebSocket 客戶端,每人每秒動 4 次,量伺服器每秒送出多少、CPU 多少,以及「我動了一下 → 伺服器回播給我」的延遲。房間上限設成 100,000 就等於全擠一間。

我跑的指令如下(server.mjs 與 loadtest.mjs 放在我本機,依賴只有 ws 套件,伺服器核心就是上一節那兩段):

npm i ws
node loadtest.mjs 2000 100 8      # 2000 人,每房 100 人,跑 8 秒
node loadtest.mjs 2000 100000 8   # 2000 人,全擠一間

實際結果(Node.js v24.11.1,Windows 11,客戶端和伺服器在同一台機器):

壓測長條圖:1000 人分房 435 ms、1000 人一間 652 ms、2000 人分房 451 ms、2000 人一間 4164 ms
人數配置房數伺服器送出伺服器 CPU延遲 p50延遲 p95收到回播次數
1000每房 100 人1018.3 MB/s41%137 ms435 ms17,824
1000全擠一間173 MB/s81%151 ms652 ms12,619
2000每房 100 人2037.9 MB/s84%167 ms451 ms36,068
2000全擠一間186 MB/s101%1,105 ms4,164 ms4,168
看兩件事:
  • 分房時延遲幾乎不動。1000 人到 2000 人,p95 從 435 ms 到 451 ms。人數加倍只是房數加倍,每個人收到的量不變。
  • 全擠一間到 2000 人就崩了。CPU 吃滿,p95 延遲 4.2 秒,在同樣 8 秒內收到的回播只剩分房時的約九分之一。玩家看到的就是「別人卡住不動、自己按了沒反應」。
數字要打折看。客戶端和伺服器搶同一顆 CPU,延遲裡也包含最多 100 ms 的 tick 等待。理論差 20 倍,實測伺服器送出量只差 2.3 倍,原因是單房配置的伺服器已經吃滿 CPU,送不出它該送的量,延遲就是這樣堆起來的。這組數字證明的是趨勢,不是正式環境的容量。

自己做一個會踩到哪些坑?

  • 線上版目前只有離線示範。要變成真的多人同房,得把 presence server 部署出去。산사的前端連的是 Cloudflare Workers,我的 server.mjs 是一般 Node.js 程序,要搬上去得改寫成 Worker 的寫法。
  • 一房上限要先定。上限決定每個人要收多少資料,也決定單房最壞情況。산사最後選 100,我的伺服器預設也是 100。
  • 每秒送幾次要先定。我用 10Hz,每人每秒動 4 次。坐著不動的人不進 dirty,所以全房都在打坐時流量很小;全部同時走動才是尖峰。
  • WebGL 環境。我用 Playwright MCP 開的瀏覽器沒有 WebGL,畫面是黑的;改用 Node 版 Playwright 加 SwiftShader 才截得到 3D 畫面。
  • 不要拿原站的素材。佛像、心經音檔、角色模型都是原作者的。我的版本全部用幾何體和 Web Audio 合成。

常見問題

為什麼不能聊天?

這是原作的設計,描述就寫「말없이 같이 앉아 있는」(不說話、一起坐著)。互動只有合掌、叩拜這類動作。

分房之後,朋友想坐同一間怎麼辦?

산사是讓使用者自己選頻道。我的伺服器在連線網址帶 ?room=3 就會進 3 號房,滿了才自動分到別間。

只靠 10Hz 批次廣播不分房夠嗎?

不夠。批次能把「包數」降下來,但每一包裡還是要放房裡所有在動的人,總資料量仍跟人數平方成正比。我的壓測就是開著批次在跑,全擠一間到 2000 人還是崩。

這組壓測數字可以當正式環境容量嗎?

不行。客戶端和伺服器跑在同一台 Windows 電腦,延遲裡也包含最多 100 ms 的 tick 等待。它能證明的是「同樣人數,分房比全擠一間穩」這個趨勢。

延伸資源

想自己玩就打開 線上禪堂,或去 산사 坐一下。要做任何「一群人同時待在同一個空間」的東西,先決定一房幾人。
author
陳
陳彥彤

AI 工程師 · AI 顧問。Java 後端 8 年、AI 工程師 2 年。AI 內訓 · AI 導入顧問 · 前後端與雲端培訓。

support

覺得文章有用可以到 GitHub 給個 star,或是透過信箱聊聊 AI 內訓、AI 導入顧問或前後端 / 雲端培訓。

related

相關文章

[工程實作] · 15min
yt-dlp「Could not copy Chrome cookie database」怎麼修?Windows Chrome 156 實測 4 種做法
Windows 上的 Chrome 和 Edge 開著時會鎖住 cookie 資料庫,yt-dlp 的 --cookies-from-browser 就會報 Could not copy Chrome cookie database。我在 Chrome 156 把四種做法各跑一次——流傳的 LockProfileCookieDatabase 旗標已經沒用,關掉瀏覽器或匯出 cookies.txt 才讀得到。
[工程實作] · 13min
企業 AI 客服的 8 層架構:只接模型 1/8、做滿 8/8,禮拜五改一行提示詞使用者看不出來
David Ooi 的 IG reel 把「公司自己的 AI」拆成 8 層:模型、提示詞、RAG、工具、權限、護欄、快取路由、可觀測性,我照著用 Claude Haiku 做了虛構家電品牌的客服 Chloe,只接模型的版本 8 題過 1 題,還主動稱讚對手。8 層全開後把提示詞改一行,5 輪重跑使用者看到的分數沒掉,護欄觸發從 8 次變 14 次——沒有可觀測性和回歸測試就看不到,而且判定式和關鍵字護欄本身也會誤判。
[工程實作] · 15min
偽 3D 賽車道路怎麼做?把 AI 海岸滑板 Reel 做成 13 KB 的網頁小遊戲
我把一支 AI 生成的海岸下坡滑板 Reel 做成能玩的網頁遊戲 Coastal Bomb,用的是 OutRun 那一代的偽 3D 道路:路切成 4,360 段,每段記彎度和高度再投影成梯形,單一 HTML 226 行、零依賴。Playwright 實測放手約 500 m 會被彎道甩出、自動駕駛 25 秒滑了 5,579 m——要做第一人稱往前衝的小遊戲,這個技術比 three.js 輕得多。