Protocol¶
分散式系統中,很重要的一塊是「不同的服務間,彼此的溝通方式是什麼?」 比起撰寫完整又清楚的 API 文件,有沒有什麼好方法可以讓服務和服務之間同步 API?
以下討論皆假設:溝通是透過網路,且以 HTTP 協定為基礎。
OSI (Open Systems Interconnection)¶
- 應用層(Application Layer)
- 表達層(Presentation Layer)
- 會議層(Session Layer)
- 傳輸層(Transport Layer)
- 網路層(Network Layer)
- 資料連結層(Data Link Layer)
- 實體層(Physical Layer)
HTTP¶
POST / HTTP/1.1
Host: www.example.com
Content-Type: application/json
Content-Length: 15
{"name":"evan.lu"}
空行後的下一行即為代表本次請求的 body,範例中的 body 是常見的 JSON
格式。
由此,可以想像 JSON
格式是在應用層之上的第八層。
JSON¶
單純透過 JSON
傳遞有什麼缺點?
- 正確的資料格式應該要長什麼樣子?
- 使用者需要閱讀相關文件,有辦法讓機器自動處理嗎?
為了解決上述問題,就會有其他 protocol 需要被引入。
不過除了用其他協定,也有一些方式可以舒緩(降低)上述發生的問題,如:
上述僅是制定一些規範,讓使用者在閱讀相關 API 文件時,能較快進入狀況。
GraphQL¶
GraphQL 讓使用者在跟服務要取資料的時候能指定特定資料,這有幾個好處:
- 可以拿到最準確的資料,減少網路傳輸
- 把多種服務的資料在一次請求中要齊
這也讓 GraphQL 通常成為 facade services,也就是在眾多服務中的首個接觸點,並作為對外溝通的唯一渠道。
GraphQL 並不限定在要 HTTP 上執行,也能執行如 TCP 等協定之上。
雖然請求時送出的是類似 Query 的語法,但 Response 並無指定,只要能代表其階層式的結果就行,如
JSON
。
規範¶
type RecipeRoot {
recipe(id: ID): Recipe
pid: Int
}
type Recipe {
id: ID!
name: String!
steps: String
ingredients: [Ingredient]!
}
type Ingredient {
id: ID!
name: String!
quantity: String
}
這份檔案是可以對外公開的,幫助使用者依此撰寫程式,類似上述提到的 OpenAPI。
請求¶
這時,我們可以依照上述的規範送出請求:
{
pid
}
{
"data": {
"pid": 9372
}
}
{
recipe(id: 42) {
name
ingredients {
name
quantity
}
}
}
{
"data": {
"recipe": {
"name": "Chicken Tikka Masala",
"ingredients": [
{ "name": "Chicken", "quantity": "1 lb" },
{ "name": "Sauce", "quantity": "2 cups" }
]
}
}
}
Code Demo¶
下列則是以 Node.js 為基礎的範例:
// 僅展示請求的範例,這裡的 `kitchenSink` 是自定義名稱,方便 debug 用的
const query = `query kitchenSink ($id:ID) {
recipe(id: $id) {
id name
ingredients {
name quantity
}
}
pid
}`;
const variables = { id: "42" };
return got(`http://${TARGET}/graphql`, {
method: "POST",
json: { query, variables },
});
import {
GraphQLID,
GraphQLInt,
GraphQLObjectType,
GraphQLSchema,
} from "graphql";
// 僅展示 RecipeRoot 的建置方式
const recipeRoot = new GraphQLObjectType({
name: "RecipeRoot",
fields: {
pid: {
type: GraphQLInt,
resolve: resolvers.RecipeRoot.pid,
},
recipe: {
type: recipeQuery,
args: { id: { type: GraphQLID } },
resolve: resolvers.RecipeRoot.recipe,
},
},
});
return new GraphQLSchema({ query: rootQuery });
Live Demo¶
gRPC¶
像是 REST 或 GraphQL 都是建立在資料之上,而透過 CRUD 的方式去執行行為,這裡就可以注意到其限制:
大量的名詞,而僅有少量的動詞
舉例: 若有一個 API endpoint 是用來建立發票,今欲新增一附帶條件:是否同時寄送信箱通知。 有什麼樣的方式?
- 再建立一個 endpoint 專門做這件事: 過多 API,難管理和理解
- 在該 endpoint 新增變數:
need_send_email
: 讓該 endpoint 越來越複雜
Remote Procedure Call
就是來解決此事的!
gRPC 為 Google 建立的 RPC 標準
gRPC 預設即非使用 JSON 格式進行資訊的傳遞,而是以 Protocol Buffers
(ProtoBufs)的方式進行傳遞。
有幾個條件:
- 所有格式皆須預先設定好,副檔名為
.proto
,且需要讓 client 擁有。 - 各值需給定順序,且之後不建議修改。
- 數字有多型別:
int32
,int64
,float
,double
等等。
這些條件有幾個好處:
- 效能、體積的最優化,binary serialize/deserialize
{"id":42} v.s. 42
- 向後相容
v1 需要 arg1 arg2
v2 需要 arg1 arg2 arg3
若 client 僅拿到 v1 的 proto,程式上會自動忽略 arg2 後的參數
Code Demo¶
- gRPC proto
syntax = "proto3";
package recipe;
service RecipeService {
rpc GetRecipe(RecipeRequest) returns (Recipe) {}
rpc GetMetaData(Empty) returns (Meta) {}
}
message Recipe {
int32 id = 1;
string name = 2;
string steps = 3;
repeated Ingredient ingredients = 4;
}
message Ingredient {
int32 id = 1;
string name = 2;
string quantity = 3;
}
message RecipeRequest {
int32 id = 1;
}
message Meta {
int32 pid = 2;
}
message Empty {}
- 建立 service,原始碼
import { loadPackageDefinition, Server } from "@grpc/grpc-js";
import { loadSync } from "@grpc/proto-loader";
// 讀取 proto 檔
const def = loadSync(__dirname + "/grpc.proto");
const proto = loadPackageDefinition(def);
// 建立處理邏輯
// handlers = ...;
const server = new Server();
server.addService(proto.recipe.RecipeService.service, handlers);
// 建立對外連線
// credentials = ...; for https
const cb = () => server.start();
server.bindAsync(`${HOST}:${PORT}`, credentials, cb);
// 建立 handlers
const handlers = {
GetMetaData: (_call, cb) => {
cb(null, {
// error = null
pid: process.pid,
});
},
GetRecipe: (call, cb) => ({}), // if (call.request.id === 42)
};
- 建立 client,原始碼
import { loadPackageDefinition } from "@grpc/grpc-js";
import { loadSync } from "@grpc/proto-loader";
// 讀取 proto 檔
const def = loadSync(__dirname + "/grpc.proto");
const proto = loadPackageDefinition(def);
// credentials = ...; for https
const client = new proto.recipe.RecipeService(TARGET, credentials);
client.getMetaData({}, cb);
client.getRecipe({ id: 42 }, cb);
Live Demo¶
Alternatives¶
除了 gRPC 還有什麼類似的東西?
ProtoBufs
MessagePack
雖然同為 binary representation of hierarchical object data,但
- 有 field
- 不需要額外檔案(如
.proto
)去描述
gRPC
- Apache Thrift
- JSON RPC
關於 gRPC 推薦的文章:
結論¶
- JSON 你需要一個 http client 來呼叫眾多 API Endpoint,訊息格式也需要有額外的 Schema 定義
- GraphQL 你還是需要一個 http client,但是這次只需要對應一個端點,而且可以自己組織查詢內容
- gRPC 你連 http client 都不用,套件會幫你產出這些呼叫的程式邏輯,而你只需要像寫一般 function 一樣呼叫即可