缓存编(转)

来源:互联网 发布:开淘宝店需要多少钱 编辑:程序博客网 时间:2024/06/05 09:16

大型web系统数据缓存设计

  1. 前言
    在高访问量的web系统中,缓存几乎是离不开的;但是一个适当、高效的缓存方案设计却并不容易;所以接下来将讨论一下应用系统缓存的设计方面应该注意哪些东西,包括缓存的选型、常见缓存系统的特点和数据指标、缓存对象结构设计和失效策略以及缓存对象的压缩等等,以期让有需求的同学尤其是初学者能够快速、系统的了解相关知识。

  2. 数据库的瓶颈
    2.1 数据量
    关系型数据库的数据量是比较小的,以我们常用的MySQL为例,单表数据条数一般应该控制在2000w以内,如果业务很复杂的话,可能还要低一些。即便是对于Oracle这些大型商业数据库来讲,其能存储的数据量也很难满足一个拥有几千万甚至数亿用户的大型互联网系统。

2.2 TPS
在实际开发中我们经常会发现,关系型数据库在TPS上的瓶颈往往会比其他瓶颈更容易暴露出来,尤其对于大型web系统,由于每天大量的并发访问,对数据库的读写性能要求非常高;而传统的关系型数据库的处理能力确实捉襟见肘;以我们常用的MySQL数据库为例,常规情况下的TPS大概只有1500左右(各种极端场景下另当别论);下图是MySQL官方所给出的一份测试数据:

而对于一个日均PV千万的大型网站来讲,每个PV所产生的数据库读写量可能要超出几倍,这种情况下,每天所有的数据读写请求量可能远超出关系型数据的处理能力,更别说在流量峰值的情况下了;所以我们必须要有高效的缓存手段来抵挡住大部分的数据请求!

2.3 响应时间
正常情况下,关系型数据的响应时间是相当不错的,一般在10ms以内甚至更短,尤其是在配置得当的情况下。但是就如前面所言,我们的需求是不一般的:当拥有几亿条数据,1wTPS的时候,响应时间也要在10ms以内,这几乎是任何一款关系型数据都无法做到的。
那么这个问题如何解决呢?最简单有效的办法当然是缓存!
3. 缓存系统选型
3.1 缓存的类型
3.1.1 本地缓存
本地缓存可能是大家用的最多的一种缓存方式了,不管是本地内存还是磁盘,其速度快,成本低,在有些场合非常有效;
但是对于web系统的集群负载均衡结构来说,本地缓存使用起来就比较受限制,因为当数据库数据发生变化时,你没有一个简单有效的方法去更新本地缓存;然而,你如果在不同的服务器之间去同步本地缓存信息,由于缓存的低时效性和高访问量的影响,其成本和性能恐怕都是难以接受的。
3.1.2 分布式缓存
前面提到过,本地缓存的使用很容易让你的应用服务器带上“状态”,这种情况下,数据同步的开销会比较大;尤其是在集群环境中更是如此!
分布式缓存这种东西存在的目的就是为了提供比RDB更高的TPS和扩展性,同时有帮你承担了数据同步的痛苦;优秀的分布式缓存系统有大家所熟知的Memcached、Redis(当然也许你把它看成是NoSQL,但是我个人更愿意把分布式缓存也看成是NoSQL),还有国内阿里自主开发的Tair等;
对比关系型数据库和缓存存储,其在读和写性能上的差距可谓天壤之别;memcached单节点已经可以做到15w以上的tps、Redis、google的levelDB也有不菲的性能,而实现大规模集群后,性能可能会更高!
所以,在技术和业务都可以接受的情况下,我们可以尽量把读写压力从数据库转移到缓存上,以保护看似强大,其实却很脆弱的关系型数据库。

3.1.3 客户端缓存
这块很容易被人忽略,客户端缓存主要是指基于客户端浏览器的缓存方式;由于浏览器本身的安全限制,web系统能在客户端所做的缓存方式非常有限,主要由以下几种:
a、 浏览器cookie;这是使用最多的在客户端保存数据的方法,大家也都比较熟悉;

b、 浏览器本地缓存;很多浏览器都提供了本地缓存的接口,但是由于各个浏览器的实现有差异,所以这种方式很少被使用;此类方案有chrome的Google Gear,IE的userData、火狐的sessionStorage和globalStorage等;

c、 flash本地存储;这个也是平时比较常用的缓存方式;相较于cookie,flash缓存基本没有数量和体积的限制,而且由于基于flash插件,所以也不存在兼容性问题;不过在没有安装flash插件的浏览器上则无法使用;

d、 html5的本地存储;鉴于html5越来越普及,再加上其本地存储功能比较强大,所以在将来的使用场景应该会越来越多。

由于大部分的web应用都会尽量做到无状态,以方便线性扩容,所以我们能使用的除了后端存储(DB、NoSQL、分布式文件系统、CDN等)外,就只剩前端的客户端缓存了。
对客户端存储的合理使用,原本每天几千万甚至上亿的接口调用,一下就可能降到了每天几百万甚至更少,而且即便是用户更换浏览器,或者缓存丢失需要重新访问服务器,由于随机性比较强,请求分散,给服务器的压力也很小!在此基础上,再加上合理的缓存过期时间,就可以在数据准确和性能上做一个很好的折衷。
3.1.4 数据库缓存
这里主要是指数据库的查询缓存,大部分数据库都是会提供,每种数据库的具体实现细节也会有所差异,不过基本的原理就是用查询语句的hash值做key,对结果集进行缓存;如果利用的好,可以很大的提高数据库的查询效率!数据库的其他一些缓存将在后边介绍。

3.2 选型指标
现在可供我们选择使用的(伪)分布式缓存系统不要太多,比如使用广泛的Memcached、最近炒得火热的Redis等;这里前面加个伪字,意思是想说,有些所谓的分布式缓存其实仍是以单机的思维去做的,不能算是真正的分布式缓存(你觉得只实现个主从复制能算分布式么?)。
既然有这么多的系统可用,那么我们在选择的时候,就要有一定的标准和方法。只有有了标准,才能衡量一个系统时好时坏,或者适不适合,选择就基本有了方向。
下边几点是我个人觉的应该考虑的几个点(其实大部分系统选型都是这么考虑的,并非只有缓存系统):

3.2.1 容量
废话,容量当然是越大越好了,这还用说么,有100G我干嘛还要用10G?其实这么说总要考虑一下成本啦,目前一台普通的PC Server内存128G已经算是大的了,再大的话不管是从硬件还是从软件方面,管理的成本都会增加。单机来讲,比如说主板的插槽数量,服务器散热、操作系统的内存分配、回收、碎片管理等等都会限制内存卡的容量;即便使用多机的话,大量内存的采购也是很费money的!
有诗云:山不在高,有仙则名;所以内存也不在多,够用就好!每个系统在初期规划的时候,都会大致计算一下所要消耗的缓存空间,这主要取决于你要缓存的对象数量和单个对象的大小。一般来说,你可以采用对象属性在内存中的存储长度简单加和的方法来计算单个对象的体积,再乘以缓存对象的数量和预期增长(当然,这里边有一个热点数据的问题,这里就不细讨论了),大概得出需要使用的缓存空间;之后就可以按照这个指标去申请缓存空间或搭建缓存系统了。

3.2.2 并发量
这里说并发量,其实还不如说是QPS更贴切一些,因为我们的缓存不是直接面向用户的,而只面向应用的,所以肯定不会有那个高的并发访问(当然,多个系统共用一套缓存那就另当别论了);所以我们关心的是一个缓存系统平均每秒能够承受多少的访问量。
我们之所以需要缓存系统,就是要它在关键时刻能抗住我们的数据访问量的;所以,缓存系统能够支撑的并发量是一个非常重要的指标,如果它的性能还不如关系型数据库,那我们就没有使用的必要了。
对于淘宝的系统来说,我们不妨按照下边的方案来估算并发量:
QPS = 日PV × 读写次数/PV ÷ (8 × 60 × 60)
这里我们是按照一天8个小时来计算的,这个值基于一个互联网站点的访问规律得出的,当然,如果你不同意这个值,可以自己定义。
在估算访问量的时候,我们不得不考虑一个峰值的问题,尤其是像淘宝、京东这样大型的电商网站,经常会因为一些大的促销活动而使PV、UV冲到平时的几倍甚至几十倍,这也正是缓存系统发挥作用的关键时刻;倍受瞩目的12306在站点优化过程中也大量的引入了缓存(内存文件系统)来提升性能。
在计算出平均值之后,再乘以一个峰值系数,基本就可以得出你的缓存系统需要承受的最高QPS,一般情况下,这个系数定在10以内是合理的。

3.2.3 响应时间
响应时间当然也是必要的,如果一个缓存系统慢的跟蜗牛一样,甚至直接就超时了,那和我们使用MySQL也没啥区别了。
一般来说,要求一个缓存系统在1ms或2ms之内返回数据是不过分的,当然前提是你的数据不会太大;如果想更快的话,那你就有点过分了,除非你是用的本地缓存;因为一般而言,在大型IDC内部,一个TCP回环(不携带业务数据)差不多就要消耗掉0.2ms至0.5ms。
大部分的缓存系统,由于是基于内存,所以响应时间都很短,但是问题一般会出现在数据量和QPS变大之后,由于内存管理策略、数据查找方式、I/O模型、业务场景等方面的差异,响应时间可能会差异很多,所以对于QPS和响应时间这两项指标,还要靠上线前充分的性能测试来进一步确认,不能只单纯的依赖官方的测试结果。

3.2.4 使用成本
一般分布式缓存系统会包括服务端和客户端两部分,所以其使用成本上也要分为两个部分来讲;
首先服务端,优秀的系统要是能够方便部署和方便运维的,不需要高端硬件、不需要复杂的环境配置、不能有过多的依赖条件,同时还要稳定、易维护;
而对于客户端的使用成本来说,更关系到程序员的开发效率和代码维护成本,基本有三点:单一的依赖、简洁的配置和人性化的API。
另外有一点要提的是,不管是服务端还是客户端,丰富的文档和技术支持也是必不可少的。

3.2.5 扩展性
缓存系统的扩展性是指在空间不足的性情况,能够通过增加机器等方式快速的在线扩容。这也是能够支撑业务系统快速发展的一个重要因素。
一般来讲,分布式缓存的负载均衡策略有两种,一种是在客户端来做,另外一种就是在服务端来做。

客户端负载均衡
在客户端来做负载均衡的,诸如前面我们提到的Memcached、Redis等,一般都是通过特定Hash算法将key对应的value映射到固定的缓存服务器上去,这样的做法最大的好处就是简单,不管是自己实现一个映射功能还是使用第三方的扩展,都很容易;但由此而来的一个问题是我们无法做到failover。比如说某一台Memcached服务器挂掉了,但是客户端还会傻不啦叽的继续请求该服务器,从而导致大量的线程超时;当然,因此而造成的数据丢失是另外一回事了。要想解决,简单的可能只改改改代码或者配置文件就ok了,但是像Java这种就蛋疼了,有可能还需要重启所有应用以便让变更能够生效。
如果线上缓存容量不够了,要增加一些服务器,也有同样的问题;而且由于hash算法的改变,还要迁移对应的数据到正确的服务器上去。

服务端负载均衡
如果在服务端来做负载均衡,那么我们前面提到的failover的问题就很好解决了;客户端能够访问的所有的缓存服务器的ip和端口都会事先从一个中心配置服务器上获取,同时客户端会和中心配置服务器保持一种有效的通信机制(长连接或者HeartBeat),能够使后端缓存服务器的ip和端口变更即时的通知到客户端,这样,一旦后端服务器发生故障时可以很快的通知到客户端改变hash策略,到新的服务器上去存取数据。
但这样做会带来另外一个问题,就是中心配置服务器会成为一个单点。解决办法就将中心配置服务器由一台变为多台,采用双机stand by方式或者zookeeper等方式,这样可用性也会大大提高。

3.2.6 容灾
我们使用缓存系统的初衷就是当数据请求量很大,数据库无法承受的情况,能够通过缓存来抵挡住大部分的请求流量,所以一旦缓存服务器发生故障,而缓存系统又没有一个很好的容灾措施的话,所有或部分的请求将会直接压倒数据库上,这可能会直接导致DB崩溃。
并不是所有的缓存系统都具有容灾特性的,所以我们在选择的时候,一定要根据自己的业务需求,对缓存数据的依赖程度来决定是否需要缓存系统的容灾特性。

3.3 常见分布式缓存系统比较
3.3.1 Memcached
Memcached严格的说还不能算是一个分布式缓存系统,个人更倾向于将其看成一个单机的缓存系统,所以从这方面讲其容量上是有限制的;但由于Memcached的开源,其访问协议也都是公开的,所以目前有很多第三方的客户端或扩展,在一定程度上对Memcached的集群扩展做了支持,但是大部分都只是做了一个简单Hash或者一致性Hash。
由于Memcached内部通过固定大小的chunk链的方式去管理内存数据,分配和回收效率很高,所以其读写性能也非常高;官方给出的数据,64KB对象的情况下,单机QPS可达到15w以上。
Memcached集群的不同机器之间是相互独立的,没有数据方面的通信,所以也不具备failover的能力,在发生数据倾斜的时候也无法自动调整。
Memcached的多语言支持非常好,目前可支持C/C++、Java、C#、PHP、Python、Perl、Ruby等常用语言,也有大量的文档和示例代码可供参考,而且其稳定性也经过了长期的检验,应该说比较适合于中小型系统和初学者使用的缓存系统。

3.3.2 Redis
Redis也是眼下比较流行的一个缓存系统,在国内外很多互联网公司都在使用(新浪微博就是个典型的例子),很多人把Redis看成是Memcached的替代品。
下面就简单介绍下Redis的一些特性;
Redis除了像Memcached那样支持普通的