Python和Go:第一部分-gRPC

原文地址: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-getbrew…)安装编译器。

安装编译器后,还需要安装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.protocgrpc_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服务。

你可能感兴趣的:(Python和Go:第一部分-gRPC)