KV系统中的chunk和路由设计
引言:
在目前很多KV系统或分布式Cache中,路由策略都是基于物理节点而设计,例如Voldemort的一致性Hash,其Ring中的每个节点,都会对应现实中的某一台物理机器。这样的设计,对于一个比较单纯且相对独立的KV系统而言,应付某个海量的KV应用场景可能没什么问题。但我们现实中的应用场景往往可能是,多个业务场景,都希望访问一个统一的KV集群。那么在这个海量KV系统中,如何有效的管理各个应用场景,并使之能够尽量均衡地并且高效地利用海量KV中的每台物理机器?是我们在设计这么一个KV系统过程中必须解决的问题。如果仅仅基于简单的类似于Voldemort的管理方式可能还远远不够,它只解决了海量数据的分布问题,但没有解决机器集群之间合理的共享资源管理资源的问题。本文探讨的是如何采用一种有效的方式来解决上述面临问题。
简单的设计:
我们比较熟悉的场景可能大部分都是基于如下图所示的分区设计(一致性Hash+物理节点):
其中每个Node节点对应着一台实际的物理机器,当某个应用存取数据对应的KEY都会经由一定相应的Hash值,而落到对应的物理机上,采用一致性Hash策略,使得我们增加或均衡某个节点的存储容量或访问量的时候,其影响的节点数会相对比较少。
然而,Hash节点直接对应物理节点可能存在如下几个方面的问题 :
- 由于KV系统中为了可用性的考虑,都会为每个物理节点做至少备份一份数据;并且,一般而言备份数据应该分布在不同的物理机器上。由此我们可能发现,任何一个KV集群,理论上我们至少得为其分配至少两台物理机器,如果备份节点更多,那么最少机器数量相对也会更多。但现实的问题是,当我们某个业务场景数据量很少的情况下(其实际使用的资源量远远小于机器的能力时),我们很难将该物理机器给其它业务场景复用,也许我们可以通过手动在该物理机器上启动多个进程实例,像目前大部分Memcached Server所做的那样,但由于每个应用各自独立的使用方式,给我们后续的系统的运维带来了很大的麻烦。
- 这种设计中,由于前端Client直接和后端的物理机器关联,使得后端Server的任何改变都会直接影响到前端的Client的分区和路由;此外,随着应用的增多,服务器的扩展和重启也会给Client增加太多的负担,尽管可以通过配置推送方式做到Client透明,但就变更可能的影响面来说是巨大的,极端情况下,由于Hash策略没有设计好,有可能一台Server的变更会影响所有的业务场景。
如何解决上面所遇到的这些问题?
在此我们需要引入Namespace和chunk的概念,其实两者都是对于应用方而存在的一个抽象的概念。
NameSpace:
如果要解决多个应用场景同时使用同一套海量KV系统的处理及存储能力,我们必须要能在逻辑上将不同的应用场景隔离开来,便于管理和维护,为此需要引入Namespace的概念,一个Namespace其实对应于一个具体的KV应用,例如我们希望在海量KV系统中存储产品明细数据,那么我们则在管理系统中建立一个产品明细的Namespace,这样我们在这个海量的KV系统中,将产品明细同其它的应用场景在逻辑上区分开来了。同时可以通过给予Namespace一定的命名规则,例如com.apache.commons.product来区分不同级别的Namespace,以便于Namespace的查询和管理,甚至Namespace本身还可以在逻辑上进行分组。
chunk
何为chunk?毫无疑问,chunk是相对于物理节点而存在的。在我们前面提到的,Client一旦要根据某个KEY计算出hash结果,在一致性Hash路由算法中可以找到一个对应的物理节点,而chunk就是取代该物理节点,换言之,路由所找到的某个节点已经不再是原来的那个物理机,而只是我们一个逻辑上存在的chunk,通过chunk和物理机的路由表,我们可以找到这个虚拟机所在的物理机。
chunk如何在对应物理机上实现?
- 配置Namespace
由于逻辑概念上,Client已经不再关注于具体的物理机,而只在意其各自的chunk,因此系统在配置Namespace的时候,我们只需要配置该Namespace下面需要多少个chunk,及每个chunk的最大存储容量大小,还有其可处理的最大请求数量等参数;此外,基于Namespace我们可以指定该业务场景下的:分区策略、Hash算法、failover策略、可用性级别、chunk分配(包括容量、访问量的规划)等参数,这样即便是同一组KV集群,针对不同级别的应用我们可以根据业务特点,对其分配不同级别的可用性、一致性及分区容错特性。 - chunk与物理Server的对应关系
chunk在物理机器上对应实现是根据存储类型的不同而不同,但其逻辑上的概念应该是一致的,即一个物理节点对应着一块具有一定存储容量限制的处理单元。以底层存储为BDB为例,我们可能根据该物理机器的存储空间大小,将其分配成空间固定的逻辑块(对应上层的chunk),底层实现时可能每个chunk对应层不同的BDB存储文件,当该服务器启动后,这些存储节点就具备对外提供服务的能力;当然实际应用中chunk的处理能力可能具有一定的动态扩展特性,这应该不影响chunk在物理机器上的对应关系及其实现。而这些可用的chunk,可以根据应用场景的不同分配给不同的业务使用。如果某台物理机器的chunk分配不合理,那么我们可以回收所有的chunk,并且“格式化”该物理机器,将其初始化成新的规格的chunk。 - chunk的分配和回收
一旦一台物理Server起来以后,逻辑上其管辖的所有chunk也已经存在并可随时对外提供服务,但如果这些chunk还没有被加入具体的Namespace,实际上它还没有真正对外提供服务。那么如何分配chunk呢?我们前面提到了Namespace,其实Namespace是整个系统的核心抽象,我们可以把当前系统中可用的chunk(一个chunk理论上应该只对应提供给一个Namespace使用),根据Namespac配置的容量及数据特点,将合适的chunk分配给指定Namespace,这样就完成了chunk的分配过程。
回收过程:一旦某个Namespace释放出来一批chunk,该chunk的数据被清空(注意清空数据,以便释放其存储空间),同时释放出来的chunk可以归入空闲chunk区,以便继续分配给其它Namespace使用。
关键场景演示
在有了Namespace及chunk的概念之后,下面演示几种常见海量KV数据存取场景在chunk下的实现流程:
- 路由
当一个KEY=’A’如图,计算其应该落到chunk2位置的时候,那么系统会查找路由表,以便找到chunk2所对应的物理机M,最后将请求转发给物理机M,并委托其获取实际的值,后者将最终结果返回给请求端。 - 迁移(迁移分以下几种场景)
a) 容量不够的迁移(注意:一旦分区基于chunk之后,理论上只会存在某个Namespace下chunk的存储容量不够,而不可能存在物理机存储容量不够的概念,因为物理机的存储容量是预分配的,当然如果chunk不够了,自然而然也可能会间接使得物理机不够),当某个Namespace存储容量不够的时候,我们可能需要往该Namespace新增新的chunk,那么部分数据需要从老的chunk迁移到新的chunk,如下图所示:
b) 访问量不均衡的迁移,如果访问量不均衡,一种情况是某个节点的访问量太大,这种情况直接调整chunk在Client中的hash算法即可,使得对所有chunk的访问量尽量均匀,这种情况下不会影响物理机器;另外一种情况是某台物理机器访问量太大,那么此时,可以将某几个访问量大的chunk迁移到某些比较空闲的机器,而由于这种迁移是发生在物理机之间,对于Client而言,chunk完全没变,因此对Client也完全透明,不会产生任何影响。
c) Failover的迁移,Failover的设计是基于chunk来设计的,即假定某个Namespace设定chunk是一主一备的方式,那么当访问chunk的主节点失败的情况下,自动切换到备份的chunk,因此在配置主备的chunk分配算法时,需要将其分散在不同的物理机上。
逻辑结构图:
小结(chunk的优点分析)
- chunk将硬件资源进行一定程度的标准化抽象,使之成为一个方便控制的虚拟单元;
- chunk可以将一个大的资源有效的划分成多个相对逻辑独立的个体,使得在访问遍历这些个体中的数据时,不必遍历整个资源池,而只需要通过映射关系找到这些更小的逻辑单元,再进行遍历即可。从某种意义上来说,它有点类似于构建在硬盘上的文件,文件作为一个存储的基本单元,结合其上的文件管理体系,可以很方便的查找并定位一个大的磁盘存储体中的某一部分内容,同时使得诸如查找、删除、移动、回收可用空间等操作更为便利。试想,如果没有文件作为基本存储管理单元,数据存储在硬盘上将是一个灾难。
- 文件在另外一层面的作用是起到隔离的作用,只要大家采用相同的文件系统,上层对文件的读写可以完全忽略底层存储介质的特性,底层可以是硬盘,可以是闪存,可以是光盘等任何其它的东西。chunk的设计也在某种程度上具有类似隔离的效果,我们可以将一个chunk的数据迁移到另外一台物理机,而无需关心底层数据的存储结构。在有些KV系统中,也许底层采用的是各种存储层,因为chunk是一个上层抽象的概念,对最终用户而言,所看到的只是一个个逻辑上存在的chunk,而且这些节点不再与具体的物理机器一一对应,这对于一个物理机器通过多个chunk共享资源成为可能,且多个物理机器间的异构存储之间的自动迁移数据也成为可能。
- 基于chunk,可以方便的实施动态数据迁移。