Istio 常见问题
介绍在使用 Istio 过程中可能遇到的一些常见问题的解决方法
Package 用于组织一组逻辑上紧密相关的 go 文件。是 go 语言中代码重用的基础单元。在文件系统中,一个 Package 对应一个文件夹,文件夹中包含该 Packag 中的多个 go 文件。在 go 语言模型中,一个 Packag 中包含了多个紧密相关的变量,结构体和方法。
Package中包含的内容:
└── package
├── variable
├── function
└── struct
├── variable
└── method
1 - 应用程序启动失败
安装了 sidecar 的应用启动失败。
故障现象
该问题的表现是安装了 Sidecar proxy 的应用在启动后的一小段时间内无法通过网络访问 Pod 外面的服务。应用在启动时通常会从一些外部服务中获取数据,并采用这些数据对自身进行初始化。例如从配置中心读取程序配置,从数据库中初始化程序用户信息等。而安装了 Sidecar proxy 的应用在启动后的一小段时间内网络是不通的。如果应用代码中没有合适的容错和重试逻辑,该问题常常会导致应用启动失败。
故障原因
如下图所示,Envoy 启动后会通过 xDS 协议向 Pilot 请求服务和路由配置信息,Pilot 收到请求后会根据 Envoy 所在的节点(Pod 或者 VM)组装配置信息,包括 Listener、Route、Cluster 等,然后再通过 xDS 协议下发给 Envoy。根据 Mesh 的规模和网络情况,该配置下发过程需要数秒到数十秒的时间。在这段时间内,由于初始化容器已经在 Pod 中创建了 Iptables rule 规则,因此应用向外发送的网络流量会被重定向到 Envoy ,而此时 Envoy 中尚没有对这些网络请求进行处理的监听器和路由规则,无法对此进行处理,导致网络请求失败。(关于 Envoy Sidecar 初始化过程和 Istio 流量管理原理的更多内容,可以参考这篇文章 Istio流量管理实现机制深度解析)。
解决方案
参见:最佳实践-在 Sidecar 初始化完成后再启动应用容器
2 - ExternalName Service 劫持了其他服务流量
故障现象
如果网格内存在一个 ExternalName 类型 Service, 网格内访问其他外部服务的的某一端口,如果这个端口刚好和该 ExternalName Service 重叠,那么流量会被路由到这个 ExternalName Service 对应的 CDS。
故障重现
正常情况
在 namespace sample 安装 sleep Pod:
kubectl create ns sample
kubectl label ns sample istio-injection=enabled
kubectl -nsample apply -f https://raw.githubusercontent.com/istio/istio/1.11.4/samples/sleep/sleep.yaml
通过 sleep 访问外部服务 https://httpbin.org:443, 请求成功:
kubectl -nsample exec sleep-74b7c4c84c-22zkq -- curl -I https://httpbin.org
HTTP/2 200
......
从 access log 确认流量是从 PassthroughCluster 出去,符合预期:
"- - -" 0 - - - "-" 938 5606 1169 - "-" "-" "-" "-" "18.232.227.86:443" PassthroughCluster 172.24.0.10:42434 18.232.227.86:443 172.24.0.10:42432 - -
异常情况
现在 在 default 下创建一个 ExternalName 类型的 Service, 端口也是 443:
kind: Service
apiVersion: v1
metadata:
name: my-externalname
spec:
type: ExternalName
externalName: bing.com
ports:
- port: 443
targetPort: 443
通过 sleep 访问外部服务 https://httpbin.org:443, 请求失败:
kubectl -nsample exec sleep-74b7c4c84c-22zkq -- curl -I https://httpbin.org
curl: (60) SSL: no alternative certificate subject name matches target host name 'httpbin.org'
More details here: https://curl.se/docs/sslcerts.html
......
查看 access log, 发现请求外部服务,被错误路由到了 my-externalname 的 ExternalName Service:
"- - -" 0 - - - "-" 706 5398 67 - "-" "-" "-" "-" "204.79.197.200:443" outbound|443||my-externalname.default.svc.cluster.local 172.24.0.10:56806 34.192.79.103:443 172.24.0.10:36214 httpbin.org -
故障原因
通过对比 sleep Pod 前后两次的 xDS, 发现增加了 ExternalName Service 后,xDS 里会多一个 LDS 0.0.0.0_443
, 该 LDS 包括一个default_filter_chain
会把该 LDS 中其他 filter chain 没有 match 到的流量,都路由到这个 default_filter_chain
中的 Cluster,也就是 my-externalname
对应的 CDS:
解决方案
该问题属于 Istio 实现缺陷,相关 issue: https://github.com/istio/istio/issues/20703
目前的解决方案是避免 ExternalName Service 和其他服务端口冲突。
3 - Gateway TLS hosts 冲突导致配置被拒绝
故障现象
网格中同时存在以下两个 Gateway
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
name: test1
spec:
selector:
istio: ingressgateway
servers:
- hosts:
- test1.example.com
port:
name: https
number: 443
protocol: HTTPS
tls:
credentialName: example-credential
mode: SIMPLE
---
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
name: test2
spec:
selector:
istio: ingressgateway
servers:
- hosts:
- test1.example.com
- test2.example.com
port:
name: https
number: 443
protocol: HTTPS
tls:
credentialName: example-credential
mode: SIMPLE
172.18.0.6 为 ingress gateway Pod IP,请求 https://test1.example.com 正常返回 404
curl -i -HHost:test1.example.com --resolve "test1.example.com:443:172.18.0.6" --cacert example.com.crt "https://test1.example.com"
HTTP/2 404
date: Mon, 29 Nov 2021 06:59:26 GMT
server: istio-envoy
请求 https://test2.example.com 异常
$ curl -HHost:test2.example.com --resolve "test2.example.com:443:172.18.0.6" --cacert example.com.crt "https://test2.example.com"
curl: (35) OpenSSL SSL_connect: Connection reset by peer in connection to test2.example.com:443
故障原因
通过 istiod 监控发现pilot_total_rejected_configs
指标异常,显示default/test2
配置被拒绝
调整 istiod 日志级别查看被拒绝的原因
--log_output_level=model:debug
2021-11-29T07:24:21.703924Z debug model skipping server on gateway default/test2, duplicate host names: [test1.example.com]
通过日志定位到具体代码位置
if duplicateHosts := CheckDuplicates(s.Hosts, tlsHostsByPort[resolvedPort]); len(duplicateHosts) != 0 {
log.Debugf("skipping server on gateway %s, duplicate host names: %v", gatewayName, duplicateHosts)
RecordRejectedConfig(gatewayName)
continue
}
// CheckDuplicates returns all of the hosts provided that are already known
// If there were no duplicates, all hosts are added to the known hosts.
func CheckDuplicates(hosts []string, knownHosts sets.Set) []string {
var duplicates []string
for _, h := range hosts {
if knownHosts.Contains(h) {
duplicates = append(duplicates, h)
}
}
// No duplicates found, so we can mark all of these hosts as known
if len(duplicates) == 0 {
for _, h := range hosts {
knownHosts.Insert(h)
}
}
return duplicates
}
校验逻辑是每个域名在同一端口上只能配置一次 TLS,我们这里 test1.example.com 在 2 个 Gateway 的 443 端口都配置了 TLS,
导致其中一个被拒绝,通过监控确认被拒绝的是 test2,test2.example.com 和 test1.example.com 配置在 test2 的同一个 Server,Server 配置被拒绝导致请求异常
解决方案
同一个域名不要在多个 Gateway 中的同一端口重复配置 TLS,这里我们删除 test1 后请求恢复正常
$ curl -i -HHost:test1.example.com --resolve "test1.example.com:443:172.18.0.6" --cacert example.com.crt "https://test1.example.com"
HTTP/2 404
date: Mon, 29 Nov 2021 07:43:40 GMT
server: istio-envoy
$ curl -i -HHost:test2.example.com --resolve "test2.example.com:443:172.18.0.6" --cacert example.com.crt "https://test2.example.com"
HTTP/2 404
date: Mon, 29 Nov 2021 07:43:41 GMT
server: istio-envoy
4 - Server Speaks First 协议访问失败
故障现象
Istio 网格开启 allow any 访问模式,在一个注入了 sidecar 的 pod 内,mysql 客户端访问 mysql-ip-1:3306 成功,访问 mysql-ip-2:10000 没有响应:
# mysql -h55.135.153.1 -utest -pxxxx -P3306
Welcome to the MariaDB monitor. Commands end with ; or \g.
# mysql -h55.108.108.2 -utest -pxxxx -P10000
(no response)
故障分析
查看日志,把 access log 设置为 debug、trace 均没有发现有用信息。
分析发现,网格内有一个 http server,也使用了和 mysql-ip-2 相同的端口 10000:
apiVersion: v1
kind: Service
metadata:
name: irrelevant-svc
......
spec:
ports:
- name: http
nodePort: 31025
port: 10000 # 端口相同
protocol: TCP
targetPort: 8080
我们尝试把该服务端口改成 10001,访问 mysql-ip-2:10000 成功,推测和端口冲突相关:
# mysql -h55.108.108.2 -utest -pxxxx -P10000
Welcome to the MariaDB monitor. Commands end with ; or \g.
我们再尝试对 mysql-ip-1 复现故障:在网格内创建了一个包括 3306 端口的 http 服务,mysql 请求无响应,问题复现。
另外我们还尝试过,如果把冲突端口的协议定义为 tcp(通过 port name),该问题不存在:
apiVersion: v1
kind: Service
metadata:
name: irrelevant-svc
......
spec:
ports:
- name: tcp # 如果是 tcp 则不会出问题
nodePort: 31025
port: 10000
protocol: TCP
targetPort: 8080
故障原因
Server Speaks First
Mysql 协议是一种 Server Speaks First 协议,也就是说 client 和 server 完成三次握手后,是 server 会先发起会话, 简要过程:
S: 服务端首先会发一个握手包到客户端
C: 客户端向服务端发送认证信息 ( 用户名,密码等 )
S: 服务端收到认证包后,会检查用户名与密码是否合法,并发送包告知客户端认证信息。
除了 Mysql,常见的 Server Speaks First 协议还包括 SMTP,DNS,MongoDB 等。下面是一个 SMTP 交互流程:
S: 220 smtp.example.com ESMTP Postfi
C: HELO relay.example.com
S: 250 smtp.example.com, I am glad to meet you
C: MAIL FROM:<[email protected]>
S: 250 Ok
C: RCPT TO:<[email protected]>
S: 250 Ok
C: RCPT TO:<[email protected]>
S: 250 Ok
C: DATA
S: 354 End data with <CR><LF>.<CR><LF>
C: From: "Bob Example" <[email protected]>
C: To: Alice Example <[email protected]>
C: Cc: [email protected]
C: Date: Tue, 15 Jan 2008 16:02:43 -0500
C: Subject: Test message
C:
C: Hello Alice.
C: This is a test message with 5 header fields and 4 lines in the message body.
C: Your friend,
C: Bob
C: .
S: 250 Ok: queued as 12345
C: QUIT
S: 221 Bye
{The server closes the connection}
istio 不是完全透明
当前 istio 的某些特性,不能做到透明兼容 Server Speaks First 协议,这些特性包括:
- 协议嗅探
- PERMISSIVE mTLS
- Authorization Policy
这些特性都希望 client 能先发起会话,以协议嗅探为例,envoy 是通过分析 client 发出的初始若干字节来推测协议类型。
对于 Server Speaks First 协议,比如 mysql,三次握手后,这时候 mysql client 在等待 mysql server 发起初次会话,而 client 端的 envoy 尝试做协议嗅探,也在等 mysql client 发出数据,这类似一个死锁,最终超时。
解决方案
以下是一些可行的方案:
- 为 Server Speaks First 协议服务创建一个 ServiceEntry,并指定协议为 TCP。
- 避免 Server Speaks First 协议服务端口和网格内服务端口重叠,这样请求可以直接走 passthrough。
- 把 Server Speaks First 服务 ip 放到 excludeIPRanges,这样请求不经过 envoy 处理,适用于 DB 服务不需要网格治理的情况。
参考资料
5 - 长连接未开启 tcp keepalive
故障现象
用户反馈链路偶发 500 错误,频率低但是持续存在。
用户访问链路较长,核心链路简化如下:
1. client ->
2. [istio ingress gateway] ->
3. podA[app->sidecar] ->
4. 腾讯云内网CLB ->
5. [istio ingress gateway] ->
6. podB[sidecar->app]
应用对外是 https 服务,证书在 istio ingress gateway 上处理。
故障分析
通过分析链路中 sidecar accesslog 日志,有以下现象:
- 第 3 跳 podA 正常发出请求,但接收到 500 返回。
- 第 5 跳 istio ingress gateway 没有该 500 对应的访问日志。
因此重点分析 第 3,4,5 跳。
在第 3 跳 podA 上抓到 500 对应的数据包:
抓包显示,podA 向一个已经断开的连接发送数据包,收到 RST 因此返回 500,但抓包并没有发现这个连接之前有主动断开的行为(FIN)。
登录 podA,查看连接情况:
ss 显示用户代码里使用了 tcp 长连接,注意这里我们使用了 ss 参数 -o
, 该参数可以显示 tcp keepalive timer 信息:
-o, --options
Show timer information. For TCP protocol, the output
format is:
timer:(<timer_name>,<expire_time>,<retrans>)
<timer_name>
the name of the timer, there are five kind of timer
names:
on : means one of these timers: TCP retrans timer,
TCP early retrans timer and tail loss probe timer
keepalive: tcp keep alive timer
timewait: timewait stage timer
persist: zero window probe timer
unknown: none of the above timers
<expire_time>
how long time the timer will expire
<retrans>
how many times the retransmission occurred
但从 ss 结果并未看到 timer 信息,推断 podA 使用的长连接并未开启 keepalive。
故障原因
podA 使用了 tcp 长连接,但是没有开启 keepalive,当长连接出现一段时间空闲,该连接可能被网络中间组件释放,比如 client、server 端的母机, 但 client 端还是持有断开连接,后续重用该链接就会导致上述异常。
解决方案
问题本质是因为长连接 idle 过长,且缺乏探活机制,导致 client 没感知到连接已释放,尝试三种方案:
- 应用代码修复
- istio 方案:client sidecar 开启 keepalive
- istio 方案:server 开启 keepalive
应用代码修复
最直接的方案是应用在使用长连接时,开启 tcp keepalive,以 golang 程序示例,我们尝试用长连接访问 https://www.baidu.com
先模拟使用长连接但不开启 keepalive:
var HTTPTransport = &http.Transport{
DialContext: (&net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: -1 * time.Second, // disable TCP KeepAlive
}).DialContext,
MaxIdleConns: 50,
IdleConnTimeout: 60 * time.Second,
MaxIdleConnsPerHost: 20,
}
func main() {
uri := "https://www.baidu.com"
times := 200
client := http.Client{Transport: HTTPTransport}
for i := 0; i < times; i++ {
time.Sleep(2 * time.Second)
req, err := http.NewRequest(http.MethodGet, uri, nil)
if err != nil {
fmt.Println("NewRequest Failed " + err.Error())
continue
}
resp, err := client.Do(req)
if err != nil {
fmt.Println("Http Request Failed " + err.Error())
continue
}
fmt.Println(resp.Status)
ioutil.ReadAll(resp.Body)
resp.Body.Close()
}
注意 KeepAlive: -1
表示禁用了 tcp keepalive 探活,ss 查看:
结果显示长连接缺乏 timer。注意测试 pod 在 istio 环境,上述第一个连接是 go 程序到 envoy,第二个连接是 envoy 到 baidu。
golang 代码修复方案很简单,只需要把 KeepAlive
设置为非负数, 代码修改
var HTTPTransport = &http.Transport{
DialContext: (&net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 120 * time.Second, // keepalive 设置为 2 分钟
}).DialContext,
MaxIdleConns: 50,
IdleConnTimeout: 60 * time.Second,
MaxIdleConnsPerHost: 20,
}
ss 查看连接情况:
ss 显示 go client 到 envoy 开启了 keepalive,问题解决。
但用户应用程序较多,不方便逐一调整 keepalive,希望通过 istio sidecar 来解决上述问题。keepalive 可以在 client、server 任意一端开启,以下是使用 istio 的两种方案:
istio 方案:client sidecar 开启 keepalive
该方案需要client 注入 istio sidecar,仍以访问 baidu https 为例,外部服务在 istio 中默认转发到 PassthroughCluster, 要对指定外部服务流量进行流控,我们需要先给该服务创建一个 service entry:
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
name: baidu-https
spec:
hosts:
- www.baidu.com
location: MESH_EXTERNAL
ports:
- number: 443
name: https
protocol: TLS
然后增加 tcp keepalive 设置:
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: baidu-https
spec:
host: www.baidu.com
trafficPolicy:
connectionPool:
tcp:
maxConnections: 100
tcpKeepalive:
time: 600s
interval: 75s
probes: 9
ss 显示 go client 到 envoy 并没有 keepalive, 但 envoy 到 baidu 开启了 keepalive。
istio 方案:server 开启 keepalive
用户异常链路的 server 入口 是 CLB 后端的 ingress gateway,在 ingress gateway 上开启 keepalive 会稍微复杂一点,需要使用 envoyfilter 来设置 socekt options:
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: ingress-gateway-socket-options
namespace: istio-system
spec:
configPatches:
- applyTo: LISTENER
match:
context: GATEWAY
listener:
name: 0.0.0.0_443
portNumber: 443
patch:
operation: MERGE
value:
socket_options:
- int_value: 1
level: 1 # SOL_SOCKET
name: 9 # SO_KEEPALIVE
state: STATE_PREBIND
- int_value: 9
level: 6 # IPPROTO_TCP
name: 6 # TCP_KEEPCNT
state: STATE_PREBIND
- int_value: 600
level: 6 # IPPROTO_TCP
name: 4 # TCP_KEEPIDLE
state: STATE_PREBIND
- int_value: 75
level: 6 # IPPROTO_TCP
name: 5 # TCP_KEEPINTVL
state: STATE_PREBIND
上述配置的含义是:对于 433 LDS,tcp 连接设置 socket options:连接空闲 600s 后,开始发送探活 probe;如果探活失败,会持续探测 9 次,探测间隔为 75 s。
在 ingress gateway 上 ss, 显示 443 上连接都开启了 keepalive:
如果用户 client 较多不便调整,更适合在 server (ingress gateway)开启 keepalive。另外该方案对 client 有无 sidecar 没有要求。
总结
使用长连接时,应用需要设置合理的 keepalive 参数,特别是对于访问频次较低的场景,以及链路较长的情况。
istio 无入侵式的流量操纵能力,可以很方便的对流量行为进行调优,这也是用户选择 istio 的重要原因。
参考资料
6 - 无法连接 gateway 上的 tcp 端口
故障现象
通过 Gateway CRD 定义了一个 tcp 端口,但是无法连接到 gateway 上该端口。
故障原因
如果通过 Gateway 定义了一个 TCP 端口,但没有采用 VS 配置相应的路由,则会出现在 gateway 上找不到该 TCP 端口的情况。
例如,通过下面的 Gateway ,在 ingress gateway 上定义了一个 TCP 端口 8888。
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
name: ingressgw
spec:
selector:
app: istio-ingressgateway
istio: ingressgateway
servers:
- hosts:
- '*'
port:
name: TCP-8888
number: 8888
protocol: TCP
此时发现通过 ingress gateway 无法访问 8888 端口,查看 ingress gateway 中的 listener 配置,找不到在 8888 上监控的 listener。
-n istio-system proxy-config listeners istio-ingressgateway-74fd488699-4v4rt
ADDRESS PORT MATCH DESTINATION
0.0.0.0 15021 ALL Inline Route: /healthz/ready*
0.0.0.0 15090 ALL Inline Route: /stats/prometheus*
此时查看 istiod 的日志,发现有下面的错误输出:
gateway omitting listener "0.0.0.0_8888" due to: must have more than 0 chains in listener "0.0.0.0_8888"
原因是 istiod 在试图生成 listener 时 filter chain 没有内容,导致 istiod 忽略了该 listener。
解决方案
采用 VS 为该 port 设置对应的路由,则 istiod 在生成 listener 时 filter chain 就不会为空,可以正常生成 listener。
创建 VS:
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: ingress
spec:
gateways:
- ingressgw
hosts:
- '*'
tcp:
- match:
- port: 8888
route:
- destination:
host: details.default.svc.cluster.local
port:
number: 9080
此时查看 ingress gateway 的配置,可以看到 8888 对应的 listener 已经成功生成:
istioctl -n istio-system proxy-config listeners istio-ingressgateway-74fd488699-4v4rt
ADDRESS PORT MATCH DESTINATION
0.0.0.0 8888 ALL Cluster: outbound|9080||details.default.svc.cluster.local
0.0.0.0 15021 ALL Inline Route: /healthz/ready*
0.0.0.0 15090 ALL Inline Route: /stats/prometheus*
备注:HTTP 端口和 TCP 的情况有所不同。如果采用 Gateway 定义了一个 HTTP 端口,没有配置相应的 VS,可以连接到该端口,gateway 将返回 404 HTTP 错误。
7 - 长链接导致 Envoy CPU 负载不均衡
Envoy 在处理长链接时 CPU 负载不均衡。
故障现象
Envoy 进程使用的多个 CPU 之间的工作负载不均衡。如下图所示,Ingress Gateway 中一共有 24 个 Worker,但只有三个 worker 的 CPU 使用率较高,其他 CPU 使用率很低。
出现该问题后,虽然 CPU 还有空闲,但会由于 Envoy 的处理能力不足而导致请求积压,请求时延变长,甚至请求超时。
故障原因
该问题是 Envoy 的线程模型 导致的。 Envoy 采用多个 worker 线程(一般和 CPU core 数量相同)来接收并处理来自 downstream 的链接。一个链接创建后,该链接后面的所有处理只在一个 worker 线程中进行处理。这种线程模型保证了一个链接中的业务处理都是单线程的,简化了代码的处理逻辑。
缺省情况下,Envoy 不会在多个 worker 线程之间对链接数量进行均衡。在大部分 upstream 链接都是短链接的情况下,操作系统可以很好地将链接比较均匀地分配到多个 worker 线程上。但是,在长链接的情况下(例如 HTTP2/GRPC),多个 worker 线程分配到的链接数量可能不够均匀,就会出现有的 CPU 使用率高,有的 CPU 使用率低的情况。
解决方案
Envoy 在 listener 的配置中提供了一个 connection_balance_config
选项来强制在多个 worker 线程之间对链接进行均匀分配。
对于大量长链接的情况,可以采用 EnvoyFilter 来启用connection_balance_config
。 下面的 EnvoyFilter 为 Ingress Gateway 启用了 worker 链接均衡功能。
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: ingress-envoy-listener-balance
namespace: istio-system
spec:
workloadSelector:
labels:
istio: ingressgateway
configPatches:
- applyTo: LISTENER
match:
context: GATEWAY
patch:
operation: MERGE
value:
connection_balance_config:
exact_balance: {}
启用上面的配置后,可以看到各个 CPU 的使用率基本相同。
注意事项:
- 如果一个 Listener 通过设置
use_original_dst
将所有链接都交给其他 Listener 处理,则建议不要在该 Listener 上设置 connection_balance_config
,以避免在该 Listener 上引入额外的开销。这种情况下,应该在真正处理链接的 Listener 上设置该选项。参见 Envoy 文档中的说明。
- 在对链接进行 balancing 时,会在多个 worker 线程中引入一个共享锁,因此对 Envoy 创建链接的性能可能会有一定影响。
8 - 通过 Ingress Gateway 访问集群外部服务 503 UC 错误
当采用和外部服务的域名不同的 sni 来请求外部 https 服务时,envoy 返回 503 UC 错误。
故障现象
该使用场景比较特殊,用户通过 Ingress Gateway 来访问一个集群外部的 HTTPS 服务,Ingress Gateway 返回 503 UC 错误。
用户的访问路径如下:
Browser –> Ingress Gateway(foo.bar.org) –> External Service(dev.bar.org)
Ingress Gateway 配置的 VS 如下:
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: nginx-vs-dev
namespace: foo-dev
spec:
gateways:
- foo-dev/barl-org
hosts:
- foo.bar.org
http:
- match:
- uri:
prefix: /api/test
route:
- destination:
host: dev.bar.org
port:
number: 443
外部服务对应的 ServiceEntry 定义如下:
apiVersion: networking.istio.io/v1beta1
kind: ServiceEntry
metadata:
name: dev-test
namespace: foo-dev
spec:
hosts:
- dev.bar.org
location: MESH_EXTERNAL
ports:
- name: https
number: 443
protocol: HTTPS
resolution: DNS
Ingress Gateway 中的错误日志如下:
{
"upstream_cluster":"outbound|443||dev.bar.org",
"response_flags":"UC",
"authority":"foo.bar.org",
"upstream_host":"47.107.45.209:443",
"bytes_sent":95,
"downstream_remote_address":"182.140.153.175:2223",
"downstream_local_address":"192.168.32.49:443",
"upstream_transport_failure_reason":null,
"istio_policy_status":null,
"response_code":503,
"duration":19,
"request_id":"054c265b-46eb-4524-a892-810ceeb26e64",
"path":"/api/test",
"protocol":"HTTP/2",
"requested_server_name":"foo.bar.org",
"upstream_local_address":"192.168.32.49:53382",
"x_forwarded_for":"182.140.153.175",
"start_time":"2022-11-23T08:08:52.303Z",
"upstream_service_time":null,
"bytes_received":0,
"user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36",
"method":"GET",
"route_name":null
}
故障原因
用户通过 Ingress Gateway 访问时,sni 是 Ingress Gateway 的域名,即 foo.bar.org
,Envoy 在和 upstream 进行 tls 握手时,在没有进行配置的情况下,缺省会使用 downstream 的 sni。而该用例中,upstream 的正确 sni 应该是 dev.bar.org
。由于 SNI 不匹配,导致 Ingress Gateway 和 外部服务 TLS 握手失败,Ingress Gateway 报 503 UC 错误。
解决方案
创建下面的 DR 指定访问该外部服务时使用的 SNI。
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: dev-test
spec:
host: dev.bar.org
trafficPolicy:
tls:
mode: SIMPLE
sni: dev.bar.org
9 - 503 UC upstream_reset_before_response_started
Upstream 断开链路导致 503 UC。
故障现象
客户端直接访问服务器正常,但在 service mesh 中经过 envoy 访问服务器则会出现一定几率的 503 错误。查看客户端侧 envoy 的访问日志,发现日志中有下面的异常信息:
[2023-01-05T04:21:37.764Z] "POST /foo/bar" 503 UC upstream_reset_before_response_started{connection_termination} - "-" 291 95 0 - "116.211.195.11,116.211.195.11" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36" "06a39679-a8d4-47f7-baf3-d688ea3e67c4" "foo.bar.com" "30.183.173.155:1984" outbound|1984||foor-service.bar-ns.svc.cluster.local 30.169.11.123:46894 30.169.11.123:443 116.211.195.11:21663 - -
故障原因
从访问日志中 503 UC upstream_reset_before_response_started{connection_termination}
的输出,我们可以初步推断出 503 的原因是连接被 upstream 侧中断了。
通过 Envoy 管理端口打开 debug 日志,可以看到在出现 503 UC 时,envoy 从 connection pool 中拿出了一个 upstream 的连接,但拿出该连接后,envoy 打印了一个 “remote close” 日志,说明该连接被对端关闭了。
Envoy 的 HTTP Router 会在第一次和 Upstream 建立 TCP 连接并使用后将连接释放到一个连接池中,而不是直接关闭该连接。这样下次 downstream 请求相同的 Upstream host 时可以重用该连接,可以避免频繁创建/关闭连接带来的开销。
当连接被 Envoy 放入连接池后,连接中不再转发来着 downstream 数据,即连接处于空闲状态。连接对端的应用程序会检查连接的空闲状态,并在空闲期间通过 TCP keepalive packet 来侦测对端状态。由于空闲的连接也会占用资源,因此应用并不会无限制地在一个空闲连接上进行等待。几乎所有语言/框架在创建 TCP 服务器时都会设置一个 keepalive timeout 选项,如果在 keepalive timeout 的时间内没有收到新的 TCP 数据包,应用就会关闭该连接。
在应用端关闭连接后的极短时间内,Envoy 侧尚未感知到该连接的状态变化,如果此时 Envoy 收到了来着 downstream 的请求并将该连接从连接池中取出来使用,就会出现 503 UC upstream_reset_before_response_started{connection_termination}
异常。
解决方案
方案一
增大服务器端 TCP keepalive timeout 的时间间隔可以减少该问题出现的几率。该问题在 nodejs 应用中出现得较多,原因是 nodejs 的缺省超时时间较短,只有 5 秒钟,因此在 Envoy 连接池中取出的连接有较大几率刚好被对端的 nodejs 应用关闭了。
Timeout in milliseconds. Default: 5000 (5 seconds).
The number of milliseconds of inactivity a server needs to wait for additional incoming data, after it has finished writing the last response, before a socket will be destroyed. If the server receives new data before the keep-alive timeout has fired, it will reset the regular inactivity timeout, i.e., server.timeout.
通过下面的方法可以在服务器端将 nodejs 的 keepalive tiemout 时间延长为 6 分钟。
const server = app.listen(port, '0.0.0.0', () => {
logger.info(`App is now running on http://localhost:${port}`)
})
server.keepAliveTimeout = 1000 * (60 * 6) // 6 minutes
其他语言的设置方法:
Python
global_config = {
'server.socket_timeout': 6 * 60,
}
cherrypy.config.update(global_config)
Go
var s = http.Server{
Addr: ":8080",
Handler: http.HandlerFunc(Index),
IdleTimeout: 6 * time.Minute,
}
s.ListenAndServe()
方案二
通过方案一可以减少 503 UC 出现的频率,但理论上无论 keepalive timeout 设置为多大,都有出现 503 UC的几率。而且我们也需要将 timeout 设置为一个合理的值,而不是无限大。要彻底解决该问题,可以采用 Virtual Service 为出现该问题的服务设置重试策略,在重试策略的 retryOn 中增加 reset
条件。
备注:
Istio 缺省为服务设置了重试策略,但缺省的重试策略中并不会对连接重置这种情况进行重试。
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: ratings-route
spec:
hosts:
- ratings.prod.svc.cluster.local
http:
- route:
- destination:
host: ratings.prod.svc.cluster.local
subset: v1
retries:
attempts: 3
retryOn: reset,connect-failure,refused-stream,unavailable,cancelled,retriable-status-codes
参考文档
10 - Metrics 导致 Envoy 内存快速增长
自定义 metrics 导致 Envoy 内存快速增长。
故障现象
Envoy 内存快速增长,不久后即内存溢出,导致 Pod 不断重启。导出 heap 并通过 pprof 查看,发现内存中有大量 Stat Tag 相关的对象。
故障原因
Envoy 中 Stats Tag 用于 Metrics 上报,因此怀疑是对 metrics 的改动导致了该问题。 查看 Istiod 的 EnvoyFilter stats-filter-1.12,发现该 EnvoyFilter 中为 metrics 增加了一个 dimension request_path:request.path
,如下图中高亮部分所示。该配置表示在 metrics 中新增一个 tag,取值为 HTTP 请求 Header 中的 path 字段。 由于该服务请求中 path 字段包含了用户 token 等变量,导致 path 的取值范围很广,导致 envoy 中的 metrics 实例数量暴增,最终导致内存溢出。
解决方案
在 EnvoyFilter 中去掉 request_path dimension,该问题即可解决。
该故障的经验教训:为 istio 数据面 metrics 增加 tag 时需要特别注意,不要随意加入取值范围较大,特别是取值为离散值的 tag。这会导致 metrics 占用的内存数量成倍增长。 例如增加一个取值范围为 10 的 tag,理论上就会导致 metrics 占用的内存增加 10 倍。