# RedisKeeper **Repository Path**: cmx1998/redis-keeper ## Basic Information - **Project Name**: RedisKeeper - **Description**: 工业化级别的 Redis 双机高可用守护进程设计方案,完全用 Go 实现,不依赖 Keepalived。它通过管理浮动 VIP 和自动切换 Redis 角色,在两台物理机上实现“互为主备”的自动故障转移。 - **Primary Language**: Go - **License**: MulanPSL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-04-30 - **Last Updated**: 2026-05-20 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Redis Keeper 工业级 Redis 双机高可用守护进程,纯 Go 语言实现,不依赖 Keepalived。通过 gRPC 双向流进行心跳通信,自动管理浮动 VIP,监测 Redis 主备角色并执行故障切换,确保两台机器上的 Redis 互为主备。 ## 特性 - **高可用自动切换**:基于事件驱动的状态机(Init/Master/Backup),支持故障检测、自动提升、脑裂防护 - **轻量级通信**:gRPC 双向流交换节点状态、健康信息和任期(Term),无外部依赖 - **浮动 VIP 管理**:通过 netlink 绑定/移除虚拟 IP,实现客户端透明切换 - **任期与冷静期**:防止脑裂和角色振荡,支持优先级抢占 - **灵活配置**:YAML 配置文件,可禁用 VIP、调整超时和冷静期 - **监控接口**:提供 HTTP `/healthz` 和 `/status` 端点,便于集成外部监控 - **纯静态编译**:交叉编译生成无依赖 ELF 二进制,部署简单 ## 软件架构 ``` ┌─────────────┐ gRPC 双向流 ┌─────────────┐ │ Node A │◄─────────────────────────►│ Node B │ │ keeper │ 心跳/状态/任期 │ keeper │ │ Redis │ │ Redis │ │ (master) │ │ (backup) │ └──────┬───────┘ └──────┬───────┘ │ VIP (192.168.1.100) │ └──────────────┬───────────────────────────┘ │ 客户端通过 VIP 访问 ``` 模块划分: - **心跳与通信**:`internal/peer` 实现 gRPC 服务端/客户端,双向流推送状态 - **VIP 管理**:`internal/network` 通过 netlink 或 `ip` 命令绑定/移除浮动 IP - **状态机**:`internal/engine` 实现事件驱动的状态转换,处理脑裂、超时、任期 - **Redis 操作**:`internal/redis` 基于 `go-redis` 的健康检查、主从切换 - **配置管理**:`config` 包加载 YAML 配置,支持命令行指定路径 - **监控**:`internal/engine` 提供 HTTP 监控端点 ## 快速开始 ### 环境要求 - **操作系统**:Linux(CentOS 7+/Debian 9+/Ubuntu 18.04+),内核需支持 netlink - **Redis**:两台机器各运行一个 Redis 实例(推荐 7.x),开启持久化 - **权限**:运行 keeper 需要 `NET_ADMIN` 权限(绑定 VIP),建议 root 或 `setcap` - **网络**:两台机器之间需放行 TCP `50051`(gRPC)和 `6399`(Redis 复制),VIP 需与机器处于同一二层网络 - **编译环境**:Go 1.21+(仅编译时需要) ### 获取源码 ```bash git clone <你的仓库地址> redis-keeper cd redis-keeper ``` ### 编译(重要) 本项目入口在 `cmd/keeper/main.go`,必须为目标 Linux 系统生成**纯静态 ELF 二进制**。 #### ✅ 正确编译方式 **Windows PowerShell(交叉编译)** ```powershell $env:CGO_ENABLED='0' $env:GOOS='linux' $env:GOARCH='amd64' go build -o bin/redis-keeper ./cmd/keeper/ ``` **Linux/macOS 本地编译** ```bash CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/redis-keeper ./cmd/keeper/ ``` > **为什么必须 `CGO_ENABLED=0`?** > 禁用 CGO 后生成的是纯静态二进制,无需任何 C 库(如 glibc),可部署到任何 Linux 发行版,避免 `status=203/EXEC` 错误。 其他平台交叉编译示例: | 目标环境 | 编译命令(Linux 作为编译机) | |----------------|------------------------------| | Linux amd64 | `CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/redis-keeper ./cmd/keeper/` | | Linux arm64 | `CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o bin/redis-keeper ./cmd/keeper/` | 编译后可检查文件类型(在目标机上执行): ```bash file bin/redis-keeper # 输出:ELF 64-bit LSB executable, x86-64, statically linked ... ``` ### 部署 使用项目自带的 `deploy.ps1`(Windows)或手动操作: ```bash # 上传二进制和配置到目标服务器 scp bin/redis-keeper config/config.node1.yaml kgooer@192.168.1.217:~/redis-keeper/ ssh kgooer@192.168.1.217 "cd ~/redis-keeper && chmod +x redis-keeper" # 启动(必须提供配置文件) sudo ./redis-keeper --config config.node1.yaml ``` #### systemd 服务(推荐) ```ini [Unit] Description=Redis Keeper After=network.target [Service] Type=simple User=root WorkingDirectory=/home/kgooer/redis-keeper ExecStart=/home/kgooer/redis-keeper/bin/redis-keeper --config /home/kgooer/redis-keeper/config.yaml Restart=always RestartSec=5 [Install] WantedBy=multi-user.target ``` ```bash sudo systemctl daemon-reload sudo systemctl enable redis-keeper sudo systemctl start redis-keeper ``` ## 使用说明 ### 配置文件示例 **节点 A 配置 `config.node1.yaml`:** ```yaml node_id: "node-1" vip: "192.168.1.100/32" interface: "eth0" vip_enabled: true # 是否启用 VIP(若双机不同网段可设为 false) local_redis: addr: "127.0.0.1:6399" password: "" # 无密码则留空 db: 0 peer: listen_addr: ":50051" # 本机 gRPC 监听端口 addr: "192.168.1.217:50051" # 对端 keeper 地址 redis_addr: "192.168.1.217:6399" # 对端 Redis 地址 timeout: 5s health_check: interval: 3000ms # 健康检查间隔 fail_threshold: 3 # 连续失败多少次判定不可用 timeout: 1000ms election: term_timeout: 5000ms cool_down: 10000ms # 角色切换冷静期(防止振荡) term_increment: 1 promotion_delay: 3s preempt: false # 是否允许抢占(建议 false) http_addr: ":8080" ``` **节点 B 配置**:修改 `node_id`(如 `"node-2"`)、`peer.addr` 和 `peer.redis_addr` 为对端地址,其余相同。 ### 运行与监控 - 启动后,主节点会自动绑定 VIP(可执行 `ip addr show ` 验证) - 查看状态: ```bash curl http://192.168.1.217:8080/status # 返回 JSON:{"node_id":"node-1","state":"master","vip_on":true} ``` - 查看日志: ```bash journalctl -u redis-keeper -f ``` ### 故障切换验证 1. 获取当前 Master: ```bash curl -s http://<任意节点IP>:8080/status | jq .state ``` 2. 停止 Master 上的 Redis 或直接关闭 Master 服务器 3. 观察 Backup 节点日志,约 `promotion_delay` 后提升为新主,并持有 VIP 4. 客户端通过 VIP 访问 Redis 将自动切换到新主(数据可能有秒级丢失) 5. 恢复原 Master 后,它会自动成为 Backup,不会抢占 ## 测试 项目采用分层测试策略,覆盖单元测试、组件测试和集成测试。 ### 运行测试 ```bash # 运行所有单元测试 go test ./internal/... -v # 仅运行状态机测试 go test ./internal/engine -v # 运行集成测试(需额外标签) go test ./test/integration -tags=integration -v # 生成覆盖率报告 go test ./internal/engine -coverprofile=coverage.out go tool cover -html=coverage.out ``` ### 测试目录结构 ``` redis-keeper/ ├── internal/ │ ├── engine/ │ │ └── state_machine_test.go # 状态机单元测试(核心) │ ├── redis/ │ │ └── manager_test.go # Redis 管理器测试 │ └── peer/ │ ├── server_test.go # gRPC 服务端测试 │ └── client_test.go # gRPC 客户端测试 ├── test/ │ ├── mock/ │ │ ├── mock_redis.go # Redis mock │ │ ├── mock_vip.go # VIP mock │ │ └── mock_peer_client.go # PeerClient mock(可选) │ ├── integration/ │ │ └── keeper_test.go # 双节点集成测试 │ └── testutil/ │ └── helper.go # 测试辅助函数 └── README.md ``` ### 核心测试场景 | 测试类型 | 场景 | 验证点 | |---------|------|--------| | 单元测试 | 初始化决策 | node_id 较小的节点成为 master | | 单元测试 | Master Redis 宕机 | 自动降级为 backup | | 单元测试 | 对端失联或对端 Redis 故障 | backup 晋升为 master,任期递增 | | 单元测试 | 脑裂检测 | 优先级较低方降级,最终收敛为单主 | | 单元测试 | 冷静期 | 切换后 N 秒内禁止再次切换 | | 集成测试 | 双节点故障转移 | 真实网络环境下的完整切换流程 | | 集成测试 | 网络分区恢复 | 自动恢复单主状态 | ## 常见问题与踩坑记录 ### 1. systemd 启动报错 `status=203/EXEC` - **原因**:交叉编译时未设置 `CGO_ENABLED=0`,生成的二进制动态链接了 glibc,而目标系统缺少相应库。 - **解决**:编译时**必须**禁用 CGO: ```bash CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/redis-keeper ./cmd/keeper/ ``` ### 2. Redis 认证错误 `ERR AUTH called without any password` - **原因**:配置文件中 `local_redis.password` 设置了非空值,但实际 Redis 实例没有配置密码。 - **解决**: - 确保 Redis 启动命令**不含** `--requirepass`(无密码模式)。 - 将配置文件中的 `password` 字段设为空字符串 `""`。 - 代码已优化:当密码为空时不发送 `AUTH` 命令。 ### 3. VIP 绑定失败 `add addr: operation not permitted` - **原因**:运行 keeper 的用户不具备添加 IP 地址的权限。 - **解决**: - 用 root 运行,或 - 赋予二进制 `NET_ADMIN` 特权:`sudo setcap cap_net_admin+ep /path/to/redis-keeper`。 - **注意**:即使 `vip_enabled: false`,代码仍会检查权限,建议仍设置该能力。 ### 4. 跨网段 VIP 导致 SSH 会话立即断开 - **现象**:VIP 与管理 IP 不在同一子网时,添加 VIP 后 Linux 可能改变源地址选择,导致远程连接中断。 - **根因**:VIP 必须与管理 IP 处于**同一二层网络(同一广播域)**,否则路由策略错乱。 - **建议**:若双机不在同一 VLAN,应设置 `vip_enabled: false`,专注验证 Redis 主备切换和心跳。 ### 5. 对端 gRPC 连接失败 - **检查清单**: - 双方防火墙放行 `50051/tcp`。 - `peer.addr` 和 `peer.listen_addr` 配置正确。 - 用 `telnet <对方IP> 50051` 测试连通性。 ### 6. 端口不一致 - **常见错误**:Redis 运行在 `6399`,但 `local_redis.addr` 误写成 `127.0.0.1:6379`。 - **解决**:统一端口,推荐在部署脚本和配置中明确端口号。 ### 7. 启动后角色不停切换(flapping) - **现象**:日志中反复出现 `Transitioning to MASTER` 和 `Transitioning to BACKUP`。 - **原因**:初始选举逻辑缺失或冷静期未生效。 - **解决**:确保配置了 `election.cool_down`(如 10s),且 `preempt: false`。升级到最新代码,已内置任期和冷静期机制。 ### 8. 启动时出现 `stream not initialized` 警告 - **现象**:启动瞬间日志报 `send local state to peer failed: stream not initialized`。 - **原因**:健康检查循环早于 gRPC 流建立就尝试发送状态。 - **解决**:已通过 `ready` 通道优化,升级后该警告将消失,不影响功能。 ### 9. 跨网段部署时禁用 VIP 的必要性 - **场景**:两台机器管理 IP 不在同一子网(如 192.168.1.x 和 192.168.6.x),VIP 无法通过 ARP 广播正常工作。 - **建议**:测试时设置 `vip_enabled: false`,仅验证 Redis 主备切换和心跳。VIP 功能留待生产环境双机处于同一 VLAN 时启用。 ## 贡献 欢迎提交 Issue 和 Pull Request。请确保: - 新功能或修复有对应的单元测试 - 通过 `go test ./...` 和 `go vet ./...` - 代码符合 Go 标准格式(`gofmt`) ## 许可证 [根据你的实际情况填写,如 MIT License]