V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
htxy1985
V2EX  ›  程序员

求一款比较成熟的 golang 服务端热更方案

  •  
  •   htxy1985 · 2 天前 · 3097 次点击
    如题,主要用于在有状态的 golang 服务端进行在线修复 bug ,找了几个方案感觉都不是很靠谱。
    AI 提到的 air 和 hotswap 简单看了一下,都不怎么维护了。
    特此来请教大佬们,有没有已经填过坑的较为成熟的框架或方案呢。
    感谢感谢/抱拳
    第 1 条附言  ·  1 天前
    按照我的理解,热更主要分为热更代码,热更资源,热更状态数据。
    我的本意是希望得到一个热更代码的方案,
    由于之前使用 java 开发的游戏服,用 arthas 这类的热更工具用的很舒服,所以就习惯用这样的思路来解决问题。写好代码,重新编译成 class ,向打疫苗那样将新写的东西注入到服务器上下文环境中。这得益于 java 从语言层面就原生支持热更新。现在的环境换成了 go,就有点难受了。
    听了大家的意见,感觉只能是别的思路了。27 ,28 楼这位老哥同行基本描述了我的现状
    我总结了一下,大致有这么几种思路,
    1. go+lua ,这个如果是项目初期还可以这么搞试试,现在基本相当于重做,对于我来讲不合适。
    2. go plugin ,类似 hotswap 之类的,已被提前踩坑的老哥告知行不通,主要原因可能是 pugin 只能加载一次,不能卸载。
    如果是这样的话,确实是个半成品方案。
    3. mosn 或 tableflip 平滑过渡,这个似乎是唯一可行的方向,也就是灰度发布的思路。慢慢将流量转移到新的服务,但是对于游戏服务器(或者说我所在的项目)来讲,目前有个问题就是玩家个体状态和内存数据是可以这样的,但全局状态就有问题了,这些数据理论上不可能同时存在于新旧两个进程。是的没错,目前的状态就是逻辑和状态基本是耦合在一起的/捂脸。按照 27 楼老哥说的,只能是加一层网关,在过渡时将状态数据迁移到正确的进程。无论如何,希望结合 mosn 或 tableflip 无缝接入并解决我的问题都是不太可能的,都需要我做很多额外的修改(当然这也可能是我了解的还不够得出的错误的结论),如果是这样的话,只能借鉴这些思路,自己对服务端进行改造了。
    但是回看这个这个做法,和直接重启服务似乎也没多大的不同了,在短暂的不可用时间里,都需要对客户端进行断线重连的处理。
    这个问题我能想到的唯一解决方案就是把全局数据直接保存在 redis 这种第三方状态数据库中,才能真正实现后台的一系列迁移逻辑,包括复制 tcp 连接之类的优雅操作。
    4.鉴于已经到开发后期了,从有状态的服务改造成无状态的服务这种“大活”就不展开讨论了。
    45 条回复    2025-01-01 14:31:11 +08:00
    wujianhua22
        1
    wujianhua22  
       2 天前
    go 没有服务端热更的方案,你说的都是开发阶段热更的方案。
    DefoliationM
        2
    DefoliationM  
       2 天前 via Android
    无,要么 wasm ,建议结合 k8s 使用,可以滚动更新,业务不中断。
    fffq
        3
    fffq  
       2 天前
    golang+lua
    MoYi123
        4
    MoYi123  
       2 天前
    有状态服务更新用 erlang 热更新都挺麻烦的, go 基本不可能了.
    guanzhangzhang
        5
    guanzhangzhang  
       2 天前
    mosn 的那个可以借鉴下,包括监控 metrics 都可以不听,Prometheus 监控都不会告警
    htxy1985
        6
    htxy1985  
    OP
       2 天前 via Android
    @guanzhangzhang @DefoliationM 老哥能不能稍微展开说下,感谢
    flyqie
        7
    flyqie  
       2 天前 via Android
    golang 因为语言特性的原因,热更不好做。

    你要不试试楼上说的 lua 做 logic ,go 做 framework ?
    guanzhangzhang
        8
    guanzhangzhang  
       2 天前
    @htxy1985 你谷歌搜下 Mosn 热更新,有 ppt 的,之前 gopher 大会听过蚂蚁金服他们讲的,用到的技术可以看下和语言无关的
    dogfood
        10
    dogfood  
       2 天前
    go+quickjs
    htxy1985
        11
    htxy1985  
    OP
       2 天前 via Android
    @guanzhangzhang 感谢,大致看了一下,我理解的这个东西类似网关层的东西,抛开核心技术思路,他的解决方式是老的服务向新的服务进行无损迁移,而不是在线热修代码。这个东西可能有一定学习和落地的成本,我这边再看看。
    @flyqie 可以是可以,但这里面有个改造成本问题,一时间不好弄成这样
    crackidz
        12
    crackidz  
       2 天前
    K8s 或者 Kamal 都有类似的解决方案啊...
    southwolf
        13
    southwolf  
       2 天前
    正常人的做法不都是滚动更新发布新版本服务? 打断几个请求不是太大问题吧, 啥业务啊这么重要还需要在线修 bug...
    htxy1985
        14
    htxy1985  
    OP
       2 天前 via Android
    @Southwolf 有长链接和大量状态数据的游戏服务
    adoal
        15
    adoal  
       2 天前
    可以换个思路,不热更,被中断的用户如果投诉了赔些游戏豆
    standchan
        16
    standchan  
       2 天前
    额,k8s 的滚动更新好像就可以了吧
    james122333
        17
    james122333  
       2 天前 via Android
    做成 cgi 或者全反射写法
    Sendya
        18
    Sendya  
       2 天前 via Android   ❤️ 4
    如果是 tcp 服务,可以用 cloudflare tableflip 做热更新,新二进制替换老二进制之后,发送 USR2 信号,双进程都会服务 tcp 监听,直到老进程的 tcp 连接全部都服务完断开,由新进程完全接管。

    我这边项目用了很久这相关的功能了。
    就跟 nginx 的 USR2 信号行为差不多
    aladdinding
        19
    aladdinding  
       2 天前
    没有什么是不能在加一层就解决的
    dzdh
        20
    dzdh  
       2 天前
    另外一个方案是 systemd 接管 socket tcp 数据会经过内核绕一下子。
    maocat
        21
    maocat  
       2 天前 via Android
    啊,楼主的根本问题不是排查 bug 吗,热更只是一个路子,日志埋点排查业务 bug ,程序启用 pprof 查看系统 profile 信息,这不才是正经路子吗
    sagaxu
        22
    sagaxu  
       2 天前   ❤️ 1
    hotswap 也就开发环境用用,保证不了原子性和正确性。A 任务处理到一半,然后开始热更新,系统应该怎么处理?

    所以现在主流更新,都是先从网关把目标机器的流量切走再更新,再灰度把流量切过去。
    ben1iu
        23
    ben1iu  
       2 天前   ❤️ 1
    go 有状态的热更 没有很好的办法 只能架构层面把 网关拆出去 网关后面的服务 新连接走新逻辑 旧的等连接断开不再接受新请求
    go plugin 方式的热更 需要从代码结构拆分 不推荐
    zihuyishi
        24
    zihuyishi  
       2 天前
    根据网游的经验,就是先上更新的服务器,不给旧的导流量,然后在 deadline 时把旧的切掉。
    实际操作就是 k8s 上新 pod ,然后下旧 pod 。
    pluswu1986
        25
    pluswu1986  
       2 天前   ❤️ 2
    1. goplugin 逻辑和状态分离,2. 大状态能且成足够小的粒度,用 config_route_shard 思路迁移小粒度对象,3. 大状态无法切割用 replicate 主副代理切换 理论上 3 种都能做到无损热更, 如果逻辑和状态强耦合(游戏服务器一般都是这种) 神都救不了, 各种原地热更都是邪教
    Kauruus
        26
    Kauruus  
       2 天前
    这还是从架构角度去整吧。

    有状态服务,无论如何都要处理断线重连、重启的状态迁移问题。

    新启一个实例,状态迁移过去,例如发消息给客户端,让它们连到新的实例。
    sunny352787
        27
    sunny352787  
       2 天前   ❤️ 3
    同行告诉你,没有

    我这边的做法是无状态服务器,数据都塞 redis 里,实时性比较高的拆出去,比如 PVP 单独开一服,打完就断掉连接

    如果你是做 ARPG 或者大世界之类的一定要保持长连接,那就加一层网关,后面连接逻辑服可以接收信号将数据用某种方式(比如 redis 中转)迁移到新进程然后再杀掉自己,这种方法以前写 C++时候就这么干

    你想完全保存内存状态的迁移本质上不现实,代码更新了内存布局肯定有变化,非脚本语言不可能完美迁移的,脚本语言也只能迁移一部分逻辑内存数据变了也是不行的
    bruce0
        28
    bruce0  
       2 天前
    看很多人的回答, 应该是没做过游戏这类的服务器开发, 楼主说的热更方案我之前也查了很多, 纯 go(不用 lua 和 js 这些脚本语言辅助)写的 有状态服务, 目前应该没有好的热更方案, 看很多人说加一个中间层, 这个真没法加,因为游戏的 server 进程有太多的全局状态和数据, 没法做到无感分层(或者非常难) 还有 k8s 滚动更新,这个更不靠谱了, web 这种无状态的服务还可以

    说一下我之前了解到的"热更"方案
    1. 双进程.类似 Nginx 的 reload, 新开一个进程, 新玩家连接到新的进程,老进程只处理老玩家数据,等所有人都下线,自动退出. 这个方案不太可行, 像是游戏内的全局数据, 跨进程同步比较难搞, 全新设计的游戏可以尝试,老游戏没戏,约等于重写

    2. 使用 go plugin, 这个方案对代码改动也不小, 而且 go 的 plugin 官方维护也不积极,还有很多不稳定的东西, 而且使用起来也不方便, 还不如 C/C++ 动态加载 .so 文件好用.

    这两个是我了解到的方案, 都不太好, 再拓展一下, 其实游戏内的很多热更只是更新策划表, 一般是 json,xml 的文件, 这时候可以在游戏内做一个命令, 比如游戏主逻辑内监听一个 web 端口, 收到 web 的命令后重新加载一下所有的表格文件.如果是更新游戏内的逻辑,修复 bug, 没办法. 有一种是游戏类型决定的, 像是 吃鸡, moba 这类的房间型游戏, 其实不用考虑热更, 这局对战完成后, 新的游戏直接用新的二进制启动就行了.像是 mmo 这类的, 如果硬要热更, 只能上 lua,js 这些脚本语言来辅助了.
    yc8332
        29
    yc8332  
       2 天前
    好像没办法吧。。。有状态的感觉是更不了或者说成本很高,而且要一开始就是设计好。。反正现在我们是直接重启,主要是 pvp 的对战或者组队服务
    hackroad
        30
    hackroad  
       2 天前
    蓝绿、A/B
    p1gd0g
        31
    p1gd0g  
       2 天前
    有状态是大坑啊,还是早点改吧
    renshengluguo
        32
    renshengluguo  
       2 天前
    参照英雄联盟的更新方式如何
    rrfeng
        33
    rrfeng  
       2 天前
    vm 都可以在线迁移,go 肯定也可以,只要你肯花时间 🐶
    rrfeng
        34
    rrfeng  
       2 天前
    @renshengluguo 一局游戏就是最大的不可更新时间了,完全没必要热更。MMO 才需要
    luaex
        35
    luaex  
       2 天前
    我之前用过 go plugin + hook 的方案,plugin 主要是用来加载 so ,然后 hook 要修改的函数,跳转到新的 so 里面的函数去执行
    hongjic93
        36
    hongjic93  
       2 天前 via iPhone   ❤️ 1
    goplugin (同一个进程,动态链接库),sidecar (不同进程,ipc ),wasm (进程内部 sandbox ) 。 第一个坑很多,ipc 很麻烦,wasm 最理想,但不成熟( golang 已经支持 wasi 编译)
    htxy1985
        37
    htxy1985  
    OP
       2 天前 via Android
    感谢各位的指教
    realpg
        38
    realpg  
       1 天前
    @southwolf #13
    滚动更新版本,如果不涉及 websocket 超长连接,或者纯 tcp 业务,设计好都不会打断请求,都是平滑的
    如果设计的好,tcp 长连接都可以不打断
    这边就有业务有完全不能缺请求的要求 golang 做的 目前没问题
    southwolf
        39
    southwolf  
       1 天前 via Android   ❤️ 1
    @realpg 所以还是有前提的嘛, rolling update 可以满足 zero downtime, 也就是旧 pod 承接现有连接,新连接去到新 pod, 但并不能保证旧连接能无缝迁移到新版本, 就是楼主要的在线更新
    另外有种玩法, 就是状态信息放在外部, 然后客户端跟服务端搞个双向心跳, 挂了再由另外的服务端重新连接, 但这么一搞工作量又更大了😂
    realpg
        40
    realpg  
       1 天前   ❤️ 1
    @southwolf #39
    我们的 api server 同时处理长短连接
    在逻辑层,api server 可以接收一个外部信号
    这个外部信号接收到以后,就会在 slb 的 service check api 中返回自己已经故障了
    但是正常提供服务
    这样 SLB 就不会发新的请求过来,这样基本只要 30 秒,就不太会有新的短 http 请求到这个 backend 了

    长连接是在逻辑层实现的,一旦这个服务端接收到了外部信号,服务端的长连接就会通知客户端我要死了 你现在跟一个新的后端建立连接,建立连接后,有交接逻辑,在某个信号以后,就全跟新连接通信

    这样基本上只要这个 signal 过来 60 秒,就可以完全无感停服务了
    southwolf
        41
    southwolf  
       1 天前 via Android
    @realpg 所以我们说的基本是一回事😂
    新版本部署之后 旧版本不再接受新建连接,所有新请求只会发到新服务
    长连接就在应用层搞定
    me1onsoda
        42
    me1onsoda  
       1 天前
    游戏热更是硬需求吧,搞不懂为啥那么多游服喜欢用 go 开发
    bigtear
        43
    bigtear  
       1 天前
    @me1onsoda Lua 更多
    htxy1985
        44
    htxy1985  
    OP
       1 天前 via Android
    @Luke 兄弟能稍微再说具体点吗,没太懂
    linuxsuren
        45
    linuxsuren  
       1 天前
    https://github.com/LinuxSuRen/api-testing 基于 gRPC 实现的插件系统
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3158 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 13:15 · PVG 21:15 · LAX 05:15 · JFK 08:15
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.