SaaS平台如何在香港服务器的Debian系统中部署分布式Redis Cluster支撑海外用户登录请求?

夜里 1 点,我们部署在香港葵湾机房的登录业务在晚高峰(美西午后、欧洲清晨)又被 P99 延迟刺了一下,应用层线程在等 Redis 单点回应,登录峰值的毛刺清晰地写在 Grafana 的红线里。我站在铁柜前把网线重新压了一遍,心里做了个决定:把单实例换成 跨机架、分片的 Redis Cluster,同时把系统从老旧镜像迁到 Debian 12。
这篇文章,就是我在香港把 Redis Cluster 从零到稳的完整现场笔记:从硬件与网络、Debian 调优、Redis 编译与 TLS/ACL、Cluster 创建与扩容、到登录业务的键模型、限流脚本、监控告警与演练,再到那些“凌晨 3 点才会踩到”的坑。我尽量把细节写全,让新同学能一步步复现,老手也能对照清单少走弯路。
1. 场景与目标
- 业务:SaaS 海外用户登录(密码校验 + 令牌下发 + 会话与风控计数器)。
- 现状痛点:单实例 Redis 在香港,跨洋 RTT 抽风时,P99 延迟放大;AOF rewrite 抢资源;连接数峰值抖动。
目标:
- 在香港同城多机架部署 Redis Cluster(3 主 3 从),单分片独立故障域;
- 全链路 TLS + ACL,避免明文与误用;
- 登录关键键模型(会话、令牌、限流)标准化,支持 原子操作;
- 可观测性完善(Prometheus + Grafana + 告警门槛);
- 提供 平滑扩容、滚动升级 与 演练 手册。
2. 架构与拓扑(香港同城、多机架)
┌───────────海外用户───────────┐
Anycast/GeoDNS -> HK LB (四层/七层)
│
应用集群 (K8s/VM,cluster-aware client)
│
┌─────城域专线/ToR──────┐
机架A │ 机架B 机架C │
┌──────┴──────┐ ┌──────┴──────┐ ┌──────┴──────┐
│ Redis-01 (主)│ │ Redis-03 (主)│ │ Redis-05 (主)│
│ Redis-02 (从)│ │ Redis-04 (从)│ │ Redis-06 (从)│
└─────────────┘ └─────────────┘ └─────────────┘
Ports: 6379(TLS) / 16379(cluster bus)
- 分片:3 主 3 从 → 集群 16384 槽平均分配;每主有一从,跨机架放置;
- 持久化:AOF v2/混合持久化,RDB 作为冷备;
- 接入:客户端使用 cluster-aware 驱动(Java Lettuce/Jedis、Node ioredis、Go go-redis);
- 异地灾备(可选):新加坡冷备只做 RDB 拉取 + 手工导入(避免跨境链路抖动导致的脑裂)。
3. 硬件与网络选型(我们在 HK 的一套基线)
| 角色 | 机型 | CPU | 内存 | 系统盘 | 数据盘 | 网卡 | OS |
|---|---|---|---|---|---|---|---|
| Redis 每节点 | 1U/2U 服务器 | 16C/32T(Ice Lake/Zen3 以上) | 64–128 GB | NVMe 480 GB | NVMe 1–2 TB(RAID1 或单盘+监控) | 2×10GbE LACP | Debian 12 (bookworm) 最小化安装 |
| 应用节点 | 通用计算 | 8C+ | 16–32 GB | SSD | SSD | 10GbE | Debian/容器 |
带宽:机柜内 10GbE 即可;跨机架走汇聚。MTU 保持 1500,除非链路全域支持 9000(否则容易踩碎包坑)。
4. Debian 系统准备(统一基线)
4.1 基础包与时钟
apt update && apt -y upgrade
apt -y install build-essential tcl pkg-config libssl-dev \
ca-certificates curl git htop chrony \
nftables rsyslog net-tools
systemctl enable --now chronyd
timedatectl set-ntp true
时钟一定要稳:Cluster 选举对时钟漂移敏感。生产里 chrony 比 ntpd 收敛更快。
4.2 内核与 fs 调优(/etc/sysctl.d/99-redis.conf)
vm.overcommit_memory = 1
vm.swappiness = 1
vm.dirty_ratio = 10
vm.dirty_background_ratio = 5
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 8192
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_keepalive_time = 120
fs.file-max = 1048576
sysctl --system
echo 'session required pam_limits.so' >> /etc/pam.d/common-session
cat >/etc/security/limits.d/redis.conf <<'EOF'
redis soft nofile 1000000
redis hard nofile 1000000
EOF
关闭 THP(Transparent Huge Pages)与自动 NUMA(若为多路机器):
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
echo 0 > /proc/sys/kernel/numa_balancing
可写入 rc.local 或 systemd-tmpfiles 持久化。
文件系统建议:ext4 挂载 noatime,nodiratime,discard(NVMe);数据盘独立。
4.3 安全基线(nftables)
cat >/etc/nftables.conf <<'EOF'
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0;
ct state established,related accept
iif "lo" accept
tcp dport {22} ct state new accept
tcp dport {6379,16379} ip saddr { <APP_NETS>/16, <BASTION_IP>/32 } accept
ip protocol icmp accept
reject with icmpx type admin-prohibited
}
}
EOF
systemctl enable --now nftables
5. 安装 Redis(启用 TLS 与 jemalloc)
Debian 源里的版本可能落后;为启用 TLS 和更稳的 jemalloc,我们用官方源码构建(7.2.x+)。
useradd -r -s /usr/sbin/nologin redis
cd /usr/local/src
curl -LO https://download.redis.io/releases/redis-7.2.5.tar.gz
tar xzf redis-7.2.5.tar.gz && cd redis-7.2.5
make BUILD_TLS=yes MALLOC=jemalloc -j$(nproc)
make install PREFIX=/usr/local
生成节点证书(简化示例,自签;生产建议用内网 CA):
mkdir -p /etc/redis/tls && cd /etc/redis/tls
# 生成 CA
openssl req -x509 -newkey rsa:4096 -days 3650 -nodes \
-keyout ca.key -out ca.crt -subj "/CN=redis-ca"
# 生成服务端证书
openssl req -newkey rsa:4096 -nodes -keyout redis.key \
-out redis.csr -subj "/CN=$(hostname -f)"
openssl x509 -req -in redis.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out redis.crt -days 3650
chown -R redis:redis /etc/redis && chmod -R 750 /etc/redis
6. Redis 配置模板(/etc/redis/6379.conf)
port 0
tls-port 6379
tls-cert-file /etc/redis/tls/redis.crt
tls-key-file /etc/redis/tls/redis.key
tls-ca-cert-file /etc/redis/tls/ca.crt
tls-auth-clients no # 我们依赖 ACL,不强制双向
bind 0.0.0.0
protected-mode yes
daemonize no
supervised systemd
pidfile /run/redis_6379.pid
logfile ""
dir /data/redis
cluster-enabled yes
cluster-config-file nodes-6379.conf
cluster-node-timeout 5000
cluster-announce-ip <THIS_NODE_IP>
cluster-announce-tls-port 6379
cluster-announce-bus-port 16379
# 持久化(混合)
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec
aof-use-rdb-preamble yes
auto-aof-rewrite-percentage 50
auto-aof-rewrite-min-size 512mb
# 内存与淘汰
maxmemory 48gb
maxmemory-policy volatile-ttl
lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes
# 安全与 ACL
requirepass "<STRONG_CLUSTER_PASS>"
masterauth "<STRONG_CLUSTER_PASS>"
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command KEYS "KEYS__DISABLED"
# 慢查询与统计
slowlog-log-slower-than 10000
slowlog-max-len 2048
latency-monitor-threshold 100
systemd 单元(/etc/systemd/system/redis@.service):
[Unit]
Description=Redis In-Memory Data Store (%i)
After=network-online.target
Wants=network-online.target
[Service]
User=redis
Group=redis
Type=notify
ExecStart=/usr/local/bin/redis-server /etc/redis/%i.conf --tls-port 6379
ExecStop=/usr/local/bin/redis-cli -h 127.0.0.1 -p 6379 --no-auth-warning -a <STRONG_CLUSTER_PASS> shutdown
LimitNOFILE=1000000
PrivateTmp=yes
ProtectSystem=full
ProtectHome=true
ReadWritePaths=/data/redis
[Install]
WantedBy=multi-user.target
创建数据目录并启动:
mkdir -p /data/redis && chown -R redis:redis /data/redis
systemctl daemon-reload
systemctl enable --now redis@6379
创建 ACL 账户(仅允许登录业务常用命令):
redis-cli -h 127.0.0.1 -p 6379 -a <STRONG_CLUSTER_PASS> \
ACL SETUSER appclient on >appclient-strong-pass< \
~* +@read +@write -FLUSHALL -FLUSHDB -KEYS
7. 初始化 Redis Cluster(3 主 3 从)
在 任意一台 节点上执行,假设 6 个节点 IP:
10.10.0.11, .12, .13, .14, .15, .16(都已启动 redis@6379)
redis-cli --tls \
--cacert /etc/redis/tls/ca.crt \
--cert /etc/redis/tls/redis.crt \
--key /etc/redis/tls/redis.key \
-a <STRONG_CLUSTER_PASS> \
--cluster create \
10.10.0.11:6379 10.10.0.12:6379 10.10.0.13:6379 \
10.10.0.14:6379 10.10.0.15:6379 10.10.0.16:6379 \
--cluster-replicas 1
验证:
redis-cli --tls --cacert /etc/redis/tls/ca.crt -a <STRONG_CLUSTER_PASS> \
-h 10.10.0.11 -p 6379 cluster info
redis-cli --tls --cacert /etc/redis/tls/ca.crt -a <STRONG_CLUSTER_PASS> \
-h 10.10.0.11 -p 6379 cluster slots
8. 登录业务的键模型与接入实践
8.1 键空间约定(含 hash tag,便于多键原子性)
| 类别 | Key 模板 | 值类型 | TTL | 说明 |
|---|---|---|---|---|
| 会话 | sess:{uid} |
Hash | 7d | 会话元数据、设备、上次登录 IP |
| 令牌 | token:{uid}:{jti} |
String | 30m | 短期访问令牌(可存加密后载荷) |
| 令牌索引 | tokenidx:{uid} |
Set | 30m | 当前活跃 jti 清单 |
| 登录尝试计数 | login:try:{ip} |
String | 10m | 失败计数,限流 |
| 人机验证令牌 | captcha:{rid} |
String | 5m | 验证通过即删 |
| 风控指纹 | risk:{uid} |
ZSet | 7d | 时间序列分值、设备指纹 |
采用 {uid} 作为 hash tag,同一用户的会话与令牌落到同一分片,方便 LUA 脚本做原子检测与更新。
8.2 原子限流脚本(登录失败计数)
-- file: incr_try.lua
-- KEYS[1] = login:try:{ip}
-- ARGV[1] = window_secs
-- ARGV[2] = threshold
local cur = redis.call('INCR', KEYS[1])
if cur == 1 then redis.call('EXPIRE', KEYS[1], ARGV[1]) end
if cur > tonumber(ARGV[2]) then return {0, cur} end
return {1, cur}
调用示例(Node.js ioredis):
import { Cluster } from "ioredis";
const cluster = new Cluster(
[
{ host: "10.10.0.11", port: 6379 },
{ host: "10.10.0.12", port: 6379 },
{ host: "10.10.0.13", port: 6379 },
{ host: "10.10.0.14", port: 6379 },
{ host: "10.10.0.15", port: 6379 },
{ host: "10.10.0.16", port: 6379 },
],
{
scaleReads: "slave",
redisOptions: {
tls: { rejectUnauthorized: true, ca: "<ca.crt content>" },
username: "appclient",
password: "appclient-strong-pass",
enableReadyCheck: true,
maxRetriesPerRequest: 2,
keepAlive: 1_200_000, // TCP keepalive 20min
},
slotsRefreshTimeout: 2000,
}
);
const sha = await cluster.script('LOAD', fs.readFileSync('incr_try.lua','utf8'));
const key = `login:try:{${ip}}`;
const [ok, cur] = await cluster.evalsha(sha, 1, key, 600, 10);
if (ok === 0) throw new Error('Rate limited');
8.3 会话创建(同分片操作)
-- file: create_session.lua
-- KEYS[1] = sess:{uid}
-- KEYS[2] = tokenidx:{uid}
-- KEYS[3] = token:{uid}:{jti}
-- ARGV[1] = sess_ttl_sec
-- ARGV[2] = token_ttl_sec
-- ARGV[3] = token_payload
redis.call('HSET', KEYS[1], 'uid', ARGV[3], 'ts', tostring(redis.call('TIME')[1]))
redis.call('EXPIRE', KEYS[1], ARGV[1])
redis.call('SADD', KEYS[2], KEYS[3])
redis.call('EXPIRE', KEYS[2], ARGV[2])
redis.call('SET', KEYS[3], ARGV[3], 'EX', ARGV[2], 'NX')
return 1
令牌吊销:列出 SMEMBERS tokenidx:{uid},批量 DEL 指定 jti,保持同分片原子。
8.4 Java(Lettuce)集群配置示例
RedisURI uri = RedisURI.Builder.redis("10.10.0.11", 6379)
.withSsl(true)
.withAuthentication("appclient", "appclient-strong-pass")
.build();
ClusterClient client = ClusterClient.create();
StatefulRedisClusterConnection<String, String> conn = client.connect(uri);
conn.setAutoFlushCommands(true);
conn.sync().set("ping","pong"); // smoke test
9. 监控与告警(Prometheus + redis_exporter)
安装 exporter:
useradd -r -s /usr/sbin/nologin redis-exp
cd /opt && curl -LO https://github.com/oliver006/redis_exporter/releases/download/v1.62.0/redis_exporter-v1.62.0.linux-amd64.tar.gz
tar xzf redis_exporter-*.tar.gz
cat >/etc/systemd/system/redis-exporter.service <<'EOF'
[Unit]
Description=Redis Exporter
After=network-online.target
[Service]
User=redis-exp
Group=redis-exp
ExecStart=/opt/redis_exporter-v1.62.0.linux-amd64/redis_exporter \
--redis.addr=rediss://127.0.0.1:6379 \
--redis.username=appclient \
--redis.password=appclient-strong-pass \
--log-format=logfmt
Restart=always
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload && systemctl enable --now redis-exporter
Grafana 关键图表:
- redis_cpu_sys, used_memory, mem_fragmentation_ratio
- instantaneous_ops_per_sec, conn_total, blocked_clients
- aof_current_rewrite_time_sec, rdb_last_bgsave_status
- cluster_state, cluster_slots_pfail, cluster_known_nodes
- 业务自定义:登录成功率、限流触发数、令牌签发耗时分布(Histogram)。
告警门槛(经验值,可按实际调整):
- cluster_state != ok 立即;
- used_memory_rss / total_memory > 0.9 连续 5 分钟;
- aof_rewrite_in_progress 持续 > 20 分钟;
- blocked_clients > 0 连续 1 分钟;
- latency-spike(自定义)P99 > 40ms 连续 3 分钟。
10. 压测方法与参考数据(香港机房,10GbE 内网)
使用 redis-benchmark + 应用层压测(wrk/gatling)双管齐下。
redis-benchmark -h 10.10.0.11 -p 6379 --tls --cacert /etc/redis/tls/ca.crt \
-a <STRONG_CLUSTER_PASS> -n 2000000 -c 512 -P 16 -d 64 -t set,get
| 项目 | 单分片 QPS(set/get) | 集群总 QPS(3主) | P95 | P99 |
|---|---|---|---|---|
| 纯内网(无 TLS) | ~200k / 250k | ~600k / 750k | 1.5 ms | 3.2 ms |
| TLS 开启 | ~170k / 210k | ~510k / 630k | 1.9 ms | 4.1 ms |
| 应用实际(混合命令) | —— | ~320k | 3.5 ms | 8–12 ms |
真实登录高峰(美西午后、欧早)迁移后:P99 从 65–120ms 降到 18–35ms,毛刺显著收敛。TLS 有开销,但可接受。
11. 扩容、滚更与演练
11.1 新增分片(加主从 2 台)
# 加入新节点为master
redis-cli --tls ... --cluster add-node 10.10.0.21:6379 10.10.0.11:6379
# 给新master分配槽
redis-cli --tls ... --cluster reshard 10.10.0.21:6379
# 再添加一个replica并指定其复制目标
redis-cli --tls ... --cluster add-node 10.10.0.22:6379 10.10.0.11:6379 --cluster-slave --cluster-master-id <NEW_MASTER_NODE_ID>
11.2 替换故障节点
# 下线旧节点
redis-cli --tls ... --cluster del-node 10.10.0.13:6379 <OLD_NODE_ID>
# 新机同 IP/不同 ID 加回(或使用 cluster replicate)
11.3 滚动升级(小版本)
- cluster failover take-over 将主从切换;
- 停原主、升级、回到从;
- 轮转所有分片,观察 cluster_state 持续 ok。
12. 优化技巧清单(踩坑后总结)
- 连接洪峰:tcp_backlog + somaxconn 提前拉高,ulimit -n 别忘;
- AOF rewrite 抢资源:设置 auto-aof-rewrite-min-size ≥ 512MB,错峰在业务低谷;
- RSS 飙升:jemalloc + 监控碎片率,开启懒释放:lazyfree-* yes;
- 键爆炸/大键:对 Set/Hash 控制成员数,必要时采用分片键(如 tokenidx:{uid}:{shard});
- 跨防火墙 NAT:Cluster bus 16379 别漏,cluster-announce-ip/port 正确配置(否则 MOVED/ASK 乱飞);
- DNS 解析:集群节点最好用 固定 IP,不要依赖频繁变更的 DNS;
- 时钟漂移:chrony 同步多个上游,makestep 打开,避免选举抖动;
- 透明大页/NUMA:一定关闭,否则遇上莫名其妙的延迟抖动;
- 容器化:k8s 里 hostNetwork + 固定 NodePort 暴露,务必处理 CLUSTER_ANNOUNCE_*,并给 Pod ulimit;
- 安全:生产禁用危险命令、强密码、TLS;Jump Server 限源;Fail2ban 针对异常连接。
13. 运维脚本片段(日常好用)
1)节点巡检
#!/usr/bin/env bash
for n in 10.10.0.{11..16}; do
echo "== $n =="
redis-cli --tls --cacert /etc/redis/tls/ca.crt -a $PASS -h $n -p 6379 INFO | egrep 'role:|connected_clients:|used_memory_human:|instantaneous_ops_per_sec:'
done
2)热点 Key 抓取(生产谨慎使用)
redis-cli --tls ... --latency-history
redis-cli --tls ... --hotkeys
3)慢日志导出
redis-cli --tls ... SLOWLOG GET 256 | jq .
14. 容量规划与成本估算(经验公式)
内存:峰值并发 * 每请求峰值键数 * 平均键大小 * 2(碎片+AOF冗余)
例如:峰值 10 万 QPS,平均 3 个键,值平均 200B,活动键总数约 5000 万,则每分片预留 30–40GB 内存较稳妥。
分片数:先 3 主,目标使用率 60% 以下;达到 70% 触发扩容。
带宽:单分片常态 < 2Gbps;AOF rewrite + 复制高峰需保留余量。
成本:优先 NVMe + 充足内存,其次才是更多核。延迟比峰值 QPS 更先影响登录体验。
15. 一周后回到机房
一周后的同一时间,我又站在那排熟悉的机柜前。Grafana 的登录延迟面板像一条安静的河——偶尔的浪花也不再刺眼。夜班同事在旁边喝着冻柠,笑说终于不用盯着报警器熬到天亮。
这一套 Redis Cluster 在香港落地后,我们把“登录是否顺滑”这件事交还给了系统本身:分片消化高并发,TLS 与 ACL 让安全不再靠人盯,AOF 与监控把可预期的风险摁在了可控范围内。
机房门拉开的瞬间,外面是维港的潮湿夜风。我知道,下次再来,大概是为了加两个分片,而不是灭火了。
附:一步一脚印的最小可执行清单(MXP)
- 准备 6 台节点(3 机架分布)+ Debian 12 + 基线安全/内核调优;
- 编译安装 Redis 7.2.x(启用 TLS & jemalloc);
- 配置模板 /etc/redis/6379.conf + systemd + 数据盘;
- TLS 自签证书(或内网 CA 发放);
- 启动各节点 systemctl enable --now redis@6379;
- 创建 Cluster redis-cli --cluster create ... --cluster-replicas 1;
- 创建 ACL appclient;
- 应用改为 cluster-aware 客户端(ioredis/Lettuce),实现键模型与 Lua 原子脚本;
- 接入 Prometheus + redis_exporter + 告警;
- 压测、演练、扩容预案(reshard & add-node 流程写入 runbook)。
如果你也在香港支撑海外登录,这份笔记可以直接拿去复用;若你的流量分布或安全策略与我不同,也欢迎在此基础上做自己的“二次标定”。



