Linux cho DE 7 — Lập lịch: cron, systemd & môi trường
Vì sao lập lịch là kỹ năng nền của DE
Phần lớn công việc data engineering ở ngân hàng chạy theo nhịp: 01:00 sáng trích export core banking, 02:00 load vào staging, 03:00 build bảng tổng hợp, 06:00 gửi báo cáo cho nghiệp vụ. Ai bấm nút cho những mốc giờ đó? Trên một server Linux đơn lẻ, câu trả lời gần như luôn là cron hoặc systemd timer. Hiểu chúng — và hiểu vì sao một script chạy tay hoàn hảo lại fail lặng lẽ khi cron gọi — là kỹ năng phòng cháy hằng ngày.
Bài này đi từ cron (công cụ lâu đời, phổ biến nhất), sang at cho job một lần, rồi systemd service + timer (cách hiện đại), logrotate cho log xoay vòng, và một mục cực kỳ quan trọng về biến môi trường & loại shell — thủ phạm số một của lỗi "chạy tay OK, cron fail". Cuối cùng so sánh cron vs systemd timer vs Airflow để biết khi nào cần công cụ nào. Bài nối tiếp Linux 6 — Tiến trình & job.
cron: bộ hẹn giờ kinh điển
cron là một daemon (tiến trình nền) khởi động cùng hệ thống, mỗi phút thức dậy đọc các bảng lịch (crontab) và chạy lệnh nào đến giờ. Mỗi user có crontab riêng, và hệ thống có các crontab chung.
Cú pháp 5 trường
Mỗi dòng crontab là năm trường thời gian rồi tới lệnh:
┌───────── phút (0-59)
│ ┌─────── giờ (0-23)
│ │ ┌───── ngày tháng (1-31)
│ │ │ ┌─── tháng (1-12)
│ │ │ │ ┌─ thứ (0-7, 0 và 7 đều = Chủ Nhật)
│ │ │ │ │
* * * * * /đường/dẫn/lệnh
Các ký tự đặc biệt trong mỗi trường:
| Ký hiệu | Ý nghĩa | Ví dụ |
|---|---|---|
* | mọi giá trị | * * * * * mỗi phút |
, | danh sách rời | 0 8,12,18 * * * lúc 8h, 12h, 18h |
- | khoảng | 0 9-17 * * * mỗi giờ 9→17h |
/ | bước nhảy | */15 * * * * mỗi 15 phút |
Vài lịch mẫu sát công việc DE:
30 1 * * * /opt/etl/extract_core.sh # 01:30 mỗi đêm: trích core banking
0 2 * * 1-5 /opt/etl/load_staging.sh # 02:00 các ngày làm việc
*/10 8-18 * * * /opt/etl/poll_kafka.sh # mỗi 10 phút, giờ hành chính
0 6 1 * * /opt/report/monthly_close.sh # 06:00 ngày 1 hàng tháng
0 0 * * 0 /opt/maint/rotate_archive.sh # 00:00 mỗi Chủ Nhật
Cạm bẫy hay gặp: trường ngày-tháng và thứ dùng cùng lúc là "HOẶC" chứ không phải "VÀ". 0 0 13 * 5 chạy vào cả ngày 13 mỗi tháng và mọi thứ Sáu, không phải chỉ "thứ Sáu ngày 13". Muốn "thứ Sáu ngày 13" phải kiểm tra trong script.
Có thể dùng shortcut thay 5 trường: @daily (= 0 0 * * *), @hourly, @weekly, @monthly, @reboot (chạy khi máy khởi động).
crontab -e / -l / -r
Quản lý crontab của chính user hiện tại:
crontab -e # mở editor để sửa (an toàn: kiểm cú pháp trước khi lưu)
crontab -l # liệt kê crontab hiện tại
crontab -r # XOÁ SẠCH crontab — cẩn thận, không hỏi lại
crontab -u airflow -l # xem crontab của user khác (cần quyền root)
Luôn crontab -l > crontab.bak trước khi sửa lớn — -r không có "undo".
/etc/cron.d và crontab hệ thống
Ngoài crontab per-user, còn các nơi lịch hệ thống:
/etc/crontabvà/etc/cron.d/*: có thêm một trường user giữa thời gian và lệnh (chạy dưới danh nghĩa user nào). Đây là chỗ nên đặt lịch cho pipeline vì nó nằm trong file, dễ đưa vào git và Ansible/IaC, dễ review./etc/cron.daily/,/etc/cron.hourly/,/etc/cron.weekly/: bỏ script thực thi vào đây là chạy theo nhịp tương ứng (dorun-partsgọi), không cần khai giờ.
# /etc/cron.d/etl-extract — chú ý có trường user (deuser)
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
[email protected]
30 1 * * * deuser /opt/etl/extract_core.sh >> /var/log/etl/extract.log 2>&1
MAILTO: nhận cảnh báo khi job có output
cron gửi mail cho biến MAILTO (hoặc chủ crontab) bất cứ khi nào job in ra stdout hoặc stderr. Nguyên tắc ngầm: job thành công thì im lặng, có lỗi mới nói. Nếu script chỉ in khi lỗi, mọi mail nhận được đều là báo động thật. Đặt MAILTO="" để tắt hẳn mail (khi bạn đã ghi log riêng và có hệ giám sát khác).
Cạm bẫy lớn nhất: PATH và môi trường của cron
Đây là lý do #1 khiến DE mất buổi tối: script chạy tay hoàn hảo, nhưng cron báo fail hoặc "command not found".
cron chạy job trong một môi trường tối giản: PATH thường chỉ là /usr/bin:/bin, không load ~/.bashrc, ~/.profile, không có các biến JAVA_HOME, PYENV_ROOT, AIRFLOW_HOME mà bạn có khi đăng nhập tương tác. Hậu quả:
python/spark-submit/awskhông tìm thấy vì nằm ở/usr/local/binhay~/.local/bin— ngoài PATH của cron.- Script dựa vào
cdthư mục hiện tại: cron chạy từ$HOME, không phải nơi bạn tưởng. - Biến bí mật/kết nối đọc từ
~/.bashrckhông tồn tại → job kết nối DB thất bại.
Cách phòng, theo thứ tự nên làm:
# 1. Khai PATH và biến ngay trong crontab / file cron.d
PATH=/usr/local/bin:/usr/bin:/bin:/home/deuser/.local/bin
# 2. Dùng đường dẫn TUYỆT ĐỐI cho mọi lệnh và file trong script
30 1 * * * /usr/bin/python3 /opt/etl/extract.py
# 3. Cho script tự source môi trường ở đầu (dòng đầu script):
# #!/usr/bin/env bash
# set -euo pipefail
# source /opt/etl/env.sh # nạp biến kết nối, PATH bổ sung
Mẹo chẩn đoán nhanh: cho cron chạy env > /tmp/cronenv.txt một lần, so sánh với env khi bạn đăng nhập — bạn sẽ thấy ngay biến nào thiếu. (Chi tiết cơ chế shell ở mục môi trường & shell bên dưới.)
Ghi log đầy đủ
cron không tự lưu log job. Luôn tự redirect cả stdout và stderr, kèm timestamp:
30 1 * * * /opt/etl/extract_core.sh >> /var/log/etl/extract.log 2>&1
2>&1 gộp stderr vào chung file với stdout — thiếu nó thì lỗi (stderr) sẽ trôi vào mail hoặc mất hẳn. Để log không phình vô hạn, dùng logrotate (mục dưới).
flock: chống chồng job
Nguy hiểm âm thầm: job trích xuất chạy 40 phút nhưng lịch đặt mỗi 30 phút → hai lần chạy chồng nhau, cùng ghi một file, làm hỏng dữ liệu hoặc nhân đôi. flock giành một khoá file; nếu khoá đang bị giữ, lần chạy mới bỏ qua thay vì chồng lên:
# -n: non-blocking, không lấy được khoá thì thoát ngay (exit 1)
*/30 * * * * /usr/bin/flock -n /tmp/extract.lock /opt/etl/extract.sh >> /var/log/etl/extract.log 2>&1
Đây là mẫu bắt buộc cho mọi job có thể chạy lâu hơn khoảng cách lịch của nó.
at: chạy MỘT lần trong tương lai
Khi chỉ cần "chạy đúng một lần vào lúc..." (không lặp), dùng at thay cron:
echo "/opt/etl/backfill.sh 2026-06-01" | at 22:00
echo "/opt/etl/rerun.sh" | at now + 2 hours
at -l # atq: liệt kê job đang chờ
at -r 7 # atrm: huỷ job số 7
at kế thừa môi trường lúc bạn đặt job, phù hợp cho backfill thủ công, chạy lại một ngày lỗi, hay hoãn một tác vụ nặng sang giờ thấp điểm. Cần dịch vụ atd đang chạy.
systemd: service cho tiến trình chạy nền
Trên các distro hiện đại (Ubuntu, RHEL, Debian mới), systemd là hệ init quản lý mọi dịch vụ. Với DE, systemd hữu ích cho service dài hạn (consumer Kafka, API serving, worker) và — qua timer — cho cả lập lịch batch thay cron.
Một service unit mô tả cách chạy và giám sát một tiến trình. Ví dụ một consumer đọc giao dịch từ Kafka:
# /etc/systemd/system/txn-consumer.service
[Unit]
Description=NCB transaction Kafka consumer
After=network-online.target
[Service]
User=deuser
WorkingDirectory=/opt/consumer
EnvironmentFile=/opt/consumer/env # nạp biến kết nối
ExecStart=/usr/bin/python3 /opt/consumer/run.py
Restart=on-failure # tự khởi động lại khi crash
RestartSec=5
[Install]
WantedBy=multi-user.target
Điểm mạnh so với cron: systemd tự restart khi crash, quản lý phụ thuộc (After=), nạp biến qua EnvironmentFile (né đúng bẫy môi trường của cron), và gom log tập trung.
sudo systemctl daemon-reload # nạp lại sau khi sửa unit file
sudo systemctl start txn-consumer # chạy ngay
sudo systemctl enable txn-consumer # bật tự chạy khi boot
sudo systemctl status txn-consumer # trạng thái + vài dòng log gần nhất
sudo systemctl restart txn-consumer # khởi động lại
sudo systemctl stop txn-consumer # dừng
journalctl: xem log của service
systemd gom stdout/stderr của service vào journal. Xem bằng journalctl:
journalctl -u txn-consumer # toàn bộ log của service
journalctl -u txn-consumer -f # theo dõi trực tiếp (như tail -f)
journalctl -u txn-consumer --since "1 hour ago"
journalctl -u txn-consumer -p err # chỉ mức lỗi trở lên
journalctl -u txn-consumer --since today -o cat # gọn, chỉ nội dung
systemd timer: cron hiện đại
Thay vì crontab, systemd cung cấp timer — một unit .timer kích hoạt một unit .service cùng tên theo lịch. Cần hai file.
# /etc/systemd/system/extract.service (type oneshot: chạy rồi thoát)
[Unit]
Description=Extract core banking
[Service]
Type=oneshot
User=deuser
EnvironmentFile=/opt/etl/env
ExecStart=/opt/etl/extract_core.sh
# /etc/systemd/system/extract.timer
[Unit]
Description=Run extract at 01:30 daily
[Timer]
OnCalendar=*-*-* 01:30:00 # 01:30 mỗi ngày
Persistent=true # chạy bù nếu máy tắt lúc đến giờ
RandomizedDelaySec=120 # rải ngẫu nhiên tối đa 2 phút, tránh dồn tải
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now extract.timer # bật + chạy timer
systemctl list-timers # xem timer, lần chạy kế tiếp/gần nhất
Cú pháp OnCalendar linh hoạt: hourly, daily, Mon,Fri *-*-* 09:00, *-*-01 06:00 (ngày 1 hàng tháng)... Kiểm tra một biểu thức bằng systemd-analyze calendar "Mon *-*-* 09:00" — nó in ra lần kích hoạt kế tiếp, rất tiện để khỏi đoán mò.
Ưu điểm nổi bật của timer so với cron:
Persistent=true: nếu server tắt đúng lúc job đến giờ, khi bật lại timer chạy bù lần bị lỡ — cron thì mất luôn lần đó. Cực quan trọng cho job kết sổ không được phép bỏ.- Log qua journalctl thống nhất với mọi service, có exit code, thời lượng.
- Không có bẫy PATH: môi trường khai tường minh trong unit file.
RandomizedDelaySectránh hàng chục job cùng nổ lúc 00:00.
logrotate: quản lý log xoay vòng
Job chạy hằng đêm ghi log liên tục → file phình lên hàng GB, đầy đĩa. logrotate (chạy sẵn qua cron.daily) tự xoay vòng: đổi tên, nén, xoá log cũ.
# /etc/logrotate.d/etl
/var/log/etl/*.log {
daily # xoay mỗi ngày
rotate 30 # giữ 30 file cũ (30 ngày)
compress # gzip file đã xoay
delaycompress # hoãn nén file gần nhất (để đọc dễ)
missingok # không có file cũng không báo lỗi
notifempty # log rỗng thì bỏ qua
copytruncate # cắt file gốc thay vì tạo mới — an toàn nếu job đang ghi
}
copytruncate hữu ích khi tiến trình giữ file mở liên tục (không đóng/mở lại được), tránh việc log tiếp tục chảy vào file cũ đã đổi tên. Test cấu hình: logrotate -d /etc/logrotate.d/etl (chế độ debug, không đổi gì).
Biến môi trường và loại shell
Đây là gốc rễ giải thích vì sao script chạy tay OK mà cron/systemd fail. Cần phân biệt hai trục.
Trục 1 — login vs non-login shell:
- login shell: khi bạn SSH vào hoặc đăng nhập console. Nó đọc
/etc/profilerồi một trong~/.bash_profile→~/.bash_login→~/.profile(đọc cái đầu tiên tồn tại). - non-login shell: khi bạn mở terminal mới trong phiên đã đăng nhập, hoặc chạy script. Nó đọc
~/.bashrc.
Trục 2 — interactive vs non-interactive:
- interactive: có bàn phím gõ vào, có prompt.
- non-interactive: chạy một script rồi thoát. bash non-interactive không đọc
.bashrc(trừ khi bạn set biếnBASH_ENV).
Kết nối vào cron: cron chạy job trong shell non-login, non-interactive → không đọc .bashrc lẫn .profile. Đó là lý do JAVA_HOME, PATH mở rộng, alias... đều biến mất. Khi bạn chạy tay sau khi SSH vào (login shell, đã load profile), mọi thứ đủ đầy nên thấy "chạy được".
Các lệnh và khái niệm cần nắm:
export DB_HOST=10.20.30.40 # tạo biến MÔI TRƯỜNG (tiến trình con kế thừa)
DB_PORT=5432 # biến shell thường (KHÔNG kế thừa)
env # liệt kê mọi biến môi trường hiện có
printenv DB_HOST # in một biến
unset DB_HOST # xoá biến
export là chìa khoá: chỉ biến được export mới truyền xuống tiến trình con (python, spark-submit...). Quy ước file:
~/.profile/~/.bash_profile: chạy một lần lúc login → nơi đặtexport PATH,export JAVA_HOME(biến môi trường cho cả phiên).~/.bashrc: chạy mỗi shell non-login interactive → nơi đặt alias, prompt, thứ chỉ hữu ích khi gõ tay.
Bài học thực chiến: đừng phụ thuộc profile cho job tự động. Với cron/systemd, khai biến tường minh — PATH= trong crontab, EnvironmentFile= trong systemd, hoặc source /opt/etl/env.sh ở đầu script. Việc này cũng giúp job tái lập được khi chuyển server.
Quản lý package: cài công cụ
Để dựng môi trường chạy job, DE thường cần cài công cụ hệ thống. Trình quản lý package tuỳ họ distro:
| Họ distro | Trình quản lý | Cài | Cập nhật index | Gỡ |
|---|---|---|---|---|
| Debian/Ubuntu | apt | sudo apt install -y jq | sudo apt update | sudo apt remove jq |
| RHEL/CentOS cũ | yum | sudo yum install -y jq | sudo yum makecache | sudo yum remove jq |
| RHEL 8+/Fedora | dnf | sudo dnf install -y jq | sudo dnf makecache | sudo dnf remove jq |
sudo apt update && sudo apt install -y jq rsync postgresql-client
apt list --installed | grep postgres # kiểm gói đã cài
dnf provides */sqlplus # tìm gói chứa một file
Lưu ý ngân hàng: server production thường không có internet trực tiếp mà đi qua repo nội bộ (mirror) hoặc proxy. Cài công cụ cần đúng cấu hình repo nội bộ; đừng tự tiện thêm repo ngoài. Với thư viện Python, ưu tiên virtualenv (python3 -m venv) thay vì cài toàn hệ thống để tránh xung đột và giữ job tái lập.
cron vs systemd timer vs Airflow
Ba công cụ, ba tầng bài toán khác nhau:
| Tiêu chí | cron | systemd timer | Airflow |
|---|---|---|---|
| Cài đặt | Có sẵn mọi Linux | Có sẵn distro hiện đại | Phải dựng scheduler + DB + web |
| Lịch | 5 trường phút→thứ | OnCalendar linh hoạt | DAG schedule / cron expr |
| Chạy bù khi máy tắt | Không | Có (Persistent) | Có (catchup) |
| Log tập trung | Tự lo (redirect) | journalctl sẵn | UI + log per-task |
| Phụ thuộc giữa job | Không | Hạn chế (After=) | Mạnh (DAG, dependency) |
| Retry / báo lỗi | Tự viết + MAILTO | Restart= + journal | Retry, alert, SLA sẵn |
| Backfill | Tay (script) | Không | Có sẵn |
| Quan sát nhiều job | Kém | list-timers | Rất tốt (UI) |
Nguyên tắc chọn:
- cron: job đơn lẻ, độc lập, không phụ thuộc nhau — dọn log, backup rsync, một script trích xuất. Đơn giản, ai cũng đọc được.
- systemd timer: như cron nhưng cần chạy bù, log thống nhất, hoặc job là service dài hạn. Tốt cho server đơn hiện đại.
- Airflow: khi có nhiều job phụ thuộc nhau thành pipeline (extract → transform → aggregate → report), cần retry/backfill/SLA/giao diện theo dõi và điều phối trên nhiều máy. Xem Airflow — Tổng quan và Pipeline với Airflow.
Thực tế NCB: một script extract cô lập nằm ở cron/timer là đủ; nhưng khi nó trở thành một mắt xích của chuỗi 12 bước có phụ thuộc và cần rerun từng ngày lỗi, đó là lúc chuyển sang Airflow.
Use case thực tế
Bối cảnh: Đội DE cần trích file giao dịch từ core banking mỗi đêm 01:30, load vào staging, và không được chạy chồng (job có đêm cao điểm kéo dài tới 50 phút). Yêu cầu: log giữ 30 ngày, chạy bù nếu server bảo trì tắt lúc 01:30, cảnh báo mail khi lỗi. Server dùng Ubuntu 22.04 (có systemd).
Vấn đề gặp phải ban đầu (dùng cron): Đội đặt 30 1 * * * /opt/etl/extract.sh. Chạy tay thì ok, nhưng cron báo spark-submit: command not found vì PATH của cron không có /opt/spark/bin, và AIRFLOW_HOME không được set (biến này nằm trong ~/.bashrc, mà cron không đọc). Thêm nữa, một đêm bảo trì server tắt lúc 01:30 → mất hẳn lần trích đó, sáng ra thiếu dữ liệu.
Giải pháp — chuyển sang systemd timer:
# /etc/systemd/system/extract.service
[Service]
Type=oneshot
User=deuser
EnvironmentFile=/opt/etl/env # SPARK_HOME, PATH, biến kết nối
ExecStart=/usr/bin/flock -n /tmp/extract.lock /opt/etl/extract.sh
# /etc/systemd/system/extract.timer
[Timer]
OnCalendar=*-*-* 01:30:00
Persistent=true
RandomizedDelaySec=90
[Install]
WantedBy=timers.target
EnvironmentFile xử lý gọn bẫy môi trường; flock -n chống chồng job (lần sau bỏ qua nếu lần trước còn chạy); Persistent=true chạy bù khi máy vừa bật lại. logrotate ở /etc/logrotate.d/etl giữ 30 ngày log nén.
Con số minh hoạ: Sau khi chuyển, trong tháng đầu có 2 đêm server bảo trì; nhờ Persistent=true, cả 2 lần job tự chạy bù ngay khi máy bật lại lúc 02:15 — không còn thiếu ngày dữ liệu. Đêm cao điểm cuối tháng job chạy 50 phút, nhưng vì OnCalendar chỉ 01:30/ngày nên không chồng; flock là lớp bảo hiểm thứ hai. Việc chẩn đoán lỗi cũng nhanh hơn: journalctl -u extract.service --since today cho exit code và thời lượng thay vì phải mò từng file log rải rác.
Kiểm chứng:
systemctl list-timers extract.timer # xem lần chạy kế tiếp
journalctl -u extract.service -n 50 --no-pager
systemd-analyze calendar "*-*-* 01:30:00" # xác nhận biểu thức đúng
Ghi nhớ
- cron 5 trường = phút giờ ngày-tháng tháng thứ;
*mọi giá trị,,danh sách,-khoảng,*/nbước nhảy. Ngày-tháng và thứ dùng chung là "HOẶC". crontab -e/-l/-rcho crontab per-user;/etc/cron.d/*có thêm trường user, hợp cho pipeline vì đưa được vào git/IaC.MAILTOnhận cảnh báo; job im lặng khi ok.- Bẫy #1: cron chạy shell non-login/non-interactive → không đọc
.bashrc/.profile, PATH tối giản. Khắc phục: khaiPATH=trong crontab, dùng đường dẫn tuyệt đối,source env.shđầu script. - Luôn
>> log 2>&1để ghi cả stdout lẫn stderr; dùngflock -n /tmp/x.lockchống job chồng nhau. atcho job một lần trong tương lai (at 22:00,at now + 2 hours);atq/atrmđể xem/huỷ.- systemd service cho tiến trình nền:
systemctl start/enable/status, tựRestart=on-failure, log quajournalctl -u <unit>(-fđể theo dõi trực tiếp). - systemd timer = cron hiện đại:
OnCalendar,Persistent=truechạy bù khi máy từng tắt,RandomizedDelaySecrải tải, môi trường tường minh quaEnvironmentFile.systemctl list-timers,systemd-analyze calendar. - logrotate (
/etc/logrotate.d/*) xoay + nén + xoá log cũ;copytruncatean toàn khi tiến trình giữ file mở. - Biến:
exportmới truyền xuống tiến trình con;~/.profile/.bash_profilecho login shell,~/.bashrccho non-login interactive. Job tự động phải khai biến tường minh. - Package:
apt(Debian/Ubuntu),yum/dnf(RHEL/Fedora); production ngân hàng dùng repo nội bộ, Python nên dùng virtualenv. - Chọn scheduler: cron cho job đơn lẻ; systemd timer khi cần chạy bù/log thống nhất; Airflow cho pipeline nhiều bước phụ thuộc cần retry/backfill/SLA/UI.
Bài viết liên quan
Continuous Integration/Delivery/Deployment, cấu trúc pipeline, ví dụ GitHub Actions và chiến lược release.
Container vs máy ảo, image/layer, Dockerfile, volume, network, Docker Compose và best practices.
Vì sao cần orchestration; Pod, Deployment, Service, Ingress; scaling, self-healing và cấu hình.
Bản chất DevOps: phá bỏ rào cản Dev–Ops, vòng lặp vô tận, CALMS, và vì sao tự động hoá.