Neutron 的软件架构


综述

OpenStack 是目前开源界第二大的项目,参与的厂商之多可谓少见,同时作为目前最大的以 Python 作为主要语言的项目,可以说这个项目是经过重重困难发展起来的,算是目前在发展的分布式系统软件大作了,面对这么一个项目,我想我们有很多值得学习和借鉴的地方,因为时间和个人经验的缘故,我在这里主要与大家分享以 Neutron 为例的 OpenStack 软件设计。

Why Python

设计一个分布式系统可以说会面对诸多挑战,这不只是 Python 会遇到的,使用别的语言也会碰到,但是呢,Python 作为一个与 Java 等语言相比不够成熟的语言可能会面临一些其他可能已经在一些语言下解决了的问题,在 OpenStack 社区里就有人提过多次这个问题,为什么我们选择 Python,而非别的语言。

在我看来,一个大型软件的选型是一个复杂的事情,对于 OpenStack,可能第一是因为历史原因。因为我们知道 OpenStack 最早的源代码是 Rackspace 和 NASA 一起贡献的,他们当初内部的选择为我们建立了一个基调,如果再用其他语言重写的话,可能在当时是一个不合实际的考虑;再有,OpenStack 与其说是一个大型软件,倒不如说是一个框架,它的虚拟化来自于 KVM、Qemu 等的支持,它的网络来自于 Open vSwitch、iptables 等的支持,至于存储,也需要 lvm、Ceph 作为底层。那么 OpenStack 是干嘛的呢,它是一个总的调度器,集成这一切功能,完成一个真正的自由云计算软件。做运维的同学一般比较熟的语言都是 Shell、Python,老一点的可能熟悉 Perl,为什么?因为这些脚本语言,很适合做这种调度的工作,或者说它很适合做粘合剂,正如一些人称 Python 为“胶水语言‘一样,它能够方便的粘合各个组件,而且代码量相对少,可以让人专注于高层的事情,而不是为了底层费脑筋;再有,作为一个高级语言,Python 有着一些相对完美的特性,比如社区的有过提交的开发者有数千,活跃的开发者也有几百,为什么 OpenStack 能快速吸引这么多开发者,所有人都是之前就接触过 Python 么?不是的,很多人都是现学的,因为 Python 的基本语法真的很简单,只需要看一个晚上第二天就能阅读 OpenStack 基本的代码了,这对吸引开发者来说很有好处,就像为什么有些公司做项目首先考虑 Java,因为好招人啊!Python 作为一个学习曲线平滑的语言,可以说为吸引开发者带来很多方便。再有,反射、自省这些高级特性也不缺,这位开发带来了方便,丰富的库,比如PasteRoutesrequestsWebObalembicJinja2 等,更是提升前期效率的利器。

分布式系统中面临的问题

  • 连接建立与服务初始化
  • 事件分离与事件处理程序分派
  • IPC 与网络协议处理
  • 静态和动态组件
  • 并发与同步

Neutron 简介

因为很多人对 OpenStack 不那么熟,我就先简单介绍下 Neutron 是什么。Neutron 是 OpenStack 的虚拟网络组件,用洋气点的话说,就是一个 SDN 控制器。为什么我们需要虚拟网络?过去我们只给客户提供虚拟机,你花钱,我租你一台,想连接上就再买个公网 IP,就像很多人在 DigitalOcean 做得一样(当然 DigitalOcean 现在也有虚拟网络)。那有了虚拟网络可以干什么呢?我们来看一下 UStack 控制面板里的一张图:

UStack VDC

这是一个所见即所得网络架构!我们可以在上面随意的增加、修改和删除2、3层网络、路由器和虚拟机。而且通过虚拟网络,为用户提供自助式的安全组、VPN、负载均衡设备等高级服务也将方便许多。

此外,从软件设计角度来看 Neutron,它做到了 Unified APISmall CorePluggable Open Architecture 和Extensible,可以说是一个设计精良的系统。


架构

Neutron 的组件设计

作为一个分布式系统,我们需要在不同的节点上运行不同的组件,然后再将之组合起来,形成一个有机的整体,首先我给出 Neutron 在 OpenStack 中宏观的位置:

OpenStack-arch

这个图是一个比较宏观的图,针对 Neutron 呢,它反映了一些事实:

  • 需要借助 Keystone 完成认证服务
  • 由 neutron-server 接受请求
  • 组件间用消息中间件完成交流
  • 消息会在 Server、Plugin 和 Agent 间传递
  • Agent 和 Plugin 与数据库交互
  • Agent 需要调用 Provider 完成功能

OK,下面我们微观的看一下 Neutron:

Neutron-arch

Neutron Server

再上面的图里我们可以看到,Neutron 的服务端是个很复杂的模块,他的复杂之处主要体现在它在”微内核“的基础上实现了 Plugin、Service Plugin、Extension 的插拔,及其与 DB 的交互。

先从请求的入口说起。简单的说,Neutron Server 就是一个 WSGI Application,WSGI 的好处相信不必我多说,它可以方便的增加或者抽取 Middleware,就像一个洋葱一样,把最核心的东西放在里面,别的各种类似 Filter 的东西一层一层叠在外面就好了。借用一个 Pylon 官方文档的图:

WSGI Middleware

对于 Neutron 来说,我们的核心 App 是什么,又需要哪些 Filter 呢?熟悉 paste 这个库的人看一下 Neutron 的 etc 目录下的 api-paste.ini 就能完全看懂,URL 路由时首先看是到首页(/)还是要调用 API(/v2.0),然后再调用pipeline_factory 这么个函数去决定认证方法,然后根据认证方法去加载 Filter,比如 Keystone 相应的 Filter 方法有request_id、 catch_errorsauthtokenkeystonecontextextensions 最后是neutronapiapp_v2_0就是核心 App 了。通过 Mddleware 这种方式,我们将核心 App(其实是APIRouter) 与其他功能性的 Filters 分离开,用这些 Filters 完成诸如验证、获取被加载的扩展等功能,设计很是精巧。

只说核心 App,APIRouter,顾名思义,它是用来路由 API 请求的,把 API 请求真正发往那些实现功能的函数,在这里 Neutron 会加载 core plugin 和 plugin extension,扩展它的 Resource map,针对每一个资源集合(比如 networks、subnets、ports 等)建立 Controller,以后 API 将被路由到 Controller 去,Controller 再去调用对应的 create、get、update、delete 方法。同时在这里还可以做一些通用的检查,比如权限、数据有效性、自动转化数据格式等等。怎样定义不同资源所需要的不同的数据有效性,是否允许 post、get 这些方法,怎么将一些字符串自动转化成整数呢,看一个基本的扩展的资源定义:

非常清晰明了!首先说明这是一个扩展的资源,而不是新的资源,然后它扩展的是 port,接着是否允许 post、put,需要怎么转换数据格式,用什么做数据验证,是否要做权限检查,默认值多少,在 get 资源时是否显示,就算不懂 Neutron 的人也能看懂这些语句的意思。这就是 Neutron/OpenStack 的代码框架设计的优雅高明的地方,节省了程序员写 dirty code 的时间,减少了初学者的学习门槛。

Core Plugin and Extensions

为什么在Middleware里要加 Extension 相关的 Filter?很简单,因为 Extension 扩展了 Resource Map,我们需要配置其相应的 URL,那么基础资源是哪些呢?在 Neutron 里,经过一些考虑后,我们把三类资源定义为核心资源,这三类资源分别是networksubnetport。这三类资源的实现呢,对应的要求由 Core Plugin 来实现。

所有的 Core Plugin 直接或间接地继承自NeutronPluginBaseV2,其加载呢,由 NeutronManager 来完成。为什么我说间接继承自NeutronPluginBaseV2呢?原因是我们的 Plugin 一般需要有 DB 相关的操作吧,在 Neutron 里 DB 也是一个 Plugin,所以 Plugin 一般只需要继承NeutronDbPluginV2即可。也就是说 Neutron 的 Plugin 的继承关系一般是这样的:

Core Plugin's UML

这里是以 ML2 Plugin 为例介绍的。用户将可以在配置文件里自由的设置 Plugin,然后再由NeutronManager加载。其结构也很清晰,CommonDbMixin完成基础的 DB 操作,比如在查询时增加 hook,设置 filter 等,NeutronDbPluginV2做与 Core Plugin 相关的数据库操作,最后 Core Plugin 继承NeutronDbPluginV2以及扩展的类。那么_supported_extension_aliases是做什么的呢,我们前边说 Server 有意层 Filter 去加载 Extension 的 Resource Map,它在查找 Extension 时呢,是先根据目录进行查找的,查找 extensions 目录下的所有 Python 文件,然后每个 extension 都会有一个 alias 名字,extension manager 再去查我们当前加载的 Core Plugin 是否支持这个 Extension,如果支持则加载,否则在日志里记录一下这个”extension not supported by plugin”。

Service Plugin

刚才只提到了加载 Core Plugin 和 Extensions,其实加载 Core Plugin 之后就加载 Service Plugin 了,而且因为 Core Plugin 和 Service Plugin 有很多相似的地方,所以 Core Plugin 很多地方也都按照 Service Plugin 去处理,就比如说 Resource Map,我们核心资源是写在RESOURCE_ATTRIBUTE_MAP里的,Plugin Extension 由 Extension Manager 加载,那 Service Plugin 呢?刚才我们说 Extension Manager,并没有区分什么 Plugin Extension Manager 或者 Service Extension Manager,就是因为这两个确实是不做区分的,而且很重要的一点是我们统一的只由 Extension 去扩展资源,就如前面 Filter 只有 Extension 但没有别的,所以如果你需要扩展 Resource Map,那么写一个扩展,然后把它加在你的 Service Plugin 的 supported_extension 里,Server 就可以接受相应的请求并路由了。其继承关系如下图:

Service Plugin's UML

这里是以 LBaaS 为例介绍的,因为 Loadbalencer 这个 Extension 是LoadBalencerPlugin的核心扩展,所以相关的数据库操作都在LoadBalencerPluginDb里完成,因此不像 Core Plugin 那样每一个 Extension 都会有一个数据库操作相关的类,然后让 Service Plugin 去继承它。

在 Core Plugin 和 Service Plugin 的介绍中,我始终没有提到和 Agent 通信或者 RPC 的问题,这个问题我在下面的 Agent 部分介绍。

Agent

把服务端做得优雅,微内核之后,我们就要将调用发到 Agent 端去干活了,首先我们要解决的是 RPC 远程调用的问题。为了简化设计,多应用已成熟的东西,Neutron 或者别的 OpenStack 组件都是使用现有的 MessageQueue 服务完成的,而且 MessageQqueue 的调用有单独抽象出来一个模块叫做 Oslo.Messaging,于是对于 Neutron、Nova 这些服务它们也就不需要去考虑底层的 MQ 支持哪些,该如何调用这些问题了。目前 Oslo.Messaging 支持的 MQ 组件有 RabbitMQ、ZeroMQ 和 Qpid。我们平时用的比较多的 MQ Backend 主要是 RabbitMQ,其对 Topic 类型消息的处理过程大概如图所示:

RabbitMQ Topic Exchange

在 RabbitMQ 中,一个消息发出去后,由 Exchange 处理,Exchange 先根据消息类系分类,检查是 Fanout、Cast 还是 Topic 等消息类型,如果是 Topic 的话就复杂一点,需要根据 Routing Key 来路由,将消息发到不同的队列中,可以说队列就是 MQ 消息的终点,因为消息会在队列中等待注册的消费者去消费,消息被消费后即从队列中删除。

为什么使用 RabbitMQ,而不是 Raw Socket 或者 ZeroMQ?那不是更快么?RabbitMQ 提供了很多良好的特性,比如消息的持久化、消息确认机制等等,而且方便扩展,所以现在 RabbitMQ 在大型系统中运用还是很多的。

OK,现在消息能发能收了,但可以通讯还不够,我们需要实现远程过程调用,俗称 RPC,在 Neutron 里这主要涉及到两个类:

Agent RPC

右边的Rpcroxy是服务端联系 Agent 用的,而RpcCallback是 Agent 联系服务端用的。在无论消息段还是服务端,都会运行来自oslo.messagingMessageHandlingServer或类似的类,也就是一个个用绿色线程启的Rpcorker,当RpcWoker收到任务时会去执行相应的调用,在一些 Agent (比如 L3 Agent)里还实现了优先队列,以保证高优先级的任务被首先执行。

举的例子都是 Common agent,因为 Service agent 其底层不一样的话功能、实现和逻辑千差万别,所以 RPC 是比较难统一的,所以不同的底层将有不同的 Service Driver,再由 Service Driver 去实现 RPC,最后再有 Agent 调用 Backend 完成功能。


总结

将分散在全世界的数百名来自不同厂家,专注不同业务的工程师集合起来,完成一个目前没有成熟标准和现成学习者的软件,不得不说开源社区的力量很强大,它既要保证代码的优秀、自然,还要兼顾不同厂商的利益与差异,能在 3 年多的时间完成一个现在我们已经敢把它部署在 UStack 线上,同时运营着共有云和托管云,支撑数千名客户的稳定高效使用的虚拟网络组件已经很不错了。

由于其优异的架构,线程之间的无状态,使得服务的迁移与横向扩展、事件的分离、不同驱动、扩展甚至插件的加载都异常轻松,对于开发者来说,学习和修改的门槛也相对较低,从中能够让我们学习到很多优秀的实践和经验。

当然 Neutron 也存在很多问题,比如 Third Party 的代码混杂,一些地方的实现不尽如人意(Break rule),但并不能够掩盖其优秀的地方。

这篇文章比较宽泛,希望将来有时间能写的更加具体!

    2 Thoughts on “Neutron 的软件架构

    1. 听了王同学的讲座,很震撼,向王同学学习

    2. 谢谢分享,梳理了这部分代码的结构,向你学习

    Post Navigation