Spring Cloud Run In Istio

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常见为EurekaConsulZooKeeprRegistry 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对于注册和心跳保持不处理直接返回200Fake 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规则的策略都会失效(熔断 & 负载均衡) |