0

[Góc System Design] File .proto: Viết "Hợp Đồng" (Contract) và Thiết lập "Luật Chơi" (Option) trong Microservices

Tiếp nối câu chuyện về tư duy thiết kế hệ thống của RPC/gRPC ở bài trước, hôm nay chúng ta sẽ mổ xẻ "trái tim" của gRPC: File .proto.

Khi làm việc với kiến trúc Microservices, bạn sẽ nghe rất nhiều đến cụm từ "API Contract" (Hợp đồng API). Trong thế giới của REST, chúng ta hay dùng Swagger/OpenAPI để làm hợp đồng. Còn trong thế giới gRPC, file .proto chính là bản hợp đồng tối thượng.

Bài viết này, mình sẽ chia sẻ dưới góc độ thực chiến về 2 khái niệm cốt lõi trong file .proto: Khai báo Contract và Sử dụng Option. Nó không chỉ là cú pháp, mà là cách chúng ta thiết lập "luật chơi" giữa các services.

Hãy tưởng tượng file .proto giống như một bản thiết kế (Blueprint) xây nhà.

  • Contract (Hợp đồng): Quy định nhà có bao nhiêu phòng, cửa ra vào ở đâu, đường ống nước đi thế nào. (Định nghĩa cấu trúc dữ liệu và các hàm).

  • Option (Tùy chọn): Là những chỉ dẫn riêng cho thợ xây: "Phòng này thợ mộc hãy dùng gỗ lim", "Tường này thợ sơn hãy sơn màu xanh". (Chỉ dẫn cho trình biên dịch - compiler khi sinh ra code).

Chúng ta sẽ đi sâu vào từng phần.

Phần 1: Khai báo Contract - Bản hợp đồng không thể chối cãi

Bản chất của file .proto là một ngôn ngữ mô tả giao diện (IDL - Interface Definition Language). Nó hoàn toàn độc lập với ngôn ngữ lập trình (Go, PHP, Node.js, C++...).

Khi team Backend và team Mobile/Client ngồi lại với nhau, họ không cãi nhau bằng code, họ chốt file .proto. Khi file .proto đã được merge, nó là hợp đồng.

Một bản hợp đồng Contract cơ bản bao gồm 2 thành phần chính:

1.1. Khai báo Data Structure (Noun - Danh từ) bằng message

Đây là nơi bạn định nghĩa các thực thể (Payload) sẽ bay qua bay lại trên network.

Protocol Buffers

syntax = "proto3"; // Bắt buộc: Khẳng định chúng ta dùng luật của phiên bản 3

// Contract định nghĩa dữ liệu
message UserProfile {
    int64 id = 1;
    string username = 2;
    string email = 3;
    bool is_active = 4;
}

💡 Kinh nghiệm thực chiến: Các con số 1, 2, 3, 4 ở trên không phải là gán giá trị, mà là Tag Number (số thứ tự định danh). Dữ liệu truyền đi qua mạng không hề chứa chữ "username" hay "email" (như JSON), nó chỉ truyền đi Tag Number ở dạng nhị phân (binary) để ép kiểu. -> Nhờ vậy, gRPC nhẹ và nhanh hơn JSON rất nhiều. Nhưng luật tối thượng là: Tuyệt đối không được thay đổi hoặc tái sử dụng Tag Number cũ, nếu không hợp đồng sẽ bị phá vỡ (vi phạm Backward Compatibility).

1.2. Khai báo Interface (Verb - Động từ) bằng service

Đây là nơi bạn định nghĩa các hành động (hàm) mà Server cung cấp cho Client gọi.

Protocol Buffers

// Contract định nghĩa hành động
service UserService {
    // Kế thừa nguyên tắc "1 Request - 1 Response"
    rpc GetUser (GetUserRequest) returns (UserProfile);
}

message GetUserRequest {
    int64 id = 1;
}

Khi khai báo xong message và service, bạn đã hoàn thành xong bản Hợp Đồng. Dù bạn code backend bằng Go hay C++, Node.js... compiler sẽ nhìn vào đây để sinh ra đúng các Class/Struct và Interface tương ứng.

Phần 2: Khai báo Option - Sức mạnh ngầm định

Nếu Contract là phần nổi của tảng băng chìm, thì Option chính là phần chìm giúp file .proto trở nên vô cùng linh hoạt.

Option không làm thay đổi logic nghiệp vụ của hợp đồng, nó đóng vai trò là Metadata (Dữ liệu siêu dữ liệu) để chỉ dẫn cho Protobuf Compiler (protoc) hoặc các Plugin bên thứ 3 biết phải làm gì khi sinh ra code.

Có 3 cấp độ dùng Option thực chiến nhất:

2.1. File-level Options (Tùy chọn cấp File)

Thường đặt ở đầu file. Định hướng cách code được sinh ra cho từng ngôn ngữ.

Nếu bạn làm việc với Golang, bạn bắt buộc phải quen với option này:

Protocol Buffers

option go_package = "github.com/my-company/my-project/pb/user_v1";

Ý nghĩa: Nó báo cho trình biên dịch biết: "Khi mày sinh ra file code Golang, hãy đặt tên package và đường dẫn module y hệt như thế này cho tao".

Hoặc nếu team có làm Java:

Protocol Buffers

option java_multiple_files = true;
option java_package = "com.mycompany.project.user";

Ý nghĩa: Báo cho compiler Java tách mỗi message ra một file .java riêng biệt để chuẩn OOP, thay vì nhét tất cả vào 1 file khổng lồ.

2.2. Field-level Options (Tùy chọn cấp Trường dữ liệu)

Được gắn trực tiếp đằng sau các thuộc tính trong message.

Ví dụ 1: Báo hiệu "Hết hạn sử dụng" (Deprecated) Một ngày đẹp trời, công ty không dùng trường username nữa mà chuyển sang đăng nhập bằng email. Bạn không được phép xóa field username vì sẽ phá vỡ hợp đồng với các app cũ chưa update. Cách làm chuẩn System Design là:

Protocol Buffers

message UserProfile {
    int64 id = 1;
    string username = 2 [deprecated = true]; // Cảnh báo dev gạch ngang field này
    string email = 3;
}

Khi sinh code, các IDE sẽ hiện dấu gạch ngang (strikethrough) cảnh báo dev không nên xài field này nữa.

Ví dụ 2: Thay đổi tên khi parse sang JSON Mặc định Protobuf dùng snake_case, nhưng frontend đôi khi lại thích camelCase.

Protocol Buffers

message UserProfile {
    bool is_active = 4 [json_name = "isActive"];
}

2.3. Custom Options (Tuyệt chiêu cho API Gateway)

Đây là level nâng cao nhất nhưng cũng "đã" nhất. Bạn có thể tự định nghĩa Option hoặc dùng Option của bên thứ 3.

Ví dụ kinh điển nhất là sử dụng google.api.http để làm gRPC-Gateway (Map từ RESTful HTTP sang gRPC). Thay vì phải code 2 lần (1 cái API REST cho web, 1 cái gRPC cho nội bộ microservices), bạn chỉ cần viết .proto và thêm Option:

Protocol Buffers

import "google/api/annotations.proto"; // Import thư viện option của Google

service UserService {
    rpc GetUser (GetUserRequest) returns (UserProfile) {
        // Khai báo Option: Map hàm RPC này thành API GET HTTP/REST
        option (google.api.http) = {
            get: "/v1/users/{id}"
        };
    }
}

Chỉ với vài dòng Option, gRPC-Gateway sẽ tự động sinh ra một con Reverse Proxy. Khi Client gọi REST API GET /v1/users/123, Proxy sẽ tự convert nó thành lời gọi gRPC GetUser xuống Backend. Viết code 1 lần, được luôn 2 chuẩn giao tiếp!

Tóm lại

Nhìn vào một file .proto thực chiến trong một dự án đủ lớn, bạn sẽ thấy nó là sự kết hợp hài hòa giữa sự chặt chẽ và sự linh hoạt:

  1. Contract (message, service): Là sự cứng nhắc cần thiết, ép mọi ngôn ngữ, mọi team phải tuân thủ chung một cấu trúc dữ liệu, đảm bảo hệ thống không bao giờ bị lệch pha.

  2. Option (option ...): Là sự uyển chuyển, giúp file .proto tương thích hoàn hảo với đặc thù của từng ngôn ngữ (Go, Java) hoặc mở rộng thêm tính năng (REST Gateway, Validation) mà không làm rác bản hợp đồng chính.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí