Vấn Đề Khởi Tạo & Thoát Khỏi Dịch Vụ - nói dối e blog

Vấn Đề Khởi Tạo & Thoát Khỏi Dịch Vụ

Tóm tắt: Mỗi đơn vị (dịch vụ) tồn tại song song với hệ thống trong hệ thống TL;DR đều phải cung cấp giao diện tắt thay vì giao diện giải phóng. Thao tác tắt chỉ cần thực hiện những bước thiết yếu, không cần giải phóng tài nguyên hay đồng bộ hóa với các thành phần khác. Khi toàn bộ hệ thống thoát, chỉ cần lệnh mọi thành phần tắt rồi dừng đột ngột toàn bộ hệ thống.

Gần đây khi cùng đồng nghiệp hoàn thiện ltask - một dự án mang tính hồi cố skynet (định hướng ứng dụng cho engine phía client), chúng tôi gặp phải nhiều vấn đề trong quy trình thoát tiến trình do cách xử lý các dịch vụ độc quyền.

Trong ltask, các thread timer, network trước đây được viết bằng C trong skynet giờ được chuyển sang triển khai bằng Lua, gọi là exclusive service. Chúng độc chiếm một thread hệ điều hành nhưng phần lớn giống dịch vụ thường. Sự thay đổi này làm mờ ranh giới giữa tầng dịch vụ nền tảng với các dịch vụ ứng dụng.

Các dịch vụ hạ tầng như timer, network, logging có đặc điểm đặc biệt:

  • Chúng tạo thành cơ sở hạ tầng cho toàn hệ thống
  • Tồn tại sự phụ thuộc chéo giữa các thành phần (ví dụ: logging phụ thuộc network và timer)
  • Gần như mọi dịch vụ đều có thể ghi log ở giai đoạn thoát

Khi tiến hành dọn dẹp code thoát tiến trình, sau khi sửa nhiều lỗi deadlock, tôi nhận ra cần thiết kế lại từ gốc:

  1. Dịch vụ có tên nên được xem là thành phần vĩnh cửu của tiến trình
  2. Quy trình thoát chỉ cần một lần gửi lệnh tắt, không cần đồng bộ giải phóng tài nguyên
  3. Việc “tắt” khác với “chấm dứt hoàn toàn” - dịch vụ vẫn có thể xử lý một số yêu cầu nhất định sau khi nhận lệnh tắt

Khác biệt quan trọng giữa ltask và skynet nằm ở điều kiện thoát tiến trình:

  • skynet: thoát khi toàn bộ dịch vụ kết thúc (đếm ngược về 0)
  • ltask: thoát khi root service (dịch vụ đầu tiên) dừng - tạo cơ chế trung tâm hóa kiểm soát thoát

Quy trình thoát trong ltask diễn ra như sau:

  1. Root service gửi tín hiệu tắt đến mọi dịch vụ có tên (cả độc quyền lẫn chia sẻ thread)
  2. Chờ xác nhận tất cả đã xử lý xong thao tác thiết yếu
  3. Root service tự dừng, kích hoạt cơ chế dừng toàn bộ hệ thống

Điểm đặc biệt: Sau khi nhận lệnh tắt, dịch vụ có quyền từ chối xử lý yêu cầu mới nhưng phải hoàn thành các tác vụ đang dang dở. Việc giải phóng tài nguyên thực hiện ở giai đoạn thu dọn cuối cùng sau khi toàn bộ dịch vụ dừng.

Về việc khởi tạo dịch vụ có tên, ltask áp dụng mô hình lazy initialization thông qua name service:

  • Không nhúng vào tầng framework như skynet
  • Name service đảm nhận cả vai trò quản lý vòng đời dịch vụ
  • Tự động kích hoạt dịch vụ khi có yêu cầu truy vấn tên

Triết lý thiết kế này mang lại nhiều lợi ích:

  • Đơn giản hóa logic dịch vụ con: không cần xử lý trường hợp dịch vụ đã tồn tại
  • Giảm thiểu deadlock do phụ thuộc giữa các dịch vụ hạ tầng
  • Tăng tính nhất quán trong quản lý vòng đời dịch vụ

Trải nghiệm thực tế từ skynet cho thấy: Trong hệ thống server game, thao tác thiết yếu khi tắt thường chỉ gồm lưu dữ liệu và ngắt kết nối mạng. Khác với mô hình RAII của C++, không phải dịch vụ nào cũng cần cơ chế khởi tạo/đóng gói hoàn chỉnh. Điều quan trọng là xác định rõ thao tác bắt buộc khi tắt, không nên sa đà vào quản lý giải phóng từng tài nguyên chi tiết.

Cơ chế mới trong ltask kết hợp ưu điểm của cả hai thế giới:

  • Tính mềm dẻo của Lua trong triển khai dịch vụ hệ thống
  • Kiểm soát tập trung thông qua root service
  • Quản lý tên linh hoạt nhờ name service
  • Thoát tiến trình an toàn nhưng dứt khoát
0%