1. 程式人生 > 實用技巧 >一般只用 20% 的程式碼就可以解決 80% 的問題。但要想解決剩下 20% 的問題的話,則需要額外 80% 的程式碼。

一般只用 20% 的程式碼就可以解決 80% 的問題。但要想解決剩下 20% 的問題的話,則需要額外 80% 的程式碼。

sniper/thought.md at master · bilibili/sniper https://github.com/bilibili/sniper/blob/master/thought.md

RPC 協議

有了語言,接下來就要確定通訊協議。首先不要使用 REST 風格介面。 REST 中看不中用。REST 的核心是資源和狀態,所有的變更都對應狀態的轉變。

對於簡單的場景,REST 看似完美,如:GET /user/123表示查詢。

但如果是傳送一條簡訊呢?一種方案是使用POST /sms表示建立一條簡訊資源,另一種方案則是POST /sms:send直接傳送。

但不管哪種方式,都不如 RPC 呼叫直觀,其原因有二:

  • 一是 http 的方法(GET, POST, PUT, DELETE 等)太少,基本都是面向靜態資源的,表達能力有限
  • 二是將業務過程轉成資源狀態變化本身就比較燒腦,而且存在無法轉化的場景

REST 還有一個比較大的問題就是 url 中有數字 id,統計 prometheus 監控指標的時候必須做歸一化處理。

所以,不用 REST。

Weisai RPC

這得從原來在 L 部門用的 Weisai-RPC 說起。該 RPC 基於 TCP 傳輸,訊息結構如下:

typedef struct swoole_message {
    uint32_t header_magic;     // magic 欄位 預設2233
    uint32_t header_ts;        // unix時間戳
    uint32_t header_check_sum; // 校驗和, 暫未定義, 預設為0
    uint32_t header_version;   // 版本號
    uint32_t header_reserved;  // 保留欄位, 預設0, live-api轉發時設定為1
    uint32_t header_seq;       // 序列號
    uint32_t header_len;       // body長度
    char cmd[32];              // 命令字串
                               // 格式 {message_type}controller.method,
                               // message_type 0 request, 1 response
                               // 長度沒滿右端補充\0, 超過自動右端截斷.
    char* body;                // 可變 長度為header_len 格式為JSON:
                               // {"header":..., "body":....}
} rpc_message_t;

典型的面向 c 語言的設計,方便 c 語言解析,但不太靈活。

比如,cmd 欄位只有 32 位元組,也就是說介面名字最多隻能是 32 位元組。還有 body 是字串,但實際傳輸的是 JSON,需要二次解析。使用結構化二進位制訊息就是為了提高解析速度,但這種改進跟 JSON 解碼相比又可以忽略。所以,這種混合型的設計除了看上去比較複雜以外,確實沒什麼優點了。

因為沒有采用 HTTP 協議,後來不得不在 body 中定義了 header 欄位用來傳輸 HTTP 請求的 header。像 nginx, curl, tcpdump 這樣的標準也基本上無法正常使用。為此,還專門引入了一個接入層負責 RPC 和 HTTP 之間的相互轉換。

切實體會到了 Weisai-RPC 的不便之後,我決定業務 RPC 協議只用 HTTP 傳輸,原則上不使用二進位制訊息格式。

關於 gRPC

說到 HTTP 就不得不說說 gRPC。gRPC 是 Google 開放的一種 RPC 協議,其主要特性:

  • 只支援 protobuf 編碼
  • 強依賴 HTTP2 協議
  • 支援 stream 介面
  • 每個訊息都有五位元組的二進位制字首 其他細節請參考PROTOCOL-HTTP2

protobuf 本身是支援 JSON 的,不明白為什麼 gRPC 的實現不支援。而支援 stream 介面則是 gRPC 的一大特色,使 gRPC 能夠勝任諸如語音實時識別等場景。但這一類場景是比較少見的。我們絕大多數業務場景都是一問一答的。為了實現這個 stream 特性,gRPC 不得不依賴 HTTP2,不得不自行定義了一種有固定五位元組頭的訊息格式。與此同時,gRPC 也就放棄了 HTTP 協議原生的壓縮功能,也沒法使用 HTTP 協議的 content-length 頭傳遞訊息長度。這也是 gRCP 訊息五位元組頭的功能所在,頭一個位元組表示是否壓縮,後四個位元組表示訊息長度。

有個所謂的2-8原則:

一般只用20%的程式碼就可以解決80%的問題。但要想解決剩下20%的問題的話,則需要額外80%的程式碼。

gRPC 的 stream 介面就是剩下的20% 的問題。

gRPC 還有個 web 支援的問題。瀏覽器的 js 無法使用 HTTP2 的特性,所以不能直接與 gRPC 服務通訊。於是有了grpc-web,還有grpc-gateway

所以,如果沒有 stream 介面需求,則完全沒有必要使用 gRPC;如果真的有這類需求,也不可能太多,直接使用原生 TCP/WebSocket 協議開發也不是難事。

最終我們選擇了twirp。twirp 可以看作是簡化版的 gRPC,同樣用 protobuf 描述,不依賴 HTTP2,同時支援 protobuf 和 JSON,沒有五位元組的二進位制字首。但我們對原生的 twirp 做了修改,形成了自己的版本,主要改動就是添加了對www-form-urlencoded編碼格式的支援,這是移動端的歷史包袱導致的,沒辦法。

現在的移動端使用 www-form-urlencoded 編碼,更加簡單;管理後臺使用 JSON 編碼,更加靈活。如果對效能有要求也可以使用 protobuf 編碼,但沒目前沒有用,估計也不會有人喜歡用。