由于当前公司的绝大多数应用都还是使用eureka来做服务发现,算法团队又提出一些灰度的需求。从不重复造轮子以及技术趋势的角度,自然想到是否能够将Istio整合到平台产品中。但是,据我所知,eureka2.0已经被放弃了,Istio也在早期版本中,直接废弃了对Eureka做服务发现的支持。因此,这可能需要对现有的脚手架做一定改造,这个后面具体再论。另一封面,听说istio在架构上发生了很大的变化,于是下载了1.5版本来亲自体验了一下。
其实也不能够算是软件架构的大变化,更准确的说法应该是部署架构的变化。就是将之前控制面上多个微服务都变为模块的形势,统一放到istiod里面去运行。这样一个比较大的好处是,部署起来就方便多了。当然,提到部署的便捷性,istioctl这个工具还是相当不错的。基于 istioctl manifest apply
可以快速按照用户指定的profile拉起来一个istio环境。但是,除了istio自带的模块被打包到了istiod里面之外,如果你还启用了调用链、监控、遥测之类的功能的话,在istio-system中,依然还有一大堆pod。如果想要彻底变革,这些是不是也可以考虑整合一把,哪怕是放到一个pod里面去,比如说kiali其实是高度依赖于prometheus的数据的。
早年openstack上neutron还不够成熟的时候,经常出现VM网络不通,然后又各种折腾排查,动不动就需要在某个接口上tcpdump。说实在的,这样的日子如履薄冰,天天神经脆弱。我所理解的Istio中traffic的部分,本质是在容器层面的SDN;比起VM网络,envoy上大量的XDS规则,一旦有问题,可能比VM网络更难排查。所以,长久以来,对这货都不敢轻易引入(当年v1.0似乎也是在赶鸭子上架,很快速的又推出了新版本)。
好在如今的版本中,通过 istioctl pc
等工具,极大的降低了登录到各个数据面节点上排查路由规则的复杂度,同时, istioctl ps
也提供了控制面板到数据面配置一致性的debug手法。所以,现在采用istio,在功能层面,我是没啥顾虑的;那么,唯一可能比较担心的还是在于性能层面。
但是,我们完全可以将该技术先只使用到开发测试环境,等待其数据面代理性能得到一个整体提升后,再最终用到生产中。
在Istio提供的所有功能中,其实也没必要全都采纳,比如调用链APM这块,我就觉得比较二,对代码的入侵性太强,我更愿意在平台类产品中整合pinpoint或者skywalking之类专业的调用链。
另外,数据安全加密这块,对于绝大多数的应用场景,其实都是非必要的,关键它还对转发性能造成了较大影响。就像做管理决策,当我们排优先级的时候,哪些事情是能够为企业带来更大价值产出的,那么我认为这些事理应提高其优先级。
如前面所述,先在开发测试环境中,使用Istio的traffic控制相关的功能,同时,能够使用遥测数据来生成项目微服务全局的流量状况视图,应该算是比较稳妥的。
我这里提到consul,其实并非是想将consul与Istio的pilot-discovery对接,因为之前在研究这块的时候,感觉不过是一个炫头,大家一般都不这么用。
这里有一个插曲,大概是2018年的时候,有一次我在团队里面提出过:“到底istio的数据面是如何转发流量的?”
这个问题是源于istio默认结合k8s的服务发现来工作,我们都知道,K8S的服务发现,是通过watch service上的cluster IP变化来刷新dns的(这个设计很巧妙,为啥不是直接刷新pod IP,而要使用cluster IP呢?因为dns client一般都有缓存,而cluster IP到pod ip的路由是由K8S实时控制的,因此…)。
我们回来讲Istio,因为Istio是基于K8S来做服务发现的,如果服务A要访问B,不管A和B的sidecar会对流量做怎么的操作。毕竟A的程序看不到sidecar,它要转流量到B之前,总是会查询B的地址,但是在DNS查询返回的IP列表里面,目的IP是cluster IP。这时,报文被封装,并基于网络协议栈发送出去,再被A的sidecar拦截。此时A的sidecar会基于路由规则来修改A的真实转发地址。那么,在这个过程中,A发送出去报文的目的IP地址,到底有啥“卵用”?
从本质上来说,这货确实没啥用,因为都被替换了。但是,也不是绝对的,假设A、B之间发送的报文是基于HTTP协议。我们知道HTTP协议有一个header,header里面有一个叫做“host”的一级公民。其实envoy就是依据这个host来首先过滤流量的。比如说,A要发送请求个B服务,如果它使用的是 service-b
这个域名的话,虽然协议栈会解析出service-b的IP来做底层IP报文的destination IP。但是顶层的HTTP协议封装的时候,依然会使用“host:service-b”来封装header。另一方面,所有TCP报文送出pod的时候,都会被其sidecar拦截并基于转发逻辑转发报文;所以,都还没有等IP报文中的destination IP发挥作用,就被sidecar替换了。因此,这个只要我们保障HTTP的“host”字段匹配了envoy中的规则,目的IP不重要。
比如,A要发送报文给B,当我们配置好了Istio规则之后,A在封装报文的时候,只需要确保HTTP的header中包含“host:B”,而目的IP可以随便填写。
所以,结论就是:A怎么知道B的目的IP这个事情,真的不重要!A是通过consul,eureka还是基于K8S dns查询到B的地址的,都一样。唯一重要的是是否满足envoy的转发规则。
但是,HTTP的很多SDK库都会忽略掉我们在上层直接设置的host值,而采用所访问域名或者目的IP地址。这就是一个坑,所以,需要对代码脚手架做一些改造,目标就是强制设置该host值。
以下是一个调用链的例子,我写了一个程序,通过环境变量传入微服务名称和工作模式,其中 stub
模式为叶子节点, ingress
模式为流量入口(树根节点),需要指定其下一跳节点(各分支或叶子)。所有的服务起来之后,都会将自己注册到consul上面,consul会基于注册的心跳时间来对各个服务发起健康检查。当服务A要访问服务B的时候,会在consul上查询存活的B列表,并访问B服务。
调用链拓扑为:
a - > c - > b - > d
我们实际部署后,在kiali上看到的流量转发图是这样的:
除了业务的调用链之外,我们看到consul有往各个service做健康检查的流量。由于consul发往各个service的流量的host我们没法做修改,因此看到了匹配passthrough cluster的力量。这个是Istio上如果流量没法匹配对应的路由,就会发送到passthrough cluster,最终基于报文的目标IP来转发(当然也可以配置成黑洞模式,这样没法匹配istio中路由规则的流量就会全部被干掉)。
下面是k8s上,启动整个应用各微服务的yaml文件和istio config文件:
apiVersion: v1 items: - apiVersion: extensions/v1beta1 kind: Deployment metadata: labels: run: centos name: centos namespace: default spec: replicas: 1 selector: matchLabels: run: centos template: metadata: labels: run: centos spec: containers: - args: - sleep - "999999" image: centos:7 imagePullPolicy: IfNotPresent name: centos - apiVersion: extensions/v1beta1 kind: Deployment metadata: labels: app: consul version: v1 name: consul namespace: default spec: replicas: 1 selector: matchLabels: app: consul version: v1 template: metadata: labels: app: consul version: v1 spec: containers: - env: - name: CONSUL_BIND_INTERFACE value: eth0 image: consul imagePullPolicy: Always name: consul ports: - containerPort: 8500 protocol: TCP - apiVersion: extensions/v1beta1 kind: Deployment metadata: labels: app: service-a version: v1 name: service-a namespace: default spec: replicas: 1 selector: matchLabels: app: service-a version: v1 template: metadata: labels: app: service-a version: v1 spec: containers: - env: - name: CONSUL_ADDR value: consul - name: CONSUL_PORT value: "8500" - name: app value: service-a - name: mode value: ingress - name: next_services value: service-b,service-c image: ljchen/istio-demo imagePullPolicy: Always name: service-a ports: - containerPort: 9090 name: http protocol: TCP - apiVersion: extensions/v1beta1 kind: Deployment metadata: labels: app: service-b version: v1 name: service-b namespace: default spec: replicas: 1 selector: matchLabels: app: service-b version: v1 template: metadata: labels: app: service-b version: v1 spec: containers: - env: - name: CONSUL_ADDR value: consul - name: CONSUL_PORT value: "8500" - name: app value: service-b - name: next_services value: service-d image: ljchen/istio-demo imagePullPolicy: Always name: service-b ports: - containerPort: 9090 name: http protocol: TCP - apiVersion: extensions/v1beta1 kind: Deployment metadata: labels: app: service-c version: v1 name: service-c namespace: default spec: replicas: 1 selector: matchLabels: app: service-c version: v1 template: metadata: labels: app: service-c version: v1 spec: containers: - env: - name: CONSUL_ADDR value: consul - name: CONSUL_PORT value: "8500" - name: app value: service-c - name: mode value: stub image: ljchen/istio-demo imagePullPolicy: Always name: service-c ports: - containerPort: 9090 name: http protocol: TCP - apiVersion: extensions/v1beta1 kind: Deployment metadata: labels: app: service-d version: v1 name: service-d-v1 namespace: default spec: replicas: 1 selector: matchLabels: app: service-d version: v1 template: metadata: labels: app: service-d version: v1 spec: containers: - env: - name: CONSUL_ADDR value: consul - name: CONSUL_PORT value: "8500" - name: app value: service-d - name: mode value: stub - name: version value: v1 image: ljchen/istio-demo imagePullPolicy: Always name: service-d-v1 ports: - containerPort: 9090 name: http protocol: TCP - apiVersion: extensions/v1beta1 kind: Deployment metadata: labels: app: service-d version: v2 name: service-d-v2 namespace: default spec: progressDeadlineSeconds: 600 replicas: 1 selector: matchLabels: app: service-d version: v2 strategy: rollingUpdate: maxSurge: 25% maxUnavailable: 25% type: RollingUpdate template: metadata: labels: app: service-d version: v2 spec: containers: - env: - name: CONSUL_ADDR value: consul - name: CONSUL_PORT value: "8500" - name: app value: service-d - name: mode value: stub - name: version value: v2 image: ljchen/istio-demo imagePullPolicy: Always name: service-d-v2 ports: - containerPort: 9090 name: http protocol: TCP dnsPolicy: ClusterFirst restartPolicy: Always - apiVersion: v1 kind: Service metadata: labels: app: consul version: v1 name: consul namespace: default spec: externalTrafficPolicy: Cluster ports: - name: http nodePort: 31057 port: 8500 protocol: TCP targetPort: 8500 selector: app: consul version: v1 sessionAffinity: None type: NodePort - apiVersion: v1 kind: Service metadata: labels: component: apiserver provider: kubernetes name: kubernetes namespace: default spec: ports: - name: https port: 443 protocol: TCP targetPort: 6443 sessionAffinity: None type: ClusterIP - apiVersion: v1 kind: Service metadata: labels: app: service-a version: v1 name: service-a namespace: default spec: ports: - name: http port: 9090 protocol: TCP targetPort: 9090 selector: app: service-a sessionAffinity: None type: ClusterIP - apiVersion: v1 kind: Service metadata: labels: app: service-b version: v1 name: service-b namespace: default spec: ports: - name: http port: 9090 protocol: TCP targetPort: 9090 selector: app: service-b sessionAffinity: None type: ClusterIP - apiVersion: v1 kind: Service metadata: labels: app: service-c version: v1 name: service-c namespace: default spec: ports: - name: http port: 9090 protocol: TCP targetPort: 9090 selector: app: service-c sessionAffinity: None type: ClusterIP - apiVersion: v1 kind: Service metadata: labels: app: service-d version: v1 name: service-d namespace: default spec: ports: - name: http port: 9090 protocol: TCP targetPort: 9090 selector: app: service-d sessionAffinity: None type: ClusterIP kind: List metadata: resourceVersion: "" selfLink: ""
apiVersion: v1 items: - apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: consul namespace: default spec: hosts: - consul http: - route: - destination: host: consul subset: v1 - apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: service-a namespace: default spec: gateways: - gateway hosts: - ljchen.net http: - route: - destination: host: service-a subset: v1 - apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: service-b namespace: default spec: hosts: - service-b http: - route: - destination: host: service-b subset: v1 - apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: service-c namespace: default spec: hosts: - service-c http: - route: - destination: host: service-c subset: v1 - apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: service-d namespace: default spec: hosts: - service-d http: - route: - destination: host: service-d subset: v1 - destination: host: service-d subset: v2 weight: 100 - apiVersion: networking.istio.io/v1beta1 kind: Gateway metadata: name: gateway namespace: default spec: selector: istio: ingressgateway servers: - hosts: - ljchen.net port: name: http number: 80 protocol: HTTP - apiVersion: networking.istio.io/v1beta1 kind: DestinationRule metadata: name: consul namespace: default spec: host: consul subsets: - labels: version: v1 name: v1 - apiVersion: networking.istio.io/v1beta1 kind: DestinationRule metadata: name: service-a namespace: default spec: host: service-a subsets: - labels: version: v1 name: v1 - apiVersion: networking.istio.io/v1beta1 kind: DestinationRule metadata: name: service-b namespace: default spec: host: service-b subsets: - labels: version: v1 name: v1 - apiVersion: networking.istio.io/v1beta1 kind: DestinationRule metadata: name: service-c namespace: default spec: host: service-c subsets: - labels: version: v1 name: v1 - apiVersion: networking.istio.io/v1beta1 kind: DestinationRule metadata: name: service-d namespace: default spec: host: service-d subsets: - labels: version: v1 name: v1 - labels: version: v2 name: v2 kind: List metadata: resourceVersion: "" selfLink: ""