Tự host quản lý log trên một VPS cần bao nhiêu RAM?
So sánh journald, logrotate, Loki và OpenSearch trên một VPS, kèm mức RAM tối thiểu và quy tắc retention để bạn chọn đúng cách lưu, tìm log.
Chi phí thực tế của việc tự host hệ thống quản lý log trên một VPS
Quản lý log tự host trên một VPS (virtual private server) quy về một câu hỏi: bạn cần một search cluster, hay chỉ cần rotation và grep? Hầu hết tài liệu hướng dẫn của vendor bắt đầu với 3 node và 12 GB RAM trước khi một dòng log được gửi đi. Trên một server, câu trả lời đó không hữu ích, nên phần so sánh dưới đây dựa trên những gì mỗi lựa chọn yêu cầu ở một máy nhỏ trước khi máy đó lưu bất kỳ dữ liệu nào.
Nếu bạn chạy 1 hoặc 2 server và muốn biết chuyện gì đã xảy ra vào thứ Ba tuần trước, systemd-journald và logrotate đã đáp ứng được việc đó, và bạn có thể dừng ở phần tiếp theo. Nếu nhiều máy phải gửi log về một nơi để tìm kiếm trong nhiều tuần, Grafana Loki phù hợp với một máy nhỏ vì nó index label thay vì nội dung các dòng log. Elasticsearch và OpenSearch cung cấp tìm kiếm toàn văn thực sự, nhưng đổi lại tiêu tốn RAM vì heap của JVM (Java virtual machine) có mức tối thiểu không thể thấp hơn.
Bắt đầu với journald vì hầu hết trường hợp chỉ cần đến đây
systemd-journald đã chạy trên mọi server Ubuntu hoặc Debian hiện tại. Nó ghi lại standard output của mọi service unit, thông báo kernel và mọi dữ liệu được gửi đến syslog. Bốn lệnh này xử lý phần lớn sự cố.
journalctl -u nginx.service --since "2026-08-14 09:00" --until "2026-08-14 10:00"
journalctl -p err -b
journalctl -f -u ssh.service
journalctl --disk-usageLệnh cuối cùng in ra một dòng như Archived and active journals take up 1.1G in the file system.. Đây là con số quyết định bạn có cần làm gì thêm hay không. Nếu giá trị chỉ vài trăm megabyte và bạn tìm được thông tin cần thiết bằng -u và --since, thì đã đủ.
Journal có tồn tại sau khi reboot hay không phụ thuộc vào Storage= và việc /var/log/journal có tồn tại hay không. Với thiết lập phổ biến Storage=auto, journald ghi vào /var/log/journal khi thư mục đó tồn tại, và ghi vào /run/log/journal khi thư mục đó không tồn tại. /run nằm trên memory, nên trên máy không có thư mục đó, mọi log sẽ bị xóa khi reboot. Đây lại chính là lúc bạn cần đọc chúng. Image Ubuntu có sẵn thư mục này. Image tối giản và image dựa trên container thường không có.
ls -d /var/log/journal
sudo mkdir -p /var/log/journal
sudo systemctl restart systemd-journald
journalctl --disk-usageSau khi restart, journalctl --disk-usage phải báo kích thước dưới /var/log/journal thay vì /run. Các giá trị mặc định vốn đã có giới hạn. Đây là lý do chính journald là giải pháp nghiêm túc, không phải phương án dự phòng. Man page journald.conf đặt SystemMaxUse= ở mức 10% kích thước filesystem và SystemKeepFree= ở mức 15%, đồng thời giới hạn mỗi giá trị mặc định được tính toán ở mức 4G. SystemMaxFileSize= mặc định bằng một phần tám của SystemMaxUse=, giới hạn ở 128M, nên thông thường bạn sẽ giữ 7 file đã rotate. MaxRetentionSec= mặc định bằng 0, tức là tắt việc xóa dựa trên tuổi file. Hãy đọc lại giá trị mặc định cuối cùng: journal mặc định chỉ bị giới hạn theo kích thước, không bao giờ theo tuổi file.
[Journal]
Storage=persistent
SystemMaxUse=2G
MaxRetentionSec=30dayGhi thiết lập đó vào /etc/systemd/journald.conf.d/99-size.conf, restart journald, rồi kiểm tra xem journalctl --disk-usage đã tiến gần đến giới hạn mới chưa. Để giải phóng dung lượng ngay thay vì chờ lần rotation tiếp theo, chạy sudo journalctl --vacuum-size=500M hoặc sudo journalctl --vacuum-time=14d. Cả hai lệnh đều in ra từng file bị xóa. Vì vậy, nếu lệnh chạy không in gì thì không có file nào cần xóa.
Mọi thứ nằm ngoài journal, chẳng hạn /var/log/nginx/access.log, thuộc phần xử lý của logrotate. logrotate chạy hằng ngày bằng systemd timer. Có một lỗi cần biết vì nó trông giống lỗi trong df. Sau khi rotation, file cũ không còn trong danh sách thư mục nhưng daemon vẫn mở file đó. Vì vậy, df -h báo disk đầy trong khi du -sh /var/log báo ít hơn nhiều. Dung lượng chỉ được trả lại khi process mở lại log. Đây là mục đích của dòng reload postrotate trong cấu hình. sudo lsof -nP +L1 liệt kê các file đã bị xóa nhưng vẫn đang được mở, đồng thời cho biết process đang giữ từng file. Dùng sudo logrotate -d /etc/logrotate.d/nginx để kiểm tra một rule mà không thay đổi gì.
Gửi log từ nhiều server về một collector
Khi có nhiều hơn một máy, quản lý nhiều server Linux cùng lúc sẽ dễ hơn nếu log của chúng được gửi về một nơi. Hầu hết distribution đã cài sẵn rsyslog, vì vậy collector tập trung đơn giản và ít tốn kém nhất là một file cấu hình trên mỗi máy gửi.
*.* action(type="omfwd" target="logs.example.com" port="514" protocol="tcp")Lưu cấu hình đó thành /etc/rsyslog.d/50-forward.conf, kiểm tra bằng sudo rsyslogd -N1. Lệnh này kiểm tra cấu hình rồi thoát mà không khởi động gì. Sau đó restart rsyslog. Trên collector, bật TCP input.
module(load="imtcp")
input(type="imtcp" port="514")Có 2 cảnh báo mang tính kỹ thuật. syslog thường không có mã hóa và xác thực. Vì vậy, bất kỳ máy nào truy cập được port 514 đều có thể chèn các dòng log trông giống hệt log của bạn. Hãy bind nó vào private network hoặc VPN, rồi cấu hình firewall cho port này. Thứ hai, action queue mặc định nằm trong memory. Khi collector không thể truy cập, queue sẽ đầy và message bị drop mà không được lưu bản sao. rsyslog có hướng dẫn cấu hình disk assisted queue cho trường hợp này trong tutorial về reliable forwarding.
Vì sao stack ELK không phù hợp với VPS nhỏ
ELK gồm Elasticsearch để lưu trữ và tìm kiếm, Logstash cho pipeline tiếp nhận log, và Kibana cho giao diện. Mức tối thiểu nằm ở JVM heap, và giá trị này được đặt trước khi có log được gửi đến.
Tài liệu của Elastic khuyến nghị đặt heap không quá 50% tổng memory khả dụng của mỗi node Elasticsearch, vì process còn dùng các buffer off heap và phụ thuộc vào file cache của hệ điều hành để đọc nhanh các file index. Vì vậy, heap 2 GB đòi hỏi một máy 4 GB, chưa tính Kibana và chưa tính ứng dụng mà server thực sự được mua để chạy. Elastic cũng nêu rằng Elasticsearch tự động xác định kích thước heap dựa trên role của node và tổng memory. Điều đó có nghĩa là máy nhỏ sẽ có heap nhỏ, rồi dành phần lớn thời gian để chạy garbage collection.
Logstash là thành phần khiến ngân sách máy nhỏ không thể đáp ứng. Trang hướng dẫn cấu hình JVM của Elastic khuyến nghị heap tối thiểu 4GB và tối đa 8GB cho workload ingestion thông thường. Con số đó đã chiếm toàn bộ một VPS 4 GB, chỉ cho một process ở giữa pipeline.
The data behind this chart
[
{
"label": "Loki plus Alloy (no JVM)",
"documented_heap_mb": 0
},
{
"label": "OpenSearch demo compose",
"documented_heap_mb": 512
},
{
"label": "OpenSearch production example",
"documented_heap_mb": 2048
},
{
"label": "Logstash recommended minimum",
"documented_heap_mb": 4096
}
]Đó là các giá trị do từng project công bố trong tài liệu riêng. Chúng không phải số đo trên một máy test, và workload của bạn có thể làm các giá trị này thay đổi. File compose mẫu của OpenSearch đặt 512 MB cho mỗi node trong bản demo và 2048 MB trong ví dụ production, trong khi mức tối thiểu được Logstash khuyến nghị là 4096 MB. Cột heap hiển thị 0 cho Loki và Alloy vì chúng là các chương trình Go, không có JVM heap cần reserve. Toàn bộ khác biệt nằm ở một con số: component dùng JVM vẫn reserve phần memory đó dù có log được gửi đến hay không.
Nếu vẫn muốn chạy Elastic stack trên một server nhỏ, hãy bỏ Logstash và gửi log thẳng vào Elasticsearch bằng một collector nhẹ. Logstash được dùng để parse và transform dữ liệu ở quy mô lớn. Trên một máy duy nhất, bạn có thể xử lý việc đó ở edge hoặc bỏ qua.
Cả Elasticsearch và OpenSearch đều cần tăng vm.max_map_count lên 262144, vì chúng memory map các file index và limit mặc định của Linux quá thấp đối với chúng. Một container thoát sau vài giây kể từ khi startup trên máy mới thường là do vấn đề này, không phải nguyên nhân nào khác.
OpenSearch hay Elasticsearch: bạn có thể triển khai loại nào?
Lịch sử license ngắn gọn, vì nó quyết định những gì bạn được phép chạy. Vào tháng 1 năm 2021, Elastic chuyển Elasticsearch và Kibana từ Apache 2.0 sang mô hình kép gồm SSPL (server side public license) và Elastic License 2.0. AWS fork phiên bản code Apache 2.0 cuối cùng thành OpenSearch, và OpenSearch vẫn dùng Apache 2.0. Vào tháng 9 năm 2024, Elastic bổ sung AGPLv3 (GNU Affero General Public License version 3) làm một lựa chọn khác cho source code miễn phí. Với một người tự host trên một VPS, tất cả các license này đều cho phép bạn làm việc đó. Các điều khoản license trở nên đáng chú ý khi bạn cung cấp phần mềm cho người khác dưới dạng managed service.
Khác biệt thực tế trên một máy nhỏ ít hơn so với những gì lịch sử license cho thấy, vì bên dưới cả hai đều dùng cùng một engine. Tên gọi khác nhau: index lifecycle trong OpenSearch là ISM (index state management), còn trong Elasticsearch là ILM (index lifecycle management). Tính đến tháng 8 năm 2026, OpenSearch 2.12 trở lên sẽ không khởi động nếu chưa đặt mật khẩu admin trong lần chạy đầu tiên.
sudo sysctl -w vm.max_map_count=262144
printf 'vm.max_map_count = 262144\n' | sudo tee /etc/sysctl.d/99-opensearch.conf
docker run -d -p 9200:9200 -p 9600:9600 -e "discovery.type=single-node" \
-e "OPENSEARCH_INITIAL_ADMIN_PASSWORD=<custom-admin-password>" \
opensearchproject/opensearch:latestDòng sysctl -w áp dụng setting ngay lúc này, còn file trong /etc/sysctl.d/ là phần vẫn còn sau khi reboot. Kiểm tra container đã chạy bằng curl -k -u admin:<password> https://localhost:9200. Container trả lời qua https bằng demo certificate, nên -k bỏ qua bước verification. Phản hồi hợp lệ là một block JSON nhỏ chứa tên cluster và version. Trang cài đặt OpenSearch cũng yêu cầu người dùng Docker Desktop cấp cho host ít nhất 4 GB memory. Đây là dấu hiệu khá rõ về lượng tài nguyên mà process cần.
Loki giữ kích thước nhỏ như thế nào: dùng labels thay vì full-text index
Loki chỉ duy trì một index trên các label và lưu các dòng log thành những chunk đã nén. Query trước tiên chọn stream, sau đó mới lọc nội dung. {unit="ssh.service"} |= "Failed password" chọn stream dựa trên label của nó, rồi quét các chunk đó để tìm chuỗi cần tìm. Không có thành phần nào index phần nội dung của dòng log, nên việc ingestion vẫn ít tốn kém và không cần giữ inverted index trong memory. Chi phí được chuyển sang thời điểm query. Đây là sự đánh đổi hợp lý khi bạn thường biết mình đang kiểm tra service nào.
Tài liệu của Grafana đưa ra monolithic mode, tức là toàn bộ Loki chạy trong một process với -target=all, cho khối lượng đọc và ghi nhỏ, tối đa khoảng 20GB mỗi ngày. Một VPS nằm khá xa dưới mức này.
Điểm dễ mắc bẫy là label cardinality. Mỗi tổ hợp khác nhau của các giá trị label là một stream, và số lượng stream quyết định mức memory Loki sử dụng cũng như kích thước index. Label chứa địa chỉ IP của client hoặc request identifier sẽ tạo một stream cho mỗi giá trị. Vì vậy, một web server có lưu lượng cao có thể tạo ra hàng chục nghìn stream trong một ngày, khiến process tăng memory cho đến khi kernel dừng nó. Chỉ dùng các label có giá trị mà bạn có thể đếm trên giấy: unit, host, job, level. Đặt phần chi tiết biến đổi trong chính dòng log, nơi filter expression có thể tìm thấy nó khi query.
Cài đặt Loki và Alloy trên cùng một VPS
Hai process đảm nhiệm công việc này. Loki lưu trữ log và xử lý truy vấn. Grafana Alloy đọc log rồi đẩy chúng lên Loki. Trước đây Promtail đảm nhiệm việc đẩy log, nhưng đã hết vòng đời vào ngày 2 March 2026, nên các bản cài đặt mới dùng Alloy. Ví dụ Docker riêng của Loki hiện cũng đi kèm một file cấu hình Alloy.
wget https://raw.githubusercontent.com/grafana/loki/v3.7.0/cmd/loki/loki-local-config.yaml -O loki-config.yamlHãy đọc file đó trước khi dùng. File đặt path_prefix: /tmp/loki với các chunk bên dưới /tmp/loki/chunks. Cách này phù hợp cho demo nhưng không phù hợp cho server: mọi thứ bên dưới /tmp của container sẽ mất khi container được tạo lại, nên lịch sử log biến mất sau lần cập nhật image tiếp theo. Hãy trỏ nó đến một path mà bạn mount.
common:
instance_addr: 127.0.0.1
path_prefix: /loki
storage:
filesystem:
chunks_directory: /loki/chunks
rules_directory: /loki/rules
replication_factor: 1
ring:
kvstore:
store: inmemorydocker volume create loki-data
docker run --name loki -d \
-v $(pwd):/mnt/config -v loki-data:/loki \
-p 127.0.0.1:3100:3100 \
grafana/loki:3.7.0 -config.file=/mnt/config/loki-config.yaml
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3100/readyLệnh cuối cùng phải in ra 200, vì /ready trả về HTTP 200 khi Loki đã sẵn sàng nhận network traffic. Giá trị khác nghĩa là process vẫn đang khởi động hoặc config bị từ chối. docker logs loki sẽ cho biết nguyên nhân. Có 2 chi tiết trong run command được đặt như vậy có chủ ý. Port chỉ được publish trên 127.0.0.1, vì sample config có auth_enabled: false và Loki không có sẵn chức năng xác thực user, nên bất kỳ client nào truy cập được port 3100 đều có thể đọc toàn bộ log và ghi log giả. Hãy giữ port này trên loopback, hoặc đặt Loki phía sau VPN hay reverse proxy có xác thực. Named volume rất quan trọng vì image chạy bằng user loki với UID 10001, nên host directory được bind mount mà root sở hữu sẽ không cho container ghi dữ liệu.
sudo apt-get update && sudo apt-get install -y gpg wget
sudo mkdir -p /etc/apt/keyrings/
sudo wget -O /etc/apt/keyrings/grafana.asc https://apt.grafana.com/gpg-full.key
echo "deb [signed-by=/etc/apt/keyrings/grafana.asc] https://apt.grafana.com stable main" \
| sudo tee /etc/apt/sources.list.d/grafana.list
sudo apt-get update
sudo apt-get install -y alloyAlloy đọc /etc/alloy/config.alloy. Cấu hình này lấy system journal và một nhóm file, rồi đẩy cả hai đến Loki cục bộ.
loki.write "local" {
endpoint {
url = "http://127.0.0.1:3100/loki/api/v1/push"
}
}
loki.relabel "journal" {
forward_to = []
rule {
source_labels = ["__journal__systemd_unit"]
target_label = "unit"
}
}
loki.source.journal "read" {
forward_to = [loki.write.local.receiver]
relabel_rules = loki.relabel.journal.rules
labels = {job = "systemd-journal", host = "app-01"}
}
local.file_match "nginx" {
path_targets = [{"__path__" = "/var/log/nginx/*.log", "job" = "nginx", "host" = "app-01"}]
}
loki.source.file "nginx" {
targets = local.file_match.nginx.targets
forward_to = [loki.write.local.receiver]
}Relabel rule sao chép field __journal__systemd_unit của journal vào label tên unit. Nhờ đó {unit="ssh.service"} hoạt động ở các bước sau. Nếu không có rule này, tên unit nằm bên trong entry thay vì nằm trong label. Bạn sẽ không thể lọc theo tên unit, và mọi query phải quét toàn bộ dữ liệu.
sudo systemctl reload alloy
systemctl show -p User alloy
sudo journalctl -n 5
sudo -u alloy journalctl -n 5Đây là chỗ khiến phần lớn setup bị mắc. Alloy chạy bằng service account riêng, không chạy bằng root. Để đọc system journal, account này phải thuộc group systemd-journal. Các file bên dưới /var/log/nginx thuộc group adm trên Debian và Ubuntu. Hãy thay account mà systemctl show đã in ra vào lệnh cuối. Nếu lệnh trả về ít entry hơn nhiều so với khi chạy bằng root, account đó không đọc được system journal. Khi đó Loki vẫn trống dù config của bạn hoàn toàn đúng. Hãy thêm các group rồi restart bằng sudo usermod -aG systemd-journal,adm alloy, sau đó chạy sudo systemctl restart alloy.
curl -G -s "http://127.0.0.1:3100/loki/api/v1/query_range" \
--data-urlencode 'query={job="systemd-journal"}' \
--data-urlencode 'limit=5' | jq '.data.result | length'Giá trị lớn hơn 0 nghĩa là có stream mang label đó và stream chứa entry. Giá trị 0 nghĩa là chưa có dữ liệu nào đến với label đó. Một default gây ra cảnh báo giả thường gặp: loki.source.journal đặt max_age thành 7h, nên khi mới khởi động, Alloy đọc 7 giờ cuối của journal và không đọc dữ liệu cũ hơn. Để có giao diện, hãy chạy Grafana trên cùng máy rồi trỏ Loki data source đến http://127.0.0.1:3100.. Container log cần một source khác: Alloy phát hiện các Docker container đang chạy và tail log của chúng. Đây cũng là cách ví dụ getting started chính thức của Loki thực hiện. Trong một k3s cluster một node trên VPS, job đó chuyển sang thư mục pod log do kubelet ghi.
Retention: chọn thời điểm log bị xóa
Hầu như không ai chọn thời gian lưu log cho đến khi disk đầy, rồi phải chọn lúc 3 giờ sáng khi service đã dừng. Hãy quyết định ngay từ ngày đầu dựa trên 2 câu hỏi: thực tế bạn cần xem lại log trong bao lâu, và trong lần xem xét sự cố vào tháng tới, bạn vẫn phải có những gì. Với một server đơn lẻ, 14 đến 30 ngày thường đáp ứng cả 2 yêu cầu.
Loki không xóa gì cả cho đến khi bạn bật compactor. Retention mặc định bị tắt. Điều này thường khiến người dùng bất ngờ khi volume đã đầy vì retention_period chỉ nằm trong config mà không làm gì.
limits_config:
retention_period: 744h
compactor:
working_directory: /loki/retention
compaction_interval: 10m
retention_enabled: true
retention_delete_delay: 2h
retention_delete_worker_count: 150
delete_request_store: filesystem744h là 31 ngày. Có 4 quy tắc được ghi rõ chi phối block này:
- Compactor áp dụng retention, và tài liệu Grafana yêu cầu chạy compactor dưới dạng một instance duy nhất. Trên một VPS, điều này tự xảy ra.
- Thời gian retention tối thiểu là 24h, và retention chỉ hoạt động khi index period là 24h. Mẫu
schema_configđã dùngperiod: 24h, nên giữ nguyên. delete_request_storelà bắt buộc khiretention_enabledlà true. Nó chỉ định store lưu các yêu cầu xóa. Vì vậy, trên một node đơn lẻ dùng filesystem, nó phải trùng vớiobject_store: filesystemđã có trong schema.- Chunks được đánh dấu trước rồi mới bị xóa sau
retention_delete_delay, ở đây là 2h. Vì vậy, dung lượng trống sẽ quay lại muộn hơn thời điểm policy quy định. Không đánh giá cấu hình dựa trêndfchỉ 5 phút sau khi reload.
OpenSearch xóa toàn bộ index thay vì từng dòng riêng lẻ. Vì vậy, log index được tạo theo từng ngày. Một ISM policy đưa index đi qua các state rồi xóa index khi đủ cũ. Một ism_template gắn policy vào các index mới, nên bạn không cần nhớ thao tác này.
ISM policy xóa log index sau 14 ngày
{
"policy": {
"description": "delete log indexes after 14 days",
"default_state": "hot",
"states": [
{
"name": "hot",
"actions": [],
"transitions": [
{ "state_name": "delete", "conditions": { "min_index_age": "14d" } }
]
},
{
"name": "delete",
"actions": [ { "delete": {} } ],
"transitions": []
}
],
"ism_template": { "index_patterns": ["logs-*"], "priority": 100 }
}
}Tạo policy bằng lệnh PUT đến _plugins/_ism/policies/logs-retention. Template áp dụng cho các index được tạo sau khi policy tồn tại. Vì vậy, các index đã có trên disk cần được gắn policy thủ công.
Dù bạn chạy hệ thống nào, giá trị retention chỉ hiệu quả khi có kiểm tra dung lượng trống đi kèm. Xóa log sau 14 ngày không giúp bạn nếu 10 ngày log đã làm đầy volume. Vì vậy, hãy kết hợp policy với monitoring tình trạng disk trên VPS và cảnh báo khi dung lượng đã sử dụng đạt 80%.
Mỗi 1 GB log cần bao nhiêu dung lượng đĩa
Câu trả lời chính xác phụ thuộc vào số dòng và các field của bạn. Vì vậy, hãy đo trên dữ liệu thực tế thay vì dựa vào một tỷ lệ được công bố. Cơ chế lưu trữ của từng hệ thống khác nhau nên có thể dự đoán xu hướng. OpenSearch và Elasticsearch tạo inverted index cho mọi field được index, đồng thời lưu document. Vì vậy, dữ liệu trên đĩa lớn hơn văn bản thô, và mỗi replica sẽ nhân thêm dung lượng. Trên một node, hãy đặt số lượng replica là 0, vì replica shard trên cùng node không thể tồn tại khi node đó bị lỗi. Nếu giữ giá trị 1, dung lượng đĩa sẽ tăng gấp đôi và cluster health sẽ luôn ở trạng thái yellow.
sudo du -sh /var/lib/docker/volumes/loki-data/_data
curl -k -u admin:<password> "https://localhost:9200/_cat/indices?v&h=index,docs.count,store.size"Hãy chạy lệnh phù hợp trong 2 ngày liên tiếp. Chênh lệch giữa 2 lần đo là mức tăng hằng ngày. Nhân giá trị đó với số ngày retention, cộng thêm khoảng 30% dung lượng dự phòng cho quá trình compaction và merge, rồi so sánh với dung lượng volume. Nếu không đủ, hãy giảm retention trước khi mua thêm disk, vì volume lớn hơn chỉ đẩy cùng một vấn đề lùi lại vài tuần.
Điều gì hỏng trước trên một máy nhỏ
Bộ nhớ cạn trước. Kernel OOM (out of memory) killer sẽ chọn một tiến trình lớn để dừng, và tiến trình lớn nhất trên máy chủ ghi log thường là JVM. journalctl -k | grep -i "killed process" hiển thị lần kill đó cùng tên tiến trình trong dấu ngoặc vuông. Tiến trình bị dừng không phải lúc nào cũng là log stack: sshd hoặc database của bạn cũng có thể bị chọn. Đây là cách một thử nghiệm ghi log lại làm ứng dụng mà bạn muốn lấy log bị dừng. Hãy đặt giới hạn rõ ràng cho container để lỗi xảy ra đúng ở nơi bạn đã chọn. Giới hạn bộ nhớ trong Docker Compose dùng cho mục đích đó.
Disk cạn tiếp theo, và search engine thường lỗi theo một cách cụ thể, dễ nhận biết. Elasticsearch và OpenSearch theo dõi mức sử dụng disk ở nhiều ngưỡng. Ngưỡng thấp là 85% và ngưỡng cao là 90%. Khi đạt flood stage 95%, mọi index có shard trên node đó sẽ nhận block index.blocks.read_only_allow_delete, sau đó các thao tác ghi sẽ lỗi với blocked by: [FORBIDDEN/12/index read-only / allow delete (api)]. Block được gỡ khi mức sử dụng giảm xuống dưới ngưỡng cao. Trước tiên hãy giải phóng dung lượng, rồi chỉ gỡ block thủ công nếu nó vẫn còn.
curl -k -u admin:<password> -X PUT "https://localhost:9200/_all/_settings" \
-H 'Content-Type: application/json' \
-d '{"index.blocks.read_only_allow_delete": null}'Loki lỗi âm thầm hơn. Loki không có read-only mode để chuyển sang, nên volume đầy sẽ biểu hiện thành các lần push thất bại ở phía sender và các khoảng trống trong kết quả query. Vấn đề cardinality sẽ xuất hiện dưới dạng mức sử dụng bộ nhớ tăng chậm thay vì một lỗi rõ ràng. Hãy định kỳ theo dõi kích thước thư mục chunks, không phải đợi đến khi có sự cố.
Lỗi cuối cùng là đưa sai loại dữ liệu vào hệ thống. Log system không phải metrics system: lấy mẫu CPU load mỗi 10 giây rồi lưu dưới dạng text vừa tốn chi phí lưu trữ vừa khó vẽ biểu đồ. Công việc đó phù hợp với một hệ thống như Zabbix monitoring server trên Ubuntu 24.04. Application exception cần grouping, deduplication và giao diện xem stack trace. Đó là công việc của error tracker tự host. Việc biết toàn site có đang down hay không lại là một công việc riêng, có thể dùng uptime và status page như Uptime Kuma. Hãy dành log system cho những dòng text mà con người sẽ đọc.
FAQ
Tôi có cần Elasticsearch để tìm kiếm log của server không?
Không cần nếu chỉ có một hoặc hai server. journalctl đã lọc được theo unit, priority, boot và khoảng thời gian; các file đã rotate xử lý được grep và zgrep. Search cluster chỉ đáng dùng tài nguyên RAM khi bạn có nhiều máy, cần tìm kiếm toàn văn trên tất cả máy cùng lúc, hoặc nhiều người cần dùng chung một giao diện. Với quy mô nhỏ hơn, journald kèm giới hạn dung lượng và thời gian lưu trữ đáp ứng cùng nhu cầu mà không cần thêm RAM.
Tôi cần bao nhiêu RAM để quản lý log tự host?
Dùng thông số do từng project công bố thay vì ước lượng theo kinh nghiệm. Loki và Alloy là các chương trình Go, không cần dành sẵn heap từ đầu, còn Grafana ghi rõ Loki monolithic có thể xử lý tối đa khoảng 20GB mỗi ngày. File compose mẫu của OpenSearch đặt heap 512 MB cho môi trường demo và 2 GB trong ví dụ production. Elastic yêu cầu heap không vượt quá 50% tổng memory, nên heap 2 GB đồng nghĩa với máy 4 GB trước khi chạy Kibana. Tài liệu của Logstash khuyến nghị riêng Logstash có ít nhất 4GB heap. Đây là các thiết lập được ghi trong tài liệu, không phải benchmark, vì vậy hãy đo tải thực tế trước khi chọn cấu hình.
Khác biệt thực tế giữa Loki và OpenSearch khi quản lý log là gì?
Khác biệt nằm ở mô hình index. Loki chỉ index label và lưu phần nội dung log dưới dạng các chunk đã nén. Khi truy vấn, Loki mới quét các chunk này, nên ghi dữ liệu ít tốn tài nguyên hơn nhưng truy vấn rộng sẽ tốn nhiều hơn. OpenSearch index nội dung của các field, nên tìm kiếm toàn văn tùy ý nhanh hơn; đổi lại, index tiêu tốn cả memory và disk. Chọn Loki khi bạn biết service và khoảng thời gian cần xem. Chọn OpenSearch khi cần tìm những đoạn text không thể dự đoán trước.
Nên lưu log trên VPS trong bao lâu?
Chọn thời gian trước khi disk tự quyết định thay bạn. Cấu hình thời gian lưu tại đúng một nơi trên mỗi hệ thống: MaxRetentionSec= và SystemMaxUse= cho journald, retention_period khi đã bật compactor cho Loki, và policy ISM kèm min_index_age cho OpenSearch. Với hầu hết hệ thống chỉ có một server, 14 đến 30 ngày thường đủ cho việc debug và xem xét incident. Những dữ liệu cần giữ lâu hơn phải được lưu thêm một bản bên ngoài máy, vì log chỉ tồn tại trên server đã bị lỗi thì không thể được xem là một bản ghi đáng tin cậy.
Promtail còn là cách phù hợp để chuyển log sang Loki không?
Không. Promtail đã hết vòng đời vào ngày 2 March 2026 và được thay thế bằng Grafana Alloy. Ví dụ cài đặt Loki trên Docker hiện đã dùng cấu hình Alloy, còn Grafana cung cấp công cụ chuyển đổi config Promtail hiện có sang cú pháp Alloy. Một cài đặt Promtail hiện tại vẫn tiếp tục chạy, nhưng không còn nhận bản sửa lỗi. Hãy xem việc migration là công tác bảo trì, thay vì một bản nâng cấp có thể trì hoãn vô thời hạn.