Kubernetes之Istio应用

一、基于istio安装灰度发布(金丝雀发布)

发布策略 核心目标 Istio 核心实现 应用场景
蓝绿发布 零停机、快速回滚 VirtualService 路由规则 版本切换、重大升级
A/B测试 根据规则分流用户 VirtualService + 请求头/URI匹配 功能试验、用户体验优化
镜像发布 生产流量复制测试 VirtualService 的 mirror 字段 新版本压力/稳定性测试
金丝雀/灰度 风险可控的渐进发布 VirtualService 的权重分流 新功能逐步上线
故障注入 主动测试系统韧性 VirtualService 的 fault 字段 混沌工程、健壮性测试

追求零风险和快速回滚 -> 蓝绿发布

需要根据用户属性测试功能 -> A/B测试

想用真实流量验证新版本性能 -> 镜像发布

需要可控、渐进式地推出新版本 -> 灰度发布

主动验证系统健壮性 -> 故障注入

1、二进制安装Istio

bash 复制代码
## 下载安装
wget https://github.com/istio/istio/releases/download/1.20.0/istio-1.20.0-linux-amd64.tar.gz

tar -xzf istio-1.20.0-linux-amd64.tar.gz
cd istio-1.20.0


# 1. 安装 istio-base (包含CRD)
helm install istio-base manifests/charts/base \
  -n istio-system \
  --create-namespace \
  --wait

# 2. 安装 istiod (控制平面)
helm install istiod manifests/charts/istio-control/istio-discovery \
  -n istio-system \
  --wait

# 3. 安装 istio-ingressgateway (入口网关)
helm install istio-ingressgateway manifests/charts/gateways/istio-ingress \
  -n istio-system \
  --set service.type=NodePort \
  --wait

2、 安装Isito查看

bash 复制代码
[root@mster isito]# helm list -n istio-system
NAME                    NAMESPACE       REVISION        UPDATED                                 STATUS          CHART                   APP VERSION
istio-base              istio-system    1               2026-02-04 01:12:00.470351762 -0800 PST deployed        base-1.20.0             1.20.0     
istio-ingressgateway    istio-system    1               2026-02-04 01:16:24.240675604 -0800 PST failed          istio-ingress-1.20.0    1.20.0     
istiod                  istio-system    1               2026-02-04 01:12:14.260723994 -0800 PST deployed        istiod-1.20.0           1.20.0    

[root@mster isito]# # 查看Pod运行状态(所有Pod应为Running)
[root@mster isito]# kubectl get pods -n istio-system -w
NAME                                    READY   STATUS    RESTARTS       AGE
istio-ingressgateway-54cb8b478b-5n946   1/1     Running   3 (114m ago)   5d23h
istiod-999d56b9f-7pt8j                  1/1     Running   3 (114m ago)   5d23h
^C[root@mster isito]# 查看安装的服务
[root@mster isito]# kubectl get svc -n istio-system
NAME                   TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)                                      AGE
istio-ingressgateway   LoadBalancer   10.104.4.238     <pending>     15021:32253/TCP,80:30881/TCP,443:30677/TCP   5d23h
istiod                 ClusterIP      10.110.103.147   <none>        15010/TCP,15012/TCP,443/TCP,15014/TCP        5d23h

3、创建流量管理配置

这是最关键的一步,你需要按顺序创建以下3个YAML文件,它们共同定义了完整的灰度规则。

bash 复制代码
cat  << EOF destinationrule.yaml   
#apiVersion: networking.istio.io/v1
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: demo-service
spec:
  host: demo-service.default.svc.cluster.local # K8s Service的完整域名
  subsets:
  - name: v1
    labels:
      version: v1 # 必须匹配v1 Pod的标签
  - name: v2
    labels:
      version: v2 # 必须匹配v2 Pod的标签
EOF
bash 复制代码
cat  << EOF gateway.yaml 
#apiVersion: networking.istio.io/v1
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
  name: demo-gateway
spec:
  selector:
    istio: ingressgateway # 使用已安装的入口网关
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "*" # 允许所有主机访问,生产环境请替换为你的域名,如 demo.example.com


EOF 
bash 复制代码
cat   << EOF virtualservice.yaml
#apiVersion: networking.istio.io/v1
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: demo-service
spec:
  hosts:
  - "*" # 匹配的域名,与Gateway中的hosts对应,生产环境请用具体域名
  gateways:
  - demo-gateway # 绑定上面创建的网关
  http:
  - route:
    - destination:
        host: demo-service.default.svc.cluster.local # 目标服务
        subset: v1 # 全部流量指向v1
      weight: 80
    - destination:
        host: demo-service.default.svc.cluster.local
        subset: v2 # 0%流量指向v2,初始状态
      weight: 20
   
EOF
bash 复制代码
# 按顺序应用配置
kubectl apply -f destinationrule.yaml
kubectl apply -f gateway.yaml
kubectl apply -f virtualservice.yaml

4、注意事项

bash 复制代码
执行kubectl apply -f destinationrule.yaml报以下错误:
error: resource mapping not found for name: "demo-service" namespace: "" from "destinationrule.yaml": no matches for kind "DestinationRule" in version "networking.istio.io/v1"
ensure CRDs are installed first
  • 查看以下模板是否安装
bash 复制代码
[root@mster isito]# kubectl get crd | grep istio
authorizationpolicies.security.istio.io               2026-02-04T09:11:58Z
destinationrules.networking.istio.io                  2026-02-04T09:11:58Z
envoyfilters.networking.istio.io                      2026-02-04T09:11:58Z
gateways.networking.istio.io                          2026-02-04T09:11:58Z
istiooperators.install.istio.io                       2026-02-04T09:11:58Z
peerauthentications.security.istio.io                 2026-02-04T09:11:58Z
proxyconfigs.networking.istio.io                      2026-02-04T09:11:58Z
requestauthentications.security.istio.io              2026-02-04T09:11:58Z
serviceentries.networking.istio.io                    2026-02-04T09:11:58Z
sidecars.networking.istio.io                          2026-02-04T09:11:58Z
telemetries.telemetry.istio.io                        2026-02-04T09:11:58Z
virtualservices.networking.istio.io                   2026-02-04T09:11:58Z
wasmplugins.extensions.istio.io                       2026-02-04T09:11:58Z
workloadentries.networking.istio.io                   2026-02-04T09:11:58Z
workloadgroups.networking.istio.io                    2026-02-04T09:11:58Z

[root@mster isito]# kubectl  get crd | grep -E 'destinationrule|virtualservice|gateway|serviceentries' | head -5
destinationrules.networking.istio.io                  2026-02-04T09:11:58Z
gateways.networking.istio.io                          2026-02-04T09:11:58Z
serviceentries.networking.istio.io                    2026-02-04T09:11:58Z
virtualservices.networking.istio.io                   2026-02-04T09:11:58Z
bash 复制代码
 # 查询 DestinationRule 支持的所有 API 版本
[root@mster isito]# kubectl api-resources | grep -i destinationrule
destinationrules                    dr           networking.istio.io/v1beta1       true         DestinationRule
[root@mster isito]# 
  • 修改apiVersion
bash 复制代码
#apiVersion: networking.istio.io/v1
apiVersion: networking.istio.io/v1beta1

# 按顺序应用配置
kubectl apply -f destinationrule.yaml
kubectl apply -f gateway.yaml
kubectl apply -f virtualservice.yaml

5、创建service和deployment

bash 复制代码
cat  << EOF demo-service.yaml 
apiVersion: v1
kind: Service
metadata:
  name: demo-service
  namespace: default
spec:
  selector:
    app: demo-service  # 这个标签必须匹配你的Deployment中Pod的标签
  ports:
  - port: 80          # Service端口
    targetPort: 80    # Pod容器端口
    protocol: TCP
EOF
bash 复制代码
cat  << EOF demo-deployment.yaml 
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo-service-v1
  labels:
    app: demo-service
    version: v1
spec:
  replicas: 2
  selector:
    matchLabels:
      app: demo-service
      version: v1
  template:
    metadata:
      labels:
        app: demo-service
        version: v1
    spec:
      containers:
      - name: nginx
        image: nginx:alpine
        ports:
        - containerPort: 80
        env:
        - name: NGINX_VERSION_TAG
          value: "v1"
        lifecycle:
          postStart:
            exec:
              command: ["/bin/sh", "-c", "echo 'Demo Service Version: v1' > /usr/share/nginx/html/index.html"]
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo-service-v2
  labels:
    app: demo-service
    version: v2
spec:
  replicas: 1
  selector:
    matchLabels:
      app: demo-service
      version: v2
  template:
    metadata:
      labels:
        app: demo-service
        version: v2
    spec:
      containers:
      - name: nginx
        image: nginx:alpine
        ports:
        - containerPort: 80
        env:
        - name: NGINX_VERSION_TAG
          value: "v2"
        lifecycle:
          postStart:
            exec:
              command: ["/bin/sh", "-c", "echo 'Demo Service Version: v2' > /usr/share/nginx/html/index.html"]
             
EOF             
bash 复制代码
## 按照顺序执行
kubectl apply -f demo-service.yaml
kubectl apply -f demo-deployment.yaml

6、测试与验证

1、修改权重比

bash 复制代码
cat << EOF virtualservice.yaml
#apiVersion: networking.istio.io/v1
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: demo-service
spec:
  hosts:
  - "*" # 匹配的域名,与Gateway中的hosts对应,生产环境请用具体域名
  gateways:
  - demo-gateway # 绑定上面创建的网关
  http:
  - route:
    - destination:
        host: demo-service.default.svc.cluster.local # 目标服务
        subset: v1 # 全部流量指向v1
      weight: 80
    - destination:
        host: demo-service.default.svc.cluster.local
        subset: v2 # 0%流量指向v2,初始状态
      weight: 20

EOF
bash 复制代码
kubectl apply -f virtualservice.yaml

2、测试联通性

bash 复制代码
 kubectl run test-curl --image=curlimages/curl --rm -it --restart=Never -- sh -c 'curl -s http://demo-service.default.svc.cluster.local'

Demo Service Version: v1
pod "test-curl" deleted

预期输出:Demo Service Version: v1 或 Demo Service Version: v2(根据你的VirtualService权重分配)。

3、通过网关进行外部测试:

bash 复制代码
export GATEWAY_URL=http://192.168.1.100:30881

# 发送多个请求,观察版本分布
for i in {1..10}; do
    curl -s $GATEWAY_URL
done

# 或者统计版本
for i in {1..20}; do
    curl -s $GATEWAY_URL | grep -o "v[12]"
done | sort | uniq -c

预期结果:根据你VirtualService中配置的权重(v1:80, v2:20),你可能会看到大约80%的请求返回"v1",20%返回"v2"。

4、再次调整权重比例

bash 复制代码
 cat << EOF  virtualservice.yaml 
#apiVersion: networking.istio.io/v1
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: demo-service
spec:
  hosts:
  - "*" # 匹配的域名,与Gateway中的hosts对应,生产环境请用具体域名
  gateways:
  - demo-gateway # 绑定上面创建的网关
  http:
  - route:
    - destination:
        host: demo-service.default.svc.cluster.local # 目标服务
        subset: v1 # 全部流量指向v1
      weight: 10
    - destination:
        host: demo-service.default.svc.cluster.local
        subset: v2 # 0%流量指向v2,初始状态
      weight: 90
     
EOF
bash 复制代码
kubectl apply -f virtualservice.yaml
bash 复制代码
kubectl run test-curl --image=curlimages/curl --rm -it --restart=Never -- sh -c 'curl -s http://demo-service.default.svc.cluster.local'
If you don't see a command prompt, try pressing enter.
Demo Service Version: v1
pod "test-curl" deleted

二、基于Isito的蓝绿发布

蓝绿发布与灰度发布的区别

  • 蓝绿发布
text 复制代码
状态 A(切换前):[🔵🔵🔵🔵🔵] 100% v1  +  🟢🟢🟢 0% v2(待机)
                ↓ 执行 kubectl apply(瞬间切换)
状态 B(切换后):[🔵🔵🔵] 0% v1      +  🟢🟢🟢🟢🟢 100% v2
  • 灰度发布
text 复制代码
阶段 1: [🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵] 100% v1  +  🟢 0% v2
                ↓ 调整权重
阶段 2: [🔵🔵🔵🔵🔵🔵🔵🔵🔵] 90% v1   +  🟢🟢 10% v2(观察1小时)
                ↓ 调整权重
阶段 3: [🔵🔵🔵🔵🔵] 50% v1          +  🟢🟢🟢🟢🟢 50% v2(观察1小时)
                ↓ 调整权重
阶段 4: [🔵] 0% v1                 +  🟢🟢🟢🟢🟢🟢🟢🟢🟢🟢 100% v2

✅ 选蓝绿发布的情况:

必须全量:法规要求或业务特性,不允许新旧版本并存(如支付接口)

依赖强一致性:数据库Schema不兼容,不能同时写两个版本

资源充足:你有两套完整的生产环境资源

追求极简操作:不想分阶段放量,就想一键切换、一键回滚

真实案例:银行核心系统升级。周日凌晨2点,100%流量从旧系统瞬间切换到新系统,验证5分钟没问题就收工,有问题立刻切回。

✅ 选灰度发布的情况:

功能迭代:每周/每天发布新功能,风险逐步释放

用户体验优先:不想让100%用户同时遇到bug

资源受限:没有两套完整环境的资源预算

需要真实反馈:想收集一小部分用户的真实使用数据

真实案例:某App改版首页。先让5%的用户体验新界面,观察3天,点击率提升10%后,扩大到50%,没问题再全量。

1、创建流量配置

bash 复制代码
cat << EOF  destinationrule.yaml 
# destinationrule.yaml
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: demo-service
  namespace: default
spec:
  host: demo-service.default.svc.cluster.local
  subsets:
  - name: blue  # 蓝组 = v1
    labels:
      version: v1
  - name: green # 绿组 = v2
    labels:
      version: v2
EOF
bash 复制代码
cat  << EOF gateway.yaml 
#apiVersion: networking.istio.io/v1
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
  name: demo-gateway
spec:
  selector:
    istio: ingressgateway # 使用已安装的入口网关
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "*" # 允许所有主机访问,生产环境请替换为你的域名,如 demo.example.com

EOF 
bash 复制代码
cat  << EOF virtualservice.yaml  
# virtualservice-bluegreen.yaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: demo-service
  namespace: default
spec:
  hosts:
  - "*"  # 与Gateway保持一致
  gateways:
  - demo-gateway
  - mesh   # 添加这一行,使规则对网格内 sidecar 也生效
  http:
  - route:
    # 🔵 蓝组 (当前生产环境 - v1)
    - destination:
        host: demo-service.default.svc.cluster.local
        subset: blue
      weight: 100  # 蓝组流量百分比
    # 🟢 绿组 (待发布版本 - v2)  
    - destination:
        host: demo-service.default.svc.cluster.local
        subset: green
      weight: 0    # 绿组流量百分比
EOF 
bash 复制代码
cat  << EOF demo-service.yaml 
apiVersion: v1
kind: Service
metadata:
  name: demo-service
  namespace: default
spec:
  selector:
    app: demo-service  # 这个标签必须匹配你的Deployment中Pod的标签
  ports:
  - port: 80          # Service端口
    targetPort: 80    # Pod容器端口
    protocol: TCP

EOF 
bash 复制代码
cat  << EOF deployment-gree-blue-v1.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo-service-v1
  namespace: default
  labels:
    app: demo-service
    version: v1
spec:
  replicas: 2
  selector:
    matchLabels:
      app: demo-service
      version: v1
  template:
    metadata:
      labels:
        app: demo-service
        version: v1
    spec:
      containers:
      - name: nginx
        image: nginx:alpine
        ports:
        - containerPort: 80
        command: ["/bin/sh"]
        args: ["-c", "echo 'Version: v1 - $(date)' > /usr/share/nginx/html/index.html; nginx -g 'daemon off;'"]

EOF 
bash 复制代码
cat << EOF  deployment-gree-blue-v2.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo-service-v2
  namespace: default
  labels:
    app: demo-service
    version: v2
spec:
  replicas: 2
  selector:
    matchLabels:
      app: demo-service
      version: v2
  template:
    metadata:
      labels:
        app: demo-service
        version: v2
    spec:
      containers:
      - name: nginx
        image: nginx:alpine
        ports:
        - containerPort: 80
        command: ["/bin/sh"]
        args: ["-c", "echo 'Version: v2 - $(date)' > /usr/share/nginx/html/index.html; nginx -g 'daemon off;'"]


EOF

2、测试与验证

1、容器内存测试(有错误后续分析原因)

  • 安装测试pod (istio是由二进制安装)
bash 复制代码
kubectl apply -f ~/istio-1.20.0/samples/sleep/sleep.yaml
bash 复制代码
kubectl exec deploy/sleep -- sh -c '
  for i in $(seq 1 100); do 
    curl -s http://demo-service.default.svc.cluster.local | grep -o "v[12]"
  done' | sort | uniq -c
     45 v1
     55 v2

2、主机测试

bash 复制代码
[root@node ~]# export GATEWAY_URL=http://192.168.1.100:30881
[root@node ~]# for i in {1..100}; do curl -s $GATEWAY_URL; done | grep -o "v[12]" | sort | uniq -c
    100 v1

3、pod内部无法安装全流量注入的原因以及修复方案

1、为 default 命名空间开启自动注入

bash 复制代码
kubectl label namespace default istio-injection=enabled --overwrite
kubectl get namespace default -L istio-injection
  • default 命名空间没有启用 Istio sidecar 自动注入(ISTIO-INJECTION 列为空),这是导致 sleep Pod 没有 sidecar 的根本原因。
bash 复制代码
# 删除已存在的 sleep 部署
kubectl delete deploy,sa,svc -l app=sleep

# 重新应用 sleep.yaml(推荐使用 Istio 官方示例)
kubectl apply -f istio-1.20.0/samples/sleep/sleep.yaml
# 如果您没有离线包,也可以使用以下命令在线获取:
# kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.20/samples/sleep/sleep.yaml
bash 复制代码
kubectl get pod -l app=sleep -w
text 复制代码
NAME                     READY   STATUS    RESTARTS   AGE
sleep-xxx-xxx            2/2     Running   0          30s
  • 这样操作下来。pod 内部流量还是会打到v2上

2、 修改pod 无法全部打到v1版本上

  • 现有的 demo-service VirtualService 仅用于网关(demo-gateway),hosts: "\*" 不变。
    新建一个 VirtualService 专门用于网格内部(mesh),hosts 设置为您的服务全名
bash 复制代码
cat  << EOF vs-internal.yaml  


apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: demo-service-internal
  namespace: default
spec:
  hosts:
  - demo-service.default.svc.cluster.local  # 必须指定确切的服务 FQDN
  gateways:
  - mesh                                    # 仅作用于网格内部
  http:
  - route:
    - destination:
        host: demo-service.default.svc.cluster.local
        subset: blue
      weight: 100
    - destination:
        host: demo-service.default.svc.cluster.local
        subset: green
      weight: 0
     
EOF      
  • pod 内部流量无法打打v1版本上,在实际生产中,如果pod 无法打到同一个版本中,就会导致,接口调用报错

4、撤换流量测试

1、为两个vs进行打标签

bash 复制代码
# 给 demo-service 打标签
kubectl label vs demo-service release-group=bluegreen

# 给 demo-service-internal 打标签
kubectl label vs demo-service-internal release-group=bluegreen

2、查看标签

bash 复制代码
## 打标签前
[root@mster isito]# kubectl get virtualservices.networking.istio.io --show-labels
NAME                    GATEWAYS           HOSTS                                        AGE     LABELS
demo-service            ["demo-gateway"]   ["*"]                                        3d17h   <none>
demo-service-internal   ["mesh"]           ["demo-service.default.svc.cluster.local"]   30h     <none>


## 打标签后
[root@mster isito]# kubectl get virtualservices.networking.istio.io --show-labels
NAME                    GATEWAYS           HOSTS                                        AGE     LABELS
demo-service            ["demo-gateway"]   ["*"]                                        3d17h   release-group=bluegreen
demo-service-internal   ["mesh"]           ["demo-service.default.svc.cluster.local"]   30h     release-group=bluegreen

3、设计流量切换

通过更改配置文件

bash 复制代码
# 获取所有带有标签 release-group=bluegreen 的 VirtualService 名称
for vs in $(kubectl get vs -n default -l release-group=bluegreen -o name); do
  echo "Patching $vs ..."
  kubectl patch $vs -n default --type='json' \
    -p='[
      {"op": "replace", "path": "/spec/http/0/route/0/weight", "value": 100},
      {"op": "replace", "path": "/spec/http/0/route/1/weight", "value": 0}
    ]'
done

通过脚本形式进行替换

bash 复制代码
cat << EOF switch.sh 
#!/bin/bash
# 用法: ./switch.sh blue 100   # 将两个 VS 的 blue 权重设为 100, green 为 0
#       ./switch.sh green 100  # 将两个 VS 的 green 权重设为 100, blue 为 0

TARGET=$1
WEIGHT=$2

if [ "$TARGET" == "blue" ]; then
  BLUE_WEIGHT=$WEIGHT
  GREEN_WEIGHT=$((100 - WEIGHT))
elif [ "$TARGET" == "green" ]; then
  BLUE_WEIGHT=$((100 - WEIGHT))
  GREEN_WEIGHT=$WEIGHT
else
  echo "Usage: $0 [blue|green] weight"
  exit 1
fi

for vs in demo-service demo-service-internal; do
  echo "Patching $vs to blue=$BLUE_WEIGHT, green=$GREEN_WEIGHT"
  kubectl patch vs $vs -n default --type='json' -p="[
    {\"op\": \"replace\", \"path\": \"/spec/http/0/route/0/weight\", \"value\": $BLUE_WEIGHT},
    {\"op\": \"replace\", \"path\": \"/spec/http/0/route/1/weight\", \"value\": $GREEN_WEIGHT}
  ]"
done

EOF

4、查看权重信息

bash 复制代码
 kubectl get vs -l release-group=bluegreen -o jsonpath='{range .items[*]}{.metadata.name}: {.spec.http[0].route}{"\n"}{end}'

5、测试版本切换

bash 复制代码
 ./switch.sh   blue 100 
./switch.sh   green 100 

## 容器测试
kubectl exec deploy/sleep -- sh -c '
  for i in $(seq 1 100); do 
    curl -s http://demo-service.default.svc.cluster.local | grep -o "v[12]"
  done' | sort | uniq -c

## 主机测试
export GATEWAY_URL=http://192.168.1.100:30881
for i in {1..100}; do curl -s $GATEWAY_URL; done | grep -o "v[12]" | sort | uniq -c


三、基于Istio的A\B测试发布方案

1、内部网关

bash 复制代码
cat << EOF vs-ab-testing.yaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: demo-service-ab-internal
  namespace: default
spec:
  hosts:
  - "demo-service.default.svc.cluster.local"
  gateways:
  - mesh
  http:
  - match:
    - headers:
        x-experiment:
          exact: "beta"
    route:
    - destination:
        host: demo-service.default.svc.cluster.local
        subset: v2
  - match:
    - headers:
        user-agent:
          regex: ".*Mobile.*"
    route:
    - destination:
        host: demo-service.default.svc.cluster.local
        subset: v2
  - route:
    - destination:
        host: demo-service.default.svc.cluster.local
        subset: v1
       
EOF      

2、外部网关

bash 复制代码
 cat  <<  EOF vs-ab-gateway.yaml 
 apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: demo-service-ab-gateway
  namespace: default
spec:
  hosts:
  - '*'
  gateways:
  - demo-gateway
  http:
  - match:
    - headers:
        cookie:
          regex: .*experiment=beta.*
    route:
    - destination:
        host: demo-service.default.svc.cluster.local
        subset: v2
  - match:
    - headers:
        user-agent:
          regex: .*(iPhone|Android|Mobile).*
    route:
    - destination:
        host: demo-service.default.svc.cluster.local
        subset: v2
  - route:
    - destination:
        host: demo-service.default.svc.cluster.local
        subset: v1

EOF

3、路由策略

bash 复制代码
cat  << EOF destinationrule.yaml
# destinationrule.yaml
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: demo-service
  namespace: default
spec:
  host: demo-service.default.svc.cluster.local
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v1
    labels:
      version: v2
   
EOF 

4、deployment

bash 复制代码
cat  << EOF demo-service-v1.yaml 
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    deployment.kubernetes.io/revision: "1"
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"apps/v1","kind":"Deployment","metadata":{"annotations":{},"labels":{"app":"demo-service","version":"v1"},"name":"demo-service-v1","namespace":"default"},"spec":{"replicas":2,"selector":{"matchLabels":{"app":"demo-service","version":"v1"}},"template":{"metadata":{"labels":{"app":"demo-service","version":"v1"}},"spec":{"containers":[{"args":["-c","echo 'Version: v1 - $(date)' \u003e /usr/share/nginx/html/index.html; nginx -g 'daemon off;'"],"command":["/bin/sh"],"image":"nginx:alpine","name":"nginx","ports":[{"containerPort":80}]}]}}}}
  creationTimestamp: "2026-02-12T16:45:43Z"
  generation: 1
  labels:
    app: demo-service
    version: v1
  name: demo-service-v1
  namespace: default
  resourceVersion: "89185"
  uid: 09f554fb-e447-4bf2-9338-71556d0cfab1
spec:
  progressDeadlineSeconds: 600
  replicas: 2
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      app: demo-service
      version: v1
  strategy:
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 25%
    type: RollingUpdate
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: demo-service
        version: v1
    spec:
      containers:
      - args:
        - -c
        - 'echo ''Version: v1 - $(date)'' > /usr/share/nginx/html/index.html; nginx
          -g ''daemon off;'''
        command:
        - /bin/sh
        image: nginx:alpine
        imagePullPolicy: IfNotPresent
        name: nginx
        ports:
        - containerPort: 80
          protocol: TCP
        resources: {}
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
      dnsPolicy: ClusterFirst
      restartPolicy: Always
      schedulerName: default-scheduler
      securityContext: {}
      terminationGracePeriodSeconds: 30
status:
  availableReplicas: 2
  conditions:
  - lastTransitionTime: "2026-02-12T16:45:43Z"
    lastUpdateTime: "2026-02-12T16:45:46Z"
    message: ReplicaSet "demo-service-v1-8689c65f4" has successfully progressed.
    reason: NewReplicaSetAvailable
    status: "True"
    type: Progressing
  - lastTransitionTime: "2026-02-14T01:32:53Z"
    lastUpdateTime: "2026-02-14T01:32:53Z"
    message: Deployment has minimum availability.
    reason: MinimumReplicasAvailable
    status: "True"
    type: Available
  observedGeneration: 1
  readyReplicas: 2
  replicas: 2
  updatedReplicas: 2

EOF
bash 复制代码
cat   << EOF  demo-service-v2.yaml 
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    deployment.kubernetes.io/revision: "1"
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"apps/v1","kind":"Deployment","metadata":{"annotations":{},"labels":{"app":"demo-service","version":"v2"},"name":"demo-service-v2","namespace":"default"},"spec":{"replicas":2,"selector":{"matchLabels":{"app":"demo-service","version":"v2"}},"template":{"metadata":{"labels":{"app":"demo-service","version":"v2"}},"spec":{"containers":[{"args":["-c","echo 'Version: v2 - $(date)' \u003e /usr/share/nginx/html/index.html; nginx -g 'daemon off;'"],"command":["/bin/sh"],"image":"nginx:alpine","name":"nginx","ports":[{"containerPort":80}]}]}}}}
  creationTimestamp: "2026-02-12T16:47:06Z"
  generation: 1
  labels:
    app: demo-service
    version: v2
  name: demo-service-v2
  namespace: default
  resourceVersion: "89242"
  uid: 1a27302b-6367-4961-a403-76b98fba7725
spec:
  progressDeadlineSeconds: 600
  replicas: 2
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      app: demo-service
      version: v2
  strategy:
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 25%
    type: RollingUpdate
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: demo-service
        version: v2
    spec:
      containers:
      - args:
        - -c
        - 'echo ''Version: v2 - $(date)'' > /usr/share/nginx/html/index.html; nginx
          -g ''daemon off;'''
        command:
        - /bin/sh
        image: nginx:alpine
        imagePullPolicy: IfNotPresent
        name: nginx
        ports:
        - containerPort: 80
          protocol: TCP
        resources: {}
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
      dnsPolicy: ClusterFirst
      restartPolicy: Always
      schedulerName: default-scheduler
      securityContext: {}
      terminationGracePeriodSeconds: 30
status:
  availableReplicas: 2
  conditions:
  - lastTransitionTime: "2026-02-12T16:47:06Z"
    lastUpdateTime: "2026-02-12T16:47:09Z"
    message: ReplicaSet "demo-service-v2-6698597ff4" has successfully progressed.
    reason: NewReplicaSetAvailable
    status: "True"
    type: Progressing
  - lastTransitionTime: "2026-02-14T01:32:56Z"
    lastUpdateTime: "2026-02-14T01:32:56Z"
    message: Deployment has minimum availability.
    reason: MinimumReplicasAvailable
    status: "True"
    type: Available
  observedGeneration: 1
  readyReplicas: 2
  replicas: 2
  updatedReplicas: 2

EOF

5、service

bash 复制代码
cat << EOF demo-service.yaml 
apiVersion: v1
kind: Service
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"name":"demo-service","namespace":"default"},"spec":{"ports":[{"port":80,"protocol":"TCP","targetPort":80}],"selector":{"app":"demo-service"}}}
  creationTimestamp: "2026-02-10T07:58:49Z"
  name: demo-service
  namespace: default
  resourceVersion: "39883"
  uid: ad840115-cc2c-4e89-bac5-158d94c524ff
spec:
  clusterIP: 10.97.68.36
  clusterIPs:
  - 10.97.68.36
  internalTrafficPolicy: Cluster
  ipFamilies:
  - IPv4
  ipFamilyPolicy: SingleStack
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: demo-service
  sessionAffinity: None
  type: ClusterIP
status:
  loadBalancer: {}

EOF

6、执行以上所有资源配置文件

7、测试验证

bash 复制代码
##集群pod内部应用
[root@mster isito]# kubectl exec deploy/sleep -- curl -s http://demo-service.default.svc.cluster.local
Version: v1 - $(date)
[root@mster isito]# kubectl exec deploy/sleep -- curl -s -H "X-Experiment: beta" http://demo-service.default.svc.cluster.local
Version: v2 - $(date)
[root@mster isito]# kubectl exec deploy/sleep -- curl -s -A "Mobile" http://demo-service.default.svc.cluster.local
Version: v2 - $(date)
[root@mster isito]# kubectl exec deploy/sleep -- curl -s -A "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)" http://demo-service.default.svc.cluster.local
Version: v2 - $(date)

### 外部链条访问
[root@node ~]# export GATEWAY_URL=http://192.168.1.100:30881
[root@node ~]# curl -s $GATEWAY_URL
Version: v1 - $(date)
[root@node ~]# curl -s --cookie "experiment=beta" $GATEWAY_URL
Version: v2 - $(date)
[root@node ~]# curl -s -A "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)" $GATEWAY_URL
Version: v2 - $(date)
[root@node ~]# 

四、基于Istio的镜像发布方案

🧩 为什么镜像发布不属于发布流程?

目的不同:发布流程的核心是让用户访问新版本(如蓝绿、灰度)。镜像发布的目的是在不影响用户的前提下,用真实流量测试新版本,新版本的响应被丢弃,用户完全无感知。

阶段不同:镜像发布通常发生在发布之前,作为"预发布"验证环节。只有当镜像验证通过后,才会进入真正的发布流程(如灰度或蓝绿发布),将流量切换到新版本。

用户无变化:发布流程一定会改变用户所见的服务版本或行为。镜像发布则完全不改变用户体验,仅用于后端评估。

1、镜像发布与A\B测试的区别

策略 镜像发布 A/B测试
本质 流量复制(影子流量) 流量分流(真实用户)
目标 在不影响生产的情况下... 对比两个版本的业务效果...
用户感知 无感知(响应仍来自原版本) 有感知(部分用户直接使用新版本)

镜像发布:主请求 → v1(响应返回客户端),同时复制一份 → v2(响应被丢弃)。用户无感知,仅用于后端测试。

权重路由(蓝绿/灰度发布):根据权重将真实用户请求直接发送到 v1 或 v2,客户端会直接收到对应版本的响应。

2、内部网关

bash 复制代码
cat   << EOF vs-mirror-internal.yaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: demo-service-mirror-internal
  namespace: default
spec:
  hosts:
  - "demo-service.default.svc.cluster.local"
  gateways:
  - mesh
  http:
  - route:
    - destination:
        host: demo-service.default.svc.cluster.local
        subset: v1
      weight: 100
    mirror:
      host: demo-service.default.svc.cluster.local
      subset: v2
    mirrorPercentage:
      value: 50.0
EOF

3、外部网关

bash 复制代码
cat  << EOF vs-mirror-gateway.yaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: demo-service-mirror-gateway
  namespace: default
spec:
  hosts:
  - "*"
  gateways:
  - demo-gateway
  http:
  - route:
    - destination:
        host: demo-service.default.svc.cluster.local
        subset: v1
      weight: 100
    mirror:
      host: demo-service.default.svc.cluster.local
      subset: v2
    mirrorPercentage:
      value: 50.0   # 镜像 50% 的请求,可调整
     
EOF      

4、应用资源

bash 复制代码
## 应用以上两个资源文件
## 关闭所有多余的vs资源
[root@mster isito]# kubectl get vs 
NAME                           GATEWAYS           HOSTS                                        AGE
demo-service-mirror-gateway    ["demo-gateway"]   ["*"]                                        82m
demo-service-mirror-internal   ["mesh"]           ["demo-service.default.svc.cluster.local"]   82m

5、查看dr资源

bash 复制代码
[root@mster isito]# kubectl get dr
NAME           HOST                                     AGE
demo-service   demo-service.default.svc.cluster.local   3d22h
[root@mster isito]# kubectl get dr demo-service  -o yaml 
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"networking.istio.io/v1beta1","kind":"DestinationRule","metadata":{"annotations":{},"name":"demo-service","namespace":"default"},"spec":{"host":"demo-service.default.svc.cluster.local","subsets":[{"labels":{"version":"v1"},"name":"v1"},{"labels":{"version":"v2"},"name":"v1"}]}}
  creationTimestamp: "2026-02-10T07:42:57Z"
  generation: 4
  name: demo-service
  namespace: default
  resourceVersion: "99440"
  uid: b5222fb0-6371-4bfc-bc84-c3ad991afc1d
spec:
  host: demo-service.default.svc.cluster.local
  subsets:
  - labels:
      version: v1
    name: v1
  - labels:
      version: v2
    name: v2

6、测试验证

bash 复制代码
## 查看deployment资源
[root@mster isito]# kubectl get deployment 
NAME              READY   UP-TO-DATE   AVAILABLE   AGE
demo-service-v1   2/2     2            2           37h
demo-service-v2   2/2     2            2           37h
sleep             1/1     1            1           36h
[root@mster isito]# kubectl get deployment --show-labels
NAME              READY   UP-TO-DATE   AVAILABLE   AGE   LABELS
demo-service-v1   2/2     2            2           37h   app=demo-service,version=v1
demo-service-v2   2/2     2            2           37h   app=demo-service,version=v2
sleep             1/1     1            1           36h   <none>
  • 内部pod测试
bash 复制代码
[root@mster isito]# for i in {1..10}; do   kubectl exec $SLEEP_POD -- curl -s http://demo-service.default.svc.cluster.local  ; done
Version: v1 - $(date)
Version: v1 - $(date)
Version: v1 - $(date)
Version: v1 - $(date)
Version: v1 - $(date)
Version: v1 - $(date)
Version: v1 - $(date)
Version: v1 - $(date)
Version: v1 - $(date)
Version: v1 - $(date)
[root@mster isito]# kubectl logs -l version=v2 -c istio-proxy --tail=20
error: container istio-proxy is not valid for pod demo-service-v2-6698597ff4-qqbpm
[root@mster isito]# kubectl logs -l version=v2 --tail=10
10.244.167.186 - - [14/Feb/2026:02:09:03 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/8.18.0" "-"
10.244.167.184 - - [14/Feb/2026:02:59:06 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/7.29.0" "10.244.219.64"
10.244.167.184 - - [14/Feb/2026:03:01:37 +0000] "GET / HTTP/1.1" 200 22 "-" "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)" "10.244.219.64"
10.244.167.184 - - [14/Feb/2026:03:22:14 +0000] "GET / HTTP/1.1" 200 22 "-" "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)" "10.244.219.64"
10.244.167.186 - - [14/Feb/2026:03:25:35 +0000] "GET / HTTP/1.1" 200 22 "-" "Mobile" "-"
10.244.167.186 - - [14/Feb/2026:03:25:44 +0000] "GET / HTTP/1.1" 200 22 "-" "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)" "-"
10.244.167.186 - - [14/Feb/2026:04:45:45 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/8.18.0" "10.244.167.186"
10.244.167.184 - - [14/Feb/2026:04:50:27 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/7.29.0" "10.244.219.64,10.244.167.184"
10.244.167.186 - - [14/Feb/2026:04:53:25 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/8.18.0" "10.244.167.186"
10.244.167.186 - - [14/Feb/2026:04:56:48 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/8.18.0" "10.244.167.186"
10.244.167.186 - - [14/Feb/2026:03:21:41 +0000] "GET / HTTP/1.1" 200 22 "-" "Mobile" "-"
10.244.167.184 - - [14/Feb/2026:03:22:06 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/7.29.0" "10.244.219.64"
10.244.167.186 - - [14/Feb/2026:03:25:27 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/8.18.0" "-"
10.244.167.186 - - [14/Feb/2026:04:45:45 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/8.18.0" "10.244.167.186"
10.244.167.186 - - [14/Feb/2026:04:45:46 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/8.18.0" "10.244.167.186"
10.244.167.184 - - [14/Feb/2026:04:51:07 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/7.29.0" "10.244.219.64,10.244.167.184"
10.244.167.186 - - [14/Feb/2026:04:56:47 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/8.18.0" "10.244.167.186"
10.244.167.186 - - [14/Feb/2026:04:56:47 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/8.18.0" "10.244.167.186"
10.244.167.186 - - [14/Feb/2026:04:56:48 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/8.18.0" "10.244.167.186"
10.244.167.186 - - [14/Feb/2026:04:58:18 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/8.18.0" "10.244.167.186"
[root@mster isito]# date
Fri Feb 13 20:58:39 PST 2026
[root@mster isito]# kubectl logs -l version=v1 --tail=10
10.244.167.186 - - [14/Feb/2026:04:58:10 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/8.18.0" "-"
10.244.167.186 - - [14/Feb/2026:04:58:10 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/8.18.0" "-"
10.244.167.186 - - [14/Feb/2026:04:58:11 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/8.18.0" "-"
10.244.167.186 - - [14/Feb/2026:04:58:11 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/8.18.0" "-"
10.244.167.186 - - [14/Feb/2026:04:58:16 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/8.18.0" "-"
10.244.167.186 - - [14/Feb/2026:04:58:17 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/8.18.0" "-"
10.244.167.186 - - [14/Feb/2026:04:58:17 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/8.18.0" "-"
10.244.167.186 - - [14/Feb/2026:04:58:18 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/8.18.0" "-"
10.244.167.186 - - [14/Feb/2026:04:58:18 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/8.18.0" "-"
10.244.167.186 - - [14/Feb/2026:04:58:18 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/8.18.0" "-"
10.244.167.186 - - [14/Feb/2026:04:56:47 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/8.18.0" "-"
10.244.167.186 - - [14/Feb/2026:04:56:47 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/8.18.0" "-"
10.244.167.186 - - [14/Feb/2026:04:56:49 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/8.18.0" "-"
10.244.167.186 - - [14/Feb/2026:04:58:07 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/8.18.0" "-"
10.244.167.186 - - [14/Feb/2026:04:58:10 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/8.18.0" "-"
10.244.167.186 - - [14/Feb/2026:04:58:10 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/8.18.0" "-"
10.244.167.186 - - [14/Feb/2026:04:58:17 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/8.18.0" "-"
10.244.167.186 - - [14/Feb/2026:04:58:17 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/8.18.0" "-"
10.244.167.186 - - [14/Feb/2026:04:58:17 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/8.18.0" "-"
10.244.167.186 - - [14/Feb/2026:04:58:18 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/8.18.0" "-"
You have new mail in /var/spool/mail/root
  • 外部主机测试
bash 复制代码
root@node ~]# for i in {1..10}; do   curl  $GATEWAY_URL ; done
Version: v1 - $(date)
Version: v1 - $(date)
Version: v1 - $(date)
Version: v1 - $(date)
Version: v1 - $(date)
Version: v1 - $(date)
Version: v1 - $(date)
Version: v1 - $(date)
Version: v1 - $(date)
Version: v1 - $(date)
[root@node ~]# date
Fri Feb 13 20:58:01 PST 2026

执行 curl 命令返回的都是 Version: v1

这是因为镜像发布中,主请求仍然由 v1 处理并返回响应,用户只能看到 v1 的结果。这是镜像发布的核心设计:让用户无感知,只将流量复制到 v2 进行后端验证。

v2 Pod 的日志中出现了最新的请求记录

您通过 kubectl logs -l version=v2 --tail=10 看到了来自 sleep Pod IP(10.244.167.186)的请求,例如 04:58:18 的日志。这些请求的时间与您测试的时间(PST 20:58)匹配,证明v2 确实收到了镜像流量。

7、版本切换

1、 切换脚本

bash 复制代码
cat    << EOF  adjust-traffic.sh 
#!/bin/bash
#
# 智能流量调整脚本 (适用于 Istio VirtualService)
# 用法:
#   ./adjust-traffic.sh [--create] <v1-weight> <v2-weight>
#   ./adjust-traffic.sh --target <version> <weight>
#   ./adjust-traffic.sh --step <target-version> <target-weight> [--interval <秒>]
#
# 示例:
#   ./adjust-traffic.sh 90 10                # 将两个 VS 的权重设置为 v1=90, v2=10
#   ./adjust-traffic.sh --target v2 50        # 将 v2 权重设为 50,v1 自动为 50
#   ./adjust-traffic.sh --create 80 20        # 创建网关和内部 VS (如果不存在),并设权重
#   ./adjust-traffic.sh --step v2 100 --interval 10  # 每10秒增加 10% 直到 v2=100

set -e

# 默认配置
NAMESPACE=${NAMESPACE:-default}
GW_VS_NAME=${GW_VS_NAME:-demo-service-canary-gateway}
INTERNAL_VS_NAME=${INTERNAL_VS_NAME:-demo-service-canary-internal}
GATEWAY_NAME=${GATEWAY_NAME:-demo-gateway}
SERVICE_FQDN=${SERVICE_FQDN:-demo-service.default.svc.cluster.local}
DR_NAME=${DR_NAME:-demo-service}

# 检查依赖命令
for cmd in kubectl jq; do
    if ! command -v $cmd &> /dev/null; then
        echo "错误: 未找到命令 '$cmd',请先安装。"
        exit 1
    fi
done

# 显示帮助
usage() {
    echo "用法: $0 [选项] <v1权重> <v2权重>"
    echo "  或: $0 --target <版本> <权重>"
    echo "  或: $0 --step <目标版本> <目标权重> [--interval <秒>]"
    echo ""
    echo "选项:"
    echo "  --create         如果 VirtualService 不存在则创建 (默认使用模板)"
    echo "  --target <ver>   指定目标版本 (v1 或 v2),权重将设为给定值,另一版本自动补全"
    echo "  --step           逐步调整模式,需指定目标版本和目标权重"
    echo "  --interval <秒>  逐步调整时的间隔 (默认 10 秒)"
    echo "  --help           显示此帮助"
    exit 0
}

# 检查参数
if [ $# -eq 0 ]; then usage; fi

# 解析参数
CREATE=false
STEP=false
INTERVAL=10
TARGET=""
TARGET_WEIGHT=""

while [[ $# -gt 0 ]]; do
    case $1 in
        --help) usage ;;
        --create) CREATE=true; shift ;;
        --step) STEP=true; shift ;;
        --interval) INTERVAL=$2; shift 2 ;;
        --target) TARGET=$2; TARGET_WEIGHT=$3; shift 3 ;;
        *) break ;;
    esac
done

# 权重处理
if [ -n "$TARGET" ]; then
    if [[ ! "$TARGET" =~ ^(v1|v2)$ ]]; then
        echo "错误: --target 必须是 v1 或 v2"
        exit 1
    fi
    if ! [[ "$TARGET_WEIGHT" =~ ^[0-9]+$ ]] || [ "$TARGET_WEIGHT" -lt 0 ] || [ "$TARGET_WEIGHT" -gt 100 ]; then
        echo "错误: 权重必须是 0-100 的整数"
        exit 1
    fi
    if [ "$TARGET" == "v1" ]; then
        V1_WEIGHT=$TARGET_WEIGHT
        V2_WEIGHT=$((100 - TARGET_WEIGHT))
    else
        V2_WEIGHT=$TARGET_WEIGHT
        V1_WEIGHT=$((100 - TARGET_WEIGHT))
    fi
else
    if [ $# -ne 2 ]; then
        echo "错误: 需要指定 v1 和 v2 权重"
        usage
    fi
    V1_WEIGHT=$1
    V2_WEIGHT=$2
    if ! [[ "$V1_WEIGHT" =~ ^[0-9]+$ ]] || ! [[ "$V2_WEIGHT" =~ ^[0-9]+$ ]] || [ $((V1_WEIGHT + V2_WEIGHT)) -ne 100 ]; then
        echo "错误: 权重必须为 0-100 的整数,且总和为 100"
        exit 1
    fi
fi

# 逐步调整模式
if [ "$STEP" = true ]; then
    if [ -z "$TARGET" ] || [ -z "$TARGET_WEIGHT" ]; then
        echo "错误: --step 需要指定 --target <版本> <目标权重>"
        exit 1
    fi
    # 获取当前权重
    if ! kubectl get vs $GW_VS_NAME -n $NAMESPACE &>/dev/null; then
        echo "错误: VirtualService $GW_VS_NAME 不存在,无法逐步调整"
        exit 1
    fi
    # 解析当前权重
    CUR_V1=$(kubectl get vs $GW_VS_NAME -n $NAMESPACE -o jsonpath='{.spec.http[0].route[?(@.destination.subset=="v1")].weight}')
    CUR_V2=$(kubectl get vs $GW_VS_NAME -n $NAMESPACE -o jsonpath='{.spec.http[0].route[?(@.destination.subset=="v2")].weight}')
    if [ -z "$CUR_V1" ] || [ -z "$CUR_V2" ]; then
        echo "错误: 无法从 $GW_VS_NAME 中获取当前权重"
        exit 1
    fi
    CURRENT_V1=$CUR_V1
    CURRENT_V2=$CUR_V2
    echo "当前权重: v1=$CURRENT_V1, v2=$CURRENT_V2"
    # 计算增量
    if [ "$TARGET" == "v1" ]; then
        TARGET_V1=$TARGET_WEIGHT
        TARGET_V2=$((100 - TARGET_WEIGHT))
        DIFF_V1=$((TARGET_V1 - CURRENT_V1))
        DIFF_V2=$(( - DIFF_V1 ))
    else
        TARGET_V2=$TARGET_WEIGHT
        TARGET_V1=$((100 - TARGET_WEIGHT))
        DIFF_V2=$((TARGET_V2 - CURRENT_V2))
        DIFF_V1=$(( - DIFF_V2 ))
    fi
    if [ $DIFF_V1 -eq 0 ] && [ $DIFF_V2 -eq 0 ]; then
        echo "当前已是目标权重,无需调整。"
        exit 0
    fi
    # 确定步长(每次调整 10% 或 5%,视差值而定,至少 1%)
    STEP_SIZE=10
    if [ ${DIFF_V1#-} -lt $STEP_SIZE ]; then
        STEP_SIZE=${DIFF_V1#-}
    fi
    if [ ${DIFF_V2#-} -lt $STEP_SIZE ]; then
        STEP_SIZE=${DIFF_V2#-}
    fi
    if [ $STEP_SIZE -eq 0 ]; then STEP_SIZE=1; fi
    echo "将逐步调整,每 $INTERVAL 秒调整 $STEP_SIZE% ..."
    while true; do
        # 计算下一步权重
        NEXT_V1=$CURRENT_V1
        NEXT_V2=$CURRENT_V2
        if [ $DIFF_V1 -gt 0 ]; then
            NEXT_V1=$((CURRENT_V1 + STEP_SIZE > TARGET_V1 ? TARGET_V1 : CURRENT_V1 + STEP_SIZE))
            NEXT_V2=$((100 - NEXT_V1))
        elif [ $DIFF_V1 -lt 0 ]; then
            NEXT_V1=$((CURRENT_V1 - STEP_SIZE < TARGET_V1 ? TARGET_V1 : CURRENT_V1 - STEP_SIZE))
            NEXT_V2=$((100 - NEXT_V1))
        fi
        echo "调整至: v1=$NEXT_V1, v2=$NEXT_V2"
        # 执行更新
        update_vs "$NEXT_V1" "$NEXT_V2"
        CURRENT_V1=$NEXT_V1
        CURRENT_V2=$NEXT_V2
        if [ $CURRENT_V1 -eq $TARGET_V1 ] && [ $CURRENT_V2 -eq $TARGET_V2 ]; then
            echo "已达到目标权重。"
            break
        fi
        sleep $INTERVAL
    done
    exit 0
fi

# 更新 VirtualService 的函数
update_vs() {
    local v1=$1
    local v2=$2
    echo "更新网关 VirtualService $GW_VS_NAME 为 v1=$v1, v2=$v2"
    kubectl patch vs $GW_VS_NAME -n $NAMESPACE --type='json' -p="[
        {\"op\": \"replace\", \"path\": \"/spec/http/0/route/0/weight\", \"value\": $v1},
        {\"op\": \"replace\", \"path\": \"/spec/http/0/route/1/weight\", \"value\": $v2}
    ]" 2>/dev/null || {
        if [ "$CREATE" = true ]; then
            echo "VirtualService $GW_VS_NAME 不存在,正在创建..."
            create_gateway_vs "$v1" "$v2"
        else
            echo "错误: 网关 VirtualService $GW_VS_NAME 不存在。请先创建或使用 --create 选项。"
            exit 1
        fi
    }
    echo "更新内部 VirtualService $INTERNAL_VS_NAME 为 v1=$v1, v2=$v2"
    kubectl patch vs $INTERNAL_VS_NAME -n $NAMESPACE --type='json' -p="[
        {\"op\": \"replace\", \"path\": \"/spec/http/0/route/0/weight\", \"value\": $v1},
        {\"op\": \"replace\", \"path\": \"/spec/http/0/route/1/weight\", \"value\": $v2}
    ]" 2>/dev/null || {
        if [ "$CREATE" = true ]; then
            echo "内部 VirtualService $INTERNAL_VS_NAME 不存在,正在创建..."
            create_internal_vs "$v1" "$v2"
        else
            echo "错误: 内部 VirtualService $INTERNAL_VS_NAME 不存在。请先创建或使用 --create 选项。"
            exit 1
        fi
    }
    echo "权重更新成功。"
}

# 创建网关 VirtualService 的模板
create_gateway_vs() {
    local v1=$1
    local v2=$2
    cat <<EOF | kubectl apply -f -
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: $GW_VS_NAME
  namespace: $NAMESPACE
spec:
  hosts:
  - "*"
  gateways:
  - $GATEWAY_NAME
  http:
  - route:
    - destination:
        host: $SERVICE_FQDN
        subset: v1
      weight: $v1
    - destination:
        host: $SERVICE_FQDN
        subset: v2
      weight: $v2
EOF
}

create_internal_vs() {
    local v1=$1
    local v2=$2
    cat <<EOF | kubectl apply -f -
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: $INTERNAL_VS_NAME
  namespace: $NAMESPACE
spec:
  hosts:
  - "$SERVICE_FQDN"
  gateways:
  - mesh
  http:
  - route:
    - destination:
        host: $SERVICE_FQDN
        subset: v1
      weight: $v1
    - destination:
        host: $SERVICE_FQDN
        subset: v2
      weight: $v2
EOF
}

# 主流程:执行更新
update_vs $V1_WEIGHT $V2_WEIGHT

# 验证 DestinationRule 是否存在
if ! kubectl get dr $DR_NAME -n $NAMESPACE &>/dev/null; then
    echo "警告: DestinationRule $DR_NAME 不存在,请确保已定义 v1 和 v2 子集。"
else
    echo "DestinationRule 检查通过。"
fi

# 可选:显示更新后的权重
echo "当前权重:"
kubectl get vs $GW_VS_NAME -n $NAMESPACE -o jsonpath='{.spec.http[0].route}' | jq .

EOF

2、脚本执行情况

bash 复制代码
./adjust-traffic.sh --create 90 10
更新网关 VirtualService demo-service-mirror-gateway 为 v1=90, v2=10
VirtualService demo-service-mirror-gateway 不存在,正在创建...
virtualservice.networking.istio.io/demo-service-mirror-gateway configured
更新内部 VirtualService demo-service-mirror-internal 为 v1=90, v2=10
内部 VirtualService demo-service-mirror-internal 不存在,正在创建...
virtualservice.networking.istio.io/demo-service-mirror-internal configured
权重更新成功。
DestinationRule 检查通过。
当前权重:
[
  {
    "destination": {
      "host": "demo-service.default.svc.cluster.local",
      "subset": "v1"
    },
    "weight": 90
  },
  {
    "destination": {
      "host": "demo-service.default.svc.cluster.local",
      "subset": "v2"
    },
    "weight": 10
  }
]
[root@mster isito]# kubectl get vs 
NAME                           GATEWAYS           HOSTS                                        AGE
demo-service-mirror-gateway    ["demo-gateway"]   ["*"]                                        99m
demo-service-mirror-internal   ["mesh"]           ["demo-service.default.svc.cluster.local"]   99m
[root@mster isito]# kubectl get vs -o yaml 
apiVersion: v1
items:
- apiVersion: networking.istio.io/v1beta1
  kind: VirtualService
  metadata:
    annotations:
      kubectl.kubernetes.io/last-applied-configuration: |
        {"apiVersion":"networking.istio.io/v1beta1","kind":"VirtualService","metadata":{"annotations":{},"name":"demo-service-mirror-gateway","namespace":"default"},"spec":{"gateways":["demo-gateway"],"hosts":["*"],"http":[{"route":[{"destination":{"host":"demo-service.default.svc.cluster.local","subset":"v1"},"weight":90},{"destination":{"host":"demo-service.default.svc.cluster.local","subset":"v2"},"weight":10}]}]}}
    creationTimestamp: "2026-02-14T04:38:04Z"
    generation: 3
    name: demo-service-mirror-gateway
    namespace: default
    resourceVersion: "120217"
    uid: ca642410-f291-4ba3-ad7b-86a9db409e1a
  spec:
    gateways:
    - demo-gateway
    hosts:
    - '*'
    http:
    - route:
      - destination:
          host: demo-service.default.svc.cluster.local
          subset: v1
        weight: 90
      - destination:
          host: demo-service.default.svc.cluster.local
          subset: v2
        weight: 10
- apiVersion: networking.istio.io/v1beta1
  kind: VirtualService
  metadata:
    annotations:
      kubectl.kubernetes.io/last-applied-configuration: |
        {"apiVersion":"networking.istio.io/v1beta1","kind":"VirtualService","metadata":{"annotations":{},"name":"demo-service-mirror-internal","namespace":"default"},"spec":{"gateways":["mesh"],"hosts":["demo-service.default.svc.cluster.local"],"http":[{"route":[{"destination":{"host":"demo-service.default.svc.cluster.local","subset":"v1"},"weight":90},{"destination":{"host":"demo-service.default.svc.cluster.local","subset":"v2"},"weight":10}]}]}}
    creationTimestamp: "2026-02-14T04:37:49Z"
    generation: 3
    name: demo-service-mirror-internal
    namespace: default
    resourceVersion: "120219"
    uid: 37e9474c-5128-4eef-9e57-f3e24db95cc4
  spec:
    gateways:
    - mesh
    hosts:
    - demo-service.default.svc.cluster.local
    http:
    - route:
      - destination:
          host: demo-service.default.svc.cluster.local
          subset: v1
        weight: 90
      - destination:
          host: demo-service.default.svc.cluster.local
          subset: v2
        weight: 10
kind: List
metadata:
  resourceVersion: ""
[root@mster isito]# ./adjust-traffic.sh --create 50 50 
更新网关 VirtualService demo-service-mirror-gateway 为 v1=50, v2=50
virtualservice.networking.istio.io/demo-service-mirror-gateway patched
更新内部 VirtualService demo-service-mirror-internal 为 v1=50, v2=50
virtualservice.networking.istio.io/demo-service-mirror-internal patched
权重更新成功。
DestinationRule 检查通过。
当前权重:
[
  {
    "destination": {
      "host": "demo-service.default.svc.cluster.local",
      "subset": "v1"
    },
    "weight": 50
  },
  {
    "destination": {
      "host": "demo-service.default.svc.cluster.local",
      "subset": "v2"
    },
    "weight": 50
  }
]
[root@mster isito]# kubectl get vs -o yaml 
apiVersion: v1
items:
- apiVersion: networking.istio.io/v1beta1
  kind: VirtualService
  metadata:
    annotations:
      kubectl.kubernetes.io/last-applied-configuration: |
        {"apiVersion":"networking.istio.io/v1beta1","kind":"VirtualService","metadata":{"annotations":{},"name":"demo-service-mirror-gateway","namespace":"default"},"spec":{"gateways":["demo-gateway"],"hosts":["*"],"http":[{"route":[{"destination":{"host":"demo-service.default.svc.cluster.local","subset":"v1"},"weight":90},{"destination":{"host":"demo-service.default.svc.cluster.local","subset":"v2"},"weight":10}]}]}}
    creationTimestamp: "2026-02-14T04:38:04Z"
    generation: 4
    name: demo-service-mirror-gateway
    namespace: default
    resourceVersion: "120709"
    uid: ca642410-f291-4ba3-ad7b-86a9db409e1a
  spec:
    gateways:
    - demo-gateway
    hosts:
    - '*'
    http:
    - route:
      - destination:
          host: demo-service.default.svc.cluster.local
          subset: v1
        weight: 50
      - destination:
          host: demo-service.default.svc.cluster.local
          subset: v2
        weight: 50
- apiVersion: networking.istio.io/v1beta1
  kind: VirtualService
  metadata:
    annotations:
      kubectl.kubernetes.io/last-applied-configuration: |
        {"apiVersion":"networking.istio.io/v1beta1","kind":"VirtualService","metadata":{"annotations":{},"name":"demo-service-mirror-internal","namespace":"default"},"spec":{"gateways":["mesh"],"hosts":["demo-service.default.svc.cluster.local"],"http":[{"route":[{"destination":{"host":"demo-service.default.svc.cluster.local","subset":"v1"},"weight":90},{"destination":{"host":"demo-service.default.svc.cluster.local","subset":"v2"},"weight":10}]}]}}
    creationTimestamp: "2026-02-14T04:37:49Z"
    generation: 4
    name: demo-service-mirror-internal
    namespace: default
    resourceVersion: "120710"
    uid: 37e9474c-5128-4eef-9e57-f3e24db95cc4
  spec:
    gateways:
    - mesh
    hosts:
    - demo-service.default.svc.cluster.local
    http:
    - route:
      - destination:
          host: demo-service.default.svc.cluster.local
          subset: v1
        weight: 50
      - destination:
          host: demo-service.default.svc.cluster.local
          subset: v2
        weight: 50
kind: List
metadata:
  resourceVersion: ""

3、测试情况

  • pod内部测试
bash 复制代码
[root@mster isito]# SLEEP_POD=$(kubectl get pod -l app=sleep -o jsonpath='{.items[0].metadata.name}')
[root@mster isito]# kubectl exec $SLEEP_POD -- sh -c 'i=1; while [ $i -le 100 ]; do curl -s http://demo-service.default.svc.cluster.local; i=$((i+1)); done' | grep -o "v[12]" | sort | uniq -c
     44 v1
     56 v2
  • 外部主机测试
bash 复制代码
[root@node ~]# export GATEWAY_URL=http://192.168.1.100:30881
[root@node ~]# for i in {1..100}; do curl -s $GATEWAY_URL; done | grep -o "v[12]" | sort | uniq -c
     87 v1
     13 v2
[root@node ~]# for i in {1..100}; do curl -s $GATEWAY_URL; done | grep -o "v[12]" | sort | uniq -c
     46 v1
     54 v2

五、基于Istio的故障注入

故障注入是一种主动向系统中引入故障(如延迟、错误响应)以测试系统弹性和容错能力的测试手段。它并不是一种发布流程(如灰度或蓝绿),而是混沌工程的核心实践,旨在验证系统在面对真实故障时的表现。

🎯 典型使用场景

  1. 验证超时与重试策略
    假设你的服务设置了超时2秒、重试2次。通过注入3秒延迟,可以验证:

请求是否在2秒后超时?

超时后是否触发重试?

重试是否超过了总次数限制?

这能确保配置的合理性,避免因超时设置不当导致连锁故障。

  1. 测试熔断与降级机制
    当你注入高比例的故障(如50%的503错误),可以验证:

服务熔断器是否会在错误率达到阈值时打开?

降级逻辑(如返回缓存数据或空结果)是否被正确触发?

熔断半开后恢复试探是否正常?

  1. 模拟依赖服务故障
    微服务架构中,一个服务的故障可能传播到上游。通过向下游服务注入故障,可以测试:

上游服务的容错逻辑(如快速失败、回退)。

全链路的监控告警是否能准确捕捉到故障源头。

  1. 验证监控与告警系统

    故障注入可以主动触发错误,验证监控系统是否能够准确记录、告警规则是否合理,确保在生产真的出问题时,团队能第一时间感知。

  2. 混沌工程实验

    在生产或类生产环境中,定期进行故障注入演练,提前发现系统弱点,提升整体韧性。例如:

"五一"大促前,模拟下游支付服务延迟,验证订单系统的处理能力。

随机注入实例故障,验证负载均衡是否重新分发流量。

📌 为什么它不属于"发布流程"?

与灰度发布、蓝绿发布等不同,故障注入的目的不是发布新功能,而是验证现有系统在异常情况下的行为。它通常发生在:

系统上线前:作为压力测试的一部分。

配置变更后:验证新设置(如超时、重试)是否生效。

定期演练:持续保证系统弹性。

1、内部网关

bash 复制代码
cat << EOF vs-fault-internal.yaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: demo-service-fault-internal
  namespace: default
spec:
  hosts:
  - "demo-service.default.svc.cluster.local"   # 必须为具体FQDN
  gateways:
  - mesh
  http:
  - fault:
      delay:
        percentage:
          value: 50.0
        fixedDelay: 3s
      abort:
        percentage:
          value: 10.0
        httpStatus: 503
    route:
    - destination:
        host: demo-service.default.svc.cluster.local
        subset: v1
      weight: 100

EOF 

2、外部网关

bash 复制代码
cat  << EOF vs-fault-gateway.yaml 
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: demo-service-fault-gateway
  namespace: default
spec:
  hosts:
  - "*"                     # 外部网关允许通配符
  gateways:
  - demo-gateway
  http:
  - fault:
      # 延迟注入:对50%的请求注入3秒延迟
      delay:
        percentage:
          value: 50.0
        fixedDelay: 3s
      # 中断注入:对10%的请求返回503错误
      abort:
        percentage:
          value: 10.0
        httpStatus: 503
    route:
    - destination:
        host: demo-service.default.svc.cluster.local
        subset: v1           # 所有流量仍去v1(故障注入发生在路由前)
      weight: 100

EOF

3 、应用资源

bash 复制代码
删除之前的vs
[root@mster isito]# kubectl get vs 
NAME                          GATEWAYS           HOSTS                                        AGE
demo-service-fault-gateway    ["demo-gateway"]   ["*"]                                        8m11s
demo-service-fault-internal   ["mesh"]           ["demo-service.default.svc.cluster.local"]   8m20s

4、 验证测试

1、内部pod 测试

bash 复制代码
SLEEP_POD=$(kubectl get pod -l app=sleep -o jsonpath='{.items[0].metadata.name}')
 for i in {1..20}; do   kubectl exec $SLEEP_POD -- time curl -s -o /dev/null -w "请求 $i: HTTP %{http_code}, 耗时 %{time_total}s\n" http://demo-service.default.svc.cluster.local; done

2、外部主机测试

bash 复制代码
 export GATEWAY_URL=http://192.168.1.100:30881
 for i in {1..20}; do   time curl -s -o /dev/null -w "请求 $i: HTTP %{http_code}, 耗时 %{time_total}s\n" $GATEWAY_URL; done

六、大成之作

1、架构图

混沌工程
发布流水线
金丝雀
蓝绿
A/B测试
代码提交
镜像构建
镜像发布验证
发布策略选择
逐步放量
瞬时切换
按条件分流
全量发布
故障注入实验
系统韧性验证
生产稳定运行
外部客户端
curl -H 'Host: gray.example.com'
curl -H 'Host: canary.example.com'
curl -H 'Host: bluegreen.example.com'
curl -H 'Host: mirror.example.com'
curl -H 'Host: fault.example.com'
Ingress Gateway

istio-ingressgateway
VirtualService gray
VirtualService canary
VirtualService bluegreen
VirtualService mirror
VirtualService fault
gray命名空间

nginx服务
canary命名空间

tomcat服务
bluegreen命名空间

alpine服务
mirror命名空间

nginx-alpine服务
fault命名空间

nginx-slim服务

2、通用网关配置(已存在)

bash 复制代码
cat  << EOF gateway.yaml  
#apiVersion: networking.istio.io/v1
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
  name: demo-gateway
spec:
  selector:
    istio: ingressgateway # 使用已安装的入口网关
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "*" # 允许所有主机访问,生产环境请替换为你的域名,如 demo.example.com

EOF 

2、 灰度发布(gray 命名空间)

bash 复制代码
kubectl create namespace gray
kubectl label namespace gray istio-injection=enabled  ## 允许自动注入

kubectl get namespace gray -L istio-injection  ## 查看标签情况,默认为空
相关推荐
Patrick_Wilson19 小时前
从「改个端口」到 502:Next.js on k8s 的容器端口、Service 映射与 env 覆盖
docker·kubernetes·next.js
探索云原生1 天前
K8s 1.36 这个 GA 特性,把 initContainer 拉模型的 hack 干掉了
ai·云原生·kubernetes
云恒要逆袭1 天前
运行你的第一个Docker容器
后端·docker·容器
Java之美2 天前
一次k8s升级引发的DevicePlugin注册失败
云原生·kubernetes
程序员老赵3 天前
10 分钟部署 OpenCode:Docker 一键安装,浏览器打开就能用 AI 写代码(附完整命令与排错)
docker·容器·ai编程
武子康6 天前
调查研究-183 Apple container:Mac 上用轻量 VM 跑 Linux 容器,Swift 会改写本地容器体验吗?
docker·容器·apple
2601_961875249 天前
决战申论100题2026|最新|范文
linux·容器·centos·debian·ssh·fabric·vagrant
java_cj9 天前
深入kube-apiserver认证机制:从Bearer Token到mTLS的完整认证链解析
linux·运维·服务器·云原生·容器·kubernetes
程序员老赵9 天前
服务器没有桌面?Docker 跑个 Chrome,浏览器就能远程用
docker·容器·devops