NPD原理解析

NPD 入門

簡介

節(jié)點(diǎn)問題檢測器(Node Problem Detector) 是一個守護(hù)程序喊巍,用于監(jiān)視和報(bào)告節(jié)點(diǎn)的健康狀況(包括內(nèi)核死鎖、OOM办陷、系統(tǒng)線程數(shù)壓力琅轧、系統(tǒng)文件描述符壓力等指標(biāo))。 你可以將節(jié)點(diǎn)問題探測器以 DaemonSet 或獨(dú)立守護(hù)程序運(yùn)行搭伤。 節(jié)點(diǎn)問題檢測器從各種守護(hù)進(jìn)程收集節(jié)點(diǎn)問題只怎,并以 NodeConditionEvent 的形式報(bào)告給 API Server。
您可以通過檢測相應(yīng)的指標(biāo)怜俐,提前預(yù)知節(jié)點(diǎn)的資源壓力身堡,可以在節(jié)點(diǎn)開始驅(qū)逐 Pod 之前手動釋放或擴(kuò)容節(jié)點(diǎn)資源壓力,防止 Kubenetes 進(jìn)行資源回收或節(jié)點(diǎn)不可用可能帶來的損失拍鲤。

Git 倉庫地址:https://github.com/kubernetes/node-problem-detector

Kubernetes 目前問題

  • 基礎(chǔ)架構(gòu)守護(hù)程序問題:ntp服務(wù)關(guān)閉贴谎;

  • 硬件問題:CPU,內(nèi)存或磁盤損壞季稳;

  • 內(nèi)核問題:內(nèi)核死鎖擅这,文件系統(tǒng)損壞;

  • 容器運(yùn)行時問題:運(yùn)行時守護(hù)程序無響應(yīng)

  • ...

當(dāng)kubernetes中節(jié)點(diǎn)發(fā)生上述問題景鼠,在整個集群中仲翎,k8s服務(wù)組件并不會感知以上問題,就會導(dǎo)致pod仍會調(diào)度至問題節(jié)點(diǎn)铛漓。

為了解決這個問題溯香,我們引入了這個新的守護(hù)進(jìn)程node-problem-detector,從各個守護(hù)進(jìn)程收集節(jié)點(diǎn)問題浓恶,并使它們對上游層可見玫坛。一旦上游層面發(fā)現(xiàn)了這些問題,我們就可以討論補(bǔ)救措施包晰。

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)建標(biāo)記
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é)點(diǎn)名稱,NPD 會優(yōu)先獲取該參數(shù)設(shè)置的節(jié)點(diǎn)名稱杜窄,其次是從 NODE_NAME 環(huán)境變量中獲取肠骆,最后從 os.Hostname() 方法獲取。

system-log-monitor 相關(guān)參數(shù)
  • --config.system-log-monitor: system log monitor 配置文件路徑塞耕,多個文件用逗號分隔, 如 config/kernel-monitor.json. NPD 會為每一個配置文件生成單獨(dú)的 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 會為每一個配置文件生成單獨(dú)的 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 會為每一個配置文件生成單獨(dú)的自定義插件監(jiān)視器磁玉。你可以使用不同的自定義插件監(jiān)視器來監(jiān)控不同的系統(tǒng)問題停忿。
K8s exporter 相關(guān)參數(shù)
  • --enable-k8s-exporter: 是否開啟上報(bào)信息到 API Server,默認(rèn)為 true.

  • --apiserver-override: 一個URI參數(shù)蚊伞,用于自定義node-problem-detector連接apiserver的地址席赂。 如果--enable-k8s-exporter為false,則忽略此內(nèi)容时迫。 格式與Heapster的源標(biāo)志相同颅停。 例如,要在沒有身份驗(yàn)證的情況下運(yùn)行掠拳,請使用以下配置: http://APISERVER_IP:APISERVER_PORT?inClusterConfig=false

    請參閱 heapster 文檔以獲取可用選項(xiàng)的完整列表癞揉。

  • --address: 綁定 NPD 服務(wù)器的地址。

  • --port: NPD 服務(wù)端口溺欧,如果為0喊熟,表示禁用 NPD 服務(wù)。

Prometheus exporter 相關(guān)參數(shù)
  • --prometheus-address: 綁定Prometheus抓取端點(diǎn)的地址姐刁,默認(rèn)為127.0.0.1芥牌。

  • --prometheus-port: 綁定Prometheus抓取端點(diǎn)的端口,默認(rèn)為20257龙填。使用0禁用胳泉。

Stackdriver exporter 相關(guān)參數(shù)
  • --exporter.stackdriver: Stackdriver exporter程序配置文件的路徑拐叉,例如 config/exporter/stackdriver-exporter.json岩遗,默認(rèn)為空字符串。 設(shè)置為空字符串以禁用凤瘦。
過期參數(shù)
  • --system-log-monitors: system log monitor 配置文件路徑宿礁,多個文件用逗號分隔。該選項(xiàng)已過期, 被 --config.system-log-monitor 取代, 即將被移除. 如果在啟動NPD時同時設(shè)置了 --system-log-monitors 和 --config.system-log-monitor蔬芥,會引發(fā)panic梆靖。

  • --custom-plugin-monitors: 用戶自定義插件配置文件路徑,多個文件用逗號分隔笔诵。該選項(xiàng)已過期, 被 --config.custom-plugin-monitor 取代, 即將被移除. 如果在啟動NPD時同時設(shè)置了 --custom-plugin-monitors 和 --config.custom-plugin-monitor返吻,會引發(fā)panic。

覆蓋配置文件

構(gòu)建節(jié)點(diǎn)問題檢測器的 docker 鏡像時乎婿,會嵌入 默認(rèn)配置测僵。

不過,你可以像下面這樣使用 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é)點(diǎn)問題檢測器:

說明: 此方法僅適用于通過 kubectl 啟動的節(jié)點(diǎn)問題檢測器榨婆。

如果節(jié)點(diǎn)問題檢測器作為集群插件運(yùn)行磁携,則不支持覆蓋配置。 插件管理器不支持 ConfigMap良风。

如何驗(yàn)證NPD捕獲信息

通常這些錯誤是比較難真實(shí)測試谊迄,只能通過發(fā)送消息到j(luò)ournal來模擬。

  • 發(fā)送一個kernel deadlock類型的condition:在對應(yīng)的node節(jié)點(diǎn)上執(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
image.png

實(shí)現(xiàn)原理

核心組件

Problem Daemon(Monitor)

Problem Daemon 是監(jiān)控任務(wù)子守護(hù)進(jìn)程鳞上,NPD 會為每一個 Problem Daemon 配置文件創(chuàng)建一個守護(hù)進(jìn)程,這些配置文件通過 --config.custom-plugin-monitor吊档、--config.system-log-monitor篙议、--config.system-stats-monitor 參數(shù)指定。每個 Problem Daemon監(jiān)控一個特定類型的節(jié)點(diǎn)故障怠硼,并報(bào)告給NPD鬼贱。目前 Problem Daemon 以 Goroutine 的形式運(yùn)行在NPD中,未來會支持在獨(dú)立進(jìn)程(容器)中運(yùn)行并編排為一個Pod香璃。在編譯期間这难,可以通過相應(yīng)的標(biāo)記禁用每一類 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實(shí)例葡秒,如果初始化過程中出錯姻乓,則拋出 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 用于上報(bào)節(jié)點(diǎn)健康信息到某種控制面。在 NPD 啟動時学少,會根據(jù)需求初始化并啟動各種 Exporter剪个。Exporter 分為三類:

  • K8s Exporter:會將節(jié)點(diǎn)健康信息上報(bào)到 API Server。

  • Prometheus Exporter:負(fù)責(zé)上報(bào)節(jié)點(diǎn)指標(biāo)信息到 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 信息會上報(bào)給 Condition Manager, Condition Manager 每秒檢查 Condition 的變化,并同步到 API Server 的 Node 對象中枷遂。

Problem Client

Problem Client 負(fù)責(zé)與 API Server 交互樱衷,并將巡檢過程中生成的 Events 和 Conditions 上報(bào)給 API Server。

type Client interface {
    // 從 API Server 獲取當(dāng)前節(jié)點(diǎn)所有指定類型的 Conditions
    GetConditions(conditionTypes []v1.NodeConditionType) ([]*v1.NodeCondition, error)
    // 調(diào)用 API Server 接口更新當(dāng)前節(jié)點(diǎn)的 Condition 列表
    SetConditions(conditions []v1.NodeCondition) error
    // 上報(bào) Event 信息到 API Server
    Eventf(eventType string, source, reason, messageFmt string, args ...interface{})
    // 從 API Server 獲取當(dāng)前 node-problem-detector 實(shí)例所在的節(jié)點(diǎn)信息
    GetNode() (*v1.Node, error)
}

Problem Detector

Problem Detector 是 NPD 的核心對象酒唉,它負(fù)責(zé)啟動所有的 Problem Daemon(也可以叫做 Monitor)矩桂,并利用 channel 收集 Problem Daemon中發(fā)現(xiàn)的異常信息,然后將異常信息提交給 Exporter痪伦,Exporter 負(fù)責(zé)將這些異常信息上報(bào)到指定的控制面(如 API Server侄榴、Prometheus、Stackdriver等)网沾。

Status

Status 是 Problem Daemon 向 Exporter 上報(bào)的異常信息對象癞蚕。

type Status struct {
    // problem daemon 的名稱
    Source string `json:"source"`
    // 臨時的節(jié)點(diǎn)問題 —— 事件對象,如果此Status用于Condition更新則此字段可以為空
  // 從老到新排列在數(shù)組中
    Events []Event `json:"events"`
    // 永久的節(jié)點(diǎn)問題 —— NodeCondition辉哥。PD必須總是在此字段報(bào)告最新的Condition
    Conditions []Condition `json:"conditions"`
}

Tomb

用于從外部控制協(xié)程的生命周期桦山, 它的邏輯很簡單,準(zhǔn)備結(jié)束生命周期時:

  1. 外部協(xié)作者發(fā)起一個通知

  2. 協(xié)作線程接收到通知醋旦,進(jìn)行清理

  3. 清理完成后恒水,協(xié)程反向通知外部協(xié)作者

  4. 外部協(xié)作者退出阻塞

啟動過程

NPD啟動過程

NPD 啟動過程完成的工作有:

  1. 打印 NPD 版本號

  2. 設(shè)置節(jié)點(diǎn)名稱,優(yōu)先使用命令行中設(shè)置的節(jié)點(diǎn)名稱饲齐,其次是環(huán)境變量 NODE_NAME 中的節(jié)點(diǎn)名稱钉凌,最次是 os.Hostname()

  3. 校驗(yàn)命令行參數(shù)的合法性

  4. 初始化 problem daemons

  5. 初始化默認(rèn) Exporters(包含 K8s Exporter、Prometheus Exporter)和可插拔 Exporters(如 Stackdriver Exporter)

  6. 使用 problem daemons 和 Exporters 構(gòu)建 Problem Detector捂人,并啟動

檢測流程

NPD檢測流程

節(jié)點(diǎn)自愈

采集節(jié)點(diǎn)的健康狀態(tài)是為了能夠在業(yè)務(wù)Pod不可用之前提前發(fā)現(xiàn)節(jié)點(diǎn)異常御雕,從而運(yùn)維或開發(fā)人員可以對Docker、Kubelet或節(jié)點(diǎn)進(jìn)行修復(fù)滥搭。在NPDPlus中酸纲,為了減輕運(yùn)維人員的負(fù)擔(dān),提供了根據(jù)采集到的節(jié)點(diǎn)狀態(tài)從而進(jìn)行不同自愈動作的能力论熙。集群管理員可以根據(jù)節(jié)點(diǎn)不同的狀態(tài)配置相應(yīng)的自愈能力福青,如重啟Docker摄狱、重啟Kubelet或重啟CVM節(jié)點(diǎn)等脓诡。同時為了防止集群中的節(jié)點(diǎn)雪崩,在執(zhí)行自愈動作之前做了嚴(yán)格的限流媒役,防止節(jié)點(diǎn)大規(guī)模重啟祝谚。同時為了防止集群中的節(jié)點(diǎn)雪崩,在執(zhí)行自愈動作之前做了嚴(yán)格的限流酣衷。具體策略為:

在同一時刻只允許集群中的一個節(jié)點(diǎn)進(jìn)行自愈行為交惯,并且兩個自愈行為之間至少間隔1分鐘

當(dāng)有新節(jié)點(diǎn)添加到集群中時,會給節(jié)點(diǎn)2分鐘的容忍時間,防止由于節(jié)點(diǎn)剛剛添加到集群的不穩(wěn)定性導(dǎo)致錯誤自愈

custom-plugin-monitor

此Problem Daemon為NPD提供了一種插件化機(jī)制席爽,允許基于任何語言來編寫監(jiān)控腳本意荤,只需要這些腳本遵循NPD關(guān)于退出碼和標(biāo)準(zhǔn)輸出的規(guī)范。通過調(diào)用用戶配置的腳本來檢測各種節(jié)點(diǎn)問題

腳本退出碼:

  1. 0:對于Evnet來說表示Normal只锻,對于NodeCondition表示False

  2. 1:對于Evnet來說表示W(wǎng)arning玖像,對于NodeCondition表示True

腳本輸出應(yīng)該小于80字節(jié),避免給Etcd的存儲造成壓力

使用標(biāo)記禁用:disable_custom_plugin_monitor

示例

{
  "plugin": "custom",  // 插件類型
  "pluginConfig": { // 插件配置
    "invoke_interval": "10s", // 執(zhí)行時間間隔
    "timeout": "3m", // 健康檢查超時時間
    "max_output_length": 80,
    "concurrency": 1 // 并行度
  },
  "source": "health-checker", // 事件源
  "metricsReporting": true,  // 是否上報(bào)指標(biāo)信息
  "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", // 二進(jìn)制文件路徑
      "args": [ // 二進(jìn)制文件啟動參數(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等進(jìn)程是否健康。

  • log-counter:依賴的插件是 journald泞歉,其作用是統(tǒng)計(jì)指定的 journal 日志中近一段時間滿足正則匹配的歷史日志條數(shù)逼侦。

  • network_problem.sh:檢查 conntrack table 的使用率是否超過 90%。

health-checker

命令行參數(shù)
參數(shù)名稱 參數(shù)說明 默認(rèn)值
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 <failureThresholdCount>:<logPattern>
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 // 要進(jìn)行健康檢查的組件名稱榛丢,支持 kubelet、docker挺庞、kube-proxy 和 cri
    service         string // 組件的服務(wù)名稱晰赞,需要通過 service 讀取 journal 日志,并檢查日志是否存在異常
    enableRepair    bool // 是否啟動自動修復(fù)选侨,如果啟動自動修復(fù)掖鱼,當(dāng)發(fā)現(xiàn)異常時會調(diào)用 repairFunc 嘗試自動修復(fù)
    healthCheckFunc func() (bool, error) // 組件健康檢查方法
    repairFunc         func() // 組件自愈方法,這是一種”best-effort“形式的自愈援制,會嘗試 kill 掉組件的進(jìn)程戏挡,但可能失敗
    uptimeFunc         func() (time.Duration, error)  // 獲取組件的啟動時間(啟動后經(jīng)過的時間)
    crictlPath         string // crictl 二進(jìn)制文件路徑,用于對 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 日志的正則表達(dá)式
}
執(zhí)行流程

health-checker 的執(zhí)行流程可以分為三個步驟:

  1. 調(diào)用 healthCheckFunc() 方法判斷組件進(jìn)程是否健康

  2. 獲取組件近一段時間的 journal 日志竟贯,判斷異常日志數(shù)量是否達(dá)到上限

  3. 如果前兩步檢查都未發(fā)現(xiàn)異常,則返回 true逝钥。否則澄耍,如果啟動了自動修復(fù)機(jī)制,則調(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)計(jì)指定的 journal 日志中近一段時間滿足正則匹配的歷史日志條數(shù)磷箕。

命令行參數(shù)
參數(shù)名稱 參數(shù)說明 默認(rèn)值
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í)行流程
log-counter執(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)計(jì) 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+
            // 計(jì)算錯誤發(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ī)則來報(bào)告問題芒填、指標(biāo)。它支持基于文件的日志空繁、Journald殿衰、kmsg。要監(jiān)控其它日志盛泡,需要實(shí)現(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 的實(shí)現(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)容進(jìn)行正則匹配挑势,以發(fā)現(xiàn)異常日志,從而判斷組件是否正常纽门。

{
    "plugin": "filelog",
    "pluginConfig": {
        "timestamp": "^time=\"(\\S*)\"",// 時間戳解析表達(dá)式
        "message": "msg=\"([^\n]*)\"",  // 日志解析表達(dá)式
        "timestampFormat": "2006-01-02T15:04:05.999999999-07:00" // 時間戳格式
    },
    "logPath": "/var/log/docker.log", // 日志路徑
    "lookback": "5m", // 日志回溯時長
    "bufferSize": 10, // 緩沖大醒Τ堋(日志條數(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)日志的更新赏陵,并且可以從指定的歷史時間點(diǎn)開始讀取饼齿。如果未指定 journal 日志路徑,則從系統(tǒng)默認(rèn)路徑讀取蝙搔。讀取到的日志會轉(zhuǎn)換成 logtypes.Log 對象缕溉,并寫入 logCh 通道中。journal 通過監(jiān)控 journal 文件更新吃型,并對日志內(nèi)容進(jìn)行正則匹配证鸥,以發(fā)現(xiàn)異常日志,從而判斷組件是否正常勤晚。

{
    "plugin": "journald",
    "pluginConfig": {
        "source": "abrt-notification"
    },
    "logPath": "/var/log/journal", // journal 日志路徑
    "lookback": "5m", // 日志回溯時長
    "bufferSize": 10, // log 緩存大型鞑恪(日志條數(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 的實(shí)現(xiàn)原理類似,它底層依賴 kmsgparser 包赐写,實(shí)現(xiàn)內(nèi)核日志的監(jiān)控更新和回溯鸟蜡。默認(rèn)的文件路徑是 /dev/kmsg。kmsg 通過監(jiān)控系統(tǒng)日志文件更新挺邀,并對日志內(nèi)容進(jìn)行正則匹配揉忘,以發(fā)現(xiàn)異常日志,從而判斷組件是否正常端铛。

{
    "plugin": "kmsg",
    "logPath": "/dev/kmsg", // 內(nèi)核日志路徑
    "lookback": "5m",  // 日志回溯時長
    "bufferSize": 10,  // 緩存大衅(日志條數(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)寫入的日志隊(duì)列,max 字段控制可記錄日志的最大條數(shù)禾蚕,當(dāng)日志條數(shù)超過 max 時您朽,就會從頭覆蓋寫入。LogBuffer 也支持正則匹配 buffer 中的日志內(nèi)容换淆。

type LogBuffer interface {
    // 把日志寫入 log buffer 中
    Push(*types.Log)
    // 對 buffer 中的日志進(jìn)行正則匹配
    Match(string) []*types.Log
  // 把 log buffer 中的日志按時間由遠(yuǎn)到近連接成一個字符串
    String() string
}

實(shí)現(xiàn)原理

啟動過程

執(zhí)行過程

system-stats-monitor

將各種健康相關(guān)的統(tǒng)計(jì)信息報(bào)告為Metrics

目前支持的組件僅僅有主機(jī)信息虚倒、磁盤:

  • disk/io_time 設(shè)備隊(duì)列非空時間,毫秒

  • disk/weighted_io 設(shè)備隊(duì)列非空時間加權(quán)产舞,毫秒

  • disk/avg_queue_len 上次調(diào)用插件以來魂奥,平均排隊(duì)請求數(shù)

使用標(biāo)記禁用:disable_system_stats_monitor

  • cpuCollector:采集 CPU 相關(guān)指標(biāo)信息。

  • diskCollector:采集磁盤相關(guān)指標(biāo)信息易猫。

  • hostCollector:采集宿主機(jī)相關(guān)指標(biāo)信息耻煤。

  • memoryCollector:采集內(nèi)存相關(guān)指標(biāo)信息。

  • osFeatureCollector:采集系統(tǒng)屬性相關(guān)指標(biāo)准颓。

  • netCollector:采集網(wǎng)絡(luò)相關(guān)指標(biāo)信息哈蝇。

檢查規(guī)則

自定義插件規(guī)則

CustomRule

// 自定義規(guī)則(插件),描述CPM如何調(diào)用插件攘已,分析調(diào)用結(jié)果
type CustomRule struct {
    // 報(bào)告永久還是臨時問題
    Type types.Type `json:"type"`
    // 此問題觸發(fā)哪種NodeCondition炮赦,僅當(dāng)永久問題才設(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

系統(tǒng)日志監(jiān)控規(guī)則

systemlogtypes.Rule

type Rule struct {
    // 報(bào)告永久還是臨時問題
    Type types.Type `json:"type"`
    // 此問題觸發(fā)哪種NodeCondition吠勘,僅當(dāng)永久問題才設(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

異常上報(bào)

node-problem-detector使用 Event 和 NodeCondition 將問題報(bào)告給apiserver剧防。

  • NodeCondition:導(dǎo)致節(jié)點(diǎn)無法處理于Pod生命周期的的永久性問題應(yīng)報(bào)告為NodeCondition植锉。

  • Event:對pod影響有限的臨時問題應(yīng)作為event報(bào)告。

異常類型

  • temporary:致節(jié)點(diǎn)無法處理于Pod生命周期的的永久性問題

  • permanent:對pod影響有限的臨時問題

指標(biāo)上報(bào)

通過配置 metricsReporting 可以選擇是否開啟 System Log Monitor 的指標(biāo)上報(bào)功能峭拘。該字段默認(rèn)為 true俊庇。

臨時異常只會上報(bào) counter 指標(biāo),如下:

# HELP problem_counter Number of times a specific type of problem have occurred.
# TYPE problem_counter counter
problem_counter{reason="TaskHung"} 2

永久異常會上報(bào) gauge 指標(biāo)和 counter 指標(biāo)鸡挠,如下:

# 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是一個累計(jì)類型的數(shù)據(jù)指標(biāo)辉饱,它代表單調(diào)遞增的計(jì)數(shù)器。
Gauge是可以任意上下波動數(shù)值的指標(biāo)類型拣展。

指標(biāo)

NPD對指標(biāo)這一概念也進(jìn)行了封裝鞋囊,它依賴OpenCensus而不是Prometheus這樣具體的實(shí)現(xiàn)的API。

所有指標(biāo)如下:

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暴露的指標(biāo)溜腐。

治愈系統(tǒng)

在NPD的術(shù)語中,治愈系統(tǒng)(Remedy System)是一個或一組進(jìn)程瓜喇,負(fù)責(zé)分析NPD檢測出的問題挺益,并且采取補(bǔ)救措施,讓K8S集群恢復(fù)健康狀態(tài)乘寒。

目前官方提及的治愈系統(tǒng)有只有Draino望众。NPD項(xiàng)目并沒有提供對Draino的集成,你需要手工部署和配置Draino伞辛。

Draino

Draino是Planet開源的小項(xiàng)目烂翰,最初在Planet用于解決GCE上運(yùn)行的K8S集群的持久卷相關(guān)進(jìn)程(mkfs.ext4、mount等)永久卡死在不可中斷睡眠狀態(tài)的問題蚤氏。Draino的工作方式簡單粗暴甘耿,只是檢測到NodeCondition并Cordon、Drain節(jié)點(diǎn)竿滨。

基于Label和NodeCondition自動的Drain掉故障K8S節(jié)點(diǎn):

  1. 具有匹配標(biāo)簽的的K8S節(jié)點(diǎn)佳恬,只要進(jìn)入指定的NodeCondition之一,立即禁止調(diào)度(Cordoned)

  2. 在禁止調(diào)度之后一段時間于游,節(jié)點(diǎn)被Drain掉

Draino可以聯(lián)用Cluster Autoscaler毁葱,自動的終結(jié)掉Drained的節(jié)點(diǎn)。

在Descheduler項(xiàng)目成熟以后贰剥,可以代替Draino倾剿。

參考文檔

kubernetes addons之node-problem-detector

Kubernetes故障檢測和自愈

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市蚌成,隨后出現(xiàn)的幾起案子前痘,更是在濱河造成了極大的恐慌凛捏,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,332評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件际度,死亡現(xiàn)場離奇詭異葵袭,居然都是意外死亡涵妥,警方通過查閱死者的電腦和手機(jī)乖菱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,508評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蓬网,“玉大人窒所,你說我怎么就攤上這事》妫” “怎么了吵取?”我有些...
    開封第一講書人閱讀 157,812評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長锯厢。 經(jīng)常有香客問我皮官,道長,這世上最難降的妖魔是什么实辑? 我笑而不...
    開封第一講書人閱讀 56,607評論 1 284
  • 正文 為了忘掉前任捺氢,我火速辦了婚禮,結(jié)果婚禮上剪撬,老公的妹妹穿的比我還像新娘摄乒。我一直安慰自己,他們只是感情好残黑,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,728評論 6 386
  • 文/花漫 我一把揭開白布馍佑。 她就那樣靜靜地躺著,像睡著了一般梨水。 火紅的嫁衣襯著肌膚如雪拭荤。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,919評論 1 290
  • 那天疫诽,我揣著相機(jī)與錄音穷劈,去河邊找鬼。 笑死踊沸,一個胖子當(dāng)著我的面吹牛歇终,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播逼龟,決...
    沈念sama閱讀 39,071評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼评凝,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了腺律?” 一聲冷哼從身側(cè)響起奕短,我...
    開封第一講書人閱讀 37,802評論 0 268
  • 序言:老撾萬榮一對情侶失蹤宜肉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后翎碑,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體谬返,經(jīng)...
    沈念sama閱讀 44,256評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,576評論 2 327
  • 正文 我和宋清朗相戀三年日杈,在試婚紗的時候發(fā)現(xiàn)自己被綠了遣铝。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,712評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡莉擒,死狀恐怖酿炸,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情涨冀,我是刑警寧澤填硕,帶...
    沈念sama閱讀 34,389評論 4 332
  • 正文 年R本政府宣布,位于F島的核電站鹿鳖,受9級特大地震影響扁眯,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜翅帜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,032評論 3 316
  • 文/蒙蒙 一姻檀、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧藕甩,春花似錦施敢、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至腋妙,卻和暖如春默怨,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背骤素。 一陣腳步聲響...
    開封第一講書人閱讀 32,026評論 1 266
  • 我被黑心中介騙來泰國打工匙睹, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人济竹。 一個月前我還...
    沈念sama閱讀 46,473評論 2 360
  • 正文 我出身青樓痕檬,卻偏偏與公主長得像,于是被迫代替她去往敵國和親送浊。 傳聞我的和親對象是個殘疾皇子梦谜,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,606評論 2 350

推薦閱讀更多精彩內(nèi)容