ARM架构离线部署Nginx实战:飞腾平台与银河麒麟V10编译指南
1. 为什么ARM架构的离线部署值得单独写一篇在x86服务器上装nginx多数人的肌肉记忆是apt install nginx或者yum install nginx网络一通、源一配几十秒就完事。但一旦落到ARM架构、又要求离线部署这套肌肉记忆基本全部失效。我最近刚在一个飞腾ARM64平台、系统为银河麒麟V10的环境里完整走了一遍nginx离线部署从依赖梳理、编译参数取舍到systemd托管中间踩的坑比预想的多得多所以把整个过程整理出来。先说清楚这篇内容适合谁看如果你手上有ARM架构的服务器或工控设备运行的是麒麟、统信UOS、openEuler、AlmaLinux这类国产或企业级Linux发行版且目标机器不能连外网需要把nginx完整装上去并跑起来那这篇就是给你写的。它不只是给几条命令而是把为什么这么选哪一步最容易翻车翻车了怎么定位讲透。ARM架构下离线部署nginx核心难点集中在三块一是依赖链的完整性ARM的软件包生态远不如x86丰富很多包在目标系统上根本没有预编译版本二是编译工具链的匹配交叉编译和本机编译的路径完全不同三是运行时的动态库依赖编译通过不等于运行通过。这三块任何一块没处理好都会卡在某个看起来莫名其妙的报错上。我这次采用的是目标机本机编译的方案而不是交叉编译。原因后面会详细展开但先给个结论对于nginx这种依赖不算特别复杂的软件在ARM目标机上直接编译比在x86开发机上搭交叉编译工具链要省心得多尤其是当你对目标系统的glibc版本没有十足把握的时候。下面按实际操作的顺序把整个流程拆开讲。每一步我都会说明操作意图和背后的判断依据你可以直接照着做也可以根据自己环境微调。2. 动手之前必须确认的四件事2.1 确认CPU架构和系统版本的真实情况很多人拿到机器就急着传安装包结果第一步就错了。ARM架构不是一个统一的概念armv7l、aarch64、arm64这些标识对应的指令集和ABI都不一样。你在一台armv7的设备上编译出来的二进制拿到aarch64机器上根本跑不起来。先执行uname -m输出aarch64或arm64说明是64位ARM输出armv7l说明是32位ARM。这两个的编译参数、依赖包、甚至nginx的某些模块支持都有差异。我这次的环境是aarch64也就是常说的ARM64。接着确认发行版和版本号cat /etc/os-release这个命令会输出ID、VERSION_ID这些关键字段。为什么要看这个因为不同发行版的包管理器和依赖命名规则不同。银河麒麟V10基于RPM体系用的是yum或dnf统信UOS有些版本基于Debian体系用的是apt。你后续准备离线依赖包的时候必须按目标系统的体系来准备不能混。还有一个容易被忽略的点glibc版本。执行ldd --version记下这个版本号。如果你打算在开发机上编译好再拷过去glibc版本必须满足编译机不高于目标机的原则否则会出现GLIBC_2.xx not found的经典报错。这也是我最终选择在目标机上直接编译的核心原因——省去版本对齐的麻烦。2.2 判断该用本机编译还是交叉编译这是整个部署方案里最关键的决策点值得单独说清楚。交叉编译的思路是在x86开发机上装一套ARM工具链编译出ARM可执行文件再拷到目标机。优点是编译速度快、开发机环境熟悉。缺点是工具链配置复杂sysroot目标系统的头文件和库必须和目标机完全一致否则编译出来的东西要么跑不起来要么运行时缺库。对于nginx这种依赖OpenSSL、PCRE、zlib的软件交叉编译时这三个依赖也得用ARM版本重新编译工作量成倍增加。本机编译的思路是把源码和依赖包传到ARM目标机上直接在目标机上编译。优点是环境天然一致不存在ABI和glibc不匹配的问题。缺点是ARM机器编译速度慢nginx完整编译大概需要十几到几十分钟取决于机器性能。我的判断标准是这样的场景推荐方案理由目标机性能尚可能装gcc/make本机编译环境一致坑最少目标机资源极度受限装不下编译工具交叉编译只能如此但要做好sysroot对齐需要批量部署几十台同架构机器交叉编译或本机编译后打包编译一次批量分发对glibc版本没把握本机编译规避版本不匹配我这次目标机是飞腾FT-2000/4性能足够所以直接本机编译。如果你确实需要交叉编译工具链的选择上优先用目标系统官方提供的工具链包而不是随便下一个通用工具链这样sysroot的匹配度最高。2.3 离线环境下的依赖包怎么准备离线部署最麻烦的就是依赖。nginx本身依赖三个核心库PCRE正则表达式用于location匹配和rewrite、zlibgzip压缩、OpenSSLHTTPS支持。这三个库在联网环境下一条命令就装了离线环境下必须手动准备。准备方式有两种第一种是在联网的同版本机器上用包管理器下载。以RPM体系为例# 在联网的同版本ARM机器上执行 yum install --downloadonly --downloaddir/tmp/nginx-deps \ gcc make pcre-devel zlib-devel openssl-devel这样会把所有依赖的rpm包下载到指定目录连同依赖关系一起。然后把这些rpm包拷到离线机器上用rpm -ivh *.rpm批量安装。注意--downloadonly下载的包可能不包含已经被安装的依赖所以最好在一台干净的同版本机器上操作或者用yumdownloader配合--resolve参数把依赖树完整拉下来。第二种是直接下载源码包手动编译依赖。这种方式更可控不依赖目标系统的包管理器。nginx官方推荐的三个依赖源码包pcrepcre2-10.42.tar.gz注意nginx 1.21.5之后推荐用pcre2zlibzlib-1.3.tar.gzopensslopenssl-3.0.x.tar.gz如果不需要HTTPS可以跳过我这次用的是第二种因为目标系统的软件源里pcre版本偏老直接编译源码更省事。源码包从各自官网下载后和nginx源码一起传到目标机。提示传输文件到离线机器时如果机器有USB接口用U盘是最直接的。如果没有可以通过内网scp传输前提是内网有可达的跳板机。传输完成后记得校验文件完整性md5sum或sha256sum对一下避免传输损坏导致编译报奇怪的错。2.4 编译工具链的最低要求本机编译需要目标机上有基础的编译环境gcc、make、g某些依赖需要。检查一下gcc --version make --version如果这两个命令不存在说明目标机没装编译工具需要先从系统安装介质里找到对应的包装上。银河麒麟V10的安装ISO里通常带有gcc和make的rpm包挂载ISO后从Packages目录里找。这里有个坑gcc版本和glibc版本的匹配。有些国产系统自带的gcc版本比较老编译新版nginx时可能报错。如果遇到编译错误先看错误信息里是不是涉及C标准或头文件如果是考虑升级gcc或者降低nginx版本。我这次用的gcc是7.3.0编译nginx 1.24.x没有问题。3. nginx源码编译的参数取舍3.1 configure阶段每个参数背后的意图nginx的configure脚本参数很多但离线部署场景下真正需要关心的就那么几个。我先把这次用的完整命令贴出来再逐条解释./configure \ --prefix/usr/local/nginx \ --with-pcre/opt/src/pcre2-10.42 \ --with-zlib/opt/src/zlib-1.3 \ --with-openssl/opt/src/openssl-3.0.13 \ --with-http_ssl_module \ --with-http_v2_module \ --with-http_gzip_static_module \ --with-http_stub_status_module \ --with-stream \ --with-threads \ --with-file-aio--prefix决定安装路径我习惯放在/usr/local/nginx和系统包管理器安装的路径分开避免冲突。如果你希望用systemd管理这个路径要记好后面写service文件要用。--with-pcre、--with-zlib、--with-openssl这三个参数指向的是源码目录不是安装目录。这是nginx编译的一个特点它会把这三个库静态编译进nginx二进制里所以不需要在目标系统上单独安装这三个库的运行版本。这也是为什么离线部署时推荐用源码方式——编译完的nginx是自包含的依赖最少。--with-http_ssl_module是HTTPS支持如果你的场景需要配置SSL证书这个必须加。--with-http_v2_module是HTTP/2支持现代浏览器基本都走HTTP/2建议加上。--with-http_gzip_static_module用于预压缩文件的直接返回对静态资源服务有性能提升。--with-http_stub_status_module提供状态监控接口运维时很有用建议加上。--with-stream是四层代理模块如果你需要代理数据库、MQTT这类非HTTP流量这个模块必须加。--with-threads启用线程池--with-file-aio启用异步文件IO这两个对高并发场景有帮助。有一个参数要特别提醒不要加--with-cc-opt和--with-ld-opt去指定额外的库路径除非你明确知道自己在做什么。这两个参数在交叉编译时常用本机编译时乱加容易导致链接错误。3.2 依赖库的编译顺序和参数PCRE2、zlib、OpenSSL这三个库nginx在configure阶段只是引用它们的源码目录实际编译时nginx的Makefile会去调用它们的构建系统。所以这三个库不需要提前单独编译安装只要源码目录存在且完整即可。但有一个例外如果你在configure时看到类似checking for PCRE2 library ... not found的报错说明nginx没找到PCRE2的源码。检查--with-pcre指向的路径是否正确以及该目录下是否有configure或CMakeLists.txt文件。OpenSSL的编译稍微特殊一点。nginx在编译时会调用OpenSSL的构建系统如果OpenSSL版本较新3.0编译时间会比较长。如果你不需要HTTPS直接去掉--with-http_ssl_module和--with-openssl编译时间能省一大半。zlib和PCRE2的编译很快基本不占时间。整个nginx编译的时间瓶颈主要在OpenSSL和nginx自身的源码编译上。3.3 编译过程中的常见报错与应对报错一C compiler cc is not found说明gcc没装或不在PATH里。检查gcc --version如果没有先装gcc。报错二the HTTP rewrite module requires the PCRE library说明PCRE没找到。检查--with-pcre路径确认该目录下有configure文件。如果是PCRE1和PCRE2混用的问题注意nginx 1.21.5之后推荐PCRE2老版本nginx用PCRE1。报错三openssl/ssl.h not found说明OpenSSL源码路径不对或者OpenSSL源码不完整。确认--with-openssl指向的目录下有Configure文件注意是大写C。报错四编译到一半报virtual memory exhaustedARM机器内存不足。nginx编译时某些阶段比较吃内存如果机器内存小于2GB可能会触发OOM。解决办法是加swap或者用make -j1单线程编译降低内存峰值。报错五error: for loop initial declarations are only allowed in C99 modegcc版本太老默认C标准不支持在for循环里声明变量。在configure时加--with-cc-opt-stdgnu99或者升级gcc。我这次编译过程中遇到的是OpenSSL 3.0的一个警告提示某些废弃API但不影响编译结果忽略即可。编译完成后objs/nginx就是最终的可执行文件。4. 从编译完成到服务跑起来4.1 安装与目录结构确认编译完成后执行make install这个命令会把nginx安装到--prefix指定的目录。安装完成后目录结构大致如下/usr/local/nginx/ ├── conf/ │ ├── nginx.conf │ ├── mime.types │ └── ... ├── html/ │ ├── index.html │ └── ... ├── logs/ ├── sbin/ │ └── nginx └── ...sbin/nginx是主程序conf/nginx.conf是主配置文件。先验证一下二进制能不能跑/usr/local/nginx/sbin/nginx -v如果输出版本号说明二进制本身没问题。如果报cannot execute binary file说明架构不对检查uname -m和编译时的架构是否一致。如果报No such file or directory但文件明明存在那基本是动态链接器的问题用ldd /usr/local/nginx/sbin/nginx看一下缺哪个库。4.2 动态库依赖的排查方法虽然nginx把PCRE、zlib、OpenSSL静态编译进去了但它仍然依赖系统的glibc和libpthread等基础库。用ldd检查ldd /usr/local/nginx/sbin/nginx正常输出应该类似linux-vdso.so.1 libdl.so.2 /lib/aarch64-linux-gnu/libdl.so.2 libpthread.so.0 /lib/aarch64-linux-gnu/libpthread.so.0 libcrypt.so.1 /lib/aarch64-linux-gnu/libcrypt.so.1 libc.so.6 /lib/aarch64-linux-gnu/libc.so.6 /lib/ld-linux-aarch64.so.1如果看到not found的条目说明缺库。缺的库如果是glibc相关的基本无解只能换编译环境重新编译。如果是其他库找到对应的ARM版本库文件放到/lib或/usr/lib下或者通过LD_LIBRARY_PATH指定路径。注意不要随便从网上下载ARM的so文件往系统里塞版本不匹配会导致更诡异的问题。缺库时优先从目标系统的安装介质里找对应的包。4.3 配置文件的最小可用版本nginx默认的nginx.conf内容比较多离线部署场景下我建议先写一个最小可用版本跑通之后再逐步加配置。最小版本如下worker_processes 1; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; server { listen 80; server_name localhost; location / { root html; index index.html index.htm; } } }这个配置只监听80端口服务html目录下的静态文件。启动nginx/usr/local/nginx/sbin/nginx然后用curl http://127.0.0.1测试如果返回index.html的内容说明服务跑起来了。如果启动报错先看logs/error.log。常见的启动错误有端口被占用bind() to 0.0.0.0:80 failed、配置文件语法错误unexpected end of file、权限不足Permission denied。端口占用用ss -tlnp | grep 80查一下谁占着语法错误用nginx -t检查权限问题检查启动用户是否有绑定80端口的权限非root用户绑定1024以下端口需要CAP_NET_BIND_SERVICE能力。4.4 用systemd托管nginx服务手动启动的nginx在机器重启后不会自动拉起生产环境必须用systemd托管。创建service文件vim /etc/systemd/system/nginx.service内容如下[Unit] Descriptionnginx - high performance web server Afternetwork.target [Service] Typeforking PIDFile/usr/local/nginx/logs/nginx.pid ExecStartPre/usr/local/nginx/sbin/nginx -t -c /usr/local/nginx/conf/nginx.conf ExecStart/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf ExecReload/bin/kill -s HUP $MAINPID ExecStop/bin/kill -s QUIT $MAINPID PrivateTmptrue [Install] WantedBymulti-user.target几个关键点解释一下Typeforking是因为nginx默认以daemon方式启动主进程会fork出子进程后退出systemd需要知道这一点才能正确追踪PID。PIDFile指向nginx的pid文件路径这个路径要和nginx.conf里的pid指令一致默认是logs/nginx.pid。ExecStartPre在启动前先做配置检查配置有问题直接启动失败避免带病上线。创建完成后systemctl daemon-reload systemctl enable nginx systemctl start nginx systemctl status nginx如果status显示active (running)说明托管成功。后续修改配置后用systemctl reload nginx平滑重载不用重启服务。5. 离线部署中最容易翻车的几个点5.1 依赖包版本不匹配导致的编译失败这是离线部署最高频的问题。典型表现是编译到某个依赖时突然报错错误信息里出现undefined reference或者version mismatch。根因通常是你在开发机上准备的依赖包版本和目标系统上已有的库版本冲突。比如目标系统自带了zlib 1.2.11你又编译了一个zlib 1.3nginx链接时可能链接到了系统自带的旧版本导致符号找不到。解决办法是在configure时明确指定依赖源码路径就是前面说的--with-zlib让nginx优先使用你提供的版本。另一个常见情况是PCRE1和PCRE2混用。nginx 1.21.5之前的版本用PCRE1之后的版本推荐PCRE2。如果你下载的nginx版本和PCRE版本对不上configure阶段就会报错。确认方法看nginx源码目录下的auto/lib/pcre/conf文件里面会写明支持的PCRE版本。5.2 编译通过但运行时报GLIBC版本错误这个坑的隐蔽性很强。编译时一切正常二进制拷到目标机后运行报./nginx: /lib/aarch64-linux-gnu/libc.so.6: version GLIBC_2.28 not found原因是编译机的glibc版本高于目标机。比如你在Ubuntu 20.04glibc 2.31上编译拿到CentOS 7glibc 2.17上跑必然报这个错。规避方法只有一个在glibc版本不高于目标机的机器上编译。最稳妥的就是在目标机本机编译。如果必须交叉编译编译机的glibc版本要选目标机同版本或更低版本。已经编译好了才发现这个问题怎么办两个选择一是换低版本glibc的机器重新编译二是用patchelf工具修改二进制的interpreter和rpath但这属于高级操作容易引入新问题不推荐在生产环境用。5.3 端口权限与SELinux的隐形拦截nginx启动时报Permission denied但你是用root启动的这就很蹊跷。两个可能一是SELinux拦截二是端口被其他进程占用但报错信息不明确。SELinux的检查方法getenforce如果输出Enforcing说明SELinux在 enforcing 模式。nginx绑定非标准端口比如8080时SELinux可能拦截。临时排查用setenforce 0切到permissive模式如果问题消失说明是SELinux的问题。永久解决需要配置SELinux策略或者把nginx加到允许绑定端口的列表里semanage port -a -t http_port_t -p tcp 8080端口占用的排查用ss -tlnp比netstat更直观。如果看到80端口被httpd或别的nginx进程占着先停掉那个进程再启动。5.4 日志目录权限导致的启动失败nginx启动时需要写logs/error.log和logs/nginx.pid。如果logs目录的属主不是启动用户或者权限不对启动会失败。报错信息通常是nginx: [emerg] open() /usr/local/nginx/logs/error.log failed (13: Permission denied)解决办法chown -R nginx:nginx /usr/local/nginx/logs chmod 755 /usr/local/nginx/logs如果你用root启动一般不会有这个问题。但生产环境不建议用root跑nginx建议创建专用用户useradd -r -s /sbin/nologin nginx chown -R nginx:nginx /usr/local/nginx然后在nginx.conf里设置user nginx;并用systemd的Usernginx指定运行用户。6. 部署完成后的验证与加固6.1 功能验证的完整清单服务跑起来只是第一步还要验证核心功能是否正常。我习惯按这个清单过一遍验证项命令预期结果版本确认nginx -v输出版本号配置语法nginx -tsyntax is ok静态服务curl http://127.0.0.1返回index.html状态接口curl http://127.0.0.1/status返回连接数信息HTTPScurl -k https://127.0.0.1返回页面需配SSL反向代理curl http://127.0.0.1/api返回后端响应开机自启systemctl is-enabled nginxenabled状态接口需要在配置里加location /status { stub_status on; allow 127.0.0.1; deny all; }反向代理的配置示例location /api { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }6.2 性能相关的几个调优参数ARM平台的nginx调优和x86有些差异主要是CPU核心数和内存带宽的特点不同。几个关键参数worker_processes建议设为CPU核心数或者auto让nginx自动检测。ARM服务器通常核心数较多设太小浪费性能设太大增加上下文切换开销。worker_connections决定每个worker能处理的最大连接数默认1024。高并发场景可以调到65535但要注意系统的ulimit -n限制需要同步调大。sendfile on和tcp_nopush on对静态文件服务有提升ARM平台同样适用。keepalive_timeout根据业务场景调整静态资源服务可以设长一点API服务设短一点。worker_rlimit_nofile建议设为worker_connections的两倍避免文件描述符不够用。这些参数没有万能值需要根据实际压测结果调整。我这次的环境是8核ARMworker_processes设为8worker_connections设为10240跑静态资源服务很稳。6.3 离线环境下的日志轮转配置nginx的日志会一直增长离线环境下没有logrotate的默认配置需要手动配。创建/etc/logrotate.d/nginx/usr/local/nginx/logs/*.log { daily missingok rotate 30 compress delaycompress notifempty create 0640 nginx nginx sharedscripts postrotate [ -f /usr/local/nginx/logs/nginx.pid ] kill -USR1 cat /usr/local/nginx/logs/nginx.pid endscript }kill -USR1是nginx的日志重开信号轮转后通知nginx重新打开日志文件。rotate 30保留30天compress压缩旧日志节省空间。这个配置在离线环境下同样有效logrotate是系统自带工具不需要额外安装。6.4 后续升级的注意事项离线环境下升级nginx不能直接覆盖二进制因为正在运行的进程还在用旧文件。正确流程是编译新版本nginx安装到新目录比如/usr/local/nginx-new用新二进制做配置检查/usr/local/nginx-new/sbin/nginx -t -c /usr/local/nginx/conf/nginx.conf平滑升级kill -USR2 $(cat /usr/local/nginx/logs/nginx.pid)这会启动新的master进程旧master进程会保留确认新进程正常后kill -QUIT旧master的pid如果新版本有问题kill -HUP旧master可以回滚这个流程的关键是新旧二进制共存升级过程中服务不中断。离线环境下升级前务必在测试环境验证新版本的配置兼容性特别是如果你用了第三方模块模块的ABI可能随nginx版本变化。7. 一些个人踩坑后的实在建议编译参数不要贪多。我一开始想把所有模块都加上结果编译时间翻倍还引入了几个不必要的依赖。后来精简到只保留实际用到的模块编译时间从40分钟降到15分钟。按需编译是离线部署的基本原则每多一个模块就多一份依赖风险。依赖源码包的版本要固定。不要用latest要用明确的版本号。我这次用的pcre2-10.42、zlib-1.3、openssl-3.0.13都是经过验证的稳定版本。版本固定后下次在同类环境部署可以直接复用不用重新试错。编译完成后先别急着make install在源码目录下用objs/nginx -V看一下编译参数确认所有需要的模块都编进去了。-V会输出完整的configure参数比-v详细得多。这个习惯帮我避免过好几次以为编进去了其实没有的情况。systemd的service文件里ExecStartPre那行配置检查很有用但要注意如果nginx.conf里有语法错误systemctl start会直接失败错误信息在journalctl -u nginx里看。养成改完配置先nginx -t的习惯能省很多排查时间。最后说一个传输文件的细节用U盘拷贝时注意文件系统格式。FAT32不支持大于4GB的单文件如果你的源码包或依赖包超过4GBU盘要格式化成exFAT或NTFS。另外Linux下挂载U盘后文件权限可能变成rwxrwxrwx拷贝到目标目录后记得修正权限否则编译时可能报权限错误。这套流程我在飞腾ARM64加银河麒麟V10的环境上完整跑通过从零开始到服务稳定运行大概花了两个小时其中编译占了大部分时间。如果你环境类似照着走应该不会有大问题。遇到报错先看日志nginx的error.log和系统的journalctl是最可靠的信息来源比在网上搜报错信息靠谱得多。