首页 > 代码库 > 构建高可用web站点学习(二)

构建高可用web站点学习(二)

web站点的缓存学习

缓存在web应用里面十分常见,也有各种各样的缓存,从请求开始一直到代码处理的阶段都可以采取缓存。下面就逐一介绍:

一、客户端缓存(浏览器和http方面)

   前端页面缓存主要遵循http协议和客户端

      如果响应头信息告诉缓存器不要保留缓存;请求信息需要认证或者安全加密;一个缓存的副本如果含有以下信息,内容将会被认为足够新:含有完整的过期时间和寿命控制头信息,并且内容仍在保鲜期内。浏览器已经使用过缓存副本,并且在一个会话中已经检查过内容的新鲜度。

Expires(过期时间)属性是http控制缓存的基本手段。静态图片

Cache-Control  : Last-Modified:当客户端第二次请求此url时,浏览器会向服务器传送If-Modified-Since报头,如果服务器端的资源没有变化,则自动返回304,内容为空。 EtagLast-Modified都起到文档唯一性标识的作用。

打开新窗口,如果指定cache-control的值为privateno-cachemust-revalidate,那么打开新窗口访问时都会重新访问服务器。而如果指定了max-age,那么在此值内的时间里就不会重新访问服务器,例如:Cache-control: max-age=5 表示当访问此网页后的5秒内再次访问不会去服务器。在地址栏回车 如果值为privatemust-revalidate,则只有第一次访问时会访问服务器,以后就不再访问。如果值为no-cache,那么每次都会访问。如果值为max-age,则在过期之前不会重复访问。按后退按扭 如果值为privatemust-revalidatemax-age,则不会重访问,而如果为no-cache,则每次都重复访问。按刷新按扭 无论为何值,都会重复访问。

二、Web服务器缓存

  在ApacheNginx中开启缓存模块,服务器缓存的原理就是一个URL在一段较长的时间内对应一个唯一的内容,比如静态内容或者更新不太频繁的动态内容,一旦将内容缓存后,下次web服务器便可以在收到请求后立即拿出事先缓存好的响应内容并返回给浏览器

还有像Apache可以使用APC opcode cache缓存PHP的动态内容的方案。这里就不细说了。

三、反向代理缓存

  首先说明何为反向代理(Reverse Proxy):反向代理:让Internet上的其他用户可以访问局域网内的资源,使用的代理服务器即为反向代理器

 

  反向代理的作用除了缓存,还有就是负载均衡。负载均衡在后面还会说到。这里主要讲的是基于反向代理的缓存。在这方面VarnishNginx都做得很不错。(Varnish是基于内存进行缓存,利用二叉堆管理缓存文件,可达到积极删除目的,Nginx是基于硬盘cache)一般用法是将Nginx运行在Apache服务器之前。监听所有请求,按需将请求转发给Apache

  反向代理服务器正是通过修改流经它的数据的HTTP头信息来达到缓存的目的的,如一些静态页面等。而Nginx一般做Cache有下面几种方法:

  1:这个办法是把nginx404错误定向到后端,然后用proxy_store把后端返回的页面保存。

  2:也是利用proxy_store,这里利用if判断cache目录是否有文件,没有的话从后端取,取回来后发送给用户并且自身用proxy_store保存起来                   

  #2种办法其实原理都差不多,只是方法不同,都是使用proxy_sore把数据保存起来,这种办法在准确定义上不能算是cache,只能算是镜像-mirror功能,因为其没有内建机制定时删除cache,等于是永远的静态保存,所以这方案适合基本不会变化的数据,更详细的说明请看下面详细介绍这2cache的做法和介绍

  3:基于memcached的缓存(第四点就会提到)

  4:新版本nginx支持真正的proxy_cache模块。                                      

    #proxy_cachenginx-0.7.44版开始,nginx支持了类似squid较为正规的cache功能,这个缓存是把链接用md5编码hash后保存。

  5:第三方插件。                                                                                                                          

四、第三方缓存

  当你学习Web的时候,经常会说Web的性能瓶颈出现在数据库上(这句话也不一定是正确的),解决的办法比较好的一个就是搭建一个数据库前端的cache来减少数据库的访问次数。

  Memcached篇:Memcached是一个免费开源的、高性能的、具有分布式内存对象的缓存系统。准备以一下几个方面来说一下它是如何缓存的,也是对一些操作系统知识的巩固:

  (1)基于libevent的事件处理

      libevent 是一个事件触发的网络库,适用于windowslinuxbsd等多种平台,内部使用selectepollkqueue等系统调用管理事件机 制。著名的用于apachephp缓存库memcached据说也是libevent based,而且libevent在使用上可以做到跨平台,如果你将要开发的应用程序需要支持以上所列出的平台中的两个以上,那么强烈建议你采用这个库,即使你的应用程序只需要支持一个平台,选择libevent也是有好处的,因为它可以根据编译/运行环境切换底层的事件驱动机制,这既能充分发挥系统的性能,又增加了软件的可移植性。它封装并且隔离了事件驱动的底层机制,除了一般的文件描述符读写操作外,它还提供有读写超时、定时器和信号回调,另外,它还允许为事件设定不同的优先级,当前版本的libevent还提供dnshttp协议的异步封装,这一切都让这个库尤其适合于事件驱动应用程序的开发。(摘抄于libevent介绍)有兴趣的可以去看看具体源码实现。

  (2)内置的内存管理方式

    Memcached利用Slab Allocation的工作机制: slabSlab Allocation)的设计理念是基于对象缓冲的,基本想法是避免重复大量的初始化和清理操作。slab主要可以用于频繁非配释放的内存对象。替代malloc/free

Memcached中内存分配机制主要理念

    1.先为分配响应的大块内存,再在上面进行无缝小对象填充

    2.懒惰检测机制,Memcached不花过多的时间在检测各个item对象是否超市,当get获取数据时,才检查item对象是否应该删除,

    3.懒惰删除机制,在memcached中删除一个item对象的时候,并不是从内存中释放,而是单单的进行标记处理,再将其指针放入slot回放插槽,下次分配的时候直接使用。

    内存不足时,slab采用LRU算法淘汰不常用item

    在 memcached 运行过程中,要把一个 item 调入内存,但内存已无空闲空间时,为了保证程序能正常运行,系统必须从内存中调出一部分数据,送磁盘的对换区中。但应将哪些数据调出,须根据一定的算法来确定。通常,把选择换出数据(页面)的算法称为页面置换算法(Page Replacement Algorithms)。 Memcached 采用最近最久未使用(LRU)置换算法,是根据数据(页面)调入内存后的使用情况进行决策的。由于无法预测各页面将来的使用情况,只能利用“最近的过去”作为“最近的将来”的近似,因此,LRU置换算法是选择最近最久未使用的页面予以淘汰。当内存不足时,memcached 会从 slab各个 class 中的双向链表的尾部开始检测,即最近最久未使用的页面,往前一直寻找合适的 item 予以淘汰。所以该 LRU 算法为 slab 局部 class 淘汰的机制。但是在一些特定情形也会可能引起一些不必要的麻烦,可以在运行时加入”-M”参数禁止该算法。

  (3)分布式

    增加多台Memcached服务器,可以把访问的压力分配到各个不同的服务器上,由于各个Memcached服务器之间互相不通信,都是独立的存取数据,不共享任何信息。所以通过对客户端的设计,让Memcached具有分布式,支持    海量缓存和大规模应用。。memcached分布式算法由客户端API程序决定和实现的,(api根据存储用的key以及已知的服务器列表,根据keyhash计算将指定的key存储到对应的服务器列表上),这种分布式是memcached的最    大特点。

    起初算法:根据余数计算分散。

    机制:键值HashCode / 服务器节点数目 =(求余服务器位置。【哈希原理: 两个集合间的映射关系函数,在我们通常的应用中基本上可以理解为 在集合A(任意字母数字等组合,此处为存储用的key)里的一条记录去查找集合B(如0-2^32)中的对应记录。(题外话:md5的碰撞或者说冲突其实 就是发生在这里,也就是说多个A的记录映射到了同一个B的记录)】当选择的服务器无法连接时,则将连接次数添加到键值后,再计算哈希值并再次尝试 (Rehash)连接选择的服务器。

    这种方法很好,但是当增删服务器时,缓存的命中率的确很失败,大概降低到23%web应用中,增删缓存服务器瞬间,缓存效率会急剧下降,负载会突然集中到数据库服务器。于是,新的事物诞生了。

    优化后的算法:一致性哈希(Consistent Hashing

    首先求出每个服务节点的hash,并将其配置到一个0~2^32的圆环(continuum)区间上。其次使用同样的方法求出你所需要存储的keyhash,也将其配置到这个圆环(continuum)上。

    然后从数据映射到的位置开始顺时针查找,将数据保存到找到的第一个服务节点上。如果超过2^32仍然找不到服务节点,就会保存到第一个memcached服务节点上。

    首先求出memcached服务器节点的哈希值,并将其配置到0232次方的环上,然后用同样的方法求出数据键值的哈希值并映射到环上,然后从数据被映射到环的位置开始顺时针查找服务器位置,将数据保存在第一个找到的服务器上。如果没有找到则保持在第一个服务器上。

    

  Redis篇:

    Redis不仅仅可以作为缓存中间件,还有其他很多的用途,当然,它的许多特性也特别吸引我,Redis 是完全开源免费的,遵守BSD协议,先进的key - value持久化产品。它通常被称为数据结构服务器,因为值(value)可以是 字符串(String), 哈希(Map), 列表(list), 集合(sets) 和 有序集合(sorted sets)等类型。主要和Memcached不一样的地方就是提供更多的数据类型和当作为分布式的时候,Redis服务器之间可以相互通信。这里对于Redis的数据类型不作详细描述,毕竟关于Redis的资料也很多。我比较感兴趣的Redis是作为nosql服务器的一个优点:内存数据的持久化

     内存快照是默认的持久化方式:将内存中的数据以快照方式写入二进制文件中,默认文件名为dump.rdb、客户端用save或者bgsave命令告诉Redis做一次内存快照。Redis由单线程处理所有请求,执行save命令可能阻塞其他客户端请求,从而导致不能快速响应请求

    下面介绍详细的快照保存过程

      1.redis调用fork,现在有了子进程和父进程。

      2. 父进程继续处理client请求,子进程负责将内存内容写入到临时文件。由于os的写时复制机制(copy on write)父子进程会共享相同的物理页面,当父进程处理写请求时os会为父进程要修改的页面创建副本,而不是写共享的页面。所以子进程的地址空间内的数 据是fork时刻整个数据库的一个快照。

      3.当子进程将快照写入临时文件完毕后,用临时文件替换原来的快照文件,然后子进程退出。

client 也可以使用save或者bgsave命令通知redis做一次快照持久化。save操作是在主线程中保存快照的,由于redis是用一个主线程来处理所有 client的请求,这种方式会阻塞所有client请求。所以不推荐使用。另一点需要注意的是,每次快照持久化都是将内存数据完整写入到磁盘一次,并不 是增量的只同步脏数据。如果数据量大的话,而且写操作比较多,必然会引起大量的磁盘io操作,可能会严重影响性能。

另外由于快照方式是在一定间隔时间做一次的,所以如果redis意外down掉的话,就会丢失最后一次快照后的所有修改。如果应用要求不能丢失任何修改的话,可以采用aof持久化方式。

      下面介绍Append-only file

      aof 比快照方式有更好的持久化性,是由于在使用aof持久化方式时,redis会将每一个收到的写命令都通过write函数追加到文件中(默认是 appendonly.aof)。当redis重启时会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。当然由于os会在内核中缓存 write做的修改,所以可能不是立即写到磁盘上。这样aof方式的持久化也还是有可能会丢失部分修改。不过我们可以通过配置文件告诉redis我们想要 通过fsync函数强制os写入到磁盘的时机。有三种方式如下(默认是:每秒fsync一次)

      appendonly yes              //启用aof持久化方式

      # appendfsync always      //每次收到写命令就立即强制写入磁盘,最慢的,但是保证完全的持久化,不推荐使用

      appendfsync everysec     //每秒钟强制写入磁盘一次,在性能和持久化方面做了很好的折中,推荐

      # appendfsync no    //完全依赖os,性能最好,持久化没保证

      aof 的方式也同时带来了另一个问题。持久化文件会变的越来越大。例如我们调用incr test命令100次,文件中必须保存全部的100条命令,其实有99条都是多余的。因为要恢复数据库的状态其实文件中保存一条set test 100就够了。为了压缩aof的持久化文件。redis提供了bgrewriteaof命令。收到此命令redis将使用与快照类似的方式将内存中的数据 以命令的方式保存到临时文件中,最后替换原来的文件。具体过程如下

      1. redis调用fork ,现在有父子两个进程

      2. 子进程根据内存中的数据库快照,往临时文件中写入重建数据库状态的命令

      3.父进程继续处理client请求,除了把写命令写入到原来的aof文件中。同时把收到的写命令缓存起来。这样就能保证如果子进程重写失败的话并不会出问题。

      4.当子进程把快照内容写入已命令方式写到临时文件中后,子进程发信号通知父进程。然后父进程把缓存的写命令也写入到临时文件。

      5.现在父进程可以使用临时文件替换老的aof文件,并重命名,后面收到的写命令也开始往新的aof文件中追加。

      需要注意到是重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似。

      fromhttp://www.cnblogs.com/xhan/archive/2011/02/04/1949151.html

  MongoDB篇:

    稍后总结。 

五、数据库(Mysql)缓存

  数据库像Mysql有很多性能优化的方案,这里主要研究一下数据库的缓存,比如说查询缓存:对于相同类型的SQL就可以跳过SQL解析和执行计划生成阶段。MySQL在某些场景下也可以实现,但是“MySQL还有另一种不同的缓存类型:缓存完整的SELECT 查询结果。几个问题:

  1.MySQL如何判断缓存命中

     缓存存放在一个引用表中,通过一个哈希值引用。这个哈希值包括了如下因素,即查询本身、当前要查询的数据库、客户端协议的版本等。如果查询中包含任何用户自定义函数、存储函数、用户变量、临时表、mysql库中的系统表,或者任何包含列级别权限的表,都不会被缓存。

     有些问题:打开查询缓存对读和写操作都会带来额外的消耗:读查询开始之前必须先检查是否命中缓存,写缓存。对查询缓存操作是一个加锁排他操作,如果查询缓存使用了很大量的查询结果,那么缓存失效操作时整个系统都可能会僵死一会儿。

   2.查询缓存如何使用内存

      除了查询结果之外,需要缓存的还有很多别的维护相关的数据。这和文件系统有些类似:需要一些内存专门用来确定哪些内存目前是可用的、哪些已经用掉、哪些用来存储数据表和查询结果之前的映射、哪些用来存储查询字符串和查询结果。当有查询结果需要缓存的时候,MySQL先从大的空间块中申请一个数据块用于存储结果。这个数据块需要大于参数query_cache_min_res_unit的配置。因为需要先锁住空间块,然后找到合适大小数据块,相对来说,分配内存块是一个非常慢的操作。当需要缓存一个查询结果的时候,它先选择一个尽可能小的内存快。MySQL自己管理一大块内存。不依赖操作系统的内存管理.

   3.什么情况下查询缓存能发挥作用

      跟服务器压力模型有关:一个判断缓存的直接数据是命中率:要么增加Qcache_hits的值,要么增加Com_select的值。

   4.通用查询缓存优化

      (1)用多个小表代替一个大表对查询缓存有好处。更合适的粒度。

      (2)批量写入时只需要做一次缓存失效

      (3)控制缓存空间的大小

      (4)SQL_CACHE控制某个语句是否缓存

 

  小结:除了上面说的缓存方法,其实还有很多缓存手段,比如说模版缓存,文件缓存等等,还有像java语言中的对象缓存,Session缓存等等都是比较期待的能提升访问速度的手段。有人说那全部用上不就好了吗?其实什么东西都是有利弊的,要根据实际情况来判断是否采取哪种手段来进行缓存。上面还有很多不足之处,我也会继续学习和完善的。

构建高可用web站点学习(二)