利用gRPC构建Python微服务

微服务架构目前仍然是一种主要的开发方式,笔者从2010年开始接触OpenStack,对OpenStack架构略有了解,于是在后期的产品研发中,沿用这一框架。但是随着项目的进行,也逐步发现了该框架中在实际应用中的缺陷,以及入门成本比较陡峭的问题。所以想结合gRPC思路,来分析一下是否有对现有框架优化的可能性。

本文大部分内容出自:https://realpython.com/python-microservices-grpc/,结合了笔者个人经验进行了整理,并非完全的翻译。国内很多网站翻译的版本并非完整版,此篇几乎完整的还原了原文,请关注老孙正经胡说网站关注后续更新。

在完成了通篇的翻译工作后,不禁为作者的细致程度点赞,细节程度“令人发指”,这种匠人精神是值得国内的技术人员学习的。

全文导航

为了方便大家阅读,这里将全部目录进行一下索引,方便大家在老孙正经胡说(https://sunqi.site)中查看相关文章:

前言

微服务架构反映了开发人员在技术迭代中最朴素的愿景,即不把鸡蛋放在一个篮子中。解决随着项目发展,所有代码放在一个项目中时,开发运维成本陡增的问题。同时也提高应用可扩展性,尽可能的降低由于需求的增加而对原有架构重构的风险。

通过该教程,你可以学会:

  • 使用Python构建微服务模块,并且通过grpc进行通讯
  • 实现中间层(middleware),实现对其他微服务监控
  • 如何对中间层和微服务模块进行单元测试和完整性测试
  • 将微服务模块部署在Kubernetes集群中

为什么要使用微服务(Microservices)?

假设你要开发一个在线销售图书的网站,你的团队里有几百人,他们都在实现某部分具体的功能,例如管理购物车、推荐、处理交易等功能。

如果按照传统方式实现,将所有的代码写入一个巨大的项目中,所有的研发人员需要耗费时间理解全部代码。同时由于代码间的关联性,测试的时间成本也会被拉长,在版本控制上,代码与数据库结构的一致性也成了管理和开发上的难题。最重要的一点,这样的架构并不适用于快速变化的需求,开发一个新功能的周期将成倍的增加。

基于以上在传统开发中的难点,就应运而生了微服务的理念。将原有模块化开发中的功能模块,拆分成一个一个微服务,微服务通过统一的接口对外通讯,模块内部的变化不会影响其他模块。在示例应用中,可以将购物车,物品清单中等都作为一个微服务模块进行拆分。

模块化开发

假如产品经理需要实现一个功能:图书买二赠一。那么作为研发人员,可以增加一个检查购物车的逻辑,发现购物车有两本以上时,直接减去最便宜的一本书的价格即可实现该功能。

接着又出现了一个新需求,需要对这一活动的效果进行跟踪,由于之前的逻辑是在购物车实现的,开发人员需要在结账时,更新一下关系型数据库中的字段

buy_two_get_one_free_promo = true

接下来,产品经理告知你,该优惠活动每个用户只能享受一次,所以呢你需要在显示页面的时候检查你上面设置的标记,如果用户已经使用过该优惠,则需要隐藏掉宣传的Banner,如果没使用过则需要发邮件告知用户。

随着时间推移,关系型数据库数据量越来越大,所以希望更换共享型数据库(shared database),但是由于这样的标记太多,导致业务逻辑和数据库之间的羁绊越来越严重。这就是为什么不在一个项目中实现所有代码的原因,从长期角度来看,边界非常重要。

交易数据库只能被交易微服务所访问,其他服务访问时都应该通过抽象的接口实现,这样可以控制代码变更后的”爆炸半径“。

灵活性

微服务为代码构建提供了灵活性,一方面每一个微服务模块的开发语言不受限制,另外一方面你还可以灵活的扩展微服务模块。根据应用的特性,你可以将微服务模块运行在不同的硬件上。

稳定性

单点瓶颈问题是单体应用面临稳定性最大的挑战。另外,由于单体应用代码变更而引起的“爆炸半径”往往不可控。

所有权

在大型单体项目中,由于每个人对于整体架构理解不同,所以在实现过程中需要有经验的人进行严格的代码检查,这样让迭代速度变慢。

微服务架构让每个开发工程师只需要关注自己的代码部分的架构,这也降低了增加新功能时对其他模块产生影响的风险。

如何划分“微”服务?

这其实是一个极具争议性的话题,就像Restful一样,并非是一种协议规范,只是一种风格、一种建议。关于这一话题有太多争议性的讨论,甚至有文章说”微服务已死“的论调。其实我们不必纠结于此,“一千个人心中有一千个哈姆雷特”,从结果导向来讲,“黑猫白猫能抓到老鼠就是好猫”。从原文作者角度看,“微”是一种不恰当的命名,而我们重点关注的是应该放在“服务”上。

所以在设计微服务时有以下几点建议:

  • 微服务太小会破坏代码的模块化,微服务中的代码实现是有价值的,这类似类的数据和方法之间的关系。所以微服务的范围要适当,不要太大也不要太小,这也是为什么上面提到微服务的拆分因人而异,没有绝对的对与错
  • 微服务的开发和测试其实比单体应用更难,因为如果一个开发人员想要开发一个模块,必须要知道如何启动其他模块,这就对CI/CD流程提出了更高的要求,几个微服务模块手动可以启动,但是如果是十几个,则相当耗时耗力
  • 微服务的设计是一门艺术,也要结合团队的实际情况,如果你团队有5个人,但是微服务模块有20个,这是相当危险的;如果你的团队负责一个微服务模块,却被其他五个团队共享使用,这也可能会导致问题
  • 微服务在拆分时,不要意味的追求“微”,某些微服务模块可能会很大,但是也要注意,如果一个微服务模块做了两件或以上不相关的事情,也说明那个功能实现并不属于这个模块

这是对于在线书店的微服务模块拆分:

  • Marketplace:用户访问网站的逻辑
  • Cart:用户购物车及购买流程
  • Transactions:付款流程和发送收据
  • Inventroy:库存管理
  • User Account:用户登陆注册、密码修改等
  • Reviews:评价管理

这只是一些例子,并不是全部,每个团队的逻辑相对独立,这样更容易控制“爆炸半径”,例如评价系统出现问题,并不会影响购买流程。

微服务架构和单体架构的平衡

在产品研发初期,选择单体架构能够更快速的添加各种功能,快速完成产品。但是随着开发的进行,产品逐渐变得越来越臃肿。使用Python构建微服务架构,短期内会消耗你一定的精力,但是具备很好的扩展性。

典型的硅谷创业周期都是从单体开始,以便企业能够快速迭代。公司可以雇佣更多的工程师后,可以考虑使用微服务架构,但是要注意选择合适的时间点。

关于微服务架构与单体架构的平衡请参考《什么时候开始使用微服务架构》(https://www.youtube.com/watch?v=GBTdnfD6s5Q

微服务示例

示例需求:

  • 定义一个API接口,实现微服务
  • 定义两个微服务:
    • Marketplace:简单的web程序,向用户展示图书列表
    • Recommendations:显示用户可能喜爱的图书的列表

下图展示了服务之间的通讯关系:

在这里插入图片描述

用户通过浏览器访问Marketplace微服务,Marketplace微服务将和Recommendations进行通讯。

Recommendations API应该包含以下功能:

  • User ID:用户个性化推荐标识,为了简单起见,样例是随机生成的
  • Book category:让API增加一些趣味性,添加图书的类目
  • Max results:最大推荐数量

返回的结果是一个列表,其中包含:

  • Book ID:图书唯一标识
  • Boot title:图书名称

我们使用**protocol buffers正式定义API接口,**这个protocol buffers中声明了你的API。Protocol buffers是Google开发的,提供了一种正式的API接口规范。这看起来有点神秘,以下注释部分是对每一行的详细描述:

# 定义了该文件使用proto3协议取代旧的proto2版本
syntax = "proto3";

# 定义图书类目
enum BookCategory {
   
    MYSTERY = 0;
    SCIENCE_FICTION = 1;
    SELF_HELP = 2;
}

# 定义API请求,user_id和max_results使用了int32类型,而category使用了上面定义的BookCategory类型,可以暂时忽略
message RecommendationRequest {
   
    int32 user_id = 1;
    BookCategory category = 2;
    int32 max_results = 3;
}

# 定义图书推荐类型
message BookRecommendation {
   
    int32 id = 1;
    string title = 2;
}

# 定义了微服务响应,replated关键字代表返回的是BookRecommendation的类型的列表
message RecommendationResponse {
   
    repeated BookRecommendation recommendations = 1;
}

# 可以看做是一个函数,输入为RecommendationRequest,输出为RecommendationResponse
service Recommendations {
   
    rpc Recommend (RecommendationRequest) returns (RecommendationResponse);
}

这里rpc就是远程调用(remote procedure call),类似本地调用函数,但实际可能是在远程服务器运行该函数。

为什么是RPC和Protocol Buffers?

为什么使用这种方式来定义API接口呢?如果你想让一个微服务调用另外一个微服务,最简单的方法是通过HTTP调用,并返回一个Json类型的字符串。你可以使用这种方式,但是使用Protocol Buffers则更有优势。

文档

首先,使用protocol buffers可以让你使用更优雅并且自定义样式的方式定义API。如果使用Json,则你需要在文档中记录包含的字段及其类型。与任何文档一样,你可能面临不准确,或者文档未及时更新的风险。

当你使用protocol buffers进行API定义时,从中生成Python代码。你的代码永远不会和文档不一致,文档是好的,但是代码中的自我文档是更好的方式。

验证

第二点优势,是自动基于类型定义的验证。例如:生成的代码不会接受错误的类型。生成的代码还内置了所有RPC样板文件。

当你使用HTTP和Json构建API时,你需要写一些代码实现请求、发送、校验返回结果等。使用protocol buffers你就像调用本地函数一样,但底层实际上是一个网络调用。

你可以使用基于HTTP和JSON框架的Swagger和RAML获得同样的优势,具体信息可以查阅其他文档。

那么是否有充足的理由使用grpc,而不选择其他的呢?答案仍然是肯定的。

性能

gRPC框架是比传统HTTP请求效率更高。gRPC是在HTTP/2基础上构建的,能够在一个长连接上使用现成安全的方式进行多次并发请求。连接创建相对较慢,所以在多次请求时,一次连接并共享连接以节省时间。gRPC信息是二进制的,并且小于JSON。未来,HTTP/2会提供内置的头部压缩。

gRPC内置了对流式请求和返回的支持。比基础HTTP连接更好的管理网络问题,即使是在长时间断线后,也会自动重连。他还有连接器,你会在本向导后面学习到。你甚至可以实现插件,已经有人做出了这样的Python库。最根本的,你可以完全免费试用这个伟大的架构。

对开发人员友好

相较于REST,很多开发人员更喜欢gRPC,这其中的原因是,你不需要像REST一样使用动词或者资源来定义功能,而是直接使用函数方式。作为一名开发人员,更习惯的是函数思考方式,这也是grpcapi的定义方式。

实现REST API定义功能通常比较麻烦,你首先要确定资源,构造路径,以及使用哪个动词。通常有多个选择,如何嵌套资源,如何使用POST或其他动词。REST和gRPC孰优孰劣又会是一个争论的焦点,但是正如之前所说的,没有最好的技术,只有最适合需求场景的技术方案。

严格来说,protocol buffers是将数据通过序列化方式在两个微服务之间传输。所以,protocol buffers和JSON或XML都是相似的方式组织数据。不同的是,protocol buffers拥有更严格的格式和更压缩的方式在网络上通讯。

另外一方面,RPC架构应该被称为gRPC或者Google RPC。这很像HTTP。但正如上述所言,gRPC实际上是基于HTTP/2构建的。

代码样例

现在正式进入实现部分,我们来看看protoco buffers能做什么?protobufs是简称,后续会大量出现。

正如上面提到的,你可以从protobufs生成Python代码。这个工作是作为grpcio-tools包的一部分。

首先,定义你的初始目录结构:

.
├── protobufs/
│   └── recommendations.proto
|
└── recommendations/

protobufs包含recommendations.proto文件,这是我们上面定义的部分。

你将在recommendations目录生成Python代码。首先,你需要安装grpcio-tools,创建recommendations/requirements.txt

grpcio-tools ~= 1.30

可以创建一个virtualenv环境来运行后续代码,在虚拟环境中安装依赖。

virtualenv venv
source venv/bin/activate
python -m pip install -r requirements.txt

从protobufs生成代码

$ cd recommendations
$ python -m grpc_tools.protoc -I ../protobufs --python_out=. \
         --grpc_python_out=. ../protobufs/recommendations.proto

此处将生成两个文件

$ ls
recommendations_pb2.py recommendations_pb2_grpc.py

这些文件包含与API通讯的Python类型和函数。编译器生成客户端代码并调用RPC和Server端代码实现远程调用。我们先来看一下客户端代码部分。

RPC客户端

这里生成的代码可读性并不高

>>> from recommendations_pb2 import BookCategory, RecommendationRequest
>>> request = RecommendationRequest(
...     user_id=1, category=BookCategory.SCIENCE_FICTION, max_results=3
... )
>>> request.category
1

protobuf编译器生成了与你的protobuf类型对应的 Python 类型。到目前为止,一切顺利。你还可以看到一些类型的检查字段:

>>> request = RecommendationRequest(
...     user_id="oops", category=BookCategory.SCIENCE_FICTION, max_results=3
... )
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'oops' has type str, but expected one of: int, long

如果传入错误字段类型,则抛出TypeError。

在proto3中所有字段是可选的,所以你要校验是否正确设置所有值。如果其中一个未提供,如果为数值类型则默认为0,如果是字符类型默认为空:

>>> request = RecommendationRequest(
...     user_id=1, category=BookCategory.SCIENCE_FICTION
... )
>>> request.max_results
0

因为没有设置int字段的默认值,所以返回0。

虽然protobufs为你进行了初步检查,但是仍然需要从自身业务角度验证数据的有效性,其实无论使用何种实现方式,这都是必须要做的。

生成的recommendations_pb2.py文件包含了类型定义,而recommendations_pb2_grpc.py文件则包含了客户端和服务端基本通讯框架。以下就是client在使用时需要的引入:

>>> import grpc
>>> from recommendations_pb2_grpc import RecommendationsStub

引入grpc是为了设置与远程服务的链接。之后导入RPC客户端stub,之所以叫做stub是因为客户端本身并不包含任何功能。只是调用远程服务并传回结果。

如果你查看protobuf的定义,你会看到在service Recommendations的定义最后。protobuf的编译器使用Recommendations,并且追加从客户端的名称Stub,就构成了RecommendationStub

service Recommendations {
   
    rpc Recommend (RecommendationRequest) returns (RecommendationResponse);
}

现在你可以发送RPC请求了

>>> channel = grpc.insecure_channel("localhost:50051")
>>> client = RecommendationsStub(channel)
>>> request = RecommendationRequest(
...     user_id=1, category=BookCategory.SCIENCE_FICTION, max_results=3
... )
>>>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
本门课程是带大家进入微服务领域入门的课程,很适合新手小白学习的课程。1: 什么是rpc?RPC(Remote Procedure Call)是函数对函数的远程调用,传输协议tcp,http,主要是基于xml,json序列化协议(这里的序列化协议是对数据编解码的方式),项目中我们真正用到的是grpcgrpc是一个框架,基于http2.0的长链接,性能有所改进,重要的是grpc用的是Google开源的protobuf序列化协议,它比json,xml性能更快,在压缩数据方面也更小。总之我们选择grpc最主要的有两点:1:支持跨语言开发(如python,golang,java)2:  grpc首先是一个框架,封装rpc,让程序员只关注代码逻辑即可 2: 为什么用grpc,而不用flask,django,tornado,即http协议?1:http的调用是根据url的(即restful),它跟rpc的调用最大的区别就是这里,rpc的调用,你就像调用一个本地函数一样简单,而且微服务,分布式也是从rpc开始的,学好rpc对以后做好分布式会更有帮助,其实go语言和python语言里都有rpc(如xmlrpc,jsonrpc,zerorpc),我们之所以学习go语言里的rpc是因为go语言的rpc相对更加灵活,go语言本身也支持高并发,这一点对于分布式来说更好。2:其次http协议,用过flask框架的人都知道,请求一次数据后就断开,而grpc基于http2.0,它不但可以保持长链接,传输效率也更高,使用方面,因为http2.0相当于tcp一样使用,现在很多大厂也都开始用http2.0了。http2.0相比http的优势很明显,头部压缩,分流,针对tcp的多路复用。所以基于http2.0的grpc无论从生态和性能方面都更好。 这张图是一个gRPC相关的架构图,同学们可以通过这张图了解gRPC在项目中的使用
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值