索引存储模块设计

  • 索引和数据需要配套使用,才能真正挖掘出系统的价值。Monitor系统中的索引有哪些?索引数据又有哪些组合形式?分别含义是什么呢?下面简单介绍一下索引模块。
  • 前面介绍过,Monitor中最基本的数据有两种:
    ||服务器数据||(server_id, attr_id) - value[01439]
    || 视图数据|| (view_id, attr_id) - value[0
    1439]
  • 其中,(server_id, attr_id)和(view_id, attr_id)就是索引。显然这是两个纬度的复合索引,其每个纬度都是有一定含义的。首先拿(server_id, attr_id)来分析,可以得到两种对应关系:
    • server_id - attr_id[ 0 ~ N]
      • 这种对应关系反映出的是:某台服务器当前(以天为单位)有实时上报哪些属性。

    • attr_id - server_id[0~N]
      - 这种对应关系反映出的是:某个属性当前(以天为单位)有哪些服务器正在上报。
  • 同理,对于(view_id, attr_id)也可以做类似的分析。当然除了直接按照某个server_id或view_id来查询attr_id[1~N]外,还需要提供一些计算交集和并集等汇总的能力,例如求多个server上报的有哪些相同的属性?多个server上报的属性的并集?等等。

索引的结构设计

  • 了解了索引的结构和作用之后,下面详细介绍一下索引的结构设计。

内存部分

  • 占用资源分析

服务器/视图查找属性(server_id - attr_id[1~N])的索引数据:

  • 与数据侧一致,保存 2天 的内存索引 。
  • 为了实现简单,节点采用 定长 的设计结构。 节点结构为:
     服务器/视图查找属性
  • 目前单台服务器支持上报 1w 个属性。因为agent的限制,一台服务器目前仅支持1w个属性的上报。从当前数据分析可知,每台服务器平均上报不到300个属性,上报属性最多的服务器也就2k多个属性。所以1w个属性的设计是充分预估了业务的发展空间的。
  • 单个节点大小约为160kB。保存2天的索引数据,1w个属性(4bytes)和最后上报时间(4bytes),所以节点大小为21w(4+4)+(4+4+4),约等于160kBytes。
  • 服务器对应此索引占用的总内存大小约为16GB。当前服务器数约为4w台,预留一定buffer,按照10w台服务器的量评估,占用空间10w*160kB,大约就是16G左右的空间。
  • 视图对应此索引占用的总内存大小约为 3.2GB。当前视图数约为4000多个,因为视图是由用户动态创建的,所以其增长速度可能会比服务器快很多,按照2w个视图的量评估,那么占用空间为2w*160kB,大约就是3.2G左右的空间。所以单机和视图总共占用的内存大约为20G。
  • 服务器对应此索引的网络流量为12.8 Mbit/s。1分钟内,大约有服务器4w台,每分钟平均上报3次,每台服务器有200个属性的上报信息,所以流量为34w2004(8/60)=12.8Mbit/s。
  • 视图对应此索引的网络流量为 32Kbit/s。1分钟内,大约有4k个视图,每分钟平均上报1次,每个视图有60个属性,所以流量为 4k60(8/60)=32Kbit/s。所以维持单机和视图索引需要的网络带宽资源大约为13Mbit/s。
  • 磁盘io资源要求不高。主要是用于定时dump内存数据到磁盘,方便死机后快速恢复共享内存;
  • cpu使用率也不会是瓶颈,最多使用cpu的地方是计算属性的交集和并集的时候。
  • 综上所述,机型选择B5或者B6都可以。

属性查找服务器/视图(attr_id- server_id[1N]/view_id[1N])的索引数据:

  • 与数据侧一致,保存 2天 的内存索引 。
  • 为了实现简单,节点采用定长的设计结构。为了节省空间,每个server_id/view_id占用一个bit,用于标识此属性对应的某个server/view是否有上报。节点结构为:
    属性查找服务器/视图
  • 设计支持至少 20w 台服务器。服务器id是由小到大分配的,目前id已经分配到将近6w(其中有效的大约有4w多个),所以bitmap最大支持服务器id为30w(有效的id可以估算至少有20w个)。
  • 设计支持至少 5w 个视图。
  • 每个节点大小为 75kB。2天的数据,bitmap支持最大id为30w,所以占用内存约为2*30w/8=75kB
  • 服务器对应该索引占用的总内存大小为22.5GB。目前统计出来的有效属性数大约在13w左右,预留一些buffer,可以按照30w去评估,那么其占用的总内存为30w*75k=22.5GB。
  • 视图对应该索引占用的总内存大小为 3.75GB。所以服务器和视图两块内存共占用内存约为26GB。

架构设计

  • 索引的存储:(上报的协议相同,所以同一个进程写到两块不同结构的内存)
    服务器/视图的存储示意图
  • 索引的查询:(查询的协议不同,所以分别由两个不同进程提供查询服务)
    服务器/视图的存储示意图
  • 单机可以保存全量的索引数据,不需要分布式,所以程序和部署都比较简单;
  • 两台服务器互备,所以没有单点问题。上报数据会双写到主备服务器,由于数据一致性要求不太高,所以主备数据没有强校验。但是如果出现极端情况,导致索引数据混乱的话,可以从数据模块中恢复(遍历数据中的复合索引部分,实现重建索引);
  • 数据都存放在共享内存中。共享内存的好处是,进程可以随意重启,而不用担心数据的丢失。共享内存的结构采用多阶hash,简单方便。

文件部分

  • 类似于数据的存储,超过2天的索引会固化到磁盘上。文件与内存结构最大的变化是:为了节省空间,文件中的每个节点都是紧密排列的。下面列出其占用的资源:
    • 两块索引每天占用的磁盘空间约为100MB。可见这里的磁盘利用率比内存利用率高很多。原因是:拿(id - attrid)这块内存索引来分析,为了效率和设计简单,内存中采用定长结构,每个节点固定为1w个节点,但实际上每台服务器平均只有250个索引,所以稀疏度较高,内存占用空间差不多是磁盘的40倍。
    • 磁盘存储近 2年的索引。考虑到一定增长量,差不多占用磁盘空间100G左右。
    • 每天保存文件需要时间 10s 以内。

整体架构

  • 架构图
    服务器/视图的存储示意图
  • 屏蔽细节,协议统一。对外提供的统一的服务,不必关心到底是什么介质存储数据,视图和服务器的索引数据是分别存储还是统一存储等等细节。
  • 分层设计。分为三层,最下面一层直接与内存或者文件进行交互;上一层实现了视图和单机索引数据的对外统一;最上面一层实现按照时间路由,不同的时间到不同的介质上去读取,层次分明,各司其职。
  • 支持交集和并集的计算操作,减少网络穿越。Read进程支持对不同的索引列表进行求交集或并集的汇总操作,可以避免把多个索引列表拉回本地,再在本地计算交集或者并集的网络流量穿越。

改进方案

  • 索引设计时选择定长的节点,虽然内存使用上稍微有点浪费,但是确实有它自身的优势:
    • 定长节点设计思路清楚,实现很简单;
    • 老架构就是采用类似方案,运行的非常稳定,所以移植过来相关的经验可以传承下来;
    • 评估了一下内存的占用情况,还是可以由一台B5或者B6的服务器轻松搞定的,所以就没有花功夫追求极致了。
  • 下面提供两种其他方案,可以有效的避免内存占用率的问题:
    • 共享内存中采用二级hash+链表方式实现变长节点。一级hash中的存放链表的表头,链表可以实现变长节点存储;另外对应链表还创建一个二级索引,用于快速查找更新节点的最后访问时间。不过此方案确实会比定长节点复杂很多,而且链表的存在会导致索引模块的鲁棒性降低,需要考虑如果断链的话,如何快速重建和恢复链表等问题。
    • 采用相对比较成熟的key-value系统。例如,公司内部的包括ckv,grocery等等,开源的包括redis等。但是这些系统如果想做一些定制化的汇总操作会麻烦一点,例如redis可以支持set之前的交集、并集和差集操作。但是如果想计算多台服务器上报属性的并集,并且统计出每个属性有多少单机上报,就会比较麻烦。