<返回更多

推荐一款nginx+redis+ehcache高并发与高可用缓存架构

2019-08-19    
加入收藏

概述

对于高并发架构,毫无疑问缓存是最重要的一环,对于大量的高并发,可以采用三层缓存架构来实现,Nginx+redis+ehcache,下面对这每个环节做一下介绍。


nginx

对于中间件nginx常用来做流量的分发,同时nginx本身也有自己的缓存(容量有限),我们可以用来缓存热点数据,让用户的请求直接走缓存并返回,减少流向服务器的流量

1、模板引擎

通常我们可以配合使用freemaker/velocity等模板引擎来抗住大量的请求

  1. 小型系统可能直接在服务器端渲染出所有的页面并放入缓存,之后的相同页面请求就可以直接返回,不用去查询数据源或者做数据逻辑处理
  2. 对于页面非常之多的系统,当模板有改变,上述方法就需要重新渲染所有的页面模板,毫无疑问是不可取的。因此配合nginx+lua(OpenResty),将模板单独保存在nginx缓存中,同时对于用来渲染的数据也存在nginx缓存中,但是需要设置一个缓存过期的时间,以尽可能保证模板的实时性

2、双层nginx来提升缓存命中率

对于部署多个nginx而言,如果不加入一些数据的路由策略,那么可能导致每个nginx的缓存命中率很低。因此可以部署双层nginx

  1. 分发层nginx负责流量分发的逻辑和策略,根据自己定义的一些规则,比如根据productId进行hash,然后对后端nginx数量取模将某一个商品的访问请求固定路由到一个nginx后端服务器上去
  2. 后端nginx用来缓存一些热点数据到自己的缓存区

3、推荐架构

nginx作为最前端的web cache系统,通常的架构如下

推荐一款nginx+redis+ehcache高并发与高可用缓存架构

 

这个结构的优点:

  1. 可以使用nginx前端进行诸多复杂的配置,这些配置从前在squid是没法做或者做起来比较麻烦的,比如针对目录的防盗链。
  2. nginx前端可以直接转发部分不需要缓存的请求。
  3. 因为nginx效率高于squid,所以某些情况下可以利用nginx的缓存来减轻squid压力。
  4. 可以实现url hash等分配策略
  5. 可以在最前端开启gzip压缩,这样后面的squid缓存的纯粹是无压缩文档,可以避免很多无谓的穿透。
  6. 因为nginx稳定性比较高,所以lvs不需要经常调整,通过nginx调整就可以。
  7. squid的文件打开数按默认的1024就绰绰有余,不过处理的请求可一个都不会少。
  8. 可以启用nginx的日志功能取代squid,这样做实时点击量统计时可以精确定位到url,不必要再用低效率的grep来过滤。
  9. 因为nginx的负载能力高于squid,所以在用lvs分流时可以不必分得特别均衡,出现单点故障的几率比较低。

nginx和squid配合搭建的web服务器前端系统架构:

推荐一款nginx+redis+ehcache高并发与高可用缓存架构

 

前端的lvs和squid,按照安装方法,把epoll打开,配置文件照搬,基本上问题不多。

这个架构和App_squid架构的区别,也是关键点就是:加入了一级中层代理,中层代理的好处实在太多了:

  1. gzip压缩:压缩可以通过nginx做,这样,后台应用服务器不管是Apache、resin、lighttpd甚至iis或其他古怪服务器,都不用考虑压缩的功能问题。
  2. 负载均衡和故障屏蔽:nginx可以作为负载均衡代理使用,并有故障屏蔽功能,这样,根据目录甚至一个正则表达式来制定负载均衡策略变成了小case。
  3. 方便的运维管理,在各种情况下可以灵活制订方案。
  4. 权限清晰:这台机器就是不写程序的维护人员负责,程序员一般不需要管理这台机器,这样假如出现故障,很容易能找到正确的人。对于应用服务器和数据库服务器,最好是从维护人员的视线中消失,我的目标是,这些服务只要能跑得起来就可以了,其它的事情全部可以在外部处理掉。

redis(看架构也可以考虑memcache)

用户的请求,在nginx没有缓存相应的数据,那么会进入到redis缓存中,redis可以做到全量数据的缓存,通过水平扩展能够提升并发、高可用的能力

1、持久化机制:将redis内存中的数据持久化到磁盘中,然后可以定期将磁盘文件上传至S3(AWS)或者ODPS(阿里云)等一些云存储服务上去。

1)RDB

对redis中的数据执行周期性的持久化,每一刻持久化的都是全量数据的一个快照。对redis性能影响较小,基于RDB能够快速异常恢复

2)AOF

以append-only的模式写入一个日志文件中,在redis重启的时候可以通过回放AOF日志中的写入指令来重新构建整个数据集。(实际上每次写的日志数据会先到linux os cache,然后redis每隔一秒调用操作系统fsync将os cache中的数据写入磁盘)。对redis有一定的性能影响,能够尽量保证数据的完整性。redis通过rewrite机制来保障AOF文件不会太庞大,基于当前内存数据并可以做适当的指令重构。

如果同时使用RDB和AOF两种持久化机制,那么在redis重启的时候,会使用AOF来重新构建数据,因为AOF中的数据更加完整,建议将两种持久化机制都开启,用AO F来保证数据不丢失,作为数据恢复的第一选择;用RDB来作不同程度的冷备,在AOF文件都丢失或损坏不可用的时候来快速进行数据的恢复。

2、redis集群

1)replication

推荐一款nginx+redis+ehcache高并发与高可用缓存架构

 

一主多从架构,主节点负责写,并且将数据同步到其他salve节点(异步执行),从节点负责读,主要就是用来做读写分离的横向扩容架构。这种架构的master节点数据一定要做持久化,否则,当master宕机重启之后内存数据清空,那么就会将空数据复制到slave,导致所有数据消失

2)sentinal哨兵

推荐一款nginx+redis+ehcache高并发与高可用缓存架构

 

哨兵是redis集群架构中很重要的一个组件,负责监控redis master和slave进程是否正常工作,当某个redis实例故障时,能够发送消息报警通知给管理员,当master node宕机能够自动转移到slave node上,如果故障转移发生来,会通知client客户端新的master地址。sentinal至少需要3个实例来保证自己的健壮性,并且能够更好地进行quorum投票以达到majority来执行故障转移。

前两种架构方式最大的特点是,每个节点的数据是相同的,无法存取海量的数据。因此哨兵集群的方式使用与数据量不大的情况

3)redis cluster

推荐一款nginx+redis+ehcache高并发与高可用缓存架构

 

redis cluster支撑多master node,每个master node可以挂载多个slave node,如果mastre挂掉会自动将对应的某个slave切换成master。需要注意的是redis cluster架构下slave节点主要是用来做高可用、故障主备切换的,如果一定需要slave能够提供读的能力,修改配置也可以实现(同时也需要修改jedis源码来支持该情况下的读写分离操作)。

redis cluster架构下,master就是可以任意扩展的,直接横向扩展master即可提高读写吞吐量。slave节点能够自动迁移(让master节点尽量平均拥有slave节点),对整个架构过载冗余的slave就可以保障系统更高的可用性。


ehcache

推荐一款nginx+redis+ehcache高并发与高可用缓存架构

 

Tomcat jvm堆内存缓存,主要是抗redis出现大规模灾难。如果redis出现了大规模的宕机,导致nginx大量流量直接涌入数据生产服务,那么最后的tomcat堆内存缓存也可以处理部分请求,避免所有请求都直接流向DB。

具有以下特性:

1、快速轻量

2、伸缩性

3、灵活性

4、标准支持

5、可扩展性

6、应用持久化

在VM重启后,持久化到磁盘的存储可以复原数据。

Ehcache是第一个引入缓存数据持久化存储的开源Java缓存框架。缓存的数据可以在机器重启后从磁盘上重新获得。

根据需要将缓存刷到磁盘。将缓存条目刷到磁盘的操作可以通过cache.flush()方法来执行,这大大方便了Ehcache的使用。

7、分布式缓存

从Ehcache 1.2开始,支持高性能的分布式缓存,兼具灵活性和扩展性。

分布式缓存的选项包括:

8、Java EE和应用缓存

为普通缓存场景和模式提供高质量的实现。

阻塞缓存:它的机制避免了复制进程并发操作的问题。

SelfPopulatingCache在缓存一些开销昂贵操作时显得特别有用,它是一种针对读优化的缓存。它不需要调用者知道缓存元素怎样被返回,也支持在不阻塞读的情况下刷新缓存条目。

CachingFilter:一个抽象、可扩展的cache filter。

SimplePageCachingFilter:用于缓存基于request URI和Query String的页面。它可以根据HTTP request header的值来选择采用或者不采用gzip压缩方式将页面发到浏览器端。你可以用它来缓存整个Servlet页面,无论你采用的是JSP、velocity,或者其他的页面渲染技术。

SimplePageFragmentCachingFilter:缓存页面片段,基于request URI和Query String。在JSP中使用jsp:include标签包含。

已经使用Orion和Tomcat测试过,兼容Servlet 2.3、Servlet 2.4规范。

Cacheable命令:这是一种老的命令行模式,支持异步行为、容错。

兼容Hibernate,兼容google App Engine。

基于JTA的事务支持,支持事务资源管理,二阶段提交和回滚,以及本地事务。


经典的缓存+数据库读写的模式

1、读的时候,先读缓存,缓存没有的话,那么就读数据库,然后取出数据后放入缓存,同时返回响应

2、更新的时候,先删除缓存,然后再更新数据库

之所以更新的时候只是删除缓存,因为对于一些复杂有逻辑的缓存数据,每次数据变更都更新一次缓存会造成额外的负担,只是删除缓存,让该数据下一次被使用的时候再去执行读的操作来重新缓存,这里采用的是懒加载的策略。

举个例子,一个缓存涉及的表的字段,在1分钟内就修改了20次,或者是100次,那么缓存跟新20次,100次;但是这个缓存在1分钟内就被读取了1次,因此每次更新缓存就会有大量的冷数据,对于缓存符合28黄金法则,20%的数据,占用了80%的访问量


数据库和redis缓存双写不一致的问题

1、最初级的缓存不一致问题以及解决方案

问题:如果先修改数据库再删除缓存,那么当缓存删除失败来,那么会导致数据库中是最新数据,缓存中依旧是旧数据,造成数据不一致。

解决方案:可以先删除缓存,再修改数据库,如果删除缓存成功但是数据库修改失败,那么数据库中是旧数据,缓存是空不会出现不一致

2、比较复杂的数据不一致问题分析

问题:对于数据发生来变更,先删除缓存,然后去修改数据库,此时数据库中的数据还没有修改成功,并发的读请求到来去读缓存发现是空,进而去数据库查询到此时的旧数据放到缓存中,然后之前对数据库数据的修改成功来,就会造成数据不一致

解决方案:将数据库与缓存更新与读取操作进行异步串行化。当更新数据的时候,根据数据的唯一标识,将更新数据操作路由到一个jvm内部的队列中,一个队列对应一个工作线程,线程串行拿到队列中的操作一条一条地执行。当执行队列中的更新数据操作,删除缓存,然后去更新数据库,此时还没有完成更新的时候过来一个读请求,读到了空的缓存那么可以先将缓存更新的请求发送至路由之后的队列中,此时会在队列积压,然后同步等待缓存更新完成,一个队列中多个相同数据缓存更新请求串在一起是没有意义的,因此可以做过滤处理。等待前面的更新数据操作完成数据库操作之后,才会去执行下一个缓存更新的操作,此时会从数据库中读取最新的数据,然后写入缓存中,如果请求还在等待时间范围内,不断轮询发现可以取到缓存中值就可以直接返回(此时可能会有对这个缓存数据的多个请求正在这样处理);如果请求等待事件超过一定时长,那么这一次的请求直接读取数据库中的旧值

对于这种处理方式需要注意一些问题:

  1. 读请求长时阻塞:由于读请求进行来非常轻度的异步化,所以对超时的问题需要格外注意,超过超时时间会直接查询DB,处理不好会对DB造成压力,因此需要测试系统高峰期QPS来调整机器数以及对应机器上的队列数最终决定合理的请求等待超时时间
  2. 多实例部署的请求路由:可能这个服务会部署多个实例,那么必须保证对应的请求都通过nginx服务器路由到相同的服务实例上
  3. 热点数据的路由导师请求的倾斜:因为只有在商品数据更新的时候才会清空缓存,然后才会导致读写并发,所以更新频率不是太高的话,这个问题的影响并不是特别大,但是的确可能某些机器的负载会高一些
声明:本站部分内容来自互联网,如有版权侵犯或其他问题请与我们联系,我们将立即删除或处理。
▍相关推荐
更多资讯 >>>