Xây Dựng Hệ Thống Tệp Ảo Đơn Giản - nói dối e blog

Xây Dựng Hệ Thống Tệp Ảo Đơn Giản

Trong ngành công nghiệp game Windows, việc đóng gói toàn bộ dữ liệu vào các tệp nén trước khi phát hành là một thực tiễn phổ biến. Hành động này xuất phát từ hai mục đích chính: thứ nhất là bảo vệ dữ liệu khỏi bị người dùng cuối truy cập trực tiếp, thứ hai là do hệ thống tệp Windows từng tồn tại vấn đề hiệu suất khi xử lý hàng ngàn tệp nhỏ. Cả cài đặt, phân phối lẫn vận hành đều gặp trở ngại về tốc độ.

Các dự án game thường sở hữu lượng tài nguyên khổng lồ, khiến nhu cầu đóng gói dữ liệu trở nên cấp thiết hơn bao giờ hết. Hầu hết các engine game hiện đại đều tích hợp ít nhất một cơ chế quản lý tài nguyên dạng gói. Tôi lần đầu tiếp cận khái niệm này qua việc nghiên cứu mã nguồn Allegro cách đây hơn một thập kỷ. Sau đó, từ các tựa game huyền thoại như Doom/Quake đến hệ thống xử lý gói dữ liệu của StarCraft, tôi nhận thấy xu hướng chung là cho phép tồn tại song song giữa dữ liệu nén và tệp vật lý.

Việc xây dựng module quản lý gói dữ liệu thường tạo thành chuẩn mực lâu dài trong nội bộ công ty. Dù có nâng cấp nhỏ, kiến trúc gốc vẫn được duy trì. Điều này không chỉ áp dụng cho định dạng gói mà còn với toàn bộ hệ sinh thái công cụ liên quan: từ phần mềm đóng/giải nén, tạo bản vá (patch), đến công cụ phát hành. Nhân tiện nói thêm, định dạng gói tối ưu có thể giảm 90% thời gian cập nhật. Ví dụ, trong hệ thống của NetEase, 95% thời gian cập nhật dùng cho tải dữ liệu; ngược lại, với Blizzard, phần lớn thời gian lại tiêu tốn cho quá trình vá lỗi.

Yếu tố then chốt nằm ở thiết kế giao diện lập trình (API). Khi một kiến trúc ổn định được thiết lập, các thế hệ kỹ sư kế nhiệm thường ngại thay đổi. Điều này khiến những khiếm khuyết từ giai đoạn thiết kế ban đầu có thể tồn tại hàng thập kỷ. Năm 2001, tôi đã xây dựng hệ thống gói dữ liệu đơn giản cho Đại Thoại Tây Du, và đến nay dù trải qua nhiều dự án của NetEase, cấu trúc gốc vẫn không thay đổi.

Năm 2006, khi thiết kế engine 3D mới, tôi cố ý phá vỡ tư duy cũ. Hệ thống mới tích hợp thông tin phụ thuộc tài nguyên, sử dụng ID thay vì tên tệp để định danh. Tuy nhiên qua thực tiễn, tôi nhận ra việc tuân thủ chuẩn mực quen thuộc lại có lợi hơn. Dù thiết kế mới mang lại hiệu suất cao hơn nhờ tối ưu đa luồng và tiền tải tài nguyên, nhưng chi phí đào tạo lại khiến lợi ích bị triệt tiêu. Mỗi khi có kỹ sư mới tham gia, tôi phải dành hàng giờ giải thích nguyên lý hoạt động.

Trừ khi bạn quyết định tự mình xây dựng toàn bộ hệ thống từ A đến Z, việc tuân thủ chuẩn mực vẫn là lựa chọn khôn ngoan. Đây cũng là lý do tôi ngày càng ít hứng thú với C++. Triết lý “All-in-One” của C++ khiến các thành phần khó tích hợp mượt mà như phong cách modul của C. Nếu muốn tạo ra các khối xây dựng linh hoạt, C vẫn là lựa chọn thực tế hơn.

Quay lại chủ đề chính, sau nhiều ngày suy ngẫm và thảo luận với đồng nghiệp, tôi quyết định cải tiến hệ thống quản lý dữ liệu trong engine hiện tại. Thời điểm này rất thích hợp vì các module khác đã ổn định, đồng đội có thể phát triển song song mà không xung đột. Hơn nữa, qua nhiều năm tích lũy kinh nghiệm, tôi đã hiểu rõ yêu cầu thực tế.

Mục tiêu của tôi là xây dựng một hệ thống tệp ảo đơn giản với giao diện quen thuộc: hỗ trợ cấu trúc cây phân cấp, đường dẫn tuyệt đối/tương đối, và liên kết mềm. Hệ thống cần đọc được từ nhiều nguồn khác nhau: tệp vật lý, bộ nhớ đệm, gói dữ liệu ZIP (kể cả cấu trúc ZIP lồng nhau). Ví dụ, một tệp ZIP chứa ZIP khác phải có thể truy cập trực tiếp qua đường dẫn phân cấp.

Ban đầu tôi thiết kế hệ thống dựa trên các loader độc lập: loader từ bộ nhớ, từ hệ thống tệp, từ gói dữ liệu… Mỗi khi mở tệp, hệ thống sẽ lần lượt kiểm tra các loader đã đăng ký. Tuy nhiên sau khi nghiên cứu kỹ, tôi nhận ra cần một cơ chế tương tự VFS (Virtual File System) của Linux, dù đơn giản hơn.

Hệ thống mới sẽ tách biệt quản lý cây thư mục khỏi các hệ thống tệp cụ thể. Mỗi nút trên cây có thể ánh xạ đến nhiều nguồn dữ liệu khác nhau. Cơ chế mount (gắn kết) cho phép gắn các hệ thống tệp vào điểm cụ thể, đồng thời hỗ trợ cache để tăng tốc truy cập. Đặc biệt, tôi thiết kế hệ thống “auto” thông minh: khi tìm kiếm tệp, nó sẽ lần lượt kiểm tra các hệ thống con như /zip, /os, /mem, và tạo liên kết mềm đến vị trí tìm thấy.

Ví dụ, khi truy cập /auto/foo/bar/foobar.txt, hệ thống sẽ lần lượt kiểm tra trong /zip/foo/bar/foobar.txt, nếu không thấy sẽ tiếp tục tìm trong /os/foo/bar/foobar.txt. Cơ chế này đảm bảo tính linh hoạt mà vẫn giữ được hiệu suất. Sau khi hoàn thiện, tôi sẽ chia sẻ chi tiết cấu trúc dữ liệu và API đã thiết kế.

0%