Endless——平滑重启
https://deepwiki.com/fvbock/endless
https://github.com/fvbock/endless
一、前置芝士
内核的文件描述符继承机制:
子进程一般默认继承父进程的stdin (0), stdout (1), 和 stderr (2)三类文件描述符。
那么其他的描述符怎么继承呢,Go用一个结构包裹传递 ExtraFiles。
它允许父进程将额外的、已经打开的文件描述符(比如一个正在监听的 TCP Socket)传递给子进程。
结构如下:
type Cmd struct {
// ... 其他字段
ExtraFiles []*os.File
}
二、结构
var (
// 这三个结构完成了关于文件描述符的继承逻辑
runningServers map[string]*EndlessServer
runningServersOrder []string
socketPtrOffsetMap map[string]uint
isChild bool
socketOrder string
runningServerReg sync.RWMutex
runningServersForked bool
……
)
type EndlessServer struct {
http.Server
EndlessListener net.Listener
SignalHooks map[int]map[os.Signal][]func()
tlsInnerListener *endlessListener
wg sync.WaitGroup
sigChan chan os.Signal
isChild bool
state uint8
lock *sync.RWMutex
BeforeBegin func(add string)
}
三、逻辑关系
- 重启脚本实行 kill 命令(SIGHUP),信号触发父进程启动fork
- 父进程便利所有服务 runningServers,将相关socket套接字(socketPtrOffsetMap 反向映射顺序关系)全部存入ExtraFiles给子进程继承
- 子进程启动后获取套接字文件描述符并复用监听,程序启动成功后,再发送SIGUSR1信号,希望父进程主动调用shutdown退出
四、疑惑
- 为什么子进程获取到的这些文件描述符一定是顺序的?假设程序没有父进程,程序第一次启动,父进程此时的监听端口的文件描述符可能是不一样的啊,假设某一个描述符为可能为15啊,那子进程按照顺序下标读取的文件描述符不就是错误的吗?
Go 语言在启动子进程的瞬间,会把这个 FD “洗白” 成 FD 3,再把写入子进程的ExtraFiles(因为子进程是全新的,从3开始,合理)
Go 运行时(Runtime)在 fork 和 exec 之间,会调用 dup2(15, 3)
五、隐患
1. 当程序前后版本监听的addr列表发生了变化,这里顺序存入结果就有问题。
因为 runningServersOrder 顺序存入此次程序监听的地址列表。
但是 socketPtrOffsetMap 却是遍历ENDLESS_SOCKET_ORDER获取父进程的监听地址,
这样会导致地址和下标映射关系不一致,则从父进程获取到的监听socket不合理。
2. 当因为外部或错误操作导致此次平滑重启失败,父进程被表示为已经触发fork runningServersForked = true
但子进程又没有启动成功,无法重新触发fork,导致只能重启