Chạy task tự động trên Linux với Crontab

Tại sao cần Crontab

Có rất nhiều việc chúng ta phải làm định kỳ nhưng quá mệt mỏi để làm bằng tay:

  • Gửi email thống kê cho khách hàng mỗi tuần
  • Backup cơ sở dữ liệu hàng ngày
  • Xóa những file cache có dung lượng quá lớn hàng tháng

Vì vậy crontab sinh ra để tự động lên lịch làm hộ chúng ta những việc này.
Give crontab a medal!

Cron là gì?

Cron là một tiện ích giúp lập lịch chạy những dòng lệnh bên phía server để thực thi một hoặc nhiều công việc (task hoặc job) nào đó theo thời gian được lập sẵn. Những task được lập lịch này được lưu trong file CRON TABle, thường nằm ở thư mục /var/spool/cron/crontabs. Trong thư mục này chứa các file crontab được đặt tên ứng với user. Ví dụ trong hệ thống có 2 user tên là rootquangtt thì sẽ có 2 file tương ứng là rootquangtt

$ ls -l /var/spool/cron/crontabs
-rw------- 1 quangtt crontab 1138 Th07 11 15:33 quangtt
-rw------- 1 root    crontab 1137 Th07 11 15:34 root

Những file này chúng ta KHÔNG NÊN tự chỉnh sửa, mà nên dùng crontab commands để thao tác, vì mỗi lần edit xong một file, cron sẽ tự động cập nhật các task được thêm mới, thay đổi. Ngoài ra, cron cũng tự động kiểm tra thư mục crontabs mỗi phút để kịp thời lên lịch. Vì vậy ta không cần restart các service tương ứng khi sửa xong file crontab.

Các lệnh crontab

Có 3 lệnh cơ bản mà chúng ta sẽ hay dùng:

  • crontab -l: Xem danh sách các task đã được lên lịch
  • crontab -r: Xóa toàn bộ các task đã được lên lịch
  • crontab -e: Chỉnh sửa file crontab
  • Ngoài ra crontab -h sẽ hiện hướng dẫn các lệnh còn lại.

Cú pháp crontab

┌───────────── minute (0 - 59)
│ ┌───────────── hour (0 - 23)
│ │ ┌───────────── day of month (1 - 31)
│ │ │ ┌───────────── month (1 - 12 or JAN - DEC)
│ │ │ │ ┌───────────── day of week (0 - 6 or SUN-SAT) (Sunday to Saturday;
│ │ │ │ │                                       7 is also Sunday on some systems)
│ │ │ │ │
│ │ │ │ │
* * * * *  /command/to/be/executed

Cú pháp trên gồm 2 phần:

  • Cụm 5 dấu sao đầu tiên để xác định thời gian
  • Cụm phía sau chính là lệnh chúng ta muốn chạy định kỳ

Mỗi vị trí của dấu * lần lượt đại diện cho:

  • Phút (0 - 59)
  • Giờ (0 - 23)
  • Ngày trong tháng (1 - 31)
  • Tháng (1 - 12)
  • Ngày trong tuần (0 - 6 với Chủ Nhật là 0)

Ví dụ tôi muốn gửi email cho khách vào mỗi tối Thứ 6 hàng tuần vào lúc 18:00:

┌───────────── Phút 0, tức là vào giờ tròn
│ ┌───────────── Vào lúc 18 giờ
│ │  ┌───────────── Chạy tất cả các ngày trong tháng
│ │  │ ┌───────────── Tháng nào cũng chạy
│ │  │ │ ┌───────────── Ngày 5 tức là Thứ Sáu
│ │  │ │ │                                     
│ │  │ │ │
│ │  │ │ │
0 18 * * 5  /command/to/send/an/email

Ở mỗi cột thời gian, ngoài các giá trị kể trên, còn có các giá trị sau:

  • Dấu sao (*): Đại diện cho tất cả các giá trị có thể
  • Dấu phẩy (,): Liệt kê giá trị. Ví dụ ở cột thứ 5 ta dùng MON,WED,FRI có nghĩa sẽ chạy vào Thứ Hai, Thứ Tư, Thứ Sáu trong tuần.
0 2 * * MON,WED,FRI  /command/to/send/an/email
  • Dấu gạch ngang (-): Khoảng giá trị. Ví dụ ở cột đầu tiên ta dùng 5-10 có nghĩa sẽ chạy vào phút thứ 5, 6, 7, 8, 9, 10.
5-10 2 * * *  /command/to/send/an/email
  • Dấu xược (/): Chia đều khoảng thời gian (bước nhảy). Ví dụ tôi muốn cứ 5 phút sẽ dọn rác một lần:
*/5 * * * *  /clean/the/system

Chý ý, đối với cột phút và giờ, giá trị phía sau dấu xược phải được chia hết bởi số phút lớn nhất và số giờ lớn nhất.
Cụ thể, số phút lớn nhất là 60 thì các giá trị có thể là: /2, /3, /4, /5, /6, /10, /12, /15, /20 và /30.
Số giờ lớn nhất là 24 thì các giá trị có thể là: /2, /3, /4, /6, /8 và /12.
Điều này để đảm bảo chúng ta có một tần suất đều đặn.
Nếu không thì sao? Thì bước nhảy sẽ bị "reset" khi qua mốc thời gian mới.
Ví dụ ta có /25 ở cột phút thì các mốc thời gian xảy ra sự kiện sẽ là: 9:00, 9:25, 9:50, 10:00 10:25, 10:50 chứ không phải là 9:00, 9:25, 9:50, 10:15, 10:40, nghĩa là nó sẽ không cộng số phút để ra thời gian tiếp theo.

Cú pháp đặc biệt

Ta có thể dùng các cú pháp đặc biệt sau để thay thế cho cụm 5 cột thời gian:

Cú pháp Ý nghĩa Tương đương
@yearly Chạy hàng năm vào nửa đêm ngày 1/1 0 0 1 1 *
@annually Tương tự @yearly 0 0 1 1 *
@monthly Chạy mỗi tháng vào nửa đêm ngày đầu tháng 0 0 1 * *
@weekly Chạy mỗi tuần vào nửa đêm Chủ Nhật 0 0 * * 0
@daily Chạy hàng ngày vào nửa đêm 0 0 * * *
@hourly Chạy mỗi giờ vào lúc giờ tròn 0 * * * *
@reboot Chạy mỗi khi khởi động hệ thống N/A

Chạy với quyền root

Bình thường thì không phải ai cũng để ý đến vấn đề này. Nhưng cũng dễ thôi.
Ví dụ tôi có một server, trên server có cài đặt SSL của Let's Encrypt và chứng chỉ SSL cần được gia hạn mỗi 3 tháng (ví dụ thế). Việc gia hạn này phải tự làm, tự chạy lệnh của Let's Encrypt để thực hiện gia hạn, và, lệnh này cần chạy với quyền root, tức là thêm sudo vào đầu dòng lệnh nếu như tự chạy bằng command ấy.
Nếu user của bạn là user thường mà vẫn cố lên lịch cho task trên, thì sẽ hoàn toàn không chạy được do bạn không có quyền. Còn việc print password của bạn ra để chạy với sudo là gần như cấm! Vì nó không an toàn:

0 0 * * * echo "password" | sudo -S /opt/letsencrypt/letsencrypt-auto renew

Có một cách đơn giản hơn nhiều, lại không cần phải thêm sudo vào đầu lệnh, đó là viết thẳng vào crontab của root! (Tất nhiên là nếu bạn có quyền truy cập vào root):

$ sudo crontab -e

Như thế này là bạn sẽ edit file crontab của root, và những gì trong đó cũng được thực hiện với quyền root, quá đơn giản! Lúc này không cần phải sudo gì nữa:

0 0 * * * /opt/letsencrypt/letsencrypt-auto renew

Chúng ta nên dùng cách này (nếu bạn có quyền root), tránh dùng cách in ra password như trên, rất là nguy hiểm.

Kết bài

Cú pháp crontab coi vậy mà khá dễ quên, hay bị lẫn, những lúc đó tôi hay vào https://crontab.guru để viết, vì nó diễn giải rất dễ hiểu, rất hay cho những bạn mới làm quen!