Skip to main content

debug-myself's blog

Tag: Go

如何优雅重启网络服务

# TL;DR

# 背景

对于一个持续迭代的网络服务, 变更是家常便饭. 常规的变更流程如下:

    graph LR
    S1["更新程序/配置"]
    S2["停止当前服务"]
    S3["启动新服务"]

    S1 --> S2 --> S3
  1. 更新程序/配置: 当修复了已知bug或者是新增功能时, 需要更新程序. 或者是更改了配置;
  2. 停止当前服务: 为避免和新程序冲突, 需要暂停当前服务;
  3. 启动新程序: 再次启动程序让新程序/配置生效;

这个变更过程存在很大问题: 变更过程会影响正在使用此服务的用户. 很多成熟的的项目都支持优雅重启(graceful restarting), 保证重启服务对用户"无感", 如Nginx, SSH server等都支持reload重启方式, 保证重启过程不影响用户使用.

普通项目尝尝也有graceful restarting的需求, graceful restarting的原理是什么呢? 要回答这个问题, 我们不妨先想想满足什么条件才能算作graceful restarting? 我认为至少要满足一下条件:

  1. 升级过程中不影响正在使用此服务的用户;
  2. 升级过程中有新的用户接入时, 也能正常提供服务;
  3. 服务升级后接入的用户由更新后的程序提供服务;
  4. 如果升级失败, 仍然可以提供正常服务;

基于以上条件, 可以想到一个简单的实现方式: 每个需要graceful restarting的服务负责把监听的socket设置为SO_REUSEPORT, 这样就允许多个进程监听同一个地址. 这个方式看起来能做到graceful restarting, 但实际上还是存在一些问题的: 多个进程分别创建socket监听同一个地址就会导致每个进程都创建一个socket, 这种情况下请求到来时, 该由哪个进程来处理是由操作系统决定的(操作系统负责为多个进程做负载均衡). 所以当新老服务进程同时存在时, 无法保证新到来的请求一定被新的服务进程处理.

要想解决这个问题, 就要让新老服务进程共享socket, 所以自然而然的会想到让老的服务进程fork子进程, 并且把自己监听的socket继承给子进程. Cloudflare就基于这个原理实现了一个Go语言版本的graceful restarting库–tableflip. 下面就来分析一下tableflip的实现.