TiKV 原始碼解析系列文章(七)gRPC Server 的初始化和啟動流程
作者:屈鵬
本篇 TiKV 原始碼解析將為大家介紹 TiKV 的另一週邊元件—— grpc-rs。grpc-rs 是 PingCAP 實現的一個 gRPC 的 Rust 繫結,其 Server/Client 端的程式碼框架都基於 Future,事件驅動的 EventLoop 被隱藏在了庫的內部,所以非常易於使用。本文將以一個簡單的 gRPC 服務作為例子,展示 grpc-rs 會生成的服務端程式碼框架和需要服務的實現者填寫的內容,然後會深入介紹伺服器在啟動時如何將後臺的事件迴圈與這個框架掛鉤,並在後臺執行緒中執行實現者的程式碼。
基本的程式碼生成及服務端 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 的幀中的,更多的細節可以參考 官方文件,這裡就不贅述了。
接著我們建立一個 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/
相關推薦
TiKV 原始碼解析系列文章(七)gRPC Server 的初始化和啟動流程
作者:屈鵬 本篇 TiKV 原始碼解析將為大家介紹 TiKV 的另一週邊元件—— grpc-rs。grpc-rs 是 PingCA
TiKV 原始碼解析系列文章(三)Prometheus(上)
開發十年,就只剩下這套架構體系了! >>>
TiKV 原始碼解析系列文章(十一)Storage
作者:張金鵬 背景知識 TiKV 是一個強一致的支援事務的分散式 KV 儲存。TiKV 通過 raft 來保證多副本之間的強一致,
DM 原始碼閱讀系列文章(七)定製化資料同步功能的實現
作者:王相 本文為 DM 原始碼閱讀系列文章的第七篇,在 上篇文章 中我們介紹了 relay log 的實現,主要包括 relay
TiDB Binlog 原始碼閱讀系列文章(四)Pump server 介紹
作者: satoru 在 上篇文章 中,我們介紹了 TiDB 如何通過 Pump client 將 binlog 發往 Pump,
TiDB 原始碼閱讀系列文章(二)初識 TiDB 原始碼
本文為 TiDB 原始碼閱讀系列文章的第二篇,第一篇文章介紹了 TiDB 整體的架構,知道 TiDB 有哪些模組,分別是做什麼的,從哪裡入手比較好,哪些可以忽略,哪些需要仔細閱讀。 這篇文章是一篇入門文件,難度係數比較低,其中部分內容可能大家在其他渠道已經看過
DM 原始碼閱讀系列文章(八)Online Schema Change 同步支援
作者:lan 本文為 DM 原始碼閱讀系列文章的第八篇,上篇文章 對 DM 中的定製化資料同步功能進行詳細的講解,包括庫表路由(T
DM 原始碼閱讀系列文章(九)shard DDL 與 checkpoint 機制的實現
作者:張學程 本文為 DM 原始碼閱讀系列文章的第九篇,在 上篇文章 中我們詳細介紹了 DM 對 online schema ch
DM 原始碼閱讀系列文章(十)測試框架的實現
作者:楊非 本文為 DM 原始碼閱讀系列文章的第十篇,之前的文章已經詳細介紹過 DM 資料同步各元件的實現原理和程式碼解析,相信大
vue原始碼(七)Vue 的初始化之開篇
本文是學習vue原始碼,之所以轉載過來是方便自己隨時檢視,在這裡要感謝HcySunYang大神,提供的開源vue原始碼解析,寫的非常非常好,簡單易懂,比自己看要容易多了,他的文章連結地址是http://hcysun.me/vue-design/art/ 用於初始化的最終選項 $options
TiDB 原始碼閱讀系列文章(十九)tikv-client(下)
上篇文章 中,我們介紹了資料讀寫過程中 tikv-client 需要解決的幾個具體問題,本文將繼續介紹 tikv-client 裡的兩個主要的模組——負責處理分散式計算的 copIterator 和執行二階段提交的 twoPhaseCommitter。 copIterator cop
TiDB 原始碼閱讀系列文章(二十)Table Partition
作者:肖亮亮 Table Partition 什麼是 Table Partition Table Partition 是指根據一定規則,將資料庫中的一張表分解成多個更小的容易管理的部分。從邏輯上看只有一張表,但是底層卻是由多個物理分割槽組成。相信對有關係型資料庫使用背景的使用者來
TiDB 原始碼閱讀系列文章(二十一)基於規則的優化 II
在 TiDB 原始碼閱讀系列文章(七)基於規則的優化 一文中,我們介紹了幾種 TiDB 中的邏輯優化規則,包括列剪裁,最大最小消除,投影消除,謂詞下推和構建節點屬性,本篇將繼續介紹更多的優化規則:聚合消除、外連線消除和子查詢優化。 聚合消除 聚合消除會檢查 SQL 查詢中 Group By 語句所使用的列是否
Spring Boot乾貨系列:(七)預設日誌logback配置解析
前言 今天來介紹下Spring Boot如何配置日誌logback,我剛學習的時候,是帶著下面幾個問題來查資料的 如何引入日誌? 日誌輸出格式以及輸出方式如何配置? 程式碼中如何使用? 正文 Spring Boot在所有
讀logback原始碼系列文章(四)——記錄日誌
今天晚上本來想來寫一下Logger怎麼記錄日誌,以及Appender元件。不過9點才從丈母孃家回來,又被幾個兄弟喊去喝酒,結果回來晚了,所以時間就只夠寫一篇Logger類的原始碼分析了。Appender找時間再寫 上篇部落格介紹了LoggerContext怎麼生成Logger
Spring Boot乾貨系列:(七)預設日誌logback配置解析
前言 今天來介紹下Spring Boot如何配置日誌logback,我剛學習的時候,是帶著下面幾個問題來查資料的,你呢 - 如何引入日誌? - 日誌輸出格式以及輸出方式如何配置? - 程式碼中如何使用? 正文 Spring Boot在所有
讀logback原始碼系列文章(八)——記錄日誌的實際工作類Encoder
本系列的部落格從logback怎麼對接slf4j開始,逐步介紹了LoggerContext、Logger、ContextInitializer、Appender、Action等核心元件。跟讀logback的原始碼到這個程度,雖然不能說精通,不過至少日常的配置,和簡單的自定義擴
Java系列文章(全)
java 學習JVMJVM系列:類裝載器的體系結構 JVM系列:Class文件檢驗器JVM系列:安全管理器JVM系列:策略文件Java垃圾回收機制深入剖析Classloader(一)--類的主動使用與被動使用深入剖析Classloader(二)-根類加載器,擴展類加載器與系統類加載器深入理解JVM—JVM內存
openstack系列文章(四)
cnblogs 調度器 5.5 min 代碼位置 虛機 inux latest 階段 學習 openstack 的系列文章 - Nova Nova 基本概念 Nova 架構 openstack Log Nova 組件介紹 Nova 操作介紹 1. Nova 基本概念
SVM支援向量機系列理論(七) 線性支援向量機與L2正則化 Platt模型
7.1 軟間隔SVM等價於最小化L2正則的合頁損失 上一篇 說到, ξi ξ i \xi_i 表示偏離邊界的度量,若樣本點