istio(二)探索”虚拟服务”概念
接前文《istio(一)探索服务发现行为》,上一次我们探索了istio的默认服务发现能力。
本文我们探索”虚拟服务“概念,利用它可以自定义请求路由规则,结合istio的服务发现功能可以解决更加实际的问题。
准备工作
与上篇博客一样,我们需要创建出包含istio-proxy的客户端与服务端环境。
创建服务端:
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 |
cat nginx.yaml 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 |
创建客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
cat ubuntu.yaml 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"] |
探索
https://nginx.com/是nginx的官网,我们接下来的实验目的就是利用istio来识别访问它的流量,做一些奇奇怪怪的事情,以便理解istio的”虚拟服务“概念。
为什么选择nginx.com呢?因为这个域名是真实存在的,因此能解析到IP,也就能走通后续4层网络通讯的事情,如果胡编一个域名还得去配置coredns解析IP比较麻烦,这个原因简单了解即可。
场景1)识别HTTP协议&转发
我们希望访问http://nginx.com(注意不是HTTPS)的时候,可以利用istio将流量转发给我们自己部署的nginx而不是真的nginx官网,这个很有趣吧?
我们要做的是:识别HTTP流量,匹配HOST是nginx.com,然后转发给my-service这个k8s service对应的POD。
我们创建一个叫做”虚拟服务(virtualservice)“的配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
cat vs.yaml apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: vs-nginx namespace: default spec: hosts: - nginx.com http: - route: - destination: host: my-nginx |
当istio-proxy拦截到程序发出的请求时,如果识别出流量是http协议的话就会提取出host,并且看一下是否是nginx.com。
如果host是nginx.com的话,它就会转发给一个目标集群(destination/upstream),这个集群的名字叫做my-nginx,实际就是my-nginx service的域名简写(istio会自动补全),此时istio-proxy的行为就是从my-nginx service的endpoints中找一个POD转发过去。
登录ubuntu容器,发起调用:
root@debian:~/kubernetes/demo# kubectl exec -it my-ubuntu-f464f6885-b9t8l bash
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 |
root@my-ubuntu-f464f6885-b9t8l:/# curl http://nginx.com/ <!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title> <style> body { width: 35em; margin: 0 auto; font-family: Tahoma, Verdana, Arial, sans-serif; } </style> </head> <body> <h1>Welcome to nginx!</h1> <p>If you see this page, the nginx web server is successfully installed and working. Further configuration is required.</p> <p>For online documentation and support please refer to <a href="http://nginx.org/">nginx.org</a>.<br/> Commercial support is available at <a href="http://nginx.com/">nginx.com</a>.</p> <p><em>Thank you for using nginx.</em></p> </body> </html> |
注意我们访问的是HTTP而不是HTTPS。
从返回内容看,我们访问的的确是my-nginx服务,而不是俄罗斯的nginx官网。之所以能实现这个效果,都是因为istio能够识别HTTP流量,并且应用了我们配置的virtualservice虚拟服务规则,才能实现host匹配与转发到service的效果。
我们看一下ubuntu的istio-proxy日志:
[2021-06-09T09:28:48.299Z] “GET / HTTP/1.1” 200 – via_upstream – “-” 0 612 13 12 “-” “curl/7.58.0” “97d36820-b512-9482-8f75-3256f48e429d” “nginx.com” “172.17.0.13:80” outbound|80||my-nginx.default.svc.cluster.local 172.17.0.15:36732 52.58.199.22:80 172.17.0.15:42982 – –
可见istio-proxy识别出了HTTP协议与nginx.com,经过路由规则后送往了outbound|80||my-nginx.default.svc.cluster.local集群。
我们先看envoy listener的配置方法(istioctl proxy-config listener 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 26 27 28 29 30 31 |
"name": "0.0.0.0_80", "address": { "socketAddress": { "address": "0.0.0.0", "portValue": 80 } }, "filterChains": [ { "filterChainMatch": { "transportProtocol": "raw_buffer", "applicationProtocols": [ "http/1.0", "http/1.1", "h2c" ] }, "filters": [ { "name": "envoy.filters.network.http_connection_manager", "typedConfig": { "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager", "statPrefix": "outbound_0.0.0.0_80", "rds": { "configSource": { "ads": {}, "initialFetchTimeout": "0s", "resourceApiVersion": "V3" }, "routeConfigName": "80" }, |
前一篇博客也说过,istio-proxy是从4层listener开始的,上面配置说明只要访问80端口的并且能识别出HTTP协议的,咱就走HTTP路由进一步判断,所以这个阶段只能是IP/PORT/Protocol上面做工作。
既然已经识别出是80端口的HTTP流量,那么就可以向上到7层做进一步路由了(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 |
{ "name": "nginx.com:80", "domains": [ "nginx.com", "nginx.com:80" ], "routes": [ { "match": { "prefix": "/" }, "route": { "cluster": "outbound|80||my-nginx.default.svc.cluster.local", "timeout": "0s", |
可见路由规则已经把virtualservice配置给下发了,匹配nginx.com或者nginx.com:80的HOST,然后转给outbound|80||my-nginx.default.svc.cluster.local集群,这时候就是再下回到4层。
outbound|80||my-nginx.default.svc.cluster.local集群则进一步查看集群信息(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" }, |
发现是EDS(endpoint discovery service)来动态服务发现的endpoints列表,其实就是my-service这个k8s service对应的endpoints,这是istiod来负责下发到istio-proxy的。
场景2)识别 TLS&转发
我们现在直接调用https协议的nginx.com域名,这是一个TLS加密的通讯,因此当istio-proxy收到这个流量时无法识别出上层协议是HTTP,自然也无法提取出Host,我们又该如何配置virtualhost呢?
虽说如此,在我们不进行任何配置的情况下直接调用https://nginx.com的话,nginx.com解析为俄罗斯官网IP,然后istio-proxy并不了解这个IP因此会走TCP透传转发,访问也是没任何问题的:
root@my-ubuntu-f464f6885-b9t8l:/# curl https://nginx.com/
[2021-06-09T10:09:28.237Z] “- – -” 0 – – – “-” 743 3341 1524 – “-” “-” “-” “-” “3.125.197.172:443” PassthroughCluster 172.17.0.15:57150 3.125.197.172:443 172.17.0.15:57148 – –
看得出istio-proxy走了PassThroughCluter透传,因为它压根没有目标IP的信息,也无法提取TLS中的协议内容。
但实际上HTTPS请求是要经过TLS握手的,而TLS协议在握手期间HTTPS客户端会在TLS握手包中携带SNI扩展字段,在里面写上要访问的HTTP HOST,这个就给了istio-proxy识别流量目标地址的机会。
下面是TLS握手流程:
在TLS client hello握手包中的SNI字段可以被istio-proxy解析到,客户端在里面写上了HTTP协议层的host,因此istio-proxy就可以基于这个host进行路由和转发目的地控制了。
我们抓包HTTPS调用过程,可以wireshark看到TLS中的SNI字段:
也就说istio-proxy收到TLS握手时就已经知道这个TCP连接要访问的目标地址是nginx.com了,虽然它并不知道应用层协议是HTTP。
我们可以注册一个virtualservice来让istio-proxy尝试做这个识别:
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 |
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: vs-nginx namespace: default spec: hosts: - nginx.com http: - route: - destination: host: my-nginx.default tls: - match: - port: 443 sniHosts: - nginx.com route: - destination: host: nginx.com --- apiVersion: networking.istio.io/v1alpha3 kind: ServiceEntry metadata: name: nginx-svcentry spec: hosts: - nginx.com location: MESH_EXTERNAL ports: - number: 443 protocol: TLS name: https resolution: DNS |
我们在virtualservice中增加了tls加密场景下的路由,我们match的是发往443端口且SNI host是nginx.com的流量,将其转发给nginx.com。
这里容易让人迷惑的是destination怎么又是nginx.com,感觉死循环了一样?其实destination通常都是写K8S中存在的service域名,这样istio-proxy会服务发现后面的endpoint来转发,这也是destination的本意。
但是我们现在希望将这个https://nginx.com的流量识别出来,然后再原样转发给俄罗斯的nginx.com就行,因此此时nginx.com就不是一个K8S service的域名了,我们需要主动通过ServiceEntry来注册这个服务到istio,这意味着nginx.com是一个外部服务,被注册到了istio注册中心,可以作为一个destination(也就是envoy的cluster概念)。
因此,当istio-proxy转发到destination nginx.com时就会基于DNS解析nginx.com域名得到俄罗斯服务器 IP,TCP连接它,然后将TLS流量透明转发就好。
请求网址的确返回了俄罗斯网站的HTML:
curl https://nginx.com/
<html>
<head><title>301 Moved Permanently</title></head>
<body>
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx/1.19.10</center>
</body>
</html>
可以看到ubuntu中istio-proxy的日志:
[2021-06-09T10:35:29.246Z] “- – -” 0 – – – “-” 743 3341 1536 – “-” “-” “-” “-” “3.125.197.172:443” outbound|443||nginx.com 172.17.0.15:46280 52.58.199.22:443 172.17.0.15:37424 nginx.com –
我们分析istio-proxy规则,先看listener(istioctl proxy-config listener 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 |
{ "name": "0.0.0.0_443", "address": { "socketAddress": { "address": "0.0.0.0", "portValue": 443 } }, "filterChains": [ { "filterChainMatch": { "serverNames": [ "nginx.com" ] }, ... { "name": "envoy.filters.network.tcp_proxy", "typedConfig": { "@type": "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy", "statPrefix": "outbound|443||nginx.com", "cluster": "outbound|443||nginx.com", |
该调用命中了443端的listener,然后SNI匹配了nginx.com域名,然后走TCP PROXY透明流量转发给集群outbound|443||nginx.com,我们看一下这个集群:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
{ "name": "outbound|443||nginx.com", "type": "STRICT_DNS", "connectTimeout": "10s", "loadAssignment": { "clusterName": "outbound|443||nginx.com", "endpoints": [ { "locality": {}, "lbEndpoints": [ { "endpoint": { "address": { "socketAddress": { "address": "nginx.com", "portValue": 443 } } }, |
能看出这个集群是STATIC DNS静态解析出的集群,也就是走域名解析转发啊给soscketAddress中的nginx.com:443地址,流量则是TCP PROXY。
从这里我们也可以看出来,因为istio-proxy无法解密TLS流量内容,因此只能做透明转发,也就只有一次通讯机会,没法实现对upstream的重试等特性,这个很容易理解。
至于istio-proxy作为中间人为什么不能解密TLS流量呢?因为大家可以回看上面TLS握手图的3~4步骤,secret是用服务端公钥加密的,只能由服务端用私钥解密,而最终TLS协商的对称通讯秘钥则需要依赖secret来生成,因此istio-proxy作为中间人不可能拥有服务端的私钥,因此不可能知道secret,因此不可能推导出对称秘钥,也不可能解密TLS流量,也自然不可能解析出7层的HTTP协议细节,无从重试。
总结
本文通过一个奇怪的例子探索了virtualservice虚拟服务,也探索了HTTP流量、HTTPS流量的处理方法,也明白了TLS请求的能力限制底层原因,同时也学会了如何向istio注册一个集群外的服务发现。
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~

看不懂,🐟大佬b站直播一下啊
1