learn-tech/专栏/周志明的架构课/60_透明通讯的涅槃(下):控制平面与数据平面.md
2024-10-16 06:37:41 +08:00

276 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到Google相关通知网站将会择期关闭。相关通知内容
60 _ 透明通讯的涅槃(下):控制平面与数据平面
你好,我是周志明。这节课,我会延续服务网格将“程序”与“网络”解耦的思路,通过介绍几个数据平面通信与控制平面通信中的核心问题的解决方案,帮助你更好地理解这两个概念。
在开始之前我想先说明一点就是我们知道在工业界数据平面领域已经有了Linkerd、Nginx、Envoy等产品在控制平面领域也有Istio、Open Service Mesh、Consul等产品。不过今天我主要讲解的是目前市场占有率最高的Istio与Envoy因为我的目的是要让你理解两种平面通信的技术原理而非介绍Istio和Envoy的功能与用法这节课中涉及到的原理在各种服务网格产品中一般都是通用的并不局限于哪一种具体实现。
好,接下来我们就从数据平面通信开始,来了解一下它的工作内容。
数据平面
首先数据平面由一系列边车代理所构成它的核心职责是转发应用的入站Inbound和出站Outbound数据包因此数据平面也有个别名叫转发平面Forwarding Plane
同时,为了在不可靠的物理网络中保证程序间通信最大的可靠性,数据平面必须根据控制平面下发策略的指导,在应用无感知的情况下自动完成服务路由、健康检查、负载均衡、认证鉴权、产生监控数据等一系列工作。
那么,为了顺利完成以上所说的工作目标,数据平面至少需要妥善解决三个关键问题:
代理注入:边车代理是如何注入到应用程序中的?
流量劫持:边车代理是如何劫持应用程序的通信流量的?
可靠通信:边车代理是如何保证应用程序的通信可靠性的?
好,下面我们就具体来看看吧。
代理注入
从职责上说,注入边车代理是控制平面的工作,但从叙述逻辑上,将其放在数据平面中介绍更合适。因为把边车代理注入到应用的过程并不一定全都是透明的,所以现在的服务网格产品产生了以下三种将边车代理接入到应用程序中的方式。
基座模式Chassis这种方式接入的边车代理对程序就是不透明的它至少会包括一个轻量级的SDK让通信由SDK中的接口去处理。基座模式的好处是在程序代码的帮助下有可能达到更好的性能功能也相对更容易实现。但坏处是对代码有侵入性对编程语言有依赖性。这种模式的典型产品是由华为开源后捐献给Apache基金会的ServiceComb Mesher。基座模式的接入方式目前并不属于主流方式我也就不展开介绍了。
注入模式Injector根据注入方式不同又可以分为
手动注入模式这种接入方式对使用者来说不透明但对程序来说是透明的。由于边车代理的定义就是一个与应用共享网络名称空间的辅助容器这天然就契合了Pod的设定。因此在Kubernetes中要进行手动注入是十分简单的——就只是为Pod增加一个额外容器而已即使没有工具帮助自己修改Pod的Manifest也能轻易办到。如果你以前未曾尝试过不妨找一个Pod的配置文件用istioctl kube-inject -f YOUR_POD.YAML命令来查看一下手动注入会对原有的Pod产生什么变化。
自动注入模式这种接入方式对使用者和程序都是透明的也是Istio推荐的代理注入方式。在Kubernetes中服务网格一般是依靠“动态准入控制”Dynamic Admission Control中的Mutating Webhook控制器来实现自动注入的。
额外知识-
istio-proxy是Istio对Envoy代理的包装容器其中包含用Golang编写的pilot-agent和用C++编写的envoy两个进程。pilot-agent进程负责Envoy的生命周期管理比如启动、重启、优雅退出等并维护Envoy所需的配置信息比如初始化配置、随时根据控制平面的指令热更新Envoy的配置等。
这里我以Istio自动注入边车代理istio-proxy容器的过程为例给你介绍一下自动注入的具体的流程。只要你对Istio有基本的了解你应该就能都知道对任何设置了istio-injection=enabled标签的名称空间Istio都会自动为其中新创建的Pod注入一个名为istio-proxy的容器。之所以能做到自动这一点是因为Istio预先在Kubernetes中注册了一个类型为MutatingWebhookConfiguration的资源它的主要内容如下所示
apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
metadata:
name: istio-sidecar-injector
.....
webhooks:
- clientConfig:
service:
name: istio-sidecar-injector
namespace: istio-system
path: /inject
name: sidecar-injector.istio.io
namespaceSelector:
matchLabels:
istio-injection: enabled
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- pods
以上配置其实就告诉了Kubernetes对于符合标签istio-injection: enabled的名称空间在Pod资源进行CREATE操作时应该先自动触发一次Webhook调用调用的位置是istio-system名称空间中的服务istio-sidecar-injector调用具体的URL路径是/inject。
在这次调用中Kubernetes会把拟新建Pod的元数据定义作为参数发送给此HTTP Endpoint然后从服务返回结果中得到注入了边车代理的新Pod定义以此自动完成注入。
流量劫持
边车代理做流量劫持最典型的方式是基于iptables进行的数据转发我曾在“Linux网络虚拟化”这个小章节中介绍过Netfilter与iptables的工作原理。这里我仍然以Istio为例它在注入边车代理后除了生成封装Envoy的istio-proxy容器外还会生成一个initContainer这个initContainer的作用就是自动修改容器的iptables具体内容如下所示
initContainers:
image: docker.io/istio/proxyv2:1.5.1
name: istio-init
- command:
- istio-iptables -p "15001" -z "15006"-u "1337" -m REDIRECT -i '*' -x "" -b '*' -d 15090,15020
以上命令行中的istio-iptables是Istio提供的用于配置iptables的Shell脚本这行命令的意思是让边车代理拦截所有的进出Pod的流量包括拦截除15090、15020端口这两个分别是Mixer和Ingress Gateway的端口关于Istio占用的固定端口你可以参考官方文档所列的信息外的所有入站流量全部转发至15006端口Envoy入站端口经Envoy处理后再从15001端口Envoy出站端口发送出去。
这个命令会在iptables中的PREROUTING和OUTPUT链中挂载相应的转发规则使用iptables -t nat -L -v命令你可以查看到如下所示配置信息
Chain PREROUTING
pkts bytes target prot opt in out source destination
2701 162K ISTIO_INBOUND tcp -- any any anywhere anywhere
Chain OUTPUT
pkts bytes target prot opt in out source destination
15 900 ISTIO_OUTPUT tcp -- any any anywhere anywhere
Chain ISTIO_INBOUND (1 references)
pkts bytes target prot opt in out source destination
0 0 RETURN tcp -- any any anywhere anywhere tcp dpt:ssh
2 120 RETURN tcp -- any any anywhere anywhere tcp dpt:15090
2699 162K RETURN tcp -- any any anywhere anywhere tcp dpt:15020
0 0 ISTIO_IN_REDIRECT tcp -- any any anywhere anywhere
Chain ISTIO_IN_REDIRECT (3 references)
pkts bytes target prot opt in out source destination
0 0 REDIRECT tcp -- any any anywhere anywhere redir ports 15006
Chain ISTIO_OUTPUT (1 references)
pkts bytes target prot opt in out source destination
0 0 RETURN all -- any lo 127.0.0.6 anywhere
0 0 ISTIO_IN_REDIRECT all -- any lo anywhere !localhost owner UID match 1337
0 0 RETURN all -- any lo anywhere anywhere ! owner UID match 1337
15 900 RETURN all -- any any anywhere anywhere owner UID match 1337
0 0 ISTIO_IN_REDIRECT all -- any lo anywhere !localhost owner GID match 1337
0 0 RETURN all -- any lo anywhere anywhere ! owner GID match 1337
0 0 RETURN all -- any any anywhere anywhere owner GID match 1337
0 0 RETURN all -- any any anywhere localhost
0 0 ISTIO_REDIRECT all -- any any anywhere anywhere
Chain ISTIO_REDIRECT (1 references)
pkts bytes target prot opt in out source destination
0 0 REDIRECT tcp -- any any anywhere anywhere redir ports 1
实际上用iptables进行流量劫持是最经典、最通用的手段。不过iptables重定向流量必须通过回环设备Loopback交换数据流量不得不多穿越一次协议栈如下图所示。
其实这种方案在网络I/O不构成主要瓶颈的系统中并没有什么不妥但在网络敏感的大并发场景下会因转发而损失一定的性能。因而目前如何实现更优化的数据平面流量劫持仍然是服务网格发展的前沿研究课题之一。
其中一种可行的优化方案是使用eBPFExtended Berkeley Packet Filter技术在Socket层面直接完成数据转发而不需要再往下经过更底层的TCP/IP协议栈的处理从而减少它数据在通信链路的路径长度。
另一种可以考虑的方案是让服务网格与CNI插件配合来实现流量劫持比如Istio就有提供自己实现的CNI插件。只要安装了这个CNI插件整个虚拟化网络都由Istio自己来控制那自然就无需再依赖iptables也不必存在initContainers配置和istio-init容器了。
这种方案有很高的上限与自由度不过要实现一个功能全面、管理灵活、性能优秀、表现稳定的CNI网络插件决非易事连Kubernetes自己都迫不及待想从网络插件中脱坑其麻烦程度可想而知因此目前这种方案使用并不广泛。
流量劫持技术的发展与服务网格的落地效果密切相关有一些服务网格通过基座模式中的SDK也能达到很好的转发性能但考虑到应用程序通用性和环境迁移等问题无侵入式的低时延、低管理成本的流量劫持方案仍然是研究的主流方向。
可靠通信
注入边车代理、劫持应用流量,最终的目的都是为了代理能够接管应用程序的通信,然而,在代理接管了应用的通信之后,它会做什么呢?这个问题的答案是:不确定。
代理的行为需要根据控制平面提供的策略来决定传统的代理程序比如HAProxy、Nginx是使用静态配置文件来描述转发策略的而这种静态配置很难跟得上应用需求的变化与服务扩缩时网络拓扑结构的变动。
因此针对这个问题Envoy在这方面进行了创新它将代理的转发的行为规则抽象成Listener、Router、Cluster三种资源。以此为基础它又定义了应该如何发现和访问这些资源的一系列API现在这些资源和API被统称为“xDS协议族”。自此以后数据平面就有了如何描述各种配置和策略的事实标准控制平面也有了与控制平面交互的标准接口目前xDS v3.0协议族已经包含有以下具体协议:
这里我就不逐一介绍这些协议了但我要给你说明清楚它们一致的运作原理。其中的关键是解释清楚这些协议的共同基础即Listener、Router、Cluster三种资源的具体含义。
Listener
Listener可以简单理解为Envoy的一个监听端口用于接收来自下游应用程序Downstream的数据。Envoy能够同时支持多个Listener且不同的Listener之间的策略配置是相互隔离的。
自动发现Listener的服务被称为LDSListener Discovery Service它是所有其他xDS协议的基础如果没有LDS也没有在Envoy启动时静态配置Listener的话其他所有xDS服务也就失去了意义因为没有监听端口的Envoy不能为任何应用提供服务。
Cluster
Cluster是Envoy能够连接到的一组逻辑上提供相同服务的上游Upstream主机。Cluster包含该服务的连接池、超时时间、Endpoints地址、端口、类型等信息。具体到Kubernetes环境下可以认为Cluster与Service是对等的概念但是Cluster实际上还承担了服务发现的职责。
自动发现Cluster的服务被称为CDSCluster Discovery Service通常情况下控制平面会将它从外部环境中获取的所有可访问服务全量推送给Envoy。与CDS紧密相关的另一种服务是EDSEndpoint Discovery Service。当Cluster的类型被标识为需要EDS时则说明该Cluster的所有Endpoints地址应该由xDS服务下发而不是依靠DNS服务去解析。
Router
Listener负责接收来自下游的数据Cluster负责将数据转发送给上游的服务而Router则决定Listener在接收到下游的数据之后具体应该将数据交给哪一个Cluster处理。由此定义可知Router实际上是承担了服务网关的职责。
自动发现Router的服务被称为RDSRouter Discovery ServiceRouter中最核心的信息是目标Cluster及其匹配规则即实现网关的路由职能。此外根据Envoy中的插件配置情况也可能包含重试、分流、限流等动作实现网关的过滤器职能。
Envoy的另外一个设计重点是它的Filter机制Filter通俗地讲就是Envoy的插件通过Filter机制Envoy就可以提供强大的可扩展能力。插件不仅是无关重要的外围功能很多Envoy的核心功能都是用Filter来实现的比如对HTTP流量的治理、Tracing机制、多协议支持等等。
另外利用Filter机制Envoy理论上还可以实现任意协议的支持以及协议之间的转换也可以在实现对请求流量进行全方位的修改和定制的同时还保持较高的可维护性。
控制平面
如果说数据平面是行驶中的车辆,那控制平面就是车辆上的导航系统;如果说数据平面是城市的交通道路,那控制平面就是路口的指示牌与交通信号灯。控制平面的特点是不直接参与程序间通信,只会与数据平面中的代理通信。在程序不可见的背后,默默地完成下发配置和策略,指导数据平面工作。
由于服务网格暂时没有大规模引入计算机网络中管理平面Management Plane等其他概念所以控制平面通常也会附带地实现诸如网络行为的可视化、配置传输等一系列管理职能其实还是有专门的管理平面工具的比如Meshery、ServiceMeshHub。这里我仍然以Istio为例具体介绍一下控制平面的主要功能。
Istio在1.5版本之前Istio自身也是采用微服务架构开发的它把控制平面的职责分解为Mixer、Pilot、Galley、Citadel四个模块去实现其中Mixer负责鉴权策略与遥测Pilot负责对接Envoy的数据平面遵循xDS协议进行策略分发Galley负责配置管理为服务网格提供外部配置感知能力Citadel负责安全加密提供服务和用户层面的认证和鉴权、管理凭据和RBAC等安全相关能力。
不过经过两、三年的实践应用很多用户都在反馈Istio的微服务架构有过度设计的嫌疑。lstio在定义项目目标时曾非常理想化地提出控制平面的各个组件都应可以独立部署然而在实际的应用场景里却并不是这样独立的组件反而带来了部署复杂、职责划分不清晰等问题。
图片来自Istio官方文档
因此从1.5版本起Istio重新回归单体架构把Pilot、Galley、Citadel的功能全部集成到新的Istiod之中。当然这也并不是说完全推翻之前的设计只是将原有的多进程形态优化成单进程的形态让之前各个独立组件变成了Istiod的内部逻辑上的子模块而已。
单体化之后出现的新进程Istiod就承担所有的控制平面职责具体包括以下几种。
1. 数据平面交互:这是部分是满足服务网格正常工作所需的必要工作。
具体包括以下几个方面:
边车注入在Kubernetes中注册Mutating Webhook控制器实现代理容器的自动注入并生成Envoy的启动配置信息。
策略分发接手了原来Pilot的核心工作为所有的Envoy代理提供符合xDS协议的策略分发的服务。
配置分发接手了原来Galley的核心工作负责监听来自多种支持配置源的数据比如kube-apiserver本地配置文件或者定义为网格配置协议Mesh Configuration ProtocolMCP的配置信息。原来Galley需要处理的API校验和配置转发功能也包含在内。
2. 流量控制:这通常是用户使用服务网格的最主要目的。
具体包括以下几个方面:
请求路由通过VirtualService、DestinationRule 等Kubernetes CRD资源实现了灵活的服务版本切分与规则路由。比如根据服务的迭代版本号如v1.0版、v2.0版、根据部署环境如Development版、Production版作为路由规则来控制流量实现诸如金丝雀发布这类应用需求。
流量治理包括熔断、超时、重试等功能比如通过修改Envoy的最大连接数实现对请求的流量控制通过修改负载均衡策略在轮询、随机、最少访问等方式间进行切换通过设置异常探测策略将满足异常条件的实例从负载均衡池中摘除以保证服务的稳定性等等。
调试能力:包括故障注入和流量镜像等功能,比如在系统中人为设置一些故障,来测试系统的容错稳定性和系统恢复的能力。又比如通过复制一份请求流量,把它发送到镜像服务,从而满足 A/B验证的需要。
3. 通信安全:包括通信中的加密、凭证、认证、授权等功能。
具体包括以下几个方面:
生成CA证书接手了原来Galley的核心工作负责生成通信加密所需私钥和CA证书。
SDS服务代理最初Istio是通过Kubernetes的Secret卷的方式将证书分发到Pod中的从Istio 1.1之后改为通过SDS服务代理来解决。这种方式保证了私钥证书不会在网络中传输仅存在于SDS代理和Envoy的内存中证书刷新轮换也不需要重启Envoy。
认证:提供基于节点的服务认证和基于请求的用户认证,这项功能我曾在服务安全的“认证”中详细介绍过。
授权:提供不同级别的访问控制,这项功能我也曾在服务安全的“授权”中详细介绍过。
4. 可观测性:包括日志、追踪、度量三大块能力。
具体包括以下几个方面:
日志收集程序日志的收集并不属于服务网格的处理范畴通常会使用ELK Stack去完成这里是指远程服务的访问日志的收集对等的类比目标应该是以前Nginx、Tomcat的访问日志。
链路追踪为请求途经的所有服务生成分布式追踪数据并自动上报运维人员可以通过Zipkin等追踪系统从数据中重建服务调用链开发人员可以借此了解网格内服务的依赖和调用流程。
指标度量:基于四类不同的监控标识(响应延迟、流量大小、错误数量、饱和度)生成一系列观测不同服务的监控指标,用于记录和展示网格中服务状态。
小结
容器编排系统管理的最细粒度只能到达容器层次,在此粒度之下的技术细节,仍然只能依赖程序员自己来管理,编排系统很难提供有效的支持。
2016年原Twitter基础设施工程师威廉·摩根William Morgan和奥利弗·古尔德Oliver Gould在GitHub上发布了第一代的服务网格产品Linkerd并在很短的时间内围绕着Linkered组建了Buoyant公司。而后担任CEO的威廉·摩根在发表的文章《Whats A Service Mesh? And Why Do I Need One?》中首次正式地定义了“服务网格”Service Mesh一词。
此后,服务网格作为一种新兴通信理念开始迅速传播,越来越频繁地出现在各个公司以及技术社区的视野中。之所以服务网格能够获得企业与社区的重视,就是因为它很好地弥补了容器编排系统对分布式应用细粒度管控能力高不足的缺憾。
说实话,服务网格并不是什么神秘难以理解的黑科技,它只是一种处理程序间通信的基础设施,典型的存在形式是部署在应用旁边,一对一为应用提供服务的边车代理,以及管理这些边车代理的控制程序。
“边车”Sidecar本来就是一种常见的容器设计模式用来形容外挂在容器身上的辅助程序。早在容器盛行以前边车代理就就已经有了成功的应用案例。
比如2014年开始的Netflix Prana项目由于Netfilix OSS套件是用Java语言开发的为了让非JVM语言的微服务比如以Python、Node.js编写的程序也同样能接入Netfilix OSS生态享受到Eureka、Ribbon、Hystrix等框架的支持Netflix建立了Prana项目它的作用是为每个服务都提供一个专门的HTTP Endpoint以此让非JVM语言的程序能通过访问该Endpoint来获取系统中所有服务的实例、相关路由节点、系统配置参数等在Netfilix组件中管理的信息。
Netflix Prana的代理需要由应用程序主动去访问才能发挥作用但在容器的刻意支持下服务网格不需要应用程序的任何配合就能强制性地对应用通信进行管理。
它使用了类似网络攻击里中间人流量劫持的手段,完全透明(既无需程序主动访问,也不会被程序感知到)地接管容器与外界的通信,把管理的粒度从容器级别细化到了每个单独的远程服务级别,这就让基础设施干涉应用程序、介入程序行为的能力大为增强。
如此一来,云原生希望用基础设施接管应用程序非功能性需求的目标,就能更进一步。从容器粒度延伸到远程访问,分布式系统继容器和容器编排之后,又发掘到了另一块更广袤的舞台空间。
一课一思
服务网格中数据平面、控制平面的概念是从计算机网络中的SDN软件定义网络借用过来的在此之前你是否有接触过SDN方面的知识呢它与今天的服务网格有哪些联系与差异
欢迎在留言区分享你的答案和见解。如果你觉得有收获,也欢迎把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。