ElasticSearch从入门到放弃

一、什么是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());
    }
}
  • 数据列(类型mysql中select列属性)过滤
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("<h2>");
    highlightBuilder.postTags("</h2>");
    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());
    }
}
  • 组合多条件查询实现
     主要参数:
       must 文档 必须 匹配这些条件才能被包含进来。
       must_not 文档 必须不 匹配这些条件才能被包含进来。
       should 如果满足这些语句中的任意语句,将增加 _score ,否则,无任何
       影响。它们主要用于修正每个文档的相关性得分。
       filter 必须 匹配,但它以不评分、过滤模式来进行。这些语句对评分没有
       贡献,只是根据过滤标准来排除或包含文档。
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分配过去,同步后续修改的数据,让集群恢复正常。

五、ElasticSearch数据读写原理
读写底层原理
                 读写底层原理图

  • 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是要保留一段时间内的数据快照的,需要确保用户不会持续不断翻页翻几个小时。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值