如何在一周内创造$7500/m的价值

2015-01-07

标题当然是噱头。是这样的,我们使用了aerospike数据库的社区版,社区版不支持热重启和连接认证功能。热重启很重要,所以我自己改源代码实现了,花了不到一个星期。对于我们的使用,商业版价格需要每月7500美刀,所以就有了这个标题。

其实这是篇纯粹的技术文章,真正的标题应该叫"aerospike热重启"。

为什么

热重启这个功能非常重要。如果不支持热重启,那么重启后需要扫描整个SSD重建索引,这个过程非常漫长,想一想遍历并处理几百G甚至近T的外存数据就知道是什么感觉。更大的问题是,由于这个时间过长,集群会判断这个节点下线而执行rebalance,而当这个节点终于重启完成之后,由于有"新"节点加入集群又什么做一次rebalance。没有热重启功能的情况下,可能几小时甚至几天,集群都不会恢复到正常状态。之前写过一篇线上的节点重启问题

像什么滚动升级类似的高级功能,更是想都不用想了。滚动升级是指集群zero-down-time,每次更新维护一个节点。而重启节点必须等待整个集群中没有节点处理数据迁移的状态才能执行,否则会出问题。如果没有热重启功能,一个节点一个节点弄,谁会笨到花一个月的时间去滚动升级呢?

分析

aerospike使用SSD做存储时,索引是全部存储在内存的,数据存储在SSD。热重启的原理是,利用共享内存技术,进程退出后不释放索引数据的内存。再次启动时,将内存挂到进程中来,恢复索引。

这里插入讲一点共享内存,系统V的api中提供了一系列的shm函数。用shmget函数可以以一个key去拿到一块共享内存,其它进程也可以拿同样的key获取这块共享内存。不需要使用这块内存之后可以用shmdt来detach掉。有一个引用计数标识当前有多少个正在使用这块共享内存的。调用shmdt会将计数减少。不调用也是可以的,进程退出后系统也会将计数减少。但是即使使用进程都退出了,计数等于0之后,共享内存并不会被释放掉。必须手动调用shmctl函数释放才行。热重启正是利用的这个特征。

去读了一个官方的文档找点线索,aerospike使用的共享内存的key很有规律,都是以0xae开头的。第1个namespace是0xae001xxx的,第2个namespace就是0xae002xxx的。

$ ipcs -m
------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status      
0xae001000 32769      root       666        2097152    0                       
0xae001100 65538      root       666        1073741824 0                       
0xae002000 98307      root       666        2097152    0                       
0xae002100 131076     root       666        1073741824 0

每个namespace分两部分,一部分直接是0xae00x000的,大小为2M。另一部分是若干个0xae00xxxx的,每个大小为1G。后面这种每个1G的共享内存,里面就是索引数据。前面的2M的共享内存,那个是记录了一个namespace和索引的元信息。

接下来就是去代码中找线索。我发现把代码开源出来的人很搞笑,他把相关代码去掉了,但是去加上了诸如“for enterprise separation only”之类的代码注释,所以,以enterprise为关键词就能搜索出很多线索。另外,可以看到有几个xxx_cold.c的文件,就是把相关的代码移除掉后改写的代码。

系统初始化时分别有两个函数as_namespaces_initas_storage_init,热重启只涉及到了这一部分代码,涉及范围不大。另外是加载过程中,所以还不涉及到并发访问相关代码的复杂锁机制,改动才有信心一点。

as_storage_init是存储设备的初始化,冷重启调用的是ssd_load_devices_load,热重启调用的是ssd_resume_devices函数。现在ssd_resume_devices函数被弄成了一个空函数,调用会直接cf_crash,如果能把这个函数恢复出来,就可以实现热重启的功能了。

实施

首先是理解存储的结构。索引的存储用的是一个红黑树数据结构,在内存里面。它的key是一个20byte的digest,value是一个as_record结构,在这结构可以看作是一块存储描述符,通过里面的信息可以知道对应于哪一块SSD,哪一个block,以及在block内的偏移量是多少,这就是一条索引记录。每个block以128字节为单位,数据都是以block对齐的。

数据的组织用的是B树,暂时不需要关注。每一块数据记录头部都有一部分的信息。扫描SSD重建索引,就是每次读了1M的数据进内存,然后扫描每一块的头部信息,调ssd_record_add加一条索引记录到红黑树中。

注意这个红黑树是flatten了的,就是说,内存分配并不是平常的稀疏的分配方式,而是全部在一块连续的内存中,这样才能用一大块1G的共享内存。指针用的是一个cf_arenax_handle结构表示,它不是一个真的指针而是一个基址+偏移的形式。使用时会做一个从cf_arenax_handle到实际内存地址的转换:

as_index *r = RESOLVE_H(r_h);

索引内存管理是用的这样一个cf_arenax_s结构,结构体里面有一些stages,每个stage是一个指向共享内存的指针,大小是1G的。这就是用ipcs命令会看到的几个0xae001100这类的大小1G的共享内存。

为了恢复索引内存,自己实现cf_arenax_add_stage函数,这个函数会加载共享内存地址,将地址保存到arenax的各个stage中。

到这里为止,已经知道了索引的结构,恢复了索引的内容。


接下来是另外一些元信息。也就是对应于那个2M的共享内存里面的东西的恢复。每个namespace都对应了一个2M大小的0xae00x000的共享内存。

具体说包括三部分,涉及的分别是两个vmapx,一个arena,以及两个红黑树根结点的数组。这些分别是namespace的信息和索引的元信息。一个一个说。

vmapx结构是一个数组一个hash表组成的结构,用于aerospike中bin和set的管理,功能是从一个name得到一个id这种映射。使用的时候是hash形态,导出的时候是一条条记录的数组形态,这样就可以放到一块连续内存。恢复起来也不难,重新扫一遍数组建一遍hash就可以了。

arena结构,前面说了,索引是一个红黑树,所有的结点数据都是存储在那个1G共享内存中的。而用于管理的arena结构体本身呢?它并不是在1G的共享内存中,是在那个2M的共享内存中的。这一部分也要恢复出来。

另外一项是红黑树根结点。跟aerospike机制有关,它用的并不是一棵大树,而是4096棵小树。里面有partition的概念,数据被hash到4096个partition中。每个partition对应一棵小红黑树。树的根节点是存在partition结构体中的。而partition是属于namespce结构体的。热重启恢复数据的时候要注意将这部分恢复回来。

对应的代码都放到了as_namespace_setup中。这一块有些猜测的成分,因为不管是从官方文档还是现有代码中,我都不能确定官方也是这么做的。所以有些主观的成分,不管怎么样,it works。


到现在为止,不仅那个1G的大内存,而且连那个2M的元数据也被恢复了。还差什么呢?

改完以后我跑了一下,可以运行,可以正确加载索引内容,也能读到数据。但是发现问题,统计信息丢失了。比如用asmonitor的info命令看,看到记录数量是0,内存使用量是0。

丢失的原因是前面的工作只是把内存重新加载进来。事实上,还需要把索引树遍历一遍,做一些处理来恢复统计信息的。

好好参考ssd_record_add函数,再改一下,ok了。

后续

我简单地测试了一下,4800万记录,热重启几秒钟时间就加载进来了。

重新加载共享内存,遍历索引树的时候,会不断地发生缺页中断,然后将物理内存映射过来。假设操作系统将这部分的内存是swap出去的,那么其实相当于文件读取。如果换成把索引另存一份文件,然后重新加载的过程,其实可以避开共享内存这类更高端的技术的。

这事说起来还真挺搞笑。一个商业软件,开源了部分代码,故意阉割掉了部分功能。我把代码拿过来改,把功能加上去。再去提一个push request,虽然官方不可能采纳的,哈哈。

由于对代码的理解有限,不确定这个改动是否引入问题。比较担忧的部分是ldt功能,改代码的时候没有做特殊的考虑的。

aerospike热重启