☰
Linux下Memcached部署与缓存优化:从安装到排查的运维实践
2026/10/1 22:30:22 网站建设 项目流程

说实话,现在聊缓存,十个人里有九个先想到Redis,memcached这个名字好像已经有点“过气”了。但我这两年做Linux运维和系统调优,反而越来越觉得memcached是个该认真学一学的东西。它不复杂,没有持久化,没有复杂数据结构,连认证都得靠外部手段,可正因为简单,它在纯KV缓存场景下的性能极其稳定,内存利用率也相当好算,出了问题基本一眼就能看穿。这篇文章就基于Linux环境,从零开始讲讲memcached怎么装、怎么用、怎么排查,适合刚接触Linux缓存这一块、或者面试前想快速把memcached脉络理清楚的朋友,你可以把它当一份带踩坑记录的入门笔记看。

1. 认识memcached:它到底解决什么问题

1.1 从数据库压力说起

先聊一个我碰到过的真实场景。某个内部运营系统,每天早高峰一堆查询请求打到MySQL上,有些统计类的SQL一张表就几百万行,虽然加了索引,但并发一上来照样把数据库的CPU打到80%以上,慢查询日志里全是同一个查询。后来做的事情很简单:把那些热点数据按key塞到memcached里,查询先走缓存,缓存没有再查库再回填,数据库负载肉眼可见地降了下来。

这就是memcached最核心的价值——给后端的数据库或API接口挡掉重复读的压力。它的定位不是存业务数据,而是存那些“读多写少、稍微丢一点也没关系、可以重建”的热数据,比如用户登录状态、商品详情、配置项、验证码。理解这点很重要,因为很多人一开始就把它当数据库用,存些不能丢的东西,结果一重启全没了,就开始骂这玩意儿不靠谱。

1.2 memcached核心特性与适用边界

memcached是纯内存缓存系统,数据只存在内存里,进程一停数据就没了,它自己也压根不打算帮你持久化。它的几个关键特性,值得一条一条捋清楚:

  • 基于key-value的简单结构,value最大默认能到1MB,改参数能调大,但真要存大对象不如直接存文件路径。
  • 协议简单,基于文本行协议,客户端随便用telnet连上去就能手动读写数据,调试非常方便。
  • 服务端是多线程模型,CPU核多的机器能跑满多核,这点比很多单线程缓存服务在纯KV场景下更占便宜。
  • 内存管理用slab allocation机制,按固定chunk大小分桶,避免内存碎片,但也会带来一定的内存浪费。
  • 支持LRU过期淘汰和主动过期,用得很省心。

那什么时候该用它而不是Redis?我个人的表是这么分的:

对比维度memcachedRedis
数据结构只有字符串KV字符串、哈希、列表、集合、有序集合等
持久化不支持RDB/AOF
多核利用多线程,天然利用多核单线程为主,高版本有一些多线程IO
内存管理slab分桶,分配固定多种策略,相对灵活
适用场景纯KV热点缓存,体积小量大数据结构复杂、需要持久化、发布订阅等

入门阶段不要纠结谁取代谁,先把memcached在纯KV缓存上的效率和Linux下的部署方式吃透,后面再学Redis,你会觉得很多东西是通的,甚至memcached的协议和内存模型对理解Redis的内存优化也有帮助。

2. Linux下安装部署:从源码编译到systemd托管

2.1 安装前先想清楚版本和依赖

memcached本身依赖libevent这个事件库,所以装之前要先搞定它。我在CentOS系的机器上一般这么做,先更新、装编译工具链和依赖:

yum install -y gcc gcc-c++ make autoconf libtool yum install -y libevent libevent-devel

如果是Ubuntu/Debian系,对应命令是:

apt update apt install -y build-essential libevent-dev

有个容易踩的坑:有些精简环境里libevent-devel没装,编译memcached时它会报找不到event.h,卡住半天。遇到这种错误别慌,先确认依赖包状态,再检查是否存在多版本libevent冲突。我在一个最小化安装的CentOS 7上试过,忘了装devel包,编译报错,补上后重新configure就好了。

2.2 编译安装还是用包管理器

我其实两种都试过。用yum或apt直接装最快,版本一般也够用:

yum install -y memcached

或者:

apt install -y memcached

但如果是生产环境,我更推荐源码编译,因为你可以指定编译参数、装到固定目录,后续多实例部署也方便。官网下载源码包,然后走标准三步:

tar zxf memcached-1.6.21.tar.gz cd memcached-1.6.21 ./configure --prefix=/usr/local/memcached --with-libevent=/usr make make install

需要注意一点,如果libevent是自定义路径安装的,configure时一定要用--with-libevent=/your/path指定,不然会提示找不到。编译过程很快,一两分钟的事。装完二进制在/usr/local/memcached/bin/memcached,用-h验证一下:

/usr/local/memcached/bin/memcached -h

能看到一大串启动参数说明,就说明装好了。

说到这里,我顺便提一下热词里总有人讲“memcached源码分析”。如果你想深入理解它的内存模型和LRU策略,建议先把1.6.x源码下下来,重点看slab.c和items.c这两个文件。入门阶段不用啃太深,但知道源码结构长什么样,面试时聊起来会扎实很多。

2.3 启动参数解析和内存分配细节

启动memcached最精简的方式就是这样:

/usr/local/memcached/bin/memcached -d -m 64 -u root -l 127.0.0.1 -p 11211 -c 1024 -P /tmp/memcached.pid

逐个拆一下,这里每个参数背后都有讲究:

  • -d:以守护进程方式运行,不加的话关闭终端它就跟着退了。
  • -m 64:给memcached分配64MB内存。这个参数特别重要,它决定了你最多能缓存多少数据,也决定了内存用完后淘汰会不会变得很激进。
  • -u root:指定运行用户。默认很多发行版会让你用memcached用户跑,但你手动测试时用root也行,生产环境建议专门建一个低权限用户。
  • -l 127.0.0.1:监听地址。默认会监听所有地址,如果你这台机器上有公网IP,最好把它改成内网IP或回环地址,不然容易被扫描利用。memcached本身没有认证,暴露在公网基本等于裸奔。
  • -p 11211:监听端口,默认就是11211。多实例部署时每个实例可以分配不同端口。
  • -c 1024:最大并发连接数,默认1024就够测试用。
  • -P /tmp/memcached.pid:写pid文件,方便后面stop或reload。

关于-m这里有个容易忽略的点:64MB是memcached用来存数据的内存上限,它实际占用的物理内存不只是64MB,还包含本身slab分配器的一些开销、连接缓冲、哈希表等等。所以在评估机器内存时,建议给memcached留出一些余量,比如-m 1024时,实际常驻可能是1.1GB上下。我见过有人把-m设成机器物理内存的90%,然后发现SWAP狂涨,机器卡死。建议最多给到机器内存的一半左右。

如果你用systemd管理,可以写一个service文件,方便开机自启。这里给一个常见模板:

[Unit] Description=Memcached Daemon After=network.target [Service] Type=forking ExecStart=/usr/local/memcached/bin/memcached -d -m 256 -u root -l 127.0.0.1 -p 11211 -c 1024 -P /tmp/memcached.pid PIDFile=/tmp/memcached.pid Restart=on-failure [Install] WantedBy=multi-user.target

把文件放到/usr/lib/systemd/system/memcached.service,然后:

systemctl daemon-reload systemctl enable memcached.service systemctl start memcached.service

这样的好处是以后可以用systemctl status memcached看运行状态,出问题也容易排查。

2.4 如何验证服务真的在跑

服务起来后,先用最基础的命令看进程和端口:

ps -ef | grep memcached ss -lntp | grep 11211

如果你用Linux的ss命令看到LISTEN状态的11211端口,说明服务已经监听。再进一步可以用stats命令确认内部状态:

printf 'stats\r\n' | nc 127.0.0.1 11211

这条命令会返回一堆以STAT开头的行,包括当前连接数、命中次数、内存使用等。只要能看到STAT uptime在增长,就说明进程是活的。

3. 基础使用:从telnet增删改查到数据过期策略

3.1 命令行手动读写数据

memcached的文本协议非常直观,入门阶段我强烈建议先别急着写代码,用telnet或nc手动读写几组数据,把协议格式吃透。首先连上去:

telnet 127.0.0.1 11211

或者用nc:

nc 127.0.0.1 11211

连上后,存一条数据用set命令,格式是这样:

set <key> <flags> <exptime> <bytes> <value>

比如我想存一个字符串hello到key为name的位置,过期时间设0表示永不过期,然后敲回车:

set name 0 0 5 hello

服务端会返回STORED,看到它就说明写入成功了。这里有几个细节容易出错:bytes必须和实际输入字节数完全一致,多一个空格、少一个字符都会失败;输入完value后需要再敲一次回车让服务端确认。我自己刚开始试的时候,经常在这里敲多了空格,返回CLIENT_ERROR bad data chunk,后来才反应过来bytes是字节数不是字符数,中文要按字节算。

我们常用的几个指令整理如下:

指令作用示例
set新增或覆盖set name 0 0 5
add仅当key不存在时新增add name 0 0 5
replace仅当key已存在时覆盖replace name 0 0 5
get取一个或多个keyget name
delete删除指定keydelete name
incr对数字value自增incr num 5
decr对数字value自减decr num 2
stats获取运行状态stats

add和replace这两个命令在并发场景下很有用:add可以用来实现简单的分布式锁;replace则适合只更新已存在的缓存,避免把刚过期的数据重新写回去。实际项目里,add配合get是防缓存击穿的一个小技巧,后面排查部分我会再细说。

3.2 数据过期时间格式,别踩这个坑

memcached的过期时间有两种格式。如果你设置的时间小于等于30天(准确说是60*60*24*30秒),它就把这个值当成从现在开始多少秒后过期。比如set name 0 60 5,60秒后这条数据会过期。如果设置的秒数超过这个阈值,memcached会将其解释为Unix时间戳,也就是从1970年1月1日到某个绝对时间点的秒数。

我踩过一次坑:想设置一个key在2天后过期,心里算了下172800秒,直接写成set key 0 172800 5,结果发现数据不到几分钟就消失了。排查半天才意识到,我那个环境里的memcached是旧版本,且我给的数值不小心超过了30天上限,被当成了时间戳解析。所以入门阶段记住一个习惯:要么用小于30天的相对秒数,要么干脆用date +%s算出绝对时间戳再填进去,不要自己脑补大数字。

读取数据用get,一次可以传多个key:

get name age city

服务端会把命中的key和value分块返回,最后以一个END结束。不存在的key不会出现在结果里,这也是memcached“无脑缓存”的体现——查不到就查不到,不会像数据库那样报错。

3.3 统计信息怎么看

stats命令用得非常多,它是排查缓存命中率和内存状况的第一入口。返回的信息里,我一般重点看这几个字段:

字段意义
uptime进程运行秒数
curr_items当前存的key数量
total_items历史累计写入次数
get_hitsget命中的次数
get_missesget未命中的次数
curr_connections当前连接数
bytes数据实际占用的字节数
evictions因内存不足被LRU淘汰的条目数
limit_maxbytes允许使用的最大内存字节数

命中率可以自己算:get_hits / (get_hits + get_misses)。我一般要求线上命中率至少到90%以上,如果低于80%,说明缓存价值不大,要么key设计有问题,要么过期时间太短。evictions这个值要是持续增长,说明内存不够用了,得考虑调大-m或优化key数量。

除了基础stats,还有stats slabs和stats items,分别查看slab分桶情况和每个桶里的item数量。入门阶段理解即可:memcached会把不同大小的value分配到不同chunk的slab桶里,如果value大小分布不均匀,比如全是1KB到2KB的数据,但slab桶划分得不对,就会有一部分内存被浪费。这是它内存碎片率高的根源,想深入再研究,初级应用只要知道有这回事就行。

3.4 多实例部署的常见思路

一台机器跑多个memcached实例,在生产环境并不罕见,特别是你要对不同业务线做隔离的时候。做法很简单:每个实例分配不同端口和不同的-m内存值,比如订单缓存跑11211端口给2GB,用户会话跑11212端口给1GB。多实例之间互不干扰,一个崩了也不影响另一个。

但要注意,多实例不等于集群。memcached本身没有原生的集群模式,所谓集群一般是客户端根据key的哈希值路由到不同节点,或者你前面再挂一层代理。入门阶段别被“集群”这个词吓到,先把单机多实例玩明白,后面理解一致性哈希就顺理成章了。

4. 常见问题与排查技巧实录

4.1 连接不上、拒绝访问怎么查

这是新手上路问得最多的一个问题。我按顺序给一个排查思路:

第一步,先确认进程是活的:

ps -ef | grep memcached

如果进程不在,看/var/log/message或systemd日志:

journalctl -u memcached -n 50

常见原因是启动参数里指定的-m超过系统可用内存或-l绑定地址写错。

第二步,确认端口在监听:

ss -lntp | grep 11211

如果没看到,确认启动时是不是加了-d,有时候你手动前台启动、然后Ctrl+C把进程带退了,端口自然就没了。

第三步,测试本机能不能连通:

nc -vz 127.0.0.1 11211

返回Connected说明本机没问题。如果本机能通、外部机器不通,90%是防火墙挡住了:

iptables -L -n | grep 11211 firewall-cmd --list-all

需要放行就执行:

firewall-cmd --permanent --add-port=11211/tcp firewall-cmd --reload

还有种情况是云服务器安全组里没有放行11211端口,这个也要检查。

4.2 内存暴涨和淘汰策略

有时候你会发现memcached进程占用的内存比-m配置多很多,或者stats里evictions值一直在涨。前者通常是slab分配器的问题:每个slab桶的最小chunk大小是固定的,如果放入的value尺寸稍大于某个chunk,就会分配下一档更大的chunk,造成内存浪费。举个例子,你存一个900字节的value,memcached可能把它放进1024字节的chunk桶里,每个item浪费124字节,数据一多内存“虚胖”就很明显。

解决思路有两个:一个是尽量把缓存value的大小控制在相似范围,避免极端尺寸;另一个是用-n参数调最小item大小,用-f参数调增长因子,这两个参数入门阶段可以不动,但要明白它们影响内存利用率。

至于evictions增长,说明内存用完了,memcached按LRU策略把最久没访问的item挤出去。表面上缓存还能用,但命中率会掉。我遇到过一个案例:一台2GB内存的机器,memcached配了1.5GB,然后命中率只有75%,evictions高达几百万。后来把一些很久不用的业务key单独拆分到另一个实例,主缓存只存高频数据,命中率才回到95%以上。

4.3 缓存穿透、击穿和雪崩的实战处理

这三个词面试常问,实际运维中也必须面对。

缓存穿透:查询一个必然不存在的数据,比如拿一个不存在的用户ID查详情,每次都会穿过缓存直接打到数据库。我在项目中一般用两个办法:一是把这类空结果也缓存起来,设一个很短的过期时间,比如30秒,让数据库喘口气;二是在接口层做参数校验,明显不合法的ID直接拒绝,不落到查询环节。

缓存击穿:某个热点key突然过期,大量请求同时打到数据库。这边可以用互斥锁或add命令实现“只放一个请求去重建缓存”的逻辑。具体做法就是当缓存miss时,先尝试add一个带特殊标记的key,如果add成功,说明自己是第一个来重建的,就去查数据库回填;如果add失败,说明别人正在重建,自己就sleep几十毫秒再重试get。

我给一段伪代码风格的示意:

# 伪代码:用add实现单飞重建缓存 cache_key = "hot_data" data = mc.get(cache_key) if data is None: lock_key = "lock:" + cache_key if mc.add(lock_key, 1, 3): # 3秒内其他请求不会重复重建 data = query_db() mc.set(cache_key, data, 60) mc.delete(lock_key) else: time.sleep(0.05) data = mc.get(cache_key)

缓存雪崩:大量key在同一时间过期,造成数据库瞬间压力飙升。我常用的规避手段有三层:过期时间加随机值,比如统一60秒过期的key,实际生成时加一个0到30秒的随机偏移,让过期时间错开;热点数据过期时间设置得足够长,并且通过后台任务主动更新;关键接口做好降级方案,数据库扛不住时直接返回兜底数据或限流。

4.4 用代码接入:Python和PHP的简单示例

虽然命令行能读写,实际业务里还是要靠客户端库来操作。这块我以最常用的Python和PHP做例子。

Python下用pymemcache比较轻量,先安装:

pip install pymemcache

然后连接和读写:

from pymemcache.client.base import Client client = Client(('127.0.0.1', 11211)) client.set('username', 'zhangsan', expire=60) value = client.get('username') print(value) # b'zhangsan'

注意,pymemcache的get返回的是bytes类型,不是str,需要自己做解码。如果你想要返回值自动出来,可以用pymemcache的JsonJsonSerde,但入门阶段先明白字节流的坑就好。

PHP下更简单了,装上memcached扩展:

$mc = new Memcached(); $mc->addServer('127.0.0.1', 11211); $mc->set('username', 'zhangsan', 60); $value = $mc->get('username');

PHP的memcached扩展默认会帮你做序列化,存数组、对象都方便,但跨语言读写要注意序列化格式不同的问题。

4.5 自检清单和日常维护小习惯

最后给一份我平时维护memcached时用的检查清单,照着做基本能避免大多数坑:

  • 启动参数里-l是否绑定了正确的网卡,生产环境不要监听0.0.0.0。
  • -m是否小于物理内存的一半,给系统和其他进程留余地。
  • 是否设置了pid文件,方便异常退出时快速定位。
  • 定期看命中率和evictions,命中率持续走低就要分析key设计。
  • 对公网环境要加防火墙策略,memcached原生没认证,不要裸奔。
  • 多实例部署时确保端口不冲突,使用ss -lntp确认。

我自己在经历过几次线上事故后,养成一个习惯:每次上线memcached配置变更,都会在变更前记录一次stats快照,变更后再快照一次,对比命中率、占用内存、淘汰数这几个指标。别看动作小,出了问题定位速度能快不少。

5. 写在最后的一点体会

我在学习和使用memcached的过程中最深的感受是:越简单的东西越容易用错,但一旦用对了,收益立竿见影。很多人觉得memcached太单调,不值得花时间,但它的内存模型、淘汰策略和文本协议,恰恰是理解其他缓存系统的基础。你如果能在Linux上徒手部署、用命令行调通、再通过stats指标判断缓存健康状态,这笔技能就算真正落地了。后续想继续深入的话,建议看看slab分配器的实现思路,再对比一下Redis的内存编码方式,会有种豁然开朗的感觉。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询