SOFAMosn 笔记


written by Alex Stocks on 2019/07/06,版权所有,无授权不得转载

关于 SOFAMosn 与愚人的故事,须从 2018 年 9 月 16 日说起。此日在离京南下某地参加毕业 X 周年聚会的火车途中,dubbogo QQ 群群友 老C 发来一条连接,说是其参与的一个叫做 SOFAMosn 的开源项目使用了愚人的 dubbogo/hessian2 项目,以与 apache/dubbo 进行通信。

老C顺便提及,项目由其所在的阿里大文娱UC事业部和蚂蚁金服共建。既然是大厂出品,愚人便兴起翻阅了下此项目,彼时对 Service Mesh 无甚了解,便意兴阑珊掩鼻而过。

话说缘分天注定。半载过后,愚人工作内容便是参与此项目相关系统开发,便需要对其机理深入了解,以免踩坑。

1 SOFAMosn

SOFAMosn 是蚂蚁金服 Service Mesh 整体实践中最基础的组件。

参考文档 蚂蚁金服 Service Mesh 落地实践与挑战 文中述及了蚂蚁金服当前的 Service Mesh 进展情况,不同于开源的 Istio 体系,蚂蚁金服内部版 Service Mesh 落地优先考虑数据面的实现与落地,控制面在逐步建设中,整体的架构上看,我们使用数据面直接和内部的各种中间件服务端对接,来完成 RPC、消息等能力的下沉,给业务应用减负。SOFAMosn 便是数据平面落地的产物。

SOFAMosn 在 Service Mesh 充当 sidecar 角色,可以粗浅地理解为 Go 语言版本的 Envoy,目前其形态如下:

整体架构与 RPC 有些许相似:

2 SOFAMosn 的基本概念

愚人刚开始学习 SOFAMosn 相关概念的时候,是通过其配置文件,并类比于 Envoy 入门的。

上图是 Service Mesh 布道师 宋净超(Jimmy Song) 绘制的 Envoy 架构图,用宋老师的一句话总结图中流程即为,host A 经过 Envoy 访问 host B 的过程,每个 host 上都可能运行多个 service,Envoy 中也可能有多个 Listener,每个 Listener 中可能会有多个 filter 组成了 chain。

2.1 SOFAMosn 配置

SOFAMosn 的配置文件大体内容如下:

{"servers": [
    {"mosn_server_name": "mosn_server_1",
      "listeners": [{
          "name": "ingress_sofa","address": "0.0.0.0:12220", "type": "ingress",
          "filter_chains": [{
              "match": "",
              "filters": [{
                  "type": "proxy",
                  "config": {
                    "downstream_protocol": "SofaRpc","name": "proxy_config","upstream_protocol": "SofaRpc","router_config_name": "test_router"}},
                    {"type": "connection_manager",
                        "config": {
                            "virtual_hosts":["routers": [{"route": {"cluster_name": "test_cpp"}]]
                    }]}]}],
  "cluster_manager": {
    "clusters": [{
        "name": "test_cpp", "lb_type": "LB_ROUNDROBIN",
        "health_check": {
          "protocol": "SofaRpc", "timeout": "90s"},
        "hosts": [{
            "address": "11.166.22.163:12200", "hostname": "downstream_machine1", "weight": 1}]}]}
}

上面内容源自开源版本 SOFAMosn 的配置文件 mosn_config.json,经愚人裁剪和合并,以利于阅读。

配置分为 "servers" 和 "cluster_manager" 两块。"servers" 主要存储了 SOFAMosn 对 "host A" 的监听端口。

"cluster_manager" 则用于描述其后端 upstream 提供 service 的 hosts 集合。

"servers" 和 "clustermanager" 衔接的关键之处是 "servers.listeners.filterchains.filters.config.virtualhosts.routers.route.clustername"。

SOFAMosn 本质是一个 Local(Client-Side) Proxy,downstream 通过它把请求路由到 upstream。

2.1.1 SOFAMosn Servers

套用宋老师的话,SOFAMosn 有多个 Listener 组成了 "listeners" ,每个 Listener 中有多个 Filters 组成了 "filter_chains"。

Listener 是处于 downstream 位置的 “host A" 可以访问的网络地址,一般为一个 tcp port。

Listener filter 可以理解为 codec(协议处理),每个port(listener)上可以有多个 filter,即在一个网络地址上可以进行多种downstream 协议解析。Listener 使用 listener filter(监听器过滤器)来操作链接的元数据。

一般地,出于效率考虑,listen filter 不会完整解析 downstream 发来的完整协议,而是只解析部分头部,获取必要的路由相关的字段即可。

配置文件中 "servers.listeners.filter_chains.filters.type" 字段的值 "proxy" 完美地点出了 SOFAMosn 的 Local Prxoy 角色。

配置文件中的 "servers.listeners.filter_chains.filters.config" 中的 "downstream" 与 "upstream" 表明了其上下游使用的 filter(codec) 协议。

Router 作为就是路由,用于选择 downstream 请求目的地 upstream cluster,是 MOSN 的核心模块,支持的功能包括:

配置文件中的 Router 是一种 VirtualHost 形式的路由,字段 "servers.listeners.filterchains.filters.config.virtualhosts.routers.route.clustername" 的值 "testcpp",表明其使用 ”clustermanager.clusters.name“:"testcpp" 的相关 cluster。

2.1.2 SOFAMosn cluster manager

集群(cluster)是一组提供相同服务的 上游(upstream) 主机(Host) 集合,类比于 dubbo 中的 provider 列表,其内容主要有:

upstream cluster 集合除了可在配置中获取外,也可以通过 XDS 方式发现上游服务,其流程如下图:

图片流程清晰如斯,愚人就不再多用文字画蛇添足了。

3 SOFAMosn 网络层

一般的 RPC,downstream 与 upstream 之间直接进行网络通信,其网络层模型如下:

上图中 RPC 各个模型的作用有文字解释,此处不再赘述。本质为 Proxy 的 SOFAMosn 隔离了 downstream 和 upstream,其网络模型如下:

各个模块作用如下:

NET/IO 在代码层映射为 Listener 和 Connection,Listener 用来监听端口,并接收新连接。Connection 用来管理 Listener 上 accept 来的 tcp 连接,包括从 tcp conn 上读写数据等,接口定义在 sofamosn/pkg/types/network.go

Protocol 收到 downstream 发来的二进制流后,根据配置文件中的协议名称选择相应的 decoder,解码后的报文整体包分为 header、body 和 tailer 三部分,接口定义在 sofamosn/pkg/types/protocol.go

SOFAMosn 的 Stream 概念非常类似网络编程的 multiplexing 概念:通过全局唯一的 stream id 实现 request 和 response 报文关联,实现在一个连接上实现多路流传输。Stream 具有方向性,区分 upStream 和 downStream,且与协议强相关,不同格式的协议使用不同的 Stream。

Proxy 则是 SOFAMosn 角色的体现,在 upStream 和 downStream 之间进行路由选择,在 SOFAMosn 中其还管理连接池、service 集群。

3.1 网络线程模型

SOFAMosn 网络层采用了两种网络模型,分别针对不同的使用场景。

上图是 SOFAMosn 0.1.0 版本的线程模型,也是一种比较经典的 Go 网络双工线程模型,其各个部分职能如下:

这种网络模型适合在连接数不满 1k 时处理吞吐比较高的长连接场景,但是在连接达 10k 的短链接场景就不合适了。最简单的道理,gr 数目达 20k 时 go 的 gr 调度器的调度处理效率极低。

上图则是 SOFAMosn 最新版本提供的第二种网络线程模型,基于 epoll/kqueue 机制重新实现的 NetPoll。SOFAMosn 根据 CPU 核数定制一个 Poller,每个 Poller 有一个常驻 gr,downstream connection 将自身注册的读写事件到某个 Poller 中,当 Poller 的 gr 接收到可读事件后,再从 gr pool 中选择一个 gr 执行网络读事件处理

这种网络模型适用于短链接较多但是网络吞吐不高的场景,如 Gateway。

SOFAMosn 默认情况下适用第一种网络模型。

4 SOFAMosn 代码分析

SOFAMosn 整体代码可读性不友好,估计其初始作者并没有很长时间的 Go 使用经验。

本节主要分析其配置解析、网络启动与网络事件处理流程,不涉及其运行流程。

4.1 Main 入口

开源版本 SOFAMosn 的 Main 入口文件在 cmd/mosn/main/mosn.go

SOFAMosn 使用了第三方库封装了一个 APP 代表整体程序,并能够处理 start、stop 和 reload 三个控制命令,但在控制命令处理文件cmd/mosn/main/control.go 中见到如下代码:

    cmdStop = cli.Command{
        Name:  "stop",
        Usage: "stop mosn proxy",
        Action: func(c *cli.Context) error {
            return nil
        },
    }

    cmdReload = cli.Command{
        Name:  "reload",
        Usage: "reconfiguration",
        Action: func(c *cli.Context) error {
            return nil
        },
    }

Stop 命令和 Reload 命令的 Action 函数为空,不知道这样的封装意义何在,涉嫌过度封装。实际使用命令仅仅 Start 而已。

整体 cmd/mosn/main 目录下有用的代码仅如下一行:

    mosn.Start(conf, serviceCluster, serviceNode)

如果还有其他有用代码的话,可能就是 mosn.go 文件的 import 语句库,可让各个相关子目录的 init() 函数在 SOFAMosn 启动时被调用。

真正的 SOFAMosn 对象为文件 mosn/starter.go 中的 mosn.Mosn,其定义如下:

// Mosn class which wrapper server
type Mosn struct {
    servers        []server.Server
    clustermanager types.ClusterManager
    routerManager  types.RouterManager
    config         *config.MOSNConfig
    adminServer    admin.Server
}

Mosn 总体启动过程如下:

4.2 读取并分析配置文件

前面 2.1 SOFAMosn 配置 一节中给出了 SOFAMosn 的标准配置文件,其对应的代码在 config/config.go,主要结构体定义如下:

type ApplicationInfo struct {
    AppName       string `json:"app_name,omitempty"`
}

// ServiceRegistryInfo
type ServiceRegistryInfo struct {
    ServiceAppInfo ApplicationInfo     `json:"application,omitempty"`
    MsgMetaInfo    map[string][]string `json:"msg_meta_info,omitempty"`
}

// MOSNConfig make up mosn to start the mosn project
// Servers contains the listener, filter and so on
// ClusterManager used to manage the upstream
type MOSNConfig struct {
    Servers         []v2.ServerConfig      `json:"servers,omitempty"`         //server config, listener
    ClusterManager  ClusterManagerConfig   `json:"cluster_manager,omitempty"` //cluster config, cluster
    ServiceRegistry v2.ServiceRegistryInfo `json:"service_registry"`          //service registry config, used by service discovery module
}

配置整体解析流程如下:

NewMosn 函数中启动了一个 config.DumpConfigHandler 的 goroutine,定时把程序对配置的变更内容更新入配置文件中。

4.3 listener 启动流程

SOFAMosn 的 servers 相关对象【server 和 listener】主要定义在 pkg/server 目录下,其主要文件内容如下:

pkg/network 目录则定义了网络连接、监听与读写处理流程。

listener 启动流程如下:

4.4 网络读写事件处理

3.1 网络线程模型 节中述到 SOFAMosn 提供了两种网络线程模型,本节只给出其第一种网络线程模型【下文简称 orow】下的读写事件处理流程。

关于写,SOFAMosn 对写采用了合并写优化。蚂蚁金服 Service Mesh 落地实践与挑战 一文写道, 通过 golang 的 writev 我们把多笔请求合并成一次写,降低 sys.call 的调用,提升整体的性能与吞吐,同时在使用 writev 的过程中,有发现 golang 对 writev 的实现有 bug,会导致部分内存无法回收,我们给 golang 提交 PR 修复此问题,已被接受:https://github.com/golang/go/pull/32138。

这个 bug 是同事元总【原 tengine 总负责人】发现并解决掉的,但是最新的 Go 语言尚未发版【Go 1.13】,实际处理方法则是把多次写的内容先在内存中合并,然后再调用一次写 sys.call 发送,相关代码如下:

    // connection.startRWLoop()
    for i := 0; i < 10; i++ {
        select {
        case buf, ok := <-c.writeBufferChan:
            if !ok {
                return
            }
            c.appendBuffer(buf)
        default:
        }
    }

参考文档

Payment

Timeline