本文通过一个简单场景,向大家展现istio的服务发现工作原理。
我使用的minikube,大家需自行安装好istio,观看安装指南。
背景
我们将在K8S中创建一个简单的nginx服务,并简单探究istio sidecar是如何无侵入的进行流量管控的。
准备工作
开启自动注入
因为istio sidecar要运行到每个POD中进行流量管控,所以我们需要为所需的namespace开启POD自动注入istio sidecar的功能:
1 |
kubectl label namespace default istio-injection=enabled |
上面就为default命名空间打开了自动注入sidecar容器的功能。
创建nginx
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 |
apiVersion: apps/v1 kind: Deployment metadata: name: my-nginx spec: selector: matchLabels: run: my-nginx replicas: 2 template: metadata: labels: run: my-nginx spec: containers: - name: my-nginx image: nginx --- apiVersion: v1 kind: Service metadata: name: my-nginx spec: ports: - port: 80 protocol: TCP selector: run: my-nginx |
创建包含2个replica的nginx部署,并且分配了一个service关联到这2个POD,开放了80端口的转发。
这个nginx作为测试用的服务端,POD内是有sidecar的,我们要探究流量进入POD时sidecar的行为。
创建ubuntu
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
apiVersion: apps/v1 kind: Deployment metadata: name: my-ubuntu spec: selector: matchLabels: run: my-ubuntu replicas: 1 template: metadata: labels: run: my-ubuntu spec: containers: - name: my-ubuntu image: ubuntu:18.04 command: ["/bin/bash"] args: ["-c", "while true;do sleep 1;done"] |
这个ubuntu作为测试用的客户端,POD内同样有sidecar,我们探究流量离开POD时sidecar的行为。
检查点
此时,我们default namespace下面有3个POD:
1 2 3 4 5 |
root@debian:~/kubernetes/demo# kubectl get pods -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES my-nginx-6f9f464dd6-6skbd 2/2 Running 0 111m 172.17.0.13 debian <none> <none> my-nginx-6f9f464dd6-fjnrk 2/2 Running 0 111m 172.17.0.11 debian <none> <none> my-ubuntu-f464f6885-b9t8l 2/2 Running 0 99m 172.17.0.15 debian <none> <none> |
每个POD里都跑着一个istio-proxy的sidecar,以某个nginx pod为例:
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 |
Containers: istio-proxy: Container ID: docker://7d872c290058f1559d32fb6ed04933753c9e38dba3cdf16316ba7ec402c3a145 Image: docker.io/istio/proxyv2:1.10.0 Image ID: docker-pullable://istio/proxyv2@sha256:88c6c693e67a0f2492191a0e7d8020ddc85603bfc704f252655cb9eb5eeb3f58 Port: 15090/TCP Host Port: 0/TCP Args: proxy sidecar --domain $(POD_NAMESPACE).svc.cluster.local --serviceCluster my-nginx.default --proxyLogLevel=warning --proxyComponentLogLevel=misc:error --log_output_level=default:info --concurrency 2 State: Running Started: Mon, 07 Jun 2021 16:27:01 +0800 Ready: True Restart Count: 0 Limits: cpu: 2 memory: 1Gi Requests: cpu: 10m memory: 40Mi Readiness: http-get http://:15021/healthz/ready delay=1s timeout=3s period=2s #success=1 #failure=30 Environment: JWT_POLICY: third-party-jwt PILOT_CERT_PROVIDER: istiod CA_ADDR: istiod.istio-system.svc:15012 POD_NAME: my-nginx-6f9f464dd6-6skbd (v1:metadata.name) POD_NAMESPACE: default (v1:metadata.namespace) INSTANCE_IP: (v1:status.podIP) SERVICE_ACCOUNT: (v1:spec.serviceAccountName) HOST_IP: (v1:status.hostIP) CANONICAL_SERVICE: (v1:metadata.labels['service.istio.io/canonical-name']) CANONICAL_REVISION: (v1:metadata.labels['service.istio.io/canonical-revision']) PROXY_CONFIG: {} ISTIO_META_POD_PORTS: [ ] ISTIO_META_APP_CONTAINERS: my-nginx ISTIO_META_CLUSTER_ID: Kubernetes ISTIO_META_INTERCEPTION_MODE: REDIRECT ISTIO_META_WORKLOAD_NAME: my-nginx ISTIO_META_OWNER: kubernetes://apis/apps/v1/namespaces/default/deployments/my-nginx ISTIO_META_MESH_ID: cluster.local TRUST_DOMAIN: cluster.local Mounts: /etc/istio/pod from istio-podinfo (rw) /etc/istio/proxy from istio-envoy (rw) /var/lib/istio/data from istio-data (rw) /var/run/secrets/istio from istiod-ca-cert (rw) /var/run/secrets/kubernetes.io/serviceaccount from default-token-j7gmk (ro) /var/run/secrets/tokens from istio-token (rw) my-nginx: Container ID: docker://c461c5b2457d44c96d9a68489bad9cf9e6ca8590fce3434b80f41f48cce4cdac Image: nginx Image ID: docker-pullable://nginx@sha256:6d75c99af15565a301e48297fa2d121e15d80ad526f8369c526324f0f7ccb750 Port: <none> Host Port: <none> State: Running Started: Mon, 07 Jun 2021 16:27:32 +0800 Ready: True Restart Count: 0 Environment: <none> Mounts: /var/run/secrets/kubernetes.io/serviceaccount from default-token-j7gmk (ro) |
探索
场景1)验证服务端sidecar行为
为了单纯探究sidecar收到调用时的inbound方向行为,我们从宿主机上直接请求nginx pod:
1 |
curl 172.17.0.11 |
然后查看nginx pod中sidecar的日志:
kubectl logs -f my-nginx-6f9f464dd6-fjnrk -c istio-proxy
[2021-06-07T08:49:10.789Z] “GET / HTTP/1.1” 200 – via_upstream – “-” 0 612 1 0 “-” “curl/7.64.0” “168fdee8-c869-9029-9ec6-fca5fc73f256” “172.17.0.11” “172.17.0.11:80” inbound|80|| 127.0.0.6:54151 172.17.0.11:80 172.17.0.1:53596 – default
我们发现,istio拦截到了进入 POD的连接,并且通过数据包内容识别出了这是一个HTTP协议的通讯。
同时,istio-proxy明确判断该流量命中了预先下发的inbound|80||规则,随后就将这个调用转发给了POD内的nginx,该转发连接来源地址刻意绑定为127.0.0.6:54151,目标地址是172.17.0.11:80(也就是nginx)。
我们猜测inbound|80|| 规则的下发应该是因为istio发现了k8s集群中的my-nginx service资源,并且意识到该service关联到了该POD,所以才会在该pod内下发这条Inbound入向规则。
为了验证猜测,我们删除my-nginx的service,看一下没有service规则的时候,istio-proxy又如何应对这次访问:
1 |
kubectl delete service my-nginx |
再次请求POD IP:
curl 172.17.0.11
查看日志:
[2021-06-07T08:55:27.740Z] “GET / HTTP/1.1” 200 – via_upstream – “-” 0 612 1 0 “-” “curl/7.64.0” “cb580813-b1e3-9177-9a72-5a6e05edd598” “172.17.0.11” “172.17.0.11:80” InboundPassthroughClusterIpv4 127.0.0.6:59195 172.17.0.11:80 172.17.0.1:57796 – default
我们发现istio拦截到了这个连接,识别了HTTP协议,但此时走了InboundPassthroughClusterIpv4规则,也就是入向流量的透传(passthrough),说白了因为service信息不存在所以istio-proxy就没有inbound|80规则,此时istio-proxy默认行为就是透传,仍旧保持通讯行为正常。
场景2)验证客户端sidecar行为
首先还原一下被删除的nginx service。
然后,我们登录到ubuntu容器内(里面有客户端sidecar),安装一下curl命令(apt update; apt install curl):
1 |
kubectl exec -it my-ubuntu-f464f6885-b9t8l bash |
该ubuntu容器的IP是172.17.0.15,从里面直接调用nginx的pod ip看一下发生什么:
1 |
curl 172.17.0.11 |
然后查看ubuntu中istio-proxy的日志:
1 |
kubectl logs -f my-ubuntu-f464f6885-b9t8l -c istio-proxy |
[2021-06-07T09:00:01.547Z] “GET / HTTP/1.1” 200 – via_upstream – “-” 0 612 0 0 “-” “curl/7.58.0” “3b560874-2939-99b7-9751-0ee75a7c7568” “172.17.0.11” “172.17.0.11:80” PassthroughCluster 172.17.0.15:40690 172.17.0.11:80 172.17.0.15:40688 – allow_any
客户端侧sidecar拦截了这个连接,识别出了http协议,但是因为我们访问的是nginx的POD IP,istio-proxy并不会下发POD IP->Service IP这样的反向关联规则,所以客户端sidecar只能passthrough透传到原始目的地即可。
因此,istio-proxy会建立源IP是172.17.0.15:40690,目标IP是172.17.0.11:80的转发连接,也就是透传给原目的地。
我们可以猜想,如果让ubuntu容器调用nginx的service ip的话,客户端sidecar想必就走到服务发现功能了吧,因此我们在ubuntu容器中调用nginx service ip:
1 |
curl 10.104.15.140 |
[2021-06-08T02:16:54.478Z] “GET / HTTP/1.1” 200 – via_upstream – “-” 0 612 2 2 “-” “curl/7.58.0” “819af402-62c7-9e20-9879-43711d981f2f” “10.104.15.140” “172.17.0.13:80” outbound|80||my-nginx.default.svc.cluster.local 172.17.0.15:43896 10.104.15.140:80 172.17.0.15:46396 – default
客户端istio-proxy拦截到了连接,识别了HTTP协议内容,命中了outbound|80||my-nginx.default.svc.cluster.local 规则,这意味着istio的确下发了一个出向流量规则,对于发往my-nginx这个service的HTTP请求完成了识别,并且基于服务发现将流量转发给了具体的nginx POD:172.17.0.13:80。
我们可以看一下客户端sidecar中这条outbound|80||my-nginx.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 |
root@debian:~/kubernetes/demo# istioctl proxy-config listener my-ubuntu-f464f6885-b9t8l ADDRESS PORT MATCH DESTINATION 10.96.0.10 53 ALL Cluster: outbound|53||kube-dns.kube-system.svc.cluster.local 0.0.0.0 80 Trans: raw_buffer; App: HTTP Route: 80 0.0.0.0 80 ALL PassthroughCluster 10.104.15.140 80 Trans: raw_buffer; App: HTTP Route: my-nginx.default.svc.cluster.local:80 10.104.15.140 80 ALL Cluster: outbound|80||my-nginx.default.svc.cluster.local 10.104.34.183 443 ALL Cluster: outbound|443||istio-ingressgateway.istio-system.svc.cluster.local 10.110.58.240 443 ALL Cluster: outbound|443||istio-egressgateway.istio-system.svc.cluster.local 10.96.0.1 443 ALL Cluster: outbound|443||kubernetes.default.svc.cluster.local 10.98.203.126 443 ALL Cluster: outbound|443||istiod.istio-system.svc.cluster.local 10.96.0.10 9153 Trans: raw_buffer; App: HTTP Route: kube-dns.kube-system.svc.cluster.local:9153 10.96.0.10 9153 ALL Cluster: outbound|9153||kube-dns.kube-system.svc.cluster.local 0.0.0.0 15001 ALL PassthroughCluster 0.0.0.0 15001 Addr: *:15001 Non-HTTP/Non-TCP 0.0.0.0 15006 Addr: *:15006 Non-HTTP/Non-TCP 0.0.0.0 15006 Trans: tls; App: istio-http/1.0,istio-http/1.1,istio-h2; Addr: 0.0.0.0/0 InboundPassthroughClusterIpv4 0.0.0.0 15006 Trans: raw_buffer; App: HTTP; Addr: 0.0.0.0/0 InboundPassthroughClusterIpv4 0.0.0.0 15006 Trans: tls; App: TCP TLS; Addr: 0.0.0.0/0 InboundPassthroughClusterIpv4 0.0.0.0 15006 Trans: raw_buffer; Addr: 0.0.0.0/0 InboundPassthroughClusterIpv4 0.0.0.0 15006 Trans: tls; Addr: 0.0.0.0/0 InboundPassthroughClusterIpv4 0.0.0.0 15010 Trans: raw_buffer; App: HTTP Route: 15010 0.0.0.0 15010 ALL PassthroughCluster 10.98.203.126 15012 ALL Cluster: outbound|15012||istiod.istio-system.svc.cluster.local 0.0.0.0 15014 Trans: raw_buffer; App: HTTP Route: 15014 0.0.0.0 15014 ALL PassthroughCluster 0.0.0.0 15021 ALL Inline Route: /healthz/ready* 10.104.34.183 15021 Trans: raw_buffer; App: HTTP Route: istio-ingressgateway.istio-system.svc.cluster.local:15021 10.104.34.183 15021 ALL Cluster: outbound|15021||istio-ingressgateway.istio-system.svc.cluster.local 0.0.0.0 15090 ALL Inline Route: /stats/prometheus* 10.104.34.183 15443 ALL Cluster: outbound|15443||istio-ingressgateway.istio-system.svc.cluster.local 10.104.34.183 31400 ALL Cluster: outbound|31400||istio-ingressgateway.istio-system.svc.cluster.local |
关键的两行:
10.104.15.140 80 Trans: raw_buffer; App: HTTP Route: my-nginx.default.svc.cluster.local:80
10.104.15.140 80 ALL Cluster: outbound|80||my-nginx.default.svc.cluster.local
对上述规则尝试解读:
istio-proxy如果拦截到一个目标地址是10.104.15.140:80的连接的话,会有2个动作:
1,如果识别出流量是HTTP协议的话,那么走my-nginx.default.svc.cluster.local:80 7层路由配置进一步判断去向。
2,如果识别不是 HTTP的话(也就是TCP流量),那么不做进一步判断,直接负载均衡到集群Cluster: outbound|80||my-nginx.default.svc.cluster.local,其实也就是my-nginx service中的一个随机POD。
进一步看HTTP协议识别出之后的Route路由的话(istioctl proxy-config route my-ubuntu-f464f6885-b9t8l -o json|less),我们会看到如下内容:
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 |
{ "name": "my-nginx.default.svc.cluster.local:80", "virtualHosts": [ { "name": "my-nginx.default.svc.cluster.local:80", "domains": [ "my-nginx.default.svc.cluster.local", "my-nginx.default.svc.cluster.local:80", "my-nginx", "my-nginx:80", "my-nginx.default.svc", "my-nginx.default.svc:80", "my-nginx.default", "my-nginx.default:80", "10.104.15.140", "10.104.15.140:80" ], "routes": [ { "name": "default", "match": { "prefix": "/" }, "route": { "cluster": "outbound|80||my-nginx.default.svc.cluster.local", |
很简单,这条Route规则叫做my-nginx.default.svc.cluster.local:80,它匹配若干domain(你会发现都是my-nginx service的各种domain别名、裸ServiceIP等),然后将流量转发给cluster:outbound|80||my-nginx.default.svc.cluster.local。
总结一下,istio默认行为是同时为k8s service下发了7层和4层转发规则,最终都是将连接转给outbound|80||my-nginx.default.svc.cluster.local集群。
但是默认下发的这些规则除了转发啥也没干,如果我们后续为istio配置一些virtualservice、destinationrule之类的配置项的话,这里的规则就可以有更多变化,比如说可以配置重试、熔断、URI复杂匹配路由等特性,这是理解istio的关键点,后续我会继续写博客讲解virtualservice之类的概念理解。
最后我们当然要看一眼cluster到底长什么样(istioctl proxy-config cluster my-ubuntu-f464f6885-b9t8l -o json|less):
1 2 3 4 5 6 7 8 9 10 |
"name": "outbound|80||my-nginx.default.svc.cluster.local", "type": "EDS", "edsClusterConfig": { "edsConfig": { "ads": {}, "initialFetchTimeout": "0s", "resourceApiVersion": "V3" }, "serviceName": "outbound|80||my-nginx.default.svc.cluster.local" }, |
可见,outbound|80||my-nginx.default.svc.cluster.local这个cluster负载均衡集群是基于EDS(endpoints discovery service)从istiod pilot服务发现的,只要istio-proxy(实际是envoy)按照EDS协议标准与istiod通讯拉取”serviceName”: “outbound|80||my-nginx.default.svc.cluster.local”对应的POD列表即可。
到这里本篇博客要表达的东西就基本结束了,为了完整性现在删除掉nginx service,然后从客户端再次调用nginx的service ip,请你想一下会发生什么?
分析:istio-proxy中压根不会下发到10.104.15.140:80的listener规则,因为这个service地址压根就不存在也不认识,此时istio-proxy的行为一定是透传连接,也就是会代理建立到10.104.15.140:80的TCP连接然后透明转发流量,但是我们会意识到10.104.15.140这个service ip压根就不存在,所以这次连接肯定是失败的。
1 |
curl 10.104.15.140 |
然后发现请求超时,查看客户端sidecar日志:
[2021-06-07T09:08:38.243Z] “GET / HTTP/1.1” 503 UF upstream_reset_before_response_started{connection_failure} – “-” 0 91 10003 – “-” “curl/7.58.0” “84a00cc9-fa37-9bb8-9468-56a281e6150c” “10.101.128.51” “10.101.128.51:80” PassthroughCluster – 10.101.128.51:80 172.17.0.15:38558 – allow_any
你会发现istio-proxy日志中报错upstream连接失败。
我们看得出这个流量走了PassthroughCluster规则,意思就是直接透传到cluster,而这个场景下的cluster含义并不是一个预先定义的负载均衡集群,而仅仅是指原始目的地址。
查看一下这个cluster的定义(istioctl proxy-config listener my-ubuntu-f464f6885-b9t8l -o json|less):
1 2 3 4 5 6 7 8 |
{ "name": "PassthroughCluster", "type": "ORIGINAL_DST", "connectTimeout": "10s", "lbPolicy": "CLUSTER_PROVIDED", "circuitBreakers": { "thresholds": [ { |
可以看到这个cluster类型是original_dst,其作用就是向连接的原始目标地址转发的意思,istio-proxy从程序角度将这种行为也抽象为cluster概念而已。
总结
istio-proxy是数据面组件,其实就是envoy,其转发规则依赖于istiod的下发,istiod则监听k8s service和endpoints的资源变化来实现服务发现,并转化成envoy格式的转发规则下发到istio-proxy。
在POD内,为了实现流量管控则是由istio的init-container通过向POD内下发iptables规则实现inbound/outbound流量拦截至istio-proxy。
istio-proxy根据拦截到的连接的原始目的地址,根据转发规则进行最优匹配,然后经过可选的协议识别努力,之后根据服务发现配置将连接转发给集群中的某个POD,这就是完整的流程。
如果我们形而上一点来理解的istio的话,我们可以说istio-proxy是从4层接入与划分流量,尝试上推到7层进行HTTP协议路由,最终定位到upstream后再下降到4层转发走流量。
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~

Pingback引用通告: istio(二)探索”虚拟服务”概念 | 鱼儿的博客
强如鱼儿
1