什么是ELK

ELK是三个开源软件的缩写,分别表示:

  1. Elasticsearch
  2. Logstash
  3. Kibana

Elasticsearch是个开源分布式搜索引擎,提供搜集、分析、存储数据三大功能。它的特点有:分布式,零配置,自动发现,索引自动分片,索引副本机制,restful风格接口,多数据源,自动搜索负载等。

Logstash主要是用来日志的搜集、分析、过滤日志的工具,支持大量的数据获取方式。一般工作方式为c/s架构,client端安装在需要收集日志的主机上,server端负责将收到的各节点日志进行过滤、修改等操作再一并发往elasticsearch上去。

Kibana也是一个开源和免费的工具,Kibana可以为Logstash和ElasticSearch提供的日志分析友好的Web界面,可以帮助汇总、分析和搜索重要数据日志。

新增了一个Beats系列组件,它是一个轻量级的日志收集处理工具(Agent),Beats占用资源少,适合于在各个服务器上搜集日志或信息后传输给Logstash。

为什么需要ELK

先来看一个应用场景,常见的WEB应用日志分析,一般我们会怎么做?登录到每台服务器上,直接在日志文件中grep、awk就可以获得自己想要的信息。但在规模较大的场景中,此方法效率低下,面临问题包括日志量太大如何归档、文本搜索太慢怎么办、如何多维度查询。

这个时候我们希望集中化的日志管理,所有服务器上的日志收集汇总。常见解决思路是建立集中式日志收集系统,将所有节点上的日志统一收集,管理,访问。这样对于大型系统来说,都是一个分布式部署的架构,不同的服务模块部署
在不同的服务器上,问题出现时,大部分情况需要根据问题暴露的关键信息,定位到具体的服务器和服务模块,构建一套集中式日志系统,可以提高定位问题的效率。

一个完整的集中式日志系统,需要包含以下几个主要特点:

  • 收集-能够采集多种来源的日志数据
  • 传输-能够稳定的把日志数据传输到中央系统
  • 存储-如何存储日志数据分析-可以支持
  • UI 分析警告-能够提供错误报告,监控机制

ELK就是这样一整套解决方案,并且都是开源软件,之间互相配合使用,完美衔接,高效的满足了很多场合的应用,而不仅仅是日志分析。

HelloWorld

打开:http://localhost:5601/,点击面板中的“Dev Tools”按钮,进入Dev工具,开始我们的Elasticsearch初次访问之旅。

ELK入门与基本概念_分布式

创建索引

输入:
put test
输出:
{
  "acknowledged" : true,
  "shards_acknowledged" : true,
  "index" : "test"
}

查看索引

输入: 
get test
输出:
{
  "test" : {
    "aliases" : { },
    "mappings" : { },
    "settings" : {
      "index" : {
        "routing" : {
          "allocation" : {
            "include" : {
              "_tier_preference" : "data_content"
            }
          }
        },
        "number_of_shards" : "1",
        "provided_name" : "test",
        "creation_date" : "1634041189082",
        "number_of_replicas" : "1",
        "uuid" : "Q6ja6kKDQ6-XUBQWcIzXxg",
        "version" : {
          "created" : "7140199"
        }
      }
    }
  }
}

添加文档

输入: 
put /test/_doc/1 
{
  "msg": "hello elasticsearch"
}
输出:
{
  "_index" : "test",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 2,
  "result" : "updated",
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "_seq_no" : 1,
  "_primary_term" : 1
}

查看文档

输入:
get /test/_doc/1
输出:
{
  "_index" : "test",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 2,
  "_seq_no" : 1,
  "_primary_term" : 1,
  "found" : true,
  "_source" : {
    "msg" : "hello elasticsearch"
  }
}

基本原理与概念

倒排索引

从上面的例子中,我们看到了很多熟悉又陌生的概念,比如索引、文档等等。这些概念和我们平时看到的数据库里的索引有什么区别呢?

举个例子,现在我们要保存唐宋诗词,数据库中我们们会怎么设计?诗词表我们可能的设计如下:

朝代

作者

标题

诗词全文


李白

静夜思

床前明月光,疑是地上霜。举头望明月,低头思故乡。


李清照

如梦令

常记溪亭日暮,沉醉不知归路,兴尽晚回舟,误入藕花深处。争渡,争渡,惊起一滩鸥鹭。


李白

蜀道难

蜀道之难难于上青天,侧身西望长咨嗟。


李隆基

春台望

暇景属三春,高台聊四望。





要根据朝代或者作者寻找诗,都很简单,比如“select 诗词全文 from 诗词表 where 作者=‘李白’”,如果数据很多,查询速度很慢,怎么办?我们可以在对应的查询字段上建立索引加速查询。

但是如果我们现在有个需求:要求找到包含“望”字的诗词怎么办?用“select 诗词全文 from 诗词表 where 诗词全文 like ‘%望%’”,这个意味着要扫描库中的诗词全文字段,逐条比对,找出所有包含关键词“望”字的记录。基本上,数据库中一般的 SQL优化手段都是用不上的。数量少,大概性能还能接受,如果数据量稍微大点,就完全无法接受了,更何况在互联网这种海量数据的情况下呢?

怎么解决这个问题呢,用倒排索引。

可以将上述诗词的中每个字都可以作为关键字,然后建立关键字和文档之间的对应关系,也就是标识关键字被哪些文档包含,于是我们可以这么保存:

序号

关键词

静夜思

如梦令

蜀道难

春台望

1






2






所以,倒排索引就是将文档中包含的关键字全部提取处理,然后再将关键字和文档之间的对应关系保存起来,最后再对关键字本身做索引排序。用户在检索某一个关键字是,先对关键字的索引进行查找,再通过关键字与文档的对应关系找到所在文档。

在存储在关系型数据库中的数据,需要我们事先分析将数据拆分为不同的字段,而在es这类的存储中,需要应用程序根据规则自动提取关键字,并形成对应关系。这些预先提取的关键字,在全文检索领域一般被称为term(词项),文档的词项提取在es中被称为文档分析,这是全文检索很核心的过程,必须要区分哪些是词项,哪些不是,比如很多场景下,apple和apples是同一个东西,望和看其实是同一个动作。

Elasticsearch基本概念

Elasticsearch中比较关键的基本概念有索引、文档、映射、映射类型、文档字段概念,为了方便理解,可以和关系数据库中的相关概念进行个比对:

关系数据库

ES


索引(Index)


映射类型(Mapping Type)

数据行

文档(Document)

字段

文档字段(Field)

表结构

映射(Mapping)

Elasticsearch索引

索引是映射类型的容器,一个Elasticsearch索引非常像关系型世界的数据库,是独立的大量文档集合。

当然在底层,肯定用到了倒排索引,最基本的结构就是“keyword”和“Posting List”,Posting list就是一个int的数组,存储了所有符合某个term的文档 id。

另外,这个倒排索引相比特定词项出现过的文档列表,会包含更多其它信息。它会保存每一个词项出现过的文档总数, 在对应的文档中一个具体词项出现的总次数,词项在文档中的位置(便于高亮显示),每个文档的长度,所有文档的平均长度等等相关信息。

文档 (Document)

文档是ES中所有可搜索数据的最小单位,比如日志文件中的日志项、一部电影的具体信息等等。

文档会被序列化JSON格式保存到ElasticSearch中,JSON对象由字段组成,每个字段都有对象的字段类型(字符串,数值,布尔,日期,二进制,范围类型)。同时每个文档都有一个Unique ID,可以自己指定ID,或者通过ElasticSearch自
动生成。

所以严格来说,es中存储的文档是一种半结构化的数据。

映射

映射(mapping)定义了每个字段的类型、字段所使用的分词器等。

可以显式映射,由我们在索引映射中进行预先定义;也可以动态映射,在添加文档的时候,由 es 自动添加到索引,这个过程不需要事先在索引进行字段数据类型匹配等等,es会自己推断数据类型。

文档字段

文档中的一个字段field就相当于关系型数据库中的一列column,那么它肯定有数据类型,es提供的数据类型有:

字符串类型:string,字符串类还可被分为text和keyword类型,如果我们让es自动映射数据,那么es会把字符串定义为 text,并且还加了一个keyword类型字段。

  • text:文本数据类型,用于索引全文值的字段。使用文本数据类型的字段,它们会被分词,在索引之前将字符串转换为单个术语的列表(倒排索引),分词过程允许ES搜索每个全文字段中的单个单词。什么情况适合使用text,只要不具备唯一性的字符串一般都可以使用text。
  • keyword:关键字数据类型,用于索引结构化内容的字段。使用keyword类型的字段,其不会被分析,给什么值就原封不动地按照这个值索引,所以关键字字段只能按其确切值进行搜索。什么情况下使用keyword,具有唯一性的字符串,例如:电子邮件地址、MAC 地址、身份证号、状态代码…等等。

数字型数据类型:long、integer、short、byte、double、float

日期类型:date

布尔类型:boolean

复杂数据类型

  • 数组:无需专门的数据类型
  • 对象数据类型:单独的 JSON 对象
  • 嵌套数据类型:nested,关于JSON对象的数组

地理数据类型:

  • 地理点数据类型
  • 地理形状数据类型

专门数据类型:

  • IPv4数据类型
  • 单词计数数据类型token_count

各种数据类型的使用

创建一个新的索引:

输入:
put base-test
输出:
{
  "acknowledged" : true,
  "shards_acknowledged" : true,
  "index" : "base-test"
}

显式映射,指定文档中各个字段的数据类型:

输入:
put /base-test/_mapping
{
  "properties": {
    "crop": {
      "type": "text"
    },
    "name": {
      "type": "text"
    },
    "lang": {
      "type": "text"
    }
  }
}
输出:
{
  "acknowledged" : true
}

假如我们在插入一个文档时,多了个star字段,会怎么样?

输入:
put /base-test/_doc/1
{
  "crop": "Apache",
  "name": "ES",
  "lang": "JAVA",
  "star": 200
}
输出:
{
  "_index" : "base-test",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 1,
  "result" : "created",
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "_seq_no" : 0,
  "_primary_term" : 1
}

查询文档,可以发现star字段已经保存成功:

输入:
get /base-test/_doc/1
输出:
{
  "_index" : "base-test",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 1,
  "_seq_no" : 0,
  "_primary_term" : 1,
  "found" : true,
  "_source" : {
    "crop" : "Apache",
    "name" : "ES",
    "lang" : "JAVA",
    "star" : 200
  }
}

查询索引的映射,可以看到es自动帮我们新增了star这个字段:

输入:
get /base-test/_mapping
输出:
{
  "base-test" : {
    "mappings" : {
      "properties" : {
        "crop" : {
          "type" : "text"
        },
        "lang" : {
          "type" : "text"
        },
        "name" : {
          "type" : "text"
        },
        "star" : {
          "type" : "long"
        }
      }
    }
  }
}

修改映射,增加一个新的字段year:

输入:
put /base-test/_mapping
{
  "properties": {
    "year": {
      "type": "integer"
    }
  }
}
输出:
{
  "acknowledged" : true
}
输入:
get /base-test/_mapping
输出:
{
  "base-test" : {
    "mappings" : {
      "properties" : {
        "crop" : {
          "type" : "text"
        },
        "lang" : {
          "type" : "text"
        },
        "name" : {
          "type" : "text"
        },
        "star" : {
          "type" : "long"
        },
        "year" : {
          "type" : "integer"
        }
      }
    }
  }
}

数组

不需要特殊配置,一个字段如果被配置为基本数据类型,就是天生支持数组类型的。任何字段都可以有0个或多个值,但是在一个数组中数据类型必须一样。
比如:

输入:
put /base-test/_doc/2
{
  "name": [
    "Apache Activemq",
    "Activemq Artemis"
  ],
  "lang": "Java",
  "corp": "Apache",
  "stars": [
    500,
    200
  ]
}
输出:
{
  "_index" : "base-test",
  "_type" : "_doc",
  "_id" : "2",
  "_version" : 1,
  "result" : "created",
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "_seq_no" : 1,
  "_primary_term" : 1
}

是没问题的,但是如果:

输入:
put /base-test/_doc/3
{
  "name": [
    "Apache Kafka"
  ],
  "lang": "Java",
  "corp": "Apache",
  "stars": [
    500,
    "java"
  ]
}
输出:
{
  "error" : {
    "root_cause" : [
      {
        "type" : "mapper_parsing_exception",
        "reason" : "failed to parse field [stars] of type [long] in document with id '3'. Preview of field's value: 'java'"
      }
    ],
    "type" : "mapper_parsing_exception",
    "reason" : "failed to parse field [stars] of type [long] in document with id '3'. Preview of field's value: 'java'",
    "caused_by" : {
      "type" : "illegal_argument_exception",
      "reason" : "For input string: \"java\""
    }
  },
  "status" : 400
}

对象

JSON文档是有层次结构的,一个文档可能包含其他文档,如果一个文档包含其他文档,那么该文档值是对象类型,其数据类型是对象。当然ElasticSearch中是没有所谓对象类型的,比如:

输入:put /base-test/_doc/4{  "name": [    "Apache ShardingSphere"  ],  "lang": "Java",  "corp": "JingDong",  "stars": 400,  "address": {    "city": "BeiJing",    "country": "亦庄"  }}输出:{  "_index" : "base-test",  "_type" : "_doc",  "_id" : "4",  "_version" : 1,  "result" : "created",  "_shards" : {    "total" : 2,    "successful" : 1,    "failed" : 0  },  "_seq_no" : 2,  "_primary_term" : 1}

对象类型可以在定义索引的映射关系时进行指定。

多数据类型

如果说数组允许你使用同一个设置索引多项数据,那么多数据类型允许使用不同的设置,对同一项数据索引多次。带来的好处就是可以同一文本有多种不同的索引方式,比如一个字符串类型的字段,可以使用text类型做全文检索,使用keyword 类型做聚合和排序。我们可以看到es的动态映射生成的字段类型里,往往字符串类型都使用了多数据类型。当然,我们一样也可以自己定义:

输入:
put /base-test/_mapping
{
  "properties": {
    "name": {
      "type": "text",
      "fields": {
        "raw": {
          "type": "keyword"
        },
        "length": {
          "type": "token_count",
          "analyzer": "standard"
        }
      }
    }
  }
}
输出:
{
  "acknowledged" : true
}

在上面的代码里,我们使用"fields"就把name字段扩充为多字段类型,为name新增了两个子字段raw和length,raw设置类型为keyword,length设置类型为token_count,告诉es这个字段在保存还需要做词频统计。

通过fields字段设置的子字段raw和length,在我们添加文档时,并不需要单独设置值,他们name共享相同的值,只是es 会以不同的方式处理字段值。同样在检索文档的时候,它们也不会显示在结果中,所以它们一般都是在检索中以查询条件的形式出现,以减少检索时的性能开销。