istio 实践指南

本书将介绍 istio 相关实战经验与总结,助你成为一名云原生服务网格老司机😎。

关于本书

本书为电子书形式,内容为本人多年的云原生与 istio 实战经验进行系统性整理的结果,不废话,纯干货。

阅读方式

评论与互动

本书已集成 giscus 评论系统,欢迎对感兴趣的文章进行评论与交流。

贡献

本书使用 mdbook 构建,已集成 Github Actions 自动构建和发布,欢迎 Fork 并 PR 来贡献干货内容 (点击右上角编辑按钮可快速修改文章)。

内容使用 markdown 格式,文章在 src 目录下。

许可证

您可以使用 署名 - 非商业性使用 - 相同方式共享 4.0 (CC BY-NC-SA 4.0) 协议共享。

优雅终止

概述

本文介绍在 istio 场景下实现优雅终止时需要重点关注的点,一些容器场景通用的关注点请参考 Kubenretes 最佳实践: 优雅终止

envoy 被强杀导致流量异常

当业务上了 istio 之后,流量被 sidecar 劫持,进程之间不会直接建立连接,而是经过了 sidecar 这一层代理:

当 Pod 开始停止时,它将从服务的 endpoints 中摘除掉,不再转发流量给它,同时 Sidecar 也会收到 SIGTERM 信号,立刻不再接受 inbound 新连接,但会保持存量 inbound 连接继续处理,outbound 方向流量仍然可以正常发起。

不过有个值得注意的细节,若 Pod 没有很快退出,istio 默认是会在停止开始的 5s 后强制杀死 envoy,当 envoy 进程不在了也就无法转发任何流量(不管是 inbound 还是 outbound 方向),所以就可能存在一些问题:

  1. 若被停止的服务提供的接口耗时本身较长(比如文本转语音),存量 inbound 请求可能无法被处理完就断开了。
  2. 若停止的过程需要调用其它服务(比如通知其它服务进行清理),outbound 请求可能会调用失败。

启用 EXIT_ON_ZERO_ACTIVE_CONNECTIONS

自 istio 1.12 开始,Sidecar 支持了 EXIT_ON_ZERO_ACTIVE_CONNECTIONS 这个环境变量,作用就是等待 Sidecar “排水” 完成,在响应时,也通知客户端去关闭长连接(对于 HTTP1 响应 “Connection: close” 这个头,对于 HTTP2 响应 GOAWAY 这个帧)。

如果想要全局启用,可以修改全局配置的 configmap,在 defaultConfig.proxyMetadata 下加上这个环境变量:

    defaultConfig:
      proxyMetadata:
        EXIT_ON_ZERO_ACTIVE_CONNECTIONS: "true"

如果想针对指定工作负载启用,可以给 Pod 加注解:

proxy.istio.io/config: '{ "proxyMetadata": { "EXIT_ON_ZERO_ACTIVE_CONNECTIONS": "true" } }'

若使用的是腾讯云服务网格 TCM,可以在控制台启用:

istio 性能优化

istiod 负载均衡

envoy 定时重连。

istiod HPA

istiod 无状态,可水平扩展。

xDS 按需下发

batch 推送间隔优化

istiod推送流控规则有合并推送策略,目前这个时间间隔默认值为100ms。可配,一般很少用户会关心这个,在 mesh 全局配置中可以改: PILOT_DEBOUNCE_AFTER 和 PILOT_DEBOUNCE_MAX。 主要取决于:用户期望流控规则更新的实时性,以及 istiod 稳定性的权衡,如果期望实时性高,则把防抖动时间设置短些,如果mesh规模大,希望istiod提高稳定性,则把防抖动时间设置长些。

关闭不必要的遥测

TODO

关闭 mtls

如果认为集群内是安全的,可以关掉 mtls 以提升性能

istio 版本

  • istio 1.8: 资源消耗上,envoy 大概有 30% 的降低

限制 namespace 以减少 sidecar 资源占用

istio 默认会下发 mesh 内集群服务所有可能需要的信息,以便让 sidecar 能够与任意 workload 通信。当集群规模较大,服务数量多,namespace 多,可能就会导致 sidecar 占用资源很高 (比如十倍于业务容器)。

如果只有部分 namespace 使用了 istio (sidecar 自动注入),而网格中的服务与其它没有注入 sidecar 的 namespace 的服务没有多大关系,可以配置下 istio 的 Sidecar 资源,限制一下 namespace,避免 sidecar 加载大量无用 outbound 的规则。

配置方法

apiVersion: networking.istio.io/v1beta1
kind: Sidecar
metadata:
  name: default
  namespace: istio-system
spec:
  egress:
  - hosts:
    - "prod/*"
    - "test/*"
  • 定义在 istio-system 命名空间下表示 Sidecar 配置针对所有 namespace 生效。
  • 在 egress 的 hosts 配置中加入开启了 sidecar 自动注入的 namespace,表示只下发这些跟这些 namespace 相关的 outbound 规则。

参考 Istio Sidecar 官方文档

为服务显式指定协议

背景

istio 需要知道服务提供什么七层协议,从而来为其配置相应协议的 filter chain,通常最好是显式声明协议,如果没有声明,istio 会自动探测,这个探测能力比较有限,有些时候可能会匹配协议错误(比如使用非标端口),导致无法正常工作。

本文将列出显示声明协议的方法。

集群内: 指定 Service 端口的协议

给集群内 Service 指定 port name 时加上相应的前缀或指定 appProtocol 字段可以显示声明协议,如:

kind: Service
metadata:
  name: myservice
spec:
  ports:
  - number: 8080
    name: rpc
    appProtocol: grpc # 指定该端口提供 grpc 协议的服务
  - number: 80
    name: http-web # 指定该端口提供 http 协议的服务

更多详细信息请参考 Explicit protocol selection

集群外: 手动创建 Service + Endpoint

如果服务在集群外部 (比如 mysql),我们可以为其手动创建一组 Service + Endpoint,且 Service 端口指定协议(跟上面一样),这样就可以在集群内通过 Service 访问外部服务,且正确识别协议。示例:

apiVersion: v1
kind: Service
metadata:
  name: mysql
  namespace: default
spec:
  ports:
  - port: 4000
    name: mysql
    protocol: TCP
    
---
apiVersion: v1
kind: Endpoints
metadata:
  name: mysql
  namespace: default
subsets:
- addresses:
  - ip: 190.64.31.232 # 替换外部服务的 IP 地址
  ports:
  - port: 4000
    name: mysql
    protocol: TCP

创建好之后就可以通过 svc 去访问外部服务了,本例中服务地址为: mysql.default.svc.cluster.local:4000

集群外: 使用 ServiceEntry 指定协议

如果外部服务可以被 DNS 解析,可以定义 ServiceEntry 来指定协议:

apiVersion: networking.istio.io/v1beta1
kind: ServiceEntry
metadata:
  name: external-mysql
spec:
  hosts:
  - mysql.example.com
  location: MESH_EXTERNAL
  ports:
  - number: 4000
    name: mysql
    protocol: mysql
  resolution: DNS

创建好之后就可以通过域名去访问外部服务了,本例中服务地址为: mysql.example.com:4000

为服务设置默认路由

很多时候一开始我们的服务没有多个版本,也没配置 vs,只有一个 deployment 和一个 svc,如果我们要为业务配置默认流量策略,可以直接创建 dr,给对应 host 设置 trafficPolicy,示例:

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: reviews
spec:
  host: reviews
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 100
  subsets:
  - name: v1
    labels:
      version: v1

需要注意的是,虽然 subsets 下也可以设置 trafficPolicy,但 subsets 下设置的不是默认策略,而且在没有 vs 明确指定路由到对应 subset 时,即便我们的服务只有一个版本,istio 也不会使用 subset 下指定的 trafficPolicy,错误示例:

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: reviews
spec:
  host: reviews
  subsets:
  - name: v1
    labels:
      version: v1
    trafficPolicy:
      connectionPool:
        tcp:
          maxConnections: 100

想要做的更好,可以定义下 vs,明确路由到指定版本(后续就可以针对不同版本指定不同的流量策略):

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: reviews
spec:
  hosts:
  - reviews
  http:
  - route:
    - destination:
        host: reviews
        subset: v1

配置 accesslog

本文介绍如何配置 istio 的 accesslog。

全局配置方法

修改 istio 配置:

kubectl -n istio-system edit configmap istio

编辑 yaml:

apiVersion: v1
kind: ConfigMap
metadata:
  name: istio
  namespace: istio-system
data:
  mesh: |
    accessLogEncoding: JSON
    accessLogFile: /dev/stdout
    accessLogFormat: ""
  • accessLogEncoding: 表示 accesslog 输出格式,istio 预定义了 TEXTJSON 两种日志输出格式。默认使用 TEXT,通常我们习惯改成 JSON 以提升可读性,同时也利于日志采集。
  • accessLogFile: 表示 accesslog 输出到哪里,通常我们指定到 /dev/stdout (标准输出),以便使用 kubectl logs 来查看日志,同时也利于日志采集。
  • accessLogFormat: 如果不想使用 istio 预定义的 accessLogEncoding,我们也可以使用这个配置来自定义日志输出格式。完整的格式规则与变量列表参考 Envoy 官方文档

如果使用 istioctl 安装的 istio,也可以用类似以下命令进行配置:

istioctl install --set profile=demo --set meshConfig.accessLogFile="/dev/stdout" --set meshConfig.accessLogEncoding="JSON"

局部启用 accesslog

在生产环境中,有时我们不想全局启用 accesslog,我们可以利用 EnvoyFilter 来实现只为部分 namespace 或 workload 启用 accesslog,参考 实用技巧: 局部启用 accesslog

日志格式

TEXT 格式

istio 的 text accesslog 配置格式见 源码 。转换成字符串为:

[%START_TIME%] "%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%" %RESPONSE_CODE% %RESPONSE_FLAGS% "%UPSTREAM_TRANSPORT_FAILURE_REASON%" %BYTES_RECEIVED% %BYTES_SENT% %DURATION% %RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% "%REQ(X-FORWARDED-FOR)%" "%REQ(USER-AGENT)%" "%REQ(X-REQUEST-ID)%" "%REQ(:AUTHORITY)%" "%UPSTREAM_HOST%" %UPSTREAM_CLUSTER% %UPSTREAM_LOCAL_ADDRESS% %DOWNSTREAM_LOCAL_ADDRESS% %DOWNSTREAM_REMOTE_ADDRESS% %REQUESTED_SERVER_NAME% %ROUTE_NAME%

JSON 格式

istio 的 json accesslog 配置格式见 源码 。转换成字符串为:

{
  "authority": "%REQ(:AUTHORITY)%",
  "bytes_received": "%BYTES_RECEIVED%",
  "bytes_sent": "%BYTES_SENT%",
  "downstream_local_address": "%DOWNSTREAM_LOCAL_ADDRESS%",
  "downstream_remote_address": "%DOWNSTREAM_REMOTE_ADDRESS%",
  "duration": "%DURATION%",
  "istio_policy_status": "%DYNAMIC_METADATA(istio.mixer:status)%",
  "method": "%REQ(:METHOD)%",
  "path": "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%",
  "protocol": "%PROTOCOL%",
  "request_id": "%REQ(X-REQUEST-ID)%",
  "requested_server_name": "%REQUESTED_SERVER_NAME%",
  "response_code": "%RESPONSE_CODE%",
  "response_flags": "%RESPONSE_FLAGS%",
  "route_name": "%ROUTE_NAME%",
  "start_time": "%START_TIME%",
  "upstream_cluster": "%UPSTREAM_CLUSTER%",
  "upstream_host": "%UPSTREAM_HOST%",
  "upstream_local_address": "%UPSTREAM_LOCAL_ADDRESS%",
  "upstream_service_time": "%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%",
  "upstream_transport_failure_reason": "%UPSTREAM_TRANSPORT_FAILURE_REASON%",
  "user_agent": "%REQ(USER-AGENT)%",
  "x_forwarded_for": "%REQ(X-FORWARDED-FOR)%"
}

参考资料

使用 corsPolicy 解决跨域问题

通常解决跨域问题都是在 web 框架中进行配置,使用 istio 后我们可以将其交给 istio 处理,业务不需要关心。本文介绍如何利用 Istio 配置来对 HTTP 服务启用跨域支持。

配置方法

Istio 中通过配置 VirtualService 的 corsPolicy 可以实现跨域支持,示例:

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: nginx
  namespace: istio-demo
spec:
  gateways:
  - istio-demo/nginx-gw
  hosts:
  - 'nginx.example.com'
  - 'test.example.com'
  http:
  - corsPolicy:
      allowOrigins:
      - regex: "https?://nginx.example.com|https?://test.example.com"
    route:
    - destination:
        host: nginx.istio-demo.svc.cluster.local
        port:
          number: 80
  • 关键配置在于 allowOrigins,表示允许带哪些 Origin 地址的请求。
  • 若有多个域名,使用 regex 匹配,| 符号分隔。
  • 若同时支持 http 和 https,regex 中的地址在 http 后面加 s?,表示匹配 httphttps,即两种协议同时支持。
  • 关于 corsPolicy 更多配置,参考 Istio CorsPolicy 官方文档

一些误区

有同学测试发现,当请求带上了错误的 Origin 或没带 Origin 时,响应内容也正常返回了:

$ curl -I -H 'Origin: http://fake.example.com' 1.1.1.1:80
HTTP/1.1 200 OK
...

为什么能正常返回呢?corsPolicy 没生效吗?有这样疑问的同学可能对 CORS 理解不太到位。

控制请求能否跨域的逻辑核心在于浏览器,浏览器通过判断请求响应的 access-control-allow-origin header 中是否有当前页面的地址,来判断该跨域请求能否被允许。所以业务要对跨域支持的关键点在于对 access-control-allow-origin 这个头的支持,通常一些 web 框架支持跨域也主要是干这个,为响应自动加上 access-control-allow-origin 响应头,istio 也不例外。

所以这里请求一般都能正常返回,只是如果跨域校验失败的话不会响应 access-control-allow-origin 这个 header 以告知浏览器该请求不能跨域,但响应的 body 是正常的,不会做修改。

基于 iphash 进行负载均衡

场景

根据源 IP 进行负载均衡,在 istio 中如何配置呢 ?

用法

配置 DestinationRule,指定 useSourceIp 负载均衡策略:

apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: bookinfo-ratings
spec:
  host: ratings.prod.svc.cluster.local
  trafficPolicy:
    loadBalancer:
      consistentHash:
        useSourceIp: true

参考资料

使用 websocket 协议

业务使用的 websocket 协议,想跑在 istio 中,那么在 istio 中如何配置 websocket 呢?

用法

由于 websocket 本身基于 HTTP,所以在 istio 中直接按照普通 http 来配就行了:

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: tornado-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "*"
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: tornado
spec:
  hosts:
  - "*"
  gateways:
  - tornado-gateway
  http:
  - match:
    - uri:
        prefix: /
    route:
    - destination:
        host: tornado
      weight: 100

参考资料

设置 max_body_size

背景

nginx 可以设置 client_max_body_size,那么在 istio 场景下如何调整客户端的最大请求大小呢?

解决方案

可以配置 envoyfilter:

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: limit-request-size
  namespace: istio-system
spec:
  workloadSelector:
    labels:
      istio: ingressgateway
  configPatches:
  - applyTo: HTTP_FILTER
    match:
      context: GATEWAY
      listener:
        filterChain:
          filter:
            name: envoy.http_connection_manager
    patch:
      operation: INSERT_BEFORE
      value:
        name: envoy.buffer
        typed_config:
          '@type': type.googleapis.com/udpa.type.v1.TypedStruct
          value:
            maxRequestBytes: 1048576  # 1 MB

已验证版本: istio 1.8

  • 更改 workloadSelector 以选中需要设置的 gateway

实现基于 Header 的授权

背景

部分业务场景在 http header 或 grpc metadata 中会有用户信息,想在 mesh 这一层来基于用户信息来对请求进行授权,如果不满足条件就让接口不返回相应的数据。

解决方案

Istio 的 AuthorizationPolicy 不支持基于 Header 的授权,但可以利用 VirtualService 来实现,匹配 http header (包括 grpc metadata),然后再加一个默认路由,使用的固定故障注入返回 401,示例:

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: helloworld-server
spec:
  hosts:
  - helloworld-server
  http:
  - name: whitelist
    match:
    - headers:
        end-user:
          regex: "roc"
    route:
    - destination:
        host: helloworld-server
        port:
          number: 9000
  - name: default
    route:
    - destination:
        host: helloworld-server
        port:
          number: 9000
    fault:
      abort:
        percentage:
          value: 100
        httpStatus: 401

利用 Prism 构造多版本测试服务

概述

Prism 是一个支持 http mock 的开源工具,可以解析 openapi 配置,根据配置进行相应的响应,我们可以利用它来实现部署多版本服务,用于测试 istio 多版本服务相关的功能。本文给出一个简单的示例。

准备 OpenAPI 配置

我们将 OpenAPI 配置文件存到 ConfigMap 中,用于后续挂载到 prism 作为配置文件 (prism-conf.yaml):

apiVersion: v1
kind: ConfigMap
metadata:
  name: prism-conf
data:
  mock-v1.yaml: |
    openapi: 3.0.3
    info:
      title: MockServer v2
      description: MockServer v2
      version: 1.0.0
    paths:
      '/':
        get:
          responses:
            '200':
              content:
                'text/plain':
                  schema:
                    type: string
                    example: v1
  mock-v2.yaml: |
    openapi: 3.0.3
    info:
      title: MockServer v2
      description: MockServer v2
      version: 1.0.0
    paths:
      '/':
        get:
          responses:
            '200':
              content:
                'text/plain':
                  schema:
                    type: string
                    example: v2

这里的配置很简单,两个 OpenAPI 配置文件,GET 方式请求 / 路径分别响应 v1v2 的字符串,以便从响应中就能区分出请求转发到了哪个版本的服务。

如果想用编辑器或 IDE 的 OpenAPI 插件编辑配置文件来定义更复杂的规则,可以先直接创建原生 OpenAPI 配置文件 (如 mock-v1.yamlmock-v2.yaml),然后使用类似下面的命令生成 configmap 的 yaml:

kubectl create configmap prism-conf --dry-run=client -o yaml
  --from-file=mock-v1.yaml \
  --from-file=mock-v2.yaml | \
  grep -v creationTimestamp > prism-conf.yaml

部署多版本服务

使用 Deployment 部署两个版本的 prism (注意开启下 sidecar 自动注入),分别挂载不同的 OpenAPI 配置,首先部署第一个版本 (mockserver-v1.yaml):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: mockserver-v1
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mockserver
      version: v1
  template:
    metadata:
      labels:
        app: mockserver
        version: v1
    spec:
      containers:
      - name: mockserver
        image: cr.imroc.cc/library/prism:4
        args:
        - mock
        - -h
        - 0.0.0.0
        - -p
        - "80"
        - /etc/prism/mock-v1.yaml
        volumeMounts:
        - mountPath: /etc/prism
          name: config
      volumes:
      - name: config
        configMap:
          name: prism-conf

再部署第二个版本 (mockserver-v2.yaml):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: mockserver-v2
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mockserver
      version: v2
  template:
    metadata:
      labels:
        app: mockserver
        version: v2
    spec:
      containers:
      - name: mockserver
        image: cr.imroc.cc/library/prism:4
        args:
        - mock
        - -h
        - 0.0.0.0
        - -p
        - "80"
        - /etc/prism/mock-v2.yaml
        volumeMounts:
        - mountPath: /etc/prism
          name: config
      volumes:
      - name: config
        configMap:
          name: prism-conf

最后创建 Service (mockserver-svc.yaml):

apiVersion: v1
kind: Service
metadata:
  name: mockserver
  labels:
    app: mockserver
spec:
  type: ClusterIP
  ports:
  - port: 80
    protocol: TCP
    name: http
  selector:
    app: mockserver

测试访问

没有定义任何其它规则,测试访问会随机负载均衡到 v1 和 v2:

$ for i in {1..10};do curl mockserver && echo ""; done
v2
v1
v2
v1
v2
v1
v2
v1
v2
v1

使用 DestinationRule 定义多版本服务

在 DestinationRule 定义使用 pod label 来区分 v1 和 v2 版本的服务 (mockserver-dr.yaml):

apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: mockserver
spec:
  host: mockserver
  subsets:
  - labels:
      app: mockserver
      version: v2
    name: v1
  - labels:
      app: mockserver
      version: v2
    name: v2

使用 VirtualService 定义多版本路由规则

这里定义一个简单的规则,v1 版本服务接收 80% 流量,v2 版本接收 20% (mockserver-vs.yaml):

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: mockserver
spec:
  hosts:
  - mockserver
  http:
  - route:
    - destination:
        host: mockserver
        port:
          number: 80
        subset: v1
      weight: 80
    - destination:
        host: mockserver
        port:
          number: 80
        subset: v2
      weight: 20

测试验证多版本流量转发规则

上面定义了 DestinationRule 和 VirtualService 之后,会根据定义的规则进行转发:

$ for i in {1..10};do curl mockserver && echo ""; done
v1
v2
v1
v1
v2
v1
v1
v1
v1
v1

隐藏自动添加的 server header

背景

出于安全考虑,希望隐藏 istio 自动添加的 server: istio-envoy 这样的 header。

解决方案

可以配置 envoyfilter ,让 envoy 返回响应时不自动添加 server 的 header,将HttpConnectionManager 的 server_header_transformation 设为 PASS_THROUGH(后端没返回该header时envoy也不会自动添加):

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: hide-headers
  namespace: istio-system
spec:
  configPatches:
  - applyTo: NETWORK_FILTER
    match:
      context: ANY
      listener:
        filterChain:
          filter:
            name: "envoy.http_connection_manager"
    patch:
      operation: MERGE
      value:
        typed_config:
          "@type": "type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager"
          server_header_transformation: PASS_THROUGH

参考资料

调试技巧

测试与 istiod 的连通性

测试数据面是否能连上 xDS 端口:

nc -vz istiod-1-8-1.istio-system 15012

测试 istiod 监控接口:

$ kubectl -n debug exec debug-6fd7477c9d-brqmq -c istio-proxy -- curl -sS istiod-1-8-1.istio-system:15014/debug/endpointz
[

{"svc": "cert-manager-webhook-dnspod.cert-manager.svc.cluster.local:https", "ep": [
{
    "service": {
      "Attributes": {
        "ServiceRegistry": "Kubernetes",
        "Name": "cert-manager-webhook-dnspod",
        "Namespace": "cert-manager",
        "Labels": {

没报错,正常返回 json 说明数据面能正常连接控制面

配置 accesslog

配置方法参考 这里

调整 proxy 日志级别

配置方法参考 这里

获取 metrics

kubectl -n test exec -c istio-proxy htmall-6657db8f8f-l74qm -- curl -sS localhost:15090/stats/prometheus

查看 envoy 配置

导出 envoy 配置:

kubectl exec -it -c istio-proxy $POD_NAME -- curl 127.0.0.1:15000/config_dump > dump.json

导出 envoy 全量配置(包含 EDS):

kubectl exec -it -c istio-proxy $POD_NAME -- curl 127.0.0.1:15000/config_dump?include_eds > dump.json

自定义 proxy 日志级别

概述

本文介绍在 istio 中如何自定义数据面 (proxy) 的日志级别,方便我们排查问题时进行调试。

动态调整

调低 proxy 日志级别进行 debug 有助于排查问题,但输出内容较多且耗资源,不建议在生产环境一直开启低级别的日志,istio 默认使用 warning 级别。

我们可以使用 istioctl 动态调整 proxy 日志级别:

istioctl -n istio-test proxy-config log productpage-v1-7668cb67cc-86q8l --level debug

还可以更细粒度控制:

istioctl -n istio-test proxy-config log productpage-v1-7668cb67cc-86q8l --level grpc:trace,config:debug

更多 level 可选项参考: istioctl proxy-config log --help

如果没有 istioctl,也可以直接使用 kubectl 进入 istio-proxy 调用 envoy 接口来动态调整:

kubectl exec -n istio-test  productpage-v1-7668cb67cc-86q8l -c istio-proxy -- curl -XPOST -s -o /dev/null http://localhost:15000/logging?level=debug

使用 annotation 指定

如果不用动态调整,也可以在部署时为 Pod 配置 annotation 来指定 proxy 日志级别:

  template:
    metadata:
      annotations:
        "sidecar.istio.io/logLevel": debug # 可选: trace, debug, info, warning, error, critical, off

全局配置

如果是测试集群,你也可以全局配置 proxy 日志级别:

kubectl -n istio-system edit configmap istio-sidecar-injector

修改 values 里面的 global.proxy.logLevel 字段即可。

如果使用 istioctl 安装 istio,也可以使用类似以下命令配置全局 proxy 日志级别:

istioctl install --set profile=demo --set values.global.proxy.logLevel=debug

配置 envoy componentLogLevel

如何细粒度的调整 envoy 自身的内部日志级别呢?可以给 Pod 指定 annotation 来配置:

  template:
    metadata:
      annotations:
        "sidecar.istio.io/componentLogLevel": "ext_authz:trace,filter:debug"

局部启用 accesslog

背景

在生产环境中,有时我们不想全局启用 accesslog,只想为部分 namespace 或 workload 启用 accesslog,而 istio 对 accesslog 的配置是全局的,如何只为部分数据面启用 accesslog 呢?下面介绍具体操作方法。

为部分 namespace 启用 accesslog

可以使用以下 Envoyfilter 来实现:

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: enable-accesslog
  namespace: test # 只为 test 命名空间开启 accesslog
spec:
  configPatches:
  - applyTo: NETWORK_FILTER
    match:
      context: ANY
      listener:
        filterChain:
          filter:
            name: envoy.http_connection_manager
    patch:
      operation: MERGE
      value:
        typed_config:
          "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager"
          access_log:
          - name: envoy.access_loggers.file
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
              path: "/dev/stdout"
              log_format:
                json_format:
                  authority: "%REQ(:AUTHORITY)%"
                  bytes_received: "%BYTES_RECEIVED%"
                  bytes_sent: "%BYTES_SENT%"
                  downstream_local_address: "%DOWNSTREAM_LOCAL_ADDRESS%"
                  downstream_remote_address: "%DOWNSTREAM_REMOTE_ADDRESS%"
                  duration: "%DURATION%"
                  method: "%REQ(:METHOD)%"
                  path: "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%"
                  protocol: "%PROTOCOL%"
                  request_id: "%REQ(X-REQUEST-ID)%"
                  requested_server_name: "%REQUESTED_SERVER_NAME%"
                  response_code: "%RESPONSE_CODE%"
                  response_flags: "%RESPONSE_FLAGS%"
                  route_name: "%ROUTE_NAME%"
                  start_time: "%START_TIME%"
                  upstream_cluster: "%UPSTREAM_CLUSTER%"
                  upstream_host: "%UPSTREAM_HOST%"
                  upstream_local_address: "%UPSTREAM_LOCAL_ADDRESS%"
                  upstream_service_time: "%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%"
                  upstream_transport_failure_reason: "%UPSTREAM_TRANSPORT_FAILURE_REASON%"
                  user_agent: "%REQ(USER-AGENT)%"
                  x_forwarded_for: "%REQ(X-FORWARDED-FOR)%"

为部分 workload 启用 accesslog

如果想要精确到只为指定的 workload 启用 accesslog,可以在 EnvoyFilter 上加一下 workloadSelector:

spec:
  workloadSelector:
    labels:
      app: "nginx"

低版本 istio

低版本 istio 使用的 envoy 不支持 v3 api,可以使用 v2:

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: enable-accesslog
  namespace: test # 只为 test 命名空间开启 accesslog
spec:
  configPatches:
  - applyTo: NETWORK_FILTER
    match:
      context: ANY
      listener:
        filterChain:
          filter:
            name: envoy.http_connection_manager
    patch:
      operation: MERGE
      value:
        typed_config:
          "@type": "type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager"
          access_log:
          - name: envoy.file_access_log
            config:
              path: /dev/stdout

Sidecar 停止问题

背景

Istio 在 1.1 版本之前有个问题: Pod 销毁时,如果进程在退出过程中继续调用其它服务 (比如通知另外的服务进行清理),会调用失败。

更多详细信息请参考 issue #7136: Envoy shutting down before the thing it's wrapping can cause failed requests

原因

Kubernetes 在销毁 Pod 的过程中,会同时给所有容器发送 SIGTERM 信号,所以 Envoy 跟业务容器同时开始停止,Envoy 停止过程中不接受 inbound 新连接,默认在 5s 内会接收 outbound 新连接,5s 后 envoy 被强制杀死。又由于 istio 会进行流量劫持,所有 outbound 流量都会经过 Envoy 进行转发,如果 Envoy 被杀死,outbound 流量无法被转发,就会导致业务调用其它服务失败。

社区解决方案

如果 Kubernetes 自身支持容器依赖管理,那这个问题自然就可以解决掉。社区也提出了 Sidecar Container 的特性,只可惜最终还是被废弃了,新的方案还未落地,详细可参考 这篇笔记

后来随着 istio 社区的推进,针对优雅终止场景进行了一些优化:

  • 2019-02: Liam White 提交 PR Envoy Graceful Shutdown ,让 Pod 在停止过程中 Envoy 能够实现优雅停止 (保持存量连接继续处理,但拒绝所有新连接),等待 terminationDrainDuration 时长后再停掉 envoy 实例。该 PR 最终被合入 istio 1.1。
  • 2019-11: Rama Chavali 提交 PR move to drain listeners admin endpoint ,将 Envoy 优雅停止的方式从热重启改成调用 Envoy 后来自身提供的 admin 接口 (/drain_listeners?inboundonly) ,重点在于带上了 inboundonly 参数,即仅仅拒绝 inbound 方向的新连接,outbound 的新连接仍然可以正常发起,这也使得 Pod 在停止过程中业务进程继续调用其它服务得以实现。该 PR 最终被合入 istio 1.5。

所以在 istio 1.5 及其以上的版本,在 Pod 停止期间的一小段时间内 (默认 5s),业务进程仍然可以对其它服务发请求。

如何解决 ?

考虑从自定义 terminationDrainDuration 或加 preStop 判断连接处理完两种方式之一,详细请参考 istio 最佳实践: 优雅终止

Sidecar 启动顺序问题

背景

一些服务在往 istio 上迁移过渡的过程中,有时可能会遇到 Pod 启动失败,然后一直重启,排查原因是业务启动时需要调用其它服务(比如从配置中心拉取配置),如果失败就退出,没有重试逻辑。调用失败的原因是 envoy 还没就绪(envoy也需要从控制面拉取配置,需要一点时间),导致业务发出的流量无法被处理,从而调用失败(参考 k8s issue #65502 )。

最佳实践

目前这类问题的最佳实践是让应用更加健壮一点,增加一下重试逻辑,不要一上来调用失败就立马退出,如果嫌改动麻烦,也可以在启动命令前加下 sleep,等待几秒 (可能不太优雅)。

如果不想对应用做任何改动,也可以参考下面的规避方案。

规避方案: 调整 sidecar 注入顺序

在 istio 1.7,社区通过给 istio-injector 注入逻辑增加一个叫 HoldApplicationUntilProxyStarts 的开关来解决了该问题,开关打开后,proxy 将会注入到第一个 container。

查看 istio-injector 自动注入使用的 template,可以知道如果打开了 HoldApplicationUntilProxyStarts 就会为 sidecar 添加一个 postStart hook:

它的目的是为了阻塞后面的业务容器启动,要等到 sidecar 完全启动了才开始启动后面的业务容器。

这个开关配置分为全局和局部两种,以下是启用方法。

全局配置:

修改 istio 的 configmap 全局配置:

kubectl -n istio-system edit cm istio

defaultConfig 下加入 holdApplicationUntilProxyStarts: true

apiVersion: v1
data:
  mesh: |-
    defaultConfig:
      holdApplicationUntilProxyStarts: true
  meshNetworks: 'networks: {}'
kind: ConfigMap

若使用 IstioOperator,defaultConfig 修改 CR 字段 meshConfig:

apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
metadata:
  namespace: istio-system
  name: example-istiocontrolplane
spec:
  meshConfig:
    defaultConfig:
      holdApplicationUntilProxyStarts: true

如果你使用了 TCM (Tecnet Cloud Mesh),已经产品化了该能力,直接开启 Sidecar 就绪保障 即可:

局部配置:

如果使用 istio 1.8 及其以上的版本,可以为需要打开此开关的 Pod 加上 proxy.istio.io/config 注解,将 holdApplicationUntilProxyStarts 置为 true,示例:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      annotations:
        proxy.istio.io/config: |
          holdApplicationUntilProxyStarts: true
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: "nginx"

需要注意的是,打开这个开关后,意味着业务容器需要等 sidecar 完全 ready 后才能启动,会让 Pod 启动速度变慢一些。在需要快速扩容应对突发流量场景可能会显得吃力,所以建议是自行评估业务场景,利用局部配置的方法,只给需要的业务打开此开关。

完美方案: K8S 支持容器依赖

最完美的方案还是 Kubernetes 自身支持容器依赖,社区也提出了 Sidecar Container 的特性,只可惜最终还是被废弃了,新的方案还未落地,详细可参考 这篇笔记

参考资料

Smart DNS 相关问题

启用 Smart DNS 后解析失败

现象

在启用了 istio 的 Smart DNS (智能 DNS) 后,我们发现有些情况下 DNS 解析失败,比如:

  • 基于 alpine 镜像的容器内解析 dns 失败。
  • grpc 服务解析 dns 失败。

原因

Smart DNS 初期实现存在一些问题,响应的 DNS 数据包格式跟普通 DNS 有些差别,走底层库 glibc 解析没问题,但使用其它 dns 客户端可能就会失败:

  • alpine 镜像底层库使用 musl libc,解析行为跟 glibc 有些不一样,musl libc 在这种这种数据包格式异常的情况会导致解析失败,而大多应用走底层库解析,导致大部分应用解析失败。
  • 基于 c/c++ 的 grpc 框架的服务,dns 解析默认使用 c-ares 库,没有走系统调用让底层库解析,c-ares 在这种数据包异常情况,部分场景会解析失败。

修复

在 istio 1.9.2 的时候修复了这个问题,参考关键 PR #31251 以及其中一个 issue

规避

如果暂时无法升级 istio 到 1.9.2 以上,可以通过以下方式来规避:

  • 基础镜像从 alpine 镜像到其它镜像 (其它基础镜像底层库基本都是 glibc)。
  • c/c++ 的 grpc 服务,指定 GRPC_DNS_RESOLVER 环境变量为 native,表示走底层库解析,不走默认的 c-ares 库。环境变量解释参考 GRPC 官方文档

HTTP Header 大小写问题

Envoy 默认会将 Header 转换为小写

Envoy 缺省会把 http header 的 key 转换为小写,例如有一个 http header Test-Upper-Case-Header: some-value,经过 envoy 代理后会变成 test-upper-case-header: some-value。这个在正常情况下没问题,RFC 2616 规范也说明了处理 HTTP Header 应该是大小写不敏感的。

可能依赖大小写的场景

通常 header 转换为小写不会有问题(符合规范),有些情况对 header 大小写敏感可能就会有问题,如:

  • 业务解析 header 依赖大小写。
  • 使用的 SDK 对 Header 大小写敏感,如读取 Content-Length 来判断 response 长度时依赖首字母大写。

Envoy 所支持的规则

Envoy 只支持两种规则:

  • 全小写 (默认使用的规则)
  • 首字母大写 (默认没有启用)

如果应用的 http header 的大小写完全没有规律,就没有办法兼容了。

这两种是可以的:

  • test-upper-case-header: some-value
  • Test-Upper-Case-Header: some-value

类似这种就没有办法兼容了:

  • Test-UPPER-CASE-Header: some-value

规避方案: 强制指定为 TCP 协议

我们可以将服务声明为 TCP 协议,不让 istio 进行七层处理,这样就不会更改 http header 大小写了,但需要注意的是同时也会丧失 istio 的七层能力。

如果服务在集群内,可以在 Service 的 port 名称中带上 "tcp" 前缀:

kind: Service
metadata:
  name: myservice
spec:
  ports:
  - number: 80
    name: tcp-web # 指定该端口协议为 tcp

如果服务在集群外,可以通过一个类似如下 ServiceEntry 将服务强制指定为 TCP Service,以避免 envoy 对其进行七层的处理:

apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: qcloud-cos
spec:
  hosts:
  - "private-1251349835.cos.ap-guangzhou.myqcloud.com"
  location: MESH_INTERNAL
  addresses:
  - 169.254.0.47
  ports:
  - number: 80
    name: tcp
    protocol: TCP
  resolution: DNS

更多协议指定方式请参考: 为服务显式指定协议

最佳实践: 使用 EnvoyFilter 指定 Header 规则为首字母大写

如果希望 Envoy 对某些请求开启 Header 首字母大写的规则,可以用 EnvoyFilter 来指定:

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: http-header-proper-case-words
  namespace: istio-system
spec:
  configPatches:
  - applyTo: NETWORK_FILTER # http connection manager is a filter in Envoy
    match:
      # context omitted so that this applies to both sidecars and gateways
      listener:
        name: XXX # 指定 cos使用的listener name,可以从config_dump中查询到
        filterChain:
          filter:
            name: "envoy.http_connection_manager"
    patch:
      operation: MERGE
      value:
        name: "envoy.http_connection_manager"
        typed_config:
          "@type": "type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager"
          http_protocol_options:
            header_key_format:
              proper_case_words: {}

注意替换 listener name

建议

应用程序应遵循 RFC 2616 规范,对 Http Header 的处理采用大小写不敏感的原则。

httpHeaderName 大写导致会话保持不生效

问题描述

在 DestinationRule 中配置了基于 http header 的会话保持,header 名称大写:

  trafficPolicy:
    loadBalancer:
      consistentHash:
        httpHeaderName: User

测试会发现会话保持不生效,每次请求传入相同 Header (如 User: roc) 却被转发了不同后端

原因

应该是 envoy 默认把 header 转成小写的缘故导致不生效。

解决方案

定义 httpHeaderName 时换成小写,如:

  trafficPolicy:
    loadBalancer:
      consistentHash:
        httpHeaderName: User

注意事项

  • 如果之前已经定义了 DestinationRule,不要直接修改,而是先删除,然后再创建修改后的 DestinationRule (实测发现直接修改成可能不生效)
  • 客户端请求时设置的 header 大小写可以无所谓。

链路追踪相关问题

tracing 信息展示不完整

通过 UI 展示的链路追踪显示不完整,缺失前面或后面的调用链路。

原因

绝大多数情况下都是因为没在业务层面将 tracing 所需要的 http header 正确传递或根本没有传递。

要在 istio 中使用链路追踪,并不是说业务无侵入,有个基本要求是:业务收到 tracing 相关的 header 要将其传递给被调服务。这个步骤是无法让 istio 帮你完成的,因为 istio 无法感知你的业务逻辑,不知道业务中调用其它服务的请求到底是该对应前面哪个请求,所以需要业务来传递 header,最终才能将链路完整串起来。

参考资料

Sidecar 注入相关问题

可以只在一端注入 sidecar 吗?

  • Q: 只在客户端和服务端其中一方注入 sidecar,是否能够正常工作呢?
  • A: 一般是建议都注入。有些功能在 outbound 和 inbound 端都需要,有些只在其中一端需要,下面一张图可以一目了然:

默认的重试策略导致非幂等服务异常

背景

Istio 为 Envoy 设置了缺省的重试策略,会在 connect-failure,refused-stream, unavailable, cancelled, retriable-status-codes 等情况下缺省重试两次。出现错误时,可能已经触发了服务器逻辑,在操作不是幂等的情况下,可能会导致错误。

解决方案

可以通过配置 VS 关闭重试:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: ratings
spec:
  hosts:
  - ratings
  http:
  - retries:
      attempts: 0

多集群相关问题

概述

本文介绍 istio 多集群下需要注意的问题。

跨集群访问 service

同一网格的多个集群之间通过 service 调用,可能调用失败,报错 dns 解析失败。

原因可能是没启用 Smart DNS,无法自动解析其它集群的 service 域名,可以通过手动在本集群创建跟对端集群一样的 service 来解决,也可以启用 Smart DNS 自动解析。

应用未监听 0.0.0.0 导致连接异常

背景

istio 要求应用提供服务时监听 0.0.0.0,因为 127 和 Pod IP 地址都被 envoy 占用了。有些应用启动时没有监听 0.0.0.0:: 的地址,就会导致无法正常通信,参考 Application Bind Address

案例: zookeeper

当 zookeeper 部署到集群中时,默认监听的 Pod IP,会导致 zookeeper 各个实例之间无法正常通信。

解决方案: 在 zk 的配置文件中键入 quorumListenOnAllIPs=true 的配置 ( 参考 istio官方文档 )

istio 1.10

在 istio 1.10 及其以上的版本,应用将无需对端口监听进行特殊处理,即如果应用只监听 eth0 (pod ip) 也能正常使用,详细参考官方博客 Upcoming networking changes in Istio 1.10

VirtualService 路由匹配顺序问题

背景

在写 VirtualService 路由规则时,通常会 match 各种不同路径转发到不同的后端服务,有时候不小心命名冲突了,导致始终只匹配到前面的服务,比如:

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: test
spec:
  gateways:
  - default/example-gw
  hosts:
  - 'test.example.com'
  http:
  - match:
    - uri:
        prefix: /usrv
    rewrite:
      uri: /
    route:
    - destination:
        host: usrv.default.svc.cluster.local
        port:
          number: 80
  - match:
    - uri:
        prefix: /usrv-expand
    rewrite:
      uri: /
    route:
    - destination:
        host: usrv-expand.default.svc.cluster.local
        port:
          number: 80

istio 匹配是按顺序匹配,不像 nginx 那样使用最长前缀匹配。这里使用 prefix 进行匹配,第一个是 /usrv,表示只要访问路径前缀含 /usrv 就会转发到第一个服务,由于第二个匹配路径 /usrv-expand 本身也属于带 /usrv 的前缀,所以永远不会转发到第二个匹配路径的服务。

解决方案

这种情况可以调整下匹配顺序,如果前缀有包含的冲突关系,越长的放在越前面:

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: test
spec:
  gateways:
  - default/example-gw
  hosts:
  - 'test.example.com'
  http:
  - match:
    - uri:
        prefix: /usrv-expand
    rewrite:
      uri: /
    route:
    - destination:
        host: usrv-expand.default.svc.cluster.local
        port:
          number: 80
  - match:
    - uri:
        prefix: /usrv
    rewrite:
      uri: /
    route:
    - destination:
        host: usrv.default.svc.cluster.local
        port:
          number: 80

也可以用正则匹配:

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: test
spec:
  gateways:
  - default/gateway
  hosts:
  - 'test.example.com'
  http:
  - match:
    - uri:
        regex: "/usrv(/.*)?"
    rewrite:
      uri: /
    route:
    - destination:
        host: nginx.default.svc.cluster.local
        port:
          number: 80
        subset: v1
  - match:
    - uri:
        regex: "/usrv-expand(/.*)?"
    rewrite:
      uri: /
    route:
    - destination:
        host: nginx.default.svc.cluster.local
        port:
          number: 80
        subset: v2

headless service 相关问题

服务间通过注册中心调用响应 404

  • 现象: 传统服务 (比如 Srping Cloud) 迁移到 istio 后,服务间调用返回 404。
  • 原因: 没走 Kubernetes 的服务发现,而是通过注册中心获取服务 IP 地址,然后服务间调用不经过域名解析,直接向获取到的目的 IP 发起调用。由于 istio 的 LDS 会拦截 headless service 中包含的 PodIP+Port 的请求,然后匹配请求 hosts,如果没有 hosts 或者 hosts 中没有这个 PodIP+Port 的 service 域名 (比如直接是 Pod IP),就会匹配失败,最后返回 404。
  • 解决方案:
    1. 注册中心不直接注册 Pod IP 地址,注册 service 域名。
    2. 或者客户端请求时带上 hosts (需要改代码)。

负载均衡策略不生效

由于 istio 默认对 headless service 进行 passthrougth,使用 ORIGINAL_DST 转发,即直接转发到原始的目的 IP,不做任何的负载均衡,所以 Destinationrule 中配置的 trafficPolicy.loadBalancer 都不会生效,影响的功能包括:

  • 会话保持 (consistentHash)
  • 地域感知 (localityLbSetting)

解决方案: 单独再创建一个 service (非 headless)

访问不带 sidecar 的 headless service 失败

  • 现象: client (有sidecar) 通过 headless service 访问 server (无sidecar),访问失败,access log 中可以看到 response_flags 为 UF,URX
  • 原因: istio 1.5/1.6 对 headless service 支持有个 bug,不管 endpoint 有没有 sidecar,都固定启用 mtls,导致没有 sidecar 的 headless 服务(如 redis) 访问被拒绝 (详见 #21964) ,更多细节可参考 Istio 运维实战系列(2):让人头大的『无头服务』-上

解决方案一: 配置 DestinationRule 禁用 mtls

kind: DestinationRule
metadata:
  name: redis-disable-mtls
spec:
  host: redis.default.svc.cluster.local
  trafficPolicy:
    tls:
      mode: DISABLE 

解决方案二: 升级 istio 到 1.7 及其以上的版本。

pod 重建后访问失败

  • 现象: client 通过 headless service 访问 server,当 server 的 pod 发生重建后,client 访问 server 失败,access log 中可以看到 response_flags 为 UF,URX
  • 原因: istio 1.5 对 headless service 支持的 bug。
    • client 通过 dns 解析 headless service,返回其中一个 Pod IP,然后发起请求。
    • envoy 检测到是 headless service,使用 ORIGINAL_DST 转发,即不做负载均衡,直接转发到原始的目的 IP。
    • 当 headless service 的 pod 发生重建,由于 client 与它的 sidecar (envoy) 是长连接,所以 client 侧的连接并没有断开。
    • 又由于是长连接,client 继续发请求并不会重新解析 dns,而是仍然发请求给之前解析到的旧 Pod IP。
    • 由于旧 Pod 已经销毁,Envoy 会返回错误 (503)。
    • 客户端并不会因为服务端返回错误而断开连接,后续请求继续发给旧的 Pod IP,如此循环,一直失败。
    • 更多详情参考 Istio 运维实战系列(3):让人头大的『无头服务』-下

解决方案: 升级 istio 到 1.6 及其以上的版本,Envoy 在 Upstream 链接断开后会主动断开和 Downstream 的长链接。

GRPC 服务负载不均

现象

grpc 调用,同一个 client 的请求始终只打到同一个 server 的 pod,造成负载不均。

分析

grpc 是基于 http2 的长连接,多次请求复用同一个连接。如果不用 istio,只用普通的 k8s service,是不会感知 grpc 协议的,只当成 tcp 来转发,在连接层面做负载均衡,不会在请求层面做负载均衡。但在 istio 中,默认会对 grpc 的请求进行请求级别的负载均衡,如果发现负载不均,通常是没有正确配置。 要让 grpc 在请求级别进行负载均衡,核心就是让 istio 正确识别是 grpc 协议,不要配置成 tcp,用 tcp 的话就只能在连接级别进行负载均衡了,请求级别可能就会负载不均。

解决方法

  1. 如果要对外暴露,gateway 里 protocal 配置 GRPC 不用 TCP,示例:
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
  name: grpc-gw
  namespace: demo
spec:
  selector:
    app: istio-ingressgateway
    istio: ingressgateway
  servers:
  - hosts:
    - '*'
    port:
      name: grpc-demo-server
      number: 9000
      protocol: GRPC # 这里使用 GRPC 不用 TCP
  1. 如果定义了 vs,需要使用 http 匹配而不用 tcp,因为 grpc 在 istio 中匹配也是用的 http 字段,示例:
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: grpc-svc
  namespace: demo
spec:
  gateways:
  - demo/grpc-gw
  hosts:
  - '*'
  http: # 这里使用 http 不用 tcp
  - match:
    - port: 9000
    route:
    - destination:
        host: grpc.demo.svc.cluster.local
        port:
          number: 9000
      weight: 100
  1. 部署服务的 service 的 port name 需要使用 "grpc-" 开头定义,让 istio 能够正确识别,示例:
apiVersion: v1
kind: Service
metadata:
  name: grpc
  namespace: demo
spec:
  ports:
  - name: grpc-9000 # 以 grpc- 开头
    port: 9000
    protocol: TCP
    targetPort: 9000
  selector:
    app: grpc
  type: ClusterIP

更多协议指定方式请参考 istio 最佳实践: 为服务显式指定协议

VirutualService 不生效

背景

使用 istio ,在集群中定义了 VirutualService,但测试发现定义的规则似乎没有生效,通常是一些配置问题,本文列举一下常见的可能原因。

集群内访问: gateway 字段没有显式指定 "mesh"

如果 VirutualService 没有指定 gateways 字段,实际隐含了一层意思,istio 会默认加上一个叫 "mesh" 的保留 Gateway,表示集群内部所有 Sidecar,也就表示此 VirutualService 规则针对集群内的访问生效。

但如果指定了 gateways 字段,istio 不会默认加上 "mesh",如:

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: productpage
spec:
  gateways:
  - istio-test/test-gateway
  hosts:
  - bookinfo.example.com
  http:
  - route:
    - destination:
        host: productpage
        port:
          number: 9080

这个表示此 VirualService 规则仅对 istio-test/test-gateway 这个 Gateway 生效,如果是在集群内访问,流量不会经过这个 Gateway,所以此规则也就不会生效。

那如果要同时在集群内也生效该怎么做呢?答案是给 gateways 显式指定上 "mesh":

  gateways:
  - istio-test/test-gateway
  - mesh

表示此 VirutualService 不仅对 istio-test/test-gateway 这个 Gateway 生效,也对集群内部访问生效。

参考 istio 官方文档 对此字段的解释:

值得注意的是,从集群内访问一般是直接访问 service 名称,这里 hosts 就需要加上访问集群内的 service 名称:

hosts:
- bookinfo.example.com
- productpage

通过 ingressgateway 访问: hosts 定义错误

若从 ingressgateway 访问,需要确保 GatewayVirtualService 中的 hosts 均包含实际访问用到的 Host 或使用通配符能匹配得上,通常是外部域名。

只要有一方 hosts 没定义正确,都可能导致 404 Not Found,正确示例:

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: test-gateway
  namespace: istio-test
spec:
  selector:
    app: istio-ingressgateway
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: HTTP-80-www
      protocol: HTTP
    hosts:
    - bookinfo.example.com # 这里定义外部访问域名
  
---

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: productpage
spec:
  gateways:
  - istio-test/test-gateway
  hosts:
  - bookinfo.example.com  # 这里也要定义外部访问域名
  http:
  - route:
    - destination:
        host: productpage
        port:
          number: 9080

Envoy 报错: gRPC config stream closed

gRPC config stream closed: 13

这通常是正常的,因为控制面(istiod)默认每 30 分钟强制断开 xDS 连接,然后数据面(proxy)再自动重连。

gRPC config stream closed: 14

如果只出现一次,通常是在 envoy 启动或重启时报这个错,没什么问题;但如果反复报这个错,可能是数据面(proxy)连接控制面(istiod)有问题,需要排查下。

参考资料

熔断不生效

未定义 http1MaxPendingRequests

我们给 DR 配置了 maxConnections:

apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: nginx
spec:
  host: nginx
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 1

但测试当并发超过这里定义的最大连接数时,并没有触发熔断,只是 QPS 很低。通常是因为没有配置 http1MaxPendingRequests,不配置默认为 2^32-1,非常大,表示如果超过最大连接数,请求就先等待(不直接返回503),当连接数低于最大值时再继续转发。

如果希望连接达到上限或超过上限一定量后或直接熔断(响应503),那么就需要显式指定一下 http1MaxPendingRequests:

apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: nginx
spec:
  host: nginx
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 1
      http:
        http1MaxPendingRequests: 1

地域感知不生效

使用 istio 地域感知能力时,测试发现没生效,本文介绍几个常见原因。

DestinationRule 未配置 outlierDetection

地域感知默认开启,但还需要配置 DestinationRule,且指定 outlierDetection 才可以生效,指定这个配置的作用主要是让 istio 感知 endpoints 是否异常,当前 locality 的 endpoints 发生异常时会 failover 到其它地方的 endpoints。

配置示例:

apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: nginx
spec:
  host: nginx
  trafficPolicy:
    outlierDetection:
      consecutive5xxErrors: 3
      interval: 30s
      baseEjectionTime: 30s

client 没配置 service

istio 控制面会为每个数据面单独下发 EDS,不同数据面实例(Envoy)的locality可能不一样,生成的 EDS 也就可能不一样。istio会获取数据面的locality信息,获取方式主要是找到数据面对应的 endpoint 上保存的 region、zone 等信息,如果 client 没有任何 service,也就不会有 endpoint,控制面也就无法获取 client 的 locality 信息,也就无法实现地域感知。

解决方案: 为 client 配置 service,selector 选中 client 的 label。如果 client 本身不对外提供服务,service 的 ports 也可以随便定义一个。

使用了 headless service

如果是访问 headless service,本身是不支持地域感知的,因为 istio 会对 headless service 请求直接 passthrough,不做负载均衡,客户端会直接访问到 dns 解析出来的 pod ip。

解决方案: 单独再创建一个 service (非 headless)

istio-init crash

问题描述

在 istio 环境下有 pod 处于 Init:CrashLoopBackOff 状态:

wk-sys-acl-v1-0-5-7cf7f79d6c-d9qcr                            0/2     Init:CrashLoopBackOff   283        64d     172.16.9.229    10.1.128.6     <none>           <none>

查得 istio-init 的日志:

Environment:
------------
ENVOY_PORT=
INBOUND_CAPTURE_PORT=
ISTIO_INBOUND_INTERCEPTION_MODE=
ISTIO_INBOUND_TPROXY_MARK=
ISTIO_INBOUND_TPROXY_ROUTE_TABLE=
ISTIO_INBOUND_PORTS=
ISTIO_LOCAL_EXCLUDE_PORTS=
ISTIO_SERVICE_CIDR=
ISTIO_SERVICE_EXCLUDE_CIDR=

Variables:
----------
PROXY_PORT=15001
PROXY_INBOUND_CAPTURE_PORT=15006
PROXY_UID=1337
PROXY_GID=1337
INBOUND_INTERCEPTION_MODE=REDIRECT
INBOUND_TPROXY_MARK=1337
INBOUND_TPROXY_ROUTE_TABLE=133
INBOUND_PORTS_INCLUDE=*
INBOUND_PORTS_EXCLUDE=15090,15021,15020
OUTBOUND_IP_RANGES_INCLUDE=*
OUTBOUND_IP_RANGES_EXCLUDE=
OUTBOUND_PORTS_EXCLUDE=
KUBEVIRT_INTERFACES=
ENABLE_INBOUND_IPV6=false

Writing following contents to rules file:  /tmp/iptables-rules-1618279687646418248.txt617375845
* nat
-N ISTIO_REDIRECT
-N ISTIO_IN_REDIRECT
-N ISTIO_INBOUND
-N ISTIO_OUTPUT
-A ISTIO_REDIRECT -p tcp -j REDIRECT --to-ports 15001
-A ISTIO_IN_REDIRECT -p tcp -j REDIRECT --to-ports 15006
-A PREROUTING -p tcp -j ISTIO_INBOUND
-A ISTIO_INBOUND -p tcp --dport 22 -j RETURN
-A ISTIO_INBOUND -p tcp --dport 15090 -j RETURN
-A ISTIO_INBOUND -p tcp --dport 15021 -j RETURN
-A ISTIO_INBOUND -p tcp --dport 15020 -j RETURN
-A ISTIO_INBOUND -p tcp -j ISTIO_IN_REDIRECT
-A OUTPUT -p tcp -j ISTIO_OUTPUT
-A ISTIO_OUTPUT -o lo -s 127.0.0.6/32 -j RETURN
-A ISTIO_OUTPUT -o lo ! -d 127.0.0.1/32 -m owner --uid-owner 1337 -j ISTIO_IN_REDIRECT
-A ISTIO_OUTPUT -o lo -m owner ! --uid-owner 1337 -j RETURN
-A ISTIO_OUTPUT -m owner --uid-owner 1337 -j RETURN
-A ISTIO_OUTPUT -o lo ! -d 127.0.0.1/32 -m owner --gid-owner 1337 -j ISTIO_IN_REDIRECT
-A ISTIO_OUTPUT -o lo -m owner ! --gid-owner 1337 -j RETURN
-A ISTIO_OUTPUT -m owner --gid-owner 1337 -j RETURN
-A ISTIO_OUTPUT -d 127.0.0.1/32 -j RETURN
-A ISTIO_OUTPUT -j ISTIO_REDIRECT
COMMIT

iptables-restore --noflush /tmp/iptables-rules-1618279687646418248.txt617375845
iptables-restore: line 2 failed
iptables-save
# Generated by iptables-save v1.6.1 on Tue Apr 13 02:08:07 2021
*nat
:PREROUTING ACCEPT [5214353:312861180]
:INPUT ACCEPT [5214353:312861180]
:OUTPUT ACCEPT [6203044:504329953]
:POSTROUTING ACCEPT [6203087:504332485]
:ISTIO_INBOUND - [0:0]
:ISTIO_IN_REDIRECT - [0:0]
:ISTIO_OUTPUT - [0:0]
:ISTIO_REDIRECT - [0:0]
-A PREROUTING -p tcp -j ISTIO_INBOUND
-A OUTPUT -p tcp -j ISTIO_OUTPUT
-A ISTIO_INBOUND -p tcp -m tcp --dport 22 -j RETURN
-A ISTIO_INBOUND -p tcp -m tcp --dport 15090 -j RETURN
-A ISTIO_INBOUND -p tcp -m tcp --dport 15021 -j RETURN
-A ISTIO_INBOUND -p tcp -m tcp --dport 15020 -j RETURN
-A ISTIO_INBOUND -p tcp -j ISTIO_IN_REDIRECT
-A ISTIO_IN_REDIRECT -p tcp -j REDIRECT --to-ports 15006
-A ISTIO_OUTPUT -s 127.0.0.6/32 -o lo -j RETURN
-A ISTIO_OUTPUT ! -d 127.0.0.1/32 -o lo -m owner --uid-owner 1337 -j ISTIO_IN_REDIRECT
-A ISTIO_OUTPUT -o lo -m owner ! --uid-owner 1337 -j RETURN
-A ISTIO_OUTPUT -m owner --uid-owner 1337 -j RETURN
-A ISTIO_OUTPUT ! -d 127.0.0.1/32 -o lo -m owner --gid-owner 1337 -j ISTIO_IN_REDIRECT
-A ISTIO_OUTPUT -o lo -m owner ! --gid-owner 1337 -j RETURN
-A ISTIO_OUTPUT -m owner --gid-owner 1337 -j RETURN
-A ISTIO_OUTPUT -d 127.0.0.1/32 -j RETURN
-A ISTIO_OUTPUT -j ISTIO_REDIRECT
-A ISTIO_REDIRECT -p tcp -j REDIRECT --to-ports 15001
COMMIT
# Completed on Tue Apr 13 02:08:07 2021
panic: exit status 1

goroutine 1 [running]:
istio.io/istio/tools/istio-iptables/pkg/dependencies.(*RealDependencies).RunOrFail(0x3bb0090, 0x22cfd22, 0x10, 0xc0006849c0, 0x2, 0x2)
	istio.io/istio/tools/istio-iptables/pkg/dependencies/implementation.go:44 +0x96
istio.io/istio/tools/istio-iptables/pkg/cmd.(*IptablesConfigurator).executeIptablesRestoreCommand(0xc0009dfd68, 0x22c5a01, 0x0, 0x0)
	istio.io/istio/tools/istio-iptables/pkg/cmd/run.go:493 +0x387
istio.io/istio/tools/istio-iptables/pkg/cmd.(*IptablesConfigurator).executeCommands(0xc0009dfd68)
	istio.io/istio/tools/istio-iptables/pkg/cmd/run.go:500 +0x45
istio.io/istio/tools/istio-iptables/pkg/cmd.(*IptablesConfigurator).run(0xc0009dfd68)
	istio.io/istio/tools/istio-iptables/pkg/cmd/run.go:447 +0x2625
istio.io/istio/tools/istio-iptables/pkg/cmd.glob..func1(0x3b5d680, 0xc0004cce00, 0x0, 0x10)
	istio.io/istio/tools/istio-iptables/pkg/cmd/root.go:64 +0x148
github.com/spf13/cobra.(*Command).execute(0x3b5d680, 0xc0004ccd00, 0x10, 0x10, 0x3b5d680, 0xc0004ccd00)
	github.com/spf13/cobra@v1.0.0/command.go:846 +0x29d
github.com/spf13/cobra.(*Command).ExecuteC(0x3b5d920, 0x0, 0x0, 0x0)
	github.com/spf13/cobra@v1.0.0/command.go:950 +0x349
github.com/spf13/cobra.(*Command).Execute(...)
	github.com/spf13/cobra@v1.0.0/command.go:887
main.main()
	istio.io/istio/pilot/cmd/pilot-agent/main.go:505 +0x2d

原因与解决方案

跟这个 issue 基本一致 https://github.com/istio/istio/issues/24148

直接原因: 这种情况应该通常是清理了已退出的 istio-init 容器,导致 k8s 检测到 pod 关联的容器不在了,然后会重新拉起被删除的容器,而 istio-init 的执行不可重入,因为之前已创建了 iptables 规则,导致后拉起的 istio-init 执行 iptables 失败而 crash。

根因与解决方案: 清理的动作通常是执行了 docker container rmdocker container prunedocker system prune。 一般是 crontab 定时脚本里定时清理了容器导致,需要停止清理。

状态码: 431 Request Header Fields Too Large

问题描述

istio 中 http 请求,envoy 返回 431 异常状态码:

HTTP/1.1 431 Request Header Fields Too Large

原因分析

此状态码说明 http 请求 header 大小超限了,默认限制为 60 KiB,由 HttpConnectionManager 配置的 max_request_headers_kb 字段决定,最大可调整到 96 KiB:

解决方案

可以通过 EnvoyFilter 调整 max_request_headers_kb 字段来提升 header 大小限制。

EnvoyFilter 示例 (istio 1.6 验证通过):

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: max-header
  namespace: istio-system
spec:
  configPatches:
  - applyTo: NETWORK_FILTER
    match:
      context: ANY
      listener:
        filterChain:
          filter:
            name: "envoy.http_connection_manager"
    patch:
      operation: MERGE
      value:
        typed_config:
          "@type": "type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager"
          max_request_headers_kb: 96

高版本兼容上面的 v2 配置,但建议用 v3 的 配置 (istio 1.8 验证通过):

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: max-header
  namespace: istio-system
spec:
  configPatches:
  - applyTo: NETWORK_FILTER
    match:
      context: ANY
      listener:
        filterChain:
          filter:
            name: "envoy.http_connection_manager"
    patch:
      operation: MERGE
      value:
        typed_config:
          "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager"
          max_request_headers_kb: 96

若 header 大小超过 96 KiB,这种情况本身也很不正常,建议将这部分数据放到 body。

状态码: 426 Upgrade Required

背景

Istio 使用 Envoy 作为数据面转发 HTTP 请求,而 Envoy 默认要求使用 HTTP/1.1 或 HTTP/2,当客户端使用 HTTP/1.0 时就会返回 426 Upgrade Required

常见的 nginx 场景

如果用 nginx 进行 proxy_pass 反向代理,默认会用 HTTP/1.0,你可以显示指定 proxy_http_version1.1:

upstream http_backend {
    server 127.0.0.1:8080;

    keepalive 16;
}

server {
    ...

    location /http/ {
        proxy_pass http://http_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        ...
    }
}

压测场景

ab 压测时会发送 HTTP/1.0 的请求,Envoy 固定返回 426 Upgrade Required,根本不会进行转发,所以压测的结果也不会准确。可以换成其它压测工具,如 wrk

让 istio 支持 HTTP/1.0

有些 SDK 或框架可能会使用 HTTP/1.0 协议,比如使用 HTTP/1.0 去资源中心/配置中心 拉取配置信息,在不想改动代码的情况下让服务跑在 istio 上,也可以修改 istiod 配置,加上 PILOT_HTTP10: 1 的环境变量来启用 HTTP/1.0。

参考资料

状态码: 404 Not Found

访问 StatefulSet Pod IP 返回 404

  • 问题描述:在 istio 中业务容器访问同集群一 Pod IP 返回 404,在 istio-proxy 中访问却正常
  • 原因: Pod 属于 StatefulSet,使用 headless svc,在 istio 中对 headless svc 的支持跟普通 svc 不太一样,如果 pod 用的普通 svc,对应的 listener 有兜底的 passthrough,即转发到报文对应的真实目的IP+Port,但 headless svc 的就没有,我们理解是因为 headless svc 没有 vip,它的路由是确定的,只指向后端固定的 pod,如果路由匹配不上就肯定出了问题,如果也用 passthrough 兜底路由,只会掩盖问题,所以就没有为 headless svc 创建 passthrough 兜底路由。同样的业务,上了 istio 才会有这个问题,也算是 istio 的设计或实现问题。
  • 示例场景: 使用了自己的服务发现,业务直接使用 Pod IP 调用 StatefulSet 的 Pod IP
  • 解决方案: 同集群访问 statefulset pod ip 带上 host,以匹配上 headless svc 路由,避免匹配不到就 4

排障案例

本节分享一些排障案例。

使用 apollo 的 java 应用启动报 404

问题描述

项目中使用了 apollo 插件,在非 istio 环境正常运行,但部署到 istio 后启动报类似如下错误:

Sync config from upstream repository class com.ctrip.framework.apollo.internals.RemoteConfigRepository failed, reason: Load Apollo Config failed - xxx, url: http://10.5.16.49:8080/configs/agent-center/see-test-02/test.common?ip=10.5.16.46 [Cause: [status code: 404] Could not find config for xxx please check whether the configs are released in Apollo!]

表示请求 apollo 的 config service 返回 404 了。

排查 accesslog

查看 envoy 的 accesslog:

  • response_flagsNR,表示找不到路由 (参考 envoy 官网解释: No route configured for a given request in addition to 404 response code, or no matching filter chain for a downstream connection)。
  • 请求使用的 Host 直接用的 PodIP:Port
  • PodIP:Port 属于 apollo 服务的 headless service 一个 endpoint (apollo 通过 statefulset 部署)。

headless service 的 xDS 规则

进一步分析之前,我们先了解下 istio 对 headless service 的 xDS 支持:

  • 下发的 LDS 规则中会监听 headless service 所有可能的 PortIP:Port,请求 headless service 的 Pod 时,这里先匹配上。
  • 然后走到 RDS 规则进行路由,路由时会匹配 hosts,hosts 列表中列举了所有可能的 service 地址 (没有 Pod IP),如果都匹配不到就会返回 404。

问题原因

由于请求 apollo 的 config service 时,Host 没有使用 service 地址,而是直接使用了 PodIP:Port,所以 RDS 匹配时找不到相应 hosts,就会返回 404。

为什么没有使用 service 地址 ?

为了实现高可用,apollo 的 java 客户端默认是从 meta server 中获取 config service 的 ip 地址 (服务发现),然后直接对该地址发起请求 (不使用 k8s service),从而导致请求 config service 时没有将其 k8s service 地址作为 Host,最后 hosts 匹配不到返回 404。

如果解决 ?

在 istio 场景下 (kubernetes 之上),请求 config service 就不需要不走 apollo meta server 获取 config service 的 ip 来实现高可用,直接用 kubernetes 的 service 做服务发现就行。幸运的是,apollo 也支持跳过 meta server 服务发现,这样访问 config service 时就可以直接请求 k8s service 了,也就可以解决此问题。

具体配置方法参考 Apollo Java 客户端使用指南

无法访问不带 sidecar 的 Pod

问题现象

不能从一个带 sidecar proxy 的 pod 访问到 Redis 服务。

问题分析

Redis是一个 Headless 服务,而 istio 1.6 之前的版本对 Headless 服务的处理有问题,会缺省启用 mTLS。

解决方案

在 1.6 之前可以采用DR规则禁用该服务的 mTLS 来规避:

apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: mysql-service
  namespace: test
spec:
  host: redis-service
  trafficPolicy:
    loadBalancer:
      simple: ROUND_ROBIN
    tls:
      mode: DISABLE

该问题在 isitio 1.6 中已经修复: https://github.com/istio/istio/pull/24319

注入 sidecar 后 grpc 请求不响应 status code

问题描述

  • 环境信息: 多集群下,子集群中 nginx 为 grpc 服务做了一层反向代理。
  • 访问链路:grpc client --> nginx --> grpc server。
  • 链路说明: grpc client 对 nginx 发起 grpc 调用,nginx 将请求 proxy_pass 到集群内的 grpc server 的 k8s service。
  • 现象: 当 nginx 注入了 istio sidecar 后,grpc client 就收不到 grpc server 响应的 grpc-status 的 trailer 了 (即没有 grpc status code)。

原因

测试在 istio 1.6.9 中存在一个 bug,在多集群下,子集群中 envoy sidecar 内部的 http route 中的 domain 中的 vip 采用的是主集群中的 Cluster IP,而不是子集群中该服务对应的 Cluster IP。 如下面 nginx pod 中 envoy proxy 的路由配置:

{
  "name": "location-svr.default.svc.cluster.local:50222",
  "domains": [
  "location-svr.default.svc.cluster.local",
  "location-svr.default.svc.cluster.local:50222",
  "location-svr",
  "location-svr:50222",
  "location-svr.default.svc.cluster",
  "location-svr.default.svc.cluster:50222",
  "location-svr.default.svc",
  "location-svr.default.svc:50222",
  "location-svr.default",
  "location-svr.default:50222",
  "172.21.255.24",              # 此处的 VIP 是主机群中的 cluster ip,而不是子集群中的 cluster ip
  "172.21.255.24:50222"
],
"routes": [
  ...
  • grpc client 发起请求时带上的 host 是 nginx 的域名地址,被 nginx proxy_pass 时带上了,但 envoy 是匹配不到这个原始 host 的,就尝试去匹配报文目的 IP (即 cluster ip)。
  • 但因为上述 bug,cluster ip 也匹配不到 (istio 1.6.9 子集群中 http route 只有主集群同名 service 的 cluster ip,这里目的 IP 可能只能是子集群自己的 cluster ip),就只有 paasthrough 了。
  • 由于 passthrough cluster 并不确认后端的 upstream 支持 http2,因此未设置 http2_protocol_options 选项。
  • 该 grpc/http2 请求被 enovy 采用 http1.1 发送到 location-svr,导致未能收到 trailer (grpc status code)。

解决方案

有以下三种解决方案:

  1. 经验证 1.8 中该 bug 已经处理,升级到 1.8 可以解决该问题。
  2. nginx proxy_pass 显示指定 proxy_set_header,使用 service 名称作为 Host。
  3. grpc client 请求时显示设置 Host Header 为 grpc server 的 service 名称。

Pod 启动卡住: MountVolume.SetUp failed for volume "istio-token"

现象

Istio 相关的 Pod (包括注入了 sidecar 的 Pod) 一直卡在 ContainerCreating,起不来,describe pod 报错 MountVolume.SetUp failed for volume "istio-token" : failed to fetch token: the server could not find the requested resource:

分析

根据官方文档(Configure third party service account tokens) 的描述可以得知:

  • istio-proxy 需要使用 K8S 的 ServiceAccount token,而 K8S 支持 third partyfirst party 两种 token。
  • third party token 安全性更高,istio 默认使用这种类型。
  • 不是所有集群都支持这种 token,取决于 K8S 版本和 apiserver 配置。

如果集群不支持 third party token,就会导致 ServiceAccount token 不自动创建出来,从而出现上面这种报错。

什么是 third party token ?

其实就是 ServiceAccountTokenVolumeProjection 这个特性,在 1.12 beta,1.20 GA。

推出该特性是为了增强 ServiceAccount token 的安全性,可以设置有效期(会自动轮转),避免 token 泄露带来的安全风险,还可以控制 token 的受众。

该特性在 istio 中用来配合 SDS 以增强安全性,参考 Istio私钥管理利器SDS浅析

如何判断集群是否启用了该特性呢?可通过一下命令查询:

kubectl get --raw /api/v1 | jq '.resources[] | select(.name | index("serviceaccounts/token"))'

若返回空,说明不支持;若返回如下 json,说明支持:

{
    "name": "serviceaccounts/token",
    "singularName": "",
    "namespaced": true,
    "group": "authentication.k8s.io",
    "version": "v1",
    "kind": "TokenRequest",
    "verbs": [
        "create"
    ]
}

解决方案

方案一:安装 istio 时不使用 third party token

官方称使用 istioctl 安装会自动检测集群是否支持 third party token,但据 issue 反馈可能有 bug,还是建议强制指定用 first party token,用参数 --set values.global.jwtPolicy=first-party-jwt 来显示指定,示例:

istioctl manifest generate  --set profile=demo  --set values.global.jwtPolicy=first-party-jwtm > istio.yaml

方案二:集群启用 ServiceAccountTokenVolumeProjection

如何启用 ServiceAccountTokenVolumeProjection 这个特性呢?需要给 apiserver 配置类似如下的参数:

--service-account-key-file=/etc/kubernetes/pki/sa.key # 这个一般都会配,重要的是下面三个参数
--service-account-issuer=kubernetes.default.svc
--service-account-signing-key-file=/etc/kubernetes/pki/sa.key # 注意替换实际路径
--api-audiences=kubernetes.default.svc

trafficPolicy 不生效

问题描述

为服务配置了 DestinationRule 和 VirtualService,且 VirtualService 绑好了 Gateway,DestinationRule 配置了 trafficPolicy,指定了熔断策略,但使用 ab 压测发现没有触发熔断 (ingressgateway 的 access_log 中 response_flags 没有 "UO")。

原因分析

ab 压测时发送的 HTTP/1.0 请求,而 envoy 需要至少 HTTP/1.1,固定返回 426 Upgrade Required,根本不会进行转发,所以也就不会返回 503,response_flags 也不会有。

解决方案

压测工具换用 wrk,默认发送 HTTP/1.1 的请求,可以正常触发熔断。

使用 istio 保留端口导致 pod 启动失败

问题现象

所有新启动的 Pod 无法 ready,sidecar 报错:

warning	envoy config	gRPC config for type.googleapis.com/envoy.config.listener.v3.Listener rejected: Error adding/updating listener(s) 0.0.0.0_15090: error adding listener: '0.0.0.0_15090' has duplicate address '0.0.0.0:15090' as existing listener

同时 istiod 也报错:

ADS:LDS: ACK ERROR sidecar~172.18.0.185~reviews-v1-7d46f9dd-w5k8q.istio-test~istio-test.svc.cluster.local-20847 Internal:Error adding/updating listener(s) 0.0.0.0_15090: error adding listener: '0.0.0.0_15090' has duplicate address '0.0.0.0:15090' as existing listener

猜想

看报错应该是 sidecar 启动时获取 LDS 规则,istiod 发现 0.0.0.0:15090 这个监听重复了,属于异常现象,下发 xDS 规则就会失败,导致 sidecar 一直无法 ready。

分析 config_dump

随便找一个还未重启的正常 Pod,看一下 envoy config_dump:

kubectl exec debug-68b799694-n9q66 -c istio-proxy -- curl localhost:15000/config_dump

分析 json 发现 static 配置中有监听 0.0.0.0:15090:

定位原因

猜测是 dynamic 配置中也有 0.0.0.0:15090 的监听导致的冲突,而 dynamic 中监听来源通常是 Kubernetes 的服务发现(Service, ServiceEntry),检查一下是否有 Service 监听 15090:

kubectl get service --all-namespaces -o yaml | grep 15090

最终发现确实有 Service 用到了 15090 端口,更改成其它端口即可恢复。

深入挖掘

搜索一下,可以发现 15090 端口是 istio 用于暴露 envoy prometheus 指标的端口,是 envoy 使用的端口之一:

参考 Ports used by Istio

但并不是所有 envoy 使用的端口都被加入到 static 配置中的监听,只有 15090 和 15021 这两个端口在 static 配置中有监听,也验证了 Service 使用 15021 端口也会有相同的问题。

Service 使用其它 envoy 的端口不会造成 sidecar 不 ready 的问题,但至少要保证业务程序也不能去监听这些端口,因为会跟 envoy 冲突,istio 官网也说明了这一点: To avoid port conflicts with sidecars, applications should not use any of the ports used by Envoy

使用建议

根据上面分析,得出以下使用建议:

  1. Service/ServiceEntry 不能定义 15090 和 15021 端口,不然会导致 Pod 无法启动成功。
  2. 业务进程不能监听 envoy 使用到的所有端口: 15000, 15001, 15006, 15008, 15020, 15021, 15090 。

最新进展

当前阶段

当前处于试验阶段,预计2022年底或2023年初 beta。

代码分支

当前 ambient 模式的代码还没有合入主干,在 experimental-ambient 分支。这个 issue 在跟进合入 master 之前需要完成的重要事项。

已知问题

环境适配问题

当前还处于早期阶段,很多环境都不支持,比如 mac m1 电脑上使用 kind 创建的集群、使用了网桥的网络模式的集群、某些使用策略路由实现的容器网络等等。

ztunnel 问题

当前 ztunnel 使用 envoy 实现,存在一些列问题,社区也在考虑替代方案,改进或者用 Rust 写一个,详见 这个 issue

其它问题

1 更多 ambient 相关 issue 看 这里

参考资料

编译与测试

基于最新代码编译并 push 镜像

下载最新代码:

git clone http://github.com/istio/istio.git

切换分支:

cd istio
git checkout -b experimental-ambient origin/experimental-ambient

编译所有镜像:

export HUB="registry.imroc.cc/istio"
export TAG="ambient"
make docker

查看镜像列表:

$ docker images | grep ambient
registry.imroc.cc/istio/install-cni               ambient                                                  d3d8fa9fff24   2 days ago      307MB
registry.imroc.cc/istio/proxyv2                   ambient                                                  94ac94a14ed6   2 days ago      277MB
registry.imroc.cc/istio/istioctl                  ambient                                                  76fea2b66ed7   2 days ago      190MB
registry.imroc.cc/istio/operator                  ambient                                                  574faf14c66b   2 days ago      191MB
registry.imroc.cc/istio/app                       ambient                                                  7c648c702595   2 days ago      188MB
registry.imroc.cc/istio/pilot                     ambient                                                  d914093f7809   2 days ago      189MB
registry.imroc.cc/istio/ext-authz                 ambient                                                  88dc93477b75   2 days ago      112MB

最后再使用 docker push 上传镜像。

实际上测试 ambient 是需要其中几个,可以用下面命令只编译需要的镜像:

make docker.pilot
make docker.install-cni
make docker.proxyv2
make docker.istioctl

从镜像拷贝出 istioctl 二进制

下面介绍将 istioctl 二进制拷贝出来的方法,首先用 istioctl 镜像运行一个容器:

docker run --rm -it --entrypoint="" --name istioctl registry.imroc.cc/istio/istioctl:ambient bash

再利用 docker cp 将二进制拷贝出来:

docker cp istioctl:/usr/local/bin/istioctl ./istioctl

使用 istioctl 安装 ambient mesh

./istioctl install --set profile=ambient --set hub=registry.imroc.cc/istio --set tag=ambient

深入分析实现原理

出方向流量分析

流量拦截原理

流量路径:

iptables 在 PREROUTING 链上匹配源 IP 是本节点中网格内的 Pod IP 的 TCP 数据包,打上 0x100 的 mark:

-A ztunnel-PREROUTING -p tcp -m set --match-set ztunnel-pods-ips src -j MARK --set-xmark 0x100/0x100

用 ipset 可以看到 ztunnel-pods-ips 正是本节点中网格内的 POD IP 列表:

$ ipset list
Name: ztunnel-pods-ips
Type: hash:ip
Revision: 0
Header: family inet hashsize 1024 maxelem 65536
Size in memory: 440
References: 1
Number of entries: 2
Members:
10.244.1.4
10.244.1.5

在策略路由可以看到,打上 0x100 mark 的数据包,会走 101 号路由表进行路由:

$ ip rule list
101:	from all fwmark 0x100/0x100 lookup 101

查看路由,会经过 istioout 网卡,使用 192.168.127.2 这个默认网关进行路由:

$ ip route show table 101
default via 192.168.127.2 dev istioout
10.244.1.3 dev vethf18f80e0 scope link

查看 istioout 网卡,是 geneve 类型的虚拟网卡,remote10.244.1.3:

$ ip -d a s istioout
5: istioout: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN group default
    link/ether 26:04:92:96:1b:67 brd ff:ff:ff:ff:ff:ff promiscuity 0 minmtu 68 maxmtu 65485
    geneve id 1001 remote 10.244.1.3 ttl auto dstport 6081 noudpcsum udp6zerocsumrx numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    inet 192.168.127.1/30 brd 192.168.127.3 scope global istioout
       valid_lft forever preferred_lft forever

10.244.1.3 是本节点上的 ztunnel pod ip:

$ kubectl -n istio-system get pod -o wide | grep 10.244.1.3
ztunnel-27nxh                           1/1     Running   0          3d3h   10.244.1.3   ambient-worker          <none>           <none>

前面提到的默认网关 192.168.127.2 也正是 ztunnel 内的 pistioout 网卡的 IP:

$ kubectl -n istio-system exec -it ztunnel-27nxh -- ip -d a s
4: pistioout: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN group default qlen 1000
    link/ether 76:b0:37:d7:16:93 brd ff:ff:ff:ff:ff:ff promiscuity 0 minmtu 68 maxmtu 65485
    geneve id 1001 remote 10.244.1.1 ttl auto dstport 6081 noudpcsum udp6zerocsumrx numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    inet 192.168.127.2/30 scope global pistioout
       valid_lft forever preferred_lft forever
    inet6 fe80::74b0:37ff:fed7:1693/64 scope link
       valid_lft forever preferred_lft forever

也就是说,节点内的 istioout 和 ztunnel pod 内的 pistioout 网卡通过 geneve tunnel 打通了,istioout 收到数据包后立即会转到 ztunnel 内的 pistioout 网卡。

所以串起来就是,从本节点网格内的 Pod 中发出的 TCP 流量,会被策略路由转发到 ztunnel pod 内的 pistioout 网卡。

而在 ztunnel 内的 pistioout 网卡收到流量后,会被 iptables 通过 tproxy 方式转发到 ztunnel 的 15001 端口上:

$ kubectl -n istio-system exec -it ztunnel-27nxh -- iptables-save | grep pistioout
-A PREROUTING -i pistioout -p tcp -j TPROXY --on-port 15001 --on-ip 127.0.0.1 --tproxy-mark 0x400/0xfff

ztunnel 接收与转发流量实现原理

ztunnel 使用 envoy 实现,导出 xds:

kubectl -n istio-system exec -it ztunnel-gm66l -- curl '127.0.0.1:15000/config_dump?include_eds' > dump.json

下面分析下处理拦截到的出方向流量的 xds 配置规则。

LDS 中监听 15001 端口,用于处理 tproxy 方式拦截到的用户 Pod 出方向流量("trasparent": true 指示 envoy 监听时使用 IP_TRANSPARENT socket 选项以便让目的 IP 不是 ztunnel 网卡 IP 的数据包也能让 ztunnel 收到,实现 tproxy 透明代理) ,非 tproxy 拦截的流量就直接丢弃:

15001 端口监听有很多 filter_chains,具体用哪个 filter,由 filter_chain_matcher 来匹配:

  • SourceIPInput 匹配源 IP,看来自节点上哪个 Pod IP。
  • DestinationIPInput 匹配目标 IP。
  • 匹配到某个 Service 的 ClusterIP,继续用 DestinationPortInput 匹配 Service 端口。
  • 匹配完成后,action 指示要使用的 filter 名称。

找到对应的 filter,指示转发给指定的 Cluster:

在 CDS 中找到对应的 Cluster:

在 EDS 中找 Cluster 对应的 endpoint:

这里应该是 envoy dump xds 的 bug,EDS 中没展示 cluster_name 这个必选字段,可能是由于 HBONE 比较新,dump 的逻辑还不完善。

  • endpoint 的 address 是 envoy_internal_address (internal_listener)。
  • filter_metadata 中的 tunnel.destination 是关键,表示给这个 endpoint 带上了要访问的实际目标IP+目标端口,后面会将其传给 HBONE 隧道。

在 LDS 中找到对应的 server_listener_name:

  • 该 listener 有 tunneling_config,即使用 HBONE 隧道方式,其中 hostname 为应用需要访问的实际目标IP和目标端口,引用 endpoint 中的 metadata tunnel.destination
  • cluster 指定转发到哪个 Cluster。

再去 CDS 中找到对应的 Cluster:

  • 目标地址已经被前面的 EDS 修改成了 Service 对应的 POD IP 和 POD 端口了,CDS 的 type 为 ORIGINAL_DSTupstream_port_override 强制将目的端口改为 15008,表示使用 POD_IP:15008 这个地址作为报文的目标地址,也就是 HBONE 隧道上游 server 端地址。
  • tls_certificates_sds_secret_configs 中指定连接上游要使用的证书,指定为目标 Pod 所使用的 service account 对应的证书,这也是在 L4 实现零信任网络的关键。

入方向流量分析

流量拦截原理

流量路径:

由前面出方向流量路径的分析可以得知,ztuunel 最终转发出来的目的IP和目的端口分别是目标 POD IP和固定的 15008 端口。

看下策略路由:

$ ip rule list
103:	from all lookup 100
32766:	from all lookup main

有个 100 号的路由表,优先级比默认的 main 表高。看下路由规则:

$ ip route show table 100
10.244.1.3 dev vethf18f80e0 scope link
10.244.1.4 via 192.168.126.2 dev istioin src 10.244.1.1
10.244.1.5 via 192.168.126.2 dev istioin src 10.244.1.1

可以看出会给本机上所有的网格内的 POD IP 都加一条路由规则,让到网格内 POD 的流量都走 istioin 这个网卡进行路由,网关是 192.168.126.2

$ ip -d a s istioin
4: istioin: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN group default
    link/ether 3a:e0:ed:06:15:8c brd ff:ff:ff:ff:ff:ff promiscuity 0 minmtu 68 maxmtu 65485
    geneve id 1000 remote 10.244.1.3 ttl auto dstport 6081 noudpcsum udp6zerocsumrx numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    inet 192.168.126.1/30 brd 192.168.126.3 scope global istioin
       valid_lft forever preferred_lft forever

与前面出向流量走的 istioout 网卡类似,入向流量走的 istioin 网卡也是 geneve tunnel 设备,remote 是 10.244.1.3,即 ztunnel 的 POD IP,而网关 192.168.126.2 也正是 ztunnel 内的 pistioin 网卡:

$ kubectl -n istio-system exec -it ztunnel-27nxh -- ip -d a s pistioin
3: pistioin: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN group default qlen 1000
    link/ether 7e:bb:b7:60:f3:f6 brd ff:ff:ff:ff:ff:ff promiscuity 0 minmtu 68 maxmtu 65485
    geneve id 1000 remote 10.244.1.1 ttl auto dstport 6081 noudpcsum udp6zerocsumrx numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    inet 192.168.126.2/30 scope global pistioin
       valid_lft forever preferred_lft forever
    inet6 fe80::7cbb:b7ff:fe60:f3f6/64 scope link
       valid_lft forever preferred_lft forever

也就是说,发送到节点上网格内的 POD 的流量,会被自动转到 ztunnel 内的 pistioin 网卡。

看 ztunnel 内的 iptables 规则,会将 pistioin 上目的端口为 15008 的 TCP 流量,通过 tproxy 方式拦截并转发给 ztunnel 监听的 15008 端口。

$ kubectl -n istio-system exec -it ztunnel-27nxh -- iptables-save | grep pistioin
-A PREROUTING -i pistioin -p tcp -m tcp --dport 15008 -j TPROXY --on-port 15008 --on-ip 127.0.0.1 --tproxy-mark 0x400/0xfff

ztunnel 接收与转发流量实现原理

查看 LDS 中 15008 端口的 Listener:

  • filter_chains 中有很多 filter,每个 filter 对应匹配本节点上,网格内的 POD IP。
  • 在 filter 的 route 中,将数据包路由到 virtual_inbound 这个 Cluster。

在 CDS 中找到 virtual_inbound 这个 Cluster:

  • use_http_header 表示将 downstream ztunnel 通过 HBONE CONNECT 传来的最原始的目标 POD IP 和目标端口替换成当前报文的目标IP和目标端口 (主要是为了替换 15008 端口为原始目标端口,目标 IP 本身都是目标 POD IP)。

最后,报文将只修改目的端口,其余均保持不变,再次被 envoy 转发出去,通过 veth pair 到达节点 root ns 的 vethxxx 网卡,由于目的端口不再是

项目结构解析

顶层目录

├── cni
│   ├── cmd
│   ├── pkg
├── istioctl
│   ├── cmd
│   └── pkg
├── operator
│   ├── cmd
│   ├── pkg
├── pilot
│   ├── cmd
│   ├── pkg
├── pkg
  • cni, istioctl, operator, pilot 目录分别包含同名相应模块的代码。下面的 cmd 是模块下相应二进制的编译入口,cmd 下面的 pkgcmd 中的代码需要调用的依赖逻辑。
  • 多个模块共同依赖的一些逻辑会放到外层的 pkg 目录下。

梳理模块与二进制

cni 模块主要包含 istio-cniinstall-cni 两个二进制,负责 cni 插件相关逻辑:

cni
├── cmd
│   ├── install-cni
│   ├── istio-cni

istioctloperator 模块都主要是一个二进制,分别用于 cli 工具和 istio 安装。

pilot 是最核心的模块,有 pilot-agentpilot-discovery 两个二进制:

pilot
├── cmd
│   ├── pilot-agent
│   └── pilot-discovery
  • pilot-discovery 就是 "istiod",即 istio 控制面。
  • pilot-agent 是连接 istiod (控制面) 和 envoy (数据面) 之间的纽带,主要负责拉起和管理数据面进程。

CNI 模块源码解析

install-cni

主要逻辑梳理

install-cni 用于 daemonset 部署到每个节点,主要逻辑如下:

  • 为节点安装 istio-cni 插件。
  • 检测 cni 配置,如果检测到被修改,立即覆盖回来。
  • 如果是 ambient 模式,会起一个 controller 来 watch pod,当本节点 ztunnel pod 起来时,会自动在节点上创建 ambient 模式中节点上所需的 iptables 规则、虚拟网卡以及策略路由。
  • 退出时清理 istio-cni 插件。

main 函数入口在 cni/cmd/install-cni/main.gocmd.GetCommand() 获取 cobra 的 Command,也就是二进制运行入口:

rootCmd := cmd.GetCommand()
if err := rootCmd.ExecuteContext(ctx); err != nil {
	os.Exit(1)
}

rootCmd.ExecuteContext(ctx) 会调用 cobra Command 的 RunE 函数 (cni/pkg/cmd/root.go):

var rootCmd = &cobra.Command{
	RunE: func(c *cobra.Command, args []string) (err error) {
...

继续看看 RunE 里面的逻辑,constructConfig() 会从启动参数和环境变量中读取参数,进行合并,构造出 install-cni 的配置信息 Config 对象:

if cfg, err = constructConfig(); err != nil {
	return
}

根据配置的端口,将指标监控信息暴露到 /metrics 接口:

		// Start metrics server
		monitoring.SetupMonitoring(cfg.InstallConfig.MonitoringPort, "/metrics", ctx.Done())

监听 Unix Domain Socket,暴露 HTTP 接口,用于接收来自 istio-cni 产生的日志,收到日志后就会在 install-cni 这里打印出来:

// Start UDS log server
udsLogger := udsLog.NewUDSLogger()
if err = udsLogger.StartUDSLogServer(cfg.InstallConfig.LogUDSAddress, ctx.Done()); err != nil {
	log.Errorf("Failed to start up UDS Log Server: %v", err)
	return
}

因为 istio-cni 自身不是常驻进程,被 kubelet 调用后立即退出,所以需要将日志投递给 install-cni 统一打印和记录,便于排查问题。

如果启用了 ambient 模式,会启动一个 ambient 模式所需要的常驻运行的 server:

if cfg.InstallConfig.AmbientEnabled {
	// Start ambient controller
	server, err := ambient.NewServer(ctx, ambient.AmbientArgs{
		SystemNamespace: ambient.PodNamespace,
		Revision:        ambient.Revision,
	})
	if err != nil {
		return fmt.Errorf("failed to create ambient informer service: %v", err)
	}
	server.Start()
}

接下来是最核心最关键的逻辑,根据安装配置将 istio-cni 插件安装到节点上,同时 watch 文件变化,如果被修改就自动覆盖回来:

installer := install.NewInstaller(&cfg.InstallConfig, isReady)

repair.StartRepair(ctx, &cfg.RepairConfig)

if err = installer.Run(ctx); err != nil {
  ...

安装 CNI 插件的逻辑梳理

install.Run(ctx) 是安装 CNI 逻辑的入口,内部 in.install(ctx) 是每次安装 CNI 插件时执行的逻辑:

if in.cfg.CNIEnableInstall {
	if err = in.install(ctx); err != nil {
		return
	}
    ...

install 中,先将 istio CNI 插件的二进制拷贝到节点的 CNI 二进制目录 (/opt/cni/bin):

if err = copyBinaries(
	in.cfg.CNIBinSourceDir, in.cfg.CNIBinTargetDirs,
	in.cfg.UpdateCNIBinaries, in.cfg.SkipCNIBinaries); err != nil {
	cniInstalls.With(resultLabel.Value(resultCopyBinariesFailure)).Increment()
	return
}

然后创建 CNI 二进制运行起来需要的 kubeconfig 文件:

if in.kubeconfigFilepath, err = createKubeconfigFile(in.cfg, in.saToken); err != nil {
	cniInstalls.With(resultLabel.Value(resultCreateKubeConfigFailure)).Increment()
	return
}

最后是创建并覆盖 CNI 插件配置文件:

if in.cniConfigFilepath, err = createCNIConfigFile(ctx, in.cfg, in.saToken); err != nil {
	cniInstalls.With(resultLabel.Value(resultCreateCNIConfigFailure)).Increment()
	return
}

istio-cni

main 函数入口在 cni/cmd/istio-cni/main.go:

func main() {
    ...
	skel.PluginMain(plugin.CmdAdd, plugin.CmdCheck, plugin.CmdDelete, version.All,
		fmt.Sprintf("CNI plugin istio-cni %v", istioversion.Info.Version))
}

关键逻辑是调用 CNI 项目中的 skel.PluginMain 这个函数来注册 CNI 插件处理函数。

最重要的是 plugin.CmdAdd,即每次 kubelet 创建 Pod 时,调用 istio-cni 插件来设置容器网络的时候,就会走到 CmdAdd 这个函数。

首先会解析 kubelet 调用 istio-cni 时通过标准输入传来的配置,以及 install-cni 里面的 ambient server 写入的 ambient 配置文件。

// CmdAdd is called for ADD requests
func CmdAdd(args *skel.CmdArgs) (err error) {
    ...
   	conf, err := parseConfig(args.StdinData)
	if err != nil {
		log.Errorf("istio-cni cmdAdd failed to parse config %v %v", string(args.StdinData), err)
		return err
	}
    ...
	ambientConf, err := ambient.ReadAmbientConfig()
	if err != nil {
		log.Errorf("istio-cni cmdAdd failed to read ambient config %v", err)
		return err
	}
    ...

主题逻辑梳理

istio-cni 是被 install-cni 安装到节点的 CNI 插件二进制,在 kubelet 每次创建 pod 时会调用的二进制,主要功能如下:

  • 当要创建的 Pod 是网格内的 Pod 时,在 Pod 所在 netns 创建相关 iptables 规则以实现流量拦截(取代 istio-init 容器)。
  • 如果是网格内的 Pod,且数据面使用的 ambient 模式的 ztunnel 而不是 sidecar,自动更新 ambient 模式在节点上所需的 ipset, 路由表等。

已知 BUG

envoy 内存不释放

  • 已知受影响的版本: istio 1.5.4
  • 已解决版本: istio 1.6.0
  • issue: #25145

高频使用链接

istio 相关

Envoy 相关

实用脚本

istioctl

查看 sidecar 证书是否正常

$ istioctl proxy-config secret accountdeletecgi-5b9d6b586-wzb7b
RESOURCE NAME     TYPE           STATUS     VALID CERT     SERIAL NUMBER                               NOT AFTER                NOT BEFORE
default           Cert Chain     ACTIVE     true           198001071566761875257861959297039696827     2021-04-16T03:33:03Z     2021-04-15T03:33:03Z
ROOTCA            CA             ACTIVE     true           205820131934050053680230040513977871884     2031-03-24T02:58:23Z     2021-03-26T02:58:23Z

查看 sidecar 证书详情

$ istioctl -n istio-test proxy-config secret productpage-v1-578c57988-j8g9d -o json | jq '[.dynamicActiveSecrets[] | select(.name == "default")][0].secret.tlsCertificate.certificateChain.inlineBytes' -r | base64 -d | openssl x509 -noout -text
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            1b:6c:19:da:04:db:cb:1b:29:48:f8:09:60:35:22:75
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: L=cls-5u6csmhf, O=Istio, CN=Intermediate CA
        Validity
            Not Before: Apr 15 03:57:51 2021 GMT
            Not After : Apr 16 03:57:51 2021 GMT
        Subject:
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:dd:4f:bb:65:fd:d2:9c:7d:29:00:a9:6b:8c:b2:
                    8b:12:17:5f:6f:1b:d6:db:a2:7a:69:23:21:6a:d1:
                    38:4e:44:d0:c9:f4:6d:13:e9:97:86:54:f2:30:e6:
                    fe:9e:41:7c:95:a7:20:ff:bb:de:62:8e:49:58:90:
                    7a:38:be:15:f1:96:6e:ff:7a:c4:61:d8:a8:25:f1:
                    92:ee:33:ae:86:bb:63:38:2c:e7:32:a5:11:be:79:
                    3e:83:67:17:4e:91:df:0a:3e:52:11:60:9a:83:5d:
                    e4:92:9a:f6:29:43:7e:60:13:03:4d:ed:fc:d1:5c:
                    e9:5b:a9:a6:ef:b8:f5:82:78:a1:ef:15:43:17:40:
                    b3:48:c2:27:33:ac:0e:aa:00:c9:da:3f:ee:5d:1a:
                    d7:7a:4f:e3:e0:26:e8:67:1a:c1:44:c5:f3:d0:1c:
                    e1:e4:53:a5:a8:0b:04:47:cd:df:d2:a9:1b:47:8f:
                    3e:dc:9a:b6:b3:a8:6d:47:da:4d:68:dd:4f:82:3f:
                    aa:25:6d:8e:c5:8c:9d:1e:7c:93:4c:55:a3:59:d7:
                    a6:42:04:05:52:01:6d:a1:c8:8f:67:48:b4:16:4b:
                    46:6e:1e:5b:97:65:99:fe:5f:f7:f2:ba:ea:3f:34:
                    28:f1:e6:18:4d:d9:de:00:f2:fd:4a:9c:f9:a5:e2:
                    9d:5b
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment
            X509v3 Extended Key Usage:
                TLS Web Server Authentication, TLS Web Client Authentication
            X509v3 Basic Constraints: critical
                CA:FALSE
            X509v3 Authority Key Identifier:
                keyid:A0:62:8E:B4:53:64:D9:1D:DC:21:41:D8:05:93:E4:6D:27:82:20:4E

            X509v3 Subject Alternative Name: critical
                URI:spiffe://cluster.local/ns/istio-test/sa/bookinfo-productpage
    Signature Algorithm: sha256WithRSAEncryption
         ab:0d:b5:e6:df:50:02:d2:85:47:62:18:b0:af:89:cc:3a:06:
         a1:19:a8:2c:58:9c:e4:1d:34:3b:f8:a2:a7:f6:f8:0e:af:a8:
         1b:35:79:9d:72:a2:a9:96:14:37:c8:76:e2:50:ae:d4:c6:33:
         43:a5:0e:e4:c9:95:a8:81:9a:6a:72:e5:eb:3c:55:20:70:a4:
         27:3c:6d:88:da:03:75:3a:99:d0:72:c2:b3:2e:66:9e:00:9a:
         13:c5:61:20:fc:35:99:30:93:33:e6:8a:2d:b4:b0:0f:23:3a:
         a1:3d:4f:01:bf:cc:2b:38:2a:41:23:13:31:52:84:d7:8d:cb:
         71:63:28:e6:1f:1f:95:20:41:63:1a:a6:5f:a5:d0:3b:35:97:
         4b:8d:6c:55:59:34:e2:36:ff:a0:38:4c:f0:1f:a3:16:bf:bc:
         75:53:35:20:60:b2:0d:4d:bd:d1:ab:a6:28:60:e4:d7:0c:e3:
         cc:19:cb:d1:4c:e7:3d:fc:21:aa:eb:e6:f4:a6:0f:ed:cd:da:
         db:ae:4c:fa:cf:55:f8:ea:d1:55:d5:6c:51:95:3f:47:13:b7:
         20:e2:5d:cc:b0:ea:8d:99:e1:9f:40:df:d3:97:af:a5:69:f4:
         c6:b7:9c:c4:55:67:47:59:2b:53:40:f2:48:88:9b:75:77:00:
         22:98:f7:61:74:05:8c:8b:e4:1f:be:c8:e9:7a:8f:9a:5d:ff:
         1d:48:0a:e9:75:da:1e:35:93:a4:a0:c0:f8:78:bc:25:a2:63:
         d3:35:83:1f:15:28:a7:31:de:5a:d8:ae:56:f8:8c:ea:da:13:
         01:81:aa:6f:0f:a5:39:78:e6:b6:e3:1c:ff:7c:03:50:22:04:
         64:0a:dc:14:2c:ed:7d:ec:91:73:dc:44:3e:60:bc:d8:69:c3:
         7c:5b:d5:16:53:1c:24:2e:1b:51:fb:93:31:37:b3:80:e6:f2:
         07:46:09:8d:d5:2c:a4:f4:e3:14:b3:d9:d7:de:de:9c:bf:84:
         67:66:e1:b9:85:26:1c:8f:5c:8d:9f:5f:53:b7:ed:c7:2b:9d:
         57:3f:3c:d6:86:f4:d8:d8:72:c3:4c:be:5e:48:a4:ac:b9:c5:
         b1:6c:4b:dc:83:a2:bc:80:c2:34:c3:1a:68:7f:e8:e8:b9:eb:
         39:2a:6d:3d:2d:90:e2:9c:52:dc:a2:99:e3:dc:dc:5a:f7:71:
         9d:5f:67:93:d6:e3:68:a2:f9:7b:6e:64:a6:0c:09:95:f6:28:
         02:e4:3f:63:fc:09:12:f7:8f:ce:4a:c3:38:02:0c:35:64:f1:
         74:93:36:93:6d:e2:8e:5b:07:b9:5a:f8:14:32:69:4f:64:8d:
         6e:a4:b0:95:73:36:b6:92
  • 确保 Subject Alternative Name 包含正确的 spiffe URI,如 URI:spiffe://cluster.local/ns/istio-test/sa/bookinfo-productpage

实用 YAML

sidecar 注入相关

为指定 workload 取消 sidecar 自动注入

  template:
    metadata:
      annotations:
        sidecar.istio.io/inject: "false"

proxy 相关

自定义 request/limit

  template:
    metadata:
      annotations:
        "sidecar.istio.io/proxyCPU": "10m"
        "sidecar.istio.io/proxyCPULimit": "2"
        "sidecar.istio.io/proxyMemory": "32Mi"
        "sidecar.istio.io/proxyMemoryLimit": "1Gi"

自定义日志级别

  template:
    metadata:
      annotations:
        "sidecar.istio.io/logLevel": debug # 可选: trace, debug, info, warning, error, critical, off
        "sidecar.istio.io/componentLogLevel": "ext_authz:trace,filter:debug"

不劫持部分外部地址的流量以提升性能(比如外部数据库)

  template:
    metadata:
      annotations:
        traffic.sidecar.istio.io/excludeOutboundIPRanges: "10.10.31.1/32,10.10.31.2/32"

mtls 配置

全局禁用 mtls

apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: istio-system
spec:
  mtls:
    mode: DISABLE

DestinationRule 相关

为某个服务启用地域感知

地域感知行为需要显式指定 outlierDetection 后才会启用:

apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: nginx
spec:
  host: nginx
  trafficPolicy:
    outlierDetection:
      consecutive5xxErrors: 3
      interval: 30s
      baseEjectionTime: 30s

常用 EnvoyFilter

从 nginx 切到 ingressgateway

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: http-options
  namespace: istio-system
spec:
  configPatches:
  - applyTo: NETWORK_FILTER
    match:
      context: ANY
      listener:
        filterChain:
          filter:
            name: "envoy.http_connection_manager"
    patch:
      operation: MERGE
      value:
        typed_config:
          "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager"
          max_request_headers_kb: 96 # 96KB, 请求 header 最大限制
  - applyTo: HTTP_FILTER
    match:
      context: GATEWAY
      listener:
        filterChain:
          filter:
            name: "envoy.http_connection_manager"
    patch:
      operation: INSERT_BEFORE
      value:
        name: "envoy.filters.http.buffer"
        typed_config:
          '@type': "type.googleapis.com/envoy.extensions.filters.http.buffer.v3.Buffer"
          max_request_bytes: 1048576  # 1MB, 请求最大限制

保留 header 大小写

对所有 HTTP/1.1 的请求和响应都保留 header 大小写:

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: header-casing
  namespace: istio-system
spec:
  configPatches:
    - applyTo: CLUSTER
      match:
        context: SIDECAR_INBOUND
      patch:
        operation: MERGE
        value:
          typed_extension_protocol_options:
            envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
              '@type': type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
              explicit_http_config:
                http_protocol_options:
                  header_key_format:
                    stateful_formatter:
                      name: preserve_case
                      typed_config:
                        '@type': type.googleapis.com/envoy.extensions.http.header_formatters.preserve_case.v3.PreserveCaseFormatterConfig
    - applyTo: NETWORK_FILTER
      match:
        listener:
          filterChain:
            filter:
              name: envoy.filters.network.http_connection_manager
      patch:
        operation: MERGE
        value:
          typed_config:
            '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
            http_protocol_options:
              header_key_format:
                stateful_formatter:
                  name: preserve_case
                  typed_config:
                    '@type': type.googleapis.com/envoy.extensions.http.header_formatters.preserve_case.v3.PreserveCaseFormatterConfig