Linux cho DE 7 — Lập lịch: cron, systemd & môi trường

13 thg 7, 2026 3 lượt xem
#scheduling
#devops
#linux
#systemd
#cron

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ĩaVí dụ
*mọi giá trị* * * * * mỗi phút
,danh sách rời0 8,12,18 * * * lúc 8h, 12h, 18h
-khoảng0 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 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/crontab/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 (do run-parts gọ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/aws không tìm thấy vì nằm ở /usr/local/bin hay ~/.local/bin — ngoài PATH của cron.
  • Script dựa vào cd thư 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ừ ~/.bashrc khô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.
  • RandomizedDelaySec trá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/profile rồ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ến BASH_ENV).

Kết nối vào cron: cron chạy job trong shell non-login, non-interactivekhô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 đặt export 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ọ distroTrình quản lýCàiCập nhật indexGỡ
Debian/Ubuntuaptsudo apt install -y jqsudo apt updatesudo apt remove jq
RHEL/CentOS cũyumsudo yum install -y jqsudo yum makecachesudo yum remove jq
RHEL 8+/Fedoradnfsudo dnf install -y jqsudo dnf makecachesudo 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ícronsystemd timerAirflow
Cài đặtCó sẵn mọi LinuxCó sẵn distro hiện đạiPhải dựng scheduler + DB + web
Lịch5 trường phút→thứOnCalendar linh hoạtDAG schedule / cron expr
Chạy bù khi máy tắtKhôngCó (Persistent)Có (catchup)
Log tập trungTự lo (redirect)journalctl sẵnUI + log per-task
Phụ thuộc giữa jobKhôngHạn chế (After=)Mạnh (DAG, dependency)
Retry / báo lỗiTự viết + MAILTORestart= + journalRetry, alert, SLA sẵn
BackfillTay (script)KhôngCó sẵn
Quan sát nhiều jobKémlist-timersRấ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 quanPipeline 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 foundPATH 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, */n bước nhảy. Ngày-tháng và thứ dùng chung là "HOẶC".
  • crontab -e/-l/-r cho crontab per-user; /etc/cron.d/* có thêm trường user, hợp cho pipeline vì đưa được vào git/IaC. MAILTO nhậ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: khai PATH= 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ùng flock -n /tmp/x.lock chống job chồng nhau.
  • at cho 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 qua journalctl -u <unit> (-f để theo dõi trực tiếp).
  • systemd timer = cron hiện đại: OnCalendar, Persistent=true chạy bù khi máy từng tắt, RandomizedDelaySec rải tải, môi trường tường minh qua EnvironmentFile. systemctl list-timers, systemd-analyze calendar.
  • logrotate (/etc/logrotate.d/*) xoay + nén + xoá log cũ; copytruncate an toàn khi tiến trình giữ file mở.
  • Biến: export mới truyền xuống tiến trình con; ~/.profile/.bash_profile cho login shell, ~/.bashrc cho 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.

13 thg 7, 2026 4

Container vs máy ảo, image/layer, Dockerfile, volume, network, Docker Compose và best practices.

13 thg 7, 2026 3

Vì sao cần orchestration; Pod, Deployment, Service, Ingress; scaling, self-healing và cấu hình.

13 thg 7, 2026 3

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á.

13 thg 7, 2026 3