在本文中,我们将尝试回答以下关于 Rasa 的动作服务器的问题:
假设我们有一个自定义操作,它将调用后端服务,这可能需要一段时间才能完成,例如检索客户过去的购买记录,检查预约的可用时间段,调用机器学习模型进行预测,等等。
在处理长时间运行的自定义操作时,操作服务器会被阻止为其他请求提供服务吗?
我假设读者熟悉 Python 中异步函数调用的基础知识。
重现本文中描述的结果的代码可以在这里找到。
对上一节提出的问题的一个常见回答是:
动作服务器基于 Sanic 。
由于 Sanic 可以同时处理许多请求,这意味着动作服务器也可以同时服务许多请求。
因此,人们希望这个定制动作不会阻塞动作服务器:
图 1:一个不希望阻塞动作服务器的自定义动作
让我们做一些测试来验证这个答案的有效性。
我们将在 16 核机器上运行我们的实验。动作服务器将仅使用 1 个工作线程启动。
在实验开始时,我们将对动作服务器进行 16 次并行调用,以运行相同的自定义动作。我们将计算所有呼叫完成后经过的时间。
由于action_long_backend_call_sync
自定义操作需要 5 秒钟才能完成,我们预计所有 16 个呼叫将在 5 秒钟左右完成。
结果如下:
图 2:16 次并行调用 action_long_backend_call_sync 的结果
16 次调用,每次 5 秒,总计 80 秒,因此图 2 中的结果表明动作服务器正在串行执行请求,即只有在前一个请求完成后才执行下一个请求。
来自操作服务器的日志证实了这一点:
图 3:服务 action_long_backend_call_sync 请求时的日志
图 3 示出了例如来自user_8
的新请求仅在例如来自user_9
的先前请求完成之后才被处理。接收到请求和完成请求之间的时间正好是 5 秒。
下面是我们如何重写图 1 中的自定义动作,以便它不会阻塞动作服务器:
图 4:图 1 中自定义动作的异步版本
注意,我们已经调用了sleep()
async(第 51 行),并将定制动作的run()
方法定义为异步(第 41 行)。
以下是此自定义操作的计时结果:
图 5:对 action_long_backend_call_async 的 16 次并行调用的结果
不出所料,所有通话都在 5 秒左右完成。
日志还确认没有调用阻塞操作服务器:
图 6:服务 action_long_backend_call_async 请求时的日志
图 6 示出了动作服务器能够接受例如来自user_2
的其他请求,而例如来自user_1
的当前请求处于休眠状态。
上一节中的实验表明,为了让自定义操作不阻止操作服务器为其他请求提供服务,我们需要:
- 将自定义动作的
run()
方法声明为async
,并且 - 确保
run()
方法的主体不调用任何阻塞函数
第二点需要更多的解释。
即使我们将它的run()
方法声明为async
,对action_long_backend_call_sync
的 16 个并行调用仍然需要大约 80 秒才能完成。这是因为该方法调用了sleep()
,不幸的是这是一个阻塞调用。在这种情况下,解决方案很简单,因为在asyncio
库中有一个sleep()
的非阻塞版本。
实际上,您的定制操作可能会使用请求库调用外部后端服务。如果您切换到 aiohttp 库来发出这些请求,您将从您的动作服务器获得更好的吞吐量。
当然,如果您的自定义动作实际上是在动作服务器的工作线程上受 CPU 限制的,那么这不会有什么不同。
本文解释了如何编写一个 I/O 绑定的自定义操作,以便它不会阻止操作服务器为其他请求提供服务。
我希望你发现这很有见地。