Hello,World

社区日报 第472期 (2018-12-08)

  1. 用于ES数据警报的实时守护进程。 http://t.cn/EyQmiuE

  2. 最小的ibana Docker镜像。 http://t.cn/EyQbwGk

  3. Lucene列式存储格式DocValues详解。 http://t.cn/EyQ6pkK
继续阅读 »
  1. 用于ES数据警报的实时守护进程。 http://t.cn/EyQmiuE

  2. 最小的ibana Docker镜像。 http://t.cn/EyQbwGk

  3. Lucene列式存储格式DocValues详解。 http://t.cn/EyQ6pkK
收起阅读 »

Day 8 - 如何使用Spark快速将数据写入Elasticsearch

如何使用Spark快速将数据写入Elasticsearch

说到数据写入Elasticsearch,最先想到的肯定是Logstash。Logstash因为其简单上手、可扩展、可伸缩等优点被广大用户接受。但是尺有所短,寸有所长,Logstash肯定也有它无法适用的应用场景,比如:

  • 海量数据ETL
  • 海量数据聚合
  • 多源数据处理

为了满足这些场景,很多同学都会选择Spark,借助Spark算子进行数据处理,最后将处理结果写入Elasticsearch。

我们部门之前利用Spark对Nginx日志进行分析,统计我们的Web服务访问情况,将Nginx日志每分钟聚合一次最后将结果写入Elasticsearch,然后利用Kibana配置实时监控Dashboard。Elasticsearch和Kibana都很方便、实用,但是随着类似需求越来越多,如何快速通过Spark将数据写入Elasticsearch成为了我们的一大问题。

今天给大家推荐一款能够实现数据快速写入的黑科技——Waterdrop,一个非常易用,高性能,能够应对海量数据的实时数据处理产品,它构建在Spark之上,简单易用,灵活配置,无需开发。

wd.png

Kafka to Elasticsearch

和Logstash一样,Waterdrop同样支持多种类型的数据输入,这里我们以最常见的Kakfa作为输入源为例,讲解如何使用Waterdrop将数据快速写入Elasticsearch

Log Sample

原始日志格式如下:

127.0.0.1 elasticsearch.cn 114.250.140.241 0.001s "127.0.0.1:80" [26/Oct/2018:21:54:32 +0800] "GET /article HTTP/1.1" 200 123 "-" - "Dalvik/2.1.0 (Linux; U; Android 7.1.1; OPPO R11 Build/NMF26X)"

Elasticsearch Document

我们想要统计,一分钟每个域名的访问情况,聚合完的数据有以下字段:

domain String
hostname String
status int
datetime String
count int

Waterdrop with Elasticsearch

接下来会给大家详细介绍,我们如何通过Waterdrop读取Kafka中的数据,对数据进行解析以及聚合,最后将处理结果写入Elasticsearch中。

Waterdrop

Waterdrop同样拥有着非常丰富的插件,支持从Kafka、HDFS、Hive中读取数据,进行各种各样的数据处理,并将结果写入Elasticsearch、Kudu或者Kafka中。

Prerequisites

首先我们需要安装Waterdrop,安装十分简单,无需配置系统环境变量

  1. 准备Spark环境
  2. 安装Waterdrop
  3. 配置Waterdrop

以下是简易步骤,具体安装可以参照Quick Start

cd /usr/local
wget https://archive.apache.org/dist/spark/spark-2.2.0/spark-2.2.0-bin-hadoop2.7.tgz
tar -xvf https://archive.apache.org/dist/spark/spark-2.2.0/spark-2.2.0-bin-hadoop2.7.tgz
wget https://github.com/InterestingLab/waterdrop/releases/download/v1.1.1/waterdrop-1.1.1.zip
unzip waterdrop-1.1.1.zip
cd waterdrop-1.1.1

vim config/waterdrop-env.sh
# 指定Spark安装路径
SPARK_HOME=${SPARK_HOME:-/usr/local/spark-2.2.0-bin-hadoop2.7}

Waterdrop Pipeline

与Logstash一样,我们仅需要编写一个Waterdrop Pipeline的配置文件即可完成数据的导入,相信了解Logstash的朋友可以很快入手Waterdrop配置。

配置文件包括四个部分,分别是Spark、Input、filter和Output。

Spark

这一部分是Spark的相关配置,主要配置Spark执行时所需的资源大小。

spark {
  spark.app.name = "Waterdrop"
  spark.executor.instances = 2
  spark.executor.cores = 1
  spark.executor.memory = "1g"
}

Input

这一部分定义数据源,如下是从Kafka中读取数据的配置案例,

kafkaStream {
    topics = "waterdrop-es"
    consumer.bootstrap.servers = "localhost:9092"
    consumer.group.id = "waterdrop_es_group"
    consumer.rebalance.max.retries = 100
}

Filter

在Filter部分,这里我们配置一系列的转化,包括正则解析将日志进行拆分、时间转换将HTTPDATE转化为Elasticsearch支持的日期格式、对Number类型的字段进行类型转换以及通过SQL进行数据聚合

filter {
    # 使用正则解析原始日志
    # 最开始数据都在raw_message字段中
    grok {
        source_field = "raw_message"
        pattern = '%{NOTSPACE:hostname}\\s%{NOTSPACE:domain}\\s%{IP:remote_addr}\\s%{NUMBER:request_time}s\\s\"%{DATA:upstream_ip}\"\\s\\[%{HTTPDATE:timestamp}\\]\\s\"%{NOTSPACE:method}\\s%{DATA:url}\\s%{NOTSPACE:http_ver}\"\\s%{NUMBER:status}\\s%{NUMBER:body_bytes_send}\\s%{DATA:referer}\\s%{NOTSPACE:cookie_info}\\s\"%{DATA:user_agent}'
   }
    # 将"dd/MMM/yyyy:HH:mm:ss Z"格式的数据转换为
    # Elasticsearch中支持的格式
    date {
        source_field = "timestamp"
        target_field = "datetime"
        source_time_format = "dd/MMM/yyyy:HH:mm:ss Z"
        target_time_format = "yyyy-MM-dd'T'HH:mm:ss.SSS+08:00"
    }
    ## 利用SQL对数据进行聚合
    sql {
        table_name = "access_log"
        sql = "select domain, hostname, int(status), datetime, count(*) from access_log group by domain, hostname, status, datetime"
    }
 }

Output

最后我们将处理好的结构化数据写入Elasticsearch。

output {
    elasticsearch {
        hosts = ["localhost:9200"]
        index = "waterdrop-${now}"
        es.batch.size.entries = 100000
        index_time_format = "yyyy.MM.dd"
    }
}

Running Waterdrop

我们将上述四部分配置组合成为我们的配置文件config/batch.conf

vim config/batch.conf
spark {
  spark.app.name = "Waterdrop"
  spark.executor.instances = 2
  spark.executor.cores = 1
  spark.executor.memory = "1g"
}
input {
    kafkaStream {
        topics = "waterdrop-es"
        consumer.bootstrap.servers = "localhost:9092"
        consumer.group.id = "waterdrop_es_group"
        consumer.rebalance.max.retries = 100
    }
}
filter {
    # 使用正则解析原始日志
    # 最开始数据都在raw_message字段中
    grok {
        source_field = "raw_message"
        pattern = '%{IP:hostname}\\s%{NOTSPACE:domain}\\s%{IP:remote_addr}\\s%{NUMBER:request_time}s\\s\"%{DATA:upstream_ip}\"\\s\\[%{HTTPDATE:timestamp}\\]\\s\"%{NOTSPACE:method}\\s%{DATA:url}\\s%{NOTSPACE:http_ver}\"\\s%{NUMBER:status}\\s%{NUMBER:body_bytes_send}\\s%{DATA:referer}\\s%{NOTSPACE:cookie_info}\\s\"%{DATA:user_agent}'
   }
    # 将"dd/MMM/yyyy:HH:mm:ss Z"格式的数据转换为
    # Elasticsearch中支持的格式
    date {
        source_field = "timestamp"
        target_field = "datetime"
        source_time_format = "dd/MMM/yyyy:HH:mm:ss Z"
        target_time_format = "yyyy-MM-dd'T'HH:mm:00.SSS+08:00"
    }
    ## 利用SQL对数据进行聚合
    sql {
        table_name = "access_log"
        sql = "select domain, hostname, status, datetime, count(*) from access_log group by domain, localhost, status, datetime"
    }
 }
output {
    elasticsearch {
        hosts = ["localhost:9200"]
        index = "waterdrop-${now}"
        es.batch.size.entries = 100000
        index_time_format = "yyyy.MM.dd"
    }
}

执行命令,指定配置文件,运行Waterdrop,即可将数据写入Elasticsearch。这里我们以本地模式为例。

./bin/start-waterdrop.sh --config config/batch.conf -e client -m 'local[2]'

最后,写入Elasticsearch中的数据如下,再配上Kibana就可以实现Web服务的实时监控了^_^.

"_source": {
    "domain": "elasticsearch.cn",
    "hostname": "localhost",
    "status": "200",
    "datetime": "2018-11-26T21:54:00.000+08:00",
    "count": 26
  }

Conclusion

在这篇文章中,我们介绍了如何通过Waterdrop将Kafka中的数据写入Elasticsearch中。仅仅通过一个配置文件便可快速运行一个Spark Application,完成数据的处理、写入,无需编写任何代码,十分简单。

当数据处理过程中有遇到Logstash无法支持的场景或者Logstah性能无法达到预期的情况下,都可以尝试使用Waterdrop解决问题。

希望了解Waterdrop与Elasticsearch、Kafka、Hadoop结合使用的更多功能和案例,可以直接进入项目主页https://github.com/InterestingLab/waterdrop

我们近期会再发布一篇《如何用Spark和Elasticsearch做交互式数据分析》,敬请期待.

Contract us

欢迎联系我们交流Spark和Elasticsearch:

Garyelephant: 微信: garyelephant

RickyHuo: 微信: chodomatte1994

继续阅读 »

如何使用Spark快速将数据写入Elasticsearch

说到数据写入Elasticsearch,最先想到的肯定是Logstash。Logstash因为其简单上手、可扩展、可伸缩等优点被广大用户接受。但是尺有所短,寸有所长,Logstash肯定也有它无法适用的应用场景,比如:

  • 海量数据ETL
  • 海量数据聚合
  • 多源数据处理

为了满足这些场景,很多同学都会选择Spark,借助Spark算子进行数据处理,最后将处理结果写入Elasticsearch。

我们部门之前利用Spark对Nginx日志进行分析,统计我们的Web服务访问情况,将Nginx日志每分钟聚合一次最后将结果写入Elasticsearch,然后利用Kibana配置实时监控Dashboard。Elasticsearch和Kibana都很方便、实用,但是随着类似需求越来越多,如何快速通过Spark将数据写入Elasticsearch成为了我们的一大问题。

今天给大家推荐一款能够实现数据快速写入的黑科技——Waterdrop,一个非常易用,高性能,能够应对海量数据的实时数据处理产品,它构建在Spark之上,简单易用,灵活配置,无需开发。

wd.png

Kafka to Elasticsearch

和Logstash一样,Waterdrop同样支持多种类型的数据输入,这里我们以最常见的Kakfa作为输入源为例,讲解如何使用Waterdrop将数据快速写入Elasticsearch

Log Sample

原始日志格式如下:

127.0.0.1 elasticsearch.cn 114.250.140.241 0.001s "127.0.0.1:80" [26/Oct/2018:21:54:32 +0800] "GET /article HTTP/1.1" 200 123 "-" - "Dalvik/2.1.0 (Linux; U; Android 7.1.1; OPPO R11 Build/NMF26X)"

Elasticsearch Document

我们想要统计,一分钟每个域名的访问情况,聚合完的数据有以下字段:

domain String
hostname String
status int
datetime String
count int

Waterdrop with Elasticsearch

接下来会给大家详细介绍,我们如何通过Waterdrop读取Kafka中的数据,对数据进行解析以及聚合,最后将处理结果写入Elasticsearch中。

Waterdrop

Waterdrop同样拥有着非常丰富的插件,支持从Kafka、HDFS、Hive中读取数据,进行各种各样的数据处理,并将结果写入Elasticsearch、Kudu或者Kafka中。

Prerequisites

首先我们需要安装Waterdrop,安装十分简单,无需配置系统环境变量

  1. 准备Spark环境
  2. 安装Waterdrop
  3. 配置Waterdrop

以下是简易步骤,具体安装可以参照Quick Start

cd /usr/local
wget https://archive.apache.org/dist/spark/spark-2.2.0/spark-2.2.0-bin-hadoop2.7.tgz
tar -xvf https://archive.apache.org/dist/spark/spark-2.2.0/spark-2.2.0-bin-hadoop2.7.tgz
wget https://github.com/InterestingLab/waterdrop/releases/download/v1.1.1/waterdrop-1.1.1.zip
unzip waterdrop-1.1.1.zip
cd waterdrop-1.1.1

vim config/waterdrop-env.sh
# 指定Spark安装路径
SPARK_HOME=${SPARK_HOME:-/usr/local/spark-2.2.0-bin-hadoop2.7}

Waterdrop Pipeline

与Logstash一样,我们仅需要编写一个Waterdrop Pipeline的配置文件即可完成数据的导入,相信了解Logstash的朋友可以很快入手Waterdrop配置。

配置文件包括四个部分,分别是Spark、Input、filter和Output。

Spark

这一部分是Spark的相关配置,主要配置Spark执行时所需的资源大小。

spark {
  spark.app.name = "Waterdrop"
  spark.executor.instances = 2
  spark.executor.cores = 1
  spark.executor.memory = "1g"
}

Input

这一部分定义数据源,如下是从Kafka中读取数据的配置案例,

kafkaStream {
    topics = "waterdrop-es"
    consumer.bootstrap.servers = "localhost:9092"
    consumer.group.id = "waterdrop_es_group"
    consumer.rebalance.max.retries = 100
}

Filter

在Filter部分,这里我们配置一系列的转化,包括正则解析将日志进行拆分、时间转换将HTTPDATE转化为Elasticsearch支持的日期格式、对Number类型的字段进行类型转换以及通过SQL进行数据聚合

filter {
    # 使用正则解析原始日志
    # 最开始数据都在raw_message字段中
    grok {
        source_field = "raw_message"
        pattern = '%{NOTSPACE:hostname}\\s%{NOTSPACE:domain}\\s%{IP:remote_addr}\\s%{NUMBER:request_time}s\\s\"%{DATA:upstream_ip}\"\\s\\[%{HTTPDATE:timestamp}\\]\\s\"%{NOTSPACE:method}\\s%{DATA:url}\\s%{NOTSPACE:http_ver}\"\\s%{NUMBER:status}\\s%{NUMBER:body_bytes_send}\\s%{DATA:referer}\\s%{NOTSPACE:cookie_info}\\s\"%{DATA:user_agent}'
   }
    # 将"dd/MMM/yyyy:HH:mm:ss Z"格式的数据转换为
    # Elasticsearch中支持的格式
    date {
        source_field = "timestamp"
        target_field = "datetime"
        source_time_format = "dd/MMM/yyyy:HH:mm:ss Z"
        target_time_format = "yyyy-MM-dd'T'HH:mm:ss.SSS+08:00"
    }
    ## 利用SQL对数据进行聚合
    sql {
        table_name = "access_log"
        sql = "select domain, hostname, int(status), datetime, count(*) from access_log group by domain, hostname, status, datetime"
    }
 }

Output

最后我们将处理好的结构化数据写入Elasticsearch。

output {
    elasticsearch {
        hosts = ["localhost:9200"]
        index = "waterdrop-${now}"
        es.batch.size.entries = 100000
        index_time_format = "yyyy.MM.dd"
    }
}

Running Waterdrop

我们将上述四部分配置组合成为我们的配置文件config/batch.conf

vim config/batch.conf
spark {
  spark.app.name = "Waterdrop"
  spark.executor.instances = 2
  spark.executor.cores = 1
  spark.executor.memory = "1g"
}
input {
    kafkaStream {
        topics = "waterdrop-es"
        consumer.bootstrap.servers = "localhost:9092"
        consumer.group.id = "waterdrop_es_group"
        consumer.rebalance.max.retries = 100
    }
}
filter {
    # 使用正则解析原始日志
    # 最开始数据都在raw_message字段中
    grok {
        source_field = "raw_message"
        pattern = '%{IP:hostname}\\s%{NOTSPACE:domain}\\s%{IP:remote_addr}\\s%{NUMBER:request_time}s\\s\"%{DATA:upstream_ip}\"\\s\\[%{HTTPDATE:timestamp}\\]\\s\"%{NOTSPACE:method}\\s%{DATA:url}\\s%{NOTSPACE:http_ver}\"\\s%{NUMBER:status}\\s%{NUMBER:body_bytes_send}\\s%{DATA:referer}\\s%{NOTSPACE:cookie_info}\\s\"%{DATA:user_agent}'
   }
    # 将"dd/MMM/yyyy:HH:mm:ss Z"格式的数据转换为
    # Elasticsearch中支持的格式
    date {
        source_field = "timestamp"
        target_field = "datetime"
        source_time_format = "dd/MMM/yyyy:HH:mm:ss Z"
        target_time_format = "yyyy-MM-dd'T'HH:mm:00.SSS+08:00"
    }
    ## 利用SQL对数据进行聚合
    sql {
        table_name = "access_log"
        sql = "select domain, hostname, status, datetime, count(*) from access_log group by domain, localhost, status, datetime"
    }
 }
output {
    elasticsearch {
        hosts = ["localhost:9200"]
        index = "waterdrop-${now}"
        es.batch.size.entries = 100000
        index_time_format = "yyyy.MM.dd"
    }
}

执行命令,指定配置文件,运行Waterdrop,即可将数据写入Elasticsearch。这里我们以本地模式为例。

./bin/start-waterdrop.sh --config config/batch.conf -e client -m 'local[2]'

最后,写入Elasticsearch中的数据如下,再配上Kibana就可以实现Web服务的实时监控了^_^.

"_source": {
    "domain": "elasticsearch.cn",
    "hostname": "localhost",
    "status": "200",
    "datetime": "2018-11-26T21:54:00.000+08:00",
    "count": 26
  }

Conclusion

在这篇文章中,我们介绍了如何通过Waterdrop将Kafka中的数据写入Elasticsearch中。仅仅通过一个配置文件便可快速运行一个Spark Application,完成数据的处理、写入,无需编写任何代码,十分简单。

当数据处理过程中有遇到Logstash无法支持的场景或者Logstah性能无法达到预期的情况下,都可以尝试使用Waterdrop解决问题。

希望了解Waterdrop与Elasticsearch、Kafka、Hadoop结合使用的更多功能和案例,可以直接进入项目主页https://github.com/InterestingLab/waterdrop

我们近期会再发布一篇《如何用Spark和Elasticsearch做交互式数据分析》,敬请期待.

Contract us

欢迎联系我们交流Spark和Elasticsearch:

Garyelephant: 微信: garyelephant

RickyHuo: 微信: chodomatte1994

收起阅读 »

Day 7 - Elasticsearch中数据是如何存储的

前言

很多使用Elasticsearch的同学会关心数据存储在ES中的存储容量,会有这样的疑问:xxTB的数据入到ES会使用多少存储空间。这个问题其实很难直接回答的,只有数据写入ES后,才能观察到实际的存储空间。比如同样是1TB的数据,写入ES的存储空间可能差距会非常大,可能小到只有300~400GB,也可能多到6-7TB,为什么会造成这么大的差距呢?究其原因,我们来探究下Elasticsearch中的数据是如何存储。文章中我以Elasticsearch 2.3版本为示例,对应的lucene版本是5.5,Elasticsearch现在已经来到了6.5版本,数字类型、列存等存储结构有些变化,但基本的概念变化不多,文章中的内容依然适用。

Elasticsearch索引结构

Elasticsearch对外提供的是index的概念,可以类比为DB,用户查询是在index上完成的,每个index由若干个shard组成,以此来达到分布式可扩展的能力。比如下图是一个由10个shard组成的index。

elasticsearch_store_arc.png

shard是Elasticsearch数据存储的最小单位,index的存储容量为所有shard的存储容量之和。Elasticsearch集群的存储容量则为所有index存储容量之和。

一个shard就对应了一个lucene的library。对于一个shard,Elasticsearch增加了translog的功能,类似于HBase WAL,是数据写入过程中的中间数据,其余的数据都在lucene库中管理的。

所以Elasticsearch索引使用的存储内容主要取决于lucene中的数据存储。

lucene数据存储

下面我们主要看下lucene的文件内容,在了解lucene文件内容前,大家先了解些lucene的基本概念。

lucene基本概念

  • segment : lucene内部的数据是由一个个segment组成的,写入lucene的数据并不直接落盘,而是先写在内存中,经过了refresh间隔,lucene才将该时间段写入的全部数据refresh成一个segment,segment多了之后会进行merge成更大的segment。lucene查询时会遍历每个segment完成。由于lucene* 写入的数据是在内存中完成,所以写入效率非常高。但是也存在丢失数据的风险,所以Elasticsearch基于此现象实现了translog,只有在segment数据落盘后,Elasticsearch才会删除对应的translog。
  • doc : doc表示lucene中的一条记录
  • field :field表示记录中的字段概念,一个doc由若干个field组成。
  • term :term是lucene中索引的最小单位,某个field对应的内容如果是全文检索类型,会将内容进行分词,分词的结果就是由term组成的。如果是不分词的字段,那么该字段的内容就是一个term。
  • 倒排索引(inverted index): lucene索引的通用叫法,即实现了term到doc list的映射。
  • 正排数据:搜索引擎的通用叫法,即原始数据,可以理解为一个doc list。
  • docvalues :Elasticsearch中的列式存储的名称,Elasticsearch除了存储原始存储、倒排索引,还存储了一份docvalues,用作分析和排序。

lucene文件内容

lucene包的文件是由很多segment文件组成的,segments_xxx文件记录了lucene包下面的segment文件数量。每个segment会包含如下的文件。

Name Extension Brief Description
Segment Info .si segment的元数据文件
Compound File .cfs, .cfe 一个segment包含了如下表的各个文件,为减少打开文件的数量,在segment小的时候,segment的所有文件内容都保存在cfs文件中,cfe文件保存了lucene各文件在cfs文件的位置信息
Fields .fnm 保存了fields的相关信息
Field Index .fdx 正排存储文件的元数据信息
Field Data .fdt 存储了正排存储数据,写入的原文存储在这
Term Dictionary .tim 倒排索引的元数据信息
Term Index .tip 倒排索引文件,存储了所有的倒排索引数据
Frequencies .doc 保存了每个term的doc id列表和term在doc中的词频
Positions .pos Stores position information about where a term occurs in the index
全文索引的字段,会有该文件,保存了term在doc中的位置
Payloads .pay Stores additional per-position metadata information such as character offsets and user payloads
全文索引的字段,使用了一些像payloads的高级特性会有该文件,保存了term在doc中的一些高级特性
Norms .nvd, .nvm 文件保存索引字段加权数据
Per-Document Values .dvd, .dvm lucene的docvalues文件,即数据的列式存储,用作聚合和排序
Term Vector Data .tvx, .tvd, .tvf Stores offset into the document data file
保存索引字段的矢量信息,用在对term进行高亮,计算文本相关性中使用
Live Documents .liv 记录了segment中删除的doc

测试数据示例

下面我们以真实的数据作为示例,看看lucene中各类型数据的容量占比。

写100w数据,有一个uuid字段,写入的是长度为36位的uuid,字符串总为3600w字节,约为35M。

数据使用一个shard,不带副本,使用默认的压缩算法,写入完成后merge成一个segment方便观察。

使用线上默认的配置,uuid存为不分词的字符串类型。创建如下索引:

PUT test_field
{
  "settings": {
    "index": {
      "number_of_shards": "1",
      "number_of_replicas": "0",
      "refresh_interval": "30s"
    }
  },
  "mappings": {
    "type": {
      "_all": {
        "enabled": false
      }, 
      "properties": {
        "uuid": {
          "type": "string",
          "index": "not_analyzed"
        }
      }
    }
  }
}

首先写入100w不同的uuid,使用磁盘容量细节如下:


health status index      pri rep docs.count docs.deleted store.size pri.store.size 
green  open   test_field   1   0    1000000            0    122.7mb        122.7mb 

-rw-r--r--  1 weizijun  staff    41M Aug 19 21:23 _8.fdt
-rw-r--r--  1 weizijun  staff    17K Aug 19 21:23 _8.fdx
-rw-r--r--  1 weizijun  staff   688B Aug 19 21:23 _8.fnm
-rw-r--r--  1 weizijun  staff   494B Aug 19 21:23 _8.si
-rw-r--r--  1 weizijun  staff   265K Aug 19 21:23 _8_Lucene50_0.doc
-rw-r--r--  1 weizijun  staff    44M Aug 19 21:23 _8_Lucene50_0.tim
-rw-r--r--  1 weizijun  staff   340K Aug 19 21:23 _8_Lucene50_0.tip
-rw-r--r--  1 weizijun  staff    37M Aug 19 21:23 _8_Lucene54_0.dvd
-rw-r--r--  1 weizijun  staff   254B Aug 19 21:23 _8_Lucene54_0.dvm
-rw-r--r--  1 weizijun  staff   195B Aug 19 21:23 segments_2
-rw-r--r--  1 weizijun  staff     0B Aug 19 21:20 write.lock

可以看到正排数据、倒排索引数据,列存数据容量占比几乎相同,正排数据和倒排数据还会存储Elasticsearch的唯一id字段,所以容量会比列存多一些。

35M的uuid存入Elasticsearch后,数据膨胀了3倍,达到了122.7mb。Elasticsearch竟然这么消耗资源,不要着急下结论,接下来看另一个测试结果。

我们写入100w一样的uuid,然后看看Elasticsearch使用的容量。

health status index      pri rep docs.count docs.deleted store.size pri.store.size 
green  open   test_field   1   0    1000000            0     13.2mb         13.2mb 

-rw-r--r--  1 weizijun  staff   5.5M Aug 19 21:29 _6.fdt
-rw-r--r--  1 weizijun  staff    15K Aug 19 21:29 _6.fdx
-rw-r--r--  1 weizijun  staff   688B Aug 19 21:29 _6.fnm
-rw-r--r--  1 weizijun  staff   494B Aug 19 21:29 _6.si
-rw-r--r--  1 weizijun  staff   309K Aug 19 21:29 _6_Lucene50_0.doc
-rw-r--r--  1 weizijun  staff   7.0M Aug 19 21:29 _6_Lucene50_0.tim
-rw-r--r--  1 weizijun  staff   195K Aug 19 21:29 _6_Lucene50_0.tip
-rw-r--r--  1 weizijun  staff   244K Aug 19 21:29 _6_Lucene54_0.dvd
-rw-r--r--  1 weizijun  staff   252B Aug 19 21:29 _6_Lucene54_0.dvm
-rw-r--r--  1 weizijun  staff   195B Aug 19 21:29 segments_2
-rw-r--r--  1 weizijun  staff     0B Aug 19 21:26 write.lock

这回35M的数据Elasticsearch容量只有13.2mb,其中还有主要的占比还是Elasticsearch的唯一id,100w的uuid几乎不占存储容积。

所以在Elasticsearch中建立索引的字段如果基数越大(count distinct),越占用磁盘空间。

我们再看看存100w个不一样的整型会是如何。

health status index      pri rep docs.count docs.deleted store.size pri.store.size 
green  open   test_field   1   0    1000000            0     13.6mb         13.6mb 

-rw-r--r--  1 weizijun  staff   6.1M Aug 28 10:19 _42.fdt
-rw-r--r--  1 weizijun  staff    22K Aug 28 10:19 _42.fdx
-rw-r--r--  1 weizijun  staff   688B Aug 28 10:19 _42.fnm
-rw-r--r--  1 weizijun  staff   503B Aug 28 10:19 _42.si
-rw-r--r--  1 weizijun  staff   2.8M Aug 28 10:19 _42_Lucene50_0.doc
-rw-r--r--  1 weizijun  staff   2.2M Aug 28 10:19 _42_Lucene50_0.tim
-rw-r--r--  1 weizijun  staff    83K Aug 28 10:19 _42_Lucene50_0.tip
-rw-r--r--  1 weizijun  staff   2.5M Aug 28 10:19 _42_Lucene54_0.dvd
-rw-r--r--  1 weizijun  staff   228B Aug 28 10:19 _42_Lucene54_0.dvm
-rw-r--r--  1 weizijun  staff   196B Aug 28 10:19 segments_2
-rw-r--r--  1 weizijun  staff     0B Aug 28 10:16 write.lock

从结果可以看到,100w整型数据,Elasticsearch的存储开销为13.6mb。如果以int型计算100w数据的长度的话,为400w字节,大概是3.8mb数据。忽略Elasticsearch唯一id字段的影响,Elasticsearch实际存储容量跟整型数据长度差不多。

我们再看一下开启最佳压缩参数对存储空间的影响:

health status index      pri rep docs.count docs.deleted store.size pri.store.size 
green  open   test_field   1   0    1000000            0    107.2mb        107.2mb 

-rw-r--r--  1 weizijun  staff    25M Aug 20 12:30 _5.fdt
-rw-r--r--  1 weizijun  staff   6.0K Aug 20 12:30 _5.fdx
-rw-r--r--  1 weizijun  staff   688B Aug 20 12:31 _5.fnm
-rw-r--r--  1 weizijun  staff   500B Aug 20 12:31 _5.si
-rw-r--r--  1 weizijun  staff   265K Aug 20 12:31 _5_Lucene50_0.doc
-rw-r--r--  1 weizijun  staff    44M Aug 20 12:31 _5_Lucene50_0.tim
-rw-r--r--  1 weizijun  staff   322K Aug 20 12:31 _5_Lucene50_0.tip
-rw-r--r--  1 weizijun  staff    37M Aug 20 12:31 _5_Lucene54_0.dvd
-rw-r--r--  1 weizijun  staff   254B Aug 20 12:31 _5_Lucene54_0.dvm
-rw-r--r--  1 weizijun  staff   224B Aug 20 12:31 segments_4
-rw-r--r--  1 weizijun  staff     0B Aug 20 12:00 write.lock

结果中可以发现,只有正排数据会启动压缩,压缩能力确实强劲,不考虑唯一id字段,存储容量大概压缩到接近50%。

我们还做了一些实验,Elasticsearch默认是开启_all参数的,_all可以让用户传入的整体json数据作为全文检索的字段,可以更方便的检索,但在现实场景中已经使用的不多,相反会增加很多存储容量的开销,可以看下开启_all的磁盘空间使用情况:


health status index      pri rep docs.count docs.deleted store.size pri.store.size 
green  open   test_field   1   0    1000000            0    162.4mb        162.4mb 

-rw-r--r--  1 weizijun  staff    41M Aug 18 22:59 _20.fdt
-rw-r--r--  1 weizijun  staff    18K Aug 18 22:59 _20.fdx
-rw-r--r--  1 weizijun  staff   777B Aug 18 22:59 _20.fnm
-rw-r--r--  1 weizijun  staff    59B Aug 18 22:59 _20.nvd
-rw-r--r--  1 weizijun  staff    78B Aug 18 22:59 _20.nvm
-rw-r--r--  1 weizijun  staff   539B Aug 18 22:59 _20.si
-rw-r--r--  1 weizijun  staff   7.2M Aug 18 22:59 _20_Lucene50_0.doc
-rw-r--r--  1 weizijun  staff   4.2M Aug 18 22:59 _20_Lucene50_0.pos
-rw-r--r--  1 weizijun  staff    73M Aug 18 22:59 _20_Lucene50_0.tim
-rw-r--r--  1 weizijun  staff   832K Aug 18 22:59 _20_Lucene50_0.tip
-rw-r--r--  1 weizijun  staff    37M Aug 18 22:59 _20_Lucene54_0.dvd
-rw-r--r--  1 weizijun  staff   254B Aug 18 22:59 _20_Lucene54_0.dvm
-rw-r--r--  1 weizijun  staff   196B Aug 18 22:59 segments_2
-rw-r--r--  1 weizijun  staff     0B Aug 18 22:53 write.lock

开启_all比不开启多了40mb的存储空间,多的数据都在倒排索引上,大约会增加30%多的存储开销。所以线上都直接禁用。

然后我还做了其他几个尝试,为了验证存储容量是否和数据量成正比,写入1000w数据的uuid,发现存储容量基本为100w数据的10倍。我还验证了数据长度是否和数据量成正比,发现把uuid增长2倍、4倍,存储容量也响应的增加了2倍和4倍。在此就不一一列出数据了。

lucene各文件具体内容和实现

lucene数据元信息文件

文件名为:segments_xxx

该文件为lucene数据文件的元信息文件,记录所有segment的元数据信息。

该文件主要记录了目前有多少segment,每个segment有一些基本信息,更新这些信息定位到每个segment的元信息文件。

lucene元信息文件还支持记录userData,Elasticsearch可以在此记录translog的一些相关信息。

文件示例

elasticsearch_store_segments.png

具体实现类

public final class SegmentInfos implements Cloneable, Iterable<SegmentCommitInfo> {
  // generation是segment的版本的概念,从文件名中提取出来,实例中为:2t/101
  private long generation;     // generation of the "segments_N" for the next commit

  private long lastGeneration; // generation of the "segments_N" file we last successfully read
                               // or wrote; this is normally the same as generation except if
                               // there was an IOException that had interrupted a commit

  /** Id for this commit; only written starting with Lucene 5.0 */
  private byte[] id;

  /** Which Lucene version wrote this commit, or null if this commit is pre-5.3. */
  private Version luceneVersion;

  /** Counts how often the index has been changed.  */
  public long version;

  /** Used to name new segments. */
  // TODO: should this be a long ...?
  public int counter;

  /** Version of the oldest segment in the index, or null if there are no segments. */
  private Version minSegmentLuceneVersion;

  private List<SegmentCommitInfo> segments = new ArrayList<>();

  /** Opaque Map&lt;String, String&gt; that user can specify during IndexWriter.commit */
  public Map<String,String> userData = Collections.emptyMap();
}

/** Embeds a [read-only] SegmentInfo and adds per-commit
 *  fields.
 *
 *  @lucene.experimental */
public class SegmentCommitInfo {

  /** The {@link SegmentInfo} that we wrap. */
  public final SegmentInfo info;

  // How many deleted docs in the segment:
  private int delCount;

  // Generation number of the live docs file (-1 if there
  // are no deletes yet):
  private long delGen;

  // Normally 1+delGen, unless an exception was hit on last
  // attempt to write:
  private long nextWriteDelGen;

  // Generation number of the FieldInfos (-1 if there are no updates)
  private long fieldInfosGen;

  // Normally 1+fieldInfosGen, unless an exception was hit on last attempt to
  // write
  private long nextWriteFieldInfosGen; //fieldInfosGen == -1 ? 1 : fieldInfosGen + 1;

  // Generation number of the DocValues (-1 if there are no updates)
  private long docValuesGen;

  // Normally 1+dvGen, unless an exception was hit on last attempt to
  // write
  private long nextWriteDocValuesGen; //docValuesGen == -1 ? 1 : docValuesGen + 1;

  // TODO should we add .files() to FieldInfosFormat, like we have on
  // LiveDocsFormat?
  // track the fieldInfos update files
  private final Set<String> fieldInfosFiles = new HashSet<>();

  // Track the per-field DocValues update files
  private final Map<Integer,Set<String>> dvUpdatesFiles = new HashMap<>();

  // Track the per-generation updates files
  @Deprecated
  private final Map<Long,Set<String>> genUpdatesFiles = new HashMap<>();

  private volatile long sizeInBytes = -1;
}

segment的元信息文件

文件后缀:.si

每个segment都有一个.si文件,记录了该segment的元信息。

segment元信息文件中记录了segment的文档数量,segment对应的文件列表等信息。

文件示例

elasticsearch_store_si.png

具体实现类

/**
 * Information about a segment such as its name, directory, and files related
 * to the segment.
 *
 * @lucene.experimental
 */
public final class SegmentInfo {

  // _bl
  public final String name;

  /** Where this segment resides. */
  public final Directory dir;

  /** Id that uniquely identifies this segment. */
  private final byte[] id;

  private Codec codec;

  // Tracks the Lucene version this segment was created with, since 3.1. Null
  // indicates an older than 3.0 index, and it's used to detect a too old index.
  // The format expected is "x.y" - "2.x" for pre-3.0 indexes (or null), and
  // specific versions afterwards ("3.0.0", "3.1.0" etc.).
  // see o.a.l.util.Version.
  private Version version;

  private int maxDoc;         // number of docs in seg

  private boolean isCompoundFile;

  private Map<String,String> diagnostics;

  private Set<String> setFiles;

  private final Map<String,String> attributes;
}

fields信息文件

文件后缀:.fnm

该文件存储了fields的基本信息。

fields信息中包括field的数量,field的类型,以及IndexOpetions,包括是否存储、是否索引,是否分词,是否需要列存等等。

文件示例

elasticsearch_store_fnm.png

具体实现类

/**
 *  Access to the Field Info file that describes document fields and whether or
 *  not they are indexed. Each segment has a separate Field Info file. Objects
 *  of this class are thread-safe for multiple readers, but only one thread can
 *  be adding documents at a time, with no other reader or writer threads
 *  accessing this object.
 **/
public final class FieldInfo {
  /** Field's name */
  public final String name;

  /** Internal field number */
  //field在内部的编号
  public final int number;

  //field docvalues的类型
  private DocValuesType docValuesType = DocValuesType.NONE;

  // True if any document indexed term vectors
  private boolean storeTermVector;

  private boolean omitNorms; // omit norms associated with indexed fields 

  //index的配置项
  private IndexOptions indexOptions = IndexOptions.NONE;

  private boolean storePayloads; // whether this field stores payloads together with term positions 

  private final Map<String,String> attributes;

  // docvalues的generation
  private long dvGen;
}

数据存储文件

文件后缀:.fdx, .fdt

索引文件为.fdx,数据文件为.fdt,数据存储文件功能为根据自动的文档id,得到文档的内容,搜索引擎的术语习惯称之为正排数据,即doc_id -> content,es的_source数据就存在这

索引文件记录了快速定位文档数据的索引信息,数据文件记录了所有文档id的具体内容。

文件示例

elasticsearch_store_fdt.png

具体实现类

/**
 * Random-access reader for {@link CompressingStoredFieldsIndexWriter}.
 * @lucene.internal
 */
public final class CompressingStoredFieldsIndexReader implements Cloneable, Accountable {
  private static final long BASE_RAM_BYTES_USED = RamUsageEstimator.shallowSizeOfInstance(CompressingStoredFieldsIndexReader.class);

  final int maxDoc;

  //docid索引,快速定位某个docid的数组坐标
  final int[] docBases;

  //快速定位某个docid所在的文件offset的startPointer
  final long[] startPointers;

  //平均一个chunk的文档数
  final int[] avgChunkDocs;

  //平均一个chunk的size
  final long[] avgChunkSizes;

  final PackedInts.Reader[] docBasesDeltas; // delta from the avg

  final PackedInts.Reader[] startPointersDeltas; // delta from the avg
}

/**
 * {@link StoredFieldsReader} impl for {@link CompressingStoredFieldsFormat}.
 * @lucene.experimental
 */
public final class CompressingStoredFieldsReader extends StoredFieldsReader {

  //从fdt正排索引文件中获得
  private final int version;

  // field的基本信息
  private final FieldInfos fieldInfos;

  //fdt正排索引文件reader
  private final CompressingStoredFieldsIndexReader indexReader;

  //从fdt正排索引文件中获得,用于指向fdx数据文件的末端,指向numChunks地址4
  private final long maxPointer;

  //fdx正排数据文件句柄
  private final IndexInput fieldsStream;

  //块大小
  private final int chunkSize;

  private final int packedIntsVersion;

  //压缩类型
  private final CompressionMode compressionMode;

  //解压缩处理对象
  private final Decompressor decompressor;

  //文档数量,从segment元数据中获得
  private final int numDocs;

  //是否正在merge,默认为false
  private final boolean merging;

  //初始化时new了一个BlockState,BlockState记录下当前正排文件读取的状态信息
  private final BlockState state;
  //chunk的数量
  private final long numChunks; // number of compressed blocks written

  //dirty chunk的数量
  private final long numDirtyChunks; // number of incomplete compressed blocks written

  //是否close,默认为false
  private boolean closed;
}

倒排索引文件

索引后缀:.tip,.tim

倒排索引也包含索引文件和数据文件,.tip为索引文件,.tim为数据文件,索引文件包含了每个字段的索引元信息,数据文件有具体的索引内容。

5.5.0版本的倒排索引实现为FST tree,FST tree的最大优势就是内存空间占用非常低 ,具体可以参看下这篇文章:http://www.cnblogs.com/bonelee/p/6226185.html

http://examples.mikemccandless.com/fst.py?terms=&cmd=Build+it 为FST图实例,可以根据输入的数据构造出FST图

输入到 FST 中的数据为:
String inputValues[] = {"mop","moth","pop","star","stop","top"};
long outputValues[] = {0,1,2,3,4,5};

生成的 FST 图为:

elasticsearch_store_tip1.png

elasticsearch_store_tip2.png

文件示例

elasticsearch_store_tip3.png

具体实现类

public final class BlockTreeTermsReader extends FieldsProducer {
  // Open input to the main terms dict file (_X.tib)
  final IndexInput termsIn;
  // Reads the terms dict entries, to gather state to
  // produce DocsEnum on demand
  final PostingsReaderBase postingsReader;
  private final TreeMap<String,FieldReader> fields = new TreeMap<>();

  /** File offset where the directory starts in the terms file. */
  /索引数据文件tim的数据的尾部的元数据的地址
  private long dirOffset;
  /** File offset where the directory starts in the index file. */

  //索引文件tip的数据的尾部的元数据的地址
  private long indexDirOffset;

  //semgent的名称
  final String segment;

  //版本号
  final int version;

  //5.3.x index, we record up front if we may have written any auto-prefix terms,示例中记录的是false
  final boolean anyAutoPrefixTerms;
}

/**
 * BlockTree's implementation of {@link Terms}.
 * @lucene.internal
 */
public final class FieldReader extends Terms implements Accountable {

  //term的数量
  final long numTerms;

  //field信息
  final FieldInfo fieldInfo;

  final long sumTotalTermFreq;

  //总的文档频率
  final long sumDocFreq;

  //文档数量
  final int docCount;

  //字段在索引文件tip中的起始位置
  final long indexStartFP;

  final long rootBlockFP;

  final BytesRef rootCode;

  final BytesRef minTerm;

  final BytesRef maxTerm;

  //longs:metadata buffer, holding monotonic values
  final int longsSize;

  final BlockTreeTermsReader parent;

  final FST<BytesRef> index;
}

倒排链文件

文件后缀:.doc, .pos, .pay

.doc保存了每个term的doc id列表和term在doc中的词频

全文索引的字段,会有.pos文件,保存了term在doc中的位置

全文索引的字段,使用了一些像payloads的高级特性才会有.pay文件,保存了term在doc中的一些高级特性

文件示例

elasticsearch_store_doc.png

具体实现类

/**
 * Concrete class that reads docId(maybe frq,pos,offset,payloads) list
 * with postings format.
 *
 * @lucene.experimental
 */
public final class Lucene50PostingsReader extends PostingsReaderBase {
  private static final long BASE_RAM_BYTES_USED = RamUsageEstimator.shallowSizeOfInstance(Lucene50PostingsReader.class);
  private final IndexInput docIn;
  private final IndexInput posIn;
  private final IndexInput payIn;
  final ForUtil forUtil;
  private int version;

  //不分词的字段使用的是该对象,基于skiplist实现了倒排链
  final class BlockDocsEnum extends PostingsEnum {
  }

  //全文检索字段使用的是该对象
  final class BlockPostingsEnum extends PostingsEnum {
  }

  //包含高级特性的字段使用的是该对象
  final class EverythingEnum extends PostingsEnum {
  }
}

列存文件(docvalues)

文件后缀:.dvm, .dvd

索引文件为.dvm,数据文件为.dvd。

lucene实现的docvalues有如下类型:

  • 1、NONE 不开启docvalue时的状态
  • 2、NUMERIC 单个数值类型的docvalue主要包括(int,long,float,double)
  • 3、BINARY 二进制类型值对应不同的codes最大值可能超过32766字节,
  • 4、SORTED 有序增量字节存储,仅仅存储不同部分的值和偏移量指针,值必须小于等于32766字节
  • 5、SORTED_NUMERIC 存储数值类型的有序数组列表
  • 6、SORTED_SET 可以存储多值域的docvalue值,但返回时,仅仅只能返回多值域的第一个docvalue
  • 7、对应not_anaylized的string字段,使用的是SORTED_SET类型,number的类型是SORTED_NUMERIC类型

其中SORTED_SET 的 SORTED_SINGLE_VALUED类型包括了两类数据 : binary + numeric, binary是按ord排序的term的列表,numeric是doc到ord的映射。

文件示例

elasticsearch_store_dvd.png

具体实现类

/** reader for {@link Lucene54DocValuesFormat} */
final class Lucene54DocValuesProducer extends DocValuesProducer implements Closeable {
  //number类型的field的列存列表
  private final Map<String,NumericEntry> numerics = new HashMap<>();

  //字符串类型的field的列存列表
  private final Map<String,BinaryEntry> binaries = new HashMap<>();

  //有序字符串类型的field的列存列表
  private final Map<String,SortedSetEntry> sortedSets = new HashMap<>();

  //有序number类型的field的列存列表
  private final Map<String,SortedSetEntry> sortedNumerics = new HashMap<>();

  //字符串类型的field的ords列表
  private final Map<String,NumericEntry> ords = new HashMap<>();

  //docId -> address -> ord 中field的ords列表
  private final Map<String,NumericEntry> ordIndexes = new HashMap<>();

  //field的数量
  private final int numFields;

  //内存使用量
  private final AtomicLong ramBytesUsed;

  //数据源的文件句柄
  private final IndexInput data;

  //文档数
  private final int maxDoc;
  // memory-resident structures
  private final Map<String,MonotonicBlockPackedReader> addressInstances = new HashMap<>();
  private final Map<String,ReverseTermsIndex> reverseIndexInstances = new HashMap<>();
  private final Map<String,DirectMonotonicReader.Meta> directAddressesMeta = new HashMap<>();

  //是否正在merge
  private final boolean merging;
}

/** metadata entry for a numeric docvalues field */
  static class NumericEntry {
    private NumericEntry() {}
    /** offset to the bitset representing docsWithField, or -1 if no documents have missing values */
    long missingOffset;

    /** offset to the actual numeric values */
    //field的在数据文件中的起始地址
    public long offset;

    /** end offset to the actual numeric values */
    //field的在数据文件中的结尾地址
    public long endOffset;

    /** bits per value used to pack the numeric values */
    public int bitsPerValue;

    //format类型
    int format;
    /** count of values written */
    public long count;
    /** monotonic meta */
    public DirectMonotonicReader.Meta monotonicMeta;

    //最小的value
    long minValue;

    //Compressed by computing the GCD
    long gcd;

    //Compressed by giving IDs to unique values.
    long table[];
    /** for sparse compression */
    long numDocsWithValue;
    NumericEntry nonMissingValues;
    NumberType numberType;
  }

  /** metadata entry for a binary docvalues field */
  static class BinaryEntry {
    private BinaryEntry() {}
    /** offset to the bitset representing docsWithField, or -1 if no documents have missing values */
    long missingOffset;
    /** offset to the actual binary values */
    //field的在数据文件中的起始地址
    long offset;
    int format;
    /** count of values written */
    public long count;

    //最短字符串的长度
    int minLength;

    //最长字符串的长度
    int maxLength;
    /** offset to the addressing data that maps a value to its slice of the byte[] */
    public long addressesOffset, addressesEndOffset;
    /** meta data for addresses */
    public DirectMonotonicReader.Meta addressesMeta;
    /** offset to the reverse index */
    public long reverseIndexOffset;
    /** packed ints version used to encode addressing information */
    public int packedIntsVersion;
    /** packed ints blocksize */
    public int blockSize;
  }

参考资料

lucene source code

lucene document

lucene字典实现原理——FST

继续阅读 »

前言

很多使用Elasticsearch的同学会关心数据存储在ES中的存储容量,会有这样的疑问:xxTB的数据入到ES会使用多少存储空间。这个问题其实很难直接回答的,只有数据写入ES后,才能观察到实际的存储空间。比如同样是1TB的数据,写入ES的存储空间可能差距会非常大,可能小到只有300~400GB,也可能多到6-7TB,为什么会造成这么大的差距呢?究其原因,我们来探究下Elasticsearch中的数据是如何存储。文章中我以Elasticsearch 2.3版本为示例,对应的lucene版本是5.5,Elasticsearch现在已经来到了6.5版本,数字类型、列存等存储结构有些变化,但基本的概念变化不多,文章中的内容依然适用。

Elasticsearch索引结构

Elasticsearch对外提供的是index的概念,可以类比为DB,用户查询是在index上完成的,每个index由若干个shard组成,以此来达到分布式可扩展的能力。比如下图是一个由10个shard组成的index。

elasticsearch_store_arc.png

shard是Elasticsearch数据存储的最小单位,index的存储容量为所有shard的存储容量之和。Elasticsearch集群的存储容量则为所有index存储容量之和。

一个shard就对应了一个lucene的library。对于一个shard,Elasticsearch增加了translog的功能,类似于HBase WAL,是数据写入过程中的中间数据,其余的数据都在lucene库中管理的。

所以Elasticsearch索引使用的存储内容主要取决于lucene中的数据存储。

lucene数据存储

下面我们主要看下lucene的文件内容,在了解lucene文件内容前,大家先了解些lucene的基本概念。

lucene基本概念

  • segment : lucene内部的数据是由一个个segment组成的,写入lucene的数据并不直接落盘,而是先写在内存中,经过了refresh间隔,lucene才将该时间段写入的全部数据refresh成一个segment,segment多了之后会进行merge成更大的segment。lucene查询时会遍历每个segment完成。由于lucene* 写入的数据是在内存中完成,所以写入效率非常高。但是也存在丢失数据的风险,所以Elasticsearch基于此现象实现了translog,只有在segment数据落盘后,Elasticsearch才会删除对应的translog。
  • doc : doc表示lucene中的一条记录
  • field :field表示记录中的字段概念,一个doc由若干个field组成。
  • term :term是lucene中索引的最小单位,某个field对应的内容如果是全文检索类型,会将内容进行分词,分词的结果就是由term组成的。如果是不分词的字段,那么该字段的内容就是一个term。
  • 倒排索引(inverted index): lucene索引的通用叫法,即实现了term到doc list的映射。
  • 正排数据:搜索引擎的通用叫法,即原始数据,可以理解为一个doc list。
  • docvalues :Elasticsearch中的列式存储的名称,Elasticsearch除了存储原始存储、倒排索引,还存储了一份docvalues,用作分析和排序。

lucene文件内容

lucene包的文件是由很多segment文件组成的,segments_xxx文件记录了lucene包下面的segment文件数量。每个segment会包含如下的文件。

Name Extension Brief Description
Segment Info .si segment的元数据文件
Compound File .cfs, .cfe 一个segment包含了如下表的各个文件,为减少打开文件的数量,在segment小的时候,segment的所有文件内容都保存在cfs文件中,cfe文件保存了lucene各文件在cfs文件的位置信息
Fields .fnm 保存了fields的相关信息
Field Index .fdx 正排存储文件的元数据信息
Field Data .fdt 存储了正排存储数据,写入的原文存储在这
Term Dictionary .tim 倒排索引的元数据信息
Term Index .tip 倒排索引文件,存储了所有的倒排索引数据
Frequencies .doc 保存了每个term的doc id列表和term在doc中的词频
Positions .pos Stores position information about where a term occurs in the index
全文索引的字段,会有该文件,保存了term在doc中的位置
Payloads .pay Stores additional per-position metadata information such as character offsets and user payloads
全文索引的字段,使用了一些像payloads的高级特性会有该文件,保存了term在doc中的一些高级特性
Norms .nvd, .nvm 文件保存索引字段加权数据
Per-Document Values .dvd, .dvm lucene的docvalues文件,即数据的列式存储,用作聚合和排序
Term Vector Data .tvx, .tvd, .tvf Stores offset into the document data file
保存索引字段的矢量信息,用在对term进行高亮,计算文本相关性中使用
Live Documents .liv 记录了segment中删除的doc

测试数据示例

下面我们以真实的数据作为示例,看看lucene中各类型数据的容量占比。

写100w数据,有一个uuid字段,写入的是长度为36位的uuid,字符串总为3600w字节,约为35M。

数据使用一个shard,不带副本,使用默认的压缩算法,写入完成后merge成一个segment方便观察。

使用线上默认的配置,uuid存为不分词的字符串类型。创建如下索引:

PUT test_field
{
  "settings": {
    "index": {
      "number_of_shards": "1",
      "number_of_replicas": "0",
      "refresh_interval": "30s"
    }
  },
  "mappings": {
    "type": {
      "_all": {
        "enabled": false
      }, 
      "properties": {
        "uuid": {
          "type": "string",
          "index": "not_analyzed"
        }
      }
    }
  }
}

首先写入100w不同的uuid,使用磁盘容量细节如下:


health status index      pri rep docs.count docs.deleted store.size pri.store.size 
green  open   test_field   1   0    1000000            0    122.7mb        122.7mb 

-rw-r--r--  1 weizijun  staff    41M Aug 19 21:23 _8.fdt
-rw-r--r--  1 weizijun  staff    17K Aug 19 21:23 _8.fdx
-rw-r--r--  1 weizijun  staff   688B Aug 19 21:23 _8.fnm
-rw-r--r--  1 weizijun  staff   494B Aug 19 21:23 _8.si
-rw-r--r--  1 weizijun  staff   265K Aug 19 21:23 _8_Lucene50_0.doc
-rw-r--r--  1 weizijun  staff    44M Aug 19 21:23 _8_Lucene50_0.tim
-rw-r--r--  1 weizijun  staff   340K Aug 19 21:23 _8_Lucene50_0.tip
-rw-r--r--  1 weizijun  staff    37M Aug 19 21:23 _8_Lucene54_0.dvd
-rw-r--r--  1 weizijun  staff   254B Aug 19 21:23 _8_Lucene54_0.dvm
-rw-r--r--  1 weizijun  staff   195B Aug 19 21:23 segments_2
-rw-r--r--  1 weizijun  staff     0B Aug 19 21:20 write.lock

可以看到正排数据、倒排索引数据,列存数据容量占比几乎相同,正排数据和倒排数据还会存储Elasticsearch的唯一id字段,所以容量会比列存多一些。

35M的uuid存入Elasticsearch后,数据膨胀了3倍,达到了122.7mb。Elasticsearch竟然这么消耗资源,不要着急下结论,接下来看另一个测试结果。

我们写入100w一样的uuid,然后看看Elasticsearch使用的容量。

health status index      pri rep docs.count docs.deleted store.size pri.store.size 
green  open   test_field   1   0    1000000            0     13.2mb         13.2mb 

-rw-r--r--  1 weizijun  staff   5.5M Aug 19 21:29 _6.fdt
-rw-r--r--  1 weizijun  staff    15K Aug 19 21:29 _6.fdx
-rw-r--r--  1 weizijun  staff   688B Aug 19 21:29 _6.fnm
-rw-r--r--  1 weizijun  staff   494B Aug 19 21:29 _6.si
-rw-r--r--  1 weizijun  staff   309K Aug 19 21:29 _6_Lucene50_0.doc
-rw-r--r--  1 weizijun  staff   7.0M Aug 19 21:29 _6_Lucene50_0.tim
-rw-r--r--  1 weizijun  staff   195K Aug 19 21:29 _6_Lucene50_0.tip
-rw-r--r--  1 weizijun  staff   244K Aug 19 21:29 _6_Lucene54_0.dvd
-rw-r--r--  1 weizijun  staff   252B Aug 19 21:29 _6_Lucene54_0.dvm
-rw-r--r--  1 weizijun  staff   195B Aug 19 21:29 segments_2
-rw-r--r--  1 weizijun  staff     0B Aug 19 21:26 write.lock

这回35M的数据Elasticsearch容量只有13.2mb,其中还有主要的占比还是Elasticsearch的唯一id,100w的uuid几乎不占存储容积。

所以在Elasticsearch中建立索引的字段如果基数越大(count distinct),越占用磁盘空间。

我们再看看存100w个不一样的整型会是如何。

health status index      pri rep docs.count docs.deleted store.size pri.store.size 
green  open   test_field   1   0    1000000            0     13.6mb         13.6mb 

-rw-r--r--  1 weizijun  staff   6.1M Aug 28 10:19 _42.fdt
-rw-r--r--  1 weizijun  staff    22K Aug 28 10:19 _42.fdx
-rw-r--r--  1 weizijun  staff   688B Aug 28 10:19 _42.fnm
-rw-r--r--  1 weizijun  staff   503B Aug 28 10:19 _42.si
-rw-r--r--  1 weizijun  staff   2.8M Aug 28 10:19 _42_Lucene50_0.doc
-rw-r--r--  1 weizijun  staff   2.2M Aug 28 10:19 _42_Lucene50_0.tim
-rw-r--r--  1 weizijun  staff    83K Aug 28 10:19 _42_Lucene50_0.tip
-rw-r--r--  1 weizijun  staff   2.5M Aug 28 10:19 _42_Lucene54_0.dvd
-rw-r--r--  1 weizijun  staff   228B Aug 28 10:19 _42_Lucene54_0.dvm
-rw-r--r--  1 weizijun  staff   196B Aug 28 10:19 segments_2
-rw-r--r--  1 weizijun  staff     0B Aug 28 10:16 write.lock

从结果可以看到,100w整型数据,Elasticsearch的存储开销为13.6mb。如果以int型计算100w数据的长度的话,为400w字节,大概是3.8mb数据。忽略Elasticsearch唯一id字段的影响,Elasticsearch实际存储容量跟整型数据长度差不多。

我们再看一下开启最佳压缩参数对存储空间的影响:

health status index      pri rep docs.count docs.deleted store.size pri.store.size 
green  open   test_field   1   0    1000000            0    107.2mb        107.2mb 

-rw-r--r--  1 weizijun  staff    25M Aug 20 12:30 _5.fdt
-rw-r--r--  1 weizijun  staff   6.0K Aug 20 12:30 _5.fdx
-rw-r--r--  1 weizijun  staff   688B Aug 20 12:31 _5.fnm
-rw-r--r--  1 weizijun  staff   500B Aug 20 12:31 _5.si
-rw-r--r--  1 weizijun  staff   265K Aug 20 12:31 _5_Lucene50_0.doc
-rw-r--r--  1 weizijun  staff    44M Aug 20 12:31 _5_Lucene50_0.tim
-rw-r--r--  1 weizijun  staff   322K Aug 20 12:31 _5_Lucene50_0.tip
-rw-r--r--  1 weizijun  staff    37M Aug 20 12:31 _5_Lucene54_0.dvd
-rw-r--r--  1 weizijun  staff   254B Aug 20 12:31 _5_Lucene54_0.dvm
-rw-r--r--  1 weizijun  staff   224B Aug 20 12:31 segments_4
-rw-r--r--  1 weizijun  staff     0B Aug 20 12:00 write.lock

结果中可以发现,只有正排数据会启动压缩,压缩能力确实强劲,不考虑唯一id字段,存储容量大概压缩到接近50%。

我们还做了一些实验,Elasticsearch默认是开启_all参数的,_all可以让用户传入的整体json数据作为全文检索的字段,可以更方便的检索,但在现实场景中已经使用的不多,相反会增加很多存储容量的开销,可以看下开启_all的磁盘空间使用情况:


health status index      pri rep docs.count docs.deleted store.size pri.store.size 
green  open   test_field   1   0    1000000            0    162.4mb        162.4mb 

-rw-r--r--  1 weizijun  staff    41M Aug 18 22:59 _20.fdt
-rw-r--r--  1 weizijun  staff    18K Aug 18 22:59 _20.fdx
-rw-r--r--  1 weizijun  staff   777B Aug 18 22:59 _20.fnm
-rw-r--r--  1 weizijun  staff    59B Aug 18 22:59 _20.nvd
-rw-r--r--  1 weizijun  staff    78B Aug 18 22:59 _20.nvm
-rw-r--r--  1 weizijun  staff   539B Aug 18 22:59 _20.si
-rw-r--r--  1 weizijun  staff   7.2M Aug 18 22:59 _20_Lucene50_0.doc
-rw-r--r--  1 weizijun  staff   4.2M Aug 18 22:59 _20_Lucene50_0.pos
-rw-r--r--  1 weizijun  staff    73M Aug 18 22:59 _20_Lucene50_0.tim
-rw-r--r--  1 weizijun  staff   832K Aug 18 22:59 _20_Lucene50_0.tip
-rw-r--r--  1 weizijun  staff    37M Aug 18 22:59 _20_Lucene54_0.dvd
-rw-r--r--  1 weizijun  staff   254B Aug 18 22:59 _20_Lucene54_0.dvm
-rw-r--r--  1 weizijun  staff   196B Aug 18 22:59 segments_2
-rw-r--r--  1 weizijun  staff     0B Aug 18 22:53 write.lock

开启_all比不开启多了40mb的存储空间,多的数据都在倒排索引上,大约会增加30%多的存储开销。所以线上都直接禁用。

然后我还做了其他几个尝试,为了验证存储容量是否和数据量成正比,写入1000w数据的uuid,发现存储容量基本为100w数据的10倍。我还验证了数据长度是否和数据量成正比,发现把uuid增长2倍、4倍,存储容量也响应的增加了2倍和4倍。在此就不一一列出数据了。

lucene各文件具体内容和实现

lucene数据元信息文件

文件名为:segments_xxx

该文件为lucene数据文件的元信息文件,记录所有segment的元数据信息。

该文件主要记录了目前有多少segment,每个segment有一些基本信息,更新这些信息定位到每个segment的元信息文件。

lucene元信息文件还支持记录userData,Elasticsearch可以在此记录translog的一些相关信息。

文件示例

elasticsearch_store_segments.png

具体实现类

public final class SegmentInfos implements Cloneable, Iterable<SegmentCommitInfo> {
  // generation是segment的版本的概念,从文件名中提取出来,实例中为:2t/101
  private long generation;     // generation of the "segments_N" for the next commit

  private long lastGeneration; // generation of the "segments_N" file we last successfully read
                               // or wrote; this is normally the same as generation except if
                               // there was an IOException that had interrupted a commit

  /** Id for this commit; only written starting with Lucene 5.0 */
  private byte[] id;

  /** Which Lucene version wrote this commit, or null if this commit is pre-5.3. */
  private Version luceneVersion;

  /** Counts how often the index has been changed.  */
  public long version;

  /** Used to name new segments. */
  // TODO: should this be a long ...?
  public int counter;

  /** Version of the oldest segment in the index, or null if there are no segments. */
  private Version minSegmentLuceneVersion;

  private List<SegmentCommitInfo> segments = new ArrayList<>();

  /** Opaque Map&lt;String, String&gt; that user can specify during IndexWriter.commit */
  public Map<String,String> userData = Collections.emptyMap();
}

/** Embeds a [read-only] SegmentInfo and adds per-commit
 *  fields.
 *
 *  @lucene.experimental */
public class SegmentCommitInfo {

  /** The {@link SegmentInfo} that we wrap. */
  public final SegmentInfo info;

  // How many deleted docs in the segment:
  private int delCount;

  // Generation number of the live docs file (-1 if there
  // are no deletes yet):
  private long delGen;

  // Normally 1+delGen, unless an exception was hit on last
  // attempt to write:
  private long nextWriteDelGen;

  // Generation number of the FieldInfos (-1 if there are no updates)
  private long fieldInfosGen;

  // Normally 1+fieldInfosGen, unless an exception was hit on last attempt to
  // write
  private long nextWriteFieldInfosGen; //fieldInfosGen == -1 ? 1 : fieldInfosGen + 1;

  // Generation number of the DocValues (-1 if there are no updates)
  private long docValuesGen;

  // Normally 1+dvGen, unless an exception was hit on last attempt to
  // write
  private long nextWriteDocValuesGen; //docValuesGen == -1 ? 1 : docValuesGen + 1;

  // TODO should we add .files() to FieldInfosFormat, like we have on
  // LiveDocsFormat?
  // track the fieldInfos update files
  private final Set<String> fieldInfosFiles = new HashSet<>();

  // Track the per-field DocValues update files
  private final Map<Integer,Set<String>> dvUpdatesFiles = new HashMap<>();

  // Track the per-generation updates files
  @Deprecated
  private final Map<Long,Set<String>> genUpdatesFiles = new HashMap<>();

  private volatile long sizeInBytes = -1;
}

segment的元信息文件

文件后缀:.si

每个segment都有一个.si文件,记录了该segment的元信息。

segment元信息文件中记录了segment的文档数量,segment对应的文件列表等信息。

文件示例

elasticsearch_store_si.png

具体实现类

/**
 * Information about a segment such as its name, directory, and files related
 * to the segment.
 *
 * @lucene.experimental
 */
public final class SegmentInfo {

  // _bl
  public final String name;

  /** Where this segment resides. */
  public final Directory dir;

  /** Id that uniquely identifies this segment. */
  private final byte[] id;

  private Codec codec;

  // Tracks the Lucene version this segment was created with, since 3.1. Null
  // indicates an older than 3.0 index, and it's used to detect a too old index.
  // The format expected is "x.y" - "2.x" for pre-3.0 indexes (or null), and
  // specific versions afterwards ("3.0.0", "3.1.0" etc.).
  // see o.a.l.util.Version.
  private Version version;

  private int maxDoc;         // number of docs in seg

  private boolean isCompoundFile;

  private Map<String,String> diagnostics;

  private Set<String> setFiles;

  private final Map<String,String> attributes;
}

fields信息文件

文件后缀:.fnm

该文件存储了fields的基本信息。

fields信息中包括field的数量,field的类型,以及IndexOpetions,包括是否存储、是否索引,是否分词,是否需要列存等等。

文件示例

elasticsearch_store_fnm.png

具体实现类

/**
 *  Access to the Field Info file that describes document fields and whether or
 *  not they are indexed. Each segment has a separate Field Info file. Objects
 *  of this class are thread-safe for multiple readers, but only one thread can
 *  be adding documents at a time, with no other reader or writer threads
 *  accessing this object.
 **/
public final class FieldInfo {
  /** Field's name */
  public final String name;

  /** Internal field number */
  //field在内部的编号
  public final int number;

  //field docvalues的类型
  private DocValuesType docValuesType = DocValuesType.NONE;

  // True if any document indexed term vectors
  private boolean storeTermVector;

  private boolean omitNorms; // omit norms associated with indexed fields 

  //index的配置项
  private IndexOptions indexOptions = IndexOptions.NONE;

  private boolean storePayloads; // whether this field stores payloads together with term positions 

  private final Map<String,String> attributes;

  // docvalues的generation
  private long dvGen;
}

数据存储文件

文件后缀:.fdx, .fdt

索引文件为.fdx,数据文件为.fdt,数据存储文件功能为根据自动的文档id,得到文档的内容,搜索引擎的术语习惯称之为正排数据,即doc_id -> content,es的_source数据就存在这

索引文件记录了快速定位文档数据的索引信息,数据文件记录了所有文档id的具体内容。

文件示例

elasticsearch_store_fdt.png

具体实现类

/**
 * Random-access reader for {@link CompressingStoredFieldsIndexWriter}.
 * @lucene.internal
 */
public final class CompressingStoredFieldsIndexReader implements Cloneable, Accountable {
  private static final long BASE_RAM_BYTES_USED = RamUsageEstimator.shallowSizeOfInstance(CompressingStoredFieldsIndexReader.class);

  final int maxDoc;

  //docid索引,快速定位某个docid的数组坐标
  final int[] docBases;

  //快速定位某个docid所在的文件offset的startPointer
  final long[] startPointers;

  //平均一个chunk的文档数
  final int[] avgChunkDocs;

  //平均一个chunk的size
  final long[] avgChunkSizes;

  final PackedInts.Reader[] docBasesDeltas; // delta from the avg

  final PackedInts.Reader[] startPointersDeltas; // delta from the avg
}

/**
 * {@link StoredFieldsReader} impl for {@link CompressingStoredFieldsFormat}.
 * @lucene.experimental
 */
public final class CompressingStoredFieldsReader extends StoredFieldsReader {

  //从fdt正排索引文件中获得
  private final int version;

  // field的基本信息
  private final FieldInfos fieldInfos;

  //fdt正排索引文件reader
  private final CompressingStoredFieldsIndexReader indexReader;

  //从fdt正排索引文件中获得,用于指向fdx数据文件的末端,指向numChunks地址4
  private final long maxPointer;

  //fdx正排数据文件句柄
  private final IndexInput fieldsStream;

  //块大小
  private final int chunkSize;

  private final int packedIntsVersion;

  //压缩类型
  private final CompressionMode compressionMode;

  //解压缩处理对象
  private final Decompressor decompressor;

  //文档数量,从segment元数据中获得
  private final int numDocs;

  //是否正在merge,默认为false
  private final boolean merging;

  //初始化时new了一个BlockState,BlockState记录下当前正排文件读取的状态信息
  private final BlockState state;
  //chunk的数量
  private final long numChunks; // number of compressed blocks written

  //dirty chunk的数量
  private final long numDirtyChunks; // number of incomplete compressed blocks written

  //是否close,默认为false
  private boolean closed;
}

倒排索引文件

索引后缀:.tip,.tim

倒排索引也包含索引文件和数据文件,.tip为索引文件,.tim为数据文件,索引文件包含了每个字段的索引元信息,数据文件有具体的索引内容。

5.5.0版本的倒排索引实现为FST tree,FST tree的最大优势就是内存空间占用非常低 ,具体可以参看下这篇文章:http://www.cnblogs.com/bonelee/p/6226185.html

http://examples.mikemccandless.com/fst.py?terms=&cmd=Build+it 为FST图实例,可以根据输入的数据构造出FST图

输入到 FST 中的数据为:
String inputValues[] = {"mop","moth","pop","star","stop","top"};
long outputValues[] = {0,1,2,3,4,5};

生成的 FST 图为:

elasticsearch_store_tip1.png

elasticsearch_store_tip2.png

文件示例

elasticsearch_store_tip3.png

具体实现类

public final class BlockTreeTermsReader extends FieldsProducer {
  // Open input to the main terms dict file (_X.tib)
  final IndexInput termsIn;
  // Reads the terms dict entries, to gather state to
  // produce DocsEnum on demand
  final PostingsReaderBase postingsReader;
  private final TreeMap<String,FieldReader> fields = new TreeMap<>();

  /** File offset where the directory starts in the terms file. */
  /索引数据文件tim的数据的尾部的元数据的地址
  private long dirOffset;
  /** File offset where the directory starts in the index file. */

  //索引文件tip的数据的尾部的元数据的地址
  private long indexDirOffset;

  //semgent的名称
  final String segment;

  //版本号
  final int version;

  //5.3.x index, we record up front if we may have written any auto-prefix terms,示例中记录的是false
  final boolean anyAutoPrefixTerms;
}

/**
 * BlockTree's implementation of {@link Terms}.
 * @lucene.internal
 */
public final class FieldReader extends Terms implements Accountable {

  //term的数量
  final long numTerms;

  //field信息
  final FieldInfo fieldInfo;

  final long sumTotalTermFreq;

  //总的文档频率
  final long sumDocFreq;

  //文档数量
  final int docCount;

  //字段在索引文件tip中的起始位置
  final long indexStartFP;

  final long rootBlockFP;

  final BytesRef rootCode;

  final BytesRef minTerm;

  final BytesRef maxTerm;

  //longs:metadata buffer, holding monotonic values
  final int longsSize;

  final BlockTreeTermsReader parent;

  final FST<BytesRef> index;
}

倒排链文件

文件后缀:.doc, .pos, .pay

.doc保存了每个term的doc id列表和term在doc中的词频

全文索引的字段,会有.pos文件,保存了term在doc中的位置

全文索引的字段,使用了一些像payloads的高级特性才会有.pay文件,保存了term在doc中的一些高级特性

文件示例

elasticsearch_store_doc.png

具体实现类

/**
 * Concrete class that reads docId(maybe frq,pos,offset,payloads) list
 * with postings format.
 *
 * @lucene.experimental
 */
public final class Lucene50PostingsReader extends PostingsReaderBase {
  private static final long BASE_RAM_BYTES_USED = RamUsageEstimator.shallowSizeOfInstance(Lucene50PostingsReader.class);
  private final IndexInput docIn;
  private final IndexInput posIn;
  private final IndexInput payIn;
  final ForUtil forUtil;
  private int version;

  //不分词的字段使用的是该对象,基于skiplist实现了倒排链
  final class BlockDocsEnum extends PostingsEnum {
  }

  //全文检索字段使用的是该对象
  final class BlockPostingsEnum extends PostingsEnum {
  }

  //包含高级特性的字段使用的是该对象
  final class EverythingEnum extends PostingsEnum {
  }
}

列存文件(docvalues)

文件后缀:.dvm, .dvd

索引文件为.dvm,数据文件为.dvd。

lucene实现的docvalues有如下类型:

  • 1、NONE 不开启docvalue时的状态
  • 2、NUMERIC 单个数值类型的docvalue主要包括(int,long,float,double)
  • 3、BINARY 二进制类型值对应不同的codes最大值可能超过32766字节,
  • 4、SORTED 有序增量字节存储,仅仅存储不同部分的值和偏移量指针,值必须小于等于32766字节
  • 5、SORTED_NUMERIC 存储数值类型的有序数组列表
  • 6、SORTED_SET 可以存储多值域的docvalue值,但返回时,仅仅只能返回多值域的第一个docvalue
  • 7、对应not_anaylized的string字段,使用的是SORTED_SET类型,number的类型是SORTED_NUMERIC类型

其中SORTED_SET 的 SORTED_SINGLE_VALUED类型包括了两类数据 : binary + numeric, binary是按ord排序的term的列表,numeric是doc到ord的映射。

文件示例

elasticsearch_store_dvd.png

具体实现类

/** reader for {@link Lucene54DocValuesFormat} */
final class Lucene54DocValuesProducer extends DocValuesProducer implements Closeable {
  //number类型的field的列存列表
  private final Map<String,NumericEntry> numerics = new HashMap<>();

  //字符串类型的field的列存列表
  private final Map<String,BinaryEntry> binaries = new HashMap<>();

  //有序字符串类型的field的列存列表
  private final Map<String,SortedSetEntry> sortedSets = new HashMap<>();

  //有序number类型的field的列存列表
  private final Map<String,SortedSetEntry> sortedNumerics = new HashMap<>();

  //字符串类型的field的ords列表
  private final Map<String,NumericEntry> ords = new HashMap<>();

  //docId -> address -> ord 中field的ords列表
  private final Map<String,NumericEntry> ordIndexes = new HashMap<>();

  //field的数量
  private final int numFields;

  //内存使用量
  private final AtomicLong ramBytesUsed;

  //数据源的文件句柄
  private final IndexInput data;

  //文档数
  private final int maxDoc;
  // memory-resident structures
  private final Map<String,MonotonicBlockPackedReader> addressInstances = new HashMap<>();
  private final Map<String,ReverseTermsIndex> reverseIndexInstances = new HashMap<>();
  private final Map<String,DirectMonotonicReader.Meta> directAddressesMeta = new HashMap<>();

  //是否正在merge
  private final boolean merging;
}

/** metadata entry for a numeric docvalues field */
  static class NumericEntry {
    private NumericEntry() {}
    /** offset to the bitset representing docsWithField, or -1 if no documents have missing values */
    long missingOffset;

    /** offset to the actual numeric values */
    //field的在数据文件中的起始地址
    public long offset;

    /** end offset to the actual numeric values */
    //field的在数据文件中的结尾地址
    public long endOffset;

    /** bits per value used to pack the numeric values */
    public int bitsPerValue;

    //format类型
    int format;
    /** count of values written */
    public long count;
    /** monotonic meta */
    public DirectMonotonicReader.Meta monotonicMeta;

    //最小的value
    long minValue;

    //Compressed by computing the GCD
    long gcd;

    //Compressed by giving IDs to unique values.
    long table[];
    /** for sparse compression */
    long numDocsWithValue;
    NumericEntry nonMissingValues;
    NumberType numberType;
  }

  /** metadata entry for a binary docvalues field */
  static class BinaryEntry {
    private BinaryEntry() {}
    /** offset to the bitset representing docsWithField, or -1 if no documents have missing values */
    long missingOffset;
    /** offset to the actual binary values */
    //field的在数据文件中的起始地址
    long offset;
    int format;
    /** count of values written */
    public long count;

    //最短字符串的长度
    int minLength;

    //最长字符串的长度
    int maxLength;
    /** offset to the addressing data that maps a value to its slice of the byte[] */
    public long addressesOffset, addressesEndOffset;
    /** meta data for addresses */
    public DirectMonotonicReader.Meta addressesMeta;
    /** offset to the reverse index */
    public long reverseIndexOffset;
    /** packed ints version used to encode addressing information */
    public int packedIntsVersion;
    /** packed ints blocksize */
    public int blockSize;
  }

参考资料

lucene source code

lucene document

lucene字典实现原理——FST

收起阅读 »

社区日报 第471期 (2018-12-07)

1、使用Elasticsearch作为主数据存储实践
http://t.cn/EyS6wcL
2、Elasticsearch 6.x启动过程
http://t.cn/EyJgQ7U
3、Elasticsearch你知道多少?
http://t.cn/EyS6I4P

编辑:铭毅天下
归档: https://elasticsearch.cn/article/6177
订阅:https://tinyletter.com/elastic-daily
继续阅读 »
1、使用Elasticsearch作为主数据存储实践
http://t.cn/EyS6wcL
2、Elasticsearch 6.x启动过程
http://t.cn/EyJgQ7U
3、Elasticsearch你知道多少?
http://t.cn/EyS6I4P

编辑:铭毅天下
归档: https://elasticsearch.cn/article/6177
订阅:https://tinyletter.com/elastic-daily 收起阅读 »

Day 6 - Logstash Pipeline-to-Pipeline 尝鲜

Logstash 在 6.0 推出了 multiple pipeline 的解决方案,即在一个 logstash 实例中可以同时进行多个独立数据流程的处理工作,如下图所示。

而在这之前用户只能通过在单机运行多个 logstash 实例或者在配置文件中增加大量 if-else 条件判断语句来解决。要使用 multiple pipeline 也很简单,只需要将不同的 pipeline 在 config/pipeline.yml中定义好即可,如下所示:

- pipeline.id: apache
  pipeline.batch.size: 125
  queue.type: persisted
  path.config: "/path/to/config/apache.cfg"
- pipeline.id: nginx
  path.config: "/path/to/config/nginx.cfg"

其中 apachenginx作为独立的 pipeline 执行,而且配置也可以独立设置,互不干扰。pipeline.yml的引入极大地简化了 logstash 的配置管理工作,使得新手也可以很快完成复杂的 ETL 配置。

在 6.3 版本中,Logstash 又增加了 Pipeline-to-Pipeline的管道机制(beta),即管道和管道之间可以连接在一起组成一个完成的数据处理流。熟悉 linux 的管道命令 |的同学应该可以很快明白这种模式的好处。这无疑使得 Logstash 的配置会更加灵活,今天我们就来了解下这种灵活自由的配置方式。

1. 上手

废话少说,快速上手。修改 config/pipeline.yml文件如下:

 - pipeline.id: upstream
   config.string: input { stdin {} } output { pipeline { send_to => [test_output] } }
 - pipeline.id: downstream
   config.string: input { pipeline { address => test_output } } output{ stdout{}}

然后运行 logstash,其中 -r 表示配置文件有改动时自动重新加载,方便我们调试。

bin/logstash -r

在终端随意输入字符(比如aaa)后回车,会看到屏幕输出了类似下面的内容,代表运行成功了。

{
    "@timestamp" => 2018-12-06T14:43:50.310Z,
    "@version" => "1",
    "message" => "aaa",
    "host" => "rockybean-MacBook-Pro.local"
}

我们再回头看下这个配置,upstreamoutput 使用了名为 pipeline 的 plugin,然后 send_to的输出对象test_output是在 downstreaminput pipeline plugin 中定义的。通过这个唯一的address(虚拟地址)就能够把不同的 pipeline 连接在一起组成一个更长的pipeline来处理数据。类似下图所示:

当数据由 upstream传递给 downstream时会进行一个复制操作,这也意味着在这两个 pipeline 中的数据是完全独立的,互不影响。有一点要注意的是:数据的复制会增加额外的性能开销,比如会加大 JVM Heap 的使用。

2. 使用场景

使用方法是不是很简单,接下来我们来看下官方为我们开的几个脑洞。

2.1 Distributor Pattern 分发者模式

该模式执行效果类似下图所示:

在一个 pipeline 处理输入,然后根据不同的数据类型再分发到对应的 Pipeline 去处理。这种模式的好处在于统一输入端口,隔离不同类型的处理配置文件,减少由于配置文件混合在一起带来的维护成本。大家可以想一想如果不用这种Pipeline-to-Pipeline的方式,我们如果轻松做到一个端口处理多个来源的数据呢?

这种模式的参考配置如下所示:

# config/pipelines.yml
- pipeline.id: beats-server
  config.string: |
    input { beats { port => 5044 } }
    output {
        if [type] == apache {
          pipeline { send_to => weblogs }
        } else if [type] == system {
          pipeline { send_to => syslog }
        } else {
          pipeline { send_to => fallback }
        }
    }
- pipeline.id: weblog-processing
  config.string: |
    input { pipeline { address => weblogs } }
    filter {
       # Weblog filter statements here...
    }
    output {
      elasticsearch { hosts => [es_cluster_a_host] }
    }
- pipeline.id: syslog-processing
  config.string: |
    input { pipeline { address => syslog } }
    filter {
       # Syslog filter statements here...
    }
    output {
      elasticsearch { hosts => [es_cluster_b_host] }
    }
- pipeline.id: fallback-processing
    config.string: |
    input { pipeline { address => fallback } }
    output { elasticsearch { hosts => [es_cluster_b_host] } }

2.2 Output Isolator Pattern 输出隔离模式

虽然 Logstash 的一个 pipeline 可以配置多个 output,但是这多个 output 会相依为命,一旦某一个 output 出问题,会导致另一个 output 也无法接收新数据。而通过这种模式可以完美解决这个问题。其运行方式如下图所示:

通过输出到两个独立的 pipeline,解除相互之间的影响,比如 http service 出问题的时候,es 依然可以正常接收数据,而且两个 pipeline 可以配置独立的队列来保障数据的完备性,其配置如下所示:

# config/pipelines.yml
- pipeline.id: intake
  queue.type: persisted
  config.string: |
    input { beats { port => 5044 } }
    output { pipeline { send_to => [es, http] } }
- pipeline.id: buffered-es
  queue.type: persisted
  config.string: |
    input { pipeline { address => es } }
    output { elasticsearch { } }
- pipeline.id: buffered-http
  queue.type: persisted
  config.string: |
    input { pipeline { address => http } }
    output { http { } }

2.3 Forked Path Pattern 克隆路径模式

这个模式类似 Output Isolator Pattern,只是在不同的 output pipeline 中可以配置不同的 filter 来完成各自输出的数据处理需求,这里就不展开讲了,可以参考如下的配置,其中不同 output pipeline 的 filter 是不同的,比如 partner 这个 pipeline 去掉了一些敏感数据:

# config/pipelines.yml
- pipeline.id: intake
  queue.type: persisted
  config.string: |
    input { beats { port => 5044 } }
    output { pipeline { send_to => ["internal-es", "partner-s3"] } }
- pipeline.id: buffered-es
  queue.type: persisted
  config.string: |
    input { pipeline { address => "internal-es" } }
    # Index the full event
    output { elasticsearch { } }
- pipeline.id: partner
  queue.type: persisted
  config.string: |
    input { pipeline { address => "partner-s3" } }
    filter {
      # Remove the sensitive data
      mutate { remove_field => 'sensitive-data' }
    }
    output { s3 { } } # Output to partner's bucket

2.4 Collector Pattern 收集者模式

从名字可以看出,该模式是将所有 Pipeline 汇集于一处的处理模式,如下图所示:

其配置参考如下:

# config/pipelines.yml
- pipeline.id: beats
  config.string: |
    input { beats { port => 5044 } }
    output { pipeline { send_to => [commonOut] } }
- pipeline.id: kafka
  config.string: |
    input { kafka { ... } }
    output { pipeline { send_to => [commonOut] } }
- pipeline.id: partner
  # This common pipeline enforces the same logic whether data comes from Kafka or Beats
  config.string: |
    input { pipeline { address => commonOut } }
    filter {
      # Always remove sensitive data from all input sources
      mutate { remove_field => 'sensitive-data' }
    }
    output { elasticsearch { } }

3. 总结

本文简单给大家讲解了 Pipeline-to-Pipeline的使用方法及官方推荐的几种模式,希望可以给大家有所帮助。另外这个机制目前还处于 Beta 阶段,尝鲜需谨慎!

继续阅读 »

Logstash 在 6.0 推出了 multiple pipeline 的解决方案,即在一个 logstash 实例中可以同时进行多个独立数据流程的处理工作,如下图所示。

而在这之前用户只能通过在单机运行多个 logstash 实例或者在配置文件中增加大量 if-else 条件判断语句来解决。要使用 multiple pipeline 也很简单,只需要将不同的 pipeline 在 config/pipeline.yml中定义好即可,如下所示:

- pipeline.id: apache
  pipeline.batch.size: 125
  queue.type: persisted
  path.config: "/path/to/config/apache.cfg"
- pipeline.id: nginx
  path.config: "/path/to/config/nginx.cfg"

其中 apachenginx作为独立的 pipeline 执行,而且配置也可以独立设置,互不干扰。pipeline.yml的引入极大地简化了 logstash 的配置管理工作,使得新手也可以很快完成复杂的 ETL 配置。

在 6.3 版本中,Logstash 又增加了 Pipeline-to-Pipeline的管道机制(beta),即管道和管道之间可以连接在一起组成一个完成的数据处理流。熟悉 linux 的管道命令 |的同学应该可以很快明白这种模式的好处。这无疑使得 Logstash 的配置会更加灵活,今天我们就来了解下这种灵活自由的配置方式。

1. 上手

废话少说,快速上手。修改 config/pipeline.yml文件如下:

 - pipeline.id: upstream
   config.string: input { stdin {} } output { pipeline { send_to => [test_output] } }
 - pipeline.id: downstream
   config.string: input { pipeline { address => test_output } } output{ stdout{}}

然后运行 logstash,其中 -r 表示配置文件有改动时自动重新加载,方便我们调试。

bin/logstash -r

在终端随意输入字符(比如aaa)后回车,会看到屏幕输出了类似下面的内容,代表运行成功了。

{
    "@timestamp" => 2018-12-06T14:43:50.310Z,
    "@version" => "1",
    "message" => "aaa",
    "host" => "rockybean-MacBook-Pro.local"
}

我们再回头看下这个配置,upstreamoutput 使用了名为 pipeline 的 plugin,然后 send_to的输出对象test_output是在 downstreaminput pipeline plugin 中定义的。通过这个唯一的address(虚拟地址)就能够把不同的 pipeline 连接在一起组成一个更长的pipeline来处理数据。类似下图所示:

当数据由 upstream传递给 downstream时会进行一个复制操作,这也意味着在这两个 pipeline 中的数据是完全独立的,互不影响。有一点要注意的是:数据的复制会增加额外的性能开销,比如会加大 JVM Heap 的使用。

2. 使用场景

使用方法是不是很简单,接下来我们来看下官方为我们开的几个脑洞。

2.1 Distributor Pattern 分发者模式

该模式执行效果类似下图所示:

在一个 pipeline 处理输入,然后根据不同的数据类型再分发到对应的 Pipeline 去处理。这种模式的好处在于统一输入端口,隔离不同类型的处理配置文件,减少由于配置文件混合在一起带来的维护成本。大家可以想一想如果不用这种Pipeline-to-Pipeline的方式,我们如果轻松做到一个端口处理多个来源的数据呢?

这种模式的参考配置如下所示:

# config/pipelines.yml
- pipeline.id: beats-server
  config.string: |
    input { beats { port => 5044 } }
    output {
        if [type] == apache {
          pipeline { send_to => weblogs }
        } else if [type] == system {
          pipeline { send_to => syslog }
        } else {
          pipeline { send_to => fallback }
        }
    }
- pipeline.id: weblog-processing
  config.string: |
    input { pipeline { address => weblogs } }
    filter {
       # Weblog filter statements here...
    }
    output {
      elasticsearch { hosts => [es_cluster_a_host] }
    }
- pipeline.id: syslog-processing
  config.string: |
    input { pipeline { address => syslog } }
    filter {
       # Syslog filter statements here...
    }
    output {
      elasticsearch { hosts => [es_cluster_b_host] }
    }
- pipeline.id: fallback-processing
    config.string: |
    input { pipeline { address => fallback } }
    output { elasticsearch { hosts => [es_cluster_b_host] } }

2.2 Output Isolator Pattern 输出隔离模式

虽然 Logstash 的一个 pipeline 可以配置多个 output,但是这多个 output 会相依为命,一旦某一个 output 出问题,会导致另一个 output 也无法接收新数据。而通过这种模式可以完美解决这个问题。其运行方式如下图所示:

通过输出到两个独立的 pipeline,解除相互之间的影响,比如 http service 出问题的时候,es 依然可以正常接收数据,而且两个 pipeline 可以配置独立的队列来保障数据的完备性,其配置如下所示:

# config/pipelines.yml
- pipeline.id: intake
  queue.type: persisted
  config.string: |
    input { beats { port => 5044 } }
    output { pipeline { send_to => [es, http] } }
- pipeline.id: buffered-es
  queue.type: persisted
  config.string: |
    input { pipeline { address => es } }
    output { elasticsearch { } }
- pipeline.id: buffered-http
  queue.type: persisted
  config.string: |
    input { pipeline { address => http } }
    output { http { } }

2.3 Forked Path Pattern 克隆路径模式

这个模式类似 Output Isolator Pattern,只是在不同的 output pipeline 中可以配置不同的 filter 来完成各自输出的数据处理需求,这里就不展开讲了,可以参考如下的配置,其中不同 output pipeline 的 filter 是不同的,比如 partner 这个 pipeline 去掉了一些敏感数据:

# config/pipelines.yml
- pipeline.id: intake
  queue.type: persisted
  config.string: |
    input { beats { port => 5044 } }
    output { pipeline { send_to => ["internal-es", "partner-s3"] } }
- pipeline.id: buffered-es
  queue.type: persisted
  config.string: |
    input { pipeline { address => "internal-es" } }
    # Index the full event
    output { elasticsearch { } }
- pipeline.id: partner
  queue.type: persisted
  config.string: |
    input { pipeline { address => "partner-s3" } }
    filter {
      # Remove the sensitive data
      mutate { remove_field => 'sensitive-data' }
    }
    output { s3 { } } # Output to partner's bucket

2.4 Collector Pattern 收集者模式

从名字可以看出,该模式是将所有 Pipeline 汇集于一处的处理模式,如下图所示:

其配置参考如下:

# config/pipelines.yml
- pipeline.id: beats
  config.string: |
    input { beats { port => 5044 } }
    output { pipeline { send_to => [commonOut] } }
- pipeline.id: kafka
  config.string: |
    input { kafka { ... } }
    output { pipeline { send_to => [commonOut] } }
- pipeline.id: partner
  # This common pipeline enforces the same logic whether data comes from Kafka or Beats
  config.string: |
    input { pipeline { address => commonOut } }
    filter {
      # Always remove sensitive data from all input sources
      mutate { remove_field => 'sensitive-data' }
    }
    output { elasticsearch { } }

3. 总结

本文简单给大家讲解了 Pipeline-to-Pipeline的使用方法及官方推荐的几种模式,希望可以给大家有所帮助。另外这个机制目前还处于 Beta 阶段,尝鲜需谨慎!

收起阅读 »

社区日报 第470期 (2018-12-06)

1.图解 Elasticsearch2.2.0 原理
http://t.cn/Eybu8a3
2.ElasticSearch 恢复类型之对等恢复
http://t.cn/EyaCYFV
3.使用带注释的文本插件搜索事物
http://t.cn/EyaCntg

编辑:金桥
归档:https://elasticsearch.cn/article/6175
订阅:https://tinyletter.com/elastic-daily
继续阅读 »
1.图解 Elasticsearch2.2.0 原理
http://t.cn/Eybu8a3
2.ElasticSearch 恢复类型之对等恢复
http://t.cn/EyaCYFV
3.使用带注释的文本插件搜索事物
http://t.cn/EyaCntg

编辑:金桥
归档:https://elasticsearch.cn/article/6175
订阅:https://tinyletter.com/elastic-daily 收起阅读 »

es创建索引失败

#10001-log
- type: log
  enable: true
  paths:
    - /mkt/tomcat/8.5.32/10001/logs/mstore.log
  fields:
    env: uat-10001-log
  include_lines: ['ERROR']
#10001-catalina
- type: log
  enable: true
  paths:
    - /mkt/tomcat/8.5.32/10001/logs/catalina.out
  fields:
    env: uat-10001-catalina
  include_lines: ['ERROR']
#11001-log
- type: log
  enabled: true
  paths:
    - /mkt/tomcat/8.5.32/11001/logs/delivery.log
  include_lines: ['ERROR']
  fields:
    env: uat-11001-log
#11001-catalina.out
- type: log
  enable: true
  paths:
    - /mkt/tomcat/8.5.32/11001/logs/catalina.out
  fields:
    env: uat-11001-catalina
  include_lines: ['ERROR']
#12001-log
- type: log
  enable: true
  paths:
    - /mkt/tomcat/8.5.32/12001/logs/mstore.log
  fields:
    env: uat-12001-log
  include_lines: ['ERROR']
#12001-catalina
- type: log
  enable: true
  paths:
    - /mkt/tomcat/8.5.32/12001/logs/catalina.out
  fields:
    env: uat-12001-catalina
  include_lines: ['ERROR']
#13001-log
- type: log
  enable: true
  paths:
    - /mkt/tomcat/8.5.32/13001/logs/pay-web-boss.log
  fields:
    env: uat-13001-log
    include_lines: ['ERROR']
#13001-catalina
- type: log
  enable: true
  paths:
    - /mkt/tomcat/8.5.32/13001/logs/catalina.out
  fields:
    env: uat-13001-catalina
  include_lines: ['ERROR']
#14001-log
- type: log
  enable: true
  paths:
    - /mkt/tomcat/8.5.32/14001/logs/pay-web-gateway.log
  fields:
    env: uat-14001-log
  include_lines: ['ERROR']
#14001-catalina
- type: log
  enable: true
  paths:
    - /mkt/tomcat/8.5.32/14001/logs/catalina.out
  fields:
    env: uat-14001-catalina
  include_lines: ['ERROR']
#15001-log
- type: log
  enable: true
  paths:
    - /mkt/tomcat/8.5.32/15001/logs/roncoo-pay-web-merchant.log
  fields:
    env: uat-15001-log
  include_lines: ['ERROR']
#15001-catalina
- type: log
  enable: true
  paths:
    - /mkt/tomcat/8.5.32/15001/logs/catalina.out
  fields:
    env: uat-15001-catalina
  include_lines: ['ERROR']      每次创建索引会少uat-11001-log和uat-12001-log,filebeat读取了这个两个日志文件
继续阅读 »
#10001-log
- type: log
  enable: true
  paths:
    - /mkt/tomcat/8.5.32/10001/logs/mstore.log
  fields:
    env: uat-10001-log
  include_lines: ['ERROR']
#10001-catalina
- type: log
  enable: true
  paths:
    - /mkt/tomcat/8.5.32/10001/logs/catalina.out
  fields:
    env: uat-10001-catalina
  include_lines: ['ERROR']
#11001-log
- type: log
  enabled: true
  paths:
    - /mkt/tomcat/8.5.32/11001/logs/delivery.log
  include_lines: ['ERROR']
  fields:
    env: uat-11001-log
#11001-catalina.out
- type: log
  enable: true
  paths:
    - /mkt/tomcat/8.5.32/11001/logs/catalina.out
  fields:
    env: uat-11001-catalina
  include_lines: ['ERROR']
#12001-log
- type: log
  enable: true
  paths:
    - /mkt/tomcat/8.5.32/12001/logs/mstore.log
  fields:
    env: uat-12001-log
  include_lines: ['ERROR']
#12001-catalina
- type: log
  enable: true
  paths:
    - /mkt/tomcat/8.5.32/12001/logs/catalina.out
  fields:
    env: uat-12001-catalina
  include_lines: ['ERROR']
#13001-log
- type: log
  enable: true
  paths:
    - /mkt/tomcat/8.5.32/13001/logs/pay-web-boss.log
  fields:
    env: uat-13001-log
    include_lines: ['ERROR']
#13001-catalina
- type: log
  enable: true
  paths:
    - /mkt/tomcat/8.5.32/13001/logs/catalina.out
  fields:
    env: uat-13001-catalina
  include_lines: ['ERROR']
#14001-log
- type: log
  enable: true
  paths:
    - /mkt/tomcat/8.5.32/14001/logs/pay-web-gateway.log
  fields:
    env: uat-14001-log
  include_lines: ['ERROR']
#14001-catalina
- type: log
  enable: true
  paths:
    - /mkt/tomcat/8.5.32/14001/logs/catalina.out
  fields:
    env: uat-14001-catalina
  include_lines: ['ERROR']
#15001-log
- type: log
  enable: true
  paths:
    - /mkt/tomcat/8.5.32/15001/logs/roncoo-pay-web-merchant.log
  fields:
    env: uat-15001-log
  include_lines: ['ERROR']
#15001-catalina
- type: log
  enable: true
  paths:
    - /mkt/tomcat/8.5.32/15001/logs/catalina.out
  fields:
    env: uat-15001-catalina
  include_lines: ['ERROR']      每次创建索引会少uat-11001-log和uat-12001-log,filebeat读取了这个两个日志文件 收起阅读 »

社区日报 第469期 (2018-12-05)

1. Elasticsearch写入原理深入详解
http://t.cn/EyI2jxr
2.Lucene倒排索引简述 细说倒排索引构建
http://t.cn/EyI2eMN
3. 知乎如何基于开源Druid打造下一代数据平台
http://t.cn/E2Kmzj0
 
编辑:江水
归档:https://elasticsearch.cn/article/6173
订阅:https://tinyletter.com/elastic-daily
 
继续阅读 »
1. Elasticsearch写入原理深入详解
http://t.cn/EyI2jxr
2.Lucene倒排索引简述 细说倒排索引构建
http://t.cn/EyI2eMN
3. 知乎如何基于开源Druid打造下一代数据平台
http://t.cn/E2Kmzj0
 
编辑:江水
归档:https://elasticsearch.cn/article/6173
订阅:https://tinyletter.com/elastic-daily
  收起阅读 »

Day 5 - Elasticsearch 存储设备全解析

day5 - es存储设备全解析

Elastic Search 作为一个分布式系统,它的最小单元(shard)实现基于 lucene , lucene是一个io密集cpu密集的系统。cpu密集可以通过使用更多核,更快的cpu以及优化算法来解决。而io密集部分需要搭配高性能的存储设备以及存储策略来解决。

传统的服务器硬盘分为SATA,SAS硬盘以及现在最高性能的SSD硬盘,其中SSD硬盘又分为 SATA SSD,PCI-E SSD ,M.2 SSD(性能依次提升)。

两者的区别在于 SATA 最高可以提供 7200转的。著名的HADOOP集群中,一半都会选择企业级SATA盘来降低存储成本。而SATA盘容易损坏以及恢复速度的问题,则交给10g高速网卡以及三副本策略来解决。

如果是了解数据库领域的同学就会知道,MySQL 之类的数据库严重推荐使用SSD来做存储。TiDB这种新时代的分布式数据库甚至在安装过程中会见存储是否是高性能设备,当时低速设备时,安装将失败。

如何查看io压力

iostat -x 1 100

可以根据 iowait , ioutil 等值来综合判断. 当iowait长期接近100%基本代表io系统出现瓶颈了。这时候可以用iotop命令来诊断出具体是什么进程在消耗io资源。

如何测试硬盘性能

通过 fio 测试 顺序读/写,随机读/写性能。

顺序读 fio -name iops -rw=read -bs=4k -runtime=60 -iodepth 32 -filename /dev/sda -ioengine libaio -direct=1 随机读 fio -name iops -rw=randread -bs=4k -runtime=60 -iodepth 32 -filename /dev/sda -ioengine libaio -direct=1 顺序写 fio -name iops -rw=write -bs=4k -runtime=60 -iodepth 32 -filename /dev/sda -ioengine libaio -direct=1 随机写 fio -name iops -rw=randwrite -bs=4k -runtime=60 -iodepth 32 -filename /dev/sda -ioengine libaio -direct=1

更具体的测试可以参考磁盘性能指标--IOPS、吞吐量及测试

RAID

RAID 0

将数据分布在N块盘中,速度最快,可以享受磁盘的并行读取和写入;安全性最低,一块盘损坏,将导致所有数据丢失。

raid0.png

RAID 1

将数据同时保存在N块盘中,写入速度最慢(需要同时写多块盘)。安全性最高。

raid1.png

RAID 10 ?

将RAID 1 和 RAID 0 结合起来,获得高安全性和高性能。最常用的RAID策略。同时也是TiDB,MySQL等数据库推荐的RAID策略。

raid10.png

RAID 5

RAID 5 最低三块盘,存储数据的异或编码,在一块盘损坏时,可以提供编码恢复出数据。

raid5.png

ElasticSearch 使用低速设备的 Tips

修改index.merge.scheduler.max_thread_count参数为1;该参数影响lucene后台的合并线程数量,默认设置只适合SDD。多个合并线程可能导致io压力过大,触发 (linux 120s timeout)[https://cyberdak.github.io/es/2018/07/01/es-force-merge-cause-es-down].

存储策略

  1. 避免单机存储过多数据,如果单机故障,将导致集群需要大量数据,影响集群的吞吐量,特别是发生在高峰时候更会影响业务。千兆网卡每小时可以同步的数据为463gb,可以参考这个速度结合资深集群网卡以及存储来调节每个节点存储的数据量。
  2. 存储有条件使用RAID10,增加单节点性能以及避免单节点存储故障

RAID卡策略

根据服务器RAID卡的等级不同,高级的RAID卡可以使用 write-back 写策略,数据写入会直接写入到缓存中,随后刷新到硬盘上。当主机掉电时,由RAID卡带的电池来保证数据成功写入到硬盘中。write back的设置需要电池有电才能支持,而某些场景可以设置为force write-back(即使电池没电了,也要写缓存),从而提高写入性能。

继续阅读 »

day5 - es存储设备全解析

Elastic Search 作为一个分布式系统,它的最小单元(shard)实现基于 lucene , lucene是一个io密集cpu密集的系统。cpu密集可以通过使用更多核,更快的cpu以及优化算法来解决。而io密集部分需要搭配高性能的存储设备以及存储策略来解决。

传统的服务器硬盘分为SATA,SAS硬盘以及现在最高性能的SSD硬盘,其中SSD硬盘又分为 SATA SSD,PCI-E SSD ,M.2 SSD(性能依次提升)。

两者的区别在于 SATA 最高可以提供 7200转的。著名的HADOOP集群中,一半都会选择企业级SATA盘来降低存储成本。而SATA盘容易损坏以及恢复速度的问题,则交给10g高速网卡以及三副本策略来解决。

如果是了解数据库领域的同学就会知道,MySQL 之类的数据库严重推荐使用SSD来做存储。TiDB这种新时代的分布式数据库甚至在安装过程中会见存储是否是高性能设备,当时低速设备时,安装将失败。

如何查看io压力

iostat -x 1 100

可以根据 iowait , ioutil 等值来综合判断. 当iowait长期接近100%基本代表io系统出现瓶颈了。这时候可以用iotop命令来诊断出具体是什么进程在消耗io资源。

如何测试硬盘性能

通过 fio 测试 顺序读/写,随机读/写性能。

顺序读 fio -name iops -rw=read -bs=4k -runtime=60 -iodepth 32 -filename /dev/sda -ioengine libaio -direct=1 随机读 fio -name iops -rw=randread -bs=4k -runtime=60 -iodepth 32 -filename /dev/sda -ioengine libaio -direct=1 顺序写 fio -name iops -rw=write -bs=4k -runtime=60 -iodepth 32 -filename /dev/sda -ioengine libaio -direct=1 随机写 fio -name iops -rw=randwrite -bs=4k -runtime=60 -iodepth 32 -filename /dev/sda -ioengine libaio -direct=1

更具体的测试可以参考磁盘性能指标--IOPS、吞吐量及测试

RAID

RAID 0

将数据分布在N块盘中,速度最快,可以享受磁盘的并行读取和写入;安全性最低,一块盘损坏,将导致所有数据丢失。

raid0.png

RAID 1

将数据同时保存在N块盘中,写入速度最慢(需要同时写多块盘)。安全性最高。

raid1.png

RAID 10 ?

将RAID 1 和 RAID 0 结合起来,获得高安全性和高性能。最常用的RAID策略。同时也是TiDB,MySQL等数据库推荐的RAID策略。

raid10.png

RAID 5

RAID 5 最低三块盘,存储数据的异或编码,在一块盘损坏时,可以提供编码恢复出数据。

raid5.png

ElasticSearch 使用低速设备的 Tips

修改index.merge.scheduler.max_thread_count参数为1;该参数影响lucene后台的合并线程数量,默认设置只适合SDD。多个合并线程可能导致io压力过大,触发 (linux 120s timeout)[https://cyberdak.github.io/es/2018/07/01/es-force-merge-cause-es-down].

存储策略

  1. 避免单机存储过多数据,如果单机故障,将导致集群需要大量数据,影响集群的吞吐量,特别是发生在高峰时候更会影响业务。千兆网卡每小时可以同步的数据为463gb,可以参考这个速度结合资深集群网卡以及存储来调节每个节点存储的数据量。
  2. 存储有条件使用RAID10,增加单节点性能以及避免单节点存储故障

RAID卡策略

根据服务器RAID卡的等级不同,高级的RAID卡可以使用 write-back 写策略,数据写入会直接写入到缓存中,随后刷新到硬盘上。当主机掉电时,由RAID卡带的电池来保证数据成功写入到硬盘中。write back的设置需要电池有电才能支持,而某些场景可以设置为force write-back(即使电池没电了,也要写缓存),从而提高写入性能。

收起阅读 »

Day 4 - PB级规模数据的Elasticsearch分库分表实践

从2018年7月在开始在某阿里云数据中心部署Elasticsearch软件,到2018年12月共创建了15个集群,服务于客户的文档检索、交通视频检索、地理信息检索、日志安全审计等业务。其中数据规模最大的一个业务,共有800张表,7万亿条数据,每天新增500亿条记录,数据要求存储半年,单条记录大小1KB左右,存储规模约10PB,需要支持1000并发查询。

一、数据存储空间规划。

数据中心能用于搭建Elasticsearch集群的SSD盘共700TB,SATA盘共50PB。根据业务类型、时间范围划分热数据和冷数据,一部分重要数据存储在SSD盘的热数据集群,其它数据存储在SATA盘的冷数据集群。热数据集群主要存储各类实体信息,包括人员、物品、事件、地址、组织数据,以及最新轨迹数据。冷数据集群主要存储历史轨迹信息。热数据和冷数据按照业务拆分多个小集群,每个集群规模保持在50个节点左右,单个集群最大不超过200个节点。利用阿里云平台弹性伸缩的能力,每个Elasticsearch集群可以先从小规模创建,根据资源使用情况来弹性扩展节点规模。

Elasticsearch集群节点配置

pb001.jpg

二、索引设计。

1.索引别名(alias)。每类数据根据数据源表名建立索引(index),索引中只包含一个类型(type)。配置索引别名(alias),业务上根据别名写入、查询数据,索引重建等数据维护操作可以通过别名切换对业务透明。

2.按时间分表。轨迹类数据按时间(日/月)拆分,每个索引存储数据量保持在1TB(10亿)左右,索引名带上日期/月份后缀,拆分后的索引配置别名区分冷热数据。配置索引模板,指定索引分片数和副本数、字段类型、分词器。配置Linux crontab定时任务,通过shell脚本创建索引。

3.分片(shard)设置。索引按照单个分片10-40GB数据大小设计分片数,数据量少于10GB(1000万)的索引设置1个分片即可,数据量大于1TB(10亿)的索引设置分片数为集群节点数整数倍(例如50个节点的集群配置50个分片)。

4.副本(replica)设置。数据首次批量导入时索引副本数设置为0,快速写入数据。生产环境索引副本数设置为1,避免集群节点故障数据丢失。

三、索引mapping设计。

1.精心设计索引字段类型。在开发环境配置Elasticsearch允许自动创建索引,从数据源每张表取1000条记录批量写入Elasticsearch,自动创建索引mapping,然后再根据业务需要修改mapping配置合适的字段类型,指定字段索引分词器、是否存储、是否索引、是否合并至全文检索字段。 对于数据量大的表尤其要精心设计字段类型,尽量减少索引存储空间占用。在生产环境中建议配置不允许自动创建索引。

2.配置全文检索字段。如果业务需要全文检索,可以配置开启全文字段,同时需要占用更多存储空间;如果业务上只是按字段查询,可以配置禁用全文字段,减少存储空间。Elasticsearch5.X及之前的版本默认启用_all字段,合并所有字段的值。Elasticsearch6.X及之后的版本默认禁用_all字段,可以通过copy_to将多个字段值合并成一个全文字段。对于数据查全率要求高的业务场景,建议对全文字段配置cjk分词器(Elasticsearch和Lucene中自带,对中日韩文进行二元分词的分词器)。

3.通用字段统一命名。各个索引中的姓名、证件号码、时间(开始时间、结束时间)、地点(始发地、目的地)等常用字段统一命名。用户指定证件号、时间范围等精确字段查询条件时,可以使用统一的查询条件并行查询多个索引。

四、分词设置。

1.选择合适的分词器。Elasticsearch中内置了很多分词器:standard、cjk、nGram等,也可以安装ik、pinyin等开源分词器, 可以根据业务场景选择合适的分词器。 常用分词器: standard:Elasticsearch默认分词,英文按空格切分,中文按单个汉字切分。 cjk:根据二元索引(两个相邻的字作为一个词条)对中日韩文分词,可以保证查全率。 NGram:可以将英文按照字母切分,结合Elasticsearch的短语搜索(match_phrase)使用。 ik:比较热门的中文分词,能按照中文语义切分,可以自定义词典。 pinyin:可以让用户输入拼音,就能查找到相关的关键词。 对于查全率要求较高的场景,建议使用cjk分词,同时能支持比较快的响应速度。对于查准率要求较高的场景,建议使用ik分词。

CJK分词和IK分词对比(测试环境:Elasticsearch5.5.3,8335万条人员档案信息,10节点集群,单节点16核CPU、64G内存、2T SSD盘,1个线程批量写入,1个并发查询)

pb002.jpg

测试分词效果: curl -XPOST "http://localhost:9200/_analyze" -H 'Content-Type: application/json' -d' { "analyzer": "ik_max_word", "text": "南京市长江大桥" }'

2.NGram分词。对于像车牌号之类数字和字母连在一起的字符,默认会被切成一个完整词条,但是业务上又需要支持前缀、后缀模糊匹配,可以根据业务需求进行分词。车牌号建议增加一个分词字段,配置NGram分词器,切分1元至7元的组合。身份证号码建议增加分词字段,根据业务需要切分18位完整词条、前2位(省)、前4位(省市)、前6位(省市区县)、后4位、出生年月日、出生年份、出生年月、出生月日等组合。

3.单字分词。对于像姓名类字段,业务上需要支持完整匹配,又需要支持单字查询。可以配置1个keyword字段(不分词);1个text字段(分词),分词器选择Elasticsearch默认分词器standard,按单个汉字切分。

五、数据写入策略。

1.批量离线数据导入。各类业务数据源主要在数据仓库MaxCompute(原ODPS),为了把表数据从MaxCompute表导入到ElasticSearch集群中, 我们基于MaxCompute MapReduce开发了MaxCompute到ElasticSearch的数据导出作业,通过简单的配置就可以把数据导入到ElasticSearch中。 数据源在关系数据库RDS或者NoSQL的数据,可以通过配置DataWorks(dataX企业版)导入Elasticsearch集群。

2.实时数据导入。实时数据源主要是流式数据服务DataHub, 配置DataHub任务即可同步至Elasticsearch集群。也可以自己开发程序调用DataHub的SDK获取实时数据,经过业务处理后,调用ES Rest Client SDK批量写入Elasticsearch。

3.冷热数据自动迁移。轨迹类实时数据默认先写入热数据集群(SSD盘Elasticsearch集群),对于热数据集群过期的索引(例如1个月前的索引)需要迁移到冷数据集群(SATA盘Elasticsearch)。为了实现数据跨集群迁移,我们开发了snapshot插件将索引备份到对象存储服务OSS或分布式文件系统盘古。配置定时任务,将热数据集群索引备份后,从冷数据集群恢复,然后再删除热集群中的过期索引,保持热数据集群只存储较小规模数据。冷数据集群的索引如果超过半年,则关闭索引,减少JVM堆内存占用。

4.配置索引主键字段。为了保证Elasticsearch集群和数据源记录的一致性,建议所有索引配置主键字段,而不是让Elasticsearch自动生成主键。配置数据业务主键字段作为Elasticsearch主键字段。如果没有主键字段,则将原始数据能确定记录惟一性的几个字段合并为主键,或者将所有字段值合并起来计算MD5值作为主键。

5.配置写入路由。如果业务上需要经常根据某个字段查询,例如用户ID、车牌号等的字段,写入时可以指定路由字段。

6.写入参数调优。调整数据写入任务参数,避免写入操作占用过多磁盘IO和CPU。使用批量请求,配置合理的写入线程数,调大索引刷新时间间隔refresh interval,调整事务日志translog同步策略。

六、数据查询策略。

1.冷热库异步查询。用户输入关键词查询时,优先从热数据集群查询,有结果立即返回,并估算命中记录条数。热数据集群命中结果集不足时,再查询冷数据集群。

2.跨集群搜索。业务上需要多个Elasticsearch集群一起参与检索时,可以通过Cross Cluster Search同时对多个集群发起检索请求合并检索结果。单独创建一个5节点的Cross Cluster,设置远程集群节点信息,用于跨集群搜索,不存储业务数据。

3.快速返回和超时设置。查询请求中设置参数teminate_after指定每个分片(shard)最多匹配N条记录后返回(例如10000),设置查询超时时间timeout(例如10s),避免查询一些宽泛的条件时耗费过多系统资源。

4.查询语法解析。解析用户查询条件,识别用户的查询类型,例如用户输入车牌号、证件号、年龄段等条件时,查询条件改写为字段精确匹配,无法识别的查询条件默认从全文字段匹配。

5.查询条件调优。查询结果不需要相关度排序时使用过滤器(filter),尽量使用路由(routing),设置较少的查询读取记录条数和字段,避免前缀模糊匹配,设置search_after规避深度翻页性能问题。

七、数据写入、查询性能测试。

SSD盘集群写入性能测试(测试环境:Elasticsearch6.3.2集群,单节点16核CPU、64G内存、2T SSD盘,写入10亿条记录,单条记录1KB,副本数为0,1台写入服务器):

pb003.jpg

SSD盘集群查询性能测试

pb004.jpg

SATA盘集群写入性能测试(测试环境:Elasticsearch5.5.3集群,单节点56核CPU、128G内存、12块 6T SATA盘,分别写入1亿、3亿、5亿、30亿、300亿条记录,单条记录1KB,0副本,50台写入服务器):

pb005.jpg

SATA盘集群查询性能测试

pb006.jpg

参考文档:

  1. 阿里云Elasticsearch帮助文档 https://help.aliyun.com/product/57736.html
  2. Elasticsearch参考 https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
  3. 《Elasticsearch: 权威指南》 https://www.elastic.co/guide/cn/elasticsearch/guide/current/index.html
  4. 《深入理解Elasticsearch》https://detail.tmall.com/item.htm?id=551001166567
  5. 《死磕Elasticsearch方法论》https://blog.csdn.net/laoyang360/article/details/79293493
  6. Elasticsearch索引别名和零停机 https://www.elastic.co/guide/cn/elasticsearch/guide/current/index-aliases.html
  7. Elasticsearch自动按天创建索引脚本 https://blog.csdn.net/reblue520/article/details/80553317
  8. Elasticsearch NGram分词器 https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-ngram-tokenizer.html
  9. Elasticsearch开源权限管理认证插件Search Guard https://github.com/floragunncom/search-guard
  10. Elasticsearch开源可视化管理插件cerebro https://github.com/lmenezes/cerebro
  11. Elasticsearch开源SQL插件 https://github.com/NLPchina/elasticsearch-sql
  12. Elasticsearch快照及恢复 https://help.aliyun.com/document_detail/65675.html

Elasticsearch技术交流钉钉群

dingdingpng.png

继续阅读 »

从2018年7月在开始在某阿里云数据中心部署Elasticsearch软件,到2018年12月共创建了15个集群,服务于客户的文档检索、交通视频检索、地理信息检索、日志安全审计等业务。其中数据规模最大的一个业务,共有800张表,7万亿条数据,每天新增500亿条记录,数据要求存储半年,单条记录大小1KB左右,存储规模约10PB,需要支持1000并发查询。

一、数据存储空间规划。

数据中心能用于搭建Elasticsearch集群的SSD盘共700TB,SATA盘共50PB。根据业务类型、时间范围划分热数据和冷数据,一部分重要数据存储在SSD盘的热数据集群,其它数据存储在SATA盘的冷数据集群。热数据集群主要存储各类实体信息,包括人员、物品、事件、地址、组织数据,以及最新轨迹数据。冷数据集群主要存储历史轨迹信息。热数据和冷数据按照业务拆分多个小集群,每个集群规模保持在50个节点左右,单个集群最大不超过200个节点。利用阿里云平台弹性伸缩的能力,每个Elasticsearch集群可以先从小规模创建,根据资源使用情况来弹性扩展节点规模。

Elasticsearch集群节点配置

pb001.jpg

二、索引设计。

1.索引别名(alias)。每类数据根据数据源表名建立索引(index),索引中只包含一个类型(type)。配置索引别名(alias),业务上根据别名写入、查询数据,索引重建等数据维护操作可以通过别名切换对业务透明。

2.按时间分表。轨迹类数据按时间(日/月)拆分,每个索引存储数据量保持在1TB(10亿)左右,索引名带上日期/月份后缀,拆分后的索引配置别名区分冷热数据。配置索引模板,指定索引分片数和副本数、字段类型、分词器。配置Linux crontab定时任务,通过shell脚本创建索引。

3.分片(shard)设置。索引按照单个分片10-40GB数据大小设计分片数,数据量少于10GB(1000万)的索引设置1个分片即可,数据量大于1TB(10亿)的索引设置分片数为集群节点数整数倍(例如50个节点的集群配置50个分片)。

4.副本(replica)设置。数据首次批量导入时索引副本数设置为0,快速写入数据。生产环境索引副本数设置为1,避免集群节点故障数据丢失。

三、索引mapping设计。

1.精心设计索引字段类型。在开发环境配置Elasticsearch允许自动创建索引,从数据源每张表取1000条记录批量写入Elasticsearch,自动创建索引mapping,然后再根据业务需要修改mapping配置合适的字段类型,指定字段索引分词器、是否存储、是否索引、是否合并至全文检索字段。 对于数据量大的表尤其要精心设计字段类型,尽量减少索引存储空间占用。在生产环境中建议配置不允许自动创建索引。

2.配置全文检索字段。如果业务需要全文检索,可以配置开启全文字段,同时需要占用更多存储空间;如果业务上只是按字段查询,可以配置禁用全文字段,减少存储空间。Elasticsearch5.X及之前的版本默认启用_all字段,合并所有字段的值。Elasticsearch6.X及之后的版本默认禁用_all字段,可以通过copy_to将多个字段值合并成一个全文字段。对于数据查全率要求高的业务场景,建议对全文字段配置cjk分词器(Elasticsearch和Lucene中自带,对中日韩文进行二元分词的分词器)。

3.通用字段统一命名。各个索引中的姓名、证件号码、时间(开始时间、结束时间)、地点(始发地、目的地)等常用字段统一命名。用户指定证件号、时间范围等精确字段查询条件时,可以使用统一的查询条件并行查询多个索引。

四、分词设置。

1.选择合适的分词器。Elasticsearch中内置了很多分词器:standard、cjk、nGram等,也可以安装ik、pinyin等开源分词器, 可以根据业务场景选择合适的分词器。 常用分词器: standard:Elasticsearch默认分词,英文按空格切分,中文按单个汉字切分。 cjk:根据二元索引(两个相邻的字作为一个词条)对中日韩文分词,可以保证查全率。 NGram:可以将英文按照字母切分,结合Elasticsearch的短语搜索(match_phrase)使用。 ik:比较热门的中文分词,能按照中文语义切分,可以自定义词典。 pinyin:可以让用户输入拼音,就能查找到相关的关键词。 对于查全率要求较高的场景,建议使用cjk分词,同时能支持比较快的响应速度。对于查准率要求较高的场景,建议使用ik分词。

CJK分词和IK分词对比(测试环境:Elasticsearch5.5.3,8335万条人员档案信息,10节点集群,单节点16核CPU、64G内存、2T SSD盘,1个线程批量写入,1个并发查询)

pb002.jpg

测试分词效果: curl -XPOST "http://localhost:9200/_analyze" -H 'Content-Type: application/json' -d' { "analyzer": "ik_max_word", "text": "南京市长江大桥" }'

2.NGram分词。对于像车牌号之类数字和字母连在一起的字符,默认会被切成一个完整词条,但是业务上又需要支持前缀、后缀模糊匹配,可以根据业务需求进行分词。车牌号建议增加一个分词字段,配置NGram分词器,切分1元至7元的组合。身份证号码建议增加分词字段,根据业务需要切分18位完整词条、前2位(省)、前4位(省市)、前6位(省市区县)、后4位、出生年月日、出生年份、出生年月、出生月日等组合。

3.单字分词。对于像姓名类字段,业务上需要支持完整匹配,又需要支持单字查询。可以配置1个keyword字段(不分词);1个text字段(分词),分词器选择Elasticsearch默认分词器standard,按单个汉字切分。

五、数据写入策略。

1.批量离线数据导入。各类业务数据源主要在数据仓库MaxCompute(原ODPS),为了把表数据从MaxCompute表导入到ElasticSearch集群中, 我们基于MaxCompute MapReduce开发了MaxCompute到ElasticSearch的数据导出作业,通过简单的配置就可以把数据导入到ElasticSearch中。 数据源在关系数据库RDS或者NoSQL的数据,可以通过配置DataWorks(dataX企业版)导入Elasticsearch集群。

2.实时数据导入。实时数据源主要是流式数据服务DataHub, 配置DataHub任务即可同步至Elasticsearch集群。也可以自己开发程序调用DataHub的SDK获取实时数据,经过业务处理后,调用ES Rest Client SDK批量写入Elasticsearch。

3.冷热数据自动迁移。轨迹类实时数据默认先写入热数据集群(SSD盘Elasticsearch集群),对于热数据集群过期的索引(例如1个月前的索引)需要迁移到冷数据集群(SATA盘Elasticsearch)。为了实现数据跨集群迁移,我们开发了snapshot插件将索引备份到对象存储服务OSS或分布式文件系统盘古。配置定时任务,将热数据集群索引备份后,从冷数据集群恢复,然后再删除热集群中的过期索引,保持热数据集群只存储较小规模数据。冷数据集群的索引如果超过半年,则关闭索引,减少JVM堆内存占用。

4.配置索引主键字段。为了保证Elasticsearch集群和数据源记录的一致性,建议所有索引配置主键字段,而不是让Elasticsearch自动生成主键。配置数据业务主键字段作为Elasticsearch主键字段。如果没有主键字段,则将原始数据能确定记录惟一性的几个字段合并为主键,或者将所有字段值合并起来计算MD5值作为主键。

5.配置写入路由。如果业务上需要经常根据某个字段查询,例如用户ID、车牌号等的字段,写入时可以指定路由字段。

6.写入参数调优。调整数据写入任务参数,避免写入操作占用过多磁盘IO和CPU。使用批量请求,配置合理的写入线程数,调大索引刷新时间间隔refresh interval,调整事务日志translog同步策略。

六、数据查询策略。

1.冷热库异步查询。用户输入关键词查询时,优先从热数据集群查询,有结果立即返回,并估算命中记录条数。热数据集群命中结果集不足时,再查询冷数据集群。

2.跨集群搜索。业务上需要多个Elasticsearch集群一起参与检索时,可以通过Cross Cluster Search同时对多个集群发起检索请求合并检索结果。单独创建一个5节点的Cross Cluster,设置远程集群节点信息,用于跨集群搜索,不存储业务数据。

3.快速返回和超时设置。查询请求中设置参数teminate_after指定每个分片(shard)最多匹配N条记录后返回(例如10000),设置查询超时时间timeout(例如10s),避免查询一些宽泛的条件时耗费过多系统资源。

4.查询语法解析。解析用户查询条件,识别用户的查询类型,例如用户输入车牌号、证件号、年龄段等条件时,查询条件改写为字段精确匹配,无法识别的查询条件默认从全文字段匹配。

5.查询条件调优。查询结果不需要相关度排序时使用过滤器(filter),尽量使用路由(routing),设置较少的查询读取记录条数和字段,避免前缀模糊匹配,设置search_after规避深度翻页性能问题。

七、数据写入、查询性能测试。

SSD盘集群写入性能测试(测试环境:Elasticsearch6.3.2集群,单节点16核CPU、64G内存、2T SSD盘,写入10亿条记录,单条记录1KB,副本数为0,1台写入服务器):

pb003.jpg

SSD盘集群查询性能测试

pb004.jpg

SATA盘集群写入性能测试(测试环境:Elasticsearch5.5.3集群,单节点56核CPU、128G内存、12块 6T SATA盘,分别写入1亿、3亿、5亿、30亿、300亿条记录,单条记录1KB,0副本,50台写入服务器):

pb005.jpg

SATA盘集群查询性能测试

pb006.jpg

参考文档:

  1. 阿里云Elasticsearch帮助文档 https://help.aliyun.com/product/57736.html
  2. Elasticsearch参考 https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
  3. 《Elasticsearch: 权威指南》 https://www.elastic.co/guide/cn/elasticsearch/guide/current/index.html
  4. 《深入理解Elasticsearch》https://detail.tmall.com/item.htm?id=551001166567
  5. 《死磕Elasticsearch方法论》https://blog.csdn.net/laoyang360/article/details/79293493
  6. Elasticsearch索引别名和零停机 https://www.elastic.co/guide/cn/elasticsearch/guide/current/index-aliases.html
  7. Elasticsearch自动按天创建索引脚本 https://blog.csdn.net/reblue520/article/details/80553317
  8. Elasticsearch NGram分词器 https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-ngram-tokenizer.html
  9. Elasticsearch开源权限管理认证插件Search Guard https://github.com/floragunncom/search-guard
  10. Elasticsearch开源可视化管理插件cerebro https://github.com/lmenezes/cerebro
  11. Elasticsearch开源SQL插件 https://github.com/NLPchina/elasticsearch-sql
  12. Elasticsearch快照及恢复 https://help.aliyun.com/document_detail/65675.html

Elasticsearch技术交流钉钉群

dingdingpng.png

收起阅读 »

社区日报 第468期 (2018-12-04)

1、创建MySQL同Elasticsearch间的数据实时同步。
http://t.cn/EyyWjcy
2、详细介绍如何在kubernetes上构建EFK。
http://t.cn/EyyTOef
3、Laravel 使用 scout 集成 elasticsearch 做全文搜索。
http://t.cn/EyyWr5o

编辑:叮咚光军
归档:https://elasticsearch.cn/publish/article/6170
订阅:https://tinyletter.com/elastic-daily
继续阅读 »
1、创建MySQL同Elasticsearch间的数据实时同步。
http://t.cn/EyyWjcy
2、详细介绍如何在kubernetes上构建EFK。
http://t.cn/EyyTOef
3、Laravel 使用 scout 集成 elasticsearch 做全文搜索。
http://t.cn/EyyWr5o

编辑:叮咚光军
归档:https://elasticsearch.cn/publish/article/6170
订阅:https://tinyletter.com/elastic-daily 收起阅读 »

社区日报 第467期 (2018-12-03)

1. Opbeat已死,请用Elastic APM
http://t.cn/EyhRQRJ

2.Amazon Elasticsearch Service 中的韩语分析程序支持
http://t.cn/EyhmEHc

3. Elasticsearch在华泰证券内部的应用实践
http://t.cn/EyhuaFC

编辑:cyberdak
归档:https://elasticsearch.cn/article/6169
订阅:https://tinyletter.com/elastic-daily
继续阅读 »
1. Opbeat已死,请用Elastic APM
http://t.cn/EyhRQRJ

2.Amazon Elasticsearch Service 中的韩语分析程序支持
http://t.cn/EyhmEHc

3. Elasticsearch在华泰证券内部的应用实践
http://t.cn/EyhuaFC

编辑:cyberdak
归档:https://elasticsearch.cn/article/6169
订阅:https://tinyletter.com/elastic-daily 收起阅读 »

Day 3 - kibana二次开发tips

介绍

大家好,我是vvv,这个名字和王者荣耀AG超玩会中的vv没有一毛钱关系,随意取的一个的名字,后来发现貌似不能改了。做过一些数据产品,正是这段时间里开始接触elasticstack,对kibana做过一些二次开发。今天主要想写一些开发过程中的一些tips,希望可以给大家带来一些帮助。

技术栈分析

既然我们的主题是kibana,我们先来看下kibana的主要技术栈。很早开始kibana就开始基于nodejs (hapi框架) + angular(1.x)来进行开发,后来引入了react。kibana本身的代码也经过了多次重构剥离。现在的kibana的代码结构更加清晰

前提

elasticstack发展迅速,现在已经是6.5版本了。我们今天要介绍的是6.x系列的版本,6.x各个版本会有一些细微差异,但大致一样

tips

官方提供kibana下载版本主要是编译后的release版本。如果要基于kibana做二次开发,我们需要去https://github.com/elastic/kibana 上面下载对应的分支。官方有相应的文档去说明如何安装开发环境。我这里有一些tips:

设置国内yarn源

yarn config --global set 'registry https://registry.npm.taobao.org'

一些耗时需要编译的包可以全局安装

yarn global add  node-sass

多环境nodejs版本

不同kibana版本对nodejs版本要求也不一样,为了减少坑我们通常和官方要求的保持一致,如果你的电脑上需要运行多套不同版本的nodejs,那么你可能需要zsh + nvs, 会根据根目录的.node-version版本自动切换当前使用的node版本

IDE推荐

推荐使用vscode,轻量免费,支持很多插件。可以安装个prettier插件,帮助对代码做更好的格式化

debug

如果你用的不是上面我推荐的vscode的话,请忽略这一条。对于使用vscode的同学,首先在vsocde的设置里面开启:

"autoAttach": "on"

然后在vsocode里面打开一个终端,输入:

node --inspect-brk scripts/kibana.js --dev --oss --no-base-path

这个时候vscode就会在启动kibana dev模式的同时attach一个进程进去用于断点调试,对于调试node层非常方便。也能帮助你更好的阅读kibana源码

本地es

我们知道kibana是长在es之上,想要运行kibana怎么少得了es。kibana又一些命令命令可以快速的启动一个es环境:

下载并启动一个当前kibana需要的es版本

yarn es snapshot

灌入一些测试数据(如果需要定制灌入的数据可以看下这个脚本的帮助内容,加-h参数即可)

node scripts/makelogs

编译

kibana代码在release之前是要进行编译的。kibana提供了方便的命令行进行编译,甚至跨平台的交叉编译(生成不同平台的kibana release版本)。但是呢,有几个问题:

  1. kibana在编译的时候需要去aws上下载一些安装包,会导致正常情况下国内访问十分缓慢。(编译命令提供了几个参数可以关掉下载一些如nodejs等,但是还是很慢)
  2. build十分消耗cpu/gpu (mac的iterm2启动会做gpu优化)

解决办法:

  1. 如果你能解决网络问题,而且有性能不错的编译机器。这都不是问题
  2. 如果你对kibana的代码更改都是无侵入的(比如只是写了一些插件扩展),那么你可以去官方下载他们的snapshot版本
  3. 当然,如果你用的kibana版本就是release版本并且你的扩展都是插件,那么你直接用官方的release版本就好了

库的选型

  1. server端: nodejs具有十分丰富的生态,你可以找到很多nodejs相关的库。kibana本身的后端web框架是基于node的hapi的。hapi是一个沃尔玛团队维护的企业级框架,其本身也有很多扩展。当你需要对web框架做一些扩展的时候,可以优先想到去hapi官方看下

  2. ui端: kibana有一套漂亮的ui,其本身也是单独剥离成了一个库,方便引入使用。当然你也可以引入一些其他的前端库来满足你的具体业务需求。推荐尽量使用原生的eui和kibana源码里面的一些封装。这样让你的引入更少,更容易维护。

国际化

国际化是kibana很早就开始立的一个项。这块的进度也是越来越快。新版的kibana里面用@kbn/i18n这个package来统一javascript,html, nodejs做国际化的事情(具体大家可以看下这个package的readme)。国际化这块有一些建议:

  1. 扩展插件的时候养成国际化的习惯
  2. 默认的语系不建议再次设置成一个json文件。因为最新的@kbn/i18n会提供一个默认的文本,用于默认情况下展示。所以我们是没必要重复去维护一个默认的语言翻译json
  3. 假设你的默认语言是英文(和kibana一致),只有当你想要替换kibana默认翻译的时候,才去覆写en.json
  4. 当你对原生kibana有国际化这种需求的时候,建议独立出一个i18n翻译的插件去维护各个语言翻译相关的东西
  5. 目前kibana的国际化还未100%,如果你想知道目前哪些文本内容是支持国际化的。可以尝试如下脚本:
node scripts/extract_default_translations \
 --path src/core_plugins/kibana \
 --output /tmp
  1. 各个插件的之间的翻译文件独立,即使是相同的翻译内容。插件文本内容养成预留国际化的习惯

总结

上面列举了一些我平时的一些经验。时间篇幅有限,未能一一列举。希望可以帮到大家吧。也希望可以和大家多多交流

继续阅读 »

介绍

大家好,我是vvv,这个名字和王者荣耀AG超玩会中的vv没有一毛钱关系,随意取的一个的名字,后来发现貌似不能改了。做过一些数据产品,正是这段时间里开始接触elasticstack,对kibana做过一些二次开发。今天主要想写一些开发过程中的一些tips,希望可以给大家带来一些帮助。

技术栈分析

既然我们的主题是kibana,我们先来看下kibana的主要技术栈。很早开始kibana就开始基于nodejs (hapi框架) + angular(1.x)来进行开发,后来引入了react。kibana本身的代码也经过了多次重构剥离。现在的kibana的代码结构更加清晰

前提

elasticstack发展迅速,现在已经是6.5版本了。我们今天要介绍的是6.x系列的版本,6.x各个版本会有一些细微差异,但大致一样

tips

官方提供kibana下载版本主要是编译后的release版本。如果要基于kibana做二次开发,我们需要去https://github.com/elastic/kibana 上面下载对应的分支。官方有相应的文档去说明如何安装开发环境。我这里有一些tips:

设置国内yarn源

yarn config --global set 'registry https://registry.npm.taobao.org'

一些耗时需要编译的包可以全局安装

yarn global add  node-sass

多环境nodejs版本

不同kibana版本对nodejs版本要求也不一样,为了减少坑我们通常和官方要求的保持一致,如果你的电脑上需要运行多套不同版本的nodejs,那么你可能需要zsh + nvs, 会根据根目录的.node-version版本自动切换当前使用的node版本

IDE推荐

推荐使用vscode,轻量免费,支持很多插件。可以安装个prettier插件,帮助对代码做更好的格式化

debug

如果你用的不是上面我推荐的vscode的话,请忽略这一条。对于使用vscode的同学,首先在vsocde的设置里面开启:

"autoAttach": "on"

然后在vsocode里面打开一个终端,输入:

node --inspect-brk scripts/kibana.js --dev --oss --no-base-path

这个时候vscode就会在启动kibana dev模式的同时attach一个进程进去用于断点调试,对于调试node层非常方便。也能帮助你更好的阅读kibana源码

本地es

我们知道kibana是长在es之上,想要运行kibana怎么少得了es。kibana又一些命令命令可以快速的启动一个es环境:

下载并启动一个当前kibana需要的es版本

yarn es snapshot

灌入一些测试数据(如果需要定制灌入的数据可以看下这个脚本的帮助内容,加-h参数即可)

node scripts/makelogs

编译

kibana代码在release之前是要进行编译的。kibana提供了方便的命令行进行编译,甚至跨平台的交叉编译(生成不同平台的kibana release版本)。但是呢,有几个问题:

  1. kibana在编译的时候需要去aws上下载一些安装包,会导致正常情况下国内访问十分缓慢。(编译命令提供了几个参数可以关掉下载一些如nodejs等,但是还是很慢)
  2. build十分消耗cpu/gpu (mac的iterm2启动会做gpu优化)

解决办法:

  1. 如果你能解决网络问题,而且有性能不错的编译机器。这都不是问题
  2. 如果你对kibana的代码更改都是无侵入的(比如只是写了一些插件扩展),那么你可以去官方下载他们的snapshot版本
  3. 当然,如果你用的kibana版本就是release版本并且你的扩展都是插件,那么你直接用官方的release版本就好了

库的选型

  1. server端: nodejs具有十分丰富的生态,你可以找到很多nodejs相关的库。kibana本身的后端web框架是基于node的hapi的。hapi是一个沃尔玛团队维护的企业级框架,其本身也有很多扩展。当你需要对web框架做一些扩展的时候,可以优先想到去hapi官方看下

  2. ui端: kibana有一套漂亮的ui,其本身也是单独剥离成了一个库,方便引入使用。当然你也可以引入一些其他的前端库来满足你的具体业务需求。推荐尽量使用原生的eui和kibana源码里面的一些封装。这样让你的引入更少,更容易维护。

国际化

国际化是kibana很早就开始立的一个项。这块的进度也是越来越快。新版的kibana里面用@kbn/i18n这个package来统一javascript,html, nodejs做国际化的事情(具体大家可以看下这个package的readme)。国际化这块有一些建议:

  1. 扩展插件的时候养成国际化的习惯
  2. 默认的语系不建议再次设置成一个json文件。因为最新的@kbn/i18n会提供一个默认的文本,用于默认情况下展示。所以我们是没必要重复去维护一个默认的语言翻译json
  3. 假设你的默认语言是英文(和kibana一致),只有当你想要替换kibana默认翻译的时候,才去覆写en.json
  4. 当你对原生kibana有国际化这种需求的时候,建议独立出一个i18n翻译的插件去维护各个语言翻译相关的东西
  5. 目前kibana的国际化还未100%,如果你想知道目前哪些文本内容是支持国际化的。可以尝试如下脚本:
node scripts/extract_default_translations \
 --path src/core_plugins/kibana \
 --output /tmp
  1. 各个插件的之间的翻译文件独立,即使是相同的翻译内容。插件文本内容养成预留国际化的习惯

总结

上面列举了一些我平时的一些经验。时间篇幅有限,未能一一列举。希望可以帮到大家吧。也希望可以和大家多多交流

收起阅读 »

【 报名开启】2018 Elastic & 袋鼠云 & 阿里云技术沙龙(杭州)

互联网时代,十亿、百亿、千亿的数据日志呈井喷式增长,基于日志搜索分析的需求也越来越强烈。Elasticsearch 作为一个分布式、可扩展、实时的搜索与数据分析引擎, 在大体量的数据处理上,无论实在全文搜索,还是在结构化数据统计中,都有非常大的优势。然而在真正实践过程中海量数据如何高效采集,如何合理优化分配索引,如何规划集群,如何满足业务分析需求都是我们可能会面临的问题。

本次袋鼠云联合阿里云、Elastic 中文社区,共同邀请滴滴、有赞等行业技术专家一同分享和探讨各自领域Elastic的实践。
 
本次活动时间12月15日 周六人数限制100人,大家抓紧报名哈,报名链接https://meetup.elasticsearch.c ... .html

参与线下互动还有机会获得技术书籍与精美礼品哦!!!
不出意外,这应该是2018年Elastic在杭州的最后一次沙龙,小伙伴们抓紧今年的尾巴,不放过任何学习的机会哦!!!
继续阅读 »
互联网时代,十亿、百亿、千亿的数据日志呈井喷式增长,基于日志搜索分析的需求也越来越强烈。Elasticsearch 作为一个分布式、可扩展、实时的搜索与数据分析引擎, 在大体量的数据处理上,无论实在全文搜索,还是在结构化数据统计中,都有非常大的优势。然而在真正实践过程中海量数据如何高效采集,如何合理优化分配索引,如何规划集群,如何满足业务分析需求都是我们可能会面临的问题。

本次袋鼠云联合阿里云、Elastic 中文社区,共同邀请滴滴、有赞等行业技术专家一同分享和探讨各自领域Elastic的实践。
 
本次活动时间12月15日 周六人数限制100人,大家抓紧报名哈,报名链接https://meetup.elasticsearch.c ... .html

参与线下互动还有机会获得技术书籍与精美礼品哦!!!
不出意外,这应该是2018年Elastic在杭州的最后一次沙龙,小伙伴们抓紧今年的尾巴,不放过任何学习的机会哦!!! 收起阅读 »

Day 2 - ES 6.x拼音分词高亮爬坑记

大家好,我是来自尚德机构ES平台的负责人,白凡,今天为大家分享一些在6.x版本中拼音分词高亮问题爬坑的心路历程~,其实问题不复杂,主要介绍下思路。

首先简单讲下背景~可能在很多公司的很多部门,都有使用到ES。包括在尚德,由于很多部门的业务都涉及到ES,于是我们为了统一管理及维护,专门成立了ES平台部门,主要扮演的是类似于op dba角色,帮助业务部门部署维护ES集群,并根据业务需求提供解决方案。当然,除此之外,我们也会在公司内部推荐业务方去尝试除了日志和搜索以外的应用场景,比如分布式计算存储、监控、安全等方面。毕竟ES相比于其他组建,搭建部署更加方便,更轻量级,查询方式更丰富。所以,现如今在尚德机构,ES平台不仅用于了传统的日志和搜索领域,也在分布式数据存储和计算方面有很多应用。当然,这里只是为大家提供一些ES应用场景及其团队构建的思路。主要还是ES这个工具确实好用。

广告先做到这,回到正文。所以,前段日子,我们接收了一个新的业务部门需求,大致是:他们之前使用的自己搭建ES 2.x集群,现在接入到我们6.x的平台上来。我们帮忙设计了mapping,数据写入及同步方案之后,数据就慢慢接入进来。但问题也随即出现,原来在2.x上使用正常的拼音高亮mapping,在6.x上只能检索但无法高亮了?

2.x field如下:
"index" : {
"analysis" : {
"analyzer" : {
"pinyin_analyzer" : {
"tokenizer" : "my_pinyin"
}
},
"tokenizer" : {
"my_pinyin" : {
"type" : "pinyin",
"keep_full_pinyin" : false,
"limit_first_letter_length" : 16,
"lowercase" : true,
"remove_duplicated_term":true,
"keep_first_letter":true,
"keep_separate_first_letter" :true
}
}
}
}
POST /medcl/doc/_mapping
{
"properties": {
"name":{
"analyzer": "pinyin_analyzer",
"type": "string"
}
}
}

可以从上面例子看出,这个analyzer并没有问题,但是在搜索时,能得到结果,却无法高亮,即分词结果中start_offset及end_offset为0,这个如何解决呢?

回到medcl的拼音分词项目:
https://github.com/medcl/elast ... inyin
其中,有个配置项引起了我们的注意:

图片1.png


没跑了,应该是要将这个参数设置为false。
并且查看了源码,在PinyinTokenizer这个类下面,看到了这一行:

图片2.png


确定了我们的思路,于是乎,在tokenizer中将此参数设为false,如下:
"tokenizer" : {
"my_pinyin" : {
"type" : "pinyin",
"keep_full_pinyin" : true,
"keep_original" : false,
"limit_first_letter_length" : 16,
"lowercase" : true,
"remove_duplicated_term":true,
"ignore_pinyin_offset": false,
"keep_first_letter":true,
"keep_separate_first_letter" :true
}
}

写入一条数据,高亮没问题了,问题“看似”解决了。
当然,没有那么简单。因为在批量写入一部分数据后,总会报如下异常:
startOffset must be non-negative, and endOffset must be >= startOffset
这个异常,导致数据无法写入集群。
这里又是为什么呢?
这个问题,我也搞了一段时间,始终没找到很好的解决方案,此处只能先@medcl。
只是猜测在end()或者reset()方法内,需要lastOffset置0或者offsetAtt清空。但尝试了几次,依然报错。。。

这就比较头疼了,不过好在条条道路通罗马。在某次蹲坑过程中,灵感如尿崩。

如果Tokenizer解决不了,为何不仅用filter就行了呢?可以先用其他分词器,按我们业务的需求进行分词,再用filter,将分词过滤为拼音呢?

大致思路如下:
目前我们这个业务,需要如对于“尚德机构”这个词,搜索“shang”,“shangde”,“deji”时,能返回结果并高亮。
所以我们先用ngram分词,将“尚德机构”这个词分为“尚”,“尚德”,“徳机”,“德机构”等等。。
再用pinyin filter将各分词过滤为拼音,即“shang”,“shangde”,“deji”等。
并在搜索时,采用standard分词。
Mapping如下:
{
"settings": {
"analysis": {
"analyzer": {
"pinyin_analyzer": {
"tokenizer": "my_ngram",
"filter": [
"pinyin_filter"
]
}
},
"tokenizer": {
"my_ngram": {
"type": "ngram",
"min_gram": 1,
"max_gram": 50,
"token_chars": [
"letter",
"digit",
"punctuation",
"symbol"
]
}
},
"filter": {
"pinyin_filter": {
"type": "pinyin",
"keep_full_pinyin": false,
"keep_joined_full_pinyin": true,
"keep_none_chinese_in_joined_full_pinyin": true,
"none_chinese_pinyin_tokenize": false,
"remove_duplicated_term": true
}
}
}
},
"mappings": {
"abia321": {
"properties": {
"name": {
"type": "text",
"analyzer": "pinyin_analyzer",
"search_analyzer": "standard",
"term_vector": "with_positions_offsets"
}
}
}
}
}
最后,高亮问题解决,数据写入问题同样解决。
当然有朋友肯定还会需要搜索拼音首字母进行搜索,如搜“s”,“sd”,“dj”,也返回结果。
其实,只需要再专门设置个field,并调整pinyin filter参数,
搜索时用bool查询,逻辑should查询,同时对完整拼音field和拼音首字母field进行搜索即可。
在此就不做过多赘述。

当然,这里仅仅只是提供一种ES在选择analyzer,tokenizer,filter解决需求的思路。拼音分词这个问题,还是需要等待后续修复

最后,这里有较为完整的issue:
https://github.com/medcl/elast ... s/169
继续阅读 »
大家好,我是来自尚德机构ES平台的负责人,白凡,今天为大家分享一些在6.x版本中拼音分词高亮问题爬坑的心路历程~,其实问题不复杂,主要介绍下思路。

首先简单讲下背景~可能在很多公司的很多部门,都有使用到ES。包括在尚德,由于很多部门的业务都涉及到ES,于是我们为了统一管理及维护,专门成立了ES平台部门,主要扮演的是类似于op dba角色,帮助业务部门部署维护ES集群,并根据业务需求提供解决方案。当然,除此之外,我们也会在公司内部推荐业务方去尝试除了日志和搜索以外的应用场景,比如分布式计算存储、监控、安全等方面。毕竟ES相比于其他组建,搭建部署更加方便,更轻量级,查询方式更丰富。所以,现如今在尚德机构,ES平台不仅用于了传统的日志和搜索领域,也在分布式数据存储和计算方面有很多应用。当然,这里只是为大家提供一些ES应用场景及其团队构建的思路。主要还是ES这个工具确实好用。

广告先做到这,回到正文。所以,前段日子,我们接收了一个新的业务部门需求,大致是:他们之前使用的自己搭建ES 2.x集群,现在接入到我们6.x的平台上来。我们帮忙设计了mapping,数据写入及同步方案之后,数据就慢慢接入进来。但问题也随即出现,原来在2.x上使用正常的拼音高亮mapping,在6.x上只能检索但无法高亮了?

2.x field如下:
"index" : {
"analysis" : {
"analyzer" : {
"pinyin_analyzer" : {
"tokenizer" : "my_pinyin"
}
},
"tokenizer" : {
"my_pinyin" : {
"type" : "pinyin",
"keep_full_pinyin" : false,
"limit_first_letter_length" : 16,
"lowercase" : true,
"remove_duplicated_term":true,
"keep_first_letter":true,
"keep_separate_first_letter" :true
}
}
}
}
POST /medcl/doc/_mapping
{
"properties": {
"name":{
"analyzer": "pinyin_analyzer",
"type": "string"
}
}
}

可以从上面例子看出,这个analyzer并没有问题,但是在搜索时,能得到结果,却无法高亮,即分词结果中start_offset及end_offset为0,这个如何解决呢?

回到medcl的拼音分词项目:
https://github.com/medcl/elast ... inyin
其中,有个配置项引起了我们的注意:

图片1.png


没跑了,应该是要将这个参数设置为false。
并且查看了源码,在PinyinTokenizer这个类下面,看到了这一行:

图片2.png


确定了我们的思路,于是乎,在tokenizer中将此参数设为false,如下:
"tokenizer" : {
"my_pinyin" : {
"type" : "pinyin",
"keep_full_pinyin" : true,
"keep_original" : false,
"limit_first_letter_length" : 16,
"lowercase" : true,
"remove_duplicated_term":true,
"ignore_pinyin_offset": false,
"keep_first_letter":true,
"keep_separate_first_letter" :true
}
}

写入一条数据,高亮没问题了,问题“看似”解决了。
当然,没有那么简单。因为在批量写入一部分数据后,总会报如下异常:
startOffset must be non-negative, and endOffset must be >= startOffset
这个异常,导致数据无法写入集群。
这里又是为什么呢?
这个问题,我也搞了一段时间,始终没找到很好的解决方案,此处只能先@medcl。
只是猜测在end()或者reset()方法内,需要lastOffset置0或者offsetAtt清空。但尝试了几次,依然报错。。。

这就比较头疼了,不过好在条条道路通罗马。在某次蹲坑过程中,灵感如尿崩。

如果Tokenizer解决不了,为何不仅用filter就行了呢?可以先用其他分词器,按我们业务的需求进行分词,再用filter,将分词过滤为拼音呢?

大致思路如下:
目前我们这个业务,需要如对于“尚德机构”这个词,搜索“shang”,“shangde”,“deji”时,能返回结果并高亮。
所以我们先用ngram分词,将“尚德机构”这个词分为“尚”,“尚德”,“徳机”,“德机构”等等。。
再用pinyin filter将各分词过滤为拼音,即“shang”,“shangde”,“deji”等。
并在搜索时,采用standard分词。
Mapping如下:
{
"settings": {
"analysis": {
"analyzer": {
"pinyin_analyzer": {
"tokenizer": "my_ngram",
"filter": [
"pinyin_filter"
]
}
},
"tokenizer": {
"my_ngram": {
"type": "ngram",
"min_gram": 1,
"max_gram": 50,
"token_chars": [
"letter",
"digit",
"punctuation",
"symbol"
]
}
},
"filter": {
"pinyin_filter": {
"type": "pinyin",
"keep_full_pinyin": false,
"keep_joined_full_pinyin": true,
"keep_none_chinese_in_joined_full_pinyin": true,
"none_chinese_pinyin_tokenize": false,
"remove_duplicated_term": true
}
}
}
},
"mappings": {
"abia321": {
"properties": {
"name": {
"type": "text",
"analyzer": "pinyin_analyzer",
"search_analyzer": "standard",
"term_vector": "with_positions_offsets"
}
}
}
}
}
最后,高亮问题解决,数据写入问题同样解决。
当然有朋友肯定还会需要搜索拼音首字母进行搜索,如搜“s”,“sd”,“dj”,也返回结果。
其实,只需要再专门设置个field,并调整pinyin filter参数,
搜索时用bool查询,逻辑should查询,同时对完整拼音field和拼音首字母field进行搜索即可。
在此就不做过多赘述。

当然,这里仅仅只是提供一种ES在选择analyzer,tokenizer,filter解决需求的思路。拼音分词这个问题,还是需要等待后续修复

最后,这里有较为完整的issue:
https://github.com/medcl/elast ... s/169 收起阅读 »