Khi số site trên cluster tăng lên, câu hỏi "site này vừa lỗi gì" bắt đầu tốn thời gian: phải tìm đúng namespace, đúng pod, chạy kubectl logs, và nếu pod đã bị tạo lại thì log cũ cũng mất theo. Tài liệu này hướng dẫn dựng hệ thống log tập trung cho cluster với Loki: so sánh promtail và fluentd, vì sao chọn Loki, cách promtail chạy dưới dạng DaemonSet, cách cho Laravel ghi log ra stdout, LogQL cơ bản, các bài học khi nâng cấp bằng Helm và những hạn chế của cấu hình mặc định.
1. Log trên Kubernetes nằm ở đâu
Container ghi log ra stdout và stderr. Container runtime trên mỗi node lưu các dòng đó vào file dưới /var/log/pods/<namespace>_<pod>_<uid>/<container>/, kubelet xoay vòng file khi đầy. Lệnh kubectl logs chỉ đọc lại các file này. Hệ quả:
- Pod bị xoá hoặc tạo lại (mỗi lần deploy, mỗi lần bị đuổi khỏi node) là log cũ biến mất.
- Không có cách tìm kiếm chung trên nhiều site, nhiều pod cùng lúc.
- Log ghi vào file bên trong container, ví dụ
storage/logs/laravel.log, thìkubectl logskhông thấy.
Cách phổ biến được mô tả trong tài liệu kiến trúc logging của Kubernetes là chạy một agent trên mỗi node, đọc các file log đó và đẩy về một kho tập trung.
2. Chọn agent: promtail hay fluentd
Hai agent hay được cân nhắc khi đẩy log về Loki:
| fluentd | promtail | |
|---|---|---|
| Phạm vi | Agent đa năng, đẩy được về nhiều đích | Agent sinh ra để đi cùng Loki |
| Cài đặt | Image phải có thêm plugin Loki và plugin metadata Kubernetes | Có sẵn trong chart, tự gắn metadata pod |
| Cấu hình | Cú pháp riêng, xen Ruby; tự dựng label namespace, pod, đoán log_level bằng regex | YAML theo kiểu Prometheus, có sẵn kubernetes_sd_configs |
| Bẫy hay gặp | Sao chép bản ghi ra stdout của chính fluentd rồi đọc lại log của mình; phải thêm luật @type null | Cần mount đúng thư mục log của node |
Với một cluster nhỏ chỉ đẩy log về Loki, promtail gọn hơn nhiều. Lưu ý promtail đang được Grafana thay dần bằng Alloy (xem mục 8).
3. Vì sao chọn Loki
- Chỉ đánh index theo label. Loki không đánh index toàn văn từng dòng log như Elasticsearch. Nó gom log theo bộ label (namespace, pod, container...) thành các stream, nén nội dung thành chunk. Tìm nội dung là quét chunk của các stream đã lọc theo label. Chi phí lưu trữ và RAM thấp hơn hẳn; đổi lại truy vấn toàn văn trên khoảng thời gian dài sẽ chậm hơn.
- Cùng hệ với Grafana. Nếu đã có Grafana như bài Giám sát Kubernetes với kube-prometheus và Grafana, thêm Loki chỉ là thêm một datasource, xem metric và log trên cùng một màn hình.
- LogQL giống PromQL. Ai đã quen truy vấn Prometheus sẽ đọc được LogQL nhanh.
- Một chart lo cả hai phía. Chart
loki-stackcài Loki, cài promtail dạng DaemonSet, và tạo sẵn ConfigMap datasource có labelgrafana_datasource: "1". Grafana của kube-prometheus-stack có sidecar đọc các ConfigMap mang label này, nên không phải khai datasource bằng tay.
Kiến trúc chi tiết có trong tài liệu Grafana Loki.
# sau khi thêm Helm repo chính thức của Grafana với tên "grafana"
helm upgrade --install loki grafana/loki-stack -n monitoring \
--version <phiên-bản> -f values-loki.yaml4. Promtail chạy dưới dạng DaemonSet
Promtail là agent thu log của Loki. Chart triển khai nó thành DaemonSet, mỗi node đúng một pod. Pod này mount thư mục log của node ở chế độ chỉ đọc:
volumes:
- name: pods
hostPath: { path: /var/log/pods } # log của mọi container
- name: containers
hostPath: { path: /var/lib/docker/containers }
- name: run
hostPath: { path: /run/promtail } # positions.yaml: đã đọc tới đâuPromtail hỏi Kubernetes API danh sách pod trên node, rồi gắn label cho từng stream theo cấu hình mặc định của chart:
| Label | Lấy từ | Ví dụ |
|---|---|---|
namespace | namespace của pod | site-a-example-com |
app | label app.kubernetes.io/name, hoặc app của pod | web, giống nhau cho mọi site cài từ cùng chart |
job | namespace/app | site-a-example-com/web |
pod, container | tên pod, tên container | tên pod đổi sau mỗi lần deploy |
node_name | node đang chạy pod | tên node |
Stage cri bóc phần tiền tố (thời điểm, stdout/stderr) mà containerd thêm vào mỗi dòng, chỉ giữ nội dung gốc.
5. Laravel phải ghi log ra stdout
Đây thường là phần tốn thời gian hơn cả việc cài Loki. Mặc định LOG_CHANNEL=daily hoặc stack khiến Laravel ghi log vào file trong storage/logs bên trong container, promtail không đọc được. Các bước nên làm:
- Thêm kênh
stdout(MonologStreamHandlerghi vàophp://stdout) vàoconfig/logging.php. Kênhstderrcó sẵn thường được đặt mứcerror; nếu chọn kênh này bạn chỉ nhận log lỗi. - Đặt
LOG_CHANNEL=stdouttrong ConfigMap để nhận đủ các mức. - Dùng
JsonFormatterkèm stack trace cho kênh stdout, mỗi bản ghi là một dòng JSON. - Nâng
limits_config.max_line_sizecủa Loki (ví dụ 256000 byte) và bật cắt dòng quá dài, vì stack trace của Laravel có thể rất dài. - Đặt
LOG_LEVELở mứcinfohoặcwarningcho production; đểdebugthì lượng log tăng theo số site.
// config/logging.php (trích)
'stdout' => [
'driver' => 'monolog',
'handler' => Monolog\Handler\StreamHandler::class,
'with' => ['stream' => 'php://stdout'],
'formatter' => Monolog\Formatter\JsonFormatter::class,
'level' => env('LOG_LEVEL', 'info'),
],Image all-in-one chạy cả nginx, PHP-FPM, cron và queue worker trong một container, nên stdout là log trộn của tất cả. Định dạng JSON giúp tách log ứng dụng ra khỏi phần còn lại bằng bộ lọc json của LogQL.
6. LogQL cơ bản
Một truy vấn LogQL gồm bộ chọn stream trong ngoặc nhọn, theo sau là các bộ lọc:
# mọi log của một site
{namespace="site-a-example-com"}
# dòng có chữ ERROR trên mọi site
{app="web"} |= "ERROR"
# bóc JSON của Laravel, lọc theo mức log
{namespace="site-a-example-com"} | json | level_name="ERROR"
# loại bỏ log truy cập của nginx, chỉ giữ dòng nhắc tới exception
{app="web"} != "GET /" |~ "(?i)exception"
# số dòng lỗi mỗi site trong 5 phút, để vẽ biểu đồ
sum by (namespace) (count_over_time({app="web"} |= "ERROR" [5m]))
# 10 site ghi log nhiều nhất
topk(10, sum by (namespace) (rate({app="web"}[5m])))Quy tắc vàng: lọc bằng label trước (nhanh, dùng index), lọc nội dung sau (quét chunk). Truy vấn bắt đầu bằng {app="web"} trên khoảng thời gian dài sẽ phải quét log của mọi site. Cú pháp đầy đủ ở trang LogQL của Loki.
7. Bài học khi nâng cấp Loki bằng Helm
- Header lọt vào file values.
helm get values lokiin kèm dòng tiêu đềUSER-SUPPLIED VALUES:; dùng nguyên văn kết quả làm file-fsẽ sinh khoá lạUSER-SUPPLIED VALUES: null. Dùnghelm get values loki -o yamlđể nhận YAML sạch. - Đọc tài liệu của đúng chart. Các khoá như
read.extraVolumesthuộc chartgrafana/lokiđời mới; chart Loki con bên trongloki-stacklà đời cũ, không có khốiread. Khai khoá không tồn tại thì Helm im lặng bỏ qua. - Upgrade chỉ với một giá trị sẽ xoá values cũ. Dù truyền bằng
--sethay một file-fchỉ có một dòng, nếu không kèm--reuse-values,helm upgradelấy values mặc định của chart rồi áp giá trị mới lên. Mọi tuỳ chỉnh trước đó (image, ruler,max_line_size) mất hiệu lực, trong khihelm listcó thể vẫn hiện APP VERSION cũ. Giữ đầy đủ values trong Git và upgrade bằng chính file đó; kiểm tra bằnghelm get values --revision N. - Label
podsinh stream mới sau mỗi lần deploy. Với ít pod mỗi site thì chưa đáng lo. Nhưng đừng đưa đường dẫn URL, IP hay ID người dùng vào label: đó là cách nhanh nhất làm Loki phình index.
8. Hạn chế của cấu hình mặc định và cách khắc phục
- Không có volume bền. Values mặc định để
persistence.enabled: false, dữ liệu nằm trongemptyDir: pod Loki khởi động lại là mất log cũ. Bật persistence với StorageClass Retain, hoặc tốt hơn là lưu chunk trên object storage. - Không có retention. Dữ liệu lớn dần trên đĩa node. Bật compactor với
retention_enabled: truevà đặtretention_periodphù hợp. - Một bản sao, không xác thực. Loki 1 replica,
auth_enabled: false: chỉ để truy cập trong cluster qua ServiceClusterIP, không mở ra ngoài. - Promtail không khai requests và limits. Pod promtail thành BestEffort, bị đuổi đầu tiên khi node thiếu RAM, như phân tích ở bài Requests và limits, QoS và nodeAffinity: xếp pod lên đúng node. Khai request nhỏ cho nó.
Về lâu dài: chart loki-stack dừng ở nhánh Loki 2.x, trong khi Loki 3.x đã ra. Grafana cũng giới thiệu Grafana Alloy, agent thu cả log, metric và trace, thay cho promtail. Khi dựng mới, nên cân nhắc chart grafana/loki với lưu trữ trên object storage, retention rõ ràng và Alloy làm agent.
9. Checklist logging với Loki
- Ứng dụng ghi log ra stdout, định dạng JSON, mức log hợp lý.
- Label ít giá trị; lọc label trước rồi mới lọc nội dung.
- Values của mọi release Helm nằm trong Git; không upgrade với một giá trị lẻ.
- Loki có lưu trữ bền và retention; promtail có requests.
- Đọc kỹ giá trị mặc định của chart trước khi coi Loki là kho lưu trữ log.
Bài tiếp theo
Dựng hạ tầng bằng Linode API: cluster, NFS và MySQL qua pipeline. Tạo cluster LKE, máy NFS và máy MySQL bằng API ngay trong GitLab CI, thay vì bấm tay trên giao diện.
