1 缓存为王

页表(page table)和内存管理单元(MMU)负责将虚拟地址转化为页的物理地址。 页表负责记录哪些是物理页,哪些是虚拟页,以及这些页的页表条目(PTE)。 MMU是一个物理硬件,负责进行虚拟地址到物理地址的翻译,翻译过程中需要从页表获取页的 PTE,MMU也会使用翻译后备缓存(TLB)的缓存页号。

系统性能指标:响应时间、延迟时间、吞吐量,并发用户数和资源利用率等

浏览器缓存

Cache-Control/Exires 优先级高于 Last-Modified/ETag,当本地副本根据 Cache-Control/Expires 发现还在有效期, 就不会再次发送请求服务器询问修改时间(Last-Modified)或实体标识(e-tag)了。

主流的缓存算法:

  • LRU(Least-Recently-Used): 替换掉最近请求最少的对象,实际中使用最广。cpu缓存淘汰和虚拟内存效果好,web应用欠佳
  • LFU(Least-Frequently-Used): 缓存污染问题(一个先前流行的缓存对象会在缓存中驻留很长时间)
  • LRU2
  • 2Q(two queues)。多级缓存
  • SIZE:替换占用空间最大的。也会有缓存污染
  • LRU-Threashold:不超过某一个size 的,其他与 LRU 相同
  • Log(Size) + LRU: 替换 size 最大的对象,当size相同按 LRU 替换
  • Hyper-G: LFU 改进版,同时考虑上次访问时间和对象 size
  • Pitkow/Recker: 替换最近最少使用对象,除非所有对象都是今天访问过的。如果是,替换调最大的对象
  • Lowest-Latency-First: 替换下载时间最少的,最小化平均延迟
  • Hybrid Hybrid: 保留效用最低的会被替换掉
  • Lowest Relative Value(LRV): 替换保留效用最低的
  • Adaptive Replacement Cache(ARC)
  • Most Recently Used(MRU)
  • First in First out(FIFO)
  • Random Cache

2 分布式系统理论

2.3 分布式系统理论

CAP:一致性、可用性、分区容忍性

Paxos: 解决分布式系统中,多个节点之间就某个值(提案)达成一致(决议)的通信协议。 《从Paxos到ZooKeper》 paper: paxos-make-simple.pdf github.com/papers-we-love

2PC: 2阶段提交协议。阻塞型协议,如果事务协调器宕机,某些参与者无法解决他们的事务。

3PC:增加了 preCommit

Raft:选举过程,然后在选举出来的领导人带领进行正常操作。

Lease机制:Lease 是由授权者授予分布式环境一段时间内的承诺

脑裂问题:双主脑裂。设置仲裁(第三方检测服务器monitor)

5 Memcached 集中式缓存

特性:

  • 协议简单:基本文本或者二进制协议
  • libevent 事件处理
  • 内存存储空间
  • 客户端分布式。客户端实现,服务端并不支持分布式

不足:

  • 无法备份(易失)。只能通过持久化解决
  • 不支持条件查询
  • 没有内置安全机制
  • 单点故障 failover。可以通过主从解决

内存管理: Slab Allocation。按照预先规定大小,将分配的内存分割成各种尺寸的块(chunk),并把尺寸相同的块分成组(chunk集合), 分配的块可以重复利用,不释放到内存

  • Page: 分配给 Slab 的内存空间,默认 1 MB
  • Chunk: 用于缓存记录的内存空间
  • Slab class: 特定大小的 chunk 组

典型问题: - 容量问题 - 服务高可用(HA) - 扩展问题

两种过期机制:

  • lazy expiration: get时候查看是否过期,不直接监视
  • LRU

哈希算法:任意长度的输入,通过散列算法,变换成固定长度的输出。 使用一致性哈希很好解决动态环境下的使用 Memcached 扩缩容带来的大量数据失效的问题。

热点问题: 如果是超高访问的热点数据 - 通用解决思路就是 client 端做本地的 LocalCache(放到进程缓存里),省去了缓存服务器IO。 - 通过多个 key_index 分散到多个机器上,减少针对一个server 的超高访问。

缓存与数据库的更新问题: 更新数据库成功,删除cache失败。别把缓存当存储,分布式架构一切都可能fail。

命名空间:通过前缀等来模拟

CAS(Check and Set): 解决原子操作问题。比如用户操作一个key对应的value,需要保证操作当中,value不允许被其他访问操作, 如果被操作过,则操作失败。 首先用 gets 指令获取 key-value 及 key 对应 value 的版本号version,然后操作产生新的value;最后使用 CAS 指令重新提交key-value,并附带之前的版本号version。服务端判断CAS操作中的version不是最新,认为key被改,本次CAS操作失败。

客户端:协议封装,连接池,sharding,故障转移,序列化

周边工具: - The innoDB memcached Plugin - Twemcache - Twemproxy: 快速的单线程代理程序 - MemcacheDB/MemcacheQ/Mcrouter - Tokyo Cabinet: DBM 数据库

6 Memcached 周边

可扩展、高可用、可维护性

Twemcache

Twemproxy

单线程代理程序,支持memcached ASCII 协议和Redis 协议。c语言编写,通过引入一个代理层,将应用程序后端多个redis或者membcached 实例进行统一管理。应用程序只需要在Wwemproxy上进行操作即可。

  • 支持失败节点自动删除
  • 支持设置Hash Tag
  • 保持与缓存服务器长连接,减少直接连接数
  • 支持多种hash 算法
  • 支持状态监控
  • 连接复用和内存复用

LVS(keepalived,双节点热备保证LVS高可用) -> Twemproxy -> cache(缓存自身高可用)

7 Redis 探秘

数据类型

Redis(REmote DIictionary Server): string/list/set/hash/sorted set

String: 能表达字节串、整数、浮点数。redis 自动识别精度、值域范围。实现为 int/sds

List :存储string序列。内部实现为 linkedlist/ziplist。 linkedlist 双链表实现。 ziplist 列表结构,连续内存。移动复杂度较高

Hash: 不能嵌套。智能是string 能表达的内容:整型、浮点型、字节串。实现 hashtable/ziplist

Set: intset(只包含整形元素时)/hashtable。intset 使用二分查找(logn)

Sorted Set: ziplist/ skiplist+hashtable skip查找O(n)

客户端

交互协议:

  • 网络模型(TCP)
  • 序列化协议: inline command/simple string/bulk string/error/integer/array

请求响应模式:

  • 串行化
  • pipeline实现

事务模式:原子化。不同事务的命令之间不会交叉。一次执行一个队列里所有请求,不会执行其他客户端命令。 非一致性:如果EXEC 中有一条执行请求失败,后续继续执行。只在返回客户端的array型 响应中标记这一条出错结果。 事务只读:每个请求的参数取值不能依赖上一个结果 乐观锁的可串行化事务隔离:通过watch机制实现乐观锁解决一致性问题。

为了实现可串行化隔离级别:Redis 使用者需要做到三点约束

  • 事务只读操作需要先于写操作(批量操作中执行度操作没意义)
  • 所有写操作的执行不依赖其他写操作的执行结果
  • 乐观锁避免一致性问题,对相同key 的并发访问频繁时候事务成功率低

脚本交互模式:

  • 每个提交给服务器的端的 lua_script_string 都会在服务端的 lua_script_map 中常驻,除非显示 FLUSH 命令清理
  • script实例的主备间可通过script重放和cmd重放两种方式实现复制
  • 之前执行过的script后续可以直接通过它的sha指定而不用再向服务器发送一边script内容

持久化:

  • 全量: SAVE/BGSAVE
  • 增量: AOF

分布式 Redis

单实例节点:数据量伸缩、访问量伸缩、单点故障

分布式方案:

  • 水平拆分:每个分组处理业务数据的一个子集。原则上没交集
  • 主备复制: W+R>N
  • 故障转移:需要保持多副本,位于不同节点上

水平拆分(sharding)

常用映射:

  • hash
  • 范围映射。key值域不确定、不可控
  • hash 范围结合

请求路由

  • 只读的跨实例请求
  • 跨实例原子读写

主备复制

保证单点间数据一致性。有些是客户端双写,有些是存储层复制。 redis master slave. PSYNC 支持断点续传

故障转移(failover)

master故障时,slave可以成为新的 master,对外提供读写服务,这种机制称为 failover。 一种方式是使用daemon 监视redis多节点,但是没法保证daemon本身带点可用性没法保证。 如果引入多个 daemon虽然解决了其单点问题,但是不同daemon观察到master 可用状态不一样,如何决策此时master是否需要failover? redis用多个 daemon 组成一个 集群称为 sentinel 集群,这些节点之间互相通信、选举、协商,在master故障发现、failover决策上表现出一致性。

过程:

sentinel 节点通过定期向maser 发送心跳包判断其存活状态,成为PING。 如果一个sentinel一旦发现master 没有正确响应,将master设置为『主观不可用』,随后将『主观不可用』发送给其他所有的 sentinel节点确认,如果确认的sentinel节点数>=quorum(可配置),判断master为『客户端不可用』,随后进入failover流程。

进入failover后,最后只能有一个sentinel 节点作为failover 发起者,此时需要一个leader选举过程,选择谁发起failover。 redis sentinel 机制采用类似 Raft 协议实现选举算法。

Redis Cluster

集群拓扑结构

slotId = crc16(key) % 16384 (2**14)

9 Tair 探秘

TaoBao Pair。

一个Tair 集群主要包括 Client, Config Server 和 DataServer

10 EVCache 探秘

分布式缓存,基于 Memcached 内存存储和Spymem-cached 客户端实现

Ephemeral: 数据存储是短暂的,有自己的存活时间 Volatile: 数据可以在任何时候消失 Cache: 一个内存型键值对存储系统

12 社交场景架构进化:从数据库到缓存

社交业务示例

post, follow, timeline

16 新的旅程

缓存使用模式

  • Cache-Aside: 业务代码中管理缓存。需要利用数据库比较成熟的高可用机制
  • Cache-As-SoR :SOR 是记录系统,就是实际存储原始数据的系统。把 cache 当做 sor,业务代码只对 cache 操作
  • Read-Through: 业务代码获取数据先从缓存获取,没有再从数据库加载,然后放入缓存
  • Refresh-Ahead: 仅调用 cache 的 get 操作,通过设定数据的过期时间在数据过期时,自动从数据库重新加载数据的模式。
  • Write-Through: 穿透写模式,业务代码首先调用 cache 写,实际由 cache 更新缓存数据和存储数据
  • Write-Behind: 业务代码只更新到缓存数据,什么时候更新到数据库,由缓存到数据库同步策略决定

管理缓存

缓存穿透

查询不存在的 key 导致大量回源。

  • 预先设定一个值,发怒 i 的时候应用可以判断出来并且决定是否回源
  • key 不存在,加锁,回源,存入缓存,解锁
缓存失效

大量缓存同时过期

  • 失效时间随机值,避免同时失效
淘汰算法

LRU, LFU, FIFO

  • LRU 周期性批量操作导致命中率急剧下降,缓存污染比较严重。LRU-k,访问次数达到 k 次的时候才将数据放入缓存
缓存可用性
  • 主备:一致性问题
  • cluster 方案
数据一致性
  • 最终一致性。对于时间敏感数据可以设置很短的过期时间(失效时间)
  • 强一致性:InnoDB memcached 插件结合 mysql 结局缓存和数据库一致性问题
热点数据处理
  • 数据预热。
    • 是否有监控机制确保预热数据都写入成功
    • 数据预热配备回滚方案
    • 评估数据量
    • 注意是否会引发数据库批量操作和慢查询问题
  • 非预期热点:nginx+lua 应用内缓存(热点发现系统)
  • 多级缓存:localCache
  • 数据复制模式:通过多个 key_index 解决热点读问题。所有热点 key 发布到所有的 web 服务器
注意事项
  • 慎用缓存当存储
  • 就近原则:cpu 缓存、客户端缓存、cdn 缓存
  • 并发控制手段:乐观锁、Latch、mutex、CAS 等。