Kubernetes 故障檢測和自愈工具 NPD
節(jié)點問題檢測器(Node Problem Detector)是一個守護程序,用于監(jiān)視和報告節(jié)點的健康狀況(包括內(nèi)核死鎖、OOM、系統(tǒng)線程數(shù)壓力、系統(tǒng)文件描述符壓力等指標)。你可以將節(jié)點問題探測器以 DaemonSet 或獨立守護程序運行。節(jié)點問題檢測器從各種守護進程收集節(jié)點問題,并以 NodeCondition 和 Event 的形式報告給 API Server。您可以通過檢測相應(yīng)的指標,提前預(yù)知節(jié)點的資源壓力,可以在節(jié)點開始驅(qū)逐 Pod 之前手動釋放或擴容節(jié)點資源壓力,防止 Kubenetes 進行資源回收或節(jié)點不可用可能帶來的損失。
?Git 倉庫地址:https://github.com/kubernetes/node-problem-detector
Kubernetes 目前問題
基礎(chǔ)架構(gòu)守護程序問題:ntp服務(wù)關(guān)閉; 硬件問題:CPU,內(nèi)存或磁盤損壞; 內(nèi)核問題:內(nèi)核死鎖,文件系統(tǒng)損壞; 容器運行時問題:運行時守護程序無響應(yīng) ...
當kubernetes中節(jié)點發(fā)生上述問題,在整個集群中,k8s服務(wù)組件并不會感知以上問題,就會導致pod仍會調(diào)度至問題節(jié)點。
為了解決這個問題,我們引入了這個新的守護進程node-problem-detector,從各個守護進程收集節(jié)點問題,并使它們對上游層可見。一旦上游層面發(fā)現(xiàn)了這些問題,我們就可以討論補救措施。
NPD 使用
構(gòu)建
NPD使用Go modules管理依賴,因此構(gòu)建它需要Go SDK 1.11+:
cd $GOPATH/src/k8s.io
go get k8s.io/node-problem-detector
cd node-problem-detector
export GO111MODULE=on
go mod vendor
# 設(shè)置構(gòu)建標記
export BUILD_TAGS="disable_custom_plugin_monitor disable_system_stats_monitor"
# 在Ubuntu 14.04上需要安裝
sudo apt install libsystemd-journal-dev
make all
安裝
# add repo
helm repo add feisky https://feisky.xyz/kubernetes-charts
helm update
# install packages
helm install feisky/node-problem-detector --namespace kube-system --name npd
啟動參數(shù)
--version: 在控制臺打印 NPD 的版本號. --hostname-override: 供 NPD 使用的自定義的節(jié)點名稱,NPD 會優(yōu)先獲取該參數(shù)設(shè)置的節(jié)點名稱,其次是從 NODE_NAME 環(huán)境變量中獲取,最后從 os.Hostname() 方法獲取。
system-log-monitor 相關(guān)參數(shù)
--config.system-log-monitor: system log monitor 配置文件路徑,多個文件用逗號分隔, 如 config/kernel-monitor.json. NPD 會為每一個配置文件生成單獨的 log monitor。你可以使用不同的 log monitors 來監(jiān)控不同的系統(tǒng)日志。
system-stats-monitor 相關(guān)參數(shù)
--config.system-stats-monitor: system status monitor 配置文件路徑,多個文件用逗號分隔, 如 config/system-stats-monitor.json. NPD 會為每一個配置文件生成單獨的 status monitor。你可以使用不同的 status monitors 來監(jiān)控系統(tǒng)的不同狀態(tài)。
custom-plugin-monitor 相關(guān)參數(shù)
--config.custom-plugin-monitor: 用戶自定義插件配置文件路徑,多個文件用逗號分隔, 如 config/custom-plugin-monitor.json. NPD 會為每一個配置文件生成單獨的自定義插件監(jiān)視器。你可以使用不同的自定義插件監(jiān)視器來監(jiān)控不同的系統(tǒng)問題。
K8s exporter 相關(guān)參數(shù)
--enable-k8s-exporter: 是否開啟上報信息到 API Server,默認為 true.
--apiserver-override: 一個URI參數(shù),用于自定義node-problem-detector連接apiserver的地址。如果--enable-k8s-exporter為false,則忽略此內(nèi)容。格式與Heapster的源標志相同。例如,要在沒有身份驗證的情況下運行,請使用以下配置:http://APISERVER_IP:APISERVER_PORT?inClusterConfig=false
請參閱 heapster 文檔以獲取可用選項的完整列表。
--address: 綁定 NPD 服務(wù)器的地址。
--port: NPD 服務(wù)端口,如果為0,表示禁用 NPD 服務(wù)。
Prometheus exporter 相關(guān)參數(shù)
--prometheus-address: 綁定Prometheus抓取端點的地址,默認為127.0.0.1。 --prometheus-port: 綁定Prometheus抓取端點的端口,默認為20257。使用0禁用。
Stackdriver exporter 相關(guān)參數(shù)
--exporter.stackdriver: Stackdriver exporter程序配置文件的路徑,例如 config/exporter/stackdriver-exporter.json,默認為空字符串。設(shè)置為空字符串以禁用。
過期參數(shù)
--system-log-monitors: system log monitor 配置文件路徑,多個文件用逗號分隔。該選項已過期, 被 --config.system-log-monitor 取代, 即將被移除. 如果在啟動NPD時同時設(shè)置了 --system-log-monitors 和 --config.system-log-monitor,會引發(fā)panic。 --custom-plugin-monitors: 用戶自定義插件配置文件路徑,多個文件用逗號分隔。該選項已過期, 被 --config.custom-plugin-monitor 取代, 即將被移除. 如果在啟動NPD時同時設(shè)置了 --custom-plugin-monitors 和 --config.custom-plugin-monitor,會引發(fā)panic。
覆蓋配置文件
構(gòu)建節(jié)點問題檢測器的 docker 鏡像時,會嵌入 默認配置。
不過,你可以像下面這樣使用 ConfigMap 將其覆蓋:
1、更改 config/ 中的配置文件
2、創(chuàng)建 ConfigMap node-strick-detector-config:
kubectl create configmap node-problem-detector-config --from-file=config/
3、更改 node-problem-detector.yaml 以使用 ConfigMap:
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-problem-detector-v0.1
namespace: kube-system
labels:
k8s-app: node-problem-detector
version: v0.1
kubernetes.io/cluster-service: "true"
spec:
selector:
matchLabels:
k8s-app: node-problem-detector
version: v0.1
kubernetes.io/cluster-service: "true"
template:
metadata:
labels:
k8s-app: node-problem-detector
version: v0.1
kubernetes.io/cluster-service: "true"
spec:
hostNetwork: true
containers:
- name: node-problem-detector
image: k8s.gcr.io/node-problem-detector:v0.1
securityContext:
privileged: true
resources:
limits:
cpu: "200m"
memory: "100Mi"
requests:
cpu: "20m"
memory: "20Mi"
volumeMounts:
- name: log
mountPath: /log
readOnly: true
- name: config # Overwrite the config/ directory with ConfigMap volume
mountPath: /config
readOnly: true
volumes:
- name: log
hostPath:
path: /var/log/
- name: config # Define ConfigMap volume
configMap:
name: node-problem-detector-config
4、使用新的配置文件重新創(chuàng)建節(jié)點問題檢測器:
說明: 此方法僅適用于通過 kubectl 啟動的節(jié)點問題檢測器。
如果節(jié)點問題檢測器作為集群插件運行,則不支持覆蓋配置。插件管理器不支持 ConfigMap。
如何驗證NPD捕獲信息
通常這些錯誤是比較難真實測試,只能通過發(fā)送消息到j(luò)ournal來模擬。
發(fā)送一個kernel deadlock類型的condition:在對應(yīng)的node節(jié)點上執(zhí)行以下操作
echo "task docker:7 blocked for more than 300 seconds." |systemd-cat -t kernel
然后通過k8s控制臺,你可以看到對應(yīng)的信息:

發(fā)送一個event
echo "Error trying v2 registry: failed to register layer: rename /var/lib/docker/image/test /var/lib/docker/image/ddd: directory not empty.*" |systemd-cat -t docker
然后通過以下命令來對應(yīng)的event
kubectl describe node/xxxx

實現(xiàn)原理
核心組件
Problem Daemon(Monitor)
Problem Daemon 是監(jiān)控任務(wù)子守護進程,NPD 會為每一個 Problem Daemon 配置文件創(chuàng)建一個守護進程,這些配置文件通過 --config.custom-plugin-monitor、--config.system-log-monitor、--config.system-stats-monitor 參數(shù)指定。每個 Problem Daemon監(jiān)控一個特定類型的節(jié)點故障,并報告給NPD。目前 Problem Daemon 以 Goroutine 的形式運行在NPD中,未來會支持在獨立進程(容器)中運行并編排為一個Pod。在編譯期間,可以通過相應(yīng)的標記禁用每一類 Problem Daemon。
custom-plugin-monitor:用戶自定義的 Problem Daemon system-log-monitor:系統(tǒng)日志監(jiān)控 system-stats-monitor:系統(tǒng)狀態(tài)監(jiān)控
ProblemDaemonHandler
ProblemDaemonHandler 定義了 Problem Daemon 的初始化方法
type ProblemDaemonHandler struct {
// 初始化 Problem Daemon實例,如果初始化過程中出錯,則拋出 panic
CreateProblemDaemonOrDie func(string) Monitor
// 說明了從命令行參數(shù)配置 Problem Daemon 的方式
CmdOptionDescription string
}
在NPD啟動時,init()方法中完成了 ProblemDaemonHandler 的注冊:
var (
// 在 NPD 啟動過程中,通過 init() 方法注冊
handlers = make(map[types.ProblemDaemonType]types.ProblemDaemonHandler)
)
// 注冊 problem daemon 工廠方法,將會用于創(chuàng)建 problem daemon
func Register(problemDaemonType types.ProblemDaemonType, handler types.ProblemDaemonHandler) {
handlers[problemDaemonType] = handler
}
Exporter
Exporter 用于上報節(jié)點健康信息到某種控制面。在 NPD 啟動時,會根據(jù)需求初始化并啟動各種 Exporter。Exporter 分為三類:
K8s Exporter:會將節(jié)點健康信息上報到 API Server。 Prometheus Exporter:負責上報節(jié)點指標信息到 Prometheus。 Plugable Exporters:可插拔的 Exporter(如 Stackdriver Exporter),我們也可以自定義 Exporter,并在 init() 方法中注冊,這樣在 NPD 啟動時就會自動初始化并啟動。
ExporterHandler
ExporterHandler 和 ProblemDaemonHandler 功能類似,其定義了 Exporter 的初始化方法。也是在NPD啟動時,init()方法中完成了 ExporterHandler 的注冊
type ExporterHandler struct {
// CreateExporterOrDie initializes an exporter, panic if error occurs.
CreateExporterOrDie func(CommandLineOptions) Exporter
// CmdOptionDescription explains how to configure the exporter from command line arguments.
Options CommandLineOptions
}
Condition Manager
K8s Exporter 獲取到的異常 Condition 信息會上報給 Condition Manager, Condition Manager 每秒檢查 Condition 的變化,并同步到 API Server 的 Node 對象中。
Problem Client
Problem Client 負責與 API Server 交互,并將巡檢過程中生成的 Events 和 Conditions 上報給 API Server。
type Client interface {
// 從 API Server 獲取當前節(jié)點所有指定類型的 Conditions
GetConditions(conditionTypes []v1.NodeConditionType) ([]*v1.NodeCondition, error)
// 調(diào)用 API Server 接口更新當前節(jié)點的 Condition 列表
SetConditions(conditions []v1.NodeCondition) error
// 上報 Event 信息到 API Server
Eventf(eventType string, source, reason, messageFmt string, args ...interface{})
// 從 API Server 獲取當前 node-problem-detector 實例所在的節(jié)點信息
GetNode() (*v1.Node, error)
}
Problem Detector
Problem Detector 是 NPD 的核心對象,它負責啟動所有的 Problem Daemon(也可以叫做 Monitor),并利用 channel 收集 Problem Daemon 中發(fā)現(xiàn)的異常信息,然后將異常信息提交給 Exporter,Exporter 負責將這些異常信息上報到指定的控制面(如 API Server、Prometheus、Stackdriver等)。
Status
Status 是 Problem Daemon 向 Exporter 上報的異常信息對象。
type Status struct {
// problem daemon 的名稱
Source string `json:"source"`
// 臨時的節(jié)點問題 —— 事件對象,如果此Status用于Condition更新則此字段可以為空
// 從老到新排列在數(shù)組中
Events []Event `json:"events"`
// 永久的節(jié)點問題 —— NodeCondition。PD必須總是在此字段報告最新的Condition
Conditions []Condition `json:"conditions"`
}
Tomb
用于從外部控制協(xié)程的生命周期, 它的邏輯很簡單,準備結(jié)束生命周期時:
外部協(xié)作者發(fā)起一個通知 協(xié)作線程接收到通知,進行清理 清理完成后,協(xié)程反向通知外部協(xié)作者 外部協(xié)作者退出阻塞
啟動過程

NPD 啟動過程完成的工作有:
打印 NPD 版本號 設(shè)置節(jié)點名稱,優(yōu)先使用命令行中設(shè)置的節(jié)點名稱,其次是環(huán)境變量 NODE_NAME 中的節(jié)點名稱,最次是 os.Hostname() 校驗命令行參數(shù)的合法性 初始化 problem daemons 初始化默認 Exporters(包含 K8s Exporter、Prometheus Exporter)和可插拔 Exporters(如 Stackdriver Exporter) 使用 problem daemons 和 Exporters 構(gòu)建 Problem Detector,并啟動
檢測流程

節(jié)點自愈
采集節(jié)點的健康狀態(tài)是為了能夠在業(yè)務(wù)Pod不可用之前提前發(fā)現(xiàn)節(jié)點異常,從而運維或開發(fā)人員可以對Docker、Kubelet或節(jié)點進行修復(fù)。在NPDPlus中,為了減輕運維人員的負擔,提供了根據(jù)采集到的節(jié)點狀態(tài)從而進行不同自愈動作的能力。集群管理員可以根據(jù)節(jié)點不同的狀態(tài)配置相應(yīng)的自愈能力,如重啟Docker、重啟Kubelet或重啟CVM節(jié)點等。同時為了防止集群中的節(jié)點雪崩,在執(zhí)行自愈動作之前做了嚴格的限流,防止節(jié)點大規(guī)模重啟。同時為了防止集群中的節(jié)點雪崩,在執(zhí)行自愈動作之前做了嚴格的限流。具體策略為:
在同一時刻只允許集群中的一個節(jié)點進行自愈行為,并且兩個自愈行為之間至少間隔1分鐘
當有新節(jié)點添加到集群中時,會給節(jié)點2分鐘的容忍時間,防止由于節(jié)點剛剛添加到集群的不穩(wěn)定性導致錯誤自愈

custom-plugin-monitor
此Problem Daemon為NPD提供了一種插件化機制,允許基于任何語言來編寫監(jiān)控腳本,只需要這些腳本遵循NPD關(guān)于退出碼和標準輸出的規(guī)范。通過調(diào)用用戶配置的腳本來檢測各種節(jié)點問題
腳本退出碼:
0:對于Evnet來說表示Normal,對于NodeCondition表示False 1:對于Evnet來說表示W(wǎng)arning,對于NodeCondition表示True
腳本輸出應(yīng)該小于80字節(jié),避免給Etcd的存儲造成壓力
使用標記禁用:disable_custom_plugin_monitor
示例
{
"plugin": "custom", // 插件類型
"pluginConfig": { // 插件配置
"invoke_interval": "10s", // 執(zhí)行時間間隔
"timeout": "3m", // 健康檢查超時時間
"max_output_length": 80,
"concurrency": 1 // 并行度
},
"source": "health-checker", // 事件源
"metricsReporting": true, // 是否上報指標信息
"conditions": [ // 發(fā)現(xiàn)異常后在 Node 中設(shè)置的 Condition 信息
{
"type": "KubeletUnhealthy",
"reason": "KubeletIsHealthy",
"message": "kubelet on the node is functioning properly"
}
],
"rules": [ // 巡檢規(guī)則
{
"type": "permanent",
"condition": "KubeletUnhealthy",
"reason": "KubeletUnhealthy",
"path": "/home/kubernetes/bin/health-checker", // 二進制文件路徑
"args": [ // 二進制文件啟動參數(shù)
"--component=kubelet",
"--enable-repair=true",// 是否啟用自愈,自愈會嘗試重啟組件
"--cooldown-time=1m", // 冷卻時間,組件啟動后的一段時間為冷卻時間,冷卻時間能如果發(fā)現(xiàn)異常,不會嘗試自愈
"--loopback-time=0",// 要回溯的 journal 日志的時間,如果為0,則從組件啟動時間開始回溯
"--health-check-timeout=10s" // 健康檢查超時時間
],
"timeout": "3m" // 巡檢超時時間
}
]
}
plugin
plugin 是NPD或用戶自定義的一些異常檢查程序,可以用任意語言編寫。custom-plugin-monitor 在執(zhí)行過程中會執(zhí)行這些異常檢測程序,并根據(jù)返回結(jié)果來判斷是否存在異常。NPD提供了三個 plugin,分別是:
health-check:檢查kubelet、docker、kube-proxy、cri等進程是否健康。 log-counter:依賴的插件是 journald,其作用是統(tǒng)計指定的 journal 日志中近一段時間滿足正則匹配的歷史日志條數(shù)。 network_problem.sh:檢查 conntrack table 的使用率是否超過 90%。
health-checker
命令行參數(shù)
| 參數(shù)名稱 | 參數(shù)說明 | 默認值 |
|---|---|---|
| systemd-service | 與 --service 相同,已被 --service 取代 | |
| service | The underlying service responsible for the component. Set to the corresponding component for docker and kubelet, containerd for cri. | |
| loopback-time | The duration to loop back, if it is 0, health-check will check from start time. | 0min |
| log-pattern | The log pattern to look for in service journald logs. The format for flag value | |
| health-check-timeout | The time to wait before marking the component as unhealthy. | 10s |
| enable-repair | Flag to enable/disable repair attempt for the component. | true |
| crictl-path | The path to the crictl binary. This is used to check health of cri component. | Linux:/usr/bin/crictl Windows:C:/etc/kubernetes/node/bin/crictl.exe |
| cri-socket-path | The path to the cri socket. Used with crictl to specify the socket path. | Linux:unix:///var/run/containerd/containerd.sock Windows:npipe:////./pipe/containerd-containerd |
| cooldown-time | The duration to wait for the service to be up before attempting repair. | 2min |
| component | The component to check health for. Supports kubelet, docker, kube-proxy, and cri. |
結(jié)構(gòu)定義
type healthChecker struct {
component string // 要進行健康檢查的組件名稱,支持 kubelet、docker、kube-proxy 和 cri
service string // 組件的服務(wù)名稱,需要通過 service 讀取 journal 日志,并檢查日志是否存在異常
enableRepair bool // 是否啟動自動修復(fù),如果啟動自動修復(fù),當發(fā)現(xiàn)異常時會調(diào)用 repairFunc 嘗試自動修復(fù)
healthCheckFunc func() (bool, error) // 組件健康檢查方法
repairFunc func() // 組件自愈方法,這是一種”best-effort“形式的自愈,會嘗試 kill 掉組件的進程,但可能失敗
uptimeFunc func() (time.Duration, error) // 獲取組件的啟動時間(啟動后經(jīng)過的時間)
crictlPath string // crictl 二進制文件路徑,用于對 CRI(Container Runtime Interface) 組件執(zhí)行健康檢查
healthCheckTimeout time.Duration // 健康檢查超時時間
coolDownTime time.Duration // 服務(wù)啟動后,在冷卻時間內(nèi)如果發(fā)現(xiàn)異常,不會嘗試自動修復(fù)。超出冷卻時間后才會嘗試自動修復(fù)
loopBackTime time.Duration // 待檢 journal 查日志的起始時間間隔,如果該值為0,則從組件啟動的日志開始檢查
logPatternsToCheck map[string]int // 要檢查的 journal 日志的正則表達式
}
執(zhí)行流程
health-checker 的執(zhí)行流程可以分為三個步驟:
調(diào)用 healthCheckFunc() 方法判斷組件進程是否健康 獲取組件近一段時間的 journal 日志,判斷異常日志數(shù)量是否達到上限 如果前兩步檢查都未發(fā)現(xiàn)異常,則返回 true。否則,如果啟動了自動修復(fù)機制,則調(diào)用 repairFunc() 嘗試自愈
健康檢查
func getHealthCheckFunc(hco *options.HealthCheckerOptions) func() (bool, error) {
switch hco.Component {
case types.KubeletComponent:
// 訪問 http://127.0.0.1:10248/healthz,判斷 kubelet 是否健康
return healthCheckEndpointOKFunc(types.KubeletHealthCheckEndpoint, hco.HealthCheckTimeout)
case types.KubeProxyComponent:
// 訪問 http://127.0.0.1:10256/healthz,判斷 kube-proxy 是否健康
return healthCheckEndpointOKFunc(types.KubeProxyHealthCheckEndpoint, hco.HealthCheckTimeout)
case types.DockerComponent:
return func() (bool, error) { // 執(zhí)行 docker ps 命令判斷 Docker 是否健康
if _, err := execCommand(hco.HealthCheckTimeout, getDockerPath(), "ps"); err != nil {
return false, nil
}
return true, nil
}
case types.CRIComponent:
return func() (bool, error) {// 執(zhí)行 circtl --runtime-endpoint=unix:///var/run/containerd/containerd.sock --image-endpoint=unix:///var/run/containerd/containerd.sock
if _, err := execCommand(hco.HealthCheckTimeout, hco.CriCtlPath, "--runtime-endpoint="+hco.CriSocketPath, "--image-endpoint="+hco.CriSocketPath, "pods"); err != nil {
return false, nil
}
return true, nil
}
default:
glog.Warningf("Unsupported component: %v", hco.Component)
}
return nil
}
組件自愈
func getRepairFunc(hco *options.HealthCheckerOptions) func() {
switch hco.Component {
case types.DockerComponent:
// Use "docker ps" for docker health check. Not using crictl for docker to remove
// dependency on the kubelet.
return func() {
execCommand(types.CmdTimeout, "pkill", "-SIGUSR1", "dockerd")
execCommand(types.CmdTimeout, "systemctl", "kill", "--kill-who=main", hco.Service)
}
default:
// Just kill the service for all other components
return func() {
execCommand(types.CmdTimeout, "systemctl", "kill", "--kill-who=main", hco.Service)
}
}
}
log-counter
依賴的插件是 journald,其作用是統(tǒng)計指定的 journal 日志中近一段時間滿足正則匹配的歷史日志條數(shù)。
命令行參數(shù)
| 參數(shù)名稱 | 參數(shù)說明 | 默認值 |
|---|---|---|
| journald-source | The source configuration of journald, e.g., kernel, kubelet, dockerd, etc | |
| log-path | The log path that log watcher looks up | |
| lookback | The log path that log watcher looks up | |
| delay | The time duration log watcher delays after node boot time. This is useful when log watcher needs to wait for some time until the node is stable. | |
| pattern | The regular expression to match the problem in log. The pattern must match to the end of the line. | |
| count | The number of times the pattern must be found to trigger the condition | 1 |
執(zhí)行流程

Count()
func (e *logCounter) Count() (count int, err error) {
start := e.clock.Now()
for {
select {
case log, ok := <-e.logCh:
if !ok {
err = fmt.Errorf("log channel closed unexpectedly")
return
}
// 只統(tǒng)計 logCounter 啟動之前的日志
if start.Before(log.Timestamp) {
return
}
e.buffer.Push(log)
if len(e.buffer.Match(e.pattern)) != 0 {
count++
}
case <-e.clock.After(timeout):
// 如果超過一定時間沒有新日志生成,則退出
return
}
}
}
journal日志檢查
func checkForPattern(service, logStartTime, logPattern string, logCountThreshold int) (bool, error) {
// 從 journal 日志中匹配符合規(guī)則的錯誤日志
out, err := execCommand(types.CmdTimeout, "/bin/sh", "-c",
// Query service logs since the logStartTime
`journalctl --unit "`+service+`" --since "`+logStartTime+
// 正則匹配
`" | grep -i "`+logPattern+
// 計算錯誤發(fā)生次數(shù)
`" | wc -l`)
if err != nil {
return true, err
}
occurrences, err := strconv.Atoi(out)
if err != nil {
return true, err
}
// 如果錯誤日志數(shù)量超過閾值,則返回 false
if occurrences >= logCountThreshold {
glog.Infof("%s failed log pattern check, %s occurrences: %v", service, logPattern, occurrences)
return false, nil
}
return true, nil
}
network_problem.sh
檢查 conntrack table 的使用率是否超過 90%
#!/bin/bash
# This plugin checks for common network issues.
# Currently only checks if conntrack table is more than 90% used.
readonly OK=0
readonly NONOK=1
readonly UNKNOWN=2
# "nf_conntrack" replaces "ip_conntrack" - support both
readonly NF_CT_COUNT_PATH='/proc/sys/net/netfilter/nf_conntrack_count'
readonly NF_CT_MAX_PATH='/proc/sys/net/netfilter/nf_conntrack_max'
readonly IP_CT_COUNT_PATH='/proc/sys/net/ipv4/netfilter/ip_conntrack_count'
readonly IP_CT_MAX_PATH='/proc/sys/net/ipv4/netfilter/ip_conntrack_max'
if [[ -f $NF_CT_COUNT_PATH ]] && [[ -f $NF_CT_MAX_PATH ]]; then
readonly CT_COUNT_PATH=$NF_CT_COUNT_PATH
readonly CT_MAX_PATH=$NF_CT_MAX_PATH
elif [[ -f $IP_CT_COUNT_PATH ]] && [[ -f $IP_CT_MAX_PATH ]]; then
readonly CT_COUNT_PATH=$IP_CT_COUNT_PATH
readonly CT_MAX_PATH=$IP_CT_MAX_PATH
else
exit $UNKNOWN
fi
readonly conntrack_count=$(< $CT_COUNT_PATH) || exit $UNKNOWN
readonly conntrack_max=$(< $CT_MAX_PATH) || exit $UNKNOWN
readonly conntrack_usage_msg="${conntrack_count} out of ${conntrack_max}"
if (( conntrack_count > conntrack_max * 9 /10 )); then
echo "Conntrack table usage over 90%: ${conntrack_usage_msg}"
exit $NONOK
else
echo "Conntrack table usage: ${conntrack_usage_msg}"
exit $OK
fi
system-log-monitor
system-log-monitor 用于監(jiān)控系統(tǒng)和內(nèi)核日志,根據(jù)預(yù)定義規(guī)則來報告問題、指標。它支持基于文件的日志、Journald、kmsg。要監(jiān)控其它日志,需要實現(xiàn)LogWatcher接口
LogMonitor
type logMonitor struct {
// 配置文件路徑
configPath string
// 讀取日志的邏輯委托給LogWatcher,這里解耦的目的是支持多種類型的日志
watcher watchertypes.LogWatcher
// 日志緩沖,讀取的日志在此等待處理
buffer LogBuffer
// 對應(yīng)配置文件中的字段
config MonitorConfig
// 對應(yīng)配置文件中的conditions字段
conditions []types.Condition
// 輸入日志條目的通道
logCh <-chan *logtypes.Log
// 輸出狀態(tài)的通道
output chan *types.Status
// 用于控制此Monitor的生命周期
tomb *tomb.Tomb
}
LogWatcher
LogWatcher 的主要作用的監(jiān)聽文件更新,并將追加的文件內(nèi)容寫入 LogBuffer 中供 LogMonitor 處理。NPD 中提供了三種 LogWatcher 的實現(xiàn):
filelog:監(jiān)聽任意文本類型日志。 journald:監(jiān)聽 journald 日志。 kmsg:監(jiān)聽內(nèi)核日志設(shè)備,如 /dev/kmsg。
LogWatcher 也需要在 init() 方法中完成注冊。
type LogWatcher interface {
// 開始監(jiān)控日志,并通過通道輸出日志
Watch() (<-chan *types.Log, error)
// 停止,注意釋放打開的資源
Stop()
}
filelog
filelog 通過監(jiān)控指定的文件更新,并對日志內(nèi)容進行正則匹配,以發(fā)現(xiàn)異常日志,從而判斷組件是否正常。
{
"plugin": "filelog",
"pluginConfig": {
"timestamp": "^time=\"(\\S*)\"",// 時間戳解析表達式
"message": "msg=\"([^\n]*)\"", // 日志解析表達式
"timestampFormat": "2006-01-02T15:04:05.999999999-07:00" // 時間戳格式
},
"logPath": "/var/log/docker.log", // 日志路徑
"lookback": "5m", // 日志回溯時長
"bufferSize": 10, // 緩沖大?。ㄈ罩緱l數(shù))
"source": "docker-monitor",
"conditions": [],
"rules": [ // 健康檢查規(guī)則
{
"type": "temporary",
"reason": "CorruptDockerImage",
"pattern": "Error trying v2 registry: failed to register layer: rename /var/lib/docker/image/(.+) /var/lib/docker/image/(.+): directory not empty.*"
}
]
}
journald
journald 底層依賴 sdjournal 包,監(jiān)控系統(tǒng)日志的更新,并且可以從指定的歷史時間點開始讀取。如果未指定 journal 日志路徑,則從系統(tǒng)默認路徑讀取。讀取到的日志會轉(zhuǎn)換成 logtypes.Log 對象,并寫入 logCh 通道中。journal 通過監(jiān)控 journal 文件更新,并對日志內(nèi)容進行正則匹配,以發(fā)現(xiàn)異常日志,從而判斷組件是否正常。
{
"plugin": "journald",
"pluginConfig": {
"source": "abrt-notification"
},
"logPath": "/var/log/journal", // journal 日志路徑
"lookback": "5m", // 日志回溯時長
"bufferSize": 10, // log 緩存大?。ㄈ罩緱l數(shù))
"source": "abrt-adaptor",
"conditions": [],
"rules": [ // 健康檢查規(guī)則
{
"type": "temporary",
"reason": "CCPPCrash",
"pattern": "Process \\d+ \\(\\S+\\) crashed in .*"
},
{
"type": "temporary",
"reason": "UncaughtException",
"pattern": "Process \\d+ \\(\\S+\\) of user \\d+ encountered an uncaught \\S+ exception"
},
{
"type": "temporary",
"reason": "XorgCrash",
"pattern": "Display server \\S+ crash in \\S+"
},
{
"type": "temporary",
"reason": "VMcore",
"pattern": "System encountered a fatal error in \\S+"
},
{
"type": "temporary",
"reason": "Kerneloops",
"pattern": "System encountered a non-fatal error in \\S+"
}
]
}
kmsg
kmsg 和 journald 的實現(xiàn)原理類似,它底層依賴 kmsgparser 包,實現(xiàn)內(nèi)核日志的監(jiān)控更新和回溯。默認的文件路徑是 /dev/kmsg。kmsg 通過監(jiān)控系統(tǒng)日志文件更新,并對日志內(nèi)容進行正則匹配,以發(fā)現(xiàn)異常日志,從而判斷組件是否正常。
{
"plugin": "kmsg",
"logPath": "/dev/kmsg", // 內(nèi)核日志路徑
"lookback": "5m", // 日志回溯時長
"bufferSize": 10, // 緩存大?。ㄈ罩緱l數(shù))
"source": "kernel-monitor",
"metricsReporting": true,
"conditions": [
{
"type": "KernelDeadlock",
"reason": "KernelHasNoDeadlock",
"message": "kernel has no deadlock"
},
{
"type": "ReadonlyFilesystem",
"reason": "FilesystemIsNotReadOnly",
"message": "Filesystem is not read-only"
}
],
"rules": [
{
"type": "temporary",
"reason": "OOMKilling",
"pattern": "Killed process \\d+ (.+) total-vm:\\d+kB, anon-rss:\\d+kB, file-rss:\\d+kB.*"
},
{
"type": "temporary",
"reason": "TaskHung",
"pattern": "task [\\S ]+:\\w+ blocked for more than \\w+ seconds\\."
},
{
"type": "temporary",
"reason": "UnregisterNetDevice",
"pattern": "unregister_netdevice: waiting for \\w+ to become free. Usage count = \\d+"
},
{
"type": "temporary",
"reason": "KernelOops",
"pattern": "BUG: unable to handle kernel NULL pointer dereference at .*"
},
{
"type": "temporary",
"reason": "KernelOops",
"pattern": "divide error: 0000 \\[#\\d+\\] SMP"
},
{
"type": "temporary",
"reason": "Ext4Error",
"pattern": "EXT4-fs error .*"
},
{
"type": "temporary",
"reason": "Ext4Warning",
"pattern": "EXT4-fs warning .*"
},
{
"type": "temporary",
"reason": "IOError",
"pattern": "Buffer I/O error .*"
},
{
"type": "temporary",
"reason": "MemoryReadError",
"pattern": "CE memory read error .*"
},
{
"type": "permanent",
"condition": "KernelDeadlock",
"reason": "AUFSUmountHung",
"pattern": "task umount\\.aufs:\\w+ blocked for more than \\w+ seconds\\."
},
{
"type": "permanent",
"condition": "KernelDeadlock",
"reason": "DockerHung",
"pattern": "task docker:\\w+ blocked for more than \\w+ seconds\\."
},
{
"type": "permanent",
"condition": "ReadonlyFilesystem",
"reason": "FilesystemIsReadOnly",
"pattern": "Remounting filesystem read-only"
}
]
}
LogBuffer
LogBuffer 是一個可循環(huán)寫入的日志隊列,max 字段控制可記錄日志的最大條數(shù),當日志條數(shù)超過 max 時,就會從頭覆蓋寫入。LogBuffer 也支持正則匹配 buffer 中的日志內(nèi)容。
type LogBuffer interface {
// 把日志寫入 log buffer 中
Push(*types.Log)
// 對 buffer 中的日志進行正則匹配
Match(string) []*types.Log
// 把 log buffer 中的日志按時間由遠到近連接成一個字符串
String() string
}
實現(xiàn)原理
啟動過程

執(zhí)行過程

system-stats-monitor
將各種健康相關(guān)的統(tǒng)計信息報告為Metrics
目前支持的組件僅僅有主機信息、磁盤:
disk/io_time 設(shè)備隊列非空時間,毫秒 disk/weighted_io 設(shè)備隊列非空時間加權(quán),毫秒 disk/avg_queue_len 上次調(diào)用插件以來,平均排隊請求數(shù)
使用標記禁用:disable_system_stats_monitor
cpuCollector:采集 CPU 相關(guān)指標信息。 diskCollector:采集磁盤相關(guān)指標信息。 hostCollector:采集宿主機相關(guān)指標信息。 memoryCollector:采集內(nèi)存相關(guān)指標信息。 osFeatureCollector:采集系統(tǒng)屬性相關(guān)指標。 netCollector:采集網(wǎng)絡(luò)相關(guān)指標信息。
檢查規(guī)則
自定義插件規(guī)則
CustomRule
// 自定義規(guī)則(插件),描述CPM如何調(diào)用插件,分析調(diào)用結(jié)果
type CustomRule struct {
// 報告永久還是臨時問題
Type types.Type `json:"type"`
// 此問題觸發(fā)哪種NodeCondition,僅當永久問題才設(shè)置此字段
Condition string `json:"condition"`
// 問題的簡短原因,對于永久問題,通常描述NodeCondition的一個子類型
Reason string `json:"reason"`
// 自定義插件(腳本)的文件路徑
Path string `json:"path"`
// 傳遞給自定義插件的參數(shù)
Args []string `json:"args"`
// 自定義插件執(zhí)行超時
TimeoutString *string `json:"timeout"`
Timeout *time.Duration `json:"-"`
}
示例
health-checker-kubelet.json(https://github.com/kubernetes/node-problem-detector/blob/master/config/health-checker-kubelet.json)
系統(tǒng)日志監(jiān)控規(guī)則
systemlogtypes.Rule
type Rule struct {
// 報告永久還是臨時問題
Type types.Type `json:"type"`
// 此問題觸發(fā)哪種NodeCondition,僅當永久問題才設(shè)置此字段
Condition string `json:"condition"`
// 問題的簡短原因,對于永久問題,通常描述NodeCondition的一個子類型
Reason string `json:"reason"`
// Pattern is the regular expression to match the problem in log.
// Notice that the pattern must match to the end of the line.
Pattern string `json:"pattern"`
}
示例
kernel-monitor.json(https://github.com/kubernetes/node-problem-detector/blob/master/config/kernel-monitor.json)
異常上報
node-problem-detector使用 Event 和 NodeCondition 將問題報告給apiserver。
NodeCondition:導致節(jié)點無法處理于Pod生命周期的的永久性問題應(yīng)報告為NodeCondition。 Event:對pod影響有限的臨時問題應(yīng)作為event報告。
異常類型
temporary:致節(jié)點無法處理于Pod生命周期的的永久性問題 permanent:對pod影響有限的臨時問題
指標上報
通過配置 metricsReporting 可以選擇是否開啟 System Log Monitor 的指標上報功能。該字段默認為 true。
臨時異常只會上報 counter 指標,如下:
# HELP problem_counter Number of times a specific type of problem have occurred.
# TYPE problem_counter counter
problem_counter{reason="TaskHung"} 2
永久異常會上報 gauge 指標和 counter 指標,如下:
# HELP problem_counter Number of times a specific type of problem have occurred.
# TYPE problem_counter counter
problem_counter{reason="DockerHung"} 1
# HELP problem_gauge Whether a specific type of problem is affecting the node or not.
# TYPE problem_gauge gauge
problem_gauge{condition="KernelDeadlock",reason="DockerHung"} 1
Counter是一個累計類型的數(shù)據(jù)指標,它代表單調(diào)遞增的計數(shù)器。`
`Gauge是可以任意上下波動數(shù)值的指標類型。
指標
NPD對指標這一概念也進行了封裝,它依賴OpenCensus而不是Prometheus這樣具體的實現(xiàn)的API。
所有指標如下:
const (
CPURunnableTaskCountID MetricID = "cpu/runnable_task_count"
CPUUsageTimeID MetricID = "cpu/usage_time"
CPULoad1m MetricID = "cpu/load_1m"
CPULoad5m MetricID = "cpu/load_5m"
CPULoad15m MetricID = "cpu/load_15m"
ProblemCounterID MetricID = "problem_counter"
ProblemGaugeID MetricID = "problem_gauge"
DiskIOTimeID MetricID = "disk/io_time"
DiskWeightedIOID MetricID = "disk/weighted_io"
DiskAvgQueueLenID MetricID = "disk/avg_queue_len"
DiskOpsCountID MetricID = "disk/operation_count"
DiskMergedOpsCountID MetricID = "disk/merged_operation_count"
DiskOpsBytesID MetricID = "disk/operation_bytes_count"
DiskOpsTimeID MetricID = "disk/operation_time"
DiskBytesUsedID MetricID = "disk/bytes_used"
HostUptimeID MetricID = "host/uptime"
MemoryBytesUsedID MetricID = "memory/bytes_used"
MemoryAnonymousUsedID MetricID = "memory/anonymous_used"
MemoryPageCacheUsedID MetricID = "memory/page_cache_used"
MemoryUnevictableUsedID MetricID = "memory/unevictable_used"
MemoryDirtyUsedID MetricID = "memory/dirty_used"
OSFeatureID MetricID = "system/os_feature"
SystemProcessesTotal MetricID = "system/processes_total"
SystemProcsRunning MetricID = "system/procs_running"
SystemProcsBlocked MetricID = "system/procs_blocked"
SystemInterruptsTotal MetricID = "system/interrupts_total"
SystemCPUStat MetricID = "system/cpu_stat"
NetDevRxBytes MetricID = "net/rx_bytes"
NetDevRxPackets MetricID = "net/rx_packets"
NetDevRxErrors MetricID = "net/rx_errors"
NetDevRxDropped MetricID = "net/rx_dropped"
NetDevRxFifo MetricID = "net/rx_fifo"
NetDevRxFrame MetricID = "net/rx_frame"
NetDevRxCompressed MetricID = "net/rx_compressed"
NetDevRxMulticast MetricID = "net/rx_multicast"
NetDevTxBytes MetricID = "net/tx_bytes"
NetDevTxPackets MetricID = "net/tx_packets"
NetDevTxErrors MetricID = "net/tx_errors"
NetDevTxDropped MetricID = "net/tx_dropped"
NetDevTxFifo MetricID = "net/tx_fifo"
NetDevTxCollisions MetricID = "net/tx_collisions"
NetDevTxCarrier MetricID = "net/tx_carrier"
NetDevTxCompressed MetricID = "net/tx_compressed"
)
其中ProblemCounterID 和 ProblemGaugeID 是針對所有Problem的Counter/Gauge,其他都是SystemStatsMonitor暴露的指標。
治愈系統(tǒng)
在NPD的術(shù)語中,治愈系統(tǒng)(Remedy System)是一個或一組進程,負責分析NPD檢測出的問題,并且采取補救措施,讓K8S集群恢復(fù)健康狀態(tài)。
目前官方提及的治愈系統(tǒng)有只有Draino。NPD項目并沒有提供對Draino的集成,你需要手工部署和配置Draino。
Draino
Draino(https://github.com/planetlabs/draino)是Planet開源的小項目,最初在Planet用于解決GCE上運行的K8S集群的持久卷相關(guān)進程(mkfs.ext4、mount等)永久卡死在不可中斷睡眠狀態(tài)的問題。Draino的工作方式簡單粗暴,只是檢測到NodeCondition并Cordon、Drain節(jié)點。
基于Label和NodeCondition自動的Drain掉故障K8S節(jié)點:
具有匹配標簽的的K8S節(jié)點,只要進入指定的NodeCondition之一,立即禁止調(diào)度(Cordoned) 在禁止調(diào)度之后一段時間,節(jié)點被Drain掉
Draino可以聯(lián)用Cluster Autoscaler,自動的終結(jié)掉Drained的節(jié)點。
在Descheduler項目成熟以后,可以代替Draino。
?原文鏈接:https://www.jianshu.com/p/eeba98425307
