
1. 为什么今天还要亲手搭 SVN不是早该被 Git 取代了吗很多人看到“Linux 搭建 SVN 服务”这个标题第一反应是都 2024 年了还搞 SVNGit 不香吗——这恰恰是我在金融、政务、军工类客户现场踩过最多次的思维坑。去年在某省级政务云平台做系统迁移评估时客户运维负责人直接把 GitLab 的部署方案拍在桌上“你们先告诉我怎么让审计系统自动抓取 SVN 的每次 commit 时间戳、操作人、文件路径和变更行数”我当场愣住。后来翻遍他们十年来的等保测评报告才发现所有审计日志规范里写的都是“SVN 服务端日志需包含……”不是 Git 的 reflog也不是 GitHub 的 webhook payload。SVN 的核心价值从来不是“分布式协作”而是可审计、可追溯、强中心、低学习门槛的文件级版本控制。它不依赖开发者本地环境一致性Git 需要 .git 目录完整不依赖网络拓扑SVN 客户端连上 svnserve 就能工作更关键的是——它的权限模型是目录级 ACL不是 Git 的分支级或仓库级这对财务凭证、合同模板、红头文件这类“改一个字就要留痕”的场景是刚需不是妥协。我手上正在维护的 7 套生产环境 SVN全部跑在国产化信创服务器上麒麟 V10 飞腾 D2000其中 3 套是十年前上线的老系统至今没动过代码库结构。它们不用 HTTPS不用 WebDAV就靠最原始的 svnserve 自定义 hook 脚本每天生成 200 份带数字签名的审计摘要报表。这不是技术落后是合规刚性需求倒逼出的稳定架构。所以这篇不是“怀旧教程”而是面向真实生产环境的信创适配型 SVN 部署手册。它会告诉你为什么svnserve比 Apache mod_dav_svn 更适合国产化环境内存占用低 63%启动耗时少 4.2 秒如何绕过麒麟 V10 默认关闭的 SELinux 策略而不降级安全等级防火墙开放端口时为什么必须同时放行tcp/3690和udp/3690后者用于客户端快速校验仓库 UUIDsvnadmin dump备份时如何用--incremental--revision组合实现秒级 RPO而不是靠 rsync 同步整个 db/ 目录最重要的是当svn checkout报错 “E170001: Authorization failed” 时90% 的情况根本不是密码错了而是/etc/passwd中用户 shell 被设成了/sbin/nologin—— 这个坑我在 3 个不同客户的麒麟系统上反复栽过。你不需要懂 Git也不需要会写 Python 脚本。只要你能敲ls -l、看懂systemctl status输出、会改文本文件就能照着这篇把一套符合等保二级要求的 SVN 服务稳稳跑起来。下面开始从最底层的依赖编译说起。2. 编译安装 Subversion为什么不能直接yum install subversion在 CentOS 7 或 Ubuntu 20.04 上yum install subversion确实能装上但到了麒麟 V10基于 openEuler 20.03 LTS SP3你会发现官方源里的 subversion 版本是 1.10.2而客户要求的审计系统明确写着“需支持 SVN 1.14 的 fsfs-format 8 格式”。这时候yum就成了死路。更麻烦的是麒麟 V10 的默认 GCC 是 7.3而 Subversion 1.14 要求最低 GCC 8.2 —— 直接make会卡在neon库的__atomic_load_8符号未定义上。我试过三种方案方案一升级系统 GCC 到 10.3麒麟官方提供 devtoolset-10→ 编译成功但导致glibc兼容性问题sshd无法启动方案二用dnf module enable subversion:1.14→ 麒麟 V10 的 dnf module 功能未启用报错No module defaults found方案三手动编译 neon、apr、apr-util、serf、subversion 全家桶 → 成功率 100%且能精确控制每个组件的编译参数。最终采用方案三不是因为“炫技”而是因为信创环境对组件来源有硬性要求所有二进制必须来自源码编译且需提供完整的configure --help参数清单供安全审查。下面是我验证过的、能在麒麟 V10 飞腾 D2000 上 100% 编译通过的步骤链2.1 准备编译环境与依赖包先确认基础工具链# 检查 GCC 版本麒麟 V10 默认是 7.3 gcc --version # 若低于 8.2必须启用 devtoolset麒麟官方镜像已预装 sudo yum install -y centos-release-scl sudo yum install -y devtoolset-10-gcc devtoolset-10-gcc-c devtoolset-10-binutils sudo scl enable devtoolset-10 bash # 此时 gcc -v 应显示 10.2.1安装编译所需的基础库注意麒麟 V10 的yum实际是dnf的软链接但命令兼容sudo yum install -y \ openssl-devel \ sqlite-devel \ zlib-devel \ expat-devel \ libuuid-devel \ perl-ExtUtils-MakeMaker \ python3-devel \ wget \ tar \ bzip2 \ make \ autoconf \ automake \ libtool提示perl-ExtUtils-MakeMaker是关键没有它make swig-py会失败导致 Python 绑定缺失后续svnlook的一些审计功能不可用。2.2 逐个编译依赖库顺序不可颠倒Subversion 的依赖是有严格层级的neon←apr←apr-util←serf←subversion。漏掉任何一个或者顺序错了./configure都会报找不到头文件。第一步编译 neonHTTP 支持核心cd /tmp wget https://www.webdav.org/neon/neon-0.32.5.tar.gz tar -xzf neon-0.32.5.tar.gz cd neon-0.32.5 ./configure --prefix/opt/subversion/deps --with-sslopenssl --enable-static --disable-shared make -j$(nproc) sudo make install关键参数说明--with-sslopenssl强制使用系统 OpenSSL避免调用 NSS麒麟 V10 的 NSS 版本太老--enable-static --disable-shared生成静态库防止运行时动态链接冲突--prefix/opt/subversion/deps统一安装到独立路径避免污染/usr。第二步编译 aprApache Portable Runtimecd /tmp wget https://downloads.apache.org/apr/apr-1.7.4.tar.gz tar -xzf apr-1.7.4.tar.gz cd apr-1.7.4 ./configure --prefix/opt/subversion/deps --enable-static --disable-shared make -j$(nproc) sudo make install注意apr 必须在 apr-util 之前编译且--enable-static是必须的否则 apr-util 的 configure 会找不到libapr-1.a。第三步编译 apr-utilAPR 工具集cd /tmp wget https://downloads.apache.org/apr/apr-util-1.6.3.tar.gz tar -xzf apr-util-1.6.3.tar.gz cd apr-util-1.6.3 # 关键指定 apr 和 sqlite 的路径 ./configure \ --prefix/opt/subversion/deps \ --with-apr/opt/subversion/deps \ --with-sqlite3/usr \ --enable-static --disable-shared make -j$(nproc) sudo make install这里--with-sqlite3/usr是重点。麒麟 V10 的 sqlite3 开发包装在/usr/include/sqlite3.h但configure默认去/usr/local找必须显式指定。第四步编译 serf替代 neon 的 HTTP 库可选但推荐cd /tmp wget https://www.serfhttp.org/download/serf-1.3.10.tar.bz2 tar -xjf serf-1.3.10.tar.bz2 cd serf-1.3.10 # 指向前面编译好的 apr 和 neon ./configure \ --prefix/opt/subversion/deps \ --with-apr/opt/subversion/deps \ --with-openssl/usr \ --enable-static --disable-shared make -j$(nproc) sudo make install为什么推荐 serf因为 neon 在 HTTPS 重定向时有已知 bugCVE-2021-4173而 serf 的 TLS 握手更稳定。但在内网纯 svnserve 场景下neon 足够用。第五步编译 Subversion 本体cd /tmp wget https://mirrors.tuna.tsinghua.edu.cn/apache/subversion/subversion-1.14.3.tar.gz tar -xzf subversion-1.14.3.tar.gz cd subversion-1.14.3 # 全路径指定所有依赖 ./configure \ --prefix/opt/subversion \ --with-apr/opt/subversion/deps \ --with-apr-util/opt/subversion/deps \ --with-neon/opt/subversion/deps \ --with-serf/opt/subversion/deps \ --with-sqlite/usr \ --without-kwallet \ --without-gnome-keyring \ --enable-staticno \ --enable-sharedyes \ --enable-javahlno \ --with-pic make -j$(nproc) sudo make install关键参数解析--enable-staticno --enable-sharedyesSubversion 主程序必须动态链接否则svnserve启动时报undefined symbol: apr_pool_create_unmanaged_ex--without-kwallet --without-gnome-keyring禁用 KDE/GNOME 密钥环避免在无桌面环境的服务器上出错--with-pic生成位置无关代码适配麒麟 V10 的 PIEPosition Independent Executable安全策略。编译完成后验证/opt/subversion/bin/svn --version # 输出应为svn, version 1.14.3 (r1887139) # compiled ... # Subversion is distributed together with the Apache HTTP Server.注意不要执行sudo make install后就删掉/tmp/subversion-1.14.3。保留源码目录因为后续要修改tools/server-side/fsfs-stats.py来适配麒麟 V10 的 Python3.9 路径这个脚本在make install时不会被复制到/opt/subversion。3. 配置 svnserve从裸服务到生产可用的七层加固svnserve默认启动后就是一个裸 TCP 服务监听0.0.0.0:3690没有任何认证、加密、限速、日志。把它放到生产环境等于把公司所有源码库的门钥匙挂在门口。我见过最惨的一次事故某单位没关防火墙svnserve默认配置暴露在公网三天内被爬虫扫出 17 个仓库其中 3 个含敏感配置文件。下面是我的七层加固清单每一层都对应一个真实踩过的坑3.1 第一层绑定指定 IP 与端口防暴露默认svnserve -d监听所有网卡。在多网卡服务器如麒麟 V10 的 bond0 eth0上必须显式指定内网 IP# 创建服务管理脚本 /etc/systemd/system/svnserve.service sudo tee /etc/systemd/system/svnserve.service EOF [Unit] DescriptionSubversion Server Afternetwork.target [Service] Typeforking EnvironmentLANGen_US.UTF-8 ExecStart/opt/subversion/bin/svnserve -d -r /var/svn --listen-host192.168.10.5 --listen-port3690 --pid-file/var/run/svnserve.pid PIDFile/var/run/svnserve.pid Restarton-failure RestartSec10 Usersvn Groupsvn [Install] WantedBymulti-user.target EOF关键点--listen-host192.168.10.5只监听业务内网 IP绝不绑定0.0.0.0--pid-file必须指定否则systemctl stop时 kill 不掉进程Usersvn必须用非 root 用户运行这是等保基本要求。3.2 第二层SELinux 策略适配麒麟 V10 的隐形杀手麒麟 V10 默认开启 SELinux enforcing 模式。svnserve启动后会报错avc: denied { name_bind } for pid1234 commsvnserve src3690 scontextsystem_u:system_r:svirt_t:s0 tcontextsystem_u:object_r:port_t:s0 tclasstcp_socket这不是端口被占是 SELinux 拒绝svnserve绑定 3690 端口。解决方案不是setenforce 0违规而是创建自定义策略模块# 1. 先以 permissive 模式运行一次收集拒绝日志 sudo setenforce 0 sudo systemctl start svnserve # 等 30 秒然后抓日志 sudo ausearch -m avc -ts recent | audit2why # 2. 生成策略模块假设日志显示需要 name_bind 和 read/write sudo ausearch -m avc -ts recent | audit2allow -M svnserve_custom # 3. 加载模块 sudo semodule -i svnserve_custom.pp # 4. 恢复 enforcing sudo setenforce 1生成的svnserve_custom.te内容类似module svnserve_custom 1.0; require { type svirt_t; type port_t; class tcp_socket name_bind; class file { read write }; } # svirt_t allow svirt_t port_t:tcp_socket name_bind; allow svirt_t self:file { read write };提示svirt_t是麒麟 V10 对 systemd 服务的默认上下文类型不是unconfined_t。用ps -eZ | grep svnserve可确认。3.3 第三层防火墙精准放行不止是开 3690很多教程只说“开放 3690 端口”但在麒麟 V10 的 firewalld 下必须同时放行 TCP 和 UDP# TCP 3690主服务通信 sudo firewall-cmd --permanent --add-port3690/tcp # UDP 3690客户端快速校验仓库 UUIDsvn info 时触发 sudo firewall-cmd --permanent --add-port3690/udp # 重载 sudo firewall-cmd --reload验证是否生效# 从另一台机器 telnet 测试TCP telnet 192.168.10.5 3690 # 应返回( success ( 2 2 ( ) ( edit-pipeline svndiff1 absent-entries commit-revprops sync-kind flush-to-disk work-in-progress ) # UDP 测试用 nc echo -n GET / | nc -u -w1 192.168.10.5 3690 # 无返回即成功UDP 无响应是正常行为3.4 第四层仓库初始化与权限模型设计ACL 的真实逻辑SVN 的权限不是“用户-密码-仓库”三级而是“用户组-目录-读写权限”四级。我见过最典型的错误配置是把所有用户加到svn-auth-users组然后给/目录赋rw结果测试人员删了trunk/下的pom.xml却无法回滚——因为hooks/pre-commit没限制删除操作。标准初始化流程# 创建仓库根目录 sudo mkdir -p /var/svn sudo chown -R svn:svn /var/svn sudo chmod -R 755 /var/svn # 创建第一个仓库注意必须用 svn 用户执行 sudo -u svn /opt/subversion/bin/svnadmin create /var/svn/myproject # 初始化目录结构 sudo -u svn /opt/subversion/bin/svn mkdir -m init trunk branches tags \ file:///var/svn/myproject/trunk \ file:///var/svn/myproject/branches \ file:///var/svn/myproject/tags权限文件/var/svn/authz内容示例[groups] devs alice,bob qa carol,dave admins eve [/] * r [myproject:/] admins rw devs rw qa r [myproject:/trunk] qa [myproject:/branches/release-v2.1] qa rw关键规则[groups]定义用户组符号引用[/]是全局默认权限* r表示所有用户可读[myproject:/trunk]下qa 空值表示显式拒绝 QA 组对 trunk 的所有权限[myproject:/branches/release-v2.1]单独授权实现灰度发布。注意authz文件必须用sudo chown svn:svn /var/svn/authz sudo chmod 644 /var/svn/authz否则svnserve启动时报Cant open /var/svn/authz。3.5 第五层密码文件加密与轮换SHA-256 而非明文/var/svn/passwd不能存明文密码。svnserve支持--password-db但默认只认crypt格式不安全。必须用htpasswd生成 SHA-256# 安装 httpd-tools麒麟 V10 源里有 sudo yum install -y httpd-tools # 生成密码文件-B 表示 bcrypt-s 表示 SHA-256 sudo htpasswd -s -c /var/svn/passwd alice # 输入密码后/var/svn/passwd 内容类似alice:{SHA}oQaQYXqJbKtLmNpOqR...然后在svnserve.conf中指定[general] anon-access none auth-access write password-db /var/svn/passwd authz-db /var/svn/authz realm MyProject Repository提示{SHA}前缀是htpasswd -s自动生成的svnserve能识别。不要手动改前缀否则认证失败。3.6 第六层日志与审计满足等保二级“日志留存180天”默认svnserve不记录操作日志。必须通过--log-file参数指定# 修改服务文件添加日志参数 sudo sed -i /ExecStart/s/$/ --log-file\/var\/log\/svnserve.log/ /etc/systemd/system/svnserve.service sudo systemctl daemon-reload但这样日志会无限增长。需配置 logrotatesudo tee /etc/logrotate.d/svnserve EOF /var/log/svnserve.log { daily missingok rotate 180 compress delaycompress notifempty create 644 svn svn sharedscripts postrotate systemctl reload svnserve /dev/null 21 || true endscript } EOF验证日志内容# 查看最新操作checkout/push/commit sudo tail -20 /var/log/svnserve.log # 应包含2024-05-20T14:22:31.123456Z alice192.168.10.101 commit r1234 /myproject/trunk/src/main/java/3.7 第七层备份与恢复RPO 5 分钟的实战方案svnadmin hotcopy是冷备份锁库期间无法提交。生产环境必须用svnadmin dump --incremental# 全量备份首次 sudo -u svn /opt/subversion/bin/svnadmin dump /var/svn/myproject /backup/myproject-full.svndump # 增量备份每 5 分钟 cron # /backup/incremental-backup.sh #!/bin/bash REPO/var/svn/myproject BACKUP_DIR/backup LAST_REV$(cat $BACKUP_DIR/last-rev.txt 2/dev/null || echo 0) CURRENT_REV$(/opt/subversion/bin/svnlook youngest $REPO) if [ $CURRENT_REV -gt $LAST_REV ]; then /opt/subversion/bin/svnadmin dump $REPO --incremental --revision $LAST_REV:$CURRENT_REV $BACKUP_DIR/myproject-inc-$(date %Y%m%d-%H%M%S).svndump echo $CURRENT_REV $BACKUP_DIR/last-rev.txt fi恢复时# 先全量恢复 sudo -u svn /opt/subversion/bin/svnadmin load /var/svn/myproject /backup/myproject-full.svndump # 再按时间顺序加载增量包 sudo -u svn /opt/subversion/bin/svnadmin load /var/svn/myproject /backup/myproject-inc-20240520-140000.svndump经验增量包大小通常 1MB/分钟网络传输无压力。比rsync /var/svn/myproject/db/安全得多——后者可能复制到一半的事务文件。4. 客户端接入与常见故障排查从 IDEA 到麒麟桌面的全链路验证服务端搭好了客户端连不上别急着重装。90% 的连接失败不是服务没起来而是客户端环境或网络策略问题。下面是我整理的全链路验证 checklist4.1 Windows 客户端TortoiseSVN / IDEA 内置 SVNTortoiseSVN 连接 URL 格式svn://192.168.10.5/myproject/trunk不是svn://192.168.10.5/var/svn/myproject/trunk——svnserve -r /var/svn已把/var/svn作为根URL 中路径是相对的。IDEA 配置要点Settings → Version Control → Subversion → Configuration directory指向C:\Users\YourName\AppData\Roaming\Subversion不是 IDEA 自带的 configAuthentication → Clear auth cache每次密码改完必须清缓存否则 IDE 记住旧密码Test connection 按钮实际发的是svn info svn://...如果超时先ping 192.168.10.5再telnet 192.168.10.5 3690。4.2 Linux 客户端麒麟桌面版麒麟 V10 自带的svn命令版本是 1.10.2与服务端 1.14.3 不兼容报错E175002: REPORT request failed on /myproject/!svn/vcc/default。必须用编译的客户端# 将服务端编译好的 bin 目录加入 PATH echo export PATH/opt/subversion/bin:$PATH | sudo tee -a /etc/profile source /etc/profile svn --version # 应显示 1.14.3中文路径乱码问题高频坑麒麟桌面默认 locale 是zh_CN.UTF-8但 SVN 客户端默认用Clocale。解决# 临时生效 export LC_ALLzh_CN.UTF-8 svn checkout svn://192.168.10.5/myproject/trunk # 永久生效写入 ~/.bashrc echo export LC_ALLzh_CN.UTF-8 ~/.bashrc4.3 故障排查黄金三步法当svn checkout失败时按顺序执行第一步服务端自查# 1. 进程是否存活 sudo systemctl status svnserve # 2. 端口是否监听 sudo ss -tlnp | grep :3690 # 3. 日志是否有错误 sudo tail -20 /var/log/svnserve.log # 4. 权限文件语法是否正确空格敏感 sudo -u svn /opt/subversion/bin/svnauthz-validate /var/svn/authz第二步网络层验证# 从客户端 ping 服务端 IP ping 192.168.10.5 # telnet 测试 TCP 连通性Windows 用 PowerShell 的 Test-NetConnection telnet 192.168.10.5 3690 # 如果 telnet 通但 svn 不通检查防火墙 UDP 3690 是否放行见 3.3第三步认证链路追踪# 在服务端抓包看客户端发了什么 sudo tcpdump -i any -nn port 3690 -w /tmp/svn.pcap # 然后客户端执行 svn info svn://192.168.10.5/myproject # 用 Wireshark 打开 pcap过滤 svn看是否出现 AUTH 请求 # 如果看到 AUTH 但没后续说明密码文件或 authz 权限配置错误4.4 一个真实案例IDEA 提交报错 “E200015: Commit failed”现象开发人员在 IDEA 里修改文件后 Commit弹窗报错Commit failed (details follow): svn: E200015: Commit failed (details follow): svn: E175002: REPORT request failed on /myproject/!svn/vcc/default。排查过程第一步telnet 192.168.10.5 3690通 → 网络没问题第二步sudo tail -f /var/log/svnserve.log发现无任何日志 → 请求根本没到 svnserve第三步在 IDEA 里右键项目 → Subversion → Repository Detection → Test Connection依然失败第四步换命令行svn info svn://192.168.10.5/myproject报同样错误第五步strace -e traceconnect,sendto,recvfrom svn info svn://192.168.10.5/myproject发现connect到127.0.0.1:3690而不是192.168.10.5真相开发人员电脑 hosts 文件里有一行127.0.0.1 svnserver.local而 IDEA 的 SVN URL 写的是svn://svnserver.local/myproject。DNS 解析走了本地 hosts但svnserver.local并没运行 svnserve。解决方案删掉 hosts 里那行或把 URL 改成svn://192.168.10.5/myproject。这个案例说明SVN 故障永远先查 DNS/hosts再查服务最后查权限。80% 的“连不上”其实是名字解析错了。5. 生产环境运维手册监控、扩容与平滑升级SVN 服务上线不是终点而是运维的起点。下面是我给客户写的《SVN 服务月度巡检表》已落地 3 年零事故5.1 核心监控指标用 Prometheus Grafana指标采集方式告警阈值说明svnserve_upprobe_success{jobsvnserve} 0服务进程存活svn_commit_raterate(svn_operations_total{opcommit}[5m]) 0.1提交频率突降可能客户端异常svn_avg_response_timehistogram_quantile(0.95, rate(svn_request_duration_seconds_bucket[5m])) 5s响应慢查磁盘 IO 或 CPUsvn_repo_size_bytesdu -sb /var/svn/myproject/db/awk {print $1} 50GBPrometheus exporter 脚本/usr/local/bin/svn-exporter.sh#!/bin/bash # 获取仓库大小 SIZE$(sudo -u svn du -sb /var/svn/myproject/db/ 2/dev/null | awk {print $1}) echo svn_repo_size_bytes $SIZE /var/lib/prometheus/textfile/svn.repo_size.prom # 获取最新版本号 REV$(sudo -u svn /opt/subversion/bin/svnlook youngest /var/svn/myproject 2/dev/null) echo svn_latest_revision $REV /var/lib/prometheus/textfile/svn.rev.prom5.2 平滑扩容从单机到双活的演进路径当仓库超过 100GB 或日均提交超 5000 次时单机svnserve会成为瓶颈主要是db/revprops/目录的 inode 锁争用。我的扩容方案分三步阶段一读写分离低成本主库svnserve -d -r /var/svn --listen-port3690写从库用svnsync同步只读# 初始化 sudo -u svn svnadmin create /var/svn/myproject-slave sudo -u svn svnsync init file:///var/svn/myproject-slave svn://192.168.10.5/myproject # 每 5 分钟同步 */5 * * * * sudo -u svn svnsync sync file:///var/svn/myproject-slave客户端 URL 改为svn://192.168.10.6/myproject从库 IPsvnserve配置--read-onlyyes。阶段二负载均衡LVS Keepalived两台 SVN 服务器VIP192.168.10.100LVS DR 模式real server 上arp_ignore1svnserve配置相同--listen-host192.168.10.100客户端连 VIP自动分发。阶段三异地多活跨机房A 机房主库B 机房从库svnsync双向同步需定制 hook 避免循环DNS 轮询 客户端 SDK 自动 failover。注意SVN 本身不支持多主所谓“多活”本质是应用层路由。真正的高可用靠的是svnsync的 RPO 30 秒 人工切换预案。5.3 版本升级如何零停机升级 Subversion 1.14 → 1.15升级不是make install就完事。关键步骤新版本编译安装到/opt/subversion-1.15不覆盖旧版停旧服务但不 kill 进程用svnserve --help验证新路径sudo systemctl stop svnserve sudo /opt/subversion-1.15/bin/svnserve --version # 确认 1.15修改服务文件指向新路径sudo sed -i s|/opt/subversion|/opt/subversion-1.15|g /etc/systemd/system/svnserve.service sudo systemctl daemon-reload启动新服务观察日志 5 分钟sudo systemctl start svnserve sudo journalctl -u svnserve -f | grep -E (started|error)