• 调试与察看 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) 的启动过程,须要通过几道砍:

  1. Istio auto inject sidecar 在容器启动时就主动启动 Envoy,很难在初始化前实现 remote debug attach 和 breakpoint 设置。
  2. /usr/local/bin/pilot-agent 负责运行 /usr/local/bin/envoy 过程,并作为其父过程,即不能够间接管制 envoy 过程的启动。

上面我解释一下如何避坑。

Envoy 的启动 attach 办法

上面钻研一下,两种场景下,Envoy 的启动 attach 办法:

  1. Istio auto inject 的 istio-proxy container (我没有应用这种办法,见附录局部)
  2. 手工 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.yamlapiVersion: apps/v1kind: StatefulSetmetadata:  creationTimestamp: null  labels:    app: fortio-server  name: fortio-serverspec:  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 -- bashsudo apt install -y tmux

我只心愿一个 app(uid=1000) 用户的 outbound 流量流经 envoy,其它 outbound 流量不通过 envoy:

kubectl exec -it fortio-server-0 -c main-app -- bashadduser -u 1000 app
kubectl exec -it fortio-server-0 -c istio-proxy -- bashtmux #开启 tmux serversudo 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 15001COMMITEOF
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 容器内一个过程的 PIDexport 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    fidone <<< "$ENVOY_PIDS"echo $POD_PIDexport PID=$POD_PID# 启动 remote debug serversudo 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 -- bashtmux 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 22023-06-05T08:04:25.267206Z     info    Effective config: binaryPath: /usr/local/bin/envoyconcurrency: 2configPath: ./etc/istio/proxycontrolPlaneAuthPolicy: MUTUAL_TLSdiscoveryAddress: istiod.istio-system.svc:15012drainDuration: 45sproxyAdminPort: 15000serviceCluster: istio-proxystatNameLength: 189statusPort: 15020terminationDrainDuration: 5stracing:  zipkin:    address: zipkin.istio-system:9411...2023-06-05T08:04:25.754381Z     info    Starting proxy agent2023-06-05T08:04:25.755875Z     info    starting2023-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 间接调用的零碎调用 syscallbreakpoint set --func-regex .*OsSysCallsImpl.*# libevent 的 syscallbreakpoint 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=envoyprogpid=`pgrep -o $progstr`while [ "$progpid" = "" ]; do  progpid=`pgrep -o $progstr`donegdb -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_elfvi /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 -- bashsu appcurl -v www.baidu.com

lldb 常用命令单

lldb(lldb) process attach --name pilot-agent --waitfor(lldb) platform process attach --name envoy --waitfor