Elasticsearch7.8详尽使用指南(二):ElasticSearch集群构建索引实践

日期 作者 版本 备注
2020-07-02 丁斌 v1.0

1. ElasticSearch集群构建索引

上一篇文章Elasticsearch7.8详尽使用指南(一):ElasticSearch集群部署实践中我们讲述了ElasticSearch集群部署方法。Es集群已经部署好了,接下来我们要新建索引,并展示如何构建全量和增量数据到索引中去的详尽实践方法论。

注意:

本文所述设计的所有的程序安装包和相关源代码均提供下载: es7.8package.rar 提取码: x4gg
解压后内容如下图:(其中esproj.zip是本文开源的es构建全量或实时索引的相关java源代码)
image.png

本文针对项目的测试数据假定来源都存储在mysql数据库中。如下图所示:
image.png

数据库data库中的position表中共计有91644条数据。我们以此91644条数据为例逐步demo实践如果将该表中所有数据构建到es索引中去。

1.1. 新建索引和配置mapping

1.1.1. Es restful API

接下来我们在叙述es新建索引和配置mapping的过程中会大量地用到es提供的restful API。以下罗列最常用的es restful API:

查看es集群健康状态:curl http://192.168.0.110:19200/_cat/health?
image.png
可见有3个节点es-node1,es-node2和es-node3 。

查看es集群有哪些索引:curl http://192.168.0.110:19200/_cat/indices?v
image.png
可见es目前没有任何索引

在elasticsearch7.x以前的版本中,可以这么理解: es也相当于一个数据库,都可以存储数据。我们对照mysql存储数据需要先创建database和table,相应地,es存储数据之前,也要创建index和type,我们简单感性地认为index和type相当于mysql的database和table。显然同mysql创建table一样,我们要定义索引数据的schema,在es中有特定的术语叫做mapping,相当于mysql的table定义。但是这样的理解在es7.x后不成立了!!!!
image.png
如上图所示,elasticsearch 7.x版本之后已经彻底去除了type的概念,elasticsearch7默认不在支持指定索引类型,默认索引类型是_doc。index下面不再允许设置type。直接给某个index设置mapping即可。相当于index就是mysql数据库中的dabase.table了。一个index唯一对应一个 mapping且不再支持type是es 7.x以后的正确现状。更详细的细节可参考:
https://www.elastic.co/guide/en/elasticsearch/reference/current/removal-of-types.html
https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html#mapping

假定对应position招聘职位表数据,我们新建的es的index名称叫:position。

创建索引名称为position: curl -XPUT http://192.168.0.110:19200/position?pretty
再次查看索引:curl http://192.168.0.110:19200/_cat/indices?v
image.png
可见名称叫position的index已经被创建,它的docs.count为0,目前没有任何文档即数据。
删除索引job: curl -XDELETE http://192.168.0.110:19200/position?pretty

接下来要为position 索引库新建一个mapping。下面重点叙述es的mapping即映射。

1.1.2. ES映射(mapping)

查看指定index的mapping命令:
curl -XGET http://192.168.0.110:19200/position/_mapping?pretty
image.png
可见此时索引没有任何mapping。Mapping为空。

查询所有index中的mapping: curl -XGET http://192.168.0.110:19200/_all/_mapping?pretty
image.png

Es7.x 版本下为position这个index新建一个mapping的restful API语法有如下两种合法的形式:

1) 事先不单独创建index,而将index和mapping一次性在一个命令中创建,语法如下:

curl  -XPUT http://192.168.0.110:19200/position/?pretty  -H 'content-Type:application/json'  -d  ' {
    "mappings":{
        "properties":{
            "title":{
                "type":"text"
            },
            "description":{
                "type":"text"
            },
            "price":{
                "type":"double"
            },
            "onSale":{
                "type":"boolean"
            },
            "type":{
                "type":"integer"
            },
            "createDate":{
                "type":"date"
            }
        }
    }
}'

image.png

2) 分阶段创建:先单独创建index, 再基于此index创建mapping,语法如下:

curl  -XPUT http://192.168.0.110:19200/position1/_mappings?pretty  -H 'content-Type:application/json'  -d  ' {
    "properties":{
        "title":{
            "type":"text"
        },
        "description":{
            "type":"text"
        },
        "price":{
            "type":"double"
        },
        "onSale":{
            "type":"boolean"
        },
        "type":{
            "type":"integer"
        },
        "createDate":{
            "type":"date"
        }
    }
}'

image.png
以上两种方式我们建议方式1)一次性创建index和mapping。简单明了。

最后再次查看现在的index和mapping:
查看index: curl http://192.168.0.110:19200/_cat/indices?v
image.png

再次执行查看maping: curl -XGET http://192.168.0.110:19200/_all/_mapping?pretty
image.png

至此,我们叙述了如何创建索引和其对应的字段类型定义mapping。这里描述的是创建静态mapping的方式。后面我们将利用更加便利的索引模板的方式来创建mapping。

这里先删除postion和position1两个index如下:
image.png

接下来叙述一下es的字段数据类型。

1.1.3. 字段数据类型

Es支持非常多的字段数据类型:
(以下叙述的都是es7.x尤其是es7.8支持的字段数据类型,低版本的es跟这个可能有些许差别,要注意!)

  • text:默认会进行分词,支持模糊查询(5.x之后版本string类型已废弃,请大家使用text)。
  • keyword:不进行分词;keyword类型默认开启doc_values来加速聚合排序操作,占用了大量磁盘io 如非必须可以禁用doc_values。
  • number:如果只有过滤场景 用不到range查询的话,使用keyword性能更佳,另外数字类型的doc_values比字符串更容易压缩。
  • array:es不需要显示定义数组类型,只需要在插入数据时用'[]'表示即可,'[]'中的元素类型需保持一致。
  • range:对数据的范围进行索引;目前支持 number range、date range 、ip range。
  • boolean: 只接受true、false 也可以是字符串类型的“true”、“false”
  • date:支持毫秒、根据指定的format解析对应的日期格式,内部以long类型存储。
  • geo_point:存储经纬度数据对。
  • ip:将ip数据存储在这种数据类型中,方便后期对ip字段的模糊与范围查询。
  • nested:嵌套类型,一种特殊的object类型,存储object数组,可检索内部子项。
1.1.3.1. 核心数据类型

image.png
核心数据类型如上图所示,其中需要特别说明的是:

string类的text和keyword区别一定要重视:把一个string字段指定为text类型说明该字段内容是要经过中文分词器进行分词,然后切分成多个term用于全文检索的。于此相对,如果一个string字段被指定为keyword,就说明内部建索引过程不会对该字段文本内容进行分词,它会作为一个整体被建到索引正排里去。
1.1.3.2. 地理数据类型

image.png
该数据类型主要针对支持地理坐标数据进行快速经纬度查询而支持的一种特殊的数据类型。

1.1.3.3. 复合数据类型

image.png

1.1.3.4. 数组类型
1.1.3.5. 多字段类型

更详细的内容细节可参见网页:
https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html

1.1.4. 利用索引模板定义和新建索引

本节是我们推荐的创建索引和索引定义mapping的方式。因为它更加便利。本文采用索引模板index templates的方式来配置索引的schema。es索引可使用预定义的模板进行创建,这个模板称作Index templates。注意:索引模板支队所有新建立的索引有效,对已经存在的索引无效。模板设置包括settings和mappings,通过模式匹配的方式使得多个索引重用一个模板。关于索引模板的更详细细节可参考网页:https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-templates.html
https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-templates-v1.html
https://www.elastic.co/guide/en/elasticsearch/reference/current/dynamic-templates.html#match-mapping-type
虽然es7.8引入了新的复合索引模板,但仍然支持传统的索引模板。本文使用传统的索引模板格式(legacy index template )。

1.1.4.1. match_mapping_type:被探测到的数据类型

在索引模板中有一个非常重要的field就是:match_mapping_type,它代表文档字段值被字面探测识别成的数据类型,它的规则如下:
image.png

1) true 或false 被自动探测识别为boolean类型;
2) 当date_detection开关打开时,一些符合特定日期格式字符串将会被自动探测识别为date类型;
3) 所有带有小数部分的数字被自动探测识别为double类型。(注意不是float类型)
4) 不带有小数部分的数字被自动识别为long类型;(注意不是integer类型。)
5) 对于对象类型统一识别为object类型;
6) 最重要的,所有字符串统一识别为string类型;
7) *代表任意类型。

image.png
上图是一个示例。

此外,_all在7.x版本已经被copy_to所代替,可用于满足特定场景。copy_to将字段数值拷贝到目标字段,实现类似_all的作用。
注意:copy_to的目标字段不出现在_source中

当date_detection被设置为true(默认)时,凡是string类型且符合
strict_date_optional_time设置的日期格式的字段都被识别为类型date。
strict_date_optional_time默认格式如下:
"strict_date_optional_time":"yyyy/MM/dd HH:mm:ss Z||yyyy/MM/dd Z"

date格式可以在put mapping的时候用 format 参数指定,如果不指定的话,则启用默认格式,是"strict_date_optional_time||epoch_millis"。这表明只接受符合"strict_date_optional_time"格式的字符串值,或者long型数字。

1.1.4.2. match/unmatch/match_pattern:如何匹配与排除匹配

match表征文档的字段名称匹配什么样的模式,是一种过滤选项。默认以通配符的形式匹配:即*或?等表示匹配含义。unmatch在上述match匹配的结果集中构建排除集。
如果想支持正则表达式匹配方式,则加上match_pattern:regex即可。一个示例如下图所示:
image.png

1.1.4.3. 索引别名与0停机时间

一般来说重新索引过程中的会遇到的一个比较头疼的问题是必须更新你的应用,来使用另一个索引名。索引别名正是用来解决这个问题的!

索引别名就像一个快捷方式或软连接, 可以指向一个或多个索引, 也可以给任何需要索引名的API 使用。别名带给我们极大的灵活性,允许我们做到:
image.png
这里有两种管理别名的途径: _alias 用于单个操作, _aliases 用于原子化多个操作。
在这一章中, 我们假设你的应用采用一个叫 my_index 的索引。 而事实上, my_index 是一个指向当前真实索引的别名。真实的索引名将包含一个版本号: my_index_v1 , my_index_v2 等等。开始, 我们创建一个索引 my_index_v1 , 然后将别名 my_index 指向它:

开始, 我们创建一个索引 my_index_v1 , 然后将别名 my_index 指向它:
image.png
<1> 创建索引 my_index_v1 。
<2> 将别名 my_index 指向 my_index_v1 。

你可以检测这个别名指向哪个索引:
image.png

然后, 我们决定修改索引中一个字段的映射。 当然我们不能修改现存的映射, 索引我们需要重新索引数据。 首先, 我们创建有新的映射的索引 my_index_v2 。
image.png
然后我们从将数据从 my_index_v1 迁移到 my_index_v2 。一旦我们认为数据已经被正确的索引了, 我们就将别名指向新的索引。

别名可以指向多个索引, 所以我们需要在新索引中添加别名的同时从旧索引中删除它。 这个操作需要原子化, 所以我们需要用 _aliases 操作:
image.png
这样,你的应用就从旧索引迁移到了新的,而没有停机时间。

提示:

即使你认为现在的索引设计已经是完美的了,当你的应用在生产 环境使用时,还是有可能在今后有一些改变的。所以请做好准备:在应用中使用别名而不是索引。然后你就可以在任何时候重建索引。别名的开销很小,应当广泛使用。

Elasticsearch的别名,就类似数据库的视图。

创建别名:

我们为索引my_index创建一个别名my_index_alias,这样我们对my_index_alias的操作就像对my_index的操作一样

POST /_aliases
{
  "actions": [
    {
      "add": {
        "index": "my_index",
        "alias": "my_index_alias"
      }
    }
  ]
}

别名不仅仅可以关联一个索引,它能聚合多个索引

我们为索引my_index_1 和 my_index_2 创建一个别名my_index_alias,这样对my_index_alias的操作(仅限读操作),会操作my_index_1和my_index_2,类似于聚合了my_index_1和my_index_2.我们是不能对my_index_alias进行写操作,当有多个索引时alias,不能区分到底操作哪一个

POST /_aliases
{
  "actions": [
    {
      "add": {
        "index": "my_index_1",
        "alias": "my_index_alias"
      }
    },
    {
      "add": {
        "index": "my_index_2",
        "alias": "my_index_alias"
      }
    }
  ]
}

GET /my_index_alias/_search
{
}

创建filtered的别名:

例如对于同一个index,我们给不同人看到不同的数据,

如my_index有个字段是team,team字段记录了该数据是那个team的。team之间的数据是不可见的。

POST /_aliases
{
  "actions": [
    {
      "add": {
        "index": "my_index",
        "alias": "my_index__teamA_alias",
        "filter":{
            "term":{
                "team":"teamA"
            }
        }
      }
    },
    {
      "add": {
        "index": "my_index",
        "alias": "my_index__teamB_alias",
        "filter":{
            "term":{
                "team":"teamB"
            }
        }
      }
    },
    {
      "add": {
        "index": "my_index",
        "alias": "my_index__team_alias"
      }
    }
  ]
}

GET /my_index__teamA_alias/_search 只能看到teamA的数据
GET /my_index__teamB_alias/_search 只能看到teamB的数据
GET /my_index__team_alias/_search 既能看到teamA的,也能看到teamB的数据

因此在索引模板中我们强烈建议引入索引别名,以应对未来在生产环境中一定会发生的索引升级。
下图是索引模板中设置索引别名的一个示例:
image.png

好了,到目前为止,关于索引模板的预备知识我们基本都讲述完毕了。下面开始实战过程:
为position招聘职位表数据定义一个索引模板。

1.1.4.4. 实战:为position数据库表数据定义动态索引模板
1.1.4.4.1. Mysql字段处理
CREATE TABLE `position` (
 `id` varchar(64) NOT NULL COMMENT '缂栧彿',
 `recruitment_id` varchar(64) NOT NULL COMMENT '鎷涜仒缂栧彿',
 `position_name` varchar(256) DEFAULT NULL COMMENT '鑱屼綅鍚嶇О',
 `student_type` varchar(256) DEFAULT NULL COMMENT '瀛﹀巻',
 `student` varchar(255) DEFAULT NULL,
 `majorName` longtext,
 `major` longtext COMMENT '涓撲笟',
 `demand_number` varchar(8) DEFAULT NULL COMMENT '闂傚倸娲犻崑鎾存叏閻熸澘鈧嘲效婢舵劕鏋?',
 `position_description` longtext COMMENT '鑱屼綅鎻忚堪',
 `city` text,
 `cityName` text,
 `college` varchar(500) DEFAULT NULL,
 `sut1` varchar(255) DEFAULT NULL,
 `major_standard` text COMMENT '标准专业',
 PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='招聘职位信息';

以上是招聘职位信息的 mysql数表定义。

为了实现对该数据表实时数据的索引增量,我们统一为所有业务表再 新增一个字段:_updatetime,类型为bigint,并且同时创建该字段的mysql 索引idx_for_updatetime,建索引的目的是为了快速查找定位。该字段存储前台发送来的对该条招聘职位信息改动(第一次叫创建)的时间戳,记录为自1970.1.1日0时0分0秒到现在经历的毫秒数,其值类似于1594508968000。

接下来,对position 插入最后一列字段为_updatetime,值统一设定为0,mysql语句如下:
ALTER TABLE position ADD COLUMN _updatetime BIGINT NOT NULL COMMENT 'updated timestamp in epoch milliseconds' after major_standard;
image.png

为_updatetime建立索引,sql执行如下:
ALTER TABLE position ADD INDEX idx_for_updatetime(_updatetime) ;
image.png

1.1.4.4.2. Es 索引mapping数据格式设计

image.png
以上表格中除了红色标注“是”的项目需要文本分词以支持全文索引,其他文本不需要分词。大部分中文分词器在检索时选择ik_max_word,在检索时选择分词组件:ik_smart。对应如下:

建索引时分词模块:"analyzer": "ik_max_word"
检索时分词模块:   "search_analyzer": "ik_smart"

_updatetime字段es格式为date,format 为epoch_millis。
对于major和city 这两个字段,虽然是文本类型,但他们的取值特点如下:
image.png

1.1.4.4.3. 以动态索引模板创建索引和mapping

根据前节分析,我们可以很容易写出招聘职位信息表数据的动态索引模板,如下:

{
    "index_patterns":[
        "collegejob_*"
    ],
    "order":0,
    "settings":{
        "number_of_shards":5,
        "number_of_replicas":1,
        "analysis":{
            "analyzer":{
                "comma":{
                    "pattern":",",
                    "type":"pattern"
                }
            }
        }
    },
    "mappings":{
        "_source":{
            "enabled":true
        },
        "dynamic":"true",
        "date_detection":false,
        "numeric_detection":true,
        "properties":{
            "id":{
                "type":"keyword"
            },
            "recruitment_id":{
                "type":"keyword"
            },
            "demand_number":{
                "type":"text",
                "index":"true",
                "analyzer":"ik_max_word",
                "search_analyzer":"ik_smart",
                "fields":{
                    "raw":{
                        "type":"keyword",
                        "index":true,
                        "ignore_above":1024
                    }
                }
            },
            "major":{
                "type":"text",
                "analyzer":"comma",
                "search_analyzer":"comma"
            },
            "city":{
                "type":"text",
                "analyzer":"comma",
                "search_analyzer":"comma"
            },
            "updatetime":{
                "type":"date",
                "format":"epoch_millis"
            }
        },
        "dynamic_templates":[
            {
                "string_fields":{
                    "match":"*",
                    "match_mapping_type":"string",
                    "mapping":{
                        "type":"text",
                        "index":"true",
                        "analyzer":"ik_max_word",
                        "search_analyzer":"ik_smart",
                        "fields":{
                            "raw":{
                                "type":"keyword",
                                "index":true,
                                "ignore_above":32766
                            }
                        }
                    }
                }
            }
        ]
    },
    "aliases":{
        "{index}-alias":{
        }
    }
}

image.png
上图是对文字版的动态索引模板的主要内容的简要说明(可能跟文字版有些许出入,以最终的文字版为准)。

新添加动态索引模板的es restfulAPI为:

curl -XPUT http://192.168.0.110:19200/_template/collegejob_template_1?pretty -H 'content-Type:application/json' -d '
{STATEMENT} '

将该语句中的{STATEMENT} 替换为上文 文字版的 动态索引模板内容即可。

执行结果为:
image.png

接下来查询刚才新建的索引模板,执行命令:
curl -XGET http://192.168.0.110:19200/_template/collegejob_template_1?pretty
image.png
可见能查到刚才新建的collegejob_template_1索引模板了。

删除索引模板的命令是:
curl -XDELETE http://192.168.0.110:19200/_template/collegejob_template_1?pretty

最后根据这个动态索引模板,我们创建一个索引名称叫做:collegejob_position_v20200712_1.

注意index名称必须要能匹配到上面我们已经创建好的动态索引模板中的index_patterns中的值的pattern。

RestfulAPI: curl -XPUT http://192.168.0.110:19200/collegejob_position_v20200712_1?pretty
image.png

再次查看索引:curl http://192.168.0.110:19200/_cat/indices?v
image.png

查看索引collegejob_position_v20200712_1的mapping:
curl http://192.168.0.110:19200/collegejob_position_v20200712_1/_mappings?pretty
image.png
可见该index已经被正确关联到我们预期的mapping上了。

至此,索引和mapping已经创建成功了。接下来开始构建全量索引数据和增量索引数据。

1.2. 构建全量索引数据

1.2.1. 准备测试数据

position ---招聘职位表,共计91644条测试数据。为了模拟分别构建全量索引数据和构建增量索引数据的过程,同时也模拟演示生产环境下数据产生与处理的过程,我们对测试数据做如下处理:

1)全量和增量数据的区分字段是_updatetime(_updatetime字段是个bigint型数据,存储自1970.1.1 0:0:0 至今的毫秒数,要求所有业务数据表都必须有此字段和定义);

2)假定从当前时刻开始建全量索引索引,记录开始建全量索引的当前时刻,记录为S时刻:2020年 07月 12日 星期日 09:51:09 CST, 转换成_updatetime的数据格式为:1594518666324,注意是一个13位长整数。记住这一时刻很重要。后面构建索引时也需要这个分界时间戳S。

3)编写sql语句实现对position 表中一半数据的_updattime设置为早于时刻S的随机位于时间段[1577808000000, S],(1577808000000为2020.1.1) 另一半数据的_updattime设置为晚于时刻S的随机位于时间段[S,1594522719000]。(1594522719000 为S+1小时时刻)。

Mysql生成在i ≤ R ≤ j 这个范围得到一个随机整数R ,公式为:FLOOR(i + RAND() * (j – i + 1))

结果如下:
update position set _updatetime=FLOOR(1577808000000+RAND() * (1594518666324-1577808000000 + 1)) limit 45822 ;
image.png

update position set _updatetime=FLOOR(1594522719000+RAND() * (1594522719000-1594518666324 + 1)) where _updatetime=0 ;
image.png

接下来查询验证一下更新时间戳早于和晚于时刻S的数据条数:
select count(1) from position where _updatetime < 1594518666324 ;
image.png

select count(1) from position where _updatetime > 1594518666324 ;
image.png
现在表中一半数据早于时刻S,另一半数据晚于时刻S。

接下来构建mysql2es的全量索引。

1.2.2. Zookeeper迅速安装部署方法

在后面章节全量和增量构建索引阶段为了提高构建索引的效率和保证高可用,我们经常用的一个实现思路是以多机器多进程代替单机单进程。而对于多机器多进程,不可避免涉及多进程之间工作进度的协同,需要用到分布式一致性的工具。建议使用zookeeper。比如分布式锁等。本节迅速简述一下zookeeper的安装部署和使用方法。

注意:安装zookeeper之前需要在每台机器上安装好jdk,建议安装至少jdk1.8及以上版本。本文安装的是jdk1.8。
1.2.2.1. Zookeeper技术简介

ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。

ZooKeeper的目标就是封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。
ZooKeeper包含一个简单的原语集,提供Java和C的接口。
ZooKeeper代码版本中,提供了分布式独享锁、选举、队列的接口,其中分布锁和队列有Java和C两个版本,选举只有Java版本。

1) 原理
ZooKeeper是以Fast Paxos算法为基础的,Paxos 算法存在活锁的问题,即当有多个proposer交错提交时,有可能互相排斥导致没有一个proposer能提交成功,而Fast Paxos作了一些优化,通过选举产生一个leader (领导者),只有leader才能提交proposer,具体算法可见Fast Paxos。因此,要想弄懂ZooKeeper首先得对Fast Paxos有所了解。

ZooKeeper的基本运转流程:

  • 1、选举Leader。
  • 2、同步数据。
  • 3、选举Leader过程中算法有很多,但要达到的选举标准是一致的。
  • 4、Leader要具有最高的执行ID,类似root权限。
  • 5、集群中大多数的机器得到响应并follow选出的Leader。

2) 特点

在Zookeeper中,znode是一个跟Unix文件系统路径相似的节点,可以往这个节点存储或获取数据。如果在创建znode时Flag设置为EPHEMERAL,那么当创建这个znode的节点和Zookeeper失去连接后,这个znode将不再存在在Zookeeper里,Zookeeper使用Watcher察觉事件信息。当客户端接收到事件信息,比如连接超时、节点数据改变、子节点改变,可以调用相应的行为来处理数据。Zookeeper的Wiki页面展示了如何使用Zookeeper来处理事件通知,队列,优先队列,锁,共享锁,可撤销的共享锁,两阶段提交。

那么Zookeeper能做什么事情呢,简单的例子:假设我们有20个搜索引擎的服务器(每个负责总索引中的一部分的搜索任务)和一个总服务器(负责向这20个搜索引擎的服务器发出搜索请求并合并结果集),一个备用的总服务器(负责当总服务器宕机时替换总服务器),一个web的cgi(向总服务器发出搜索请求)。搜索引擎的服务器中的15个服务器提供搜索服务,5个服务器正在生成索引。这20个搜索引擎的服务器经常要让正在提供搜索服务的服务器停止提供服务开始生成索引,或生成索引的服务器已经把索引生成完成可以提供搜索服务了。使用Zookeeper可以保证总服务器自动感知有多少提供搜索引擎的服务器并向这些服务器发出搜索请求,当总服务器宕机时自动启用备用的总服务器。

1.2.2.2. Zookeeper集群快速搭建

从zookeeper官网https://zookeeper.apache.org/releases.html 下载当前最新版本的zookeeper3.6.1。
image.png

分布式zookeeper(简称zk)集群至少要求运行在3台或以上服务器上。本文讲述是基于安装在3台vmware虚拟机上,各虚拟机机器结点如下表:
image.png
本节使用的所有vmware虚拟机配置均为CPU:8核,内存6G,硬盘足够。

根据我们一贯部署分布式服务的做法:
1) 先创建zk用户和zk组;

groupadd zk
vim /etc/group 会发现最后一行有zk用户组

adduser -g zk zk
创建zk用户,同时加入zk用户组,自动创建zk的homedir为/home/zk
vim /etc/passwd 可以看到最后一行是zk用户。

passwd zk
为zk用户新设立密码

将zk用户加入sudo权限
注意:本步骤非必须,可选。
chmod +w /etc/sudoers
vim /etc/sudoers
添加如下行:

image.png

然后再chmod -w /etc/sudoers

2) 创建zk服务的basedir:/opt/zk

chown -R zk:zk /opt/zk

3) 在/opt/zk下分别创建app data logs temp分别作为zk的app/data/logs/temp 目录。
image.png

4) 配置zk

解压zk压缩包文件:apache-zookeeper-3.6.1-bin.tar.gz 到/opt/zk/app目录下:
image.png
在cent7a机器上执行:

cd /opt/zk/app/apache-zookeeper-3.6.1-bin/conf
mv zoo_sample.cfg zoo.cfg
vim zoo.cfg 修改如下:

image.png
其中clientPort 2181是客户端连接zk集群的端口,dataDir和dataLogDir分别是数据目录和日志目录。文件最后的3行是用于zk集群互联。

server.A = B:C:D
A:zookeeper服务器的序号,即第几号服务器.
 注意这个序号要与zookeeper的myid保持一致
B:服务器的 IP 地址
C:服务器跟随者follower与集群中的 Leader 服务器交换信息的端口
D:如果集群中的 Leader 服务器宕机,需要一个端口通信重新进行选举,选出一个新的 Leader。这个端口就是用来做leader选举的端口
注意server.1/server.2/server.3 中的1/2/3是zk 结点的序号,不同结点必须不能相同。

直接将此zoo.cfg一行不用修改原样拷贝到cent7b和cent7c机器上相同目录下。
接下来在cent7a的datadir即:/opt/zk/data下新创建myid文件,并写入1:
image.png

同样地,在cent7b的datadir即:/opt/zk/data下新创建myid文件,并写入2:
image.png

在cent7c的datadir即:/opt/zk/data下新创建myid文件,并写入3:
image.png

至此,zk集群配置结束。启动zk集群之前不要忘记开放3台机器上2181/2888/3888 三个端口:

systemctl start firewalld
firewall-cmd --zone=public --add-port=2181/tcp --permanent
firewall-cmd --zone=public --add-port=2888/tcp --permanent
firewall-cmd --zone=public --add-port=3888/tcp --permanent
firewall-cmd --reload

image.png

Zk集群主要操作命令如下:

  • 服务端命令

在所有机器上执行:
/opt/zk/app/apache-zookeeper-3.6.1-bin/bin/zkServer.sh start/stop/status/restart ##启动/停止/查询状态/重启 zk服务
image.png
可见zk集群成功,1个leader和2个follower。

  • 客户端命令

在所有机器上执行:
/opt/zk/app/apache-zookeeper-3.6.1-bin/bin/zkCli.sh ##连接本地服务器,默认是2181端口
/opt/zk/app/apache-zookeeper-3.6.1-bin/bin/zkCli.sh -server ip:port ##连接指定zk服务器和端口

1.2.2.3. 交互式命令行使用

ZooKeeper是通过客户端脚本来操作的。客户端脚本:zkCli.sh,存放在ZooKeeper的bin目录下。
默认连接本地的ZooKeeper服务器:#zkCli.sh
连接指定的ZooKeeper服务器:#zkCli.sh –server Server IP:port
在cent7a上运行:
/opt/zk/app/apache-zookeeper-3.6.1-bin/bin/zkCli.sh -server 192.168.0.112:2181 ,显示如下:
执行:/opt/zk/app/apache-zookeeper-3.6.1-bin/bin/zkCli.sh -server 192.168.0.112:2181
image.png

命令行工具的一些简单操作如下:

1) 显示根目录下、文件: ls / 使用 ls 命令来查看当前 ZooKeeper 中所包含的内容
2) 显示根目录下、文件: ls2 / 查看当前节点数据并能看到更新次数等数据
3) 创建文件,并设置初始内容: create /zk "test" 创建一个新的 znode节点“ zk ”以及与它关联的字符串
4) 获取文件内容: get /zk 确认 znode 是否包含我们所创建的字符串
5) 修改文件内容: set /zk "zkbak" 对 zk 所关联的字符串进行设置
6) 删除文件: delete /zk 将刚才创建的 znode 删除
7) 退出客户端: quit
8) 帮助命令: help
1.2.2.4. Java API 使用

Zookeeper提供了丰富的java api 。后续可直接在附件的java工程中详见。

1.2.2.5. Zookeeper可视化工具 ZooInspector使用

Zookeeper 有很多可视化工具,其中一个轻便易用的工具是ZooInspector. 程序包是:ZooInspector.zip (该文件附于项目交付清单中) 。解压后 直接在windows上双击 ZooInspector/build/ zookeeper-dev-ZooInspector.jar 即可打开图形界面如下:
image.png

1.2.3. Mysql2Es构建全量索引

1.2.3.1. 架构设计

构建从mysql2es的全量索引数据基于一个基本的假设:_updatetime字段值只会不变或增大,永远不会减小。

构建全量索引之初,我们锁定当前时刻,即上节中的时刻S。我们要做的就是编程实现select 表中所有_updattime <= 时刻S的数据,然后add进入到es索引collegejob_position_v20200712_1(前节已经创建好)中去即可。

实现上述编程逻辑并不十分困难,但仍然需要商榷以下细节:

1) 如果全量数据规模十分庞大,单进程程序进行mysql选择数据并且构建全量索引到ES的过程可能会比较耗时,如果提高效率?可采用的方案是多进程实现。可以单击多进程,也可以多机多进程。当然,如果你觉得你的数据量小,没必要多进程时,我们的方案也可以适应。以上就是我们具体构建全量索引的重要原则:多进程构建全量索引。

2) 多进程实现时首先会遇到多个进程同时select mysql库,如何解决数据冲突? 要避免进程A已经select 出去的数据绝对不能子再别进程B再次select!如何解决?

解决方案是:分布式锁(Distributed locks)。分布式锁的主流实现有很多,本文采用zookeeper实现。关于zookeeper集群安装部署和简单实用本文前节中已经叙述过。

3) 最后一个细节需要知道的是,考虑到全量构建索引数据规模可能很大,全量阶段向es添加文档构建索引采用ES提供的bulk API,以提高构建效率。有关es bulk API更详细的说明可参考:https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html

1.2.3.2. Java编程实现

构建全量和增量索引数据的java编程实现工程都在附件文件:esproj.zip。其项目工程目录如下图:
image.png

配置文件:

  • c3p0-config.xml : 连接数据库配置文件;
  • log4j.properties: 日志配置文件
  • es.properties: es集群连接信息配置文件
  • esbuilder-config.properties: es索引构建配置文件(包含全量构建和增量(实时)构建配置)
  • EsFullBuilder 是全量索引构建主类;
  • EsInstBuilder 是增量(实时)索引构建主类;
  • EsRrecuritmentPositionSearchDemo 是招聘职位信息检索ES JAVA api demo。

下面重点解释一下esbuilder-config.properties文件的细节:
image.png

  • es.fullbuilder.version :一次全量构建索引的版本号,必须能区别于不同次构建
  • es.fullbuilder.dbname:数据库名
  • es.fullbuilder.dbname:数据库表名
  • es.fullbuilder.datasource:数据库中数据源范围的限定,必须是逗号分隔的3项,如:0,1594518666324,5000,含义是:_updatetime起始时间戳,_updatetime终止时间戳,每次从mysql最多选取多少条数据。0,1594518666324表示(0, 1594518666324],左开右闭,select:_updatetime > 0 and _updatetime <=1594518666324 limit 5000。1594518666324是13位数字,表示1970年1月1日到现在的毫秒数,即前文所述的时刻S。
  • es.fullbuilder.datasource.primarykey:数据库中主键字段名;
  • es.fullbuilder.sql:sql语句中select 后面 by前面的部分,常见的配置是*,表示全部字段都要往ES中建立索引。需要主要一点:默认数据库中字段名和es索引中字段名相同,如果某个字段不同名称,比如数据库中字段name1要建到es索引中字段名为name2,那么这里的sql应当为:name1 as name2。
  • es.fullbuilder.zkconnectinfo:zookeeper连接信息。注意:esfullbuilder是支持多机器分布式多进程并行构建索引,构建进度等信息需要通过zk来存取。本程序依赖zookeeper服务。
  • es.fullbuilder.esindexname:es索引名称;
  • es.fullbuilder.bulkactions:es bulk processor批量建索引参数:达到多少个request就往es发送给Es;
  • es.fullbuilder.bulksizemb: es bulk processor批量建索引参数:达到多少个字节就往es发送;
  • es.fullbuilder.bulkconcurrentrequests:es bulk processor批量建索引参数:开启多少个线程来建索引;
  • es.fullbuilder.bulkawaitminute:es bulk processor批量建索引参数:最长等待多少分钟就退出builder。

编译和运行方法:
在主目录下执行:mvn clean install -U 即可。会生成:

1)lib目录;
2)config目录;
3)esbuilder.jar。

其中lib是依赖库,config是包含esbuilder-config.properties等全部需要的配置文件。Esbuilder.jar是最终的jar包。
运行时,必须保证以上3个目录和文件在同一个目录下。
执行 java -cp esbuilder.jar com.freedom.es.es.main. EsFullBuilder 即可。
因为支持多机器分布式部署,建议使用时,在多个机器上同时运行。
本文时间是在cent7a cent7b cent7c 3台机器上同时运行上述命令,执行如下:
1) 初始:
image.png

2) 如上图,下面在3个机器上同时执行java -cp esbuilder.jar com.freedom.es.es.main.EsFullBuilder 命令,结果如下:
image.png
上图是执行过程中zk上树节点的图。能看到每个EsFullBuider会以自己所在机器IP:workdir的名称注册到zk上nodes节点下。data节点存放数据库_updatetime时间戳依次被处理后的剩余时间段范围。

3) 几分钟以后,执行结束。通过下面日志可到3个进程都把能从数据库获取的数据都获取了。执行结果去zk上看,如下:
image.png
所有的实例心跳结点都已经消失了,说明全部进程都退出了。每个实例上面的内容都是SUCCESS,表明全部都成功了。

再看下es上索引情况:image.png
数据库查询得出的记录条数也是45822,与ES索引文档数量一致,可见全量索引构建正确。

至此,全量索引构建过程全部结束。接下来到了构建增量实时索引过程。

1.3. 构建增量索引数据

构建增量索引是相对于构建全量索引而言的,一般指响应线上服务实时增加的文档流数据而构建的实时索引。

增量构建索引的数据源是一张mysql表,表示线上依次发生的实时流水数据,假定这张表的名称后缀都含有_instflow(实时流水的意思)。

对应招聘职位这一业务场景,如下创建其流水表:

CREATE TABLE `position_instflow` (
  `id` varchar(64) NOT NULL COMMENT '文档id',
  `doctype` int(11) NOT NULL COMMENT '文档类型,增删改分别对应数字0,1,2',
  `sequenceby3` int(11) NOT NULL COMMENT '文档id相对于总数3的序号',
  `sequenceby5` int(11) NOT NULL COMMENT '文档id相对于总数5的序号',
  `sequenceby7` int(11) NOT NULL COMMENT '文档id相对于总数7的序号',
  `_updatetime` bigint(20) NOT NULL COMMENT 'updated timestamp in epoch milliseconds',
  `handled` int(11) NOT NULL COMMENT '该文档是否被处理过',
  PRIMARY KEY (`id`) USING BTREE,
  KEY `idx_for_sequenceby3` (`sequenceby3`),
  KEY `idx_for_sequenceby5` (`sequenceby5`),
  KEY `idx_for_sequenceby7` (`sequenceby7`),
  KEY `idx_for_updatetime` (`_updatetime`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='招聘职位信息流水表';

image.png

其中:

  • id 是文档id,这里是varchar类型,可以是int类型,类型定义必须与全量数据表中的主键定义一致。数据也要一致。
  • doctype表示对流水数据的修改类型,取值0,1,2分别表示增,删,修改。
  • sequenceby3 表示当前文档主键id被hash成hash数值后求余3后的余数,sequenceby5,sequenceby7类型,表示求余5和7后的余数。该字段设计的思想是为了支持文档id分段:余数相同的文档落到同一个段上去,被同一个EsInstBuilder处理。这样设计就支持了多机器分布式多进程实时构建索引,能显著提高构建实时索引的效率。该字段预先就存好,并构建数据库索引,查询时速度能非常快。
  • _updatetime 是实时文档的更新时间戳,其内容必须与全量索引中的一致。
  • handled 字段表示是否被处理过,0未处理,1已处理。
  • 最后注意,为了加速mysql查询,idx_for_sequenceby3,idx_for_sequenceby5,idx_for_sequenceby7,idx_for_updatetime 都建了mysql索引。

如果说按照文档id分段是显著提高了实时索引构建效率,引入一个id段内支持多机器分布式多进程EsInstBuilder的策略,则显著提高了实时索引构建系统的安全性和可靠性。因为每个id段有多个EsInstBuilder进程,且都分布在多个机器上,如果其中的1个进程挂了,还有其它进程,并不影响实时构建流程。这两个策略是我们本文实时构建索引系统设计的精华和灵魂,也是亮点。

因为我们只存储了idx_for_sequenceby3、idx_for_sequenceby5、idx_for_sequenceby7,即表明我们目前只希望支持id分3/5/7段,更多的其他分段方案只需要修改流水表即可支持。本文实践以id分3段演示。
image.png
增量构建索引过程的配置文件仍然是同全量构建索引一个文件,如上图所示。

各参数解释如下:
image.png

上图是构建es索引的java 工程示意图,EsInstBuilder是主类和入口。Esbuilder-config.properties同上文所述,是配置文件。

编译方法还是mvn clean install -U 。
程序运行方法是:仍然将config、lib、esbuider.jar 3个文件或目录平行放置在某个目录下。然后运行java -cp esbuilder.jar com.freedom.es.es.main. EsInstBuilder 即可。

接下来,我们以id分3段,即es.instbuilder.buildercount设置为3,es.instbuilder.buildersequence分别为0,1,2。注意,同一个id段内我们设置了2个进程,共同消费同一个id段内的实时文档。部署分布如下图所示:
image.png

上图可见3个分段,buildersequence分别要设置成为0,1,2,同一个分段内有2个进程。
再看看zk上树形结构分布如下图所示:
image.png
目前是空的。

再看看ES索引文档情况,如下图:
image.png
目前流水表是空的。

接下来,我们分别在3个机上的以命令:java -cp esbuilder.jar com.freedom.es.es.main. EsInstBuilder 启动6个EsInstBuilder进程,如下图:
image.png
由上图可见,zk上目前有6个节点代表每个进程的心跳结点,在分别共同争抢3把锁,有序地进行着消费流水数据的过程。因为目前流水表中是空的,所以6个进程就在空转。

接下来为了模拟线上实时数据,我们利用java工程中的com.freedom.es.util.TmpInstImportData主类的功能来从全量数据表中抽取所有_updatetime>1594518666324时刻S的所有文档,动态每个40毫秒插入一条到实时流水表中:jyfw_recruitment_position_instflow。

执行java -cp esbuilder.jar com.freedom.es.util. TmpInstImportData ,它会每40毫秒插入一条实时数据到流水表中,如下图所示:
image.png
可见这边在插入流水数据过程中了。

再看看EsInstBuilder这边的日志,如下图:
image.png
可见都分别在有序地消费流水数据了。注意图中的耗时10秒是程序为了演示过程特地sleep 10秒每一轮,实际中消费流水速度都是非常快的。

再看看流水表中:
image.png
流水表中的handled字段为0表示没有被消费,为1表示已经被消费了。图中全部为1可见这些流水数据都被建入到ES索引中去了。

再看看此时ES索引中文档的情况,如下图所示:
image.png
可见es索引中文档数据在增加,从45822已经增加到了52481,这个数字还在不断增大。

生产环境下,上述6个EsInstBuilder要一直启动着,永远不能停止,以保证远远不断消费线上实时来的流水数据,再构建到ES索引中去。这个过程是没有尽头的。

至此,构建增量索引过程已经完毕。接下来,也是最后,我们给出一个java api 查询es服务的API demo,供生产环境使用。

1.4. ES查询JAVA API demo

image.png
Es提供了丰富的java API 用于搜索查询。上图是java工程中给出的一个简单的java API demo,它展示了检索 position_description中含有“自然语言”这个词的检索召回结果。可见一共有匹配52条,要求召回了top10条。

更详细丰富的ES java search API使用方法,可根据需要参见官方ES文档:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/_search_apis.html

你可能感兴趣的