istio
迈入 1.6
之后趋于稳定,大家都慢慢的开始尝试起来,摆在大家面前会有这么一个问题,原来的旧系统如何兼容 istio
?
¶How Spring Cloud Works
Spring Cloud
体系是如何工作的呢?
Spring Cloud
体系中,每一个服务实例会将自己的 IP
地址上报至 Service Registry
中,这一步是通过 Registry Client
实现,当其需要访问其他的实例的时候,也会去 Service Registry
中获取其他实例的 IP
地址
Registry Client
和 Service Registry
都是抽象的构成,在实现的过程中
Service Registry
常见为Eureka
Consul
ZooKeepr
Registry Client
则对应这三者(eurke-client
/consul-client
/zookeerer-client
),这三者会有一个共同的抽象:DiscoveryClient
¶通用概念
实例
: 每一个Registry Client
所代表的就是一个服务实例,[ PS:通常一个Java程序就是一个实例]服务
: 多个实例组合成一个服务,服务是Spring Cloud
负载均衡的对象
对于 Eureka
所包含的 Zone
概念在其他的 Client
中并没有相对应的,因此对于 Spring Cloud
通用的抽象是比较的简单的。
¶基于 Eureka 的流程分析
¶注册流程
不同的注册中心的区别主要在注册中心自身逻辑上[AP/CP],对于其他部分的影响不大
实例所对应的服务是由 元数据 提供,IP 地址是由系统检测生成 (但是可以手动覆盖
)
¶调用过程
而调用过程,Biz
一般会通过 RestfulTemaplate
/Feign
来访问远程地址,通过一些中间件的集成。将 服务名
翻译成 IP
地址
1 | this.restTemplate.exchange( |
此例中 bookmark-service
-> 192.168.xxx.yyy
在 k8s
中,此 IP
约等同于 POD IP
¶How Istio Works
那么相对应的 istio
体系又是如何工作的呢?
对于 istio
来说,poliot
将监控所有的关心的数据,在客户端进行访问远程地址的时候,通过的是 ClusterIP/DNS
进行访问,istio
通过 tcp
协议中的 dest ip
或者是 http
协议中的 host
进行判断需要访问某个具体的服务,在 Envoy
进行负载均衡。
在 Isito の Pilot 组件 有深入的解读,就不作展开。
如果我们直接访问某个不受 istio
托管的服务地址 (比如 pod ip
/未知的service ip
) ,istio 会认为这次请求是 PassthroughCluster
无法利用任何 istio
的特性。
¶那么问题呢?
我们希望我们的系统能够跑在 istio
享受到 istio
所带来的诸多特性,而非是通过 sidecar
绕了一圈而什么都没有做。
在 Spring Cloud
体系中,Biz
通过 IP
访问其他服务,istio
并不能区分 192.168.2.3
是哪个服务的地址,会选择直接转发,就谈不上任何管控维度。
¶WHY [TLDR]
这就要了解到 Istio
的工作原理,istio
将配置转为 Envoy
的配置
xds | 备注 | |
---|---|---|
Listener | LDS | 监听一个端口,可以配置多条,监听多个端口 |
Routes | RDS | 一个 cluster 是具有完全相同行为的多个 endpoint 它们组成一个Cluster,从 cluster 到 endpoint 的过程称为负载均衡 |
Clusters | CDS | 有时候多个 cluster 具有类似的功能,但是是不同的版本号, 可以通过 route 规则,选择将请求路由到某一个版本号 |
Endpoints | EDS | 目标的 ip 地址和端口,这个是 proxy 最终将请求转发到的地方 |
举个例子
比如远端有一个 helloworld.sample.svc.cluster.local
,端口 5000
提供 HTTP
服务
- Envoy 的 Listener 配置中会监听 0.0.0.0:5000 的数据
1 | $ istioctl proxy-config listeners sleep-854565cb79-9t29h.sample --context cluster2 |
- Envoy 的 Route 配置从上面就知道指向名为
5000
的Route
配置
1 | $ istioctl proxy-config routes sleep-854565cb79-9t29h.sample --context cluster2 |
根据 route
规则,我们知道这个服务的出口 cluster
是 outbound|5000||helloworld.sample.svc.cluster.local
1 | "route": { |
outbound|5000||helloworld.sample.svc.cluster.local
的Cluster
配置
1 | $ istioctl proxy-config clusters sleep-854565cb79-9t29h.sample --fqdn helloworld.sample.svc.cluster.local --context cluster2 |
我们可以发现,其实最终的目标地址也就是我们的 K8S
的 POD
地址,而整个拦截的链路最核心的是在 Route
的匹配定义中
1 | "domains": [ |
可见,istio
需要目标地址访问的是 dns
中的域名或者是 ClusterIP
才会进行拦截,
¶解决之道
显然我们有好几个办法来做这个兼容设计,大致上的思路可以分为
- 让
istio
匹配pod ip
- 让
spring cloud
访问cluster ip
¶Spring Cloud 访问 Cluster IP
¶By Eureka Server
从 eureka
解决这个问题,想办法让 Eureka Server
返回给客户端的是 Service Address
Fake Eureka Server
对于注册和心跳保持不处理直接返回200
Fake Eureka Server
对于Fetch
请求,返回服务名对应的ClusterIP
我们为每个服务服务创建一个 Cluster IP
对象即可。
¶By Eureka Client
Eureka Client
在返回给Ribbin
之前将IP
翻译成Srv IP
Ribbon: Spring Cloud 客户端负载均衡组件
¶Istio 匹配 Pod IP
¶Add Http Header
对于 istio
来说(Envoy
), Http filter
是 一个接着一个运行的。
Routing
的 HTTP Filter
优先级较低,如果我们能在 Routing
之前增加一个 Filter
然后将 IP
转化为 Host
然后加入 HttpHeaders
中,应该就可以满足需求。https://github.com/envoyproxy/envoy-filter-example/blob/master/http-filter-example/http_filter.cc
1 | use log::info; |
¶Custom Registry
虽然官网并没有写这部分,其实我们类似于 MCP
的实现,将 Eureka
的数据作为其中一个 Registery
实现一遍,不过这个工程过于浩大,不做展开。
¶Best Way
重构代码,将 Spring Cloud
的部分除去,保留 Spring Boot
部分即可,不仅仅减少复杂度而且系统会运行的更快。
方案 | 优势 | 劣势 | 研发复杂度 | 性能 |
---|---|---|---|---|
Fake Eureka | 架构简单,易于排查 | 用户可见,增加了非透明性 | 多版本的适配性 | 与原生SpringCloud相比没有任何性能的损耗 |
Isito WASM | 用户无感知 | 增大的系统复杂度 & 降低了可调试性 | 语言切换等 | 在转发逻辑中将无法利用原生的缓冲的机制,会带来一些转发性能的损耗(因是in Memory逻辑,应该是很低的值) |
公共 | 一切基于 IP规则的策略都会失效(熔断 & 负载均衡) |