一、为什么用ElasticSearch?
1、支持多种数据类型。它可以处理非结构化、数值和地理信息等多种类型的数据;
2、简单的RESTful API。ES提供了一个简单易用的RESTful API,使得它可以从任何编程语言中调用,降低了学习的曲线。
3、近实时搜索。ES每隔1秒将数据存储至系统缓存中,使用倒排索引提高检索效率,使得搜索数据变得快速且高效。
4、支持相关性搜索。它可以根据条件对搜索结果进行打分,提供了基于文档的全文检索能力。
5、天然分布式存储。ES是分布式的,使用分片支持处理PB级的数据量,易于扩展,可部署在数百台服务器的集群中。
6、降低全文检索的学习曲线。它可以被任何编程语言调用,使得开发变得更加容易。
7、高可用性。由于其分布式特性,ES可以通过添加更多节点来分担负载,增加可靠性,无需对应用进行任何改动。
二、ES基本概念名词
Cluster
代表一个集群,集群中有多个节点,其中有一个为主节点,这个主节点是可以通过选举产生的,主从节点是对于集群内部来说的。ES的一个概念就是去中心化,字面上理解就是无中心节点,这是对于集群外部来说的,因为从外部来看es集群,在逻辑上是个整体,你与任何一个节点的通信和与整个es集群通信是等价的。
Shards
代表索引分片,es可以把一个完整的索引分成多个分片,这样的好处是可以把一个大的索引拆分成多个,分布到不同的节点上。构成分布式搜索。分片的数量只能在索引创建前指定,并且索引创建后不能更改
replicas
代表索引副本,es可以设置多个索引的副本。
副本的作用:
一是提高系统的容错性,当某个节点某个分片损坏或丢失时可以从副本中恢复。
二是提高es的查询效率,es会自动对搜索请求进行负载均衡。
Recovery
代表数据恢复或叫数据重新分布,es在有节点加入或退出时会根据机器的负载对索引分片进行重新分配;挂掉的节点重新启动时也会进行数据恢复。
三、ES基础架构
角色
五大角色:
Master Node
主节点,该节点不和应用创建连接,每个节点都保存了集群状态,master节点控制整个集群的元数据。只有master节点可以修改节点状态信息及元数据的处理,比如索引的新增、删除、分片路由分配、所有索引和相关Mapping、Setting配置等等。
Master eligible nodes
合格主节点。合格节点,每个节点部署后不修改配置信息,默认就是一个 eligible 节点。有资格成为Master节点但暂时并不是Master的节点被称为 eligible 节点,该节点可以参加选主流程,成为Mastere节点。该节点只是与集群保持心跳,判断Master是否存活,如果Master故障则参加新一轮的Master选举。
Data Node
数据节点,用于简历文档索引,接受应用创建连接,接受索引请求,接受用户的搜索请求。是真实春初数据的节点,正常情况下节点数量越多,集群的性能就越强大
Coordinating Node
协调节点(/路由节点/client节点) 协调节点,该节点专用与接收应用的查询连接、接受搜索请求,但其本身不负责存储数据。
协调节点接受客户端搜索请求后将请求转发到与查询条件相关的多个data节点的分片上,然后多个data节点的分片执行查询语句或者查询结果再返回给协调节点,协调节点把各个data节点的返回结果进行整合、排序等一系列操作后再将最终结果返回给用户请求。
搜索请求在两个阶段中执行(query 和 fetch),这两个阶段由接收客户端请求的节点 - 协调节点协调。在请求query 阶段,协调节点将请求转发到保存数据的数据节点。 每个数据节点在本地执行请求并将其结果返回给协调节点。在收集fetch阶段,协调节点将每个数据节点的结果汇集为单个全局结果集。
Ingest Node
ingest 节点可以看作是数据前置处理转换的节点,支持 pipeline管道设置,可以使用 ingest 对数据进行过滤、转换等操作,类似于 logstash 中 filter 的作用,功能相当强大。
Ingest节点处理时机:在数据被索引之前,通过预定义好的处理管道对数据进行预处理。默认情况下,所有节点都启用Ingest,因此任何节点都可以处理Ingest任务。
ingest的详细可以参考elasticsearch高可用 原理 (图解+秒懂+史上最全)-CSDN博客
存储结构
Elasticsearch -> Index-> Types -> Documents -> Fields
1、ES的index代表Mysql中的数据库
2、ES的types代表Mysql中的Tables,新版本中弱化types概念
3、ES的Document代表Mysql中的行
4、ES的Fields代表Mysql中的列
5、ES的mapping代表Mysql中的表结构,
由于Elasticsearch底层使用了lucene的原因,不支持对mapping的修改,可使用索引重建的方式。而且不能更改类型,为什么不能修改一个字段的type?原因是一个字段的类型修改以后,那么该字段的所有数据都需要重新索引。Elasticsearch底层使用的是lucene库,字段类型修改以后索引和搜索要涉及分词方式等操作,不允许修改类型在我看来是符合lucene机制的。
存储就是集群,分片,副本。如下图所示
Lucene
Lucene是使用Java语言开发的开源的,高性能的查询库。Apache Solr,Apache Nutch,OpenSearch和Elasticsearch都是在Lucene的基础上创建的。Lucene已经有超过20年的历史,是Apache基金会管理的成熟项目。
Lucene的核心是倒排索引(inverted search index),这是Lucene有快速查询能力的核心。倒排索引提供了关键词与包含关键词的文档的对应关系。在查询的过程中,从排序的列表中快速找到关键词并且找到关键词对应的文档列表。Lucene支持的信息类型包含数字、字符串以及文本类型。Lucene包含丰富的搜索接口,支持自然语言搜索,通配符搜索,模糊搜索和邻近搜索。
倒排索引
所谓倒排索引,就是相对于正排索引,正排索引典型例子就是mysql的B+树主键索引,一个索引字段对应一列数据,key是主键ID,value是内容。而倒排索引,key是内容,value是关联的主键列表。如下图所示。
倒排索引有什么优点呢?
1、快速查找包含特定单词的文档,这使得它特别适合用于搜索引擎和文本搜索应用。
2、倒排索引可以快速查找文档集合中包含多个单词的文档,这在文本分类和关键词提取中也很有用、
缺点:
1、额外的存储空间来存储索引;
2、倒排索引在建立和更新时需要一定的时间;
Lucene中的倒排索引
lucene就是影响最为广泛的一款倒排索引及搜索工具,lucene把关键字按照字典顺序的存储在磁盘上,这样可以充分的发挥二分查找的优势,加快查找的速度。
存储内容
term index | term dictionary | Posting List |
小米 | 1,2...4 | |
华为 | 1,4,6 | |
手机 | 1,2,3...6 |
FST
、
新增一条数据的过程
1、分词
lucene使用字典文件记录所有的关键字,每个关键字不会重复。这个关键字是通过分词来完成的,如果是英文很简单就是按照英文单词来分割,但是中文就需要一个中文的分词器。
2、分析整理
下一步就是对分割出来的词进行分析和整理,比如时态统一,去除不需要的词比如“是”,“的”等等
3、建立对应关系
最后就是把关键词和对应的文件id关联上,一个关键词可以关联多个文件id。
对索引的压缩算法
针对海量的数据,会产生大量的索引文件,如果不对索引进行压缩,可以想见光索引文件就需要大量的硬件资源,同时对于索引的检索也会更加费时。此时压缩算法就非常必要了。
1、大量的对数字的压缩
索引中的Posting List不管主键Id是什么数据类型的,统一都是int类型,最大能够表示的正整数是2的31次方减1,当id很大的时候,比如存储两个连续的大整数,比如[1000000000,1000000001]这两个数字压缩之后可能会成为[100000000,1]类似的情况,后面的1表示和前一个数字的差值。因为 Posting List的id不一定是连续的但是肯定是有序的。所以使用差值列表来存储[Posting List]。
2、关键词压缩
比如有很多关键词都有相同的文字,比如中国,中国人,中国话,都有中国,那么就会把中国作为一个编码,变成中国,<1,人>,<1,话>
四、Elasticsearch数据备份与恢复
快照和还原机制
Snapshot: 快照,是Elasticsearch中用于备份数据的核心概念。Snapshot是一个时间点上的数据的完整拷贝,可以用于恢复数据或迁移到其他集群。
Restore: 还原,是从Snapshot中恢复数据的过程。Restore可以用于恢复单个索引或整个集群。
创建快照:curl -X PUT "http://localhost:9200/_snapshot/my_snapshot/snapshot_1?wait_for_completion=true" -H 'Content-Type: application/json' -d' { "indices": "my_index", "ignore_unavailable": true, "include_global_state": false }'
恢复快照:curl -X POST "http://localhost:9200/_snapshot/my_snapshot/snapshot_1/_restore" -H 'Content-Type: application/json' -d' { "indices": "my_index", "ignore_unavailable": true }' 或者使用elasticsearch-snapshot工具恢复快照: bin/elasticsearch-snapshot restore my_snapshot snapshot_1
分页查询与插入
可以使用scroll分页查询的方式,按照时间周期,从源ES中导入到目标ES中。代码如下:
// 设定滚动时间间隔
final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1L));
SearchRequest searchRequest = new SearchRequest(formIndices);
searchRequest.scroll(scroll);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// 设定每次返回多少条数据
int number = size + new Random().nextInt(size / 2);
searchSourceBuilder.size(number);
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse;
try {
CountRequest countRequest = new CountRequest(formIndices);
CountResponse countResponse = client.count(countRequest, getNewOptions());
total = countResponse.getCount();
searchResponse = client.search(searchRequest, getNewOptions());
} catch (IOException e) {
LOGGER.error("formIndices:" + formIndices, e);
return;
}
count = count + searchResponse.getHits().getHits().length;
String scrollId = searchResponse.getScrollId();
SearchHit[] searchHits = searchResponse.getHits().getHits();
LOGGER.info("-----首页-----" + System.currentTimeMillis());
request = new BulkRequest();
for (SearchHit documentFields : searchHits) {
Map<String, Object> sourceAsMap = documentFields.getSourceAsMap();
String timestamp = (String) sourceAsMap.get("@timestamp");
// 过滤path
String path = (String) sourceAsMap.get("path");
if(checkDiscardPath(path)){
continue;
}
if (!StringUtils.isEmpty(timestamp)) {
try {
sourceAsMap.put("@timestamp", toIndices + timestamp.substring(10));
} catch (Exception e) {
e.printStackTrace();
}
}
Map<String, Object> httpMap = (Map<String, Object>) sourceAsMap.get("http");
if (httpMap != null && httpMap.size() > 0) {
Map<String, Object> requestMap = (Map<String, Object>) httpMap.get("request");
if (requestMap != null && requestMap.size() > 0) {
requestMap.remove("body");
requestMap.remove("params");
httpMap.put("request", requestMap);
}
Map<String, Object> responseMap = (Map<String, Object>) httpMap.get("response");
if (responseMap != null && responseMap.size() > 0) {
responseMap.remove("body");
httpMap.put("response", responseMap);
}
sourceAsMap.put("http", httpMap);
}
indexRequest = new IndexRequest("ai-cloud-gateway-prod-packet-" + toIndices)
.source(sourceAsMap);
request.add(indexRequest);
}
// 写
BulkResponse bulk = clientNew.bulk(request, getNewOptions());
LOGGER.info(bulk.status().getStatus() + ":" + System.currentTimeMillis());
SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId);
// 遍历搜索命中的数据,直到没有数据
while (searchHits.length > 0) {
try {
scrollRequest.scroll(scroll);
try {
searchResponse = client.scroll(scrollRequest, getNewOptions());
} catch (IOException e) {
e.printStackTrace();
errorTimes++;
continue;
}
scrollId = searchResponse.getScrollId();
searchHits = searchResponse.getHits().getHits();
if (searchHits.length == 0) {
LOGGER.info(" searchHits is null ----- end");
break;
} else {
LOGGER.info(" searchHits is not null ----- " + searchHits.length);
}
count = count + searchHits.length;
LOGGER.info("-----下一页-----");
request = new BulkRequest();
LOGGER.info("-----过滤前----- {}",searchHits.length);
SearchHit[] finalSearchHits = checkDiscardHits(searchHits);
LOGGER.info("-----过滤后----- {}",finalSearchHits.length);
IndexRequest[] requests = new IndexRequest[finalSearchHits.length];
CountDownLatch countDownLatch = new CountDownLatch(finalSearchHits.length);
try {
for (int i = 0; i < finalSearchHits.length; i++) {
int finalI = i;
// 清除 request response 中的body 减少数据量
executorServiceTwo.execute(() -> {
try {
Map<String, Object> sourceAsMap = finalSearchHits[finalI].getSourceAsMap();
String timestamp = (String) sourceAsMap.get("@timestamp");
sourceAsMap.put("@timestamp", toIndices + timestamp.substring(10));
Map<String, Object> httpMap = (Map<String, Object>) sourceAsMap.get("http");
if (httpMap != null && httpMap.size() > 0) {
Map<String, Object> requestMap = (Map<String, Object>) httpMap.get("request");
if (requestMap != null && requestMap.size() > 0) {
requestMap.remove("body");
requestMap.remove("params");
httpMap.put("request", requestMap);
}
Map<String, Object> responseMap = (Map<String, Object>) httpMap.get("response");
if (responseMap != null && responseMap.size() > 0) {
responseMap.remove("body");
httpMap.put("response", responseMap);
}
sourceAsMap.put("http", httpMap);
}
requests[finalI] = new IndexRequest("ai-cloud-gateway-prod-packet-" + toIndices)
.source(sourceAsMap);
countDownLatch.countDown();
} catch (Exception e) {
LOGGER.error("", e);
}
});
}
} catch (Exception e) {
LOGGER.error("", e);
Map<String, Object> sourceAsMap;
for (SearchHit documentFields : searchHits) {
sourceAsMap = documentFields.getSourceAsMap();
// 过滤path
String path = (String) sourceAsMap.get("path");
if(checkDiscardPath(path)){
continue;
}
String timestamp = (String) sourceAsMap.get("@timestamp");
if (!StringUtils.isEmpty(timestamp)) {
try {
sourceAsMap.put("@timestamp", toIndices + timestamp.substring(10));
} catch (Exception ee) {
LOGGER.error("", ee);
}
}
Map<String, Object> httpMap = (Map<String, Object>) sourceAsMap.get("http");
if (httpMap != null && httpMap.size() > 0) {
Map<String, Object> requestMap = (Map<String, Object>) httpMap.get("request");
if (requestMap != null && requestMap.size() > 0) {
requestMap.remove("body");
requestMap.remove("params");
httpMap.put("request", requestMap);
}
Map<String, Object> responseMap = (Map<String, Object>) httpMap.get("response");
if (responseMap != null && responseMap.size() > 0) {
responseMap.remove("body");
httpMap.put("response", responseMap);
}
sourceAsMap.put("http", httpMap);
}
indexRequest = new IndexRequest("ai-cloud-gateway-prod-packet-" + toIndices)
.source(sourceAsMap);
request.add(indexRequest);
countDownLatch.countDown();
}
}
try {
countDownLatch.await(60, TimeUnit.SECONDS);
} catch (InterruptedException e) {
LOGGER.warn("InterruptedException", e);
}
LOGGER.info("request {}",requests.length);
request.add(requests);
// 写
LOGGER.info("begin writing to " + toIndices + ",from" + formIndices);
bulk = clientNew.bulk(request, getNewOptions());
LOGGER.info(bulk.status().getStatus() + ":" + System.currentTimeMillis());
if (count >= total - 5000) {
break;
}
Thread.sleep(100);
} catch (Exception e) {
......
}
}
完整代码:GitHub - EricLoveMia/elasticsearch-import
五、性能调优
1、不要返回数据量非常大的结果集
2、避免出现大文档,即单条索引记录的体积不要过大。
3、有条件的话尽量使用大的内存,SSD硬盘,分配给ES的内存为最大内存的50%;
4、尽量批请求bulk;
5、大量写入的情况下,增加索引刷新时间大小index.refresh_interval
,默认1s刷新一次,设置为-1表示关闭索引刷新;
6、初始加载数据时禁用副本,将index.number_of_replicas
设置为0;
7、尽量避免使用深度分页,实在不能避免可以采用Scroll 遍历查询或Search After 查询;
8、Data too large 的报错。下图可以看到有两条界限:驱逐线 和 断路器。当缓存数据到达驱逐线时,会自动驱逐掉部分数据,把缓存保持在安全的范围内。当用户准备执行某个查询操作时,断路器就起作用了,缓存数据+当前查询需要缓存的数据量到达断路器限制时,会返回Data too large错误,阻止用户进行这个查询操作。
此时就要根据情况来修改一些参数的值。
indices.fielddata.cache.size :配置fieldData的Cache大小,可以配百分比也可以配一个准确的数值。cache到达约定的内存大小时会自动清理,驱逐一部分FieldData数据以便容纳新数据。默认值为unbounded无限。
indices.fielddata.cache.expire:用于约定多久没有访问到的数据会被驱逐,默认值为-1,即无限。expire配置不推荐使用,按时间驱逐数据会大量消耗性能。而且这个设置在不久之后的版本中将会废弃。
indices.breaker.fielddata.limit:这个 fielddata 断路器限制fielddata的大小,默认情况下为堆大小的60%。
indices.breaker.request.limit:这个 request 断路器估算完成查询的其他部分要求的结构的大小, 默认情况下限制它们到堆大小的40%。
indices.breaker.total.limit:这个 total 断路器封装了 request 和 fielddata 断路器去确保默认情况下这2个部分使用的总内存不超过堆大小的70%。
参考:
elasticsearch高可用 原理 (图解+秒懂+史上最全)-CSDN博客
elasticsearch常识:存储结构、优化_es的存储结构-CSDN博客
ElasticSearch:从[FIELDDATA]Data too large错误看FieldData配置_indices.fielddata.cache.size-CSDN博客ES 性能调优,这可能是全网最详细的 Elasticsearch 性能调优指南_es性能优化-CSDN博客