共计 11507 个字符,预计需要花费 29 分钟才能阅读完成。
-
调试与察看 istio-proxy Envoy sidecar 的启动过程
- debug 初始化之难
-
Envoy 的启动 attach 办法
-
手工 inject 的 istio-proxy container
- 1. 定制手工拉起的 istio-proxy 环境
-
2. 启动 remote debug server 与 vscode debug session
- 2.1 设置断点
- 3. 启动 pilot-agent 和 envoy
- 4. 开始 debug
- 罕用断点
-
-
附录 – 写给本人的一些备忘
-
Istio auto inject 的 sidecar container (我没有应用这种办法)
- 在 worker node 上 Debugger wait process
- Debugger follow process fork
- Debugger wrapper script
- 流量 debug
- lldb 常用命令单
-
调试与察看 istio-proxy Envoy sidecar 的启动过程
学习 Istio 下 Envoy sidecar 的初始化过程,有助于了解 Envoy 是如何构建起整个事件驱动和线程互动体系的。其中 Listener socket 事件监初始化是重点。而获取这个常识最间接的办法是 debug Envoy 启动初始化过程,这样能够间接察看运行状态的 Envoy 代码,而不是间接读无聊的 OOP 代码去猜事实行为。但要 debug sidecar 初始化有几道砍要过。本文记录了我通关打怪的过程。
本文转自我的开源图书《Istio & Envoy 底细》
本文基于我之前写的:《调试 Istio 网格中运行的 Envoy sidecar C++ 代码》。你可能须要看看前者的背景,才比拟容易读懂本文。
debug 初始化之难
有教训的程序员都晓得,debug 的难度和要 debug 的指标场景呈现频率成反比。而 sidecar 的初始化只有一次。
要 debug istio-proxy(Envoy) 的启动过程,须要通过几道砍:
- Istio auto inject sidecar 在容器启动时就主动启动 Envoy,很难在初始化前实现 remote debug attach 和 breakpoint 设置。
/usr/local/bin/pilot-agent
负责运行/usr/local/bin/envoy
过程,并作为其父过程,即不能够间接管制 envoy 过程的启动。
上面我解释一下如何避坑。
Envoy 的启动 attach 办法
上面钻研一下,两种场景下,Envoy 的启动 attach 办法:
- Istio auto inject 的 istio-proxy container (我没有应用这种办法,见附录局部)
- 手工 inject 的 istio-proxy container (我应用这种办法)
手工 inject 的 istio-proxy container
要不便精准地在 envoy 开始初始化前 attach envoy 过程,一个办法是不要在容器启动时主动启动 envoy。要手工启动 pilot-agent
,一个办法是不要 auto inject sidecar,用 istioctl
手工 inject:
1. 定制手工拉起的 istio-proxy 环境
# fortio-server.yaml 是定义 pod 的 k8s StatefulSet/deployment
$ ./istioctl kube-inject -f fortio-server.yaml > fortio-server-injected.yaml
$ vi fortio-server-injected.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
creationTimestamp: null
labels:
app: fortio-server
name: fortio-server
spec:
replicas: 1
selector:
matchLabels:
app: fortio-server
serviceName: fortio-server
template:
metadata:
annotations:
kubectl.kubernetes.io/default-container: main-app
kubectl.kubernetes.io/default-logs-container: main-app
prometheus.io/path: /stats/prometheus
prometheus.io/port: "15020"
prometheus.io/scrape: "true"
sidecar.istio.io/proxyImage: 192.168.122.1:5000/proxyv2:1.17.2-debug
sidecar.istio.io/inject: "false" #退出这行
creationTimestamp: null
labels:
app: fortio-server
app.kubernetes.io/name: fortio-server
security.istio.io/tlsMode: istio
service.istio.io/canonical-name: fortio-server
service.istio.io/canonical-revision: latest
spec:
containers:
- args:
- 10d
command:
- /bin/sleep #不启动 pilot-agent
image: docker.io/nicolaka/netshoot:latest
imagePullPolicy: IfNotPresent
name: main-app
ports:
- containerPort: 8080
name: http
protocol: TCP
resources: {}
- args:
- 20d
command:
- /usr/bin/sleep
env:
- name: JWT_POLICY
value: third-party-jwt
- name: PILOT_CERT_PROVIDER
value: istiod
- name: CA_ADDR
value: istiod.istio-system.svc:15012
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: INSTANCE_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: SERVICE_ACCOUNT
valueFrom:
fieldRef:
fieldPath: spec.serviceAccountName
- name: HOST_IP
valueFrom:
fieldRef:
fieldPath: status.hostIP
- name: PROXY_CONFIG
value: |
{}
- name: ISTIO_META_POD_PORTS
value: |-
[{"name":"http","containerPort":8080,"protocol":"TCP"}
,{"name":"http-m","containerPort":8070,"protocol":"TCP"}
,{"name":"grpc","containerPort":8079,"protocol":"TCP"}
]
- name: ISTIO_META_APP_CONTAINERS
value: main-app
- name: ISTIO_META_CLUSTER_ID
value: Kubernetes
- name: ISTIO_META_NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: ISTIO_META_INTERCEPTION_MODE
value: REDIRECT
- name: ISTIO_META_MESH_ID
value: cluster.local
- name: TRUST_DOMAIN
value: cluster.local
image: 192.168.122.1:5000/proxyv2:1.17.2-debug
name: istio-proxy
ports:
- containerPort: 15090
name: http-envoy-prom
protocol: TCP
- containerPort: 2159
name: http-m
protocol: TCP
resources:
requests:
cpu: 100m
memory: 128Mi
securityContext:
allowPrivilegeEscalation: true
capabilities:
add:
- ALL
privileged: true
readOnlyRootFilesystem: false
runAsGroup: 1337
runAsNonRoot: true
runAsUser: 1337
volumeMounts:
- mountPath: /var/run/secrets/workload-spiffe-uds
name: workload-socket
- mountPath: /var/run/secrets/credential-uds
name: credential-socket
- mountPath: /var/run/secrets/workload-spiffe-credentials
name: workload-certs
- mountPath: /var/run/secrets/istio
name: istiod-ca-cert
- mountPath: /var/lib/istio/data
name: istio-data
- mountPath: /etc/istio/proxy
name: istio-envoy
- mountPath: /var/run/secrets/tokens
name: istio-token
- mountPath: /etc/istio/pod
name: istio-podinfo
restartPolicy: Always
volumes:
- name: workload-socket
- name: credential-socket
- name: workload-certs
- emptyDir:
medium: Memory
name: istio-envoy
- emptyDir: {}
name: istio-data
- downwardAPI:
items:
- fieldRef:
fieldPath: metadata.labels
path: labels
- fieldRef:
fieldPath: metadata.annotations
path: annotations
name: istio-podinfo
- name: istio-token
projected:
sources:
- serviceAccountToken:
audience: istio-ca
expirationSeconds: 43200
path: istio-token
- configMap:
name: istio-ca-root-cert
name: istiod-ca-cert
updateStrategy: {}
status:
availableReplicas: 0
replicas: 0
$ kubectl apply -f fortio-server-injected.yaml
为防止 kubectl exec 在容器中启动过程的意外退出,和能够屡次接入同一个 shell 实例,我应用了 tmux
:
kubectl exec -it fortio-server-0 -c istio-proxy -- bash
sudo apt install -y tmux
我只心愿一个 app(uid=1000) 用户的 outbound 流量流经 envoy,其它 outbound 流量不通过 envoy:
kubectl exec -it fortio-server-0 -c main-app -- bash
adduser -u 1000 app
kubectl exec -it fortio-server-0 -c istio-proxy -- bash
tmux #开启 tmux server
sudo iptables-restore <<"EOF"
*nat
:PREROUTING ACCEPT [8947:536820]
:INPUT ACCEPT [8947:536820]
:OUTPUT ACCEPT [713:63023]
:POSTROUTING ACCEPT [713:63023]
: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 15008 -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
# do not redirect remote lldb inbound
-A ISTIO_INBOUND -p tcp -m tcp --dport 2159 -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
# only redirct app user outbound
-A ISTIO_OUTPUT -m owner ! --uid-owner 1000 -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
# only redirct app user outbound
-A ISTIO_OUTPUT -m owner ! --gid-owner 1000 -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
EOF
2. 启动 remote debug server 与 vscode debug session
在 isto-proxy 运行的 worker node 上启动 remote debug server:
ssh labile@192.168.122.55 # ssh 到运行 istio-proxy 的 worker node
# 获取 istio-proxy 容器内一个过程的 PID
export POD="fortio-server-0"
ENVOY_PIDS=$(pgrep sleep) #容器中有个叫 /usr/bin/sleep 的过程
while IFS= read -r ENVOY_PID; do
HN=$(sudo nsenter -u -t $ENVOY_PID hostname)
if [["$HN" = "$POD"]]; then # space between = is important
sudo nsenter -u -t $ENVOY_PID hostname
export POD_PID=$ENVOY_PID
fi
done <<< "$ENVOY_PIDS"
echo $POD_PID
export PID=$POD_PID
# 启动 remote debug server
sudo nsenter -t $PID -u -p -m bash -c 'lldb-server platform --server --listen *:2159' #留神没有 -n:
为何不应用 kubectl port forward?
我尝试过:
kubectl port-forward --address 0.0.0.0 pods/fortio-server-0 2159:2159
可能因为 debug 的流量很大,forward 很不稳固。
在 lldb-vscode-server
的 .vscode/launch.json
文件中,退出一个 debug 配置:
{
"version": "0.2.0",
"configurations": [
{
"name": "AttachLLDBWaitRemote",
"type": "lldb",
"request": "attach",
"program": "/usr/local/bin/envoy",
// "stopOnEntry": true,
"waitFor": true,
"sourceMap": {
"/proc/self/cwd": "/work/bazel-work",
"/home/.cache/bazel/_bazel_root/1e0bb3bee2d09d2e4ad3523530d3b40c/sandbox/linux-sandbox/263/execroot/io_istio_proxy": "/work/bazel-work"
},
"initCommands": [
// "log enable lldb commands",
"platform select remote-linux", // Execute `platform list` for a list of available remote platform plugins.
"platform connect connect://192.168.122.55:2159",
],
}
而后在 vscode 中启动 AttachLLDBWaitRemote。这将与 lldb-server 建设连贯,并剖析 /usr/local/bin/envoy
。因为这是一个 1GB 的 ELF,这步在我的机器中用了 100% CPU 和 16GB RSS 内存,耗时 1 分钟以上。实现后,可见 istio-proxy 中有一个 100% CPU 占用的 lldb-server
过程,其实就是 "waitFor": true
命令 lldb-server 一直扫描过程列表。
2.1 设置断点
你能够在设置断点在你的趣味点上,我是:
envoy/source/exe/main.cc
即:Envoy::MainCommon::main(...)
3. 启动 pilot-agent 和 envoy
kubectl exec -it fortio-server-0 -c istio-proxy -- bash
tmux a #连贯上之前启动的 tmux server
/usr/local/bin/pilot-agent proxy sidecar --domain ${POD_NAMESPACE}.svc.cluster.local --proxyLogLevel=warning --proxyComponentLogLevel=misc:error --log_output_level=default:info --concurrency 2
2023-06-05T08:04:25.267206Z info Effective config: binaryPath: /usr/local/bin/envoy
concurrency: 2
configPath: ./etc/istio/proxy
controlPlaneAuthPolicy: MUTUAL_TLS
discoveryAddress: istiod.istio-system.svc:15012
drainDuration: 45s
proxyAdminPort: 15000
serviceCluster: istio-proxy
statNameLength: 189
statusPort: 15020
terminationDrainDuration: 5s
tracing:
zipkin:
address: zipkin.istio-system:9411
...
2023-06-05T08:04:25.754381Z info Starting proxy agent
2023-06-05T08:04:25.755875Z info starting
2023-06-05T08:04:25.758098Z info Envoy command: [-c etc/istio/proxy/envoy-rev.json --drain-time-s 45 --drain-strategy immediate --local-address-ip-version v4 --file-flush-interval-msec 1000 --disable-hot-restart --allow-unknown-static-fields --log-format %Y-%m-%dT%T.%fZ %l envoy %n %g:%# %v thread=%t -l warning --component-log-level misc:error --concurrency 2]
4. 开始 debug
这时,lldb-server 会扫描到 envoy 过程的启动,并 attach 和 挂起 envoy 过程,而后告诉到 vscode。vscode 设置断点,而后持续 envoy 的运行,而后过程跑到断点,vscode 反馈到 GUI:
罕用断点
以下是一些我罕用的断点:
# Envoy 间接调用的零碎调用 syscall
breakpoint set --func-regex .*OsSysCallsImpl.*
# libevent 的 syscall
breakpoint set --shlib libc.so.6 --func-regex 'epoll_create.*|epoll_wait|epoll_ctl'
breakpoint set --shlib libc.so.6 --basename 'epoll_create'
breakpoint set --shlib libc.so.6 --basename 'epoll_create1'
breakpoint set --shlib libc.so.6 --basename 'epoll_wait'
breakpoint set --shlib libc.so.6 --basename 'epoll_ctl'
附录 – 写给本人的一些备忘
Istio auto inject 的 sidecar container (我没有应用这种办法)
做过 k8s 运维的同学都晓得,一个时常遇到,但又短少非入侵办法定位的问题是:容器启动时出错。很难有方法让出错的启动过程暂停下来,留短缺的工夫,让人工进入环境中去做 troubleshooting。而 gdb/lldb 这类 debuger 天生就有这种让任意过程挂起的“魔法”。
对于 Istio auto inject 的 sidecar container,是很难在 envoy 初始化前 attach 到刚启动的 envoy 过程的。实践上有几个可能的办法( 留神:我未测试过 ):
- 在 worker node 上 Debugger wait process
- debugger follow process fork
- debugger wrapper script
上面简略阐明一下实践。
在 worker node 上 Debugger wait process
在 worker node 上,让 gdb/lldb 一直扫描过程列表,发现 envoy 立刻 attach
对于 gdb,网上 有个 script:
#!/bin/sh
# 以下脚本启动前,要求 worker node 下未有 envoy 过程运行
progstr=envoy
progpid=`pgrep -o $progstr`
while ["$progpid" = ""]; do
progpid=`pgrep -o $progstr`
done
gdb -ex continue -p $progpid
对于 本文的配角 lldb,有内置的办法:
(lldb) process attach --name /usr/local/bin/envoy --waitfor
这个办法毛病是 debugger(gdb/lldb) 与 debuggee(envoy) 运行在不同的 pid namespace 和 mount namespace,会让 debugger 产生很多奇怪的问题,所以不倡议应用。
Debugger follow process fork
咱们晓得:
envoy
过程由容器的 pid 1 过程(这里为pilot-agent
)启动pilot-agent
由长寿过程runc
启动runc
由/usr/local/bin/containerd-shim-runc-v2
启动containerd-shim-runc-v2
由/usr/local/bin/containerd
启动
参考:https://iximiuz.com/en/posts/implementing-container-runtime-s…
只有用 debugger 跟踪 containerd,一步步 follow process fork 就能够跟踪到 exec /usr/local/bin/envoy。
对于 gdb 能够用
(gdb) set follow-fork-mode child
参见:
https://visualgdb.com/gdbreference/commands/set_follow-fork-mode
对于 lldb 能够用:
(lldb) settings set target.process.follow-fork-mode child
参见:
- LLDB support for fork(2) and vfork(2)
- LLDB Improvements Part II – Additional CPU Support, Follow-fork operations, and SaveCore Functionality
- lldb equivalent of gdb’s “follow-fork-mode” or “detach-on-fork”
Debugger wrapper script
咱们没方法间接批改 pilot-agent
注入 debugger,但能够用一个 wrapper script
替换 /usr/local/bin/envoy
,而后由这个 wrapper script
启动 debugger , 让 debugger 启动 真正的 envoy ELF。
能够通过批改 istio-proxy docker image 的办法,去实现:
如:
mv /usr/local/bin/envoy /usr/local/bin/real_envoy_elf
vi /usr/local/bin/envoy
...
chmod +x /usr/local/bin/envoy
/usr/local/bin/envoy
写成这样:
#!/bin/bash
# This is a gdb wrapper script.
# Get the arguments passed to the script.
args=$@
# Start gdb.
gdb -ex=run --args /usr/local/bin/real_envoy_elf $args
参见:
- Debugging binaries invoked from scripts with GDB
流量 debug
发动一些 通过 envoy 的 outbound 流量:
kubectl exec -it fortio-server-0 -c main-app -- bash
su app
curl -v www.baidu.com
lldb 常用命令单
lldb
(lldb) process attach --name pilot-agent --waitfor
(lldb) platform process attach --name envoy --waitfor