Memcached 是一个免费开源、高性能、分布式内存对象缓存系统,支持将任意数据类型的 chunk 数据以键值对的方式存储。本质上 Memcached 是通用于所有的应用的,但最初用于存储被经常访问的静态数据,减轻数据库负载来加速动态 Web 应用程序。
Memcached 的所有数据都存储在内存中,与 PostgreSQL、MySQL 等持久化数据库相比存储过程无需重复往返磁盘,从而提供内存级别的响应时间(< 1ms),每秒可以执行十亿次操作。
分布式多线程架构使其易于扩展。支持将数据分散到多个节点中,从而通过向集群添加新节点来扩展容量。此外,Memcached 指定节点的多个核心,使用多线程的方式提高处理速度。
Memcached 存储任意类型的数据,支持存储所有被经常访问的数据,适用于各种应用场景。不支持联合查询等复杂操作,以提高查询效率。
Memcached 提供各种语言框架的客户端程序,如 Java、C/C++、Go 等。
适用场景
缓存
Memcached 的最初使用场景,存储网页静态数据(HTML、CSS、图片、会话等),这些数据读取次数多,修改次数少。用户在访问网页时可以优先返回内存中的静态数据,从而提高网站的响应速度。同其他的缓存一样,Memcached 不保证每次访问的数据都存在,而是提供了短期内访问加速的可能性。
数据库前端
Memcached 可作为高性能内存缓存,在系统中处于客户端与持久化数据库之间,以减少对较慢的数据库的访问次数,减少后端系统的负载,此时 Memcached 基本等于数据库的副本。在读取数据库时,首先检查 Memcached 中是否存在,若不存在则进一步访问数据库,若存在则直接返回。在写数据库时,写入 Memcached 后直接返回,后续在空闲时间时再写入数据库。
例如:在社交类 APP 中,常常有“最新评论”“最热评论”等功能,其需要大量的数据计算,并且调用频繁。若是仅用关系型数据库,则需要频繁地大量读写磁盘。在做出此类运算时,有大部分数据都会重复被使用,因此短暂地放在内存中可以大大加速整个过程。上次根据 1000 条评论计算了最热评论后,有 800 条被再次访问,它们能留在 Memcached 中。下一次计算最热评论时,只需再从数据库获取 200 条评论,节省了 80% 的数据库读写次数。
大数据量、高频读写场景
Memcached 是纯内存存储的数据库,其访问速度要远远高于其他持久化设备。此外,作为分布式数据库,Memcached 支持水平方向的扩展,将高负载请求分散到多个节点中,提供高并发的访问模式。
不适用场景
缓存对象过大
因为存储设计,官方推荐缓存的对象不要大于 1MB,Memcached 本身就不是为了存储处理庞大的多媒体(large media)和巨大的二进制块(streaming huge blobs)而设计的。
需要遍历数据
Memcached 在设计时仅支持少部分命令:set、put、inc、dec、del、cas,不支持对数据的遍历访问。Memcached 需要在常量时间内完成读写操作,但遍历数据的耗时会随着数据量的增大而增大,在这个过程中会影响其他命令的执行速度,不符合设计理念。
高可用性和容错性
Memcached 不保证缓存的数据是不被丢失的,相反,Memcached 存在命中率这一概念,即预期的数据丢失这一行为是可以接受的。当需要保证数据容灾备份、自动分区、故障转移等功能,最好将数据存储在持久性的数据库中,如:MySQL。
内存管理
如图为 Memcached 的内存管理方式:
为了防止内存碎片的出现,memcached 使用了 slab 来管理。memcached 将内存分割为多个区域,称为 slab。在存储数据时,根据数据的大小选择 slab,每个 slab 只负责一定范围内的数据存储。例如:图中的 slab 只存储 1001~2000bytes 大小的值。Memcached 默认情况下下一个 slab 的最大值为前一个的 1.25 倍,这个可以通过修改 -f 参数来修改增长比例。
slab 由 page 组成,page 的固定大小有 1M,可以通过 -I 参数在启动时指定。如果需要申请内存时,Memcached 会划分出一个新的 page 并分配给需要的 slab 区域。page 一旦被分配在重启前不会被回收或者重新分配。
存储数据时,memcached 在 page 中分配一个固定大小的 chunk,并将值插入该 chunk。需要注意的是,不管插入数据的大小是多少,chunk 的大小总是固定的,摒弃每一个数据都会独占一个 chunk,不存在两个较少的数据存储在同一个 chunk 中。如果 chunk 不够用了,只能转到下一个 slab class 中的 chunk 中存储,造成空间浪费。
LRU
如图所示,Memcached 使用改良后的 LRU 机制来管理内存中的 item。
因为 item 可能表现出很强的时间局部性或非常短的 TTL(生存时间)。因此,item 永远不会在 HOT 内发生移动:一旦某个 item 到达队列尾部,如果该项目处于活动状态 (3 ) ,它将被移动到 WARM ,如果它处于非活动状态 (5),它将被移动到 COLD。
充当扫描工作负载的缓冲区,例如网络爬虫读取旧帖子。从未被击中两次的 item 无法进入 WARM。WARM item 有更大的机会活过其 TTL,同时也减少了锁争用。如果尾部 item 处于活动状态,我们将其移动回头部 (4)。否则,我们将不活动的 item 移至 COLD (7)。
包含活动最少的 item。非活动 item 将从 HOT (5) 和 WARM (7) 流向 COLD。一旦内存已满,item 就会从 COLD 的尾部移出。如果某个 item 处于活动状态,它将排队异步移动到 WARM (6)。在 COLD 出现突发或大量点击的情况下,缓冲队列可能会溢出,item 将保持非活动状态。在过载情况下,来自 COLD 的移动将成为概率事件,而不会阻塞工作线程。
作为新 item 的队列,其 TTL 很短(2)(通常为几秒)。TEMP 中的 item 不会被替换,也不会流向其他 LRU,从而节省了 CPU 和锁的争用。目前默认情况下未启用该功能。
crawl 同时从下到上通过 LRU 向后遍历每个爬虫 item。爬虫会检查它传递的每个项目,看看它是否过期,如果过期则回收。
接下来介绍 Prometheus 监控开源 Memcached 的一些关键指标。
运行状态
启动状态是监控 Memcached 最基础的指标,表示 Memcached 实例是否在正常运行,或是否重启。当 Memcached 停机时,对整个系统的功能影响可能不大,因为对数据的访问会提交到底层的持久化数据库中,但缺少了 Memcached 的缓存会使系统运行效率降低至少一个数量级。
检查 Memcached 启动时长能帮助验证 Memcached 是否重启。因为 Memcached 将所有数据放置在内存,所有当 Memcached 重启后缓存的所有数据将会丢失。此时 Memcached 的命中率会骤降,导致缓存雪崩的情况,同样会给底层数据库带来很大压力,且降低系统效率。
内存使用
Memcached 作为高性能缓存,需要充分利用节点的硬件资源来提供快速的数据存储和查询服务,如果节点的资源使用超过了预期或者达到了极限,可能会导致性能下降或者系统崩溃,影响业务的正常运行。Memcached 把数据放置在内存中,所以我们需要重点关注内存的使用率。当 Memcached 的内存占用率过高,可能影响节点的其他运行任务。需要考虑 key 的设计是否科学、增加硬件资源等优化方案。
读写速率
读写速率是 Memcached 集群性能的重要指标,如果读写延迟较高,则可能会导致系统响应时间长,节点出现高负载、系统出现瓶颈等问题。读写指标表示了 Memcached 的运行效率总览,如果发现读写延迟较高,运维人员可以需要关注其他监控数据来排查问题。读写速率慢可能有多种原因,如命中率低、节点资源紧张等。针对不同的问题,需要采取不同的排查和优化措施来提高性能。
命令速率
Memcached 支持多种命令,如:set、get、delete、CAS、incr 等。监控各个命令的速率能得出 Memcached 瓶颈所在,当某种命令速率较低,导致了整体速率不高,可以考虑调整 item 存储策略、修改访问方式等方案提高 Memcached 性能。
命中率
缓存可以提高查询的性能和效率,减少对磁盘的读取次数,从而提高系统的响应速度和吞吐量。命中率是 Memcached 最重要的指标,命中指上层应用在获取某个数据时,发现能从 Memcached 中获取,而不需要访问底层的数据库。当命中率越高,表明大部分数据的访问都在内存中。
在使用缓存的系统中,有可能发生缓存雪崩、缓存穿透、缓存击穿等状况,导致在某一个时间段内命中率很低,此时系统的运行是很低的,因为大部分数据访问都落在了磁盘上,底层数据库压力很大。我们需要时刻关注命中率的变化情况,保证 Memcached 在系统中发挥着应有的作用。例如:网站某段时间的响应突然变得很慢,发现在该时间段命中率很低,再检查日志后发现:因为业务变化导致某个页面突然从低频访问变成高频访问,这是发生了缓存穿透的情况。解决命中率低的问题需要结合其他的指标来分析,如下文的 slab 指标。
作为键值数据库,Memcached 将内存中的一对键值称为 item。了解 item 在 Memcached 中的存储情况,能够优化其内存的使用效率,并提高命中率。
Item 存储
item 的存储状态一般监控:Memcached 存储的 item 总量、回收的 item 总量、驱逐的 item 总量。通过观察 item 总量的趋势可以看到 Memcached 存储的压力,以及使用 Memcached 应用的存储模式。
驱逐和回收的区别是:当需要驱逐 item 时,Memcached 会查看 LRU 尾部周围的一些 item,寻找可以回收的过期 item,复用其内存空间,而不是驱逐真正的尾部。
Slab 使用情况
根据 Memcached 的设计思路,每个 slab 存储的 item 大小都相同,并且每个 item 独占一个 chunk,这是为了提高内存利用率。然而在使用过程中,这样的设计仍有优化空间。
Memcached 钙化是容易出现的一种问题。当内存达到 Memcached 限制的时候,服务进程会执行一系列的内存回收方案,但是,不管是什么内存回收方案,回收的大前提就只有一种:只回收与即将写入数据写入数据块一致的 Slabs。
例如:我们之前存储了大量大小为 64Kb 的 item,现在又需要存储大量大小为 128Kb 的 item。若内存的额度不够,且这些 128Kb 的 item 在不断地更新,为了存储新的数据 Memcached 只能选择其他 128Kb 的 item 驱逐。我们发现,原有的 64Kb 的 item 一直不被驱逐,在过期之前不停地占用内存空间,导致空间浪费,最终导致命中率降低。
要解决钙化的问题,就需要了解各个 slab 中 item 的分布指标。我们提供此类监控,观测 slab 之间存储 item 数量的对比。若发现某几个 slab 存储 item 数量远高于其他 slab,可以考虑调整各个 slab 的大小。默认的 slab 大小增长因子为 1.25,也就是每个 slab 的容量都是前一个的 1.25 倍。通过调低增长因子,并设置 slab 的起始大小,使 item 均匀分布在各个slab中,能有效缓解钙化的问题,并提高内存使用效率。
区域 Item 数
在 Memcached 的 LRU 中,每个区域都有着不同的功能。HOT 中的是最新被存储的 item,WARM 中的是比较热点的 item,COLD 的是即将过期的 item。了解各个区域的 Item 数量能对 Memcached 的状态有较深的理解,并提供调优方案。下面是一些例子:
HOT 区域 item 较少,其他区域 item 较多。表明新创建的 item 较少,Memcached 中的数据比较少被更新,系统中的数据以读为主。反之,HOT 区域 item 较多,表明新创建的 item 较多,此时系统以写为主。
WARM 区域 item 比 COLD 区域 item 多。此时 item 被命中情况较多,属于比较理想的情况。反之命中率较低,需要考虑优化。
移动 Item 数
Item 命中与否,会影响其在哪个 LRU 区域。基于 LRU 的设计思路,通过监控 item 在 LRU 区域移动的情况,可以得出数据访问的状态。以下是一些例子:
COLD 区域移动至 WARM 区域的 item 数增大。表明即将过期的 item 被命中,当该数值过大,表明某些冷门数据突然成为热点数据。需要关注此类情况防止命中率降低。
HOT 区域移动至 COLD 区域的 item 数较大。表明有大量的 item 被插入后就不再被访问,这些数据可能不需要被缓存,考虑直接存入底层数据库以缓解内存压力。
WARM 区域的 item 数较大。表明大量的 item 被插入后被访问,这类情况比较理想,Memcached 存储的数据是被经常访问的,此时命中率较高。
连接状态
由于 Memcached 使用基于事件的架构,因此大量客户端通常不会减慢速度。当用户拥有数十万个连接的客户端时,Memcached 也能正常工作。但监控当前的用户连接数可提供 Memcached 工作状态的总览。
连接错误
Memcached 限制单个客户端连接可以为每个事件发出的请求数,在启用时用-R参数决定。在客户端超出此值后,服务器会优先处理其他客户端,然后再继续处理原始客户端请求。应当时刻监控是否发生这样的情况,保证客户端正常使用 Memcached。Memcached 默认最大连接数 1024,运维人员需要监控连接数不突破限制,影响正常功能使用。
系统指标
指标名 | 指标说明 |
memcached_process_system_cpu_seconds_total | 进程的system CPU使用时间 |
memcached_process_user_cpu_seconds_total | 进程的user CPU使用时间 |
memcached_limit_bytes | 存储大小 |
memcached_time_seconds | 当前时间 |
memcached_up | 是否启动 |
memcached_uptime_seconds | 启动时间 |
memcached_version | 版本 |
读写指标
指标名 | 指标说明 |
memcached_read_bytes_total | server中read数据的总大小 |
memcached_written_bytes_total | server中send数据的总大小 |
memcached_commands_total | server中按命令分类的所有请求总数 |
memcached_slab_commands_total | slab class中按命令分类的所有请求总数 |
存储指标
slab存储指标
指标名 | 指标说明 |
memcached_items_total | 存储item的总数 |
memcached_items_evicted_total | 被驱逐的item数量 |
memcached_slab_items_reclaimed_total | 过期item的数量,以slab分类 |
memcached_items_reclaimed_total | 过期item的总数量 |
memcached_current_items | 当前实例的item数量 |
memcached_malloced_bytes | slab page大小 |
memcached_slab_chunk_size_bytes | chunk大小 |
memcached_slab_chunks_free | 空闲chunk数量 |
memcached_slab_chunks_used | 非空闲chunk数量 |
memcached_slab_chunks_free_end | 最新的page中空闲chunk数量 |
memcached_slab_chunks_per_page | page中chunk数量 |
memcached_slab_current_chunks | slab class中chunk数量 |
memcached_slab_current_items | slab class中item数 |
memcached_slab_current_pages | slab class中page数 |
memcached_slab_items_age_seconds | 自上次访问slab class中最近一个item以来的秒数 |
memcached_current_bytes | item实际占用大小 |
memcached_slab_items_outofmemory_total | 触发out of memory error的item数 |
memcached_slab_mem_requested_bytes | slab中item存储大小 |
LRU存储指标
指标名 | 指标说明 |
memcached_lru_crawler_enabled | LRU crawler是否启用 |
memcached_lru_crawler_hot_max_factor | 设置HOT LRU转为WARM LRU的闲置时期 |
memcached_lru_crawler_warm_max_factor | 设置WARM LRU转为COLD LRU的闲置时期 |
memcached_lru_crawler_hot_percent | slab留给HOT LRU的百分比 |
memcached_lru_crawler_warm_percent | slab留给WARM LRU的百分比 |
memcached_lru_crawler_items_checked_total | LRU Crawler检查的item数 |
memcached_lru_crawler_maintainer_thread | 拆分LRU模式和后台线程 |
memcached_lru_crawler_moves_to_cold_total | 移入COLD LRU的item数 |
memcached_lru_crawler_moves_to_warm_total | 移入WARM LRU的item数 |
memcached_slab_items_moves_to_cold | 移入COLD LRU的item数,以slab分类 |
memcached_slab_items_moves_to_warm | 移入WARM LRU的item数,以slab分类 |
memcached_lru_crawler_moves_within_lru_total | 被crawler处理,在HOT、WARM LRU中打乱的item数 |
memcached_slab_items_moves_within_lru | 在HOT、WARM间被命中而交换的item数 |
memcached_lru_crawler_reclaimed_total | LRU中被LRU Crawler清理的item数 |
memcached_slab_items_crawler_reclaimed_total | slab中被LRU Crawler清理的item数 |
memcached_lru_crawler_sleep | LRU Crawler的间隔时间 |
memcached_lru_crawler_starts_total | LRU启动时间 |
memcached_lru_crawler_to_crawl | 每个slab被crawl的最大item数量 |
memcached_slab_cold_items | COLD LRU中的item数 |
memcached_slab_hot_items | HOT LRU中的item数 |
memcached_slab_hot_age_seconds | HOT LRU中最老的item年龄 |
memcached_slab_cold_age_seconds | COLD LRU中最老的item年龄 |
memcached_slab_warm_age_seconds | WARM LRU中最老的item年龄 |
memcached_slab_items_evicted_nonzero_total | 明确设置了过期时间的item在过期前必须从 LRU 中删除的总次数 |
memcached_slab_items_evicted_total | 被驱逐且从未取回的item总数 |
memcached_slab_items_evicted_unfetched_total | 过期且从未取回的item总数 |
memcached_slab_items_tailrepairs_total | 特定 ID 的item需要取回的总次数 |
memcached_slab_lru_hits_total | LRU总命中数 |
memcached_slab_items_evicted_time_seconds | 自上次访问该slab中最近被驱逐的item起的秒数。 |
memcached_slab_warm_items | WARM中的item数,以slab分类 |
连接指标
指标名 | 指标说明 |
memcached_connections_total | 接受连接总数 |
memcached_connections_yielded_total | 因触及 memcached -R 限制而运行的连接总数。 |
memcached_current_connections | 打开连接数 |
memcached_connections_listener_disabled_total | 触及限制的连接总数 |
memcached_connections_rejected_total | 拒绝连接数,触发memcached -c 限制 |
memcached_max_connections | client限制大小 |
我们默认提供了 Memcached Overview 大盘。
在该 panel 能看到Memcached运行时需要重点关注的指标,在检查 Memcached 状态时,首先查看总览中是否有异常状态,再检查具体的指标。
在以下 panel 能看到 Memcached 的运行速度,分为以下三类。
命中率是需要重点关注的指标,通过以下三个 panel 能详细了解 Memcached 的命中状态。
item 表示 Memcached 的存储状态,通过以下 panel 检查 Memcached 的内存使用状态。
内存是 Memcached 的重点关注硬件资源,通过该 panel 能了解 Memcached 的内存使用情况:
尽管 Memcached 提供高并发无损耗的支持,但网络资源不是无限的,需要随时关注:
句柄是系统指标,涉及到网络连接等资源。
在对 Memcached 进行告警规则配置时,我们推荐基于以上采集得到的指标,从以下几个方面进行告警规则的配置,分别是运行情况、资源使用情况、连接使用情况。一般来说,我们默认生成影响 Memcached 正常使用的告警规则,优先级较高。读写速率等与业务相关的告警则由用户自定义。以下是一些推荐的告警规则。
Memcached 停机
Memcached 停机是 0/1 阈值的告警规则。一般来说,部署在 ACK 等阿里云环境的 Memcached 服务具有高可用的能力,当一个 Memcached 实例停止,其他的实例会继续工作。有可能出现程序错误,导致 Memcached 实例无法重新部署,这是非常严重的情况。我们默认设定 5 分钟内 Memcached 无法恢复的告警。
Memcached 重启
对于其他的服务,实例重启不算问题。然而 Memcached 是典型的有状态服务,它的主要数据都存储在内存中。当实例被重启,缓存的数据将全部丢失,此时命中率下降,导致系统性能很差。因此我们推荐监控 Memcached 的重启告警,当重启发生后找到对应问题,防止该情况再次出现。
内存使用率
当内存使用率过高,Memcached 无法正常运行。我们设定的内存使用阈值为:危险值 80%,告警值 90%。当内存使用率为 80% 时,节点高负荷运转,但一般不影响正常使用。当内存长时间使用率为 90% 时,将发出告警,提示运维资源紧缺,尽早处理。
item 触发 OOM
当内存使用量超过节点总内存时,内存不足 (OOM) 情况可能会导致数据完全缓存刷新,从而中断您的应用程序和业务。当节点内存利用率指标接近 100% 时,实例更有可能遇到 OOM 情况。我们设置了 0/1 阈值的告警,一旦出现 OOM 则立刻告警。
拒绝连接
一般情况下连接数的增多不影响 Memcached 的运行,除非突破了最大连接数限制,此时客户端无法正常获取 Memcached 服务。我们设置了 0/1 阈值的告警,一旦出现拒绝连接的情况,立刻告警,保证客户端正常运行。
连接请求数过多
我们设置了 0/1 阈值的告警,一旦出现连接请求数过多的情况,立刻告警,保证客户端正常运行。在这样的情况下,Memcached 暂时不接受该连接的命令。
命中率低有各种各样的原因,我们需要联系多个指标进行排查。
检查内存使用率
检查 item 情况
检查 LRU 各区域 item 情况
本文系作者在时代Java发表,未经许可,不得转载。
如有侵权,请联系nowjava@qq.com删除。