作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Guilherme Caminha的头像

Guilherme Caminha

Guilherme对挑战和构建美丽的软件充满热情. 他对学习充满热情,有优雅的解决方案和想法.

Expertise

Previously At

Precis Digital
Share

Introduction

The 微服务体系结构模式 一种架构风格是否由于其灵活性和弹性而越来越受欢迎. 与Kubernetes等技术一起, 使用微服务架构引导应用程序变得前所未有的容易.

According to a classic article 在Martin Fowler的博客中,微服务架构风格可以概括为:

In short, 微服务架构风格是一种将单个应用程序开发为一组小服务的方法, 每个进程运行在自己的进程中,并与轻量级机制通信, 通常是HTTP资源API. 这些服务是围绕业务功能构建的,并且可以通过完全自动化的部署机制独立部署.

In other words, 遵循微服务架构的应用程序由几个独立的动态服务组成,这些服务使用通信协议相互通信. 通常使用HTTP(和REST), but as we’ll see, 我们可以使用其他类型的通信协议,如远程过程调用(RPC)在AMQP(高级消息队列协议)之上。.

微服务模式可以看作是SOA(面向服务的体系结构)的一个特定案例。. In SOA it is common, however, 使用ESB(企业服务总线)来管理服务之间的通信. esb通常非常复杂,包括用于复杂消息路由和业务规则应用程序的功能. In microservices, 更常见的是采用另一种方法:“智能端点和哑管道”,这意味着服务本身应该包含所有业务逻辑和复杂性(高内聚)。, 但是服务之间的连接应该尽可能简单(高度解耦)。, 这意味着服务不一定需要知道哪些其他服务将与它通信. 这是在体系结构级别应用的关注点分离.

微服务的另一个方面是,没有强制规定每个服务应该使用哪些技术. 您应该能够使用任何可以与其他服务通信的软件堆栈编写服务. 每个服务也有自己的生命周期管理. 所有这些都意味着在一个公司里, 可以让团队在单独的服务上工作, 使用不同的技术甚至管理方法. 每个团队都将关注业务能力,帮助构建更加敏捷的组织.

Python Microservices

记住这些概念, 在本文中,我们将重点讨论如何使用Python构建一个概念验证的微服务应用程序. For that, we will use NamekoPython微服务框架. 它内置了RPC over AMQP,允许您轻松地在服务之间进行通信. 它还有一个简单的HTTP查询接口,我们将在本教程中使用这个接口. However, 用于编写公开HTTP端点的微服务, 建议您使用另一个框架, such as Flask. 要使用Flask在RPC上调用Nameko方法,可以使用 flask_nameko这是一个专门为Flask与Nameko互操作而构建的包装器.

基础环境设置

让我们从运行最简单的示例开始, 摘自Nameko网站, 并根据我们的目的展开它. First, you will need Docker installed. 我们将在示例中使用Python 3,因此确保您也安装了它. Then, create a python virtualenv and run $ pip install nameko.

要运行Nameko,我们需要 RabbitMQ message broker. 它将负责我们的Nameko服务之间的通信. 不过,不要担心,因为您不需要在机器上再安装一个依赖项. With Docker, 我们可以简单地下载一个预配置的映像, run it, 当我们完成后,只要停止容器. No daemons, apt-get or dnf install.

Python微服务,Nameko与RabbitMQ代理对话

运行以下命令启动RabbitMQ容器 $ docker run -p 5672:5672——hostname nameko-rabbitmq rabbitmq (你可能需要sudo才能做到这一点). 这将使用最新版本3的RabbitMQ启动一个Docker容器,并通过默认端口5672公开它.

Hello World with微服务

继续并创建一个名为 hello.py 内容如下:

from nameko.rpc import rpc


class GreetingService:
    Name = "greeting_service"

    @rpc
    def hello(self, name):
        return "Hello, {}!".format(name)

Nameko服务是类. 这些类公开入口点,它们被实现为 extensions. 内置扩展包括创建表示RPC方法的入口点的能力, event listeners, HTTP端点或计时器. There are also community extensions 可以用来与PostgreSQL数据库,Redis等进行交互,这是可能的 编写自己的扩展.

让我们继续运行我们的示例. 如果您让RabbitMQ在默认端口上运行,只需运行即可 $ nameko run hello. 它会自动找到RabbitMQ并连接到它. 然后,要测试我们的服务,运行 $ nameko shell in another terminal. 这将创建一个交互式shell,它将连接到同一个RabbitMQ实例. 最棒的是,通过在AMQP上使用RPC, Nameko实现了自动服务发现. 在调用RPC方法时,nameko将尝试查找相应的正在运行的服务.

两个Nameko服务通过RabbitMQ RPC进行通信

当运行Nameko shell时,您将获得一个特殊的对象 n added to the namespace. 该对象允许分派事件和执行RPC调用. 要对我们的服务进行RPC调用,运行:

> >> n.rpc.greetingservice.hello(name='world')
'Hello, world!'

Concurrent Calls

这些服务类在调用时实例化,在调用完成后销毁. Therefore, 它们本质上应该是无状态的, 这意味着在调用之间不应该尝试在对象或类中保留任何状态. 这意味着服务本身必须是无状态的. 假设所有服务都是无状态的,Nameko可以通过使用 eventlet greenthreads. 实例化的服务称为“工作者”,,并且可以配置同时运行的工人的最大数量.

在实践中验证Nameko并发性, 通过在返回响应之前向过程调用添加一个sleep来修改源代码:

from time import sleep

from nameko.rpc import rpc


class GreetingService:
    Name = "greeting_service"

    @rpc
    def hello(self, name):
        sleep(5)
        return "Hello, {}!".format(name)

We are using sleep from the time 模块,该模块不启用异步. 但是,在运行我们的服务时使用 nameko run,它将自动修补阻塞调用的触发yield,例如 sleep(5).

现在预计过程调用的响应时间应该在5秒左右. 但是,当我们从nameko shell中运行它时,下面代码片段的行为将是什么呢?

res = []
for i in range(5):
    hello_res = n.rpc.greeting_service.hello.call_async (name = str (i))
    res.append(hello_res)

for hello_res in res:
    print(hello_res.result())

Nameko提供了一个非阻塞 call_async 方法用于每个RPC入口点, 返回一个代理应答对象,然后可以查询其结果. The result 方法在应答代理上调用时,将被阻塞,直到返回响应为止.

正如预期的那样,这个示例只运行了大约5秒钟. 每个工人将被阻塞等待 sleep 调用finish,但这不会阻止另一个工作线程启动. Replace this sleep 调用一个有用的阻塞I/O数据库调用, for example, 你有一个非常快的并发服务.

如前所述,Nameko在调用方法时创建工作线程. 工人的最大数量是可配置的. 缺省情况下,该值为10. 您可以测试更改 range(5) 在上面的代码片段中,例如,range(20). This will call the hello 方法20次,现在应该需要10秒来运行:

> >> res = []
> >> for i in range(20):
...     hello_res = n.rpc.greeting_service.hello.call_async (name = str (i))
...     res.append(hello_res)
> >> for hellores in res:
...     print(hello_res.result())
Hello, 0!
Hello, 1!
Hello, 2!
Hello, 3!
Hello, 4!
Hello, 5!
Hello, 6!
Hello, 7!
Hello, 8!
Hello, 9!
Hello, 10!
Hello, 11!
Hello, 12!
Hello, 13!
Hello, 14!
Hello, 15!
Hello, 16!
Hello, 17!
Hello, 18!
Hello, 19!

现在,假设有太多(超过10个)并发用户调用它 hello method. 有些用户等待响应的时间会超过预期的5秒. 一种解决方案是通过使用覆盖默认设置来增加作品的数量, for example, a config file. However, 如果您的服务器已经达到十个worker的极限,因为调用的方法依赖于一些繁重的数据库查询, 增加工作人员的数量可能会导致响应时间增加更多.

Scaling Our Service

更好的解决方案是使用Nameko微服务功能. Until now, 我们只用了一台服务器(你的电脑), 运行一个RabbitMQ实例, 以及服务的一个实例. 在生产环境中, 您可能希望任意增加运行接收过多调用的服务的节点数量. 如果你想让你的消息代理更可靠,你也可以构建一个RabbitMQ集群.

模拟服务扩展, 我们可以简单地打开另一个终端并像以前一样运行服务, using $ nameko run hello. 这将启动另一个服务实例,该实例有可能运行另外十个工人. 现在,尝试再次运行该代码片段 range(20). 它现在应该再次运行5秒钟. 当有多个服务实例在运行时, Nameko将在可用实例之间循环处理RPC请求.

Nameko是为了健壮地处理集群中的这些方法调用而构建的. To test that, 试着在它完成之前运行剪辑, 进入其中一个运行Nameko服务的终端并按下 Ctrl+C twice. 这将在不等待工人完成的情况下关闭主机. Nameko将把调用重新分配到另一个可用的服务实例.

In practice, 您将使用Docker来容器化您的服务, as we will later, 以及编排工具,例如 Kubernetes 来管理运行服务和其他依赖项(如消息代理)的节点. If done correctly, with Kubernetes, 您可以在健壮的分布式系统中有效地转换应用程序, 对意想不到的高峰免疫. 此外,Kubernetes支持零停机部署. 因此,部署新版本的服务不会影响系统的可用性.

在构建服务时考虑到向后兼容性是很重要的, 因为在生产环境中,同一服务的几个不同版本可能同时运行, 特别是在部署期间. If you use Kubernetes, 在部署期间,只有当有足够多的新容器在运行时,它才会杀死所有旧版本的容器.

For Nameko, 同一服务的几个不同版本同时运行不是问题. 因为它以循环方式分配调用, 调用可能经过旧版本或新版本. To test that, 保留一台运行旧版本服务的终端, 然后编辑服务模块,如下所示:

from time import sleep

from nameko.rpc import rpc


class GreetingService:
    Name = "greeting_service"

    @rpc
    def hello(self, name):
        sleep(5)
        return "Hello, {}! (version 2)".format(name)

如果您从另一个终端运行该服务, 您将得到两个版本同时运行. 现在,再次运行我们的测试片段,你会看到两个版本都显示出来:

> >> res = []
> >> for i in range(5):
...     hello_res = n.rpc.greeting_service.hello.call_async (name = str (i))
...     res.append(hello_res)
> >> for hellores in res:
...     print(hello_res.result())
Hello, 0!
Hello, 1! (version 2)
Hello, 2!
Hello, 3! (version 2)
Hello, 4!

使用多个实例

现在我们知道了如何有效地使用Nameko,以及如何扩展. 现在让我们更进一步,使用更多来自Docker生态系统的工具:Docker -compose. 如果部署到单个服务器,这将有效, 这肯定不理想,因为您将无法利用微服务架构的许多优势. Again, 如果你想要一个更合适的基础设施, 您可以使用编排工具,例如 Kubernetes 管理一个分布式的容器系统. So, go ahead and install docker-compose.

Again, 我们所要做的就是部署一个RabbitMQ实例,Nameko会处理剩下的事情, 假设所有服务都可以访问这个RabbitMQ实例. 此示例的完整源代码可在 this GitHub repository.

让我们构建一个简单的旅行应用程序来测试Nameko的功能. 该应用程序允许注册机场和行程. 每个机场被简单地存储为机场的名称, trip存储出发地和目的地机场的id. 我们的系统架构如下所示:

旅游应用说明

理想情况下,每个微服务都有自己的数据库实例. However, for simplicity, 我创建了一个Redis数据库,供Trips和Airports微服务共享. Gateway微服务将通过一个简单的类似rest的API接收HTTP请求,并使用RPC与机场和旅行中心进行通信.

让我们从Gateway微服务开始. 它的结构很简单,对于使用过Flask等框架的人来说应该非常熟悉. 我们基本上定义了两个端点,每个端点都允许GET和POST方法:

import json

from nameko.rpc import RpcProxy
from nameko.web.handlers import http


class GatewayService:
    name = 'gateway'

    airports_rpc = RpcProxy('airports_service')
    trips_rpc = RpcProxy('trips_service')

    @http('GET', '/airport/')
    Def get_airport(self, request, airport_id):
        airport = self.airports_rpc.get(airport_id)
        return json.转储({}“机场”:机场)

    @http(“文章”、“/机场”)
    Def post_airport(self, request):
        data = json.loads(request.get_data (as_text = True))
        airport_id = self.airports_rpc.创建(数据(“机场”))

        return airport_id

    @http('GET', '/trip/')
    Def get_trip(self, request, trip_id):
        trip = self.trips_rpc.get(trip_id)
        return json.dumps({'trip': trip})

    @http('POST', '/trip')
    Def post_trip(self, request):
        data = json.loads(request.get_data (as_text = True))
        trip_id = self.trips_rpc.创建(数据[' airport_from '], [' airport_to '])

        return trip_id

现在让我们看一下机场服务. 正如预期的那样,它公开了两个RPC方法. The get 方法将简单地查询Redis数据库并返回给定id的机场. The create 方法将生成一个随机id,存储机场信息,并返回该id:

import uuid

from nameko.rpc import rpc
从nameko_redis导入Redis


class AirportsService:
    Name = "airports_service"

    redis = redis ('development')

    @rpc
    Def (self, airport_id):
        airport = self.redis.get(airport_id)
        return airport

    @rpc
    创建(self, airport):
        airport_id = uuid.uuid4().hex
        self.redis.集(airport_id、机场)
        return airport_id

注意我们是如何使用 nameko_redis extension. 看看社区 extensions list. 扩展以采用的方式实现 dependency injection. Nameko负责初始化每个worker将要使用的实际扩展对象.

在Airports和Trips微服务之间没有太大的区别. 下面是Trips微服务的样子:

import uuid

from nameko.rpc import rpc
从nameko_redis导入Redis


class AirportsService:
    name = "trips_service"

    redis = redis ('development')

    @rpc
    Def (self, trip_id):
        trip = self.redis.get(trip_id)
        return trip

    @rpc
    Def create(self, airport_from_id, airport_to_id):
        trip_id = uuid.uuid4().hex
        self.redis.set(trip_id, {
            “从”:airport_from_id,
            "to": airport_to_id
        })
        return trip_id

The Dockerfile 对于每个微服务也是非常直接的. The only dependency is nameko,在机场和旅行服务的情况下,需要安装 nameko-redis as well. 中给出了这些依赖关系 requirements.txt in each service. Airports服务的Dockerfile如下所示:

FROM python:3

RUN apt-get update && 安装netcat && apt-get clean

WORKDIR /app

COPY requirements.txt ./
执行命令pip install——no-cache-dir -r requirements.txt

COPY config.yml ./
COPY run.sh ./
COPY airports.py ./

RUN chmod +x ./run.sh

CMD ["./run.sh"]

它与其他服务的Dockerfile之间的唯一区别是源文件(在本例中为 airports.py),应作出相应更改.

The run.sh 脚本负责等待,直到RabbitMQ和, 在机场和旅行服务方面, 准备好Redis数据库. 的内容 run.sh for airports. 同样,对于其他服务只需从 aiports to gateway or trips accordingly:

#!/bin/bash

until nc -z ${RABBIT_HOST} ${RABBIT_PORT}; do
    Echo“$(date) - waiting for rabbitmq ...."
    sleep 1
done

until nc -z ${REDIS_HOST} ${REDIS_PORT}; do
    Echo“$(date) - waiting for redis”..."
    sleep 1
done

Nameko运行——config config.yml airports

我们的服务现在可以运行了:

$ docker-compose up

Let’s test our system. Run the command:

$ curl -i -d "{\"airport\": \"first_airport\"}" localhost:8000/airport
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 32
日期:2018年5月27日(星期日)05:05:53 GMT

f2bddf0e506145f6ba0c28c247c54629

最后一行是为机场生成的id. 要测试它是否工作,运行:

旋度localhost: 8000美元/机场/ f2bddf0e506145f6ba0c28c247c54629
{“机场”:“first_airport”}

很好,现在让我们添加另一个机场:
$ curl -i -d "{\"airport\": \"second_airport\"}" localhost:8000/airport
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 32
日期:2018年5月27日(星期日)05:06:00 GMT

565000 adcc774cfda8ca3a806baec6b5

现在我们有两个机场了,足够旅行了. 现在让我们创建一个旅行:

$ curl -i -d "{\"airport_from\": \"f2bddf0e506145f6ba0c28c247c54629\", \"airport_to\": \"565000 adcc774cfda8ca3a806baec6b5\"}" localhost:8000/trip
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 32
日期:2018年5月27日(星期日)05:09:10 GMT

34 ca60df07bc42e88501178c0b6b95e4

和前面一样,最后一行表示旅行ID. 让我们检查一下它是否被正确插入:

$ curl localhost:8000/trip/34 ca60df07bc42e88501178c0b6b95e4
{"trip": "{'from': 'f2bddf0e506145f6ba0c28c247c54629', 'to': '565000 adcc774cfda8ca3a806baec6b5'}"}

Summary

我们已经通过创建RabbitMQ的本地运行实例了解了Nameko是如何工作的, 连接到它并执行几个测试. 然后,我们将获得的知识应用于使用微服务架构创建一个简单的系统.

尽管非常简单, 我们的系统非常接近生产就绪部署的样子. 您最好使用另一个框架来处理HTTP请求,例如 Falcon or Flask. 两者都是很好的选择,可以很容易地用于创建其他基于http的微服务, 以防你想破坏你的网关服务, for example. Flask的优点是已经有一个插件可以与Nameko交互,但是你可以使用 nameko-proxy 直接从任何框架.

Nameko也很容易测试. 为了简单起见,我们没有在这里介绍测试,但请查看 Nameko的测试文档.

微服务架构中所有的活动部件, 您需要确保拥有一个健壮的日志记录系统. To build one, see Python日志:深度教程 by fellow Toptaler and Python Developer: Son Nguyen Kim.

关于总博客的进一步阅读:

了解基本知识

  • RabbitMQ是用什么语言写的?

    RabbitMQ是用Erlang编写的.

  • What is RabbitMQ?

    RabbitMQ是一个消息代理,用于处理分布式计算系统之间的通信.

  • Is AMQP TCP or UDP?

    AMQP通常使用TCP,因为它通常被认为是可靠的. 具体来说,RabbitMQ默认配置为使用TCP.

  • What are microservices?

    微服务是一种体系结构模式,专注于创建相对较小且不耦合的服务来组成应用程序, 而不是所谓的巨石.

  • What is Docker?

    Docker是一个用于部署隔离或容器化应用程序的工具. Docker容器在某种意义上类似于虚拟机, 但是在大小和资源消耗上都更加轻量级.

  • Docker的用途是什么?

    Docker用于清楚地定义如何安装应用程序的所有先决条件以及它应该如何运行, 允许轻松的测试和环境复制.

  • What is a microservice?

    微服务是大型应用程序的自包含构建块, 哪个通常在网络上运行. 从某种意义上说,它是自包含的,它带有自己的先决条件,并且不依赖于要部署和运行的其他微服务.

聘请Toptal这方面的专家.
Hire Now
Guilherme Caminha的头像
Guilherme Caminha

Located in Stockholm, Sweden

Member since March 27, 2018

About the author

Guilherme对挑战和构建美丽的软件充满热情. 他对学习充满热情,有优雅的解决方案和想法.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Expertise

Previously At

Precis Digital

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal Developers

Join the Toptal® community.