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-toolsnumpy。好的做法是将此文件置于源代码管理中,并始终对依赖项(例如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服务。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342