本文根据徐海峰2018年5月12日在【第九届中国数据库技术大会】上的演讲内容整理而成。
讲师介绍:
徐海峰,花名:大嘴。10年互联网经验。现任阅文集团首席架构师、技术专家。主要负责阅文集团内容中心分布式系统的架构与实现、海量数据的分布式存储与分布式计算。兼负责公司的专利、技术等开源布道。曾任ctrip国际机票计价引擎架构师、5173分布式存储与计算架构师等工作。多年来一直专心致力于网站的分布式架构、海量数据存储与计算等中间件的研究与实现,并形成有成型的技术认知与理论体系。对大型网站的架构与分布式系统有丰富的实战经验。
内容摘要:
通常的缓存系统(典型如memcahed)普遍都将数据内存化,而不支持持久化。纵使后来的Redis解决了数据无法持久化的“硬伤”,但通常缓存系统的持久化功能是否启用也一直是一个让人很纠结的问题。之所以纠结主要是几个原因:1. 缓存系统启用持久化后性能明显下降;2. 数据开启了持久化,但机器down机恢复后依然无法使用或者数据无法自动更新到最新版本;3. 主存依然是内存,所以数据大小需受制于内存,依然无法存储比内存大的数据,故持久化仅仅是备份;4. 设计的时候没考虑持久化,启用持久化后使用非常别扭;而我们的lest从设计开始就解决了这些问题,并且还带来了更多很有意思、也很有实用价值的技术,比如私有的通讯与存储协议、全程无锁的多线程模型等等。
正文演讲:
今天主要是想和大家分享我们现在用的持久化缓存—lest,说是缓存,但因为是持久化,所以我个人认为称作存储系统可能更好一些,它确实是KV结构,并且包含了String、List、Map等等。目前已上线使用,支持1PB到2PB的数据。
讲到缓存,在大家的印象中缓存像什么?其实缓存和神药有很多的相同之处,首先它们都是为了解决“行不行”的问题,使用之后99%作用明显,1%无作用,而且是立马见效,通常会在几分钟或者几小时失效,而且都是走的“治标治本”的路子,多级缓存,从客户端到数据库。
除此之外,二者的出发点都是为了稳定、快速和持久,通常用户都是不管三七二十一,先用了再说,且还会产生心理依赖,领导对于其效果也会比较满意,自己也感觉从苦逼码农晋升到了金光闪闪的框架师。
虽然现实生活中我们很难拒绝使用缓存,但是缓存用多了也会出现很多问题,尤其是当数据量大和机器多了以后,各种问题就会接踵而至。例如现在的缓存基本都是内存式的,一断电数据就没有了,恢复起来也是相当困难。
做了主备之后,你会发现备机其实没有什么用,主机宕了,切到备机上,很多数据都是不同步的,想要同步还需要时间。前两天,我们还讨论,主备好像没什么用,还是多主比较好用。
最关键的问题就是很难管理。缓存用了之后就扔不了,只敢加量,不敢减量。缓存服务器越来越多,可能从一台变成了两台、四台、八台……不仅管理成本越来越高,写代码也变得很复杂,因为很多缓存系统都会为了速度快而设在客户端,所以,每增加一个机器,所有的客户端都要配置,可能有的做得好的团队会有配置系统自动完成,但要是做的不好的团队,就需要重新发布一下程序,如果要是个新手,很可能还会给你写成个死的。
所以,归根结底还是要强身健体的,为了杜绝这些情况,我们实现了Lest。
首先就是缓存同步,可以做到扩容时无感知;第二,主机宕机了也能很容易的起来,备机可能需要稍微顶一下,但主机必须很快起来,因为我们的访问基本上一天七、八亿次,如果主机宕掉打到数据库上,缓存穿透的话,那就基本上完了。所以我们采用了上述四个策略来解决了这个问题。
我们的缓存内容是String、List和Map。这其中List和Map的存储比较难做,因为其包含有结构的数据。例如,如果要在List中查询从第二个到第十个的数据,Redis很容易就做到了,但如果是全内存,存储在磁盘上就比较困难。
所以,我们自己做了一些设计来实现,上图中就是我们的总体架构图。右上是Tracker,类似于很多大厂都在做的缓存代理层,接下来是存储机器,操作机器会分段,如256段、128段等等,数据会分配到不同的段上去。通讯和存储的实现,我们用了自己设计的协议。
负载均衡,其实是老生常谈了,缓存的一个最大特点就是key要自定义。业务自定义因为要存储到磁盘上,因此很难做类似元数据管理的工作。我们选择的方式是Hash,不过使用Hash比较麻烦的地方是,如果机器增加的话,Hash值也会发生变化。所以,我们在增加机器的时候会有一个小窍门,以2*数字的方式去增加,比如一台变两台,两条边四台,四台变八台。这种方式同步量是最少的,50%,假设你是一台变三台,那么动的数据就是66.7%。如果大家是使用Hash,我建议大家用2*数字的扩容方式会比较好。
数据存储下来之后,我们就需要同步,我们有组的标签,同组之内可以数据同步,相互备份,它是没有Slave的,全部都是主。这里会牵扯到版本问题,我们后面会讲到。
负载均衡的算法就是二次Hash到加权二次Hash的演变,刚开始的时候,我们使用两次Hash去做,第一次Hash得到段,第二次Hash得到是哪台机器,但其实ID生成器生成的ID因为业务的关系并不均衡,这导致缓存的存储量大小很偏,可能出现一台机器中有20%,另一台则有80%。
这种情况也很好处理,加权就可以了,相当于一致性Hash,每台机器都有一个类似7%这样的素数百分数加权。为什么选择素数呢?这是因为对素数Hash会比较均匀。
我们在磁盘上做了一个256×256的文件夹,在磁盘层面就把一些文件打乱。我们知道磁盘对小文件其实是比较可怕的,因为使用SSD,我们现在的成本还是比较高,之后我们会考虑使用磁盘,会加类似B树这样子的东西。
目前,因为考虑到有很多小文件,选用了SSD,从而避免掉了磁盘会遇到的一些问题。如果是1亿KB的数据,经过Hash放到一个文件夹中大概也只有几百KB,不会超过3000KB,这个压力还是可以接受的。
上图是数据存储的模型,最前面是Head,头部加了很多元数据。比如整个是一个string,那么黄色的部分是客户端存下来的真正内容,len表示长度,Version表示版本,我们整套都是用C来写的,因此性能大概会提升十倍以上。并且我们还做了一个保证单调递增的ID生成器,它的算法其实就是一个时间向量算法的衍生,解决了版本控制的问题,换句话说就是哪个数字最大,肯定就是最后的版本。Reserved代表类型,这里存的可能是string、list或者是map。同时为了未来的扩展,我们还会有一个预留出来的地方。
其实List和Map与String差不多,大家看图即可,就不再一一介绍了。
这些存储如果要手工实现可能有些困难,所以我们去做了一个HMS对象,这是一个支持全部类型的数据协议,包括int、long等等。最经典的使用方式是部署在服务器端,因为服务器端使用C无法像Java那样反射,这时我们会用一个数学结构来代表整个内容,如果使用Key的话可以达到log N的查询效率。
以上是我们做的API,几乎可以支持所有的操作,用法与Redis差不多,Redis能做的操作,lest基本也能做。
同步架构如上图,如果是同组的话,两个storage之间到Tracker上拿到数据,然后再做同步,这其中还会涉及到一个高速IP协议。
每次记录都会产生binlog,将binlog分门别类的记下来,然后去其上读即可。
同步复制状态,状态转移就是1 1大于等于2,控制简单,传输数据量大,而复制状态机,控制比较复杂,传输的数据其实很少。
上图是我们第一次对lest做性能测试得到的结果。前段时间,我们申请到了新的机器,我们又重新做了一遍测试。
上图是处理请求正确响应数,其中红色的是万兆服务器 SSD,黄色的是千兆服务器 SATA硬盘。得到的这个性能结果我个人认为还有提升空间,因为我们的客户端不够多,只有十台,至少要有二三十台才能压出它的真实性能。现在的性能数据差不多是它真正性能的60%。
从图中我们可以看到,当数据达到10K以上,性能其实一直在走下坡路,如果大家去对比Redis也会发现,当数据到10K,Redis性能也会下降45%。这说明缓存还是和小数据更合拍。
上图是最大响应时间,图中的822我也不知道怎么来的,可能是一个特别异常的值,除去这个值,其它数值的状态还是比较平稳的,最大响应时间的单位是毫秒,基本上是在一两百毫秒之间。
上图是最小响应时间,其值基本分布在一点几毫秒。
上图是传输量,也就是网卡,很明显,万兆占据优势,这是因为它本来就比千兆要大,其可能只打到了80%,还有10%的增长空间。
上图是SSD服务器压力,CPU的状况差不多,如果十个服务器疯狂打,那么CPU的压力在20%左右,随着数据变大,处理时间变多,CPU压力就下降了。图中还有网络的出和入,其中入会比较小,而出会比较大,如果是Redis,出会更大。
Lest的优劣势很明显,它是一个吃磁盘不太吃内存的东西,具体优劣势可参考上图。相比来说,如果使用Redis服务器需要二三十台的场景,lest三台就可以扛下来。另外,相比其它缓存,lest基本可以做到写代码无感知,另外,我比较推荐使用SSD,因为现在SSD还是蛮便宜的,比内存要便宜。
,