1.6 服务器缓存

服务器缓存是整个缓存体系的重头戏。服务器缓存在网站架构演进中是系统性能的重中之重。

数据库是整个系统中的“慢性子”,有时候数据库调优能够以小博大,在不改变架构和代码逻辑的前提下,缓存参数的调整往往是条捷径。系统开发过程中,可以直接在平台侧使用缓存框架,当缓存框架不能满足系统对性能的要求时,就需要在应用层自主研发应用级缓存了,即使利用可参考的开源架构,应用级缓存的开发也是一件有挑战的事情。

1.6.1  数据库缓存

数据库库属于IO密集型应用,主要负责数据的管理及存储。数据库缓存是一类特殊的缓存,是数据库自身的缓存机制。大多数数据库不需要配置就可以快速运行,并没有因为特定的需求进行优化。在数据库调优的时候,缓存优化是一项很重要的工作。MYSQL为例,

1.MySQL的查询缓存

Query cache 作用于整个MySQL实例,主要用于缓存MySQL中的ResultSet,也就是一条语句的执行结果集,所以只是针对select语句。

打开Query Cache功能,MySQL在接收到一条select语句请求后,如果语句满足Query Cache的要求,MySQL会根据预先设定好的HASH算法将收到的select语句以字符串方式进行hash,后到Query Cache中直接查找是否已缓存。如果已缓存直接返回数据,省略后续步骤(节省了SQL语句解析、优化器优化,向存储引擎请求数据等),极大提升了性能。当然,对于变化频繁的数据使用Query Cache可能得不偿失

Query Cache的使用需要多个参数配合,关键参数:

      1. query_cache_size 设置ResultSet的内存大小
      2. query_cache_type 设置什么场景下使用缓存
        0(OFF)不使用Query Cache,1(ON)除显示要求不使用Query Cache之外的所有select都是用Query Cache,2(DEMAND)只有显示要求才使用Query Cache

2.检验Query Cache的合理性

可以通过以下命令观察:

        1. SHOW VARIABLES LIKE '%query_cache%';
        2. SHOW STATUS LIKE 'Qcache%';

调节一下参数可以知道query_cache_size 设置是否合理:

        1. Qcache inserts; 表示未命中然后插入缓存数;
        2. Qcache hits; 表示命中次数;值非常大,表示缓冲使用频繁;值很小,会影响效率,可考虑不用查询缓存;
        3. Qcache lowmen prunes; 表示多少条Query因为内存不足而被清除出Query Cache。
        4. Qcache free blocks; 表示缓存区中碎片多少;如果值较大应找机会整理;

Qcache_hits可以看到Query Cache的基本效果。通过Qcache_hits和 Qcache_inserts可以算出命中率

Query Cache命中率=Qcache_hits  /  (Qcache_hits + Qcache_inserts)

通过Qcache_lowmen_prunes 和 Qcache_free_memory互相结合,能更清楚的了解系统中Query Cache的内存大小是否真的足够,是否频繁出现因为内存不足而有Query被换出的情况。

3. InnoDB的缓存性能

innodb_bugger_pool_size,设置用于缓存InnoDB索引及数据块的内存区域大小(更像是Oracle数据库中的db_cache_size)

key_buffer_size对于MyISAM引擎一样,innodb_buffer_pool_size设置了InnoDB存储引擎需求最大的一块内存区域的大小,直接关系InnoDB存储引擎的性能,如果有足够的内存,尽量将该参数设置到足够大,尽可能多的InnoDB的索引及数据都放入到该缓存区域中,直至全部。

缓存命中率:(Innodb_buffer_pool_read_request  -  Innodb_buffer_pool_reads) /  Innodb_buffer_pool_read_requests * 100%,根据命中率调整innodb_buffer_pool_size的大小

另一个重要性能参数:table_cache

1.6.2  平台级缓存

适当使用平台及缓存,往往可以取得事半功倍的效果。

平台缓存这里指用来写带有缓存特性的应用框架,或者可用于缓存功能的专用库(如PHP中Samrty模板库)

Java语言中,缓存框架更多,如Ehcache,Cacheonix,Voldemort,JBoss Cache ,OSCache等

1.6.3  应用级缓存

平台级缓存不能满足系统性能需求的时候,就要考虑应用级缓存了。

应用级缓存,需要开发者通过代码来实现缓存机制。这里是NoSQL的胜场,不论是Redis还是MongoDB,以及Memcached都可以作为应用级缓存的作用技术。

一种典型的方式是每分钟或每一段时间后同一生成某类页面存储在缓存中,或在热数据变化时更新缓存。

1.面向Redis的缓存应用

Redis支持主从同步,主服务器可向任意数量的从服务器同步,从服务器可以是其他关联服务器的主服务器。这使得Redis可执行单层树状复制。因为完全实现了发布/订阅机制,使得从服务器在任何地方同步树的时候,可订阅一个频道并接收主服务器完整的消息发布记录。同步对读取操作的可扩展性和数据冗余很有帮助。

Redis3.0 加入cluster功能,解决了Redis单点无法横向扩展的问题。Redis集群采用无中心节点方式实现,无需proxy代理,客户端直接与Redis集群的每个节点连接,根据同样的哈希算法计算出key对应的slot,然后直接在slot对应的Redis上执行命令。

Redis认为响应时间是苛刻的条件,增加一层带来的开销是不能接受的。因此,Reids实现了客户端对接点的直接访问,为了去中心化,节点之间通过Gossip协议(???)交换状态,以及探测新加入的节点信息。Redis集群支持动态加入节点,动态迁移slot,以及自动故障转移。

集群架构图如下图:

所有Redis节点通过PING-PONG机制彼此互联,内部使用二进制协议优化传输速度和带宽

节点故障通过集群中半数的节点是小时生效。

客户端与redis节点直连,客户端不需要连接集群所有节点,集群中任一可用节点即可。

Redis CLuster 把所有物理节点映射到slot,cluster负责维护node、slot、value的映射关系。

节点故障时,选举过程是集群中所有master参与的,如果半数以上master节点与当前master节点间的通信超时,认为当前master节点挂掉。如果半数以上master节点挂掉,无论是否有slave集群,Redis整个集群处于不可用状态。集群不可用时,所有对集群的操作都不可用,都会受到错误信息【(error)CLUSTERDOWN the cluster is down】

 2.多级缓存实例

一个使用Redis集群和其他多种缓存技术的系统架构如图所示:

用户请求被负载均衡服务器分发到Nginx上,此处常用负载均衡算法是轮训或一致性哈希,轮训使请求均衡,一致性哈希提升Nginx应用的缓存命中率。

接着,Nginx应用服务器读取本地缓存,实现本地缓存的方式1.Lua Shared Dict或者2.面向磁盘或内存的Nginx Proxy Cache,以及3.本地的Reids实现等,缓存命中直接返回。Nginx应用服务器使用本地缓存可以提升整体的吞吐量,降低后端的压力,尤其对应热点数据的反复读取问题非常有效。

Nginx没有命中缓存,进一步读取分布式缓存----Reids分布式缓存的集群,使用主从架构来提升性能和吞吐量,如果分布式缓存命中则直接返回数据,并回到Nginx应用服务器的本地缓存中。

如果Reids分布式缓存没有命中,则会回源到Tomcat集群,在回源到Tomcat集群时也可以使用轮训和一致性哈希作为负载均衡算法。当然,Nginx应用服务器可以在尝试一次读取主Redis集群操作,目的是防止当前Redis集群有问题时可能发生的流量冲击。

在Tomcat集群应用中,首先读取本地平台级缓存,如果命中则直接返回数据,并同步写到主Redis及集群,然后再同步到从Redis集群。此处可能存在多个Tomcat实例同时写主Redis集群的情况,可能造成数据错乱,注意缓存更新机制和原子化操作

如果所有缓存没命中,系统就只能查询数据库或其他相关服务获取相关数据并返回,当然,我们知道数据库也是有缓存的。

整体来说这时一个使用多级缓存的系统。

Nginx应用服务器的本地缓存解决了热点数据的缓存entity

Redis分布式缓存集群减少了访问回源率

Tomcat应用集群使用的平台级缓存防止相关缓存失效/崩溃后的冲击

数据库缓存提升数据库查询时的效率

正是多级缓存的使用,才能保障系统具备优良的性能。

3.缓存算法(替代策略)

    1. Least-Recently-Used(LRU)
      替换掉最近被请求最少的对象,这种传统的策略在实际应用中最广。
      在CPU缓存淘汰和虚拟内存系统中效果很好。然而在直接应用与代理缓存中效果欠佳,因为Web访问的时间局部性变化很大
      浏览器一般使用LRU作缓存算法。

      LRU步骤
      a)新数据直接插入到列表头部
      b)缓存数据被命中,将数据移动到列表头部
      c)缓存已满的时候,移除列表尾部数据。

    2. Least-Frequently-Used(LFU)
      替换访问次数最少的缓存。
      然而,有的文档可能有很高的使用率,但之后就在也用不到了。传统的LFU策略没有提供任何移除这类文件的机制,因此会导致“缓存污染“,即一个先前流行的缓存对象在缓存中驻留很长时间,阻碍新进来可能会流行的对象对它的替代。

      LRU对于循环出现的数据,缓存命中不高
      LFU对于交替出现的数据,缓存命中不高
      View Code
    3. LRU-K
      LRU-K中的K代表最近使用的次数,因此LRU可以认为是LRU-1。LRU-K的主要目的是为了解决LRU算法“缓存污染”的问题(偶发性的、周期性的批量操作会使临时数据涌入缓存,挤出热点数据,导致LRU热点命中率急剧下降,缓存污染情况比较严重),其核心思想是将“最近使用过1次”的判断标准扩展为“最近使用过K次”。
      相比LRU,LRU-K需要多维护一个队列(按照一定的规则FIFO或LRU淘汰数据),用于记录所有缓存数据被访问的历史。只有当数据的访问次数达到K次的时候,将数据索引从历史队列删除,将数据放入缓存。当需要淘汰数据时,LRU-K会淘汰第K次访问时间距当前时间最大的数据

      LRU-K降低了“缓存污染”带来的问题,命中率比LRU要高。但是比LRU算法复杂,内存消耗多。

    4. 实际应用中LRU-2是综合各种因素的最优选择(???),LRU-3或者更大K值命中率会高,但适应性差,需要大量的数据访问才能将历史访问记录清除掉。

      因为要跟踪对象K次,访问负载会随着缓存池的增加而增加。

    5. Two Queues(2Q)
      该算法类似于LRU-2,不同点在于2Q将LRU-2算法中的访问历史队列(注意这不是缓存数据的)改为一个FIFO缓存队列,即:2Q算法有两个缓存队列,一个是FIFO队列,一个是LRU队列。
      当数据第一次访问时,2Q算法将数据缓存在FIFO队列里面,当数据第二次被访问时,则将数据从FIFO队列移到LRU队列里面,两个队列各自按照自己的方法淘汰数据。
      去除对象是为了保持第一个缓存池是第二个缓存池的1/3(为什么?)。当缓存的访问负载是固定时,把LRU换成LRU-2,就比增加缓存容量更好

    6. LRU-Threshold
    7. Log(Size)+ LRU
    8. Hyper-G
    9. Pitkow/Recker
    10. Lowest-Letency-First
    11. Hybrid-Hybrid
    12. Lowest Relative Value(LRV)
    13. Adaptive Replacement Cache(ARC)
    14. Most Recently Used(MRU)
    15. First in First out
    16. Random Cache

还有很多缓存算法,例如Second Chance、Clock、Simple time-based、Extended time-based expiration、Sliding time-based expiration等,各种缓存算法没有优劣之分,不同的实际应用场景,会用到不同的缓存算法。实际应考虑使用频率,获取成本,缓存容量和时间等因素。

4.使用公有云的缓存服务

国内的公有云服务提供商如阿里云、青云、百度云等都推出了基于Redis的云存储服务,有如下特点:

    1. 动态扩容
    2. 数据多备
    3. 自动容灾
    4. 成本较低

总而言之,现代软件系统中,缓存无处不在,是一种空间换时间的艺术

posted @ 2020-05-06 16:13  vvf  阅读(150)  评论(0编辑  收藏  举报