Z.S.K.'s Records

Istio学习(请求路由分析)

前面大致学习了下Istio的整体架构, 没有涉及它里面的细节, 这次主要说明一下istio是如何将请求进行转发的. 信息量确实比较大.

整体架构

还是放一下Istio整体架构图

各个组件的说明, 请参考这里

请求路由

同其它的云原生工具一样, Istio中做服务发现也是基于Kubernetes的List/Watch机制来做

Pilot中会保存两大类信息

  1. 静态信息: 诸如Pilot实例的地址, mixer对接的后端实例地址等信息
  2. 动态信息: 所有跟应用相关的动态变化信息, 如cluster, endpoint等

这两类信息会被Pilot转化成Pod中的envoy所能识别的Manifest, 通过XDS协议进行下发到envoy中

为了更好地说明路由之间的转发, 在Istio中常用到的CRD概念说明如下

名词解释

  • Sidecar:缺省情况下,Pilot将会把和Envoy Sidecar所在namespace的所有services的相关配置,包括inbound和outbound listenter, cluster, route等,都下发给Enovy。使用Sidecar可以对Pilot向Envoy Sidcar下发的配置进行更细粒度的调整,例如只向其下发该Sidecar 所在服务需要访问的那些外部服务的相关outbound配置。

  • Virtualservice:用于定义路由规则,如根据来源或 Header 制定规则,或在不同服务版本之间分拆流量。

  • DestinationRule:定义目的服务的配置策略以及可路由子集。策略包括断路器、负载均衡以及 TLS 等。

  • xDS: 这里的x是个泛指, xDS是一类发现服务的总称,包含LDS,RDS,CDS,EDS,ADS以及 SDS。Envoy通过xDS API可以动态获取Listener(监听器), Route(路由),Cluster(集群),Endpoint(集群成员)以 及Secret(证书)配置

  • ServiceEntry:可以使用ServiceEntry向Istio中加入附加的服务条目,以使网格内可以向istio 服务网格之外的服务发出请求。

  • Gateway:为网格配置网关,以允许一个服务可以被网格外部访问。

    这里要说明一下Istio gateway vs kubernetes ingress:

    ingress只支持http,无法配置4层协议, gateway支持4-6层的协议,且支持7层的设置与virtualservice绑定

  • EnvoyFilter:可以为Envoy配置过滤器。由于Envoy已经支持Lua过滤器,因此可以通过EnvoyFilter启用Lua过滤器,动态改变Envoy的过滤链行为。我之前一直在考虑如何才能动态扩展Envoy的能力,EnvoyFilter提供了很灵活的扩展性。

今天讲路由转发主要会聚焦在前四个概念上, 后面几个后续再更, 这里不过多介绍.

sidecar注入

自动注入

默认情况下,istio开启了自动注入功能,但namespace是disabled的,可以通过给ns打label,使istio的sidecar自动注入到pod中.

同时, 需要apiserver启动参数–enable-admission -plugins需要开启MutatingAdmissionWebhook

这里简单说明下MutatingAdmissionWebhook的流程

Istio 和 sidecar 配置保存在 istioistio-sidecar-injector 这两个 ConfigMap 中,其中包含了 Go template,自动 sidecar 注入就是将生成 Pod 配置从应用 YAML 文件期间转移MutatingAdmissionWebhook 中

1
2
3
4
5
6
7
#apiserver 
--enable-admission-plugins=NodeRestriction,MutatingAdmissionWebhook

kubectl label namespace default istio-injection=enabled

#如要禁止defaule namespace自动注入sidecar功能,则使用
kubectl label namespace default istio-injection

自动注入功能开启之后,在ns中生成的deploy,statefulset, rs等对象都将会自动添加envoy-proxy容器(用于流量代理)及initcontainer(用于生成iptables规则)

手动注入

如果不开启自动注入的话,在需要的时候也可以手动注入,方法如下

istioctl kube-inject -f <your-app-spec>.yaml | kubectl apply -f -

sidecar 默认不能被注入到 kube-system 和 kube-public,istio-system 这三个 namespace

下面所提到的一些静态配置都是由注入模块生成的,大家可使用kubectl get cm -nistio-system查看具体内容

Example Bookinfo

这里使用istio官网使用的例子进行分析,这里已经事先将gateway中的loadbalance改成了NodePort类型代理出来.

1
2
3
4
5
6
7
8
9
kubectl apply -f samples/bookinfo/platform/kube/bookinfo.yaml

kubectl apply -f samples/bookinfo/networking/bookinfo-gateway.yaml

curl -o /dev/null -s -w "%{http_code}\n" http://10.4.97.112:30732/productpage
# 返回200
# 这里为了简单,没有使用域名.直接通过宿主机ip访问.
# 其中 10.4.97.112为宿主机ip
# 30732为宿主机的随机端口

Istio-agent容器启动

最后生成的pod yaml文件如下:

pod yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
apiVersion: v1
kind: Pod
metadata:
annotations:
sidecar.istio.io/status: '{"version":"8d80e9685defcc00b0d8c9274b60071ba8810537e0ed310ea96c1de0785272c7","initContainers":["istio-init"],"containers":["istio-proxy"],"volumes":["istio-envoy","istio-certs"],"imagePullSecrets":null}'
creationTimestamp: "2020-01-02T11:18:50Z"
generateName: sleep-f8cbf5b76-
labels:
app: sleep
pod-template-hash: f8cbf5b76
security.istio.io/tlsMode: istio
name: sleep-f8cbf5b76-z4g5w
namespace: default
ownerReferences:
- apiVersion: apps/v1
blockOwnerDeletion: true
controller: true
kind: ReplicaSet
name: sleep-f8cbf5b76
uid: 8fbae215-c254-4795-b316-cbe15c72da5f
resourceVersion: "409671"
selfLink: /api/v1/namespaces/default/pods/sleep-f8cbf5b76-z4g5w
uid: 352f3bbb-d77d-4133-9bcd-55e18374f667
spec:
containers:
- command:
- /bin/sleep
- 3650d
image: governmentpaas/curl-ssl
imagePullPolicy: IfNotPresent
name: sleep
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /etc/sleep/tls
name: secret-volume
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
name: sleep-token-t2xgb
readOnly: true
- args:
- proxy
- sidecar
- --domain
- $(POD_NAMESPACE).svc.cluster.local
- --configPath
- /etc/istio/proxy
- --binaryPath
- /usr/local/bin/envoy
- --serviceCluster
- sleep.$(POD_NAMESPACE)
- --drainDuration
- 45s
- --parentShutdownDuration
- 1m0s
- --discoveryAddress
- istio-pilot.istio-system:15010
- --zipkinAddress
- zipkin.istio-system:9411
- --proxyLogLevel=warning
- --proxyComponentLogLevel=misc:error
- --connectTimeout
- 10s
- --proxyAdminPort
- "15000"
- --concurrency
- "2"
- --controlPlaneAuthPolicy
- NONE
- --dnsRefreshRate
- 300s
- --statusPort
- "15020"
- --applicationPorts
- ""
- --trust-domain=cluster.local
env:
- name: POD_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
- name: INSTANCE_IP
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: status.podIP
- name: SERVICE_ACCOUNT
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: spec.serviceAccountName
- name: HOST_IP
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: status.hostIP
- name: ISTIO_META_POD_PORTS
value: |-
[
]
- name: ISTIO_META_CLUSTER_ID
value: Kubernetes
- name: ISTIO_META_POD_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.name
- name: ISTIO_META_CONFIG_NAMESPACE
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
- name: SDS_ENABLED
value: "false"
- name: ISTIO_META_INTERCEPTION_MODE
value: REDIRECT
- name: ISTIO_META_INCLUDE_INBOUND_PORTS
- name: ISTIO_METAJSON_LABELS
value: |
{"app":"sleep","pod-template-hash":"f8cbf5b76"}
- name: ISTIO_META_WORKLOAD_NAME
value: sleep
- name: ISTIO_META_OWNER
value: kubernetes://api/apps/v1/namespaces/default/deployments/sleep
- name: ISTIO_META_MESH_ID
value: cluster.local
image: docker.io/istio/proxyv2:1.4.2
imagePullPolicy: IfNotPresent
name: istio-proxy
ports:
- containerPort: 15090
name: http-envoy-prom
protocol: TCP
readinessProbe:
failureThreshold: 30
httpGet:
path: /healthz/ready
port: 15020
scheme: HTTP
initialDelaySeconds: 1
periodSeconds: 2
successThreshold: 1
timeoutSeconds: 1
resources:
limits:
cpu: "2"
memory: 1Gi
requests:
cpu: 10m
memory: 40Mi
securityContext:
readOnlyRootFilesystem: true
runAsUser: 1337
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /etc/istio/proxy
name: istio-envoy
- mountPath: /etc/certs/
name: istio-certs
readOnly: true
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
name: sleep-token-t2xgb
readOnly: true
dnsPolicy: ClusterFirst
enableServiceLinks: true
initContainers:
- command:
- istio-iptables
- -p
- "15001"
- -z
- "15006"
- -u
- "1337"
- -m
- REDIRECT
- -i
- '*'
- -x
- ""
- -b
- '*'
- -d
- "15020"
image: docker.io/istio/proxyv2:1.4.2
imagePullPolicy: IfNotPresent
name: istio-init
resources:
limits:
cpu: 100m
memory: 50Mi
requests:
cpu: 10m
memory: 10Mi
securityContext:
capabilities:
add:
- NET_ADMIN
runAsNonRoot: false
runAsUser: 0
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
name: sleep-token-t2xgb
readOnly: true
nodeName: g105e1900156
priority: 0
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
serviceAccount: sleep
serviceAccountName: sleep
terminationGracePeriodSeconds: 30
tolerations:
- effect: NoExecute
key: node.kubernetes.io/not-ready
operator: Exists
tolerationSeconds: 300
- effect: NoExecute
key: node.kubernetes.io/unreachable
operator: Exists
tolerationSeconds: 300
volumes:
- name: secret-volume
secret:
defaultMode: 420
optional: true
secretName: sleep-secret
- name: sleep-token-t2xgb
secret:
defaultMode: 420
secretName: sleep-token-t2xgb
- emptyDir:
medium: Memory
name: istio-envoy
- name: istio-certs
secret:
defaultMode: 420
optional: true
secretName: istio.sleep

istio-proxy(envoy)

从上面的pod中可以看到, 其中 envoy的启动命令,涉及的端口如下:

  • 9080: productpage进程对外提供的服务端口
  • 15001: Envoy的Virtual Outbound监听器,iptable会将productpage服务发出的出向流量导入该端口中由Envoy进行处理
  • 15006: Envoy的Virtual Inbound监听器,iptable会将发到productpage的入向流量导入该端口中由Envoy进行处理
  • 15000: Envoy管理端口,该端口绑定在本地环回地址上,只能在Pod内访问。
  • 15090: 指向127.0.0.1:15000/stats/prometheus, 用于对外提供Envoy的性能统计指标
  • 15020: 监测envoy的健康状态的端口. 在iptables中已经将该端口排除,不对该端口的流量进行操作.

istio-init

其中, init容器 istio-init中生成一系列iptables规则后退出, 这些规则用于将流量劫持到envoy容器,详细过程可查看这里

而istio-proxy容器的Dockerfile启动entrypoint为/usr/local/bin/pilot-agent, 加上上述参数形成了istio-proxy容器的启动命令, pid=1的进程为pilot-agent,该进程又启动了/usr/local/bin/envoy,envoy的配置文件为/etc/istio/proxy/envoy-rev0.json

/etc/istio/proxy/envoy-rev0.json的内容里主要定义了各种服务的配置信息.在envoy进行流量转发时会按照这些配置对流量进行处理.

envoy-rev0.json

envoy-rev0.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
{
"node": {
"id": "sidecar~10.244.0.21~sleep-f8cbf5b76-z4g5w.default~default.svc.cluster.local",
"cluster": "sleep.default",
"locality": {
},
"metadata": {"CLUSTER_ID":"Kubernetes","CONFIG_NAMESPACE":"default","EXCHANGE_KEYS":"NAME,NAMESPACE,INSTANCE_IPS,LABELS,OWNER,PLATFORM_METADATA,WORKLOAD_NAME,CANONICAL_TELEMETRY_SERVICE,MESH_ID,SERVICE_ACCOUNT","INCLUDE_INBOUND_PORTS":"","INSTANCE_IPS":"10.244.0.21","INTERCEPTION_MODE":"REDIRECT","ISTIO_PROXY_SHA":"istio-proxy:a7b2578494d79fe098f0e99282e0236946562fa0","ISTIO_VERSION":"1.4.2","LABELS":{"app":"sleep","pod-template-hash":"f8cbf5b76"},"MESH_ID":"cluster.local","NAME":"sleep-f8cbf5b76-z4g5w","NAMESPACE":"default","OWNER":"kubernetes://api/apps/v1/namespaces/default/deployments/sleep","POD_NAME":"sleep-f8cbf5b76-z4g5w","POD_PORTS":"[\n]","SERVICE_ACCOUNT":"sleep","WORKLOAD_NAME":"sleep","app":"sleep","pod-template-hash":"f8cbf5b76"}
},
"stats_config": {
"use_all_default_tags": false,
"stats_tags": [
{
"tag_name": "cluster_name",
"regex": "^cluster\\.((.+?(\\..+?\\.svc\\.cluster\\.local)?)\\.)"
},
{
"tag_name": "tcp_prefix",
"regex": "^tcp\\.((.*?)\\.)\\w+?$"
},
{
"regex": "(response_code=\\.=(.+?);\\.;)|_rq(_(\\.d{3}))$",
"tag_name": "response_code"
},
{
"tag_name": "response_code_class",
"regex": "_rq(_(\\dxx))$"
},
{
"tag_name": "http_conn_manager_listener_prefix",
"regex": "^listener(?=\\.).*?\\.http\\.(((?:[_.[:digit:]]*|[_\\[\\]aAbBcCdDeEfF[:digit:]]*))\\.)"
},
{
"tag_name": "http_conn_manager_prefix",
"regex": "^http\\.(((?:[_.[:digit:]]*|[_\\[\\]aAbBcCdDeEfF[:digit:]]*))\\.)"
},
{
"tag_name": "listener_address",
"regex": "^listener\\.(((?:[_.[:digit:]]*|[_\\[\\]aAbBcCdDeEfF[:digit:]]*))\\.)"
},
{
"tag_name": "mongo_prefix",
"regex": "^mongo\\.(.+?)\\.(collection|cmd|cx_|op_|delays_|decoding_)(.*?)$"
},
{
"regex": "(reporter=\\.=(.+?);\\.;)",
"tag_name": "reporter"
},
{
"regex": "(source_namespace=\\.=(.+?);\\.;)",
"tag_name": "source_namespace"
},
{
"regex": "(source_workload=\\.=(.+?);\\.;)",
"tag_name": "source_workload"
},
{
"regex": "(source_workload_namespace=\\.=(.+?);\\.;)",
"tag_name": "source_workload_namespace"
},
{
"regex": "(source_principal=\\.=(.+?);\\.;)",
"tag_name": "source_principal"
},
{
"regex": "(source_app=\\.=(.+?);\\.;)",
"tag_name": "source_app"
},
{
"regex": "(source_version=\\.=(.+?);\\.;)",
"tag_name": "source_version"
},
{
"regex": "(destination_namespace=\\.=(.+?);\\.;)",
"tag_name": "destination_namespace"
},
{
"regex": "(destination_workload=\\.=(.+?);\\.;)",
"tag_name": "destination_workload"
},
{
"regex": "(destination_workload_namespace=\\.=(.+?);\\.;)",
"tag_name": "destination_workload_namespace"
},
{
"regex": "(destination_principal=\\.=(.+?);\\.;)",
"tag_name": "destination_principal"
},
{
"regex": "(destination_app=\\.=(.+?);\\.;)",
"tag_name": "destination_app"
},
{
"regex": "(destination_version=\\.=(.+?);\\.;)",
"tag_name": "destination_version"
},
{
"regex": "(destination_service=\\.=(.+?);\\.;)",
"tag_name": "destination_service"
},
{
"regex": "(destination_service_name=\\.=(.+?);\\.;)",
"tag_name": "destination_service_name"
},
{
"regex": "(destination_service_namespace=\\.=(.+?);\\.;)",
"tag_name": "destination_service_namespace"
},
{
"regex": "(request_protocol=\\.=(.+?);\\.;)",
"tag_name": "request_protocol"
},
{
"regex": "(response_flags=\\.=(.+?);\\.;)",
"tag_name": "response_flags"
},
{
"regex": "(connection_security_policy=\\.=(.+?);\\.;)",
"tag_name": "connection_security_policy"
},
{
"regex": "(permissive_response_code=\\.=(.+?);\\.;)",
"tag_name": "permissive_response_code"
},
{
"regex": "(permissive_response_policyid=\\.=(.+?);\\.;)",
"tag_name": "permissive_response_policyid"
},
{
"regex": "(cache\\.(.+?)\\.)",
"tag_name": "cache"
},
{
"regex": "(component\\.(.+?)\\.)",
"tag_name": "component"
},
{
"regex": "(tag\\.(.+?)\\.)",
"tag_name": "tag"
}
],
"stats_matcher": {
"inclusion_list": {
"patterns": [
{
"prefix": "reporter="
},
{
"prefix": "component"
},
{
"prefix": "cluster_manager"
},
{
"prefix": "listener_manager"
},
{
"prefix": "http_mixer_filter"
},
{
"prefix": "tcp_mixer_filter"
},
{
"prefix": "server"
},
{
"prefix": "cluster.xds-grpc"
},
{
"suffix": "ssl_context_update_by_sds"
},
]
}
}
},
"admin": {
"access_log_path": "/dev/null",
"address": {
"socket_address": {
"address": "127.0.0.1",
"port_value": 15000
}
}
},
"dynamic_resources": {
"lds_config": {
"ads": {}
},
"cds_config": {
"ads": {}
},
"ads_config": {
"api_type": "GRPC",
"grpc_services": [
{
"envoy_grpc": {
"cluster_name": "xds-grpc"
}
}
]
}
},
"static_resources": {
"clusters": [
{
"name": "prometheus_stats",
"type": "STATIC",
"connect_timeout": "0.250s",
"lb_policy": "ROUND_ROBIN",
"hosts": [
{
"socket_address": {
"protocol": "TCP",
"address": "127.0.0.1",
"port_value": 15000
}
}
]
},
{
"name": "xds-grpc",
"type": "STRICT_DNS",
"dns_refresh_rate": "300s",
"dns_lookup_family": "V4_ONLY",
"connect_timeout": "10s",
"lb_policy": "ROUND_ROBIN",
"hosts": [
{
"socket_address": {"address": "istio-pilot.istio-system", "port_value": 15010}
}
],
"circuit_breakers": {
"thresholds": [
{
"priority": "DEFAULT",
"max_connections": 100000,
"max_pending_requests": 100000,
"max_requests": 100000
},
{
"priority": "HIGH",
"max_connections": 100000,
"max_pending_requests": 100000,
"max_requests": 100000
}
]
},
"upstream_connection_options": {
"tcp_keepalive": {
"keepalive_time": 300
}
},
"http2_protocol_options": { }
}
,
{
"name": "zipkin",
"type": "STRICT_DNS",
"dns_refresh_rate": "300s",
"dns_lookup_family": "V4_ONLY",
"connect_timeout": "1s",
"lb_policy": "ROUND_ROBIN",
"hosts": [
{
"socket_address": {"address": "zipkin.istio-system", "port_value": 9411}
}
]
}
],
"listeners":[
{
"address": {
"socket_address": {
"protocol": "TCP",
"address": "0.0.0.0",
"port_value": 15090
}
},
"filter_chains": [
{
"filters": [
{
"name": "envoy.http_connection_manager",
"config": {
"codec_type": "AUTO",
"stat_prefix": "stats",
"route_config": {
"virtual_hosts": [
{
"name": "backend",
"domains": [
"*"
],
"routes": [
{
"match": {
"prefix": "/stats/prometheus"
},
"route": {
"cluster": "prometheus_stats"
}
}
]
}
]
},
"http_filters": {
"name": "envoy.router"
}
}
}
]
}
]
}
]
}
,
"tracing": {
"http": {
"name": "envoy.zipkin",
"config": {
"collector_cluster": "zipkin",
"collector_endpoint": "/api/v1/spans",
"trace_id_128bit": "true",
"shared_span_context": "false"
}
}
}
}

从Envoy初始化配置文件中,我们可以大致看到Istio通过Envoy来实现服务发现和流量管理的基本原理。即控制面将xDS server信息通过static resource的方式配置到Envoy的初始化配置文件中,Envoy启动后,控制面通过xDS将dynamic resource下发给envoy,包括网格中的service信息及路由规则

可以看一下容器中打开的端口:

从图中可以看到,连着几个控制面应用的端口.

Envoy配置初始化流程

img

  • Pilot-agent根据启动参数和K8S API Server中的配置信息生成Envoy的初始配置文件envoy-rev0.json,该文件告诉Envoy从xDS server中获取动态配置信息,并配置了xDS server的地址信息,即控制面的Pilot。
  • Pilot-agent使用envoy-rev0.json启动Envoy进程。
  • Envoy根据初始配置获得Pilot地址,采用xDS接口从Pilot获取到Listener,Cluster,Route等动态配置信息。
  • Envoy根据获取到的动态配置启动Listener,并根据Listener的配置,结合Route和Cluster对拦截到的流量进行处理

VirtualService、DestinationRule

这两个是istio中最核心的两个东西, 鉴于本篇的篇幅已经很好, 就不在这里展开说明了,后续再更

这里贴个两者的配置格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: my-namespace
namespace: istio
spec:
hosts:
- my-svc #virtualservice 中的规则只影响与 hosts 匹配的请求。hosts可以是通配符前缀或 CIDR 前缀的 DNS 名称,在kubernetes中,全称为my-svc.istio.svc.cluster.local
#注意: 这里的namespace是以virtualservice所在的ns为准,而不是pod所在的ns.
#所以建议写服务完整的fqns以免产生歧义.
http:
- match:
- uri:
prefix: /svc-1
route:
- destination:
host: svc-1.my-namespace.svc.cluster.local
- match:
- uri:
prefix: /svc-2
route:
- destination:
host: svc-2.my-namespace.svc.cluster.local

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: my-destination-rule
spec:
host: svc-1.my-namespace.svc.cluster.local
trafficPolicy:
loadBalancer:
simple: RANDOM
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
trafficPolicy:
loadBalancer:
simple: ROUND_ROBIN
- name: v3
labels:
version: v3

xDS

Xds是istio控制面实现的协议,主要为数据面(envoy, pilot-agent)提供配置中心和服务中心两个功能,并把配置发现和服务发现以一组统一的 xDS 接口提供出来,数据面的 Envoy 通过 xDS 获取需要的信息来做服务间通信和服务治理, 分为以下几种

  • cds(cluster discovery service): 提供相同服务的一组pod(pod中不同的版本为不同的集群)在istio中为一个集群, 类似k8s中的service
  • lds(listener discovery service):
  • rds(route discovery service): 当我们做灰度发布、金丝雀发布时,同一个服务会同时运行多个版本,每个版本对应一个 cluster。这时需要通过 route 规则规定请求如何路由到其中的某个版本的 cluster 上
  • eds(endpoint discovery service):一个具体的「应用实例」,对应 ip 和端口号,类似 Kubernetes 中的一个 Pod
  • ads(aggregated discovery service): 可以通过ads实现在同一个 gRPC 连接上实现多种 xds 来下发配置,从而节省网络连接,ads 还有一个非常重要的作用是解决 cdsrds 信息更新顺序依赖的问题,从而保证以一定的顺序同步各类配置信息

通过istio-pilot接口查看xds数据:

获取istio-pilot的svc的ip,8080是pilot http端口,15010是pilot向envoy提供配置下发端口(grpc协议端口, 会与envoy保持长连接),已不支持curl操作.

不过我们也可以从envoy提供的管理端口15000查询,15000是绑定在envoy容器里回环地址上的端口,因为只能从容器内访问:

1
2
3
export PILOT_SVC_IP=$(kubectl -n istio-system get svc istio-pilot -o go-template='{{.spec.clusterI')}'
# 通过envoy管理端口访问
kubectl exec -it productpage-v1-6d8bc58dd7-ts8kw -c istio-proxy curl http://127.0.0.1:15000/config_dump > config_dump

查看eds

1
curl -s http://$PILOT_SVC_IP:8080/debug/edsz|grep -A100  "outbound|9080||reviews.default.svc.clusterlocal"

查看cds:

1
curl http://$PILOT_SVC_IP:8080/debug/cdsz

查看ads:

1
curl http://$PILOT_SVC_IP:8080/debug/adsz

envoy配置文件详解

envoy的配置文件非常长, 因为包含了整个集群的静态及动态信息, 主要有用的信息分为以下几个红框标识出来的:

  • Bootstrap
  • clusters
  • listeners
  • Routes

bootstrap

文件中的内容和之前介绍的envoy-rev0.json是一致的, 是envoy初始化的配置, 主要包含了如Piolt, prometheus等后端实例的静态地址信息, 分为以下几部分:

  • node: 部分描述了pod相关信息

  • static_resources: 描述了一些静态地址,主要是cluster部分涉及zipkin, prometheus,等路由地址,特别重要的是指定了pilot的地址,如上图红框中所示.

  • Dynamic_resources: 则是envoy启动后,pilot下发的动态配置, 这里主要注意ads_config, 这里指定了ads指向的cluster_name为xds-grpc, 就是上图中的pilot地址.

clusters

Clusters是一个服务集群(类比kubernetes对应serivce的概念),包含一个到多个endpoint,每个endpoint都可以提供服务,Envoy根据负载均衡算法将请求发送到这些endpoint中。

  • static_clusters: 同bootstrap中的static_resources。

  • dynamic_active_clusters : 是通过xDS接口从Istio控制面获取的动态服务信息。

​ 对于一个风格服务来说,比如这里的productpage服务,其它的服务对它来说就是网格外的服务,如果productpage请求review, 这条请求对于productpage来就是出口流量(outbound),所以的出口流量都会先被envoy拦截(iptables定义所有的出口流量都被envoy的15001监听器拦截转发到envoy),对于productpage来说入口(iptables定义所有的入口流量都被envoy的15006监听器拦截转发到envoy)流量只有一个,就是别的服务请求它自己,所以在上面我们只会看到一个(inbound), 它的地址是127.0.0.1,而127.0.0.1上的请求被iptables指定不会经过envoy处理,直接被转到productpage本身了.

​ 同时,对于outbound来说,比如上图中的cluster: outbound|14250||jaeger-collector.istio-system.svc.cluster.local, 则会dymanic_routing_config中找出最终服务的svc ip.

​ 因为outbound|14250||jaeger-collector.istio-system.svc.cluster.local为动态资源,需要通过eds查询得到该cluster中有几个endpoint , 类似使用pilot的debug端口查看cluster的endpoint,当然实际情况会使用缓存

1
curl 10.96.255.184:8080/debug/edsz|grep 'outbound|9080||reviews.default.svc.cluster.local'

listeners

listeners: Envoy采用listener来接收并处理downstream发过来的请求,listener的处理逻辑是插件式的,可以通过配置不同的filter来插入不同的处理逻辑。

  • static_listeners
  • dynamic_active_listeners

routes

routes: 配置Envoy的路由规则。Istio下发的缺省路由规则中对每个端口设置了一个路由规则,根据host来对请求进行路由分发。

  • static_route_configs

  • dynamic_route_configs

endpoint

endpoint对应的就是cluster后端的所有实例列表中的一个了, 这个概念跟kubernetes中的endpoint打平

endpoin是动态获取的,当请求转到cluster时,通过xDS动态查找cluster后端的ep list, 然后根据配置的负载均衡策略选择一个将请求直接转发过去, 而不是像kubernetes中一样需要经过kube-proxy.

具体流程参考下面端到端分析.

envoy端到端调用分析

下图描述了istio对于服务间端到端调用链流量分析.

Productpage服务调用Reviews服务的请求流程

详细分析过程:

当 productpage请求reviews时,(get http://reviews:9080/reviews/0)

  1. 对于productpage业说, 这个请求为向外请求(outbound),所有的outbound请求都会被iptables设置的规则捕获(envoy init容器设置),重新定位到15001端口,在15001端口上监听的Envoy Virtual Outbound Listener收到了该请求

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    {
    "version_info": "2019-12-04T03:08:06Z/13",
    "listener": {
    "name": "virtualOutbound",
    "address": {
    "socket_address": {
    "address": "0.0.0.0", //接受所有15001端口来的流量
    "port_value": 15001
    }
    },
    ......

    "use_original_dst": true //请求转发给和原始目的IP:Port匹配的listener,而不是直接处理
    },
    "last_updated": "2019-12-04T03:08:22.919Z"
    }

    “use_original_dst”: true 参数的主要用途是,Envoy 通过监听 15001 端口将应用的流量截取后再转发给由其他最符合请求原始目标的监听器(如果找不到任何匹配的虚拟监听器,它会将请求发送给返回 404 的 BlackHoleCluster) 处理而不是直接转发出去.默认为false, 后续该参数会被废弃

  2. 请求被Virtual Outbound Listener根据”use_original_dst”: true配置进行转发,根据原目标IP(通配所有的ipv4地址)和端口9080(该端口是请求review应用带的端口),转发到name为0.0.0.0_9080这个outbound listener(这条规则是envoy启动之后动态获取的)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    {
    "version_info": "2019-12-04T03:08:06Z/13",
    "listener": {
    "name": "0.0.0.0_9080",
    "address": {
    "socket_address": {
    "address": "0.0.0.0", //通配所有的ipv4地址
    "port_value": 9080
    }
    },
    "filter_chains": [
    {
    "filters": [
    {
    "name": "envoy.http_connection_manager",
    "typed_config": {
    "@type": "type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager",
    "stat_prefix": "outbound_0.0.0.0_9080",
    "http_filters": [
    {
    "name": "mixer",
    ......
    },
    {
    "name": "envoy.cors"
    },
    {
    "name": "envoy.fault"
    },
    {
    "name": "envoy.router"
    }
    ],
    ......

    "rds": {
    "config_source": {
    "ads": {} //表明是动态获取的
    },
    "route_config_name": "9080" //采用“9080” route进行分发
    }
    }
    }
    ]
    }
    ],
    "deprecated_v1": {
    "bind_to_port": false
    },
    "listener_filters_timeout": "0.100s",
    "traffic_direction": "OUTBOUND",
    "continue_on_listener_filters_timeout": true
    },
    "last_updated": "2019-12-04T03:08:22.822Z"
    }

    从上面可以看到,这个outbound listener的路由转到了”route_config_name”: “9080”,名为9080的route config name.

  3. 找到”route_config_name”= “9080”

​ 从route_config_name=9080来看,下面有好多个virtual_hosts,这说明,存在多个应用是通过9080提供服务,然后根据请求的域名为reviews,可发现route为 “outbound|9080||reviews.default.svc.cluster.local”

​ 这里简单说一下outbound|9080||reviews.default.svc.cluster.local格式

1
2
3
4
outbound说明是出向流量
9080为端口
||之间其实是subset的key,这个是在配置的destinatiorule中定义,如果没有,则为空
reviews.default.svc.cluster.local即是kubernetes中完整的svc的域名.
  1. route= “outbound|9080||reviews.default.svc.cluster.local”

    cluster为动态资源,通过eds查询得到该cluster中有3个endpoint

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    {
    "clusterName": "outbound|9080||reviews.default.svc.cluster.local",
    "endpoints": [
    {
    "lbEndpoints": [
    {
    "endpoint": {
    "address": {
    "socketAddress": {
    "address": "10.40.0.15",
    "portValue": 9080
    }
    }
    },
    "metadata": {
    "filterMetadata": {
    "envoy.transport_socket_match": {
    "tlsMode": "istio"
    },
    "istio": {
    "uid": "kubernetes://reviews-v1-75b979578c-pw8zs.default"
    }
    }
    },
    "loadBalancingWeight": 1
    },
    {
    "endpoint": {
    "address": {
    "socketAddress": {
    "address": "10.40.0.16",
    "portValue": 9080
    }
    }
    },
    "metadata": {
    "filterMetadata": {
    "envoy.transport_socket_match": {
    "tlsMode": "istio"
    },
    "istio": {
    "uid": "kubernetes://reviews-v3-54c6c64795-wbls7.default"
    }
    }
    },
    "loadBalancingWeight": 1
    },
    {
    "endpoint": {
    "address": {
    "socketAddress": {
    "address": "10.40.0.17",
    "portValue": 9080
    }
    }
    },
    "metadata": {
    "filterMetadata": {
    "envoy.transport_socket_match": {
    "tlsMode": "istio"
    },
    "istio": {
    "uid": "kubernetes://reviews-v2-597bf96c8f-l2fp8.default"
    }
    }
    },
    "loadBalancingWeight": 1
    }
    ],
    "loadBalancingWeight": 3
    }
    ]
    }

    请求被转发到其中一个endpoint 10.40.0.15,即Reviews-v1所在的Pod。

    在reviews端,该请求被iptable规则拦截,所有的入向请求(inbound)都被重定向到本地的15006端口。

    在15006端口上监听的Envoy Virtual Inbound Listener收到了该请求。

    根据匹配条件,请求被Virtual Inbound Listener内部配置的Http connection manager filter处理,该filter设置的路由配置为将其发送给inbound|9080|http|reviews.default.svc.cluster.local这个inbound cluster

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    {
    "version_info": "2019-12-04T03:07:44Z/12",
    "listener": {
    "name": "virtualInbound",
    "address": {
    "socket_address": {
    "address": "0.0.0.0",
    "port_value": 15006
    }
    },
    "filter_chains": [
    ......
    {
    "filter_chain_match": {
    "prefix_ranges": [
    {
    "address_prefix": "10.40.0.15",
    "prefix_len": 32
    }
    ],
    "destination_port": 9080
    },
    "filters": [
    {
    "name": "envoy.http_connection_manager",
    "typed_config": {
    "@type": "type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager",
    "stat_prefix": "inbound_10.40.0.15_9080",
    "http_filters": [
    {
    "name": "istio_authn",
    ......
    },
    {
    "name": "mixer",
    ......
    },
    {
    "name": "envoy.cors"
    },
    {
    "name": "envoy.fault"
    },
    {
    "name": "envoy.router"
    }
    ],
    ......
    "route_config": {
    "name": "inbound|9080|http|reviews.default.svc.cluster.local",
    "virtual_hosts": [
    {
    "name": "inbound|http|9080",
    "domains": [
    "*"
    ],
    "routes": [
    {
    "match": {
    "prefix": "/"
    },
    ......
    "route": {
    "timeout": "0s",
    "max_grpc_timeout": "0s",
    "cluster": "inbound|9080|http|reviews.default.svc.cluster.local" //对应的inbound cluster
    }
    }
    ]
    }
    ],
    "validate_clusters": false
    }
    }
    }
    ],
    ......
    }
    1. “route_config_name”=”inbound|9080|http|reviews.default.svc.cluster.local”

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      {
      "version_info": "2019-12-04T03:08:06Z/13",
      "cluster": {
      "name": "inbound|9080|http|productpage.default.svc.cluster.local",
      "type": "STATIC",
      "connect_timeout": "1s",
      "circuit_breakers": {
      "thresholds": [
      {
      "max_connections": 4294967295,
      "max_pending_requests": 4294967295,
      "max_requests": 4294967295,
      "max_retries": 4294967295
      }
      ]
      },
      "load_assignment": {
      "cluster_name": "inbound|9080|http|productpage.default.svc.cluster.local",
      "endpoints": [
      {
      "lb_endpoints": [
      {
      "endpoint": {
      "address": {
      "socket_address": {
      "address": "127.0.0.1", //cluster配置的endpoint地址
      "port_value": 9080
      }
      }
      }
      }
      ]
      }
      ]
      }
      },
      "last_updated": "2019-12-04T03:08:22.658Z"

      到此,一条完整的调用链分析完成.

      当然,以上过程除了可以从配置文件分析而来, 也可以通过Istioctl命令行的方式获取, 这里不展开说明, 后续有时间单独更新.

      需要注意的是,服务网格内的应用仍然通过 ClusterIP 与网格外的应用通信,但有一点需要注意:这里并没有 kube-proxy 的参与!Envoy 自己实现了一套流量转发机制,当你访问 ClusterIP 时,Envoy 就把流量转发到具体的 Pod 上去,不需要借助 kube-proxy 的 iptablesipvs 规则

      可以看出请求首先到达 Listener,然后通过 Http Route Table(HTTP 的路由规则,例如请求的域名,Path 符合什么规则【envoy会将domain中的字符串与请求中的header进行匹配】,从而决定转发给哪个 Cluster) 转到具体的 Cluster,再通过cluster动态查询ep list,最后由具体的某个ep pod对请求做出响应

Istio缺点

从上面的流量分析来看,最影响性能的就是一但集群中的实例数太多, 当有某个实例有更新都要全量的下发整个集群的配置给envoy,envoy需要重启等, 一旦更新的比较频繁, 这必然会影响性能, 所以这块社区也一直在想办法解决, 比如如何按需下发配置、限定只在同一个namespace下的工作负载下发更新配置等,相信不久的将来会有更完美的解决方案.

参考文章:

转载请注明原作者: 周淑科(https://izsk.me)

 wechat
Scan Me To Read on Phone
I know you won't do this,but what if you did?