移除 oslo.service 中的 Eventlet¶
https://blueprints.launchpad.net/oslo?searchtext=remove-eventlet-from-oslo-service
Oslo.service 提供了一个框架,用于定义长期运行的服务。为了实现这个目标,oslo.service 依赖于 Eventlet 及其协程。
Eventlet 是 oslo.service 的核心。
然而,从 Openstack 中移除 Eventlet 现在已经成为一个事实。
现在必须从 oslo.service 中移除 Eventlet。
本规范旨在设计从 oslo.service 中移除 eventlet 的方案。
本文档的目标是:1. 识别 oslo.service 当前使用的 Eventlet 功能的替代方案;2. 定义如何实施这些替代方案;3. 定义正确移除 Eventlet 所需的各种里程碑;4. 尽量减少对 oslo.service 用户的影响。
问题描述¶
移除 Eventlet 现在已经成为一个事实。官方 Eventlet 项目将在不久的将来退役,Openstack T.C 官方接受了社区提出的从 Openstack 运行时环境中移除 Eventlet 的目标。
oslo.service 提供的主要功能是
loopingcall:一个模块,用于循环运行方法;periodic task:可以在单独进程中运行的周期性任务;service:一个服务管理器,可以处理工作进程;wsgi:一个实用工具,用于创建和启动 wsgi 服务器;systemd:一个辅助模块,用于 systemd 服务就绪通知;threadgroup:一个辅助工具,用于创建绿线程和定时器组。
问题在于 oslo.service 严重依赖 Eventlet。下面列出的所有 oslo.service 功能都基于 Eventlet
loopingcall;periodic task;服务;wsgi;threadgroup.
与此同时,oslo.service 还提供了一些辅助功能,例如
eventlet_backdoor;fixture;sslutils.
这些辅助功能与 Eventlet 的行为机制密切相关,严格来说并不是 oslo.service 的功能。
因此,如果我们想从 oslo.service 中移除 Eventlet,就必须确定如何处理以下模块
loopingcall;periodic task;服务;wsgi;threadgroup;eventlet_backdoor;fixture;sslutils.
我们必须确定所有这些模块是否都可以通过使用替代方案重写,以及使用这些替代方案是否会影响 oslo.service 现有的 API(附加参数、默认值等)。
我们必须决定:- 如何从 oslo.service 的当前版本过渡到未来版本,一个没有 Eventlet 的版本;- 未来版本的 oslo.service 是否会再次提供所有这些功能,或者我们是否应该在此过程中删除其中一些功能。
如果决定删除现有功能,那么我们必须决定如何过渡我们的消费者。
在所有情况下,在某个时刻,我们都必须发布 oslo.service 的主要版本,其中向后兼容性将被彻底破坏。
本文档旨在回答所有这些问题。
约束¶
为了保持一致性,我们必须定义一些约束。这些约束的目标是尽可能地保持迁移对消费者的透明度。
我们不能突然移除 Eventlet,因此,对于前面描述的所有 oslo.service 功能,两种实现都必须共存,即 Eventlet 版本和新版本;
在 oslo.service 的消费者层面,过渡必须尽可能平滑。这意味着消费者不必重写所有导入才能继续使用基于 Eventlet 的实现或新版本。一些 oslo.service 模块可能会被放弃,只有在消费者层面导入这些模块时才需要删除它们;
消费者必须决定何时从一种实现切换到另一种实现;
移除 Eventlet 实现不应以随机方式影响消费者。如果从 oslo.service 中显式删除某个功能,则必须在 oslo.service 专门的迁移指南中记录替代方案。必须通过弃用警告告知客户功能的移除;
非主动维护的交付物不应受到 Eventlet 实现移除的影响;
非主动维护的交付物不应无限期地阻止 Eventlet 的移除。如果发现这种情况,则必须通知专门负责迁移的 Pop Up 团队。
提议的变更¶
功能分类¶
再次说明,oslo.service 提供的主要模块是
loopingcall;periodic task;服务;wsgi;threadgroup;systemd;eventlet_backdoor;fixture;sslutils.
每个模块或多或少都是一个功能。
如前所述,一些模块是特定于 Eventlet 的
eventlet_backdoor:用于附加基于 Eventlet 的进程的后门;fixture:用于模拟wait()的固定装置;sslutils:Eventlet 专用的 ssl 包装器。
因此,新实现不会重新实现这些模块。这些模块将简单地从新实现中删除。
由于 wsgi 模块基于 Eventlet wsgi 服务器,因此我们也建议从新实现中删除该模块。
我们希望鼓励项目之间的一致性。避免在不同项目中启动服务的方式多种多样至关重要。统一的方法将简化维护并增强用户体验。我们不希望项目看起来/运行不同。因此,我们提倡采用以下一个或两个软件包,它们可以作为 oslo.service 暴露的 Eventlet WSGI 模块的可信替代方案
uWSGI <https://uwsgi-docs.readthedocs.io/en/latest/>:经过充分测试且在 OpenStack 环境中运行的同步 WSGI 服务器;基于线程;
uvicorn <https://pypi.ac.cn/project/uvicorn/>:一个 Python 的 ASGI Web 服务器实现;
asgiref <https://pypi.ac.cn/project/asgiref/>:允许包装或装饰异步或同步函数以从另一种风格调用它们(因此您可以从同步线程调用异步函数,反之亦然)。
这样,我们将为所有交付物提供统一的方法。这种方法与两种世界(同步和异步)兼容。
这两个库都由许多开发人员良好且积极地维护。
在应用程序层,我们提倡使用 FastAPI <https://pypi.ac.cn/project/fastapi/>,它也与两种世界(同步和异步)兼容,并且由数百人积极维护。FastAPI 正在成为一种主流库,在 AI 领域被广泛使用,因此我们认为它是一个具有长期生命力的可信替代方案。关于应用程序层的考虑略微偏离当前主题,但此处提供是为了引发讨论。
以下模块将保留并需要进行过渡
loopingcall;periodic task;服务;threadgroup;systemd;
systemd 模块的实现似乎是一个 CPython 原生实现,因此可以保持不变。
如何进行?¶
oslo.service 不能一次性过渡。我们建议在 oslo.service 中引入后端概念,以允许并行使用两种实现。
后端将允许实现新版本的 oslo.service,同时保留现有版本。
后端将简化用户在过渡期间的生活。
我们建议以下里程碑来在实现之间以及使用后端进行切换
(SLURP) 2025.1:将当前实现移动到
eventlet后端(配置中的默认后端);(SLURP) 2025.1:实现
threading后端;(NON-SLURP) 2025.2:弃用
eventlet后端,并使threading成为默认后端;(NON-SLURP) 2026.2:移除
eventlet实现,并将threading实现移动到根级别,并移除后端概念。
实际上,oslo.service 是一个扁平模块。它的所有子模块都位于根级别。这意味着用户从 oslo.service 模块的根级别导入他们需要的特性,例如
from oslo_service import wsgi
from oslo_service import service
from oslo_service import loopingcall
...
如果我们不引入后端概念,所有使用 oslo.service 的 OpenStack 服务都必须至少重写两次所有导入。第一次是当他们希望使用新实现时
from oslo_service.threading import service
第二次是当旧实现被移除时,并且因此,当新实现移动到根级别时
from oslo_service import service
这不是一个可接受的场景,因为它会导致在导入级别进行许多无用的来回切换,而不会为用户带来任何额外的价值。
使用后端将隐藏 oslo.service 中的这种切换的复杂性。用户不必一遍又一遍地更改他们的导入。
实际上,oslo.service 看起来像
oslo_service
├── eventlet_backdoor.py
├── fixture.py
├── _i18n.py
├── __init__.py
├── locale
│ └── .. (ignored)
├── loopingcall.py
├── _options.py
├── periodic_task.py
├── service.py
├── sslutils.py
├── systemd.py
├── tests
│ └── .. (ignored)
├── threadgroup.py
├── version.py
└── wsgi.py
一旦添加了后端,oslo.service 的结构将看起来像
oslo_service
├── backends
│ ├── eventlet
│ │ ├── eventlet_backdoor.py
│ │ ├── fixture.py
│ │ ├── __init__.py
│ │ ├── loopingcall.py
│ │ ├── periodic_task.py
│ │ ├── service.py
│ │ ├── sslutils.py
│ │ ├── threadgroup.py
│ │ └── wsgi.py
│ └── threading
│ ├── __init__.py
│ ├── loopingcall.py
│ ├── periodic_task.py
│ ├── service.py
│ └── threadgroup.py
├── eventlet_backdoor.py
├── fixture.py
├── _i18n.py
├── __init__.py
├── locale
│ └── .. (ignored)
├── loopingcall.py
├── _options.py
├── periodic_task.py
├── service.py
├── sslutils.py
├── systemd.py
├── tests
│ └── .. (ignored)
├── threadgroup.py
├── version.py
└── wsgi.py
每个根子模块将简单地有条件地导入正确的后端,例如使用 service 子模块
if _options.backend == "threading":
from oslo_service.threading import service
else
from oslo_service.eventlet import service
如果新实现中不存在子模块,那么根级别子模块将使用 debtcollector 发出弃用警告并向用户提供说明,例如使用 wsgi 子模块
debtcollector.deprecate(
"""
The WSGI module is no longer supported
You see this deprecation warning because you are importing
the oslo.service wsgi module. This module is deprecated and will
be soon removed. Please consider using uwsgi and consider following
the migration path described here:
https://docs.openstack.org/oslo.service/latest/migration/wsgi.html
",
version="1.0"
)
if _options.backend == "eventlet":
from oslo_service.eventlet import service
else
raise ImportError("WSGI module not found in the threading backend...")
关于保留在 threading 实现中的模块,它们将通过使用新的底层库(如 cotyledon、futurist 和 stdlib 中的 threading/concurrent)进行重写。有关这些新库的使用,请参阅依赖项和 API 部分的详细信息。
如果某个子模块不受 Eventlet 移除的影响,因此未重新实现,那么它可以保留在根级别。例如,systemd 子模块在我们的先前树示例中保留在根级别。
如果消费者未在指定时间内将其 oslo.service 后端移动到 threading 后端,则应警告 T.C。然后 T.C 将肯定通知专门负责整个 Eventlet 移除的 Pop Up 团队。在这种情况下,Pop Up 团队可以决定迁移此交付物,或者如果无人主动维护它,则可以建议将其退役。
备选方案¶
也可以完全弃用 oslo.service,并在弃用警告中指向可用的替代方案,从而将重构的负担交给消费者。
这种方法的问题是它肯定会引发各种方法,从而导致多种解决方案。
创建 Oslo 项目背后的动机是统一解决方案并减少技术债务。
如果我们将重构委托给 oslo.service 消费者,这将导致技术债务的增加。
Impact on Existing APIs¶
临时后端¶
将修改现有的 API 以引入临时后端。后端将保持私有模块,消费者无法直接导入。通过配置选择一个或另一个后端,将通过经典的导入来导入一个或另一个后端。
公共 API 将保持几乎不变,直到移除 Eventlet 后端为止。
一旦移除 Eventlet 后端,那么与 Eventlet 相关的公共 API 也将被移除,请参阅下一节。
移除的子模块¶
一旦迁移完成,后端概念将被移除,新实现将被移动到根级别,这意味着一旦迁移完成,oslo.service 将看起来像
oslo_service
├── _i18n.py
├── __init__.py
├── locale
│ └── .. (voluntary ignored)
├── loopingcall.py
├── _options.py
├── periodic_task.py
├── service.py
├── systemd.py
├── tests
│ └── .. (voluntary ignored)
├── threadgroup.py
└── version.py
以下模块将不再存在,因此将无法再导入
wsgi
eventlet_backdoor
fixture
sslutils
在存在 backends 概念期间,用户将面临导入错误,直到从用户代码库中移除 wsgi、eventlet_backdoor、fixture、sslutils 模块为止。实际上,实现 NotImplemented 接口是没有用的,因为在所有情况下,用户都必须移除它们。
并且瞬态 backends 子模块及其内容也将被移除。
周期性任务¶
periodic_task 子模块将成为 futurist 库的代理。
oslo.service periodic_task 子模块提供以下抽象来管理周期性任务
oslo_service.periodic_task.periodic_task,表示一个周期性任务;
oslo_service.periodic_task.PeriodicTasks,是周期性任务的管理器(一对多)。可以看作是一个工作进程,我们将可调用对象附加到它以周期性地运行。
Futurist 定义了以下方法和类来管理周期性任务
futurist.periodics.PeriodicWorker,允许周期性地调用一个可调用对象集合。这是一个工作进程,我们将可调用对象附加到它以周期性地运行;
futurist.periodics.periodic,允许将方法/函数标记为希望/能够周期性地执行;
futurist.periodics.Watcher,是周期性回调活动的可读对象。
因此,建议以下绑定来使用 oslo.service 作为 futurist 的代理
oslo_service.periodic_task.periodic_task将绑定到futurist.periodics.periodic;oslo_service.periodic_task.PeriodicTasks将绑定到futurist.periodics.PeriodicWorker;
由于我们的目标是尽可能保持 oslo.service 现有的 API 不变,因此我们建议忽略 futurist.periodics.Watcher 类。
futurist.periodics.periodic 实现 `enabled` 概念。这个选项在 oslo.service 中不存在。事实上,在 oslo.service 中,如果 `spacing` 参数设置为负数,则会禁用一个周期性任务。在这种情况下,如果这个数字在 oslo.service 上是负数,那么我们将不得不将 futurist 的 `enabled` 参数设置为 `False`。
Futurist 周期性任务 接受一个 executor 参数。Futurist 定义不同种类的 executors。
我们应该提供一种选择 futurist 将使用的 executor 类型的方式,为此,我们将不得不实现对此概念的抽象,以便为用户提供一种选择的方式。
Futurist 提供一个基于 Eventlet 的 executor。由于我们的目标是移除 Eventlet,因此我们不会在 oslo.service 级别提供对这个 executor 的访问。
Oslo.service 将只允许使用/选择以下 futurist executors
futurist.ProcessPoolExecutor
futurist.SynchronousExecutor
futurist.ThreadPoolExecutor
Service¶
为了实现 oslo.service’ service 子模块的新版本,我们建议使用 Cotyledon。
Cotyledon 模块提供以下公共 API
cotyledon.Service:service 的基类;
cotyledon.ServiceManager:管理 service 的生命周期。
而 oslo.service 的 service 子模块提供以下公共 API
oslo_service.service.Launcher:启动一个或多个 service 并等待它们完成;
oslo_service.service.ProcessLauncher:使用给定的 worker 数量启动一个 service;
oslo_service.service.Service:运行在主机上的二进制文件的 service 对象;
oslo_service.service.ServiceBase:所有 service 的基类;
oslo_service.service.ServiceLauncher:在父进程中运行一个或多个 service;
oslo_service.service.launch:使用给定的 worker 数量启动一个 service。
我们提出以下绑定
`oslo_service.service.Launcher` 将委托给 `cotyledon.ServiceManager`;
`oslo_service.service.ServiceLauncher` 将委托给 `cotyledon.ServiceManager`;
`oslo_service.service.Service` 将委托给 `cotyledon.Service`;
而 `oslo_service.service.launch` 和 `oslo_service.service.ProcessLauncher` 将保持与当前实现大致相同的逻辑,减少 Eventlet 的 monkey patching。
与 oslo.service 不同,cotyledon 仅允许一个 service worker manager 同时运行。Oslo.service 允许同时运行多个 Service launcher。这种差异应该记录下来。
Loopingcall & threadgroup¶
由于 `loopingcall` 和 `threadgroup` 模块基于 greenthreads,因此我们必须重新实现它们。我们建议使用 CPython `threading` 模块来重构它们。
`loopingcall` 似乎只需要线程来循环运行方法。
`threadgroup` 使用 eventlet green pool 来管理线程组。再次使用 stdlib `threading` 模块提供了一种将线程附加到定义组的方法。可能还需要使用 ThreadPoolExecutor 来允许异步行为。
这两个子模块不应受到 API 更改的重大影响。只有内部机制会发生变化,而公共 API 肯定会保持不变。
安全影响¶
无
性能影响¶
移除 Eventlet 将意味着在某些情况下转变为原生线程模型。Eventlet 基于 greenlet 提供的协同协程,而 cotyledon 或 futurist 使用线程,后者是抢占式的。
线程往往比协程更昂贵和更慢,因为它们涉及上下文切换。操作系统将继续与所有线程共享 CPU 操作,即使它们还没有准备好工作(网络 IO 等)。
事实上,根据分配给 service 或周期性任务的 worker 数量,在大量线程并发的上下文中,线程可能会降低机器的流速。这与资源密集型的上下文切换有关。
线程是抢占式的,因此与协同协程相比,它们更容易导致竞争条件。
Configuration Impact¶
这个话题将以多种方式影响配置。第一个影响将与添加一个新的配置选项以允许切换实现有关。切换后端。
[DEFAULT]
oslo_service_backend = eventlet
这个新选项将命名为 `oslo_service_backend`,它将是一个 `cfg.StrOpt`。
它将提出以下选择作为有效设置
choices=['eventlet', 'threading']
并且默认设置为 `eventlet`,用户将在清理代码库中 oslo.service 弃用子模块的使用后,将此值更改为 `threading`。
在弃用期结束后,此选项将被删除。
正如前面所说,使用后端概念,因此这个选项将允许 oslo.service 内部的瞬态状态,从而使我们能够交换内部实现。
一旦完成交换,与 wsgi 模块和 oslo.service 的 sslutils 相关的现有配置将被删除,因为这些子模块将被淘汰。
首先,这些配置部分(wsgi、sslutils)将被完全弃用,以警告用户他们必须停止使用它们。
我们还应该弃用默认配置部分中的 `backdoor_socket` 和 `backdoor_port`,因为 eventlet_backdoor 模块将被删除,因此这些选项在完成交换后也将被删除。
开发人员影响¶
从 oslo.service 中删除 Eventlet 将允许进行其他工作,例如:- 删除 oslo.log 中的互斥技巧;- 删除 futurist 中的 greenlet/eventlet executor。
Testing Impact¶
由于当前测试也依赖于 Eventlet 和 monkey patching,因此所有新实现也应该引入自己的测试。
现有的测试应该保留,并且 `tests` 目录结构应该反映新的模块树,同时包含两种后端。
删除和弃用的测试将由 `eventlet` 后端负责。我们不想用过时的工件污染新的 `threading` 实现。
Oslo.service 没有实现功能测试,因此这次重构不会添加功能测试。
实现¶
负责人¶
- 主要负责人
Hervé Beraud (hberaud)
里程碑¶
完成目标里程碑
(SLURP) 2025.1:将当前实现移动到
eventlet后端(配置中的默认后端);(SLURP) 2025.1:实现
threading后端;(NON-SLURP) 2025.2:弃用 eventlet 后端,并使 `threading` 成为默认值;
(NON-SLURP) 2026.2:移除
eventlet实现,并将threading实现移动到根级别,并移除后端概念。
工作项¶
创建一个 `eventlet` 子模块来托管现有的实现,并将根级别子模块连接到这个新模块
创建一个 `threading` 子模块来托管新的实现,并添加一个新的后端配置,默认设置为 `eventlet` 子模块
弃用 `eventlet` 子模块
将后端配置默认设置为 `threading` 实现
删除 `eventlet` 子模块并删除后端配置选项
将 `threading` 实现移动到根级别
文档影响¶
由于将添加后端概念,因此针对相同子模块的两种实现将同时存在不同的文档。
文档结构将反映 oslo.service 模块
doc/source/backends/
允许交换实现的新选项将被记录下来。
每个新实现都必须在其文档字符串级别指定其特殊性。
文档还应提供迁移指南,以指导有关已删除子模块的信息。
每次从子模块发出特定的弃用警告时,弃用消息应提供一个链接,该链接指向此迁移指南的正确部分。
此迁移指南将托管在以下路径中
doc/source/migration
此特定的迁移指南应至少记录 `oslo_service.wsgi` 的删除,并提供要遵循的跟踪信息(WSGI/ASGI (uwsgi, uvicorn 等)、应用程序层 (flask 等)、HTTP…)。文档的这部分将遵循 HTTP SGI 工作组 定义的标准
其他特定于 Eventlet 的已删除子模块(eventlet_backdoor、fixture 等)不需要记录。
删除 Eventlet 后端后,此迁移也将从文档中删除。
依赖项¶
我们建议创建两个 额外的环境标记。一个与 `eventlet` 后端相关,另一个与 `threading` 后端相关。它们将避免安装不必要的软件包,并有助于减少打包和磁盘大小。例如,如果用户决定使用 `threading` 包,则不需要构建一个不需要 `eventlet` 的容器。事实上,在许多地方,`eventlet` 的存在会触发行为变化,避免安装 `eventlet` 将减少遇到这种情况的可能性。
额外的环境标记将如下所示
[extras]
eventlet =
eventlet>=0.36.1 # MIT
threading =
futurist>=3.0.0 # Apache-2.0
cotyledon>=1.7.3 # Apache-2.0
有关 `futurist` 和 `cotyledon` 的更多详细信息和上下文,请参阅下文。
Periodic task¶
对于 `periodic task` 模块,我们建议使用 Futurist 来代替 Eventlet。事实上,Futurist 是一个提供周期性任务管理的库。
`oslo_service.periodic_task` 模块将开始 futurist 的代理。
Service¶
Cotyledon 多年来一直被创建,以提供 oslo.service 的 Service 模块的替代方案。一个摆脱 Eventlet 的替代方案。我们建议使用 cotyledon 作为 oslo.service 的 Service 模块的新实现的底层库。
换句话说,`oslo_service.service` 模块将开始 cotyledon 的代理。
参考资料¶
注意
本作品采用知识共享署名 3.0 非移植许可协议授权。 http://creativecommons.org/licenses/by/3.0/legalcode