背景
MOSN在热升级上面,也曾做过自己的探索;业界虽然有Nginx Envoy 也都实现了热升级方式,那么这里有什么异同呢?
Nginx: 通过Fork的方式直接继承父进程的监听信息和链接信息等,仅仅只用于重启
Envoy: Envoy对端口监听(Listener)进行了迁移,但是对建立的连接(connection)则通过命令的方式,进行主动断开重连
MOSN: 鉴于低版本Bolt 等协议,不支持主动断连,且是长连接,导致MOSN在进行热升级的时候,不仅仅进行了端口监听的迁移,还有connection的迁移,保证了热升级过程中链接不中断,客户端服务端无感的升级体验
升级流程
触发情况
这里赘述一下,MOSN的热升级方式,是通过一个Operator实现的
- Operator在Pod中增加新的MOSN容器
- New Mosn启动时候,观测到有Old Mosn存在,开启热升级的逻辑
- New Mosn启动成功后,Old Mosn进行退出
- Operator销毁Old Mosn的Container
至此,一个完整的流程热升级流程结束
整体设计
MOSN热升级中核心的数据
- 配置数据(这样New Mosn才知道代理的是什么应用,有哪些配置信息等等)
- 监听端口
- 连接
MOSN热升级的交互流程设计如上,接下来对各个模块迁移逻辑进行一下跟踪
源码解析
代码逻辑仅保留核心流程
socket监听情况
func (stm *StageManager) Run() {
// 1: parser params
stm.runParamsParsedStage()
// 2: init
stm.runInitStage()
// 3: pre start
stm.runPreStartStage()
// 4: run
stm.runStartStage()
// 5: after start
stm.runAfterStartStage()
stm.SetState(Running)
}
MOSN启动分为上述几个流程,其中 热升级逻辑主要分布在 InitStage
和 StartStage
InitStage: 迁移配置信息 和 迁移Listener
StartStage: 创建 reconfig.sock 的监听 和 迁移 connection
在MOSN中有4个socket监听
reconfig.sock: 由 old mosn 监听,用于 new mosn 感知 old mosn存在
listen.sock: 由new mosn 监听,用于old mosn传递 listener数据给new mosn
conn.sock: 由 new mosn监听,用于 old mosn 传递 connection 的 fd和connection读取到的数据 (没有则不传)给 new mosn
mosnconfig.sock: 由 new mosn 监听,用于 old mosn 传递配置信息给 new mosn
Old Mosn 迁移主流程
func ReconfigureHandler() error {
// dump lastest config, and stop DumpConfigHandler()
configmanager.DumpLock()
configmanager.DumpConfig()
// if reconfigure failed, enable DumpConfigHandler()
defer configmanager.DumpUnlock()
// transfer listen fd
var listenSockConn net.Conn
var err error
var n int
var buf [1]byte
if listenSockConn, err = sendInheritListeners(); err != nil {
return err
}
if enableInheritOldMosnconfig {
if err = SendInheritConfig(); err != nil {
listenSockConn.Close()
log.DefaultLogger.Alertf(types.ErrorKeyReconfigure, "[old mosn] [SendInheritConfig] new mosn start failed")
return err
}
}
// Wait new mosn parse configuration
listenSockConn.SetReadDeadline(time.Now().Add(10 * time.Minute))
n, err = listenSockConn.Read(buf[:])
if n != 1 {
log.DefaultLogger.Alertf(types.ErrorKeyReconfigure, "[old mosn] [read ack] new mosn start failed")
return err
}
// ack new mosn
if _, err := listenSockConn.Write([]byte{0}); err != nil {
log.DefaultLogger.Alertf(types.ErrorKeyReconfigure, "[old mosn] [write ack] new mosn start failed")
return err
}
// stop other services
store.StopService()
// Wait for new mosn start
time.Sleep(3 * time.Second)
// Stop accepting new connections & graceful close the existing connections if they supports graceful close.
shutdownServers()
// Wait for all connections to be finished
WaitConnectionsDone(GracefulTimeout)
log.DefaultLogger.Infof("[server] [reconfigure] process %d gracefully shutdown", os.Getpid())
// will stop the current old mosn in stage manager
return nil
}
- old mosn 首先将 listener 迁移到new mosn
- 然后 迁移 配置信息,这一步是可选的
- 等待 new mosn ack完成后
- 调用 WaitConnectionsDone 将 connection close掉,迁移 connection
配置迁移
配置迁移是用过 mosnconfig.sock 来完成的,new mosn 监听,old mosn 连接上去并传输数据
new mosn监听是在 GetInheritConfig
这个方法中实现的
func GetInheritConfig() (*v2.MOSNConfig, error) {
......
l, err := net.Listen("unix", types.TransferMosnconfigDomainSocket)
......
defer l.Close()
ul := l.(*net.UnixListener)
ul.SetDeadline(time.Now().Add(time.Second * 10))
uc, err := ul.AcceptUnix()
......
defer uc.Close()
log.StartLogger.Infof("[server] Get GetInheritConfig Accept")
configData := make([]byte, 0)
buf := make([]byte, 1024)
for {
n, err := uc.Read(buf)
configData = append(configData, buf[:n]...)
.......
}
// log.StartLogger.Infof("[server] inherit mosn config data: %v", string(configData))
oldConfig := &v2.MOSNConfig{}
err = json.Unmarshal(configData, oldConfig)
if err != nil {
return nil, err
}
return oldConfig, nil
}
New mosn 建立好socket监听后,卡在 Accept 函数中,等待 old mosn 建立链接
然后 接收old mosn 传递过来的数据即可
old mosn是在 SendInheritConfig
中与 new mosn建立链接并传递配置信息的
func SendInheritConfig() error {
var unixConn net.Conn
var err error
// retry 10 time
for i := 0; i < 10; i++ {
unixConn, err = net.DialTimeout("unix", types.TransferMosnconfigDomainSocket, 1*time.Second)
......
}
......
configData, err := configmanager.InheritMosnconfig()
......
uc := unixConn.(*net.UnixConn)
defer uc.Close()
n, err := uc.Write(configData)
......
return nil
}
至此,配置数据传递也就完成了
监听端口迁移
listener迁移是用过 listen.sock 来完成的,new mosn 监听,old mosn 连接上去并传输数据,跟配置传输的逻辑差不多
new mosn监听是在 GetInheritListeners
这个方法中实现的,并获取所有的listener
func GetInheritListeners() ([]net.Listener, []net.PacketConn, net.Conn, error) {
l, err := net.Listen("unix", types.TransferListenDomainSocket)
......
defer l.Close()
ul := l.(*net.UnixListener)
ul.SetDeadline(time.Now().Add(time.Second * 10))
// 这里卡主,等待 old mosn连接
uc, err := ul.AcceptUnix()
buf := make([]byte, 1)
oob := make([]byte, 1024)
// 接收 old mosn传递过来的数据
_, oobn, _, _, err := uc.ReadMsgUnix(buf, oob)
scms, err := unix.ParseSocketControlMessage(oob[0:oobn])
// 解析出来fd
gotFds, err := unix.ParseUnixRights(&scms[0])
var listeners []net.Listener
var packetConn []net.PacketConn
for i := 0; i < len(gotFds); i++ {
fd := uintptr(gotFds[i])
file := os.NewFile(fd, "")
.....
defer file.Close()
// 通过fd 恢复 listener,本质是对fd的监听
fileListener, err := net.FileListener(file)
if err != nil {
pc, err := net.FilePacketConn(file)
if err == nil {
packetConn = append(packetConn, pc)
} else {
log.StartLogger.Errorf("[server] recover listener from fd %d failed: %s", fd, err)
return nil, nil, nil, err
}
} else {
// for tcp or unix listener
listeners = append(listeners, fileListener)
}
}
return listeners, packetConn, uc, nil
}
通过上述方法,new mosn建立 socket监听,并等待 old mosn的连接,old mosn连接上后,等待 old mosn 传递 所有listener 的fd,然后 new mosn 进行恢复即可
但是 同一个 fd,有两个监听,不就乱了吗,所以 当 old mosn 传递过来fd后,会主动 stop accept,不再进行监听
new mosn 的listener 传递逻辑在sendInheritListeners
里面
func sendInheritListeners() (net.Conn, error) {
// 列出来所有的 listener,返回格式 os.File
lf := ListListenersFile()
......
lsf, lerr := admin.ListServiceListenersFile()
......
var files []*os.File
files = append(files, lf...)
files = append(files, lsf...)
......
fds := make([]int, len(files))
for i, f := range files {
// 获取 file 的 fd
fds[i] = int(f.Fd())
defer f.Close()
}
var unixConn net.Conn
var err error
// retry 10 time
for i := 0; i < 10; i++ {
unixConn, err = net.DialTimeout("unix", types.TransferListenDomainSocket, 1*time.Second)
.......
}
......
uc := unixConn.(*net.UnixConn)
buf := make([]byte, 1)
// 将 fd 转成 socket message
rights := syscall.UnixRights(fds...)
n, oobn, err := uc.WriteMsgUnix(buf, rights, nil)
......
return uc, nil
}
Old mosn 通过 ListListenersFile 将所有的listener罗列出来,这个主要得益于MOSN良好的设计模式;mosn维护了一个全局的servers,而server的结构如下
type server struct {
serverName string
stopChan chan struct{}
handler types.ConnectionHandler
}
type ConnectionHandler interface {
......
// ListListenersFD reports all listeners' fd
ListListenersFile(lctx context.Context) []*os.File
}
每个server对自己的listener描述清晰
继续回到 old mosn传递listener的过程
- old mosn 罗列出来所有的listener,并获取file
- old mosn 获取所有 file的fd,并将fd通过UnixRights转成 socket message,供传递
- new mosn 接收到 socket message,转成fd,并通过文件建立listener
然后 old mosn 会关闭掉所有的listener,停止accept 新的链接
func shutdownServers() {
for _, server := range servers {
server.Shutdown()
}
}
func (ch *connHandler) GracefulStopListeners() error {
var failed bool
listeners := ch.listeners
wg := sync.WaitGroup{}
wg.Add(len(listeners))
for _, l := range listeners {
al := l
log.DefaultLogger.Infof("graceful shutdown listener %v", al.listener.Name())
// Shutdown listener in parallel
utils.GoWithRecover(func() {
defer wg.Done()
if err := al.listener.Shutdown(); err != nil {
log.DefaultLogger.Errorf("failed to shutdown listener %v: %v", al.listener.Name(), err)
failed = true
}
}, nil)
}
wg.Wait()
return nil
}
func (l *listener) Shutdown() error {
changed, err := l.stopAccept()
if changed {
l.cb.OnShutdown()
}
return err
}
至此,只有new mosn 能建立新的链接,old mosn不再建立新的链接了
listener传递完成后,新的链接都建立到 new mosn上去了,剩余的就是存量长链接了
长链接迁移
长链接迁移是用过 conn.sock 来完成的,同上,也是由new mosn 监听,old mosn 连接上去并传输数据;不过,这里并没有传递配置和listener那样简单,需要考虑很多边际问题
在这里,链接分为两部分
- client/server -> mosn的链接
- mosn -> client/server 的链接
我们只需要考虑 client/server -> mosn的链接
的情况即可,mosn -> client/server 的链接
这种情况,由于mosn是主动连接方,断开并不会对下游造成任何影响
先来看下 长链接迁移的流程
- Client 发送请求到 MOSN
- MOSN 通过 domain socket(conn.sock) 把 TCP1 的 FD 和连接的状态数据发送给 New MOSN
- New MOSN 接受 FD 和请求数据创建新的 Conection 结构,然后把 Connection id 传给 MOSN,New MOSN 此时就拥有了TCP1 的一个拷贝。Old MOSN 停止读取 TCP1 的请求,New MOSN 开始读取 TCP1的请求,TCP1的迁移就完成了
- New MOSN 通过 LB 选取一个新的 Server,建立 TCP3 连接,转发请求到 Server
- Server 回复响应到 New MOSN
- New MOSN 通过 MOSN 传递来的 TCP1 的拷贝,回复响应到 Client
接下来看下代码
注: 前面 WaitConnectionsDone
已经将 connection.stopChan close掉了
链接迁移是在 startReadLoop
中完成的
func (c *connection) startReadLoop() {
var transferTime time.Time
for {
......
select {
case <-c.stopChan:
// 首先设置 transfer 时间
if transferTime.IsZero() {
if c.transferCallbacks != nil && c.transferCallbacks() {
randTime := time.Duration(rand.Intn(int(TransferTimeout.Nanoseconds())))
transferTime = time.Now().Add(TransferTimeout).Add(randTime)
log.DefaultLogger.Infof("[network] [read loop] transferTime: Wait %d Second", (TransferTimeout+randTime)/1e9)
} else {
// set a long time, not transfer connection, wait mosn exit.
transferTime = time.Now().Add(10 * TransferTimeout)
log.DefaultLogger.Infof("[network] [read loop] not support transfer connection, Connection = %d, Local Address = %+v, Remote Address = %+v",
c.id, c.rawConnection.LocalAddr(), c.RemoteAddr())
}
} else {
if transferTime.Before(time.Now()) {
c.transfer()
return
}
}
default:
}
.......
}
- 在 old mosn 调用
WaitConnectionsDone
将 connection.stopChan close之后,在startReadLoop
循环中 首先设置以下 随机 transfer 时间 - 在 transfer时间到了之后,开始 迁移 链接
func (c *connection) transfer() {
c.notifyTransfer()
id, _ := transferRead(c)
c.transferWrite(id)
}
func transferRead(c *connection) (uint64, error) {
......
unixConn, err := net.Dial("unix", types.TransferConnDomainSocket)
......
defer unixConn.Close()
file, tlsConn, err := transferGetFile(c)
......
uc := unixConn.(*net.UnixConn)
// send type and TCP FD
err = transferSendType(uc, file)
......
// send header + buffer + TLS
err = transferReadSendData(uc, tlsConn, c.readBuffer)
......
// recv ID
id := transferRecvID(uc)
log.DefaultLogger.Infof("[network] [transfer] [read] TransferRead NewConn Id = %d, oldId = %d, %p, addrass = %s", id, c.id, c, c.RemoteAddr().String())
return id, nil
}
- 每一个connection迁移的时候,会首先构建一个 unix connection 用于 old mosn 和 new mosn交互
- 首先将 connection 的 fd 通过 scoket传递给new mosn
- 然后 将 connection tls 和 读取的buf数据再传递给 new mosn 处理
- 最后 记录下来 new mosn 根据 fd 创建的 新的connection的id
这里mosn构造了一个简单的socket协议,用于 传递 connection 的tls和buf数据
/**
* transfer read protocol
* header (8 bytes) + (readBuffer data) + TLS
*
* 0 4 8
* +-----+-----+-----+-----+-----+-----+-----+-----+
* | data length | TLS length |
* +-----+-----+-----+-----+-----+-----+-----+-----+
* | data |
* +-----+-----+-----+-----+-----+-----+-----+-----+
* | TLS |
* +-----+-----+-----+-----+-----+-----+-----+-----+
*
接下来继续看下,new mosn 收到 connection后的处理, new mosn 处理是在 transferHandler
中完成的
func transferHandler(c net.Conn, handler types.ConnectionHandler, transferMap *sync.Map) {
......
uc, ok := c.(*net.UnixConn)
......
// recv type
conn, err := transferRecvType(uc)
......
if conn != nil {
// transfer read
// recv header + buffer
dataBuf, tlsBuf, err := transferReadRecvData(uc)
......
connection := transferNewConn(conn, dataBuf, tlsBuf, handler, transferMap)
if connection != nil {
transferSendID(uc, connection.id)
} else {
transferSendID(uc, transferErr)
}
}
......
}
new mosn 收到connection迁移请求后,根据传递过来的fd,首先转换成connection,然后 根据 tls 数据,构建为 tls conn,这些完成后,根据 conn的监听信息和handler类型,找到对应的listener,并将这个connection加进去,然后就可以开始处理 传递过来的buf数据了
最后,new mosn 将新的connection的id,传给old mosn,用于传递 写请求
至此,old mosn connection的 readLoop 也退出了,不再读取新的数据,数据也都由new mosn来读取了
接下来就是写请求了
old mosn 如果继续往连接里面写数据,可能会和new mosn冲突,导致数据操作,所以 old mosn的写请求,是直接转给 new mosn来处理的
Old mosn 迁移写请求:
func transferWrite(c *connection, id uint64) error {
......
unixConn, err := net.Dial("unix", types.TransferConnDomainSocket)
......
defer unixConn.Close()
uc := unixConn.(*net.UnixConn)
err = transferSendType(uc, nil)
......
// build net.Buffers to IoBuffer
buf := transferBuildIoBuffer(c)
// send header + buffer
err = transferWriteSendData(uc, int(id), buf)
......
return nil
}
Mosn 也为写请求,构建了一个简单的socket协议
* transfer write protocol
* header (8 bytes) + (writeBuffer data)
*
* 0 4 8
* +-----+-----+-----+-----+-----+-----+-----+-----+
* | data length | connection ID |
* +-----+-----+-----+-----+-----+-----+-----+-----+
* | data |
* +-----+-----+-----+-----+-----+-----+-----+-----+
*
这里会将,迁移 读请求时获取到的 connection id 记录下来,并传递给 new mosn,让 new mosn 根据 id 找到对应的链接
new mosn 接收写请求处理逻辑,也是在 transferHandler
中完成的:
func transferHandler(c net.Conn, handler types.ConnectionHandler, transferMap *sync.Map) {
......
// transfer write
// recv header + buffer
id, buf, err := transferWriteRecvData(uc)
......
connection := transferFindConnection(transferMap, uint64(id))
......
err = transferWriteBuffer(connection, buf)
......
}
New mosn 从 old mosn的socket请求中,解析出来 connection id 和 buf 数据
根据 id 找到 new mosn中的 connection,然后将buf写入即完成
至此,读写请求都迁移完成,整个长连接的迁移也就完成了
总结
MOSN对热升级的处理,是做到极致的,深度是远远高于市场上其他产品的;同时,深度也往往伴随着风险,做到链接迁移层面,可能也并不是MOSN的本意,而是历史原因的驱动
在源码逻辑层面,个人也有一点简单的看法
- 整个热升级模块,更像是函数驱动,而缺少设计,从 conn.sock listen.sock 等多个sock文件就可以看出,如果设计好点的话,完全可以通过构建socket协议,而规避掉多个socket文件,同时,逻辑也会更清晰简洁一点,而非散落在各个方法里面
- 有些边界性问题还是没有处理的很好;例如,listener迁移的时候,如果有listener close掉了,但是配置还存在MOSN中,这里就会panic了;eg: reconfig.sock 在处理最后会删除,如果走到了这一步,然后回滚了,后续也无法进行热升级
最后,MOSN还是很优秀的,respect!!!