1 前言
本文针对开源openstack tricircle组件做如下的反向设计,以帮助理解该组件的设计原理。
2 总体架构
整体上会部署一个Tricircle集群(通常3个节点做集群即可)、多个Openstack集群。统一一套keystone集群、glance集群(不是必须)。每个openstack集群有各自的db、mq,Tricircle集群有独立的db、mq。所有neutron资源的操作都直接调用tricircle集群上的openstack neutron标准接口,其他资源(如nova)操作则调用具体的openstack集群上的openstack标准接口。
下面是tricircle服务引入的重要组件说明:
- Tricircle节点
该节点上主要有如下几个重要组件:
- Tricircle Central Neutron Plugin(简称: central plugin)
负责neutron资源的集中管理,包含network、subnet、port、router、floatingip、security_group、qos等网络资源。提供标准openstack neutron api接口给上层应用操作资源的CRUD。同时也存在central plugin之间通过restapi接口调用local plugin的情况,例如查询一下bottom资源信息时。
这里会启动一个neutron server进程。
- Admin API
对外暴露restapi,用于操作job、pod、resource routing三个重要的数据模型的CRUD。其中job用于表示一个异步任务、pod用于表示一个openstack集群、resource routing用于记录tricircle节点上的neutron资源与pod节点上的neutron资源之间的映射关系
这里会启动一个admin api的进程。
- XJob
维护xjob异步任务。这里会启动一个xjob进程,xjob进程会作为rpc server端处理由rpc client发来的xjob异步任务。xjob进程作为rpc server端在处理异步任务的时候会调用pod的openstack neutron接口。
- RPC
在Tricircle节点上,唯一用到的RPC通信的地方主要在于rpc client(有:central plugin及admin api)和rpc server(xjob进程)之间的交互,只有单向通信,即: rpc client -> rpc server。
- DB
数据库服务,用于存储数据,这些数据包含: openstack neutron资源以及tricircle服务引入的三个重要数据模型:pod、job、resource routing。
- POD节点
该节点上只需增加Tricircle Local Neutron Plugin即可:
- Tricircle Plugin Neutron Plugin(简称: local plugin)
该local plugin相当于嵌入在neutron server和core plugin(当前使用ml2 plugin)中间,从而实现一些neutron资源处理的特殊拦截,通过拦截钩子函数实现特殊化处理。例如:create_port的时候,由于此时local plugin并未创建对应的network、subnet等相关联的资源,这种情况下,通过local plugin拦截请求,通过调用central plugin的restapi接口将network、subnet等相关资源同步创建在该local neutron中。
这里继续寄宿在原有的neutron server进程中。
根据上述内容的描述,总结数据的交互方式有如下:
- rpc交互方式
rpc client(central plugin/admin api) -> rpc server(xjob进程)
- restapi交互方式
xjob(进程) ->central plugin、local plugin <-> central plugin
- db交互方式
central plugin、admin api、xjob都需要访问db
3 数据模型
这里只介绍tricircle服务新引入的数据模型,对于已有的neutron相关数据模型则不再赘述。
3.1 pod数据模型
此数据模型用于表示一个openstack集群实例。pod/az/region_name在tricircle服务中的关系如下:
一个az可以包含一个或多个pod。
一个pod和一个region_name一一对应。
pod数据模型包含如下的信息:
Name |
Type |
Description |
pod_id |
string |
pod对象的uuid。作为主键 |
region_name |
string |
对应openstack集群所属的region name。此region name必须在keystone里已经注册了。 |
az_name |
string |
pod所属的az。对于central neutron而言,az_name为空。如果az_name不为空,说明此pod属于此az。一个az允许包含多个pod。因此多个pod里的az_name属性值是存在相同的情况。 |
pod_az_name |
string |
实际源码中未使用该字段来参与资源调度。 |
dc_name |
string |
pod所处的机房名称。根据华为合营的做法:一个region包含多个az,一个az包含多个dc、一个dc包含多个pod。实际源码中未使用该字段来参与资源调度,估计只是用于展示而已。 |
3.2 job数据模型
此数据模型用于表示一个异步任务。有两个数据结构,如下:
- AsyncJob
这个数据结构用于存储异步任务,主要包含status=new、running、fail这三种状态的job信息。当前也会短时间存储一些success的job(因为是先更新job为success,然后再执行删除,因此存在比较短的时间)。包含如下的信息:
Name |
Type |
Description |
id |
string |
对象的uuid。作为主键 |
project_id |
string |
租户uuid |
resource_id |
string |
job数据。可能包含一个或多个资源id信息,如果是多个则用#分割开,例如:'%s#%s#%s' % (pod_id, router_id, net_id) |
type |
string |
job 类型。具体有哪些类型,参加下面 |
timestamp |
timestamp |
时间戳 |
status |
string |
状态。包含:new、running、success、fail |
extra_id |
string |
额外的uuid。主要用于实现多个worker互斥处理同一个资源。具体原理参见下文 |
这个表中,虽然id作为主键,但是(type、status、resource_id、extra_id)组合有唯一性的约束条件。这样做的目的就是为了实现多worker创建new job时,只允许其中一个worker成功插入,其他worker等待。
- AsyncJobLog
这个数据结构只用于存储status=success的job。感觉只是用于查看,其他作用在源码中暂时未看到。包含如下的信息:
Name |
Type |
Description |
id |
string |
对象的uuid。作为主键 |
project_id |
string |
租户uuid |
resource_id |
string |
job数据。可能包含一个或多个资源id信息,如果是多个则用#分割开,例如:'%s#%s#%s' % (pod_id, router_id, net_id) |
type |
string |
job 类型。具体有哪些类型,参加下面 |
timestamp |
timestamp |
时间戳 |
JT_CONFIGURE_ROUTE = 'configure_route'
JT_ROUTER_SETUP = 'router_setup'
JT_PORT_DELETE = 'port_delete'
JT_SEG_RULE_SETUP = 'seg_rule_setup'
JT_NETWORK_UPDATE = 'update_network'
JT_SUBNET_UPDATE = 'subnet_update'
JT_SHADOW_PORT_SETUP = 'shadow_port_setup'
JT_TRUNK_SYNC = 'trunk_sync'
JT_SFC_SYNC = 'sfc_sync'
JT_RESOURCE_RECYCLE = 'resource_recycle'
JT_QOS_CREATE = 'qos_create'
JT_QOS_UPDATE = 'qos_update'
JT_QOS_DELETE = 'qos_delete'
JT_SYNC_QOS_RULE = 'sync_qos_rule'
3.3 routing数据模型
该数据模型主要用于映射central neutron与local neutron之间的资源,例如:同一个network资源,在central neutron创建的uuid为id1,而在local neutron创建的uuid为id2,这样通过routing数据模型来记录id1和id2之间的映射关系,从而辅助资源的全局调度。
包含如下的信息:
Name |
Type |
Description |
id |
biginteger |
对象的uuid,作为主键。 |
top_id |
string |
在central neutron中记录的资源uuid |
bottom_id |
string |
在local neutron中记录的资源uuid |
pod_id |
string |
pod uuid |
project_id |
string |
租户uuid |
resource_type |
string |
资源类型。具体包含有哪些资源,参见下面 |
created_at |
timestamp |
创建的时间戳 |
updated_at |
timestamp |
更新的时间戳 |
虽然id作为主键,但是要求(top_id、pod_id、resource_type)三者组合有唯一性的约束。这样做的目的是为了防止在local neutron中创建冗余的资源(因为涉及多worker并发处理的情况)。
RT_NETWORK = 'network'
RT_SD_NETWORK = 'shadow_network'
RT_SUBNET = 'subnet'
RT_SD_SUBNET = 'shadow_subnet'
RT_PORT = 'port'
RT_TRUNK = 'trunk'
RT_SD_PORT = 'shadow_port'
RT_PORT_PAIR = 'port_pair'
RT_PORT_PAIR_GROUP = 'port_pair_group'
RT_FLOW_CLASSIFIER = 'flow_classifier'
RT_PORT_CHAIN = 'port_chain'
RT_ROUTER = 'router'
RT_NS_ROUTER = 'ns_router'
RT_SG = 'security_group'
RT_FIP = 'floatingip'
RT_QOS = 'qos_policy'
3.4 shadow agent数据模型
shadown agent设计的目的是为了欺骗local neutron的l2population机制,从而为跨pod实现二层vxlan互通提供理论基础。包含如下的信息:
Name |
Type |
Description |
id |
string |
对象的uuid。作为主键 |
pod_id |
string |
pod uuid |
host |
string |
host id,通常为主机名。 |
type |
string |
agent类型,对我们而言对应ovs agent |
tunnel_ip |
string |
隧道ip,即:vtep ip地址 |
虽然id作为主键,但是要求(host,type)组合有唯一性约束。
3.5 endpoint数据模型
该数据模型用于同步存储keystone里记录的各个pod里各个neutron service的信息。保存的信息示例如下图所示(个人觉得只会用到neutron及keystone相关,其他在central neutron中应该用不上):
该数据模型存储了各个pod下的neutron service的endpoint url信息(public类型),借助这些url信息就可以构造neutron client,进而调用对应pod的neutron restapi接口。
包含如下的信息:
Name |
Type |
Description |
service_id |
string |
service uuid。在keystone唯一标识某个service |
pod_id |
string |
pod uuid |
service_type |
string |
service type,例如’neutron’ |
service_url |
string |
service url,记录的是service public url。利用这个url就可以调用对应pod的neutron restapi接口 |
3.6 其他
这个数据模型和sfc(服务功能链)相关,暂时用不上,不再赘述。
4 源码解读
整个源码目录可分为如下几个部分:
- tricircle/db
源码文件 |
说明 |
api.py |
提供CRUD方法操作第3节所述的资源,调用core.py的接口操作db |
core.py |
抽象封装,适用于所有资源的CRUD db操作 |
migration_helpers.py |
数据库升级相关,可忽略 |
opts.py |
无重要内容,忽略 |
models.py |
定义第3节所述的资源对应的数据库表信息,包含表的各个字段、唯一性约束、主键等信息 |
- tricircle/api
源码文件 |
说明 |
app.py |
配置wsgi app |
wsgi.py |
启动wsgi app入口 |
opts.py |
无重要内容,忽略 |
controllers/root.py |
顶层 rest接口 |
controllers/pod.py |
pod资源相关的rest接口定义,会调用tricircle/db/api.py的接口处理 |
controllers/job.py |
job资源相关的rest接口定义,会调用tricircle/db/api.py的接口处理 |
controllers/routing.py |
resource routing资源相关的rest接口定义,会调用tricircle/db/api.py的接口处理 |
- tricircle/cmd
源码文件 |
说明 |
api.py |
admin api进程入口 |
xjob.py |
xjob进程入口 |
manage.py |
tricircle-db-manage命令执行的入口。与部署的时候创建tricircle相关的数据库表有关。 |
- tricircle/common
源码文件 |
说明 |
baserpc.py |
个人觉得无实际作用,可忽略 |
client.py |
一个很重要的基础文件。具体说明参见4.1节 |
config.py |
注册一些配置项,同时初始化一些mq的连接 |
constants.py |
定义一些常量 |
context.py |
- |
exceptions.py |
定义一些异常类 |
httpclient.py |
实际中未用到,可忽略 |
lock_handle.py |
一个很重要的基础文件。具体说明参见4.3节 |
opts.py |
- |
policy.py |
- |
request_source.py |
实际中未用到,可忽略 |
resource_handle.py |
一个很重要的基础文件。具体说明参见4.2节 |
restapp |
配合admin api使用,不太重要 |
rpc.py |
mq通道的一些初始化操作 |
serializer.py |
配合rpc.py初始化mq通道 |
topics.py |
定义xjob rpc topic常量 |
utils.py |
一些工具类方法,不重要 |
version.py |
- |
xrpcapi.py |
作为xjob rpc client接口,重要的基础文件,具体说明参见4.4节 |
- tricircle/xjob
源码文件 |
说明 |
opts.py |
- |
xservice.py |
创建service,启动service,监听rpc topic |
xmanager.py |
xjob rpc server回调。重要的基础文件。具体说明参见4.5节 |
- tricircle/network
源码文件 |
说明 |
drivers/type_flat.py |
继承原生的FlatTypeDriver类,处理flat网络类型 |
drivers/type_local.py |
Local网络,我们不用,可以忽略 |
drivers/type_vlan.py |
继承原生的VlanTypeDriver类,处理vlan网络类型 |
drivers/type_vxlan.py |
继承原生的VxlanTypeDriver类,处理vxlan网络类型 |
central_fc_driver.py |
和sfc相关,可以忽略 |
central_sfc_driver.py |
和sfc相关,可以忽略 |
central_sfc_plugin.py |
和sfc相关,可以忽略 |
central_trunk_plugin.py |
和vlan trunk相关,可以忽略 |
central_qos_plugin.py |
继承原生的QoSPlugin类,处理qos。 |
central_plugin.py |
central neutron plugin, 重要的文件。 |
exceptions.py |
异常相关 |
helper.py |
一些辅助的方法。比较重要的基础文件。 |
local_l3_plugin.py |
基础原生L3RouterPlugin类,只复写了get_router_for_floatingip方法,其他和原生的L3逻辑一致。 |
managers.py |
继承原生TypeManager类。根据配置的type_dirvers注册各个type drivers。 |
qos_driver.py |
qos driver |
security_groups.py |
继承原生SecurityGroupDbMixin类,处理安全组资源 |
local_plugin.py |
local neutron plugin. 重要的文件 |
4.1 tricircle/common/client.py解读
此文件中提供了class Client,该类主要是openstack service client的操作封装,通过该类的对象,可以操作openstack的所有资源,包含CRUD操作。
虽然该类可以适用于所有的openstack services,但是在tricircle组件中,该类只用于openstack neutron service,因此该类的对象也可以简单看成是一个openstack neutron client,从而能够操控neutron资源的CRUD。
首先来看下该类的__init__初始化工作,具体说明可参见图中的注释,这里不再赘述。
上图中的handle_obj.support_resource的信息如下:
support_resource = {
'network': LIST | CREATE | DELETE | GET | UPDATE,
'subnet': LIST | CREATE | DELETE | GET | UPDATE,
'port': LIST | CREATE | DELETE | GET | UPDATE,
'router': LIST | CREATE | DELETE | ACTION | GET | UPDATE,
'security_group': LIST | CREATE | GET,
'security_group_rule': LIST | CREATE | DELETE,
'floatingip': LIST | CREATE | UPDATE | DELETE,
'trunk': LIST | CREATE | UPDATE | GET | DELETE | ACTION,
'port_chain': LIST | CREATE | DELETE | GET | UPDATE,
'port_pair_group': LIST | CREATE | DELETE | GET | UPDATE,
'port_pair': LIST | CREATE | DELETE | GET | UPDATE,
'flow_classifier': LIST | CREATE | DELETE | GET | UPDATE,
'qos_policy': LIST | CREATE | DELETE | GET | UPDATE,
'bandwidth_limit_rule': LIST | CREATE | DELETE | GET | UPDATE,
'dscp_marking_rule': LIST | CREATE | DELETE | GET | UPDATE,
'minimum_bandwidth_rule': LIST | CREATE | DELETE | GET | UPDATE}
实例化Client对象之后,后面就可以用该对象来操作资源了,无论是CRUD什么类型的资源,最终都会调用该对象中的create/update/delete/get/list/action_resources方法,其中第一个参数resource则为资源的类型。方法里会继续调用到resource_handle.py中的NeutronResourceHandle类对象的handle_create/update/delete/get/list/action方法。
还有个细节需要注意:Client对象里的create/update/delete/get/list/action_resources方法都使用了@_safe_operation作标注,有这个标注的方法,在调用的时候会执行下面的函数(client.py文件最上面的函数):
除了图中给出的注释外,还有如下几个重要的函数需要说明:
- _ensure_endpoint_set
上图中最终确保resource_handle对象配置上self.endpoint_url=region_name对应neutron service的public url,从而能够构造出该region_name的openstack neutron client调用neutron接口。
- _update_endpoint_from_keystone
该方法首先会从keystone db中获取所有的service及endpoint信息,最后构造出:region_service_endpoint_map[region_id][service_name] = url(public url)。接着会根据region_service_endpoint_map的信息更新或创建对应的cached endpoint表项。
总结:使用该类的对象,执行如下的方法
create/update/delete/get/list/action_resources---》接着调用resource handle中的方法handle_create/update/delete/get/list/action---》接着使用_get_client方法获取到openstack neutron client(具体某个pod的,使用region_name查询),最后用openstack neutron client调用neutron资源接口(CRUD)。
4.2 tricircle/common/resource_handle.py解读
该文件中存在两个类,父类为:ResourceHandle,基类为:NeutronResourceHandle。
这个文件在central neutron端使用时会结合4.1节的Client类来使用,这个时候,父类ResourceHandle中的成员self. endpoint_url=具体pod对应neutron service的public url;当这个文件在local neutron端使用时,会直接设置父类ResourceHandle中的成员self. endpoint_url=central_neutron_url(从配置解析出来的)。
父类为:ResourceHandle中的成员self.auth_url为keystone的认证url。因此该文件中提供了一些获取keystone token的方法。
基类为:NeutronResourceHandle中的方法_get_client可用于获取一个openstack neutron client,从而用这个client来调用neutorn资源的CRUD接口。
该文件中还存在各种handle_xxx方法,这些方法里最后都调用openstack neutron client来调用具体的接口。
4.3 tricircle/common/lock_handle.py解读
这里有两个重要的函数,如下:
- get_or_create_route
说明见图中注释。
- get_or_create_element函数
说明参见图中注释。
4.4 tricircle/common/xrpcapi.py
该文件中针对每一个job type会对应一个处理方法,这些方法中会调用invoke_method方法,invoke_method方法中首先会插入一个new状态的job entry,然后通过RPC通知xjob进程(rpc server端)。
4.5 tricircle/xjob/xmanager.py解读
首先要分析下文件中最顶层的方法:_job_handle,直接分析while True里面的内容即可。
图中注释已说明。
对于get_latest_job方法,获取某个status下的资源,可能会获取到多个,因为存在多个worker同时向db中插入某个status且相同资源的情况。处理只需返回最新的job即可。其他旧的job依然存在db中,待最新的job成功处理之后,会将这些冗余的job都删除掉。
对于register_job方法,有个特殊的地方是:extra_id使用的是全0的uuid,这样设计的原因是为了防止多个worker同时注册同一个job,由于(type,status,resource_id,extra_id)组合存在唯一性,而extra_id全0,因此对于同一个job而言,最终只能有一个worker注册成功。
说明参见图中注释。
上图中,finish_job方法需要说明下:
说明参见图中注释。
上图注释中说到:对于fail的job会有周期的任务再次触发尝试,这个周期任务函数如下:
注意添加了注解:“@periodic_task.periodic_task”。
xmanager作为xjob rpc server回调,各个回调方法都使用了注解:@_job_handle,例如:
同时,xmanager还会同时承担xjob rpc client,因为在处理rpc server回调方法时,需要处理那些未明确指定pod的逻辑,对于这种情况,本地查询出所有关联的pod,然后再通过xrpcapi将job分发到具体的pod,例如:
总结:xmanager主要的工作内容是处理xjob rpc server回调函数的处理,同时会启用周期任务处理一些fail job。xmanager会同时承担xjob server和xjob client的角色。
5 设计考虑
5.1 DHCP考虑
为了避免多个local neutron自动创建dhcp port,出现dhcp port ip地址冲突的问题,由central neutron统一管理dhcp port。当central neutron处理subnet创建时,会同时创建一个dhcp port,即:为该子网预留一个dhcp ip。当local neutron创建对应的subnet时,local neutron会调用central neutron的接口查询预留的dhcp port,然后会使用该port的ip创建local neutron subnet的dhcp port。
因此,对于一个subnet而言,对应的dhcp ip在所有的local neutron中都是完全相同的。
Dhcp ip地址完全相同,这样会出现一个pod的vm的dhcp请求会发往其他pod的dhcp吗?对于这个问题,可以利用l2 population机制下发的单播流表来拦截。因为其他pod的dhcp port在本地的pod不存在,也不会创建对应的shadow port(参见5.6),因此通过单播流表可以实现dhcp的请求被发往到其他pod。
当配置dhcp_agents_per_network=xxx多个dhcp实例时,central neutron是否会创建多个IP的dhcp port?local neutron会对应创建多个dhcp namespace?这个能力有待验证。
5.2 Qouter默认网关考虑
当network需要跨越多个pod时,这个时候需要在不同的pod上给subnet创建默认网关ip。Tricircle当前的设计是:不同的pod下,同一个subnet,默认网关ip是不同的。它这样做的目的是为了能够在本地pod下完成三层路由转发,而如果每个pod的默认网关ip都相同,则会出现三层路由转发的流量走到其他pod上,这样路径并非是最佳同时也是不可预测的。
为了实现上述的目标,在local neutron创建subnet之前,会先去调用central neutron的接口创建默认网关port,local neutron会使用这个port的ip作为在local neutron的subnet默认网关IP。在central neutron创建的默认网关port的命名方式包含了:local neutron所属的region_name以及subnet uuid,从而实现同一个subnet,不同pod有不同的默认网关IP。
其实,如果用分布式qrouter,不同的pod下,同一个subnet使用不同的默认网关IP也是没问题的,三层路由转发的流量不会跑到其他pod(因为br-tun上的流表已经做了限制,不允许请求qrouter的流量跑到其他计算节点,也就不会出现三层路由的流量跑到其他pod了)。
5.3 安全组考虑
对于安全组而言,一个难点就是支持remote_group_id特性。该特性实现的目标是:让关联了安全组id=remote_group_id的vm之间能够相互访问。
但是考虑两个vm,vm1在pod1,vm2在pod2,这个时候,我们不能在这两个pod下都创建安全组,拷贝central neutron中的所有rules到这两个pod。因为在pod1上并没有vm2的port,不能通过remote_group_id的特性配置iptables规则,让vm2能够访问vm1.
为了支持remote_group_id,考虑有如下几个方法:
方法1:在pod下都创建其他pod的port,因此也需要对应创建network、subnet。这样做会带来很多不必要的资源消耗。
方法2:扩展remote_group_id的rule,将这种rule转换为多个remote_ip_prefix rules。在central neutron中收集到所有成员vm的ip地址,然后对应创建多个remote_ip_prefix rules。但是这种方法会带来严重的性能瓶颈。严重违背了remote_group_id的设计初衷。可靠性也比较难做。
面对上述这些难题,tricircle则直接不支持remote_group_id特性,对于其他rule因为可以直接的拷贝到local neutron中创建并应用。
5.4 xjob异步管理
首先一个xjob唯一性约束由(type, status, resource_id, extra_id)组合控制。
xjob rpc client会利用xrpcapi,创建一个new状态的job,保存在db里,由于创建的new状态的job,其extra_id是随机生成的,因此会出现相同资源,多个new状态的job存储在db中。xjob rpc server在处理的时候只会选择时间戳最新的new job进行处理。
其他的考虑可以参考4.5节源码的注释,这里就不再赘述了。
5.5 并发性考虑
Tricircle负责在central neutron和local neutron之间创建对应的neutron资源,如果处理不恰当,则会出现在local neutron中创建额外的资源。为了避免这种问题,进行了一些设计考虑,具体可以参见4.3节源码的注释,这里就不再赘述了。
5.6 shadow agent/port考虑
为了实现neutron network跨越多个pod,这里设计了shadow agent/port。
在一个pod里,会为处于其他pod的port(一般只考虑vm port)相应地创建shadow agent/port,然后欺骗本地的l2 population,让l2 population认为这些shadow agent/port是本地的,然后由l2 population机制在相应的计算节点及网络节点上生成vxlan隧道口、单播流表、arp代答流表,最终实现network跨越多个pod实现二层互通。
但是,当计算节点规模比较庞大时,这种设计方式必然会出现性能瓶颈(vxlan隧道数量大、mq消息量大)。为了解决这个性能瓶颈问题,可以引入l2gw转发机制,如下所示:
采用l2gw机制后,本地的pod只需和本地的l2gw及其他pod的l2gw建立vxlan即可,从而大大减少vxlan隧道的数量。
但是依然还使用了l2 population机制,mq的消息量大的问题依然还存在。