本文已超過一年。較舊的文章可能包含過時的內容。請檢查頁面中的資訊自發布以來是否已變得不正確。

鑑識容器分析

在我之前的文章 Kubernetes 中的鑑識容器檢查點 中,我介紹了 Kubernetes 中的檢查點功能,以及如何設定和使用它。該功能的名稱是鑑識容器檢查點,但我沒有詳細說明如何對 Kubernetes 建立的檢查點進行實際分析。在本文中,我想詳細說明如何分析檢查點。

檢查點仍然是 Kubernetes 中的 Alpha 功能,本文旨在預覽該功能未來可能如何運作。

準備工作

關於如何配置 Kubernetes 和底層 CRI 實作以啟用檢查點支援的詳細資訊,可以在我的 Kubernetes 中的鑑識容器檢查點 文章中找到。

作為範例,我準備了一個容器映像(quay.io/adrianreber/counter:blog),我想在本篇文章中對其進行檢查點並進行分析。此容器允許我在容器中建立檔案,並將資訊儲存在記憶體中,以便稍後在檢查點中找到。

要執行該容器,我需要一個 Pod,在此範例中,我使用以下 Pod 清單

apiVersion: v1
kind: Pod
metadata:
  name: counters
spec:
  containers:
  - name: counter
    image: quay.io/adrianreber/counter:blog

這會產生一個名為 counter 的容器,在名為 counters 的 Pod 中執行。

容器執行後,我對該容器執行以下操作

$ kubectl get pod counters --template '{{.status.podIP}}'
10.88.0.25
$ curl 10.88.0.25:8088/create?test-file
$ curl 10.88.0.25:8088/secret?RANDOM_1432_KEY
$ curl 10.88.0.25:8088

第一次存取會在容器中建立一個名為 test-file 的檔案,內容為 test-file,第二次存取會將我的機密資訊 (RANDOM_1432_KEY) 儲存在容器記憶體的某個位置。最後一次存取只是在內部日誌檔案中新增一行。

在我可以分析檢查點之前的最後一步是告訴 Kubernetes 建立檢查點。如前一篇文章所述,這需要僅存取 kubeletcheckpoint API 端點。

對於名為 counter 的容器,在名為 counters 的 Pod 中,在名為 default 的命名空間中,kubelet API 端點可透過以下方式存取

# run this on the node where that Pod is executing
curl -X POST "https://localhost:10250/checkpoint/default/counters/counter"

為了完整性,以下 curl 命令列選項是必要的,以便 curl 接受 kubelet 的自我簽署憑證並授權使用 kubelet checkpoint API

--insecure --cert /var/run/kubernetes/client-admin.crt --key /var/run/kubernetes/client-admin.key

檢查點完成後,檢查點應可在 /var/lib/kubelet/checkpoints/checkpoint-<pod-name>_<namespace-name>-<container-name>-<timestamp>.tar 中找到

在本文的後續步驟中,我將在使用名為 checkpoint.tar 的檔案來分析檢查點封存。

使用 checkpointctl 進行檢查點封存分析

為了獲得有關檢查點容器的一些初始資訊,我使用工具 checkpointctl,如下所示

$ checkpointctl show checkpoint.tar --print-stats
+-----------+----------------------------------+--------------+---------+---------------------+--------+------------+------------+-------------------+
| CONTAINER |              IMAGE               |      ID      | RUNTIME |       CREATED       | ENGINE |     IP     | CHKPT SIZE | ROOT FS DIFF SIZE |
+-----------+----------------------------------+--------------+---------+---------------------+--------+------------+------------+-------------------+
| counter   | quay.io/adrianreber/counter:blog | 059a219a22e5 | runc    | 2023-03-02T06:06:49 | CRI-O  | 10.88.0.23 | 8.6 MiB    | 3.0 KiB           |
+-----------+----------------------------------+--------------+---------+---------------------+--------+------------+------------+-------------------+
CRIU dump statistics
+---------------+-------------+--------------+---------------+---------------+---------------+
| FREEZING TIME | FROZEN TIME | MEMDUMP TIME | MEMWRITE TIME | PAGES SCANNED | PAGES WRITTEN |
+---------------+-------------+--------------+---------------+---------------+---------------+
| 100809 us     | 119627 us   | 11602 us     | 7379 us       |          7800 |          2198 |
+---------------+-------------+--------------+---------------+---------------+---------------+

這已經為我提供了有關該檢查點封存中檢查點的一些資訊。我可以看到容器的名稱、有關容器運行時和容器引擎的資訊。它還列出了檢查點的大小 (CHKPT SIZE)。這主要是檢查點中包含的記憶體頁面的大小,但也有關於容器中所有已更改檔案的大小資訊 (ROOT FS DIFF SIZE)。

額外的參數 --print-stats 會解碼檢查點封存中的資訊,並在第二個表格(CRIU dump statistics)中顯示它們。此資訊在檢查點建立期間收集,並概述了 CRIU 建立容器中進程檢查點所需的時間,以及在檢查點建立期間分析和寫入的記憶體頁面數量。

深入挖掘

借助 checkpointctl,我可以獲得有關檢查點封存的一些高階資訊。為了能夠進一步分析檢查點封存,我必須提取它。檢查點封存是一個 tar 封存,可以使用 tar xf checkpoint.tar 提取。

提取檢查點封存將產生以下檔案和目錄

  • bind.mounts - 此檔案包含有關綁定掛載的資訊,在還原期間需要它,以便在正確的位置掛載所有外部檔案和目錄
  • checkpoint/ - 此目錄包含 CRIU 建立的實際檢查點
  • config.dumpspec.dump - 這些檔案包含有關容器的中繼資料,還原期間需要它
  • dump.log - 此檔案包含 CRIU 在檢查點期間建立的偵錯輸出
  • stats-dump - 此檔案包含 checkpointctl 用於顯示 dump 統計資訊 (--print-stats) 的資料
  • rootfs-diff.tar - 此檔案包含容器檔案系統上所有已更改的檔案

檔案系統變更 - rootfs-diff.tar

進一步分析容器檢查點的第一步是查看容器中已變更的檔案。這可以透過查看檔案 rootfs-diff.tar 來完成

$ tar xvf rootfs-diff.tar
home/counter/logfile
home/counter/test-file

現在可以研究容器中已變更的檔案

$ cat home/counter/logfile
10.88.0.1 - - [02/Mar/2023 06:07:29] "GET /create?test-file HTTP/1.1" 200 -
10.88.0.1 - - [02/Mar/2023 06:07:40] "GET /secret?RANDOM_1432_KEY HTTP/1.1" 200 -
10.88.0.1 - - [02/Mar/2023 06:07:43] "GET / HTTP/1.1" 200 -
$ cat home/counter/test-file
test-file 

與此容器所基於的容器映像 (quay.io/adrianreber/counter:blog) 相比,我可以看到檔案 logfile 包含有關所有對容器提供的服務的存取資訊,並且檔案 test-file 已按預期建立。

借助 rootfs-diff.tar,可以檢查所有已建立或變更的檔案,與容器的基礎映像相比。

分析檢查點進程 - checkpoint/

目錄 checkpoint/ 包含 CRIU 在對容器中的進程進行檢查點時建立的資料。目錄 checkpoint/ 中的內容由不同的 映像檔案 組成,可以使用工具 CRIT 進行分析,該工具作為 CRIU 的一部分發布。

首先,讓我們了解容器內進程的概況

$ crit show checkpoint/pstree.img | jq .entries[].pid
1
7
8

此輸出表示我在容器的 PID 命名空間內有三個進程,其 PID 為:1、7、8

這僅是從容器的 PID 命名空間內部的視圖。在還原期間,將準確地重新建立這些 PID。從容器的 PID 命名空間外部來看,PID 將在還原後變更。

下一步是獲取有關這三個進程的一些其他資訊

$ crit show checkpoint/core-1.img | jq .entries[0].tc.comm
"bash"
$ crit show checkpoint/core-7.img | jq .entries[0].tc.comm
"counter.py"
$ crit show checkpoint/core-8.img | jq .entries[0].tc.comm
"tee"

這表示我的容器中的三個進程是 bashcounter.py(Python 解釋器)和 tee。有關這些進程的父子關係的詳細資訊,checkpoint/pstree.img 中有更多資料需要分析。

讓我們將到目前為止收集的資訊與仍在運行的容器進行比較

$ crictl inspect --output go-template --template "{{(index .info.pid)}}" 059a219a22e56
722520
$ ps auxf | grep -A 2 722520
fedora    722520  \_ bash -c /home/counter/counter.py 2>&1 | tee /home/counter/logfile
fedora    722541      \_ /usr/bin/python3 /home/counter/counter.py
fedora    722542      \_ /usr/bin/coreutils --coreutils-prog-shebang=tee /usr/bin/tee /home/counter/logfile
$ cat /proc/722520/comm
bash
$ cat /proc/722541/comm
counter.py
$ cat /proc/722542/comm
tee

在此輸出中,我首先檢索容器中第一個進程的 PID,然後我在容器運行的系統上尋找該 PID 和子進程。我看到三個進程,第一個是 "bash",它是容器 PID 命名空間內部的 PID 1。然後我查看 /proc/<PID>/comm,我可以找到與檢查點映像中完全相同的值。

重要的是要記住,檢查點將包含來自容器 PID 命名空間內部的視圖,因為該資訊對於還原進程很重要。

crit 可以告訴我們有關容器的最後一個範例是有關 UTS 命名空間的資訊

$ crit show checkpoint/utsns-12.img
{
    "magic": "UTSNS",
    "entries": [
        {
            "nodename": "counters",
            "domainname": "(none)"
        }
    ]
}

這告訴我 UTS 命名空間內的主機名稱是 counters

對於 CRIU 在檢查點期間收集的每個資源,checkpoint/ 目錄都包含相應的映像檔案,可以使用 crit 進行分析。

查看記憶體頁面

除了可以使用 CRIT 解碼的 CRIU 中的資訊之外,還有包含 CRIU 寫入磁碟的原始記憶體頁面的檔案

$ ls  checkpoint/pages-*
checkpoint/pages-1.img  checkpoint/pages-2.img  checkpoint/pages-3.img

當我最初使用容器時,我將一個隨機金鑰 (RANDOM_1432_KEY) 儲存在記憶體的某個位置。讓我們看看我是否能找到它

$ grep -ao RANDOM_1432_KEY checkpoint/pages-*
checkpoint/pages-2.img:RANDOM_1432_KEY

而且,確實,我的資料就在那裡。這樣我可以輕鬆查看容器中進程的所有記憶體頁面的內容,但同樣重要的是要記住,任何可以存取檢查點封存的人都可以存取儲存在容器進程記憶體中的所有資訊。

使用 gdb 進行進一步分析

查看檢查點映像的另一種可能性是 gdb。CRIU 儲存庫包含腳本 coredump,它可以將檢查點轉換為 coredump 檔案

$ /home/criu/coredump/coredump-python3
$ ls -al core*
core.1  core.7  core.8

運行 coredump-python3 腳本會將檢查點映像轉換為容器中每個進程的一個 coredump 檔案。使用 gdb,我也可以查看進程的詳細資訊

$ echo info registers | gdb --core checkpoint/core.1 -q

[New LWP 1]

Core was generated by `bash -c /home/counter/counter.py 2>&1 | tee /home/counter/logfile'.

#0  0x00007fefba110198 in ?? ()
(gdb)
rax            0x3d                61
rbx            0x8                 8
rcx            0x7fefba11019a      140667595587994
rdx            0x0                 0
rsi            0x7fffed9c1110      140737179816208
rdi            0xffffffff          4294967295
rbp            0x1                 0x1
rsp            0x7fffed9c10e8      0x7fffed9c10e8
r8             0x1                 1
r9             0x0                 0
r10            0x0                 0
r11            0x246               582
r12            0x0                 0
r13            0x7fffed9c1170      140737179816304
r14            0x0                 0
r15            0x0                 0
rip            0x7fefba110198      0x7fefba110198
eflags         0x246               [ PF ZF IF ]
cs             0x33                51
ss             0x2b                43
ds             0x0                 0
es             0x0                 0
fs             0x0                 0
gs             0x0                 0

在此範例中,我可以看到檢查點期間所有暫存器的值,也可以看到容器 PID 1 進程的完整命令列:bash -c /home/counter/counter.py 2>&1 | tee /home/counter/logfile

總結

借助容器檢查點,可以建立正在運行的容器的檢查點,而無需停止容器,也無需讓容器知道它已被檢查點。在 Kubernetes 中檢查點容器的結果是一個檢查點封存;使用不同的工具,如 checkpointctltarcritgdb,可以分析檢查點。即使使用像 grep 這樣的簡單工具,也可以在檢查點封存中找到資訊。

我在本文中展示的分析檢查點的不同範例僅僅是起點。根據您的需求,可以更詳細地查看某些內容,但本文應為您提供如何開始分析檢查點的簡要介紹。

我如何參與?

您可以透過多種方式聯繫 SIG Node