服务器更新二进制程序

(本文样例和背景基于C/C++/Golang编译出的二进制产物)

当使用二进制编译产物的服务器程序版本有迭代时,一般会按照这样的步骤操作更新: [程序停止]->[更新二进制]->[程序启动] 。排除使用动态库的更新机制,流程可以覆盖基本90%以上的场景。

但在实际的生产环境中并不是简单的[程序停止]->[更新二进制]->[程序启动]这么直接的流程,根据具体业务会有比较多需要注意和处理的细节,描述这些细节有一个比较精确的术语叫:优雅重启

针对不同的业务,优雅重启的思路也不相同,就我个人经历且实操过的方案而言,优雅重启主要需要考虑以下几个方面的问题:

  • 被更新进程退出前的资源处理,包括:内存数据是否回写数据库或者存盘TCP链接是否有重连机制程序内部队列的处理还有在等待回调逻辑是否需要处理
  • 被更新进程在链路中所处位置评估,包括:上下游的依赖情况更新是否会导致其他模块异常或者服务降级若发生异常是否能自愈(开发时就应该提前评估好),上游若有队列确保不丢消息。根据服务业务不同,少数情况需要进入业务层定制处理方式;
  • 评估进程停启过程需要时长所带来的影响,比如:3s、30s这二个不同停启时长的程序在处理上会完全不一样;(注意这个是停止到启动整体时间,因为在进程收到停止信号时就已经“拒绝服务“准备退出了,退出前的资源处理也需要时间)

整体而言没有一个万金油的方案,部分场景会有较为通用的方案,比如队列、TCP链接重连机制。对于大多数其他问题,不同的业务场景有不同的解法。解法的核心围绕这2个关键词:状态停启时长

本文围绕状态与停启时长聊聊几种典型更新方式的方案原型:

无状态更新

无状态的服务器简单解释成:服务器不保存持续状态,处理每个请求时状态都是相对最新;或者是服务器逻辑不存在状态,只有判断和运算。

我们可以将这种服务流程简单概述成:[收到请求]->[拉取需要处理的数据到内存 (若需要) ]->[逻辑判断与处理]->[保存修改过的数据(若需要)]。这种场景在Http服务中应用较多,其他场景也会有一些典型应用,例如:几年前很火的serverless,在处理逻辑时无状态,或者可以从储存恢复状态。

当然,从更广义的说法来解释,程序重启或者崩溃总可以从其他地方(数据库、共享内存或者磁盘等)恢复到最新数据也可以认为一种无状态。它的最大优点是服务平行扩展比较容易,缺点是处理逻辑时状态恢复会占用较多的运行时资源。

那么其更新流程如下:

有状态更新

基于无状态的特性,每次请求可能都会有数据的加载,在高并发的情况下,状态恢复这一行为会有明显的性能瓶颈。

所以为了解决无状态的性能问题,提高性能,服务进程会把数据持有在内存里,读写直接操作内存(是一个持续的状态),定期进行落地,这样的服务我们称为有状态服务。

有状态服务在更新时需要额外考虑更多的问题:

  1. 数据可靠性,程序正常退出时,当前数据落地;程序定期落地频次控制,太短影响整体性能,太长影响宕机时数据丢失;
  2. 数据一致性,在程序并行部署时,是否会有数据一致性问题;
  3. 数据合理性,设置单进程最大可用内存且有淘汰机制;

其更新流程如下:

进程替换式更新

有一些服务器因为业务特性,停启时间长达几十秒甚至更长,若直接更新进程会导致服务至少停止启动时长那么久;还有一些服务器逻辑具有上下文连续性,比如socket连接还在传输数据,直接更新进程会造成客户端业务中断。

为了解决这些问题,有一种方案叫进程替换式更新,简单描述如此:进程A需要更新,启动更新进程B,将业务流量导入B,进程A在处理完所有任务后停止,进程B此时转正。下次更新B、A交替,A转正。最经典的使用场景应该是nginx reload。

当然,上面描述的仅仅是一种思路,很多场景在实际使用中会在服务上游添加接入路由层,避免进程更新时对socket的影响,还有一些辅助流量控制的手段,甚至可以加入版本号的逻辑辅助用户或者路由层进行分流。

需要特殊说明的是:这也不是一个万金油的思路,特殊场景也需要特殊分析,举个例子:进程A存在私有消息队列,若使用这种更新方式,在启动更新进程B后,进程A需要等待消息队列消息处理完才能退出,需要评估等待A退出过程中是否会和进程B的新消息处理产生数据不一致。

还有一点需要注意:因为短时间会运行进程数量翻倍,如果进程占用比较多的运行时资源,需要评估更新时的资源分配情况,制定相应的策略

就问题的本质来说,替换式更新也要分清有状态和无状态两种情况,以及消息逻辑是否有严格的上下文关系,导致很多问题需要特化分析解决。

更新大致流程如下(省略了特化的部分):

三种思路的比对

思路复杂度 停启时长要求无需额外运行时资源业务持续性保持
无状态更新
有状态更新
进程替换式更新

其他

服务器二进制程序逻辑也有类似动态库更新的方案,但是普适性很低,更多用于特定的bugfix。比如这种动态库C++热更新;当然也有替换动态库式更新,这个方案对于动态库逻辑全局变量的写法有强要求(一般方案都是编译成另外一个库),导致开发有潜规则,且极易出错,一旦出错就是数据写坏或者程序崩溃的严重问题。

(全文结束)


转载文章请注明出处:漫漫路 - lanindex.com

Leave a Comment

Your email address will not be published.