Memcache

memecache是facebook的一个缓存中间件,称为后来系统设计缓存的一个参考。

facebook的建构演变

单体应用

最初用户量不大的时候使用单体应用架构。 数据库使用MySQL, 使用Apache做web服务器,用php生成html。

image-20221109114811282

增加前端服务器

随着用户增加,首先生成html的php服务器出现瓶颈。

于是需要增前端FE服务器,这些服务器共享一个MySQL数据库。

image-20221109115100601

数据库分片

用户进一步增加,单个MySQL数据库也撑不住了。

这时候就需要对MySQL数据库进行分片(分表)。

这是就有了新要求:

  • 前端网页服务器需要了解分片规则,同时要和多个数据库进行通信。

  • 由于分片(分表),无法依赖单个数据库的事务,需要执行多个数据库之间的分布式事务,而分布式事务的执行本身更费时间。

image-20221109115441816

引入缓存

若用户数再进一步增长,对于分片数据库来说,虽然可以进一步增加分片,但:

  • 对于热点数据,可能就是同一个数据,无论分片(分表)的粒度多么细,但是热点数据还是位于同个分片内,这时候增加分片并没有太大的帮助。
  • 增加分片需要增加数据库,数据库本身执行效率较慢,增加分片代价高。

此时可以选择引入缓存,由于缓存的数据存在RAM中,执行效率远高于db,这种方案就更具性价比。

image-20221109120032739

缓存主要有两种模式:

  • look-aside cache: 先去缓存里查数据,缓存未命中的话就需要去数据库查,然后把数据写回数据库。
  • look-through cache:缓存就称为数据库的代理了。

Facebook的memcache就属于look-aside cache。

Facebook在使用memcache时主要关注:

  • 性能问题:facebook想要引入带来更好的新年,在使用缓存后,能带来远大于数据库本身能承受的数据处理能力,大量的热点读请求都可以只打在缓存上。但是要是缓存出现了某些问题,导致大量的请求在一瞬间达到数据库,数据库会承受不住。
  • 缓存一致性问题:memcache和数据库里的数据一致性问题。

性能Performence

分区+复制

Partition:

​ + Ram efficient:相同的内存能存更多的数据

​ - not good for hot keys: 对缓解热点数据帮助不大(有可能热点就是同一个数据,不管怎么分,都在一个分区)

​ - clients talk to many part: 客户端需要和多个分区通信,可能会建立很多的TCP连接。

Replication:

​ + good if hot keys: 对缓解热点数据有帮助

​ + few tcp connections:客户端只和一个复制通信,不会有太多的tcp连接

​ - less total data:相同的内存存的数据少

介于分区和复制各有优缺点,facebook将两者联合起来使用。

image-20221109152051206

facebook在东西海岸有两个不同的reigon。西海岸的reigon作为主数据中心,所有的写请求都达到东海岸,并通过mysql支持的异步复制,在数秒内就可以复制到西海岸的reigon。

image-20221110120220331

在每个reigon内,有一组DB集群。

然后有若干小集群,每个小集群由多个前端服务器,分片的memcache服务器组成。这个小集群不可以太大,防止FE和MC之间有太多的TCP连接,而且可以做到集群内的网络快速通讯。这些小集群共享DB集群。

对于一些不太热门的数据,请求量少,如果在存在每个小集群内,可能会浪费资源。所以facebook引入一组regional pool,即region内所有小集群共享的memcache,将较冷的数据存放在其中,而集群的memcache则主要处理热门数据。

image-20221109172518364

facebook通过上述方式将分区和复制结合起来使用。

冷启动

如果一个小集群刚启动,其上面的memcache中并没有数据,缓存命中率极低。这时候如果将流量引到新启动的这个小集群内,将会有大量的请求打在数据库上。

为了解决这个问题,引入cold start模式,在冷启动模式下,如果新启动的小集群内缓存并未命中,则会向其他集群的memcache请求数据,缓解数据库的压力。

缓存惊群效应 THUNDRING HERD (缓存击穿)

image-20221111134624460

缓存中一个key失效,但是此时多个客户端同时请求该key,由于未命中,所以多个客户端会同时把请求打到db上。

这种行为非常浪费,其实只需要一个客户端去向db请求数据,其他客户端等待该客户端把读取的数据写会缓存即可。

image-20221111134851019

facebook引入LEASE解决:

  • 客户端向memcache请求未命中key,memcache会生成一个LEASE(很大的一串数字),并对这个key进行LEASE标记。
  • 其他客户端请求该key时,看到LEASE标记,就会被通知等待一段时间后在解决。
  • 拿到LEASE的客户端向db请求完数据后,会携带LEASE把数据写回memcache并且释放LEASE标记,只有LEASE正确才可以写回。
  • 如果拿到LEASE的客户端超时,memcache也会释放LEASE。

缓存雪崩

如果负责某一段key分片的memcache挂了,这时候这些分片的请求将会打到数据库,给db带来压力。而手动重启或替换一个memcache需要大量时间。

为了应对缓存雪崩,facebook引入gutter server(一小组的备用服务器),这些gutter服务器会临时接管挂掉的memcache,可以一定程度缓解数据库压力。

image-20221109175258931

一致性 consistency

一致性要求

对于facebook来说:

  • 无需在意毫秒级别的一致性,但是数据不能过时太久
  • 用户刚刚更新了数据,要让他能看到自己的更新

失效策略

image-20221109152902707

再向数据库写入数据后,让memcache中的缓存失效。

image-20221109153441670

需要保证写入数据库以后再删除,在这中间有人去读数据库,然后写入缓存,就会导致缓存里有旧的数据。

为何使用失效策略,而不是更新策略?(写入操作之间的并发)

image-20221109154129941

并发执行时, Set(x, 2)比Set(x,1)先执行了, 导致缓存写入旧的数据。

读和写操作的并发

image-20221109180145757

在这个例子中,C1在数据库中读取了指定key的值,随后C2更新这个key所对应的值

但是,c1将值写回数据库操作慢于c2使得缓存失效的操作,c1向缓存中写入了一个过时的数据。

facebook也是通过LEASE来解决。

image-20221109180857189

  • 客户端向缓存的GET请求也会获得一个LEASE,再读完db写回数据时会携带该LEASE
  • 客户端更新完数据,除了会使得缓冲中该key失效,也会使得LEASE失效
  • memcahe会拒绝失效的LEASE的写入请求。

这样即使c1的set操作晚到,也会因为LEASE失效而无法更新。

Q.E.D.