Arbiter và worker trong ASGI server

Một ASGI server hiệu năng cao thường tách quy trình master và quy trình con thành kiến trúc arbiter–worker. Arbiter chịu trách nhiệm khởi tạo môi trường chung (listen socket, SSL context, cấu hình), spawn nhiều worker process rồi giám sát vòng đời của chúng. Mỗi worker chạy event loop riêng và xử lý request độc lập, không chia sẻ trạng thái với worker khác ngoài phần socket lắng nghe. Mô hình này lặp lại ở Gunicorn (WSGI/ASGI), uWSGI, và uvicorn khi cấu hình nhiều worker; uASGI là một cài đặt tham chiếu tối giản để minh họa rõ pattern.

flowchart TD
    A["Arbiter - master process"] -->|fork + inherit fd| W1["Worker 1 - event loop + Server"]
    A -->|fork + inherit fd| W2["Worker 2 - event loop + Server"]
    A -->|fork + inherit fd| W3["Worker N - event loop + Server"]
    W1 -.->|stdout pipe| A
    W2 -.->|stdout pipe| A
    W3 -.->|stdout pipe| A

Trách nhiệm của arbiter

Arbiter tạo listen socket trước khi fork worker. Trong uASGI, Config.create_socket mở socket với các cờ SO_REUSEPORT, SO_REUSEADDR, TCP_NODELAY, đặt non-blocking và set_inheritable(True). Khi worker fork qua multiprocessing.Process, worker kế thừa file descriptor của socket nên cả N worker cùng accept() trên một socket chung. Hai cờ tái sử dụng port cho phép quá trình khởi tạo socket mới trên cùng địa chỉ trong tình huống reload mà không phải chờ trạng thái TIME_WAIT.

Arbiter giữ một multiprocessing.Event làm tín hiệu stop và chạy một asyncio event loop trong thread daemon riêng để gom log. Khi worker emit ra qua write end của pipe, arbiter đăng ký reader trên read end và dùng os.sendfile(out_fd, in_fd, 0, 1024) để copy zero-copy sang stdout của master, tránh đẩy buffer log qua user-space.

Vòng đời của worker

Worker là một multiprocessing.Process không daemon — daemon process sẽ bị parent kill bất ngờ khi thoát, làm rớt request đang xử lý. Trong process con, worker dup2 write end của pipe lên sys.stdout.fileno()sys.stderr.fileno() để mọi log từ ứng dụng chảy về master qua pipe đã thiết lập. Sau đó worker load app từ chuỗi module:attr, khởi tạo Server rồi gọi asyncio.run để vào event loop.

Khi người dùng gửi Ctrl-C, kernel broadcast SIGINT cho toàn bộ process group — gồm cả arbiter và worker. Arbiter bắt KeyboardInterruptstop_event.wait() rồi join từng worker. Worker tự bắt KeyboardInterrupt trong server.main() và gọi server.stop() để event loop đóng accept loop. Cơ chế truyền tín hiệu qua process group đảm bảo shutdown đồng bộ mà không cần kênh IPC riêng cho tín hiệu.

So sánh với SO_REUSEPORT thuần

Pattern phổ biến khác là mỗi worker tự bind socket với SO_REUSEPORT, kernel sẽ hash 4-tuple để phân phối kết nối tới một worker cụ thể. Pattern fork và inherit như uASGI ngược lại: một socket duy nhất chia sẻ qua fd cho mọi worker, kernel cho phép nhiều process cùng accept() đồng thời trên fd đó. Hai cách đều khai thác đa nhân CPU; pattern inherit không yêu cầu kernel hỗ trợ SO_REUSEPORT, đổi lại các worker chia sẻ chung một accept queue thay vì có queue riêng cho mỗi socket.

Trải nghiệm cá nhân

Wiki này được kết tinh khi viết uASGI tại Zen8labs (7-8/2025) — practice cá nhân để hiểu cặn kẽ ASGI và cơ chế high-performance web server. Pattern arbiter–worker được học từ đọc source Gunicorn rất lâu trước; tác giả thừa nhận đây là vùng còn cần đào sâu phần vì sao chứ không chỉ cách làm. Chi tiết bối cảnh trong transcript phỏng vấn.

Cập nhật: 2026-05-29