Christos Markou

如何使用 Elastic 可观测性启用 Kubernetes 警报

在 Kubernetes 世界中,不同的角色需要不同类型的洞察力。在这篇文章中,我们将重点关注警报,并概述 Elastic 可观测性中的警报如何帮助用户快速识别 Kubernetes 问题。

阅读时间:24分钟
How to enable Kubernetes alerting with Elastic Observability

在 Kubernetes 世界中,不同的角色需要不同类型的洞察力。开发人员对细粒度的指标和调试信息感兴趣。 SRE 则希望能够一览无遗,以便在问题发生时快速收到通知并找到根本原因。在这篇文章中,我们将重点关注警报,并概述 Elastic 可观测性中的警报如何帮助用户快速识别 Kubernetes 问题。

为什么我们需要警报?

日志、指标和跟踪只是构建完整 Kubernetes 集群监控解决方案 的基础。它们的主要目标是为基础设施提供调试信息和历史证据。

虽然 Kibana 中开箱即用的仪表板、基础设施拓扑和日志探索对于执行临时分析已经非常方便,但添加通知和基础设施的主动监控可以让用户尽早处理检测到的问题,甚至可以主动采取措施来防止 Kubernetes 环境面临更严重的问题。

如何实现?

通过在其基础设施之上构建警报,用户可以利用数据并将其有效地关联到特定通知,从而创造广泛的可能性来动态监控和观察其 Kubernetes 集群。

在这篇博文中,我们将探讨用户如何利用 Elasticsearch 的搜索功能来定义警报规则,以便在发生特定条件时收到通知。

SLI、警报和 SLO:为什么它们对 SRE 如此重要?

对于站点可靠性工程师 (SRE) 而言,事件响应时间 与日常工作的成功密切相关。监控、警报和操作将有助于发现、解决或预防系统中的问题。

  • SLA(服务等级协议)是您与用户之间达成的协议,用于指定他们可以期望的服务水平。
  • SLO(服务等级目标)是 SLA 中关于特定指标(如正常运行时间或响应时间)的协议。
  • SLI(服务等级指标)衡量是否符合 SLO。

SRE 的日常任务和项目都由 SLO 驱动。通过确保短期内维护 SLO 并能够在中长期内保持 SLO,我们为稳定的工作基础设施奠定了基础。

话虽如此,识别 SLO 的高级别类别对于组织 SRE 的工作至关重要。然后,在每个 SLO 类别中,SRE 将需要相应的 SLI,这些 SLI 可以涵盖其观察到的系统最重要的案例。因此,我们需要哪些 SLI 的决策需要对底层系统基础设施有额外的了解。

对 SLI 和 SLO 进行分类的一种广泛使用的方法是 四个黄金指标 方法。定义的类别是延迟、流量、错误和饱和度。

一种更具体的方法是 RED 方法,由曾在 Google 担任 SRE 并使用四个黄金指标的 Tom Wilkie 开发。RED 方法放弃了饱和度类别,因为该类别主要用于更高级的案例——人们更容易记住三个东西。

专注于 Kubernetes 基础设施运营商,我们将考虑以下基础设施 SLI/SLO 组

  • 组 1:控制平面延迟(apiserver,
  • 组 2:节点/Pod 的资源利用率(消耗了多少 CPU、内存等)
  • 组 3:错误(日志或事件中的错误,或组件、网络等的错误计数)

为 Kubernetes 集群创建警报

现在我们已经完整概述了基于 SLI/SLO 定义警报的目标,我们将深入探讨定义适当警报的过程。警报可以使用 Kibana 构建。

请参阅 Elastic 的 文档

在这篇博文中,我们将根据 Watcher 功能提供的复杂 Elasticsearch 查询定义更复杂的警报。详细了解 Watcher 以及如何在除了本博文中的示例之外正确使用它。

延迟警报

对于这种警报,我们希望为 Kubernetes 控制平面定义基本 SLO,这将确保基本控制平面组件能够在没有问题的情况下为最终用户提供服务。例如,在针对 Kubernetes API 服务器的查询中遇到高延迟就足以表明需要采取措施。

资源饱和度

下一组警报将是资源利用率。节点的 CPU 利用率或节点状态的变化对于集群确保顺利服务为运行最终用户将与之交互的应用程序而预配的工作负载至关重要。

错误检测

最后但并非最不重要的是,我们将根据特定错误定义警报,例如网络错误率或 Pod 故障(如 OOMKilled 情况)。对于 SRE 团队来说,这是一个非常有用的指标,可以检测基础设施层面的问题,或者只是能够通知开发团队有关有问题的负载的信息。稍后我们将检查的一个示例是,有一个应用程序作为 Pod 运行,并且不断重新启动,因为它达到了其内存限制。在这种情况下,此应用程序的所有者将需要收到通知以采取适当措施。

从 Kubernetes 数据到 Elasticsearch 查询

制定了我们想要实施的警报的可靠计划后,是时候探索我们从 Kubernetes 集群收集并存储在 Elasticsearch 中的数据了。为此,我们将参考使用 Elastic Agent Kubernetes 集成 摄取的可用数据字段列表(可以在 此处 找到完整字段列表)。使用这些字段,我们可以创建各种警报,例如

  • 节点 CPU 利用率
  • 节点内存利用率
  • 带宽利用率
  • Pod 重启
  • Pod CPU/内存利用率

CPU 利用率警报

我们的第一个示例将使用 CPU 利用率字段来计算节点的 CPU 利用率并创建警报。对于此警报,我们利用以下指标

kubernetes.node.cpu.usage.nanocores
kubernetes.node.cpu.capacity.cores.

以下计算 (nodeUsage / 1000000000 ) /nodeCap 按节点名称分组将提供我们集群节点的 CPU 利用率。

实现此查询的 Watcher 定义可以通过以下 API 调用到 Elasticsearch 来创建

curl -X PUT "https://elastic:changeme@localhost:9200/_watcher/watch/Node-CPU-Usage?pretty" -k -H 'Content-Type: application/json' -d'
{
  "trigger": {
    "schedule": {
      "interval": "10m"
    }
  },
  "input": {
    "search": {
      "request": {
        "body": {
          "size": 0,
          "query": {
            "bool": {
              "must": [
                {
                  "range": {
                    "@timestamp": {
                      "gte": "now-10m",
                      "lte": "now",
                      "format": "strict_date_optional_time"
                    }
                  }
                },
                {
                  "bool": {
                    "must": [
                      {
                        "query_string": {
                          "query": "data_stream.dataset: kubernetes.node OR data_stream.dataset: kubernetes.state_node",
                          "analyze_wildcard": true
                        }
                      }
                    ],
                    "filter": [],
                    "should": [],
                    "must_not": []
                  }
                }
              ],
              "filter": [],
              "should": [],
              "must_not": []
            }
          },
          "aggs": {
            "nodes": {
              "terms": {
                "field": "kubernetes.node.name",
                "size": "10000",
                "order": {
                  "_key": "asc"
                }
              },
              "aggs": {
                "nodeUsage": {
                  "max": {
                    "field": "kubernetes.node.cpu.usage.nanocores"
                  }
                },
                "nodeCap": {
                  "max": {
                    "field": "kubernetes.node.cpu.capacity.cores"
                  }
                },
                "nodeCPUUsagePCT": {
                  "bucket_script": {
                    "buckets_path": {
                      "nodeUsage": "nodeUsage",
                      "nodeCap": "nodeCap"
                    },
                    "script": {
                      "source": "( params.nodeUsage / 1000000000 ) / params.nodeCap",
                      "lang": "painless",
                      "params": {
                        "_interval": 10000
                      }
                    },
                    "gap_policy": "skip"
                  }
                }
              }
            }
          }
        },
        "indices": [
          "metrics-kubernetes*"
        ]
      }
    }
  },
  "condition": {
    "array_compare": {
      "ctx.payload.aggregations.nodes.buckets": {
        "path": "nodeCPUUsagePCT.value",
        "gte": {
          "value": 80
        }
      }
    }
  },
  "actions": {
    "log_hits": {
      "foreach": "ctx.payload.aggregations.nodes.buckets",
      "max_iterations": 500,
      "logging": {
        "text": "Kubernetes node found with high CPU usage: {{ctx.payload.key}} -> {{ctx.payload.nodeCPUUsagePCT.value}}"
      }
    }
  },
  "metadata": {
    "xpack": {
      "type": "json"
    },
    "name": "Node CPU Usage"
  }
}

OOMKilled Pod 检测和警报

我们将探索的另一个 Watcher 用于检测由于 OOMKilled 错误导致重新启动的 Pod。此错误在 Kubernetes 工作负载中非常常见,尽早检测它非常有用,可以通知拥有此工作负载的团队,以便他们可以调查可能导致内存泄漏的问题,或者考虑增加工作负载本身所需的资源。

此信息可以从以下查询中检索

kubernetes.container.status.last_terminated_reason: OOMKilled

以下是我们如何使用 API 调用创建相应的 Watcher

curl -X PUT "https://elastic:changeme@localhost:9200/_watcher/watch/Pod-Terminated-OOMKilled?pretty" -k -H 'Content-Type: application/json' -d'
{
  "trigger": {
    "schedule": {
      "interval": "1m"
    }
  },
  "input": {
    "search": {
      "request": {
        "search_type": "query_then_fetch",
        "indices": [
          "*"
        ],
        "rest_total_hits_as_int": true,
        "body": {
          "size": 0,
          "query": {
            "bool": {
              "must": [
                {
                  "range": {
                    "@timestamp": {
                      "gte": "now-1m",
                      "lte": "now",
                      "format": "strict_date_optional_time"
                    }
                  }
                },
                {
                  "bool": {
                    "must": [
                      {
                        "query_string": {
                          "query": "data_stream.dataset: kubernetes.state_container",
                          "analyze_wildcard": true
                        }
                      },
                      {
                        "exists": {
                          "field": "kubernetes.container.status.last_terminated_reason"
                        }
                      },
                      {
                        "query_string": {
                          "query": "kubernetes.container.status.last_terminated_reason: OOMKilled",
                          "analyze_wildcard": true
                        }
                      }
                    ],
                    "filter": [],
                    "should": [],
                    "must_not": []
                  }
                }
              ],
              "filter": [],
              "should": [],
              "must_not": []
            }
          },
          "aggs": {
            "pods": {
              "terms": {
                "field": "kubernetes.pod.name",
                "order": {
                  "_key": "asc"
                }
              }
            }
          }
        }
      }
    }
  },
  "condition": {
    "array_compare": {
      "ctx.payload.aggregations.pods.buckets": {
        "path": "doc_count",
        "gte": {
          "value": 1,
          "quantifier": "some"
        }
      }
    }
  },
  "actions": {
    "ping_slack": {
      "foreach": "ctx.payload.aggregations.pods.buckets",
      "max_iterations": 500,
      "webhook": {
        "method": "POST",
        "url": "https://hooks.slack.com/services/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",
        "body": "{\"channel\": \"#k8s-alerts\", \"username\": \"k8s-cluster-alerting\", \"text\": \"Pod {{ctx.payload.key}} was terminated with status OOMKilled.\"}"
      }
    }
  },
  "metadata": {
    "xpack": {
      "type": "json"
    },
    "name": "Pod Terminated OOMKilled"
  }
}

从 Kubernetes 数据到告警摘要

到目前为止,我们已经了解了如何从普通的 Kubernetes 字段开始,在 ES 查询中使用它们,并在其之上构建 Watcher 和告警。

您可以探索更多可能的组合数据,并根据我们在此处提供的示例构建查询和告警。提供了 完整的告警列表,以及 安装它们的脚本基本方法

当然,这些示例带有简单的定义动作,这些动作仅将消息记录到 Elasticsearch 日志中。但是,您可以使用更高级和有用的输出,例如 Slack 的 Webhook。

"actions": {
    "ping_slack": {
      "foreach": "ctx.payload.aggregations.pods.buckets",
      "max_iterations": 500,
      "webhook": {
        "method": "POST",
        "url": "https://hooks.slack.com/services/T04SW3JHXasdfasdfasdfasdfasdf",
        "body": "{\"channel\": \"#k8s-alerts\", \"username\": \"k8s-cluster-alerting\", \"text\": \"Pod {{ctx.payload.key}} was terminated with status OOMKilled.\"}"
      }
    }
  }

结果将是如下所示的 Slack 消息

后续步骤

在接下来的步骤中,我们希望将这些告警作为 Kubernetes 集成的一部分,这意味着在用户安装或启用 Kubernetes 集成时将安装预定义的告警。同时,我们计划将其中一些实现为 Kibana 的原生 SLI,为用户提供通过友好的用户界面在 SLI 之上快速定义 SLO 的选项。如果您有兴趣了解更多信息,请关注公共 GitHub 问题以获取更多信息,并随时提供您的反馈。

对于那些渴望立即开始使用 Kubernetes 告警的用户,以下操作步骤供参考

  1. 确保您有一个正在运行的 Elastic 集群。部署集群最快的方法是启动 Elasticsearch Service 的免费试用版
  2. 按照相应的 文档在您的 Kubernetes 集群上安装最新的 Elastic Agent。
  3. 安装我们提供的告警,这些告警可以在 https://github.com/elastic/integrations/tree/main/packages/kubernetes/docshttps://github.com/elastic/k8s-integration-infra/tree/main/scripts/alerting 中找到。

当然,如果您有任何问题,请记住我们随时乐意在 Discuss 论坛 上提供帮助。