☰
日志轮转也能搞崩构建?Apache SIGHUP 导致 SVN E175002 的完整排查与修复
2026/10/20 11:20:10 网站建设 项目流程

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 update

svn 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,但仍然失败:

  1. Apache 重启过程影响了正在传输的响应流;
  2. REPORT的 XML 或目录清单没有完整传输;
  3. KeepAlive 连接在请求过程中被关闭或复用失败;
  4. Apache 子进程切换期间,请求被中断、截断或出现协议解析异常。

因此,下面这条错误并不表示请求完整成功:

E175002: REPORT ... 200 OK

它更准确的含义是:HTTP 头部可能已经正常返回,但 SVN 客户端没有拿到一个可用的完整协议响应。

五、完整链路与验证方法

下面用一张流程图把从 logrotate 执行到 SVN 更新失败的完整链路串起来,并在每个环节标注对应的验证方法:

验证: logrotate -dv /etc/logrotate.d/httpd

验证: grep -n 'reload' /etc/init.d/httpd

验证: 查看 error_log 中 SIGHUP received

验证: 观察 access_log 中 REPORT 请求时间

验证: svn update 复测 + 时间关联

logrotate 执行日志轮转

postrotate 调用 service httpd reload

init 脚本向 Apache 主进程发送 SIGHUP

Apache 进入常规重启处理

正在执行的 SVN HTTP/DAV 请求受影响

svn: E175002 更新失败

各环节验证方法说明:

  • 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 活跃请求的风险
reloadSIGHUP常规重启处理,子进程重建较高
gracefulSIGUSR1尽量让旧请求处理完成,再平滑切换较低

需要强调: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 OK

2. 检查 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,会不会也把请求打断?

结论是:不会,而且是从机制上就不会。原因是两家的信号语义是反过来的。

信号语义对比

操作Apachenginx
reload/SIGHUP硬重启:子进程立即被替换,活跃连接被掐断平滑重启:旧 worker 处理完手上请求才退出
SIGUSR1graceful 平滑重启仅重开日志文件,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) endscript

USR1只让 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 也有三种情况会真的断

  1. 长连接撞上worker_shutdown_timeout(nginx 1.11.11 起才有这个参数)
    • 不设:WebSocket / SSE 这类长连接不会被 nginx 主动断,但旧 worker 会一直等到连接自然结束。频繁 reload 会让 worker 越堆越多(ps里能看到is shutting down),吃掉 fd 和内存。
    • 设了10s:到点强制关闭,WS/SSE 会瞬断(客户端通常自动重连,但业务上有感知)。这是 nginx reload 唯一真正的中断点。
  2. 误用restart/stop:logrotate 里若写成systemctl restart nginx或nginx -s stop,那就是硬中断。reload 和 restart 务必分清。
  3. 切换瞬间资源翻倍:新旧 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、长连接或大文件传输的服务,一次不恰当的重载就可能变成构建链路中的偶发故障。

本次处理建议可以总结为五点:

  1. 不直接删除postrotate,避免服务持续写入旧日志;
  2. 将httpd reload替换为经验证的httpd graceful;
  3. 通过 Jenkins、Apache 错误日志和访问日志建立时间关联;
  4. 在低峰维护窗口完成验证,确认日志轮转、日志写入与 SVN 更新均稳定;
  5. 同样的动作换个组件(如 nginx),先确认信号语义再动手——HUP在不同服务里可能是完全相反的行为。

运维中的“无感操作”往往只是没有碰巧撞上业务高峰。把重载行为、信号语义和请求链路串起来,才能真正找到偶发故障的原因。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询