istio(一)探索服务发现行为

本文通过一个简单场景,向大家展现istio的服务发现工作原理。

我使用的minikube,大家需自行安装好istio,观看安装指南

背景

我们将在K8S中创建一个简单的nginx服务,并简单探究istio sidecar是如何无侵入的进行流量管控的。

准备工作

开启自动注入

因为istio sidecar要运行到每个POD中进行流量管控,所以我们需要为所需的namespace开启POD自动注入istio sidecar的功能:

上面就为default命名空间打开了自动注入sidecar容器的功能。

创建nginx

创建包含2个replica的nginx部署,并且分配了一个service关联到这2个POD,开放了80端口的转发。

这个nginx作为测试用的服务端,POD内是有sidecar的,我们要探究流量进入POD时sidecar的行为。

创建ubuntu

这个ubuntu作为测试用的客户端,POD内同样有sidecar,我们探究流量离开POD时sidecar的行为。

检查点

此时,我们default namespace下面有3个POD:

每个POD里都跑着一个istio-proxy的sidecar,以某个nginx pod为例:

探索

场景1)验证服务端sidecar行为

为了单纯探究sidecar收到调用时的inbound方向行为,我们从宿主机上直接请求nginx pod:

然后查看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又如何应对这次访问:

再次请求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):

该ubuntu容器的IP是172.17.0.15,从里面直接调用nginx的pod ip看一下发生什么:

然后查看ubuntu中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:

[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 规则的样子:

关键的两行:

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),我们会看到如下内容:

很简单,这条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):

可见,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压根就不存在,所以这次连接肯定是失败的。

然后发现请求超时,查看客户端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):

可以看到这个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层转发走流量。

如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~