重构 L3 Agent¶
https://blueprints.launchpad.net/neutron/+spec/restructure-l3-agent
- 作者:
Carl Baldwin <carl.baldwin@hp.com>
L3 agent 的实现主要在一个 Python 文件中。目前统计,这个文件超过 2,000 行代码 [1]。大部分功能由 L3NATAgent 类提供,该类约占文件代码量的 75%。这个类处理从处理 RPC 消息到为路由器命名空间内的接口发送 gratuitous arp 的所有事情。这种结构使得 agent 很难扩展和修改。这是一种技术债务。偿还它将有助于启用新功能的开发。
问题描述¶
如前言所述,L3 agent 已经变得难以控制。这种代码结构使得扩展和开发新功能变得困难。以下是 l3_agent.py 文件以及该文件中的 L3NATAgent 类负责的功能列表。
定义 L3PluginApi
管理 DVR fip 命名空间的 link local 地址
定义 RouterInfo,基本上是关于路由器的庞大数据结构
处理来自 RPC 的路由器更新消息,放入队列
处理所有路由器的定期同步
L3NATAgent
管理命名空间生命周期
处理路由器添加/删除
在每个路由器中运行 metadata proxy
一个非常大的方法,名为 process_router
snat_rules, dnat_rules
floating_ip_address
external gateway
internal network interfaces
ipv6 支持
清理过时的接口
static routes
HA router keepalive
DVR routers
rtr_2_fip
dvr floating ips
snat namespace handling
arp entries
routes_updated
_process_routers_loop
_sync_routers
gratuitous arp
L3NATAgentWithStateReport
DVR 和 HA 路由器的代码混杂在一起。有很多“if router[‘distributed’]”和“if ri.is_ha”这样的语句。
缺乏清晰的资源生命周期管理策略,例如命名空间和设备。大部分都是随着时间的推移,随着发现初始实现的问题而演变而来的。这些问题通常与它们应该被删除时没有被删除有关。
提议的变更¶
概述¶
这不会从头重写 L3 agent。将这个项目作为一项巨大的努力,旨在一次性提交补丁,这肯定会导致失败。这项工作的大部分将是纯粹的重构,但并非全部。工作将尽早且经常地发布以供审查。尽可能避免补丁之间的依赖关系。每个补丁都应作为对现有代码库的合理可审查的改进而独立存在。
适当的分离关注点是一个目标,但将分步进行。我们将从路由器抽象与 L3 agent 的高级分离开始。
L3 Agent¶
L3 agent 将负责监听来自 RPC 的更新,将更新排队以供 worker 处理。它将继续监督由 agent 管理的路由器集合。如果启用了命名空间,这可能是一个大型路由器集合。如果未启用命名空间,这将是一个单一路由器。(_process_routers, _sync_routers, routes_updated, _router_added/removed)
agent 仍然将管理 agent 上路由器可用的外部网络。
L3PluginApi 类将保持在 l3_agent.py 中的状态。
agent 将保留 L3NATAgentWithStateReport 功能。
Router¶
将引入一个新的路由器类。当前 L3 agent 处理的许多功能——特别是 process_routers 方法中的功能——将封装在此新类中。当前的 RouterInfo 类将移动到这个抽象之下。这个类将使 RouterInfo 类过时并取代它。
路由器类不仅仅是一个包含路由器数据的结构体。它将是一个功能齐全的类,能够处理路由器的实现。需要定义一个清晰且简单的 Python API。
截至 Juno 版本,有三种可用的路由器。它们是 distributed、highly available 和 legacy 路由器。将添加一个新的路由器类层次结构来封装每种可用路由器类型的细节。在首次创建路由器实例时,将加载适当的类。
Kilo 或更高版本将添加第四种路由器类型,它结合了 DVR 和 HA 路由器。添加这种第四种类型不在这个蓝图的范围内。但是,在完成这个蓝图后,通过创建一个新的类类型来组合两个基本类的功能,添加这种新型路由器应该相对容易。这些新类应该以一种有效地利用两个基本类中现有代码的方式编写。这两个类之间的任何额外复杂性都应该只存在于需要发生的任何协调工作。
上述使用继承来封装各种路由器的细节,抽象的基路由器作为基类,其他路由器作为子类实现。HA DVR 类型的路由器将使用多重继承。以下是我想象的层次结构的点表示。请注意,LegacyRouter 不是基类。这反映了“DistributedRouter 是 LegacyRouter”不是一个真实陈述。此外,还有两个 DVR 类。这反映了非网络节点具有 DVR 的分布式部分,而网络节点具有构建于分布式部分之上的中心部分。
digraph inheritence {
"LegacyRouter" -> "Router"
"DistributedRouter" -> "Router"
"DistributedRouterCentral" -> "DistributedRouter"
"HARouter" -> "Router"
"HADistributedRouter" -> "HARouter"
"HADistributedRouter" -> "DistributedRouterCentral"
}
鉴于 HA 和 DVR 是单个路由器的属性,而不是部署的属性,我们需要注意从一种类型到另一种类型的迁移路径。代码应该完全期望路由器可以从一种类型更改为另一种类型,并能够通过更改用于路由器的类来处理它。我预计路由器应该能够使用其新类型正常工作,并且在路由器更改类型后不再需要的任何命名空间、设备或其他资源都将被清理。清理将由资源生命周期模式处理,如 资源生命周期 部分所述。
非常长的 _process_router 方法需要使用此方法进行重构。以下是此处处理的职责。最终,这些将抽象到其他接口之后(例如,iptables 抽象),但这可能不完全是这项工作的一部分。从高级别来看,此方法的重构将分离将接口连接到网络与路由职责等问题。
snat_rules, dnat_rules
floating_ip_address
external_gateway_added
internal network added
static routes
Services¶
L3 agent 中以各种方式实现了几个服务。这个蓝图将添加一个简单的服务驱动程序模型,以支持将这些服务从 L3 agent 类及其继承层次结构中解耦。如前所述,将不会使用继承来集成这些服务。每个服务将被移动到一个新的服务特定模块
本质上,agent 将是一个基本的容器,它将服务作为类加载。路由服务通过将路由器事件按顺序分派给每个已知的服务来协调服务的流程。对于这个蓝图,分派可能实现为对通用服务接口的简单方法调用。这可以扩展为支持更可插拔的模型,作为后续工作。
服务将拥有对路由器的引用,以便访问 L3 功能,例如添加/删除 NAT 规则和打开端口。
我无意在 FW 和 VPN 服务中实现的设备驱动程序模型中进行任何重大更改,在蓝图的范围内。我不期望这项工作对服务的配置产生任何影响。将积极保留向后兼容性。这可能涉及在 VPNAgent 和其他代理中留下存根以加载启用了 VPN 的 L3 agent。
现有的集成测试将被修改以使用新的结构。
这里的意图不是创建一个对每个人都适用的一切的模型。这不在这个蓝图的范围内。意图是迭代地开发一个适用于以下已经与 L3 agent 集成的服务的接口。目标是减少耦合并为未来可能需要的更复杂的模型铺平道路。它们将按列表顺序处理,并且接口将演变以支持所有这些服务。
Metadata Proxy
最容易的一个。低垂的果实。
FWaaS
希望将其删除为 L3NATAgent 的超类
VPNaaS
希望将其删除为 L3NATAgent 的子类
第一步是创建一个服务抽象类,然后为各种服务创建子类以将其用作 L3 agent 的观察者。基类将对 L3 agent 可以通知的每个操作具有无操作方法,子类将实现它们感兴趣的方法。每个服务将注册为观察者。
目前,L3 agent(和 VPN agent)加载服务的设备驱动程序。在此第一步中可以做的就是,创建一个服务对象。该对象将执行加载并注册以获取来自 L3 agent 的通知。
将各种 agent 类中的代码移动到新的服务子类中,并根据需要进行调整,将填充子服务的通知处理程序。
任何比这更复杂的事情都应被认为不在这个蓝图的范围内。
这项工作的一些指导原则
我们不需要在预先完全定义服务抽象类。我打算迭代地完成这项工作,按上述顺序处理服务。这意味着我们不会在考虑 VPN agent 的需求的情况下审查解耦 metadata proxy 的更改。
初始分解应该在不更改任何配置或其他部署细节的情况下完成。这意味着我们可能会留下一个微小的 VPNAgent 类存根。
最初,服务将在创建时获得一个 L3 agent,但随着蓝图的进展,可以将路由器实例传递给服务。
DVR Router Class¶
与 DVR 添加的浮动 IP 命名空间相关的所有内容都应封装在驱动程序中,用于将路由器插入到外部网络并处理浮动 IP 设置。这包括 LinkLocalAllocator、DVR 特定的浮动 IP 处理、fip 命名空间管理、连接路由器到 fip(rtr_2_fip、fip_2_rtr)、_create_dvr_gateway 以及代理 arp 条目的管理。
HA Router Class¶
此封装将隐藏与启动 keepalived 以及创建和使用 HA 网络上所需的接口相关的细节。
Resource Lifecycle¶
主要问题是资源经常在其有用生命周期结束后仍然存在。对 agent 的可靠可用性、RPC 消息的保证排序和传递以及其他不切实际的保证做出了假设。新的设计将考虑到这些领域的问题。不会做出任何假设。这将导致更强大的实现。
我们遇到的问题是 agent 未能在应该删除资源时清理资源。为了解决这个问题,我正在考虑使用命名空间作为示例支持以下模式的东西
if full_sync:
with namespace_manager.prepare_to_clean_up_stale() as nsm:
for router in all_active_routers:
nsm.link_router_to_ns_somehow(router)
__enter__ 和 __exit__ 方法应该协同工作以发现过时的命名空间,然后清理它们。我想也许命名空间对象应该持有对占用它的路由器的弱引用。当弱引用失效时,命名空间可以被删除。这种模式与现在代码中存在的一些早期重构不太不同。但是,这项工作将形式化该模式并将其从代码的其余部分中抽象出来。已经启动了代码来说明这种模式 [2]。
该模式可以应用于其他资源,例如命名空间内的接口。我们一直难以确保在它们不再有用时删除这些接口。对于路由器中的设备和其他资源,每次处理路由器时都会标记所有活动资源。然后识别并删除过时的资源。
由于系统中使用的 iproute 版本存在问题,因此命名空间一直难以持久删除 [3] 和 [4]。除了重新启动机器之外,真的没有什么可以删除这些东西的。但是,新的资源生命周期管理实现将保存一个它尝试删除的命名空间的集合。如果删除失败,它将在未来的清理运行中跳过此删除。理想情况下,操作员应该禁用命名空间删除或升级系统上的 iproute 包以避免这些问题。
https://bugs.launchpad.net/ubuntu/+source/iproute/+bug/1238981
Configuration Handling¶
配置选项的处理将得到清理;配置选项中也有太多的“如果这样”和“如果那样”。需要正确封装行为,这样我们就不需要那么频繁地进行分支。一些示例链接在参考资料中 [5] [6] [7] [8]。
https://github.com/openstack/neutron/blob/c9bea66dfe/neutron/agent/l3_agent.py#L584
https://github.com/openstack/neutron/blob/c9bea66dfe/neutron/agent/l3_agent.py#L743
https://github.com/openstack/neutron/blob/c9bea66dfe/neutron/agent/l3_agent.py#L1349
https://github.com/openstack/neutron/blob/c9bea66dfe/neutron/agent/l3_agent.py#L1460
数据模型影响¶
无
REST API 影响¶
无
安全影响¶
预计没有影响。我们需要小心审查代码,以确保这些更改不会在 agent 中引入漏洞。
通知影响¶
无
其他最终用户影响¶
无
性能影响¶
无
IPv6 影响¶
我们将注意保留 Neutron 中所有现有的 IPv6 功能。不计划对当前的 IPv6 功能进行任何更改或添加。
其他部署者影响¶
无
开发人员影响¶
l3_agent.py 文件中的大部分代码将被移动到其他文件中。这种重构将引入更好的软件工程模式,以便更容易地扩展、修改和维护功能。
已经习惯于当前实现方式的开发人员可能无法识别最终结果。但是,他们将能够轻松地重新熟悉新代码。
为了避免重基和潜在的回归问题,在进行繁重的工作期间,应避免对 L3 agent 进行非关键更改。将向 openstack-dev ML 发送邮件以开始冻结非关键更改,并发送另一封邮件以结束冻结。冻结仅在初始的更具破坏性的重组期间需要。随着某些部分稳定下来,冻结将被解除。例如,一旦 VPN 和 FW 服务与 agent 代码解耦——这将是第一步——就可以继续对这些服务进行开发。
社区影响¶
此更改是 Kilo Neutron 批准的优先级之一。
它支持至少以下可能也计划用于 Kilo 的工作。
可插拔外部网络蓝图(间接动态路由集成)
启用 HA 路由器和 DVR 协同工作。
更好地集成 L3 服务。
推出高级服务
备选方案¶
另一种选择是保持现状,仅在特定新功能需要时进行小规模重构。这并非理想之选,因为已经有很多方面需要此重构的支持。如果推迟重构,将会减慢相关工作的开发速度。
编写新的代理并最终弃用当前的代理是另一种选择吗?我个人对这种方法没有太好的经验。它似乎是用一组已知的难题换来另一组未知的难题。回归问题层出不穷。我更倾向于以可审查的小块进行重构。这并不能保证没有回归,但可以在流程的早期发现它们,并且更容易定位和修复。
实现¶
负责人¶
- 主要负责人
- 其他贡献者
工作项¶
我预计一些初始工作项需要按顺序处理,因为代码中存在高度耦合。但是,随着分解的进行和耦合的降低,其他工作项可以并行处理。
例如,由于服务代理与 L3 代理继承层次结构耦合,因此在可行的情况下,需要先将它们移出,才能实现适当的路由器抽象。
代理的功能测试
服务驱动程序
从简单开始。目前这还不能满足所有人的需求。它并非旨在成为完全可插拔的服务驱动程序。
Metadata Proxy
FWaaS
VPNaaS
DVR、HA 和传统路由器的分解和模块化
创建适当的路由器抽象,以取代 RouterInfo
可以作为其他路由器实现的抽象。再次强调,我们将从简单开始引入抽象。
创建继承层次结构。
这可能分几个步骤完成。最初,继承层次结构可能很薄,大部分实现仍然在基类中。未来的步骤会将职责转移到子类,并演化接口。
依赖项¶
无
测试¶
除了下面讨论的功能测试外,将尽力使用现有的单元测试,以确保保留现有覆盖范围并避免它们旨在防止的回归。最终结果可能看起来像是所有旧的单元测试都被删除,并且编写了新的、更好的单元测试来代替它们。
所有新的和重构的代码都将涵盖适当的单元测试覆盖范围。使用新的代码结构进行单元测试会更容易。如果不是,那么我们就做错了。
我没有计划在代码重构之前努力添加缺失的单元测试覆盖范围。
Tempest 测试¶
没有计划进行新的 tempest 测试。
功能测试¶
在对代理代码进行任何重大重构之前,将从 L3 代理添加功能测试。Assaf [9] 将负责这项测试工作,并得到 John Schwarz [10] 以及此蓝图中所列出的所有其他委派人员的帮助。这包括为新的 DVR 和 HA [11] 功能添加功能测试。
API 测试¶
没有计划进行新的 API 测试。
文档影响¶
无
用户文档¶
无
开发人员文档¶
代码中的新 API 接口将使用文档字符串进行记录
参考资料¶
https://etherpad.openstack.org/p/kilo-neutron-agents-technical-debt https://review.openstack.org/#/c/105078/