为了兼容 Service mesh 的非容器化场景,TSF Mesh 基于 Istio 构建了 Service mesh 微服务平台,对原生 Istio 实现进行了适当的改造,支持应用同时运行于容器环境和虚拟机环境(同时也打通了 Spring Cloud 框架,实现 Mesh 服务和 Spring Cloud 服务互联互通,服务统一治理)。
TSF Mesh 对容器和虚拟机统一化的改造主要体现在以下几个方面:
- 应用部署和Sidecar注入
- 流量劫持
-
服务注册与发现
针对这几点,下面会先剖析对比 Istio service mesh 基于 K8s 的实现方案,再讲述 TSF Mesh 是如何实现的,做了哪些改造。
应用部署和 Sidecar 注入
首先,回顾下 Istio service mesh 的应用部署和 Sidecar 注入方式:
应用部署:Istio service mesh 依赖 K8s 进行应用的生命周期管理,包括应用的部署和管理(扩缩容、自动恢复、发布)
Sidecar 注入:分为手动注入和自动注入, 如下图所示:
-
手工注入通过手工执行 istioctl kube-inject 来重新构造应用的 CRD yaml
-
自动注入通过 K8s 的 mutable webhook 回调 istio-sidecar-injector 服务来重新构造应用的 CRD yaml
无论是手工注入还是自动注入,Sidecar 注入的本质是将运行 Sidecar 所需要的镜像地址、启动参数、所连接的 Istio 集群(Pilot、Mixes、Citadel)及配置信息填充到注入模版,并添加到应用的 CRD yaml 中,最终通过 K8s 持久化资源并拉起应用和 Sidecar 的 POD。
那 TSF Mesh 如何做应用部署和 Sidecar 注入的呢?
由于 TSF Mesh 需要同时支持容器和虚拟机环境,则首先需要解决虚拟机部署的问题,要实现等同 K8s 的部署能力,需要解决以下几个问题:
- 资源和配置管理,如 Istio 集群信息、配置信息等
- 对应于容器的镜像,虚拟机就是程序包,那就涉及到包管理
- 虚拟机应用生命周期的管理
-
虚拟机 Sidecar 注入
为了解决容器和虚拟机统一部署问题,不能再用 K8s 的存储方式,而是需要更高层的管理模式,我们引入了 tsf-resource 资源管控模块来负责容器和虚拟机相关资源的统一管理,像 Istio 集群相关的信息在控制平台部署时会持久化在 TSF 的 DB 中。
对于容器平台,当用户从 TSF 控制台部署一个容器应用时,tsf-resource 从 DB 中获取像容器的镜像地址、Istio 集群信息、配置、启动参数等,进行 K8s CRD 的组装,组装完将 CRD 创建请求发送给容器平台完成应用 POD 的拉起,其实这里在组装 CRD 时已经实现了 Sidecar 的自动注入,注入时的动态参数由控制台传递,静态参数如 Sidecar 镜像地址、启动参数等从 DB 中获取。
对于虚拟机平台,TSF 引入了以下几个模块来解决程序包管理和应用部署的问题:
- tsf-repo,程序包仓库管理,存储应用程序包及相关依赖
- tsf-master,虚拟机节点管理 master,发送部署/下线/启动/停止等任务给 tsf-agent
- tsf-agent,虚拟机节点管理 agent,部署在应用机器上,负责初始化机器环境、执行应用部署/下线/启动/停止等任务
对于虚拟机应用的变更,如例如应用部署、启动、停止、下线,TSF 通过任务的方式来跟踪每个变更,在任务下发的具体流程中,所有任务都是异步执行的,tsf-resource 将任务转发给 tsf-master 后就返回给 TSF 控制台,并由 tsf-master 完成任务的下发和状态跟踪;用户在 TSF 控制台执行操作后,可以根据返回的任务 ID 查询执行结果。
流量劫持
Service mesh 需要透明的进行服务治理,也就需要透明的接管服务进出流量,将流量劫持到 Sidecar,由 Sidecar 进行流量管理,传统的方式是 iptables 流量劫持(也可采用 BPF、IPVS 等方式),同样下面先回顾下 Istio 的 Service mesh 方案具体是如何劫持流量的,然后再看下 TSF mesh 为了统一容器和虚拟机做了哪些改造。
查看经过 Sidecar 注入后的应用 YAML 文件,发现 istio-sidecar-injector 服务在注入 Sidecar 容器本身时,还注入了 istio-init 容器,istio-init 容器属于 init 容器(init 容器在应用程序容器启动之前运行,用来初始化一些应用镜像中不存在的实用工具或安装脚本),下面是官方例子中注入的 init 容器部分:
initContainers:
args:
-p
"15001"
-u
"1337"
-m
REDIRECT
-i
'*'
-x
""
-b
9080,
-d
""
image: istio/istio-release-proxy_init:1.0.1
imagePullPolicy: IfNotPresent
name: istio-init
resources: {}
securityContext:
capabilities:
add:
NET_ADMIN
privileged: true
...
可以看出 init 容器 istio-init,被赋予了 NET_ADMIN 的 POD 网络空间权限,具体执行了哪些初始化还看不出来,那再来看下 istio/istio-release-proxy_init:1.0.1 镜像的 Dockerfile。
FROM ubuntu:xenial
RUN apt-get update && apt-get install -y \
iproute2 \
iptables \
&& rm -rf /var/lib/apt/lists/*
ADD istio-iptables.sh /usr/local/bin/
ENTRYPOINT ["/usr/local/bin/istio-iptables.sh"]
istio-init 容器的 ENTRYPOINT 是 /usr/local/bin/istio-iptables.sh 脚本,顾名思义用于 Istio iptables 流量劫持的脚本,组合上面 istio-init 容器的启动参数,完整命令为:
/usr/local/bin/istio-iptables.sh -p 15001 -u 1337 -m REDIRECT -i '*' -x "" -b 9080 -d ""
该命令的主要作用是,将应用容器中访问9080端口的流量(inbound 流量)和所有出站流量(outbound 流量)重定向到 Sidecar(即 envoy)的15001端口。
总结下来,Istio 是通过 init 容器完成了流量劫持到 Sidecar 的初始化工作。
TSF Mesh 如何实现流量劫持的呢?
TSF Mesh 同样采用 iptables 方式,不过要兼顾虚拟机平台,需要解决两个主要问题:
- 虚拟机下如何执行 iptables 应用劫持策略
- 虚拟机下如何劫持流量,不能劫持虚拟机整个网络空间的流量
问题1的解决比较简单,我们对 pilot-agent 做些一些扩展,在 pilot-agent 中执行 iptables 脚本,pilot-agent 一个主要工作是生成 envoy 的 bootstrap 配置并启动 envoy、管理 envoy 的生命周期,类似容器环境下做 envoy 启动前的 init 准备,在启动 envoy 前执行 iptables 脚本,也比较合理。
问题2的解决就比较麻烦了,但又非常重要,不像 K8s 的 POD,POD 间网路是隔离的,一个 POD 一般只会运行一个应用,劫持整个 POD 网路空间里的流量完全没有问题,而虚拟机中可能还有其它进程的存在,这些进程可能也有 Outbound 的流量,因此我们不能劫持虚拟机所有的流量,一种比较合理的劫持方案应该是:
- 对于 Inbound 流量,只劫持到部署应用的端口,这个原生 Istio 已经做到,无需改造
- 对于 Outbound 流量,只劫持注册中心已注册服务的流量
下面来具体讲下 TSF Mesh 如何针对服务来劫持 Outbound 流量的。
其实我们的方案和 K8s 的 kube-DNS+kube-proxy 的服务发现机制类似,TSF Mesh 在数据平面引入了一个 mesh-dns 模块,通过连接 pilot-discovery 同步获取注册中心的服务变更来更新本地的 DNS cache,对于来自注册中心的服务会被解析到一个特定的 IP,然后在 iptables 策略中把目的地址到这个特定 IP 的流量重定向 envoy,当然,还需要劫持 DNS 53 端口的流量,先把 DNS 请求引到 mesh-dns,可以看下 iptables nat 表中完整的规则内容:
Inbound 流量劫持跟原生 Istio 实现类似就不赘述了,下图显示的是 Outbound 流量 iptables 劫持的详细过程,其中红色部分为新增的 DNS 劫持规则。
注册服务的域名劫持,除了引入了 mesh-dns 自研模块,还涉及到 pilot-discovery 和 pilot-agent 的改造:
pilot-discovery 改造点
- pilot-discovery 扩展一个 ServiceInfos 的 grpc 服务,提供注册服务变更同步接口
- pilot-discovery 早期的 consul controller 实现是,定时通过 Consul 的 Rest API 获取服务数据并和上一次的查询结果进行对比,如果数据发生了变化则通知 Pilot discovery 进行更新,这里我们进行了优化,采用 Consul 的 watch 机制来代替轮询(下面服务注册与发现中也有提到),并在 ServiceInfos 服务初始化时向 consul controller 注册了服务变更的 event 通知
- ServiceInfos 服务在 mesh-dns 请求第一次到来时同步全量的服务注册表,之后则根据服务的变更情况增量同步
mesh-dns实现
- DNS 服务基于 github.com/miekg/dns 实现(一个非常轻量级的 DNS 库)
- 和 pilot-discovery 保持注册服务列表的同步,mesh-dns 启动时进行全量同步,运行时进行增量同步
- 处理 DNS 请求时,先检查 Domain 是否在注册服务列表里,如果在则返回一个特定的 IP(可配置),否则请求本地配置的域名服务进行解析