极客大学 - 架构师训练营 第五周
第五周 技术选型(一)
分布式缓存架构:架构原理与使用中的注意事项
1. 缓存概念
缓存是指存储在计算机上的一个原始数据复制集,以便与多次访问
缓存是介于数据访问和数据源之间的一种高速存储,当数据需要多次读取的时候,用于加快读取的速度
典型应用: 在应用程序和数据库之间,加一个缓存 (通常在内存中)
2. 缓存 (cache) vs 缓冲 (buffer)
注意缓存和缓冲是两个不同的概念,缓冲通常应用在高速设备和低速设备之间,但是不是为了加快多次的读取,而是为了把突发的大数量较小规模的 I/O 整理成平稳的小数量较大规模的 I/O,以减少响应次数,从而让系统读取平衡。
3. 各种介质数据的访问延迟
4. 缓存的应用场景
CPU 缓存
提高访问内存(RAM)的效率
CPU 缓存有多级缓存,比如 L1, L2, L3 等
L1 容量最小,速度最快,每个核都有 L1 缓存
L2 容量比 L1 大,速度比 L1 慢,每个核都有 L2 缓存
L3 容量最大,速度最慢,多个核共享一个 L3 缓存
操作系统缓存
读取文件的时候,使用缓存
Linux 系统默认的设置倾向于把内存尽可能的用于文件 cache,所以在一台大内存机器上,往往我们可能发现没有多少剩余内存
可采用
free
的命令来查看数据库缓存
数据库缓存的第一个技术特点就是提高性能,所以数据库缓存的数据基本上都是存储在内存中,相比 io 读写的速度,数据访问快速返回
memcache 这种跟数据库缓存直接挂钩的中间件基本已经集成进去了了数据库应用,不用单独部署
数据库缓存技术应用场景绝大部分针对的是“查”的场景
数据库缓存场景的开源技术,那必然是 memcache 和 redis 这两个中间件
JVM 编译缓存
编译缓存存储指令,不是每次都解释执行
JVM 代码缓存是 JVM 将其字节码存储为本机代码的区域 。我们将可执行本机代码的每个块称为 nmethod 。该 nmethod 可能是一个完整的或内联 Java 方法。
CDN 缓存
Content Delivery Network - 用于网站加速或者用户下载资源加速
CDN 节点解决了跨运营商和跨地域访问的问题,访问延时大大降低
大部分请求在 CDN 边缘节点完成,CDN 起到了分流作用,减轻了源站的负载
客户端浏览器先检查是否有本地缓存是否过期,如果过期,则向 CDN 边缘节点发起请求,CDN 边缘节点会检测用户请求数据的缓存是否过期,如果没有过期,则直接响应用户请求,此时一个完成 http 请求结束;如果数据已经过期,那么 CDN 还需要向源站发出回源请求(back to the source request),来拉取最新的数据。
代理与反向代理缓存
代理(正向代理,目标服务器不知道谁在访问)
反向代理(用户实际并不知道最终服务器,只是访问一个反向代理服务器而已)
对于一些含有大量内容的网站来说,随着访问量的增多,对于经常被访问的内容,如果每一次都从服务器中获取,则给服务器很大的压力。所以我们可以利用反向代理服务器对访问频率较多的内容进行缓存,有利于节省后端服务器的资源。
web 缓存服务器位于内容源 web 服务器和客户端之间,当客户端访问一个 url 时,缓存服务器请求内容源服务器,并将响应结果缓存到内存或硬盘,当下一次请求同一个 url 时,缓存服务器直接将已缓存的内容输出给客户端,这样就减少了再次向内容源服务器请求的次数。
前端缓存
“请求”和“响应”中进行
在“请求”步骤中,浏览器也可以通过存储结果的方式直接使用资源,直接省去了发送请求
而“响应”步骤需要浏览器和服务器共同配合,通过减少响应内容来缩短传输时间
应用程序缓存
应用程序缓存是 HTML5 的重要特性之一,基于 web 的应用程序可以离线运行
开发者可以使用 Application Cache (AppCache) 接口设定浏览器应该缓存的资源并使得离线用户可用
在处于离线状态时,即使用户点击刷新按钮,应用也能正常加载与工作
好处
离线浏览: 用户可以在离线状态下浏览网站内容。
更快的速度: 因为数据被存储在本地,所以速度会更快。
减轻服务器的负载: 浏览器只会下载在服务器上发生改变的资源
分布式对象缓存
应用于高并发的分布式系统中
加速读写。因为缓存通常是全内存的,比如 Redis、Memcache。对内存的直接读写会比传统的存储层如 MySQL,性能好很多
降低后端的负载。缓存一些复杂计算或者耗时得出的结果可以降低后端系统对 CPU、IO、线程这些资源的需求,让系统运行在一个相对资源健康的环境
LRU/LFU/FIFO
超时剔除
主动更新
5. 缓存的工作原理
哈希表
Hash 算法一般也被称为散列算法,通过散列算法将任意的值转化成固定的长度输出,该输出就是散列值,这是一种压缩映射,也就是,散列值的空间远远小于输入的值空间
hash 算法的意义在于提供了一种快速存取数据的方法,它用一种算法建立键值与真实值之间的对应关系
哈希表 hashtable(key,value) 就是把 Key 通过一个固定的算法函数既所谓的哈希函数转换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下标,将 value 存储在以该数字为下标的数组空间里。
举一个例子,假如我的数组 A 中,第 i 个元素里面装的 key 就是 i,那么数字 3 肯定是在第 3 个位置,数字 10 肯定是在第 10 个位置。哈希表就是利用利用这种基本的思想,建立一个从 key 到位置的函数,然后进行直接计算查找。
树形结构 Redis
6. 缓存的关键指标
缓存命中率: 缓存中一次写进去的内容能不能够多次去读出来响应业务的请求。这个指标就叫做缓存命中率。计算方法是查询得到的正确结果除以查询次数,得到的值就是缓存命中率。比如说十次查询有九次能够得到正确的结果,那么命中率就是 90%
影响缓存命中率的主要指标
缓存键集合大小: 尽量减少缓存键的数量,键的数量越少,缓存效率越高
缓存可使用内存空间: 物理上缓存的空间越大,缓存的对象越多,命中率越高
缓存对象生存时间 (TTL): 对象缓存的时间越长,被重用的可能性就越高
7. 技术栈各个层次的缓存
分布式缓存架构:常见的缓存实现形式
通读缓存
代理缓存、反向代理缓存、CDN 缓存,都是通读缓存。它代理了用户的请求,也就是说用户在访问数据的时候,总是要通过通读缓存。当通读缓存中有需要访问的数据的时候,直接就把这个数据返回;如果没有,再由通读缓存向真正的数据提供者发出请求。其中重要的一点是客户端连接的是通读缓存,而不是生成响应的原始服务器,客户端并不知道真正的原始服务器在哪里,不会直接连接原始服务器,而是由通读缓存进行代理。
1. 代理缓存
一般是指正向代理缓存,正向代理(Proxy)模式是代理网络用户访问 internet,客户端将本来要直接发送到 internet 上源服务器的连接请求发送给代理服务器处理。向代理的目的是加速用户在使用浏览器访问 Internet 时的请求响应时间,并提高广域网线路的利用率。正向代理浏览器无需和该站点建立联系,只访问到 Web 缓存即可。通过正向代理,大大提高了后续用户的访问速度,使他们无需再穿越 Internet,只要从本地 Web 缓存就可以获取所需要的信息,避免了带宽问题,同时可以大量减少重复请求在网络上的传输,从而降低网络流量,节省资费。
2. 反向代理缓存
反向代理(Reverse Proxy)模式是针对 Web 服务器加速功能的,在该模式中,缓存服务器放置在 web 应用服务器的前面,当用户访问 web 应用服务器的时候,首先经过缓存服务器,并将用户的请求和应用服务器应答的内容写入缓存服务器中,从而为后续用户的访问提供更快的响应。
3. CDN 内容分发网络
尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输的更快、更稳定。通过在网络各处放置节点服务器所构成的在现有的互联网基础之上的一层智能虚拟网络,CDN 系统能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。
解决因分布、带宽、服务器性能带来的访问延迟问题,适用于站点加速、点播、直播等场景。使用户可就近取得所需内容,解决 Internet 网络拥挤的状况,提高用户访问网站的响应速度和成功率。
控制时延无疑是现代信息科技的重要指标,CDN 的意图就是尽可能的减少资源在转发、传输、链路抖动等情况下顺利保障信息的连贯性。
旁路缓存
和通读缓存相对应的叫作旁路缓存。前面提到的 key、value 这样的对象缓存就属于旁路缓存。旁路缓存和通读缓存不同。旁路缓存是客户端先访问旁路缓存中是否有自己想要的数据,如果旁路缓存中没有需要的数据,那么由客户端自己去访问真正的数据服务提供者,获取数据。客户端获得数据以后,会自己把这个数据写入到旁路缓存中,这样下一次或者其他客户端去读取旁路缓存的时候就可以获得想要的数据了。
本地对象缓存
对象直接缓存在应用程序的内存中。
对象存储在共享内存,同一台机器的多个进程可以访问它们。
缓存服务器作为独立的应用部署在同一个服务器上。
远程分布式对象缓存
应用程序和缓存都部署在独立的服务器上,而多个缓存服务器构成一个缓存集群,共同对外部的应用程序服务器提供缓存服务。应用程序想要缓存的时候,通过远程 RPC 调用的方式去访问分布式对象缓存集群,获得其想要的数据。
缓存服务器不需要占用服务器内部的资源,有利于提高应用服务器自身的性能,而构建缓存集群可以实现线性伸缩的方式来进行扩展。
这个方案也是目前互联网应用架构中主要使用的方案 (Memcached, Redis)
Memcached 分布式对象缓存 (Share-Nothing)
三台 memchched 服务器构建的集群。
memcache 的实现通过客户端(Client) 和 服务端(Server)组成,Client 将需要读取的 Key 通过一致性 Hash 算法选择可用的 Server 进行读写操作。Memecache 的 Server 集群之间相互并不感知彼此的存在。Server 直接没有任何通信,可以通过添加 Servers 的方式轻松扩展虚拟内存。Server 通过最近最少使用算法 LRU (Least Recently Used)进行内存置换。
memcached 的客户端来计算写入和需要读取的数据,每一台 memcached 服务器的存储的数据都不一样。一个 key-value 只会写入到一个 memcached 服务器里面去。这样我们就可以通过线性增加服务器的方式来进行集群计算资源的扩容,实现处理更多并发的能力。
memcached 客户端
假设客户端要写入一个 key-value 键值对,那么客户端的应用程序先调用 memcached 的 API 的 set 访问,然后 API 会调用模块内的路由算法去进行计算,计算出这样的 key-value 应该写入到哪个服务器去。
路由算法的原理类似 hash,每个 key 都有自己的 hashcode, 然后把 hashcode 对 node 的数量取模,就知道写入的是哪台服务器。
分布式缓存架构:一致性 Hash 算法
1. 一致性 hash 解决的问题
一致性 Hash 算法是为了解决普通的 hash 算法在分布式应用中的不足而应运而生的,在分布式的存储系统中,要将数据存储到具体的节点上,如果我们采用普通的 hash 算法进行路由,将数据映射到具体的节点上,如 key%N,key 是数据的 key,N 是机器节点数,如果有一个机器加入或退出这个集群,则所有的数据映射都无效了,如果是持久化存储则要做数据迁移,如果是分布式缓存,则其他缓存就失效了。这样很有可能会引起由于缓存命中率低下而带来的大量的对于数据库的访问,增加系统负担。
2. 一致性 hash 算法需要满足的条件
均衡性(Balance)
平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。很多哈希算法都能够满足这一条件。
单调性(Monotonicity)
单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲区加入到系统中,那么哈希的结果应能够保证原有已分配的内容可以被映射到新的缓冲区中去,而不会被映射到旧的缓冲集合中的其他缓冲区。
分散性(Spread)
在分布式环境中,终端有可能看不到所有的缓冲,而是只能看到其中的一部分。当终端希望通过哈希过程将内容映射到缓冲上时,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是相同的内容被不同的终端映射到不同的缓冲区中。这种情况显然是应该避免的,因为它导致相同内容被存储到不同缓冲中去,降低了系统存储的效率。分散性的定义就是上述情况发生的严重程度。好的哈希算法应能够尽量避免不一致的情况发生,也就是尽量降低分散性。
负载(Load)
负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷。
3. 一致性 hash 的设计
按照常用的 hash 算法来将对应的 key 哈希到一个具有 2^32 次方个节点的空间中,即 0 ~ (2^32)-1 的数字空间中。现在我们可以将这些数字头尾相连,想象成一个闭合的环形。
将各个服务器使用 Hash 进行一个哈希,具体可以选择服务器的 ip 或唯一主机名作为关键字进行哈希,这样每台机器就能确定其在哈希环上的位置。假设我们将 3 台服务器(左图)使用 IP 地址哈希后在环空间的位置如上图(左图)。NODE0, NODE1, NODE2。白色圆圈为 key 的结点位置,key 结点按照顺时针方向查找距离自己最近的一个 Node 结作为真正的数据存储结点。因为 hash 值带有随机性,每一个物理节点很难做到均匀的分布在环上,这样就会导致 NODE 上的数据分布不均匀,而导致不同的 NODE 的不同负载压力。负载不均衡的问题就出现了。
而当我们需要增加一个新的服务器的时候,如右图所示如果向 hash 环中添加一个 Node3 结点,原来介于 Node2 和 Node1 之间的 key3 将有一部分数据转移到 Node3 上实现水平伸缩。带来的问题也是很明显的,如果大部分的 keys 是介于 Node0 和 Node2 之间,并没有因为 Node3 结点的加入而减少本身结点的负载。所以此类算法并未达到负载均衡的效果。
4. 基于虚拟节点的一致性 ahsh 算法
为了解决这种数据倾斜问题,一致性哈希算法引入了虚拟节点机制,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。然后把若干个虚拟节点放在环上。
由于 Hash 值取值(按照 HashCode 于 2^32 取模)带有一定的随机性所以理论上 Node0 到 Node3 的虚拟结点是存在重合的但是带来的负面影响不大可以忽略。按照上面提到过的 key 的选择方式 key 将先会找到某个 Node 结点的虚拟结点,再通过虚拟结点找到真正的 Node 结点,实现 key 数据的读取或保存。平均下来每个虚拟节点的访问量相近。实践中一般是一个物理节点包含 150 个虚拟节点。
那么我们计算一下如果再增加一个节点 NODE4,会有多少数据受到影响呢?假设每一个 Node 结点一共有 10 个虚拟结点。原来虚拟结点的总数量有 30 个,原来 key 的数量是 300 而且均匀的分布在每一个结点之上。每一个结点承载 10 个 key。新增 10 个结点后的影响,每个节点承载 300/40=7.5 个 key。那么受到影响的 key 为 2.5 个/节点。
5. 缓存为什么能够显著提升性能
缓存数据通常来自于内存,比磁盘上的数据有更快的访问速度。
缓存数据存储数据的最终结果形态,不需要中间计算,减少 cpu 资源消耗。
缓存降低数据库,磁盘,网络的负载压力,使这些 io 设备获得更好的响应特性。
6. 合理使用缓存需要考虑的问题
频繁修改的数据不适用缓存,一般来说读写比在 2:1 以上,缓存才有意义。
没有热点的访问 (二八定律)
缓存使用内存作为存储,内存资源宝贵而有限,不能将所有数据都缓存起来,如果应用系统访问数据没有热点,不遵循二八定律,即大部分数据访问不是集中在小部分数据上,那么缓存就没有意义,因为大部分数据还没有被再次访问就已经被挤出缓存了。
数据不一致与脏读
一般会对缓存的数据设置失效时间,一旦超过失效时间,就要从数据库中重新加载。因此应用要容忍一定时间的数据不一致,如卖家已经编辑了商品属性,但是需要过一段时间才能被买家看到。在互联网应用中,这种延迟通常是可以接受的,但是具体应用仍需慎重对待。还有一种策略是数据更新时立即更新缓存,不过也会带来更多系统开销和事务一致性的问题。因此数据更新时通知缓存失效,删除该缓存数据,是一种更加稳妥的做法。
缓存雪崩
缓存是为了提高数据读取性能的,缓存数据丢失或者缓存不可用不会影响到应用程序的处——它可以从数据库直接获取数据。但是随着业务的发展,缓存会承担大部分的数据访问压力,数据库已经习惯了有缓存的日子,所以当缓存服务崩溃的时候,数据库会因为完全不能承受如此大的压力而宕机,进而导致整个网站不可用。这种情况,被称作缓存雪崩,发生这种故障,甚至不能简单的重启缓存服务器和数据库服务器来恢复网站访问。
缓存预热
缓存中存放的是热点数据,热点数据又是缓存系统利用 LRU(最近最久未用)算法对不断访问的数据筛选淘汰出来的,这个过程需要花费较长的时间,在这段时间,系统的性能和数据库负载都不太好,那么最好在缓存系统启动的时候就把热点数据加载好,这个缓存预加载手段叫做缓存预热(warm up)。对于一些元数据如城市地名列表、类目信息,可以启动时加载数据库中全部数据到缓存进行预热。
缓存穿透。
如果不恰当的业务、或者恶意攻击持续高并发的请求某个不存在的数据,因为缓存没有保存该数据,所有的请求都会落到数据库上,会对数据库造成很大的压力,甚至崩溃。一个简单的对策是将不存在的数据也缓存起来(其 value 值为 null),并设定一个较短的失效时间。
消息队列:如何避免系统故障传递?
写数据的时候通常使用异步架构的消息队列。以发送邮件为例,以下是发送消息的同步调用和异步调用,同步调用发送邮件耗时较长,而异步调用使用消息队列,不需要等到整个流程结束。邮件的发送可以异步的去执行。但是这样的异步调用有客户端不知道消息是否发送成功的弊端。
为了解决这个问题,我们可以增加回调,如下图所示
通过回调函数来决定如何处理。
放大来看,整个异步调用的流程
1. 如何构建一个消息队列的异步调用架构
主要有三个角色,消息生产者(产生消息),消息队列(获取消息,处理消息),以及消息消费者(接受消息)。如下图所示
2. 消息队列的点对点模型
一次生产,一次消费
3. 消息队列发布订阅模型
一次生产,多次消费
4. 使用消息队列的好处
实现异步处理,提升处理性能
应用服务器可以把消息写入队列,然后实现异步写入数据库,不用等待较慢的数据库写操作完成
更好的伸缩性
削峰填谷
一般来说一个应用的负载是不均匀的,有时段的。利用消息队列,在高峰期把用户请求写入到消息队列,作为一个和数据库之间的缓冲。让数据库不用高并发的去处理高峰期的消息。
失败隔离和自我修复
因为发布者不直接依赖消费者,所以消息系统可以将消费者系统错误与生产者系统组件隔离。生产者和消费者互相不受对方失败影响。这意味着任意时刻,我们都可以对后端服务器执行维护和发布操作。我们可以重启、添加或删除服务 器而不影响生产者可用性,这样简化了部署和服务器管理的难度。
解耦
开发程序的消息发布者,不需要知道消息的消费者在哪里以及消费者的程序是什么样子,没有直接的耦合。反而消费者也不需要关心是谁发布消息,直接从消息队列里去消息消费就好。
5. 利用消息队列构建事件驱动架构 EDA
一种经典和广泛的架构方式。当一个生产者去发布一个事件,而消费者通过消息队列获取这个事件然后去进行处理。当事件产生到达的时候,因为事件而驱动后续消费者程序的执行。这个过程叫做事件驱动。
比如一个新用户的注册,当用户注册时候,会构建一个新用户注册的消息,而该消息将会被发布到消息队列中的一个消费主题,各种消费者可以订阅这个新用户注册主题,根据主题里的消息,进行对应的操作。
6. 主要 MQ 产品比较
RabbitMQ 的主要特点是性能好,社区活跃,但是 RabbitMQ 用 Erlang 开发,对不熟悉 Erlang 的同学而言不便于二次开发和维护。(49M)
ActiveMQ 影响比较广泛,可以跨平台,使用 Java 开发,对 Java 比较友好。(27M)
RocketMQ 是阿里推出的一个开源产品,也是使用 Java 开发,性能比较好,可靠性也比较高。(35M)
Kafka ,LinkedIn 出品的,Scala 开发,专门针对分布式场景进行了优化,因此分布式的伸缩性会比较好。(63M)
负载均衡的原理
1. 什么是负载均衡
负载均衡建立在现有网络结构之上,它提供了一种廉价有效透明的方法扩展网络设备和服务器的带宽、增加吞吐量、加强网络数据处理能力、提高网络的灵活性和可用性。
负载均衡(Load Balance)其意思就是分摊到多个操作单元上进行执行,例如 Web 服务器、FTP 服务器、企业关键应用服务器和其它关键任务服务器等,从而共同完成工作任务。
2. HTTP 重定向负载均衡
实现起来比较简单
部署起来也很简单
效率低,每次请求实际上是两次请求
安全性低,容易暴露应用服务器的 IP
2. DNS 负载均衡
实现起来比较简单
部署起来也很简单
安全性低,容易暴露应用服务器的 IP
百度,淘宝等大型网站都使用了 DNS 负载均衡(两级负载均衡)
3. 反向代理负载均衡
通常应用在比较小型的系统中,比如背后的服务器只有 10 几台或者几十台的时候。成千上万台服务器的时候,就比较少的使用这种负载均衡。主要原因是反向代理负载均衡的效率比较低,性能比较差。因为一个完整的 HTTP 请求通常是由很多 TCP 的包组成,那么在一个 HTTP 请求到达的时候,反向代理服务器需要把 HTTP 协议包在应用层组合起来,恢复这个 HTTP 请求,再进行代理请求。同样当内部服务器反应 HTTP 请求之后,反向代理服务器需要做同样的事情。应用层的 HTTP 协议通常是比较重的,所以这样导致了效率不高。
4. IP 负载均衡
在 TCP/IP 层进行数据包的转发,不在应用层进行构建和转发。提高负载均衡的效率。网卡带宽可能成为响应的瓶颈。
5. 数据链路层负载均衡
通过修改 Mac 地址来实现负载均衡。虚拟 IP 地址技术。应用服务器直接响应给用户的目标地址,因为源 IP 地址没有改变。这种负载均衡方式是目前互联网应用里面最主要的负载均衡架构,也是最好最高效的一种。
6. 负载均衡算法
轮询:所有请求被依次分发到每个应用服务器上,适合于所有服务器硬件都相同的场景
加权轮询:根据应用服务器硬件性能的情况,在轮询的基础上,按照配置的权重将请求分发到每个服务器,高性能的服务器分配更多请求
随机:请求被随机分配到各个应用服务器,在许多场合下,这种方案都很简单实用,因为好的随机数本身就很均衡。如果应用服务器硬件配置不同,也可以很容易的使用加权随机算法
最少连接:记录每个应用服务器正在处理的连接数(请求数),将新到的请求分发到最少连接的服务器上,应该说,这是最符合负载均衡定义的算法
源地址散列:根据请求来源的 IP 地址进行 Hash 计算,得到应用服务器,该算法可以保证同一个来源的请求总在同一个服务器上处理,实现会话粘滞
评论