高可用日志探险——基于 Kubernetes 中的 ELK


【编者的话】本文主要讲述了作者的团队基于 Kubernetes 中的 ELK,构建高可用日志系统的实践,并总结一些过程中踩到的坑和一些对于从头开始搭建的建议,本文也是此系列文章的第一篇,后续文章可持续关注。

【3 天烧脑式基于Docker的CI/CD实战训练营 | 北京站】本次培训围绕基于Docker的CI/CD实战展开,具体内容包括:持续集成与持续交付(CI/CD)概览;持续集成系统介绍;客户端与服务端的 CI/CD 实践;开发流程中引入 CI、CD;Gitlab 和 CI、CD 工具;Gitlab CI、Drone 的使用以及实践经验分享等。

在 Parsec,我们是一个负责整个堆栈问题的小团队。我们四个人主要负责基于 Mac、Windows、Linux 以及树莓派广域网 PC 游戏的低延迟,并为即将到来的平台发布提供支持。尽可能通过优化硬件,让您获得低延迟。我们服务的游戏玩家从澳大利亚遍布到北美,他们希望瘦客户端可以梦想成真,而 AWS 主办、Kubernetes 支持的基础设施就有这种魔力。

早期,为了解决出现的问题,我们需要一个可扩展但易于管理的日志解决方案,以便我们可以花费精力来构建用户所需的所有功能,而不是被恼人的日志系统调整萦身。

在过去的几个月中,我们已经尝试并调整了我们的解决方案,并达到了一个我认为可以运行良好的状态。因此,我想展示我们所做的一些工作,突出我们所学到的东西,并希望帮助其他人愉快地开启创业梦想。

这篇文章将是一个简短系列中的第一篇,详细介绍了我们堆栈的各个方面,以及我们遇到困难时思维是如何转变的。

对于日志的初始化设置,我们制定了双重目标。将所有的日志放到一个系统中,使其可靠,而不用花费太多时间管理。我过去曾经使用过Elasticsearch,令人印象深刻。所以我受欲望驱使,看是否可以把它作为一个“一站式”的衡量标准和日志拖尾(tailing)。我们也不知道日志可能要作何用途,但我们需要将日志安全地存储起来,以便我们需要的时候可以获取到。

为此,我们安装了使用 logstash 的 HTTPS 终端,所有 logger 都可以通过简单的 POST 进行通话。反过来可以将所有消息转发到Elasticsearch 和 S3 上。之后我们可以使用 Kibana 查询和可视化日志,并在失败的情况下从 S3 恢复日志。
1.jpeg

Elasticsearch

事实上,Elasticsearch 不仅仅是一个组件,而且值得用整篇幅来写它,但是已经有一些比我更加了解 Elasticsearch 的人写过很多文章来介绍,所以这里我就不再赘述。我们决定在由 Pires 构建的 container 上自行托管我们的集群库,你可以在这里以及这里了解到更多信息。

因为我们不需要支持旧的日志系统,遵循“更新更好”的原则,所以文中使用的是最新版本的 Elastic 套件,即 5.X。

我们从 ES 身上学到的最重要的一点是,它很脆弱,特别是当你像我们一样在 Kubernets 上运行它时。你可以将其设置如你所想的那样健壮,但以我估计,这样做不值得。ES 是一个非常复杂的软件,它可能会因为各种原因而挂掉,所以与其担心持续的正常运行和恢复,最好还是在 ES 之前有一个持久层,在 ES 挂掉的时候可以重新向 ES 打 log。第一次安装 ES 时,我们以为可以管理它,并且不用太担心备份。但当 ES 挂掉时就,我们被打脸了,日志无处可寻。我们从内存压力,磁盘使用量到难以定位的索引损坏等方面出现了一些问题,并且索引已经不止一次地被破坏了。我们从日志丢失的事实中汲取了教训,所以现在我们合理的看待 ES - “内存”索引可以很容易地消失,但也可以从持久的“磁盘”备份很容易地恢复。自己动手,并提前备份。

特别是在 kubernetes 上运行 ES,这里是对我帮助比较大的一些捷径:
  • 使用 _eth0_ 作为网络主机,如这里所述。
  • 增加运行 ES 的节点的磁盘大小。默认的 20G 会很快用完,特别是当 Kubernetes 尝试恢复失败的节点并复制磁盘上的索引时。
  • 如果追求更好的性能,请首先增加 es 数据 节点的内存分配,因为这个节点完成了大部分工作。我们的 master 节点分配了 256M,目前没有任何问题。
  • 确保运行 curator 来清理旧的索引,只保留合理的查询数量(且需要适配你的群集)。


我不确定管理自己的 Elasticsearch 是正确的解决之道,还是使用 AWS 托管 ES 更加划算。我发现纠结这件事给自己上了一堂有价值的课,但同时感到心碎,也浪费了时间。如果你真想搭建自己的 ES,我建议让 AWS 为你处理。我们可能会在不久的将来尝试迁移到 AWS,所以请密切关注以后的博客。

最后一个注意事项:注意日志记录的内容!如果发送到 Elasticsearch 的任何字段有类型冲突,ES 将拒绝该消息,这时你可能百思不得解,为何看不到任何条目。我鼓励发送尽可能多的结构化数据 - ES 似乎对处理数千个字段没有太多压力,但是不要将 user_id 从字符串 '123' 切换到整数 123(或 ES 中的数字类型),除非你想找事儿。我仍然没有找到一个很好的方法在 ES 中来强制类型转换(如果你正在阅读本文并知道如何实现,请告诉我),所以请保持跨栈的嵌套属性没有冲突,清理有冲突的索引是一件痛苦的事儿。

Kibana

我们决定坚持使用一个可视化解决方案,因为在确切地知道哪些问题值得解决之前,我们不想太快地提升装备。面向用户的功能成就了今天的 Parsec,最终我们只需要足够的指标来帮助我们明确要做什么。

Kibana 看起来很棒,大大降低了和 ES 笨重的 API 进行交互的难度。你可以点击按钮搜索并显示日志语句。别搞错了,搜索是 ES 做的,而且做得不错。你可以使用布尔逻辑或匹配特定字段,轻易搜索到子串匹配。由于所有东西都被索引了,所以通常搜索速度非常快。当你第一次通过成千上万的分散的日志语句,在几秒钟内找到只有六个相关的消息时,Arthur C. Clarke 的话涌入脑海,“任何足够先进的技术与魔术别无二致”。

但是,在 Kibana 的世界里,并不全是优点,一些缺点也值得一提。

在学习曲线方面,Kibana 就像一个平缓的草坡,顶上是一个纯粹的砖墙。大多数事情都是简单直观的,生成的图表看起来很漂亮,表现也很出色。不过,你有时会尝试做一些相当简单的工作(“如何过滤掉10以下的值”),你会发现他们好看的文档会失真,Google 查询结果可能会出现无关的、来自旧版本文档(我感觉4.x版本更受欢迎)。当你找到答案时,它经常会向你解释如何在 Elasticsearch 中直接进行某些操作,这和回答“做不到”的效果一样。如果你对搜索是认真的,系统的学习 Elasticsearch 是极好的,但如果你像我一样,对日志只是射后不理(fire-and-forget),就不要花时间学习它了。

与 Grafana 相比,Kibana 是一个非常笨拙的可视化框架。它确实是用于日志可视化,但我们的目标是使其承担专用度量基础设施的全部责任。一些简单的任务无法如预期或者安装复杂,其中大部分归因为 Kibana 以日志为中心的范式。例如,缩放日志不是图形上的转换,而需要使用日志消息字段上的脚本来完成,因此,ES 将在每个日志语句中创建一个临时字段,用于计算,即使只有一个图的规模。

但是,无论如何,一旦你习惯了 Kibana 的怪癖,它完全可以作为实时指标的基础,你只需要对自己的日志和记录进行一些限制。下面是我发现有价值但很难发现的事情:

注意日志记录的内容(II)

Kibana 可以绘制日志记录的所有内容,但是你需要使用发送的日志来规划应用程序所要显示的内容。我的建议是记录比你认为所需更多的字段,将每个日志语句作为一个潜在的指标,但不要随意发送所有的对象。所以,如果你还没统计事件的数量,可以仿照本例[标准Python日志记录]:
logger.info(“I did something”, extra={“something_count” : 1})  

logger.info(“I did a test”, extra={“test_worked” : 1 if worked else 0}) 

这种做法很好,因为直接使用 Kibana 可以很容易地统计,求和和绘制图表。但是,像下面这样做:
try:

except Exception as e:
logger.info(“there was an error!”, extra={“exception” : e})
resp = json.loads(requests.post(“http://some-api.com”, json={“oh”:  “my!”}))
logger.info(“response”, extra={response: resp}) 

就是在找事儿,在本例中,如果 API 响应中的类型或字段发生改变,ES 可能会间歇性地拒绝“响应”消息,并且异常对象可能会被你没注意到一些字段填充。以我的经验来看,最好是明确记录你感兴趣的字段,例如行号和位置、响应状态代码和你关心的字段,从而保持索引的清洁。

还值得注意的是,Kibana 不能(据我所知)在同一个图上绘制两个不同的搜索查询。所以,如果你认为两件事情可能相关,并且想要在一张图表中使用,最好将它们记录在一起。
logger.info(“number of active users”, 
extra={“num_active”: num_active})
logger.info(“total users”, 
extra={“num_users”: num_users})  

像上面这样,不能通过 “users” -- 不唯一的搜索字符串进行搜索,你就不能将活跃用户与总用户放在一个图表上,而需要更加明确的指标:
logger.debug(“there are {} active users”.format(num_active))
logger.info(“active users metric”, 
extra={“num_active”: num_active, “num_users”: num_users}) 

这里的假设是 debug 日志用于展示感兴趣的东西,但与任何图形无关,只有 info 日志会被发送到 ES。现在,您可以通过“活跃用户指标”进行搜索,并使用过滤器绘制这些元素(下面会提到更多)。

备份 Kibana

如果你没有阅读任何关于 Kibana 的内容,请记住此图片。这个按钮很有用(除非你已经有一个稳定的,牛逼的 ES 集群,如果这样的话 - 你也不会读到这里吧?)
2.png

因为我经常备份, 我一直想为自己编写一个自动备份代理,但写成之前,需要手动备份。我的建议是:经常使用,不要在意昂贵的仪表板和可视化的损失。

一个有趣的事实:你会注意到,当导入数据时,如果可视化需要的字段丢失,导入将会中断!这是一个烦人的bug,除非你遵循我的建议,在持久化和数据恢复上做些工作,所以请留意这个问题。

脚本化字段

上面提到过脚本字段是如何扩展仪表盘。这些字段不是 Kibana 输出的一部分,如果每个查询都运行,似乎会产生一些负担,所以我们不会过度使用它们。如果你发现自己改变了很多字段,你应该去改变日志记录的内容。

例如,对于一些图表,我们喜欢将存储为秒数的字段转换为小时数,并绘制小时数。为了实现这一点,我们将其扩展到一个新字段:
double val = doc['connection_duration'].value;
return val / (60.0 * 60.0);

Elastic 是在文档的几个部分宣称可能会提供这些“无痛”脚本,我相信效果会很棒。但是,直到它变得更加健壮,我建议保持简单,并在少数可选择的地方使用它。

图表中的过滤器

我发现过滤器非常有用,令人惊讶的是文档中并没有很好的描述。所以想要在这里记录一下。

过滤器可用作图形拆分,并允许你将子查询放入图表中,仅显示过滤后的结果。根据我的经验,将这些与所记录的“合适的内容”结合起来通常足以满足大多数场景的需求。我觉得一图胜千言,所以这里随便举个例子,根据消息中一条数据的值将事件分为 “Long”,“Short” 和 “Failed”。请注意,第三个过滤器使用了更复杂的查询,如布尔 和 “AND” 逻辑,这些功能都非常方便。
3.png

S3 实现持久化

说到魔术,S3 则是另一个令人兴奋的软件或者说基础设施,对于我们的案例来说,这是托管我们原始日志的最好解决方案。S3 被广泛使用,上一次宕机时,整个互联网基本上都瘫痪了,所以当这种情况出现时,你通常会遇到比日志更严重的问题。

我们的持久性策略是将日志尽快传送到 S3 上,之后根据需要重现。这样做给我们带来了很大的灵活性。例如,我一直在做的是在 ES / Kibana 中展示长期图表,将统计数据拉入和过滤成专用的“长期” ES 索引。为了支持上述想法(或将来用于适当的指标基础设施),对所有日志进行备份是至关重要的。日志放在远端使我们能够快速,宽松地使用 kubernetes 集群,即使容器被破坏,也不用担心容器中存储的状态。

Logstash

Logstash 将所有这些组件都集成在一起。简而言之,我们使用 Logstash,是因为我们必须要用 - 根据我的估计,当谈到日志过滤,聚合等问题是,Logstash 并不是最佳之选,但它使用广泛以及容易配置,所以它是最佳之选。

对于我们来说,我们采用集中式的日志 API,所有 logger 都可以连接。logstash 的 http 插件前面运行 nginx,后者又转发到 ES 和 S3。

输入

就如之前所述,logstash 易于配置,所以不会让人头大。
input {
http {
port => 8080
}


你不必在 logstash 之前使用 nginx,因为我发现期间需要考虑的东西太多。如果你想支持SSL,使用 nginx 是必须的。

如果你正在发送像我们这样的日志,也可以使用codec => json,但是如果你使用 application/json 内容类型发送消息,则可以指定插件。

过滤器

以下是过滤器部分:
filter {
….


丢弃事件

if [headers][request_path] == “/liveness” {
drop {}


Drop 可以方便地进行健康检查,活力探测等。这些调用仍然会返回 “ok”,就好像它们遍历了整个管道,其实是被立即丢弃。保持容器的活力探测是一个好主意,而且很容易设置。只需确保你将活力探测设置在正确的容器上。最初,我们在 nginx 容器上设置了活动探测器,所以当 Logstash 关闭并且探测失败时,需要重新启动 Logstash 时Kubernetes 尝试重新会启动 nginx 不会产生影响。

节流

throttle {
after_count => 2
period => 10
max_age => 20
key => “user_id”
add_tag => “throttle_warn”
}
throttle {
after_count => 3
period => 10
max_age => 20
key => “user_id”
add_tag => “throttled”
}
if “throttled” in [tags] {
drop{}
}
if “throttle_warn” in [tags] {
mutate {
replace => {“message” => “throttling events for user:%{user_id}” }
}


如果你在某些情况下有很多的日志,就可以耍一个一本正经的小把戏。如果你在发送端没有解决这个问题,实际上可以指示 logger 有条件地将其删除。上面的示例仅用于举例,因为它是非常严格的 - 发送警告消息后,在间隔10秒内它会发现两个消息有相同的 “user_id” 字段,并丢弃所有后续有此 ID 的消息,直到这种情况不再发生。

强类型

mutate {
convert => {
“user_id” => “integer”
}


与其说是在 ES schema 中处理类型,不如像文章末尾那样进行强制类型转换,让我们自己变得舒服。

增加环境变量

mutate {
add_field => {“[@metadata][docker_compose]” => “${DEBUG}” }


检查布尔逻辑时,Logstash 无法从 env 变量中读取,所以 if/else 处理起来可能比较困难。幸运的是,有一个简单的办法,通过添加实际上不会添加到事件中的 @metadata 标签,然后在过滤器中查询这些标签(见下文)。

输出

output {
elasticsearch { hosts => [“${ES_HOST}:9200”] }
if [@metadata][docker_compose] != “true” {
s3 {
bucket => “your-logs”
prefix => “logstash/”
codec => “json”
encoding => “gzip”
access_key_id => “${AWS_ACCESS_KEY_ID}”
secret_access_key => “${AWS_SECRET_ACCESS_KEY}”
region => “us-east-1”
}
}


向 ES 发送消息与设置 HTTP 输入几乎一样简单!我不敢想象 JRuby 的代码竟然必须在引擎盖(hood)之下?

值得注意的是,当我们在本地开发时,使用 env 变量来关闭 S3 输出(如你所猜测的,我们使用 docker-compose 提供正确的 env 环境变量并充当我们“本地” 的 Kubernets)。

下面给出一个完整示例:
input {
http {
port => 8080
}
}

filter {

if [headers][request_path] == "/liveness" {
drop {}
}

throttle {
after_count => 2
period => 10
max_age => 20
key => "user_id"
add_tag => "throttle_warn"
}

throttle {
after_count => 3
period => 10
max_age => 20
key => "user_id"
add_tag => "throttled"
}

if "throttled" in [tags] {
drop{}
}

if "throttle_warn" in [tags] {
mutate {
  replace => {"message" => "throttling events for user:%{user_id}" }
}
}

mutate {
convert => {
  "user_id" => "integer"
}
}

mutate {
add_field => {"[@metadata][docker_compose]" => "${DEBUG}" }
}

}

output {
elasticsearch { hosts => ["${ES_HOST}:9200"] }

stdout { }

if [@metadata][docker_compose] != "true" {
s3 {
  bucket => "your-logs"
  prefix => "logstash/"
  codec => "json"
  encoding => "gzip"
  access_key_id => "${AWS_ACCESS_KEY_ID}"
  secret_access_key => "${AWS_SECRET_ACCESS_KEY}"
  region => "us-east-1"
}


总结

总而言之,我们的初始日志安装是相当简单的。Logstash 和 Nginx 在kubernetes 中的一组 pod 中运行,并且附加了所有的 logger,并通过 JSON 写入日志。我们将日志进行少许改动,然后直接发送到 Elasticsearch 和 S3,并使用 Kibana 来实时可视化数据。

在下一部分中,我将进一步详细介绍如何实现高(更高)可用性。当 Logstash HTTP 端点(不可避免地)挂掉时,我们怎么做?从集群启动时,如何恢复 ES?

原文链接:Adventures In High Availability Logging — Elasticsearch, Logstash, and Kibana (ELK) on Kubernetes(翻译:李加庆

0 个评论

要回复文章请先登录注册