前言
同城双活(适合中小团体) 解决的主要是 单机房 故障 及突破单机房的 机子的上线
这里主要讲述通用架构,跟上层业务层无关
1:可用行
单机 99.9% 年8.76小时不可用
同区多机 99.95% 年4.38小时不可用
跨区多机 99.99% 年52.56分钟不可用
2: 适用区域(同城双活)
东南亚(集中为新加坡部署) 中国大陆(需就近接入) 欧美 (不拆分就得异地多活,拆分为 EU, US-E US-W),日韩等
3: 东南亚(新加坡单点接入 为例)
速度 网上找的
新加坡 → 马来西亚:≈ 20--30ms
新加坡 → 泰国:≈ 30--40ms
新加坡 → 菲律宾:≈ 40--60ms
新加坡 → 印尼 :≈ 40--60ms (需本地存储,实体,排除)
新加坡 → 越南:≈ 30--50ms(需版号,本地存储,实体,排除)
普通云DNS/云解析(开启健康检查)eg:以AZ1 AZ2(跨机房)为例
(1) 缺点 DNS 有缓存,增加/删除后有延迟,所以对游戏不友好
客户端
↓
云 DNS → 解析到 AZ1 Gateway IP / AZ2 Gateway IP(跨 AZ 入口)
↓
AZ1 Gateway + AZ2 Gateway(都属于同一个网关集群)(可以用NGINX/OPENRESTY集群)
↓
【所有后端服务视为一个集群】
登录服 (AZ1)、登录服 (AZ2)
匹配服 (AZ1)、匹配服 (AZ2)
逻辑服 (AZ1)、逻辑服 (AZ2)
(2)优化 增加NLB(网络负载均衡,四层)
NLB 自动做:健康检查 自动剔除坏的 Nginx 自动加入新的 Nginx
客户端
↓
云 DNS → 解析到 NLB(固定 IP/域名,永远不变)
↓
NLB(四层 TCP/UDP 负载均衡)
↓
Nginx 集群(弹性伸缩组,自动扩缩容,跨 AZ)
↓
后端游戏服务(登录/匹配/逻辑/API)
EG:ngx.var.target
1>CLIENT<--通过NLB转发-->NGINX/OPENRESTY集群里的某一台
2>CLIENT 第一条消息发「路由头」 EG:[0xAA][0xBB][ServerID: 1234][...]
3>Nginx(OpenResty)解析前几个字节
读到魔数 + ServerID
查表:ServerID=1234 → 对应后端 10.0.0.123:9000
Nginx 把这条 TCP 连接与后端服务器绑定
记录:conn -> upstream = SceneSvr-1234
preread_by_lua_block 里做,只在连接建立时执行一次,不影响后续转发性能;
IP 黑白名单
连接限流(并发 + 新建速率)
读取魔术路由头
协议合法性校验
灰度发布(选集群)
熔断检查(检查选中的后端)
设置转发目标
matlab
1. IP 黑白名单(最早)
纯内存判断,几乎零成本
恶意 IP 直接拒绝,不浪费后续任何资源
必须最先做
2. 连接级限流(并发连接数 / 新建速率)
防止 CC 攻击、泛洪连接
同样是轻量操作,早拒绝早节省资源
放在名单之后,因为名单已经过滤一批恶意 IP
3. 读取并校验协议头(魔术包)
必须先读包,才能拿到:server_id、uid、version 等
这是后续路由、灰度、熔断的 "数据来源"
不能更早,也不能更晚
4. 基础合法性校验(魔数、长度、版本)
非法协议包直接拒绝
避免脏数据进入后面的路由 / 灰度 / 熔断逻辑
5. 灰度路由决策(按 uid / 版本 / 渠道)
灰度是 "选哪个后端集群" 的逻辑
必须在熔断之前,因为熔断是针对具体后端的
6. 后端熔断检查(针对灰度 / 正式选中的后端)
熔断是 "这个后端能不能用"
必须在路由目标确定之后才能判断
熔断开启 → 拒绝或降级
7. 设置最终转发目标(最后一步)
javascript
worker_processes auto; # Linux 自动使用 CPU 核心数
events {
worker_connections 8192; #1024*8
}
stream {
# 共享内存:所有 worker 都能读写
lua_shared_dict route_dict 100m; # scene_id → backend
lua_shared_dict breaker_dict 10m; # 熔断状态
lua_shared_dict limit_conn_dict 10m; # IP 并发限流
# ====================== 只在 worker 0 做定时同步 ======================
init_worker_by_lua_block {
local ngx = ngx
local worker_id = ngx.worker.id()
-- 只让第 0 号 worker 做同步,其他 worker 不做
if worker_id ~= 0 then
return
end
local redis = require "resty.redis"
local route_dict = ngx.shared.route_dict
-- 同步函数:从 Redis 拉取所有 scene 路由
local function sync_routes()
local red = redis:new()
red:set_timeout(1000)
-- 连接 Redis(改成你的 Redis 地址)
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.log(ngx.ERR, "redis connect failed: ", err)
return
end
--Redis 里的数据格式key->value
--scene:1001 → 10.0.0.10:9000
--scene:1002 → 10.0.0.11:9000
--scene:1003 → 10.0.0.12:9000
-- 假设 Redis key 格式:scene:{scene_id} → ip:port
local keys, err = red:keys("scene:*")
if not keys then
ngx.log(ngx.ERR, "redis keys failed: ", err)
red:close()
return
end
-- 清空旧路由(可选,或增量更新)
route_dict:flush_all()
-- 写入共享内存
for _, key in ipairs(keys) do
local scene_id = string.match(key, "scene:(.*)")
local backend, err = red:get(key)
if backend and backend ~= ngx.null then
route_dict:set(scene_id, backend)
ngx.log(ngx.INFO, "sync scene=", scene_id, " → ", backend)
end
end
-- 连接池复用
red:set_keepalive(10000, 100)
end
-- 每 5 秒同步一次
local ok, err = ngx.timer.every(5, sync_routes)
if not ok then
ngx.log(ngx.ERR, "failed to create timer: ", err)
end
}
# ====================== 网关监听端口 ======================
server {
listen 9000;
preread_by_lua_block {
local ngx = ngx
local var = ngx.var
local req = ngx.req
-- 所有 worker 都只读共享内存,不写
local route_dict = ngx.shared.route_dict
--------------------------------------------------------------------
-- 1. IP 黑名单
--------------------------------------------------------------------
local ip = var.remote_addr
local blacklist = {["1.1.1.1"] = true, ["2.2.2.2"] = true}
if blacklist[ip] then
ngx.exit(ngx.ERROR)
return
end
--------------------------------------------------------------------
-- 2. IP 并发连接限流
--------------------------------------------------------------------
local lc = ngx.shared.limit_conn_dict
local conn_key = "conn:" .. ip
local conn = lc:incr(conn_key, 1, 0, 60)
if not conn or conn > 10 then
if conn then lc:decr(conn_key, 1) end
ngx.exit(ngx.ERROR)
return
end
local ok, err = ngx.on_abort(function()
lc:decr(conn_key, 1)
end)
--------------------------------------------------------------------
-- 3. 读取安全包:magic(1) + uid(4) + scene_id(4) + ticket(16)
--------------------------------------------------------------------
local pkt_len = 1 + 4 + 4 + 16
local data, err = req.peek(pkt_len)
if not data or #data < pkt_len then
lc:decr(conn_key, 1)
ngx.exit(ngx.ERROR)
return
end
--------------------------------------------------------------------
-- 4. 解析 + 验签
--------------------------------------------------------------------
local magic = string.byte(data, 1)
if magic ~= 0xAA then
lc:decr(conn_key, 1)
ngx.exit(ngx.ERROR)
return
end
local uid = (string.byte(data, 2) << 24) |
(string.byte(data, 3) << 16) |
(string.byte(data, 4) << 8) |
string.byte(data, 5)
local scene_id = (string.byte(data, 6) << 24) |
(string.byte(data, 7) << 16) |
(string.byte(data, 8) << 8) |
string.byte(data, 9)
local ticket = string.sub(data, 10, 10 + 16 - 1)
-- 验签(和登录服同密钥)
local secret = "your_login_server_secret_123"
local raw = uid .. ":" .. scene_id .. ":" .. secret
local calc_ticket = ngx.md5(raw)
if calc_ticket ~= ticket then
lc:decr(conn_key, 1)
ngx.exit(ngx.ERROR)
return
end
--------------------------------------------------------------------
-- 5. 查表:scene_id → 真实 backend(读共享内存)
--------------------------------------------------------------------
local scene_key = tostring(scene_id)
local backend = route_dict:get(scene_key)
if not backend then
lc:decr(conn_key, 1)
ngx.exit(ngx.ERROR)
return
end
--------------------------------------------------------------------
-- 6. 熔断检查
--------------------------------------------------------------------
local brk = ngx.shared.breaker_dict
local brk_key = "brk:" .. backend
if brk:get(brk_key) then
lc:decr(conn_key, 1)
ngx.exit(ngx.ERROR)
return
end
--------------------------------------------------------------------
-- 7. 设置转发目标
--------------------------------------------------------------------
var.target = backend --把后端地址存入连接变量(存在内存,当前连接有效)
req.discard(pkt_len ) -- 消费掉路由头(不再发给后端)
}
proxy_pass $target;
proxy_timeout 3600s;
proxy_connect_timeout 3s;
}
}
其他的 灰度 熔断 开关 也可以加这里,后面有空再继续
4:其他(简诉,后面有空再聊)
跨内网专线:游戏服、DB、Redis、Git、配置中心、日志 / 监控等,必须内网专线打通,不能走公网。否则延迟、丢包、安全都扛不住 。
Redis / MySQL 至少主从,推荐集群:
MySQL:游戏服写量大,至少一主多从,分库分表 + 读写分离;核心服建议集群 / 半同步强一致。
Redis:排行榜、公会、在线状态、缓存,至少主从,高并发场景建议集群(分片 + 主从)。
Git / 配置中心 / 镜像仓库:同样走内网专线,双机房最好各有一套(或至少有副本),避免单机房挂了连代码 / 镜像都拉不到。
5: 容器编排工具
少量或测试 可以用docker compose
中等100实例内 可以 用docker swarm
几百实例 k3s 再大 k8s
建议优先选择高配 服务器

6:如果觉得有用,麻烦点个赞,加个收藏