Istio 常见问题
- 1: 应用程序启动失败/启动时无法访问网络
- 2: ExternalName Service 劫持了其他服务流量
- 3: Gateway TLS hosts 冲突导致配置被拒绝
- 4: Server Speaks First 协议访问失败
- 5: 长连接未开启 tcp keepalive
1 - 应用程序启动失败/启动时无法访问网络
故障现象
该问题的表现是安装了 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流量管理实现机制深度解析)。
解决方案
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:<bob@example.com>
S: 250 Ok
C: RCPT TO:<alice@example.com>
S: 250 Ok
C: RCPT TO:<theboss@example.com>
S: 250 Ok
C: DATA
S: 354 End data with <CR><LF>.<CR><LF>
C: From: "Bob Example" <bob@example.com>
C: To: Alice Example <alice@example.com>
C: Cc: theboss@example.com
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 的重要原因。