唯品会日志系统,承接了公司上千个应用的日志,提供了日志快速查询、统计、告警等基础服务,是保障公司技术体系正常运行必不可缺的重要系统之一。日均接入应用日志 600 亿条,压缩后大小约 40TB,大促时日志峰值流量达到每分钟 3 亿条。
唯品会日志系统,取名 Dragonfly,寓意像蜻蜓复眼一样,可以依据应用日志既准确又快速的观察到系统的运行细节、并发现系统的任何异动。最初,Dragonfly 是围绕开源的 ELK(Elasticsearch/Logstash/Kibana)技术栈打造的,后来架构不断演进,增加新的功能组件。目前的系统架构如下图所示。
但无论怎么演进,快速、稳定的日志查询始终是日志系统的核心服务所在,因此 Elasticsearch 可谓是 Dragonfly 系统中的核心组件。本文将重点介绍唯品会日志系统有效使用 Elasticsearch 的各种实践经验。
Elasticsearch 简介
朋友圈里这个月最为热议的技术新闻之一,就是 Elastic 公司 2018.10.6 在纳斯达克上市,当天开盘后股价就上涨了一倍。将一项技术通过开源不断做大做强,成为无可争议的垂直领域领导者,最终实现上市获得更大发展——Elastic 公司实现了无数软件创业公司的梦想,并提供了一个完美的创业成功典范。而 Elasticsearch 正是这家公司的招牌产品。
Elasticsearch(下面简称 ES)是基于 Apache Lucene 打造的分布式文档存储+文本查询引擎。它通过倒排索引(inverted-index)技术提供极快速的文本查询和聚合统计功能,通过合理的索引和分片设计,又可以支持海量的文本信息。因此非常适合用于搭建日志平台。
尽管如此,将这样一个开源软件,既要能贴合公司内部的实际使用场景,又要做到高吞吐、高容量、高可靠,还是需要做不少细致的工作,下面会一一道来。
Dragonfly 系统从 2015 年开始搭建,最初使用 Elasticsearch 1.x,后来升级到 5.6 版本。
硬件配置
Dragonfly 系统在不同机房搭建了多个 ES 集群(配置了跨集群查询),共上百台机器,最大的 2 个集群均有接近 50 台服务器。这些服务器包含了两种硬件配置类型:
一种使用多个 SSD 磁盘,并拥有性能强劲的 CPU。 SSD 服务器作为集群中的热数据服务器使用,负责新日志的写入,并提供使用频率最高的最近几天的日志查询服务。
另一种使用多个大容量的 HDD/SATA 磁盘,搭配较弱的 CPU。 HDD 服务器作为集群中的冷数据服务器使用,负责保存较早前的日志。由于早前的日志访问频率较低,因此不需要很好的性能,而看重磁盘容量。
两种类型服务器的磁盘均使用 RAID0 阵列,内存大小为 64GB 或 128GB。
通过冷热分离,可以保证 ES 集群的写入和实时查询性能,又能在成本较低的情况下提供较长的日志保存时间,下面还会详细介绍 ES 集群的冷热分离是怎么实现的。
日志索引管理
ES 中的文档数据是保存在索引(index)之中的。索引可以划分多个分片(shard)分布在不同的节点上,并通过配置一定的备份数实现高可用。
对于日志数据,通常把每个应用、每天的日志保存到不同的索引中(这是以下很多讨论的前提)。要承接公司上千个应用的海量日志,又要应对每个应用每天不同的数据量,如何有效管理这些日志索引,成为 Dragonfly 能否提供稳定和快速服务的关键。下面将分多个方面为你介绍。你不会在官方文档中直接找到这些知识,它们来源于我们多年的实战经验。
1. 索引预创建
当有一个写入请求,而请求中指定的索引不存在时,ES 会自动创建一个 5 分片的新索引。因为要完成寻找合适的节点、创建主分片、创建备份分片、广播新的集群信息、更新字段 mapping 等一系列的操作,创建一个新索引是耗时的。根据集群规模、新索引的分片数量而不同,在我们的集群中创建一个新索引需要几秒到几十秒的时间。如果要在新的一天开始的时刻,大量新一天的日志同时到来时触发创建上千个索引,必然会造成集群的长时间服务中断。因此,每天的索引必须提前创建。
此外,还有一个必须提前创建索引的关键原因,就是 ES 索引的分片数在创建时就必须给定,不能再动态增加(早期版本还不能减少,ES 5.x 开始提供了 shrink 操作)。在两种情况下会造成问题:
如果某个应用的日志量特别大,如果我们使用过少的分片数(如默认的 5 个分片),就会造成严重的热点问题,也就是分配到这个大索引分片的节点,将要处理更多的写入和查询数据量,成为集群的瓶颈。
大部分的应用日志量非常小,如果分配了过多的分片数,就会造成集群需要维护的活跃分片数目很大,在节点间定时同步的集群信息数据也会很大,在集群繁忙的时候更容易出现请求超时,在集群出现故障的时候恢复时间也更长。
出于以上考虑,必须提前创建索引,并为不同应用的索引指定合适的分片数量。分片的数量可以由以下公式计算:
n_shards=avg_index_size/ size_per_shard * magnified_factor
其中:
avg_index_size 为前 N 天这个应用索引大小的指数移动平均值(日期越靠近、权重越大)
size_per_shard 为期望每个分片的大小。分片过大容易造成上述热点问题,同时 segment merge 操作的开销更大(下面详细介绍),在需要移动分片时耗时也会较久;而过小又会造成集群分片总数过多。因此要选取一个均衡的大小,我们使用的是 20GB。
magnified_factor 为扩大系数,是我们根据公司特有的促销场景引入的。在促销日,业务请求数会数倍甚至数十倍于平时,日志量也会相应增长,因此我们需要在促销日为索引增加更多分片数。
索引预创建的操作我们放在前一天的清早(后面会看到,开销很大的操作我们都放在夜间和凌晨)进行。即今天凌晨会根据之前索引大小的 EMA 及上述公式计算出分片数,创建明天的索引。
2. 替补索引
这是很有意思的一个概念,你不会在任何其他 ES 相关的文章找到它(如果有,请注意百分百是抄袭:)
它解决的是这样的问题:即使我们已经提前估算出一个应用的日志索引的分片数,但是仍然会有异常情况。有时你会发现某个应用某一天的日志忽然增加了很多,可能是开发打开了 DEBUG 级别开关查问题,也可能在做压测,或者可能是 bug 死循环的打印了无数 stacktrace。有时是一个新接入的应用,由于没有历史索引,所以并不能估算出需要的分片数。以上情况都有可能引起分片数不足的问题。
记得上面提到过索引在创建后就不能再动态调整分片数了,怎么破?
让我们来新创建一个替补索引(substitude index)吧,这一天接下来的所有日志都会写到这里。替补索引的命名,我们会在原索引名的基础上加上-subst 后缀,因此用户在查询今天的日志时,查询接口仍可以通过一定的规则指定同时使用今天的“正选”索引和替补索引。
我们定时扫描当天索引的平均分片大小,一旦发现某个索引的分片大小过大,就及时创建替补索引。注意,创建替补索引时仍需指定分片数,这是根据当天已过去的小时数比上还剩下的小时数,以及当时已经写入的数据量估算得出的。
替补索引还有一种使用场景。有时促销是在晚上进行的,一到促销开始,请求量和日志量会出现一个非常大的尖峰。而由于已经写入了大半天的日志,分片已经比较大,这时任何的写入都可能引起更大规模的 segment merge(熟悉 ES 的同学会知道,每次 refresh 会生成新的 segment,小的 segment 会不断 merge 变成更大的 segment,越大的 segment merge 时对磁盘 IO 和 CPU 的开销占用越大。这相当于 HBase 的 compaction 行为,也同样会有写放大问题),会使写入速率受到限制。而如果这时切换到新的索引写入(场上球员请全部给我下来~),就可以减轻 merge 的影响,对保障流量尖峰时 ES 集群的写入速率非常有帮助。
这种促销日的替补索引是提前创建的。分片数的计算、以及还有可能使用到“替补的替补”,就不再赘述了。
3. Force Merge
当数据写入 ES 时先产生小的 segment。segment 会占用堆内存,数量过多对查询时间也有影响。因此 ES 会按一定的策略进行自动合并 segment 的动作,这前文已经提过。此外,ES 还提供 force merge 的方法(在早期版本称为 optimize),通过调用此方法可以进一步强化 merge 的效果。
具体的,我们在凌晨执行定时任务,对前一天的索引,按照分片大小以及每个 segment 500MB 的期望值,计算出需要合并到的 segment 数量,然后执行 force merge。
通过这个操作,我们可以节省很多堆内存。具体每个节点 segment 的内存占用,可以在_nodes//stats api 中查看到。当 segment memory 一项占用 heap 大小 2/3 以上,很容易造成各种 gc 问题。
另外由于 force merge 操作会大量读写磁盘,要保证只在 SSD 服务器上执行。
4. 冷热分离
最初搭建 ES 集群时,为了保证读写速率,我们使用的都是 SSD 类型的服务器。但由于 SSD 磁盘成本高,容量小,随着接入应用越来越多,不得不缩短日志保存天数,一度只能提供 7 天的日志,用户开始产生抱怨。
我们自然而然产生了冷热分离的想法。当时在网上并不能找到 ES 冷热分离部署的案例,但出于曾经翻阅过官方文档多遍后对 ES 各种 api 的熟悉,我们认为是可行的。
首先,在节点的配置文件中通过 node.tag 属性使用不同的标签可以区分不同类型的服务器(如硬件类型,机柜等);
通过 index.routing.allocation.include/exclude 的索引设置可以指定索引分片只能分配在某种 tag 的服务器上;
利用以上方法,我们在创建的新的索引时,指定只分配在热数据服务器(SSD 服务器)上。使拥有强劲磁盘 IO 和 CPU 性能的热数据服务器承接了新日志的写入和查询;
设置夜间定时任务,将 N 天前的索引修改为只能分配在冷数据服务器(HDD 服务器)上。使访问频率降低的索引 relocate 到大磁盘容量的服务器中保存。
通过以上冷热分离的部署方法,我们只增加了少量大磁盘容量的服务器(成本比 SSD 类型的服务器还低很多),就将日志保存时长增加到了 30 天,部分应用可以按需保存更长时间。
5. 日志归档
从上面可以看出,通过冷热分离,热数据服务器只存放最近 3 天的日志索引,其余 27 天的索引都存放在冷数据服务器上。这样的话,冷数据服务器的 segment memory 将非常大,远超过官方文档建议的不超过 32GB 的 heap 配置。如果要增加 heap 到 32GB 以上,会对性能有一定影响。
考虑到越久远的日志,查询的可能性越低,我们将 7 天以前的日志进行了归档——也就是 close 索引的操作。close 的索引是不占用内存的,也就解决了冷数据服务器的 heap 使用问题。
Dragonfly 前端放开放了归档日志管理的页面,用户如需查询 7 天前的日志,可以自助打开已经归档的日志——也就是重新 open 已经 close 掉的索引。
日志写入降级策略
前面已经提到过,我们公司促销高峰时的日志量会达到平日的数十倍。我们不可能配备足够支撑促销高峰期日志量的服务器规模,因为那样的话在全年大部分时间内很多服务器资源是浪费的。而在服务器规模有限的情况下,ES 集群的写入能力是有限的,如果不采取任何措施,在促销高峰期、以及万一集群有故障发生时,接入的日志将发生堆积,从而造成大面积的写入延迟,用户将完全无法查询最新的日志。此时如果有业务故障发生但无法通过日志来分析的话,问题会非常致命。
因此就需要有合理的降级措施和预案,以保证当日志流入速率大于日志写入 ES 集群的速率时,Dragonfly 仍能提供最大限度的服务。我们首先需要做出取舍,制订降级服务的目标,最终我们采用的是:保证各个应用都有部分服务器的日志可以实时查询。
唯品会的日志,是从应用服务器先采集到 Kafka 做缓存的,因此可以在读取 Kafka 数据写入 ES 的过程中做一些处理。严格来说如何实现这一目标并不属于 Elasticsearch 的范畴。
要达到降级服务目标,我们需要设计一套组合拳:
日志上传到 Kafka 时,使用主机名作为 partitioning hash key,即同一台应用服务器的日志会采集到同一个 Kafka topic 的 partition 中;
在消费 Kafka topic 时,使用同一个 consumer group 的多个实例,每个 consumer 实例设置不同的处理优先级;
对 Kafka consumer 按照优先级不同,配置不同的写 ES 的线程数。优先级越高,写入线程数越多,写入速率越快;优先级越低,写入线程数越少,甚至暂停写入;
这样就能保证高优先级的 consumer 能够实时消费某些 partition 并写入到 ES 中,通过 hashing 采集到那些被实时消费的 partition 的主机日志,也将在 ES 中可以被实时查询;
随着度过日志流量高峰,当 ES 集群支持高优先级 consumer 的日志写入没有压力时,可以逐步增加次高优先级 consumer 的写入线程数。直到对应 partition 的堆积日志被消费完,又可以对更低优先级的 consumer 进行同样的调整;
最终所有堆积的日志都被消费完,降级结束。
结语
除了核心组件 Elasticsearch,Dragonfly 日志系统还在日志格式规范、日志采集客户端、查询前端、规则告警、错误日志异常检测等方面做了很多的工作,有机会再跟大家继续分享。
本文由唯技术公众号(VIP-Tech)授权 InfoQ 中文站转载。
评论