93 KiB
使用 gRPC 的 Python 微服务
微服务是一种组织复杂软件系统的方式。你不是把你所有的代码都放在一个 app 里,而是把你的 app 分解成独立部署的微服务,互相通信。本教程将教您如何使用 gRPC(最流行的框架之一)启动和运行 Python 微服务。
很好地实现微服务框架很重要。当您构建一个框架来支持关键应用程序时,您必须确保它是健壮的和对开发人员友好的。在本教程中,您将学习如何做到这一点。这些知识会让你对成长中的公司更有价值。
为了从本教程中获益最大,你应该理解 Python 和 web 应用的基础。如果你想重温这些,请先通读提供的链接。
本教程结束时,你将能够:
- 用 Python 实现通过 gRPC 相互通信的微服务
- 实现中间件来监控微服务
- 单元测试和集成测试你的微服务和中间件
- 使用 Kubernetes 将微服务部署到 Python 生产环境中
您可以通过单击下面的链接下载本教程中使用的所有源代码:
获取源代码: 单击此处获取源代码,您将在本教程中使用了解如何使用 gRPC 创建 Python 微服务。
为什么选择微服务?
假设您在 Online Books For You 工作,这是一家流行的在线销售图书的电子商务网站。该公司有数百名开发人员。每个开发人员都在为一些产品或后端功能编写代码,比如管理用户的购物车、生成推荐、处理支付交易或处理仓库库存。
现在问问你自己,你希望所有的代码都在一个巨大的应用程序中吗?这有多难理解?测试需要多长时间?你如何保持代码和数据库模式的合理性?这肯定会很难,尤其是在业务试图快速发展的时候。
难道你不希望对应于模块化产品特性的代码是模块化的吗?管理购物车的购物车微服务。管理库存的库存微服务。
在下面的章节中,您将会更深入地探究将 Python 代码分离到微服务中的一些原因。
模块化
代码更改通常会选择阻力最小的路径。你心爱的在线图书为你 CEO 想增加一个新的买两本书送一本书的功能。你是被要求尽快启动它的团队的一员。看看当所有代码都在一个应用程序中时会发生什么。
作为您团队中最聪明的工程师,您提到您可以向购物车逻辑添加一些代码,以检查购物车中是否有两本书以上。如果是这样,你可以简单地从购物车总额中减去最便宜的书的价格。没问题——你提出一个拉取请求。
然后你的产品经理说你需要跟踪这个活动对图书销售的影响。这也很简单。因为实现买二送一功能的逻辑在购物车代码中,所以您将在 checkout 流中添加一行来更新交易数据库中的一个新列,以表明该销售是促销的一部分:buy_two_get_one_free_promo = true。完成了。
接下来,您的产品经理会提醒您,该交易仅对每位客户使用一次有效。您需要添加一些逻辑来检查之前的事务是否设置了那个buy_two_get_one_free_promo标志。哦,你需要隐藏主页上的推广横幅,所以你也添加了检查。哦,你还需要发送电子邮件给没有使用过该宣传片的人。把那个也加上。
几年后,事务数据库变得太大,需要用新的共享数据库替换。所有这些参考资料都需要修改。不幸的是,在这一点上,整个代码库都引用了这个数据库。你认为添加所有这些引用实际上有点太容易了。
这就是为什么从长远来看,将所有代码放在一个应用程序中是危险的。有时候有界限是好事。
事务数据库应该只能由事务微服务访问。然后,如果你需要缩放它,它不是那么糟糕。代码的其他部分可以通过隐藏实现细节的抽象 API 与事务进行交互。你可以在一个应用程序中做到这一点——只是不太可能。代码更改通常会选择阻力最小的路径。
灵活性
将 Python 代码拆分成微服务可以给你更多的灵活性。首先,你可以用不同的语言编写你的微服务。通常,一个公司的第一个网络应用程序会用 Ruby 或者 T2 PHP 编写。这并不意味着其他一切都必须如此!
您还可以独立扩展每个微服务。在本教程中,您将使用一个 web 应用程序和一个建议微服务作为运行示例。
你的网络应用很可能会受到 I/O 的限制,从数据库中获取数据,或许从磁盘中加载模板或其他文件。一个推荐微服务可能正在做大量的数字运算,使其受到 CPU 的限制。在不同的硬件上运行这两个 Python 微服务是有意义的。
鲁棒性
如果你所有的代码都在一个应用程序中,那么你必须立刻部署它。这是很大的风险!这意味着对一小部分代码的修改可能会毁掉整个网站。
所有权
当一个单一的代码库被许多人共享时,对于代码的架构通常没有清晰的愿景。在员工来来去去的大公司尤其如此。可能有人对代码应该是什么样子有一个愿景,但是当任何人都可以修改它并且每个人都在快速移动时,很难执行。
微服务的一个好处是团队可以清楚地拥有他们的代码。这使得代码更有可能有一个清晰的愿景,代码也更有条理。它还清楚地表明,当出现问题时,谁负责向代码添加特性或进行更改。
“微”有多小?
微服务应该有多小是工程师们激烈争论的话题之一。以下是我的两点看法:微是用词不当。我们应该只说服务。然而,在本教程中,您将看到用于一致性的微服务。
把微服务做得太小会出问题。首先,它实际上违背了使代码模块化的目的。微服务中的代码应该一起有意义,就像类中的数据和方法一起有意义一样。
用类做类比,考虑 Python 中的file对象。file对象拥有您需要的所有方法。你可以用 .read()和.write() 来代替它,或者你可以用.readlines()来代替它。你不应该需要一个FileReader和一个FileWriter类。也许你熟悉这样做的语言,也许你一直认为这有点麻烦和混乱。
微服务也一样。代码的范围应该是正确的。不要太大,也不要太小。
第二,微服务比单片代码更难测试。如果开发人员想要测试一个跨越许多微服务的特性,那么他们需要在他们的开发环境中启动并运行这些特性。这增加了摩擦。对于一些微服务来说,情况还不算太糟,但如果是几十个,那么这将是一个重大问题。
获得正确的微服务规模是一门艺术。需要注意的一点是每个团队都应该拥有合理数量的微服务。如果你的团队有五个人,但是有二十个微服务,那么这是一个危险信号。另一方面,如果你的团队只开发一个微服务,并且被其他五个团队共享,那么这也可能是一个问题。
不要为了做微服务而把微服务做得越小越好。有些微服务可能比较大。但是要注意单个微服务正在做两件或更多完全不相关的事情。这通常是因为向现有微服务添加不相关的功能是阻力最小的途径,而不是因为它属于那里。
这里有一些方法可以将你假想的网上书店分解成微服务:
- Marketplace 为用户在网站中导航提供逻辑服务。
- 购物车跟踪用户放入购物车的商品和结账流程。
- 交易处理支付处理和发送收据。
- Inventory 提供关于哪些书籍有库存的数据。
- 用户帐户管理用户注册和帐户详情,例如更改他们的密码。
- 评论存储用户输入的图书评分和评论。
这些只是几个例子,并不是详尽的清单。然而,你可以看到它们中的每一个都可能被自己的团队所拥有,并且它们的逻辑是相对独立的。此外,如果 Reviews 微服务部署时出现了导致其崩溃的错误,那么用户仍然可以使用该网站并进行购买,尽管点评无法加载。
微服务与整体服务的权衡
微服务并不总是比将所有代码保存在一个应用程序中的单片要好。一般来说,尤其是在软件开发生命周期的开始,monoliths 会让你走得更快。它们使共享代码和添加功能变得不那么复杂,并且只需部署一个服务就可以让用户快速获得应用。
权衡的结果是,随着复杂性的增长,所有这些事情会逐渐使整体更难开发,部署更慢,并且更脆弱。实现一个整体可能会节省你的时间和精力,但它可能会回来困扰你。
在 Python 中实现微服务可能会在短期内耗费您的时间和精力,但如果做得好,从长远来看,它可以让您更好地扩展。当然,在速度最重要的时候,过早实施微服务可能会降低您的速度。
典型的硅谷创业周期是从一个整体开始,以便在企业找到适合客户的产品时能够快速迭代。等公司有了成功的产品,雇佣了更多的工程师之后,就该开始考虑微服务了。不要过早实施,但也不要等待太久。
要了解更多关于微服务与整体服务的权衡,请观看 Sam Newman 和 Martin Fowler 的精彩讨论,何时使用微服务(以及何时不使用!)。
微服务示例
在本节中,您将为您的在线图书网站定义一些微服务。在本教程中,您将为它们定义一个 API ,并编写 Python 代码将它们实现为微服务。
为了便于管理,您将只定义两个微服务:
- 市场将是一个非常小的网络应用程序,向用户显示图书列表。
- 推荐将是一个微服务,提供用户可能感兴趣的书籍列表。
下图显示了您的用户如何与微服务交互:
您可以看到,用户将通过他们的浏览器与 Marketplace 微服务进行交互,Marketplace 微服务将与推荐微服务进行交互。
思考一下推荐 API。您希望建议请求具有一些特征:
- 用户 ID: 您可以用它来个性化推荐。然而,为了简单起见,本教程中的所有建议都是随机的。
- **图书类别:**为了让 API 更有趣,您将添加图书类别,例如推理小说、自助书籍等等。
- Max results: 您不想退回所有库存图书,所以您将为请求添加一个限制。
响应将是一个图书列表。每本书将包含以下数据:
- 图书 ID: 图书的唯一数字 ID。
- **图书标题:**可以向用户显示的标题。
一个真实的网站会有更多的数据,但是为了这个例子,你将保持特征的数量有限。
现在,您可以更正式地定义这个 API,使用协议缓冲区的语法:
1syntax = "proto3";
2
3enum BookCategory {
4 MYSTERY = 0;
5 SCIENCE_FICTION = 1;
6 SELF_HELP = 2;
7}
8
9message RecommendationRequest {
10 int32 user_id = 1;
11 BookCategory category = 2;
12 int32 max_results = 3;
13}
14
15message BookRecommendation {
16 int32 id = 1;
17 string title = 2;
18}
19
20message RecommendationResponse {
21 repeated BookRecommendation recommendations = 1;
22}
23
24service Recommendations {
25 rpc Recommend (RecommendationRequest) returns (RecommendationResponse);
26}
这个协议缓冲区文件声明了您的 API。协议缓冲区是 Google 开发的,它提供了一种正式指定 API 的方法。这乍一看可能有点神秘,所以下面是一行一行的分析:
-
第 1 行指定文件使用
proto3语法,而不是旧的proto2版本。 -
第 3 行到第 7 行定义了你的图书类别,每个类别还分配了一个数字 ID。
-
第 9 行到第 13 行定义了您的 API 请求。一个
message包含字段,每一个都是特定的类型。对于user_ID和max_results字段,您使用的是一个 32 位整数int32。您还使用了上面定义为category类型的BookCategory枚举。除了每个字段都有名称之外,还会分配一个数字字段 ID。你可以暂时忽略这个。 -
第 15 到 18 行定义了一个新的类型,您可以用它来推荐一本书。它有一个 32 位的整数 ID 和一个基于字符串的标题。
-
第 20 行到第 22 行定义您的建议微服务响应。注意
repeated关键字,它表明响应实际上有一个BookRecommendation对象的列表。 -
第 24 到 26 行定义了 API 的方法。你可以把它想象成一个类中的函数或方法。它接受一个
RecommendationRequest并返回一个RecommendationResponse。
rpc代表远程过程调用。很快您就会看到,您可以像 Python 中的普通函数一样调用 RPC。但是 RPC 的实现在另一个服务器上执行,这就是为什么它是一个远程过程调用。
为什么是 RPC 和协议缓冲区?
好吧,那么你为什么要使用这种正式语法来定义你的 API 呢?如果要从一个微服务向另一个微服务发出请求,难道就不能发出一个 HTTP 请求得到一个 JSON 响应吗?你可以这么做,但是使用协议缓冲区也有好处。
文档
使用协议缓冲区的第一个好处是,它们为您的 API 提供了一个定义良好且自我记录的模式。如果您使用 JSON,那么您必须记录它包含的字段及其类型。与任何文档一样,您可能会面临文档不准确、不完整或过期的风险。
当您用协议缓冲区语言编写 API 时,您可以从中生成 Python 代码。您的代码永远不会与您的文档不同步。文档是好的,但是自文档代码更好。
验证
第二个好处是,当您从协议缓冲区生成 Python 代码时,您可以免费获得一些基本的验证。例如,生成的代码不会接受错误类型的字段。生成的代码还内置了所有的 RPC 样板文件。
如果您的 API 使用 HTTP 和 JSON,那么您需要编写一点代码来构造请求、发送请求、等待响应、检查状态代码,并解析和验证响应。使用协议缓冲区,您可以生成看起来像普通的函数调用,但实际上是网络请求的代码。
使用 HTTP 和 JSON 框架,比如 Swagger 和 RAML ,你也可以获得同样的好处。关于 Swagger 的一个例子,请查看带有 Flask、Connexion 和 SQLAlchemy 的Python REST API。
那么,是否有理由使用 gRPC 而不是其他替代方案呢?答案还是肯定的。
性能
gRPC 框架通常比使用典型的 HTTP 请求更有效。gRPC 构建在 HTTP/2 之上,它可以以线程安全的方式在一个长期连接上并行发出多个请求。连接设置相对较慢,因此只需设置一次并在多个请求之间共享连接就可以节省时间。gRPC 消息也是二进制的,比 JSON 小。此外,HTTP/2 具有内置的报头压缩。
gRPC 内置了对流式请求和响应的支持。它将比基本的 HTTP 连接更好地管理网络问题,即使在长时间断开连接后也能自动重新连接。它还有拦截器,你将在本教程的后面了解到。你甚至可以对生成的代码实现插件,人们已经这样做了,以输出 Python 类型提示。基本上,你可以免费获得很多很棒的基础设施!
开发者友好型
许多人喜欢 gRPC 胜过 REST 的最有趣的原因可能是,您可以根据函数来定义您的 API,而不是 HTTP 动词和资源。作为一名工程师,你习惯于从函数调用的角度思考,这正是 gRPC APIs 的样子。
将功能映射到 REST API 上通常很困难。你必须决定你的资源是什么,如何构造路径,以及使用哪些动词。通常有多种选择,比如如何嵌套资源,或者是否使用 POST 或其他动词。REST vs gRPC 可能会变成一场关于偏好的辩论。一个并不总是比另一个更好,所以使用最适合你的用例。
严格来说,协议缓冲区是指两个微服务之间发送数据的序列化格式。因此,协议缓冲区类似于 JSON 或 XML,它们是格式化数据的方式。与 JSON 不同,协议缓冲区有严格的模式,通过网络发送时更加紧凑。
另一方面,RPC 基础设施实际上被称为 gRPC ,或者 Google RPC。这更类似于 HTTP。事实上,如上所述,gRPC 是建立在 HTTP/2 之上的。
实施示例
在讨论了协议缓冲区之后,是时候看看它们能做些什么了。术语协议缓冲区很拗口,所以你会看到本教程中常用的简写协议缓冲区。
正如几次提到的,您可以从 protobufs 生成 Python 代码。该工具作为grpcio-tools包的一部分安装。
首先,定义您的初始目录结构:
.
├── protobufs/
│ └── recommendations.proto
|
└── recommendations/
protobufs/目录将包含一个名为recommendations.proto的文件。这个文件的内容就是上面的 protobuf 代码。为了方便起见,您可以通过展开下面的可折叠部分来查看代码:
1syntax = "proto3";
2
3enum BookCategory {
4 MYSTERY = 0;
5 SCIENCE_FICTION = 1;
6 SELF_HELP = 2;
7}
8
9message RecommendationRequest {
10 int32 user_id = 1;
11 BookCategory category = 2;
12 int32 max_results = 3;
13}
14
15message BookRecommendation {
16 int32 id = 1;
17 string title = 2;
18}
19
20message RecommendationResponse {
21 repeated BookRecommendation recommendations = 1;
22}
23
24service Recommendations {
25 rpc Recommend (RecommendationRequest) returns (RecommendationResponse);
26}
您将生成 Python 代码,以便在recommendations/目录中与之交互。首先你必须安装grpcio-tools。创建文件recommendations/requirements.txt,并添加以下内容:
grpcio-tools ~= 1.30
要在本地运行代码,您需要将依赖项安装到一个虚拟环境中。以下命令将在 Windows 上安装依赖项:
C:\ python -m venv venv
C:\ venv\Scripts\activate.bat
(venv) C:\ python -m pip install -r requirements.txt
在 Linux 和 macOS 上,使用以下命令创建虚拟环境并安装依赖项:
$ python3 -m venv venv
$ source venv/bin/activate # Linux/macOS only
(venv) $ python -m pip install -r requirements.txt
现在,要从 protobufs 生成 Python 代码,运行以下代码:
$ cd recommendations
$ python -m grpc_tools.protoc -I ../protobufs --python_out=. \
--grpc_python_out=. ../protobufs/recommendations.proto
这将从.proto文件生成几个 Python 文件。这里有一个细目分类:
python -m grpc_tools.protoc运行 protobuf 编译器,它将从 protobuf 代码生成 Python 代码。-I ../protobufs告诉编译器在哪里找到你的 protobuf 代码导入的文件。您实际上并没有使用 import 特性,但是仍然需要使用-I标志。--python_out=. --grpc_python_out=.告诉编译器将 Python 文件输出到哪里。很快您就会看到,它将生成两个文件,如果您愿意,您可以使用这些选项将每个文件放在一个单独的目录中。../protobufs/recommendations.proto是 protobuf 文件的路径,该文件将用于生成 Python 代码。
如果您查看生成的内容,您会看到两个文件:
$ ls
recommendations_pb2.py recommendations_pb2_grpc.py
这些文件包括 Python 类型和函数,用于与 API 进行交互。编译器将生成调用 RPC 的客户端代码和实现 RPC 的服务器代码。您将首先查看客户端。
RPC 客户端
生成的代码只有主板才会喜欢。也就是说不是很好看的 Python。这是因为它并不真正适合人类阅读。打开 Python shell,查看如何与之交互:
>>> 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
这表明,如果将错误的类型传递给 protobuf 字段之一,就会得到一个 TypeError 。
一个重要的注意事项是,proto3中的所有字段都是可选的,因此您需要验证它们是否都已设置。如果不设置,那么对于数值类型,它将默认为零,对于字符串,它将默认为空字符串:
>>> request = RecommendationRequest(
... user_id=1, category=BookCategory.SCIENCE_FICTION
... )
>>> request.max_results
0
这里您得到了0,因为这是未设置的int字段的默认值。
虽然 protobufs 会为您进行类型检查,但您仍然需要验证实际值。因此,当您实现您的建议微服务时,您应该验证所有字段都有良好的数据。对于任何服务器来说都是如此,不管您使用 protobufs、JSON 还是其他什么。始终验证输入。
为您生成的recommendations_pb2.py文件包含类型定义。recommendations_pb2_grpc.py文件包含客户端和服务器的框架。看一下创建客户机所需的导入:
>>> import grpc
>>> from recommendations_pb2_grpc import RecommendationsStub
您导入模块grpc,该模块提供了一些用于建立到远程服务器的连接的功能。然后导入 RPC 客户端存根。它被称为存根,因为客户端本身没有任何功能。它调用远程服务器并将结果传回。
如果你回头看看你的 protobuf 定义,那么你会在最后看到service Recommendations {...}部分。protobuf 编译器获取这个微服务名Recommendations,并在它后面加上Stub,形成客户端名RecommendationsStub。
现在,您可以发出 RPC 请求:
>>> channel = grpc.insecure_channel("localhost:50051")
>>> client = RecommendationsStub(channel)
>>> request = RecommendationRequest(
... user_id=1, category=BookCategory.SCIENCE_FICTION, max_results=3
... )
>>> client.Recommend(request)
Traceback (most recent call last):
...
grpc._channel._InactiveRpcError: <_InactiveRpcError of RPC that terminated with:
status = StatusCode.UNAVAILABLE
details = "failed to connect to all addresses"
...
您在端口50051上创建一个到您自己的机器localhost的连接。该端口是 gRPC 的标准端口,但是如果您愿意,您可以更改它。现在,您将使用一个不安全的通道,它未经身份验证和加密,但是您将在本教程的后面学习如何使用安全通道。然后将这个通道传递给存根来实例化您的客户机。
您现在可以调用您在Recommendations微服务上定义的Recommend方法。回想一下 protobuf 定义中的第 25 行:rpc Recommend (...) returns (...)。这就是Recommend方法的由来。您将得到一个异常,因为实际上没有微服务在localhost:50051上运行,所以接下来您将实现它!
既然已经解决了客户端的问题,接下来我们来看看服务器端。
RPC 服务器
在控制台中测试客户机是一回事,但是在那里实现服务器就有点多了。您可以让您的控制台保持打开,但是您将在一个文件中实现微服务。
从导入和一些数据开始:
1# recommendations/recommendations.py
2from concurrent import futures
3import random
4
5import grpc
6
7from recommendations_pb2 import (
8 BookCategory,
9 BookRecommendation,
10 RecommendationResponse,
11)
12import recommendations_pb2_grpc
13
14books_by_category = {
15 BookCategory.MYSTERY: [
16 BookRecommendation(id=1, title="The Maltese Falcon"),
17 BookRecommendation(id=2, title="Murder on the Orient Express"),
18 BookRecommendation(id=3, title="The Hound of the Baskervilles"),
19 ],
20 BookCategory.SCIENCE_FICTION: [
21 BookRecommendation(
22 id=4, title="The Hitchhiker's Guide to the Galaxy"
23 ),
24 BookRecommendation(id=5, title="Ender's Game"),
25 BookRecommendation(id=6, title="The Dune Chronicles"),
26 ],
27 BookCategory.SELF_HELP: [
28 BookRecommendation(
29 id=7, title="The 7 Habits of Highly Effective People"
30 ),
31 BookRecommendation(
32 id=8, title="How to Win Friends and Influence People"
33 ),
34 BookRecommendation(id=9, title="Man's Search for Meaning"),
35 ],
36}
这段代码导入您的依赖项并创建一些示例数据。这里有一个细目分类:
- 第 2 行导入
futures,因为 gRPC 需要一个线程池。你以后会明白的。 - 第 3 行导入
random,因为您将随机选择书籍进行推荐。 - 第 14 行创建
books_by_category字典,其中键为图书类别,值为该类别图书的列表。在真正的推荐微服务中,书籍将被存储在数据库中。
接下来,您将创建一个实现微服务功能的类:
29class RecommendationService(
30 recommendations_pb2_grpc.RecommendationsServicer
31):
32 def Recommend(self, request, context):
33 if request.category not in books_by_category:
34 context.abort(grpc.StatusCode.NOT_FOUND, "Category not found")
35
36 books_for_category = books_by_category[request.category]
37 num_results = min(request.max_results, len(books_for_category))
38 books_to_recommend = random.sample(
39 books_for_category, num_results
40 )
41
42 return RecommendationResponse(recommendations=books_to_recommend)
您已经创建了一个包含实现Recommend RPC 的方法的类。以下是详细情况:
-
第 29 行定义了
RecommendationService类。这就是你的微服务的实现。请注意,您子类化了RecommendationsServicer。这是您需要做的与 gRPC 集成的一部分。 -
第 32 行在你的类上定义了一个
Recommend()方法。这必须与您在 protobuf 文件中定义的 RPC 同名。它也接受一个RecommendationRequest并返回一个RecommendationResponse,就像在 protobuf 定义中一样。它还需要一个context参数。上下文允许您设置响应的状态代码。 -
第 33 行和第 34 行使用
abort()结束请求,如果您得到一个意外的类别,则将状态代码设置为NOT_FOUND。因为 gRPC 构建在 HTTP/2 之上,所以状态代码类似于标准的 HTTP 状态代码。设置它允许客户端根据它收到的代码采取不同的操作。它还允许中间件(如监控系统)记录有多少请求有错误。 -
第 36 至 40 行从给定的类别中随机挑选一些书籍推荐。请确保将推荐数量限制在
max_results以内。你使用min()来确保你不会要求比实际更多的书,否则random.sample就会出错。 -
第 38 行T3】返回一个
RecommendationResponse对象和你的书籍推荐列表。
注意,在错误条件下引发一个异常比像本例中那样使用abort()更好,但是这样的响应不会正确设置状态码。有一种方法可以解决这个问题,在本教程的后面,当您查看拦截器时,您将会看到这种方法。
RecommendationService类定义了你的微服务实现,但是你仍然需要运行它。这就是serve()的作用:
41def serve():
42 server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
43 recommendations_pb2_grpc.add_RecommendationsServicer_to_server(
44 RecommendationService(), server
45 )
46 server.add_insecure_port("[::]:50051")
47 server.start()
48 server.wait_for_termination()
49
50
51if __name__ == "__main__":
52 serve()
serve()启动网络服务器并使用您的微服务类来处理请求:
- 第 42 行创建一个 gRPC 服务器。您告诉它使用
10线程来服务请求,这对于这个演示来说完全是多余的,但是对于实际的 Python 微服务来说是一个很好的默认设置。 - 第 43 行将你的类与服务器关联起来。这就像为请求添加一个处理程序。
- 第 46 行告诉服务器在端口
50051上运行。如前所述,这是 gRPC 的标准端口,但是您可以使用任何您喜欢的端口。 - 47 和 48 线调用
server.start()和server.wait_for_termination()启动微服务,等待微服务停止。在这种情况下,停止它的唯一方法是在终端中键入Ctrl+C。在生产环境中,有更好的关闭方法,稍后您将会看到。
在不关闭用于测试客户端的终端的情况下,打开一个新的终端并运行以下命令:
$ python recommendations.py
这将运行建议微服务,以便您可以在一些实际数据上测试客户端。现在返回到您用来测试客户机的终端,这样您就可以创建通道存根了。如果您让控制台保持打开状态,那么您可以跳过导入,但这里会重复这些内容作为复习:
>>> import grpc
>>> from recommendations_pb2_grpc import RecommendationsStub
>>> channel = grpc.insecure_channel("localhost:50051")
>>> client = RecommendationsStub(channel)
现在您有了一个客户机对象,您可以发出一个请求:
>>> request = RecommendationRequest(
... user_id=1, category=BookCategory.SCIENCE_FICTION, max_results=3)
>>> client.Recommend(request)
recommendations {
id: 6
title: "The Dune Chronicles"
}
recommendations {
id: 4
title: "The Hitchhiker\'s Guide To The Galaxy"
}
recommendations {
id: 5
title: "Ender\'s Game"
}
有用!你向你的微服务发出 RPC 请求,得到了响应!请注意,您看到的输出可能会有所不同,因为推荐是随机选择的。
现在您已经实现了服务器,您可以实现 Marketplace 微服务并让它调用建议微服务。如果您愿意,现在可以关闭 Python 控制台,但让建议微服务保持运行。
把它绑在一起
为您的 Marketplace 微服务创建一个新的marketplace/目录,并在其中放入一个marketplace.py文件。您的目录树现在应该如下所示:
.
├── marketplace/
│ ├── marketplace.py
│ ├── requirements.txt
│ └── templates/
│ └── homepage.html
|
├── protobufs/
│ └── recommendations.proto
|
└── recommendations/
├── recommendations.py
├── recommendations_pb2.py
├── recommendations_pb2_grpc.py
└── requirements.txt
请注意您的微服务代码requirements.txt的新marketplace/目录和一个主页。所有这些都将在下面描述。您现在可以为它们创建空文件,以后再填充它们。
可以从微服务代码开始。Marketplace 微服务将是一个向用户显示网页的应用程序。它将调用推荐微服务来获取图书推荐,并显示在页面上。
打开marketplace/marketplace.py文件并添加以下内容:
1# marketplace/marketplace.py
2import os
3
4from flask import Flask, render_template
5import grpc
6
7from recommendations_pb2 import BookCategory, RecommendationRequest
8from recommendations_pb2_grpc import RecommendationsStub
9
10app = Flask(__name__)
11
12recommendations_host = os.getenv("RECOMMENDATIONS_HOST", "localhost")
13recommendations_channel = grpc.insecure_channel(
14 f"{recommendations_host}:50051"
15)
16recommendations_client = RecommendationsStub(recommendations_channel)
17
18
19@app.route("/")
20def render_homepage():
21 recommendations_request = RecommendationRequest(
22 user_id=1, category=BookCategory.MYSTERY, max_results=3
23 )
24 recommendations_response = recommendations_client.Recommend(
25 recommendations_request
26 )
27 return render_template(
28 "homepage.html",
29 recommendations=recommendations_response.recommendations,
30 )
您设置了 Flask,创建了一个 gRPC 客户机,并添加了一个函数来呈现主页。这里有一个细目分类:
- 第 10 行创建一个 Flask app,为用户呈现网页。
- 第 12 行到第 16 行创建您的 gRPC 通道和存根。
- 第 20 到 30 行创建当用户访问你的应用程序的主页时被调用的
render_homepage()。它返回一个从模板加载的 HTML 页面,带有三个科幻书籍推荐。
**注意:**在本例中,您将 gRPC 通道和存根创建为全局变量。通常全局变量是不允许的,但是在这种情况下,例外是允许的。
gRPC 通道保持与服务器的持久连接,以避免必须重复连接的开销。它可以处理许多同时发生的请求,并将重新建立丢失的连接。但是,如果您在每次请求之前创建一个新的通道,那么 Python 将对它进行垃圾收集,您将无法获得持久连接的大部分好处。
您希望通道保持开放,这样就不需要为每个请求重新连接到建议微服务。您可以将通道隐藏在另一个模块中,但是在这种情况下,因为您只有一个文件,所以您可以通过使用全局变量来简化事情。
打开您的marketplace/templates/目录中的homepage.html文件,并添加以下 HTML:
1<!-- homepage.html -->
2<!doctype html>
3<html lang="en">
4<head>
5 <title>Online Books For You</title>
6</head>
7<body>
8 <h1>Mystery books you may like</h1>
9 <ul>
10 {% for book in recommendations %}
11 <li>{{ book.title }}</li>
12 {% endfor %}
13 </ul>
14</body>
这只是一个演示主页。当你完成后,它会显示一个书籍推荐列表。
要运行这段代码,您需要以下依赖项,您可以将它们添加到marketplace/requirements.txt:
flask ~= 1.1
grpcio-tools ~= 1.30
Jinja2 ~= 2.11
pytest ~= 5.4
推荐和市场微服务都有自己的requirements.txt,但是为了方便起见,在本教程中,您可以为两者使用相同的虚拟环境。运行以下命令来更新您的虚拟环境:
$ python -m pip install -r marketplace/requirements.txt
既然已经安装了依赖项,那么还需要在marketplace/目录中为 protobufs 生成代码。为此,请在控制台中运行以下命令:
$ cd marketplace
$ python -m grpc_tools.protoc -I ../protobufs --python_out=. \
--grpc_python_out=. ../protobufs/recommendations.proto
这与您之前运行的命令相同,因此这里没有什么新内容。在marketplace/和recommendations/目录中有相同的文件可能会感觉奇怪,但是稍后您将看到如何在部署中自动生成这些文件。您通常不会将它们存储在像 Git 这样的版本控制系统中。
要运行 Marketplace 微服务,请在控制台中输入以下内容:
$ FLASK_APP=marketplace.py flask run
现在,您应该在两个独立的控制台中运行推荐和市场微服务。如果您关闭了建议微服务,请使用以下命令在另一个控制台中重新启动它:
$ cd recommendations
$ python recommendations.py
这将运行您的 Flask 应用程序,该应用程序默认运行在端口5000上。继续,在浏览器中打开它并检查它:
您现在有两个微服务在相互对话!但是它们仍然只是在你的开发机器上。接下来,您将学习如何将它们应用到生产环境中。
你可以在运行 Python 微服务的终端输入 Ctrl + C 来停止它们。接下来您将在 Docker 中运行这些,这就是它们在生产环境中的运行方式。
生产就绪 Python 微服务
此时,您的开发机器上运行着一个 Python 微服务架构,这对于测试来说非常好。在本节中,您将让它在云中运行。
码头工人
Docker 是一项了不起的技术,可以让你将一组进程与同一台机器上的其他进程隔离开来。您可以有两组或更多组进程,它们有自己的文件系统、网络端口等等。你可以把它想象成一个 Python 虚拟环境,但对整个系统来说更安全。
Docker 非常适合部署 Python 微服务,因为您可以打包所有依赖项,并在隔离的环境中运行微服务。当你将你的微服务部署到云中时,它可以和其他微服务运行在同一台机器上,而不会互相影响。这允许更好的资源利用。
本教程不会深入介绍 Docker,因为它需要一整本书来介绍。相反,您只需了解将 Python 微服务部署到云中所需的基础知识。关于 Docker 的更多信息,可以查看 Python Docker 教程。
在开始之前,如果你想在你的机器上继续,那么确保你已经安装了 Docker。可以从官网下载。
您将创建两个 Docker 图像,一个用于市场微服务,另一个用于推荐微服务。映像基本上是一个文件系统加上一些元数据。本质上,你的每个微服务都有一个迷你 Linux 环境。它可以在不影响实际文件系统的情况下写入文件,并在不与其他进程冲突的情况下打开端口。
要创建您的图像,您需要定义一个Dockerfile。你总是从一个有一些基本东西的基础图像开始。在这种情况下,您的基本映像将包含一个 Python 解释器。然后将文件从开发机器复制到 Docker 映像中。您还可以在 Docker 映像中运行命令。这对于安装依赖项很有用。
推荐Dockerfile
您将从创建建议微服务 Docker 映像开始。创建recommendations/Dockerfile并添加以下内容:
1FROM python
2
3RUN mkdir /service
4COPY protobufs/ /service/protobufs/
5COPY recommendations/ /service/recommendations/
6WORKDIR /service/recommendations
7RUN python -m pip install --upgrade pip
8RUN python -m pip install -r requirements.txt
9RUN python -m grpc_tools.protoc -I ../protobufs --python_out=. \
10 --grpc_python_out=. ../protobufs/recommendations.proto
11
12EXPOSE 50051
13ENTRYPOINT [ "python", "recommendations.py" ]
下面是一行一行的演练:
-
第 1 行用一个基本的 Linux 环境加上最新版本的 Python 来初始化你的映像。此时,您的映像具有典型的 Linux 文件系统布局。如果你往里面看,它会有
/bin、/home,以及你所期望的所有基本文件。 -
第 3 行在
/service创建一个新目录来包含你的微服务代码。 -
第 4 行和第 5 行将
protobufs/和recommendations/目录复制到/service中。 -
第 6 行给 Docker 一个
WORKDIR /service/recommendations指令,有点像在图像内部做一个cd。你给 Docker 的任何路径都将相对于这个位置,当你运行一个命令时,它将在这个目录中运行。 -
第 7 行更新
pip以避免关于旧版本的警告。 -
第 8 行告诉 Docker 在图像内部运行
pip install -r requirements.txt。这将把所有的grpcio-tools文件以及您可能添加的任何其他包添加到映像中。请注意,您没有使用虚拟环境,因为这是不必要的。在这个映像中唯一运行的将是你的微服务,所以你不需要进一步隔离它的环境。 -
第 9 行运行
python -m grpc_tools.protoc命令从 protobuf 文件生成 Python 文件。映像中的/service目录现在看起来像这样:/service/ | ├── protobufs/ │ └── recommendations.proto | └── recommendations/ ├── recommendations.py ├── recommendations_pb2.py ├── recommendations_pb2_grpc.py └── requirements.txt` -
第 12 行告诉 Docker,您将在端口
50051上运行一个微服务,并且您想要在映像之外公开它。 -
第 13 行告诉 Docker 如何运行你的微服务。
现在您可以从您的Dockerfile生成一个 Docker 图像。从包含所有代码的目录中运行下面的命令——不是在recommendations/目录中,而是在该目录的上一级:
$ docker build . -f recommendations/Dockerfile -t recommendations
这将为建议微服务构建 Docker 映像。当 Docker 构建图像时,您应该会看到一些输出。现在您可以运行它了:
$ docker run -p 127.0.0.1:50051:50051/tcp recommendations
您不会看到任何输出,但是您的推荐微服务现在正在 Docker 容器中运行。当你运行一个图像时,你得到一个容器。您可以多次运行该图像以获得多个容器,但是仍然只有一个图像。
-p 127.0.0.1:50051:50051/tcp选项告诉 Docker 将机器上端口50051上的 TCP 连接转发到容器内的端口50051。这为您提供了在机器上转发不同端口的灵活性。
例如,如果您正在运行两个容器,它们都在端口50051上运行 Python 微服务,那么您将需要在您的主机上使用两个不同的端口。这是因为两个进程不能同时打开同一个端口,除非它们在不同的容器中。
市场Dockerfile
接下来,你将建立你的市场形象。创建marketplace/Dockerfile并添加以下内容:
1FROM python
2
3RUN mkdir /service
4COPY protobufs/ /service/protobufs/
5COPY marketplace/ /service/marketplace/
6WORKDIR /service/marketplace
7RUN python -m pip install --upgrade pip
8RUN python -m pip install -r requirements.txt
9RUN python -m grpc_tools.protoc -I ../protobufs --python_out=. \
10 --grpc_python_out=. ../protobufs/recommendations.proto
11
12EXPOSE 5000
13ENV FLASK_APP=marketplace.py
14ENTRYPOINT [ "flask", "run", "--host=0.0.0.0"]
这与建议Dockerfile非常相似,但有几点不同:
- 第 13 行使用
ENV FLASK_APP=marketplace.py设置图像内部的环境变量FLASK_APP。弗拉斯克需要这个来运行。 - 第 14 行将
--host=0.0.0.0添加到flask run命令中。如果不添加这个,那么 Flask 将只接受来自本地主机的连接。
但是等等,你不是还在localhost上运行一切吗?不完全是。当您运行 Docker 容器时,默认情况下它与您的主机是隔离的。集装箱内的localhost与集装箱外的localhost不同,即使在同一台机器上。这就是为什么你需要告诉 Flask 接受来自任何地方的连接。
去打开一个新的终端。您可以使用以下命令构建您的市场形象:
$ docker build . -f marketplace/Dockerfile -t marketplace
这创造了市场形象。现在,您可以使用以下命令在容器中运行它:
$ docker run -p 127.0.0.1:5000:5000/tcp marketplace
您不会看到任何输出,但是您的 Marketplace 微服务现在正在运行。
联网
不幸的是,即使你的推荐和市场容器都在运行,如果你现在在浏览器中进入http://localhost:5000,你会得到一个错误。您可以连接到您的 Marketplace 微服务,但它无法再连接到建议微服务。容器是隔离的。
幸运的是,Docker 提供了一个解决方案。您可以创建一个虚拟网络,并将两个容器都添加到其中。您还可以为他们提供 DNS 名称,以便他们可以找到彼此。
下面,您将创建一个名为microservices的网络,并在其上运行建议微服务。您还将为其指定 DNS 名称recommendations。首先,用 Ctrl + C 停止当前运行的容器。然后运行以下命令:
$ docker network create microservices
$ docker run -p 127.0.0.1:50051:50051/tcp --network microservices \
--name recommendations recommendations
docker network create命令创建网络。你只需要这样做一次,然后你可以连接多个容器。然后将‑‑network microservices添加到docker run命令中,以启动这个网络上的容器。‑‑name recommendations选项赋予它 DNS 名称recommendations。
在重启 marketplace 容器之前,您需要更改代码。这是因为您在来自marketplace.py的这一行中硬编码了localhost:50051:
recommendations_channel = grpc.insecure_channel("localhost:50051")
现在你想连接到recommendations:50051而不是。但是,您可以从环境变量中加载它,而不是再次硬编码。用下面的两行替换上面的行:
recommendations_host = os.getenv("RECOMMENDATIONS_HOST", "localhost")
recommendations_channel = grpc.insecure_channel(
f"{recommendations_host}:50051"
)
这将在环境变量RECOMMENDATIONS_HOST中加载建议微服务的主机名。如果没有设置,那么可以默认为localhost。这允许您直接在机器上或在容器中运行相同的代码。
由于更改了代码,您需要重新构建市场映像。然后尝试在您的网络上运行它:
$ docker build . -f marketplace/Dockerfile -t marketplace
$ docker run -p 127.0.0.1:5000:5000/tcp --network microservices \
-e RECOMMENDATIONS_HOST=recommendations marketplace
这与您之前运行它的方式相似,但是有两个不同之处:
-
您添加了
‑‑network microservices选项,以便在与您的建议微服务相同的网络上运行它。您没有添加‑‑name选项,因为与推荐微服务不同,不需要查找市场微服务的 IP 地址。-p 127.0.0.1:5000:5000/tcp提供的端口转发就够了,不需要 DNS 名称。 -
您添加了
-e RECOMMENDATIONS_HOST=recommendations,它在容器内部设置环境变量。这就是将建议微服务的主机名传递给代码的方式。
此时,您可以在浏览器中再次尝试localhost:5000,它应该会正确加载。万岁。
坞站组成〔t0〕
你可以用 Docker 做到这一切,这很神奇,但是有点繁琐。如果有一个命令可以启动所有的容器,那就太好了。幸好有!它叫做docker-compose,是 Docker 项目的一部分。
您可以在 YAML 文件中声明您的微服务,而不是运行一堆命令来构建映像、创建网络和运行容器:
1version: "3.8" 2services: 3
4 marketplace: 5 build: 6 context: . 7 dockerfile: marketplace/Dockerfile 8 environment: 9 RECOMMENDATIONS_HOST: recommendations 10 image: marketplace 11 networks: 12 - microservices 13 ports: 14 - 5000:5000 15
16 recommendations: 17 build: 18 context: . 19 dockerfile: recommendations/Dockerfile 20 image: recommendations 21 networks: 22 - microservices 23
24networks: 25 microservices:
通常,您将它放入一个名为docker-compose.yaml的文件中。将它放在项目的根目录中:
.
├── marketplace/
│ ├── marketplace.py
│ ├── requirements.txt
│ └── templates/
│ └── homepage.html
|
├── protobufs/
│ └── recommendations.proto
|
├── recommendations/
│ ├── recommendations.py
│ ├── recommendations_pb2.py
│ ├── recommendations_pb2_grpc.py
│ └── requirements.txt
│
└── docker-compose.yaml
本教程不会涉及太多语法细节,因为它在其他地方有很好的文档。它实际上只是做了你已经手动做过的同样的事情。然而,现在您只需要运行一个命令来启动您的网络和容器:
$ docker-compose up
一旦运行完毕,你应该能够在浏览器中再次打开localhost:5000,并且一切都应该运行良好。
请注意,当recommendations容器与 Marketplace 微服务在同一个网络中时,您不需要在容器中公开50051,因此您可以删除这一部分。
**注意:**用docker-compose开发时,如果你更改了任何一个文件,那么运行docker-compose build来重建镜像。如果你运行docker-compose up,它将使用旧的图像,这可能会令人困惑。
如果您想在向上移动之前停止docker-compose进行一些编辑,请按 Ctrl + C 。
测试
为了单元测试你的 Python 微服务,你可以实例化你的微服务类并调用它的方法。这里有一个针对您的RecommendationService实现的基本示例测试:
1# recommendations/recommendations_test.py
2from recommendations import RecommendationService
3
4from recommendations_pb2 import BookCategory, RecommendationRequest
5
6def test_recommendations():
7 service = RecommendationService()
8 request = RecommendationRequest(
9 user_id=1, category=BookCategory.MYSTERY, max_results=1
10 )
11 response = service.Recommend(request, None)
12 assert len(response.recommendations) == 1
这里有一个细目分类:
- 第 6 行像实例化其他类一样实例化该类,并在其上调用方法。
- 第 11 行为上下文传递
None,只要不使用就行。如果您想测试使用上下文的代码路径,那么您可以模拟它。
集成测试包括使用多个未模拟出来的微服务运行自动化测试。所以这有点复杂,但并不难。添加一个marketplace/marketplace_integration_test.py文件:
from urllib.request import urlopen
def test_render_homepage():
homepage_html = urlopen("http://localhost:5000").read().decode("utf-8")
assert "<title>Online Books For You</title>" in homepage_html
assert homepage_html.count("<li>") == 3
这会向主页 URL 发出一个 HTTP 请求,并检查它是否返回了一些带有标题和三个<li>项目符号元素的 HTML。这不是最大的测试,因为如果页面上有更多的内容,它就不太容易维护,但是它证明了一点。只有当建议微服务启动并运行时,此测试才会通过。您甚至可以通过向 Marketplace 微服务发出 HTTP 请求来测试它。
那么,如何运行这种类型的测试呢?幸运的是,Docker 的好心人也提供了这样的方法。一旦使用docker-compose运行 Python 微服务,就可以使用docker-compose exec在其中运行命令。因此,如果您想在marketplace容器中运行您的集成测试,您可以运行以下命令:
$ docker-compose build
$ docker-compose up
$ docker-compose exec marketplace pytest marketplace_integration_test.py
这将在marketplace容器中运行pytest命令。因为您的集成测试连接到localhost,所以您需要在与微服务相同的容器中运行它。
部署到 Kubernetes
太好了!现在,您的计算机上运行了几个微服务。您可以快速启动它们,并对它们运行集成测试。但是你需要让他们进入生产环境。为此,您将使用 Kubernetes 。
本教程不会深入讨论 Kubernetes,因为这是一个很大的主题,其他地方有全面的文档和教程。然而,在本节中,您将找到将 Python 微服务部署到云中的 Kubernetes 集群的基础知识。
注意:要将 Docker 映像部署到云提供商,您需要将您的 Docker 映像推送到像 Docker Hub 这样的映像注册中心。
以下示例使用了本教程中的图像,这些图像已经被推送到 Docker Hub。如果你想改变它们,或者如果你想创建自己的微服务,那么你需要在 Docker Hub 上创建一个帐户,这样你就可以推送图像。如果你愿意,你也可以创建一个私人注册中心,或者使用另一个注册中心,比如亚马逊的 ECR。
立方结构〔t0〕
您可以从kubernetes.yaml中的最小 Kubernetes 配置开始。完整的文件有点长,但它由四个不同的部分组成,所以您将逐个查看它们:
1--- 2apiVersion: apps/v1 3kind: Deployment 4metadata: 5 name: marketplace 6 labels: 7 app: marketplace 8spec: 9 replicas: 3 10 selector: 11 matchLabels: 12 app: marketplace 13 template: 14 metadata: 15 labels: 16 app: marketplace 17 spec: 18 containers: 19 - name: marketplace 20 image: hidan/python-microservices-article-marketplace:0.1 21 env: 22 - name: RECOMMENDATIONS_HOST 23 value: recommendations
这为市场微服务定义了一个部署。一个部署告诉 Kubernetes 如何部署你的代码。Kubernetes 需要四条主要信息:
- 要部署什么 Docker 映像
- 要部署多少个实例
- 微服务需要什么样的环境变量
- 如何识别你的微服务
你可以告诉 Kubernetes 如何通过使用标签来识别你的微服务。虽然这里没有显示,但是你也可以告诉 Kubernetes 你的微服务需要什么内存和 CPU 资源。您可以在 Kubernetes 文档中找到许多其他选项。
下面是代码中发生的情况:
-
第 9 行告诉 Kubernetes 为你的微服务创建多少个 pod。一个 pod 基本上是一个隔离的执行环境,就像作为一组容器实现的轻量级虚拟机。设置
replicas: 3为每个微服务提供三个 pod。拥有多台机器可以实现冗余,无需停机即可滚动更新,可以根据需要扩展更多机器,并在一台机器出现故障时进行故障转移。 -
第 20 行是要部署的 Docker 映像。您必须在图像注册表中使用 Docker 图像。要在那里获得您的图像,您必须将其推送到图像注册中心。当你在 Docker Hub 上登录你的账户时,有关于如何做到这一点的说明。
建议微服务的部署非常相似:
24--- 25apiVersion: apps/v1 26kind: Deployment 27metadata: 28 name: recommendations 29 labels: 30 app: recommendations 31spec: 32 replicas: 3 33 selector: 34 matchLabels: 35 app: recommendations 36 template: 37 metadata: 38 labels: 39 app: recommendations 40 spec: 41 containers: 42 - name: recommendations 43 image: hidan/python-microservices-article-recommendations:0.1
主要区别是一个用名字marketplace,一个用recommendations。您还在marketplace部署上设置了RECOMMENDATIONS_HOST环境变量,但是没有在recommendations部署上设置。
接下来,为推荐微服务定义一个服务。部署告诉 Kubernetes 如何部署您的代码,而服务告诉它如何将请求路由到它。为了避免与通常用来谈论微服务的术语服务混淆,当提到 Kubernetes 服务时,您会看到这个词被大写。
以下是recommendations的服务定义:
44--- 45apiVersion: v1 46kind: Service 47metadata: 48 name: recommendations 49spec: 50 selector: 51 app: recommendations 52 ports: 53 - protocol: TCP 54 port: 50051 55 targetPort: 50051
下面是定义中发生的情况:
-
**第 48 行:**当您创建一个服务时,Kubernetes 实际上会在集群中创建一个具有相同
name的 DNS 主机名。因此集群中的任何微服务都可以向recommendations发送请求。Kubernetes 会将此请求转发到您的部署中的一个单元。 -
**第 51 行:**该行将服务连接到部署。它告诉 Kubernetes 将对
recommendations的请求转发到recommendations部署中的一个 pod。这必须匹配部署的labels中的一个键值对。
marketplace服务是类似的:
56--- 57apiVersion: v1 58kind: Service 59metadata: 60 name: marketplace 61spec: 62 type: LoadBalancer 63 selector: 64 app: marketplace 65 ports: 66 - protocol: TCP 67 port: 5000 68 targetPort: 5000
除了名称和端口,只有一个区别。您会注意到type: LoadBalancer只出现在marketplace服务中。这是因为marketplace需要从 Kubernetes 集群外部访问,而recommendations只需要在集群内部访问。
**注意:**在有很多微服务的大型集群中,使用 Ingress 服务比使用 LoadBalancer 服务更常见。如果你在企业环境中开发微服务,那么这可能是一条可行之路。
查看 Sandeep Dinesh 的文章 Kubernetes 节点端口 vs 负载平衡器 vs 入口?什么时候该用什么?了解更多信息。
您可以通过展开下面的框来查看完整的文件:
1--- 2apiVersion: apps/v1 3kind: Deployment 4metadata: 5 name: marketplace 6 labels: 7 app: marketplace 8spec: 9 replicas: 3 10 selector: 11 matchLabels: 12 app: marketplace 13 template: 14 metadata: 15 labels: 16 app: marketplace 17 spec: 18 containers: 19 - name: marketplace 20 image: hidan/python-microservices-article-marketplace:0.1 21 env: 22 - name: RECOMMENDATIONS_HOST 23 value: recommendations 24--- 25apiVersion: apps/v1 26kind: Deployment 27metadata: 28 name: recommendations 29 labels: 30 app: recommendations 31spec: 32 replicas: 3 33 selector: 34 matchLabels: 35 app: recommendations 36 template: 37 metadata: 38 labels: 39 app: recommendations 40 spec: 41 containers: 42 - name: recommendations 43 image: hidan/python-microservices-article-recommendations:0.1 44--- 45apiVersion: v1 46kind: Service 47metadata: 48 name: recommendations 49spec: 50 selector: 51 app: recommendations 52 ports: 53 - protocol: TCP 54 port: 50051 55 targetPort: 50051 56--- 57apiVersion: v1 58kind: Service 59metadata: 60 name: marketplace 61spec: 62 type: LoadBalancer 63 selector: 64 app: marketplace 65 ports: 66 - protocol: TCP 67 port: 5000 68 targetPort: 5000
现在您已经有了 Kubernetes 配置,下一步是部署它!
部署 Kubernetes
您通常使用云提供商来部署 Kubernetes。有很多云提供商可以选择,包括谷歌 Kubernetes 引擎(GKE) 、亚马逊弹性 Kubernetes 服务(EKS) 、数字海洋。
如果您在自己的公司部署微服务,那么您使用的云提供商可能会由您的基础架构决定。对于这个演示,您将在本地运行 Kubernetes。几乎一切都将和使用云提供商一样。
如果你在 Mac 或 Windows 上运行 Docker Desktop,那么它会附带一个本地 Kubernetes 集群,你可以在 Preferences 菜单中启用它。通过点按系统托盘中的 Docker 图标打开偏好设置,然后找到 Kubernetes 部分并启用它:
如果你运行的是 Linux,那么你可以安装 minikube 。按照起始页上的说明进行设置。
创建集群后,可以使用以下命令部署微服务:
$ kubectl apply -f kubernetes.yaml
如果你想尝试在云中部署到 Kubernetes,DigitalOcean 是设置最简单的,并且有一个简单的定价模型。你可以注册账户,然后点击几下就可以创建一个 Kubernetes 集群。如果您更改默认值,只使用一个节点和最便宜的选项,那么在撰写本文时,成本仅为每小时 0.015 美元。
按照 DigitalOcean 提供的说明为kubectl下载一个配置文件并运行上面的命令。然后你可以点击 DigitalOcean 中的 Kubernetes 按钮来查看你的服务在那里运行。DigitalOcean 将为您的LoadBalancer服务分配一个 IP 地址,因此您可以通过将该 IP 地址复制到浏览器中来访问您的 Marketplace 应用程序。
**重要提示:**当你完成后,销毁你的集群,这样你就不会继续被收费。您还应该转到 Networking 选项卡并销毁负载平衡器,它独立于群集,但也会产生费用。
部署到库伯内特斯。接下来,您将学习如何监控 Python 微服务。
使用拦截器的 Python 微服务监控
一旦你在云中有了一些微服务,你就想要了解它们的运行情况。您希望监控的一些内容包括:
- 每个微服务收到多少请求
- 有多少请求会导致错误,它们会引发什么类型的错误
- 每个请求的延迟
- 异常日志,以便您以后进行调试
在下面几节中,您将了解到实现这一点的几种方法。
为什么不是装修工
有一种方法可以做到这一点,对 Python 开发人员来说最自然的方法是为每个微服务端点添加一个装饰器。然而,在这种情况下,使用 decorators 有几个缺点:
- 新微服务的开发人员必须记住将它们添加到每个方法中。
- 如果你有很多监控,那么你可能会有一堆装饰器。
- 如果你有一个 decorators 的堆栈,那么开发者可能会以错误的顺序堆叠它们。
- 您可以将所有的监控整合到一个装饰器中,但是这样会很麻烦。
这一堆装饰器是你想要避免的:
1class RecommendationService(recommendations_pb2_grpc.RecommendationsServicer):
2 @catch_and_log_exceptions
3 @log_request_counts
4 @log_latency
5 def Recommend(self, request, context):
6 ...
在每个方法上都有这一堆装饰器是丑陋和重复的,并且违反了干编程原则:不要重复自己。装饰者也是一个编写挑战,尤其是如果他们接受参数的话。
截击机
有一种使用装饰器的替代方法,您将在本教程中学习:gRPC 有一个拦截器的概念,它提供了类似于装饰器的功能,但方式更简洁。
实现拦截器
不幸的是,gRPC 的 Python 实现为拦截器提供了一个相当复杂的 API。这是因为它的非常灵活。但是,有一个 grpc-interceptor 包可以简化它们。为了充分披露,我是作者。
将它与 pytest 一起添加到您的recommendations/requirements.txt中,您很快就会用到它们:
grpc-interceptor ~= 0.12.0
grpcio-tools ~= 1.30
pytest ~= 5.4
然后更新您的虚拟环境:
$ python -m pip install recommendations/requirements.txt
现在可以用下面的代码创建一个拦截器。您不需要将它添加到您的项目中,因为它只是一个示例:
1from grpc_interceptor import ServerInterceptor
2
3class ErrorLogger(ServerInterceptor):
4 def intercept(self, method, request, context, method_name):
5 try:
6 return method(request, context)
7 except Exception as e:
8 self.log_error(e)
9 raise
10
11 def log_error(self, e: Exception) -> None:
12 # ...
每当调用微服务中未处理的异常时,它都会调用log_error()。例如,您可以通过将异常记录到岗哨来实现这一点,这样当它们发生时,您就可以得到警告和调试信息。
要使用这个拦截器,您应该像这样将它传递给grpc.server():
interceptors = [ErrorLogger()]
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10),
interceptors=interceptors)
有了这段代码,Python 微服务的每个请求和响应都将通过拦截器,因此您可以计算它收到了多少请求和错误。
grpc-interceptor还为每个 gRPC 状态代码提供了一个异常和一个名为ExceptionToStatusInterceptor的拦截器。如果微服务引发其中一个异常,那么ExceptionToStatusInterceptor将设置 gRPC 状态代码。这使您可以通过对recommendations/recommendations.py进行以下突出显示的更改来简化您的微服务:
1from grpc_interceptor import ExceptionToStatusInterceptor 2from grpc_interceptor.exceptions import NotFound 3
4# ...
5
6class RecommendationService(recommendations_pb2_grpc.RecommendationsServicer):
7 def Recommend(self, request, context):
8 if request.category not in books_by_category:
9 raise NotFound("Category not found") 10
11 books_for_category = books_by_category[request.category]
12 num_results = min(request.max_results, len(books_for_category))
13 books_to_recommend = random.sample(books_for_category, num_results)
14
15 return RecommendationResponse(recommendations=books_to_recommend)
16
17def serve():
18 interceptors = [ExceptionToStatusInterceptor()] 19 server = grpc.server( 20 futures.ThreadPoolExecutor(max_workers=10),
21 interceptors=interceptors
22 )
23 # ...
这样可读性更强。您还可以从调用堆栈中的许多函数中抛出异常,而不必传递context,这样您就可以调用context.abort()。您也不必在您的微服务中自己捕捉异常——拦截器会为您捕捉它。
测试拦截器
如果您想编写自己的拦截器,那么您应该测试它们。但是在测试拦截器这样的东西时,过多地模仿是危险的。例如,您可以在测试中调用.intercept(),并确保它返回您想要的结果,但是这不会测试真实的输入,或者它们甚至根本不会被调用。
为了改进测试,您可以运行带有拦截器的 gRPC 微服务。grpc-interceptor包提供了一个框架来完成这项工作。下面,您将为ErrorLogger拦截器编写一个测试。这只是一个示例,所以您不需要将其添加到项目中。如果您要添加它,那么您应该将它添加到测试文件中。
下面是如何为拦截器编写测试的方法:
1from grpc_interceptor.testing import dummy_client, DummyRequest, raises
2
3class MockErrorLogger(ErrorLogger):
4 def __init__(self):
5 self.logged_exception = None
6
7 def log_error(self, e: Exception) -> None:
8 self.logged_exception = e
9
10def test_log_error():
11 mock = MockErrorLogger()
12 ex = Exception()
13 special_cases = {"error": raises(ex)}
14
15 with dummy_client(special_cases=special_cases, interceptors=[mock]) as client:
16 # Test no exception
17 assert client.Execute(DummyRequest(input="foo")).output == "foo"
18 assert mock.logged_exception is None
19
20 # Test exception
21 with pytest.raises(grpc.RpcError) as e:
22 client.Execute(DummyRequest(input="error"))
23 assert mock.logged_exception is ex
这里有一个演练:
-
第 3 行到第 8 行子类
ErrorLogger模仿出log_error()。你实际上不希望日志副作用发生。你只是想确保它被调用。 -
第 15 到 18 行使用
dummy_client()上下文管理器创建一个连接到真实 gRPC 微服务的客户端。你发送DummyRequest给微服务,它回复DummyResponse。默认情况下,DummyRequest的input回显到DummyResponse的output。但是,你可以传递给dummy_client()一个特殊情况的字典,如果input匹配其中一个,那么它将调用你提供的函数并返回结果。 -
**第 21 行到第 23 行:**测试
log_error()被调用时出现了预期的异常。raises()返回引发所提供异常的另一个函数。您将input设置为error,这样微服务就会引发一个异常。
关于测试的更多信息,你可以阅读用 Pytest 进行有效的 Python 测试和了解 Python 模拟对象库。
在某些情况下,拦截器的替代方案是使用服务网格。它将通过代理发送所有微服务请求和响应,因此代理可以自动记录请求量和错误计数等信息。为了获得准确的错误记录,您的微服务仍然需要正确设置状态代码。所以在某些情况下,您的拦截器可以补充服务网格。一个受欢迎的服务网是 T2 Istio T3。
最佳实践
现在您已经有了一个可以工作的 Python 微服务设置。您可以创建微服务,一起测试它们,将它们部署到 Kubernetes,并用拦截器监控它们。此时,您可以开始创建微服务。但是,您应该记住一些最佳实践,因此您将在本节中学习一些。
Protobuf 组织
通常,您应该将 protobuf 定义与微服务实现分开。客户端几乎可以用任何语言编写,如果你把你的 protobuf 文件捆绑到一个 Python wheel 或者类似的东西中,那么如果有人想要一个 Ruby 或者 Go 客户端,他们将很难得到 protobuf 文件。
即使你所有的代码都是 Python,为什么有人需要为微服务安装包,只是为了给它写一个客户端?
一个解决方案是将 protobuf 文件放在与微服务代码分开的 Git repo 中。许多公司将所有微服务的所有proto buf 文件放在一个单独的 repo 中。这使得更容易找到所有的微服务,在它们之间共享公共的 protobuf 结构,并创建有用的工具。
如果您选择将 protobuf 文件存储在单个 repo 中,那么您需要注意保持 repo 的有序,并且您一定要避免 Python 微服务之间的循环依赖。
Protobuf 版本控制
API 版本控制可能很难。最主要的原因是,如果你改变了一个 API,更新了微服务,那么可能仍然会有客户端使用旧的 API。当客户端运行在客户的机器上时,例如移动客户端或桌面软件,情况尤其如此。
不能轻易强迫人家更新。即使你可以,网络延迟会导致竞争条件,并且你的微服务很可能使用旧的 API 来获得请求。好的 API 应该是向后兼容的或者版本化的。
为了实现向后兼容性,使用 protobufs 版本 3 的 Python 微服务将接受缺少字段的请求。如果你想增加一个新的领域,那也没问题。您可以先部署微服务,它仍然会接受来自旧 API 的请求,而没有新字段。微服务只需要优雅地处理这个问题。
如果你想做更大的改变,那么你需要更新你的 API。Protobufs 允许您将 API 放入一个包名称空间,其中可以包含一个版本号。如果你需要彻底改变 API,那么你可以创建一个新的版本。微服务也可以继续接受旧版本。这允许您推出新的 API 版本,同时逐步淘汰旧版本。
通过遵循这些惯例,你可以避免做出突破性的改变。在公司内部,人们有时认为对 API 进行突破性的修改是可以接受的,因为他们控制了所有的客户。这由您来决定,但请注意,做出突破性的更改需要协调客户端和微服务的部署,这使得回滚变得复杂。
在微服务生命周期的早期,当没有生产客户端时,这是可以接受的。然而,一旦你的微服务对你公司的健康至关重要,养成只做非破坏性改变的习惯是很好的。
原蟾蜍林挺
确保你不会对你的 protobufs 做破坏性改变的一个方法是使用 linter 。比较流行的一个是 buf 。您可以将此设置为您的 CI 系统的一部分,这样您就可以检查拉动式请求中的重大变更。
类型检查 Protobuf 生成的代码
Mypy 是一个静态类型检查 Python 代码的项目。如果您不熟悉 Python 中的静态类型检查,那么您可以阅读 Python 类型检查来了解它的全部内容。
protoc生成的代码有点粗糙,而且没有类型注释。如果你试着用 Mypy 检查它,那么你会得到很多错误,而且它不会发现真正的错误,比如拼错的字段名。幸运的是,Dropbox 的好心人写了一个插件,让protoc编译器生成类型存根。这些不应与 gRPC 存根混淆。
为了使用它,您可以安装mypy-protobuf包,然后更新命令以生成 protobuf 输出。请注意新的‑‑mypy_out选项:
$ python -m grpc_tools.protoc -I ../protobufs --python_out=. \
--grpc_python_out=. --mypy_out=. ../protobufs/recommendations.proto
你的大多数错误应该会消失。您可能仍然会得到一个关于grpc包没有类型信息的错误。你可以安装非官方的 gRPC 类型的存根或者添加以下内容到你的 Mypy 配置中:
[mypy-grpc.*] ignore_missing_imports = True
您仍然可以获得类型检查的大部分好处,比如捕捉拼写错误的字段。这对于在 bug 进入生产之前捕捉它们非常有帮助。
优雅地关机
在你的开发机器上运行你的微服务时,你可以按 Ctrl + C 来停止它。这将导致 Python 解释器引发一个KeyboardInterrupt异常。
当 Kubernetes 正在运行你的微服务,需要停止它以推出更新时,它会向你的微服务发送信号。具体来说,它会发出一个SIGTERM信号,等待三十秒。如果你的微服务到那时还没有退出,它会发送一个SIGKILL信号。
您可以并且应该捕获并处理SIGTERM,这样您就可以完成当前请求的处理,但拒绝新的请求。您可以通过将以下代码放入serve()中来实现这一点:
1from signal import signal, SIGTERM
2
3...
4
5def serve():
6 server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
7 ...
8 server.add_insecure_port("[::]:50051")
9 server.start()
10
11 def handle_sigterm(*_):
12 print("Received shutdown signal")
13 all_rpcs_done_event = server.stop(30)
14 all_rpcs_done_event.wait(30)
15 print("Shut down gracefully")
16
17 signal(SIGTERM, handle_sigterm)
18 server.wait_for_termination()
这里有一个细目分类:
- 线 1 导入
signal,允许你捕捉和处理来自 Kubernetes 或者几乎任何其他过程的信号。 - 第 11 行定义了一个函数来处理
SIGTERM。该函数将在 Python 接收到SIGTERM信号时被调用,Python 将向其传递两个参数。然而,你不需要这些参数,所以用*_来忽略它们。 - 第 13 行调用
server.stop(30)优雅地关闭服务器。它将拒绝新的请求,并等待30秒,等待当前请求完成。它立即返回,但是它返回一个可以等待的threading.Event对象。 - 第 14 行等待
Event对象,这样 Python 就不会过早退出。 - 第 17 行注册你的处理程序。
当您部署新版本的微服务时,Kubernetes 会发送信号关闭现有的微服务。处理这些问题以优雅地关闭将确保请求不会被丢弃。
安全通道
到目前为止,您一直在使用不安全的 gRPC 通道。这意味着几件事:
-
客户端无法确认它正在向目标服务器发送请求。有人可以创建一个冒名顶替的微服务,并将其注入客户端可能发送请求的某个地方。例如,他们可能能够将微服务注入到负载平衡器将向其发送请求的 pod 中。
-
服务器无法确认客户端向其发送请求。只要有人可以连接到服务器,他们就可以向它发送任意 gRPC 请求。
-
流量是未加密的,因此路由流量的任何节点也可以查看它。
本节将描述如何添加 TLS 认证和加密。
**注意:**这不解决用户认证问题,只解决微服务流程。
您将学习两种设置 TLS 的方法:
- 最简单的方法是,客户端可以验证服务器,但是服务器不验证客户端。
- 更复杂的方式,使用相互 TLS,客户端和服务器互相验证。
在这两种情况下,流量都是加密的。
TLS 基础知识
在深入研究之前,先简要概述一下 TLS:通常,客户端验证服务器。例如,当你访问 Amazon.com 时,你的浏览器会验证这是真的 Amazon.com 而不是冒名顶替者。要做到这一点,客户必须从一个值得信赖的第三方那里得到某种保证,就像只有当你有一个共同的朋友为他们担保时,你才能信任一个新人。
使用 TLS,客户端必须信任一个证书颁发机构(CA) 。CA 将对服务器保存的内容进行签名,这样客户端就可以对其进行验证。这有点像你共同的朋友在纸条上签名,而你认出了他们的笔迹。更多信息,请参见互联网安全如何工作:TLS、SSL 和 CA 。
您的浏览器隐式信任一些 ca,它们通常是 GoDaddy、DigiCert 或 Verisign 等公司。其他公司,如亚马逊,付钱给 CA 为他们签署数字证书,这样你的浏览器就可以信任他们。通常情况下,CA 会在签署证书之前验证亚马逊是否拥有 Amazon.com。这样,冒名顶替者就不会在 Amazon.com 的证书上签名,你的浏览器也会屏蔽这个网站。
对于微服务,你不能真的要求 CA 签署证书,因为你的微服务运行在内部机器上。CA 可能会很乐意签署证书并向您收费,但问题是这不切实际。在这种情况下,您的公司可以充当自己的 CA。如果 gRPC 客户端拥有由您的公司或您(如果您正在做个人项目)签名的证书,它将信任服务器。
服务器认证
以下命令将创建一个可用于签署服务器证书的 CA 证书:
$ openssl req -x509 -nodes -newkey rsa:4096 -keyout ca.key -out ca.pem \
-subj /O=me
这将输出两个文件:
ca.key是私钥。ca.pem是一种公共证书。
然后,您可以为您的服务器创建一个证书,并用您的 CA 证书对其进行签名:
$ openssl req -nodes -newkey rsa:4096 -keyout server.key -out server.csr \
-subj /CN=recommendations
$ openssl x509 -req -in server.csr -CA ca.pem -CAkey ca.key -set_serial 1 \
-out server.pem
这将产生三个新文件:
server.key是服务器的私钥。server.csr是一个中间文件。server.pem是服务器的公共证书。
**注意:**这些命令仅用于举例。私钥是而不是加密的。如果您想为您的公司生成证书,请咨询您的安全团队。他们可能有创建、存储和吊销证书的策略,您应该遵循这些策略。
您可以将此添加到推荐微服务Dockerfile。很难安全地将秘密添加到 Docker 图像中,但有一种方法可以使用最新版本的 Docker 来做到这一点,如下所示:
1# syntax = docker/dockerfile:1.0-experimental 2# DOCKER_BUILDKIT=1 docker build . -f recommendations/Dockerfile \ 3# -t recommendations --secret id=ca.key,src=ca.key 4
5FROM python
6
7RUN mkdir /service
8COPY infra/ /service/infra/
9COPY protobufs/ /service/protobufs/
10COPY recommendations/ /service/recommendations/
11COPY ca.pem /service/recommendations/ 12
13WORKDIR /service/recommendations
14RUN python -m pip install --upgrade pip
15RUN python -m pip install -r requirements.txt
16RUN python -m grpc_tools.protoc -I ../protobufs --python_out=. \
17 --grpc_python_out=. ../protobufs/recommendations.proto
18RUN openssl req -nodes -newkey rsa:4096 -subj /CN=recommendations \ 19 -keyout server.key -out server.csr 20RUN --mount=type=secret,id=ca.key \ 21 openssl x509 -req -in server.csr -CA ca.pem -CAkey /run/secrets/ca.key \ 22 -set_serial 1 -out server.pem 23
24EXPOSE 50051
25ENTRYPOINT [ "python", "recommendations.py" ]
新行将突出显示。这里有一个解释:
- 需要第 1 行来启用机密。
- 第 2 行和第 3 行显示了如何构建 Docker 映像的命令。
- 第 11 行将 CA 公共证书复制到镜像中。
- 第 18 行和第 19 行生成新的服务器私有密钥和证书。
- 第 20 到 22 行临时加载 CA 私钥,这样您就可以用它来签署服务器的证书。但是,它不会保留在图像中。
**注:**关于‑‑mount=type=secret的更多信息,参见 Docker 文档。将来,这个特性可能会被提升到稳定版本,那时你就不需要在Dockerfile中包含syntax = docker/dockerfile:1.0-experimental评论了。
根据版本化策略,实验性语法不会消失,所以你可以无限期地继续使用它。
您的映像现在将包含以下文件:
ca.pemserver.csrserver.keyserver.pem
您现在可以更新突出显示的recommendations.py中的serve():
1def serve():
2 server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
3 recommendations_pb2_grpc.add_RecommendationsServicer_to_server(
4 RecommendationService(), server
5 )
6
7 with open("server.key", "rb") as fp: 8 server_key = fp.read() 9 with open("server.pem", "rb") as fp: 10 server_cert = fp.read() 11
12 creds = grpc.ssl_server_credentials([(server_key, server_cert)]) 13 server.add_secure_port("[::]:443", creds) 14 server.start()
15 server.wait_for_termination()
变化如下:
- 第 7 行到第 10 行加载服务器的私钥和证书。
- 第 12 行和第 13 行使用 TLS 运行服务器。它现在只接受 TLS 加密的连接。
您需要更新marketplace.py来加载 CA 证书。现在,您只需要客户端中的公共证书,如突出显示的那样:
1recommendations_host = os.getenv("RECOMMENDATIONS_HOST", "localhost")
2with open("ca.pem", "rb") as fp: 3 ca_cert = fp.read() 4creds = grpc.ssl_channel_credentials(ca_cert) 5recommendations_channel = grpc.secure_channel( 6 f"{recommendations_host}:443", creds 7) 8recommendations_client = RecommendationsStub(recommendations_channel)
您还需要将COPY ca.pem /service/marketplace/添加到市场Dockerfile中。
现在可以加密运行客户机和服务器,客户机将验证服务器。为了使运行一切简单明了,您可以使用docker-compose。然而,在撰写本文时,docker-compose 还不支持建造秘密。你将不得不手动建立 Docker 图像,而不是用docker-compose build。
但是,您仍然可以运行docker-compose up。更新docker-compose.yaml文件,删除build部分:
1version: "3.8" 2services: 3
4 marketplace: 5 environment: 6 RECOMMENDATIONS_HOST: recommendations 7 # DOCKER_BUILDKIT=1 docker build . -f marketplace/Dockerfile \ 8 # -t marketplace --secret id=ca.key,src=ca.key 9 image: marketplace 10 networks: 11 - microservices 12 ports: 13 - 5000:5000 14
15 recommendations: 16 # DOCKER_BUILDKIT=1 docker build . -f recommendations/Dockerfile \ 17 # -t recommendations --secret id=ca.key,src=ca.key 18 image: recommendations 19 networks: 20 - microservices 21
22networks: 23 microservices:
您现在正在加密流量,并验证您是否连接到了正确的服务器。
相互认证
服务器现在证明了它是可信的,但是客户端却不可信。幸运的是,TLS 允许对双方进行验证。更新高亮显示的市场Dockerfile:
1# syntax = docker/dockerfile:1.0-experimental 2# DOCKER_BUILDKIT=1 docker build . -f marketplace/Dockerfile \ 3# -t marketplace --secret id=ca.key,src=ca.key 4
5FROM python
6
7RUN mkdir /service
8COPY protobufs/ /service/protobufs/
9COPY marketplace/ /service/marketplace/
10COPY ca.pem /service/marketplace/
11
12WORKDIR /service/marketplace
13RUN python -m pip install -r requirements.txt
14RUN python -m grpc_tools.protoc -I ../protobufs --python_out=. \
15 --grpc_python_out=. ../protobufs/recommendations.proto
16RUN openssl req -nodes -newkey rsa:4096 -subj /CN=marketplace \ 17 -keyout client.key -out client.csr 18RUN --mount=type=secret,id=ca.key \ 19 openssl x509 -req -in client.csr -CA ca.pem -CAkey /run/secrets/ca.key \ 20 -set_serial 1 -out client.pem 21
22EXPOSE 5000
23ENV FLASK_APP=marketplace.py
24ENTRYPOINT [ "flask", "run", "--host=0.0.0.0"]
这些更改类似于您在上一节中对建议微服务所做的更改。
**注意:**如果您将私有密钥放在 docker 文件中,那么不要将它们放在公共存储库中。最好是在运行时通过网络从只能通过 VPN 访问的服务器上加载私钥。
更新recommendations.py中的serve()以验证突出显示的客户端:
1def serve():
2 server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
3 recommendations_pb2_grpc.add_RecommendationsServicer_to_server(
4 RecommendationService(), server
5 )
6
7 with open("server.key", "rb") as fp:
8 server_key = fp.read()
9 with open("server.pem", "rb") as fp:
10 server_cert = fp.read()
11 with open("ca.pem", "rb") as fp: 12 ca_cert = fp.read() 13
14 creds = grpc.ssl_server_credentials( 15 [(server_key, server_cert)], 16 root_certificates=ca_cert, 17 require_client_auth=True, 18 ) 19 server.add_secure_port("[::]:443", creds)
20 server.start()
21 server.wait_for_termination()
这将加载 CA 证书并要求客户端验证。
最后,更新marketplace.py以将其证书发送到服务器,如突出显示的那样:
1recommendations_host = os.getenv("RECOMMENDATIONS_HOST", "localhost")
2with open("client.key", "rb") as fp: 3 client_key = fp.read() 4with open("client.pem", "rb") as fp: 5 client_cert = fp.read() 6with open("ca.pem", "rb") as fp:
7 ca_cert = fp.read()
8creds = grpc.ssl_channel_credentials(ca_cert, client_key, client_cert) 9recommendations_channel = grpc.secure_channel(
10 f"{recommendations_host}:443", creds
11)
12recommendations_client = RecommendationsStub(recommendations_channel)
这会加载证书并将它们发送到服务器进行验证。
现在,如果您试图用另一个客户端连接到服务器,即使是一个使用 TLS 但具有未知证书的客户端,服务器也会拒绝它,并显示错误PEER_DID_NOT_RETURN_A_CERTIFICATE。
**重要提示:**虽然以这种方式管理相互 TLS 是可能的,但要做到这一点并不容易。如果您希望只允许某些微服务向其他微服务发出请求,这将变得特别困难。
如果您需要像这样增强安全性,那么最好使用一个服务网格,让它为您管理证书和授权。除了拦截器部分提到的流量监控, Istio 还可以管理相互 TLS 和每个服务的授权。它也更安全,因为它将为您管理机密,并更频繁地重新颁发证书。
这就完成了微服务之间的安全通信。接下来,您将了解如何将 AsyncIO 与微服务结合使用。
AsyncIO and gRPC
官方 gRPC 包中缺少 AsyncIO 支持已经很长时间了,但是最近已经添加了。它仍然处于实验阶段,正在积极开发中,但如果你真的想在你的微服务中尝试 AsyncIO,那么它可能是一个不错的选择。您可以查看 gRPC AsyncIO 文档了解更多详细信息。
还有一个名为 grpclib 的第三方包,它实现了对 gRPC 的 AsyncIO 支持,并且已经存在了很长时间。
在服务器端使用 AsyncIO 要非常小心。很容易一不小心写了阻塞代码,让你的微服务瘫痪。作为示范,下面是如何使用 AsyncIO 编写建议微服务,去掉所有逻辑:
1import time
2
3import asyncio
4import grpc
5import grpc.experimental.aio
6
7from recommendations_pb2 import (
8 BookCategory,
9 BookRecommendation,
10 RecommendationResponse,
11)
12import recommendations_pb2_grpc
13
14class AsyncRecommendations(recommendations_pb2_grpc.RecommendationsServicer):
15 async def Recommend(self, request, context):
16 print("Handling request")
17 time.sleep(5) # Oops, blocking!
18 print("Done")
19 return RecommendationResponse(recommendations=[])
20
21async def main():
22 grpc.experimental.aio.init_grpc_aio()
23 server = grpc.experimental.aio.server()
24 server.add_insecure_port("[::]:50051")
25 recommendations_pb2_grpc.add_RecommendationsServicer_to_server(
26 AsyncRecommendations(), server
27 )
28 await server.start()
29 await server.wait_for_termination()
30
31asyncio.run(main())
这段代码中有一个错误。在第 17 行,您意外地在一个async函数中进行了一个阻塞调用,这是一个大禁忌。因为 AsyncIO 服务器是单线程的,这阻塞了整个服务器,所以它一次只能处理一个请求。这比线程服务器差多了。
您可以通过发出多个并发请求来证明这一点:
1from concurrent.futures import ThreadPoolExecutor
2
3import grpc
4
5from recommendations_pb2 import BookCategory, RecommendationRequest
6from recommendations_pb2_grpc import RecommendationsStub
7
8request = RecommendationRequest(user_id=1, category=BookCategory.MYSTERY)
9channel = grpc.insecure_channel("localhost:50051")
10client = RecommendationsStub(channel)
11
12executor = ThreadPoolExecutor(max_workers=5)
13a = executor.submit(client.Recommend, request)
14b = executor.submit(client.Recommend, request)
15c = executor.submit(client.Recommend, request)
16d = executor.submit(client.Recommend, request)
17e = executor.submit(client.Recommend, request)
这将产生五个并发请求,但是在服务器端,您会看到:
Handling request
Done
Handling request
Done
Handling request
Done
Handling request
Done
Handling request
Done
请求是按顺序处理的,这不是您想要的!
在服务器端有 AsyncIO 的用例,但是你必须非常小心不要阻塞。这意味着你不能使用像 requests 这样的标准包,甚至不能对其他微服务进行 RPC,除非你使用 run_in_executor 在另一个线程中运行它们。
您还必须小心处理数据库查询。您已经开始使用的许多优秀的 Python 包可能还不支持 AsyncIO,所以要小心检查它们是否支持 AsyncIO。除非您非常需要服务器端的 AsyncIO,否则等到有更多的软件包支持时可能会更安全。阻塞错误可能很难发现。
如果你想了解更多关于 AsyncIO 的知识,那么你可以查看Python 中的异步特性入门和Python 中的异步 IO:完整演练。
结论
微服务是管理复杂系统的一种方式。随着组织的发展,它们成为组织代码的自然方式。了解如何在 Python 中有效地实现微服务,可以让你随着公司的发展而变得更有价值。
在本教程中,您已经学习了:
- 如何用 gRPC 有效实现 Python 微服务
- 如何将微服务部署到 Kubernetes
- 如何将集成测试、拦截器、 TLS 和 AsyncIO 等特性整合到您的微服务中
- 创建 Python 微服务时要遵循哪些最佳实践
现在,您已经准备好将较大的 Python 应用程序分解成较小的微服务,使您的代码更有组织性和可维护性。要回顾您在本教程中学到的所有内容,您可以通过单击下面的链接下载示例的源代码:
获取源代码: 单击此处获取源代码,您将在本教程中使用了解如何使用 gRPC 创建 Python 微服务。**********


