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

此外,我們不想給我們的數據庫帶來太多壓力,所以我們在 stats api 和 stats writer 之間放了一個隊列,它會以 10 個項目為一組寫入數據庫。

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

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

3. 那么選項是?

開發人員和架構師選擇 RESTful API 作為服務之間的通信方式是很常見的,但我想解釋為什么 REST 可能是我實在沒辦法才會考慮的選項之一。REST

當今最常見的 API 實現是 REST。REST 是 REpresentational State Transfer(表征狀態轉移)的首字母縮寫詞。

REST 依賴于一個無狀態的客戶端 – 服務器協議,其中客戶端和服務器是完全分離的(關注點分離)。可以使用緩存來提高網絡效率和性能。

REST API 有一個統一的接口,允許應用程序獨立演進,而無需應用程序的服務或模型和動作與 API 層本身緊密耦合。

REST API 也是由一些限制組件行為的分層結構組成的,因此每個組件無法看到與其交互的所在層之外的內容。

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

聽起來就該是它了?其實不一定。

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

除了面向公眾的 API 之外,現在的通信完全是內部的、服務到服務的,沒有人參與。

當你遇到下列情況時,REST 是一個不錯的選擇:

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

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

代碼生成

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

JSON 是迄今為止最流行的 REST API 數據格式,但它有幾個限制:

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

可維護性:

考慮上面的場景,哪種請求方法最適合檢索玩家的統計數據呢?

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

應該發送哪些標頭、查詢參數和 / 或請求正文?應該有什么響應?我們如何傳達錯誤?要問的問題太多,要做出的決定也太多了。

開發人員可能需要通讀由什么人撰寫的 API 文檔,并且通常還需要閱讀應用程序的代碼以了解端點的實際工作方式。

于是我們又要花費很多寶貴的時間。

5. 了解 RPC

本質上,RPC 的用途是讓一臺機器上的程序能夠調用網絡上另一臺機器上的子程序。RPC 更多是關于動作的,而 REST 的重點則在資源上。

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

6. Go 中的 RPC

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

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

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

gRPC 的缺點是你經常會遇到各種問題,需要第三方支持,例如 grpc-gateway、grpc-web 等。

7. Protobuf

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

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

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

與其他類型相比,Protobuf 具有以下優勢:

  1. 有一個模式;
  2. 更快更小;
  3. 向后兼容;
  4. 具有內置的驗證和擴展能力;
  5. 支持更多的數據類型。

為了對比 Twirp 和 REST,我編寫了這個基礎應用程序,可以通過 RPC 和 REST 發送 / 檢索玩家統計數據。

REST 實現很簡單,可以在這里找到,我們就跳過它直接來看 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;
}

該文件以一種結構化格式對我們的消息建模,在這里我們以請求 – 響應格式定義我們的 RPC 服務。例如:AddStats 函數接收 AddStatsRequest 消息,它本質上是一個 Stats 消息的數組,并返回一個 AddStatsResponse 格式的消息,這里就是一個字符串。

如何生成 proto 文件

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

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。

然后運行:

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

兩個新文件:stats.pb.go 和 stats.twirp.go 包含一個客戶端和服務器實用程序。

還有一件很重要的事情需要提一下,我們需要在代碼中實現 StatsService 接口。

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

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
}

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

首先,需要啟動服務器。

~ $ go run cmd/main.go

現在發送請求。首先是 AddStats,然后是 GetStats

我創建了一個示例客戶端實現來演示請求:

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)
}

如你所見,請求發送就像常規函數調用一樣!

~ $ 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}}

還有很酷的一點是我們也可以使用 curl 來發送請求。調試和初始設置時這非常方便:

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

為了測試性能差異,我創建了兩個測試文件(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

在較小的負載上差異可能會小一些,但也足夠明顯了,意味著必要時還是應該使用 RPC。

本文章轉載微信公眾號@InfoQ

上一篇:

最流行的 RESTful API 要怎么設計?

下一篇:

API設計:從REST到RPC
#你可能也喜歡這些API文章!

我們有何不同?

API服務商零注冊

多API并行試用

數據驅動選型,提升決策效率

查看全部API→
??

熱門場景實測,選對API

#AI文本生成大模型API

對比大模型API的內容創意新穎性、情感共鳴力、商業轉化潛力

25個渠道
一鍵對比試用API 限時免費

#AI深度推理大模型API

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

10個渠道
一鍵對比試用API 限時免費