Python 微服务开发--Python Microservices Development

贲绪
2023-12-01

用Python构建、测试、部署和扩展微服务

本文为 Python Microservices Development 的选择性翻译,本书暂时无中文版,有能力请直接看原版

什么是微服务?

A microservice is a lightweight application, which provides a narrowed list of features with a well-defined contract. It's a component with a single responsibility, which can be developed and deployed independently.

微服务是一个轻量级的应用程序,它通过定义良好的契约提供窄范围的特性列表。 它是一个具有单一职责的组件,可以独立地进行开发和部署。

本书内容:
第一章 理解微服务,定义什么是微服务,以及在现在web应用当中的角色.说明为什么Python非常适合构建微服务。
第二章 了解Flask,介绍Flask的主要特性。用Flask搭建一个简单的web应用微服务。
第三章 编码,文档,和测试——良性循环,描述测试驱动和持续集成方法,Flask应用构建和打包实践 第四章 设计Runnerly,通过应用的功能和用户故事,解释如何将他建成一个整体应用,然后把它分解成微服务,并解释它们如何与数据进行交互。还将介绍Open API 2.0 规范(ex-Swagger),该规范可用于藐视HTTP APIs。
第五章 整合其他服务,解释一个服务如何与后端服务整合,怎样处理网络分割和其他交互问题,以及如何在隔离状态下测试服务。
第六章 服务安全,解释如何保护你的微服务,以及如何处理用户身份严重「」服务到服务身份验证和用户管理。还将介绍欺诈和滥用,以及如何将他们降低。 第七章 监控你的服务,解释如何在你的代码中添加日志和指标(metrics),以及如何确保你对你的应用程序中正在发生的事情有一个清晰的全局理解,从而跟踪问题并了解你的服务使用情况。
第八章 整合起来,描述了如何设计和构建一个使用了微服务的JavaScript应用程序,给终端用户界面。 第九章 打包运行Runnerly,描述如何打包、构建和运行整个Forrest应用程序。作为一名开发人员,能够将构成应用程序的所有部分运行到单个开发包中是必不可少的。
第十章 集装箱式服务,解释什么是虚拟化,如何使用Docker,以及如何将你的服务进行Dockerize。
第十一章 在AWS上部署,介绍现有的云服务提供商,和AWS世界,展示了如何实例化服务器,对于运行微服务的应用程序非常有用的AWS服务。本文还介绍了CoreOS,一个专门为在云中部署Docker容器而创建的Linux发行版。
第十二章 下一步是什么,在书的结尾处给出了一些关于如何独立于特定的云供应商和虚拟化技术构建微服务的建议,以避免将所有鸡蛋放在同一个篮子里。它强调了你在第九章中学到的东西,打包和运行Runnerly。

单体应用的利弊:

  • 可能是开始一个项目最简单的方式
  • 集中式数据库简化了数据的设计和组织。
  • 部署一个应用程序很简单。
  • 代码中的任何更改都可能影响不相关的特性。当某个功能出现问题时,整个应用程序都可能出现问题。
  • 扩展应用程序的解决方案是有限的:你可以部署多个实例,但是如果应用程序中的一个特定功能占用了所有资源,那么它就会影响到所有方面。
  • 随着代码库的增长,很难保持它的整洁和受控。

一个酒店预订网站的结构

1. 单体应用结构

2. 微服务结构

  1. 在这个设计中,每个组件都使用 HTTP 协议进行通信,并且特性可以通过 RESTful web 服务获得。
  2. 没有集中的数据库,每个微服务在内部处理自己的数据结构,进出的数据使用语言无关的格式,如 JSON。

微服务优点

  1. 关注点分离

    • 首先,每个微服务都可以由一个独立的团队独立开发。 例如,构建一个预订服务本身就是一个完整的项目。 负责团队可以使用任何编程语言和数据库,只要它有一个良好文档的 HTTP API。
    • 这也意味着相比单体应用,程序的进化更好地受到控制。例如,如果支付系统改变了与银行的底层交互,那么影响就局限在该服务内部,而应用程序的其余部分保持稳定,可能不会受到影响。
    • 这种松散耦合大大提高了整体项目的速度,因为我们在服务层面采用了类似于单一责任原则的理念
  2. 需要处理的是较小项目

    • 第二个好处是打破了项目的复杂性。 当向应用程序添加一个特性(如 PDF 报告)时,即使做得干净利落,也会使基本代码变得更大、更复杂,有时还会变慢。
    • 在单独的应用程序中构建这个特性可以避免这个问题,并且可以使用任何你想要的工具更容易地编写它。 你可以经常重构它,缩短发布周期,能够更好的掌控。程序的增长仍在控制之下。
    • 在改进应用程序时,处理较小的项目也可以降低风险: 如果团队想要尝试最新的编程语言或框架,他们可以快速迭代一个原型,实现相同的微服务 API,尝试它并决定是否仍然使用它
    • 一个真实的例子是 Firefox Sync 存储微服务。 目前有一些试验从当前的 Python + MySQL 实现切换到基于 go 的实现,该实现将用户的数据存储在独立的 SQLite 数据库中。 这个原型是高度试验性的,但是因为我们已经用一个定义良好的 HTTP API 将存储特性分离到微服务中,所以很容易用一小部分用户尝试一下。(看来有时间还是要学习一下Go)
  3. 更多的扩展和部署选项

    • 最后,将应用程序分割成组件,可以更容易地根据约束进行扩展。 假设你有很多客户每天预定酒店,而生成PDF消耗大量cpu。这时可以将这个特定的微服务部署到拥有更大 cpu 的服务器上。
    • 另一个典型的例子是 RAM 消耗型的微服务,比如那些与内存数据库(如 Redis 或 Memcache)交互的服务。 您可以调整部署,将其部署到具有更少 CPU 和更多 RAM 的服务器上。

因此,我们可以将微服务的好处概括如下:

  • 一个团队可以独立开发每个微服务,使用任何能使用的技术栈。 他们可以自定义一个发布周期,只需要完成一个与语言无关的 HTTP API。
  • 开发人员将应用程序的复杂性分解为逻辑组件。每个微服务都专注于做好一件事情。
  • 由于微服务是独立的应用程序,因此对部署有更好的控制,这使得扩展更加容易。

微服务体系结构有助于解决应用程序开始增长时可能出现的许多问题。 然而,我们需要意识到它带来的一些新问题。

微服务隐患

  1. 不合逻辑的分割
    • 微服务架构的第一个问题是如何设计它。一个团队不可能在第一次就想出完美的微服务架构。 一些微服务(如 PDF 生成器)是显而易见的用例。而只要是处理业务逻辑,你的代码就有很大的可能,在你理解如何将应用分割成正确的微服务集合之前,四处移动。
    • 成熟的设计需要一些尝试和失败的循环。 添加和删除微服务可能比重构单体应用程序更痛苦。
    • 如果分隔不明显的话,可以避免分割应用成微服务
    • 如果有任何怀疑分割有无意义,就保持在一起。将一些代码分割成一个新的微服务,比在合并回两个微服务要容易得多
    • 例如,如果你总是必须将两个微服务部署在一起,或者一个微服务中的一个更改影响到另一个微服务的数据模型,那么您没有正确地分割应用程序,并且这两个服务应该重新组合。
  2. 更多的网络交互
  3. 数据存储和共享
    • 一个有效的微服务需要独立于其他微服务,理想情况下不应该共享一个数据库。 这对我们的酒店预订应用程序意味着什么?
    • 同样,这也引出了很多问题,比如:我们是在所有数据库中使用相同的用户 id,还是在每个服务中使用独立的id并将其作为一个隐藏的实现细节?
    • 一旦用户添加到系统中,我们是通过数据抽取策略在其他服务数据库中复制一些它的信息,还是这样做有点过了?
    • 如何处理数据删除?
    • 尽可能避免数据重复,同时将微服务隔离开来,是设计基于微服务的应用程序的最大挑战之一。

这些都是很难回答的问题,有很多不同的方法可以解决这些问题,我们将在书中学到这一点。

  1. 兼容性问题

    • 另一个问题发生在功能更改影响多个微服务时。如果更改以向后不兼容的方式影响在服务之间传输的数据,那么就会遇到麻烦。
    • 你部署的新服务是否可以与其他服务的旧版本一起使用?还是需要同时更改和部署多个服务?这是否意味着你发现了一些服务应该被合并回来?
  2. 测试

    • 最后,当你想要进行一些端到端测试并部署整个应用程序时,您现在必须处理许多应用。你需要一个健壮的、敏捷的部署流程来提高效率。你需要能够在开发整个应用程序时使用它。你不可能仅仅用几个用例就完全测试出来。
    • 介绍一些促进微服务的工具

WSGI标准

WSGI 最大的问题在于它的同步性。你在前面的代码中看到的应用程序函数对每个传入请求只调用一次,当函数返回时,它必须返回响应。 这意味着每次调用函数时,它都会阻塞,直到响应准备好为止。

WSGI 服务器将允许你运行一个线程池来同时处理多个请求。 但是你不能运行成千上万个这样的服务,一旦这个池用完了,下一个请求就会阻止客户的访问,而你的微服务什么也不做,只是空闲地等待后端服务的响应。

这就是为什么 Twisted 和 Tornado 这样的非 wsgi 框架( 在JavaScript 领域中是node.js),非常成功的原因——它完全是异步的。

在编写 Twisted 应用程序时,可以使用回调来暂停和恢复生成响应的工作。 这意味着你可以接受新的请求并开始处理它们。 这种模式显著地减少了进程中的空闲时间。 它可以处理成千上万的并发请求。 当然,这并不意味着应用程序会更快地返回每个响应。 它仅仅意味着一个进程可以接受更多的并发请求,并且在数据准备发回时在这些请求之间进行切换。

WSGI 标准没有简单的方法来引入类似的东西,社区已经争论了多年来达成共识---- 但失败了。 可能的情况是,社区最终会放弃 WSGI 标准。
与此同时,如果你考虑到WSGI标准,一个请求等于一个线程,那么构建具有同步框架的微服务仍然是可能的并且完全没问题。

但是,有一个增强同步 web 应用程序的技巧—— Greenlet,将在下一节解释。

Gevent

Gevent提供了 socket 模块的合作版本,该模块使用 greenlets 来在socket中有数据可用时自动暂停和恢复执行。 甚至还有一个 monkey 补丁功能,可以用 Gevent 的版本自动替换标准库socket。 这使你的标准同步代码在每次使用 socket时都神奇地异步——只需多加一行:

from gevent import monkey

monkey.patch_all()


def application(environ, start_response):
    headers = [('Content-type', 'application/json')]
    start_response('200 OK', headers)
    # ...do something with sockets here...
    return result
复制代码

不过,这种隐式的魔力是有代价的。 为了让 Gevent 能够正常工作,所有的底层代码都需要与 Gevent 所做的修补兼容。 一些来自社区的软件包会因为这个原因而继续阻塞甚至产生意外的结果---- 特别是,如果他们使用 c 扩展,并绕过了标准库 Gevent 补丁的一些特性。 但在大多数情况下效果都很好。 与 Gevent 兼容的项目被称为绿色项目。

Twisted and Tornado

如果你正在构建微服务,而且并发请求的数量很重要,那么放弃WSGI标准是很诱人的,你可以使用异步框架Twisted 或 Tornado

import time
import json
from twisted.web import server, resource
from twisted.internet import reactor, endpoints


class Simple(resource.Resource):
    isLeaf = True

    def render_GET(self, request):
        request.responseHeaders.addRawHeader(b"content-type",
                                             b"application/json")
        return bytes(json.dumps({'time': time.time()}), 'utf8')

site = server.Site(Simple())
endpoint = endpoints.TCP4ServerEndpoint(reactor, 8080)
endpoint.listen(site)
reactor.run()
复制代码

虽然 Twisted 是一个非常健壮和高效的框架,但是它在构建 HTTP 微服务时遇到了以下几个问题:

  • 您需要使用从Resource类派生的类来实现微服务中的每个端点,并实现每个受支持的方法。对于一些简单的API,它添加了许多样板代码。
  • 由于其异步性质,扭曲的代码很难理解和调试。
  • 当你链接太多连续依次触发的函数时,很容易陷入回调地狱 - 代码可能变得混乱。
  • 正确测试Twisted应用程序很困难,您必须使用特定于Twisted的单元测试模型。

Tornado 基于类似的模型,但在某些领域做得更好。它有一个更轻的路由系统,并尽一切可能使代码更接近普通的Python。 Tornado也使用回调模型,因此调试很困难。 依赖 Python 3中引入的新的异步特性。两个框架都在努力弥合这一问题。

asyncio

from aiohttp import web
import time


async def handle(request):
    return web.json_response({'time': time.time()})


if __name__ == '__main__':
    app = web.Application()
    app.router.add_get('/', handle)
    web.run_app(app)

复制代码

但是,基于Python 3的异步框架和库仍然在不断涌现,如果使用异步或aiohttp之类的框架,则需要针对每个需要的特性坚持使用特定的异步实现。 如果需要在代码中使用非异步的库,则从异步代码使用它意味着如果要防止阻塞事件循环,则需要执行一些额外的、具有挑战性的工作。

如果你的微服务处理的资源数量有限,这可能是可控的。 但是在写这篇文章的时候,坚持使用已经存在了一段时间的同步框架而不是异步框架可能是一个更安全的选择。 让我们享受成熟软件包的现有生态系统,并等待异步生态系统变得更加完善。

这本书的第二版很有可能使用异步框架。 但是对于这个版本,我们将在整本书中使用 Flask 框架。

语言性能

当然,每个人都知道Python比Java或GO慢,但是执行速度并不总是最优先考虑的。微服务通常是一层很薄的代码,其生命周期的大部分时间都在等待来自其他服务的一些网络响应。与从 Postgres 服务器返回 SQL 查询的速度相比,它的核心速度通常不那么重要,因为后者占用了响应的大部分时间。

但是想要一个尽可能快的应用程序是合理的

Python 社区中关于加速语言的一个有争议的话题是,GIL如何破坏性能,因为多线程应用程序不能使用多个进程。

GIL 有存在的充分理由。它保护CPython解释器中非线程安全的部分,并且存在于其他语言中,如 Ruby。到目前为止,所有试图删除它的尝试都未能生成更快的 CPython 实现。

对于微服务,除了防止在同一进程中使用多个内核之外,GIL 在高负载时会稍微降低性能,因为互斥锁引入了系统调用开销。

然而,围绕 GIL 的所有审查都是有益的: 在过去几年中已经完成了减少解释器中 GIL 争论的工作,并且在某些方面,Python 的性能有了很大的提高。

请记住,即使核心团队删除 GIL,Python 也是一种解释语言和垃圾收集语言,并且会因为这些属性而遭受性能损失。

在静态编译语言中编写一个类似的函数将大大减少产生相同结果所需的操作数量。

不过,有一些方法可以提高 Python 的执行速度。

一种方法是通过构建 c 扩展,或者使用语言的静态扩展(如 Cython (http: / / Cython. org /) ,将部分代码编写到已编译的代码中,但这会使代码更加复杂。

另一个解决方案是最有希望的,那就是使用 PyPy 解释器(http: / / PyPy. org /)简单地运行应用程序。

Pypy 实现一个实时(JIT)编译器。 这个编译器在运行时直接用 CPU 可以直接使用的机器代码替换部分 Python 代码。 对于 JIT 来说,整个技巧就是要在执行之前提前检测到什么时候以及如何去做。

即使PyPy总是CPython之后的几个Python版本,但它已经达到了可以在生产中使用的程度,而且它的性能相当惊人。 我们在 Mozilla 的一个项目需要快速执行,PyPy 版本几乎和 Go 版本一样快,所以我们决定在那里使用 Python。 ... 无论如何,对于大多数项目来说,Python 及其生态系统的好处大大超过了本节描述的性能问题,因为微服务的开销很少成为问题。 如果性能有问题,微服务方法允许你重写性能关键组件,而不会影响系统的其余部分。

在本章中,我们比较了单体应用和微服务的方法来构建 web 应用程序,很明显,这不是一个二元世界,你不是必须在第一天就选择一种方法并一直使用它。

你应该将微服务视为一个单体应用程序的改进。 随着项目的成熟,服务逻辑的一部分应该迁移到微服务中。 正如我们在本章学到的,这是一个有用的方法,但是要小心谨慎,以免落入一些常见的陷阱。

Flask

  1. 不同类型的测试

    • 单元测试: 确保一个类或一个函数独立地工作
    • 功能测试: 从使用者的角度验证微服务是否言行一致,即使对于错误请求,微服务也能正确运行
    • 集成测试: 验证微服务如何与其所有网络依赖项集成
    • 负载测试: 测量微服务性能
      • 当我的服务承受压力时,它是 RAM 还是主要受 cpu 限制?
      • 是否可以添加相同服务的其他实例并横向扩展?
      • 如果我的微服务调用其他服务,可以使用连接池,还是必须通过一个连接序列化所有的交互?
      • 服务能一次运行多天而不降级吗?
      • 服务在使用高峰期之后是否正常工作?
    • 端到端测试: 验证整个系统是否与端到端测试一起工作
    • 总结:
      • 功能测试是要编写的最重要的测试,并且通过在测试中实例化应用程序并与之交互,很容易在 Flask 中完成这项工作
      • 单元测试是一个很好的补充,但是不要滥用模拟
      • 集成测试类似于功能测试,但是与真正的部署相对立
      • 负载测试对于了解微服务瓶颈和规划下一步非常有用
      • 端到端测试需要使用客户端通常使用的相同 UI
  2. 使用 WebTest

  3. 使用 pytest 和 Tox

    • pytest自动发现和运行项目中的所有测试
    • Tox python多版本测试
  4. 开发者文档

  5. 持续整合

 类似资料: