前言
大家好,我是老马。
sofastack 其实出来很久了,第一次应该是在 2022 年左右开始关注,但是一直没有深入研究。
最近想学习一下 SOFA 对于生态的设计和思考。
sofaboot 系列
SOFAStack-00-sofa 技术栈概览
MOSN(Modular Open Smart Network)-00-简单聊一聊
MOSN(Modular Open Smart Network)-01-是一款主要使用 Go 语言开发的云原生网络代理平台
MOSN(Modular Open Smart Network)-02-核心概念
MOSN(Modular Open Smart Network)-03-流量劫持
MOSN(Modular Open Smart Network)-04-TLS 安全链路
MOSN(Modular Open Smart Network)-05-MOSN 平滑升级原理解析
MOSN(Modular Open Smart Network)-06-MOSN 多协议机制解析
MOSN(Modular Open Smart Network)-07-Sidecar 模式
MOSN(Modular Open Smart Network)-08-MOSN 扩展机制解析
平滑升级原理解析
如何在升级 Sidecar(MOSN)的时候而不影响业务,对于存量的长连接如何迁移,本文将为你介绍 MOSN 的解决之道。
Service Mesh 中 Sidecar 运维一直是一个比较棘手的问题,数据平面的 Sidecar 升级是常有的事情,如何在升级 Sidecar(MOSN)的时候而不影响业务,对于存量的长连接如何迁移,本文将为你介绍 MOSN 的解决之道。
背景
本文介绍 MOSN 支持平滑升级的原因和解决方案,对于平滑升级的一些基础概念,大家可以通过 Nginx vs Enovy vs Mosn 平滑升级原理解析了解。
先简单介绍一下为什么 Nginx 和 Envoy 不需要具备 MOSN 这样的连接无损迁移方案,主要还是跟业务场景相关,Nginx 和 Envoy 主要支持的是 HTTP1 和 HTTP2 协议,HTTP1使用 connection: Close,HTTP2 使用 Goaway Frame 都可以让 Client 端主动断链接,然后新建链接到新的 New process,但是针对 Dubbo、SOFA PRC 等常见的多路复用协议,它们是没有控制帧,Old process 的链接如果断了就会影响请求的。
一般的升级做法就是切走应用的流量,比如自己UnPub掉服务,等待一段时间没有请求之后,升级MOSN,升级好之后再Pub服务,整个过程比较耗时,并且会有一段时间是不提供服务的,还要考虑应用的水位,在大规模场景下,就很难兼顾评估。
MOSN 为了满足自身业务场景,开发了长连接迁移方案,把这条链接迁移到 New process 上,整个过程对 Client 透明,不需要重新建链接,达到请求无损的平滑升级。

正常流程

- Client 发送请求 Request 到 MOSN
- MOSN 转发请求 Request 到 Server
- Server 回复响应 Response 到 MOSN
- MOSN 回复响应 Response 到 Client
上图简单介绍了一个请求的正常流程,我们后面需要迁移的是 TCP1 链接,也就是 Client 到 MOSN 的连接,MOSN 到 Server 的链接 TCP2 不需要迁移,因为 MOSN 访问 Server 是根据 LoadBalance 选择,我们可以主动控制断链建链。
平滑升级流程
触发条件
有两个方式可以触发平滑升级流程:
- MOSN 对 SIGHUP 做了监听,发送 SIGHUP 信号给 MOSN 进程,通过 ForkExec 生成一个新的 MOSN 进程。
- 直接重新启动一个新 MOSN 进程。
为什么提供两种方式?最开始我们支持的是方法1,也就是 nginx 和 Envoy 使用的方式,这个在虚拟机或者容器内替换 MOSN 二级制来升级是可行的,但是我们的场景需要满足容器间的升级,所以需要新拉起一个容器,就需要重新启动一个新的 MOSN 进程来做平滑升级,所以后续又支持了方法2。容器间升级还需要 operator 的支持,本文不展开叙述。
交互流程


首先,老的 MOSN 在启动最后阶段会启动一个协程运行 ReconfigureHandler() 函数监听一个 Domain Socket(reconfig.sock), 该接口的作用是让新的 MOSN 来感知是否存在老的 MOSN。
func ReconfigureHandler() {
l, err := net.Listen("unix", types.ReconfigureDomainSocket)
for {
uc, err := ul.AcceptUnix()
_, err = uc.Write([]byte{0})
reconfigure(false)
}
}
触发平滑升级流程的两种方式最终都是启动一个新的 MOSN 进程,然后调用GetInheritListeners(),通过 isReconfigure() 函数来判断本机是否存在一个老的 MOSN(就是判断是否存在 reconfig.sock 监听),如果存在一个老的 MOSN,就进入迁移流程,反之就是正常的启动流程。
// 保留了核心流程
func GetInheritListeners() ([]net.Listener, net.Conn, error) {
if !isReconfigure() {
return nil, nil, nil
}
l, err := net.Listen("unix", types.TransferListenDomainSocket)
uc, err := ul.AcceptUnix()
_, oobn, _, _, err := uc.ReadMsgUnix(buf, oob)
file := os.NewFile(fd, "")
fileListener, err := net.FileListener(file)
return listeners, uc, nil
}
如果进入迁移流程,新的 MOSN 将监听一个新的 Domain Socket(listen.sock),用于老的 MOSN 传递 listen FD 到新的 MOSN。FD 的传递使用了sendMsg 和 recvMsg。在收到 listen FD 之后,调用 net.FileListener() 函数生产一个 Listener。此时,新老 MOSN 都同时拥有了相同的 Listen 套接字。
// FileListener returns a copy of the network listener corresponding
// to the open file f.
// It is the caller's responsibility to close ln when finished.
// Closing ln does not affect f, and closing f does not affect ln.
func FileListener(f *os.File) (ln Listener, err error) {
ln, err = fileListener(f)
if err != nil {
err = &OpError{Op: "file", Net: "file+net", Source: nil, Addr: fileAddr(f.Name ()), Err: err}
}
return
}
这里的迁移和 Nginx 还是有一些区别,Nginx 是 fork 的方式,子进程自动就继承了 listen FD,MOSN 是新启动的进程,不存在父子关系,所以需要通过 sendMsg 的方式来传递。
在进入迁移流程和 Listen 的迁移过程中,一共使用了两个 Domain Socket:
reconfig.sock是 Old MOSN 监听,用于 New MOSN 来判断是否存在listen.sock是 New MOSN 监听,用于 Old MOSN 传递 listen FD
两个 sock 其实是可以复用的,也可以用 reconfig.sock 进行 listen 的传递,由于一些历史原因搞了两个,后续可以优化为一个,让代码更精简易读。
这儿再看看 Old MOSN 的处理,在收到 New MOSN 的通知之后,将进入reconfigure(false) 流程,首先就是调用 sendInheritListeners() 传递 listen FD,原因上面内容已经描述,最后调用 WaitConnectionsDone() 进入存量长链接的迁移流程。
// 保留了核心流程
func reconfigure(start bool) {
if start {
startNewMosn()
return
}
// transfer listen fd
if notify, err = sendInheritListeners(); err != nil {
return
}
// Wait for all connections to be finished
WaitConnectionsDone(GracefulTimeout)
os.Exit(0)
}
在 Listen FD 迁移之后,New MOSN 通过配置启动,然后在最后启动一个协程运行TransferServer(),将监听一个新的 DomainSocket(conn.sock),用于后续接收 Old MOSN 的长连接迁移。迁移的函数是 transferHandler()
func TransferServer(handler types.ConnectionHandler) {
l, err := net.Listen("unix", types.TransferConnDomainSocket)
utils.GoWithRecover(func() {
for {
c, err := l.Accept()
go transferHandler(c, handler, &transferMap)
}
}, nil)
}
Old MOSN 将通过 transferRead() 和 transferWrite() 进入最后的长链接迁移流程,下面主要分析这块内容。
总结
长连接的 FD 迁移是比较常规的操作,sendMsg 和 connection repair 都可以。
在整个过程中最麻烦的是应用层数据的迁移,一般想法就是把应用层的数据结构等都迁移到新的进程,比如已经读取的协议 HEAD 等结构体,但这就导致你的迁移过程会很复杂,每个协议都需要单独处理。
MOSN 的方案是把迁移放到了 IO 层,不关心应用层具体是什么协议,我们迁移最原始的 TCP 数据包,然后让 New MOSN 来 codec 这个数据包来拼装 HEAD 等结构体,这个过程



















