容器程式Core Dump處理

小米運維發表於2019-03-07

本文主要介紹了Core Dump實現容器程式的方法和相關內容。

上篇文章回顧:IPv6入門教程

引子

在我們除錯程式時經常會使用到Core Dump()功能,使用偵錯程式(如gdb())分析其產生的Core Dump檔案(以下稱"core檔案"),對於排查問題、定位bug堪稱無往不利的利器。當前容器技術使用愈加普遍,線上大量業務使用容器技術部署,那我們的業務程式在容器環境下core檔案是如何產生、與在宿主機中有什麼不同呢?本文針對這個問題簡略說明,拋磚引玉。

什麼是Core檔案

Core檔案是當一個程式在收到某些訊號後終止時產生的檔案,其中包含程式終止時刻程式記憶體的映象。我們可以使用gdb從該映象中觀察程式終止時處於什麼狀態,用於追蹤排查定位問題。

如下示例,其中/usr/share/core_pipe/test是crash程式,core.29873就是core檔案,其中29873是crash程式的PID。

# gdb /usr/share/core_pipe/test core.29873GNU gdb (Ubuntu 8.1-0ubuntu3) 8.1.0.20180409-gitCopyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from /usr/share/core_pipe/test...done.
[New LWP 34]

warning: .dynamic section for "/lib64/ld-linux-x86-64.so.2" is not at the expected address (wrong library or version mismatch?)warning: Could not load shared library symbols for /lib64/libc.so.6.
Do you need "set solib-search-path" or "set sysroot"?
Core was generated by `./test'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x0000556039e756aa in main () at main.c:7
7     printf("this is null p %d \n", *intp)
;
(gdb)

宿主機程式Core Dump

在講容器程式Core Dump之前,我們先來簡單回顧下宿主機程式Core Dump在什麼條件下產生。這裡的宿主機泛指物理機和虛擬主機,宿主機程式特指執行在系統主機Initial Namespace的程式,

容器程式特指執行在容器Namespace中的程式。

前文說到當程式收到某種訊號才會產生,那麼什麼才是"某種訊號"呢?這些訊號大約有十多個,這裡只列出我們常見比較熟悉的幾個:

- SIGQUIT  數值2  從鍵盤輸入 Ctrl+'\'可以產生此訊號

- SIGILL   數值4  非法指令

- SIGABRT  數值6  abort呼叫

- SIGSEGV  數值11 非法記憶體訪問

- SIGTRAP  數值5  除錯程式時使用的斷點

其中SIGSEGV應該是我們最常接觸的,尤其是使用C/C++程式的同學更是常見。

main.c
#include <stdio.h>int main(){  int *p = NULL;  
 printf("hello world! \n");
 printf("this will cause core dump p %d", *p);
}

使用下面命名編譯即可產生執行時觸發非法記憶體訪問SIGSEGV訊號,其中-g選項是編譯新增除錯資訊,對於gdb除錯非常有用。

# gcc -g main.c -o test

除了上述產生訊號的方法外,使用我們經常使用的kill命令可以非常方便的產生這些訊號,另外還有gcore命令可以在不終止程式的前提下產生core檔案。

那麼只要程式收到這些訊號就一定會有core檔案嗎?顯然不是,linux在這方面提供了相關配置。那麼這些配置都有哪些呢?

1設定Core檔案大小上限

ulimit
# ulimit -c 0 // 0表示不產生core檔案# ulimit -c 100 // 100表示設定core檔案最大為100k,當然可以根據自己需要設定,注意設定單位是KB# ulimit -c unlimited // 不限制core檔案大小

使用上述命令設定core檔案大小隻對當前shell環境有效,系統重啟或者退出當前shell設定就會失效,恢復預設設定。

若想永久生效可以把該shell放到/etc/profile檔案中,系統重新啟動初始化環境時會執行其中的命令,該設定就會在全域性範圍內生效,達到永久生效的效果。也可以使用 source /etc/profile命令立即全域性生效。

# echo "unlimit -c unlimited" >> /etc/profile // 配置新增到/etc/profile中# source /etc/profile // 立即生效

2設定Core檔案儲存目錄

# echo "/var/core-dir/core-%e-%p-%t" >  /proc/sys/kernel/core_pattern

該命令可以控制core檔案儲存位置和檔名格式。注意需要使用root許可權執行,並且儲存路徑必須是絕對路徑,不能是相對路徑

其中%e表示新增使用者程式名稱,%p表示新增程式程式PID,%t表示新增產生core檔案時的時間戳,還有其他一些非常有用的格式,可以參閱CORE(5)文件。

這樣修改後系統重啟後也會消失,同樣也有永久生效的辦法

修改/etc/sysctl.conf檔案在其中修改或者新增

/etc/sysctl.conf
kernel.core_pattern = /var/core-dir/core-%e-%p-%t

然後執行下面命令配置立即生效,這樣系統重啟後配置依然有效

# sysctl –p /etc/sysctl.conf

3宿主機Core檔案小結

宿主機產生core檔案可以使用如下步驟

1. ulimit -c 命令設定core檔案大小2. 修改core_pattern選項配置core檔案儲存目錄

具備上述兩個條件後,當程式在一定條件core後,就會儲存core檔案到我們配置的目錄中。


Core檔案產生流程


大概說一下從程式出現異常到生成core檔案的流程,不會涉及太多Linux系統實現細節,具體細節可以參考相關文件和linux核心原始碼。

  1. 程式執行時發生一個異常,比如非法記憶體地址訪問(即段錯誤),相應硬體會上報該異常,CPU檢測到該異常時會進入異常處理流程,包括儲存當前上下文,跳轉到對應的中斷向量執行入口等

  2. 在異常處理流程中如果判斷該異常發生時是處於使用者態,則該異常只會影響當前程式,此時向使用者態程式傳送相應的訊號,如段錯誤就會傳送SIGSEGV訊號

  3. 當使用者態程式被排程時會檢查pending的訊號,如果發現pending的訊號是SIG_KERNEL_COREDUMP_MASK中的一個,就會進入core檔案內容收集儲存流程,然後根據配置(core_pattern等)生成core檔案

容器程式Core Dump

那麼如果我們要收集在容器裡執行的程式的core檔案應該如何設定呢?

答案是上述宿主機針對core檔案的設定對容器中的程式依然有效。

眾所周知,宿主機上所有的容器都是共享系統核心的,/proc/sys檔案下只有一小部分支援namespace隔離,而core_pattern恰巧不支援隔離的,所以無論是從宿主機還是容器裡修改core_pattern,最終修改的是同一個設定,並且全域性生效,不管是對宿主機還是對容器都是有效的。

一般情況下每個容器都有自己的mount namespace(),其中的檔案系統與宿主機和其他容器相隔離,那麼在core_pattern指定的core檔案儲存目錄是容器中的檔案目錄還是宿主機中呢?不妨推測一二,剛才我們已經說過這個core_pattern是全域性生效,如果該目錄是針對某個容器的檔案目錄,那麼肯定是不合理的,因為如果宿主機上程式Core Dump時就會找不到對應的目錄,無法儲存。

實際上有效的core_pattern中的目錄必須是宿主機中的絕對目錄,更準確的描述是宿主機Initial Namespace中的絕對路徑。

另外一個問題是,每個容器都有自己pid namespace(),我們再core_pattern中設定的獲取crash程式的各種資訊比如PID,可執行檔名,是容器namespace中的還是宿主機namespace中的呢?從相關文件和實驗得知,可以同時獲取crash程式在容器中的PID(透過%p格式指定)和在宿主機Initial Namespace中的PID(透過%P格式指定),可執行檔名稱(透過%e或%E格式指定)是容器的namespace中的。

之所以造成上述情況,根本原因是Core Dump流程中核心程式最後負責處理core檔案儲存的,而核心程式執行在宿主機Initial Namespace中,實際上所有的容器程式在宿主機Initial Namespace都有對映,對核心來講,宿主機程式和容器程式可以統一處理,並沒有本質區別。

1使用管道解決容器程式Core Dummp問題

上文中我們得知了容器程式core檔案產生的方法,但是有一個問題就是上述方法的設定是對宿主機和容器內所有的程式都生效的。無法針對特定容器程式特定設定。比如說我們希望宿主機程式core檔案儲存到/var/crash目錄,而對容器的core檔案儲存在/var/container/crash目錄,或者我要限制某個容器產生core檔案的總儲存大小,而不是單個core檔案的大小;如果我們做一個服務平臺對其他使用者開放Core Dump功能的話,我們肯定還希望獲取一下crash程式的其他額外資訊比如程式當前環境變數、當前使用者、當前程式有效UID和GID、任務名稱屬性;如果我們希望針對core事件進行統計分析的話,可能還需要各種回撥通知等等操作。

顯然上述簡單的設定core檔案儲存目錄的方法無法滿足我們的需求的,那麼我們還有另外一個選擇,就是使用linux的piping技術轉儲core檔案。

從linux核心版本2.6.19之後,核心就開支支援在/proc/sys/kernel/core_pattern檔案中指定一個管道程式來實際處理core檔案儲存。core檔案內容會作為該管道程式的標準輸入傳輸給管道程式,管道程式就接管了接下來的core檔案內容的所有處理。如下設定可以使用piping技術轉儲core檔案

# echo "|/usr/share/core_pipe/core_pipe core -H=%h -p=%p -i=%i -s=%s  -c=%c > /proc/sys/kernel/core_pattern
# cat /proc/sys/kernel/core_pattern
|/usr/share/core_pipe/core_pipe core -H=%h -p=%p -i=%i -s=%s  -c=%c

其中/usr/share/core_pipe/core_pipe是我們的管道程式,需要注意的是必須以|開發, |之後必須緊接管道程式的路徑,沒有空格。當有程式core時,就會呼叫該管道程式進行處理。

我們可以開發自己的管道處理程式,從管道程式啟動的引數獲取crash的程式資訊,從管道程式的標準輸入獲取core檔案的內容。

我們現在知曉該管道程式什麼時候被呼叫(程式Core Dump時),那麼管道程式是由誰來呼叫呢?

既然管道程式是我們自己開發的,我們就可以獲取管道程式的父程式是誰,也就是被誰呼叫的,透過實驗我們可一知道父程式的PID是2,當我們再看該程式的父程式是誰:

# ps -ef -q 2UID         PID   PPID  C STIME TTY          TIME CMD
root          2      0  0 Jan20 ?        00:00:00 [kthreadd]

程式PID2的父程式是PID 0,而PID 0代表的是linux系統核心idle程式,Linux系統中共有三個特殊程式,分別是idle(PID 0), init(PID 1), kthreadd(PID 2),而kthreadd是所有核心程式的父程式,也就是說我們的管道程式是作為核心執行緒在執行的,執行在核心態,並且在宿主機Initial Namespace中以root使用者身份執行,不在任何容器內。

2Socket Activation應用到容器程式Core Dummp

上文說了管道程式執行在核心態,而且是在宿主機的Initial Namespace中執行,容器的各種限制對其不起作用,比如core檔案大小有可能超過容器的硬碟空間限制。當然我們管道程式可以透過crash程式的PID拿到crash程式的容器namespace以及各種cgroup限制,然後針對性處理。這樣顯然對容器極有侵入性,程式碼寫起來也不夠優雅。如果處理core檔案儲存程式在容器中執行,就能較優雅的解決好這個問題。管道程式已經作為核心執行緒執行在宿主機的Initial Namespace了,雖然有辦法可以動態的加入和退出某個namespace和cgroup,但是考慮的邊界條件多,易出錯,並不優雅。 

如果管道程式能夠和容器內某個程式進行互動,可以解決上述問題,同一個宿主機程式通訊的方式有很多,比如共享記憶體,管道,訊息佇列等。但是這裡的兩個程式是分佈在不同的namespace中,而且彼此並不知道什麼時候可以互動,我們為了低機率的core檔案長時間讓容器內某個程式空跑佔用資源嗎?那麼socket activation技術可以用來解決這個問題。

socket activation並不是一種新技術,其技術理念和原理早就被應用到Linux和MacOS中,關於socket activation技術原理細節又是需要另一篇的長篇大論,這裡暫且不再詳述,簡單來說,就是由系統init程式(對於目前大多數linux系統來說是systemd)來為普通應用程式監聽特定socket,此時應用程式並未啟動,當有連線到達該socket後,由init程式接管該連線並跟進配置檔案啟動相應的應用程式,然後把連線傳遞給應用程式來處理,主要好處是當沒有連線到達時,應用程式無需常駐後臺空跑耗費系統資源。非常適合像Core Dump這種低頻服務。

我們可以設定一個unix socket來把管道程式的檔案描述符傳遞到容器內程式,完成傳遞後, 管道程式就可以退出,由容器內程式處理core檔案的儲存。

下面是一個socket activation示例,其中/usr/share/core_pipe/core_pipe是我們的core 檔案處理程式, /run/core_pipe.socket是我們unix socket檔案,存在容器中,該檔案我們在Initial Namespace中的管道程式可以透過/proc/${crash pid}/root/run/core_pipe.socket拿到,然後與之互動。

core_pipe-forward.socket
# 此為Unit檔案,儲存內容為檔案到 /etc/systemd/system/core_pipe-forward.socket
[Unit]
Description=Unix socket for core_pipe crash forwarding
ConditionVirtualization=container

[Socket]
ListenStream=/run/core_pipe.socket
SocketMode=0600Accept=yes
MaxConnections=10Backlog=5PassCredentials=true[Install]
WantedBy=sockets.target
# 此為service檔案,儲存內容到 /etc/systemd/system/core_pipe-forward.service
[Unit]
Description=Core Pipe crash forwarding receiver
Requires=core_pipe-forward.socket

[Service]
Type=oneshot
ExecStart=/usr/share/core_pipe/core_pipe

core_pipe-forward.socket

執行下面命令使得socket生效

# systemctl enable core_pipe-forward.socket
# systemctl start core_pipe-forward.socket
# systemctl status core_pipe-forward.socket

上述命令如果是在容器內的init程式不是systemd情況下會出錯,大多數情況下容器內的init程式並不是systemd,此時可以退一步使用容器內常駐程式的方式來實現core檔案的處理。

總結

本文簡單說明了實現容器程式Core Dump的方法,概況一下主要有三點:

  1. 使用ulimit -c和/proc/sys/kernel/core_pattern設定Core Dump檔案大小限制和儲存位置

  2. 使用管道程式增強Core Dump檔案處理能力

  3. 使用管道程式和容器內程式結合的方式完成核心態轉到使用者態,在容器內處理Core檔案儲存

參考文獻:

CORE(5) 

GETRLIMIT(2)

GDB(1)

KILL(2)

SIGNAL(7)

NAMESPACE(7)

BASH(1) 

go-systemd  

systemd-socket-activation-in go 

Core Dump流程分析

https://blog.csdn.net/omnispace/article/details/77600721

Socket activation in systemd 

socket activation 

http://0pointer.de/blog/projects/socket-activation2.htm

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31559359/viewspace-2637839/,如需轉載,請註明出處,否則將追究法律責任。

相關文章