缓存中间件-缓存架构的实现(上)
前言
一眨眼,2019年就过去了。我希望从按照中间件,分别阐述一些常见的架构问题,以及解决方案。一方面这些问题与解决方案具备一定通用性 。另一方面,也算是面试中常见的问题。
我希望根据自己待过各种规模公司的经验来谈一些看法。
- 如果是针对大部分小公司的工作或面试,这些问题都稍微留下个印象即可。因为小公司的技术对这些问题并不是很看重,或者说机会用不到(小型公司往往追求产品功能的实现,业务的推进等)。
- 如果是针对大部分中型公司的工作或面试,希望可以完整地知道这些问题与解决方案。因为在中型公司中,这些问题都或多或少遇到,甚至是需要迫切解决的。
- 如果是大型公司的话,那么不仅仅需要知道这些问题与解决方案。还需要从中理解为什么会有这样的问题,为什么这样解决,在现有的项目中应该如何应用,是否提升空间等。因为在大公司中,一方面其内部往往采用自研框架,其它框架能够借鉴的只有方案,思想等精髓;另一方面大公司不缺乏那些应用开源框架的人,缺的是把握方案通用思想的人。
如果上述无法理解的话,大家可以从功能性追求与非功能性追求两个方面去思考。就像写一个简单的方法一样,最基本的要求是实现其功能,紧接着就是不断追求其非功能性(如性能,扩展性,安全性等)。放大来看,对于公司的技术发展也是如此,或者说更为严格。
之后找个机会,专门写个博客,来谈谈我对公司技术与公司的看法。
话题收回来,接下来,让我开始有关中间件问题与解决方案的阐述吧。
概述
缓存的认识
既然提及缓存中间件相关的问题及方案,首先就要谈谈这个缓存。
原本我想通过高速缓存举例,但是想了想还是用内存举例子吧。
比如我们现在玩的单机游戏,往往都容量都非常大(几十G,乃至上百G),轻轻松松都超过了电脑内存(16G)。那么很明显电脑在运行游戏时,是不可能将整个游戏文件都放入内存的。但是如果文件都在硬盘里,需要的时候再读取,显然硬盘的读写速度时不够的(由于游戏文件类别很多,所以硬盘不可能一直顺序读写),那游戏也会经常卡顿,加载缓慢等。那么该如何解决这个问题呢?
其实这个问题和我们业务中遇到的一些问题是很类似的。一方面我们希望用户可以在保证用户体验的前提下查询数据(如设备列表,订单列表等),另一方面我们不可能将所有数据都放在内存(内存的读写速度比硬盘快,所以就不解释为什么用硬盘了)中。那么到底该怎么解决这个问题呢?
这里就需要说到局部性原理了。局部性原理指的是数据的访问往往趋向于聚集在较小的连续区域。这里的连续区域包含两个方面:
- 时间维度:一个被使用的数据,在接下来较短的时间内,往往会被再次使用。
- 空间维度:一个被使用的数据,其关联的数据,往往也会被使用。
局部性原理是在内存,高速缓存部分,提出来用于解决问题的。
其实,我与朋友交流分布式的一些想法时,经常说:分布式系统和单机内部是非常相似的,很多理念都是相通的。当想通了这点后,就可以去思考两者的区别的。
缓存中间件其实就是利用了局部性原理,不过缓存中间件本身只实现了局部性原理的时间维度。这也是为什么很多人都说缓存中间件是用来保存热点数据,符合二八定律。不过我们可以在应用部分实现局部性原理的空间维度。
缓存的定位
五六年前,有人就提出一个有关缓存的问题,那就是缓存作为一个非持久化数据,我们该怎么划分它。是否需要保证它的可用性。其中就有一位阿里的前辈在他的书中提到,他更倾向于认为缓存并不是一种持久化数据,不该将缓存作为一种可靠数据源。但是这位前辈也表示现有的框架中对缓存依赖较重,应该在一定程度上保护它们,避免缓存雪崩等情况。
我的看法是,在现有的技术体系中,缓存中间件等已经不再只是一个缓存了。一方面我们已经将Session等重要数据放在了缓存中,并且目前没有一个更合适的对应存储(我认为暂时也不需要一个新的存储方式。但是如果需要的话,可以将缓存中间件实例等按照内容的生命周期等进行分组)。另一方面,我们会需要明确缓存在系统中职责,它只是用来作为缓存,以及一些分布式内存。但是诸如单机所有的内部调用,应该通过消息中间件或RPC等来实现。并且明确不同缓存的职责,如Session不该放在Cookie中等。
缓存的分类
缓存框架大致可以从客户端到数据源,分成以下分类。
- 浏览器缓存
- Cookie
- LocalStorage
- SessionStorage
- CDN缓存
- 负载层缓存
- Nginx缓存模块
- Squid缓存服务器
- Lua扩展
- 应用层缓存
- Etag
- ThreadLocal
- Guava
- 外部缓存
- Redis
- 数据库缓存
- MySql缓存
我特意查询了一下百度,首页上的有关缓存架构的博客,一半都只是在围绕着缓存中间件阐述缓存架构,剩下的一般也往往在大分类上有所遗漏(如浏览器缓存,数据库缓存)。当然也有一些博客在专门的领域阐述得较为深入,或者层次的划分比较不错。故本博客只是在阐述现阶段我对缓存架构的认识(也借鉴了一些书籍,课程的缓存体系)。
浏览器缓存
浏览器缓存,也是很多时候被后端所遗忘的部分。因为这已经不属于后端的工作了,但这一定属于架构师或者相关技术负责的职责。当然还有一个原因是我做过专门的前端开发。
说白了,就是在浏览器保存一部分数据,当然这需要前端进行开发。
这里直接上图,大家可以看一下Cookie,LocalStorage,SessionStorage:
PS:图片来自网络
优势
由于是浏览器缓存,位于整个web请求相应框架的client端,所以对业务提供方没有任何负载压力与影响。只是客户端的浏览器存在些许的存储占据与计算负载。
注意
- Cookie等的存储容量是有限的,需要注意分配。
- Cookie等的存储是明文的,不可以存储敏感数据,否则会存在安全隐患。
- Cookie等需要注意存储时间时间的有效设置。
- Cookie等存在一定的学习成本,与相关特性(如Cookie的域名设置问题,父域名无法读子域名的Cookie数据)。
- Cookie等需要明确业务中有哪些数据适合放在这里,如域名等。
实际应用
在我之前负责的IOT项目中,页面往往存在大量的数据,如终端列表,传感器列表,监测点列表等。并且数据间存在一定数据关系,如需要通过现存的终端列表来获取对应传感器列表,又如通过传感器列表来获取对应报警列表等。
为了避免页面切换时,为了获取一个列表而需要多次请求(如为了获得已选定的终端列表的传感器列表,需要先请求终端列表),所以通过LocalStorage来存储终端列表。
CDN缓存
CDN,Content Delivery Network,即内容分发网络。
- CDN是构建网络上的内容分发网络
- CDN可以使得用户就近获取所需内容,避免网络拥塞,提高用户访问速度
- CDN依靠部署在各地的服务器,通过镜像服务器实现内容同步,其包括负载均衡,内容分发,调度等模块。
优势
- 降低访问延迟。使得用户就近获取所需内容,避免过多路由造成用户访问延迟问题。
- 降低服务器压力。毕竟放在CDN服务器的内容,就不用到应用服务器获取了。
- 消除运营商差别。消除运营商之家互联的瓶颈造成的影响,使得所有用户获得同样的访问质量
- 集群抗攻击。广泛分布的CDN节点,可有有效避免DDOS等攻击。
缺点
- 同步缓慢。由于CDN是大量且分层的节点分布,所以数据的下发与同步会比较缓慢。
- 如果是使用收费服务,则需要一定支出。如果是自建CDN,则需要技术付出。个人推荐,不必要的话,还是直接采用CDN收费服务吧,性价比更高一些。
- 自身Web体系需要进行相应的调整。如CDN文件更新与服务器文件更新(版本号等手段)等问题。
关键技术
该部分内容,引自网易云课堂。
- 缓存
- 缓存代理软件:Squid
- 缓存算法决定命中率,源服务器压力,FTP节点存储能力
- 分发能力
- 分发能力取决于IDC(网络数据中西)能力和IDC策略性分布
- 负载均衡
- 负载均衡软件:Nginx
- 负载均衡(智能调度)决定最佳路由,响应时间,可用性,服务质量
- 基于DNS
- DNS服务器软件:BIND
- 基于DNS的负载均衡以CNAME实现域名中专,智取最优节点服务
- 缓存点有客户端浏览器缓存,本地DNS服务器缓存
- 缓存内哦让那个有DNS地址缓存,客户请求内容缓存,动态内容缓存
- 支持协议
- 静动态加速(图片加速,https带证书加速)
- 下载加速
- 流媒体加速
- 企业应用加速
- 手机应用加速
就当扩展一下见识吧(囧)
实际应用
如果写过前端代码,会知道有的时候,我们采用的jQuery等通用JS,CSS等大多是使用公共的cdn地址。
有的公司,会将公司的一些公共JS,图片等静态资源(尤其是公司Logo等),放在CDN上。进行网页开发时,直接引用对应的CDN地址。
负载层缓存
负载层缓存一般是与负载均衡器相关的缓存,这里我就拿Nginx举例。
Nginx可以通过以下三种手段,实现缓存:
- 本身的缓存模块
- 转发请求至对应缓存服务器
- 可以通过lua模块,直接从外部缓存(如Redis等)获取缓存数据
接下来一一阐述
Nginx缓存模块
Nginx的http_proxy模块,可以实现类似于Squid的缓存功能.
Nginx对客户端已经访问的内容在Nginx服务器本地建立缓存副本,那么在一定时间内再次访问这些内容时,就不需要请求后面的应用服务器了。
与此同时,当后面的应用服务器无法提供服务时(如宕机),Nginx服务器上的缓存资源还能够回应相关的用户请求,提高了后面应用服务器的鲁棒性(健壮性)。
优势
- 商业成本无。Nginx是开源的,无需商业付费。
- 技术迭代成本低。现有的Web体系大多采用Nginx,进行技术迭代时,在Nginx只需要增加一个新的模块即可。
- 可定制。可以根据需要,对指定路径,指定资源等进行定制化的缓存策略。
缺点
- 需要对Nginx的缓存模块进行一定的认识与学习。毕竟很多人使用Nginx都只是CV一下配置。
- 需要根据业务需要与技术特点,进行缓存策略的调整。如果缺乏经验与足够的认识,可能会指定出不恰当的缓存技术规范(如哪些数据该走Nginx缓存模块等)。
基本认识
缓存文件位置设置
通过proxy_cache_path参数指定。proxy_cache_path有两个必填参数:
- 第一个参数为缓存目录。
- 第二个keys_zone参数指定缓存名称和占用内存空间的大小。
指定特定请求被缓存
- Nginx默认会缓存所有get和head方法的请求结果,缓存的key默认使用请求字符串
- 自定义key。如proxy_cache_key “ h o s t host hostrequest_uri$cookie_user”;
- 指定请求至少被发送了多少次以上才被缓存,从而避免低频请求被缓存。如proxy_cache_min_uses 5;
- 指定哪些方法的请求被缓存。如proxy_cache_methods GET HEAD POST;
缓存有效期
默认情况下,缓存内容是长期留存,除非缓存的容量超出谁知的限制。也可以自定义设置有效时间。如:
- 响应状态码为200 302时,10分钟有效期限:proxy_cache_valid 200 302 10m;
- 对任何状态码,5分钟有效期限:proxy_cache_valid any 5m;
部分请求跳过缓存
通过proxy_cache_bypass指令,明确请求对应的响应来自原始数据,而不是缓存。
例如(该示例来自网易云课堂) proxy_cache_bypass $cookie_nocache a r g n o c a c h e arg_nocache argnocachearg_comment;
表示:如果任何一个参数不为空,或者不等于0,nginx就不会查找缓存,直接进行代理转发。
扩展
网页的缓存是由HTTP消息头中的“Cache-control”来控制的,常见的取值有private,no-cache,max-age,must-revalidate等,默认为private。详见下表:
Squid缓存服务器
其实Squid缓存服务器与Nginx缓存十分类似(毕竟Nginx的缓存就是仿照Squid的),所以这里只是表示有这么个选择,不做深入。
Lua扩展
Nginx是C语言开发(这也是Nginx高性能的根本原因之一),并且Nginx模块需要用C开发,并且需要符合一系列复杂的规则,还需要熟悉Nginx源码。
ngx_lua模块
所以Nginx提供了ngx_lua模块,通过lua解释器集成进Nginx。而ngx_lua模块具备以下特性:
- 高并发,非阻塞地处理各种请求。
- Lua内建协程(可对比golang),从而将异步回调转换成顺序调用的形式。
- 每个协程都有一个独立的全局环境(变量空间),继承于全局共享的,只读的“comman data”。
上述只是简单提一下Lua扩展,感兴趣的可以查询相关资料。
这里继续阐述Lua扩展,实现缓存功能。
实际应用
为了帮助大家理解,先说一下实际应用。
Nginx针对HTTP请求处理,有十一个阶段。与之相对的,ngx_lua模块的执行指令都包含在了上述的十一个阶段。这里只说一下其中的content_by_lua指令,针对的是Nginx的content阶段,可以在location,location if范围内使用,主要作为内容处理器,接收请求处理并输出响应。
具体配置如下:
这样配置后,直接浏览器访问本地ip(或者通过curl命令),可以看到“Hello,world”。
当然,这种用法相对比较初级。在OpenResty中存在一些组件,可以帮助ngx_lua模块直接访问Redis这样的数据源。这样就可以将一些简单的数据通过这种方式来进行访问,降低应用服务器压力。
优势
- 降低应用服务器压力
- 门槛较低。可以按照一些配置模板,直接进行使用
- 扩展性较强。ngx_lua模块的应用上限还是比较高的
- 灵活性强。ngx_lua模块的灵活性,表示其在缓存方面具有较高的灵活性
缺点
- 精通难。想要精通这部分的话,需要了解lua脚本,以及Nginx的HTTP请求阶段等。
- 额外的开发任务。除了应用开发外,还需要专门的lua开发。
- 耦合性较高。一个页面,一个功能,却往往需要进行Nginx与后端联合开发。
- 任务难以界定。在业务上难以界定一些功能的开发该归于哪个模块(Nginx,后端)。
总结
至此,我们已经了解了缓存架构中最靠近用户的三层缓存:浏览器缓存,CDN缓存,负载层缓存。
如果存在什么问题,或者疑惑,可以私信或@我。