Tìm hiểu khung gọi thủ tục từ xa brpc của Baidu

Tổng quan về brpc

brpc là một khung gọi thủ tục từ xa (RPC) hiệu suất cao, đa nền tảng do Baidu phát triển và mã nguồn mở. Mục tiêu chính của nó là đơn giản hóa giao tiếp giữa các dịch vụ trong hệ thống phân tán. Khung này cung cấp một cách đơn giản và hiệu quả để xây dựng và quản lý các lời gọi dịch vụ trong kiến trúc vi dịch vụ. brpc hỗ trợ cả hai kiểu gọi RPC đồng bộ và bất đồng bộ.

Đặc điểm nổi bật

  • Hiệu suất cao: brpc sử dụng nhiều kỹ thuật tối ưu như sao chép không (zero-copy) và phương thức tuần tự hóa hiệu quả, cho phép xử lý các yêu cầu đồng thời cao. Nó hỗ trợ cơ chế đa luồng và bể luồng, phân phối tài nguyên tính toán hiệu quả trên các máy đa nhân.
  • Hỗ trợ bất đồng bộ: Cho phép client gửi yêu cầu mà không bị chặn và nhận callback khi xử lý hoàn tất, cải thiện đáng kể hiệu suất đồng thời.
  • Đa nền tảng: Hỗ trợ nhiều hệ điều hành như Linux, macOS, Windows và nhiều ngôn ngữ lập trình (C++ và Python).
  • Tính sẵn sàng cao và cân bằng tải: Cung cấp các tính năng như bể kết nối, tự động thử lại, kiểm soát thời gian chờ để đảm bảo tính sẵn sàng cao.
  • Khám phá và quản lý dịch vụ: Tích hợp với các hệ thống khám phá dịch vụ như ZooKeeper, Consul, hỗ trợ tự động đăng ký và khám phá dịch vụ. Quản lý dịch vụ thông qua lớp brpc::Server.
  • Định nghĩa giao diện và giao thức: Hỗ trợ sử dụng Protobuf để định nghĩa giao diện dịch vụ và mô hình dữ liệu, giúp định nghĩa giao diện rõ ràng và độc lập với ngôn ngữ lập trình.

Luồng hoạt động cơ bản

Phía Server

  • Định nghĩa giao diện dịch vụ (ví dụ: sử dụng Protobuf).
  • Triển khai logic cụ thể của server (bằng cách kế thừa lớp giao diện dịch vụ được tạo ra và triển khai phương thức).
  • Khởi động dịch vụ brpc và lắng nghe các yêu cầu.

Phía Client

  • Tạo đối tượng brpc::Channel và khởi tạo kết nối.
  • Sử dụng giao diện client được tạo bởi example::EchoService_Stub để thực hiện lời gọi RPC.
  • Xử lý yêu cầu và phản hồi đồng bộ hoặc bất đồng bộ.

Hướng dẫn sử dụng cơ bản

Tìm hiểu các lớp giao diện

Các lớp giao diện dưới đây được tìm hiểu dựa trên mã nguồn client và server.

  • brpc::Channel: Quản lý giao tiếp với server, chịu trách nhiệm kết nối và trao đổi dữ liệu.
  • brpc::Controller: Quản lý trạng thái lời gọi RPC, ghi lại thông tin lỗi và kiểm soát luồng gọi.
  • example::EchoService_Stub: Giao diện client giao tiếp với server, chứa phương thức yêu cầu RPC.
  • example::EchoRequestexample::EchoResponse: Định nghĩa định dạng tin nhắn yêu cầu và phản hồi.
  • google::protobuf::ClosureNewCallback: Được sử dụng để xử lý callback trong lời gọi bất đồng bộ.
  • brpc::Server: Lớp quan trọng nhất trong khung brpc, quản lý việc đăng ký dịch vụ, khởi động và lắng nghe yêu cầu. Sử dụng các phương thức như AddServiceStart để đăng ký dịch vụ và khởi động server.
  • brpc::ServiceOwnership: Kiểm soát vòng đời của đối tượng dịch vụ, lựa chọn để server brpc hoặc nhà phát triển quản lý.
  • brpc::ClosureGuard: Một công cụ hỗ trợ, đảm bảo tự động gọi hàm callback khi kết thúc lời gọi RPC bất đồng bộ.
  • brpc::ServerOptions: Cung cấp các tham số cấu hình cho server brpc, như thời gian chờ rảnh, số luồng, v.v.
Lớp brpc::Channel

Lớp này chịu trách nhiệm chính cho việc giao tiếp giữa client và server, quản lý kết nối TCP, gửi yêu cầu RPC và nhận phản hồi. Nó đại diện cho kênh kết nối giữa client và server.

  • Init(const std::string& server, const ChannelOptions* options): Khởi tạo kênh và kết nối đến địa chỉ server được chỉ định.
  • CallMethod: Gửi yêu cầu RPC, thường được sử dụng cùng với Controller.
brpc::Channel channel;
brpc::ChannelOptions options;
options.connect_timeout_ms = 1000;  // Đặt thời gian chờ kết nối là 1 giây
options.timeout_ms = 5000;          // Đặt thời gian chờ yêu cầu là 5 giây
options.max_retry = 3;              // Đặt số lần thử lại tối đa là 3
options.protocol = "baidu_std";     // Đặt giao thức giao tiếp là baidu_std

int ret = channel.Init("127.0.0.1:8080", &options);  // Khởi tạo kênh và kết nối đến server
if (ret != 0) {
    std::cout << "Khởi tạo kênh thất bại!" << std::endl;
}
Lớp brpc::Controller

Lớp này kiểm soát và quản lý trạng thái lời gọi RPC, bao gồm việc gửi yêu cầu, nhận phản hồi và xử lý thông tin lỗi. Mỗi yêu cầu RPC cần một đối tượng Controller, thường được tạo trong mỗi lần yêu cầu và truyền cho stub để gọi.

  • Failed(): Trả về true nếu có lỗi xảy ra trong lời gọi RPC.
  • ErrorText(): Lấy thông tin lỗi (nếu Failed() trả về true).
  • Reset(): Đặt lại trạng thái của Controller, thích hợp khi tái sử dụng cùng một đối tượng.
brpc::Controller cntl;
example::EchoResponse response;

cntl.Reset();  // Đặt lại trạng thái nếu muốn tái sử dụng
stub.Echo(&cntl, &req, &response, nullptr);

if (cntl.Failed()) {
    std::cout << "Lời gọi RPC thất bại: " << cntl.ErrorText() << std::endl;
} else {
    std::cout << "Nhận được phản hồi: " << response.message() << std::endl;
}
Lớp example::EchoService_Stub

Lớp stub client được tự động tạo bởi Protocol Buffers. Nó cung cấp giao diện để giao tiếp với dịch vụ EchoService từ xa. Phương thức Echo được định nghĩa để gửi yêu cầu đến server và nhận phản hồi.

  • Echo(Controller* cntl, const EchoRequest* request, EchoResponse* response, google::protobuf::Closure* done): Thực hiện yêu cầu RPC và nhận phản hồi, hỗ trợ cả đồng bộ và bất đồng bộ. Tham số done được sử dụng để truyền hàm callback.
// Gọi đồng bộ
example::EchoService_Stub stub(&channel);
example::EchoRequest req;
req.set_message("Xin chào, Echo Server");

example::EchoResponse rsp;
brpc::Controller cntl;
stub.Echo(&cntl, &req, &rsp, nullptr);

if (cntl.Failed()) {
    std::cerr << "Lời gọi RPC thất bại: " << cntl.ErrorText() << std::endl;
} else {
    std::cout << "Nhận được phản hồi: " << rsp.message() << std::endl;
}
Lớp example::EchoRequest

Lớp tin nhắn được tự động tạo bởi Protocol Buffers, dùng để định nghĩa cấu trúc yêu cầu RPC. Trong ví dụ này, nó chứa trường message để truyền tin nhắn yêu cầu.

  • set_message(const std::string& msg): Đặt nội dung tin nhắn yêu cầu.
  • message(): Lấy nội dung tin nhắn yêu cầu.
example::EchoRequest req;
req.set_message("Xin chào, dịch vụ RPC!");  // Đặt nội dung tin nhắn yêu cầu
Lớp example::EchoResponse

Lớp tin nhắn được tự động tạo bởi Protocol Buffers, dùng để định nghĩa cấu trúc phản hồi RPC.

  • set_message(const std::string& msg): Đặt nội dung tin nhắn phản hồi.
  • message(): Lấy nội dung tin nhắn phản hồi.
example::EchoResponse rsp;
rsp.set_message("Xin chào, client!");  // Đặt nội dung tin nhắn phản hồi
google::protobuf::Closure và NewCallback

google::protobuf::Closure là lớp của Protocol Buffers để đóng gói hàm callback. NewCallback là một hàm tĩnh dùng để tạo đối tượng callback, liên kết hàm callback với các tham số của nó.

  • google::protobuf::NewCallback(callback, args...): Tạo một đối tượng callback, trong đó callback là hàm callback và args... là các tham số truyền cho callback.
auto closure = google::protobuf::NewCallback(callback, cntl, rsp);
stub.Echo(&cntl, &req, &rsp, closure);  // Truyền hàm callback closure để thực hiện gọi bất đồng bộ
brpc::Server

Lớp cốt lõi trong khung brpc, dùng để quản lý và khởi động server. Nó chịu trách nhiệm đăng ký dịch vụ, khởi động server, lắng nghe yêu cầu từ client và xử lý các yêu cầu RPC.

  • AddService(Service* service, ServiceOwnership ownership): Thêm dịch vụ vào server. Tham số service là đối tượng dịch vụ (thường kế thừa từ brpc::Service), ownership chỉ định brpc có quản lý vòng đời của đối tượng dịch vụ hay không.
  • Start(int port, const ServerOptions* options = nullptr): Khởi động server và lắng nghe trên cổng được chỉ định.
  • RunUntilAskedToQuit(): Không có tham số. Đi vào vòng lặp sự kiện chính của server cho đến khi nhận được yêu cầu thoát.
brpc::Server server;
EchoServiceImpl echo_service;

// Thêm dịch vụ vào server, brpc không quản lý vòng đời của đối tượng
int ret = server.AddService(&echo_service, brpc::ServiceOwnership::SERVER_DOESNT_OWN_SERVICE);
if (ret == -1) {
    std::cout << "Thêm dịch vụ RPC thất bại!\n";
    return -1;
}

brpc::ServerOptions options;
options.idle_timeout_sec = -1;  // Không có thời gian chờ rảnh
options.num_threads = 1;        // 1 luồng IO để xử lý yêu cầu
ret = server.Start(8080, &options);  // Khởi động server trên cổng 8080
if (ret == -1) {
    std::cout << "Khởi động server thất bại!\n";
    return -1;
}

server.RunUntilAskedToQuit();  // Chạy server cho đến khi nhận được yêu cầu thoát
brpc::ServiceOwnership

Một kiểu liệt kê, chỉ định server có quản lý vòng đời của đối tượng dịch vụ hay không.

  • SERVER_OWNS_SERVICE: Server brpc chịu trách nhiệm quản lý vòng đời của đối tượng dịch vụ. Khi server đóng, đối tượng dịch vụ sẽ tự động bị hủy.
  • SERVER_DOESNT_OWN_SERVICE: Vòng đời của đối tượng dịch vụ do người dùng kiểm soát, server không quản lý.
brpc::ClosureGuard

Một lớp RAII, đảm bảo hàm callback (truyền qua đối tượng Closure) sẽ tự động được thực thi khi kết thúc phạm vi. Nó đảm bảo done->Run() được gọi vào cuối phương thức, thường được dùng để xử lý callback RPC bất đồng bộ.

void Echo(google::protobuf::RpcController* controller,
          const ::example::EchoRequest* request,
          ::example::EchoResponse* response,
          ::google::protobuf::Closure* done) {
    brpc::ClosureGuard rpc_guard(done);  // Tự động gọi done->Run()

    // Xử lý yêu cầu và đặt phản hồi
    response->set_message(request->message() + "-- Đây là phản hồi!!");
}
brpc::ServerOptions

Cấu hình các tham số cho server brpc, như thời gian chờ rảnh, số luồng.

class ServerOptions {
public:
    int idle_timeout_sec;  // Thời gian chờ rảnh kết nối (giây)
    int num_threads;       // Số luồng xử lý yêu cầu
};

Viết Protobuf, Server và Client

Định nghĩa Protobuf
syntax = "proto3";

package example;

// Cho phép Protobuf tự động tạo triển khai C++ chung cho dịch vụ RPC
option cc_generic_services = true;

message EchoRequest {
    string message = 1;
}

message EchoResponse {
    string message = 1;
}

// Định nghĩa dịch vụ Echo, với phương thức Echo nhận EchoRequest và trả về EchoResponse
service EchoService {
    rpc Echo(EchoRequest) returns (EchoResponse);
}
Server

Tóm tắt logic triển khai:

  • Định nghĩa và triển khai dịch vụ: Kế thừa giao diện EchoService do Protobuf tạo ra và triển khai phương thức Echo, cung cấp logic nghiệp vụ cụ thể. Phương thức Echo nhận tin nhắn từ client, thêm hậu tố "-- Đây là phản hồi!!" và trả về.
  • Khởi động server brpc:
    • Tạo và cấu hình brpc::Server.
    • Thêm dịch vụ EchoServiceImpl vào server.
    • Khởi động server và lắng nghe trên cổng 8080.
#include <brpc/server.h>
#include <butil/logging.h>
#include "main.pb.h"

// 1. Kế thừa EchoService tạo lớp con và triển khai chức năng nghiệp vụ RPC
class EchoServiceImpl : public example::EchoService {
public:
    EchoServiceImpl() {}
    ~EchoServiceImpl() {}

    // Triển khai phương thức Echo, xử lý yêu cầu từ client
    void Echo(google::protobuf::RpcController* controller,
              const ::example::EchoRequest* request,
              ::example::EchoResponse* response,
              ::google::protobuf::Closure* done) {
        // brpc::ClosureGuard đảm bảo done->Run() được gọi sau khi phản hồi
        brpc::ClosureGuard rpc_guard(done);

        // In tin nhắn yêu cầu nhận được
        std::cout << "Nhận được tin nhắn: " << request->message() << std::endl;

        // Xử lý phản hồi: thêm hậu tố vào tin nhắn yêu cầu
        std::string str = request->message() + "-- Đây là phản hồi!!";
        response->set_message(str);  // Đặt tin nhắn phản hồi

        // Lưu ý: done->Run() được brpc::ClosureGuard tự động gọi, không cần gọi thủ công
    }
};

int main(int argc, char *argv[]) {
    // 2. Tắt đầu ra log mặc định của brpc
    logging::LoggingSettings settings;
    settings.logging_dest = logging::LoggingDestination::LOG_TO_NONE;
    logging::InitLogging(settings);

    // 3. Tạo đối tượng server
    brpc::Server server;

    // 4. Thêm dịch vụ EchoService vào server
    EchoServiceImpl echo_service;
    int ret = server.AddService(&echo_service, brpc::ServiceOwnership::SERVER_DOESNT_OWN_SERVICE);
    if (ret == -1) {
        std::cout << "Thêm dịch vụ RPC thất bại!\n";
        return -1;
    }

    // 5. Khởi động server
    brpc::ServerOptions options;
    options.idle_timeout_sec = -1;  // Không có thời gian chờ rảnh kết nối
    options.num_threads = 1;        // 1 luồng IO
    ret = server.Start(8080, &options);  // Khởi động server trên cổng 8080
    if (ret == -1) {
        std::cout << "Khởi động server thất bại!\n";
        return -1;
    }

    // 6. Vào vòng lặp chính, chờ yêu cầu từ client
    server.RunUntilAskedToQuit();

    return 0;
}
Client

Tóm tắt logic:

  • Sử dụng EchoService_Stub để gửi yêu cầu đến server và xử lý phản hồi thông qua hàm callback.
  • Triển khai cả hai phương thức gọi đồng bộ và bất đồng bộ.
#include <brpc/channel.h>
#include <thread>
#include "main.pb.h"

// Hàm callback cho lời gọi RPC bất đồng bộ, xử lý phản hồi từ server
void callback(brpc::Controller* cntl, ::example::EchoResponse* response) {
    std::unique_ptr<brpc::Controller> cntl_guard(cntl);
    std::unique_ptr<example::EchoResponse> resp_guard(response);

    if (cntl->Failed()) {
        std::cout << "Lời gọi RPC thất bại: " << cntl->ErrorText() << std::endl;
        return;
    }

    std::cout << "Nhận được phản hồi: " << response->message() << std::endl;
}

int main(int argc, char *argv[]) {
    // 1. Tạo và khởi tạo kênh giao tiếp RPC (Channel)
    brpc::ChannelOptions options;
    options.connect_timeout_ms = -1;  // Chờ kết nối vô thời hạn
    options.timeout_ms = -1;          // Chờ phản hồi vô thời hạn
    options.max_retry = 3;            // Thử lại tối đa 3 lần
    options.protocol = "baidu_std";   // Giao thức baidu_std

    brpc::Channel channel;
    int ret = channel.Init("127.0.0.1:8080", &options);
    if (ret == -1) {
        std::cout << "Khởi tạo kênh thất bại!\n";
        return -1;
    }

    // 2. Tạo đối tượng stub để gọi dịch vụ Echo từ xa
    example::EchoService_Stub stub(&channel);

    // 3. Tạo yêu cầu và gửi lời gọi RPC
    example::EchoRequest req;
    req.set_message("Xin chào~RPC~!");

    brpc::Controller *cntl = new brpc::Controller();
    example::EchoResponse *rsp = new example::EchoResponse();

    // Gọi đồng bộ
    stub.Echo(cntl, &req, rsp, nullptr);
    if (cntl->Failed()) {
        std::cout << "Lời gọi RPC thất bại: " << cntl->ErrorText() << std::endl;
        delete cntl;
        delete rsp;
        return -1;
    }
    std::cout << "Nhận được phản hồi: " << rsp->message() << std::endl;
    delete cntl;
    delete rsp;

    // Gọi bất đồng bộ (đã được comment trong mã gốc)
    // auto closure = google::protobuf::NewCallback(callback, cntl, rsp);
    // stub.Echo(cntl, &req, rsp, closure);
    // std::this_thread::sleep_for(std::chrono::seconds(3));

    return 0;
}

Vấn đề với thư viện tĩnh và động

Mô tả sự cố

Sau khi thực hiện quy trình cài đặt, thư viện động không được tìm thấy trong đường dẫn dự kiến.

Giải pháp

Thư viện động đã được tạo trong thư mục xây dựng, vấn đề nằm ở bước make install. Đường dẫn cài đặt thư viện động không được định nghĩa đúng hoặc tệp CMakeLists.txt có vấn đề. Giải pháp là sao chép thư viện động từ thư mục xây dựng vào đường dẫn hệ thống.

# Sao chép thư viện động vào /usr/lib
sudo cp output/lib/libbrpc.a /usr/lib
sudo cp output/lib/libbrpc.so /usr/lib

# Làm mới bộ nhớ cache của trình liên kết động
sudo ldconfig

# Xác minh tệp thư viện
ls /usr/lib | grep libbrpc
Xác minh
#include <brpc/server.h>
#include <iostream>

int main() {
    brpc::Server server;
    std::cout << "BRPC kiểm tra thành công!" << std::endl;
    return 0;
}

Tổng kết và suy ngẫm

Nhìn lại thư mục cài đặt:

-- Installing: /usr/lib/x86_64-linux-gnu/libbrpc.so
-- Installing: /usr/lib/x86_64-linux-gnu/libbrpc.a

Theo nhật ký cài đặt, thư viện động đã được tạo. Trên các bản phân phối Linux như Ubuntu hoặc Debian, CMake sử dụng đường dẫn đa kiến trúc (ví dụ: /usr/lib/x86_64-linux-gnu) thay vì /usr/lib. Đây là hành vi mặc định của hệ thống để hỗ trợ nhiều kiến trúc. Tệp CMakeLists.txt của BRPC có thể đã chỉ định đường dẫn này hoặc CMake đã chọn nó dựa trên cấu hình mặc định của hệ thống.

Người dùng đã kiểm tra /usr/lib theo thói quen và bỏ qua đường dẫn cài đặt thực tế.

Giải pháp 1: Thêm thư viện động vào đường dẫn hệ thống và làm mới bộ nhớ cache.

echo "/usr/lib/x86_64-linux-gnu" | sudo tee -a /etc/ld.so.conf.d/brpc.conf
sudo ldconfig

ldconfig -p | grep libbrpc

Giải pháp 2: Chỉ định đường dẫn rõ ràng khi cấu hình CMake. Vấn đề có thể là do hệ thống tự động chọn đường dẫn đa kiến trúc, không phải đường dẫn mong muốn.

Nguyên nhân gốc rễ là môi trường hệ điều hành sử dụng đường dẫn đa kiến trúc, dẫn đến thư viện động được cài đặt vào /usr/lib/x86_64-linux-gnu.

Vấn đề xung đột nhiều phiên bản Protobuf

Mô tả sự cố

Xảy ra xung đột khi liên kết thư viện Protobuf.

Giải pháp

# Gỡ cài đặt phiên bản Protobuf xung đột
sudo apt remove protobuf-compiler libprotobuf-dev

# Cập nhật bộ nhớ cache thư viện động
sudo ldconfig

Tổng kết và suy ngẫm

Vấn đề phát sinh do hệ thống có nhiều phiên bản libprotobuf cùng tồn tại. Phiên bản được sử dụng khi biên dịch không khớp với phiên bản mà dự án phụ thuộc, dẫn đến trình liên kết không thể phân giải chính xác các ký hiệu.

Để tránh vấn đề này:

  • Sử dụng CMake để chỉ định rõ ràng phiên bản cần dùng.
  • Gỡ bỏ phiên bản mặc định và cài đặt phiên bản phù hợp với yêu cầu của dự án.

Thẻ: brpc RPC protobuf client-server C++

Đăng vào ngày 4 tháng 6 lúc 21:55