Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,32 @@ systemctl enable --now rsync-proxy.service
cp logrotate.conf /etc/logrotate.d/
```

### 零停机更新(Linux)

运行中的版本具备升级协议后,可以先原子替换安装路径上的二进制,再执行:

```shell
sudo install -m 0755 rsync-proxy.new /usr/bin/rsync-proxy.next
sudo mv /usr/bin/rsync-proxy.next /usr/bin/rsync-proxy
sudo /usr/bin/rsync-proxy --config=/etc/rsync-proxy/config.toml upgrade --timeout=30s
```

`upgrade` 等待新进程就绪并完成监听交接后返回,不等待旧连接结束。已有明文/TLS 传输、握手和排队连接留在原进程;新连接由新进程接收。可以在旧连接未结束时继续升级,形成多代共存。`reload` 仍然只重读配置;`systemctl restart` 是普通重启,不是零停机更新入口。

- `[proxy].state_dir` 默认为 `/run/rsync-proxy`,保存 supervisor 的 Unix socket 和进程锁文件。全局限额、FIFO 队列和各代快照保存在 supervisor 内存中,不使用数据库。手动运行时须使用可写目录,各实例须使用独立目录;不要在进程运行期间删除 socket 或锁文件。
- `[proxy].upgrade_drain_timeout` 单位为秒,默认 `0`。非零值从该代停止接收新连接时起计算,到期关闭剩余连接,包括握手和排队。采用本次成功升级的新配置值;后续更新不会改变或重置更早各代的期限。默认不增加期限,原有 idle/max-duration/throughput 规则继续生效。
- 所有代合计执行上游并发、排队和单 IP 限额,排队按全局 FIFO 晋升。调低限额不会主动断开已有连接;等待占用降到新限额以下再放行。旧连接继续使用已经选定的上游目标。
- 热更新要求 `listen`、`listen_tls`、`listen_http` 和状态目录保持一致。新程序、配置、证书或协议不兼容,或就绪超过 `--timeout`,会报告失败并保留旧进程。准入操作不做磁盘写入,队列变化通过 IPC 通知 worker。
- `connections`、`/status` 和 `/metrics` 汇总各代,逐连接数据新增 `generation` 标识;Go/runtime 指标属于当前接收连接的 worker。跨代快照每秒发布,正常退出前发布最终值,业务累计指标保留已退出代的贡献;异常退出可能损失最后一次未发布的统计。一次完全停止后的启动会重置运行状态。
- 使用配套 systemd unit,主 PID 始终是 supervisor。supervisor 保留监听 FD,通过 Unix socket 协调 worker;`upgrade` 启动安装路径上的新 worker,并让旧 worker 排空连接。普通停止仍受 systemd 的停止超时约束。旧 worker 仍有连接时会继续占用内存和 FD,可通过退出期限限制。
- supervisor 确认 worker 退出后回收其额度;当前 worker 意外退出时,会从安装路径启动替代 worker。supervisor 或控制连接异常失效时,worker 停止接收并关闭连接,不会另建空状态继续放行。
- `upgrade` 更新 worker,不替换常驻 supervisor;修改 supervisor 实现或 IPC 协议后,需要普通重启才能应用这部分更新。不兼容的 worker 会被拒绝,旧 worker 继续服务。
- 日志轮转使用 `rsync-proxy --config=... reopen-logs`,让所有存活的代重新打开日志。升级与日志重开命令读取配置中的管理地址,显式 `--host` 可覆盖。

**首次从不支持 supervisor 协议的旧版(包括 SQLite 版本)迁入需要一次普通重启。** deb 升级会对正在运行的服务调用 `upgrade`,失败不会自动强制重启;首次迁入或不兼容升级时,检查错误后手动 `systemctl restart rsync-proxy`,再完成包配置。进程崩溃、主机重启、主动停止和配置的连接超时不属于零停机保证。

开发验证:`go test -race ./...` 包含真实子进程的多代交接、supervisor IPC 并发调度及故障测试。真实用户级 systemd 验证可运行 `python3 test/systemd/upgrade.py --binary /absolute/path/to/rsync-proxy`;该脚本只创建临时测试 unit、回环监听和临时目录。

### 安装 fail2ban filter

```shell
Expand Down Expand Up @@ -90,7 +116,7 @@ cp fail2ban/filter.d/* /etc/fail2ban/filter.d/
| `relay_idle_timeout` | int 秒 | 0 | relay 阶段双向无 I/O 多久后关闭连接。语义同 rsyncd `timeout`。 | `600` |
| `relay_max_duration` | int 秒 | 0 | relay 阶段总时长上限。超时关闭,rsync 客户端通常会重连续传。 | `14400`(4h) |
| `tcp_keepalive` | int 秒 | 0 | 客户端连接和上游连接的 TCP keepalive 周期,0 沿用 OS 默认(通常 ~2h)。 | `120` |
| `per_ip_max_active_connections` | int | 0 | 单 IP 对单上游最多并发 relay 连接数,proxy-wide 默认值。NAT/校园出口 IP 时取值需放宽。 | `4` |
| `per_ip_max_active_connections` | int | 0 | 单 IP 对单上游最多同时持有的连接数(含排队),proxy-wide 默认值。NAT/校园出口 IP 时取值需放宽。 | `4` |
| `dial_timeout` | int 秒 | 0 | 拨号上游的超时;0 沿用内核 SYN 重试(~75s)。 | `5`(LAN) |
| `min_throughput_bytes` | int64 字节 | 0 | relay 阶段最近 `min_throughput_window` 秒内累计收发须 ≥ 此值,否则视作慢速吸血并关闭。0 关闭整组检查。 | `1048576` |
| `min_throughput_window` | int 秒 | 60 | 上述滑动窗口长度。 | `60` |
Expand All @@ -109,7 +135,7 @@ cp fail2ban/filter.d/* /etc/fail2ban/filter.d/
| `discover_modules` | bool | false | 启动/reload 时自动从上游拉 module 列表。上游不可达会导致启动失败。与 `modules` 二选一。 |
| `use_proxy_protocol` | bool | false | 与上游通信时附加 PROXY protocol 头,便于上游记录真实客户端 IP。需上游 rsyncd 启用 PROXY protocol 支持。 |
| `max_active_connections` | int | 0 | 该上游最大并发 relay 连接数,0 表示不限制。 |
| `max_queued_connections` | int | 0 | 达到上限后排队的最大长度,0 表示不排队。 |
| `max_queued_connections` | int | 0 | 达到上限后排队的最大长度,0 表示不限排队长度。 |
| `per_ip_max_active_connections` | int | 0 | 覆盖 `[proxy]` 中的同名值;0 表示继承 proxy-wide 默认。 |

# 监控
Expand Down
14 changes: 12 additions & 2 deletions assets/config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ error_log = "/var/log/rsync-proxy/error.log"
tls_cert_file = "/etc/rsync-proxy/tls/server.crt"
tls_key_file = "/etc/rsync-proxy/tls/server.key"

# Supervisor Unix socket and process lock directory (Linux).
# Use a local directory writable by the service user, never NFS.
#state_dir = "/run/rsync-proxy"

# Maximum seconds to drain a retiring generation after a successful upgrade.
# 0 (default) adds no deadline; existing connection protection rules still apply.
# Each retiring generation keeps the timeout assigned when it stopped accepting.
#upgrade_drain_timeout = 0

motd = "Served by rsync-proxy (https://github.com/ustclug/rsync-proxy)"

# Idle timeout (seconds) applied during the bidirectional relay phase
Expand All @@ -37,8 +46,8 @@ motd = "Served by rsync-proxy (https://github.com/ustclug/rsync-proxy)"
# is 120 (2 minutes).
#tcp_keepalive = 0

# Default per-IP per-upstream concurrent active connection cap.
# Limits how many simultaneous active relay connections a single
# Default per-IP per-upstream connection cap (active AND queued).
# Limits how many simultaneous connections a single
# client IP may have to any one upstream, preventing a single client
# from monopolizing upstream slots. 0 (the default) disables the
# limit. A typical value for public mirrors is 4. Each upstream may
Expand Down Expand Up @@ -75,6 +84,7 @@ motd = "Served by rsync-proxy (https://github.com/ustclug/rsync-proxy)"
address = "127.0.0.1:1234"
modules = ["foo"]
max_active_connections = 60
# 0 means unlimited queue length. Limits apply across all live generations.
max_queued_connections = 60
# Override the proxy-wide per_ip_max_active_connections for this
# upstream. 0 means inherit the proxy-wide default.
Expand Down
2 changes: 1 addition & 1 deletion assets/logrotate.conf
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@
missingok

postrotate
systemctl reload rsync-proxy.service
/usr/bin/rsync-proxy --config=/etc/rsync-proxy/config.toml reopen-logs
endscript
}
10 changes: 7 additions & 3 deletions assets/postinst.sh
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,15 @@ if [ "$1" = "configure" ] || [ "$1" = "abort-upgrade" ] || [ "$1" = "abort-decon
deb-systemd-helper update-state rsync-proxy.service >/dev/null || true
fi

# Restart only if it was already started
# Upgrade only a running service. Never turn a failed handoff into a
# disruptive restart; pre-handoff versions need one manual restart.
if [ -d /run/systemd/system ]; then
systemctl --system daemon-reload >/dev/null || true
if [ -n "$2" ]; then
deb-systemd-invoke try-restart rsync-proxy.service >/dev/null || true
if [ -n "$2" ] && systemctl --system is-active --quiet rsync-proxy.service; then
if ! /usr/bin/rsync-proxy --config=/etc/rsync-proxy/config.toml upgrade; then
echo "rsync-proxy: hot upgrade failed; the existing process was retained. Versions without handoff support need one manual systemctl restart rsync-proxy." >&2
exit 1
fi
fi
fi
fi
5 changes: 4 additions & 1 deletion assets/rsync-proxy.service
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
Description=Rsync Proxy
After=network.target
ConditionPathExists=/etc/rsync-proxy/config.toml
StartLimitIntervalSec=10s

[Service]
Type=notify
NotifyAccess=main
KillMode=mixed
Restart=on-failure
StartLimitInterval=10s

ExecStart=/usr/bin/rsync-proxy --config=/etc/rsync-proxy/config.toml
ExecReload=/usr/bin/rsync-proxy --config=/etc/rsync-proxy/config.toml reload
Expand Down
16 changes: 7 additions & 9 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ func SendConnectionsRequest(addr string, stdout, stderr io.Writer) error {
var result struct {
Connections []struct {
Index int `json:"index"`
Generation string `json:"generation"`
RemoteAddr string `json:"remote"`
Module string `json:"module"`
Upstream string `json:"upstream"`
Expand Down Expand Up @@ -176,6 +177,7 @@ func SendConnectionsRequest(addr string, stdout, stderr io.Writer) error {
}),
tablewriter.WithHeaderAutoFormat(tw.Off),
tablewriter.WithAlignment(tw.Alignment{
tw.AlignDefault, // Generation
tw.AlignRight, // Index
tw.AlignRight, // RemoteAddr
tw.AlignDefault, // Module
Expand All @@ -185,9 +187,10 @@ func SendConnectionsRequest(addr string, stdout, stderr io.Writer) error {
tw.AlignRight, // SentBytes
}),
)
table.Header("Index", "Remote", "Module", "Upstream", "Connected", "Received", "Sent")
table.Header("Generation", "Index", "Remote", "Module", "Upstream", "Connected", "Received", "Sent")
for _, conn := range result.Connections {
_ = table.Append([]string{
conn.Generation,
strconv.Itoa(conn.Index),
conn.RemoteAddr,
conn.Module,
Expand Down Expand Up @@ -353,14 +356,7 @@ func New() *cobra.Command {
}

log.SetOutput(cmd.ErrOrStderr())
if err := s.ReadConfigFromFile(true); err != nil {
return fmt.Errorf("load config: %w", err)
}

if err := s.Listen(); err != nil {
return fmt.Errorf("server listen: %w", err)
}
return s.Run()
return runDaemon(s)
},
SilenceUsage: true,
}
Expand All @@ -372,6 +368,8 @@ func New() *cobra.Command {
c.AddCommand(
newConnectionsCmd(),
newReloadCmd(),
newUpgradeCmd(s),
newReopenLogsCmd(s),
newUpstreamModulesCmd(s),
newVersionCmd(),
)
Expand Down
Loading
Loading