最近都在做自研 Service Mesh 方案的落地和后续迭代优化,目前稳定承接了旧系统的大部分流量,这里分享一下这套架构,以及过程中的思考和遇到的一些问题。

Service Mesh 现状

服务网格(Service Mesh)是致力于解决服务间通讯的基础设施层。它负责在现代云原生应用程序的复杂服务拓扑来可靠地传递请求。实际上,Service Mesh 通常是通过一组轻量级网络代理(Sidecar proxy),与应用程序代码部署在一起来实现,而无需感知应用程序本身。

微服务发展到一定阶段,Service Mesh 更像是借助于容器平台的兴起而对 API 网关的一种解耦和优化。

目前看来最主要的开源解决方案是 Istio 和 Linkerd。前者甚至已经隐隐成为一种事实上的标准,是谈论 Service Mesh 绕不过去的一个项目,当你提及任何一个其他项目时,与 Istio 的对比可以检验一个项目的成熟度。

为什么需要自研 Service Mesh 方案

  • 在开始设计这套 Service Mesh 架构时,Istio 尚处于 0.X 的版本,没有达到一个生产级别的可用性,但是其所展现出的一些功能特性和理念还是很有吸引力。
  • Istio 的 proxy 使用了 envoy,兼具丰富的功能和高性能。但是采用 C++ 开发,对于以 Golang 为主的开发团队来说,将我们需要的功能移植上去,且保证可靠性是有一定难度的。反过来看,用 Golang 实现一个 proxy,确是非常简单且可靠的,虽然在性能上做了一些取舍。
  • 公司内部本身已经有现成的网关组件以及丰富的 Golang 的 package,如果直接使用开源的方案,会放弃很多已有的功能,例如自定义的负载均衡策略。
  • Istio 通过 iptables 进行流量劫持,大大降低了服务接入 Service Mesh 平台的难度,但是对于不需要劫持的流量却需要通过定义白名单的方式排除在外,不是很友好,且有额外不多的一些性能损耗。而通过环境变量或代码指定 proxy 的方式,有一定侵入性,但是几乎没什么难度,且可以根据用户需要自行管理。
  • 旧的服务迁移并非一件简单的事情,在这个过渡的过程中,需要考虑同时兼容多个多个平台和架构。Istio 虽然说是跨平台,但是实际上目前还是主要基于 kubernetes,对于运行在 kubernetes 之外的服务来说并不友好。自研的架构会针对这个问题做优化,帮助服务平滑迁移。

核心功能

流量管理

流量管理是最核心的一部分内容,大部分功能是从原先的 API 网关中剥离出来抽象而成。

提供了针对服务级别的断路器,超时,重试等功能,且支持复杂的规则配置,例如根据 HTTP Response Header 进行正则匹配来判断该请求是否需要重试。开发,运维可以根据需要动态调整集群中各个服务的参数配置。

支持策略路由,例如将指定用户或按照百分比过滤的流量切到一个指定某个服务的指定版本的实例上,在灰度发布,A/B 测试等场景下可以提供帮助。

安全性

支持服务级别的访问控制,目前没有支持 TLS 和身份验证,因为当前只考虑内部的服务,没有这个需求。

负载均衡

支持轮询,最小连接数,优先本地节点和 Remote API 等负载均衡算法。通过规则配置,实现错误检测并自动摘除后端故障节点。

对某些均衡性要求较高的服务,支持 Remote API,利用远端全局视角来做负载均衡,避免由于 proxy 较多而导致负载均衡的能力下降。

多种接入方式

由于新的架构中所有服务的入流量和出流量都需要经过 proxy 中转才能使用其中的能力,目前支持两种方式来实现这个需求,服务可以选择其中任意一种方式来接入到这个平台。

  1. 支持 HTTP_PROXY 这种主动指定代理的方式,A 服务访问 B 服务时,主动向 proxy 发送请求,由 proxy 转发给 B 服务。这种方式的优点是一次转发,性能消耗小,且对外的请求是否需要经过 proxy 可以根据服务需要来指定,灵活性更高。缺点是服务需要感知 proxy 的存在并做针对的设置。
  2. 流量劫持的方式,通过 iptables 等方式将指定出口的流量劫持到 proxy 中。优缺点和第一种代理的方式正好相反。

多协议支持

支持 HTTP, HTTP2, grpc 等 rpc 协议,可扩展。

统一的监控及日志

对于服务调用之间的响应时间,请求数,状态码等会由 proxy 对外提供统一的监控数据 (通过 prometheus 采集和 grafana 展示) 和审计日志。

跨区域访问

对于不在一个机房的多个 k8s 集群,能够共享服务发现数据,实现跨区域访问服务的功能。

在某个机房出现故障或资源不足问题时,多机房的集群可以互为灾备。

多平台,架构支持

架构上尽量的抽象,屏蔽底层平台的细节,能够通过扩展的方式方便地支持在 k8s, mesos 或是在物理机上部署的服务。

架构

architecture

discovery

discovery 为平台提供服务发现的能力,通过抽象,支持同时对接多种后端数据源,例如 k8s, consul, config 等。

discovery 屏蔽了后端各个数据源的细节,对外提供统一的 GRPC 服务发现接口,将服务发现的数据传播到 proxy 中。

commander

平台中的所有功能都依赖于配置来管理,commander 提供了对这些配置的抽象。开发,运维通过 commander 提供的统一接口来管理集群的流量及行为。

这些配置可能来自于 k8s CRD,静态配置文件,远端 API。commander 会将这些配置发送给所有的 proxy。

proxy

在 k8s 中,proxy 会以 sidecar 的方式和每一个服务部署在一起,负责代理服务的出入流量。通过 discovery 获取服务发现数据源,从 commander 获取策略配置,来实现管理服务调用的能力。

和 Istio 不同的是,绝大多数核心能力在这个组件中实现,而不是额外的 mixer 组件。

global-lb

global-lb 提供统一视角负载均衡的功能。

由于新架构的变更,每一个服务实例都会有一个与之对应的 proxy。而每一个 proxy 的负载均衡都使用的是自身视角的数据,均衡性会大打折扣。在某些对均衡性需求较高的场景下, proxy 可以借助于 global-lb 来实现统一视角的负载均衡。

zone-agent

跨集群服务代理组件。

在跨区域服务访问中,由于 k8s 集群之间的服务不能直接通信,A 集群的服务访问 B 集群的服务,需要经过 zone-agent 代理。zone-agent 作为一个反向代理会通过 ingress 的方式暴露给集群外部的服务。

一些思考

Mixer 组件

Istio 一个很好的抽象就是独立出了 Mixer 组件,也就是 Service Mesh 中的控制面。为 Mixer 开发一些 Adapter 就可以很方便的在这套架构上扩展新的功能,例如 QPS 限制,监控,日志收集等。

但是对于我们目前而言,每一个请求都需要经过 Mixer 组件,存在一定的性能和可靠性的问题。我觉得这是一个过于超前的设计,在目前的架构上可能并不是最合适的解决方案。虽然可以在 proxy 中通过缓存来缓解压力,但是目前看来也不够完善,不能解决所有问题,且还存在着一些隐患,例如缓存的内存占用问题,在量级小的时候不明显,后期上量之后可能才会暴露。

我们将 Mixer 中的大部分功能都合并到了 proxy 中,虽然 proxy 因此变得复杂,但是带来的收益确实实实在在的。减少了一次额外的链路访问,降低延迟。Mixer 虽然可以水平扩展,但是通常和 proxy 的数量比起来还是较少,监控和日志的量级,如果都放在 Mixer 上去做,未必可靠,加上我们已有的监控和日志收集的体系,proxy 可以很方便的接入,所以这套架构中没有了类似 Mixer 的组件。

流量劫持

Istio 的一个理念就是让平台上的服务对这套架构完全没有感知,可以不需要任何改动的情况下从旧的平台架构中迁移过来。

我个人认为这并不是一个需要十分在意的点。服务感知 proxy 的存在,收益远大于成本。当前 Istio 通过 iptables 流量劫持的方式,虽然服务几乎不需要改动,但是非常暴力,如果有额外的数据库或者不需要经过代理的集群外部的服务,都需要通过白名单的方式不去劫持这部分流量,不友好。

目前的架构中同时支持流量劫持和 HTTP_PROXY,并且 HTTP_PROXY 是推荐的使用姿势,适配起来几乎没有难度。只有在服务自身难以通过修改代码来使用代理的时候才会采用流量劫持的方式,且这一方式和 Istio 也存在一些差异,后面会专门写一篇文章讲下这个方案。