2026年3月14日 星期六

這麼多年後再遇到laravel+swoole,這次我成功建起來了嗎?

想不到洨編主題轉換得這麼快吧,昨天還在那邊前端golang今天就在跟你php laravel + swoole,附上兩份同一間公司給我的JD


GO
1.Go 開發經驗 4 年以上,熟悉 goroutine / channel / context
2.熟悉 Go Web Framework(Gin 優先,Echo、Fiber其次),有 gRPC / protobuf / gRPC-Gateway 經驗加分
3.熟悉 sync package(Mutex / WaitGroup 等)
4.熟悉 Redis / Cache 機制,具 Message Queue 使用經驗(Kafka / RabbitMQ / NATS 任一)
5.具備高併發、高流量場景的系統設計架構經驗
6.加分條件: 有 Laravel 開發及維護經驗
7.加分條件: 有使用過 context/error handling/interface 設計/dependency injection概念,能針對併發控制與效能優化做處理

PHP
1.熟悉 Laravel(實務經驗 2–3 年以上)
2.熟 原生PHP 7 語法與除錯能力
3.具備 Blade / HTML / CSS 基本前端開發修改能力
4.熟 Linux 指令操作(log、排錯、部署)
5.熟 Git(branch/merge/conflict 解決)
6.有 Workerman/Swoole 使用經驗佳

不是7:6的問題,是php我幾乎全部條件都符合,golang我才剛開始,大概只有一半把握,不得不低頭啊

依然是最熟悉的AI學習,沒想到現在出了好多伺服器套件swoole不是唯一選擇,甚至官方都不是預設推薦swoole,是推薦一套叫做RoadRunner,原因是安裝門檻較低。

很久沒碰PHP的我一定不知道差別,所以AI回答如下,各位自行參考

1. 技術特性深度對照

維度Swoole (PHP 擴充)RoadRunner (Go 伺服器)
本質C++ 撰寫的 PHP 擴充功能Go 語言撰寫的 高效能應用伺服器
運作方式修改 PHP 執行行為,直接在 PHP 進程內處理異步、協程。作為獨立的二進制執行檔,透過 RPC (Goridge) 與 PHP 通訊。
安裝門檻較高。需透過 pecl 編譯,常因系統環境(如 Docker、M1/M2 晶片)缺漏共用庫而失敗。極低。只需下載一個編譯好的二進制檔(.exe 或專屬檔案)即可運作。
穩定性效能極致,但若發生 Segment Fault 可能導致整個 PHP 進程崩潰。極高。進程隔離做得更好,即使單個 PHP Worker 壞掉,伺服器主體依然穩固。
功能範疇功能強大,包含協程、定時器、自定義 TCP/UDP 伺服器等。專注於高效能 HTTP 服務,並提供外掛式的功能(如 Queue、KV 儲存)。

2. 我該如何選擇?(推薦指南)

這兩者沒有絕對的高下,只有「適不適合」您的開發環境與團隊背景:

### 推薦選擇 Swoole 的情境:

  • 追求極致效能:如果您需要微秒級的反應速度,Swoole 減少了 RPC 通訊開銷,表現通常更勝一籌。

  • 需要進階異步功能:如果您要在專案中使用 Swoole Table(記憶體共享)、協程(Coroutines) 或底層通訊協定。

  • 已有成熟運維團隊:能搞定 Docker 映像檔編譯與 CI/CD 環境配置的團隊。

### 推薦選擇 RoadRunner 的情境(我的首選建議):

  • 新手或快速開發:安裝過程極度簡單(composer require 後跑個指令就裝好了),不需要處理複雜的編譯問題。

  • 追求系統穩定性:由於它是用 Go 撰寫的獨立伺服器,對 PHP 的侵入性較低,發生致命錯誤(Fatal Error)時的復原機制較為強健。

  • 容器化友善:在 Docker 環境中部署非常輕鬆,不需要為了安裝 PHP 擴充而增加映像檔體積或建構時間。


3. 專家總結

對於大多數的 Laravel 開發者,我更推薦從 RoadRunner 開始

原因在於它的開發者體驗 (DX) 優異,且效能表現已經足以應付 90% 以上的高併發場景。除非您的應用有特殊的協程需求,或是在極限壓力測試下發現 RPC 通訊成為瓶頸,才需要考慮切換到配置較為複雜的 Swoole。

事實上我也沒得選,畢竟是公司自己選,但我也沒想到laravel更新到現在12版,竟然連套件都有了,甚至都寫在官方網站說明頁,照著指令

#因為洨編是mac,所以以下步驟皆為mac的指令,先確保你有php
brew update
brew install php

#安裝composer或更新,可以上官網
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php -r "if (hash_file('sha384', 'composer-setup.php') === 'c8b085408188070d5f52bcfe4ecfbee5f727afa458b2573b8eaaf77b3419b0bf2768dc67c86944da1544f06fa544fd47') { echo 'Installer verified'.PHP_EOL; } else { echo 'Installer corrupt'.PHP_EOL; unlink('composer-setup.php'); exit(1); }"
php composer-setup.php
php -r "unlink('composer-setup.php');"
sudo mv composer.phar /usr/local/bin/composer

#安裝laravel指令
composer global require laravel/installer

#要把 composer 放到環境變數,你才能使用laravel指令
echo export PATH="$PATH:$HOME/.composer/vendor/bin" >> ~/.zshrc
source ~/.zshrc

#接著透過指令建立新專案
laravel new app

#在新專案中安裝octant
composer require laravel/octane

#手動安裝swoole
prel install swoole

#開啟swoole擴充,php.ini路徑可透過以下路徑尋找
php --ini

#將swoole擴充加入php.ini(路徑請改成自己的)
echo extension="swoole.so" >> /usr/local/etc/php/8.5/php.ini

#檢查swoole是否安裝成功,若畫面上出現 swoole 字樣,代表安裝成功。
php -m | grep swoole

基本上就可以裝起來,是不是看了就覺得很麻煩。再裝不起來可以請AI代勞,洨邊就經歷composer$PATH 沒設定,perl install swoole手動裝裝不好,php.ini沒有開啟swoole套件等細節,官方也沒說,AI幫我找到的,總之在AI幫助下花了點時間還是裝好了,跟當年沒有AI比起來輕鬆不只一點半點。但跟RoadRunner比起來,真的是挺麻煩了,據說只要跑composer就可以弄好!!不管了

然後最讓我驚艷的是,他連前端都整合好了,只要在laravel new app時選擇一下,連前端都幫忙建制起來,甚至還有ssr

最終我們裝好swoole跟octane套件後,把composer.json > scripts > dev & ssr: php artisan start 改成 php artisan octane:start就好,其他文字不要動,就可以直接composer run dev跑起來了(composer run dev......不能說跟前端一模一樣,簡直87%像)

#改完如下,其他都不用動喔,只要改關鍵字
"dev": [
"Composer\\Config::disableProcessTimeout",
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan octane:start\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
],
#如果你要檔案修改,伺服器會自動重啟需要
#安裝 chokidar:這是 Laravel Octane 監控檔案變化所需的 Node.js 套件。
#更新 composer.json:我已經在 dev 腳本中的 octane:start 指令加入了 --watch 參數
"dev": [
"Composer\\Config::disableProcessTimeout",
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan octane:start --watch\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
],
# 或是你不一定要ctrl+c,也可以下
php artisan octane:reload



# 正式環境啟動指令
php artisan octane:start --server=swoole --host=0.0.0.0 --port=8000 --workers=auto --max-requests=10000
  • --workers=auto: 根據 CPU 核心數自動分配處理程序。
  • --max-requests=10000: 處理一萬個請求後重啟 Worker,避免 PHP 記憶體洩漏 (Memory Leak)。
# AI是建議用supervisor寫個腳本,因為我是mac,或是你想用PM2應該都可以吧

最終來科普一下有沒有swoole到底有什麼區別

內容有點多,大家想要完整理解,可以直接點這裡,下面是我整理適合PO出來的內容


Q:Workerman/Swoole這兩個東西做什麼用的,然後告訴我跟laravel要如何使用

A:1. Workerman 與 Swoole 是做什麼用的?

傳統的 PHP 運作模式(PHP-FPM)像是一間「吃完就走」的快餐店:

  • 傳統 PHP-FPM: 每個請求進來,PHP 都要重新加載所有檔案、初始化環境,處理完就結束並釋放資源。這在處理 WebSocket(長連接)高頻率請求 時非常低效。

Workerman 與 Swoole 的核心價值: 它們讓 PHP 變成 「常駐記憶體」 的服務。

  • Workerman: 純 PHP 開發的非同步事件驅動框架,相容性高,適合處理 WebSocket、即時通訊。

  • Swoole: 以 C 語言寫成的 PHP 擴展(Extension),效能極高。它提供了協程(Coroutine)能力,能像 Go 語言一樣處理萬級以上的併發。

原本 Laravel 是跑在 PHP-FPM 上,但因為 Laravel 框架啟動(Bootstrapping)時會加載非常多類別,效能會受限。

兩者的結合點: 透過 Workerman 或 Swoole 作為 Laravel 的 「伺服器容器」,讓 Laravel 啟動後常駐在記憶體中。

  • 優點: 請求進來時不需要重新加載核心檔案,效能通常能提升 5~10 倍。

  • 痛點: 必須非常小心「記憶體洩漏(Memory Leak)」和「全域變數污染」,因為變數在請求結束後不會被銷毀。

為何要這樣並存?

  • Swoole: 雖然 Swoole 也能做 WebSocket,但 Laravel Octane 的設計初衷是為了優化 HTTP 效能。若你的專案對 WebSocket 有高度自定義需求(例如特殊的心跳包、TCP 協定解析),Workerman 的純 PHP 環境在除錯與開發上非常靈活。

注意事項:資料庫連接池

  • 痛點: 由於兩者都是「常駐記憶體」,資料庫連線可能會因為逾時而斷開。

  • 解法: 在 Laravel 指令中使用 Workerman 時,需確保資料庫連線有自動重連機制。Laravel 的 DB facade 通常會處理掉大部分情況,但在常駐程序中,最好在 onMessage 裡面執行 DB::reconnect() 以防萬一。

記憶體管理

  • 守則: 嚴禁在這些服務中使用 static 變數來儲存請求相關的資料。

  • 原因: 因為請求結束後變數不會消失,下一個用戶進來會看到上一個用戶的資料(發生污染)。


Q:何謂常駐記憶體
A:

傳統模式 (PHP-FPM):請求結束,煙消雲散

在傳統 Nginx + PHP-FPM 環境下,每一個 HTTP 請求都是獨立的:

  1. 載入: 啟動 PHP 行程,加載 vendor/autoload.php、載入所有 Laravel 核心檔案(Service Providers, Routes...)。

  2. 執行: 處理業務邏輯。

  3. 釋放: 請求結束後,銷毀所有變數、關閉資料庫連線,並清空記憶體

缺點: 每次請求都要重複加載框架,高併發下 CPU 損耗極大。

常駐模式 (Swoole / Workerman):一次啟動,持久運作

PHP 行程啟動後會「常駐」在記憶體中:

  1. 載入: 僅在服務啟動時載入一次框架核心與業務代碼。

  2. 執行: 請求進來時,直接跳到邏輯處理部分,執行完畢後不銷毀行程。

  3. 保持: 等待下一個請求。

優點: 省去了框架初始化的時間,效能提升 5-10 倍。

如果面試官問:「既然這麼快,為什麼不全部都用常駐模式?」你必須提到以下風險:

  1. 記憶體洩漏 (Memory Leak): 如果你在常駐程序中不斷往一個全域數組 (static $data = []) 塞東西卻沒清空,記憶體會被吃光,導致 OOM (Out of Memory)。

  2. 變數污染: 如果沒寫好,A 用戶的登入資訊可能會殘留在全域變數中,被 B 用戶讀取到。

  3. 程式碼熱更新 (Hot Reload): 在 FPM 下,改完代碼存檔即生效;在常駐模式下,你必須重啟服務(或監控檔案更動自動重啟),因為舊的代碼已經編譯並載入記憶體了。


Q:那使用swoole是不是就不用nginx + php-fpm了
A:雖然 Swoole 可以直接對外提供服務,但我們通常會在前面擋一層 Nginx(作為 反向代理 Reverse Proxy),原因如下:
  • 靜態資源處理:Nginx 處理圖片、CSS、JS 等靜態檔案的效能與穩定性遠高於 PHP。讓 Swoole 專心處理動態邏輯(API / Blade)。

  • 負載平衡 (Load Balancing):如果你有多個 Swoole 實例跑在不同伺服器或 Port,Nginx 可以幫你平均分配流量。

  • 安全性 (SSL/TLS):在 Nginx 層級處理 HTTPS 憑證配置非常簡單且成熟,不需要在 PHP 代碼中處理加密。

  • 緩衝區與穩定性:Nginx 可以處理大量慢速連線(Slow Clients),保護後端的 Swoole 不會因為網路波動而被佔滿資源。


Q:在 Laravel 中如何應用協程?
A:
use Laravel\Octane\Facades\Octane;

// 同時執行兩個耗時任務
[$users, $orders] = Octane::concurrently([
    fn () => User::all(),         // 任務 A
    fn () => Order::recent(),     // 任務 B
]);

// 總耗時 = 兩者之中最慢的那個任務,而不是相加。

面試必問:使用協程的禁忌

這部分是面試官用來確認你是否真的「實戰過」的考題:

  1. 不能使用傳統的阻塞函數: 在協程中,嚴禁使用 sleep()curlmysqli 的傳統函數。因為它們會把整個行程鎖死,導致協程失效。

    • 對策:使用 Swoole 提供的協程版本,或開啟一鍵協程化 (Coroutine::set(['hook_flags' => SWOOLE_HOOK_ALL]))。

  2. 全域變數是危險的: 在協程切換過程中,如果你使用 $_GET 或 Laravel 的單例(Singleton)來存個人資料,可能會發生「資料穿透」,讓 A 使用者看到 B 使用者的訂單。

  3. 資料庫連線池 (Connection Pool): 協程並發量很高,如果每個協程都開一個連線,資料庫會瞬間爆掉。

    • 對策:必須配置 連線池,讓協程複用現有的連線。


面試應對總結

當被問到協程時,你的標準回答架構:

  • 定義:協程是輕量級的線程(User-level thread),由開發者在應用層控制切換。

  • 優勢:在處理 I/O 密集型任務(如 API 呼叫、資料庫查詢)時,能極大化利用 CPU。

  • 實踐:在 Laravel 中透過 Octane 與 Swoole 配合,並注意「一鍵協程化」的配置。


Q: 一鍵協程化是什麼,有什麼作用,傳統的阻塞函數有哪些,為什麼會卡住,個別替代方案是什麼? 
A:

1. 什麼是「一鍵協程化」?

在 Swoole 早期,如果你想讓代碼變成非同步,你必須把所有的 PHP 內建函式改寫成 Swoole 專用的 API(例如用 Swoole\Coroutine\Http\Client 取代 curl)。這對既有的 Laravel 專案來說是巨大的災難,因為你不可能改動框架底層。

「一鍵協程化」 (One-click Coroutine Runtime Hook) 的出現解決了這個問題: 它會在底層 劫持 (Hook) 那些傳統的阻塞函數,將它們自動轉化為非同步。

它的作用:

  • 無痛升級:你不需要改動任何 Laravel 的代碼,原本的 DB::query()Http::get() 就會自動具備協程能力。

  • 開發效率:工程師可以繼續沿用 PHP 的同步寫法,但享受非同步的效能。

2. 傳統的阻塞函數有哪些?為什麼會卡住?

為什麼會卡住?

傳統 PHP (FPM) 是 同步阻塞 (Synchronous Blocking) 的。當執行到一個網路請求時,PHP 進程會發出一個系統調用 (System Call),然後 CPU 就會在那裡 乾等 (Wait)

在等待期間,這個進程什麼事都不能做,這就是「卡住」。

常見阻塞函數與替代方案:

類別傳統阻塞函數 (不可直接用)為什麼卡住協程化後的替代 (或自動 Hook 後)
網路請求curl_*, file_get_contents()等待伺服器回應時,整個進程掛起。Swoole\Coroutine\Http\Client 或 Hook 後的 curl
資料庫mysqli_*, PDO::query()等待 SQL 查詢與結果傳回。使用連線池 (Connection Pool) 與 Hook 後的 PDO
快取/儲存Redis::get() (PhpRedis)等待 Redis TCP 回應。Swoole\Coroutine\Redis 或 Hook 後的 Redis
檔案操作file_put_contents(), fopen()等待硬碟讀寫 (Disk I/O)。Swoole\Coroutine\System::readFile
定時/暫停sleep(), usleep()強制讓整個進程休眠。Swoole\Coroutine::sleep() (僅掛起當前協程,不卡進程)

3. 深度對比:sleep() vs Coroutine::sleep()

這是面試最愛考的細節,用來測試你有沒有「協程思維」。

傳統 sleep(2)

  1. 進程執行到這裡。

  2. 核心暫停這個進程 2 秒。

  3. 這 2 秒內,伺服器完全無法接收新的 Request(如果是單進程模式)。

協程 Coroutine::sleep(2)

  1. 協程 A 執行到這裡,發現要等 2 秒。

  2. Swoole 引擎說:「你先去旁邊休息,我把 CPU 讓給協程 B」。

  3. 這 2 秒內,伺服器可以繼續處理幾百個其他人的請求

  4. 2 秒後,引擎再把協程 A 叫醒繼續執行。


4. 面試必殺技:一鍵協程化不是萬能的

當面試官問到這裡,如果你能主動說出限制,你就是全場最強的 Candidate:

「雖然一鍵協程化很強大,但它也有局限性

  1. C 擴展限制:如果某個 PHP 擴展是用 C 寫的,且沒有遵循 PHP 的流 (Stream) 規範,一鍵協程化可能無法劫持它。

  2. 全域變數污染:因為代碼變成了非同步,多個請求可能在同一個進程內交錯執行。如果使用了 $_GETstatic 屬性,會發生資料錯亂。

  3. CPU 密集型任務:如果你的代碼是在做大數運算(而非 I/O 等待),協程也救不了你,因為 CPU 真的在忙,沒辦法切換。」



Q: 資料庫連線池 (Connection Pool):
協程並發量很高,如果每個協程都開一個連線,資料庫會瞬間爆掉。
對策:必須配置 連線池,讓協程複用現有的連線。
這應該怎麼配置,假設我配10個,那意思就是最多十個人同時連線,其他人會塞車嗎?


A:

1. 連線池的運作原理

連線池(Connection Pool)就像是**「公務車車隊」**:

  • 傳統模式:每個人出門都買一台新車,回報後就把車砸掉(開銷大、浪費)。

  • 連線池模式:公司只有 10 台車。有人要出門就去領鑰匙,用完把車開回停車場還給公司,讓下一個人用。

3. 回答你的核心問題:配 10 個會塞車嗎?

答案是:會,但情況跟你想的不一樣。

Q:配 10 個連線,意思就是最多 10 個人同時連線嗎?

是的。 在微秒($μs$)級別的瞬間,確實只有 10 個 SQL 能在資料庫裡執行。

Q:其他人會塞車嗎?

這就是**「協程」「傳統執行緒」**最大的差異:

  1. 排隊機制:當 10 個連線都被借走時,第 11 個請求會進入一個極短暫的等待隊列 (Channel Queue)

  2. 非阻塞等待:因為是協程,第 11 個請求在等待連線時,並不會卡死整個 PHP 進程。它只是「暫停」自己的進度,讓 CPU 先去處理其他不涉及資料庫的邏輯。

  3. 複用率極高:一個 SQL 查詢可能只需要 5ms。這意味著 1 個連線在 1 秒內可以服務 200 個請求。10 個連線在 1 秒內理論上可以處理 2,000 個查詢。

結論:比起「增加連線數」,**「縮短 SQL 執行時間」**對連線池的效能提升更顯著。


4. 面試應對:如何決定連線池的大小?

面試官可能會追問:「那我要怎麼知道該配多少個連線?」你可以這樣回答:

  1. 資料庫承受力:觀察 MySQL 的 max_connections 設定。如果資料庫上限是 100,你 5 個 PHP 節點各配 20 個就滿了。

  2. 壓測觀察

    • 如果連線數太少:請求回應時間(Latency)會變長,因為大家都在排隊等連線。

    • 如果連線數太多:資料庫的 Context Switch 增加,反而導致整體吞吐量下降。

  3. 黃金公式(參考值)

    $$連線數 = (核心數 \times 2) + 硬碟 IO 效能$$

    通常一個 API 服務,連線數設在 20~50 就能應付極大的流量。


5. ⚠️ 注意:連線洩漏 (Connection Leak)

這也是面試必考。如果你的代碼寫了 $pdo = $pool->get(); 但因為報錯(Exception)沒有執行到 $pool->put($pdo);,這個連線就永遠消失在池子裡了。

資深寫法 (Try-Finally):

$pdo = $pool->get();

try {

    // 執行邏輯

} finally {

    $pool->put($pdo); // 確保無論成功或失敗都會歸還連線

}


2026年3月13日 星期五

V8引擎運作原理與垃圾回收機制

🚀 深入 V8:從編譯原理到記憶體回收的效能實戰

今天在面試中踢到了鐵板。原本以為只要會寫 JS、會處理資料就好,沒想到面試官直接往底層挖,問我 「V8 引擎怎麼運作」 以及 「記憶體洩漏如何精確定位」

當下沒答好,回頭翻了資料才發現,這不只是理論,這跟我們在高併發情境下寫出的程式碼品質息息相關。這份紀錄將 V8 的運作機制與監控實戰整理出來,當作以後的防守指南。

一、 V8 引擎運作流程:從原始碼到機器碼

V8 引擎(Chrome 與 Node.js 的核心)不只是一個直譯器,它是一個強大的 JIT (Just-In-Time) 編譯器。

  1. 解析 (Parsing):將 JavaScript 原始碼轉化為 AST (抽象語法樹)

  2. 解釋 (Ignition):解釋器將 AST 轉為較低階的 Bytecode (位元組碼) 並立即執行。這保證了程式碼能快速啟動。

  3. 編譯優化 (TurboFan):監控程式碼運行(Profiling),如果某段函式被頻繁呼叫(熱點程式碼),編譯器會將其轉換為優化過的 機器碼 (Machine Code),效能直接噴發。

  4. 去優化 (Deoptimization):這是效能損耗的元兇。如果你原本傳入 Int,後來突然變成 String,V8 會發現之前的優化作廢,被迫拋棄機器碼,退回到位元組碼。

    💡 心法:盡量維持變數型別的穩定(Monormorphic),能幫助 V8 保持優化狀態。


二、 V8 垃圾回收機制 (Garbage Collection)

V8 將記憶體分為 新生代老生代,就像是公司的「實習生」與「資深員工」,管理制度完全不同。

1. 新生代:Scavenge 演算法

  • 特性:存活時間短、回收極其頻繁。

  • 機制:空間平分為 FromTo。新物件先放 From,回收時將還活著的物件複製到 To,然後清空 From 並將兩者角色對調(翻轉)。

  • 晉升 (Promotion):撐過兩次回收還沒死的物件,會被升遷到「老生代」。

2. 老生代:Mark-Sweep & Mark-Compact

  • 特性:存活長、空間大。

  • 機制

    • 標記 (Marking):從根節點(如 window)出發,標記所有還有人用的物件。

    • 清除 (Sweeping):把沒標記到的「垃圾」清掉。

    • 整理 (Compacting):把分散的物件推擠到一起,清出連續空間,避免「記憶體碎片」導致大物件放不進來。


三、 記憶體洩漏 (Memory Leak) 診斷實戰

當物件不再需要,卻仍被「根節點」間接引用,就會導致 GC 無法回收。在高併發系統中,這會演變成災難。

1. 常見的洩漏殺手

  • 意外的全域變數window.data = [...]

  • 被遺忘的計時器setInterval 沒清掉,裡面的閉包會一直抓著外部變數不放。

  • 脫離文件的 DOM:JS 變數存了某個按鈕,但該按鈕已經從網頁刪除了,這顆按鈕就成了「殭屍節點」。

2. 實戰定位:三點定位法

當你感覺頁面越來越卡時,請打開 Chrome DevTools:

  1. 快照 A:初始狀態。

  2. 快照 B:執行操作(例如重複開啟彈窗 10 次)。

  3. 快照 C:再次執行操作。

重點:使用 Comparison 模式比較 B 與 C。如果某個物件在 B 到 C 之間持續增加且沒消失,那 99% 就是洩漏點。


四、 程式碼優化建議:寫出對 GC 友善的 Code

  • 優先使用弱引用:如果只是想關聯資料,請用 WeakMapWeakSet,這不會阻止 GC 回收該物件。

  • 手動斷開引用:處理大型數據(如 5MB 的 JSON)後,用完記得 data = null

  • 生命週期清理:在 React 的 useEffect return 或 Vue 的 onUnmounted 中,養成習慣清除 Event Listener 和 Timer。


結語

面試官問這些,其實是在考你:「當系統在高載壓下效能下降,你有沒有科學的方法去排查?」

雖然平常寫 Code 不一定會天天看 AST,但了解 「去優化 (Deoptimization)」「記憶體碎片化」 的概念,能讓我們從寫出「會動的程式碼」,進化成寫出「高效的程式碼」。

前端高併發情境範例

🛑 前端甘我事?高併發情境下的前端防禦指南

今天面試被問到一題:「高併發你怎麼處理?

當下我心裡的 OS 是:「高併發不是後端要處理的嗎?」結果講完後對方顯然不滿意。事後回想才發現,面試官要的不是背誦技術名詞,而是情境設計能力

雖然這些東西真的遇到時,查一下文件半小時就能寫出來,但為了應付那些「愛問」的公司,我們還是得把這套「前端流量削峰術」內化成自己的邏輯。這份指南紀錄了前端在高併發情境下的核心任務:流量削峰壓力隔離


🎯 核心設計原則:別讓請求直達資料庫

在高併發情境下,前端的目標不再只是把畫面畫出來,而是要當一個聰明的「過濾器」,透過以下原則防止後端崩潰:

  1. 動靜分離:不常變動的 HTML/JS/CSS 與素材(圖片、影片)通通丟給 CDN,別讓這些請求去佔用應用伺服器的頻寬。

  2. 請求削峰:利用緩衝、延遲或合併請求,把尖峰流量「平滑化」。

  3. 客戶端降級:系統快不行時,主動關掉非核心功能(例如:自動補完搜尋、即時進度更新)。


🛠 三大代表性情境範例

1. 電商秒殺/搶購系統 (Rush Sale)

挑戰:數百萬用戶在同一秒瘋狂點擊按鈕。

  • 按鈕防抖與置灰:點擊後立即禁用按鈕,防止使用者因為焦慮而產生重複請求。

  • 靜態資源預熱:活動開始前,利用 Service Worker 預先下載好活動頁所需的資源。

  • 排隊等待機制:發送請求後進入輪詢或 WebSocket 狀態,顯示「排隊中」而非直接報錯,安撫用戶情緒。

  • 驗證碼與異步化:透過驗證碼強制拉長操作路徑,人為分散瞬間的請求壓力。

2. 大數據流與無限捲動 (Infinite Scroll)

挑戰:快速滑動產生大量 API 請求,且大量 DOM 節點導致瀏覽器卡頓。

  • 虛擬列表 (Virtual List):只渲染可見視窗內的節點。即使有 10 萬筆資料,DOM 永遠只維持 10-20 個,保證瀏覽器不崩潰。

  • 分頁請求與預加載:設定滑動閾值,在快滑到底前才拿下一頁。

  • 請求取消 (AbortController):當用戶切換分類或滑太快時,果斷取消過時的舊請求。

3. 直播間海量彈幕 (Live Streaming Chat)

挑戰:數萬人同時發言,頻繁的 DOM 更新與網路通訊會讓頁面直接當掉。

  • 彈幕池緩衝 (Buffer):不要收到一則就畫一次。建立緩衝池,每 500ms 批量渲染一次。

  • 頻率限制與過濾:前端根據等級篩選訊息,或限制本地發言頻率。

  • Canvas 渲染:針對海量彈幕,放棄 DOM,改用 Canvas 繪製以追求更高的幀數(FPS)。


💡 技術手段總結清單

技術手段解決的問題應用範例
CDN / Edge減少伺服器頻寬壓力全域靜態檔案分發
Service Worker離線快取與預加載秒殺頁面預熱
Debounce / Throttle過度頻繁的觸發搜尋框、視窗縮放
Request Batching減少 HTTP 請求數量點讚數合併計算

📝 結語

繞了一圈才發現,前端在高併發裡的角色,說穿了就是:「想盡辦法防止伺服器爆炸」

透過在前端攔截無效請求、合併必要請求、優化渲染效率,我們可以把原本會壓垮後端的「海嘯」,變成一波波平緩的「浪潮」。早說嗎,原來前端也要當保全啊!==


希望這份紀錄對下次面試有幫助。如果你有遇過更奇葩的情境,歡迎跟我分享!

2026年3月12日 星期四

Golang, gRPC(protobuf)學習筆記

這似乎也是一們必修的課,不然json傳來傳去不香嗎


首先要建立資料型態 p5_grpc/simple/pb/simple.proto,為了辨識這副檔名,我安裝了vscode-proto3套件

Greeter名字可以自取,但在使用時例如在之後的server: pb.Unimplemented[自取姓名]Server , pb.Register[自取姓名]Server就會變成這樣

剩下就像interface定義輸入輸出模型

// 指定使用最新的 protobuf 第三版語法
syntax = "proto3";

// 定義 protobuf 內部的包裝名稱,避免跟別人寫的 message 命名衝突
package simple;

// 關鍵設定:告訴編譯器,生成的 Go 程式碼要放在哪一個 Go package 中
// 這裡我們指定生成到專案中 p5_grpc/simple/pb 這個 package
option go_package = "p5_grpc/simple/pb";

// 定義一個名為 Greeter 的「服務」(Service),就像是定一個 API 介面
service Greeter {
// 定義一個名為 SayHello 的 API 方法
// 它的輸入是 HelloRequest,輸出是 HelloReply
rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// 定義客戶端傳給伺服器的資料結構 (請求)
message HelloRequest {
// 第一個欄位是字串型態,名稱為 name,數字 "1" 是它在二進位傳輸時的順序標籤
string name = 1;
}

// 定義伺服器回傳給客戶端的資料結構 (回應)
message HelloReply {
// 同樣地,定義一個字串欄位 message,順序標籤為 1
string message = 1;
}

在proto設定完成後還要輸入一段指令好產生simple.pb.go和simple_grpc.pb.go,千萬別傻傻的自己打,然後protoc指令不知道為什麼電腦理本來就有,不用特別裝什麼,改天沒有在研究吧


# 確保你已經有安裝 protoc 與 go 的 protoc 外掛
# 實際生成指令如下:
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
simple/pb/simple.proto

接著要來建立server: p5_grpc/simple/server/main.go

package main

import (
"context"
"log"
"net"

// 匯入上一步工具幫我們自動產生的 pb package
pb "p5_grpc/simple/pb"

"google.golang.org/grpc"
)

// 定義一個我們自己的 sever 結構體
type server struct {
// 必須嵌入這個,這是 gRPC 為了向前相容的強制規定
pb.UnimplementedGreeterServer
}

// 正式實作 proto 中定義的 SayHello 方法!
// 注意:函數名稱和傳入傳出的參數型別,都必須跟自動生成的 pb 程式碼一致
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
// in.GetName() 可以安全地拿到客戶端傳來的 name 欄位
log.Printf("收到客戶端的請求名稱: %v", in.GetName())

// 把 "Hello " 加上客戶端傳來的名字,包裝成 HelloReply 回傳給客戶端
return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}

func main() {
// 1. 在本地端的 50052 port 開啟一個 TCP 監聽器
lis, err := net.Listen("tcp", ":50052")
if err != nil {
log.Fatalf("無法開啟監聽: %v", err)
}

// 2. 建立一台全新的 gRPC 伺服器
s := grpc.NewServer()

// 3. 把我們自己寫的 server 邏輯結構體,註冊到 gRPC 伺服器上
pb.RegisterGreeterServer(s, &server{})
log.Printf("簡單版 gRPC Server 正在監聽: %v", lis.Addr())

// 4. 開始伺服器以接受客戶端連線 (程式會卡在這裡持續運行)
if err := s.Serve(lis); err != nil {
log.Fatalf("伺服器啟動失敗: %v", err)
}
}

最後再來個client就搞定: p5_grpc/simple/client/main.go

package main

import (
"context"
"log"
"time"

// 同樣地,匯入自動產生的 pb package
pb "p5_grpc/simple/pb"

"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)

func main() {
// 1. 設定並連接到目標伺服器的位置 (localhost:50052)
// grpc.WithTransportCredentials(insecure.NewCredentials()) 代表我們不使用 SSL/TLS 加密連線 (僅供本地測試用)
conn, err := grpc.Dial("localhost:50052", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("無法連線至伺服器: %v", err)
}
// 確保程式結束前會關閉連線釋放資源
defer conn.Close()

// 2. 透過連線建立一個存取 Greeter 服務的客戶端實例
c := pb.NewGreeterClient(conn)

// 3. 建立一個有 1 秒超時限制的 Context
// 這是為了避免連線卡住,如果在 1 秒內沒完成就會自動取消
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()

log.Println("準備呼叫 SayHello 方法...")

// 4. 透過客戶端實例呼叫遠端的 SayHello 方法,並把自己的名字 (Gopher) 當作參數傳過去
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: "Gopher"})
if err != nil {
log.Fatalf("呼叫失敗: %v", err)
}

// 5. 成功收到回應後,印出伺服器回傳的內容 (Message 欄位)
log.Printf("收到伺服器回傳: %s", r.GetMessage())
}

最後就是相繼把server, client啟動,就會看到Hello Gopher。大概就是這樣吧,明天再來研究最後一堂課MQ