Skip to content

Memcached 存储架构

Memcached采用纯内存的存储架构,所有数据都存储在内存中,不涉及磁盘IO操作,因此具有极高的读写性能。其存储架构设计的核心目标是高效利用内存资源,提供快速的数据访问能力。

内存存储模型

1. 数据存储方式

  • 键值对存储:Memcached以键值对(Key-Value)的形式存储数据
  • 纯内存存储:所有数据都存储在内存中,没有持久化到磁盘
  • 无数据结构:只支持简单的键值对,不支持复杂数据结构
  • 数据大小限制:单个键值对的大小限制为1MB

2. 内存分配策略

  • 预分配内存:Memcached启动时预分配固定大小的内存
  • Slab分配:使用Slab分配器管理内存,避免内存碎片
  • Chunk复用:释放的数据块会被重新利用,提高内存利用率
  • LRU淘汰:当内存不足时,使用LRU算法淘汰最近最少使用的数据

Slab分配器

1. Slab分配器原理

Slab分配器是Memcached的核心内存管理机制,它将内存划分为不同大小的Slab类,每个Slab类包含固定大小的Chunk,用于存储不同大小的数据。

  • Slab类:具有相同Chunk大小的内存块集合
  • Chunk:最小的内存分配单元,用于存储单个键值对
  • Slab页面:每个Slab类由多个Slab页面组成,默认大小为1MB
  • 增长因子:控制不同Slab类之间Chunk大小的增长比例,默认为1.25

2. Slab分配器工作流程

  1. 内存预分配:Memcached启动时,根据配置的内存大小预分配内存
  2. Slab类初始化:根据增长因子创建一系列Slab类,每个Slab类的Chunk大小不同
  3. 数据存储:当存储数据时,根据数据大小选择合适的Slab类
  4. Chunk分配:从选定的Slab类中分配一个空闲Chunk存储数据
  5. Chunk回收:当数据过期或被删除时,Chunk被标记为空闲,可重新使用
  6. 内存扩展:当某个Slab类的空闲Chunk不足时,分配新的Slab页面

3. Slab分配器配置

  • 增长因子:通过-f参数配置,默认为1.25
  • 初始Chunk大小:通过-n参数配置,默认为48字节
  • Slab页面大小:固定为1MB,不可配置

4. Slab分配器优缺点

优点

  • 避免内存碎片:每个Chunk大小固定,不会产生内存碎片
  • 快速内存分配:直接从对应Slab类中分配Chunk,无需复杂的内存管理
  • 高效内存复用:释放的Chunk可以被同一Slab类的其他数据使用

缺点

  • 内存浪费:如果数据大小与Chunk大小不匹配,会导致内存浪费
  • Slab类间内存不可共享:不同Slab类的内存不能互相借用,可能导致部分Slab类内存不足,而其他Slab类内存闲置

数据存储结构

1. 键值对结构

Memcached中的每个键值对由以下部分组成:

  • 键(Key)

    • 最大长度:250字节
    • 不允许包含空格和换行符
    • 区分大小写
  • 值(Value)

    • 最大大小:1MB
    • 可以是任意二进制数据
    • 需要客户端进行序列化和反序列化
  • 过期时间(Expiration)

    • 可以设置为相对时间(秒)或绝对时间(Unix时间戳)
    • 0表示永不过期
    • 超过30天的相对时间会被视为绝对时间
  • 标志位(Flags)

    • 32位无符号整数
    • 用于存储客户端自定义信息,如数据类型、压缩标志等
  • CAS值(CAS Token)

    • 64位无符号整数
    • 用于实现乐观并发控制
    • 每次数据修改时自动递增

2. 内存中的存储布局

在内存中,键值对数据的存储布局如下:

  • 头信息:包含过期时间、标志位、CAS值等元数据
  • 键数据:存储实际的键字符串
  • 值数据:存储实际的值数据

3. 哈希表结构

Memcached使用哈希表来存储键值对的索引,便于快速查找:

  • 主哈希表:存储键的哈希值到对应Slab中Chunk位置的映射
  • 哈希桶:主哈希表由多个哈希桶组成
  • 链表结构:每个哈希桶中存储多个键值对的指针,形成链表
  • 哈希冲突:通过链表解决哈希冲突

数据过期与淘汰机制

1. 过期机制

Memcached采用两种方式处理过期数据:

惰性删除(Lazy Expiration)

  • 触发时机:当访问数据时检查是否过期
  • 处理方式:如果数据已过期,删除该数据并返回NOT_FOUND
  • 优点:不需要额外的过期检查线程,节省CPU资源
  • 缺点:过期数据可能会占用内存一段时间

主动清理(Active Expiration)

  • 触发时机:定期运行过期清理线程
  • 处理方式:随机检查部分数据,如果已过期则删除
  • 检查频率:默认每60秒运行一次
  • 检查比例:每次检查100个数据项,或5%的总数据项(取较小值)

2. 淘汰机制

当内存不足时,Memcached使用LRU(最近最少使用)算法淘汰数据:

  • 淘汰触发条件:当需要分配新的Chunk,但对应Slab类没有空闲Chunk,且无法分配新的Slab页面时
  • 淘汰范围:只在当前Slab类中进行淘汰
  • 淘汰策略:选择该Slab类中最近最少使用的数据进行淘汰
  • LRU实现:每个Slab类维护一个LRU链表,记录数据的访问顺序

3. 淘汰策略配置

  • maxmemory:通过-m参数配置Memcached可以使用的最大内存
  • maxmemory-policy:Memcached不直接支持该参数,但其LRU淘汰策略相当于allkeys-lru

存储性能优化

1. 内存使用优化

  • 合理设置增长因子:根据数据大小分布调整增长因子,减少内存浪费
  • 避免存储大对象:将大对象分割成多个小对象,或考虑使用其他存储系统
  • 设置合理的过期时间:避免数据长期占用内存
  • 监控内存使用率:及时调整内存大小,避免频繁的LRU淘汰

2. 键设计优化

  • 使用短键:减少键占用的内存空间
  • 使用命名空间:便于管理和批量删除
  • 避免热点键:热点键会导致单个Slab类内存不足
  • 均匀分布键:避免哈希冲突,提高查找效率

3. 数据序列化优化

  • 选择高效的序列化方式:如MessagePack、Protocol Buffers等,比JSON更高效
  • 压缩大体积数据:对超过一定大小的数据进行压缩,减少内存占用和网络传输量
  • 避免过度序列化:对于简单数据,可以直接存储为字符串

4. 访问模式优化

  • 批量操作:使用批量GET命令减少网络往返次数
  • 减少过期数据访问:避免频繁访问已过期的数据
  • 合理使用CAS:只在需要并发控制时使用CAS,避免额外开销

存储架构监控

1. 内存使用监控

  • total_items:存储的总数据项数量
  • bytes:已使用的内存字节数
  • curr_items:当前存储的数据项数量
  • evictions:因内存不足而淘汰的数据项数量
  • reclaimed:通过惰性删除回收的内存字节数

2. Slab分配监控

  • slab_stats:各个Slab类的详细统计信息,包括:
    • chunk_size:Chunk大小
    • chunks_per_page:每个Slab页面包含的Chunk数量
    • total_pages:已分配的Slab页面数量
    • total_chunks:总Chunk数量
    • used_chunks:已使用的Chunk数量
    • free_chunks:空闲的Chunk数量
    • free_chunks_end:Slab页面末尾的空闲Chunk数量
    • mem_requested:实际存储数据使用的内存字节数
    • get_hits:该Slab类的GET命中次数
    • cmd_set:该Slab类的SET命令次数
    • delete_hits:该Slab类的DELETE命中次数
    • incr_hits:该Slab类的INCR命中次数
    • decr_hits:该Slab类的DECR命中次数
    • cas_hits:该Slab类的CAS命中次数
    • cas_badval:该Slab类的CAS值不匹配次数

3. 监控工具

  • memcached-tool:Memcached自带的监控工具,用于查看Slab分配情况
  • stats命令:通过客户端发送stats命令获取Memcached的统计信息
  • Prometheus + Grafana:用于全面监控和可视化Memcached的性能指标
  • Zabbix:用于系统级监控和告警

存储架构演进

1. 早期版本(1.0-1.2)

  • 基本的Slab分配器实现
  • 支持基本的键值对存储
  • 简单的LRU淘汰机制

2. 稳定版本(1.4)

  • 改进的Slab分配器,减少内存浪费
  • 优化的LRU算法,提高淘汰效率
  • 增加了更多的统计信息
  • 支持CAS操作

3. 现代版本(1.5-1.6)

  • 重写的Slab分配器,提高性能和可维护性
  • 改进的内存管理,减少内存碎片
  • 支持TLS加密
  • 优化的过期清理机制

常见问题(FAQ)

Q1: 为什么Memcached的内存使用率总是达不到100%?

A1: Memcached的内存使用率通常不会达到100%,主要原因包括:

  • Slab分配器的内存浪费:数据大小与Chunk大小不匹配导致的内存浪费
  • Slab类间内存不可共享:不同Slab类的内存不能互相借用
  • 预留内存:Memcached会预留一些内存用于内部操作

Q2: 如何解决Memcached的内存碎片问题?

A2: Memcached使用Slab分配器从根本上避免了内存碎片问题。每个Slab类的Chunk大小固定,分配和释放都不会产生内存碎片。如果发现内存使用率不高但频繁出现evictions,可以考虑调整增长因子或重新设计键值对大小。

Q3: 如何选择合适的增长因子?

A3: 选择增长因子时应考虑数据大小的分布:

  • 较小的增长因子(如1.1):适合数据大小分布均匀的场景,内存浪费少,但Slab类数量多
  • 较大的增长因子(如1.5):适合数据大小差异较大的场景,Slab类数量少,但内存浪费可能较多
  • 建议:根据实际数据大小分布进行测试,选择最优的增长因子

Q4: Memcached支持持久化吗?

A4: Memcached本身不支持数据持久化,所有数据都存储在内存中。如果需要持久化功能,可以考虑:

  • 使用第三方工具,如memcachedb、membase等
  • 在应用层实现数据持久化,如定期将数据备份到磁盘
  • 考虑使用Redis,它支持多种持久化方式

Q5: 如何处理Memcached中的大对象?

A5: 处理大对象的方法包括:

  • 分割大对象:将大对象分割成多个小对象,分别存储
  • 压缩数据:对大对象进行压缩,减少内存占用
  • 使用其他存储系统:对于超过1MB的对象,考虑使用对象存储服务或数据库
  • 优化数据结构:重新设计数据结构,减少数据大小

Q6: 如何监控Memcached的Slab分配情况?

A6: 可以使用以下方法监控Slab分配情况:

  • 使用memcached-tool命令:memcached-tool <host>:<port> display
  • 通过客户端发送stats slabs命令
  • 使用监控工具,如Prometheus + Grafana,配置相关指标的监控和告警

Q7: 什么是Memcached的CAS操作?

A7: CAS(Check-and-Set)是Memcached支持的一种乐观并发控制机制。它通过为每个数据项分配一个唯一的CAS值,实现多个客户端对同一数据项的安全修改。当使用CAS命令修改数据时,只有当提供的CAS值与服务器上的CAS值匹配时,修改才会成功。

Q8: 如何提高Memcached的内存利用率?

A8: 提高内存利用率的方法包括:

  • 合理设置增长因子,减少内存浪费
  • 优化键值对大小,使其与Chunk大小匹配
  • 设置合理的过期时间,及时释放不再使用的内存
  • 避免存储过大的数据对象
  • 监控Slab分配情况,及时调整配置

Q9: Memcached的LRU淘汰机制是如何实现的?

A9: Memcached的LRU淘汰机制是通过每个Slab类维护的LRU链表实现的。当数据被访问时,会被移到链表的头部;当需要淘汰数据时,会从链表的尾部选择最近最少使用的数据进行淘汰。

Q10: 如何处理Memcached中的热点数据?

A10: 处理热点数据的方法包括:

  • 将热点数据分布到多个节点,避免单个节点过载
  • 对热点数据设置较长的过期时间或永不过期
  • 使用本地缓存作为Memcached的补充,减少对Memcached的访问压力
  • 优化应用程序逻辑,减少对热点数据的访问频率