谈谈ES读写

ES是谁?

一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎

Ps:Lucene是一套用于全文检索和搜寻的开源程式库,由Apache软件基金会支持和提供。Lucene提供了一个简单却强大的应用程式接口,能够做全文索引和搜寻。

ES是干啥的?

ES主要用于做以下三个功能的组合

  • 全文检索
  • 结构化搜索
  • 分析

小扩展一下Elasticsearch是与Logstash的数据收集和日志解析引擎以及Kibana的分析和可视化平台一起开发的,合称ELK。

Elasticsearch 的实现原理主要分为以下几个步骤,首先用户将数据提交到Elasticsearch 数据库中,再通过分词控制器去将对应的语句分词,将其权重和分词结果一并存入数据,当用户搜索数据时候,再根据权重将结果排名,打分,再将返回结果呈现给用户。

ES怎么进行读写?

ES整体构造

image.png

如何写or更新数据

image
image

Ps: _source是元数据,具体可看 https://www.elastic.co/guide/cn/elasticsearch/guide/current/root-object.html

简要流程如下:

image

详细流程如下:

image

关键点

  1. Auto Create Index

判断当前 Index 是否存在,如果不存在,则需要自动创建 Index,需要和 Master 交互。可通过配置关闭自动创建 Index 的功能。

// 无须创建index,直接执行后续操作

if (autoCreateIndices.isEmpty()) {

    executeBulk(……);

} else {

    for (String index : autoCreateIndices) {

        // 在此处创建新的index,监听所有待创建index创建完成后,执行后续操作

        createIndex(index, timeout, new ActionListener() {

            @Override

            public void onResponse(CreateIndexResponse result) {

                if (counter.decrementAndGet() == 0) {

                    executeBulk(……);

                }

            }

        }

    });

}
  1. Send Request To Primary

并行发送bulk请求给各primary。

// primary不存在或者未active,则重试等待

if (primary == null || primary.active() == false) {

    retryBecauseUnavailable(request.shardId(), "primary shard is not active");

    return true;

}



// primary在relocating,路由至目标primary

if (primaryShardReference.isRelocated()) {

    DiscoveryNode relocatingNode = clusterService.state().nodes().get(primary.relocatingNodeId());

    transportService.sendRequest(relocatingNode, ……);

}



for (Map.Entry> entry : requestsByShard.entrySet()) {

 shardBulkAction.execute(bulkShardRequest, new ActionListener<>() {

 @Override

        public void onResponse(BulkShardResponse bulkShardResponse) {

            // 等到所有的request均得到response,则结束。原子减操作

 if (counter.decrementAndGet() == 0) {

                finishHim();

            }

        }

    });

}
  1. Update Mapping

自动更新 Mapping 的功能会先挑选出 Mapping 中未包含的新 Field,然后判断是否运行自动更新 Mapping,如果允许,则更新 Mapping。更新集群状态的过程阻塞写操作。

Mapping update = operation.parsedDoc().dynamicMappingsUpdate();

if (update != null) {

    // 与Master交互,进行动态mapping

    mappingUpdatedAction.updateMappingOnMaster(……);

}



// 在timeout超时时间内,同步等待master创建

if (updateMappingRequest(index, type, mappingUpdate, timeout).get().isAcknowledged() == false) {

    throw new TimeoutException("Failed to acknowledge mapping update");

}
  1. 写一致性

ES1.5-版本中:支持主从异步复制的方式 + QUORUM

// 主从异步复制,主分片写完后,直接返回给Client Node

if (replicationType == ReplicationType.ASYNC) {

    postPrimaryOperation(internalRequest, response);

    // async replication, notify the listener

    listener.onResponse(response.response());

    // now, trick the counter so it won't decrease to 0 and notify the listeners

    replicaCounter = Integer.MIN_VALUE;

}



// 多数active或指定ALL或默认主分片active,即可写

final int sizeActive;

final int requiredNumber;

if (consistencyLevel == WriteConsistencyLevel.QUORUM) {

    requiredNumber = (shardRoutingTable.getSize() / 2) + 1;

} else if (consistencyLevel == WriteConsistencyLevel.ALL) {

    requiredNumber = shardRoutingTable.getSize();

} else {

    requiredNumber = 1;

}

ES2.0+版本中:不支持主从异步复制 + WaitForActiveShardNum

// 监听到所有活跃的分片都响应后,返回给Client Node

final AtomicInteger counter = new AtomicInteger(requestsByShard.size());

public void onResponse(BulkShardResponse bulkShardResponse) {

    // 等到所有的request均得到response,则结束。原子减操作

 if (counter.decrementAndGet() == 0) {

        finishHim();

    }

}



// 在timeout时间内等待活跃副本数达到WaitForActiveShardNum,才进行primary的写操作

final int activeShardCount = shardRoutingTable.activeShards().size();

// 主分片 和 所有副本分片均 active

if (this == ActiveShardCount.ALL) {

    return activeShardCount == shardRoutingTable.replicaShards().size() + 1;

// 默认主分片 active 即可

} else if (this == ActiveShardCount.DEFAULT) {

    return activeShardCount >= 1;

// 指定 active 分片数

} else {

    return activeShardCount >= value;

}



// 失败

if (activeShardCountFailure != null) {

    finishAsFailed(new UnavailableShardsException(……));

    return;

}



// 监听集群状态有变化后,重试

void retry(Exception failure) {

 observer.waitForNextChange(new ClusterStateObserver.Listener() {

        @Override

        public void onNewClusterState(ClusterState state) {

            run();

        }

    });

}
  1. Add Doc To Lucene

这一步开始的时候会给特定_uid 加锁,然后判断该_uid 对应的 Version 是否等于之前获取到的 Version,如果不相等,则说明刚才读取 Doc 后,该 Doc 发生了变化,出现了版本冲突,这时候会抛出一个 VersionConflict 的异常,该异常会在 Primary Node 最开始处捕获,重新执行以上步骤。如果 Version 相等,则继续执行。

Lucene 的 UpdateDocument 接口中就只是处理多个 Field,会遍历每个 Field 逐个处理,处理顺序是 invert index,store field,doc values,point dimension.

// 向Lucene中添加新的Document,并不一定刷到os cache,何时刷回由当前引擎的刷回策略决定

try (Releasable ignored = acquireLock(index.uid())) {

    indexResult = indexIntoLucene(index, plan);

}

indexWriter.addDocuments(docs);

// 将Document写入buffer,并获取Sequence Number,此时Search并不能查询到

seqNo = dwpt.updateDocument(doc, analyzer, delTerm); 

assert seqNo > perThread.lastSeqNo: "……";

perThread.lastSeqNo = seqNo;
  1. Send Requests To Replicas

这里会将刚才构造的新的 Bulk Request 并行发送给多个 Replica,然后等待 Replica 的返回,这里需要等待所有 Replica 返回后(可能有成功,也有可能失败),Primary Node 才会返回用户。如果某个 Replica 失败了,则 Primary 会给 Master 发送一个 Remove Shard 请求,要求 Master 将该 Replica Shard 从可用节点中移除。

这里,同时会将 SequenceID,PrimaryTerm,GlobalCheckPoint 等传递给 Replica。

发送给 Replica 的请求中,Action Name 等于原始 ActionName + [R],这里的 R 表示 Replica。通过这个 [R] 的不同,可以找到处理 Replica 请求的 Handler。

for (final ShardRouting shard : shards) {

    if (shard.currentNodeId().equals(localNodeId) == false) {

        performOnReplica(shard, replicaRequest);

    }



    // 该副本在relocating,则转发至目标shard

 if (shard.relocating() && shard.relocatingNodeId().equals(localNodeId) == false) {

        performOnReplica(shard.getTargetRelocatingShard(), replicaRequest);

    }

}

如何持久化?

image.png
  1. 发生数据写入时,需用同时写入到 内存buffer && translog os cache 才算成功
  2. 每1s,refresh 到 segment文件,buffer 会清空
  3. 每隔30m或者大于512M,flush 持久化到磁盘,且translog 清空
  4. 每隔5s,持久化到磁盘(唯一会掉是数据的环节)

Ps: 数据到了document os cache 即可被读取到

关键点

  1. Write Translog(in os cache)

写完内存buffer后,会以 keyvalue 的形式写 TransLog,Key 是_id,Value 是 Doc 内容。当查询的时候,如果请求是 GetDocByID,则可以直接根据_id 从 TransLog 中读取到,满足 NoSQL 场景下的实时性要去。

这一步的最后,会标记当前 SequenceID 已经成功执行,接着会更新当前 Shard 的 LocalCheckPoint。

// 写入Translog并返回在translog中的位置,但不刷回磁盘

Translog.Location location = translog.add(new Translog.Index(index, indexResult));
  1. Refresh / Flush

Refresh / Flush 的频率越高,可靠性越高,对写入性能影响越大。

refresh触发条件:达到最大文档数、最大内存上限,或 定时1s触发, 或 REST API 请求指定refresh等。

document os cache flush触发条件:API触发、定时30min触发等。

translog flush触发条件:API触发、达到大小、定时5s触发。

如何吐数据

image

详细流出如下:

image

ES查询执行流程按如下3个执行步骤介绍:

  • 监听请求及转发;
  • Query阶段(包含coordinator节点 + Data节点的操作)
  • Fetch阶段(包含coordinator节点 + Data节点的操作)

关键点

  1. Merge result

若使用From/Size比如一个排序分页查询,查前200条,ES是在每个分片中去找前200条,再去把各个分片数据在协调节点中进行聚合、排序取前200条。

这也是不建议 出现多页后的查询,比如我们要查从100000条起的前200条,那就是每个分片查100200条,然后在协调节点中进行排序取前100200,再切除前100000条,返回200条

可能成为坑的点

Mapping & Write
  • 对于新创建的mapping,大集群可以考虑提前创建,避免集中创建造成较长时间的写入阻塞;
  • 对于已创建的mapping,用户写入的数据结构与mapping保持一致,新增字段会触发动态mapping,涉及到集群状态变更,阻塞写入;也可禁用dynamic mapping;
  • 在reindex时,建议refresh_interval = -1(不允许ES后台线程将 新写入到 in-memory buffer里面的文档自动刷新生成 segments)number_of_replicas = 0 (避免写入时候,除了写主分片还要写从分片)
Shard
  • 分片在进行relocating时,阻塞写入操作;
  • 适当减少副本数:牺牲部分可靠性,提高写入速度。
  • 单个分片的数据量最好是20G~50G,可以根据预估数据的增长提前设着好shard数量
  • 单个分片的文档数量上限是2147483519(Integer.MAX_VALUE - 128)
Refresh
  • 适当增大refresh间隔:牺牲可见性的时间,减少CPU消耗;
  • 通过REST API可强制refresh,可使立即搜索可见,但消耗CPU资源;
Search
  • 大数据量的查询请求,不建议使用多个分片
  • 大数据量的查询请求,可以使用scroll 进行分批获取,但是在分批读取过程中修改的数据是不会被更新到获取结果中。所以不适用于实时请求(scroll 查询会取某个时间点的快照数据)。
  • 并发使用scroll查询时,需要注意并发的scroll数量,一般在不修改配置的情况下,最大值是500,超过500后要么会导致内存使用过高,要么直接报错;若真需要大于500时,需要调整配置项:search.max_open_scroll_context
  • 若使用from\size查询, 单个size不能大于10000
如果对ES怎么做分布式部署比较感兴趣:

等楼主空了写一篇出来

小故事

上帝介绍游戏

欢迎MYSQL、ES、HTTP服务、RPC服务、程序员小A,程序员小B、业务运营、业务客户、业务PM入场,各位要领好自己的身份牌,本局配置是三狼三神三民局,其中神为预言家、女巫、猎人。

天黑请闭眼

狼人请杀人,狼人请刀人……

滴答、滴答,时钟不停转

哗~一晚上过去了

竞选警长

竞选警长的有:小A 和 ES

小A:代码能力强,擅长分析各种问题,强神带队,昨夜有惊喜,强势拿警长……

ES:我来就是找存在感的,我是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,我老厉害了~退水退水~

小A自动当选警长

昨天情况

昨天晚上挂掉的是:业务客户

业务客户遗言:我是好人,我昨天晚上查了一下列表页面,等了好久还没出来,等着等着我就再也没醒过来!裸点三狼:运营、PM、小B,小A搞不好也是狼!

……

警长总结

小A:我先给大家报下查验信息,昨天晚上我查的是运营, 金水,剩下的发言时间让我来详细分析案发现场。

现场:列表搜索页面,一般是ES做的,而ES的这个索引吃了300G数据,但这个索引只有3个分片,这不让客户等到地老天荒?ES你知错了么!你就是那头刀了用户狼!之一!

警长归票ES,ES被全票出局

ES遗言

我不本来不想刀客户的,我提几个保护客户的点吧:

  1. +分片
  2. -数据量
  3. +索引
  4. +内存
  5. +机器

参考文档

https://github.com/elastic/elasticsearch

https://elasticsearch.cn/article/13533

https://zhuanlan.zhihu.com/p/341984620

你可能感兴趣的:(谈谈ES读写)