服务公告

服务公告 > 综合新闻 > Memcached:性能优化

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内存伺候,业务扛不住浪费钱,业务扛得住说明你根本不需要这么大的资源。监控数据驱动决策才是正经路数。