探索ES高可用:滴滴自研跨数据中心复制技术详解

Elasticsearch 是一个基于Lucene构建的开源、分布式、RESTful接口的全文搜索引擎,其每个字段均可被索引,且能够横向扩展至数以百计的服务器存储以及处理TB级的数据,其可以在极短的时间内存储、搜索和分析大量的数据。

滴滴ES发展至今,承接了公司绝大部分端上检索和日志场景,包括地图POI检索、订单检索、客服、内搜及把脉ELK场景等。

近几年围绕稳定性、成本、效率和数据安全这几个方向持续探索:

  • 滴滴ES有很多在线P0级检索场景,为了提升集群稳定性,我们自研了跨数据中心复制能力,实现多机房数据写入强一致性,并配合管控平台让ES支持多活能力;
  • 为了提升查询性能和解决查询毛刺问题,我们在7.6版本上原地升级支持JDK 17;
  • ES日志场景每天写入量在5PB-10PB量级,写入压力和业务成本压力大,为了提升ES的写入性能,我们让ES支持ZSTD压缩算法;
  • 由于ES索引里包含很多敏感数据,我们又完善了ES的安全认证能力。

基于以上探索,我们总结了一定的经验,现分成4篇文章详细介绍。本篇文章介绍滴滴ES如何实现索引的跨数据中心复制从而保证索引的高可用。

滴滴跨数据中心复制能力 - Didi Cross Datacenter Replication,由滴滴自研,简称DCDR,它能够将数据从一个 Elasticsearch 集群原生复制到另一个 Elasticsearch 集群。如图所示,DCDR工作在索引模板或索引层面,采用主从索引设计模型,由Leader索引主动将数据push到Follower索引,从而保证了主从索引数据的强一致性。

DCDR跨数据中心复制能力图

Read More

Elasticsearch常用的命令

集群健康状态相关

查看集群健康状态

1
GET _cluster/health
number_of_pending_tasks:是指主节点创建索引并分配shards等任务,如果该指标数值一直未减小代表集群存在不稳定因素 。

查看未分配原因

1
GET _cluster/allocation/explain

有以下几种可能

  • INDEX_CREATED : Unassigned as a result of an API creation of an index. 索引创建 : 由于API创建索引而未分配的
  • CLUSTER_RECOVERED : Unassigned as a result of a full cluster recovery. 集群恢复 : 由于整个集群恢复而未分配
  • INDEX_REOPENED : Unassigned as a result of opening a closed index. 索引重新打开
  • DANGLING_INDEX_IMPORTED : Unassigned as a result of importing a dangling index. 导入危险的索引
  • NEW_INDEX_RESTORED : Unassigned as a result of restoring into a new index. 重新恢复一个新索引
  • EXISTING_INDEX_RESTORED : Unassigned as a result of restoring into a closed index. 重新恢复一个已关闭的索引
  • REPLICA_ADDED : Unassigned as a result of explicit addition of a replica. 添加副本
  • ALLOCATION_FAILED : Unassigned as a result of a failed allocation of the shard. 分配分片失败
  • NODE_LEFT : Unassigned as a result of the node hosting it leaving the cluster. 集群中节点丢失
  • REROUTE_CANCELLED : Unassigned as a result of explicit cancel reroute command. reroute命令取消
  • REINITIALIZED : When a shard moves from started back to initializing, for example, with shadow replicas. 重新初始化
  • REALLOCATED_REPLICA : A better replica location is identified and causes the existing replica allocation to be cancelled. 重新分配副本

查看具体索引未分配或不搬迁的原因

1
2
3
4
5
6
GET _cluster/allocation/explain
{
"index":"index_name",
"shard":0,
"primary":true
}

返回结果:

  1. The current state of the shard.
  2. The reason for the shard originally becoming unassigned.
  3. Whether to allocate the shard.
  4. Whether to allocate the shard to the particular node.
  5. The decider which led to the no decision for the node.导致该节点没有决策的决策器。
  6. An explanation as to why the decider returned a no decision, with a helpful hint pointing to the setting that led to the decision. 解释为什么决策器返回“否”决策,并提供一个有用的提示,指出导致该决策的设置。

解释一个索引为什么分配到该节点

1
2
3
4
5
6
7
GET _cluster/allocation/explain
{
"index": "my-index-000001",
"shard": 0,
"primary": false,
"current_node": "my-node"
}

重新尝试分配失败的shard

1
POST _cluster/reroute?retry_failed=true

查看等待中的任务

1
GET _cat/pending_tasks

可以看到任务都被指派了优先级( 比如说 URGENT 要比 HIGH 更早的处理 )

查看nodes

1
GET _cat/nodes

Read More

科普下搜索索引里常用的压缩算法

前言

数据压缩是存储领域常用的优化手段,压缩算法可以减少数据的大小减少存储成本、减少磁盘的寻道时间提高I/O的性能、减少数据的传输时间并提高缓冲区的命中率,节省的I/O时间可以轻易补偿它带来的CPU额外开销。目前在用的主流压缩算法包括zlib、snappy和lz4等。压缩算法并不是压缩比越高越好,压缩比越高,其解压缩速度可能越慢,CPU消耗就会越大,这需要根据硬件配置和业务场景做Trade off。本文主要介绍了如下几种搜索索引里常见的压缩算法,大部分压缩算法也适用于OLAP领域,同时有些场景可能需要结合多种场景实现,比如倒排索引的posting list压缩就需要结合PForDelta及Simple算法才能获得更好的压缩比。同时在选择压缩算法时,也会考虑该压缩算法是否支持流式压缩等。

  • Fixed length
  • Variable Byte
  • Improved Variable Byte
  • Group Varint
  • Run Length Encoding
  • Dictionary Coding
  • Simple 9
  • Simple 16
  • PForDelta
  • Huffman Coding
  • LZ77
  • Elasticsearch 行存压缩算法

Fixed length

压缩方法

找到一组数据的最大值,之后计算出最大位宽N:

示例

1
10,35,100,170,370,29000,30000,30010

2^15 = 32768 > 30010,则位宽为15,即每个32bit的数据可以用15bit表示

Read More

Elasticsearch性能调优之毫秒级搜索POI业务

背景

地图POI搜索业务使用的Elasticsearch集群是自建的2.3版本,维护ES需要耗费很多精力和人力。而目前公司有专门的ES搜索团队,且ES版本已经升级到7.6版本。ES 7.6版本有很多新的特性,如IndexSorting,查询加速,索引off-heap,全新的熔断器等等。6月份,ES搜索团队决定与地图POI合作共建,将ES升级到公司里的ES 7.6,专业的人做专业的事,ES团队维护集群稳定性、提升性能等,这样地图POI团队可以更好的聚焦到业务架构和推荐系统中台化上。

地图POI分为国内POI和国际化POI,其面向在线搜索业务,对搜索耗时要求比较高,国内POI要求TP50耗时小于5ms,TP99 耗时小于20ms,国际化POI要求TP50耗时小于5ms,TP99耗时小于60ms。且两个业务方的索引查询超时时间都为180ms。由于其业务特点,所以在迁移ES 7.6时,并非一帆风顺,遇到很多性能和稳定性问题,这篇文章会介绍下遇到的性能问题和解决方法。由于篇幅有限,本文只介绍了影响性能最大的三个问题及优化手段和排查问题过程,分别为:

  • 前缀查询优化
  • 查询截断
  • 查询超时毛刺

前缀查询优化

调好合适的分片数,之后将索引从Hive离线导入到ES,做一轮压测,耗时曲线图如下图所示,耗时都接近800ms了,并且超过180ms就认为是超时,这与期望的性能有比较大的差距:

通过gateway里面的审计日志,计算出国际化POI TP50、TP90、TP99时间,如下图所示:

可以看到TP99时间到了619ms,这与预期的性能有很大差距。所以我们需要看下为什么查询性能这么慢,我们将审计日志里面的压测DSL全部拿到,之后根据查询耗时降序排序,然后将DSL模板归类,发现耗时最久的是类似下面这种DSL:

Read More

ES 内存管理分析

刚接触ES,研究了下ES内存管理,参考了一些文章,整理了一篇文章,方便自己记忆。

命令 GET _cat/nodes?help 列出所有node, 并展示node所在机器的运行状态信息,help可显示帮助信息

1
GET _cat/nodes?h=name,hp,hm,rp,rm,qcm,rcm,fm,sm&v

解析下上面参数的意义

信息如下:

以红框里的node为例, 内存占用 = (8.6 gb)qcm + (1gb) rcm + (0.35gb) fm + (2.2 gb)sm,大约12 gb。

Read More

使用火焰图定位 OLAP 引擎瓶颈

在维护 OLAP 引擎时,很多时候需要对引擎做系统的性能分析和优化,此时往往需要查看 CPU 耗时,了解主要耗时点及瓶颈在哪里。俗语有曰:兵欲善其事必先利其器,程序员定位性能问题也需要一件“利器”。性能调优工具(perf)能够显示系统的调用栈及时间分布,但是呈现内容上只能单一的列出调用栈或者非层次化的时间分布,不够直观。火焰图(flame graph)能够帮助大家更直观的发现问题。本文将以 Presto 为例,介绍火焰图的使用技巧。

初识火焰图

Perf 的原理是这样子的:每隔一个固定的时间,就在 CPU 上(每个核上都有)产生一个中断,在中断上看看,当前是哪个 pid,哪个函数,然后给对应的 pid 和函数加一个统计值,这样,我们就知道 CPU 有百分几的时间在某个 pid,或者某个函数上了。而火焰图(Flame Graph)是由 Linux 性能优化大师 Brendan Gregg 发明的,和所有其他的 profiling 方法不同的是,火焰图以一个全局的视野来看待时间分布,它从底部往顶部,列出所有可能导致性能瓶颈的调用栈。

火焰图整个图形看起来就像一个跳动的火焰,这就是它名字的由来。火焰图有以下特征(这里以采样CPU 火焰图为例):

  • 每一列代表一个调用栈,每一个格子代表一个函数。
  • 纵轴展示了栈的深度,按照调用关系从下到上排列。最顶上格子代表采样时,正在占用 CPU 的函数
  • 横轴的意义是指:火焰图将采集的多个调用栈信息,并行关系。横轴格子的宽度代表其在采样中出现频率,所以一个格子的宽度越大,说明它是瓶颈原因的可能性就越大。
  • 火焰图格子的颜色是随机的暖色调,方便区分各个调用信息。
  • 其他的采样方式也可以使用火焰图, 比如内存

所以,火焰图就是看顶层的哪个函数占据的宽度最大。只要有”平顶”,就表示该函数可能存在性能问题,也是我们性能优化收益最大的地方。
Java生态常见的用于perf的工具有:allocation-instrumenter、YourKit Profiler、async-profiler、JProfiler、Arthas(基于 async-profiler )。笔者推荐使用阿里巴巴出品的 Arthas 或 async-profiler,笔者喜欢使用 async-profiler 这个 perf 工具生成火焰图,主要原因是用法简单,足够满足日常排查性能问题了。

Read More

说下那些导致Presto查询变慢的JVM Bug和解决方法

背景

维护Presto集群,遇到了一些JVM Bug会严重导致Presto查询变慢,这里分享下Bugs表现及解决方法。

Ref Proc 耗时太久

线上Presto运行一段时间后,查询会越来越慢,打了下火焰图,发现30%的CPU都浪费在young gc上了,看了下gc log,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

2020-12-21T20:50:15.009+0800: 4753827.928: [GC pause (GCLocker Initiated GC) (young) (initial-mark), 0.5723063 secs]
[Parallel Time: 66.9 ms, GC Workers: 33]
[GC Worker Start (ms): Min: 4753827940.4, Avg: 4753827940.5, Max: 4753827940.7, Diff: 0.4]
[Ext Root Scanning (ms): Min: 26.2, Avg: 27.3, Max: 54.9, Diff: 28.7, Sum: 899.6]
[Update RS (ms): Min: 0.0, Avg: 9.2, Max: 9.8, Diff: 9.8, Sum: 304.3]
[Processed Buffers: Min: 0, Avg: 19.9, Max: 35, Diff: 35, Sum: 657]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 0.5, Avg: 11.2, Max: 13.8, Diff: 13.3, Sum: 369.9]
[Termination (ms): Min: 0.0, Avg: 7.8, Max: 8.4, Diff: 8.4, Sum: 256.8]
[Termination Attempts: Min: 1, Avg: 1.5, Max: 4, Diff: 3, Sum: 51]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.5]
[GC Worker Total (ms): Min: 55.3, Avg: 55.5, Max: 55.7, Diff: 0.5, Sum: 1831.3]
[GC Worker End (ms): Min: 4753827996.0, Avg: 4753827996.0, Max: 4753827996.1, Diff: 0.1]
[Code Root Fixup: 0.4 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.6 ms]
[Other: 504.4 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 501.3 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.5 ms]
[Humongous Register: 0.2 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.2 ms]
[Eden: 320.0M(3264.0M)->0.0B(3392.0M) Survivors: 416.0M->288.0M Heap: 9068.7M(72.0G)->8759.2M(72.0G)]
[Times: user=1.70 sys=0.39, real=0.57 secs]

从上面我们可以看到 Ref Proc: 501.3 ms,总共young gc耗时0.57s,而Ref Proc耗时就达到了0.5s。我们需要看下Ref Proc耗时主要在哪个地方。
我们通过配置gc参数-XX:+PrintReferenceGC,可以看到详细的Reference GC时间,并且如下图Case,JNI Weak Reference是主要瓶颈点。

Read More