Cập Nhật Gần Đây Của Skynet Và Sửa Lỗi Trong Sproto - nói dối e blog

Cập Nhật Gần Đây Của Skynet Và Sửa Lỗi Trong Sproto

Phiên bản 1.0 của Skynet đã chính thức phát hành 3 phiên bản alpha, và sẽ tiếp tục ra mắt phiên bản beta sau khi hoàn tất ổn định hóa. Những vấn đề gần đây chủ yếu tập trung vào các tính năng chưa từng được sử dụng trong các dự án cũ. Đặc biệt là module Sproto - giao thức truyền thông được khuyến nghị cho các dự án tương lai. Tuy nhiên, do các dự án cũ của chúng tôi bắt đầu trước khi Sproto ra đời, toàn bộ đều sử dụng Google Protocol Buffers (kèm theo một số cải tiến do tôi tự phát triển). Khi triển khai các dự án mới, Sproto đã được áp dụng rộng rãi trong nội bộ công ty, qua đó phát hiện và sửa chữa nhiều lỗi tồn đọng.

Với kiến trúc đa Lua VM của Skynet, để tránh việc tải trùng lặp giao thức Sproto trong từng VM, chúng tôi đã bổ sung cơ chế chia sẻ đối tượng giao thức Sproto giữa các VM. Đây là một tính năng chưa được ghi chú trong tài liệu chính thức, nhưng hoàn toàn không ảnh hưởng đến việc sử dụng Sproto trong các lĩnh vực khác. Tuy nhiên, do triển khai gấp gáp, phiên bản đầu tiên đã để lại lỗi tiềm ẩn trong quá trình thu gom rác (GC), có thể dẫn đến việc nhiều VM cùng giải phóng đối tượng C nhiều lần. Vấn đề này đã được khắc phục trong commit mới nhất trên kho mã nguồn.

Một tính năng đặc biệt khác được thêm vào để phục vụ Skynet là cho phép hàm giải mã (decode) của Sproto nhận tham số kiểu con trỏ (lightuserdata). Mặc dù việc chỉ chấp nhận kiểu string sẽ giúp hệ thống ổn định hơn, nhưng trong Skynet nhiều nơi sử dụng kết hợp string và lightuserdata + size. Việc hỗ trợ đồng thời giúp giảm một lần sao chép bộ nhớ không cần thiết.

Dựa trên phản hồi từ người dùng, chúng tôi đã cải tiến phần thông báo lỗi trong binding Lua của Sproto để cung cấp thông tin chi tiết hơn, hỗ trợ định vị lỗi chính xác hơn trong quá trình sử dụng thực tế. Đồng thời tăng cường kiểm tra kiểu nghiêm ngặt hơn. Việc thiếu kiểm tra kiểu có thể coi là lỗi nghiêm trọng, bởi mục đích sử dụng Sproto thay vì JSON (một giao thức không có kiểm tra kiểu) chính là để thực hiện kiểm tra kiểu mạnh mẽ. Đặc biệt với các kiểu phức tạp (được hiện thực bằng table trong Lua), việc thiếu kiểm tra có thể dẫn đến sụp đổ tiến trình - điều hoàn toàn không thể chấp nhận.

Một lỗi nghiêm trọng cuối cùng xuất phát từ thiết kế gốc:

API mã hóa (encode) của Sproto sử dụng cơ chế callback. Người dùng (thường là các binding từ ngôn ngữ khác) cung cấp hàm callback, nhân C sẽ gọi hàm này mỗi lần xử lý một trường dữ liệu theo giao thức. Nếu callback trả về -1, quá trình mã hóa sẽ báo lỗi (thường do bộ đệm không đủ lớn). Trả về 0 cho biết trường dữ liệu không tồn tại - điều được phép trong Sproto để bỏ qua các trường không cần thiết. Với mảng, việc trả về 0 cũng được dùng để đánh dấu kết thúc mảng. Các trường hợp khác cần trả về số nguyên dương chỉ độ dài dữ liệu cần mã hóa.

Với kiểu dữ liệu đơn giản như boolean hay integer, giá trị trả về là cố định (boolean trả về 4, integer trả về 4 hoặc 8 tương ứng với số 32bit/64bit). Nhân C sẽ xử lý chuyển đổi địa chỉ và sắp xếp bộ nhớ theo quy tắc mã hóa, đồng thời giải quyết vấn đề thứ tự byte. Với kiểu dữ liệu có độ dài thay đổi như string hoặc kiểu tự định nghĩa, giá trị trả về giúp nhân C xác định cần dịch chuyển con trỏ bộ đệm bao nhiêu byte. Tuy nhiên, khi string rỗng (độ dài 0), hệ thống sẽ hiểu nhầm là trường không tồn tại, dẫn đến không mã hóa được string rỗng. Nghiêm trọng hơn, nếu gặp string rỗng trong mảng, quá trình mã hóa sẽ dừng lại. Giải pháp được áp dụng là quy ước trả về độ dài string + 1. Đây là lỗi thiết kế, nên không chỉ binding Lua mà các binding ngôn ngữ khác (như Python) cũng cần điều chỉnh. May mắn là binding Python do nhóm phát triển nội bộ thực hiện nên có thể sửa nhanh chóng.

Với kiểu tự định nghĩa, ngay cả khi đối tượng trống vẫn có header dữ liệu, nên không thể trả về 0. Trong trường hợp đối tượng trống không nằm trong mảng, binding Lua sẽ trả về 0 để bỏ qua trường này, nhưng nếu nằm trong mảng sẽ trả về header dữ liệu trống.

Một lỗi khác được phát hiện trong sharemap (cơ chế dựa trên Sproto) là do tham chiếu vòng lặp trong metatable. Khi một trường không tồn tại, Lua sẽ phát hiện vòng lặp metatable và báo lỗi.

Vấn đề nữa phát sinh khi sử dụng httpc: Lớp socket của Skynet gọi trực tiếp hàm hệ thống getaddrinfo khi xử lý tên miền, gây chặn luồng xử lý. Vì Skynet sử dụng kiến trúc socket đơn luồng, việc tra cứu tên miền sẽ làm trì hoãn toàn bộ xử lý tin nhắn socket. Mặc dù trước đây ít gặp vấn đề này (thường chỉ trong kết nối CSDL đầu tiên), nhưng khi dùng httpc để kết nối tên miền bên ngoài, lỗi này trở nên nghiêm trọng. Do hệ thống không cung cấp cơ chế tra cứu tên miền không đồng bộ, nhiều thư viện mạng khác dùng thread phụ để xử lý. Tuy nhiên để giữ nguyên kiến trúc socket ổn định của Skynet, chúng tôi đã triển khai giải pháp độc lập: Gửi gói UDP theo giao thức DNS ở tầng trên. Đồng thời điều chỉnh httpc để cho phép kết nối trực tiếp bằng địa chỉ IP, đồng thời cho phép người dùng tự điền trường Host trong header HTTP.

Ngoài ra, khi đóng vai trò client HTTP, nhiều server trả về nhiều trường Set-Cookie cùng tên trong header. Phiên bản trước xử lý không chính xác các trường cùng tên này, hiện đã được sửa để gộp chúng thành một bảng (table) dữ liệu.

0%