问题描述
当OpenStack集群部署规模大,资源数量多时(虚机资源,镜像资源,云硬盘资源,网络,子网,Port资源等)越来越多时, 浏览器前端列表显示随着资源增大会越来越慢。目前前端列表都是在前端JS分页,列表的API都是返回所有数据。
方案提议
-
方案思路
- 使用API分页
优点 可彻底解决列表慢的问题。
缺点 API分页无法实现列表搜索功能。 - 使用缓存方案,后续通过缓存分页
优点 速度快,侵入性小。
缺点 需要保证缓存的实时和一直性, 不同的缓存方案实现的复杂度也不同。 - 优化各个组件性能
缺点 需要优化整个链路上的各个环节的性能,工作量不可估计。
- 使用API分页
-
缓存位置分析
- Horizon rest API结果缓存: 缓存API请求结果,API太多,且查询条件多,难以管理。
- OpenStack API结果缓存: 缓存OpenStack API时的缓存结果,并保证缓存按时更新。
- 数据库查询结果缓存: 缓存sql查询结果,sql查询组合多,缓存难以管理。
综上所述选择OpenStack API结果缓存。
- 方案设计
-
项目架构
cache.png
(图片未更新)其中缓存介质为redis,并且后期加了cache-api组件,通过rpc调用给cache下发task(task msg格式类似jsonrpc)。cache api组件同时也给horizon提供rest api接口。
- 缓存媒介: memcache(后改成了redis,原因参见此前文章:由用户session过期引发的memcached内存分配的思考 - 简书 (jianshu.com)
) - memcache python client: pylibmc(c语言实现,查询性能高)
- 缓存数据结构设计: 以云主机数据为例
-
缓存数据结构
key | value | 说明 |
---|---|---|
instance_uuids | [uuid1, uuid2…] | value为所有云主机uuid列表 |
instance_tenant_uuid1 | [uuid1, uuid2…] | key的末尾的uuid为租户uuid, value为对应租户下所有云主机 uuid列表 |
uuid1 | {...} | value为uuid1对应云主机的数据 |
- 当有云主机数据发生变化时,只需要更新对于云主机uuid对应的缓存数据和instance_uuids以及 instance_tenant_uuid列表即可。
-
缓存数据一致性: 每个种类的缓存均对应一种plugin来维护缓存数据的一致性(nova,neutron,cinder,glance)。 其中每种plugin中主要分为三个部分(periodic_sync, consume_queue, quick_periodic_sync, others)
- periodic_sync: 每隔5分钟对插件通过调用组件client获取负责资源的所有数据,并将缓存数据进行一次更新。
- consume_queue: 通过消费OpenStack组件的notification msg,并更加notification的event type和资源uuid来热更新缓存数据。
- others: 参见FAQ[1]
-
实现:
- 每种缓存插件都有两个个queue,一个用于缓冲msg,一个用于接受cache api组件的rpc调用的msg。
- 当插件运行时会启动多个个单独的线程。其中一个用于消费OpenStack组件的notification,当消费者收到消息后会将notification的msg解码后放入缓冲queue。
- 另外还有一个线程用于处理queue中的msg,每当从queue中获取到msg时,先根据msg中的event_type类型,将msg传入对应 类型的handle_msg方法中进行处理。不直接从MQ的queue中消费消息
- 使用自己的queue做缓冲的原因
- 不会因接收到消息后处理慢就导致msg堆积。
- 对于 有的OpenStack组件有的操作没法notification的情况,我们可以直接构造一个fake_msg后发送到对应组件的queue中,用于 更新组件对应资源的缓存数据。不通过MQ的生产者发送原因,由于平台其他功能也有使用OpenStack的notification (例如计费等)所以为了不影响原有功能,中间加一层queue做缓冲,即防止msg堆积,也能解决组件某些操作无notification 导致的缓存数据不同步的问题。
项目剖析
-
代码结构
escache ├── cache.py # 函数入口 ├── cmd │ ├── cache.py # entrypoint script │ └── __init__.py ├── common # 存放公共函数 │ ├── __init__.py │ ├── memcache.py # 用于get/set/del缓存数据 │ ├── rabbit.py # 用于生成MQ消费者 │ └── utils.py # 公共函数(由于是并发可能导致list中重复append数据,用于解决此类问题) ├── config.py # 配置 ├── __init__.py ├── plugins # 组件plugin,每种组件的插件维护一类资源,例如nova负责云主机缓存数据的一致性维护 │ ├── base.py # plugin基类,组建根据自身需要重写单独的方法 │ ├── cinder.py │ ├── glance.py │ ├── __init__.py │ ├── neutron.py │ └── nova.py └── tests # UT测试脚本目录 └── __init__.py
- 错误处理
所有的exception均在common/base中处理,plugin内部不做try catch处理, 需记录日志。 memcache操作error时,需要重连memcache,原因参见common/memcache部分。 - common/memcache
潜在问题 平台中memcached是多副本形态,当主 memcache和”备”memcache pod之间网络不通时,”备”memcache会修改memcached svc的 endpoints,将memcached的域名解析到自己的pod ip。而cache组件和memecache建立的是一个长连接,horizon中CacheManager每次查询都会建立一个新连接,并在操作结束后关闭连接,会导致数据不同步。
为了解决上述问题,每当memcache操作error后都重新建立一个新的memcache连接。 - common/rabbit
连接RabbitMQ并创建一个消费者,工作模式[Topic](RabbitMQ tutorial - Topics — RabbitMQ
)。 - plugins/base
负责启动3个线程(consume_queue, periodic_sync, quick_periodic_sync)。- periodic_sync(nova为例)
periodic_sync会启动每个plugin的sync_cache。sync_cache中首先会定义两个变量 (sync_uuids, sync_tenant_uuids)分别用于保存OpenStack client查询出来的 数据的数据的uuid列表和对应租户下的数据的uuid列表。
由于OpenStack client list获取资源数据耗时较长,所以在此期间用户可能对资源进行了 增删改,如果直接用这时候list出来的数据并直接更新缓存会导致缓存中数据不是最新的。 因此plugin中会定义两个deque(recent_deleted_uuids, recent_updated_uuids) 分别用于保存OpenStack client list期间内被删除的资源列表和更新的资源列表。每次 sync_cache中执行OpenStack client list前会清空这两个deque。如果listu出的数据的 uuid在recent_deleted_uuids中则说明在list期间这个资源已经被删除,直接跳过。如果 uuid在recent_updated_uuids中,但是没在list到的数据中,说明该uuid对应的资源是在 list期间创建的,则需要将这个uuid添加到sync_uuids和sync_tenant_uuids中。
更新list到中的数据时,需要判断list到的数据的updated字段如果晚于缓存中该数据的updated 字段才需要更新缓存中的数据。 - consume_queue
consume_queue会启动每个pulgin的handle_msg。其中根据msg的event_type来将msg丢 给对应的msg处理函数处理,其中如果删除资源,会将uuid记录到recent_deleted_uuids中, 并且从recent_updated_uuids中移除,如果存在recent_updated_uuids中的话。当更新 操作时当数据在缓存中更新完后会将uuid记录至recent_updated_uuids中。(fake_msg也 由此方法i处理,不同的fake_msg需要根据需要单独处理) - quick_periodic_sync
quick_periodic_sync会触发plugin的quick_sync_cache方法,用于间隔查询某些资源 数据并更新缓存数据,其间隔时间较短,用于一些特殊的资源(例如error的volume),该类 资源的操作无任何notification,所以只能通过OpenStack的client list去获取该资源 的数据,并更新缓存数据。
- periodic_sync(nova为例)
- plugin(neutron为例)
plugin用于维护某一类特定资源的缓存数据,例如neutron用于维护port和floatingip的缓存数 据。但是用户进行网络资源的操作时会引起port的增删,或者floatingip的绑定与解绑操作也会引 起云主机的某些字段更新等,并且由于这些操作虽然引发的数据更新,但是对应的nova或者neutron 却并未发送notification所以会导致缓存数据的不同步,所以plugin中除了维护常规的资源uuid 列表数据之外还需要单独维护例如port_device_data, port_fip_data, fip_device_data, fip_router_data, router_port_data, fip_port_data, lb_port_data等字典,用于 查询资源与资源之间uuid的对应关系(例如port_device_data中以port uuid为key,云主机的 instance_uuid为value,以此当云主机断开网络时,port删除时可以根据port的uuid获取对应 云主机的instance_uuid,并以此构造fake_msg通知nova的缓存plugin更新对应uuid的云主机 缓存数据。该部分数据不仅在sync_cache中需要统一维护(当缓存启动前就存在部分数据有关联关 系,所以需要维护),并且在处理msg时也需要维护其对应关系。
其中每种资源的msg都分为两大类,删除和更新,其中更新中也包含了新建。处理消息时除了正常的 删除,更新数据外,同样需要根据event_type来判断该资源的更新删除会不会引发关联的资源发生 更新,如果会则需要单独处理。并且有的操作引发的关联资源的更新需要等待资源更新生效,所以如 果存在这种情况时,我们需要在send fake msg之前sleep一下或者在关联资源检查状态时加上状 态标志并一直通过OpenStack client获取该资源直至状态符合或者超时,来等待资源更新生效。 (例如云主机绑定公网ip时检查port状态是否为active) - 已知OpenStack组件缺失notification情况汇总及处理
其中处理大多数均通过构造fake_msg通知对应组建更新数据- 云主机绑定或解绑公网ip时需通知nova更新云主机数据
- 申请公网ip时需通知neutron更新port数据
- 新建或删除loadbalancer时需通知neutron更新port数据
- 删除或者断开网络时port时通知nova, neutron更新云主机和floatingip数
- 连接网络时通知nova更新云主机状态
- 创建删除路由时通知neutron更新port
- 路由器设置网关时通知neutron更新floatingip
- 删除错误的云硬盘时无notification,每隔2秒获取一下状态为error的云硬盘数据,并更新缓存数据
- Horizon hooks
horizon部分在api中有一个cache.py里面定义了一个CacheManager的类,nova,glance,cinder, neutron api当调用OpenStack client时获取数据时,先会通过CacheManager获取对应的list数据, 如果未获取到数据,再通过OpenStack client去获取数据。其中CacheManager获取数据支持search_opts 并且只有cloud_admin可以获取所有的缓存数据,否则只能获取对应租户的所有缓存数据。
FAQ
[1] OpenStack中某些操作缺失notification(eg:nova删除错误虚机时不会发送port.delete.end的notification,会导致neutron中port数据没删除)
解决方案:
- 删除错误虚机时,cache的nova plugin收到instance.delete.end消息时判断虚机状态,如果是error望neutron plugin的queue中放入一个port.delete.end msg(使用该方案)
- sqlalchemy中添加trigger,这样可以不通过notification来更新缓存中数据
网友评论