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 logs khô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:

fluentdpromtail
Phạm viAgent đa năng, đẩy được về nhiều đíchAgent sinh ra để đi cùng Loki
Cài đặtImage phải có thêm plugin Loki và plugin metadata KubernetesCó sẵn trong chart, tự gắn metadata pod
Cấu hìnhCú pháp riêng, xen Ruby; tự dựng label namespace, pod, đoán log_level bằng regexYAML theo kiểu Prometheus, có sẵn kubernetes_sd_configs
Bẫy hay gặpSao 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 nullCầ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-stack cài Loki, cài promtail dạng DaemonSet, và tạo sẵn ConfigMap datasource có label grafana_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.yaml

4. 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 đâu

Promtail 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:

LabelLấy từVí dụ
namespacenamespace của podsite-a-example-com
applabel app.kubernetes.io/name, hoặc app của podweb, giống nhau cho mọi site cài từ cùng chart
jobnamespace/appsite-a-example-com/web
pod, containertên pod, tên containertên pod đổi sau mỗi lần deploy
node_namenode đang chạy podtê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:

  1. Thêm kênh stdout (Monolog StreamHandler ghi vào php://stdout) vào config/logging.php. Kênh stderr có sẵn thường được đặt mức error; nếu chọn kênh này bạn chỉ nhận log lỗi.
  2. Đặt LOG_CHANNEL=stdout trong ConfigMap để nhận đủ các mức.
  3. Dùng JsonFormatter kèm stack trace cho kênh stdout, mỗi bản ghi là một dòng JSON.
  4. Nâng limits_config.max_line_size củ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.
  5. Đặt LOG_LEVEL ở mức info hoặc warning cho production; để debug thì 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 loki in kèm dòng tiêu đề USER-SUPPLIED VALUES:; dùng nguyên văn kết quả làm file -f sẽ sinh khoá lạ USER-SUPPLIED VALUES: null. Dùng helm get values loki -o yaml để nhận YAML sạch.
  • Đọc tài liệu của đúng chart. Các khoá như read.extraVolumes thuộc chart grafana/loki đời mới; chart Loki con bên trong loki-stack là đời cũ, không có khối read. 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 --set hay một file -f chỉ có một dòng, nếu không kèm --reuse-values, helm upgrade lấ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 khi helm list có thể vẫn hiện APP VERSION cũ. Giữ đầy đủ values trong Git và upgrade bằng chính file đó; kiểm tra bằng helm get values --revision N.
  • Label pod sinh 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 trong emptyDir: 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: true và đặt retention_period phù 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 Service ClusterIP, 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.