美文网首页程序开发程序员架构师成长记
浩仔和你说说 可伸缩性、可扩展性、缓存可扩展性

浩仔和你说说 可伸缩性、可扩展性、缓存可扩展性

作者: AQ王浩 | 来源:发表于2015-07-14 16:07 被阅读1765次

    一、什么是可伸缩性

    可伸缩性是一种对软件系统计算处理能力的设计指标,高可伸缩性代表一种弹性,在系统扩展成长的过程中,软件能够保证旺盛的生命力,通过很少的改动甚至只是硬件设置的添置,就能实现整个系统能力的线性增长,实现高吞吐量和低延迟性能。

    二、可伸缩性和纯粹性能调优是有区别的:

    • 可伸缩性是高性能、低成本和可维护性等诸多因素的综合考量和平衡,可伸缩性讲究平滑线性的性能提升,更侧重于系统的水平伸缩,通过廉价的服务器实现分布式计算;

    • 而普通性能优化只是单台机器的性能指标优化。他们共同点都是根据应用系统特点在吞吐量和延迟之间进行一个侧重选择,当水平伸缩分区后会带来CAP定理约束

    三、软件的可扩展性设计注意

    软件的可扩展性设计非常重要,但又比较难以掌握,业界试图通过云计算或高并发语言等方式节省开发者精力,但是,无论采用什么技术,如果应用系统内部是铁板一块,例如 严重依赖数据库, 系统达到一定访问规模,负载都集中到一两台数据库服务器上,这是进行分区扩展伸缩就比较困难。

    关系数据库是最不可扩展的。

    四、性能和扩展性

    • 什么是性能问题?如果你的系统对于一个用户访问还很慢,那就是性能问题。

    • 什么是扩展性问题?如果你的系统对于一个用户来说很快的,但是在用户不断增长的高访问量下就慢了。

    五、可扩展性的目的

    延迟和吞吐量是衡量可扩展性的一对指标,我们希望获得低延迟高吞吐量的系统架构。所谓低延迟,也就是用户能感受到的系统响应时间,比如一个网页在几秒内打开,越短表示延迟越低,而吞吐量表示同时有多少用户能够享受到这种低延迟,如果并发用户量很大时,用户感觉网页的打开速度很慢,这意味着系统架构的吞吐量有待提高。

    扩展性的目标是用可接受的延迟获得最大的吞吐量。可靠性(可用性)目标:用可接受的延迟获得数据更新的一致性

    六、缓存的可扩展性

    缓存层的伸缩性,最简单粗暴的方式是什么呢?

    趁着半夜量比较低的时候,把整个缓存层全部下线,然后上线新的缓存层。新的缓存层启动起来之后,再等这些缓存慢慢预热。当然这里一个要求,你的数据库能抗住低估期的请求量。如果扛不住呢?取决于缓存类型,下面我们先可以将缓存的类型区分一下。

    • 强一致性缓存:无法接受从缓存拿到错误的数据 (比如用户余额,或者会被下游继续缓存这种情形)

    • 弱一致性缓存:能接受在一段时间内从缓存拿到错误的数据 (比如微博的转发数)。

    • 不变型缓存:缓存key对应的value不会变更 (比如从SHA1推出来的密码, 或者其他复杂公式的计算结果)

    那什么缓存类型伸缩性比较好呢?

    • 弱一致性和不变型缓存的扩容很方便,用一致性Hash即可;

    • 使用一致性Hash,而不用简单Hash的原因是缓存的失效率。如果缓存从9台扩容到10台,简单Hash 情况下90%的缓存会马上失效,而如果使用一致性Hash情况,只有10%的缓存会失效。

    强一致性缓存会有什么问题?

    • 第一个问题是,缓存客户端的配置更新时间会有微小的差异,在这个时间窗内有可能会拿到过期的数据。

    • 第二个问题是,如果扩容之后再裁撤节点,会拿到脏数据。比如 a 这个key之前在机器1,扩容后在机器2,数据更新了,但裁撤节点后key回到机器1,这时候就会拿到脏数据。

    要解决问题二比较简单,要么保持永不减少节点,要么节点调整间隔大于数据的有效时间。

    问题一可以用如下的步骤来解决:

    1、两套hash配置都更新到客户端,但仍然使用旧配置;
    2、逐个客户端改为只有两台Hash结果一致的情况下会适用缓存,其余情况从数据库读,但写入缓存;
    3、逐个客户端通知使用新配置;

    Memcache 设计得比较早,导致在伸缩性高可用方面的考虑得不太周到。Redis 在这方面有不少改进,特别是 @ngaut 团队基于 redis 开发了 codis 这个软件,一次性地解决了缓存层的绝大部分问题。推荐大家考察一下。

    八、缓存穿透,缓存雪崩
    • 缓存穿透:查询一个必然不存在的数据。比如文字表,查询一个不存在的id,每次都会访问DB,如果有人恶意破坏,很可能直接对DB造成影响。解决办法:对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合直接丢弃

    • 缓存失效:如果缓存在一段时间内失效,DB的压力凸显。这个没有完美的解决办法,但可以分析用户行为,尽量让失效时间点均匀分布。

    • 缓存雪崩:当发生大量的缓存穿透,例如对某个失效的缓存的大并发访问就造成了缓存雪崩。比如前端的Cache挂掉,或者比较极端的整个机房断电了,那么在机器重启后,原来Cache机器在内存中的缓存会全部清空,在客户端访问过程中,会百分之百的不命中,这样数据库会在瞬间接受巨大的读压力。

    试想如果一个64GB的缓存失效了,在其重建时,假设与数据库连接的千兆网卡,假设其以极限速度100M每秒从数据库取数据过来重建缓存,那么也需要10分钟才能建完。更何况这是理想情况,对于客户端触发式的随机缓存重建,可能会花掉更长的时间。这还是在数据库能提供100M每秒的数据读请求的前提下。

    我们经常看到一些网站挂掉后又恢复,恢复后又挂掉,如此反复几次才能真正恢复,原因就在于其第一次Cache倒了,数据库无法承受相应的读压力,在缓存重建了一小部分后被压死。相当于数据库每重启一次,可以恢复部分缓存,直到缓存的非命中率到达数据库可承受的压力时,才能够真正恢复服务。

    九、如何解决缓存雪崩

    第一种方式

    概述: 做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期

    oschina首页使用了缓存,假设我们前面举的例子中所使用的缓存region(region1),设置了自动失效时间为5分钟,相等于说每5分钟就会发现首页很卡。因此引入第二个缓存region(region2),这个缓存的对象不会自动失效,也就说该区域的数据长期有效。

    引入了第二个长效的region后,数据的读取流程这样的:

    1. 从 region1 读取数据,有则直接返回

    2. region1 没数据则启动数据更新线程,然后从 region2 读数据,有则返回

    3. region2 也没有数据

    针对第3种,这种情况属于系统刚刚启动,缓存还没有填充数据的情况,没办法,这时候肯定会卡,或者你应该在系统启动的时候,自行填充一下数据,很简单,我一般在tomcat启动后,用命令访问下首页就有了缓存数据。

    这样做的目的是为了正常的缓存失效后,无需等待重新从数据库中获取数据,而是直接在 region2 中获取数据并返回。因此对用户来讲,不会感觉请求被堵塞。

    就这么简单,其实也可以工作,但会有一个问题:假设缓存失效的时候,同时来了100个请求,那么这100个请求会同时启动100个数据更新线程,这100个数据更新线程会到数据库执行同样的SQL语句获得同样的结果,因此这种做法对数据库的压力并没有降低。

    所以缓存失效的情况下,保证有且只有一个线程去更新缓存数据。

    其他方式

    • 对查询结果为空的情况也进行缓存,缓存时间设置短一些,或者该key对应的数据insert了之后清理缓存。

    • 不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀

    • 可以用一些可以提供持久化功能的缓存来实现,比如Redis,在未开启aof的情况下,其定期dump出来的rdb文件出能自动恢复出绝大部分数据,当然,在有的时候这可能导致缓存和数据库数据不一致的情况,需要根据应用场景选择性的使用。

    十、缓存重建

    上面是对分布式Cache的问题,而对于很多数据库存储,实际上也几乎都是将热数据尽量放在内存中的。但很多数据库在实现上是自己在内存中实现了Cache机制,这样在数据库重启(非操作系统重启)时,这些Cache可能也就随之被清空了,对于数据库来说,也需要重建缓存,而数据库这时所有的操作可能都落在磁盘IO上,带来了同样的问题。

    而MongoDB与上面的方式不太一样,MongoDB采用mmap来将数据文件映射到内存中,所以当MongoDB重启时,这些映射的内存并不会清掉,因为它们是由操作系统维护的(所以当操作系统重启时,MongoDB才会有相同问题)。相对于其它一些自己维护Cache的数据库,MongoDB在重启后并不需要进行缓存重建与预热。

    另外,新浪微博的timyang也曾经提出过一种缓存重建加锁的方式,也能部分解决此问题。简单来说就是缓存重建时,当多个客户端对同一个缓存数据发起请求时,会在客户端采用加锁等待的方式,对同一个Cache的重建需要获取到相应的锁才行只有一个客户端能拿到锁,并且只有拿到锁的客户端才能访问数据库重建缓存,其它的客户端都需要等待这个拿到锁的客户端重建好缓存后直接读缓存,其结果是对同一个缓存数据,只进行一次数据库重建访问。但是如果访问分散比较严重,还是会瞬间对数据库造成非常大的压力。

    下面是几点比较实用的知识:

    1. 无论使用哪个存储,都最好先搞清楚其缓存重建的过程,如果一次重启就可能导致数据库崩溃,还是小心为好,最好把重启时间选在访问量比较小的时候

    2. 重启MongoDB不会导致MongoDB的缓存失效(除非重启服务器)

    3. 当你重新mount磁盘时,文件系统的缓存会失效,这和重启机器时一样,MongoDB也无法避免

    4. 一个使用MongoDB的小技巧,当MongoDB服务器刚启动时,你可以将其所有文件copy到/dev/null中,这会触发操作系统对这些文件的读操作,从而在内存允许的条件下,会将尽可能多的MongoDB数据文件映射到物理内存中。当然,如果在MongoDB运行过程中,你能够判断哪些文件保存的数据是热数据,也可以将这些文件copy到/dev/null 来为其争取更多的物理内存。

    十一、缓存一致性

    1. 缓存系统与底层数据的一致性。这点在底层系统是“可读可写”时,写得尤为重要。

    2. 有继承关系的缓存之间一致性。为了尽量提高缓存命中率,缓存也是分层:全局缓存,二级缓存。他们是存在继承关系的。全局缓存可以有二级缓存来组成。

    3. 多个缓存副本之间的一致性。为了保证系统的高可用,缓存系统背后往往会接两套存储系统(如memcache,redis等)

    十二、缓存数据的淘汰

    1. 定时去清理过期的缓存。

    2. 当有用户请求过来时,在判断这个请求所用到的缓存是佛过期,过期的话就去底层得到新数据并更新缓存。

    两者各有优劣:
    第一种的缺点是维护大量缓存的key是比较麻烦的
    第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂,具体哪种方案,大家可以根据自己的应用场景来权衡。

    缓存淘汰的几种会用到的技巧

    1. 预估失效时间。

    2. 版本号(必须单调递增,时间戳是最好的选择)。

    3. 提供手动清理缓存的接口。

    十三、高手说可扩展

    衡量架构伸缩性的主要标准就是是否可以用多台服务器构建集群,是否容易向集群中添加新的服务器。加入新的服务器后是否可以提供和原来的服务器无差异的服务。集群中可容纳的总的服务器数量是否有限制。

    对于应用服务器集群,只要服务器上不保存数据,所有服务器都是对等的,通过使用合适的负载均衡设备就可以向集群中不断加入服务器。

    对于缓存服务器集群,加入新的服务器可能会导致缓存路由失效,进而导致集群中大部分缓存数据都无法访问。虽然缓存的数据可以通过数据库重新加载,但是如果应用已经严重依赖缓存,可能会导致整个网站崩溃。需要改进缓存路由算法保证缓存数据的可访问性。

    关系型数据库虽然支持数据复制,主从热备机制,但是很难做到大规模集群的可伸缩性,因此关系数据库的集群伸缩性方案必须在数据库之外实现,通过路由分区等手段将部署有多个数据库的服务器组成一个集群。

    至于大部分NoSQL数据库产品,由于其先天就是为海量数据而生,因此其对伸缩性的支持通常都非常好,可以做到在较少运维参与的情况下实现集群规模的线性绳索。

    《大型网站技术架构核心原理与案例分析》

    十四、总结

    1. 缓存均匀分布,均匀失效。

    2. 二级缓存解决缓存雪崩

    3. 缓存很重要,但是数据库的查询优化也很重要

    4. 情况允许,扩大MySQL临时表的空间,以防缓存雪崩瞬间巨大访问,导致MySQL崩溃

    5. 需要在流量少的情况下,切换缓存,切换完毕之后。需要缓存预热

    6. 多看书,多试验

    参考

    oschina 上的一种双缓存思路
    名词解释“缓存穿透”与“缓存雪崩”
    高可用可伸缩架构实用经验谈
    大型网站技术架构核心原理与案例分析
    缓存穿透与缓存雪崩
    不可忽略的缓存重建

    相关文章

      网友评论

      • Eric_P:这个逗比写的 恐怕他自己都没有明白 可伸缩性与可扩展性的区别 真是逗比 还有5个人喜欢 是不是拖? 简书 怎么都是这种逗比的存在 不知道从哪里复制粘贴的 有意思吗 自己不假思索 完全复制别人的 不觉得丢人
        AQ王浩:@Eric_P 感谢批评指正,这个确实从一本书里面COPY出来的。看来是高手,现在看这篇文章我也觉得惭愧

      本文标题:浩仔和你说说 可伸缩性、可扩展性、缓存可扩展性

      本文链接:https://www.haomeiwen.com/subject/ibwuqttx.html