计算机系统应用教程网站

网站首页 > 技术文章 正文

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

btikc 2024-10-20 05:00:17 技术文章 5 ℃ 0 评论

概述

对于高并发架构,毫无疑问缓存是最重要的一环,对于大量的高并发,可以采用三层缓存架构来实现,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系统,通常的架构如下

这个结构的优点:

  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服务器前端系统架构:

前端的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

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

2)sentinal哨兵

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

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

3)redis cluster

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

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

具有以下特性:

1、快速轻量

  • 过去几年,诸多测试表明Ehcache是最快的Java缓存之一。
  • Ehcache的线程机制是为大型高并发系统设计的。
  • 大量性能测试用例保证Ehcache在不同版本间性能表现得一致性。
  • 很多用户都不知道他们正在用Ehcache,因为不需要什么特别的配置。
  • API易于使用,这就很容易部署上线和运行。
  • 很小的jar包,Ehcache 2.2.3才668kb。
  • 最小的依赖:唯一的依赖就是SLF4J了。

2、伸缩性

  • 缓存在内存和磁盘存储可以伸缩到数G,Ehcache为大数据存储做过优化。
  • 大内存的情况下,所有进程可以支持数百G的吞吐。
  • 为高并发和大型多CPU服务器做优化。
  • 线程安全和性能总是一对矛盾,Ehcache的线程机制设计采用了Doug Lea的想法来获得较高的性能。
  • 单台虚拟机上支持多缓存管理器。
  • 通过Terracotta服务器矩阵,可以伸缩到数百个节点。

3、灵活性

  • Ehcache 1.2具备对象API接口和可序列化API接口。
  • 不能序列化的对象可以使用除磁盘存储外Ehcache的所有功能。
  • 除了元素的返回方法以外,API都是统一的。只有这两个方法不一致:getObjectValue和getKeyValue。这就使得缓存对象、序列化对象来获取新的特性这个过程很简单。
  • 支持基于Cache和基于Element的过期策略,每个Cache的存活时间都是可以设置和控制的。
  • 提供了LRU、LFU和FIFO缓存淘汰算法,Ehcache 1.2引入了最少使用和先进先出缓存淘汰算法,构成了完整的缓存淘汰算法。
  • 提供内存和磁盘存储,Ehcache和大多数缓存解决方案一样,提供高性能的内存和磁盘存储。
  • 动态、运行时缓存配置,存活时间、空闲时间、内存和磁盘存放缓存的最大数目都是可以在运行时修改的。

4、标准支持

  • Ehcache提供了对JSR107 JCACHE API最完整的实现。因为JCACHE在发布以前,Ehcache的实现(如net.sf.jsr107cache)已经发布了。
  • 实现JCACHE API有利于到未来其他缓存解决方案的可移植性。
  • Ehcache的维护者Greg Luck,正是JSR107的专家委员会委员。

5、可扩展性

  • 监听器可以插件化。Ehcache 1.2提供了CacheManagerEventListener和CacheEventListener接口,实现可以插件化,并且可以在ehcache.xml里配置。
  • 节点发现,冗余器和监听器都可以插件化。
  • 分布式缓存,从Ehcache 1.2开始引入,包含了一些权衡的选项。Ehcache的团队相信没有什么是万能的配置。
  • 实现者可以使用内建的机制或者完全自己实现,因为有完整的插件开发指南。
  • 缓存的可扩展性可以插件化。创建你自己的缓存扩展,它可以持有一个缓存的引用,并且绑定在缓存的生命周期内。
  • 缓存加载器可以插件化。创建你自己的缓存加载器,可以使用一些异步方法来加载数据到缓存里面。
  • 缓存异常处理器可以插件化。创建一个异常处理器,在异常发生的时候,可以执行某些特定操作。

6、应用持久化

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

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

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

7、分布式缓存

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

分布式缓存的选项包括:

  • 通过Terracotta的缓存集群:设定和使用Terracotta模式的Ehcache缓存。缓存发现是自动完成的,并且有很多选项可以用来调试缓存行为和性能。
  • 使用RMI、JGroups或者JMS来冗余缓存数据:节点可以通过多播或发现者手动配置。状态更新可以通过RMI连接来异步或者同步完成。
  • Custom:一个综合的插件机制,支持发现和复制的能力。
  • 可用的缓存复制选项。支持的通过RMI、JGroups或JMS进行的异步或同步的缓存复制。
  • 可靠的分发:使用TCP的内建分发机制。
  • 节点发现:节点可以手动配置或者使用多播自动发现,并且可以自动添加和移除节点。对于多播阻塞的情况下,手动配置可以很好地控制。
  • 分布式缓存可以任意时间加入或者离开集群。缓存可以配置在初始化的时候执行引导程序员。
  • BootstrapCacheLoaderFactory抽象工厂,实现了BootstrapCacheLoader接口(RMI实现)。
  • 缓存服务端。Ehcache提供了一个Cache Server,一个war包,为绝大多数web容器或者是独立的服务器提供支持。
  • 缓存服务端有两组API:面向资源的RESTful,还有就是SOAP。客户端没有实现语言的限制。
  • RESTful缓存服务器:Ehcached的实现严格遵循RESTful面向资源的架构风格。
  • SOAP缓存服务端:Ehcache RESTFul Web Services API暴露了单例的CacheManager,他能在ehcache.xml或者IoC容器里面配置。
  • 标准服务端包含了内嵌的Glassfish web容器。它被打成了war包,可以任意部署到支持Servlet 2.5的web容器内。Glassfish V2/3、Tomcat 6和Jetty 6都已经经过了测试。

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. 热点数据的路由导师请求的倾斜:因为只有在商品数据更新的时候才会清空缓存,然后才会导致读写并发,所以更新频率不是太高的话,这个问题的影响并不是特别大,但是的确可能某些机器的负载会高一些

后面会分享更多devops和DBA方面的内容,感兴趣的朋友可以关注下~

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表