Grafana + Loki + VictoriaMetrics: combo quan sát homelab bất bại
Ba con máy LAN, một cluster k3s, một đống app tự host. Mình dựng hệ quan sát bằng đúng ba món Grafana, Loki, VictoriaMetrics, và nó đã cứu mình một lần trong tuần đầu tiên.
Mở bài
Homelab của mình không lớn. Ba con máy trong LAN, một con chạy k3s chở hơn chục app tự host, một con chuyên database (MySQL, Redis, PostgreSQL), con còn lại làm điểm backup kiêm chạy vài service ngoài cluster.
Lâu nay chuyện hỏng hóc diễn ra đúng một kịch bản: hỏng, rồi phát hiện khi có người la, hoặc chính mình la khi mở app lên không thấy chạy. Xong SSH vào từng máy đọc log như điều tra hiện trường. Backup thì có chạy đêm, nhưng chết hôm nào không ai biết. Backup mà không báo lỗi là loại hỏng nguy hiểm nhất, vì nó cho mình cảm giác an toàn giả cho tới lúc cần restore.
Tuần trước mình quyết làm một hệ quan sát tử tế. Không phải kiểu cài Prometheus rồi xong. Phải đủ ba việc: nhìn thấy được mọi thứ ở một chỗ, được báo tin về Telegram khi có chuyện, và config phải nằm hết trong git chứ không phải đồ nhớ đầu trên từng máy.
Kết quả là combo trong tiêu đề. Bài này kể vì sao mình chọn đúng ba món đó, kiến trúc ra sao, và quan trọng nhất là vụ nó báo tin cho mình đúng một lần ngay tuần đầu vận hành. Thứ mà tháng trước chắc mình phát hiện sau vài tiếng.
Thân bài
1. Bối cảnh bài toán
Trước khi chọn công cụ thì nên nói rõ nó phải gánh gì.
Ba máy của mình chia vai trò rõ ràng:
srv-web-02: node k3s duy nhất, chạy hơn chục app production (mấy con Laravel/Go/NestJS tự viết), cộng Traefik làm ingress.srv-db: MySQL 8, Redis 7, PostgreSQL 18 cài thẳng trên OS. Cụm app trong k8s đi qua nó.srv-backup: điểm đến của backup đêm (rsync + dump), cộng một service TTS chạy PM2 ngoài cluster.
Yêu cầu đặt ra theo thứ tự ưu tiên:
- Ít bộ phận nhất có thể. Homelab mà nuôi năm hệ thống cho việc xem log thì tốn công maintain hơn lợi ích. Cứ mỗi thứ thêm vào là một thứ phải update, phải backup config, phải hiểu khi nó chết.
- Một điểm nhìn. Mở một trang ra là thấy server nào sống chết, app nào đang phun lỗi, backup hôm qua có chạy không.
- Alert về Telegram. Mình không nhìn dashboard hàng ngày, nhưng mình đọc Telegram hàng giờ. Alert phải tới nơi mình thực sự đọc.
- Infrastructure as code. Mọi thứ từ scrape config tới dashboard và alert rule phải nằm trong repo. Máy cháy mình rebuild lại được, không phải nhớ mình đã chỉnh gì bằng tay.
2. Vì sao VictoriaMetrics mà không Prometheus
Đây là câu hỏi đầu tiên người ta thường hỏi mình, và cũng là quyết định mình mất nhiều thời gian cân nhất.
Prometheus là chuẩn công nghiệp, không cãi. Nhưng với homelab nó đem theo vài thứ mình không cần: ăn RAM kha khá, cấu hình phức tạp hơn khi muốn lưu trữ lâu, và cái ecosystem khổng lồ của nó thật ra hơi thừa khi chương trình chỉ có ba con máy.
VictoriaMetrics thì ngược lại:
- Một binary duy nhất lo cả scrape, storage lẫn query. Deploy của mình là đúng một Deployment, một PVC 10Gi, không operator, không CRD.
kubectl applyxong là có metrics backend. - Nhẹ hơn rõ rệt cùng lượng series. Trên con k3s vốn đã gánh hơn chục app, RAM mỗi pod đều đáng giá.
- Nói PromQL. Query, dashboard Grafana, alert rule, kiến thức cộng đồng, xài chung được hết. Với người dùng cuối thì nó "là" Prometheus, chỉ khác phía sau.
- Config scrape dùng đúng cú pháp
prometheus.yml. Tất cả thứ mình viết chuyển sang Prometheus chuẩn được trong một buổi chiều nếu sau này đổi ý.
Câu chốt: với usecase này thì nó là đúng công cụ đúng việc. Đủ mạnh cho tương lai gần, đủ nhẹ để không phải nghĩ về nó.
3. Kiến trúc
flowchart TB
TG["Telegram (alert)"]
subgraph K3S["srv-web-02 — k3s"]
AL["Alloy<br/>DaemonSet, tail pod logs"] --> LO["Loki"]
VM["VictoriaMetrics<br/>scrape + store + query"] --> GF["Grafana<br/>dashboards + unified alerting"] --> TF["Traefik"]
A82["Alloy trên .82"] -->|"loki-push :31310"| LO
end
GF --> TG
VM -->|"scrape"| DB["srv-db .80<br/>node_exp + mysql/redis/pg exporters"]
VM -->|"scrape"| BK["srv-backup .82<br/>node_exp + tts :8000"]
VM -->|"scrape"| IC["in-cluster<br/>pods + kubelet/cadvisor + kube-state-metrics"]
Ba kênh dữ liệu đổ về cùng một chỗ:
- Metrics: VictoriaMetrics tự scrape. Pods thì qua annotation
prometheus.io/*, service ngoài cluster (TTS trên .82) qua EndpointSlice, node_exporter hai máy LAN, kubelet + cadvisor cho pod saturation, kube-state-metrics cho restart count, ba exporter cho MySQL/Redis/Postgres. - Logs: Alloy. Trong cluster là DaemonSet tail
/var/log; máy .82 chạy thêm một Alloy đọc log TTS rồi push ngược vào Loki qua NodePort. Chỉ một endpoint LAN, không phải mở ingress ra internet. - Nhìn và báo: Grafana. Dashboard một trang, 5 ô stat đầu là CPU/RAM/Disk/Load/Scrape-down của cả ba máy, dưới là golden signals theo app (req/s, err %, p50/p95/p99, cpu, mem), rồi MySQL/Redis/PG metrics. Alert thì dùng luôn unified alerting của Grafana, 9 rule critical bắn ra Telegram.
4. Hai quyết định nghe hơi lạ
Một backend metrics duy nhất. Có giai đoạn mình suýt có hai hệ thống metrics, kiểu Prometheus cho app này, VM cho app kia. Nghĩ lại thì thấy ngay vấn đề: hai nơi chứa dữ liệu nghĩa là hai dashboard nửa vời, hai datasource phải nhớ đổi, và khi điều tra thì query cái này xong quên query cái kia. Nguyên tắc giờ: mọi thứ đổ về VictoriaMetrics, app chỉ việc expose /metrics, không cần biết ai đọc.
Không cài Alertmanager. Nghe hơi phản truyền thống, nhưng Grafana đã có unified alerting đủ dùng: evaluate PromQL/LogQL, contact point Telegram, notification policy, silence, tất cả provision qua file như datasource/dashboard. Cài Alertmanager riêng để được thêm dedup/grouping mà Grafana đã lo được nghĩa là thêm một stateful service phải nuôi. Không đáng.
Alerting nằm hết trong git. File grafana-alerting.yaml chứa contact point, policy và cả chín rule. Muốn thêm rule thì sửa file, apply, restart pod. Không có alert nào được tạo bằng tay trong UI, vì alert tạo tay là alert không tồn tại khi rebuild.
5. Số liệu sau một ngày chạy thật
- 31 scrape targets up. Pods, external service, node exporters, db exporters, kubelet, cadvisor, kube-state-metrics.
- Dashboard 27 panel. Từ CPU server tới p99 latency từng app, slowest routes, MySQL buffer pool, Redis hit rate.
- 9 alert rules critical đổ về một kênh Telegram duy nhất.
- Resource cho cả stack quan sát (VM + Loki + Grafana + 4 exporter + KSM) vào khoảng 400m CPU, 1.5Gi RAM. Rẻ.
6. Thế mà lại hay, deploy lên cái hoạt động luôn
Tuần đầu sau khi hệ thống đứng, một agent bên repo app push lên một deploy có file Caddyfile sai syntax (log_skip nhận list path trần thay vì named matcher, Caddy chỉ chịu một tham số). Rollout kẹt giữa chừng: pod mới crashloop, pod cũ còn hai con nên app vẫn sống.
Ngày xưa cái này sẽ lặng yên vài tiếng, cho tới khi có ai mở trang admin thấy chậm hoặc mình tình cờ kubectl get pods. Lần này thì năm phút sau deploy lỗi, tin nhắn về Telegram:
🚨 AppDown
📌 App tmoi-ad-admin scrape fail >5 phút
💡 Pod crashloop hoặc service ngoài cluster down
Mình mở log, thấy Caddy parse fail ngay dòng log_skip, sửa matcher, push, CI build deploy. Mười phút sau tin ✅ RESOLVED tự tới. App chưa bao giờ chết hoàn toàn, nhưng nếu không ai để ý thì cái rollout kẹt đó sẽ nằm yên đó rất lâu, nuôi cảm giác "vẫn ổn" giả.
Bài test hoàn hảo nhất cho hệ thống alert chính là sự cố thật. Và nó tới sớm hơn mình tưởng.
7. Phần mất
Đúng kiểu "mất thật", không phải "nhược điểm: quá tốt":
- Grafana chết là mù và câm luôn. Cả dashboard lẫn alert nằm trong một pod duy nhất. Pod đó chết thì không ai báo cho mình biết. Đây là trade-off mình chấp nhận có ý thức cho homelab, chứ production thật thì cần alerting ngoài chuỗi chính.
chatidphải viết thẳng vào config. Provisioning của Grafana expand biến môi trường kiểu số thành số (bug #69950, năm 2026 rồi vẫn còn), nên chat-id không đi qua env được. Chấp nhận vì chat-id chỉ là định danh, không ai gửi được tin mà không có bot token.- Backup alert dựa trên log marker. Rule
BackupFailedđếm dòng[ok] remote xongtrong 26h. Sau này đổi cách script echo thì rule lặng yên chết. Đã ghi chú trong file. - NoData là bẫy thật.
noDataState: NoDatanghe tưởng "bỏ qua", thực ra vẫn báo. Phải đặtOKcho rule không phải kiểu "thiếu dữ liệu là lỗi". Học được bài này sau một đêm telegram bị spam.
Kết bài
Ba câu hỏi mình tự đặt khi bắt đầu: nhìn thấy chưa, được báo chưa, giữ config được chưa. Giờ trả lời được hết. Dashboard mở một trang thấy đủ, tin Telegram tới đúng chỗ đọc, và toàn bộ stack nằm trong repo để kubectl apply lại là sống.
Cái mình thích nhất không phải là số lượng thứ đã cài, mà là số lượng thứ đã không phải cài. Không Prometheus server, không Alertmanager, không CRD, không operator. Ba món, một trang nhìn, một bot báo. Với ba con máy trong nhà thì combo này bất bại, nó giải đúng bài toán, không hơn.
Còn đường đi tiếp theo thì rõ rồi: Tempo cho traces, pg_stat_statements cho slow query. Nhưng hôm nay, tin Telegram cuối cùng đến là ✅ RESOLVED, và mình đi ngủ yên.