Apache 日志轮转触发 SVN 更新异常:一次SIGHUP与E175002的实战排查
文中域名、仓库路径均为文档示例(
example.com为 IANA 保留域名),请按自己环境替换。
结论先行:当 Apache 同时承载 SVN HTTP 服务时,日志轮转后执行
service httpd reload可能向 Apache 主进程发送SIGHUP。如果此时 Jenkins 正在执行svn update,活跃的 SVN/DAVREPORT请求可能受到影响,并表现为svn: E175002。更稳妥的处理方式是把轮转后的操作调整为graceful,并通过时间关联、日志和复测确认效果。
一、故障背景
最近在一台提供 SVN HTTP 访问的 Apache 服务器上,Jenkins 构建任务出现 SVN 更新失败。环境大致如下:
- 服务组件:Apache HTTP Server、SVN、Jenkins
- Apache:2.2.15
- SVN:1.8.11
- 日志轮转:
/etc/logrotate.d/httpd - 访问方式:Jenkins 通过 HTTP 协议访问 SVN 仓库
这类故障的特殊点在于:Apache 不只是普通 Web 服务,还在承载 SVN 的 WebDAV 协议请求。一次看似常规的“日志轮转后重载”,可能刚好打断构建中的 SVN 更新过程。
二、故障现象
Jenkins 在预构建 Shell 阶段依次执行:
svn cleanup svn updatesvn cleanup正常完成;svn update已开始更新工作副本、创建版本目录并拉取资源文件,随后失败:
svn: E175002: REPORT of '/repos/proj-a/!svn/me': 200 OK (http://devsvn.example.com) Failed build for hudson.tasks.Shell@47e96163 ERROR: pre-build step failed to setup environment Finished: FAILURE与此同时,在 Apache 的归档错误日志中发现:
[Fri Oct 16 03:46:10 2026] [notice] SIGHUP received. Attempting to restart这意味着 Apache 在该时刻接收到了SIGHUP,并进入重启处理逻辑。
三、为什么200 OK仍会让 SVN update 失败?
初看错误信息很容易产生疑问:既然服务端返回了200 OK,为什么 SVN 还会报错?
关键在于:SVN 的REPORT不是一个只看 HTTP 状态码的普通请求。客户端除了要求收到 HTTP 200,还需要接收到完整且符合 SVN/WebDAV 协议预期的响应内容。
在以下情况下,客户端可能已经收到200 OK,但仍然失败:
- Apache 重启过程影响了正在传输的响应流;
REPORT的 XML 或目录清单没有完整传输;- KeepAlive 连接在请求过程中被关闭或复用失败;
- Apache 子进程切换期间,请求被中断、截断或出现协议解析异常。
因此,下面这条错误并不表示请求完整成功:
E175002: REPORT ... 200 OK它更准确的含义是:HTTP 头部可能已经正常返回,但 SVN 客户端没有拿到一个可用的完整协议响应。
五、完整链路与验证方法
下面用一张流程图把从 logrotate 执行到 SVN 更新失败的完整链路串起来,并在每个环节标注对应的验证方法:
各环节验证方法说明:
- logrotate 执行:用
logrotate -dv调试模式确认轮转规则匹配到的日志文件和脚本逻辑; - postrotate 调用 reload:检查
/etc/init.d/httpd中reload()的实现,确认是否发送SIGHUP; - 发送 SIGHUP:在 Apache 归档错误日志中检索
SIGHUP received. Attempting to restart记录; - Apache 重启处理:在 access_log 中定位 Jenkins 节点 IP 对应的
REPORT请求时间; - SVN 请求受影响:将 Jenkins 失败时间与
SIGHUP时间做关联比对,相差数秒到几十秒即高度相关; - E175002 失败:用独立测试工作副本执行
svn update --non-interactive复测确认。
四、定位日志轮转行为
查看 Apache 的 logrotate 配置:
cat/etc/logrotate.d/httpd发现其中包含:
/var/log/httpd/*log { compress olddir oldlog daily rotate 7 missingok notifempty sharedscripts delaycompress postrotate /sbin/service httpd reload > /dev/null 2>/dev/null || true endscript }这里最关键的是:
/sbin/service httpd reload继续检查系统服务脚本:
grep-n-A15-B5'reload'/etc/init.d/httpd可以看到reload()最终执行类似逻辑:
LSB=1killproc-p${pidfile}$httpd-HUP也就是说,service httpd reload并不是一个纯配置检查动作,而是会向 Apache 主进程发送SIGHUP。
这与错误日志中的内容完全对应:
SIGHUP received. Attempting to restart由此可以建立完整链路:
logrotate 执行 ↓ postrotate 调用 service httpd reload ↓ init 脚本向 Apache 主进程发送 SIGHUP ↓ Apache 进入常规重启处理 ↓ 正在执行的 SVN HTTP/DAV 请求可能受影响五、为什么不建议简单删除 postrotate?
有人看到问题后,第一反应是直接删掉postrotate。这并不推荐。
日志轮转时,logrotate 会对旧日志重命名或移动,并创建新的活动日志文件。如果 Apache 不重新打开日志文件描述符,它可能继续写入已经被移动到oldlog目录的旧文件。
最终会出现两个问题:
- 新的
access_log、error_log看起来不再有日志; - 实际日志继续写入归档文件,影响排障和监控采集。
因此,正确方向不是“取消 Apache 重新打开日志”,而是:
保留日志重开动作,但将对业务请求影响较大的常规重启,调整为更温和的平滑重启。
六、推荐方案:reload改为graceful
先备份原配置:
cp-a/etc/logrotate.d/httpd /etc/logrotate.d/httpd.bak将原配置:
postrotate /sbin/service httpd reload > /dev/null 2>/dev/null || true endscript修改为:
postrotate /sbin/service httpd graceful > /dev/null 2>&1 || true endscript完整示例:
/var/log/httpd/*log { compress olddir oldlog daily rotate 7 missingok notifempty sharedscripts delaycompress postrotate /sbin/service httpd graceful > /dev/null 2>&1 || true endscript }reload与graceful的区别
| 操作 | 常见信号 | 典型行为 | 对 SVN 活跃请求的风险 |
|---|---|---|---|
reload | SIGHUP | 常规重启处理,子进程重建 | 较高 |
graceful | SIGUSR1 | 尽量让旧请求处理完成,再平滑切换 | 较低 |
需要强调:graceful不是“完全无影响”。它仍会触发 Apache 的平滑重启和日志重开,只是比直接以SIGHUP进行常规重启更适合存在长请求的服务场景。
七、实施前必须确认的一件事
不同发行版、不同版本的/etc/init.d/httpd实现可能不同。不要假定graceful一定发送SIGUSR1,应先在服务器上确认:
grep-n-A10-B3'^graceful'/etc/init.d/httpd预期可以看到类似:
killproc-p${pidfile}$httpd-USR1只有确认graceful走的是平滑重启逻辑,修改才有意义。
八、验证步骤
1. 检查 Apache 配置
/sbin/service httpd configtest预期:
Syntax OK2. 检查 logrotate 规则
logrotate-dv/etc/logrotate.d/httpd-d为调试模式,不会真正执行轮转。-v用于输出详细过程,便于确认匹配到的日志文件和脚本逻辑。
3. 在低峰窗口验证日志持续写入
变更后,检查活动日志和归档日志:
ls-lh/var/log/httpd/ls-lh/var/log/httpd/oldlog/观察活动日志是否继续增长:
tail-f/var/log/httpd/access_log如果日志文件命名不同,请替换为实际文件名。
4. 验证 SVN 服务
优先使用独立测试工作副本,而不是直接操作 Jenkins 正在使用的生产工作目录:
svn info svn update --non-interactive同时观察 Apache 错误日志,确认是否再次出现:
E175002: REPORT of '/repos/proj-a/!svn/me'九、怎样确认本次故障的因果关系?
目前我们可以确认:
- 日志轮转配置会触发
httpd reload; reload会发送SIGHUP;- Apache 在对应时段记录了重启尝试;
- Jenkins 的 SVN 更新出现
E175002。
但要把“高度相关”升级为“确认根因”,还需要进行时间关联。
建议核对:
Jenkins 中 svn update 的失败时间 ↕ Apache error_log 中 SIGHUP 的时间 ↕ Apache access_log 中对应 REPORT 请求的时间可重点检查:
grep-nE'SIGHUP|restart|graceful|child pid|segfault|DAV|svn'\/var/log/httpd/oldlog/error_log-20261016如果已知 Jenkins 节点 IP,则定位其REPORT请求:
grep'JENKINS_IP'/var/log/httpd/oldlog/access_log-20261016\|grep'"REPORT '如果 Jenkins 失败时间与SIGHUP相差数秒到几十秒,并且同一时间段存在对应的REPORT /repos/proj-a/!svn/me请求,那么这条因果链就有较强证据支持。
十、一个容易忽略的运维改进
很多生产配置习惯这样写:
/sbin/service httpd graceful>/dev/null2>&1||true它的优点是:即使重开日志失败,也不会导致 logrotate 返回失败。
但缺点也明显:所有失败都会被吞掉。如果 Apache 没有成功 graceful,新日志可能无法被正常打开,故障会隐藏很久。
如果运维体系允许,建议至少记录一次系统日志:
postrotate if ! /sbin/service httpd graceful; then logger -t logrotate-httpd "httpd graceful failed after log rotation" fi endscript这样既不会阻断日志轮转流程,也能在系统日志中保留异常线索。
十一、回滚方案
若变更后发现 Apache 日志未持续写入、SVN 请求异常或服务行为不符合预期,可以恢复原配置:
cp-a/etc/logrotate.d/httpd.bak /etc/logrotate.d/httpd恢复后不建议马上执行强制轮转。应先检查 Apache 服务状态、当前日志文件及文件描述符写入情况,再安排下一次维护窗口验证。
十二、延伸:同样是日志轮转,nginx 为什么不会踩这个坑?
排查完 Apache 后自然会想到一个问题:如果换成 nginx 承载业务,日志轮转后 reload,会不会也把请求打断?
结论是:不会,而且是从机制上就不会。原因是两家的信号语义是反过来的。
信号语义对比
| 操作 | Apache | nginx |
|---|---|---|
reload/SIGHUP | 硬重启:子进程立即被替换,活跃连接被掐断 | 平滑重启:旧 worker 处理完手上请求才退出 |
SIGUSR1 | graceful 平滑重启 | 仅重开日志文件,worker 完全不动 |
也就是说:
- Apache 把“硬重启”给了
SIGHUP,把“优雅”给了USR1; - nginx 把“优雅”做进了
SIGHUP,把“重开日志”单独拆给了USR1。
这就意味着,本文第四节那条链路:
logrotate → postrotate → service httpd reload → SIGHUP → 活跃请求中断在 nginx 上从源头就不存在。因为 nginx 的 logrotate 配置通常是:
postrotate [ -s /run/nginx.pid ] && kill -USR1 $(cat /run/nginx.pid) endscriptUSR1只让 master 重新打开日志文件的文件描述符,worker 进程一个都不重启,任何连接都不受影响。
nginx reload 时到底发生了什么
即使真的执行nginx -s reload(发的是HUP),nginx 也是平滑的:
master 收到 HUP ├── 启动新 worker(加载新配置)→ 承接新连接 └── 旧 worker 关闭监听 socket → 继续处理手上已有的请求 → 处理完才退出所以:
- 前端页面请求:旧 worker 把请求传完才退,不会中断;
- proxy 到后端的 API:同理,旧 worker 会把当前请求代理完;到后端的 upstream keepalive 空闲连接会在旧 worker 退出时关闭,后端看到的是空闲连接被关,不影响业务;
- HTTP/2:nginx 会对旧连接发
GOAWAY,客户端自动重建,用户无感。
但 nginx 也有三种情况会真的断
- 长连接撞上
worker_shutdown_timeout(nginx 1.11.11 起才有这个参数)- 不设:WebSocket / SSE 这类长连接不会被 nginx 主动断,但旧 worker 会一直等到连接自然结束。频繁 reload 会让 worker 越堆越多(
ps里能看到is shutting down),吃掉 fd 和内存。 - 设了
10s:到点强制关闭,WS/SSE 会瞬断(客户端通常自动重连,但业务上有感知)。这是 nginx reload 唯一真正的中断点。
- 不设:WebSocket / SSE 这类长连接不会被 nginx 主动断,但旧 worker 会一直等到连接自然结束。频繁 reload 会让 worker 越堆越多(
- 误用
restart/stop:logrotate 里若写成systemctl restart nginx或nginx -s stop,那就是硬中断。reload 和 restart 务必分清。 - 切换瞬间资源翻倍:新旧 worker 并存期间,连接数和文件描述符是双份。高并发机器若
worker_connections或ulimit -n卡得紧,这段时间可能触顶。
nginx 场景的排查清单
# 1. 确认 logrotate 用的是 USR1(看到 HUP / reload 就是隐患)cat/etc/logrotate.d/nginx# 2. 有没有设强制关闭旧 worker 的超时nginx-T2>/dev/null|grep-nworker_shutdown_timeout# 3. reload 后确认旧 worker 真的退了# 长时间不回落 = 有 WebSocket / SSE / 大文件传输挂着ps-eopid,etimes,cmd|grep'[n]ginx: worker'# 4. 高并发机器看 fd 余量ulimit-n同样是“日志轮转后让服务重开日志”这个动作,Apache 需要把
reload换成graceful才能不伤业务,nginx 只要用对了USR1就天然无损。运维里很多“无感操作”只是没碰巧撞上业务高峰,把信号语义和请求链路串起来,才能判断它到底安不安全。
十三、总结
这次问题提醒我们:日志轮转不是纯粹的文件维护操作,它常常隐含服务重载行为。
对于只提供静态页面的服务,短暂 reload 可能不容易暴露问题;但对于承载 SVN、WebDAV、长连接或大文件传输的服务,一次不恰当的重载就可能变成构建链路中的偶发故障。
本次处理建议可以总结为五点:
- 不直接删除
postrotate,避免服务持续写入旧日志; - 将
httpd reload替换为经验证的httpd graceful; - 通过 Jenkins、Apache 错误日志和访问日志建立时间关联;
- 在低峰维护窗口完成验证,确认日志轮转、日志写入与 SVN 更新均稳定;
- 同样的动作换个组件(如 nginx),先确认信号语义再动手——
HUP在不同服务里可能是完全相反的行为。
运维中的“无感操作”往往只是没有碰巧撞上业务高峰。把重载行为、信号语义和请求链路串起来,才能真正找到偶发故障的原因。