
如何快速實現REST API集成以優化業務流程
stats api 將接收來自不同類型來源的請求,各自請求不同類型的數據。
此外,我們不想給我們的數據庫帶來太多壓力,所以我們在 stats api 和 stats writer 之間放了一個隊列,它會以 10 個項目為一組寫入數據庫。
其他組件會收到諸如“我想對比 Devin Booker 和 Chris Middleton”之類的請求,因此它們必須從數據庫中獲取數據并做一些高級計算。
這種請求是由用戶發起的,必須在幾秒鐘或更短的時間內返回,因此我們必須讓它們保持同步。
開發人員和架構師選擇 RESTful API 作為服務之間的通信方式是很常見的,但我想解釋為什么 REST 可能是我實在沒辦法才會考慮的選項之一。REST
當今最常見的 API 實現是 REST。REST 是 REpresentational State Transfer(表征狀態轉移)的首字母縮寫詞。
REST 依賴于一個無狀態的客戶端 – 服務器協議,其中客戶端和服務器是完全分離的(關注點分離)。可以使用緩存來提高網絡效率和性能。
REST API 有一個統一的接口,允許應用程序獨立演進,而無需應用程序的服務或模型和動作與 API 層本身緊密耦合。
REST API 也是由一些限制組件行為的分層結構組成的,因此每個組件無法看到與其交互的所在層之外的內容。
由于這些原因,REST API 在過去十年中憑借可擴展性、性能和易用性的優勢而廣受歡迎,幾乎所有人都在使用它們。
聽起來就該是它了?其實不一定。
除了面向公眾的 API 之外,現在的通信完全是內部的、服務到服務的,沒有人參與。
當你遇到下列情況時,REST 是一個不錯的選擇:
但上面這些都不符合我們的情況。
REST API 的代碼生成需要你使用第三方工具,并且不受原生支持。這可能會有非常多的局限,例如在 Go 中就沒有用于生成完全兼容的 OAS3 客戶端的庫。JSON
JSON 是迄今為止最流行的 REST API 數據格式,但它有幾個限制:
考慮上面的場景,哪種請求方法最適合檢索玩家的統計數據呢?
POST /stats/:name
PUT /stats/:name
應該發送哪些標頭、查詢參數和 / 或請求正文?應該有什么響應?我們如何傳達錯誤?要問的問題太多,要做出的決定也太多了。
開發人員可能需要通讀由什么人撰寫的 API 文檔,并且通常還需要閱讀應用程序的代碼以了解端點的實際工作方式。
于是我們又要花費很多寶貴的時間。
本質上,RPC 的用途是讓一臺機器上的程序能夠調用網絡上另一臺機器上的子程序。RPC 更多是關于動作的,而 REST 的重點則在資源上。
RPC 服務可以比 REST 更簡單、性能更好,但代價是靈活性和獨立性。對于服務到服務的通信來說,這完全不是什么問題。
雖然 Go 中還有其他一些 RPC 框架,但除非我的確沒得選,否則我會使用 Twirp,原因如下:
gRPC 應該獲得榮譽提名,并且絕對有它自己的用武之地,尤其是當你需要雙向流傳輸、長生命周期連接和客戶端負載平衡時。
gRPC 的缺點是你經常會遇到各種問題,需要第三方支持,例如 grpc-gateway、grpc-web 等。
Protobuf 是谷歌編寫的一種數據序列化機制,并且越來越流行了。
它是開源的,也是語言和平臺中立的。
Protobuf 使用一個二進制傳輸格式,這意味著它不是人類可讀的,但也意味著它會消耗更少的空間和帶寬,消耗更少的 CPU。
與其他類型相比,Protobuf 具有以下優勢:
為了對比 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 格式的消息,這里就是一個字符串。
為此,我們需要安裝這兩個生成器:
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