istio 实践指南
本书将介绍 istio 相关实战经验与总结,助你成为一名云原生服务网格老司机😎。
关于本书
本书为电子书形式,内容为本人多年的云原生与 istio 实战经验进行系统性整理的结果,不废话,纯干货。
阅读方式
- 在线阅读: https://imroc.cc/istio/
- 导出 PDF: 点击右上角打印按钮,可保存为 PDF 文件。
评论与互动
本书已集成 giscus 评论系统,欢迎对感兴趣的文章进行评论与交流。
贡献
本书使用 mdbook 构建,已集成 Github Actions 自动构建和发布,欢迎 Fork 并 PR 来贡献干货内容 (点击右上角编辑按钮可快速修改文章)。
内容使用 markdown 格式,文章在 src
目录下。
许可证
您可以使用 署名 - 非商业性使用 - 相同方式共享 4.0 (CC BY-NC-SA 4.0) 协议共享。
优雅终止
概述
本文介绍在 istio 场景下实现优雅终止时需要重点关注的点,一些容器场景通用的关注点请参考 Kubenretes 最佳实践: 优雅终止 。
envoy 被强杀导致流量异常
当业务上了 istio 之后,流量被 sidecar 劫持,进程之间不会直接建立连接,而是经过了 sidecar 这一层代理:
当 Pod 开始停止时,它将从服务的 endpoints 中摘除掉,不再转发流量给它,同时 Sidecar 也会收到 SIGTERM
信号,立刻不再接受 inbound 新连接,但会保持存量 inbound 连接继续处理,outbound 方向流量仍然可以正常发起。
不过有个值得注意的细节,若 Pod 没有很快退出,istio 默认是会在停止开始的 5s 后强制杀死 envoy,当 envoy 进程不在了也就无法转发任何流量(不管是 inbound 还是 outbound 方向),所以就可能存在一些问题:
- 若被停止的服务提供的接口耗时本身较长(比如文本转语音),存量 inbound 请求可能无法被处理完就断开了。
- 若停止的过程需要调用其它服务(比如通知其它服务进行清理),outbound 请求可能会调用失败。
启用 EXIT_ON_ZERO_ACTIVE_CONNECTIONS
自 istio 1.12 开始,Sidecar 支持了 EXIT_ON_ZERO_ACTIVE_CONNECTIONS
这个环境变量,作用就是等待 Sidecar “排水” 完成,在响应时,也通知客户端去关闭长连接(对于 HTTP1 响应 “Connection: close” 这个头,对于 HTTP2 响应 GOAWAY 这个帧)。
如果想要全局启用,可以修改全局配置的 configmap,在 defaultConfig.proxyMetadata
下加上这个环境变量:
defaultConfig:
proxyMetadata:
EXIT_ON_ZERO_ACTIVE_CONNECTIONS: "true"
如果想针对指定工作负载启用,可以给 Pod 加注解:
proxy.istio.io/config: '{ "proxyMetadata": { "EXIT_ON_ZERO_ACTIVE_CONNECTIONS": "true" } }'
若使用的是腾讯云服务网格 TCM,可以在控制台启用:
istio 性能优化
istiod 负载均衡
envoy 定时重连。
istiod HPA
istiod 无状态,可水平扩展。
xDS 按需下发
- lazy loading
- Delta xDS
batch 推送间隔优化
istiod推送流控规则有合并推送策略,目前这个时间间隔默认值为100ms。可配,一般很少用户会关心这个,在 mesh 全局配置中可以改: PILOT_DEBOUNCE_AFTER 和 PILOT_DEBOUNCE_MAX。 主要取决于:用户期望流控规则更新的实时性,以及 istiod 稳定性的权衡,如果期望实时性高,则把防抖动时间设置短些,如果mesh规模大,希望istiod提高稳定性,则把防抖动时间设置长些。
关闭不必要的遥测
TODO
关闭 mtls
如果认为集群内是安全的,可以关掉 mtls 以提升性能
istio 版本
- istio 1.8: 资源消耗上,envoy 大概有 30% 的降低
限制 namespace 以减少 sidecar 资源占用
istio 默认会下发 mesh 内集群服务所有可能需要的信息,以便让 sidecar 能够与任意 workload 通信。当集群规模较大,服务数量多,namespace 多,可能就会导致 sidecar 占用资源很高 (比如十倍于业务容器)。
如果只有部分 namespace 使用了 istio (sidecar 自动注入),而网格中的服务与其它没有注入 sidecar 的 namespace 的服务没有多大关系,可以配置下 istio 的 Sidecar
资源,限制一下 namespace,避免 sidecar 加载大量无用 outbound 的规则。
配置方法
apiVersion: networking.istio.io/v1beta1
kind: Sidecar
metadata:
name: default
namespace: istio-system
spec:
egress:
- hosts:
- "prod/*"
- "test/*"
- 定义在 istio-system 命名空间下表示 Sidecar 配置针对所有 namespace 生效。
- 在 egress 的 hosts 配置中加入开启了 sidecar 自动注入的 namespace,表示只下发这些跟这些 namespace 相关的 outbound 规则。
为服务显式指定协议
背景
istio 需要知道服务提供什么七层协议,从而来为其配置相应协议的 filter chain,通常最好是显式声明协议,如果没有声明,istio 会自动探测,这个探测能力比较有限,有些时候可能会匹配协议错误(比如使用非标端口),导致无法正常工作。
本文将列出显示声明协议的方法。
集群内: 指定 Service 端口的协议
给集群内 Service 指定 port name 时加上相应的前缀或指定 appProtocol
字段可以显示声明协议,如:
kind: Service
metadata:
name: myservice
spec:
ports:
- number: 8080
name: rpc
appProtocol: grpc # 指定该端口提供 grpc 协议的服务
- number: 80
name: http-web # 指定该端口提供 http 协议的服务
更多详细信息请参考 Explicit protocol selection 。
集群外: 手动创建 Service + Endpoint
如果服务在集群外部 (比如 mysql),我们可以为其手动创建一组 Service + Endpoint,且 Service 端口指定协议(跟上面一样),这样就可以在集群内通过 Service 访问外部服务,且正确识别协议。示例:
apiVersion: v1
kind: Service
metadata:
name: mysql
namespace: default
spec:
ports:
- port: 4000
name: mysql
protocol: TCP
---
apiVersion: v1
kind: Endpoints
metadata:
name: mysql
namespace: default
subsets:
- addresses:
- ip: 190.64.31.232 # 替换外部服务的 IP 地址
ports:
- port: 4000
name: mysql
protocol: TCP
创建好之后就可以通过 svc 去访问外部服务了,本例中服务地址为: mysql.default.svc.cluster.local:4000
。
集群外: 使用 ServiceEntry 指定协议
如果外部服务可以被 DNS 解析,可以定义 ServiceEntry 来指定协议:
apiVersion: networking.istio.io/v1beta1
kind: ServiceEntry
metadata:
name: external-mysql
spec:
hosts:
- mysql.example.com
location: MESH_EXTERNAL
ports:
- number: 4000
name: mysql
protocol: mysql
resolution: DNS
创建好之后就可以通过域名去访问外部服务了,本例中服务地址为: mysql.example.com:4000
。
为服务设置默认路由
很多时候一开始我们的服务没有多个版本,也没配置 vs,只有一个 deployment 和一个 svc,如果我们要为业务配置默认流量策略,可以直接创建 dr,给对应 host 设置 trafficPolicy,示例:
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: reviews
spec:
host: reviews
trafficPolicy:
connectionPool:
tcp:
maxConnections: 100
subsets:
- name: v1
labels:
version: v1
需要注意的是,虽然 subsets 下也可以设置 trafficPolicy,但 subsets 下设置的不是默认策略,而且在没有 vs 明确指定路由到对应 subset 时,即便我们的服务只有一个版本,istio 也不会使用 subset 下指定的 trafficPolicy,错误示例:
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: reviews
spec:
host: reviews
subsets:
- name: v1
labels:
version: v1
trafficPolicy:
connectionPool:
tcp:
maxConnections: 100
想要做的更好,可以定义下 vs,明确路由到指定版本(后续就可以针对不同版本指定不同的流量策略):
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: reviews
spec:
hosts:
- reviews
http:
- route:
- destination:
host: reviews
subset: v1
配置 accesslog
本文介绍如何配置 istio 的 accesslog。
全局配置方法
修改 istio 配置:
kubectl -n istio-system edit configmap istio
编辑 yaml:
apiVersion: v1
kind: ConfigMap
metadata:
name: istio
namespace: istio-system
data:
mesh: |
accessLogEncoding: JSON
accessLogFile: /dev/stdout
accessLogFormat: ""
accessLogEncoding
: 表示 accesslog 输出格式,istio 预定义了TEXT
和JSON
两种日志输出格式。默认使用TEXT
,通常我们习惯改成JSON
以提升可读性,同时也利于日志采集。accessLogFile
: 表示 accesslog 输出到哪里,通常我们指定到/dev/stdout
(标准输出),以便使用kubectl logs
来查看日志,同时也利于日志采集。accessLogFormat
: 如果不想使用 istio 预定义的accessLogEncoding
,我们也可以使用这个配置来自定义日志输出格式。完整的格式规则与变量列表参考 Envoy 官方文档 。
如果使用 istioctl 安装的 istio,也可以用类似以下命令进行配置:
istioctl install --set profile=demo --set meshConfig.accessLogFile="/dev/stdout" --set meshConfig.accessLogEncoding="JSON"
局部启用 accesslog
在生产环境中,有时我们不想全局启用 accesslog,我们可以利用 EnvoyFilter 来实现只为部分 namespace 或 workload 启用 accesslog,参考 实用技巧: 局部启用 accesslog
日志格式
TEXT 格式
istio 的 text accesslog 配置格式见 源码 。转换成字符串为:
[%START_TIME%] "%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%" %RESPONSE_CODE% %RESPONSE_FLAGS% "%UPSTREAM_TRANSPORT_FAILURE_REASON%" %BYTES_RECEIVED% %BYTES_SENT% %DURATION% %RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% "%REQ(X-FORWARDED-FOR)%" "%REQ(USER-AGENT)%" "%REQ(X-REQUEST-ID)%" "%REQ(:AUTHORITY)%" "%UPSTREAM_HOST%" %UPSTREAM_CLUSTER% %UPSTREAM_LOCAL_ADDRESS% %DOWNSTREAM_LOCAL_ADDRESS% %DOWNSTREAM_REMOTE_ADDRESS% %REQUESTED_SERVER_NAME% %ROUTE_NAME%
JSON 格式
istio 的 json accesslog 配置格式见 源码 。转换成字符串为:
{
"authority": "%REQ(:AUTHORITY)%",
"bytes_received": "%BYTES_RECEIVED%",
"bytes_sent": "%BYTES_SENT%",
"downstream_local_address": "%DOWNSTREAM_LOCAL_ADDRESS%",
"downstream_remote_address": "%DOWNSTREAM_REMOTE_ADDRESS%",
"duration": "%DURATION%",
"istio_policy_status": "%DYNAMIC_METADATA(istio.mixer:status)%",
"method": "%REQ(:METHOD)%",
"path": "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%",
"protocol": "%PROTOCOL%",
"request_id": "%REQ(X-REQUEST-ID)%",
"requested_server_name": "%REQUESTED_SERVER_NAME%",
"response_code": "%RESPONSE_CODE%",
"response_flags": "%RESPONSE_FLAGS%",
"route_name": "%ROUTE_NAME%",
"start_time": "%START_TIME%",
"upstream_cluster": "%UPSTREAM_CLUSTER%",
"upstream_host": "%UPSTREAM_HOST%",
"upstream_local_address": "%UPSTREAM_LOCAL_ADDRESS%",
"upstream_service_time": "%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%",
"upstream_transport_failure_reason": "%UPSTREAM_TRANSPORT_FAILURE_REASON%",
"user_agent": "%REQ(USER-AGENT)%",
"x_forwarded_for": "%REQ(X-FORWARDED-FOR)%"
}
参考资料
使用 corsPolicy 解决跨域问题
通常解决跨域问题都是在 web 框架中进行配置,使用 istio 后我们可以将其交给 istio 处理,业务不需要关心。本文介绍如何利用 Istio 配置来对 HTTP 服务启用跨域支持。
配置方法
Istio 中通过配置 VirtualService 的 corsPolicy 可以实现跨域支持,示例:
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: nginx
namespace: istio-demo
spec:
gateways:
- istio-demo/nginx-gw
hosts:
- 'nginx.example.com'
- 'test.example.com'
http:
- corsPolicy:
allowOrigins:
- regex: "https?://nginx.example.com|https?://test.example.com"
route:
- destination:
host: nginx.istio-demo.svc.cluster.local
port:
number: 80
- 关键配置在于
allowOrigins
,表示允许带哪些 Origin 地址的请求。 - 若有多个域名,使用
regex
匹配,|
符号分隔。 - 若同时支持 http 和 https,
regex
中的地址在http
后面加s?
,表示匹配http
或https
,即两种协议同时支持。 - 关于
corsPolicy
更多配置,参考 Istio CorsPolicy 官方文档 。
一些误区
有同学测试发现,当请求带上了错误的 Origin
或没带 Origin
时,响应内容也正常返回了:
$ curl -I -H 'Origin: http://fake.example.com' 1.1.1.1:80
HTTP/1.1 200 OK
...
为什么能正常返回呢?corsPolicy 没生效吗?有这样疑问的同学可能对 CORS 理解不太到位。
控制请求能否跨域的逻辑核心在于浏览器,浏览器通过判断请求响应的 access-control-allow-origin header 中是否有当前页面的地址,来判断该跨域请求能否被允许。所以业务要对跨域支持的关键点在于对 access-control-allow-origin
这个头的支持,通常一些 web 框架支持跨域也主要是干这个,为响应自动加上 access-control-allow-origin
响应头,istio 也不例外。
所以这里请求一般都能正常返回,只是如果跨域校验失败的话不会响应 access-control-allow-origin
这个 header 以告知浏览器该请求不能跨域,但响应的 body 是正常的,不会做修改。
基于 iphash 进行负载均衡
场景
根据源 IP 进行负载均衡,在 istio 中如何配置呢 ?
用法
配置 DestinationRule
,指定 useSourceIp
负载均衡策略:
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: bookinfo-ratings
spec:
host: ratings.prod.svc.cluster.local
trafficPolicy:
loadBalancer:
consistentHash:
useSourceIp: true
参考资料
使用 websocket 协议
业务使用的 websocket 协议,想跑在 istio 中,那么在 istio 中如何配置 websocket 呢?
用法
由于 websocket 本身基于 HTTP,所以在 istio 中直接按照普通 http 来配就行了:
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: tornado-gateway
spec:
selector:
istio: ingressgateway
servers:
- port:
number: 80
name: http
protocol: HTTP
hosts:
- "*"
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: tornado
spec:
hosts:
- "*"
gateways:
- tornado-gateway
http:
- match:
- uri:
prefix: /
route:
- destination:
host: tornado
weight: 100
参考资料
- 官方 sample: Tornado - Demo Websockets App
设置 max_body_size
背景
nginx 可以设置 client_max_body_size
,那么在 istio 场景下如何调整客户端的最大请求大小呢?
解决方案
可以配置 envoyfilter:
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: limit-request-size
namespace: istio-system
spec:
workloadSelector:
labels:
istio: ingressgateway
configPatches:
- applyTo: HTTP_FILTER
match:
context: GATEWAY
listener:
filterChain:
filter:
name: envoy.http_connection_manager
patch:
operation: INSERT_BEFORE
value:
name: envoy.buffer
typed_config:
'@type': type.googleapis.com/udpa.type.v1.TypedStruct
value:
maxRequestBytes: 1048576 # 1 MB
已验证版本: istio 1.8
- 更改
workloadSelector
以选中需要设置的 gateway
实现基于 Header 的授权
背景
部分业务场景在 http header 或 grpc metadata 中会有用户信息,想在 mesh 这一层来基于用户信息来对请求进行授权,如果不满足条件就让接口不返回相应的数据。
解决方案
Istio 的 AuthorizationPolicy 不支持基于 Header 的授权,但可以利用 VirtualService 来实现,匹配 http header (包括 grpc metadata),然后再加一个默认路由,使用的固定故障注入返回 401,示例:
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: helloworld-server
spec:
hosts:
- helloworld-server
http:
- name: whitelist
match:
- headers:
end-user:
regex: "roc"
route:
- destination:
host: helloworld-server
port:
number: 9000
- name: default
route:
- destination:
host: helloworld-server
port:
number: 9000
fault:
abort:
percentage:
value: 100
httpStatus: 401
利用 Prism 构造多版本测试服务
概述
Prism 是一个支持 http mock 的开源工具,可以解析 openapi 配置,根据配置进行相应的响应,我们可以利用它来实现部署多版本服务,用于测试 istio 多版本服务相关的功能。本文给出一个简单的示例。
准备 OpenAPI 配置
我们将 OpenAPI 配置文件存到 ConfigMap 中,用于后续挂载到 prism 作为配置文件 (prism-conf.yaml
):
apiVersion: v1
kind: ConfigMap
metadata:
name: prism-conf
data:
mock-v1.yaml: |
openapi: 3.0.3
info:
title: MockServer v2
description: MockServer v2
version: 1.0.0
paths:
'/':
get:
responses:
'200':
content:
'text/plain':
schema:
type: string
example: v1
mock-v2.yaml: |
openapi: 3.0.3
info:
title: MockServer v2
description: MockServer v2
version: 1.0.0
paths:
'/':
get:
responses:
'200':
content:
'text/plain':
schema:
type: string
example: v2
这里的配置很简单,两个 OpenAPI 配置文件,GET
方式请求 /
路径分别响应 v1
和 v2
的字符串,以便从响应中就能区分出请求转发到了哪个版本的服务。
如果想用编辑器或 IDE 的 OpenAPI 插件编辑配置文件来定义更复杂的规则,可以先直接创建原生 OpenAPI 配置文件 (如 mock-v1.yaml
和 mock-v2.yaml
),然后使用类似下面的命令生成 configmap 的 yaml:
kubectl create configmap prism-conf --dry-run=client -o yaml
--from-file=mock-v1.yaml \
--from-file=mock-v2.yaml | \
grep -v creationTimestamp > prism-conf.yaml
部署多版本服务
使用 Deployment 部署两个版本的 prism (注意开启下 sidecar 自动注入),分别挂载不同的 OpenAPI 配置,首先部署第一个版本 (mockserver-v1.yaml
):
apiVersion: apps/v1
kind: Deployment
metadata:
name: mockserver-v1
spec:
replicas: 1
selector:
matchLabels:
app: mockserver
version: v1
template:
metadata:
labels:
app: mockserver
version: v1
spec:
containers:
- name: mockserver
image: cr.imroc.cc/library/prism:4
args:
- mock
- -h
- 0.0.0.0
- -p
- "80"
- /etc/prism/mock-v1.yaml
volumeMounts:
- mountPath: /etc/prism
name: config
volumes:
- name: config
configMap:
name: prism-conf
再部署第二个版本 (mockserver-v2.yaml
):
apiVersion: apps/v1
kind: Deployment
metadata:
name: mockserver-v2
spec:
replicas: 1
selector:
matchLabels:
app: mockserver
version: v2
template:
metadata:
labels:
app: mockserver
version: v2
spec:
containers:
- name: mockserver
image: cr.imroc.cc/library/prism:4
args:
- mock
- -h
- 0.0.0.0
- -p
- "80"
- /etc/prism/mock-v2.yaml
volumeMounts:
- mountPath: /etc/prism
name: config
volumes:
- name: config
configMap:
name: prism-conf
最后创建 Service (mockserver-svc.yaml
):
apiVersion: v1
kind: Service
metadata:
name: mockserver
labels:
app: mockserver
spec:
type: ClusterIP
ports:
- port: 80
protocol: TCP
name: http
selector:
app: mockserver
测试访问
没有定义任何其它规则,测试访问会随机负载均衡到 v1 和 v2:
$ for i in {1..10};do curl mockserver && echo ""; done
v2
v1
v2
v1
v2
v1
v2
v1
v2
v1
使用 DestinationRule 定义多版本服务
在 DestinationRule 定义使用 pod label 来区分 v1 和 v2 版本的服务 (mockserver-dr.yaml
):
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: mockserver
spec:
host: mockserver
subsets:
- labels:
app: mockserver
version: v2
name: v1
- labels:
app: mockserver
version: v2
name: v2
使用 VirtualService 定义多版本路由规则
这里定义一个简单的规则,v1 版本服务接收 80% 流量,v2 版本接收 20% (mockserver-vs.yaml
):
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: mockserver
spec:
hosts:
- mockserver
http:
- route:
- destination:
host: mockserver
port:
number: 80
subset: v1
weight: 80
- destination:
host: mockserver
port:
number: 80
subset: v2
weight: 20
测试验证多版本流量转发规则
上面定义了 DestinationRule 和 VirtualService 之后,会根据定义的规则进行转发:
$ for i in {1..10};do curl mockserver && echo ""; done
v1
v2
v1
v1
v2
v1
v1
v1
v1
v1
隐藏自动添加的 server header
背景
出于安全考虑,希望隐藏 istio 自动添加的 server: istio-envoy
这样的 header。
解决方案
可以配置 envoyfilter ,让 envoy 返回响应时不自动添加 server 的 header,将HttpConnectionManager 的 server_header_transformation 设为 PASS_THROUGH(后端没返回该header时envoy也不会自动添加):
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: hide-headers
namespace: istio-system
spec:
configPatches:
- applyTo: NETWORK_FILTER
match:
context: ANY
listener:
filterChain:
filter:
name: "envoy.http_connection_manager"
patch:
operation: MERGE
value:
typed_config:
"@type": "type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager"
server_header_transformation: PASS_THROUGH
参考资料
调试技巧
测试与 istiod 的连通性
测试数据面是否能连上 xDS 端口:
nc -vz istiod-1-8-1.istio-system 15012
测试 istiod 监控接口:
$ kubectl -n debug exec debug-6fd7477c9d-brqmq -c istio-proxy -- curl -sS istiod-1-8-1.istio-system:15014/debug/endpointz
[
{"svc": "cert-manager-webhook-dnspod.cert-manager.svc.cluster.local:https", "ep": [
{
"service": {
"Attributes": {
"ServiceRegistry": "Kubernetes",
"Name": "cert-manager-webhook-dnspod",
"Namespace": "cert-manager",
"Labels": {
没报错,正常返回 json 说明数据面能正常连接控制面
配置 accesslog
配置方法参考 这里 。
调整 proxy 日志级别
配置方法参考 这里 。
获取 metrics
kubectl -n test exec -c istio-proxy htmall-6657db8f8f-l74qm -- curl -sS localhost:15090/stats/prometheus
查看 envoy 配置
导出 envoy 配置:
kubectl exec -it -c istio-proxy $POD_NAME -- curl 127.0.0.1:15000/config_dump > dump.json
导出 envoy 全量配置(包含 EDS):
kubectl exec -it -c istio-proxy $POD_NAME -- curl 127.0.0.1:15000/config_dump?include_eds > dump.json
自定义 proxy 日志级别
概述
本文介绍在 istio 中如何自定义数据面 (proxy) 的日志级别,方便我们排查问题时进行调试。
动态调整
调低 proxy 日志级别进行 debug 有助于排查问题,但输出内容较多且耗资源,不建议在生产环境一直开启低级别的日志,istio 默认使用 warning
级别。
我们可以使用 istioctl 动态调整 proxy 日志级别:
istioctl -n istio-test proxy-config log productpage-v1-7668cb67cc-86q8l --level debug
还可以更细粒度控制:
istioctl -n istio-test proxy-config log productpage-v1-7668cb67cc-86q8l --level grpc:trace,config:debug
更多 level 可选项参考:
istioctl proxy-config log --help
如果没有 istioctl,也可以直接使用 kubectl 进入 istio-proxy 调用 envoy 接口来动态调整:
kubectl exec -n istio-test productpage-v1-7668cb67cc-86q8l -c istio-proxy -- curl -XPOST -s -o /dev/null http://localhost:15000/logging?level=debug
使用 annotation 指定
如果不用动态调整,也可以在部署时为 Pod 配置 annotation 来指定 proxy 日志级别:
template:
metadata:
annotations:
"sidecar.istio.io/logLevel": debug # 可选: trace, debug, info, warning, error, critical, off
全局配置
如果是测试集群,你也可以全局配置 proxy 日志级别:
kubectl -n istio-system edit configmap istio-sidecar-injector
修改 values
里面的 global.proxy.logLevel
字段即可。
如果使用 istioctl 安装 istio,也可以使用类似以下命令配置全局 proxy 日志级别:
istioctl install --set profile=demo --set values.global.proxy.logLevel=debug
配置 envoy componentLogLevel
如何细粒度的调整 envoy 自身的内部日志级别呢?可以给 Pod 指定 annotation 来配置:
template:
metadata:
annotations:
"sidecar.istio.io/componentLogLevel": "ext_authz:trace,filter:debug"
- Envoy component logging 说明
- 该配置最终会作为 envoy 的
--component-log-level
启动参数。
局部启用 accesslog
背景
在生产环境中,有时我们不想全局启用 accesslog,只想为部分 namespace 或 workload 启用 accesslog,而 istio 对 accesslog 的配置是全局的,如何只为部分数据面启用 accesslog 呢?下面介绍具体操作方法。
为部分 namespace 启用 accesslog
可以使用以下 Envoyfilter 来实现:
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: enable-accesslog
namespace: test # 只为 test 命名空间开启 accesslog
spec:
configPatches:
- applyTo: NETWORK_FILTER
match:
context: ANY
listener:
filterChain:
filter:
name: envoy.http_connection_manager
patch:
operation: MERGE
value:
typed_config:
"@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager"
access_log:
- name: envoy.access_loggers.file
typed_config:
"@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
path: "/dev/stdout"
log_format:
json_format:
authority: "%REQ(:AUTHORITY)%"
bytes_received: "%BYTES_RECEIVED%"
bytes_sent: "%BYTES_SENT%"
downstream_local_address: "%DOWNSTREAM_LOCAL_ADDRESS%"
downstream_remote_address: "%DOWNSTREAM_REMOTE_ADDRESS%"
duration: "%DURATION%"
method: "%REQ(:METHOD)%"
path: "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%"
protocol: "%PROTOCOL%"
request_id: "%REQ(X-REQUEST-ID)%"
requested_server_name: "%REQUESTED_SERVER_NAME%"
response_code: "%RESPONSE_CODE%"
response_flags: "%RESPONSE_FLAGS%"
route_name: "%ROUTE_NAME%"
start_time: "%START_TIME%"
upstream_cluster: "%UPSTREAM_CLUSTER%"
upstream_host: "%UPSTREAM_HOST%"
upstream_local_address: "%UPSTREAM_LOCAL_ADDRESS%"
upstream_service_time: "%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%"
upstream_transport_failure_reason: "%UPSTREAM_TRANSPORT_FAILURE_REASON%"
user_agent: "%REQ(USER-AGENT)%"
x_forwarded_for: "%REQ(X-FORWARDED-FOR)%"
- 已测试版本 istio 1.8。
- 若不指定
log_format
将会使用 Envoy Default Format String 。
为部分 workload 启用 accesslog
如果想要精确到只为指定的 workload 启用 accesslog,可以在 EnvoyFilter 上加一下 workloadSelector
:
spec:
workloadSelector:
labels:
app: "nginx"
低版本 istio
低版本 istio 使用的 envoy 不支持 v3 api,可以使用 v2:
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: enable-accesslog
namespace: test # 只为 test 命名空间开启 accesslog
spec:
configPatches:
- applyTo: NETWORK_FILTER
match:
context: ANY
listener:
filterChain:
filter:
name: envoy.http_connection_manager
patch:
operation: MERGE
value:
typed_config:
"@type": "type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager"
access_log:
- name: envoy.file_access_log
config:
path: /dev/stdout
Sidecar 停止问题
背景
Istio 在 1.1 版本之前有个问题: Pod 销毁时,如果进程在退出过程中继续调用其它服务 (比如通知另外的服务进行清理),会调用失败。
更多详细信息请参考 issue #7136: Envoy shutting down before the thing it's wrapping can cause failed requests 。
原因
Kubernetes 在销毁 Pod 的过程中,会同时给所有容器发送 SIGTERM 信号,所以 Envoy 跟业务容器同时开始停止,Envoy 停止过程中不接受 inbound 新连接,默认在 5s 内会接收 outbound 新连接,5s 后 envoy 被强制杀死。又由于 istio 会进行流量劫持,所有 outbound 流量都会经过 Envoy 进行转发,如果 Envoy 被杀死,outbound 流量无法被转发,就会导致业务调用其它服务失败。
社区解决方案
如果 Kubernetes 自身支持容器依赖管理,那这个问题自然就可以解决掉。社区也提出了 Sidecar Container 的特性,只可惜最终还是被废弃了,新的方案还未落地,详细可参考 这篇笔记 。
后来随着 istio 社区的推进,针对优雅终止场景进行了一些优化:
- 2019-02: Liam White 提交 PR Envoy Graceful Shutdown ,让 Pod 在停止过程中 Envoy 能够实现优雅停止 (保持存量连接继续处理,但拒绝所有新连接),等待
terminationDrainDuration
时长后再停掉 envoy 实例。该 PR 最终被合入 istio 1.1。 - 2019-11: Rama Chavali 提交 PR move to drain listeners admin endpoint ,将 Envoy 优雅停止的方式从热重启改成调用 Envoy 后来自身提供的 admin 接口 (/drain_listeners?inboundonly) ,重点在于带上了
inboundonly
参数,即仅仅拒绝 inbound 方向的新连接,outbound 的新连接仍然可以正常发起,这也使得 Pod 在停止过程中业务进程继续调用其它服务得以实现。该 PR 最终被合入 istio 1.5。
所以在 istio 1.5 及其以上的版本,在 Pod 停止期间的一小段时间内 (默认 5s),业务进程仍然可以对其它服务发请求。
如何解决 ?
考虑从自定义 terminationDrainDuration
或加 preStop 判断连接处理完两种方式之一,详细请参考 istio 最佳实践: 优雅终止 。
Sidecar 启动顺序问题
背景
一些服务在往 istio 上迁移过渡的过程中,有时可能会遇到 Pod 启动失败,然后一直重启,排查原因是业务启动时需要调用其它服务(比如从配置中心拉取配置),如果失败就退出,没有重试逻辑。调用失败的原因是 envoy 还没就绪(envoy也需要从控制面拉取配置,需要一点时间),导致业务发出的流量无法被处理,从而调用失败(参考 k8s issue #65502 )。
最佳实践
目前这类问题的最佳实践是让应用更加健壮一点,增加一下重试逻辑,不要一上来调用失败就立马退出,如果嫌改动麻烦,也可以在启动命令前加下 sleep,等待几秒 (可能不太优雅)。
如果不想对应用做任何改动,也可以参考下面的规避方案。
规避方案: 调整 sidecar 注入顺序
在 istio 1.7,社区通过给 istio-injector 注入逻辑增加一个叫 HoldApplicationUntilProxyStarts
的开关来解决了该问题,开关打开后,proxy 将会注入到第一个 container。
查看 istio-injector 自动注入使用的 template,可以知道如果打开了 HoldApplicationUntilProxyStarts
就会为 sidecar 添加一个 postStart hook:
它的目的是为了阻塞后面的业务容器启动,要等到 sidecar 完全启动了才开始启动后面的业务容器。
这个开关配置分为全局和局部两种,以下是启用方法。
全局配置:
修改 istio 的 configmap 全局配置:
kubectl -n istio-system edit cm istio
在 defaultConfig
下加入 holdApplicationUntilProxyStarts: true
apiVersion: v1
data:
mesh: |-
defaultConfig:
holdApplicationUntilProxyStarts: true
meshNetworks: 'networks: {}'
kind: ConfigMap
若使用 IstioOperator,defaultConfig 修改 CR 字段 meshConfig
:
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
metadata:
namespace: istio-system
name: example-istiocontrolplane
spec:
meshConfig:
defaultConfig:
holdApplicationUntilProxyStarts: true
如果你使用了 TCM (Tecnet Cloud Mesh),已经产品化了该能力,直接开启 Sidecar 就绪保障
即可:
局部配置:
如果使用 istio 1.8 及其以上的版本,可以为需要打开此开关的 Pod 加上 proxy.istio.io/config
注解,将 holdApplicationUntilProxyStarts
置为 true
,示例:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
annotations:
proxy.istio.io/config: |
holdApplicationUntilProxyStarts: true
labels:
app: nginx
spec:
containers:
- name: nginx
image: "nginx"
需要注意的是,打开这个开关后,意味着业务容器需要等 sidecar 完全 ready 后才能启动,会让 Pod 启动速度变慢一些。在需要快速扩容应对突发流量场景可能会显得吃力,所以建议是自行评估业务场景,利用局部配置的方法,只给需要的业务打开此开关。
完美方案: K8S 支持容器依赖
最完美的方案还是 Kubernetes 自身支持容器依赖,社区也提出了 Sidecar Container 的特性,只可惜最终还是被废弃了,新的方案还未落地,详细可参考 这篇笔记 。
参考资料
- Istio 运维实战系列(1):应用容器对 Envoy Sidecar 的启动依赖问题
- PR: Allow users to delay application start until proxy is ready
- Kubernetes Sidecar Containers 特性调研笔记
Smart DNS 相关问题
启用 Smart DNS 后解析失败
现象
在启用了 istio 的 Smart DNS (智能 DNS) 后,我们发现有些情况下 DNS 解析失败,比如:
- 基于 alpine 镜像的容器内解析 dns 失败。
- grpc 服务解析 dns 失败。
原因
Smart DNS 初期实现存在一些问题,响应的 DNS 数据包格式跟普通 DNS 有些差别,走底层库 glibc 解析没问题,但使用其它 dns 客户端可能就会失败:
- alpine 镜像底层库使用 musl libc,解析行为跟 glibc 有些不一样,musl libc 在这种这种数据包格式异常的情况会导致解析失败,而大多应用走底层库解析,导致大部分应用解析失败。
- 基于 c/c++ 的 grpc 框架的服务,dns 解析默认使用 c-ares 库,没有走系统调用让底层库解析,c-ares 在这种数据包异常情况,部分场景会解析失败。
修复
在 istio 1.9.2 的时候修复了这个问题,参考关键 PR #31251 以及其中一个 issue 。
规避
如果暂时无法升级 istio 到 1.9.2 以上,可以通过以下方式来规避:
- 基础镜像从 alpine 镜像到其它镜像 (其它基础镜像底层库基本都是 glibc)。
- c/c++ 的 grpc 服务,指定
GRPC_DNS_RESOLVER
环境变量为native
,表示走底层库解析,不走默认的 c-ares 库。环境变量解释参考 GRPC 官方文档 。
HTTP Header 大小写问题
Envoy 默认会将 Header 转换为小写
Envoy 缺省会把 http header 的 key 转换为小写,例如有一个 http header Test-Upper-Case-Header: some-value
,经过 envoy 代理后会变成 test-upper-case-header: some-value
。这个在正常情况下没问题,RFC 2616 规范也说明了处理 HTTP Header 应该是大小写不敏感的。
可能依赖大小写的场景
通常 header 转换为小写不会有问题(符合规范),有些情况对 header 大小写敏感可能就会有问题,如:
- 业务解析 header 依赖大小写。
- 使用的 SDK 对 Header 大小写敏感,如读取
Content-Length
来判断 response 长度时依赖首字母大写。
Envoy 所支持的规则
Envoy 只支持两种规则:
- 全小写 (默认使用的规则)
- 首字母大写 (默认没有启用)
如果应用的 http header 的大小写完全没有规律,就没有办法兼容了。
这两种是可以的:
- test-upper-case-header: some-value
- Test-Upper-Case-Header: some-value
类似这种就没有办法兼容了:
- Test-UPPER-CASE-Header: some-value
规避方案: 强制指定为 TCP 协议
我们可以将服务声明为 TCP 协议,不让 istio 进行七层处理,这样就不会更改 http header 大小写了,但需要注意的是同时也会丧失 istio 的七层能力。
如果服务在集群内,可以在 Service 的 port 名称中带上 "tcp" 前缀:
kind: Service
metadata:
name: myservice
spec:
ports:
- number: 80
name: tcp-web # 指定该端口协议为 tcp
如果服务在集群外,可以通过一个类似如下 ServiceEntry 将服务强制指定为 TCP Service,以避免 envoy 对其进行七层的处理:
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
name: qcloud-cos
spec:
hosts:
- "private-1251349835.cos.ap-guangzhou.myqcloud.com"
location: MESH_INTERNAL
addresses:
- 169.254.0.47
ports:
- number: 80
name: tcp
protocol: TCP
resolution: DNS
更多协议指定方式请参考: 为服务显式指定协议
最佳实践: 使用 EnvoyFilter 指定 Header 规则为首字母大写
如果希望 Envoy 对某些请求开启 Header 首字母大写的规则,可以用 EnvoyFilter 来指定:
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: http-header-proper-case-words
namespace: istio-system
spec:
configPatches:
- applyTo: NETWORK_FILTER # http connection manager is a filter in Envoy
match:
# context omitted so that this applies to both sidecars and gateways
listener:
name: XXX # 指定 cos使用的listener name,可以从config_dump中查询到
filterChain:
filter:
name: "envoy.http_connection_manager"
patch:
operation: MERGE
value:
name: "envoy.http_connection_manager"
typed_config:
"@type": "type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager"
http_protocol_options:
header_key_format:
proper_case_words: {}
注意替换 listener name
建议
应用程序应遵循 RFC 2616 规范,对 Http Header 的处理采用大小写不敏感的原则。
httpHeaderName 大写导致会话保持不生效
问题描述
在 DestinationRule 中配置了基于 http header 的会话保持,header 名称大写:
trafficPolicy:
loadBalancer:
consistentHash:
httpHeaderName: User
测试会发现会话保持不生效,每次请求传入相同 Header (如 User: roc
) 却被转发了不同后端
原因
应该是 envoy 默认把 header 转成小写的缘故导致不生效。
解决方案
定义 httpHeaderName
时换成小写,如:
trafficPolicy:
loadBalancer:
consistentHash:
httpHeaderName: User
注意事项
- 如果之前已经定义了 DestinationRule,不要直接修改,而是先删除,然后再创建修改后的 DestinationRule (实测发现直接修改成可能不生效)
- 客户端请求时设置的 header 大小写可以无所谓。
链路追踪相关问题
tracing 信息展示不完整
通过 UI 展示的链路追踪显示不完整,缺失前面或后面的调用链路。
原因
绝大多数情况下都是因为没在业务层面将 tracing 所需要的 http header 正确传递或根本没有传递。
要在 istio 中使用链路追踪,并不是说业务无侵入,有个基本要求是:业务收到 tracing 相关的 header 要将其传递给被调服务。这个步骤是无法让 istio 帮你完成的,因为 istio 无法感知你的业务逻辑,不知道业务中调用其它服务的请求到底是该对应前面哪个请求,所以需要业务来传递 header,最终才能将链路完整串起来。
参考资料
Sidecar 注入相关问题
可以只在一端注入 sidecar 吗?
- Q: 只在客户端和服务端其中一方注入 sidecar,是否能够正常工作呢?
- A: 一般是建议都注入。有些功能在 outbound 和 inbound 端都需要,有些只在其中一端需要,下面一张图可以一目了然:
默认的重试策略导致非幂等服务异常
背景
Istio 为 Envoy 设置了缺省的重试策略,会在 connect-failure,refused-stream, unavailable, cancelled, retriable-status-codes 等情况下缺省重试两次。出现错误时,可能已经触发了服务器逻辑,在操作不是幂等的情况下,可能会导致错误。
解决方案
可以通过配置 VS 关闭重试:
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: ratings
spec:
hosts:
- ratings
http:
- retries:
attempts: 0
多集群相关问题
概述
本文介绍 istio 多集群下需要注意的问题。
跨集群访问 service
同一网格的多个集群之间通过 service 调用,可能调用失败,报错 dns 解析失败。
原因可能是没启用 Smart DNS,无法自动解析其它集群的 service 域名,可以通过手动在本集群创建跟对端集群一样的 service 来解决,也可以启用 Smart DNS 自动解析。
应用未监听 0.0.0.0 导致连接异常
背景
istio 要求应用提供服务时监听 0.0.0.0
,因为 127 和 Pod IP 地址都被 envoy 占用了。有些应用启动时没有监听 0.0.0.0
或 ::
的地址,就会导致无法正常通信,参考 Application Bind Address 。
案例: zookeeper
当 zookeeper 部署到集群中时,默认监听的 Pod IP,会导致 zookeeper 各个实例之间无法正常通信。
解决方案: 在 zk 的配置文件中键入 quorumListenOnAllIPs=true 的配置 ( 参考 istio官方文档 )
istio 1.10
在 istio 1.10 及其以上的版本,应用将无需对端口监听进行特殊处理,即如果应用只监听 eth0 (pod ip) 也能正常使用,详细参考官方博客 Upcoming networking changes in Istio 1.10 。
VirtualService 路由匹配顺序问题
背景
在写 VirtualService 路由规则时,通常会 match 各种不同路径转发到不同的后端服务,有时候不小心命名冲突了,导致始终只匹配到前面的服务,比如:
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: test
spec:
gateways:
- default/example-gw
hosts:
- 'test.example.com'
http:
- match:
- uri:
prefix: /usrv
rewrite:
uri: /
route:
- destination:
host: usrv.default.svc.cluster.local
port:
number: 80
- match:
- uri:
prefix: /usrv-expand
rewrite:
uri: /
route:
- destination:
host: usrv-expand.default.svc.cluster.local
port:
number: 80
istio 匹配是按顺序匹配,不像 nginx 那样使用最长前缀匹配。这里使用 prefix 进行匹配,第一个是 /usrv
,表示只要访问路径前缀含 /usrv
就会转发到第一个服务,由于第二个匹配路径 /usrv-expand
本身也属于带 /usrv
的前缀,所以永远不会转发到第二个匹配路径的服务。
解决方案
这种情况可以调整下匹配顺序,如果前缀有包含的冲突关系,越长的放在越前面:
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: test
spec:
gateways:
- default/example-gw
hosts:
- 'test.example.com'
http:
- match:
- uri:
prefix: /usrv-expand
rewrite:
uri: /
route:
- destination:
host: usrv-expand.default.svc.cluster.local
port:
number: 80
- match:
- uri:
prefix: /usrv
rewrite:
uri: /
route:
- destination:
host: usrv.default.svc.cluster.local
port:
number: 80
也可以用正则匹配:
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: test
spec:
gateways:
- default/gateway
hosts:
- 'test.example.com'
http:
- match:
- uri:
regex: "/usrv(/.*)?"
rewrite:
uri: /
route:
- destination:
host: nginx.default.svc.cluster.local
port:
number: 80
subset: v1
- match:
- uri:
regex: "/usrv-expand(/.*)?"
rewrite:
uri: /
route:
- destination:
host: nginx.default.svc.cluster.local
port:
number: 80
subset: v2
headless service 相关问题
服务间通过注册中心调用响应 404
- 现象: 传统服务 (比如 Srping Cloud) 迁移到 istio 后,服务间调用返回 404。
- 原因: 没走 Kubernetes 的服务发现,而是通过注册中心获取服务 IP 地址,然后服务间调用不经过域名解析,直接向获取到的目的 IP 发起调用。由于 istio 的 LDS 会拦截 headless service 中包含的 PodIP+Port 的请求,然后匹配请求 hosts,如果没有 hosts 或者 hosts 中没有这个 PodIP+Port 的 service 域名 (比如直接是 Pod IP),就会匹配失败,最后返回 404。
- 解决方案:
- 注册中心不直接注册 Pod IP 地址,注册 service 域名。
- 或者客户端请求时带上 hosts (需要改代码)。
负载均衡策略不生效
由于 istio 默认对 headless service 进行 passthrougth,使用 ORIGINAL_DST
转发,即直接转发到原始的目的 IP,不做任何的负载均衡,所以 Destinationrule
中配置的 trafficPolicy.loadBalancer
都不会生效,影响的功能包括:
- 会话保持 (consistentHash)
- 地域感知 (localityLbSetting)
解决方案: 单独再创建一个 service (非 headless)
访问不带 sidecar 的 headless service 失败
- 现象: client (有sidecar) 通过 headless service 访问 server (无sidecar),访问失败,access log 中可以看到 response_flags 为
UF,URX
。 - 原因: istio 1.5/1.6 对 headless service 支持有个 bug,不管 endpoint 有没有 sidecar,都固定启用 mtls,导致没有 sidecar 的 headless 服务(如 redis) 访问被拒绝 (详见 #21964) ,更多细节可参考 Istio 运维实战系列(2):让人头大的『无头服务』-上。
解决方案一: 配置 DestinationRule
禁用 mtls
kind: DestinationRule
metadata:
name: redis-disable-mtls
spec:
host: redis.default.svc.cluster.local
trafficPolicy:
tls:
mode: DISABLE
解决方案二: 升级 istio 到 1.7 及其以上的版本。
pod 重建后访问失败
- 现象: client 通过 headless service 访问 server,当 server 的 pod 发生重建后,client 访问 server 失败,access log 中可以看到 response_flags 为
UF,URX
。 - 原因: istio 1.5 对 headless service 支持的 bug。
- client 通过 dns 解析 headless service,返回其中一个 Pod IP,然后发起请求。
- envoy 检测到是 headless service,使用
ORIGINAL_DST
转发,即不做负载均衡,直接转发到原始的目的 IP。 - 当 headless service 的 pod 发生重建,由于 client 与它的 sidecar (envoy) 是长连接,所以 client 侧的连接并没有断开。
- 又由于是长连接,client 继续发请求并不会重新解析 dns,而是仍然发请求给之前解析到的旧 Pod IP。
- 由于旧 Pod 已经销毁,Envoy 会返回错误 (503)。
- 客户端并不会因为服务端返回错误而断开连接,后续请求继续发给旧的 Pod IP,如此循环,一直失败。
- 更多详情参考 Istio 运维实战系列(3):让人头大的『无头服务』-下 。
解决方案: 升级 istio 到 1.6 及其以上的版本,Envoy 在 Upstream 链接断开后会主动断开和 Downstream 的长链接。
GRPC 服务负载不均
现象
grpc 调用,同一个 client 的请求始终只打到同一个 server 的 pod,造成负载不均。
分析
grpc 是基于 http2 的长连接,多次请求复用同一个连接。如果不用 istio,只用普通的 k8s service,是不会感知 grpc 协议的,只当成 tcp 来转发,在连接层面做负载均衡,不会在请求层面做负载均衡。但在 istio 中,默认会对 grpc 的请求进行请求级别的负载均衡,如果发现负载不均,通常是没有正确配置。 要让 grpc 在请求级别进行负载均衡,核心就是让 istio 正确识别是 grpc 协议,不要配置成 tcp,用 tcp 的话就只能在连接级别进行负载均衡了,请求级别可能就会负载不均。
解决方法
- 如果要对外暴露,gateway 里 protocal 配置 GRPC 不用 TCP,示例:
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
name: grpc-gw
namespace: demo
spec:
selector:
app: istio-ingressgateway
istio: ingressgateway
servers:
- hosts:
- '*'
port:
name: grpc-demo-server
number: 9000
protocol: GRPC # 这里使用 GRPC 不用 TCP
- 如果定义了 vs,需要使用 http 匹配而不用 tcp,因为 grpc 在 istio 中匹配也是用的 http 字段,示例:
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: grpc-svc
namespace: demo
spec:
gateways:
- demo/grpc-gw
hosts:
- '*'
http: # 这里使用 http 不用 tcp
- match:
- port: 9000
route:
- destination:
host: grpc.demo.svc.cluster.local
port:
number: 9000
weight: 100
- 部署服务的 service 的 port name 需要使用 "grpc-" 开头定义,让 istio 能够正确识别,示例:
apiVersion: v1
kind: Service
metadata:
name: grpc
namespace: demo
spec:
ports:
- name: grpc-9000 # 以 grpc- 开头
port: 9000
protocol: TCP
targetPort: 9000
selector:
app: grpc
type: ClusterIP
更多协议指定方式请参考 istio 最佳实践: 为服务显式指定协议
VirutualService 不生效
背景
使用 istio ,在集群中定义了 VirutualService
,但测试发现定义的规则似乎没有生效,通常是一些配置问题,本文列举一下常见的可能原因。
集群内访问: gateway 字段没有显式指定 "mesh"
如果 VirutualService
没有指定 gateways
字段,实际隐含了一层意思,istio 会默认加上一个叫 "mesh" 的保留 Gateway,表示集群内部所有 Sidecar,也就表示此 VirutualService
规则针对集群内的访问生效。
但如果指定了 gateways
字段,istio 不会默认加上 "mesh",如:
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: productpage
spec:
gateways:
- istio-test/test-gateway
hosts:
- bookinfo.example.com
http:
- route:
- destination:
host: productpage
port:
number: 9080
这个表示此 VirualService
规则仅对 istio-test/test-gateway
这个 Gateway 生效,如果是在集群内访问,流量不会经过这个 Gateway,所以此规则也就不会生效。
那如果要同时在集群内也生效该怎么做呢?答案是给 gateways
显式指定上 "mesh":
gateways:
- istio-test/test-gateway
- mesh
表示此 VirutualService
不仅对 istio-test/test-gateway
这个 Gateway 生效,也对集群内部访问生效。
参考 istio 官方文档 对此字段的解释:
值得注意的是,从集群内访问一般是直接访问 service 名称,这里 hosts
就需要加上访问集群内的 service 名称:
hosts:
- bookinfo.example.com
- productpage
通过 ingressgateway 访问: hosts 定义错误
若从 ingressgateway 访问,需要确保 Gateway
和 VirtualService
中的 hosts 均包含实际访问用到的 Host
或使用通配符能匹配得上,通常是外部域名。
只要有一方 hosts 没定义正确,都可能导致 404 Not Found
,正确示例:
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: test-gateway
namespace: istio-test
spec:
selector:
app: istio-ingressgateway
istio: ingressgateway
servers:
- port:
number: 80
name: HTTP-80-www
protocol: HTTP
hosts:
- bookinfo.example.com # 这里定义外部访问域名
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: productpage
spec:
gateways:
- istio-test/test-gateway
hosts:
- bookinfo.example.com # 这里也要定义外部访问域名
http:
- route:
- destination:
host: productpage
port:
number: 9080
Envoy 报错: gRPC config stream closed
gRPC config stream closed: 13
这通常是正常的,因为控制面(istiod)默认每 30 分钟强制断开 xDS 连接,然后数据面(proxy)再自动重连。
gRPC config stream closed: 14
如果只出现一次,通常是在 envoy 启动或重启时报这个错,没什么问题;但如果反复报这个错,可能是数据面(proxy)连接控制面(istiod)有问题,需要排查下。
参考资料
熔断不生效
未定义 http1MaxPendingRequests
我们给 DR 配置了 maxConnections
:
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: nginx
spec:
host: nginx
trafficPolicy:
connectionPool:
tcp:
maxConnections: 1
但测试当并发超过这里定义的最大连接数时,并没有触发熔断,只是 QPS 很低。通常是因为没有配置 http1MaxPendingRequests
,不配置默认为 2^32-1
,非常大,表示如果超过最大连接数,请求就先等待(不直接返回503),当连接数低于最大值时再继续转发。
如果希望连接达到上限或超过上限一定量后或直接熔断(响应503),那么就需要显式指定一下 http1MaxPendingRequests
:
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: nginx
spec:
host: nginx
trafficPolicy:
connectionPool:
tcp:
maxConnections: 1
http:
http1MaxPendingRequests: 1
地域感知不生效
使用 istio 地域感知能力时,测试发现没生效,本文介绍几个常见原因。
DestinationRule 未配置 outlierDetection
地域感知默认开启,但还需要配置 DestinationRule,且指定 outlierDetection
才可以生效,指定这个配置的作用主要是让 istio 感知 endpoints 是否异常,当前 locality 的 endpoints 发生异常时会 failover 到其它地方的 endpoints。
配置示例:
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: nginx
spec:
host: nginx
trafficPolicy:
outlierDetection:
consecutive5xxErrors: 3
interval: 30s
baseEjectionTime: 30s
client 没配置 service
istio 控制面会为每个数据面单独下发 EDS,不同数据面实例(Envoy)的locality可能不一样,生成的 EDS 也就可能不一样。istio会获取数据面的locality信息,获取方式主要是找到数据面对应的 endpoint 上保存的 region、zone 等信息,如果 client 没有任何 service,也就不会有 endpoint,控制面也就无法获取 client 的 locality 信息,也就无法实现地域感知。
解决方案: 为 client 配置 service,selector 选中 client 的 label。如果 client 本身不对外提供服务,service 的 ports 也可以随便定义一个。
使用了 headless service
如果是访问 headless service,本身是不支持地域感知的,因为 istio 会对 headless service 请求直接 passthrough,不做负载均衡,客户端会直接访问到 dns 解析出来的 pod ip。
解决方案: 单独再创建一个 service (非 headless)
istio-init crash
问题描述
在 istio 环境下有 pod 处于 Init:CrashLoopBackOff
状态:
wk-sys-acl-v1-0-5-7cf7f79d6c-d9qcr 0/2 Init:CrashLoopBackOff 283 64d 172.16.9.229 10.1.128.6 <none> <none>
查得 istio-init 的日志:
Environment:
------------
ENVOY_PORT=
INBOUND_CAPTURE_PORT=
ISTIO_INBOUND_INTERCEPTION_MODE=
ISTIO_INBOUND_TPROXY_MARK=
ISTIO_INBOUND_TPROXY_ROUTE_TABLE=
ISTIO_INBOUND_PORTS=
ISTIO_LOCAL_EXCLUDE_PORTS=
ISTIO_SERVICE_CIDR=
ISTIO_SERVICE_EXCLUDE_CIDR=
Variables:
----------
PROXY_PORT=15001
PROXY_INBOUND_CAPTURE_PORT=15006
PROXY_UID=1337
PROXY_GID=1337
INBOUND_INTERCEPTION_MODE=REDIRECT
INBOUND_TPROXY_MARK=1337
INBOUND_TPROXY_ROUTE_TABLE=133
INBOUND_PORTS_INCLUDE=*
INBOUND_PORTS_EXCLUDE=15090,15021,15020
OUTBOUND_IP_RANGES_INCLUDE=*
OUTBOUND_IP_RANGES_EXCLUDE=
OUTBOUND_PORTS_EXCLUDE=
KUBEVIRT_INTERFACES=
ENABLE_INBOUND_IPV6=false
Writing following contents to rules file: /tmp/iptables-rules-1618279687646418248.txt617375845
* nat
-N ISTIO_REDIRECT
-N ISTIO_IN_REDIRECT
-N ISTIO_INBOUND
-N ISTIO_OUTPUT
-A ISTIO_REDIRECT -p tcp -j REDIRECT --to-ports 15001
-A ISTIO_IN_REDIRECT -p tcp -j REDIRECT --to-ports 15006
-A PREROUTING -p tcp -j ISTIO_INBOUND
-A ISTIO_INBOUND -p tcp --dport 22 -j RETURN
-A ISTIO_INBOUND -p tcp --dport 15090 -j RETURN
-A ISTIO_INBOUND -p tcp --dport 15021 -j RETURN
-A ISTIO_INBOUND -p tcp --dport 15020 -j RETURN
-A ISTIO_INBOUND -p tcp -j ISTIO_IN_REDIRECT
-A OUTPUT -p tcp -j ISTIO_OUTPUT
-A ISTIO_OUTPUT -o lo -s 127.0.0.6/32 -j RETURN
-A ISTIO_OUTPUT -o lo ! -d 127.0.0.1/32 -m owner --uid-owner 1337 -j ISTIO_IN_REDIRECT
-A ISTIO_OUTPUT -o lo -m owner ! --uid-owner 1337 -j RETURN
-A ISTIO_OUTPUT -m owner --uid-owner 1337 -j RETURN
-A ISTIO_OUTPUT -o lo ! -d 127.0.0.1/32 -m owner --gid-owner 1337 -j ISTIO_IN_REDIRECT
-A ISTIO_OUTPUT -o lo -m owner ! --gid-owner 1337 -j RETURN
-A ISTIO_OUTPUT -m owner --gid-owner 1337 -j RETURN
-A ISTIO_OUTPUT -d 127.0.0.1/32 -j RETURN
-A ISTIO_OUTPUT -j ISTIO_REDIRECT
COMMIT
iptables-restore --noflush /tmp/iptables-rules-1618279687646418248.txt617375845
iptables-restore: line 2 failed
iptables-save
# Generated by iptables-save v1.6.1 on Tue Apr 13 02:08:07 2021
*nat
:PREROUTING ACCEPT [5214353:312861180]
:INPUT ACCEPT [5214353:312861180]
:OUTPUT ACCEPT [6203044:504329953]
:POSTROUTING ACCEPT [6203087:504332485]
:ISTIO_INBOUND - [0:0]
:ISTIO_IN_REDIRECT - [0:0]
:ISTIO_OUTPUT - [0:0]
:ISTIO_REDIRECT - [0:0]
-A PREROUTING -p tcp -j ISTIO_INBOUND
-A OUTPUT -p tcp -j ISTIO_OUTPUT
-A ISTIO_INBOUND -p tcp -m tcp --dport 22 -j RETURN
-A ISTIO_INBOUND -p tcp -m tcp --dport 15090 -j RETURN
-A ISTIO_INBOUND -p tcp -m tcp --dport 15021 -j RETURN
-A ISTIO_INBOUND -p tcp -m tcp --dport 15020 -j RETURN
-A ISTIO_INBOUND -p tcp -j ISTIO_IN_REDIRECT
-A ISTIO_IN_REDIRECT -p tcp -j REDIRECT --to-ports 15006
-A ISTIO_OUTPUT -s 127.0.0.6/32 -o lo -j RETURN
-A ISTIO_OUTPUT ! -d 127.0.0.1/32 -o lo -m owner --uid-owner 1337 -j ISTIO_IN_REDIRECT
-A ISTIO_OUTPUT -o lo -m owner ! --uid-owner 1337 -j RETURN
-A ISTIO_OUTPUT -m owner --uid-owner 1337 -j RETURN
-A ISTIO_OUTPUT ! -d 127.0.0.1/32 -o lo -m owner --gid-owner 1337 -j ISTIO_IN_REDIRECT
-A ISTIO_OUTPUT -o lo -m owner ! --gid-owner 1337 -j RETURN
-A ISTIO_OUTPUT -m owner --gid-owner 1337 -j RETURN
-A ISTIO_OUTPUT -d 127.0.0.1/32 -j RETURN
-A ISTIO_OUTPUT -j ISTIO_REDIRECT
-A ISTIO_REDIRECT -p tcp -j REDIRECT --to-ports 15001
COMMIT
# Completed on Tue Apr 13 02:08:07 2021
panic: exit status 1
goroutine 1 [running]:
istio.io/istio/tools/istio-iptables/pkg/dependencies.(*RealDependencies).RunOrFail(0x3bb0090, 0x22cfd22, 0x10, 0xc0006849c0, 0x2, 0x2)
istio.io/istio/tools/istio-iptables/pkg/dependencies/implementation.go:44 +0x96
istio.io/istio/tools/istio-iptables/pkg/cmd.(*IptablesConfigurator).executeIptablesRestoreCommand(0xc0009dfd68, 0x22c5a01, 0x0, 0x0)
istio.io/istio/tools/istio-iptables/pkg/cmd/run.go:493 +0x387
istio.io/istio/tools/istio-iptables/pkg/cmd.(*IptablesConfigurator).executeCommands(0xc0009dfd68)
istio.io/istio/tools/istio-iptables/pkg/cmd/run.go:500 +0x45
istio.io/istio/tools/istio-iptables/pkg/cmd.(*IptablesConfigurator).run(0xc0009dfd68)
istio.io/istio/tools/istio-iptables/pkg/cmd/run.go:447 +0x2625
istio.io/istio/tools/istio-iptables/pkg/cmd.glob..func1(0x3b5d680, 0xc0004cce00, 0x0, 0x10)
istio.io/istio/tools/istio-iptables/pkg/cmd/root.go:64 +0x148
github.com/spf13/cobra.(*Command).execute(0x3b5d680, 0xc0004ccd00, 0x10, 0x10, 0x3b5d680, 0xc0004ccd00)
github.com/spf13/cobra@v1.0.0/command.go:846 +0x29d
github.com/spf13/cobra.(*Command).ExecuteC(0x3b5d920, 0x0, 0x0, 0x0)
github.com/spf13/cobra@v1.0.0/command.go:950 +0x349
github.com/spf13/cobra.(*Command).Execute(...)
github.com/spf13/cobra@v1.0.0/command.go:887
main.main()
istio.io/istio/pilot/cmd/pilot-agent/main.go:505 +0x2d
原因与解决方案
跟这个 issue 基本一致 https://github.com/istio/istio/issues/24148
直接原因: 这种情况应该通常是清理了已退出的 istio-init 容器,导致 k8s 检测到 pod 关联的容器不在了,然后会重新拉起被删除的容器,而 istio-init 的执行不可重入,因为之前已创建了 iptables 规则,导致后拉起的 istio-init 执行 iptables 失败而 crash。
根因与解决方案: 清理的动作通常是执行了 docker container rm
或 docker container prune
或 docker system prune
。 一般是 crontab 定时脚本里定时清理了容器导致,需要停止清理。
状态码: 431 Request Header Fields Too Large
问题描述
istio 中 http 请求,envoy 返回 431 异常状态码:
HTTP/1.1 431 Request Header Fields Too Large
原因分析
此状态码说明 http 请求 header 大小超限了,默认限制为 60 KiB,由 HttpConnectionManager
配置的 max_request_headers_kb
字段决定,最大可调整到 96 KiB:
解决方案
可以通过 EnvoyFilter 调整 max_request_headers_kb
字段来提升 header 大小限制。
EnvoyFilter 示例 (istio 1.6 验证通过):
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: max-header
namespace: istio-system
spec:
configPatches:
- applyTo: NETWORK_FILTER
match:
context: ANY
listener:
filterChain:
filter:
name: "envoy.http_connection_manager"
patch:
operation: MERGE
value:
typed_config:
"@type": "type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager"
max_request_headers_kb: 96
高版本兼容上面的 v2 配置,但建议用 v3 的 配置 (istio 1.8 验证通过):
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: max-header
namespace: istio-system
spec:
configPatches:
- applyTo: NETWORK_FILTER
match:
context: ANY
listener:
filterChain:
filter:
name: "envoy.http_connection_manager"
patch:
operation: MERGE
value:
typed_config:
"@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager"
max_request_headers_kb: 96
若 header 大小超过 96 KiB,这种情况本身也很不正常,建议将这部分数据放到 body。
状态码: 426 Upgrade Required
背景
Istio 使用 Envoy 作为数据面转发 HTTP 请求,而 Envoy 默认要求使用 HTTP/1.1 或 HTTP/2,当客户端使用 HTTP/1.0 时就会返回 426 Upgrade Required
。
常见的 nginx 场景
如果用 nginx 进行 proxy_pass
反向代理,默认会用 HTTP/1.0,你可以显示指定 proxy_http_version 为 1.1
:
upstream http_backend {
server 127.0.0.1:8080;
keepalive 16;
}
server {
...
location /http/ {
proxy_pass http://http_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
...
}
}
压测场景
ab 压测时会发送 HTTP/1.0 的请求,Envoy 固定返回 426 Upgrade Required,根本不会进行转发,所以压测的结果也不会准确。可以换成其它压测工具,如 wrk 。
让 istio 支持 HTTP/1.0
有些 SDK 或框架可能会使用 HTTP/1.0 协议,比如使用 HTTP/1.0 去资源中心/配置中心 拉取配置信息,在不想改动代码的情况下让服务跑在 istio 上,也可以修改 istiod 配置,加上 PILOT_HTTP10: 1
的环境变量来启用 HTTP/1.0。
参考资料
状态码: 404 Not Found
访问 StatefulSet Pod IP 返回 404
- 问题描述:在 istio 中业务容器访问同集群一 Pod IP 返回 404,在 istio-proxy 中访问却正常
- 原因: Pod 属于 StatefulSet,使用 headless svc,在 istio 中对 headless svc 的支持跟普通 svc 不太一样,如果 pod 用的普通 svc,对应的 listener 有兜底的 passthrough,即转发到报文对应的真实目的IP+Port,但 headless svc 的就没有,我们理解是因为 headless svc 没有 vip,它的路由是确定的,只指向后端固定的 pod,如果路由匹配不上就肯定出了问题,如果也用 passthrough 兜底路由,只会掩盖问题,所以就没有为 headless svc 创建 passthrough 兜底路由。同样的业务,上了 istio 才会有这个问题,也算是 istio 的设计或实现问题。
- 示例场景: 使用了自己的服务发现,业务直接使用 Pod IP 调用 StatefulSet 的 Pod IP
- 解决方案: 同集群访问 statefulset pod ip 带上 host,以匹配上 headless svc 路由,避免匹配不到就 4
排障案例
本节分享一些排障案例。
使用 apollo 的 java 应用启动报 404
问题描述
项目中使用了 apollo 插件,在非 istio 环境正常运行,但部署到 istio 后启动报类似如下错误:
Sync config from upstream repository class com.ctrip.framework.apollo.internals.RemoteConfigRepository failed, reason: Load Apollo Config failed - xxx, url: http://10.5.16.49:8080/configs/agent-center/see-test-02/test.common?ip=10.5.16.46 [Cause: [status code: 404] Could not find config for xxx please check whether the configs are released in Apollo!]
表示请求 apollo 的 config service 返回 404 了。
排查 accesslog
查看 envoy 的 accesslog:
response_flags
为NR
,表示找不到路由 (参考 envoy 官网解释: No route configured for a given request in addition to 404 response code, or no matching filter chain for a downstream connection)。- 请求使用的
Host
直接用的PodIP:Port
。 - 该
PodIP:Port
属于 apollo 服务的 headless service 一个 endpoint (apollo 通过 statefulset 部署)。
headless service 的 xDS 规则
进一步分析之前,我们先了解下 istio 对 headless service 的 xDS 支持:
- 下发的
LDS
规则中会监听 headless service 所有可能的PortIP:Port
,请求 headless service 的 Pod 时,这里先匹配上。 - 然后走到
RDS
规则进行路由,路由时会匹配 hosts,hosts 列表中列举了所有可能的 service 地址 (没有 Pod IP),如果都匹配不到就会返回 404。
问题原因
由于请求 apollo 的 config service 时,Host
没有使用 service 地址,而是直接使用了 PodIP:Port
,所以 RDS
匹配时找不到相应 hosts,就会返回 404。
为什么没有使用 service 地址 ?
为了实现高可用,apollo 的 java 客户端默认是从 meta server 中获取 config service 的 ip 地址 (服务发现),然后直接对该地址发起请求 (不使用 k8s service),从而导致请求 config service 时没有将其 k8s service 地址作为 Host
,最后 hosts 匹配不到返回 404。
如果解决 ?
在 istio 场景下 (kubernetes 之上),请求 config service 就不需要不走 apollo meta server 获取 config service 的 ip 来实现高可用,直接用 kubernetes 的 service 做服务发现就行。幸运的是,apollo 也支持跳过 meta server 服务发现,这样访问 config service 时就可以直接请求 k8s service 了,也就可以解决此问题。
具体配置方法参考 Apollo Java 客户端使用指南 。
无法访问不带 sidecar 的 Pod
问题现象
不能从一个带 sidecar proxy 的 pod 访问到 Redis 服务。
问题分析
Redis是一个 Headless 服务,而 istio 1.6 之前的版本对 Headless 服务的处理有问题,会缺省启用 mTLS。
解决方案
在 1.6 之前可以采用DR规则禁用该服务的 mTLS 来规避:
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: mysql-service
namespace: test
spec:
host: redis-service
trafficPolicy:
loadBalancer:
simple: ROUND_ROBIN
tls:
mode: DISABLE
该问题在 isitio 1.6 中已经修复: https://github.com/istio/istio/pull/24319
注入 sidecar 后 grpc 请求不响应 status code
问题描述
- 环境信息: 多集群下,子集群中 nginx 为 grpc 服务做了一层反向代理。
- 访问链路:grpc client --> nginx --> grpc server。
- 链路说明: grpc client 对 nginx 发起 grpc 调用,nginx 将请求 proxy_pass 到集群内的 grpc server 的 k8s service。
- 现象: 当 nginx 注入了 istio sidecar 后,grpc client 就收不到 grpc server 响应的
grpc-status
的 trailer 了 (即没有 grpc status code)。
原因
测试在 istio 1.6.9 中存在一个 bug,在多集群下,子集群中 envoy sidecar 内部的 http route 中的 domain 中的 vip 采用的是主集群中的 Cluster IP,而不是子集群中该服务对应的 Cluster IP。 如下面 nginx pod 中 envoy proxy 的路由配置:
{
"name": "location-svr.default.svc.cluster.local:50222",
"domains": [
"location-svr.default.svc.cluster.local",
"location-svr.default.svc.cluster.local:50222",
"location-svr",
"location-svr:50222",
"location-svr.default.svc.cluster",
"location-svr.default.svc.cluster:50222",
"location-svr.default.svc",
"location-svr.default.svc:50222",
"location-svr.default",
"location-svr.default:50222",
"172.21.255.24", # 此处的 VIP 是主机群中的 cluster ip,而不是子集群中的 cluster ip
"172.21.255.24:50222"
],
"routes": [
...
- grpc client 发起请求时带上的 host 是 nginx 的域名地址,被 nginx proxy_pass 时带上了,但 envoy 是匹配不到这个原始 host 的,就尝试去匹配报文目的 IP (即 cluster ip)。
- 但因为上述 bug,cluster ip 也匹配不到 (istio 1.6.9 子集群中 http route 只有主集群同名 service 的 cluster ip,这里目的 IP 可能只能是子集群自己的 cluster ip),就只有 paasthrough 了。
- 由于 passthrough cluster 并不确认后端的 upstream 支持 http2,因此未设置
http2_protocol_options
选项。 - 该 grpc/http2 请求被 enovy 采用 http1.1 发送到 location-svr,导致未能收到 trailer (grpc status code)。
解决方案
有以下三种解决方案:
- 经验证 1.8 中该 bug 已经处理,升级到 1.8 可以解决该问题。
- nginx proxy_pass 显示指定 proxy_set_header,使用 service 名称作为 Host。
- grpc client 请求时显示设置 Host Header 为 grpc server 的 service 名称。
Pod 启动卡住: MountVolume.SetUp failed for volume "istio-token"
现象
Istio 相关的 Pod (包括注入了 sidecar 的 Pod) 一直卡在 ContainerCreating,起不来,describe pod 报错 MountVolume.SetUp failed for volume "istio-token" : failed to fetch token: the server could not find the requested resource
:
分析
根据官方文档(Configure third party service account tokens) 的描述可以得知:
- istio-proxy 需要使用 K8S 的 ServiceAccount token,而 K8S 支持
third party
和first party
两种 token。 third party token
安全性更高,istio 默认使用这种类型。- 不是所有集群都支持这种 token,取决于 K8S 版本和 apiserver 配置。
如果集群不支持 third party token
,就会导致 ServiceAccount token 不自动创建出来,从而出现上面这种报错。
什么是 third party token ?
其实就是 ServiceAccountTokenVolumeProjection 这个特性,在 1.12 beta,1.20 GA。
推出该特性是为了增强 ServiceAccount token 的安全性,可以设置有效期(会自动轮转),避免 token 泄露带来的安全风险,还可以控制 token 的受众。
该特性在 istio 中用来配合 SDS 以增强安全性,参考 Istio私钥管理利器SDS浅析。
如何判断集群是否启用了该特性呢?可通过一下命令查询:
kubectl get --raw /api/v1 | jq '.resources[] | select(.name | index("serviceaccounts/token"))'
若返回空,说明不支持;若返回如下 json,说明支持:
{
"name": "serviceaccounts/token",
"singularName": "",
"namespaced": true,
"group": "authentication.k8s.io",
"version": "v1",
"kind": "TokenRequest",
"verbs": [
"create"
]
}
解决方案
方案一:安装 istio 时不使用 third party token
官方称使用 istioctl 安装会自动检测集群是否支持 third party token
,但据 issue 反馈可能有 bug,还是建议强制指定用 first party token
,用参数 --set values.global.jwtPolicy=first-party-jwt
来显示指定,示例:
istioctl manifest generate --set profile=demo --set values.global.jwtPolicy=first-party-jwtm > istio.yaml
方案二:集群启用 ServiceAccountTokenVolumeProjection
如何启用 ServiceAccountTokenVolumeProjection 这个特性呢?需要给 apiserver 配置类似如下的参数:
--service-account-key-file=/etc/kubernetes/pki/sa.key # 这个一般都会配,重要的是下面三个参数
--service-account-issuer=kubernetes.default.svc
--service-account-signing-key-file=/etc/kubernetes/pki/sa.key # 注意替换实际路径
--api-audiences=kubernetes.default.svc
trafficPolicy 不生效
问题描述
为服务配置了 DestinationRule 和 VirtualService,且 VirtualService 绑好了 Gateway,DestinationRule 配置了 trafficPolicy,指定了熔断策略,但使用 ab 压测发现没有触发熔断 (ingressgateway 的 access_log 中 response_flags 没有 "UO")。
原因分析
ab 压测时发送的 HTTP/1.0 请求,而 envoy 需要至少 HTTP/1.1,固定返回 426 Upgrade Required,根本不会进行转发,所以也就不会返回 503,response_flags 也不会有。
解决方案
压测工具换用 wrk,默认发送 HTTP/1.1 的请求,可以正常触发熔断。
使用 istio 保留端口导致 pod 启动失败
问题现象
所有新启动的 Pod 无法 ready,sidecar 报错:
warning envoy config gRPC config for type.googleapis.com/envoy.config.listener.v3.Listener rejected: Error adding/updating listener(s) 0.0.0.0_15090: error adding listener: '0.0.0.0_15090' has duplicate address '0.0.0.0:15090' as existing listener
同时 istiod 也报错:
ADS:LDS: ACK ERROR sidecar~172.18.0.185~reviews-v1-7d46f9dd-w5k8q.istio-test~istio-test.svc.cluster.local-20847 Internal:Error adding/updating listener(s) 0.0.0.0_15090: error adding listener: '0.0.0.0_15090' has duplicate address '0.0.0.0:15090' as existing listener
猜想
看报错应该是 sidecar 启动时获取 LDS 规则,istiod 发现 0.0.0.0:15090
这个监听重复了,属于异常现象,下发 xDS 规则就会失败,导致 sidecar 一直无法 ready。
分析 config_dump
随便找一个还未重启的正常 Pod,看一下 envoy config_dump:
kubectl exec debug-68b799694-n9q66 -c istio-proxy -- curl localhost:15000/config_dump
分析 json 发现 static 配置中有监听 0.0.0.0:15090
:
定位原因
猜测是 dynamic 配置中也有 0.0.0.0:15090
的监听导致的冲突,而 dynamic 中监听来源通常是 Kubernetes 的服务发现(Service, ServiceEntry),检查一下是否有 Service 监听 15090:
kubectl get service --all-namespaces -o yaml | grep 15090
最终发现确实有 Service 用到了 15090 端口,更改成其它端口即可恢复。
深入挖掘
搜索一下,可以发现 15090 端口是 istio 用于暴露 envoy prometheus 指标的端口,是 envoy 使用的端口之一:
参考 Ports used by Istio 。
但并不是所有 envoy 使用的端口都被加入到 static 配置中的监听,只有 15090 和 15021 这两个端口在 static 配置中有监听,也验证了 Service 使用 15021 端口也会有相同的问题。
Service 使用其它 envoy 的端口不会造成 sidecar 不 ready 的问题,但至少要保证业务程序也不能去监听这些端口,因为会跟 envoy 冲突,istio 官网也说明了这一点: To avoid port conflicts with sidecars, applications should not use any of the ports used by Envoy
。
使用建议
根据上面分析,得出以下使用建议:
- Service/ServiceEntry 不能定义 15090 和 15021 端口,不然会导致 Pod 无法启动成功。
- 业务进程不能监听 envoy 使用到的所有端口: 15000, 15001, 15006, 15008, 15020, 15021, 15090 。
最新进展
当前阶段
当前处于试验阶段,预计2022年底或2023年初 beta。
代码分支
当前 ambient 模式的代码还没有合入主干,在 experimental-ambient 分支。这个 issue 在跟进合入 master 之前需要完成的重要事项。
已知问题
环境适配问题
当前还处于早期阶段,很多环境都不支持,比如 mac m1 电脑上使用 kind 创建的集群、使用了网桥的网络模式的集群、某些使用策略路由实现的容器网络等等。
ztunnel 问题
当前 ztunnel 使用 envoy 实现,存在一些列问题,社区也在考虑替代方案,改进或者用 Rust 写一个,详见 这个 issue。
其它问题
1 更多 ambient 相关 issue 看 这里。
参考资料
编译与测试
基于最新代码编译并 push 镜像
下载最新代码:
git clone http://github.com/istio/istio.git
切换分支:
cd istio
git checkout -b experimental-ambient origin/experimental-ambient
编译所有镜像:
export HUB="registry.imroc.cc/istio"
export TAG="ambient"
make docker
查看镜像列表:
$ docker images | grep ambient
registry.imroc.cc/istio/install-cni ambient d3d8fa9fff24 2 days ago 307MB
registry.imroc.cc/istio/proxyv2 ambient 94ac94a14ed6 2 days ago 277MB
registry.imroc.cc/istio/istioctl ambient 76fea2b66ed7 2 days ago 190MB
registry.imroc.cc/istio/operator ambient 574faf14c66b 2 days ago 191MB
registry.imroc.cc/istio/app ambient 7c648c702595 2 days ago 188MB
registry.imroc.cc/istio/pilot ambient d914093f7809 2 days ago 189MB
registry.imroc.cc/istio/ext-authz ambient 88dc93477b75 2 days ago 112MB
最后再使用 docker push 上传镜像。
实际上测试 ambient 是需要其中几个,可以用下面命令只编译需要的镜像:
make docker.pilot
make docker.install-cni
make docker.proxyv2
make docker.istioctl
从镜像拷贝出 istioctl 二进制
下面介绍将 istioctl 二进制拷贝出来的方法,首先用 istioctl 镜像运行一个容器:
docker run --rm -it --entrypoint="" --name istioctl registry.imroc.cc/istio/istioctl:ambient bash
再利用 docker cp 将二进制拷贝出来:
docker cp istioctl:/usr/local/bin/istioctl ./istioctl
使用 istioctl 安装 ambient mesh
./istioctl install --set profile=ambient --set hub=registry.imroc.cc/istio --set tag=ambient
深入分析实现原理
出方向流量分析
流量拦截原理
流量路径:
iptables 在 PREROUTING 链上匹配源 IP 是本节点中网格内的 Pod IP 的 TCP 数据包,打上 0x100 的 mark:
-A ztunnel-PREROUTING -p tcp -m set --match-set ztunnel-pods-ips src -j MARK --set-xmark 0x100/0x100
用 ipset 可以看到 ztunnel-pods-ips
正是本节点中网格内的 POD IP 列表:
$ ipset list
Name: ztunnel-pods-ips
Type: hash:ip
Revision: 0
Header: family inet hashsize 1024 maxelem 65536
Size in memory: 440
References: 1
Number of entries: 2
Members:
10.244.1.4
10.244.1.5
在策略路由可以看到,打上 0x100 mark 的数据包,会走 101 号路由表进行路由:
$ ip rule list
101: from all fwmark 0x100/0x100 lookup 101
查看路由,会经过 istioout
网卡,使用 192.168.127.2
这个默认网关进行路由:
$ ip route show table 101
default via 192.168.127.2 dev istioout
10.244.1.3 dev vethf18f80e0 scope link
查看 istioout
网卡,是 geneve
类型的虚拟网卡,remote
是 10.244.1.3
:
$ ip -d a s istioout
5: istioout: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN group default
link/ether 26:04:92:96:1b:67 brd ff:ff:ff:ff:ff:ff promiscuity 0 minmtu 68 maxmtu 65485
geneve id 1001 remote 10.244.1.3 ttl auto dstport 6081 noudpcsum udp6zerocsumrx numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
inet 192.168.127.1/30 brd 192.168.127.3 scope global istioout
valid_lft forever preferred_lft forever
而 10.244.1.3
是本节点上的 ztunnel pod ip:
$ kubectl -n istio-system get pod -o wide | grep 10.244.1.3
ztunnel-27nxh 1/1 Running 0 3d3h 10.244.1.3 ambient-worker <none> <none>
前面提到的默认网关 192.168.127.2
也正是 ztunnel 内的 pistioout
网卡的 IP:
$ kubectl -n istio-system exec -it ztunnel-27nxh -- ip -d a s
4: pistioout: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN group default qlen 1000
link/ether 76:b0:37:d7:16:93 brd ff:ff:ff:ff:ff:ff promiscuity 0 minmtu 68 maxmtu 65485
geneve id 1001 remote 10.244.1.1 ttl auto dstport 6081 noudpcsum udp6zerocsumrx numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
inet 192.168.127.2/30 scope global pistioout
valid_lft forever preferred_lft forever
inet6 fe80::74b0:37ff:fed7:1693/64 scope link
valid_lft forever preferred_lft forever
也就是说,节点内的 istioout
和 ztunnel pod 内的 pistioout
网卡通过 geneve tunnel 打通了,istioout
收到数据包后立即会转到 ztunnel 内的 pistioout
网卡。
所以串起来就是,从本节点网格内的 Pod 中发出的 TCP 流量,会被策略路由转发到 ztunnel pod 内的 pistioout
网卡。
而在 ztunnel 内的 pistioout
网卡收到流量后,会被 iptables 通过 tproxy 方式转发到 ztunnel 的 15001 端口上:
$ kubectl -n istio-system exec -it ztunnel-27nxh -- iptables-save | grep pistioout
-A PREROUTING -i pistioout -p tcp -j TPROXY --on-port 15001 --on-ip 127.0.0.1 --tproxy-mark 0x400/0xfff
ztunnel 接收与转发流量实现原理
ztunnel 使用 envoy 实现,导出 xds:
kubectl -n istio-system exec -it ztunnel-gm66l -- curl '127.0.0.1:15000/config_dump?include_eds' > dump.json
下面分析下处理拦截到的出方向流量的 xds 配置规则。
LDS 中监听 15001 端口,用于处理 tproxy 方式拦截到的用户 Pod 出方向流量("trasparent": true
指示 envoy 监听时使用 IP_TRANSPARENT
socket 选项以便让目的 IP 不是 ztunnel 网卡 IP 的数据包也能让 ztunnel 收到,实现 tproxy 透明代理) ,非 tproxy 拦截的流量就直接丢弃:
15001 端口监听有很多 filter_chains
,具体用哪个 filter,由 filter_chain_matcher
来匹配:
SourceIPInput
匹配源 IP,看来自节点上哪个 Pod IP。DestinationIPInput
匹配目标 IP。- 匹配到某个 Service 的 ClusterIP,继续用
DestinationPortInput
匹配 Service 端口。 - 匹配完成后,action 指示要使用的
filter
名称。
找到对应的 filter,指示转发给指定的 Cluster:
在 CDS 中找到对应的 Cluster:
在 EDS 中找 Cluster 对应的 endpoint:
这里应该是 envoy dump xds 的 bug,EDS 中没展示
cluster_name
这个必选字段,可能是由于 HBONE 比较新,dump 的逻辑还不完善。
- endpoint 的 address 是
envoy_internal_address
(internal_listener)。 filter_metadata
中的tunnel.destination
是关键,表示给这个 endpoint 带上了要访问的实际目标IP+目标端口,后面会将其传给 HBONE 隧道。
在 LDS 中找到对应的 server_listener_name
:
- 该 listener 有
tunneling_config
,即使用HBONE
隧道方式,其中hostname
为应用需要访问的实际目标IP和目标端口,引用 endpoint 中的 metadatatunnel.destination
。 - cluster 指定转发到哪个 Cluster。
再去 CDS 中找到对应的 Cluster:
- 目标地址已经被前面的 EDS 修改成了 Service 对应的 POD IP 和 POD 端口了,CDS 的 type 为
ORIGINAL_DST
,upstream_port_override
强制将目的端口改为 15008,表示使用POD_IP:15008
这个地址作为报文的目标地址,也就是 HBONE 隧道上游 server 端地址。 tls_certificates_sds_secret_configs
中指定连接上游要使用的证书,指定为目标 Pod 所使用的 service account 对应的证书,这也是在 L4 实现零信任网络的关键。
入方向流量分析
流量拦截原理
流量路径:
由前面出方向流量路径的分析可以得知,ztuunel 最终转发出来的目的IP和目的端口分别是目标 POD IP和固定的 15008 端口。
看下策略路由:
$ ip rule list
103: from all lookup 100
32766: from all lookup main
有个 100 号的路由表,优先级比默认的 main 表高。看下路由规则:
$ ip route show table 100
10.244.1.3 dev vethf18f80e0 scope link
10.244.1.4 via 192.168.126.2 dev istioin src 10.244.1.1
10.244.1.5 via 192.168.126.2 dev istioin src 10.244.1.1
可以看出会给本机上所有的网格内的 POD IP 都加一条路由规则,让到网格内 POD 的流量都走 istioin
这个网卡进行路由,网关是 192.168.126.2
。
$ ip -d a s istioin
4: istioin: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN group default
link/ether 3a:e0:ed:06:15:8c brd ff:ff:ff:ff:ff:ff promiscuity 0 minmtu 68 maxmtu 65485
geneve id 1000 remote 10.244.1.3 ttl auto dstport 6081 noudpcsum udp6zerocsumrx numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
inet 192.168.126.1/30 brd 192.168.126.3 scope global istioin
valid_lft forever preferred_lft forever
与前面出向流量走的 istioout
网卡类似,入向流量走的 istioin
网卡也是 geneve tunnel 设备,remote 是 10.244.1.3
,即 ztunnel 的 POD IP,而网关 192.168.126.2
也正是 ztunnel 内的 pistioin
网卡:
$ kubectl -n istio-system exec -it ztunnel-27nxh -- ip -d a s pistioin
3: pistioin: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN group default qlen 1000
link/ether 7e:bb:b7:60:f3:f6 brd ff:ff:ff:ff:ff:ff promiscuity 0 minmtu 68 maxmtu 65485
geneve id 1000 remote 10.244.1.1 ttl auto dstport 6081 noudpcsum udp6zerocsumrx numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
inet 192.168.126.2/30 scope global pistioin
valid_lft forever preferred_lft forever
inet6 fe80::7cbb:b7ff:fe60:f3f6/64 scope link
valid_lft forever preferred_lft forever
也就是说,发送到节点上网格内的 POD 的流量,会被自动转到 ztunnel 内的 pistioin
网卡。
看 ztunnel 内的 iptables 规则,会将 pistioin
上目的端口为 15008 的 TCP 流量,通过 tproxy 方式拦截并转发给 ztunnel 监听的 15008 端口。
$ kubectl -n istio-system exec -it ztunnel-27nxh -- iptables-save | grep pistioin
-A PREROUTING -i pistioin -p tcp -m tcp --dport 15008 -j TPROXY --on-port 15008 --on-ip 127.0.0.1 --tproxy-mark 0x400/0xfff
ztunnel 接收与转发流量实现原理
查看 LDS 中 15008 端口的 Listener:
filter_chains
中有很多 filter,每个 filter 对应匹配本节点上,网格内的 POD IP。- 在 filter 的 route 中,将数据包路由到
virtual_inbound
这个 Cluster。
在 CDS 中找到 virtual_inbound
这个 Cluster:
use_http_header
表示将 downstream ztunnel 通过 HBONE CONNECT 传来的最原始的目标 POD IP 和目标端口替换成当前报文的目标IP和目标端口 (主要是为了替换 15008 端口为原始目标端口,目标 IP 本身都是目标 POD IP)。
最后,报文将只修改目的端口,其余均保持不变,再次被 envoy 转发出去,通过 veth pair 到达节点 root ns 的 vethxxx 网卡,由于目的端口不再是
项目结构解析
顶层目录
├── cni
│ ├── cmd
│ ├── pkg
├── istioctl
│ ├── cmd
│ └── pkg
├── operator
│ ├── cmd
│ ├── pkg
├── pilot
│ ├── cmd
│ ├── pkg
├── pkg
cni
,istioctl
,operator
,pilot
目录分别包含同名相应模块的代码。下面的cmd
是模块下相应二进制的编译入口,cmd
下面的pkg
是cmd
中的代码需要调用的依赖逻辑。- 多个模块共同依赖的一些逻辑会放到外层的
pkg
目录下。
梳理模块与二进制
cni
模块主要包含 istio-cni
和 install-cni
两个二进制,负责 cni 插件相关逻辑:
cni
├── cmd
│ ├── install-cni
│ ├── istio-cni
istioctl
和 operator
模块都主要是一个二进制,分别用于 cli 工具和 istio 安装。
pilot
是最核心的模块,有 pilot-agent
和 pilot-discovery
两个二进制:
pilot
├── cmd
│ ├── pilot-agent
│ └── pilot-discovery
pilot-discovery
就是 "istiod",即 istio 控制面。pilot-agent
是连接 istiod (控制面) 和 envoy (数据面) 之间的纽带,主要负责拉起和管理数据面进程。
CNI 模块源码解析
install-cni
主要逻辑梳理
install-cni
用于 daemonset 部署到每个节点,主要逻辑如下:
- 为节点安装 istio-cni 插件。
- 检测 cni 配置,如果检测到被修改,立即覆盖回来。
- 如果是 ambient 模式,会起一个 controller 来 watch pod,当本节点 ztunnel pod 起来时,会自动在节点上创建 ambient 模式中节点上所需的 iptables 规则、虚拟网卡以及策略路由。
- 退出时清理 istio-cni 插件。
main 函数入口在 cni/cmd/install-cni/main.go
,cmd.GetCommand()
获取 cobra 的 Command,也就是二进制运行入口:
rootCmd := cmd.GetCommand()
if err := rootCmd.ExecuteContext(ctx); err != nil {
os.Exit(1)
}
rootCmd.ExecuteContext(ctx)
会调用 cobra Command 的 RunE
函数 (cni/pkg/cmd/root.go
):
var rootCmd = &cobra.Command{
RunE: func(c *cobra.Command, args []string) (err error) {
...
继续看看 RunE
里面的逻辑,constructConfig()
会从启动参数和环境变量中读取参数,进行合并,构造出 install-cni
的配置信息 Config 对象:
if cfg, err = constructConfig(); err != nil {
return
}
根据配置的端口,将指标监控信息暴露到 /metrics
接口:
// Start metrics server
monitoring.SetupMonitoring(cfg.InstallConfig.MonitoringPort, "/metrics", ctx.Done())
监听 Unix Domain Socket,暴露 HTTP 接口,用于接收来自 istio-cni
产生的日志,收到日志后就会在 install-cni
这里打印出来:
// Start UDS log server
udsLogger := udsLog.NewUDSLogger()
if err = udsLogger.StartUDSLogServer(cfg.InstallConfig.LogUDSAddress, ctx.Done()); err != nil {
log.Errorf("Failed to start up UDS Log Server: %v", err)
return
}
因为
istio-cni
自身不是常驻进程,被 kubelet 调用后立即退出,所以需要将日志投递给install-cni
统一打印和记录,便于排查问题。
如果启用了 ambient 模式,会启动一个 ambient 模式所需要的常驻运行的 server:
if cfg.InstallConfig.AmbientEnabled {
// Start ambient controller
server, err := ambient.NewServer(ctx, ambient.AmbientArgs{
SystemNamespace: ambient.PodNamespace,
Revision: ambient.Revision,
})
if err != nil {
return fmt.Errorf("failed to create ambient informer service: %v", err)
}
server.Start()
}
接下来是最核心最关键的逻辑,根据安装配置将 istio-cni
插件安装到节点上,同时 watch 文件变化,如果被修改就自动覆盖回来:
installer := install.NewInstaller(&cfg.InstallConfig, isReady)
repair.StartRepair(ctx, &cfg.RepairConfig)
if err = installer.Run(ctx); err != nil {
...
安装 CNI 插件的逻辑梳理
install.Run(ctx)
是安装 CNI 逻辑的入口,内部 in.install(ctx)
是每次安装 CNI 插件时执行的逻辑:
if in.cfg.CNIEnableInstall {
if err = in.install(ctx); err != nil {
return
}
...
在 install
中,先将 istio CNI 插件的二进制拷贝到节点的 CNI 二进制目录 (/opt/cni/bin
):
if err = copyBinaries(
in.cfg.CNIBinSourceDir, in.cfg.CNIBinTargetDirs,
in.cfg.UpdateCNIBinaries, in.cfg.SkipCNIBinaries); err != nil {
cniInstalls.With(resultLabel.Value(resultCopyBinariesFailure)).Increment()
return
}
然后创建 CNI 二进制运行起来需要的 kubeconfig 文件:
if in.kubeconfigFilepath, err = createKubeconfigFile(in.cfg, in.saToken); err != nil {
cniInstalls.With(resultLabel.Value(resultCreateKubeConfigFailure)).Increment()
return
}
最后是创建并覆盖 CNI 插件配置文件:
if in.cniConfigFilepath, err = createCNIConfigFile(ctx, in.cfg, in.saToken); err != nil {
cniInstalls.With(resultLabel.Value(resultCreateCNIConfigFailure)).Increment()
return
}
istio-cni
main 函数入口在 cni/cmd/istio-cni/main.go
:
func main() {
...
skel.PluginMain(plugin.CmdAdd, plugin.CmdCheck, plugin.CmdDelete, version.All,
fmt.Sprintf("CNI plugin istio-cni %v", istioversion.Info.Version))
}
关键逻辑是调用 CNI 项目中的 skel.PluginMain
这个函数来注册 CNI 插件处理函数。
最重要的是 plugin.CmdAdd
,即每次 kubelet 创建 Pod 时,调用 istio-cni
插件来设置容器网络的时候,就会走到 CmdAdd
这个函数。
首先会解析 kubelet 调用 istio-cni
时通过标准输入传来的配置,以及 install-cni
里面的 ambient server 写入的 ambient 配置文件。
// CmdAdd is called for ADD requests
func CmdAdd(args *skel.CmdArgs) (err error) {
...
conf, err := parseConfig(args.StdinData)
if err != nil {
log.Errorf("istio-cni cmdAdd failed to parse config %v %v", string(args.StdinData), err)
return err
}
...
ambientConf, err := ambient.ReadAmbientConfig()
if err != nil {
log.Errorf("istio-cni cmdAdd failed to read ambient config %v", err)
return err
}
...
主题逻辑梳理
istio-cni
是被 install-cni
安装到节点的 CNI 插件二进制,在 kubelet 每次创建 pod 时会调用的二进制,主要功能如下:
- 当要创建的 Pod 是网格内的 Pod 时,在 Pod 所在 netns 创建相关 iptables 规则以实现流量拦截(取代
istio-init
容器)。 - 如果是网格内的 Pod,且数据面使用的 ambient 模式的 ztunnel 而不是 sidecar,自动更新 ambient 模式在节点上所需的 ipset, 路由表等。
已知 BUG
envoy 内存不释放
- 已知受影响的版本: istio 1.5.4
- 已解决版本: istio 1.6.0
- issue: #25145
高频使用链接
istio 相关
Envoy 相关
实用脚本
istioctl
查看 sidecar 证书是否正常
$ istioctl proxy-config secret accountdeletecgi-5b9d6b586-wzb7b
RESOURCE NAME TYPE STATUS VALID CERT SERIAL NUMBER NOT AFTER NOT BEFORE
default Cert Chain ACTIVE true 198001071566761875257861959297039696827 2021-04-16T03:33:03Z 2021-04-15T03:33:03Z
ROOTCA CA ACTIVE true 205820131934050053680230040513977871884 2031-03-24T02:58:23Z 2021-03-26T02:58:23Z
查看 sidecar 证书详情
$ istioctl -n istio-test proxy-config secret productpage-v1-578c57988-j8g9d -o json | jq '[.dynamicActiveSecrets[] | select(.name == "default")][0].secret.tlsCertificate.certificateChain.inlineBytes' -r | base64 -d | openssl x509 -noout -text
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
1b:6c:19:da:04:db:cb:1b:29:48:f8:09:60:35:22:75
Signature Algorithm: sha256WithRSAEncryption
Issuer: L=cls-5u6csmhf, O=Istio, CN=Intermediate CA
Validity
Not Before: Apr 15 03:57:51 2021 GMT
Not After : Apr 16 03:57:51 2021 GMT
Subject:
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:dd:4f:bb:65:fd:d2:9c:7d:29:00:a9:6b:8c:b2:
8b:12:17:5f:6f:1b:d6:db:a2:7a:69:23:21:6a:d1:
38:4e:44:d0:c9:f4:6d:13:e9:97:86:54:f2:30:e6:
fe:9e:41:7c:95:a7:20:ff:bb:de:62:8e:49:58:90:
7a:38:be:15:f1:96:6e:ff:7a:c4:61:d8:a8:25:f1:
92:ee:33:ae:86:bb:63:38:2c:e7:32:a5:11:be:79:
3e:83:67:17:4e:91:df:0a:3e:52:11:60:9a:83:5d:
e4:92:9a:f6:29:43:7e:60:13:03:4d:ed:fc:d1:5c:
e9:5b:a9:a6:ef:b8:f5:82:78:a1:ef:15:43:17:40:
b3:48:c2:27:33:ac:0e:aa:00:c9:da:3f:ee:5d:1a:
d7:7a:4f:e3:e0:26:e8:67:1a:c1:44:c5:f3:d0:1c:
e1:e4:53:a5:a8:0b:04:47:cd:df:d2:a9:1b:47:8f:
3e:dc:9a:b6:b3:a8:6d:47:da:4d:68:dd:4f:82:3f:
aa:25:6d:8e:c5:8c:9d:1e:7c:93:4c:55:a3:59:d7:
a6:42:04:05:52:01:6d:a1:c8:8f:67:48:b4:16:4b:
46:6e:1e:5b:97:65:99:fe:5f:f7:f2:ba:ea:3f:34:
28:f1:e6:18:4d:d9:de:00:f2:fd:4a:9c:f9:a5:e2:
9d:5b
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
TLS Web Server Authentication, TLS Web Client Authentication
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Authority Key Identifier:
keyid:A0:62:8E:B4:53:64:D9:1D:DC:21:41:D8:05:93:E4:6D:27:82:20:4E
X509v3 Subject Alternative Name: critical
URI:spiffe://cluster.local/ns/istio-test/sa/bookinfo-productpage
Signature Algorithm: sha256WithRSAEncryption
ab:0d:b5:e6:df:50:02:d2:85:47:62:18:b0:af:89:cc:3a:06:
a1:19:a8:2c:58:9c:e4:1d:34:3b:f8:a2:a7:f6:f8:0e:af:a8:
1b:35:79:9d:72:a2:a9:96:14:37:c8:76:e2:50:ae:d4:c6:33:
43:a5:0e:e4:c9:95:a8:81:9a:6a:72:e5:eb:3c:55:20:70:a4:
27:3c:6d:88:da:03:75:3a:99:d0:72:c2:b3:2e:66:9e:00:9a:
13:c5:61:20:fc:35:99:30:93:33:e6:8a:2d:b4:b0:0f:23:3a:
a1:3d:4f:01:bf:cc:2b:38:2a:41:23:13:31:52:84:d7:8d:cb:
71:63:28:e6:1f:1f:95:20:41:63:1a:a6:5f:a5:d0:3b:35:97:
4b:8d:6c:55:59:34:e2:36:ff:a0:38:4c:f0:1f:a3:16:bf:bc:
75:53:35:20:60:b2:0d:4d:bd:d1:ab:a6:28:60:e4:d7:0c:e3:
cc:19:cb:d1:4c:e7:3d:fc:21:aa:eb:e6:f4:a6:0f:ed:cd:da:
db:ae:4c:fa:cf:55:f8:ea:d1:55:d5:6c:51:95:3f:47:13:b7:
20:e2:5d:cc:b0:ea:8d:99:e1:9f:40:df:d3:97:af:a5:69:f4:
c6:b7:9c:c4:55:67:47:59:2b:53:40:f2:48:88:9b:75:77:00:
22:98:f7:61:74:05:8c:8b:e4:1f:be:c8:e9:7a:8f:9a:5d:ff:
1d:48:0a:e9:75:da:1e:35:93:a4:a0:c0:f8:78:bc:25:a2:63:
d3:35:83:1f:15:28:a7:31:de:5a:d8:ae:56:f8:8c:ea:da:13:
01:81:aa:6f:0f:a5:39:78:e6:b6:e3:1c:ff:7c:03:50:22:04:
64:0a:dc:14:2c:ed:7d:ec:91:73:dc:44:3e:60:bc:d8:69:c3:
7c:5b:d5:16:53:1c:24:2e:1b:51:fb:93:31:37:b3:80:e6:f2:
07:46:09:8d:d5:2c:a4:f4:e3:14:b3:d9:d7:de:de:9c:bf:84:
67:66:e1:b9:85:26:1c:8f:5c:8d:9f:5f:53:b7:ed:c7:2b:9d:
57:3f:3c:d6:86:f4:d8:d8:72:c3:4c:be:5e:48:a4:ac:b9:c5:
b1:6c:4b:dc:83:a2:bc:80:c2:34:c3:1a:68:7f:e8:e8:b9:eb:
39:2a:6d:3d:2d:90:e2:9c:52:dc:a2:99:e3:dc:dc:5a:f7:71:
9d:5f:67:93:d6:e3:68:a2:f9:7b:6e:64:a6:0c:09:95:f6:28:
02:e4:3f:63:fc:09:12:f7:8f:ce:4a:c3:38:02:0c:35:64:f1:
74:93:36:93:6d:e2:8e:5b:07:b9:5a:f8:14:32:69:4f:64:8d:
6e:a4:b0:95:73:36:b6:92
- 确保
Subject Alternative Name
包含正确的 spiffe URI,如URI:spiffe://cluster.local/ns/istio-test/sa/bookinfo-productpage
。
实用 YAML
sidecar 注入相关
为指定 workload 取消 sidecar 自动注入
template:
metadata:
annotations:
sidecar.istio.io/inject: "false"
proxy 相关
自定义 request/limit
template:
metadata:
annotations:
"sidecar.istio.io/proxyCPU": "10m"
"sidecar.istio.io/proxyCPULimit": "2"
"sidecar.istio.io/proxyMemory": "32Mi"
"sidecar.istio.io/proxyMemoryLimit": "1Gi"
自定义日志级别
template:
metadata:
annotations:
"sidecar.istio.io/logLevel": debug # 可选: trace, debug, info, warning, error, critical, off
"sidecar.istio.io/componentLogLevel": "ext_authz:trace,filter:debug"
不劫持部分外部地址的流量以提升性能(比如外部数据库)
template:
metadata:
annotations:
traffic.sidecar.istio.io/excludeOutboundIPRanges: "10.10.31.1/32,10.10.31.2/32"
mtls 配置
全局禁用 mtls
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: istio-system
spec:
mtls:
mode: DISABLE
DestinationRule 相关
为某个服务启用地域感知
地域感知行为需要显式指定 outlierDetection
后才会启用:
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: nginx
spec:
host: nginx
trafficPolicy:
outlierDetection:
consecutive5xxErrors: 3
interval: 30s
baseEjectionTime: 30s
常用 EnvoyFilter
从 nginx 切到 ingressgateway
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: http-options
namespace: istio-system
spec:
configPatches:
- applyTo: NETWORK_FILTER
match:
context: ANY
listener:
filterChain:
filter:
name: "envoy.http_connection_manager"
patch:
operation: MERGE
value:
typed_config:
"@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager"
max_request_headers_kb: 96 # 96KB, 请求 header 最大限制
- applyTo: HTTP_FILTER
match:
context: GATEWAY
listener:
filterChain:
filter:
name: "envoy.http_connection_manager"
patch:
operation: INSERT_BEFORE
value:
name: "envoy.filters.http.buffer"
typed_config:
'@type': "type.googleapis.com/envoy.extensions.filters.http.buffer.v3.Buffer"
max_request_bytes: 1048576 # 1MB, 请求最大限制
保留 header 大小写
对所有 HTTP/1.1 的请求和响应都保留 header 大小写:
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: header-casing
namespace: istio-system
spec:
configPatches:
- applyTo: CLUSTER
match:
context: SIDECAR_INBOUND
patch:
operation: MERGE
value:
typed_extension_protocol_options:
envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
'@type': type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
explicit_http_config:
http_protocol_options:
header_key_format:
stateful_formatter:
name: preserve_case
typed_config:
'@type': type.googleapis.com/envoy.extensions.http.header_formatters.preserve_case.v3.PreserveCaseFormatterConfig
- applyTo: NETWORK_FILTER
match:
listener:
filterChain:
filter:
name: envoy.filters.network.http_connection_manager
patch:
operation: MERGE
value:
typed_config:
'@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
http_protocol_options:
header_key_format:
stateful_formatter:
name: preserve_case
typed_config:
'@type': type.googleapis.com/envoy.extensions.http.header_formatters.preserve_case.v3.PreserveCaseFormatterConfig