原文地址:https://www.ardanlabs.com/blog/2020/06/python-go-grpc.html
介绍
像工具一样,编程语言也倾向于解决那些他们被设计之初想解决的问题。你可以使用小刀拧紧螺丝,但最好还是使用螺丝刀,同时这也可以避免在此过程中受伤。
Go编程语言在编写高吞吐量服务时很有用,而Python在处理数据科学相关问题时表现的很棒。在这一系列博客文章中,我们将探讨如何使用各种语言来做的更好,并探讨Go和Python之间的各种通信方法。
注意:在项目中使用多种语言会产生额外成本。如果你能只用Go或Python编写所有功能,那么一定要这样做。但是,在某些情况下,使用正确的语言来完成工作可以减少可读性,维护性和应用性能各方面的总体开销。
在本文中,我们将学习Go和Python程序如何使用gRPC相互通信。这篇文章假定你具有一定Go和Python的基本知识储备。
gRPC概述
gRPC是Google的远程过程调用(RPC)框架。它使用Protobuf作为序列化格式,并使用HTTP2作为传输介质。
通过使用这两种完善的技术,你可以学习到许多现有的知识和工具。我咨询过的许多公司,他们都在使用gRPC连接内部服务。
使用Protobuf的另一个优点是,你只需编写一次消息定义,然后从同一来源生成对其他语言的绑定。这意味着可以使用不同的编程语言编写各种服务,并且所有应用程序都统一消息的格式。
Protobuf也是一种高效的二进制格式:你可以获得更快的序列化速度和更少的在线字节数,仅此一项就可以节省很多成本。在我的机器上进行基准测试,与JSON相比,序列化时间要快7.5倍左右,生成的数据要小4倍左右。
示例:异常检测
异常检测是一种在数据中查找异常值的方法。系统从其服务中收集了大量指标,很难通过简单的阈值找到有故障的服务,这意味着即使凌晨2点也要呼叫开发人员。
我们将实现一个Go服务收集指标。然后,使用gRPC,我们会将这些指标发送到Python服务,该服务将对它们进行异常检测。
项目结构
在这个项目中,我们将采用一种简单的方法,在源代码树中将Go作为主项目,将Python作为子项目。
代码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显示了我们项目的目录结构。该项目正在使用Go模块并且在go.mod
文件中定义了模块的名称(请参见代码2)。我们将在多个地方引用模块(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项目的文件。你可以在第01行看到定义模块名称的位置。
定义消息和服务
在gRPC中,您首先要编写一个.proto文件,该文件定义了要发送的消息和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
的内容。这里要重点提及第02行,这里导入了Protobuf定义的timestamp
,然后在05行,定义了完整的Go包名称-github.com/ardanlabs/python-go/grpc/pb
指标是对资源使用情况的计量标准,用于监视和诊断系统。我们在第07行定义一个Metric
,带有时间戳,名称(例如“ CPU”)和浮点值。例如,我们可以说在2020-03-14T12:30:14
测量到CPU利用率为41.2%
。
每个RPC方法都有一个或多个输入类型和一个输出类型。我们的方法Detect
(第22行)使用OutliersRequest
消息类型(第13行)作为输入,并使用OutliersResponse
消息类型(第17行)作为输出。OutliersRequest
消息类型是Metric
的列表/切片,OutliersResponse
消息类型是列表索引,表示发现的异常值的列表和/切片。例如,如果我们具有的值[1, 2, 100, 1, 3, 200, 1],则结果将2, 5]表示100和200的索引。
Python服务
在本节中,我们将介绍Python服务代码。
代码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服务的代码位于项目根目录之外的py
目录中。
要生成Python绑定,需要安装protoc
编译器,你可以在此处下载。也可以使用操作系统软件包管理器(例如apt-get
,brew
…)安装编译器。
安装编译器后,还需要安装Python grpcio-tools
软件包。
注意:我强烈建议你为所有Python项目都使用虚拟环境。阅读这部分内容以了解更多信息。
代码5
$ cat requirements.txt
OUTPUT:
grpcio-tools==1.29.0
numpy==1.18.4
$ python -m pip install -r requirements.txt
代码5显示了如何检查和安装Python项目的外部依赖。在requirements.txt
为项目指定外部依赖,很像go项目中的go.mod
。
从cat
命令的输出中可以看到,我们需要两个外部依赖项:grpcio-tools
和numpy。好的做法是将此文件置于源代码管理中,并始终对依赖项(例如numpy==1.18.4
)进行版本控制,类似于对Go项目中go.mod
的操作。
一旦安装完成,就可以生成Python绑定了。
代码6
$ python -m grpc_tools.protoc \
-I.. --python_out=. --grpc_python_out=. \
../outliers.proto
代码6显示了如何为gRPC支持生成Python绑定。让我们分解一下这个长命令:
-
python -m grpc_tools.protoc
将grpc_tools.protoc
模块作为脚本运行。 -
-I..
告诉工具.proto
可以在哪里找到。 -
--python_out=.
告诉该工具在当前目录中生成Protobuf序列化代码。 -
--grpc_python_out=.
告诉工具在当前目录中生成gRPC代码。 -
../outliers.proto
是Protobuf+ gRPC定义文件的名称。
这条命令运行时没有任何输出,最后,你将看到两个新文件:outliers_pb2.py
这是Protobuf代码,outliers_pb2_grpc.py
这是gRPC客户端和服务器代码。
注意:我通常使用 Makefile
来自动化Python项目中的任务,并创建一条make
规则来运行此命令。将生成的文件添加到源代码管理中,以便部署计算机不必安装protoc
编译器。
要编写Python服务,你需要继承outliers_pb2_grpc.py
中的OutliersServicer
并覆写Detect
方法。我们将使用numpy包,并使用一种简单的方法来选择所有与均值超过两个标准差的值。
代码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服务所需的全部代码。在第19行中,我们复写Detect为编写实际的异常值检测代码。在第34行中,我们创建了一个使用ThreadPoolExecutor的gRPC服务器,在第35行中,我们注册了OutliersServer来处理服务器中的请求。
代码8
$ python server.py
OUTPUT:
2020-05-23 13:45:12,578 - INFO - server ready on port 9999
代码8显示了如何运行服务。
Go客户端
现在我们已经运行了Python服务,我们可以编写与其通信的Go客户端。
我们从为gRPC生成Go绑定开始。为了使这个过程自动化,我通常有一个带有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
如何执行gRPC插件来生成绑定。
让我们分解第04行的命令:
- protoc 是Protobuf编译器。
-
--go-out=plugins=grpc:pb
告诉protoc
使用gRPC插件并将文件放置在pb目录中。 -
--go_opt=source_relative
告诉protoc
在pb相对于当前目录中生成代码。 - outliers.proto 是Protobuf+ gRPC定义文件的名称。
当你运行go generate
后,你应该看不到输出,但会在pb目录出现一个名为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`目录和调用·go generate·生成的新文件`outliers.pb.go`。我将`pb`目录添加到源代码管理中,因此,如果将项目克隆到新计算机上,无需重新安装`protoc`该项目也可以运行。
现在我们可以构建并运行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`的值填充了一些虚拟数据(由第34行的`dummyData`函数生成),然后在第27行调用Python服务。对Python服务的调用返回一个`OutlirsResponse`。
让我们进一步分解代码:
* 在第16行,我们使用`WithInsecure`选项连接到Python服务器,因为我们编写的Python服务器不支持HTTPS。
* 在第22行,我们使用第16行创建的链接创建了一个新`OutliersClient`对象。
* 在第23行,我们创建了gPRC请求。
* 在第27行,我们执行了实际的gRPC调用。每个gRPC调用都有一个`context.Context`作为第一个参数,这让我们可以控制超时和取消请求。
* gRPC拥有自己的`Timestamp`结构实现。在第56行,我们使用一个通用的程序函数将Go的`time.Time`值转换为gRPC的`Timestamp`值。
**代码12**
$ go run client.go
OUTPUT:
2020/05/23 14:07:18 outliers at: [7 113 835]
代码12显示了如何运行Go客户端。假设Python服务器在同一台计算机上运行。
### 结论
gRPC使得将消息从一种服务传递到另一种服务变得容易且安全。你可以维护一个定义了所有数据类型和方法的地方,同时gRPC框架提供了出色的工具并进行过实践。
整个代码:`outliers.proto`,`py/server.py`和`client.go`少于100行。你可以在[grpc](https://github.com/ardanlabs/python-go/tree/master/grpc)查看项目代码。
gRPC还有更多功能,例如超时,负载均衡,TLS和流式传输。我强烈建议浏览[官方网站](https://grpc.io/)阅读文档并尝试一下提供的示例。
在本系列的下一篇文章中,我们将调换角色并让Python调用Go服务。