2 giờ sáng thứ Ba, điện thoại mình rung liên hồi vì chuông alert. Dịch vụ đồng bộ đơn hàng đột ngột ngừng chạy. Tệ hơn nữa, cronjob sao lưu database PostgreSQL 80GB không ghi thêm bất kỳ dòng log nào suốt 6 tiếng trước đó.
Mở máy SSH vào server, gõ crontab -l thì lịch vẫn còn nguyên. Thế nhưng do thiếu biến môi trường PATH, script đã crash âm thầm từ nửa đêm mà không ai hay biết. Sau đêm trực đó, team mình quyết định loại bỏ hoàn toàn crontab cũ kỹ để chuyển hết sang systemd service và systemd timer trên cụm Ubuntu 22.04.
Quick start: Tạo Service và Timer trong 5 phút
Nếu bạn cần một giải pháp chạy tác vụ định kỳ có log rõ ràng, không lo chết âm thầm, hãy làm theo 4 bước sau.
Bước 1: Viết script cần chạy
Tạo một file bash script tại /usr/local/bin/backup-task.sh:
sudo nano /usr/local/bin/backup-task.sh
Thêm đoạn mã kiểm tra dung lượng và dọn dẹp file tạm dưới đây:
#!/usr/bin/env bash
set -euo pipefail
NOW=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$NOW] Bắt đầu dọn dẹp cache và nén log..."
# Xóa cache ứng dụng cũ hơn 7 ngày
find /tmp/app-cache/ -type f -mtime +7 -delete 2>/dev/null || true
echo "[$NOW] Hoàn tất tác vụ thành công!"
Cấp quyền thực thi cho file:
sudo chmod +x /usr/local/bin/backup-task.sh
Bước 2: Tạo Systemd Service
Service đóng vai trò định nghĩa lệnh nào sẽ chạy và chạy dưới quyền ai. Tạo file unit tại /etc/systemd/system/backup-task.service:
sudo nano /etc/systemd/system/backup-task.service
Điền nội dung cấu hình:
[Unit]
Description=Tac vu don dep cache va sao luu dinh ky
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup-task.sh
User=root
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
Bước 3: Tạo Systemd Timer để lên lịch chạy
Timer đóng vai trò kích hoạt service. Bạn tạo file cùng tên tại /etc/systemd/system/backup-task.timer:
sudo nano /etc/systemd/system/backup-task.timer
Thiết lập thời gian chạy (ví dụ: 02:00 sáng mỗi ngày):
[Unit]
Description=Kich hoat backup-task vao 02:00 sang moi ngay
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
[Install]
WantedBy=timers.target
Bước 4: Nạp cấu hình và kích hoạt
Chạy các lệnh sau trên terminal:
# 1. Yêu cầu systemd đọc lại các file unit mới
sudo systemctl daemon-reload
# 2. Test chạy thử service ngay lập tức để kiểm tra lỗi cú pháp
sudo systemctl start backup-task.service
# 3. Soi log xem script chạy ra kết quả gì
journalctl -u backup-task.service -n 20 --no-pager
# 4. Kích hoạt timer tự chạy cùng hệ thống
sudo systemctl enable --now backup-task.timer
Chỉ mất 5 phút cài đặt. Từ bây giờ, script có người canh gác cẩn thận và toàn bộ log output đều được ghi nhận đầy đủ.
Vì sao Systemd Timer đáng tin cậy hơn Crontab?
Crontab ra đời từ năm 1975. Thời điểm đó, server chưa phức tạp như bây giờ. Khi lệnh cron gặp lỗi, cron mặc định chuyển output sang sendmail. Phần lớn server hiện nay không cài mail server cục bộ. Kết quả là lỗi biến mất không để lại dấu vết.
Ngược lại, kiến trúc tách rời của systemd mang lại 3 lợi thế rõ rệt:
- Quản lý dependency rõ ràng: Bạn dễ dàng khai báo
After=network-online.targethoặcWants=docker.service. Script chỉ chạy khi hạ tầng mạng và database đã sẵn sàng. Crontab không làm được điều này. - Tính năng bù giờ (Persistent=true): Giả sử lịch chạy là 02:00 sáng nhưng server tắt nguồn bảo trì từ 01:30 đến 03:00. Khi khởi động lại, systemd timer phát hiện job bị lỡ và kích hoạt chạy bù ngay. Với crontab, tác vụ đó đơn giản là bị bỏ qua đến ngày hôm sau.
- Giới hạn tài nguyên (cgroups): Bạn có thể đặt
MemoryMax=500MhoặcCPUQuota=30%ngay trong file service. Script chạy nặng sẽ không lo làm nghẽn CPU của các tiến trình web bên cạnh.
Cấu hình Production: Daemon tự hồi sinh khi crash
Ngoài các script chạy một lần (oneshot), systemd còn là giải pháp tuyệt vời để quản lý các tiến trình chạy nền liên tục (daemon) như Node.js, Python FastAPI hay Golang.
Dưới đây là file service mẫu tại /etc/systemd/system/api-gateway.service mà team mình đang dùng trên môi trường production:
[Unit]
Description=Production API Gateway
After=network.target postgresql.service
Wants=postgresql.service
[Service]
Type=simple
User=deploy
Group=deploy
WorkingDirectory=/opt/api-gateway
ExecStart=/usr/bin/node /opt/api-gateway/server.js
# Tự động khởi động lại khi ứng dụng bị crash
Restart=always
RestartSec=5s
# Môi trường và giới hạn file descriptors
LimitNOFILE=65535
Environment=NODE_ENV=production
EnvironmentFile=/opt/api-gateway/.env
# Siết chặt bảo mật hệ thống tệp
ProtectSystem=full
ProtectHome=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
Có hai chi tiết bạn nên đặc biệt lưu ý:
RestartSec=5s: Đặt khoảng nghỉ 5 giây trước khi khởi động lại. Nếu app lỗi logic nghiêm trọng dẫn đến exit liên tục, thông số này ngăn tiến trình rơi vào vòng lặp crash-loop đốt cạn 100% CPU.EnvironmentFile: Nạp toàn bộ biến môi trường từ file.env. Cách này giải quyết dứt điểm lỗi thường gặp: script chạy tay bằng tài khoản cá nhân thì được, nhưng chạy nền qua daemon lại thiếu biến cấu hình.
Kiểm tra lịch OnCalendar không sợ sai cú pháp
Viết chuỗi 5 dấu sao trên crontab rất dễ nhầm giữa ngày trong tháng và thứ trong tuần. Với systemd, bạn kiểm tra trực tiếp bằng lệnh systemd-analyze calendar:
systemd-analyze calendar "Mon..Fri *-*-* 09:00:00"
Hệ thống lập tức phân tích và đưa ra mốc chạy tiếp theo kèm thời gian đếm ngược chính xác:
Normalized form: Mon..Fri *-*-* 09:00:00
Next elapse: Fri 2026-10-23 09:00:00 UTC
(in 22h left)
Kinh nghiệm debug nhanh khi hệ thống gặp sự cố
Khi có cảnh báo dịch vụ, hãy sử dụng 3 câu lệnh sau để khoanh vùng nguyên nhân trong vòng chưa đầy 1 phút:
1. Xem danh sách toàn bộ Timer đang chạy
systemctl list-timers --all
Lệnh này trả về bảng theo dõi tổng thể. Cột NEXT cho biết thời điểm chạy kế tiếp, LEFT đếm ngược số giờ còn lại, và LAST RESULT hiển thị mã thoát (exit status) của lần chạy gần nhất.
2. Đọc log theo thời gian thực với Journalctl
Thay vì mở từng file text phân mảnh trong /var/log/, bạn có thể lọc log trực tiếp theo tên unit:
# Theo dõi log mới đẩy ra liên tục (tương tự tail -f)
journalctl -u backup-task.service -f
# Lọc toàn bộ log sinh ra từ 30 phút trước
journalctl -u backup-task.service --since "30 minutes ago"
# Chỉ hiển thị các dòng log có mức độ cảnh báo từ Error trở lên
journalctl -u backup-task.service -p err
3. Khắc phục lỗi “Command not found” do lệch PATH
Đây là vấn đề kinh điển nhất khi chuyển từ bash sang service. Systemd không load file ~/.bashrc hay ~/.profile của user, nên biến PATH mặc định chỉ chứa các thư mục hệ thống cơ bản: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin.
Nếu script dùng binary cài qua NVM, Pyenv hoặc Poetry, hãy khai báo rõ PATH trong khối [Service]:
[Service]
Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/home/deploy/.nvm/versions/node/v20.10.0/bin"
Hoặc an toàn nhất: luôn dùng đường dẫn tuyệt đối cho các binary trong script (ví dụ gọi /usr/bin/python3 thay vì chỉ gõ python3).
Đầu tư 15 phút chuyển đổi từ Crontab sang Systemd Timer giúp hệ thống của bạn vận hành minh bạch hơn hẳn. Bạn nắm được thời điểm chạy, kiểm soát tài nguyên tiêu hao và có sẵn hệ thống log tập trung để tra cứu khi có sự cố phát sinh.

