上一篇 下一篇 分享链接 返回 返回顶部

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

发布人:Minchunlin 发布时间:2025-09-14 11:22 阅读量:622


夜里 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. 优化技巧清单(踩坑后总结)

  1. 连接洪峰:tcp_backlog + somaxconn 提前拉高,ulimit -n 别忘;
  2. AOF rewrite 抢资源:设置 auto-aof-rewrite-min-size ≥ 512MB,错峰在业务低谷;
  3. RSS 飙升:jemalloc + 监控碎片率,开启懒释放:lazyfree-* yes;
  4. 键爆炸/大键:对 Set/Hash 控制成员数,必要时采用分片键(如 tokenidx:{uid}:{shard});
  5. 跨防火墙 NAT:Cluster bus 16379 别漏,cluster-announce-ip/port 正确配置(否则 MOVED/ASK 乱飞);
  6. DNS 解析:集群节点最好用 固定 IP,不要依赖频繁变更的 DNS;
  7. 时钟漂移:chrony 同步多个上游,makestep 打开,避免选举抖动;
  8. 透明大页/NUMA:一定关闭,否则遇上莫名其妙的延迟抖动;
  9. 容器化:k8s 里 hostNetwork + 固定 NodePort 暴露,务必处理 CLUSTER_ANNOUNCE_*,并给 Pod ulimit;
  10. 安全:生产禁用危险命令、强密码、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)。

如果你也在香港支撑海外登录,这份笔记可以直接拿去复用;若你的流量分布或安全策略与我不同,也欢迎在此基础上做自己的“二次标定”。