在切换Istio过程中,鉴于Istio强大的路由功能,希望对服务架构中的路由服务进行改造(替换),使用Istio中的相关路由功能来代替传统路由服务,故进行如下探索......
原服务架构如下图:
服务请求步骤:
(1)客户端通过网关请求服务;
(2)网关对用户进行鉴权(不通过则返回错误,通过则下一步);
(3)网关获取服务地址;
(4)网关转发请求到具体业务服务;
服务模型:
通过服务type+version来唯一确定服务地址:type+version -> serviceUrl/contextPath;
type:数字规定的服务类型,代表某一具体服务;
version:数字规定的服务版本,用于区分具体服务的不同版本;
contextPath:应用的上下文路径(例如tomcat中项目的contextPath:http://localhost:8080/myWebApp);
也就是说客户端通过类似http://gatewayUrl/type/version/svcPath的地址来通过网关请求具体的服务,
而网关通过type+version来调用路由服务获取服务的具体地址:servcieUrl/contextPath,并转发该请求到:http://serviceUrl/contextPath/svcPath(svcPath:具体的业务Url,由客户端指定);
路由服务就是用来专门维护服务路由信息的,服务路由信息格式:type+version -> serviceUrl/contextPath;
接下来的问题就是如何在Istio中实现type+version -> serviceUrl/contextPath的映射;
初步提出路由服务替换方案如下(假定服务为svc1,type=128, verison=1,contextPath=/base):
定义一个VirtualService,由该VirtualService统一维护所有路由信息,然后网关统一请求该VirtualService;
例如:设定该VirtualService为route.default,网关统一请求:route.default/{type}/{version}/svcPath,
而在VirtualService route中做如下定义:
......
# match(uri.prefix==/type/version) -> rewrite(uri=/contextPath)-> route.destination(host+subset)
# match(uri.prefix==/128/1) -> rewrite(uri=base) -> svc1.v1
- match:
- uri:
prefix: /128/1
rewire:
uri: /base
route:
- destination:
host: svc1.default
subset: v1
# match(uri.prefix==/128/2) -> rewrite(uri=base) -> svc1.v2
- match:
- uri:
prefix: /128/2
rewire:
uri: /base
rewire:
uri: /base
route:
- destination:
host: svc1.default
subset: v2
# match(uri.prefix==/129/1) -> rewrite(uri=base) -> svc2.v1
- match:
- uri:
prefix: /129/1
rewire:
uri: /base
route:
- destination:
host: svc2.default
subset: v1
......
通过上述VirtualServie配置,完成如下服务路由功能:
route.default/128/1/svcPath -> svc1.default/base/svcPath
优点:不用写代码(可替换原路由服务),以后route规则都在该virutalService中维护(支持各自服务维护自己的VS+DR);
缺点:统一维护配置繁琐,维护量大,新服务发布时需要手动添加映射规则;
保留原路由服务,在K8s集群中添加K8s Api监听服务,动态监听Deployment(自定义labels app, type, version, contextPath)资源的变化并将路由信息注册到路由服务中;
例如:在default命名空间定义K8s Deployment.svc1,并且自定义labels:app=svc1(需保证app值与对应k8s.Service同名), type=128,version=1,contextPath=/base,当K8s Api监听服务监听到该Deployment.svc1新创建(或发生变化时)则去动态维护原路由服务中的路由信息,格式如下:
type | version | serviceUrl |
---|---|---|
128 | 1 | svc1.default/base |
同时再定义K8s Deployment.svc1-v2,并且自定义labels:app=svc1,type=128,version=2,contextPath=/base当K8s Api监听服务监听到该Deployment.svc1-v2新创建(或发生变化时)则同样去添加svc1-v2的路由信息,格式如下(第二行):
type | version | serviceUrl |
---|---|---|
128 | 1 | svc1.default/base |
128 | 2 | svc1.default/base |
优点:动态注册服务路由信息,无需手动维护;
缺点:原路由服务依然存在,与Istio路由功能重合,并没有达到真正改造的目的,且还需再维护K8s Api监听服务;
客户端替换type+version为svcName.namespace/contextPath,
直接通过http://gatewayUrl/svcName.namespace/contextPath/svcPath请求网关,
而网关直接转发http://svcName.namespace/contextPath/svcPath到具体服务;
进一步优化:
替换type+version为svcName,namespace由网关指定(根据具体环境进行配置),
version统一在istio中维护,前端、网关无需知道版本号,
若具体服务鉴权策略不同,可在服务名svcName上添加固定后缀,例如:xx_1, xx_2来区分鉴权策略(命名空间等);
优点:客户端直接通过svcName/contextPath进行服务调用,网关无需进行type->service的转换;
缺点:客户端改动较大,且有的客户端写死在硬件中无法进行修改(硬伤);
在方案3中客户端无法替换type+version,那就使K8s Service.name=type,客户端无需改动而在K8s中对Service.name进行替换,
例如:服务svc1,type=128, verison=1,contextPath=/base,原来在k8s中服务名称为svc1,现在直接定义服务名称为s128;
注:实际测试域名不能数字开头(字母、'-'、数字,且字母开头和结尾),故在服务type前面加上固定字母前缀s;
客户端服务请求地址不变:http://gatewayUrl/128/1/svcPath(http://gatewayUrl/type/version/svcPath),
网关在收到请求后直接进行服务转发:http://s128.default/1/svcPath(http://s{type}.{namespace}/{version}/svcPath),
namespace由网关指定(根据具体环境进行配置),
具体的路由规则由每个服务的VirtualService+DestinationRule进行维护(与方案1配置类似);
完整配置如下:
# Deployment svc1 配置
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: svc1
spec:
replicas: 1
selector:
matchLabels:
app: svc1
version: v1
template:
metadata:
labels:
app: svc1
version: v1
spec:
containers:
- name: app
image: xxx/svc:1
---
# Service s128 配置
# Service.name=s128(s+type)
apiVersion: v1
kind: Service
metadata:
name: s128
labels:
app: svc1
spec:
ports:
- port: 80
targetPort: 8080
name: http
selector:
app: svc1
---
# VirtualService svc1 配置
# match ( uri.prefix==/1 ) -> rewrite ( /1->/base ) -> route s128.default -> subset v1
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: svc1
spec:
hosts:
- s128.default.svc.cluster.local
gateways:
- mesh
http:
- match:
- uri:
prefix: /1
rewrite:
uri: /base
route:
- destination:
host: s128.default.svc.cluster.local
subset: v1
---
# DestinationRule svc1 配置
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: svc1
spec:
host: s128.default.svc.cluster.local
subsets:
- name: v1
labels:
version: v1
---
上述路由配置将:http://s128.default/1/svcPath(http://s{type}.{namespace}/{version}/svcPath)
转换为http://s128.default/base/svcPath(http://s{type}.{namespace}/{contextPath}/svcPath);
且在上述VritualService+DestinationRule中可以灵活指定路由规则;
优点:客户端无需改动,可直接通过Istio路由替换原路由服务,直接定义K8s服务名为type则网关无需进行type->service的转换;
缺点:K8s服务名需要改造,每个服务需要单独维护Istio路由规则;
综上,考虑采用方案4,方案4将每个服务的路由单独配置维护(相比于方案1),避免集中管理(过于庞大难于维护),可以完全替代原路由服务(相比于方案2)且不必添加K8s Api监听服务(不用编写程序和维护代码),而且不用修改原客户端调用方式(相比于方案3),仅通过Istio相关路由配置即可实现灵活的路由控制(可以满足当前系统的路由控制需求);
关于当前系统的路由控制,围绕服务type+version来展开(type+version -> http://svcUrl/contextPath),而关于version则存在多种含义,总结如下:
(1)迭代版本(正常的迭代开发,服务地址不变)
1+1 -> http://svcUrl-1/svc1
(2)金丝雀版本(服务地址相同,但却路由到2个不同版本,对客户端透明)
2+1 -> http://svcUrl-2/svc2
2+2 -> http://svcUrl-2/svc2
(3)同一服务的不同controller
3+1 -> http://svcUrl-3/svc3/ctrl1
3+2 -> http://svcUrl-3/svc3/ctrl2
(4)完全不同的服务
4+1 -> http://svcUrl-4-1/svc4-1
4+2 -> http://svcUrl-4-2/svc4-2
采用方案4,则对以上4种version定义均可支持,示例配置如下:
(1)迭代版本
无需特殊配置,仅去修改每次部署的Docker 镜像版本即可(用于日常迭代开发);
(2)金丝雀版本
# Deployment svc2-1 配置
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: svc2-1
spec:
replicas: 1
selector:
matchLabels:
app: svc2
version: v1
template:
metadata:
labels:
app: svc2
version: v1
spec:
containers:
- name: app
image: xxx/svc2:1
---# Deployment svc2-1 配置
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: svc2-2
spec:
replicas: 1
selector:
matchLabels:
app: svc2
version: v2
template:
metadata:
labels:
app: svc2
version: v2
spec:
containers:
- name: app
image: xxx/svc2:2
---
# Service s129 配置
# Service.name=s129(s+type)
apiVersion: v1
kind: Service
metadata:
name: s129
labels:
app: svc2
spec:
ports:
- port: 80
targetPort: 8080
name: http
selector:
app: svc2
---
# VirtualService svc2 配置
# match ( uri.prefix==/1 ) -> rewrite ( /1->/base ) -> route s129.default -> subset v1
# match ( uri.prefix==/2 ) -> rewrite ( /2->/base ) -> route s129.default -> subset v2
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: svc2
spec:
hosts:
- s129.default.svc.cluster.local
gateways:
- mesh
http:
- match:
- uri:
prefix: /1
rewrite:
uri: /base
route:
- destination:
host: s129.default.svc.cluster.local
subset: v1
- match:
- uri:
prefix: /2
rewrite:
uri: /base
route:
- destination:
host: s129.default.svc.cluster.local
subset: v2
---
# DestinationRule svc2 配置
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: svc2
spec:
host: s129.default.svc.cluster.local
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
---
(3)同一服务的不同controller
# Deployment svc3 配置
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: svc3
spec:
replicas: 1
selector:
matchLabels:
app: svc3
version: v1
template:
metadata:
labels:
app: svc3
version: v1
spec:
containers:
- name: app
image: xxx/svc3:1
---# Service s130配置
# Service.name=s130(s+type)
apiVersion: v1
kind: Service
metadata:
name: s130
labels:
app: svc3
spec:
ports:
- port: 80
targetPort: 8080
name: http
selector:
app: svc3
---
# VirtualService svc2 配置
# match ( uri.prefix==/1 ) -> rewrite ( /1->/base/ctrl1 ) -> route s130.default -> subset v1
# match ( uri.prefix==/2 ) -> rewrite ( /2->/base/ctrl2 ) -> route s130.default -> subset v1
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: svc3
spec:
hosts:
- s130.default.svc.cluster.local
gateways:
- mesh
http:
- match:
- uri:
prefix: /1
rewrite:
uri: /base/ctrl1
route:
- destination:
host: s130.default.svc.cluster.local
subset: v1
- match:
- uri:
prefix: /2
rewrite:
uri: /base/ctrl2
route:
- destination:
host: s130.default.svc.cluster.local
subset: v1
---
# DestinationRule svc3 配置
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: svc3
spec:
host: s130.default.svc.cluster.local
subsets:
- name: v1
labels:
version: v1
---
(4)完全不同的服务
# Deployment svc4-1 配置
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: svc4-1
spec:
replicas: 1
selector:
matchLabels:
app: svc4-1
version: v1
template:
metadata:
labels:
app: svc4-1
version: v1
spec:
containers:
- name: app
image: xxx/svc4-1:1
---# Deployment svc4-2 配置
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: svc4-2
spec:
replicas: 1
selector:
matchLabels:
app: svc4-2
version: v1
template:
metadata:
labels:
app: svc4-2
version: v1
spec:
containers:
- name: app
image: xxx/svc4-2:1
---# Service s131配置
# Service.name=s131(s+type)
apiVersion: v1
kind: Service
metadata:
name: s131
labels:
app: svc4-1
spec:
ports:
- port: 80
targetPort: 8080
name: http
selector:
app: svc4-1
---# Service s132配置
# Service.name=s132(s+type)
apiVersion: v1
kind: Service
metadata:
name: s132
labels:
app: svc4-2
spec:
ports:
- port: 80
targetPort: 8080
name: http
selector:
app: svc4-2
---
# VirtualService svc4 配置
# match ( uri.prefix==/1 ) -> rewrite ( /1->/base1 ) -> route s131.default -> subset v1
# match ( uri.prefix==/2 ) -> rewrite ( /2->/base2 ) -> route s132.default -> subset v1
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: svc4
spec:
hosts:
- s131.default.svc.cluster.local
gateways:
- mesh
http:
- match:
- uri:
prefix: /1
rewrite:
uri: /base1
route:
- destination:
host: s131.default.svc.cluster.local
subset: v1
- match:
- uri:
prefix: /2
rewrite:
uri: /base2
route:
- destination:
host: s132.default.svc.cluster.local
subset: v1
---
# DestinationRule svc4-1 配置
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: svc4-1
spec:
host: s131.default.svc.cluster.local
subsets:
- name: v1
labels:
version: v1
---# DestinationRule svc4-2 配置
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: svc4-2
spec:
host: s132.default.svc.cluster.local
subsets:
- name: v1
labels:
version: v1
---
(5)金丝雀版本 - 补充(在原基础上添加基于header[user-name]=lhq路由)
# Deployment svc2-1 配置 - 同示例(2)
# Deployment svc2-1 配置 - 同示例(2)
# Service s129 配置 - 同示例(2)
---
# VirtualService svc2 配置
# match ( uri.prefix==/1 and header[user-name]=luohq) -> rewrite ( /1->/base ) -> route s129.default -> subset v1
# match ( uri.prefix==/2 ) -> rewrite ( /2->/base ) -> route s129.default -> subset v2
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: svc2
spec:
hosts:
- s129.default.svc.cluster.local
gateways:
- mesh
http:
- match: #匹配prefix==/1并且header[user-name]==lhq则路由到v1
- uri:
prefix: /1
header:
user-name:
exact: luohq
rewrite:
uri: /base
route:
- destination:
host: s129.default.svc.cluster.local
subset: v1
- match:
- uri:
prefix: /1
rewrite:
uri: /base
route:
- destination:
host: s129.default.svc.cluster.local
subset: v1
- match:
- uri:
prefix: /2
rewrite:
uri: /base
route:
- destination:
host: s129.default.svc.cluster.local
subset: v2
---
# DestinationRule svc2 配置
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: svc2
spec:
host: s129.default.svc.cluster.local
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
---
注:在Istio中关于match中header的定义需要注意,header名称必须全部包括且仅包括[小写字母,'-' ],实际测试过程中user(user中没有中横线)不生效,改为user-name生效(若请求header为USER-NAME也是生效的);
官方header说明:
以上,基于Istio的服务版本路由改造方案基本完成。