本文讲的是高可用日志探险——基于 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 恢复日志。
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 集群,如果这样的话 - 你也不会读到这里吧?)
因为我经常备份, 我一直想为自己编写一个自动备份代理,但写成之前,需要手动备份。我的建议是:经常使用,不要在意昂贵的仪表板和可视化的损失。
一个有趣的事实:你会注意到,当导入数据时,如果可视化需要的字段丢失,导入将会中断!这是一个烦人的bug,除非你遵循我的建议,在持久化和数据恢复上做些工作,所以请留意这个问题。
脚本化字段
上面提到过脚本字段是如何扩展仪表盘。这些字段不是 Kibana 输出的一部分,如果每个查询都运行,似乎会产生一些负担,所以我们不会过度使用它们。如果你发现自己改变了很多字段,你应该去改变日志记录的内容。
例如,对于一些图表,我们喜欢将存储为秒数的字段转换为小时数,并绘制小时数。为了实现这一点,我们将其扩展到一个新字段:
double val = doc['connection_duration'].value;
return val / (60.0 * 60.0);
Elastic 是在文档的几个部分宣称可能会提供这些“无痛”脚本,我相信效果会很棒。但是,直到它变得更加健壮,我建议保持简单,并在少数可选择的地方使用它。
图表中的过滤器
我发现过滤器非常有用,令人惊讶的是文档中并没有很好的描述。所以想要在这里记录一下。
过滤器可用作图形拆分,并允许你将子查询放入图表中,仅显示过滤后的结果。根据我的经验,将这些与所记录的“合适的内容”结合起来通常足以满足大多数场景的需求。我觉得一图胜千言,所以这里随便举个例子,根据消息中一条数据的值将事件分为 “Long”,“Short” 和 “Failed”。请注意,第三个过滤器使用了更复杂的查询,如布尔 和 “AND” 逻辑,这些功能都非常方便。
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(翻译:李加庆)
原文发布时间为:2017-08-01
本文作者:李加庆
本文来自云栖社区合作伙伴Dockerone.io,了解相关信息可以关注Dockerone.io。
原文标题:高可用日志探险——基于 Kubernetes 中的 ELK