TiKV 源碼解析系列文章(七)gRPC Server 的初始化和啓動流程

做者:屈鵬git

本篇 TiKV 源碼解析將爲你們介紹 TiKV 的另外一週邊組件—— grpc-rs。grpc-rs 是 PingCAP 實現的一個 gRPC 的 Rust 綁定,其 Server/Client 端的代碼框架都基於 Future,事件驅動的 EventLoop 被隱藏在了庫的內部,因此很是易於使用。本文將以一個簡單的 gRPC 服務做爲例子,展現 grpc-rs 會生成的服務端代碼框架和須要服務的實現者填寫的內容,而後會深刻介紹服務器在啓動時如何將後臺的事件循環與這個框架掛鉤,並在後臺線程中運行實現者的代碼。github

基本的代碼生成及服務端 API

gRPC 使用 protobuf 定義一個服務,以後調用相關的代碼生成工具就能夠生成服務端、客戶端的代碼框架了,這個過程能夠參考咱們的 官方文檔。客戶端能夠直接調用這些生成的代碼,向服務端發送請求並接收響應,而服務端則須要服務的實現者本身來定製對請求的處理邏輯,生成響應併發回給客戶端。舉一個例子:編程

#[derive(Clone)]
struct MyHelloService {}
impl Hello for MyHelloService {
    // trait 中的函數簽名由 grpc-rs 生成,內部實現須要用戶本身填寫
    fn hello(&mut self, ctx: RpcContext, req: HelloRequest, sink: UnarySink<HelloResponse>) {
        let mut resp = HelloResponse::new();
        resp.set_to(req.get_from());
        ctx.spawn(
            sink.success(resp)
                .map(|_| println!("send hello response back success"))
                .map_err(|e| println!("send hello response back fail: {}", e))
        );
    }
}

咱們定義了一個名爲 Hello 的服務,裏面只有一個名爲 hello 的 RPC。grpc-rs 會爲服務生成一個 trait,裏面的方法就是這個服務包含的全部 RPC。在這個例子中惟一的 RPC 中,咱們從 HelloRequest 中拿到客戶端的名字,而後再將這個名字放到 HelloResponse 中發回去,很是簡單,只是展現一下函數簽名中各個參數的用法。服務器

而後,咱們須要考慮的是如何把這個服務運行起來,監聽一個端口,真正可以響應客戶端的請求呢?下面的代碼片斷展現瞭如何運行這個服務:網絡

fn main() {
    // 建立一個 Environment,裏面包含一個 Completion Queue
    let env = Arc::new(EnvBuilder::new().cq_count(4).build());
    let channel_args = ChannelBuilder::new(env.clone()).build_args();
    let my_service = MyHelloWorldService::new();
    let mut server = ServerBuilder::new(env.clone())
        // 使用 MyHelloWorldService 做爲服務端的實現,註冊到 gRPC server 中
        .register_service(create_hello(my_service))
        .bind("0.0.0.0", 44444)
        .channel_args(channel_args)
        .build()
        .unwrap();
    server.start();
    thread::park();
}

以上代碼展現了 grpc-rs 的足夠簡潔的 API 接口,各行代碼的意義如其註釋所示。併發

Server 的建立和啓動

下面咱們來看一下這個 gRPC server 是如何接收客戶端的請求,並路由到咱們實現的服務端代碼中進行後續的處理的。框架

第一步咱們初始化一個 Environment,並設置 Completion Queue(完成隊列)的個數爲 4 個。完成隊列是 gRPC 的一個核心概念,grpc-rs 爲每個完成隊列建立一個線程,並在線程中運行一個事件循環,相似於 Linux 網絡編程中不斷地調用 epoll_wait 來獲取事件,進行處理:函數

// event loop
fn poll_queue(cq: Arc<CompletionQueueHandle>) {
    let id = thread::current().id();
    let cq = CompletionQueue::new(cq, id);
    loop {
        let e = cq.next();
        match e.event_type {
            EventType::QueueShutdown => break,
            EventType::QueueTimeout => continue,
            EventType::OpComplete => {}
        }
        let tag: Box<CallTag> = unsafe { Box::from_raw(e.tag as _) };
        tag.resolve(&cq, e.success != 0);
    }
}

事件被封裝在 Tag 中。咱們暫時忽略對事件的具體處理邏輯,目前咱們只須要知道,當這個 Environment 被建立好以後,這些後臺線程便開始運行了。那麼剩下的任務就是監聽一個端口,將網絡上的事件路由到這幾個事件循環中。這個過程在 Server 的 start 方法中:工具

/// Start the server.
pub fn start(&mut self) {
    unsafe {
        grpc_sys::grpc_server_start(self.core.server);
        for cq in self.env.completion_queues() {
            let registry = self
                .handlers
                .iter()
                .map(|(k, v)| (k.to_owned(), v.box_clone()))
                .collect();
            let rc = RequestCallContext {
                server: self.core.clone(),
                registry: Arc::new(UnsafeCell::new(registry)),
            };
            for _ in 0..self.core.slots_per_cq {
                request_call(rc.clone(), cq);
            }
        }
    }
}

首先調用 grpc_server_start 來啓動這個 Server,而後對每個完成隊列,複製一份 handler 字典。這個字典的 key 是一個字符串,而 value 是一個函數指針,指向對這個類型的請求的處理函數——其實就是前面所述的服務的具體實現邏輯。key 的構造方式其實就是 /<ServiceName>/<RpcName>,實際上就是 HTTP/2 中頭部字段中的 path 的值。咱們知道 gRPC 是基於 HTTP/2 的,關於 gRPC 的請求、響應是如何裝進 HTTP/2 的幀中的,更多的細節能夠參考 官方文檔,這裏就不贅述了。oop

接着咱們建立一個 RequestCallContext,而後對每一個完成隊列調用幾回 request_call。這個函數會往完成隊列中註冊若干個 Call,至關於用 epoll_ctl 往一個 epoll fd 中註冊一些事件的關注。Call 是 gRPC 在進行遠程過程調用時的基本單元,每個 RPC 在創建的時候都會從完成隊列裏取出一個 Call 對象,後者會在這個 RPC 結束時被回收。所以,在 start 函數中每個完成隊列上註冊的 Call 個數決定了這個完成隊列上能夠併發地處理多少個 RPC,在 grpc-rs 中默認的值是 1024 個。

小結

以上代碼基本都在 grpc-rs 倉庫中的 src/server.rs 文件中。在 start 函數返回以後,服務端的初始化及啓動過程便結束了。如今,能夠快速地用幾句話回顧一下:首先建立一個 Environment,內部會爲每個完成隊列啓動一個線程;接着建立 Server 對象,綁定端口,並將一個或多個服務註冊到這個 Server 上;最後調用 Server 的 start 方法,將服務的具體實現關聯到若干個 Call 上,並塞進全部的完成隊列中。在這以後,網絡上新來的 RPC 請求即可以在後臺的事件循環中被取出,並根據具體實現的字典分別執行了。最後,不要忘記 start 是一個非阻塞的方法,調用它的主線程在以後能夠繼續執行別的邏輯或者掛起。

本篇源碼解析就到這裏,下篇關於 grpc-rs 的文章咱們會進一步介紹一個 Call 或者 RPC 的生命週期,以及每一階段在 Server 端的完成隊列中對應哪種事件、會被如何處理,這一部分是 grpc-rs 的核心代碼,敬請期待!

原文連接:https://www.pingcap.com/blog-cn/tikv-source-code-reading-7/

相關文章
相關標籤/搜索