Istio学习笔记
术语服务网格(Service Mesh)用于描述微服务之间的网络,以及通过此网络进行的服务之间的交互。随着服务数量和复杂度的增加,服务网格将变的难以理解和管理。
对服务网格的需求包括:服务发现、负载均衡、故障恢复、指标和监控,以及A/B测试、金丝雀发布、限速、访问控制、端对端身份验证等。
使用云平台给DevOps团队带来了额外的约束,为了Portability开发人员通常需要使用微服务架构,运维人员则需要管理非常多数量的服务。Istio能够连接、保护、控制、观察这些微服务。
Istio是运行于分布式应用程序之上的透明(无代码入侵)服务网格,它同时也是一个平台,提供集成到其它日志、监控、策略系统的接口。
Istio的实现原理是,为每个微服务部署一个Sidecar,代理微服务之间的所有网络通信。在此基础上你可以通过Istio的控制平面实现:
- 针对HTTP、gRPC、WebSocket、TCP流量的负载均衡
- 细粒度的流量控制行为,包括路由、重试、故障转移、故障注入(fault injection)
- 可拔插的策略层+配置API,实现访问控制、限速、配额
- 自动收集指标、日志,跟踪集群内所有流量,包括Ingress/Egress
- 基于强身份认证和授权来保护服务之间的通信
使用Istio你可以很容易的通过配置,对流量和API调用进行控制。服务级别的可配置属性包括断路器(circuit breakers)、超时、重试。
Istio支持基于流量百分比切分的A/B测试、金丝雀滚动发布、分阶段滚动发布。
可以提供安全信道,管理身份验证和授权,加密通信流量。
联用K8S的网络策略可以获得更多益处,例如保护Pod-to-Pod之间的通信。
Istio强大的跟踪、监控、日志能力,让服务网格内部结构更容易观察 —— 一个服务的性能对上下游的影响可以直观的展现在仪表盘上。
Istio的Mixer组件——通用的策略和监控(Telemetry)中心(Hub)——负责策略控制、指标收集。Mixer提供后端基础设施(例如K8S)的隔离层,Istio的其它部分不需要关注后端细节。
Istio当前支持:
- Kubernetes上的Service
- 通过Consul注册的服务
- 在独立虚拟机上运行的服务
从整体上看,Istio的服务网格由数据平面、控制平面两部分组成:
- 数据平面由一系列作为Sidecar部署的智能代理(Envoy)构成。这些代理联合Mixer,中继、控制所有微服务之间的网络通信。需要注意,还有一些Envoy是独立部署(而非Sidecar)的,用来实现K8S Ingress控制器、Istio的Ingress/Egress网关
- 控制平面负责管理、配置智能代理,实现流量路由;配置Citadel实现TLS证书管理;配置Mixers来应用策略、收集指标
架构图:
Istio使用一个扩展过的Envoy版本。Envoy是基于C++开发的高性能代理,Istio使用它的以下特性:
- 动态服务发现
- 负载均衡
- TLS termination —— 可将后端的HTTP服务包装为HTTPS
- HTTP/2和gRPC代理
- 断路器
- 健康检查
- 分阶段(基于流量百分比)发布
- 故障注入
- 丰富的监控指标
一般情况下Envoy在和目标服务的相同Pod中,以Sidecar形式部署。少量的Istio组件的主进程就是Envoy,包括Ingress控制器、Ingress/Egress网关。
一个平台无关的组件:
- 为服务网格应用访问控制策略
- 从Envoy和其它服务中收集指标
- Envoy收集的请求级别的属性,被发送到Mixer进行分析
Mixer提供了一个灵活的插件模型,让Istio能够灵活的和多种宿主机环境、基础设施后端进行对接。
该组件是Istio的控制器,它会监控各种规则、策略(通常存储在K8S中),一旦配置文件发生变化,就会提取、处理,并同步给Envoy:
- 为Envoy提供服务发现
- 为智能路由(AB测试、金丝雀部署……)提供流量管理能力
- 提供弹性(Resiliency)——超时、重试、断路器等
- 分发身份验证策略给Envoy
Pilot将高级别的路由规则转换为Envoy理解的配置信息,并在运行时将这些配置传播到Sidecars。
Pilot将平台相关的服务发现机制抽象为标准的(Envoy data plane API,xDS)格式,这让Istio可以在K8S、Consul、Nomad等多种环境下运行。
负责证书、密钥的管理。提供服务-服务之间、或者针对终端用户的身份验证功能,可以加密服务网格中的流量。
部署服务网格带来了额外的性能损耗,下面是Istio组件的推荐资源配置:
- 对于启用了访问日志(默认启用)的Sidecar,每1000请求/s可以配备1个vCPU,如果没有启用访问日志则配备0.5个vCPU
- 节点上的Fluentd是资源消耗大户,它会捕获并上报日志,如果不关心可以排除Sidecar日志不上报
- 在大部分情况下,Mixer Check有超过80%的缓存命中率,在此前提下,可以为Mixer的Pod每1000请求/s配备0.5个vCPU
- 由于在通信两端都引入了Sidecar代理、并且需要上报Mixer,大概会引入10ms的延迟
任何控制平面组件都支持多实例部署,在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 |
- 进一步完善istiod(单体化)
- 可观察性增强:更多配置项、对追踪采样率的控制、更新的Grafana仪表盘
- 更好的虚拟机支持(未来将更进一步强化):WorkloadEntry资源让非K8S工作负载成为Istio的一等公民,物理机或者虚拟机和Pod现在处于同一级别。你可以定义同时由Pod和物理机Back的服务。这让逐步迁移到K8S提供便利
1.5版本的最大变化是,控制平面的大部分组件(pilot、citadel、sidecar-injector)合并到名为istiod的单体应用中,现在istiod负责:
- 配置处理和推送
- 证书分发
- Sidecar注入
以下组件被移除:
- SDS的node-agent(合并到Pilot Agent)
Mixer被废弃,但是仍然可以使用。
新版本HTTP遥测基于Envoy过滤器Stats filter实现,可以节省50%的CPU用量。
- 提升了 ServiceEntry 的性能。
- 修复了 readiness 探针不一致问题
- 通过定向局部更新的方式改善了配置更新的性能
- 添加了为 host 设置所在负载均衡器设置的选项
- 修复了 Pod 崩溃会触发过度配置推送的问题
- 添加了使用 Istio CNI 时对 iptables 的探测
- 添加了 consecutive_5xx 和 gateway_errors 作为离群值探测选项。
- 提升了 EnvoyFilter 匹配性能
- 添加了对 HTTP_PROXY 协议的支持
- 改进了 iptables 设置,默认使用 iptables-restore
- 默认开启自动协议探测
- 添加 Beta 认证 API。新 API 分为 PeerAuthentication 和 RequestAuthenticaiton,面向工作负载
- 添加认证策略,支持 deny 操作和语义排除
- Beta 版本默认开启自动 mTLS
- 稳定版添加 SDS
- Node agent 和 Pilot agent 合并,移除了 Pod 安全策略的需要,提升了安全性
- 合并 Citadel 证书发放功能到 Pilot
- 支持 Kubernetes first-party-jwt 作为集群中 CSR 认证的备用 token
- 通过 Istio Agent 向 Prometheus 提供密钥和证书
- 支持 Citadel 提供证书给控制平面
- 为 v2 版本的遥测添加 TCP 协议支持
- 在指标和日志中支持添加 gRPC 响应状态码
- 改进 v2 遥测流程的稳定性
- 为 v2 遥测的可配置性提供 alpha 级别的支持
- 支持在 Envoy 节点的元数据中添加 AWS 平台的元数据
- 更新了 Mixer 的 Stackdriver 适配器,以支持可配置的刷新间隔来跟踪数据
- 支持对 Jaeger 插件的 headless 收集服务
- 修复了 kubernetesenv 适配器以提供对名字中有.的 Pod 的支持
- 改进了 Fluentd 适配器,在导出的时间戳中提供毫秒级输出
仍然以用户体验改进为主,简化配置难度。
可以简化安装、Mesh的操控,并很大程度上改善性能。
istio-proxy内部生成HTTP指标的特性,从试验升级为Alpha。
istio-proxy内部生成TCP指标(试验)
不需要配置DestinationRule即可自动启用。
该版本主要专注于用户体验的改善。
为了使用Istio的路由特性,服务端口名必须遵循特定的命名约定,来声明其使用的协议。
从1.3开始,出站流量的协议可以自动识别为TCP或HTTP,这意味着服务端口命名协定不再必须
现在大部分的安全策略(例如RBAC)已经直接在Envoy里面实现了。
istio-proxy现在可以直接向Prometheus发送数据,而不需要istio-telemetry来丰富、完善信息。
对于Envoy中的高级特性,Istio可能尚未提供对应API。你现在可以基于EnvoyFilter API来定制:
- LDS返回的HTTP/TCP监听器,及其过滤器链
- RDS返回的Envoy HTTP路由配置
- CDS返回的上游集群
- 添加注解 traffic.sidecar.istio.io/includeInboundPorts,不再强制要求服务在deployment的YAML中声明contrainerPort
- 添加IPv6的试验性支持
- 在多集群环境下,优化了基于位置的路由
- 优化了ALLOW_ANY模式(即允许任何未知的出站流量,配置位于configmap istio中)下的出站流量策略。针对未知HTTP/HTTPS主机+已知端口的流量,会原样的转发
- 支持设置针对上游服务的idle超时
- 改进了NONE模式(不用iptables,不进行流量捕获)的Sidecar支持
- 可以配置Sidecar的Envoy的DNS刷新率,防止DNS服务器过载
- 扩展自签名Citadel根证书有效期为10年
- 可以为Deployment添加PodSpec注解 sidecar.istio.io/rewriteAppHTTPProbers: "true",来重写健康探针
- 对Envoy统计信息生成的完全控制,基于注解控制
- Prometheus生成的不包含在指标中
控制平面性能:Pilot的CPU、内存消耗受网格的配置变化情况、工作负载变化情况、连接到Pilot的代理数量的影响。增加Pilot的实例数来可以减少配置分发处理的时间,提高性能。网格中包含1000服务、2000工作负载、1000QPS的情况下,单个Pilot实例消耗1个vCPU和1.5GB内存。
数据平面性能:
- CPU,代理在1000QPS下大概消耗0.6个vCPU
- 内存,代理的内存消耗取决于它需要处理的配置数量,大量的Listener、Cluster、Route会增加内存使用。此外,代理在1000QPS下需要消耗50MB内存
- 延迟,请求需要同时经过客户端/服务器的sidecar,在1000QPS下P99大概是10ms级别。启用策略检查(Check)会额外增加延迟。L4过滤器的逻辑比较简单,因此TCP流量不会引入显著延迟
- CRD从Istio的Chart中独立出来,放入istio-init这个Chart。这避免了升级Istio而丢失CR数据
- 添加几个安装配置Profile,这些配置提供了常用的安装模式,简化了Istio的安装过程
- 增强了多集群集成
- 新的资源类型Sidecar。用于对工作负载的边车代理进行细粒度的控制,特别是可以限制边车能够向哪些服务发送流量。此资源可以减少需要计算和传输给边车代理的配置信息,提升启动速度,减少资源消耗,提升控制平面可扩容性。在大型集群中,建议为每个命名空间提供一个Sidecar资源
- 支持限制服务(ServiceEntry、VirtualService)的可见范围。exportTo可用于限制哪些命名空间能够访问本服务。除了在Istio CR上指定exportTo以外,你还可以在K8S的Service上使用networking.istio.io/exportTo注解
- Gateway引用VirtualService时,可能存在歧义,因为不同命名空间可能定义具有相同hostname的VirtualService。在1.1版本中,你可以用namespace/hostname-match的形式来来设置hosts字段,以避免歧义。类似的在Sidecar中你也可以为egress配置为这种形式
- 支持设置ServiceEntry的Locality,以及关联的SAN(用于mTLS)。现在使用HTTPS端口的ServiceEntry不再需要配套的VirtualService来工作
- 位置感知路由(Locality-Aware Routing),优先路由到位于当前地理位置(Locality)的服务端点
- 简化了多集群模式的安装、支持额外的部署模式。现在可以用Ingress网关连接多个集群,而不需要Pod级别的VPN。为了HA可以在各集群中部署控制平面,这种部署方式下位置感知路由自动开启。可以将命名空间扩展到多个集群,以创建全局命名空间
- Istio的Ingress组件被废弃
- 性能和可扩容性得到提升
- 默认关闭访问日志
- 健康检查探针,当启用mTLS的情况下,支持Kubernetes的Readiness/Liveness探针。当启用mTLS后,进入Envoy的HTTP探针请求会被拒绝,1.1能够自动进行HTTP探针的重写,将探针请求转发给pilot-agent,并由后者直接转发给工作负载,绕开Envoy的TLS认证
- 集群RBAC配置,将RbacConfig资源替换为ClusterRbacConfig资源
- 基于SDS的身份配置(Identity Provisioning ),不需要重启Envoy即可实现动态证书轮换,实现on-node的密钥生成增强了安全性
- 在HTTP/gRPC的基础上,支持TCP服务的授权
- 支持基于终端用户组的授权
- Ingress网关控制器支持外部证书管理,新的控制器用于支持动态的加载、轮换外部证书
- 定制PKI集成,支持Vault PKI,基于Vault CA签发证书
- 策略检查默认关闭,主要是出于性能方面的考虑
- 性能的增强:
- 极大的减少了Envoy默认生成的统计信息的收集
- 为Mixer工作负载添加了负载限制的功能
- 改善了Mixer和Envoy之间的交互协议
- 适配器现在能够影响入站请求的头和路由
- 进程外适配器,生产级可用,进程内适配器被废弃
- 添加了默认的用于跟踪TCP连接的指标
在1.0版本Istio只提供了一种基于扁平网络的多集群方案:Istio控制平面部署在其中单个Kubernetes集群中。这种方案要求各集群的 Pod 地址范围不能重叠,且所有的 Kubernetes 控制平面API Server 互通。看上去就是物理上把多个Kubernetes并到一个Istio控制面下,在Istio看来是同一个网格。这种方案的网络要求苛刻,实际应用并不多。
1.1版本对多集群上做了非常多的增强,除了保留1.0扁平网络作为一种单控制面的方案外,还提出了另外两种方案供用户根据实际环境和需求灵活选择,这两种方案都不要求是扁平网络:
- 多控制平面方案:在每个集群中安装完整的Istio控制平面,可以看成是一种松散的联邦,集群间服务在Gateway处联通即可。通过一个全局的DNS将跨集群的请求路由到另外一个集群里去。这种集群的访问是基于Istio的ServiceEntry和Gateway来实现的,配置较多且复杂,需用户自己维护
- 一种集群感知(Split Horizon EDS)的单控制平面方案:Istio控制平面只在一个Kubernetes集群中安装,Istio控制平面仍然需要连接所有Kubernetes集群的K8S API Server。每个集群都有集群标识和网络标识。在服务间访问时,如果目标是本集群的负载,则类似单集群的方式直接转发;如果是其他集群的实例,则将流量转发到集群的入口网关上,再经过网关转发给实际负载
在1.1版本中添加了一个重要字段exportTo。用于控制VirtualService、DestinationRule和 ServiceEntry 跨Namespace的可见性。这样就可以控制一个Namespace下定义的资源是否可以被其他Namespace下的Envoy访问。
如果不设置exportTo则默认全局可见。目前exportTo仅仅支持两个取值:
".",表示仅仅当前命名空间可以使用当前资源
"*",表示任何命名空间都可以使用当前资源
如果服务对Pod不可见,则Istio不会为该Pod的Envoy生成Listener、Cluster等信息,因而可以减少内存消耗。
Sidecar是具有命名空间的资源。每个Sidecar应用到命名空间的一个和多个工作负载,工作负载的选择通过workloadSelector进行,如果不指定workloadSelector(每个命名空间只能有一个这样的Sidecar)则Sidecar应用到命名空间的所有(没有被其它带有workloadSelector的Sidecar匹配的)工作负载。
如果命名空间包含多个没有workloadSelector的Sidecar,或者多个Sidecar的workloadSelector匹配同一工作负载,则网格的行为是未定义的。
配置示例一,允许prod-us1命名空间的Pod发起针对prod-us1, prod-apis, istio-system命名空间的公共服务的egress流量:
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" |
配置示例二:
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资源是唯一的配置工作负载边车端口的途径:
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 |
不进行流量捕获,当用在:
|
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),直接指定不同地理位置负责提供服务的权重。
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 |
一组子命令,用于操控Istio的身份验证策略:
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 - |
创建策略、规则:
1 |
istioctl create -f example-routing.yaml |
获取策略、规则:
1 2 3 4 5 6 7 |
# 获取所有虚拟服务 istioctl get virtualservices # 获取所有目的地规则 istioctl get destinationrules # 输出为YAML istioctl get virtualservice bookinfo -o yaml |
替换掉策略、规则:
1 |
istioctl replace -f example-routing.yaml |
删除策略、规则,命令格式:
1 |
istioctl delete <type> <name> [<name2> ... <nameN>] [flags] |
示例:
1 2 3 4 |
istioctl delete virtualservice bookinfo # 也可以通过yaml文件删除 istioctl delete -f example-routing.yaml |
注册一个服务实例(例如一个VM)到服务网格中:
1 |
istioctl register <svcname> <ip> [name1:]port1 [name2:]port2 ... [flags] |
注销虚拟服务实例,命令格式:
1 |
istioctl deregister <svcname> <ip> [flags] |
一组试验性命令,未来可能修改或删除。
子命令 | 说明 | ||||
convert-ingress |
尽最大努力的把Ingress资源转换为VirtualService,示例:
输出的VirtualService总是以svc.cluster.local为集群DNS后缀,可能需要修改 |
||||
metrics |
打印一个或多个虚拟服务的统计指标,示例:
|
||||
rbac |
检查是否有权限访问服务,格式:
示例:
|
||||
post-install |
安装后相关操作: post-install webhook disable 禁用Webhook |
||||
remove-from-mesh |
将工作负载从Mesh中移除: remove-from-mesh deployment 移除此Deployment对应的Pod的Sidecar并重启 |
||||
upgrade | 检查新版本并升级 | ||||
wait |
等待和Istio有关的资源Ready |
手工的将Envoy Sidecar注入到K8S的工作负载的定义文件中。定义文件中包含多种不支持注入的资源是安全的:
1 2 3 4 |
# 输出到标准输出 kubectl apply -f <(istioctl kube-inject -f resource.yaml) # 输出到文件 istioctl kube-inject -f deployment.yaml -o deployment-injected.yaml |
一组命令,用于Dump出Envoy代理的各方面的配置:
子命令 | 说明 | ||
bootstrap |
获取Envoy实例的Boostrap信息:
|
||
cluster | 获取Envoy实例的集群配置信息 | ||
listener | 获取Envoy实例的listener配置信息 | ||
route | 获取Envoy实例的路由配置信息 |
本节不做更细致的描述,具体参考运维-Dump出Envoy配置一节。
查询从Pilot最后一次发送到各Envoy的xDS同步的状态:
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 |
分析配置文件并打印校验信息: istioctl analyze <file>... [flags]
示例:
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/ |
访问Istio的Web UI。
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] |
生成、应用(到K8S)Istio资源的清单文件,或者显示Diff:
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 |
列出、Dump、Diff可用的Profile,这些Profile可以在manifest命令中引用:
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] |
这是Istio的负载测试工具。
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等 |
Mixer的命令行客户端。
用于执行前置条件检查、配额(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格式指定,例如:
|
||
--mixer | -m | 目标Mixer实例 | ||
--quotas | -q | 需要分配的配额的列表,形式name1=amount1,name2=amount2... | ||
--repeat | -r | 连续发送指定次数的请求,默认1 | ||
--trace_jaeger_url | 追踪相关 | |||
--trace_log_spans | ||||
--trace_zipkin_url |
用于产生遥感(telemetry)数据。Mixer期望以一组Attributes为输入,它基于自己的配置来判断调用哪个适配器、如何传递参数,进而输出遥感数据。
选项和check子命令一样。
1 2 3 |
cd /home/alex/Applications curl -L https://git.io/getLatestIstio | sh - mv istio-1.0.3 istio |
然后把istio/bin目录加入PATH环境变量。
Istio可以自动为Pod注入Sidecar,它基于Webhook实现此功能。
你可以执行下面的命令,确认MutatingAdmissionWebhook、ValidatingAdmissionWebhook这两种Admission controllers是否默认启用:
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是否启用:
1 2 |
kubectl api-versions | grep admissionregistration admissionregistration.k8s.io/v1beta1 |
和手工注入不同,自动注入是发生在Pod级别的。在Deployment上看不到任何变化,你需要直接检查Pod才能看到注入的Envoy。
为了加入到服务网格,K8S中的相关Pod、Service必须满足如下条件:
- 命名端口:服务(指K8S的服务,不是Pod)的端口必须被命名,为了使用Istio的路由特性,端口名必须是
<protocol>[-<suffix>]的形式。protocol可以是http, http2, grpc, mongo或者redis,suffix是可选的,如果指定suffix则必须在前面加上短横线。如果protocol不支持,或者端口没有命名,则针对该端口的流量作为普通TCP流量看待(除非通过Protocol: UDP明确指定为UDP)。示例:
1234567891011apiVersion: v1kind: Servicemetadata:name: detailsspec:ports:- port: 9080# 命名服务端口name: httpselector:app: details - 服务关联:如果一个Pod属于多个Kubernetes Service,则这些Service不得将同一端口用作不同协议
- app和version标签:建议为Pod添加app和version两个标签,分别表示应用程序的名称和版本。在分布式追踪(distributed tracing)中app标签用于提供上下文信息。在Metrics收集时app、version标签也提供上下文信息
Istio的Chart定义在install/kubernetes/helm/instio目录下 。默认情况下,Istio会以Sub chart的方式安装很多组件:
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版本:
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版本:
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:
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的注册:
1 |
helm install install/kubernetes/helm/istio-init --name istio-init --namespace istio-system -f install/kubernetes/helm/istio-init/overrides/gmem.yaml |
安装Istio组件:
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 |
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组件。
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 |
如果你的Kubernetes集群不使用缺省的集群DNS后缀(即cluster.local),则很多组件的参数需要修改,可以参考chart-istio项目,搜索{{ .Values.global.domain }}找到需要修改的位置。
注意:1.1版本的Chart这块已经处理好,修改values.yaml中的global.proxy.clusterDomain即可。
Istio暴露了几个Configmap,修改这些Configmap以影响Istio的行为。
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.9+版本的Kubernetes集群
- 有权在其中一个集群上部署Istio控制平面
- 使用RFC1918网络、VPN或更高级的网络技术将集群连接在一起,并且满足:
- 所有集群的Pod网络CIDR、Service网络CIDR不重叠
- 所有集群的Pod网络可以相互路由
- 所有集群的API Server可相互联通
- Helm 2.7.2或更高版本
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插件:
1 |
helm install install/kubernetes/helm/istio-cni --name istio-cni --namespace kube-system |
然后,安装Istio,注意设置参数:
1 |
helm install install/kubernetes/helm/istio --name istio --namespace istio-system --set istio_cni.enabled=true |
此版本的Helm Chart不正常,安装出来的不是isitd单体模式。
推荐使用 istioctl manifest命令来安装,生成的清单文件包括namespace istio-system,因此删除操作:
1 |
istioctl manifest generate <your original installation options> | kubectl delete -f - |
会导致预先创建手工创建在istio-system中的资源丢失。建议先 istioctl manifest generate,然后修改它。
使用istioctl预置的Profile不能满足需要的话,可通过-f参数自己提供一个IstioOperator资源,在其中覆盖参数值:
1 |
istioctl manifest generate -f gmem.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代替:
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 |
1 |
istioctl manifest generate --set profile=demo | kubectl delete -f - |
警告:命名空间也会被删除。
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 |
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 |
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 |
Istio提供了一个示例应用程序Bookinfo,我可以部署该应用 ,并为其配置服务网格,以学习Istio。该应用程序能够显示书籍的基本信息,包括ISBN、页数,还能显示书籍的评论信息。
Bookinfo由4个独立的微服务组成:
- productpage,调用details、reviews渲染页面
- details,提供书籍基本信息
- reviews,提供书评信息,调用ratings。该服务有v1、v2、v3三个版本
- ratings,提供书籍的评级信息
下图显示了没有部署Istio之前,Bookinfo的端对端架构:
下图显示受到Istio服务网格管理的Bookinfo的架构:
如果启用了自动化的Sidecar注入,你需要在安装到的命名空间上打标签:
1 |
kubectl label namespace default istio-injection=enabled |
打上此标签后,default命名空间中创建的新Pod,自动会有一个名为stio-proxy的容器,它运行istio/proxyv2镜像。
如果没有启用自动化的Sidecar注入,你需要执行:
1 |
kubectl apply -f <(istioctl kube-inject -f samples/bookinfo/platform/kube/bookinfo.yaml) |
然后,执行下面的命令安装所有组件:
1 |
kubectl apply -f samples/bookinfo/platform/kube/bookinfo.yaml |
等所有容器都运行起来了,检查一下当前命名空间,应该多出6个Pod、4个Service。
当所有Pod都进入Running状态后,你需要创建一个(入口)网关,这样浏览器才能访问到集群中的Bookinfo服务:
1 |
kubectl apply -f samples/bookinfo/networking/bookinfo-gateway.yaml |
上述命令会创建一个Gateway,一个VirtualService:
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:
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),则参考如下命令:
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}') |
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负载均衡机制导致的。
下面的DestinationRule声明了productpage、reviews、ratings、details等微服务的不同版本。版本通过Pod的version标签来区分。
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 --- |
一个简单的HTTP测试服务,用于试验Istio的各种特性:
1 |
kubectl apply -f samples/httpbin/httpbin.yaml |
Fortio是Istio的负载测试工具,提供CLI和Web UI。
Fortio能够按照指定的QPS来执行HTTP请求,录制执行耗时、百分比分布(percentiles,例如p99表示99%的请求耗时小于的数值)直方图。
1 |
kubectl apply -f samples/httpbin/sample-client/fortio-deploy.yaml |
这是一个基于Ubuntu的容器,它启动后只是简单的休眠。你可以通过kubectl连接到此容器并执行curl命令:
1 |
kubectl apply -f samples/sleep/sleep.yaml |
下面的例子创建了几个“虚拟服务“,这些虚拟服务将请求流量全部路由给各微服务的v1版本:
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日志,可以发现类似下面的内容:
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日志,可以发现相呼应的内容:
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,看看有何效果:
1 |
kubectl edit virtualservice reviews |
虚拟服务支持根据请求信息路由,下面的例子依据请求头end-user的值决定使用什么版本的reviews服务:
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的延迟:
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%的概率看到无星或红星:
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,这意味着访问该服务的客户端都会基本都会收到超时错误:
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。
istio-ingressgateway负责处理Gateway资源,就像K8S中Ingress Controller负责处理Ingress资源那样。
istio-ingressgateway默认已经同时支持HTTP/HTTPS,并在80/443端口上监听。
下面的例子创建一个HTTPS网关:
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:
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可以看到如下输出:
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的定义:
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为客户端签发证书,并在访问服务器时指定自己的密钥和此证书:
1 |
curl ... --cacert /home/alex/Documents/puTTY/ca.crt --cert alex.cert --key alex.key ... |
要从服务网格内访问外部URL,需要配置绕过代理的IP范围,或者配置出口网关,否则无法访问仅仅能得到404:
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的例子:
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服务:
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服务时生效:
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服务:
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个并发请求,因此会有几率出现错误:
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%:
1 2 |
Code 200 : 94 (9.4 %) Code 503 : 906 (90.6 %) |
查询客户端Proxy的统计信息,可以看到更多细节:
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:
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指标值:
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 |
下面的例子,为每个请求产生一条日志:
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的访问:
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是等价的:
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 |
以--set tracing.enabled=true选项安装Istio Chart,即可使用。由于默认设置的采样率较低,你需要反复访问多次/productpage才能捕获到Trace。
打开Istio内置Jaeger服务的Web界面,点击顶部按钮切换到Search页签,在Find Traces面板中Service选取productpage,点击Find Traces,看到如下的Trace列表:
点击其中一个Trace,可以查看其调用链信息:
可以看到,这个由单次/productpage请求产生的调用链由6个Span构成,每个Span对应一个彩色条带。每个Span消耗的时间都标注在界面上了。
打开Istio内置Jaeger服务的Web界面,点击顶部按钮切换到Dependencies页签,可以看到Jaeger依据Traces计算出的服务依赖关系图(DAG,有向无环图):
尽管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编写,收集请求头的代码如下:
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编写, 收集请求头的代码如下:
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默认会追踪任何请求, 对于开发、测试环境这是可以的,线上环境通常需要降低采样率,否则压力太大。调整采样率有两种方式:
- 使用官方Chart安装时,调整pilot.traceSampling,当前使用的Chart版本已经将其设置为1.0,即百分之一的采样率
- 修改Deployment istio-pilot的环境变量PILOT_TRACE_SAMPLING
采样率的精度是0.01
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代理实例,对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提供了一系列开箱即用的错误恢复机制:
- 超时
- 有限次数的重试,支持可变的重试延迟(jitter)
- 限制针对上游服务的并发连接数、并发请求数
- 主动健康检查——针对负载均衡池中所有成员进行健康检查,并移除不健康实例,移回健康实例
- 可精细控制的断路器(被动健康检查) ,同样针对负载均衡池中的每个实例
所有这些特性,都可以在运行时,利用Istio的流量管理规则动态配置。
联用主动、被动健康检查,可以更大程度上降低访问到不健康实例的几率。联用底层基础设施的健康检查机制,不健康实例可以很快的被剔除。
利用流量管理规则,你可以为每个服务/版本来配置默认的故障恢复特性。
用于辅助测试端对端的故障恢复能力。
Istio支持多种具体的协议,并向其注入错误信息。而不是粗暴的杀死Pod,或者在TCP层延迟/污染网络包。你可以在应用层注入更加有意义的错误,例如HTTP的状态码。
Istio支持对特定请求进行错误注入,或者指定多少百分比的请求被错误注入。
可以注入的错误有两类:
- 延迟:模拟网络延迟和上游服务过载
- 中止:模拟上游服务错误,通常使用HTTP错误码、TCP连接错误的方式
流量镜像(Traffic mirroring)也叫shadowing,允许开发团队在最小风险的前提下改变产品的功能。其做法是将线上环境的流量复制到一个“镜像服务“中,镜像服务和主请求处理链是隔离的。
VirtualService支持为route配置mirror,实现流量镜像。
网关是运行在服务网格边缘的负载均衡器,它处理入站或出站的TCP/HTTP连接。
Istio提供入口网关,出口网关,对应:
- 的CRD为Gateway、Gateway+ServiceEntry
- 的Controller为istio-ingressgateway、istio-egressgateway
当提起术语“网关”时,基于上下文可能指Gateway这种资源,也可能指出口/入口网关的Service/Pod。
在典型的Kubernetes环境下,Ingress控制器读取Ingress资源,提供外部访问K8S集群的入口。
而使用Istio时,Ingress资源的地位可以由Gateway + VirtualServices代替。在集群内部组件相互交互时,不需要配置Gateway资源。需要注意,Istio也可以作为Ingress控制器,处理K8S原生的Ingress资源。
使用Istio Gateway时的典型的客户端访问时序如下:
- 客户端发起针对负载均衡器的请求
- 负载均衡器将请求转发给集群的istio-ingressgateway服务,该服务可以部署为NodePort,它的后端是一个Deployment
- IngressGateway根据Gateway资源、VirtualService资源的配置信息,将请求路由给K8S的Service
- Gateway提供端口、协议、数字证书信息
- VirtualService提供到K8S Service的路由信息
- K8S Service将请求路由给Pod
该时序如下图所示,注意IngressGateway的Sidecar负责转发客户端请求:
每当你创建/修改Gateway、VirtualService资源,Pilot会监测到并将其转换为Envoy配置,然后发送给相关的Sidecar,包括运行在IngressGateway Pod中的Envoy。
默认情况下,启用了Istio的微服务无法访问集群外部的URL, 这是因为Envoy修改了Pod所在网络命名空间的Iptables,透明的将所有出口流量都重定向到Sidecar,而不经任何配置Sidecar只会处理网格内部的Destination。
要将外部服务暴露给启用了Sidecar的网格内部Pod/VM(它们是外部服务的客户端),必须:
- 定义ServiceEntry,这种自定义资源允许针对指定的外部IP/DNS名称的流量通过Sidecar。ServiceEntry支持通配符匹配主机,可以降低配置工作量
- 或者,配置一个IP范围,让针对此IP范围的出站请求经过/绕过Envoy代理。你可以设置Chart变量,
12# 仅仅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,这种配置方式的适用场景是:
- 离开网格的所有流量必须流经一组专用节点,这一组节点有特殊的监控和审查
- 集群中的一般性节点不能联通网格外部
Istio使用一个简单的配置模型来描述API调用/TCP流量应该如何在服务网格中流动。使用此配置模型,你可以:
- 配置服务级别的属性,例如断路器、超时、重试
- 配置通用的持续交付(CD)任务,例如金丝雀发布(Canary rollout,即灰度发布)、A/B测试、阶段性部署(Staged rollout)——基于百分比的流量分配
Istio的配置模型映射为K8S的资源,包括VirtualService、DestinationRule、ServiceEntry、Gateway四种。在K8S中你可以用kubectl来配置这些资源,它们都是CR。
定义服务的请求如何在服务网格中路由。虚拟服务能够将请求路由给服务的不同版本,甚至是完全不同的服务。
VirtualService通过Istio的服务注册表定义了如何路由到一个或多个特定的Destinations(目标服务)。VirtualService可以根据请求的属性(如HTTP头、路径等)来匹配请求,并根据这些规则将流量路由到不同的目标。这意味着,对于给定的域名,基于请求的不同属性(比如URL路径),VirtualService可以将流量路由到多个不同的Destination。
虚拟服务自身不会在Istio服务注册表中添加新的服务条目。它的spec.hosts,用来指定进行路由时,如何匹配主机名,这通常对应HTTP请求的Host头。
Istio服务注册表中服务条目的来源包括:
- K8s服务
- ServiceEntry定义的服务
- Consul、Eureka等服务发现机制暴露的服务
- 在某些情况下,服务信息可以通过Istio的配置文件静态定义。这种方式较少用于生产环境,但可能在测试或特定场景下有其用途
下面的示例,将针对review服务的请求,3/4发给v1版本,1/4发给v2版本:
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秒,你可以覆盖此默认值。重试也类似:
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秒的延迟:
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错误:
1 2 3 4 5 6 7 8 9 10 |
... spec: ... http: - fault: abort: # 多少比例的请求被注入故障 percent: 10 # 返回的HTTP状态码 httpStatus: 400 |
delay和abort是可以联合使用的:
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:
1 2 3 4 5 6 7 8 9 |
... spec: http: # 仅针对reviews服务v2版本发起的请求 - match: - sourceLabels: app: reviews version: v2 route: ... |
也可以限定HTTP请求头:
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值:
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:
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:
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子元素),那么第一个(配置文件最前面)匹配的规则优先级最高。
Istio支持为虚拟服务配置跨源资源共享策略:
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重定向给调用者:
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请求的特定部分进行修改:
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路由的例子:
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 |
在VirtualService定义了路由规则后,DestinationRule进一步应用一系列的策略到请求上。DestinationRule定义了对于特定目标服务的流量策略,如负载均衡策略、连接池大小、TLS设置等。当VirtualService根据规则将流量路由到特定目标后,DestinationRule定义了如何处理到达这些目标的流量。
DestinationRule中的spec.host同样是指定这个规则应用到哪些请求,不会在Istio注册表中定义服务条目。
下面的示例,声明了服务子集:
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 |
可以针对所有子集配置:
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 |
也可以针对特定子集:
1 2 3 4 5 6 7 8 |
spec: subsets: - name: v2 labels: version: v2 trafficPolicy: loadBalancer: simple: ROUND_ROBIN |
基于一致性哈希的负载均衡可以实现基于HTTP请求头、Cookie或其它属性的软的会话绑定(Session affinity):
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 |
可以限制某个服务/子集的并发度:
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 # 连接超时 connectTimeout: http: # 最大等待处理的请求数量 http1MaxPendingRequests: 1024 # HTTP2最大请求数 http2MaxRequests: 1024 # 每个TCP连接可以被多少请求共享使用(重用),设置为1则禁用keep-alive maxRequestsPerConnection: 32 # 最大重试次数 maxRetries: 3 |
可以为某个服务/子集配置断路器:
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 |
用于访问不受网格管理的服务,将其作为服务条目添加到Istio内部管理的服务注册表(service registry)中。
ServiceEntry允许在Istio的内部服务注册表中手动添加条目,这些条目可以是网格内部的服务(MESH_INTERNAL)或网格外的服务(MESH_EXTERNAL)。这意味着你可以使Istio意识到那些不直接由Istio管理的服务。
对于MESH_INTERNAL类型的ServiceEntry,它用于添加网格内的服务到Istio的内部服务发现系统,这些服务可能由于某些原因(比如,它们运行在不同的Kubernetes集群或不同的网络命名空间中)而未被自动识别为网格服务。
下面的例子,允许Envoy代理针对gmem.cc的HTTP请求:
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。
下面的例子以主机名来静态的指定外部服务端点:
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来指定外部服务端点:
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文档有如下说明:
- hosts字段:
- 对于非HTTP服务(包括不透明HTTPS)来说,此字段中指定的DNS名称被忽略
- 对于非HTTP服务,如果此字段指定的是DNS名称,则ports,或/和addresses中的IP地址用于唯一性的标识目的地
- addresses字段: 表示关联到此服务的虚拟IP地址,可以是CIDR前缀:
- 对于HTTP服务来说,此字段被忽略,目的地根据HTTP头Host/Authority确定
- 对于非HTTP服务,hosts头被忽略。如果指定了addresses,则入站请求的IP地址和addresses指定的CIDR进行匹配,如果匹配成功,则认为请求的是当前服务。如果不指定addresses,则入站流量仅仅根据ports来识别,这种情况下,服务的访问端口不能被网格内部任何其它服务共享
这段描述让人费解,也不符合实际试验结果。下面的两个ServiceEntry,都是非HTTP服务,都使用443端口,都没有指定addresses字段,但是没有任何冲突出现:
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配置如下(可以看到集群、端点信息都是正常有效的):
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 } } ] } ] |
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 |
配置在网格的边缘,用于外部访问服务网格,为TCP/HTTP流量提供负载均衡。 Gateway仅仅用在独立运行Istio Proxy的那些Ingress/Egress网关上,定义在网格边缘上暴露的名字:
- 如果Gateway定义在Ingress网关上,那么对网格外部暴露名字。当然,外部需要将这些名字的DNS记录指向Istio网关对外的IP地址
- 如果Gateway定义在Egress网关上:
- 如果通过配置限定所有出口流量必须走Egress网关,那么对集群内部暴露名字
- 使用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)匹配。
下面的例子,允许网格外部发送针对gmem.cc的HTTPS请求:
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,它才能工作,精确的说,是网关产生的物理监听器才拥有了路由表:
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服务建立入口Gateway时,hosts字段被忽略:
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 |
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 |
需要以下细节:
- 上述虚拟服务和网关配对后,istio-ingressgateway的Pod立刻开始监听2181端口
- 但是,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设置等。
一个简单的配置示例:
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:
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:
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 |
然后,定义一个虚拟服务:
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 |
1.6新增,让运维人员能够描述单个非K8S工作负载,例如物理机或虚拟机,的属性。
必须伴随ServiceEntry一起使用,ServiceEntry使用选择器来匹配工作负载(包括Pod以及虚拟机、物理机),产生MESH_INTERNAL类型的服务。
当工作负载连接到istiod后,WorkloadEntry的Status会发生变化,反应工作负载的状态和一些其它细节,类似于K8S更新Pod的状态。
下面是一个例子:
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:
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 来允许访问外部 HTTP 服务:
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 |
注意:
- 没有 ServiceEntry,网格中的服务无法通过 Istio 的 DNS 发现机制解析外部服务的名称。但是底层DNS机制还是能够解析域名的
- 如果你需要对某些外部服务应用特定的路由规则,或者需要 Istio 对这些流量进行更细粒度的管理(例如,只允许访问特定的外部服务),则 ServiceEntry 是必需的
- 如果你需要详细控制如何访问特定的外部服务,包括服务发现、安全、和流量管理,那么 ServiceEntry 是必要的
如果你只是定义了一个 ServiceEntry 来允许访问外部服务,而没有配置流量经过 Egress Gateway,那么流量将直接从 Pod 的 Sidecar 代理出去,直接到达外部服务。ServiceEntry 使得服务网格内的服务能够访问和调用网格外的服务,但它本身并不强制流量经过 Egress Gateway。
定义 Gateway 作为 Egress Gateway:
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 |
注意:
- Gateway定义的是一个网关规则,而不是定义网关
- 网关其实就是运行Envoy的容器,它可以作为独立Pod运行,用作边界网关;也可以作为Sidecar运行,作为每个工作负载和外部通信的网关
- selector选择器决定了这个规则运用到哪些网关或者说Envoy/Istio Proxy容器。这里的含义是仅仅应用到出口网关
- 这个网关中定义的域名httpbin.org用前面的ServiceEntry负责,即方访问外部
定义 VirtualService 将流量从 Sidecar 代理路由到 Egress Gateway,然后到外部服务:
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 |
注意:
- gateways定义了这个虚拟服务应用到哪些网关,这里是应用到:
- mesh 特殊关键字,代表所有Sidecar
- 特定网关,具体来说,是VirtualService所在命名空间中,名为istio-egressgateway的网关。如果要应用到其它命名空间中运行的网关,使用gateway-namespace/gateway-name这种形式
- 路由规则:
- 对于来自mesh内部也就是sidecar的流量,转发给istio-egressgateway这个网关
- 对于来自istio-egressgateway的流量,发送给httpbin.org前面已经定义了对应的Gateway,负责发送到网格外部
Istio提供了一个灵活的模型,用以收集网格中服务的各种指标(telemetry)。指标收集工作主要由Mixer负责。
从逻辑上说,每次:
- 请求发起前,消费者的Envoy都会调用Mixer,执行前提条件检查(Check)
- 每次请求处理完成后,都会调用Mixer,发送监控指标(Report)
Sidecar在本地具有缓存,大部分前提条件检查都在本地完成。此外监控指标也具有本地缓冲,不会频繁的发送给Mixer。
Istio的策略和监控特性基于一个公共的模型来配置,你需要配置三类资源:
- Handlers:决定使用哪些适配器,这些适配器如何运作
- Instances:定义如何把请求属性映射为适配器的输入。Instance代表一个或多个适配器需要处理的数据块
- Rules:决定何时调用适配器,传递什么instance给它
而这些配置是基于:
- Adapters:封装Mixer和特定的基础设施后端的交互逻辑
- Templates:定义将请求属性映射为适配器输入的Schema。一个适配器可以支持多个模板
Mixer是高度模块化的、可扩展的组件。通过适配器,它支持多种日志记录、配额、授权、监控后端,并将这些策略、监控后端和Istio的其它部分解耦。
适配器封装了Mixer和外部基础设施后端(例如Prometheus)交互的逻辑,每个适配器都需要特定的配置参数才能工作,例如logging适配器需要知道后端服务的IP和端口。
具体使用哪些适配器,也就是启用哪些后端,通过配置文件来指定。
Istio提供了很多内置适配器,每种适配器都对应一个CRD,这些CRD的实例就是适配器的实例,也即Handler。Adaptor CRD的实例,表示一种自定义的适配器。自定义适配器实例化时使用Handler类型的CR。
适配器的类型使用CRD来描述,执行下面的命令查看系统中可用的适配器类型:
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适配器的实例:
1 2 3 |
kubectl -n istio-system get prometheuses.config.istio.io # NAME AGE # handler 1h |
本节后续内容讨论主要的内置适配器。
支持 metric 模板
此适配器暴露了HTTP访问端点,Prometheus可以来采集此端点,获取服务网格的各项指标
使用Istio官方提供的Chart时,默认会在当前命名空间安装一个Prometheus服务器,并自动配置以抓取以下端点:
- istio-mixer.istio-system:42422,所有由Mixer的Prometheus适配器生成的网格指标。指标名以istio_开头
- istio-mixer.istio-system:9093,用于监控Mixer自身的指标
- istio-mixer.istio-system:9102,Envoy生成的原始统计信息。指标名以envoy_开头
注意:目前Envoy的http://0.0.0.0:15090/stats/prometheus也暴露了Prometheus指标
该适配器包含的配置参数如下:
参数 | 说明 | ||||
metrics |
类型 Params.MetricInfo[],需要插入到Prometheus的指标的列表。如果指标在Istio中定义了,但是没有在这里进行适当配置,则不会推送到Prometheus:
示例:
|
||||
metricsExpirationPolicy |
可选参数,指定此Prometheus适配器生成的指标的过期策略 示例:
|
支持 metric 模板
将指标数据递送给StatsD后端
支持 checknothing、listentry、quota模板
用于Check,总是让检查失败
支持logentry模板
将Istio日志条目递送给Fluentd守护程序
支持kubernetes模板
从Kubernetes环境抽取信息,转换为Attributes,供下游适配器使用
支持listentry模板
用于Check,实现简单的黑、白名单
支持listentry模板
用于支持Istio的配额管理系统,用于开发环境下
支持quota模板
用于支持Istio的配额管理系统,用于生产环境下
支持 logentry、metric 模板
让Istio将日志、指标输出到标准输出、错误流,或者任何本地文件
支持tracespan模板
将追踪数据递送给Zipkin和兼容的Tracer
模板是Instance如何生成的依据。通常情况下都是直接定义Instance,并使用kind字段引用实例的模板类型。
内置模板不需要在配置存储中定义Template类型的CR。每种内置模板都对应独立的CRD,这些CRD的实例就是模板的实例,也即Instance。Template CRD的实例,表示一种自定义的模板。自定义模板实例化时使用Instance类型的CR。
本节描述的是内置模板的实例配置方式。要了解如何使用自定义模板,参考扩展Istio一文。
该模板用于描述需要分发给特定监控后端的运行时指标,配置示例:
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表达式的推断类型必须和字段的类型匹配。
该模板用于从请求中抽取单个值,并和listchecker适配器联用,实现值检查。配置示例:
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"] |
从请求中抽取单条日志。配置示例:
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"' |
声明用于进行限额的各个维度的取值。配置示例:
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" |
表示分布式跟踪中一个单独的Span。配置示例:
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"] | "" |
如果满足选择器,则执行一组intentions —— 将instance发送给handler处理。示例:
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[] |
每个Rule可以包含多个Action,表示选择器匹配时应该将哪些Instance发送给某个handler:
1 2 3 4 |
handler: prometheus-handler instances: - RequestCountByService - RequestCountByEndpoint |
Handler就是适配器的实例。配置项说明:
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类型的处理器:
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是依据模板创建出来的。 配置项说明:
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:
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是非常高可用的组件,并且,它能在整体上提供服务网格的可用性、降低延迟:
- 无状态,Mixer没有任何持久化状态
- 高度健壮,每个实例都具有99.999%的可用性
- 缓存和缓冲,Mixer可以在本地累积大量的临时数据
在每个Pod上都要部署的Sidecar,必须尽可能占用少的内存。这意味着Sidecar的本地缓存、缓冲不能太大。Mixer可以作为Sidercar的高可用二级缓存使用。此外,Mixer的缓冲可以在后端基础设施(例如Prometheus)失败的情况下,继续运行(接受Envoy发来的指标),因而提高了可用性。
Mixer的缓存/缓冲也减少了对后端基础设施的访问频率,甚至减少访问数据量,因为它可以在本地进行数据聚合。
描述一组属性,这些属性由Istio的某个组件产生。配置示例:
1 2 3 4 5 6 7 |
# 此配置的修订版号,由服务器分配 revision: string # 产生属性的组件的名称,可以是istio-proxy # 或者是类型为attributes的Mixer适配器 name: string # 在运行时,组件负责产生的属性的映射,从属性名到属性Spec attributes: map<string, AttributeManifest.AttributeInfo> |
属性(Attributes)是Istio策略/监控功能的关键概念。属性是一小块数据,描述某个请求的自身的特征(property)或请求所处的环境,例如一个属性可能表示请求的长度、响应的状态码、请求来自的IP地址。每个属性都具有名称和取值。这里的请求不一定是HTTP请求,它可以是任意的网络活动,例如TCP连接,TCP连接的原始客户端IP就是一个属性。
属性的字段,可能由请求的客户端Sidecar报告,也可能由请求的服务端Sidecar报告。
为了简化系统、改善开发者体验,Istio使用共享的属性定义 —— 所由组件都使用这套定义。
Mixer本质上就是属性处理程序。Envoy为每个请求调用Mixer,并且发送请求的属性。Mixer依据请求属性,以及Mixer自己的配置,对若干基础设施后端发起调用:
此对象用于描述一个属性的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调用是否命中本地缓存 |
在你配置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_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 |
判断是否字符串以目标子串开头/结尾,示例:
|
||
.endsWith | |||
emptyStringMap |
创建一个空的map[string][string],示例:
|
||
conditional |
模拟三元表达式:
|
表达式 | 说明 |
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安装时,它自动提供某些指标:
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客户端库被静态编译到Envoy代理中。你可以通过Istio提供的CRD和其它对象,对Mixer客户端进行配置。
istio.mixer.v1.Attributes表示一组强类型的名称/值对。很多Mixer的API可能产生或消费属性。
Istio使用属性来控制网格中服务的运行时行为。属性可以描述:
- Ingress流量的信息
- Egress流量的信息
- 流量所在的环境的信息
你部署的Istio具有一个固定的、它能够理解的属性表(vocabulary)。尽管某些特殊的Mixer适配器、服务能产生属性,但是属性主要由Envoy产生。
配置示例:
1 |
attributes: map<string, istio.mixer.v1.Attributes.AttributeValue> |
AttributeValue是string、int64、double、bool、bytes、 google.protobuf.Timestamp、google.protobuf.Duration、StringMap的一种
执行下面的命令列出这类资源:
1 |
kubectl -n istio-system get apikeys.config.istio.io |
使用APIKey,你可以明确的定义如何生成HTTP请求的Attribute:request.api_key。
APIKey的字段包括:
字段 | 描述 |
query |
string 三选一,是从给定的请求参数、请求头,还是Cookie中获取api_key |
header | |
cookie |
指定用来匹配Istio Attribute的子句:
字段 | 描述 | ||
clause |
map<string, StringMatch> 将属性名string映射到StringMatch。示例:
|
通过匹配HTTP请求的HTTP方法、URI路径,来定义用于生成API相关的Attributes的规范化配置。此CRD用于API属性生成的目的,而不是用于呈现身份验证、配、文档等其它API规范(例如OpenAPI)中常见的其它信息。
现有的,基于HTTP动词+路径的定义操作(方法),可以用此CRD来规范化,以便在Istio中使用。
例如,基于OpenAPI v2的宠物商店的API可以配置为如下的HTTPAPISpec:
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 |
定义HTTPAPISpecs和IstioService之间的绑定关系。下面的例子将HTTPAPISpec petstore和命名空间bar中的foo服务建立绑定:
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 |
这不是一个CRD,但是多个CRD会引用它,IstioService用于识别一个服务(包括可选的版本),服务的FQDN由名称、命名空间、实现相关的Domain后缀组成。
IstioService包括的字段如下:
字段 | 说明 |
name | 服务的短名称 |
namespace | 服务的命名空间 |
domain | 服务的DNS后缀 |
service | FQDN |
labels | 标签集 |
指定用于每个独立请求的配额规则集。示例:
1 2 3 4 5 6 7 8 9 10 |
# QuotaRule[] 此规则集包括的规则列表 rules: # AttributeMatch[] 如果为空,匹配所有请求。仅仅匹配的请求才应用配额 - match: # Quota[] 使用的配额列表 quotas: # 配额的名称 - name: string # 配额的量 charge: int64 |
用于指定配额规则集和IstioService之间的绑定关系。示例:
1 2 3 4 5 6 7 |
# IstioService[] 服务列表 services: # QuotaSpecBinding.QuotaSpecReference[] 对配额规则集的引用 quotaSpecs # QuotaSpec对象的名字和命名空间 - name: string namespace: string |
定义每个服务的(per-service)的客户端配置。示例:
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 |
定义客户端的TCP配置。示例:
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 |
列出Mixer可用的CRD:
1 2 3 4 5 6 7 8 |
# 列出此二进制文件中所有CRD mixs crd all # 列出此二进制文件中所有适配器的CRD mixs crd adapter # 列出此二进制文件中所有模板的CRD mixs crd instance |
对本地运行的Mixer服务进行liveness/readiness探测。
启动一个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 |
每种适配器都有自己的CRD,下面的例子是listchecker。该适配器检查输入参数是否在列表中,如果是,适配器返回true:
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,该适配器消费指标并且进行预聚合:
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] |
每种Instance都有自己的CRD,用于将请求属性映射为适配器的输入。下面的例子为prometheus适配器提供输入:
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"' |
指定在何时调用handler,传递什么instance给它。下面的规则表示,如果被访问的服务是service1,且请求头x-user为user1,则将名为requestduration的metric传递给Prometheus:
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两个地方进行配置:
- Client:
- QuotaSpec,指定配额的名称,客户端需要请求的量
- QuotaSpecBinding,条件性的将QuotaSpec关联到一个或多个服务
- Mixer:
- quota instance:从请求总抽取配额使用信息
- memquota handler:处理instance的适配器
- quota rule:规定何时将instance发送给handler
每个Quota实例都维护了一组计数器的集合,集合的大小即为dimensions的笛卡尔积。
当一个新请求到来时,如果Mixer发现超过在validDuration时间内超过maxAmount限制,则它发送RESOURCE_EXHAUSTED消息给请求方的Envoy,Envoy则返回429响应码给调用者。
memquota的时间区间使用滑动窗口的方式,分辨率为亚秒级。redisquota则既支持滚动窗口、也支持固定窗口算法。
适配器配置如下:
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,如果发现匹配则不再继续寻找,如果找不到匹配使用默认模式。
该Instance从请求中抽取memquota所需的输入:
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" |
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 |
配额规格,可以指定单次请求会消耗哪些配额,消耗的量是多大:
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 |
将配额规格关联到需要启用限速功能的服务:
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 |
如果要将配额绑定到所有服务,限制它们作为客户端时的访问速率,则:
1 |
- service: '*' |
上述几个CR可以通过下面的命令创建:
1 |
kubectl apply -f samples/bookinfo/policy/mixer-rule-productpage-ratelimit.yaml |
然后,反复刷新/productpage, 你会接收到错误提示:RESOURCE_EXHAUSTED:Quota is exhausted for: requestcount。这个信息是被调用服务的Envoy的响应体,它同时提供了429状态码。
查看productpage的日志,可以看到:
1 |
[2018-11-29T13:37:47.294Z] "GET /productpageHTTP/1.1" 429 |
再查看调用者即入口网关的日志,可以看到:
1 |
"GET /productpageHTTP/1.1" 429 ... outbound|9080||productpage.default.svc.k8s.gmem.cc |
被限速的情况下,入口网关根本没有访问到productpage,直接被后者的Sidecar给拦截了。B
本例中大部分资源都在istio-system中创建,如果你希望仅仅影响单个命名空间而非整个服务网格,则可以把它们都创建到指定的其它命名空间。
将单体应用分解为微服务可以获得很多好处,例如更灵活、更容易扩容、更容易重用。但是,微服务也引入额外的安全需求:
- 为了防止中间人攻击,需要进行流量加密。安全内网环境下可以忽略
- 为了提供灵活的访问控制,需要双向TLS认证,以及细粒度的访问控制策略
- 为了审计谁在什么时候访问了什么,需要审计工具
Istio的安全特性能够满足以上需求。Istio安全支持:
- 强身份验证(strong identity)
- 强大的策略配置
- 透明的TLS传输
- AAA —— 认证、授权、审计
和Istio安全有关的组件包括:
- Citadel,负责密钥、证书管理
- Sidecar和周边代理(perimeter proxies,运行在Ingress/Egress的代理 ),实现客户端 - 服务之间的安全通信
- Pilot,分发身份验证策略、安全命名信息给代理
- Mixer,管理授权和审计
身份标识(Identity)是任何安全基础设施的基础。两个服务开始交互前,需要相互交换自己身份的凭证信息,以进行双向身份验证:
- 客户端利用安全命名(secure naming information)信息检查服务器的身份,以确保它是服务的授权提供者(Runner)
- 在服务器端:
- 利用授权策略(authorization policies)来决定是否接受客户访问
- 记录审计日志 —— 谁在何时访问了什么
- 根据其使用的服务,对客户端计费
- 拒绝没付费的客户端的访问请求
在不同平台上,Istio使用不同方式来实现服务身份(Service identities):
- 在K8S上使用ServiceAccount
- 在GKE/GCE上使用GCP服务账号
- 在AWS上使用AWS IAM用户/角色账号
- 裸机器,可以使用用户账号、自定义服务账号、服务名称、Istio服务账号,等等
SPIFFE标准提供了一套框架,用来向复杂环境下的任何服务授予身份标识。
Istio兼容SPIFFE,例如在K8S中,X.509证书具有URI字段,并且格式为: spiffe://<domain>/ns/<namespace>/sa/<serviceaccount>,这允许K8S的服务和其它SPIFFE兼容系统进行交互。
Istio的PKI建立在Citadel之上,能够安全的为任何工作负载提供代表身份的数字证书(X.509,SPIFFE格式),并且支持密钥和证书的自动轮换(更新)。
在K8S场景下:
- Citadel会监控API Server,为所有Service Account创建SPIFFE格式的密钥对,并且存储为K8S的Sercret
- 当创建Pod后,Pod使用的Service Account的密钥对会挂载为卷
- Citadel会监控证书的生命周期,并自动轮换,然后自动更新Secret,从而让Pod自动获得更新
- Pilot会创建安全命名信息,此信息定义了什么Service Account能够运行什么服务。安全命名信息被传播给Envoy
Istio提供两种类型的身份验证:
- 传输身份验证:即服务-服务身份验证。 认证直接的请求发起者的身份,Istio支持双向身份验证
- 源(Origin)身份验证:即终端用户身份验证。验证最初发起请求的用户或者设备。Istio支持基于JSON Web Token(JWK)的请求级身份验证
通过配置服务的身份验证策略(authentication policies),可以为其启用身份验证功能。
通过服务消费者、提供者的Envoy代理,Istio实现了服务-服务通信的安全隧道。请求的处理步骤如下:
- 将消费者的请求重新路由到本地的Sidecar —— Envoy
- 消费者Envoy向提供者的Envoy发起双向认证的TLS握手。在握手期间,消费者Envoy也会对提供者进行安全命名检查,看看它的Service Account是否有权提供目标服务
- 两个Envoy建立TLS连接,流量通过此连接转发
- 提供者确认消费者有权限访问后,将流量转发给本地的应用容器
安全命名信息是一个多对多映射,从编码在数字证书中的服务的身份标识(K8S中为Service Account),到(被发现服务或DNS引用的)服务名称。从A到B的映射,其含义是A有资格运行B服务。
Pilot会监控K8S的API Server,取得Service Account + Pod等信息,生成安全命名信息,并安全的分发给Envoy。
在K8S中,Node Agent负责证书、密钥的提供,流程如下:
- Citadel创建一个gRPC服务,负责处理CSR请求
- Envoy通过SDS(Secrets discovery service) API发送证书、密钥请求给Node Agent
- Node Agent会根据请求来创建私钥、发送CSR给Citadel来签名。私钥一直不会离开节点,保证了安全性
- Citadel校验CSR并签名证书
- Node Agent将签名后的证书通过SDS API返回给Envoy
证书轮换时流程类似。
Istio使用基于角色的权限控制(RBAC),允许命名空间级别、服务级别、HTTP方法级别的访问控制。Istio支持:
- 基于角色的授权,简单易用
- 服务-服务、终端用户-服务,两种访问控制
- 高性能,授权直接在Envoy上发生
架构图如下:
默认情况下Citadel会自己生成密钥对并自签名,作为CA证书。你也可以提供现有的CA证书,只需要创建对应的保密字典即可:
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。
默认情况下,Citadel为工作负载生成密钥和证书,并且通过Secret卷挂载给Sidecar。这种方式的缺点包括:
- 证书轮换时的性能减退:混乱时Envoy需要热重启来得到新的证书、密钥
- 可能的安全隐患,因为私钥保存在K8S Secret中
除了使用适当的Helm参数之外,你还需要修改API Server,添加命令行参数:
1 2 |
--service-account-issuer=kubernetes.default.svc --service-account-signing-key-file=/etc/kubernetes/pki/sa.key |
这里的策略都是针对服务端(服务提供者)的:
与此特性相关的istio配置参数为:enableAutoMtls,对应的注入istio-proxy环境变量为ISTIO_AUTO_MTLS_ENABLED。
当启用此特性时,你仅仅需要配置授权策略(Authentication policy),而不需要配置DestinationRule的字段:
- 对于具有Sidecar的服务端负载,Istio自动配置客户端,使用mTLS流量
- 对于没有Sidecar的服务器负载,Istio自动配置客户端,使用明文
要启用,设置Chart参数 --set values.global.mtls.auto=true --set values.global.mtls.enabled=false
你可以为整个服务网格设置默认的身份验证策略(不指定targets):
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配置,则无法访问。
或者为某个命名空间设置默认的身份验证策略:
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: {} |
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字段中配置:
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:
1 2 3 |
spec: peers: - mtls: {} |
配置mode为PERMISSIVE,则mTLS是可选的,允许基于明文发起请求:
1 2 3 |
peers: - mTLS: mode: PERMISSIVE |
有了上面的、针对服务端的配置后,如果客户端不经任何配置,所有请求都会遭遇503错误。
要解决此问题,你必须为客户端配置DestinationRule,声明使用mtls:
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目录下:
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:
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 |
Kubernetes的API Server也没有Sidecar,如果网格内部服务需要访问K8S API Server,则也需要DestinationRule配置:
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 |
origins字段定义了终端用户的身份验证策略。目前仅仅支持JWT认证。
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,但是不保证同时送达。为了保证行为一致性:
- 需要启用/禁用mTLS时,先切换为临时的mode: PERMISSIVE,这样明文和加密都可以接受
- 对于JWT源身份认证,在切换策略之前,请求必须携带新的JWT
要启用访问控制,你需要配置CRD —— RbacConfig。这是一个全局的单例对象,名字必须为default:
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"] |
已经废弃,使用AuthorizationPolicy代替。
定义一个角色,并声明该角色对哪些服务具有哪些访问权限。
ServiceRole包含一系列rules,也就是许可(permissions):
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 |
除了限定命名空间 + 服务 + 动词 + 路径以外,你还可以提供额外的约束,例如限定请求头:
1 2 3 4 |
constraints: # 请求头中的version必须声明为v1或v2 - key: request.headers[version] values: ["v1", "v2"] |
已经废弃,使用AuthorizationPolicy代替。
为用户、组、服务授予ServiceRole。示例:
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" |
要配置Istio授权策略,现在应该使用这个新API。
每个AuthorizationPolicy包含:
- 一个选择器,对应selector字段,指定策略作用于的Target,selector和metadata.namespace共同决定了Target
- 多个规则,每个规则指定Who允许在什么Conditions下做What动作
- rule.from字段即Who
- rule.to字段对应What
- rule.when对应Conditions
示例,如果请求来自dev命名空间或SA账号并且携带了合法JWT则允许访问foo命名空间的httpbin:v1服务:
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的行为,可以修改istio-sidecar-injector这个Configmap,或者给Pod添加对应的注解。
istio-sidecar-injector声明了需要给目标Pod注入的两个容器 —— istio-init、istio-proxy —— 的全部配置信息。这两个容器会自动读取Pod上的注解,覆盖对应的默认配置。
默认配置在安装时,通过Helm Chart的Values传入。
如果当前命名空间启用了自动化的Sidecar注入,而某个Deployment却不想加入网格,可以在其PodTemplate上添加注解:
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:
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} |
此容器运行脚本/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规则的变更。例如:
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-sidecar-injector中istio-proxy容器的命令行参数,从而间接的影响Envoy的行为。
此代理的入口点是pilot-agent,该进程会创建envoy进程,执行下面的命令可以了解可传递给Envoy的参数列表:
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 |
下面的配置,可以让位于网格内部的容器访问10.0.0.0/8网段,而不需要定义ServiceEntry即可访问该网络的任意IP(或者DNS名称)的任意端口:
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 |
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。
使用这个CRD,可以配置其关联的工作负载(如果没有workloadSelector则命名空间所有工作负责)的入站、出站通信规则。
具体参考Sidecar资源一节。
这个CRD用于描述Envoy代理的网络过滤器/HTTP过滤器的配置信息,可以用来定制化Istio生成的Envoy配置。使用EnvoyFilter你可以:
- 修改某个字段的值
- 添加过滤器
- 添加一个完整的Listener、Cluster,等等
使用此CR要小心,错误的配置可能破坏网格。
对于每个工作负载,多个EnvoyFilter可以被增量的应用上去。这些EnvoyFilter的应用顺序是:
- 定义在rootNamespace中的所有EnvoyFilter
- 定义在工作负载所属命名空间的、匹配工作负载的过滤器
配置项 | 说明 |
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:
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:
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 |
例子三, 配置入站网关:
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 |
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参数,例如:
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。
使用proxy-status命令可以获取网格的整体状态,并识别由代理造成的问题。使用proxy-config命令可以查看Envoy的配置以帮助诊断问题。
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(路由发现服务)的状态都被显示。状态的值可以是:
- SYNCED,Envoy已经确认了Pilot发送给它的最后配置
- SYNCED (100%) ,Envoy已经完全同步了集群的端点
- NOT SENT ,Pilot没有发送任何东西给Envoy
- STALE,Pilot发送了更新给Enovy,但是后者没有接收到并给与确认。可能原因是网络故障或者Istio的Bug
执行下面的命令可以查看Pilot配置和某个Envoy实例配置存在什么差异,也就是哪些配置没有同步:
1 |
istioctl proxy-status details-v1-876bf485f-bxwh7.default |
istioctl proxy-config包含以下子命令:
Kiali是当前Istio主推的前端工具,能够很容易的显示服务网格包含哪些微服务,这些微服务如何连接在一起。
Kiali能够显示Service Mesh拓扑结构,展示请求速率、断路器等Istio特性。它还能结合Jaeger,以提供调用链跟踪功能。
你可以使用动态客户端来CRUD各种Istio对象,Istio项目本身将K8S资源表示为IstioObject,后者需要转化为model.Config,并进一步获取Istio内部对象:
1 2 |
config, err := ConvertObject(schema, item, c.client.domainSuffix) serviceEntry := config.Spec.(*networking.ServiceEntry) |
该项目提供了Istio的强类型客户端:
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 } |
要使用此客户端,可以:
1 2 3 4 |
// 先获得*restclient.Config cfg, err := clientcmd.BuildConfigFromFlags(masterURL, kubeconfig) // 然后构造Istio客户端 istioClient, err := istioclientset.NewForConfig(cfg) |
- 当创建Pod之后,Admission Controller自动为其注入两个容器:istio-init、istio-proxy
- istio-init仅仅是一个脚本,它负责配置Pod网络命名空间的iptables。默认使用REDIRECT方式,将任何入站、出站流量重定向给istio-proxy中运行的Envoy所监听的15001端口,此端口由名为virtual的Envoy监听器监听
- istio-proxy启动后,使用xDS协议向Pilot发起请求,以同步配置
- 不论是入站流量,还是出站流量,都会被劫持到15001这个Envoy监听器:
- 入站流量的流动轨迹:iptables ⇨ Istio代理入站处理器(InboundHandler) ⇨ 应用容器
- 出站流量的流动轨迹:iptables ⇨ Istio代理出站处理器(OutboundHandler) ⇨ 上游集群的特定端点
以Bookinfo应用为例,当productpage访问reviews时,入站(到reivew的Pod)流量流动轨迹如下:
说明:
- Iptables的FILTER-PREROUTING链拦截到进入review pod的流量
- 流量被转发给ISTIO_INBOUND链
- 流量被转发给ISTIO_IN_REDIRECT链
- 流量被重定向给15001端口,即运行在Sidecar中Envoy的virtual监听器
- virtual监听器use_original_dst=true,导致:
- 流量转发给虚拟监听器ReviewPod_9080
- 监听器ReviewPod_9080将流量路由给上游集群inbound|9080|reviews.default.svc.gmem.cc
- 此上游集群的唯一端点是127.0.0.1:9080,也就是应用程序容器
- 发往127.0.0.1:9080的流量被iptables的OUTPUT链拦截,转发给ISTIO_OUTPUT链
- ISTIO_OUTPUT将流量转发给ISTIO_REDIRECT链
- ISTIO_REDIRECT链将流量达到应用程序容器的9080端口上
以Bookinfo应用为例,当reviews访问ratings时,出站(到ratings的Pod)流量流动轨迹如下:
说明:
- Iptables的FILTER-OUTPUT链拦截到从review pod发往ratings集群的流量
- 流量被转发给ISTIO_OUTPUT链
- 流量被转发给ISTIO_REDIRECT链
- 流量被重定向给15001端口,即运行在Sidecar中Envoy的virtual监听器。virtual监听器的use_original_dst=true,导致:
- 流量转发给虚拟监听器0.0.0.0_9080
- 通过解析HTTP头,虚拟监听器将请求路由给上游集群outbound|9080|ratings.default.svc.gmem.cc
- 经过负载均衡策略判断,选中某个RatingsPod作为端点
- 流量再次被转发给OUTPUT链,并依次经过ISTIO_OUTPUT ⇨ ISTIO_REDIRECT ⇨ POSTROUTING链出站,送达RatingsPod的监听端口
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 |
大概分析一下包处理流程:
- 外部客户端请求PodIP:80端口
- 第一次进入Iptabels
- 进入PREROUTING链
- 进入mangle表
- 跳转到ISTIO_INBOUND链
- 跳转到ISTIO_TPROXY链
- 由于目的地址不是127.0.0.1/32,因此封包被打上标记1337,并标记为由15001的监听者处理
- 封包通过mangle表,进而通过Iptables
- 进入mangle表
- PREROUTING之后需要进行选路处理,由于它包含1337标记,因此路由目标是lo网卡
- 由于之前已经标记由15001处理,因此Envoy进程获得封包的处理权
- Envoy进行内部处理后转发给匹配其元素目的地址的虚拟监听器,也就是PodIP:80
- 此监听器向127.0.0.1:80发起请求,这是另一个TCP连接,进入Iptables,OUTPUT链
- 如果目的地址不是127.0.0.1则会强制重定向到15001 —— Envoy总是以127.0.0.1访问负载的服务,因此不会重定向
- 如果发起请求的用户不是1337/1337,强制重定向到15001 —— Envoy总是以1337身份运行,因此不会重定向
- 请求被负载接收并处理
ISTIO_DIVERT链相关的规则并非必须(可以快速确定一个套接字是否应该本地处理,不需要遍历一系列iptables规则),删除Iptables的相应条目不影响Enovoy的工作。
我们可以通过istio proxy-config listener来Dump出任何代理的监听器列表,并且已经发现以下规律:
- 对于每个K8S服务的IP:PORT组合
- 如果服务是TCP的,则创建IP:PORT监听器
- 如果服务是HTTP的,则创建0.0.0.0:PORT监听器,如果多个HTTP服务使用PORT,则贡献之
- 对于本负载暴露的任何服务,创建IP:PORT监听器
上述行为会导致一些问题:
- 网格中任意一个HTTP服务占用了PORT,则TCP服务无法再使用此PORT
如果使用了Headless服务,额外的问题会出现。假设部署了三节点的ZooKeeper集群:
1 2 |
zk ClusterIP 10.111.213.56 <none> 2181/TCP 83m zk-headless ClusterIP None <none> 2181/TCP,3888/TCP,2888/TCP 83m |
则在每个节点的代理上,都会创建如下监听器:
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节点的集群请求。这就导致了第二个问题:
- 网格中任何两个Headless服务,不能使用相同的端口
- Headless服务的端口不能和集群中某个TCP服务的端口冲突
- Headless服务的端口也不能和集群中某个HTTP服务的端口冲突
相关Issue:https://github.com/istio/istio/issues/9784
此问题目前的进展:
- 现在具有命名空间隔离机制,不同命名空间可以使用同一端口
- 现在具有细粒度控制机制,只要一个服务不同时依赖于2-N个使用相同端口的Headless服务,也可以规避端口冲突问题
- HTTP服务和Headless TCP服务端口冲突问题,从1.3开始,可以通过协议嗅探特性解决
- 有个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,也就是简单的透传,这样貌似也就不会有问题?
接上个问题,对于ZooKeeper这样的中间件,其节点集之间本身的交互就无法在Istio下进行。
我们看一下ZooKeeper节点的Envoy集群配置:
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:
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地址,例如:
- 源IP地址作为身份信息的一部分
- 源IP地址和网络策略有关
- 需要对源IP地址进行审计
但凡这些场景,都会面临问题。
解决ZooKeeper不能在网格中运行的问题,可以使用Istio的注解:
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模式行为并不符合期望:
- 由于FeatureGate RunAsGroup没有开启,导致istio-proxy容器运行在root用户下,这与istio-init的Iptable规则冲突:
12iptables -t nat -A ISTIO_OUTPUT -m owner --uid-owner 1337 -j RETURNiptables -t nat -A ISTIO_OUTPUT -m owner --gid-owner 1337 -j RETURN其后果就是istio-proxy无限重定向,根本没法启动起来
-
即使开启此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地址的多种方法:
- 对于L7:可以考虑时用HTTP头传递源IP地址,例如x-forwarded-for
- 代理协议:HAProxy的代理协议可以提供源IP地址的元数据信息,通过过滤器envoy.listener.proxy_protocol,Envoy可以消费元数据,但是如何把源IP发给上游还是个问题
- 使用监听器过滤器envoy.listener.original_src,此过滤器能够复制下游连接的源IP地址,并将其作为上游的连接的源IP地址
original_src出现的比较晚,到2019年底Istio仍然没有考虑支持该监听器过滤器。
目前(2020),透明代理问题已经可以解决,具体方案如下:
- Istio开启TPROXY拦截模式
- 使用EnvoyFilter,注入监听器过滤器original_src,参考EnvoyFilter
- 添加必要的路由规则、Iptables规则,参考original_src
服务网格中,调用链路中每多一跳,就会引入两个Sidecar的逻辑,让Sidecar性能优化到极致非常必要。可以从如何拦截流量,如何在Sidecar之间进行通信这两个角度进行性能优化:
为了将流量引入到Sidecar,Istio使用了iptables规则,所有流量需要经过内核态 - 用户态 -内核态的转移,数据被反复拷贝、大量的中断、上下文切换,都影响了性能。
这属于iptables的固有缺陷,解决思路有两个:
- bpfilter,基于BPF的网络过滤内核模块,用于替换netfilter作为iptables的底层实现。对内核版本有需求
- 使用自定义网络套接字,支持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的支持。
报错信息:iptables v1.6.0: can't initialize iptables table `nat': Permission denied (you must be root)
报错原因:如果Pod使用了securityContext,会出现该情况。
解决方案:
- 不使用securityContext,或者将运行用户改为0
- 或者,修改Configmap istio-sidecar-injector,将istio-init容器的运行用户设置为0:
1234567891011121314apiVersion: v1data:config: |-policy: enabledtemplate: |-initContainers:- name: istio-initsecurityContext:runAsUser: 0capabilities:add:- NET_ADMINprivileged: truerestartPolicy: Always
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 即可:
1 2 3 4 5 6 7 8 |
command: - istio-iptables - "-p" - "15001" - "-z" - "15006" - "-u" - 1337 # 改为0 |
解决方法二:为API Server开启特性 --feature-gates=RunAsGroup=true。
Leave a Reply