当前位置: 首页 > 工具软件 > Nameko > 使用案例 >

Nameko 学习笔记

马丰
2023-12-01

一、简介

Nameko是Python的一种微服务框架。

例如:

from nameko.rpc import rpc, RpcProxy

class Service:
    name = "service"

    # we depend on the RPC interface of "another_service"
    # 其他依赖服务作为声明参数
    other_rpc = RpcProxy("another_service")

    @rpc  # `method` is exposed over RPC
    def method(self):
        # application logic goes here
        pass

二、关键概念

1.服务剖析

一个服务即Python中的一个类,这个类把服务逻辑封装到方法中,而且把任何的依赖都作为方法的参数。

endpoint(入口点)

endpoint 可以简单理解为带有@rpc标记的方法所关联的服务入口。在方法上使用了@rpc修饰的方法都将暴露给外部业务。这些endpoint一般会会监视外部事件。例如一个消息队列中的消息事件,将触发endpoint修饰的方法执行并返回结果。

worker(工作器)

worker是进入点发放被触发的时候产生的微服务类实例。但是如果有依赖,那么就会被依赖的实例代替。
一个worker实例只处理一次请求,提供的是无状态服务。
一个服务可以同时运行多个worker,但最多只能是用户预定义的并发值。

Dependency Injection(依赖注入)

服务类的依赖添加是声明式的。声明时不是使用接口,而是通过使用参数进行声明。
这个参数是一个DependencyProvider。这个东西负责提供注入到服务工作器的对象。
所有的provider都要提供get_dependency()方法生产要注入到工作器中的对象。

Concurrency (并发)

Nameko基于eventlet库,这个库实现的同步模型是基于隐式yield模式的协程,通过“绿色的线程”提供同步功能。

隐式的yield基于monkey patching基础库。当一个线程等待IO时就会触发yield。通过命令nameko run启动的服务将会应用这个模式。

每一个工作器都有自己的线程。最大的同步工作器数量可以基于每个工作器等待IO时间的总量来动态调整。

工作器都是无状态的所以天生线程安全。但是外部依赖应该确保他们每个工作线程都是用同一个依赖或者多个工作器都能安全地同步访问。

然而c扩展体系都使用socket通信,他们通常都不认为绿色线程的工作能满足线程安全。其中就包括 librabbitmq, MySQLdb等。

Extensions (扩展)

所有的入口点和依赖提供者都作为“扩展”实现。因为他们存在于服务代码之外,又不是所有服务都需要的。(例如一个纯的AMQP暴露的服务将不会使用HTTP入口点)

Nameko有大量的内建扩展,一些是有社区提供的,而你也可以实现自己的扩展。

Running Services (运行服务)

运行服务需要的所有东西:服务类和有关的配置。
最简单的运行一个或者多个服务的方法是使用Nameko命令行:

$ nameko run module:[ServiceClass]

运行某module下的所有服务或者运行某module下的特定的ServiceClass服务。

Service Containers (服务容器)

每个服务类都委托给一个ServiceContainer。这个容器封装了所有需要运行一个服务的方法,而且装载了在服务类上的任何扩展。

使用ServiceContainer运行单个服务:

from nameko.containers import ServiceContainer

class Service:
    name = "service"

# create a container
container = ServiceContainer(Service, config={})

# ``container.extensions`` exposes all extensions used by the service
service_extensions = list(container.extensions)

# start service
container.start()

# stop service
container.stop()

Service Runner (服务运行器)

ServiceRunner 是多个服务容器的简单包装,同时提供启动和停止所有包装容器的方法。这个其实是nameko run内部使用的,但这也能实现程序化控制。

from nameko.runners import ServiceRunner
from nameko.testing.utils import get_container

class ServiceA:
    name = "service_a"

class ServiceB:
    name = "service_b"

# create a runner for ServiceA and ServiceB
runner = ServiceRunner(config={})
runner.add_service(ServiceA)
runner.add_service(ServiceB)

# ``get_container`` will return the container for a particular service
container_a = get_container(runner, ServiceA)

# start both services
runner.start()

# stop both services
runner.stop()

三、安装

Install nameko

$ pip install nameko

或者

$ git clone git@github.com:nameko/nameko.git
$ python setup.py install

安装rabbitmq

$ brew install rabbitmq
$ apt-get install rabbitmq-server

四、命令行接口

Running a Service (运行服务)

$ nameko run <module>[:<ServiceClass>]

这个命令将会在前台启动服务并且运行到进程终止。

也可以用–config选项重写默认配置,并提供一个YAML 格式的配置文件

# foobar.yaml

AMQP_URI: 'pyamqp://guest:guest@localhost'
WEB_SERVER_ADDRESS: '0.0.0.0:8000'
rpc_exchange: 'nameko-rpc'
max_workers: 10
parent_calls_tracked: 10

LOGGING:
    version: 1
    handlers:
        console:
            class: logging.StreamHandler
    root:
        level: DEBUG
        handlers: [console]

LOGGING 项会传递到logging.config.dictConfig(),而且会适应调用的样式。

配置值可以通过内建的Config依赖提供器读取。

Environment variable substitution (环境变量解决方案)

YAML配置文件为环境变量提供了基本的支持。可以使用bash风格的语法:$ {ENV_VAR},另外还可以提供默认值 $ {ENV_VAR:default_value}

# foobar.yaml
AMQP_URI: pyamqp://${RABBITMQ_USER:guest}:${RABBITMQ_PASSWORD:password}@${RABBITMQ_HOST:localhost}

使用环境变量的运行方式示例:

$ RABBITMQ_USER=user RABBITMQ_PASSWORD=password RABBITMQ_HOST=host nameko run --config ./foobar.yaml <module>[:<ServiceClass>]

如果需要在YAML文件里使用引号(引号里使用环境变量),显式声明!env_var处理器是必须的

# foobar.yaml
AMQP_URI: !env_var "pyamqp://${RABBITMQ_USER:guest}:${RABBITMQ_PASSWORD:password}@${RABBITMQ_HOST:localhost}"

Interacting with running services (与运行服务交互)

$ nameko shell

为了与远程服务工作,运行了一个交互式python脚本环境。规范的交互式解释器,提供一个添加了内建命名空间的特殊模块n,用来支持RPC调用和分发事件。

发起RPC到目标服务

$ nameko shell
>>> n.rpc.target_service.target_method(...)
# RPC response

作为源服务分发事件

$ nameko shell
>>> n.dispatch_event("source_service", "event_type", "event_payload")

五、Built-in Extensions(内建扩展)

RPC

Nameko包含了一个基于AMQP的RPC实现。它包括@rpc入口点,一个与其他服务对话的代理,以及一个非Nameko客户端也能发起RPC调用到集群的独立的代理。

from nameko.rpc import rpc, RpcProxy

class ServiceY:
    name = "service_y"

    @rpc
    def append_identifier(self, value):
        return u"{}-y".format(value)


class ServiceX:
    name = "service_x"

    y = RpcProxy("service_y")

    @rpc
    def remote_method(self, value):
        res = u"{}-x".format(value)
        return self.y.append_identifier(res)
from nameko.standalone.rpc import ClusterRpcProxy

config = {
    'AMQP_URI': AMQP_URI  # e.g. "pyamqp://guest:guest@localhost"
}

with ClusterRpcProxy(config) as cluster_rpc:
    cluster_rpc.service_x.remote_method("hellø")  # "hellø-x-y"

一般的rpc调用会一直阻塞直到远程方法完成为止。但是代理也提供了一个异步调用模式到后台或者并行化RPC调用:

with ClusterRpcProxy(config) as cluster_rpc:
    hello_res = cluster_rpc.service_x.remote_method.call_async("hello")
    world_res = cluster_rpc.service_x.remote_method.call_async("world")
    # do work while waiting
    hello_res.result()  # "hello-x-y"
    world_res.result()  # "world-x-y"

在一个集群里面拥有超过一个的目标服务实例,RPC请求在这些实例间循环。请求只会由目标服务中的一个实例来处理。
AMQP消息只会在请求被成功处理后才被确认。如果服务确认消息失败,AMQP连接关闭broker将重试调用然后分发消息到可用的服务实例上。
请求和相应的负载为了通过网线传输而被序列化到JSON。

Events (事件发布订阅)

Nameko 事件是一个异步的消息系统,实现了发布订阅模式。服务分发事件,而这些事件可以被0到多个其他服务所接收。

from nameko.events import EventDispatcher, event_handler
from nameko.rpc import rpc

class ServiceA:
    """ Event dispatching service. """
    name = "service_a"

    dispatch = EventDispatcher()

    @rpc
    def dispatching_method(self, payload):
        self.dispatch("event_type", payload)


class ServiceB:
    """ Event listening service. """
    name = "service_b"

    @event_handler("service_a", "event_type")
    def handle_event(self, payload):
        print("service b received:", payload)

EventHandler进入点有三个处理器类型决定事件消息是如何被一个集群接收的;
SERVICE_POOL: 所有事件处理器通过服务名称联合在一起,并且每个池中只有一个实例接收到事件,类似于RPC进入点的集群行为。这是默认的处理类型。
BROADCAST:每个监听服务实例都会接收到事件。
SINGLETON:只有一个监听服务实例会接收到事件。

广播例子:

from nameko.events import BROADCAST, event_handler

class ListenerService:
    name = "listener"

    @event_handler(
        "monitor", "ping", handler_type=BROADCAST, reliable_delivery=False
    )
    def ping(self, payload):
        # all running services will respond
        print("pong from {}".format(self.name))

为了通过网络传输,事件都被序列化成了JSON。

HTTP GET & POST

Nameko 的HTTP进入点支持简单的GET和POST

# http.py

import json
from nameko.web.handlers import http

class HttpService:
    name = "http_service"

    @http('GET', '/get/<int:value>')
    def get_method(self, request, value):
        return json.dumps({'value': value})

    @http('POST', '/post')
    def do_post(self, request):
        return u"received: {}".format(request.get_data(as_text=True))

启动服务

$nameko run http
starting services: http_service

测试服务

$ curl -i localhost:8000/get/42
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 13
Date: Fri, 13 Feb 2015 14:51:18 GMT

{'value': 42}
$ curl -i -d "post body" localhost:8000/post
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 19
Date: Fri, 13 Feb 2015 14:55:01 GMT

received: post body

HTTP进入点是基于werkzeug库的。服务方法必须返回以下任一值:

一个字符串,将变成响应实体response body
一个二元组,(status code, response body)
一个三元组,(status_code, headers dict, response body)
一个werkzeug.wrappers.Response实例

例如:

# advanced_http.py

from nameko.web.handlers import http
from werkzeug.wrappers import Response

class Service:
    name = "advanced_http_service"

    @http('GET', '/privileged')
    def forbidden(self, request):
        return 403, "Forbidden"

    @http('GET', '/headers')
    def redirect(self, request):
        return 201, {'Location': 'https://www.example.com/widget/1'}, ""

    @http('GET', '/custom')
    def custom(self, request):
        return Response("payload")

运行服务

$ nameko run advanced_http
starting services: advanced_http_service

测试:

$ curl -i localhost:8000/privileged
HTTP/1.1 403 FORBIDDEN
Content-Type: text/plain; charset=utf-8
Content-Length: 9
Date: Fri, 13 Feb 2015 14:58:02 GMT
curl -i localhost:8000/headers
HTTP/1.1 201 CREATED
Location: https://www.example.com/widget/1
Content-Type: text/plain; charset=utf-8
Content-Length: 0
Date: Fri, 13 Feb 2015 14:58:48 GMT

可以通过重写response_from_exception()方法格式化控制的错误返回(服务器异常控制)。

import json
from nameko.web.handlers import HttpRequestHandler
from werkzeug.wrappers import Response
from nameko.exceptions import safe_for_serialization


class HttpError(Exception):
    error_code = 'BAD_REQUEST'
    status_code = 400


class InvalidArgumentsError(HttpError):
    error_code = 'INVALID_ARGUMENTS'

#重写异常处理
class HttpEntrypoint(HttpRequestHandler):
    def response_from_exception(self, exc):
        if isinstance(exc, HttpError):
            response = Response(
                json.dumps({
                    'error': exc.error_code,
                    'message': safe_for_serialization(exc),
                }),
                status=exc.status_code,
                mimetype='application/json'
            )
            return response
        return HttpRequestHandler.response_from_exception(self, exc)


http = HttpEntrypoint.decorator


class Service:
    name = "http_service"

    @http('GET', '/custom_exception')
    def custom_exception(self, request):
        raise InvalidArgumentsError("Argument `foo` is required.")

运行

$ nameko run http_exceptions
starting services: http_service

测试

$ curl -i http://localhost:8000/custom_exception
HTTP/1.1 400 BAD REQUEST
Content-Type: application/json
Content-Length: 72
Date: Thu, 06 Aug 2015 09:53:56 GMT

{"message": "Argument `foo` is required.", "error": "INVALID_ARGUMENTS"}

Timer(计时器)

计时器是一个每达到可配置的秒数时刻就触发的简单入口点。计时器是非集群定制的,而且在所有的服务实例都会触发。

from nameko.timer import timer

class Service:
    name ="service"

    @timer(interval=1)
    def ping(self):
        # method executed every second
        print("pong")

六、Built-in Dependency Providers

Nameko 包含了一些常用的Dependency Providers。

Config

Config是一个简单的依赖提供器,提供了在运行时只读取配置值的能力。

from nameko.dependency_providers import Config
from nameko.web.handlers import http


class Service:

    name = "test_config"

    config = Config()

    @property
    def foo_enabled(self):
        return self.config.get('FOO_FEATURE_ENABLED', False)

    @http('GET', '/foo')
    def foo(self, request):
        if not self.foo_enabled:
            return 403, "FeatureNotEnabled"

        return 'foo'

七、Community(社区支持)

社区有大量不是核心项目但你偶尔会发现在开发自己的nameko服务是很有用的nameko扩展和补充的库。

扩展
nameko-sqlarchemy
nameko-sentry
nameko-amqp-retry
nameko-bayeux-client
nameko-slack
nameko-eventlog-dispatcher
nameko-redis-py
nameko-redis
nameko-statsd

补充库
django-nameko
flask_nameko
nameko-proxy

八、Testing Services(测试服务)

Nameko规约设计得很容易进行测试。服务可能很小且功能单一,而且依赖注入使得她很简单就可以替换及分离函数片段。nameko自己的测试套件使用pytest库。

Unit Test(单元测试)

单元测试意味着分离地测试一个单一的服务。例如没有了任何或者大部分的依赖。

worker_factory()工具将从一个服务类创建工作器,并使用mock.MagicMock创建的对象替换掉原来的依赖。依赖函数可以通过side_effect和return_value伪造。

""" Service unit testing best practice.
"""

from nameko.rpc import RpcProxy, rpc
from nameko.testing.services import worker_factory


class ConversionService(object):
    """ Service under test
    """
    name = "conversions"

    maths_rpc = RpcProxy("maths")

    @rpc
    def inches_to_cm(self, inches):
        return self.maths_rpc.multiply(inches, 2.54)

    @rpc
    def cms_to_inches(self, cms):
        return self.maths_rpc.divide(cms, 2.54)


def test_conversion_service():
    # create worker with mock dependencies
    service = worker_factory(ConversionService)

    # add side effects to the mock proxy to the "maths" service
    service.maths_rpc.multiply.side_effect = lambda x, y: x * y
    service.maths_rpc.divide.side_effect = lambda x, y: x / y

    # test inches_to_cm business logic
    assert service.inches_to_cm(300) == 762
    service.maths_rpc.multiply.assert_called_once_with(300, 2.54)

    # test cms_to_inches business logic
    assert service.cms_to_inches(762) == 300
    service.maths_rpc.divide.assert_called_once_with(762, 2.54)

有些情况下使用一个代替性的依赖比伪造依赖更有用。这可能是一个全功能的替换或者一个轻量级的提供部分功能的垫片。

""" Service unit testing best practice, with an alternative dependency.
"""

import pytest
from sqlalchemy import Column, Integer, String, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

from nameko.rpc import rpc
from nameko.testing.services import worker_factory

# using community extension from http://pypi.python.org/pypi/nameko-sqlalchemy
from nameko_sqlalchemy import Session


Base = declarative_base()


class Result(Base):
    __tablename__ = 'model'
    id = Column(Integer, primary_key=True)
    value = Column(String(64))


class Service:
    """ Service under test
    """
    name = "service"

    db = Session(Base)

    @rpc
    def save(self, value):
        result = Result(value=value)
        self.db.add(result)
        self.db.commit()

@pytest.fixture
def session():
    """ Create a test database and session
    """
    engine = create_engine('sqlite:///:memory:')
    Base.metadata.create_all(engine)
    session_cls = sessionmaker(bind=engine)
    return session_cls()


def test_service(session):

    # create instance, providing the test database session
    service = worker_factory(Service, db=session)

    # verify ``save`` logic by querying the test database
    service.save("helloworld")
    assert session.query(Result.value).all() == [("helloworld",)]

Integration Testing(集成测试)

在nameko中集成测试意味着在数个服务间测试接口。建议的方法是以正常的方式运行所有的被测试服务,然后使用帮助类触发入口点的行为。

""" Service integration testing best practice.
"""

from nameko.rpc import rpc, RpcProxy
from nameko.testing.utils import get_container
from nameko.testing.services import entrypoint_hook


class ServiceX:
    """ Service under test
    """
    name = "service_x"

    y = RpcProxy("service_y")

    @rpc
    def remote_method(self, value):
        res = "{}-x".format(value)
        return self.y.append_identifier(res)


class ServiceY:
    """ Service under test
    """
    name = "service_y"

    @rpc
    def append_identifier(self, value):
        return "{}-y".format(value)


def test_service_x_y_integration(runner_factory, rabbit_config):

    # run services in the normal manner
    runner = runner_factory(rabbit_config, ServiceX, ServiceY)
    runner.start()

    # artificially fire the "remote_method" entrypoint on ServiceX
    # and verify response
    container = get_container(runner, ServiceX)
    with entrypoint_hook(container, "remote_method") as entrypoint:
        assert entrypoint("value") == "value-x-y"

注意这里在ServiceX和ServiceY之间的接口就像在正常操作一样。

对于一个特殊的测试,接口如果在超出测试范围的,可以使用下面其中一个测试帮助器进行禁用。

retrict_entrypoints

nameko.testing.services.restrict_entrypoints(container, *entrypoints)

限制在container的进入点为特定的名称的进入点。
这些方法必须在容器启动前被调用。
用法
下面的服务定义有两个进入点:

class Service(object):
    name = "service"

    @timer(interval=1)
    def foo(self, arg):
        pass

    @rpc
    def bar(self, arg)
        pass

    @rpc
    def baz(self, arg):
        pass

container = ServiceContainer(Service, config)

禁用在foo上的定时器进入点,只留下RPC进入点

restrict_entrypoints(container, "bar", "baz")

注意不可能单独地把多个进入点看成同一个方法。

replace_dependencies

nameko.testing.services.replace_dependencies(container, *dependencies, **dependency_map)

替换依赖提供器在容器上使用伪造的依赖提供器。
在*dependencies声明的依赖将由一个MockDependencyProvider替换,这个MockDependencyProvider会注入一个魔术伪造器而不是依赖。
另外,你可能使用关键字参数命名依赖,而且提供MockDependencyProvider应该注入的代替的值。
为每个*dependencies声明的依赖返回MockDependencyProvider.dependency,这样对替换的依赖调用就能被检查出来。如果只有一个依赖被替换,则返回一个单一的对象,否则由生成器yield出在*dependencies声明的依赖。注意任何在**dependency_map指出的替换的依赖都不会被返回。

替换是在容器实例里执行的,而且对服务类没有影响。所以新的容器实例不会影响旧的容器实例。

用法

from nameko.rpc import RpcProxy, rpc
from nameko.standalone.rpc import ServiceRpcProxy

class ConversionService(object):
    name = "conversions"

    maths_rpc = RpcProxy("maths")

    @rpc
    def inches_to_cm(self, inches):
        return self.maths_rpc.multiply(inches, 2.54)

    @rpc
    def cm_to_inches(self, cms):
        return self.maths_rpc.divide(cms, 2.54)

container = ServiceContainer(ConversionService, config)
mock_maths_rpc = replace_dependencies(container, "maths_rpc")
mock_maths_rpc.divide.return_value = 39.37

container.start()

with ServiceRpcProxy('conversions', config) as proxy:
    proxy.cm_to_inches(100)

# assert that the dependency was called as expected
mock_maths_rpc.divide.assert_called_once_with(100, 2.54)

通过关键字指定特殊的替换

class StubMaths(object):

    def divide(self, val1, val2):
        return val1 / val2

replace_dependencies(container, maths_rpc=StubMaths())

container.start()

with ServiceRpcProxy('conversions', config) as proxy:
    assert proxy.cm_to_inches(127) == 50.0

Other Helpers(其他帮助方法)

entrypoint_hook

提供了context_data模仿特殊调用上下文的方法。

import pytest

from nameko.contextdata import Language
from nameko.rpc import rpc
from nameko.testing.services import entrypoint_hook


class HelloService:
    """ Service under test
    """
    name = "hello_service"

    language = Language()

    @rpc
    def hello(self, name):
        greeting = "Hello"
        if self.language == "fr":
            greeting = "Bonjour"
        elif self.language == "de":
            greeting = "Gutentag"

        return "{}, {}!".format(greeting, name)


@pytest.mark.parametrize("language, greeting", [
    ("en", "Hello"),
    ("fr", "Bonjour"),
    ("de", "Gutentag"),
])
def test_hello_languages(language, greeting, container_factory, rabbit_config):

    container = container_factory(HelloService, rabbit_config)
    container.start()

    context_data = {'language': language}
    with entrypoint_hook(container, 'hello', context_data) as hook:
        assert hook("Matt") == "{}, Matt!".format(greeting)

entrypoint_waiter

提供了控阻塞调用的测试方法。

from nameko.events import event_handler
from nameko.standalone.events import event_dispatcher
from nameko.testing.services import entrypoint_waiter


class ServiceB:
    """ Event listening service.
    """
    name = "service_b"

    @event_handler("service_a", "event_type")
    def handle_event(self, payload):
        print("service b received", payload)


def test_event_interface(container_factory, rabbit_config):

    container = container_factory(ServiceB, rabbit_config)
    container.start()

    dispatch = event_dispatcher(rabbit_config)

    # prints "service b received payload" before "exited"
    with entrypoint_waiter(container, 'handle_event'):
        dispatch("service_a", "event_type", "payload")
    print("exited")

 类似资料: