通过使用一个真实的使用场景,我们探讨 Istio 如何路由 TCP 流量,以及如何克服我们亲身遇到的一些常见陷阱。
概述
我最近遇到一个 Istio 的设置,下游(客户端)和上游(服务器)都在使用同一组端口。
1. 8080
端口用于 HTTP 协议
2. 5701
端口用于 Hazelcast 协议,这是一个基于 Java 的内存数据库,嵌入到 pod 的工作负载中并使用 TCP 协议
这里介绍了该设置:
理解 Istio 和 TCP 服务
理论上,这里有两种类型的通信发生:
- 每一个 Hazelcast 数据库(上图中红色和紫色的圆柱体)通过 TCP 协议在 5701 端口互相通信。首先通过 Hazelcast Kubernetes 插件 发现集群,将该插件设置为调用 API 以获取 Pod IP。然后在 TCP 层面使用 pod 的 IP:port 进行连接。
manager
在 HTTP 8080 端口上调用app
我们现在关注第一种通信类型的连接,特别是发生在 manager Pod 之间的连接,因为它们经由了 Istio Proxy。
让我们首先利用 istioctl
CLI 来获取其中一个 pod 上的监听器的配置。
istioctl pc listeners manager-c844dbb5f-ng5d5.manager --port 5701
ADDRESS PORT TYPE
10.12.0.11 5701 TCP
10.0.23.154 5701 TCP
10.0.18.143 5701 TCP
我们有 3 个端口号是 5701
的 entry。它们的类型都是我们定义好的 TCP。 可以清楚地看到,除了本地 IP(
10.12.0.11
)有一个 entry 外,其余使用 5701
端口的每个服务如 manager manager
(10.0.23.154
) 和 app app
服务 (10.0.18.143
).
inbound (入站) 连接
第一个 entry (地址为 10.12.0.11
) 是 INBOUND 监听器,当连接进入 Pod 时监听器被使用。 由于我们正在运行一个 TCP 服务,因此该 Inbound 连接没有经由路由,而是直接指向一个集群,集群名是 inbound|5701|tcp-hazelcast|manager.manager.svc.cluster.local
检查 5701 5701
端口上的所有集群:
istioctl pc clusters manager-7948dffbdd-p44xx.manager --port 5701
SERVICE FQDN PORT SUBSET DIRECTION TYPE
app.app.svc.cluster.local 5701 - outbound EDS
manager.manager.svc.cluster.local 5701 - outbound EDS
manager.manager.svc.cluster.local 5701 tcp-hazelcast inbound STATIC
最后一条是我们上面提到的 INBOUND, 检查它。
istioctl pc clusters manager-7948dffbdd-p44xx.manager --port 5701 --direction inbound -o json
[
{
"name": "inbound|5701|tcp-hazelcast|manager.manager.svc.cluster.local",
"type": "STATIC",
"connectTimeout": "1s",
"loadAssignment": {
"clusterName": "inbound|5701|tcp-hazelcast|manager.manager.svc.cluster.local",
"endpoints": [
{
"lbEndpoints": [
{
"endpoint": {
"address": {
"socketAddress": {
"address": "127.0.0.1",
"portValue": 5701
}
}
}
}
]
}
]
},
"circuitBreakers": {
"thresholds": [
{
"maxConnections": 4294967295,
"maxPendingRequests": 4294967295,
"maxRequests": 4294967295,
"maxRetries": 4294967295
}
]
}
}
]
检查 lbEndpoints
键对应的内容,发现该集群只是将连接转发到本地 (127.0.0.1
) 的 5701
端口,即我们的 app
。
Outbound (出站) 连接
outbound 连接是从 pod 内部发起,到达外部资源。
从我们看到的情况来看,有两个已知的 endpoint (端点) 定义了 5701
端口: manager.manager
服务和 app.app
服务。
检查对 manager
的内容:
istioctl pc listeners manager-7948dffbdd-p44xx.manager --port 5701 --address 10.0.23.154 -o json
[
{
"name": "10.0.23.154_5701",
"address": {
"socketAddress": {
"address": "10.0.23.154",
"portValue": 5701
}
},
"filterChains": [
{
"filters": [
{
"name": "envoy.tcp_proxy",
"typedConfig": {
"[@type](https://twitter.com/type)": "type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy",
"statPrefix": "outbound|5701||manager.manager.svc.cluster.local",
"cluster": "outbound|5701||manager.manager.svc.cluster.local",
"accessLog": [
...
]
}
}
]
}
],
"deprecatedV1": {
"bindToPort": false
},
"trafficDirection": "OUTBOUND"
}
]
我们查看到了一条 filterChain 和一个名为 envoy.tcp.proxy filter的 filter。
这里,代理再次将我们指向名为 outbound|5701||manager.manager.svc.cluster.local
的集群。
Envoy 并没有使用任何路由,因为我们使用的是 TCP 协议,而且除了 IP 和端口之外我们没有任何其他的路由依据。
查看
istioctl pc clusters manager-7948dffbdd-p44xx.manager --port 5701 --fqdn manager.manager.svc.cluster.local --direction outbound -o json
[
{
"transportSocketMatches": [
{
"name": "tlsMode-istio",
"match": {
"tlsMode": "istio"
},
...
}
},
{
"name": "tlsMode-disabled",
"match": {},
"transportSocket": {
"name": "envoy.transport_sockets.raw_buffer"
}
}
],
"name": "outbound|5701||manager.manager.svc.cluster.local",
"type": "EDS",
"edsClusterConfig": {
"edsConfig": {
"ads": {}
},
"serviceName": "outbound|5701||manager.manager.svc.cluster.local"
},
"connectTimeout": "1s",
"circuitBreakers": {
...
},
"filters": [
...
]
}
]
关注重要的信息:
- transportSocketMatches中的前两个 block:Envoy 会检查是否可以使用 SSL (TLS),如果可以就设置证书, 否则使用普通 TCP。
- 然后使用
EDS
协议找到目的地 pod。 EDS 是 Endpoint Discovery Service 端点发现服务的缩写。 - Envoy 为以下服务查找其端点列表。
outbound|5701||manager.manager.svc.cluster.local
- 这些端点是在 Kubernetes 服务端点列表(
kubectl get endpoints -n manager manager
)的基础上选择的。
我们也可以检查 Istio 中配置的端点列表。
istioctl pc endpoints manager-7948dffbdd-p44xx.manager --cluster "outbound|5701||manager.manager.svc.cluster.local"
ENDPOINT STATUS OUTLIER CHECK CLUSTER
10.12.0.12:5701 HEALTHY OK outbound|5701||manager.manager.svc.cluster.local
10.12.1.6:5701 HEALTHY OK outbound|5701||manager.manager.svc.cluster.local
到目前为止一切顺利。
测试设置
为了演示整个过程,我们连接到其中一个 manager Pod,并在 5701
端口上调用服务。
k -n manager exec -ti manager-7948dffbdd-p44xx -c manager sh
telnet manager.manager 5701
在按了几次回车键后,你应该得到以下答案。
Connected to manager.manager
Connection closed by foreign host
我们使用的服务器其实是一个 HTTPS 的 Web 服务器,期待 TLS 握手...... 但不管怎样,我们只想连接到这里的 TCP 端口。
重复这个命令多次。
让我们看看来自 Istio-Proxy sidecars 的日志。 我在这里使用的是 Stern ,它是一个以简单而优雅的方式从 K8s 转储日志的工具。 如果你没有 Stern,请使用 kubectl logs
(但使用时应当谨慎)。
stern -n manager manager -c istio-proxy
manager-7948dffbdd-p44xx istio-proxy [2020-07-23T14:26:27.081Z] "- - -" 0 - "-" "-" 6 0 506 - "-" "-" "-" "-" "10.12.0.11:5701" outbound|5701||manager.manager.svc.cluster.local 10.12.0.11:51100 10.0.23.154:5701 10.12.0.11:47316 - -
manager-7948dffbdd-p44xx istio-proxy [2020-07-23T14:26:27.081Z] "- - -" 0 - "-" "-" 6 0 506 - "-" "-" "-" "-" "127.0.0.1:5701" inbound|5701|tcp-hazelcast|manager.manager.svc.cluster.local 127.0.0.1:59430 10.12.0.11:5701 10.12.0.11:51100 outbound_.5701_._.manager.manager.svc.cluster.local -
manager-7948dffbdd-p44xx istio-proxy [2020-07-23T14:26:08.632Z] "- - -" 0 - "-" "-" 6 0 521 - "-" "-" "-" "-" "10.12.1.6:5701" outbound|5701||manager.manager.svc.cluster.local 10.12.0.11:49150 10.0.23.154:5701 10.12.0.11:47258 - -
manager-7948dffbdd-sh7rx istio-proxy [2020-07-23T14:26:08.634Z] "- - -" 0 - "-" "-" 6 0 519 - "-" "-" "-" "-" "127.0.0.1:5701" inbound|5701|tcp-hazelcast|manager.manager.svc.cluster.local 127.0.0.1:57844 10.12.2.8:5701 10.12.0.11:49150 outbound_.5701_._.manager.manager.svc.cluster.local -
我将请求两两分组,获得了两对不同的日志。
1. 第一条日志是 outbound 连接到 manager.manager.svc
2. 第二条日志是 inbound 连接到本地
3. 第三条日志是 outbound 连接到 manager.manager.svc
4. 第四条日志是在第二个 manager Pod(10.12.2.8:5701
)上的 inbound 连接
当然,Istio 默认使用的是 round-robin 负载均衡算法,所以它完全可以解释这里发生了什么。连续的请求会转到不同的 pod。
下图中蓝色的连线是 outbound 的,粉色的连线是 inbound 的。
好吧,其实不是这样的!我骗了你!Istio(Envoy)不向 Kubernetes 服务发送流量。Istiod(Pilot)用服务构建网格拓扑,然后将信息发送到每个 Istio-proxy,再由 Istio-proxy 向 Pod 发送流量。最后看起来更像这样。(下图中的 ip 地址 10.120.0.11 应改为 10.12.0.11)
但 Hazelcast 服务器也不完全是这样工作的!
Hazelcast 集群通信
真相是 Hazelcast 并没有使用服务名称进行通信。
事实上,它利用 Kubernetes API(或 Headless 服务)来了解集群中所有的 Pod。我不清楚它当时使用的是 Pod 的 FQDN 还是它的 IP,这对我们来说并不重要。
就像每一个使用 "智能" 客户端的应用一样,比如 Kafka,每个实例都需要直接与集群中的其他每个实例通信。
那么,如果我们尝试用第二个 manager Pod 的 IP 来调用它,会发生什么呢?
manager-7948dffbdd-p44xx istio-proxy [2020-07-23T14:39:12.587Z] "- - -" 0 - "-" "-" 6 0 2108 - "-" "-" "-" "-" "10.12.2.8:5701" PassthroughCluster 10.12.0.11:51428 10.12.2.8:5701 10.12.0.11:51426 - -
manager-7948dffbdd-sh7rx istio-proxy [2020-07-23T14:39:13.590Z] "- - -" 0 - "-" "-" 6 0 1113 - "-" "-" "-" "-" "127.0.0.1:5701" inbound|5701|tcp-hazelcast|manager.manager.svc.cluster.local 127.0.0.1:59986 10.12.2.8:5701 10.12.0.11:51428 - -'
1. outbound 连接使用的是 Passthrough 集群,因为网格内部不知道目的地 IP
2. 与之前相同,上游连接使用 inbound 集群
尽管这不是很理想,但至少它是可以工作的。
事情可能变得更糟糕
后来,集群中发生了一些奇怪的事情。
在某些时候,当 manager
试图连接到 Hazelcast 端口时,该连接被路由到 manager
namespace 中的 idle
pod。
这怎么可能? 这个 idle
Pod/Service 甚至没有暴露t 5701
端口!
示意图是这样的:
在 manager Namespace 中没有发生任何变化,但是我查看了app
Namespace 里面的 Services ,看到增加了一个 ExternalName
Service。
kubectl get svc -n app
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
app ClusterIP 10.0.18.143 8080/TCP,5701/TCP 18h
app-ext ExternalName idle.manager.svc.cluster.local 8080/TCP,5701/TCP 117s
服务类型 ExternalName
并不是定义了一个持有 active target pod 列表的内部负载均衡器,而只是到另外 Service 的 CNAME。
这是它的定义。
apiVersion: v1
kind: Service
metadata:
labels:
app/name: app
name: app-ext
namespace: app
spec:
ports:
- name: http-app
port: 8080
protocol: TCP
targetPort: 8080
- name: tcp-hazelcast
port: 5701
protocol: TCP
targetPort: 5701
externalName: idle.manager.svc.cluster.local
sessionAffinity: None
type: ExternalName
以上的 Service 定义使得 app-ext.app.svc.cluster.local
这个名字可以被解析为 idle.manager.svc.cluster.local
(即 CNAME,然后被解析为服务的 IP,10.0.23.221)。
我们再来查看监听 manager
Pod 的 listener。
istioctl pc listeners manager-7948dffbdd-p44xx.manager --port 5701
ADDRESS PORT TYPE
10.12.0.12 5701 TCP
10.0.18.143 5701 TCP
10.0.23.154 5701 TCP
0.0.0.0 5701 TCP
现在出现了一条新的 0.0.0.0
的 entry!
查看配置:
istioctl pc listeners manager-7948dffbdd-p44xx.manager --port 5701 --address 0.0.0.0 -o json
[
{
"name": "0.0.0.0_5701",
"address": {
"socketAddress": {
"address": "0.0.0.0",
"portValue": 5701
}
},
"filterChains": [
{
"filterChainMatch": {
"prefixRanges": [
{
"addressPrefix": "10.12.0.11",
"prefixLen": 32
}
]
},
"filters": [
{
"name": "envoy.filters.network.wasm",
...
},
{
"name": "envoy.tcp_proxy",
"typedConfig": {
"[@type](https://twitter.com/type)": "type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy",
"statPrefix": "BlackHoleCluster",
"cluster": "BlackHoleCluster"
}
}
]
},
{
"filters": [
{
"name": "envoy.filters.network.wasm",
...
},
{
"name": "envoy.tcp_proxy",
"typedConfig": {
"[@type](https://twitter.com/type)": "type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy",
"statPrefix": "outbound|5701||app-ext.app.svc.cluster.local",
"cluster": "outbound|5701||app-ext.app.svc.cluster.local",
"accessLog": [
...
]
}
}
]
}
],
"deprecatedV1": {
"bindToPort": false
},
"trafficDirection": "OUTBOUND"
}
]
事情变得有点复杂了。
1. 首先接受任何目的地 IP,只要是到端口 5701
2. 然后进入 filterChains
3. 如果真正的目的地是本地(pod IP10.12.0.11
),则放弃请求(将其发送到 BlackHoleCluster)
4. 不然的话使用集群 outbound|5701||app-ext.app.svc.cluster.local
寻找转发地址
查看这个集群:
istioctl pc clusters manager-7948dffbdd-p44xx.manager --fqdn app-ext.app.svc.cluster.local --port 5701 -o json
[
{
"name": "outbound|5701||app-ext.app.svc.cluster.local",
"type": "STRICT_DNS",
"connectTimeout": "1s",
"loadAssignment": {
"clusterName": "outbound|5701||app-ext.app.svc.cluster.local",
"endpoints": [
{
"locality": {},
"lbEndpoints": [
{
"endpoint": {
"address": {
"socketAddress": {
"address": "idle.manager.svc.cluster.local",
"portValue": 5701
}
}
},
这个集群非常简单,它只是将流量转发到服务器 idle.manager.svc.cluster.local
,使用 DNS 获取真正的目的地 IP。
使用 telnet 远程登陆到第二个manager
Pod 中并检查日志。可以看到:
manager-7948dffbdd-p44xx istio-proxy [2020-07-23T14:47:24.040Z] "- - -" 0 UF,URX "-" "-" 0 0 1000 - "-" "-" "-" "-" "10.0.23.221:5701" outbound|5701||app-ext.app.svc.cluster.local - 10.12.1.6:5701 10.12.0.12:52852 - -
1. 请求返回了一个 error:0 UF, URX
从 Envoy doc中可知,UF 指上游连接失败,URX 指达到了 TCP 最大连接尝试次数 。
这是完全正常的,因为 idle
并没有暴露 5701
端口(idle Pod 也没绑定 idle Service)。
2. 请求转发到 outbound|5701||app-ext.app.svc.cluster.local
集群。
什么 ?
一个在另外的 Namespace(app)中创建的 Service 破坏了 Hazelcast 集群?
这里的解释其实很简单。在这个服务被创建之前,真正 Pod 的 IP 在 Mesh 中是未知的,Envoy 使用 Passthrough 集群直接向它发送请求。 现在,IP 仍然是未知的,但被 catchall(捕集器) 0.0.0.0:5710
Listener 匹配,并转发到一个已知的集群,即 outbound|5701||app-ext.app.svc.cluster.local
,而这个 Cluster 指向的是 idle
Service。
解决问题
怎样才能恢复 Hazelcast 集群?
不暴露 5701 端口
其中一个解决方案是不暴露 ExternalName
服务中的 5701
端口。 然后将不存在 0.0.0.0:5701
Listener 且流量将流经 Passthrough
集群。 这对于跟踪网格流量来说并不理想,但它工作得很好。
不使用 ExternalName
另一种解决方案是不使用 ExternalName
。
The Externalname
实际上是在我们期望前往 app
服务的所有调用都转发到 idle.manager
服务的情况下添加的新服务。
除了破坏 Hazelcast 集群,添加 Externalname 服务也意味着我们将不得不删除一个服务,然后将该服务重建为 ExternalName
类型。 而这两个操作都迫使 Istiod(Pilot)重建完整的网格配置并更新网格中的所有代理,包括 Listener 的更改 -- 两次导致所有打开的连接耗尽。
一种可能的方法是为 VirtualService
应用添加一个 app
定义,它只会在我们需要的时候向 idle.manager
发送流量。 这样不会创建或删除任何 Listener,只会更新 app
HTTP Service 的路由。
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: app-idle
spec:
hosts:
- app.app.svc.cluster.local
http:
- name: to-idle
route:
- destination:
host: idle.manager.svc.cluster.local
port:
number: 8080
以上配置表示 app.app.svc.cluster.local
的所有流量都会发送到 idle.manager.svc.cluster.local:8080
.
。当我们想让流量有效的流向 app
应用时,只需要更新这个 VirtualService
配置并将 destination
项设置为导向 app.app.svc.cluster.local
或删除该 VirtualService 配置。
Sidecar
通过最新的 Istio,我们还可以利用 Sidecar
资源的使用来限制 manager
在网格中可以看到的东西。
具体到本例中,我们可以在 ExternalName
服务上使用一个注解,使其只在 app
Namespace 中可见。
apiVersion: v1
kind: Service
metadata:
labels:
app/name: app
annotations:
networking.istio.io/exportTo: "."
name: app-ext
namespace: app
spec:
ports:
- name: http-app
port: 8080
protocol: TCP
targetPort: 8080
- name: tcp-hazelcast
port: 5701
protocol: TCP
targetPort: 5701
externalName: idle.manager.svc.cluster.local
sessionAffinity: None
type: ExternalName
通过添加注解 networking.istio.io/exportTo: “.”
,意为 "只把这个资源导出到它被发布的命名空间中",这个服务不会被 manager
的 Pod 看到,也不会被 app
Namespace 之外的任何 pod 看到。因此不再有 0.0.0.0:5701
:
istioctl pc listeners manager-7948dffbdd-p44xx.manager --port 5701
ADDRESS PORT TYPE
10.0.18.143 5701 TCP
10.12.0.12 5701 TCP
10.0.25.229 5701 TCP
不同的 TCP 端口
如果我们愿意更新应用程序,那还可以使用其他一些解决方案。
我们可以为不同的 TCP 服务使用不同的端口。尽管这在你处理数据库等复杂应用时很难落实,但这是 Istio 中长期以来唯一可用的选项。
我们也可以更新应用程序以使用 TLS,并启用 Server Name Indication(SNI)。Envoy/Istio 可以使用 SNI 为同一端口上的 TCP 服务路由流量,因为 Istio 对待路由 TLS/TCP 流量的 SNI 就像对待 HTTP 流量的 Host header 一样。
结论
首先我想说明的是,在这个演示中没有任何的 Hazelcast 集群被损坏。 以上讨论的问题与 Hazelcast 本身无关,任何使用相同端口的服务集都可能发生上述的问题。Istio 和 Envoy 对 TCP 或未知协议的支持非常有限。当你只需要检查 IP 和端口时,你能做的就不多了。
牢记以下对于配置集群的建议!
- 尽量避免对不同的 TCP 服务使用相同的端口号。
- 始终在端口名中加入协议前缀(`tcp-hazelcast`、`http-fronted`、`grpc-backend`),具体参见 协议选择 文档。
- 尽早添加 Sidecar 资源以限制配置蔓延,并将默认的
exportTo
设置为 Istio 安装中的 namespace local。 - 配置应用程序以通过名称(FQDN)而非 IP 进行通信。
- 总是在 Istio 资源中配置 FQDN(包括 `svc.cluster.local`)。
Sebastien Thomas is a Tetrate engineer specializing in customer reliability and Istio setup.