See an article use and precautions , there is a description of initiating remote calls
Immediately after rpc is the value of the class variable name when the microservice is defined, which is the name of the microservice, followed by the rpc method, using call_async as an asynchronous call, and when calling result_async.result(), it will wait for the asynchronous task to return the result. It should be noted, is created when you run ClusterRpcProxy (config) and the connection queue, the operation is relatively time-consuming, if there are a large number of micro-service calls should not be repeated to create a connection, all calls should be completed within a block of statements . The result of an asynchronous call can only be obtained within the statement block, that is, calling .result() to wait for the result. The connection is disconnected outside the statement block and cannot be obtained.
It probably means that ClusterRpcProxy
is frequently instantiated. Combined with the strange phenomenon I saw in the RabbitMQ background, I feel it is necessary to study it:
The following are common nameko
calls
api.py
from fastapi import FastAPI
from rpc import (
upload_service_rpc
)
app = FastAPI()
@app.get('/')
async def root():
return {"message": "Hello World"}
@app.post('/upload/')
def upload(data: UploadRequestBody):
logger.debug(data.json(ensure_ascii=False))
success: bool = upload_service_rpc(data) # 此处发起 rpc 调用
return {
'status': success
}
upload_service_rpc
method is a function wrapper nameko
provided by ClusterRpcProxy
rpc.py
from nameko.standalone.rpc import ClusterRpcProxy
import settings
from schemas import (
UploadRequestBody,
)
from loguru import logger
config = {
'AMQP_URI': f'amqp://{settings.AMQP_URI.RABBIT_USER}:'
f'{settings.AMQP_URI.RABBIT_PASSWORD}@{settings.AMQP_URI.RABBIT_HOST}:'
f'{settings.AMQP_URI.RABBIT_PORT}/{settings.AMQP_URI.RABBIT_VHOST}'
}
def upload_service_rpc(data: UploadRequestBody) -> bool:
""" 给 fatapi 暴露的 rpc 接口 """
with ClusterRpcProxy(config) as cluster_rpc: # 通过 ClusterRpcProxy 发起 RPC 请求
success: bool = cluster_rpc.console_service.upload(
data=data.json(ensure_ascii=False)
)
return success
But the wording of the above, looks perfect, but nameko
realize that each instantiation of ClusterRpcProxy
will be in RabbitMQ
create a new in queue
, if every time we rpc
request wants above code as frequent instances of ClusterRpcProxy
cause a lot of Time wasted creating queue
.
The figure is RabbmitMQ
screenshot of the admin interface, you can see when multiple requests initiated when there will be a large number of rpc.reply-standalone_rpc_proxy_{routing_key}
format queue
These rpc.reply-standalone_rpc_proxy_{routing_key}
queues will be closed after a few seconds after there are no messages and will not exist forever
Next, modify the code:
api.py
import settings
from loguru import logger
from fastapi import FastAPI
from schemas import (
UploadRequestBody
)
from rpc import (
init_rpc_proxy
)
app = FastAPI()
rpc_proxy = init_rpc_proxy() # 把 rpc_proxy 对象变成一个全局变量,生命周期伴随整个程序
@app.post('/upload/')
def upload(data: UploadRequestBody):
logger.debug(data.json(ensure_ascii=False))
success: bool = rpc_proxy.console_service.upload( # 执行 rpc 调用 console_service 的 upload 方法
data=data.json(ensure_ascii=False)
)
return {
'status': success
}
rpc.py
# coding=utf-8
from nameko.standalone.rpc import ClusterRpcProxy
import settings
from schemas import (
UploadRequestBody,
)
from loguru import logger
config = {
'AMQP_URI': f'amqp://{settings.AMQP_URI.RABBIT_USER}:'
f'{settings.AMQP_URI.RABBIT_PASSWORD}@{settings.AMQP_URI.RABBIT_HOST}:'
f'{settings.AMQP_URI.RABBIT_PORT}/{settings.AMQP_URI.RABBIT_VHOST}'
}
def init_rpc_proxy():
return ClusterRpcProxy(config) # init_rpc_proxy 只负责返回对象,不执行代码
But when we execute the new code above, we get an error
AttributeError: 'ClusterRpcProxy' object has no attribute 'console_service'
why? The reason is that ClusterRpcProxy class __enter__
methods, but we do not use with
context manager, they will not perform __enter__
content methods, and secret lies in __enter__
approach, let's look at __enter__
what methods have it!
nameko/standalone/rpc.py
class StandaloneProxyBase(object): # StandaloneProxyBase 是 ClusterRpcProxy 的父类
class ServiceContainer(object):
""" Implements a minimum interface of the
:class:`~containers.ServiceContainer` to be used by the subclasses
and rpc imports in this module.
"""
service_name = "standalone_rpc_proxy"
def __init__(self, config):
self.config = config
self.shared_extensions = {}
class Dummy(Entrypoint):
method_name = "call"
_proxy = None
def __init__(
self, config, context_data=None, timeout=None,
reply_listener_cls=SingleThreadedReplyListener
):
container = self.ServiceContainer(config)
self._worker_ctx = WorkerContext(
container, service=None, entrypoint=self.Dummy,
data=context_data)
self._reply_listener = reply_listener_cls(
timeout=timeout).bind(container)
def __enter__(self):
return self.start()
def __exit__(self, tpe, value, traceback):
self.stop()
def start(self):
self._reply_listener.setup()
return self._proxy
def stop(self):
self._reply_listener.stop()
class ClusterRpcProxy(StandaloneProxyBase):
def __init__(self, *args, **kwargs):
super(ClusterRpcProxy, self).__init__(*args, **kwargs)
self._proxy = ClusterProxy(self._worker_ctx, self._reply_listener)
StandaloneProxyBase
is the parent class of ClusterRpcProxy. You can see that the __enter__
method executes return self.start()
, and the start method returns return self._proxy
instead of the common return self
, so this leads to our previous error.
Once you know the cause of the problem, you can fix it quickly!
api.py
import settings
from loguru import logger
from fastapi import FastAPI
from schemas import (
UploadRequestBody
)
from rpc import (
init_rpc_proxy
)
app = FastAPI()
_rpc_proxy = init_rpc_proxy() # 区分两个 _rpc_proxy 和 rpc_proxy
rpc_proxy = _rpc_proxy.start()
@app.post('/upload/')
def upload(data: UploadRequestBody):
logger.debug(data.json(ensure_ascii=False))
# success: bool = upload_service_rpc2(data)
success: bool = rpc_proxy.console_service.upload( # 使用 rpc_proxy 调用 rpc 方法
data=data.json(ensure_ascii=False)
)
return {
'status': success
}
rpc.py
# coding=utf-8
from nameko.standalone.rpc import ClusterRpcProxy
import settings
from schemas import (
UploadRequestBody,
)
from loguru import logger
config = {
'AMQP_URI': f'amqp://{settings.AMQP_URI.RABBIT_USER}:'
f'{settings.AMQP_URI.RABBIT_PASSWORD}@{settings.AMQP_URI.RABBIT_HOST}:'
f'{settings.AMQP_URI.RABBIT_PORT}/{settings.AMQP_URI.RABBIT_VHOST}'
}
def init_rpc_proxy():
return ClusterRpcProxy(config)
Well, let's look at the speed difference before and after:
Test code:
import requests
data = {
# 隐藏了这部分内容
}
for i in range(20):
response = requests.post('http://localhost:63000/upload/', json=data)
print(response.status_code, response.text)
Run the loop 20 times:
before fixing:
─➤ time python test_api.py
200 {"status":true}
200 {"status":true}
200 {"status":true}
200 {"status":true}
200 {"status":true}
200 {"status":true}
200 {"status":true}
200 {"status":true}
200 {"status":true}
200 {"status":true}
200 {"status":true}
200 {"status":true}
200 {"status":true}
200 {"status":true}
200 {"status":true}
200 {"status":true}
200 {"status":true}
200 {"status":true}
200 {"status":true}
200 {"status":true}
python test_api.py 0.14s user 0.05s system 1% cpu 14.696 total
After modification:
─➤ time python test_api.py
200 {"status":true}
200 {"status":true}
200 {"status":true}
200 {"status":true}
200 {"status":true}
200 {"status":true}
200 {"status":true}
200 {"status":true}
200 {"status":true}
200 {"status":true}
200 {"status":true}
200 {"status":true}
200 {"status":true}
200 {"status":true}
200 {"status":true}
200 {"status":true}
200 {"status":true}
200 {"status":true}
200 {"status":true}
200 {"status":true}
python test_api.py 0.14s user 0.05s system 2% cpu 7.271 total
Because it avoids creating a queue
for each RPC request, the speed is greatly improved.
14
seconds compared to 7
seconds, double the speed!
thread safety
But it should be noted that ClusterProxy is not concurrently safe, as you can see from the comments in the source code, A single-threaded RPC proxy to a cluster of services.
nameko/standalone/rpc.py
class ClusterProxy(object):
"""
A single-threaded RPC proxy to a cluster of services. Individual services
are accessed via attributes, which return service proxies. Method calls on
the proxies are converted into RPC calls to the service, with responses
returned directly.
Enables services not hosted by nameko to make RPC requests to a nameko
cluster. It is commonly used as a context manager but may also be manually
started and stopped.
This is similar to the service proxy, but may be uses a single reply queue
for calls to all services, where a collection of service proxies would have
one reply queue per proxy.
*Usage*
As a context manager::
with ClusterRpcProxy(config) as proxy:
proxy.service.method()
proxy.other_service.method()
The equivalent call, manually starting and stopping::
proxy = ClusterRpcProxy(config)
proxy = proxy.start()
proxy.targetservice.method()
proxy.other_service.method()
proxy.stop()
If you call ``start()`` you must eventually call ``stop()`` to close the
connection to the broker.
You may also supply ``context_data``, a dictionary of data to be
serialised into the AMQP message headers, and specify custom worker
context class to serialise them.
When the name of the service is not legal in Python, you can also
use a dict-like syntax::
with ClusterRpcProxy(config) as proxy:
proxy['service-name'].method()
proxy['other-service'].method()
"""
Therefore, it is not feasible to use the global singleton ClusterProxy concurrently in a multi-threaded or coroutine environment. At this time, we can refer to the connection pool of the database and create a multi-threaded concurrent safe connection pool of ClusterProxy.
The reference code is as follows:
rpc.py
# coding=utf-8
from nameko.standalone.rpc import ClusterRpcProxy
import settings
import threading
import queue
from nameko.standalone.rpc import (ClusterProxy, ClusterRpcProxy)
config = {
'AMQP_URI': f'amqp://{settings.AMQP_URI.RABBIT_USER}:'
f'{settings.AMQP_URI.RABBIT_PASSWORD}@{settings.AMQP_URI.RABBIT_HOST}:'
f'{settings.AMQP_URI.RABBIT_PORT}/{settings.AMQP_URI.RABBIT_VHOST}'
}
def synchronized(func):
func.__lock__ = threading.Lock()
def lock_func(*args, **kwargs):
with func.__lock__:
return func(*args, **kwargs)
return lock_func
class RpcProxyPool:
queue = queue.Queue()
@synchronized
def get_connection(self) -> ClusterProxy:
if self.queue.empty():
conn = self.create_connection()
self.queue.put(conn)
return self.queue.get()
def init_rpc_proxy(self):
return ClusterRpcProxy(config)
@synchronized
def create_connection(self) -> ClusterProxy:
_rpc_proxy: ClusterRpcProxy = self.init_rpc_proxy()
rpc_proxy: ClusterProxy = _rpc_proxy.start()
return rpc_proxy
@synchronized
def put_connection(self, conn: ClusterProxy) -> bool:
if isinstance(conn, ClusterProxy):
self.queue.put(conn)
return True
return False
api.py
from loguru import logger
from fastapi import FastAPI
from schemas import (
AddStruct
)
from rpc import (
RpcProxyPool
)
app = FastAPI(
)
pool = RpcProxyPool()
@app.get('/')
async def root():
return {"message": "Hello World"}
@app.post('/upload/')
def upload(data: AddStruct):
logger.debug(data.dict())
rpc_proxy = pool.get_connection()
c: int = rpc_proxy.add_service.add(
data.a, data.b
)
pool.put_connection(rpc_proxy)
return {
'r': c
}
Wrote a RpcProxyPool
, unbounded connection pool
The flow chart is roughly as follows, although the drawing is very bad
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。