Menu

  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay
  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay

Istio学习笔记

10
Sep
2018

Istio学习笔记

By Alex
/ in PaaS
/ tags K8S, ServiceMesh
0 Comments
简介
服务网格是什么

术语服务网格(Service Mesh)用于描述微服务之间的网络,以及通过此网络进行的服务之间的交互。随着服务数量和复杂度的增加,服务网格将变的难以理解和管理。

对服务网格的需求包括:服务发现、负载均衡、故障恢复、指标和监控,以及A/B测试、金丝雀发布、限速、访问控制、端对端身份验证等。

Istio是什么

使用云平台给DevOps团队带来了额外的约束,为了Portability开发人员通常需要使用微服务架构,运维人员则需要管理非常多数量的服务。Istio能够连接、保护、控制、观察这些微服务。

Istio是运行于分布式应用程序之上的透明(无代码入侵)服务网格,它同时也是一个平台,提供集成到其它日志、监控、策略系统的接口。

Istio的实现原理是,为每个微服务部署一个Sidecar,代理微服务之间的所有网络通信。在此基础上你可以通过Istio的控制平面实现:

  1. 针对HTTP、gRPC、WebSocket、TCP流量的负载均衡
  2. 细粒度的流量控制行为,包括路由、重试、故障转移、故障注入(fault injection)
  3. 可拔插的策略层+配置API,实现访问控制、限速、配额
  4. 自动收集指标、日志,跟踪集群内所有流量,包括Ingress/Egress
  5. 基于强身份认证和授权来保护服务之间的通信
Istio核心特性
流量管理

使用Istio你可以很容易的通过配置,对流量和API调用进行控制。服务级别的可配置属性包括断路器(circuit breakers)、超时、重试。

Istio支持基于流量百分比切分的A/B测试、金丝雀滚动发布、分阶段滚动发布。

安全性

可以提供安全信道,管理身份验证和授权,加密通信流量。

联用K8S的网络策略可以获得更多益处,例如保护Pod-to-Pod之间的通信。

可观察性

Istio强大的跟踪、监控、日志能力,让服务网格内部结构更容易观察 —— 一个服务的性能对上下游的影响可以直观的展现在仪表盘上。

Istio的Mixer组件——通用的策略和监控(Telemetry)中心(Hub)——负责策略控制、指标收集。Mixer提供后端基础设施(例如K8S)的隔离层,Istio的其它部分不需要关注后端细节。

后端支持

Istio当前支持:

  1. Kubernetes上的Service
  2. 通过Consul注册的服务
  3. 在独立虚拟机上运行的服务
架构

从整体上看,Istio的服务网格由数据平面、控制平面两部分组成:

  1. 数据平面由一系列作为Sidecar部署的智能代理(Envoy)构成。这些代理联合Mixer,中继、控制所有微服务之间的网络通信。需要注意,还有一些Envoy是独立部署(而非Sidecar)的,用来实现K8S Ingress控制器、Istio的Ingress/Egress网关
  2. 控制平面负责管理、配置智能代理,实现流量路由;配置Citadel实现TLS证书管理;配置Mixers来应用策略、收集指标

架构图:

istio-arch

Envoy

Istio使用一个扩展过的Envoy版本。Envoy是基于C++开发的高性能代理,Istio使用它的以下特性:

  1. 动态服务发现
  2. 负载均衡
  3. TLS termination —— 可将后端的HTTP服务包装为HTTPS
  4. HTTP/2和gRPC代理
  5. 断路器
  6. 健康检查
  7. 分阶段(基于流量百分比)发布
  8. 故障注入
  9. 丰富的监控指标

一般情况下Envoy在和目标服务的相同Pod中,以Sidecar形式部署。少量的Istio组件的主进程就是Envoy,包括Ingress控制器、Ingress/Egress网关。

Mixer

一个平台无关的组件:

  1. 为服务网格应用访问控制策略
  2. 从Envoy和其它服务中收集指标
  3. Envoy收集的请求级别的属性,被发送到Mixer进行分析

Mixer提供了一个灵活的插件模型,让Istio能够灵活的和多种宿主机环境、基础设施后端进行对接。

Pilot

该组件是Istio的控制器,它会监控各种规则、策略(通常存储在K8S中),一旦配置文件发生变化,就会提取、处理,并同步给Envoy:

  1. 为Envoy提供服务发现
  2. 为智能路由(AB测试、金丝雀部署……)提供流量管理能力
  3. 提供弹性(Resiliency)——超时、重试、断路器等
  4. 分发身份验证策略给Envoy

Pilot将高级别的路由规则转换为Envoy理解的配置信息,并在运行时将这些配置传播到Sidecars。

Pilot将平台相关的服务发现机制抽象为标准的(Envoy data plane API,xDS)格式,这让Istio可以在K8S、Consul、Nomad等多种环境下运行。

Citadel

负责证书、密钥的管理。提供服务-服务之间、或者针对终端用户的身份验证功能,可以加密服务网格中的流量。

Istio的性能

部署服务网格带来了额外的性能损耗,下面是Istio组件的推荐资源配置:

  1. 对于启用了访问日志(默认启用)的Sidecar,每1000请求/s可以配备1个vCPU,如果没有启用访问日志则配备0.5个vCPU
  2. 节点上的Fluentd是资源消耗大户,它会捕获并上报日志,如果不关心可以排除Sidecar日志不上报
  3. 在大部分情况下,Mixer Check有超过80%的缓存命中率,在此前提下,可以为Mixer的Pod每1000请求/s配备0.5个vCPU
  4. 由于在通信两端都引入了Sidecar代理、并且需要上报Mixer,大概会引入10ms的延迟
Istio的HA

任何控制平面组件都支持多实例部署,在K8S中可以考虑配置HPA。

考虑将Mixer的check、report功能分开部署。当前官方Chart已经分开,分别对应istio-policy、istio-telemetry两个Deployment。

术语列表
术语 说明
Service

一个应用程序“行为单元”,它对应服务注册表(例如K8S的集群服务DNS)中的唯一的名称

服务具有0-N个网络端点(Endpoint),这些端点可以是Pod、容器、虚拟机

Service versions

也叫subsets,这是持续部署(CD)中的概念

在持续部署时,网格中可能运行着某个微服务的多种变体,这些变体的二进制代码不同,但是API版本不一定不同。这些变体代表了对某个服务的迭代性的改变,部署在不同的环境中

Source 访问当前服务的下游(downstream)客户端
Host 客户端连接到服务器时所使用的DNS名称
Destination 表示一个可寻址的服务,请求/连接在经过路由处理后,将发送到Destination
新特性
1.6版本
  1. 进一步完善istiod(单体化)
  2. 可观察性增强:更多配置项、对追踪采样率的控制、更新的Grafana仪表盘
  3. 更好的虚拟机支持(未来将更进一步强化):WorkloadEntry资源让非K8S工作负载成为Istio的一等公民,物理机或者虚拟机和Pod现在处于同一级别。你可以定义同时由Pod和物理机Back的服务。这让逐步迁移到K8S提供便利
1.5版本
单体化

1.5版本的最大变化是,控制平面的大部分组件(pilot、citadel、sidecar-injector)合并到名为istiod的单体应用中,现在istiod负责:

  1. 配置处理和推送
  2. 证书分发
  3. Sidecar注入

以下组件被移除:

  1. SDS的node-agent(合并到Pilot Agent)
废弃Mixer

Mixer被废弃,但是仍然可以使用。

新版本HTTP遥测基于Envoy过滤器Stats filter实现,可以节省50%的CPU用量。

流量管理
  1. 提升了 ServiceEntry 的性能。
  2. 修复了 readiness 探针不一致问题
  3. 通过定向局部更新的方式改善了配置更新的性能
  4. 添加了为 host 设置所在负载均衡器设置的选项
  5. 修复了 Pod 崩溃会触发过度配置推送的问题
  6. 添加了使用 Istio CNI 时对 iptables 的探测
  7. 添加了 consecutive_5xx 和 gateway_errors 作为离群值探测选项。
  8. 提升了 EnvoyFilter 匹配性能
  9. 添加了对 HTTP_PROXY 协议的支持
  10. 改进了 iptables 设置,默认使用 iptables-restore
  11. 默认开启自动协议探测
安全
  1. 添加 Beta 认证 API。新 API 分为 PeerAuthentication 和 RequestAuthenticaiton,面向工作负载
  2. 添加认证策略,支持 deny 操作和语义排除
  3. Beta 版本默认开启自动 mTLS
  4. 稳定版添加 SDS
  5. Node agent 和 Pilot agent 合并,移除了 Pod 安全策略的需要,提升了安全性
  6. 合并 Citadel 证书发放功能到 Pilot
  7. 支持 Kubernetes first-party-jwt 作为集群中 CSR 认证的备用 token
  8. 通过 Istio Agent 向 Prometheus 提供密钥和证书
  9. 支持 Citadel 提供证书给控制平面
遥测
  1. 为 v2 版本的遥测添加 TCP 协议支持
  2. 在指标和日志中支持添加 gRPC 响应状态码
  3. 改进 v2 遥测流程的稳定性
  4. 为 v2 遥测的可配置性提供 alpha 级别的支持
  5. 支持在 Envoy 节点的元数据中添加 AWS 平台的元数据
  6. 更新了 Mixer 的 Stackdriver 适配器,以支持可配置的刷新间隔来跟踪数据
  7. 支持对 Jaeger 插件的 headless 收集服务
  8. 修复了 kubernetesenv 适配器以提供对名字中有.的 Pod 的支持
  9. 改进了 Fluentd 适配器,在导出的时间戳中提供毫秒级输出
1.4版本

仍然以用户体验改进为主,简化配置难度。

无Mixer遥测

可以简化安装、Mesh的操控,并很大程度上改善性能。

istio-proxy内部生成HTTP指标的特性,从试验升级为Alpha。

istio-proxy内部生成TCP指标(试验)

自动mTLS

不需要配置DestinationRule即可自动启用。

1.3版本

该版本主要专注于用户体验的改善。

智能协议检测(试验)

为了使用Istio的路由特性,服务端口名必须遵循特定的命名约定,来声明其使用的协议。

从1.3开始,出站流量的协议可以自动识别为TCP或HTTP,这意味着服务端口命名协定不再必须

无Mixer遥测(试验)

现在大部分的安全策略(例如RBAC)已经直接在Envoy里面实现了。

istio-proxy现在可以直接向Prometheus发送数据,而不需要istio-telemetry来丰富、完善信息。

对生成的Envoy配置的定制能力

对于Envoy中的高级特性,Istio可能尚未提供对应API。你现在可以基于EnvoyFilter API来定制:

  1. LDS返回的HTTP/TCP监听器,及其过滤器链
  2. RDS返回的Envoy HTTP路由配置
  3. CDS返回的上游集群
1.2版本
一般特性
  1. 添加注解 traffic.sidecar.istio.io/includeInboundPorts,不再强制要求服务在deployment的YAML中声明contrainerPort
  2. 添加IPv6的试验性支持
流量管理
  1. 在多集群环境下,优化了基于位置的路由
  2. 优化了ALLOW_ANY模式(即允许任何未知的出站流量,配置位于configmap istio中)下的出站流量策略。针对未知HTTP/HTTPS主机+已知端口的流量,会原样的转发
  3. 支持设置针对上游服务的idle超时
  4. 改进了NONE模式(不用iptables,不进行流量捕获)的Sidecar支持
  5. 可以配置Sidecar的Envoy的DNS刷新率,防止DNS服务器过载
安全方面
  1. 扩展自签名Citadel根证书有效期为10年
  2. 可以为Deployment添加PodSpec注解 sidecar.istio.io/rewriteAppHTTPProbers: "true",来重写健康探针
遥感方面
  1. 对Envoy统计信息生成的完全控制,基于注解控制
  2. Prometheus生成的不包含在指标中
1.1版本
性能方面

控制平面性能:Pilot的CPU、内存消耗受网格的配置变化情况、工作负载变化情况、连接到Pilot的代理数量的影响。增加Pilot的实例数来可以减少配置分发处理的时间,提高性能。网格中包含1000服务、2000工作负载、1000QPS的情况下,单个Pilot实例消耗1个vCPU和1.5GB内存。

数据平面性能:

  1. CPU,代理在1000QPS下大概消耗0.6个vCPU
  2. 内存,代理的内存消耗取决于它需要处理的配置数量,大量的Listener、Cluster、Route会增加内存使用。此外,代理在1000QPS下需要消耗50MB内存
  3. 延迟,请求需要同时经过客户端/服务器的sidecar,在1000QPS下P99大概是10ms级别。启用策略检查(Check)会额外增加延迟。L4过滤器的逻辑比较简单,因此TCP流量不会引入显著延迟
安装部署方面
  1. CRD从Istio的Chart中独立出来,放入istio-init这个Chart。这避免了升级Istio而丢失CR数据
  2. 添加几个安装配置Profile,这些配置提供了常用的安装模式,简化了Istio的安装过程
  3. 增强了多集群集成
流量管理方面
  1. 新的资源类型Sidecar。用于对工作负载的边车代理进行细粒度的控制,特别是可以限制边车能够向哪些服务发送流量。此资源可以减少需要计算和传输给边车代理的配置信息,提升启动速度,减少资源消耗,提升控制平面可扩容性。在大型集群中,建议为每个命名空间提供一个Sidecar资源
  2. 支持限制服务(ServiceEntry、VirtualService)的可见范围。exportTo可用于限制哪些命名空间能够访问本服务。除了在Istio CR上指定exportTo以外,你还可以在K8S的Service上使用networking.istio.io/exportTo注解
  3. Gateway引用VirtualService时,可能存在歧义,因为不同命名空间可能定义具有相同hostname的VirtualService。在1.1版本中,你可以用namespace/hostname-match的形式来来设置hosts字段,以避免歧义。类似的在Sidecar中你也可以为egress配置为这种形式
  4. 支持设置ServiceEntry的Locality,以及关联的SAN(用于mTLS)。现在使用HTTPS端口的ServiceEntry不再需要配套的VirtualService来工作
  5. 位置感知路由(Locality-Aware Routing),优先路由到位于当前地理位置(Locality)的服务端点
  6. 简化了多集群模式的安装、支持额外的部署模式。现在可以用Ingress网关连接多个集群,而不需要Pod级别的VPN。为了HA可以在各集群中部署控制平面,这种部署方式下位置感知路由自动开启。可以将命名空间扩展到多个集群,以创建全局命名空间
  7. Istio的Ingress组件被废弃
  8. 性能和可扩容性得到提升
  9. 默认关闭访问日志
安全方面
  1. 健康检查探针,当启用mTLS的情况下,支持Kubernetes的Readiness/Liveness探针。当启用mTLS后,进入Envoy的HTTP探针请求会被拒绝,1.1能够自动进行HTTP探针的重写,将探针请求转发给pilot-agent,并由后者直接转发给工作负载,绕开Envoy的TLS认证
  2. 集群RBAC配置,将RbacConfig资源替换为ClusterRbacConfig资源
  3. 基于SDS的身份配置(Identity Provisioning ),不需要重启Envoy即可实现动态证书轮换,实现on-node的密钥生成增强了安全性
  4. 在HTTP/gRPC的基础上,支持TCP服务的授权
  5. 支持基于终端用户组的授权
  6. Ingress网关控制器支持外部证书管理,新的控制器用于支持动态的加载、轮换外部证书
  7. 定制PKI集成,支持Vault PKI,基于Vault CA签发证书
策略和遥感方面
  1. 策略检查默认关闭,主要是出于性能方面的考虑
  2. 性能的增强:
    1. 极大的减少了Envoy默认生成的统计信息的收集
    2. 为Mixer工作负载添加了负载限制的功能
    3. 改善了Mixer和Envoy之间的交互协议
  3. 适配器现在能够影响入站请求的头和路由
  4. 进程外适配器,生产级可用,进程内适配器被废弃
  5. 添加了默认的用于跟踪TCP连接的指标
多集群支持

在1.0版本Istio只提供了一种基于扁平网络的多集群方案:Istio控制平面部署在其中单个Kubernetes集群中。这种方案要求各集群的 Pod 地址范围不能重叠,且所有的 Kubernetes 控制平面API Server 互通。看上去就是物理上把多个Kubernetes并到一个Istio控制面下,在Istio看来是同一个网格。这种方案的网络要求苛刻,实际应用并不多。

1.1版本对多集群上做了非常多的增强,除了保留1.0扁平网络作为一种单控制面的方案外,还提出了另外两种方案供用户根据实际环境和需求灵活选择,这两种方案都不要求是扁平网络:

  1. 多控制平面方案:在每个集群中安装完整的Istio控制平面,可以看成是一种松散的联邦,集群间服务在Gateway处联通即可。通过一个全局的DNS将跨集群的请求路由到另外一个集群里去。这种集群的访问是基于Istio的ServiceEntry和Gateway来实现的,配置较多且复杂,需用户自己维护
  2. 一种集群感知(Split Horizon EDS)的单控制平面方案:Istio控制平面只在一个Kubernetes集群中安装,Istio控制平面仍然需要连接所有Kubernetes集群的K8S API Server。每个集群都有集群标识和网络标识。在服务间访问时,如果目标是本集群的负载,则类似单集群的方式直接转发;如果是其他集群的实例,则将流量转发到集群的入口网关上,再经过网关转发给实际负载
exportTo字段

在1.1版本中添加了一个重要字段exportTo。用于控制VirtualService、DestinationRule和 ServiceEntry 跨Namespace的可见性。这样就可以控制一个Namespace下定义的资源是否可以被其他Namespace下的Envoy访问。

如果不设置exportTo则默认全局可见。目前exportTo仅仅支持两个取值:

".",表示仅仅当前命名空间可以使用当前资源

"*",表示任何命名空间都可以使用当前资源

如果服务对Pod不可见,则Istio不会为该Pod的Envoy生成Listener、Cluster等信息,因而可以减少内存消耗。

sidecar资源

Sidecar是具有命名空间的资源。每个Sidecar应用到命名空间的一个和多个工作负载,工作负载的选择通过workloadSelector进行,如果不指定workloadSelector(每个命名空间只能有一个这样的Sidecar)则Sidecar应用到命名空间的所有(没有被其它带有workloadSelector的Sidecar匹配的)工作负载。

如果命名空间包含多个没有workloadSelector的Sidecar,或者多个Sidecar的workloadSelector匹配同一工作负载,则网格的行为是未定义的。

配置示例一,允许prod-us1命名空间的Pod发起针对prod-us1, prod-apis, istio-system命名空间的公共服务的egress流量:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: networking.istio.io/v1alpha3
kind: Sidecar
metadata:
  name: default
  namespace: prod-us1
spec:
  # 需要通信的服务白名单,可以很大程度上减少Envoy内存消耗
  # 经过测试,创建此资源后,istioctl proxy-config listener PODNAME 输出立刻变得非常少
  # 但是并没有禁止对外访问,可能和ALLOW_ANY有关?
  egress:
  - hosts:
    # 支持namespace/hostname-match形式的host规则
    - "prod-us1/*"
    - "prod-apis/*"
    - "istio-system/*"
    # 当前命名空间
    - "./ratings.default.svc.cluster.local"

配置示例二:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
apiVersion: networking.istio.io/v1alpha3
kind: Sidecar
metadata:
  name: default
  namespace: prod-us1
spec:
  # 入站流量规则
  ingress:
  # 对于从9080端口入站的HTTP流量,将其转发给Sidecar关联的工作负载监听的UDS
  - port:
      number: 9080
      protocol: HTTP
      name: somename
    defaultEndpoint: unix:///var/run/someuds.sock
  # 出站流量规则
  egress:
  # 允许针对istio-system命名空间的出站流量
  - hosts:
    - "istio-system/*"
  # 允许针对prod-us1命名空间的9080端口的HTTP流量
  - port:
      number: 9080
      protocol: HTTP
      name: egresshttp
    hosts:
    - "prod-us1/*"

配置示例三,如果工作负载没有启用基于Iptables的流量劫持,则Sidecar资源是唯一的配置工作负载边车端口的途径:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# 假设匹配的目标工作负载没有设置Iptables规则(没有istio-init容器)并且代理元数据ISTIO_META_INTERCEPTION_MODE设置为NONE
apiVersion: networking.istio.io/v1alpha3
kind: Sidecar
metadata:
  name: no-ip-tables
  namespace: prod-us1
spec:
  # 启用此规则的工作负载选择器
  workloadSelector:
    labels:
      app: productpage
  # 入站流量规则
  ingress:
  - port:
      # 让边车绑定 0.0.0.0:9080
      number: 9080
      protocol: HTTP
      name: somename
    # 并且将9080的流量转发给工作负载的8080端口
    defaultEndpoint: 127.0.0.1:8080
    # 如果为整个代理设置了元数据则不需要
    captureMode: NONE
  # 出站流量规则
  egress:
  # 让边车绑定 127.0.0.1:3306,并且将流量转发给外部的MySQL服务mysql.gmem.cc:3306
  - port:
      number: 3306
      protocol: MYSQL
      name: egressmysql
    captureMode: NONE
    bind: 127.0.0.1
    hosts:
    # 外部服务,命名空间部分写*
    - "*/mysql.gmem.cc"
 
 
# 外部服务用ServiceEntry来定义
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: external-svc-mysql
  namespace: ns1
spec:
  hosts:
  - mysql.gmem.cc
  ports:
  - number: 3306
    name: mysql
    protocol: MYSQL
  location: MESH_EXTERNAL
  resolution: DNS

字段captureMode指定流向监听器的流量如何被捕获(劫持),仅仅当监听器绑定到IP地址的情况下此字段才有意义:

取值 说明
DEFAULT 由环境定义的默认捕获模式
IPTABLES 基于Iptables重定向进行捕获
NONE

不进行流量捕获,当用在:

  1. egress监听器中时,应用程序被期望明确的和监听的端口/UDS进行直接通信
  2. ingress监听器中时,必须保证工作负载的其它进程不会占用目标监听端口
位置感知路由

Istio支持使用三元组:Region、Zone、Sub-zone来描述网格的地理位置,地理位置通常精确到某个数据中心。Istio能够使用此地理位置信息来对负载均衡池进行优先级控制。

在1.1版本中,基于地理位置的负载均衡仍然是试验特性,且默认关闭。设置环境变量PILOT_ENABLE_LOCALITY_LOAD_BALANCING到所有Pilot实例以开启此特性。

服务发现平台负责自动产生地理位置信息,在K8S中,Pod的地理位置基于它所在Node的知名标签来获取。在云环境下,Node的这些标签会自动设置,否则你需要手工设置Node标签。这些标签包括:

failure-domain.beta.kubernetes.io/region

failure-domain.beta.kubernetes.io/zone

注意,K8S没有sub-zone的概念。

基于地理位置的负载均衡,其默认模式是地理位置优先负载均衡(locality-prioritized load balancing )。Istio会提示Envoy,尽可能将流量转发给和当前Enovy更近的目标工作负载实例。

另一种模式是地理位置权重负载均衡(Locality-weighted load balancing),直接指定不同地理位置负责提供服务的权重。

CLI
istioctl

Istio的命令行配置工具,用于创建、查询、修改、删除Istio配置资源。

通用选项
选项 说明
--context 使用的kubeconfig context,默认""
-i Istio安装在的命名空间,默认istio-system
-c 使用的kubeconfig
--namespace 在哪个命名空间执行命令
--log_output_level  

日志级别,格式为scope:level,scope:level...默认default:info

scope取值:ads, default, model, rbac

level取值:debug, info, warn, error, none

authn

一组子命令,用于操控Istio的身份验证策略:

Shell
1
2
3
4
5
6
7
# 列出所有服务的身份验证策略
istioctl authn tls-check
# 仅仅显示单个服务的
istioctl authn tls-check grafana.istio-system.svc.k8s.gmem.cc
# 服务主机名和端口                 状态                    身份验证策略(服务器配置)                     客户端配置
# HOST:PORT                     STATUS  SERVER  CLIENT  AUTHN POLICY                              DESTINATION RULE
# grafana.istio-system...:3000  OK      HTTP    HTTP    grafana-ports-mtls-disabled/istio-system  -
create

创建策略、规则:

Shell
1
istioctl create -f example-routing.yaml
get

获取策略、规则:

Shell
1
2
3
4
5
6
7
# 获取所有虚拟服务
istioctl get virtualservices
# 获取所有目的地规则
istioctl get destinationrules
 
# 输出为YAML
istioctl get virtualservice bookinfo -o yaml 
replace

替换掉策略、规则:

Shell
1
istioctl replace -f example-routing.yaml 
delete 

删除策略、规则,命令格式:

Shell
1
istioctl delete <type> <name> [<name2> ... <nameN>] [flags]

示例:

Shell
1
2
3
4
istioctl delete virtualservice bookinfo
 
# 也可以通过yaml文件删除
istioctl delete -f example-routing.yaml
register

注册一个服务实例(例如一个VM)到服务网格中:

Shell
1
istioctl register <svcname> <ip> [name1:]port1 [name2:]port2 ... [flags]
deregister 

注销虚拟服务实例,命令格式:

Shell
1
istioctl deregister <svcname> <ip> [flags]
experimental 

一组试验性命令,未来可能修改或删除。

子命令 说明
convert-ingress

尽最大努力的把Ingress资源转换为VirtualService,示例:

Shell
1
2
kubectl -n devops get ingresses.extensions grafana --export -o yaml > /tmp/grafana.yaml
istioctl experimental convert-ingress -f /tmp/grafana.yaml

输出的VirtualService总是以svc.cluster.local为集群DNS后缀,可能需要修改 

metrics

打印一个或多个虚拟服务的统计指标,示例:

Shell
1
2
3
4
5
6
7
# 列出所有虚拟服务
istioctl get virtualservice
# 显示某个服务的指标
istioctl experimental metrics productpage
#  工作负载名称  请求/秒       错误/秒     延迟百分比分布
   WORKLOAD    TOTAL RPS    ERROR RPS  P50 LATENCY  P90 LATENCY  P99 LATENCY
productpage        0.000        0.000           0s           0s           0s

 

rbac

检查是否有权限访问服务,格式:

Shell
1
istioctl experimental rbac can METHOD SERVICE PATH [flags]

示例:

Shell
1
2
3
4
# 用户alex是否有权访问rating服务的/v1/health路径
istioctl experimental rbac can -u alex GET rating /v1/health
# 服务product-page是否有权以POST方法访问rating服务的dev版本的/data路径
istioctl experimental rbac can -s service=product-page POST rating /data -a version=dev
post-install

安装后相关操作:

post-install webhook disable 禁用Webhook
post-install webhook enable 启用Webhook
post-install webhook status 查看Webhook状态

remove-from-mesh

将工作负载从Mesh中移除:

remove-from-mesh deployment 移除此Deployment对应的Pod的Sidecar并重启
remove-from-mesh service 移除此服务对应的Pod的Sidecar并重启
remove-from-mesh external-service 移除ServiceEntry以及对应的K8S服务

upgrade 检查新版本并升级
wait

等待和Istio有关的资源Ready

kube-inject

手工的将Envoy Sidecar注入到K8S的工作负载的定义文件中。定义文件中包含多种不支持注入的资源是安全的:

Shell
1
2
3
4
# 输出到标准输出
kubectl apply -f <(istioctl kube-inject -f resource.yaml)
# 输出到文件
istioctl kube-inject -f deployment.yaml -o deployment-injected.yaml
proxy-config

一组命令,用于Dump出Envoy代理的各方面的配置:

子命令 说明
bootstrap

获取Envoy实例的Boostrap信息:

Shell
1
istioctl proxy-config bootstrap <pod-name> [flags]
cluster 获取Envoy实例的集群配置信息
listener 获取Envoy实例的listener配置信息
route 获取Envoy实例的路由配置信息

本节不做更细致的描述,具体参考运维-Dump出Envoy配置一节。

proxy-status

查询从Pilot最后一次发送到各Envoy的xDS同步的状态:

Shell
1
2
3
istioctl proxy-status
# PROXY                               CDS     LDS    EDS            RDS    PILOT                         VERSION
# details-v1-67c8f895b4-dnddn.default SYNCED  SYNCED SYNCED (100%)  SYNCED istio-pilot-77646875cd-jqmdh  1.0.2
analyze

分析配置文件并打印校验信息: istioctl analyze <file>... [flags]

示例:

Shell
1
2
3
4
5
6
7
8
# 分析当前服务网格
istioctl analyze
 
# 分析应用指定配置文件后的当前服务网格
istioctl analyze a.yaml b.yaml my-app-config/
 
# 不连接到服务网格,进行分析
istioctl analyze --use-kube=false a.yaml b.yaml my-app-config/
dashboard

访问Istio的Web UI。

Shell
1
2
3
4
5
6
7
8
9
10
11
12
# 为Istio控制仪表盘中打开指定Pod的ControlZ WebUI
istioctl dashboard controlz <pod-name[.namespace]> [flags]
 
# 为指定的Sidecar打开Envoy Admin仪表盘
istioctl dashboard envoy <pod-name[.namespace]> [flags]
 
# 打开集成的软件
istioctl dashboard grafana [flags]
istioctl dashboard jaeger [flags]
istioctl dashboard kiali [flags]
istioctl dashboard prometheus [flags]
istioctl dashboard zipkin [flags]
manifest

生成、应用(到K8S)Istio资源的清单文件,或者显示Diff:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 生成清单文件直接写入K8S
istioctl manifest apply
  --dry-run                         # 只是打印会做什么
  --filename,-f                  # 指向IstioOperator资源,用于在Base配置上进行Overlay,可以指定多次
  --force                         # 即使校验失败也继续
  --kubeconfig,-c                # Kubeconfig文件
  --set,-s                       # 覆盖IstioOperator中的值,下载Istio并到install/kubernetes/operator/profiles
                                 # 中看有哪些值,Spec字段下的值可以直接Set,例如Spec.hub可以 -s hub=docker.gmem.cc
  --wait,-w                      # 等待必要的资源就绪
 
# 应用默认配置
istioctl manifest apply
# 设置变量
istioctl manifest apply --set values.grafana.enabled=true
# 应用Profile,可用的Profile列表位于install/kubernetes/operator/profiles
istioctl manifest apply --set profile=demo --skip-confirmation
# 特殊字符转义
istioctl manifest apply --set "values.sidecarInjectorWebhook.injectedAnnotations.container\.apparmor\.security\.beta\.kubernetes\.io/istio-proxy=runtime/default"
 
# 显示两个目录或文件的区别
istioctl manifest diff
 
# 只是生成到标准输出,不Apply到K8S
istioctl manifest generate [flags]
 
# 从Helm Values或IstioControlPlane转换为IstioOperator格式的配置
istioctl manifest migrate
profile

列出、Dump、Diff可用的Profile,这些Profile可以在manifest命令中引用:

Shell
1
2
3
4
5
6
7
8
9
10
# 列出Profile
istioctl profile list [flags]
 
 
# 打印出来
istioctl profile dump [<profile>] [flags]
 
 
# 显示两个Profile的差异
istioctl profile diff <file1.yaml> <file2.yaml> [flags]
fortio

这是Istio的负载测试工具。

格式
Shell
1
2
       # 子命令 # 选项   # 目标URL
fortio command [flags] target
子命令
子命令 说明
load 执行负载测试
server 启动GRPC ping/http服务
grpcping 启动GRPC客户端
report 启动用于查看报告的UI服务器
redirect 启动仅执行重定向的服务器
curl 简单的调试一个URL,显示响应的详细信息
选项
选项 说明
-H value 添加额外的请求头
-abort-on int 如果遇到目标HTTP状态码,立即终止运行。例如503或-1,表示套接字错误
-allow-initial-errors 允许预热期间的错误,且不终止运行
-c int 连接/Goroutine/线程的并发数,默认4
-compression 启用HTTP压缩
-http1.0 使用HTTP 1.0而非1.1
-k 不校验服务器端证书
-keepalive 传输层连接重用,默认true
-qps float 目标QPS,如果为0则不做任何休眠,尽可能提高QPS,默认8
-n int 运行指定的次数
-t duration 运行指定的时间,默认5s,指定为0则一直运行
-p string 统计的百分比值(即耗时N%百分比的请求的最大耗时值),默认 50,75,90,99,99.9
-timeout duration HTTP连接/读取超时,默认15s
-user string HTTP基本认证的凭证信息,格式user:pswd
-loglevel lv 日志级别,可以取值Warning、Error等
mixc

Mixer的命令行客户端。

check

用于执行前置条件检查、配额(quota allocations)。Mixer期望以一组Attributes为输入,它基于自己的配置来判断调用哪个适配器、如何传递参数,进而进行策略检查、配额。

选项 短选项 说明
--attributes  -a 自动感知的属性列表,形式name1=value1,name2=value2...
--bool_attributes -b 布尔值属性的列表
--bytes_attributes   字节值属性的列表
--concurrency -c 并行的调用Mixer的线程数
--double_attributes -d 浮点数属性的列表
--duration_attributes   时间长度属性的列表
--int64_attributes -i 整数属性的列表
--string_attributes -s 字符串属性的列表
--stringmap_attributes   string:string映射类型的属性列表,形式name1=k1:v1;k2:v2,name2=k3:v3...
--timestamp_attributes -t

时间戳属性的列表

时间戳必须以ISO8601格式指定,例如:

Shell
1
2
3
4
5
6
7
8
# UTC时间
2019-03-27T11:42:57Z
 
# 北京时间
2019-03-27T11:42:57+08:00
 
# 带毫秒
2019-03-27T11:42:57.000+08:00
--mixer -m 目标Mixer实例
--quotas -q 需要分配的配额的列表,形式name1=amount1,name2=amount2...
--repeat -r 连续发送指定次数的请求,默认1
--trace_jaeger_url   追踪相关
--trace_log_spans  
--trace_zipkin_url  
report

用于产生遥感(telemetry)数据。Mixer期望以一组Attributes为输入,它基于自己的配置来判断调用哪个适配器、如何传递参数,进而输出遥感数据。

选项和check子命令一样。

安装到K8S
下载最新版
Shell
1
2
3
cd /home/alex/Applications
curl -L https://git.io/getLatestIstio | sh -
mv istio-1.0.3 istio

然后把istio/bin目录加入PATH环境变量。

支持Sidecar自动注入

Istio可以自动为Pod注入Sidecar,它基于Webhook实现此功能。

你可以执行下面的命令,确认MutatingAdmissionWebhook、ValidatingAdmissionWebhook这两种Admission controllers是否默认启用:

Shell
1
2
3
kubectl -n kube-system exec kube-apiserver-xenon -- kube-apiserver -h | grep enable-admission-plugins
# 如果在你的K8S版本中,in addition to default enabled ones...后面的括号中有MutatingAdmissionWebhook
# 和ValidatingAdmissionWebhook则说明默认已经启用

如果没有启用,则需要修改API Server的命令行参数。

此外,你还需要检查admissionregistration API是否启用:

Shell
1
2
kubectl api-versions | grep admissionregistration
admissionregistration.k8s.io/v1beta1

和手工注入不同,自动注入是发生在Pod级别的。在Deployment上看不到任何变化,你需要直接检查Pod才能看到注入的Envoy。 

对Pod/Service的要求

为了加入到服务网格,K8S中的相关Pod、Service必须满足如下条件:

  1. 命名端口:服务(指K8S的服务,不是Pod)的端口必须被命名,为了使用Istio的路由特性,端口名必须是 <protocol>[-<suffix>]的形式。protocol可以是http, http2, grpc, mongo或者redis,suffix是可选的,如果指定suffix则必须在前面加上短横线。如果protocol不支持,或者端口没有命名,则针对该端口的流量作为普通TCP流量看待(除非通过Protocol: UDP明确指定为UDP)。示例:
    YAML
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    apiVersion: v1
    kind: Service
    metadata:
      name: details
    spec:
      ports:
      - port: 9080
        # 命名服务端口
        name: http
      selector:
        app: details 
  2. 服务关联:如果一个Pod属于多个Kubernetes Service,则这些Service不得将同一端口用作不同协议
  3. app和version标签:建议为Pod添加app和version两个标签,分别表示应用程序的名称和版本。在分布式追踪(distributed tracing)中app标签用于提供上下文信息。在Metrics收集时app、version标签也提供上下文信息
通过Helm安装

Istio的Chart定义在install/kubernetes/helm/instio目录下 。默认情况下,Istio会以Sub chart的方式安装很多组件:

requirements.yaml
YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
dependencies:
  # 这是一个Webhook,自动为所有Pod注入Sidecar。默认启用
  - name: sidecarInjectorWebhook
    version: 1.0.3
    condition: sidecarInjectorWebhook.enabled
  # 安全模块,即citadel。默认禁用
  - name: security
    version: 1.0.3
    condition: security.enabled
  # Ingress控制器,用于处理K8S的Ingress资源。默认禁用
  - name: ingress
    version: 1.0.3
    condition: ingress.enabled
  # 入口、出口网关。默认启用
  - name: gateways
    version: 1.0.3
    condition: gateways.enabled
  # 默认启用,核心组件
  - name: mixer
    version: 1.0.3
    condition: mixer.enabled
  # 默认启用,核心组件
  - name: pilot
    version: 1.0.3
    condition: pilot.enabled
  # 默认禁用
  - name: grafana
    version: 1.0.3
    condition: grafana.enabled
  # 默认启用
  - name: prometheus
    version: 1.0.3
    condition: prometheus.enabled
  # 用于绘制调用链图。默认禁用,被kiali代替
  - name: servicegraph
    version: 1.0.3
    condition: servicegraph.enabled
  # APM。默认禁用
  - name: tracing
    version: 1.0.3
    condition: tracing.enabled
  # 用于服务器端的配置合法性验证。默认启用
  - name: galley
    version: 1.0.3
    condition: galley.enabled
  # 服务网格可视化。默认禁用
  - name: kiali
    version: 1.0.3
    condition: kiali.enabled
  # 负责基于ACME执行数字证书签名。默认禁用
  - name: certmanager
    version: 1.0.3
    condition: certmanager.enabled 

如需禁用,在values.yaml中修改对应配置项即可。

镜像列表

1.0.3版本:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
docker pull docker.io/istio/proxyv2:1.0.3
docker pull docker.io/istio/proxy_init:1.0.3
docker pull docker.io/istio/sidecar_injector:1.0.3
docker pull docker.io/istio/galley:1.0.3
docker pull docker.io/istio/mixer:1.0.3
docker pull docker.io/istio/pilot:1.0.3
docker pull docker.io/istio/citadel:1.0.3
docker pull docker.io/istio/servicegraph:1.0.3
docker pull docker.io/jaegertracing/all-in-one:1.5
docker pull docker.io/kiali/kiali:v0.9
docker pull docker.io/prom/prometheus:v2.3.1
docker pull quay.io/jetstack/cert-manager-controller:v0.3.1
docker pull quay.io/coreos/hyperkube:v1.7.6_coreos.0
docker pull grafana/grafana:5.2.3

1.1.4版本:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
docker pull docker.io/istio/kubectl:1.1.4
docker pull docker.io/istio/node-agent-k8s:1.1.4
docker pull docker.io/istio/citadel:1.1.4
docker pull docker.io/istio/proxy_init:1.1.4
docker pull docker.io/istio/proxyv2:1.1.4
docker pull docker.io/istio/galley:1.1.4
docker pull docker.io/istio/pilot:1.1.4
docker pull docker.io/istio/mixer:1.1.4
docker pull docker.io/istio/sidecar_injector:1.1.4
docker pull docker.io/jaegertracing/all-in-one:1.9
docker pull docker.io/kiali/kiali:v0.16
docker pull docker.io/prom/prometheus:v2.3.1
docker pull busybox:1.30.1
docker pull grafana/grafana:6.0.2
docker pull quay.io/jetstack/cert-manager-controller:v0.6.2
执行安装

到Github下载Istio的压缩包,解压后在其根目录执行。

我的环境下,镜像拉取需要Secret:

Shell
1
2
3
4
5
6
7
8
9
export PSWD=...
 
# 创建命名空间
kubectl create ns istio-system
 
# 创建访问Docker镜像仓库的保密字典
kubectl -n istio-system create secret docker-registry gmemregsecret \
    --docker-server=docker.gmem.cc --docker-username=alex  \
    --docker-password=$PSWD --docker-email=k8s@gmem.cc

对于Istio 1.1,需要首先安装独立的Chart istio-init,进行CRD的注册:

Shell
1
helm install install/kubernetes/helm/istio-init  --name istio-init --namespace istio-system -f install/kubernetes/helm/istio-init/overrides/gmem.yaml

安装Istio组件:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 创建入口网关控制器所需的数字证书,这里是*.k8s.gmem.cc的通配符证书
# 注意,ingressgateway没有自动reload数字证书的能力,必须重启Pod
pushd /home/alex/Documents/puTTY/mesh.gmem.cc
kubectl create -n istio-system secret generic istio-ingressgateway-certs --from-file=.
popd
 
# 创建kiali的Ingress所需的数字证书保密字典
pushd /etc/letsencrypt/live/kiali.k8s.gmem.cc
kubectl create -n istio-system secret generic gmemk8scert-kiali --from-file=tls.crt=fullchain.pem,tls.key=privkey.pem
popd
# 创建Jaeger的Ingress所需的数字证书保密字典
pushd /etc/letsencrypt/live/jaeger.k8s.gmem.cc
kubectl create -n istio-system secret generic gmemk8scert-jaeger --from-file=tls.crt=fullchain.pem,tls.key=privkey.pem
popd
 
# 为默认证书设置imagePullSecrets
kubectl -n istio-system patch serviceaccount default -p '{"imagePullSecrets": [{"name": "gmemregsecret"}]}'
 
# 不让Citadel使用自签名根证书
pushd ~/Documents/puTTY/
touch empty.pem
kubectl -n istio-system create secret generic cacerts --from-file=ca-cert.pem=ca.crt \
                                                      --from-file=ca-key.pem=ca.key \
                                                      --from-file=root-cert.pem=ca.crt \
                                                      --from-file=cert-chain.pem=empty.pem
popd
 
# 挂载Ceph卷所需的密钥
kubectl get secrets pvc-ceph-key -o yaml --export | kubectl -n istio-system apply -f -
 
 
# 安装Istio
# 1.0
helm install install/kubernetes/helm/istio --name istio --namespace istio-system --set kiali.dashboard.passphrase=$PSWD -f install/kubernetes/helm/istio/overrides/gmem.yaml
# 1.1
kubectl -n istio-system create secret generic kiali --from-literal=username=alex --from-literal=passphrase=$PSWD
 
# 为kiali的服务账号授予必要的权限
kubectl create clusterrolebinding istio-system-kiali-role-binding --clusterrole=cluster-admin --serviceaccount=istio-system:kiali-service-account
最小化安装
Shell
1
2
3
4
5
6
7
8
9
10
11
helm install install/kubernetes/helm/istio --name istio-minimal --namespace istio-system \
  --set security.enabled=false \
  --set ingress.enabled=false \
  --set gateways.istio-ingressgateway.enabled=false \
  --set gateways.istio-egressgateway.enabled=false \
  --set galley.enabled=false \
  --set sidecarInjectorWebhook.enabled=false \
  --set mixer.enabled=false \
  --set prometheus.enabled=false \
  --set global.proxy.envoyStatsd.enabled=false \
  --set pilot.sidecar=false

最小化安装时,仅仅安装pilot组件。 

执行删除
Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
helm delete istio --purge
 
kubectl -n istio-system delete all --all
 
kubectl delete ns istio-system
 
kubectl delete crd adapters.config.istio.io            
kubectl delete crd apikeys.config.istio.io              
kubectl delete crd attributemanifests.config.istio.io  
kubectl delete crd authorizations.config.istio.io      
kubectl delete crd bypasses.config.istio.io            
kubectl delete crd certificates.certmanager.k8s.io      
kubectl delete crd checknothings.config.istio.io        
kubectl delete crd circonuses.config.istio.io          
kubectl delete crd cloudwatches.config.istio.io        
kubectl delete crd clusterissuers.certmanager.k8s.io    
kubectl delete crd deniers.config.istio.io              
kubectl delete crd destinationrules.networking.istio.io
kubectl delete crd dogstatsds.config.istio.io          
kubectl delete crd edges.config.istio.io                
kubectl delete crd envoyfilters.networking.istio.io    
kubectl delete crd fluentds.config.istio.io            
kubectl delete crd gateways.networking.istio.io        
kubectl delete crd handlers.config.istio.io            
kubectl delete crd httpapispecbindings.config.istio.io  
kubectl delete crd httpapispecs.config.istio.io        
kubectl delete crd instances.config.istio.io            
kubectl delete crd issuers.certmanager.k8s.io          
kubectl delete crd kubernetesenvs.config.istio.io      
kubectl delete crd kuberneteses.config.istio.io        
kubectl delete crd listcheckers.config.istio.io        
kubectl delete crd listentries.config.istio.io          
kubectl delete crd logentries.config.istio.io          
kubectl delete crd memquotas.config.istio.io            
kubectl delete crd meshpolicies.authentication.istio.io
kubectl delete crd metrics.config.istio.io              
kubectl delete crd noops.config.istio.io                
kubectl delete crd opas.config.istio.io                
kubectl delete crd policies.authentication.istio.io    
kubectl delete crd prometheuses.config.istio.io        
kubectl delete crd quotas.config.istio.io              
kubectl delete crd quotaspecbindings.config.istio.io    
kubectl delete crd quotaspecs.config.istio.io          
kubectl delete crd rbacconfigs.rbac.istio.io            
kubectl delete crd rbacs.config.istio.io                
kubectl delete crd redisquotas.config.istio.io          
kubectl delete crd reportnothings.config.istio.io      
kubectl delete crd rules.config.istio.io                
kubectl delete crd servicecontrolreports.config.istio.io
kubectl delete crd servicecontrols.config.istio.io      
kubectl delete crd serviceentries.networking.istio.io  
kubectl delete crd servicerolebindings.rbac.istio.io    
kubectl delete crd serviceroles.rbac.istio.io          
kubectl delete crd signalfxs.config.istio.io            
kubectl delete crd solarwindses.config.istio.io        
kubectl delete crd stackdrivers.config.istio.io        
kubectl delete crd statsds.config.istio.io              
kubectl delete crd stdios.config.istio.io              
kubectl delete crd templates.config.istio.io            
kubectl delete crd tracespans.config.istio.io          
kubectl delete crd virtualservices.networking.istio.io  
处理集群DNS后缀

如果你的Kubernetes集群不使用缺省的集群DNS后缀(即cluster.local),则很多组件的参数需要修改,可以参考chart-istio项目,搜索{{ .Values.global.domain }}找到需要修改的位置。

注意:1.1版本的Chart这块已经处理好,修改values.yaml中的global.proxy.clusterDomain即可。

配置

Istio暴露了几个Configmap,修改这些Configmap以影响Istio的行为。

istio
YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
apiVersion: v1
kind: ConfigMap
metadata:
  name: istio
  namespace: istio-system
  labels:
    app: istio
    chart: istio-1.0.5
    release: istio
    heritage: Tiller
data:
  mesh: |-
    # 设置为true则禁用策略检查
    # 不影响遥测指标的报送
    disablePolicyChecks: false
 
    # 是否启用请求跟踪
    enableTracing: true
 
    # 设置为空字符串可以禁用访问日志
    accessLogFile: "/dev/stdout"
 
    # Deprecated: mixer is using EDS
    mixerCheckServer: istio-policy.istio-system.svc.k8s.gmem.cc:9091
    mixerReportServer: istio-telemetry.istio-system.svc.k8s.gmem.cc:9091
 
    # 如果设置为true,则Mixer的策略服务不可用导致策略检查失败时,允许请求而非拒绝
    policyCheckFailOpen: false
 
    defaultConfig:
      #
      # Envoy和应用之间、Envoy之间的TCP链接超时
      connectTimeout: 10s
      #
      ### ADVANCED SETTINGS #############
      # istio-proxy容器中,Envoy的配置存放在何处
      configPath: "/etc/istio/proxy"
      # Envoy二进制文件的路径
      binaryPath: "/usr/local/bin/envoy"
      # The pseudo service name used for Envoy.
      serviceCluster: istio-proxy
      # 如果发生偶然性的Envoy reload,旧的Envoy进程保留多长时间
      drainDuration: 45s
      parentShutdownDuration: 1m0s
      #
      # 将入口流量重定向到Envoy的模式,对出口流量无影响,后者总是使用iptables REDIRECT
      # 取值:
      #   REDIRECT,使用iptables REDIRECT来NAT,并重定向到Envoy。源地址信息会丢失
      #   TPROXY,使用iptables TPROXY来重定向到Envoy,同时保留原始的source/dest IP地址和端口
      #           TPROXY可以在不对封包进行任何变动(例如NAT)的情况下重定向封包给一个本地套接字
      #           可以用于高级过滤和操控。此模式会配置sidecar,启用CAP_NET_ADMIN能力,这是TPROXY所需的
      #
      interceptionMode: REDIRECT
      #
      # Envoy在本地环回网卡上监听的管理端口,你可以进入istio-proxy容器并执行
      # curl http://localhost:15000/获取Envoy的诊断信息
      # 访问https://lyft.github.io/envoy/docs/operations/admin.html了解更多信息
      proxyAdminPort: 15000
      #
      # Envoy工作线程数量,如果设置为0,自动根据CPU核心数量确定
      concurrency: 0
      #
      # Zipkin/Jaeger的访问地址,Envoy向其报送APM数据
      zipkinAddress: zipkin.istio-system:9411
      #
      # Sidecar和Istio控制平面的mTLS认证策略
      controlPlaneAuthPolicy: NONE
      #
      # Pilot服务的地址
      discoveryAddress: istio-pilot.istio-system:15007
跨集群网格

可以跨越多个Kubernetes集群来部署Istio服务网格。

前提条件
  1. 两个或更多的运行1.9+版本的Kubernetes集群
  2. 有权在其中一个集群上部署Istio控制平面
  3. 使用RFC1918网络、VPN或更高级的网络技术将集群连接在一起,并且满足:
    1. 所有集群的Pod网络CIDR、Service网络CIDR不重叠
    2. 所有集群的Pod网络可以相互路由
    3. 所有集群的API Server可相互联通
  4. Helm 2.7.2或更高版本
Istio CNI插件

Istio 将用户 pod 流量转发到 proxy 的默认方式是使用 privileged 权限的 istio-init 容器,Istio CNI插件可以消除这一priviledged要求。

Istio CNI Plugin是Kubernetes CNI的一个实现,Kubernetes CNI插件是一个链,在创建和销毁pod的时候会调用链上所有插件来安装和卸载容器的网络。

此插件会运行一个Daemonset: istio-cni-node,在每个节点上安装CNI插件。此插件负责写入iptables规则。

启用此插件,istio-init容器不再需要。

安装

首先安装CNI插件:

Shell
1
helm install install/kubernetes/helm/istio-cni --name istio-cni --namespace kube-system

然后,安装Istio,注意设置参数:

Shell
1
helm install install/kubernetes/helm/istio --name istio --namespace istio-system --set istio_cni.enabled=true 
安装1.5版本

此版本的Helm Chart不正常,安装出来的不是isitd单体模式。

推荐使用 istioctl manifest命令来安装,生成的清单文件包括namespace istio-system,因此删除操作:

Shell
1
istioctl manifest generate <your original installation options> | kubectl delete -f -

会导致预先创建手工创建在istio-system中的资源丢失。建议先 istioctl manifest generate,然后修改它。 

覆盖默认值

使用istioctl预置的Profile不能满足需要的话,可通过-f参数自己提供一个IstioOperator资源,在其中覆盖参数值:

Shell
1
istioctl manifest generate -f gmem.yaml

gmem.yaml
YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
metadata:
  namespace: istio-system
spec:
  hub: docker.gmem.cc/istio
  imagePullSecrets:
    - gmemregsecret
  addonComponents:
    kiali:
      enabled: true
    grafana:
      enabled: true
    tracing:
      enabled: true
  values:
    global:
      logging:
        level: "default:info"
      proxy:
        clusterDomain: "k8s.gmem.cc"
        logLevel: info
        privileged: true
        accessLogFile: /dev/stdout
      trustDomain: "k8s.gmem.cc"
      sds:
        enabled: true
    security:
      selfSigned: false
    prometheus:
      hub: docker.gmem.cc/prom
    grafana:
      image:
        repository: docker.gmem.cc/grafana
    tracing:
      jaeger:
        hub: docker.gmem.cc/jaegertracing
    kiali:
      hub: docker.gmem.cc/kiali

1.5.0版本中-f的行为异常,不能生成资源清单。暂时可用-s代替:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
istioctl manifest apply \
  -s profile=demo -s hub=docker.gmem.cc/istio -s values.global.logging.level="default:info" \
  -s values.global.proxy.clusterDomain=k8s.gmem.cc \
  -s values.global.proxy.logLevel=info -s values.global.proxy.privileged=true \
  -s values.global.proxy.accessLogFile=/dev/stdout \
  -s values.global.trustDomain=k8s.gmem.cc -s values.global.sds.enabled=true \
  -s values.security.selfSigned=false  -s values.prometheus.hub=docker.gmem.cc/prom \
  -s values.grafana.image.repository=docker.gmem.cc/grafana \
  -s values.tracing.jaeger.hub=docker.gmem.cc/jaegertracing \
  -s values.kiali.hub=docker.gmem.cc/kiali -s values.kiali.contextPath=/ \
  -s values.tracing.contextPath=/ \
  -s values.global.imagePullSecrets[0]=gmemregsecret -s values.global.imagePullPolicy=Always
卸载
Shell
1
istioctl manifest generate --set profile=demo | kubectl delete -f -

警告:命名空间也会被删除。 

构建和调试
构建
环境变量
Shell
1
2
3
4
5
export HUB=docker.gmem.cc
export TAG=1.5
# 使用本地构建的,位于$GOPATH/src/github.com/istio/proxy/bazel-bin/src/envoy/envoy的Istio Proxy
export USE_LOCAL_PROXY=1
export GOOS=linux
构建Istio Proxy
Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
export BAZEL_BUILD_ARGS="-c dbg"
 
cd $GOPATH/src/istio.io/
git clone https://github.com/istio/proxy.git
 
# 安装Bazelisk
sudo wget -O /usr/local/bin/bazel https://github.com/bazelbuild/bazelisk/releases/download/v0.0.8/bazelisk-linux-amd64
sudo chmod +x /usr/local/bin/bazel
 
# 安装相关依赖
sudo apt-get install libtool  cmake automake autoconf make ninja-build curl unzip  build-essential software-properties-common
sudo pip install virtualenv
 
# 安装GCC 7+ 或 Clang/LLVM 7+,因为使用了C++14。我们使用LLVM 7.1.0
cd/home/alex/CPP/llvm
wget https://github.com/llvm/llvm-project/releases/download/llvmorg-7.1.0/clang+llvm-7.1.0-x86_64-linux-gnu-ubuntu-14.04.tar.xz
xz -d clang+llvm-7.1.0-x86_64-linux-gnu-ubuntu-14.04.tar.xz
tar xvf clang+llvm-7.1.0-x86_64-linux-gnu-ubuntu-14.04.tar
mv clang+llvm-7.1.0-x86_64-linux-gnu-ubuntu-14.04 7.1.0
 
# Bazel文件格式化工具
go get -u github.com/bazelbuild/buildtools/buildifier
# 用于移动应用打包
go get -u github.com/bazelbuild/buildtools/buildozer
 
# 执行构建
cd proxy
bazel-proxy/external/envoy/bazel/setup_clang.sh /home/alex/CPP/llvm/7.1.0
http_proxy=http://127.0.0.1 https_proxy=http://127.0.0.1:8087 make
构建Istio
Shell
1
2
3
4
5
6
7
8
9
10
11
cd $GOPATH/src/istio.io/
git clone https://github.com/istio/istio.git
 
cd istio/
# 构建出Docker镜像
make docker
# 推送镜像
make push
 
# 生成K8S资源清单
make generate_yaml
示例应用
bookinfo

Istio提供了一个示例应用程序Bookinfo,我可以部署该应用 ,并为其配置服务网格,以学习Istio。该应用程序能够显示书籍的基本信息,包括ISBN、页数,还能显示书籍的评论信息。

Bookinfo由4个独立的微服务组成:

  1. productpage,调用details、reviews渲染页面
  2. details,提供书籍基本信息
  3. reviews,提供书评信息,调用ratings。该服务有v1、v2、v3三个版本
  4. ratings,提供书籍的评级信息

下图显示了没有部署Istio之前,Bookinfo的端对端架构:

下图显示受到Istio服务网格管理的Bookinfo的架构:

为命名空间启用自动注入

如果启用了自动化的Sidecar注入,你需要在安装到的命名空间上打标签:

Shell
1
kubectl label namespace default istio-injection=enabled

打上此标签后,default命名空间中创建的新Pod,自动会有一个名为stio-proxy的容器,它运行istio/proxyv2镜像。 

手工注入

如果没有启用自动化的Sidecar注入,你需要执行:

Shell
1
kubectl apply -f <(istioctl kube-inject -f samples/bookinfo/platform/kube/bookinfo.yaml)
安装bookinfo资源

然后,执行下面的命令安装所有组件:

Shell
1
kubectl apply -f samples/bookinfo/platform/kube/bookinfo.yaml

等所有容器都运行起来了,检查一下当前命名空间,应该多出6个Pod、4个Service。

安装Istio网关

当所有Pod都进入Running状态后,你需要创建一个(入口)网关,这样浏览器才能访问到集群中的Bookinfo服务:

Shell
1
kubectl apply -f samples/bookinfo/networking/bookinfo-gateway.yaml

上述命令会创建一个Gateway,一个VirtualService:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# kubectl get gateway bookinfo-gateway -o yaml --export
 
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: bookinfo-gateway
  namespace: default
spec:
  selector:
    istio: ingressgateway
  servers:
  - hosts:
    - '*'
    port:
      name: http
      number: 80
      protocol: HTTP
 
 
# kubectl get virtualservice bookinfo -o yaml --export
 
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: bookinfo
spec:
  gateways:
  - bookinfo-gateway  # 和前面的网关绑定
  hosts:
  - '*'
  http:
  - match:
    # 以下3个URL模式发送给productpage服务
    - uri:
        exact: /productpage
    - uri:
        exact: /login
    - uri:
        exact: /logout
    - uri:
        prefix: /api/v1/products
    route:
    - destination:
        host: productpage
        port:
          number: 9080
获取网关地址

我的本地环境,通过配置路由,允许集群外部访问ClusterIP:

Shell
1
2
3
kubectl get svc istio-ingressgateway -n istio-system
# NAME                   TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)                                                                                                                AGE
# istio-ingressgateway   NodePort   10.109.97.20   <none>        80:20080/TCP,443:20443/TCP,31400:31400/TCP,15011:20493/TCP,8060:1164/TCP,853:5745/TCP,15030:1299/TCP,15031:11899/TCP   14d

因此直接使用上述地址10.109.97.20即可。如果你使用NodePort方式,则需要使用宿主机IP + 映射后的端口。

如果使用外部负载均衡器(LoadBalancer类的Service),则参考如下命令:

Shell
1
2
3
4
5
6
export INGRESS_HOST=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
export INGRESS_PORT=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.spec.ports[?(@.name=="http2")].port}')
export SECURE_INGRESS_PORT=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.spec.ports[?(@.name=="https")].port}')
 
# 某些环境下,LB通过主机名而非IP暴露
export INGRESS_HOST=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')
确认应用可访问
Shell
1
2
3
4
5
export INGRESS_HOST=`kubectl -n istio-system get service istio-ingressgateway --no-headers | awk '{print $3}'`
export INGRESS_PORT=80
export GATEWAY_URL=$INGRESS_HOST:$INGRESS_PORT
 
curl -o /dev/null -s -w "%{http_code}\n" http://${GATEWAY_URL}/productpage

如果一切正常,应该打印200到控制台。 如果通过浏览器查看,界面右侧的Reviews面板会随机出现无星、黑星、红星,这种随机是K8S的Service负载均衡机制导致的。

创建默认DestRule

下面的DestinationRule声明了productpage、reviews、ratings、details等微服务的不同版本。版本通过Pod的version标签来区分。

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# kubectl apply -f samples/bookinfo/networking/destination-rule-all.yaml
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: productpage
spec:
  host: productpage
  subsets:
  - name: v1
    labels:
      version: v1
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: reviews
spec:
  host: reviews
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2
  - name: v3
    labels:
      version: v3
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: ratings
spec:
  host: ratings
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2
  - name: v2-mysql
    labels:
      version: v2-mysql
  - name: v2-mysql-vm
    labels:
      version: v2-mysql-vm
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: details
spec:
  host: details
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2
---
httpbin

一个简单的HTTP测试服务,用于试验Istio的各种特性:

Shell
1
kubectl apply -f samples/httpbin/httpbin.yaml
fortio

Fortio是Istio的负载测试工具,提供CLI和Web UI。

Fortio能够按照指定的QPS来执行HTTP请求,录制执行耗时、百分比分布(percentiles,例如p99表示99%的请求耗时小于的数值)直方图。

Shell
1
kubectl apply -f  samples/httpbin/sample-client/fortio-deploy.yaml
sleep

这是一个基于Ubuntu的容器,它启动后只是简单的休眠。你可以通过kubectl连接到此容器并执行curl命令:

Shell
1
kubectl apply -f samples/sleep/sleep.yaml 
试验流量管理
请求路由

下面的例子创建了几个“虚拟服务“,这些虚拟服务将请求流量全部路由给各微服务的v1版本:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# kubectl apply -f samples/bookinfo/networking/virtual-service-all-v1.yaml
 
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: productpage
spec:
  hosts:
  - productpage
  http:
  - route:
    - destination:
        host: productpage
        subset: v1
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: reviews
spec:
  hosts:
  - reviews
  http:
  - route:
    - destination:
        host: reviews
        subset: v1
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: ratings
spec:
  hosts:
  - ratings
  http:
  - route:
    - destination:
        host: ratings
        subset: v1
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: details
spec:
  hosts:
  - details
  http:
  - route:
    - destination:
        host: details
        subset: v1
---

虚拟服务的会被转换为Envoy配置信息,在若干秒内传递到所有Pod的Sidecar,达到最终一致性。

虚拟服务可以遮蔽掉K8S的同名Service,启用了Envoy Sidecar的Pod(包括入口网关)访问productpage、reviews、ratings、details这几个微服务(各自对应了一个K8S Service、一个虚拟服务、以及一个DNS名称)时,会自动经由上面定义的几个虚拟服务处理,其效果就是全部访问v1版本的微服务。虚拟服务的路由逻辑在请求方的Sidecar中实现。

现在再打开浏览器访问/productpage,你会发现,不管刷新多少次,Reviews面板总是显示为无星。

从本章开始处的架构图可以知道,productpage会访问reviews服务,我们看一下前者Sidecar日志,可以发现类似下面的内容:

Shell
1
2
3
"GET /reviews/0 HTTP/1.1" 200 - 0 295 85 84 "-" "python-requests/2.18.4" "4e51c6d9-b7d7-483f-912c-986b4ca1ccff"
# 目标服务      实际访问的IP:PORT组合    出站流量      访问reviews的v1版本                     # 服务端             客户端
"reviews:9080" "172.27.252.190:9080" outbound|9080|v1|reviews.default.svc.k8s.gmem.cc - 10.105.173.219:9080 172.27.121.172:56202

在看看后者v1版本的Sidecar日志,可以发现相呼应的内容:

Shell
1
2
3
"GET /reviews/0 HTTP/1.1" 200 - 0 295 14 8 "-" "python-requests/2.18.4" "148f4c19-8106-4833-8a62-7b464b33e807"
                                     # 入站流量
"reviews:9080" "127.0.0.1:9080"      inbound|9080||reviews.default.svc.k8s.gmem.cc    - 172.27.252.190:9080 172.27.121.172:49704

你可以修改虚拟服务reviews,将v1替换为v2、v3,看看有何效果:

Shell
1
kubectl edit virtualservice reviews
基于请求头路由

虚拟服务支持根据请求信息路由,下面的例子依据请求头end-user的值决定使用什么版本的reviews服务:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# kubectl apply -f samples/bookinfo/networking/virtual-service-reviews-test-v2.yaml
 
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: reviews
spec:
  hosts:
    - reviews
  http:
  # 如果登陆用户为alex,则使用v2版本
  - match:
    - headers:
        end-user:
          exact: alex
    route:
    - destination:
        host: reviews
        subset: v2
  # 否则使用v1版本
  - route:
    - destination:
        host: reviews
        subset: v1

现在,刷新/productpage可以发现是无星,以alex登陆(密码随意)后则显示为黑星。 

故障注入

故障注入(Fault Injection)属于破坏性测试,用于考验应用的韧性(resiliency)。

下面的例子,专为alex用户在reviews:v2和ratings之间引入7s的延迟:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# kubectl apply -f samples/bookinfo/networking/virtual-service-ratings-test-delay.yaml
 
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: ratings
spec:
  hosts:
  - ratings
  http:
  # 对于alex用户,全部请求引入7s延迟,使用ratings的v1版本
  - match:
    - headers:
        end-user:
          exact: alex
    fault:
      delay:
        percent: 100
        fixedDelay: 7s
    route:
    - destination:
        host: ratings
        subset: v1
  # 对于其它用户,也使用ratings的v1版本,但是不引入延迟
  - route:
    - destination:
        host: ratings
        subset: v1

以alex访问/productpage,页面会在挂起6秒后提示reviews不可用。这是由于productpage到reviews的超时被硬编码为6秒,而reviews到ratings的超时被硬编码为10秒,我们的故障注入导致productpage访问reviews出现超时错误。

除了timeout之外,你还可以注入abort形式的故障,即立刻访问一个HTTP错误码。

灰度发布

Istio支持逐步的将流量从一个版本迁移到另一个,通常是从老版本迁移到新版本。

定义虚拟服务时,你可以设置不同destination的权重,并逐步调整,直到完全切换到另一个版本。下面的例子会让你由50%的概率看到无星或红星:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# kubectl apply -f samples/bookinfo/networking/virtual-service-reviews-50-v3.yaml
 
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: reviews
spec:
  hosts:
    - reviews
  http:
  - route:
    - destination:
        host: reviews
        subset: v1
      weight: 50
    - destination:
        host: reviews
        subset: v3
      weight: 50
设置请求超时

你可以为虚拟服务中的路由规则设置超时,默认的HTTP调用超时为15秒。

下面的例子把reviews服务的timeout设置为1ms,这意味着访问该服务的客户端都会基本都会收到超时错误:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# kubectl edit virtualservice reviews
 
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: reviews
  namespace: default
spec:
  hosts:
  - reviews
  http:
  - route:
    - destination:
        host: reviews
        subset: v1
    # 立即超时
    timeout: 1ms

此时再访问/productpage,会提示product reviews are currently unavailable。

使用HTTPS网关

istio-ingressgateway负责处理Gateway资源,就像K8S中Ingress Controller负责处理Ingress资源那样。

istio-ingressgateway默认已经同时支持HTTP/HTTPS,并在80/443端口上监听。

下面的例子创建一个HTTPS网关:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
cat <<EOF | kubectl apply -f -
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: httpbin-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 443
      name: https
      protocol: HTTPS
    tls:
      # 单向认证
      mode: SIMPLE
      # 下面指定istio-ingressgateway的Pod的一个文件系统路径,提示HTTPS所需的证书、私钥的所在位置
      # 默认情况下,网关控制器会将保密字典istio-ingressgateway-certs挂载到/etc/istio/ingressgateway-certs目录下
      # 但是此保密字典默认并不创建,挂载也是可选的(optional: true),本文在《安装到K8S》一章已经创建了保密字典
 
      # 如果你要多套数字证书需要使用,则必须在部署Charts时指定:
      #   --set gateways.istio-ingressgateway.secretVolumes[N].name=ingressgateway-*-certs \
      #   --set gateways.istio-ingressgateway.secretVolumes[N].secretName=istio-ingressgateway-*-certs \
      #   --set gateways.istio-ingressgateway.secretVolumes[N].mountPath=/etc/istio/ingressgateway-*-certs \
      #   ...
      # 来挂载所有数字证书,添加新证书后,deployment istio-ingressgateway必须被编辑以使用新证书,对应Pod会重启
      # 使用通配符证书以免除此麻烦
      serverCertificate: /etc/istio/ingressgateway-certs/tls.crt
      privateKey: /etc/istio/ingressgateway-certs/tls.key
      # 如果mode为MUTUAL则需要提供CA证书位置
      caCertificates: /etc/istio/ingressgateway-certs/ca.crt
    hosts:
    - "httpbin.k8s.gmem.cc"
EOF

你总是需要一个VirtualService来配合Gateway:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
cat <<EOF | kubectl apply -f -
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: httpbin
spec:
  hosts:
  - "httpbin.k8s.gmem.cc"
  gateways:
  - httpbin-gateway
  http:
  - match:
    - uri:
        prefix: /status
    - uri:
        prefix: /delay
    route:
    - destination:
        port:
          number: 8000
        host: httpbin
EOF

注意SSL Termination在网关,后端服务不需要启用HTTPS。

完成上述步骤后,访问https://httpbin.k8s.gmem.cc/status/418可以看到如下输出:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
                                                                  # 这里是入口网关的IP
curl -HHost:httpbin.k8s.gmem.cc --resolve httpbin.k8s.gmem.cc:443:10.102.172.91 https://httpbin.k8s.gmem.cc/status/418
 
#   -=[ teapot ]=-
 
#      _...._
#    .'  _ _ `.
#   | ."` ^ `". _,
#   \_;`"---"`|//
#     |       ;/
#     \_     _/
#       `"""`
使用双向认证网关

和上一节的单向认证类似,但双向(mutual TLS )认证需要额外为入口网关控制器提供一个CA证书,用于校验客户端的数字证书。

使用官方Istio的Chart安装时,它默认从保密字典istio-ingressgateway-ca-certs加载CA证书,且挂载到/etc/istio/ingressgateway-ca-certs。

参考下面的代码修改Gateway的定义:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
cat <<EOF | kubectl apply -f -
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: httpbin-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 443
      name: https
      protocol: HTTPS
    tls:
      # 双向认证
      mode: MUTUAL
      # 客户端证书和私钥
      serverCertificate: /etc/istio/ingressgateway-certs/tls.crt
      privateKey: /etc/istio/ingressgateway-certs/tls.key
      # 服务器用来校验客户端证书的CA证书
      caCertificates: /etc/istio/ingressgateway-ca-certs/ca-chain.cert.pem
    hosts:
    - "httpbin.k8s.gmem.cc"
EOF

然后,通过CA为客户端签发证书,并在访问服务器时指定自己的密钥和此证书:

Shell
1
curl ... --cacert /home/alex/Documents/puTTY/ca.crt --cert alex.cert --key alex.key ... 
控制出口流量

要从服务网格内访问外部URL,需要配置绕过代理的IP范围,或者配置出口网关,否则无法访问仅仅能得到404:

Shell
1
2
3
4
SLEEP_POD=`kubectl get pod -l app=sleep -o jsonpath={.items..metadata.name}`
 
kubectl exec -it $SLEEP_POD -c sleep -- curl -o /dev/null -s -w "%{http_code}\n" http://gmem.cc 
# 404

下面是通过出口网关访问gmem.cc的例子:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
cat <<EOF | kubectl apply -f -
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: gmem-ext
spec:
  hosts:
  - gmem.cc
  ports:
  - number: 443
    name: https
    protocol: HTTPS
  # 如果解析名称:
  #   NONE,假设入站连接已经被解析。也就是说必须路由给请求发起者已经解析出的那个IP
  #   STATIC,使用Endpoint中定义的静态IP地址
  #   DNS,通过DNS服务器解析。可能转发给不同的IP(与请求者的解析结果不同)
  resolution: DNS
  # 服务位置:
  #   MESH_EXTERNAL,提示目标服务位于网格外部
  #   MESH_INTERNAL,提示目标服务位于网格内部。用于那些手工加入的服务,例如手工加入到K8S网格的VM
  #                  对于网格外部的服务,必须明确指定,否则Kiali会显示Unkown节点
  location: MESH_EXTERNAL
EOF

现在再尝试上面的kubectl exec命令,会发现能正常获得响应。 

当访问外部HTTPS服务(TLS协议)时,可能需要同时创建ServiceEntry + VirtualService。此外如同访问网格内部服务一样,你可以通过VirtualService对外部服务进行流量管理。

下面的例子,允许网格内部访问mynewproddb.idc2提供的MySQL服务:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: mynewproddb
spec:
  hosts:
  - mynewproddb.idc2
  ports:
  - name: tcp
    number: 3306
    protocol: TCP
  resolution: NONE
  location: MESH_EXTERNAL
断路器

断路器(和异常检测)是创建韧性微服务应用所常用的一种模式,可以让应用程序免受上游(Upstream,即调用链中更加远离根的节点)服务失败、延迟峰值(latency spikes)或其它网络异常的侵害。

下面的示例配置了一个断路器,它在客户端访问httpbin服务时生效:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
cat <<EOF | kubectl apply -f -
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: httpbin
spec:
  host: httpbin
  trafficPolicy:
    # 在客户端Sidecar中维护连接池,包含断路器配置(负载限制)
    connectionPool:
      # 对应Envoy的断路器配置
      tcp:
        # 最大TCP连接数
        maxConnections: 1
      http:
        # 最大等待转发的、从应用容器发来的HTTP请求数
        http1MaxPendingRequests: 1
        # 每个TCP连接最多可以处理的HTTP请求数
        maxRequestsPerConnection: 1
    # 异常检测,用于剔除不正常的上游服务的实例
    outlierDetection:
      # 将实例从负载均衡池中剔除,需要连续的错误(HTTP 5XX或者TCP断开/超时)次数
      consecutiveErrors: 1
      # 分析是否需要剔除的频率,多久分析一次
      interval: 1s
      # 实例被剔除后,至少多久不得返回负载均衡池
      baseEjectionTime: 3m
      # 负载均衡池中最多有多大比例被剔除
      maxEjectionPercent: 100
EOF

登陆到fortio来访问受断路器控制的httpbin服务: 

Shell
1
2
3
FORTIO_POD=`kubectl get pod -l app=fortio -o  jsonpath={.items..metadata.name}`
                                           # 负载测试   # 并发2,不限QPS,执行20次    
kubectl exec -it $FORTIO_POD  -c fortio -- fortio load -c 2 -qps 0 -n 20 -loglevel Error http://httpbin:8000/get

上面的fortio命令并发度为2,而断路器仅仅允许1个并发请求,因此会有几率出现错误:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
Fortio 1.0.1 running at 0 queries per second, 4->4 procs, for 20 calls: http://httpbin:8000/get
Aggregated Function Time : count 20 avg 0.022291169 +/- 0.03153 min 0.000243608 max 0.086822606 sum 0.44582338
# target 50% 0.0055
# target 75% 0.0225
# target 90% 0.0834113
# target 99% 0.0864815
# target 99.9% 0.0867885
Sockets used: 5 (for perfect keepalive, would be 2)
# 错误率15%
Code 200 : 17 (85.0 %)
Code 503 : 3 (15.0 %)
All done 20 calls (plus 0 warmup) 22.291 ms avg, 86.0 qps

如果将并发度设置为20,连续发送1000次,则错误率飙高到90%:

Shell
1
2
Code 200 : 94 (9.4 %)
Code 503 : 906 (90.6 %)

查询客户端Proxy的统计信息,可以看到更多细节:

Shell
1
2
3
4
5
6
7
kubectl exec -it $FORTIO_POD  -c istio-proxy  -- sh -c 'curl localhost:15000/stats' | grep httpbin | grep pending
 
# cluster.outbound|8000||httpbin.default.svc.k8s.gmem.cc.upstream_rq_pending_active: 0
# cluster.outbound|8000||httpbin.default.svc.k8s.gmem.cc.upstream_rq_pending_failure_eject: 0
# pstream_rq_pending_overflow提示被断路掉的请求次数
# cluster.outbound|8000||httpbin.default.svc.k8s.gmem.cc.upstream_rq_pending_overflow: 10326
# cluster.outbound|8000||httpbin.default.svc.k8s.gmem.cc.upstream_rq_pending_total: 1016
流量复制

下面的虚拟服务,将所有请求发送给v1,同时镜像一份给v2:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cat <<EOF | kubectl apply -f -
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: httpbin
spec:
  hosts:
    - httpbin
  http:
  - route:
    - destination:
        host: httpbin
        subset: v1
      weight: 100
    # 流量镜像
    mirror:
      host: httpbin
      subset: v2
EOF

发送给镜像服务的请求,其Host/Authority请求头被自动加上-shadow后缀。 

试验遥测功能

利用Mixer和Sidercar,可以获得服务网格的各种统计指标(Metrics)、日志,或者跨越不同的服务进行分布式的调用链追踪。

收集指标和日志

通过配置,Istio可以自动收集网格中某个服务的统计指标,或者为每次请求产生日志。

下面的例子,为每个请求生成一个Prometheus指标值:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# 定义一个指标(Metrics)类型的instances的规格,将Istio请求的属性映射为包含维度、度量的指标
# 针对每次请求,都会根据这里的配置,生成一个metrics类型的实例
apiVersion: "config.istio.io/v1alpha2"
kind: metric
metadata:
  # 名称
  name: doublerequestcount
  namespace: istio-system
spec:
  # 度量信息,为固定值2
  value: "2"
  # 维度信息
  dimensions:
    # 维度名   维度值
    # context,source,destination等都是预定义的Istio请求属性
    reporter: conditional((context.reporter.kind | "inbound") == "outbound", "client", "server")
    #                            竖线用于提供默认值
    source: source.workload.name | "unknown"
    destination: destination.workload.name | "unknown"
    #        除了请求属性表达式外,还可以使用字面值
    message: '"twice the fun!"'
  monitored_resource_type: '"UNSPECIFIED"'
 
---
# 配置一个prometheus适配器,也叫处理器(Handler)
# 适配器能够处理instances并和输出到外部系统
apiVersion: "config.istio.io/v1alpha2"
kind: prometheus
metadata:
  name: doublehandler
  namespace: istio-system
spec:
  metrics:
    # 生成的Prometheus指标的名称,注意,prometheus适配器总是自动添加istio_前缀
  - name: double_request_count
    # instance规格的全限定名称,提供当前适配器的输入
    instance_name: doublerequestcount.metric.istio-system
    # 指标的类型
    kind: COUNTER
    # 产生哪些标签
    label_names:
      # 这些维度作为标签
    - reporter
    - source
    - destination
    - message
 
---
# 将doublerequestcount.metric的所有instance派发给适配器prometheus
apiVersion: "config.istio.io/v1alpha2"
kind: rule
metadata:
  name: doubleprom
  # 定义在默认配置命名空间,且没有match配置,因此会派发所有instance
  namespace: istio-system
spec:
  actions:
  - handler: doublehandler.prometheus
    instances:
    - doublerequestcount.metric

下面的例子,为每个请求产生一条日志:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# 定义另外一种instance —— 日志条目 —— 的规格
apiVersion: "config.istio.io/v1alpha2"
kind: logentry
metadata:
  name: newlog
  namespace: istio-system
spec:
  # 日志严重度级别
  severity: '"warning"'
  # 日志时间戳
  timestamp: request.time
  # 日志变量
  variables:
    source: source.labels["app"] | source.workload.name | "unknown"
    user: source.user | "unknown"
    destination: destination.labels["app"] | destination.workload.name | "unknown"
    responseCode: response.code | 0
    responseSize: response.size | 0
    latency: response.duration | "0ms"
  monitored_resource_type: '"UNSPECIFIED"'
---
 
# 这种处理器处理instance并输出到标准输出
apiVersion: "config.istio.io/v1alpha2"
kind: stdio
metadata:
  name: newhandler
  namespace: istio-system
spec:
severity_levels:
   # 将instance.severity == warning映射为Params.Level.WARNING这个日志级别
   warning: 1
# 生成JSON格式的日志
outputAsJson: true
 
---
# 将logentry的所有实例发送给stdio处理
apiVersion: "config.istio.io/v1alpha2"
kind: rule
metadata:
  name: newlogstdio
  namespace: istio-system
spec:
  # 匹配所有请求
  match: "true"
  actions:
   - handler: newhandler.stdio
     instances:
     - newlog.logentry
---

 将上述两端配置文件保存为yaml文件,并通过kubectl apply命令安装到K8S,然后进行下一步试验。 

查询指标

打开Istio内置Prometheus服务的Web界面, 输入istio_double_request_count进行查询,可以看到每个微服务的请求次数都被client、server方分别报送,并预先聚合了。

实际上Istio官方提供的Chart中,以及提供了HTTP/TCP请求的次数、持续时间、请求长度、应答长度,以及访问日志的instance/handler/rule配置,可以直接安装使用。

指标可视化

打开Istio内置Grafana服务的Web界面,可以看到Istio目录下预定义好了7个仪表盘页面。该服务使用grafana/grafana:5.0.4版本的镜像。

试验策略功能
启用请求限速

参考配置请求限速一节。

禁止访问

你可以使用Mixer中可见的任何请求属性来控制对服务的访问。下面的例子禁用了对ratings:v3的访问:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 这种适配器用于提示Envoy拒绝访问
apiVersion: "config.istio.io/v1alpha2"
kind: denier
metadata:
  name: denyreviewsv3handler
spec:
  status:
    # 状态码和消息
    code: 7
    message: Not allowed
---
# instance
apiVersion: "config.istio.io/v1alpha2"
kind: checknothing
metadata:
  name: denyreviewsv3request
spec:
---
# rule
apiVersion: "config.istio.io/v1alpha2"
kind: rule
metadata:
  name: denyreviewsv3
spec:
  # 只要是reviews访问ratings的v3版本,立即拒绝
  match: destination.labels["app"] == "ratings" && source.labels["app"]=="reviews" && source.labels["version"] == "v3"
  actions:
  - handler: denyreviewsv3handler.denier
    instances: [ denyreviewsv3request.checknothing ]
黑白名单

Istio也支持基于请求属性的黑白名单,下面的例子和上面的denier是等价的:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 列表检查器,值匹配列表项则进入黑或白名单
apiVersion: config.istio.io/v1alpha2
kind: listchecker
metadata:
  name: whitelist
spec:
  overrides: ["v1", "v2"]  # 静态值列表
  # 这是白名单而非黑名单
  blacklist: false        
 
---
# 列表条目instance,从请求抽取单个值
apiVersion: config.istio.io/v1alpha2
kind: listentry
metadata:
  name: appversion
spec:
  # 取版本号
  value: destination.labels["version"]
 
---
# 关联两者,效果就是检查版本号,如果不是v1/v2则拒绝访问
apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
  name: checkversion
spec:
  match: destination.labels["app"] == "ratings"
  actions:
  - handler: whitelist.listchecker
    instances:
    - appversion.listentry&nbsp;
试验分布式追踪

以--set tracing.enabled=true选项安装Istio Chart,即可使用。由于默认设置的采样率较低,你需要反复访问多次/productpage才能捕获到Trace。

Search

打开Istio内置Jaeger服务的Web界面,点击顶部按钮切换到Search页签,在Find Traces面板中Service选取productpage,点击Find Traces,看到如下的Trace列表:

jaeger-search

点击其中一个Trace,可以查看其调用链信息:

jaeger-search-2

可以看到,这个由单次/productpage请求产生的调用链由6个Span构成,每个Span对应一个彩色条带。每个Span消耗的时间都标注在界面上了。

Dependencies

打开Istio内置Jaeger服务的Web界面,点击顶部按钮切换到Dependencies页签,可以看到Jaeger依据Traces计算出的服务依赖关系图(DAG,有向无环图):

jaeger-dependencies

 

Trace如何收集

尽管Istio的Sidecar能够自动发送Span给Jaeger,要将这些Span合并为完整的Trace还需要额外的信息,应用程序必须提供必要的HTTP请求头。

每个微服务都需要收集调用它的HTTP请求的x-request-id、x-b3-traceid、x-b3-spanid、x-b3-parentspanid、x-b3-sampled、x-b3-flags、x-ot-span-context头,并在它调用任何上游(Upstream,上图的ratings是reviews的Upstream)微服务时,带上这些头。

Bookinfo中,productpage服务基于Python编写,收集请求头的代码如下:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def getForwardHeaders(request):
    headers = {}
 
    if 'user' in session:
        headers['end-user'] = session['user']
 
    incoming_headers = [ 'x-request-id',
                         'x-b3-traceid',
                         'x-b3-spanid',
                         'x-b3-parentspanid',
                         'x-b3-sampled',
                         'x-b3-flags',
                         'x-ot-span-context'
    ]
 
    for ihdr in incoming_headers:
        val = request.headers.get(ihdr)
        if val is not None:
            headers[ihdr] = val
 
    return headers

reviews服务则基于Java编写, 收集请求头的代码如下:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@GET
@Path("/reviews/{productId}")
public Response bookReviewsById(@PathParam("productId") int productId,
                            @HeaderParam("end-user") String user,
                            @HeaderParam("x-request-id") String xreq,
                            @HeaderParam("x-b3-traceid") String xtraceid,
                            @HeaderParam("x-b3-spanid") String xspanid,
                            @HeaderParam("x-b3-parentspanid") String xparentspanid,
                            @HeaderParam("x-b3-sampled") String xsampled,
                            @HeaderParam("x-b3-flags") String xflags,
                            @HeaderParam("x-ot-span-context") String xotspan) {
  int starsReviewer1 = -1;
  int starsReviewer2 = -1;
 
  if (ratings_enabled) {
    JsonObject ratingsResponse = getRatings(Integer.toString(productId), u
      ser, xreq, xtraceid, xspanid, xparentspanid, xsampled, xflags, xotspan);
  }
}
关于采样率

Istio默认会追踪任何请求, 对于开发、测试环境这是可以的,线上环境通常需要降低采样率,否则压力太大。调整采样率有两种方式:

  1. 使用官方Chart安装时,调整pilot.traceSampling,当前使用的Chart版本已经将其设置为1.0,即百分之一的采样率
  2. 修改Deployment istio-pilot的环境变量PILOT_TRACE_SAMPLING

采样率的精度是0.01

删除示例应用
Shell
1
2
3
4
samples/bookinfo/platform/kube/cleanup.sh
kubectl delete -f samples/httpbin/httpbin.yaml
kubectl delete -f  samples/httpbin/sample-client/fortio-deploy.yaml
kubectl delete -f samples/sleep/sleep.yaml
流量管理基础

Istio的流量管理模型,从根本上将流量(traffic flow)和基础设施扩容(infrastructure scaling)进行了解耦。通过Pilot,你可以通过更细致的规则来说明流量如何流动,而不是简单的指定哪个VM/Pod来接收流量。例如,你可以指定某个服务的5%的流量发往金丝雀版本(不管金丝雀的实例数量),或者指定根据请求内容来选择特定版本的服务:

将流量(traffic flow)和基础设施扩容解耦后,Istio可以在应用程序代码外部提供大量的流量管理特性,包括AB测试的请求路由、逐步发布、金丝雀发布,基于超时、重试、断路器的故障恢复,以及为了测试故障恢复策略而进行的故障注入(fault injection)。

Pilot和Envoy

流量管理的核心组件是Pilot,它管理、配置所有部署在服务网格中的Envoy代理实例,对Envoy的完整生命周期负责。Pilot除了可以配置路由规则、配置故障恢复特性之外,还维护网格中所有服务的规范(canonical)模型。Envoy就是基于此模型,通过自己的发现服务,找到网格中其它Envoy。

每个Envoy实例都基于从Pilot得到的信息、以及针对负载均衡池中的实例进行周期性的健康检查,来维护负载均衡信息。根据此负载均衡信息,Envoy能够在遵守路由规则的同时,智能的在目的实例之间分发请求。

规范模型是独立于底层平台的,和底层平台的对接由Pilot中的适配器负责,适配器调用底层平台的API(例如K8S适配器实现了必要的控制器来Watch API Server,获取Pod、Ingress等资源的变动),并产生适当的规范模型。

Pilot启用了服务发现,路由表,以及对负载均衡池的动态更新。

使用Pilot的规则配置(Rule Configuration),可以制定高级别的流量管理规则。这些规则会被转换为低级别的配置信息,并分发给Envoy实例。

请求路由
服务版本

如上文所述,网格中服务的规范模型由Pilot维护。Istio还引入了服务版本的概念,用于对服务实例进行细粒度区分。服务版本既可以指服务API的版本(v1,v2),也可以指服务运行环境(stg,pdt)或任何其它属性。

在上图中,服务的客户端对服务的不同版本毫无感知。客户端仍然使用服务的IP或主机名来访问,只是Envoy在客户端-服务器之间拦截并转发了请求和响应。

Envoy根据你通过Pilot指定的路由规则,决定实际使用的服务版本。这让应用程序代码和被依赖服务之间实现解耦,可分别独立演化。在路由规则中,你可以指定让Envoy依据源/目标的消息头、Tag来判定服务版本。

入口/出口

Istio假设服务网格的所有出入流量都经由Envoy代理转发。通过这种前置代理可以实现面向用户的服务,包括AB测试、金丝雀部署。在访问外部服务时,Envoy可以实现必要的故障恢复特性,包括超时、重试,并获取外部服务的性能指标。

发现和负载均衡

Istio能够在网格中服务实例之间进行负载均衡。 

Istio假设基础设置提供一个服务注册表(service registry,例如K8S的Service),此表跟踪某个服务的VM/Pod集。此服务注册表应当负责新实例的加入、不健康实例的移除。

Istio从服务注册表中读取服务的信息,并生成平台无关的服务发现接口。Envoy执行服务发现(基于xDS和Pilot交互),并动态的更新其本地负载均衡池。

网格中的服务,利用DNS名称来相互访问。所有针对某个服务的流量,都会在出站前经由Envoy进行重新路由。负载均衡池是决定路由到哪个节点的关键因素。Envoy支持多种负载均衡算法,但是Istio目前仅仅允许:轮询、随机、带权重的最少请求。

Envoy还会周期性的检查负载均衡池中实例的健康情况,这种检查从外部(准确的说是服务消费者)发出,而不是像K8S Pod健康检查那样,在Pod本地执行。

判断实例是否健康时,Envoy遵循一种断路器模式(circuit breaker pattern),此模式下调用实例API的故障率决定了实例是否健康。当健康检查连续失败指定的次数后,实例被移除负载均衡池;当被移除的实例连续成功指定次数后,它又回到负载均衡池。

如果实例以503代码响应健康检查请求,则它会立即被调用者移出负载均衡池。

错误处理

Envoy提供了一系列开箱即用的错误恢复机制:

  1. 超时
  2. 有限次数的重试,支持可变的重试延迟(jitter)
  3. 限制针对上游服务的并发连接数、并发请求数
  4. 主动健康检查——针对负载均衡池中所有成员进行健康检查,并移除不健康实例,移回健康实例
  5. 可精细控制的断路器(被动健康检查) ,同样针对负载均衡池中的每个实例

所有这些特性,都可以在运行时,利用Istio的流量管理规则动态配置。

联用主动、被动健康检查,可以更大程度上降低访问到不健康实例的几率。联用底层基础设施的健康检查机制,不健康实例可以很快的被剔除。 

利用流量管理规则,你可以为每个服务/版本来配置默认的故障恢复特性。

故障注入

用于辅助测试端对端的故障恢复能力。

Istio支持多种具体的协议,并向其注入错误信息。而不是粗暴的杀死Pod,或者在TCP层延迟/污染网络包。你可以在应用层注入更加有意义的错误,例如HTTP的状态码。

Istio支持对特定请求进行错误注入,或者指定多少百分比的请求被错误注入。

可以注入的错误有两类:

  1. 延迟:模拟网络延迟和上游服务过载
  2. 中止:模拟上游服务错误,通常使用HTTP错误码、TCP连接错误的方式
流量镜像/复制

流量镜像(Traffic mirroring)也叫shadowing,允许开发团队在最小风险的前提下改变产品的功能。其做法是将线上环境的流量复制到一个“镜像服务“中,镜像服务和主请求处理链是隔离的。

VirtualService支持为route配置mirror,实现流量镜像。

网关

网关是运行在服务网格边缘的负载均衡器,它处理入站或出站的TCP/HTTP连接。

Istio提供入口网关,出口网关,对应:

  1. 的CRD为Gateway、Gateway+ServiceEntry
  2. 的Controller为istio-ingressgateway、istio-egressgateway

当提起术语“网关”时,基于上下文可能指Gateway这种资源,也可能指出口/入口网关的Service/Pod。

IngressGateway

在典型的Kubernetes环境下,Ingress控制器读取Ingress资源,提供外部访问K8S集群的入口。

而使用Istio时,Ingress资源的地位可以由Gateway + VirtualServices代替。在集群内部组件相互交互时,不需要配置Gateway资源。需要注意,Istio也可以作为Ingress控制器,处理K8S原生的Ingress资源。

使用Istio Gateway时的典型的客户端访问时序如下:

  1. 客户端发起针对负载均衡器的请求
  2. 负载均衡器将请求转发给集群的istio-ingressgateway服务,该服务可以部署为NodePort,它的后端是一个Deployment
  3. IngressGateway根据Gateway资源、VirtualService资源的配置信息,将请求路由给K8S的Service
    1. Gateway提供端口、协议、数字证书信息
    2. VirtualService提供到K8S Service的路由信息
  4. K8S Service将请求路由给Pod

该时序如下图所示,注意IngressGateway的Sidecar负责转发客户端请求:

istio-networking

 

每当你创建/修改Gateway、VirtualService资源,Pilot会监测到并将其转换为Envoy配置,然后发送给相关的Sidecar,包括运行在IngressGateway Pod中的Envoy。

EgressGateway

默认情况下,启用了Istio的微服务无法访问集群外部的URL, 这是因为Envoy修改了Pod所在网络命名空间的Iptables,透明的将所有出口流量都重定向到Sidecar,而不经任何配置Sidecar只会处理网格内部的Destination。

要将外部服务暴露给启用了Sidecar的网格内部Pod/VM(它们是外部服务的客户端),必须:

  1. 定义ServiceEntry,这种自定义资源允许针对指定的外部IP/DNS名称的流量通过Sidecar。ServiceEntry支持通配符匹配主机,可以降低配置工作量
  2. 或者,配置一个IP范围,让针对此IP范围的出站请求经过/绕过Envoy代理。你可以设置Chart变量,
    Shell
    1
    2
    # 仅仅10.5.0.0/16走Envoy代理,其它直连。你可以将此Pod网络、K8S服务网络的CIDR配置到该字段中。这样,所有针对网格外部的访问都开放了
    --global.proxy.includeIPRanges="10.5.0.0/16"

    则当从网格内部访问10.5网段时直接绕过代理。
    注意:此特性已经弃用,可以使用配置字典istio-sidecar-injector中的的includeOutboundIpRanges,或者Pod注解traffic.sidecar.istio.io/includeOutboundIPRanges代替

强调一下,访问集群内部域名、IP地址不受限制,即使不配置VirtualService也可以访问,而且Kiali直接可以识别出这种流量。

ServiceEntry还可以配合Gateway+VirtualService,将出口流量导向特殊的服务 —— istio-egressgateway,这种配置方式的适用场景是:

  1. 离开网格的所有流量必须流经一组专用节点,这一组节点有特殊的监控和审查
  2. 集群中的一般性节点不能联通网格外部
流量管理配置

Istio使用一个简单的配置模型来描述API调用/TCP流量应该如何在服务网格中流动。使用此配置模型,你可以:

  1. 配置服务级别的属性,例如断路器、超时、重试
  2. 配置通用的持续交付(CD)任务,例如金丝雀发布(Canary rollout,即灰度发布)、A/B测试、阶段性部署(Staged rollout)——基于百分比的流量分配 

Istio的配置模型映射为K8S的资源,包括VirtualService、DestinationRule、ServiceEntry、Gateway四种。在K8S中你可以用kubectl来配置这些资源,它们都是CR。

VirtualService

定义服务的请求如何在服务网格中路由。虚拟服务能够将请求路由给服务的不同版本,甚至是完全不同的服务。

VirtualService通过Istio的服务注册表定义了如何路由到一个或多个特定的Destinations(目标服务)。VirtualService可以根据请求的属性(如HTTP头、路径等)来匹配请求,并根据这些规则将流量路由到不同的目标。这意味着,对于给定的域名,基于请求的不同属性(比如URL路径),VirtualService可以将流量路由到多个不同的Destination。

虚拟服务自身不会在Istio服务注册表中添加新的服务条目。它的spec.hosts,用来指定进行路由时,如何匹配主机名,这通常对应HTTP请求的Host头。

Istio服务注册表中服务条目的来源包括:

  1. K8s服务
  2. ServiceEntry定义的服务
  3. Consul、Eureka等服务发现机制暴露的服务
  4. 在某些情况下,服务信息可以通过Istio的配置文件静态定义。这种方式较少用于生产环境,但可能在测试或特定场景下有其用途
按版本路由

下面的示例,将针对review服务的请求,3/4发给v1版本,1/4发给v2版本:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  # 虚拟服务的名字
  name: reviews
spec:
  hosts:
  # 访问到此虚拟服务时,使用主机名,可以是DNS名称、带通配符的DNS名称、IP地址
  # DNS名称可以是FQDN,对于K8S这样的平台,可以使用短DNS名称(无点号),例如使用reviews代替reviews.default.svc.cluster.local
  # Istio会根据此虚拟服务(而非K8S的Service)所在的命名空间,生成FQDN
  # 注意:
  # 1、每个host只能被单个虚拟服务声明。单个虚拟服务可以用于描述多个HTTP/TCP端口的流量属性
  # 2、对于HTTP/TCP流量,hosts字段均生效
  # 3、对于网格内部的服务(存在于服务注册表),必须总是使用服务DNS名,不得使用IP地址
  - reviews
  http:
  # 路由规则的数组
  - route:
    - destination:
        # host必须是Istio服务注册表中,没有歧义的名称。此注册表中的服务来源包括:
        # 1、底层平台(K8S)服务注册表的全部条目(即K8S Service)
        # 2、通过ServiceEntry声明的服务条目
        #
        # 对于K8S用户需要注意,使用DNS短名时,Istio将根据DestinationRule的命名空间来生成FQDN
        # 而不是当前VirtualService的命名空间
        host: reviews
        # 发送给reviews服务实例的v1子集
        # 这里仅仅指出子集的名称,子集的规格通过DestinationRule配置
        subset: v1
      # 路由到此版本的权重,取值0-100
      weight: 75
    - destination:
        host: reviews
        # 如果目标服务仅仅暴露单个端口,则不需要声明下面这个字段
        port: 80
        subset: v2
      weight: 25
超时和重试

默认情况下HTTP请求的超时为15秒,你可以覆盖此默认值。重试也类似:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
...
spec:
  ...
  http:
  - route:
    # 超时
    timeout: 10s
    # 重试
    retries:
      # 最多重试次数
      attempts: 3
      # 每次重试的超时
      perTryTimeout: 2s

也可以在请求级别覆盖超时、重试配置,只需要提供Envoy规定的特殊请求头x-envoy-upstream-rq-timeout-ms、x-envoy-max-retries即可。 

故障注入

你可以为http进行故障注入,要么是delay,要么是abort。

下面的例子,为ratings服务的v1版本10%的请求引入5秒的延迟:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: ratings
spec:
  hosts:
  - ratings
  http:
  - fault:
      delay:
        # 多少比例的请求被注入故障
        percent: 10
        # 引入固定5秒的延迟
        fixedDelay: 5s
    route:
    - destination:
        host: ratings
        subset: v1

下面的例子,则为10%的请求引发400错误:

YAML
1
2
3
4
5
6
7
8
9
10
...
spec:
  ...
  http:
  - fault:
      abort:
        # 多少比例的请求被注入故障
        percent: 10
        # 返回的HTTP状态码
        httpStatus: 400

delay和abort是可以联合使用的:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...
metadata:
  name: ratings
spec:
  http:
  # 从reviews服务v2版本发起的,针对ratings服务v1版本的请求
  - match:
    - sourceLabels:
        app: reviews
        version: v2
    fault:
      # 引入5秒延迟
      delay:
        fixedDelay: 5s
      # 为10%请求引发400错误
      abort:
        percent: 10
        httpStatus: 400
有条件路由

规则可以仅仅针对满足特定条件的请求。

可以限定请求者的标签,在K8S环境下即Pod的Label:

YAML
1
2
3
4
5
6
7
8
9
...
spec:
  http:
  # 仅针对reviews服务v2版本发起的请求
  - match:
    - sourceLabels:
        app: reviews
        version: v2
    route: ...

也可以限定HTTP请求头: 

YAML
1
2
3
4
5
6
7
8
...
spec:
  http:
  - match:
    - headers:
        # end-user头必须为alex
        end-user:
          exact: alex

还可以限定请求URL路径、HTTP方法、HTTP Scheme、HTTP Authority值: 

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
spec:
  http:
  - match:
    - uri:
        # 请求URL,除去scheme://host部分,必须以/api/v1开头
        # 除了prefix,还可以配置
        #   exact,表示精确匹配
        #   regex,表示正则式匹配
        prefix: /api/v1
 
    - scheme:
        exact: https
    - authority:
        regex: ".*.gmem.cc"
    - method:
        regex: "GET|POST"

多个条件可以进行逻辑与/或。在单个match元素中声明多个条件,则为AND:

YAML
1
2
3
4
5
6
7
8
9
10
spec:
  http:
  - match:
    # 同时限定Pod标签、请求头
    - sourceLabels:
        app: reviews
        version: v2
      headers:
        end-user:
          exact: alex

在多个match元素中分别声明,则为OR:

YAML
1
2
3
4
5
6
7
8
9
10
11
spec:
  http:
  - match:
    # Pod标签匹配
    - sourceLabels:
        app: reviews
        version: v2
    # 或者请求头匹配
    - headers:
        end-user:
          exact: alex
多规则优先级

如果对于相同的目的地(host + subset),配置了两个或更多的规则(http子元素),那么第一个(配置文件最前面)匹配的规则优先级最高。 

CORS配置

Istio支持为虚拟服务配置跨源资源共享策略:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
spec:
  http:
  - route:
    corsPolicy:
      # 允许发起CORS请求的源,设置为*则允许任何源
      allowOrigin:
      - gmem.cc
      # CORS请求可以使用的HTTP方法
      allowMethods:
      - POST
      - GET
      # Access-Control-Allow-Credentials头
      allowCredentials: false
      # 允许的请求头
      allowHeaders:
      - X-Foo-Bar
      # preflight request可以被缓存的时间
      maxAge: "1d" 
请求重定向

虚拟服务可以直接返回301重定向给调用者:

YAML
1
2
3
4
5
6
7
spec:
  http:
  redirect:
    # 重定向时覆盖URL查询路径
    uri: /v1/bookRatings
    # 重定向时覆盖URL的Authority(Host)部分
    authority: newratings.default.svc.cluster.local
请求重写

虚拟服务可以在转发请求给Dest之前,对HTTP请求的特定部分进行修改:

YAML
1
2
3
4
5
6
7
8
9
spec:
  http:
  - match:
    - uri:
        prefix: /ratings
    # 将URL前缀从/ratings修改为/v1/bookRatings
    rewrite:
      uri: /v1/bookRatings
      authority: ...
流量复制

参考上文的例子。

TCP服务

这里是一个TCP路由的例子:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: tcp-route
  namespace: default
spec:
  hosts:
  - mesh.gmem.cc
  tcp:
  - match:
    - port: 3306
      source_labels:
        app: mysql-client
    route:
    - destination:
        host: mysql
        subset: v2
        port:
          number: 3306
      weight: 100
  - match:
    - port: 2181
    route:
    - destination:
        host: zk
        subset: v1
        port:
          number: 2181
      weight: 100
DestinationRule

在VirtualService定义了路由规则后,DestinationRule进一步应用一系列的策略到请求上。DestinationRule定义了对于特定目标服务的流量策略,如负载均衡策略、连接池大小、TLS设置等。当VirtualService根据规则将流量路由到特定目标后,DestinationRule定义了如何处理到达这些目标的流量。

DestinationRule中的spec.host同样是指定这个规则应用到哪些请求,不会在Istio注册表中定义服务条目。

服务子集

下面的示例,声明了服务子集:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: reviews
spec:
  # 访问什么服务时应用此规则
  host: reviews
  # 配置服务的子集
  # 在K8S中,服务的具有标签version=v1的Pod,并加入到v1子集
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2
简单负载均衡

可以针对所有子集配置:

YAML
1
2
3
4
5
6
7
8
9
10
11
...
spec:
  host: reviews
  trafficPolicy:
    # 负载均衡策略
    loadBalancer:
      # 算法:ROUND_ROBIN(默认)
      #      LEAST_CONN, O(1)复杂度算法,随机选取两个健康实例,并取其中活动请求少的
      #      RANDOM
      #      PASSTHROUGH,直接转发给调用者请求的IP地址,不进行任何负载均衡
      simple: RANDOM 

也可以针对特定子集:

YAML
1
2
3
4
5
6
7
8
spec:
  subsets:
  - name: v2
    labels:
      version: v2
    trafficPolicy:
      loadBalancer:
        simple: ROUND_ROBIN
一致性哈希负载均衡

基于一致性哈希的负载均衡可以实现基于HTTP请求头、Cookie或其它属性的软的会话绑定(Session affinity):

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
   name: bookinfo-ratings
spec:
   host: ratings.prod.svc.cluster.local
   trafficPolicy:
     loadBalancer:
       consistentHash:
         # 三选一
         # 基于请求头进行哈希计算
         httpHeaderName: end-user
         # 基于源IP地址进行哈希计算
         useSourceIp: true
         # 基于Cookie进行哈希计算,如果Cookie不存在,Envoy会自动产生
         httpCookie:
           # Cookie的名称、路径、生命周期
           name: user
           path: ..
           ttl: 0s
并发限制

可以限制某个服务/子集的并发度:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
spec:
  subsets:
  - name: v1
    labels:
      version: v1
    trafficPolicy:
      # 限制并发
      # 每个Sidecar(也就是每个Pod)为每个上游服务的实例都配备一个连接池,断路器依赖此连接池才能工作
      # connectionPool可以有tcp、http两个字段
      connectionPool:
        tcp:
          # 最大连接数
          maxConnections: 100&nbsp;
          # 连接超时
          connectTimeout:
        http:
          # 最大等待处理的请求数量
          http1MaxPendingRequests: 1024
          # HTTP2最大请求数
          http2MaxRequests: 1024
          # 每个TCP连接可以被多少请求共享使用(重用),设置为1则禁用keep-alive
          maxRequestsPerConnection: 32
          # 最大重试次数
          maxRetries: 3 
断路器

可以为某个服务/子集配置断路器:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
spec:
  subsets:
  - name: v1
    labels:
      version: v1
    trafficPolicy:
      # 异常检测,断路
      outlierDetection:
        # 将实例从负载均衡池中剔除,需要连续的错误(HTTP 5XX或者TCP断开/超时)次数
        consecutiveErrors: 1
        # 分析是否需要剔除的频率,多久分析一次
        interval: 1s
        # 实例被剔除后,至少多久不得返回负载均衡池
        baseEjectionTime: 3m
        # 负载均衡池中最多有多大比例被剔除
        maxEjectionPercent: 100
ServiceEntry

用于访问不受网格管理的服务,将其作为服务条目添加到Istio内部管理的服务注册表(service registry)中。

ServiceEntry允许在Istio的内部服务注册表中手动添加条目,这些条目可以是网格内部的服务(MESH_INTERNAL)或网格外的服务(MESH_EXTERNAL)。这意味着你可以使Istio意识到那些不直接由Istio管理的服务。

对于MESH_INTERNAL类型的ServiceEntry,它用于添加网格内的服务到Istio的内部服务发现系统,这些服务可能由于某些原因(比如,它们运行在不同的Kubernetes集群或不同的网络命名空间中)而未被自动识别为网格服务。

简单示例

下面的例子,允许Envoy代理针对gmem.cc的HTTP请求:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: gmem-ext-svc
spec:
  hosts:
  # 可以使用通配符(仅仅针对网格外部服务),表示一个白名单,允许从服务网格内部访问它们
  # 注意,1.0.5貌似不支持通配符?
  - *.gmem.cc
  location: MESH_EXTERNAL
  resolution: DNS
  ports:
  - number: 80
    name: http
    protocol: HTTP
  - number: 443
    name: https
    protocol: HTTPS

ServiceEntry可以和VirtualService一起工作,作为VirtualService的host。这是因为从概念上来说,来自底层基础设施注册表的服务、ServiceEntry定义的服务,都是Istio的概念性的Service。

指定静态端点

下面的例子以主机名来静态的指定外部服务端点:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: external-svc-dns
spec:
  hosts:
  - foo.bar.com
  location: MESH_EXTERNAL
  ports:
  - number: 80
    name: https
    protocol: HTTP
  resolution: DNS
  # 端点列表
  endpoints:
  - address: us.foo.bar.com
    ports:
      https: 8080
  - address: uk.foo.bar.com
    ports:
      https: 9080
  - address: in.foo.bar.com
    ports:
      https: 7080

下面的例子以IP来指定外部服务端点:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: external-svc-mongocluster
spec:
  hosts:
  - mymongodb.somedomain
  # 下面的字段仅仅支持resolution=NONE|STATIC
  addresses:
  - mymongodb.ip/32
  ports:
  - number: 27018
    name: mongodb
    protocol: MONGO
  location: MESH_INTERNAL
  # 使用endpoints中定义的静态IP地址
  resolution: STATIC
  # 解析到:
  endpoints:
  - address: 2.2.2.2
  - address: 3.3.3.3

关于hosts、addresses,以及服务的“唯一性标识”,Istio官方API文档有如下说明:

  1. hosts字段:
    1. 对于非HTTP服务(包括不透明HTTPS)来说,此字段中指定的DNS名称被忽略
    2. 对于非HTTP服务,如果此字段指定的是DNS名称,则ports,或/和addresses中的IP地址用于唯一性的标识目的地
  2. addresses字段: 表示关联到此服务的虚拟IP地址,可以是CIDR前缀:
    1. 对于HTTP服务来说,此字段被忽略,目的地根据HTTP头Host/Authority确定
    2. 对于非HTTP服务,hosts头被忽略。如果指定了addresses,则入站请求的IP地址和addresses指定的CIDR进行匹配,如果匹配成功,则认为请求的是当前服务。如果不指定addresses,则入站流量仅仅根据ports来识别,这种情况下,服务的访问端口不能被网格内部任何其它服务共享

这段描述让人费解,也不符合实际试验结果。下面的两个ServiceEntry,都是非HTTP服务,都使用443端口,都没有指定addresses字段,但是没有任何冲突出现:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: cdn.gmem.cc
spec:
  hosts:
  - cdn.gmem.cc
  ports:
  - name: tcp
    number: 443
    protocol: TCP
  resolution: DNS
  location: MESH_EXTERNAL
 
---
 
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: blog.gmem.cc
spec:
  hosts:
  - blog.gmem.cc
  ports:
  - name: tcp
    number: 443
    protocol: TCP
  resolution: DNS
  location: MESH_EXTERNAL

生成的Envoy配置如下(可以看到集群、端点信息都是正常有效的):

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// istioctl proxy-config cluster ubuntu --fqdn=cdn.gmem.cc -o json
[
    {
        "name": "outbound|443||cdn.gmem.cc",
        "type": "STRICT_DNS",
        "hosts": [
            {
                "socketAddress": {
                    "address": "cdn.gmem.cc",
                    "portValue": 443
                }
            }
        ]
    }
]
 
// istioctl proxy-config cluster ubuntu --fqdn=blog.gmem.cc -o json
[                                                                                                            
    {                                                                                                        
        "name": "outbound|443||blog.gmem.cc",                                                                
        "type": "STRICT_DNS",                                                                                
        "hosts": [                                                                                          
            {                                                                                                
                "socketAddress": {                                                                              
                    "address": "blog.gmem.cc",                                                              
                    "portValue": 443                                                                        
                }                                                                                            
            }                                                                                                
        ]                                                                                      
    }                                                                                                        
]  
// istioctl proxy-config endpoint ubuntu --cluster='outbound|443||cdn.gmem.cc' -o json      
[
    {
        "name": "outbound|443||cdn.gmem.cc",
        "addedViaApi": true,
        "hostStatuses": [
            {
                "address": {
                    "socketAddress": {
                        "address": "27.221.30.56",
                        "portValue": 443
                    }
                }
            }
        ]
    }
]
 
// istioctl proxy-config endpoint ubuntu --cluster='outbound|443||blog.gmem.cc' -o json        
[                                                                                                            
    {                                                                                                        
        "name": "outbound|443||blog.gmem.cc",                                                                
        "addedViaApi": true,                                                                                  
        "hostStatuses": [                                                                                      
            {                                                                                                
                "address": {                                                                                
                    "socketAddress": {
                        "address": "39.107.94.255",
                        "portValue": 443
                    }
                }
        ]
    }
]
UDS的例子
YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: unix-domain-socket-example
spec:
  hosts:
  - "example.unix.local"
  location: MESH_EXTERNAL
  ports:
  - number: 80
    name: http
    protocol: HTTP
  resolution: STATIC
  endpoints:
  - address: unix:///var/run/example/socket
Gateway

配置在网格的边缘,用于外部访问服务网格,为TCP/HTTP流量提供负载均衡。 Gateway仅仅用在独立运行Istio Proxy的那些Ingress/Egress网关上,定义在网格边缘上暴露的名字:

  1. 如果Gateway定义在Ingress网关上,那么对网格外部暴露名字。当然,外部需要将这些名字的DNS记录指向Istio网关对外的IP地址
  2. 如果Gateway定义在Egress网关上:
    1. 如果通过配置限定所有出口流量必须走Egress网关,那么对集群内部暴露名字
    2. 使用VirtualService,可以将某个名字强行引导自网关,自网关出去

和Kubernetes的Ingress不同,Istio网关仅仅在L4-L6上配置,而不使用L7。 你可以把Gateway绑定到VirtualService,进而使用Istio标准的方式来管理HTTP请求和TCP流量。

Gateway可以定义多个“服务器”(servers),每个服务器可以匹配多个hosts,注意,这些hosts同样只是用来说明哪些名字匹配这个Gateway定义。每个服务器会在网关Istio Proxy中产生一个监听端口,这个端口有port字段确定。

当流量进入网关后,具体转发给谁,取决于此流量HTTP头Host,和Istio什么服务条目(来自K8s的服务或者ServiceEntry定义的服务)、路由规则(hosts相匹配,且应用到此网关的VirtualService)匹配。

HTTP网关

下面的例子,允许网格外部发送针对gmem.cc的HTTPS请求:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: mesh.gmem.cc
spec:
  # 标签列表,指定应用此网关配置的Pod/VM具有的标签集
  # 具有匹配标签的Pod(通常是ingressgateway/egressgateway)的Envoy接收配置
  # 目标Pod必须是以proxy模式运行的Pilot agent,也就是说你不能随意指定标签选择器
  selector:
    istio: ingressgateway
  # 在这里定义所有的服务器,其端口都将成为网关Pod的物理监听端口
  # 对于这个例子,你可以使用*.mesh.gmem.cc域名,访问istio-ingressgateway的任意Pod的443端口
  servers:
  # 多个服务器可以分开配置,也可以配置在同一个Gateway资源中
  - port:
      # 此Gateway创建后,ingressgateway的Envoy会立即添加一个0.0.0.0_443监听器
      # 并且引用名为https.443.https的路由
      number: 443
      name: https
      protocol: HTTPS
    hosts:
    # 可以使用通配符,这样可以任何子域名访问网关
    - *.mesh.gmem.cc
    tls:
      mode: SIMPLE
      # 服务器数字证书,必须挂载到istio-ingressgateway
      serverCertificate: /etc/istio/ingressgateway-certs/tls.crt
      # 服务器私钥,必须挂载到istio-ingressgateway
      privateKey: /etc/istio/ingressgateway-certs/tls.key
      # 如果客户端发送HTTP请求,则将其重定向到HTTPS
      httpsRedirect: true
 
   # 下面是一个TCP网关,使用MongoDB协议
  - port:
      number: 2379
      name: mongo
      protocol: MONGO
    hosts: [ "*" ]

必须把上述网关绑定到VirtualService,它才能工作,精确的说,是网关产生的物理监听器才拥有了路由表:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: ubuntu.mesh.gmem.cc
spec:
  hosts:
    # 域名必须和绑定的网关的域名兼容,例如是它的子集
    # 这里的hosts+绑定网关的hosts,生成Envoy路由表的virtualHosts.domains字段
    - ubuntu.mesh.gmem.cc
 
  # 将虚拟服务绑定到网关
  # 指定需要应用此虚拟服务(定义的路由规则)的Gateway(所选择的Pod)或Sidecar
  # 保留字mesh表示,网格中所有Sidecar使用此虚拟服务定义的路由规则
  # 如果要同时为Gateway和Sidecar应用此虚拟服务,传入mesh+相关Gateway的名字
  gateways:
  - mesh.gmem.cc
  http:
  - match:
    - uri:
        prefix: /blog
    route:
    - destination:
        # 生成Envoy路由表的virtualHosts[0].routes[0].route.cluster
        # 例如 outbound|8086||ubuntu.default.svc.k8s.gmem.cc
        host: ubuntu
        port:
          number: 8000
          # 端口可以自动选择,也可以明确指定
          # 这里明确指定了8000,生成Envoy Cluster
          # outbound|8000||ubuntu.default.svc.k8s.gmem.cc
          # 注意,不能随意指定,必须在default命名空间有个叫ubuntu的服务,在暴露其8000端口才可以
TCP网关

为集群内的TCP服务建立入口Gateway时,hosts字段被忽略:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: zookeeper
spec:
  selector:
    istio: ingressgateway
  servers:
  - hosts:
    # 这边写什么都无所谓,只认客户端的目的IP地址
    - 'zk.mesh.gmem.cc'
    port:
      name: zookeeper
      number: 2181
      protocol: TCP

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: zk.mesh.gmem.cc
spec:
  gateways:
  - zookeeper
  hosts:
  - zk.mesh.gmem.cc
  tcp:
  - route:
    - destination:
        host: zk

需要以下细节:

  1. 上述虚拟服务和网关配对后,istio-ingressgateway的Pod立刻开始监听2181端口
  2. 但是,istio-ingressgateway服务默认只配置了80、443端口

这意味着上述网关配置不可用,你还需要修改istio-ingressgateway服务的配置。

限制所有出口流量必须走网关

首先,需要在Istio的安装或配置中禁用允许服务直接访问外部服务的能力。这通常通过设置Istio的出站流量策略为 REGISTRY_ONLY 来实现。REGISTRY_ONLY 模式意味着所有出站流量都必须通过Istio的Egress网关,除非流量的目的地已经在Istio的服务注册表中定义。这可以通过在安装Istio时配置meshConfig.outboundTrafficPolicy.mode为REGISTRY_ONLY来完成,或者在已经安装的Istio上通过修改ConfigMap来实现。

接下来,需要配置一个或多个Egress网关来处理出站流量。这通常涉及到创建一个Gateway资源,用于指定Egress网关应该接受哪些出站流量(基于端口、主机名等条件)。

为了允许出站流量访问外部服务,需要为每个外部服务定义一个ServiceEntry资源。ServiceEntry使外部服务在Istio的内部服务注册表中可见,即使这些服务不是网格的一部分。这样,即使启用了REGISTRY_ONLY模式,出站流量也可以被正确地路由到外部服务。

然后,为每个需要通过Egress网关访问的外部服务创建一个VirtualService。VirtualService定义了流量从Egress网关到外部服务的具体路由规则,可以基于URL路径、请求头等条件进行路由。

最后,可能需要为通过Egress网关访问的外部服务配置DestinationRule,以指定流量策略,如负载均衡、TLS设置等。

一个简单的配置示例:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: istio-egressgateway
spec:
  selector:
    istio: egressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "*.example.com"
---
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: external-svc-example-com
spec:
  hosts:
  - "example.com"
  ports:
  - number: 80
    name: http
    protocol: HTTP
  resolution: DNS
  location: MESH_EXTERNAL
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: route-via-egressgateway
spec:
  hosts:
  - "example.com"
  gateways:
  - istio-egressgateway
  - mesh
  http:
  - match:
    - gateways:
      - mesh
    route:
    - destination:
        host: istio-egressgateway.istio-system.svc.cluster.local
        subset: v1
        port:
          number: 80
      weight: 100
  - route:
    - destination:
        host: example.com
        port:
          number: 80
      weight: 100
将出口流量导向网关

ServiceEntry可以配合Gateway+VirtualService,将出口流量导向一组特殊的Pod —— istio-egressgateway。本节给出一个样例。

首先定义一个ServiceEntry:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: gmem
spec:
  hosts:
  - k8s.gmem.cc
  ports:
  - number: 80
    name: http
    protocol: HTTP
  resolution: DNS

有了这个配置以后,就可以从网格内部访问k8s.gmem.cc了,但是流量直接从Sidecar发往k8s.gmem.cc。

现在,我们要让流量通过istio-egressgateway的Pod转发。首先,定义一个Gateway:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: gmem
spec:
  # 指定哪些虚拟机/Pod应用这个网关配置,在K8S中,可匹配任意命名空间中的Pod
  selector:
    # 使用deployment istio-egressgateway定义的Pod,此deployment作为出口网关使用
    istio: egressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - k8s.gmem.cc

然后,定义一个虚拟服务:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
spec:
  hosts:
  - k8s.gmem.cc
  gateways:
  # 此虚拟服务生成的Envoy配置(监听器路由),将应用到网关gmem所应用到的Pod,以及所有sidecar
  - gmem
  - mesh
  http:
  - match:
    # 如果HTTP请求从mesh发出,也就是从sidecar发出
    - gateways:
      - mesh
      port: 80
    route:
    # 那么将流量转发到istio-egressgateway,也就是出口网关服务
    - destination:
        host: istio-egressgateway.istio-system.svc.k8s.gmem.cc
        port:
          number: 80
      weight: 100
  - match:
    # 如果HTTP请求从gmem网关所应用到的Pod,也就是istio-egressgateway对应的Pod发出
    - gateways:
      - gmem
      port: 80
    route:
    # 那么将流量直接发往外部
    - destination:
        host: k8s.gmem.cc
        port:
          number: 80
      weight: 100 
WorkloadEntry

1.6新增,让运维人员能够描述单个非K8S工作负载,例如物理机或虚拟机,的属性。

必须伴随ServiceEntry一起使用,ServiceEntry使用选择器来匹配工作负载(包括Pod以及虚拟机、物理机),产生MESH_INTERNAL类型的服务。

当工作负载连接到istiod后,WorkloadEntry的Status会发生变化,反应工作负载的状态和一些其它细节,类似于K8S更新Pod的状态。

下面是一个例子:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: networking.istio.io/v1beta1
kind: WorkloadEntry
metadata:
  name: details-svc
spec:
  # 此工作负载具有Sidecar,且Sidecar使用该SA启动
  serviceAccount: details-legacy
  # 工作负载的IP地址
  address: 2.2.2.2
  # 此工作负载的标签
  labels:
    app: details-legacy
    instance-id: vm1

对应的ServiceEntry:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: networking.istio.io/v1beta1
kind: ServiceEntry
metadata:
  name: details-svc
spec:
  hosts:
  - details.bookinfo.com
  # 内部服务
  location: MESH_INTERNAL
  ports:
  - number: 80
    name: http
    protocol: HTTP
  resolution: STATIC
  # 此服务匹配的工作负载,不管它是Pod还是WorkloadEntry
  workloadSelector:
    labels:
      app: details-legacy

WorkloadEntry的address也可以指定为FQDN,对应的ServiceEntry的解析策略应该设置为 resolution: DNS。 

配置案例参考
访问ServiceEntry定义的服务时重定向到出口网关

定义 ServiceEntry 来允许访问外部 HTTP 服务:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: external-svc-httpbin
spec:
  hosts:
  - httpbin.org
  ports:
  - number: 80
    name: http
    protocol: HTTP
  resolution: DNS
  location: MESH_EXTERNAL

注意:

  1. 没有 ServiceEntry,网格中的服务无法通过 Istio 的 DNS 发现机制解析外部服务的名称。但是底层DNS机制还是能够解析域名的
  2. 如果你需要对某些外部服务应用特定的路由规则,或者需要 Istio 对这些流量进行更细粒度的管理(例如,只允许访问特定的外部服务),则 ServiceEntry 是必需的
  3. 如果你需要详细控制如何访问特定的外部服务,包括服务发现、安全、和流量管理,那么 ServiceEntry 是必要的

如果你只是定义了一个 ServiceEntry 来允许访问外部服务,而没有配置流量经过 Egress Gateway,那么流量将直接从 Pod 的 Sidecar 代理出去,直接到达外部服务。ServiceEntry 使得服务网格内的服务能够访问和调用网格外的服务,但它本身并不强制流量经过 Egress Gateway。 

定义 Gateway 作为 Egress Gateway:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: istio-egressgateway
spec:
  selector:
    istio: egressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - httpbin.org

注意:

  1. Gateway定义的是一个网关规则,而不是定义网关
  2. 网关其实就是运行Envoy的容器,它可以作为独立Pod运行,用作边界网关;也可以作为Sidecar运行,作为每个工作负载和外部通信的网关
  3. selector选择器决定了这个规则运用到哪些网关或者说Envoy/Istio Proxy容器。这里的含义是仅仅应用到出口网关
  4. 这个网关中定义的域名httpbin.org用前面的ServiceEntry负责,即方访问外部

定义 VirtualService 将流量从 Sidecar 代理路由到 Egress Gateway,然后到外部服务:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: route-via-egressgateway
spec:
  hosts:
  - httpbin.org
  gateways:
  - mesh
  - istio-egressgateway
  http:
  - match:
    - gateways:
      - mesh
      port: 80
    route:
    - destination:
        host: istio-egressgateway.istio-system.svc.cluster.local
        port:
          number: 80
      weight: 100
  - match:
    - gateways:
      - istio-egressgateway
      port: 80
    route:
    - destination:
        host: httpbin.org
        port:
          number: 80
      weight: 100

注意:

  1. gateways定义了这个虚拟服务应用到哪些网关,这里是应用到:
    1. mesh 特殊关键字,代表所有Sidecar
    2. 特定网关,具体来说,是VirtualService所在命名空间中,名为istio-egressgateway的网关。如果要应用到其它命名空间中运行的网关,使用gateway-namespace/gateway-name这种形式
  2. 路由规则:
    1. 对于来自mesh内部也就是sidecar的流量,转发给istio-egressgateway这个网关
    2. 对于来自istio-egressgateway的流量,发送给httpbin.org前面已经定义了对应的Gateway,负责发送到网格外部
策略和遥测基础

Istio提供了一个灵活的模型,用以收集网格中服务的各种指标(telemetry)。指标收集工作主要由Mixer负责。

从逻辑上说,每次:

  1. 请求发起前,消费者的Envoy都会调用Mixer,执行前提条件检查(Check)
  2. 每次请求处理完成后,都会调用Mixer,发送监控指标(Report)

Sidecar在本地具有缓存,大部分前提条件检查都在本地完成。此外监控指标也具有本地缓冲,不会频繁的发送给Mixer。

配置模型

Istio的策略和监控特性基于一个公共的模型来配置,你需要配置三类资源:

  1. Handlers:决定使用哪些适配器,这些适配器如何运作
  2. Instances:定义如何把请求属性映射为适配器的输入。Instance代表一个或多个适配器需要处理的数据块
  3. Rules:决定何时调用适配器,传递什么instance给它

而这些配置是基于:

  1. Adapters:封装Mixer和特定的基础设施后端的交互逻辑
  2. Templates:定义将请求属性映射为适配器输入的Schema。一个适配器可以支持多个模板
适配器

Mixer是高度模块化的、可扩展的组件。通过适配器,它支持多种日志记录、配额、授权、监控后端,并将这些策略、监控后端和Istio的其它部分解耦。

适配器封装了Mixer和外部基础设施后端(例如Prometheus)交互的逻辑,每个适配器都需要特定的配置参数才能工作,例如logging适配器需要知道后端服务的IP和端口。

具体使用哪些适配器,也就是启用哪些后端,通过配置文件来指定。

Istio提供了很多内置适配器,每种适配器都对应一个CRD,这些CRD的实例就是适配器的实例,也即Handler。Adaptor CRD的实例,表示一种自定义的适配器。自定义适配器实例化时使用Handler类型的CR。

适配器类型

适配器的类型使用CRD来描述,执行下面的命令查看系统中可用的适配器类型:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
kubectl get crd -l istio=mixer-adapter
# NAME                              CREATED AT
# adapters.config.istio.io          2019-03-21T11:09:28Z
# bypasses.config.istio.io          2019-03-21T11:09:28Z
# circonuses.config.istio.io        2019-03-21T11:09:28Z
# deniers.config.istio.io           2019-03-21T11:09:28Z
# fluentds.config.istio.io          2019-03-21T11:09:29Z
# kubernetesenvs.config.istio.io    2019-03-21T11:09:29Z
# listcheckers.config.istio.io      2019-03-21T11:09:29Z
# memquotas.config.istio.io         2019-03-21T11:09:29Z
# noops.config.istio.io             2019-03-21T11:09:29Z
# opas.config.istio.io              2019-03-21T11:09:29Z
# prometheuses.config.istio.io      2019-03-21T11:09:29Z
# rbacs.config.istio.io             2019-03-21T11:09:29Z
# redisquotas.config.istio.io       2019-03-21T11:09:29Z
# servicecontrols.config.istio.io   2019-03-21T11:09:30Z
# signalfxs.config.istio.io         2019-03-21T11:09:30Z
# solarwindses.config.istio.io      2019-03-21T11:09:30Z
# stackdrivers.config.istio.io      2019-03-21T11:09:30Z
# statsds.config.istio.io           2019-03-21T11:09:30Z
# stdios.config.istio.io            2019-03-21T11:09:30Z

执行下面的命令查看Prometheus适配器的实例:

Shell
1
2
3
kubectl -n istio-system get prometheuses.config.istio.io
# NAME      AGE
# handler   1h

本节后续内容讨论主要的内置适配器。

Prometheus

支持 metric 模板

此适配器暴露了HTTP访问端点,Prometheus可以来采集此端点,获取服务网格的各项指标

使用Istio官方提供的Chart时,默认会在当前命名空间安装一个Prometheus服务器,并自动配置以抓取以下端点:

  1. istio-mixer.istio-system:42422,所有由Mixer的Prometheus适配器生成的网格指标。指标名以istio_开头
  2. istio-mixer.istio-system:9093,用于监控Mixer自身的指标
  3. istio-mixer.istio-system:9102,Envoy生成的原始统计信息。指标名以envoy_开头

注意:目前Envoy的http://0.0.0.0:15090/stats/prometheus也暴露了Prometheus指标

该适配器包含的配置参数如下:

参数 说明
metrics

类型 Params.MetricInfo[],需要插入到Prometheus的指标的列表。如果指标在Istio中定义了,但是没有在这里进行适当配置,则不会推送到Prometheus:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
metrics:
  # 作为指标名前缀的命名空间。如果指标名为requests_total,该字段设置为istio,则最终完整的、
  # 提供给Prometheus的指标名是istio_requests_total。默认istio
- namespace: string
  # 注册到Prometheus的指标的名称,如果不指定从instanceName自动生成
  name: string
  # 必须,此MetricInfo处理的Istio Metric实例的全限定名称
  instanceName: string
  # 易读的描述
  description: string
  # 该指标的类别
  # Params.MetricInfo.Kind
  kind: UNSPECIFIED | GAUGE | COUNTER | DISTRIBUTION
  # 对于DISTRIBUTION类型的metric,该配置指定了聚合后数据所存放的桶
  # 对于非DISTRIBUTION类型的metric该字段无意义
  # Params.MetricInfo.BucketsDefinition
  buckets:
    # 三选一:
    # 线性桶,每个桶的大小(区间一致)
    linearBuckets:
      # 桶数量为: N = num_finite_buckets + 2
      numFiniteBuckets: int32
      # 桶的宽度
      width: double
      # 第一个桶的下限
      offset: double
    # 指数桶
    exponentialBuckets:
      # 桶数量为: N = num_finite_buckets + 2,两个额外的桶用作下溢、上溢桶
      # 对于桶i,它的左右边界为:
      # [scale * (growth_factor ^ (i - 1)), scale * (growth_factor ^ i) ]
      growthFactor: double
      scale: double
      numFiniteBuckets: int32
    # 明确定义的桶
    explicitBuckets:
      bounds: [ double,... ]
  # 指标的标签名的数组,这些标签必须和metric实例的dimension匹配
  labelNames:
  - dimension1
  - dimension2

示例:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
apiVersion: config.istio.io/v1alpha2
kind: prometheus
metadata:
  name: handler
spec:
  metrics:
  # 从那个metric类型的instance上读取生成该指标所需的信息
  - instance_name: requestcount.metric.istio-system
    # 生成Prometheus的Counter
    kind: COUNTER
    # 将instance上的以下维度作为标签
    label_names:
    - reporter
    - source_app
    - source_principal
    - source_workload
    - source_workload_namespace
    - source_version
    - destination_app
    - destination_principal
    - destination_workload
    - destination_workload_namespace
    - destination_version
    - destination_service
    - destination_service_name
    - destination_service_namespace
    - request_protocol
    - response_code
    - connection_security_policy
    name: requests_total
  - buckets:
      # 明确定义的桶,桶数量是下面数组的长度+2
      explicit_buckets:
        bounds:
        - 0.005
        - 0.01
        - 0.025
        - 0.05
        - 0.1
        - 0.25
        - 0.5
        - 1
        - 2.5
        - 5
        - 10
    instance_name: requestduration.metric.istio-system
    # 生成Prometheus的Histogram
    kind: DISTRIBUTION
    label_names:
    - reporter
    - source_app
    - source_principal
    - source_workload
    - source_workload_namespace
    - source_version
    - destination_app
    - destination_principal
    - destination_workload
    - destination_workload_namespace
    - destination_version
    - destination_service
    - destination_service_name
    - destination_service_namespace
    - request_protocol
    - response_code
    - connection_security_policy
    name: request_duration_seconds
metricsExpirationPolicy

可选参数,指定此Prometheus适配器生成的指标的过期策略

示例:

YAML
1
2
3
4
5
metricsExpirationPolicy:
  # 删除超过10分钟没有接收到更新的指标
  metricsExpiryDuration: "10m"
  # 每分钟进行一次是否过期的检查
  expiryCheckIntervalDuration: "1m"
Statsd

支持 metric 模板

将指标数据递送给StatsD后端

Denier

支持 checknothing、listentry、quota模板

用于Check,总是让检查失败

Fluentd

支持logentry模板

将Istio日志条目递送给Fluentd守护程序

KubernetesEnv

支持kubernetes模板

从Kubernetes环境抽取信息,转换为Attributes,供下游适配器使用

List

支持listentry模板

用于Check,实现简单的黑、白名单

MemQuota

支持listentry模板

用于支持Istio的配额管理系统,用于开发环境下

RedisQuota

支持quota模板

用于支持Istio的配额管理系统,用于生产环境下

Stdio

支持 logentry、metric 模板

让Istio将日志、指标输出到标准输出、错误流,或者任何本地文件

Zipkin

支持tracespan模板 

将追踪数据递送给Zipkin和兼容的Tracer

模板

模板是Instance如何生成的依据。通常情况下都是直接定义Instance,并使用kind字段引用实例的模板类型。

内置模板不需要在配置存储中定义Template类型的CR。每种内置模板都对应独立的CRD,这些CRD的实例就是模板的实例,也即Instance。Template CRD的实例,表示一种自定义的模板。自定义模板实例化时使用Instance类型的CR。

本节描述的是内置模板的实例配置方式。要了解如何使用自定义模板,参考扩展Istio一文。

Metric

该模板用于描述需要分发给特定监控后端的运行时指标,配置示例:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: "config.istio.io/v1alpha2"
kind: metric
metadata:
  name: requestsize
  namespace: istio-system
spec:
  value: request.size | 0
  dimensions:
    source_version: source.labels["version"] | "unknown"
    destination_service: destination.service.host | "unknown"
    destination_version: destination.labels["version"] | "unknown"
    response_code: response.code | 200
  monitored_resource_type: '"UNSPECIFIED"'

模板规格如下:

字段 说明
value

stio.policy.v1beta1.Value,此指标报告的值

dimensions

map<string, istio.policy.v1beta1.Value>,用以唯一的识别此指标的维度名值映射

monitoredResourceType 如果后端基础设施支持监控资源(monitored resource),这两个字段有用  
monitoredResourceDimensions

注意:当编写配置时,字段的值可以是字面值或Go表达式。如果字段的类型不是istio.policy.v1beta1.Value,则Go表达式的推断类型必须和字段的类型匹配。

Listentry

该模板用于从请求中抽取单个值,并和listchecker适配器联用,实现值检查。配置示例:

YAML
1
2
3
4
5
6
7
apiVersion: "config.istio.io/v1alpha2"
kind: listentry
metadata:
  name: appversion
  namespace: istio-system
spec:
  value: source.labels["version"]
Logentry

从请求中抽取单条日志。配置示例:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
apiVersion: "config.istio.io/v1alpha2"
kind: logentry
metadata:
  name: accesslog
  namespace: istio-system
spec:
  # 日志严重性级别
  severity: '"Default"'
  # 时间戳
  timestamp: request.time
  # 变量列表
  variables:
    sourceIp: source.ip | ip("0.0.0.0")
    destinationIp: destination.ip | ip("0.0.0.0")
    sourceUser: source.principal | ""
    method: request.method | ""
    url: request.path | ""
    protocol: request.scheme | "http"
    responseCode: response.code | 0
    responseSize: response.size | 0
    requestSize: request.size | 0
    latency: response.duration | "0ms"
  monitored_resource_type: '"UNSPECIFIED"'
Quota

声明用于进行限额的各个维度的取值。配置示例:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: "config.istio.io/v1alpha2"
kind: quota
metadata:
  name: requestcount
  namespace: istio-system
spec:
  # 维度列表,配额适配器可以针对不同的维度,应用不同的限额策略
  dimensions:
    source: source.name | "unknown"
    sourceVersion: source.labels["version"] | "unknown"
    destination: destination.labels["app"] | destination.service.host | "unknown"
    destinationVersion: destination.labels["version"] | "unknown"
Tracespan

表示分布式跟踪中一个单独的Span。配置示例:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
apiVersion: "config.istio.io/v1alpha2"
kind: tracespan
metadata:
  name: default
  namespace: istio-system
spec:
  # 此Span所属的Trace
  traceId: request.headers["x-b3-traceid"]
  # 此Span的标识符
  spanId: request.headers["x-b3-spanid"] | ""
  # 父Span的标识符,单个Trace中的所有Span,形成一棵树
  parentSpanId: request.headers["x-b3-parentspanid"] | ""
  # 此Span的名称
  spanName: request.path | "/"
  # 此Span的开始、结束时间
  startTime: request.time
  endTime: response.time
  # 是由客户端还是服务器Span,默认服务器Span
  clientSpan: (context.reporter.kind | "inbound") == "inbound"
  rewriteClientSpanId: false
  # Span的元数据
  spanTags:
    http.method: request.method | ""
    http.status_code: response.code | 200
    http.url: request.path | ""
    request.size: request.size | 0
    response.size: response.size | 0
    source.principal: source.principal | ""
    source.version: source.labels["version"] | ""
规则
Rule

如果满足选择器,则执行一组intentions —— 将instance发送给handler处理。示例:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 基于属性来匹配请求,如果匹配,则执行所有Actions
match: match(destination.service.host, "*")
# Action[]
actions:
  # 需要调用的处理器(适配器)的全限定名称
- handler: prometheus-handler
  # 发送给处理器的instance的全限定名称
  # 列出的instance的所有属性/字面值被解析,最后创建出一个对象,然后发送给处理器
  instances:
  - RequestCountByService
# 针对请求头的模板化的操作,使用由actions产生的值。需要Check action结果为OK
requestHeaderOperations: Rule.HeaderOperationTemplate[]
# 针对响应头的模板化的操作,使用由actions产生的值。需要Check action结果为OK
responseHeaderOperations: Rule.HeaderOperationTemplate[] 
Action

每个Rule可以包含多个Action,表示选择器匹配时应该将哪些Instance发送给某个handler:

YAML
1
2
3
4
handler: prometheus-handler
instances:
- RequestCountByService
- RequestCountByEndpoint
Handler

Handler就是适配器的实例。配置项说明:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 供Action引用的处理器的名称,必须全局唯一
name: string
# 需要实例化的、编译进Mixer二进制文件的适配器的名称
# 适配器名称通常以常量形式写在代码中
compiledAdapter: string
# 需要实例化的、外置的适配器实现的名称
# 适配器名称通常以常量形式写在代码中
adapter: string
# 传递给适配器的参数
params: google.protobuf.Struct
# 包含如何连接到外置(独立进程)适配器的信息
connection: Connection
  address: string
  timeout: google.protobuf.Duration
  # tls和mutual
  authentication: Authentication

下面的例子,定义了一个基于外置适配器的Prometheus类型的处理器:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: "config.istio.io/v1alpha2"
# 此处理器的适配器的类型
kind: prometheus
metadata:
  name: handler
  namespace: istio-system
spec:
  # 传递给适配器的参数
  param:
    metrics:
    - name: request_count
      instance_name: requestcount.metric.istio-system
      kind: COUNTER
      label_names:
      - source_service
      - source_version
      - destination_service
      - destination_version
  # 适配器的连接方式
  connection:
    address: localhost:8090
Instance

Instance就是模板的实例,或者说,Instance是依据模板创建出来的。 配置项说明:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
# Instance的名称
name: string
# 此实例使用的内置模板的名称
compiledTemplate: string
# 用于引用非内置(compiled-in)模板
template: string
# 传递给模板的参数
params: google.protobuf.Struct
# 用于将适配器产生的属性,绑定回属性空间
attributeBindings: map<string, string>
  # 将适配器的source_namespace作为Attribute source.namespace的值
  source.namespace: output.source_namespace

下面的例子创建了一个可以由Action直接引用的Instance:

YAML
1
2
3
4
5
6
7
8
9
- name: RequestCountByService
  # 该实例使用的模板
  template: istio.mixer.adapter.metric.Metric
  # 传递给模板的参数
  params:
    value: 1
    dimensions:
      source: source.name
      destination_ip: destination.ip 
性能和可靠性

Mixer是非常高可用的组件,并且,它能在整体上提供服务网格的可用性、降低延迟:

  1. 无状态,Mixer没有任何持久化状态
  2. 高度健壮,每个实例都具有99.999%的可用性
  3. 缓存和缓冲,Mixer可以在本地累积大量的临时数据

在每个Pod上都要部署的Sidecar,必须尽可能占用少的内存。这意味着Sidecar的本地缓存、缓冲不能太大。Mixer可以作为Sidercar的高可用二级缓存使用。此外,Mixer的缓冲可以在后端基础设施(例如Prometheus)失败的情况下,继续运行(接受Envoy发来的指标),因而提高了可用性。

Mixer的缓存/缓冲也减少了对后端基础设施的访问频率,甚至减少访问数据量,因为它可以在本地进行数据聚合。

AttributeManifest

描述一组属性,这些属性由Istio的某个组件产生。配置示例:

YAML
1
2
3
4
5
6
7
# 此配置的修订版号,由服务器分配
revision: string
# 产生属性的组件的名称,可以是istio-proxy
# 或者是类型为attributes的Mixer适配器
name: string
# 在运行时,组件负责产生的属性的映射,从属性名到属性Spec
attributes: map<string, AttributeManifest.AttributeInfo>
Attributes

属性(Attributes)是Istio策略/监控功能的关键概念。属性是一小块数据,描述某个请求的自身的特征(property)或请求所处的环境,例如一个属性可能表示请求的长度、响应的状态码、请求来自的IP地址。每个属性都具有名称和取值。这里的请求不一定是HTTP请求,它可以是任意的网络活动,例如TCP连接,TCP连接的原始客户端IP就是一个属性。

属性的字段,可能由请求的客户端Sidecar报告,也可能由请求的服务端Sidecar报告。

为了简化系统、改善开发者体验,Istio使用共享的属性定义 —— 所由组件都使用这套定义。

Mixer本质上就是属性处理程序。Envoy为每个请求调用Mixer,并且发送请求的属性。Mixer依据请求属性,以及Mixer自己的配置,对若干基础设施后端发起调用:

AttributeInfo

此对象用于描述一个属性的Schema —— 定义属性的元数据:属性值的类型、属性的意义的描述。

任何Istio属性都必须对应某个AttributeManifest中的一个AttributeInfo。每个属性的名字必须是全局唯一的,在所由属性清单中,它只能出现一次。

属性的运行时形式没有做规定,不管使用JSON、XML还是Protocol Buffer来传递属性,其语义不会变化。

可用属性列表
属性 说明
source.uid

平台相关的源工作负载实例的唯一标识,例如:

kubernetes://redis-master-0.default

source.ip ip_address,源工作负载实例的IP地址
source.labels

map[string, string],源工作负载实例的标签集,例如:

version => v1

source.name 源工作负载实例的名称,例如:redis-master-0
source.namespace 源工作负载实例所在的命名空间,例如default
source.principal 源工作负载实例的身份认证主体,例如service-account-redis
source.owner 源工作负载实例的所有者资源的唯一标识,Pod常常由Deployment所有,例如:kubernetes://apis/extensions/v1beta1/namespaces/istio-system/deployments/istio-policy
source.workload.uid 源工作负载(非实例,也就是服务)的信息
source.workload.name
source.workload.namespace
destination.uid 目标工作负载实例的信息
destination.ip
destination.port
destination.labels
destination.name
destination.namespace
destination.principal
destination.owner
destination.workload.uid 目标工作负载的信息
destination.workload.name
destination.workload.namespace
destination.container.name 目标工作负载实例的容器名,例如runtime
destination.container.image 目标工作负载实例的容器镜像 
destination.service.host 目标工作负载的主机名,例如grafana.default.svc.k8s.gmem.cc 
destination.service.uid 目标工作负载的唯一标识 
destination.service.name 目标工作负载的名称,也就是K8S服务短名 
destination.service.namespace 目标工作负载所在命名空间
request.headers map[string, string],HTTP请求头,对于gRPC,其元数据存放在此 
request.id 低碰撞风险的请求唯一标识 
request.path 请求的URL查询路径部分 
request.host 请求的URL主机部分 
request.method 请求的HTTP方法 
request.reason 用于审计系统的请求原因 
request.referer HTTP referer头 
request.scheme 请求URL的Schema部分 
request.size 请求字节数,对于HTTP来说等于Content-Length  
request.total_size 请求总大小,包括请求头、请求体、请求尾 
request.time timestamp,目标接收到请求的时间
request.useragent HTTP User-Agent头 
response.headers HTTP响应头 
response.size HTTP响应大小  
response.total_size
response.time timestamp,目标返回响应的时间
response.duration duration,目标处理请求所消耗的时间 
response.code int64,HTTP状态码 
response.grpc_status gRPC状态码
response.grpc_message gRPC状态消息
connection.id 低碰撞风险的TCP连接的唯一标识
connection.event TCP连接的状态,open/continue/close之一 
connection.received.bytes 针对连接的最后一次Report()调用之后,接收到的字节数
connection.received.bytes_total 接收到的总字节数 
connection.sent.bytes 针对连接的最后一次Report()调用之后,发送的字节数
connection.sent.bytes_total 发送的总字节数 
connection.duration TCP连接已经打开的时间
connection.mtls boolean,是否启用mTLS 
connection.requested_server_name 连接所请求的服务器名称(SNI) 
context.protocol 请求或被代理连接使用的协议,例如tcp 
context.time Mixer操作的时间戳
context.reporter.kind

如果当前请求属性是从客户端报告,则为outbound

如果当前请求属性是从服务端报告,则为inbound

context.reporter.uid 请求报告者的UID
api.service gRPC公共服务名,不是网格内部的服务标识,而是暴露给客户端的服务的名称
api.version gRPC接口版本 
api.operation gRPC操作名称,例如getPetsById 
api.protocol 暴露给客户端的协议,可以是http, https, grpc
request.auth.principal 源身份验证相关 
request.auth.audiences
request.auth.presenter
request.auth.claims
request.api_key
check.error_code Mixer Check调用的错误码
check.error_message Mixer Check调用的错误消息 
check.cache_hit Mixer Check调用是否命中本地缓存 
quota.cache_hit Mixer Quota调用是否命中本地缓存
Attributes表达式

在你配置Instance的时候,往往需要对Attributes进行各种计算,才能得到Instance的字段。Attributes表达式是Go expressions的子集。

操作符
操作符 说明
== 逻辑等于,示例: request.size == 200
!= 逻辑不等,示例: request.auth.principal != "admin"
|| 逻辑或,示例: (request.size == 200) || (request.auth.principal == "admin")
&& 逻辑与
[ ] 用于访问映射,示例: request.headers["x-id"]
+ 算术加或字符串拼接,示例: request.host + request.path
|

如果左操作数为空,则表达式值为右操作数,示例:

source.labels["app"] | source.labels["svc"] | "unknown"

函数
函数 说明
match Glob匹配,示例: match(destination.service, "*.default.svc.k8s.gmem.cc")
email 将文本转换为EMAIL_ADDRESS类型,示例: email("alex@gmem.cc")
dnsName 将文本转换为DNS_NAME类型,示例: dnsName("www.gmem.cc")
ip 将文本转换为IP_ADDRESS类型,示例: source.ip == ip("10.11.12.13")
timestamp 将RFC 3339文本转换为TIMESTAMP类型,示例: timestamp("2015-01-02T15:04:35Z")
uri 将文本转换为URI类型,示例: uri("http://istio.io")
.matches 正则式匹配,示例: "svc.*".matches(destination.service)
.startsWith

判断是否字符串以目标子串开头/结尾,示例:

Go
1
2
destination.service.startsWith("acme")
destination.service.endsWith("acme")    
.endsWith
emptyStringMap

创建一个空的map[string][string],示例:

Go
1
request.headers | emptyStringMap()
conditional

模拟三元表达式:

Go
1
2
             // 如果为true                                      则        否则
conditional((context.reporter.kind | "inbound") == "outbound", "client", "server")
示例
表达式 说明
request.size | 200 返回int,返回请求的尺寸或者200
request.headers["x-forwarded-host"] == "myhost" 返回boolean,判断请求头的值
(request.headers["x-user-group"] == "admin") || (request.auth.principal == "admin") 返回boolean,逻辑或
(request.auth.principal | "nobody" ) == "user1" 返回boolean,如果request.auth.principal缺失不会导致错误
source.labels["app"]=="reviews" && source.labels["version"]=="v3" 返回boolean,逻辑与
默认指标

使用Istio默认Chart安装时,它自动提供某些指标:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
kubectl -n istio-system get metrics.config.istio.io
NAME              AGE
# COUNTER Istio代理处理的请求计数
requestcount      1d
# DISTRIBUTION 请求处理耗时
requestduration   1d
# DISTRIBUTION 请求体尺寸
requestsize       1d
# DISTRIBUTION 响应体尺寸
responsesize      1d
# COUNTER TCP接收字节数
tcpbytereceived   1d
# COUNTER TCP发送字节数
tcpbytesent       1d
mixer客户端

Mixer客户端库被静态编译到Envoy代理中。你可以通过Istio提供的CRD和其它对象,对Mixer客户端进行配置。

Attributes

istio.mixer.v1.Attributes表示一组强类型的名称/值对。很多Mixer的API可能产生或消费属性。

Istio使用属性来控制网格中服务的运行时行为。属性可以描述:

  1. Ingress流量的信息
  2. Egress流量的信息
  3. 流量所在的环境的信息

你部署的Istio具有一个固定的、它能够理解的属性表(vocabulary)。尽管某些特殊的Mixer适配器、服务能产生属性,但是属性主要由Envoy产生。

配置示例:

YAML
1
attributes: map<string, istio.mixer.v1.Attributes.AttributeValue>

AttributeValue是string、int64、double、bool、bytes、 google.protobuf.Timestamp、google.protobuf.Duration、StringMap的一种 

APIKey

执行下面的命令列出这类资源:

Shell
1
kubectl -n istio-system get apikeys.config.istio.io

使用APIKey,你可以明确的定义如何生成HTTP请求的Attribute:request.api_key。

APIKey的字段包括:

字段 描述
query

string

三选一,是从给定的请求参数、请求头,还是Cookie中获取api_key

header
cookie
AttributeMatch

指定用来匹配Istio Attribute的子句:

字段 描述
clause

map<string, StringMatch>

将属性名string映射到StringMatch。示例:

YAML
1
2
3
4
5
clause:
  source.uid:
    exact: SOURCE_UID
  request.http_method:
    exact: POST
HTTPAPISpec

通过匹配HTTP请求的HTTP方法、URI路径,来定义用于生成API相关的Attributes的规范化配置。此CRD用于API属性生成的目的,而不是用于呈现身份验证、配、文档等其它API规范(例如OpenAPI)中常见的其它信息。

现有的,基于HTTP动词+路径的定义操作(方法),可以用此CRD来规范化,以便在Istio中使用。

例如,基于OpenAPI v2的宠物商店的API可以配置为如下的HTTPAPISpec:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
apiVersion: config.istio.io/v1alpha2
kind: HTTPAPISpec
metadata:
  name: petstore
  namespace: default
spec:
  # istio.mixer.v1.Attributes
  # 任何HTTP图式匹配的情况下,都会生成的属性列表
  # 通常会包含api.service、api.version属性
  attributes:
    attributes:
      api.service:
        stringValue: petstore.swagger.io
      api.version:
        stringValue: 1.0.0
  # 用于匹配的HTTP图式 HTTPAPISpecPattern[]
  patterns:
    # 如果请求匹配该图式(的HTTP方法或URI模板),则会生成的属性列表
  - attributes:
      attributes:
        api.operation:
          stringValue: findPets
    # 什么情况下匹配此图式:HTTP方法为GET,且URI模板为/api/ptes
    httpMethod: GET
    # uriTemplate 和 regex 二选一,请求的路径必须匹配它们
 
    # uriTemplate,来自规范rfc6570,示例:
    # /pets
    # /pets/{id}
    # /dictionary/{term:1}/{term}
    # /search{?q*,lang}
    uriTemplate: /api/pets
    # 正则式
    regex: "^/pets/(.*?)?"
 
  # 更多图式
  - attributes:
      attributes:
        api.operation:
          stringValue: addPet
    httpMethod: POST
    uriTemplate: /api/pets
  - attributes:
      attributes:
        api.operation:
          stringValue: findPetById
    httpMethod: GET
    uriTemplate: /api/pets/{id}
  - attributes:
      attributes:
        api.operation:
          stringValue: deletePet
    httpMethod: DELETE
    uriTemplate: /api/pets/{id}
  # 如何从HTTP请求中抽取API-KEY
  api_keys:
  # 首先尝试从请求参数中抽取,然后header,然后cookie
  - query: api-key
    header: api-key
    cookie: api-key
HTTPAPISpecBinding

定义HTTPAPISpecs和IstioService之间的绑定关系。下面的例子将HTTPAPISpec petstore和命名空间bar中的foo服务建立绑定:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: config.istio.io/v1alpha2
kind: HTTPAPISpecBinding
metadata:
  name: my-binding
  namespace: default
spec:
  # Istio服务 IstioService[]
  services:
  - name: foo
    namespace: bar
  # API规范引用 HTTPAPISpecReference[]
  api_specs:
  - name: petstore
    namespace: default
IstioService

这不是一个CRD,但是多个CRD会引用它,IstioService用于识别一个服务(包括可选的版本),服务的FQDN由名称、命名空间、实现相关的Domain后缀组成。

IstioService包括的字段如下:

字段 说明
name 服务的短名称
namespace 服务的命名空间
domain 服务的DNS后缀
service FQDN
labels 标签集
QuotaSpec

指定用于每个独立请求的配额规则集。示例:

YAML
1
2
3
4
5
6
7
8
9
10
# QuotaRule[] 此规则集包括的规则列表
rules:
  # AttributeMatch[] 如果为空,匹配所有请求。仅仅匹配的请求才应用配额
- match:
  # Quota[] 使用的配额列表
  quotas:
    # 配额的名称
  - name: string
    # 配额的量
    charge: int64
QuotaSpecBinding

用于指定配额规则集和IstioService之间的绑定关系。示例:

YAML
1
2
3
4
5
6
7
# IstioService[] 服务列表
services:
# QuotaSpecBinding.QuotaSpecReference[] 对配额规则集的引用
quotaSpecs
  # QuotaSpec对象的名字和命名空间
- name: string
  namespace: string
ServiceConfig

定义每个服务的(per-service)的客户端配置。示例:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 是否禁用Check调用
disableCheckCalls: bool
# 是否禁用Report调用
disableReportCalls: bool
# 不管是Check/Report调用,都发送下面的属性到Mixer
mixerAttributes: istio.mixer.v1.Attributes    
# 用于生成API相关属性的HTTPAPISpec列表
httpApiSpec: HTTPAPISpec[]
# 用于生成配额需求的QuotaSpec列表
quotaSpec: QuotaSpec[]
# 如果客户端无法连接到Mixer时的行为
# 覆盖网格级别的策略istio.mixer.v1.config.client.TransportConfig.networkfailpolicy
networkFailPolicy:
  # FAIL_OPEN  如果无法连接到Mixer,允许请求并将其转发给目标服务
  # FAIL_CLOSE 如果无法连接到Mixer,不允许请求
  policy: NetworkFailPolicy.FailPolicy
  # 最大重试次数
  maxRetry: uint32
  # 基本的重试等待延迟。实际使用的延迟以此值为基础进行指数backoff和抖动
  baseRetryWait: google.protobuf.Duration
  # 最大重试等待延迟
  maxRetryWait: google.protobuf.Duration
 
# 默认的转发给上游的属性列表。典型包括source.ip、source.uid
# 转发的属性,其优先级高于静态Mixer属性。具体来说优先级从低到高为:
#   过滤器中配置的静态Mixer属性
#   路由配置中的静态Mixer属性
#   来自源过滤器配置的转发属性
#   来自源路由配置的转发属性
#   从请求元数据推导出的属性
forwardAttributes: istio.mixer.v1.Attributes
TcpClientConfig

定义客户端的TCP配置。示例:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# TCP传输配置
transport: TransportConfig
  # 是否禁用Check缓存
  disableCheckCache: bool
  # 是否禁用Quota缓存
  disableQuotaCache: bool
  # 是否禁用批量Report
  disableReportBatch: bool
  # 无法连接到Mixer时是否拒绝请求
  networkFailPolicy: NetworkFailPolicy
  # 将客户端统计信息刷入到Envoy共享内存的间隔。默认10s
  statsUpdateInterval: google.protobuf.Duration
  # 可以将Check、Report请求发送给不同的Mixer集群
  # 会将Check请求转发到一组Mixer服务器的Cluster的名称,默认mixer_server
  checkCluster: string
  # 会将Report请求转发到一组Mixer服务器的Cluster的名称,默认mixer_server
  reportCluster: string
  # 默认的转发给Mixer上游集群的属性,典型包括source.ip、source.uid
  # 这些属性被位于Mixer前面的Proxy(Sidecar)消费
  attributesForMixerProxy: istio.mixer.v1.Attributes
# 不管是Check/Report调用,都发送下面的属性到Mixer
mixerAttributes: istio.mixer.v1.Attributes
# 禁用Check或Report调用
disableCheckCalls: bool
disableReportCalls: bool
connectionQuotaSpec: QuotaSpec
# 对于长TCP连接,发送周期性报告的间隔。默认10s
# 不得小于1s
reportInterval: google.protobuf.Duration 
mixs命令
crd子命令

列出Mixer可用的CRD:

Shell
1
2
3
4
5
6
7
8
# 列出此二进制文件中所有CRD
mixs crd all
 
# 列出此二进制文件中所有适配器的CRD
mixs crd adapter
 
# 列出此二进制文件中所有模板的CRD
mixs crd instance
probe子命令

对本地运行的Mixer服务进行liveness/readiness探测。

server子命令

启动一个Mixer服务器。选项:

选项 说明
--adapterWorkerPoolSize 适配器工作线程池最多包含的Goroutine数量,默认1024
--address Mixer的gRPC API的地址,例如tcp://127.0.0.1:9092或unix:///path/to/file 
--port 如果不指定--address,Mixer的gRPC API的端口,默认9091
--apiWorkerPoolSize API工作线程池最多包含的Goroutine数量,默认1024
--configDefaultNamespac 网格范围的配置信息存放在什么命名空间,默认istio-system
--configStoreURL 配置存储的URL,例如k8s://path_to_kubeconfig或fs://,对于k8s://(path_to_kubeconfig为空)则使用in-cluster的kubeconfig
--ctrlz_address ControlZ自省机制的监听地址,默认127.0.0.1
--ctrlz_port ControlZ自省机制的监听端口,默认9876
--livenessProbeInterval liveness探针文件的更新间隔,默认0s
--livenessProbePath liveness探针文件的存储位置
--readinessProbeInterval readiness探针文件的更新间隔,默认0s
--readinessProbePath readiness探针文件的存储位置
--log_as_json 日志输出为console还是json格式
--log_caller 需要包含调用者信息的日志Scope列表,Scope可用:adapters, api, attributes, default, grpcAdapter
--log_output_level 日志输出级别,格式为scope:level,scope:level...,级别可用:debug, info, warn, error, none
--log_target 日志输出目标,默认stdout
--maxConcurrentStreams 每个TCP链接上开启的出站GRPC流最大数量,默认1024
--maxMessageSize 每个GRPC消息的最大长度,默认1048576
--monitoringPort 暴露Mixer自我监控信息的端口,默认9093
--numCheckCacheEntries (策略)检查(Check)缓存条目的最大数量,默认1500000
--profile 启用基于Web接口http://host:port/debug/pprof的性能剖析
--singleThreaded 如果为true,则所有针对Mixer的请求都会在单个Goroutine中处理,用于调试
--trace_jaeger_url Jaeger的地址,例如http://jaeger:14268/api/traces?format=jaeger.thrift
--trace_log_spans 是否打印追踪Span的日志
--trace_zipkin_url Zipkin的地址,例如http://zipkin:9411/api/v1/spans
策略和遥测配置
三类自定义资源
Handlers

每种适配器都有自己的CRD,下面的例子是listchecker。该适配器检查输入参数是否在列表中,如果是,适配器返回true:

YAML
1
2
3
4
5
6
7
8
apiVersion: config.istio.io/v1alpha2
kind: listchecker
metadata:
  name: staticversion
  namespace: istio-system
spec:
  providerUrl: http://white_list_registry/
  blacklist: false

下面的例子是prometheus,该适配器消费指标并且进行预聚合:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
apiVersion: config.istio.io/v1alpha2
kind: prometheus
metadata:
  name: handler
  namespace: istio-system
spec:
  metrics:
  # 指标列表
  - name: request_count
    # 使用哪个instance
    instance_name: requestcount.metric.istio-system
    # 指标类型
    kind: COUNTER
    # 额外添加的标签,这里指定的都是instance的dimensions元素
    label_names:
    - destination_service
    - destination_version
    - response_code
  - name: request_duration
    instance_name: requestduration.metric.istio-system
    kind: DISTRIBUTION
    label_names:
    - destination_service
    - destination_version
    - response_code
    buckets:
      explicit_buckets:
        bounds: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
Instances

每种Instance都有自己的CRD,用于将请求属性映射为适配器的输入。下面的例子为prometheus适配器提供输入:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: config.istio.io/v1alpha2
kind: metric
metadata:
  name: requestduration
  namespace: istio-system
spec:
  # 注意可以用 | 提供默认值
  value: response.duration | "0ms"
  # 额外的标签
  dimensions:
    destination_service: destination.service | "unknown"
    destination_version: destination.labels["version"] | "unknown"
    response_code: response.code | 200
  monitored_resource_type: '"UNSPECIFIED"'
Rules

指定在何时调用handler,传递什么instance给它。下面的规则表示,如果被访问的服务是service1,且请求头x-user为user1,则将名为requestduration的metric传递给Prometheus:

YAML
1
2
3
4
5
6
7
8
9
10
11
apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
  name: promhttp
  namespace: istio-system
spec:
  match: destination.service == "service1.ns.svc.cluster.local" && request.headers["x-user"] == "user1"
  actions:
  - handler: handler.prometheus
    instances:
    - requestduration.metric.istio-system
配置请求限速

适配器memquota可以用于实现流量配额,也就是限制请求速率。在生产环境下常常使用redisquota代替之。

要实现速率限制,需要在Client、Mixer两个地方进行配置:

  1. Client:
    1. QuotaSpec,指定配额的名称,客户端需要请求的量
    2. QuotaSpecBinding,条件性的将QuotaSpec关联到一个或多个服务
  2. Mixer:
    1. quota instance:从请求总抽取配额使用信息
    2. memquota handler:处理instance的适配器
    3. quota rule:规定何时将instance发送给handler
限速逻辑

每个Quota实例都维护了一组计数器的集合,集合的大小即为dimensions的笛卡尔积。

当一个新请求到来时,如果Mixer发现超过在validDuration时间内超过maxAmount限制,则它发送RESOURCE_EXHAUSTED消息给请求方的Envoy,Envoy则返回429响应码给调用者。

memquota的时间区间使用滑动窗口的方式,分辨率为亚秒级。redisquota则既支持滚动窗口、也支持固定窗口算法。

memquota

适配器配置如下:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
apiVersion: "config.istio.io/v1alpha2"
kind: memquota
metadata:
  name: handler
  namespace: istio-system
spec:
  # 定义了四个不同的限速模式(Scheme),本质上是依据请求属性的不同,应用不同的限速规则
  quotas:
    # 针对的instance的全限定名称
  - name: requestcount.quota.istio-system
    # 第一个,默认模式:对于不匹配任何overrides的请求,应用 500 req/s 的限制
    maxAmount: 500
    validDuration: 1s
    overrides:
    # 第二个,不管源(source,在本示例中即请求头x-forwarded-for)是谁,只要目标(服务)是reviews,应用 1 req/5s 的限制
    - dimensions:
        destination: reviews
      maxAmount: 1
      validDuration: 5s
    # 第三个,如果源的IP是10.28.11.20,且目标是productpage服务,应用 500 req/s 的限制
    - dimensions:
        destination: productpage
        source: "10.28.11.20"
      maxAmount: 500
      validDuration: 1s
    # 第四个,如果目标是productpage服务,应用 2 req/5s 的限制
    - dimensions:
        destination: productpage
      maxAmount: 2
      validDuration: 5s

对于每个请求,从上往下寻找匹配的overrides,如果发现匹配则不再继续寻找,如果找不到匹配使用默认模式。

quota

该Instance从请求中抽取memquota所需的输入:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: "config.istio.io/v1alpha2"
kind: quota
metadata:
  name: requestcount
  namespace: istio-system
spec:
  dimensions:
    # 源,以x-forwarded-for头为准,此头是请求经过的代理的链,逗号分隔,第一个是真实IP
    source: request.headers["x-forwarded-for"] | "unknown"
    # 目的服务,优先取app标签
    destination: destination.labels["app"] | destination.service | "unknown"
    # 目的服务的版本,取version标签
    destinationVersion: destination.labels["version"] | "unknown"
rule
YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
  name: quota
  # 需要注意,官方下载的sample中忘记了下面这行
  namespace: istio-system
spec:
  # 条件性配额,只有满足条件的请求才被限速
  match: match(request.headers["cookie"], "user=*") == false
  actions:
  # 将handler和instance绑定
  - handler: handler.memquota
    instances:
    - requestcount.quota
quotaspec

配额规格,可以指定单次请求会消耗哪些配额,消耗的量是多大:

YAML
1
2
3
4
5
6
7
8
9
10
11
apiVersion: config.istio.io/v1alpha2
kind: QuotaSpec
metadata:
  name: request-count
  namespace: istio-system
spec:
  rules:
  - quotas:
    # 每个请求消耗多少份量的requestcount配额。如果配额500,该参数设置为5,则validDuration时间区间内你最多请求100次
    - charge: 1
      quota: requestcount
quotaspecbinding

将配额规格关联到需要启用限速功能的服务:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: config.istio.io/v1alpha2
kind: QuotaSpecBinding
metadata:
  name: request-count
  namespace: istio-system
spec:
  quotaSpecs:
  - name: request-count
    namespace: istio-system
  services:
 
  # default命名空间的productpage服务,客户端访问它时,会被限速
  - name: productpage
    # 由于被限制的服务不在当前命名空间(istio-system),需要明确指定
    namespace: default

如果要将配额绑定到所有服务,限制它们作为客户端时的访问速率,则:

YAML
1
  - service: '*'
测试效果

上述几个CR可以通过下面的命令创建:

Shell
1
kubectl apply -f samples/bookinfo/policy/mixer-rule-productpage-ratelimit.yaml

然后,反复刷新/productpage, 你会接收到错误提示:RESOURCE_EXHAUSTED:Quota is exhausted for: requestcount。这个信息是被调用服务的Envoy的响应体,它同时提供了429状态码。

查看productpage的日志,可以看到:

Shell
1
[2018-11-29T13:37:47.294Z] "GET /productpageHTTP/1.1" 429

再查看调用者即入口网关的日志,可以看到: 

Shell
1
"GET /productpageHTTP/1.1" 429 ... outbound|9080||productpage.default.svc.k8s.gmem.cc

被限速的情况下,入口网关根本没有访问到productpage,直接被后者的Sidecar给拦截了。B

关于命名空间

本例中大部分资源都在istio-system中创建,如果你希望仅仅影响单个命名空间而非整个服务网格,则可以把它们都创建到指定的其它命名空间。

安全特性基础

将单体应用分解为微服务可以获得很多好处,例如更灵活、更容易扩容、更容易重用。但是,微服务也引入额外的安全需求:

  1. 为了防止中间人攻击,需要进行流量加密。安全内网环境下可以忽略
  2. 为了提供灵活的访问控制,需要双向TLS认证,以及细粒度的访问控制策略
  3. 为了审计谁在什么时候访问了什么,需要审计工具 

Istio的安全特性能够满足以上需求。Istio安全支持:

  1. 强身份验证(strong identity)
  2. 强大的策略配置
  3. 透明的TLS传输
  4. AAA —— 认证、授权、审计
架构

和Istio安全有关的组件包括:

  1. Citadel,负责密钥、证书管理
  2. Sidecar和周边代理(perimeter proxies,运行在Ingress/Egress的代理 ),实现客户端 - 服务之间的安全通信
  3. Pilot,分发身份验证策略、安全命名信息给代理
  4. Mixer,管理授权和审计

身份标识

身份标识(Identity)是任何安全基础设施的基础。两个服务开始交互前,需要相互交换自己身份的凭证信息,以进行双向身份验证:

  1. 客户端利用安全命名(secure naming information)信息检查服务器的身份,以确保它是服务的授权提供者(Runner)
  2. 在服务器端:
    1. 利用授权策略(authorization policies)来决定是否接受客户访问
    2. 记录审计日志 —— 谁在何时访问了什么
    3. 根据其使用的服务,对客户端计费
    4. 拒绝没付费的客户端的访问请求

在不同平台上,Istio使用不同方式来实现服务身份(Service identities):

  1. 在K8S上使用ServiceAccount
  2. 在GKE/GCE上使用GCP服务账号
  3. 在AWS上使用AWS IAM用户/角色账号
  4. 裸机器,可以使用用户账号、自定义服务账号、服务名称、Istio服务账号,等等
SPIFFE

SPIFFE标准提供了一套框架,用来向复杂环境下的任何服务授予身份标识。

Istio兼容SPIFFE,例如在K8S中,X.509证书具有URI字段,并且格式为: spiffe://<domain>/ns/<namespace>/sa/<serviceaccount>,这允许K8S的服务和其它SPIFFE兼容系统进行交互。

密钥和证书

Istio的PKI建立在Citadel之上,能够安全的为任何工作负载提供代表身份的数字证书(X.509,SPIFFE格式),并且支持密钥和证书的自动轮换(更新)。

在K8S场景下:

  1. Citadel会监控API Server,为所有Service Account创建SPIFFE格式的密钥对,并且存储为K8S的Sercret
  2. 当创建Pod后,Pod使用的Service Account的密钥对会挂载为卷
  3. Citadel会监控证书的生命周期,并自动轮换,然后自动更新Secret,从而让Pod自动获得更新
  4. Pilot会创建安全命名信息,此信息定义了什么Service Account能够运行什么服务。安全命名信息被传播给Envoy
身份验证

Istio提供两种类型的身份验证:

  1. 传输身份验证:即服务-服务身份验证。 认证直接的请求发起者的身份,Istio支持双向身份验证
  2. 源(Origin)身份验证:即终端用户身份验证。验证最初发起请求的用户或者设备。Istio支持基于JSON Web Token(JWK)的请求级身份验证

通过配置服务的身份验证策略(authentication policies),可以为其启用身份验证功能。

双向TLS认证 

通过服务消费者、提供者的Envoy代理,Istio实现了服务-服务通信的安全隧道。请求的处理步骤如下:

  1. 将消费者的请求重新路由到本地的Sidecar —— Envoy
  2. 消费者Envoy向提供者的Envoy发起双向认证的TLS握手。在握手期间,消费者Envoy也会对提供者进行安全命名检查,看看它的Service Account是否有权提供目标服务
  3. 两个Envoy建立TLS连接,流量通过此连接转发
  4. 提供者确认消费者有权限访问后,将流量转发给本地的应用容器
安全命名

安全命名信息是一个多对多映射,从编码在数字证书中的服务的身份标识(K8S中为Service Account),到(被发现服务或DNS引用的)服务名称。从A到B的映射,其含义是A有资格运行B服务。

Pilot会监控K8S的API Server,取得Service Account + Pod等信息,生成安全命名信息,并安全的分发给Envoy。

Node Agent

在K8S中,Node Agent负责证书、密钥的提供,流程如下:

  1. Citadel创建一个gRPC服务,负责处理CSR请求
  2. Envoy通过SDS(Secrets discovery service) API发送证书、密钥请求给Node Agent
  3. Node Agent会根据请求来创建私钥、发送CSR给Citadel来签名。私钥一直不会离开节点,保证了安全性
  4. Citadel校验CSR并签名证书
  5. Node Agent将签名后的证书通过SDS API返回给Envoy

证书轮换时流程类似。

权限控制

Istio使用基于角色的权限控制(RBAC),允许命名空间级别、服务级别、HTTP方法级别的访问控制。Istio支持:

  1. 基于角色的授权,简单易用
  2. 服务-服务、终端用户-服务,两种访问控制
  3. 高性能,授权直接在Envoy上发生

架构图如下:

安全特性配置
Citadel配置
使用外部CA证书

默认情况下Citadel会自己生成密钥对并自签名,作为CA证书。你也可以提供现有的CA证书,只需要创建对应的保密字典即可:

Shell
1
2
3
4
5
6
7
8
9
kubectl create secret generic cacerts -n istio-system \
    # CA证书
    --from-file=certs/ca-cert.pem \
    # CA密钥
    --from-file=certs/ca-key.pem \
    # 根证书
    --from-file=certs/root-cert.pem \
    # 证书链
    --from-file=scerts/cert-chain.pem

你需要使用如下参数重新部署: --set values.global.mtls.enabled=true,values.security.selfSigned=false,Citadel会使用该CA证书为工作负载的数字证书进行签名。 

为了确保工作负载使用新的证书,你可能需要删除Citadel生成的、命名为istio.*的Secrets。

通过SDS分发身份标识

默认情况下,Citadel为工作负载生成密钥和证书,并且通过Secret卷挂载给Sidecar。这种方式的缺点包括:

  1. 证书轮换时的性能减退:混乱时Envoy需要热重启来得到新的证书、密钥
  2. 可能的安全隐患,因为私钥保存在K8S Secret中

除了使用适当的Helm参数之外,你还需要修改API Server,添加命令行参数:

Shell
1
2
--service-account-issuer=kubernetes.default.svc
--service-account-signing-key-file=/etc/kubernetes/pki/sa.key 
服务端传输身份验证

这里的策略都是针对服务端(服务提供者)的:

自动mtls

与此特性相关的istio配置参数为:enableAutoMtls,对应的注入istio-proxy环境变量为ISTIO_AUTO_MTLS_ENABLED。

当启用此特性时,你仅仅需要配置授权策略(Authentication policy),而不需要配置DestinationRule的字段:

  1. 对于具有Sidecar的服务端负载,Istio自动配置客户端,使用mTLS流量
  2. 对于没有Sidecar的服务器负载,Istio自动配置客户端,使用明文

要启用,设置Chart参数 --set values.global.mtls.auto=true  --set values.global.mtls.enabled=false

全局强制mtls

你可以为整个服务网格设置默认的身份验证策略(不指定targets):

YAML
1
2
3
4
5
6
7
8
9
apiVersion: "authentication.istio.io/v1alpha1"
# 全局授权策略
kind: "MeshPolicy"
metadata:
  # 名字必须为default
  name: "default"
spec:
  peers:
  - mtls: {}

上述配置应用后,所有被网格管理的服务仅仅接受基于TLS加密的请求。

Chart参数global.mtls.enabled=true则自动配置上述MeshPolicy。这样,如果目标服务对应的DestinationRule没有对应的mTLS配置,则无法访问。

命名空间启用mtls

或者为某个命名空间设置默认的身份验证策略:

YAML
1
2
3
4
5
6
7
8
apiVersion: "authentication.istio.io/v1alpha1"
kind: "Policy"
metadata:text-processing-with-gotext-processing-with-go
  name: "default"
  namespace: "ns1"
spec:
  peers:
  - mtls: {}
服务启用mtls
YAML
1
2
3
4
5
6
7
8
9
10
11
apiVersion: "authentication.istio.io/v1alpha1"
kind: "Policy"
metadata:
  name: "reviews"
spec:
  targets:
  # 对于reviews服务
  - name: reviews
  peers:
  # 必须启用双向的TLS
  - mtls: {} 

使用mutual TLS时,客户端必须在DestinationRule中配置TLSSettings。

目标选择器

身份验证策略所针对的服务,在targets字段中配置:

YAML
1
2
3
4
5
6
7
8
spec:
  targets:
   # 任何单品页服务
   - name: product-page
   # 运行在9000端口的reviews服务
   - name: reviews
     ports:
     - number: 9000
可选验证

peers字段定义了服务-服务的身份验策略。0.7版本的Istio仅支持mTLS:

YAML
1
2
3
spec:  
  peers:
    - mtls: {}

配置mode为PERMISSIVE,则mTLS是可选的,允许基于明文发起请求:

YAML
1
2
3
peers:
- mTLS:
    mode: PERMISSIVE
客户端传输身份验证

有了上面的、针对服务端的配置后,如果客户端不经任何配置,所有请求都会遭遇503错误。

要解决此问题,你必须为客户端配置DestinationRule,声明使用mtls:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: "networking.istio.io/v1alpha3"
kind: "DestinationRule"
metadata:
  name: "default"
  namespace: "default"
spec:
  # 针对所有DNS名称的访问,都启用mtls
  host: "*.svc.k8s.gmem.cc"
  trafficPolicy:
    tls:
      # Istio会自动设置客户端私钥和证书的路径
      mode: ISTIO_MUTUAL

不要忘记,DestinationRule也用于流量管理的配置。因此,如果某个具体服务需要独立的DestinationRule配置,一定要加上tls配置,因为上述DestinationRule被覆盖,而不是合并。

客户端证书位置

Istio会自动把客户端证书注入到所有Sidecar的/etc/certs目录下:

Shell
1
2
3
4
5
6
7
kubectl exec $(kubectl get pod -l app=httpbin -o jsonpath={.items..metadata.name}) -c istio-proxy -- ls /etc/certs
# 证书链
# cert-chain.pem
# 客户端私钥
# key.pem
# CA跟证书,用于验证服务端
# root-cert.pem
与网格外部交互

本节介绍当启用mTLS后,服务网格内外进行通信的可能性。

从网格外部发请求

对于来自网格外部(包括没有Sidecar的集群内部组件)的请求,由于无法初始化mTLS连接,一定会失败。curl会得到退出码56,表示接收网络数据失败。

这个问题目前无解,除非降低目标服务的身份验证需求。

向网格外部发请求

同样会失败,因为客户端会发起mTLS连接,而没有Sidecar的外部服务无法处理这种连接。

解决办法是为目标服务配置DestinationRule,并禁用TLS:

YAML
1
2
3
4
5
6
7
8
9
10
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: "httpbin-noistio"
spec:
host: "httpbin.noistio.svc.k8s.gmem.cc"
trafficPolicy:
   tls:
     # 禁用TLS
     mode: DISABLE
向API Server发请求

Kubernetes的API Server也没有Sidecar,如果网格内部服务需要访问K8S API Server,则也需要DestinationRule配置:

YAML
1
2
3
4
5
6
7
8
9
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: "api-server"
spec:
host: "kubernetes.default.svc.k8s.gmem.cc"
trafficPolicy:
   tls:
     mode: DISABLE
源身份验证
JWT认证

origins字段定义了终端用户的身份验证策略。目前仅仅支持JWT认证。

YAML
1
2
3
4
5
6
7
8
9
10
spec:
  origins:
  # 承认Google签发的JWT
  - jwt:
      issuer: "https://accounts.google.com"
      jwksUri: "https://www.googleapis.com/oauth2/v3/certs"
    # 哪些路径不需要身份验证
    trigger_rules:
    - excluded_paths:
      - exact: /health
身份验证策略更新

你可以随时修改身份验证策略,Istio会实时的推送给Envoy,但是不保证同时送达。为了保证行为一致性:

  1. 需要启用/禁用mTLS时,先切换为临时的mode: PERMISSIVE,这样明文和加密都可以接受
  2. 对于JWT源身份认证,在切换策略之前,请求必须携带新的JWT
启用授权

要启用访问控制,你需要配置CRD —— RbacConfig。这是一个全局的单例对象,名字必须为default:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: "rbac.istio.io/v1alpha1"
kind: RbacConfig
metadata:
  name: default
spec:
  # OFF 禁用访问控制
  # ON  为所有服务启用访问控制
  # ON_WITH_INCLUSION 仅仅为指定的服务、命名空间启用访问控制
  # ON_WITH_EXCLUSION 除了指定的服务、命名空间,都启用访问控制
  mode: 'ON_WITH_INCLUSION'
  # 指定启用访问控制的命名空间、服务
  inclusion:
    namespaces: ["default"]
授权策略
ServiceRole

已经废弃,使用AuthorizationPolicy代替。

定义一个角色,并声明该角色对哪些服务具有哪些访问权限。

ServiceRole包含一系列rules,也就是许可(permissions):

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: "rbac.istio.io/v1alpha1"
kind: ServiceRole
metadata:
  name: service-admin
  namespace: default
spec:
  # 许可列表
  rules:
    # 针对的服务,通配符*表示当前命名空间的任何服务。admin-*表示任何以admin-开头的服务
  - services: ["*", "admin-*", "httpbin.default.svc.k8s.gmem.cc"]
    # 允许的HTTP方法,对于gRPC请求来说,HTTP方法总是POST。通配符*表示允许任何方法
    methods: ["*"]
    # 允许的URL路径,或者gRPC方法。
    # gRPC方法必须是 /packageName.serviceName/methodName 形式,大小写敏感
    paths:
    - /admin
    # 允许 /books/reviews, /events/booksale/reviews, /reviews
    - */reviews

除了限定命名空间 + 服务 + 动词 + 路径以外,你还可以提供额外的约束,例如限定请求头:

YAML
1
2
3
4
    constraints:
    # 请求头中的version必须声明为v1或v2
    - key: request.headers[version]
      values: ["v1", "v2"]
ServiceRoleBinding

已经废弃,使用AuthorizationPolicy代替。

为用户、组、服务授予ServiceRole。示例:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apiVersion: "rbac.istio.io/v1alpha1"
kind: ServiceRoleBinding
metadata:
  name: test-binding-products
  namespace: default
spec:
  # 被授予角色的主体
  subjects:
    # 主体为service-account-a,它是服务a使用的service-account
  - user: "service-account-a"
    # 主体为Istio的Ingress服务账号
  - user: "istio-ingress-service-account"
    # 同时要求请求的JWT的email为me@gmem.cc
    properties:
      request.auth.claims[email]: "me@gmem.cc"
    # 允许任何人访问服务
  - user: "*"
    # 允许任何通过身份验证的人访问服务
  - properties:
      source.principal: "*"
  # 角色
  roleRef:
    kind: ServiceRole
    name: "products-viewer"
AuthorizationPolicy

要配置Istio授权策略,现在应该使用这个新API。

每个AuthorizationPolicy包含:

  1. 一个选择器,对应selector字段,指定策略作用于的Target,selector和metadata.namespace共同决定了Target
  2. 多个规则,每个规则指定Who允许在什么Conditions下做What动作
    1. rule.from字段即Who
    2. rule.to字段对应What
    3. rule.when对应Conditions

示例,如果请求来自dev命名空间或SA账号并且携带了合法JWT则允许访问foo命名空间的httpbin:v1服务:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
# 对foo命名空间的httpbin服务
metadata:
name: httpbin
namespace: foo
spec:
selector:
   matchLabels:
     app: httpbin
     version: v1
rules:
# 不指定from则针对所有用户
- from:
   # 允许SA和命名空间
   - source:
       principals: ["cluster.local/ns/default/sa/sleep"]
   - source:
       namespaces: ["dev"]
   # 所有通过身份验证的用户
   - source:
       principals: ["*"]
 
   # 进行GET操作
   to:
   - operation:
       methods: ["GET"]
       paths: ["/test/*", "*/info"]
 
   when:
   # 如果具有合法的JWT
   - key: request.auth.claims[iss]
     values: ["https://accounts.google.com"]
   # 所有支持的键 https://istio.io/docs/reference/config/security/conditions/
   - key: request.headers[version]
     values: ["v1", "v2"]

 对于任何Plain TCP协议,Istio授权策略也支持,配置方式和上面这个HTTP的例子类似,但是需要注意某些字段仅仅支持HTTP。

Sidecar配置

要定制Sidecar的行为,可以修改istio-sidecar-injector这个Configmap,或者给Pod添加对应的注解。

istio-sidecar-injector声明了需要给目标Pod注入的两个容器 —— istio-init、istio-proxy —— 的全部配置信息。这两个容器会自动读取Pod上的注解,覆盖对应的默认配置。

默认配置在安装时,通过Helm Chart的Values传入。

禁止注入
特定资源

如果当前命名空间启用了自动化的Sidecar注入,而某个Deployment却不想加入网格,可以在其PodTemplate上添加注解:

YAML
1
2
3
4
5
6
7
8
9
10
11
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: ignored
spec:
  template:
    metadata:
      annotations:
        # 禁止注入
        sidecar.istio.io/inject: "false"
    spec:
基于选择器

你还可以修改配置字典istio-sidecar-injector,指定什么样的Pod绝不被注入Sidecar:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: ConfigMap
metadata:
  name: istio-sidecar-injector
data:
  config: |-
    policy: enabled
    neverInjectSelector:
      - matchExpressions:
        - {key: openshift.io/build.name, operator: Exists}
      - matchExpressions:
        - {key: openshift.io/deployer-pod-for.name, operator: Exists} 
istio-init
命令行选项

此容器运行脚本/usr/local/bin/istio-iptables.sh,命令行选项如下:

  默认值 说明
-p 15001 通过Iptables将所有TCP流量重定向到什么端口,Envoy必须在此端口上监听
-u 1337 对于什么用户的流量,不进行重定向,通常是代理容器的UID,此UID默认值为$ENVOY_USER=1337
-g   对于什么用户组的流量,不进行重定向
-m REDIRECT 重定向入站流量到Envoy的方式。REDIRECT或TPROXY
-i *

逗号分隔的CIRD形式的IP范围列表,匹配此CIRD的出站流量将被重定向给Envoy。设置为*则所有出站流量都被重定向给Envoy

如果没有配置适当的Istio资源,例如ServiceEntry,Envoy默认情况下可能返回404,这导致默认情况下你无法访问网格外部的服务

-x

""

当上一个参数设置为*时,用来指明哪些出站流量不重定向给Envoy,形式与上一参数相同

不重定向给Envoy,则流量的行为和没有部署服务网格时一致

-b ""

逗号分隔的端口列表,来自这些端口的入站流量将重定向给Envoy。设置为*则所有入站流量都重定向给Envoy,设置为空则禁用入站流量重定向

默认情况下,已经在匹配当前Pod的K8S Service声明的端口,重定向给Envoy,也就是说,虽然容器监听了8086端口,但是没有配置相应的K8S Service,就不走Envoy,因而不能进行策略、安全检查

-d 15020

当上一个参数设置为*时,用来指定哪些入站流量不重定向给Envoy,形式与上一参数相同

默认情况下,Pilot代理状态端口15020不重定向给Envoy

注解列表

所有注解:https://istio.io/docs/reference/config/annotations/

常用的注解如下:

注解 对应选项
sidecar.istio.io/interceptionMode

-m

traffic.sidecar.istio.io/includeOutboundIPRanges -i
traffic.sidecar.istio.io/excludeOutboundIPRanges

-x

traffic.sidecar.istio.io/includeInboundPorts -b
traffic.sidecar.istio.io/excludeInboundPorts -d

traffic.sidecar.istio.io/*会导致Iptables规则的变更。例如:

Shell
1
2
3
4
5
6
7
8
9
10
11
# 注解:
# traffic.sidecar.istio.io/includeOutboundIPRanges: "221.0.0.0/8"
# 产生
iptables -t nat -A ISTIO_OUTPUT -d 221.0.0.0/8 -j ISTIO_REDIRECT
 
# 注解:
# traffic.sidecar.istio.io/includeOutboundIPRanges: "*"
# traffic.sidecar.istio.io/excludeOutboundIPRanges: "221.0.0.0/8"
# 产生:
iptables -t nat -A ISTIO_OUTPUT -j ISTIO_REDIRECT
iptables -t nat -A ISTIO_OUTPUT -d 221.0.0.0/8 -j RETURN

istio-init会把上述规则打印到标准输出。

istio-proxy

可以修改istio-sidecar-injector中istio-proxy容器的命令行参数,从而间接的影响Envoy的行为。

命令行选项

此代理的入口点是pilot-agent,该进程会创建envoy进程,执行下面的命令可以了解可传递给Envoy的参数列表:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
docker run -it --rm registry.k8s.arch.mid/istio/proxyv2:1.0.3 proxy --help
# 查看proxy子命令(启动envoy)的参数:
# --applicationPorts stringSlice      应用程序暴露的端口        
# --availabilityZone string           可用性区域
# --binaryPath string                 Envoy的位置,默认/usr/local/bin/envoy
# --concurrency int                   工作线程数量
# --configPath string                 自动产生的Envoy配置文件位置,默认/etc/istio/proxy
# --connectTimeout duration           Envoy连接到支持性服务的超时,默认1s  
# --controlPlaneAuthPolicy string     控制平面身份验证策略,默认NONE      
# --customConfigFile string           自定义配置文件的位置
# --disableInternalTelemetry          禁用内部遥测
# --discoveryAddress string           暴露xDS的发现服务的地址,默认istio-pilot:15007
# --discoveryRefreshDelay duration    服务发现轮询间隔,EDS, CDS, LDS使用,RDS不使用,默认1秒
# --domain string                     DNS后缀,默认${POD_NAMESPACE}.svc.cluster.local
# --drainDuration duration            热重启时Envoy Drain连接的时间,默认2秒
# --id string                         代理的唯一标识,默认${POD_NAME}.${POD_NAMESPACE}
# --ip string                         代理的IP地址,默认${INSTANCE_IP}                        
# --proxyAdminPort uint16             Envoy监听管理命令的端口,默认15000
# --proxyLogLevel string              Envoy的日志级别,trace, debug, info, warn, err, critical, off。默认warn
# --serviceregistry string            服务注册表的底层平台,Kubernetes, Consul, CloudFoundry, Mock, Config。默认Kubernetes
# --statsdUdpAddress string           Statd的UDP监听地址
# --statusPort uint16                 在什么端口上暴露Pilot代理的状态
# --zipkinAddress string              Zipkin服务的地址
 
# 全局参数:
# --log_as_json                   是否将日志输出为JSON格式,默认输出人类友好的控制台格式
# --log_output_level string       debug, info, warn, error, none,默认default:info
注解配置示例
出站流量默认经过Envoy

下面的配置,可以让位于网格内部的容器访问10.0.0.0/8网段,而不需要定义ServiceEntry即可访问该网络的任意IP(或者DNS名称)的任意端口:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: ubuntu
    tier: devops
  name: ubuntu
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ubuntu
      tier: devops
  template:
    metadata:
      annotations:
        traffic.sidecar.istio.io/includeOutboundIPRanges: "*"
        # 10.0.0.0/8网段直接访问,不走Envoy代理
        # 要仅仅允许单个IP绕过Envoy代理,可以用 10.5.12.157/32的形式
        traffic.sidecar.istio.io/excludeOutboundIPRanges: "10.0.0.0/8"
      labels:
        app: ubuntu
        tier: devops
    spec:
      containers:
      - args:
        - -c
        - sleep 365d
        command:
        - /bin/sh
        image: docker.gmem.cc/ubuntu:16.04
        imagePullPolicy: Always
        name: ubuntu
      dnsPolicy: ClusterFirst
      restartPolicy: Always
出站流量默认绕过Envoy
YAML
1
2
3
# 让容器网络、集群服务网络,以及单个IP地址10.5.12.157走Envoy代理
# 其它地址都不走Envoy代理
traffic.sidecar.istio.io/includeOutboundIPRanges: "172.27.0.0/16,10.96.0.0/12,10.5.12.157/32"

注意一个现象:通过IP地址10.5.12.157访问,可以发现是通过Envoy的(符合预期)。但是通过域名(解析到10.5.12.157)访问,则绕过了Envoy。

Sidecar

使用这个CRD,可以配置其关联的工作负载(如果没有workloadSelector则命名空间所有工作负责)的入站、出站通信规则。

具体参考Sidecar资源一节。

EnvoyFilter

这个CRD用于描述Envoy代理的网络过滤器/HTTP过滤器的配置信息,可以用来定制化Istio生成的Envoy配置。使用EnvoyFilter你可以:

  1. 修改某个字段的值
  2. 添加过滤器
  3. 添加一个完整的Listener、Cluster,等等

使用此CR要小心,错误的配置可能破坏网格。

对于每个工作负载,多个EnvoyFilter可以被增量的应用上去。这些EnvoyFilter的应用顺序是:

  1. 定义在rootNamespace中的所有EnvoyFilter
  2. 定义在工作负载所属命名空间的、匹配工作负载的过滤器
配置说明
配置项 说明
ApplyTo INVALID
LISTENER 给监听器打补丁
FILTER_CHAIN  给过滤器链打补丁
NETWORK_FILTER 给网络过滤器打补丁,增删改HTTP过滤器
HTTP_FILTER 给HTTP过滤器打补丁,增删改HTTP过滤器
ROUTE_CONFIGURATION 修改路由配置(RDS输出),不会修改虚拟主机,仅仅支持MERGE操作
VIRTUAL_HOST 修改路由配置中的虚拟主机
HTTP_ROUTE 修改路由配置中匹配的虚拟主机的路由对象HTTP_ROUTE
CLUSTER  修改集群配置(CDS输出)
InsertPosition.Index FIRST  插入在最前面
LAST 插入在最后
BEFORE  插入在特定过滤器之前
AFTER 插入在特定过滤器之后
Patch.Operation MERGE  将提供的配置合并到现有的、自动生成的配置
ADD  添加到现有的监听器、集群、虚拟主机、网络过滤器、HTTP过滤器的列表中
REMOVE  从列表中移除
INSERT_BEFORE  在命名对象之前插入
INSERT_AFTER 在命名对象之后插入
INSERT_FIRST 插入在列表最前面
PatchContext ANY  修改网关+边车
SIDECAR_INBOUND  修改边车入站配置
SIDECAR_OUTBOUND  修改边车出站配置
GATEWAY 修改网关配置
配置示例

例子一,定义在rootNamespace中的全局默认EnvoyFilter:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: custom-protocol
  # rootNamespace的实际值取决于meshConfig
  namespace: istio-config
spec:
  # Envoy补丁,由于此资源定义在rootNamespace,因此对所有Sidecar有效
  configPatches:
    # 应用到L4过滤器
  - applyTo: NETWORK_FILTER
    # 匹配规则
    match:
      # 匹配出站监听器
      context: SIDECAR_OUTBOUND
      # 匹配9307监听器
      # 的过滤器 envoy.tcp_proxy
      listener:
        portNumber: 9307
        filterChain:
          filter:
            name: "envoy.tcp_proxy"
    # 补丁规格
    patch:
      # 在envoy.tcp_proxy后面插入
      operation: INSERT_BEFORE
      value:
        #  插入的过滤器的name,以及config或typed_config配置
        name: "envoy.config.filter.network.custom_protocol"
        config:
         ...
  - applyTo: NETWORK_FILTER
    match:
      # 匹配HTTP连接管理器(L4)
      listener:
        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"
          idle_timeout: 30s

例子二,匹配特定工作负载的EnvoyFilter:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: reviews-lua
  namespace: bookinfo
spec:
  # 匹配当前命名空间的哪些工作负载
  workloadSelector:
    labels:
      app: reviews
  configPatches:
    # 添加L7过滤器
  - applyTo: HTTP_FILTER
    match:
      # 匹配入站8080监听器
      context: SIDECAR_INBOUND
      listener:
        portNumber: 8080
        filterChain:
          filter:
            name: "envoy.http_connection_manager"
            subFilter:
              name: "envoy.router"
    patch:
      # 在L7过滤器envoy.router之前插入
      operation: INSERT_BEFORE
      value:
       name: envoy.lua
       typed_config:
          "@type": "type.googleapis.com/envoy.config.filter.http.lua.v2.Lua"
         inlineCode: |
           function envoy_on_request(request_handle)
             local headers, body = request_handle:httpCall(
              "lua_cluster",
              {
               [":method"] = "POST",
               [":path"] = "/acl",
               [":authority"] = "internal.org.net"
              },
             "authorize call",
             5000)
           end
  # 上面的Lua代码调用lua_cluster,这里是Cluster的定义
  - applyTo: CLUSTER
    match:
      # 添加出站集群
      context: SIDECAR_OUTBOUND
    patch:
      operation: ADD
      value:
        name: "lua_cluster"
        type: STRICT_DNS
        connect_timeout: 0.5s
        lb_policy: ROUND_ROBIN
        hosts:
        - socket_address:
            protocol: TCP
            address: "internal.org.net"
            port_value: 8888

例子三, 配置入站网关:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: hcm-tweaks
  namespace: istio-system
spec:
  workloadSelector:
    # 这里要和网关的Pod匹配
    labels:
      istio: ingress-gateway
  configPatches:
  - applyTo: NETWORK_FILTER
    match:
      # 配置网关,针对SNI进行匹配
      context: GATEWAY
      listener:
        filterChain:
          sni: app.example.com
          filter:
            name: "envoy.http_connection_manager"
    patch:
      operation: MERGE
      # 修改一些字段
      value:
        idle_timeout: 30s
        xff_num_trusted_hops: 5

例子四,添加监听器过滤器:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: nginx-original-src
  namespace: default
spec:
  workloadSelector:
    labels:
      app: nginx
  configPatches:
  - applyTo: LISTENER
    match:
      context: SIDECAR_INBOUND
      listener:
        portNumber: 80
    patch:
      operation: MERGE
      value:
        # 增加一个监听器过滤器,这会添加在尾部
        listenerFilters:
        - name: envoy.listener.original_src
          config:
            mark: 1986
运维
组件日志

Istio的组件使用了一个灵活的日志框架,你可以很方便的切换日志级别(修改命令行参数),查看某个组件的日志。

日志范围

一个组件输出的日志被分类到scope中,scope代表一组相关的、可以一起控制的日志消息。不同组件具有不同的scopes,所有组件都包含一个名为default的scope,所有未分类的日志消息都在其中。

例如,Mixer包含5个scope,分别对应了Mixer的不同功能区域:adapter、sapi、attributes、default、grpcAdapter。

每个scope都包含以下日志级别:none、error、warning、info、debug。

要控制输出哪些日志,你可以用--log_output_level参数,例如:

Shell
1
mixs server --log_output_level attributes=debug,adapters=warning

你也可以使用ControlZ在运行时动态的修改日志级别。

日志输出

选项--log_target用于重定向日志到任意位置,可以指定逗号分割的系统路径,以及stdout、stderr。

选项--log_as_json可以让日志输出为JSON格式。

日志轮换

选项--log_rotate控制日志轮换的basename。--log_rotate_max_size指定日志的最大MB。--log_rotate_max_backups指定保留的日志文件个数。

调试信息

选项--log_caller和--log_stacktrace_level可以控制日志中是否包含编程级别的信息,用于跟踪组件的潜在缺陷。

组件内省

Istio的组件使用了一个灵活的内省(Introspection)框架,让你能够轻松的查看、操控运行中的组件的内部状态,这个功能叫ControlZ。

组件可以暴露一个端口,此端口提供一个WebUI供你访问ControlZ。

通过ControlZ可以调整日志级别、查看组件内存用量、环境变量、进程信息、命令行参数、版本信息、统计指标。

选项--ctrlz_address、--ctrlz_port用于控制在什么网络接口、端口上暴露ControlZ。 

调试Envoy/Pilot

使用proxy-status命令可以获取网格的整体状态,并识别由代理造成的问题。使用proxy-config命令可以查看Envoy的配置以帮助诊断问题。

网格整体状态
Shell
1
2
3
istioctl proxy-status
# PROXY               CDS        LDS        EDS            RDS      PILOT             VERSION
# details-v1.default  SYNCED     SYNCED     SYNCED (100%)  SYNCED   istio-pilot-8499b 1.0.2

如上面的例子所示,Envoy的CDS(集群发现服务)、LDS(监听器发现服务)、EDS(端点发现服务)、RDS(路由发现服务)的状态都被显示。状态的值可以是:

  1. SYNCED,Envoy已经确认了Pilot发送给它的最后配置
  2. SYNCED (100%) ,Envoy已经完全同步了集群的端点
  3. NOT SENT ,Pilot没有发送任何东西给Envoy
  4. STALE,Pilot发送了更新给Enovy,但是后者没有接收到并给与确认。可能原因是网络故障或者Istio的Bug 
检查未同步配置

执行下面的命令可以查看Pilot配置和某个Envoy实例配置存在什么差异,也就是哪些配置没有同步:

Shell
1
istioctl proxy-status details-v1-876bf485f-bxwh7.default
Dump出Envoy配置

istioctl proxy-config包含以下子命令:

子命令 说明
bootstrap

自举配置,包含一些启动时必要的信息,例如Pilot地址(对于Envoy来说,xDS管理服务器的地址)是什么

命令示例:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// istioctl proxy-config bootstrap productpage-v1-8d69b45c-jltzt
{
    "bootstrap": {
        "node": {
            "id": "sidecar~172.27.226.67~productpage-v1-8d69b45c-jltzt.default~default.svc.k8s.gmem.cc",
            "cluster": "productpage",
            "metadata": {
                // 各种元数据,例如Pod的标签
            },
            // Envoy的版本
            "buildVersion": "0/1.8.0-dev//RELEASE"
        },
        "staticResources": {
            "listeners": [/* 静态配置的监听器 */],
            "clusters": [
                {
                    // Pilot集群,xDS服务器
                    "name": "xds-grpc",
                    "type": "STRICT_DNS",
                    "connectTimeout": "10.000s",
                    "hosts": [
                        {
                            "socketAddress": {
                                "address": "istio-pilot.istio-system",
                                "portValue": 15010
                            }
                        }
                    ],
                },
                {
                    // Tracer集群
                    "name": "zipkin",
                    "type": "STRICT_DNS",
                    "hosts": [
                        {
                            "socketAddress": {
                                "address": "zipkin.istio-system",
                                "portValue": 9411
                            }
                        }
                    ]
                }
            ]
        },
        // 动态发现配置
        "dynamicResources": {
            "ldsConfig": {
                "ads": {
                }
            },
            "cdsConfig": {
                "ads": {
                }
            },
            // ADS,聚合发现服务(Aggregated Discovery Service)
            "adsConfig": {
                "apiType": "GRPC",
                "grpcServices": [
                    {
                        "envoyGrpc": {
                            "clusterName": "xds-grpc"
                        }
                    }
                ],
                "refreshDelay": "1.000s"
            }
        },
        "statsConfig": {
            // 监控指标配置
        },
        // 追踪配置
        "tracing": {
            "http": {
                "name": "envoy.zipkin",
                "config": {
                    "collector_cluster": "zipkin"
                }
            }
        },
        // 管理端口
        "admin": {
            "accessLogPath": "/dev/null",
            "address": {
                "socketAddress": {
                    "address": "127.0.0.1",
                    "portValue": 15000
                }
            }
        }
    }
}
cluster

上游集群配置

命令示例,根据全限定DNS名称查询某个Pod的某个集群:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// istioctl proxy-config clusters productpage-v1-8d69b45c-jltzt \
//          --fqdn details.default.svc.k8s.gmem.cc -o json
 
[
    {
        //     出站集群,即当前节点需要访问的集群
        //               此集群对应的Istio服务是details.default.svc.k8s.gmem.cc:9080
        //                     不是服务子集
        "name": "outbound|9080||details.default.svc.k8s.gmem.cc",
        // 集群的端点通过EDS协议发现
        "type": "EDS",
         // EDS通过ADS协议获得
        "edsClusterConfig": {
            "edsConfig": {
                "ads": {}
            },
            "serviceName": "outbound|9080||details.default.svc.k8s.gmem.cc"
        },
        // 超时和断路器配置,对应Istio的DestinationRule中的配置信息
        // 由于每个子集都产生Cluster,因此单个DestinationRule可以产生多个Cluster配置
        "connectTimeout": "1.000s",
        "circuitBreakers": {
            "thresholds": [
                {}
            ]
        }
    },
    {
        "name": "outbound|9080|v1|details.default.svc.k8s.gmem.cc",
        "type": "EDS",
    },
    {
        "name": "outbound|9080|v2|details.default.svc.k8s.gmem.cc",
        "type": "EDS",
    }
]

可以看到此例子中有三个上游集群的FQDN是details.default.svc.k8s.gmem.cc,它们对应了不同的subset

endpoint

查看某个节点所有集群的端点列表,或者某个集群的端点列表,或者特定端点的信息:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
# 查看全限定DNS名称为kubernetes.default.svc.k8s.gmem.cc的集群列表
istioctl proxy-config cluster ubuntu --fqdn=kubernetes.default.svc.k8s.gmem.cc
 
# 查看上述集群的端点列表                   此参数可以从上个命令的-o json中获得
istioctl proxy-config endpoint  ubuntu --cluster='outbound|443||kubernetes.default.svc.k8s.gmem.cc'
# ENDPOINT          STATUS      CLUSTER
# 10.0.2.1:6443     HEALTHY     outbound|443||kubernetes.default.svc.k8s.gmem.cc
# 10.0.3.1:6443     HEALTHY     outbound|443||kubernetes.default.svc.k8s.gmem.cc
# 10.0.5.1:6443     HEALTHY     outbound|443||kubernetes.default.svc.k8s.gmem.cc
 
# 查看某个特定端点
istioctl proxy-config endpoint ubuntu --address=10.0.2.1 --port=6443 -o json

你也可以使用-o json选项,将集群的端点配置输出为JSON:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
[                                                                                    
    {                      
        // 集群名称                                                        
        "name": "outbound|443||kubernetes.default.svc.k8s.gmem.cc",                  
        "addedViaApi": true,                                                          
        // 端点列表
        "hostStatuses": [                                                              
            {    
                // 端点的地址                                                                  
                "address": {                                                        
                    "socketAddress": {                                                  
                        "address": "10.0.2.1",                                      
                        "portValue": 6443                                            
                    }                                                                
                },        
                // 端点的监控指标                                                          
                "stats": {                                                          
                    "cx_active": {                                                  
                        "type": "GAUGE"                                              
                    },                                                              
                    "cx_connect_fail": {},                                                
                    "cx_total": {},                                                  
                    "rq_active": {                                                  
                        "type": "GAUGE"                                              
                    },                                                              
                    "rq_error": {},                                                  
                    "rq_success": {},                                                
                    "rq_timeout": {},                                                
                    "rq_total": {}                                                  
                },            
                // 端点的健康状态                                                      
                "healthStatus": {                                                      
                    "edsHealthStatus": "HEALTHY"                                          
                }                                                                    
            },
            {
                "address": {
                    "socketAddress": {
                        "address": "10.0.3.1",
                        "portValue": 6443
                    }
                },
                "stats": {
                }
             }
         ]
    }
]

注意,只能将集群的所有端点一起输出,不能输出单个端点

listener

获取监听器配置

下面的例子获取名为ubuntu的工作负载的监听器列表。此工作负载有一个监听8080端口的服务:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# istioctl proxy-config listener ubuntu
ADDRESS            PORT      TYPE
 
# 绝大部分监听器,都是虚拟监听器
# 之所以叫“虚拟”,是因为这些监听器的deprecatedV1.bindToPort = false
# 因而并没有创建对应的监听套接字,通过netstat你可以验证这一事实:
# tcp        0      0 0.0.0.0:15090           0.0.0.0:*               LISTEN    
# tcp        0      0 127.0.0.1:15000         0.0.0.0:*               LISTEN    
# tcp        0      0 0.0.0.0:15001           0.0.0.0:*               LISTEN    
# tcp6       0      0 :::15020                :::*                    LISTEN
# 可以看到,真正监听的只有Prometheus stat、Admin endpoint、
#                     IP table劫持目的端口、代理状态端口
# 警告:这里谈得是以sidecar模式运行的代理
# 以proxy模式运行(ingressgateway、egressgatey)的代理,可能由Gateway资源生成很多物理监听器
 
 
 
# 下面这批,是为每个K8S服务所创建的监听器
# 它们负责接收并处理 Pod ⇨ 0.0.0.0_15001 ⇨ 对应IP:PORT的出站非HTTP流量
10.110.228.191     80        TCP  
10.103.208.103     42422     TCP  
10.106.199.4       15011     TCP  
10.107.171.192     9300      TCP  
10.98.46.166       16686     TCP  
10.107.18.161      14268     TCP  
10.102.62.178      80        TCP  
10.100.71.207      9300      TCP  
10.96.0.10         53        TCP  
10.99.111.5        44134     TCP  
10.101.112.205     15011     TCP  
10.101.112.205     31400     TCP  
10.101.112.205     853       TCP  
10.110.142.202     80        TCP  
10.103.113.112     80        TCP  
10.104.57.35       443       TCP  
10.96.0.1          443       TCP  
10.106.219.132     443       TCP  
10.99.131.204      80        TCP  
10.101.112.205     443       TCP  
10.101.112.205     8060      TCP  
10.98.219.80       443       TCP  
10.109.240.127     9100      TCP  
10.110.126.109     27017     TCP  
10.107.18.161      14267     TCP  
10.96.63.168       80        TCP  
10.97.147.18       443       TCP  
10.99.99.66        443       TCP  
 
# 它们负责接收并处理 Pod ⇨ 0.0.0.0_15001 ⇨ 对应PORT的出站HTTP流量
# 也就是说,此Pod需要对外访问的HTTP服务,如果它们端口相同,则共享一个监听器
# 而不是像非HTTP服务那样,每个IP+PORT的组合都需要一个监听器
 
# Istio Pilot发现服务
0.0.0.0            15010     HTTP
0.0.0.0            8080      HTTP
0.0.0.0            15030     HTTP
0.0.0.0            15031     HTTP
0.0.0.0            15004     HTTP
0.0.0.0            3000      HTTP
0.0.0.0            9091      HTTP
0.0.0.0            9200      HTTP
0.0.0.0            8060      HTTP
0.0.0.0            9411      HTTP
0.0.0.0            80        HTTP
0.0.0.0            9093      HTTP
0.0.0.0            20001     HTTP
0.0.0.0            9090      HTTP
0.0.0.0            8086      HTTP
# 上面这个是因为本Pod在8086上暴露服务(因为有对应的Service配置)而创建的供出站流量
# 使用的监听器 —— 因为可以访问自己
 
 
 
# 这些在Pod IP上监听的虚拟监听器,接收并处理:
# 外部 ⇨ PodIP:8086 ⇨ 0.0.0.0_15001 ⇨ 127.0.0.1:8086 的入站流量
172.27.121.134     8086      HTTP
 
# Pilot代理状态端口
172.27.121.188     15020     TCP
 
# 这个是实际存在的监听套接字,负责处理所有(被iptables重定向给15001的)出入Pod的流量
0.0.0.0            15001     TCP

你会看到输出的监听器很多,主要包括:

  1. 0.0.0.0:15001,此监听器接收出入Pod的所有流量,然后将请求转发给虚拟监听器处理,它本身不进行任何处理
  2.  每个K8S Service IP、每个非HTTP流量出口(Host+Port)对应一个虚拟监听器
  3. 在Pod IP上监听的,为入口流量暴露的每一个端口,对应一个虚拟监听器
  4. 为每个HTTP出口端口创建的在0.0.0.0上监听的虚拟监听器

使用-p参数,可以输出单个监听器的详细配置:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// istioctl proxy-config listener productpage-v1-8d69b45c-jltzt  --port 15001 -o json
[
    {
        "name": "virtual",
        "address": {
            "socketAddress": {
                "address": "0.0.0.0",
                "portValue": 15001
            }
        },
        "filterChains": [
            {
                "filters": [
                    {
                        "name": "envoy.tcp_proxy",
                        "config": {
                            "cluster": "BlackHoleCluster",
                            "stat_prefix": "BlackHoleCluster"
                        }
                    }
                ]
            }
        ],
        "useOriginalDst": true
    }
]

需要注意的是0.0.0.0:15001监听器,Iptables会把所有入口、出口流量都路由到此监听器。 此监听器的useOriginalDst设置为true,表示该监听器会把请求转交给最匹配请求目的地的虚拟监听器处理,如果找不到匹配,则给BlackHoleCluster处理,BlackHoleCluster返回404

例如,对于向9080端口发送的出站请求,会由虚拟监听器0.0.0.0:9080负责处理。此监听器会从RDS中查找路由,路由的名字可以使用下面的命令得到:

JavaScript
1
2
3
4
5
6
7
8
9
//  istioctl proxy-config listener productpage-v1-8d69b45c-jltzt --address 0.0.0.0 --port 9080
 
"rds": {
    "config_source": {
        "ads": {}
    },
    // 配置的来源是ADS,路由的名字是9080
    "route_config_name": "9080"
},
route

路由配置,注意监听器配置中可能仅仅提示了路由配置的名字,你需要使用route子命令再查询一次

执行下的命令,可以看到路由单品页Pod的9080路由的详细信息:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// istioctl proxy-config routes productpage-v1-8d69b45c-jltzt --name 9080 -o json
 
[                                                                                                                                                                                        
    {                                                                                                                                                                                    
        "name": "9080",                                                                                                                                                                  
        "virtualHosts": [                                                                                                                                                                
            {                                                                                                                                                                            
                "name": "details.default.svc.k8s.gmem.cc:9080",                                                                                                                          
                "domains": [                                                                                                                                                            
                    "details.default.svc.k8s.gmem.cc",                                                                                                                                  
                    "details.default.svc.k8s.gmem.cc:9080",                                                                                                                              
                    "details",
                    "details:9080",
                    "details.default.svc.k8s.gmem",
                    "details.default.svc.k8s.gmem:9080",
                    "details.default.svc.k8s",
                    "details.default.svc.k8s:9080",
                    "details.default.svc",
                    "details.default.svc:9080",
                    "details.default",
                    "details.default:9080",
                    "10.98.119.24",
                    "10.98.119.24:9080"
                ],
                "routes": [
                    {
                        "match": {
                            "prefix": "/"
                        },
                        "route": {
                            "cluster": "outbound|9080|v1|details.default.svc.k8s.gmem.cc",
                            "timeout": "0.000s",
                            "maxGrpcTimeout": "0.000s"
                        },
...

可以看到一个路由下面可以定义很多虚拟主机, 以details.default.svc.k8s.gmem.cc:9080为例,它仅仅包含一个路由,将请求发往集群outbound|9080|v1|details.default.svc.k8s.gmem.cc

Kiali

Kiali是当前Istio主推的前端工具,能够很容易的显示服务网格包含哪些微服务,这些微服务如何连接在一起。

Kiali能够显示Service Mesh拓扑结构,展示请求速率、断路器等Istio特性。它还能结合Jaeger,以提供调用链跟踪功能。

客户端编程

你可以使用动态客户端来CRUD各种Istio对象,Istio项目本身将K8S资源表示为IstioObject,后者需要转化为model.Config,并进一步获取Istio内部对象:

Go
1
2
config, err := ConvertObject(schema, item, c.client.domainSuffix)
serviceEntry := config.Spec.(*networking.ServiceEntry)
knative

该项目提供了Istio的强类型客户端:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import istioclientset "github.com/knative/pkg/client/clientset/versioned"
 
// 客户端
type Clientset struct {
    *discovery.DiscoveryClient
    authenticationV1alpha1 *authenticationv1alpha1.AuthenticationV1alpha1Client
    networkingV1alpha3     *networkingv1alpha3.NetworkingV1alpha3Client
}
// networking.istio.io/v1alpha3组的接口
func (c *Clientset) NetworkingV1alpha3() networkingv1alpha3.NetworkingV1alpha3Interface {
    return c.networkingV1alpha3
}
type NetworkingV1alpha3Interface interface {
    RESTClient() rest.Interface
    DestinationRulesGetter
    GatewaysGetter
    VirtualServicesGetter
}
type VirtualServicesGetter interface {
    VirtualServices(namespace string) VirtualServiceInterface
}
// VirtualService CRD的接口
type VirtualServiceInterface interface {
    Create(*v1alpha3.VirtualService) (*v1alpha3.VirtualService, error)
    Update(*v1alpha3.VirtualService) (*v1alpha3.VirtualService, error)
    Delete(name string, options *v1.DeleteOptions) error
    DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error
    Get(name string, options v1.GetOptions) (*v1alpha3.VirtualService, error)
    List(opts v1.ListOptions) (*v1alpha3.VirtualServiceList, error)
    Watch(opts v1.ListOptions) (watch.Interface, error)
    Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha3.VirtualService, err error)
    VirtualServiceExpansion
}

要使用此客户端,可以:

Go
1
2
3
4
// 先获得*restclient.Config
cfg, err := clientcmd.BuildConfigFromFlags(masterURL, kubeconfig)
// 然后构造Istio客户端
istioClient, err := istioclientset.NewForConfig(cfg) 
高级主题
Istio如何劫持流量
  1. 当创建Pod之后,Admission Controller自动为其注入两个容器:istio-init、istio-proxy
  2. istio-init仅仅是一个脚本,它负责配置Pod网络命名空间的iptables。默认使用REDIRECT方式,将任何入站、出站流量重定向给istio-proxy中运行的Envoy所监听的15001端口,此端口由名为virtual的Envoy监听器监听
  3. istio-proxy启动后,使用xDS协议向Pilot发起请求,以同步配置
  4. 不论是入站流量,还是出站流量,都会被劫持到15001这个Envoy监听器:
    1. 入站流量的流动轨迹:iptables  ⇨ Istio代理入站处理器(InboundHandler) ⇨ 应用容器
    2. 出站流量的流动轨迹:iptables  ⇨ Istio代理出站处理器(OutboundHandler) ⇨ 上游集群的特定端点
入站流量流动轨迹

以Bookinfo应用为例,当productpage访问reviews时,入站(到reivew的Pod)流量流动轨迹如下:

inbound-istio-traffic-flow

说明:

  1. Iptables的FILTER-PREROUTING链拦截到进入review pod的流量
  2. 流量被转发给ISTIO_INBOUND链
  3. 流量被转发给ISTIO_IN_REDIRECT链
  4. 流量被重定向给15001端口,即运行在Sidecar中Envoy的virtual监听器
  5. virtual监听器use_original_dst=true,导致:
    1. 流量转发给虚拟监听器ReviewPod_9080
    2. 监听器ReviewPod_9080将流量路由给上游集群inbound|9080|reviews.default.svc.gmem.cc
    3. 此上游集群的唯一端点是127.0.0.1:9080,也就是应用程序容器
  6. 发往127.0.0.1:9080的流量被iptables的OUTPUT链拦截,转发给ISTIO_OUTPUT链
  7. ISTIO_OUTPUT将流量转发给ISTIO_REDIRECT链
  8. ISTIO_REDIRECT链将流量达到应用程序容器的9080端口上
出站流量流动轨迹

以Bookinfo应用为例,当reviews访问ratings时,出站(到ratings的Pod)流量流动轨迹如下:

outbound-istio-traffic-flow

说明:

  1. Iptables的FILTER-OUTPUT链拦截到从review pod发往ratings集群的流量
  2. 流量被转发给ISTIO_OUTPUT链
  3. 流量被转发给ISTIO_REDIRECT链
  4. 流量被重定向给15001端口,即运行在Sidecar中Envoy的virtual监听器。virtual监听器的use_original_dst=true,导致:
    1. 流量转发给虚拟监听器0.0.0.0_9080
    2. 通过解析HTTP头,虚拟监听器将请求路由给上游集群outbound|9080|ratings.default.svc.gmem.cc
    3. 经过负载均衡策略判断,选中某个RatingsPod作为端点
  5. 流量再次被转发给OUTPUT链,并依次经过ISTIO_OUTPUT ⇨ ISTIO_REDIRECT ⇨ POSTROUTING链出站,送达RatingsPod的监听端口
Iptables解读-TPROXY
Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
### MANGLE表  ###
 
# 如果使用透明代理模式
 
# 在MANGLE表创建新规则链ISTIO_DIVERT,注意此表优先级高于NAT
iptables -t mangle -N ISTIO_DIVERT
# 为封包打标记 1337
iptables -t mangle -A ISTIO_DIVERT -j MARK --set-mark 1337
# 允许封包通过
iptables -t mangle -A ISTIO_DIVERT -j ACCEPT
 
# 上面的封包路由给本地lo接口
ip -f inet rule add fwmark 1337 lookup 133
ip -f inet route add local default dev lo table 133
 
# 在MANGLE表创建新规则链ISTIO_TPROXY,位于ISTIO_DIVERT链后面
iptables -t mangle -N ISTIO_TPROXY
 
# 目的地址不是127.0.0.1的封包被重定向给15001的监听进程,但却不修改包头的任何信息,这和REDIRECT行为不同
# 同时设置标记1337,导致后续路由时从lo发出,给15001进程
iptables -t mangle -A ISTIO_TPROXY '!' -d 127.0.0.1/32 -p tcp -j TPROXY --tproxy-mark 1337/0xffffffff --on-port 15001
 
# 创建新规则链ISTIO_INBOUND,PREROUTING链直接跳转到此链
iptables -t mangle -N ISTIO_INBOUND
iptables -t mangle -A PREROUTING -p tcp -j ISTIO_INBOUND        ##### PREROUTING入口规则
 
# 处理负载服务端口
iptables -t mangle -A ISTIO_INBOUND -p tcp --dport 80 -m socket -j ISTIO_DIVERT
iptables -t mangle -A ISTIO_INBOUND -p tcp --dport 80 -j ISTIO_TPROXY
 
 
### NAT表  ###
 
iptables -t nat -N ISTIO_REDIRECT
# 任何TCP协议,都重定向到15001端口,也就是修改封包的目的地址为本机地址
iptables -t nat -A ISTIO_REDIRECT -p tcp -j REDIRECT --to-port 15001
 
# 类似的另一个规则链
iptables -t nat -N ISTIO_IN_REDIRECT
iptables -t nat -A ISTIO_IN_REDIRECT -p tcp -j REDIRECT --to-port 15001
 
# 在NAT表创建新规则链
iptables -t nat -N ISTIO_OUTPUT
iptables -t nat -A OUTPUT -p tcp -j ISTIO_OUTPUT
 
# 如果出口网卡是lo,但是目的地址非本地地址,重定向
iptables -t nat -A ISTIO_OUTPUT -o lo '!' -d 127.0.0.1/32 -j ISTIO_REDIRECT
 
# 如果封包由1337用户/组产生,则退出ISTIO_OUTPUT链,返回OUTPUT
iptables -t nat -A ISTIO_OUTPUT -m owner --uid-owner 1337 -j RETURN
iptables -t nat -A ISTIO_OUTPUT -m owner --gid-owner 1337 -j RETURN
# 否则,执行重定向
iptables -t nat -A ISTIO_OUTPUT -j ISTIO_REDIRECT

大概分析一下包处理流程:

  1. 外部客户端请求PodIP:80端口
  2. 第一次进入Iptabels
  3. 进入PREROUTING链
    1. 进入mangle表
      1. 跳转到ISTIO_INBOUND链
      2. 跳转到ISTIO_TPROXY链
      3. 由于目的地址不是127.0.0.1/32,因此封包被打上标记1337,并标记为由15001的监听者处理
      4. 封包通过mangle表,进而通过Iptables
  4. PREROUTING之后需要进行选路处理,由于它包含1337标记,因此路由目标是lo网卡
  5. 由于之前已经标记由15001处理,因此Envoy进程获得封包的处理权
  6. Envoy进行内部处理后转发给匹配其元素目的地址的虚拟监听器,也就是PodIP:80
  7. 此监听器向127.0.0.1:80发起请求,这是另一个TCP连接,进入Iptables,OUTPUT链
    1. 如果目的地址不是127.0.0.1则会强制重定向到15001 —— Envoy总是以127.0.0.1访问负载的服务,因此不会重定向
    2. 如果发起请求的用户不是1337/1337,强制重定向到15001 —— Envoy总是以1337身份运行,因此不会重定向
  8. 请求被负载接收并处理

ISTIO_DIVERT链相关的规则并非必须(可以快速确定一个套接字是否应该本地处理,不需要遍历一系列iptables规则),删除Iptables的相应条目不影响Enovoy的工作。

Headless服务问题

我们可以通过istio proxy-config listener来Dump出任何代理的监听器列表,并且已经发现以下规律:

  1. 对于每个K8S服务的IP:PORT组合
    1. 如果服务是TCP的,则创建IP:PORT监听器
    2. 如果服务是HTTP的,则创建0.0.0.0:PORT监听器,如果多个HTTP服务使用PORT,则贡献之
  2. 对于本负载暴露的任何服务,创建IP:PORT监听器

上述行为会导致一些问题:

  1. 网格中任意一个HTTP服务占用了PORT,则TCP服务无法再使用此PORT

如果使用了Headless服务,额外的问题会出现。假设部署了三节点的ZooKeeper集群:

Shell
1
2
zk            ClusterIP   10.111.213.56   <none>        2181/TCP                     83m
zk-headless   ClusterIP   None            <none>        2181/TCP,3888/TCP,2888/TCP   83m

则在每个节点的代理上,都会创建如下监听器:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 这三个是入站监听器,转发给inbound|3888|election|zk-headless.default.svc.k8s.gmem.cc
# 由于Headless服务引发
172.27.252.180     2181      TCP
172.27.252.180     3888      TCP
172.27.252.180     2888      TCP
 
# 这三个是出站监听器,转发给outbound|3888||zk-headless.default.svc.k8s.gmem.cc
# 由于Headless服务引发
0.0.0.0            2888      TCP
0.0.0.0            3888      TCP
0.0.0.0            2181      TCP
 
# 这是出站监听器,由于zk服务导致
10.111.213.56 2181 TCP

可以看到,由于Headless服务没有IP地址,Istio直接在0.0.0.0上监听,代理ZooKeeper节点的集群请求。这就导致了第二个问题:

  1. 网格中任何两个Headless服务,不能使用相同的端口
  2. Headless服务的端口不能和集群中某个TCP服务的端口冲突
  3. Headless服务的端口也不能和集群中某个HTTP服务的端口冲突

相关Issue:https://github.com/istio/istio/issues/9784

此问题目前的进展:

  1. 现在具有命名空间隔离机制,不同命名空间可以使用同一端口
  2. 现在具有细粒度控制机制,只要一个服务不同时依赖于2-N个使用相同端口的Headless服务,也可以规避端口冲突问题
  3. HTTP服务和Headless TCP服务端口冲突问题,从1.3开始,可以通过协议嗅探特性解决
  4. 有个PR解决Headless服务在0.0.0.0:PORT监听的问题,它为Headless服务的每个Endpoint创建一个PodIP:Port监听器,这可以解决99%的场景,除非你的集群非常大,Headless服务下Pod总数很多。使用此特性,需要为Pilot设置环境变量 PILOT_ENABLE_HEADLESS_SERVICE_POD_LISTENERS=true。我在1.5.1版本测试,发现这个环境变量没有效果,监测Headless服务的OUTBOUND监听器,发现上游集群类型设置为ORIGINAL_DST,也就是简单的透传,这样貌似也就不会有问题?
真实源IP问题

接上个问题,对于ZooKeeper这样的中间件,其节点集之间本身的交互就无法在Istio下进行。

我们看一下ZooKeeper节点的Envoy集群配置:

Shell
1
2
3
4
5
6
7
8
9
10
# istioctl proxy-config cluster zk-0 --fqdn zk-headless.default.svc.k8s.gmem.cc
SERVICE FQDN                            PORT     SUBSET       DIRECTION     TYPE
# 和其他ZK节点进行通信时,会使用原始目的地址
zk-headless.default.svc.k8s.gmem.cc     2181     -            outbound      ORIGINAL_DST
zk-headless.default.svc.k8s.gmem.cc     2888     -            outbound      ORIGINAL_DST
zk-headless.default.svc.k8s.gmem.cc     3888     -            outbound      ORIGINAL_DST
 
zk-headless.default.svc.k8s.gmem.cc     2181     client       inbound       STATIC
zk-headless.default.svc.k8s.gmem.cc     3888     election     inbound       STATIC
zk-headless.default.svc.k8s.gmem.cc     2888     server       inbound       STATIC

可以看到zk-0节点向zk-1节点发送的通信请求,是可以正确到达的,原因是ORIGINAL_DST。

但是,在zk-1节点那边,zk-0的请求被拦截,其源IP地址变为127.0.0.1:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
# netstat -nt | grep 2181
tcp        0      0 127.0.0.1:48020         127.0.0.1:2181          TIME_WAIT  
tcp        0      0 127.0.0.1:48124         127.0.0.1:2181          TIME_WAIT  
tcp        0      0 127.0.0.1:47984         127.0.0.1:2181          TIME_WAIT  
tcp        0      0 127.0.0.1:48144         127.0.0.1:2181          TIME_WAIT  
tcp        0      0 127.0.0.1:48038         127.0.0.1:2181          TIME_WAIT  
tcp        0      0 127.0.0.1:48108         127.0.0.1:2181          TIME_WAIT  
tcp        0      0 127.0.0.1:48000         127.0.0.1:2181          TIME_WAIT  
tcp        0      0 127.0.0.1:48088         127.0.0.1:2181          TIME_WAIT  
tcp        0      0 127.0.0.1:47946         127.0.0.1:2181          TIME_WAIT  
tcp        0      0 127.0.0.1:48162         127.0.0.1:2181          TIME_WAIT  
tcp        0      0 127.0.0.1:48070         127.0.0.1:2181          TIME_WAIT  

这无法满足ZooKeeper的要求,它要求集群节点必须是直接进行通信的,也就是说,要能够得到正确的源IP地址。在Istio代理存在的情况下,显然无法满足。

很多场景下都需要知道源的真实IP地址,例如:

  1. 源IP地址作为身份信息的一部分
  2. 源IP地址和网络策略有关
  3. 需要对源IP地址进行审计

但凡这些场景,都会面临问题。

禁用或穿透

解决ZooKeeper不能在网格中运行的问题,可以使用Istio的注解:

YAML
1
2
3
4
5
6
# 让入站的这些端口不走Envoy代理
traffic.sidecar.istio.io/excludeInboundPorts: 2888,3888
traffic.sidecar.istio.io/includeInboundPorts: '*'
 
# 或者,直接禁止ZooKeeper的自动注入
sidecar.istio.io/inject: "false"
透明代理

透明代理是解决ZooKeeper问题的一个可能方案,不幸的是,Istio的TPROXY模式行为并不符合期望:

  1. 由于FeatureGate RunAsGroup没有开启,导致istio-proxy容器运行在root用户下,这与istio-init的Iptable规则冲突:
    Shell
    1
    2
    iptables -t nat -A ISTIO_OUTPUT -m owner --uid-owner 1337 -j RETURN
    iptables -t nat -A ISTIO_OUTPUT -m owner --gid-owner 1337 -j RETURN

    其后果就是istio-proxy无限重定向,根本没法启动起来

  2. 即使开启此RunAsGroup或者修改istio-sidecar-injector的Configmap将istio-proxy用户改为root,仍然不行,负载获得的源IP地址仍然是127.0.0.1,仅仅是Envoy看到了真实源IP,也就是说Istio的TPROXY模式只是解决了Envoy看不到真实源IP的问题

第2个问题和Envoy相关,默认情况下,Envoy不会将下游真实地址暴露给工作负载,因此后者看到的是127.0.0.1。

Envoy提供了获取真实源IP地址的多种方法:

  1. 对于L7:可以考虑时用HTTP头传递源IP地址,例如x-forwarded-for
  2. 代理协议:HAProxy的代理协议可以提供源IP地址的元数据信息,通过过滤器envoy.listener.proxy_protocol,Envoy可以消费元数据,但是如何把源IP发给上游还是个问题
  3. 使用监听器过滤器envoy.listener.original_src,此过滤器能够复制下游连接的源IP地址,并将其作为上游的连接的源IP地址

original_src出现的比较晚,到2019年底Istio仍然没有考虑支持该监听器过滤器。

目前(2020),透明代理问题已经可以解决,具体方案如下:

  1. Istio开启TPROXY拦截模式
  2. 使用EnvoyFilter,注入监听器过滤器original_src,参考EnvoyFilter
  3. 添加必要的路由规则、Iptables规则,参考original_src
性能问题

服务网格中,调用链路中每多一跳,就会引入两个Sidecar的逻辑,让Sidecar性能优化到极致非常必要。可以从如何拦截流量,如何在Sidecar之间进行通信这两个角度进行性能优化:

流量拦截

为了将流量引入到Sidecar,Istio使用了iptables规则,所有流量需要经过内核态 - 用户态 -内核态的转移,数据被反复拷贝、大量的中断、上下文切换,都影响了性能。

这属于iptables的固有缺陷,解决思路有两个:

  1. bpfilter,基于BPF的网络过滤内核模块,用于替换netfilter作为iptables的底层实现。对内核版本有需求
  2. 使用自定义网络套接字,支持VPP、Cilium,以便完全在用户态或内核态处理封包

DPDK(Intel Data Plane Development Kit)允许在用户态高效处理网络包,它提供了必要库函数和驱动支持。VPP(Vector Packet Processor)是基于DPDK的网络包处理框架。

eBPF允许用户提供指令来改变内核处理数据的行为。XDP(eXpress Data Path)为Linux内核提供了高性能、可编程的网络数据路径。由于网络包在还未进入网络协议栈之前就处理,它给Linux网络带来了巨大的性能提升(性能比DPDK还要高)。

Fackbook的katran就是基于eBPF+XDP,它是一个网络负载均衡器,据说性能比IPVS高10倍。

通信协议

UDP容易丢包,TCP慢。QUIC结合了两者的优点——既保证了TCP的可靠与安全性,又兼具UDP的速度与效率,QUIC还内置TLS支持、一部分HTTP/2的支持。

常见问题
proxy-init
无法操控iptables

报错信息:iptables v1.6.0: can't initialize iptables table `nat': Permission denied (you must be root)

报错原因:如果Pod使用了securityContext,会出现该情况。

解决方案:

  1. 不使用securityContext,或者将运行用户改为0
  2. 或者,修改Configmap istio-sidecar-injector,将istio-init容器的运行用户设置为0:
    YAML
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    apiVersion: v1
    data:
      config: |-
        policy: enabled
        template: |-
          initContainers:
          - name: istio-init
            securityContext:
              runAsUser: 0
              capabilities:
                add:
                - NET_ADMIN
              privileged: true
            restartPolicy: Always 
TPROXY相关
TPROXY模式下无法连接pilot

Sidecar报错信息:Envoy proxy is NOT ready: config not received from Pilot (is Pilot running?)

原因:Kubernetes 1.13默认没有启用特性RunAsGroup,而Istio假设这一特性是开启的,这属于兼容性问题。已经提Issue到Istio。

解决方法一:修改istio-sidecar-injector,将 -u改为 0 即可:

YAML
1
2
3
4
5
6
7
8
  command:
  - istio-iptables
  - "-p"
  - "15001"
  - "-z"
  - "15006"
  - "-u"
  - 1337  # 改为0

解决方法二:为API Server开启特性 --feature-gates=RunAsGroup=true。

← 2018年7月青岛
Etcd学习笔记 →

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">

Related Posts

  • Envoy学习笔记
  • 如何在Pod中执行宿主机上的命令
  • 服务网格的现状和未来
  • Istio Mixer与Envoy的交互机制解读
  • Flexvolume学习笔记

Recent Posts

  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
  • A Comprehensive Study of Kotlin for Java Developers
  • 背诵营笔记
  • 利用LangChain和语言模型交互
  • 享学营笔记
ABOUT ME

汪震 | Alex Wong

江苏淮安人,现居北京。目前供职于腾讯云,专注容器方向。

GitHub:gmemcc

Git:git.gmem.cc

Email:gmemjunk@gmem.cc@me.com

ABOUT GMEM

绿色记忆是我的个人网站,域名gmem.cc中G是Green的简写,MEM是Memory的简写,CC则是我的小天使彩彩名字的简写。

我在这里记录自己的工作与生活,同时和大家分享一些编程方面的知识。

GMEM HISTORY
v2.00:微风
v1.03:单车旅行
v1.02:夏日版
v1.01:未完成
v0.10:彩虹天堂
v0.01:阳光海岸
MIRROR INFO
Meta
  • Log in
  • Entries RSS
  • Comments RSS
  • WordPress.org
Recent Posts
  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
    In this blog post, I will walk ...
  • A Comprehensive Study of Kotlin for Java Developers
    Introduction Purpose of the Study Understanding the Mo ...
  • 背诵营笔记
    Day 1 Find Your Greatness 原文 Greatness. It’s just ...
  • 利用LangChain和语言模型交互
    LangChain是什么 从名字上可以看出来,LangChain可以用来构建自然语言处理能力的链条。它是一个库 ...
  • 享学营笔记
    Unit 1 At home Lesson 1 In the ...
  • K8S集群跨云迁移
    要将K8S集群从一个云服务商迁移到另外一个,需要解决以下问题: 各种K8S资源的迁移 工作负载所挂载的数 ...
  • Terraform快速参考
    简介 Terraform用于实现基础设施即代码(infrastructure as code)—— 通过代码( ...
  • 草缸2021
    经过四个多月的努力,我的小小荷兰景到达极致了状态。

  • 编写Kubernetes风格的APIServer
    背景 前段时间接到一个需求做一个工具,工具将在K8S中运行。需求很适合用控制器模式实现,很自然的就基于kube ...
  • 记录一次KeyDB缓慢的定位过程
    环境说明 运行环境 这个问题出现在一套搭建在虚拟机上的Kubernetes 1.18集群上。集群有三个节点: ...
  • eBPF学习笔记
    简介 BPF,即Berkeley Packet Filter,是一个古老的网络封包过滤机制。它允许从用户空间注 ...
  • IPVS模式下ClusterIP泄露宿主机端口的问题
    问题 在一个启用了IPVS模式kube-proxy的K8S集群中,运行着一个Docker Registry服务 ...
  • 念爷爷
      今天是爷爷的头七,十二月七日、阴历十月廿三中午,老人家与世长辞。   九月初,回家看望刚动完手术的爸爸,发

  • 6 杨梅坑

  • liuhuashan
    深圳人才公园的网红景点 —— 流花山

  • 1 2020年10月拈花湾

  • 内核缺陷触发的NodePort服务63秒延迟问题
    现象 我们有一个新创建的TKE 1.3.0集群,使用基于Galaxy + Flannel(VXLAN模式)的容 ...
  • Galaxy学习笔记
    简介 Galaxy是TKEStack的一个网络组件,支持为TKE集群提供Overlay/Underlay容器网 ...
TOPLINKS
  • Zitahli's blue 91 people like this
  • 梦中的婚礼 64 people like this
  • 汪静好 61 people like this
  • 那年我一岁 36 people like this
  • 为了爱 28 people like this
  • 小绿彩 26 people like this
  • 彩虹姐姐的笑脸 24 people like this
  • 杨梅坑 6 people like this
  • 亚龙湾之旅 1 people like this
  • 汪昌博 people like this
  • 2013年11月香山 10 people like this
  • 2013年7月秦皇岛 6 people like this
  • 2013年6月蓟县盘山 5 people like this
  • 2013年2月梅花山 2 people like this
  • 2013年淮阴自贡迎春灯会 3 people like this
  • 2012年镇江金山游 1 people like this
  • 2012年徽杭古道 9 people like this
  • 2011年清明节后扬州行 1 people like this
  • 2008年十一云龙公园 5 people like this
  • 2008年之秋忆 7 people like this
  • 老照片 13 people like this
  • 火一样的六月 16 people like this
  • 发黄的相片 3 people like this
  • Cesium学习笔记 90 people like this
  • IntelliJ IDEA知识集锦 59 people like this
  • 基于Kurento搭建WebRTC服务器 38 people like this
  • Bazel学习笔记 37 people like this
  • PhoneGap学习笔记 32 people like this
  • NaCl学习笔记 32 people like this
  • 使用Oracle Java Mission Control监控JVM运行状态 29 people like this
  • Ceph学习笔记 27 people like this
  • 基于Calico的CNI 27 people like this
Tag Cloud
ActiveMQ AspectJ CDT Ceph Chrome CNI Command Cordova Coroutine CXF Cygwin DNS Docker eBPF Eclipse ExtJS F7 FAQ Groovy Hibernate HTTP IntelliJ IO编程 IPVS JacksonJSON JMS JSON JVM K8S kernel LB libvirt Linux知识 Linux编程 LOG Maven MinGW Mock Monitoring Multimedia MVC MySQL netfs Netty Nginx NIO Node.js NoSQL Oracle PDT PHP Redis RPC Scheduler ServiceMesh SNMP Spring SSL svn Tomcat TSDB Ubuntu WebGL WebRTC WebService WebSocket wxWidgets XDebug XML XPath XRM ZooKeeper 亚龙湾 单元测试 学习笔记 实时处理 并发编程 彩姐 性能剖析 性能调优 文本处理 新特性 架构模式 系统编程 网络编程 视频监控 设计模式 远程调试 配置文件 齐塔莉
Recent Comments
  • qg on Istio中的透明代理问题
  • heao on 基于本地gRPC的Go插件系统
  • 黄豆豆 on Ginkgo学习笔记
  • cloud on OpenStack学习笔记
  • 5dragoncon on Cilium学习笔记
  • Archeb on 重温iptables
  • C/C++编程:WebSocketpp(Linux + Clion + boostAsio) – 源码巴士 on 基于C/C++的WebSocket库
  • jerbin on eBPF学习笔记
  • point on Istio中的透明代理问题
  • G on Istio中的透明代理问题
  • 绿色记忆:Go语言单元测试和仿冒 on Ginkgo学习笔记
  • point on Istio中的透明代理问题
  • 【Maven】maven插件开发实战 – IT汇 on Maven插件开发
  • chenlx on eBPF学习笔记
  • Alex on eBPF学习笔记
  • CFC4N on eBPF学习笔记
  • 李运田 on 念爷爷
  • yongman on 记录一次KeyDB缓慢的定位过程
  • Alex on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • haolipeng on 基于本地gRPC的Go插件系统
  • 吴杰 on 基于C/C++的WebSocket库
©2005-2025 Gmem.cc | Powered by WordPress | 京ICP备18007345号-2