原文地址:https://www.ardanlabs.com/blog/2020/06/python-go-grpc.html
介紹
像工具一樣猛铅,編程語言也傾向于解決那些他們被設(shè)計(jì)之初想解決的問題。你可以使用小刀擰緊螺絲,但最好還是使用螺絲刀,同時(shí)這也可以避免在此過程中受傷。
Go編程語言在編寫高吞吐量服務(wù)時(shí)很有用袭厂,而Python在處理數(shù)據(jù)科學(xué)相關(guān)問題時(shí)表現(xiàn)的很棒。在這一系列博客文章中饭入,我們將探討如何使用各種語言來做的更好嵌器,并探討Go和Python之間的各種通信方法。
注意:在項(xiàng)目中使用多種語言會(huì)產(chǎn)生額外成本谐丢。如果你能只用Go或Python編寫所有功能爽航,那么一定要這樣做。但是乾忱,在某些情況下讥珍,使用正確的語言來完成工作可以減少可讀性,維護(hù)性和應(yīng)用性能各方面的總體開銷窄瘟。
在本文中衷佃,我們將學(xué)習(xí)Go和Python程序如何使用gRPC相互通信。這篇文章假定你具有一定Go和Python的基本知識(shí)儲(chǔ)備蹄葱。
gRPC概述
gRPC是Google的遠(yuǎn)程過程調(diào)用(RPC)框架氏义。它使用Protobuf作為序列化格式锄列,并使用HTTP2作為傳輸介質(zhì)。
通過使用這兩種完善的技術(shù)惯悠,你可以學(xué)習(xí)到許多現(xiàn)有的知識(shí)和工具邻邮。我咨詢過的許多公司,他們都在使用gRPC連接內(nèi)部服務(wù)克婶。
使用Protobuf的另一個(gè)優(yōu)點(diǎn)是筒严,你只需編寫一次消息定義,然后從同一來源生成對(duì)其他語言的綁定情萤。這意味著可以使用不同的編程語言編寫各種服務(wù)鸭蛙,并且所有應(yīng)用程序都統(tǒng)一消息的格式。
Protobuf也是一種高效的二進(jìn)制格式:你可以獲得更快的序列化速度和更少的在線字節(jié)數(shù)筋岛,僅此一項(xiàng)就可以節(jié)省很多成本娶视。在我的機(jī)器上進(jìn)行基準(zhǔn)測(cè)試,與JSON相比睁宰,序列化時(shí)間要快7.5倍左右歇万,生成的數(shù)據(jù)要小4倍左右。
示例:異常檢測(cè)
異常檢測(cè)是一種在數(shù)據(jù)中查找異常值的方法勋陪。系統(tǒng)從其服務(wù)中收集了大量指標(biāo)贪磺,很難通過簡(jiǎn)單的閾值找到有故障的服務(wù),這意味著即使凌晨2點(diǎn)也要呼叫開發(fā)人員诅愚。
我們將實(shí)現(xiàn)一個(gè)Go服務(wù)收集指標(biāo)寒锚。然后,使用gRPC违孝,我們會(huì)將這些指標(biāo)發(fā)送到Python服務(wù)刹前,該服務(wù)將對(duì)它們進(jìn)行異常檢測(cè)。
項(xiàng)目結(jié)構(gòu)
在這個(gè)項(xiàng)目中雌桑,我們將采用一種簡(jiǎn)單的方法喇喉,在源代碼樹中將Go作為主項(xiàng)目,將Python作為子項(xiàng)目校坑。
代碼1
.
├── client.go
├── gen.go
├── go.mod
├── go.sum
├── outliers.proto
├── pb
│ └── outliers.pb.go
└── py
├── Makefile
├── outliers_pb2_grpc.py
├── outliers_pb2.py
├── requirements.txt
└── server.py
代碼1顯示了我們項(xiàng)目的目錄結(jié)構(gòu)拣技。該項(xiàng)目正在使用Go模塊并且在go.mod
文件中定義了模塊的名稱(請(qǐng)參見代碼2)。我們將在多個(gè)地方引用模塊(github.com/ardanlabs/python-go/grpc
)耍目。
代碼2
01 module github.com/ardanlabs/python-go/grpc
02
03 go 1.14
04
05 require (
06 github.com/golang/protobuf v1.4.2
07 google.golang.org/grpc v1.29.1
08 google.golang.org/protobuf v1.24.0
09 )
代碼2展示了go.mod項(xiàng)目的文件膏斤。你可以在第01行看到定義模塊名稱的位置。
定義消息和服務(wù)
在gRPC中邪驮,您首先要編寫一個(gè).proto文件莫辨,該文件定義了要發(fā)送的消息和RPC方法。
代碼3
01 syntax = "proto3";
02 import "google/protobuf/timestamp.proto";
03 package pb;
04
05 option go_package = "github.com/ardanlabs/python-go/grpc/pb";
06
07 message Metric {
08 google.protobuf.Timestamp time = 1;
09 string name = 2;
10 double value = 3;
11 }
12
13 message OutliersRequest {
14 repeated Metric metrics = 1;
15 }
16
17 message OutliersResponse {
18 repeated int32 indices = 1;
19 }
20
21 service Outliers {
22 rpc Detect(OutliersRequest) returns (OutliersResponse) {}
23 }
代碼3顯示了outliers.proto
的內(nèi)容。這里要重點(diǎn)提及第02行沮榜,這里導(dǎo)入了Protobuf定義的timestamp
盘榨,然后在05行,定義了完整的Go包名稱-github.com/ardanlabs/python-go/grpc/pb
指標(biāo)是對(duì)資源使用情況的計(jì)量標(biāo)準(zhǔn)蟆融,用于監(jiān)視和診斷系統(tǒng)较曼。我們?cè)诘?7行定義一個(gè)Metric
,帶有時(shí)間戳振愿,名稱(例如“ CPU”)和浮點(diǎn)值。例如弛饭,我們可以說在2020-03-14T12:30:14
測(cè)量到CPU利用率為41.2%
冕末。
每個(gè)RPC方法都有一個(gè)或多個(gè)輸入類型和一個(gè)輸出類型。我們的方法Detect
(第22行)使用OutliersRequest
消息類型(第13行)作為輸入侣颂,并使用OutliersResponse
消息類型(第17行)作為輸出档桃。OutliersRequest
消息類型是Metric
的列表/切片,OutliersResponse
消息類型是列表索引,表示發(fā)現(xiàn)的異常值的列表和/切片憔晒。例如藻肄,如果我們具有的值[1, 2, 100, 1, 3, 200, 1],則結(jié)果將2, 5]表示100和200的索引拒担。
Python服務(wù)
在本節(jié)中嘹屯,我們將介紹Python服務(wù)代碼。
代碼4
.
├── client.go
├── gen.go
├── go.mod
├── go.sum
├── outliers.proto
├── pb
│ └── outliers.pb.go
└── py
├── Makefile
├── outliers_pb2_grpc.py
├── outliers_pb2.py
├── requirements.txt
└── server.py
代碼4中我們可以看見Python服務(wù)的代碼位于項(xiàng)目根目錄之外的py
目錄中从撼。
要生成Python綁定州弟,需要安裝protoc
編譯器,你可以在此處下載低零。也可以使用操作系統(tǒng)軟件包管理器(例如apt-get
婆翔,brew
…)安裝編譯器。
安裝編譯器后掏婶,還需要安裝Python grpcio-tools
軟件包啃奴。
注意:我強(qiáng)烈建議你為所有Python項(xiàng)目都使用虛擬環(huán)境。閱讀這部分內(nèi)容以了解更多信息雄妥。
代碼5
$ cat requirements.txt
OUTPUT:
grpcio-tools==1.29.0
numpy==1.18.4
$ python -m pip install -r requirements.txt
代碼5顯示了如何檢查和安裝Python項(xiàng)目的外部依賴最蕾。在requirements.txt
為項(xiàng)目指定外部依賴,很像go項(xiàng)目中的go.mod
老厌。
從cat
命令的輸出中可以看到揖膜,我們需要兩個(gè)外部依賴項(xiàng):grpcio-tools
和numpy。好的做法是將此文件置于源代碼管理中梅桩,并始終對(duì)依賴項(xiàng)(例如numpy==1.18.4
)進(jìn)行版本控制壹粟,類似于對(duì)Go項(xiàng)目中go.mod
的操作。
一旦安裝完成,就可以生成Python綁定了趁仙。
代碼6
$ python -m grpc_tools.protoc \
-I.. --python_out=. --grpc_python_out=. \
../outliers.proto
代碼6顯示了如何為gRPC支持生成Python綁定洪添。讓我們分解一下這個(gè)長(zhǎng)命令:
-
python -m grpc_tools.protoc
將grpc_tools.protoc
模塊作為腳本運(yùn)行。 -
-I..
告訴工具.proto
可以在哪里找到雀费。 -
--python_out=.
告訴該工具在當(dāng)前目錄中生成Protobuf序列化代碼干奢。 -
--grpc_python_out=.
告訴工具在當(dāng)前目錄中生成gRPC代碼。 -
../outliers.proto
是Protobuf+ gRPC定義文件的名稱盏袄。
這條命令運(yùn)行時(shí)沒有任何輸出忿峻,最后,你將看到兩個(gè)新文件:outliers_pb2.py
這是Protobuf代碼辕羽,outliers_pb2_grpc.py
這是gRPC客戶端和服務(wù)器代碼逛尚。
注意:我通常使用 Makefile
來自動(dòng)化Python項(xiàng)目中的任務(wù),并創(chuàng)建一條make
規(guī)則來運(yùn)行此命令刁愿。將生成的文件添加到源代碼管理中绰寞,以便部署計(jì)算機(jī)不必安裝protoc
編譯器。
要編寫Python服務(wù)铣口,你需要繼承outliers_pb2_grpc.py
中的OutliersServicer
并覆寫Detect
方法滤钱。我們將使用numpy包,并使用一種簡(jiǎn)單的方法來選擇所有與均值超過兩個(gè)標(biāo)準(zhǔn)差的值脑题。
代碼7
01 import logging
02 from concurrent.futures import ThreadPoolExecutor
03
04 import grpc
05 import numpy as np
06
07 from outliers_pb2 import OutliersResponse
08 from outliers_pb2_grpc import OutliersServicer, add_OutliersServicer_to_server
09
10
11 def find_outliers(data: np.ndarray):
12 """Return indices where values more than 2 standard deviations from mean"""
13 out = np.where(np.abs(data - data.mean()) > 2 * data.std())
14 # np.where returns a tuple for each dimension, we want the 1st element
15 return out[0]
16
17
18 class OutliersServer(OutliersServicer):
19 def Detect(self, request, context):
20 logging.info('detect request size: %d', len(request.metrics))
21 # Convert metrics to numpy array of values only
22 data = np.fromiter((m.value for m in request.metrics), dtype='float64')
23 indices = find_outliers(data)
24 logging.info('found %d outliers', len(indices))
25 resp = OutliersResponse(indices=indices)
26 return resp
27
28
29 if __name__ == '__main__':
30 logging.basicConfig(
31 level=logging.INFO,
32 format='%(asctime)s - %(levelname)s - %(message)s',
33 )
34 server = grpc.server(ThreadPoolExecutor())
35 add_OutliersServicer_to_server(OutliersServer(), server)
36 port = 9999
37 server.add_insecure_port(f'[::]:{port}')
38 server.start()
39 logging.info('server ready on port %r', port)
40 server.wait_for_termination()
代碼7顯示了server.py文件中的代碼件缸。這就是我們編寫Python服務(wù)所需的全部代碼。在第19行中叔遂,我們復(fù)寫Detect為編寫實(shí)際的異常值檢測(cè)代碼停团。在第34行中,我們創(chuàng)建了一個(gè)使用ThreadPoolExecutor的gRPC服務(wù)器掏熬,在第35行中佑稠,我們注冊(cè)了OutliersServer來處理服務(wù)器中的請(qǐng)求。
代碼8
$ python server.py
OUTPUT:
2020-05-23 13:45:12,578 - INFO - server ready on port 9999
代碼8顯示了如何運(yùn)行服務(wù)旗芬。
Go客戶端
現(xiàn)在我們已經(jīng)運(yùn)行了Python服務(wù)舌胶,我們可以編寫與其通信的Go客戶端。
我們從為gRPC生成Go綁定開始疮丛。為了使這個(gè)過程自動(dòng)化幔嫂,我通常有一個(gè)帶有go:generate
命令的gen.go
文件來生成綁定。你可以在github.com/golang/protobuf/protoc-gen-go下載Go的gRPC插件模塊誊薄。
代碼9
01 package main
02
03 //go:generate mkdir -p pb
04 //go:generate protoc --go_out=plugins=grpc:pb --go_opt=paths=source_relative outliers.proto
代碼9顯示了gen.go
文件以及go:generate
如何執(zhí)行g(shù)RPC插件來生成綁定履恩。
讓我們分解第04行的命令:
- protoc 是Protobuf編譯器。
-
--go-out=plugins=grpc:pb
告訴protoc
使用gRPC插件并將文件放置在pb目錄中呢蔫。 -
--go_opt=source_relative
告訴protoc
在pb相對(duì)于當(dāng)前目錄中生成代碼切心。 - outliers.proto 是Protobuf+ gRPC定義文件的名稱飒筑。
當(dāng)你運(yùn)行go generate
后,你應(yīng)該看不到輸出绽昏,但會(huì)在pb目錄出現(xiàn)一個(gè)名為outliers.pb.go
的新文件协屡。
代碼10
.```
├── client.go
├── gen.go
├── go.mod
├── go.sum
├── outliers.proto
├── pb
│ └── outliers.pb.go
└── py
├── Makefile
├── outliers_pb2_grpc.py
├── outliers_pb2.py
├── requirements.txt
└── server.py
代碼10顯示了`pb`目錄和調(diào)用·go generate·生成的新文件`outliers.pb.go`。我將`pb`目錄添加到源代碼管理中全谤,因此肤晓,如果將項(xiàng)目克隆到新計(jì)算機(jī)上,無需重新安裝`protoc`該項(xiàng)目也可以運(yùn)行认然。
現(xiàn)在我們可以構(gòu)建并運(yùn)行Go客戶端补憾。
**代碼11**
01 package main
02
03 import (
04 "context"
05 "log"
06 "math/rand"
07 "time"
08
09 "github.com/ardanlabs/python-go/grpc/pb"
10 "google.golang.org/grpc"
11 pbtime "google.golang.org/protobuf/types/known/timestamppb"
12 )
13
14 func main() {
15 addr := "localhost:9999"
16 conn, err := grpc.Dial(addr, grpc.WithInsecure(), grpc.WithBlock())
17 if err != nil {
18 log.Fatal(err)
19 }
20 defer conn.Close()
21
22 client := pb.NewOutliersClient(conn)
23 req := pb.OutliersRequest{
24 Metrics: dummyData(),
25 }
26
27 resp, err := client.Detect(context.Background(), &req)
28 if err != nil {
29 log.Fatal(err)
30 }
31 log.Printf("outliers at: %v", resp.Indices)
32 }
33
34 func dummyData() []pb.Metric {
35 const size = 1000
36 out := make([]pb.Metric, size)
37 t := time.Date(2020, 5, 22, 14, 13, 11, 0, time.UTC)
38 for i := 0; i < size; i++ {
39 m := pb.Metric{
40 Time: Timestamp(t),
41 Name: "CPU",
42 // Normally we're below 40% CPU utilization
43 Value: rand.Float64() * 40,
44 }
45 out[i] = &m
46 t.Add(time.Second)
47 }
48 // Create some outliers
49 out[7].Value = 97.3
50 out[113].Value = 92.1
51 out[835].Value = 93.2
52 return out
53 }
54
55 // Timestamp converts time.Time to protobuf *Timestamp
56 func Timestamp(t time.Time) *pbtime.Timestamp {
57 return &pbtime.Timestamp {
58 Seconds: t.Unix(),
59 Nanos: int32(t.Nanosecond()),
60 }
61 }
代碼11顯示了`client.go`中的代碼。在第23行代碼為`OutliersRequest`的值填充了一些虛擬數(shù)據(jù)(由第34行的`dummyData`函數(shù)生成)卷员,然后在第27行調(diào)用Python服務(wù)盈匾。對(duì)Python服務(wù)的調(diào)用返回一個(gè)`OutlirsResponse`。
讓我們進(jìn)一步分解代碼:
* 在第16行子刮,我們使用`WithInsecure`選項(xiàng)連接到Python服務(wù)器验辞,因?yàn)槲覀兙帉懙腜ython服務(wù)器不支持HTTPS篷帅。
* 在第22行,我們使用第16行創(chuàng)建的鏈接創(chuàng)建了一個(gè)新`OutliersClient`對(duì)象鼻由。
* 在第23行担钮,我們創(chuàng)建了gPRC請(qǐng)求橱赠。
* 在第27行,我們執(zhí)行了實(shí)際的gRPC調(diào)用箫津。每個(gè)gRPC調(diào)用都有一個(gè)`context.Context`作為第一個(gè)參數(shù)狭姨,這讓我們可以控制超時(shí)和取消請(qǐng)求。
* gRPC擁有自己的`Timestamp`結(jié)構(gòu)實(shí)現(xiàn)苏遥。在第56行饼拍,我們使用一個(gè)通用的程序函數(shù)將Go的`time.Time`值轉(zhuǎn)換為gRPC的`Timestamp`值。
**代碼12**
$ go run client.go
OUTPUT:
2020/05/23 14:07:18 outliers at: [7 113 835]
代碼12顯示了如何運(yùn)行Go客戶端田炭。假設(shè)Python服務(wù)器在同一臺(tái)計(jì)算機(jī)上運(yùn)行师抄。
### 結(jié)論
gRPC使得將消息從一種服務(wù)傳遞到另一種服務(wù)變得容易且安全。你可以維護(hù)一個(gè)定義了所有數(shù)據(jù)類型和方法的地方教硫,同時(shí)gRPC框架提供了出色的工具并進(jìn)行過實(shí)踐叨吮。
整個(gè)代碼:`outliers.proto`,`py/server.py`和`client.go`少于100行瞬矩。你可以在[grpc](https://github.com/ardanlabs/python-go/tree/master/grpc)查看項(xiàng)目代碼茶鉴。
gRPC還有更多功能,例如超時(shí)景用,負(fù)載均衡涵叮,TLS和流式傳輸。我強(qiáng)烈建議瀏覽[官方網(wǎng)站](https://grpc.io/)閱讀文檔并嘗試一下提供的示例。
在本系列的下一篇文章中围肥,我們將調(diào)換角色并讓Python調(diào)用Go服務(wù)剿干。