2026年3月18日 星期三

🚀 拒絕當 CRUD 工程師!資深 PHP/Laravel 面試必考的硬核實戰筆記

 

1. PHP 7 核心特性、底層效能與語法陷阱

  • PHP 7 為什麼快?底層與核心優化:

    • ZVAL 結構重構: 變數容器從 24 降至 16 bytes,大幅降低內存占用並提升 CPU Cache 命中率。

    • AST (抽象語法樹): 引入 AST 作為編譯的中間層,讓編譯器與執行器解耦,提升編譯效率與語法解析的穩定性。

    • OPCache: 實務必開的效能神器。它會將 PHP 腳本編譯後的 Bytecode 快取在共享記憶體 (Shared Memory) 中,省去每次 Request 都要重新讀取、解析和編譯腳本的極大開銷。

  • 現代化語法與嚴謹度 (Robustness):

    • 嚴格類型 (Strict Types): 強烈建議在檔案開頭宣告 declare(strict_types=1);。這不只是為了效能,更是為確保傳入與回傳的資料型別完全一致,減少進入邏輯層前的低級錯誤(如字串 "10" 被當成整數 10 運算)。

    • Null 合併運算符 (??): 取代冗長的 isset() 三元運算,讓程式碼更優雅:$username = $_GET['user'] ?? 'Guest';

    • 箭頭函式 (Arrow Functions): PHP 7.4 引入的 fn()。它會「自動以值傳遞 (By Value)」的方式捕捉父層作用域的變數,適合用在 array_map 等單行回呼處理:$multiplied = array_map(fn($n) => $n * $multiplier, $numbers);

  • 閉包 (Closure) 的變數陷阱: 一般的匿名函式 function() use ($var) 預設是「值傳遞」。若要內外同步改變,務必加上 & 變為「引用傳遞」。

    PHP
    $count = 1;
    $func = function() use (&$count) { echo $count; }; // 加上 & 才是引用
    $count++;
    $func(); // 面試陷阱:若沒加 & 會輸出 1,加上 & 才會正確輸出 2
    

2. 軟體工程實踐與資安防護 (開發品質與防禦)

  • 軟體工程實踐 (Code Quality):

    • 規範與邏輯: 嚴格遵循 PSR-12 編碼規範。架構設計上落實 SOLID 原則DRY (Don't Repeat Yourself),特別是單一職責原則 (SRP),確保一個 Controller 或 Service 只處理一件事,避免義大利麵條程式碼。

    • 測試覆蓋率: 推動 Unit Testing (PHPUnit)。在核心業務邏輯(如:折扣計算、金流處理、庫存扣減)必須要求 100% 的測試覆蓋率,確保重構時不會引發災難。

  • 安全性問題與資安意識:

    • SQL Injection: Laravel 的 Eloquent 底層使用 PDO Parameter Binding(參數綁定),預設已防禦 SQLi。但極度危險的陷阱在於使用 DB::raw() 時,務必手動進行參數綁定,絕對不可直接拼接變數。

    • XSS (跨站腳本攻擊): Blade 模板的 {{ $var }} 會自動執行 htmlspecialchars 轉義。若需輸出 HTML 才使用 {!! $var !!}(需確保資料來源絕對安全)。

    • CSRF (跨站請求偽造): Laravel 內建 VerifyCsrfToken 中間件,所有 POST/PUT 請求都必須攜帶合法的 Token。

    • 進階防禦: 實務上會透過 Nginx 或 Middleware 配置 Security Headers (如 HSTS 強制 HTTPS、Content-Security-Policy 防止外部惡意腳本載入);針對敏感個資 (如身分證、信用卡) 寫入資料庫前,嚴格使用 Laravel 的 Crypt facade 進行 AES-256-CBC 加密。

  • Laravel 中間件 (Middleware) 的妙用: 除了認證 (Auth),資深開發者常利用 Middleware 處理跨領域關注點 (Cross-Cutting Concerns):

    1. API 限流 (Rate Limiting): 搭配 Redis,針對特定 API (如登入、發送簡訊) 限制每分鐘請求次數,防止暴力破解或惡意洗流量。

    2. 日誌審計 (Log Audit): 攔截並記錄所有 POST/PUT/DELETE 的 Request Payload (請求內容) 與操作者 IP/ID,用於後續資安追蹤與行為稽核。

    3. 語系自動切換 (Localization): 根據前端 Header 帶上的 Accept-Language,在 Request 進入 Controller 前自動切換 App::setLocale() 全域語系。

3. 千萬級資料庫設計、優化與 Laravel 實戰架構

  • 資料庫設計與大數據量優化 (千萬級訂單表):orders 表達到千萬等級,單純加索引已無法解決效能瓶頸,必須採取分層策略:

    1. 短線方案 (SQL 優化): 啟用慢查詢日誌 (Slow Query Log),使用 EXPLAIN 分析查詢語句。抓出索引失效的元凶(例如:欄位類型不匹配導致的隱式轉換、使用了 LIKE '%keyword' 導致全表掃描)。

    2. 中線方案 (冷熱分離): 將超過一年的歷史訂單移至 orders_archive 表。線上業務只查近一年的「熱資料」,大幅縮小 B+Tree 的深度與掃描範圍。

    3. 長線方案 (分庫分表 Sharding): 根據 user_id 取模(Hash Modulo,例如 user_id % 10)進行水平拆分,將資料分散到 10 張表或不同的資料庫實例,徹底減輕單機 I/O 壓力。

  • 千萬級資料下 Laravel Eloquent 的優化策略:

    • 解決 N+1 問題: 絕對使用 with() 進行 Eager Loading 預先載入關聯。但若關聯資料極大,改用 joins 或針對特定條件進行 Lazy Eager Loading (load()) 以節省記憶體。

    • 巨量資料處理 (chunk vs chunkById vs cursor):

      • 要更新 10 萬筆資料時,千萬別用 all() 撐爆記憶體。使用 chunk(1000) 分批取出。

      • 陷阱: 如果你在 chunk 迴圈中「更新了過濾條件的欄位」,會導致分頁偏移漏撈資料!這時必須改用基於主鍵的 chunkById()

      • 極低記憶體消耗: 若只是要匯出報表或單純遍歷,使用 cursor()。它底層利用 PHP Generators (生成器),每次只在記憶體保留一個 Model 實例。

  • 讀寫分離與同步延遲 (Read/Write Splitting & Lag):

    • 實作配置:config/database.php 中配置 read (多台 Slave IP 陣列) 與 write (單台 Master IP)。Laravel 底層會自動將 SELECT 隨機分配給 Slave,INSERT/UPDATE/DELETE 送給 Master,從而讓整體 QPS 呈倍數增長。

    • 主從延遲痛點 (Replication Lag): Master 寫入後,同步到 Slave 需要幾十到幾百毫秒。若用戶剛寫入馬上讀取,可能會因為查到還沒同步的 Slave 而看到舊資料。

    • 解法 1:開啟 Sticky (黏滯性):在配置中加上 'sticky' => true。原理是 Laravel 會在同一個 Request 週期內,只要執行過任何寫入,後續所有的讀取都會被強制導向 Master,完美解決「即寫即讀」的空值問題。

    • 解法 2:強制走主庫:如果是跨 Request 的高一致性需求(如結帳後跳轉看餘額),在查詢時明確使用 onWriteConnection()$user = User::onWriteConnection()->find($id);

  • 高併發下的防超賣 (樂觀鎖 vs 悲觀鎖):

    • 悲觀鎖 (SELECT ... FOR UPDATE): 預設一定會有人搶。查詢當下利用 InnoDB 行鎖 (Row Lock) 鎖死資料,直到 Transaction 結束才釋放。其他 Request 會被強制 Block 等待。優點是絕對不會超賣,缺點是並發量高時會導致連線排隊甚至死鎖。

    • PHP
      DB::transaction(function () use ($productId, $buyCount) {
          // lockForUpdate() 會在底層產生 SELECT * FROM products WHERE id = ? FOR UPDATE
          $product = Product::where('id', $productId)->lockForUpdate()->first();
      
          if ($product->stock >= $buyCount) {
              $product->stock -= $buyCount;
              $product->save();
              // 執行到這裡,Transaction 結束,鎖定自動釋放
          } else {
              throw new Exception('庫存不足');
          }
      });
      

    • 樂觀鎖: 適合讀多寫少。在資料表加一個 version 欄位。更新時檢查版本號是否與讀取時一致。如果更新筆數為 0,代表被別人搶先,可選擇報錯或重試。

      PHP
      $affectedRows = DB::table('products')->where('id', 1)->where('version', $oldVersion)
          ->update(['stock' => $stock - 1, 'version' => $oldVersion + 1]);
      
  • 最終殺器:引入 Elasticsearch (ES) 處理複雜搜尋

    • 為什麼要引入 ES? MySQL 的 B+Tree 索引在處理「多條件組合過濾(如電商的價格區間+品牌+多標籤)」或「模糊搜尋 (LIKE %keyword%)」時極易失效,導致全表掃描。ES 使用「倒排索引 (Inverted Index)」,能達到毫秒級的全文檢索與複雜聚合分析,徹底解放 MySQL 的 CPU 與 I/O 壓力。

    • 怎麼引入?

      1. 套件選擇: 在 Laravel 中,通常透過 Laravel Scout 搭配 ES Driver,或直接使用官方的 elasticsearch/elasticsearch 套件。

      2. 資料同步 (Data Sync): MySQL 依然是 Source of Truth。同步資料通常採用兩種方式:

        • 異步隊列 (Queue): 在 Model 的 saved / deleted 事件中,把資料丟進 RabbitMQ 或 Laravel Queue,再由 Worker 寫入 ES。

        • Binlog 訂閱 (進階): 使用 Canal 或 Maxwell 監聽 MySQL 的 Binlog,實現與業務代碼完全解耦的即時同步。

    • 實作範例 (Laravel Scout):

      PHP
      // 在 Model 中引入 Searchable
      class Order extends Model {
          use Searchable;
      
          // 定義要寫入 ES 的資料結構
          public function toSearchableArray() {
              return [
                  'id' => $this->id,
                  'user_id' => $this->user_id,
                  'status' => $this->status,
                  'product_names' => $this->products->pluck('name')->implode(','),
              ];
          }
      }
      
      // 複雜搜尋時,直接將壓力轉移給 ES
      $orders = Order::search('iPhone 15')->where('status', 'paid')->get();
      

4. 高併發架構、Redis 深度應用與常駐記憶體 (Swoole)

  • 每秒萬次請求 (10k TPS) 的解耦與快取架構: 面對瞬間湧入的秒殺或搶票流量,不能讓請求直接打進 MySQL。

    1. 限流攔截: 最外層 Nginx / API 閘道先擋掉惡意請求。

    2. Redis 預扣庫存: 將庫存提早載入 Redis,利用單執行緒原子的 DECR 指令進行高併發扣減,MySQL 完全不參與此階段。

    3. 非同步解耦 (Message Queue): 扣減成功的 Request 不會立刻同步寫入 DB,而是將「訂單建立任務」丟入 RabbitMQ 或 Kafka。

    4. Worker 削峰填谷: 後台的 Consumer (Worker) 依照資料庫能承受的速度,平穩地將隊列中的訂單寫入 MySQL。

  • Redis 實戰場景與三大陷阱防禦:

      1. 快取穿透 (Cache Penetration):

        • 定義: 駭客惡意頻繁查詢一個「快取與資料庫都不存在」的 Key(例如 id = -1)。因為快取查不到,請求全部「穿透」打向 MySQL。

        • 解法: 把 null 值也存進快取(設定極短的過期時間如 60 秒),或在最前端使用布隆過濾器 (Bloom Filter) 直接攔截不存在的 Key。

        PHP
        $user = Cache::remember("user:{$id}", 60, function () use ($id) {
            $data = User::find($id);
            return $data ?: 'null_placeholder'; // 找不到也快取起來,防穿透
        });
        
      2. 快取擊穿 (Cache Breakdown / Hotspot Invalid):

        • 定義: 一個極度熱門的 Key(例如:五月天演唱會售票資訊),在過期失效的「那一瞬間」,剛好有上萬個併發請求湧入。發現快取沒資料,這上萬個請求會同時衝向資料庫去重建快取,瞬間壓垮 DB。

        • 解法: 使用互斥鎖 (Mutex Lock)。發現快取失效時,只有「第一個」拿到鎖的請求可以去查 DB 並重建快取,其他請求必須等待重試或返回舊資料。

        PHP
        $data = Cache::get($key);
        if (!$data) {
            // 嘗試獲取一個 5 秒自動過期的鎖
            $lock = Cache::lock("lock:{$key}", 5);
            if ($lock->get()) {
                // 只有拿到鎖的這單一 Request 能查 DB
                $data = DB::table('hot_data')->first();
                Cache::put($key, $data, 3600);
                $lock->release(); // 釋放鎖
            } else {
                // 沒拿到鎖的,休眠 50ms 後重試,或直接報錯/返回預設值
                usleep(50000); 
                return $this->getData(); 
            }
        }
        
      3. 快取雪崩 (Cache Avalanche):

        • 定義: 大量的 Key 在「同一時間」過期失效,或者 Redis 伺服器直接掛掉。導致原本由快取擋下的海量查詢,像雪崩一樣全部砸在 MySQL 上。

        • 解法: 設定過期時間時,務必加上「隨機擾動值 (Jitter)」將失效時間打散;架構上確保 Redis 的高可用性(Sentinel 或 Cluster)。

        PHP
        // 基礎 60 分鐘 + 隨機 1~300 秒,避免大量 Key 在同一秒集體失效
        Cache::put('key', $data, now()->addMinutes(60)->addSeconds(rand(1, 300)));
        
    • Redis 的其他強大應用:

      1. Atomic Locks (分散式鎖): 利用 SETNX (Laravel 中的 Cache::lock()) 防止同一秒內用戶連按按鈕導致的重複發放優惠券或重複扣款。

      2. Rate Limiting (API 限流): 利用 Redis 的 Sorted Set 或簡單計數器實作令牌桶/漏桶演算法,控制每分鐘 API 呼叫上限。

      3. Pub/Sub (發布/訂閱): 用於輕量級的即時訊息推播系統(如聊天室廣播、系統即時通知),比傳統輪詢 (Polling) 更省資源。

  • WebSocket 長連接:穩定性與橫向擴展 (Horizontal Scaling):

    • 單機 Swoole/Workerman 處理 WebSocket 連線數有上限。當需要橫向擴展部署多台伺服器時,會遇到「廣播難題」:A 用戶連在 Server 1,B 用戶連在 Server 2,Server 1 收到 A 的訊息,要怎麼傳給 B?

    • 解法:引入 Redis Pub/Sub 作為內部通訊橋樑。 當 Server 1 收到訊息時,不直接廣播,而是 Publish 到 Redis 頻道。所有 Server (包含 Server 2) 都 Subscribe 該頻道,收到 Redis 推播後,再各自找出維持在自己記憶體內的 WebSocket 連線 (FD) 並發送出去。

    • 穩定性 (心跳機制 Heartbeat): 必須實作 Ping/Pong 心跳包機制。由客戶端定期發送 Ping,伺服器若超時未收到,主動斷開該連線 (Close FD),避免「幽靈連線」耗盡伺服器資源。

  • Swoole 常駐記憶體與 max_request 治理: Swoole 採用 Event Loop 常駐記憶體,效能極高,但也帶來了致命的 Memory Leak (記憶體洩漏) 危機。因為在傳統 FPM 中,Request 結束後全域變數會被銷毀;但在 Swoole 中,如果你的 Singleton 或靜態陣列 (static $cache = []) 持續塞入資料且未 unset(),記憶體會無限膨脹直到 OOM (Out of Memory)。

    • 治理方案 (安全墊配置):

      PHP
      $server->set([
          'max_request' => 1000, 
      ]);
      
    • 設定原因: 既然無法保證龐大的第三方擴展或團隊寫的每一行程式碼都 100% 不會 Leak,透過設定 max_request,強迫 Worker 進程在處理完 1000 次 Request 後「主動平滑重啟 (Graceful Restart)」。這能徹底銷毀並釋放該進程佔用的所有記憶體與殘留垃圾,是常駐記憶體環境中最有效的保底防禦手段。

    • 檢測工具: 開發階段可運用 gc_status() 觀察引用計數垃圾,或在 Request 前後調用 memory_get_usage() 計算差值;線上排查則依賴 Swoole Tracker 等分析工具 Dump 出記憶體快照抓出未釋放的物件。

    • memory_get_usage() 監控記憶體差值: 最簡單粗暴的方式。在 Worker 進程處理 Request 的開頭與結尾,分別紀錄記憶體消耗。如果 diff 數字隨著請求次數不斷上升且不回落,100% 有 Leak。

      PHP
      $server->on('request', function ($request, $response) {
          $startMemory = memory_get_usage();
      
          // ... 執行業務邏輯 ...
      
          $endMemory = memory_get_usage();
          $diff = $endMemory - $startMemory;
          if ($diff > 1024 * 500) { // 單次請求殘留超過 500KB 就發出告警
              Log::warning("Potential Memory Leak! Leaked: {$diff} bytes.");
          }
      });
      
    • gc_status() 分析垃圾回收池: PHP 的 GC 機制是基於引用計數。如果產生了「循環引用」或被全域變數咬住,物件就不會被銷毀。透過呼叫 gc_status() 可以觀察 roots 的數量。如果在壓力測試下,roots 數量無止盡地瘋狂增長,代表產生了大量無法回收的垃圾。

    • Swoole Tracker / Xhprof (專業工具抓兇手): 當確定有 Leak 後,要在幾萬行代碼中找到是哪一個變數沒釋放,人力很難辦到。這時必須在測試環境掛載 Swoole Tracker(官方商業工具)或 Xhprof 擴展。

      • 作法: 開啟 Memory Leak 檢測模式,發送壓測請求。工具會自動 Dump 出記憶體快照 (Memory Profiling),並在視覺化介面中明確告訴你:「是 App\Services\OrderService 裡面的 $staticCache 陣列佔據了 80% 的記憶體且未被釋放」,工程師只需精準去該處加上 unset() 即可解決。

5. 伺服器極限排錯與 Git 救火指南

  • API 變慢與 CPU 100% 排查流程:

    1. 輸入 top 看 Load Average 與 CPU 佔用排行。

    2. ps aux --sort=-%cpu | head -n 5 精準抓出最操 CPU 的進程 PID。

    3. 若為 PHP-FPM 進程,用 strace -p <PID> 觀察系統調用,看是否卡在某個 Socket 網路請求或 I/O 讀取。

    4. 調閱 PHP-FPM 的 slowlog,這裡會直接印出執行時間超標的 PHP 函式堆疊 (Stack Trace),直接定位到出問題的原始碼行數。

  • 磁碟空間 100% 爆滿急救陷阱: 當伺服器因 Nginx 或 Laravel 專案的 Log 塞滿導致完全卡死時,千萬不可以直接使用 rm -rf laravel.log!因為若 PHP-FPM 等進程還咬著該檔案的 File Descriptor,實體磁碟空間並不會被釋放。

    • 正確清空方式: 使用 > /var/log/laravel.logcat /dev/null > /var/log/laravel.log。這能瞬間將檔案大小歸零,且不破壞檔案的讀寫關聯。

    • 若已誤刪且空間沒回來:執行 lsof | grep deleted 找出殭屍進程,重啟該進程服務即可釋放。

  • Git 進階協作與機密撤回:

    • 線上緊急 Bug (Hotfix): 正在開發 Feature 時突發 Bug,立刻 git stash 暫存手邊工作。切回 main 分支拉出 hotfix 分支。修復並測試後,合併回 main (使用 merge --no-ff 保留清晰的修復節點),推上線後,再回到 Feature 分支 git stash pop 繼續開發。

    • 重大資安:誤 Push .env 到 GitHub:

      1. 第一步:立刻更改所有外洩的真實密碼、API Keys。

      2. 使用 git filter-branchBFG Repo-Cleaner 將該檔案從所有歷史 Commit 中徹底抹除。

        Bash
        git filter-branch --force --index-filter 'git rm --cached --ignore-unmatch .env' --prune-empty --tag-name-filter cat -- --all
        
      3. 將 .env 加入 .gitignore

      4. 執行強制推送 git push origin --force --all 覆蓋遠端紀錄。

2026年3月15日 星期日

如果你有鼻塞困擾,你一定要試試看

想不到吧,洨編涉略的的領域除了資訊,現在包含到醫學了

其實也不確定算不算正統醫學,但這兩天嘗試我覺得挺有效的,跟大家分享

相信點進這篇文章的你,一定有鼻塞的困擾。

先假設如果你現在是單邊鼻塞,可以試試看這個方法:

1. 將鼻塞那邊的那隻手握拳。
2. 伸到另一邊的腋下夾起來。
3. 這時候應該會頂到你二頭跟三頭肌中間的縫,會有一點痛痛的(沒有關係,也可以多試幾個角度夾一下)。

過大約半分鐘,你鼻塞那邊的鼻子就會通了,真的非常神奇,大家可以試試看。

我目前還沒有過兩邊鼻塞,那你可以嘗試兩邊都這樣夾夾看,看會不會兩邊都通。那持續的時間,目前也還沒有實際計算跟對照樣本。

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應該都可以吧


# GET /api/posts
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Post;
use App\Models\User;
use Laravel\Octane\Facades\Octane;

class PostController extends Controller
{
public function index()
{
// Measure Octane::concurrently time
$startConcurrent = microtime(true);
[$users, $posts] = Octane::concurrently([
fn() => User::limit(10)->get(),
fn() => Post::with(['author', 'comments.author'])->limit(10)->get(),
]);
$endConcurrent = microtime(true);
$concurrentTime = ($endConcurrent - $startConcurrent) * 1000; // ms

// Measure Sequential time
$startSequential = microtime(true);
$seqUsers = User::limit(10)->get();
$seqPosts = Post::with(['author', 'comments.author'])->limit(10)->get();
$endSequential = microtime(true);
$sequentialTime = ($endSequential - $startSequential) * 1000; // ms

return [
'concurrent' => [
'time_ms' => round($concurrentTime, 2),
// 'users' => $users,
// 'posts' => $posts,
],
'sequential' => [
'time_ms' => round($sequentialTime, 2),
// 'users' => $seqUsers,
// 'posts' => $seqPosts,
],
'performance_difference_ms' => round($sequentialTime - $concurrentTime, 2),
];
}
}

# 最後就可以透過Octane::concurrently來取資料,但洨邊實際測試時間沒有差很多,甚至大部份測試結果比分開取還要慢
# 可能要更複雜的邏輯才能感受到協程的效果吧


# 接著來看一下執行時間
# php artisan octane:start(swoole)
[server] 200 GET /api/posts .................. 24.65 mb 14.05 ms
# php artisan serve
2026-03-16 13:52:42 /api/posts ........................... ~ 504.57ms

# SQL 執行時間
# 初次讀取沒有顯著差異,但如果第二是執行swoole有快取,就會差近10倍,5ms跟0.5ms的差別
# 但是如果是php artisan serve,但卻使用Octane::concurrently,那執行時間會爆喔


最終來科普一下有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); // 確保無論成功或失敗都會歸還連線

}