App Chạy Chậm — Nhưng Chậm Ở Đâu?
Bạn đã có metrics từ Prometheus. Có traces từ Jaeger. Có logs từ Loki. Vậy mà khi service production đột ngột vọt lên 90% CPU lúc 2 giờ sáng, không công cụ nào trong số đó cho bạn biết hàm nào đang ngốn tài nguyên. Đó chính là khoảng trống mà continuous profiling lấp đầy.
Tôi từng gặp đúng vấn đề này khi quản lý một Go microservice xử lý batch job. Memory cứ tăng dần — không đủ nhanh để kích hoạt cảnh báo, nhưng đều đặn đến mức pod phải restart mỗi vài ngày. Prometheus cho thấy RSS tăng. Log không có gì bất thường. Phải mất ba ngày chụp pprof snapshot thủ công, tôi mới tìm ra goroutine leak ẩn sâu trong một thư viện bên thứ ba. Sau đó, continuous profiling trở thành thứ bắt buộc trong mọi Kubernetes setup production của tôi.
Grafana Pyroscope là một nền tảng continuous profiling mã nguồn mở. Nó liên tục lấy mẫu call stack và tạo flame graph từ các ứng dụng đang chạy 24/7 — không cần chụp snapshot thủ công. Pyroscope tích hợp với observability stack của Grafana và hỗ trợ Go, Java, Python, Ruby và nhiều ngôn ngữ khác.
Tại Sao Profiling Truyền Thống Không Đủ Dùng trên Kubernetes
Quy trình profiling cổ điển thường như sau: tái hiện sự cố ở local, gắn profiler vào, chụp snapshot, phân tích. Ổn khi làm trên máy developer. Nhưng trên Kubernetes, cách này nhanh chóng thất bại:
- Pod có vòng đời ngắn — khi bạn phát hiện sự cố, pod thủ phạm có thể đã restart và biến mất.
- Spike khó tái hiện — traffic pattern trên production rất khó mô phỏng lại. Spike xảy ra lúc 2 giờ sáng với tải thực tế sẽ không xuất hiện trên máy local của bạn.
- Nhiều replica — một bug về hiệu năng có thể chỉ xuất hiện ở một trong mười pod replica, khiến profiling thủ công chẳng khác nào đoán mò.
- Không có dữ liệu lịch sử — nếu không có continuous profiling, bạn chỉ thấy được tình trạng hiện tại, chứ không biết điều gì đã xảy ra trước đó.
Pyroscope giải quyết tất cả những vấn đề này bằng cách chạy một agent nhẹ (hoặc dùng eBPF) để liên tục lấy mẫu stack trace và đẩy về một kho lưu trữ trung tâm. Bạn có thể truy vấn bất kỳ khung thời gian nào, so sánh trước và sau khi deploy, hoặc đối chiếu CPU spike với đúng call stack gây ra nó.
Các Khái Niệm Cơ Bản Trước Khi Triển Khai
Flame Graph
Flame graph là dạng biểu đồ trong đó mỗi thanh đại diện cho một lần gọi hàm. Chiều rộng của thanh tương ứng với lượng thời gian CPU (hoặc memory) mà hàm đó tiêu tốn, bao gồm tất cả các hàm được gọi bên trong. Thanh càng rộng, bottleneck càng lớn. Pyroscope tạo flame graph liên tục, cho bạn luôn có dữ liệu lịch sử để truy vấn.
Pull Mode và Push Mode
Có hai chế độ hoạt động. Ở pull mode, Pyroscope server chủ động scrape endpoint /debug/pprof của ứng dụng — native với Go, không cần instrumentation. Ở push mode, bạn tích hợp Pyroscope SDK vào app để tự đẩy profile theo lịch của nó. Với Go và Java, pull mode là điểm khởi đầu dễ nhất.
eBPF Profiling
Không thể hoặc không muốn instrumentation app? Pyroscope có thể dùng eBPF để profile ở cấp kernel. Cách này hoạt động với mọi ngôn ngữ mà không cần thay đổi code, nhưng yêu cầu một DaemonSet có đặc quyền cao. Đặc biệt hữu ích cho các service C/C++ hoặc khi bạn muốn có visibility trên toàn bộ node.
Triển Khai Pyroscope trên Kubernetes
Yêu Cầu
- Kubernetes 1.24+ cluster
- Helm 3.x
- kubectl đã cấu hình
- Một Grafana instance đang chạy (hoặc deploy cùng với Pyroscope)
Cài Đặt qua Helm
Thêm Grafana Helm chart repository và deploy Pyroscope vào một namespace riêng:
helm repo add grafana https://grafana.github.io/helm-charts
helm repo update
kubectl create namespace pyroscope
helm install pyroscope grafana/pyroscope \
--namespace pyroscope \
--set pyroscope.replicationFactor=1 \
--set minio.enabled=true
MinIO được đóng gói sẵn trong chart mặc định để lưu trữ object ở local. Với môi trường production, hãy thay bằng S3, GCS, hoặc Azure Blob — đặt storage.backend=s3 (hoặc gcs / azure) cùng với thông tin xác thực bucket trong Helm values.
Kiểm Tra Sau Khi Deploy
kubectl get pods -n pyroscope
# NAME READY STATUS RESTARTS
# pyroscope-0 1/1 Running 0
# pyroscope-minio-0 1/1 Running 0
kubectl port-forward svc/pyroscope -n pyroscope 4040:4040
Mở http://localhost:4040 để truy cập trực tiếp vào Pyroscope UI.
Scrape Ứng Dụng Go (Pull Mode)
Nếu service Go của bạn đã import net/http/pprof, Pyroscope có thể scrape mà không cần thay đổi bất kỳ dòng code nào. Đây là một Go service tối giản để expose pprof:
package main
import (
"net/http"
_ "net/http/pprof" // đăng ký các handler /debug/pprof
)
func main() {
http.ListenAndServe(":8080", nil)
}
Tiếp theo, cấu hình Pyroscope để scrape app đó. Tạo file scrape-config.yaml:
scrapeConfigs:
- jobName: my-go-service
scrapeInterval: 15s
staticConfigs:
- targets:
- my-go-service.default.svc.cluster.local:8080
profilingConfig:
pprof:
enabled: true
path: /debug/pprof/
Áp dụng cấu hình này như một phần trong Pyroscope Helm values hoặc dưới dạng ConfigMap được mount vào Pyroscope pod.
Instrument Ứng Dụng Python (Push Mode)
Các service Python đẩy profile bằng Pyroscope SDK:
pip install pyroscope-io
import pyroscope
pyroscope.configure(
application_name="my-python-service",
server_address="http://pyroscope.pyroscope.svc.cluster.local:4040",
tags={
"env": "production",
"version": "1.2.3",
}
)
# Code ứng dụng của bạn chạy bình thường ở đây
# Pyroscope lấy mẫu ngầm ở background
Đừng bỏ qua dict tags. Các label đó là cách bạn lọc và so sánh profile về sau — ví dụ như diff version=1.2.2 với version=1.2.3 sau khi deploy để thấy chính xác điều gì bị regression.
Kết Nối Pyroscope với Grafana
Thêm Pyroscope làm data source trong Grafana:
- Vào Configuration → Data Sources → Add data source
- Tìm Grafana Pyroscope
- Đặt URL thành
http://pyroscope.pyroscope.svc.cluster.local:4040 - Nhấn Save & Test
Sau khi kết nối, mở view Explore, chọn data source Pyroscope, chọn ứng dụng của bạn và bắt đầu duyệt flame graph. Tính năng correlations của Grafana đưa điều này lên tầm cao hơn: kết nối Prometheus và Loki cùng với Pyroscope trong một panel duy nhất. Khi CPU spike xuất hiện, bạn nhấn thẳng từ metric sang flame graph — không cần chuyển tab, không cần đoán mò.
Triển Khai eBPF Agent dưới Dạng DaemonSet
Để profile mọi pod trên một node mà không phân biệt ngôn ngữ, hãy deploy Pyroscope eBPF agent:
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: pyroscope-ebpf
namespace: pyroscope
spec:
selector:
matchLabels:
app: pyroscope-ebpf
template:
metadata:
labels:
app: pyroscope-ebpf
spec:
hostPID: true
containers:
- name: pyroscope-ebpf
image: grafana/pyroscope-ebpf:latest # ghim vào phiên bản cụ thể trong production
securityContext:
privileged: true
env:
- name: PYROSCOPE_SERVER_ADDRESS
value: "http://pyroscope.pyroscope.svc.cluster.local:4040"
volumeMounts:
- name: host-sys
mountPath: /sys
readOnly: true
volumes:
- name: host-sys
hostPath:
path: /sys
privileged: true là bắt buộc với eBPF — không có cách nào khác. Hãy xem xét kỹ pod security policy của cluster trước khi triển khai trong môi trường dùng chung, và ghim tag image cụ thể thay vì dùng latest.
Đọc Flame Graph: Hướng Dẫn Thực Tế
Khi mở flame graph trong Pyroscope hoặc Grafana, hãy nhìn ngay vào những thanh rộng nhất — đó chính là bottleneck của bạn. Một số dạng pattern phổ biến cần lưu ý:
- Một thanh rộng duy nhất ở đỉnh call stack — một hàm đang tiêu tốn CPU không cân xứng. Bắt đầu điều tra từ đây.
- Nhiều thanh nhỏ cùng xuất phát từ một hàm cha — một hàm được gọi trong vòng lặp dày đặc. Thường caching hoặc batching sẽ giúp ích.
- GC frame chiếm hơn 10% CPU — trong Go hoặc Java, điều này thường có nghĩa là quá nhiều object có vòng đời ngắn được tạo ra trên hot path. Tìm những chỗ copy không cần thiết hoặc slice trung gian.
- I/O wait frame chiếm phần lớn — bottleneck không nằm ở CPU mà là latency. Flame graph sẽ chỉ ra database call hoặc HTTP request nào đang gây blocking.
Nên làm quen với diff view sớm. Deploy một bản release, rồi so sánh flame graph của nó với phiên bản trước. Bạn sẽ thấy chính xác điều gì đã thay đổi trong performance profile — không chỉ biết latency tăng 8%, mà còn biết hàm nào đang tiêu tốn CPU gấp ba lần so với trước.
Tổng Kết
Metrics cho bạn biết có gì đó đang sai. Traces cho bạn biết sai ở đâu trong vòng đời request. Profile cho bạn biết tại sao — những dòng code nào thực sự là thủ phạm. Pyroscope lấp đầy khoảng trống đó.
Việc cài đặt khá đơn giản: một lệnh Helm install, một scrape config hoặc vài dòng SDK, một Grafana data source. Lần tới khi pod ngốn CPU bất thường lúc 2 giờ sáng, bạn sẽ không cần đoán mò. Bạn sẽ có ngay flame graph chỉ ra hàm nào chịu trách nhiệm, cùng với dữ liệu lịch sử lưu lại nhiều ngày hoặc nhiều tuần — Pyroscope mặc định giữ lại 7 ngày, có thể cấu hình theo giới hạn storage của bạn.
Bắt đầu với pull mode cho các service Go hoặc Java. Đây là điểm khởi đầu dễ nhất, không cần instrumentation. Khi bạn đã tìm ra bottleneck thực sự đầu tiên từ flame graph, hãy mở rộng sang push mode cho Python hoặc thêm eBPF DaemonSet để có visibility toàn bộ node.

