ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

memcached入门与实战:缓存原理、核心命令及性能调优

memcached入门与实战:缓存原理、核心命令及性能调优 最近在折腾Linux服务端的东西发现不少初学者一上来就直接问“怎么用memcached”但其实连“为什么需要缓存”都没理清楚。我当年也是这么过来的上来就敲命令结果缓存命中了也不知道命中了什么没命中也不知道该看哪里。所以这篇就按我实操的路径来写——先搞明白缓存解决什么问题再动手部署然后一步步把命令、客户端、内存机制和排错串起来。1. 先搞明白缓存这层到底值不值得加1.1 读多写少场景下的性能瓶颈点先聊一个最常见的情况你的MySQL在某个查询上已经出现了明显的响应变慢业务高峰期CPU和IO负载都上去了再查几次数据库可能就要出问题。这个时候你的第一反应也许是“加索引”“分库分表”“上读写分离”但这些都属于比较重的改造。如果业务的特点是典型的读多写少、数据实时性要求又不高那加一层缓存往往是性价比最高的第一步。memcached的定位就在这里它是一个高性能、分布式的内存对象缓存系统核心逻辑极其简单——按照key-value的方式来存取数据数据只在内存里停留不负责持久化。换句话说它就是把最热的那些数据从磁盘数据库的“慢车道”挪到了内存的“快车道”上。打个比方。数据库好比超市的总仓库每次取货都要跑到仓库去翻货架memcached就像是超市门口的储物柜热销商品先在储物柜里放一批顾客来了直接拿不用每次都跑去仓库。但如果储物柜里的东西没有了还是得老老实实去仓库取再补一批放进储物柜。1.2 memcached和Redis到底怎么选很多人一提到缓存就只想到Redis于是忍不住问都用Redis不就行了我个人的看法是这两个东西定位上有重叠但memcached在纯KV的简单场景下依然有自己的优势。先看一个最简单的对比对比项memcachedRedis数据结构只支持字符串key-value字符串、哈希、列表、集合、有序集合等持久化不支持RDB、AOF多线程模型1.4以上版本支持多线程单线程事件循环6.0开始支持多线程IO内存管理Slab Allocator机制多种内存策略简单动态字符串等适合场景纯KV缓存、Session共享、热点数据复杂数据结构、队列、排行榜、缓存等我的经验是如果你的需求就是最简单的“我把这个字符串按key存进去按key取出来”不折腾列表和有序集合那memcached在内存利用率和并发处理上并不吃亏甚至更纯粹、更容易排查。而且memcached的分布式方案在客户端层面非常成熟PHP、Python、Java的客户端都有内置的一致性哈希实现。1.3 不适合干的事别拿它当数据库还有一个初学者很容易踩的误区把memcached当成数据库用。我见过有人把用户订单放memcached里然后重启memcached之后发现数据全没了跑过来问“我的数据去哪了”。memcached的数据只活在内存里进程一重启、机器一断电什么都没了。另外memcached的value大小默认限制在1MB以内不适合存储大文本、大文件。它也不支持复杂的查询条件只能按key精确存取。所以如果在设计阶段就发现自己的数据需要遍历、聚合、排序那应该考虑的是Redis或者数据库本身而不是硬塞给memcached。把这些问题想清楚之后再动手部署才算真正“入门”。2. 环境准备与启动三分钟跑起来但别急着写代码2.1 安装环节的三种主流方式我用过的安装方式无非三种包管理器直接装、源码编译、容器部署。包管理器最省事适合绝大多数场景。在Debian/Ubuntu上sudo apt update sudo apt install memcached -y在CentOS/RHEL上sudo yum install memcached -y装完之后可以用which memcached确认一下二进制位置再用memcached -h看版本和参数说明。如果想定制编译参数或者跟进比较新的特性可以走源码编译wget https://memcached.org/files/memcached-1.6.21.tar.gz tar -zxvf memcached-1.6.21.tar.gz cd memcached-1.6.21 ./configure --prefix/usr/local/memcached make sudo make install编译依赖libevent如果报错找不到event头文件先sudo apt install libevent-dev或者sudo yum install libevent-devel。容器部署适合已经有Kubernetes或者Docker Compose环境的团队docker run -d --name memcached -p 11211:11211 memcached:latest装完就算完事儿了吗没有参数才是真正体现你是否懂它的地方。2.2 启动参数里藏着大讲究用包管理器装完systemd一般会直接帮你把服务跑起来但默认参数往往不一定符合你的场景。我建议一定要手动理解这几个核心参数参数作用我的建议-d以守护进程方式运行后台运行必备-m分配的最大内存单位MB默认64MB生产上按业务数据量预估-p监听端口默认11211如无特殊需求别改-l监听的IP地址默认监听所有地址安全起见建议只监听内网IP-u以哪个用户运行不要用root跑-c最大并发连接数默认1024高并发场景注意调整-P保存PID的文件路径配合脚本管理时有用-t线程数默认4可以按CPU核数调举个例子假设我准备给一个日活几万的业务用我会这样启动/usr/local/memcached/bin/memcached -d -m 512 -u memcache -l 192.168.1.10 -p 11211 -c 4096 -t 4 -P /var/run/memcached.pid这里-m 512指的是memcached最多能用512MB内存给数据存储项不是说一启动就占512MB它是随着set数据逐渐增长的。-c 4096是允许最多4096个客户端连接如果你用PHP-FPM并发比较多连接数很容易上去。2.3 启动成功了吗第一个验证命令启动之后怎么确认它真的在工作最简单的办法是看看端口有没有监听ss -tlnp | grep 11211然后直接用telnet连上去试着存取一个keytelnet 127.0.0.1 11211连上之后输入set hello 0 0 5 world返回STORED再输入get hello返回VALUE hello 0 5和world就说明一切正常了最后输quit退出。这一步走通之后才到了真正值得花时间研究的阶段——命令行工具其实是最好的学习入口。3. 核心命令实操把telnet当成你的第一把钥匙3.1 set、add、replace三个命令的门道memcached的存储命令里set、add、replace是三个经常被一起提的命令它们的区别直接决定了使用逻辑。set不管key是否存在都直接覆盖写入。最常用用于更新数据。add只有当key不存在时才写入成功。典型场景是防止重复写入比如分布式锁的抢占。replace只有当key已存在时才更新。常用于“只更新已有数据”的逻辑。在telnet里敲set的命令格式是set key flags exptime bytes [noreply]参数说明flags一个无符号整数是留给客户端用的附加标记位memcached本身不解析原样存原样返回。比如你可以用1表示JSON格式2表示序列化对象。exptime过期时间。0表示永不过期小于30天的数字按秒计算大于30天的数字按Unix时间戳计算。这一点特别容易踩坑下面细讲。bytesvalue的字节长度。noreply可选参数告诉服务端不用返回结果减少网络回包。实际操作感受一下set user:1001 0 300 11 hello world这个命令会往keyuser:1001写入值hello world注意看hello world是11个字符300秒之后过期。返回STORED。3.2 get、gets与cas并发安全原来这么简单get是最简单的读取命令get user:1001返回结果第一行是VALUE key flags bytes第二行是实际数据最后一行END表示结束。而gets命令会额外返回一个CAS令牌CAS unique这个令牌表示当前value的唯一版本号。配合cas命令可以实现“乐观锁式”的更新。使用场景比如说一个计数器的值要递增但是在并发环境下两个客户端同时get到了旧值然后都算出了新值并set回去最后一个set会覆盖前一个的结果导致计数丢失。用cas命令就能解决gets visit:count VALUE visit:count 0 2 50 END我拿到的CAS值是某个数字实际输出中会显示如果我要把它改成51cas visit:count 0 0 2 cas值 51只有在我拿到值之后没有别人改过的情况下这个操作才会成功返回STORED如果别人已经改了就返回EXISTS说明我基于的版本已经过期。我处理并发库存扣减时用过这种方式比“先get再set”靠谱得多。3.3 append、prepend、incr、decr偷懒时的好帮手有些时候不需要把整个value重新写入。比如日志累加、字符串后缀追加用append在已有value后面追加append user:1001 0 0 7 world!返回STORED之后再get user:1001得到的就是hello world world!。prepend则是往前面插入。incr和decr用于对数字类型的value做增减操作注意value必须能解析成十进制无符号整数否则返回CLIENT_ERROR。set count 0 0 1 5 STORED incr count 5 10在实际项目里可以用incr做计数器比如“今日访问量”“点赞数”“库存余量”比先读取再计算再写回的整个流程简单很多。但是要记住incr之后如果想规定上限memcached本身没有“到达上限则拒绝”的能力这个逻辑需要自己在客户端判断或者配合cas来做原子校验。3.4 delete、flush_all、stats现场运维三件套删除单个key用deletedelete user:1001 DELETED如果key不存在会返回NOT_FOUND。危险命令是flush_all它并不是立即把所有数据物理清掉而是把所有entry标记为过期新写入不受影响。在测试环境没问题生产环境请务必确认你搞清楚后果再执行。stats是我最常用的排查命令它会返回一大串统计信息我一般只挑几个关注stats重点看这几个字段curr_items当前存储的数据项数量total_items从启动到现在累计写入的数据项数量get_hits/get_misses缓存命中与未命中次数curr_connections当前连接数evictions因内存不足被强制淘汰的key数量bytes当前数据占用的总字节数这些指标在后面的监控排错部分会详细讲怎么解读。4. 客户端接入从命令行到业务代码如何安全落地4.1 各种语言客户端的一行启动示例telnet只是验证和学习工具真正接入业务需要客户端库。不同语言成熟度不同我按自己常用的列几个Python最推荐pymemcachefrom pymemcache.client.base import Client client Client((127.0.0.1, 11211)) client.set(site:home, hello world, expire300) print(client.get(site:home))PHP注意两个扩展的差别。很多人分不清memcache和memcached前者是老扩展API风格偏过程式后者是基于libmemcached的新扩展API更面向对象功能也更全。建议用后者$memcached new Memcached(); $memcached-addServer(127.0.0.1, 11211); $memcached-set(site:home, hello world, 300); echo $memcached-get(site:home);Java可以使用spymemcached或者xmemcached个人更常用xmemcached因为文档和更新频率都更友好MemcachedClient client new XMemcachedClient(127.0.0.1, 11211); client.set(site:home, 300, hello world); System.out.println(client.get(site:home));Go语言可以用github.com/bradfitz/gomemcachemc : memcache.New(127.0.0.1:11211) mc.Set(memcache.Item{Key: site:home, Value: []byte(hello world), Expiration: 300})这些都是最简用法跑通很容易。4.2 接入业务前必须想清楚的三个问题第一序列化方案。memcached只存字节所以对象要转成字符串。常见的组合是JSON序列化或者PHP的serialize、Java的序列化。我个人的经验是统一用JSON最省心排查问题的时候直接get出来能看懂性能要求极高的场景再考虑protobuf或者msgpack。第二key的命名规则。这个太重要了。key必须全局唯一而且要能让人一眼看懂对应什么业务。我习惯用冒号分层比如user:1001:profile、order:20240601:stats。这样在排查问题的时候按前缀就能快速定位一类数据。还要注意memcached的key长度上限是250字节超过会被拒绝。第三超时和降级。memcached服务如果挂了业务不能跟着挂。所有缓存读写都要有超时设置并且在读取失败时走“回源数据库”的逻辑把错误吞掉记录日志就行不能让用户直接看到500。这一点值得展开说。我见过不止一次线上事故是因为memcached节点宕了业务代码里没有try-catch缓存异常直接抛到上层整个接口全挂。正确做法是把缓存当成“加速器”而不是“数据库中转站”——缓存没了原逻辑至少还能从数据库查到数据只是慢一点。4.3 客户端要处理的分布式问题如果你的服务有多个memcached节点客户端需要通过一致性哈希把key分布到不同节点上。PHP的Memcached扩展可以直接addServers传多个地址Python的pymemcache也支持HashRing客户端。一致性哈希的好处是增减节点时只有部分key会重新映射而不是全部失效。在实际扩容的时候这直接决定了缓存命中率掉多少。不过话又说回来我刚接触的时候总想自己实现一套哈希算法后来想想完全没必要。成熟的客户端库都已经把这些做完了你要做的就是配置好节点列表别重复造轮子。5. slab分配、LRU淘汰与过期机制新手最容易用错的三个点5.1 Slab Allocator为什么是memcached的内存基石先问个问题memcached存100万个大小不同的key-value内存是怎么分配的如果每次都malloc一块刚刚好的内存碎片会越来越多如果干脆一次性分配一大块固定大小小数据浪费严重。memcached的解法是Slab Allocator——内存预先划分成多个Slab Class每个Class负责管理固定大小的Chunk。比如Slab Class 1的chunk是96字节Class 2是120字节Class 3是152字节按几何倍数递增。数据进来时memcached找到第一个能装下它的chunk大小然后把数据存进去。这种设计极大地减少了内存碎片同时让分配速度变得非常快。代价就是你存一个50字节的小value也可能占用一个96字节的chunk多出来的那46字节就算内部浪费了。小value越多、大小越不均匀浪费越明显。用生活化的类比理解这就像租房房源都是固定户型你一个人来租也得占一套一居室没法只租半个房间。5.2 过期了但内存没释放懒删除机制memcached的过期机制和我们直觉不太一样。你设了300秒过期时间到了key并不会被后台线程立即清理。它只是被标记为“过期”当下一次get到这个key时memcached才发现“这玩意过期了”然后返回NOT_FOUND并顺势把空间回收。这个设计叫做懒过期Lazy Expiration。好处是节省了后台扫描的CPU开销坏处是——如果过期数据一直没人访问它就继续占着内存。内存不够了怎么办这就到LRU淘汰机制登场的时候了。5.3 LRU和“穿越缓存的大锤”当某个Slab Class的内存用完了又有新数据要往这个Class里写memcached会按LRULeast Recently Used最近最少使用策略把最久没有被访问的过期数据或最冷数据踢出去。但这里有个历史上被坑过很多次的细节在旧版本里LRU是per-slab-class的也就是说某个Class满了只能淘汰这个Class里的数据哪怕其他Class还有大量空闲内存。如果你突然写入大量某个尺寸范围的数据很可能把同Class里其他业务的好数据给挤出去了。现代memcached1.4.x之后做了不少改进增加了分段LRUsegmented LRU把冷热数据分开管理还引入了slab reassignment允许在Slab Class之间重新分配内存。但这些机制都改变了“淘汰”的语义你必须通过evictions和expired_unfetched这些统计字段来观察实际行为。实操中我的体感是如果看到evictions持续增长但get_hits没有明显下降说明LRU表现得还行如果命中率在掉就要考虑是不是数据大小分布和配置的SLAB尺寸不匹配或者内存总量给少了。给一个具体的优化建议memcached启动参数里有-fgrowth factor和-n最小chunk大小比如memcached -d -m 512 -f 1.5 -n 64-f控制每个Slab Class之间chunk大小的增长倍数默认是1.25。如果你存的数据长度比较集中在几百字节到一两KB之间调大-f可以减少Class数量、提高内存利用率。这属于可以按业务实际调整的调优项不是固定配置。实践中我还见过一个问题有人把所有value都塞成500KB以上直接撞上了1MB的value上限客户端报错SERVER_ERROR object too large for cache。这种只能走改业务方案把大对象拆分存储或者干脆别进缓存。5.4 缓存穿透、击穿、雪崩先能在memcached里看明白这个三角问题在任何缓存系统里都会遇到memcached也不例外。缓存穿透是指查询一个压根不存在的key缓存里没有数据库里也没有每次请求都穿过缓存打到数据库。解决思路有两个一是对空结果也做短暂缓存比如把不存在的key也set一个空值过期时间设短一点二是用Bloom Filter在缓存前面挡一下。缓存击穿是指某个非常热的key在过期的一瞬间大量并发请求同时回源数据库。memcached本身没有“只有一个线程回源”的能力所以要在客户端做互斥常用的分布式锁配合add命令就能实现——多个客户端同时尝试add一个锁key只有一个是成功的。缓存雪崩是指大量key同时过期比如key的过期时间都设成了同一个整点数据库压力瞬间翻倍。解决思路很简单过期时间加随机扰动不要设成整齐划一的固定值。比如基础过期时间300秒再给它加上0到60秒的随机数。6. 监控、排错和面试追问实战里真正见分晓的地方6.1 stats命令逐字段解读别等出问题了才看很多人部署完memcached之后就再也不看了直到线上出问题才想起来登录服务器这是不对的。我建议在接入之后的第一时间就把stats输出完整跑一遍并存档作为基线数据。重点关注这几个指标的变化趋势指标含义预警信号curr_items当前数据项数量长时间不变化可能LRU在疯狂淘汰get_hits/get_misses命中与未命中累计命中率突然下降检查数据是否大面积过期evictions因内存不足淘汰的条目数持续增长说明内存容量不够reclaimed过期条目被复用的数量增长说明过期数据在回收cas_badvalCAS操作失败的次数频繁失败说明并发冲突严重bytes数据占用的内存字节数接近-m设置值时要关注LRU命中率计算很简单get_hits / (get_hits get_misses)。正常业务的缓存命中率应该在90%以上如果低于80%基本可以判断缓存设计有问题——要么key设计不合理要么过期时间太短要么数据本身不适合缓存。6.2 连不上的几种常见原因排查顺序线上最常遇到的几个问题我按排查顺序列一下第一端口没监听。先看ss -tlnp | grep 11211没有输出说明memcached进程挂了。查日志、查是否OOM被杀了都从这一步开始。第二防火墙挡住了。很多人在云服务器上明明装了memcached本地telnet却连不上。先看安全组有没有放行11211端口再看服务器本地防火墙sudo firewall-cmd --list-all第三绑定地址问题。如果-l参数只绑定了127.0.0.1那外网IP自然连不上如果绑定了0.0.0.0任何人都能连有安全风险。我的建议只监听内网IP并且配合严格的防火墙规则。memcached本身没有认证机制暴露在公网基本等于裸奔。第四连接数满了。默认-c 1024当连接超过上限时新请求直接拒绝。可以看stats里的curr_connections和rejected_connections新版才有rejected统计。出现这种情况要么调大-c要么检查客户端连接池配置是不是把连接泄漏了。6.3 从一次真实故障看排查思路之前碰到过一个问题某天下午服务响应突然变慢数据库CPU却不高查了memcached的stats发现get_misses从早上的几千涨到了几十万。一开始以为是数据过期时间设太短检查代码发现TTL确实没问题。然后又怀疑memcached重启过看uptime字段确认不是。最后发现是运维同事发布新版本的时候把某个批量任务的数据写入了大量新key导致同一Slab Class内存被挤占旧数据全被LRU淘汰了缓存命中率直接从95%掉到了40%。排查过程用到的思路就是先看命中率变化再看evictions是否飙升然后确认是不是有新的大批量写入。整个过程其实就是在stats的几个字段之间来回对比。这个故障最终的解决方案也很简单把这批批量任务的数据单独用一个memcached实例避免不同类型的数据互相污染Slab Class。6.4 面试官爱问的memcached相关问题如果你在学习Linux的同时准备面试memcached这块有几道高频题我按难度排一下memcached和Redis的区别memcached内存管理机制是什么说完Slab Allocator基本就过关了。memcached的过期数据怎么清理要答出懒过期LRU这两个关键词。缓存穿透、缓存击穿、缓存雪崩分别怎么解决memcached如何实现分布式答客户端一致哈希别说什么服务端集群。如果memcached内存满了会发生什么答LRU淘汰举例说明evictions指标的判断逻辑。为什么memcached默认限制value不能超过1MB因为chunk的设计上限和常见业务数据的大小边界。cas命令解决什么问题答并发更新时防止丢失更新。这些问题不是背答案就行既然这篇文章前面几节你都实操过面试时把这些过程讲出来比干巴巴背概念有说服力得多。另外多说一句很多朋友会混淆“memcached部署失败”和“memcached性能差”。我见过太多人花大量时间调memcached参数结果瓶颈其实在客户端序列化那一步。工具本身很简单真正影响效果的是业务侧的使用姿势。先把基础命令和stats指标吃透比盲目照抄网上的“最优配置”有用得多。
返回列表