一种基于虚拟机的ADT服务器

1   摘要

这篇文章介绍了一种基于虚拟机的ADT服务器的设计,该文章尤其针对redis

2   概念

2.1   ADT服务器

该文章主要讨论的是key-value类型的内存数据库

在内存数据库的设计中,有两种思路:

  1. 将任意数据序列化后存储在内存中,数据库不了解保存的内容类型
  2. 将特定类型的数据用既定的接口存储在内存中,数据库了解保存的
    内容的类型,并能对其做精细化的控制

第一种方式的典型代表是memcached, 而第二种方式的典型代表则是redis

这里我们主要讨论第二种方式。

3   redis分析

3.1   redis是什么

以下摘录来自redis官方的描述文档:

Redis is an open source, advanced key-value store.
It is often referred to as a data structure server
since keys can contain strings, hashes, lists,
sets and sorted sets.

注意,它也认可自身是一个数据结构服务器的说法

3.2   redis有什么特性

  1. 服务器提供多种数据类型,string, list, set, hash, sorted set,

    并且根据每种类型提供相应的操作方法,例如对于sorted set类型,

    就有获取某些区间内值的操作方法,这个对于典型的web列表分页是

    非常有帮助的

  2. 服务器的数据类型是抽象的,比如hash类型,自从2.2以后,对于小数据量

    的hash类型,使用的是一种优化过的叫做zipmap的存储,而大于一定数量

    以后,则切换到另外一种存储,(由此引发了切换攻击,:])。这一点

    是很便利的,用户得到了使用上的便利与存储上的便宜。

3.3   redis有什么问题

3.3.1   数据类型仍然不够丰富

redis相对memecache开启了内存存储与操纵结构数据的先河,大量的用户开始

把数据存储迁移到redis上,而把后面的sql只当作一种备份策略,但是问题在于

传统sql能够提供的一些策略并非全部都能映射到redis当前所能提供的一些方案

上的,例如 sorted set数据类型 只能根据一个score来排序,而传统sql方案可能

会支持两个到三个field的排序,ORDER BY field1 DESC, field2 ASC 这样的效果

目前官方是不支持的。又比如要限制某些类型的取值之类的问题,存储人的年龄的

至少不应该有负数吧

3.3.2   策略不够灵活

前面说到,redis的数据类型是抽象的,举的例子是hash类型,提到小于一定数量的

hash类型其存储类型是使用zipmap,而超过以后则切换到另外一个存储类型,虽然

这个边界值是可以调整的,默认是512,你可以根据需要来调整,但是过了512以后又

到了65536呢?也许你可能要考虑使用SSD硬盘来替代部分内存的工作,毕竟内存虽然

便宜又大,但如果真超过了机器限制转而走集群的话,那速度未必有走SSD快呢。当然

这个问题官方不是不能解决,而是解决无力,因为官方不能保证用户服务器上有SSD,

或者是虽然你把数据都存在redis里了,但是有许多数据只是衍生的数据,比如sorted

set这个类型大多数情况下就是冗余的,相当于sql里的一个索引,一般来说他的member存

key引用,而score则存对应的排序依据字段。这种情况可能不同的数据类型重要性不一样

我们可能希望string和hash类型的数据都能够在硬盘上有一个备份,而list set sorted

set这些类型 可能由于存储的是key引用,所以不那么重要,丢了还可以重建,无所谓,

但很可惜官方目前无法提供如此灵活的策略

3.3.3   命令设计没有策略

redis的协议是人类友好的,命令式的,基本上来说是 数据类型+操作 对应一个命令,

但问题在于有许多操作从抽象角度来说是一样的,既然redis在数据类型上可以提供

抽象的数据类型,为何在操作上不能也做到这一点呢?

我举个最简单的例子就是 INCRBY 和 HINCRBY 这两个命令,这两个其实抽象的操作

效果是一致,都无非是将该命令相关参数确定的某个值给加上一些数值而已。但是由于

INCRBY是找到一个string类型的key 并增加他的值,而HINCRBY 是找到一个hash 中的

一个key ,并增加他的值,参数个数都不一样,可能大家会有疑问,这个怎么抽象?

其实换个角度来想问题,你就会发现参数个数是可以一样的。

我们参考下web前端的开发,以前要修改一个element,使用传统的w3c那套DOM操作,

你需要一层套一层的get_element_by_name, children 之类的调用来定位到最终需要操

作的元素上,然后调用相关的操作函数,这个一层又一层的调用层数是不固定的,有可

能1层就到,也可能5层,6层,所以这个问题与INCRBY和HINCRBY的参数个数不一样是类

似,最终的解决办法是,把定位当作一个过程,提供一些参数来一次定位,比如jQuery

的选择器,只要你提供一些魔法参数,立刻就能定位到相关的元素上,然后调用相关的

操作了,redis其实也可以这么做,无非是先定位,使用一套标记方法来限制数据类型,

分割符什么的 ,我现在就可以设计个简单的标记法解决INCRBY与HINCRBY的问题:

1, 设计一个命令叫 PLUS, 他只有固定两个参数,一个是定位器,一个是值

2, 定位标记可以规定,#开头的为hash类型, $开头的为字符类型,.(点)作为分隔符

    因此必须相应的禁止redis的key中使用特殊符号如# $ . 之类,这不是问题

3,根据1和2的规定 原来的 INCRBY str_key 1 命令可以改为 PLUS $str_key 1

   而 HINCRBY hkey field 1 则可以改为 PLUS #hkey.field 1

4,该方法还可以进一步套到 sorted set上去。

3.4   redis官方的补救

官方也意识到了一些问题,可能没我想得那么多,所以2.6以后带了lua script,原来只

是个分支,但是antire已经弄到主版本了,但问题在于lua虽然跟python ruby比是个小巧

的语言,但是在redis这种应用场景里,就算是个大语言了,lua核心也谈不上最小,

iolanguage就号称vm比lua的小多了。关键还不在于此,lua为着通用目的考虑,给语言加

了一些特性,这些是定制化无法删除的,你总不能把那套meta table的机制删掉吧?该类

脚本一出手,可能自身的语言机制消耗的性能要比完成的逻辑消耗的性能大多了,lua本

来就是面向table的,如果真的性能比redis的hash高,那就完全可以自己做个redis

server的角色了,我以前写的饭否爬虫就是用lua的table在内存里缓存了200k的用户信息

用起来就跟redis的hash一样的。

当然官方还无意识的提供了另外的有一些补救,比如由于是c写的,代码组织上又比较友

好,所以实际上你当然可以根据自己的特殊需求来定制一些数据类型,例如前面提到的

支持多个排序依据的sorted set,但是阿,这解决不了所有问题哥哥,如果你想在hash里

的一个field里再存一个hash,并且要跟系统的实现一致,你怎么办呢? 如果你是自己实

现了一个 naked hash 选定调用了官方的一个实现,那么如果有另外一个人也实现了一套

hash,你是否又要改代码以便在某些条件下去调用他的实现函数呢? 所以说这是个泥淖

任谁走进来,过一阵都会陷下去无法自拔了。

3.5   我的总结

redis 相比 memcache是开启了一种使用内存的新方式,这个开创性的举动当然是值

得赞扬的,但是一旦现在大家都加入进来,大规模使用以后,是可以发现其中存在的许多

问题的,例如 redis比memcache多的就是数据类型,但自己数据类型不够怎么办? 还有

策略不灵活,以前只是把memcache当缓存用,这个问题无足轻重,现在有的已经当作主要

数据库用了,那么这个问题就很重要了,再有就是命令设计的不够规范,或者再具体点,

不够正交,基本上是需要一个功能就多个命令,而不是从全盘考虑是否需要增加新命令

还是修改已有命令的实现。这个有点类似处理器分类中的 CISC 类型。像那个INCRBY与

HINCRBY两个命令的存在就好像x86里有这种寻址那种寻址N多种寻址一样。

4   我的方案

针对redis的那些问题,我给了一个另外的解决问题的方案。 这个方案在文章标题里就有

体现,既基于虚拟机实现的 ADT服务器,这个解决方案有两个重点:虚拟机与ADT

4.1   虚拟机

就像前面提到的那样,redis目前就像是CISC cpu, 有一个功能就加一个指令,这个

颇有点头疼医头,脚疼医脚,既然谈到CISC,那么不得不提到RISC,RISC对于CISC的改进

可以看得到的就是简化指令,这个大家可以参考下intel的x86指令集砖头书与mips的

mips32指令集的示意图,两者的差别应该是很明显的,如我先前所提到的那样,INCRBY

与HINCRBY 完全可以设计成一个指令,只是得配合另外一个定位功能,所以就像RISC虽然

精简了指令集数目,但往往实现同样一个功能会增加几个指令一样,我之前自己设计的

PLUS指令可能就会相对INCRBY和HINCRBY要多出一些判断数据类型以及多重定位的处理步

骤,这一点就我个人来说,是可以接受的。

另外的问题是策略不够灵活,我之前说过你完全可以自己定制类型实现,但如果是

嵌套的类型就比较难办,你得能够调用其他人写的实现,如果你不了解内在的实现,就c

语言来说就无法动态的调用相应的实现,也许函数指针是可以的,但你得定个调用规范,

类似FFI那样麻烦,这种情况下,不如使用分离式的设计,既将redis server分离为虚拟

机与存储器实现两个部分,虚拟机指令集应该由官方控制,但是预留客户定制的指令空间

至于存储器实现那就可以官方只实现经典的那些,而客户可以自行根据自己的策略实现某

种符合官方定义的数据类型,例如带SSD备份的hash类型,而list仍然只用在内存里的方

式,掉电不管。

这里的好处在于,通过分离式的设计,达到了策略上的完全灵活,一个命令,你既可

以带检查,也可以直接映射某个存储类型提供的操作,这一切取决于你的程序,相比较

redis2.6提供的lua方案,这个虚拟机的开销要远比起个lua其语言的开销小多了。另外

这个方案的好处还在于提供升级与降级的简易方案,写过虚拟机的朋友都晓得,移植cpu

远比移植一个完整应用容易,因为cpu就那几个指令,像我的tweezervm那更是精简得吓人

因为是堆栈式的,只有30来个指令,c/py的版本都能轻松实现,当然,其实你也可以用

fpga烧录一个专用的cpu,这个相比你的汇编实现的redis还给力哦。何况,必要时候你可

以轻松切换到jvm上去 :] 考虑到最近facebook正在往jvm上迁,企业用户应该更喜欢这

个方案吧。

4.2   ADT

ADT就是抽象数据类型的意思,学过c的应该有所了解,我虽然半路出家,恰好也在

云风那学到了这口黑话。ADT的好处在于你可以依据策略来调整实现,但行为的效果却是

对外一致的。这种方式,当然是很适合虚拟机这种设计的,因为你如果用c的宏 那只是在

编译时候就确定的了,在你运行时就糟糕了。另外核心的逻辑代码能够大大的精简,我看

c代码有大量的宏判断,为相关的调用做数据准备之类的冗余,核心逻辑往往就隐藏在这

一堆里,十分影响后来的人理解系统

4.3   具体的工程方案

我是个开发工程师而不是CS理论科学家,所以我能够给出一些工程上的实践方案,由

于ADT的部分实在是很简单,而且具体的工程方法与策略有关,我当然无法给出什么具体

的建议,因此我着重针对虚拟机的设计给出一些建议

4.3.1   虚拟机的建议

  1. 要设计cpu首先的一个问题是,设计成寄存器机还是堆栈机?关于这个的争论真是一坨

    又一坨,我个人比较喜欢堆栈机的概念,但是从性能上来讲,如果是软实现,寄存器

    机多半由于其可以映射到真实cpu上而变得性能很高,反观堆栈机,从forth的实践

    来说,一般操作深度大概是20左右,这个在mips机器上可以映射到32个通用寄存器中

    的20个,但是在x86上就悲剧了,当然如果你使用特殊硬件的例如chunk moore的

    green array,那情况又颠倒过来了。只是现实中毕竟是寄存器机比较多。至于寄存

    器机中存在的流水线,超标量,乱序等工程方法那倒是不必要的,我们应该向GPU看

    齐,他的内部是有好多的流处理器,每个只处理一个任务而已。

  2. 本来我是希望更灵活一点,把数据类型也变成一种附加的指令数据,但是由于不同的

    抽象数据类型有着完全不同的操作,而且由于存储实现那已经够灵活了,所以倒是可

    以把一些抽象数据类型独有的操作给独立出来,比如只有sorted set类型才有取排序

    后范围的指令,也就是 zrange家族指令,这个用在其他抽象数据类型上是无用的。

    至于说 给一个字符串类型设置字符串值与给一个哈希类型的一个field设置字符串值

    这种的是可以合并成一个指令的,既给某一个存储引用设置字符串值,当然前提是之

    前有个指令定位到了那个存储引用。

  3. 此外需要一些专有加速,比如查找hash key, 不光是在全局找,也可以在一个hash类

    型下找,甚至可以在一个hash类型下的嵌套hash里找,这需要设计者的通盘考虑,并

    设计出一套规范hash实现的机制。又比如一些 整数/浮点数检测之类的常用辅助函数

  4. 虚拟机的指令编译可以考虑做在服务器端,我指的是应用服务器,不是虚拟机本身,

    而客户端仍然像以往一样根据既定的协议发送指令过来,并且,客户端可以像soc

    开发一样,在线运行时给某个指令烧录其他的实现。考虑到一个指令往往逻辑并不

    复杂,这完全是可行的

4.4   我的方案的一些问题

  1. 代码品质问题,有可能某些恶劣的实现拖累了整体的运行速度,或者是某些实现

    会泄漏内存,额。

  2. 多种实现的冲突问题,比如两个插件都强调写硬盘,由于互相争抢,反而有可能

    导致双方的效率都低下从而无法达到预期的效果

  3. 开发者的预期可能和具体的实现不一致,不过我觉得出现这种问题,多半是那种

    开发与运维有隔阂的大公司,小团队应该问题不大。

Comments !

blogroll

social