Openstack平台列表缓存概要设计

问题描述

当OpenStack集群部署规模大,资源数量多时(虚机资源,镜像资源,云硬盘资源,网络,子网,Port资源等)越来越多时, 浏览器前端列表显示随着资源增大会越来越慢。目前前端列表都是在前端JS分页,列表的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去获取该资源 的数据,并更新缓存数据。
  • 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数据没删除)
解决方案:

  1. 删除错误虚机时,cache的nova plugin收到instance.delete.end消息时判断虚机状态,如果是error望neutron plugin的queue中放入一个port.delete.end msg(使用该方案)
  2. sqlalchemy中添加trigger,这样可以不通过notification来更新缓存中数据

你可能感兴趣的:(Openstack平台列表缓存概要设计)