本文為從零開始寫 Docker 系列第十篇,實現類似 docker logs 的功能,使得我們能夠查檢視容器日誌。
完整程式碼見:https://github.com/lixd/mydocker
歡迎 Star
推薦閱讀以下文章對 docker 基本實現有一個大致認識:
- 核心原理:深入理解 Docker 核心原理:Namespace、Cgroups 和 Rootfs
- 基於 namespace 的檢視隔離:探索 Linux Namespace:Docker 隔離的神奇背後
- 基於 cgroups 的資源限制
- 初探 Linux Cgroups:資源控制的奇妙世界
- 深入剖析 Linux Cgroups 子系統:資源精細管理
- Docker 與 Linux Cgroups:資源隔離的魔法之旅
- 基於 overlayfs 的檔案系統:Docker 魔法解密:探索 UnionFS 與 OverlayFS
- 基於 veth pair、bridge、iptables 等等技術的 Docker 網路:揭秘 Docker 網路:手動實現 Docker 橋接網路
開發環境如下:
root@mydocker:~# lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 20.04.2 LTS
Release: 20.04
Codename: focal
root@mydocker:~# uname -r
5.4.0-74-generic
注意:需要使用 root 使用者
1. 概述
上一篇已經實現了mydocker ps
命令,可以檢視到當前執行中的容器了。
本篇主要實現 mydocker logs
,讓我們可以隨時檢視容器日誌。
一般來說,對於容器中執行的程序,使日誌列印到標準輸出是一個非常好的實現方案,因此需要將容器中的標準輸出儲存下來,以便需要的時候訪問。
我們就以此作為思路來實現 mydocker logs 命令:
- 啟動時將容器程序的標準輸出掛載到
/var/lib/mydocker/containers/{containerId}/{containerId}-json.log
檔案中 - mydocker logs 則讀取這個檔案以獲取容器日誌
實際上 docker 實現也類似,他會把容器日誌儲存在
var/lib/docker/containers/{containerId}/{containerId}-json.log
檔案中。
2. 實現
具體實現包括兩部分:
- 1)重定向輸出到檔案
- 2)實現 mydocker logs 命令
輸出重定向
首先,需要修改一下原來的實現,在建立後臺執行容器的時候,把程序的標準輸出重新定向一下到日誌檔案。
前臺容器依舊列印到 Stdout 即可
func NewParentProcess(tty bool, volume, containerId string) (*exec.Cmd, *os.File) {
// 省略其他記憶體
if tty {
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
} else {
// 對於後臺執行容器,將 stdout、stderr 重定向到日誌檔案中,便於後續檢視
dirPath := fmt.Sprintf(InfoLocFormat, containerId)
if err := os.MkdirAll(dirURL, constant.Perm0622); err != nil {
log.Errorf("NewParentProcess mkdir %s error %v", dirURL, err)
return nil, nil
}
stdLogFilePath := dirPath + LogFile
stdLogFile, err := os.Create(stdLogFilePath)
if err != nil {
log.Errorf("NewParentProcess create file %s error %v", stdLogFilePath, err)
return nil, nil
}
cmd.Stdout = stdLogFile
cmd.Stderr = stdLogFile
}
// ...
}
實現 logs 命令
在 main_command.go 中新增一個 logCommand:
var logCommand = cli.Command{
Name: "logs",
Usage: "print logs of a container",
Action: func(context *cli.Context) error {
if len(context.Args()) < 1 {
return fmt.Errorf("please input your container name")
}
containerName := context.Args().Get(0)
logContainer(containerName)
return nil
},
}
並加到 main 函式中。
func main(){
// 省略其他內容
app.Commands = []cli.Command{
initCommand,
runCommand,
commitCommand,
listCommand,
logCommand,
}
}
具體實現如下:
func logContainer(containerName string) {
logFileLocation := fmt.Sprintf(container.InfoLocFormat, containerName) + container.LogFile
file, err := os.Open(logFileLocation)
defer file.Close()
if err != nil {
log.Errorf("Log container open file %s error %v", logFileLocation, err)
return
}
content, err := ioutil.ReadAll(file)
if err != nil {
log.Errorf("Log container read file %s error %v", logFileLocation, err)
return
}
_, err = fmt.Fprint(os.Stdout, string(content))
if err != nil {
log.Errorf("Log container Fprint error %v", err)
return
}
}
實現很簡單,根據 containerId 拼接出完整路徑,讀取檔案內容並重定向到標準輸出即可。
3. 測試
啟動一個後臺容器
root@mydocker:~/feat-logs/mydocker# go build .
root@mydocker:~/feat-logs/mydocker# ./mydocker run -d -name mytop top
{"level":"info","msg":"createTty false","time":"2024-01-26T11:25:53+08:00"}
{"level":"info","msg":"resConf:\u0026{ 0 }","time":"2024-01-26T11:25:53+08:00"}
{"level":"info","msg":"busybox:/root/busybox busybox.tar:/root/busybox.tar","time":"2024-01-26T11:25:53+08:00"}
{"level":"error","msg":"mkdir dir /root/merged error. mkdir /root/merged: file exists","time":"2024-01-26T11:25:53+08:00"}
{"level":"error","msg":"mkdir dir /root/upper error. mkdir /root/upper: file exists","time":"2024-01-26T11:25:53+08:00"}
{"level":"error","msg":"mkdir dir /root/work error. mkdir /root/work: file exists","time":"2024-01-26T11:25:53+08:00"}
{"level":"info","msg":"mount overlayfs: [/usr/bin/mount -t overlay overlay -o lowerdir=/root/busybox,upperdir=/root/upper,workdir=/root/work /root/merged]","time":"2024-01-26T11:25:53+08:00"}
{"level":"info","msg":"command all is top","time":"2024-01-26T11:25:53+08:00"}
檢視容器列表
root@mydocker:~/feat-logs/mydocker# ./mydocker ps
{"level":"error","msg":"read file /var/lib/mydocker/containers/0439540405/config.json error open /var/lib/mydocker/containers/0439540405/config.json: no such file or directory","time":"2024-01-26T11:26:13+08:00"}
{"level":"error","msg":"get container info error open /var/lib/mydocker/containers/0439540405/config.json: no such file or directory","time":"2024-01-26T11:26:13+08:00"}
ID NAME PID STATUS COMMAND CREATED
0439540405 mytop 171754 running top 2024-01-26 11:25:53
容器 Id 為 0439540405,檢視對應目錄是否生成了日誌檔案
root@mydocker:~/feat-logs/mydocker# ls /var/lib/mydocker/containers/0439540405/
0439540405-json.log config.json
其中的0439540405-json.log
就是日誌檔案,config.json
則是上一次新增的容器資訊記錄檔案。
檢視日誌檔案內容
root@mydocker:~/feat-logs/mydocker# cat /var/lib/mydocker/containers/0439540405/0439540405-json.log
Mem: 1793456K used, 241932K free, 1064K shrd, 91276K buff, 1354272K cached
CPU: 0.4% usr 0.0% sys 0.0% nic 99.3% idle 0.0% io 0.0% irq 0.2% sirq
Load average: 0.01 0.01 0.00 1/141 4
PID PPID USER STAT VSZ %VSZ CPU %CPU COMMAND
說明,日誌儲存是正常的。
接下里執行 mydocker logs
看看是否能查詢到日誌
root@mydocker:~/feat-logs/mydocker# ./mydocker logs 0439540405
Mem: 1793424K used, 241964K free, 1064K shrd, 91316K buff, 1354280K cached
CPU: 0.0% usr 0.0% sys 0.0% nic 100% idle 0.0% io 0.0% irq 0.0% sirq
Load average: 0.00 0.00 0.00 1/141 4
PID PPID USER STAT VSZ %VSZ CPU %CPU COMMAND
可以看到,mydocker logs 命令成功執行並輸出了容器的日誌。
至此,說明我們的 mydocker logs 命令實現是 ok 的。
4. 小結
本篇主要實現 mydocker logs
命令,和 docker 實現基本類似:
- 容器啟動把 stdout、stderr 重定向到
/var/lib/mydocker/container/{containerId}/{containerId}-json.log
檔案 - 執行 mydocker logs 則根據容器 Id 找到對應檔案,讀取檔案內容並列印
【從零開始寫 Docker 系列】持續更新中,搜尋公眾號【探索雲原生】訂閱,閱讀更多文章。
完整程式碼見:https://github.com/lixd/mydocker
歡迎關注~
相關程式碼見 feat-logs
分支,測試指令碼如下:
需要提前在 /root 目錄準備好 busybox.tar 檔案,具體見第四篇第二節。
# 克隆程式碼
git clone -b feat-logs https://github.com/lixd/mydocker.git
cd mydocker
# 拉取依賴並編譯
go mod tidy
go build .
# 測試
./mydocker run -d -name c1 top
# 檢視容器 Id
./mydocker ps
# 根據 Id 查詢日誌
./mydocker logs ${containerId}