1. 为什么我宁愿多花二十分钟也要源码编译安装 Memcached先把结论摆出来编译安装 Memcached 这件事在容器化和包管理高度成熟的今天依然不是老派工程师的怀旧仪式而是解决具体问题的必要手段。我最早接触缓存服务那会儿图省事直接装了系统源里的包跑起来确实没毛病直到线上出现一个诡异的现象——同样的配置两台机器上的内存占用曲线完全不一样一台平稳一台隔几小时就抖一下。折腾了两天才定位到根因发行版打包时链接的 libevent 版本和内核事件机制跟我预期的完全不是一回事而我所有基于默认行为做的容量估算全都建立在错误的前提上。这件事之后我把手上所有缓存节点的部署方式统一改成了源码编译。理由很朴素我需要知道自己装的到底是什么。源码编译安装 Memcached 给你的不是更新的版本这么简单的东西而是编译期的每一个开关都握在自己手里——事件库链接哪个、是否启用 SASL 认证、是否开启扩展存储、二进制要不要带调试符号、堆内存分配器用系统自带的还是 jemalloc。这些东西在打包版本里是别人替你做的决定出了问题时你连他当初为什么这么选都无从查起。这篇内容适合三类人看第一类是刚开始接触缓存服务、想搞明白编译安装到底比装包多了什么的同学第二类是在做环境标准化、需要一套可复现编译流程的运维或平台同学第三类是遇到过版本不一致、参数不生效、启动就崩这类问题想从根上把变量控制住的人。我会把 configure 的每个关键参数为什么这么选、编译完怎么验证、跑起来之后哪些参数必须重新算一条条摊开讲。需要提前说明的是文中涉及的具体数值和目录规划是基于我在常见生产环境中的实践总结你可以照着抄但一定要结合自己的机器规格重新算一遍尤其是内存相关的参数直接照搬别人的数字是最容易出事的。还有一个前置认知得讲清楚Memcached 本身是个极简的东西——纯内存、无持久化、单进程多线程、协议简单到用 telnet 就能操作。它的复杂度不在自身而在你和它之间的那层适配事件模型、内存分配、连接管理、key 分布策略。编译安装的意义就在于这层适配的每一个环节你都能看得见、改得动。2. 动手之前先把环境盘清楚依赖、账户与内核参数2.1 编译器与构建工具别在这一步省事编译 Memcached 需要的构建工具链其实不多但版本别太老。核心是gcc或clang、make、autoconf、automake、libtool、pkg-config这几样。很多人卡在第一步不是缺东西而是pkg-config找不到 libevent 的.pc文件导致 configure 阶段直接报libevent not found明明libevent-devel已经装上了。我的做法是先统一装一套基础工具再单独确认 pkg-config 的搜索路径# 以常见的 RPM 系为例其他发行版换成对应的包管理器即可 sudo yum install -y gcc gcc-c make autoconf automake libtool pkg-config wget # 确认版本gcc 建议 4.8 以上太老的编译器对 C99 支持不完整 gcc --version make --version pkg-config --version这里有个细节值得说autoconf和automake只有在你要从 Git 仓库源码直接构建、需要重新生成configure脚本时才用得上。如果下载的是官方发布的.tar.gz发行包里面已经带好了configure理论上不用装 autotools。但我还是建议装上——因为你后面很可能会想打补丁或者改点源码那时候现装工具链会更麻烦。提前装好工具的成本是几十兆磁盘事后补装的成本是重新走一遍排查流程。2.2 libevent整个编译过程中最关键的第三方依赖Memcached 的网络事件处理完全依赖 libevent这是唯一一个必须有、且版本影响很大的外部库。经验上libevent 2.0 以上是底线我一般要求 2.1.x 或更新。原因在于 2.1 版本之后对 epoll 的封装做了不少改进在高连接数下的表现明显更稳。如果发行版自带的 libevent 版本够新直接装libevent-devel就行如果版本低于 2.1 而你又不方便升级系统库那就得自己编一份装到独立目录然后在 configure 时用--with-libevent指过去。这两种方式我都在不同环境用过说下区别方式优点代价适用场景使用系统 libevent 包装完即用系统统一升级版本受发行版限制可能偏旧对版本无特殊要求、追求省事源码编译独立 libevent版本可控可与其他软件隔离需额外维护一份库注意运行期动态库路径系统库版本过旧或需多版本共存独立编译 libevent 的时候--prefix建议放在一个语义明确的目录比如/opt/libevent-2.1而不是随手扔进/usr/local。因为/usr/local下面很容易堆积多个软件的产物后面排查到底加载了哪个 .so时会很痛苦。编完之后记得把它加进动态库搜索路径否则 Memcached 启动时会直接报error while loading shared libraries# 编译安装独立 libevent ./configure --prefix/opt/libevent-2.1 --disable-openssl make -j$(nproc) sudo make install # 让运行期能找到它写入 /etc/ld.so.conf.d/ 更持久 echo /opt/libevent-2.1/lib | sudo tee /etc/ld.so.conf.d/libevent-local.conf sudo ldconfig注意ldconfig之后一定要用ldconfig -p | grep libevent确认新路径确实生效了。我见过好几次编完库、加了 conf、但忘了执行ldconfig然后 configure 测试通不过白白怀疑了半天参数。2.3 专用运行账户与目录规划Memcached 不应该用 root 跑这是硬规矩。建一个不能登录的专用账户把数据目录其实只是 pid 文件和可能的日志位置和运行时目录分开规划后面写 systemd unit 会省很多事。sudo useradd -r -s /sbin/nologin -M memcached sudo mkdir -p /var/run/memcached sudo chown memcached:memcached /var/run/memcached目录规划上我的习惯是二进制装到/usr/local/memcached-版本号这种带版本号的目录然后用一个软链接/usr/local/memcached指向当前使用的版本。这样做的好处是升级的时候新版本装到新目录改一下软链接再重启服务回滚就是把软链接改回去一分钟的事。比起覆盖安装然后祈祷的做法这个习惯在我手上救过至少两次场。2.4 内核和资源限制编译前就该调别等启动报错有相当一部分Memcached 启动失败的问题根子不在编译而在系统层面。两个参数建议在编译之前就调好免得后面测试时反复怀疑是自己参数写错了。第一个是最大文件描述符数nofile。Memcached 每个连接占一个 fd加上监听 fd 和内部使用的你要支撑多少并发连接nofile就得比它高出一截。放到/etc/security/limits.conf里给memcached用户单独提权或者在 systemd unit 里用LimitNOFILE指定后者更干净。第二个是内存锁定memlock。如果你打算用-L参数开启大页内存就必须给足够大的memlock限制否则启动时直接失败。不过大页内存这东西适合内存规格固定、长期稳定运行的大实例普通场景不建议开反而增加不确定性。第三个容易被忽略的是连接队列相关参数。并发上来时如果somaxconn太小新连接会被丢弃客户端看到的是偶发连接超时这种问题最难查因为压力测试时可能复现不出来。3. configure 参数逐个拆解每个开关背后的取舍3.1--prefix决定的不只是安装位置./configure --prefix/usr/local/memcached-1.6.21这个参数看起来人畜无害但它决定了后面二进制、man 手册、可能的 include 文件全部落在哪里。用带版本号的目录配合软链接切换是我推荐的稳定做法。如果你图省事写成--prefix/usr/local那么make install会直接往/usr/local/bin里扔文件跟系统已有的同名程序混在一起版本管理瞬间失控。另一个容易踩的点是--prefix指定的目录不存在也没关系make install会自动创建但如果你给的路径没有写权限它会在编译的最后一步才失败白等几分钟。养成习惯configure 之前先确认目标父目录可写。3.2--with-libevent与事件模型的对应关系./configure --prefix/usr/local/memcached-1.6.21 \ --with-libevent/opt/libevent-2.1这个参数指向 libevent 的安装前缀configure 会去prefix/include找头文件、去prefix/lib找库文件。如果不显式指定它会走 pkg-config 和系统默认路径。前面提到的明明装了却找不到问题大多是因为装在了非标准路径却没走 pkg-config。判断是否真的链接对了最可靠的方法不是看 configure 输出而是编译完之后用ldd查二进制ldd /usr/local/memcached-1.6.21/bin/memcached | grep libevent # 期望输出指向你指定的那份 libevent而不是 /usr/lib64 下的旧版本这个检查我每次必做。曾经有一次configure 输出里显示的 libevent 版本是新编的但ldd出来仍然是系统旧库——原因是系统库里那份的 soname 优先级更高。这种情况下程序能跑但用的不是你以为的库非常隐蔽。3.3 那些可选特性开之前先想清楚有没有人用Memcached 有几个编译期开关开了会引入额外依赖或者改变运行行为逐个说下我的取舍--enable-sasl启用 SASL 认证。如果缓存里放的是敏感数据、网络环境不完全可信这个值得开。代价是需要额外装libsasl2-dev并且启动时得配合-S参数和配置文件。如果服务只在本地回环或者严格隔离的内网里跑开了反而增加运维负担我个人在纯内网场景通常不开。--enable-extstore启用外部存储引擎把冷数据溢出到磁盘。这是个有争议的特性官方文档也写得很克制。我的观点是如果你需要持久化说明选型方向本身就该换个组件用 Memcached 去追持久化等于用跑车拉货。我基本不开。--enable-dtraceDTrace 探针用于深度性能分析。生产的机器一般不开但在性能压测环境里开一下很有价值能直接看到内存分配和连接处理的内部调用分布。要开的话记得装好对应的系统开发包。--enable-64bit在 64 位平台上扩大计数器和部分内部结构的位宽。只要你的机器是 64 位且内存规格超过 4G这个基本属于该开就开代价可以忽略。--enable-debug会打开大量断言和调试检查性能直接掉一大截只用于问题复现绝不能带到生产二进制里。3.4 编译期优化参数别乱加也别不加CFLAGS的设置有讲究。默认的-O2对 Memcached 来说已经足够加-O3收益很小反而可能因为激进的向量化让某些代码路径的调试信息变得难以理解。真正值得加的是-g——不是为了让程序更慢而是保留符号表将来出了问题能用gdb或者perf看到函数名而不是一串地址。CFLAGS-O2 -g -fno-omit-frame-pointer ./configure --prefix/usr/local/memcached-1.6.21-fno-omit-frame-pointer这一项是为了让火焰图采样能正确回溯调用栈属于平时看不出用、出事时救命的设置。二进制会大一点点完全值得。如果你的机器是新架构 CPU加-marchnative能榨出一点性能但要小心编出来的二进制绑定了这台机器的指令集拷到老机器上会直接报非法指令。只在单机部署且确认不会迁移的情况下用。4. 从 make 到跑通第一个请求完整链路与验证方法4.1 下载与完整性校验官方发布的压缩包一般会附带校验信息。这一步很多人跳过我的建议是别跳——尤其你在做环境标准化、要把包分发到多台机器上的时候一个字节的差异都可能让你在半年后排查一个莫名其妙的 bug。wget https://example-dist.example.org/memcached-1.6.21.tar.gz wget https://example-dist.example.org/memcached-1.6.21.tar.gz.sha256 sha256sum -c memcached-1.6.21.tar.gz.sha256 tar -xzf memcached-1.6.21.tar.gz cd memcached-1.6.21上面的下载地址是占位写法实际请以你获取发行包的渠道为准。4.2 configure 与 make 的实际执行细节把参数组合起来一条完整命令大致长这样CFLAGS-O2 -g -fno-omit-frame-pointer \ ./configure --prefix/usr/local/memcached-1.6.21 \ --with-libevent/opt/libevent-2.1 \ --enable-64bit \ --disable-coverage \ --disable-docs make -j$(nproc) sudo make install--disable-docs是为了跳过文档构建能省下对 docbook 之类工具链的依赖。如果你需要 man 手册就别加这个参数并且提前把相关工具装好。make阶段的并行度用-j$(nproc)就好Memcached 代码量不大普通机器几十秒就能编完。如果 make 报错第一件事不是改源码而是往上翻找第一条 error中间那一大片 warning 基本都是噪音。这里说个我踩过的坑有次在资源受限的小规格机器上编译make -j开了太多并行任务导致 OOM报错信息看起来像编译器内部错误internal compiler error非常唬人。实际原因就是内存不够。遇到莫名其妙的编译器崩溃先降并行度再试一次能省很多排查时间。4.3 安装产物核对清单make install跑完之后不要急着启动花半分钟核对下产物ls -l /usr/local/memcached-1.6.21/bin/ # 应该能看到 memcached 可执行文件 /usr/local/memcached-1.6.21/bin/memcached -h | head -20 # 看版本号和编译参数是否符合预期-h输出里会列出编译时启用的选项这是最直接的我到底编出了什么的证据。我习惯把这行输出存进部署记录将来对比不同机器的差异时非常好用。4.4 前台试跑与最小验证正式的 systemd 托管之前先用前台模式跑一次把参数问题暴露在最容易看日志的阶段/usr/local/memcached-1.6.21/bin/memcached \ -u memcached \ -m 1024 \ -p 11211 \ -l 127.0.0.1 \ -c 1024 \ -vv-vv会打印非常详细的请求日志只适合本地调试生产不要开。另开一个终端验证# 查看基本统计 echo -e stats\r | nc 127.0.0.1 11211 # 写入和读取各一次 printf set foo 0 0 3\r\nbar\r\nget foo\r\n | nc 127.0.0.1 11211看到STORED和VALUE foo 0 3加bar的返回基本链路就通了。这一步看着简单但能帮你把编译错了libevent 没链接对账户权限不对端口被占这几类问题在几分钟内全部筛掉。5. 交给 systemd 托管权限边界与参数换算5.1 启动账户与最小权限前面建的memcached账户在这里派上用场。需要注意的是如果用-u参数指定用户Memcached 会自己降权但在 systemd 场景下更推荐直接用User指定避免两套权限机制打架。用-u的时候如果启动账户本身权限不足去切换会报cant run as root without the -u switch之类让人一头雾水的错误。5.2 一份可以直接改改就用的 unit 文件[Unit] DescriptionMemcached Cache Service Afternetwork.target [Service] Typesimple Usermemcached Groupmemcached ExecStart/usr/local/memcached/bin/memcached -m 4096 -p 11211 -l 0.0.0.0 -c 4096 -t 4 -I 2m LimitNOFILE65536 Restarton-failure RestartSec3 [Install] WantedBymulti-user.target几个参数值得单独说明-t 4是工作线程数经验值是 CPU 核数超过核数收益递减甚至可能因为上下文切换变差-I 2m调整单个 item 的大小上限默认 1m如果你的业务里有大 value不改这个会直接写入失败报SERVER_ERROR object too large for cache。-I调大之后要注意所有 slab class 的尺寸分布会跟着变内存利用率可能下降。这是很多人不知道的副作用。调大 item 上限意味着最小的 slab class 起点变高小 key 占用同样多的内存实际能存的条数会减少。所以-I不是越大越好按业务里最大的那个 value 尺寸来定留一点余量就够。5.3 内存与连接数参数怎么算-m是总内存上限-c是最大并发连接数。这两个数字不能拍脑袋。连接数的算法每个连接除了 fd 之外还有固定的内存开销连接结构体、读写缓冲区经验上按每条连接几十 KB 估。所以-m 4096配-c 10000的时候光连接本身就可能吃掉几百兆实际可用的缓存空间会比你以为的少。大连接数加上大内存的配置一定要在压测里实测过内存曲线才敢上线。内存上限-m还有个隐性约束它是给 item 存储用的进程本身还有额外的常驻开销代码段、线程栈、哈希表。所以机器上不要把它设成接近物理内存的值留出至少 20% 给系统和进程本身。我有次把-m设成物理内存的 90%结果机器在负载高峰期开始出现严重的内存回收压力整个系统响应都受影响——缓存服务把宿主机的内存挤爆了这属于典型的局部最优害了全局。6. 踩坑实录那些编译和启动阶段的经典报错6.1 符号找不到libevent 版本错配的典型表现报错长这样error while loading shared libraries: libevent-2.1.so.7: cannot open shared object file或者启动瞬间崩掉日志里提示某个event_*符号未定义。根因就两种动态库路径没配或者链接时用了一份、运行时加载了另一份。排查链路我是这么走的第一步ldd看二进制实际依赖哪些 .so第二步ldconfig -p | grep libevent看系统里注册了哪几份第三步对比两者路径是否一致。八成情况是第二步缺了你编的那份。修法就是补ld.so.conf.d里的配置再ldconfig。还有一种更隐蔽的编译时链接的头文件版本和库版本不一致——头文件是新的、库是旧的或者反过来。症状是编译能过、运行时行为诡异。这时候pkg-config --modversion libevent和ldd的结果要交叉比对两个都对上了才放心。6.2 内存分配器系统 malloc 还是 jemallocMemcached 内部有自己的 slab 分配器管理 item 内存但它自身进程的堆分配哈希表扩容、连接结构体创建走的是系统 malloc。高并发场景下换用 jemalloc 能明显减少内存碎片和分配延迟。做法是在编译时通过链接参数引入CFLAGS-O2 -g LDFLAGS-L/usr/local/jemalloc/lib \ ./configure --prefix/usr/local/memcached-1.6.21 \ --with-libevent/opt/libevent-2.1然后ldd确认二进制里确实带了libjemalloc。这里要提醒一句jemalloc 不是万灵药。如果你的实例连接数不大、QPS 中等系统自带分配器的表现完全够用贸然引入只是多一个依赖。判断要不要换看两个指标长时间运行后 RSS 是否持续缓慢上涨碎片以及高并发下的 P99 延迟是否有明显的抖动。这两条都命中再考虑换。6.3 大内存实例启动失败从报错到根因的完整路径症状-m 32768这种大内存配置下进程启动即退出日志只给了一句很含糊的失败提示。排查顺序我固定走这四步看是不是大页内存的问题。如果参数里带了-L先确认/proc/meminfo里的HugePages_Total是否已经预留。没预留就直接去掉-L再试。看memlock限制。ulimit -l查一下很多环境默认是 64KB开大页必然不够。看是不是内存真的不够。-m加上进程开销超过物理可用内存系统会拒绝大块分配。用free -g确认。看内核的 overcommit 策略。某些环境下vm.overcommit_memory2会让大块分配直接失败这时候要评估整个机器的策略而不是只为这一个服务改。这四步走完基本能定位。我遇过最折腾的一次是第二步——memlock限制在 systemd unit 里没写limits.conf里写了但对 systemd 托管的服务不生效两套配置互相以为对方管了最后是显式加上LimitMEMLOCKinfinity才解决。6.4 连接数打满与 fd 限制的层叠关系客户端报连接被拒绝或者偶发超时先别怀疑服务本身。按这个顺序查检查点命令期望服务进程 fd 上限cat /proc/pid/limits与配置一致不低于 65536系统级 fd 上限cat /proc/sys/fs/file-max远大于服务需求当前已用 fdls /proc/ /fdwc -l连接队列溢出netstat -sgrep -i listen服务端连接数stats里的curr_connections未达到-c最后一项要特别注意stats里的curr_connections包含了监听连接等内部连接所以实际可用会比-c少几个。如果你的业务按-c的精确值做容量规划就会在临界点莫名其妙地被拒绝。7. 上线前的性能基线与版本演进策略7.1 先用简单工具跑出可对比的基线编译完、服务起来之后别急着切流量。先跑一轮基线把数字记下来将来任何变更参数调整、版本升级、机器更换都能拿来做对比。最省事的做法是用自带的memcached-tool部分发行包会带或者简单的脚本灌数据然后用stats观察关键指标# 灌入测试数据并观察 for i in $(seq 1 10000); do printf set key_$i 0 0 10\r\nvalue_$i\r\n | nc -q1 127.0.0.1 11211 /dev/null done echo -e stats\r | nc 127.0.0.1 11211 | grep -E curr_items|bytes|evictions|get_hits|get_misses重点盯三个数evictions淘汰数非零说明内存不够、get_hits/get_misses的比例命中率衡量缓存有效性、bytes实际占用跟-m对比看余量。有专门的压力测试工具能给到 QPS 和延迟分布环境允许的话建议用一轮把 P50、P99、P999 都记录下来。7.2 slab 分布观察比总内存更重要的指标stats items能看到每个 slab class 的 item 数量和淘汰情况。这个输出经常被人忽略但它才是判断内存配得合不合理的关键。echo -e stats items\r | nc 127.0.0.1 11211如果你看到某个 class 的evicted一直在涨而总内存还没用满说明内存被分割成了不合适的尺寸块——这是 Memcached 固定 slab 尺寸分布带来的固有约束。缓解办法有两个一是通过-f调整 slab 的增长因子默认 1.25调小能让尺寸划分更细减少浪费但会增加 class 数量二是从业务侧把 value 尺寸控制在相对集中的范围里。我个人的经验是如果业务里的 value 大小分布很分散从几十字节到几百 KB 都有-f调成 1.15 左右能明显提升内存利用率代价是 slab class 变多管理起来稍微复杂点。这个参数值得花时间实测收益是实打实的。7.3 版本升级与多实例共存因为用了带版本号的安装目录加软链接的方案升级路径非常清晰# 新版本装到新目录 ./configure --prefix/usr/local/memcached-1.6.25 ... make sudo make install # 切换软链接 sudo ln -sfn /usr/local/memcached-1.6.25 /usr/local/memcached sudo systemctl restart memcached出问题就ln -sfn指回旧目录再重启回滚时间以秒计。要注意的是软链接切换之后 systemd 需要daemon-reload吗如果ExecStart写的是软链接路径重启服务时 systemd 会重新解析一般不需要但如果你的 unit 里写死了具体版本路径那就必须改 unit 文件。另一个实践是同一台机器跑多个 Memcached 实例各占不同端口和内存配额。这么做的好处是故障隔离——一个实例出问题不影响其他也方便按业务线做独立配额。多实例场景下记得每个实例的 pid 文件和日志路径要分开否则 systemd 会互相覆盖。用memcached.service这种模板 unit 配合实例名管理起来很清爽。最后分享一个我这些年形成的习惯每次编译安装完把memcached -h的输出、ldd的结果、systemd unit内容、启动参数这几样东西存成一个部署快照文件跟版本号绑在一起。后来遇到这台机器的行为跟那台不一样的问题时直接 diff 两个快照五分钟就能定位差异比登上去一条条查参数快得多。这个习惯本身不产生任何功能但它把环境不一致这类最难查的问题变成了一次文本对比。