在内网环境里维护过老系统的同行大概都遇到过这种局面:一台跑着 CentOS 7 或者 Anolis OS 的机器,业务不能停,外网又不通,偏偏某个中间件或者安全扫描报告指出系统自带的 curl 版本太低,需要升到 8.4.0。这时候 yum 用不了,源码编译又缺一堆依赖,很多人第一反应是"把 rpm 包拷进去装一下不就完了",结果一执行就撞上依赖地狱——libcurl 和 curl 是两套包,nss、openssl、libnghttp2 的版本又互相牵扯,装到一半系统里原有的 curl 被卸掉,连 yum 自己都跑不起来了。
这篇内容就是围绕"离线升级 curl 到 8.4.0"这件事展开的。我会把整个过程的原理、依赖关系、编译参数取舍、升级后的验证方式,以及那些文档里不会写的坑,一条条拆开讲。适合两类人看:一类是手里有内网机器、需要在不联网的前提下完成升级的运维;另一类是想搞清楚 curl 和 libcurl、静态库和动态库、编译选项之间到底怎么回事的技术爱好者。哪怕你之前没编译过 C 项目,跟着思路走也能明白每一步在干什么。
先说清楚一个前提:curl 这个工具本身和它依赖的库是分开的。命令行里敲的curl是一个可执行程序,而大量应用程序(比如 yum、git、各种语言的 HTTP 客户端)链接的是libcurl这个共享库。升级的时候如果只换了可执行文件,库还是老的,那业务侧的问题依然存在;反过来只换库不换命令,手工调试时又会发现版本对不上。所以离线升级的正确姿势是"库和命令一起升,且要保证系统里原有依赖它的程序不被搞崩"。这个平衡点怎么找,后面会详细说。
1. 先搞清楚离线升级到底难在哪
很多人把离线升级想得太简单,觉得无非是把编译好的二进制拷过去。真正动手才会发现,难点根本不在"编译"这个动作本身,而在三个地方:依赖的完整性、系统原有软件的兼容性、以及升级失败后的回退能力。这三件事任何一件没处理好,都可能让一台好好的机器变成需要重装的状态。
1.1 依赖链条:curl 不是孤岛
curl 8.4.0 在编译时会去探测系统里的一堆库,探测到了就编进去,探测不到就跳过对应功能。常见的依赖包括 OpenSSL(负责 HTTPS)、zlib(负责压缩传输)、libnghttp2(HTTP/2 支持)、libssh(SFTP 支持)、brotli 和 zstd(新型压缩算法)等等。在联网环境下这些都好办,yum-builddep一把梭。离线环境下,你得先确认目标机器上这些库的开发包(也就是带.so符号链接和头文件的那种-devel包)到底装没装。
这里有个特别容易踩的坑:运行时库和开发库是两回事。系统里可能装了openssl-libs,程序能正常跑,但没有openssl-devel,头文件缺失,编译 curl 时就会报openssl/ssl.h: No such file or directory。所以离线环境的第一步不是急着编译,而是拿rpm -qa把目标机器上已有的库和开发包列一遍,和编译机上的环境做个对照。
我一般会先在编译机上把 curl 的configure跑一遍,看它探测到了哪些特性,把这份清单记下来,然后到目标机器上逐项核对。探测不到的项要么补装开发包,要么在编译时显式关掉对应功能。关功能这件事要谨慎,比如把 HTTPS 关了,那这个 curl 基本就废了,业务侧但凡有个 https 请求全得挂。
1.2 系统自带 curl 的"绑架"问题
CentOS 和 Anolis 这类系统里,curl 和 libcurl 是基础包,yum、rpm、甚至一些系统服务都依赖它们。你如果直接用rpm -Uvh覆盖安装,rpm 会检查依赖关系,发现新包和旧包的文件路径冲突,或者依赖的库版本对不上,轻则报错中止,重则把旧包卸了新的没装上,系统里 curl 直接消失。更麻烦的是 libcurl 的 soname(共享库的版本标识)如果变了,所有链接它的程序启动时都会报libcurl.so.4: cannot open shared object file。
所以离线升级 curl 有一条铁律:不要动系统包管理器管理的那些文件。正确做法是把新版本编译安装到一个独立的前缀目录,比如/usr/local/curl-8.4.0,然后通过环境变量或者软链接的方式让需要的地方用新版本,系统原有的/usr/bin/curl和/usr/lib64/libcurl.so.4保持原样。这样即使新版本有问题,把软链接一撤就回到原状,风险可控。
1.3 回退方案必须在动手前就准备好
我见过太多人升级到一半发现编译参数选错了,想回退却发现旧版本的文件已经被覆盖,只能重装系统。离线环境尤其要命,因为你没法临时下载一个旧包救急。动手之前,至少要做两件事:一是把/usr/bin/curl和/usr/lib64/libcurl.so.4*这些文件备份一份;二是记录当前 curl 的版本和编译参数(curl -V能看到一部分),万一要回退,照着原样恢复。
提示:备份的时候连同软链接一起备份,用
cp -a保留属性,恢复时用mv而不是cp,避免权限和属主出问题。
2. 编译环境的搭建与依赖核对
离线升级的编译工作通常在一台"编译机"上完成,这台机器可以联网,装好所有依赖,编译出二进制后再拷到目标机器。编译机和目标机器的系统版本、架构(x86_64 还是 aarch64)必须一致,glibc 版本也要接近,否则编出来的程序在目标机器上可能因为符号版本不匹配而跑不起来。
2.1 编译机上的依赖安装清单
在编译机上,我一般会装这么一组包(以 CentOS 7 系为例,Anolis 类似):
yum install -y gcc make perl \ openssl-devel zlib-devel \ libnghttp2-devel libssh-devel \ brotli-devel zstd-devel \ libidn2-devel libpsl-devel这里面每一项都对应 curl 的一个特性:openssl-devel给 HTTPS,zlib-devel给 gzip 压缩,libnghttp2-devel给 HTTP/2,libssh-devel给 SFTP,brotli-devel和zstd-devel给新压缩算法,libidn2-devel给国际化域名,libpsl-devel给公共后缀列表校验。装不全也能编,只是对应功能会被禁用,curl -V里看不到那个特性。
装完之后建议跑一下pkg-config --list-all | grep -E 'openssl|nghttp2|zlib',确认这些库的.pc文件都在,因为 curl 的 configure 脚本会通过 pkg-config 去探测它们。如果某个库装了但 pkg-config 找不到,多半是PKG_CONFIG_PATH没设对,或者装的是运行时包不是开发包。
2.2 目标机器的环境核对
编译机准备好之后,别急着拷二进制,先到目标机器上核对几件事。第一是架构,uname -m看是 x86_64 还是 aarch64,编译机必须一致。第二是 glibc 版本,ldd --version看输出,编译机的 glibc 不能比目标机器新太多,否则编出来的程序会依赖目标机器上没有的符号版本。第三是目标机器上已有的 curl 版本和它链接的库,curl -V和ldd $(which curl)各跑一遍,把结果记下来。
这里有个细节值得说:CentOS 7 自带的 curl 通常是 7.29 这个很老的版本,链接的是 NSS 而不是 OpenSSL。如果你编 8.4.0 时用的是 OpenSSL,那新版本和旧版本在 TLS 实现上就不一样,某些依赖系统证书库路径的业务可能会有细微差异。这不是说不能升,而是升级后要重点验证 HTTPS 请求是否正常,尤其是那些依赖/etc/pki/tls/certs下证书的场景。
2.3 源码包的获取与校验
curl 8.4.0 的源码包从官方发布渠道拿,文件名一般是curl-8.4.0.tar.gz。拿到之后先校验一下,官方会提供 sha256 和 GPG 签名。离线环境里 GPG 校验可能因为缺公钥而麻烦,但 sha256 一定要对一遍:
sha256sum curl-8.4.0.tar.gz把结果和官方公布的值比对,不一致就说明包在传输过程中损坏了,这种包编出来的东西不可信。校验通过后解压,进入源码目录,下一步就是 configure。
3. configure 参数怎么选才不埋雷
configure 是整个编译过程里最需要动脑子的环节。参数选得好,编出来的 curl 功能全、体积适中、和系统兼容;选得不好,要么功能缺失,要么链接了一堆目标机器上没有的库,拷过去直接跑不起来。我下面按"必选"和"可选"分开讲,每个参数为什么这么设都会说清楚。
3.1 前缀目录与安装布局
第一个要定的参数是--prefix。前面说过,为了不污染系统原有文件,新版本装到独立目录:
./configure --prefix=/usr/local/curl-8.4.0这样make install之后,可执行文件在/usr/local/curl-8.4.0/bin/curl,库在/usr/local/curl-8.4.0/lib/,头文件在include/下。整个目录可以打包拷到目标机器的相同路径,互不干扰。
有人会问,为什么不直接--prefix=/usr覆盖系统文件?因为那样make install会往/usr/bin和/usr/lib64里写文件,和 rpm 管理的文件混在一起,后续系统升级或者包管理操作会出乱子,而且回退困难。独立前缀是离线升级场景下最稳妥的选择。
3.2 关键特性开关的取舍
curl 的 configure 支持大量--with-xxx和--without-xxx参数。我的建议是:目标机器上有的库就带上,没有的显式关掉,不要指望 configure 自动探测的结果在目标机器上也成立。因为编译机探测到了某个库,编出来的二进制就会动态链接它,目标机器没这个库,运行时直接报错。
一个比较稳的参数组合是这样的:
./configure --prefix=/usr/local/curl-8.4.0 \ --with-ssl \ --with-zlib \ --with-nghttp2 \ --with-libssh \ --with-brotli \ --with-zstd \ --enable-shared \ --enable-static \ --disable-ldap \ --disable-ldaps逐条解释一下。--with-ssl让 curl 用 OpenSSL 做 TLS,这是 HTTPS 的基础,不能省。--with-zlib支持 gzip 压缩传输,省带宽。--with-nghttp2支持 HTTP/2,现在很多站点默认走 HTTP/2,没有这个特性会退回 HTTP/1.1,性能有差距。--with-libssh支持 SFTP,如果业务不涉及可以关掉减少依赖。--with-brotli和--with-zstd是新型压缩算法,现代 CDN 常用,建议带上。
--enable-shared和--enable-static同时开,是为了既生成.so动态库又生成.a静态库。动态库给需要链接 libcurl 的程序用,静态库在某些不方便带动态库的场景下有用。--disable-ldap和--disable-ldaps是因为 LDAP 支持依赖的库比较重,一般业务用不上,关掉能减少依赖和体积。
注意:如果你不确定目标机器上有没有某个库的开发包,宁可先关掉对应特性,编一个功能稍少但一定能跑的版本,也不要编一个功能全但跑不起来的版本。功能可以后续再补,跑不起来就是事故。
3.3 运行时库路径的处理
编译时还有一个容易忽略的点:-Wl,-rpath。默认情况下,动态链接的程序在运行时去/etc/ld.so.cache和系统默认路径找库。你把新 libcurl 装在/usr/local/curl-8.4.0/lib,程序运行时未必找得到。有两种解决办法:一是设置LD_LIBRARY_PATH环境变量,二是编译时用 rpath 把库路径写死进二进制。
我倾向于用 rpath,因为环境变量容易在 sudo、cron、systemd 这些场景下丢失,rpath 是编进二进制的,更可靠:
./configure --prefix=/usr/local/curl-8.4.0 \ ... \ LDFLAGS="-Wl,-rpath,/usr/local/curl-8.4.0/lib"这样编出来的 curl 可执行文件,运行时优先去/usr/local/curl-8.4.0/lib找 libcurl,不受环境变量影响。代价是这个路径必须和目标机器上的安装路径一致,所以编译机和目标机器的安装前缀要统一。
4. 编译、打包与离线搬运
configure 跑完之后会输出一份特性摘要,把探测到的库和启用的功能列出来。这份摘要一定要看,确认 HTTPS、HTTP/2 这些关键特性都是 enabled 状态,如果发现某个该有的特性是 disabled,说明对应的开发包没装好,回去补装再重新 configure。
4.1 make 与 make install 的实操细节
编译命令就是常规的:
make -j$(nproc) make install-j$(nproc)用满所有 CPU 核心并行编译,能省不少时间。curl 的编译量不算大,一般几分钟就完事。make install会把文件装到--prefix指定的目录。
这里有个小技巧:make install之前可以先make DESTDIR=/tmp/curl-stage install,把文件装到一个临时目录,检查一下目录结构对不对,确认无误再正式装。离线场景下这个检查很有必要,因为装错了要重来,浪费时间。
装完之后,/usr/local/curl-8.4.0目录下应该有bin/curl、lib/libcurl.so.4.x.x、lib/libcurl.a、include/curl/这些内容。用ldd bin/curl看一下它链接的库,确认没有指向编译机上才有的路径。
4.2 打包与传输
打包的时候把整个/usr/local/curl-8.4.0目录打成一个 tar 包:
tar czf curl-8.4.0-offline.tar.gz -C /usr/local curl-8.4.0传输到目标机器的方式看环境而定,U 盘、内网文件服务器、跳板机中转都行。传过去之后解压到相同路径:
tar xzf curl-8.4.0-offline.tar.gz -C /usr/local解压后检查一下文件权限,bin/curl要有可执行权限,lib/下的.so文件要有读权限。如果打包时用了-p保留权限,一般不会有问题,但保险起见还是ls -l看一眼。
4.3 让新版本生效的几种方式
装好之后,怎么让系统用上新版本,有几种做法,各有适用场景。
第一种是直接用绝对路径调用:/usr/local/curl-8.4.0/bin/curl -V。这种方式最安全,完全不影响系统原有 curl,适合先做验证。
第二种是改 PATH,把/usr/local/curl-8.4.0/bin放到 PATH 前面。这样命令行里敲curl用的就是新版本。改 PATH 可以写在/etc/profile.d/下建个脚本,对所有用户生效。但要注意,PATH 的修改对已经运行的进程和某些服务不生效,需要重新登录或者重启服务。
第三种是替换/usr/bin/curl为指向新版本的软链接。这种方式影响面最大,所有调用curl的地方都会用新版本,包括 yum 这类系统工具。风险也最大,如果新版本有问题,yum 可能跟着出问题。我一般不建议在离线升级场景下这么做,除非你非常确定新版本和系统完全兼容。
对于 libcurl,情况更复杂一些。系统里原有的程序链接的是/usr/lib64/libcurl.so.4,你如果把这个软链接指向新版本,所有程序都会用新 libcurl。但新 libcurl 的 soname 如果和旧的不一样(比如从.so.4变成了.so.4但内部符号有变化),可能导致某些程序启动失败。稳妥的做法是保持系统 libcurl 不动,只让明确需要新版本的程序通过 rpath 或者LD_LIBRARY_PATH用新库。
5. 升级后的验证与常见故障排查
装完不等于完事,验证这一步不能省。验证分两个层面:一是 curl 命令本身能不能正常工作,二是系统里依赖 curl/libcurl 的原有程序有没有被影响。
5.1 curl 命令的功能验证
先看版本和特性:
/usr/local/curl-8.4.0/bin/curl -V输出里应该能看到curl 8.4.0,以及Features:后面列出 SSL、HTTP2、brotli、zstd 这些。如果 SSL 没在列表里,说明编译时 OpenSSL 没探测到,HTTPS 用不了,得回去检查。
然后做几个实际请求测试。HTTP 请求测基本连通性,HTTPS 请求测 TLS 握手和证书校验,HTTP/2 请求测协议协商:
/usr/local/curl-8.4.0/bin/curl -I http://example.com /usr/local/curl-8.4.0/bin/curl -I https://example.com /usr/local/curl-8.4.0/bin/curl -I --http2 https://example.comHTTPS 请求如果报证书错误,多半是 CA 证书路径的问题。curl 8.4.0 默认会去几个常见路径找 CA bundle,CentOS 系一般在/etc/pki/tls/certs/ca-bundle.crt。如果找不到,可以用--cacert显式指定,或者检查编译时有没有带对默认路径。
5.2 系统原有程序的兼容性检查
这一步最容易被忽略,但恰恰是离线升级风险最高的地方。升级后要确认 yum、rpm 这些系统工具还能正常跑:
yum --version rpm -qa | head如果这些命令报libcurl.so.4: cannot open shared object file或者类似的错误,说明系统 libcurl 被影响了,得赶紧回退。这也是为什么前面强调不要动系统原有文件——只要系统 libcurl 没被覆盖,这些工具就不会受影响。
另外,如果业务程序链接了 libcurl,升级后要跑一遍业务的核心流程,确认 HTTP 请求正常。有些程序对 libcurl 的版本有要求,新版本可能改了某些默认行为(比如默认的 TLS 版本、重定向策略),这些细微差异在测试环境不一定暴露,生产环境要重点看。
5.3 典型故障与排查思路
离线升级 curl 常见的故障就那么几类,我把排查思路整理成表格,遇到问题可以对照着看:
| 故障现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 运行 curl 报找不到 libcurl.so.4 | rpath 没设或路径不对 | ldd看链接路径,检查 rpath 编译参数 |
| HTTPS 请求报证书错误 | CA bundle 路径不对 | --cacert指定,或检查编译默认路径 |
| 编译时 openssl 探测失败 | 缺 openssl-devel 或 pkg-config 路径不对 | 补装开发包,检查 PKG_CONFIG_PATH |
| yum 报错无法运行 | 系统 libcurl 被覆盖 | 恢复备份的 libcurl.so.4 软链接 |
| 新 curl 功能缺失 | configure 时对应库没探测到 | 看 configure 摘要,补装库重新编译 |
| 目标机器运行报 GLIBC 版本错误 | 编译机 glibc 比目标机新 | 在更老或相同版本的系统上编译 |
排查的核心思路是"分层定位":先确认是编译期问题还是运行期问题,运行期再确认是库找不到还是库版本不对,库版本不对再确认是 glibc 还是 libcurl 本身。ldd、curl -V、strace这几个工具配合使用,大部分问题都能定位到。
6. 几个值得记住的经验点
折腾过几轮离线升级之后,我总结出几条不太会写在文档里、但实际很管用的经验。
第一条,编译机和目标机器的系统版本尽量一致。跨大版本编译(比如在 CentOS 8 上编给 CentOS 7 用)十有八九会撞 glibc 版本问题,省那点装环境的功夫不值得。如果实在没有同版本机器,退而求其次用 Docker 起一个对应版本的基础镜像来编译,比直接跨版本编靠谱得多。
第二条,configure 的摘要输出一定要逐行看。很多人 configure 跑完看到没报错就直接 make,结果编出来的版本缺了关键特性,装到目标机器上才发现 HTTPS 用不了。摘要里每个特性后面都有 enabled 或 disabled,花两分钟核对一遍,能省后面几小时的排查。
第三条,升级前把旧版本的编译参数记下来。curl -V的输出里有一部分信息,但完整的编译参数在curl --version的某些版本里能看到,或者查系统包的 changelog。记下来是为了万一要回退或者对比行为差异时有依据。
第四条,别在生产机器上直接试编译。离线环境里编译工具链可能不全,在生产机器上折腾容易把环境搞乱。正确流程是编译机上编好、打包、传到目标机器解压、验证,每一步都可控。
第五条,libcurl 的升级要单独评估。curl 命令升级相对简单,影响面小;libcurl 升级影响的是所有链接它的程序,风险高一个量级。如果业务只是需要命令行工具有新特性,那就只升命令,库保持不动;如果业务程序明确需要新 libcurl 的某个 API,那再单独规划库的升级,并且做好充分的兼容性测试。
最后说一个实际场景里的细节:有些内网机器的/usr/local分区空间很小,装一个完整的 curl(含静态库和头文件)可能占几十 MB,装之前用df -h /usr/local看一眼剩余空间。空间不够的话,可以在 configure 时加--disable-static不生成静态库,能省不少空间。这种小地方不注意,装到一半报磁盘满,又得清理重来。