通用架构(同城双活)(单点接入)

前言

同城双活(适合中小团体) 解决的主要是 单机房 故障 及突破单机房的 机子的上线

这里主要讲述通用架构,跟上层业务层无关

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:如果觉得有用,麻烦点个赞,加个收藏

相关推荐
麦聪聊数据5 小时前
Web 原生架构如何重塑企业级数据库协作流?
数据库·sql·低代码·架构
程序员侠客行6 小时前
Mybatis连接池实现及池化模式
java·后端·架构·mybatis
bobuddy7 小时前
射频收发机架构简介
架构·射频工程
桌面运维家8 小时前
vDisk考试环境IO性能怎么优化?VOI架构实战指南
架构
一个骇客9 小时前
让你的数据成为“操作日志”和“模型饲料”:事件溯源、CQRS与DataFrame漫谈
架构
鹏北海-RemHusband10 小时前
从零到一:基于 micro-app 的企业级微前端模板完整实现指南
前端·微服务·架构
2的n次方_12 小时前
Runtime 内存管理深化:推理批处理下的内存复用与生命周期精细控制
c语言·网络·架构
前端市界13 小时前
用 React 手搓一个 3D 翻页书籍组件,呼吸海浪式翻页,交互体验带感!
前端·架构·github
文艺理科生13 小时前
Nginx 路径映射深度解析:从本地开发到生产交付的底层哲学
前端·后端·架构