Memcached:性能优化
发布时间:2026-04-24 12:02
一、前言
搞过的人都清楚,Memcached跑久了内存越吃越多,QPS死活上不去,连接数动不动打满,命中率莫名其妙掉下来。重启能好一阵子,但问题根源不解决,过几天又复发。这篇文章把10年踩坑经验摊开讲,从内存分配、网络优化、客户端调参到监控排障,手把手让你把Memcached性能榨干。
二、操作步骤
第1步:确认当前状态,别瞎调
# 先看看当前Memcached跑成什么鬼样子
ps aux | grep memcached
netstat -an | grep 11211 | wc -l
memcached-tool 127.0.0.1:11211 stats
预期输出类似:
root 23541 0.3 2.1 356280 109248 ? Sl Jan15 1234:23 /usr/bin/memcached -d -m 256 -p 11211 -u root
32
accepting connections
bytes 104857600
bytes_written 0
bytes_read 1234567890
get_hits 9876543
get_misses 234567
eviction 1234
curr_connections 32
listen_queue 0
重点盯这几个指标:
get_hits / (get_hits + get_misses) 命中率低于80%说明有问题
eviction 大于0说明内存不够用,有淘汰
curr_connections 接近最大连接数要加连接池
第2步:内存分配不是越大越好
# CentOS/RHEL 系统查看当前内存分配
grep "memcached" /etc/sysconfig/memcached
# 输出
PORT="11211"
USER="memcached"
MAXCONN="1024"
CACHESIZE="64"
OPTIONS=""
# Ubuntu 系统
grep -r "memcached" /etc/memcached.conf
# 输出
-d
-m 64
-p 11211
-u memcached
-c 1024
64MB缓存?在生产环境这是找死。但也别傻乎乎给到4G,Memcached单实例超过2G slab重分配会卡死。建议分片部署,单实例不超过1G。
# 修改内存配置(CentOS/RHEL)
sed -i 's/CACHESIZE="64"/CACHESIZE="1024"/' /etc/sysconfig/memcached
# 修改内存配置(Ubuntu)
sed -i 's/^-m 64$/-m 1024/' /etc/memcached.conf
# 重启生效
systemctl restart memcached # CentOS/RHEL
systemctl restart memcached # Ubuntu
# 验证
memcached-tool 127.0.0.1:11211 settings | grep -E "limit_maxbytes|chunk_size"
预期输出:
limit_maxbytes 1073741824
chunk_size 48
第3步:网络参数调优,别让连接排队
# 查看当前连接队列
netstat -an | grep 11211 | grep TIME_WAIT | wc -l
# 查看监听队列
ss -ltn | grep 11211
预期输出:
1234
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 0.0.0.0:11211 0.0.0.0:*
Send-Q 是128?如果你的QPS超过1万,这里会爆。调大它:
# 在 /etc/sysctl.conf 添加
cat >> /etc/sysctl.conf << 'EOF'
# Memcached network tuning
net.core.somaxconn = 1024
net.core.netdev_max_backlog = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.ip_local_port_range = 10240 65000
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 30
EOF
# 生效
sysctl -p
# 启动参数加上 -c 4096(最大连接数)
# CentOS/RHEL
sed -i 's/MAXCONN="1024"/MAXCONN="4096"/' /etc/sysconfig/memcached
# Ubuntu - 修改 OPTIONS
sed -i 's/OPTIONS=""/OPTIONS="-c 4096"/' /etc/memcached.conf
systemctl restart memcached
第4步:启用水大物博的二进制协议
# 文本协议 vs 二进制协议,QPS能差40%
# Python客户端为例,启用二进制协议
python3 << 'EOF'
from pymemcache.client.base import Client
# 老写法(文本协议)
# client = Client(('127.0.0.1', 11211))
# 新写法(二进制协议)
client = Client(
('127.0.0.1', 11211),
serializer=lambda k, v: (v.encode('utf-8'), 1),
deserializer=lambda k, v, f: v.decode('utf-8'),
connect_timeout=2,
timeout=3,
no_delay=True
)
# 测试写入性能
import time
start = time.time()
for i in range(10000):
client.set(f"key_{i}", f"value_{i}")
elapsed = time.time() - start
print(f"写入10000条耗时: {elapsed:.2f}s, QPS: {10000/elapsed:.0f}")
EOF
预期输出:
写入10000条耗时: 2.34s, QPS: 4273
第5步:做连接池,别每次新建TCP
# Java客户端连接池配置
# 在项目的 application.yml 或代码中配置
// 错误示范:每次请求创建新连接
public class BadExample {
public String get(String key) {
MemcachedClient client = new MemcachedClient(
new InetSocketAddress("127.0.0.1", 11211)
);
return (String) client.get(key); // 每次new,连接开销巨大
}
}
// 正确示范:全局连接池
public class GoodExample {
private static MemcachedClient client;
static {
// 连接池大小根据CPU核数和QPS调整,一般8-32
ConnectionFactoryBuilder builder = new ConnectionFactoryBuilder();
builder.setDaemon(true);
builder.setFailureMode(FailureMode.Redistribute);
client = new XMemcachedClient(
builder.build(),
AddrUtil.getAddresses("127.0.0.1:11211 127.0.0.1:11212")
);
// 最小连接数保持常驻,避免冷启动慢
client.setOpTimeout(3000);
}
public String get(String key) {
return (String) client.get(key);
}
}
预期QPS对比:单连接 vs 连接池能差5-10倍
单连接 QPS: 3241
连接池(8) QPS: 28456
连接池(16) QPS: 41523
第6步:开启UDP小包,加速读请求
# UDP适合小数据(<1KB)的get请求,比TCP少了握手开销
# 只读场景特别有效
# 检查当前是否支持UDP
memcached-tool 127.0.0.1:11211 settings | grep udp
# 默认关闭,启用UDP监听
# CentOS/RHEL - 修改配置
cat >> /etc/sysconfig/memcached << 'EOF'
OPTIONS="-U 11211 -m 1024 -c 4096"
EOF
# Ubuntu
sed -i 's/OPTIONS=""/OPTIONS="-U 11211"/' /etc/memcached.conf
systemctl restart memcached
# 验证UDP端口监听
ss -uln | grep 11211
预期输出:
State Recv-Q Send-Q Local Address:Port Peer Address:Port
UNCONN 0 0 0.0.0.0:11211 0.0.0.0:*
第7步:监控+告警,永远在线的保障
# 用nagios/ Prometheus监控这些关键指标
#!/bin/bash
# check_memcached_stats.sh - Nagios插件示例
HOST="$1"
PORT="${2:-11211}"
WARN_HIT_RATE="${3:-80}"
# 获取命中率
STATS=$(echo "stats" | nc -w2 $HOST $PORT | grep -E "^stat get_hits|^stat get_misses")
# 解析计算
HIT=$(echo "$STATS" | grep get_hits | awk '{print $3}')
MISS=$(echo "$STATS" | grep get_misses | awk '{print $3}')
TOTAL=$((HIT + MISS))
HIT_RATE=$(echo "scale=2; $HIT * 100 / $TOTAL" | bc)
if [ $HIT_RATE -lt $WARN_HIT_RATE ]; then
echo "CRITICAL: Hit rate ${HIT_RATE}%, expect >${WARN_HIT_RATE}%"
exit 2
else
echo "OK: Hit rate ${HIT_RATE}%"
exit 0
fi
# 使用示例
./check_memcached_stats.sh 127.0.0.1 11211 80
预期输出:
OK: Hit rate 95.23%
三、常见问题FAQ
Q1: 命中率从95%突然掉到60%,但缓存数据量没变,咋回事?
这一般是LRU淘汰或者连接复用出了问题。先跑这个:
# 检查淘汰数和过期数
memcached-tool 127.0.0.1:11211 stats | grep -E "eviction|expired|reclaimed"
# 输出
eviction 12345
expired_unfetched 2345
reclaimed 5678
如果eviction飙升说明内存不够,继续加大 -m 参数。如果eviction很少但命中率还是掉,99%是客户端没做连接池,每次新建连接导致请求分散到不同slab。检查客户端代码,确认使用了单例或连接池模式。
Q2: 业务高峰期Memcached响应变慢,延迟从1ms飙升到100ms+
先确认是不是网络排队:
# 查看监听队列是否积压
ss -ltn | grep 11211
# 或者
netstat -an | grep 11211 | grep LISTEN | wc -l
# 查看TCP重传率
netstat -s | grep -i retransmit
listen queue积压说明somaxconn太小,参照第3步调大。TCP重传多说明网络拥塞或者MTU不匹配,检查机器网络配置。都没问题的话,大概率是Memcached的slab重分配阻塞了——单实例内存别超过1G。
Q3: 怎么判断Memcached已经撑不住,该上集群还是换Redis?
达到以下任一条件就考虑换:
1. 单实例内存需求 > 4GB
2. 需要持久化或事务支持
3. 需要复杂数据结构(List/Hash/Sorted Set)
4. QPS > 50万 且 需要多核并行
5. 需要主从复制自动 failover
如果只是缓存简单KV,Memcached比Redis内存效率高30%左右,继续优化Memcached更划算。但如果已经踩了上面的坑还是性能不达标,直接换Redis,别浪费时间。
Q4: 服务重启后缓存冷启动,打满数据库怎么办?
常见死法是缓存击穿。解决方案:
# 方法1:缓存预热脚本
#!/bin/bash
# preheat_cache.sh
REDIS_HOST="YOUR_REDIS_HOST" # 临时借用Redis做预热源
MC_HOST="127.0.0.1"
MC_PORT="11211"
# 从备份库或者只读副本读取热点数据预加载
mysql -h YOUR_DB_HOST -uYOUR_USER -pYOUR_PASSWORD
-e "SELECT cache_key, cache_value FROM cache_backup LIMIT 10000" |
while read key value; do
echo "set $key 0 3600" | nc $MC_HOST $MC_PORT
done
# 方法2:客户端防击穿(伪代码)
def get_cached(key):
val = memcached.get(key)
if val is None:
# 加锁,只允许一个请求回源
lock = redis.setnx(f"lock:{key}", 1, nx=True, ex=10)
if lock:
val = db.query(key)
memcached.set(key, val, expire=3600)
redis.delete(f"lock:{key}")
else:
time.sleep(0.1) # 等另一个请求回源
return memcached.get(key)
return val
预热脚本执行后命中率能从0%直接拉到80%,数据库QPS不会被打爆。
四、总结
核心要点:
- 内存不是越大越好,单实例控制在1G以内,超过就分片
- 连接池是性能关键,客户端必须复用TCP连接
- 二进制协议比文本协议QPS高40%,生产环境必开
- 网络参数(somaxconn/tcp_tw_reuse)调优能减少连接排队
- UDP模式适合小数据读场景,读多写少时效果明显
- 命中率+eviction+连接数三个指标必须监控
延伸阅读:
调优这事没有银弹,先监控找到瓶颈点,再针对性地改配置。别一上来就16核CPU+32G内存伺候,业务扛不住浪费钱,业务扛得住说明你根本不需要这么大的资源。监控数据驱动决策才是正经路数。