第5章 性能与效率
服务器指南:性能与效率
我们对 memcached 的速度有很高期待。偶尔,用户测试时可能会看到与预期不符的结果。以下是开发者用户对 memcached 性能表现的主要预期。如果你发现情况不同,通常是某一层配置出了问题,建议进行故障排查,或在社区寻求帮助以获得调优得当的配置。
总体性能
处理极高负载 在配备高速网络的大型机上,memcached 轻松应对每秒 20 万次以上的请求。经过深度调优或使用更快的硬件,性能还能提升数倍。即使在较慢的机器上,每秒几百次请求通常也不成问题。
不应挂起 memcached 的操作几乎全是 $O(1)$ 复杂度。连接它并执行 get 或 stat 命令不应产生延迟。如果连接时出现延迟,可能触碰了最大连接数限制。
如果执行命令时出现延迟,可能存在多种调优问题。最常见的是硬件故障、内存不足(导致交换)或网络问题(带宽、丢包、半双工连接)。罕见情况下,操作系统或 memcached 本身的 Bug 也可能有影响。
操作系统问题 / 防火墙 通常无需在 memcached 服务器前运行有状态防火墙系统。除非默认配置如此,或需要保护 memcached 免受互联网攻击。
这些系统对特定时间内的连接数及连接或包的处理速度有限制。许多连接性问题最终都追溯到防火墙配置。
响应迅速 在正常状态下,memcached 能在 1 毫秒内处理请求。即使考虑操作系统抖动、CPU 调度或网络抖动导致的离群值,绝大多数命令也不应耗时超过一两毫秒。
过期时间应准确 memcached 会尝试在你指定时过期项目。为避免频繁访问时钟,它每秒更新一次内部时钟值。这意味着过期时间的精度可达 1 秒。因此,过期可能提前或滞后 1 秒,但不应超过此范围。
由于过期值以时间戳存储,请确保操作系统时钟准确。如果时钟设定在未来,而你向 memcached 存储项目后突然将时钟大幅调回过去,那些项目将不会按时过期。反之,时钟大幅超前会导致项目提前过期。
set 失败的处理方式 在某些条件下,memcached 会“失败” set 操作,例如无法找到空闲内存、对象过大或未知因素。在大多数针对已存在项目的 set 操作中,失败会导致旧项目从缓存中移除。
memcached 假设,即使你执行 set 时项目无效,这也意味着你意图更新缓存,且其中的内容已过时。它选择谨慎处理,替你移除旧项目。
理论限制
最大客户端数 由于 memcached 采用基于事件的建筑,大量客户端通常不会拖慢其速度。用户连接数十万客户端并正常工作的案例比比皆是。
但有几个硬性限制: * 每个连接的客户端使用部分 TCP 内存。可连接的客户端数量受空闲 RAM 限制。 * 仅有一个线程接受新连接。如果连接轮换极快,可能压垮该线程。此时请使用持久连接或 UDP。 * 高连接流失率需要操作系统调优。你可能会耗尽本地端口、TIME_WAIT 桶等资源。请研究如何为你的操作系统正确调优 TCP 堆栈。
集群中的最大节点数
从客户端视角看 大量服务器可能从多个角度拖慢客户端。如果每次请求都计算服务器哈希表,随着服务器增加性能会下降。大多数客户端在启动时计算一次表并在请求间复用。
查找以发起请求的服务器仅是哈希查找,因此不是性能问题。
* 从客户端建立过多 TCP Socket 浪费 RAM。 * 禁用持久连接意味着客户端可能每次请求都连接所有服务器。三次握手会增加每个请求的延迟和包量。
Multiget 的痛点 曾有 Facebook 笔记讨论指出,随着集群中服务器增加,multiget 的分散程度越高。对 2 服务器集群执行 10 个键的 multiget,会产生两次系统调用(每个服务器一次)。若有 10 个服务器,最终每个服务器产生一次系统调用。服务器本身处理较小入站请求的系统调用增多,处理能力下降。
这不是不可逾越的问题。许多客户端可以分组相关键。例如,某用户画像的所有键可能落在同一 memcached 实例,因此 multiget 只需命中单个服务器,总系统调用数大幅减少。
设计良好的二进制协议客户端 以 spymemcached 为例,客户端可接管多个应用线程,使用单一 TCP 连接回到 memcached。通过二进制协议,可将不同客户端实例的请求打包进同一 TCP Socket,再将结果分发回所有者。
这意味着应用服务器理论上每个 memcached 实例只需一个 TCP Socket。如果每个应用节点运行 50 个线程或进程,这可大幅减少大型部署的开销。
记住摩尔定律 如果许多大型站点仍使用五年前的硬件新建 memcached 实例,其服务器数量可能是现在的四倍或更多。随着时间推移,内存变便宜,网络带宽增加。硬件提升未必能跟上你的增长曲线,但理论上可以每几年将服务器数量减半,从而大幅降低开销。
如何为项目分配内存 通过 -m 命令行参数分配给 memcached 的内存专用于项目数据存储。主存储(默认)被分割为 1MB 页。每页按需分配给 Slab 类,再切分为该 Slab 类特定大小的块。
一旦某页分配给某类,永不移动。如果访问模式导致 80% 的页面在类 3,则类 4 可用内存减少。最佳理解方式是:memcached 实际上是许多较小的独立缓存。每个类有自己的统计计数器和 LRU。
使用 -vv 参数启动 memcached 可查看类、大小和块:
$ ./memcached -vv slab class 1: chunk size 80 perslab 13107 slab class 2: chunk size 104 perslab 10082 slab class 3: chunk size 136 perslab 7710 slab class 4: chunk size 176 perslab 5957 slab class 5: chunk size 224 perslab 4681 slab class 6: chunk size 280 perslab 3744 slab class 7: chunk size 352 perslab 2978 slab class 8: chunk size 440 perslab 2383 slab class 9: chunk size 552 perslab 1899 slab class 10: chunk size 696 perslab 1506 [...等...]
在 Slab 类 1 中,每个块为 80 字节,每页可容纳 13,107 个块(或项目)。这一直持续到 1MB。
存储项目时,推入最近适配的 Slab 类。如果键 + 杂项数据 + 值共 50 字节,进入类 1,开销 30 字节。如果数据共 90 字节,进入类 2,开销 14 字节。
可用 -f 调整 Slab 类,并以多种方式检查,但这些是更高级的主题。了解基础很有必要,因为它们可能会影响你。
其他内存使用情况 memcached 使用内存块执行其他功能。用于通过哈希表查找项目的哈希表有开销。每个连接还使用几个小缓冲区。这不应导致超过指定 -m 限制的百分之几额外内存,但请记住它们的存在。
何时回收内存 在 1.5.0 之前的版本中,默认不主动回收过期项目。如果运行旧版本并添加 -o modern start 选项,或在 1.5.0 之后,后台线程定期扫描缓存并释放过期对象。
此外,如果获取过期项目,memcached 会发现该项目已过期并释放其内存。
项目也可能被驱逐,为新存储项目腾出空间,或发现过期项目并重新使用其内存。
项目使用多少内存 项目使用键的全长、项目的内部数据结构以及数据长度所占空间。
可编译 memcached 并运行构建的 “./sizes” 工具来发现项目大小。
示例:
$ ./sizes Slab Stats 56 Thread stats 176 Global stats 108 Settings 88 Item (no cas) 32 Item (cas) 40 Libevent thread 96 Connection 320 ---------------------------------------- libevent thread cumulative 11472 Thread stats cumulative 11376
何时驱逐项目 如果项目未过期(过期时间为 0 或未来某时),Slab 类完全无空闲块,且无空闲页可分配给 Slab 类,则项目会被驱逐。
LRU 如何决定驱逐对象 存储新项目时也回收内存。如果无空闲块,且相应 Slab 类无空闲页,memcached 会查看 LRU 末端的项目以“回收”。它会在尾部最后几个项目中寻找已过期、可复用的项目。如果找不到过期项目,则驱逐尚未过期的项目。这在多个统计计数器中有记录。
有关最新 LRU 算法的详细信息,请参阅 LRU 文档。
libevent + Socket 可扩展性 memcached 使用 libevent 实现可扩展 Socket,轻松处理数万连接。memcached 中的每个工作线程运行自己的事件循环并处理自己的客户端。它们通过集中锁共享缓存,并分散处理协议。
这具有极佳的可扩展性。在极高负载(每秒 20 万+ 操作)下可能出现一些问题,但如果遇到任何限制,请告诉我们,它们通常可解决 :)
维护:硬件与实例