一、什么是ElasticSearch
ElasticSearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口。Elasticsearch是用Java语言开发的,并作为Apache许可条款下的开放源码发布,是一种流行的企业级搜索引擎。
二、ElasticSearch的核心--------倒排索引
倒排索引(Inverted Index)也叫反向索引,有反向索引必有正向索引。通俗地来讲,正向索引是通过key找value,反向索引则是通过value找key。倒排索引是实现“单词-文档矩阵”的一种具体存储形式,通过倒排索引,可以根据单词快速获取包含这个单词的文档列表。倒排索引主要由两个部分组成:“单词词典”和“倒排文件”。可以参考这篇博客,讲的非常通俗易懂。
三、ElasticSearch常用的API
private static String host="192.168.1.108"; // 服务器地址
private static int port=9300; // 端口
public static void main(String[] args) throws Exception{
TransportClient client = new PreBuiltTransportClient(Settings.EMPTY)
.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName(Test.host), Test.port));
System.out.println(client);
client.close();
}
public void testIndex()throws Exception{
IndexResponse response =client.prepareIndex("twitter", "tweet", "1")
.setSource(XContentFactory.jsonBuilder()
.startObject()
.field("user", "kimchy")
.field("postDate", new Date())
.field("message", "trying out Elasticsearch")
.endObject()
)
.get();
System.out.println("索引名称:"+response.getIndex());
System.out.println("类型:"+response.getType());
System.out.println("文档ID:"+response.getId()); // 第一次使用是1
System.out.println("当前实例状态:"+response.status());
}
public void testGet(){
GetResponse getResponse=client.prepareGet("twitter", "tweet", "1").get();
System.out.println(getResponse.getSourceAsString());
}
public void testUpdate(){
JsonObject jsonObject=new JsonObject();
jsonObject.addProperty("user", "xiaoxsen");
jsonObject.addProperty("postDate", "2019-7-11");
jsonObject.addProperty("message", "学习Elasticsearch");
UpdateResponse response = client.prepareUpdate("twitter", "tweet", "1").setDoc(jsonObject.toString(),XContentType.JSON).get();
System.out.println("索引名称:"+response.getIndex());
System.out.println("类型:"+response.getType());
System.out.println("文档ID:"+response.getId()); // 第一次使用是1
System.out.println("当前实例状态:"+response.status());
}
public void testDelete(){
DeleteResponse response=client.prepareDelete("twitter", "tweet", "1").get();
System.out.println("索引名称:"+response.getIndex());
System.out.println("类型:"+response.getType());
System.out.println("文档ID:"+response.getId()); // 第一次使用是1
System.out.println("当前实例状态:"+response.status());
}
public void searchOrderBy()throws Exception{
SearchRequestBuilder srb=client.prepareSearch("film").setTypes("dongzuo");
SearchResponse sr=srb.setQuery(QueryBuilders.matchAllQuery())
.addSort("publishDate", SortOrder.DESC)
.execute()
.actionGet(); // 分页排序所有
SearchHits hits=sr.getHits();
for(SearchHit hit:hits){
System.out.println(hit.getSourceAsString());
}
}
public void searchPaging()throws Exception{
SearchRequestBuilder srb=client.prepareSearch("film").setTypes("dongzuo");
SearchResponse sr=srb.setQuery(QueryBuilders.matchAllQuery()).setFrom(1).setSize(2).execute().actionGet(); // 查询所有
SearchHits hits=sr.getHits();
for(SearchHit hit:hits){
System.out.println(hit.getSourceAsString());
}
}
public void searchInclude()throws Exception{
SearchRequestBuilder srb=client.prepareSearch("film").setTypes("dongzuo");
SearchResponse sr=srb.setQuery(QueryBuilders.matchAllQuery())
.setFetchSource(new String[]{"title","price"}, null)
.execute()
.actionGet(); // 分页排序所有
SearchHits hits=sr.getHits();
for(SearchHit hit:hits){
System.out.println(hit.getSourceAsString());
}
}
public void searchHighlight()throws Exception{
SearchRequestBuilder srb=client.prepareSearch("film").setTypes("dongzuo");
HighlightBuilder highlightBuilder=new HighlightBuilder();
highlightBuilder.preTags("");
highlightBuilder.postTags("
");
highlightBuilder.field("title");
SearchResponse sr=srb.setQuery(QueryBuilders.matchQuery("title", "战"))
.highlighter(highlightBuilder)
.setFetchSource(new String[]{"title","price"}, null)
.execute()
.actionGet(); // 分页排序所有
SearchHits hits=sr.getHits();
for(SearchHit hit:hits){
System.out.println(hit.getSourceAsString());
System.out.println(hit.getHighlightFields());
}
}
public void searchMutil3()throws Exception{
SearchRequestBuilder srb=client.prepareSearch("film").setTypes("dongzuo");
QueryBuilder queryBuilder=QueryBuilders.matchPhraseQuery("title", "战");
QueryBuilder queryBuilder2=QueryBuilders.matchPhraseQuery("content", "星球");
QueryBuilder queryBuilder3=QueryBuilders.rangeQuery("publishDate").gt("2018-01-01");
SearchResponse sr=srb.setQuery(QueryBuilders.boolQuery()
.must(queryBuilder)
.should(queryBuilder2)
.should(queryBuilder3))
.execute()
.actionGet();
SearchHits hits=sr.getHits();
for(SearchHit hit:hits){
System.out.println(hit.getScore()+":"+hit.getSourceAsString());
}
}
四、ElasticSearch高可用架构
索引可以拆分成多个shard,每个shard存储部分数据。每个shard的数据实际是有多个备份,就是说每个shard都有一个primary shard,负责写入数据,但是还有几个replica shard。primary shard写入数据之后,会将数据同步到其他几个replica shard上去。如果某个机器宕机了,没关系啊,还有别的数据副本在别的机器上。把副本shard切换成primary shard继续提供服务,这样就实现了高可用。
es集群多个节点,会自动选举一个节点为master节点,这个master节点其实就是干一些管理的工作的,比如维护索引元数据,负责切换primary shard和replica shard身份,如果master节点宕机了,那么会重新选举一个节点为master节点。如果是非master节点宕机了,那么会由master节点,让那个宕机节点上的primary shard的身份转移到其他机器上的replica shard,让replica shard变成primary shard继续提供服务。宕机节点重启了之后,master节点会控制将缺失的replica shard分配过去,同步后续修改的数据,让集群恢复正常。
ES写数据的过程
1)客户端选择一个node发送请求过去,这个node就是coordinating node(协调节点)
2)coordinating node,对document进行路由,将请求转发给对应的node(有primary shard)
3)实际的node上的primary shard处理请求,然后将数据同步到replica node
4)coordinating node,如果发现primary node和所有replica node都搞定之后,就返回响应结果给客户端
ES读数据的过程
写入某个document时,该document会自动分配一个全局唯一的id,doc id,也可以手动指定doc id,比如用订单id,用户id,然后根据doc id进行hash路由到对应的primary shard上面去完成写入操作。。查询数据时可以通过doc id来查询,coordinate node会根据doc id进行hash,判断出来当时把doc id分配到了哪个shard上面去,最后从那个shard去查询
1)客户端发送请求到任意一个node,成为coordinate node(ES节点,也叫协调节点)
2)coordinate node对document进行路由,将请求转发到对应的node,此时会使用round-robin随机轮询算法,在primary shard以及其所有replica中随机选择一个,让读请求负载均衡
3)接收请求的node返回document给coordinate node
4)coordinate node返回document给客户端
ES全文检索过程
1)客户端发送请求到一个coordinate node(ES节点,也叫协调节点)
2)协调节点将搜索请求转发到所有的shard对应的primary shard或replica shard也可以
3)query phase:每个shard将自己的搜索结果(其实就是一些doc id),返回给协调节点,由协调节点进行数据的合并、排序、分页等操作,产出最终结果
4)fetch phase:接着由协调节点,根据doc id去各个节点上拉取实际的document数据,最终返回给客户端
写数据底层原理:
1)先写入buffer,在buffer里的时候数据是搜索不到的;同时将数据写入translog日志文件
2)如果buffer快满了,或者到一定时间,就会将buffer数据refresh到一个新的segment file中,但是此时数据不是直接进入segment file的磁盘文件的,而是先进入os cache的。这个过程就是refresh。
每隔1秒钟,es将buffer中的数据写入一个新的segment file,每秒钟会产生一个新的磁盘文件,segment file,这个segment file中就存储最近1秒内buffer中写入的数据
但是如果buffer里面此时没有数据,那当然不会执行refresh操作咯,每秒创建换一个空的segment file,如果buffer里面有数据,默认1秒钟执行一次refresh操作,刷入一个新的segment file中
操作系统里面,磁盘文件其实都有一个东西,叫做os cache,操作系统缓存,就是说数据写入磁盘文件之前,会先进入os cache,先进入操作系统级别的一个内存缓存中去
只要buffer中的数据被refresh操作,刷入os cache中,就代表这个数据就可以被搜索到了
为什么叫es是准实时的?NRT,near real-time,准实时。默认是每隔1秒refresh一次的,所以es是准实时的,因为写入的数据1秒之后才能被看到。
可以通过es的restful api或者java api,手动执行一次refresh操作,就是手动将buffer中的数据刷入os cache中,让数据立马就可以被搜索到。
只要数据被输入os cache中,buffer就会被清空了,因为不需要保留buffer了,数据在translog里面已经持久化到磁盘去一份了
3)只要数据进入os cache,此时就可以让这个segment file的数据对外提供搜索了
4)重复1~3步骤,新的数据不断进入buffer和translog,不断将buffer数据写入一个又一个新的segment file中去,每次refresh完buffer清空,translog保留。随着这个过程推进,translog会变得越来越大。当translog达到一定长度的时候,就会触发commit操作。
buffer中的数据,倒是好,每隔1秒就被刷到os cache中去,然后这个buffer就被清空了。所以说这个buffer的数据始终是可以保持住不会填满es进程的内存的。
每次一条数据写入buffer,同时会写入一条日志到translog日志文件中去,所以这个translog日志文件是不断变大的,当translog日志文件大到一定程度的时候,就会执行commit操作。
5)commit操作发生第一步,就是将buffer中现有数据refresh到os cache中去,清空buffer
6)将一个commit point写入磁盘文件,里面标识着这个commit point对应的所有segment file
7)强行将os cache中目前所有的数据都fsync到磁盘文件中去
translog日志文件的作用是什么?就是在你执行commit操作之前,数据要么是停留在buffer中,要么是停留在os cache中,无论是buffer还是os cache都是内存,一旦这台机器死了,内存中的数据就全丢了。
所以需要将数据对应的操作写入一个专门的日志文件,translog日志文件中,一旦此时机器宕机,再次重启的时候,es会自动读取translog日志文件中的数据,恢复到内存buffer和os cache中去。
commit操作:1、写commit point;2、将os cache数据fsync强刷到磁盘上去;3、清空translog日志文件
8)将现有的translog清空,然后再次重启启用一个translog,此时commit操作完成。默认每隔30分钟会自动执行一次commit,但是如果translog过大,也会触发commit。整个commit的过程,叫做flush操作。我们可以手动执行flush操作,就是将所有os cache数据刷到磁盘文件中去。
不叫做commit操作,flush操作。es中的flush操作,就对应着commit的全过程。我们也可以通过es api,手动执行flush操作,手动将os cache中的数据fsync强刷到磁盘上去,记录一个commit point,清空translog日志文件。
9)translog其实也是先写入os cache的,默认每隔5秒刷一次到磁盘中去,所以默认情况下,可能有5秒的数据会仅仅停留在buffer或者translog文件的os cache中,如果此时机器挂了,会丢失5秒钟的数据。但是这样性能比较好,最多丢5秒的数据。也可以将translog设置成每次写操作必须是直接fsync到磁盘,但是性能会差很多。
es丢数据的问题,数据写入1秒后可以搜索到;可能会丢失数据的,数据有5秒的数据,停留在buffer、translog os cache、segment file os cache中,有5秒的数据不在磁盘上,translog默认是5s会写入一次磁盘,此时如果宕机,translog数据还没有写入磁盘,buffer,和os cache 的数据会丢失,这样就导致了这5秒的数据丢失。
如果希望一定不能丢失数据的话,可以设置个参数,每次写入一条数据,都是写入buffer,同时写入磁盘上的translog,但是这会导致写性能、写入吞吐量会下降一个数量级。本来一秒钟可以写2000条,现在你一秒钟只能写200条,都有可能。
10)如果是删除操作,commit的时候会生成一个.del文件,里面将某个doc标识为deleted状态,那么搜索的时候根据.del文件就知道这个doc被删除了
11)如果是更新操作,就是将原来的doc标识为deleted状态,然后新写入一条数据
12)buffer每次refresh一次,就会产生一个segment file,所以默认情况下是1秒钟一个segment file,segment file会越来越多,此时会定期执行merge
13)每次merge的时候,会将多个segment file合并成一个,同时这里会将标识为deleted的doc给物理删除掉,然后将新的segment file写入磁盘,这里会写一个commit point,标识所有新的segment file,然后打开segment file供搜索使用,同时删除旧的segment file。
es里的写流程,有4个底层的核心概念,refresh、flush、translog、merge
当segment file多到一定程度的时候,es就会自动触发merge操作,将多个segment file给merge成一个segment file。
六、海量数据时ElasticSearch性能优化
(1)性能优化的杀手锏——filesystem cache
ES的搜索引擎严重依赖于底层的filesystem cache,如果给filesystem cache更多的内存,尽量让内存可以容纳所有的indx segment file索引数据文件,那么搜索的时候就基本都是走内存的,性能会非常高。为了让内存可以容纳更多的索引数据,如果单条document的数据特别大(搜索的字段就那几个,剩下的全部不需要搜索),需要进行垂直拆分,将需要检索的字段存在ES中,剩下的字段存到Hbase中去,hbase的特点是适用于海量数据的在线存储,就是对hbase可以写入海量数据。查询时可以先去ES中搜索出来一些id,通过id去hbase里面查询明细的数据。
(2)数据预热
如果按照方案一处理了,ES集群中每个机器写入的数据量还是超过了filesystem cache一倍,这时候就可以把常查询的数据提前加载到os cache中,用户来查询时数据时直接走内存,就很快可以查到数据。
举个例子,就比如说,微博,你可以把一些大v,平时看的人很多的数据给可以后台搞个系统,每隔一会儿,自动去后台系统去搜索一下热数据,刷到filesystem cache里去,后面用户实际上来看这个热数据的时候,直接从内存里搜索了,这样就很快了。
电商系统设计时,会将平时查看最多的一些商品,比如说Mac,iPhone等经常查询的热数据提前后台搞个程序,每隔1分钟自己主动访问一次,刷到filesystem cache里去。
对于那些比较热的,经常会有人访问的数据,最好做一个专门的缓存预热子系统,就是对热数据,每隔一段时间,提前访问一下,让数据进入filesystem cache里面去。这样期待下次别人访问的时候,一定性能会好一些。
(3)冷热分离
第一个方案中将大量不搜索的字段,拆分到别的存储(mysql,hbase)中去,这个就类似于mysql分库分表的垂直拆分。
es可以做类似于mysql的水平拆分,就是将大量的访问很少,频率很低的数据,单独写一个索引,然后将访问很频繁的热数据单独写一个索引,将冷数据写入一个索引中,然后热数据写入另外一个索引中,这样可以确保热数据在被预热之后,尽量都让他们留在filesystem os cache里,别让冷数据给冲刷掉。
假设有6台机器,2个索引,一个放冷数据,一个放热数据,每个索引3个shard,3台机器放热数据index;另外3台机器放冷数据index,然后这样的话,大量的时候是在访问热数据index,热数据可能就占总数据量的10%,此时数据量很少,几乎全都保留在filesystem cache里面了,就可以确保热数据的访问性能是很高的。但是对于冷数据而言,是在别的index里的,跟热数据index都不再相同的机器上,大家互相之间都没什么联系了。如果有人访问冷数据,可能大量数据是在磁盘上的,此时性能差点,就10%的人去访问冷数据;90%的人在访问热数据。
(4)document模型设计
document模型设计是非常重要的,很多操作,不要在搜索的时候才想去执行各种复杂的乱七八糟的操作。es能支持的操作就是那么多,不要考虑用es做一些它不好操作的事情。如果真的有那种操作,尽量在document模型设计的时候,写入的时候就完成。另外对于一些太复杂的操作,比如join,nested,parent-child搜索都要尽量避免,性能都很差的。
两个思路,在搜索/查询的时候,要执行一些业务强相关的特别复杂的操作:
1)在写入数据的时候,就设计好模型,加几个字段,把处理好的数据写入加的字段里面
2)自己用java程序封装,es能做的,用es来做,搜索出来的数据,在java程序里面去做,比如说我们,基于es,用java封装一些特别复杂的操作
(5)分页性能优化
es的分页是较坑的,为啥呢?举个例子吧,假如你每页是10条数据,你现在要查询第100页,实际上是会把每个shard上存储的前1000条数据都查到一个协调节点上,如果你有个5个shard,那么就有5000条数据,接着协调节点对这5000条数据进行一些合并、处理,再获取到最终第100页的10条数据。
分布式的,要查第100页的10条数据,必须得从每个shard都查1000条数据过来,然后根据需求进行排序、筛选等等操作,最后再次分页,拿到里面第100页的数据。
翻页的时候,翻的越深,每个shard返回的数据就越多,而且协调节点处理的时间越长。非常坑爹。所以用es做分页的时候,会发现越翻到后面,就越是慢。
针对这种情况策略就是:
1)不允许深度分页/默认深度分页性能很惨
2)类似于微博a这种下拉刷新的app,可以用scroll api。
scroll会一次性生成所有数据的一个快照,然后每次翻页就是通过游标移动,获取下一页下一页这样子,性能会比上面说的那种分页性能也高很多很多。
但是需要注意的是,这个适合于那种类似微博下拉翻页的,不能随意跳到任何一页的场景。同时这个scroll是要保留一段时间内的数据快照的,需要确保用户不会持续不断翻页翻几个小时。