
如何快速實(shí)現(xiàn)REST API集成以優(yōu)化業(yè)務(wù)流程
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)返回,因此我們必須讓它們保持同步。
開發(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í)不一定。
除了面向公眾的 API 之外,現(xiàn)在的通信完全是內(nèi)部的、服務(wù)到服務(wù)的,沒有人參與。
當(dāng)你遇到下列情況時(shí),REST 是一個(gè)不錯(cuò)的選擇:
但上面這些都不符合我們的情況。
REST API 的代碼生成需要你使用第三方工具,并且不受原生支持。這可能會(huì)有非常多的局限,例如在 Go 中就沒有用于生成完全兼容的 OAS3 客戶端的庫。JSON
JSON 是迄今為止最流行的 REST API 數(shù)據(jù)格式,但它有幾個(gè)限制:
考慮上面的場景,哪種請求方法最適合檢索玩家的統(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í)間。
本質(zhì)上,RPC 的用途是讓一臺機(jī)器上的程序能夠調(diào)用網(wǎng)絡(luò)上另一臺機(jī)器上的子程序。RPC 更多是關(guān)于動(dòng)作的,而 REST 的重點(diǎn)則在資源上。
RPC 服務(wù)可以比 REST 更簡單、性能更好,但代價(jià)是靈活性和獨(dú)立性。對于服務(wù)到服務(wù)的通信來說,這完全不是什么問題。
雖然 Go 中還有其他一些 RPC 框架,但除非我的確沒得選,否則我會(huì)使用 Twirp,原因如下:
gRPC 應(yīng)該獲得榮譽(yù)提名,并且絕對有它自己的用武之地,尤其是當(dāng)你需要雙向流傳輸、長生命周期連接和客戶端負(fù)載平衡時(shí)。
gRPC 的缺點(diǎn)是你經(jīng)常會(huì)遇到各種問題,需要第三方支持,例如 grpc-gateway、grpc-web 等。
Protobuf 是谷歌編寫的一種數(shù)據(jù)序列化機(jī)制,并且越來越流行了。
它是開源的,也是語言和平臺中立的。
Protobuf 使用一個(gè)二進(jìn)制傳輸格式,這意味著它不是人類可讀的,但也意味著它會(huì)消耗更少的空間和帶寬,消耗更少的 CPU。
與其他類型相比,Protobuf 具有以下優(yōu)勢:
為了對比 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è)字符串。
為此,我們需要安裝這兩個(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