经典分布式论文阅读:Memcache

2019-09-18   IT技术分享

本文是Memcached论文的阅读笔记,本文主要描述了如何使用 memcached 构建一个分布式键值存储系统。

一个社交网络的基础设施应该满足以下特性:

  1. 允许几乎实时的通讯;
  2. 从多个源头快速聚合内容;
  3. 能够访问和更新热门的共享内容;
  4. 每秒能够处理百万用户请求。

本文以 memcached 为基础构建分布式键值存储系统,在特定的规模之下,某些特性需要更多的努力去满足。

总览

以下两个特性影响了系统的设计:

  1. 用户消费的内容比生产的内容多得多;
  2. 一个读取操作需要从多个源头获取数据。=-

下文使用 memcached 指代源代码或者运行中二进制文件,使用 memcached 指代分布式系统。

查询缓存: memcache 作为demand-filled look-aside缓存来使用,当某个Web请求数据的时候,首先尝试从缓存中获取,如果缓存中不存在,那么从后端获取数据并写回缓存,如果更新数据的话,需要将旧的数据从缓存中删除。

通用缓存: memcache 也可以作为通用的键值存储来使用,例如保存机器学习计算结果等等。

本文描述了不同尺度下的重点,但是始终强调两个设计目标:

  1. 任何改进必须解决用户或者运维问题;
  2. 将读取到短暂的过时数据的可能性作为可以调整的参数。

集群尺度:延迟和负载

当 memcache 运行在集群尺度上的时候,主要考虑的问题是减少解析缓存数据延迟和缓存缺失带来的负载。

降低延迟

memcache 使用一致哈希将存储对象分散到各个服务器上,因此全部Web服务器短时间内会连接所有的服务器。降低延迟的工作主要在客户端完成序列化、压缩、请求路由、错误处理和批量请求等工作

并行请求和批量:可以使用有向无环图分析数据之间的依赖关系,使得一次请求获取尽可能多的键值。

客户端-服务器通讯:服务器之间不进行互相通信,由客户端处理系统复杂性。客户端的逻辑由两部分组成:

mcrouter

get 请求使用UDP协议来降低延迟和负载,如果数据包丢失或者乱序将被视为错误。Web服务器将客户端错误视为缓存缺失,但是从数据源获取数据不做写回。 set 和 delete 操作通过TCP协议进行。每个线程创建一个连接非常浪费资源,运行在同一个机器上 mcrouter 将这些连接进行合并,提高网络利用率。

请求拥塞: memcache 使用流控制机制来限制请求拥塞,客户端使用滑动窗口来控制请求数量,窗口随着成功请求数据而扩大,随着未答复请求而减小。

过小的窗口会导致不必要的等待,而过大的窗口会导致请求阻塞,需要找到平衡点。

降低负载

租期:系统使用了租期机制来解决 过时数据惊群效应 。并发更新乱序之后会产生过时的数据。频繁的写入操作会频繁地使得缓存失效,大量的读取操作会同时转向请求数据源。

当客户端请求的缓存缺失的时候,服务器分配一个和键绑定的租期,在写回数据的时候需要验证租期合法性。如果验证之前服务器收到过删除请求,那么租期就失效了。

惊群效应可以限制租期产生速率解决,例如10秒钟一个,如果其他客户端在这个10秒之前请求对应的键,那么让它等待一小会儿时间,当客户端再次尝试的时候,数据一般已经在缓存之中。

某些场景下过时数据是可以接受的,当数据被删除后被转移到一个保存最近删除数据的数据结构中,存活一小段时间,一个获取请求返回租期或者过时的数据。

Memcache池:不同的应用程序使用同一个 memcache 会相互干涉产生负作用,因此可以考虑将 memcached 服务器分散到不同的池中,将不同读写特性的数据存入不同的池中。

在池中多副本:如果满足以下特性,那么可以在池中建立多副本:

memcached

如果使用了多副本,那么在使缓存失效时要对所有副本进行。

故障处理

我们必须解决两种规模下的故障:

  1. 因为网络或者服务器错误导致少量主机不可达;
  2. 影响集群中大部分服务器的大规模宕机。

对于小规模的宕机,系统设置了少量称为Gutter的机器来暂时管理不可用机器的键范围,Gutter中的数据有效期很短,这样避免了需要使缓存失效的操作。

区域尺度:副本

随着负载规模增大,简单地扩充机器是不够的。作者将Web服务器和 memcached 服务器分为多个前端集群,和一个存储集群组成一个 区域

区域失效

存储集群保存着最新数据,用户请求将数据副本写入到前端服务器。存储集群负责发送缓存失效命令,来保持数据的一致性。对数据产生修改的SQL会附上需要失效的 mamcache 键,应用更改之后由 mcsqueal 将缓存失效消息发送给各个前端集群中的服务器。

降低数据包率:如果直接将失效缓存消息会浪费网络资源,因此需要将缓存失效消息批量发送给每个前端集群中的 mcrouter ,再由 mcrouter 转发给集群内的服务器。

通过Web服务器失效:之所以不让Web服务器直接发送缓存失效消息是因为:

  • 通过Web服务器进行批量处理比较低效
  • 在消息路由错误时候方便补救

区域池

集群独立地缓存数据,一个数据可能在多个集群上有副本,可以通过让两个集群共享 memcached 服务器,这种形式称为区域池。可以根据访问速率、数据集大小和访问用户数量来决定是否将数据存入区域池。

集群冷启动

如果上线一个新的集群,那么刚开始负载都会指向数据库。作者通过 冷启动预热 机制解决,允许Web服务器从其他集群获取数据并写回。

当然这样会带来不一致的问题,例如当一个客户端更新了数据,同时另外一个客户端刚好转向其他集群获取数据,刚好缓存失效消息还没有到达。作者采用删除拖延解决这个问题,在收到删除消息两秒之内禁止写回。当客户端写回被拒绝后,说明数据已经被更新,需要从数据库获取。

跨区域:一致性

在多个地理位置部署数据中心有以下好处:

  1. 将Web服务器放在距离用户近的位置可以减少延迟
  2. 地理分散可以缓解自然灾害和断电事故
  3. 新的地点可能会提供更廉价的电力以及其他经济效益

跨区域方式设计成一个区域管理主数据库,其他区域保存只读副本。这样带来的一个问题就是:副本数据和主数据库之间存在一些延迟。系统在强调性能和可用的同时,提供尽最大力的最终一致性。

写入主区:使用 mcsqueal 传输失效消息可以避免失效信息先于数据更新到达。

写入非主区:如果一个非主区域的用户更新了数据,那么如果它接着从本地区域读取数据可能看不到更改。这种情况下要求必须在本地数据库赶上之后才能重来填充缓存,作者使用了 远程标记 来解决这个问题。

当Web服务器更新键值k时:

  1. 在区域建立一个远程标记
  2. 在主区域进行写操作,并指示使k和 失效
  3. 删除本地集群上的k

这样当k缺失的时候,如果客户端看到 ,那么就去从主区域获取数据。

运维考虑:跨区域通信代价高,因此删除消息和副本同步共用一个信道。

单机优化

性能优化: memcached 主要的性能优化为:

  1. 允许哈希表自动展开,避免查询时间退化到
  2. 让多线程服务器使用全局锁来保护多种数据结构
  3. 分配给线程独立的UDP端口来避免数据传输竞争

自适应的slab分配器:

memcached 使用slab分配器管理内存,将内存空间块组织成slab类,为数据分配内存的时候会寻找符合其大小最小的块,如果内存块不足则继续申请。当机器内存不足以分配新的内存块是,淘汰类中的LRU内存块。

作者优化了slab分配器来调整slab中内存块数量来适应负载。如果某个类正在淘汰内存块并且下一个要被淘汰的数据比每一类平均要淘汰的数据新20%,那么说明这个类需要更多的内存块。从当前保存全局LRU的slab中回收内存分配给这个slab。

短周期数据缓存:短时间活动产生的短命键会浪费内存,作者使用混合策略来解决这个问题:对大多数键进行惰性回收,对短命键进行主动回收。把短命键数据放在一个按照过期时间排序的链表里面,每秒钟回收头部的短命键。

软件更新:为了在软件更新过程尽可能减少中断时间,作者修改了 memcache ,将缓存和数据结构保存在System V共享内存区域中,避免在更新时丢失数据。

end:如果你觉得本文对你有帮助的话,记得关注点赞转发,你的支持就是我更新动力。