1. 半夜被叫醒的那种故障:Apache 启动失败到底在报什么
apache 启动失败这四个字,看上去像是一个故障,实际上是几十个完全不同的问题共用一个表象。我做过几年中间件运维支撑,凌晨被电话叫起来的次数里,Apache 起不来大概能占三成,而真正让人抓狂的不是修复本身,是你敲完systemctl restart httpd之后屏幕上只有一句干巴巴的 "Job for httpd.service failed",什么有用信息都没有。新手到这一步容易慌,开始乱改配置文件,结果本来只是端口被占,硬是改成了语法错误加权限错误,故障面积翻了三倍。
这篇东西就是把我这些年处理 Apache 启动失败的一套固定动作拆开写清楚:先怎么在五分钟内把范围缩到最小,再按六类高频原因逐个对照现象和根因,最后给一份可以直接贴在工位上的速查表。适合刚接手 Linux 服务器、被 httpd 服务折腾过的朋友,也适合已经能改配置但排查思路还比较散的同学。全文命令在 CentOS/RHEL 系的 httpd 和 Debian/Ubuntu 系的 apache2 上都通用,包名和服务名不一样的地方我会标出来,配置文件路径也会分别说明,你按自己机器上的实际名字替换就行。
有个前提认知我想先摆在前面:Apache 启动失败从来不是"Apache 出问题了",而是"启动链路里某一环的条件没有被满足"。这个链路是有固定顺序的,顺序搞明白了,排查就是按顺序验证,而不是撞运气。
1.1 把启动链路拆成四道关,故障必然落在其中一道
不管你用的是 systemd 还是老的 sysv 脚本,Apache 真正起来的全过程都逃不过这四个阶段,我习惯把它们叫四道关。
第一道关是二进制与依赖关。systemd 找到httpd.service这个单元文件,读里面的ExecStart,去执行/usr/sbin/httpd。这一步失败通常是二进制不存在、被误删、依赖的共享库缺失,或者单元文件里的路径写错了。现象是systemctl status里显示Failed to start并且提示No such file or directory。
第二道关是配置解析关。httpd 启动时会读主配置httpd.conf,再 Include 一堆conf.d/*.conf、conf.modules.d/*.conf。任何一个文件里有语法错误、指令拼写错、模块没加载就用了模块专属指令,都会在这一关被拦下来。这一关的特点是报错非常明确,直接告诉你在哪个文件第几行。
第三道关是资源申请关。配置通过了,进程开始向内核要资源:监听 80/443 端口、写 PID 文件到/run/httpd/、打开日志文件、创建子进程。端口被别人占了、没权限绑低于 1024 的端口、/run/httpd目录权限不对、磁盘满了写不了日志,全都死在这一关。这一关的报错关键词是make_sock、Permission denied、No space left on device。
第四道关是运行期关。进程起来了,PID 文件也写了,但立刻就退出了。常见原因是 MPM 配置和实际负载不匹配导致 fork 失败、证书文件读不了、模块初始化崩溃、SELinux 拦住了网络连接。这一关最难查,因为systemctl status可能显示 "active (running)" 然后两秒后变成 "failed",需要去看error_log里有没有caught SIGTERM或者seg fault。
提示:这四道关的判断依据不一样,第一关看
systemctl status的文字描述,第二关看configtest,第三关看journalctl和error_log的末尾,第四关看error_log的完整时间线。别用一种手段去查四种问题,这是最常见的效率浪费。
1.2 排查顺序定死了,就不要随意跳步
我见过太多人一上来就翻error_log,翻了半天发现日志根本没写进去,因为磁盘满了或者目录权限不对。也见过人上来就netstat查端口,结果发现配置文件第 27 行少个引号,根本没走到绑端口那一步。
我自己固化的顺序是这样:先看服务状态摘要 → 再跑一次配置检查 → 然后看系统日志 → 最后才去翻 error_log 的业务细节。这个顺序不是随便排的,它是按照"信息可信度从高到低"和"覆盖范围从大到小"来设计的。
服务状态摘要是最快的一手信息,systemctl status会把退出码、最近的几行 stderr 直接摆出来,很多问题在这一步就能定性。配置检查是唯一能确认"配置本身有没有问题"的手段,它不启动进程,纯解析,零副作用,所以一定要先跑。系统日志(journalctl)记录的是进程启动时的完整 stderr 输出,包括那些没有写进 error_log 的早期错误。最后翻 error_log,是因为它有缓存和缓冲,有时候最新的错误还没落盘,你看到的可能是上一次启动的记录。
这个顺序能保证你在三到五分钟内至少确定"问题在哪一段",后面的深挖才有方向。
2. 五分钟定性:把错误信息读准确的三个动作
排查效率的高低,八成取决于你会不会读错误信息。Apache 的报错其实很守规矩,格式相当统一,只是很多人看到一大段英文就滑过去了,或者只盯着最后一行看。这三个动作练熟了,基本能覆盖 70% 的启动失败场景。
2.1 systemctl status 别只看红字,重点在退出码和最后五行
敲完systemctl status httpd之后,输出分三块:顶部是服务描述和状态行,中间是进程树(如果起来了),底部是最近的日志。绝大多数人只看顶部那个红色的failed,这等于什么都没看到。
真正有价值的是两块。一块是Process: xxx ExecStart=... (code=exited, status=1/FAILURE)这一行,退出码能告诉你失败性质:status=1一般是配置或运行错误,status=203/EXEC是二进制执行不了(权限或路径问题),status=226/NAMESPACE是命名空间问题(多见于容器环境),status=exited, status=0/SUCCESS然后服务还是 failed,说明进程正常启动后自己退了,要去看是不是被别的机制杀掉了。
另一块是底部日志的最后五行。Apache 启动失败时,关键错误往往就在这几行里,格式通常是[core:error] [pid xxxx] (98)Address already in use: AH00072: make_sock: could not bind to address [::]:80。这里面有三个信息点必须抠出来:方括号里的模块名(core还是ssl还是proxy)、圆括号里的 errno(98 是端口占用,13 是权限拒绝,2 是文件不存在,28 是磁盘满)、以及AHxxxxx这个错误码。
# 只要状态和最近日志,输出干净 systemctl status httpd --no-pager -l # 直接取退出码,脚本里好用 systemctl show httpd -p ExecMainStatus -p Result那个AH开头的错误码是 Apache 官方的错误编号,比英文描述更精准。同样的英文描述在不同版本里措辞会变,错误码不会。我个人的习惯是把常遇到的十几个错误码背下来,看到就知道该去哪一段。
2.2 configtest 是零成本的体检,一定要在改配置之前跑
apachectl configtest(RHEL 系也可以直接httpd -t,Debian 系是apache2ctl configtest)这条命令的本质是让 httpd 完整解析一遍所有配置文件和 Include 的文件,但不绑定端口、不写 PID、不启动任何进程。它唯一的副作用是可能创建或读取一些临时文件,风险极低。
这条命令的价值在于它能把问题从"服务起不来"收敛到"第几行第几个字符"。输出长这样:
$ apachectl configtest AH00526: Syntax error on line 42 of /etc/httpd/conf.d/ssl.conf: SSLCertificateFile: file '/etc/pki/tls/certs/server.crt' does not exist or is empty看到这种输出,你就不需要再猜了,直接去第 42 行。注意它一次只报第一个错误,改完再跑一次,可能会冒出第二个。所以我的习惯是改一处跑一次,别攒着一次改五个地方,否则改错了你都不知道是哪一处引入的新问题。
还有几个常用的变形一定要会:
# 只看虚拟主机配置有没有冲突(重名 ServerName、DocumentRoot 缺失) httpd -S # 列出当前加载的所有模块,查模块缺失神器 httpd -M # 完整 dump 最终生效的配置(所有 Include 展开后),排查"配置被覆盖" httpd -Thttpd -S特别值得单独说。虚拟主机配置冲突是启动失败的经典原因,尤其是NameVirtualHost(老版本)或者多个 vhost 用了同一个ServerName+ 端口组合。-S会把所有 vhost 按监听端口分组列出来,重复的、默认的、没匹配上的,一目了然。
2.3 journalctl 和 error_log 的区别,很多人搞混
这里必须澄清一个高频误区:journalctl -u httpd看到的是 systemd 捕获的进程标准输出和标准错误,error_log是 Apache 自己写的日志文件。两者的内容有重叠但不等价。
Apache 启动早期(还没打开日志文件之前)的错误只会出现在 journal 里,因为那时候 error_log 还没建立文件句柄。反过来,运行期的详细错误(比如某个请求触发的模块报错)只会出现在 error_log 里。所以查启动失败,先 journalctl 后 error_log,顺序不能反。
# 看本次启动周期的全部日志,带时间戳 journalctl -u httpd --since "10 min ago" --no-pager # 只看错误级别 journalctl -u httpd -p err --no-pager # 看 error_log 末尾,注意先确认文件有没有在写 tail -n 80 /var/log/httpd/error_log # Debian 系路径是 /var/log/apache2/error_log翻 error_log 有个技巧:不要只看末尾。如果 Apache 反复重启失败,error_log 末尾可能是一堆无关的请求日志。正确做法是按时间戳过滤,锁定失败发生的那几十秒:
# 精确到分钟定位 grep "$(date -d '5 min ago' '+%Y-%m-%d %H:%M')" /var/log/httpd/error_log | tail -50还有一个容易踩的坑:日志轮转。如果 logrotate 刚切过日志,error_log 变成了空文件,你看末尾什么都看不到,会误以为"没报错"。这时候要看error_log-20240101这种带日期后缀的历史文件。判断方法很简单,ls -l看一下文件的修改时间,如果它不是刚刚被写的,说明当前进程根本没写进这个文件,问题在更早的阶段。
3. 六类高频原因:现象、根因、处置一次说透
理论上启动失败的原因有无穷多,但实际工作里 95% 的案例集中在下面六类。我把每一类的"长什么样""为什么会这样""怎么处理"都写清楚,你对号入座就行。
3.1 端口被占:Address already in use 的三种变体
这是最高频的一类,错误长这样:
(98)Address already in use: AH00072: make_sock: could not bind to address [::]:80 (98)Address already in use: AH00072: no listening sockets available, shutting downerrno 是 98,含义非常明确:这个端口已经被别的进程绑定了。但"别的进程"可能是三种东西,处理方式完全不同。
第一种是上一个 httpd 残留进程。systemctl stop有时候没杀干净,尤其是 graceful restart 卡住的时候。表现为ps aux | grep httpd还能看到几个进程,父进程 PID 是 1(被 init 收养了)。处理方式是先systemctl stop httpd,等几秒,还不行就pkill -9 httpd,确认ps里干净了再启动。粗暴但有效,前提是确认这台机器上只有你在用 Apache。
第二种是别的服务占了 80/443。常见的嫌疑对象是 nginx、某些面板程序自带的 web 服务、docker 映射出来的容器端口、甚至是某个开发调试工具。查法:
ss -lntp | grep -E ':(80|443)\s' # 老系统没有 ss 就用 netstat -lntp | grep -E ':(80|443)\s' # 想要更详细的信息 lsof -i :80 -sTCP:LISTEN输出里的最后一列会告诉你进程名和 PID。这时候要做决策:是停掉那个服务,还是让 Apache 换个端口。生产环境上如果那个服务是业务需要的,别硬停,改成让 Apache 监听 8080,前面挂个反向代理,比停服务安全得多。
第三种比较隐蔽:IPv4 和 IPv6 的监听冲突。Listen 80在老版本 Apache 上会同时尝试绑 IPv4 和 IPv6,如果系统里 IPv6 被禁用或者被别的进程占了一半,就会报这个错但显示的是[::]:80。处理方式是明确指定:
# 只监听 IPv4,避免 IPv6 相关的诡异问题 Listen 0.0.0.0:80注意:改监听地址之前先确认这台机器的业务是不是真的需要 IPv6 访问。有些环境前面有双栈负载均衡,你把 IPv6 关了会导致一部分用户访问不了,这种问题排查起来比启动失败还费劲。
3.2 配置语法错误:报错最明确,但最容易改出新问题
这类错误带AH00526前缀,格式是Syntax error on line N of /path/to/file.conf: 具体原因。看着最好处理,实际上翻车率很高,因为很多人改的时候不看上下文。
几个典型子类:
指令拼写错误或大小写错误。Apache 的指令是大小写敏感的,documentroot写成小写不认,DocumentRoot才对。还有Servername、Listen写成listen,都报Invalid command。
指令用在了不允许的上下文。比如DocumentRoot只能出现在 server config 或 virtualhost 上下文,你把它塞进<Directory>块里就报错。这类错误报错信息通常是DocumentRoot not allowed here,看到not allowed here就知道是位置问题,不是拼写问题。
块结构没闭合。<VirtualHost>少了</VirtualHost>,<Directory>少了闭合标签。这种错误最坑,因为它报的行号往往在文件末尾或者下一个配置块的开头,跟你实际漏掉的地方差了十万八千里。我的经验是:看到报错行号指向一个看起来完全正常的行,往上找最近的<开头的标签,检查它有没有闭合。
引号不配对。路径里有空格必须加引号,加了引号又忘了闭合,报错信息会很奇怪。比如DocumentRoot "/var/www/my site"是对的,少个右引号就会把后面几行全吃掉,报错行号乱跳。
Include 路径不存在。IncludeOptional不存在不报错,Include不存在直接失败。RHEL 系升级后conf.d/*.conf目录里如果有.rpmnew这种备份文件,有时候会被 Include 进去导致重复定义,报AH00526: Cannot define multiple Listeners on the same IP:port。查法就是ls -la /etc/httpd/conf.d/看有没有奇怪后缀的文件。
处理这一类,我的固定动作是:先configtest拿到行号,sed -n '35,50p'把那一行前后十几行打出来看上下文,改完立刻再configtest,确认通过再重启。别嫌麻烦,一次改一处,比一次改五处最后不知道哪处错了要快得多。
3.3 权限与文件系统:errno 13 和 errno 28 的处置差别很大
(13)Permission denied和(28)No space left on device是两类完全不同的问题,但经常被混为一谈,因为都表现为"启动失败"。
errno 13 权限拒绝,常见于三个位置。一是 PID 文件目录,RHEL 系是/run/httpd/,Debian 系是/var/run/apache2/,这个目录属主应该是 apache 用户(RHEL 是apache,Debian 是www-data),权限 755。二是日志目录/var/log/httpd/或/var/log/apache2/,需要写权限。三是证书私钥文件,SSLCertificateKeyFile指向的.key文件如果权限是 644 且属主是 root,Apache 以 apache 用户读不了,会报Permission denied或者unable to read.
# 一次性核对三个关键目录的属主权限 ls -ld /run/httpd /var/log/httpd /etc/httpd/conf # 修复 PID 目录(RHEL 系) install -d -o apache -g apache -m 755 /run/httpd # 修复日志目录 chown -R apache:apache /var/log/httpd私钥文件的处理要谨慎,别为了省事直接chmod 777。正确做法是属主 root、属组 apache(或 ssl-cert)、权限 640,让 Apache 通过组权限读。
errno 28 磁盘满,报错长这样:AH00098: pid file /run/httpd/httpd.pid overwritable: No space left on device。有意思的是,报"磁盘满"的地方往往是 PID 文件,但真正的根因可能是/var分区满了、inode 用尽了、或者/run这个 tmpfs 被占满了。
df -h df -i # inode 用尽也会报 No space left du -sh /var/log/* | sort -rh | head -10df -h显示还有空间但就是报 No space,八成是 inode 用完了,日志目录里堆积了几百万个小文件。这种情况df -i一看就明白,处理办法是清理旧日志,顺手检查 logrotate 配置有没有生效。
还有一种情况:磁盘确实满了,但你不能删业务数据。这时候最安全的临时处理是把ErrorLog和CustomLog重定向到另一个分区,先把服务拉起来,再慢慢清理。别在磁盘满的状态下继续往同一个分区写日志,会雪崩。
3.4 模块加载失败:Invalid command 背后往往是 .so 没加载
这类错误的表现形式很统一:
Invalid command 'RewriteEngine', perhaps misspelled or defined by a module not included in the server configuration这句话翻译过来就是:我不认识RewriteEngine这个指令,要么你拼错了,要么定义它的模块没加载。90% 的情况是后者。
Apache 的模块分两种加载方式。一种是编译进核心的(built-in),httpd -M里显示(static);另一种是动态加载的.so文件,需要在配置文件里用LoadModule指令加载,httpd -M里显示(shared)。
# 看某个模块到底加载了没 httpd -M | grep -i rewrite # 输出 shared 说明加载了,没输出就是没有RHEL 系的做法是在/etc/httpd/conf.modules.d/目录下放00-base.conf、10-ssl.conf这类文件,里面写LoadModule行。Debian 系用a2enmod命令来启用模块,它会创建软链接到mods-enabled/。
# Debian/Ubuntu 启用模块 a2enmod rewrite a2enmod ssl a2enmod headers # 启用后必须 reload,不是 restart 也行,但启动失败状态下只能 restart systemctl restart apache2模块加载失败还有两个衍生问题要注意。
一是.so文件本身不存在或版本不匹配。报错会变成Cannot load /etc/httpd/modules/mod_ssl.so into server: /etc/httpd/modules/mod_ssl.so: cannot open shared object file。这通常是系统升级后 Apache 大版本变了,旧模块被删了但配置文件还留着 LoadModule 行。处理方式是确认模块包有没有装(RHEL 系是mod_ssl包),或者直接注释掉那一行。
二是模块之间的依赖顺序。mod_ssl依赖mod_socache_shmcb,如果只加载了前者没加载后者,会报AH00526: Syntax error ... SSLSessionCache: 'shmcb' session cache not supported。这种情况下httpd -M看到 ssl 是 loaded,但依赖没满足,很容易误判成"模块已经加载了为什么还报错"。
提示:排查模块问题的黄金命令是
httpd -M和httpd -T组合使用。前者告诉你加载了什么,后者告诉你最终生效的配置内容。两个一起看,能定位到"配置文件里写了但没生效"这种最阴的问题。
3.5 证书与监听地址:那些"配置语法对了但就是起不来"的情况
证书问题很典型,因为它的语法永远是对的,configtest也可能通过(新版本会检查文件存在性,老版本不一定),但启动就失败。
AH02217: ssl_stapling_init_cert: can't retrieve issuer certificate! AH02564: Failed to configure encrypted (?) private key AH02312: Fatal error initialising mod_ssl, exiting.几个高频原因:
证书文件路径不对或文件为空。SSLCertificateFile指向的文件不存在,或者是个零字节文件(常见于证书申请脚本失败后留下的空壳)。
证书和私钥不匹配。两份文件的 modulus 对不上。查法:
openssl x509 -noout -modulus -in server.crt | openssl md5 openssl rsa -noout -modulus -in server.key | openssl md5 # 两个 md5 必须一致证书链不完整。浏览器能打开但某些客户端报错,同时 Apache 启动可能报AH02032: Unable to configure verify locations for client authentication。处理方式是把中间 CA 证书按顺序拼到证书文件后面。
私钥被加密了。带密码的私钥在非交互启动时必然失败,因为没法输入密码。报错AH02564: Failed to configure encrypted (?) private key。解决要么去掉密码openssl rsa -in enc.key -out plain.key,要么配置SSLPassPhraseDialog(生产环境不推荐,密码明文放配置里风险太大)。
另一大类是监听地址不对:
(99)Cannot assign requested address: AH00072: make_sock: could not bind to address 10.0.0.5:80errno 99,意思是"你要绑的这个 IP 地址本机上不存在"。典型场景是机器换了网卡或者 IP 变了,配置文件里还写着旧 IP;或者是从别的机器拷过来的配置,Listen指令里硬编码了源机器的 IP。
ip addr show | grep inet # 确认 Listen 指令里的 IP 是否真的在本机上 grep -r "^\s*Listen" /etc/httpd/处理很简单:改成Listen 80(监听所有地址)或者改成当前机器实际的 IP。
3.6 系统层面:SELinux、文件描述符、MPM 参数
这一类是最容易被忽略的,因为报错不写在 Apache 的日志里,而是写在系统日志里。
SELinux 拦截。RHEL 系默认开启,Apache 想连个后端端口、想读写非标准目录、想发邮件,都会被拦下来,表现为"配置全对但就是访问不了"或者"启动时报 Permission denied 但ls -l看起来权限没问题"。
# 看有没有被拦 ausearch -m avc -ts recent # 或者 grep -i denied /var/log/audit/audit.log | tail -20常见的几个开关:
# 允许 httpd 发起网络连接(反向代理后端时必须) setsebool -P httpd_can_network_connect 1 # 允许读写用户家目录 setsebool -P httpd_enable_homedirs 1 # 非标准目录递归打标签 semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?" restorecon -Rv /data/www注意setsebool一定要加-P,否则重启失效,你会陷入"改完好了,重启又不行"的循环。这个坑我踩过,排查了半小时才发现是重启后策略回来了。
文件描述符限制。报错形态是AH00024: Failed to open the scoreboard或者(24)Too many open files。systemd 管理的服务默认LimitNOFILE可能是 1024,高并发下不够用。需要改 unit 文件:
# systemctl edit httpd 添加 [Service] LimitNOFILE=65535然后systemctl daemon-reload && systemctl restart httpd。数值怎么定?粗略估算是MaxRequestWorkers × 2 + 200,因为每个连接至少占一个 fd,加上日志、管道、监听套接字。比如MaxRequestWorkers是 400,那 1000 就够了,但我习惯直接给 65535,反正不占实际内存。
MPM 参数配置错误。这个稍微进阶一点。Apache 有三种 MPM:prefork、worker、event。ServerLimit和MaxRequestWorkers之间有硬约束:
MaxRequestWorkers = ServerLimit × ThreadsPerChild如果你设了ServerLimit 10、ThreadsPerChild 64,那MaxRequestWorkers最大只能是 640,写 1000 就会报AH00161: server reached MaxRequestWorkers setting或者干脆启动时警告WARNING: MaxRequestWorkers of 1000 would require raising ServerLimit to 16。
配置不当导致的启动失败还有一种:php模块和 event MPM 不兼容。老版本的mod_php只能跑在 prefork 下,强行切到 event 会报错或者启动后立刻崩溃。新版php-fpm就没这个问题,所以我一般建议直接上php-fpm,别用mod_php。
4. 一次完整的现场复现:从报错到恢复的全过程
光讲理论没意思,我把上周处理的一个案例完整走一遍。这台机器是某业务系统的应用服务器,早上例行巡检发现 Apache 挂了,值班的同学重启了三次都失败,找到我的时候已经过了一小时。
4.1 现场信息采集:三分钟锁定范围
第一步,看状态。
$ systemctl status httpd --no-pager ● httpd.service - The Apache HTTP Server Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled) Active: failed (Result: exit-code) since Tue 2024-07-09 08:12:41 CST Process: 8821 ExecStart=/usr/sbin/httpd $OPTIONS -DFOREGROUND (code=exited, status=1/FAILURE) Main PID: 8821 (code=exited, status=1/FAILURE) Jul 09 08:12:41 host-01 httpd[8821]: AH00558: httpd: Could not reliably determine the server's fully qualified domain name Jul 09 08:12:41 host-01 httpd[8821]: (13)Permission denied: AH00072: make_sock: could not bind to address [::]:443信息量很大。第一,退出码status=1,是常规失败,不是二进制问题。第二,AH00558那条是警告,不影响启动,是ServerName没配,可以后面再说。第三,真正致命的是最后一条,errno 13,绑 443 端口时权限拒绝。
这里就有一个判断:绑 443 报权限拒绝,最常见的原因有两个,一是非 root 进程绑低于 1024 的端口,二是 SELinux 拦了httpd_t绑http_port_t之外的端口。但这台机器明明是 root 启动的服务,所以大概率不是权限模型的问题,而是端口被占了之后内核返回的错误码翻译偏差,或者端口号本身不在 SELinux 允许列表里。
第二步,跑配置检查。
$ httpd -t Syntax OK配置语法干净,说明问题不在配置解析这一关,直接跳到资源申请关。
第三步,查端口和 SELinux。
$ ss -lntp | grep 443 LISTEN 0 128 0.0.0.0:443 0.0.0.0:* users:(("nginx",pid=2210,fd=6)) $ semanage port -l | grep http_port_t http_port_t tcp 80, 81, 443, 488, 8008, 8009, 8443问题找到了。443 端口被 nginx 占着,而 nginx 是上周另一个团队部署的,部署完没通知任何人。Apache 和 nginx 抢同一个端口,谁先起谁占住。
4.2 决策与执行:为什么最后选了改端口而不是停 nginx
到这一步有两个选择:停掉 nginx 让 Apache 拿 443,或者让 Apache 换端口。
我先做了个信息核对:跟部署 nginx 的团队确认,这个 nginx 是给另一个业务做静态资源服务的,已经在跑流量,不能停。那么方案就定了,Apache 改监听。
改配置:
# /etc/httpd/conf.d/ssl.conf # 原来是 Listen 443 https Listen 8443 https # VirtualHost 块也要同步改 <VirtualHost *:8443> ServerName app.example.internal SSLEngine on SSLCertificateFile /etc/pki/tls/certs/app.crt SSLCertificateKeyFile /etc/pki/tls/private/app.key </VirtualHost>改完先跑检查:
$ httpd -t Syntax OK $ httpd -S VirtualHost configuration: *:8443 is a NameVirtualHost default server app.example.internal (/etc/httpd/conf.d/ssl.conf:12)-S的输出确认 vhost 已经绑到 8443 了。启动试试:
$ systemctl start httpd $ systemctl status httpd --no-pager Active: active (running)起来了。但 8443 不在http_port_t的默认列表里,SELinux 会拦。先加标签:
$ semanage port -a -t http_port_t -p tcp 8443 # 如果提示已存在则用 -m 修改 $ semanage port -m -t http_port_t -p tcp 8443然后本机验证:
$ curl -kI https://127.0.0.1:8443 HTTP/1.1 200 OK到这里服务恢复,但还差最后一步:前面挂的负载均衡还指向 443,需要把上游端口改成 8443。这一步不做,业务依然不通,只是服务"看起来起来了"。很多值班同学就卡在这,觉得服务起来了就完事了。
4.3 恢复之后必须补做的四件事
故障处理完不等于结束。我给自己定了个规矩,每次处理完启动失败,必须补做四件事,否则同类问题还会再来。
第一件,把警告清干净。AH00558那条 FQDN 警告虽然不致命,但会污染日志,下次真出问题的时候容易误判。加一行ServerName localhost:80(或者实际域名)就解决了。还有AH00548: NameVirtualHost has no effect and is deprecated这类,顺手删掉。
第二件,确认开机自启状态。systemctl is-enabled httpd,如果不是enabled,下次重启机器还是会挂。
第三件,把这次的原因写进变更记录。端口冲突这种事,不写下来,三个月后换个人值班,还得从头查一遍。
第四件,加监控。至少要有端口监听检查(ss -lnt | grep 8443)和 HTTP 探活(curl -f)两个维度,光监控进程在不在是不够的,进程在但监听失败的情况很常见。
5. 常见问题速查表与几处踩坑心得
理论和流程讲完了,最后这部分是最实用的。我把自己这几年攒的速查表整理出来,按报错关键词索引,看到什么现象直接对到原因和处理动作。
5.1 启动失败速查表
| 报错关键词 / 错误码 | 常见根因 | 快速验证命令 | 处置动作 |
|---|---|---|---|
(98)Address already in use | 端口被占用 | ss -lntp | grep :80 | 停占用进程或改 Listen 端口 |
(13)Permission denied | PID/日志目录属主错误,或非 root 绑低位端口 | ls -ld /run/httpd /var/log/httpd | chown修正属主,或用 authbind/capabilities |
(99)Cannot assign requested address | Listen 的 IP 本机不存在 | ip addr show | 改 Listen 为0.0.0.0:80或实际 IP |
(28)No space left on device | 磁盘或 inode 满 | df -h; df -i | 清日志 / 迁移日志目录 |
AH00526: Syntax error on line N | 配置语法错误 | httpd -t | 按行号修,一次改一处 |
Invalid command 'XXX' | 模块未加载或指令拼错 | httpd -M | grep xxx | 加 LoadModule 或 a2enmod |
Cannot load ... .so | 模块文件缺失或版本不匹配 | ls /etc/httpd/modules/ | 装对应模块包或注释该行 |
AH00558: Could not reliably determine FQDN | 未配 ServerName(警告,不致命) | grep ServerName | 加ServerName localhost:80 |
AH02217 / AH02312 | SSL 证书或私钥问题 | openssl x509 -noout -modulus | 修证书路径 / 补全证书链 / 去私钥密码 |
AH00024: Failed to open the scoreboard | 文件描述符不足 | ulimit -n | 改 unit LimitNOFILE |
AH00161: server reached MaxRequestWorkers | MPM 参数不匹配 | httpd -M | grep mpm | 调 ServerLimit / ThreadsPerChild |
Job for httpd.service failed且无详情 | 需要看 journal | journalctl -u httpd -n 50 | 读 journal 拿真实错误 |
| 服务 active 后立刻 failed | 进程自退,可能证书/模块初始化 | tail -100 error_log | 看 SIGTERM 前的最后一条 |
| SELinux 相关 | 端口或目录无正确标签 | ausearch -m avc -ts recent | semanage port/restorecon |
表格里最后一行值得单独说一句:SELinux 的坑在于它不会让启动失败,而是让服务"起来了但不通"。所以如果你的现象是"服务状态正常,本机 curl 也通,但外部访问不了",先去查防火墙和 SELinux,别在 Apache 配置里绕圈子。
5.2 踩坑心得:文档里不会写的几条经验
第一条,改配置之前先备份,备份文件别放在 conf.d 目录里。我见过有人在/etc/httpd/conf.d/里cp ssl.conf ssl.conf.bak,结果重启后报重复定义,因为Include conf.d/*.conf把 bak 文件也加载了。备份要放到/root/backup/这种配置目录外面的地方,或者至少改成ssl.conf.20240709但放在同级目录外。
第二条,restart和reload不是一回事,启动失败状态下只能用restart。reload是 graceful 的,会让旧进程继续服务、新进程加载新配置,前提是当前服务是活的。服务已经挂了的时候reload什么也不做,会给你一个"成功"的假象。这个坑我踩过,改完配置reload显示 OK,实际服务还是死的。
第三条,别迷信configtest输出的 Syntax OK。它只做语法解析,不检查语义。DocumentRoot指向一个不存在的目录,configtest照样通过,启动到打开日志之后才报错。所以检查配置要三步:httpd -t看语法、httpd -S看虚拟主机、httpd -T看最终生效内容。三样都过一遍,才算真的检查过。
第四条,生产环境排查优先用只读命令。ss、lsof、ps、tail、grep、httpd -t/-M/-S/-T都不会改变系统状态。kill、chmod、setsebool、semanage这些是写操作,动手之前先想清楚影响范围。我见过有人为了排查端口占用kill -9了一个进程,结果那是数据库的连接池守护进程,连带把业务搞挂了。查清楚再动手,这个顺序不能反。
第五条,把 "服务起不来" 和 "服务起来了但访问不了" 当成两个完全不同的故障树。前者查启动链路(二进制 → 配置 → 资源 → 运行期),后者查网络链路(监听地址 → 防火墙 → SELinux → 上游代理 → 后端应用)。很多人在服务已经正常运行时还在改 Apache 配置,方向从一开始就错了。
第六条,日志时间戳是最可靠的线索。排查的时候第一件事是date确认服务器时间对不对,如果时区错了或者 NTP 没同步,你按时间戳 grep 日志会全是错的。这个低级错误我自己犯过,查了二十分钟发现是日志时区和我预期差了八小时。
第七条,给每台机器配一份"启动检查清单"。我现在管理的机器上都有一个/root/apache-check.sh,内容就是上面那几条命令串起来:状态、configtest、端口、磁盘、SELinux、日志末尾。启动失败的时候先跑一遍,输出的信息足够定性,比凭记忆一条条敲要稳定得多。脚本很简单,但把"排查"这件依赖经验的事变成了"执行清单",值班的同学也能照着走。
最后分享一个我自己的小习惯:处理完任何一次启动失败,我会在error_log里手动写一行注释,比如# 2024-07-09 端口冲突,nginx 占用 443,已改 8443。日志文件本身支持以#开头的行作为注释,写进去不影响任何东西。三个月后再出问题,翻日志的时候这一行就是最好的上下文,比翻工单系统快得多。