-
Notifications
You must be signed in to change notification settings - Fork 2
/
index.json
1 lines (1 loc) · 627 KB
/
index.json
1
[{"body":"本文描述的是 MOSN 的 ListenerFilter 配置。\nListenerFilter 主要用于 listener 透明代理配置。\n目前 MOSN 一个 Listener 只支持一个 ListenerFilter。\nListenerFilter 的配置结构如下所示。\n{ \u0026#34;type\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;fallback_to_local\u0026#34;:\u0026#34;\u0026#34; } type 透明代理类型,当前版本下此处 type 需要与 listener 中的 use_original_dst 配置一致。 fallback_to_local bool 类型,用于标记匹配本地监听的 ip 地址,true 使用 127.0.0.1,false 使用 0.0.0.0,当所有 listener 的 ip 都不匹配代理到的请求时将尝试使用监听本地 ip 的 listener 处理。 ","excerpt":"本文描述的是 MOSN 的 ListenerFilter 配置。\nListenerFilter 主要用于 listener 透明代理配置。\n目前 MOSN 一个 Listener …","ref":"https://mosn.io/docs/products/configuration-overview/server/listener/listener-filter/","title":"ListenerFilter"},{"body":"MOSN(Modular Open Smart Network)是一款主要使用 Go 语言开发的云原生网络代理平台,由蚂蚁集团开源并经过双 11 大促几十万容器的生产级验证。 MOSN 为服务提供多协议、模块化、智能化、安全的代理能力,融合了大量云原生通用组件,同时也可以集成 Envoy 作为网络库,具备高性能、易扩展的特点。 MOSN 可以和 Istio 集成构建 Service Mesh,也可以作为独立的四、七层负载均衡,API Gateway、云原生 Ingress 等使用。\n核心能力 Istio 集成 集成 Istio 1.10 版本,可基于全动态资源配置运行 核心转发 自包含的网络服务器 支持 TCP 代理 支持 UDP 代理 支持透明劫持模式 多协议 支持 HTTP/1.1,HTTP/2 支持基于 XProtocol 框架的多协议扩展 支持多协议自动识别 支持 gRPC 协议 核心路由 支持基于 Domain 的 VirtualHost 路由 支持 Headers/Path/Prefix/Variable/DSL 等多种匹配条件的路由 支持重定向、直接响应、流量镜像模式的路由 支持基于 Metadata 的分组路由、支持基于权重的路由 支持基于路由匹配的重试、超时配置 支持基于路由匹配的请求头、响应头处理 后端管理 \u0026amp; 负载均衡 支持连接池管理 支持长连接心跳处理 支持熔断、支持后端主动健康检查 支持 Random/RR/WRR/EDF 等多种负载均衡策略 支持基于 Metadata 的分组负载均衡策略 支持 OriginalDst/DNS/SIMPLE 等多种后端集群模式,支持自定义扩展集群模式 可观察性 支持格式可扩展的 Trace 模块,集成了 jaeger/skywalking 等框架 支持基于 prometheus 的 metrics 格式数据 支持可配置的 AccessLog 支持可扩展的 Admin API 集成 Holmes,自动监控 pprof TLS 支持多证书匹配模式、支持 TLS Inspector 模式 支持基于 SDS 的动态证书获取、更新机制 支持可扩展的证书获取、更新、校验机制 支持基于 CGo 的国密套件 进程管理 支持平滑升级,包括连接、配置的平滑迁移 支持优雅退出 扩展能力 支持基于 go-plugin 的插件扩展模式的 支持基于进程的扩展模式 支持基于 WASM 的扩展模式 支持自定义扩展配置 支持自定义的四层、七层Filter扩展 社区介绍 MOSN 开源仍在高速发展中,有很多能力需要补全,欢迎所有人参与进来与我们一起共建。\n关于 MOSN 社区的详细介绍请查看 mosn/community 仓库,如有任何疑问欢迎提交 Issue。\n","excerpt":"MOSN(Modular Open Smart Network)是一款主要使用 Go 语言开发的云原生网络代理平台,由蚂蚁集团开源并经过双 11 大促几十万容器的生产级验证。 MOSN 为服务提供多协 …","ref":"https://mosn.io/docs/products/overview/","title":"MOSN 简介"},{"body":"本文用于帮助初次接触 MOSN 项目的开发人员,快速搭建开发环境,完成编译,测试,镜像制作和示例配置的运行。\n准备运行环境 如果您使用容器运行 MOSN,请先 安装 docker 如果您使用本地机器,请使用类 Unix 环境 安装 Go 的编译环境 获取代码 MOSN 项目的代码托管在 Github,获取方式如下:\ngit clone [email protected]:mosn/mosn.git 最终 MOSN 的源代码代码路径为 $GOPATH/src/mosn.io/mosn\n导入 IDE 使用您喜爱的 Go IDE 导入 mosn 项目,推荐 Goland。\n编译代码 在项目根目录下,根据自己机器的类型以及欲执行二进制的环境,选择以下命令编译 MOSN 的二进制文件。\n切换 Istio 支持版本 MOSN 目前支持xDS v2 与 xDS v3,分别以Istio 1.5.2 和 Istio 1.10.6 为代表,可以根据需求在不同的版本支持之间切换。默认使用的是1.10.6版本。\n切换到1.5.2 版本(xDS v2)\nmake istio-1.5.2 切换到1.10.6版本(xDS v3)\nmake istio-1.10.6 使用 docker 镜像编译 make build // 编译出 linux 64bit 可运行二进制文件 本地编译 使用下面的命令编译本地可运行二进制文件。\nmake build-local 完成后可以在 build/bundles/${version}/binary 目录下找到编译好的二进制文件。\n运行测试 支持两种环境来运行测试,如果有 docker 环境的,推荐使用 docker 环境,环境更干净可控。 MOSN 项目集成的 CI 是使用的 docker 环境来运行的。\n使用 docker 环境运行测试 在项目根目录下执行如下命令:\n# 单元测试 make unit-test # 集成测试(较慢) make integrate # 新版集成测试(较慢) make integrate-new 使用本地环境运行测试 在项目根目录下执行如下命令:\n# 单元测试 make ut-local # 集成测试(较慢) make integrate-local # 新版集成测试(较慢) make integrate-framework 运行 MOSN 运行下面的命令,将使用一个 示例配置文件 启动 MOSN。\n./build/bundles/${version}/binary/mosn start -c configs/mosn_config.json MOSN 配置说明 这个示例,我们模拟了经典的 service mesh 中,MOSN 作为 sidecar 的场景。\n POD A POD B ------------------ ------------------- | App A =\u0026gt; MOSN A | ==\u0026gt; | MOSN B =\u0026gt; App B | ------------------- ------------------- 建议打开 示例配置文件 ,阅读如下配置说明。\n应用服务 其中,appListener 这个 listener 监听了 2047 端口,使用 application 这个 router, router 内配置了 direct_response,输出静态配置内容。 在这个示例里,模拟一个应用服务,App B。\n我们可以使用如下命令测试:\n$ curl \u0026#39;http://localhost:2047/\u0026#39; Welcome to MOSN! The Cloud-Native Network Proxy Platform. 流量代理转发 其中,serverListener 这个 listener 监听了 2046 端口,使用 server_router 这个 router, router 内配置启用了 proxy 这个 filter,转发到 serverCluster 这个 cluster。\n在 cluster_manager 中可以看到 serverCluster 的具体配置:127.0.0.1:2047,也就是上面的 App B。 serverListener 在示例里模拟了 MOSN B,代理了 App B 的入口流量。\nclientListener 和 serverListener 类似,由 2045 端口转发到 2046。 模拟了 MOSN A,代理了 App A 的入口流量。\n剩下 App A,我们可以通过 curl 来模拟:\n$ curl \u0026#39;http://localhost:2045/\u0026#39; Welcome to MOSN! The Cloud-Native Network Proxy Platform. 以上就构建了 MOSN 作为 sidecar 的典型使用示例。 其他更多场景的用法,请参考 配置概览。\n创建镜像 执行如下命令创建 docker image\nmake image 更多 MOSN 示例程序 参考 examples 目录下的示例工程运行 Samples。\n使用 MOSN 搭建 Service Mesh 平台 请参考与 Istio 集成。\n","excerpt":"本文用于帮助初次接触 MOSN 项目的开发人员,快速搭建开发环境,完成编译,测试,镜像制作和示例配置的运行。\n准备运行环境 如果您使用容器运行 MOSN,请先 安装 docker 如果您使用本地机 …","ref":"https://mosn.io/docs/user-guide/start/proxy/","title":"快速开始"},{"body":"MOSN 贡献指引 首先很感谢您有兴趣给 MOSN 提交 PR。 为了能更高效的推进 PR 的合并,我们有一些推荐的做法,希望能有所帮助。\n 创建分支 推荐使用新分支来开发,master 分支推荐保持跟 MOSN 上游主分支保持一致 PR 需要说明意图 如果已经有对应讨论的 issue,可以引用 issue 如果没有 issue,需要描述清楚 PR 的意图,比如 bug 的情况。 如果改动比较大,最好可以比较详细的改动说明介绍。 提交新的 commit 来处理 review 意见 当 PR 收到 review 意见后,有新的改动,推荐放到新的 commit,不要追加到原来的 commit,这样方便 reviewer 查看新的变更 尽量提交小 PR 不相关的改动,尽量放到不同的 PR,这样方便快速 review 合并 尽量减少 force push 因为 force push 之后,review 意见就对不上原始的代码记录了,这样不利于其他人了解 review 的过程。除非是因为需要 rebase 处理跟 master 的冲突,这种 rebase 后就只能 force push 了。 最后,很重要的一点,写清楚 commit log。\n首先 commit log 推荐以一个单词开头,这样方便快速知晓 commit 的类型,比如下面的这些:\n feature: 实现了一个新的功能/特性 change: 没有向后兼容的变更 refactor: 代码重构 bugfix: bug 修复 optimize: 性能优化相关的变更 doc: 文档变更,包括注释 tests: 测试用例相关的变更 style: 代码风格相关的调整 sample: 示例相关的变更 chore: 其他不涉及核心逻辑的小改动 开头单词之后,是简要介绍一下改动的内容,比如新增了什么功能,如果是 bugfix 的话,还需要说明 bug 复现的条件,以及危害。 比如:\nbugfix: got the wrong CACert filename when converting the listen filter from istio LDS, mosn may not listen success then. 如果比较复杂,一句话写不清楚的话,也可以写多行,commit log 不用怕太长。\n如果英文不容易写清楚,在 PR comment 里用中文描述清楚也可以的。\n最后,祝玩得开心。\n","excerpt":"MOSN 贡献指引 首先很感谢您有兴趣给 MOSN 提交 PR。 为了能更高效的推进 PR 的合并,我们有一些推荐的做法,希望能有所帮助。\n 创建分支 推荐使用新分支来开发,master 分支推荐保持 …","ref":"https://mosn.io/docs/open-source/contributing-source-code/contribute/","title":"贡献指引"},{"body":"本文描述的是 SkyWalking Trace 配置。\n目前支持 HTTP1 协议追踪。\nSkyWalking 描述的 MOSN 的基本全局参数如下所示。\n{ \u0026#34;tracing\u0026#34;: { \u0026#34;enable\u0026#34;: true, \u0026#34;driver\u0026#34;: \u0026#34;SkyWalking\u0026#34;, \u0026#34;config\u0026#34;: { \u0026#34;reporter\u0026#34;: \u0026#34;gRPC\u0026#34;, \u0026#34;backend_service\u0026#34;: \u0026#34;127.0.0.1:11800\u0026#34;, \u0026#34;service_name\u0026#34;: \u0026#34;mosn\u0026#34;, \u0026#34;max_send_queue_size\u0026#34;: 30000, \u0026#34;authentication\u0026#34;: \u0026#34;mosn\u0026#34;, \u0026#34;tls\u0026#34;: { \u0026#34;cert_file\u0026#34;: \u0026#34;cert.crt\u0026#34;, \u0026#34;server_name_override\u0026#34;: \u0026#34;mosn.io\u0026#34; } } } } reporter trace 数据上报模式, 支持 log(仅用于测试) 和 gRPC 两种模式 。\n 如果配置为空,则默认为 log。 backend_service SkyWalking 后端服务地址,仅在上报模式为 gRPC 模式时使用 。\n 示例:127.0.0.1:11800。 service_name 注册到 SkyWalking 的服务名称,仅在上报模式为 gRPC 模式时使用 。\n 如果配置为空,则默认为 mosn。 max_send_queue_size trace 数据缓冲队列大小,仅在上报模式为 gRPC 模式时使用 。\n 如果配置为空,则默认为 30000。 authentication gRPC 身份认证参数,仅在上报模式为 gRPC 模式时使用 。\n 如果配置不为空,在与 SkyWalking 后端服务建立连接时会使用此参数进行身份认证。 tls 仅在上报模式为 gRPC 模式时使用 。\n 如果配置不为空,将使用 TLS 连接 SkyWalking 后端服务。 cert_file TLS 客户端证书。\nserver_name_override 服务名称。\n配置示例 更多细节可以参考《MOSN 支持使用 SkyWalking 进行分布式追踪》 。这篇文档提供了配置示例和演示视频。\n","excerpt":"本文描述的是 SkyWalking Trace 配置。\n目前支持 HTTP1 协议追踪。\nSkyWalking 描述的 MOSN 的基本全局参数如下所示。\n{ \u0026#34;tracing\u0026#34;: { …","ref":"https://mosn.io/docs/products/configuration-overview/trace/skywalking/","title":"SkyWalking 配置"},{"body":"本文描述的是 MOSN 的 FilterChain 配置。\nFilterChain 是 MOSN Listener 配置中核心逻辑配置,不同的 FilterChain 配置描述了 Listener 会如何处理请求。\n目前 MOSN 一个 Listener 只支持一个 FilterChain。\nFilterChain 的配置结构如下所示。\n{ \u0026#34;tls_context\u0026#34;: {}, \u0026#34;tls_context_set\u0026#34;: [], \u0026#34;filters\u0026#34;: [] } tls_context_set 一组 tls_context 配置,MOSN 默认使用 tls_context_set 来描述 listener 的 TLS 的证书信息。 一个 listener 可同时支持配置多张 TLS 证书。 tls_context 单独配置 tls_context 而不是使用 tls_context_set 是兼容 MOSN 历史配置(只支持一张证书配置时)的场景,这种配置方式后面会逐步废弃。 tls_context 的详细配置说明,参考 tls_context。 filters 一组 network filter 配置。\nnetwork filter network filter 描述了 MOSN 在连接建立以后如何在 4 层处理连接数据。\n{ \u0026#34;type\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;config\u0026#34;: {} } type 是一个字符串,描述了 filter 的类型。 config 可以是任意 json 配置,描述不同 filter 的配置。 network filter 可自定义扩展实现,默认支持的 type 包括 proxy、tcp proxy、connection_manager。 connection_manager 是一个特殊的 network filter,它需要和 proxy 一起使用,用于描述 proxy 中路由相关的配置,是一个兼容性质的配置,后续可能有修改。 ","excerpt":"本文描述的是 MOSN 的 FilterChain 配置。\nFilterChain 是 MOSN Listener 配置中核心逻辑配置,不同的 FilterChain 配置描述了 Listener 会 …","ref":"https://mosn.io/docs/products/configuration-overview/server/listener/filter-chain/","title":"FilterChain"},{"body":"proxy 是 MOSN 最常用的 network filter,其配置格式如下。\n{ \u0026#34;downstream_protocol\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;upstream_protocol\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;router_config_name\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;extend_config\u0026#34;:{} } downstream_protocol 描述 proxy 期望收到的请求协议,在连接收到数据时,会使用此协议去解析数据包并完成转发,如果收到的数据包协议和配置不符,MOSN 会将连接断开。 upstream_protocol 描述 proxy 将以何种协议转发数据,通常情况下应该和downstream_protocol 保持一致,只有特殊的场景会进行对应协议的转换。 router_config_name 描述 proxy 的路由配置的索引,通常情况下,这个配置会和同 listener 下的 connection_manager 中配置的 router_config_name 保持一致。 extend_config 扩展配置,目前仅在 MOSN 的 XProtocol 协议中使用。 ","excerpt":"proxy 是 MOSN 最常用的 network filter,其配置格式如下。\n{ \u0026#34;downstream_protocol\u0026#34;:\u0026#34;\u0026#34;, …","ref":"https://mosn.io/docs/products/configuration-overview/server/listener/network-filter/proxy/","title":"proxy"},{"body":"bin/init.sh\n-WASM_RELEASE_DIR=${ISTIO_ENVOY_LINUX_RELEASE_DIR} -for plugin in stats metadata_exchange -do - FILTER_WASM_URL=\u0026#34;${ISTIO_ENVOY_BASE_URL}/${plugin}-${ISTIO_ENVOY_VERSION}.wasm\u0026#34; - download_wasm_if_necessary \u0026#34;${FILTER_WASM_URL}\u0026#34; \u0026#34;${WASM_RELEASE_DIR}\u0026#34;/\u0026#34;${plugin//_/-}\u0026#34;-filter.wasm - FILTER_WASM_URL=\u0026#34;${ISTIO_ENVOY_BASE_URL}/${plugin}-${ISTIO_ENVOY_VERSION}.compiled.wasm\u0026#34; - download_wasm_if_necessary \u0026#34;${FILTER_WASM_URL}\u0026#34; \u0026#34;${WASM_RELEASE_DIR}\u0026#34;/\u0026#34;${plugin//_/-}\u0026#34;-filter.compiled.wasm -done +#WASM_RELEASE_DIR=${ISTIO_ENVOY_LINUX_RELEASE_DIR} +#for plugin in stats metadata_exchange +#do +# FILTER_WASM_URL=\u0026#34;${ISTIO_ENVOY_BASE_URL}/${plugin}-${ISTIO_ENVOY_VERSION}.wasm\u0026#34; +# download_wasm_if_necessary \u0026#34;${FILTER_WASM_URL}\u0026#34; \u0026#34;${WASM_RELEASE_DIR}\u0026#34;/\u0026#34;${plugin//_/-}\u0026#34;-filter.wasm +# FILTER_WASM_URL=\u0026#34;${ISTIO_ENVOY_BASE_URL}/${plugin}-${ISTIO_ENVOY_VERSION}.compiled.wasm\u0026#34; +# download_wasm_if_necessary \u0026#34;${FILTER_WASM_URL}\u0026#34; \u0026#34;${WASM_RELEASE_DIR}\u0026#34;/\u0026#34;${plugin//_/-}\u0026#34;-filter.compiled.wasm +#done bin/update_proxy.sh\n-WASM_URL=${ISTIO_ENVOY_BASE_URL}/${plugin}-${ISTIO_ENVOY_VERSION}.wasm -printf \u0026#34;Verifying %s is available\\n\u0026#34; \u0026#34;$WASM_URL\u0026#34; -until curl --output /dev/null --silent --head --fail \u0026#34;$WASM_URL\u0026#34;; do - printf \u0026#39;.\u0026#39; - sleep $SLEEP_TIME -done +#WASM_URL=${ISTIO_ENVOY_BASE_URL}/${plugin}-${ISTIO_ENVOY_VERSION}.wasm +#printf \u0026#34;Verifying %s is available\\n\u0026#34; \u0026#34;$WASM_URL\u0026#34; +#until curl --output /dev/null --silent --head --fail \u0026#34;$WASM_URL\u0026#34;; do +# printf \u0026#39;.\u0026#39; +# sleep $SLEEP_TIME +#done pilot/docker/Dockerfile.proxyv2\n-COPY stats-filter.wasm /etc/istio/extensions/stats-filter.wasm -COPY stats-filter.compiled.wasm /etc/istio/extensions/stats-filter.compiled.wasm -COPY metadata-exchange-filter.wasm /etc/istio/extensions/metadata-exchange-filter.wasm -COPY metadata-exchange-filter.compiled.wasm /etc/istio/extensions/metadata-exchange-filter.compiled.wasm +#COPY stats-filter.wasm /etc/istio/extensions/stats-filter.wasm +#COPY stats-filter.compiled.wasm /etc/istio/extensions/stats-filter.compiled.wasm +#COPY metadata-exchange-filter.wasm /etc/istio/extensions/metadata-exchange-filter.wasm +#COPY metadata-exchange-filter.compiled.wasm /etc/istio/extensions/metadata-exchange-filter.compiled.wasm tools/istio-docker.mk\n# rule for wasm extensions. -$(ISTIO_ENVOY_LINUX_RELEASE_DIR)/stats-filter.wasm: init -$(ISTIO_ENVOY_LINUX_RELEASE_DIR)/stats-filter.compiled.wasm: init -$(ISTIO_ENVOY_LINUX_RELEASE_DIR)/metadata-exchange-filter.wasm: init -$(ISTIO_ENVOY_LINUX_RELEASE_DIR)/metadata-exchange-filter.compiled.wasm: init +#$(ISTIO_ENVOY_LINUX_RELEASE_DIR)/stats-filter.wasm: init +#$(ISTIO_ENVOY_LINUX_RELEASE_DIR)/stats-filter.compiled.wasm: init +#$(ISTIO_ENVOY_LINUX_RELEASE_DIR)/metadata-exchange-filter.wasm: init +#$(ISTIO_ENVOY_LINUX_RELEASE_DIR)/metadata-exchange-filter.compiled.wasm: init -docker.proxyv2: $(ISTIO_ENVOY_LINUX_RELEASE_DIR)/stats-filter.wasm -docker.proxyv2: $(ISTIO_ENVOY_LINUX_RELEASE_DIR)/stats-filter.compiled.wasm -docker.proxyv2: $(ISTIO_ENVOY_LINUX_RELEASE_DIR)/metadata-exchange-filter.wasm -docker.proxyv2: $(ISTIO_ENVOY_LINUX_RELEASE_DIR)/metadata-exchange-filter.compiled.wasm +#docker.proxyv2: $(ISTIO_ENVOY_LINUX_RELEASE_DIR)/stats-filter.wasm +#docker.proxyv2: $(ISTIO_ENVOY_LINUX_RELEASE_DIR)/stats-filter.compiled.wasm +#docker.proxyv2: $(ISTIO_ENVOY_LINUX_RELEASE_DIR)/metadata-exchange-filter.wasm +#docker.proxyv2: $(ISTIO_ENVOY_LINUX_RELEASE_DIR)/metadata-exchange-filter.compiled.wasm pkg/config/constants/constants.go\n- BinaryPathFilename = \u0026#34;/usr/local/bin/envoy\u0026#34; + BinaryPathFilename = \u0026#34;/usr/local/bin/mosn\u0026#34; ","excerpt":"bin/init.sh\n-WASM_RELEASE_DIR=${ISTIO_ENVOY_LINUX_RELEASE_DIR} -for plugin in stats …","ref":"https://mosn.io/docs/user-guide/start/istio/istio-diff/","title":""},{"body":"本文是关于 MOSN ClusterManager 配置的说明。\nMOSN 中通过 cluster_manager 来管理转发的集群地址,通常与 Router 配合使用。\n\u0026quot;cluster_manager\u0026quot;:{ \u0026quot;tls_context\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;cluster_pool_enable\u0026quot;: \u0026quot;\u0026quot;, \u0026quot;clusters_configs\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;clusters\u0026quot;:[] } tls_context,可选配置,用于描述 Cluster 全局共享的 TLS 配置,该配置项需要结合 clusters 配置中的 cluster_manager_tls 配置项一起使用,TLS 详细配置见 tls_context 文档。 cluster_pool_enable,可选配置,bool 类型,用于控制所有Cluster是否使用独占的连接池,为 true 则使用Cluster独占连接池,默认值为 false。 clusters_configs,可选配置,字符串类型,用于设置 Cluster 列表从 clusters_configs 指定的文件中解析。 clusters,用于描述每个 Cluster 所采用的负载均衡算法、类型等细节信息。 注意:cluster_manager 中的 clusters 和 clusters_configs 不能同时配置。\ncluster { \u0026quot;name\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;type\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;sub_type\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;lb_type\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;max_request_per_conn\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;conn_buffer_limit_bytes\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;circuit_breakers\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;health_check\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;spec\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;lb_subset_config\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;original_dst_lb_config\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;cluster_manager_tls\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;tls_context\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;hosts\u0026quot;:[], \u0026quot;connect_timeout\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;idle_timeout\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;lbconfig\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;dns_refresh_rate\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;respect_dns_ttl\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;dns_lookup_family\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;dns_resolvers\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;dns_resolver_file\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;dns_resolver_port\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;cluster_pool_enable\u0026quot;:\u0026quot;\u0026quot; } name,字符串。用作 Cluster 的唯一标识。 type,字符串。用于表示 Cluster 的类型,目前支持的类型如下: SIMPLE,是最基础的类型。 ORIGINAL_DST,该种类型的一般会在透明劫持场景中使用,他会自动的把负载均衡 lb_type 修改成 LB_ORIGINAL_DST 类型,具体作用见下文 STRICT_DNS,该种类型会动态解析 Cluster 中的域名列表,并将域名对应的 A 记录全部加入转发列表中。 sub_type,已废弃。 lb_type,字符串。在集群中选择主机时使用的负载平衡器类型,目前支持的类型如下: LB_ROUNDROBIN,不带权重的轮训转发。 LB_RANDOM,随机转发。 LB_WEIGHTED_ROUNDROBIN,根据 host 的权重转发。 LB_ORIGINAL_DST,在透明劫持场景下使用原始目标地址做转发,也可以通过请求 header 设置目标地址,详情可看 original_dst_lb_config 配置项。 LB_LEAST_REQUEST,选择请求数最少的 host 转发。 LB_MAGLEV,一致性 hash 转发。 LB_REQUEST_ROUNDROBIN,同一个请求粒度的轮训转发。 LB_LEAST_CONNECTION,选择连接数最少的 host 转发。 LB_PEAK_EWMA,基于延迟的负载均衡算法,通过记录主机的延迟,选择延迟较好并避免选择较差的主机 max_request_per_conn,uint32 类型,暂未实现。 conn_buffer_limit_bytes,uint32 类型,暂未实现。 circuit_breakers,CircuitBreakers 类型,既 Thresholds 类型的数组,用于配置 Cluster 的熔断配置。 health_check,HealthCheck 类型,群集可选的健康检查配置。如果未指定该配置,则不会执行主动健康检查,且默认群集中的成员都将是健康状态。 spec,暂未使用。 lb_subset_config,LBSubsetConfig 类型,用于配置负载均衡的子集。 original_dst_lb_config,LBOriDstConfig 类型,用于配置 LB_ORIGINAL_DST 类型的负载均衡器配置。 cluster_manager_tls,bool 类型,用于控制每个 Cluster 是否使用全局共享的 TLS 配置,为 true 则共享,默认值为 false。 tls_context,TLSConfig 类型,连接到上游群集的 TLS 配置。若没有指定 TLS 配置,则新连接不会使用 TLS,配置实例参考 tls_context 文档。 hosts,[]Host 类型,用于配置 Cluster 中的机器列表。 connect_timeout,Duration 类型,连接到该群集中主机的超时时长,默认值为 3 秒。 idle_timeout,Duration 类型,用于设置 Cluster 中的连接空闲超时时间,若发生超时则会断开连接,中默认值为 0,表示不设置连接的空闲超时。 lbconfig,LbConfig类型,为负载均衡器提供的扩展配置,目前只有 LB_LEAST_REQUEST/LB_LEAST_CONNECTION/LB_PEAK_EWMA 类型的负载均衡器有使用。 dns_refresh_rate,Duration 类型,在群集类型是 STRICT_DNS 时,用于设置 DNS 刷新频率,此值默认为 5 秒。 respect_dns_ttl,bool 类型,用于设置当 Cluster 类型为 STRICT_DNS 时,其域名对应的解析频率是否遵循 DNS 返回的 TTL。 dns_lookup_family,字符串类型,DNS IP 地址解析策略。 如果未指定此设置,则该值默认为 V4_ONLY。取值列表如下: V4_ONLY,表示只解析 IPv4 V6_ONLY,表示只解析 IPv6 dns_resolvers,DnsResolverConfig 类型,在群集类型是 STRICT_DNS,此值用于指定群集的 DNS 解析相关配置。 dns_resolver_file,字符串类型,用于设置 DNS server 列表的文件路径,该值默认为使用 /etc/resolv.conf 配置的默认解析器,该配置项仅在 dns_resolvers 未配置时生效。 dns_resolver_port,字符串类型,用于设置 DNS server 地址的 port,默认值为 53,该配置项仅在 dns_resolvers 未配置时生效。 cluster_pool_enable,bool 类型,用于控制当前Cluster是否使用独占的连接池,为 true 则使用独占连接池,默认值为 false。 Thresholds { \u0026quot;max_connections\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;max_pending_requests\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;max_requests\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;max_retries\u0026quot;:\u0026quot;\u0026quot; } max_connections,uint32 类型。用于设置 Cluster 中每台机器的最大连接数,对于 HTTP 协议,超过后会响应 502,对于多路复用协议则是控制单个 host 建立的最大连接数。默认值为 0 表示不启用该配置。 max_pending_requests,uint32 类型。代表 Cluster 的最大排队数量,暂未使用到。 max_requests,uint32 类型。将对上游群集执行的最大并行请求数,若超过限制则会响应 502,目前仅在 HTTP 系协议下生效。默认值为 0 表示不启用该配置。 max_retries,uint32 类型。允许上游集群执行的最大并行重试次数,目前只在 HTTP 系协议下生效。默认值为 0 表示不启用该配置。 HealthCheck HealthCheckConfig\n{ \u0026quot;protocol\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;timeout\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;interval\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;interval_jitter\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;healthy_threshold\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;unhealthy_threshold\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;service_name\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;check_config\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;event_log_path\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;common_callbacks\u0026quot;:\u0026quot;\u0026quot; } protocol,字符串类型,用于设置 Cluster 发起健康检查使用的协议类型,目前只支持 TCP。 timeout,Duration 类型,等待健康检查响应的时间。如果达到超时,则尝试健康检查将被视为失败。 interval,Duration 类型,每次尝试健康检查之间的时间间隔。 interval_jitter,Duration 类型,用于设置 interval 的随机抖动量,设置后将抖动量叠加到 interval 上。 healthy_threshold,uint32 类型,主机在标记为健康之前所需的连续健康检查次数,默认值为 1。 unhealthy_threshold,uint32 类型,在主机被标记为不健康之前,需要进行连续不健康的健康检查次数,默认值为 1。 service_name,字符串类型,暂未支持。 check_config,map[string]interface{} 类型,用于健康检查的扩展配置,当前支持 \u0026ldquo;http_check_config\u0026rdquo;。 event_log_path, 字符串类型,健康检查日志路径。 common_callbacks,[]string 类型,用于设置对应 Cluster 健康检查时执行的 callback。 LBSubsetConfig LBSubsetConfig 主要用于 Cluster 中更为灵活的请求路由,列如 ABTesting、金丝雀发布、单元化等。详细使用可以参考 MOSN subset 路由实践。\n{ \u0026quot;fall_back_policy\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;default_subset\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;subset_selectors\u0026quot;:\u0026quot;\u0026quot; } fall_back_policy,uint8 类型,用于设置在查找子集群失败时的容灾策略,当前支持如下配置: 0,表示 NoFallBack,没有查找到匹配的子集群则不使用容灾策略 1,表示 AnyEndPoint,既在 Cluster 的 Host 列表中轮训选择目标机器 2,表示使用 DefaultSubset 策略重新查找子集群 default_subset,map[string]string 类型,如果 fallback_policy 为 2 既 DEFAULT_SUBSET,则指定在回退期间使用的端点的默认子集。 subset_selectors,[][]string 类型,用于设置子集群匹配规则查找的条目。 LBOriDstConfig { \u0026quot;use_header\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;header_name\u0026quot;:\u0026quot;\u0026quot; } use_header,bool 类型,将该配置设置为 true 且负载均衡器使用 LB_ORIGINAL_DST 类型时,则转发的目标地址通过 header_name 从当前请求 header 中获取。 header_name,字符串类型,用于设置目标地址从 header_name 对应的 header 中获取,默认值为 host。 Host { \u0026quot;address\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;hostname\u0026quot;:\u0026quot;\u0026quot; \u0026quot;weight\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;metadata\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;tls_disable\u0026quot;:\u0026quot;\u0026quot; } address,字符串类型,用于设置集群的地址和端口。 hostname,字符串类型,对应地址的名称,可以是一个域名。 weight,uint32 类型,对应地址的权重。 metadata,*MetadataConfig 类型,用于设置对应机器的 metadata 信息,通常和 LBSubsetConfig 一起使用。 tls_disable,bool 类型,用于标记对应机器是否开启 TLS。 DnsResolverConfig { \u0026quot;servers\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;search\u0026quot;:\u0026quot;\u0026quot; \u0026quot;port\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;ndots\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;timeout\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;attempts\u0026quot;:\u0026quot;\u0026quot; } servers,[]string 类型,用于设置 DNS 服务器列表。 search,[]string 类型,用于设置和目标域名拼接的后缀列表,结合 ndots 使用。 port,字符串类型,设置发起 DNS 请求的端口,默认值为 53。 ndots,int 类型,用于设置 DNS 查询域名是否将 search 中的列表依次追加到待查询域名末尾,如果该值大于待查询域名中的 “.” 数量,则将待查询域名末尾拼依次接 search 中设置的后缀,默认值为 0。 timeout,int 类型,用于设置 DNS 更新超时时间,单位为秒。 attempts,int 类型,一次 DNS 请求中尝试查询的 DSN server 的次数。 LbConfig { \u0026#34;choice_count\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;active_request_bias\u0026#34;: \u0026#34;\u0026#34; } choice_count,uint32类型,用来设置随机算法的随机选择次数,当配置了lbconfig时必填,且必须大于0 active_request_bias,float64类型,用来设置负载均衡算法对active_request和connection_active指标的偏好 ","excerpt":"本文是关于 MOSN ClusterManager 配置的说明。\nMOSN 中通过 cluster_manager 来管理转发的集群地址,通常与 Router 配合使用。 …","ref":"https://mosn.io/docs/products/configuration-overview/clustermanager/","title":"ClusterManager 配置"},{"body":"一、前言 MOSN 为开发者提供了灵活的变量机制,用户通过变量机制能够实现以下目的:\n 获取请求上下文的相关信息 在一次请求的生命周期内传递自定义数据 (跨 Filter 传递数据) 影响 MOSN 路由框架的运行结果 二、快速开始 假设我们现在正在开发一个 simpleFilter,该 Filter 处理 Http 请求,且需要实现以下功能:\n 判断请求的 Method,只接受Post请求 修改 Post请求 URL 中的 Query String 显然,通过请求的 Header、Body 和 Trailer是无法获取请求的Method信息,也无法修改请求的 URL,但我们可以通过变量机制实现以上功能:\nfunc (f *simpleFilter) OnReceive(ctx context.Context, headers api.HeaderMap, buf buffer.IoBuffer, trailers api.HeaderMap) api.StreamFilterStatus { // 1. 通过变量机制获取请求 Method method, err := variable.GetString(ctx, types.VarMethod) if err != nil { return api.StreamFilterStop } // 2. 拦截非 Post 请求 if method != fasthttp.MethodPost { return api.StreamFilterStop } // 3. 修改请求 URL 中的 Query String variable.SetString(ctx, types.VarQueryString, \u0026#34;hello=world\u0026amp;foo=bar\u0026#34;) return api.StreamFilterContinue } 从上述简单例子可见,MOSN 变量机制允许开发者灵活地获取请求上下文的相关信息,并按需修改请求的内容。当然,变量机制的功能远不止上述 Filter 展现的能力,下文将详细介绍变量机制的方方面面。\n三、现有变量 上文的例子展示了如何通过变量机制获取请求的 Method信息。除此之外,MOSN 还提供了大量变量,用于提供请求的上下文信息。\n下面的表格中,变量名一列方便开发者在编写代码的过程中获取变量内容;字符串值一列则方便在配置文件中获取变量值 (例如配置 access_log时使用 %bytes_received% 获取变量内容)。\n需要注意的是:\n 大部分变量通常具有只读属性,若不清楚后续影响,请不要试图修改变量值 MOSN 的变量机制支持任意类型 interface{}的变量,但目前提供的绝大多数变量都是 string 类型的,下文如无特殊备注,均为 string 类型。 3.1 通用变量 变量名 字符串值 含义 备注 VarStartTime \u0026ldquo;start_time\u0026rdquo; 请求开始时间,七层 stream 开始创建的时间 格式:\u0026ldquo;2006/01/02 15:04:05.000\u0026rdquo;。 VarRequestReceivedDuration \u0026ldquo;request_received_duration\u0026rdquo; 接收 downstream 请求的耗时,从请求开始,到开始向 upstream 转发请求为止的耗时 VarResponseReceivedDuration \u0026ldquo;response_received_duration\u0026rdquo; 接收 upstream 响应的耗时,从请求开始,到收到 upstream 响应为止的耗时 VarRequestFinishedDuration \u0026ldquo;request_finished_duration\u0026rdquo; 请求耗时,从请求开始,到请求结束为止的耗时 VarBytesSent \u0026ldquo;bytes_sent\u0026rdquo; 发出的字节数 VarBytesReceived \u0026ldquo;bytes_received\u0026rdquo; 收到的字节数 VarProtocol \u0026ldquo;protocol\u0026rdquo; 请求的具体协议 VarResponseCode \u0026ldquo;response_code\u0026rdquo; 响应码 VarDuration \u0026ldquo;duration\u0026rdquo; 请求开始至今的耗时 VarResponseFlag \u0026ldquo;response_flag\u0026rdquo; 响应 flag VarResponseFlags \u0026ldquo;response_flags\u0026rdquo; 响应 flags \u0026ldquo;response_flag\u0026rdquo; 和 \u0026ldquo;response_flags\u0026rdquo; 两个变量的内容完全一致,后者 \u0026ldquo;flags\u0026rdquo; 是为了与 istio 日志格式保持一致,参考 VarUpstreamLocalAddress \u0026ldquo;upstream_local_address\u0026rdquo; upstream 本端地址 VarDownstreamLocalAddress \u0026ldquo;downstream_local_address\u0026rdquo; downstream 本端地址 VarDownstreamRemoteAddress \u0026ldquo;downstream_remote_address\u0026rdquo; downstream 远端地址 VarUpstreamHost \u0026ldquo;upstream_host\u0026rdquo; upstream 主机名 VarUpstreamTransportFailureReason \u0026ldquo;upstream_transport_failure_reason\u0026rdquo; upstream 传输失败原因 VarUpstreamCluster \u0026ldquo;upstream_cluster\u0026rdquo; upstream 集群名 VarProtocolConfig \u0026ldquo;protocol_config\u0026rdquo; 请求的协议配置 VarProxyTryTimeout \u0026ldquo;proxy_try_timeout\u0026rdquo; 每次 upstream 请求的超时时间 每个 upstream 请求的超时时间 (包含正常请求和重试请求) VarProxyGlobalTimeout \u0026ldquo;proxy_global_timeout\u0026rdquo; 所有 upstream 请求的总超时时间 所有 upstream 请求的总超时 (包含正常请求和重试请求) VarProxyHijackStatus \u0026ldquo;proxy_hijack_status\u0026rdquo; hijack 状态 VarProxyGzipSwitch \u0026ldquo;proxy_gzip_switch\u0026rdquo; gzip 开关 用于 pkg/filter/stream/gzip VarProxyIsDirectResponse \u0026ldquo;proxy_direct_response\u0026rdquo; 是否是 direct response VarProxyDisableRetry \u0026ldquo;proxy_disable_retry\u0026rdquo; 是否不允许请求重试 VarDirection \u0026ldquo;x-mosn-direction\u0026rdquo; 流向 VarScheme \u0026ldquo;x-mosn-scheme\u0026rdquo; 请求 Scheme VarHost \u0026ldquo;x-mosn-host\u0026rdquo; 请求 Host VarPath \u0026ldquo;x-mosn-path\u0026rdquo; 请求 Path 已转义,human-readable VarPathOriginal \u0026ldquo;x-mosn-path-original\u0026rdquo; 请求原始 Path VarQueryString \u0026ldquo;x-mosn-querystring\u0026rdquo; 请求 Query VarMethod \u0026ldquo;x-mosn-method\u0026rdquo; 请求 Method VarIstioHeaderHost \u0026ldquo;authority\u0026rdquo; 请求 Host 内容同 VarHost,与 istio 保持一致 VarHeaderStatus \u0026ldquo;x-mosn-status\u0026rdquo; 状态码 VarHeaderRPCService \u0026ldquo;x-mosn-rpc-service\u0026rdquo; RPC Service VarHeaderRPCMethod \u0026ldquo;x-mosn-rpc-method\u0026rdquo; RPC Method VarRouterMeta \u0026ldquo;x-mosn-router-meta\u0026rdquo; 路由 meta 3.2 Http1 协议变量 变量名 字符串值 含义 备注 VarHttpRequestScheme \u0026ldquo;Http1_request_scheme\u0026rdquo; 请求 Scheme VarHttpRequestMethod \u0026ldquo;Http1_request_method\u0026rdquo; 请求 Method VarHttpRequestLength \u0026ldquo;Http1_request_length\u0026rdquo; 请求长度 VarHttpRequestUri \u0026ldquo;Http1_request_uri\u0026rdquo; 请求 URI VarHttpRequestPath \u0026ldquo;Http1_request_path\u0026rdquo; 请求 Path VarHttpRequestPathOriginal \u0026ldquo;Http1_request_path_original\u0026rdquo; 请求原始 Path VarHttpRequestArg \u0026ldquo;Http1_request_arg\u0026rdquo; 请求 Query VarPrefixHttpHeader \u0026ldquo;Http1_request_header_\u0026rdquo; 获取请求 Header 值 前缀变量,需 + \u0026ldquo;headerName\u0026rdquo; 获取对应 header 的值 VarPrefixHttpArg \u0026ldquo;Http1_request_arg_\u0026rdquo; 获取请求 Query 值 前缀变量,需 + \u0026ldquo;QueryName\u0026rdquo; 获取对应 query 的值 VarPrefixHttpCookie \u0026ldquo;Http1_cookie_\u0026rdquo; 获取请求 Cookie 值 前缀变量,需 + \u0026ldquo;CookieName\u0026rdquo; 获取对应 cookie 的值 3.3 Http2 协议变量 变量名 字符串值 含义 备注 VarHttp2RequestScheme \u0026ldquo;Http2_request_scheme\u0026rdquo; 请求 Scheme VarHttp2RequestMethod \u0026ldquo;Http2_request_method\u0026rdquo; 请求 Method 未实现 VarHttp2RequestLength \u0026ldquo;Http2_request_length\u0026rdquo; 请求长度 未实现 VarHttp2RequestUri \u0026ldquo;Http2_request_uri\u0026rdquo; 请求 URI 未实现 VarHttp2RequestPath \u0026ldquo;Http2_request_path\u0026rdquo; 请求 Path 未实现 VarHttp2RequestPathOriginal \u0026ldquo;Http2_request_path_original\u0026rdquo; 请求原始 Path 未实现 VarHttp2RequestArg \u0026ldquo;Http2_request_arg\u0026rdquo; 请求 Query 未实现 VarHttp2RequestUseStream \u0026ldquo;Http2_request_use_stream\u0026rdquo; 请求是否为流式 VarHttp2ResponseUseStream \u0026ldquo;Http2_response_use_stream\u0026rdquo; 响应是否为流式 VarPrefixHttp2Header \u0026ldquo;Http2_request_header_\u0026rdquo; 获取请求 Header 值 前缀变量,需 + \u0026ldquo;headerName\u0026rdquo; 获取对应 header 的值 VarPrefixHttp2Arg \u0026ldquo;Http2_request_arg_\u0026rdquo; 获取请求 Query 值 未实现 VarPrefixHttp2Cookie \u0026ldquo;Http2_cookie_\u0026rdquo; 获取请求 Cookie 值 前缀变量,需 + \u0026ldquo;CookieName\u0026rdquo; 获取对应 cookie 的值 3.4 其它变量 MOSN 还包含一些特殊模块的变量,对于这些变量,基本原则是:如果你不知道要用这些变量,那就不要用\n 变量名 字符串值 含义 备注 grpc.VarGrpcServiceName \u0026ldquo;gRPC_serviceName\u0026rdquo; GRPC 请求 Service grpc.VarGrpcRequestResult \u0026ldquo;requestResult\u0026rdquo; GRPC 请求结果 dubbo.VarDubboRequestService \u0026ldquo;Dubbo_service\u0026rdquo; Dubbo 请求 Service dubbo.VarDubboRequestMethod \u0026ldquo;Dubbo_method\u0026rdquo; Dubbo 请求方法 seata.XID \u0026ldquo;x_seata_xid\u0026rdquo; seata.BranchID \u0026ldquo;x_seata_branch_id\u0026rdquo; 四、增加自定义变量 上文罗列了 MOSN 中已经预定义的所有变量,基本能够满足 MOSN 路由转发场景的需求,若仍有未能满足的定制化需求,可以考虑通过自定义变量的方式来实现。\n一个典型的使用场景是跨 Filter 传递数据,即 Filter1 处理完请求后希望将自定义数据传递给后续 Filter2、3 处理。实现这一需求的方法有以下几种:\n 方案一:使用请求 Header 携带自定义数据 方案二:使用请求 ctx 携带自定义数据 方案一的缺点在于会污染用户的原始请求,Filter1塞进 Header 的数据需要在后续 Filter中清理掉,否则就是被带到上游服务端。此外,受限于 Header 值的类型是 string,方案一无法携带非 string 类型的数据。\n方案二的缺点在于性能,ctx 的底层实现是单链表结构,每使用 context.WithValue添加一个数据时均会将单链表延长一节,且从 ctx 获取数据时,需要遍历单链表,时间复杂度为 O(n)\nMOSN 提供的变量机制即是解决上述问题的推荐方案。使用变量机制传递自定义数据具有以下优点:\n 数据类型无限制:除了最基础的 string 类型外,支持 interface{} 类型的自定义数据 性能优异:读写自定义变量的时间复杂度均为 O(1) 无侵入性:不侵入用户原始请求、不侵入其它 Filter MOSN 变量框架提供以下 API 来操作变量: 本节所有 API 均位于 mosn.io/mosn/pkg/variable 包\n4.1 注册变量 // 注册新变量 // 需要在 init 函数中调用,否则可能不生效 func Register(variable Variable) error // 注册前缀变量 // 需要在 init 函数中调用,否则可能不生效 func RegisterPrefix(prefix string, variable Variable) error // 检查是否已注册某变量 func Check(name string) (Variable, error) 4.2 创建变量 Variable 类型的变量通过以下 API 创建:\n// 新建 string 类型变量 func NewStringVariable(name string, data interface{}, getter StringGetterFunc, setter StringSetterFunc, flags uint32) Variable // 新建 interface{} 类型变量 func NewVariable(name string, data interface{}, getter GetterFunc, setter SetterFunc, flags uint32) Variable 对于绝大多数 (99.99%) 使用场景而言,并不需要去理解上述两个 API 的所有入参的含义,MOSN 变量框架提供了默认实现,直接 Ctrl-CV 即可:\n// 方式一: // 创建并注册新变量:Hello_Variable_1 variable.NewVariable(\u0026#34;Hello_Variable_1\u0026#34;, nil, nil, variable.DefaultSetter, 0) // 方式二: // 创建并注册新变量:Hello_Variable_2 variable.NewVariable(\u0026#34;Hello_Variable_2\u0026#34;, nil, valueGetter, nil, 0) 上述两种方式的主要区别在于新创建的变量是否有初始值:\n 方式一创建的变量没有初始值,需要先调用 Set方法设置内容后,Get方法才能获取到 方式二创建的变量有初始值,初始值由自定义函数 valueGetter提供,无需先 Set便可获取到内容 方式一通常更符合自定义变量的使用场景,即自定义变量需要先 Set后 Get\n方式二则是 MOSN 框架自身常用的用法,例如:对于 \u0026quot;Http1_request_method\u0026quot;这个变量而言,① 它是有初始值的,无需先Set;② 它的获取方式较为复杂,需要用底层 Http 请求的结构体中获取,因此在 MOSN 中,该变量的创建方式为:\n variable.NewStringVariable(types.VarHttpRequestMethod, nil, requestMethodGetter, nil, 0)\n 其中 requestMethodGetter便是从 Http 请求的底层结构体获取 Method 值。\n4.3 使用变量 一旦注册好变量,便可通过以下 API 来读写变量:\n// 获取 string 类型变量值 func GetString(ctx context.Context, name string) (string, error) // 设置 string 类型变量值 func SetString(ctx context.Context, name, value string) error // 获取 interface{} 类型变量值,string 类型变量也可通过该 API 获取,Set 同理 func Get(ctx context.Context, name string) (interface{}, error) // 设置 interface{} 类型变量值 func Set(ctx context.Context, name string, value interface{}) error // 创建 VariableContext // 需要注意的是,使用变量机制时,ctx 必须是以下函数创建的 VariableContext // MOSN 框架中的 ctx 已经默认是 VariableContext,无需额外调用该函数。但在单测场景,需要使用该函数创建 VariableContext,否则无法正常使用变量 func NewVariableContext(ctx context.Context) context.Context 五、通过变量机制与 MOSN 路由框架进行交互 变量机制除了上文所展现的 “数据搬运工” 的身份外,还具备影响 MOSN 核心路由框架的能力。MOSN 核心路由框架本身会在路由过程中读取某些变量来进行路由决策,因此,开发者可以直接通过变量机制来影响 MOSN 路由的结果。MOSN 变量机制支持以下能力:\n5.1 通过变量修改路由元信息 变量名 含义 详细解释 VarProxyTryTimeout 每次 upstream 请求的超时时间 用于设置每个 upstream 请求的超时时间 (单个正常和重试请求的超时) VarProxyGlobalTimeout 所有 upstream 请求的总超时时间 用于设置所有 upstream 请求的总超时 (正常 + 重试请求的总超时) VarProxyDisableRetry 是否不允许请求重试 upstream 请求失败时,是否允许重试 VarRouterMeta 路由 meta 该变量设置的 metadata 后续会被用于 MOSN 路由时选择与之匹配的 Host。典型应用场景是 header_to_metadata 这个 Filter,它将请求 Header 中的数据作为 metadata 塞入变量中,用于后续的 MOSN 路由,使用者便可直接通过 Header 值来控制路由结果。 5.2 通过变量指定路由集群 用户可以在 mosn.json 配置中指定用于匹配路由集群的变量名,例如:\n{ \u0026quot;routers\u0026quot;: [{ \u0026quot;router_config_name\u0026quot;: \u0026quot;hello_mosn\u0026quot;, \u0026quot;virtual_hosts\u0026quot;: [{ \u0026quot;name\u0026quot;: \u0026quot;mosnHost\u0026quot;, \u0026quot;domains\u0026quot;: [\u0026quot;*\u0026quot;], \u0026quot;routers\u0026quot;: [{ \u0026quot;match\u0026quot;: {\u0026quot;prefix\u0026quot;: \u0026quot;/\u0026quot;}, \u0026quot;route\u0026quot;: {\u0026quot;cluster_variable\u0026quot;: \u0026quot;VAR_NAME\u0026quot;} }] }] }] } 在上述配置下,MOSN 在路由时会获取变量 VAR_NAME的值,并将请求路由到该变量值对应的 Cluster 上:用户可以在路由前 (BeforeRoute) 的 Filter 中,通过以下代码让请求被路由到 dst_cluster这个集群上:\n variable.SetString(ctx, VAR_NAME, \u0026ldquo;dst_cluster\u0026rdquo;)\n 5.3 通过变量影响路由匹配结果 用于可以在 mosn.json 配置中指定用于匹配路由的变量名,例如:\n{ \u0026#34;routers\u0026#34;: [{ \u0026#34;router_config_name\u0026#34;: \u0026#34;hello_mosn\u0026#34;, \u0026#34;virtual_hosts\u0026#34;: [{ \u0026#34;name\u0026#34;: \u0026#34;mosnHost\u0026#34;, \u0026#34;domains\u0026#34;: [\u0026#34;*\u0026#34;], \u0026#34;routers\u0026#34;: [{ \u0026#34;match\u0026#34;: { \u0026#34;variables\u0026#34;: [{ \u0026#34;name\u0026#34;: \u0026#34;VAR_NAME\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;VAR_VALUE\u0026#34; }] }, \u0026#34;route\u0026#34;: {\u0026#34;cluster_name\u0026#34;: \u0026#34;dst_cluster\u0026#34;} }] }] }] } 在上述配置下,MOSN 在路由时,会将具有变量 VAR_NAME且变量值为 VAR_VALUE的请求转发到 dst_cluster这个集群上\n六、总结 综上,变量机制是 MOSN 提供高可扩展性的重要一环,本文对 MOSN 中的变量机制进行了详细介绍,由浅及深,从介绍现有变量开始,到增加自定义变量,再到使用变量与 MOSN 核心路由框架进行交互,供用户一窥变量机制的全貌。\n","excerpt":"一、前言 MOSN 为开发者提供了灵活的变量机制,用户通过变量机制能够实现以下目的:\n 获取请求上下文的相关信息 在一次请求的生命周期内传递自定义数据 (跨 Filter 传递数据) 影响 MOSN …","ref":"https://mosn.io/docs/developer-guide/variable/","title":"MOSN 变量机制"},{"body":"router 用于描述 MOSN 的路由配置,通常与 proxy 配合使用。\n{ \u0026quot;router_config_name\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;virtual_hosts\u0026quot;: [ ] } router_config_name,唯一的路由配置标识,与 proxy 中配置的字段对应。 virtual_hosts,描述具体的路由规则细节。 VirtualHost { \u0026#34;name\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;domains\u0026#34;:[], \u0026#34;routers\u0026#34;:[] } name,字符串。用作 virtual host 的唯一标识。 domains,字符串数组。表示一组可以匹配到该 virtual host 的 domain,支持配置通配符。domain 的匹配优先级如下: 首先匹配精确的,如 www.foo.com。 其次匹配最长后缀的通配符,如 *.foo.com、*-bar.foo.com,其中如果一个 domain 是 foo-bar.foo.com,那么会优先匹配 *-bar.foo.com。 最后匹配任意domain的通配符 * 。 routers,一组具体的路由匹配规则。 Router { \u0026#34;match\u0026#34;:{}, \u0026#34;route\u0026#34;:{}, \u0026#34;redirect\u0026#34;:{}, \u0026#34;direct_response\u0026#34;:{}, \u0026#34;per_filter_config\u0026#34;:{} } match,路由的匹配参数。 route,路由行为,描述请求将被路由的 upstream 信息。 redirect,路由行为,直接转发。 direct_response, 路由行为,直接响应。 per_filter_config,是一个 key: json 格式的 json。 其中 key 需要匹配一个 stream filter 的 type,key 对应的 json 是该 stream filter 的 config。 当配置了该字段时,对于某些 stream filter(依赖具体 filter 的实现),可以使用该字段表示的配置覆盖原有 stream filter 的配置,以此做到路由匹配级别的 stream filter 配置。 match { \u0026#34;prefix\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;path\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;regex\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;headers\u0026#34;: [], \u0026#34;variables\u0026#34;: [], \u0026#34;dsl_expressions\u0026#34;: [] } 路径(path)匹配 prefix,表示路由会匹配 path 的前缀,该配置的优先级高于 path 和 regex。 如果 prefix 被配置,那么请求首先要满足 path 的前缀与 prefix 配置相符合。 path,表示路由会匹配精确的 path,该配置的优先级高于 regex。如果 path被配置,那么请求首先要满足 path 与 path 配置相符合。 regex,表示路由会按照正则匹配的方式匹配 path。如果 regex 被配置,那么请求首先要满足 path 与 regex 配置相符合。 路径匹配配置同时存在时,只有高优先级的配置会生效。 Header 匹配 headers,表示一组请求需要匹配的 header。请求需要满足配置中所有的 Header 配置条件才算匹配成功。 Variable 匹配 variables,表示一组请求需要匹配的 variable,请求需要满足配置中所有的 variable 配置条件才算匹配成功。 DSL 匹配 dsl_expressions,表示一组请求需要匹配的 dsl,请求满足配置条件才算匹配成功。 header { \u0026#34;name\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;value\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;regex\u0026#34;:\u0026#34;\u0026#34; } name,表示 header 的 key。 value,表示 header 对应 key 的 value。 regex,bool 类型,如果为 true,表示 value 支持按照正则表达式的方式进行匹配。 variable { \u0026#34;name\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;value\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;regex\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;model\u0026#34;:\u0026#34;\u0026#34; } name,表示 variable 的 key。 value,表示 variable 对应 key 的 value。 regex,表示按照正则表达式的方式进行匹配。 model,可配置 \u0026ldquo;and\u0026rdquo; 和 “or”。 route { \u0026quot;cluster_name\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;cluster_variable\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;metadata_match\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;timeout\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;retry_policy\u0026quot;:{}, \u0026quot;hash_policy\u0026quot;:{}, \u0026quot;prefix_rewrite\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;regex_rewrite\u0026quot;:{}, \u0026quot;host_rewrite\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;request_headers_to_add\u0026quot;:[], \u0026quot;request_headers_to_remove\u0026quot;:[], \u0026quot;response_headers_to_add\u0026quot;:[], \u0026quot;response_headers_to_remove\u0026quot;:[] } 满足match之后的路由策略。\n cluster_name,表示请求将路由到的 upstream cluster。 cluster_variable,表示请求将路由到的变量指定的 upstream cluster,可动态设置变量路由到不同的后端。 metadata_match,metadata,如果配置了该字段,表示该路由会基于该 metadata 去匹配 upstream cluster 的 subset 。 timeout,Duration String,表示默认情况下请求转发的超时时间。如果请求中明确指定了超时时间,那么这个配置会被忽略。 retry_policy,重试配置,表示如果请求在遇到了特定的错误时采取的重试策略,默认没有配置的情况下,表示没有重试。 hash_policy,一致性Hash负载均衡算法使用的hash key。 prefix_rewrite regex_rewrite host_rewrite,修改请求的 path 和 host。 request_headers_to_add request_headers_to_remove,表示增加或者删除请求的 header。 response_headers_to_add response_headers_to_remove,表示增加或者删除响应的 header。 redirect { \u0026#34;response_code\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;path_redirect\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;host_redirect\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;scheme_redirect\u0026#34;:\u0026#34;\u0026#34; } 满足 match 条件之后,对请求进行跳转。\n response_code,跳转的 HTTP code,默认为 301。 path_redirect,修改跳转的 path。 host_redirect,修改跳转的 host。 scheme_redirect,修改跳转的 scheme。 direct_response { \u0026#34;status\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;body\u0026#34;:\u0026#34;\u0026#34; } 直接回复响应, status是状态码,body是内容。\nretry_policy { \u0026#34;retry_on\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;retry_timeout\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;num_retries\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;status_codes\u0026#34;:[] } retry_on,bool 类型,表示是否开启重试。 retry_timeout,Duration String,表示每次重试的超时时间。当 retry_timeout 大于 route 配置的 timeout 或者请求明确指定的 timeout 时,属于无效配置。 num_retries,表示最大的重试次数。 status_codes,重试状态码,配置后仅列表中的状态码会触发重试。 例子 默认匹配规则 所有请求转发到名字为 backend 的 cluster 集群。\n\u0026quot;routers\u0026quot;: [ { \u0026quot;match\u0026quot;:{ \u0026quot;prefix\u0026quot;:\u0026quot;/\u0026quot; }, \u0026quot;route\u0026quot;: { \u0026quot;cluster_name\u0026quot;: \u0026quot;backend\u0026quot; } } ] 匹配规则 - 路径 请求 /index.html 转发到名字为 backend 的 cluster 集群。\n\u0026quot;routers\u0026quot;: [ { \u0026quot;match\u0026quot;:{ \u0026quot;path\u0026quot;:\u0026quot;/index.html\u0026quot; }, \u0026quot;route\u0026quot;: { \u0026quot;cluster_name\u0026quot;: \u0026quot;backend\u0026quot; } } ] 匹配规则 - 正则 数字开头的请求转发到名字为 backend 的 cluster 集群。\n\u0026quot;routers\u0026quot;: [ { \u0026quot;match\u0026quot;:{ \u0026quot;regex\u0026quot;:\u0026quot;^/\\\\d+\u0026quot; }, \u0026quot;route\u0026quot;: { \u0026quot;cluster_name\u0026quot;: \u0026quot;backend\u0026quot; } } ] 匹配规则 - header 包含 a:b header的请求转发到名字为 backend 的 cluster 集群。\n\u0026quot;routers\u0026quot;: [ { \u0026quot;match\u0026quot;:{ \u0026quot;headers\u0026quot;: [{ \u0026quot;name\u0026quot;:\u0026quot;a\u0026quot;, \u0026quot;value\u0026quot;:\u0026quot;b\u0026quot;, \u0026quot;regex\u0026quot;:false }] }, \u0026quot;route\u0026quot;: { \u0026quot;cluster_name\u0026quot;: \u0026quot;backend\u0026quot; } } ] 匹配规则 - 变量 可以通过 filter 设置新的变量,以及 MOSN 内置的变量,进行路由转发规则。\n如下例子变量 x-mosn-path( MOSN 内置变量,表示请求的 path) 等于 /b 满足匹配。\n\u0026quot;routers\u0026quot;: [ { \u0026quot;match\u0026quot;:{ \u0026quot;variables\u0026quot;: [{ \u0026quot;name\u0026quot;:\u0026quot;x-mosn-path\u0026quot;, \u0026quot;value\u0026quot;:\u0026quot;/b\u0026quot; }] }, \u0026quot;route\u0026quot;: { \u0026quot;cluster_name\u0026quot;: \u0026quot;backend\u0026quot; } } ] 匹配行为 - 修改path 下例把请求的 path 修改为 /abc\n\u0026quot;routers\u0026quot;: [ { \u0026quot;match\u0026quot;:{ \u0026quot;prefix\u0026quot;: \u0026quot;/\u0026quot; }, \u0026quot;route\u0026quot;:{ \u0026quot;cluster_name\u0026quot;: \u0026quot;backend\u0026quot;, \u0026quot;prefix_rewrite\u0026quot;: \u0026quot;/abc\u0026quot; } } ] 匹配行为 - 添加删除 header 下例在转发给后端之前,新增test:ok ,删除hello.\n\u0026quot;routers\u0026quot;: [ { \u0026quot;match\u0026quot;:{ \u0026quot;prefix\u0026quot;: \u0026quot;/\u0026quot; }, \u0026quot;route\u0026quot;:{ \u0026quot;cluster_name\u0026quot;: \u0026quot;backend\u0026quot;, \u0026quot;request_headers_to_add\u0026quot;: [ { \u0026quot;header\u0026quot;: { \u0026quot;key\u0026quot;: \u0026quot;test\u0026quot;, \u0026quot;value\u0026quot;: \u0026quot;ok\u0026quot; } } ], \u0026quot;request_headers_to_remove\u0026quot;:[\u0026quot;hello\u0026quot;] } } ] 匹配行为 - redirect 除了转发到 cluster 之外,也支持 redirect 的匹配动作。\n下例将 301 跳转,Location: http://test/b\n\u0026quot;routers\u0026quot;: [ { \u0026quot;match\u0026quot;:{ \u0026quot;prefix\u0026quot;: \u0026quot;/\u0026quot; }, \u0026quot;redirect\u0026quot;:{ \u0026quot;response_code\u0026quot;:301, \u0026quot;path_redirect\u0026quot;:\u0026quot;/b\u0026quot;, \u0026quot;host_redirect\u0026quot;:\u0026quot;test\u0026quot;, \u0026quot;scheme_redirect\u0026quot;:\u0026quot;http\u0026quot; } } ] 匹配行为 - 直接响应 满足匹配条件直接响应请求。\n\u0026quot;routers\u0026quot;: [ { \u0026quot;match\u0026quot;:{ \u0026quot;prefix\u0026quot;: \u0026quot;/\u0026quot; }, \u0026quot;direct_response\u0026quot;:{ \u0026quot;status\u0026quot;:404, \u0026quot;body\u0026quot;:\u0026quot;no found\u0026quot; } } ] 高级技巧 MOSN 路由框架详解\n","excerpt":"router 用于描述 MOSN 的路由配置,通常与 proxy 配合使用。\n{ \u0026quot;router_config_name\u0026quot;:\u0026quot;\u0026quot;, …","ref":"https://mosn.io/docs/products/configuration-overview/server/router/","title":"Router 配置"},{"body":" MOSN 从 v1.0.0 版本开始 已通过 Istio 1.10.6 的 Bookinfo 测试,关于最新版 Istio 的支持情况可关注 MOSN Istio WG。\n 本文介绍的内容将包括 :\n MOSN 与 Istio 的关系 部署 Istio 与 MOSN Bookinfo 实验 MOSN 与 Istio 的关系 我们曾在 MOSN 介绍 中介绍过,MOSN 是一款采用 Go 语言开发的 Service Mesh 数据平面代理。\n下图是 Istio 整体框架下,MOSN 的工作示意图。\n 部署 Istio 与 MOSN 注意:Istio 1.10.6 不支持在 arm64 上运行 Kubernetes 的集群 。 Istio Issues\n安装 kubectl 命令行工具 kubectl 是用于针对 Kubernetes 集群运行命令的命令行接口,安装参考 kubectl doc。\n安装 Kubernetes 平台 安装 Istio,首先需要根据实际需求选择安装平台,可参考 Istio 官方文档推荐的方式 Platform Setup。 后文中,我们假定选择的是minikube的安装方式,方便进行介绍。\n安装minikube 流程:\n1、根据本机环境选择下载地址 Minikube 官网,下面用的系统是macOS x86系统。\n$ curl -LO https://storage.googleapis.com/minikube/releases/v1.22.0/minikube-darwin-amd64 $ sudo install minikube-darwin-amd64 /usr/local/bin/minikube 2、启动 minikube\n$ minikube start --memory=7851 --cpus=4 --image-mirror-country=\u0026#39;cn\u0026#39; 注意:内存必须大于4GB,且镜像为cn(国内)\n3、安装成功后,查看Pod情况\n$ minikube kubectl -- get pods -A NAMESPACE NAME READY STATUS RESTARTS AGE kube-system coredns-64897985d-vw7b8 1/1 Running 2 (16h ago) 7d20h kube-system etcd-minikube 1/1 Running 2 (16h ago) 7d20h kube-system kube-apiserver-minikube 1/1 Running 2 (16h ago) 7d20h kube-system kube-controller-manager-minikube 1/1 Running 2 (16h ago) 7d20h kube-system kube-proxy-cmjcq 1/1 Running 2 (16h ago) 7d20h kube-system kube-scheduler-minikube 1/1 Running 2 (16h ago) 7d20h kube-system storage-provisioner 1/1 Running 5 (16h ago) 7d20h 安装 Istio,使用 MOSN 作为数据面 1、下载对应的 Istio Release 版本,可以在 Istio release 页面下载与您操作系统匹配的压缩文件,或者使用官方提供的下载方式\nVERSION=1.10.6 # istio version export ISTIO_VERSION=$VERSION \u0026amp;\u0026amp; curl -L https://istio.io/downloadIstio | sh - 2、下载完成以后(或者解压完成),切换到对应的目录,同时可以设置对应的 istioctl 命令行工具到环境变量,方便配置自定义 Istio 控制平面和数据平面配置参数。\n$ cd istio-$ISTIO_VERSION/ $ export PATH=$PATH:$(pwd)/bin 3、执行默认安装 istioctl install\n$ istioctl install This will install the Istio 1.14.1 default profile with [\u0026#34;Istio core\u0026#34; \u0026#34;Istiod\u0026#34; \u0026#34;Ingress gateways\u0026#34;] components into the cluster. Proceed? (y/N) y ✔ Istio core installed ✔ Istiod installed ✔ Ingress gateways installed ✔ Installation complete Making this installation the default for injection and validation. Thank you for installing Istio 1.10. Please take a few minutes to tell us about your install/upgrade experience! https://forms.gle/KjkrDnMPByq7akrYA 4、创建 istio 命名空间,并且设置 MOSN proxyv2 镜像为数据面镜像\n下载 MOSN proxyv2 的镜像,并设置其为 Istio 的 proxy 镜像。 --set .values.global.proxy.image=${MOSN IMAGE} 也可以通过手动去创建 proxy 镜像 (MOSN 与 Istio 的 proxyv2 镜像 build 方法介绍)。 以下将使用我们提供的镜像版本 mosnio/proxyv2:v1.0.0-1.10.6\n$ kubectl create namespace istio-system $ istioctl manifest apply --set .values.global.proxy.image=mosnio/proxyv2:v1.0.0-1.10.6 --set meshConfig.defaultConfig.binaryPath=\u0026#34;/usr/local/bin/mosn\u0026#34; 注意:当你失败时,可以通过 minikube ssh 进入虚机所构建的集群内部,并通过 docker pull mosnio/proxyv2:v1.0.0-1.10.6 来获取镜像\n5、验证 Istio 相关 POD 服务是否部署成功\n$ kubectl get pod -n istio-system NAME READY STATUS RESTARTS AGE istio-ingressgateway-6b7fb88874-rgmrj 1/1 Running 0 102s istiod-65c9767c55-vjppv 1/1 Running 0 109s 如果 pod 显示所有容器 READY,并且 STATUS 为 Running,则表示 Istio 安装成功\nBookinfo 实验 MOSN 已经通过 Istio 1.10.6 的 Bookinfo 测试,可以通过 MOSN with Istio 的教程来进行 Bookinfo 示例的演示操作,另外在该教程中您也可以找到更多关于使用 MOSN 和 Istio 的说明。 更多的使用场景可以参考 Istio 官方 Example。 MOSN 目前并没有支持 Istio 的所有场景,如果您在运行实验过程中有遇到不支持的情况,请给我们提出 issue,欢迎贡献代码。\nBookinfo 介绍 Bookinfo 是一个类似豆瓣的图书应用,它包含四个基础服务:\n Product Page:主页,由 python 开发,展示所有图书信息,它会调用 Reviews 和 Details 服务 Reviews:评论,由 java 开发,展示图书评论,会调用 Ratings 服务 Ratings:评分服务,由 nodejs 开发 Details:图书详情,由 ruby 开发 部署 Bookinfo 应用并注入 MOSN 详细过程可以参考 Bookinfo doc\n 通过 kube-inject 的方式实现 Sidecar 注入:\nistioctl kube-inject -f samples/bookinfo/platform/kube/bookinfo.yaml \u0026gt; bookinfo.yaml # sed -i \u0026#39;\u0026#39; is the MacOS command, if you are in linux, use sed -i instead. sed -i \u0026#39;\u0026#39; \u0026#34;s/\\/usr\\/local\\/bin\\/envoy/\\/usr\\/local\\/bin\\/mosn/g\u0026#34; ./bookinfo.yaml 部署注入 Sidecar 后的 Bookinfo 应用:\n$ kubectl apply -f bookinfo.yaml 验证部署是否成功:\n$ kubectl get services NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE details ClusterIP 192.168.248.118 \u0026lt;none\u0026gt; 9080/TCP 5m7s kubernetes ClusterIP 192.168.0.1 \u0026lt;none\u0026gt; 443/TCP 25h productpage ClusterIP 192.168.204.255 \u0026lt;none\u0026gt; 9080/TCP 5m6s ratings ClusterIP 192.168.227.164 \u0026lt;none\u0026gt; 9080/TCP 5m7s reviews ClusterIP 192.168.181.16 \u0026lt;none\u0026gt; 9080/TCP 5m6s 等待所有的 pod 等成功运行起来:\n$ kubectl get pods NAME READY STATUS RESTARTS AGE details-v1-77497b4899-67gfn 2/2 Running 0 98s productpage-v1-68d9cf459d-mv7rh 2/2 Running 0 97s ratings-v1-65f97fc6c5-npcrz 2/2 Running 0 98s reviews-v1-6bf4444fcc-9cfrw 2/2 Running 0 97s reviews-v2-54d95c5444-5jtxp 2/2 Running 0 97s reviews-v3-dffc77d75-jd8cr 2/2 Running 0 97s 当上述状态为 Running 后,可通过如下方式确认 Bookinfo 应用是否正常运行:\nkubectl exec -it $(kubectl get pod -l app=ratings -o jsonpath=\u0026#39;{.items[0].metadata.name}\u0026#39;) -c ratings -- curl productpage:9080/productpage | grep -o \u0026#34;\u0026lt;title\u0026gt;.*\u0026lt;/title\u0026gt;\u0026#34; 访问 Bookinfo 服务 开启 gateway 模式。\n$ kubectl apply -f samples/bookinfo/networking/bookinfo-gateway.yaml $ kubectl get gateway NAME AGE bookinfo-gateway 6s 在后台运行ingress 网关,通过1234端口转发到80端口。参考文档\nkubectl port-forward -n istio-system --address 0.0.0.0 service/istio-ingressgateway 1234:80 \u0026gt;/dev/null 2\u0026gt;\u0026amp;1 \u0026amp; 验证 gateway 是否生效,输出 200 表示成功。\n$ curl -o /dev/null -s -w \u0026#34;%{http_code}\\n\u0026#34; http://127.0.0.1:1234/productpage 200 观察页面情况\n访问 http://$GATEWAY_URL/productpage (注意: $GATEWAY_URL 需要替换成你设置的地址,如:127.0.0.1:1234),正常的话通过刷新会看到如下所示 Bookinfo 的界面,其中 Book Reviews 有三个版本,刷新后依次会看到(可以查看 samples/bookinfo/platform/kube/bookinfo.yaml 中的配置发现为什么是这三个版本)版本一的界面。\n版本二的界面。\n版本三的界面。\n验证 MOSN 按 version 路由能力 首先为 Bookinfo 的 service 创建一系列的 destination rules。\n$ kubectl apply -f samples/bookinfo/networking/destination-rule-all.yaml 指定 reviews 服务只访问 v1 版本。\n$ kubectl apply -f samples/bookinfo/networking/virtual-service-all-v1.yaml 访问 http://$GATEWAY_URL/productpage 发现 reviews 固定在如下版本一的页面不再变化。\n验证 MOSN 按 weight 路由能力 我们通过下面操作将 v1 和 v3 版本各分配 50% 的流量。\n$ kubectl apply -f samples/bookinfo/networking/virtual-service-reviews-50-v3.yaml 访问 http://$GATEWAY_URL/productpage 这次 v1 和 v3 各有 1/2 几率出现。\n验证 MOSN 按照特定 header 路由能力 Bookinfo 系统右上角有一个登陆的入口,登陆以后请求会带上 end-user 这个自定义,值是 user name,Mosn 支持根据这个 header 的值来做路由。比如,我们尝试将 jason 这个用户路由到 v2 版本,其他的路由到 v1 版本(用户名和密码均是:jason,为什么是这个用户可以查看对应的 yaml 文件)。\n$ kubectl apply -f samples/bookinfo/networking/virtual-service-reviews-test-v2.yaml 访问 http://$GATEWAY_URL/productpage 时:\n以 jason 身份登陆,会看到 v2 版本。\n以其他身份登录,始终在 v1 版本。\n验证 MOSN 的故障注入功能 初始状态准备 运行以下命令来初始化 Bookinfo 应用程序版本路由信息:\nkubectl apply -f samples/bookinfo/networking/virtual-service-all-v1.yaml kubectl apply -f samples/bookinfo/networking/virtual-service-reviews-test-v2.yaml 经过上面的配置后,其请求链路如下所示:\nproductpage → reviews:v2 → ratings (针对 jason 用户) productpage → reviews:v1 (其他用户)\n注入延时故障 执行下面的命令将会为用户 jason 在 reviews:v2 和 ratings 服务之间注入一个 7 秒的延迟:\nkubectl apply -f samples/bookinfo/networking/virtual-service-ratings-test-delay.yaml 访问 http://$GATEWAY_URL/productpage\n此时发现使用 jason 这个用户登录访问会有延时, Reviews 部分显示了错误消息:\nError fetching product reviews! Sorry, product reviews are currently unavailable for this book.\n注入 abort 故障 执行下面的命令将会为用户 jason 访问 ratings 微服务时引入一个 HTTP abort:\nkubectl apply -f samples/bookinfo/networking/virtual-service-ratings-test-abort.yaml 访问 http://$GATEWAY_URL/productpage\n此时发现使用 jason 这个用户登录访问时, 此时 Book Reviews 中会显示如下错误消息:\nRatings service is currently unavailable\n卸载 Bookinfo 可以使用下面的命令来完成应用的删除和清理工作:\n删除路由规则,并销毁应用的 Pod。\n$ samples/bookinfo/platform/kube/cleanup.sh \u0026lt;\u0026lt;EOF Y; EOF 确认 Bookinfo 应用已经关停:\n$ kubectl get virtualservices #-- there should be no virtual services $ kubectl get destinationrules #-- there should be no destination rules $ kubectl get gateway #-- there should be no gateway $ kubectl get pods #-- the Bookinfo pods should be deleted 卸载 Istio 执行如下命令,删除 Istio 相关 CRD 以及 pod 等资源:\n$ istioctl manifest generate | kubectl delete -f - 确认 Istio 是否成功卸载:\n$ kubectl get namespace istio-system ","excerpt":"MOSN 从 v1.0.0 版本开始 已通过 Istio 1.10.6 的 Bookinfo 测试,关于最新版 Istio 的支持情况可关注 MOSN Istio WG。\n 本文介绍的内容将包括 : …","ref":"https://mosn.io/docs/user-guide/start/istio/","title":"MOSN 作为 Istio 的数据平面"},{"body":"本文的目的是分析 MOSN 的启动流程。基于 mosn 版本 v0.4.0,commit 为: dc35c8fc95435a47e6393db1c79dd9f60f7eb898\nMOSN 简介 MOSN 是一款使用 Go 语言开发的网络代理软件,作为云原生的网络数据平面,旨在为服务提供多协议,模块化,智能化,安全的代理能力。\nMOSN 在基于 Kubernetes 的 service mesh 中通常扮演数据平面的角色,它作为 sidecar 注入到集群的 Pod 中,接管在 Pod 之间的网络连接。\nMOSN 启动流程 我们先找到程序的入口,很多 go 的项目都将 程序入口写在 cmd 文件夹中,然后具体的实现写在 pkg 中。MOSN 项目也正好如此。在 cmd/mosn/main/mosn.go 中有我们要的 main 函数,提供了 start, stop 和 reload 命令。其中 stop 和 reload 还未做实现。\n//commands app.Commands = []cli.Command{ cmdStart, cmdStop, cmdReload, } 在 cmd/mosn/main/control.go 中,有 mosn start 执行的代码部分。mosn start 当前有5个参数:\n config, c: 提供配置文件的路径,默认值是 configs/mosn_config.json service-cluster, s: xdsclient 的初始化参数:服务集群名称,这里的集群是 MOSN 连接到的一组逻辑上相似的上游主机 service-node, n: xdsclient 的初始化参数:服务集群中的节点 service-meta, sm: xdsclient 的初始化参数:元数据 feature-gates, f: feature-gates 是 MOSN 的特性开关,当前有三个特性: XdsMtlsEnable, PayLoadLimitEnable 和 MultiTenantMode。 最终可以在 pkg/mosn/starter.go 中找到启动方法:\n// Start mosn project // step1. NewMosn // step2. Start Mosn func Start(c *v2.MOSNConfig) { //log.StartLogger.Infof(\u0026#34;[mosn] [start] start by config : %+v\u0026#34;, c) \tMosn := NewMosn(c) Mosn.Start() Mosn.wg.Wait() } 启动方法很简单,Mosn := NewMosn(c) 实例化了一个 Mosn 实例。Mosn.Start() 开始运行。 下面主要就 NewMosn(c) 和 Start() 方法做分析。\nMOSN 的初始化 在进入到具体初始化之前,我们先看 MOSN 的结构:\ntype Mosn struct { servers []server.Server clustermanager types.ClusterManager routerManager types.RouterManager config *v2.MOSNConfig adminServer admin.Server xdsClient *xds.Client wg sync.WaitGroup // for smooth upgrade. reconfigure \tinheritListeners []net.Listener reconfigure net.Conn } servers 是一个数组,server.Server 是接口类型。但是目前的代码逻辑中只会有一个 server。\nclustermanager 顾名思义就是集群管理器。 types.ClusterManager 也是接口类型。这里的 cluster 指得是 MOSN 连接到的一组逻辑上相似的上游主机。MOSN 通过服务发现来发现集群中的成员,并通过主动运行状况检查来确定集群成员的健康状况。MOSN 如何将请求路由到集群成员由负载均衡策略确定。\nrouterManager 是路由管理器,MOSN 根据路由规则来对请求进行代理。\nadminServer 是一个服务,可以通过 http 请求获取 MOSN 的配置、状态等等\nxdsClient 是 xds 协议的客户端。关于 xds, Envoy 通过查询文件或管理服务器来动态发现资源。概括地讲,对应的发现服务及其相应的 API 被称作 xDS。mosn 也使用 xDS,这样就可以兼容 istio。\ninheritListeners 和 reconfigure 都是为了实现 MOSN 的平滑升级和重启。\n这里我们也要注意到 inheritListeners 和 reconfigure 参数,MOSN 在启动时分为两种情况:\n 普通启动流程:这种情况下是 MOSN 作为 sidecar 第一次在 Pod 中启动,不需要考虑长连接转移等情况。 热升级/重启流程:在 MOSN 已经作为 sidecar 运行的情况下,如果此时要做 MOSN 的升级/重启,则必须要考虑当前 MOSN 上已有的长连接,如果直接断开连接重启,肯定会对业务有影响。所以 MOSN 在升级/重启时,会进行比较复杂的长连接转移的工作。 在了解以上内容的前提下,现在我们开始具体分析。\nNewMosn函数在63行左右,一上来就开始初始化各种配置。比如日志,进程id路径,unix socket 路径,trace的开关(SOFATracer)以及日志插件等等。这里的代码比较简单,就不具体分析每个方法了。\ninitializeDefaultPath(configmanager.GetConfigPath()) initializePidFile(c.Pid) initializeTracing(c.Tracing) initializePlugin(c.Plugin.LogBase) 下面大概在 70 行左右,主要开始做listener的转移。\n注意:为了贴出来的代码可以简单,我删除了一些错误处理和日志的语句。并在代码中添加了注释帮助理解\n//get inherit fds inheritListeners, reconfigure, err := server.GetInheritListeners() if reconfigure != nil { // set Mosn Active_Reconfiguring \tstore.SetMosnState(store.Active_Reconfiguring) // parse MOSNConfig again \tc = configmanager.Load(configmanager.GetConfigPath()) } else { // start init services \tif err := store.StartService(nil); err != nil { log.StartLogger.Fatalf(\u0026#34;[mosn] [NewMosn] start service failed: %v, exit\u0026#34;, err) } } 这段代码的第一行调用了 GetInheritListeners() 来获取要继承过来的监听器。 reconfigure 这个返回值是新旧 MOSN 在 listen.sock 上的连接,如果为空代表了 MOSN 为普通启动,反之为热升级/重启。下面开始分析 GetInheritListeners()。在 GetInheritListeners() 中调用了 isReconfigure() 方法。\nfunc isReconfigure() bool { var unixConn net.Conn var err error unixConn, err = net.DialTimeout(\u0026#34;unix\u0026#34;, types.ReconfigureDomainSocket, 1*time.Second) defer unixConn.Close() uc := unixConn.(*net.UnixConn) buf := make([]byte, 1) n, _ := uc.Read(buf) if n != 1 { return false } return true } 这里通过连接 unix socket reconfig.sock,判断能否读取到数据。旧 MOSN 会监听 reconfig.sock,在有连接进来时发送数据。\nl, err := net.Listen(\u0026#34;unix\u0026#34;, types.ReconfigureDomainSocket) defer l.Close() ul := l.(*net.UnixListener) for { uc, err := ul.AcceptUnix() _, err = uc.Write([]byte{0}) uc.Close() reconfigure(false) } 如果能读到,说明已经有一个旧的 MOSN 启动并监听了 reconfig.sock,那么本次启动就是热升级/重启了。同时向 reconfig.sock 发起连接,会使得旧的 MOSN 尝试向 listen.sock 发送要转移的 listener 数组。这个逻辑在上面的 reconfigure(false) 方法中调用 sendInheritListeners() 实现。为了保证旧的 MOSN 可以连上新的 MOSN,这里还重试了10次,并且每次等待1s。也就是说,新 MOSN 在接下来 10s 内可以监听 listen.sock 即可。\n// retry 10 time for i := 0; i \u0026lt; 10; i++ { unixConn, err = net.DialTimeout(\u0026#34;unix\u0026#34;, types.TransferListenDomainSocket, 1*time.Second) if err == nil { break } time.Sleep(1 * time.Second) } 同时,该连接在旧 MOSN 中保持10分钟后,或者读到了代表要退出的数据,旧 MOSN 就会自动退出。如果是确定了是升级或重启,那么 GetInheritListeners() 还会继续执行以下的代码:\n// unlink 系统调用比较特殊。关于它的描述中有一点:如果这个文件是一个 unix socket,它会被移除,但是打开它的进程可以继续使用它。也就是说新旧 mosn 都会在这个地址监听。 syscall.Unlink(types.TransferListenDomainSocket) // 监听 l, err := net.Listen(\u0026#34;unix\u0026#34;, types.TransferListenDomainSocket) defer l.Close() ul := l.(*net.UnixListener) ul.SetDeadline(time.Now().Add(time.Second * 10)) uc, err := ul.AcceptUnix() buf := make([]byte, 1) oob := make([]byte, 1024) _, oobn, _, _, err := uc.ReadMsgUnix(buf, oob) scms, err := unix.ParseSocketControlMessage(oob[0:oobn]) if len(scms) != 1 { log.StartLogger.Errorf(\u0026#34;[server] expected 1 SocketControlMessage; got scms = %#v\u0026#34;, scms) return nil, nil, err } // 解析从另一个进程传来的socket控制消息:打开的文件描述符的整型数组 gotFds, err := unix.ParseUnixRights(\u0026amp;scms[0]) listeners := make([]net.Listener, len(gotFds)) // 这个循环中将文件描述符转换成了listener for i := 0; i \u0026lt; len(gotFds); i++ { fd := uintptr(gotFds[i]) file := os.NewFile(fd, \u0026#34;\u0026#34;) defer file.Close() fileListener, err := net.FileListener(file) if listener, ok := fileListener.(*net.TCPListener); ok { listeners[i] = listener } else { return nil, nil, errors.New(\u0026#34;not a tcp listener\u0026#34;) } } return listeners, uc, nil 上面的代码中新的 MOSN 监听了 listen.sock,这样就能获取到旧 MOSN 的所有 listener。当然,如果本次启动是普通启动,那么获取的 inheritListeners 就是 nil。然后根据本次启动是普通启动还是热升级/重启:\n 如果是普通启动,则直接调用 StartService。需要注意在后面的执行流程中,还会再一次调用 StartService。 如果是热升级/重启,设置当前状态为 Active_Reconfiguring,然后重新加载配置文件,注意在此时并没有调用 StartService。 因为后面还会有StartService的调用,因此 StartService 的逻辑在后面分析,以便理解在不同地方调用的逻辑。\n88 行的 initializeMetrics(c.Metrics) 初始化了监控指标,使用了 go-metrics。这里不做分析。\n90 行开始是 Mosn 实例的初始化。它传入了上面的 inheritListeners 和 reconfigure 变量。\nm := \u0026amp;Mosn{ config: c, wg: sync.WaitGroup{}, inheritListeners: inheritListeners, reconfigure: reconfigure, } 123 行开始是 clustermanager 的初始化,它会根据是否是 Xds 模式来选择不同的初始化方式。如果是 Xds, 则先用空配置初始化,集群信息会在之后通过 Xds 来获取,否则的话就用配置文件来初始化。\n//cluster manager filter cmf := \u0026amp;clusterManagerFilter{} // parse cluster all in one clusters, clusterMap := configmanager.ParseClusterConfig(c.ClusterManager.Clusters) // create cluster manager if mode == v2.Xds { m.clustermanager = cluster.NewClusterManagerSingleton(nil, nil) } else { m.clustermanager = cluster.NewClusterManagerSingleton(clusters, clusterMap) } 136 行是路由管理器的初始化\n// initialize the routerManager m.routerManager = router.NewRouterManager() 138 行开始就是对配置中的 servers 进行解析,我们可以根据上面的一处判断得知,当前 MOSN 只会有一个 server,这里的 for 循环应该是为了之后功能扩展准备的。\nsrvNum := len(c.Servers) if srvNum == 0 { log.StartLogger.Fatalf(\u0026#34;[mosn] [NewMosn] no server found\u0026#34;) } else if srvNum \u0026gt; 1 { log.StartLogger.Fatalf(\u0026#34;[mosn] [NewMosn] multiple server not supported yet, got %d\u0026#34;, srvNum) } for 循环的代码,具体执行的逻辑我用注释写在了代码上。\nfor _, serverConfig := range c.Servers { //1. server config prepare \t//server config \tc := configmanager.ParseServerConfig(\u0026amp;serverConfig) // new server config \tsc := server.NewConfig(c) // init default log \tserver.InitDefaultLogger(sc) var srv server.Server if mode == v2.Xds { // xds 模式下,server 的配置是在上面创建的 \tsrv = server.NewServer(sc, cmf, m.clustermanager) } else { // 这里的server配置是从文件中读取的, 可以看 configs 下配置文件来帮助理解 \t//initialize server instance \tsrv = server.NewServer(sc, cmf, m.clustermanager) //add listener \tif serverConfig.Listeners == nil || len(serverConfig.Listeners) == 0 { log.StartLogger.Fatalf(\u0026#34;[mosn] [NewMosn] no listener found\u0026#34;) } for idx, _ := range serverConfig.Listeners { // parse ListenerConfig, 这里面会解析 listeners 的配置,并且和 inheritListeners 中的 listener 比对,如果是同一个连接(端口号相同,ip配置相同),就继承过来 \tlc := configmanager.ParseListenerConfig(\u0026amp;serverConfig.Listeners[idx], inheritListeners) // parse routers from connection_manager filter and add it the routerManager \tif routerConfig := configmanager.ParseRouterConfiguration(\u0026amp;lc.FilterChains[0]); routerConfig.RouterConfigName != \u0026#34;\u0026#34; { m.routerManager.AddOrUpdateRouters(routerConfig) } var nfcf []api.NetworkFilterChainFactory var sfcf []api.StreamFilterChainFactory // Note: as we use fasthttp and net/http2.0, the IO we created in mosn should be disabled \t// network filters \tif !lc.UseOriginalDst { // network and stream filters \tnfcf = configmanager.GetNetworkFilters(\u0026amp;lc.FilterChains[0]) sfcf = configmanager.GetStreamFilters(lc.StreamFilters) } _, err := srv.AddListener(lc, nfcf, sfcf) if err != nil { log.StartLogger.Fatalf(\u0026#34;[mosn] [NewMosn] AddListener error:%s\u0026#34;, err.Error()) } } } m.servers = append(m.servers, srv) } 上面就是 MOSN 初始化的分析。主要做了下列的工作:\n 初始化配置文件路径,日志,进程id路径,unix socket 路径,trace的开关(SOFATracer)以及日志插件。 通过 server.GetInheritListeners() 来判断启动模式(普通启动或热升级/重启),并在热升级/重启的情况下继承旧 MOSN 的监听器文件描述符。 如果是热升级/重启,则设置 Mosn 状态为 Active_Reconfiguring;如果是普通启动,则直接调用 StartService(),关于 StartService 会在之后分析。 初始化指标服务。 根据是否是 Xds 模式初始化配置。 xds 模式下,使用 nil 来初始化 clustermanager, 非 Xds 模式下(也就是File, Mix模式) ,从配置文件中初始化 clustermanager xds 模式下,使用默认配置来实例化 routerManager, 非 Xds 模式下,初始化 routerManager,并从配置文件中读取路由配置更新 xds 模式下,使用默认配置来实例化 server,非 Xds 模式下,还要从配置文件中读取 listener 并添加。 这里也用时序图来展示热升级/重启的初始化流程:\n如果是普通启动,则6,7,8,9,10,11,12是没有的,并且在第5步后会调用StartService。为了便于对比,这里仍然用时序图来展示:\nMOSN 的启动 MOSN 启动逻辑实现在 Mosn 的 Start() 方法中,代码如下\nfunc (m *Mosn) Start() { m.wg.Add(1) // Start XDS if configured \tm.xdsClient = \u0026amp;xds.Client{} utils.GoWithRecover(func() { m.xdsClient.Start(m.config) }, nil) // start mosn feature \tfeaturegate.StartInit() // TODO: remove it \t//parse service registry info \tconfigmanager.ParseServiceRegistry(m.config.ServiceRegistry) // beforestart starts transfer connection and non-proxy listeners \tm.beforeStart() // start mosn server \tfor _, srv := range m.servers { utils.GoWithRecover(func() { srv.Start() }, nil) } } 我们从代码中可以知道,Start() 方法主要做了以下的工作:\n 启动 xdsClient, xdsClient 负责从 pilot 周期地拉取 listeners/clusters/clusterloadassignment 配置。这个特性使得用户可以通过 crd 来动态的改变 service mesh 中的策略。\n 开始执行所有注册在 featuregate中 feature 的初始化函数。在 pkg/featuregate/mosn_features.go 文件中的 init() 方法中,可以看到 XdsMtlsEnable、PayLoadLimitEnabl\te 和 MultiTenantMode 的注册。\n 解析服务注册信息\n MOSN 启动前的准备工作。详细解析见下面的小章节 2.2.1\n 正式启动 MOSN 的服务。详细解析见下面的小章节 2.2.2\n beforeStart(): MOSN 启动前的最后一步 beforeStart 中主要做了以下的几个工作:\n 构造 adminServer,将 admin server 加入到全局的 services 中 根据 MOSN 的状态是否是 Active_Reconfiguring 如果是热升级/重启,调用 store.StartService(m.inheritListeners),继承 listener 直接启动。通知旧 MOSN 退出,并从旧 MOSN 中转移长连接。 如果是普通启动,store.StartService(nil) 会先监听 listener在启动。 关闭遗留的 listener,也就是在第 2 步中没有使用的 lisenter 开启 dump config 监听 reconfig.sock,这样就可以接收下一次的平滑升级或重启 这里我们先思考一下 store.StartService 的调用逻辑,因为在前文提到,初始化 MOSN 的时候,如果是普通启动,则会调用一次 store.StartService。而热升级/重启则不会调用。而后面无论何种情况都会调用store.StartService,是否多此一举呢?通过注释可以发现,前面是 start init services,后面是 start other services,同时 service 的结构如下:\ntype service struct { start bool *http.Server name string init func() exit func() } 这里想表达的逻辑应该是,如果是普通启动,那么有一些具有 init 变量的服务需要提前启动。在 StartService 中也可以验证这个想法:\nif s.init != nil { s.init() } 但是在整个项目中我只找到了三处 service,没有符合条件的。这里可能是为了之后的功能扩展使用的。三处 service 如下所示:\n store.AddService(s, \u0026ldquo;pprof\u0026rdquo;, nil, nil) store.AddService(srv, \u0026ldquo;Mosn Admin Server\u0026rdquo;, nil, nil) store.AddService(srv, \u0026ldquo;prometheus\u0026rdquo;, nil, nil) 因为普通启动时只有store.StartService(nil), 因此我们这里接着分析在热升级/重启时的长连接转移逻辑。在初始化 MOSN 部分,新的 MOSN 实例已经继承过来 listener 了,为了实现平滑的升级或重启,还需要把旧 MOSN 上的连接也转移过来。在 2.1 小章节中说到,GetInheritListeners() 方法通过监听 listen.sock,继承了旧 MOSN 中的 listener。在这里我们通过继承过来的 listener 在新 MOSN 中启动服务。\n// start other services if err := store.StartService(m.inheritListeners); err != nil { log.StartLogger.Fatalf(\u0026quot;[mosn] [NewMosn] start service failed: %v, exit\u0026quot;, err) } 同时新 MOSN 实例拥有了一个叫做 reconfigure(不要被名字误导了,它是新旧 MOSN 在 listen.sock 上的连接) 的连接。该连接会在旧 MOSN 中保持10分钟或者读到了代表要退出的数据。在 beforeStart 中,就是使用了 reconfigure 来在新 MOSN 即将启动之际,通知旧的 MOSN 退出。可以看 pkg/mosn/starter.go 的 202 行左右。下面一小段代码中,向 reconfigure 连接写入了一个 0 来通知旧 Mosn 退出。\n// notify old mosn to transfer connection if _, err := m.reconfigure.Write([]byte{0}); err != nil { log.StartLogger.Fatalf(\u0026quot;[mosn] [NewMosn] graceful failed, exit\u0026quot;) } m.reconfigure.Close() 旧的 MOSN 在读到上面的 0 后,会执行以下逻辑\n 停止服务,也就是关闭数据平面 等待3s,这是为了新的 MOSN 启动 停止 accept,这时候就不会有新的连接到旧的 MOSN 上了 等待已有连接完成逻辑。默认是 30s 退出 代码在 pkg/server/reconfigure.go 的 87 行左右:\n// Wait new mosn parse configuration notify.SetReadDeadline(time.Now().Add(10 * time.Minute)) n, err = notify.Read(buf[:]) if n != 1 { log.DefaultLogger.Alertf(types.ErrorKeyReconfigure, \u0026#34;new mosn start failed\u0026#34;) return } // stop other services store.StopService() // Wait for new mosn start time.Sleep(3 * time.Second) // Stop accepting requests StopAccept() // Wait for all connections to be finished WaitConnectionsDone(GracefulTimeout) log.DefaultLogger.Infof(\u0026#34;[server] [reconfigure] process %d gracefully shutdown\u0026#34;, os.Getpid()) keeper.ExecuteShutdownCallbacks(\u0026#34;\u0026#34;) // Stop the old server, all the connections have been closed and the new one is running os.Exit(0) 这个时候,我们再回来看长连接的转移。新 MOSN 在通知之后,会立即启动 TransferServer,也就是转移长连接的服务。\n// transfer old mosn connections utils.GoWithRecover(func() { network.TransferServer(m.servers[0].Handler()) }, nil) 旧 MOSN 会通过 conn.sock 来发送长连接的文件描述符给新 MOSN。具体调用的方法是pkg/network/transfer.go中的transferRead 和 transferWrite 方法。关于长连接转移的细节非常复杂,MOSN 官网上提供了详细的文档,很值得学习:MOSN 平滑升级原理解析\n下面我们用时序图来展示一下长连接的转移过程:\nStart(): MOSN 正式启动 经过上面复杂的启动过程,MOSN 正式启动就很简单了。\nfor _, srv := range m.servers { utils.GoWithRecover(func() { srv.Start() }, nil) } 对于当前来说,只有一个 server,这个 server 是在 NewMosn 中初始化的。\nserver := \u0026amp;server{ serverName: config.ServerName, stopChan: make(chan struct{}), handler: NewHandler(cmFilter, clMng), } Start 方法如下:\nfunc (srv *server) Start() { // TODO: handle main thread panic @wugou srv.handler.StartListeners(nil) for { select { case \u0026lt;-srv.stopChan: return } } } 基本术语参考 下面基本术语的解释来自于这篇文章:Envoy 中的基本术语\n Host:能够进行网络通信的实体(在手机或服务器等上的应用程序)。在 Envoy 中主机是指逻辑网络应用程序。只要每台主机都可以独立寻址,一块物理硬件上就运行多个主机。\n Downstream:下游(downstream)主机连接到 Envoy,发送请求并或获得响应。\n Upstream:上游(upstream)主机获取来自 Envoy 的链接请求和响应。\n Cluster: 集群(cluster)是 Envoy 连接到的一组逻辑上相似的上游主机。Envoy 通过服务发现发现集群中的成员。Envoy 可以通过主动运行状况检查来确定集群成员的健康状况。Envoy 如何将请求路由到集群成员由负载均衡策略确定。\n Mesh:一组互相协调以提供一致网络拓扑的主机。Envoy mesh 是指一组 Envoy 代理,它们构成了由多种不同服务和应用程序平台组成的分布式系统的消息传递基础。\n 运行时配置:与 Envoy 一起部署的带外实时配置系统。可以在无需重启 Envoy 或 更改 Envoy 主配置的情况下,通过更改设置来影响操作。\n Listener: 监听器(listener)是可以由下游客户端连接的命名网络位置(例如,端口、unix域套接字等)。Envoy 公开一个或多个下游主机连接的侦听器。一般是每台主机运行一个 Envoy,使用单进程运行,但是每个进程中可以启动任意数量的 Listener(监听器),目前只监听 TCP,每个监听器都独立配置一定数量的(L3/L4)网络过滤器。Listenter 也可以通过 Listener Discovery Service(LDS)动态获取。\n Listener filter:Listener 使用 listener filter(监听器过滤器)来操作链接的元数据。它的作用是在不更改 Envoy 的核心功能的情况下添加更多的集成功能。Listener filter 的 API 相对简单,因为这些过滤器最终是在新接受的套接字上运行。在链中可以互相衔接以支持更复杂的场景,例如调用速率限制。Envoy 已经包含了多个监听器过滤器。\n Http Route Table:HTTP 的路由规则,例如请求的域名,Path 符合什么规则,转发给哪个 Cluster。\n Health checking:健康检查会与SDS服务发现配合使用。但是,即使使用其他服务发现方式,也有相应需要进行主动健康检查的情况。详见 health checking。\n 参考资料 Envoy 中的基本术语 Service Mesh 架构反思:数据平面和控制平面的界线该如何划定? 深入解读 Service Mesh 背后的技术细节 使用 Istio 打造微服务(第1部分) 蚂蚁集团 Service Mesh 新型网络代理的思考与实践 MOSN 平滑升级原理解析 ","excerpt":"本文的目的是分析 MOSN 的启动流程。基于 mosn 版本 v0.4.0,commit 为: dc35c8fc95435a47e6393db1c79dd9f60f7eb898\nMOSN …","ref":"https://mosn.io/blog/code/mosn-startup/v0.4.0/","title":"MOSN 源码解析 - 启动流程(v0.4.0)"},{"body":"connection_manager 用于描述 MOSN 的路由配置,通常与 proxy 配合使用。配置详细描述见router\n注意:这是一个已经废弃的配置项。依然保留它的存在,是为了兼容性考虑。 新的配置模式下,应该配置在server的routers中\n","excerpt":"connection_manager 用于描述 MOSN 的路由配置,通常与 proxy 配合使用。配置详细描述见router\n注意:这是一个已经废弃的配置项。依然保留它的存在,是为了兼容性考虑。 新 …","ref":"https://mosn.io/docs/products/configuration-overview/server/listener/network-filter/connection-manager/","title":"connection_manager"},{"body":"本文描述的是 MOSN listener 配置。\n Listener 配置详细描述了 MOSN 启动时监听的端口,以及对应的端口对应不同逻辑的配置。 Listener 的配置可以通过Listener动态接口进行添加和修改。 { \u0026#34;name\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;type\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;address\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;bind_port\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;use_original_dst\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;access_logs\u0026#34;:[], \u0026#34;listener_filters\u0026#34;:[], \u0026#34;filter_chains\u0026#34;:[], \u0026#34;stream_filters\u0026#34;:[], \u0026#34;inspector\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;connection_idle_timeout\u0026#34;:\u0026#34;\u0026#34; } name 用于唯一区分 Listener,如果配置为空,会默认生成一个 UUID 作为 name。在对 Listener 进行动态更新时,使用 name 作为索引,如果 name 不存在,则是新增一个 listener,如果 name 存在则是对 listener 进行更新。\ntype 标记 Listener 的类型,目前支持 ingress 和 egress 两种类型。不同 type 的 Listener 输出的 tracelog 不同。\naddress IP:Port 形式的字符串,Listener 监听的地址,唯一。\nbind_port bool 类型,表示 Listener 是否会占用 address 配置的地址,通常情况下都需要配置为true。\nuse_original_dst 标记使用的透明代理类型,目前支持 redirect 和 tproxy 两种类型。(v1.2.0 之前为 bool 类型,标记是否开启 redirect 模式的透明代理)\naccess_logs 一组 access_log 配置。\nlistener_filters 一组 ListenerFilter 配置,目前 MOSN 仅支持一个 listener_filter。\nfilter_chains 一组 FilterChain 配置,目前 MOSN 仅支持一个 filter_chain。\nstream_filters 一组 stream_filter 配置,目前只在 filter_chain 中配置了 filter 包含 proxy 时生效。\ninspector bool 类型,当此值为 true 时,表示即便 listener 在 filter_chain 中配置开启了 TLS 监听,listener 依然可以处理非 TLS 的请求。\nconnection_idle_timeout Duration String,空闲连接超时配置。当 listener 上建立的连接空闲超过配置的超时时间以后,MOSN 会将此连接关闭。\n","excerpt":"本文描述的是 MOSN listener 配置。\n Listener 配置详细描述了 MOSN 启动时监听的端口,以及对应的端口对应不同逻辑的配置。 Listener 的配置可以通过Listener动 …","ref":"https://mosn.io/docs/products/configuration-overview/server/listener/","title":"Listener 配置"},{"body":"","excerpt":"","ref":"https://mosn.io/docs/products/configuration-overview/server/listener/network-filter/","title":"Network Filter"},{"body":"本文是关于 MOSN server 配置的说明。\n虽然 MOSN 的配置结构里 servers 是一个 server 数组,但是目前最多只支持配置一个server。\nserver 描述的 MOSN 的基本的全局参数如下所示。\n{ \u0026quot;mosn_server_name\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;default_log_path\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;default_log_level\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;global_log_roller\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;use_netpoll_mode\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;graceful_timeout\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;optimize_local_write\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;processor\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;listeners\u0026quot;:[], \u0026quot;routers\u0026quot;:[] } mosn_server_name 字符串类型,用于设置当前 server 的标识。\ndefault_log_path 默认的错误日志文件路径,支持配置完整的日志路径,以及标准输出(stdout)和标准错误(stderr)。\n 如果配置为空,则默认输出到标准错误(stderr)。 default_log_level 默认的错误日志级别,支持DEBUG、INFO、WARN、ERROR、FATAL。\n 如果配置为空,则默认为 INFO。 global_log_roller 日志轮转配置,会对所有的日志生效,如 tracelog、accesslog、defaultlog。 字符串配置,支持两种模式的配置,一种是按时间轮转,一种是按日志大小轮转。同时只能有一种模式生效。 按照日志大小轮转 size, 表示日志达到多少 M 进行轮转。 age,表示最多保存多少天的日志。 keep,表示最多保存多少个日志。 compress,表示日志是否压缩,on 为压缩,off 为不压缩。 \u0026quot;global_log_roller\u0026quot;: \u0026quot;size=100 age=10 keep=10 compress=off\u0026quot; 按照时间轮转 time,表示每个多少个小时轮转一次。 \u0026quot;global_log_roller\u0026quot;:\u0026quot;time=1\u0026quot; use_netpoll_mode bool 类型,设置为 true 则表示开启 MOSN 的网络处理采用 netpoll 模型,默认值为 false。 graceful_timeout Duration String 的字符串配置,表示 MOSN 在进行平滑升级时,等待连接关闭的最大时间。 如果没有配置,默认为 30s。 optimize_local_write bool 类型,设置为 true 则表示当连接的目标地址是 Localhost 时,将使用 goroutine 进行异步写入,这样可以获得更好的性能,但降低了写入时间的准确性,默认值为 false。 processor MOSN 使用的 GOMAXPROCS 数量\n 如果没有配置,默认为 CPU 数量。 如果配置为 0,等价于没有配置。 listeners 一组 Listener 的配置。\nrouters 一组Router的配置。\n","excerpt":"本文是关于 MOSN server 配置的说明。\n虽然 MOSN 的配置结构里 servers 是一个 server 数组,但是目前最多只支持配置一个server。\nserver 描述的 MOSN 的 …","ref":"https://mosn.io/docs/products/configuration-overview/server/","title":"Server 配置"},{"body":"","excerpt":"","ref":"https://mosn.io/docs/products/","title":"产品介绍"},{"body":"MOSN 主要划分为如下模块,包括了网络代理具备的基础能力,也包含了 xDS 等云原生能力。\nxDS(UDPA)支持 MOSN 支持云原生统一数据面 API(UDPA),支持全动态配置更新。\nxDS 是 Envoy 创建的一个关键概念,它是一类发现服务的统称,其包括如下几类:\n CDS:Cluster Discovery Service EDS:Endpoint Discovery Service SDS:Secret Discovery Service RDS:Route Discovery Service LDS:Listener Discovery Service 正是通过对 xDS 的请求来动态更新 Envoy 配置,另外还有个 ADS(Aggregated Discovery Service)通过聚合的方式解决以上 xDS 的更新顺序问题。\n业务支持 MOSN 作为底层的高性能安全网络代理,支撑了 RPC、消息(Messaging)、网关(Gateway)等业务场景。\nIO 模型 MOSN 支持以下两种 IO 模型:\n Golang 经典 netpoll 模型:goroutine-per-connection,适用于在连接数不是瓶颈的情况。\n RawEpoll 模型:也就是 Reactor 模式,I/O 多路复用(I/O multiplexing)+ 非阻塞 I/O(non-blocking I/O)的模式。对于接入层和网关有大量长链接的场景,更加适合于 RawEpoll 模型。\n netpoll 模型 MOSN 的 netpoll 模型如上图所示,协程数量与链接数量成正比,大量链接场景下,协程数量过多,存在以下开销:\n Stack 内存开销 Read buffer 开销 Runtime 调度开销 RawEpoll 模型 RawEpoll 模型如上图所示,使用 epoll 感知到可读事件之后,再从协程池中为其分配协程进行处理,步骤如下:\n 链接建立后,向 Epoll 注册 oneshot 可读事件监听;并且此时不允许有协程调用 conn.read,避免与 runtime netpoll 冲突。 可读事件到达,从 goroutine pool 挑选一个协程进行读事件处理;由于使用的是 oneshot 模式,该 fd 后续可读事件不会再触发。 请求处理过程中,协程调度与经典 netpoll 模式一致。 请求处理完成,将协程归还给协程池;同时将 fd 重新添加到 RawEpoll 中。 协程模型 MOSN 的协程模型如下图所示。\n 一条 TCP 连接对应一个 Read 协程,执行收包、协议解析; 一个请求对应一个 worker 协程,执行业务处理,proxy 和 Write 逻辑; 常规模型一个 TCP 连接将有 Read/Write 两个协程,我们取消了单独的 Write 协程,让 workerpool 工作协程代替,减少了调度延迟和内存占用。\n能力扩展 协议扩展 MOSN 通过使用统一的编解码引擎以及编/解码器核心接口,提供协议的 plugin 机制,包括支持:\n SOFARPC HTTP1.x/HTTP2.0 Dubbo NetworkFilter 扩展 MOSN 通过提供 network filter 注册机制以及统一的 packet read/write filter 接口,实现了 Network filter 扩展机制,当前支持:\n TCP proxy Fault injection StreamFilter 扩展 MOSN 通过提供 stream filter 注册机制以及统一的 stream send/receive filter 接口,实现了 Stream filter 扩展机制,包括支持:\n 流量镜像 RBAC 鉴权 TLS 安全链路 通过测试,原生的 Go 的 TLS 经过了大量的汇编优化,在性能上是 Nginx(OpenSSL)的80%,Boring 版本的 Go(使用 cgo 调用 BoringSSL)因为 cgo 的性能问题, 并不占优势,所以我们最后选择使用原生 Go 的 TLS,相信 Go Runtime 团队后续会有更多的优化,我们也会有一些优化计划。\nGo vs Nginx 测试结果如下图所示:\n Go 在 RSA 上没有太多优化,go-boring(CGO)的能力是 Go 的两倍。 p256 在 Go 上有汇编优化,ECDSA 优于go-boring。 在 AES-GCM 对称加密上,Go 的能力是 go-boring 的 20 倍。 在 SHA、MD 等 HASH 算法也有对应的汇编优化。 为了满足金融场景的安全合规,我们同时也对国产密码进行了开发支持,这个是 Go Runtime 所没有的。虽然目前的性能相比国际标准 AES-GCM 还是有一些差距,大概是 50%,但是我们已经有了后续的一些优化计划,敬请期待。\n支持国密的性能测试结果如下图所示:\n","excerpt":"MOSN 主要划分为如下模块,包括了网络代理具备的基础能力,也包含了 xDS 等云原生能力。\nxDS(UDPA)支持 MOSN 支持云原生统一数据面 API(UDPA),支持全动态配置更新。\nxDS …","ref":"https://mosn.io/docs/products/structure/core-concept/","title":"MOSN 核心概念"},{"body":"本文的完整构建镜像方法均是基于 MacOS 和 Istio 1.10.6 版本进行的构建,在其他操作系统 Istio 版本上可能存在部分细节差异,需要进行调整。 除了完整构建方式外,如果仅有 MOSN 代码发生变化,还可以使用 单独更新 MOSN 版本 的方式构建镜像。\n通常情况下,您不需要额外构建镜像,可直接用我们提供的镜像 mosnio/proxyv2:${MOSN-VERSION}-${ISTIO_VERSION},如docker pull mosnio/proxyv2:v1.0.0-1.10.6\n完整的镜像构建(基于 MacOS 和 Istio 1.10.6) 1、下载完整的 istio 源代码,并且切换到对应的版本\ngit clone [email protected]:istio/istio.git cd istio git checkout 1.10.6 2、由于目前 Istio 默认会加载 wasm,我们需要将相关逻辑注释掉,再重新编译镜像,避免一些不必要的错误。详细的改动可见 istio-diff\n3、编译 MOSN 二进制,MOSN 提供了镜像编译的方式可直接编译 linux 的二进制;同时由于在 MacOS 上构建的过程中,Istio 还会下载一个 MacOS 版本,因此还需要编译一个 MacOS 的二进制\n4、将编译好的二进制,使用 tar 方式进行打包,并且打包路径需要是 usr/local/bin\ncd ${MOSNProject Path} mkdir -p usr/local/bin make build # build mosn binary on linux cp build/bundles/${MOSNVERSION}/binary/mosn usr/local/bin tar -zcvf mosn.tar.gz usr/local/bin/mosn cp mosn.tar.gz mosn-centos.tar.gz # copy a renamed tar.gz file make build-local # build mosn binary on macos cp build/bundles/${MOSNVERSION}/binary/mosn usr/local/bin tar -zcvf mosn-macos.tar.gz usr/local/bin/mosn 5、将生成的mosn-macos.tar.gz mosn-centos.tar.gz mosn.tar.gz 上传到一个编译环境可访问的存储服务中,可用 Go 语言简单快速在本地环境搭建一个\nfunc main() { address := \u0026#34;\u0026#34; // an address can be reached when proxyv2 image build. for example, 0.0.0.0:8080 filespath := \u0026#34;\u0026#34; // where the .tar.gz files stored. http.ListenAndServe(address, http.FileServer(http.Dir(filespath))) } 6、指定参数,开始编译 proxyv2 镜像\naddress=$1 # your download service address export ISTIO_ENVOY_VERSION=$2 # MOSN Version, can be any value. export ISTIO_ENVOY_RELEASE_URL=http://$address/mosn.tar.gz export ISTIO_ENVOY_CENTOS_RELEASE_URL=http://$address/mosn-centos.tar.gz export ISTIO_ENVOY_MACOS_RELEASE_URL=http://$address/mosn-macos.tar.gz export ISTIO_ENVOY_MACOS_RELEASE_NAME=mosn-$2 # can be any value export SIDECAR=mosn make clean # clean the cache make docker.proxyv2 \\ SIDECAR=$SIDECAR \\ ISTIO_ENVOY_VERSION=$ISTIO_ENVOY_VERSION \\ ISTIO_ENVOY_RELEASE_URL=$ISTIO_ENVOY_RELEASE_URL \\ ISTIO_ENVOY_CENTOS_RELEASE_URL=$ISTIO_ENVOY_CENTOS_RELEASE_URL \\ ISTIO_ENVOY_MACOS_RELEASE_URL=$ISTIO_ENVOY_MACOS_RELEASE_URL \\ ISTIO_ENVOY_MACOS_RELEASE_NAME=$ISTIO_ENVOY_MACOS_RELEASE_NAME 7、编译完成以后,可以将镜像打上新的 Tag 并且上传(如个人测试 dockerhub 的地址),确保 istio 使用时可访问即可\n单独更新 MOSN 版本 1、重新编译 MOSN 二进制\ncd ${MOSNProject Path} make build # build mosn binary on linux 2、直接基于现有 MOSN 的 proxyv2 镜像更新二进制\nFROMmosnio/proxyv2:v1.0.0-1.10.6COPY build/bundles/${MOSNVERSION}/binary/mosn /usr/local/bin/mosndocker build --no-cache --rm -t ${yourimage tag} 3、将新镜像上传,确保 istio 使用时可访问即可\nistioctl manifest apply --set .values.global.proxy.image=${MOSNIMAGE} --set meshConfig.defaultConfig.binaryPath=\u0026#34;/usr/local/bin/mosn\u0026#34; ","excerpt":"本文的完整构建镜像方法均是基于 MacOS 和 Istio 1.10.6 版本进行的构建,在其他操作系统 Istio 版本上可能存在部分细节差异,需要进行调整。 除了完整构建方式外,如果仅有 MOSN …","ref":"https://mosn.io/docs/user-guide/start/images/","title":"MOSN 与 Istio 的 proxyv2 镜像 build 方法"},{"body":"MOSN编码规范 Go Code Review Comments https://github.com/golang/go/wiki/CodeReviewComments\n","excerpt":"MOSN编码规范 Go Code Review Comments https://github.com/golang/go/wiki/CodeReviewComments","ref":"https://mosn.io/docs/developer-guide/code-review/","title":"编码规范"},{"body":"本文是关于 MOSN trace 配置的说明。\nMOSN 的 tracing 框架 MOSN 的 tracing 框架由 Driver、Tracer 和 Span 三个部分组成。\nDriver 是 Tracer 的容器,管理注册的 Tracer 实例,Tracer 是 tracing 的入口,根据请求信息创建一个 Span,Span 存储当前跨度的链路信息。\n目前 MOSN tracing 有 SOFATracer 和 SkyWalking 两种实现。SOFATracer 支持 http1 和 xprotocol 协议的链路追踪,将 trace 数据写入本地日志文件中。SkyWalking 支持 http1 协议的链路追踪,使用原生的 Go 语言探针 go2sky 将 trace 数据通过 gRPC 上报到 SkyWalking 后端服务。\n配置项说明 tracing 相关配置项如下所示。\n{ \u0026#34;tracing\u0026#34;: { \u0026#34;enable\u0026#34;: true, \u0026#34;driver\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;config\u0026#34;: { } } } enable bool类型,表示启用或禁用trace。\ndriver 目前支持 SOFATracer 和 SkyWalking。\nconfig 不同driver的自定义配置。config里面的配置项由每个driver自己设计。比如 SkyWalking 的配置项可以参考 SkyWalking配置\n","excerpt":"本文是关于 MOSN trace 配置的说明。\nMOSN 的 tracing 框架 MOSN 的 tracing 框架由 Driver、Tracer 和 Span 三个部分组成。\nDriver …","ref":"https://mosn.io/docs/products/configuration-overview/trace/","title":"Trace 配置"},{"body":"本文档中提供了 MOSN 的示例工程。\n使用 MOSN 作为 HTTP 代理 请参考 MOSN 转发 HTTP 的示例工程 http-sample 。\n使用 MOSN 作为 SOFARPC 代理 请参考 MOSN 转发 SOFARPC 的示例工程 sofarpc-with-xprotocol-sample 。\n使用 MOSN 作为TCP 代理 请参考 MOSN 作为 TCP Proxy 的示例工程 tcpproxy-sample 。\n使用 SkyWalking 作为 Trace 实现 请参考 SkyWalking 作为 Trace 实现的示例工程 skywalking-sample 。\n","excerpt":"本文档中提供了 MOSN 的示例工程。\n使用 MOSN 作为 HTTP 代理 请参考 MOSN 转发 HTTP 的示例工程 http-sample 。\n使用 MOSN 作为 SOFARPC 代理 请参 …","ref":"https://mosn.io/docs/best-practices/","title":"最佳实践"},{"body":"","excerpt":"","ref":"https://mosn.io/docs/products/structure/","title":"架构原理"},{"body":"本文描述的是 MOSN 作为 Sidecar 使用时的流量劫持方案。\nMOSN 作为 Sidecar 和业务容器部署在同一个 Pod 中时,需要使得业务应用的 Inbound 和 Outbound 服务请求都能够经过 Sidecar 处理。区别于 Istio 社区使用 iptables 做流量透明劫持,MOSN 目前使用的是流量接管方案,并在积极探索适用于大规模流量下的透明劫持方案。\n流量接管 区别于 Istio 社区的 iptables 流量劫持方案,MOSN 使用的流量接管的方案如下:\n 假设服务端运行在 1.2.3.4 这台机器上,监听 20880 端口,首先服务端会向自己的 Sidecar 发起服务注册请求,告知 Sidecar 需要注册的服务以及 IP + 端口(1.2.3.4:20880) 服务端的 Sidecar 会向服务注册中心(如 SOFA Registry)发起服务注册请求,告知需要注册的服务以及 IP + 端口,不过这里需要注意的是注册上去的并不是业务应用的端口(20880),而是 Sidecar 自己监听的一个端口(例如:20881) 调用端向自己的 Sidecar 发起服务订阅请求,告知需要订阅的服务信息 调用端的 Sidecar 向调用端推送服务地址,这里需要注意的是推送的 IP 是本机,端口是调用端的 Sidecar 监听的端口(例如 20882) 调用端的 Sidecar 会向服务注册中心(如 SOFA Registry)发起服务订阅请求,告知需要订阅的服务信息; 服务注册中心(如 SOFA Registry)向调用端的 Sidecar 推送服务地址(1.2.3.4:20881) 服务调用过程 经过上述的服务发现过程,流量转发过程就显得非常自然了:\n 调用端拿到的服务端地址是 127.0.0.1:20882,所以就会向这个地址发起服务调用 调用端的 Sidecar 接收到请求后,通过解析请求头,可以得知具体要调用的服务信息,然后获取之前从服务注册中心返回的地址后就可以发起真实的调用(1.2.3.4:20881) 服务端的 Sidecar 接收到请求后,经过一系列处理,最终会把请求发送给服务端(127.0.0.1:20880) 透明劫持 上文通过在服务注册过程中把服务端地址替换成本机监听端口实现了轻量级的“流量劫持”,在存在注册中心,且调用端和服务端同时使用特定SDK的场景中可以很好的工作,如果不满足这两个条件,则无法流量劫持。为了降低对于应用程序的要求,需要引入透明劫持。\n使用 iptables 做流量劫持 iptables 通过 NAT 表的 redirect 动作执行流量重定向,通过 syn 包触发新建 nefilter 层的连接,后续报文到来时查找连接转换目的地址与端口。新建连接时同时会记录下原始目的地址,应用程序可以通过(SOL_IP、SO_ORIGINAL_DST)获取到真实的目的地址。\niptables 劫持原理如下图所示:\n使用 iptables 做流量劫持时存在的问题 目前 Istio 使用 iptables 实现透明劫持,主要存在以下三个问题:\n 需要借助于 conntrack 模块实现连接跟踪,在连接数较多的情况下,会造成较大的消耗,同时可能会造成 track 表满的情况,为了避免这个问题,业内有关闭 conntrack 的做法。 iptables 属于常用模块,全局生效,不能显式的禁止相关联的修改,可管控性比较差。 iptables 重定向流量本质上是通过 loopback 交换数据,outbond 流量将两次穿越协议栈,在大并发场景下会损失转发性能。 上述几个问题并非在所有场景中都存在,比方说某些场景下,连接数并不多,且 NAT 表未被使用到的情况下,iptables 是一个满足要求的简单方案。为了适配更加广泛的场景,透明劫持需要解决上述三个问题。\n透明劫持方案优化 使用 tproxy 处理 inbound 流量\ntproxy 可以用于 inbound 流量的重定向,且无需改变报文中的目的 IP/端口,不需要执行连接跟踪,不会出现 conntrack 模块创建大量连接的问题。受限于内核版本,tproxy 应用于 outbound 存在一定缺陷。目前 Istio 支持通过 tproxy 处理 inbound 流量。\n使用 hook connect 处理 outbound 流量\n为了适配更多应用场景,outbound 方向通过 hook connect 来实现,实现原理如下:\n无论采用哪种透明劫持方案,均需要解决获取真实目的 IP/端口的问题,使用 iptables 方案通过 getsockopt 方式获取,tproxy 可以直接读取目的地址,通过修改调用接口,hook connect 方案读取方式类似于tproxy。\n实现透明劫持后,在内核版本满足要求(4.16以上)的前提下,通过 sockmap 可以缩短报文穿越路径,进而改善 outbound 方向的转发性能。\n总结 总结来看,如果应用程序通过注册中心发布/订阅服务时,可以结合注册中心劫持流量;在需要用到透明劫持的场景,如果性能压力不大,使用 iptables redirect 即可,大并发压力下使用 tproxy 与hook connect 结合的方案。\n","excerpt":"本文描述的是 MOSN 作为 Sidecar 使用时的流量劫持方案。\nMOSN 作为 Sidecar 和业务容器部署在同一个 Pod 中时,需要使得业务应用的 Inbound 和 Outbound 服 …","ref":"https://mosn.io/docs/products/structure/traffic-hijack/","title":"流量劫持"},{"body":"本文是对 MOSN 自定义配置的说明。\nDuration String 字符串,由一个十进制数字和一个时间单位后缀组成,有效的时间单位为 ns、us(或µs)、ms、s、m、h,例如 1h、3s、500ms。 metadata metadata 用于 MOSN 路由和 Cluster Host 之间的匹配。\n{ \u0026#34;filter_metadata\u0026#34;:{ \u0026#34;mosn.lb\u0026#34;:{} } } mosn.lb 可对应任意的 string-string 的内容。\ntls_context { \u0026#34;status\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;type\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;server_name\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;ca_cert\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;cert_chain\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;private_key\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;verify_client\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;require_client_cert\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;insecure_skip\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;cipher_suites\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;ecdh_curves\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;min_version\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;max_version\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;alpn\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;fall_back\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;extend_verify\u0026#34;:\u0026#34;\u0026#34;, \u0026#34;sds_source\u0026#34;:{} } status,bool类型,表示是否开启 TLS,默认是 false。 type,字符串类型,描述 tls_context 的类型。tls_context 支持扩展实现,不同的 type 对应不同的实现方式,默认实现方式对应的 type 是空字符串。 server_name,当没有配置 insecure_skip 时,用于校验服务端返回证书的 hostname。作为Cluster配置时有效。 ca_cert,证书签发的根 CA 证书。 cert_chain,TLS 证书链配置。 private_key,证书私钥配置。 verify_client,bool 类型,作为 Listener 配置时有效,表示是否要校验 Client 端证书 require_client_cert,bool 类型,表示是否强制 Client 端必须携带证书。 insecure_skip,bool 类型,作为 Cluster 配置时有效,表示是否要忽略 Server 端的证书校验。 cipher_suites,如果配置了该配置,那么 TLS 连接将只支持配置了的密码套件,并且会按照配置的顺序作为优先级使用,支持的套件类型如下: ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-RSA-AES256-GCM-SHA384 ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-RSA-AES128-GCM-SHA256 ECDHE-ECDSA-WITH-CHACHA20-POLY1305 ECDHE-RSA-WITH-CHACHA20-POLY1305 ECDHE-RSA-AES256-CBC-SHA ECDHE-RSA-AES128-CBC-SHA ECDHE-ECDSA-AES256-CBC-SHA ECDHE-ECDSA-AES128-CBC-SHA RSA-AES256-CBC-SHA RSA-AES128-CBC-SHA ECDHE-RSA-3DES-EDE-CBC-SHA RSA-3DES-EDE-CBC-SHA ECDHE-RSA-SM4-SM3 ECDHE-ECDSA-SM4-SM3 ecdh_curves,如果配置了该配置,那么 TLS 连接将只支持配置了的曲线。\n 支持 x25519、p256、p384、p521。 min_version,最低的 TLS 协议版本,默认是 TLS1.0。\n 支持 TLS1.0、TLS1.1、TLS1.2。 默认会自动识别可用的 TLS 协议版本。 max_version,最高的 TLS 协议版本,默认是 TLS1.2。\n 支持 TLS1.0、TLS1.1、TLS1.2。 默认会自动识别可用的 TLS 协议版本。 alpn,TLS 的 ALPN 配置。\n 支持 h2、http/1.1、 sofa。 fall_back,bool类型,当配置为 true 时,如果证书解析失败,不会报错而是相当于没有开启 TLS。\n extend_verify,任意 json 类型,当 type 为非空时,作为扩展的配置参数。\n sds_source,访问 SDS API 的配置,如果配置了这个配置,ca_cert、cert_chain 和 private_key 都会被忽略,但是其余的配置依然有效。\n sds_source { \u0026#34;CertificateConfig\u0026#34;:{}, \u0026#34;ValidationConfig\u0026#34;:{} } CertificateConfig 描述了如何获取 cert_chain 和 private_key 的配置。 ValidationConfig 描述了如何获取 ca_cert 的配置。 详细的 Config 内容参考 envoy: sdssecretconfig。 ","excerpt":"本文是对 MOSN 自定义配置的说明。\nDuration String 字符串,由一个十进制数字和一个时间单位后缀组成,有效的时间单位为 ns、us(或µs)、ms、s、m、h, …","ref":"https://mosn.io/docs/products/configuration-overview/custom/","title":"自定义配置"},{"body":"本文将向您展示 MOSN 的 TLS 安全能力。\n证书方案 MOSN 支持通过 Istio Citadel 的证书签发方案,基于 Istio 社区的 SDS (Secret Discovery Service)方案为 Sidecar 配置证书,支持证书动态发现和热更新能力。为了支持更高级的安全能力,MOSN 没有使用 Citadel 的证书自签发能力,而是通过对接内部 KMS 系统获取证书。同时提供证书缓存和证书推送更新能力。\n我们先来看看 MOSN 证书方案的架构图,如下图所示:\n各组件职能如下:\n Pilot:负责 Policy、SDS 配置下发,为简化复杂度,图中未标出 Citadel:Citadel 作为 Certificate Provider ,同时作为 MCP Server 为 Citadel Agent 提供 Pod、CR等资源 Citadel Agent:提供 SDS Server 服务,为MOSN、DB Sidecar、Security Sidecar 提供Certificate和CR下发能力 KMS:密钥管理系统负责证书签发 证书获取流程 对整体架构有个大致理解后,我们分解下 Sidecar 获取证书的流程,如下图所示:\n补充说明下图中的每一步环节:\n Citadel 与 Citadel agent(nodeagent)组件通过MCP协议(Mesh Configuration Protocol)同步Pod 和 CR 信息,避免 citadel agent 直接请求 API Server 导致 API Server 负载过高 MOSN 通过Unix Domain Socket 方式向 Citadel Agent 发起 SDS 请求 Citadel Agent 会进行防篡改校验,并提取appkey Citadel Agent 携带 appkey 请求 Citadel 签发证书 Citadel 检查证书是否已缓存,如果缓存证书未过期,Citadel 将直接响应缓存证书 证书不在缓存中,Citadel 会基于 appkey 构造证书签发请求,向 KMS 申请签发证书 KMS 会将签发的证书响应回Citadel,另外 KMS 也支持证书过期轮换通知 Citadel 收到证书后,会将证书传递给到对应的 Citadel Agent Citadel Agent 收到证书后,会在内存中缓存证书,并将证书下发给到 MOSN ","excerpt":"本文将向您展示 MOSN 的 TLS 安全能力。\n证书方案 MOSN 支持通过 Istio Citadel 的证书签发方案,基于 Istio 社区的 SDS (Secret Discovery …","ref":"https://mosn.io/docs/products/structure/tls/","title":"TLS 安全链路"},{"body":"","excerpt":"","ref":"https://mosn.io/docs/user-guide/start/","title":"开始"},{"body":"MOSN 的配置文件可以分为以下四大部分:\n Servers 配置,目前仅支持最多 1 个 Server 的配置,Server 中包含一些基础配置以及对应的 Listener 配置 ClusterManager 配置,包含 MOSN 的 Upstream 详细信息 对接控制平面(Pilot)的 xDS 相关配置 其他配置 Trace、Metrics、Debug、Admin API 相关配置 扩展配置,提供自定义配置扩展需求 配置文件概览\nMOSN 的基本配置部分如下所示:\n{ \u0026#34;servers\u0026#34;: [], \u0026#34;cluster_manager\u0026#34;: {}, \u0026#34;dynamic_resources\u0026#34;: {}, \u0026#34;static_resources\u0026#34;: {}, \u0026#34;admin\u0026#34;:{}, \u0026#34;pprof\u0026#34;:{}, \u0026#34;tracing\u0026#34;:{}, \u0026#34;metrics\u0026#34;:{} } 配置类型 MOSN 的配置包括以下几种类型:\n 静态配置 动态配置 混合模式 静态配置 静态配置是指 MOSN 启动时,不对接控制平面 Pilot 的配置,用于一些相对固定的简单场景(如 MOSN 的示例)。 使用静态配置启动的 MOSN,也可以通过扩展代码,调用动态更新配置的接口实现动态修改。 静态配置启动时必须包含一个 Server 以及至少一个 Cluster。 动态配置 动态配置是指 MOSN 启动时,只有访问控制平面相关的配置,没有 MOSN 运行时所需要的配置。\n 使用动态配置启动的 MOSN,会向管控面请求获取运行时所需要的配置,管控面也可能在运行时推送更新 MOSN 运行配置。\n 动态配置启动时必须包含 DynamicResources 和 StaticResources 配置。\n 混合模式 MOSN 启动时的配置可以同时包含静态模式与动态模式,以混合模式启动的 MOSN 会先以静态配置完成初始化,随后可能由控制平面获取配置更新。\n配置示例 静态配置示例 静态配置的示例如下所示。\n{ \u0026#34;servers\u0026#34;: [ { \u0026#34;default_log_path\u0026#34;: \u0026#34;/home/admin/logs/mosn/default.log\u0026#34;, \u0026#34;default_log_level\u0026#34;: \u0026#34;DEBUG\u0026#34;, \u0026#34;processor\u0026#34;: 4, \u0026#34;listeners\u0026#34;: [ { \u0026#34;address\u0026#34;: \u0026#34;0.0.0.0:12220\u0026#34;, \u0026#34;bind_port\u0026#34;: true, \u0026#34;filter_chains\u0026#34;: [ { \u0026#34;filters\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;proxy\u0026#34;, \u0026#34;config\u0026#34;: { \u0026#34;downstream_protocol\u0026#34;: \u0026#34;SofaRpc\u0026#34;, \u0026#34;upstream_protocol\u0026#34;: \u0026#34;SofaRpc\u0026#34;, \u0026#34;router_config_name\u0026#34;: \u0026#34;test_router\u0026#34; } }, { \u0026#34;type\u0026#34;: \u0026#34;connection_manager\u0026#34;, \u0026#34;config\u0026#34;: { \u0026#34;router_config_name\u0026#34;: \u0026#34;test_router\u0026#34;, \u0026#34;virtual_hosts\u0026#34;: [] } } ] } ] } ] } ], \u0026#34;cluster_manager\u0026#34;: { \u0026#34;clusters\u0026#34;: [ { \u0026#34;name\u0026#34;:\u0026#34;example\u0026#34;, \u0026#34;lb_type\u0026#34;: \u0026#34;LB_ROUNDROBIN\u0026#34;, \u0026#34;hosts\u0026#34;: [ {\u0026#34;address\u0026#34;: \u0026#34;127.0.0.1:12200\u0026#34;} ] } ] } } 动态配置示例 动态配置的示例如下所示。\n{ \u0026#34;servers\u0026#34;: [ { \u0026#34;default_log_path\u0026#34;: \u0026#34;stdout\u0026#34;, \u0026#34;default_log_level\u0026#34;: \u0026#34;DEBUG\u0026#34; } ], \u0026#34;static_resources\u0026#34;: { \u0026#34;clusters\u0026#34;: [ { \u0026#34;connect_timeout\u0026#34;: \u0026#34;1s\u0026#34;, \u0026#34;load_assignment\u0026#34;: { \u0026#34;cluster_name\u0026#34;: \u0026#34;xds_cluster\u0026#34;, \u0026#34;endpoints\u0026#34;: [ { \u0026#34;lb_endpoints\u0026#34;: [ { \u0026#34;endpoint\u0026#34;: { \u0026#34;address\u0026#34;: { \u0026#34;socket_address\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;127.0.0.1\u0026#34;, \u0026#34;port_value\u0026#34;: 9002 } } } } ] } ] }, \u0026#34;http2_protocol_options\u0026#34;: {}, \u0026#34;name\u0026#34;: \u0026#34;xds_cluster\u0026#34; } ] }, \u0026#34;dynamic_resources\u0026#34;: { \u0026#34;ads_config\u0026#34;: { \u0026#34;api_type\u0026#34;: \u0026#34;GRPC\u0026#34;, \u0026#34;transport_api_version\u0026#34;: \u0026#34;V3\u0026#34;, \u0026#34;grpc_services\u0026#34;: [ { \u0026#34;envoy_grpc\u0026#34;: { \u0026#34;cluster_name\u0026#34;: \u0026#34;xds_cluster\u0026#34; } } ], \u0026#34;set_node_on_first_message_only\u0026#34;: true }, \u0026#34;cds_config\u0026#34;: { \u0026#34;resource_api_version\u0026#34;: \u0026#34;V3\u0026#34;, \u0026#34;api_config_source\u0026#34;: { \u0026#34;api_type\u0026#34;: \u0026#34;GRPC\u0026#34;, \u0026#34;transport_api_version\u0026#34;: \u0026#34;V3\u0026#34;, \u0026#34;grpc_services\u0026#34;: [ { \u0026#34;envoy_grpc\u0026#34;: { \u0026#34;cluster_name\u0026#34;: \u0026#34;xds_cluster\u0026#34; } } ], \u0026#34;set_node_on_first_message_only\u0026#34;: true } }, \u0026#34;lds_config\u0026#34;: { \u0026#34;resource_api_version\u0026#34;: \u0026#34;V3\u0026#34;, \u0026#34;api_config_source\u0026#34;: { \u0026#34;api_type\u0026#34;: \u0026#34;GRPC\u0026#34;, \u0026#34;transport_api_version\u0026#34;: \u0026#34;V3\u0026#34;, \u0026#34;grpc_services\u0026#34;: [ { \u0026#34;envoy_grpc\u0026#34;: { \u0026#34;cluster_name\u0026#34;: \u0026#34;xds_cluster\u0026#34; } } ], \u0026#34;set_node_on_first_message_only\u0026#34;: true } } }, \u0026#34;node\u0026#34;: { \u0026#34;cluster\u0026#34;: \u0026#34;test-cluster\u0026#34;, \u0026#34;id\u0026#34;: \u0026#34;test-id\u0026#34; }, \u0026#34;layered_runtime\u0026#34;: { \u0026#34;layers\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;runtime-0\u0026#34;, \u0026#34;rtds_layer\u0026#34;: { \u0026#34;rtds_config\u0026#34;: { \u0026#34;resource_api_version\u0026#34;: \u0026#34;V3\u0026#34;, \u0026#34;api_config_source\u0026#34;: { \u0026#34;transport_api_version\u0026#34;: \u0026#34;V3\u0026#34;, \u0026#34;api_type\u0026#34;: \u0026#34;GRPC\u0026#34;, \u0026#34;grpc_services\u0026#34;: { \u0026#34;envoy_grpc\u0026#34;: { \u0026#34;cluster_name\u0026#34;: \u0026#34;xds_cluster\u0026#34; } } } }, \u0026#34;name\u0026#34;: \u0026#34;runtime-0\u0026#34; } } ] }, \u0026#34;admin\u0026#34;: { \u0026#34;access_log_path\u0026#34;: \u0026#34;/dev/null\u0026#34;, \u0026#34;address\u0026#34;: { \u0026#34;socket_address\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;127.0.0.1\u0026#34;, \u0026#34;port_value\u0026#34;: 9003 } } } } ","excerpt":"MOSN 的配置文件可以分为以下四大部分:\n Servers 配置,目前仅支持最多 1 个 Server 的配置,Server 中包含一些基础配置以及对应的 Listener …","ref":"https://mosn.io/docs/products/configuration-overview/","title":"配置概览"},{"body":"Service Mesh 中 Sidecar 运维一直是一个比较棘手的问题,数据平面的 Sidecar 升级是常有的事情,如何在升级 Sidecar(MOSN)的时候而不影响业务,对于存量的长连接如何迁移,本文将为你介绍 MOSN 的解决之道。\n背景 本文介绍 MOSN 支持平滑升级的原因和解决方案,对于平滑升级的一些基础概念,大家可以通过 Nginx vs Enovy vs Mosn 平滑升级原理解析了解。\n先简单介绍一下为什么 Nginx 和 Envoy 不需要具备 MOSN 这样的连接无损迁移方案,主要还是跟业务场景相关,Nginx 和 Envoy 主要支持的是 HTTP1 和 HTTP2 协议,HTTP1使用 connection: Close,HTTP2 使用 Goaway Frame 都可以让 Client 端主动断链接,然后新建链接到新的 New process,但是针对 Dubbo、SOFA PRC 等常见的多路复用协议,它们是没有控制帧,Old process 的链接如果断了就会影响请求的。\n一般的升级做法就是切走应用的流量,比如自己UnPub掉服务,等待一段时间没有请求之后,升级MOSN,升级好之后再Pub服务,整个过程比较耗时,并且会有一段时间是不提供服务的,还要考虑应用的水位,在大规模场景下,就很难兼顾评估。MOSN 为了满足自身业务场景,开发了长连接迁移方案,把这条链接迁移到 New process 上,整个过程对 Client 透明,不需要重新建链接,达到请求无损的平滑升级。\n正常流程 Client 发送请求 Request 到 MOSN MOSN 转发请求 Request 到 Server Server 回复响应 Response 到 MOSN MOSN 回复响应 Response 到 Client 上图简单介绍了一个请求的正常流程,我们后面需要迁移的是 TCP1 链接,也就是 Client 到 MOSN 的连接,MOSN 到 Server 的链接 TCP2 不需要迁移,因为 MOSN 访问 Server 是根据 LoadBalance 选择,我们可以主动控制断链建链。\n平滑升级流程 触发条件 有两个方式可以触发平滑升级流程:\n MOSN 对 SIGHUP 做了监听,发送 SIGHUP 信号给 MOSN 进程,通过 ForkExec 生成一个新的 MOSN 进程。 直接重新启动一个新 MOSN 进程。 为什么提供两种方式?最开始我们支持的是方法1,也就是 nginx 和 Envoy 使用的方式,这个在虚拟机或者容器内替换 MOSN 二级制来升级是可行的,但是我们的场景需要满足容器间的升级,所以需要新拉起一个容器,就需要重新启动一个新的 MOSN 进程来做平滑升级,所以后续又支持了方法2。容器间升级还需要 operator 的支持,本文不展开叙述。\n交互流程 首先,老的 MOSN 在启动最后阶段会启动一个协程运行 ReconfigureHandler() 函数监听一个 Domain Socket(reconfig.sock), 该接口的作用是让新的 MOSN 来感知是否存在老的 MOSN。\nfunc ReconfigureHandler() { l, err := net.Listen(\u0026#34;unix\u0026#34;, types.ReconfigureDomainSocket) for { uc, err := ul.AcceptUnix() _, err = uc.Write([]byte{0}) reconfigure(false) } } 触发平滑升级流程的两种方式最终都是启动一个新的 MOSN 进程,然后调用GetInheritListeners(),通过 isReconfigure() 函数来判断本机是否存在一个老的 MOSN(就是判断是否存在 reconfig.sock 监听),如果存在一个老的 MOSN,就进入迁移流程,反之就是正常的启动流程。\n// 保留了核心流程 func GetInheritListeners() ([]net.Listener, net.Conn, error) { if !isReconfigure() { return nil, nil, nil } l, err := net.Listen(\u0026#34;unix\u0026#34;, types.TransferListenDomainSocket) uc, err := ul.AcceptUnix() _, oobn, _, _, err := uc.ReadMsgUnix(buf, oob) file := os.NewFile(fd, \u0026#34;\u0026#34;) 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 套接字。\n// FileListener returns a copy of the network listener corresponding // to the open file f. // It is the caller\u0026#39;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 = \u0026amp;OpError{Op: \u0026#34;file\u0026#34;, Net: \u0026#34;file+net\u0026#34;, Source: nil, Addr: fileAddr(f.Name()), Err: err} } return } 这里的迁移和 Nginx 还是有一些区别,Nginx 是 fork 的方式,子进程自动就继承了 listen FD,MOSN 是新启动的进程,不存在父子关系,所以需要通过 sendMsg 的方式来传递。\n在进入迁移流程和 Listen 的迁移过程中,一共使用了两个 Domain Socket:\n reconfig.sock 是 Old MOSN 监听,用于 New MOSN 来判断是否存在 listen.sock 是 New MOSN 监听,用于 Old MOSN 传递 listen FD 两个 sock 其实是可以复用的,也可以用 reconfig.sock 进行 listen 的传递,由于一些历史原因搞了两个,后续可以优化为一个,让代码更精简易读。\n这儿再看看 Old MOSN 的处理,在收到 New MOSN 的通知之后,将进入reconfigure(false) 流程,首先就是调用 sendInheritListeners() 传递 listen FD,原因上面内容已经描述,最后调用 WaitConnectionsDone() 进入存量长链接的迁移流程。\n// 保留了核心流程 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()\nfunc TransferServer(handler types.ConnectionHandler) { l, err := net.Listen(\u0026#34;unix\u0026#34;, types.TransferConnDomainSocket) utils.GoWithRecover(func() { for { c, err := l.Accept() go transferHandler(c, handler, \u0026amp;transferMap) } }, nil) } Old MOSN 将通过 transferRead() 和 transferWrite() 进入最后的长链接迁移流程,下面主要分析这块内容。\n长连接迁移流程 首先先粗略看一下新请求的迁移流程。\n Client 发送请求到 MOSN MOSN 通过 domain socket(conn.sock) 把 TCP1 的 FD 和连接的状态数据发送给 New MOSN New MOSN 接受 FD 和请求数据创建新的 Conection 结构,然后把 Connection id 传给 MOSN,New MOSN 此时就拥有了 TCP1 的一个拷贝。 New MOSN 通过 LB 选取一个新的 Server,建立 TCP3 连接,转发请求到 Server Server 回复响应到 New MOSN New MOSN 通过 MOSN 传递来的 TCP1 的拷贝,回复响应到 Client 之前的 WaitConnectionsDone() 函数中,s.stopChan 已经关闭,在链接的 ReadLoop 中,将设置一个 [TransferTimeout, 2 * TransferTimeout] 的随机时间进入迁移流程,随机数主要是为了打散每个 Client 的 TCP 连接迁移时机,让迁移更平滑。\nfunc (c *connection) startReadLoop() { var transferTime time.Time for { select { case \u0026lt;-c.stopChan: if transferTime.IsZero() { if c.transferCallbacks != nil \u0026amp;\u0026amp; c.transferCallbacks() { randTime := time.Duration(rand.Intn(int(TransferTimeout.Nanoseconds()))) transferTime = time.Now().Add(TransferTimeout).Add(randTime) log.DefaultLogger.Infof(\u0026#34;[network] [read loop] transferTime: Wait %d Second\u0026#34;, (TransferTimeout+randTime)/1e9) } else { // set a long time, not transfer connection, wait mosn exit. transferTime = time.Now().Add(10 * TransferTimeout) log.DefaultLogger.Infof(\u0026#34;[network] [read loop] not support transfer connection, Connection = %d, Local Address = %+v, Remote Address = %+v\u0026#34;, c.id, c.rawConnection.LocalAddr(), c.RemoteAddr()) } } else { if transferTime.Before(time.Now()) { c.transfer() return } } 在等待一个随机时间之后,c.tranfer() 将进入迁移流程,c.notifyTransfer() 的作用是暂停 write 操作,在迁移 read 操作的时候,不能有 write 操作,因为两个进程 MOSN 同时都做 write,会导致数据错乱。\nfunc (c *connection) transfer() { c.notifyTransfer() id, _ := transferRead(c) c.transferWrite(id) } 然后进入的是 transferRead(),这个函数的作用就是把连接的 FD 和状态数据通过 conn.sock传递给 New MOSN,跟之前迁移 Listen FD 时方式一样,NEW MOSN 在成功处理之后会返回一个 ID,这个 ID 是 NEW MOSN 新建立的 Connection ID,这个 ID 后面会用到。\n// old mosn transfer readloop func transferRead(c *connection) (uint64, error) { unixConn, err := net.Dial(\u0026#34;unix\u0026#34;, types.TransferConnDomainSocket) 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, log.DefaultLogger) // recv ID id := transferRecvID(uc) return id, nil } 我们构造了一个简单的读迁移协议, 主要包括了 TCP 原始数据长度,TLS 数据长度,TCP 原始数据,TLS 数据。\n/** * transfer read protocol * header (8 bytes) + (readBuffer data) + TLS * * 0 4 8 * +-----+-----+-----+-----+-----+-----+-----+-----+ * | data length | TLS length | * +-----+-----+-----+-----+-----+-----+-----+-----+ * | data | * +-----+-----+-----+-----+-----+-----+-----+-----+ * | TLS | * +-----+-----+-----+-----+-----+-----+-----+-----+ * 现在看下 New MOSN 收到迁移请求之后的处理,它会针对每个迁移请求会启动一个协程运行 transferHandler() 函数, 函数会根据读取的协议判断是读迁移还是写迁移,我们这儿先介绍读迁移,New MOSN 会调用 transferNewConn 把 Old MOSN 传递过来的 FD 和数据包重新生成一个新的 Connection 结构体,并把生成的新的 connection ID 传递给 Old MOSN。\n此后,New MOSN 将从该 TCP 连接读取数据,开始正常的业务请求流程。\nfunc transferHandler(c net.Conn, handler types.ConnectionHandler, transferMap *sync.Map) { // recv type conn, err := transferRecvType(uc) if err != nil { log.DefaultLogger.Errorf(\u0026#34;[network] [transfer] [handler] transferRecvType error :%v\u0026#34;, err) return } if conn != nil { // transfer read // recv header + buffer dataBuf, tlsBuf, err := transferReadRecvData(uc) if err != nil { log.DefaultLogger.Errorf(\u0026#34;[network] [transfer] [handler] transferRecvData error :%v\u0026#34;, err) return } connection := transferNewConn(conn, dataBuf, tlsBuf, handler, transferMap) if connection != nil { transferSendID(uc, connection.id) } else { transferSendID(uc, transferErr) } } else { // transfer write // recv header + buffer id, buf, err := transferWriteRecvData(uc) if err != nil { log.DefaultLogger.Errorf(\u0026#34;[network] [transfer] [handler] transferRecvData error :%v\u0026#34;, err) } connection := transferFindConnection(transferMap, uint64(id)) if connection == nil { log.DefaultLogger.Errorf(\u0026#34;[network] [transfer] [handler] transferFindConnection failed, id = %d\u0026#34;, id) return } err = transferWriteBuffer(connection, buf) if err != nil { log.DefaultLogger.Errorf(\u0026#34;[network] [transfer] [handler] transferWriteBuffer error :%v\u0026#34;, err) return } } } 此后,Old MOSN 不再读取该 TCP1 连接上的数据,全部由 New MOSN 来读取 TCP1 上的数据并处理,对于新的请求,整个迁移过程就已经完成。\n残留响应迁移流程 大家想想为什么还有残留响应的迁移流程?因为多路复用协议,在之前读连接迁移流程的时候,TCP2 上还有之前残留的响应需要回复给Client,如果同时 MOSN 和 New MOSN 都进行 Write 操作 TCP1,数据可能会乱序,所以需要让New MOSN来统一处理之前 TCP2 上残留的响应。\n Server 回复残留的响应到 MOSN MOSN 把之前从 New MOSN 获取的 Connection id 和响应数据,通过 domain socket(conn.sock) 传递给 New MOSN New MOSN 通过 id 查询 TCP1 连接,回复响应到 Client 在 transferRead() 之后,就进入了 transferWrite() 阶段,该阶段会把需要 write 的数据包和之前 New MOSN 传回来的 Connection ID 一并传给 New MOSN。\n// old mosn transfer writeloop func transferWrite(c *connection, id uint64) error { unixConn, err := net.Dial(\u0026#34;unix\u0026#34;, types.TransferConnDomainSocket) 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) if err != nil { log.DefaultLogger.Errorf(\u0026#34;[network] [transfer] [write] transferWrite failed: %v\u0026#34;, err) return err } return nil } 我们构造了一个简单的写迁移协议, 主要包括了TCP原始数据长度, connection ID,TCP原始数据。\n/* * transfer write protocol * header (8 bytes) + (writeBuffer data) * * 0 4 8 * +-----+-----+-----+-----+-----+-----+-----+-----+ * | data length | connection ID | * +-----+-----+-----+-----+-----+-----+-----+-----+ * | data | * +-----+-----+-----+-----+-----+-----+-----+-----+ * **/ 在New MOSN的transferHandler()函数中,会判断出写迁移协议,然后 transferFindConnection() 函数通过 connection ID 找到 TCP1 连接,然后直接把数据写入即可。\n这儿需要说明一点,新请求Request的转发已经使用了 TCP3,TCP2 上只会有之前请求的 Response 响应,如果在整个迁移期间 2 * TransferTimeout 都没有回复响应,那么这个请求将会超时失败。\n连接状态数据 在连接迁移时,除了TCP FD的迁移,还有连接状态的迁移,这样New MOSN才知道怎样去初始化这个新的连接。\n主要有如下几个状态:\n读缓存\n表示在迁移时,已经从 TCP 读取的数据,还没有被应用层处理的数据。\n写数据\n在迁移之后,MOSN 收到的响应数据。\nTLS状态迁移\n如果是 TLS 加密请求,需要迁移 TLS 的状态,有如下状态需要迁移:\n 加密秘钥 Seq序列 读缓存数据(加密和未加密) cipher类型 TLS版本 type TransferTLSInfo struct { Vers uint16 CipherSuite uint16 MasterSecret []byte ClientRandom []byte ServerRandom []byte InSeq [8]byte OutSeq [8]byte RawInput []byte Input []byte } 总结 长连接的 FD 迁移是比较常规的操作,sendMsg 和 connection repair 都可以。\n在整个过程中最麻烦的是应用层数据的迁移,一般想法就是把应用层的数据结构等都迁移到新的进程,比如已经读取的协议 HEAD 等结构体,但这就导致你的迁移过程会很复杂,每个协议都需要单独处理。\nMOSN 的方案是把迁移放到了 IO 层,不关心应用层具体是什么协议,我们迁移最原始的 TCP 数据包,然后让 New MOSN 来 codec 这个数据包来拼装 HEAD 等结构体,这个过程是标准的处理流程了,这样就保证迁移对整个协议解析是透明的,只要这个协议是无状态的,这个迁移框架就可以自动支持。\n最后的残留响应迁移流程可能不太好理解,为什么不等所有响应完成之后才开始迁移,就不需要这个流程了?是因为在多路复用协议场景下,请求一直在发送,你不能总是找到一个时间点所有响应都完成了。\n反馈 关于该问题的讨论请见 Github Issue:MOSN smooth upgrade problem #866。\n","excerpt":"Service Mesh 中 Sidecar 运维一直是一个比较棘手的问题,数据平面的 Sidecar 升级是常有的事情,如何在升级 Sidecar(MOSN)的时候而不影响业务,对于存量的长连接如何 …","ref":"https://mosn.io/docs/products/structure/smooth-upgrade/","title":"MOSN 平滑升级原理解析"},{"body":"MOSN 多协议机制解析\n","excerpt":"MOSN 多协议机制解析","ref":"https://mosn.io/docs/products/structure/multi-protocol/","title":"MOSN 多协议机制解析"},{"body":"","excerpt":"","ref":"https://mosn.io/docs/user-guide/","title":"用户指南"},{"body":"","excerpt":"","ref":"https://mosn.io/docs/developer-guide/operations-api/","title":"运维 API"},{"body":"Featuregate 介绍 在 MOSN 中,存在一些功能需要在启动时决定是否开启的,为了满足这种需求,MOSN 推出了 featuregate(功能开关)的能力。\nFeaturegate 描述了一组 MOSN 中需要开启 / 关闭的 feature 状态,每个 feature 都有自己默认的状态,每个 MOSN 版本支持的 feature、feature 默认的版本都有所不同;featuregate 的描述用一个字符串表示,按照${feature}=${bool}的方式,用逗号进行分割:\n// 通用模版 ./mosn start -c ${config path} -f ${feature gates description} // 示例 ./mosn start -c mosn_config.json -f \u0026quot;auto_config=true,XdsMtlsEnable=true\u0026quot; Featuregate 不仅仅是提供了一种功能切换的能力,同时也提供了一种可扩展的开发机制,基于 MOSN 进行二次开发时,可以使用 featuregate 做到如下的功能:\n 功能切换的能力,可以控制某个 feature 的开启 / 关闭 feature 之间的依赖关系管理,包括 feature 之间的启动顺序依赖、开启 / 关闭状态的依赖等 举例说明,基于 MOSN 实现两个 feature,分别为 A 和 B,需要在 A 初始化完成以后,B 会使用 A 初始化的结果进行初始化,这就是 B 依赖 A,当 feature A 处于 Disable 状态时,B 显然也会处于 Disable 或者需要作出对应的“降级”; feature gate 框架提供了一种简单的方式,可以更加专注于 feature 的开发,而不用去管理对应的启动与依赖 基于 featuregate 的框架,在 MOSN 中进行不同 feature 的二次开发,是 featuregate 框架最主要的目的。\n基于 featuregate 进行开发 Featuregate 实现 首先,我们来看一下,featuregate 框架提供了哪些接口:\n// 返回一个 Feature 当前的状态,true 表示 enable,false 表示 disable func Enabled(key Feature) bool // “订阅”一个 Feature,并且返回其订阅完成以后的状态。 // 当订阅的 Feature 初始化完成以后,会返回其是否 Enable。 // 如果订阅的 Feature 是 Disable 的,会直接返回 false;如果在订阅的 timeout 期间,Feature 依然没有 // 初始化完成,那么会返回订阅超时的错误,如果 timeout 小于等于 0,则没有订阅超时 func Subscribe(key Feature, timeout time.Duration) (bool, error) // 设置 feature gates 的状态,value 为一个完整的 feature gates 描述 func Set(value string) error // 设置 feature gates 的状态,其中 map 的 key 为 feature 的 key,value 是期望设置的 feature 状态 func SetFromMap(m map[string]bool) error // 新注册一个 feature 到 feature gate 中 func AddFeatureSpec(key Feature, spec FeatureSpec) error // 设置一个 feature 的状态 func SetFeatureState(key Feature, enable bool) error // 开启初始化 feature func StartInit() // 等待所有的 feature 初始化结束 func WaitInitFinsh() error 这其中,StartInit 和 WaitInitFinsh 是由 MOSN 框架进行调用,基于 MOSN 进行二次开发时无须关注和调用;通常情况下,Set 和 SetFromMap 也无须关注。所有的上述接口,都是由框架下默认的一个不可导出的全局 featuregate 对象暴露,在没有极为特殊需求的场景下(如编写单元测试),不需要额外生成 FeatureGate 对象,使用默认的即可。\n接下来,我们看一下 featuregate 的实现:\ntype knownFeatureSpec struct { FeatureSpec once sync.Once channel chan struct{} } type Feature string type FeatureGate struct { // lock guards writes to known, enabled, and reads/writes of closed lock sync.Mutex known map[Feature]*knownFeatureSpec // inited is set to true when StartInit is called. inited bool wg sync.WaitGroup once sync.Once } Featuregate 包含了一个 map,用于记录所有被支持的 feature;一个inited状态标,表示 featuregate 是否已经完成了初始化;once用于确保 featuregate 的初始化只执行一次,WaitGroup则用于同步 feature 初始化的结果;一个Mutex用于并发保护。 按照 featuregate 的设计,不同的 feature 是可以通过Add的方式新增,以及不同的Set方法改变状态的,而不同 feature 的初始化Init函数都会统一执行,因此一旦执行完Init,则不再允许新增 feature、修改 feature 状态;因此我们需要一个inited的标记来记录这个行为。 knownFeatureSpec是一个不可导出的结构体,用于对表示不同 feature 的FeatureSpec封装,其中的once和channel均是用于 featuregate 中订阅和初始化使用,在此不做详细说明。 下面,我们来看一下FeatureSpec的定义,这也是我们基于 featuregate 框架进行开发的核心数据结构。\ntype prerelease string const ( // Values for PreRelease. Alpha = prerelease(\u0026#34;ALPHA\u0026#34;) Beta = prerelease(\u0026#34;BETA\u0026#34;) GA = prerelease(\u0026#34;\u0026#34;) ) type FeatureSpec interface { // Default is the default enablement state for the feature Default() bool // LockToDefault indicates that the feature is locked to its default and cannot be changed LockToDefault() bool // SetState sets the enablement state for the feature SetState(enable bool) // State indicates the feature enablement State() bool // InitFunc used to init process when StartInit is invoked InitFunc() // PreRelease indicates the maturity level of the feature PreRelease() prerelease } prerelease 是不可导出的定义,有三个约定的导出变量可以使用,相当于传统语言的 Enum 类型,用于描述 feature 的信息,没有明确的作用 FeatureSpec可以自行实现,同时多数情况下可以用框架实现的BaseFeatureSpec,或者基于BaseFeatureSpec进行封装;如注释描述,通常情况下只需要额外封装实现一个InitFunc函数即可 // BaseFeatureSpec is a basic implementation of FeatureSpec. // Usually, a feature spec just need an init func. type BaseFeatureSpec struct { // 默认状态 DefaultValue bool // 是否可修改状态,如果为 true,说明这个 feature 只能保持默认状态 // 一般情况下设置这个为 true 的时候,default 也是 true // 这种 feature 主要会用于做为其他 feature 的“基础依赖” IsLockedDefault bool PreReleaseValue prerelease stateValue bool // stateValue shoule be setted by SetState inited int32 // inited cannot be setted } Featuregate 的使用 了解了 featuregate 的基本实现,就可以考虑使用 featuregate 进行基本的编程扩展了。下面会介绍几种 featuregate 的使用场景,以及如何编写 feature。\n1. 基本的“全局”开关 对于 feature 切换最基本的使用场景,就是使用一个类似“全局变量”进行控制,通过if条件判断执行不同的逻辑。使用 featuregate 框架实现这种能力,可以把控制 feature 切换的参数全部统一到启动参数中。\nvar featureName featuregate.Feature = \u0026#34;simple_feature\u0026#34; func init() { fs := \u0026amp;featuregate.BaseFeatureSpec{ DefaultValue: true } featuregate.AddFeatureSpec(featureName,fs) } func myfunc() { if featuregate.Enable(featureName) { dosth() } else { dosth2() } } 2. 需要进行“初始化”操作 通过封装扩展 InitFunc 函数,让相关的初始化工作在 MOSN 启动时统一完成,如果 feature 处于 disable 状态,那么 InitFunc 不会执行。\nvar featureName featuregate.Feature = \u0026#34;init_feature\u0026#34; type MyFeature struct { *BaseFeatureSpec } func (f *MyFeature) InitFunc() { doInit() } // 其他的类似 1. 3. Feature 之间存在依赖关系 这个功能是 featuregate 框架提供的最重要的能力,可以方便的解决下面的场景:\n 假设我们存在四个独立的组件(feature),分别是 A、B、C,D B 和 C 的启动都依赖于 A,即首先要 A 启动完成,然后 B 和 C 才能启动完成;D 依赖于 B,必须 B 启动完成,D 才可以启动 如果 A 没有启动,B 就不能启动,而 C 存在一种降级方案,依然可以继续工作 四个 feature 在 featuregate 框架下可各自实现,如下 var FeatureA featuregate.Feature = \u0026#34;A\u0026#34; func init() { fs := \u0026amp;featuregate.BaseFeatureSpec{ DefaultValue: true } featuregate.AddFeatureSpec(FeatureA,fs) } var FeatureB featuregate.Feature = \u0026#34;B\u0026#34; type FB struct { *BaseFeatureSpec } func (f *FB) InitFunc() { enabled, err := featuregate.Subscribe(FeatureA, 5 * time.Second) if err != nil || !enabled { f.SetState(false) // 如果 FeatureA 没有开启,则 FeatureB 也不开启 } } var FeatureC featuregate.Feature = \u0026#34;C\u0026#34; type FC struct { *BaseFeatureSpec mode int32 } func (f *FC) InitFunc() { enabled, err := featuregate.Subscribe(FeatureA, 5 * time.Second) if err != nil || !enabled { f.mode = -1 // 降级模式 return } if enabled { f.mode = 1 // 正常模式 } } func (f *FC) MyFunc() { if f.mode == 1 { dosth() } else if f.mode == -1 { dosth2() } } type FeatureD featuregate.Feature = \u0026#34;D\u0026#34; type FD struct { *BaseFeatureSpec } func (f *FD) InitFunc() { enabled, err := featuregate.Subscribe(FeatureB, 0) // 不超时,一定要等待 B 结束 if err != nil || !enabled { return } f.Start() } func (f *FD) Start() { } FAQ 为什么不使用配置的方式,而要使用 featuregate? 配置文件需要进行解析,featuregate 更有利于扩展能力的实现 有的 feature 需要判断的时机,比配置文件解析要早,甚至可能影响配置解析的逻辑 ","excerpt":"Featuregate 介绍 在 MOSN 中,存在一些功能需要在启动时决定是否开启的,为了满足这种需求,MOSN 推出了 featuregate(功能开关)的能力。\nFeaturegate 描述了一 …","ref":"https://mosn.io/docs/developer-guide/featuregate-introduce/","title":"Featuregate 介绍"},{"body":"","excerpt":"","ref":"https://mosn.io/docs/developer-guide/","title":"开发者指南"},{"body":"","excerpt":"","ref":"https://mosn.io/docs/open-source/","title":"开源共建"},{"body":"Sidecar 模式是 Service Mesh 中习惯采用的模式,是容器设计模式的一种,在 Service Mesh 出现之前该模式就一直存在,本文将为您讲解 Sidecar 模式。\n什么是 Sidecar 模式 将应用程序的功能划分为单独的进程可以被视为 Sidecar 模式。如图所示,Sidecar 模式允许您在应用程序旁边添加更多功能,而无需额外第三方组件配置或修改应用程序代码。\n就像连接了 Sidecar 的三轮摩托车一样,在软件架构中, Sidecar 连接到父应用并且为其添加扩展或者增强功能。Sidecar 应用与主应用程序松散耦合。它可以屏蔽不同编程语言的差异,统一实现微服务的可观察性、监控、日志记录、配置、断路器等功能。\n使用 Sidecar 模式的优势 Sidecar 模式具有以下优势:\n 将与应用业务逻辑无关的功能抽象到共同基础设施降低了微服务代码的复杂度。\n 因为不再需要编写相同的第三方组件配置文件和代码,所以能够降低微服务架构中的代码重复度。\n 降低应用程序代码和底层平台的耦合度。\n Sidecar 模式如何工作 Sidecar 是容器应用模式的一种,也是在 Service Mesh 中发扬光大的一种模式,详见 Service Mesh 架构解析,其中详细描述使用了节点代理和 Sidecar 模式的 Service Mesh 架构。\n使用 Sidecar 模式部署服务网格时,无需在节点上运行代理,但是集群中将运行多个相同的 Sidecar 副本。在 Sidecar 部署方式中,每个应用的容器旁都会部署一个伴生容器,这个容器称之为 Sidecar 容器。Sidecar 接管进出应用容器的所有流量。在 Kubernetes 的 Pod 中,在原有的应用容器旁边注入一个 Sidecar 容器,两个容器共享存储、网络等资源,可以广义的将这个包含了 Sidecar 容器的 Pod 理解为一台主机,两个容器共享主机资源。\n","excerpt":"Sidecar 模式是 Service Mesh 中习惯采用的模式,是容器设计模式的一种,在 Service Mesh 出现之前该模式就一直存在,本文将为您讲解 Sidecar 模式。 …","ref":"https://mosn.io/docs/products/structure/sidecar-pattern/","title":"Sidecar 模式"},{"body":"","excerpt":"","ref":"https://mosn.io/docs/open-source/contributing-source-code/","title":"贡献源码须知"},{"body":"","excerpt":"","ref":"https://mosn.io/docs/open-source/contributing-documents/","title":"贡献文档须知"},{"body":"","excerpt":"","ref":"https://mosn.io/docs/open-source/contribution-process/","title":"贡献流程"},{"body":"","excerpt":"","ref":"https://mosn.io/docs/user-guide/multilingual/","title":"多语言"},{"body":"","excerpt":"","ref":"https://mosn.io/docs/user-guide/manual-faq/","title":"参考手册/FAQ"},{"body":"MOSN 官方文档。\n","excerpt":"MOSN 官方文档。","ref":"https://mosn.io/docs/","title":"MOSN 文档"},{"body":"","excerpt":"","ref":"https://mosn.io/blog/news/","title":"新闻"},{"body":"","excerpt":"","ref":"https://mosn.io/docs/products/report/releases/","title":"版本发布"},{"body":"","excerpt":"","ref":"https://mosn.io/blog/posts/","title":"分享"},{"body":"","excerpt":"","ref":"https://mosn.io/blog/code/","title":"源码解析"},{"body":"1. 前言 MOSN(Modular Open Smart Network)是一款主要使用 Go 语言开发的云原生网络代理平台,由蚂蚁集团开源并经过双11大促几十万容器的生产级验证。 MOSN 为服务提供多协议、模块化、智能化、安全的代理能力,融合了大量云原生通用组件,同时也可以集成 Envoy 作为网络库,具备高性能、易扩展的特点。MOSN 可以和 Istio 集成构建 Service Mesh,也可以作为独立的四、七层负载均衡,API Gateway、云原生 Ingress 等使用。\nMOSN 作为数据面,整体 NET/IO、Protocol、Stream、Proxy 四个层次组成,其中\n NET/IO 用于底层的字节流传输 Protocol 用于协议的 decode/encode Stream 用于封装请求和响应,在一个 conn 上做连接复用 Proxy 做 downstream 和 upstream 之间 stream 的转发 那么 MOSN 是如何工作的呢?下图展示的是使用 Sidecar 方式部署运行 MOSN 的示意图,您可以在配置文件中设置 MOSN 的上游和下游协议,协议可以在 HTTP、HTTP2.0、以及SOFA RPC 等中选择。 以上内容来自官网 https://mosn.io/\n2. RPC 场景下 MOSN 的工作机制 RPC 场景下 MOSN 的工作机制示意图如下 我们简单理解一下上面这张图的意义:\n Server 端 MOSN 会将自身 ingress 的协议端口写入到注册中心 Client 端 MOSN 会从注册中心订阅地址列表,第一次订阅也会返回全量地址列表,端口号是 Server 端 ingress 绑定的端口号 注册中心会实时推送地址列表变更到 Client 端(全量) Client 端发起rpc 调用时,请求会被 SDK 打到本地 Client 端 MOSN 的 egress 端口上 Client 端 MOSN 将 RPC 请求通过网络转发,将流量通过负载均衡转发到某一台 Server 端 MOSN 的 ingress 端口处理 最终到了 Server 端 ingress listener,会转发给本地 Server 应用 最终会根据原来的 TCP 链路返回 3. 全局视野下的 MOSN 工作流程 为了方便大家理解,我将以上时序图内容进行拆分,我们一一攻破。\n3.1 建立连接 MOSN 在启动期间,会暴露本地 egress 端口接收 Client 的请求。MOSN 会开启 2 个协程,分别死循环去对 TCP 进行读取和写处理。MOSN 会通过读协程获取到请求字节流,进入 MOSN 的协议层处理。\n// 代码路径 mosn.io/mosn/pkg/network/connection.go func (c *connection) Start(lctx context.Context) { // udp downstream connection do not use read/write loop if c.network == \u0026quot;udp\u0026quot; \u0026amp;\u0026amp; c.rawConnection.RemoteAddr() == nil { return } c.startOnce.Do(func() { // UseNetpollMode = false if UseNetpollMode { c.attachEventLoop(lctx) } else { // 启动读/写循环 c.startRWLoop(lctx) } }) } func (c *connection) startRWLoop(lctx context.Context) { // 标记读循环已经启动 c.internalLoopStarted = true utils.GoWithRecover(func() { // 开始读操作 c.startReadLoop() }, func(r interface{}) { c.Close(api.NoFlush, api.LocalClose) }) // 省略。。。 } 3.2 Protocol 处理 Protocol 作为多协议引擎层,对数据包进行检测,并使用对应协议做 decode/encode 处理。MOSN 会循环解码,一旦收到完整的报文就会创建与其关联的 xstream,用于保持 tcp 连接用于后续响应。\n// 代码路径 mosn.io/mosn/pkg/stream/xprotocol/conn.go func (sc *streamConn) Dispatch(buf types.IoBuffer) { // decode frames for { // 协议 decode,比如 dubbo、bolt 协议等 frame, err := sc.protocol.Decode(streamCtx, buf) if frame != nil { // 创建和请求 frame 关联的 xstream,用于保持 tcp 连接用于后续响应 sc.handleFrame(streamCtx, xframe) } } } func (sc *streamConn) handleFrame(ctx context.Context, frame api.XFrame) { switch frame.GetStreamType() { case api.Request: // 创建和请求 frame 关联的 xstream,用于保持 tcp 连接用于后续响应,之后进入 proxy 层 sc.handleRequest(ctx, frame, false) } } func (sc *streamConn) handleRequest(ctx context.Context, frame api.XFrame, oneway bool) { // 创建和请求 frame 关联的 xstream serverStream := sc.newServerStream(ctx, frame) // 进入 proxy 层并创建 downstream serverStream.receiver = sc.serverCallbacks.NewStreamDetect(serverStream.ctx, sender, span) serverStream.receiver.OnReceive(serverStream.ctx, frame.GetHeader(), frame.GetData(), nil) } 3.3 Proxy 层处理 proxy 层负责 filter 请求/响应链、路由匹配、负载均衡最终将请求转发到集群的某台机器上。\n3.3.1 downStream 部分 // 代码路径 mosn.io/mosn/pkg/proxy/downstream.go func (s *downStream) OnReceive(ctx context.Context, headers types.HeaderMap, data types.IoBuffer, trailers types.HeaderMap) { s.downstreamReqHeaders = headers // filter 请求/响应链、路由匹配、负载均衡 phase = s.receive(s.context, id, phase) } func (s *downStream) receive(ctx context.Context, id uint32, phase types.Phase) types.Phase { for i := 0; i \u0026lt;= int(types.End-types.InitPhase); i++ { s.phase = phase switch phase { // downstream filter 相关逻辑 case types.DownFilter: s.printPhaseInfo(phase, id) s.tracks.StartTrack(track.StreamFilterBeforeRoute) s.streamFilterChain.RunReceiverFilter(s.context, api.BeforeRoute, s.downstreamReqHeaders, s.downstreamReqDataBuf, s.downstreamReqTrailers, s.receiverFilterStatusHandler) s.tracks.EndTrack(track.StreamFilterBeforeRoute) if p, err := s.processError(id); err != nil { return p } phase++ // route 相关逻辑 case types.MatchRoute: s.printPhaseInfo(phase, id) s.tracks.StartTrack(track.MatchRoute) s.matchRoute() s.tracks.EndTrack(track.MatchRoute) if p, err := s.processError(id); err != nil { return p } phase++ // 在集群中选择一个机器、包含cluster和loadblance case types.ChooseHost: s.printPhaseInfo(phase, id) s.tracks.StartTrack(track.LoadBalanceChooseHost) // 这里很重要,在选中一个机器之后,这里upstreamRequest对象有两个作用 // 1. 这里通过持有downstream保持着对客户端app的tcp引用,用来接收请求 // 2. 转发服务端tcp引用,转发客户端app请求以及响应服务端response时的通知 s.chooseHost(s.downstreamReqDataBuf == nil \u0026amp;\u0026amp; s.downstreamReqTrailers == nil) s.tracks.EndTrack(track.LoadBalanceChooseHost) if p, err := s.processError(id); err != nil { return p } phase++ } } } 3.3.2 upStream 部分 至此已经选中一台服务端的机器,开始准备转发。\n// 代码路径 mosn.io/mosn/pkg/proxy/upstream.go func (r *upstreamRequest) appendHeaders(endStream bool) { if r.downStream.oneway { _, streamSender, failReason = r.connPool.NewStream(r.downStream.context, nil) } else { // 会使用 ChooseHost 中选中的机器 host 创建 sender,xstream 是客户端的流对象 _, streamSender, failReason = r.connPool.NewStream(r.downStream.context, r) } } 接下来会到达 conn.go 的 handleFrame 的 handleResponse 方法,此时 handleResponse 方法继续调用 downStream 的 receiveData 方法接收数据。\n//代码路径 mosn.io/mosn/pkg/stream/xprotocol/conn.go func (sc *streamConn) handleFrame(ctx context.Context, frame api.XFrame) { switch frame.GetStreamType() { case api.Response: // 调用 downStream 的 receiveData 方法接收数据 // 因为 mosn 在转发之前修改了请求id,因此会重新 encode 请求 sc.handleResponse(ctx, frame) } } 一旦准备好转发就会通过 upstreamRequest 选择的下游主机直接发送 write 请求,请求的协程此时会被阻塞。\n// 代码路径 mosn.io/mosn/pkg/stream/xprotocol/stream.go func (s *xStream) endStream() { defer func() { if s.direction == stream.ServerStream { s.DestroyStream() } }() if log.Proxy.GetLogLevel() \u0026gt;= log.DEBUG { log.Proxy.Debugf(s.ctx, \u0026quot;[stream] [xprotocol] connection %d endStream, direction = %d, requestId = %v\u0026quot;, s.sc.netConn.ID(), s.direction, s.id) } if s.frame != nil { // replace requestID s.frame.SetRequestId(s.id) // 因为 mosn 在转发之前修改了请求 id,因此会重新 encode 请求 buf, err := s.sc.protocol.Encode(s.ctx, s.frame) if err != nil { log.Proxy.Errorf(s.ctx, \u0026quot;[stream] [xprotocol] encode error:%s, requestId = %v\u0026quot;, err.Error(), s.id) s.ResetStream(types.StreamLocalReset) return } tracks := track.TrackBufferByContext(s.ctx).Tracks tracks.StartTrack(track.NetworkDataWrite) // 一旦准备好转发就会通过upstreamRequest选择的下游主机直接发送 write 请求,请求的协程此时会被阻塞 err = s.sc.netConn.Write(buf) tracks.EndTrack(track.NetworkDataWrite) } } } 3.4 准备将响应写回客户端 接下来客户端 xstream 将通过读协程接收响应的字节流,proxy.go 的 OnData 方法作为 proxy 层的数据接收点。\n// 代码位置 mosn.io/mosn/pkg/proxy/proxy.go func (p *proxy) OnData(buf buffer.IoBuffer) api.FilterStatus { // 这里会做两件事 // 1. 调用 protocol 层进行decode // 2. 完成后通知upstreamRequest对象,唤醒downstream阻塞的协程 p.serverStreamConn.Dispatch(buf) return api.Stop } // 代码位置 mosn.io/mosn/pkg/proxy/upstream.go func (r *upstreamRequest) OnReceive(ctx context.Context, headers types.HeaderMap, data types.IoBuffer, trailers types.HeaderMap) { // 结束当前stream r.endStream() // 唤醒 r.downStream.sendNotify() } downstream 被唤醒处理收到的响应,重新替换回正确的请求ID,并调用 protocol 层重新编码成字节流写回客户端,最后销毁请求相关的资源,流程执行完毕。\n// 比如我的 demo 是 dubbo 协议 func encodeFrame(ctx context.Context, frame *Frame) (types.IoBuffer, error) { // 1. fast-path, use existed raw data if frame.rawData != nil { // 1.1 replace requestId binary.BigEndian.PutUint64(frame.rawData[IdIdx:], frame.Id) // hack: increase the buffer count to avoid premature recycle frame.data.Count(1) return frame.data, nil } // alloc encode buffer frameLen := int(HeaderLen + frame.DataLen) buf := buffer.GetIoBuffer(frameLen) // encode header buf.WriteByte(frame.Magic[0]) buf.WriteByte(frame.Magic[1]) buf.WriteByte(frame.Flag) buf.WriteByte(frame.Status) buf.WriteUint64(frame.Id) buf.WriteUint32(frame.DataLen) // encode payload buf.Write(frame.payload) return buf, nil } 4. 总结 本文以工作中非常常见的一个思路为出发点,详细描述了 MOSN 内部网络转发的详细流程,可以帮助小伙伴加深对 MOSN 的理解。MOSN 是一款非常优秀的开源产品, MOSN 支持多种网络协议(如HTTP/2, gRPC, Dubbo等)并且能够很容易地增加对新协议的支持;MOSN 提供了丰富的流量治理功能,例如限流、熔断、重试、 负载均衡等;MOSN 在性能方面进行了大量优化,比如内存零拷贝、自适应缓冲区、连接池、协程池等,这些都有助于提升其在高并发环境下的表现。除此之外 MOSN 在连接管理方面,MOSN 设计了多协议连接池;在内存管理方面,MOSN 在 sync.Pool 之上封装了一层资源对的注册管理模块,可以方便的扩展各种类型的 对象进行复用和管理。总的来说,MOSN 的设计体现了可扩展性、高性能、安全性、以及对现代云环境的适应性等多方面的考虑。对于开发者来说,深入研究MOSN的 代码和架构,无疑可以学到很多关于高性能网络编程和云原生技术的知识。\n MOSN 官网:https://mosn.io/ MOSN Github:https://github.com/mosn/mosn ","excerpt":"1. 前言 MOSN(Modular Open Smart Network)是一款主要使用 Go 语言开发的云原生网络代理平台,由蚂蚁集团开源并经过双11大促几十万容器的生产级验证。 MOSN 为服务 …","ref":"https://mosn.io/blog/posts/mosn_workflow/","title":"以一次 RPC 请求为例探索 MOSN的 工作流程"},{"body":"本文基于 MOSN V1.6.0版本的源码基础上进行整理。该版本对比之前 V0.4.0 版本启动逻辑有比较大的变化。其中比较明显的差异是该版本新增了 StageManager 结构,该结构对 MOSN 的生命周期进行封装并加以维护,使得 MOSN 从启动到停止过程中每个逻辑更易于维护和扩展。\nMOSN 启动入口 MOSN 利用 cli 组件 (github.com/urfave/cli)来实现命令行的控制。启动之后默认执行 cmdStart 命令,之后就进入下面的启动逻辑。\n在 control.go 文件中 cmdStart.Action 方法是整个 MOSN 启动的入口方法。首先调用 NewMosn() 这个方法只是返回了一个空的 Mosn 对象,该对象代表着 Mosn 应用,该对象定义如下:\ntype Mosn struct { isFromUpgrade bool // hot upgrade from old MOSN \tUpgrade UpgradeData Clustermanager types.ClusterManager RouterManager types.RouterManager Config *v2.MOSNConfig // internal data \tservers []server.Server xdsClient *istio.ADSClient } 场景管理器 下面的逻辑中创建了一个叫 StageManager(场景管理器)的对象,这个对象是用来管理 MOSN 的生命周期,后面的逻辑都是围绕着这个对象来编码的,定义如下:\n// stagemanagr/stage_manager.go // StageManager is used to controls service life stages. type StageManager struct { lock sync.Mutex state State exitCode int stopAction StopAction data Data //保存了app不同生命周期阶段所使用的数据 \tapp Application // Application interface app其实就是MOSN \twg sync.WaitGroup //以下是不同生命周期对应的处理函数 -- start \tparamsStages []func(*cli.Context) //参数准备阶段 \tinitStages []func(*v2.MOSNConfig) //初始化场景 \tpreStartStages []func(Application) //启动之前 \tstartupStages []func(Application) //启动 \tafterStartStages []func(Application) //启动之后 \tbeforeStopStages []func(StopAction, Application) error //停止之前 \tgracefulStopStages []func(Application) error //优雅停止 \tafterStopStages []func(Application) //停止之后处理 //生命周期阶段 --- end \tonStateChangedCallbacks []func(State) upgradeHandler func() error // old server: send listener/config/old connections to new server \tnewServerC chan bool } // Data contains objects used in stages type Data struct { // ctx contains the start parameters \tctx *cli.Context // config path represents the config file path, \t// will create basic config from it and if auto config dump is set, \t// new config data will write into this path \tconfigPath string // basic config, created after parameters parsed stage \tconfig *v2.MOSNConfig } 上面代码增加了注释,可以看到 stageManager 的生命周期包含 参数准备、初始化、启动之前处理、启动、启动之后处理、停止之前处理、优雅停止、停止之后处理等几个阶段。每个阶段对应的是一个函数的数组,也就是说每个阶段的处理可以有多个处理函数。\n这里可以好好的看一下 stage_manager.go 的源码,里面定义了11种场景的状态和2种额外的场景 (stage_manager.go 原文件头部有大块的注释里把场景的含义描述的比较清楚),那么当状态发生变化的时候,就会调用上文提到的场景管理器维护的回调函数。\nApplication 同时还定义了一个 Application 的接口,是对一个应用进行抽象,其中之前说的 Mosn 对象就是 Application 的一个实现。 Application 被 stageManager 所管理,Application 本身定义了生命周期 ( application 不同的生命周期,会触发 stage 场景的切换),周期包括:初始化,启动,停止。那么 stageManager 根据这些周期的变化,同时回调切换场景的函数。\n阶段小结 到这里先不着急往下看启动逻辑,我们先总结一下目前掌握的内容及背后的设计思路,这样后面梳理逻辑会变得很轻松。 如上图所示,Application 应用(其实就是 Mosn 本身)包含若干方法:初始化、启动、停止。这些方法调用后让应用进入不同的生命周期,同时场景管理器 (StageManager)维护的场景状态也对应发生改变,应用生命周期与场景二者是有关联关系的。\n那么先不看源码只凭借猜测,到底是应用的生命周期发生变化后触发场景改变状态;还是先触发场景改变状态进而触发应用改变生命周期呢?我的猜测是这样,因为上文提到场景管理器(StageManager)用来管理应用的生命周期,StageManager 结构体中也包含 Application 对象。那么很大的可能是先由场景管理来触发状态改变,再触发应用对应的方法来改变生命周期。\n其实因为场景的状态有11个粒度要比应用的生命周期粒度更细,从这点上来看也只可能场景状态(细粒度)切换同时调用应用的方法切换生命周期(粗粒度),而反之行不通(粗粒度的一方无法识别什么时候调用细粒度一方)。\n既然我猜测是 StageManager 场景来控制切换,那么肯定有对应的方法提供场景切换。这个时候我们再来看代码,发现确实存在这样的方法来证明我的猜测。下面是切换场景的方法:\n详细分析 场景管理器的启动 我们继续看一下 stage_manager.go 的 Run() 方法,可以清楚的看到场景管理器的启动逻辑,就是调用了不同子阶段,每个阶段都会有切换状态的逻辑,在方法的最后逻辑可以看到,启动之后设置场景状态为 Running。\n// Run until the application is started func (stm *StageManager) Run() { // 1: parser params \tstm.runParamsParsedStage() // 2: init \tstm.runInitStage() // 3: pre start \tstm.runPreStartStage() // 4: run \tstm.runStartStage() // 5: after start \tstm.runAfterStartStage() stm.SetState(Running) } 可以看到这个 Run() 方法执行了参数解析、初始化、启动前处理、启动、启动后处理等方法。我猜测每个方法一定是执行前文说的每个阶段对应的回调函数数组。找其中一个方法看一下逻辑,果然如我猜测一样。\nfunc (stm *StageManager) runParamsParsedStage() { st := time.Now() //设置场景状态 \tstm.SetState(ParamsParsed) //果然和猜测一样,遍历回调函数的数组并调用 \tfor _, f := range stm.paramsStages { f(stm.data.ctx) } // after all registered stages are completed \tstm.data.config = configmanager.Load(stm.data.configPath) log.StartLogger.Infof(\u0026#34;parameters parsed stage cost: %v\u0026#34;, time.Since(st)) } 可以说 Run() 方法作用就是场景管理器 stageManager 来启动应用 Application,那这个 Run() 方法在什么地方被调用呢?梳理一下代码,调用链路是:control.go cmdStart.Action(就是前面提到的程序启动的入口)-\u0026gt;stm.RunAll()-\u0026gt;stm.Run()。这样从上文提到 MOSN 命令行启动到这个 Run() 方法就串起来了。\n其中还有一个比较重要的方法是 stm.RunAll() ,该方法是场景管理器中完整的场景都会执行一遍: Run 是启动,之后 WaitFinish 就等待 server 停止,最后是 Stop 场景。每个过程都会调用若干场景切换。\n// run all stages func (stm *StageManager) RunAll() { // start to work \tstm.Run() // wait server finished \tstm.WaitFinish() // stop working \tstm.Stop() } 启动逻辑详解 好了现在我们回到最开始的 control.go cmdStart.Action 逻辑。\n159 Action: func(c *cli.Context) error { // 创建Application 160 app := mosn.NewMosn() // 创建stagemanager场景管理器 161 stm := stagemanager.InitStageManager(c, c.String(\u0026#34;config\u0026#34;), app) 162 // if needs featuregate init in parameter stage or init stage 163 // append a new stage and called featuregate.ExecuteInitFunc(keys...) 164 // parameter parsed registered 165 stm.AppendParamsParsedStage(ExtensionsRegister) 166 stm.AppendParamsParsedStage(DefaultParamsParsed) 167 // initial registered 168 stm.AppendInitStage(func(cfg *v2.MOSNConfig) { 169 drainTime := c.Int(\u0026#34;drain-time-s\u0026#34;) 170 server.SetDrainTime(time.Duration(drainTime) * time.Second) 171 // istio parameters 172 serviceCluster := c.String(\u0026#34;service-cluster\u0026#34;) 173 serviceNode := c.String(\u0026#34;service-node\u0026#34;) 174 serviceType := c.String(\u0026#34;service-type\u0026#34;) 175 serviceMeta := c.StringSlice(\u0026#34;service-meta\u0026#34;) 176 metaLabels := c.StringSlice(\u0026#34;service-lables\u0026#34;) 177 clusterDomain := c.String(\u0026#34;cluster-domain\u0026#34;) 178 podName := c.String(\u0026#34;pod-name\u0026#34;) 179 podNamespace := c.String(\u0026#34;pod-namespace\u0026#34;) 180 podIp := c.String(\u0026#34;pod-ip\u0026#34;) 181 182 if serviceNode != \u0026#34;\u0026#34; { 183 istio1106.InitXdsInfo(cfg, serviceCluster, serviceNode, serviceMeta, metaLabels) 184 } else { 185 if istio1106.IsApplicationNodeType(serviceType) { 186 sn := podName + \u0026#34;.\u0026#34; + podNamespace 187 serviceNode = serviceType + \u0026#34;~\u0026#34; + podIp + \u0026#34;~\u0026#34; + sn + \u0026#34;~\u0026#34; + clusterDomain 188 istio1106.InitXdsInfo(cfg, serviceCluster, serviceNode, serviceMeta, metaLabels) 189 } else { 190 log.StartLogger.Infof(\u0026#34;[mosn] [start] xds service type is not router/sidecar, use config only\u0026#34;) 191 istio1106.InitXdsInfo(cfg, \u0026#34;\u0026#34;, \u0026#34;\u0026#34;, nil, nil) 192 } 193 } 194 }) 195 stm.AppendInitStage(mosn.DefaultInitStage) 196 stm.AppendInitStage(func(_ *v2.MOSNConfig) { 197 // set version and go version 198 metrics.SetVersion(Version) 199 metrics.SetGoVersion(runtime.Version()) 200 admin.SetVersion(Version) 201 }) 202 stm.AppendInitStage(holmes.Register) 203 // pre-startup 204 stm.AppendPreStartStage(mosn.DefaultPreStartStage) // called finally stage by default 205 // startup 206 stm.AppendStartStage(mosn.DefaultStartStage) 207 // after-stop 208 stm.AppendAfterStopStage(holmes.Stop) 209 // execute all stages //执行所有场景 210 stm.RunAll() 211 return nil 212 }, (line160)创建应用 mosn.NewMosn , (line161)创建场景管理器 stagemanager.InitStageManager(c, c.String(\u0026ldquo;config\u0026rdquo;), app) (line210)启动场景管理器 stm.RunAll() ,触发执行启动、等待停止、停止。 而中间(第165行到第208行)的一堆逻辑其实就是在设置 StageManager(场景管理器),为每个状态设置了对应的回调函数,后面启动过程中调用场景切换的时候其实就是调用这里设置的回调函数。 ParamsParsed 阶段 (stm *StageManager) AppendParamsParsedStage(f func(*cli.Context))\n这个阶段是整个生命周期的第一个阶段,如果需要有需要通过命令行参数来初始化的工作,可以在这个阶段完成。这个阶段注册了两个函数 ExtensionsRegister 和 DefaultParamsParsed 这两个函数作用如下:\n ExtensionsRegister :主要用来初始化一些扩展的组件。这块我理解 MOSN 在落地的时候会根据具体场景来接入一些特定的组件。这些组件如果需要初始化,可以在这个方法里初始化。为什么要在这个函数里初始化呢?一个是函数的参数是 cli.Context 命令行的封装,可以方便获取命令行参数来初始化组件,另外这个函数执行也是整个生命周期最开始执行,如果需要 MOSN 启动首先初始化的组件可以在这里来实现。而 v1.6.0 版本里该函数主要用来初始化一些链路追踪的配置,以及网络协议编解码的设置。这里就不展开分析了。 DefaultParamsParsed :这里就是 MOSN 默认的在命令解析阶段进行初始化的内容一般不用修改。目前作用是用来设置日志级别及从命令行里解析各种开关。 InitStage阶段 (stm *StageManager) AppendInitStage(f func(*v2.MOSNConfig)) *StageManager\n这个是第二个阶段,这个阶段也是用来初始化,只不过初始化的来源是通过 MOSN 的配置文件。\n (line168-194)这部分主要是从 Config 配置文件中获取运行环境的相关元数据,用这些数据来初始化 xds 客户端。xds 客户端使用xds协议与控制面组件进行交互。( xds 是 Istio 标准的协议) (line195)调用了 MOSN 的默认初始化,这部分逻辑非常的多。里面又细分很多步骤。这里我对每个方法都加上了注释,见下面代码段。 func DefaultInitStage(c *v2.MOSNConfig) { InitDefaultPath(c) //初始化mosn需要的相关运行时的目录,比如:日志,存储mosn进程ID的文件等 \tInitDebugServe(c) //启动mosn的debug信息查看服务,可以查看pprof信息 \tInitializePidFile(c) //初始化mosn pid持久化的文件 \tInitializeTracing(c) //初始化mosn 链路追踪的组件,根据配置文件中链路相关的配置。在之前的分析ParamParsed阶段会维护一些mosn支持的链路追踪的驱动列表,然后在这个阶段里会选择一个具体的组件作为链路追踪的实现。 \tInitializePlugin(c) //这个阶段是初始化一下插件,不过看了实现其实就是初始化log配置 \tInitializeWasm(c) //初始化web assembly 环境 \tInitializeThirdPartCodec(c) //初始化第三方的编解码的配置,这块我也不太理解,需要后面继续研究一下。个人感觉是加载动态代码,目前支持wasm和goPlugin } (line196-201)这部分比较简单,AppendInitStage 这部分就是初始化 metrics 初始化统计的组件 接下来(line202)stm.AppendInitStage(holmes.Register) 这句初始化一个蚂蚁开源的可观测性组件 holmes。参考:holmes PreStartStage阶段 func (stm *StageManager) AppendPreStartStage(f func(Application)) *StageManager\n这个阶段逻辑并不复杂,主要是启动 xds 客户端。\n// Default Pre-start Stage wrappers func DefaultPreStartStage(mosn stagemanager.Application) { m := mosn.(*Mosn) // start xds client \t_ = m.StartXdsClient() featuregate.FinallyInitFunc() //初始化配置中指定的Feature \tm.HandleExtendConfig() //将配置中扩展配置信息转换成对象 } StartStage阶段 func (stm *StageManager) AppendStartStage(f func(Application)) *StageManager\n (line206) stm.AppendStartStage(mosn.DefaultStartStage) 这个阶段启动了 MOSN 的管理服务,通过配置文件进行启动。 // Default Start Stage wrappers func DefaultStartStage(mosn stagemanager.Application) { m := mosn.(*Mosn) // register admin server \t// admin server should register after all prepares action ready \tsrv := admin.Server{} srv.Start(m.Config) } AfterStopStage阶段 func (stm *StageManager) AppendAfterStopStage(f func(Application)) *StageManager\n (line208) stm.AppendAfterStopStage(holmes.Stop) 这个阶段是在 MOSN 服务关闭后调用,这里就直接调用 holmes.Stop 关闭 holmes 组件。 ","excerpt":"本文基于 MOSN V1.6.0版本的源码基础上进行整理。该版本对比之前 V0.4.0 版本启动逻辑有比较大的变化。其中比较明显的差异是该版本新增了 StageManager 结构,该结构对 MOSN …","ref":"https://mosn.io/blog/code/mosn-startup/v1.6.0/","title":"MOSN 源码解析 - 启动流程"},{"body":"前两篇介绍了内存安全和并发安全,今天来到了安全性的最后一篇,沙箱安全,也是相对来说,最简单的一篇。\n沙箱安全 所谓的沙箱安全,是为了保护 Envoy,这个宿主程序的安全,也就是说,扩展的 Go 代码运行在一个沙箱环境中,即使 Go 代码跑飞了,也不会把 Envoy 搞挂。\n具体到一个场景,也就是当我们使用 Golang 来扩展 Envoy 的时候,不用担心自己的 Go 代码写的不好,而把整个 Envoy 进程搞挂了。\n那么目前 Envoy Go 扩展的沙箱安全做到了什么程度呢?\n简单来说,目前只做到了比较浅层次的沙箱安全,不过,也是实用性比较高的一层。\n严格来说,Envoy Go 扩展加载的是可执行的机器指令,是直接交给 cpu 来运行的,并不像 Wasm 或者 Lua 一样由虚拟机来解释执行,所以,理论上来说,也没办法做到绝对的沙箱安全。\n实现机制 目前实现的沙箱安全机制,依赖的是 Go runtime 的 recover 机制。\n具体来说,Go 扩展底层框架会自动的,或者(代码里显示启动的协程)依赖人工显示的,通过 defer 注入我们的恢复机制,所以,当 Go 代码发生了奔溃的时候,则会执行我们注入的恢复策略,此时的处理策略是,使用 500 错误码结束当前请求,而不会影响其他请求的执行。\n但是这里有一个不太完美的点,有一些异常是 recover 也不能恢复的,比如这几个:\nConcurrent map writes Out of memory Stack memory exhaustion Attempting to launch a nil function as a goroutine All goroutines are asleep - deadlock 好在这几个异常,都是不太容易出现的,唯一一个值得担心的是 Concurrent map writes,不熟悉 Go 的话,还是比较容易踩这个坑的。\n所以,在写 Go 扩展的时候,我们建议还是小心一些,写得不好的话,还是有可能会把 Envoy 搞挂的。\n当然,这个也不是一个很高的要求,毕竟这是 Gopher 写 Go 代码的很常见的基本要求。\n好在大多常见的异常,都是可以 recover 恢复的,这也就是为什么现在的机制,还是比较有实用性。\n未来 那么,对于 recover 恢复不了的,也是有解决的思路:\n比如 recover 恢复不了 Concurrent map writes,是因为 runtime 认为 map 已经被写坏了,不可逆了。\n那如果我们放弃整个 runtime,重新加载 so 来重建 runtime 呢?那影响面也会小很多,至少 Envoy 还是安全的,不过实现起来还是比较的麻烦。\n眼下比较浅的安全机制,也足够解决大多数的问题了,嗯。\n","excerpt":"前两篇介绍了内存安全和并发安全,今天来到了安全性的最后一篇,沙箱安全,也是相对来说,最简单的一篇。\n沙箱安全 所谓的沙箱安全,是为了保护 Envoy,这个宿主程序的安全,也就是说,扩展的 Go 代码运 …","ref":"https://mosn.io/blog/posts/moe-extend-envoy-using-golang-7/","title":"MoE 系列[七] - Envoy Go 扩展之沙箱安全"},{"body":"前言 这篇文章主要是介绍mosn在v1.5.0中新引入的基于延迟的负载均衡算法(#2253)。首先会对分布式系统中延迟出现的原因进行剖析,之后介绍mosn都通过哪些方法来降低延迟,最后构建来与生产环境性能分布相近的测试用例来对算法进行验证。\n在开始聊基于延迟的负载均衡算法之前,先介绍下什么是负载均衡\n什么是负载均衡 Wikipedia中 Load Balancing (Computing) 词条是这样介绍负载均衡的:\n 负载均衡是将一组任务分配到一组资源(计算单元)上的过程,目的是使它们的整体处理更有效率。负载均衡可以优化响应时间,避免负载不均匀导致一些计算节点过载而其他计算节点处于空闲状态\n 负载均衡在大型分布式系统中是关键的组成部分。负载均衡解决了分布式系统中最重要的两个问题:可伸缩性(scalability)和韧性(resilience)。\n 可伸缩性:应用程序部署在多个相同的副本中。当计算资源不足时可以通过部署额外的副本来增加计算资源,而当计算资源大量冗余时可以通过减少副本来节省成本。通过负载均衡可以将请求负载分布到不同的副本中。\n 韧性:分布式系统的故障是部分的。应用程序通过冗余副本的方式,保证在部分组件故障时仍能正常地提供服务。负载均衡通过感知节点的故障,调整流量的分配,将流量更多的分配到那些能够正常提供服务的节点上。\n 走得更快 负载均衡使得现代软件系统具备了可扩展性和韧性。但在分布式系统中还存在不容忽视的问题:延迟。\n延迟来自哪里 现代软件系统通常是多层级结构大型分布式系统,即使是只服务单个终端用户的请求,它背后也有可能经过了上百次的数据访问,这种情况在微服务架构中更是尤为普遍。\n微服务架构(引用自Microservices Pattern) 单台性能稳定的服务器中延迟通常由以下几个方面造成:\n 计算任务本身的复杂度 内容的传输过程中的延迟 请求排队等待的延迟 后台任务活动所导的资源竞争 这些服务器之间的延迟将会叠加,任何显著的延迟增加都会影响终端用户的体验。此外,任何来自单个节点的延迟峰值也会直接影响到终端用户体验。最后,越来越多地使用公有云部署应用程序,进一步加剧了响应时间的不可预测性,因为在这些环境中存在共享资源(CPU、内存和IO)的争用,应用程序机几乎不可避免地遇到性能影响,并且这种影响是随时发生的。\n如何减少延迟 有研究表明,在大型互联网应用中,延迟往往具有长尾特点,P999比中位数高出几个数量级。如果在应用架构的每层都能够减少这些尾部延迟,那么对终端用户整体的尾部延迟将会显著降低。\n在服务网格中,所有接收和发送的流量都会经过边车代理,通过边车代理可以轻松地控制网格的流量,而无需对服务进行任何修改。如果边车代理在对应用层流量进行转发时,总是通过负载均衡时选择响应时间较短的服务器,那么将会显著降低对终端用户的尾部延迟。\n基于此,我们准备开始为mosn引入基于延迟的负载均衡算法,并进行适当调整来保证能够在大多数使用场景下显著减少延迟。\n性能问题是局部的 前面提到了,每个节点的性能受到多种因素的影响,这些影响因素是动态的,难以准确预测每个节点的性能,因此我们无法精确地选择最好的节点,但是可以避免较差的节点。\n在云环境中,服务器的性能常常是难以预测的,但是我们可以通过对大量的数据进行分析,发现服务器性能的分布大多数情况下是符合正态分布的。因此,尽管有一部分的服务器在性能方面表现比较差,它们的数量通常都是少数的(3sigma),而绝大部分服务器节点的表现是正常的。\n除了服务器之间的差异,还存在由基础设施导致的动态延迟,这种延迟可能是由于网络拥塞、故障或不断增长的流量所导致。这种延迟通常具有持续性和局部性。持续性则表示延迟会长时间存在,不会在短时间内消失;而局部性指的是延迟往往只出现在某些特定服务器上,而不会在全局发生。\nPeakEWMA 面对这些问题,我们使用PeakEWMA(Peak Exponentially Weighted Moving Average)计算响应时间指标,并根据这个指标来对节点进行负载均衡。\nEWMA是一种动态权重调整算法,各数值的加权影响力随时间而指数式衰退,越近期的数据加权影响力越重,但较旧的数据也给予一定的加权值。\n它以相对较高的权重考虑了最近响应时间的影响,因此更具有针对性和时效性。加权的程度以常数 𝛼 决定, 𝛼 数值介于 0 至 1,它用来控制数据加权影响力衰退的速率。\n作为一种统计学指标,EWMA的计算过程不需要大量的采样点以及时间窗口的设定,有效地避免了计算资源的浪费,更适合在mosn这样的边车代理中使用。\n由于响应时间是历史指标,当服务器出现性能问题导致长时间未返回时,负载均衡算法会错误地认为这台服务器仍是最优的,而不断地向其发送请求而导致长尾延迟增高。我们使用活跃连接数作为实时变化的指标对响应时间进行加权,表示等待所有活跃的连接都返回所需要的最大时间。\nP2C(Power of Two Choice) 在大规模集群中,如果使用遍历所有服务器选择最好的服务器的方法,虽然可以找到最轻负载的服务器来处理请求,但这种方法通常需要大量的计算资源和时间,因此无法处理大规模的请求。因此,我们使用P2C(Power of Two Choice)来选择最优节点。相比之下,P2C算法可以在常数时间内选择两个服务器进行比较,并选择其中负载更轻的服务器来处理请求。P2C基于概率分配,即不直接基于权重分配,而是根据每个服务器优于其他服务器的概率值来决定请求的分配。\n此外,在多个负载均衡器的情况下,不同负载均衡器可能会有不同的节点视图,这可能导致某些负载均衡器选择的最优节点总是最差的节点。这是因为负载均衡器选择最优节点时基于自己的视图信息,而节点视图随着时间的变化可能会发生变化,因此不同的负载均衡器选择的最优节点也可能不同。P2C算法通过对随机选择的两个节点进行比较,可以使节点间的负载均衡更加均匀,即使节点视图发生变化,也能提供稳定的负载均衡效果。\n 在mosn的v1.5.0版本中,只有节点权重相同时会使用P2C,当权重不同时会使用EDF进行加权选择。后续会提供可配置的选项。\n 模拟流量验证 我们构建了与生产环境性能分布相近的测试用例来对算法进行验证。\n首先我们使用正态分布生成了10台服务器的基准性能,其中数学期望为50ms,标准差为10ms。接下来,我们将这些基准性能作为数学期望,并以标准差为5ms的正态分布随机生成了请求延迟,以模拟真实世界的情况。此外,我们还在其中一台服务器注入了概率为0.1的故障,故障发生时会产生1000ms的延迟,以测试系统的容错性。\n为了模拟请求倾斜时请求排队等待的延迟,我们限制了每台服务器的最大并发数为8,当同时处理的最大请求数超过了最大并发数时,将会排队等待。这样能够更加真实地模拟出系统的运行情况。\n最后,我们使用了Round Robin、Least Request和PeakEWMA三种算法,分别以16并发同时发送请求,得到的P99如下\nRound Robin算法虽然平衡,但是始终会选择到注入了故障的服务器,导致P99始终在1000ms上下波动;Least Request算法虽然避开了故障服务器,但是其P99值依然表现出较大的波动。\n与此相比,PeakEWMA算法在保持稳定的同时,P99值始终低于Round Robin和Least Request算法。这恰当地体现了mosn在性能优化方面的成功,mosn确实做到了走得更快。\n期待走得更稳 虽然mosn在服务网格中解决了让应用跑得更快的问题,但是分布式系统中的故障却时刻存在。我们期望通过mosn的负载均衡算法,可以让我们的服务走得更稳。\n快速失败的挑战 根据经验,故障时的响应时间往往远远小于正常值,比如网络分区导致的连接超时,而没有实际处理请求。我们称这种错误时响应时间远远小于正常值的情况为快速失败。\n在服务器出现快速失败时,从负载均衡的角度看,就会错误地认为该服务器是最优的选择。尽管可以通过断路器来避免向该服务器发送长期请求,但断路器本身也是一种快速失败,错误的视图依然会传播。此外,断路器的阈值设置也存在挑战。此外,断路器需要足够的错误样本才能触发,而我们期望尽可能避免错误的发生。\n因此,我们在后续版本中将会对负载均衡算法进行调整,让负载均衡算法能够感知错误的发生,并在触发断路器前就避免将请求转发到故障的服务器中。\n","excerpt":"前言 这篇文章主要是介绍mosn在v1.5.0中新引入的基于延迟的负载均衡算法(#2253)。首先会对分布式系统中延迟出现的原因进行剖析,之后介绍mosn都通过哪些方法来降低延迟,最后构建来与生产环境 …","ref":"https://mosn.io/blog/posts/mosn-loadbalancer-peakewma/","title":"mosn基于延迟负载均衡算法 -- 走得更快,期待走得更稳"},{"body":"前一篇介绍了 Envoy Go 扩展的内存安全,相对来说,还是比较好理解的,主要是 Envoy C++ 和 Go GC 都有自己一套的内存对象的生命周期管理。\n这篇聊的并发安全,则是专注在并发场景下的内存安全,相对来说会复杂一些。\n并发的原因 首先,为什么会有并发呢?\n本质上因为 Go 有自己的抢占式的协程调度,这是 Go 比较重的部分,也是与 Lua 这类嵌入式语言区别很大的点。\n细节的话,这里就不展开了,感兴趣的可以看这篇 cgo 实现机制 - 从 c 调用 go\n这里简单交代一下的,因为 c 调用 go,入口的 Go 函数的运行环境是,Goroutine 运行在 Envoy worker 线程上,但是这个时候,如果发生了网络调用这种可能导致 Goroutine 挂起的,则会导致 Envoy worker 线程被挂起。\n所以,解决思路就是像 Go 扩展的异步模式 中的示例一样,新起一个 Goroutine,它会运行在普通的 go 线程上。\n那么此时,对于同一个请求,则会同时有 Envoy worker 线程和 Go 线程,两个线程并发在处理这个请求,这个就是并发的来源。\n但是,我们并不希望用户操心这些细节,而是在底层提供并发安全的 API,把复杂度留在 Envoy Go 扩展的底层实现里。\n并发安全的实现 接下来,我们就针对 Goroutine 运行在普通的 Go 线程上,这个并发场景,来聊一聊如何实现并发安全的。\n对于 Goroutine 运行在 Envoy 线程上,因为并不存在并发冲突,这里不做介绍。\n写 header 操作 我们先聊一个简单的,比如在 Go 里面通过 header.Set 写一个请求头。\n核心思路是,是通过 dispatcher.post,将写操作当做一个事件派发给 Envoy worker 线程来执行,这样就避免了并发冲突。\n读 header 操作 读 header 则要复杂不少,因为写不需要返回值,可以异步执行,读就不行了,必须得到返回值。\n为此,我们根据 Envoy 流式的处理套路,设计了一个类似于所有权的机制。\nEnvoy 的流式处理,可以看这篇 搞懂 http filter 状态码\n简单来说,我们可以这么理解,当进入 decodeHeaders 的时候,header 所有权就交给 Envoy Go 的 c++ 侧了,然后,当通过 cgo 进入 Go 之后,我们会通过一个简单的状态机,标记所有权在 Go 了。\n通过这套设计/约定,就可以安全的读取 header 了,本质上,还是属于规避并发冲突。\n为什么不通过锁来解决呢?因为 Envoy 并没有对于 header 的锁机制,c++ 侧完全不会有并发冲突。\n读写 data 操作 有了这套所有权机制,data 操作就要简单很多了。\n因为 header 只有一份,并发冲突域很大,需要考虑 Go 代码与 c++ 侧的其他 filter 的竞争。\ndata 则是流式处理,我们在 c++ 侧设计了两个 buffer 对象,一个用于接受 filter manager 的流式数据,一个用于缓存交给 Go 侧的数据。\n这样的话,交给 Go 来处理的数据,Go 代码拥有完整的所有权,不需要考虑 Go 代码与 C++ 侧其他 filter 的竞争,可以安全的读写,也没有并发冲突。\n请求生命周期 另外一个很大的并发冲突,则关乎请求的生命周期,比如 Envoy 随时都有可能提前销毁请求,此时 Goroutine 还在 go thread 上继续执行,并且随时可能读写请求数据。\n处理的思路是:\n 并没有有效的办法,能够立即 kill goroutine,所以,我们允许 goroutine 可能在请求被销毁之后继续执行 但是,goroutine 如果读写请求数据,goroutine 会被终止,panic + recover(recover 细节我们下一篇会介绍)。 那么,我们要做的就是,所有的 API 都检查当前操作的请求是否合法,这里有两个关键:\n 每请求有一个内存对象,这个对象只会由 Go 来销毁,并不会在请求结束时,被 Envoy 销毁,但是这个内存对象中保存了一个 weakPtr,可以获取 C++ filter 的状态。\n通过这个机制,Go 可以安全的获取 C++ 侧的 filter,判断请求是否还在。\n 同时,我们还会在 onDestroy,也就是 C++ filter 被销毁的 hook 点;以及 Go thread 读写请求数据,这两个位置都加锁处理,以解决这两个之间的并发冲突。\n 最后 对于并发冲突,其实最简单的就是,通过加锁来竞争所有权,但是 Envoy 在这块的底层设计并没有锁,因为它根本不需要锁。\n所以,基于 Envoy 的处理模型,我们设计了一套类似所有权的机制,来避免并发冲突。\n所有权的概念也受到了 Rust 的启发,只是两者工作的层次不一样,Rust 是更底层的语言层面,可以作用于语言层面,我们这里则是更上层的概念,特定于 Envoy 的处理模型,也只能作用于这一个小场景。\n但是某种程度上,解决的问题,以及其中部分思想是一样的。\n","excerpt":"前一篇介绍了 Envoy Go 扩展的内存安全,相对来说,还是比较好理解的,主要是 Envoy C++ 和 Go GC 都有自己一套的内存对象的生命周期管理。\n这篇聊的并发安全,则是专注在并发场景下的 …","ref":"https://mosn.io/blog/posts/moe-extend-envoy-using-golang-6/","title":"MoE 系列[六] - Envoy Go 扩展之并发安全"},{"body":"前面几篇介绍了 Envoy Go 扩展的基本用法,接下来几篇将介绍实现机制和原理。\nEnvoy 是 C++ 实现的,那 Envoy Go 扩展,本质上就相当于把 Go 语言嵌入 C++里 了。\n在 Go 圈里,将 Go 当做嵌入式语言来用的,貌似并不太多见,这里面细节还是比较多的。 比如:\n Envoy 有一套自己的内存管理机制,而 Go 又是一门自带 GC 的语言 Envoy 是基于 libevent 封装的事件驱动,而 Go 又是包含了抢占式的协程调度 为了降低用户开发时的心智负担,我们提供了三种的安全保障。有了这三层保障,用户写 Go 来扩展 Envoy 的时候,就可以像平常写 Go 代码一样简单,而不必关心这些底层细节。\n三种安全 内存安全\n用户通过 API 获取到的内存对象,可以当做普通的 Go 对象来使用\n比如,通过 headers.Get 得到的字符串,在请求结束之后还可以使用,而不用担心请求已经在 Envoy 侧结束了,导致这个字符串被提前释放了\n 并发安全\n当启用协程的时候,我们的 Go 代码将会运行在另外的 Go 线程上,而不是在当前的 Envoy worker 线程上,此时对于同一个请求,则存在 Envoy worker 线程和 Go 线程的并发\n但是,用户并不需要关心这个细节,我们提供的 API 都是并发安全的,用户可以不感知并发的存在\n 沙箱安全\n这一条是针对宿主 Envoy 的保障,因为我们并不希望某一个 Go 扩展的异常,把整个 Envoy 进程搞奔溃了。\n目前我们提供的是,Go runtime 可以 recover 的有限沙箱安全,这通常也足够了。\n更深度的,runtime 也 recover 不了的,比如 map 并发访问,则只能将 Go so 重载,重建整个 Go runtime 了,这个后续也可以加上。\n 内存安全实现机制 要提供安全的内存机制,最简单的办法,也是(几乎)唯一的办法,就是复制。 但是,什么时候复制,怎么复制,还是有一些讲究的。这里权衡的目标是降低复制的开销,提升性能。\n这里讲的内存安全,还不涉及并发时的内存安全,只是 Envoy(C++)和 Go 这两个语言/运行时之间的差异。\nPS:以前混 OpenResty 的时候,也是复制的玩法,只是有一点区别是,Lua string 的 internal 归一化在大内存场景下,会有相对较大的开销;Go string 则没有这一层开销,只有 memory copy + GC 的开销。\n复制时机 首先是复制时机,我们选择了按需复制,比如 header,body data 并不是一开始就复制到 Go 里面,只在有对应的 API 调用时,才会真的去 Envoy 侧获取 \u0026amp; 复制。\n如果没有被真实需要,则并不会产生复制,这个优化对于 header 这种常用的,效果倒是不太明显,对于 body 这种经常不需要获取内容的,效果则会比较的明显。\n复制方式 另一个则是复制方式,比如 header 获取上,我们采用的是在 Go 侧预先申请内存,在 C++ 侧来完成赋值的方式,这样我们只需要一次内存赋值即可完成。\n这里值得一提的是,因为我们在进入 Go 的时候,已经把 header 的大小传给了 Go,所以我们可以在 Go 侧预先分配好需要的内存。\n不过呢,这个玩法确实有点 tricky,并不是 Go 文档上注明推荐的用法,但是呢,也确实是我们发现的最优的解法了。\n如果按照 Go 常规的玩法,我们可能需要一次半/两次内存拷贝,才能保证安全,这里有个半次的差异,就是我们下回要说的并发造成的。\n另外,在 API 实现上,我们并不是每次获取一个 header,而是直接一次性把所有的 header 全复制过来了,在 Go 侧缓存了。 这是因为大多数场景下,我们需要获取的 header 数量会有多个,在权衡了 cgo 的调用开销和内存拷贝的开销之后,我们认为一次性全拷贝是更优的选择。\n最后 相对来说,不考虑并发的内存安全,还是比较简单的,只有复制最安全,需要权衡考虑的则更多是优化的事情了。\n比较复杂的还是并发时的安全处理,这个我们下回再聊。\n","excerpt":"前面几篇介绍了 Envoy Go 扩展的基本用法,接下来几篇将介绍实现机制和原理。\nEnvoy 是 C++ 实现的,那 Envoy Go 扩展,本质上就相当于把 Go 语言嵌入 C++里 了。 …","ref":"https://mosn.io/blog/posts/moe-extend-envoy-using-golang-5/","title":"MoE 系列 [五] - Envoy Go 扩展之内存安全"},{"body":"上一篇我们体验了用 Istio 做控制面,给 Go 扩展推送配置,这次我们来体验一下,在 Go 扩展的异步模式下,对 goroutine 等全部 Go 特性的支持。\n异步模式 之前,我们实现了一个简单的 basic auth,但是,那个实现是同步的,也就是说,Go 扩展会阻塞,直到 basic auth 验证完成,才会返回给 Envoy。\n因为 basic auth 是一个非常简单的场景,用户名密码已经解析在 Go 内存中了,整个过程只是纯 CPU 计算,所以,这种同步的实现方式是没问题的。\n但是,如果我们要实现一个更复杂的需求,比如,我们要将用户名密码,调用远程接口查询,涉及网络操作,这个时候,同步的实现方式就不太合适了。因为,同步模式下,如果我们要等待远程接口返回,那么,Go 扩展就会阻塞,Envoy 也就无法处理其他请求了。\n所以,我们需要一种异步模式:\n 我们在 Go 扩展中,启动一个 goroutine,然后立即返回给 Envoy,当前正在处理的请求会被挂起,Envoy 则可以继续处理其他请求。 goroutine 在后台异步执行,当 goroutine 中的任务完成之后,再回调通知 Envoy,挂起的请求可以继续处理了。 注意:虽然 goroutine 是异步执行,但是 goroutine 中的代码,与同步模式下的代码,几乎是一样的,并不需要特别的处理。\n为什么需要 为什么需要支持 Goroutine 等全部 Go 的特性呢?\n有两方面的原因:\n 有了 full feature supported Go,我们可以实现很非常强大,复杂的扩展\n 可以非常方便的集成现有的 Go 世界的代码,享受 Go 生态的红利\n如果不支持全部的 Go 特性,那么在集成现有 Go 代码的时候,会有诸多限制,导致需要重写大量的代码,这样,就享受不到 Go 生态的红利了。\n 实现 接下来,我们还是通过一个示例来体验,这次我们实现 basic auth 的远程校验版本,关键代码如下:\nfunc (f *filter) DecodeHeaders(header api.RequestHeaderMap, endStream bool) api.StatusType { go func() { // verify 中的代码,可以不需要感知是否异步 // 同时,verify 中是可以使用全部的 Go 特性,比如,http.Post \tif ok, msg := f.verify(header); !ok { f.callbacks.SendLocalReply(401, msg, map[string]string{}, 0, \u0026#34;bad-request\u0026#34;) return } // 这里是唯一的 API 区别,异步回调,通知 Envoy,可以继续处理当前请求了 \tf.callbacks.Continue(api.Continue) }() // Running 表示 Go 还在处理中,Envoy 会挂起当前请求,继续处理其他请求 \treturn api.Running } 再来看 verify 的代码,重点是,我们可以在这里使用全部的 Go 特性:\n// 这里使用了 http.Post func checkRemote(config *config, username, password string) bool { body := fmt.Sprintf(`{\u0026#34;username\u0026#34;: \u0026#34;%s\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;%s\u0026#34;}`, username, password) remoteAddr := \u0026#34;http://\u0026#34; + config.host + \u0026#34;:\u0026#34; + strconv.Itoa(int(config.port)) + \u0026#34;/check\u0026#34; resp, err := http.Post(remoteAddr, \u0026#34;application/json\u0026#34;, strings.NewReader(body)) if err != nil { fmt.Printf(\u0026#34;check error: %v\\n\u0026#34;, err) return false } if resp.StatusCode != 200 { return false } return true } // 这里操作 header 这个 interface,与同步模式完全一样 func (f *filter) verify(header api.RequestHeaderMap) (bool, string) { auth, ok := header.Get(\u0026#34;authorization\u0026#34;) if !ok { return false, \u0026#34;no Authorization\u0026#34; } username, password, ok := parseBasicAuth(auth) if !ok { return false, \u0026#34;invalid Authorization format\u0026#34; } fmt.Printf(\u0026#34;got username: %v, password: %v\\n\u0026#34;, username, password) if ok := checkRemote(f.config, username, password); !ok { return false, \u0026#34;invalid username or password\u0026#34; } return true, \u0026#34;\u0026#34; } 另外,我们还需要实现一个简单的 HTTP 服务,用来校验用户名密码,这里就不展开了,用户名密码还是 foo:bar。\n完整的代码,请移步 github。\n测试 老规矩,启动之后,我们使用 curl 来测试一下:\n$ curl -s -I -HHost:httpbin.example.com \u0026#34;http://$INGRESS_HOST:$INGRESS_PORT/status/200\u0026#34; HTTP/1.1 401 Unauthorized # valid foo:bar $ curl -s -I -HHost:httpbin.example.com \u0026#34;http://$INGRESS_HOST:$INGRESS_PORT/status/200\u0026#34; -H \u0026#39;Authorization: basic Zm9vOmJhcg==\u0026#39; HTTP/1.1 200 OK 依旧符合预期。\n总结 在同步模式下,Go 代码中常规的异步非阻塞,也会变成阻塞执行,这是因为 Go 和 Envoy 是两套事件循环体系。\n而通过异步模式,Go 可以在后台异步执行,不会阻塞 Envoy 的事件循环,这样,就可以用上全部的 Go 特性了。\n由于 Envoy Go 暴露的是底层的 API,所以实现 Go 扩展的时候,需要关心同步和异步的区别。\n当然,这对于普通的扩展开发而言,并不是一个友好的设计,只所有这么设计,更多是为了极致性能的考量。\n大多数场景下,其实并不需要到这么极致,所以,我们会在更上层提供一种,默认异步的模式,这样,Go 扩展的开发者,就不需要关心同步和异步的区别了。\n欢迎感兴趣的持续关注~\n","excerpt":"上一篇我们体验了用 Istio 做控制面,给 Go 扩展推送配置,这次我们来体验一下,在 Go 扩展的异步模式下,对 goroutine 等全部 Go 特性的支持。\n异步模式 之前,我们实现了一个简单 …","ref":"https://mosn.io/blog/posts/moe-extend-envoy-using-golang-4/","title":"MoE 系列 [四] - Go 扩展的异步模式"},{"body":"上一篇 我们用 Go 扩展实现了 basic auth,体验了 Go 扩展从 Envoy 接受配置。\n只所以这么设计,是想复用 Envoy 原有的 xDS 配置推送通道,这不,今天我们就来体验一番,云原生的配置变更。\n前提准备 这次我们需要一套 k8s 环境,如果你手头没有,推荐使用 kind 安装一套。具体安装方式,这里就不展开了。\n安装 Istio 我们直接安装最新版的 Istio:\n# 下载最新版的 istioctl $ export ISTIO_VERSION=1.18.0-alpha.0 $ curl -L https://istio.io/downloadIstio | sh - # 将 istioctl 加入 PATH $ cd istio-$ISTIO_VERSION/ $ export PATH=$PATH:$(pwd)/bin # 安装,包括 istiod 和 ingressgateway $ istioctl install 是的,由于 Go 扩展已经贡献给了上游官方,Istiod(pilot)和 ingressgateway 都已经默认开启了 Go 扩展,并不需要重新编译。\nIstio 配置 Ingress 我们先用 Istio 完成标准的 Ingress 场景配置,具体可以看 istio 的官方文档\n配置好了之后,简单测试一下:\n$ curl -s -I -HHost:httpbin.example.com \u0026quot;http://$INGRESS_HOST:$INGRESS_PORT/status/200\u0026quot; HTTP/1.1 200 OK server: istio-envoy date: Fri, 10 Mar 2023 15:49:37 GMT 基本的 Ingress 已经跑起来了。\n挂载 Golang so 之前我们介绍过,Go 扩展是单独编译为 so 文件的,所以,我们需要把 so 文件,挂载到 ingressgateway 中。\n这里我们把上次 basic auth 编译出来的 libgolang.so,通过本地文件挂载进来。简单点搞,直接 edit deployment 加了这些配置:\n# 申明一个 hostPath 的 volumevolumes:- name:golang-so-basic-authhostPath:path:/data/golang-so/example-basic-auth/libgolang.sotype:File# 挂载进来volumeMounts:- mountPath:/etc/golang/basic-auth.soname:golang-so-basic-authreadOnly:true开启 Basic auth 认证 Istio 提供了 EnvoyFilter CRD,所以,用 Istio 来配置 Go 扩展也比较的方便,apply 这段配置,basic auth 就开启了。\napiVersion:networking.istio.io/v1alpha3kind:EnvoyFiltermetadata:name:golang-filternamespace:istio-systemspec:configPatches:# The first patch adds the lua filter to the listener/http connection manager- applyTo:HTTP_FILTERmatch:context:GATEWAYlistener:filterChain:filter:name:\u0026#34;envoy.filters.network.http_connection_manager\u0026#34;subFilter:name:\u0026#34;envoy.filters.http.router\u0026#34;patch:operation:INSERT_BEFOREvalue:# golang filter specificationname:envoy.filters.http.golangtyped_config:\u0026#34;@type\u0026#34;: \u0026#34;type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config\u0026#34;library_id:examplelibrary_path:/etc/golang/basic-auth.soplugin_name:basic-authplugin_config:\u0026#34;@type\u0026#34;: \u0026#34;type.googleapis.com/xds.type.v3.TypedStruct\u0026#34;type_url:typexxvalue:username:foopassword:bar虽然有点长,但是,也很明显,配置的用户名密码还是:foo:bar\n测试 我们测试一下:\n$ curl -s -I -HHost:httpbin.example.com \u0026#34;http://$INGRESS_HOST:$INGRESS_PORT/status/200\u0026#34; HTTP/1.1 401 Unauthorized # valid foo:bar $ curl -s -I -HHost:httpbin.example.com \u0026#34;http://$INGRESS_HOST:$INGRESS_PORT/status/200\u0026#34; -H \u0026#39;Authorization: basic Zm9vOmJhcg==\u0026#39; HTTP/1.1 200 OK 符合预期。\n接下来,我们改一下 EnvoyFilter 中的密码,重新 apply,再测试一下:\n# foo:bar not match the new password $ curl -s -I -HHost:httpbin.example.com \u0026#34;http://$INGRESS_HOST:$INGRESS_PORT/status/200\u0026#34; -H \u0026#39;Authorization: basic Zm9vOmJhcg==\u0026#39; HTTP/1.1 401 Unauthorized 此时的 Envoy 并不需要重启,新的配置就立即生效了,云原生的体验就是这么溜~\n总结 因为 Go 扩展可以利用 Envoy 原有的 xDS 来接受配置,所以,从 Istio 推送配置也变得很顺利。\n不过呢,Istio 提供的 EnvoyFilter CRD 在使用上,其实并不是那么方便 \u0026amp; 自然,后面我们找机会试试 EnvoyGateway,看看 k8s Gateway API 的体验咋样。\n至此,我们已经体验了整个 Envoy Go 的开发 \u0026amp; 使用流程,在云原生时代,人均 Golang 的背景下,相信可以很好的完成网关场景的各种定制需求。\n下一篇,我们将介绍,如何在 Go 扩展中使用异步协程。这意味着,我们可以使用的是一个全功能的 Go 语言,而不是像 Go Wasm 那样,只能用阉割版的。\n敬请期待 ~\n","excerpt":"上一篇 我们用 Go 扩展实现了 basic auth,体验了 Go 扩展从 Envoy 接受配置。\n只所以这么设计,是想复用 Envoy 原有的 xDS 配置推送通道,这不,今天我们就来体验一番,云 …","ref":"https://mosn.io/blog/posts/moe-extend-envoy-using-golang-3/","title":"MoE 系列 [三] - 使用 Istio 动态更新 Go 扩展配置"},{"body":"上一篇 我们用一个简单的示例,体验了用 Golang 扩展 Envoy 的极速上手。\n这次我们再通过一个示例,来体验 Golang 扩展的一个强大的特性:从 Envoy 接收配置。\nBasic Auth 我们还是从一个小示例来体验,这次我们实现标准的 basic auth 的认证,与上一次示例不同的是,这次认证的用户密码信息,需要从 Envoy 传给 Go,不能在 Go 代码中写死了。\n完整的代码可以看 example-basic-auth,下面我们展开介绍一番。\n获取配置 为了更加灵活,在设计上,Envoy 传给 Go 的配置是 Protobuf 的 Any 类型,也就是说,配置内容对于 Envoy 是透明的,我们在 Go 侧注册一个解析器,来完成这个 Any 配置的解析。\n如下示例:\nfunc init() { // 注册 parser \thttp.RegisterHttpFilterConfigParser(\u0026amp;parser{}) } func (p *parser) Parse(any *anypb.Any) interface{} { configStruct := \u0026amp;xds.TypedStruct{} if err := any.UnmarshalTo(configStruct); err != nil { panic(err) } v := configStruct.Value conf := \u0026amp;config{} if username, ok := v.AsMap()[\u0026#34;username\u0026#34;].(string); ok { conf.username = username } if password, ok := v.AsMap()[\u0026#34;password\u0026#34;].(string); ok { conf.password = password } return conf } 这里为了方便,Any 中的类型是 Envoy 定义的 TypedStruct 类型,这样我们可以直接使用现成的 Go pb 库。\n值得一提的是,这个配置解析,只有在首次加载的时候需要执行,后续在 Go 使用的是解析后的配置,所以,我们解析到一个 Go map 可以拥有更好的运行时性能。\n同时,由于 Envoy 的配置,也是有层级关系的,比如 http-filter, virtual host, router, virtual clusters 这四级,我们也支持这四个层级同时有配置,在 Go 侧来组织 merge。\n当然,这个只有在 Go 侧有复杂的 filter 组织逻辑的时候用得上,后面我们在 MOSN 的上层封装的时候,可以看到这种用法,这里暂时不做展开介绍。\n认证 具体的 Basic Auth 认证逻辑,我们可以参考 Go 标准 net/http 库中的 BasicAuth 实现。\nfunc (f *filter) verify(header api.RequestHeaderMap) (bool, string) { auth, ok := header.Get(\u0026#34;authorization\u0026#34;) if !ok { return false, \u0026#34;no Authorization\u0026#34; } username, password, ok := parseBasicAuth(auth) if !ok { return false, \u0026#34;invalid Authorization format\u0026#34; } if f.config.username == username \u0026amp;\u0026amp; f.config.password == password { return true, \u0026#34;\u0026#34; } return false, \u0026#34;invalid username or password\u0026#34; } 这里面的 parseBasicAuth 就是从 net/http 库中的实现,是不是很方便呢。\n配置 简单起见,这次我们使用本地文件的配置方式。如下是关键的配置:\nhttp_filters:- name:envoy.filters.http.golangtyped_config:\u0026#34;@type\u0026#34;: type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Configlibrary_id:examplelibrary_path:/etc/envoy/libgolang.soplugin_name:basic-authplugin_config:\u0026#34;@type\u0026#34;: type.googleapis.com/xds.type.v3.TypedStructvalue:username:\u0026#34;foo\u0026#34;password:\u0026#34;bar\u0026#34;这里我们配置了用户名密码:foo:bar。\n预告一下,下一篇我们会体验通过 Istio 来推送配置,体会一番动态更新配置的全流程。\n测试 编译,运行,跟上篇一样,我们还是使用 Envoy 官方提供的镜像即可。\n跑起来之后,我们测试一下:\n$ curl -s -I \u0026#39;http://localhost:10000/\u0026#39; HTTP/1.1 401 Unauthorized # invalid username:password $ curl -s -I \u0026#39;http://localhost:10000/\u0026#39; -H \u0026#39;Authorization: basic invalid\u0026#39; HTTP/1.1 401 Unauthorized # valid foo:bar $ curl -s -I \u0026#39;http://localhost:10000/\u0026#39; -H \u0026#39;Authorization: basic Zm9vOmJhcg==\u0026#39; HTTP/1.1 200 OK 是不是很简单呢,一个标准的 basic-auth 扩展就完成了。\n总结 Envoy 是面向云原生的架构设计,提供了配置动态变更的机制,Go 扩展可以从 Envoy 接受配置,也就意味着 Go 扩展也可以很好的利用这套机制。\nGo 扩展的开发者,不需要关心配置的动态更新,只需要解析配置即可,非常的方便~\n下一篇我们会介绍,配合 Istio 来动态更新用户名密码,体验一番云原生的配置变更体验。\n后续还有更多 Golang 扩展的特性介绍,原理解析,以及,更上层的 MOSN 集成体验,欢迎持续关注。\n","excerpt":"上一篇 我们用一个简单的示例,体验了用 Golang 扩展 Envoy 的极速上手。\n这次我们再通过一个示例,来体验 Golang 扩展的一个强大的特性:从 Envoy 接收配置。\nBasic …","ref":"https://mosn.io/blog/posts/moe-extend-envoy-using-golang-2/","title":"MoE 系列 [二] - Golang 扩展从 Envoy 接收配置"},{"body":"背景 MoE,MOSN on Envoy 是 MOSN 团队提出的技术架构,经过近两年的发展,在蚂蚁内部已经得到了很好的验证;并且去年我们也将底层的 Envoy Go 七层扩展贡献了 Envoy 官方,MOSN 也初步支持了使用 Envoy 作为网络底座的能力。\n准备写一系列的文章,逐一介绍这里面的技术,本文是开篇,重点介绍 MoE 中的基础技术,Envoy Go 扩展。\nFAQ 开始前,先回答几个基本的问题:\n MoE 与 Envoy Go 扩展的区别\nMoE 是技术架构,Envoy Go 扩展是连接 MOSN 和 Envoy 的基础技术\n Envoy Go 扩展,与用 Go 来编译 Wasm 有什么区别\nEnvoy Go 支持 Go 语言的所有特性,包括 Goroutine,Go Wasm 则只能使用少量的 Go 语言特性,尤其是没有 Goroutine 的支持\n Go 是静态链接到 Envoy 么?\n不是的,Go 扩展编译成为 so,Envoy 动态加载 so,不需要重新编译 Envoy\n Envoy Go 支持流式处理么?\n支持的。\n由于 Go 扩展提供的是底层的 API,非常的灵活,使用上相对会稍微复杂一些;如果只想简单的使用,可以使用 MOSN 的 filter,后面我们也会介绍。\n 需求 我们先实现一个小需求,来实际体会一下:\n对请求需要进行验签,大致是从 URI 上的某些参数,以及私钥计算一个 token,然后和 header 中的 token 进行对比,对不上就返回 403。\n很简单的需求,仅仅作为示例,主要是体验一下过程。\n代码实现 完整的代码可以看 envoy-go-filter-example 这个仓库\n这里摘录最核心的两个函数:\nconst secretKey = \u0026#34;secret\u0026#34; func verify(header api.RequestHeaderMap) (bool, string) { token, ok := header.Get(\u0026#34;token\u0026#34;) if ok { return false, \u0026#34;missing token\u0026#34; } path, _ := header.Get(\u0026#34;:path\u0026#34;) hash := md5.Sum([]byte(path + secretKey)) if hex.EncodeToString(hash[:]) != token { return false, \u0026#34;invalid token\u0026#34; } return true, \u0026#34;\u0026#34; } func (f *filter) DecodeHeaders(header api.RequestHeaderMap, endStream bool) api.StatusType { if ok, msg := verify(header); !ok { f.callbacks.SendLocalReply(403, msg, map[string]string{}, 0, \u0026#34;bad-request\u0026#34;) return api.LocalReply } return api.Continue } DecodeHeaders 是扩展 filter 必须实现的方法,我们就是在这个阶段对请求 header 进行校验。\nverfiy 是校验函数,这里的 RequestHeaderMap 是 Go 扩展提供的 interface,我们可以通过它来读写 header,其他都是常见的 Go 代码写法。\n编译 编译很简单,与常见的 Go 编译一样,这里我们使用 Golang 官方的 docker 镜像来编译:\ndocker run --rm -v `pwd`:/go/src/go-filter -w /go/src/go-filter \\ -e GOPROXY=https://goproxy.cn \\ golang:1.19.8 \\ go build -v -o libgolang.so -buildmode=c-shared . Go 编译还是很快的,只需要几秒钟,当前目录下,就会产生一个 libgolang.so 的文件。\n反观 Envoy 的编译速度,一次全量编译,动辄几十分钟,上小时的,这幸福感提升了不止一个档次。\n运行 我们可以使用 Envoy 官方提供的镜像来运行,如下示例:\ndocker run --rm -v `pwd`/envoy.yaml:/etc/envoy/envoy.yaml \\ -v `pwd`/libgolang.so:/etc/envoy/libgolang.so \\ -p 10000:10000 \\ envoyproxy/envoy:contrib-v1.26-latest \\ envoy -c /etc/envoy/envoy.yaml 只需要把上一步编译的 libgolang.so 和 envoy.yaml 挂载进去就可以了。\n值得一提的是,我们需要在 envoy.yaml 配置中启用 Go 扩展,具体是这段配置:\nhttp_filters:- name:envoy.filters.http.golangtyped_config:\u0026#34;@type\u0026#34;: type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Configlibrary_id:examplelibrary_path:/etc/envoy/libgolang.soplugin_name:example-1跑起来之后,我们测试一下:\n$ curl \u0026#39;http://localhost:10000/\u0026#39; missing token $ curl -s \u0026#39;http://localhost:10000/\u0026#39; -H \u0026#39;token: c64319d06364528120a9f96af62ea83d\u0026#39; -I HTTP/1.1 200 OK 符合期望,是不是很简单呢\n后续 什么?这个示例太简单?\n是的,这里主要是体验下开发流程,下篇我们再介绍更高级的玩法:\nGo 接受来自 Envoy 侧的配置,异步 Goroutine,以及与 Istio 配合的用法。\n","excerpt":"背景 MoE,MOSN on Envoy 是 MOSN 团队提出的技术架构,经过近两年的发展,在蚂蚁内部已经得到了很好的验证;并且去年我们也将底层的 Envoy Go 七层扩展贡献了 Envoy 官 …","ref":"https://mosn.io/blog/posts/moe-extend-envoy-using-golang-1/","title":"MoE 系列 - 如何使用 Golang 扩展 Envoy [一]"},{"body":"我们很高兴的宣布 MOSN v1.3.0 发布,以下是该版本的变更日志。\nv1.3.0 重构 迁移合并 Proxy-Wasm 的实现,并默认启用 wazero (#2172) @Crypt Keeper 优化 优化解析 xDS 透明代理配置:增加对未识别地址的透传配置 (#2171) @3062 优化 CI 测试中 golangci 执行流程 (#2166) @taoyuanyuan (#2167) @taoyuanyuan 为 Proxy-Wasm 添加集成基准测试 (#2164) @Crypt Keeper (#2169) @Crypt Keeper 升级 MOSN 支持的 Go 的最低版本至 1.17 (#2160) @Crypt Keeper 改正 README.md 中的一些问题 (#2161) @liaolinrong 新增基准测试 (#2173) @3062 subsetLoadBalancer 重用子集条目以优化分配/使用内存 (#2119) @dzdx (#2188) @liwu Bug 修复 修复 connpool_binging 在连接 upstream timeout 时出现的 panic 问题 (#2180) @EraserTime 修复 cluster LB 算法为 LB_ORIGINAL_DST 时 retryTime 是 0 的问题 (#2170) @3062 修复平滑升级失败 (#2129) @Bryce-Huang (#2193) @3062 修改解析 xDS Listener 日志的方式 (#2182) @3062 修复示例代码打印错误 (#2190) @liaolinrong ","excerpt":"我们很高兴的宣布 MOSN v1.3.0 发布,以下是该版本的变更日志。\nv1.3.0 重构 迁移合并 Proxy-Wasm 的实现,并默认启用 wazero (#2172) @Crypt …","ref":"https://mosn.io/docs/products/report/releases/v1.3.0/","title":"MOSN v1.3.0 发布"},{"body":"我们很高兴的宣布 MOSN v1.2.0 发布,以下是该版本的变更日志。\nv1.2.0 新功能 支持配置 HTTP 重试状态码 (#2097) @dengqian 新增 dev 容器构建配置与说明 (#2108) @keqingyuan 支持 connpool_binding GoAway (#2115) @EraserTime 支持配置 listener 默认读缓存大小 (#2133) @3062 支持 proxy-wasm v2 ABI (#2089) @lawrshen 支持基于 iptables tproxy 的透明代理 (#2142) @3062 重构 删除 MOSN 扩展的 context 框架,使用变量机制代替。将 MOSN 中的变量机制(variable)和内存复用框架(buffer)迁移到 mosn.io/pkg 中 (#2055) @nejisama 迁移 metrics 接口到 mosn.io/api 中 (#2124) @YIDWang Bug 修复 修复部分日志参数缺失 (#2141) @lawrshen 通过 error 判断获取的 cookie 是否存在 (#2136) @greedying ","excerpt":"我们很高兴的宣布 MOSN v1.2.0 发布,以下是该版本的变更日志。\nv1.2.0 新功能 支持配置 HTTP 重试状态码 (#2097) @dengqian 新增 dev …","ref":"https://mosn.io/docs/products/report/releases/v1.2.0/","title":"MOSN v1.2.0 发布"},{"body":"我们很高兴的宣布 MOSN v1.1.0 发布,以下是该版本的变更日志。\nv1.1.0 新功能 TraceLog 支持 zipkin (#2014) @fibbery 支持云边互联 (#1640) @CodingSinger,细节可以参考博客 Trace 以 Driver 的形式支持插件化扩展,使用 Skywalking 作为跟踪实现 (#2047) @YIDWang xDS 支持 stream filter 解析扩展 (#2095) @Bryce-huang stream filter: ipaccess 扩展实现 xDS 解析逻辑 (#2095) @Bryce-huang MakeFile 添加打包 tar 命令 (#1968) @doujiang24 变更 调整连接读超时从 buffer.ConnReadTimeout 到 types.DefaultConnReadTimeout (#2051) @fibbery 修复文档错字 (#2056) (#2057) @threestoneliu (#2070) @chenzhiguo 更新 license-checker.yml 的配置文件 (#2071) @kezhenxu94 新增遍历 SubsetLB 的接口 (#2059) (#2061) @nejisama 添加 tls.Conn 的 SetConfig 接口 (#2088) @antJack 添加 xds-server 作为 MOSN 控制面的示例 (#2075) @Bryce-huang 新增 HTTP 请求解析失败时的错误日志 (#2085) @taoyuanyuan (#2066) @fibbery 负载均衡在重试时跳过上一次选择的主机 (#2077) @dengqian 访问日志支持打印 traceID,connectionID 和 UpstreamConnectionID (#2107) @Bryce-huang 重构 重构 HostSet 的使用方式 (#2036) @dzdx 更改连接写数据调整为只支持同步写的模式 (#2087) @taoyuanyuan 优化 优化创建 subset 负载均衡的算法,降低内存占用 (#2010) @dzdx 支持可扩展的集群更新方式操作 (#2048) @nejisama 优化多证书匹配逻辑:优先匹配 servername,全部 servername 匹配不上以后才按照 ALPN 进行匹配 (#2053) @MengJiapeng Bug 修复 修复 wasm 示例中的 latest 镜像版本为固定的版本(#2033)@antJack 调整 MOSN 退出时日志关闭执行顺序,修复部分退出日志无法正确输出的问题 (#2034) @doujiang24 修复 OriginalDst 匹配成功以后没有正确处理的问题 (#2058) @threestoneliu 修复协议转换场景没有正确处理异常情况的问题,新增协议转换实现规范 (#2062) @YIDWang 修复 stream proxy 没有正确处理连接写超时/断开等异常事件 (#2080) @dengqian 修复连接事件监听时机错误可能引发的 panic 问题 (#2082) @dengqian 避免在事件监听连接之前发生关闭事件 (#2098) @dengqian HTTP1/HTTP2 协议在处理时在上下文中保存协议信息 (#2035) @yidwang 修复 xDS 推送时可能存在的并发问题 (#2101) @yzj0911 找不到 upstream 地址变量时,不再返回空,返回 ValidNotFound (#2049) @songzhibin97 修复健康检查不支持 xDS (#2084) @Bryce-huang 修正判断上游地址方法 (#2093) @dengqian ","excerpt":"我们很高兴的宣布 MOSN v1.1.0 发布,以下是该版本的变更日志。\nv1.1.0 新功能 TraceLog 支持 zipkin (#2014) @fibbery 支持云边互联 (#1640) …","ref":"https://mosn.io/docs/products/report/releases/v1.1.0/","title":"MOSN v1.1.0 发布"},{"body":"前言 MOSN 使用了 subset 算法作为其标签匹配路由负载均衡的方式,本文主要介绍 Subset 的原理,同时在超大规模集群下MOSN的 subset 所遇到的一些性能瓶颈与优化算法。\nSubset 基本原理 在一个集群里,通常机器会有不同的标签,如何将一个请求路由到指定标签的一组机器呢?MOSN 把一个服务下的机器按照机标签组合进行预先分组成多个子集,在请求的时候,根据请求中的metadata信息可以快速查询到这个请求应该匹配到哪个子集。\n当前有4个节点\n[ { \u0026#34;hostname\u0026#34;:\u0026#34;h1\u0026#34;, \u0026#34;metadata\u0026#34;:{ \u0026#34;zone\u0026#34;:\u0026#34;zone1\u0026#34;, \u0026#34;mosn_version\u0026#34;:\u0026#34;version1\u0026#34;, \u0026#34;mosn_aig\u0026#34;:\u0026#34;aig1\u0026#34; } }, { \u0026#34;hostname\u0026#34;:\u0026#34;h2\u0026#34;, \u0026#34;metadata\u0026#34;:{ \u0026#34;zone\u0026#34;:\u0026#34;zone1\u0026#34;, \u0026#34;mosn_version\u0026#34;:\u0026#34;version2\u0026#34;, \u0026#34;mosn_aig\u0026#34;:\u0026#34;aig1\u0026#34; } }, { \u0026#34;hostname\u0026#34;:\u0026#34;h1\u0026#34;, \u0026#34;metadata\u0026#34;:{ \u0026#34;zone\u0026#34;:\u0026#34;zone2\u0026#34;, \u0026#34;mosn_version\u0026#34;:\u0026#34;version1\u0026#34;, \u0026#34;mosn_aig\u0026#34;:\u0026#34;aig1\u0026#34; } }, { \u0026#34;hostname\u0026#34;:\u0026#34;h4\u0026#34;, \u0026#34;metadata\u0026#34;:{ \u0026#34;zone\u0026#34;:\u0026#34;zone2\u0026#34;, \u0026#34;mosn_version\u0026#34;:\u0026#34;version2\u0026#34;, \u0026#34;mosn_aig\u0026#34;:\u0026#34;aig1\u0026#34; } } ] 标签匹配规则会根据 zone 、mosn_aig 、mosn_version 这 3 个字段进行匹配路由,根据这3个key排序后进行组合得到以下匹配路径\n[zone] [mosn_version] [mosn_version, zone] [mosn_aig] [mosn_aig, zone] [mosn_aig, mosn_version] [mosn_aig, mosn_version, zone] 相对应的匹配树如下\n假设需要访问 {zone: zone1, mosn_aig: aig1} , 那么经过排序后查找顺序为 mosn_aig:aig1 -\u0026gt; zone:zone1 , 查找到 [h1, h2]\n以上就是subset的基本原理\nSubset 构建 那么如何构建上述的匹配树呢?首先输入参数有两个\n 带标签的机器列表hosts,比如 [h1, h2, h3, h4]\n 用于匹配的subSetKeys, 比如\n[zone] [mosn_version] [mosn_version, zone] [mosn_aig] [mosn_aig, zone] [mosn_aig, mosn_version] [mosn_aig, mosn_version, zone] 我们阅读源码看一下MOSN的subsetLoadbalancer是如何构建这棵树\nMOSN遍历每一个 Host的labels 和 SubsetKey 递归去创建一棵树\n对于树的每一个节点,都会遍历一次hosts列表过滤出匹配这个节点的kvs的subHosts,每个节点创建一个子 loadbalancer\n构建 profile 在生产的超大集群,我们发现MOSN在接收到注册中心的机器列表推送的时候,出现了较高的cpu毛刺,短暂的内存出现了上涨,gc频率也大幅度增加。\n通过对生产的profile,我们发现 subsetLoadbalancer 的 createSubsets 在cpu和alloc的火焰图中都占比较高。\n下面我们开始编写benchmark优化这一部分的性能\n我们的输入参数为:\n subsetKeys func benchSubsetConfig() *v2.LBSubsetConfig { return \u0026amp;v2.LBSubsetConfig{ SubsetSelectors: [][]string{ {\u0026#34;zone\u0026#34;, \u0026#34;physics\u0026#34;}, {\u0026#34;zone\u0026#34;}, {\u0026#34;physics\u0026#34;}, {\u0026#34;zone\u0026#34;, \u0026#34;physics\u0026#34;, \u0026#34;mosn_aig\u0026#34;}, {\u0026#34;zone\u0026#34;, \u0026#34;physics\u0026#34;, \u0026#34;mosn_version\u0026#34;}, {\u0026#34;zone\u0026#34;, \u0026#34;physics\u0026#34;, \u0026#34;mosn_aig\u0026#34;, \u0026#34;mosn_version\u0026#34;}, {\u0026#34;zone\u0026#34;, \u0026#34;mosn_aig\u0026#34;}, {\u0026#34;zone\u0026#34;, \u0026#34;mosn_version\u0026#34;}, {\u0026#34;zone\u0026#34;, \u0026#34;mosn_aig\u0026#34;, \u0026#34;mosn_version\u0026#34;}, {\u0026#34;physics\u0026#34;, \u0026#34;mosn_aig\u0026#34;}, {\u0026#34;physics\u0026#34;, \u0026#34;mosn_version\u0026#34;}, {\u0026#34;physics\u0026#34;, \u0026#34;mosn_aig\u0026#34;, \u0026#34;mosn_version\u0026#34;}, {\u0026#34;mosn_aig\u0026#34;}, {\u0026#34;mosn_version\u0026#34;}, {\u0026#34;mosn_aig\u0026#34;, \u0026#34;mosn_version\u0026#34;}, }} } 8000个 hosts 每个 hosts 都有4个label, 每个label 3 种value\nfunc BenchmarkSubsetLoadBalancer(b *testing.B) { ps := createHostset(benchHostConfigs(8000, 3)) subsetConfig := benchSubsetConfig() b.Run(\u0026#34;subsetLoadBalancer\u0026#34;, func(b *testing.B) { for i := 0; i \u0026lt; b.N; i++ { newSubsetLoadBalancer(types.RoundRobin, ps, newClusterStats(\u0026#34;BenchmarkSubsetLoadBalancer\u0026#34;), NewLBSubsetInfo(subsetConfig)) } }) } cpu\nalloc_space\n从上面的火焰图可以看到HostMatches和setFinalHost 占较多的cpu_time 和 alloc_space\n我们首先看下HostMatches\n他的作用是判断一个host是不是完全匹配给定的键值对,用于判断这个host有没有匹配这个匹配树节点\n开销主要在于执行次数过多: 树节点数 * len(hosts) 。 在集群变大时,这边的运行开销会大幅度上升。\n下面我们来看一下 setFinalHost\n他的主要逻辑是按IP进行去重,同时会附带copy。\n如果我们在subsetLoadbalancer的顶层进行去重,那么他的任意subset都不需要再次去重的,因此这边可以改成不去重\n构建优化 HostMatches的那么多次匹配中,实际上有很多的重复操作,对host label中某个kv判断equals,在构建过程中重复了相当多的次数,优化的思路可以基于避免这部分重复的开销,预先构建倒排索引出发。\n 输入参数 subsetKeys\n[ {\u0026#34;zone\u0026#34;, \u0026#34;physics\u0026#34;}, {\u0026#34;zone\u0026#34;}, {\u0026#34;physics\u0026#34;, \u0026#34;mosn_aig\u0026#34;, \u0026#34;mosn_version\u0026#34;}, {\u0026#34;zone\u0026#34;, \u0026#34;physics\u0026#34;, \u0026#34;mosn_version\u0026#34;}] ... ] hosts\n[ { \u0026#34;hostname\u0026#34;:\u0026#34;h1\u0026#34;, \u0026#34;metadata\u0026#34;:{ \u0026#34;zone\u0026#34;:\u0026#34;zone1\u0026#34;, \u0026#34;mosn_version\u0026#34;:\u0026#34;version_none\u0026#34;, \u0026#34;mosn_aig\u0026#34;:\u0026#34;aig_none\u0026#34;, \u0026#34;physics\u0026#34;:\u0026#34;m1\u0026#34; } }, { \u0026#34;hostname\u0026#34;:\u0026#34;h2\u0026#34;, \u0026#34;metadata\u0026#34;:{ \u0026#34;zone\u0026#34;:\u0026#34;zone1\u0026#34;, \u0026#34;mosn_version\u0026#34;:\u0026#34;version_none\u0026#34;, \u0026#34;mosn_aig\u0026#34;:\u0026#34;aig_none\u0026#34; } }, { \u0026#34;hostname\u0026#34;:\u0026#34;h3\u0026#34;, \u0026#34;metadata\u0026#34;:{ \u0026#34;zone\u0026#34;:\u0026#34;zone2\u0026#34;, \u0026#34;mosn_version\u0026#34;:\u0026#34;version_none\u0026#34;, \u0026#34;mosn_aig\u0026#34;:\u0026#34;aig_none\u0026#34;, \u0026#34;physics\u0026#34;:\u0026#34;m1\u0026#34; } } ] 遍历一次 hosts 针对每个kv我们用bitmap构建倒排索引 { \u0026#34;zone\u0026#34;: { \u0026#34;zone1\u0026#34;: bitmap(110) \u0026#34;zone2\u0026#34;: bitmap(001) }, \u0026#34;physics\u0026#34;: { \u0026#34;m1\u0026#34;: bitmap(101) }, \u0026#34;mosn_aig\u0026#34;: { \u0026#34;aig_none\u0026#34;: bitmap(111) }, \u0026#34;mosn_version\u0026#34;: { \u0026#34;version_none\u0026#34;: bitmap(111) } } 根据subsetKeys和倒排索引中的kvs,构建出匹配树,因为索引中是去重的与hosts数目无关,这个操作开销占比很低 对于树的每个节点,利用倒排索引中的bitmap做交集快速得到匹配全部kv的hosts的索引bitmap 使用Bitmap中存储的index从hosts中取出对应subHosts构建子loadbalancer,同时此处不需要使用setFinalHosts进行去重 基于上述思路过程开发新的subset preIndex 构建算法,代码参考 https://github.com/mosn/mosn/pull/2010\n添加benchmark进行测试\nfunc BenchmarkSubsetLoadBalancer(b *testing.B) { ps := createHostset(benchHostConfigs(8000, 3)) subsetConfig := benchSubsetConfig() b.Run(\u0026#34;subsetLoadBalancer\u0026#34;, func(b *testing.B) { for i := 0; i \u0026lt; b.N; i++ { newSubsetLoadBalancer(types.RoundRobin, ps, newClusterStats(\u0026#34;BenchmarkSubsetLoadBalancer\u0026#34;), NewLBSubsetInfo(subsetConfig)) } }) b.Run(\u0026#34;subsetLoadBalancerPreIndex\u0026#34;, func(b *testing.B) { for i := 0; i \u0026lt; b.N; i++ { newSubsetLoadBalancerPreIndex(types.RoundRobin, ps, newClusterStats(\u0026#34;BenchmarkSubsetLoadBalancer\u0026#34;), NewLBSubsetInfo(subsetConfig)) } }) } dzdx@B-QBDRMD6M-0201 ~/Documents/mosn-open/pkg/upstream/cluster perf/subsetlb ±✚ go test -bench=^BenchmarkSubsetLoadBalancer . -run=^$ -benchmem 2022-04-14 18:55:42,662 [INFO] [network] [ register pool factory] register protocol: mock factory goos: darwin goarch: amd64 pkg: mosn.io/mosn/pkg/upstream/cluster BenchmarkSubsetLoadBalancer/subsetLoadBalancer-12 9 118816550 ns/op 10105291 B/op 7217 allocs/op BenchmarkSubsetLoadBalancer/subsetLoadBalancerPreIndex-12 246 5452478 ns/op 2427298 B/op 11463 allocs/op PASS ok mosn.io/mosn/pkg/upstream/cluster 4.993s 可以看到相对于之前的构建方式,构建速度快了20倍,alloc_space 减小了75%, alloc次数少量上升,因为需要额外构建一次倒排索引。\n下面观察一下gc:\ndefault: gc 7 @0.247s 5%: 0.006+16+0.017 ms clock, 0.027+0/15/32+0.070 ms cpu, 48-\u0026gt;49-\u0026gt;34 MB, 50 MB goal, 4 P preIndex: gc 7 @0.229s 7%: 0.005+17+0.017 ms clock, 0.021+3.6/17/33+0.069 ms cpu, 44-\u0026gt;46-\u0026gt;33 MB, 45 MB goal, 4 P 相对之前的构建方式,运行期间的内存更小,cpu回收的内存也变少,gc并行扫描的时长小幅上涨,STW时间变短。\n测试一下在不同hosts数目下的优化程度,可以看到在hosts数目较多(\u0026gt;100) , 新的构建算法都会大幅度优于旧的构建算法\n hosts cpu (before) cpu (after) alloc (before) alloc (after) 20 0.23ms 0.35ms 65KB 189KB 100 0.93ms 0.38ms 154KB 214KB 500 8.1ms 0.56ms 632KB 322KB 2000 27ms 1.3ms 2.4MB 738KB 8000 102ms 5.1ms 10MB 2.4MB 总结 通过预先构建 bitmap 倒排索引加速subset构建,cpu有超过10倍的提升,alloc_space也降低了数倍,gc的扫描时长小幅增加,STW时长小幅减少\n","excerpt":"前言 MOSN 使用了 subset 算法作为其标签匹配路由负载均衡的方式,本文主要介绍 Subset 的原理,同时在超大规模集群下MOSN的 subset 所遇到的一些性能瓶颈与优化算法。 …","ref":"https://mosn.io/blog/posts/mosn-optimize-build-subset/","title":"构建 subset 优化"},{"body":"前言 对于系统的性能尖刺问题,我们通常使用Go官方内置的pprof包进行分析,但是难点是对于一闪而过的“尖刺”, 开发人员很难及时地保存现场:当你收到告警信息,从被窝中爬起来,打开电脑,链接VPN,系统说不定都已经重启三四趟了。\nMOSN社区的Holmes是一个基于golang实现的,轻量级性能监控系统,当应用的性能指标 发生了异常波动时,holmes会在第一时间保留现场,让你第二天上班可以一边从容地喝着枸杞茶,一边追查问题的根因。\n本文将介绍如何使用 holmes对您的应用进行监控,并简单分析了holmes的实现原理。\nQuick Start 使用holmes的方式十分简单,只需要在您的系统初始化逻辑内添加以下代码:\n// 配置规则 h, _ := holmes.New( holmes.WithCollectInterval(\u0026#34;5s\u0026#34;), // 指标采集时间间隔 holmes.WithDumpPath(\u0026#34;/tmp\u0026#34;), // profile保存路径 holmes.WithCPUDump(10, 25, 80, 2 * time.Minute), // 配置CPU的性能监控规则 holmes.WithMemDump(30, 25, 80, 2 * time.Minute),// 配置Heap Memory 性能监控规则 holmes.WithGCHeapDump(10, 20, 40, 2 * time.Minute), // 配置基于GC周期的Heap Memory 性能监控规则 holmes.WithGoroutineDump(500, 25, 20000, 100*1000, 2 * time.Minute), //配置Goroutine数量的监控规则 ) // enable all h.EnableCPUDump(). EnableGoroutineDump(). EnableMemDump(). EnableGCHeapDump().Start() 类似于holmes.WithGoroutineDump(min, diff, abs,max,2 * time.Minute)的API含义为:\n 当goroutine指标满足以下条件时,将会触发dump操作。 current_goroutine_num \u0026gt; 10 \u0026amp;\u0026amp; current_goroutine_num \u0026lt; 100*1000 \u0026amp;\u0026amp; current_goroutine_num \u0026gt; 125% * previous_average_goroutine_num ||current_goroutine_num \u0026gt; 2000.\n 当goroutine数大于max时,holmes会跳过本次dump操作,因为当goroutine数过大时,goroutine dump操作成本很高。\n 2 * time.Minute 是两次dump操作之间最小时间间隔,避免频繁profiling对性能产生的影响。\n 更多使用案例点击这里。\nProfile Types holmes支持以下五种Profile类型,用户可以按需配置。\n mem: 内存分配 cpu: cpu使用率 thread: 线程数 goroutine: 协程数 gcHeap: 基于GC周期监控的内存分配 指标采集 mem, cpu, thread, goroutine这四种类型是根据用户配置的CollectInterval,每隔一段时间采集一次应用当前的性能指标, 而gcHeap时基于GC周期采集性能指标。本小节会分析一下两种指标。\n根据CollectInterval周期采集 holmes每隔一段时间采集应用各项指标,并使用一个固定大小的循环链表来存储它们。\n根据GC周期采集 在一些场景下,我们无法通过定时的memory dump保留到现场, 比如应用在一个CollectInterval周期内分配了大量内存, 又快速回收了它们,此时holmes在周期前后的采集到内存使用率没有产生过大波动,与实际情况不符。\n为了解决这种情况,holmes开发了基于GC周期的 Profile类型,它会在堆内存使用率飙高的前后两个GC周期内各dump一次profile,然后开发人员可以使用pprof --base命令去对比 两个时刻堆内存之间的差异。 具体实现介绍。\n根据GC周期采集到的数据也会放在循环列表中。\n规则判断 本小节介绍holmes是如何根据规则判断系统出现异常的。\n阈值含义 每个Profile都可以配置min,diff,abs,coolDown四个指标,含义如下:\n 当前指标小于min时,不视为异常。 当前指标大于(100+diff)*100%*历史指标,说明系统此时产生了波动,视为异常。 当前指标大于abs(绝对值),视为异常。 cpu和goroutine这两个profile类型提供max参数配置,基于以下考虑。\n cpu 的profiling操作大约会有5%的性能损耗, 所以当在cpu过高时,不应当进行profiling操作,否则会拖垮系统。 当goroutine数过大时,goroutine dump操作成本很高,会进行STW操作,从而拖垮系统。 Warming up 当holmes启动时,会根据CollectInterval周期采集十次各项指标,在这期间内采集到的指标只会存入循环链表中,不会进行规则判断。\n扩展功能 除了基本的监控之外,holmes还提供了一些扩展功能。\n事件上报 您可以通过实现Reporter 来实现以下功能:\n 发送告警信息,当holmes触发Dump操作时。 将Profiles上传到其他地方,以防实例被销毁,从而导致profile丢失,或进行分析。 type ReporterImpl struct{} func (r *ReporterImple) Report(pType string, buf []byte, reason string, eventID string) error{ // do something\t } ...... r := \u0026amp;ReporterImpl{} // a implement of holmes.ProfileReporter Interface. h, _ := holmes.New( holmes.WithProfileReporter(reporter), holmes.WithDumpPath(\u0026#34;/tmp\u0026#34;), holmes.WithLogger(holmes.NewFileLog(\u0026#34;/tmp/holmes.log\u0026#34;, mlog.INFO)), holmes.WithBinaryDump(), holmes.WithMemoryLimit(100*1024*1024), // 100MB holmes.WithGCHeapDump(10, 20, 40, time.Minute), ) 动态配置 您可以通过Set方法在应用运行时更新holmes的配置。它的使用十分简单,和初始化时的New方法一样。\n有些配置时不支持动态更改的,比如Core数,如果在系统运行期间更改这个参数,会导致CPU使用率产生巨大 波动,从而触发Dump操作。\nh.Set( WithCollectInterval(\u0026#34;2s\u0026#34;), WithGoroutineDump(10, 10, 50, 90, time.Minute)) 配置中心支持 利用Set方法,您可以轻松地对接自己公司的配置中心,比如,将holmes作为数据面,配置中心作为控制面。 并对接告警系统(邮件/短信等),搭建一套简单的监控系统。\n具体架构如下:\n总结 本文简单地介绍了Holmes的使用方法与原理。希望holmes能在您提高应用的稳定性时帮助到你。\n参考 Holmes 无人值守的自动 dump(一) 无人值守的自动 dump(二) go 语言 pprof heap profile 实现机制 ","excerpt":"前言 对于系统的性能尖刺问题,我们通常使用Go官方内置的pprof包进行分析,但是难点是对于一闪而过的“尖刺”, 开发人员很难及时地保存现场:当你收到告警信息,从被窝中爬起来,打开电脑,链接VPN,系 …","ref":"https://mosn.io/blog/posts/mosn-holmes-design/","title":"Holmes 原理浅析"},{"body":"MOSN(Modular Open Smart Network)是一款主要使用 Go 语言开发的云原生网络代理平台,由蚂蚁集团开源并经过双11大促几十万容器的生产级验证,具备高性能、易扩展的特点。 MOSN 可以和 Istio 集成构建 Service Mesh,也可以作为独立的四、七层负载均衡,API Gateway、云原生 Ingress 等使用。\nMOSN的反向通道实现 在云边协同的网络场景,通常都是单向网络,云侧节点无法主动发起连接与边缘节点通讯。这种限制极大程度保证了边缘节点的安全,但缺点也很明显,即只允许边缘节点主动发起访问云端节点。\n云边隧道旨在解决云端无法主动访问边缘节点的问题,其本质是一个反向通道(后文统称为反向通道)。通过在边缘侧主动发起建连的方式与云端节点之间构建一条专用的全双工连接,用来传输云端节点的请求数据和回传最终的响应结果。\n目前例如SuperEdge、Yurttunnel等业界知名云边协同开源框架,对于云边通信的实现方案都基于反向通道。\n本文将着重介绍MOSN之上的反向通道运作流程和原理。\n总体架构如下所示(图中箭头表示TCP建连反向):\n整个运作流程简单概括:\n 边缘侧的mosn实例(后文统称为tunnel agent)在启动时tunnel agent相关服务协程。 通过指定的静态配置或者动态服务发现方式拿到需要反向建连的公有云侧的mosn server地址列表(后文统称tunnel server),并且建立反向连接。 云侧的Frontend与tunnel server侧的转发端口进行数据交互,这部分数据会被托管到之前建立的反向连接进行发送。 边缘节点接受到请求之后,再将请求转发给实际的后端目标节点,回包过程则原路返回。 反向通道启动过程 MOSN Agent通过ExtendConfig特性,在MOSN启动时加载和完成初始化Tunnel Agent的工作。\nExtendConfig中定义AgentBootstrapConfig结构如下:\ntype AgentBootstrapConfig struct { Enable bool `json:\u0026quot;enable\u0026quot;` // The number of connections established between the agent and each server ConnectionNum int `json:\u0026quot;connection_num\u0026quot;` // The cluster of remote server Cluster string `json:\u0026quot;cluster\u0026quot;` // After the connection is established, the data transmission is processed by this listener HostingListener string `json:\u0026quot;hosting_listener\u0026quot;` // Static remote server list StaticServerList []string `json:\u0026quot;server_list\u0026quot;` // DynamicServerListConfig is used to specify dynamic server configuration DynamicServerListConfig struct { DynamicServerLister string `json:\u0026quot;dynamic_server_lister\u0026quot;` } // ConnectRetryTimes ConnectRetryTimes int `json:\u0026quot;connect_retry_times\u0026quot;` // ReconnectBaseDuration ReconnectBaseDurationMs int `json:\u0026quot;reconnect_base_duration_ms\u0026quot;` // ConnectTimeoutDurationMs specifies the timeout for establishing a connection and initializing the agent ConnectTimeoutDurationMs int `json:\u0026quot;connect_timeout_duration_ms\u0026quot;` CredentialPolicy string `json:\u0026quot;credential_policy\u0026quot;` // GracefulCloseMaxWaitDurationMs specifies the maximum waiting time to close conn gracefully GracefulCloseMaxWaitDurationMs int `json:\u0026quot;graceful_close_max_wait_duration_ms\u0026quot;` TLSContext *v2.TLSConfig `json:\u0026quot;tls_context\u0026quot;` } ConnectionNum:tunnel agent和每个tunnel server建立的物理连接数量。\nHostingListener:指定agent建立连接之后托管的mosn listener,即tunnel server发来的请求会由该listener托管处理。\nDynamicServerListConfig:动态tunnel server的服务发现相关配置,可通过自定义的服务发现组件提供动态的地址服务。\nCredentialPolicy: 自定义的连接级别的鉴权策略配置。\nTLSContext:MOSN TLS配置,提供TCP之上通信的保密性和可靠性。\n针对每个远端的tunnel server实例,agent对应一个AgentPeer对象,启动时除了主动建立ConnectionNum个反向通信连接,还会额外建立一条旁路连接,这条旁路连接主要是用来发送一些管控参数,例如平滑关闭连接、调整连接比重。\nfunc (a *AgentPeer) Start() { connList := make([]*AgentClientConnection, 0, a.conf.ConnectionNumPerAddress) for i := 0; i \u0026lt; 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方法进行具体的初始化完整的反向连接,采取指数退避的方式保证在最大重试次数之内建连成功。\nfunc (a *connection) initConnection() error { var err error backoffConnectDuration := a.reconnectBaseDuration for i := 0; i \u0026lt; a.connectRetryTimes || a.connectRetryTimes == -1; i++ { if a.close.Load() { return fmt.Errorf(\u0026quot;connection closed, don't attempt to connect, address: %v\u0026quot;, a.address) } // 1. 初始化物理连接和传输反向连接元数据 err = a.init() if err == nil { break } log.DefaultLogger.Errorf(\u0026quot;[agent] failed to connect remote server, try again after %v seconds, address: %v, err: %+v\u0026quot;, 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 } 该方法主要步骤:\n a.init()方法会调用initAgentCoreConnection`方法初始化物理连接并完成建连交互过程。tunnel server通过agent传输的元数据信息,进行管理反向连接。具体的交互过程和协议后文会细讲。 建连成功之后,tunnel agent托管raw conn给指定的listener。之后该raw conn的生命周期由该listener全权管理,并且完全复用该listener的能力。 其定义了初始化反向连接的交互流程,具体代码细节可以看pkg/filter/network/tunnel/connection.go:250,本文不展开技术细节。\n交互过程 目前MOSN的反向通道只支持了raw conn的实现,因此定义了一套简单明了的网络通信协议。\n主要包括:\n 协议魔数:2 byte 协议版本:1 byte 主体结构类型:1 byte,包括初始化、平滑关闭等。 主体数据长度:2 byte json序列化的主体数据 MOSN反向通道完整的生命周期交互过程:\n建连过程中由tunnel agent主动发起,并且在TCP连接建立成功(TLS握手成功)之后,将反向建连的关键信息ConnectionInitInfo序列化并传输给对端tunnel server,该结构体定义了反向通道的元数据信息。\n// ConnectionInitInfo is the basic information of agent host, // it is sent immediately after the physical connection is established type ConnectionInitInfo struct { ClusterName string `json:\u0026quot;cluster_name\u0026quot;` Weight int64 `json:\u0026quot;weight\u0026quot;` HostName string `json:\u0026quot;host_name\u0026quot;` CredentialPolicy string `json:\u0026quot;credential_policy\u0026quot;` Credential string `json:\u0026quot;credential\u0026quot;` Extra map[string]interface{} `json:\u0026quot;extra\u0026quot;` } tunnel server接受该元数据信息之后,主要工作包括:\n 如果有设置自定义鉴权方式,则进行连接鉴权。 clusterManager将该连接加入到指定的ClusterSnapshot并回写建连结果。 此时建连过程才算完成。\nfunc (t *tunnelFilter) handleConnectionInit(info *ConnectionInitInfo) api.FilterStatus { // Auth the connection conn := t.readCallbacks.Connection() if info.CredentialPolicy != \u0026quot;\u0026quot; { // 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 } 然后是通信过程,为了便于理解,以下图请求单向流转示意图举例,\n在传统的MOSN sidecar应用场景中,Frontend发送的请求首先经过Client-MOSN,然后通过路由模块,主动创建连接(虚线部分)并流转到对端,经由Server-MOSN biz-listener处理转交给Backend。\n而在云边场景的反向通道实现中,Client MOSN(tunnel server)在接受到对端tunnel agent发起创建反向通道的请求后,即将该物理连接加入路由到对端MOSN的cluster snapshot中。从而Frontend的请求流量能由该反向通道流转到对端MOSN,而因为tunnel agent侧把该连接托管给了biz-listener,则读写处理都由biz-listener进行处理,biz-listener将处理完的请求再转发给真正的Backend服务,\n总结和规划:\n本文主要介绍了MOSN反向通道的实现原理和设计思路,MOSN作为高性能的云原生网络代理,希望反向通道的能力能更加有效地支持其作为云边协同场景中承接东西向流量的职责。\n当然,后续我们也会继续做一系列的拓展支持,包括但不限于:\n 反向通道支持gRPC实现,gRPC作为云原生时代最通用的服务通讯框架,本身内置了各种强大的治理能力。 结合更多云原生场景,内置更加通用的tunnel server动态服务发现能力组件。 更多的配套自动化运维和部署工具。 ","excerpt":"MOSN(Modular Open Smart Network)是一款主要使用 Go 语言开发的云原生网络代理平台,由蚂蚁集团开源并经过双11大促几十万容器的生产级验证,具备高性能、易扩展的特点。 …","ref":"https://mosn.io/blog/posts/mosn-tunnel/","title":"MOSN 反向通道详解"},{"body":"我们很高兴的宣布 MOSN v0.26.0 发布,以下是该版本的变更日志。\nv0.26.0 不兼容变更 为了更自然的添加扩展协议,新版对 XProtocol 进行了重构,XProtocol 不再是一种协议,而是便于协议扩展实现的框架。 扩展协议的实现需要一些调整,具体请见 XProtocol协议改造适配指南\n新功能 新增 ip_access filter,基于来源 IP 的 ACL 控制器 (#1797) @Bryce-huang 允许 Admin Api 扩展验证方法 (#1834) @nejisama transcoder filter:支持通过配置指定阶段,取代固定的阶段 (#1815) @YIDWang 为 tls connection 增加 SetConnectionState 方法,在 pkg/mtls/crypto/tls.Conn 中 (#1804) @antJack 增加了 after-start 和 after-stop 这两个新的执行阶段,并允许在这两个阶段注册处理函数 @doujiang24 新增 uds_dir 配置项,用于指定 unix domain socket 的目录 (#1829) @dengqian 支持go plugin加载协议转化插件,并支持动态选择协议转换插件 @Tanc010 增加更多的 HTTP 协议方法,使动态协议匹配更加精准 (#1870) @XIEZHENGYAO 支持动态设置上游协议 (#1808) @YIDWang 支持动态设置 HTTP 默认最大值配置 #1886 @nejisama 变更 将 HTTP 协议的默认最大请求头大小调整到 8KB (#1837) @nejisama 重构默认的 HTTP1 和 HTTP2 的协议转换,删除了 proxy 中的转换,使用 transcoder filter 来代替 @nejisama transcoder filter:使用注册转换器工厂来替代注册转换器 (#1879) @YIDWang Bug 修复 修复:HTTP buffer 复用在高并发场景下可能导致 nil panic @nejisama 修复:response_flag 变量值获取错误 (#1814) @lemonlinger 修复:prefix_write 在 \u0026ldquo;/\u0026rdquo; 的场景下不能正常工作 @Bryce-huang 修复:在热升级过程中,手动 kill 老的 MOSN,可能会导致新 MOSN 的 reconfig.sock 会被错误的删除 (#1820) @XIEZHENGYAO 修复:请求上游失败时,在 doretry 中不应该直接设置 setupRetry (#1807) @taoyuanyuan 修复:热升级中继承了老 MOSN 的配置之后,应该将配置设置到新的 MOSN 结构体中 @XIEZHENGYAO 修复:当取消客户端的 grpc 的时候,没有发送 resetStreamFrame 到上游,使得 server 端没有及时结束 @XIEZHENGYAO 修复:应该在关闭 stream connection 之前设置 resetReason,否则可能导致获取不到真实的原因 (#1828) @wangfakang 修复:当有多个匹配的 listener 的时候,应该选择最优的匹配的 listener,否则可能导致 400 错误 @MengJiapeng 修复:HTTP2 协议处理 broadcast 可能导致 map 并发读写 panic @XIEZHENGYAO 修复:XProtocol 连接池(binding connpool) 中的内存泄漏 (#1821) @Dennis8274 修复:应该将 close logger 放在最后,否则在关闭 MOSN 实例过程中将没有日志输出 (#1845) @doujiang24 修复:XProtocol PingPong 类型连接超时的时候,因为 codecClient 没有初始化,会导致 panic (#1849) @cuiweixie 修复:当 unhealthyThreshold 是一个空值时,健康检查将不会工作,修改为空值时使用默认值 (#1853) @Bryce-huang 修复:WRR 负载均衡算法可能导致死循环(发生在 unweightChooseHost)#1860 @alpha-baby 修复:direct response 中 hijack 不应该再执行转换 @nejisama 修复:当一个不健康的 host 有很高的权重时,EDF wrr 将不再选择其他健康的 host @lemonlinger 修复:Istio LDS 中的 CACert 文件名获取错误,导致 MOSN listen 失败,不会接受请求 (#1893). @doujiang24 修复:DNS 解析 STRICT_DNS_CLUSTER 中 host 的 goroutine 没法停止 #1894 @bincherry ","excerpt":"我们很高兴的宣布 MOSN v0.26.0 发布,以下是该版本的变更日志。\nv0.26.0 不兼容变更 为了更自然的添加扩展协议,新版对 XProtocol 进行了重构,XProtocol 不再是一种 …","ref":"https://mosn.io/docs/products/report/releases/v0.26.0/","title":"MOSN v0.26.0 发布"},{"body":"前言 MOSN 作为 Sidecar 容器部署时,对于 MOSN 容器自身的升级,可以采用的一种形式是先将业务切流,再升级版本。这种形式用户POD需要销毁、重新调度、重建,带来较大开销的同时,也可能影响业务的稳定性。\n为此 MOSN 提供了对业务无感的热升级方式,具体的原理可移步这里查阅。\n本文将介绍如何利用 OpenKruise 对 Kubernetes 的扩展 SidecarSet,来具体操作 MOSN 容器的原地热升级。\n环境准备 在 k8s 中安装 OpenKruise, 安装步骤见 OpenKruise 官方文档\n测试步骤 打包 MOSN 镜像 本文利用当前 MOSN 社区提供的构建镜像的方式来准备测试用的 MOSN 镜像,在构建镜像之前需要做如下调整:\n 修改etc/supervisor/supervisord.conf, 因为当前 supervisor 配置使用/dev/shm/目录,同一 POD 内不同容器之间会冲突,会导致新容器启动失败 [unix_http_server] -file=/dev/shm/supervisor.sock +file=/var/run/supervisor_mosn.sock 修改 MOSN 配置文件configs/mosn_config.json, 添加配置: { \u0026quot;uds_dir\u0026quot;: \u0026quot;/home/admin/mosn/logs\u0026quot;, \u0026quot;inherit_old_mosnconfig\u0026quot;: true, ... } 其中uds_dir为 MOSN reconfig.sock存储目录,该目录需要挂载为共享卷,使得热升级过程中两个容器之间可以相互访问。\n如果从pilot获取动态下发的配置,则需要添加inherit_old_mosnconfig配置项,新的 MOSN 进程启动之后继承老的 MOSN 进程监听的fd,避免端口冲突导致进程启动失败。\n 构建镜像,并上传至可访问的镜像仓库 make image docker tag [imageid] [repo:tag] docker push [repo:tag] 创建SidecarSet资源 注意 image 修改为自己使用版本 volume 挂载目录和配置文件中uds_dir目录保持一致 $ kubectl apply -f sidecarset.yaml $ cat sidecarset.yaml apiVersion: apps.kruise.io/v1alpha1 kind: SidecarSet metadata: name: mosn spec: containers: - image: tinyqian/mosn:v1 imagePullPolicy: IfNotPresent name: mosn podInjectPolicy: BeforeAppContainer resources: {} shareVolumePolicy: type: enabled terminationMessagePath: /dev/termination-log terminationMessagePolicy: File upgradeStrategy: hotUpgradeEmptyImage: openkruise/hotupgrade-sample:empty upgradeType: HotUpgrade volumeMounts: - mountPath: /home/admin/mosn/logs name: mosn-log lifecycle: postStar: exec: command: - /bin/sh - -c - sleep 30 injectionStrategy: {} namespace: default selector: matchLabels: mesh.apsara-edge.com/mosn-injected: \u0026quot;true\u0026quot; updateStrategy: maxUnavailable: 1 partition: 0 type: RollingUpdate volumes: - emptyDir: {} name: mosn-log 部署测试应用 apiVersion: extensions/v1beta1 kind: Deployment metadata: labels: mesh.apsara-edge.com/mosn-injected: \u0026quot;true\u0026quot; name: test-app spec: selector: matchLabels: mesh.apsara-edge.com/mosn-injected: \u0026quot;true\u0026quot; replicas: 1 template: metadata: labels: mesh.apsara-edge.com/mosn-injected: \u0026quot;true\u0026quot; spec: containers: - name: test-app image: nginx:1.20 部署应用之后,查看 POD\n$ kubectl get pods -n ppe-t-e3496bc319e941ed8ec8349b8c65b366-cn3691 -o wide NAME READY STATUS RESTARTS AGE test-app-5fd64d788c-g9wg9 3/3 Running 0 2m56s 可以看到 POD 一共创建了三个容器,一个是业务容器,一个是 MOSN 容器,一个是 empty容器。\n测试升级镜像版本 部署成功后,可以测试升级 MOSN 容器的镜像版本,更新前面创建的 sidecarset 资源的 image 版本。\n$ kubectl edit sidecarset mosn application.core.oam.dev/mosn edited 升级过程中可以看到 RESTARTS 字段变化,POD内容器有重启,但是POD没有重建\nNAME READY STATUS RESTARTS AGE test-app-5fd64d788c-g9wg9 3/3 Running 2 10m 实际的升级过程为:\n empty 容器升级为 MOSN v2 版本容器\n MOSN v2 版本容器和v1版本容器之间进行连接迁移\n MOSN v1 版本容器退出,变更为 empty 容器\n 查看升级过程中的进程可以发现,在v1版本的容器没有退出之前,新老容器中的进程是并存的\n同时,升级的过程中你可以不断地发送通过 MOSN 代理的测试请求,可以验证升级过程中 MOSN 一直处于可用状态,对业务是无损的。\n总结 通过上述步骤,可以实现用户 POD 和 sidecar 容器的部署,同时实现 sidecar 容器的原地热升级,做到 sidecar 容器升级业务无感。 需要注意的是 SidecarSet 支持image版本升级,修改其他字段将会导致容器无法升级。\n参考 OpenKruise SidecarSet 介绍\nOpenKruise 安装指南\n","excerpt":"前言 MOSN 作为 Sidecar 容器部署时,对于 MOSN 容器自身的升级,可以采用的一种形式是先将业务切流,再升级版本。这种形式用户POD需要销毁、重新调度、重建,带来较大开销的同时,也可能影 …","ref":"https://mosn.io/blog/posts/mosn-sidecarset-hotupgrade/","title":"利用 SidecarSet 实现 MOSN 容器原地热升级"},{"body":"前言 MOSN 作为网络边缘代理组件,路由功能是核心功能,本文将介绍 MOSN 路由如何使用,以及 MOSN 路由的一些高级使用技巧,MOSN 官网介绍了路由功能的基本使用配置: 点击链接\n路由基本设计 在 MOSN 的路由设计中,cluster 和 route 是高度关联的,说白了,route 的配置,就是为了表达如何准确的找到你想找到的 cluster,另外,一个 cluster 可以有多个 host 机器,例如一个 cluster 有 100 台机器,其中有50台是 v1 版本,50台是 v2 版本,如何根据一些特定的规则,准确地把请求路由到 v1 版本或者 v2 版本呢?\n再例如,我想根据 header 里的某个值,再将这个值和“配置中心”里的某个值进行计算,才能找到 cluster,那么我该如何配置呢?\n首先,我们看最简单的路由设置。\n上图是一个简单的 json 配置,其中,cluster manager 和 routers 的配置,是路由的关键。我们可以 cluster manager 中配置多个 cluster,每个 cluster 配置多个 host。\n然后在 routers 配置中,根据一些规则,告诉 MOSN,如何将请求路由到 cluster 中。如下图:\n此配置表示,现在有一个 rouer 配置,名为 server_router,有一个虚拟主机,可配置多个域名,这里匹配所有域名,同时,这个域名有多个路由配置,这里暂且配置了一个路由配置:前缀匹配,只要是 / 开头的,就转发到 serverCluster 里的 host 中,也就是下面的 cluster manager 配置里的 serverCluster。\n这样,就实现了一个简单的 mosn 路由的配置。\n动态路由 cluster 大部分情况下,如果我们的路由逻辑很简单,例如根据 header 里的某个名字,找到对应的 cluster,代码或者配置就是这么写的:\nrouter := v2.Router{ // header 匹配 RouterConfig: v2.RouterConfig{ Match: v2.RouterMatch{ Headers: []v2.HeaderMatcher{ // 这个 header 匹配, 就转发到 app.Name cluster. { Name: \u0026#34;X-service-id\u0026#34;, Value: app.Name, }, }, }, // cluster 名称匹配. Route: v2.RouteAction{ RouterActionConfig: v2.RouterActionConfig{ ClusterName: app.Name, }, }, }, } r.VirtualHosts[0].Routers = append(r.VirtualHosts[0].Routers, router) 上面代码的意思是如果 header 里有 X-service-id 这个 kv,那么就能找到下面 RouteAction 对应的 Cluster 了。\n那如果是更复杂的逻辑呢?比如利用请求里的 header 和“配置中心”的某个值进行计算,才能找到 cluster?\n此时,通过配置已经无法解决这个需求,因为这其中涉及到了计算逻辑。\nMOSN 通过动态配置可以支持该需求。如下图配置:\n我们设置了一个 \u0026quot;cluster_variable\u0026quot;: \u0026quot;My-ClusterVariable\u0026quot; 的 KV 配置。\n同时,我们还需要在 StreamFilter 中,利用变量机制,设置 key 为 “My-ClusterVariable” 的 value,这个 value 就是计算出来的 Cluster 名称。\n代码如下\n// 先注册这个 key 到变量表中。 func init() { variable.Register(variable.NewStringVariable(\u0026#34;My-ClusterVariable\u0026#34;, nil, nil, variable.DefaultStringSetter, 0)) } var clusterMap = make(map[int]string, 0) func (f *MyFilter) OnReceive(ctx context.Context, headers api.HeaderMap, buf buffer.IoBuffer, trailers api.HeaderMap) api.StreamFilterStatus { l := len(clusterMap) // 找 Cluster \tcluster := // 执行一些计算 // 设置到上下文变量中。这个 key 必须和配置文件中保持一致。 \tvariable.SetString(ctx, \u0026#34;My-ClusterVariable\u0026#34;, cluster) return api.StreamFilterContinue } 上面的代码展示了如何基于变量机制动态的找到 Cluster,这种机制在面对复杂路由逻辑的场景时,能够解决你的问题。\nMOSN subset 如上面所述,我们经常有在一个集群里,有多个版本,如何根据某些标签,将请求路由到指定的版本呢?通常,我们会使用 subset 方案,即,子集合,可在一个 cluster 里面,为每个应用打标,同时我们的路由也配置相关的配置(MOSN 称为 metadata),实现较为复杂的路由。\nMOSN 官方文档中,简单介绍了 metadata 的使用:metadata\n下面让我们更详细的介绍 subset 的使用。\n上图中,左边是 cluster host 配置,右边是 router 配置.\n这个路由配置的 match 意思是,当请求者的 header 里指定了 name 和 value, 且其值匹配这个路由值 service 和 service.green, 那么该请求就被路由到了这个 cluster_subset 集群中。\n然后, 这个集群可能有多个机器, 那么需要这个机器的元数据和路由配置的元数据相同, 必须都是 subset:green, 才能匹配上这个 Host,否则提示找不到(fall_back_policy 策略是 0 为前提)。\n由此,我们解决了一个 cluster 里面有多个版本的 host 的路由问题。\n再进一步,一个 cluster 会有多个 host,每个 host 可能有不同的 subset,这可能就需要很多的路由,如果都使用配置文件的方式写死,就比较麻烦。\nMOSN 支持基于 stream filter 的方式,设置动态路由。如下:\n基于 MOSN 的变量机制,在请求级别的 VarRouterMeta 中设置 kv metadata 组合,效果和上面配置文件的方式类似。\n另外, 如果路由配置中配置 MetaData, 请求级别也配置了 MetaData, 那么, MOSN 会将 2 个元数据进行合并, 来和 Host 进行匹配. 这个逻辑在 pkg/proxy/downstream.go:1497 代码中有体现.\n来个简单的例子:\n例如分组里指定机器调用;\n1 请求时, 可在 header 里,指定 ip,并在 varRouterMeta 里设置这个 ip。\n2 host 配置,可在 metadata 里,配置 ip kv,例如 ip:192.168.2.3; 如下图:\n这样,就能匹配到指定机器了。\nps: 关于这个例子,我们其实也可以使用 MOSN 的 ORIGINAL_DST 机制,将 cluster 的 type 设置为 ORIGINAL_DST(MOSN 还支持 DNS 集群类型),然后配置 cluster.original_dst_lb_config.use_header = true, 这样,我们请求的时候,在 header 里加入 host = {目标地址}, MOSN 就会根据这个指定的 host header 进行转发。当然,MOSN 也可以自定义名字,不一定要叫 host。\n来个复杂的例子。 假设一个场景:单个 host 存在于多个分组,而请求时,只能指定一个分组。如下图:\n我们现在有 2 台机器,共 3 个分组,AAA,BBB,CCC。每个机器都包含 AAA 分组。 现在有 3 个请求,每个请求都是不同的分组,此时,我们该如何配置 元数据呢? 首先,本质上,给机器加分组,其实就是打标。我们将元数据想象成 tag 列表即可。\n上面的代码,展示了:我们将多个分组标签,转换成 MOSN 可以认识的元数据 kv,每个标签对应一个固定的 value true(为什么设置为 true 呢?value 自身其实在 MOSN 的 subsetLB 中是有含义的,即最终根据请中携带的 metadata 的值去匹配 cluster 中满足条件的 subset host entry。但由于 metadata 是个 map, 而因为我们这个例子的特殊性,只能使用 key 自身做分组,所有的 value 都保持一样,本质上任何值都是可以的)。同时注意,这些 key,都要保存到 SubsetSelectors 中,否则,MOSN 无法识别。 每次调用时,我们在 filter 里,从 header 里面取出分组标签,然后设置进“上下文变量”中。例如:\n这样,我们就能够完成更加复杂的分组路由。\n那 MOSN 是如何寻找 subset 的呢?代码如下:\n当执行 choose host 时,subsetLoadBalancer.findSubset 函数会根据当前请求的元数据,从 subSetLoadbalancer 里找出匹配的 host List。\n总结 总结一下,我们先讲了基于简单的配置,来实现简单的 router 和 cluster 的配置文件路由。再讲了可以基于 stream filter 的方式实现动态寻找 cluster。同时 MOSN 支持 subset,可以基于 route 配置文件来进行路由和 cluster host 进行匹配,如果逻辑复杂,也可以基于 stream filter + varRouterMeta 变量的方式来 动态寻找 subset。\n其实大部分情况下,我们用 json 配置就能解决我们的路由问题。如果复杂的话,我们就用 stream filter + varRouterMeta / stream filter + cluster_variable 这两种动态机制解决我们的需求。下面尝试用一张图来结束本文。\n参考 Router 配置 MOSN SubsetLB 开发文档 Load Balancer Subsets\n","excerpt":"前言 MOSN 作为网络边缘代理组件,路由功能是核心功能,本文将介绍 MOSN 路由如何使用,以及 MOSN 路由的一些高级使用技巧,MOSN 官网介绍了路由功能的基本使用配置: 点击链接\n路由基本设 …","ref":"https://mosn.io/blog/posts/how-use-dynamic-metadata/","title":"MOSN 路由框架详解"},{"body":"我们很高兴的宣布 MOSN v0.25.0 发布,以下是该版本的变更日志。\nv0.25.0 新功能 路由支持删除请求头指定字段的配置 @wangfakang WASM 支持 Reload @zu1k 集成 SEATA TCC 模式,支持 HTTP 协议 [@dk-lockdown]((https://github.com/dk-lockdown) 新增 boltv2 协议的 tracelog 支持 @nejisama gRPC 框架新增 Metrics 统计相关 Filter 扩展 @wenxuwan 新增 xds cluster 解析支持 DNS 相关字段 @antJack 重构 MOSN 核心代码和 Istio 引入相关 xDS 代码解耦合 @nejisama 更新 proxy-wasm-go-host 版本 @zhenjunMa 修改 networkfilter 配置解析逻辑,支持更新添加接口、查询接口 @antJack 优化 Makefile 中执行模式使用mod vendor代替GO111MODULE=off @scaat 转移部分 archived 到 mosn.io/pkg 路径下 @nejisama 优化 EDF 负载均衡:在首次选择时的机器进行随机选择 @alpha-baby 提升 EDF 负载均衡函数的性能 @alpha-baby 调整 boltv2 心跳请求和心跳响应的处理 @nejisama 优化 HTTP2 在 Stream 模式下的重试处理和 Unary 请求优化 @XIEZHENGYAO 当通过环境变量设置 GOMAXPROCS 时,无视 CPU 数量的限制 @wangfakang 优化 subset 创建时的内存使用 @dzdx 优化 gRPC 框架,支持不同的 Listener 可以支持同名 Server 独立运行 @nejisama Bug 修复 修复重试时如果返回的机器地址为空会导致卡死的问题 @XIEZHENGYAO 修复消息连接池处理连接事件的 BUG @RayneHwang 修复没有初始化 Trace Driver 时调用 Enable Trace 导致的 panic 问题 @nejisama 修复 boltv2 协议在构造异常响应时数据错误的问题 @nejisama 修复 HTTP2 连接失败时异常处理的问题 @XIEZHENGYAO typo 错误修复 @jxd134 @yannsun 修复 RequestInfo 输出 ResponseFlag 的错误 @wangfakang 修复 bolt/boltv2 协议编码时,在空数据时没有重新计算长度位的问题 @hui-cha ","excerpt":"我们很高兴的宣布 MOSN v0.25.0 发布,以下是该版本的变更日志。\nv0.25.0 新功能 路由支持删除请求头指定字段的配置 @wangfakang WASM 支持 Reload @zu1k …","ref":"https://mosn.io/docs/products/report/releases/v0.25.0/","title":"MOSN v0.25.0 发布"},{"body":"我们很高兴的宣布 MOSN v0.24.0 发布,恭喜付建豪(@alpha-baby)成为 MOSN Committer,感谢他为 MOSN 社区所做的贡献。\n以下是该版本的变更日志。\nv0.24.0 新功能 支持使用 jaeger 收集 OpenTracing 信息 @Roger 路由配置新增变量配置模式,可通过修改变量的方式修改路由结果 @wangfakang 路由 virtualhost 匹配支持端口匹配模式 @jiebin 实现 envoy 中的 filter: header_to_metadata @antJack 支持 UDS 的热升级 @taoyuanyuan 新增 subset 负载均衡逻辑,在没有元数据匹配的场景下使用全量机器列表进行负载均衡 @nejisama MOSN 的 gRPC 框架支持优雅关闭 @alpha-baby 优化 优化 Cluster 配置更新时的健康检查更新模式 @alpha-baby api.Connection 新增 OnConnectionEvent 接口 @CodingSinger 权重轮询负载均衡兜底策略调整为普通轮询负载均衡 @alpha-baby 在 MOSN 变量模块中增加 interface 值类型 @antJack Subset 判断机器个数与是否存在时,同样遵循兜底策略 @antJack Bug 修复 dubbo stream filter 支持协议自动识别 @Thiswang 修复轮询负载均衡在并发情况下结果异常 @alpha-baby 修复 unix 地址解析异常 @taoyuanyuan 修复 HTTP1 短连接无法生效的异常 @taoyuanyuan 修复国密 TLS SM3 套件在连接断开后存在的内存泄漏 @ZengKe 当连接被对端重置或管道断裂时 HTTP2 支持重试 @taoyuanyuan 修复从连接池中获取到的 host 信息错误 @Sharember 修复在 route 模块中选择权重集群的数据竞争 @alpha-baby 如果 host 不健康时,在Edf负载均衡算法中不能正确返回 @alpha-baby 修复 XProtocol 路由配置超时无效的问题 @nejisama ","excerpt":"我们很高兴的宣布 MOSN v0.24.0 发布,恭喜付建豪(@alpha-baby)成为 MOSN Committer,感谢他为 MOSN 社区所做的贡献。\n以下是该版本的变更日志。\nv0.24.0 …","ref":"https://mosn.io/docs/products/report/releases/v0.24.0/","title":"MOSN v0.24.0 发布"},{"body":"我们很高兴的宣布 MOSN v0.23.0 发布\n以下是该版本的变更日志。\nv0.23.0 新功能 新增 networkfilter:grpc,支持通过 networkfilter 扩展方式在 MOSN 中实现可复用 MOSN 其他能力的 grpc server @nejisama @zhenjunMa StreamFilterChain 新增遍历调用的扩展接口 @wangfakang bolt 协议新增 HTTP 403 状态码的映射 @pxzero 新增主动关闭 upstream 连接的能力 @nejisama 优化 networkfilter 配置解析能力优化 @nejisama proxy 配置解析支持按照协议扩展,配置解析时机优化 @nejisama TLS 连接新增证书缓存,减少重复证书的内存占用 @nejisama 优化 Quick Start Sample @nobodyiam 优化默认路由处理时的 context 对象生成 @alpha-baby 优化 Subset LoadBalancer 的创建函数接口 @alpha-baby 新增使用 so plugin 扩展方式接入协议扩展的示例 @yichouchou 优化 makefile 中获取 GOPATH 环境变量的方式 @bincherry 支持 darwin + arrch64 架构的编译 @nejisama 优化日志打开方式 @taoyuanyuan Bug 修复 HTTP1 修复 URL 处理编码问题 @morefreeze HTTP1 修复 URL 处理大小写敏感错误问题 @GLYASAI TLS 修复 SM4 套件异常处理时存在的内存泄漏问题 @william-zk ","excerpt":"我们很高兴的宣布 MOSN v0.23.0 发布\n以下是该版本的变更日志。\nv0.23.0 新功能 新增 networkfilter:grpc,支持通过 networkfilter …","ref":"https://mosn.io/docs/products/report/releases/v0.23.0/","title":"MOSN v0.23.0 发布"},{"body":"我们很高兴的宣布 MOSN v0.22.0 发布\n以下是该版本的变更日志。\nv0.22.0 新功能 新增 Wasm 扩展框架 @antJack XProtocol 协议新增 x-bolt 子协议,支持基于 Wasm 的协议编解码能力 @zonghaishang 支持自动协议识别失败时根据 SO_ORIGINAL_DST 进行自动转发报文的能力 @antJack XProtocol 支持 Go Plugin 模式扩展 @fdingiit 新增网络扩展层 @wangfakang 支持 Istio xDS v3 API @champly 所属分支: istio-1.7.7 优化 去除 StreamFilter 配置解析中多余的路径清洗 @eliasyaoyc 支持为 StreamFilterChain 设置统一的回调接口 @antJack FeatureGate 支持不同启动阶段执行, 去除 FeatureGate 状态判断的全局锁 @nejisama Http2 模块新增对 trace 能力的支持 @OrezzerO 重构 新增 StageManager,将 MOSN 启动流程划分为四个可自定义的阶段 @nejisama 统一 XProtocol 模块的类型定义,移动至 mosn.io/api 包 @fdingiit XProtocol 接口新增 GetTimeout 方法,取代原有的变量获取方式 @nejisama Bug修复 修复 Proxy 中请求信息的并发冲突问题 @nejisama 修复 URL 处理时的安全漏洞 @antJack 修复配置持久化时 Router 配置的并发冲突问题 @nejisama ","excerpt":"我们很高兴的宣布 MOSN v0.22.0 发布\n以下是该版本的变更日志。\nv0.22.0 新功能 新增 Wasm 扩展框架 @antJack XProtocol 协议新增 x-bolt 子协议,支 …","ref":"https://mosn.io/docs/products/report/releases/v0.22.0/","title":"MOSN v0.22.0 发布"},{"body":"作为金融级服务网格中的流量代理组件,MOSN 在承载蚂蚁数十万服务容器之间流量的同时,也承载着诸多例如限流、鉴权、路由等中间件基础能力。这些能力以不同的扩展形式与 MOSN 运行于同一进程内。非隔离的运行方式在保障性能的同时,却也给 MOSN 带来了不可预知的安全风险。\n针对上述问题,我们采用 WebAssembly(Wasm) 技术,给 MOSN 实现了一个安全隔离的沙箱环境,让扩展程序能够运行在隔离沙箱之中,并对其资源、能力进行严格限制,使程序故障止步于沙箱,从而实现安全隔离的目标。本文将着重叙述 MOSN 中的 Wasm 扩展框架,并介绍我们在 Proxy-Wasm 这一代理扩展规范上的工作。\n总体设计 上图为 MOSN Wasm 扩展框架的整体示意图。如图所示,对于 MOSN 的任意扩展点(Codec、NetworkFilter、StreamFilter 等),用户均能够通过 Wasm 扩展框架,以隔离沙箱的形式运行自定义的扩展代码。而 MOSN 与 Wasm 扩展代码之间的交互,是通过 Proxy-Wasm 标准 ABI 来完成的。\n隔离沙箱 当我们在讨论 Wasm 时,都明白 Wasm 能够提供一个安全隔离的沙箱环境,但并不是每个人都了解 Wasm 实现隔离沙箱的技术原理。这时又要搬出计算机科学中的至理名言: “计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决”。Wasm 实际上也是通过引用一个“中间层”来实现的安全隔离。简单来说,Wasm 通过一个运行时(Runtime)来运行 Wasm 沙箱扩展,每个 Wasm 沙箱都有其独立的线性内存空间和一组导入/导出模块。\n一方面,每个 Wasm 沙箱都有其独立的线性内存空间,其内存模型如上图所示。Wasm 代码只能通过简单的 load/store 等指令访问线性内存空间的有限部分,并通过符号(下标)的方式来间接访问函数、全局变量等,杜绝了类似 C 语言中访问任意内存地址的骚操作。同时,用于间接调用函数的符号表对于 Wasm 代码而言是只读的,从而保证 Wasm 代码的执行是受控的。此外,Wasm 沙箱的整个线性内存空间由宿主机(Wasm Runtime)分配及管理,通过严格的内存管理保证沙箱的隔离性。\n另一方面,Wasm 也规定了代码中任何可能产生外部影响的操作只能通过导入/导出模块来实现。以 C 语言为例,我们可以直接通过系统调用来访问系统的环境变量、文件、网络等资源。而在 Wasm 的世界中,并不存在系统调用相关的指令,任何对外部资源的访问必须通过导入模块来间接实现。以文件读写为例,在 Wasm 中要想进行文件读写,需要宿主机提供实现文件读写功能的导入函数,Wasm 代码调用该导入函数,由宿主机间接进行文件读写,再将操作结果返回给 Wasm 扩展。在上述过程中,实际的文件读写操作由宿主机完成,宿主机对这一过程有绝对的控制权,包括但不限于只允许读写指定文件、限制读写内容、完全禁止读写等。\n扩展框架 MOSN 以 插件(Plugin) 的形式对 Wasm 扩展进行统一管理,插件是指一组 Wasm 沙箱实例及其相应配置的集合。用户通过配置来加载、更新以及卸载 Wasm 插件,并通过配置来描述沙箱实例的运行规格(使用的执行引擎、Wasm 文件路径、实例数量等)。下面展示了一个典型的 Wasm 插件配置:\n{ \u0026#34;plugin_name\u0026#34;: \u0026#34;global_plugin_id\u0026#34;, // 1. 插件名 \u0026#34;instance_num\u0026#34;: 4, // 2. 沙箱实例个数 \u0026#34;vm_config\u0026#34;: { \u0026#34;engine\u0026#34;: \u0026#34;wasmer\u0026#34;, // 3. 使用的虚拟机(Runtime) \u0026#34;path\u0026#34;: \u0026#34;/foo/bar.wasm\u0026#34;, // 4. wasm 文件路径 \u0026#34;url\u0026#34;: \u0026#34;http://xxx/bar.wasm\u0026#34; } } 当 MOSN 加载上述插件配置时,会按照以下流程生成插件对应的 Wasm 沙箱实例:\n在后续运行的过程中,用户通过 Wasm 扩展框架获取指定插件的沙箱实例, 然后通过沙箱实例暴露的 API 与扩展程序进行交互。本文的下一小节将对此交互过程进行详细描述。在 MOSN 中,Wasm 扩展框架与具体用途无关,在 MOSN 已有的任何一处扩展点,均可以直接使用 Wasm 框架来获取安全隔离的插件执行能力。\n如下图所示,Wasm 扩展框架主要分为 Manager、VM 和 ABI 三个子模块。其中\n Manager 模块负责对 Wasm 插件的配置进行统一管理,提供插件的增删查改功能,负责将用户提供的配置渲染成最终的 Wasm 沙箱实例 VM 模块提供对 Wasm Runtime(虚拟机) 的统一封装,负责 .wasm 文件的编译、执行,以及 Wasm 沙箱实例的资源管理 ABI 模块则提供对外的使用接口,可以看作是 MOSN 与 Wasm 扩展代码之间交互的胶水层 本文不再对框架内的具体子模块进行介绍,感兴趣的读者可以阅读开源 PR 的文档了解细节。\n由于当前市面上几乎不存在使用 Go 语言直接编写的 Wasm Runtime,因此 MOSN 只能通过 CGO 调用的方式来间接地调用由 C++/Rust 编写的 Wasm 执行引擎。我们从 SDK 完善程度、性能、项目活跃度等角度综合考虑,经过一系列横向对比之后,选择了 Wasmer 作为 MOSN 默认的执行引擎。\nProxy-Wasm ABI 规范 本小节将介绍 MOSN 具体是如何跟 Wasm 扩展程序进行交互的。先说结论: MOSN 跟 Wasm 扩展代码之间的交互采用的是社区规范: Proxy-Wasm\nProxy-Wasm 是开源社区针对「网络代理场景」设计的一套 ABI 规范,属于当前的事实规范。当前支持该规范的网络代理软件包括 Envoy、MOSN 和 ATS(Apache Traffic Server),支持该规范的 Wasm 扩展 SDK 包括 C++、Rust 和 Go。采用该规范的好处在于能让 MOSN 复用社区既有的 Wasm 扩展 (包括 Go 实现以及 C++/Rust 实现),也能让本为 MOSN 开发的 Wasm 扩展运行在 Envoy 等网络代理产品上。\nProxy-Wasm 规范定义了宿主机与 Wasm 扩展程序之间的交互细节,包括 API 列表、函数调用规范以及数据传输规范这几个方面。其中,API 列表包含了 L4/L7、property、metrics、日志等方面的扩展点,涵盖了网络代理场景下所需的大部分交互点,且可以划分为宿主侧扩展和 Wasm 侧扩展点。这里简单展示规范中的部分内容,完整内容请参考 spec。\n// Functions implemented in the Wasm module // 由 Wasm 侧实现的扩展点 // L7: proxy_on_http_request_headers params: i32 (uint32_t) context_id i32 (size_t) num_headers i32 (bool) end_of_stream returns: i32 (proxy_action_t) next_action // L4: proxy_on_downstream_data params: i32 (uint32_t) context_id i32 (size_t) data_size i32 (bool) end_of_stream returns: i32 (proxy_action_t) next_action // Functions implemented in the host environment // 由宿主侧实现的扩展点 // 日志 proxy_log params: i32 (proxy_log_level_t) log_level i32 (const char*) message_data i32 (size_t) message_size returns: i32 (proxy_result_t) call_result // 数据 proxy_get_map params: i32 (proxy_map_type_t) map_type i32 (const char**) return_map_data i32 (size_t*) return_map_size returns: i32 (proxy_result_t) call_result // MapData Format // Map 数据传输规范 mapsize | key1size | value1size | key2size | value2size | ... | key1 | \\0 | value1 | \\0 | ... 规范的实现需要宿主侧和 Wasm 侧两边配合才能正常工作。对于 Wasm 侧,社区已经有 C++、Rust 和 Go 三种语言实现的 SDK,用户可以直接使用这些 SDK 来编写与宿主无关的 Wasm 扩展程序。而对于宿主侧,社区只提供了 C++ 和 Rust 的宿主侧实现。为此,我们在项目中使用 Go 语言对 Proxy-Wasm 规范的宿主侧进行了实现,并将其贡献给开源社区,使之成为社区推荐的 Go-Host 实现。需要强调的是,宿主侧实现并不依赖具体的网络代理程序,理论上任何直接通过 Host 程序与 Wasm 扩展进行交互。\n我们以 HTTP 场景为例,介绍在 MOSN 中是如何通过 Proxy-Wasm 规范来与 Wasm 扩展程序进行交互,处理 HTTP 请求的。\n MOSN 收到 HTTP 请求时,将请求解码成 Header、Body、Trailer 三元组结构,按照配置依次执行 StreamFilters 执行到 Wasm StreamFilter 时,MOSN 将请求三元组传递给 Proxy-Wasm 宿主侧实现 proxy-wasm-go-host 宿主侧 go-host 将 MOSN 请求三元组编码成规范指定的格式,并调用规范中的 proxy_on_request_headers 等接口,将请求信息传递至 Wasm 侧 Wasm 侧 SDK 将请求数据从规范格式转换为便于用户使用的格式,随后调用用户编写的扩展代码 用户代码返回,Wasm 侧将返回结果按规范格式传递回 MOSN 侧 MOSN 继续执行后续 StreamFilter 工程实践 Quick Start 本小节主要演示如何在 MOSN 中进行配置并运行 Wasm 扩展插件流程。演示所需的源文件参考 example。\n在演示中,我们通过配置让 Wasm 扩展插件来处理 MOSN 接收的 HTTP 请求,MOSN 的监听端口为 2045。在 Wasm 处理请求的源码中,我们通过 Proxy-Wasm 规范中的 proxy_dispatch_http_call 接口向外部 HTTP 服务器发起请求,Wasm 源码内指定外部 HTTP 服务器的监听端口为 2046。演示场景的流程如下图所示:\n该演示流程主要分为以下步骤:\n 将扩展程序编译成 .wasm 文件 启动 MOSN 并加载 Wasm 插件 启动外部 HTTP 服务器 请求验证 1. 编译 Wasm 扩展程序 我们在示例工程中提供了 C 和 Go 两种语言实现的 Wasm 扩展源码,对 Proxy-Wasm 规范的采用使得我们能够利用多种语言 (C++/Rust/Go) 来编写 Wasm 扩展代码。出于编译的便利性,这里使用 Go 源码实现进行演示。 进入 example/wasm/httpCall 目录,执行命令:\nmake 上述操作会将目录下的 filter-go.go 源码文件编译成 filter-go.wasm 文件\n2. 启动 MOSN 示例工程提供了一份加载 filter-go.wasm 扩展文件的配置,通过以下命令即可启动:\n./mosn start -c config.json 上述命令中使用的 MOSN 可执行程序可以通过以下命令由源码构建:\n# step 1: # 创建源码路径 mkdir -p $GOPATH/src/mosn.io cd $GOPATH/src/mosn.io # step 2: # 下载 MOSN 代码仓库 git clone https://github.com/mosn/mosn.git # step 3: # 编译,以下命令最终将产生 mosn 可执行文件 sudo make build-local tags=wasmer mv build/bundles/v0.21.0/binary/mosn mosn 3. 启动外部 HTTP 服务器 该示例工程中,Wasm 扩展源码会通过 MOSN 向外部 HTTP 服务器发起请求,请求的 URL 为\n http://127.0.0.1:2046/\n 为此,示例工程也提供了一段 HTTP 服务器代码,当其收到 HTTP 请求时,均会返回响应头: from: external http server,返回响应体: response body from external http server 执行以下命令将启动上述 HTTP 服务器:\ngo run server.go 4. 请求验证 上述操作准备就绪后,便可通过 Curl 来进行请求验证了\ncurl -v http://127.0.0.1:2045/ 执行上述命令后,MOSN 终端将能够观察到以下日志:\n[INFO] response header from http://127.0.0.1:2046/: From: external http server [INFO] response header from http://127.0.0.1:2046/: Date: Wed, 17 Mar 2021 12:12:38 GMT [INFO] response header from http://127.0.0.1:2046/: Content-Length: 39 [INFO] response header from http://127.0.0.1:2046/: Content-Type: text/plain; charset=utf-8 [INFO] response body from http://127.0.0.1:2046/: response body from external http server 性能测试 本小节对 Wasm 框架的性能进行测试\n测试环境: OS: macOS Catalina 10.15.4 CPU: Intel(R) Core(TM) i7-7660U CPU @ 2.50GHz 4Core MEM: 16 GB 2133 MHz LPDDR3 Go Version: go1.14.13 darwin/amd64 测试场景: 拓扑: client \u0026ndash;http1.1\u0026ndash;\u0026gt; MOSN 操作: MOSN 收到 H1 请求后,往请求头中添加一个 Header 随后返回 200\n测试数据: 「native」表示添加 Header 的操作使用 MOSN 原生的 Stream Filter 完成;\n「wasm」表示添加 Header 的操作使用 Wasm 扩展完成\n 固定 QPS 模式,将 QPS 固定为 2000 进行压测 压测命令: sofaload \u0026ndash;h1 -c 100 -t 4 \u0026ndash;qps=2000 -D 30 http://127.0.0.1:2045/\n qps avg P75 P90 P99 native 2000 698us 856us 1.09ms 1.87ms wasm 2000 763us 940us 1.21ms 2.35ms -9.3% 压测模式,不限制压测 QPS,将流量打到最大 压测命令: sofaload \u0026ndash;h1 -c 100 -t 4 -n 1000000 http://127.0.0.1:2045/\n qps avg P75 P90 P99 native 36013 2.78ms 3.80ms 5.44ms 10.39ms wasm 26542 3.77ms 5.14ms 7.42ms 14.00ms -26% 异常调试 对于实际的工程项目而言,光能运行是不够的,必须具备一定的问题排查和定位能力,才能在遇到程序故障时,解析异常源码的调用堆栈,快速定位第一现场,从而提高开发及调试的效率。\n由于 Wasm 本身的定位是与编程语言无关的字节码规范,不同语言的源代码 (C++/Go/JavaScript 等) 均能够编译为统一的 Wasm 字节码,因此如何屏蔽具体编程语言的细节模型,制定语言无关的调试信息规范,是社区需要解决的难题之一。 针对这一问题,在当前的工程实践中,JavaScript 语言采用的是 Source Map 格式,而 C++、Rust 和 Go 语言采用的是 Dwarf 格式的调试信息。对具体调试信息格式的介绍并不在本文的范围之内,读者可自行参考外部文章。这里需要强调的是,对于 Wasm 而言,还需要对调试信息的格式进行一定的扩展,才能满足实际的应用需要。与其他编程语言不同的是,.wasm 文件是能够被转换成 .wat 格式,并手动编辑内容的,编译好的 .wasm 文件仍然有修改段内容的可能。为了适应这种场景,Wasm 调试规范对 Dwarf 格式中的位置信息编码进行了调整,指令的偏移值被设置成基于 Code 段的偏移:\n With WebAssembly, the .debug_line section maps Code section-relative instruction offsets to source locations.\n 为此,我们在解析指令偏移时,需要偏移数值进行调整,减去 Code 段的偏移量,才能得到 Wasm 指令的实际偏移值,进而利用 .debug_line 段定位到准确的源码行。下图展示了利用 MOSN 输出的错误日志定位 Wasm 故障源码行的示例。\n总结 对于蚂蚁而言,安全可信永远是我们追求的目标,而面对越来越多的扩展场景,MOSN 需要一个安全可靠的隔离环境,以避免扩展代码给 MOSN 运行造成的安全风险。为此,我们采用 WebAssembly 技术,为 MOSN 实现了一个基于 Wasm 隔离沙箱的插件扩展框架。MOSN 采用网络代理社区中的 Proxy-Wasm 规范,实现了语言无关、宿主无关的网络代理扩展能力。同时,我们也向开源社区贡献了 Proxy-Wasm-Go-Host 实现,积极融入开源社区。\n需要注意的是,当前 WebAssembly 技术仍处于发展阶段,Go 语言自身对 WebAssenbly 生态的支持仍有巨大的提升空间。我们在实践的过程中,也总是面临 Go 语言在 Wasm 生态中不够给力的情况。由于 Go 官方编译器还不支持将 Go 源码程序编译成 WASI 系统接口 (GOOS=wasi) 的 .wasm 文件,我们不得不借助 TinyGo 来完成 Go 扩展程序的编译,而这也导致我们需要面对 TinyGo 在语言特性支持程度、性能、稳定性等方面不足的痛点。与之相比,C++/Rust 对 Wasm 生态的支持程度就要完善得多。\n总而言之,WebAssembly 技术的出现仍然为我们提供了一种启发和希望,促使我们进一步思考如何在云原生时代更好地践行安全可信这一信条。\n","excerpt":"作为金融级服务网格中的流量代理组件,MOSN 在承载蚂蚁数十万服务容器之间流量的同时,也承载着诸多例如限流、鉴权、路由等中间件基础能力。这些能力以不同的扩展形式与 MOSN 运行于同一进程内。非隔离的 …","ref":"https://mosn.io/blog/posts/mosn-wasm-framework/","title":"WebAssembly 在 MOSN 中的实践 - 基础框架篇"},{"body":"我们很高兴的宣布 MOSN v0.21.0 发布,恭喜郑泽超(@CodingSinger)成为 MOSN Committer,感谢他为 MOSN 社区所做的贡献。\n以下是该版本的变更日志。\nv0.21.0 优化 升级sentinel版本到v1.0.2 @ansiz 读超时收缩tls的read buffer,降低tls内存消耗 @cch123 增加注释,简化xprotocol协议连接池实现 @cch123 更新mosn registry版本 @cadeeper @cch123 重构 优化路由Header匹配逻辑,支持通用的RPC路由匹配 @nejisama 删除原有部分常量,新增用于描述变量机制的常量 @nejisama 限流模块重构,支持自定义回调扩展,可实现自定义的过滤条件,上下文信息修改等能力 @ansiz Bug修复 修复请求异常时metrics统计错误 @cch123 修复http场景转发前没有对url进行转义的问题 @antJack 修复HTTP协议中变量注入错误的问题, 修复HTTP2协议中不支持路由Rewrite的bug @nejisama 新功能 支持Domain-Specific Language路由实现 @CodingSinger StreamFilter支持go编写的动态链接库加载的方式 @CodingSinger 路由配置中VirtualHost支持per_filter_config配置 @machine3 支持dubbo thrift协议 @cadeeper ","excerpt":"我们很高兴的宣布 MOSN v0.21.0 发布,恭喜郑泽超(@CodingSinger)成为 MOSN Committer,感谢他为 MOSN 社区所做的贡献。\n以下是该版本的变更日志。 …","ref":"https://mosn.io/docs/products/report/releases/v0.21.0/","title":"MOSN v0.21.0 发布"},{"body":"我们很高兴的宣布 MOSN v0.20.0 发布,恭喜黄润豪(@GLYASAI)成为 MOSN Committer,感谢他为 MOSN 社区所做的贡献。\n以下是该版本的变更日志。\nv0.20.0 优化 优化 TCP 地址解析失败默认解析 UDS 地址的问题,地址解析前添加前缀判断 @wangfakang 优化连接池获取的尝试间隔 @nejisama 支持通过全局配置关闭循环写模式 @nejisama 优化协议自动识别的配置示例和测试用例 @taoyuanyuan 用更高效的变量机制替换请求头 @CodingSinger 将 WriteBufferChan 的定时器池化以降低负载 @cch123 TraceLog 中新增 MOSN 处理失败的信息 @nejisama HTTP协议处理中,新增读完成channel @alpha-baby 日志轮转功能加强 @nejisama 重构 使用的 Go 版本升级到 1.14.13 @nejisama 将路由链扩展方式修改为路由Handler扩展方式,支持配置不同的路由Handler @nejisama MOSN 扩展配置修改,支持按照配置顺序进行解析 @nejisama Bug 修复 修复 doubbo 版本升级至 2.7.3 之后 Provider 不可用的问题 @cadeeper 修复 netpoll 模式下,错误将UDS连接处理成TCP连接的问题 @wangfakang 修复 HTTP Header 被设置为空字符串时无法正确 Get 的问题 @ianwoolf 新功能 支持新旧 MOSN 之间通过 UDS 转移配置,解决 MOSN 使用 XDS 获取配置无法平滑升级的问题 @alpha-baby 协议自动识别支持 XProtocol @cadeeper 支持配置 XProtocol 的 keepalive 参数 @cch123 支持更详细的用时追踪 @nejisama 支持度量指标懒加载的方式,以解决服务数目过多 metrics 空间占用过大的问题 @champly 添加设置 XProtocol 连接池大小默认值的函数 @cch123 支持 netpoll 模式 @cch123 支持广播功能 @dengqian 支持从 LDS 响应中获取 tls 配置 @wZH-CN SDS 新增 ACK response @wZH-CN ","excerpt":"我们很高兴的宣布 MOSN v0.20.0 发布,恭喜黄润豪(@GLYASAI)成为 MOSN Committer,感谢他为 MOSN 社区所做的贡献。\n以下是该版本的变更日志。\nv0.20.0 …","ref":"https://mosn.io/docs/products/report/releases/v0.20.0/","title":"MOSN v0.20.0 发布"},{"body":"我们很高兴的宣布 MOSN v0.19.0 发布。\n以下是该版本的变更日志。\nv0.19.0 优化 使用最新的 TLS 内存优化方案 @cch123 proxy log 优化,减少内存逃逸 @taoyuanyuan 增加最大连接数限制 @champly AccessLog 获取变量失败时,使用”-”代替 @champly MaxProcs 支持配置基于 CPU 使用限制自动识别 @champly 支持指定 Istio cluster 的网络 @champly 重构 重构了 StreamFilter 框架,减少 streamfilter 框架与 proxy 的耦合,支持其他 network filter 可复用 stream filter 框架 @antJack Bug 修复 修复 HTTP Trace 获取 URL 错误 @wzshiming 修复 xds 配置解析时没有解析连接超时的错误 @dengqian 修复变量获取 Hostname 的错误 @dengqian 修复 tcp proxy 没有正确关闭连接的错误 @dengqian 修复 mixer filter 缺少默认配置,导致空指针问题 @glyasai 修复 HTTP2 直接响应没有正确地设置 Content-length 的问题 @wangfakang 修复 getAPISourceEndpoint 方法空指针问题 @dylandee 修复 Write 堆积时,过多的 Timer 申请导致内存上涨的问题 @champly 修复 Dubbo Filter 收到非法响应时,stats 统计缺失的问题 @champly ","excerpt":"我们很高兴的宣布 MOSN v0.19.0 发布。\n以下是该版本的变更日志。\nv0.19.0 优化 使用最新的 TLS 内存优化方案 @cch123 proxy log 优化, …","ref":"https://mosn.io/docs/products/report/releases/v0.19.0/","title":"MOSN v0.19.0 发布"},{"body":"我们很高兴的宣布 MOSN v0.18.0 发布。\n以下是该版本的变更日志。\nv0.18.0 新功能 新增 MOSN 配置文件扩展机制 @nejisama 新增 MOSN 配置工具,提升用户配置体验 mosn/configure @cch123 优化 HTTP 协议 stream 处理过程中,避免多次拷贝 HTTP body @wangfakang 升级了 github.com/TarsCloud/TarsGo 包到 v1.1.4 版本 @champly 补充了连接池的单元测试 @cch123 使用内存池减少了 TLS 连接的内存占用 @cch123 减少 xprotocol stream 处理过程的临界区大小,提升性能 @cch123 删除 network.NewClientConnection 方法冗余参数,删除 streamConn 结构体 Dispatch 方法 ALPN 检查 @nejisama StreamReceiverFilterHandler 增加 TerminateStream API,可在处理流的时候传入 HTTP code 异步关闭流 @nejisama client 端 TLS handshake 失败时增加降级逻辑 @nejisama 修改 TLS hashvalue 计算方式 @nejisama 修正 disable_log admin api typo @nejisama Bug 修复 修复执行 go mod tidy 失败 @champly 修复 MOSN 接收 XDS 消息大于 4M 时的 ResourceExhausted: grpc: received message larger than max 错误 @champly 修复容错单元测试用例 @wangfakang 修复 MOSNConfig.servers[].listeners[].bind_port 设置为 false 时热重启出错 @alpha-baby 本地写 buffer 增加超时时间,避免本地写失败导致 goroutine 过多 OOM @cch123 修复 TLS 超时导致死循环 @nejisama 修复 dubbo.Frame struct 使用 SetData 方法之后数据没有被修改的问题 @lxd5866 ","excerpt":"我们很高兴的宣布 MOSN v0.18.0 发布。\n以下是该版本的变更日志。\nv0.18.0 新功能 新增 MOSN 配置文件扩展机制 @nejisama 新增 MOSN 配置工具, …","ref":"https://mosn.io/docs/products/report/releases/v0.18.0/","title":"MOSN v0.18.0 发布"},{"body":"我们很高兴的宣布 MOSN v0.17.0 发布。\n以下是该版本的变更日志。\nv0.17.0 新功能 新增最大 Header 大小限制的配置选项 @wangfakang 支持协议实现时选择是否需要 workerpool 模式,在 workerpool 模式下,支持可配置的连接并发度 @cch123 Listener 配置新增对 UDS 的支持 @CodingSinger 添加在 Dubbo 协议下通过 xDS HTTP 配置进行转换的过滤器 @champly 优化 优化 http 场景下的 buffer 申请 @wangfakang 优化 SDS Client 使用读写锁获取 @chainhelen 更新 hessian2 v1.7.0 库 @cch123 修改 NewStream 接口,从回调模式调整为同步调用的模式 @cch123 重构 XProtocol 连接池,支持 pingpong 模式、多路复用模式与连接绑定模式 @cch123 优化 XProtocol 多路复用模式,支持单机 Host 连接数可配置,默认是 1 @cch123 优化正则路由配置项,避免 dump 过多无用配置 @wangfakang Bug 修复 修复 README 蚂蚁 logo 地址失效的问题 @wangfakang 修复当请求 header 太长覆盖请求内容的问题 @cch123 修复 Dubbo 协议解析 attachment 异常的问题 @champly ","excerpt":"我们很高兴的宣布 MOSN v0.17.0 发布。\n以下是该版本的变更日志。\nv0.17.0 新功能 新增最大 Header 大小限制的配置选项 @wangfakang …","ref":"https://mosn.io/docs/products/report/releases/v0.17.0/","title":"MOSN v0.17.0 发布"},{"body":"MOSN 版本发布步骤 一、冻结代码 在准备一个版本发布期间,停止代码往 master 分支的合并 二、更新依赖 发布 mosn/api,版本与即将发布的 mosn 相同 更新 mosn/pkg 的 mosn/api 版本至最新,然后发布 mosn/pkg 更新 mosn 的 api、pkg 至最新 三、整理 Release notes 基于 Github 的 PullRequest 记录,整理本次发布的内容与上一个版本之间的差异,需要注意仅统计目标分支是 master 且正常合并的 PullRequest 首先整理完原始信息以后,进行提炼和总结 通常情况下,一个 PullRequest 对应一个改动记录 存在部分特殊情况是一个 PullRequest 包含多个改动的情况,可以请 PullRequest 提供者提供详细信息 也可能存在多个 PullRequest 是针对同一个改动的情况(如新功能,分开提 PullRequest,或者在同一个版本迭代中不断优化) 提炼后的完整 Release notes 记录格式可以参考 CHANGELOG 提炼后的 Release notes 需要有英文版本的记录 四、测试报告 所有的改动点都需要有对应的测试记录,测试方式可以有多种,包括但不限于 完整的单元测试覆盖,确保基本的功能场景正确,默认代码合并的时候会执行 性能测试 Benchmark,针对可能对性能有影响的改动,需要执行性能测试。 手动模拟测试,主要针对一些单元测试无法很好覆盖的场景(如涉及网络 IO、Proxy 转发等多模块交互),通过手动搭建测试环境(配置、模拟 Server 与 Client)进行场景复现与验证 手动模拟测试完成以后记录详细的测试步骤,包括:模拟的场景、使用的配置、使用的 Server、使用的 Client 等;后续可以考虑将手动场景实现为 integrate 测试 测试完成以后,产出测试报告,说明对应的功能点使用哪种测试方式测试与测试的结果 五、版本发布 版本发布 PullRequest\n 修改VERSION 修改CHANGELOG.md、CHNAGELOH_ZH.md 上传测试报告reports/${VERSION}.md 官网文档的同步修改\n github.com/mosn/mosn.io/content/zh/blog/releases 下新增对应的 release notes. 英文版 release notes 也需要做一样的更新 正式 release\n 合并完版本发布 PullRequest后,基于 master 分支完成 release release 时的描述内容填写英文版 release notes 的内容 通过make build命令编译出对应的二进制并且上传 完成 release ","excerpt":"MOSN 版本发布步骤 一、冻结代码 在准备一个版本发布期间,停止代码往 master 分支的合并 二、更新依赖 发布 mosn/api,版本与即将发布的 mosn 相同 更新 mosn/pkg …","ref":"https://mosn.io/docs/products/report/release-products/","title":"版本发布介绍"},{"body":"我们很高兴的宣布 MOSN v0.16.0 发布。\n以下是该版本的变更日志。\nv0.16.0 优化 Logger Roller 支持自定义 Roller 的实现 @wenxuwan StreamFilter 新增接口 SendHijackReplyWithBody @wenxuwan 配置项新增关闭热升级选项,关闭热升级以后一个机器上可以同时存在多个不同的 MOSN 进程 @cch123 优化 MOSN 集成测试框架,补充单元测试 @nejisama @wangfakang @taoyuanyuan xDS 配置解析支持 DirectResponse 的路由配置 @wangfakang ClusterManager 配置新增 TLSContext @nejisama Bug 修复 修复在热升级时 UDP 连接超时会导致死循环的 BUG @dengqian 修复在 SendFilter 中执行 DirectResponse 会触发死循环的 BUG @taoyuanyuan 修复 HTTP2 的 Stream 计数并发统计冲突的 BUG @wenxuwan 修复 UDP 连接因读超时导致的数据丢失问题 @dengqian 修复触发重试时因为协议标识丢失导致无法正确记录响应 StatusCode 的 BUG @dengqian 修复 BoltV2 协议解析错误的 BUG @nejisama 修复 Listener Panic 后无法自动 Restart 的 BUG @alpha-baby 修复变量机制中 NoCache 标签无效的 BUG @wangfakang 修复 SDS 重连时可能存在并发冲突的 BUG @nejisama ","excerpt":"我们很高兴的宣布 MOSN v0.16.0 发布。\n以下是该版本的变更日志。\nv0.16.0 优化 Logger Roller 支持自定义 Roller 的实现 @wenxuwan …","ref":"https://mosn.io/docs/products/report/releases/v0.16.0/","title":"MOSN v0.16.0 发布"},{"body":"我们很高兴的宣布 MOSN v0.15.0 发布,恭喜邓茜(@dengqian)成为 MOSN Committer,感谢她为 MOSN 社区所做的贡献。\n以下是该版本的变更日志。\n新功能 路由 Path Rewrite 支持按照正则表达式的方式配置 Rewrite 的内容 @liangyuanpeng 配置新增字段: 扩展配置字段,可通过扩展配置字段自定义启动配置;Dubbo 服务发现配置通过扩展的配置字段实现 @cch123 支持 DSL 新特性,可以方便的对请求的处理行为进行控制 @wangfakang StreamFilter 新增流量镜像功能的扩展实现 @champly Listener 配置新增对 UDP 的支持 @dengqian 配置格式支持 Yaml 格式解析 @GLYASAI 路由支持 HTTP 重定向配置 @knight42 优化 支持 istio 的 stats filter,可以根据匹配条件进行 metrics 的个性化记录 @wzshiming Metrics 配置支持配置 Histogram 的输出百分比 @champly StreamFilter 新增状态用于直接中止请求,并且不响应客户端 @taoyuanyuan XProtocol Hijack 响应支持携带 Body @champly Skywalking 升级到 0.5.0 版本 arugal Upstream 连接 TLS 状态判断修改,支持通过 TLS 配置的 Hash 判断是否需要重新建立连接 @nejisama 优化 DNS cache 逻辑,防止在 DNS 失效时可能引起的 DNS flood 问题 @wangfakang Bug 修复 修复开启 TLS 加密场景下,XProtocol 协议在有多个协议的场景下判断协议错误的 BUG @nejisama 修复 AccessLog 中前缀匹配类型的变量不生效的 BUG @dengqian 修复 Listener 配置解析处理不正确的 BUG @nejisama 修复 Router/Cluster 在文件持久化配置类型中,Name 字段包含路径分隔符时会保存失败的 BUG @nejisama ","excerpt":"我们很高兴的宣布 MOSN v0.15.0 发布,恭喜邓茜(@dengqian)成为 MOSN Committer,感谢她为 MOSN 社区所做的贡献。\n以下是该版本的变更日志。 …","ref":"https://mosn.io/docs/products/report/releases/v0.15.0/","title":"MOSN v0.15.0 发布"},{"body":"我们很高兴的宣布 MOSN v0.14.0 发布,恭喜姚昌宇(@trainyao)成为 MOSN Committer,感谢他为 MOSN 社区所做的贡献。\n以下是该版本的变更日志。\n新功能 支持 Istio 1.5.X @wangfakang @trainyao @champly go-control-plane 升级到 0.9.4 版本 xDS 支持 ACK,新增 xDS 的 Metrics 支持 Istio sourceLabels 过滤功能 支持 pilot-agent 的探测接口 支持更多的启动参数,适配 Istio agent 启动场景 gzip、strict-dns、original-dst 支持 xDS 更新 移除 Xproxy 逻辑 Maglev 负载均衡算法支持 @trainyao 新增连接池实现,用于支持消息类请求 @cch123 新增 TLS 连接切换的 Metrics @nejisama 新增 HTTP StatusCode 的 Metrics @dengqian 新增 Metrics Admin API 输出 @dengqian proxy 新增查询当前请求数的接口 @zonghaishang 支持 HostRewrite Header @liangyuanpeng 优化 升级 tars 依赖,修复在高版本 Golang 下的编译问题 @wangfakang xDS 配置解析升级适配 Istio 1.5.x @wangfakang 优化 proxy 的日志输出 @wenxuwan DNS Cache 默认时间修改为 15s @wangfakang HTTP 参数路由匹配优化 @wangfakang 升级 fasthttp 库 @wangfakang 优化 Dubbo 请求转发编码 @zonghaishang 支持 HTTP 的请求最大 body 可配置 @wangfakang Bug 修复 修复 Dubbo Decode 无法解析 attachment 的 bug @champly 修复 HTTP2 连接建立之前就可能创建 stream 的 bug @dunjut 修复处理 HTTP2 处理 Trailer 空指针异常 @taoyuanyuan 修复 HTTP 请求头默认不标准化处理的 bug @nejisama 修复 HTTP 请求处理时连接断开导致的 panic 异常 @wangfakang 修复 dubbo registry 的读写锁拷贝问题 @champly ","excerpt":"我们很高兴的宣布 MOSN v0.14.0 发布,恭喜姚昌宇(@trainyao)成为 MOSN Committer,感谢他为 MOSN 社区所做的贡献。\n以下是该版本的变更日志。 …","ref":"https://mosn.io/docs/products/report/releases/v0.14.0/","title":"MOSN v0.14.0 发布"},{"body":"Service Mesh 简介 Service Mesh 本身的理念并不复杂,就是将现代微服务应用的功能性与非功能性需求进行分离,并将非功能性需求下沉到应用的外部模块,从而使应用模块可以尽量聚焦于业务,不用关心诸如:服务发现、限流、熔断、tracing 这类非业务需求。下沉之后,相关的 Service Mesh 模块可以交由基础架构团队进行维护,使基础设施和业务能够完成解耦。\nService Mesh 设计一般划分为两个模块,控制面和数据面。可以通过下图来理解相应的职责:\n对于应用来说,所有流量都会经过 Service Mesh 中的数据面进行转发。而能顺利转发的前提:数据面需要知道转发的目标地址,目标地址本身是由一些业务逻辑来决定的(例如服务发现),所以自然而然地,我们可以推断控制面需要负责管理数据面能正常运行所需要的一些配置:\n 需要知道某次请求转发去哪里:服务发现配置 外部流量进入需要判断是否已经达到服务流量上限:限流配置 依赖服务返回错误时,需要能够执行相应的熔断逻辑:熔断配置 开源界目前比较有名的主要是 istio,envoy 和 linkerd 这几个项目,今天我们来介绍一下蚂蚁推出的 Service Mesh 数据面项目:MOSN。\nMOSN 简介 MOSN 是蚂蚁集团出品的用 Go 语言实现的 Service Mesh 数据面,在蚂蚁内部已大规模落地,在开源过程中我们了解到外部用户有较多的 dubbo 用户,这些 dubbo 用户也希望能够享受 Service Mesh 社区的发展红利。同时可以针对自己公司的特殊业务场景,对 Service Mesh 的数据面进行一定的扩展。\n谈到扩展,MOSN 使用 Go 编写的优势就体现出来了。相比 C++,Go 语言通过自带的内存分配器与 GC 实现了一定程度的内存安全,解放了程序员的心智。相比 C++ 编写的 envoy,无论是编程和问题定位都要轻松不少。\nMOSN 同时提供了强大的 XProtocol 协议扩展框架,用户可以根据自己的需求编写自定义协议解析。如果你使用的是 SOFA/Dubbo/HTTP/HTTP2,那么 MOSN 已经为你准备好了现成的实现。开箱即用。\n为了满足社区的需求,从今年 4 月开始,MOSN 社区与 dubbo-go 社区进行了深入的交流与合作。可能还有些同学对 dubbo-go 不太了解,简单介绍一下。\ndubbo-go 简介 Dubbo 是阿里巴巴出品一个非常优秀的 Java RPC 框架,相比其它框架,有较为全面的服务治理功能。\ndubbo-go 是 dubbo 的 Go 语言版本,该项目已进入 apache 一年有余,开发社区很活跃,版本发布节奏较快。功能上也基本和 dubbo 的 Java 版都对齐了。\n对于喜欢 dubbo 的 gopher 来说,dubbo-go 是个不错的选择。使用它来构建整个公司的服务框架,省时省心。\nMOSN + dubbo-go 双剑合璧 Service Mesh 能够给微服务的整体架构带来很多好处,然而业界提供的全家桶方案不一定能很好地在企业内落地,有下面一些原因:\n 要求数据面和控制面一起上线,早期 istio 因为设计问题,有众多模块。会大幅增加企业的运维负担。 企业上 mesh 一定是渐进部署,不可能一次性让所有服务全部上。这样就会有在 mesh 中的应用和非 mesh 中的应用需要能够互通的需求。 蚂蚁的 Service Mesh 落地过程相对较为成功,值得参考。在内部落地时,首先只落地数据面,这样运维负担较轻,同时出问题时也容易排查。\n但只落地数据面的话,有些控制面的功能我们就没有了,比如服务发现、路由配置订阅等等。因此在 MOSN 中,又额外对这些功能进行了支持(相当于目前的数据面单模块同时承载了数据面和控制面的部分功能)。当数据面稳定落地之后,再进行控制面的落地相对来说负担就小很多了。\n上面的是蚂蚁内部的情况,在社区内,MOSN 借助 dubbo-go 的能力,已经实现了服务发现功能,用户应用启动、退出时需要和 MOSN 进行简单的交互,以完成服务的发布和订阅功能,下面是发布流程:\n在应用启动时,访问本地的 MOSN http 接口,告知需要将本地的服务发布出去,MOSN 收到请求后对外发布 MOSN 的 ip 和端口到注册中心。这样监听本服务的 consumer 便可以从注册中心收到服务变更的通知,并将本实例加入到相应的 provider 列表。当应用退出时,需要主动进行 unpub。\n这种情况下的改造成本:\n 对于业务方来说,只需要升级 sdk。 对于 sdk 维护方,在执行 sub/pub/unsub/unpub 时,需要增加一个开关判断,开关打开时,说明本实例已上 Service Mesh。请求相应的本地 MOSN 接口。 对于 mesh 提供方,只要做好配置和部署就可以了。 接入 MOSN 后,mesh 化和非 mesh 化的应用可以互通:\n当然,开发过程也并不是一帆风顺的。在我们刚开始使用 dubbo-go 时,便遇到了 dubbo-go 的依赖与 MOSN 的依赖有冲突的问题:\nGo 语言的 go mod 语义会“自作聪明”地认为 0.y.z 的外部依赖都是彼此兼容的,然而我们从 semver 规范可以学习到:\n Major version zero (0.y.z) is for initial development. Anything MAY change at any time. The public API SHOULD NOT be considered stable.\n 0.y.z 只是用来做初始的开发,本身 API 的变化就是很频繁的,依赖管理工具不应该自动去升级这些依赖。但是 Go 社区的人有不少人坚持 go mod 的行为正确,并且祭出 MVS 算法,表示碰到问题的只是不懂 Go 的哲学。\n当前 Go 的依赖管理使得两个大项目发生依赖冲突时,比较难处理,我们只能根据实际情况去 go.mod 中去写一些 replace 逻辑,来锁定外部依赖库版本。\n除了依赖问题以外,dubbo-go 最初的设计大量使用了 init 函数。init 函数用来实现一些初始化和依赖注入确实比较方便,但如果一个项目中的模块会被其它外部项目依赖时,init 则可能给我们造成麻烦,举个简单的例子:\npackage x var GlobalMap = map[string]int{} package a func init() { x.GlobalMap[\u0026#34;x\u0026#34;] = 1 } package b func init() { x.GlobalMap[\u0026#34;x\u0026#34;] = 2 } 如果我们在某个包中同时依赖 a 和 b,那么下面的两种写法,得到的结果是不一样的:\npackage show import ( \u0026#34;a\u0026#34; \u0026#34;b\u0026#34; \u0026#34;x\u0026#34; ) func main() { println(x.GlobalMap[\u0026#34;x\u0026#34;]) } package show import ( \u0026#34;b\u0026#34; \u0026#34;a\u0026#34; \u0026#34;x\u0026#34; ) func main() { println(x.GlobalMap[\u0026#34;x\u0026#34;]) } 为了避免这些隐式的 init 行为,我们实际上是 fork 了 dubbo-go 并进行了少量修改的。当然,改动并不多,未来如果 dubbo-go 有更好的模块化方法的话,我们也可以很轻松地迁移回直接依赖 dubbo-go。\n在 MOSN 集成 dubbo-go 的过程中,dubbo-go 社区的老哥们给予了我们大量的支持,贤哥(@zouyx) 帮助我们在 dubbo-go 中实现了之前不支持的 unsub/unpub 功能,并帮助我们解决了很多技术方面的问题。dubbo-go 社区负责人于雨(@alexstocks)也在开发过程中提供了大量的信息和技术支持。有这两位的支持,MOSN 和 dubbo 的集成才顺畅无比,否则的话要多走很多弯路。\n其它选择 除了本文提到的 MOSN + dubbo-go 集成方式,多点的陈鹏同学为我们提供了另一种思路进行集成,本文就不展开了,感兴趣的同学可以参考文末的资料。\n作者简介 曹春晖,开源 MOSN committer,@cch123,蚂蚁集团系统部技术专家,主攻 Service Mesh 方向。个人技术网站 xargin.com,和他人合著《Go 语言高级编程》。\n参考资料 什么是Service Mesh(服务网格) MOSN 多协议机制解析 多点生活在 Service Mesh 上的实践 ","excerpt":"Service Mesh 简介 Service Mesh 本身的理念并不复杂,就是将现代微服务应用的功能性与非功能性需求进行分离,并将非功能性需求下沉到应用的外部模块,从而使应用模块可以尽量聚焦于业 …","ref":"https://mosn.io/blog/posts/mosn-dubbo-integrate/","title":"在 MOSN 中玩转 dubbo-go"},{"body":"蚂蚁集团内部对 Service Mesh 的稳定性和性能要求是比较高的,内部 MOSN 广泛用于生产环境。在云上和开源社区,RPC 领域 Dubbo 和 Spring Cloud 同样广泛用于生产环境,我们在 MOSN 基础上,支持了 Dubbo 和 spring cloud 流量代理。我们发现在支持 Dubbo 协议过程中,经过 Mesh 流量代理后,性能有非常大的性能损耗,在大商户落地 Mesh 中也对性能有较高要求,因此本文会重点描述在基于 Go 语言库 dubbo-go-hessian2 、Dubbo 协议中对 MOSN 所做的性能优化。\n性能优化概述 根据实际业务部署场景,并没有选用高性能机器,使用普通 linux 机器,配置和压测参数如下:\n Intel (R) Xeon (R) Platinum 8163 CPU @ 2.50GHz 4 核 16G 。 pod 配置 2c、1g,JVM 参数 -server -Xms1024m -Xmx1024m。 网络延迟 0.23 ms, 2 台 linux 机器,分别部署 server + mosn, 压测程序 rpc-perfomance。 经过 3 轮性能优化后,使用优化版本 MOSN 将会获得以下性能收益(框架随机 512 和 1k 字节压测):\n 512 字节数据:MOSN + Dubbo 服务调用 TPS 整体提升 55-82.8%,RT 降低 45% 左右,内存占用 40M, 1k 数据:MOSN + Dubbo 服务调用 TPS 整体提升 51.1-69.3%,RT 降低 41% 左右,内存占用 41M。 性能优化工具 pprof 磨刀不误砍柴工,在性能优化前首先要找到性能卡点,找到性能卡点后,另一个难点就是如何用高效代码优化替代 slow code。因为蚂蚁集团 Service Mesh 是基于 go 语言实现的,我们首选 go 自带的 pprof 性能工具,我们简要介绍这个工具如何使用。如果我们 go 库自带 http.Server 时并且在 main 头部导入 import _ \u0026quot;net/http/pprof\u0026quot;,go 会帮我们挂载对应的 handler , 详细可以参考 godoc 。\n因为 mosn 默认会在 34902 端口暴露 http 服务,通过以下命令轻松获取 mosn 的性能诊断文件:\ngo tool pprof -seconds 60 http://benchmark-server-ip:34902/debug/pprof/profile # 会生成类似以下文件,该命令采样cpu 60秒 # pprof.mosn.samples.cpu.001.pb.gz 然后继续用 pprof 打开诊断文件,方便在浏览器查看,在图 1-1 给出压测后 profiler 火焰图:\n# http=:8000代表pprof打开8000端口然后用于web浏览器分析 # mosnd代表mosn的二进制可执行文件,用于分析代码符号 # pprof.mosn.samples.cpu.001.pb.gz是cpu诊断文件 go tool pprof -http=:8000 mosnd pprof.mosn.samples.cpu.001.pb.gz 图 1-1 MOSN 性能压测火焰图\n在获得诊断数据后,可以切到浏览器 Flame Graph(火焰图,go 1.11 以上版本自带),火焰图的 x 轴坐标代表 CPU 消耗情况, y 轴代码方法调用堆栈。在优化开始之前,我们借助 go 工具 pprof 可以诊断出大致的性能卡点在以下几个方面(直接压 server 端 MOSN):\n MOSN 在接收 Dubbo 请求,CPU 卡点在 streamConnection.Dispatch MOSN 在转发 Dubbo 请求,CPU 卡点在 downStream.Receive 可以点击火焰图任意横条,进去查看长方块耗时和堆栈明细(请参考图 1-2 和 1-3 所示):\n图 1-2 Dispatch 火焰图明细\n图 1-3 Receive 火焰图明细\n性能优化思路 本文重点记录优化了哪些 case 才能提升 50% 以上的吞吐量和降低 RT,因此后面直接分析当前优化了哪些 case。在此之前,我们以 Dispatch 为例,看下它为甚么那么吃性能 。在 terminal 中通过以下命令可以查看代码行耗费 CPU 数据(代码有删减):\ngo tool pprof mosnd pprof.mosn.samples.cpu.001.pb.gz (pprof) list Dispatch Total: 1.75mins 370ms 37.15s (flat, cum) 35.46% of Total 10ms 10ms 123:func (conn *streamConnection) Dispatch(buffer types.IoBuffer) { 40ms 630ms 125: log.DefaultLogger.Tracef(\u0026#34;stream connection dispatch data string = %v\u0026#34;, buffer.String()) . . 126: . . 127: // get sub protocol codec . 250ms 128: requestList := conn.codec.SplitFrame(buffer.Bytes()) 20ms 20ms 129: for _, request := range requestList { 10ms 160ms 134: headers := make(map[string]string) . . 135: // support dynamic route 50ms 920ms 136: headers[strings.ToLower(protocol.MosnHeaderHostKey)] = conn.connection.RemoteAddr().String() . . 149: . . 150: // get stream id 10ms 440ms 151: streamID := conn.codec.GetStreamID(request) . . 156: // request route . 50ms 157: requestRouteCodec, ok := conn.codec.(xprotocol.RequestRouting) . . 158: if ok { . 20.11s 159: routeHeaders := requestRouteCodec.GetMetas(request) . . 165: } . . 166: . . 167: // tracing 10ms 80ms 168: tracingCodec, ok := conn.codec.(xprotocol.Tracing) . . 169: var span types.Span . . 170: if ok { 10ms 1.91s 171: serviceName := tracingCodec.GetServiceName(request) . 2.17s 172: methodName := tracingCodec.GetMethodName(request) . . 176: . . 177: if trace.IsEnabled() { . 50ms 179: tracer := trace.Tracer(protocol.Xprotocol) . . 180: if tracer != nil { 20ms 1.66s 181: span = tracer.Start(conn.context, headers, time.Now()) . . 182: } . . 183: } . . 184: } . . 185: . 110ms 186: reqBuf := networkbuffer.NewIoBufferBytes(request) . . 188: // append sub protocol header 10ms 950ms 189: headers[types.HeaderXprotocolSubProtocol] = string(conn.subProtocol) 10ms 4.96s 190: conn.OnReceive(ctx, streamID, protocol.CommonHeader(headers), reqBuf, span, isHearbeat) 30ms 60ms 191: buffer.Drain(requestLen) . . 192: } . . 193:} 通过上面 list Dispatch 命令,性能卡点主要分布在 159 、 171 、172 、 181 、和 190 等行,主要卡点在解码 dubbo 参数、重复解参数、tracer、发序列化和 log 等。\n1. 优化 dubbo 解码 GetMetas 我们通过解码 dubbo 的 body 可以获得以下信息,调用的目标接口( interface )和调用方法的服务分组( group )等信息,但是需要跳过所有业务方法参数,目前使用开源的 dubbo-go-hessian2 库,解析 string 和 map 性能较差,提升 hessian 库解码性能,会在本文后面讲解。\n优化思路:\n在 mosn 的 ingress 端( mosn 直接转发请求给本地 java server 进程), 我们根据请求的 path 和 version 窥探用户使用的 interface 和 group , 构建正确的 dataID 可以进行无脑转发,无需解码 body,榨取性能提升。\n我们可以在服务注册时,构建服务发布的 path 、version 和 group 到 interface 、group 映射。在 mosn 转发 dubbo 请求时可以通过读锁查 cache + 跳过解码 body,加速 mosn 性能。\n因此我们构建以下 cache 实现(数组 + 链表数据结构), 可参见 优化代码 diff :\n// metadata.go // DubboPubMetadata dubbo pub cache metadata var DubboPubMetadata = \u0026amp;Metadata{} // DubboSubMetadata dubbo sub cache metadata var DubboSubMetadata = \u0026amp;Metadata{} // Metadata cache service pub or sub metadata. // speed up for decode or encode dubbo peformance. // please do not use outside of the dubbo framwork. type Metadata struct { data map[string]*Node mu sync.RWMutex // protect data internal } // Find cached pub or sub metatada. // caller should be check match is true func (m *Metadata) Find(path, version string) (node *Node, matched bool) { // we found nothing if m.data == nil { return nil, false } m.mu.RLocker().Lock() // for performance // m.mu.RLocker().Unlock() should be called. // we check head node first head := m.data[path] if head == nil || head.count \u0026lt;= 0 { m.mu.RLocker().Unlock() return nil, false } node = head.Next // just only once, just return // for dubbo framwork, that\u0026#39;s what we\u0026#39;re expected. if head.count == 1 { m.mu.RLocker().Unlock() return node, true } var count int var found *Node for ; node != nil; node = node.Next { if node.Version == version { if found == nil { found = node } count++ } } m.mu.RLocker().Unlock() return found, count == 1 } // Register pub or sub metadata func (m *Metadata) Register(path string, node *Node) { m.mu.Lock() // for performance // m.mu.Unlock() should be called. if m.data == nil { m.data = make(map[string]*Node, 4) } // we check head node first head := m.data[path] if head == nil { head = \u0026amp;Node{ count: 1, } // update head m.data[path] = head } insert := \u0026amp;Node{ Service: node.Service, Version: node.Version, Group: node.Group, } next := head.Next if next == nil { // fist insert, just insert to head head.Next = insert // record last element head.last = insert m.mu.Unlock() return } // we check already exist first for ; next != nil; next = next.Next { // we found it if next.Version == node.Version \u0026amp;\u0026amp; next.Group == node.Group { // release lock and no nothing m.mu.Unlock() return } } head.count++ // append node to the end of the list head.last.Next = insert // update last element head.last = insert m.mu.Unlock() } 通过服务注册时构建好的 cache,可以在 MOSN 的 stream 做解码时命中 cache , 无需解码参数获取接口和 group 信息,可参见优化代码 diff :\n// decoder.go // for better performance. // If the ingress scenario is not using group, // we can skip parsing attachment to improve performance if listener == IngressDubbo { if node, matched = DubboPubMetadata.Find(path, version); matched { meta[ServiceNameHeader] = node.Service meta[GroupNameHeader] = node.Group } } else if listener == EgressDubbo { // for better performance. // If the egress scenario is not using group, // we can skip parsing attachment to improve performance if node, matched = DubboSubMetadata.Find(path, version); matched { meta[ServiceNameHeader] = node.Service meta[GroupNameHeader] = node.Group } } 在 MOSN 的 egress 端( mosn 直接转发请求给本地 java client 进程), 我们采用类似的思路,我们根据请求的 path 和 version 去窥探用户使用的 interface 和 group , 构建正确的 dataID 可以进行无脑转发,无需解码 body,榨取性能提升。\n2. 优化 dubbo 解码参数 在 dubbo 解码参数值的时候 ,MOSN 采用的是 hessian 的正则表达式查找,非常耗费性能。我们先看下优化前后 benchmark 对比,性能提升 50 倍。\ngo test -bench=BenchmarkCountArgCount -run=^$ -benchmem BenchmarkCountArgCountByRegex-12 200000 6236 ns/op 1472 B/op 24 allocs/op BenchmarkCountArgCountOptimized-12 10000000 124 ns/op 0 B/op 0 allocs/op 优化思路:\n可以消除正则表达式,采用简单字符串解析识别参数类型个数, Dubbo 编码参数个数字符串实现 并不复杂,主要给对象加 L 前缀、数组加 [、primitive 类型有单字符代替。采用 go 可以实现同等解析,可以参考优化代码 diff :\nfunc getArgumentCount(desc string) int { len := len(desc) if len == 0 { return 0 } var args, next = 0, false for _, ch := range desc { // is array ? if ch == \u0026#39;[\u0026#39; { continue } // is object ? if next \u0026amp;\u0026amp; ch != \u0026#39;;\u0026#39; { continue } switch ch { case \u0026#39;V\u0026#39;, // void \u0026#39;Z\u0026#39;, // boolean \u0026#39;B\u0026#39;, // byte \u0026#39;C\u0026#39;, // char \u0026#39;D\u0026#39;, // double \u0026#39;F\u0026#39;, // float \u0026#39;I\u0026#39;, // int \u0026#39;J\u0026#39;, // long \u0026#39;S\u0026#39;: // short args++ default: // we found object if ch == \u0026#39;L\u0026#39; { args++ next = true // end of object ? } else if ch == \u0026#39;;\u0026#39; { next = false } } } return args } 3. 优化 dubbo hessian go 解码 string 性能 在图 1-2 中可以看到 dubbo hessian go 在解码 string 占比 CPU 采样较高,我们在解码 dubbo 请求时,会解析 dubbo 框架版本、调用 path 、接口版本和方法名,这些都是 string 类型,dubbo hessian go 解析 string 会影响 RPC 性能。\n我们首先跑一下 benchmar k 前后解码 string 性能对比,性能提升 56.11%, 对应到 RPC 中有 5% 左右提升。\nBenchmarkDecodeStringOriginal-12 1967202 613 ns/op 272 B/op 6 allocs/op BenchmarkDecodeStringOptimized-12 4477216 269 ns/op 224 B/op 5 allocs/op 优化思路:\n直接使用 UTF-8 byte 解码,性能最高,之前先解码 byte 成 rune , 对 rune 解码成 string ,及其耗费性能。增加批量 string chunk copy ,降低 read 调用,并且使用 unsafe 转换 string (避免一些校验),因为代码优化 diff 较多,这里给出优化代码 PR 。\ngo SDK 代码 runtime/string.go#slicerunetostring( rune 转换成 string ), 同样是把 rune 转成 byte 数组,这里给了我优化思路启发。\n4. 优化 hessian 库编解码对象 虽然消除了 dubbo 的 body 解码部分,但是 MOSN 在处理 dubbo 请求时,必须要借助 hessian 去 decode 请求头部的框架版本、请求 path 和接口版本值。但是每次在解码的时候都会创建序列化对象,开销非常高,因为 hessian 每次在创建 reader 的时候会 allocate 4k 数据并 reset。\n10ms 10ms 75:func unSerialize(serializeId int, data []byte, parseCtl unserializeCtl) *dubboAttr { 10ms 140ms 82: attr := \u0026amp;dubboAttr{} 80ms 2.56s 83: decoder := hessian.NewDecoderWithSkip(data[:]) ROUTINE ======================== bufio.NewReaderSize in /usr/local/go/src/bufio/bufio.go 50ms 2.44s (flat, cum) 2.33% of Total . 220ms 55: r := new(Reader) 50ms 2.22s 56: r.reset(make([]byte, size), rd) . . 57: return r . . 58:} 我们可以写个池化内存前后性能对比,性能提升 85.4% , benchmark 用例 :\nBenchmarkNewDecoder-12 1487685 803 ns/op 4528 B/op 9 allocs/op BenchmarkNewDecoderOptimized-12 10564024 117 ns/op 128 B/op 3 allocs/op 优化思路:\n在每次编解码时,池化 hessian 的 decoder 对象,新增 NewCheapDecoderWithSkip 并支持 reset 复用 decoder 。\nvar decodePool = \u0026amp;sync.Pool{ New: func() interface{} { return hessian.NewCheapDecoderWithSkip([]byte{}) }, } // 在解码时按照如下方法调用 decoder := decodePool.Get().(*hessian.Decoder) // fill decode data decoder.Reset(data[:]) hessianPool.Put(decoder) 5. 优化重复解码 service 和 methodName 值 xprotocol 在实现 xprotocol.Tracing 获取服务名称和方法时,会触发调用并解析 2 次,调用开销比较大。\n10ms 1.91s 171: serviceName := tracingCodec.GetServiceName(request) . 2.17s 172: methodName := tracingCodec.GetMethodName(request) 优化思路:\n因为在 GetMetas 里面已经解析过一次了,可以把解析过的 headers 传进去,如果 headers 有了就不用再去解析了,并且重构接口名称为一个,返回值为二元组,消除一次调用。\n6. 优化 streamID 类型转换 在 go 中将 byte 数组和 streamID 进行互转的时候,比较费性能。\n优化思路:\n生产代码中,尽量不要使用 fmt.Sprintf 和 fmt.Printf 去做类型转换和打印信息。可以使用 strconv 去转换。\n. 430ms 147: reqIDStr := fmt.Sprintf(\u0026#34;%d\u0026#34;, reqID) 60ms 4.10s 168: fmt.Printf(\u0026#34;src=%s, len=%d, reqid:%v\\n\u0026#34;, streamID, reqIDStrLen, reqIDStr) 7. 优化昂贵的系统调用 mosn 在解码 dubbo 的请求时,会在 header 中塞一份远程 host 的地址,并且在 for 循环中获取 remote IP,系统调用开销比较高。\n优化思路:\n50ms 920ms 136: headers[strings.ToLower(protocol.MosnHeaderHostKey)] = conn.connection.RemoteAddr().String() 在获取远程地址时,尽可能在 streamConnection 中 cache 远程 IP 值,不要每次都去调用 RemoteAddr。\n8. 优化 slice 和 map 触发扩容和 rehash 在 mosn 处理 dubbo 请求时,会根据接口、版本和分组去构建 dataID ,然后匹配 cluster , 会创建默认 slice 和 map 对象,经过性能诊断,导致不断 allocate slice 和 grow map 容量比较费性能。\n优化思路:\n使用 slice 和 map 时,尽可能预估容量大小,使用 make (type, capacity) 去指定初始大小。\n9. 优化 trace 日志级别输出 mosn 中不少代码在处理逻辑时,会打很多 trace 级别的日志,并且会传递不少参数值。\n优化思路:\n调用 trace 输出前,尽量判断一下日志级别,如果有多个 trace 调用,尽可能把所有字符串写到 buf 中,然后把 buf 内容写到日志中,并且尽可能少的调用 trace 日志方法。\n10. 优化 tracer、log 和 metrics 在大促期间,对机器的性能要求较高,经过性能诊断,tracer、mosn log 和 cloud metrics 写日志( IO 操作)非常耗费性能。\n优化思路:\n通过配置中心下发配置或者增加大促开关,允许 API 调用这些 feature 的开关。\n/api/v1/downgrade/on /api/v1/downgrade/off 11. 优化 route header 解析 MOSN 中在做路由前,需要做大量的 header 的 map 访问,比如 IDC、antvip 等逻辑判断,商业版或者开源 mosn 不需要这些逻辑,这些也会占用一些开销。\n优化思路:\n如果是云上逻辑,主站的逻辑都不走。\n12. 优化 featuregate 调用 在 MOSN 中处理请求时,为了区分主站和商业版路由逻辑,会通过 featuregate 判断逻辑走哪部分。通过 featuregate 调用开销较大,需要频繁的做类型转换和多层 map 去获取。\n优化思路:\n通过一个 bool 变量记录 featuregate 对应开关,如果没有初始化过,就主动调用一下 featuregate。\n未来性能优化思考 经过几轮性能优化 ,目前看火焰图,卡点都在 connection 的 read 和 write ,可以优化的空间比较小了。但是可能从以下场景中获得收益:\n 减少 connection 的 read 和 write 次数 (syscall) 。 优化 IO 线程模型,减少携程和上下文切换等。 作为结束,给出了最终优化后的火焰图 ,大部分卡点都在系统调用和网络读写,请参考图 1-4。\n图 1-4 优化版本 MOSN + Dubbo 火线图\n其他 pprof 工具异常强大,可以诊断 CPU、memory、go 协程、tracer 和死锁等,该工具可以参考 godoc,性能优化参考:\n https://blog.golang.org/pprof https://www.cnblogs.com/Dr-wei/p/11742414.html 关于作者 商宗海(诣极),GitHub ID zonghaishang,Apache Dubbo PMC,目前就职于蚂蚁集团金服中间件团队,主攻 RPC 和 Service Mesh 方向。 《深入理解 Apache Dubbo 与实战》一书作者。\n","excerpt":"蚂蚁集团内部对 Service Mesh 的稳定性和性能要求是比较高的,内部 MOSN 广泛用于生产环境。在云上和开源社区,RPC 领域 Dubbo 和 Spring Cloud 同样广泛用于生产环 …","ref":"https://mosn.io/blog/posts/mosn-dubbo-go-hessian2/","title":"记一次在 MOSN 对 Dubbo、dubbo-go-hessian2 的性能优化"},{"body":"Dubbo 介绍 Dubbo 最初是 Java 开发的一套 RPC 框架,随着社区的发展。当前 dubbo 也渐渐成为一套跨语言的解决方案。除了 Java 以外,还有相应的 Go 实现。有规律的版本发布节奏,社区较为活跃。\nDubbo 服务 mesh 化 接入 service mesh 的应用,其服务发现应该由相应的 mesh 模块接管。一般由控制面将相应的服务发现配置进行订阅和下发。但这里存在几个问题:\n 如果公司是第一次接入 service mesh,不希望一次引入太多模块,这样会增加整体的运维负担。如果可以渐进地迁移到 service mesh 架构,例如先接入数据面,再接入控制面。那么就可以随时以较低的成本进行回滚。也不会给运维造成太大的压力。\n 每个公司都有自己的发展规划,并不是每个公司都完整地拥抱了云原生。大部分公司可能存在部分上云,部分未上云的情况,在迁移到 service mesh 时,也存在部分应用接入了 service mesh,而另一部分未接入的情况。需要考虑跨架构互通。\n 我们这里提出的方案希望能够解决这些问题。\n服务发现接入 配置工作 在配置文件中,我们配置了两个 listener:\n 一个是 serverListener,负责拦截外部进入的流量,转发给本地模块,这个方向的请求不需要做特殊处理,只要使用 xprotocol 转发给本机即可。\n 一个是 clientListener,负责拦截本机向外发起的请求,因为外部集群根据服务注册中心下发的 endpoint 列表动态变化,所以该 listener 对应的也是一个 特殊的 router 名 \u0026ldquo;dubbo\u0026rdquo;。,这里务必注意。\n \u0026#34;listeners\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;serverListener\u0026#34;, \u0026#34;address\u0026#34;: \u0026#34;127.0.0.1:2046\u0026#34;, \u0026#34;bind_port\u0026#34;: true, \u0026#34;log_path\u0026#34;: \u0026#34;stdout\u0026#34;, \u0026#34;filter_chains\u0026#34;: [ { \u0026#34;tls_context\u0026#34;: {}, \u0026#34;filters\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;proxy\u0026#34;, \u0026#34;config\u0026#34;: { \u0026#34;downstream_protocol\u0026#34;: \u0026#34;X\u0026#34;, \u0026#34;upstream_protocol\u0026#34;: \u0026#34;X\u0026#34;, \u0026#34;router_config_name\u0026#34;: \u0026#34;server_router\u0026#34;, \u0026#34;extend_config\u0026#34;: { \u0026#34;sub_protocol\u0026#34;: \u0026#34;dubbo\u0026#34; } } } ] } ] }, { \u0026#34;name\u0026#34;: \u0026#34;clientListener\u0026#34;, \u0026#34;address\u0026#34;: \u0026#34;0.0.0.0:2045\u0026#34;, \u0026#34;bind_port\u0026#34;: true, \u0026#34;log_path\u0026#34;: \u0026#34;stdout\u0026#34;, \u0026#34;filter_chains\u0026#34;: [ { \u0026#34;tls_context\u0026#34;: {}, \u0026#34;filters\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;proxy\u0026#34;, \u0026#34;config\u0026#34;: { \u0026#34;downstream_protocol\u0026#34;: \u0026#34;X\u0026#34;, \u0026#34;upstream_protocol\u0026#34;: \u0026#34;X\u0026#34;, \u0026#34;router_config_name\u0026#34;: \u0026#34;dubbo\u0026#34;, \u0026#34;extend_config\u0026#34;: { \u0026#34;sub_protocol\u0026#34;: \u0026#34;dubbo\u0026#34; } } } ] } ] } ] 开发工作 第一步,在 MOSN 配置中增加 dubbo_registry 扩展选项:\n\u0026#34;extends\u0026#34; : [{ \u0026#34;type\u0026#34; : \u0026#34;dubbo_registry\u0026#34;, \u0026#34;config\u0026#34; : { \u0026#34;enable\u0026#34; : true, \u0026#34;server_port\u0026#34; : 20080, \u0026#34;api_port\u0026#34; : 22222, \u0026#34;log_path\u0026#34; : \u0026#34;/tmp\u0026#34; } }] 该配置与 tracing、admin 等为平级配置。\n第二步,针对接入的服务,需要简单修改 sdk 中的 pub、sub 环节代码:\n pub 时,如果当前环境为接入 MOSN 环境(可通过配置系统下发的开关来判断),则调用 MOSN 的 pub 接口,而非直接去注册中心 pub。\n sub 时,如果当前环境为接入 MOSN 环境,则调用 MOSN 的 sub 接口,不去注册中心 sub。\n 第三步,应用退出时,需要将所有 pub、sub 的服务执行反向操作,即 unpub、unsub。\n在本文中使用 httpie 来发送 http 请求。使用 dubbo-go 中的样例程序作为我们的服务的 client 和 server。\n接下来我们使用 httpie 来模拟各种情况下的 pub、sub 流程。\n直连 client 与正常的 dubbo service 互通 例子路径\nService 是正常的 dubbo service,所以会自动注册到 zk 中去,不需要我们帮它 pub,这里只要 sub 就可以了,所以执行流程为:\n第一步,修改 MOSN 配置,增加 dubbo_registry 的 extend 扩展。\n第二步,mosn start。\n第三步,start server。\n第四步,subscribe service。\nhttp --json post localhost:22222/sub registry:=\u0026#39;{\u0026#34;type\u0026#34;:\u0026#34;zookeeper\u0026#34;, \u0026#34;addr\u0026#34; : \u0026#34;127.0.0.1:2181\u0026#34;}\u0026#39; service:=\u0026#39;{\u0026#34;interface\u0026#34; : \u0026#34;com.ikurento.user.UserProvider\u0026#34;, \u0026#34;methods\u0026#34; :[\u0026#34;GetUser\u0026#34;], \u0026#34;group\u0026#34; : \u0026#34;\u0026#34;, \u0026#34;version\u0026#34; : \u0026#34;\u0026#34;}\u0026#39; --verbose 第五步,start client。\n在 client 中正确看到返回结果的话,说明请求成功了。\n直连 client 与直连 dubbo service 互通 例子路径\n直连的服务不会主动对自身进行发布,直连的 client 不会主动进行订阅。因此此例子中,pub 和 sub 都是由我们来辅助进行的。\n第一步,修改 MOSN 配置,增加 dubbo_registry 的 extend 扩展。\n第二步,mosn start\n第三步,start server\n第四步,subscribe service\nhttp --json post localhost:22222/sub registry:=\u0026#39;{\u0026#34;type\u0026#34;:\u0026#34;zookeeper\u0026#34;, \u0026#34;addr\u0026#34; : \u0026#34;127.0.0.1:2181\u0026#34;}\u0026#39; service:=\u0026#39;{\u0026#34;interface\u0026#34; : \u0026#34;com.ikurento.user.UserProvider\u0026#34;, \u0026#34;methods\u0026#34; :[\u0026#34;GetUser\u0026#34;], \u0026#34;group\u0026#34; : \u0026#34;\u0026#34;, \u0026#34;version\u0026#34; : \u0026#34;\u0026#34;}\u0026#39; --verbose 第五步,publish service\nhttp --json post localhost:22222/pub registry:=\u0026#39;{\u0026#34;type\u0026#34;:\u0026#34;zookeeper\u0026#34;, \u0026#34;addr\u0026#34; : \u0026#34;127.0.0.1:2181\u0026#34;}\u0026#39; service:=\u0026#39;{\u0026#34;interface\u0026#34; : \u0026#34;com.ikurento.user.UserProvider\u0026#34;, \u0026#34;methods\u0026#34; :[\u0026#34;GetUser\u0026#34;], \u0026#34;group\u0026#34; : \u0026#34;\u0026#34;, \u0026#34;version\u0026#34; : \u0026#34;\u0026#34;}\u0026#39; --verbose 第六步,start client\n此时应该能看到 client 侧的响应。\n正常的 client 与直连 dubbo service 互通 例子路径\nClient 是正常 client,因此 client 会自己去 subscribe。我们只要正常地把服务 pub 出去即可:\n第一步,修改 MOSN 配置,增加 dubbo_registry 的 extend 扩展。\n第二步,mosn start\n第三步,start server\n第四步,publish service\nhttp --json post localhost:22222/sub registry:=\u0026#39;{\u0026#34;type\u0026#34;:\u0026#34;zookeeper\u0026#34;, \u0026#34;addr\u0026#34; : \u0026#34;127.0.0.1:2181\u0026#34;}\u0026#39; service:=\u0026#39;{\u0026#34;interface\u0026#34; : \u0026#34;com.ikurento.user.UserProvider\u0026#34;, \u0026#34;methods\u0026#34; :[\u0026#34;GetUser\u0026#34;], \u0026#34;group\u0026#34; : \u0026#34;\u0026#34;, \u0026#34;version\u0026#34; : \u0026#34;\u0026#34;}\u0026#39; --verbose 第五步,start client\n此时应该能看到 client 侧的响应。\nFAQ 目前还存在哪些问题么? 暂时还不支持 Dubbo 的路由,未来会进行支持。\n","excerpt":"Dubbo 介绍 Dubbo 最初是 Java 开发的一套 RPC 框架,随着社区的发展。当前 dubbo 也渐渐成为一套跨语言的解决方案。除了 Java 以外,还有相应的 Go 实现。有规律的版本发 …","ref":"https://mosn.io/docs/developer-guide/dubbo-integrate/","title":"Dubbo 集成"},{"body":"我们很高兴的宣布 MOSN v0.13.0 发布。\n以下是该版本的变更日志。\n新功能 支持 Strict DNS Cluster @dengqian 支持 GZip 处理的 Stream Filter @wangfakang Dubbo 服务发现完成 Beta 版本 @cch123 支持单机故障隔离的 Stream Filter @NeGnail 集成 Sentinel 限流能力 @ansiz 优化 优化 EDF LB 的实现,使用 EDF 重新实现 WRR LB @CodingSinger 配置获取 ADMIN API 优化,新增 Features 和环境变量相关 ADMIN API @nejisama 更新 Host 时触发健康检查的更新从异步模式修改为同步模式 @nejisama 更新了 Dubbo 库,优化了 Dubbo Decode 的性能 @zonghaishang 优化 Metrics 在 Prometheus 中的输出,使用正则过滤非法的 Key @nejisama 优化 MOSN 的返回状态码 @wangfakang Bug 修复 修复健康检查注册回调函数时的并发冲突问题 @nejisama 修复配置持久化函数没有正确处理空配置的错误 @nejisama 修复 ClusterName/RouterName 过长时,以文件形式 DUMP 会失败的问题 @nejisama 修复获取 XProtocol 协议时,无法正确获取协议的问题 @wangfakang 修复创建 StreamFilter 时,获取的 context 错误的问题 @wangfakang ","excerpt":"我们很高兴的宣布 MOSN v0.13.0 发布。\n以下是该版本的变更日志。\n新功能 支持 Strict DNS Cluster @dengqian 支持 GZip 处理的 Stream …","ref":"https://mosn.io/docs/products/report/releases/v0.13.0/","title":"MOSN v0.13.0 发布"},{"body":"起因 在2020年伊始,MOSN 团队在社区发起了 MOSN 源码解析系列活动,本次活动旨在增强社区对 MOSN 的认知,促进开源社区的交流,是大家学习和使用 MOSN,与 MOSN 的核心开发者直接交流的一个良好契机。\n经过十几位社区同学的参与,目前十四篇文章都已经完成,本文将做一个整体介绍,方便大家更好的了解 MOSN。查看原文解析系列文章请访问: https://mosn.io/blog/code/。\n模块能力 首先是 MOSN 的 启动流程,通过这篇文章,你可以了解 MOSN 的启动过程,包括配置解析,日志初始化,Xds 初始化,各子模块启动。另外通过 启动流程(v0.4.0) 这篇文章针对 AdminApi 初始化等过程做了分析,同时也介绍了普通启动和热升级启动的区别,对 MOSN 的平滑升级能力有一个初步的了解。\n路由 这个章节,你可以了解路由的配置解析,运行方式和动态路由等能力。\n在 TLS 你可以了解 MOSN 怎样集成 Go Runtime 的 TLS 能力,并在此基础上我们进行了那些能力的加强,比如明文密文自动识别能力,SDS 能力支持,对 TLS 握手进行自定义校验的扩展能力等。\n多协议机制 是 MOSN 比较重要的一部分,你可以了解多协议机制产生的背景与实践痛点,一些常见的协议扩展思路初探,SOFABolt 协议接入实践,以及 MOSN 多协议机制设计解读。通过阅读本文,你可以很容易的实现一个自己的协议了。\n变量机制 是 MOSN 的一个核心能力,通过该机制,MOSN 可以方便的获取和设置一些自定义的值,来满足打印日志,路由规则,请求自定变量等能力。\n共享内存模型 讲解了 MOSN 的共享内存框架,MOSN 通过这个框架实现了共享内存 metrics 实现,用于平滑升级时保证 metric 数据的准确性。\n为了支持 MOSN 跨语言的扩展机制能力,我们开发了 plugin机制,通过独立进程进行GRPC交互,让用户可以用任何语言来开发插件。\n连接池 介绍了 MOSN 针对连接池的管理和使用。连接池是上下游 MOSN 之间进行长连接复用以提高转发效率与降低时延的关键,MOSN 连接池提供基于 HTTP1, HTTP2, SOFARPC, XProtocol 协议的连接池。\n协程模型 讲解了MOSN的整个转发流程,包括读写IO流程,proxy状态机,协程池等核心能力,可以让读者更加清晰的了解 MOSN 的处理流程。\nGo 语言的 GC 对程序的性能有很大的影响,针对于此我们开发了 内存复用机制,减少 GC 来提升 MOSN 性能,本文分析了 MOSN 对内存复用的设计和用法,其基于 sync.Pool 之上封装了一层自己的注册管理逻辑,增强了管理能力、易用性和复用性。\nlog系统 介绍了 log 日志和 metric 两部分内容,log 也作为一个单独的库,读者可以单独使用。\nxds 介绍了 MOSN 对齐 xds 能力的细节。\nHTTP能力 介绍 MOSN 对 HTTP 的处理流程,包括适配 fasthttp,内存复用,连接池等能力。\nfilter扩展机制 分析了 filter 扩展机制的实现,并简述了实现自己的 filter 需要做的东西。大家可以通过该机制,使用 MOSN 来处理自己的业务场景。\n最后 感谢社区同学的热情参与,MOSN 源码解析活动圆满收尾,也敬请关注我们后续的其他活动。\n","excerpt":"起因 在2020年伊始,MOSN 团队在社区发起了 MOSN 源码解析系列活动,本次活动旨在增强社区对 MOSN 的认知,促进开源社区的交流,是大家学习和使用 MOSN,与 MOSN 的核心开发者直接 …","ref":"https://mosn.io/blog/code/mosn-overview/","title":"MOSN 源码解析 - 总览"},{"body":"我们很高兴的宣布 MOSN v0.12.0 发布,感谢孙福泽(@peacocktrain)对该版本做出的巨大贡献,经 MOSN 社区 Lead 们认证为 commiter 🎉。\n以下是该版本的变更日志。\n新功能 支持 Skywalking @arugal Stream Filter 新增了一个 Receive Filter 执行的阶段,可在 MOSN 路由选择完 Host 以后,再次执行 Receive Filter @wangfakang HTTP2 支持流式 @peacocktrain @taoyuanyuan FeatureGate 新增接口 KnownFeatures,可输出当前 FeatureGate 状态 @nejisama 提供一种协议透明的方式获取请求资源(PATH、URI、ARG),对于资源的定义由各个协议自身定义 @wangfakang 新增负载均衡算法 支持 ActiveRequest LB @CodingSinger 支持 WRR LB @nejisama 优化 XProtocol 协议引擎优化 @neverhook 修改 XProtocol 心跳响应接口,支持协议的心跳响应可返回更多的信息 优化 connpool 的心跳触发,只有实现了心跳的协议才会发心跳 Dubbo 库依赖版本从 v1.5.0-rc1 更新到 v1.5.0 @cch123 API 调整,HostInfo 新增健康检查相关的接口 @wangfakang 熔断功能实现优化 @wangfakang 负责均衡选择逻辑简化,同样地址的 Host 复用相同的健康检查标记 @nejisama @cch123 优化 HTTP 建连逻辑,提升 HTTP 建立性能 @wangfakang 日志轮转逻辑从写日志触发,调整为定时触发 @nejisama typo 调整 @xujianhai666 @candyleer Bug 修复 修复 xDS 解析故障注入配置的错误 @champly 修复 MOSN HTTP HEAD 方法导致的请求 Hold 问题 @wangfakang 修复 XProtocol 引擎对于 StatusCode 映射缺失的问题 @neverhook 修复 DirectReponse 触发重试的 BUG @taoyuanyuan ","excerpt":"我们很高兴的宣布 MOSN v0.12.0 发布,感谢孙福泽(@peacocktrain)对该版本做出的巨大贡献,经 MOSN 社区 Lead 们认证为 commiter 🎉。\n以下是该版本的变更日 …","ref":"https://mosn.io/docs/products/report/releases/v0.12.0/","title":"MOSN v0.12.0 发布"},{"body":"相比传统的巨石(Monolith)应用,微服务的一个主要变化是将应用中的不同模块拆分为了独立的进程。在微服务架构下,原来进程内的方法调用成为了跨进程的远程方法调用。相对于单一进程内的方法调用而言,跨进程调用的调试和故障分析是非常困难的,难以使用传统的代码调试程序或者日志打印来对分布式的调用过程进行查看和分析。\n如上图右边所示,微服务架构中系统中各个微服务之间存在复杂的调用关系。\n一个来自客户端的请求在其业务处理过程中经过了多个微服务进程。我们如果想要对该请求的端到端调用过程进行完整的分析,则必须将该请求经过的所有进程的相关信息都收集起来并关联在一起,这就是“分布式追踪”。\n以上关于分布式追踪的介绍引用自 Istio Handbook。\nMOSN 中 tracing 的架构 MOSN 的 tracing 框架由 Driver、Tracer 和 Span 三个部分组成。\nDriver 是 Tracer 的容器,管理注册的 Tracer 实例,Tracer 是 tracing 的入口,根据请求信息创建一个 Span,Span 存储当前跨度的链路信息。\n目前 MOSN tracing 有 SOFATracer 和 SkyWalking 两种实现。SOFATracer 支持 http1 和 xprotocol 协议的链路追踪,将 trace 数据写入本地日志文件中。SkyWalking 支持 http1 协议的链路追踪,使用原生的 Go 语言探针 go2sky 将 trace 数据通过 gRPC 上报到 SkyWalking 后端服务。\n快速开始 下面将使用 Docker 和 docker-compose 来快速开始运行一个集成了 SkyWalking 的分布式追踪示例,该示例代码请见 MOSN GitHub。\n准备 安装 docker 和 docker-compose。\n 安装 docker\n 安装 docker-compose\n 需要一个编译好的 MOSN 程序,您可以下载 MOSN 源码自行编译,或者直接下载 MOSN v0.12.0 发行版以获取 MOSN 的运行时二进制文件。\n下面将以源码编译的方式演示 MOSN 如何与 SkyWalking 集成。\ncd ${projectpath}/cmd/mosn/main go build 获取示例代码目录。\n${targetpath} = ${projectpath}/examples/codes/trace/skywalking/http/ 将编译好的程序移动到示例代码目录。\nmv main ${targetpath}/ cd ${targetpath} 目录结构 下面是 SkyWalking 的目录结构。\n* skywalking └─── http │ main # 编译完成的 MOSN 程序 | server.go # 模拟的 Http Server | clint.go # 模拟的 Http Client | config.json # MOSN 配置 | skywalking-docker-compose.yaml # skywalking docker-compose 运行说明 启动 SkyWalking oap \u0026amp; ui。\ndocker-compose -f skywalking-docker-compose.yaml up -d 启动一个 HTTP Server。\ngo run server.go 启动 MOSN。\n./main start -c config.json 启动一个 HTTP Client。\ngo run client.go 打开 http://127.0.0.1:8080 查看 SkyWalking-UI,SkyWalking Dashboard 界面如下图所示。\n在打开 Dashboard 后请点击右上角的 Auto 按钮以使页面自动刷新。\nDemo 视频 下面来看一下该 Demo 的操作视频。\n\n清理 要想销毁 SkyWalking 后台运行的 docker 容器只需要下面的命令。\ncd ${projectpath}/examples/codes/trace/skywalking/http/ docker-compose -f skywalking-docker-compose.yaml down 未来计划 在今年五月份,SkyWalking 8.0 版本会进行一次全面升级,采用新的探针协议和分析逻辑,探针将更具互感知能力,更好的在 Service Mesh 下使用探针进行监控。同时,SkyWalking 将开放之前仅存在于内核中的 metrics 指标分析体系。Prmoetheus、Spring Cloud Sleuth、Zabbix 等常用的 metrics 监控方式,都会被统一的接入进来,进行分析。此外, SkyWalking 与 MOSN 社区将继续合作:支持追踪 Dubbo 和 SOFARPC,同时适配 sidecar 模式下的链路追踪。\n关于 MOSN MOSN 是一款使用 Go 语言开发的网络代理软件,由蚂蚁集团开源并经过几十万容器的生产级验证。 MOSN 作为云原生的网络数据平面,旨在为服务提供多协议、模块化、智能化、安全的代理能力。 MOSN 是 Modular Open Smart Network 的简称。 MOSN 可以与任何支持 xDS API 的 Service Mesh 集成,亦可以作为独立的四、七层负载均衡,API Gateway、云原生 Ingress 等使用。\n GitHub:https://github.com/mosn/mosn 官网:https://mosn.io 关于 Skywalking SkyWalking 是观察性分析平台和应用性能管理系统。提供分布式追踪、服务网格遥测分析、度量聚合和可视化一体化解决方案。支持 Java、.Net Core、PHP、NodeJS、Golang、LUA 语言探针,支持 Envoy/MOSN + Istio 构建的 Service Mesh。\n GitHub:https://github.com/apache/skywalking 官网:https://skywalking.apache.org 关于本文中的示例请参考 MOSN GitHub 和 MOSN 官方文档。\n","excerpt":"相比传统的巨石(Monolith)应用,微服务的一个主要变化是将应用中的不同模块拆分为了独立的进程。在微服务架构下,原来进程内的方法调用成为了跨进程的远程方法调用。相对于单一进程内的方法调用而言,跨进 …","ref":"https://mosn.io/blog/posts/skywalking-support/","title":"MOSN 支持使用 SkyWalking 进行分布式追踪"},{"body":"本文基于的内容是 MOSN v0.12.0。\n概述 MOSN 提供了基于 TLS 加密的安全通信的能力,本文主要从三个方面介绍 MOSN 的 TLS 相关实现,包括:MOSN 作为服务端提供 TLS 的能力、MOSN 作为客户端提供 TLS 的能力,以及 TLS 模块的实现。关于 TLS 的配置,可以参考配置文件说明的文档。\n服务端 (Listener) MOSN 作为服务端的时候,就是有请求发送到 MOSN,基于 MOSN 的 Listener 处理请求。Listener 做配置解析的时候,如果存在 TLS 相关的配置,会尝试生成一个 TLSManager,在连接建立的时候,会根据 TLSManager 返回的状态判断是否需要建立 TLS 加密连接,代码如下:\nfunc newActiveListener(lc *v2.Listener, ...) (*activeListener, error) { // 其他参数省略 ... // 省略 mgr, err := mtls.NewTLSServerContextManager(lc) if err != nil { return nil,err } al.tlsMng = mgr return al, nil } func (al *activeListener) OnAccept(rawc net.Conn, ch chan api.Connection, ...) { // 其他参数省略 ...// 省略 // if ch is not nil, the conn has been initialized in func transferNewConn if al.tlsMng != nil \u0026amp;\u0026amp; ch == nil { conn, err := al.tlsMng.Conn(rawc) if err != nil { rawc.Close() return } rawc = conn } arc := newActiveRawConn(rawc, al) ...// 省略 } 判断是否需要建立 TLS 连接会基于 TLSManager 的状态。\n 如果 TLSManager 是关闭 TLS 状态,则一定不支持 TLS 连接。 如果 TLSManager 是开启 TLS 状态,还需要额外判断是否支持 Inspector 模式,如果支持 Inspector,则说明 MOSN 的 Listener 可以同时处理 TLS 加密连接和明文的非加密连接;此时 MOSN 会等待连接上收到的第一个数据,判断请求是明文还是 TLS,从而决定使用连接状态。如果不支持 Inspector 模式,那么就只支持 TLS 连接。 判断是否兼容 Inspector 的逻辑如下。MOSN 会在建立上执行Peek,尝试获取连接上第一个数据字节,如果是 0x16(来自 TLS 握手的 Client Hello 的第一个字节),则判断为 TLS 连接,否则判断为明文连接。\nfunc (mng *serverContextManager) Conn(c net.Conn) (net.Conn, error) { if _, ok := c.(*net.TCPConn); !ok { return c, nil } if !mng.Enabled() { return c, nil } if !mng.inspector { return \u0026amp;TLSConn{ tls.Server(c, mng.config.Clone()), }, nil } // inspector conn := \u0026amp;Conn{ Conn: c, } buf, err := conn.Peek() if err != nil { return nil, err } switch buf[0] { // TLS handshake case 0x16: return \u0026amp;TLSConn{ tls.Server(conn, mng.config.Clone()), }, nil // Non TLS default: return conn, nil } } 除了普通的 TLS 以外,MOSN 还支持双向 TLS,即 Server 端要求 Client 端也提供证书,也是可以配置不同的兼容场景,包括:\n Client 必须提供 TLS 证书,完成双向 TLS 加密。 要求 Client 提供 TLS 证书,但是如果 Client 没有提供证书,也可以兼容执行普通的 TLS 加密逻辑实现如下。 func (ctx *tlsContext) setServerConfig(tmpl tls.Config, cfg *v2.TLSConfig, hooks ConfigHooks) { tlsConfig := \u0026amp;tmpl // no certificate should be set no server tls config if len(tlsConfig.Certificates) == 0 { return } if !cfg.RequireClientCert { tlsConfig.ClientAuth = tls.NoClientCert } else { if cfg.VerifyClient { tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert } else { tlsConfig.ClientAuth = tls.VerifyClientCertIfGiven } } tlsConfig.VerifyPeerCertificate = hooks.ServerHandshakeVerify(tlsConfig) ctx.server = tlsConfig // build matches ctx.buildMatch() } 客户端 (Cluster) MOSN 作为客户端的时候,就是 MOSN 在把请求向后端(Upstream)转发的时候,基于 MOSN 的 Cluster 配置转发请求。在 Cluster 配置解析的时候,如果存在 TLS 相关的配置,会尝试生成一个 TLSManager。在转发请求向后端建立连接的时候,会基于两个维度判断是否要建立 TLS 连接。\n 首先判断建立连接的 Host 配置是否允许建立 TLS 连接,这个是考虑到有的场景特定的 Host 不希望建立 TLS 连接或者不支持 TLS 连接进行的设计。默认配置情况下,Host 配置都是允许建立 TLS 连接的。 在 Host 允许建立 TLS 的情况下,会根据 TLSManager 的状态,判断是否要建立 TLS 连接。 func newSimpleCluster(clusterConfig v2.Cluster) *simpleCluster { ... // 省略 mgr, err := mtls.NewTLSClientContextManager(\u0026amp;clusterConfig.TLS) if err != nil { // 日志记录,此时 Cluster 还可以正常创建,等同于不支持 TLS } info.tlsMng = mgr cluster := \u0026amp;simpleCluster{ info: info, } ... // return cluster } func (sh *simpleHost) CreateConnection(context context.Context) types.CreateConnectionData { var tlsMng types.TLSContextManager if sh.SupportTLS() { tlsMng = sh.clusterInfo.TLSMng() } clientConn := network.NewClientConnection(nil, sh.clusterInfo.ConnectTimeout(), tlsMng, sh.Address(), nil) ... // 省略 } TLS 模块 MOSN 的 TLS 能力,都通过 TLSManager 提供,TLSManager 分为 ServerManager 和 ClientManager,分别有各自的逻辑,其核心都是通过 Conn 方法,利用 Provider 提供tls.Config,建立 TLS 连接。\nprovider provider 是 MOSN 的 TLS 模块中提供 TLS 运行时配置的模块,支持静态配置模式staticProvider和动态 SDS 模式sdsProvider。无论是哪种模式,provider 中最终存储的对象都是 MOSN 定义的tlsContext。\nMOSN 在进行配置解析时,会判断使用哪种模式,然后基于不同的情况解析出 tlsContext。同时需要说明的是,静态模式只是区别于动态 SDS 模式的一种模式,通过 TLS 的扩展,我们也可以做到静态模式下,让证书动态获取,这一点在后文中会进行介绍。\nfunc NewProvider(cfg *v2.TLSConfig) (types.TLSProvider, error) { if !cfg.Status { // 未开启 TLS return nil,nil } if cfg.SdsConfig != nil { // 动态 SDS 模式 ... // 省略 return getOrCreateProvider(cfg), nil } else { // 静态配置模式 ctx, err := newTLSContext(cfg, secret) ... // 省略 } } tlsContext tlsContext是 MOSN 中 TLS 运行时的基础单元,主要功能是负责提供 MOSN 运行时所需要的tls.Config。其定义与方法如下。\ntype tlsContext struct { serverName string ticket string matches map[string]struct{} client *tls.Config server *tls.Config } func (ctx *tlsContext) buildMatch() {} func (ctx *tlsContext) setServerConfig(tmpl tls.Config, cfg *v2.TLSConfig, hooks ConfigHooks) {} func (ctx *tlsContext) setClientConfig(tmpl tls.Config, cfg *v2.TLSConfig, hooks ConfigHooks) {} func (ctx *tlsContext) MatchedServerName(sn string) bool {} func (ctx *tlsContext) MatchedALPN(protos []string) bool {} func (ctx *tlsContext) GetTLSConfig(client bool) *tls.Config {} 基于 TLS 的配置,通过newTLSContext生成tlsContext,随后在 TLSManager 中通过MatchedServerName和MatchedALPN判断是否要选择该tlsContext,最后通过GetTLSConfig提供tls.Config建立 TLS 连接。\nfunc newTLSContext(cfg *v2.TLSConfig, secret *secretInfo) (*tlsContext, error) { ... // 省略 // extension config factory := getFactory(cfg.Type) hooks := factory.CreateConfigHooks(cfg.ExtendVerify) // pool can be nil, if it is nil, TLS uses the host\u0026#39;s root CA set. pool, err := hooks.GetX509Pool(secret.Validation) if err != nil { return nil, err } tmpl.RootCAs = pool tmpl.ClientCAs = pool // set tls context ctx := \u0026amp;tlsContext{ serverName: cfg.ServerName, ticket: cfg.Ticket, } cert, err := hooks.GetCertificate(secret.Certificate, secret.PrivateKey) ... // 省略 // needs copy template config if len(tmpl.Certificates) \u0026gt; 0 { ctx.setServerConfig(*tmpl, cfg, hooks) } ctx.setClientConfig(*tmpl, cfg, hooks) return ctx, nil } secretInfo包含了可以获取完整的 TLS 证书信息的内容,完整的证书信息包括证书、私钥、以及签发证书的 CA 信息。通常情况下,secretInfo 就是完整的 TLS 证书信息,在有 tls 扩展的场景,则是利用secretInfo通过扩展实现进行获取。代码中通过getFactory获取到的hooks就是扩展实现,在没有扩展的时候,返回的就是默认的hooks。\n动态 SDS 模式下,当 MOSN 收到来自 SDS 服务端的信息以后,也是通过调用newTLSContext生成tlsContext提供 TLS 能力。\nTLS 扩展 考虑到一些 TLS 运行时的配置安全需求以及证书校验需求,MOSN 对 TLS 运行时配置提供了一个扩展点,可以按照需求扩展两种类型,四个功能:\n 解析证书和私钥的方式可扩展,默认是读取证书 / 私钥的明文字符串或明文文件 解析 CA 证书的方式可扩展,默认是读取 CA 证书的明文字符串或明文文件 Server 端握手校验的方式可扩展,默认是标准的 TLS 握手校验方式 Client 端握手校验的方式可扩展,默认是标准的 TLS 握手校验方式 // ConfigHooks is a set of functions used to make a tls config type ConfigHooks interface { // GetCertificate returns the tls.Certificate by index. // By default the index is the cert/key file path or cert/key pem string GetCertificate(certIndex, keyIndex string) (tls.Certificate, error) // GetX509Pool returns the x509.CertPool, which is a set of certificates. // By default the index is the ca certificate file path or certificate pem string GetX509Pool(caIndex string) (*x509.CertPool, error) // ServerHandshakeVerify returns a function that used to set \u0026#34;VerifyPeerCertificate\u0026#34; defined in tls.Config. // If it is returns nil, the normal certificate verification will be used. // Notice that we set tls.Config.InsecureSkipVerify to make sure the \u0026#34;VerifyPeerCertificate\u0026#34; is called, // so the ServerHandshakeVerify should verify the trusted ca if necessary. // If the TLSConfig.RequireClientCert is false, the ServerHandshakeVerify will be ignored ServerHandshakeVerify(cfg *tls.Config) func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error // ClientHandshakeVerify returns a function that used to set \u0026#34;VerifyPeerCertificate\u0026#34; defined in tls.Config. // If it is returns nil, the normal certificate verification will be used. // Notice that we set tls.Config.InsecureSkipVerify to make sure the \u0026#34;VerifyPeerCertificate\u0026#34; is called, // so the ClientHandshakeVerify should verify the trusted ca if necessary. // If TLSConfig.InsecureSkip is true, the ClientHandshakeVerify will be ignored. ClientHandshakeVerify(cfg *tls.Config) func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error } ","excerpt":"本文基于的内容是 MOSN v0.12.0。\n概述 MOSN 提供了基于 TLS 加密的安全通信的能力,本文主要从三个方面介绍 MOSN 的 TLS 相关实现,包括:MOSN 作为服务端提供 TLS …","ref":"https://mosn.io/blog/code/mosn-tls/","title":"MOSN 源码解析 - TLS"},{"body":"本文基于的内容是 MOSN v0.12.0。\nMOSN 的路由能力目前仍然处于不断更新完善的阶段,详细的配置介绍可以参考 MOSN 配置文档中的路由部分。本文将主要从三个方面来介绍 MOSN 的路由功能:\n 针对路由模块的配置解析 路由模块是如何运行的 动态路由的实现 配置解析逻辑 路由的配置要在 MOSN 中生效,包含两个部分:\n proxy 的配置中,指定路由的名字router_config_name,说明 proxy 需要引用对应名字的路由。 路由的配置,包含路由的名字以及其他路由配置。 其中路由的配置可以配置在两个地方,一个是通过connection_manager进行配置,这个方式是历史遗留配置,作为兼容性保留的内容,不推荐使用这种方式;另一个是通过routers进行配置。如果两处配置包含同名的路由配置,会以routers中的配置为准。代码如下:\nfunc NewMosn(c *v2.MOSNConfig) *Mosn { ... // 省略 for _, serverConfig := range c.Servers { ... // 省略 for idx, _ := range serverConfig.Listeners { lc := configmanager.ParseListenerConfig(\u0026amp;serverConfig.Listeners[idx], inheritListeners) // deprecated: keep compatible for route config in listener\u0026#39;s connection_manager deprecatedRouter, err := configmanager.ParseRouterConfiguration(\u0026amp;lc.FilterChains[0]) if err != nil { log.StartLogger.Fatalf(\u0026#34;[mosn] [NewMosn] compatible router: %v\u0026#34;, err) } if deprecatedRouter.RouterConfigName != \u0026#34;\u0026#34; { m.routerManager.AddOrUpdateRouters(deprecatedRouter) } ... // 省略 } // Add Router Config for _, routerConfig := range serverConfig.Routers { if routerConfig.RouterConfigName != \u0026#34;\u0026#34; { m.routerManager.AddOrUpdateRouters(routerConfig) } } } ... // 省略 } 根据代码,可以看到在解析配置时,会先解析 Listener 中的路由配置(已废弃字段,兼容逻辑),随后单独解析 Routers 配置。配置解析完成以后,都是通过AddOrUpdateRouters使配置生效,所以后解析的 Routers 配置如果与 Listener 中的配置同名,则会通过 Update 覆盖掉 Listener 中的配置。由此我们也可以看到不同的 Listener、不同的 Proxy,如果引用了相同名字的路由,则对应的是同一份路由规则。\n路由模块运行 当前的路由能力,是与proxy进行绑定的。为了说明路由的逻辑,我们先来看一下在proxy 模式下的 MOSN,收到一个请求后,是如何处理的。\n首先在 MOSN 的 Listener 新建一个连接的时候,通过OnNewConnection创建NetworkFilters,而proxy就是其中一个,因此一个连接对应一个proxy。在proxy创建的时候会基于配置获取对应的路由信息routersWrapper,所以在连接创建以后,在此连接上的请求会使用哪个路由配置就已经决定了。\nfunc (al *activeListener) OnNewConnection(ctx context.Context, conn api.Connection) { filterManager := conn.FilterManager() for _, nfcf := range al.networkFiltersFactories { nfcf.CreateFilterChain(ctx, filterManager) } ... // 省略 } func NewProxy(ctx context.Context, config *v2.Proxy) Proxy { ... // 省略 if routersWrapper := router.GetRoutersMangerInstance().GetRouterWrapperByName(proxy.config.RouterConfigName); routersWrapper != nil { proxy.routersWrapper = routersWrapper } ... // 省略 } func NewProxy(ctx context.Context, config *v2.Proxy) Proxy { ... // 省略 if routersWrapper := router.GetRoutersMangerInstance().GetRouterWrapperByName(proxy.config.RouterConfigName); routersWrapper != nil { proxy.routersWrapper = routersWrapper } ... // 省略 } 在连接创建以后,开始处理请求。每次收到一个请求,经过协议解析以后,最终会走到proxy中downStream的OnReceive方法。在OnReceive中,经过一系列处理,可能会走到路由匹配阶段matchRoute。MOSN 通过routersWrapper获取到当前运行时的routers,随后执行路由匹配逻辑,完成路由匹配。\nfunc (s *downStream) matchRoute() { if s.proxy.routersWrapper == nil || s.proxy.routersWrapper.GetRouters() == nil { s.requestInfo.SetResponseFlag(api.NoRouteFound) s.sendHijackReply(types.RouterUnavailableCode, headers) return } routers := s.proxy.routersWrapper.GetRouters() ... // 省略 } 动态路由 MOSN 在路由模块中,设计了动态更新的接口,可以让 MOSN 在运行时动态更新路由的配置。经过前面的分析,我们可以看到 MOSN 配置解析时,也时通过动态更新的接口让路由配置生效的。除了配置解析以外,MOSN 默认可以通过 xDS 完成路由的动态更新,也可以通过扩展调用动态更新的接口完成动态路由配置更新。\n路由动态更新主要提供了三个接口。\nfunc (rm *routersManagerImpl) AddOrUpdateRouters(routerConfig *v2.RouterConfiguration) error {} func (rm *routersManagerImpl) AddRoute(routerConfigName, domain string, route *v2.Router) error {} func (rm *routersManagerImpl) RemoveAllRoutes(routerConfigName, domain string) error {} 默认情况下,都是使用AddOrUpdateRouters,使用一个完整的路由配置,对路由进行更新。AddRoute和RemoveAllRoutes是在特殊情况下,用于特定的路由配置追加 / 删除路由匹配规则使用的。\n动态路由更新还涉及到一个问题就是更新以后需要动态生效。从之前的路由运行逻辑分析中,我们知道 MOSN 在连接建立的时候,就通过GetRouterWrapperByName完成了路由选择,为了让路由配置更新对存量连接也可以生效,AddOrUpdateRouters的逻辑进行了针对性处理。\n// 省略了日志部分 func (rm *routersManagerImpl) AddOrUpdateRouters(routerConfig *v2.RouterConfiguration) error { if routerConfig == nil { return ErrNilRouterConfig } if v, ok := rm.routersWrapperMap.Load(routerConfig.RouterConfigName); ok { rw, ok := v.(*RoutersWrapper) if !ok { return ErrUnexpected } routers, err := NewRouters(routerConfig) if err != nil { return err } rw.mux.Lock() rw.routers = routers rw.routersConfig = routerConfig rw.mux.Unlock() } else { routers, _ := NewRouters(routerConfig) rm.routersWrapperMap.Store(routerConfig.RouterConfigName, \u0026amp;RoutersWrapper{ routers: routers, routersConfig: routerConfig, }) } // update admin stored config for admin api dump store.SetRouter(routerConfig.RouterConfigName, *routerConfig) } func (rm *routersManagerImpl) GetRouterWrapperByName(routerConfigName string) types.RouterWrapper { if v, ok := rm.routersWrapperMap.Load(routerConfigName); ok { rw, ok := v.(*RoutersWrapper) if !ok { return nil } return rw } } 当一个路由配置是新增的时候,MOSN 会忽略掉NewRouters的错误,即允许路由配置解析失败,此时的目的是为了让GetRouterWrapperByName能返回一个RoutersWrapper。但是如果是路由配置更新,则不会忽略NewRouters的错误,必须是有效的配置才能完成更新。 在之后的运行过程中,如果存在路由配置更新,更新的是RoutersWrapper中的routers,存量连接中的RoutersWrapper也可以获取到最新的routers,实现路由规则的动态生效。\n总结 ","excerpt":"本文基于的内容是 MOSN v0.12.0。\nMOSN 的路由能力目前仍然处于不断更新完善的阶段,详细的配置介绍可以参考 MOSN 配置文档中的路由部分。本文将主要从三个方面来介绍 MOSN 的路由功 …","ref":"https://mosn.io/blog/code/mosn-router/","title":"MOSN 源码解析 - 路由"},{"body":"MOSN 扩展机制解析\n","excerpt":"MOSN 扩展机制解析","ref":"https://mosn.io/docs/products/structure/extensions/","title":"MOSN 扩展机制解析"},{"body":"本文根据 SOFAChannel#14 直播分享整理,主题:云原生网络代理 MOSN 扩展机制解析。\n大家好,我是今天的讲师永鹏,来自蚂蚁集团,目前主要负责 MOSN 的开发,也是 MOSN 的Committer。今天我为大家分享的是云原生网络代理 MOSN 的扩展机制,希望通过这次分享以后,能让大家了解 MOSN 的可编程扩展能力,可以基于 MOSN 的扩展能力,按照自己实际的业务需求进行二次开发。\n前言 今天我们将从以下几个方面,对 MOSN 的扩展机制进行介绍:\n MOSN 扩展能力和扩展机制的详细介绍; 结合示例对 MOSN 的 Filter 扩展机制与插件扩展机制进行详细介绍; MOSN 后续扩展能力规划与展望; 欢迎大家有兴趣一起共建 MOSN。在本次演讲中涉及到的示例就在我们的 Github 的 examples/codes/mosn-extensions 目录下,大家有兴趣的也可以下载下来运行一下,关于这些示例我们还做了一些小活动,也希望大家可以踊跃参与。\nMOSN:https://github.com/mosn/mosn\nMOSN 简介 MOSN 作为云原生的网络代理,旨在为服务提供多协议、模块化、智能化、安全的代理能力。在实际生产使用中,不同的厂商会有不同的使用场景,通用的网络代理能力面对具体的业务场景会显得有些不足,通常都需要进行二次开发以满足业务需求。MOSN 在核心框架中,提供了一系列的扩展机制和扩展点,就是为了满足需要基于业务进行二次开发的场景,同时 MOSN 提供的部分通用逻辑也是基于扩展机制和扩展点的实现。\n比如通过 MOSN “内置实现”的透明劫持的能力,就是通过 MOSN Filter 机制实现。而要实现消息的代理,则可以通过类似的扩展实现。在通用代理的情况下,可以通过 Filter 机制实现业务的认证鉴权,也可以实现定制的负载均衡逻辑;除了转发流程可以扩展实现以外,MOSN 还可以扩展日志的实现,用于对标已有的日志系统,也可以扩展 XDS 实现定制的配置更新;根据不同的业务场景还会有很多具体的扩展情况,就不在此展开了,有兴趣的可以关注 MOSN 社区正在建设的源代码分析系列文章与文档。\nMOSN 作为一款网络代理,在转发链路上的网络层、协议层、转发层,在非转发链路上的配置、日志、Admin API 等都提供了扩展能力,对于协议扩展的部分,有兴趣的可以看一下上期直播讲的 MOSN 多协议机制解析,我们今天将重点介绍一下转发层的 Stream Filter 扩展机制与 MOSN 的插件机制。\nStream Filter 机制 在实际业务场景中,在转发请求之前或者回写响应之前,都可能需要对请求/响应做一些处理,如判断是否需要进行转发的认证/鉴权,是否需要限流,又或者需要对请求/响应做一些具有业务语义的记录,需要对协议进行转换等。这些场景都与具体的业务高度耦合,是一个典型的需要进行二次开发的情况。MOSN 的 Stream Filter 机制就是为了满足这样的扩展场景所设计的,它也成为目前 MOSN 扩展中使用频率最高的扩展点。\n在目前的内置 MOSN 实现中,Stream Filter 机制暂时与内置的 network filter: proxy 是绑定的,后面我们也考虑将这部分能力进行抽象,让其他 network filter 也可以复用这部分能力。\n关于 Stream Filter,今天会为大家讲解两个部分的内容:\n 一个 Stream Filter 包含哪些部分以及在 MOSN 中是如何工作的; 通过一个 Demo 演示来加深对 Stream Filter 的实现与应用; 一个完整的 Stream Filter 一个完整的 StreamFilter,包含三个部分的内容:\n 一个 StreamFilter 对象,存在于每一个请求/响应当中,在 MOSN 收到请求的时候发挥作用,我们称为 ReceiverFilter,在 MOSN 收到响应时发挥作用,我们称为 SenderFilter。一个 StreamFilter 可以是其中任意一种,也可以是两种都是; 一个 StreamFilterFactory 对象,用于 MOSN 在每次收到请求时,生成 StreamFilter 对象。在 Listener 配置解析时,一个 StreamFilter 的配置会生成一个其对于的 StreamFilterFactory。同一个 StreamFilter 在不同的 Listener 下可能对应不同的 StreamFilterFactory,但是也有的特殊情况下,StreamFilterFactory 可能需要实现为单例; 一个 CreateStreamFilterFactory 方法,配置解析时生成 StreamFilterFactory 就是调用它; Stream Filter 在 MOSN 中是如何工作的 接下来,我们看下 Stream Filter 在 MOSN 中是如何工作的。\n当 MOSN 经过协议解析,收到一个完整的请求时,会创建一个 Stream。此时收到请求的 Listener 中每存在 StreamFilterFactory,就会生成一个 StreamFilter 对象,随后进入到 proxy 流程。\n进入 proxy 流程以后,如果存在 ReceiverFilter,那么就会执行对应的逻辑,ReceiverFilter 包括两个阶段,“路由前”和“路由后”,在每个 Filter 处理完成以后,会返回一个状态,如果是 Stop 则会中止后续尚未执行的 ReceiverFilter,通常情况下,返回 Stop 状态的 Filter 都会回写一个响应。如果是 Continue 则会执行下一个 ReceiverFilter,直到本阶段的 ReceiverFilter 都执行完成或中止;路由前阶段的 ReceiverFIlter 执行完成后,就会执行路由后阶段,其逻辑和路由前一致。如果是正常转发,那么随后 MOSN 会收到一个响应或者发现其他异常直接回写一个响应,此时就会进入到 SenderFilter 的流程中,完成 SenderFilter 的处理。SenderFilter 处理完成以后,MOSN 会写响应给 Client,并且完成最后的收尾工作,收尾工作包括一些数据的回收、日志的记录,以及 StreamFilter 的“销毁”(调用 OnDestroy)。\nStream Filter Demo 对 StreamFilter 有了一个基本的认识以后,我们来看一个实际的 Demo 代码来看下如何实现一个 StreamFilter 并且让它在 MOSN 中发挥作用。\n按照刚才我们的介绍,一个 Stream FIlter 要包含三部分:Filter、Factory、CreateFactory。\n 首先我们实现一个 Filter,其逻辑是模拟一个鉴权的 Filter:只有请求的 Header 中包含所配置的 Key-Value 时,MOSN 才会对请求做继续转发,否则直接返回 403 错误; 然后我们实现一个 Factory,它负责生成我们实现的 Filter,并且说明 Filter 应该发挥作用的阶段(在请求阶段、路由匹配之前); 最后我们定义了一个生成 DemoFactory 的函数 CreateDemoFactory,并且通过 init 将其“注册”,注册完成以后,MOSN 配置解析就可以识别这个 StreamFilter; 完成实现以后,我们就可以通过具体的配置来实现对应的功能了。在示例的配置中,配置 StreamFilter 为我们刚才实现的 Filter,只转发 Header 中包含 user:admin 的请求。示例配置中监听的端口是 2046,转发的后端 server 端口是 8080。在演示之前,我已经完成了 8080 server 的启动,这个 server 会对收到的任意请求返回 200 。我们来看一下 MOSN 转发情况。Demo 操作可以在文末直播的视频回顾中查看。\n Stream Filter Demo: https://github.com/mosn/mosn/tree/master/examples/codes/mosn-extensions/simple_streamfilter Demo Readme:https://github.com/mosn/mosn/tree/master/examples/cn_readme/mosn-extensions MOSN Plugin 机制 下面我们来了解一下 MOSN 的 Plugin 机制。\n刚才我们对 Stream Filter 有了一个了解,MOSN 中其余的扩展实现也是类似的方法,思路就是编码实现 MOSN 扩展点所需要的接口然后利用 MOSN 的框架运行扩展的实现。\n但是这里会发现一个问题,就是有时候我们需要的扩展能力已经有现成可用的实现了,那么我们是否可以做简单的改造就让 MOSN 可以获取对应的能力,哪怕目前可用的实现不是 Go 语言的实现,比如现成的限流能力的实现、注入能力的实现等;又或者对于某些特定的能力,它需要有更严格的控制,更高的标准,比如安全相关的能力。\n类似这样的场景,我们引入了 MOSN 的 Plugin 机制,它支持我们可以对 MOSN 需要的能力进行独立开发或者我们对现有的程序进行适当的改造以后,就可以将它们引入到 MOSN 当中来。\nMOSN 的 Plugin 机制包含了两部分内容,一是 MOSN 自定义的 Plugin 框架,它支持通过在 MOSN 中实现 agent 与一个独立的进程进行交互来完成 MOSN 扩展能力的实现。二是基于 Golang 的 Plugin 框架,通过动态库(SO)加载的方式,实现 MOSN 的扩展。其中动态库加载的方式目前还存在一些局限性,还处于 beta 阶段。\n我们先来看一下多进程 Plugin 框架。\n多进程 Plugin 框架 MOSN 的 Plugin 框架是 MOSN 封装的一个可以让 MOSN 通过 gRPC 和独立进程进行交互的方式,它包含两部分:\n 独立的进程通过 MOSN Plugin 框架管理,作为 MOSN 的子进程;MOSN 的 Plugin 框架可以管理它们,如启动、关闭等; 通过在 MOSN 中实现的 agent,使用 gRPC 的方式和子进程进行交互,gRPC 可以是基于 tcp 的,也可以是基于 domain socket 的; 基于这个框架,我们只需要开发或者进行一些改造,让程序满足 MOSN 框架的规范,就可以作为 MOSN 多进程插件的一部分。\n首先我们需要提供一个 gRPC 的服务,并且满足 MOSN 框架下的 proto 定义。当 gRPC server 启动完成以后,向标准输出(stdout)输出一段约定的字符串,作为 MOSN 和子进程之间的握手协议。MOSN 中的对应 agent 会通过握手协议完成与子进程之间的连接建立。握手协议的字符串包含5个字段,每个字段之间用\u0026quot;|\u0026ldquo;分割,其中带$符号的是根据实际进程情况需要填写的值,其余的是当前约定的固定字段。network 支持 tcp/unix,代表通过 tcp 方式还是 unix domain socket 的方式进行通信,addr 表示 gRPC server 监听的地址。\nMOSN 提供了 go 语言的子进程 server 封装,在 go 语言场景下,作为子进程的程序只需要实现一个 MOSN 框架下的 plugin.Service 接口,并且通过 plugin.Serve 方法启动即可。\n通过 Plugin 框架,让 MOSN 做到在扩展功能实现的时候,支持隔离性、支持异构语言扩展能力、支持模块化,以及具备进程管理的能力。\n对于 MOSN 通过多进程方式完成扩展,今天准备了两个示例和大家进行分享。一个是基于 MOSN 的 TLS 扩展,模拟了通过一个安全等级比较高的证书管理程序来获取 TLS 配置证书、私钥等敏感信息的能力;第二个是将之前演示的 Stream Filter 修改为了“子进程”,模拟“如何将现成的能力”引入 MOSN。\n基于 MOSN 的 TLS 扩展示例\n首先来看 TLS 的扩展,示例包含两部分内容:\n 独立的子进程,用 Go 语言实现,实现了 plugin.Service 接口,并通过 plugin.Serve 方法启动; MOSN 扩展点实现交互 agent。在这里就不详细展开TLS扩展点的细节了,只关注交互过程:通过 Call 方法发送 gRPC 请求,获取响应,完成相关逻辑; load cert demo: https://github.com/mosn/mosn/tree/master/examples/codes/mosn-extensions/plugin/cert_loader Demo Readme:https://github.com/mosn/mosn/tree/master/examples/cn_readme/mosn-extensions\n下面我们来看一下效果,首先配置依然是监听 2046 的端口,配置了扩展的 TLS 配置,就需要 HTTPS 才可以访问 MOSN。\nStream Filter 作为 agent 示例\n下面我们来看下 Stream Filter 作为 agent,与多进程之间的示例,模拟“如何将现成的能力”引入 MOSN。在示例中我们把之前的“鉴权”认为是一个“现成的”能力。\n独立进程中实现和之前一样的“鉴权”能力,其配置来自进程的启动参数。Stream Filter 作为 agent 实现,其中“校验”逻辑修改为和子进程交互,在生成 Factory 时完成子进程的启动和配置设置。\n这个示例运行以后和之前 Stream Filter 的效果是一样的。\n Stream Filter Plugin demo: https://github.com/mosn/mosn/tree/master/examples/codes/mosn-extensions/plugin/filter Demo Readme:https://github.com/mosn/mosn/tree/master/examples/cn_readme/mosn-extensions 动态库(SO)扩展机制 在目前的多进程框架中,虽然扩展能力可以通过一个独立的子程序实现,但是仍然需要在 MOSN 中实现一个 agent 用于交互,依然需要在MOSN中编写一部分代码;而我们希望引入动态库(SO)加载的机制,实现在不重新编译 MOSN 的情况下,通过加载不同的 SO,做到不同的扩展能力。\n与子程序模式相比,SO 虽然也是一个独立的二进制,但是最终启动的时候,不会有额外的子进程存在,其生命周期可以和 MOSN 完全保持一致,而且动态库机制还有一个优势:它可以让扩展代码和 MOSN 完全解耦合。\n但是,目前使用动态库加载的方式还存在一些限制,因此 MOSN 对于这个能力也还处于 Beta 阶段,并没有投入实际使用,需要完善。相关的原因包括:\n 部分 MOSN 扩展的实现需要用到 MOSN 中的一些定义,因此在动态库实现时不能完全做到解耦合。 为了解决这个问题,MOSN 将一些基础库(如日志、buffer 等),一些 API 定义从 MOSN 的核心仓库中独立出来,这样扩展实现和 MOSN 核心都引用这些“独立”的库,减少扩展对 MOSN 核心代码的依赖。\n如果某一个扩展点要支持完全解耦合的动态库扩展,那么对应的扩展点都需要进行支持动态库加载的改造,包括配置模型与实现。\n MOSN 动态库加载的方式,其实是基于 Go 语言的 plugin 包实现的,它可以加载用 Go 语言编译的动态库。但是对于动态库的编译环境存在一些限制,编译它时必须和 MOSN 编译时的 GOPATH 保持一致;同时引用的代码路径都需要保持一致,如果存在 vendor 目录,那么意味着编译动态库时的项目路径也得和 MOSN 核心保持一致。 为了解决这个问题,我们考虑使用 Docker 编译,在编译时统一 GOPATH,强制修改代码目录结构,屏蔽掉 Vendor 目录差异的方式来解决,这种方式目前仍然在验证中。\n因此理论上 MOSN 目前所有的扩展点都可以使用 Go 语言原生机制通过加载 SO 的方式来实现,而目前 MOSN 最适合实现这个能力的一个扩展点就是 Stream Filter。\n我们只需要实现一个通用的、可以加载 SO 的 Filter,然后在具体的 SO 中实现真正的 StreamFilter 逻辑,由于 StreamFilter 实现所需要的接口定义都在 mosn.io/api 中,所以 SO 可以做到和 MOSN 核心框架解耦合。\n关键点就是这个通用 Filter 的设计和实现,我们也通过 Demo 来看一下。\n通用 Filter 的设计和实现\n这个通用的 Filter 和普通的 StreamFilter 不同,它只包含一个要素:CreateFactory。思路是通过通用的 CreateFactory,加载 SO 中的 CreateFactory 并执行,让 SO 中的 Factory 发挥作用。\n通用 CreateFactory 包括:\n 配置解析,解析出两部分内容:一是需要加载的 SO 路径,二是 SO 中对应 Filter 所需要的配置; SO 路径就代表了 SO 中 Filter 的“注册”,以及本次会选择这个 Filter; 加载 SO,基于其中约定好的函数名,获取真正的 CreateFactory 函数; 调用真正的 CreateFactory 函数,实现 SO 中 StreamFilter 的加载; 由此,我们可以看到,SO 中的 StreamFIlter 也和普通的 FIlter 有些区别:\n 生成 StreamFilterChainFactory 的函数必须是固定的名字;\n 不再需要 init “注册”该函数;\n Stream Filter SO Demo: https://github.com/mosn/mosn/tree/master/examples/codes/mosn-extensions/plugin/so\n Demo Readme:https://github.com/mosn/mosn/tree/master/examples/cn_readme/mosn-extensions\n 下面我们来看一下这个 Demo 的效果。本次 Demo 中的 Filter 实现依然是之前的“鉴权”示例。经过验证,我们发现这个思路是可行的,但是离生产实践还需要完善更多的细节。\n代码扩展活动 经过这些演示,相信大家对 MOSN 的扩展能力也有所了解了,这里我们来做一个代码扩展活动,希望大家可以踊跃参与。完成活动任务,提交相关代码 PR 到 MOSN 的仓库,我们会进行 CodeReview 和验证,第一个验证通过的代码将合并到 MOSN 的 example 中,并且对提交的同学送上一份奖励;对于前3名提交、同样结果正确并且是原创的,虽然我们不能合并对应的代码,但是我们也将送上奖励。\n活动任务共有五个:\n 多进程 Demo 中证书加载的独立进程,使用 python 或者 java 实现以后,demo 运行演示成功。任意一种语言就算完成一个任务。 examples/codes/mosn-extensions/plugin/cert_loader/python/ examples/codes/mosn-extensions/plugin/cert_loader/java/ 多进程 Demo 中 stream filter 的独立进程,使用 python 或者 java 实现以后,demo 运行演示成功。任意一种语言就算完成一个任务。 examples/codes/mosn-extensions/plugin/filter/python/ examples/codes/mosn-extensions/plugin/filter/java/ SO 动态加载 Demo 中,SO 里实现的 Stream Filter 结合多进程框架(GO 语言)实现,Demo 运行演示成功。 examples/codes/mosn-extensions/plugin/so/subprocess/ 跨语言相关的实现可以参考以下示例:\nhttps://github.com/mosn/mosn/tree/master/examples/codes/plugin/across-languages/server/\n规划与展望 最后向大家介绍一下 MOSN 后续扩展能力的规划,也希望大家有需求的可以向我们反馈,有兴趣的一起参与到 MOSN 的建设中来。首先就是要完善 SO 动态库加载机制,让 MOSN 支持 SO 方式加载扩展;然后就是针对 LUA 的脚本扩展以及支持 WASM 的扩展能力;最后 MOSN 还会增加更多的扩展点,以满足更多更复杂的场景。非常欢迎大家参与到 MOSN 社区的共建中。\n MOSN:https://github.com/mosn/mosn MOSN 官网:https://mosn.io/ 以上就是本期分享的全部内容,如果大家对 MOSN 有问题以及建议,欢迎在群内与我们交流。\n本期直播视频回顾以及 PPT 查看地址见:https://tech.antfin.com/community/live/1152。\n","excerpt":"本文根据 SOFAChannel#14 直播分享整理,主题:云原生网络代理 MOSN 扩展机制解析。\n大家好,我是今天的讲师永鹏,来自蚂蚁集团,目前主要负责 MOSN 的开发,也是 MOSN …","ref":"https://mosn.io/blog/posts/mosn-extensions/","title":"MOSN 扩展机制解析"},{"body":"本文的内容基于 MOSN v0.10.0。\n在连接管理中我们主要介绍 MOSN 实现连接池的功能,连接池是上下游 MOSN 之间进行长连接复用以提高转发效率与降低时延的关键,MOSN 连接池提供基于 HTTP1, HTTP2, SOFARPC, XProtocol 协议的连接池。\n而“健康检查”是一种实时检测上游服务器是否正确提供服务的机制,一般分为“主动健康检查”和“被动健康检查”。主动健康检查由健康检查模块主动发起,通过周期性的发送健康检查数据包对服务器进行健康检查;被动健康检查则是在转发业务数据报文时发现上游服务器无法正确的提供服务,而被动摘除的手段。\nMOSN 当前只支持主动健康检查,支持的协议包括 SOFARPC 以及 HTTP2,它是 Cluster 的一个模块,可通过配置决定是否开启对 Cluster 中的 Host 进行健康检查,以及配置健康检查时使用的参数。\n通过主动健康检查,MOSN 可以及时摘除不健康的机器,从而为连接管理提供有效的 Upstream,提高建连的成功率。同时基于健康检查模块还可以做连接的保活,从而维持长连接。\nMOSN 连接管理 HTTP/1.0 默认使用短连接,即客户端和服务器每进行一次 HTTP 操作建立一次连接,任务结束中断连接。HTTP/1.1 起默认使用长连接用以保持连接特性。使用长连接的 HTTP 协议需在响应头添加 Connection:keep-alive。HTTP/1.0 虽然能够维持长连接,但是单条连接同一时间只能处理一个请求/响应,意味着如果同时收到4个请求需要建立4条 TCP 连接,连接的成本相对来说比较高昂;HTTP/2 引入 Stream/Frame 的概念,支持分帧多路复用能力,在逻辑上区分出成对的请求 Stream 和响应 Stream,从而单条连接并发处理多个请求/响应,解决 HTTP/1.0 连接数与并发数成正比的问题。\nHTTP/1.1 不支持多路复用,使用经典的 Ping-Pong 模式:在请求发送之后必须独占当前连接等待服务器端给出这个请求的应答然后才能释放连接。因此 HTTP/1.1 下并发多个请求就必须采用多连接,为了提升性能通常使用长连接+连接池的设计。MOSN 长连接多路复用处理流程:\n MOSN 从 downstream 接收一个请求 request,依据报文扩展多路复用接口 GetRequestId 获取到请求在这条连接上的身份标识并且记录到关联映射中待用; 请求经过 MOSN 的路由、负载均衡处理,选择一个 upstream,同时在这条连接上新建一个请求流,并调用文扩展多路复用接口 SetRequestId 封装新的身份标识,并记录到关联映射中与 downstream 信息组合; MOSN 从 upstream 接收一个响应 response,依据报文扩展多路复用接口 GetRequestId 获取到请求在这条连接上的身份标识。此时可以从上下游关联映射表中,根据 upstream 信息找到对应的 downstream 信息; 依据 downstream request 的信息,调用文扩展多路复用接口 SetRequestId 设置响应的 requestId,并回复给 downstream。 MOSN 在 Proxy 下游 Request 时,会根据 Upstream Host 地址以及与 Upstream Host 的应用层协议获取对应的长连接,之后在长连接上封装对应协议的 Stream, 并转发数据,从而避免每次与 Upstream 新建连接带来的握手开销以提高转发性能、降低时延。在创建 Stream 的时候,连接池还提供熔断保护功能,将 Stream 创建的数量约束在一个阈值以下。当前 MOSN 支持的连接池包括:SOFARPC、HTTP1、HTTP2、XProocol 等应用层协议的连接池.\n MOSN 的 Proxy 模块在 Downstream 收到 Request 的时候,在经过路由、负载均衡等模块处理获取到 Upstream Host 以及对应的转发协议时,会去 Cluster Manager 获取连接池 ,如果连接池不存在则创建并加入缓存,之后在长连接上创建 Stream,并发送数据。 如下图所示为连接池工作的示意图:\n接口描述 连接池接口\n Protocol() 用于返回当前连接池对应的协议; NewStream() 用于在当前连接池上创建 Stream; Close() 用于关闭长连接中的 TCP 套接字。 连接池事件监听的接口\n OnFailure 用于创建 Stream 失败时的回调,用于 reset stream 相关工作; OnReady 是连接池准备就绪,Stream 创建成功时的回调,用于 Stream 发送数据。 数据结构 ConnNewPoolFactories 为不同协议创建对应连接池的工厂类,HTTP1、SOFARPC、 HTTP2、XProtocol 等协议分别调用注册函数 RegisterNewPoolFactory ,注册自己的创建方法。 类 clusterManager 中的 protocolConnPool 为全局连接池的实现,其类型为二级 sync.Map,其中第一级 Map 的 key 为协议类型,第二级 Map 的 key 为 Host 地址,value 为 ConnectionPool。 类 upstreamRequest 中维护 connPool 是 connPool 的使用者,用来创建连接。 连接池使用 初始化连接池 clusterManager 在查询本地连接池时,如果对应协议和 Host 的连接池不存在,则调用对应协议的工厂方法创建连接池,如果存在的话,则直接返回连接池。\n获取连接池 downstream 在收到下游的 request 并往上游转发时,会根据协议和挑选的 upstream 地址选择连接池并赋值给 upstream 的 connPool。\n使用连接池 upstreamRequest 在往上游转发数据时,会调用 connPool 对应的 Newstream 方法封装 stream 并发送。\nMOSN 健康检查 MOSN 的健康检查是一种主动健康检查机制,他是 Cluster 内在的一个模块。当前支持的健康检查功能包括:\n 健康检查支持可配置,配置包括健康检查使用的协议、健康检查报文发送间隔、超时时间、更新阈值等; 提供健康检查接口,允许不同健康检查协议的扩展; 提供基于健康检查结果的 callback 机制,允许自定义对健康检查结果的处理方式; 基于周期性的健康检查,可实现心跳机制,用于长连接的保活。 如下为当前健康检查工作的示意图:\nCluster 的 HealthChecker 模块在开启的情况下,会根据配置的协议创建基于 SOFARPC, HTTP2 或者其他协议的健康检查;健康检查模块会与 Cluster 中的所有 Host - 创建 HealthCheckSession,在 Session 上周期性发送健康检查报文,并开启定时器,如果 Response 在规定时间内未达/到达则判断健康检查失败/成功,健康检查失败/成功次数到一定阈值后 Callback 模块会更新机器的健康状态。\n健康检查配置 对 Cluster 的健康检查支持的配置包括:\n 发起健康检查的应用层协议,如果子协议存在,还支持配子协议 健康检查报文的超时时间 发起健康检查报文的间隔 更新 Host 状态的阈值 健康检查的路径和对应的 Service 信息 接口描述 HealthChecker 接口用来控制对整个 Cluster 进行健康检查,其中:\n Start/Stop 开启/关闭 对 Cluster 的健康检查,会开启 Host 的健康检查 Session; AddHostCheckCompleteCb 用来设置对健康检查结果的回调,关于 cb 的定义在下面会介绍; OnClusterMemberUpdate 是在外部 Cluster 的 Hosts 发生变化时,例如主机上下线等,来更新健康检查的范围。 HealthCheckCb 是基于健康检查结果对 Host 状态进行更新的回调函数,发生在健康检查的结果对 Host 的状态产生影响之后,如果健康检查失败,则会将 Host 置为 Unhealty,否则置为 Healthy。\nHealthCheckSession 是健康检查模块与每个 Host 建立的用于健康检查的 session,在这个 session 上可以发送健康检查数据包并处理 response。\n Start/Stop 开启/停止 对 Host 的健康检查; SetUnhealthy 为健康检查失败达到一定阈值之后将 Host 的状态设置为 Unhealthy。 数据结构 Cluster 中的 healthChecker 在 Cluster 创建的时候基于配置进行初始化,如果配置支持健康检查,则开启。 healthChecker 是实现 Cluster 健康检查的类,其中包含对所有的 Host 的进行健康检查的 healthCheckSessions。在实现基于不同的协议的健康检查时,会继承这个类,并重写一些方法。 healthCheckSesion 是对 Host 健康检查的 session,其中包含两个定时器,以及对应的健康状态更新阈值。在实现基于不同的协议的健康检查时,会继承这个类,并重写一些方法。 工作流程 下图是健康检查的工作时序图,包括 从Cluster 创建健康检查器开始,到对每个 Host 创建健康检查的 Session,到对 Host 发送健康检查数据包,到处理 Host 的响应,到更新 Host 的状态。\n总结 本文根据 MOSN 的源码分析 MOSN 对连接池的设计与实现,其基于 upstreamRequest 往上游转发数据调用 connPool 对应的 Newstream 方法发送 Stream。\n","excerpt":"本文的内容基于 MOSN v0.10.0。\n在连接管理中我们主要介绍 MOSN 实现连接池的功能,连接池是上下游 MOSN 之间进行长连接复用以提高转发效率与降低时延的关键,MOSN …","ref":"https://mosn.io/blog/code/mosn-connection-pool/","title":"MOSN 源码分析 - 连接池"},{"body":"我们很高兴的宣布 MOSN v0.11.0 发布。以下是该版本的变更日志。\n新功能 支持 Listener Filter 的扩展,透明劫持能力基于 Listener Filter 实现 @wangfakang 变量机制新增 Set 方法 @neverhook 新增 SDS Client 失败时自动重试和异常处理 @pxzero 完善 TraceLog,支持注入 context @taoyuanyuan 新增 FeatureGate auto_config,当开启该Feature以后动态更新的配置会保存到启动配置中 @nejisama 重构 重构 XProtocol Engine,并且重新实现了 SofaRPC 协议 @neverhook 移除了 SofaRpc Healthcheck filter,改为 xprotocol 内建的 heartbeat 实现 移除了 SofaRpc 协议原本的协议转换 (protocol conv) 支持,新增了基于 stream filter 的的协议转换扩展实现能力 xprotocol 新增 idle free 和 keepalive 协议解析优化 修改 HTTP2 协议的 Encode 方法参数 @taoyuanyuan 精简了 LDS 接口参数 @nejisama 修改了路由配置模型,废弃了connection_manager@nejisama 优化 优化 Upstream 动态解析域名机制 @wangfakang 优化 TLS 封装,新增了错误日志,修改了兼容模式下的超时时间 @nejisama 优化超时时间设置,使用变量机制设置超时时间 @neverhook Dubbo 解析库依赖升级到 1.5.0 @cch123 引用路径迁移脚本新增 OS 自适应 @taomaree Bug 修复 修复 HTTP2 协议转发时丢失 query string 的问题 @champly ","excerpt":"我们很高兴的宣布 MOSN v0.11.0 发布。以下是该版本的变更日志。\n新功能 支持 Listener Filter 的扩展,透明劫持能力基于 Listener Filter …","ref":"https://mosn.io/docs/products/report/releases/v0.11.0/","title":"MOSN v0.11.0 发布"},{"body":" 本文根据 SOFAChannel#13 直播分享整理,主题:云原生网络代理 MOSN 多协议机制解析,查看视频回顾。\n 作者:无钩,目前主要从事蚂蚁集团网络代理相关的研发工作,也是 MOSN 的 Committer。\n今天我要和大家分享的是《云原生网络代理 MOSN 多协议机制解析》,并介绍对应的私有协议快速接入实践案例以及对 MOSN 实现多协议低成本接入的设计进行解读。\n我们将按以下顺序进行介绍:\n 多协议机制产生的背景与实践痛点; 常见的协议扩展思路初探; SOFABolt 协议接入实践;(重点) MOSN 多协议机制设计解读;(重点) 后续规划及展望; 其中第三点「接入实践」是今天分享的重点,希望能给大家就「如何在 MOSN 中快速扩展私有协议接入」有一个具体的感受。另外「MOSN 如何实现多协议框架」也是很多人关心和问题,我们将摘选几个技术功能,对其背后的设计思考进行解读。\nMOSN 简介 云原生网络代理 MOSN 定位是一个全栈的网络代理,支持包括网络接入层(Ingress)、API Gateway、Service Mesh 等场景,目前在蚂蚁集团内部的核心业务集群已经实现全面落地,并经受了 2019 年双十一大促的考验。今天要向大家介绍的是云原生网络代理 MOSN 核心特性之一的多协议扩展机制,目前已经支持了包括 SOFABolt、Dubbo、TARS 等多个协议的快速接入。\nMOSN:https://github.com/mosn/mosn\n多协议机制产生的背景与实践痛点 首先介绍一下多协议机制产生的背景。\n前面提到,蚂蚁集团 2019 年双十一核心链路百分之百 Mesh 化,是业界当时已知的最大规模的 Service Mesh 落地,为什么我们敢这么做?因为我们具备能够让架构平滑迁移的方案。\u0026ldquo;兼容性\u0026quot;是任何架构演进升级都必然要面对的一个问题,这在早已实践微服务化架构的蚂蚁集团内部同样如此。为了实现架构的平滑迁移,需要让新老节点的外在行为尽可能的表现一致,从而让依赖方无感知,这其中很重要的一点就是保持协议兼容性。\n因此,我们需要在 Service Mesh 架构下,兼容现有微服务体系中的通信协议——也就是说需要在 MOSN 内实现对目前蚂蚁集团内部通信协议的扩展支持。\n基于 MOSN 本身的扩展机制,我们完成了最初版本的协议扩展接入。但是在实践过程中,我们发现这并不是一件容易的事情:\n 相比编解码,协议自身的处理以及与框架集成才是其中最困难的环节,需要理解并实现包括请求生命周期、多路复用处理、链接池等等机制; 社区主流的 xDS 路由配置是面向 HTTP 协议的,无法直接支持私有协议,存在适配成本; 基于这些实践痛点,我们设计了 MOSN 多协议框架,希望可以降低私有协议的接入成本,加快普及 ServiceMesh 架构的落地推进。\n常见的协议扩展思路初探 前面介绍了背景,那么具体协议扩展框架要怎么设计呢?我们先来看一下业界的思路与做法。\n协议扩展框架 - Envoy 注:图片来自 Envoy 分享资料\n第一个要介绍的是目前发展势头强劲的 Envoy。从图上可以看出,Envoy 支持四层的读写过滤器扩展、基于 HTTP 的七层读写过滤器扩展以及对应的 Router/Upstream 实现。如果想要基于 Envoy 的扩展框架实现 L7 协议接入,目前的普遍做法是基于 L4 filter 封装相应的 L7 codec,在此基础之上再实现对应的协议路由等能力,无法复用 HTTP L7 的扩展框架。\n协议扩展框架 - Nginx 第二个则是老牌的反向代理软件 Nginx,其核心模块是基于 Epoll/Kqueue 等 I/O 多路复用技术之上的离散事件框架,基于事件框架之上构建了 Mail、Http 等协议模块。与 Envoy 类似,如果要基于 Nginx 扩展私有协议,那么也需要自行对接事件框架,并完整实现包括编解码、协议处理等能力。\n协议扩展框架 - MOSN 最后回过头来,我们看一下 MOSN 是怎么做的。实际上,MOSN 的底层机制与 Envoy、Nginx 并没有核心差异,同样支持基于 I/O 多路复用的 L4 读写过滤器扩展,并在此基础之上再封装 L7 的处理。但是与前两者不同的是,MOSN 针对典型的微服务通信场景,抽象出了一套适用于基于多路复用 RPC 协议的扩展框架,屏蔽了 MOSN 内部复杂的协议处理及框架流程,开发者只需要关注协议本身,并实现对应的框架接口能力即可实现快速接入扩展。\n三种框架成本对比 最后对比一下,典型微服务通信框架协议接入的成本,由于 MOSN 针对此类场景进行了框架层面的封装支持,因此可以节省开发者大量的研发成本。\nSOFABolt 协议接入实践 初步了解多协议框架的设计思路之后,让我们以 SOFABolt 协议为例来实际体验一下协议接入的过程。\nSOFABolt 简介 这里先对 SOFABolt 进行一个简单介绍,SOFABolt 是一个开源的轻量、易用、高性能、易扩展的 RPC 通信框架,广泛应用于蚂蚁集团内部。\nSOFABolt:https://github.com/sofastack/sofa-bolt\n基于 MOSN 的多协议框架,实际编写了 7 个代码文件,一共 925 行代码(包括 liscence、comment 在内)就完成了接入。如果对于协议本身较为熟悉,且具备一定的 MOSN/Golang 开发经验,甚至可以在一天内就完成整个协议的扩展,可以说接入成本是非常之低。\nGithub: https://github.com/mosn/mosn/tree/master/pkg/protocol/xprotocol/bolt\n下面让我们进入正题,一步一步了解接入过程。\nStep1:确认协议格式 第一步,需要确认要接入的协议格式。为什么首先要做这个,因为协议格式是一个协议最基本的部分,有以下两个层面的考虑:\n 任何协议特性以及协议功能都能在上面得到一些体现,例如有无 requestId/streamId 就直接关联到协议是否支持连接多路复用; 协议格式与报文模型直接相关,两者可以构成逻辑上的映射关系;而这个映射关系也就是所谓的编解码逻辑; 以 SOFABolt 为例,其第一个字节是协议 magic,可以用于校验当前报文是否属于 SOFABolt 协议,并可以用于协议自动识别匹配的场景;第二个字节是 type,用于标识当前报文的传输类型,可以是 Request / RequestOneway / Response 中的一种;第三个字节则是当前报文的业务类型,可以是心跳帧,RPC 请求/响应等类型。后面的字段就不一一介绍了,可以发现,**理解了协议格式本身,其实对于协议的特性支持和模型编解码就理解了一大半,**因此第一步协议格式的确认了解是重中之重,是后续一切工作开展的前提。\nStep2:确认报文模型 顺应第一步,第二步的主要工作是确认报文编程模型。一般地,在第一步完成之后,应当可以很顺利的构建出相应的报文模型,SOFABolt 例子中可以看出,模型字段设计基本与协议格式中的 header / payload 两部分相对应。有了编程模型之后,就可以继续进行下一步——基于模型实现对应的框架扩展了。\nStep3:接口实现 - 协议 协议扩展,顾名思义,是指协议层面的扩展,描述的是协议自身的行为(区别于报文自身)。\n目前多协议框架提供的接口包括以下五个:\n Name:协议名称,需要具备唯一性; Encoder:编码器,用于实现从报文模型到协议传输字节流的映射转换; Decoder:解码器,用于实现从协议传输字节流到报文模型的映射转换; Heartbeater:心跳处理,用于实现心跳保活报文的构造,包括探测发起与回复两个场景; Hijacker:错误劫持,用于在特定错误场景下错误报文的构造; Step4:接口实现 - 报文 前面介绍了协议扩展,接下里则是报文扩展,这里关注的是单个请求报文需要实现的行为。\n目前框架抽象的接口包括以下几个:\n Basic:需要提供 GetStreamType、GetHeader、GetBody 几个基础方法,分别对应传输类型、头部信息、载荷信息; Multiplexing:多路复用能力,需要实现 GetRequestId 及 SetRequestId; HeartbeatPredicate:用于判断当前报文是否为心跳帧; GoAwayPredicate:用于判断当前报文是否为优雅退出帧; ServiceAware:用于从报文中获取 service、method 等服务信息; 举个例子 这里举一个例子,来让大家对框架如何基于接口封装处理流程有一个体感:服务端心跳处理场景。当框架收到一个报文之后:\n 根据报文扩展中的 GetStreamType 来确定当前报文是请求还是响应。如果是请求则继续 2; 根据报文扩展中的 HeartbeatPredicate 来判断当前报文是否为心跳包,如果是则继续 3; 当前报文是心跳探测(request + heartbeat),需要回复心跳响应,此时根据协议扩展中的 Heartbeater.Reply 方法构造对应的心跳响应报文; 再根据协议扩展的 Encoder 实现,将心跳响应报文转换为传输字节流; 最后调用 MOSN 网络层接口,将传输字节流回复给发起心跳探测的客户端; 当协议扩展与报文扩展都实现之后,MOSN 协议扩展接入也就完成了,框架可以依据协议扩展的实现来完成协议的处理,让我们实际演示一下 SOFABolt 接入的 example。\nDemo 地址:https://github.com/mosn/mosn/tree/master/examples/codes/sofarpc-with-xprotocol-sample\nMOSN 多协议机制设计解读 通过 SOFABolt 协议接入的实践过程,大家对如何基于 MOSN 来做协议扩展应该有了一个初步的认知。那么 MOSN 多协议机制究竟封装了哪些逻辑,背后又是如何思考设计的?接下来将会挑选几个典型技术案例为大家进行解读。\n协议扩展框架 协议扩展框架 - 编解码\n最先介绍的是编解码机制,这个在前面 SOFABolt 接入实践中已经简单介绍过,MOSN 定义了编码器及解码器接口来屏蔽不同协议的编解码细节。协议接入时只需要实现编解码接口,而不用关心相应的接口调用上下文。\n协议扩展框架 - 多路复用\n接下来是多路复用机制的解读,这也是流程中相对不太好理解的一部分。首先明确一下链接多路复用的定义:允许在单条链接上,并发处理多个请求/响应。那么支持多路复用有什么好处呢?\n以 HTTP 协议演进为例,HTTP/1 虽然可以维持长连接,但是单条链接同一时间只能处理一个请求/相应,这意味着如果同时收到了 4 个请求,那么需要建立四条 TCP 链接,而建链的成本相对来说比较高昂;HTTP/2 引入了 stream/frame 的概念,支持了分帧多路复用能力,在逻辑上可以区分出成对的请求 stream 和响应 stream,从而可以在单条链接上并发处理多个请求/响应,解决了 HTTP/1 链接数与并发数成正比的问题。\n类似的,典型的微服务框架通信协议,如 Dubbo、SOFABolt 等一般也都实现了链接多路复用能力,因此 MOSN 封装了相应的多路复用处理流程,来简化协议接入的成本。让我们跟随一个请求代理的过程,来进一步了解。\n MOSN 从 downstream(conn=2) 接收了一个请求 request,依据报文扩展多路复用接口 GetRequestId 获取到请求在这条连接上的身份标识(requestId=1),并记录到关联映射中待用; 请求经过 MOSN 的路由、负载均衡处理,选择了一个 upstream(conn=5),同时在这条链接上新建了一个请求流(requestId=30),并调用文扩展多路复用接口 SetRequestId 封装新的身份标识,并记录到关联映射中与 downstream 信息组合; MOSN 从 upstream(conn=5) 接收了一个响应 response,依据报文扩展多路复用接口 GetRequestId 获取到请求在这条连接上的身份标识(requestId=30)。此时可以从上下游关联映射表中,根据 upstream 信息(connId=5, requestId=30) 找到对应的 downstream 信息(connId=2, requestId=1); 依据 downstream request 的信息,调用文扩展多路复用接口 SetRequestId 设置响应的 requestId,并回复给 downstream; 在整个过程中,框架流程依赖的报文扩展 Multiplexing 接口提供的能力,实现了上下游请求的多路复用关联处理,除此之外,框架还封装了很多细节的处理,例如上下游复用内存块合并处理等等,此处限于篇幅不再展开,有兴趣的同学可以参考源码进行阅读。\n统一路由框架 接下来要分析的是「统一路由框架」的设计,此方案主要解决的是非 HTTP 协议的路由适配问题。我们选取了以下三点进行具体分析:\n 通过基于属性匹配(attribute-based)的模式,与具体协议字段解耦; 引入层级路由的概念,解决属性扁平化后带来的线性匹配性能问题; 通过变量机制懒加载的特定,按需实现深/浅解包; 统一路由框架 – 基于属性匹配\n首先来看一下典型的 RDS 配置,可以看到其中的 domains、path 等字段,对应的是 HTTP 协议里的域名、路径概念,这就意味着其匹配条件只有 HTTP 协议才有字段能够满足,配置结构设计是与 HTTP 协议强相关的。这就导致了如果我们新增了一个私有协议,无法复用 RDS 的配置来做路由。\n那么如何解决配置模型与协议字段强耦合呢?简单来说就是把匹配字段拆分为扁平属性的键值对(key-value pair),匹配策略基于键值对来处理,从而解除了匹配模型与协议字段的强耦合,例如可以配置 key: $http_host,也可以配置 key:$dubbo_service,这在配置模型层面都是合法的。\n但是这并不是说匹配就有具体协议无关了,这个关联仍然是存在的,只是从强耦合转换为了隐式关联,例如配置 key: $http_host,从结构来说其与 HTTP 协议并无耦合,但是值变量仍然会通过 HTTP 协议字段来进行求值。\n统一路由框架 - 层级路由\n在引入「基于属性的匹配」之后,我们发现了一个问题,那就是由于属性本身的扁平化,其内在并不包含层级关系。如果没有层级关系,会导致匹配时需要遍历所有可能的情况组合,大量条件的场景下匹配性能近似于线性的 O(n),这显然是无法接受的。\n举例来说,对于 HTTP 协议,我们总是习惯与以下的匹配步骤:\n 匹配 Host(:authority) ; 匹配 Path ; 匹配 headers/args/cookies ; 这其实构成了一个层级关系,每一层就像是一个索引,通过层级的索引关系,在大量匹配条件的情况下仍然可以获得一个可接受的耗时成本。但是对于属性(attribute),多个属性之间并没有天然的层级关系(相比于 host、path 这种字段),这依赖于属性背后所隐式关联的字段,例如对于 Dubbo 协议,我们希望的顺序可能是:\n 匹配 $dubbo_service; 匹配 $dubbo_group; 匹配 $dubbo_version; 匹配 $dubbo_attachments_xx; 因此在配置模型上,我们引入了对应的索引层级概念,用于适配不同协议的结构化层级路由,解决扁平属性的线性匹配性能问题。\n统一路由框架 - 浅解包优化\n最后,介绍一下浅解包优化的机制。利用 MOSN 变量懒加载的特性,我们可以在报文解析时,先不去解析成本较高的部分,例如 dubbo 协议的 attachments。那么在代理请求的实际过程中,需要使用到 attachments 里的信息时,就会通过变量的 getter 求值逻辑来进行真正的解包操作。依靠此特性,可以大幅优化在不需要深解包的场景下 dubbo 协议代理转发的性能表现,实现按需解包。\n解读总结 最后,对设计部分的几个技术案例简单总结一下,整体的思路仍然是对处理流程进行抽象封装,并剥离可扩展点,从而降低用户的接入成本。\n在协议扩展支持方面:\n 封装编解码流程,抽象编解码能力接口作为协议扩展点 封装协议处理流程,抽象多路复用、心跳保活、优雅退出等能力接口作为协议扩展点 在路由框架方面:\n 通过改为基于属性匹配的机制,与具体协议字段解耦,支持多协议适配; 引入层级路由机制,解决属性扁平化的匹配性能问题; 利用变量机制懒加载特性,按需实现深/浅解包; 后续规划及展望 更多流模式支持、更多协议接入 当前 MOSN 多协议机制,已经可以比较好的支持像 Dubbo、SOFABolt 这样基于多路复用流模型的微服务协议,后续会继续扩展支持的类型及协议,例如经典的 PING-PONG 协议、Streaming 流式协议,也欢迎大家一起参与社区建设,贡献你的 PR。\n社区标准方案推进 与此同时,我们注意到 Istio 社区其实也有类似的需求,希望设计一套协议无关的路由机制——\u0026ldquo;Istio Meta Routing API\u0026rdquo;。其核心思路与 MOSN 的多协议路由框架基本一致,即通过基于属性的路由来替代基于协议字段的路由。目前该草案还处于一个比较初级的阶段,对于匹配性能、字段扩展方面还没有比较完善的设计说明,后续 MOSN 团队会积极参与社区方案的讨论,进一步推动社区标准方案的落地。\n以上就是本期分享的全部内容,如果大家对 MOSN 有问题以及建议,欢迎加入 MOSN 社区与我们交流。\n","excerpt":"本文根据 SOFAChannel#13 直播分享整理,主题:云原生网络代理 MOSN 多协议机制解析,查看视频回顾。\n 作者:无钩,目前主要从事蚂蚁集团网络代理相关的研发工作,也是 MOSN …","ref":"https://mosn.io/blog/posts/multi-protocol-deep-dive/","title":"MOSN 多协议机制解析"},{"body":"本文的目的是分析 MOSN 源码中的 HTTP 系能力,内容基于 MOSN 0.9.0\n概述 HTTP 是互联网界最常用的一种协议之一,MOSN 也提供了对其强大的支持。\nMOSN HTTP 报文组成 上图是图解 HTTP 中关于 HTTP 报文报文的介绍。MOSN 对于 HTTP 报文的处理并没有使用go 官网 net/http中的结构也没有独立设计一套相关结构 而是复用了业界开源的fasthttp 的结构。\ntype stream struct { str.BaseStream id uint64 readDisableCount int32 ctx context.Context // 请求报文 \trequest *fasthttp.Request // 响应报文 \tresponse *fasthttp.Response receiver types.StreamReceiveListener } MOSN HTTP 处理流程 上图是HTTP请求在MOSN中的流动过程,下面将具体讲解。\n流程注册 func init() { str.Register(protocol.HTTP1, \u0026amp;streamConnFactory{}) } 在 pkg/stream/http 包加载过程中将包含 HTTP 对于MOSN上下游连接的处理逻辑的结构体值注册到统一的stream处理工厂。\n捕获请求 由上图可知,MOSN捕捉到一个请求之后会开启一个goroutine读取连接中的数据,也就是 serverStreamConnection.serve 函数。\nfunc (conn *serverStreamConnection) serve() { for { // 1. pre alloc stream-level ctx with bufferCtx \tctx := conn.contextManager.Get() // 通过sync.Pool 实现实现内存复用 \tbuffers := httpBuffersByContext(ctx) request := \u0026amp;buffers.serverRequest // 2. blocking read using fasthttp.Request.Read \terr := request.ReadLimitBody(conn.br, defaultMaxRequestBodySize) if err == nil { // 3. \u0026#39;Expect: 100-continue\u0026#39; request handling. \t// See http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html for details. \tif request.MayContinue() { // Send \u0026#39;HTTP/1.1 100 Continue\u0026#39; response. \tconn.conn.Write(buffer.NewIoBufferBytes(strResponseContinue)) // read request body \terr = request.ContinueReadBody(conn.br, defaultMaxRequestBodySize) // remove \u0026#39;Expect\u0026#39; header, so it would not be sent to the upstream \trequest.Header.Del(\u0026#34;Expect\u0026#34;) } } // 读取错误的处理 \tif err != nil { // \u0026#34;read timeout with nothing read\u0026#34; is the error of returned by fasthttp v1.2.0 \t// if connection closed with nothing read. \tif err != errConnClose \u0026amp;\u0026amp; err != io.EOF \u0026amp;\u0026amp; err.Error() != \u0026#34;read timeout with nothing read\u0026#34; { // write error response \tconn.conn.Write(buffer.NewIoBufferBytes(strErrorResponse)) // close connection with flush \tconn.conn.Close(api.FlushWrite, api.LocalClose) } return } // 数据读取结束 \tid := protocol.GenerateID() s := \u0026amp;buffers.serverStream // 4. request processing \ts.stream = stream{ id: id, ctx: mosnctx.WithValue(ctx, types.ContextKeyStreamID, id), request: request, response: \u0026amp;buffers.serverResponse, } s.connection = conn s.responseDoneChan = make(chan bool, 1) s.header = mosnhttp.RequestHeader{\u0026amp;s.request.Header, nil} var span types.Span if trace.IsEnabled() { tracer := trace.Tracer(protocol.HTTP1) if tracer != nil { span = tracer.Start(ctx, s.header, time.Now()) } } // 上下文 中注入链式追踪信息 \ts.stream.ctx = s.connection.contextManager.InjectTrace(ctx, span) if log.Proxy.GetLogLevel() \u0026gt;= log.DEBUG { log.Proxy.Debugf(s.stream.ctx, \u0026#34;[stream] [http] new stream detect, requestId = %v\u0026#34;, s.stream.id) } s.receiver = conn.serverStreamConnListener.NewStreamDetect(s.stream.ctx, s, span) conn.mutex.Lock() conn.stream = s conn.mutex.Unlock() if atomic.LoadInt32(\u0026amp;s.readDisableCount) \u0026lt;= 0 { s.handleRequest() } // 5. wait for proxy done \tselect { case \u0026lt;-s.responseDoneChan: case \u0026lt;-conn.connClosed: return } conn.contextManager.Next() } } 由以上代码可以得知,因为不能判断连接是长连接还是短连接,所以MOSN调用方不主动关闭连接,MOSN也不会主动关闭,除非出现错误。\n转发请求 由上图可知,MOSN在捕获请求之后,开启一个goroutine 创建对upstream的连接,并且通过这个连接和upstream进行数据交互,与此同时开启另外一个goroutine对这个连接进行监控用来处理连接返回数据。其主要处理逻辑在downStream.receive中。\nfor i := 0; i \u0026lt;= int(types.End-types.InitPhase); i++ { fmt.Println(\u0026#34;yu\u0026#34;,i,s.downstreamReqTrailers == nil,phase,int(types.End-types.InitPhase),types.WaitNofity) switch phase { // init phase \tcase types.InitPhase: phase++ // downstream filter before route \tcase types.DownFilter: if log.Proxy.GetLogLevel() \u0026gt;= log.DEBUG { log.Proxy.Debugf(s.context, \u0026#34;[proxy] [downstream] enter phase %d, proxyId = %d \u0026#34;, phase, id) } s.runReceiveFilters(phase, s.downstreamReqHeaders, s.downstreamReqDataBuf, s.downstreamReqTrailers) if p, err := s.processError(id); err != nil { return p } phase++ // match route \tcase types.MatchRoute: if log.Proxy.GetLogLevel() \u0026gt;= log.DEBUG { log.Proxy.Debugf(s.context, \u0026#34;[proxy] [downstream] enter phase %d, proxyId = %d \u0026#34;, phase, id) } s.matchRoute() if p, err := s.processError(id); err != nil { return p } phase++ // downstream filter after route \tcase types.DownFilterAfterRoute: if log.Proxy.GetLogLevel() \u0026gt;= log.DEBUG { log.Proxy.Debugf(s.context, \u0026#34;[proxy] [downstream] enter phase %d, proxyId = %d \u0026#34;, phase, id) } s.runReceiveFilters(phase, s.downstreamReqHeaders, s.downstreamReqDataBuf, s.downstreamReqTrailers) if p, err := s.processError(id); err != nil { return p } phase++ // downstream receive header \tcase types.DownRecvHeader: if s.downstreamReqHeaders != nil { if log.Proxy.GetLogLevel() \u0026gt;= log.DEBUG { log.Proxy.Debugf(s.context, \u0026#34;[proxy] [downstream] enter phase %d, proxyId = %d \u0026#34;, phase, id) } s.receiveHeaders(s.downstreamReqDataBuf == nil \u0026amp;\u0026amp; s.downstreamReqTrailers == nil) if p, err := s.processError(id); err != nil { return p } } phase++ // downstream receive data \tcase types.DownRecvData: if s.downstreamReqDataBuf != nil { if log.Proxy.GetLogLevel() \u0026gt;= log.DEBUG { log.Proxy.Debugf(s.context, \u0026#34;[proxy] [downstream] enter phase %d, proxyId = %d \u0026#34;, phase, id) } s.downstreamReqDataBuf.Count(1) s.receiveData(s.downstreamReqTrailers == nil) if p, err := s.processError(id); err != nil { return p } } phase++ // downstream receive trailer \tcase types.DownRecvTrailer: if s.downstreamReqTrailers != nil { if log.Proxy.GetLogLevel() \u0026gt;= log.DEBUG { log.Proxy.Debugf(s.context, \u0026#34;[proxy] [downstream] enter phase %d, proxyId = %d \u0026#34;, phase, id) } s.receiveTrailers() if p, err := s.processError(id); err != nil { return p } } phase++ // downstream oneway \tcase types.Oneway: if s.oneway { if log.Proxy.GetLogLevel() \u0026gt;= log.DEBUG { log.Proxy.Debugf(s.context, \u0026#34;[proxy] [downstream] enter phase %d, proxyId = %d \u0026#34;, phase, id) } s.cleanStream() // downstreamCleaned has set, return types.End \tif p, err := s.processError(id); err != nil { return p } } // no oneway, skip types.Retry \tphase = types.WaitNofity // retry request \tcase types.Retry: if log.Proxy.GetLogLevel() \u0026gt;= log.DEBUG { log.Proxy.Debugf(s.context, \u0026#34;[proxy] [downstream] enter phase %d, proxyId = %d \u0026#34;, phase, id) } if s.downstreamReqDataBuf != nil { s.downstreamReqDataBuf.Count(1) } s.doRetry() if p, err := s.processError(id); err != nil { return p } phase++ // wait for upstreamRequest or reset \tcase types.WaitNofity: if log.Proxy.GetLogLevel() \u0026gt;= log.DEBUG { log.Proxy.Debugf(s.context, \u0026#34;[proxy] [downstream] enter phase %d, proxyId = %d \u0026#34;, phase, id) } if p, err := s.waitNotify(id); err != nil { return p } if log.Proxy.GetLogLevel() \u0026gt;= log.DEBUG { log.Proxy.Debugf(s.context, \u0026#34;[proxy] [downstream] OnReceive send downstream response %+v\u0026#34;, s.downstreamRespHeaders) } phase++ // upstream filter \tcase types.UpFilter: if log.Proxy.GetLogLevel() \u0026gt;= log.DEBUG { log.Proxy.Debugf(s.context, \u0026#34;[proxy] [downstream] enter phase %d, proxyId = %d \u0026#34;, phase, id) } s.runAppendFilters(phase, s.downstreamRespHeaders, s.downstreamRespDataBuf, s.downstreamRespTrailers) if p, err := s.processError(id); err != nil { return p } // maybe direct response \tif s.upstreamRequest == nil { fakeUpstreamRequest := \u0026amp;upstreamRequest{ downStream: s, } s.upstreamRequest = fakeUpstreamRequest } phase++ // upstream receive header \tcase types.UpRecvHeader: // send downstream response \tif s.downstreamRespHeaders != nil { if log.Proxy.GetLogLevel() \u0026gt;= log.DEBUG { log.Proxy.Debugf(s.context, \u0026#34;[proxy] [downstream] enter phase %d, proxyId = %d \u0026#34;, phase, id) } s.upstreamRequest.receiveHeaders(s.downstreamRespDataBuf == nil \u0026amp;\u0026amp; s.downstreamRespTrailers == nil) if p, err := s.processError(id); err != nil { return p } } phase++ // upstream receive data \tcase types.UpRecvData: if s.downstreamRespDataBuf != nil { if log.Proxy.GetLogLevel() \u0026gt;= log.DEBUG { log.Proxy.Debugf(s.context, \u0026#34;[proxy] [downstream] enter phase %d, proxyId = %d \u0026#34;, phase, id) } s.upstreamRequest.receiveData(s.downstreamRespTrailers == nil) if p, err := s.processError(id); err != nil { return p } } phase++ // upstream receive triler \tcase types.UpRecvTrailer: if s.downstreamRespTrailers != nil { if log.Proxy.GetLogLevel() \u0026gt;= log.DEBUG { log.Proxy.Debugf(s.context, \u0026#34;[proxy] [downstream] enter phase %d, proxyId = %d \u0026#34;, phase, id) } s.upstreamRequest.receiveTrailers() if p, err := s.processError(id); err != nil { return p } } phase++ // process end \tcase types.End: return types.End default: log.Proxy.Errorf(s.context, \u0026#34;[proxy] [downstream] unexpected phase: %d\u0026#34;, phase) return types.End } } log.Proxy.Errorf(s.context, \u0026#34;[proxy] [downstream] unexpected phase cycle time\u0026#34;) return types.End } 这个函数是处理转发逻辑的核心函数规定在处理的各个阶段需要做的事情。比如在case types.UpRecvHeader的时候进行连接创建以及对下游数据的初步读取。\n其他 连接复用 在MOSN处理 HTTP 请求的过程中我们可以很明显的看到,针对可能频繁使用的连接,MOSN实现了一套复用机制。\ntype connPool struct { MaxConn int host types.Host statReport bool clientMux sync.Mutex availableClients []*activeClient // available clients \ttotalClientCount uint64 // total clients } func (p *connPool) getAvailableClient(ctx context.Context) (*activeClient, types.PoolFailureReason) { p.clientMux.Lock() defer p.clientMux.Unlock() n := len(p.availableClients) // no available client \tif n == 0 { // max conns is 0 means no limit \tmaxConns := p.host.ClusterInfo().ResourceManager().Connections().Max() if maxConns == 0 || p.totalClientCount \u0026lt; maxConns { ac, reason := newActiveClient(ctx, p) if ac != nil \u0026amp;\u0026amp; reason == \u0026#34;\u0026#34; { p.totalClientCount++ } return ac, reason } else { p.host.HostStats().UpstreamRequestPendingOverflow.Inc(1) p.host.ClusterInfo().Stats().UpstreamRequestPendingOverflow.Inc(1) return nil, types.Overflow } } else { n-- c := p.availableClients[n] p.availableClients[n] = nil p.availableClients = p.availableClients[:n] return c, \u0026#34;\u0026#34; } } 由上述代码可知,MOSN 维护了一个有效长连接的栈,当栈中还有有效连接则从栈顶取出有效的长连接,如果不存在则新建一个tcp长连接。MOSN通过这种方式维护了连接池来实现高效的连接复用。\n内存复用 HTTP 的处理过程中会频繁的申请空间来解析 HTTP 报文,为了减少频繁的内存申请,常规的做法是内存复用,MOSN也不例外,其基于sync.pool 设计了内存复用模块。\nfunc httpBuffersByContext(context context.Context) *httpBuffers { ctx := buffer.PoolContext(context) return ctx.Find(\u0026amp;ins, nil).(*httpBuffers) } 总结 本文简单的分析了MOSN中对于HTTP请求的处理过程,其中优化方式主要如下:\n tcp 黏包:使用fasthttp 的request和response来解析报文。 实现连接复用:连接池。 实现内存复用:sync.pool。 ","excerpt":"本文的目的是分析 MOSN 源码中的 HTTP 系能力,内容基于 MOSN 0.9.0\n概述 HTTP 是互联网界最常用的一种协议之一,MOSN 也提供了对其强大的支持。\nMOSN HTTP …","ref":"https://mosn.io/blog/code/mosn-http/","title":"MOSN 源码解析 - HTTP 系能力"},{"body":"本文的目的是分析 MOSN 源码中的Log系统。\n本文的内容基于 MOSN v0.10.0。\n概述 MOSN 日志系统分为日志和Metric两大部分,其中日志主要包括errorlog和accesslog,Metrics主要包括console数据和prometheus数据\n日志 errorlog errorlog 主要是用来记录MOSN运行时候的日志信息,配置结构:\ntype ServerConfig struct { ...... DefaultLogPath string `json:\u0026#34;default_log_path,omitempty\u0026#34;` DefaultLogLevel string `json:\u0026#34;default_log_level,omitempty\u0026#34;` GlobalLogRoller string `json:\u0026#34;global_log_roller,omitempty\u0026#34;` ...... } 初始化 errorlog 包括两个对象StartLogger和DefaultLogger\n StartLogger 主要用来记录 mosn 启动的日志信息,日志级别是 INFO DefaultLogger 主要是用来记录MOSN启动之后的运行日志信息,默认和 StartLogger 一样,可以通过配置文件覆盖 代码如下:\nfunc init() { ...... // use console as start logger \tStartLogger, _ = GetOrCreateDefaultErrorLogger(\u0026#34;\u0026#34;, log.INFO) // 默认INFO \t// default as start before Init \tlog.DefaultLogger = StartLogger DefaultLogger = log.DefaultLogger // default proxy logger for test, override after config parsed \tlog.DefaultContextLogger, _ = CreateDefaultContextLogger(\u0026#34;\u0026#34;, log.INFO) ...... } ...... func InitDefaultLogger(output string, level log.Level) (err error) { // 使用配置文件来覆盖默认配置 \tDefaultLogger, err = GetOrCreateDefaultErrorLogger(output, level) ...... } accesslog accesslog 主要用来记录上下游请求的数据信息,配置结构:\ntype AccessLog struct { Path string `json:\u0026#34;log_path,omitempty\u0026#34;` Format string `json:\u0026#34;log_format,omitempty\u0026#34;` } 每个配置文件下面 servers-\u0026gt;listener-\u0026gt;access_logs,具体配置示例如下:\n{ \u0026#34;servers\u0026#34;: [ { \u0026#34;mosn_server_name\u0026#34;: \u0026#34;mosn_server_1\u0026#34;, ...... \u0026#34;listeners\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;ingress_sofa\u0026#34;, ...... \u0026#34;log_path\u0026#34;: \u0026#34;./logs/ingress.log\u0026#34;, \u0026#34;log_level\u0026#34;: \u0026#34;DEBUG\u0026#34;, \u0026#34;access_logs\u0026#34;: [ { \u0026#34;log_path\u0026#34;: \u0026#34;./logs/access_ingress.log\u0026#34;, \u0026#34;log_format\u0026#34;: \u0026#34;%start_time% %request_received_duration% %response_received_duration% %bytes_sent% %bytes_received% %protocol% %response_code% %duration% %response_flag% %response_code% %upstream_local_address% %downstream_local_address% %downstream_remote_address% %upstream_host%\u0026#34; } ] } ] } ] } accesslog 实现如下接口:\nAccessLog interface { // Log write the access info. Log(ctx context.Context, reqHeaders HeaderMap, respHeaders HeaderMap, requestInfo RequestInfo) } 调用 Log 记录日志的时候,通过使用 变量机制 来填充log_format里面的变量,相关信息保存在 ctx 里面。用于保存变量信息的 entries 通过 NewAccessLog 初始化的时候,调用 parseFormat 方法来初始化的,参考相关代码。\nlog 的具体实现 log 的具体实现已经分离到了 mosn/pkg/log 下面,errorlog 和 accesslog 的具体实现都是通过 log.GetOrCreateLogger 来初始化的。当 roller 为空的时候使用默认的 defaultRoller,默认每天轮转。\ndefaultRoller = Roller{MaxTime: defaultRotateTime} ...... defaultRotateTime = 24 * 60 * 60 start 根据不同的输出方式,初始化不同的 io.Writer 对象, 详情\n type io.Writer \u0026ldquo;\u0026rdquo;, \u0026ldquo;stderr\u0026rdquo;, \u0026ldquo;/dev/stderr\u0026rdquo; os.Stderr \u0026ldquo;stdout\u0026rdquo;, \u0026ldquo;/dev/stdout\u0026rdquo; os.Stdout \u0026ldquo;syslog\u0026rdquo; gsyslog本地对象 其他 gsyslog远程对象 创建好 log 对象之后,通过 loggers 保存起来,避免创建多个对象,loggers 是一个 sync.Map对象,是 golang1.9 之后加入的一个新的线程安全的 map。\nstart 启动之后会 创建一个一直循环读取的协程 handler\nhandler 相关代码\n在初始化的时候,创建了一个 500 大小的 chan writeBufferChan,并且在 handler 里面处理需要记录的日志、重命名的事件、关闭的事件。\nlg := \u0026amp;Logger{ output: output, roller: roller, writeBufferChan: make(chan buffer.IoBuffer, 500), reopenChan: make(chan struct{}), closeChan: make(chan struct{}), // writer and create will be setted in start() } for { select { case \u0026lt;-l.reopenChan: ...... case \u0026lt;-l.closeChan: ...... case buf = \u0026lt;-l.writeBufferChan: ...... runtime.Gosched() } reopenChan 通过重命名文件之后,重新调用 start 方法创建新文件,主要使用在文件轮转的时候。os.Stdout os.Stderr 不支持操作,会报错。\ncloseChan 把当前 writeBufferChan 需要写入的数据写入到对象中,然后退出当前协程。\nwriteBufferChan for i := 0; i \u0026lt; 20; i++ { select { case b := \u0026lt;-l.writeBufferChan: buf.Write(b.Bytes()) buffer.PutIoBuffer(b) default: break } } buf.WriteTo(l) buffer.PutIoBuffer(buf) 当收到第一次写数据的时候不是立刻写入数据到 log 对象,而是在等待 20 次读取信息,一起写入到对 log 象中,在大量写日志的时候不会导致调用太频繁。如频繁写入文件、频繁调用写日志接口,相反,这会增加内存分配,最好的其实是使用 writev,但是 go runtime 的 io 库没有这个实现。可以采用 plugin 机制 来接管日志的打印,减少 io 卡顿对 go runtime 的调度影响\n*当一次循环处理完之后,会调用 runtime.Gosched() 主动让出当前协程的 cpu 资源\nMetrics Metrics 是一种规范的度量,分为如下类型,摘抄至 METRIC TYPES\n Gauges: 代表可以任意上下波动的单个数值,通常用来表示测量值。比如内存,cpu,磁盘等信息。 Counters: 累计度量,代表单调递增的计数器,只有在重启或者重置的时候数量为 0,其他时候一般不使用减少。可以用来表示请求的数量。 Histograms: 直方图,对观察值(通常是请求持续时间或返回大小之类的数据)进行采样,并将其计数放到对应的配置桶中,也提供所有观测值总和信息。 Summary: 类似于直方图,摘要采样的观测结果,可以计算滑动时间窗口内的可配置分位数。 主要代码在 pkg/metrics 下面包括 pkg/metrics/sink 和 pkg/metrics/shm。\nsink pkg/metrics/sink 包含 console 和 prometheus,两者都实现了 types.MetricsSink 接口。prometheus 是通过工厂方法 注册 进去使用的;console 是通过直接调用 console.NewConsoleSink() 来使用的。\nprometheus 主要是通过 prometheus 的 metrics 统计请求的信息,配置文件示例:\n{ \u0026#34;metrics\u0026#34;: { ...... \u0026#34;sinks\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;prometheus\u0026#34;, \u0026#34;config\u0026#34;: { \u0026#34;port\u0026#34;: 34903 } } ] } } 其中 type 目前只支持 prometheus\n通过 prometheus 库 提供的 http 能力,使用配置信息启动一个 http 服务,把 Metrics 信息通过 http://host:port/metrics 的方式供prometheus收集或展示。\nconsole 主要用于 admin api 的 /api/v1/stats 展示。所以必须配置 admin 相关信息,示例:\n{ \u0026#34;admin\u0026#34;: { \u0026#34;address\u0026#34;: { \u0026#34;socket_address\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;0.0.0.0\u0026#34;, \u0026#34;port_value\u0026#34;: 34901 } } } } 如果不配置会打印 no admin config, no admin api served 告警信息,参考\nadmin api 中还包括如下接口\n /api/v1/config_dump /api/v1/stats /api/v1/update_loglevel /api/v1/enable_log /api/v1/disbale_log /api/v1/states /api/v1/plugin / 其中 update_loglevel 用于更新 errorlog 日志的输出级别,enable_log 和 disbale_log 用于启用/禁用 errorlog 的输出\nshm pkg/metrics/shm 主要是通过 mmap 将一个文件或者其它对象映射进内存,让多个进程共用,可以让 MOSN 在热升级的过程中 metrics 数据不会出现 \u0026quot;断崖\u0026quot;,关于 shm 的分析内容可以参考 共享内存模型。\n 不鼓励在 Go 里面使用共享内存,除非你有明确的使用场景 总结 通过分析 MOSN 源码的 log系统 模块,不单单是了解了日志部分,从配置、启动流程,到上下游请求都有所涉及。学习了很多,希望 MOSN 越来越强大。\n MOSN 源码 v0.10.0 pkg 源码 commit 1e41847 ","excerpt":"本文的目的是分析 MOSN 源码中的Log系统。\n本文的内容基于 MOSN v0.10.0。\n概述 MOSN 日志系统分为日志和Metric两大部分,其中日志主要包括errorlog …","ref":"https://mosn.io/blog/code/mosn-log/","title":"MOSN 源码解析 - log 系统"},{"body":"作为云原生网络代理,Service Mesh 是 MOSN 的重要应用场景。随着 Service Mesh 概念的日益推广,大家对这套体系都已经不再陌生,有了较为深入的认知。但是与理论传播相对应的是,生产级别的大规模落地实践案例却并不多见。这其中有多方面的原因,包括社区方案饱受诟病的“大规模场景性能问题”、“配套的运维监控基础设施演进速度跟不上”、“存量服务化体系的兼容方案”等等。\n现实场景中,大部分中国厂商都有一套自研 RPC 的服务化体系,属于「存量服务化体系的兼容方案」中的协议适配问题。为此,MOSN 设计了一套多协议框架,用于降低自研体系的协议适配及接入成本,加速 Service Mesh 的落地普及。本次演讲将向大家介绍 MOSN 实现多协议低成本接入的设计思路,以及相应的快速接入实践案例。\n讲师:无钩(@neverhook)\n本期大纲 一个请求的 MOSN 之旅 如何在 MOSN 中接入新的协议 SOFABolt 协议接入实践 未来发展:统一路由框架 直播报名:https://tech.antfin.com/community/live/1131\n扫描社区页面的二维码加入钉钉群参与互动。\n","excerpt":"作为云原生网络代理,Service Mesh 是 MOSN 的重要应用场景。随着 Service Mesh 概念的日益推广,大家对这套体系都已经不再陌生,有了较为深入的认知。但是与理论传播相对应的是, …","ref":"https://mosn.io/blog/news/mosn-channel-1/","title":"直播预告:MOSN 的多协议机制解析"},{"body":"为什么要使用 MOSN 替换 Istio 的数据面? 蚂蚁集团在进行 Mesh 改造前,已经预料到作为下一代蚂蚁集团的基础架构,Mesh 化势必带来革命性的变革以及演进成本,我们有非常宏大的蓝图:准备将原有的网络和中间件方面的各种能力重新沉淀和打磨,打造成为未来新一代架构的底层平台,承载各种服务通讯的职责。\n这是一个需要多年时间打造,满足未来五年乃至十年需求的长期规划项目,合作共建团队跨业务、SRE、中间件、基础架构等部门。我们必须有一个具备灵活扩展、高性能、满足长期演进的网络代理转发平面。Nginx、Envoy 在网络代理领域有非常长期的能力积累和活跃的社区,我们也同时借鉴了 Nginx、Envoy 等其他优秀的开源网络代理, 同时在研发效率、灵活扩展等方面进行了加强,同时在整个 Mesh 改造涉及到非常多的部门和研发人员,必须考虑到跨团队合作的落地成本,所以我们基于 Go 自研了云原生场景下的新型网络代理 MOSN。对于 Go 的性能,我们前期也做了充分的调研和测试,满足蚂蚁集团业务对性能的要求。\n同时我们从社区用户方面收到了很多的反馈和需求,大家有同样的需求以及思考,所以我们结合社区与自身的实际情况,从满足社区以及用户角度出发进行了 MOSN 的研发工作,我们认为开源的竞争主要是标准与规范的竞争,我们需要基于开源标准做最适合自身的实现选择。\nMOSN 与 Envoy 不同点是什么?优势在哪里? 语言栈的不同\nMOSN 使用 Go 语言编写,Go 语言在生产效率,内存安全上有比较强的保障,同时 Go 语言在云原生时代有广泛的库生态系统,性能在 Mesh 场景下被我们评估以及实践是可以接受的。所以 MOSN 对于使用 Go、Java 等语言的公司和个人的心智成本更低。\n核心能力的差异化\n MOSN 支持多协议框架,用户可以比较容易的接入私有协议,具有统一的路由框架;\n 多进程的插件机制,可以通过插件框架很方便的扩展独立 MOSN 进程的插件,做一些其他管理,旁路等的功能模块扩展;\n 具备中国密码合规的传输层国密算法支持;\n 开源的 MOSN 和蚂蚁集团内部使用的 MOSN 是同一个版本吗? 首先蚂蚁集团内部并没有一个所谓独立的 MOSN 版本。蚂蚁集团内部有较多的基于 MOSN 开发的模块,内部模块依赖开源版的 MOSN。业务无关的 MOSN 核心能力的研发,均是直接在开源版本上进行。\nMOSN 的开源版本和商业版本的区别是什么? 蚂蚁集团有 Mesh 商业产品,商业产品主要是提供从开发到部署再到运行时的一套完整的解决方案,同时为了满足商业用户自身的业务需求会对 MOSN 进行扩展,所以所谓的 MOSN 商业版本主要是承载了商业用户自身业务模块的版本。\nMOSN 的开源计划是什么? MOSN 开源的发布周期是一个月,我们即将公布 2021 年的 Roadmap,期待与更多企业共建。\nMOSN 支持 Istio 的什么版本?什么时候可以在 Istio 中可用? 目前 MOSN 可基于 Istio 1.5.2 跑通 bookinfo example,预计 2020 年 9 月份将完整支持 Istio 的能力,并成为 Istio 中可用的 Sidecar 部署选项。请加入 MOSN 社区 了解适配 Istio 的工作。\nMOSN 支持哪些服务注册和发现机制? MOSN 主要支持两种服务注册与发现机制:一种是直接和 Istio 适配,另一种是集成 SDK,与不同的注册中心和配置中心来搭配使用。\n如何参与 MOSN 开源社区? MOSN 社区分为用户组和按需求创建的 Working Group。您可以使用钉钉扫描社区页面上的二维码加入 MOSN 用户群,参与社区讨论,获取社区最新活动通知。访问 Community 仓库了解 MOSN 开源社区的组织架构和获取社区资料。\n","excerpt":"为什么要使用 MOSN 替换 Istio 的数据面? 蚂蚁集团在进行 Mesh 改造前,已经预料到作为下一代蚂蚁集团的基础架构,Mesh 化势必带来革命性的变革以及演进成本,我们有非常宏大的蓝图:准备 …","ref":"https://mosn.io/docs/faq/","title":"FAQ"},{"body":"我们很高兴的宣布 MOSN v0.10.0 发布。以下是该版本的变更日志。\n新功能 支持多进程插件模式(#979,@taoyuanyuan) 启动参数支持 service-meta参数(#952,@trainyao) 支持 abstract uds 模式挂载 sds socket 重构 分离部分 MOSN 基础库代码到 mosn.io/pkg 分离部分 MOSN 接口定义到 mosn.io/api 优化 日志基础模块分离到 mosn.io/pkg,MOSN 的日志实现优化 优化 FeatureGate(#927,@nejisama) 新增处理获取 SDS 配置失败时的处理 CDS 动态删除 Cluster时,会同步停止对应 Cluster 的健康检查 SDS 触发证书更新时的回调函数新增证书配置作为参数 Bug 修复 修复在 SOFARPC Oneway 请求失败时,导致的内存泄漏问题 修复在收到非标准的 HTTP 响应时,返回 502 错误的问题 修复 DUMP 配置时可能存在的并发冲突 修复 TraceLog 统计的 Request 和 Response Size 错误问题 修复因为并发写连接导致写超时失效的问题 修复 serialize 序列化的 bug 修复连接读取时内存复用保留 buffer 过大导致内存占用过高的问题 优化 XProtocol 中 Dubbo 相关实现 ","excerpt":"我们很高兴的宣布 MOSN v0.10.0 发布。以下是该版本的变更日志。\n新功能 支持多进程插件模式(#979,@taoyuanyuan) 启动参数支持 service-meta参 …","ref":"https://mosn.io/docs/products/report/releases/v0.10.0/","title":"MOSN v0.10.0 发布"},{"body":"","excerpt":"","ref":"https://mosn.io/blog/code/mosn-startup/","title":"MOSN 源码解析 - 启动流程"},{"body":"基本概念 MOSN 中的概念比较多,以sofarpc-sample下面的config.json为例,结合上图依次看下:\n Downstream:调用端的数据流向统称。 Upstream:服务端的数据流向统称。 clientListener:用于接收调用端(业务进程)请求数据的监听端口。 serverListener:作为服务端流量代理,用于接收调用端的请求 clientCluster:服务提供者的地址列表,实际应用中这块数据应该来自于注册中心。 serverCluster:真正提供服务的业务进程,也就是说一个MOSN可以代理多个服务端进程。 流程概述 这是官方提供的一张流程图,已经很清晰了,这里简要说明一下:\n MOSN 无论是收到调用端(Downstream)发来的请求还是服务端(Upstream)发来的响应,都需要通过网络层,然后根据指定协议解码request跟response,最后交给stream层处理(当然这中间会有各式各样的过滤器链可以进行扩展,后面跟着源码会说)。 由于 MOSN 夹在调用端跟服务端中间,分别跟调用端、服务端都会建立连接,因此在stream层采用的是同步阻塞的方式,也就是说调用端的请求转发出去以后对应的协程就会挂起,在收到服务端发来的响应以后再唤醒该等待协程,而关联请求跟响应的关键就是 requestID。 明白了大致流程以后,下面就通过源码来分析一下整个过程。\n源码分析 为了便于理解,这里从下往上看,也就是先从网络层接收数据的逻辑开始,一步一步来分析 MOSN 是怎么做编解码,怎么转发请求。\n发起请求 MOSN 对于网络层的操作,无论是调用端还是服务端,都封装在eventloop.go文件中,每当连接建立以后,MOSN 都会开启两个协程分别处理该连接上的读写操作,分别对应startReadLoop跟startWriteLoop两个方法。\n当调用端(业务进程)发起请求时,根据clientListener指定的地址跟 MOSN 建立连接,然后发起调用。MOSN 在建立连接以后,会等待请求数据的到达,这部分逻辑就在startReadLoop 中:\nfunc (c *connection) startReadLoop() { var transferTime time.Time for { //省略部分逻辑... \tselect { case \u0026lt;-c.internalStopChan: return case \u0026lt;-c.readEnabledChan: default: if c.readEnabled { //readEnabled 默认为true \t//真正的读取数据逻辑在这里 \terr := c.doRead() if err != nil { //读取失败进行处理 \t} } else { select { case \u0026lt;-c.readEnabledChan: case \u0026lt;-time.After(100 * time.Millisecond): } } } } } 逻辑比较直观,就是一个死循环不断的读取该连接上面的数据。 下面看一下关键的doRead()方法:\nfunc (c *connection) doRead() (err error) { //为该连接创建一个buffer来保存读入的数据 \tif c.readBuffer == nil { c.readBuffer = buffer.GetIoBuffer(DefaultBufferReadCapacity) } var bytesRead int64 //从连接中读取数据,返回实际读取到的字节数,rawConnection对应的就是原始连接 \tbytesRead, err = c.readBuffer.ReadOnce(c.rawConnection) if err != nil { //错误处理 \t} //没有读取到数据,也没有报错 \tif bytesRead == 0 \u0026amp;\u0026amp; err == nil { err = io.EOF } //进行读取字节函数的回调,可以进行数据统计 \tfor _, cb := range c.bytesReadCallbacks { cb(uint64(bytesRead)) } //通知上层读取到了新的数据 \tc.onRead() return } 上面的ReadOnce 方法比较简单,就不单独列出来了,其实就是在该连接上设置一个超时时间进行读取,并把读取到的数据放入buffer中,结合最外层的死循环,不难理解这个不断尝试读取数据的模型。\n下面重点看一下回调方法onRead() \nfunc (c *connection) onRead() { //不再可读,这里可能跟热升级有关? \tif !c.readEnabled { return } //没有需要处理的数据 \tif c.readBuffer.Len() == 0 { return } //filterManager过滤器管理者,把读取到的数据交给过滤器链路进行处理 \tc.filterManager.OnRead() } //上述OnRead方法实现 func (fm *filterManager) OnRead() { fm.onContinueReading(nil) } func (fm *filterManager) onContinueReading(filter *activeReadFilter) { var index int var uf *activeReadFilter if filter != nil { index = filter.index + 1 } //这里可以清楚的看到网络层读取到数据以后,通过filterManager把数据交给整个过滤器链路处理 \tfor ; index \u0026lt; len(fm.upstreamFilters); index++ { uf = fm.upstreamFilters[index] uf.index = index //针对还没有初始化的过滤器回调其初始化方法OnNewConnection \tif !uf.initialized { uf.initialized = true status := uf.filter.OnNewConnection() if status == api.Stop { return } } //取出该连接中刚才读取到的数据 \tbuf := fm.conn.GetReadBuffer() if buf != nil \u0026amp;\u0026amp; buf.Len() \u0026gt; 0 { //通知过滤器进行处理 \tstatus := uf.filter.OnData(buf) if status == api.Stop { return } } } } 在sofarpc-sample中,这个过滤器对应的实现就在proxy.go文件中,一起来看下具体实现:\nfunc (p *proxy) OnData(buf buffer.IoBuffer) api.FilterStatus { //针对使用的协议类型初始化serverStreamConn \tif p.serverStreamConn == nil { var prot string if conn, ok := p.readCallbacks.Connection().RawConn().(*mtls.TLSConn); ok { prot = conn.ConnectionState().NegotiatedProtocol } protocol, err := stream.SelectStreamFactoryProtocol(p.context, prot, buf.Bytes()) if err == stream.EAGAIN { return api.Stop } else if err == stream.FAILED { var size int if buf.Len() \u0026gt; 10 { size = 10 } else { size = buf.Len() } log.DefaultLogger.Errorf(\u0026#34;[proxy] Protocol Auto error magic :%v\u0026#34;, buf.Bytes()[:size]) p.readCallbacks.Connection().Close(api.NoFlush, api.OnReadErrClose) return api.Stop } log.DefaultLogger.Debugf(\u0026#34;[proxy] Protoctol Auto: %v\u0026#34;, protocol) p.serverStreamConn = stream.CreateServerStreamConnection(p.context, protocol, p.readCallbacks.Connection(), p) } //把数据分发到对应协议的的解码器,在这里当然就是sofa协议解析器 \tp.serverStreamConn.Dispatch(buf) //结合上面过滤器链路的调用逻辑看,返回Stop表示处理完成,不会再继续调用剩余的过滤器 \treturn api.Stop } 由于我们是以sofarcp-sample为例进行分析,所以上述的Dispatch()方法自然落在了pkg/stream/sofarpc/stream.go文件中,一起来看一下:\nfunc (conn *streamConnection) Dispatch(buf types.IoBuffer) { for { // 1. pre alloc stream-level ctx with bufferCtx \tctx := conn.contextManager.Get() // 2. decode process \t// 针对读取到的数据,按照协议类型进行解码 \tcmd, err := conn.codecEngine.Decode(ctx, buf) // No enough data \t//如果没有报错且没有解析成功,那就说明当前收到的数据不够解码,推出循环,等待更多数据到来 \tif cmd == nil \u0026amp;\u0026amp; err == nil { break } if err != nil { //错误处理 \t} // Do handle staff. Error would also be passed to this function. \t//解码成功以后,开始处理该请求 \t//注意不能并行对数据进行解码,不然数据都乱了,解码之后可以引入多线程提高吞吐量 \tconn.handleCommand(ctx, cmd, err) if err != nil { break } conn.contextManager.Next() } } 上述解码过程的具体实现就不单独列出来了,根据协议规范处理字节即可。\n下面重点看一下解码成功后的后续处理,继续handleCommand方法:\nfunc (conn *streamConnection) handleCommand(ctx context.Context, model interface{}, err error) { if err != nil { conn.handleError(ctx, model, err) return } //类型校验 \tcmd, ok := model.(sofarpc.SofaRpcCmd) if !ok { conn.handleError(ctx, model, ErrNotSofarpcCmd) return } //根据数据类型创建对应的stream \tstream := conn.processStream(ctx, cmd) //处理该stream的后续工作 \tif stream != nil { timeoutInt := cmd.GetTimeout() timeout := strconv.Itoa(timeoutInt) // timeout, ms \tcmd.Set(types.HeaderGlobalTimeout, timeout) //转发数据的逻辑封装在这里 \tstream.receiver.OnReceive(stream.ctx, cmd, cmd.Data(), nil) } } //这里是区分请求跟响应的关键部分,关系到数据流向 func (conn *streamConnection) processStream(ctx context.Context, cmd sofarpc.SofaRpcCmd) *stream { switch cmd.CommandType() { case sofarpc.REQUEST, sofarpc.REQUEST_ONEWAY: var span types.Span if trace.IsEnabled() { // try build trace span \ttracer := trace.Tracer(protocol.SofaRPC) if tracer != nil { span = tracer.Start(ctx, cmd, time.Now()) } } //请求处理 \treturn conn.onNewStreamDetect(ctx, cmd, span) case sofarpc.RESPONSE: //响应处理 \treturn conn.onStreamRecv(ctx, cmd) } return nil } 上述stream的处理逻辑,是我认为整个数据流处理中最复杂的部分,首先这里出现了分歧,根据当前的数据是request还是response进行不同的处理,顺着我们的思路,现在还在请求转发阶段,因此我们先来看下请求处理:\nfunc (conn *streamConnection) onNewStreamDetect(ctx context.Context, cmd sofarpc.SofaRpcCmd, span types.Span) *stream { //每个请求新建一个stream \tbuffers := sofaBuffersByContext(ctx) stream := \u0026amp;buffers.server //保存requestID,后面要用来关联请求及响应 \tstream.id = cmd.RequestID() stream.ctx = mosnctx.WithValue(ctx, types.ContextKeyStreamID, stream.id) stream.ctx = mosnctx.WithValue(ctx, types.ContextSubProtocol, cmd.ProtocolCode()) stream.ctx = conn.contextManager.InjectTrace(stream.ctx, span) //数据流向 \tstream.direction = ServerStream stream.sc = conn //根据请求类型进行处理 \tif cmd.CommandType() == sofarpc.REQUEST_ONEWAY { stream.receiver = conn.serverStreamConnectionEventListener.NewStreamDetect(stream.ctx, nil, span) } else { //为该stream创建一个用于处理收到响应以后的对象 \tstream.receiver = conn.serverStreamConnectionEventListener.NewStreamDetect(stream.ctx, stream, span) } return stream } //receiver的具体实现 func (p *proxy) NewStreamDetect(ctx context.Context, responseSender types.StreamSender, span types.Span) types.StreamReceiveListener { //再次是一个新的stream \tstream := newActiveStream(ctx, p, responseSender, span) if value := mosnctx.Get(p.context, types.ContextKeyStreamFilterChainFactories); value != nil { ff := value.(*atomic.Value) ffs, ok := ff.Load().([]api.StreamFilterChainFactory) if ok { for _, f := range ffs { f.CreateFilterChain(p.context, stream) } } } p.asMux.Lock() stream.element = p.activeSteams.PushBack(stream) p.asMux.Unlock() return stream } //真正receiver的创建过程 func newActiveStream(ctx context.Context, proxy *proxy, responseSender types.StreamSender, span types.Span) *downStream { if span != nil \u0026amp;\u0026amp; trace.IsEnabled() { ctx = mosnctx.WithValue(ctx, types.ContextKeyActiveSpan, span) ctx = mosnctx.WithValue(ctx, types.ContextKeyTraceSpanKey, \u0026amp;trace.SpanKey{TraceId: span.TraceId(), SpanId: span.SpanId()}) } //从对象池中选一个 \tproxyBuffers := proxyBuffersByContext(ctx) stream := \u0026amp;proxyBuffers.stream stream.ID = atomic.AddUint32(\u0026amp;currProxyID, 1) stream.proxy = proxy stream.requestInfo = \u0026amp;proxyBuffers.info stream.requestInfo.SetStartTime() stream.requestInfo.SetDownstreamLocalAddress(proxy.readCallbacks.Connection().LocalAddr()) stream.requestInfo.SetDownstreamRemoteAddress(proxy.readCallbacks.Connection().RemoteAddr()) stream.context = ctx stream.reuseBuffer = 1 stream.notify = make(chan struct{}, 1) //省略部分数据 \tif responseSender == nil || reflect.ValueOf(responseSender).IsNil() { stream.oneway = true } else { stream.responseSender = responseSender stream.responseSender.GetStream().AddEventListener(stream) } return stream } 整个stream的构建过程代码多且复杂,但其实总的来说就是针对每个请求创建了两个stream对象,一个用于封装请求逻辑,一个用于封装收到响应以后的处理逻辑。\n接下来需要回到handleCommand方法,当stream创建好之后,会直接调用其receiver的OnReceive方法,由于现在还是处理请求,所以对应的是downstream.go中的实现:\n注意:每个请求数据都分为了header,body,trailers三部分。\nfunc (s *downStream) OnReceive(ctx context.Context, headers types.HeaderMap, data types.IoBuffer, trailers types.HeaderMap) { //head body trailer \ts.downstreamReqHeaders = headers if data != nil { s.downstreamReqDataBuf = data.Clone() data.Drain(data.Len()) } s.downstreamReqTrailers = trailers id := s.ID //把给任务丢给协程池进行处理即可 \tpool.ScheduleAuto(func() { defer func() { if r := recover(); r != nil { if id == s.ID { s.delete() } } }() //一旦该协程被CPU调度到以后,就开始继续执行发送请求的逻辑: \tphase := types.InitPhase for i := 0; i \u0026lt; 10; i++ { s.cleanNotify() //真正的处理逻辑在这里 \tphase = s.receive(ctx, id, phase) switch phase { case types.End: return case types.MatchRoute: case types.Retry: case types.UpFilter: } } }) } receive方法的逻辑我觉得很有意思,总体来说在请求转发阶段,依次需要经过DownFilter -\u0026gt; MatchRoute -\u0026gt; DownFilterAfterRoute -\u0026gt; DownRecvHeader -\u0026gt; DownRecvData -\u0026gt; DownRecvTrailer -\u0026gt; WaitNofity这么几个阶段,从字面意思可以知道MatchRoute就是构建路由信息,也就是转发给哪个服务,而WaitNofity则是转发成功以后,等待被响应数据唤醒。\n下面就依次来看一下:\nfunc (s *downStream) receive(ctx context.Context, id uint32, phase types.Phase) types.Phase { for i := 0; i \u0026lt;= int(types.End-types.InitPhase); i++ { switch phase { // init phase \tcase types.InitPhase: phase++ // downstream filter before route \tcase types.DownFilter: s.runReceiveFilters(phase, s.downstreamReqHeaders, s.downstreamReqDataBuf, s.downstreamReqTrailers) //有错误就退出 \tif p, err := s.processError(id); err != nil { return p } phase++ // match route \tcase types.MatchRoute: //生成服务提供者的地址列表以及路由规则 \ts.matchRoute() if p, err := s.processError(id); err != nil { return p } phase++ // downstream filter after route \tcase types.DownFilterAfterRoute: s.runReceiveFilters(phase, s.downstreamReqHeaders, s.downstreamReqDataBuf, s.downstreamReqTrailers) if p, err := s.processError(id); err != nil { return p } phase++ // downstream receive header \tcase types.DownRecvHeader: //这里开始依次发送数据 \tif s.downstreamReqHeaders != nil { s.receiveHeaders(s.downstreamReqDataBuf == nil \u0026amp;\u0026amp; s.downstreamReqTrailers == nil) if p, err := s.processError(id); err != nil { return p } } phase++ // downstream receive data \tcase types.DownRecvData: if s.downstreamReqDataBuf != nil { s.downstreamReqDataBuf.Count(1) s.receiveData(s.downstreamReqTrailers == nil) if p, err := s.processError(id); err != nil { return p } } phase++ // downstream receive trailer \tcase types.DownRecvTrailer: if s.downstreamReqTrailers != nil { s.receiveTrailers() if p, err := s.processError(id); err != nil { return p } } phase++ case types.WaitNofity: //这里阻塞等待返回及结果 \tif p, err := s.waitNotify(id); err != nil { return p } phase++ } } return types.End } 真正的发送数据逻辑是在receiveHeaders、receiveData 、receiveTrailers 这三个方法里,当然每次请求不一定都需要有这三部分的数据,这里我们以receiveHeaders方法为例来进行说明:\nfunc (s *downStream) receiveHeaders(endStream bool) { s.downstreamRecvDone = endStream //省略部分逻辑。。。 //这里的的clusterName就对应上面的\u0026#34;clientCluster\u0026#34; \tclusterName := s.route.RouteRule().ClusterName() s.cluster = s.snapshot.ClusterInfo() s.requestInfo.SetRouteEntry(s.route.RouteRule()) //初始化连接池 \tpool, err := s.initializeUpstreamConnectionPool(s) if err != nil { //错误处理 \t} parseProxyTimeout(\u0026amp;s.timeout, s.route, s.downstreamReqHeaders) prot := s.getUpstreamProtocol() s.retryState = newRetryState(s.route.RouteRule().Policy().RetryPolicy(), s.downstreamReqHeaders, s.cluster, prot) //构建对应的upstream请求 \tproxyBuffers := proxyBuffersByContext(s.context) s.upstreamRequest = \u0026amp;proxyBuffers.request s.upstreamRequest.downStream = s s.upstreamRequest.proxy = s.proxy s.upstreamRequest.protocol = prot s.upstreamRequest.connPool = pool s.route.RouteRule().FinalizeRequestHeaders(s.downstreamReqHeaders, s.requestInfo) //这里发送数据 \ts.upstreamRequest.appendHeaders(endStream) //这里开启超时计时器 \tif endStream { s.onUpstreamRequestSent() } } // func (r *upstreamRequest) appendHeaders(endStream bool) { if r.downStream.processDone() { return } r.sendComplete = endStream if r.downStream.oneway { r.connPool.NewStream(r.downStream.context, nil, r) } else { r.connPool.NewStream(r.downStream.context, r, r) } } // func (p *connPool) NewStream(ctx context.Context, responseDecoder types.StreamReceiveListener, listener types.PoolEventListener) { subProtocol := getSubProtocol(ctx) //从连接池中获取连接 \tclient, _ := p.activeClients.Load(subProtocol) if client == nil { listener.OnFailure(types.ConnectionFailure, p.host) return } activeClient := client.(*activeClient) if atomic.LoadUint32(\u0026amp;activeClient.state) != Connected { listener.OnFailure(types.ConnectionFailure, p.host) return } if !p.host.ClusterInfo().ResourceManager().Requests().CanCreate() { listener.OnFailure(types.Overflow, p.host) p.host.HostStats().UpstreamRequestPendingOverflow.Inc(1) p.host.ClusterInfo().Stats().UpstreamRequestPendingOverflow.Inc(1) } else { atomic.AddUint64(\u0026amp;activeClient.totalStream, 1) p.host.HostStats().UpstreamRequestTotal.Inc(1) p.host.ClusterInfo().Stats().UpstreamRequestTotal.Inc(1) var streamEncoder types.StreamSender // oneway \tif responseDecoder == nil { streamEncoder = activeClient.client.NewStream(ctx, nil) } else { //这里会把streamId对应的stream保存起来 \tstreamEncoder = activeClient.client.NewStream(ctx, responseDecoder) streamEncoder.GetStream().AddEventListener(activeClient) } //发送数据 \tlistener.OnReady(streamEncoder, p.host) } return } // func (c *client) NewStream(context context.Context, respReceiver types.StreamReceiveListener) types.StreamSender { // oneway \tif respReceiver == nil { return c.ClientStreamConnection.NewStream(context, nil) } wrapper := \u0026amp;clientStreamReceiverWrapper{ streamReceiver: respReceiver, } streamSender := c.ClientStreamConnection.NewStream(context, wrapper) wrapper.stream = streamSender.GetStream() return streamSender } // func (conn *streamConnection) NewStream(ctx context.Context, receiver types.StreamReceiveListener) types.StreamSender { buffers := sofaBuffersByContext(ctx) stream := \u0026amp;buffers.client stream.id = atomic.AddUint64(\u0026amp;conn.currStreamID, 1) stream.ctx = mosnctx.WithValue(ctx, types.ContextKeyStreamID, stream.id) stream.direction = ClientStream stream.sc = conn stream.receiver = receiver //oneway的请求不需要处理结果 \tif stream.receiver != nil { conn.mutex.Lock() //按照id放进map \tconn.streams[stream.id] = stream conn.mutex.Unlock() } return stream } // func (r *upstreamRequest) OnReady(sender types.StreamSender, host types.Host) { r.requestSender = sender r.host = host r.requestSender.GetStream().AddEventListener(r) r.startTime = time.Now() endStream := r.sendComplete \u0026amp;\u0026amp; !r.dataSent \u0026amp;\u0026amp; !r.trailerSent //发送数据 \tr.requestSender.AppendHeaders(r.downStream.context, r.convertHeader(r.downStream.downstreamReqHeaders), endStream) r.downStream.requestInfo.OnUpstreamHostSelected(host) r.downStream.requestInfo.SetUpstreamLocalAddress(host.AddressString()) } // func (s *stream) AppendHeaders(ctx context.Context, headers types.HeaderMap, endStream bool) error { cmd, ok := headers.(sofarpc.SofaRpcCmd) var err error、 switch s.direction { case ClientStream: s.sendCmd = cmd case ServerStream: switch cmd.CommandType() { case sofarpc.RESPONSE: s.sendCmd = cmd case sofarpc.REQUEST, sofarpc.REQUEST_ONEWAY: //服务端发给调用端的数据 \ts.sendCmd, err = s.buildHijackResp(cmd) } } if endStream { s.endStream() } return err } // func (s *stream) endStream() { if s.sendCmd != nil { s.sendCmd.SetRequestID(s.id) s.sendCmd.Del(types.HeaderGlobalTimeout) //编码 \tbuf, err := s.sc.codecEngine.Encode(s.ctx, s.sendCmd) if err != nil { //... \t} //这里相当于是上面的编码只编码的头部,如果有body那就一起发送? \tif dataBuf := s.sendCmd.Data(); dataBuf != nil { err = s.sc.conn.Write(buf, dataBuf) } else { err = s.sc.conn.Write(buf) } //错误处理 \tif err != nil { } } } 网络层的write:\nfunc (c *connection) Write(buffers ...buffer.IoBuffer) (err error) { //同样经过过滤器 \tfs := c.filterManager.OnWrite(buffers) if fs == api.Stop { return nil } if !UseNetpollMode { if c.useWriteLoop { c.writeBufferChan \u0026lt;- \u0026amp;buffers } else { err = c.writeDirectly(\u0026amp;buffers) } } else { //netpoll模式写 \t} return } 在对应的eventloop.go中的startWriteLoop方法:\nfunc (c *connection) startWriteLoop() { var err error for { select { case \u0026lt;-c.internalStopChan: return case \u0026lt;-c.transferChan: needTransfer = true return case buf, ok := \u0026lt;-c.writeBufferChan: if !ok { return } c.appendBuffer(buf) c.rawConnection.SetWriteDeadline(time.Now().Add(types.DefaultConnWriteTimeout)) _, err = c.doWrite() } if err != nil { //错误处理 \t} } } 请求数据发出去以后当前协程就阻塞了,看下waitNotify方法的实现:\nfunc (s *downStream) waitNotify(id uint32) (phase types.Phase, err error) { if s.ID != id { return types.End, types.ErrExit } //阻塞等待 \tselect { case \u0026lt;-s.notify: } return s.processError(id) } 经过上面的几个步骤,请求被成功转发出去,并且对应的stream在阻塞等待响应。\n结果响应 接下来我们再回头看看收到响应时候的处理过程,由于网络层的处理逻辑都是一样的,所以我们从前面出现分歧的地方开始看,也就是processStream方法,当收到的数据类型是RESPONSE时,它会调用onStreamRecv,一起来看下:\nfunc (conn *streamConnection) onStreamRecv(ctx context.Context, cmd sofarpc.SofaRpcCmd) *stream { requestID := cmd.RequestID() conn.mutex.Lock() defer conn.mutex.Unlock() //通过requestID找到对应的阻塞的stream \tif stream, ok := conn.streams[requestID]; ok { delete(conn.streams, requestID) buffer.TransmitBufferPoolContext(stream.ctx, ctx) return stream } return nil } 该stream同样会走到handleCommand方法中的OnReceive,如下:\nfunc (w *clientStreamReceiverWrapper) OnReceive(ctx context.Context, headers types.HeaderMap, data types.IoBuffer, trailers types.HeaderMap) { w.stream.DestroyStream() w.streamReceiver.OnReceive(ctx, headers, data, trailers) } func (r *upstreamRequest) OnReceive(ctx context.Context, headers types.HeaderMap, data types.IoBuffer, trailers types.HeaderMap) { if r.downStream.processDone() || r.setupRetry { return } r.endStream() if code, err := protocol.MappingHeaderStatusCode(r.protocol, headers); err == nil { r.downStream.requestInfo.SetResponseCode(code) } r.downStream.requestInfo.SetResponseReceivedDuration(time.Now()) r.downStream.downstreamRespHeaders = headers if data != nil { r.downStream.downstreamRespDataBuf = data.Clone() data.Drain(data.Len()) } r.downStream.downstreamRespTrailers = trailers //唤醒downstream \tr.downStream.sendNotify() } 逻辑很简单,就是把根据requestID匹配到的stream唤醒,下面来看一下唤醒以后的处理逻辑, 这里需要回到前面阻塞的receive方法中,唤醒以后会从之前阻塞的地方开始继续执行,如下:\nfunc (s *downStream) receive(ctx context.Context, id uint32, phase types.Phase) types.Phase { for i := 0; i \u0026lt;= int(types.End-types.InitPhase); i++ { switch phase { case types.WaitNofity: //从这里醒来 \tif p, err := s.waitNotify(id); err != nil { return p } phase++ case types.UpFilter: s.runAppendFilters(phase, s.downstreamRespHeaders, s.downstreamRespDataBuf, s.downstreamRespTrailers) if p, err := s.processError(id); err != nil { return p } if s.upstreamRequest == nil { fakeUpstreamRequest := \u0026amp;upstreamRequest{ downStream: s, } s.upstreamRequest = fakeUpstreamRequest } phase++ case types.UpRecvHeader: //同样是在这里返回响应结果 \tif s.downstreamRespHeaders != nil { s.upstreamRequest.receiveHeaders(s.downstreamRespDataBuf == nil \u0026amp;\u0026amp; s.downstreamRespTrailers == nil) if p, err := s.processError(id); err != nil { return p } } phase++ case types.UpRecvData: if s.downstreamRespDataBuf != nil { s.upstreamRequest.receiveData(s.downstreamRespTrailers == nil) if p, err := s.processError(id); err != nil { return p } } phase++ case types.UpRecvTrailer: if s.downstreamRespTrailers != nil { s.upstreamRequest.receiveTrailers() if p, err := s.processError(id); err != nil { return p } } phase++ case types.End: return types.End default: return types.End } } log.Proxy.Errorf(s.context, \u0026#34;[proxy] [downstream] unexpected phase cycle time\u0026#34;) return types.End } 上面的receiveXXX方法会把响应数据转发给业务进程,之前分析过了,这里就不再赘述。\n协程池 前面在请求处理过程中提到了会把请求任务交给一个协程池去处理,这里就简单看一下 MOSN 中协程池的实现原理:\ntype workerPool struct { work chan func() sem chan struct{} } func NewWorkerPool(size int) WorkerPool { return \u0026amp;workerPool{ work: make(chan func()), sem: make(chan struct{}, size), } } 初始化过程很简单,协程池的默认大小为poolSize := runtime.NumCPU() * 256,接下来看一下调度方法的实现:\nfunc (p *workerPool) ScheduleAuto(task func()) { select { case p.work \u0026lt;- task: return default: } select { case p.work \u0026lt;- task: case p.sem \u0026lt;- struct{}{}: go p.spawnWorker(task) default: //如果有多余的任务,则会临时创建协程执行 \tutils.GoWithRecover(func() { task() }, nil) } } //额外创建出来的协程在执行完任务以后会自动退出 func GoWithRecover(handler func(), recoverHandler func(r interface{})) { go func() { //省略defer方法... \thandler() }() } func (p *workerPool) spawnWorker(task func()) { defer func() { if r := recover(); r != nil { log.DefaultLogger.Errorf(\u0026#34;[syncpool] panic %v\\n%s\u0026#34;, p, string(debug.Stack())) } //整个函数退出时,协程数量减1,后面可以再创建出来 \t//这里正常情况下下面的死循环是不会退出的,也就是说基础协程一旦创建就不会被回收 \t\u0026lt;-p.sem }() for { //执行任务 \ttask() //如果还有任务等待执行,则循环执行任务,否则等待 \ttask = \u0026lt;-p.work } } 总结 MOSN 对于数据的处理及转发这块非常复杂,主要是概念很多,尤其是stream部分,对象之间互相引用,错综复杂,考虑到篇幅原因,本文只说明了流程,其他比如路由策略的细节等需要通过其他文章进行分析。\n","excerpt":"基本概念 MOSN 中的概念比较多,以sofarpc-sample下面的config.json为例,结合上图依次看下:\n Downstream:调用端的数据流向统称。 Upstream:服务端的数据流 …","ref":"https://mosn.io/blog/code/mosn-eventloop/","title":"MOSN 源码解析 - 协程模型"},{"body":"前言 在2019年5月,CNCF 筹建通用数据平面API工作组(Universal Data Plane API Working Group / UDPA-WG),以制定数据平面的标准API。\n当时我写了一个博客文章 “CNCF正在筹建通用数据平面API工作组,以制定数据平面的标准API” 对此进行了介绍。当时 UDPA 还处于非常早期的筹备阶段,信息非常的少。\n现在9个月过去了,我最近收集并整理了一下 UDPA 目前的情况和信息,给大家介绍一下 UDPA 目前最新的进展(截止2020年2月24日)。\nUDPA介绍 首先快速介绍一下什么是 UDPA:\n UDPA :“Universal Data Plane API”的缩写, “通用数据平面API”。 UDPA-WG:”Universal Data Plane API Working Group”的缩写,这是CNCF下的一个工作组,负责制定 UDPA。 UDPA的目标,援引自 https://github.com/cncf/udpa 的描述:\n 通用数据平面API工作组(UDPA-WG)的目标是召集对数据平面代理和负载均衡器的通用控制和配置API感兴趣的业界人士。\n UDPA的愿景,同样援引:\n 通用数据平面API(UDPA)的愿景在 https://blog.envoyproxy.io/the-universal-data-plane-api-d15cec7a 中阐明。 我们将寻求一组API,它们为L4/L7数据平面配置提供事实上的标准,类似于SDN中L2/L3/L4的OpenFlow所扮演的角色。\n这些API将在proto3中规范定义,并通过定义良好的 稳定API版本控制策略,从现有的Envoy xDS API逐步演进。 API将涵盖服务发现,负载均衡分配,路由发现,监听器配置,安全发现,负载报告,运行状况检查委托等。\n我们将对API进行改进和成型,以支持客户端 lookaside 负载均衡(例如gRPC-LB),Envoy之外的数据平面代理,硬件LB,移动客户端以及其他范围。 我们将努力尽可能与供应商和实现无关,同时坚持支持已投入生产的UDPA的项目(到目前为止,Envoy和gRPC-LB)。\n 对 UDPA 感兴趣的同学,可以通过以下两个途径进一步深入了解:\n UDPA @ GitHub:UDPA 在 github 上的项目,UDPA API 定义的代码都在这里 Universal Data Plane API Working Group (UDPA-WG):CNCF 的 UDPA 工作组,可以通过加入工作组的方式了解更多信息 UDPA和xDS的关系 在展开 UDPA 的细节之前,有必要先解释清楚 UDPA 和 xDS 的关系,因为这对理解 UDPA 会有很大帮助。\n在2019年11月的 EnvoyCon 上,Envoy 的 开发者,也是目前 UDPA 最主要的负责人之一,来自 Google 的 Harvey Tuch,有一个演讲非常详细而清晰的解答了这个问题,这个演讲的标题是:“The Universal Dataplane API (UDPA): Envoy’s Next Generation APIs”。\n 备注:这里我直接援引这份演讲的部分内容,以下两张图片均出自 [这份演讲的PPT](https://static.sched.com/hosted_files/envoycon2019/ac/EnvoyCon UDPA 2019.pdf) 。鸣谢 Harvey。\n 下图展示了近年来 xDS 协议的演进历程和未来规划:\n 2017年,xDS v2 引入 proto3 和 gRPC,同年 Istio 项目启动 2018和2019年,xDS v2 API继续发展,陆续引入了新的API定义,如 HDS / LRS / SDS 等,尤其是为了改进 Pilot 下发性能,开始引入增量推送机制 xDS v3 API 原计划于2019年年底推出,但目前看技术推迟,目前 v3 还是 alpha1 状态,预计在即将发布的 Istio 1.5 中会有更多的 v3 API 引入。同时 v3 API 也引入了 UDPA 的部分内容,但是由于 UDPA 目前进展缓慢,对 xDS 的影响并不大,主要还是 xDS 自身的发展,比如对API和技术债务的清理 但在2020年,预计 UDPA 会有很大的进展,尤其是下面我们将会展开的 UDPA-TP 和 UDPA-DM 的设计开始正式制定为 API 之后。而 xDS v4 预计将基于 UDPA ,因此 xDS v4 可能会迎来比较大的变动。 简单总结说:xDS 将逐渐向 UDPA 靠拢,未来将基于 UDPA 。\n下面的图片则展示了 Envoy 在 xDS 版本支持上的时间线:\n目前看这个计划在执行时稍微有一点点延误,原计划于2019年年底推出的 v3 的 stable 版本实际上是在1月中定稿的。(备注:具体可参考 Envoy PR api: freeze v3 API )。然后目前正在广泛使用的 v2 API 将被标记为 depreated。而且在2020年底,v3 API 预计被 v4 API 取代(注意v4 API 将会是基于 UDPA),而目前我们最熟悉的 v2 API 将计划在2020年底移除,不再支持!\n上图也展示了未来 xDS 协议的大版本演进和更替的方式,总的来说规律是这样:\n 一年一个大版本:2019 v2 -》 2020 v3 -》2021 v4 -》2022 v5 每个大版本都要经历 alpha -》 stable -》deprecated -》removed 四个阶段,每个阶段历时一年 稳定后 Envoy 会同时存在三个API大版本:正在使用的稳定版本,已经弃用的上一个稳定版本,准备开发的新的下一个大版本(但只会是Alpha) 发布一个新的 stable 的大版本,就一定会 deprecated 上一个稳定的大版本,同时 remove 更前一个已经 deprecated 的大版本 所谓 “长江后浪推前浪,前浪死在沙滩上”,又或者说,“江山代有新版出,各领风骚12个月”。\n 备注:Envoy 具体的稳定API版本控制策略,可以参见 Envoy 的设计文档 “Stable Envoy API versioning” ,不过这个文档长的有点过分,嫌长的同学可以直接看这个文档的缩减版本 API versioning guidelines。\n UDPA API进展 言归正传,我们来看一下 UDPA 目前的最新进展。\n从 https://github.com/cncf/udpa ,可以看到目前 UDPA 中已经定义好的部分 API 内容:\n类型定义 目前只定义了一个类型 TypedStruct。\nTypedStruct包含任意 JSON 序列化后的 protocol buffer 消息,以及一个描述序列化消息类型的URL。 这与 google.protobuf.Any 非常相似,它使用 google.protobuf.Struct 作为值,而不是使用 protocol buffer 二进制。\nmessage TypedStruct { // 用于唯一标识序列化 protocol buffer 消息的类型的URL // 这与 google.protobuf.Any 中描述的语义和格式相同: // https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/any.proto string type_url = 1; // 上述指定类型的JSON表示形式。 google.protobuf.Struct value = 2;}TypedStruct 定义的背景是:如何在 protocol buffer 的静态类型报文中嵌入一个不透明的配置?这是一个普遍需求,涉及 google.protobuf.Any 和 google.protobuf.Struct 的差别和权衡使用。具体内容请见我之前翻译的博客文章 “[译] 动态可扩展性和Protocol Buffer”,对此做了非常好的介绍和分析讨论。\nTypedStruct 可以说是到目前为止对此需求的最佳实践,算是为这一话题正式画上了句号。\n数据定义 数据定义也只定义了一个数据 OrcaLoadReport。\n其中 ORCA 是 Open Request Cost Aggregation 的缩写,OrcaLoadReport 用于提交请求开销汇总的负载报告。\nORCA的简短介绍:\n 如今,在Envoy中,可以通过考虑后端负载(例如CPU)的本地或全局知识来做出简单的负载均衡决策。更复杂的负载均衡决策可能需要借助特定于应用的知识,例如队列深度,或组合多个指标。\n这对于可能在多个维度上受到资源限制的服务(例如,CPU和内存都可能成为瓶颈,取决于所应用的负载和执行环境,无法确定是哪个先触及瓶颈),以及这些维度不在预定类别中的位置时很有用(例如,资源可能是“池中的可用线程数”,磁盘IOPS等)。\n 有关 Orca 的更详细的信息,请见设计文档 Open Request Cost Aggregation (ORCA) 。\n目前 Envoy 正在实现对 ORCA 的支持,然后这个特性被作为 UPDA 标准的一部分直接在 UDPA API 中定义。\n以下为 OrcaLoadReport 定义,可以看到包含有CPU/内存的利用率和RPS信息:\nmessage OrcaLoadReport { // CPU利用率表示为可用CPU资源的一部分。 应该来自最新的样本或测量。 double cpu_utilization = 1 [(validate.rules).double.gte = 0, (validate.rules).double.lte = 1]; // 内存利用率表示为可用内存资源的一部分。 应该来自最新的样本或测量。 double mem_utilization = 2 [(validate.rules).double.gte = 0, (validate.rules).double.lte = 1]; // 端点已服务的总RPS。 应该涵盖端点负责的所有服务。 uint64 rps = 3; ...}服务定义 服务定义依然也只定义了一个服务 OpenRcaService。\nOpenRcaService是一个带外(Out-of-band/OOB)负载报告服务,它不在请求路径上。OpenRcaService定期以足够的频率对报告进行采样,以提供与请求的关联。 OOB报告弥补了带内(in-band)报告的局限性。\nservice OpenRcaService { rpc StreamCoreMetrics(OrcaLoadReportRequest) returns (stream udpa.data.orca.v1.OrcaLoadReport);}注解定义 UDPA目前定义了四个注解(Annotation):\n MigrateAnnotation: 用于标记在前后版本中的和迁移相关的API变更,包括 rename / oneof_promotion / move_to_package 等多种语义。 SensitiveAnnotation:将某个字段标记为“敏感”字段,例如个人身份信息,密码或私钥 StatusAnnotation:标记状态,比如将某个文件标记为“work_in_progress/进行中” VersioningAnnotation:用于记录版本信息,比如通过 previous_message_type 表示当前message在上一个版本中的类型 还有一个 ProtodocAnnotation 在提出设计后,存在分歧,暂时还没有正式加入 UDPA。这个注解的目的是标记当前尚未实现的UDPA消息。\nUDPA API总结 从上面列出的UDPA API列表可以看到,目前 UDPA 中正式推出的API 内容非常的少,也就:\n 一个TypedStruct类型定义 一个OpenRcaService服务定义和配套的OracLoadReport数据定义 4个注解 考虑到UDPA推出的时间是 2019年5月份,迄今有9个月的时间,这个进展有些出乎意料。\n翻了一遍 https://github.com/cncf/udpa 上的内容,包括所有的 commit 和 PR ,发现活跃的开发者主要是两位同学:Google 的 htuch 和 Tetrate公司的 Lizan。然后 cncf/UDPA 项目的 star 数量也非常低,才 55 个 star,可以认为社区基本上没什么人关注。\n但是,稍后当我看到 UDPA 的设计文档时,才发现原来 UDPA 的精华都在设计中,只是进度原因还未能正式出成型的API。\nUDPA设计 我们来重点看一下 UDPA 的设计,主要的设计文档有两份:\n UDPA-TP strawman: UDPA设计之传输协议(TransPort),是用于UDPA的下一代传输协议 UDPA-DM: L7 routing strawman: UDPA设计之数据模型(Data Model),是对通过 UDPA-TP 传输的资源定义 UDPA对此的解释是:\n Envoy v2 xDS API 当前正在转向通用数据平面API(Universal Dataplane API/UDPA)。重点是传输协议与数据模型的关注点分离。\n 关于传输协议与数据模型的关注点分离,一个典型的例子是“集装箱运输机制”(类比 UDPA-TP )和 “集装箱中标准规格”(类比 UDPA-DM)。在 UDPA 的设计中,数据模型的定义和传输协议的实现是分离的,这意味着只要设计不同的数据模型,就可以重用一套统一的传输协议。因此,UDPA 的可扩展性就变得非常强大。\n对此,我个人有些惊喜,因为去年年底我和彦林同学在商讨通过 MCP/xDS/UDPA 协议融合注册中心和控制平面时,就发现这三者的工作机制非常类似。考虑到后续可能会有各种不同的资源需要定义并在这个工作机制上做资源同步和分发,当时有过类似的想法,希望能把底层这套资源同步机制标准化,以便重用:\n目前看来,UDPA-TP 已经在朝这个目标迈出了坚实的步伐。当然如果能再往前迈进一步就更好了:这个底层资源同步的工作机制,没有必要限制在 UDPA 的范畴,完全可以变成一个用途更加广泛的通用机制。\n下面来详细看一下 UDPA-TP 和 UDPA-DM 的设计,以下内容来自 UDPA 的两份设计文档以及个人的解读。\nUDPA-TP设计 UDPA-TP的设计文档,在开始部分列出了 UDPA-TP 的关键设计动机,具体包括:\n 在保留 Core v2 xDS 中存在的概念性pub-sub模型的同时,还支持高级功能,例如LRS/HDS。 支持Envoy Mobile和其他DPLB客户端的大规模扩展 简单的客户端和简单的管理服务器实现。 使增量更新资源高效而简单。(备注:增量更新是目前 Istio/Envoy 正在努力实现的重点特性) 支持资源联邦。(备注:和我之前构想的通过MCP协议聚合多个注册中心/配置中心的思路一致,当现实中存在多个资源的来源时,就必须提供机制来聚合这些资源并提供一个全局的视图) 使API资源的子资源变得简单且实现成本低,并且使其可以增量更新和可联合 维护对一致性模型的支持 消除v2 xDS奇怪之处: ACK/NACK与订阅消息的合并。在v2 xDS中,DiscoveryRequest既是订阅请求,又是对先前消息的潜在确认。这导致了一些复杂的实现和调试体验。(备注:这会造成 UDPA 交互模式和xDS的不同) CDS/LDS是与EDS/RDS不同的API层。在v2 xDS中,EDS/RDS是准增量的,而CDS/LDS是最新状态。 从概念上讲,在Envoy v2 xDS API 基础上小范围变更。我们不希望对UDPA管理服务器实现者造成重大的概念和实现开销。(备注:个人理解,这应该是目前 UDPA 没有大张旗鼓的制定各种API的原因,UDPA 和 xDS,包括和 xDS 的实际实现者 Envoy 的关系过于紧密,因此需要考虑从 xDS 到 UDPA 的过渡,不能简单的推出一套全新的API) 以下是 UDPA 的术语,对于后面理解 UDPA 非常有帮助:\n DPLB:data plane load balancer的缩写。涵盖诸如代理(例如Envoy)或客户端RPC库(例如gRPC和Envoy Mobile)的用例。 DPLB是UDPA客户端。他们负责启动到管理服务器的UDPA流。(备注:注意,DPLB不仅仅包含以Envoy为代表的service mesh sidecar,也包括了以SDK形式存在的类库如 gRPC,而 gRPC 目前已经在实现 对 xDS 接口的支持) Management server/管理服务器:能够提供UDPA服务的实体。管理服务器可以仅是UDPA服务器,也可以是UDPA客户端和服务器(在联邦的情况下)。 UDPA:通用数据平面API,其中包括数据模型(UDPA-DM)和传输协议(UDPA-TP)。 UDPA-TP:UDPA API的基准传输协议。 UDPA-DM:UDPA API的数据模型。 UaaS:UDPA-as-a-service,云托管的UDPA管理服务。(备注:Google在 GCP 上提供的 Google Traffic Director 和 AWS 提供的 App Mesh,可以视为就是 UaaS 雏形) 然后是和联邦相关的术语:\n Federation/联邦:多个UDPA管理服务器的互操作以生成UDPA配置资源。 Direct federation/直接联邦:当DPLB直接连接到多个UDPA管理服务器并能够从这些流中合成其配置资源时。 Indirect federation/间接联邦:当DPLB一次最多连接一个UDPA管理服务器(对于某些给定的资源类型),并且UDPA管理服务器执行所有与联邦有关的工作时。 Advanced DPLB/高级DPLB:支持直接联邦的DPLB(需要UDPA-Fed-TP)。 Simple DPLB/简单DPLB:不支持直接联邦的DPLB,即仅基线UDPA-TP。 UDPA-Fed-DM:UDPA-DM的超集,具有用于联邦的其他资源类型。 UDPA-Fed-TP:支持联邦的UDPA-TP的超集。 下图可以帮助理解这些术语:\n(备注:图中可能有误,simple UDPA management server 和 Advanced UDPA management server 之间应该是 “UDPA-TP”, 而不应该是 “UDPA-Fed-TP”。)\nUDPA-TP 传输协议提供了在管理服务器和 DPLB 客户端之间传输命名和版本化资源的方法。我们称这些实体为 UDPA-TP 端点。 UDPA-TP 端点可以是客户端和管理服务器,也可以是两个管理服务器(例如,在联邦配置时)。 上图说明了使用 UDPA 在 UDPA-TP 端点之间传送资源的各种方式。\n其中,UDPA管理服务器分为两种:\n 简单(simple):实际上只是对不透明资源的缓存(几乎不了解UDPA-DM),主要功能是从上游高级UDPA管理服务器获取资源,并分发资源给 DPLB,自身不产生资源。 高级(advanced):对 UDPA-DM 有感知,通常是用来获取信息并转换为标准化的资源(典型如 Istio 中的 Pilot)。可以直接分发资源给到 DPLB,也可以发送资源给简单UDPA管理服务器,然后由简单UDPA管理服务器再分发给 DPLB。 而对应的 DPLB 客户端也分为两种:\n 简单(simple):对于任何配置源,简单的DPLB在任何时间点最多只能与一台管理服务器进行对话。 高级(advanced):将实现对 UDPA-Fed-TP 的支持,并能够直接作为联邦端点参与。 简单 DPLB 虽然只能连接一台管理服务器,但是也是可以实现联邦的:简单 DPLB 连接的管理服务器可以实现联邦,然后为 DPLB 实现了间接联邦。 (备注:上图中的简单 DPLB就是例子,两个 Advanced UDPA management server 之间做了联邦)\nUDPA-TP设计解读 在解读 UDPA-TP 的设计之前,我们回顾一下 Istio 经典的组件和架构图,下面分别是 Istio 1.0 和 Istio 1.1 的架构图:\n考虑到 Mixer 和 Citadel 两个组件和 UDPA 的关系相比没那么大, 我们重点看 Proxy / Pilot / Galley:\n Proxy在 Istio 中就是 Envoy (我们的MOSN正在努力成为候选方案) ,直接对等 UDPA 中的 DPLB 的概念。但是Istio 中的 Proxy,功能上只和上图中的 Simple DPLB 对齐。相比之下,UDPA-TP 设计中增加了一个能够连接多个 UDPA management server进行联邦的 Advanced DPLB。 Pilot 和 Galley,从和 DPLB 的连接关系看,分别对应着UDPA-TP 设计中的 Simple UDPA management server 和 Advanced UDPA management server。但从实际实现的功能看,目前Pilot的职责远远超过了 Simple UDPA management server 的设计,而 Galley 的功能则远远少于 Advanced UDPA management server。如果未来 Istio 的架构和组件要向 UDPA 的设计靠拢,则显然 Pilot 和 Galley 的职责要发生巨大调整。 最大的差异还是在于 UDPA-TP 中引入了联邦的概念,而且同时支持在 DPLB 和 Advanced UDPA management server 上做联邦。尤其是 DPLB 要实现联邦功能,则必然会让 DPLB 的功能大为增加,相应的 DPLB 和 UDPA management server 的通讯协议 (目前是xDS)也将为联邦的实现增加大量内容。 对照 UDPA-TP 的设计:\nUDPA-TP的设计目前应该还没有对应的具体的实现产品,而且我也还没有找到 UDPA-Fed-TP 的详细的API设计。资料来源太少,所以只能简单的做一些个人的初步解读:\n 首先,Simple UDPA management server 的引入是一大亮点,功能足够简单而且专注,聚焦在将数据(在UDPA中体现为资源)分发给 DPLB (如大家熟悉的数据平面)。毫无疑问,Simple UDPA management server 的重点必然会在诸如 容量 / 性能 / 下发效率 / 稳定性 等关键性能指标,弥补目前 Istio 设计中 Pilot 下发性能赢弱的短板。从这个角度说,我倾向于将 Simple UDPA management server 理解为一个新的组件,介于 DPLB 和 Pilot 之间。\n小结:解决容量和性能问题。\n 其次,Advanced UDPA management server 引入了联邦的概念, 上面的图片显示是为了在两个不同的云供应商(Cloud Provider X 和 Cloud Provider Y)和本地(On-premise)之间进行联邦,这是典型的混合云的场景。而我的理解是,联邦不仅仅用于多云,也可以用于多数据来源,比如打通多个不同的注册中心,解决异构互通问题。\n小结:解决多数据来源的全局聚合问题。\n 然后,比较费解的是引入了 Advanced DPLB 的概念,而且从图上看,使用场景还非常复杂:1. 第一个DPLB是间接联邦的典型场景,还比较简单 2. 第二个 DPLB除了以同样的方式做了间接联邦,还直接通过 UDPA-Fed-TP 协议和 On-Premise 的 Advanced UDPA management server 连接,实现了直接联邦 3. 第三个 DPLB 则更复杂,做了三个management server的联邦 。\n小结:复杂的场景必然带来复杂的机制,背后推动力待查\n 对于 UDPA-TP 的设计,我个人有些不太理解,主要是对于联邦的使用场景上,我的疑虑在于:真的有这么复杂的场景吗?尤其是将联邦的功能引入到 DPLB,这必然会使得 xDS/UDPA 协议不得不为此提供联邦 API 支持,而 Envoy/MOSN 等的实现难度也要大为提升。因此,除非有特别强烈的需求和场景推动,否则最好能在复杂度和功能性之间做好平衡。\n我个人更倾向于类似下面的设计:\n 维持 DPLB 和 Simple UDPA management server 的简单性 DPLB 只对接 Simple UDPA management server,协议固定为 UDPA-TP,联邦对DPLB透明(只实现间接联邦) Simple UDPA management server 重点在于容量和性能的提升,包括目前正在进行中的支持各种资源的增量更新。 Advanced UDPA management server 负责完成联邦,包括和其他环境的Advanced UDPA management server做联邦,以及从本地的其他注册中心聚合数据。 总之,我个人更倾向于让 DPLB 和 Simple UDPA management server 保持简单和高性能,而将复杂功能交给 Advanced UDPA management server 。后续我会重点关注 UDPA 在联邦功能的实现,如有新进展尽量及时撰文分享。\nUDPA的资源定义和设计 在 UDPA-TP 的设计中,根据资源的来源,资源被划分为两类类型:\n Configuration Resources/配置资源:控制平面生成,由管理服务器分发到 DPLB。平常大家熟悉的,通过xDS协议传输的 Listener / Route / Cluster / Endpoint 等资源就属于这种类型。 Client Resources/客户资源:DPLB生成,准备交给控制平面使用。前面 UDPA 中定义的 OracLoadReport 就是典型例子,这是最近才有的新特性,由 DPLB 收集统计信息和状态,汇报给到控制平面,以便控制平面在决策时可以有多完善的参考信息。 资源在定义时,将有三个重要属性:\n 名称/Name:对于给定类型是唯一的,而且资源名称是结构化的,其路径层次结构如下:/ ,例如 com.acme.foo/service-a 。注意 namespace 的引入,是后面的资源联邦和所有权委派的基础。 类型/Type:资源类型由 Type URL 提供的字符串来表示。 版本/Version:对于给定的命名资源,它可能在不同的时间点具有不同的版本。在资源定义中带上版本之后,检测资源是否有更新就非常方便了。 以下是资源定义的实例(只是示意,暂时还没正式成为 UDPA API的内容):\nmessage Resource { // 资源的名称,以区别于其他同类型的资源。 // 遵循反向DNS格式,例如 com.acme.foo/listener-a string name = 1; // 资源级别版本 string version = 2; // 资源有效负载。 // 通过 Any 的 type URL 指定资源类型 google.protobuf.Any resource = 3; // 资源的TTL。 // 此时间段后,资源将在DPLB上失效。 // 当管理服务器的连接丢失时,将支持资源的优雅降级,例如端点分配。 // 使用新的TTL接收到相同的资源 name/version/type 将不会导致除了刷新TTL之外的任何状态更改。 // 按需资源可能被允许过期,并且可能在TTL过期时被重新获取。 // TTL刷新消息中的resource字段可能为空,name/version/type用于标识要刷新的资源。 google.protobuf.Duration ttl = 4; // 资源的出处(所有权,来源和完整性)。 Provenance origin_info = 5;}UDPA-TP 设计中的其他内容,如安全 / 错误处理 / 传输 / 用户故事 等,就不一一展开了,这些设计目前看离正式成为 API 还有点远。如有兴趣可以直接翻阅 UDPA-TP 的设计文档。\nUDPA-DM设计 UDPA 设计的一个核心内容就是将传输(TransPort)与数据模型(Date Model)的关注点分离,前面介绍了 UDPA-TP 的设计,可以说目前还在进行中,并未完全定型。\n而 UDPA-DM 的设计,感觉进度上比 UDPA-TP 还要更早期,这多少有点出乎意料:原以为 UDPA 会基于 xDS 现有的成熟 API 定义,快速推出一套覆盖常见通用功能的 API ,甚至直接把 xDS 中的部分内容清理干净之后搬过来。但事实是:目前 UDPA-DM 中已经定义的 API 内容非常少,仅有 L7 Routing ,而且还在设计中,其他大家熟悉的 Listener / Cluster / Endpoint / Security / RatingLimit 等API都还没有看到。\n而 UDPA-DM 的设计和实现方式,也因为资料较少而有些不够明朗。在 UDPA-DM 的设计文档的开头,有如下一段描述:\n As a starting point, we recognize that the UDPA-DM is not required to be as expressive as any given DPLB client’s full set of capabilities, instead it should be possible to translate from UDPA-DM to various DPLB native configuration formats. We envisage UDPA-DM as a lingua franca that captures a large amount of useful functionality that a DPLB may provide, making it possible to build common control planes and ecosystems around UDPA capable DPLBs.\n首先,我们认识到 UDPA-DM 不需要像任何已有的 DPLB 客户端那样,全部能力都具备表现力,而是应该可以从 UDPA-DM 转换为各种 DPLB 原生配置格式。我们将 UDPA-DM 设想为一种通用语言,它具备大量 DPLB 应该提供的有用的功能,从而有可能在支持 UDPA 的 DPLB 周围构建通用的控制平面和生态系统。\nThe UDPA-DM will be a living standard. We anticipate that it will initially cover some obvious common capabilities shared by DPLBs, while leaving other behaviors to proxy specific API fields. Over time, we expect that the UDPA-DM will evolves via a stable API versioning policy to accommodate functionalities as we negotiate a common representation.\nUDPA-DM 将成为事实标准。我们期望它最初将涵盖 DPLB 共有的一些显而易见的通用功能,同时将其他行为留给代理特定的API字段。随着时间的推移,我们期望 UDPA-DM 将通过稳定的API版本控制策略来发展,以容纳各种功能,而我们将协商通用的表示形式。\n 对这两段文字描述的理解,我是有一些困惑的,主要在清楚解 UDPA-DM 的定义和具体的 DPLB 原生实现(典型如 Envoy 的 xDS)之间的关系。下面这张图是我画的:\n 左边的图是转换方案:按照第一段文字的描述,UDPA-DM 是做通用定义,然后转换(Translate)为各种 DPLB 的原生配置格式。这意味着 UDPA-DM API 和实际的 DPLB 原生配置格式可能会有非常大的不同,甚至有可能是完全不一样的格式定义。这个关系有点类似 Istio API 和 xDS API 的关系,也类似于 SMI (微软推出的 Service Mesh Interface)和 xDS 的关系。 右边的图是子集方案:按照第二段文字的描述,UDPA-DM 是做通用定义,但是不会做转换,其他 DPLB 会直接复用这些 UDPA-DM 的 API 定义,然后补充自身特有的 API 定义。 这样 UDPA-DM 会以通用子集的形式出现,逐渐扩大范围,然后其他 API 会逐渐将自身的 API 中和 UDPA-DM 重叠的部分替换为 UDPA-DM API,只保留自身特有的扩展API。 前面谈到 xDS 的演进路线, v3 / v4 会逐渐向 UDPA 靠拢,尤其 v4 会基于 UDPA 来。目前由于 UDPA API 远未成型,而 xDS v3 中对 UDPA API 的使用非常少(基本只用到了 annotation 定义),因此目前到底是哪个方案尚不明朗。\n以下是 UDPA-DM 设计文档中描述的 UDPA-DM 的关键设计:\n 描述L7路由层,该层描述主要 L7 DPLB 之间的常见行为,包括 Envoy,HAproxy,Nginx,Google Cloud Platform URL mapping 和 Linkerd。 为代理/LB的特有扩展提供灵活性,用于不适合通用抽象的行为。 即 逃生舱口(escape hatch)(备注:类似SQL 方言) 提供L7路由表的可伸缩性和有效的对数评估(logarithmic evaluation)。 例如,Envoy v2 xDS是严格线性评估的路由表,具有明显的扩展限制。 对于可以支持 UDPA-TP 这个特性的DPLB,应该可以按需获取路由表段。 在v2 Envoy xDS API中支持线性匹配路由表的旧有用户。 删除多xDS样式API的需求,例如 RDS,VHDS和SRDS。 资源的可组合性; 应该有可能支持UDPA联邦用例,带有诸如虚拟主机这种被独立管理的资源等 下面重点看 L7 Routing 的设计。\nRouting API 设计 Routing API 中有三个术语:\n 服务/Service:描述后端服务的不透明字符串(或在Envoy的术语中,为集群/Cluster)。 当前文档未提供有关服务表示的任何进一步详细说明,这留待以后的工作。 路由表/Route table:一组匹配条件,用于HTTP请求和相关操作。 操作可以包括重定向或转发到特定服务。 L7路由/L7 routing:根据路由表评估给定的HTTP请求。 在 UDPA-DM 的 Routing API 设计中,针对请求匹配的方式,相比 xDS 做了重大的改动,主要体现在除了线性匹配之外,还支持分层匹配。\n这里先解释一下线性匹配和分层匹配这两种路由时需要用到的请求匹配方式:\n 线性(Linear):其中路由表类似于[([Match], Action)] 类型。 在此模型中,路由表是 匹配 criteria-action 条件的有序列表。 每个匹配的 criteria 是匹配 criteria 的逻辑”与”,例如 If :authority == foo.com AND path == / AND x-foo == bar THEN route to X If :authority == bar.com AND x-baz == wh00t THEN route to Y 分层(Hierarchical):其中路由表类似于树结构,每个节点具有(MatchCriteria, [(Value, Action)]) 类型。 通过这种结构,任何给定的节点都会只评估单个匹配条件,例如:authority 的值。分层匹配能提供相对高效的实现。 目前 xDS v2 API使用的是线性匹配方式,而 UDPA-DM 的 Routing API 会引入分层匹配,形成线性-分层的混合匹配方式,如下图所示:\n设计文档对这个混合匹配模型有如下说明:\n The model does not map directly to any given DPLB today but borrows from some Envoy concepts and should have an efficient realization. It may be possible to use HAproxy ACLs to model this topology.\n目前该模型并没有直接映射到任何给定的DPLB,而是借鉴了Envoy的一些概念,这个模型应该会有一个有效的实现。可能会使用HAproxy ACL对这种拓扑进行建模。\n 由于目前 Routing API 并没有完成设计,也没有正式成为 UDPA 的API,而在最新刚定稿的 xDS v3 协议中,RoutesDiscoveryService 和 ScopedRoutesDiscoveryService 也都没有引入这个新的模型,因此预期这个模型将在2020年继续完成设计和定稿,可能在年底的 xDS v4 中会有所体现。然后,UDPA-DM 和 xDS 之间到底会是转换模型,还是子集模型,届时就清楚了。\n由于 Routing API 尚未设计完成,所以这里不详细展开 Routing API 的定义了。Routing API 的相关资料如下,有兴趣的同学可以关注(然而已经很长时间没有新的进展了):\n Issue 讨论 [WiP] L7 routing straw man 在 PR 中提交的 Routing API 定义文件 udpa/config/v1alpha/routing.proto:可以自行和xDS v3 的API 做一个比较 总结 UDPA 目前还处于早期设计阶段,关键的 UDPA-TP 和 UDPA-DM 的设计有推出草稿但是远未完成,内容也和我们期望的一个完整的通用数据平面API有很长的距离。\n而且项目进展并不理想,感觉重视程度和人力投入都有限。\n附言 最近因为想了解一下 UDPA 的进展,所以做了 UDPA 的调研和学习,比较遗憾的是 UDPA 的资料非常匮乏,除了我本文列出来的几个官方网站和设计文档之外,基本就只有 Harvey 的演讲。\n调研完成之后发现 UDPA 的进展不如人意,尤其是最近的工作几乎停滞,关键的 UDPA-TP 和 UDPA-DM 的设计未能完稿,xDS v3 中也只引用了极少的 UDPA API 定义。这篇总结文章差点因此难产,因为未知/待定/未完成的内容太多,而且由于缺乏资料输入,很多信息也只是我个人的理解和想法,按说这不是一个严谨的深度介绍文章应有的态度。\n但考虑到目前 UDPA 的资料实在是太少,本着“有比没有好”的想法,我硬着头皮完成了这篇文章。后续如果有新的输入,我会及时完善或者修订本文,也欢迎对 UDPA 有兴趣和了解的同学联系我讨论和指导。\n参考资料 https://github.com/cncf/udpa :UDPA在 github 的项目,API 定义来自这里 Universal Data Plane API Working Group (UDPA-WG): UDPA设计文档,但是内容很少 Universal Data Plane API Working Group (UDPA-WG):CNCF 下的 UDPA 工作组,加入之后能看到一些资料 UDPA-TP strawman: UDPA-TP的设计文档 UDPA-DM: L7 routing strawman: UDPA-DM的设计文档 Stable Envoy API versioning :Envoy 官方文档,讲述 Envoy 的稳定API版本控制策略 CNCF正在筹建通用数据平面API工作组,以制定数据平面的标准API:我去年写的 UDPA 介绍文章 The Universal Dataplane API (UDPA): Envoy’s Next Generation APIs:Harvey Tuch 的演讲,帮助理解 xDS 和 UDPA 的关系 ","excerpt":"前言 在2019年5月,CNCF 筹建通用数据平面API工作组(Universal Data Plane API Working Group / UDPA-WG),以制定数据平面的标准API。\n当时我 …","ref":"https://mosn.io/blog/posts/udpa-follow-up/","title":"UDPA 最新进展深度介绍"},{"body":"本文记录了对 MOSN 的源码研究 - MOSN 的共享内存模型。\n本文的内容基于 MOSN v0.9.0,commit id b2a239f5。\n机制 MOSN 用共享内存来存储 metrics 信息。MOSN 用 mmap 将文件映射到内存,在内存数组之上封装了一层关于 metrics 的存取逻辑,实现了 go-metrics 包的关于 metrics 的接口,通过这种方式组装出了一种基于共享内存的 metrics 实现供 MOSN 使用。\n创建共享内存:Mmap 操作共享内存的方法主要在 pkg/shm/shm.go 文件下:\nfunc Alloc(name string, size int) (*ShmSpan, error) { ... return NewShmSpan(name, data), nil } func Free(span *ShmSpan) error { Clear(span.name) return syscall.Munmap(span.origin) } func Clear(name string) error { return os.Remove(path(name)) } 都是围绕着 ShmSpan 结构体的几个操作方法。再来看 ShmSpan 结构体:\ntype ShmSpan struct { origin []byte // mmap 返回的数组 \tname string // span 名, 创建时指定 data uintptr // 保存 mmap 内存段的首指针 \toffset int // span 已经使用的字节长度 \tsize int // span 大小 } Alloc 方法按照给定的 name 参数,在配置文件的目录下创建文件,并执行 sync.Mmap,其文件尺寸即 size 参数大小。Mmap 过后,将信息保存在 ShmSpan结构内返回。\n代码逻辑比较简单,大家可以自行阅读:https://github.com/mosn/mosn/blob/b2a239f5/pkg/shm/shm.go#L28\n由此看出,一个 ShmSpan 可以看做是一个共享内存块。\n下面我们将会分析共享内存块在 MOSN 里的使用场景:metrics。\n操作共享内存:配置 在分析如何通过共享内存存取 metrics 之前,首先看这相关的功能是如何配置的。\nhttps://github.com/mosn/mosn/blob/b2a239f5/pkg/mosn/starter.go#L318\nfunc initializeMetrics(config v2.MetricsConfig) { // init shm zone \tif config.ShmZone != \u0026#34;\u0026#34; \u0026amp;\u0026amp; config.ShmSize \u0026gt; 0 { shm.InitDefaultMetricsZone(config.ShmZone, int(config.ShmSize), store.GetMosnState() != store.Active_Reconfiguring) ... 从这里看出,通过读取配置文件的 ShmZone 和 ShmSize 来初始化共享内存,即配置文件的以下两个字段是控制着共享内存的文件名和大小的:\n{ ... \u0026#34;metrics\u0026#34;: { ... \u0026#34;shm_zone\u0026#34;: \u0026#34;文件名\u0026#34;, \u0026#34;shm_size\u0026#34;: \u0026#34;共享内存文件大小\u0026#34; }, ... } 操作共享内存:metrics metrics 相关的逻辑在 pkg/metrics 包下。\n上文说的 ShmSpan 是保存共享内存信息的结构体,而要理解 MOSN metrics 对共享内存的使用,还要先理解 MOSN 封装的几个结构体:zone、hashSet 和 hashEntry。\ntype zone struct { span *shm.ShmSpan ref *uint32 set *hashSet // mutex + ref = 64bit, so atomic ops has no problem } ... type hashSet struct { entry []hashEntry meta *meta slots []uint32 } ... type hashEntry struct { metricsEntry next uint32 // Prevents false sharing on widespread platforms with \t// 128 mod (cache line size) = 0 . \tpad [128 - unsafe.Sizeof(metricsEntry{})%128 - 4]byte } 这几个结构体与 ShmSpan 的关系大致是这样的:\nShmSpan 是共享内存块,而 zone、hashSet 和 hashEntry 对 ShmSpan 进行了划分:\n hashSet 封装出了 metrics name 映射到 metrics value 的哈希表 hashEntry 是哈希表的值,也是 metrics 值的保存的共享内存空间 zone 对 ShmSpan 进行了划分,划分出了一个 int32 值作为互斥锁;一个 int32 值作为 zone 的引用计数;也划分出了一片空间保存 hashSet 以上步骤做好后,创建一个 metrics 就可以通过创建对应的哈希 key value,拿到对应的共享内存地址,存取 metrics 信息。\n 实现小细节\n hashEntry 内部有一个 pad 字段,其作用是保持 hashEntry 结构体大小是 128 的倍数,避免 false sharing 结构体内的字段顺序和大小是调整过的,比如 hashEntry.value、zone.set,目的是满足原子操作的内存对齐 下面是源码步骤,大家可以自行跟踪调试:\n1) 创建 zone:\nhttps://github.com/mosn/mosn/blob/b2a239f5/pkg/metrics/shm/zone.go#L81\nfunc newSharedMetrics(name string, size int) (*zone, error) { alignedSize := align(size, pageSize) // 申请 ShmSpan \tspan, err := shm.Alloc(name, alignedSize) if err != nil { return nil, err } // 1. mutex and ref \t// 从 span 里取 4 个字节做互斥锁 \tmutex, err := span.Alloc(4) if err != nil { return nil, err } // 从 span 里取 4 个字节做引用计数 \tref, err := span.Alloc(4) if err != nil { return nil, err } zone := \u0026amp;zone{ span: span, mutex: (*uint32)(unsafe.Pointer(mutex)), ref: (*uint32)(unsafe.Pointer(ref)), } // 2. hashSet \t// 划分哈希表过程 // assuming that 100 entries with 50 slots, so the ratio of occupied memory is \t// entries:slots = 100 x 128 : 50 x 4 = 64 : 1 \t// so assuming slots memory size is N, total allocated memory size is M, then we have: \t// M - 1024 \u0026lt; 65N + 28 \u0026lt;= M // 计算 slot 的数量和内存占用大小 \tslotsNum := (alignedSize - 28) / (65 * 4) slotsSize := slotsNum * 4 // 计算 entry 数量和内存占用大小 \tentryNum := slotsNum * 2 entrySize := slotsSize * 64 // 哈希表内存大小 = entry 内存占用 + 20 字节 + slot 内存占用大小 \thashSegSize := entrySize + 20 + slotsSize hashSegment, err := span.Alloc(hashSegSize) if err != nil { return nil, err } // if zones\u0026#39;s ref \u0026gt; 0, no need to initialize hashset\u0026#39;s value \t// 初始化哈希表结构 \tset, err := newHashSet(hashSegment, hashSegSize, entryNum, slotsNum, atomic.LoadUint32(zone.ref) == 0) if err != nil { return nil, err } zone.set = set // add ref \tatomic.AddUint32(zone.ref, 1) return zone, nil } 这里可以大致说一下哈希表初始化的算法:首先 alignedSize 表示 4k 对齐后的 ShmSpan 大小,前 8 个字节被分配为互斥锁和引用计数, 另外 20 个字节被分配为哈希表的 meta 结构体,\nhttps://github.com/mosn/mosn/blob/b2a239f5/pkg/metrics/shm/hashset.go#L54\ntype hashSet struct { entry []hashEntry meta *meta slots []uint32 } ... type meta struct { cap uint32 size uint32 freeIndex uint32 slotsNum uint32 bytesNum uint32 } 所以真正能被分配为哈希表信息储存的空间 = 总空间 - 8 字节 - 20 字节。那能分配多少个哈希表信息呢?要看看 MOSN 的哈希表组织形式:entry 最开始首尾相连,后面会被组织成一个一个的 slot 链表供哈希碰撞时遍历查询。 所以 slot 和 entry 的比例控制着哈希表查找的性能:entry 比 slot 作为比例的话,比例越高意味着更容易碰撞,链表越长,查找性能下降;相反比例越低链表越短,查找性能越高,但是有越多 slot 闲置,空间会浪费。\n从注释看,MOSN 将比例写死为 2:1。假设 100 个 entry + 50 个 slot,其内存比等于 100 * 128(entry 内存占用):50 * 4 = 64:1,即一份 2:1 的 entry+slot 需要用到(64 + 1)* 4 个字节。\n所以,如果按照 2:1 来分配的话,一共可以分配的份数 = 哈希表信息储存空间 / 每一份空间占用 = (总空间 - 8 - 20) / (64 + 1) * 4 份。由此就可以算出 entry 和 slot 可以分配多少份了。\n2) 创建指标:\nhttps://github.com/mosn/mosn/blob/b2a239f5/pkg/metrics/shm/counter.go#L56\nfunc NewShmCounterFunc(name string) func() gometrics.Counter { return func() gometrics.Counter { if defaultZone != nil { if entry, err := defaultZone.alloc(name); err == nil { ... https://github.com/mosn/mosn/blob/b2a239f5/pkg/metrics/shm/zone.go#L166\nfunc (z *zone) alloc(name string) (*hashEntry, error) { z.lock() defer z.unlock() entry, create := z.set.Alloc(name) ... https://github.com/mosn/mosn/blob/b2a239f5/pkg/metrics/shm/hashset.go#L135\nfunc (s *hashSet) Alloc(name string) (*hashEntry, bool) { // 1. search existed slots and entries \t// 计算 hash 值作为 slot index \th := hash(name) slot := h % s.meta.slotsNum // name convert if length exceeded \tif len(name) \u0026gt; maxNameLength { // if name is longer than max length, use hash_string as leading character \t// and the remaining maxNameLength - len(hash_string) bytes follows \thStr := strconv.Itoa(int(h)) name = hStr + name[len(hStr)+len(name)-maxNameLength:] } nameBytes := []byte(name) // 查找链表找到对应的 entry \tvar entry *hashEntry for index := s.slots[slot]; index != sentinel; { entry = \u0026amp;s.entry[index] if entry.equalName(nameBytes) { return entry, false } index = entry.next } // 2. create new entry \t// 如果找不到, 创建新的 entry \tif s.meta.size \u0026gt;= s.meta.cap { return nil, false } // 创建新的 entry 从 hashset 的 meta 信息里拿 next free index \tnewIndex := s.meta.freeIndex newEntry := \u0026amp;s.entry[newIndex] newEntry.assignName(nameBytes) newEntry.ref = 1 if entry == nil { // 所以是链表头,保存 index 到 slot \ts.slots[slot] = newIndex } else { // 否则保存在上一个 entry 的 next 字段内 \tentry.next = newIndex } s.meta.size++ // 设置 next free index \ts.meta.freeIndex = newEntry.next // 设置队尾 \tnewEntry.next = sentinel return newEntry, true } 3) 用 Entry 保存 metrics 值\nhttps://github.com/mosn/mosn/blob/b2a239f5/pkg/metrics/shm/counter.go#L56\nfunc NewShmCounterFunc(name string) func() gometrics.Counter { return func() gometrics.Counter { if defaultZone != nil { if entry, err := defaultZone.alloc(name); err == nil { return ShmCounter(unsafe.Pointer(\u0026amp;entry.value)) } ... 可以看出,entry 的 value 是真正被用作记录 metrics 值的地方,它是一个 64 位的空间。\n为什么使用共享内存保存 metrics 看到这里你可能会问,为什么要这么辛苦封装共享内存来保存 metrics 值?为什么不直接使用堆空间来做呢?\n其实在源码里也有答案:\nhttps://github.com/mosn/mosn/blob/b2a239f5/pkg/mosn/starter.go#L318\nfunc initializeMetrics(config v2.MetricsConfig) { // init shm zone \tif config.ShmZone != \u0026#34;\u0026#34; \u0026amp;\u0026amp; config.ShmSize \u0026gt; 0 { shm.InitDefaultMetricsZone(config.ShmZone, int(config.ShmSize), store.GetMosnState() != store.Active_Reconfiguring) ... https://github.com/mosn/mosn/blob/b2a239f5/pkg/metrics/shm/zone.go#L58\nfunc createMetricsZone(name string, size int, clear bool) *zone { if clear { shm.Clear(name) } ... 如果 clear 为真,共享内存就会被清除,那么什么时候为假呢?当 store.GetMosnState() 方法返回 store.Active_Reconfigureing 的时候。即,当 MOSN reconfig 重启的时候, 已经保存的 metrics 是会被保留的。\n而且从这里可以看出:\nhttps://github.com/mosn/mosn/blob/b2a239f5/pkg/metrics/shm/zone.go#L124\nfunc newSharedMetrics(name string, size int) (*zone, error) { ... set, err := newHashSet(hashSegment, hashSegSize, entryNum, slotsNum, atomic.LoadUint32(zone.ref) == 0) ... https://github.com/mosn/mosn/blob/b2a239f5/pkg/metrics/shm/hashset.go#L78\nfunc newHashSet(segment uintptr, bytesNum, cap, slotsNum int, init bool) (*hashSet, error) { set := \u0026amp;hashSet{} ... 当 zone.ref 引用计数不为 0 的时候,哈希表里面的信息也是会被保留的。\n再来看 zone.mutex 互斥锁的使用:\nhttps://github.com/mosn/mosn/blob/b2a239f5/pkg/metrics/shm/zone.go#L136\nfunc (z *zone) lock() { times := 0 // 5ms spin interval, 5 times burst \tfor { if atomic.CompareAndSwapUint32(z.mutex, 0, pid) { return } ... 是通过设置进程 ID 来获取锁的,由此能看出 MOSN 的用意:这个以文件作为 mmap 的共享内存是可以被多个 MOSN 进程共用的。\n例如 MOSN 支持跨容器热重启的场景,基于内存共享的 metrics 可以保证热重启过程中不出现指标抖动而造成监控异常。 而且这个文件可以看作是一种文件格式,在任何时候都可以被持久化保存和提取分析使用的。\n当你不需要这个功能时,可以关闭内存共享 metrics 的配置即可,MOSN 会 fallback 到 go-metrics 的实现,该实现就是通过堆分配内存保存 metrics 信息。\n总结 本文通过分析 MOSN 源码,简述了 MOSN 的共享内存模型,分析了 MOSN 创建共享内存、配置 metrics 和 metrics 对共享内存块的使用。\n最后,不鼓励在 Go 里面使用共享内存,除非你有明确的使用场景,例如 MOSN 热升级场景下的 metrics 共享。\n 参考资料:\n MOSN 源码 ","excerpt":"本文记录了对 MOSN 的源码研究 - MOSN 的共享内存模型。\n本文的内容基于 MOSN v0.9.0,commit id b2a239f5。\n机制 MOSN 用共享内存来存储 metrics 信 …","ref":"https://mosn.io/blog/code/mosn-shm/","title":"MOSN 源码解析 - 共享内存模型"},{"body":"本文的目的是分析 MOSN 源码中的变量机制。\n什么是变量机制 我们通过一个单元测试来理解什么是变量机制,完整代码清参考这里:\n// DefaultAccessLogFormat provides a pre-defined format const DefaultAccessLogFormat = \u0026#34;%start_time% %request_received_duration% %response_received_duration% %bytes_sent%\u0026#34; + \u0026#34; \u0026#34; + \u0026#34;%bytes_received% %protocol% %response_code% %duration% %response_flag% %response_code% %upstream_local_address%\u0026#34; + \u0026#34; \u0026#34; + \u0026#34;%downstream_local_address% %downstream_remote_address% %upstream_host%\u0026#34; func TestAccessLog(t *testing.T) { registerTestVarDefs() format := types.DefaultAccessLogFormat logName := \u0026#34;/tmp/mosn_bench/benchmark_access.log\u0026#34; os.Remove(logName) accessLog, err := NewAccessLog(logName, format) ... ctx := prepareLocalIpv6Ctx() accessLog.Log(ctx, nil, nil, nil) ... } 执行这个单元测试,并查看日志内容:\ngo test mosn.io/mosn/pkg/log -run ^TestAccessLog$ ok mosn.io/mosn/pkg/log 2.867s cat /tmp/mosn_bench/benchmark_access.log 2020/02/16 21:26:26.078 2.543µs 2.000004307s 2048 2048 0 24.471µs false 0 127.0.0.1:23456 [2001:db8::68]:12200 127.0.0.1:53242 - 请留意 accessLog.Log 打印出来的日志,可以发现 DefaultAccessLogFormat 中的变量, 都被替换成了实际的值。比如:%start_time% 被替换成 2020/02/16 21:26:26.078, %request_received_duration% 被替换成 2.543µs。\n变量机制允许用户在配置中使用预定义的变量,而非确定值;变量将在请求处理过程中被替换成其对应的实际值。\n变量机制的原理 请留意上面的变量机制原理图,它分为 3 部分;最上边的是Variable 缓存,左边虚线框的是初始化阶段,而右边虚线是对变量的使用。\nVariable 缓存 Variable 缓存 是一组 Variable 的 key-value 对,key 是变量的名字,value 是 Variable 接口;以 map 的形式存在于内存中。Variable 接口含有变量的名字,变量值的获取方式, 变量值的设置方式 等属性。\n初始化阶段 在初始化阶段中,首先定义好各变量的 Variable(预定义变量);然后注册到 Variable 缓存中。另外,还需初始化 mosn ctx,mosn ctx 是一个 context.Context,里面含有变量实际值的相关信息;在后续获取变量的实际值时会用到它。\n对变量的使用 在请求处理的过程中,根据变量名去缓存中获取相应的 Variable。然后调用 Variable 的 Getter 方法,得到变量的获取方式 getter。最后调用 getter 函数,getter 函数会根据 mosn ctx 生成变量的实际值。\n可以看出,变量机制 用到了 生产者-消费者模式。生产者 负责定义变量对应的 Variable,并注册到 Variable 缓存中。而这些 Variable 会在请求处理过程中被消费,生成变量的实际值。\n了解完变量机制的原理后,我们继续围绕什么是变量机制中的单元测试,深入探讨它背后的实现逻辑。\n注册变量 该单元测试通过函数 registerTestVarDefs 进行变量的注册,代码片段如下,完整代码清参考这里:\nvar ( builtinVariables = []variable.Variable{ variable.NewBasicVariable(varStartTime, nil, startTimeGetter, nil, 0), variable.NewBasicVariable(varRequestReceivedDuration, nil, receivedDurationGetter, nil, 0), ... } prefixVariables = []variable.Variable{ variable.NewBasicVariable(reqHeaderPrefix, nil, requestHeaderMapGetter, nil, 0), variable.NewBasicVariable(respHeaderPrefix, nil, responseHeaderMapGetter, nil, 0), } ) func registerTestVarDefs() { // register built-in variables for idx := range builtinVariables { variable.RegisterVariable(builtinVariables[idx]) } // register prefix variables, like header_xxx/arg_xxx/cookie_xxx for idx := range prefixVariables { variable.RegisterPrefixVariable(prefixVariables[idx].Name(), prefixVariables[idx]) } } 通过 variable.NewBasicVariable 初始化各种 variable.Variable 的定义,再利用 variable.RegisterVariable 或 RegisterPrefixVariable 函数进行注册 接口 variable.Variable 的定义如下, 完整代码清参考这里:\ntype Variable interface { // variable name \tName() string // variable data, which is useful for getter/setter \tData() interface{} // variable flags \tFlags() uint32 // value getter \tGetter() GetterFunc // value setter \tSetter() SetterFunc } Name: 获取变量名 Data: 获取变量的实际值 Flags: 是否需要缓存的标志 Getter: 获取变量值的函数 Setter: 设置变量值的函数 再来看函数 variable.RegisterVariable, 完整代码清参考这里:\nvar ( // global scope mux sync.RWMutex variables = make(map[string]Variable, 32) // all built-in variable definitions indexedVariables = make([]Variable, 0, 32) // indexed variables ... ) func RegisterVariable(variable Variable) error { mux.Lock() defer mux.Unlock() name := variable.Name() // check conflict if _, ok := variables[name]; ok { return errors.New(errVariableDuplicated + name) } // register variables[name] = variable // check index if indexer, ok := variable.(Indexer); ok { index := len(indexedVariables) indexer.SetIndex(uint32(index)) indexedVariables = append(indexedVariables, variable) } return nil } 定义全局变量 variables 和 indexedVariables 由于使用了全局变量,为了防止竞态条件,使用了互斥锁 检查变量是否已经存在,如果已存在,返回错误;否则,变量正常注册到缓存 variables 中 将 variables 转化成 Indexer, 设置 index,并注册到缓存 indexedVariables 中 RegisterPrefixVariable 函数的功能和 RegisterVariable 大同小异,RegisterVariable 根据变量名注册,而 RegisterPrefixVariable 通过变量名的前缀注册。\n 变量注册大致的过程是:在代码初始化的时候,实例化所有定义好的变量(主要含有变量名,变量的获取方式 getter),并通过函数 RegisterVariable 或 RegisterPrefixVariable 注册到缓存(全局变量)中。\n获取变量 获取变量的原来则需要分析函数 NewAccessLog,完整代码清参考这里:\nfunc NewAccessLog(output string, format string) (api.AccessLog, error) { ... entries, err := parseFormat(format) if err != nil { return nil, err } l := \u0026amp;accesslog{ output: output, entries: entries, logger: lg, } ... } 解析字符串 format, 创建想要的变量,记录到变量条目 entries 里 将变量条目 entries 赋给结构体 accesslog 再来分析解析 format 的函数 parseFormat, 完整代码清参考这里:\nfunc parseFormat(format string) ([]*logEntry, error) { ... for pos, ch := range format { switch ch { case \u0026#39;%\u0026#39;: // check previous character, \u0026#39;\\\u0026#39; means it is escaped if pos \u0026gt; 0 \u0026amp;\u0026amp; format[pos-1] == \u0026#39;\\\\\u0026#39; { continue } // parse entry if pos \u0026gt; lastMark { if varDef { // empty variable definition: %% if pos == lastMark+1 { return nil, ErrEmptyVarDef } // var def ends, add variable varEntry, err := variable.AddVariable(format[lastMark+1 : pos]) if err != nil { return nil, err } entries = append(entries, \u0026amp;logEntry{variable: varEntry}) } else { // ignore empty text if pos \u0026gt; lastMark+1 { // var def begin, add text textEntry := format[lastMark+1 : pos] entries = append(entries, \u0026amp;logEntry{text: textEntry}) } } lastMark = pos } ... } } ... } 根据标识符 % 截取变量名 利用函数 variable.AddVariable 检查该变量名是否已经注册过了,并返回对应的变量 所有变量记录到切片 entries 中,一并返回 接下来分析函数 variable.AddVariable,完整代码清参考这里:\nfunc AddVariable(name string) (Variable, error) { mux.Lock() defer mux.Unlock() // find built-in variables if variable, ok := variables[name]; ok { return variable, nil } // check prefix variables for prefix, variable := range prefixVariables { if strings.HasPrefix(name, prefix) { return variable, nil ... } } return nil, errors.New(errUndefinedVariable + name) } 如果能在注册了变量的缓存 variables 找到 name 对应的变量,则直接返回变量 如果能在注册了变量的缓存 prefixVariables 找到变量名前缀是 name 的变量,则返回变量 没有找到想要的变量,则返回错误,改错误可以在程序启动的时候被发现 获取变量大致的流程是:format 解析中解析出变量名,然后去注册了变量的缓存 variables 或 prefixVariables 中获取相应的变量。\n变量生效 本小节将会分析方法 accessLog.Log 的逻辑,从而了解变量生效的过程,完整代码清参考这里:\nfunc (l *accesslog) Log(ctx context.Context, reqHeaders api.HeaderMap, respHeaders api.HeaderMap, requestInfo api.RequestInfo) { // return directly if l.logger.Disable() { return } buf := buffer.GetIoBuffer(AccessLogLen) for idx := range l.entries { l.entries[idx].log(ctx, buf) } buf.WriteString(\u0026#34;\\n\u0026#34;) l.logger.Print(buf, true) } 一些日志的处理逻辑 遍历变量条目 entries,执行 l.entries[idx].log,获取变量对应的实际值 再来分析方法 l.entries[idx].log, 完整代码清参考这里:\nfunc (le *logEntry) log(ctx context.Context, buf buffer.IoBuffer) { if le.text != \u0026#34;\u0026#34; { buf.WriteString(le.text) } else { value, err := variable.GetVariableValue(ctx, le.variable.Name()) if err != nil { buf.WriteString(variable.ValueNotFound) } else { buf.WriteString(value) } } } 主要是利用函数 variable.GetVariableValue 获取变量的实际值 将获取到的实际值写到 buf 里 接着分析函数 variable.GetVariableValue,完整代码清参考这里:\nfunc GetVariableValue(ctx context.Context, name string) (string, error) { // 1. find built-in variables if variable, ok := variables[name]; ok { // 1.1 check indexed value if indexer, ok := variable.(Indexer); ok { return getFlushedVariableValue(ctx, indexer.GetIndex()) } // 1.2 use variable.Getter() to get value getter := variable.Getter() if getter == nil { return \u0026#34;\u0026#34;, errors.New(errGetterNotFound + name) } return getter(ctx, nil, variable.Data()) } // 2. find prefix variables for prefix, variable := range prefixVariables { if strings.HasPrefix(name, prefix) { getter := variable.Getter() if getter == nil { return \u0026#34;\u0026#34;, errors.New(errGetterNotFound + name) } return getter(ctx, nil, name) } } return \u0026#34;\u0026#34;, errors.New(errUndefinedVariable + name) } 根据变量名,重新从缓存 variables 中获取相应的变量 variable 调用变量的 Getter 方法,获取函数 getter(GetterFunc),一个获取变量实际值的方法 执行 getter 函数,得到变量的实际值 接下来分析类型是 GetterFunc 的函数,以 startTimeGetter 为例,完整代码清参考这里:\n// StartTimeGetter // get request\u0026#39;s arriving time func startTimeGetter(ctx context.Context, value *variable.IndexedValue, data interface{}) (string, error) { info := ctx.Value(requestInfoKey).(api.RequestInfo) return info.StartTime().Format(\u0026#34;2006/01/02 15:04:05.000\u0026#34;), nil } 调用 ctx.Value, 根据 requestInfoKey 从 ctx 中获取相应的对象 该对象已经事先在这里,通过 ctx 的 WithValue 设置到了 ctx 里 转化成接口 api.RequestInfo,改接口有获取 StartTime 的能力 获取 StartTime 并格式化 变量生效大致的流程是:遍历已获取的变量条目 entries, 根据变量名获取缓存中的变量,调用变量中的 getter 方法, 获取相应的变量值。\n总结 变量机制 的实现是比较面向对象的,首先定义了结构体 Variable, 主要记录了变量名和获取对应变量值的实现GetterFunc;GetterFunc 定义好了获取变量值的函数定义,每个变量都得实现自己的 getter(面向接口编程)。 然后在代码初始化的时候把所有变量都实例化,并注册到作为缓存的全局变量 variables 中 (表驱动法); 最后,更加变量名去缓存 variables 获取相应的实现,获取变量的实际值。\n另外,很好地利用了 golang 特有的 context 贯穿了整个流程。\n","excerpt":"本文的目的是分析 MOSN 源码中的变量机制。\n什么是变量机制 我们通过一个单元测试来理解什么是变量机制,完整代码清参考这里:\n// DefaultAccessLogFormat provides a …","ref":"https://mosn.io/blog/code/mosn-variable/","title":"MOSN 源码解析 - 变量机制"},{"body":"本文的内容基于 MOSN v0.9.0,commit id 1609ae14。\nMOSN 在内存管理复用方面有 内存对象注册/管理 和 ByteBuffer/IOBuffer 复用 两部分内容。MOSN 最新的 master 分支用了 mod 管理依赖, 发现后一部分也迁移到了 vendor 目录下,可单独使用。下面就分这两部分来讲述 MOSN 的内存复用机制。\n机制 简述一下两部分内容的机制,具体实现原理会在后面带上源码解析。\n1. 内存对象注册/管理 MOSN 在 go sync 包外,对 sync.Pool 对象进行了进一步封装,增加了管理和易用性。\nMOSN 的 buffer 包提供了注册函数和统一的接口。将实现了接口的不同类型的 buffer 对象注册到 buffer 包, 在用到的时候通过 buffer 包导出的方法进行初始化和管理,增强了内存对象的管理。\n而易用性方面,MOSN 封装了 bufferValue 对象,管理上面初始化出来的对象,并且将 bufferValue 对象也进行了池化管理。在这之上,封装出方法 NewBufferPoolContext 和 PoolContext,使内部根据 context 传值的场景更加易用。MOSN 里面在不同协程协作(比如连接被协程1 accept 后, 交由 worker 协程2 进行 IO)的过程,会将必要参数使用内部实现的 context with value 机制进行传递, 其中 buffer 传递的方法就是通过上述封装的方法进行传递的。\n2. ByteBuffer/IOBuffer 复用 为了提高 byte 数组的复用率,MOSN 封装出了对齐64字节的 byte buffer pool 管理,以及在其之上的 IO buffer pool 管理包,内部需要用到的时候可以直接调用。\n之前这部分代码是放在 pkg 下的,在最新的 master 迁移到了 vendor 下,不依赖 pkg 包下任何的其他包。这种情况下如果开发者自己 的项目有这部分需求,其实也可以直接使用 MOSN 写好的包,不用重复造轮子。\n源码解析 1. 内存对象注册/管理 注册管理 这是 bufferPool 相关的简单类图。\nMOSN 定义了 bufferPoolCtx 接口,使用 buffer 包需要将实现了这个接口的对象,比如图中的 ABufferCtx、BBufferCtx,通过 RegisterBuffer 方法注册到 buffer 包。\n其中 Index() 方法返回注册时写入的 index 值;New() 方法是用来初始化待缓存对象的;而 Reest() 方法是将内存对象放回 pool 前的重置逻辑。\nhttps://github.com/mosn/mosn/blob/1609ae1441/pkg/buffer/buffer.go#L70\nRegisterBuffer(poolCtx types.BufferPoolCtx) { ... bPool[i].ctx = poolCtx setIndex(poolCtx, int(i)) ... 注册过程大致是将传入的对象保存在全局变量 bPool 中,并给它分配一个全局唯一标记。\n注册后的结构图大概是这样的:\nbPool 全局变量保存着已注册的 ctx, 在需要获取对象时找到对应的 pool,调用 ctx.New(),或 sync.Pool.Get(); 在需要 give 对象时,先调用 ctx.Reset() 方法对复用对象进行重置,然后调用 sync.Pool.Put(),至此实现了对 sync.Pool 的封装管理和扩展。\nhttps://github.com/mosn/mosn/blob/1609ae1441/pkg/buffer/buffer.go#L91\n// Take returns a buffer from buffer pool func (p *bufferPool) take() (value interface{}) { value = p.Get() if value == nil { value = p.ctx.New() } return } // Give returns a buffer to buffer pool func (p *bufferPool) give(value interface{}) { p.ctx.Reset(value) p.Put(value) } 易用性 然后是结构图右边的 valuePool 部分。valuePool 是 bufferValue 对象的 sync.Pool。我们先来看 valuePool 的结构:\nhttp://github.com/mosn/mosn/blob/1609ae1441/pkg/buffer/buffer.go#L105\n// bufferValue is buffer pool\u0026#39;s Value type bufferValue struct { value [maxBufferPool]interface{} transmit [maxBufferPool]interface{} } 其中 value/transmit 域用来保存从注册表初始化出来的内存对象的指针(transmit 域保存着从其他 context 复制过来的内存对象)。 全局变量 vPool 保存了 bufferValue 的 sync.Pool,即 bufferValue 本身也是可以复用的。\n这里为什么要一个 transmit 域和复制功能呢?可以从使用到的地方看到,在接收到 upstream response 的时候,因为还没解析 stream, goroutine 还不能知道对应的 downstream request 和其 context 的 bufferValue,这时需要分配一个 bufferValue 保存解析 stream 的信息, 等能够关联上的时候再拷贝到 transmit 域,等释放的时候统一释放。\nhttps://github.com/mosn/mosn/blob/1609ae1441/pkg/stream/http2/stream.go#L652\nfunc (conn *clientStreamConnection) handleFrame(ctx context.Context, i interface{}, err error) { ... mbuffer.TransmitBufferPoolContext(stream.ctx, ctx) 回到易用性的介绍,在使用时,通过 NewBufferPoolContext 方法新建一个 bufferValue:\nhttps://github.com/mosn/mosn/blob/1609ae1441/pkg/buffer/buffer.go#L112\n// NewBufferPoolContext returns a context with bufferValue func NewBufferPoolContext(ctx context.Context) context.Context { return mosnctx.WithValue(ctx, types.ContextKeyBufferPoolCtx, newBufferValue()) } // newBufferValue returns bufferValue func newBufferValue() (value *bufferValue) { // 从 vPool 里 get 复用的 bufferValue \tv := vPool.Get() if v == nil { value = new(bufferValue) } else { value = v.(*bufferValue) } return } 获取内存对象时,调用 PoolContext 方法获取 bufferValue 对象,传入注册表对象调用其 Find 方法,Find 方法会根据注册表对象获取对应的 pool,并且初始化一个内存对象放在 value 域里。\nhttps://github.com/mosn/mosn/blob/1609ae1441/pkg/buffer/buffer.go#L182\nPoolContext(ctx context.Context) *bufferValue { if ctx != nil { if val := mosnctx.Get(ctx, types.ContextKeyBufferPoolCtx); val != nil { return val.(*bufferValue) } } return newBufferValue() } https://github.com/mosn/mosn/blob/1609ae1441/pkg/buffer/buffer.go#L138\n(bv *bufferValue) Find(poolCtx types.BufferPoolCtx, x interface{}) interface{} { i := poolCtx.Index() if i \u0026lt;= 0 || i \u0026gt; int(index) { panic(\u0026#34;buffer should call buffer.RegisterBuffer()\u0026#34;) } if bv.value[i] != nil { return bv.value[i] } return bv.Take(poolCtx) } // Take returns buffer from buffer pools func (bv *bufferValue) Take(poolCtx types.BufferPoolCtx) (value interface{}) { i := poolCtx.Index() // 获取全局唯一标记 \tvalue = bPool[i].take() // 调用注册表获取对象 \tbv.value[i] = value // 放入 value \treturn } 使用完毕,只需调用 bufferValue 的 Give 方法,该方法会将其下管理的内存对象都归还到对应的 Pool 去,并且将自己归还到 vPool。\nhttps://github.com/mosn/mosn/blob/1609ae1441/pkg/buffer/buffer.go#L158\n// Give returns buffer to buffer pools func (bv *bufferValue) Give() { if index \u0026lt;= 0 { return } // first index is 1 \t// 归还 value \u0026amp; transmit \tfor i := 1; i \u0026lt;= int(index); i++ { value := bv.value[i] if value != nil { bPool[i].give(value) } value = bv.transmit[i] if value != nil { bPool[i].give(value) } } bv.value = nullBufferValue bv.transmit = nullBufferValue // Give bufferValue to Pool \t// 归还自己 \tvPool.Put(bv) } 使用场景 上述的方法会在哪里用到呢?\nMOSN 的请求处理是交给不同的 goroutine 来进行的,而请求上下文信息,如 host、header、body 等信息通过 context 来在不同的协程之间传递。 而内存复用 bufferValue 与 context 进行绑定,意味着在请求处理期间不同的协程都可以通过 context 获取到请求上下文信息。 所以,bufferValue 在请求 accept 时申请,在请求处理结束时释放。\nhttps://github.com/mosn/mosn/blob/1609ae1441/pkg/stream/http/stream.go#L338\nfunc newServerStreamConnection(ctx context.Context, connection api.Connection, ... // init first context \t// Next 方法会调用上文的 NewBufferPoolContext 方法 \tssc.contextManager.Next() ... 使用完毕,清理 downstream 时清理 bufferValue:\nhttps://github.com/mosn/mosn/blob/1609ae1441/pkg/proxy/downstream.go#L1325\nfunc (s *downStream) giveStream() { ... // Give buffers to bufferPool \tif ctx := mbuffer.PoolContext(s.context); ctx != nil { ctx.Give() } } 小结:MOSN 的 buffer 包保存了待复用的内存对象的注册表(bPool对象),用来对待复用对象的初始化和管理;另外,MOSN 定义了统一管理待缓存对象的结构:bufferValue,统一保存通过注册表初始化出来的对象。\n2. ByteBufer/IOBuffer 复用 ByteBuffer 先来看相关的结构体:\n// byteBufferPool is []byte pools type byteBufferPool struct { minShift int minSize int maxSize int pool []*bufferSlot } type bufferSlot struct { defaultSize int pool sync.Pool } 每个 slot 对应一种尺寸的 byteBuffer 的 pool,以及 defaultSize 域保存着尺寸。byteBufferPool 对象的 pool 域保存着多个 slot。\n再来看操作方法 GetBytes PutBytes,具体逻辑主要是操作 take 和 give 方法。这两个方法后面会分析。\nhttps://github.com/mosn/mosn/blob/1609ae1441/vendor/mosn.io/pkg/buffer/bytebuffer_pool.go#L28\nhttps://github.com/mosn/mosn/blob/1609ae1441/vendor/mosn.io/pkg/buffer/bytebuffer_pool.go#L145\n... // global bbPool var bbPool *byteBufferPool ... // GetBytes returns *[]byte from byteBufferPool func GetBytes(size int) *[]byte { return bbPool.take(size) } // PutBytes Put *[]byte to byteBufferPool func PutBytes(buf *[]byte) { bbPool.give(buf) } ... 为了提高复用率,当申请一个非 64 字节对齐尺寸的 byte buffer 时(如 200),MOSN 实际上会从 slot 2,即 defaultSize = 256 的 slot 返回对象,并返回切片 len = 200 的 byte 切片。\n初始化时,将 64、128、256\u0026hellip; 以此类推的尺寸的 byte slot 初始化到 byteBufferPool 的 pool 域内:\nhttps://github.com/mosn/mosn/blob/1609ae1441/vendor/mosn.io/pkg/buffer/bytebuffer_pool.go#L49\n// newByteBufferPool returns byteBufferPool func newByteBufferPool() *byteBufferPool { p := \u0026amp;byteBufferPool{ minShift: minShift, minSize: 1 \u0026lt;\u0026lt; minShift, maxSize: 1 \u0026lt;\u0026lt; maxShift, } for i := 0; i \u0026lt;= maxShift-minShift; i++ { slab := \u0026amp;bufferSlot{ // 通过左移算出 defaultSize = 64/128/256...等等 \tdefaultSize: 1 \u0026lt;\u0026lt; (uint)(i+minShift), } // 依次append \tp.pool = append(p.pool, slab) } return p } 使用时,根据尺寸算出对应的 slot,从对应的 slot 返回该尺寸的 byte 数组:\nhttps://github.com/mosn/mosn/blob/1609ae1441/vendor/mosn.io/pkg/buffer/bytebuffer_pool.go#L65\nfunc (p *byteBufferPool) slot(size int) int { // 比如要获取 200 size 的 buffer \tif size \u0026gt; p.maxSize { return errSlot } slot := 0 shift := 0 if size \u0026gt; p.minSize { // size - 199 \t// 位: 1100 0111,经过 8 次右移会\u0026lt;=0 \tsize-- for size \u0026gt; 0 { size = size \u0026gt;\u0026gt; 1 shift++ } // slot = 8 - 6 = 2, 该 slot 的 defaultSize = 256 \tslot = shift - p.minShift } return slot } https://github.com/mosn/mosn/blob/1609ae1441/vendor/mosn.io/pkg/buffer/bytebuffer_pool.go#L87\n// take returns *[]byte from byteBufferPool func (p *byteBufferPool) take(size int) *[]byte { slot := p.slot(size) if slot == errSlot { b := newBytes(size) return \u0026amp;b } // slot = 2, \tv := p.pool[slot].pool.Get() if v == nil { // 如果 slot get 方法没有返回, new 一个 \tb := newBytes(p.pool[slot].defaultSize) b = b[0:size] return \u0026amp;b } b := v.(*[]byte) // 调整切片长度为请求的 size \t*b = (*b)[0:size] return b } byte 数组使用完毕时,对应的就是将 byte 数组放回对应的 slot 里,这里比较好理解,各位可以自行看源码:\nhttps://github.com/mosn/mosn/blob/1609ae1441/vendor/mosn.io/pkg/buffer/bytebuffer_pool.go#L106\n这两处使用切片指针,主要考虑操作 sync.Pool 的 Get、Put 方法时避免参数拷贝问题。\nIOBuffer IOBuffer 及 IO buffer pool 就比较好理解了,主要是定义了与 IO 相关的接口,然后实现方法是基于上文 byte buffer 的使用方法的封装,即 read 是从 byte buffer 里读取、write 是将数据 copy 进 byte buffer。 有了上文的基础,这里大家可以根据源码去看具体的实现,并不难。\nhttps://github.com/mosn/mosn/blob/1609ae1441/vendor/mosn.io/pkg/buffer/types.go#L34\nIO 相关的接口:\ntype IoBuffer interface { Read(p []byte) (n int, err error) ReadOnce(r io.Reader) (n int64, err error) Write(p []byte) (n int, err error) WriteString(s string) (n int, err error) WriteTo(w io.Writer) (n int64, err error) ... } 总结 本文根据 MOSN 的源码分析了 MOSN 对内存复用的设计和用法,其基于 sync.Pool 之上封装了一层自己的注册管理逻辑,增强了管理能力、易用性和复用性。\n 参考资料:\n MOSN 源码 ","excerpt":"本文的内容基于 MOSN v0.9.0,commit id 1609ae14。\nMOSN 在内存管理复用方面有 内存对象注册/管理 和 ByteBuffer/IOBuffer 复用 两部分内 …","ref":"https://mosn.io/blog/code/mosn-buffer/","title":"MOSN 源码解析 - 内存复用机制"},{"body":"概述 Plugin 机制是 MOSN 提供的一种方式,可以让 MOSN 和一个独立的进程进行交互,这个进程可以用任何语言开发,只要满足 gRPC 的 proto 定义。\n为什么我们支持这个功能,跟我们遇到的一些业务场景有关:\n 比如 log 打印,在 io 卡顿的时候会影响 Go Runtime 的调度,导致请求延迟。我们需要把 log 独立成进程做隔离。 我们会有一些异构语言的扩展,比如 streamfilter 的实际逻辑是一个 Java 语言实现的。 我们需要快速更新一些业务逻辑,但不能频繁的去更新 MOSN 的代码。 作为类似 Supervisor 的管理工具,管理一些其他进程。 总结下来就是隔离性,支持异构语言扩展,模块化,进程管理等场景,大家也可以看看还有哪些场景可以用到。\n使用方法 examples/codes/plugin/pluginfilter/提供了一个使用示例,通过 streamfilter 把数据传递给一个独立进程处理并反馈。\n我们这儿简单看下pkg/plugin/example/:\nclient client, err := plugin.Register(\u0026#34;plugin-server\u0026#34;, nil) if err != nil { fmt.Println(err) return } response, err := client.Call(\u0026amp;proto.Request{ Body: []byte(\u0026#34;hello\u0026#34;), }, time.Second) } 通过 plugin.Register注册一个 plugin,name 需要和编译的二进制名字一样,然后函数会去启动这个程序,这个程序就是下面的 server 代码。 调用 client.Call 发送请求给 server 进程,然后收到响应并处理。 server type filter struct{} func (s *filter) Call(request *proto.Request) (*proto.Response, error) { if string(request.GetBody()) != \u0026#34;hello\u0026#34; { return nil, errors.New(\u0026#34;request body error\u0026#34;) } response := new(proto.Response) response.Body = []byte(\u0026#34;world\u0026#34;) response.Status = 1 return response, nil } func main() { plugin.Serve(\u0026amp;filter{}) } 首先实现 plugin.Service接口,Call 函数将接收 client 的请求,处理之后返回响应。 main函数执行plugin.Serve,运行服务监听 client 的请求。 运行 执行如下命令:\n$ go build -o plugin-client client/plugin.go $ go build -o plugin-server server/plugin.go $ ./plugin-client 2\u0026gt; /tmp/1 success! response body: world 初始化 MOSN 的 plugin 底层使用了github.com/hashicorp/go-plugin库,该库是 HashiCorp 公司提供的一个成熟的扩展系统,可以方便的扩展自己的插件机制。\n先看一下配置文件:\n\u0026#34;plugin\u0026#34;: { \u0026#34;log_base\u0026#34;: \u0026#34;/home/admin/mosn/logs/\u0026#34; } log_base plugin 传递给扩展进程的日志目录 在看一下 proto 定义,Request 和 Resonse 定义了几个通用的数据结构,在使用的时候可以选择使用,比如打印 log 就需要使用 Request 的 boy 字段。Call 方法就是我们需要实现的,来进行请求的发送和处理处理。\nsyntax = \u0026#34;proto3\u0026#34;;package proto;message Request {\tmap\u0026lt;string, string\u0026gt; header=1;\tmap\u0026lt;string, string\u0026gt; trailer=2;\tbytes body = 3;\tstring type = 4;}message Response {\tmap\u0026lt;string, string\u0026gt; header=1;\tmap\u0026lt;string, string\u0026gt; trailer=2;\tbytes body = 3;\tint32 status = 4;}service Plugin {\trpc Call(Request) returns (Response);}Client管理 client 的整个生命周期,用户使用该结构体发送请求。\n// Client is a plugin client, It\u0026#39;s primarily used to call request. type Client struct { pclient *plugin.Client config *Config name string fullName string service *client enable bool on bool sync.Mutex } Config初始化 Client 的时候使用,MaxProcs表示独立进程的 GOMAXPROCS 配置,Args表示独立进程的启动参数。\ntype Config struct { MaxProcs int Args []string } 启动 Register用于 client 端注册 plugin,参数name表示 server 的二进制名字,文件路径同于 client 的二进制路径。返回的 Client 用于管理整个 agent-client 的生命周期。\n// Register called by plugin client and start up the plugin main process. func Register(name string, config *Config) (*Client, error) { pluginLock.Lock() defer pluginLock.Unlock() if c, ok := pluginFactories[name]; ok { return c, nil } c, err := newClient(name, config) if err != nil { return nil, err } pluginFactories[name] = c return c, nil } newClient生成 Client,其中最重要的是Check方法,用于启动 server。\n 首先把GOMAXPROCS和日志路径通过环境变量传递给server进程。 plugin.NewClient开启 plugin 框架之后, pclient.Client()启动 server 子进程。 rpcClient.Dispense(\u0026quot;MOSN_SERVICE\u0026quot;) 返回真正的实例 client。 client 的整个启动过程就完成了,主要就是启动了 server 子进程,然后初始化了 client GRPC 的实例,用于请求发送和接收。\nfunc (c *Client) Check() error { . . cmd := exec.Command(c.fullName, c.config.Args...) procs := 1 if c.config != nil \u0026amp;\u0026amp; c.config.MaxProcs \u0026gt;= 0 \u0026amp;\u0026amp; c.config.MaxProcs \u0026lt;= 4 { procs = c.config.MaxProcs } cmd.Env = append(cmd.Env, fmt.Sprintf(\u0026#34;MOSN_PROCS=%d\u0026#34;, procs)) cmd.Env = append(cmd.Env, fmt.Sprintf(\u0026#34;MOSN_LOGBASE=%s\u0026#34;, pluginLogBase)) pclient := plugin.NewClient(\u0026amp;plugin.ClientConfig{ HandshakeConfig: Handshake, Plugins: PluginMap, Cmd: cmd, AllowedProtocols: []plugin.Protocol{ plugin.ProtocolGRPC}, }) rpcClient, err := pclient.Client() if err != nil { return err } raw, err := rpcClient.Dispense(\u0026#34;MOSN_SERVICE\u0026#34;) . . } 接下来是 server,server 端的代码也可以用其他语言实现,只要满足一定的规范,下面看看 Go 的实现:\n 首先执行checkParentAlive()主要是检查父进程(也就是 client )是否退出,如果退出了,自己也需要退出。 然后读取环境变量,来设置GOMAXPROCS。 最后调用plugin.Serve启动 GRPC Server 服务接收处理请求。 // Serve is a function used to serve a plugin. This should be ran on the plugin\u0026#39;s main process. func Serve(service Service) { checkParentAlive() p := os.Getenv(\u0026#34;MOSN_PROCS\u0026#34;) if procs, err := strconv.Atoi(p); err == nil { runtime.GOMAXPROCS(procs) } plugin.Serve(\u0026amp;plugin.ServeConfig{ HandshakeConfig: Handshake, Plugins: map[string]plugin.Plugin{ \u0026#34;MOSN_SERVICE\u0026#34;: \u0026amp;Plugin{Impl: service}, }, GRPCServer: plugin.DefaultGRPCServer, }) } server 最主要的就是实现Service接口,用于处理请求,之前的 exmple 就有一个简单的实现。\n// Service is a service that Implemented by plugin main process type Service interface { Call(request *proto.Request) (*proto.Response, error) } 执行 client 只需要执行Call函数就可以发送和接收请求,实现会先通过Check()来检查 server 是否健康,如果不健康会先启动 server,然后调用真正的实现。\n// Call invokes the function synchronously. func (c *Client) Call(request *proto.Request, timeout time.Duration) (*proto.Response, error) { if err := c.Check(); err != nil { return nil, err } return c.service.Call(request, timeout) } 首先设置 timeout 请求超时时间,然后在调用 GRPC 的接口发送请求。\nfunc (c *client) Call(request *proto.Request, timeout time.Duration) (*proto.Response, error) { var ctx context.Context var cancel context.CancelFunc if timeout \u0026gt; 0 { ctx, cancel = context.WithTimeout(context.Background(), timeout) defer cancel() } else { ctx = context.Background() } response, err := c.PluginClient.Call(ctx, request) return response, err } 在 server 端会直接执行我们实现的 Call 接口。\nfunc (s *server) Call(ctx context.Context, req *proto.Request) (*proto.Response, error) { return s.Impl.Call(req) } 管理 MOSN 提供了 HTTP 接口来查看 plugin 的运行状态,以及开启关闭 Plugin。\nUsage: /plugin?status=all /plugin?status=pluginname /plugin?enable=pluginname /plugin?disable=pluginname 在 disable 之后,server 就会被关闭,并且不会再启动,主要为了防止 server 有问题的时候可以关闭掉。\n总结 寄托于开源社区,我们方便的搭建了自己的扩展机制,MOSN 也把自己的想法反馈给社区,共同进步。\n拥抱开源,反哺开源。\n","excerpt":"概述 Plugin 机制是 MOSN 提供的一种方式,可以让 MOSN 和一个独立的进程进行交互,这个进程可以用任何语言开发,只要满足 gRPC 的 proto 定义。\n为什么我们支持这个功能,跟我们 …","ref":"https://mosn.io/blog/code/mosn-plugin/","title":"MOSN 源码解析 - Plugin 机制"},{"body":"本文的内容基于 MOSN v0.9.0。\nXDS用来与pilot-discovery通讯做服务发现功能。\nXDS是一类发现服务的总称,包含LDS, RDS, CDS, EDS以及SDS。\nMOSN通过XDS API可以动态获取Listener(监听器),Route(路由), Cluster(集群), Endpoint(集群成员)以及Secret(证书)配置。\nXDS的基本流程:Pilot-Discovery的Model -\u0026gt; XDS.pb -\u0026gt; GRPC -\u0026gt; XDS.pb -\u0026gt; MOSN的Model (GRPC包括序列化和网络传输)。\n配置文件\u0026amp;解析 if len(DynamicResources) \u0026gt; 0 \u0026amp;\u0026amp; len(StaticResources) \u0026gt; 0 进入XDS模式。\nXDS模式下的MOSN配置文件mosn_config.json:\n{ \u0026#34;dynamic_resources\u0026#34;: { \u0026#34;lds_config\u0026#34;: { \u0026#34;ads\u0026#34;: {} }, \u0026#34;cds_config\u0026#34;: { \u0026#34;ads\u0026#34;: {} }, \u0026#34;ads_config\u0026#34;: { \u0026#34;api_type\u0026#34;: \u0026#34;GRPC\u0026#34;, \u0026#34;cluster_names\u0026#34;: [\u0026#34;xxx\u0026#34;], \u0026#34;grpc_services\u0026#34;: [ { \u0026#34;envoy_grpc\u0026#34;: { \u0026#34;cluster_name\u0026#34;: \u0026#34;xds-grpc\u0026#34; } } ] } }, \u0026#34;static_resources\u0026#34;: { \u0026#34;clusters\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;xds-grpc\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;STRICT_DNS\u0026#34;, \u0026#34;lb_policy\u0026#34;: \u0026#34;RANDOM\u0026#34;, \u0026#34;hosts\u0026#34;: [ { \u0026#34;socket_address\u0026#34;: {\u0026#34;address\u0026#34;: \u0026#34;istio-pilot.istio-system.svc.boss.twl\u0026#34;, \u0026#34;port_value\u0026#34;: 15010} } ], \u0026#34;http2_protocol_options\u0026#34;: { } } ] } } 解析配置文件构建XDSConfig(XDS客户端的配置)。\n构建adsClient(XDS客户端)。\nfunc (c *Client) Start(config *config.MOSNConfig) error { log.DefaultLogger.Infof(\u0026#34;xds client start\u0026#34;) //解析配置文件 \tdynamicResources, staticResources, err := UnmarshalResources(config) if err != nil { log.DefaultLogger.Warnf(\u0026#34;fail to unmarshal xds resources, skip xds: %v\u0026#34;, err) return errors.New(\u0026#34;fail to unmarshal xds resources\u0026#34;) } //构建xdsConfig \txdsConfig := v2.XDSConfig{} err = xdsConfig.Init(dynamicResources, staticResources) if err != nil { log.DefaultLogger.Warnf(\u0026#34;fail to init xds config, skip xds: %v\u0026#34;, err) return errors.New(\u0026#34;fail to init xds config\u0026#34;) } //构建adsCLient \tstopChan := make(chan int) sendControlChan := make(chan int) recvControlChan := make(chan int) adsClient := \u0026amp;v2.ADSClient{ AdsConfig: xdsConfig.ADSConfig, StreamClientMutex: sync.RWMutex{}, StreamClient: nil, MosnConfig: config, SendControlChan: sendControlChan, RecvControlChan: recvControlChan, StopChan: stopChan, } adsClient.Start() c.adsClient = adsClient return nil } 初始化和启动xds连接 adsClient.start()\nfunc (adsClient *ADSClient) Start() { //构建grpc双向流连接。 \tadsClient.StreamClient = adsClient.AdsConfig.GetStreamClient() utils.GoWithRecover(func() { //认证和开启xds传输,并且设置定时重传 \tadsClient.sendThread() }, nil) utils.GoWithRecover(func() { //接受下发数据,并根据类型选择不同的handler处理 \tadsClient.receiveThread() }, nil) } 函数细节: https://github.com/mosn/mosn/blob/master/pkg/xds/v2/adssubscriber.go\nXDS消息处理和发送 四种类型处理器注册。\nfunc init() { RegisterTypeURLHandleFunc(EnvoyListener, HandleEnvoyListener) RegisterTypeURLHandleFunc(EnvoyCluster, HandleEnvoyCluster) RegisterTypeURLHandleFunc(EnvoyClusterLoadAssignment, HandleEnvoyClusterLoadAssignment) RegisterTypeURLHandleFunc(EnvoyRouteConfiguration, HandleEnvoyRouteConfiguration) } 接受数据类型,将XDS类型转换成MOSN数据类型,并且加入对应的manager。\n以HandlerListener为例:\nfunc HandleEnvoyListener(client *ADSClient, resp *envoy_api_v2.DiscoveryResponse) { log.DefaultLogger.Tracef(\u0026#34;get lds resp,handle it\u0026#34;) //解析返回消息,生成envoy_listener \tlisteners := client.handleListenersResp(resp) log.DefaultLogger.Infof(\u0026#34;get %d listeners from LDS\u0026#34;, len(listeners)) //转换envoy_listener为mosn_listener,并且加入ListenerAdapter \tconv.ConvertAddOrUpdateListeners(listeners) if err := client.reqRoutes(client.StreamClient); err != nil { log.DefaultLogger.Warnf(\u0026#34;send thread request rds fail!auto retry next period\u0026#34;) } } 函数细节: https://github.com/mosn/mosn/blob/master/pkg/xds/v2/default_handler.go\n请求与处理流程简单所示,也可接受pilot-discovery的推送:\nswitch(type): case: cluster: 接收cluster返回,根据clusterName请求Endpoints case: endpoint: 接收Endpoints,请求Listener case: listener 接受Listener,请求Routes case: router 接受Routes XDS类型转换 XDS.pd数据结构类型在: https://github.com/envoyproxy/go-control-plane\n收到数据并反序列化为XDS的Model之后,进行结构体转换。\n类型转换代码如下: https://github.com/mosn/mosn/blob/master/pkg/xds/conv/convertxds.go\n","excerpt":"本文的内容基于 MOSN v0.9.0。\nXDS用来与pilot-discovery通讯做服务发现功能。\nXDS是一类发现服务的总称,包含LDS, RDS, CDS, EDS以及SDS。\nMOSN通 …","ref":"https://mosn.io/blog/code/mosn-xds/","title":"MOSN 源码解析 - XDS"},{"body":"开始之前 在开始编写 MOSN 文档之前,首先需要你创建一个 MOSN 文档存储库,和熟悉 MOSN 的文档结构。\n页面类型 文档 系统化介绍 MOSN 使用的文档,由 MOSN 团队官方维护。\n博客 周期化发布的 MOSN 博客,来自社区贡献。\n新闻 关于 MOSN 社区的新闻信息。\n发布 MOSN 的新版本发布信息。\n文档结构 所有文档都位于 content 目录下,content/zh 为中文文档,content/en 为英文文档,要想在某一层级的文档下再创建一个新的文档需要先创建一个目录,并根据:\n 所有没有子目录的文档都以 index.md 命名。 所有包含子目录的文档都以 _index.md 命名。 文档元数据 每个文档都有元数据信息,元数据信息是介于两个 YAML 块之间通过 3 个“-”分割的信息。下面就是你必须填写的元数据信息:\ntitle:\u0026#34;标题\u0026#34;linkTitle:\u0026#34;标题\u0026#34;date:2020-02-11weight:1description:\u0026gt; 关于本页内容的简介。 以下是详细介绍:\n title:即本文章的标题。 linkTitle:显示在侧边栏的文档标题,一般写成跟 title 的内容一致即可。 date:该文档的创作日期,格式为 YYYY-MM-dd。 weight:在同一文档层级,weight 数字越小的文档在侧边栏中显示约靠前,对于非 docs 目录下的文章不需要设置。 description:对本文档的简介。 对于博客、发布、新闻文档,还需要填写作者信息:\nauthor:\u0026#34;作者信息\u0026#34;注意:作者信息的值支持 Markdown。\n文档命名 文档的 URL 是根据该篇文档所在的目录层级而确定的,文档的目录名称规范:\n 使用英文单词命名 不同的单词间使用连字符连接 不得出现其他标点符号 名称尽量简短 ","excerpt":"开始之前 在开始编写 MOSN 文档之前,首先需要你创建一个 MOSN 文档存储库,和熟悉 MOSN 的文档结构。\n页面类型 文档 系统化介绍 MOSN 使用的文档,由 MOSN 团队官方维护。 …","ref":"https://mosn.io/docs/open-source/contributing-documents/creating-pages/","title":"创建页面"},{"body":"要处理 MOSN 文档,您需要:\n 创建一个 GitHub 账户。 该文档是根据 Apache 2.0 协议许可发布的。\n如何贡献 您可以通过以下三种方式为 MOSN 文档做出贡献:\n 如果您想要编辑现有页面,可以在浏览器中打开页面,然后点击页面右侧的编辑本页选项,这将带您到 GitHub 页面进行编辑操作并提交相应的更改。 如果您想使用通用的方式,请遵循我们的如何添加内容中的步骤。 如果您想对现有的 pull request(PR)进行 review,请参考如何 review 中的步骤。 PR 合并后会立即显示在 https://mosn.io 上。\n如何添加内容 要添加内容,您必须创建存储库的分支,并从该分支向文档主存储库提交 PR。以下步骤描述了该过程:\n 访问 GitHub MOSN 官网仓库 https://github.com/mosn/mosn.io。 单击屏幕右上角的 Fork 按钮,以在您的 GitHub 帐户中创建 MOSN 官网仓库的副本。 克隆您的 fork 到本地,然后进行所需的任何更改。 当您准备将这些更改发送给我们时,请将更改推送到您的 fork 仓库。 进入 fork 仓库的索引页面,然后单击 New Pull Request 提交 PR。 如何 review 请直接在 PR 上发表评论。如果您评论的内容很详细,请按照以下步骤操作:\n 在 PR 中评论具体信息。如果可以的话,请在受影响的文件和文件行上直接评论特定的具体信息。 适当的时候,在评论中向 PR 提交者与参与者提供建议。 发布您的评论,与 PR 参与者分享您的评论和建议。 发布评论后,大家经过讨论一致同意合并 PR。 如何预览 您可以根据需要,选择在线预览,或者在本地使用 Hugo 命令行运行本站实时预览。\n在线预览 在提交 PR 后,GitHub 上对应的 PR 页面会显示一系列检查选项,其中 deploy/netlify 选项将会生成 MOSN 官网的预览页面,点击 Details 可以跳转到预览界面。对于同一个 PR 每次提交都会触发一次构建预览。\n这个是个临时网站,可以确保本次 PR 合并后的页面显示正常。\n本地预览 除了在页面上预览以外,您还可以使用 Hugo(建议使用 v0.55.5 extended 版本),在代码仓库的根目录下执行 hugo server,即可在浏览器中打开 http://localhost:1313 预览。\n","excerpt":"要处理 MOSN 文档,您需要:\n 创建一个 GitHub 账户。 该文档是根据 Apache 2.0 协议许可发布的。\n如何贡献 您可以通过以下三种方式为 MOSN 文档做出贡献:\n 如果您想要编 …","ref":"https://mosn.io/docs/open-source/contributing-documents/github/","title":"文档贡献指南"},{"body":"格式标准 必须使用 Markdown 格式编辑文档正文。 文档正文标题从二级标题开始。 图片使用本地图片,跟 index.md 文件放在同一个目录下,使用相对位置引用。 所有代码都需要指定代码语言。 中英文之间要加空格,如果句子末尾是英文则不需要。 请不要将有序列表和无序列表穿插混用,容易造成格式混乱。 对于直接出现的 URL 链接请使用 \u0026lt;URL\u0026gt; 包裹起来。 对于非通用词汇、代码中词组的引用请使用反括号包裹起来。 ","excerpt":"格式标准 必须使用 Markdown 格式编辑文档正文。 文档正文标题从二级标题开始。 图片使用本地图片,跟 index.md 文件放在同一个目录下,使用相对位置引用。 所有代码都需要指定代码语言。 …","ref":"https://mosn.io/docs/open-source/contributing-documents/style-guide/","title":"样式指南"},{"body":"本文的内容基于 MOSN v0.9.0。\n机制 使用过滤器模式来实现扩展是常见的设计模式,MOSN 也是使用了这种方式来构建可扩展性。\nMOSN 把过滤器相关的代码放在了 pkg/filter 目录下:\n➜ mosn git:(2c6f58c5) ✗ ll pkg/filter total 24 drwxr-xr-x 8 mac staff 256 Feb 5 08:52 . drwxr-xr-x 30 mac staff 960 Feb 5 08:52 .. drwxr-xr-x 3 mac staff 96 Aug 28 22:37 accept -rw-r--r-- 1 mac staff 2556 Feb 5 08:52 factory.go -rw-r--r-- 1 mac staff 2813 Feb 5 08:52 factory_test.go drwxr-xr-x 6 mac staff 192 Aug 28 22:37 network drwxr-xr-x 7 mac staff 224 Aug 28 22:37 stream -rw-r--r-- 1 mac staff 1248 Feb 5 08:52 types.go ➜ mosn git:(2c6f58c5) ✗ 包括 accept 过程的 filter,network 处理过程的 filter,以及 stream 处理的 filter。其中 accept filters 目前暂不提供扩展(加载、运行写死在代码里面,如要扩展需要修改源码), steram、network filters 是可以通过定义新包在 pkg/filter 目录下实现扩展。\n每一个 filter 包都会有一个 init 函数,拿 pkg/filter/network/proxy 包为例:\n... * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package proxy import ( ... ) func init() { filter.RegisterNetwork(v2.DEFAULT_NETWORK_FILTER, CreateProxyFactory) } ... 包被运行时会将 filter 注册到 filter factory 里,在必要时候,注册的回调函数(如例子里的 CreateProxyFactory)就会被调用。\n加载 \u0026amp; 运行 filters 下面来详细看看 filters 被加载和运行的过程。\n加载 我们可以看看,init 函数注册的回调函数是在什么时机被调用的:\nmosn.io/mosn/pkg/filter/factory.go L57:\n... // CreateNetworkFilterChainFactory creates a StreamFilterChainFactory according to filterType func CreateNetworkFilterChainFactory(filterType string, config map[string]interface{}) (types.NetworkFilterChainFactory, error) { if cf, ok := creatorNetworkFactory[filterType]; ok { ... ↓\nmosn.io/mosn/pkg/config/parser.go L372:\n... // GetNetworkFilters returns a network filter factory by filter.Type func GetNetworkFilters(c *v2.FilterChain) []types.NetworkFilterChainFactory { var factories []types.NetworkFilterChainFactory for _, f := range c.Filters { factory, err := filter.CreateNetworkFilterChainFactory(f.Type, f.Config) ... 可见,MOSN 在调用 config.GetNetworkFilters 会根据配置信息中 filter.Type 作为名字,在已注册的 filters 中找到需要加载的 filter,并返回 networkFilterChainFactory。 可以看到, filter.Type 其实就是 init 函数调用时,包调用 filter.RegisterNetwork 函数的第一个参数:filter 名。\n那么,配置信息又是从哪里来的呢?\n来源1: 配置文件。\n用户可以通过定义 MOSN 的 config.json,MOSN 启动时会根据 listener 指定的 filter 数组进行 filters 的初始化。\nhttps://github.com/mosn/mosn/blob/0.9.0/pkg/mosn/starter.go#L173\nfunc NewMosn(c *config.MOSNConfig) *Mosn { ... var nfcf []types.NetworkFilterChainFactory var sfcf []types.StreamFilterChainFactory // Note: as we use fasthttp and net/http2.0, the IO we created in mosn should be disabled // network filters if !lc.UseOriginalDst { // network and stream filters nfcf = config.GetNetworkFilters(\u0026amp;lc.FilterChains[0]) sfcf = config.GetStreamFilters(lc.StreamFilters) ... 来源2:XDS 信息解析。\nMOSN 里的 XDS client 会将 XDS resources 解析成 MOSN 的 config,当 downstream client 连接进来的时候根据 config 进行组装需要用到的 filters。\nhttps://github.com/mosn/mosn/blob/0.9.0/pkg/xds/conv/update.go#L71\n// ConvertAddOrUpdateListeners converts listener configuration, used to add or update listeners func ConvertAddOrUpdateListeners(listeners []*envoy_api_v2.Listener) { ... var streamFilters []types.StreamFilterChainFactory var networkFilters []types.NetworkFilterChainFactory if !mosnListener.UseOriginalDst { for _, filterChain := range mosnListener.FilterChains { nf := config.GetNetworkFilters(\u0026amp;filterChain) networkFilters = append(networkFilters, nf...) } streamFilters = config.GetStreamFilters(mosnListener.StreamFilters) ... 所以,filters 的配置主要是来源于配置文件。\n运行 filters 的配置是在 listener 之下的,配置解析会将每个 listener 配置声明要用到的 filters 初始化到 listener 实例里,在连接 accept 以及处理过程中,调用上文的函数初始化 filters 并调用。\nMOSN 会将连接的上下文信息放在 context 内传给 filter,并将 stream 传给 filter。下面是代码流程:\n(1) 连接 accept:\nhttps://github.com/mosn/mosn/blob/0.9.0/pkg/server/handler.go#L394\nfunc (al *activeListener) OnAccept(rawc net.Conn, useOriginalDst bool, oriRemoteAddr net.Addr, ch chan types.Connection, buf []byte) { ... arc.ContinueFilterChain(ctx, true) (2) 将连接上下文放入 context,并用以创建 filter chain,并初始化 filter manager,执行 filters 的 OnNewConnection 函数。 filter manager 是 filters 的代理,外部会在不同阶段调用 filter manager 的不同函数,filter manager 管理 filters 的执行逻辑:\nhttps://github.com/mosn/mosn/blob/0.9.0/pkg/server/handler.go#L394\nfunc (al *activeListener) OnAccept(rawc net.Conn, useOriginalDst bool, oriRemoteAddr net.Addr, ch chan types.Connection, buf []byte) { ... ctx := mosnctx.WithValue(context.Background(), types.ContextKeyListenerPort, al.listenPort) ctx = mosnctx.WithValue(ctx, types.ContextKeyListenerType, al.listener.Config().Type) ctx = mosnctx.WithValue(ctx, types.ContextKeyListenerName, al.listener.Name()) ctx = mosnctx.WithValue(ctx, types.ContextKeyNetworkFilterChainFactories, al.networkFiltersFactories) ctx = mosnctx.WithValue(ctx, types.ContextKeyStreamFilterChainFactories, \u0026amp;al.streamFiltersFactoriesStore) ctx = mosnctx.WithValue(ctx, types.ContextKeyAccessLogs, al.accessLogs) if rawf != nil { ctx = mosnctx.WithValue(ctx, types.ContextKeyConnectionFd, rawf) } if ch != nil { ctx = mosnctx.WithValue(ctx, types.ContextKeyAcceptChan, ch) ctx = mosnctx.WithValue(ctx, types.ContextKeyAcceptBuffer, buf) } if oriRemoteAddr != nil { ctx = mosnctx.WithValue(ctx, types.ContextOriRemoteAddr, oriRemoteAddr) } ... ↓\nhttps://github.com/mosn/mosn/blob/0.9.0/pkg/server/handler.go#L454\nfunc (al *activeListener) OnNewConnection(ctx context.Context, conn types.Connection) { //Register Proxy\u0026#39;s Filter filterManager := conn.FilterManager() for _, nfcf := range al.networkFiltersFactories { nfcf.CreateFilterChain(ctx, al.handler.clusterManager, filterManager) } filterManager.InitializeReadFilters() ... (3) 执行 filter 的 OnData 方法\nhttps://github.com/mosn/mosn/blob/0.9.0/pkg/network/connection.go#L428 -\u0026gt;\nhttps://github.com/mosn/mosn/blob/0.9.0/pkg/network/connection.go#L484\nfunc (c *connection) doRead() (err error) { ... c.onRead() ... func (c *connection) onRead() { ... c.filterManager.OnRead() 至此,filter 就实现了对连接的干预,filter 就像中间件,可以返回 type.Continue 控制连接继续进行, 也可以返回 type.Stop 停止连接继续处理。即可以对连接内容进行干预,比如在 static response 的场景, 返回既定的 response 内容;也可以对连接处理流程进行干预,比如在 fault injection 的场景,增加连接延时,等等。\nMOSN 实现了的 filters 根据文件目录我们可以看出 MOSN 目前实现了哪些 filter:\n➜ mosn git:(2c6f58c5) ✗ ll pkg/filter/network total 0 drwxr-xr-x 7 mac staff 224 Aug 28 22:37 . drwxr-xr-x 8 mac staff 256 Feb 9 15:49 .. drwxr-xr-x 3 mac staff 96 Feb 8 10:35 connectionmanager // 连接管理 drwxr-xr-x 4 mac staff 128 Feb 5 08:52 faultinject // 错误注入相关 drwxr-xr-x 3 mac staff 96 Feb 9 12:37 proxy // 代理逻辑, 这个 filter 目前是 MONS 里的逻辑一部分, 负责将 stream 发送到 serverStream, 开启流量的代理 drwxr-xr-x 6 mac staff 192 Feb 5 08:52 tcpproxy // tcp 代理逻辑 ➜ mosn git:(2c6f58c5) ✗ ll pkg/filter/stream total 0 drwxr-xr-x 7 mac staff 224 Aug 28 22:37 . drwxr-xr-x 8 mac staff 256 Feb 9 15:49 .. drwxr-xr-x 11 mac staff 352 Feb 5 08:52 commonrule // stream filter 逻辑公共部分, rule engine 等等 drwxr-xr-x 6 mac staff 192 Feb 5 08:52 faultinject // 错误注入相关 drwxr-xr-x 3 mac staff 96 Aug 28 22:37 healthcheck // upstream 健康检查相关 drwxr-xr-x 3 mac staff 96 Feb 5 08:52 mixer // mixer 逻辑相关 drwxr-xr-x 4 mac staff 128 Feb 5 08:52 payloadlimit // 限流相关 具体每个 filter 的逻辑都可以根据下述的 filter 结构,进行代码跟读和调试,分别理解每个 filter 的作用。\nfilter 包含的内容 \u0026amp; 如何扩展 MOSN filters 一个 filter 包含以下内容:\n init 函数,register 创建 filter manager 的回调函数 回调函数需要返回一个实现了 types.NetworkFilterChainFactory 接口的 struct 该 struct 的 CreateFilterChain 方法创建具体逻辑的实例,并调用 callback 参数的 addReadFilter | addWriteFilter 函数,进行 filter 注入到 filter manager 具体的业务逻辑 由此可见,通过 filter 机制扩展 MOSN,我们只需实现上述 4 样东西,与 MOSN 一同编译。再通过适当的 config 内容配置,或者与 XDS server 协作并生成含有你扩展的 filter 名及配置 (这部分需要修改 MOSN 源码,MOSN 与 XDS server 交互并生成 config,选用哪些 filter 目前是写死在代码里的),MOSN 就会在适当时候加载并运行你的 filter,对代理流量进行干预。\n总结 本文通过分析 MOSN 源码,简述了 MOSN 的filter扩展机制,并简述了实现自己的 filter 需要做的东西。大家可以通过该机制,使用 MONS 轻松 cover 自己具体的使用场景。\n 参考资料:\n MOSN 源码 ","excerpt":"本文的内容基于 MOSN v0.9.0。\n机制 使用过滤器模式来实现扩展是常见的设计模式,MOSN 也是使用了这种方式来构建可扩展性。\nMOSN 把过滤器相关的代码放在了 pkg/filter 目录 …","ref":"https://mosn.io/blog/code/mosn-filters/","title":"MOSN 源码解析 - filter扩展机制"},{"body":"前言 MOSN是基于Go开发的sidecar,用于service mesh中的数据面代理,建议先看下一个sidecar的自我修养 对sidecar 的基本概念、原理有所了解。\n手感 使用 MOSN 作为 HTTP 代理。\n通过设置 log level 为debug,代码中加更多日志来辅助分析代码。本文主要以http-example 为例来分析。\n基本使用 MOSN源码解析—配置详解。\nmosn examples codes http-example // mosn start -c config.json 即可启动mosn config.json // golang 实现的一个简单的http server,直接go run server.go 即可启动 server.go 使用http://localhost:8080 和 http://localhost:2345 都可以拿到数据。\n配置理解 对应config.json 的内容如下。\n{ \u0026#34;servers\u0026#34;: [ { \u0026#34;default_log_path\u0026#34;: \u0026#34;stdout\u0026#34;, \u0026#34;listeners\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;serverListener\u0026#34;, \u0026#34;address\u0026#34;: \u0026#34;127.0.0.1:2046\u0026#34;, \u0026#34;bind_port\u0026#34;: true, \u0026#34;log_path\u0026#34;: \u0026#34;stdout\u0026#34;, \u0026#34;filter_chains\u0026#34;: [ {} ] }, { \u0026#34;name\u0026#34;: \u0026#34;clientListener\u0026#34;, \u0026#34;address\u0026#34;: \u0026#34;127.0.0.1:2045\u0026#34;, \u0026#34;bind_port\u0026#34;: true, \u0026#34;log_path\u0026#34;: \u0026#34;stdout\u0026#34;, \u0026#34;filter_chains\u0026#34;: [ {} ] } ] } ], \u0026#34;cluster_manager\u0026#34;: {}, \u0026#34;admin\u0026#34;: {} } 单拎出来 admin 部分, MOSN 监听34901 端口。\n\u0026quot;admin\u0026quot;: { \u0026quot;address\u0026quot;: { \u0026quot;socket_address\u0026quot;: { \u0026quot;address\u0026quot;: \u0026quot;0.0.0.0\u0026quot;, \u0026quot;port_value\u0026quot;: 34901 } } } 访问http://localhost:34901/的返回结果。\nsupport apis: /api/v1/update_loglevel /api/v1/enable_log /api/v1/disbale_log /api/v1/states /api/v1/config_dump /api/v1/stats 代码结构 几乎所有的 interface 定义在 pkg/types 中,MOSN 基于四层架构实现(见下文),每一个 layer 在 types 中有一个 go 文件,在pkg 下有一个专门的文件夹。\n分层架构 一般的服务端编程,二级制数据经过协议解析为协议对应的model(比如HttpServletRequest) 进而交给上层业务方处理,对于 MOSN:\n 协议上数据统一划分为 header/data/Trailers 三个部分,转发也是以这三个子部分为基本单位。 借鉴了http2 的stream 的理念(所以Stream interface 上有一个方法是ID()),Stream 可以理解为一个子Connection,Stream 之间可以并行请求和响应,通过StreamId关联,用来实现在一个Connection 之上的“多路复用”。PS:为了连接数量与请求数量解耦。 代码的组织(pkg/stream,pkg/protocol,pkg/proxy) 跟上述架构是一致的。\n pkg/types/connection.go Connection. pkg/types/stream.go StreamConnection is a connection runs multiple streams. pkg/types/stream.go Stream is a generic protocol stream 一堆listener 和filter 比较好理解:Method in listener will be called on event occur, but not effect the control flow.Filters are called on event occurs, it also returns a status to effect control flow. Currently 2 states are used: Continue to let it go, Stop to stop the control flow. protocol 和 stream 两个layer 因和协议有关,不同协议之间实现差异很大,层次不是很清晰。 跨层次调用/数据传输通过跨层次struct 的“组合”来实现。也有一些特别的,比如http net/io 和 stream 分别启动goroutine read/write loop,通过共享数据来变相的实现跨层调用。 MOSN的核心概念解析。\n MOSN 在 IO 层读取数据,通过 read filter 将数据发送到 Protocol 层进行 Decode。 Decode 出来的数据,根据不同的协议,回调到 stream 层,进行 stream 的创建和封装。 stream 创建完毕后,会回调到 Proxy 层做路由和转发,Proxy 层会关联上下游(upstream,downstream)间的转发关系。 Proxy 挑选到后端后,会根据后端使用的协议,将数据发送到对应协议的 Protocol 层,对数据重新做 Encode。 Encode 后的数据会经过 write filter 并最终使用 IO 的 write 发送出去。 一个请求可能会触发多次读取操作,因此单个请求可能会多次调用插件的onData 函数。\n连接管理 该图主要说的连接管理部分。\n 不同颜色表示所处的 package 不同。 因为MOSN主要是的用途是“代理”, 所以笔者一开始一直在找代理如何实现,但其实呢,MOSN 首先是一个tcp server,像tomcat一样,MOSN 主要分为连接管理和业务处理两个部分。 业务处理的入口就是filterManager, 主要由filterManager.onRead 和 filterManager.onWrite 来实现。filterManager 聚合ReadFilter 链和WriterFilter链,构成对数据的处理。 Envoy 对应逻辑 深入解读Service Mesh的数据面Envoy。\n数据处理 一些细节:\n SOFAMesh中的多协议通用解决方案x-protocol介绍系列(1)-DNS通用寻址方案\niptables在劫持流量时,除了将请求转发到localhost的Sidecar处外,还额外的在请求报文的TCP options 中将 ClusterIP 保存为 original dest。在 Sidecar (Istio默认是Envoy)中,从请求报文 TCP options 的 original dest 处获取 ClusterIP。\n SOFAMesh中的多协议通用解决方案x-protocol介绍系列(2)-快速解码转发\n 转发请求时,由于涉及到负载均衡,我们需要将请求发送给多个服务器端实例。因此,有一个非常明确的要求:就是必须以单个请求为单位进行转发。即单个请求必须完整的转发给某台服务器端实例,负载均衡需要以请求为单位,不能将一个请求的多个报文包分别转发到不同的服务器端实例。所以,拆包是请求转发的必备基础。 多路复用的关键参数:RequestId。RequestId用来关联request和对应的response,请求报文中携带一个唯一的id值,应答报文中原值返回,以便在处理response时可以找到对应的request。当然在不同协议中,这个参数的名字可能不同(如streamid等)。严格说,RequestId对于请求转发是可选的,也有很多通讯协议不提供支持,比如经典的HTTP1.1就没有支持。但是如果有这个参数,则可以实现多路复用,从而可以大幅度提高TCP连接的使用效率,避免出现大量连接。稍微新一点的通讯协议,基本都会原生支持这个特性,比如SOFARPC,Dubbo,HSF,还有HTTP/2就直接內建了多路复用的支持。 我们可以总结到,对于Sidecar,要正确转发请求:\n 必须获取到destination信息,得到转发的目的地,才能进行服务发现类的寻址。 必须要能够正确的拆包,然后以请求为单位进行转发,这是负载均衡的基础。 可选的RequestId,这是开启多路复用的基础。 深入解读Service Mesh的数据面Envoy下文以Envoy 实现做一下类比用来辅助理解MOSN 相关代码的理念:\n对于每一个Filter,都调用onData函数,咱们上面解析过,其中HTTP对应的ReadFilter是ConnectionManagerImpl,因而调用ConnectionManagerImpl::onData函数。ConnectionManager 是协议插件的处理入口,同时也负责对整个处理过程的流程编排。\n数据“上传” 一次http1协议请求的处理过程。\n绿色部分表示另起一个协程。\n转发流程 Downstream stream, as a controller to handle downstream and upstream proxy flow downStream.OnReceive 逻辑。\nfunc (s *downStream) OnReceive(ctx context.Context,..., data types.IoBuffer, ...) { ... pool.ScheduleAuto(func() { phase := types.InitPhase for i := 0; i \u0026lt; 10; i++ { s.cleanNotify() phase = s.receive(ctx, id, phase) switch phase { case types.End: return case types.MatchRoute: log.Proxy.Debugf(s.context, \u0026#34;[proxy] [downstream] redo match route %+v\u0026#34;, s) case types.Retry: log.Proxy.Debugf(s.context, \u0026#34;[proxy] [downstream] retry %+v\u0026#34;, s) case types.UpFilter: log.Proxy.Debugf(s.context, \u0026#34;[proxy] [downstream] directResponse %+v\u0026#34;, s) } } } } downStream.receive 会根据当前所处的phase 进行对应的处理。\nfunc (s *downStream) receive(ctx context.Context, id uint32, phase types.Phase) types.Phase { for i := 0; i \u0026lt;= int(types.End-types.InitPhase); i++ { switch phase { // init phase case types.InitPhase: phase++ // downstream filter before route case types.DownFilter: s.runReceiveFilters(phase, s.downstreamReqHeaders, s.downstreamReqDataBuf, s.downstreamReqTrailers) phase++ // match route case types.MatchRoute: s.matchRoute() phase++ // downstream filter after route case types.DownFilterAfterRoute: s.runReceiveFilters(phase, s.downstreamReqHeaders, s.downstreamReqDataBuf, s.downstreamReqTrailers) phase++ // downstream receive header case types.DownRecvHeader: //check not null s.receiveHeaders(s.downstreamReqDataBuf == nil \u0026amp;\u0026amp; s.downstreamReqTrailers == nil) phase++ // downstream receive data case types.DownRecvData: //check not null s.receiveData(s.downstreamReqTrailers == nil) phase++ // downstream receive trailer case types.DownRecvTrailer: // check not null s.receiveTrailers() phase++ // downstream oneway case types.Oneway: ... case types.Retry: ... case types.WaitNofity: ... // upstream filter case types.UpFilter: s.runAppendFilters(phase, s.downstreamRespHeaders, s.downstreamRespDataBuf, s.downstreamRespTrailers) // maybe direct response phase++ // upstream receive header case types.UpRecvHeader: // send downstream response // check not null s.upstreamRequest.receiveHeaders(s.downstreamRespDataBuf == nil \u0026amp;\u0026amp; s.downstreamRespTrailers == nil) phase++ // upstream receive data case types.UpRecvData: // check not null s.upstreamRequest.receiveData(s.downstreamRespTrailers == nil) phase++ // upstream receive triler case types.UpRecvTrailer: //check not null s.upstreamRequest.receiveTrailers() phase++ // process end case types.End: return types.End default: return types.End } } return types.End } pkg/types/proxy.go 有phase 的定义。\n phase 对应方法 执行逻辑(部分) InitPhase DownFilter runReceiveFilters MatchRoute matchRoute DownFilterAfterRoute runReceiveFilters DownRecvHeader receiveHeaders ==\u0026gt; upstreamRequest.appendHeaders DownRecvData receiveData ==\u0026gt; upstreamRequest.appendData DownRecvTrailer receiveTrailers ==\u0026gt; upstreamRequest.appendTrailers() Oneway/Retry/WaitNofity UpFilter runAppendFilters UpRecvHeader upstreamRequest.receiveHeaders ==\u0026gt; downStream.onUpstreamData UpRecvData upstreamRequest.receiveData ==\u0026gt; downStream.onUpstreamData UpRecvTrailer upstreamRequest.receiveTrailers ==\u0026gt; downStream.onUpstreamTrailers End 上述流程才像是一个 proxy 层的活儿,请求转发到 upstream,从upstream 拿到响应, 再转回给downStream。\n与control plan 的交互 pkg/xds/v2/adssubscriber.go 启动发送线程和接收线程\nfunc (adsClient *ADSClient) Start() { adsClient.StreamClient = adsClient.AdsConfig.GetStreamClient() utils.GoWithRecover(func() { adsClient.sendThread() }, nil) utils.GoWithRecover(func() { adsClient.receiveThread() }, nil) } 定时发送请求。\nfunc (adsClient *ADSClient) sendThread() { refreshDelay := adsClient.AdsConfig.RefreshDelay t1 := time.NewTimer(*refreshDelay) for { select { ... case \u0026lt;-t1.C: err := adsClient.reqClusters(adsClient.StreamClient) if err != nil { log.DefaultLogger.Infof(\u0026#34;[xds] [ads client] send thread request cds fail!auto retry next period\u0026#34;) adsClient.reconnect() } t1.Reset(*refreshDelay) } } } 接收响应。\nfunc (adsClient *ADSClient) receiveThread() { for { select { default: adsClient.StreamClientMutex.RLock() sc := adsClient.StreamClient adsClient.StreamClientMutex.RUnlock() ... resp, err := sc.Recv() ... typeURL := resp.TypeUrl HandleTypeURL(typeURL, adsClient, resp) } } } 处理逻辑是事先注册好的函数。\nfunc HandleTypeURL(url string, client *ADSClient, resp *envoy_api_v2.DiscoveryResponse) { if f, ok := typeURLHandleFuncs[url]; ok { f(client, resp) } } func init() { RegisterTypeURLHandleFunc(EnvoyListener, HandleEnvoyListener) RegisterTypeURLHandleFunc(EnvoyCluster, HandleEnvoyCluster) RegisterTypeURLHandleFunc(EnvoyClusterLoadAssignment, HandleEnvoyClusterLoadAssignment) RegisterTypeURLHandleFunc(EnvoyRouteConfiguration, HandleEnvoyRouteConfiguration) } 以cluster 信息为例 HandleEnvoyCluster。\nfunc HandleEnvoyCluster(client *ADSClient, resp *envoy_api_v2.DiscoveryResponse) { clusters := client.handleClustersResp(resp) ... conv.ConvertUpdateClusters(clusters) clusterNames := make([]string, 0) ... for _, cluster := range clusters { if cluster.GetType() == envoy_api_v2.Cluster_EDS { clusterNames = append(clusterNames, cluster.Name) } } ... } 会触发ClusterManager 更新cluster。\nfunc ConvertUpdateEndpoints(loadAssignments []*envoy_api_v2.ClusterLoadAssignment) error { for _, loadAssignment := range loadAssignments { clusterName := loadAssignment.ClusterName for _, endpoints := range loadAssignment.Endpoints { hosts := ConvertEndpointsConfig(\u0026amp;endpoints) clusterMngAdapter := clusterAdapter.GetClusterMngAdapterInstance() ... clusterAdapter.GetClusterMngAdapterInstance().TriggerClusterHostUpdate(clusterName, hosts); ... } } return errGlobal } 学到的 不要硬看代码,尤其对于多协程程序。\n 打印日志。 debug.printStack 来查看某一个方法之前的调用栈。 fmt.Printf(\u0026quot;==\u0026gt; %T\\n\u0026quot;,xx) 如果一个interface 有多个“实现类” 可以通过%T 查看struct 的类型。 ","excerpt":"前言 MOSN是基于Go开发的sidecar,用于service mesh中的数据面代理,建议先看下一个sidecar的自我修养 对sidecar 的基本概念、原理有所了解。\n手感 使用 MOSN 作 …","ref":"https://mosn.io/blog/posts/mosn-source-code-brief/","title":"MOSN 源码浅析"},{"body":"2019 年 12 月 28 日,杭州,在第 9 届 Service Mesh Meetup 上,MOSN 开源负责人肖涵(涵畅)宣布,MOSN 从 SOFAStack 社区中迁移出来,成立独立的 Github 组织 https://github.com/mosn,并作为独立项目运营,并开启 MOSN 开源社区。\n同时公布了 MOSN 的 Roadmap,开启新的域名 mosn.io。\n","excerpt":"2019 年 12 月 28 日,杭州,在第 9 届 Service Mesh Meetup 上,MOSN 开源负责人肖涵(涵畅)宣布,MOSN 从 SOFAStack 社区中迁移出来, …","ref":"https://mosn.io/blog/news/announcing-mosn-communtiy/","title":"MOSN 社区成立"},{"body":"前言 本文是对 Nginx、Envoy 及 MOSN 的平滑升级原理区别的分析,适合对 Nginx 实现原理比较感兴趣的同学阅读,需要具备一定的网络编程知识。\n平滑升级的本质就是 listener fd 的迁移,虽然 Nginx、Envoy、MOSN 都提供了平滑升级支持,但是鉴于它们进程模型的差异,反映在实现上还是有些区别的。这里来探讨下它们其中的区别,并着重介绍 Nginx 的实现。\nNginx 相信有很多人认为 Nginx 的 reload 操作就能完成平滑升级,其实这是个典型的理解错误。实际上 reload 操作仅仅是平滑重启,并没有真正的升级新的二进制文件,也就是说其运行的依然是老的二进制文件。\nNginx 自身也并没有提供平滑升级的命令选项,其只能靠手动触发信号来完成。具体正确的操作步骤可以参考这里:Upgrading Executable on the Fly,这里只分析下其实现原理。\nNginx 的平滑升级是通过 fork + execve 这种经典的处理方式来实现的。准备升级时,Old Master 进程收到信号然后 fork 出一个子进程,注意此时这个子进程运行的依然是老的镜像文件。紧接着这个子进程会通过 execve 调用执行新的二进制文件来替换掉自己,成为 New Master。\n那么问题来了:New Master 启动时按理说会执行 bind + listen 等操作来初始化监听,而这时候 Old Master 还没有退出,端口未释放,执行 execve 时理论上应该会报:Address already in use 错误,但是实际上这里却没有任何问题,这是为什么?\n因为 Nginx 在 execve 的时候压根就没有重新 bind + listen,而是直接把 listener fd 添加到 epoll 的事件表。因为这个 New Master 本来就是从 Old Master 继承而来,自然就继承了 Old Master 的 listener fd,但是这里依然有一个问题:该怎么通知 New Master 呢?\n环境变量。execve 在执行的时候可以传入环境变量。实际上 Old Master 在 fork 之前会将所有 listener fd 添加到 NGINX 环境变量:\nngx_pid_t ngx_exec_new_binary(ngx_cycle_t *cycle, char *const *argv) { ... ctx.path = argv[0]; ctx.name = \u0026#34;new binary process\u0026#34;; ctx.argv = argv; n = 2; env = ngx_set_environment(cycle, \u0026amp;n); ... env[n++] = var; env[n] = NULL; ... ctx.envp = (char *const *) env; ccf = (ngx_core_conf_t *) ngx_get_conf(cycle-\u0026gt;conf_ctx, ngx_core_module); if (ngx_rename_file(ccf-\u0026gt;pid.data, ccf-\u0026gt;oldpid.data) == NGX_FILE_ERROR) { ... return NGX_INVALID_PID; } pid = ngx_execute(cycle, \u0026amp;ctx); return pid; } Nginx 在启动的时候,会解析 NGINX 环境变量:\nstatic ngx_int_t ngx_add_inherited_sockets(ngx_cycle_t *cycle) { ... inherited = (u_char *) getenv(NGINX_VAR); if (inherited == NULL) { return NGX_OK; } if (ngx_array_init(\u0026amp;cycle-\u0026gt;listening, cycle-\u0026gt;pool, 10, sizeof(ngx_listening_t)) != NGX_OK) { return NGX_ERROR; } for (p = inherited, v = p; *p; p++) { if (*p == \u0026#39;:\u0026#39; || *p == \u0026#39;;\u0026#39;) { s = ngx_atoi(v, p - v); ... v = p + 1; ls = ngx_array_push(\u0026amp;cycle-\u0026gt;listening); if (ls == NULL) { return NGX_ERROR; } ngx_memzero(ls, sizeof(ngx_listening_t)); ls-\u0026gt;fd = (ngx_socket_t) s; } } ... ngx_inherited = 1; return ngx_set_inherited_sockets(cycle); } 一旦检测到是继承而来的 socket,那就说明已经打开了,不会再继续 bind + listen 了:\nngx_int_t ngx_open_listening_sockets(ngx_cycle_t *cycle) { ... /* TODO: configurable try number */ for (tries = 5; tries; tries--) { failed = 0; /* for each listening socket */ ls = cycle-\u0026gt;listening.elts; for (i = 0; i \u0026lt; cycle-\u0026gt;listening.nelts; i++) { ... if (ls[i].inherited) { /* TODO: close on exit */ /* TODO: nonblocking */ /* TODO: deferred accept */ continue; } ... ngx_log_debug2(NGX_LOG_DEBUG_CORE, log, 0, \u0026#34;bind() %V #%d \u0026#34;, \u0026amp;ls[i].addr_text, s); if (bind(s, ls[i].sockaddr, ls[i].socklen) == -1) { ... } ... } } if (failed) { ngx_log_error(NGX_LOG_EMERG, log, 0, \u0026#34;still could not bind()\u0026#34;); return NGX_ERROR; } return NGX_OK; } Envoy Envoy 使用的是单进程多线程模型,其局限就是无法通过环境变量来传递 listener fd。因此 Envoy 采用的是 UDS(unix domain sockets)方案。当 New Envoy 启动完成后,会通过 UDS 向 Old Envoy 请求 listener fd 副本,拿到 listener fd 之后开始接管新来的连接,并通知 Old Envoy 终止运行。\n file descriptor 是可以通过 sendmsg/recvmsg 来传递的。\n MOSN MOSN 的方案和 Envoy 类似,都是通过 UDS 来传递 listener fd。但是其比 Envoy 更厉害的地方在于它可以把老的连接从 Old MOSN 上迁移到 New MOSN 上。也就是说把一个连接从进程 A 迁移到进程 B,而保持连接不断!!!厉不厉害?听起来很简单,但是实现起来却没那么容易,比如数据已经被拷贝到了应用层,但是还没有被处理,怎么办?这里面有很多细节需要处理。它子所以能做到这种层面,靠的也是内核的 sendmsg/recvmsg 技术。\n SCM_RIGHTS - Send or receive a set of open file descriptors from another process. The data portion contains an integer array of the file descriptors. The passed file descriptors behave as though they have been created with dup(2). http://linux.die.net/man/7/unix\n 这里有一个 Go 实现的小 Demo: tcp链接迁移。\n对比 Nginx 的实现是兼容性最强的,因为 Envoy 和 MOSN 都依赖 sendmsg/recvmsg 系统调用,需要内核 3.5+ 支持。MOSN 的难度最高,算得上是真正的无损升级,而 Nginx 和 Envoy 对于老的连接,仅仅是实现 graceful shutdown,严格来说是有损的。这对于 HTTP(通过 Connection: close) 和 gRPC(GoAway Frame) 协议支持很友好,但是遇到自定义的 TCP 协议就抓瞎了。如果遇到客户端没有处理 close 异常,很容易发生 socket fd 泄露问题。\n本文作者 ms2008,转载自Nginx vs Envoy vs Mosn 平滑升级原理解析。\n","excerpt":"前言 本文是对 Nginx、Envoy 及 MOSN 的平滑升级原理区别的分析,适合对 Nginx 实现原理比较感兴趣的同学阅读,需要具备一定的网络编程知识。\n平滑升级的本质就是 listener …","ref":"https://mosn.io/blog/posts/nginx-envoy-mosn-hot-upgrade/","title":"Nginx vs Envoy vs MOSN 平滑升级原理解析"},{"body":"以下的的性能报告为 MOSN 0.1.0 在做 Bolt 与 HTTP1.x 协议的纯 TCP 转发上与 envoy 的一些性能对比数据,主要表现在 QPS、RTT、失败率/成功率等。\n这里需要强调的是,为了提高 MOSN 的转发性能,在 0.1.0 版本中,我们做了如下的一些优化手段:\n 在线程模型优化上,使用 worker 协程池处理 stream 事件,使用两个独立的协程分别处理读写 IO 在单核转发优化上,在指定 P=1 的情况下,我们通过使用 CPU 绑核的形式来提高系统调用的执行效率以及 cache 的 locality affinity 在内存优化上,同样是在单核绑核的情况下,我们通过使用 SLAB-style 的回收机制来提高复用,减少内存 copy 在 IO 优化上,主要是通过读写 buffer 大小以及读写时机和频率等参数的控制上进行调优 以下为具体的性能测试数据。\nTCP 代理性能数据 这里,针对相同的部署模式,我们分别针对上层协议为 \u0026quot;Bolt(SofaRpc相关协议)\u0026quot; 与 \u0026quot;HTTP1.1\u0026quot; 来进行对比。\n部署模式 压测采用纯代理模式部署,client 进程通过 MOSN 进程作为转发代理访问server进程。其中,client 进程,MOSN 进程,server 进程分别运行在属于不同网段的机器中。client 直连访问 server 网络延时为 2.5ms 左右。\n客户端 Bolt 协议(发送 1K 字符串) 发送 Bolt 协议数据的客户端使用 \u0026ldquo;蚂蚁集团\u0026quot;内部开发的线上压力机,并部署 sofa rpc client。 通过压力机的性能页面,可反映压测过程中的QPS、成功/失败次数,以及RT等参数。\nHTTP1.1 协议(发送 1K 字符串) 使用 ApacheBench/2.3, 测试指令:\nab -n $RPC -c $CPC -p 1k.txt -T \u0026#34;text/plain\u0026#34; -k http://11.166.161.136:12200/tcp_bench \u0026gt; ab.log.$CPU_IDX \u0026amp; Service mesh 运行机器规格 Service mesh 运行在容器中,其中 CPU 为独占的一个逻辑核,具体规格如下:\n 类别 信息 OS 3.10.0-327.ali2008.alios7.x86_64 CPU Intel(R) Xeon(R) CPU E5-2650 v2 @ 2.60GHz X 1 Upstream 运行机器规格 类别 信息 OS 2.6.32-431.17.1.el6.FASTSOCKET CPU Intel(R) Xeon(R) CPU E5620 @ 2.40GHz X 16 Bolt 协议测试结果 性能数据 指标 MOSN Envoy QPS 103500 104000 RT 16.23ms 15.88ms MEM 31m 18m CPU 100% 100% 结论 可以看到,在单核 TCP 转发场景下,MOSN 0.1.0 版本和 Envoy 1.7版本,在满负载情况下的 QPS、RTT、成功数/失败数等性能数据上相差不大,后续版本我们会继续优化。\nHTTP/1.1 测试结果 由于 HTTP/1.1 的请求响应模型为 PING-PONG,因此 QPS 与并发数会呈现正相关。下面分别进行不同并发数的测试。\n并发20 指标 MOSN Envoy QPS 5600 5600 RT(mean) 3.549ms 3.545ms RT(P99) 4ms 4ms RT(P98) 4ms 4ms RT(P95) 4ms 4ms MEM 24m 23m CPU 40% 20% 并发40 指标 MOSN Envoy QPS 11150 11200 RT(mean) 3.583ms 3.565ms RT(P99) 4ms 4ms RT(P98) 4ms 4ms RT(P95) 4ms 4ms MEM 34m 24m CPU 70% 40% 并发200 指标 MOSN Envoy QPS 29670 38800 RT(mean) 5.715ms 5.068ms RT(P99) 16ms 7ms RT(P98) 13ms 7ms RT(P95) 11ms 6ms MEM 96m 24m CPU 100% 95% 并发220 指标 MOSN Envoy QPS 30367 41070 RT(mean) 8.201ms 5.369ms RT(P99) 20ms 9ms RT(P98) 19ms 8ms RT(P95) 16ms 8ms MEM 100m 24m CPU 100% 100% 结论 可以看到,在上层协议为 HTTP/1.X 时,MOSN 的性能和 Envoy 的性能存在一定差距,对于这种现象我们的初步结论为:在 PING-PONG 的发包模型下,MOSN 无法进行 read/write 系统调用合并,相比 SOFARPC 可以合并的场景,syscall 数量大幅上升,因此导致相比 SOFARPC 的场景,HTTP 性能上相比 Envoy 会存在差距。针对这个问题,在 0.2.0 版本中,我们会进行相应的优化。\n附录 Envoy 版本信息 version:1.7 tag:1ef23d481a4701ad4a414d1ef98036bd2ed322e7 Envoy TCP 测试配置 static_resources:listeners:- address:socket_address:address:0.0.0.0port_value:12200filter_chains:- filters:- name:envoy.tcp_proxyconfig:stat_prefix:ingress_tcpcluster:sofa_serverclusters:- name:sofa_serverconnect_timeout:0.25stype:staticlb_policy:round_robinhosts:- socket_address:address:10.210.168.5port_value:12222- socket_address:address:10.210.168.5port_value:12223- socket_address:address:10.210.168.5port_value:12224- socket_address:address:10.210.168.5port_value:12225admin:access_log_path:\u0026#34;/dev/null\u0026#34;address:socket_address:address:0.0.0.0port_value:8001","excerpt":"以下的的性能报告为 MOSN 0.1.0 在做 Bolt 与 HTTP1.x 协议的纯 TCP 转发上与 envoy 的一些性能对比数据,主要表现在 QPS、RTT、失败率/成功率等。\n这里需要强调的 …","ref":"https://mosn.io/docs/products/report/releases/mosn-0.1.0-perfermence-report/","title":"MOSN 0.1.0 性能报告"},{"body":"在 0.2.1 版本中,我们进行了如下一些优化手段:\n 添加内存复用框架,涵盖 io/protocol/stream/proxy 层级,减少对象分配、内存使用和 GC 压力。 针对大量链接场景,新增 Raw Epoll 模式,该模式使用了事件回调机制 + IO 协程池,规避了海量协程带来的堆栈内存消耗以及调度开销。 需要注意的是,由于目前 SOFARPC 和 H2 的压测工具还没有 pxx 指标的展示,我们在性能报告中选取的数据都为均值。后续需要我们自行进行相关压测环境工具的建设来完善相关指标(P99,P95……)\n总览 本次性能报告在0.1.0 性能报告的基础上,新增了若干场景的覆盖,总体包含以下几部分:\n 单核性能(sidecar场景) 7层代理 Bolt(串联) Http/1.1(串联) Http/2(串联) 多核性能(gateway场景) 7层代理 Bolt(直连) Http/1.1(直连) Http/2(直连) 长连接网关 Bolt(read/write loop with goroutine/raw epoll) 单核性能(sidecar 场景) 测试环境 机器信息 机器 OS CPU 11.166.190.224 3.10.0-327.ali2010.rc7.alios7.x86_64 Intel(R) Xeon(R) CPU E5-2640 v3 @ 2.60GHz 11.166.136.110 3.10.0-327.ali2010.rc7.alios7.x86_64 Intel(R) Xeon(R) CPU E5-2430 0 @ 2.20GHz bolt client client 为压力平台,有 5 台压力机,共计与client MOSN 之间会建立 500 条链接 http1 client(10.210.168.5) ApacheBench/2.3 -n 2000000 -c 500 -k http2 client(10.210.168.5) nghttp.h2load -n1000000 -c5 -m100 -t4 部署结构 压测模式 部署结构 串联 client \u0026ndash;\u0026gt; MOSN(11.166.190.224) \u0026ndash;\u0026gt; MOSN(11.166.136.110) \u0026ndash;\u0026gt; server(11.166.136.110) 网络时延 节点 PING client \u0026ndash;\u0026gt; MOSN(11.166.190.224) 1.356ms MOSN(11.166.190.224) \u0026ndash;\u0026gt; MOSN(11.166.136.110) 0.097 ms 请求模式 请求内容 1K req/resp 7层代理 场景 QPS RT(ms) MEM(K) CPU(%) Bolt 16000 15.8 77184 98 Http/1.1 4610 67 47336 90 Http/2 5219 81 31244 74 多核性能(gateway 场景) 测试环境 机器信息 机器 OS CPU 11.166.190.224 3.10.0-327.ali2010.rc7.alios7.x86_64 Intel(R) Xeon(R) CPU E5-2640 v3 @ 2.60GHz 11.166.136.110 3.10.0-327.ali2010.rc7.alios7.x86_64 Intel(R) Xeon(R) CPU E5-2430 0 @ 2.20GHz bolt client client为压力平台,有5台压力机,共计与client MOSN之间会建立500条链接 http1 client(10.210.168.5) ApacheBench/2.3 -n 2000000 -c 500 -k http2 client(10.210.168.5) nghttp.h2load -n1000000 -c5 -m100 -t4 部署结构 压测模式 部署结构 直连 client \u0026ndash;\u0026gt; MOSN(11.166.190.224) \u0026ndash;\u0026gt; server(11.166.136.110) 网络时延 节点 PING client \u0026ndash;\u0026gt; MOSN(11.166.190.224) 1.356ms MOSN(11.166.190.224) \u0026ndash;\u0026gt; MOSN(11.166.136.110) 0.097 ms 请求模式 请求内容 1K req/resp 7层代理 场景 QPS RT(ms) MEM(K) CPU(%) Bolt 45000 23.4 544732 380 Http/1.1 21584 23 42768 380 Http/2 8180 51.7 173180 300 长连接网关 测试环境 机器信息 机器 OS CPU 11.166.190.224 3.10.0-327.ali2010.rc7.alios7.x86_64 Intel(R) Xeon(R) CPU E5-2640 v3 @ 2.60GHz 11.166.136.110 3.10.0-327.ali2010.rc7.alios7.x86_64 Intel(R) Xeon(R) CPU E5-2430 0 @ 2.20GHz 部署结构 压测模式 部署结构 直连 client \u0026ndash;\u0026gt; MOSN(11.166.190.224) \u0026ndash;\u0026gt; server(11.166.136.110) 网络时延 节点 PING client \u0026ndash;\u0026gt; MOSN(11.166.190.224) 1.356ms MOSN(11.166.190.224) \u0026ndash;\u0026gt; MOSN(11.166.136.110) 0.097 ms 请求模式 链接数 请求内容 2 台压力机,每台 5w 链接 + 500 QPS,共计10W链接 + 1000 QPS 1K req/resp 长连接网关 场景 QPS MEM(g) CPU(%) goroutine RWLoop + goroutine 1000 3.3 60 200028 Raw epoll 1000 2.5 18 28 总结 MOSN 0.2.1引入了内存复用框架,相比0.1.0,在 bolt 协议转发场景性能表现得到了大幅优化。在提升了20% 的 QPS 的同时,还优化了 30% 的内存占用。\n与此同时,我们对 HTTP/1.1 及 HTTP/2 的场景也进行了初步的性能测试,目前来看性能表现比较一般。这主要是由于目前 HTTP 协议族的 IO、Stream 都由三方库进行处理,与 MOSN 现有的处理框架整合度较差。我们会在后续迭代进行专项优化,提升 MOSN 处理 HTTP 协议族的表现。\n此外,针对大量链接场景(例如长连接网关),我们引入了 Raw Epoll + 协程池的模式来应对协程暴增的问题,从而大幅优化了该场景下的 QPS 和内存表现。\n附录 版本对比 对比条件:\n 页面大小 0~10k,平均5k downstream 链接 1000 upstream链接 6 单核压测 版本 QPS 内存 0.1.0 10500 175M 0.2.1 13000 122M ","excerpt":"在 0.2.1 版本中,我们进行了如下一些优化手段:\n 添加内存复用框架,涵盖 io/protocol/stream/proxy 层级,减少对象分配、内存使用和 GC 压力。 针对大量链接场景, …","ref":"https://mosn.io/docs/products/report/releases/mosn-0.2.1-performance-report/","title":"MOSN 0.2.1 性能报告"},{"body":"本文记录了对 MOSN 的源码研究,研究 MOSN 是如何做到平滑重启的。\n本文的内容基于 MOSN v0.8.1。\n我们先将被重启的 MOSN 进程称为 旧 MOSN,将重启并接管流量的进程成为 新 MOSN。\n机制 MOSN 没有使用重新读取 config 文件的方法来实现 reconfig,而是通过 unix socket 作为进程间通信,并将旧进程的监听 fd 通过 socket 传过去,新 MOSN 接管 fd 并且重新读取 config,旧 MOSN 进行 gracefully shutdown,以达到 reconfig 和平滑重启的功能。\n旧 MOSN 我们先从一个启动着的 MOSN 进程看起,看看它是如何被重启的。\nMOSN 的 reconfig 逻辑在 server 包的 reconfigure.go 内。\nMOSN 进程启动后,会创建一个叫 reconfig.sock 的 unix socket,创建一个协程,开始监听并往里面写入一个字节的内容,这时会出现写阻塞。一旦有另一个进程从 reconfig.sock 读取到这一个字节,旧 MOSN 便开始 reconfig 逻辑。\nreconfig 逻辑: 当写阻塞结束,协程会尝试链接另一个 unix socket :listen.sock\n 一旦链接上,负责 reconfig 的协程会将已经存在的 fd 从 listen.sock 发送,发送是通过 out-of-band 数据进行的。 很显然,这个 listen.sock 是新的 MOSN 进程创建的,用来接收正在服务的 fd,接管并用以继续服务。\n这里的 已经存在的 fd 包括两种:\n server listener fd, 负责监听业务的 tcp 连接,对应 config.json 里的 servers 数组里的 listeners 数组里的 ip +端口 service listener fd,控制相关的端口,比如 pprof、prometheus metrics export、admin 端口 发送完fd之后,旧 MOSN 会接收 listen.sock 的数据(由新 mosn 写入的数据,代表 fd 已接管完毕),并进入 gracefully shutdown 状态,不再接收新的链接,等已有请求处理完再关闭\n 旧 MOSN 有30秒时间处理完现存请求\n 30秒后,旧 mosn 关闭,链接由新 mosn 接管\n 新 MOSN 接下来是新 MOSN 的启动。有两个问题:\n 新 MOSN 是如何将旧 MOSN 的fd据为己有,并且接受数据的呢?\n 新 MOSN 通过 net.FileListener 方法对接受到的 fd 进行处理,该函数会返回 fd 的一个 network listener 副本,改副本可以用来接收数据,新 MOSN 就是通过这个操作来从旧 MOSN 做 fd 的接管 新 MOSN 会重新解析一遍 config.json 进行解析,而在上一部处理过的 network listener 副本也能查找到对应的地址和端口。通过比对两者相同之处,就能在创建新的 server listener 和 service listener 时,接管旧 MOSN 的 fd,用来初始化新的 listener。 新 MOSN 接收到 fd 信息后,会做些什么呢?\n 接管 server listener fd 接管 service listener fd 给 listen.sock 发送一个字节数据,通知旧 MOSN:可以关闭了 至此,MOSN reconfig 结束 后续疑问 新 MOSN 通过 net.FileListener 方法处理完 fd 并初始化了 listener,由于处理后的 fd 是一个副本,如果这个时候来了一个连接,那这个连接是会被旧 MOSN 处理到,还是新 MOSN,还是两者都会通知到呢? 关于这方面,可以做一个实验验证一下。\n","excerpt":"本文记录了对 MOSN 的源码研究,研究 MOSN 是如何做到平滑重启的。\n本文的内容基于 MOSN v0.8.1。\n我们先将被重启的 MOSN 进程称为 旧 MOSN, …","ref":"https://mosn.io/blog/code/mosn-reconfig-mechanism/","title":"MOSN 源码解析 - reconfig 机制"},{"body":"","excerpt":"","ref":"https://mosn.io/index.json","title":""},{"body":" #td-cover-block-0 { background-image: url(/featured-background_hu82ae0439b5aa134274e6954a2aec5274_285821_960x540_fill_q75_catmullrom_top.jpg); } @media only screen and (min-width: 1200px) { #td-cover-block-0 { background-image: url(/featured-background_hu82ae0439b5aa134274e6954a2aec5274_285821_1920x1080_fill_q75_catmullrom_top.jpg); } } Modular Open Smart Network 开始了解 下载 云原生网络代理平台\n\n 关于 MOSN MOSN(Modular Open Smart Network)是一款主要使用 Go 语言开发的云原生网络代理平台,由蚂蚁集团开源并经过双11大促几十万容器的生产级验证。\nMOSN 为服务提供多协议、模块化、智能化、安全的代理能力,融合了大量云原生通用组件,同时也可以集成 Envoy 作为网络库,具备高性能、易扩展的特点。\nMOSN 可以和 Istio 集成构建 Service Mesh,也可以作为独立的四、七层负载均衡,API Gateway、云原生 Ingress 等使用。\n MOSN 是 CNCF Landscape 中的项目。 反馈问题 功能请求、提问和 Bug,请使用 Github Issue 报告给我们或加入钉钉群与我们交流。\n 欢迎贡献 不要犹豫,请在 Github 上提交 Pull Request!\n 在 Twitter 上关注我们 在 Twitter 上关注 @MosnProxy 获取 MOSN 的最新消息。\n ","excerpt":"#td-cover-block-0 { background-image: …","ref":"https://mosn.io/","title":"MOSN"},{"body":"我们很高兴的宣布 MOSN v1.4.0 发布,以下是该版本的变更日志。\nv1.4.0 新功能 支持记录 HTTP 健康检查日志 (#2096) @dengqian 新增最小连接负载均衡器 (#2184) @dengqian 新增 API: 强制断开并重新连接 ADS 服务 (#2183) @dengqian 支持 pprof debug server 配置 endpoint (#2202) @dengqian 集成 mosn.io/envoy-go-extension,参考文档 (#2200) @antJack (#2222) @3062 新增 API: 支持覆盖注册 Variable (mosn/pkg#72) @antJack 新增记录 mosn 处理时间的字段的变量 (#2235) @z2z23n0 支持将 cluster idle_timeout 设置为零以表示从不超时 (#2197) @antJack 重构 import pprof 迁移至 pkg/mosn (#2216) @3062 优化 减少 proxywasm 基准测试的日志记录 (#2189) @Crypt Keeper Bug 修复 增大 UDP DNS 解析缓冲区 (#2201) @dengqian 修复平滑升级时未继承 debug server 的问题 (#2204) @dengqian 修复平滑升级失败时,新 mosn 会删除 reconfig.sock 的问题 (#2205) @dengqian 修复 HTTP 健康检查 URL query string 未转译的问题 (#2207) @dengqian 修复 ReqRoundRobin 负载均衡器在索引超过 host 数量时,host 选择失败的问题 (#2209) @dengqian 修复 RDS 创建路由之后,已建连的连接无法找到路由的问题 (#2199) @dengqian (#2210) @3062 修复 Variable.Set 执行后会读取到旧缓存值的问题 (mosn/pkg#73) @antJack 修复 DefaultRoller 未设置时间导致 panic 的问题 (mosn/pkg#74) @dengqian 提前 metrics 初始化时间使其适用于 static config (#2221) @dengqian 修复多个 health checker 共用 rander 导致的并发问题 (#2228) @dengqian 设置 HTTP/1.1 作为发往上游的 HTTP 协议 (#2225) @dengqian 补全缺失的统计信息 (#2215) @3062 ","excerpt":"我们很高兴的宣布 MOSN v1.4.0 发布,以下是该版本的变更日志。\nv1.4.0 新功能 支持记录 HTTP 健康检查日志 (#2096) @dengqian …","ref":"https://mosn.io/docs/products/report/releases/v1.4.0/","title":"MOSN v1.4.0 发布"},{"body":"我们很高兴的宣布 MOSN v1.5.0 发布,以下是该版本的变更日志。\nv1.5.0 新功能 EdfLoadBalancer 支持慢启动 (#2178) @jizhuozhi 支持集群独占连接池 (#2281) @yejialiango LeastActiveRequest 和 LeastActiveConnection 负载均衡器支持设置 active_request_bias (#2286) @jizhuozhi 支持配置指标采样器 (#2261) @jizhuozhi 新增 PeakEWMA 负载均衡器 (#2253) @jizhuozhi 变更 README 更新 partners \u0026amp; users (#2245) @doujiang24 更新依赖 (#2242) (#2248) (#2249) @dependabot 升级 MOSN 支持的最低 Go 版本至 1.18 (#2288) @muyuan0 优化 使用逻辑时钟使 edf 调度器更加稳定 (#2229) @jizhuozhi proxywasm 中缺少 proxy_on_delete 的日志级别从 error 更改为 warn (#2246) @codefromthecrypt 修正 connection 对象接收者命名不同的问题 (#2262) @diannaowa 禁用 workflow 中过于严格的 linters (#2259) @jizhuozhi 当 PR 是未完成状态时不启用 workflow (#2269) @diannaowa 使用指针减少 duffcopy 和 duffzero 开销 (#2272) @jizhuozhi 删除不必要的导入 (#2292) @spacewander CI 增加 goimports 检测 (#2297) @spacewander Bug 修复 修复健康检查时多个 host 使用同一个 rander 引发的异常 (#2240) @dengqian 修复连接池绑定连接标识错误 (#2263) @antJack 修复在上下文中将 client stream 协议信息保存到 DownStreamProtocol 的错误 (#2270) @nejisama 修复未使用正确的 Go 版本进行测试 (#2288) @muyuan0 修复无法找到实际值为 \u0026lsquo;-\u0026rsquo; 的变量 (#2174) @3062 修复 cluster 证书配置错误导致的空接口异常 (#2291) @3062 修复 leastActiveRequestLoadBalancer 配置中使用了接口类型导致的解析错误 (#2284) @jizhuozhi 修复配置文件 lbConfig 未生效的问题 (#2299) @3062 修复 activeRequestBias 缺少默认值和一些命名大小写错误 (#2298) @jizhuozhi ","excerpt":"我们很高兴的宣布 MOSN v1.5.0 发布,以下是该版本的变更日志。\nv1.5.0 新功能 EdfLoadBalancer 支持慢启动 (#2178) @jizhuozhi …","ref":"https://mosn.io/docs/products/report/releases/v1.5.0/","title":"MOSN v1.5.0 发布"},{"body":"我们很高兴的宣布 MOSN v1.6.0 发布,以下是该版本的变更日志。\nv1.6.0 新功能 PeakEWMA 支持配置 activeRequestBias (#2301) @jizhuozhi gRPC filter 支持 UDS (#2309) @wenxuwan 支持初始化热升级时 config 继承函数 (#2241) @dengqian 允许自定义 proxy defaultRouteHandlerName (#2308) @fibbery 变更 示例 http-sample README 增加配置文件链接 (#2226) @mimani68 将 wazero 更新到 1.2.1 (#2254) @codefromthecrypt 更新依赖 (#2230) (#2233) (#2247) (#2302) (#2326) (#2332) (#2333) @dependabot 重构 重构调试日志内容,打印 data 移至 tracef 中 (#2316) @antJack 优化 EWMA 优化新添加的主机的默认值 (#2301) @jizhuozhi PeakEwma LB 不再统计错误响应 (#2323) @jizhuozhi Bug 修复 修复 edfScheduler 在动态负载算法中错误地将权重固定为1 (#2306) @jizhuozhi 修复 cluster hosts 顺序改变导致的 LB 行为不稳定 (#2258) @dengqian 修复 NilMetrics 缺少 EWMA() 方法导致的 panic (#2310) @antJack (#2312) @jizhuozhi 修复 xDS 更新时,cluster hosts 为空导致的 panic (#2314) @dengqian 修复 MOSN 异常退出时 UDS 套接字文件未删除导致重启失败 (#2318) @wenxuwan 修复 xDS 状态码未转换错误。修复未处理 istio inbound IPv6 地址错误 (#2144) @kkrrsq 修复非热升级优雅退出时 Listener 未直接关闭导致新建连接报错 (#2234) @hui-cha 修复 goimports lint 错误 (#2313) @spacewander ","excerpt":"我们很高兴的宣布 MOSN v1.6.0 发布,以下是该版本的变更日志。\nv1.6.0 新功能 PeakEWMA 支持配置 activeRequestBias (#2301) @jizhuozhi …","ref":"https://mosn.io/docs/products/report/releases/v1.6.0/","title":"MOSN v1.6.0 发布"},{"body":"This is the blog section. It has two categories: News and Releases.\nFiles in these directories will be listed in reverse chronological order.\n","excerpt":"This is the blog section. It has two categories: News and Releases.\nFiles in these directories will …","ref":"https://mosn.io/blog/","title":"MOSN 博客"},{"body":"","excerpt":"","ref":"https://mosn.io/docs/products/report/","title":"发布报告"},{"body":"","excerpt":"","ref":"https://mosn.io/search/","title":"搜索结果"},{"body":"MOSN 教程使用 Killercoda 作为线上环境,无需自己部署环境,提供交互式的学习体验,请访问 Killercoda 上的 MOSN 教程。\n教程列表 当前支持的教程如下。\n 在 Istio 中集成 MOSN 使用 MOSN 在 Istio 环境中运行 Dubbo 在 MOSN 中集成 SkyWalking 贡献教程 MOSN 教程源码位于 mosn/mosn-tutorial 仓库中,参与贡献请阅读贡献书册。\n","excerpt":"MOSN 教程使用 Killercoda 作为线上环境,无需自己部署环境,提供交互式的学习体验,请访问 Killercoda 上的 MOSN 教程。\n教程列表 当前支持的教程如下。\n 在 Istio …","ref":"https://mosn.io/docs/user-guide/start/tutorial/","title":"教程"},{"body":"","excerpt":"","ref":"https://mosn.io/docs/products/ecology/","title":"生态建设"},{"body":"MOSN 是一个开源项目,于 2018 年 7 月由蚂蚁集团开源,使用 Apache 2.0 协议,任何人都可以使用和参与改进。MOSN 社区期待您的加入!\n关于 MOSN 社区的详细资料请访问 Community 仓库。\n工作组 目前 MOSN 包含以下工作组:\n Istio 工作组 Dubbo 工作组 选择加入您感兴趣的工作组,开始您的 MOSN 之旅吧!\n社区会议 MOSN 社区定期召开社区会议。\n 每双周三晚 8 点(北京时间)\n 会议纪要\n 合作伙伴 合作伙伴参与 MOSN 共建,使 MOSN 变得更好。\n 终端用户 以下是 MOSN 的用户(部分):\n 请在此处登记并提供反馈来帮助 MOSN 做的更好。\n商业用户 以下是 MOSN 的商业版用户(部分):\n 开源生态 MOSN 社区积极拥抱开源生态,与以下开源社区建立了良好的合作关系。\n Committer 列表 MOSN 社区认证的 Committer 如下:\n 姓名 GitHub 公司 田阳 taoyuanyuan 蚂蚁集团 王发康 wangfakang 蚂蚁集团 白鹏 nejisama 蚂蚁集团 曹春晖 cch123 蚂蚁集团 孙福泽 peacocktrain Boss 直聘 陈鹏 champly 多点生活 姚昌宇 trainyao 有米科技 邓茜 dengqian 阿里云 黄润豪 glyasai 好雨科技 郑泽超 CodingSinger 字节跳动 付建豪 AlphaBaby 京东 纪卓志 jizhuozhi 京东物流 Committer 是具有 MOSN 仓库写权限的个人,标准如下:\n 能够在长时间内做持续贡献 issue、PR 的个人; 参与 issue 列表的维护及重要功能的讨论; 积极参与 code review 和社区会议; Meetup https://github.com/mosn/meetup\n教程 MOSN 提供线上教程,见教程页面。\n加入社区 使用钉钉扫描下面的二维码加入 MOSN 用户群。\n","excerpt":"MOSN 是一个开源项目,于 2018 年 7 月由蚂蚁集团开源,使用 Apache 2.0 协议,任何人都可以使用和参与改进。MOSN 社区期待您的加入!\n关于 MOSN …","ref":"https://mosn.io/docs/community/","title":"社区"}]