MOSN 反向通道详解

简介: 本文主要介绍之前新合入 master 分支的「反向通道」的使用场景和设计原理,欢迎大家留言探讨。

图片

文|郑泽超(GitHub ID:CodingSinger )

字节跳动高级工程师

热衷于微服务和 ServiceMesh 开源社区

本文 6802 字,阅读 15 分钟

Part.1--贡献者前言

说起来非常的抓马,当时和 MOSN 的相遇是在给于雨负责的开源项目 Dubbo-go 贡献代码那阵。在自己顺利当上了 Dubbo 开源社区的 Committer 之后,心想着能更深入的学习 Golang 语言,机缘巧合之下碰到了 MOSN 的老大哥烈元 (也是元总领我进了 MOSN 社区的大门)

作为一款目标对齐 Envoy 的高性能可扩展安全网络代理,MOSN 支持的生态能力更贴近国内互联网公司的技术栈,并且对新功能的响应也很迅速。其次 MOSN 有着很多值得借鉴的巧妙设计和进阶的使用技巧,能充分满足自己在工作之外深入学习 Golang 语言的诉求。

目前,我在社区里陆续参与了 EDF Scheduler、LAR、WRR 负载均衡、DSL 路由能力、UDS Listener、Plugin 模式的 Filter 扩展以及反向通道等一些比较大的 feature 能力建设。再次感谢雨哥、元总、鹏总、毅松等社区内一众大佬们帮我考究方案并且帮我 Review 代码。

本文主要介绍之前新合入 master 分支的「反向通道」的使用场景和设计原理,欢迎大家留言探讨。

MOSN 项目概述

MOSN(Modular Open Smart Network)是一款主要使用 Go 语言开发的云原生网络代理平台,由蚂蚁集团开源并经过双 11 大促几十万容器的生产级验证,具备高性能易扩展的特点。MOSN 可以和 Istio 集成构建 Service Mesh,也可以作为独立的四、七层负载均衡、API Gateway、云原生 Ingress 等使用。

Part.2--MOSN 的反向通道实现

在云边协同的网络场景,通常都是单向网络,云侧节点无法主动发起连接与边缘节点通讯。这种限制虽然在极大程度上保证了边缘节点的安全,但缺点也很明显,即只允许边缘节点主动发起访问云端节点。

云边隧道旨在解决云端无法主动访问边缘节点的问题,其本质是一个反向通道 (后文统称为反向通道) 。通过在边缘侧主动发起建连的方式与云端节点之间构建一条专用的全双工连接,用来传输云端节点的请求数据和回传最终的响应结果。

目前例如 SuperEdge、Yurttunnel 等业界知名云边协同开源框架,对于云边通信的实现方案都是基于反向通道。

本文将着重介绍 MOSN 之上的反向通道运作流程和原理。总体架构如下所示 (图中箭头表示 TCP 建连反向)

图片

整个运作流程可以简单概括为:

1. 边缘侧的 MOSN 实例 (后文统称为 Tunnel Agent) 在启动时 Tunnel Agent 相关服务协程。

2. 通过指定的静态配置或者动态服务发现方式拿到需要反向建连的公有云侧的 MOSN Server 地址列表 (后文统称 Tunnel Server ) ,并且建立反向连接。

3. 云侧的 Frontend 与 Tunnel Server 侧的转发端口进行数据交互,这部分数据会被托管到之前建立的反向连接进行发送。

4. 边缘节点接受到请求之后,再将请求转发给实际的后端目标节点,回包过程则原路返回。

Part.3--反向通道启动过程

MOSN Agent 通过 ExtendConfig 特性,在 MOSN 启动时加载和完成初始化 Tunnel Agent 的工作。

ExtendConfig 中定义 AgentBootstrapConfig 结构如下:

type AgentBootstrapConfig struct {
    Enable bool `json:"enable"`
    // The number of connections established between the agent and each server
    ConnectionNum int `json:"connection_num"`
    // The cluster of remote server
    Cluster string `json:"cluster"`
    // After the connection is established, the data transmission is processed by this listener
    HostingListener string `json:"hosting_listener"`
    // Static remote server list
    StaticServerList []string `json:"server_list"`

    // DynamicServerListConfig is used to specify dynamic server configuration
    DynamicServerListConfig struct {
        DynamicServerLister string `json:"dynamic_server_lister"`
    }

    // ConnectRetryTimes
    ConnectRetryTimes int `json:"connect_retry_times"`
    // ReconnectBaseDuration
    ReconnectBaseDurationMs int `json:"reconnect_base_duration_ms"`

    // ConnectTimeoutDurationMs specifies the timeout for establishing a connection and initializing the agent
    ConnectTimeoutDurationMs int    `json:"connect_timeout_duration_ms"`
    CredentialPolicy         string `json:"credential_policy"`
    // GracefulCloseMaxWaitDurationMs specifies the maximum waiting time to close conn gracefully
    GracefulCloseMaxWaitDurationMs int `json:"graceful_close_max_wait_duration_ms"`

    TLSContext *v2.TLSConfig `json:"tls_context"`
}

- ConnectionNum:Tunnel Agent 和每个 Tunnel Server 建立的物理连接数量。

- HostingListener:指定 Agent 建立连接之后托管的 MOSN Listener,即 Tunnel Server 发来的请求会由该 Listener 托管处理。

- DynamicServerListConfig:动态 Tunnel Server 的服务发现相关配置,可通过自定义的服务发现组件提供动态的地址服务。

- CredentialPolicy:自定义的连接级别的鉴权策略配置。

- TLSContext:MOSN TLS 配置,提供 TCP 之上通信的保密性和可靠性。

针对每个远端的 Tunnel Server 实例,Agent 对应一个 AgentPeer 对象,启动时除了主动建立 ConnectionNum 个反向通信连接,还会额外建立一条旁路连接,这条旁路连接主要是用来发送一些管控参数,例如平滑关闭连接、调整连接比重。

func (a *AgentPeer) Start() {
    connList := make([]*AgentClientConnection, 0, a.conf.ConnectionNumPerAddress)
    for i := 0; i < a.conf.ConnectionNumPerAddress; i++ {
      // 初始化和建立反向连接
        conn := NewAgentCoreConnection(*a.conf, a.listener)
        err := conn.initConnection()
        if err == nil {
            connList = append(connList, conn)
        }
    }
    a.connections = connList
    // 建立一个旁路控制连接
    a.initAside()
}

initConnection 方法进行具体的初始化完整的反向连接,采取指数退避的方式保证在最大重试次数之内建连成功。

func (a *connection) initConnection() error {
    var err error
    backoffConnectDuration := a.reconnectBaseDuration

    for i := 0; i < a.connectRetryTimes || a.connectRetryTimes == -1; i++ {
        if a.close.Load() {
            return fmt.Errorf("connection closed, don't attempt to connect, address: %v", a.address)
        }
        // 1. 初始化物理连接和传输反向连接元数据
        err = a.init()
        if err == nil {
            break
        }
        log.DefaultLogger.Errorf("[agent] failed to connect remote server, try again after %v seconds, address: %v, err: %+v", backoffConnectDuration, a.address, err)
        time.Sleep(backoffConnectDuration)
        backoffConnectDuration *= 2
    }
    if err != nil {
        return err
    }
    // 2. 托管listener
    utils.GoWithRecover(func() {
        ch := make(chan api.Connection, 1)
        a.listener.GetListenerCallbacks().OnAccept(a.rawc, a.listener.UseOriginalDst(), nil, ch, a.readBuffer.Bytes(), []api.ConnectionEventListener{a})
    }, nil)
    return nil
}

该方法主要步骤:

1. a.init( ) 方法会调用 initAgentCoreConnection 方法初始化物理连接并完成建连交互过程。Tunnel Server 通过 Agent 传输的元数据信息,进行管理反向连接。具体的交互过程和协议后文会细讲。

2. 建连成功之后,Tunnel Agent 托管 raw conn 给指定的 Listener。之后该 raw conn 的生命周期由该 Listener 全权管理,并且完全复用该 Listener 的能力。

其定义了初始化反向连接的交互流程,具体代码细节可以看:

pkg/filter/network/tunnel/connection.go:250,本文不展开技术细节。

Part.4--交互过程

目前 MOSN 的反向通道只支持了 raw conn 的实现,因此定义了一套简单明了的网络通信协议。

图片

主要包括:

- 协议魔数:2 byte;

- 协议版本:1 byte;

- 主体结构类型:1 byte,包括初始化、平滑关闭等;

- 主体数据长度:2 byte;

- JSON 序列化的主体数据。

MOSN 反向通道完整的生命周期交互过程:

图片

建连过程中由 Tunnel Agent 主动发起,并且在 TCP 连接建立成功 (TLS 握手成功) 之后,将反向建连的关键信息 ConnectionInitInfo 序列化并传输给对端 Tunnel Server,该结构体定义了反向通道的元数据信息。

// ConnectionInitInfo is the basic information of agent host,
// it is sent immediately after the physical connection is established
type ConnectionInitInfo struct {
    ClusterName      string                 `json:"cluster_name"`
    Weight           int64                  `json:"weight"`
    HostName         string                 `json:"host_name"`
    CredentialPolicy string                 `json:"credential_policy"`
    Credential       string                 `json:"credential"`
    Extra            map[string]interface{} `json:"extra"`
}

Tunnel Server 接受该元数据信息之后,主要工作包括:

1. 如果有设置自定义鉴权方式,则进行连接鉴权;

2. clusterManager 将该连接加入到指定的 ClusterSnapshot 并回写建连结果。

此时建连过程才算完成。

func (t *tunnelFilter) handleConnectionInit(info *ConnectionInitInfo) api.FilterStatus {
    // Auth the connection
    conn := t.readCallbacks.Connection()
    if info.CredentialPolicy != "" {
        // 1. 自定义鉴权操作,篇幅原因省略
    }
    if !t.clusterManager.ClusterExist(info.ClusterName) {
        writeConnectResponse(ConnectClusterNotExist, conn)
        return api.Stop
    }
    // Set the flag that has been initialized, subsequent data processing skips this filter
    err := writeConnectResponse(ConnectSuccess, conn)
    if err != nil {
        return api.Stop
    }
    conn.AddConnectionEventListener(NewHostRemover(conn.RemoteAddr().String(), info.ClusterName))
    tunnelHostMutex.Lock()
    defer tunnelHostMutex.Unlock()
    snapshot := t.clusterManager.GetClusterSnapshot(context.Background(), info.ClusterName)
    // 2. host加入到指定的cluster
    _ = t.clusterManager.AppendClusterTypesHosts(info.ClusterName, []types.Host{NewHost(v2.Host{
        HostConfig: v2.HostConfig{
            Address:    conn.RemoteAddr().String(),
            Hostname:   info.HostName,
            Weight:     uint32(info.Weight),
            TLSDisable: false,
        }}, snapshot.ClusterInfo(), CreateAgentBackendConnection(conn))})
    t.connInitialized = true
    return api.Stop
}

然后是通信过程,为了便于理解,以下图请求单向流转示意图举例:

图片

在传统的 MOSN Sidecar 应用场景中,Frontend 发送的请求首先经过 Client-MOSN,然后通过路由模块,主动创建连接 (虚线部分) 并流转到对端,经由 Server-MOSN biz-listener 处理转交给 Backend。

而在云边场景的反向通道实现中,Client MOSN (Tunnel Server) 在接受到对端 Tunnel Agent 发起创建反向通道的请求后,即将该物理连接加入路由到对端 MOSN 的 cluster snapshot 中。从而 Frontend 的请求流量能由该反向通道流转到对端 MOSN,而因为 Tunnel Agent 侧把该连接托管给了 biz-listener,则读写处理都由 biz-listener 进行处理,biz-listener 将处理完的请求再转发给真正的 Backend 服务。

Part.5--总结和规划

本文主要介绍了 MOSN 反向通道的实现原理和设计思路。MOSN 作为高性能的云原生网络代理,希望反向通道的能力能更加有效地支持其作为云边协同场景中承接东西向流量的职责。

当然,后续我们也会继续做一系列的拓展支持,包括但不限于:

1. 反向通道支持 gRPC 实现,gRPC 作为云原生时代最通用的服务通讯框架,本身内置了各种强大的治理能力;

2. 结合更多云原生场景,内置更加通用的 Tunnel Server 动态服务发现能力组件;

3. 更多的配套自动化运维和部署工具。

了解更多…

MOSN Star 一下✨:
https://github.com/mosn/mosn

快来和我们一起共建吧🧸

本周推荐阅读

Go 原生插件使用问题全解析

图片

MOSN 构建 Subset 优化思路分享

图片

MOSN 文档使用指南

图片

MOSN 1.0 发布,开启新架构演进

图片

相关文章
|
Kubernetes 网络协议 调度
聊聊k8s中的hostport和NodePort
让服务基于节点暴露端口,大家最先想到的应该都是 nodeport 这种 service,他可以非常方便的为pod代理外部流量,那么 hostport 是什么呢?他和 nodeport 的区别是什么,今天我们来聊聊这个比较小众的 hostport。
7735 0
聊聊k8s中的hostport和NodePort
|
Rust 算法 Go
【密码学】一文读懂FNV Hash
FNV哈希全名为Fowler-Noll-Vo算法,是以三位发明人Glenn Fowler,Landon Curt Noll,Phong Vo的名字来命名的,最早在1991年提出。它可以快速hash大量的数据并保持较小的冲突概率,适合hash一些相近的字符串比如IP地址、URL、文件名等等。目前FNV算法有三个版本,分别是: FNV-0(已废弃)、FNV-1以及FNV-1a。这三个算法的结构非常相似,因此呢,在这里就一块说了。
4831 0
【密码学】一文读懂FNV Hash
|
Ubuntu 开发工具
Ubuntu更换阿里云软件源
Ubuntu更换阿里云软件源
146427 0
|
8月前
|
存储 监控 Java
“代码跑着跑着,就变快了?”——揭秘Java性能幕后引擎:即时编译器
HotSpot虚拟机内置C1和C2两个即时编译器。C1启动快,适合快速执行;C2优化强,适合长期运行。自Java 9起,默认启用C2或分层编译。分层编译结合C1与C2优势,共分5层,逐步提升编译质量。方法调用计数器与循环回边计数器用于识别热点代码,触发JIT编译。循环回边计数器还可启动栈上替换(OSR),提升大循环性能。本文详解JIT编译机制与性能优化策略。
361 75
|
移动开发 JavaScript 前端开发
精通服务器推送事件(SSE)与 Python 和 Go 实现实时数据流 🚀
服务器推送事件(SSE)是HTML5规范的一部分,允许服务器通过HTTP向客户端实时推送更新。相比WebSocket,SSE更轻量、简单,适合单向通信场景,如实时股票更新或聊天消息。它基于HTTP协议,使用`EventSource` API实现客户端监听,支持自动重连和事件追踪。虽然存在单向通信与连接数限制,但其高效性使其成为许多轻量级实时应用的理想选择。文中提供了Python和Go语言的服务器实现示例,以及HTML/JavaScript的客户端代码,帮助开发者快速集成SSE功能,提升用户体验。
|
人工智能 自然语言处理 IDE
Trae 接入 Claude 3.7:AI 编程工具界的“卷王”,完全免费使用!
Trae 是一款完全免费的AI编程工具,现已接入 Claude 3.7 模型,提供代码生成、调试等强大功能,支持多模态输入和上下文理解,用户可享受24小时高速服务,无需担心付费限制。Trae 支持多平台,安装简便,适合开发者快速上手。
4487 24
Trae 接入 Claude 3.7:AI 编程工具界的“卷王”,完全免费使用!
|
存储 缓存 Java
写代码原来如此简单:两种常用代码范式
一次项目包含非常多的流程,有需求拆解,业务建模,项目管理,风险识别,代码模块设计等等,如果我们在每次项目中,都将精力大量放在这些过程的思考上面,那我们剩余的,放在业务上思考的精力和时间就会大大减少;这也是为什么我们要 总结经验/方法论/范式 的原因;这篇文章旨在建立代码模块设计上的思路,给出了两种非常常用的设计范式,减少未来在这一块的精力开销。
332 11
|
存储 缓存 数据库
CodeFuse开源ModelCache大模型语义缓存
CodeFuse 开源火热进行中!本次开源的是 ModelCache 大模型语义缓存,可大幅降低大模型应用的推理成本,提升用户体验。 CodeFuse-ModelCache 项目地址: https://github.com/codefuse-ai/CodeFuse-ModelCache
1018 0
|
缓存 NoSQL Java
互联网大厂的缓存策略:抵抗超高并发的秘密武器,已开源!
该文探讨了如何在互联网大厂项目中设计和使用缓存,以应对百万级流量。作者以秒杀系统为例,提出采用本地缓存+分布式缓存的混合型缓存架构,强调了缓存设计在抗高并发流量中的重要性。文章详细阐述了秒杀系统对缓存的核心诉求,包括流量分层、读写策略和并发控制,以及如何处理缓存失效的情况。此外,还介绍了缓存的使用场景,如接口缓存策略,并分析了缓存集群方案、缓存交互流程和缓存刷新机制。最后,讨论了数据一致性问题和代码的扩展性实现。文中提供了开源的缓存方案链接,包括GitHub、Gitee和GitCode仓库。
444 0
|
人工智能 JSON 监控
探索Viper-适用于GoLang的完整配置解决方案
探索Viper-适用于GoLang的完整配置解决方案

热门文章

最新文章

下一篇
开通oss服务