Skip to main content

3.1. Operations Deployment: Boot, systemd service và bash script

Mục lục​


1. Reboot và shutdown an toàn​

Một server vừa được vá kernel và cần reboot, nhưng cửa sổ bảo trì duy nhất lại là hai hoặc ba giờ sáng. Thức dậy giữa đêm chỉ để gõ một lệnh là việc không ai muốn làm. Linux có sẵn cách hẹn giờ cho đúng tình huống đó, nhưng trước khi tới phần hẹn giờ thì cần nắm hai lệnh cơ bản nhất đã.

Lệnh dùng để đổi trạng thái máy là systemctl, viết tắt của system control:

sudo systemctl reboot
sudo systemctl poweroff

Đổi trạng thái hệ thống là việc cần quyền quản trị. User root đã có sẵn quyền đó nên gõ thẳng systemctl reboot là đủ. User thường không được phép, nhưng có thể tạm vay quyền root bằng cách thêm sudo phía trước. Quy ước này áp dụng cho mọi ví dụ về sau trong tài liệu: đang đăng nhập bằng root thì bỏ chữ sudo đi. Chi tiết về cơ chế sudo nằm ở Part 4.2: sudo, truy cập root và xác thực tập trung.

Thỉnh thoảng máy từ chối reboot hoặc shutdown theo cách bình thường, thường là vì một chương trình nào đó treo và không chịu đóng lại. Lúc đó có thể ép đóng mọi chương trình và khởi động lại theo cách thô bạo hơn:

sudo systemctl reboot --force
sudo systemctl poweroff --force

Nếu cả cách này vẫn không ăn, --force được phép khai hai lần. Đây là phương án cuối cùng, chỉ dùng khi không còn lựa chọn nào khác: hệ thống reboot ngay lập tức và chương trình không có cơ hội đóng file hay lưu dữ liệu.

💡 Hình dung: --force --force với reboot giống bấm nút Reset trên thùng máy, còn với poweroff thì giống rút thẳng dây nguồn ra khỏi ổ điện. Máy dừng tức khắc, mọi thứ đang nằm trong RAM mất sạch.

Quay lại bài toán ban đầu. Việc reboot theo lịch nên dùng lệnh shutdown, vì lệnh này nhận được tham số thời gian:

sudo shutdown 02:00

Thời gian viết theo định dạng 24 giờ, nhận giá trị từ 00:00 tới 23:59. Muốn tắt máy sau X phút tính từ lúc gõ lệnh thì dùng dạng +X:

sudo shutdown +15

Mặc định shutdown là tắt máy. Thêm option -r để biến nó thành reboot:

sudo shutdown -r +15

Trên một server có nhiều người đang đăng nhập, việc máy biến mất không báo trước khiến ai cũng mất việc đang làm mà không hiểu vì sao. shutdown cho phép kèm một wall message, tức thông báo gửi tới mọi user đang đăng nhập:

sudo shutdown -r +15 'Scheduled restart to upgrade our Linux kernel'

Thông báo nằm trong cặp nháy đơn, hiện ra trước khi máy reboot hoặc tắt. Nó cho người đang làm việc biết chuyện gì sắp xảy ra, vào lúc nào, và vì lý do gì, để họ kịp lưu lại công việc đang dở.

Khi vận hành, thói quen nên giữ là: lệnh có hẹn giờ thì luôn kèm wall message, và --force chỉ xuất hiện khi lệnh bình thường đã thất bại thật sự.


2. systemd target và trạng thái boot mặc định​

Để đi tới được màn hình login, hàng chục chương trình phải nạp lên, một số trong đó phải nạp theo đúng thứ tự. Linux biết phải làm gì nhờ cái gọi là systemd target.

Lúc boot, hệ điều hành xem target mặc định là gì, rồi đọc file target đó để biết cần khởi chạy những chương trình nào và theo trình tự nào. Target hiện tại tra bằng:

systemctl get-default

# Output:
graphical.target

Tên graphical.target cho biết hệ điều hành đang được cấu hình để boot vào môi trường đồ họa. Boot vào graphical target khá nặng. Một server không cần giao diện đồ họa thì nên đổi target mặc định:

sudo systemctl set-default multi-user.target

multi-user.target làm Linux boot lên bình thường với đầy đủ daemon, database server, utility và mọi thứ đã cấu hình, chỉ bỏ qua phần giao diện đồ họa: tất cả chuyển sang dạng text. Tên "multi-user" đến từ chỗ nhiều user có thể đăng nhập và dùng hệ thống cùng lúc khi máy đang ở mode này. Target này cũng bật các network service, nên máy vẫn truy cập được internet và mạng nội bộ.

Reboot sau khi đổi target mặc định sẽ thấy console login dạng text thay cho màn hình login đồ họa.

Nhưng giả sử cần dùng một ứng dụng 3D trong một giờ. Đổi target mặc định rồi reboot là cách vòng vo. Có cách chuyển sang target khác ngay lập tức mà không reboot:

sudo systemctl isolate graphical.target

Lệnh này nạp toàn bộ ứng dụng đồ họa lên và trả lại giao diện người dùng. Nó không đổi target mặc định: reboot máy thì vẫn rơi về console text, vì multi-user.target đã được set làm mặc định ở bước trước. isolate chỉ là cách tạm thời bật giao diện đồ họa lên.

Hai target khác cũng hữu ích khi cần cứu máy là emergency.target và rescue.target.

Boot target decides what systemd loadsPower on, GRUBsystemd reads default targetsystemctl get-defaultgraphical.targetLoads the GUI loginHeavier: full desktopsystemctl isolateswitches now, no rebootmulti-user.targetText-only console loginDaemons plus networksystemctl set-defaultchanges the next bootisolate switches the running target right now.set-default only decides what the next boot does.

3. init system và systemd unit​

Khi Linux boot lên, một số ứng dụng quan trọng tự khởi chạy, và một số trong đó khởi chạy theo thứ tự nhất định: nếu app 2 phụ thuộc app 1 thì app 1 nạp trước. Ứng dụng quan trọng bị crash cũng được restart lại tự động, nhờ vậy hệ thống chạy tiếp bình thường dù có một cú trượt nhỏ.

Việc này do init system lo, viết tắt của initialization system. Để làm được việc, init system cần chỉ dẫn cụ thể, và chỉ dẫn đó nằm trong các systemd unit: những file text mô tả logic cần thiết.

Chữ systemd mang hai nghĩa, dễ gây nhầm. Nó là tên một bộ sưu tập lớn gồm nhiều tool, component và ứng dụng giúp khởi chạy, vận hành, quản lý phần lớn các hệ điều hành nền Linux. Nó cũng là tên của chính chương trình chạy với vai trò init system, theo dõi hệ thống như một tổng thể để đảm bảo mọi thứ chạy trơn.

Unit có nhiều loại: service, socket, device, timer và các loại khác. Timer unit cho phép bảo init system khởi chạy một ứng dụng mỗi tuần một lần, chẳng hạn để dọn file hoặc kiểm tra database. Phần còn lại của mục này tập trung vào service unit.

Service unit chứa chỉ dẫn rõ ràng về những thứ như: lệnh nào dùng để khởi chạy chương trình, phải làm gì khi chương trình crash, lệnh nào chạy khi chương trình được restart, và nhiều thứ khác nữa. Nó nói cho init system biết toàn bộ những gì cần biết để quản lý vòng đời của một ứng dụng. Danh sách đầy đủ các chỉ dẫn có thể khai trong một service unit nằm ở:

man systemd.service

Một ví dụ thật: server cần chạy SSH daemon để người dùng kết nối từ xa, và có một service unit hướng dẫn init system cách khởi chạy daemon đó cùng cách giữ nó sống. Mở file service của SSH daemon ra sẽ thấy những dòng kiểu này:

# Output (rút gọn):
ExecStart=/usr/sbin/sshd -D $SSHD_OPTS
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure

ExecStart nói cho init system biết lệnh nào cần chạy khi muốn khởi động SSH daemon. ExecReload nói lệnh nào cần chạy để nạp lại cấu hình cho daemon. Dòng Restart bảo systemd tự restart process này, nhưng chỉ khi có cái gì đó thất bại: SSH daemon crash thì restart lại là hợp lý, để còn kết nối được vào server.

Muốn sửa file service này và đổi các chỉ dẫn trong đó thì dùng lệnh sửa của systemctl, còn muốn bỏ hết các thay đổi đã làm và đưa file service về đúng mặc định của nhà sản xuất thì dùng:

sudo systemctl revert ssh.service

Nắm được service unit là gì rồi, phần việc thường ngày là điều khiển nó qua systemctl.


4. Quản lý service bằng systemctl​

Lệnh đầu tiên cần biết là lệnh xem trạng thái:

systemctl status ssh.service

Output của lệnh này nhiều thông tin, và dưới đây là những phần đáng đọc nhất.

Service có được enable hay không. Enable nghĩa là systemd sẽ tự khởi chạy SSH daemon mỗi lần hệ thống boot. Disable nghĩa là nó không tự chạy, nhưng người quản trị vẫn khởi động tay được.

Chương trình do service quản lý có đang chạy hay không. Nếu nó đã được nạp vào bộ nhớ và đang chạy thì trạng thái hiện active (running). Có trường hợp service ở trạng thái active nhưng chương trình lại không chạy, vì nó đã khởi chạy, làm xong việc rồi thoát; lúc đó trạng thái hiện active (exited).

PID của process. Mỗi lần khởi chạy một chương trình trên Linux, một process được tạo ra. Process bao gói phần code và tài nguyên được nạp vào bộ nhớ, dùng CPU khi cần thực thi lệnh. Mỗi process có một số định danh duy nhất gọi là PID, dùng để tương tác với process đó. Phần thao tác với PID nằm ở Part 3.2: Process, log hệ thống và giám sát tài nguyên.

Lệnh chính xác đã dùng để khởi chạy process, cộng với vài dòng log cuối: các thông báo trạng thái và lỗi do ứng dụng sinh ra. Phần log này cho biết chuyện gì xảy ra sau khi chương trình khởi chạy, nó có gặp lỗi gì không, cấu hình nào đang có hiệu lực.

Nhấn q để thoát khỏi màn hình status.

Dừng, khởi động, restart và reload​

sudo systemctl stop ssh.service
sudo systemctl start ssh.service

stop đóng chương trình mà service unit đó điều khiển. start là cách khởi động tay một service không tự chạy lúc boot.

Khi đổi cấu hình của một chương trình, ví dụ sửa cách SSH daemon hoạt động trong file /etc/ssh/sshd_config, process đang chạy vẫn dùng cấu hình cũ cho tới khi được khởi động lại. Một cách ép nó đọc lại cấu hình mới:

sudo systemctl restart ssh.service

restart có thể hơi thô: nếu đang có vài người dùng chương trình đó, việc restart làm gián đoạn công việc của họ. Cách nhẹ nhàng hơn là nạp lại cấu hình mà không đóng rồi mở lại cả ứng dụng:

sudo systemctl reload ssh.service

Không phải ứng dụng nào cũng hỗ trợ reload. Với trường hợp không chắc, có lệnh tự thử reload trước rồi mới restart nếu ứng dụng không hỗ trợ reload:

sudo systemctl reload-or-restart ssh.service

Enable, disable và mask​

Muốn chặn SSH daemon tự khởi chạy lúc boot:

sudo systemctl disable ssh.service
systemctl is-enabled ssh.service

is-enabled là cách kiểm tra nhanh thay cho việc đọc cả output của status. Bật lại:

sudo systemctl enable ssh.service

⚠️ Lưu ý: nếu đang thực hành trên máy ảo riêng và vừa disable SSH service, hãy enable lại ngay, nếu không lần boot tới sẽ không đăng nhập lại được.

Sau khi cài một ứng dụng mới, nhất là daemon hoặc server dạng database server, web server, thường có hai việc cần làm: enable nó để tự chạy mỗi lần hệ điều hành boot, và khởi động tay để dùng được ngay lập tức. Hệ điều hành thuộc nhóm Debian và Ubuntu thường tự làm hai bước này sau khi cài một số chương trình, nhưng nhóm Red Hat thì không, nên việc biết làm tay vẫn cần thiết. Hai lệnh enable rồi start làm được việc đó, hoặc gộp thành một lệnh:

sudo systemctl enable --now some.service
sudo systemctl disable --now some.service

Option --now bảo systemctl vừa enable service để tự chạy mỗi lần boot, vừa khởi động nó ngay. Lệnh disable cũng nhận --now, tương đương disable rồi stop.

Có những service khá cứng đầu: đã disable khỏi autostart và đã stop, nhưng một lúc sau vẫn thấy nó chạy. Nguyên nhân là một service có thể tự khởi chạy service khác, tạo ra hiệu ứng domino: service 1 kéo service 2 lên dù service 2 đã bị disable và stop. Cách chặn thô bạo cho chuyện này gọi là masking:

sudo systemctl mask atd.service

atd là daemon cho phép lập lịch những task cần chạy trong tương lai, chi tiết nằm ở Part 3.3: Lập lịch task, kernel parameter và quản lý package. Trên một số hệ điều hành như Ubuntu và các biến thể, atd có thể chưa được cài sẵn và cần cài tay.

Một service đã bị mask thì không enable được, cũng không start được. Hai lệnh đó sẽ thất bại với thông báo kiểu "failed to enable unit" và "failed to start the service". Cách này đảm bảo service không bao giờ khởi động được, kể cả khi một chương trình, user hay service khác muốn khởi động nó. Hủy mask:

sudo systemctl unmask atd.service

Tìm đúng tên service​

Các lệnh systemctl khá dễ đoán. Phần khó đoán hơn lại là tên service của một ứng dụng cụ thể. Cài web server Apache và trông đợi service tên apache.service, nhưng trên một số hệ điều hành nó lại tên httpd.service. Vì vậy nên biết cách lấy danh sách toàn bộ service unit có trên hệ thống, bất kể chúng đang enable, disable, start hay stop:

systemctl list-units --type=service --all

systemctl list-units khi đứng một mình sẽ in mọi loại systemd unit, gồm cả socket unit, timer unit và các loại khác. --type=service giới hạn output chỉ còn service unit. Kết hợp thêm --all để thấy tất cả service unit, không phụ thuộc trạng thái của chúng.

Current state and boot behaviour are two axesinactive, deadnot loaded in memoryactive, runningthe process has a PIDsystemctl startsystemctl stopmaskedcannot start at allnot even by other unitssystemctl mask or unmaskBoot behaviour is a separate axis from the state aboveenabledstarts automatically at bootenable --now does bothdisabledno autostart at bootstill startable by hand

5. Viết bash script để tự động hóa​

Sau khi đăng nhập vào Linux, màn hình hiện ra một command line. Chuyện xảy ra bên dưới là: đăng nhập thành công thì một chương trình tên Bash mở ra, và người dùng được đưa vào môi trường text của nó. Mọi lệnh gõ vào đều do Bash thông dịch, Bash tự tính xem cần làm gì để thực thi chúng. Vì vậy Bash còn được gọi là command interpreter, hay shell.

Cách dùng vừa mô tả là dùng tương tác: viết một dòng, nhấn Enter, nhận kết quả. Nhưng Bash còn một cách dùng khác là script. Script là file chứa nhiều chỉ dẫn cho command interpreter. Interpreter đọc file đó và thực thi các chỉ dẫn theo thứ tự: dòng một trước, rồi dòng hai, rồi tiếp tục.

Cách hiểu nhanh nhất là dựng thử một cái. Tạo file script.sh:

vim script.sh

Đuôi .sh là quy ước chuẩn. Nó không bắt buộc, chỉ giúp nhìn output của ls là nhận ra ngay file nào là script. Có một số ít tình huống phải bỏ đuôi .sh đi để chương trình khác chịu thực thi file; ví dụ như vậy nằm ở phần lập lịch task của Part 3.3.

Nội dung file:

#!/bin/bash

# Ghi lại thời điểm chạy và phiên bản kernel đang dùng
date >> /tmp/script.log
cat /proc/version >> /tmp/script.log

Dòng đầu tiên quan trọng nhất. Nó phải nằm ở dòng một, không phải dòng hai hay dòng ba, và không được có khoảng trắng nào trước dấu #. Dòng này gọi là shebang: dấu thăng cộng dấu chấm than cộng đường dẫn đầy đủ tới command interpreter sẽ chạy script. Ở đây là /bin/bash, tức Bash.

Dấu # ở đầu một dòng khác thì đánh dấu dòng đó là comment. Comment là thứ interpreter bỏ qua, không thực thi gì bất kể viết gì trong đó. Nó hữu ích khi cần ghi chú cho người khác đọc script hiểu dòng kế tiếp làm gì, hoặc cho chính mình đọc lại sáu tháng sau.

Lệnh date in ra ngày và giờ, và redirection đưa output của nó vào file /tmp/script.log. /proc/version là file đặc biệt chứa thông tin chi tiết về phiên bản kernel Linux đang chạy, nội dung của nó cũng được đẩy vào cùng file log. Hai dòng này tạo ra một script theo dõi được server đã từng chạy những phiên bản kernel nào trong quá khứ. Lệnh trong script viết y như lúc gõ ở command line: redirect output, redirect lỗi, pipe sang chương trình khác đều được, xem thêm ở Part 2.4: grep, regular expression, archive và I/O redirection.

Lưu file bằng Escape, rồi :wq, rồi Enter. Muốn chạy được script thì phải cấp quyền thực thi cho nó:

chmod u+x script.sh
chmod +x script.sh

Dòng trên chỉ cho owner của file, tức user hiện tại, được thực thi. Dòng dưới cấp quyền thực thi cho tất cả: user owner, group owner và others. Phần phân quyền chi tiết nằm ở Part 2.2: Phân quyền file và permission đặc biệt.

Để chạy, phải đưa đường dẫn đầy đủ tới script, hoặc dùng dạng dấu chấm:

/home/aaron/script.sh
./script.sh

Dấu chấm đại diện cho thư mục hiện tại. Đứng trong /home/aaron mà gõ ./script.sh thì dấu chấm được thay bằng /home/aaron, nên lệnh tương đương với /home/aaron/script.sh. Xem kết quả:

cat /tmp/script.log

Thiết lập để script này tự chạy mỗi ngày là có một lịch sử đầy đủ mọi phiên bản kernel đã dùng. Khi có sự cố, đọc lại log này là biết vấn đề bắt đầu từ lúc cập nhật lên bản 5.15.x.

Bash built-in: if và test​

Phần cốt lõi của việc viết script chỉ gồm vậy: tạo file, cấp quyền thực thi, viết shebang trỏ tới đường dẫn của interpreter, rồi viết các chỉ dẫn cho interpreter đó. Script thông minh hơn thì cần tới Bash built-in. Danh sách nhanh các built-in có sẵn:

help

Danh sách in ra khá dài, nhưng hai built-in dưới đây đủ để thấy chúng mở ra những gì: if và test.

Bài toán cụ thể: viết script backup nội dung thư mục /etc/app. Tạo file archive-app.sh bằng vim (vim tự tạo file khi lưu, nên chạy lệnh được dù file chưa tồn tại):

#!/bin/bash

tar -czf /tmp/archive.tar.gz /etc/app

Cấp quyền thực thi rồi chạy, archive được tạo ra. Xem danh sách file bên trong:

tar tf /tmp/archive.tar.gz

Giờ hình dung tình huống xấu. Ai đó xóa nhầm vài file trong /etc/app. Script backup chạy tiếp theo lịch. /tmp/archive.tar.gz bị tạo lại, ghi đè lên bản backup trước. Kết quả là có một /etc/app hỏng và một /tmp/archive.tar.gz cũng hỏng, vì nó vừa backup đúng cái thư mục đã hỏng. Bản backup cũ, bản còn chứa nội dung tốt của /etc/app, đã mất.

Script có thể thông minh hơn thế. Logic cần có: kiểm tra xem /tmp/archive.tar.gz có tồn tại hay không bằng if và test. Nếu có thì đổi tên nó thành /tmp/archive.tar.gz.old rồi mới tạo /tmp/archive.tar.gz mới. Như vậy luôn có hai bản backup, bản mới và bản cũ ngay trước đó. Nếu chưa có file archive nào thì tạo bản đầu tiên của chuỗi.

Thông tin về một built-in và cú pháp của nó tra bằng help kèm tên nó, ví dụ help if và help test. File archive-app-2.sh triển khai logic trên:

#!/bin/bash

if test -f /tmp/archive.tar.gz; then
mv /tmp/archive.tar.gz /tmp/archive.tar.gz.old
tar -czf /tmp/archive.tar.gz /etc/app
else
tar -czf /tmp/archive.tar.gz /etc/app
fi

Đọc file đó theo từng dòng thì if mở một lần kiểm tra điều kiện: nó xác minh biểu thức đứng sau nó là đúng hay sai. Bên trong điều kiện là built-in test. Theo help test, built-in này làm được nhiều loại kiểm tra; với file nó kiểm tra được file thuộc loại nào, có thực thi được không, và nhiều thứ nữa. Ở đây option -f bắt nó kiểm tra /tmp/archive.tar.gz có tồn tại dưới dạng regular file hay không. Có thì test trả về true, không thì trả về false.

if không tự biết phần điều kiện kết thúc ở đâu, nên phải báo cho nó bằng dấu ; ở cuối điều kiện cần xác minh. Chữ then ở cuối đánh dấu những gì sẽ xảy ra nếu test trả về true: hai lệnh phía dưới được thực thi, đổi tên file archive.tar.gz đang có thành archive.tar.gz.old, rồi tạo archive mới tên archive.tar.gz.

Bốn khoảng trắng trước mỗi lệnh là quy ước chuẩn để script dễ đọc. Các dòng thụt vào bên phải giúp nhìn một cái là thấy ngay phần nào chạy khi test trả về true.

else đánh dấu những gì xảy ra khi test trả về false, tức khi /tmp/archive.tar.gz không tồn tại: chỉ tạo một archive đơn giản, không đổi tên bản cũ vì không có bản cũ nào. Dòng cuối là fi, chính là if viết ngược, đánh dấu kết thúc khối if.

Lưu file, cấp quyền thực thi rồi chạy. Nhìn vào /tmp sẽ thấy archive cũ đã được chuyển sang file có đuôi .old, và một archive mới vừa được tạo. Hai bản cùng tồn tại, đúng như thiết kế.

archive-app-2.sh keeps two backup generationsScript runstest -f /tmp/archive.tar.gzdoes a previous backup existif trueif falsePrevious backup foundmv /tmp/archive.tar.gz/tmp/archive.tar.gz.oldthen tar a fresh archiveNo backup yettar the first archivenothing to renamefi closes the if blockTwo generations on disk

Exit status: cách if đánh giá mọi lệnh​

Gần như lệnh nào cũng dùng được trong một khối if, vì phần lớn lệnh trả về cái gọi là exit status code. Lệnh chạy thành công thì trả về 0. Lệnh không tìm thấy thứ nó đi tìm, hoặc gặp lỗi, thì trả về một giá trị lớn hơn 0. Khi if thấy lệnh trả về 0, nó coi đó là true; khi lệnh trả về giá trị khác 0, if coi đó là false.

grep là ví dụ rõ: tìm thấy text cần tìm thì trả về 0, không thấy thì trả về 1. Script dưới đây kiểm tra GRUB có timeout 5 giây hay không:

#!/bin/bash

if grep -q 5 /etc/default/grub; then
echo "GRUB has a timeout of 5 seconds"
else
echo "GRUB does not have a timeout of 5 seconds"
fi

grep tìm số 5 trong file /etc/default/grub. Option -q là quiet, bảo grep không in ra các dòng khớp, vì script không cần những dòng đó. Script in ra "GRUB has a timeout of 5 seconds" nếu tìm thấy số 5, và in ra "GRUB does not have a timeout of 5 seconds" nếu không tìm thấy. Cấp quyền thực thi rồi chạy là thấy kết quả.

Đó là phần nền của việc viết script với Bash built-in. Script cần logic phức tạp hơn thì phải đi sâu vào các built-in khác như vòng lặp for và while, function, biến, những thứ cho phép dựng script theo lối lập trình thật.

Cần nhắc lại cú pháp nhanh thì có một nguồn tra cứu sẵn trên máy: các file trong /etc/cron.daily, /etc/cron.weekly và /etc/cron.monthly. Hầu hết file ở đó đều là script, nên chúng thành một bảng ghi nhớ sống về cú pháp hay dùng: shebang ở dòng đầu, cú pháp if, và những thứ tương tự.


6. Tự tạo một systemd service file​

Giả sử có một ứng dụng tự viết, cần chạy thường trực trên server. Vấn đề của chương trình là chúng có thể crash, nên cần một thứ canh chừng ứng dụng và restart lại nó mỗi khi nó chết. Cũng muốn nó tự khởi chạy khi hệ thống boot. systemd service làm được cả hai việc và còn nhiều hơn: service quản lý toàn bộ vòng đời ứng dụng theo các chỉ dẫn viết trong service file.

Trước hết cần một ứng dụng để thực hành. Tạo /usr/local/bin/myapp.sh:

sudo vim /usr/local/bin/myapp.sh
#!/bin/bash

echo "MyApp started" | systemd-cat -t MyApp -p info
sleep 5
echo "MyApp encountered an error" | systemd-cat -t MyApp -p err

Dòng echo đầu tiên gửi một thông báo tới syslog nói rằng MyApp đã khởi chạy. systemd-cat là utility nhận thông báo đó và ghi vào log. Option -t thêm một identifier vào log để biết các thông báo này thuộc một chương trình tên MyApp. Option -p đặt mức ưu tiên cho thông báo, ở đây là info, tức báo cho system logger biết đây là thông báo trạng thái bình thường và ứng dụng đang hoạt động đúng.

Dòng kế tiếp bắt ứng dụng giả này chờ 5 giây, mô phỏng việc nó đang làm việc đúng trong khoảng thời gian đó. Dòng cuối đặt mức ưu tiên của log thành lỗi, báo cho system logger biết ứng dụng gặp vấn đề; logger sẽ tô đỏ dòng này cho nổi bật. Kịch bản mô phỏng ở đây là: ứng dụng gặp lỗi sau 5 giây, và in ra thông báo lỗi đó lúc thoát.

Lưu file rồi cấp quyền thực thi cho nó.

Tìm option trong manual​

Phần khó khi viết service file không phải cú pháp, mà là tìm ra đúng option cần dùng. Manual của systemd service unit file là nơi tra:

man systemd.service

Hai mục đáng xem trong man page này. Thứ nhất là mục OPTIONS, chứa các option cấu hình cho service. Option Restart nói cho systemd biết khi nào nên restart ứng dụng, và các giá trị gán được cho nó gồm: no để systemd không bao giờ restart nó, on-failure để chỉ restart khi ứng dụng trả về error code, và always để systemd luôn restart ứng dụng mỗi khi nó thoát. Ứng dụng demo ở đây quá đơn giản để trả về error code nào, nên giá trị phù hợp là always.

Cuộn lên một chút sẽ thấy option RestartSec, đi cặp rất khớp với option trên. Nó bảo systemd chờ một lúc trước khi thử restart một ứng dụng vừa dừng. Ứng dụng demo chờ 5 giây trước khi thoát nên không cần độ trễ này, nhưng một ứng dụng thật có thể lỗi và thoát chỉ trong vài millisecond; nếu systemd restart ngay lập tức, ứng dụng sẽ restart hàng trăm lần mỗi phút. Hiện tượng đó gọi là restart loop và nó làm hệ thống chậm đi. Bắt systemd chờ vài giây trước khi restart là cách tránh.

Thứ hai là mục EXAMPLES. Ví dụ số hai trong đó là service file systemd đơn giản nhất có thể tạo, rất hữu ích để nắm cấu trúc file. Nhìn vào đó thấy cần có ba section: [Unit], [Service] và [Install].

Dựng file từ một template có sẵn​

Viết một service file từ con số không khi mới bắt đầu thì khó, vì không biết chính xác cần viết text gì. Có một mẹo: thư mục /lib/systemd/system chứa sẵn các service unit file đang dùng thật, nên lấy một file ở đó làm điểm khởi đầu là có ngay bộ khung để sửa.

Service file tự tạo thì nên đặt vào thư mục /etc/systemd/system:

sudo cp /lib/systemd/system/ssh.service /etc/systemd/system/myapp.service
sudo vim /etc/systemd/system/myapp.service

Section [Unit]. Đầu tiên là dòng mô tả service unit, sửa lại cho khớp ứng dụng mới. Ứng dụng demo không có manual nên dòng khai tài liệu ở dưới xóa đi được.

Dòng kế tiếp là dòng quan trọng nhất của section [Unit]. Nó bảo systemd khi nào nên chạy service unit này; ở file gốc của SSH, nó được yêu cầu chỉ chạy sau khi cả network.target và service auditd đã khởi động. Một target là một tập hợp service cần khởi động cùng nhau, nên dòng này nói với systemd rằng chỉ khởi động service khi các service liên quan tới mạng và service auditd đã lên. Việc này thiết yếu vì phần lớn ứng dụng phụ thuộc ứng dụng hoặc service khác: SSH daemon không nên khởi động trước khi mạng được cấu hình, nếu không nó không biết phải nghe trên IP address và network interface nào cho các kết nối vào. Dòng này gom các dependency của ứng dụng lại và đảm bảo chúng đã khởi động và đang chạy trước khi ứng dụng chạy. Với mục đích demo thì cứ để nguyên dòng này.

Dòng sau đó có một dấu chấm than, ký hiệu của phủ định, nghĩa là "không phải cái này". Nó bảo systemd chỉ khởi động service nếu file được trỏ tới không tồn tại. Demo không cần dòng này nên xóa, nhưng nó cho thấy một service file có thể chi tiết tới mức nào trong các chỉ dẫn của nó.

Section [Service]. Đây là nơi khai cách vòng đời ứng dụng được điều khiển: khởi động, restart, reload thế nào, và chuyện gì xảy ra khi nó crash.

Dòng đầu khai một environment file, chỉ việc nạp các biến môi trường liệt kê trong file đó, ví dụ biến PATH. Demo không cần nên xóa.

ExecStartPre cho phép khai một chương trình cần chạy trước khi ứng dụng khởi chạy, kiểu dọn đường trước: có thể chạy một chương trình quét lỗi database trước khi khởi động database server. Ở demo này, đổi nó thành một lệnh in ra thông báo để sau đó nhìn thấy trong log, xác nhận rằng lệnh ở ExecStartPre đã chạy trước ứng dụng.

ExecStart khai lệnh dùng để khởi chạy ứng dụng, nên trỏ nó tới đường dẫn của shell script vừa viết.

ExecReload khai thứ sẽ được thực thi khi người quản trị chạy systemctl reload, tức khi có người muốn reload nhẹ nhàng để áp cấu hình mới. Ứng dụng demo không hỗ trợ reload nên xóa cả hai dòng ExecReload.

KillMode khai cách ứng dụng này bị dừng, ví dụ khi người quản trị chạy systemctl stop. Một service unit có thể khởi chạy nhiều process, hoặc ứng dụng do service chạy lại tự sinh ra nhiều process con, nên systemd cần biết nó phải dừng những process nào. KillMode=process bảo systemd chỉ kill process chính, không kill các process con có thể đã được sinh ra; với demo thì giữ nguyên giá trị này. Trong tình huống khác có thể đặt KillMode=control-group để kill mọi process do service tạo ra.

Dòng tiếp theo trong file gốc bảo systemd chỉ restart ứng dụng khi có lỗi. Ứng dụng demo luôn thoát bình thường với exit code 0 nên systemd sẽ không restart nó với thiết lập đó. Đổi dòng này thành Restart=always để nó được restart mỗi lần thoát, rồi thêm RestartSec=1 như đã xem trong manual: systemd chờ đủ một giây trước khi restart.

Chỉ dẫn RestartPreventExitStatus=255 bảo systemd không restart service nếu ứng dụng trả về exit code 255. Demo không cần nên xóa.

Type quan trọng hay không tùy loại ứng dụng. Type=notify nói với systemd rằng ứng dụng sẽ tự thông báo khi nó khởi động xong. Ứng dụng demo không có khả năng đó nên đặt Type=simple. Một giá trị hay dùng khác là Type=oneshot, dành cho ứng dụng không cần chạy nền, ví dụ chương trình mở lên nhanh, xóa vài file rồi thoát vì hết việc. Type=forking cũng hay gặp, dành cho ứng dụng sinh nhiều process con khi được thực thi.

Hai dòng cuối của section này khai đường dẫn tương đối tới thư mục mà ứng dụng nên chạy trong đó, cùng quyền của thư mục đó, dùng khi ứng dụng cần tạo file tạm hoặc tương tự. Demo không cần nên xóa cả hai.

Section [Install]. Dòng đầu tiên cần hiểu khái niệm systemd target. Một server thường boot lên không có giao diện đồ họa, nhưng nhiều user vẫn đăng nhập được vào giao diện text; để có môi trường đó, một tập hợp ứng dụng phải khởi động, và tập hợp này được gom vào multi-user.target. Nếu server boot lên có giao diện đồ họa thì đó là graphical.target. Có nhiều target khác, mỗi target chỉ là một tập hợp service cần khởi động để đạt một mục đích nhất định; network.target ở trên gom các service cần cho một mạng hoạt động. Dòng này nói với systemd rằng service đang khai thuộc về target multi-user.

Dòng Alias bảo systemd tạo link tới file .service ở các vị trí khác. Lý do nó có mặt trong file gốc: service quản lý SSH daemon tên sshd.service trên hệ điều hành nhóm Red Hat, nhưng trên nhóm Debian và Ubuntu nó tên ssh.service, không có chữ d ở cuối. Người quản trị quen gõ systemctl reload sshd.service sẽ gặp lỗi trên Ubuntu; nhờ link file ssh.service sang một file nữa tên sshd.service, cả hai tên đều dùng được trên Ubuntu. Demo không cần alias nên xóa dòng này.

File service hoàn chỉnh sau khi dọn:

[Unit]
Description=MyApp demo service
After=network.target auditd.service

[Service]
ExecStartPre=/bin/echo "ExecStartPre ran before MyApp"
ExecStart=/usr/local/bin/myapp.sh
KillMode=process
Restart=always
RestartSec=1
Type=simple

[Install]
WantedBy=multi-user.target
The three sections of myapp.service[Unit]Description, After=network.target auditd.serviceanswers: when is this service allowed to startdocumented in man systemd.unit[Service]ExecStartPre, ExecStart, ExecReloadRestart=always with RestartSec=1Type=simple, oneshot, forking, notifyKillMode=process or control-groupanswers: how to run, restart and kill itsee man systemd.exec and man systemd.kill[Install]WantedBy=multi-user.target, Alias=sshd.serviceanswers: which target pulls it in at boot timeRun systemctl daemon-reload after every edit to this file.

Nạp và kiểm tra service​

Mỗi lần thêm, xóa hoặc sửa service file, phải nạp lại systemd daemon:

sudo systemctl daemon-reload
sudo systemctl start myapp.service
journalctl -f

Option -f bật follow mode, hiển thị log ngay khi chúng được sinh ra. Mỗi lần ứng dụng crash sẽ thấy thông báo lỗi tô đỏ, và systemd bắt được điều đó rồi tự restart ứng dụng. Chỉ dẫn ExecStartPre cũng chạy đúng, nhận ra qua thông báo đã thêm vào option đó. Nhấn Ctrl-C để thoát follow mode.

Việc viết service file không khó, chỉ là một đống chỉ dẫn trong một file. Phần khó là tìm ra option cần dùng. Biết mình muốn systemd làm gì rồi thì tra bằng cách gõ man systemd. (có dấu chấm ở cuối, không khoảng trắng) rồi nhấn Tab hai lần để thấy danh sách manual liên quan.

Không phải option nào cũng nằm trong man systemd.service. Những thứ khai trong section [Unit] đầu tiên của service file nằm ở man systemd.unit. Các chi tiết về cách ứng dụng trong service được thực thi nằm ở man systemd.exec. Chi tiết về cách systemd dừng hoặc kill process do service khởi chạy nằm ở man systemd.kill.

Người mới thì học nhanh hơn bằng cách đọc các service file có sẵn. Chạy ls /lib/systemd/system rồi mở vài file ra xem chúng được viết thế nào để phục vụ mục đích của từng ứng dụng cụ thể.


Nguồn tham khảo​

Nguồn gốc: Khóa "Prep Course - Linux Foundation Certified System Administrator (LFCS) Certification" — phần "Operations Deployment" (boot, reboot và shutdown Linux an toàn; systemd target và default boot target; dùng scripting để tự động hóa tác vụ bảo trì; quản lý startup process và service; tạo systemd service file), nền tảng KodeKloud. Giảng viên: Jeremy Morgan và Alexandru Andrei.