Memcached是Facebook用来解决高负载的方案。当前很多公司尝试去构建高负载基础架构时,都会采取这个方案。
架构的演变
许多公司在构建网站的时候,首先考虑的往往都不是基础架构的问题,而是将精力集中在如何推广,设置广告位之类的东西。因为那些东西才是获利的关键,仅当他们碰到了当前架构的性能瓶颈,在迫不得已的时候才会想着去优化改进架构。
下面我们将讲述处理能力由弱到强的架构演变
单台服务器架构
当你的网站的人流量很小,仅有很少的用户访问时。此时前端和后端可以全部汇聚在一台服务器上即可。一台服务器上同时运行了Apache、php脚本和MySQL。
多前端、单数据库服务器架构
当你的网站的人流量增大,越来越多的人通过网络访问网站,每个人访问网站,服务器都会调用php编写的服务来生成指定的网页给用户。此时服务器的CPU资源将被php脚本吃满并不够用,我们需要给php脚本的执行提供更多的CPU资源。
此时就会转向第二种架构,我们将提供更多的机器(前端)专门用于运行Apache和用户交互并执行PHP脚本。而数据库将单独运行在一台服务器(数据库服务器)上,它用来处理所有来自前端服务器对数据库的请求。
这种架构还是十分简单,前端的性能可以通过不断增加前端服务器来解决。但是随着规模的扩大,数量仅有一个的数据库服务器会达到性能瓶颈,此时就需要增多数据库服务器,也就是我们所说的分片,但是这就涉及到了分布式事务的问题了。因此第三种架构和第二种架构之间存在的技术沟壑比较大。
多前端、多数据库服务器架构
第三种架构是基本所有大型网站都会演变过来的形态。此时,这个网站有上万的用户,大量的前端服务器,以及多个数据库服务器。网站中的数据通过分片拆分在这些数据库服务器上。
此时,前端服务器需要知道数据库服务器的配置信息,并通过配置信息来得知php脚本所涉及的数据在哪台服务器,并和那台服务器进行通信。
这些分片数据库可以并行读写数据,这给性能带来了极大的提高。
但是有一点问题就是PHP脚本的执行需要数据库分片的配置信息,当数据库的配置信息发生了改变,例如:数据库服务器数量的变化、数据分布在服务器上的方式发生了变化。前端服务器都需要及时得知,并丢弃过时的配置信息。当然这些配置信息的管理,我们前面就学过,可以使用zookeeper来解决。
此外,在分片数据库上执行事务需要使用特别的分布式事务方案。分布式事务的执行速度往往会比较低。但是这种架构能使得网站承载更高的容量,此外像facebook运营的业务对延迟的要求并不高,因此还是可以接受的。
但是这种架构还是存在两个关键问题。1、分片无法解决热点数据的问题,热点数据依旧只有一份,会出现数据库服务器负载不均衡的问题;2、成本过于高昂,每台数据库服务器都很贵。
因此,第四种类似facebook中使用的架构出现了,这种架构中使用了memcache。
多前端、多数据库服务器、memcache粗略架构介绍
memcached比MySQL数据库服务器更快。memcache作为数据库的缓冲区,可以将一些比较常用的数据放到memcache中。前端的绝大多数请求都是读取数据,而memcache中存放了常用数据,前端直接访问memcache即可获取,无需再向数据库请求。而写请求是需要持久化保存,前端直接向数据库发起请求即可,随后将memcache中过时的数据无效化即可。
读取操作和memcache的交互
当前端读取数据时,先向memcache发起get动作,查看memcache中是否有目标数据。如果有则直接返回即可;如果没有的话,那前端向指定的数据库服务器发起请求,返回数据后还会向memcache发起set动作,将这个读取的数据保存到memcache中。
memcache十分简单,其实就是在内存中有一个大型的hash table,仅需检查这个table中是否存在这个key,随后即可返回结果。整个过程十分迅速,在相同的硬件条件下,memcache的读取速度要比数据库的读取速度快10倍以上。可见memcache的性价比比数据库服务器高了太多了,这能帮助公司剩下大量的金钱。
针对上面读操作中还有一些细节需要讨论:
为什么memcache没有命中时,不直接让memcache去请求数据库,接受到数据后直接保存到本地并返回结果给前端?
memcache中存储的数据和数据库中存储的数据的不同,一般来说前端的php脚本会对数据库中取得数据进行一定的处理,将其转换为html,而前端从数据库中取得数据后也不会立马将其缓存到memcache中,而是处理后,再将这个数据缓存到memcache中,这样可以下一次读取操作将可以省去处理的时间。
此外,memcache的设计十分简单,它根本不理解数据库中的数据以及自身缓存的数据,那些逻辑都是存放在前端的php脚本中。memcache这种设计也是叫look-aside cache,即前端来操作memcache和数据库中的数据,memcache无法直接和数据库进行交互,这也使得memcache并不是必需的存在,它好比是一个附加模块,即使没了,前端也可以正常向数据库服务器请求。
但是这种架构还是存在些许问题,例如memcache中的数据和数据库中保存的数据可能会失去同步,还有memcache服务器一旦崩溃,就会有大量的请求转发给数据库服务器,这会大大增大数据库的压力。假如原本memcache的命中率为99%,memcache基本把所有的读请求都处理了,后端数据库不会收到多少读的请求。一旦有一个memcache服务器崩溃,那么这个memcache上承担的读请求将转移给数据库服务器,如果有4台memcache服务器,一台崩溃了,那么数据库服务器就要处理量就会由原来的1%变为26%,这个请求数量将暴增26倍,这大概率会让数据库服务器直接崩溃。因此,一旦采取依赖缓存机制,那么必须要采取一些严谨的措施来避免数据库在没有缓存的情况下直接暴露给前端。
而Facebook的中采取的存储架构添加了很多机制并成功解决以上的问题。
Facebook采用的架构
脸书拥有大量的用户,记录用户的推文、状态、点赞记录等信息。这些用于向用户展示的数据,用户在使用脸书的过程中,对于数据一致性的要求也不高,即使读取得到的数据是几秒前的也可以接受。但是用户需要另一种一致性,就是用户刚更新了某个数据,他后续读取时应该能够第一时间看到更新的结果。这些特性将在后续的设计中提及。
下面具体介绍架构。脸书有两个数据中心,分别在西海岸和东海岸,西海岸的数据中心为主数据中心,西海岸的那个为备份的,两个数据中心就能就近提供读请求,但是写请求都要发送给主数据中心来处理,同时这些更新操作也会被发送到东海岸的数据中心中。
这就是总体架构图了,看上去和上一种架构没什么差别,就只是多了一个通过冗余复制的数据中心。下面深入讨论细节。
如图所示,用代码原语简单介绍了,前端进行读写操作的步骤,读取操作和上个架构是一抹一样的,这里也不赘述了。
写操作和memcache的交互
当前端写数据时,向指定的数据库服务器发起请求,随后便会向memcache发起删除刚被更新的数据,删除这个过时的数据。同时,数据库服务器接受到了这个写请求后,paper中介绍了数据库服务器具备mcsqueal机制,数据库还会将相关的delete操作发送给持有该数据的memcache服务器。
性能方面
一般来说性能的提升可以通过分片和复制。Facebook把这两个技术结合使用了。
分片:对数据进行分片,将数据分散到多台服务器上,多台服务器可以独立运行。多台服务器中的内存都是独立的,可以存放不同的数据,因此内存效率高。如果所有数据的使用频率都差不多,分区可以很好的工作,但是如果存在热点数据,但是分区无法帮助解决热点数据的问题,热点数据还是只有一份。同时,分区使得前端服务器需要和分区进行通信,当分区数量越来越多,通信开销也会变得明显。
复制:对数据进行复制,这确保热门资源能够分布在多台服务器上。这很好地解决了热点数据问题,并且即使有多台服务器,由于这些服务器上存放的都是相同的数据,因此前端服务器每次仅需和其中的一台服务器通信即可,这不会增加TCP的连接数量。但是众多服务器都是存放相同的数据会导致多台服务器中能够存放数据量并没有增加。
以上就是分片和复制的利弊。facebook在不同的层面上,分别使用了这两个技术。
东西海岸的数据库层面的复制
首先Facebook的东西海岸层面的两个数据中心的数据库服务器是复制关系,都保存着相同的数据。但是这两个数据中心的memcache中的数据是不同的,因为不同地方的人们的访问偏好不同,因此存放在memcache中的常用数据也是不同的。
为什么东西两岸的数据中心的数据库采用的是复制形式?
因为,前端服务器来动态生成网页时,往往需要从memcache或数据库中请求数十甚至上百个data item,如果两地的数据库是分片关系,那么前端服务器大概率需要向彼岸的数据库中请求数据,这两地的巨大距离差距会给读取操作带来巨大的延迟。如果所有读取操作都是向本地的memcache或本地的数据库中获取,那么就可以快速完成这些查询,基本不会有延迟。
此外,采用复制的形式可以提高数据库的可用性,当西海岸的数据库崩溃时,东海岸的back up将会顶替。
不过这种形式下,东海岸的前端想要发起写操作,需要将操作发送给西海岸的primary数据库服务器,这会给写操作带来很大的开销,不过相比读操作,写操作的发生频率很低,因此在设计上,这是一种很好的取舍。
单个数据中心内层面的复制和分片
数据中心中的数据库都是采用了分片的技术,数据被划分到多个数据库服务器上。
数据中心中的memcache既使用了复制技术又使用了分片技术。在一个数据中心中,实际上有多个集群,每个集群中是由一堆前端服务器和memcache服务器组成,这些集群都是互相独立的。一个集群中的前端服务器只会和本集群中的memcache服务器交互。
为什么不使用单集群?所有的前端服务器共用一组memcache服务器
因为,前端服务器会和每个memcache服务器建立连接,当一个集群中的前端服务器或者是memcache服务器数量的增多,这个集群内的通信连接的数量是以N的平方量级去增长,当数量超越一定时,就需要花费大量的资源去维护这些连接。集群网络中的流量过大还会导致丢包率的提升,导致性能的下降。此外,尤为关键的就是想要在数据中心中构建出那种能够让大量的不同机器之间通信的高速网络是很难的。因此,需要避免集群规模过大,可以采取的措施就是将单集群拆分为多个小集群。
此外,多个小集群是复制的形式,相比原来的单集群,多个集群中的memcache可以存放多个热点数据,解决热点数据问题。
但是对于那些不是特别热门的数据来说,它们不会因为有多个集群可以存放多份拷贝在memcache中而得到性能的提升,那么在memcache中提供内存给这些数据就是一种浪费,况且服务器内存的价格十分昂贵,因此为了节约使用memcache服务器内存,facebook还会提供一个regional pool,前端服务器将会把那些冷门的数据发送到regional pool中的memcache中,仅将保存一份冷门数据在这儿,这儿的memcache是供所有前端服务器使用的。
创建新集群带来的临时性能问题
创建一个新的集群,这个新的集群中的memcache是空的,此时基本就等于没有缓存,那么将会有大量的请求直接转发给数据库服务器,这将给数据库服务器带来巨大的压力。
cold start
因此,在一个新的集群被加入后,将进入cold start状态,在这种情况下,如果缓存没有命中,前端将先向另个集群中的对应的memcache获取数据并缓存到本地的memcache中,只有当本地和旁边集群中的memcache中都没有命中时才会向数据库发起请求。在这段时间,其实相当于新开的集群中的前端服务器将借用旁边集群中的memcache,并缓冲热门数据到本地的memcache。当本地memcache中的缓存了足够的数据后即可关闭cold start模式。
Thundering Herd
惊群问题是look-aside cache衍生而出的一个问题。
当一个热门数据被更新后,memcache中的热点数据被删除后,后续大量的请求该热点数据的请求都将转发给数据库服务器,这会给服务器带来巨大的负担,并且服务器会将会重复相同的工作,因为这些请求都是查询同个key值的数据。
facebook解决这个问题的办法是lease机制。这个lease机制和GFS中的lease机制不同。当前端向memcache请求一个热点数据,但是memcache中的该数据查询不到,memcache便会给该前端一个lease token,并将该lease号放到表中记录。当其余的前端来请求这个热点数据时,memcache就会让这些前端服务器等待,因为memcache已经给了一个前端服务器lease token让他去数据库服务器查询并保存到memcache中。当授予lease token的前端服务器取得数据后,将会将这个取得的数据附带lease给memcache服务器,memcache通过这个lease来判断是否合法,合法即可保存。随后其余前端服务器即可正常请求获得数据,在整个过程中数据库服务器将只会收到一个读取请求。
当然,如果被授予lease的前端服务器可能会发生故障,那么lease机制还要超时机制,就会颁发给另一个前端服务器。
集群中memcache的可用性保障
如果一个集群中的一个memcache服务器发生了故障,那么大量的请求都将转发给数据库服务器。因此,Facebook还使用了一种自动替换故障机的机制。
首先我们想到的方法就是立马启动新的一台memcache来顶替,但是这个方法需要花费很多的时间,并且需要将那些请求都重新导向这个新的memcache服务器处。因此,memcache那边恢复之前,还是需要有一个临时的解决方案度过这段时间。
Facebook选择引入Gutter Server,这些服务器平时就是闲置状态,唯有memcache服务器发生了故障才会发挥作用,Gutter Server可以接手一个或者多个发生故障的memcache服务器的处理的请求。当前端发送请求给memcache发现无法通信,那么就会发送相同的请求给其中一个Gutter Server,随后这个Gutter Server的行为就和memcache一样了。同样,Gutter Server也会具备lease机制来解决惊群问题。
当发生故障的memcache被替换掉,或者是恢复了,Gutter Server的工作就结束了。
此外还有一点,gutter server可以接手多个已经挂掉的memcache,因此,它可以去缓存任何key对应的数据。因此,gutter server中可能存在任何key的数据副本,当前端发送delete操作给memcache时,按理来说也需要发送delete操作给gutter server,删除缓存中的数据。此外,数据库服务器也同理,mcsqueal发送delete给相关数据的memcache,也需要发送delete给每一个gutter server。这就会导致delete操作需要发送的数量大大增多。但是,Gutter Server其实大多数时候基本都是闲置状态,无需缓存任何数据,因此为了避免这些额外的delete操作,Facebook对这些Gutter Server做了一些处理,无需前端或者是数据库服务器去显式删除Gutter Server上过时的数据。
一致性方面
前端向数据库服务器请求写数据的时候,数据库服务器本身也会发送delete给持有该数据的memcache服务器,为何前端还要发送delete请求给memcache?以及delete操作能不能放在send操作更新数据给数据库之前进行?
因为前面说到脸书要求的一致性,必须确保人们能够第一时间看到自己刚刚更新的数据,如果前端在执行write操作不发送delete请求给memcache,那么memcache中的内容只能等数据库服务器发送delete给memcache来删除,有可能在这期间用户立马刷新获取数据,此时将会在数据库服务器向memcache发出delete命令前获取到memcache中的旧数据,这是不允许的。
如果delete操作放在send操作之前,那么以下这种情况就会发生,在delete操作和send操作之间,此时,memcache中的相关数据的缓存已经被删除,但是数据库中的数据还没更新,另一个前端恰巧请求了这个数据,发现memcache中没有这个数据的缓存,便向数据库直接请求读取数据并将这个旧数据重新写回了memcache,这个数据只能等待后续数据库服务器发送delete来重新删除,这就导致刚刚发送更新请求的用户,立马刷新获取数据后得到的还是旧数据。
写操作中为什么要采取失效策略,而非采取更新策略,前端立马通过set操作将更新后的数据保存到memcache中?
我们可以通过以下的例子来了解更新策略的问题。当有两个更新操作并发执行,数据库处x变量的更新顺序和memcache处x变量的更新顺序不一致,那就会导致memcache中的数据是过时的,并且数据库服务器不会发送delete操作给memcache,这将导致memcache中的数据一直是过时的,这是极其严重的事故!决不允许发生。
一致性的问题主要来自数据库数据存在多个副本,主数据中心中存在一份,副数据中心也存在一份,同时每个本地集群中的memcache服务器中也存有它的副本,同时Gutter Server中可能也会有它的副本。我们每次进行写操作时,需要把这个写操作的结果传达到这个数据的所有副本上。此外写操作还是由多个前端服务器产生,需要并发执行,要并发执行的写操作传达到众多副本上,这将会导致update race,这很可能也会导致旧的数据停留在系统中较长时间,就是上述的情况。那么采取失效策略是否能够解决update race的问题?显然是不行,可见以下的情况。
这种情况,c1将会把旧数据保存到memcache中,也会发生和上面一样的严重事故。因此,为了解决这种情况,还需利用了lease机制,当前端要执行更新memcache的操作前,memcache会给前端分配一个lease,memcache会记录这个lease和本地hash table中的指定数据的key的关系,唯有持有这个lease的前端才可去更新该key的内容。不过当该key的数据被删除后,对应的lease也会跟着被删除,随后持有被删除的lease前端将无法更新数据,memcache将会那些更新操作视为过时的,直接无视。
lease机制确保了memcache上的请求执行的有序性,避免了原来memcache中没有锁之类的同步机制导致update race。
总结
在相同的硬件条件下,使用部分硬件来充当缓存,系统能够获得更好的性能,系统能够承载更高的负载。但是缓存并不能降低处理延迟。同时,分片和复制都是提高性能的好办法,这两者之间各有优缺点,我们需要进行trade off,得到最具性价比的架构。此外,通过lease机制、失效策略也能够使得有大量副本和多源前端的情况下,实现了较好的一致性。