在軟體開發與維運的漫漫長路中,效能問題如同一隻隻難以捉摸的幽靈,時而顯現,時而隱匿。傳統的效能分析(Profiling)方法,往往像是在特定時間點拍攝一張快照,雖能捕捉到瞬間的狀態,卻難以洞察在真實生產環境中,那些與用戶負載、時間推移相關的效能瓶頸。為了解決這個困境,持續性效能分析(Continuous Profiling) 的理念應運而生。
它主張對應用程式進行全天候、低開銷的效能監控,將無數個效能快照匯集成一部紀錄片,讓我們得以隨時回溯,精準定位任何時間點的效能瓶頸。Grafana Pyroscope 正是實現此理念的佼佼者,它是一個開源的持續性效能分析平台,能持續收集、儲存並以火焰圖(Flame Graph) 的形式,直觀地視覺化應用程式的效能數據。
今天,我們將從零開始,親手搭建一個完整的實驗環境,感受 Pyroscope 如何為我們揭示程式碼的效能秘密。
首先,我們需要一個「被觀察」的對象。這裡,我們編寫一個簡單的 Python 應用程式,它會模擬不同的工作負載。建立一個名為 main.py 的檔案,並填入以下內容:
import os
import time
import threading
import pyroscope
# 從環境變數初始化 Pyroscope
pyroscope.configure(
    application_name="my.python.app", # 應用名稱,將顯示在 Grafana 中
    server_address=os.environ.get("PYROSCOPE_SERVER_ADDRESS", "http://pyroscope:4040"),
    enable_logging=True,
)
def slow_function():
    """一個模擬耗時較長的操作"""
    time.sleep(0.1)
def fast_function():
    """一個模擬耗時較短的操作"""
    time.sleep(0.05)
def work(tags):
    """主要的工作負載,會呼叫其他函式"""
    # 使用 tag_wrapper 為這個執行緒的效能數據打上標籤
    with pyroscope.tag_wrapper(tags):
        while True:
            fast_function()
            slow_function()
            print(f"Task with tags {tags} finished a cycle.")
            time.sleep(1) # 每秒執行一次循環
if __name__ == "__main__":
    print("Starting Python application with continuous profiling...")
    # 建立並啟動一個執行緒,標記為 "fast-lane"
    threading.Thread(target=work, args=({"speed": "fast"},)).start()
    # 建立並啟動另一個執行緒,標記為 "slow-lane"
    threading.Thread(target=work, args=({"speed": "slow"},)).start()
這個腳本的精華在於 pyroscope.configure 初始化了分析工具,並透過 pyroscope.tag_wrapper 為不同的執行緒(我們用以模擬快、慢兩種任務)打上了 speed="fast" 和 speed="slow" 的標籤。這將在後續的分析中展現出強大的威力。
為了讓這個 Python 應用能順利執行,我們還需要告訴 Python 它依賴哪些套件。建立一個名為 requirements.txt 的檔案,內容僅需一行:
pyroscope-io
接下來,我們使用 Docker Compose 來編排我們的觀測環境,它將包含 Pyroscope 伺服器、我們剛才設計的 Python 應用,以及用於視覺化的 Grafana。建立一個名為 docker-compose.yml 的檔案:
version: '3.8'
services:
  grafana:
    image: grafana/grafana:10.2.0
    ports:
      - "3000:3000"
    volumes:
      - ./grafana-provisioning/datasources:/etc/grafana/provisioning/datasources
    environment:
      - GF_AUTH_ANONYMOUS_ENABLED=true
      - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
      - GF_AUTH_DISABLE_LOGIN_FORM=true
    networks:
      - loki
  pyroscope:
    image: grafana/pyroscope:1.1.0
    ports:
      - "4040:4040"
    networks:
      - loki
  app:
    image: python:3.9-slim
    volumes:
      - ./app:/app
    working_dir: /app
    command: pip install -r requirements.txt && python main.py
    environment:
      - PYROSCOPE_SERVER_ADDRESS=http://pyroscope:4040
    depends_on:
      - pyroscope
    networks:
      - loki
networks:
  loki:
    driver: bridge
這個設定檔定義了三個服務,並讓它們共享一個名為 loki 的網路,以便互相通訊。
為了讓 Grafana 一啟動就能自動連接到 Pyroscope,我們利用 Grafana 的 Provisioning(自動化佈建)功能。我們需要建立一個設定檔,告訴 Grafana Pyroscope 伺服器的位置。
請建立如下的路徑與檔案:grafana-provisioning/datasources/datasource.yml,並填入以下內容:
apiVersion: 1
datasources:
  - name: Pyroscope
    type: grafana-pyroscope-datasource
    access: proxy
    url: http://pyroscope:4040
    isDefault: true
    version: 1
    editable: false
萬事俱備!現在,請將 main.py 和 requirements.txt 放在名為 app 的子目錄中,並將 datasource.yml 放在 grafana-provisioning/datasources 的路徑下。你的目錄結構看起來應該像這樣:
. (你的工作目錄)
├── docker-compose.yml
├── app/
│   ├── main.py
│   └── requirements.txt
└── grafana-provisioning/
    └── datasources/
        └── datasource.yml
在你的工作目錄下,執行以下指令啟動所有服務:
docker-compose up -d
等待片刻,待 Docker 下載映像檔並啟動容器後,我們的實驗就正式開始了。
http://localhost:3000。my.python.app,然後點擊「Run query」。螢幕上出現的,就是著名的「火焰圖」。圖中每個長條代表一個函式,長條的寬度與它佔用的 CPU 時間成正比。透過這張圖,slow_function 佔用了更多時間的秘密便一目了然。speed,然後在「Value」中選擇 fast。再次查詢,火焰圖便只會顯示 speed="fast" 那個執行緒的效能數據。切換到 slow,你就能精準比較兩種不同任務的效能開銷。恭喜你!今天,你不僅學會了如何搭建一個持續性效能分析環境,更親身體驗了如何像偵探一樣,利用 Pyroscope 和火焰圖,從程式碼的蛛絲馬跡中找出效能的真相。將這項技術融入你的開發流程,將賦予你一雙洞察系統瓶頸的火眼金睛。