Neutron 分析总结
Neutron 是 OpenStack 项目中负责提供网络服务的组件,它基于软件定义网络的思想,实现了网络虚拟化下的资源管理
注意:本文所有的分析都是基于neutron的stable/train版本
本文将从源码入手对neutron进行一个大致的剖析。读完本文您可以了解到的知识有:
- neutron server是如何通过命令行启动的
- plugin是如何加载创建的
- plugin的callback system
- plugin和agent之间的rpc调用
- 如何动手写一个插件并部署到服务器上
一、neutron整体结构
neutron从软件层面整体可以分为两个部分:Neutron Server
,Agent
。
- 其中
Neutron Server
部分主要负责接收来自web服务的RESTful API请求,并将请求分发到对应的Plugin
上。plugin
主要负责做一些数据库相关的操作,并发送通过rpc
和agent
通信。 Agent
部分则是提供rpc server
接口供plugin
调用,这些接口用来来实现实际的业务功能。
二、Neutron Server的启动流程
1. 命令行启动neutron
neutron server命令行启动的命令为neutron-server --config-file /usr/share/neutron/neutron-dist.conf --config-dir /usr/share/neutron/server --config-file /etc/neutron/neutron.conf --config-file /etc/neutron/plugin.ini --config-dir /etc/neutron/conf.d/common --config-dir /etc/neutron/conf.d/neutron-server --log-file /var/log/neutron/server.log
通过查看setup.cfg
文件可知,在我们敲neutron-server
命令来启动Neutron Server时,实际上是调用了**neutron/cmd/eventlet/server/**模块的main
方法。
33 [entry_points]
36 console_scripts =
51 neutron-server = neutron.cmd.eventlet.server:main
52 neutron-rpc-server = neutron.cmd.eventlet.server:main_rpc_eventlet
下面我们来具体分析一下这个函数。
13 from neutron import server
14 from neutron.server import rpc_eventlet
15 from neutron.server import wsgi_eventlet
16
17
18 def main():
19 server.boot_server(wsgi_eventlet.eventlet_wsgi_server)
20
21
22 def main_rpc_eventlet():
23 server.boot_server(rpc_eventlet.eventlet_rpc_server)
server.boot_server(wsgi_eventlet.eventlet_wsgi_server)
实际上就是通过调用wsgi_eventlet.eventlet_wsgi_server()
这个方法创建了一个wsgi app
。
WSGI的全称是Web Server Gateway Interface,翻译过来就是Web服务器网关接口。具体的来说,WSGI是一个规范,定义了Web服务器如何与Python应用程序进行交互,使得使用Python写的Web应用程序可以和Web服务器对接起来。
通过查看配置文件neutron/etc/api-paste.ini
41 [app:neutronapiapp_v2_0]
42 paste.app_factory = neutron.api.v2.router:APIRouter.factory
可以看出,创建一个app实际调用的是neutron.api.v2.router:APIRouter.factory
24 def _factory(global_config, **local_config):
25 return pecan_app.v2_factory(global_config, **local_config)
26
27
28 setattr(APIRouter, 'factory', _factory)
27 def v2_factory(global_config, **local_config):
......
42 app = pecan.make_app(root.V2Controller(),
43 debug=False,
44 force_canonical=False,
45 hooks=app_hooks,
46 guess_content_type_from_ext=True)
47 startup.initialize_all()
48 return app
可以看出实际的初始化流程其实是在startup.initialize_all()
里面做的。它所做的事情就是准备好 neutron-server.service 启动所必须的前提条件,包括:Plugins 的加载、API Resources Controller 的实例化以及处理 API Resources、Controllers、Plugins 三者之间的映射关系,下面我们分别讲解。此函数代码较长,不再列出来,感兴趣可以自行下载源码查看,neutron/neutron/pecan_wsgi/startup.py
。
2. plugin的加载
2.1 plugin概念
上节我们讲到neutron-server
启动时,调用startup.initialize_all()
来完成所有初始化操作,实际上在此函数中,第一步就是调用manager.init()
加载插件。
Neutron的插件分为Core Plugins
和Service Plugins
。其中Core Plugins
只有一个,默认的是ml2
,而Service Plugins
可以有很多,例如:L3RouterPlugin、FWaaSPlugin、LBaaSPlugin、VPNaaSPlugin
等等。Neutron的扩展性很好,所有的这些Service Plugins
都可以通过修改**/etc/neutron/neutron.conf**这个配置文件,来选择是否加载。
[DEFAULT]
core_plugin = ml2
service_plugins = router, firewall_v2, h3crouter
如上所示,如果不想启用router
这个插件,就可以将配置文件修改为:
[DEFAULT]
core_plugin = ml2
service_plugins = firewall_v2, h3crouter
2.2 plugin的加载
进入正题,看一下manager.init()
函数:
294 def init():
295 """Call to load the plugins (core+services) machinery."""
296 if not directory.is_loaded():
297 NeutronManager.get_instance()
看到get_instance
就应该知道NeutronManager
这个类是用单例模式实现的,保证全局有且只有一个实例对象。我们来看其__init__
函数,下面列出部分代码,我对其的理解以注释的形式体现
def __init__(self, options=None, config_file=None):
# 通过配置文件读取Core Plugins
plugin_provider = cfg.CONF.core_plugin
LOG.info("Loading core plugin: %s", plugin_provider)
# 加载Core Plugins的类,生成一个实例,可以理解为调用了ml2的构造函数(__init__函数)
plugin = self._get_plugin_instance(CORE_PLUGINS_NAMESPACE,
plugin_provider)
directory.add_plugin(lib_const.CORE, plugin)
# 将核心插件也放到services插件中,统一管理
self._load_services_from_core_plugin(plugin)
# 加载service plugins
self._load_service_plugins()
def _load_service_plugins(self):
# 和core plugin一个套路,先从配置文件中获取支持哪些插件
plugin_providers = cfg.CONF.service_plugins
# 推测和entry points有关,类似于找到实际的lib路径
plugin_providers.extend(self._get_default_service_plugins())
LOG.debug("Loading service plugins: %s", plugin_providers)
# 循环加载所有插件,即循环创建插件实例
for provider in plugin_providers:
if provider == '':
continue
# 获取插件类
plugin_class = self._get_plugin_class(
'neutron.service_plugins', provider)
# 获取插件类的依赖
required_plugins = getattr(
plugin_class, "required_service_plugins", [])
# 循环加载其依赖类
for req_plugin in required_plugins:
LOG.info("Loading service plugin %s, it is required by %s",
req_plugin, provider)
# 加载依赖类
self._create_and_add_service_plugin(req_plugin)
'''
加载插件类
此函数比较重要的代码如下
plugin.agent_notifiers.update(plugin_inst.agent_notifiers)
这句话声明了每个插件需要通知的agent服务,以及通知agent所用的API接口
'''
self._create_and_add_service_plugin(provider)
2.3 plugin的启动
以l3_router_plugin
为例,创建实例时首先调用构造函数
def __init__(self):
self.router_scheduler = importutils.import_object(
cfg.CONF.router_scheduler_driver)
self.add_periodic_l3_agent_status_check()
super(L3RouterPlugin, self).__init__()
if 'dvr' in self.supported_extension_aliases:
l3_dvrscheduler_db.subscribe()
if 'l3-ha' in self.supported_extension_aliases:
l3_hascheduler_db.subscribe()
self.agent_notifiers.update(
{n_const.AGENT_TYPE_L3: l3_rpc_agent_api.L3AgentNotifyAPI()})
rpc_worker = service.RpcWorker([self], worker_process_count=0)
self.add_worker(rpc_worker)
self.l3_driver_controller = driver_controller.DriverController(self)
l3_router_plugin
初始化时,创建了一个router_scheduler
实例,其扩展功能dvr
和l3-ha
,通过Callback System
订了一些资源变化的消息。最重要的时,创建了rpc server
和rpc client
。其中server
用来接受agent
的消息,client
用来向agent
发送消息。
rpc client
的创建是由agent_notifiers.update()
完成的,它注册了一个类型AGENT_TYPE_L3
和一个具体的消息发送接口类L3AgentNotifyAPI()
。
rpc server
则是通过创建一个rpcWorker
实例,并将其添加到统一的rpc server
中管理。server
通过调用l3_router_plugin
实例内部方法start_rpc_listerners
来创建一个监听线程。
150 def start_rpc_listeners(self):
151 # RPC support
152 self.topic = topics.L3PLUGIN
153 self.conn = n_rpc.Connection()
154 self.endpoints = [l3_rpc.L3RpcCallback()]
155 self.conn.create_consumer(self.topic, self.endpoints,
156 fanout=False)
157 return self.conn.consume_in_threads()
2.4 plugin的callback机制
Callback System 与 RPC 一样是为了实现通信,不同之处在于:RPC 是为了实现 neutron-server 与 agent 不同服务进程之间的任务消息传递;Callback System 是为了实现 core and service components 之间的、同一进程内部的通信,传递 Resource 的 Lifecycle Events(e.g. before creation, before deletion, etc.),让不同的 Core 和 Services 之间、不同的 Services 之间可以感知到特定 Resource 的状态变化。
举例说明 Callback System 的作用:Service A, B, and C 都需要知道 router creation event。如果没有一个中介来采用消息的方式通知这些 Services,那么,A 在执行 router creation 的时候就需要直接 call B/C,告知他们:“我要创建路由器”。但如果有了中介 X(Callback System),那么执行的流程就会变成:
- B 和 C 向 X 订阅了 A create router 的 event
- 当 A 完成了 created router
- A calls X(A 具有 X 的调用句柄)
- X 就会将 A created router 的 event 通知 B 和 C(X -> notify)
整个过程中间 A、B、C 三者没有直接通信,实现了 A、B、C(Services)之间的解耦。这就是所谓的 Callback(回调)。
上一个官方实例:
from neutron_lib.callbacks import events
from neutron_lib.callbacks import resources
from neutron_lib.callbacks import registry
def callback1(resource, event, trigger, payload):
print('Callback1 called by trigger: ', trigger)
print('payload: ', payload)
def callback2(resource, event, trigger, payload):
print('Callback2 called by trigger: ', trigger)
print('payload: ', payload)
def callbackhighproirity(resource, event, trigger, payload):
print("Prepared data for entities")
# A is using event in case for some callback or internal operations
registry.subscribe(callbackhighpriority, resources.ROUTER,
events.BEFORE_CREATE, priority=0)
# B and C express interest with I
registry.subscribe(callback1, resources.ROUTER, events.BEFORE_CREATE)
registry.subscribe(callback2, resources.ROUTER, events.BEFORE_CREATE)
print('Subscribed')
# A notifies
def do_notify():
registry.publish(resources.ROUTER, events.BEFORE_CREATE,
do_notify, events.EventPayload(None))
print('Notifying...')
do_notify()
运行结果为:
> Subscribed
> Notifying...
> callbackhighpriority called by trigger: <function do_notify at 0x7f2a5d663410>
> payload: <neutron_lib._callbacks.events.EventPayload object at 0x7ff9ed253510>
> Callback2 called by trigger: <function do_notify at 0x7f2a5d663410>
> payload: <neutron_lib._callbacks.events.EventPayload object at 0x7ff9ed253510>
> Callback1 called by trigger: <function do_notify at 0x7f2a5d663410>
> payload: <neutron_lib._callbacks.events.EventPayload object at 0x7ff9ed253510>
更多内容可以查看官方文档https://docs.openstack.org/neutron-lib/latest/contributor/callbacks.html
三、plugin和agent之间的通信
依然以l3_router_plugin
为例子,看删除路由这个操作是怎么从plugin
走到agent
的。
1967 def delete_router(self, context, id):
1968 super(L3_NAT_db_mixin, self).delete_router(context, id)
1969 self.notify_router_deleted(context, id)
插件先是调用其父类的delete_router
来完成一些必要的业务操作,删除数据库等,接着就调用rpc消息来通知agent
.
1947 def notify_router_deleted(self, context, router_id):
1948 self.l3_rpc_notifier.router_deleted(context, router_id)
neutron/neutron/api/rpc/agentnotifiers/l3_rpc_agent_api.py
132 def router_deleted(self, context, router_id):
133 self._notification_fanout(context, 'router_deleted', router_id)
105 def _notification_fanout(self, context, method, router_id=None, **kwargs):
106 """Fanout the information to all L3 agents.
107
108 This function will fanout the router_id or ext_net_id
109 to the L3 Agents.
110 """
111 ext_net_id = kwargs.get('ext_net_id')
112 if router_id:
113 kwargs['router_id'] = router_id
114 LOG.debug('Fanout notify agent at %(topic)s the message '
115 '%(method)s on router %(router_id)s',
116 {'topic': topics.L3_AGENT,
117 'method': method,
118 'router_id': router_id})
119 if ext_net_id:
120 LOG.debug('Fanout notify agent at %(topic)s the message '
121 '%(method)s for external_network %(ext_net_id)s',
122 {'topic': topics.L3_AGENT,
123 'method': method,
124 'ext_net_id': ext_net_id})
125 cctxt = self.client.prepare(fanout=True)
126 cctxt.cast(context, method, **kwargs)
可以看到插件最终是通过cctxt.cast
方法来发送rpc
消息,消息的主题在插件启动时指定的rpc消息接口api的构造函数中就已经确定为topics.L3_AGENT
40 def __init__(self, topic=topics.L3_AGENT):
41 target = oslo_messaging.Target(topic=topic, version='1.0')
42 self.client = n_rpc.get_client(target)
消息传递的参数为kwargs
,远程调用的方法为method
。订阅了该主题的agent
就会收到消息,然后根据method
去调用自己的函数。
四、如何自己写一个插件并部署到服务器上
前面已经介绍过,插件其实就是一个类,neutron server
通过命令空间和类名来加载它。所以我们在写自己的类的时候,命名空间一定要和neutron
的原生插件保持一致,否则,neutron server
无法找到该类,更别说实例化了。查看setup.cfg
找到原生插件的命令空间
neutron.service_plugins =
dummy = neutron.tests.unit.dummy_plugin:DummyServicePlugin
router = neutron.services.l3_router.l3_router_plugin:L3RouterPlugin
显然,我们在写插件时,也要使用neutron.service_plugins
这个命名空间。
另外,作为一个插件有一些接口是必须要提供的,比如下面这段加载插件的代码:
def _create_and_add_service_plugin(self, provider):
plugin_inst = self._get_plugin_instance('neutron.service_plugins',
provider)
plugin_type = plugin_inst.get_plugin_type()
directory.add_plugin(plugin_type, plugin_inst)
显然,get_plugin_type
这个方法是必须的。
所以我们在定义插件类的时候直接继承neutron_lib.services.base.ServicePluginBase这个基础插件类即可。
class MyPlugin(service_base.ServicePluginBase):
def __init__(self):
super(MyPlugin, self).__init__()
LOG.info("Load MyPlugin successfully.")
@classmethod
def get_plugin_type(cls):
return 'my_plugin'
def get_plugin_description(self):
return "This is my plugin for test."
接下来我们将该插件部署到服务器上。
首先,编写,setup.py
文件,将插件打包。
假如我们的目录结构是这样的:
那么我们的setup.py
文件应该这么写:
from setuptools import setup, find_packages
setup(
name = "neutron_h3c",
version = "0.1",
packages = find_packages(),
description = "plugin",
long_description = "plugin",
author = "yuhao",
author_email = "me.yuhao@outlook.com",
license = "GPL",
keywords = ("plugin", "myself"),
platforms = "Independant",
url = "http://blog.csdn.net/laplaca/",
entry_points = {
'neutron.service_plugins': [
'myplugin = plugin.my_plugin:MyPlugin',
]
}
)
这里注意命名空间一定要为neutron.service_plugins
。
最后,修改服务器上的**/etc/neutron/neutron.conf**配置文件,修改插件相关的配置。
[DEFAULT]
core_plugin = ml2
service_plugins = router, firewall_v2, myplugin
allow_overlapping_ips = true
在service_plugins
变量的最后添加myplugin
,即可。当然最后还需要重启neutron server
,来让我们的插件加载到服务器上。