stats api 將接收來自不同類型來源的請求,各自請求不同類型的數(shù)據(jù)。

此外,我們不想給我們的數(shù)據(jù)庫帶來太多壓力,所以我們在 stats api 和 stats writer 之間放了一個(gè)隊(duì)列,它會(huì)以 10 個(gè)項(xiàng)目為一組寫入數(shù)據(jù)庫。

其他組件會(huì)收到諸如“我想對比 Devin Booker 和 Chris Middleton”之類的請求,因此它們必須從數(shù)據(jù)庫中獲取數(shù)據(jù)并做一些高級計(jì)算。

這種請求是由用戶發(fā)起的,必須在幾秒鐘或更短的時(shí)間內(nèi)返回,因此我們必須讓它們保持同步。

3. 那么選項(xiàng)是?

開發(fā)人員和架構(gòu)師選擇 RESTful API 作為服務(wù)之間的通信方式是很常見的,但我想解釋為什么 REST 可能是我實(shí)在沒辦法才會(huì)考慮的選項(xiàng)之一。REST

當(dāng)今最常見的 API 實(shí)現(xiàn)是 REST。REST 是 REpresentational State Transfer(表征狀態(tài)轉(zhuǎn)移)的首字母縮寫詞。

REST 依賴于一個(gè)無狀態(tài)的客戶端 – 服務(wù)器協(xié)議,其中客戶端和服務(wù)器是完全分離的(關(guān)注點(diǎn)分離)。可以使用緩存來提高網(wǎng)絡(luò)效率和性能。

REST API 有一個(gè)統(tǒng)一的接口,允許應(yīng)用程序獨(dú)立演進(jìn),而無需應(yīng)用程序的服務(wù)或模型和動(dòng)作與 API 層本身緊密耦合。

REST API 也是由一些限制組件行為的分層結(jié)構(gòu)組成的,因此每個(gè)組件無法看到與其交互的所在層之外的內(nèi)容。

由于這些原因,REST API 在過去十年中憑借可擴(kuò)展性、性能和易用性的優(yōu)勢而廣受歡迎,幾乎所有人都在使用它們。

聽起來就該是它了?其實(shí)不一定。

4. 為什么 REST API 并不一定是正確的選擇服務(wù)到服務(wù)通信?

除了面向公眾的 API 之外,現(xiàn)在的通信完全是內(nèi)部的、服務(wù)到服務(wù)的,沒有人參與。

當(dāng)你遇到下列情況時(shí),REST 是一個(gè)不錯(cuò)的選擇:

  1. 需要支持不同類型的客戶端:瀏覽器、手機(jī)等;
  2. 希望你的請求 – 響應(yīng)是人類可讀的;
  3. 需要一個(gè)標(biāo)準(zhǔn)的和廣泛采用的接口和消息格式;
  4. 需要支持大量的語言和庫。

但上面這些都不符合我們的情況。

代碼生成

REST API 的代碼生成需要你使用第三方工具,并且不受原生支持。這可能會(huì)有非常多的局限,例如在 Go 中就沒有用于生成完全兼容的 OAS3 客戶端的庫。JSON

JSON 是迄今為止最流行的 REST API 數(shù)據(jù)格式,但它有幾個(gè)限制:

  1. 沒有模式(schema):我們的數(shù)據(jù)庫有模式,我們的代碼編寫的時(shí)候就保留了一種模式,那么為什么我們的數(shù)據(jù)格式?jīng)]有模式呢?也有用于 JSON 的模式驗(yàn)證器,但它們并不常用,并且是作為外部庫提供的,還需要額外的代碼;
  2. 速度:除了瀏覽器、服務(wù)端等用 JS 編寫的 JavaScript 原生環(huán)境外,JSON 序列化最高可比 protobuf 慢 6 倍。二進(jìn)制序列化往往比文本序列化更快;
  3. 大小:JSON 生成的對象比二進(jìn)制選項(xiàng)要大;
  4. 額外代碼:JSON 需要樣板代碼來序列化 / 反序列化數(shù)據(jù),但是你編寫的代碼越多,出錯(cuò)的概率也就越大。此外,你正在浪費(fèi)時(shí)間編寫與業(yè)務(wù)無關(guān)的代碼;
  5. 數(shù)據(jù)類型:JSON 僅支持有限數(shù)量的數(shù)據(jù)類型:字符串、數(shù)字、布爾值、空值、對象、數(shù)組;
  6. 向后兼容性:JSON 不向后兼容。

可維護(hù)性:

考慮上面的場景,哪種請求方法最適合檢索玩家的統(tǒng)計(jì)數(shù)據(jù)呢?

POST /stats/:name
PUT /stats/:name

應(yīng)該發(fā)送哪些標(biāo)頭、查詢參數(shù)和 / 或請求正文?應(yīng)該有什么響應(yīng)?我們?nèi)绾蝹鬟_(dá)錯(cuò)誤?要問的問題太多,要做出的決定也太多了。

開發(fā)人員可能需要通讀由什么人撰寫的 API 文檔,并且通常還需要閱讀應(yīng)用程序的代碼以了解端點(diǎn)的實(shí)際工作方式。

于是我們又要花費(fèi)很多寶貴的時(shí)間。

5. 了解 RPC

本質(zhì)上,RPC 的用途是讓一臺機(jī)器上的程序能夠調(diào)用網(wǎng)絡(luò)上另一臺機(jī)器上的子程序。RPC 更多是關(guān)于動(dòng)作的,而 REST 的重點(diǎn)則在資源上。

RPC 服務(wù)可以比 REST 更簡單、性能更好,但代價(jià)是靈活性和獨(dú)立性。對于服務(wù)到服務(wù)的通信來說,這完全不是什么問題。

6. Go 中的 RPC

雖然 Go 中還有其他一些 RPC 框架,但除非我的確沒得選,否則我會(huì)使用 Twirp,原因如下:

  1. 它的設(shè)置非常簡單,這對我來說最重要;
  2. 同時(shí)支持 http 1.1 和 http 2.0;
  3. 同時(shí)支持 Protobuf 和 JSON;
  4. 易于調(diào)試。

gRPC 應(yīng)該獲得榮譽(yù)提名,并且絕對有它自己的用武之地,尤其是當(dāng)你需要雙向流傳輸、長生命周期連接和客戶端負(fù)載平衡時(shí)。

gRPC 的缺點(diǎn)是你經(jīng)常會(huì)遇到各種問題,需要第三方支持,例如 grpc-gateway、grpc-web 等。

7. Protobuf

Protobuf 是谷歌編寫的一種數(shù)據(jù)序列化機(jī)制,并且越來越流行了。

它是開源的,也是語言和平臺中立的。

Protobuf 使用一個(gè)二進(jìn)制傳輸格式,這意味著它不是人類可讀的,但也意味著它會(huì)消耗更少的空間和帶寬,消耗更少的 CPU。

與其他類型相比,Protobuf 具有以下優(yōu)勢:

  1. 有一個(gè)模式;
  2. 更快更小;
  3. 向后兼容;
  4. 具有內(nèi)置的驗(yàn)證和擴(kuò)展能力;
  5. 支持更多的數(shù)據(jù)類型。

為了對比 Twirp 和 REST,我編寫了這個(gè)基礎(chǔ)應(yīng)用程序,可以通過 RPC 和 REST 發(fā)送 / 檢索玩家統(tǒng)計(jì)數(shù)據(jù)。

REST 實(shí)現(xiàn)很簡單,可以在這里找到,我們就跳過它直接來看 twirp。

首先,我們看看 stats.proto 文件:

syntax = "proto3";
import "google/protobuf/timestamp.proto";
option go_package = "./rpc/stats";
package stats;
service StatsService {
rpc AddStats(AddStatsRequest) returns (AddStatsResponse);
rpc GetStats(GetStatsRequest) returns (GetStatsResponse);}
message Stats {
string player_name = 1;
float minutes = 2;
int32 field_goals = 3;
int32 field_goal_attempts = 4;
int32 three_pointers_made = 5;
int32 three_pointer_attempts = 6;
int32 free_throws_made = 7;
int32 free_throw_attempts = 8;
int32 offensive_rebounds = 9;
int32 defensive_rebounds = 10;
int32 assists = 11;
int32 steals = 12;
int32 blocks = 13;
int32 turnovers = 14;
int32 personal_fouls = 15;
int32 points = 16;
string team = 17;
string opponent = 18;
google.protobuf.Timestamp game_date = 19;
}
message AddStatsRequest {
repeated Stats stats = 1;
}
message GetStatsRequest {
string player_name = 1;
}
message AddStatsResponse {
string status = 1;
}
message GetStatsResponse {
repeated Stats stats = 1;
}

該文件以一種結(jié)構(gòu)化格式對我們的消息建模,在這里我們以請求 – 響應(yīng)格式定義我們的 RPC 服務(wù)。例如:AddStats 函數(shù)接收 AddStatsRequest 消息,它本質(zhì)上是一個(gè) Stats 消息的數(shù)組,并返回一個(gè) AddStatsResponse 格式的消息,這里就是一個(gè)字符串。

如何生成 proto 文件

為此,我們需要安裝這兩個(gè)生成器:

go install github.com/twitchtv/twirp/protoc-gen-twirp
go install google.golang.org/protobuf/cmd/protoc-gen-go

從 proto 文件中生成.go 文件:

確保 $GOPATH 指向你的 golang 安裝目錄,在我的例子中是 /usr/local/go。

然后運(yùn)行:

protoc --proto_path=$GOPATH/src:. --twirp_out=../ --go_out=../ rpc/stats/stats.proto

兩個(gè)新文件:stats.pb.go 和 stats.twirp.go 包含一個(gè)客戶端和服務(wù)器實(shí)用程序。

還有一件很重要的事情需要提一下,我們需要在代碼中實(shí)現(xiàn) StatsService 接口。

下面是代碼(轉(zhuǎn)換部分沒在里面,以突出重點(diǎn)):

package twirphandler
import (
"context"
"net/http"
"time"
"github.com/subzero112233/golang-twirp/entity"
"github.com/subzero112233/golang-twirp/rpc/stats"
"github.com/subzero112233/golang-twirp/usecase/playerstats"
"github.com/twitchtv/twirp"
"google.golang.org/protobuf/types/known/timestamppb"
)
type TwirpHandler struct {
Usecase playerstats.UseCase
}
func NewTwirpHandler(usecase playerstats.UseCase) http.Handler {
t := &TwirpHandler{
Usecase: usecase,
}
return stats.NewStatsServiceServer(t)
}
// errors may not be working well. check this by returning an error from the usecase
func (t *TwirpHandler) GetStats(ctx context.Context, input *stats.GetStatsRequest) (*stats.GetStatsResponse, error) {
var stat entity.Stats
stat.PlayerName = input.PlayerName
data, err := t.Usecase.GetStats(stat.PlayerName)
if err != nil {
return &stats.GetStatsResponse{}, twirp.InternalError(err.Error())
}
statsSlice := convertFromEntity(data)
return &stats.GetStatsResponse{Stats: statsSlice}, nil
}
// errors may not be working well. check this by returning an error from the usecase
func (t *TwirpHandler) AddStats(ctx context.Context, input *stats.AddStatsRequest) (*stats.AddStatsResponse, error) {
e := convertToEntity(input.Stats)
err := t.Usecase.AddStats(e)
if err != nil {
return &stats.AddStatsResponse{}, twirp.InternalError(err.Error())
}
return &stats.AddStatsResponse{Status: "success"}, nil
}

我們看看它是如何工作的。

首先,需要啟動(dòng)服務(wù)器。

~ $ go run cmd/main.go

現(xiàn)在發(fā)送請求。首先是 AddStats,然后是 GetStats

我創(chuàng)建了一個(gè)示例客戶端實(shí)現(xiàn)來演示請求:

package main
import (
"context"
"fmt"
"net/http"
"github.com/subzero112233/golang-twirp/rpc/stats"
"github.com/twitchtv/twirp"
"google.golang.org/protobuf/types/known/timestamppb"
)
func main() {
endpoint := "http://localhost:8000"
client := stats.NewStatsServiceProtobufClient(endpoint, &http.Client{})
header := make(http.Header)
ctx := context.Background()
ctx, err := twirp.WithHTTPRequestHeaders(ctx, header)
if err != nil {
return
}
var statz []*stats.Stats
statz = append(statz, &stats.Stats{
PlayerName: "Devin Booker",
Minutes: 44,
FieldGoals: 18,
FieldGoalAttempts: 27,
ThreePointersMade: 6,
ThreePointerAttempts: 10,
FreeThrowsMade: 4,
FreeThrowAttempts: 4,
OffensiveRebounds: 0,
DefensiveRebounds: 5,
Assists: 3,
Steals: 1,
Blocks: 0,
Turnovers: 3,
PersonalFouls: 3,
Points: 55,
Team: "Phoenix Suns",
Opponent: "Milwaukee_Bucks",
GameDate: timestamppb.Now(),
})
respAdd, err := client.AddStats(ctx, &stats.AddStatsRequest{
Stats: statz})
if err != nil {
fmt.Println("AddStats returned an error :", err)
}
fmt.Println(respAdd)
respGet, err := client.GetStats(ctx, &stats.GetStatsRequest{
PlayerName: "Nicolas Batum"})
if err != nil {
fmt.Println("GetStats returned an error :", err)
}
fmt.Println(respGet)
}

如你所見,請求發(fā)送就像常規(guī)函數(shù)調(diào)用一樣!

~ $ go run example/example.go 
status:”success”
stats:{player_name:”Reggie Jackson” minutes:32.9 field_goals:14 field_goal_attempts:20 three_pointers_made:4 three_pointer_attempts:7 free_throws_made:6 free_throw_attempts:6 offensive_rebounds:1 defensive_rebounds:3 assists:6 steals:1 turnovers:2 personal_fouls:3 points:38 game_date:{seconds:1625916051}}

還有很酷的一點(diǎn)是我們也可以使用 curl 來發(fā)送請求。調(diào)試和初始設(shè)置時(shí)這非常方便:

echo 'player_name:"Jae Crowder"' \    
| protoc --encode stats.GetStatsRequest ./rpc/stats/stats.proto \
| curl -s --request POST \
--header "Content-Type: application/protobuf" \
--data-binary @- \
http://localhost:8000/twirp/stats.StatsService/GetStats \
| protoc --decode stats.GetStatsResponse ./rpc/stats/stats.proto

為了測試性能差異,我創(chuàng)建了兩個(gè)測試文件(rest_test.go、twirp_test.go):

~ $ go test -bench=. -benchtime=5000x
goos: linux
goarch: amd64
pkg: github.com/subzero112233/golang-twirp
cpu: Intel(R) Core(TM) i7–8550U CPU @ 1.80GHz
BenchmarkRestAdd-8 5000 533270 ns/op
BenchmarkRestGet-8 5000 475062 ns/op
BenchmarkTwirpAdd-8 5000 90284 ns/op
BenchmarkTwirpGet-8 5000 91833 ns/op
PASS
ok github.com/subzero112233/golang-twirp 5.966s

在較小的負(fù)載上差異可能會(huì)小一些,但也足夠明顯了,意味著必要時(shí)還是應(yīng)該使用 RPC。

本文章轉(zhuǎn)載微信公眾號@InfoQ

上一篇:

最流行的 RESTful API 要怎么設(shè)計(jì)?

下一篇:

API設(shè)計(jì):從REST到RPC
#你可能也喜歡這些API文章!

我們有何不同?

API服務(wù)商零注冊

多API并行試用

數(shù)據(jù)驅(qū)動(dòng)選型,提升決策效率

查看全部API→
??

熱門場景實(shí)測,選對API

#AI文本生成大模型API

對比大模型API的內(nèi)容創(chuàng)意新穎性、情感共鳴力、商業(yè)轉(zhuǎn)化潛力

25個(gè)渠道
一鍵對比試用API 限時(shí)免費(fèi)

#AI深度推理大模型API

對比大模型API的邏輯推理準(zhǔn)確性、分析深度、可視化建議合理性

10個(gè)渠道
一鍵對比試用API 限時(shí)免費(fèi)