當在 Node.js 或 Python FastAPI 中寫下 async 和 await 時,感覺就像施展了魔法。程式碼在 await 的地方優雅地「暫停」,伺服器卻沒有因此卡死,反而能繼續處理成千上萬的請求。 但魔法的背後總是科學。那個被「暫停」的任務,尤其是像資料庫查詢、API 呼叫這種 I/O 操作,究竟是誰在處理?它被交給了機器的哪個部分? 答案可能會讓你驚訝:把最耗時的「等待」工作,外包給了電腦中最勤勞、最高效的員工——作業系統核心 (Operating System Kernel)。 讓我們深入探討這個過程。 迷思:是非同步框架自己開了個線程嗎? 一個常見的誤解是,每當 await 一個 I/O 操作,框架就會在背景開一個新的線程 (thread) 去處理它。雖然這在某些情況下(如 CPU 密集型任務)是可能的,但對於處理大量 I/O 請求來說,這種模型效率極低。每開一個線程都要消耗記憶體和 CPU 上下文切換的成本,無法支撐成千上萬的併發連接。 真正的答案,是一場由 應用程式執行環境 (Runtime) 和 作業系統 (OS) 配合演出的精采好戲。 真正的演員:專案經理與行政部門 我們可以把整個非同步流程想像成一個高效的辦公室: 專案經理 (The Runtime / Event Loop) 在 Node.js 的世界裡,它就是 Node.js Runtime (由 V8 引擎和 libuv 函式庫組成)。 在 FastAPI 的世界裡,它就是由 Uvicorn 伺服器所驅動的 Python 事件循環 (Event Loop)。 職責:執行程式碼 (JavaScript 或 Python)。它非常忙碌,討厭等待。 行政部門 (The Operating System Kernel) 無論是 Linux, macOS, 還是 Windows,它們的核心都具備強大的 I/O 處理能力。 職責:處理所有與硬體相關的「髒活累活」,比如網路通訊、讀寫檔案。它有自己的高效工具。 表演開始:一次非同步 API 呼叫的旅程 假設 Node.js 或 FastAPI 應用需要呼叫一個外部 API。 步驟 1:專案經理下達指令 當程式碼執行到 await 這一行時: Node.js (使用 node-fetch) import fetch from 'node-fetch'; app.get('/data', async (req, res) => { console.log("準備發送請求..."); // 指令點 const response = await fetch('https://api.example.com/items'); const data = await response.json(); console.log("收到資料!"); res.send(data); }); FastAPI (使用 httpx) import httpx from fastapi import FastAPI app = FastAPI() @app.get("/data") async def get_data(): print("準備發送請求...") async with httpx.AsyncClient() as client: # 指令點 response = await client.get("https://api.example.com/items") print("收到資料!") return response.json() 專案經理 (Runtime) 看到這個指令,它並不會傻等。它會透過一個系統呼叫 (System Call),把任務交給行政部門 (OS)。它對 OS 說: 「嘿,OS!請透過網路向 api.example.com 發送這個 GET 請求。完成後,在你的任務看板上通知我。這是這個任務的編號。」 步驟 2:專案經理回去忙別的 下達指令後,專案經理的雙手就自由了!它會立刻返回它的事件佇列 (Event Queue),看看有沒有其他事情可以做,比如: 處理另一個剛進來的 HTTP 請求。 執行一個剛剛計時器到期的 setTimeout。 處理任何已經完成的 I/O 事件。 這就是非阻塞 (Non-blocking) 的核心:永不等待 I/O。 步驟 3:行政部門的高科技看板 (epoll / kqueue / IOCP) 作業系統是如何高效管理成千上萬個待辦的 I/O 任務的?它有秘密武器: Linux: epoll macOS/BSD: kqueue Windows: IOCP (I/O Completion Ports) 可以把 epoll 想像成一個極其高效的電子任務看板。OS 把所有待辦的 I/O 請求(來自不同應用、不同進程)都註冊到這個看板上。 專案經理 (Runtime 的 Event Loop) 現在只需要做一件非常簡單的事:每隔一小段時間就問 OS 同一個問題: 「看板上有沒有任何任務更新狀態為『已完成』?」 這個查詢操作本身非常快,幾乎不耗費資源。 步驟 4:任務完成,領取結果 當外部 API 回傳資料,電腦的網路卡接收到數據後,會透過硬體中斷通知 OS。OS 知道這是之前那個任務的結果,於是它在 epoll 看板上,把對應任務的狀態更新為「已完成」。 下一次,當專案經理 (Event Loop) 再來詢問時,OS 就會告訴它:「是的,任務 #123 已經完成了,結果在這裡。」 專案經理拿到結果後,就會喚醒之前在 await 點被「暫停」的函式,把結果 (response) 交給它,讓它繼續執行後續的程式碼,例如 response.json() 和 res.send(data)。 資源分配:CPU 和記憶體用在哪? 既然工作外包給了 OS,還需要為此分配 CPU 和記憶體嗎? 是的,但分配的對象是應用程式進程 (Node.js 或 Uvicorn),而不是那個抽象的「等待」過程。 CPU: 何時使用: 當你的程式碼正在被執行時。比如解析 JSON、執行業務邏輯、序列化回應。 何時不用: 在 await I/O 的漫長等待期間,你的進程會讓出 CPU,讓 CPU 可以去執行其他工作(或者被 OS 閒置)。這就是非同步模型省 CPU 的關鍵。 記憶體: 用在哪裡: 用於儲存每一個併發請求的上下文 (Context)。即使一個請求正在 await,它的所有狀態(局部變數、函式呼叫堆疊等)都必須保存在記憶體中,以便在任務完成時可以從中斷的地方繼續。 這意味著:你的應用能處理的併發量,直接受限於可用記憶體。 結論 async/await 的魔法,源於一場分工明確的合作: 應用程式 Runtime (Node.js/Uvicorn) 擔任「專案經理」,負責執行業務邏輯程式碼;而它將所有耗時的 I/O 等待工作,外包給了最高效的「行政部門」——作業系統核心。 它們之間透過一個名為事件循環 (Event Loop) 的高效溝通機制來協調。 下一次當你寫下 await 時,可以自豪地知道,你不是在施展魔法,而是在指揮現代計算機中最優雅、最高效的協作模式之一。

在現代軟體開發中,管理應用程式的設定是一個不可或缺的環節。從資料庫連線字串、API 金鑰到各種服務的憑證,這些敏感或因環境而異的資訊,都不希望直接寫死在程式碼中。這就是「環境變數」發揮關鍵作用的地方。 本文將帶您深入了解環境變數的核心概念、為什麼它如此重要,以及如何在各種環境中有效地設定與使用它。 什麼是環境變數? 環境變數(Environment Variables)是在作業系統層級定義的動態具名值。它們存在於應用程式的執行環境中,可以被應用程式讀取和使用。簡單來說,它們是將設定與程式碼分離的一種強大機制。 為什麼要使用環境變數? 使用環境變數主要有以下幾個優點: 安全性 (Security):將 API 金鑰、密碼等敏感資訊儲存在環境變數中,可以避免將它們提交到版本控制系統(如 Git),從而降低洩漏風險。 可攜性 (Portability):應用程式可以在不同的環境(開發、測試、生產)中執行,而無需修改任何程式碼。只需為每個環境設定對應的變數值即可。 靈活性 (Flexibility):當設定需要變更時(例如更換資料庫),只需要更新環境變數的值並重新啟動應用程式,而不需要重新部署整個程式。 遵循十二因子應用程式 (The Twelve-Factor App):這是建構現代雲端原生應用程式的一套方法論,其中第三條原則明確指出「在環境中儲存設定」。 如何設定環境變數? 設定環境變數的方式因作業系統而異。 在 macOS 與 Linux 在類 Unix 系統中,可以使用 export 指令來設定一個在當前終端機 session 中有效的環境變數。 export DATABASE_URL="postgresql://user:password@host:port/dbname" 若要讓變數永久生效,可以將此行加入到 shell 的設定檔中,例如 ~/.bash_profile、~/.zshrc 或 ~/.profile。 在 Windows 在 Windows 中,可以使用圖形化介面或 setx 指令來設定。 圖形化介面: 在「開始」功能表中搜尋「編輯系統環境變數」。 點擊「環境變數…」按鈕。 在「系統變數」或「使用者變數」區塊中新增或編輯。 命令提示字元 (Command Prompt): 使用 setx 指令可以永久設定一個環境變數。 setx DATABASE_URL "postgresql://user:password@host:port/dbname" 請注意,setx 設定的變數只會在新的命令提示字元視窗中生效。 在專案中使用 .env 檔案 在開發環境中,每次都手動 export 或 setx 變數可能有點繁瑣。一個更常見且方便的做法是使用 .env 檔案。 .env 是一個純文字檔案,可以在其中以 KEY=VALUE 的格式定義環境變數。 # .env file DATABASE_URL=postgresql://user:password@localhost:5432/mydb API_KEY=your_secret_api_key DEBUG=True 大多數現代程式語言和框架都有對應的函式庫可以自動讀取 .env 檔案並將其載入到執行環境中。 Python: python-dotenv Node.js: dotenv Ruby: dotenv-rails Go: godotenv 重要提示:永遠不要將 .env 檔案提交到版本控制中!請務必將它加入到 .gitignore 檔案中。通常,您會提交一個名為 .env.example 或 .env.template 的範本檔案,其中包含所有必要的環境變數名稱,但值是空的或範例值,以供其他開發者參考。 # .gitignore .env 多環境設定 (.env.dev, .env.prod) 當需要管理多個環境時,可以建立不同的 .env 檔案,例如 .env.dev 和 .env.prod。 工具如何載入非標準 .env 檔案? python-dotenv:from dotenv import load_dotenv # 明確指定要載入的 .env 檔案路徑 load_dotenv(dotenv_path=".env.dev") uv / poetry:# 使用 --env-file 旗標 uv run --env-file .env.dev -- python my_script.py docker-compose:# 在 docker-compose.yml 中明確指定 services: web: env_file: - .env.dev docker run:docker run --env-file .env.dev myimage 現代化 Python 工具的自動載入機制 值得注意的是,許多現代化的 Python 開發工具,例如 poetry 和 uv,已經內建了自動偵測並載入 .env 檔案的功能。當使用 poetry run 或 uv run 來執行腳本或應用程式時,它們會自動將專案根目錄下的 .env 檔案中的變數載入到執行環境中。 這意味著在本地開發時,通常不需要在 Python 程式碼中額外引用 python-dotenv 這類的函式庫來手動載入 .env 檔案,讓開發流程更加簡潔。 如何在程式碼中讀取環境變數? 一旦環境變數被設定(無論是透過 export、.env 檔案或容器設定),就可以在程式碼中存取它們。 以 Python 為例:os.getenv() 在 Python 中,標準函式庫 os 提供了讀取環境變數最直接的方法。 import os # 讀取環境變數 'DATABASE_URL' database_url = os.getenv("DATABASE_URL") # 建議提供一個預設值,以防變數未被設定 # os.getenv('KEY', 'default_value') api_key = os.getenv("API_KEY", "default_api_key_for_development") if database_url: print("成功讀取到資料庫連線資訊。") else: print("警告:未設定 DATABASE_URL 環境變數。") os.getenv(KEY, default=None) 是最常用的函式。它的優點是當環境變數不存在時,它會回傳 None(或指定的預設值),而不會導致程式出錯。這使得處理可選的設定變得非常方便。 Docker 與 Docker Compose 中的環境變數 Docker & Docker Compose 中的變數管理 核心:變數的優先級 理解變數如何被載入和覆蓋是關鍵。對於 docker-compose,優先級順序如下(數字越小,優先級越高): Shell 環境變數:在執行 docker-compose up 的終端機中設定的變數,或是在 CI step 的 env 區塊中設定的變數。這是最高優先級。 docker-compose.yml 中的 environment 區塊:這裡定義的變數。 docker-compose.yml 中的 env_file 區塊:env_file 指定的檔案中的變數。 .env 檔案:位於專案根目錄下的 .env 檔案(會被自動載入)。 Dockerfile 中的 ENV:映像檔內建的環境變數(最低優先級)。 docker run vs docker-compose docker run 不會自動載入 .env 檔案。它是一個較低階的指令,必須明確地使用 -e (單個變數) 或 --env-file (檔案) 來傳遞變數。# 從 shell 傳遞單個變數 export API_KEY="123" docker run -e API_KEY my_image # 使用 env file docker run --env-file ./.env.prod my_image docker-compose 會自動載入 .env 檔案,這使得它在開發中更為方便。 Dockerfile 中的 ENV Dockerfile 中的 ENV 指令是在建置映像檔 (build time) 時設定的,它不支持 ${VARIABLE} 這種執行時的變數替換。它定義的是映像檔的「預設」環境變數,優先級最低,會被任何執行期的設定所覆蓋。 # Dockerfile # 設定一個靜態的預設值 ENV GREETING="Hello" # 這個值可以在執行時被 docker run 或 docker-compose 的設定覆蓋 關鍵差異:docker-compose vs uv 的載入行為 這是一個極其重要的細節: docker-compose (合併與覆蓋): 當在 docker-compose.yml 中使用 env_file 指定了 .env.dev,docker-compose 仍然會先載入預設的 .env 檔案,然後再載入 .env.dev。這是一個合併 (merge) 的過程,如果兩個檔案有相同變數,.env.dev 的值會覆蓋 .env 的值。 uv run (精確指定): 當使用 uv run --env-file .env.dev 時,uv 只會載入您指定的 .env.dev 這一個檔案,它不會再去自動尋找並載入預設的 .env 檔案。 CI/CD 整合策略 如何將 GitHub Actions 中的 secrets 安全地注入到 docker-compose 服務中? 策略一:隱性繼承 (最簡潔) 此策略充分利用了 Docker Compose 的自動化特性,實現了極致的簡潔。 1. docker-compose.yml 設定 docker-compose.yml 中完全不提及環境變數。 # docker-compose.yml services: web: image: my_app # 此處留空,讓容器直接繼承執行環境的變數 這是最關鍵的一點:當服務中沒有 environment 或 env_file 區塊時,Compose 會將從 Shell 或 .env 檔案中讀取到的所有變數自動傳遞給容器。 2. 本地與 CI 的實現 本地開發:開發者在專案根目錄下建立 .env 檔案。執行 docker compose up 時,Compose 會自動讀取它並將變數注入容器。 CI/CD (GitHub Actions):在 step 中使用 env 區塊設定變數。 # .github/workflows/ci.yml ... - name: Build and run containers env: DATABASE_URL: ${{ secrets.DATABASE_URL }} API_KEY: ${{ secrets.API_KEY }} run: docker-compose up -d --build 由於 Shell 環境變數(由 env 區塊設定)的優先級最高,這些變數會被直接注入容器,完美實現了目標。 優點: 極致簡潔:docker-compose.yml 和 CI 腳本都達到了最簡潔的狀態。 無縫開發體驗:本地開發者只需維護 .env 檔案,符合直覺。 缺點: 隱性依賴:容器的行為完全依賴於外部環境,對於不熟悉 Compose 優先級規則的人來說,可能會感到困惑(「變數是從哪裡來的?」)。 策略二:動態生成 .env 檔案 (最明確) 此策略的核心是,無論在哪個環境,都明確地透過一個 .env 檔案來提供設定。 1. docker-compose.yml 設定 docker-compose.yml 保持乾淨,不需要設定 env_file,因為 Compose 會自動尋找 .env。 # docker-compose.yml services: web: image: my_app # 留空,依賴自動載入的 .env 檔案 2. 本地與 CI 的實現 本地開發:開發者手動建立和維護 .env 檔案。 CI/CD (GitHub Actions):在執行 docker-compose 前,動態生成 .env 檔案。 # .github/workflows/ci.yml ... - name: Create .env file from secrets run: | echo "DATABASE_URL=${{ secrets.DATABASE_URL }}" >> .env echo "API_KEY=${{ secrets.API_KEY }}" >> .env - name: Run containers run: docker-compose up -d 優點: 高度明確:.env 檔案是唯一的事實來源,非常清晰。 工作流程一致:本地和 CI 都圍繞 .env 這個核心檔案工作。 缺點: CI 腳本稍繁瑣:需要在 workflow 中維護一個變數列表來生成檔案。 結論與如何選擇? 經過最終的分析,兩種策略都非常優秀,且都利用了 docker-compose 自動載入 .env 的特性。 策略一 (隱性繼承) 更為優雅、簡潔。它相信工具的自動化能力,適合追求極致效率和簡潔性的團隊。 策略二 (動態生成 .env) 更為穩健、明確。它將「環境設定」這個關注點顯式地固化在一個檔案中,適合大型、複雜或對可讀性要求極高的專案。 最終的選擇取決於團隊的風格和偏好:是擁抱「隱性的魔法」,還是堅持「明確的契約」?理解這兩種方法的底層邏輯和權衡,是做出最佳決策的關鍵。 結論 有效地管理環境變數是專業軟體開發的基石。它不僅能提升應用程式的安全性與可攜性,更是實踐 CI/CD 與雲端原生開發的必要技能。 無論是個人開發者還是團隊的一員,都應該養成從專案一開始就使用環境變數來管理設定的好習慣。從本地開發利用 .env 和 os.getenv,到深刻理解 Docker Compose 變數載入的優先級,為專案選擇最合適的 CI/CD 整合策略,再到最終在容器中進行標準化部署,掌握這些技巧將使開發流程更加順暢、安全和專業。

AMQP 的全名是 Advanced Message Queuing Protocol (進階訊息隊列協定)。 可以把它想像成是訊息世界的「HTTP」。就像 HTTP 定義了瀏覽器(客戶端)如何與網頁伺服器溝通的規則一樣, AMQP 是一個開放標準的應用層協定,它定義了訊息發送者(Producer)和訊息接收者(Consumer)之間,如何透過一個中介(Broker)來傳遞訊息的標準化規則。 AMQP 的核心目標 AMQP (Advanced Message Queuing Protocol,進階消息隊列協定) 作為一個開放標準的應用層協定,它的設計初衷非常明確: 可靠性 (Reliability): 確保訊息在傳輸過程中絕不遺失。從發送到接收,每一步都有確認機制。 互通性 (Interoperability): 打破語言和平台的壁壘。一個用 Python 寫的服務可以和一個用 Java 寫的服務透過 AMQP 完美溝通,因為它們說的是同一種「語言」。 RabbitMQ 是市面上最流行、最穩定的 AMQP 協定實現者 (Message Broker) 之一。 AMQP 的核心概念與運作流程 要理解 AMQP 的運作模式,需要先認識幾個關鍵角色,把它們想像成一個高效率的數位郵局系統。 核心成員 Producer (生產者): 訊息的創建者與發送方。例如,一個網站後端,在使用者上傳圖片後,它會發送一個「請處理這張圖片」的訊息。 Consumer (消費者 / Worker): 訊息的接收者與處理方。例如,一個專門用來壓縮圖片、加上浮水印的背景服務。 Broker (中介 / 伺服器): 整個系統的核心,也就是 RabbitMQ 自身。它像一個郵政總局,負責接收、暫存並路由所有訊息。 Exchange (交換機): Broker 內部的「分揀中心」。它從 Producer 接收訊息,並根據路由規則 (Routing Key) 將訊息推送到一個或多個佇列中。 Queue (佇列): 儲存訊息的「信箱」。訊息會在這裡排隊,等待 Consumer 前來取用。 Binding (綁定): 連接 Exchange 和 Queue 的規則。一個 Binding 會告訴 Exchange:「符合這個規則的訊息,請幫我送到那個 Queue 去」。 運作流程 發布 (Publish): Producer 將訊息發送到 Broker 內的某個 Exchange。發送時通常會附帶一個 Routing Key,例如 image.process.new。 路由 (Route): Exchange 收到訊息後,會檢查其 Routing Key,並根據它與各個 Queue 之間的 Binding 規則,決定訊息的去向。 入隊 (Enqueue): 訊息被精準地放入指定的 Queue 中排隊等待。 消費 (Consume): Consumer 會訂閱 (subscribe) 它感興趣的 Queue。一旦 Queue 中有新訊息,Broker 就會將訊息推送給 Consumer。 處理與確認 (Process & Acknowledge): Consumer 收到訊息後,開始執行任務(例如壓縮圖片)。任務完成後,它會向 Broker 發送一個確認信號 (Acknowledgement, ack)。這個 ack 至關重要,它等於在告訴 Broker:「這個訊息我已成功處理,你可以從 Queue 中將它安全刪除了」。如果 Consumer 在處理過程中崩潰且未發送 ack,Broker 會將該訊息重新交給另一個健康的 Consumer 處理,這就是 AMQP 可靠性的關鍵。 AMQP 的三大殺手級應用 AMQP 究竟如何解決真實世界的工程問題?以下是三個最經典的應用場景: 1. 非同步背景任務 (Asynchronous Background Jobs) 痛點: 使用者在網站註冊,點擊「提交」後,系統需要發送歡迎郵件。如果同步處理,使用者必須盯著加載圈圈,直到郵件伺服器回應為止,體驗極差。 AMQP 應用: 註冊服務 (Producer) 只需發送一條訊息 {"action": "send_welcome_email", "to": "user@example.com"} 到 RabbitMQ,然後立刻回應用戶「註冊成功!」。另一個獨立的郵件服務 (Consumer) 會從 Queue 中取得訊息,在背景悠閒地發送郵件。使用者體驗瞬間提升。 2. 服務解耦 (Decoupling Services) 痛點: 在微服務架構中,訂單服務(Order Service)建立新訂單後,需要通知庫存服務(Inventory Service)減庫存,還要通知物流服務(Shipping Service)準備出貨。如果訂單服務直接呼叫這兩個服務的 API,它們就緊緊地耦合在一起了。未來若新增一個數據分析服務也需要訂單資料,就必須修改訂單服務的程式碼。 AMQP 應用: 訂單服務 (Producer) 只需發送一條「訂單 #123 已建立」的訊息到一個 “fanout” 型別的 Exchange。庫存和物流服務 (Consumers) 各自監聽自己的 Queue,而這些 Queue 都綁定到同一個 Exchange。訊息一到,所有 Queue 都會收到一份拷貝。未來新增數據分析服務時,只需讓它也建立一個 Queue 並綁定到該 Exchange 即可,訂單服務完全無感知。這就是真正的「高內聚,低耦合」。 3. 流量削峰 (Buffering for Load Spikes) 痛點: 舉辦「雙十一」限時秒殺活動,午夜 12 點整,數百萬用戶的請求像洪水一樣湧向你的資料庫,瞬間就可能導致資料庫過載崩潰。 AMQP 應用: Web 伺服器 (Producers) 不再直接寫入資料庫,而是將所有秒殺請求快速轉化為訊息,然後塞進 RabbitMQ 的 Queue 中。後端的資料庫寫入服務 (Consumers) 則按照自己的最大處理能力,平穩地、一個一個地從 Queue 中取出請求來處理。Queue 在這裡扮演了一個巨大的緩衝區,將瞬間的流量洪峰「削平」,保護了脆弱的後端系統。 總結 AMQP 不僅僅是一個協定,它更是一種設計哲學,旨在構建可靠、可擴展且具備彈性的分散式系統。透過它,我們可以輕鬆實現非同步處理、服務解耦和流量控制等現代應用架構的關鍵需求。 總結來說,AMQP 是一個強大的協定,而 RabbitMQ 則是這個協定的優秀實現者。它們共同構成的訊息系統是現代分散式應用程式架構中不可或缺的一環。

在現代後端開發中,「非同步(asynchronous)」是一個重要的技術趨勢,尤其面對大量 I/O 操作或高併發需求時。本文將從 Python 非同步的運作流程談起,並探討在後端是否有必要使用非同步處理資料流。 非同步的基本觀念:為什麼非同步不會阻塞? JavaScript 及 Python 的非同步設計,核心在於: 避免主執行緒阻塞 把耗時的 I/O 任務交給背景系統處理 等待完成後,再透過事件排程讓結果回到主執行緒 重要觀念:非同步任務完成後需要排隊回傳結果,但這段等待並不會阻塞主執行緒,因為主執行緒可以去執行其他任務。 非同步與阻塞的差異比喻 同步(阻塞) 非同步(非阻塞) 你排隊等餐,直到拿到餐才離開 你點完餐回位子坐著,等叫號再拿餐 Python 非同步運作流程(asyncio 與事件迴圈) Python 透過 asyncio 套件實現非同步,核心架構包含: 協程(coroutine):用 async def 定義的非同步函式 事件迴圈(event loop):管理協程的執行與切換 非阻塞 I/O:遇到 await 時暫停協程,讓事件迴圈去處理其他工作 Python 非同步示意程式 import asyncio async def download_file(filename): print(f"Start downloading {filename}") await asyncio.sleep(2) # 模擬非同步 I/O 任務 print(f"Finished downloading {filename}") return filename async def main(): tasks = [ download_file("file1.txt"), download_file("file2.txt"), download_file("file3.txt") ] await asyncio.gather(*tasks) asyncio.run(main()) 在上面例子中,三個下載任務會「同時」開始,因為事件迴圈會在等待(sleep)時切換到其他任務,達到非阻塞效果。 非同步 vs 多執行緒:效能差異與適用場景 特色 非同步(Event Loop) 多執行緒(Thread-based) 執行緒數 單一主執行緒 + 背景任務 多個執行緒 適合任務類型 I/O-bound(網路、檔案存取) CPU-bound(密集運算) 資源使用 較少(低記憶體占用) 較高(每個執行緒需獨立記憶體) 阻塞風險 只要避免同步阻塞就不會卡死 一個執行緒卡死不影響其他執行緒 並行度 非同步模擬並行,非真正多核心運算 可利用多核 CPU 進行真正多工 複雜度 回呼函式、事件排程較複雜 執行緒同步、鎖與死鎖問題需注意 非同步在背後執行效能是否較差? 非同步任務雖然不是在主線程跑,但不代表效率低: I/O-bound 任務本質上耗時在等待資料回應,非同步能利用等待時間切換其他任務,提高資源利用率。 背景系統(如 Node.js 的 libuv threadpool 或瀏覽器的 Web API)專門負責 I/O,效率通常比主線程好。 但如果用非同步處理大量 CPU 運算,則會卡主事件迴圈,這時應該用多執行緒或多進程。 在後端是否有必要用非同步處理資料流? 適合使用非同步的場景 高併發 API 請求(數千到數萬連線) 需要大量等待 I/O(資料庫、檔案、第三方服務) WebSocket 或長連線通訊 大量資料串流與處理(爬蟲、資料管線) 不適合使用非同步的場景 CPU 密集計算(影像處理、科學運算) 使用大量同步套件(無法 async 支援) 系統結構複雜,非同步調試困難 總結 問題 答案 非同步完成後回主線程排隊會阻塞嗎? 不會,因為主線程可同時處理其他任務,排隊是非阻塞等待。 非同步背景執行效能差嗎? 對 I/O-bound 非同步任務效率高,但 CPU-bound 任務需多執行緒處理。 後端要不要用非同步? 若主要是 I/O-bound 或高併發,推薦使用非同步。CPU-heavy 則不適合。

在現代軟體架構中,非同步處理是提升系統吞吐量和響應能力的關鍵。然而,不同的非同步技術解決的是不同層次的問題。本文旨在精確剖析三種常見的非同步原語:async/await、Message Queue 和 setTimeout,闡明其核心差異與適用場景。 一句話總結:它們不能通用,因為它們是不同維度的工具。 async/await:是語言層面的語法糖,用來管理單一進程內的非阻塞 I/O 操作。它是「微觀」的。 Message Queue:是架構層面的組件,用來解耦多個獨立進程或服務之間的通訊。它是「宏觀」的。 setTimeout:是執行環境提供的 API,用來將一個函式的執行延遲到未來的某個時間點,通常在單一進程內。 讓我們用一個「餐廳廚房」的比喻來深入理解。 async/await:一位廚師的多工處理技巧 想像廚房裡只有一位廚師 (單一執行緒)。他需要同時做三道菜: 烤箱裡的牛排(需要 20 分鐘) 爐子上的湯(需要 10 分鐘) 切沙拉(需要 5 分鐘) 一個同步 (Synchronous) 的廚師會: 把牛排放進烤箱,然後呆呆地站在烤箱前等 20 分鐘。 20 分鐘後,拿出牛排,開始煮湯,又在爐子前等 10 分鐘。 10 分鐘後,湯好了,才開始切沙拉。 結果: 總共耗時 35 分鐘,效率極低。 一個使用 async/await (非同步) 的廚師會: await 烤牛排: 把牛排放進烤箱(發起一個 I/O 操作),然後立即轉身去做別的事。 await 煮湯: 把湯放上爐子(發起另一個 I/O 操作),然後立即轉身。 切沙拉: 這是一個 CPU 密集型操作,他現在就做。 在切沙拉的過程中,他會時不時地檢查(事件循環 Event Loop)烤箱和爐子好了沒有。烤箱的計時器響了(I/O 完成),他就去處理牛排。 結果: 大約 20 分鐘後,所有菜都做好了。 總結 async/await: 範疇: 單一進程/執行緒內。 目的: 避免在等待 I/O(網路請求、資料庫查詢、檔案讀寫)時阻塞整個執行緒,從而提高單一進程的併發處理能力。 核心: 事件循環 (Event Loop)。 不能做什麼: 它不能讓 CPU 密集型任務(如圖像處理、複雜計算)變快,也不能讓任務在另一個伺服器上執行。它只是讓等待時間被有效利用。 Message Queue (MQ):一個分工明確的大型中央廚房 現在想像一個大型連鎖餐廳的中央廚房 (一個服務) 和遍布全市的多家分店 (多個其他服務)。 場景: 分店(例如,網頁前端)接到顧客的訂單(例如,使用者註冊)。這個註冊流程需要: 建立使用者帳號(立即完成)。 發送一封歡迎郵件(可能很慢)。 為使用者初始化數據分析報告(非常耗時,可能要幾分鐘)。 如果分店經理(主應用程式)自己做所有事,顧客就要在櫃台前等很久。 使用 Message Queue 的流程: 分店經理(主應用程式)完成使用者帳號建立後,立即告訴顧客:「好了,您可以點餐了」。 同時,他寫了兩張工作單:「給這位顧客發歡迎郵件」、「為這位顧客準備分析報告」。 他把這兩張工作單放進一個傳送帶 (Message Queue) 上。 傳送帶將工作單送到了中央廚房的後廚。 後廚裡有專門的郵件師傅 (Email Worker 進程) 和數據分析師傅 (Report Worker 進程)。他們從傳送帶上拿走屬於自己的工作單,然後開始工作。 總結 Message Queue: 範疇: 跨進程、跨服務、甚至跨伺服器。 目的: 解耦: 分店不需要知道後廚在哪,也不需要關心他們怎麼工作。 非同步執行: 將耗時的、非核心的任務交給背景去處理,讓主應用程式快速響應。 削峰填谷/可靠性: 即使後廚很忙,工作單也可以在傳送帶上排隊,不會丟失。 核心: 一個獨立的中介軟體 (Broker),如 RabbitMQ, Kafka, Redis。 setTimeout:一個簡單的廚房計時器 setTimeout 是最簡單的非同步形式。 場景: 廚師把麵包放進烤箱,他需要 5 分鐘後回來檢查。 流程: 他設定了一個計時器 (setTimeout),告訴計時器:「5 分鐘後提醒我(執行一個回呼函式)」。然後他就去做別的事了。5 分鐘後,計時器響了,他就回來處理麵包。 總結 setTimeout: 範疇: 單一進程/執行緒內。 目的: 將一個函式的執行延遲到一個確定的未來時間點。它只是推遲執行,不涉及併發或跨進程通訊。 核心: 由執行環境(瀏覽器、Node.js)提供的計時器功能。 總結 特性 async/await Message Queue (e.g., Celery) setTimeout 層次 語言語法 系統架構 環境 API 尺度 進程內 (微觀) 跨進程/跨服務 (宏觀) 進程內 (微觀) 解決問題 管理非阻塞 I/O,提高單進程併發 解耦服務,處理耗時/可靠的背景任務 延遲執行函式 持久性 無 (進程結束任務消失) 有 (Broker 持久化訊息) 無 (進程結束任務消失) 依賴 語言本身 外部中介軟體 (Broker) 執行環境 (瀏覽器/Node.js) 比喻 一位廚師的多工技巧 大型中央廚房與分店的協作 一個廚房計時器 結論:它們完全不能通用。 不能用 async/await 來代替 Message Queue,因為它無法將任務發送到另一台伺服器上的 Worker 去執行。 不能用 Message Queue 來代替 async/await,如果只是想在單一請求中非阻塞地查詢一下資料庫,引入 MQ 會是極其笨重和錯誤的設計。 不能用 setTimeout 來實現 async/await 的併發管理,也無法用它實現 MQ 的可靠任務處理。 它們是解決不同問題的工具,一個成熟的系統通常會同時使用它們: 在一個使用 FastAPI (async/await) 的 Web 服務中,當收到使用者請求後,它會非阻塞地從資料庫查詢一些資料,然後將一個耗時的任務(如生成報告)透過 Celery (Message Queue) 發送到背景 Worker,並立即返回響應給使用者。 下次當思考如何優化應用時,不妨問問自己:我需要的,是一位懂得時間管理的多工主廚,一個連接所有分店的中央廚房系統,還是一個簡單可靠的廚房計時器?

我們每天都在打造各式各樣的 Web 應用,從電商網站到社群平台。專注於功能、效能和使用者體驗,但常常忽略一個潛伏在暗處的威脅——CSRF (Cross-Site Request Forgery)。 你可能聽過這個名詞,但真的了解它是如何運作的,以及它會帶來多大的危害嗎?這篇文章將用一個簡單的故事,搞懂 CSRF 是什麼、它想解決什麼問題,以及如何有效地防禦它。 一個關於「代簽」的故事:什麼是 CSRF? 想像一下,你是一位公司大老闆,在你的銀行網站(my-bank.com)上登入了,瀏覽器就像一位忠誠的秘書,手上拿著你的「身份憑證」(Session Cookie),可以幫你處理各種事務。 這時,一位心懷不軌的攻擊者寄了一封有趣的 Email 給你,標題是「快來看超可愛的貓咪動圖!」。你好奇地點開了連結,進入了一個看似無害的網站(evil-cat-gifs.com)。 你不知道的是,這個網站的 HTML 裡藏了一段惡意的程式碼,例如一個看不見的表單: <!-- evil-cat-gifs.com 的惡意程式碼 --> <body onload="document.forms[0].submit()"> <form action="https://my-bank.com/api/transfer" method="POST" style="display:none;"> <input type="hidden" name="to" value="Mallory" /> <input type="hidden" name="amount" value="100000" /> </form> </body> 當你的瀏覽器載入這個頁面時,onload 事件觸發了 JavaScript,自動提交了這個隱藏的表單。 接下來發生的事,就是 CSRF 的核心: 請求發送: 瀏覽器向 https://my-bank.com/api/transfer 發送了一個 POST 請求。 憑證自動附加: 因為這個請求是發往 my-bank.com 的,你的瀏覽器秘書很「貼心」地把之前儲存的 my-bank.com 的身份憑證(Cookie)一起附上。 伺服器驗證: my-bank.com 的伺服器收到了這個請求。它檢查了 Cookie,發現身份憑證是合法的——「嗯,是大老闆本人沒錯!」 執行操作: 伺服器信任了這個請求,因為它通過了身份驗證。於是,它執行了轉帳操作,將 10 萬元轉給了攻擊者。 整個過程中,你只是點開了一個貓咪網站,錢就不翼而飛了。完全被蒙在鼓裡,因為這個請求是在不知情的情況下,被偽造並從你的瀏覽器發送出去的。 這就是 跨站請求偽造 (Cross-Site Request Forgery)。攻擊者利用了你已登入的狀態,誘騙瀏覽器發送一個非你本意的請求。 CSRF 想要解決的核心問題是什麼? 從上面的故事我們可以總結出 CSRF 攻擊成功的兩個關鍵因素: 攻擊利用了瀏覽器在發送跨站請求時,會自動攜帶目標網站 Cookie 的特性。 受害網站的伺服器無法區分一個請求是來自於使用者在自家網站上的真實點擊,還是來自於第三方惡意網站的偽造請求。 因此,CSRF 防禦機制想要解決的核心問題是:如何驗證一個請求的「意圖」而非僅僅是「身份」? 伺服器只驗證 Cookie,等於只問:「你是誰?」。答案是:「我是已登入的使用者」。這沒錯,但伺服器還需要問一個更重要的問題:「這個操作真的是你本人想要執行的嗎?」 CSRF 防禦的目標,就是讓伺服器有能力驗證後者,確保每一個會改變狀態的請求(如新增、修改、刪除)都是源自於使用者在我們自己網站上的真實意圖。 如何防禦 CSRF?——給請求一個「暗號」 既然問題在於伺服器無法驗證請求的來源與意圖,那我們就給它一個方法來驗證。以下是目前最主流且有效的防禦策略。 1. Anti-CSRF Token (同步權杖模式) 這是最經典也最可靠的防禦方式。 原理: 伺服器生成 Token: 當使用者登入或訪問一個包含表單的頁面時,伺服器會生成一個獨一無二、無法被預測的隨機字串,稱為「CSRF Token」。伺服器會將這個 Token 儲存在伺服器端的 Session 中。 前端嵌入 Token: 伺服器將這個 Token 傳遞給前端,前端在渲染頁面時,將它作為一個隱藏欄位嵌入到表單中。<form action="/update-profile" method="POST"> <input type="text" name="email" value="user@example.com"> <!-- 嵌入由伺服器給予的 Token --> <input type="hidden" name="csrf_token" value="aBcDeFgHiJkLmNoPqRsTuVwXyZ123456"> <button type="submit">更新資料</button> </form> 伺服器驗證 Token: 當使用者提交表單時,這個 csrf_token 會一起被送到伺服器。伺服器在處理請求前,會比較「表單送來的 Token」和「儲存在 Session 中的 Token」是否一致。 為什麼有效? 攻擊者在 evil-cat-gifs.com 上無法得知這個 csrf_token 的值。因為瀏覽器的同源政策 (Same-Origin Policy) 會阻止惡意網站的腳本讀取 my-bank.com 的頁面內容,自然也就偷不到這個 Token。沒有正確的 Token,偽造的請求就會被伺服器拒絕。 2. SameSite Cookie 屬性 這是一種更現代、從瀏覽器層面進行防禦的策略。SameSite 是 HTTP Cookie 的一個屬性,用來告訴瀏覽器在跨站請求中是否應該攜帶 Cookie。 它有三個值: Strict: 最嚴格。完全禁止瀏覽器在任何跨站請求中攜帶 Cookie。例如,就算你從 Google 搜尋結果點擊連結到你的網站,如果你的 Cookie 是 SameSite=Strict,你也會是登出狀態。 Lax (常用預設值): 較寬鬆。允許在一些安全的頂層導航(如點擊連結 <a href="...">)中攜帶 Cookie,但會阻止在 POST、PUT、DELETE 等可能改變狀態的跨站請求中攜帶 Cookie。這恰好能防禦我們故事中那個 POST 表單的 CSRF 攻擊。 None: 關閉 SameSite 限制。瀏覽器會在所有跨站請求中攜帶 Cookie,但必須同時設定 Secure 屬性(即只能在 HTTPS 下傳輸)。 如何使用? 在設定 Cookie 時,加上 SameSite 屬性即可。 Set-Cookie: session_id=...; SameSite=Lax; HttpOnly; Secure 將 SameSite 設定為 Lax 或 Strict 是非常有效的 CSRF 防禦手段。目前主流瀏覽器已將 Lax 作為預設值,這大大降低了 CSRF 的風險。但為了兼容舊版瀏覽器和增加防禦深度,建議將 SameSite Cookie 與 Anti-CSRF Token 結合使用。 3. 檢查來源 (Origin/Referer Header) 伺服器也可以檢查 HTTP 請求頭中的 Origin 或 Referer 欄位,確保請求是從我們自己的網站域名發出的。 Referer:表示請求的來源頁面 URL。 Origin:更安全,只包含來源的域名,不含路徑。 為什麼不建議單獨使用? 可靠性問題: Referer 可能因為使用者隱私設定或代理伺服器而被移除。 安全性問題: 在某些舊瀏覽器或特定情況下,Referer 可能被偽造。 因此,它可以作為輔助的防禦手段,但不應作為唯一的防線。 結論 CSRF 是什麼? 一種攻擊,誘騙已登入使用者的瀏覽器,在他們不知情的情況下,向目標網站發送惡意的偽造請求。 它想解決什麼問題? 解決伺服器無法驗證請求意圖的問題,確保狀態變更的請求確實源自使用者在我們網站上的真實操作。 如何防禦? 首選方案: 使用 Anti-CSRF Token,為每個請求加上只有伺服器和合法前端才知道的「暗號」。 強力輔助: 設定 Cookie 的 SameSite=Lax 或 SameSite=Strict 屬性,從瀏覽器層面阻斷惡意請求。 縱深防禦: 結合上述兩者,並可考慮檢查 Origin Header,建立多層次的安全防護。 Web 安全是一個攻防不斷演進的領域。理解像 CSRF 這樣的經典漏洞,不僅能幫助我們寫出更安全的程式碼,更能培養我們「預設不信任」的安全思維。希望這篇文章能幫助你鞏固對 CSRF 的理解,並在你的下一個專案中,自信地築起堅固的防線!

在現代軟體開發中,持續整合(Continuous Integration, CI)已經不是一個「選項」,而是一個「必需品」。它能確保我們每次提交的程式碼都經過自動化測試,及早發現問題,從而提升整個團隊的開發品質與效率。對於專案來說,一個好的 CI 流程更是不可或缺。 我們將深入解析一個為 Django 專案設計的 GitHub Actions 工作流程。這個流程不僅涵蓋了基本的測試,還巧妙地結合了原生 Python 環境與 Docker Compose 環境,實現了快速回饋與高擬真度測試的雙重保障。 GitHub Actions 核心概念 在深入程式碼之前,先快速了解幾個 GitHub Actions 的核心概念: Workflow(工作流程): 由一個或多個 job 組成,定義了整個自動化過程。通常以 YAML 檔案形式存放在專案的 .github/workflows/ 目錄下。 Event(事件): 觸發工作流程的特定活動,例如 push(推送到儲存庫)或 pull_request(發起拉取請求)。 Job(任務): 工作流程中的一個執行單元,會在一台虛擬機(runner)上執行。一個工作流程可以有多個任務,它們可以並行或依序執行。 Step(步驟): 任務中的一個獨立指令或動作。一個任務由多個步驟組成。 Action(動作): 可重用的程式碼單元,是建構步驟的積木。例如 actions/checkout 就是一個官方提供的 Action,用來拉取你的程式碼。 工作流程檔案解析 (Django CI Tests) 讓我們來看看這次的主角,這個精心設計的 Django CI Tests 工作流程。 name: Django CI Tests on: [push, pull_request] jobs: # ... 兩個 jobs 的定義將在下面詳細解說 ... name: Django CI Tests:為這個工作流程取一個易於辨識的名字。 on: [push, pull_request]:這行是關鍵!它定義了觸發條件。這意味著,每當有新的程式碼被推送到任何分支,或者當有人發起一個 Pull Request 時,這個自動化測試流程就會被啟動。 這個工作流程包含了兩個並行執行的任務(Jobs):native-pytest 和 docker-compose-pytest。讓我們逐一拆解它們的設計理念與實現細節。 任務一:native-pytest - 追求極速的原生 Python 測試 第一個任務的目標是「快」。它在一個乾淨的 Ubuntu 環境中直接設定 Python,並執行測試。這適用於那些不依賴外部服務(如資料庫、快取)的單元測試,能夠在最短時間內給予開發者回饋。 native-pytest: name: Native Python Pytest runs-on: ubuntu-latest defaults: run: working-directory: backend env: SECRET_KEY: ${{ secrets.SECRET_KEY }} DEBUG: "True" USE_POSTGRES: "False" # 關鍵點:不使用 PostgreSQL ALLOWED_HOSTS: localhost,127.0.0.1 SOCIAL_AUTH_GOOGLE_CLIENT_ID: ${{ secrets.SOCIAL_AUTH_GOOGLE_CLIENT_ID }} SOCIAL_AUTH_GOOGLE_CLIENT_SECRET: ${{ secrets.SOCIAL_AUTH_GOOGLE_CLIENT_SECRET }} steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.13" - name: Install uv run: pip install uv - name: Install backend dependencies run: uv sync - name: Run migrations run: uv run python manage.py migrate - name: Run pytest run: uv run pytest -v --tb=short --maxfail=5 --disable-warnings 拆解步驟: 基本設定: runs-on: ubuntu-latest: 指定任務運行在最新版的 Ubuntu 虛擬機上。 defaults.run.working-directory: backend: 這是一個很方便的設定,它將後續所有 run 指令的預設工作目錄都設定為 backend,省去了在每個步驟中都 cd backend 的麻煩。 環境變數 (env): 這裡設定了 Django 運作所需的環境變數。 SECRET_KEY 等敏感資訊透過 ${{ secrets.SECRET_KEY }} 從 GitHub 的 Secrets 中安全地讀取,避免了硬編碼在程式碼中。 最重要的設定是 USE_POSTGRES: "False"。這通常會告訴 Django 的 settings.py 使用輕量級的 SQLite 作為測試資料庫,因為它不需要額外的服務,啟動速度極快。 執行步驟 (steps): actions/checkout@v4: 第一步總是它!這個 Action 會將你的專案程式碼拉取到虛擬機中。 setup-python@v5: 設定指定的 Python 版本(這裡用了最新的 3.13!)。 Install uv & uv sync: 這裡使用 uv 這個新一代的超高速 Python 套件管理器來取代傳統的 pip。uv sync 會根據 pyproject.toml 或 requirements.txt 檔案來安裝所有依賴,速度飛快。 uv run python manage.py migrate: 在執行測試前,先執行資料庫遷移。對於 SQLite 來說,這會在記憶體或一個暫存檔案中建立資料庫結構。 uv run pytest ...: 核心步驟!uv run 會在 uv 管理的虛擬環境中執行 pytest。 -v: 顯示詳細的測試結果。 --tb=short: 使用簡潔的錯誤追蹤格式。 --maxfail=5: 當有 5 個測試失敗時就立即停止,節省時間。 --disable-warnings: 隱藏不影響功能的警告,讓輸出更乾淨。 小結:native-pytest 的優勢是速度快、設定簡單,適合快速驗證程式碼邏輯。 任務二:docker-compose-pytest - 模擬生產環境的高擬真度測試 第二個任務的目標是「擬真」。它使用 Docker Compose 來啟動整個應用程式環境,包括 Django 應用本身和 PostgreSQL 資料庫。這確保了測試環境與你的開發環境,甚至生產環境,都盡可能地保持一致。 docker-compose-pytest: name: Docker Compose Pytest runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Create backend .env file run: | echo "SECRET_KEY=${{ secrets.SECRET_KEY }}" > backend/.env # ... 其他 echo 指令 ... echo "USE_POSTGRES=True" >> backend/.env echo "POSTGRES_HOST=db" >> backend/.env # ... - name: Build docker-compose services run: docker compose -f docker-compose.dev.yml build - name: Run pytest in docker-compose run: docker compose -f docker-compose.dev.yml run --rm backend uv run pytest -v --tb=short --maxfail=5 --disable-warnings - name: Check container logs on failure if: failure() run: docker compose -f docker-compose.dev.yml logs backend - name: Shutdown docker-compose run: docker compose -f docker-compose.dev.yml down -v 拆解步驟: 建立 .env 檔案: 這是此任務中最巧妙的一步。Docker Compose 通常會讀取一個 .env 檔案來設定容器的環境變數。 這個步驟動態地建立了一個 backend/.env 檔案,並將 GitHub Secrets 和其他設定值寫入其中。 注意這裡的關鍵差異:USE_POSTGRES 被設為 "True",並且新增了所有 POSTGRES_* 相關的變數,例如資料庫主機 POSTGRES_HOST=db(db 是 docker-compose.dev.yml 中定義的資料庫服務名稱)。 建置與執行 Docker Compose: docker compose ... build: 根據 docker-compose.dev.yml 和相關的 Dockerfile 來建置服務的映像檔。 docker compose ... run --rm backend ...: 這是執行測試的核心指令。 run backend: 告訴 Docker Compose 啟動 backend 服務(以及它所依賴的 db 服務),並在 backend 容器內執行後續的指令。 --rm: 表示指令執行完畢後,自動刪除這個臨時建立的容器,保持環境乾淨。 後面的 uv run pytest ... 指令與任務一相同,但這次,它是在一個包含完整資料庫連線的 Docker 容器內執行的。 偵錯與清理: if: failure(): 這是 GitHub Actions 的一個強大功能。這個步驟只會在前面的步驟失敗時才會執行。 docker compose ... logs backend: 如果測試失敗,這個指令會印出 backend 容器的日誌,這對於排查連線問題或環境錯誤非常有幫助。 docker compose ... down -v: 無論成功或失敗,最後都要執行清理工作。down 會關閉並移除所有容器和網路,-v 則會一併刪除相關的 volumes(例如資料庫數據),確保每次執行都是從零開始的乾淨狀態。 小結:docker-compose-pytest 的優勢是環境擬真度高,能測試應用與資料庫等外部服務的整合,確保程式碼在類生產環境下也能正常運作。 為什麼需要兩種測試策略? 這個工作流程最精彩的設計,就是同時採用了這兩種策略: 快速回饋:開發者推送一個小修改後,native-pytest 會在 1-2 分鐘內給出結果。如果只是修改了一個不涉及資料庫的函式,可以很快知道有沒有改壞。 深度驗證:docker-compose-pytest 雖然慢一些(可能需要 3-5 分鐘),但它提供了更全面的保障。它能捕捉到原生測試無法發現的問題,例如錯誤的資料庫查詢、環境變數設定錯誤等。 當你在 GitHub 上看到一個 Pull Request 時,這兩個任務會並行執行。你將會看到兩個綠色的勾勾,這代表你的程式碼不僅通過了快速的單元測試,也通過了嚴苛的整合測試,讓你有十足的信心去合併它。 總結 透過這個 Django CI Tests 工作流程,學習到如何利用 GitHub Actions 建立一個高效且可靠的自動化測試流程。它結合了: 事件驅動的自動化 (on: [push, pull_request])。 使用 GitHub Secrets 安全管理敏感資訊。 利用原生環境實現快速測試。 利用 Docker Compose 模擬生產環境進行整合測試。 巧妙的條件執行 (if: failure()) 來輔助偵錯。 將這樣的 CI 流程整合到專案中,無疑會大大提升開發品質與部署信心!

在設計一套後端系統時,經常會遇到一個關鍵抉擇:使用同步(synchronous)還是非同步(asynchronous)伺服器模型? 這個選擇不只是語法風格的不同,更直接影響: 效能與併發能力(Concurrency & Throughput) 系統架構與部署方式 開發難度與維運策略 本文將以實務角度,帶你全面了解兩者的差異、優劣與適用場景。 同步伺服器(Synchronous Server) 運作方式 同步伺服器在處理請求時會「一條請求、一條線程(或進程)」,並且整個執行流程(例如存取資料庫、外部 API、檔案)是阻塞式的。 def handle_request(): user = db.query_user() # 等待資料庫查詢完成 data = requests.get(API_URL) # 等待外部 API 回應 return render(data) 特點 特性 說明 阻塞式處理 一條線程被某個 I/O 任務卡住時,無法處理其他請求 簡單直覺 程式碼撰寫與除錯容易 資源密集 多請求需要多線程,記憶體與 CPU 成本高 常用框架 Django(WSGI)、Flask、Laravel、Spring MVC 非同步伺服器(Asynchronous Server) 運作方式 非同步伺服器採用 事件迴圈 + 協程 模型,能在等待 I/O 操作時釋出資源,去處理其他請求。核心在於 await 或事件回呼(callback)。 async def handle_request(): user = await db.fetch_user() # 非同步資料庫 data = await http.get(API_URL) # 非同步 HTTP return render(data) 特點 特性 說明 非阻塞處理 請求之間可共享同一事件迴圈,無需多條 thread 複雜度較高 需學習 async/await,debug 相對困難 高併發效率 大幅減少 thread 切換與記憶體使用 常用框架 FastAPI、Node.js、Django ASGI、Go、Vert.x 優缺點比較總表 項目 同步伺服器 非同步伺服器 開發容易度 ✅ 簡單 ❌ 複雜(需 async/await) 單請求效能 ✅ 穩定 ✅ 穩定 高併發表現 ❌ thread 限制 ✅ 優異 記憶體使用 ❌ 高 ✅ 低 長連線支援(WebSocket) ❌ 不適合 ✅ 適合 舊程式碼相容性 ✅ 高 ❌ async 套件有限 除錯追蹤 ✅ 易懂堆疊 ❌ StackTrace 被拆開 常見應用 CMS、內部系統、企業服務 API Gateway、聊天系統、即時平台 案例分析:同時處理 1000 個請求 同步伺服器(如:Django + Gunicorn) 每個請求綁一條 thread(或 worker) 等待資料庫或 API 期間 thread 閒置,但資源仍被佔用 若有 1000 個請求 → 需要 1000 條 thread,否則排隊 非同步伺服器(如:FastAPI + Uvicorn) 所有請求共享事件迴圈 一旦遇到 await 就切去處理其他請求 少量 thread 即可服務大量併發請求 常見誤解釐清 Q:前端用 async/await,後端也要非同步伺服器嗎? A:不一定。前端非同步只是 UI 不被卡住,與後端架構無關。但若請求量大,後端 async 可顯著減少資源浪費。 Q:非同步伺服器無敵嗎? A:不是。對 CPU-bound 任務(如報表、壓縮、加解密),async 無助效能,反而需用多進程處理。 Q:用的是同步框架(如 Flask),怎麼辦? A:可透過多進程(如 Gunicorn)擴展,或漸進遷移至 async 架構。 實務建議:哪時候該選哪個? 情境 建議 網站用戶量不高、以 CRUD 為主 同步伺服器(Flask、Django) API Gateway、大量查詢或等待外部資源 非同步伺服器(FastAPI、Node.js) 即時互動(聊天室、推播、遊戲) 非同步 + 長連線(WebSocket)架構 有舊系統整合需求、需穩定維運 同步架構保守推進 總結 結論 同步架構簡單、穩定,適合中小型應用與企業內部系統 非同步適合高併發、I/O 密集、WebSocket 等場景 現代框架(如 FastAPI、Django 4+)皆支援「同步 + 非同步混合」,可漸進導入 選擇前先分析瓶頸,是 CPU-bound 還是 I/O-bound 「同步好開發、非同步好擴展」,選擇要根據實際需求、開發人力與併發目標靈活判斷!

Nginx 是現代 Web 架構中不可或缺的高效能反向代理伺服器。它能處理靜態檔案、反向代理、負載平衡、快取與安全性強化等多種任務。以下以 Django 專案為例,說明 Nginx 的功能與設定檔設計。 Nginx 的主要功能 反向代理 將外部請求轉發給後端應用(如 Gunicorn + Django),隱藏內部架構,提升安全性與彈性。 靜態檔案服務 直接由 Nginx 回應 CSS、JS、圖片等靜態檔案,減輕後端負擔,加速回應速度。 快取與壓縮 可設定快取策略(如 Cache-Control、expires),提升效能並減少頻寬消耗。 負載平衡 將請求分配到多個後端服務,提升可用性與擴展性。 安全性強化 可設定防火牆、限制來源、阻擋惡意請求、加強 HTTP 標頭等。 Nginx 生產環境設定(prod.conf)範例 server { listen 80; server_name shr.today www.shr.today; location /static/ { alias /staticfiles/; expires 7d; add_header Cache-Control "public"; } location / { proxy_pass http://backend:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; } } 設定重點說明 listen 80; 監聽 80 port,處理 HTTP 請求。 server_name shr.today www.shr.today; 指定服務的網域名稱。 location /static/ { ... } alias /staticfiles/;:將 /static/ 路徑對應到伺服器上的 /staticfiles/ 目錄(通常由 Django collectstatic 輸出)。 expires 7d;:設定瀏覽器快取靜態檔案 7 天。 add_header Cache-Control "public";:允許快取。 location / { ... } proxy_pass http://backend:8000;:反向代理所有非靜態請求到後端 Django 服務(如 Gunicorn)。 其他 proxy_set_header:傳遞原始請求資訊給後端,方便日誌與安全性判斷。 Nginx 開發環境設定(dev.conf)範例 server { listen 80; server_name localhost 127.0.0.1; location / { proxy_pass http://backend:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } 說明 listen 80; 監聽本機 80 port,處理 HTTP 請求。 server_name localhost 127.0.0.1; 僅允許本機或 127.0.0.1 的請求,適合開發測試。 location / { ... } proxy_pass http://backend:8000; 將所有請求轉發到本機的 Django 服務(通常由 Gunicorn 或 runserver 啟動)。 其他 proxy_set_header 傳遞原始請求資訊給後端,方便日誌與除錯。 未設定靜態檔案服務 開發時通常由 Django 直接服務 static 檔案,Nginx 不需額外處理。 Nginx 負載平衡設定範例 當網站流量增加時,可用 Nginx 的負載平衡功能,將請求分散到多個 Django 應用實例: upstream django_app { server backend1:8000; server backend2:8000; server backend3:8000; } server { listen 80; server_name shr.today www.shr.today; location /static/ { alias /staticfiles/; expires 7d; add_header Cache-Control "public"; } location / { proxy_pass http://django_app; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; } } upstream django_app { ... }:定義多個後端服務,Nginx 會自動分配請求。 proxy_pass http://django_app;:將請求分流到多個後端。 實務建議 生產環境:靜態檔案務必交給 Nginx 處理,並設定合理快取。高流量時建議啟用負載平衡。 開發環境:可簡化設定,讓 Django 直接服務 static,方便開發測試。 安全性:可進一步加上 HTTPS、限制來源、加強 header 等。 結論 Nginx 能大幅提升專案的效能、可用性與安全性。 理解設定檔的每一段功能,包含靜態檔案服務、反向代理與負載平衡,有助於打造穩定、可維護且可擴展的生產環境。

在現代 Python 專案中,維持一致的程式碼風格與自動排版已成為團隊協作的基本需求。Ruff 是一套超快速、全方位的 Python linter 與 formatter,能同時做到靜態檢查、import 排序與自動格式化。 本文將介紹 Ruff 的安裝、基本設定與常見用法,以及如何在 VS Code 開發環境下,讓 Ruff 在每次儲存檔案時自動執行,確保團隊程式碼品質一致。 Ruff 是什麼? Ruff 是用 Rust 開發的 Python 靜態分析工具,主打「快」與「多合一」: Linter:檢查語法錯誤、潛在 bug、PEP8 風格等 Formatter:自動排版、統一引號、行寬等 Import 排序:自動整理 import 順序 如何安裝 Ruff 可以直接用 pip 安裝: pip install ruff 如果專案有 pyproject.toml,也可以直接在 dev dependencies 加入: [dependency-groups] dev = [ "ruff>=0.12.0", ] 設定 Ruff:pyproject.toml Ruff 支援在 pyproject.toml 設定,常見設定如下: [tool.ruff] select = ["E", "F", "I"] # 啟用基本錯誤檢查與 import 排序 exclude = ["venv", ".venv", "__pycache__", "migrations"] # 排除目錄 line-length = 88 target-version = "py311" fix = true # 預設自動修復違規 [tool.ruff.format] quote-style = "single" # 統一用單引號 line-ending = "lf" # 換行符號 [tool.ruff.isort] combine-as-imports = true # 排序 import 時合併 as 語法 VS Code 設定:儲存時自動執行 Ruff 安裝 Python 與 Ruff 擴充套件 安裝 Python extension for VS Code 安裝 Ruff extension for VS Code 編輯 .vscode/settings.json 在專案根目錄建立或編輯 .vscode/settings.json,加入: { "python.formatting.provider": "ruff", "editor.formatOnSave": true, "ruff.enable": true, "ruff.formatOnSave": true, "python.linting.enabled": true, "python.linting.ruffEnabled": true } 若同時有安裝 black/autopep8/yapf,請確保 python.formatting.provider 設為 "ruff"。 實際效果 每次儲存 Python 檔案時,Ruff 會自動格式化、排序 import、修正違規 所有格式與靜態檢查規則都依 pyproject.toml 設定 團隊協作時,程式碼風格一致,減少 review friction 常用指令 自動格式化所有 Python 檔案 ruff format . 檢查並自動修正違規 ruff check . --fix 只檢查,不修正 ruff check . 結論 Ruff 結合 VS Code 的自動化設定,能大幅提升 Python 專案的開發效率與品質。 只要簡單設定,從此不再為格式與 import 排序煩惱!
0%