Thông thường daemon tự bind cổng khi khởi động. Với systemd socket activation, systemd mở listening socket trước, giữ nó độc lập với vòng đời tiến trình và chỉ khởi động service khi có traffic. Cách này cho phép khởi động theo nhu cầu, bind cổng đặc quyền mà daemon vẫn chạy user không đặc quyền, đồng thời giữ hàng đợi kết nối mới trong lúc service được khởi động lại.
Bài viết triển khai cặp unit .socket/.service, nhận file descriptor bằng sd_listen_fds(), phân biệt Accept=no và Accept=yes, hardening, rollout và debug. Nội dung dựa trên nguồn systemd chính thức về socket unit, socket activation API và công cụ systemd-socket-activate.
1. Socket activation thay đổi quyền sở hữu cổng
Trong mô hình truyền thống, daemon gọi socket(), bind(), listen() rồi accept(). Nếu daemon chưa chạy hoặc đang restart, không tiến trình nào giữ listening socket.
Với socket activation:
- systemd tạo và giữ listening socket từ unit
.socket. - Kết nối đến kích hoạt unit
.servicetương ứng. - systemd truyền file descriptor đã mở cho tiến trình.
- Daemon dùng descriptor đó thay vì bind lại địa chỉ.
Tên mặc định phải khớp: report-api.socket kích hoạt report-api.service. Có thể override bằng Service= với Accept=no, nhưng quy ước cùng tên dễ vận hành hơn.
2. Unit socket TCP tối thiểu
# /etc/systemd/system/report-api.socket
[Unit]
Description=Report API listening socket
[Socket]
ListenStream=127.0.0.1:9100
Accept=no
Backlog=256
NoDelay=yes
FileDescriptorName=http
[Install]
WantedBy=sockets.target
ListenStream= tạo TCP stream socket khi dùng địa chỉ IP và port. Accept=no là mặc định: service nhận listening socket và tự gọi accept() cho mọi client. FileDescriptorName= hữu ích khi service nhận nhiều socket và muốn nhận diện chúng bằng tên thay vì chỉ dựa vào thứ tự.
Địa chỉ loopback phù hợp khi reverse proxy cùng máy đứng phía trước. Nếu bind 0.0.0.0 hoặc [::], service có thể tiếp xúc mạng ngoài; socket activation không thay thế firewall hay access control.
3. Unit service chạy không đặc quyền
# /etc/systemd/system/report-api.service
[Unit]
Description=Report API service
Requires=report-api.socket
After=report-api.socket network.target
[Service]
Type=simple
User=report-api
Group=report-api
ExecStart=/usr/local/bin/report-api
Restart=on-failure
RestartSec=2s
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/lib/report-api
[Install]
WantedBy=multi-user.target
PID 1 bind cổng rồi truyền descriptor, vì vậy daemon không cần capability chỉ để mở port thấp. Tuy nhiên, quyền đọc cấu hình, ghi dữ liệu và gọi network outbound vẫn phải được khai báo phù hợp với sandbox thực tế.
Trong mô hình thuần on-demand, thường enable/start socket thay vì service. Nếu service luôn phải chạy, vẫn có thể enable service; ứng dụng cần xử lý socket được truyền khi có và có policy rõ khi chạy ngoài systemd.
4. Nhận file descriptor trong C
#include <systemd/sd-daemon.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int count = sd_listen_fds(1);
if (count < 0) {
fprintf(stderr, "sd_listen_fds failed: %d\n", count);
return EXIT_FAILURE;
}
if (count != 1) {
fprintf(stderr, "expected one activated socket, got %d\n", count);
return EXIT_FAILURE;
}
int listen_fd = SD_LISTEN_FDS_START; /* file descriptor 3 */
if (sd_is_socket_inet(listen_fd, AF_UNSPEC, SOCK_STREAM, 1, 9100) != 1) {
fprintf(stderr, "unexpected socket\n");
return EXIT_FAILURE;
}
for (;;) {
int client = accept4(listen_fd, NULL, NULL, SOCK_CLOEXEC);
if (client < 0) continue;
handle_client(client);
close(client);
}
}
Protocol native đặt descriptor liên tiếp từ SD_LISTEN_FDS_START, thường là FD 3, và cung cấp biến môi trường như LISTEN_PID, LISTEN_FDS, LISTEN_FDNAMES. Dùng thư viện libsystemd thay vì tự tin mù vào FD 3: helper kiểm tra descriptor có thực sự dành cho PID hiện tại và có thể xóa biến môi trường sau khi đọc.
5. Nhiều socket và tên descriptor
[Socket]
ListenStream=127.0.0.1:9100
ListenStream=/run/report-api/admin.sock
SocketMode=0660
SocketUser=report-api
SocketGroup=report-admin
FileDescriptorName=report-api
Một unit có thể khai báo nhiều socket; mọi socket của unit được truyền khi service kích hoạt. Khi kiến trúc cần phân biệt rõ public/admin, tách thành hai socket unit có FileDescriptorName= riêng và để service dùng sd_listen_fds_with_names(). Không dựa vào thứ tự giữa nhiều socket unit vì tài liệu không bảo đảm ordering giữa các unit.
Với Unix socket nằm trên filesystem, SocketMode=, SocketUser= và SocketGroup= tạo hàng rào truy cập ngay tại node socket. Đây không phải lựa chọn tương đương cho TCP.
6. Accept=no và Accept=yes
| Chế độ | Service nhận gì | Phù hợp |
|---|---|---|
Accept=no | Listening socket | HTTP server, daemon hiệu năng cao, event loop |
Accept=yes | Một connected socket cho mỗi instance | Dịch vụ thưa, chương trình kiểu inetd, cô lập từng connection |
Với Accept=yes, echo.socket cần template echo@.service; systemd tạo một instance cho mỗi connection. Cách này đơn giản hóa chương trình chỉ biết xử lý một phiên, nhưng chi phí fork/process và số unit tăng nhanh. Tài liệu systemd khuyến nghị Accept=no cho dịch vụ nhạy hiệu năng vì chỉ connection đầu trả activation cost.
Datagram socket và FIFO luôn do một service xử lý; Accept= không có cùng ý nghĩa per-connection ở đó.
7. Restart giữ được gì, không giữ được gì?
Khi chỉ service dừng nhưng socket unit vẫn active, systemd tiếp tục giữ listening socket. Kết nối mới có thể nằm trong listen backlog và kích hoạt service lại. Điều này giảm cửa sổ connection refused khi process chưa sẵn sàng.
Socket activation không tự di chuyển connection đã được daemon cũ accept(). Những connection đang xử lý vẫn thuộc tiến trình cũ. Muốn deploy không gián đoạn cho HTTP keep-alive, WebSocket hoặc phiên dài, cần graceful shutdown: ngừng nhận connection mới, drain connection cũ theo deadline và chỉ kill khi hết thời gian. Có thể cần reverse proxy, nhiều instance hoặc cơ chế FD handoff riêng.
Không quảng cáo “zero downtime” chỉ vì cổng luôn mở; hãy đo success rate và latency trong lúc restart.
8. Activation storm, backlog và giới hạn
Service lỗi ngay sau khi khởi động có thể bị traffic kích hoạt lặp lại. Kết hợp:
StartLimitIntervalSec=vàStartLimitBurst=trong unit/service policy.TriggerLimitIntervalSec=vàTriggerLimitBurst=cho socket phù hợp.Restart=on-failurevới delay có chủ đích.- Alert theo activation failure thay vì để client chỉ thấy timeout.
Backlog= không phải hàng đợi vô hạn. Khi startup chậm hoặc lưu lượng burst vượt capacity, client vẫn có thể timeout hoặc bị từ chối. Định kích thước bằng load test, kernel limits và SLO; không dùng backlog để che một service không đủ công suất.
9. Bảo mật ở ranh giới socket
- Bind địa chỉ hẹp nhất có thể; loopback hoặc Unix socket nếu chỉ proxy local cần truy cập.
- Đặt owner/group/mode chặt cho filesystem socket.
- Chạy daemon bằng user riêng, không dùng root chỉ để bind port.
- Dùng firewall cho TCP và xác thực ở application/reverse proxy.
- Hardening service bằng
NoNewPrivileges, namespace và filesystem policy sau khi test. - Không tin
REMOTE_ADDRnhư identity; địa chỉ mạng không thay thế authentication.
Với Accept=yes, sandbox mỗi service instance có thể cô lập connection tốt hơn, nhưng phải đặt giới hạn process, memory và activation rate để tránh cạn tài nguyên.
10. Cài đặt và rollout
sudo systemd-analyze verify \
/etc/systemd/system/report-api.socket \
/etc/systemd/system/report-api.service
sudo systemctl daemon-reload
sudo systemctl enable --now report-api.socket
systemctl status report-api.socket
systemctl list-sockets --all | grep report-api
curl http://127.0.0.1:9100/health
Ngay sau enable --now, socket phải ở trạng thái listening còn service có thể inactive. Request đầu kích hoạt service. Rollout an toàn nên chứng minh thứ tự này trên staging, sau đó theo dõi startup latency và response request đầu trên production.
Khi bảo trì không muốn traffic kích hoạt lại service, dừng socket trước rồi dừng service. Chỉ dừng service trong khi socket còn active có thể khiến connection kế tiếp khởi động nó lại.
11. Test daemon trước khi cài unit
systemd-socket-activate \
--listen=127.0.0.1:9100 \
--fdname=http \
/usr/local/bin/report-api
systemd-socket-activate mô phỏng protocol truyền descriptor từ command line. Test cần bao phủ không có FD, sai số lượng FD, sai socket type/port, nhiều FD có tên, service crash, restart và burst khi service chưa sẵn sàng. Application nên fail rõ nếu cấu hình yêu cầu activation nhưng descriptor nhận được không hợp lệ.
12. Quan sát và debug
systemctl status report-api.socket report-api.service
systemctl show report-api.socket -p ActiveState -p Triggers
journalctl -u report-api.socket -u report-api.service --since today
ss -ltnp 'sport = :9100'
systemctl list-sockets --all
Hãy phân biệt ba lớp lỗi: socket không listen, service không activate và service activate nhưng không xử lý FD. Metric nên có activation count/failure, thời gian từ connection đầu tới service ready, backlog pressure, request latency/error trong restart và số graceful shutdown bị quá deadline.
Nếu service báo address already in use, thường code vẫn tự bind thay vì dùng descriptor được truyền. Nếu request treo, kiểm tra daemon có gọi accept() trên listening FD đúng loại hay không.
Checklist production
- Tên
.socket/.servicehoặcService=mapping đúng. - Daemon dùng
sd_listen_fds()và xác minh loại socket, không hardcode mù FD 3. - Chọn
Accept=no/yestheo concurrency và isolation thực tế. - Listening address, firewall và Unix socket permissions đúng phạm vi.
- Service chạy user riêng và hardening không chặn tài nguyên cần thiết.
- Backlog, restart policy và trigger/start limits đã load test.
- Graceful drain xử lý connection đã accept; không nhầm socket activation với migration connection.
- Đã test cold start, crash loop, burst, maintenance stop và rollback.
- Dashboard phân biệt socket state, activation failure và application failure.
Kết luận
systemd socket activation tách tuổi thọ listening socket khỏi tuổi thọ daemon. Kết quả là service có thể khởi động theo traffic, chạy không đặc quyền và restart mà cổng cùng hàng đợi kết nối mới vẫn do systemd giữ. Giá trị này chỉ phát huy khi daemon nhận và xác minh FD đúng protocol, unit có giới hạn tài nguyên, security boundary rõ và deployment có graceful drain cho connection đã accept. Triển khai tốt bắt đầu bằng cặp unit nhỏ, test bằng systemd-socket-activate, rồi đo request đầu và restart dưới tải thật.




Chưa có bình luận. Hãy là người đầu tiên chia sẻ ý kiến.