使用netstat -lntp来看看有侦听在网络某端口的进程。当然,也可以使用 lsof。

Elasticsearch 2.x mapping tips

elasticsearch 2.x mapping tips

作者:杨振涛  首发于:Elasticsearch 中文社区  日期:2017-1-10

如果把elasticsearch中的mapping类比为关系型数据库中的schema的话,那么我们可能重点强调了两者之间的共性,而忽略了elasticsearch里mapping很不相同的部分 —— 这恰恰是实践中最容易被坑的地方。这里总结了几点实践中的小心得,希望对你所有帮助。

mapping 基础
创建索引库index
curl -XPOST "http://192.168.9.19:9200/vivo_vimc"

查看指定索引库的mapping:



curl -XGET "http://192.168.9.19:9200/vivo_ ... ot%3B
 


PS: 这时你获得的结果为空,因为刚建的库,没有mapping信息。

创建索引类型type并指定mapping :
curl -XPOST http://192.168.9.19:9200/vivo_vmic/apps/_mapping -d '{
"apps" : {
"properties" : {
"appName" : {
"type" : "string",
"index" : "not_analyzed",
"fields" :{
"cn": {
"type" : "string",
"index" : "analyzed",
"analyzer": "ik"
},
"en": {
"type" : "string",
}
},
"store":"yes"
},
"status" : {
"type" : "boolean"
},
"type" : {
"type" : "integer"
},
"onsaleDate" : {
"type" : "date"
},
}
}
}'

更新mapping (只能增加字段,不能删除字段,也不能修改字段类型,或者说无法增加一个不同类型的同名字段):

增加属性 score:
curl -XPOST "http://192.168.9.19:9200/vivo_ ... ot%3B -d '{
"apps": {
"properties": {
"score":{
"type":"float"
}
}
}
}'
   
更新成功会返回:
{
"acknowledged" : true
}


删除mapping :
2.4版本开始ES已经不支持mapping的删除了。

tip1 dynamic 模式

动态mapping是ES的一个重要特性,这个配置的可选值及含义如下:
  • true  :支持动态扩展,新增数据有新的属性时,自动添加,索引成功
  • false :不支持动态扩展,新增数据有新的属性时,直接忽略,索引成功
  • strict: 不支持动态扩展,新增数据有新的属性时,会报错,索引失败



tip2 主要数据类型及注意事项
  • string

    分词和不分词的值都需要,中英文都需要 ,
    长度截取,超长过滤 ,
    大小写问题(不分词时索引数据不会转小写,搜索都会转小写)    
    analyzer: analyzed, not_analyzed, no(表示该属性不能用来做搜索和聚合)
    properties : .raw, .en/.cn
    
  • date :           如果不明确指定,那么默认的date格式是:"strict_date_optional_time||epoch_millis",这是官网的表述,意思是可以是一个字符串类型的输入,也可以是数值类型的输入,前者可以是日期或者日期加上时间,后者则是毫秒数。关于时区信息:不管业务上是否需要时区信息,我们建议依然保存,以防万一。另外,data类型在明确指定 format 参数时,也有很多坑,对于format: epoch_second, epools_millis ,如果你想用来排序,那么为了性能,我们强烈建议你使用 epoc_second,差距很大哟,你可以亲自做一个对比测试。

 
  •  long, integer, short, byte, double ,float 希望此类字段参与搜索和聚合的话,就不能设置not_analyzed。

 
  • boolean, binaryboolean类型比较特殊,在ES里面只定义了false类的值( false, "false", "off", "no", "0", "" , 0, 0.0 ),其他所有都认为是true。实践中,我们建议优先使用 0(编程和性能友好),其次使用 true(兼容json默认的类型)。

 
  •  ipv4 type:ip 日志分析等最常用的数据类型,注意这里的是ipv4,ipv6目前暂不支持(ES 2.x);赋值时其实传递的是字符串,但ES内部其实保存的是一个long类型。

 
  • geo type:geo_point , type:geo_shape  LBS服务的必选数据类型,但不建议完全依赖此特性,业务层面要尽可能地缩小范围,或者在使用围栏类功能时,只要业务容忍,使用正方形代替圆形。

 
  • 数组,对象,内嵌将一个复杂对象放在一个属性中,其中数组最常用。

 
  • completion主要是用来做自动完成和拼写纠错的。



tip3 id设置  

在不设置id的情况下,默认的ES会给一个类似HASH串的随机ID;如果业务上需要且可以保证索引数据的唯一性,也可以使用业务ID作为索引ID,好处就是可以根据业务ID轻松地GET到索引数据,而无需维护索引ID和业务ID的关系。

同时,设置mapping的时候也可以指定ID的生成策略,比如UUID:
curl -s -XPUT http://192.168.9.19:9200/vivo_vimc -d '
{
"mappings": {
"apps": {
"_id": {
"path": "uuid"
},
"properties": {
"cnName": {
"type": "string",
"index": "analyzed"
}
}
}
}
}'


tip4 index和type规划

index的别名这个特性就不再强调了,不管是否用到,第一时间设置别名是最佳实践! schema 比较相似的type,放在同一个index里;schema差异非常大的type,建议放在不同的index里;原因是跟搜索引擎的segment以及lucene有关,本质上同一个index里的type底层是同样的存储结构,差异越大意味着type a的属性在type b里大部分都是空值,那么最终会得到一个非常稀疏的矩阵,影响计算效率并浪费存储空间。

关于滚动index的问题,对于日志类的搜索应用,按天或其他维度做滚动index是非常好必要的,这样可以更好地区分冷热数据。比如:


index                        alias
vivo_appstore_log_20160108  
vivo_appstore_log_20160109  vivo_appstore_log
vivo_appstore_log_20160110  vivo_appstore_log
vivo_appstore_log_20160111  vivo_appstore_log
...



如果只需要查询最近3天的数据,那么只需要对3天前的index remove alias即可,然后每天循环滚动。一个细节是,对于这种场景下的索引,写入的时候必须使用原始的index name,而不能使用alias;查询的时候则使用alias。


另一个问题,就是index容量的规划,副本数直接决定需要多少冗余空间;另外,索引数据本身也会有膨胀的现象,尤其是基于中文的全文搜索应用,term集可能会比较大。比如有10000个docs,占用100MB空间时,并不能简单认为100000个docs就占用约1GB。


tip5 测试分词器

如果使用的是基于词典的分词器,比如IK这类,那么线上系统可能会需要按需添加自定义词,或者同义词等,技术上我们可以暴露该类功能给搜索引擎运营人员使用。所以,需要提供一个测试分词器的接口,方便对比和验证。ES默认就提供这样的REST接口的。

按指定分词器分词指定文本:
GET /vivo_vimc/apps/_analyze?text=Hello, vivo 移动互联网&analyzer=ik

按指定索引库的属性测试分词效果:
GET /vivo_vimc/apps/_analyze
{
"field": "appName",
"text": "Pokemon Go"
}

以上关于 mapping 的几点心得,并非金科玉律,需要根据不同的业务需求场景来区别分析和应对。如果你有更多心得,欢迎回复本文分享。


关于作者:
杨振涛,vivo移动互联网 搜索架构师,关注实时搜索,搜索广告,以及大数据的存储、索引、搜索和可视化。
继续阅读 »
elasticsearch 2.x mapping tips

作者:杨振涛  首发于:Elasticsearch 中文社区  日期:2017-1-10

如果把elasticsearch中的mapping类比为关系型数据库中的schema的话,那么我们可能重点强调了两者之间的共性,而忽略了elasticsearch里mapping很不相同的部分 —— 这恰恰是实践中最容易被坑的地方。这里总结了几点实践中的小心得,希望对你所有帮助。

mapping 基础
创建索引库index
curl -XPOST "http://192.168.9.19:9200/vivo_vimc"

查看指定索引库的mapping:



curl -XGET "http://192.168.9.19:9200/vivo_ ... ot%3B
 


PS: 这时你获得的结果为空,因为刚建的库,没有mapping信息。

创建索引类型type并指定mapping :
curl -XPOST http://192.168.9.19:9200/vivo_vmic/apps/_mapping -d '{
"apps" : {
"properties" : {
"appName" : {
"type" : "string",
"index" : "not_analyzed",
"fields" :{
"cn": {
"type" : "string",
"index" : "analyzed",
"analyzer": "ik"
},
"en": {
"type" : "string",
}
},
"store":"yes"
},
"status" : {
"type" : "boolean"
},
"type" : {
"type" : "integer"
},
"onsaleDate" : {
"type" : "date"
},
}
}
}'

更新mapping (只能增加字段,不能删除字段,也不能修改字段类型,或者说无法增加一个不同类型的同名字段):

增加属性 score:
curl -XPOST "http://192.168.9.19:9200/vivo_ ... ot%3B -d '{
"apps": {
"properties": {
"score":{
"type":"float"
}
}
}
}'
   
更新成功会返回:
{
"acknowledged" : true
}


删除mapping :
2.4版本开始ES已经不支持mapping的删除了。

tip1 dynamic 模式

动态mapping是ES的一个重要特性,这个配置的可选值及含义如下:
  • true  :支持动态扩展,新增数据有新的属性时,自动添加,索引成功
  • false :不支持动态扩展,新增数据有新的属性时,直接忽略,索引成功
  • strict: 不支持动态扩展,新增数据有新的属性时,会报错,索引失败



tip2 主要数据类型及注意事项
  • string

    分词和不分词的值都需要,中英文都需要 ,
    长度截取,超长过滤 ,
    大小写问题(不分词时索引数据不会转小写,搜索都会转小写)    
    analyzer: analyzed, not_analyzed, no(表示该属性不能用来做搜索和聚合)
    properties : .raw, .en/.cn
    
  • date :           如果不明确指定,那么默认的date格式是:"strict_date_optional_time||epoch_millis",这是官网的表述,意思是可以是一个字符串类型的输入,也可以是数值类型的输入,前者可以是日期或者日期加上时间,后者则是毫秒数。关于时区信息:不管业务上是否需要时区信息,我们建议依然保存,以防万一。另外,data类型在明确指定 format 参数时,也有很多坑,对于format: epoch_second, epools_millis ,如果你想用来排序,那么为了性能,我们强烈建议你使用 epoc_second,差距很大哟,你可以亲自做一个对比测试。

 
  •  long, integer, short, byte, double ,float 希望此类字段参与搜索和聚合的话,就不能设置not_analyzed。

 
  • boolean, binaryboolean类型比较特殊,在ES里面只定义了false类的值( false, "false", "off", "no", "0", "" , 0, 0.0 ),其他所有都认为是true。实践中,我们建议优先使用 0(编程和性能友好),其次使用 true(兼容json默认的类型)。

 
  •  ipv4 type:ip 日志分析等最常用的数据类型,注意这里的是ipv4,ipv6目前暂不支持(ES 2.x);赋值时其实传递的是字符串,但ES内部其实保存的是一个long类型。

 
  • geo type:geo_point , type:geo_shape  LBS服务的必选数据类型,但不建议完全依赖此特性,业务层面要尽可能地缩小范围,或者在使用围栏类功能时,只要业务容忍,使用正方形代替圆形。

 
  • 数组,对象,内嵌将一个复杂对象放在一个属性中,其中数组最常用。

 
  • completion主要是用来做自动完成和拼写纠错的。



tip3 id设置  

在不设置id的情况下,默认的ES会给一个类似HASH串的随机ID;如果业务上需要且可以保证索引数据的唯一性,也可以使用业务ID作为索引ID,好处就是可以根据业务ID轻松地GET到索引数据,而无需维护索引ID和业务ID的关系。

同时,设置mapping的时候也可以指定ID的生成策略,比如UUID:
curl -s -XPUT http://192.168.9.19:9200/vivo_vimc -d '
{
"mappings": {
"apps": {
"_id": {
"path": "uuid"
},
"properties": {
"cnName": {
"type": "string",
"index": "analyzed"
}
}
}
}
}'


tip4 index和type规划

index的别名这个特性就不再强调了,不管是否用到,第一时间设置别名是最佳实践! schema 比较相似的type,放在同一个index里;schema差异非常大的type,建议放在不同的index里;原因是跟搜索引擎的segment以及lucene有关,本质上同一个index里的type底层是同样的存储结构,差异越大意味着type a的属性在type b里大部分都是空值,那么最终会得到一个非常稀疏的矩阵,影响计算效率并浪费存储空间。

关于滚动index的问题,对于日志类的搜索应用,按天或其他维度做滚动index是非常好必要的,这样可以更好地区分冷热数据。比如:


index                        alias
vivo_appstore_log_20160108  
vivo_appstore_log_20160109  vivo_appstore_log
vivo_appstore_log_20160110  vivo_appstore_log
vivo_appstore_log_20160111  vivo_appstore_log
...



如果只需要查询最近3天的数据,那么只需要对3天前的index remove alias即可,然后每天循环滚动。一个细节是,对于这种场景下的索引,写入的时候必须使用原始的index name,而不能使用alias;查询的时候则使用alias。


另一个问题,就是index容量的规划,副本数直接决定需要多少冗余空间;另外,索引数据本身也会有膨胀的现象,尤其是基于中文的全文搜索应用,term集可能会比较大。比如有10000个docs,占用100MB空间时,并不能简单认为100000个docs就占用约1GB。


tip5 测试分词器

如果使用的是基于词典的分词器,比如IK这类,那么线上系统可能会需要按需添加自定义词,或者同义词等,技术上我们可以暴露该类功能给搜索引擎运营人员使用。所以,需要提供一个测试分词器的接口,方便对比和验证。ES默认就提供这样的REST接口的。

按指定分词器分词指定文本:
GET /vivo_vimc/apps/_analyze?text=Hello, vivo 移动互联网&analyzer=ik

按指定索引库的属性测试分词效果:
GET /vivo_vimc/apps/_analyze
{
"field": "appName",
"text": "Pokemon Go"
}

以上关于 mapping 的几点心得,并非金科玉律,需要根据不同的业务需求场景来区别分析和应对。如果你有更多心得,欢迎回复本文分享。


关于作者:
杨振涛,vivo移动互联网 搜索架构师,关注实时搜索,搜索广告,以及大数据的存储、索引、搜索和可视化。 收起阅读 »

《Elasticsearch 权威指南》中文版

es-guide.gif

 在几十位社区同学的共同努力下,《Elasticsearch 权威指南》的翻译工作接近尾声,
在线访问链接如下:
http://es-guide-preview.elasticsearch.cn
 
晚点会放到 elastic.co 官网上,大家学习 Elasticsearch 又多了一份好的资料,大家在访问的过程,如果发现有问题(翻译的各种 bug,翻译有误,不合理,不通顺,标点,格式等等),欢迎前往  https://github.com/elasticsear ... guide 提交 Issue,同时也欢迎直接提交 pull request 来改进本书。
 
同时也希望更多的志愿者加入我们一起进行翻译,后续我们会继续翻译其他的手册,另外有很多同学自己已经在翻译部分内容,也欢迎加入我们一起,有兴趣的同学加入我们翻译的QQ群:109764489 ,一起为 Elastic 的中文资料贡献力量。

最后,再次感谢以下本书的志愿者:
薛杰,骆朗,彭秋源,魏喆,饶琛琳, 风虎,路小磊,michealzh,nodexy,sdlyjzh,落英流离, sunyonggang,Singham,烧碱,龙翔,陈思,陈华, 追风侃侃,Geolem,卷发,kfypmqqw,袁伟强,yichao, 小彬,leo,tangmisi,Alex,baifan,Evan,fanyer, wwb,瑞星,刘碧琴,walker,songgl, 吕兵,东,杜宁,秦东亮,biyuhao,刘刚, yumo,王秀文,zcola,gitqh,blackoon,David,韩炳辰, 韩陆,echolihao,Xargin,abel-sun,卞顺强, bsll,冬狼,王琦。
 
继续阅读 »
es-guide.gif

 在几十位社区同学的共同努力下,《Elasticsearch 权威指南》的翻译工作接近尾声,
在线访问链接如下:
http://es-guide-preview.elasticsearch.cn
 
晚点会放到 elastic.co 官网上,大家学习 Elasticsearch 又多了一份好的资料,大家在访问的过程,如果发现有问题(翻译的各种 bug,翻译有误,不合理,不通顺,标点,格式等等),欢迎前往  https://github.com/elasticsear ... guide 提交 Issue,同时也欢迎直接提交 pull request 来改进本书。
 
同时也希望更多的志愿者加入我们一起进行翻译,后续我们会继续翻译其他的手册,另外有很多同学自己已经在翻译部分内容,也欢迎加入我们一起,有兴趣的同学加入我们翻译的QQ群:109764489 ,一起为 Elastic 的中文资料贡献力量。

最后,再次感谢以下本书的志愿者:
薛杰,骆朗,彭秋源,魏喆,饶琛琳, 风虎,路小磊,michealzh,nodexy,sdlyjzh,落英流离, sunyonggang,Singham,烧碱,龙翔,陈思,陈华, 追风侃侃,Geolem,卷发,kfypmqqw,袁伟强,yichao, 小彬,leo,tangmisi,Alex,baifan,Evan,fanyer, wwb,瑞星,刘碧琴,walker,songgl, 吕兵,东,杜宁,秦东亮,biyuhao,刘刚, yumo,王秀文,zcola,gitqh,blackoon,David,韩炳辰, 韩陆,echolihao,Xargin,abel-sun,卞顺强, bsll,冬狼,王琦。
  收起阅读 »

2017年学习内容

  • 重新看lucene源码
  • 看es源码
  • 对比lucene和es
  • 基于lucene实现自己的搜索框架

  • 重新看lucene源码
  • 看es源码
  • 对比lucene和es
  • 基于lucene实现自己的搜索框架

Elastic{ON}DevChina2016 资料下载

本月10号在北京的第一次 Elastic 中国开发者大会的资料现在已经可以下载了,没有去现场的或者去现场的还想回顾的都可以去看看,欢迎大家踊跃交流讨论!
 
https://pan.baidu.com/s/1slbCZ9b

 
继续阅读 »
本月10号在北京的第一次 Elastic 中国开发者大会的资料现在已经可以下载了,没有去现场的或者去现场的还想回顾的都可以去看看,欢迎大家踊跃交流讨论!
 
https://pan.baidu.com/s/1slbCZ9b

  收起阅读 »

无外网环境10分钟快速集成 elasticsearch-head

一台连上外网的机器
1.下载对应的 node 安装包

2.下载 Elasticsearch-head 安装包(顺便解压)

3.cmd 输入下面命令安装taobao提供的镜像(原因你懂的)
$ npm install -g cnpm --registry=https://registry.npm.taobao.org
 
4.cmd cd 到你解压的 elasticsearch-head 目录下
$ cnpm install
这时你会发现多了一个 node_modules 目录

5.install 后,head 目录下会自动生成一个 node_modules 目录,里面为相关的依赖

到此,本地的准备工作都已完成。
可以把两个安装包上传到服务器。
建议:elasticsearch-head 重新压缩后上传
 
/etc/profile 添加(改成你的路径)
export NODE_HOME=/usr/local/elastic/node/node-v6.9.2-linux-x64
export PATH=$NODE_HOME/bin:$PATH
记得
$ source /etc/profile

在 elastic/config/elasticsearch.yml 添加(否则完成后网页中会显示未连接)
http.cors.enabled: true
http.cors.allow-origin: "*"

之后(后台启动加 &)
./grunt server 

在网页中输入你的http://localhost:9100
继续阅读 »
一台连上外网的机器
1.下载对应的 node 安装包

2.下载 Elasticsearch-head 安装包(顺便解压)

3.cmd 输入下面命令安装taobao提供的镜像(原因你懂的)
$ npm install -g cnpm --registry=https://registry.npm.taobao.org
 
4.cmd cd 到你解压的 elasticsearch-head 目录下
$ cnpm install
这时你会发现多了一个 node_modules 目录

5.install 后,head 目录下会自动生成一个 node_modules 目录,里面为相关的依赖

到此,本地的准备工作都已完成。
可以把两个安装包上传到服务器。
建议:elasticsearch-head 重新压缩后上传
 
/etc/profile 添加(改成你的路径)
export NODE_HOME=/usr/local/elastic/node/node-v6.9.2-linux-x64
export PATH=$NODE_HOME/bin:$PATH
记得
$ source /etc/profile

在 elastic/config/elasticsearch.yml 添加(否则完成后网页中会显示未连接)
http.cors.enabled: true
http.cors.allow-origin: "*"

之后(后台启动加 &)
./grunt server 

在网页中输入你的http://localhost:9100 收起阅读 »

Day 14: Elasticsearch 5 入坑指南

尝鲜

10月26日,Elasticsearch5.0.0 GA终于放出,携程ES Ops团队也在第一时间在DEV和UAT环境分别进行了2.4.0 至5.0.0的升级和测试。升级完成后,除了部分Query不向前兼容(主要是Filtered Query),需要在应用端做一些修改以外,未发现其他问题。通过监控系统看对比升级前后的主要系统指标,在同等索引量的情况下,CPU使用率有明显下降 ( 30% - 50%左右) ,相信性能方面5.0应该是有较大提升的。 

在测试环境稳定运行了2周以后,我们决定选定一个生产集群进行升级,考验新版本在更为复杂的用户环境下的表现。 出于对业务影响最小化的考虑,用于日志分析的集群被圈定为升级目标。该集群也是携程十几个集群中规模最大的一个,共有120个数据结点运行于70台物理机上,总数据量接近1PB。

升级前需要做一些准备工作,下载官方的Migration Helper插件,检查集群设置和索引的兼容性。对于不兼容的配置项,MH会详尽列出,其中标注为红色部分为为升级前必须修改项。1.x版本创建的索引,是无法直接升级到5的,需要先在2.x集群里做一次reindex 。 MH提供了不兼容索引扫描功能,对于找到的不兼容索引,可以直接在UI上发起reindex操作,等待结束即可。 如果是用于业务搜索集群,数据可能比较重要,建议升级前做一个Snapshot,万一升级过程出现意外,可以回退版本从备份里快速恢复数据。我们的日志集群数据量极大,也没有对数据100%不丢的要求,因此升级前没有做Snapshot。 做完所有的准备工作后,预先通知所有用户集群升级的时间以及可能产生的影响,选定了周五深夜用户低峰期,开始正式升级工作。 

首先通过Ansible将新版本批量部署到所有结点并统一配置,紧接着对原有集群做了Full Stop,校验所有的ES已经停下后,开始Full Start。整个过程比较顺利,所有结点正常启动,数据恢复完成后,集群重新回到正常服务状态。

周末两天运行,未发现有任何的异样,CPU利用率也降了不少,看起来很靠谱……直到周一


踏坑

周一早上,随着用户访问量高峰来临,马上浮现出一个诡异的现象: 索引速率遇到了瓶颈,数据开始在前置的消息队列(Kafka)里堆积。 从监控数据看,尽管所有的数据结点CPU消耗都比上周同期低,磁盘IO也很低,但索引速率却低了很多。反复对比查看升级前后各类监控指标后,终于发现一个可疑点,所有结点的网络流量比升级前高了好几倍!  在集群架构上,我们是单独架设了几台client node做为数据写入和分发的入口,现在这几个node的网络流量已经饱和,成为数据写入的瓶颈。一开始,怀疑是否2.4启用了tcp压缩,而5.0取消了,但翻查官方文档后发现transport.tcp.compress在2.4和5.0里默认都是关闭的! 这时候只有两个解决办法了,要么启用tcp压缩,要么扩容client node。 先考虑了一下tcp压缩的方案,快速扒了一下ES源码,在transport.TcpTransport这个类里,sendRequest和sendResponse两个方法会根据transport.tcp.compress设置来决定发送的消息是否要经过压缩,而在messageReceived方法则会读取消息头部的状态信息,探测消息是否经过压缩以及压缩的方法,而后决定是否需要解压,以及采用的解压方式。 这样看起来,ES是允许tcp压缩和不压缩的结点之间通讯的,那么只对client node启用压缩应该就可以了。测试环境测试过后,验证了想法的可行性。于是对生产的client node开启tcp压缩,同时在数据发送端(hangout的ES output)也启用tcp压缩,重启client node后入口网络流量降到和之前2.4差不多的程度,问题得到规避。 针对这个问题在Github上提交了issue https://github.com/elastic/ela ... 21612, 但未得到官方合理的解释。

解决好这个问题,另外一个问题来了,很多执行大量历史数据搜索的用户反映出不了结果。 从监控数据看,这类查询的搜索耗时非常久,直到网关300秒超时(查询api前置的nginx代理)。我们之前对集群设置过Global Search timeout为60s,用来保护集群资源过多被超高代价的查询消耗,在2.4版本是有效果的,现在看来不起作用了。手动测试了一下,这个参数果然失效! 于是向官方报告了第2个问题:https://github.com/elastic/ela ... 21595 。 这个问题很快被官方确认为Bug,修复也很快加入到了5.0.2。 为了规避这个问题,我们只好临时修改了一下Kibana以及第三方API访问要经过的nginx proxy,默认为所有的search request加入一个超时选项。此后,问题有一些缓解,但仍然发现用户查询大范围历史数据时,部分用于存储历史数据的结点响应很慢。

我们的集群是做了冷热分离的结构的,热节点主要承担写入和存放过去24小时数据,冷结点没有写入,查询频率也低,所以为了最大化利用硬件资源,一台物理机上跑了3个实例,这样一台128GB内存的机器可以存放下近30TB的索引。查看冷结点的监控数据,看到用户查询期间磁盘的read IO非常高,直接将磁盘IO Util%撑到100%,并且可持续数小时,同时search thread pool有大量的active thread处于无法完成状态,search queue不断攀升直至饱和、开始reject。 表象上看search thread似乎一直在尝试从磁盘大量读取数据,一次search甚至可以持续几十分钟至一个小时,耗尽了所有的搜索线程,导致拒绝后续的搜索服务。 于是Github上报了第3个issue: https://github.com/elastic/ela ... 21611  这个问题找到解决办法之前,我们只能通过反复重启有问题的冷结点来缓解。 和官方讨论过程中,得知5.0在Lucene文件访问方式上有一个比较大的改动,2.4使用mmapfs读取索引文件的部分,而5.0以后改为用mmapfs读取索引文件的全部。怀疑问题和这个变动有关,尝试将所有索引文件的设置改为NIOFS后,问题迎刃而解。 搜索性能一下回到了2.4时代,再也没出现搜索线程超长时间执行的问题。之后找时间复现了这个问题,并抓取了线程栈,看到长时间执行的搜索线程一直在做Global Ordinal的构造工作。 至于为何会这样,还不清楚。 从官方给出的信息看,底层索引文件的访问模式是没有变化的,仅仅是将文件读取方式全部改成了mmapfs,理论上应该性能更好,但是看起来在我们这种一台机器跑多个ES实例,所有分配的heap为系统缓存3倍的极端用例下,大范围的数据搜索可能造成过高的磁盘读IO,集群性能指数级下降。

以上问题前后耗了4天才完全规避掉,支持团队连续熬夜后集群总算回复到平稳状态。然而好景不长,运行一段时间以后,数据结点出现疑似内存泄漏现象。结点总数据没怎么增加、甚至还有减少的情况下,heap使用率一只呈攀升趋势,Old GC无法回收内存。这个问题对用户影响较小,通过监控我们可以及时发现内存即将用尽的结点,做一次重启很快就恢复了。 为排查根源,我们对一个有问题的结点做了dump,通过MAT工具分析,看到meta data相关的一个alias对象被实例化了有6600万次之多! 在Github上提交了第四个issue: https://github.com/elastic/ela ... 22013,不多久被确认为已知问题https://github.com/elastic/ela ... 21284 ,在5.0.1已经修复。

最后还存在一个master node内存泄漏的问题,这个问题在2.4.0时代就存在了,升级到5.0.0以后依然没有修复。由于我们的master node和data node是分离的,所以这个问题比较容易通过监控发现,解决方式也很简单和迅速,重启master node即可,对用户完全无影响。之后不久,5.0.2版本正式发布,release notes里提到了对这个问题的修复 https://github.com/elastic/ela ... 21578

上周周末我们将集群rolling upgrade到了5.0.2,global search timeout失效和两个内存泄漏的问题从根源上解决掉了。 网络流量增大的问题依然存在,仍然需要通过启用client结点的transport.tcp.compress规避。 冷结点搜索性能的问题没看到有提及,估计没解决,安全起见,还是保持索引的文件系统为NIOFS。升级完成运行一段时间后,可以肯定,5.0.2已经比较稳定。


心得

升到5.0.2后,对于其中一组数据结点这两天特意加了点索引负载,通过监控数据将v5.0.2与2.4.0做实际运行环境的索引吞吐量对比。

2.4_.png


5.0_.png

 
在近似的CPU使用率和load情况下,5.0.2能够支撑更大的吞吐量。另外5.0带来的Instant aggregation功能,对于跨多个索引的时序类型数据的聚合也可以有效Cache了,在使用Kibana的时候提速感觉非常明显。

升级过程虽然遇到很多波折,但由于集群架构上做了角色分离(client,master,data)和冷热分离,因而Bug引起的故障比较容易被限定在一个较小的范围而不至于影响所有的功能和所有的用户。 故障点定位更加容易,规避措施也更容易实施。 部分规避措施实施过程中甚至对用户是完全无影响的,比如: 重启内存泄漏的master node)。详尽的监控为问题的发现和诊断提供了有力的支持。

Elasticsearch是非常复杂的系统,官方的测试无法覆盖所有的用例场景和数据规模,一些极端的应用场景可能触发某个深藏的Bug或者缺陷而陷入困境。 因此对于稳定性要求极高的应用,最好还是采用经过长时间考验的版本,比如v2.4.2。
继续阅读 »
尝鲜

10月26日,Elasticsearch5.0.0 GA终于放出,携程ES Ops团队也在第一时间在DEV和UAT环境分别进行了2.4.0 至5.0.0的升级和测试。升级完成后,除了部分Query不向前兼容(主要是Filtered Query),需要在应用端做一些修改以外,未发现其他问题。通过监控系统看对比升级前后的主要系统指标,在同等索引量的情况下,CPU使用率有明显下降 ( 30% - 50%左右) ,相信性能方面5.0应该是有较大提升的。 

在测试环境稳定运行了2周以后,我们决定选定一个生产集群进行升级,考验新版本在更为复杂的用户环境下的表现。 出于对业务影响最小化的考虑,用于日志分析的集群被圈定为升级目标。该集群也是携程十几个集群中规模最大的一个,共有120个数据结点运行于70台物理机上,总数据量接近1PB。

升级前需要做一些准备工作,下载官方的Migration Helper插件,检查集群设置和索引的兼容性。对于不兼容的配置项,MH会详尽列出,其中标注为红色部分为为升级前必须修改项。1.x版本创建的索引,是无法直接升级到5的,需要先在2.x集群里做一次reindex 。 MH提供了不兼容索引扫描功能,对于找到的不兼容索引,可以直接在UI上发起reindex操作,等待结束即可。 如果是用于业务搜索集群,数据可能比较重要,建议升级前做一个Snapshot,万一升级过程出现意外,可以回退版本从备份里快速恢复数据。我们的日志集群数据量极大,也没有对数据100%不丢的要求,因此升级前没有做Snapshot。 做完所有的准备工作后,预先通知所有用户集群升级的时间以及可能产生的影响,选定了周五深夜用户低峰期,开始正式升级工作。 

首先通过Ansible将新版本批量部署到所有结点并统一配置,紧接着对原有集群做了Full Stop,校验所有的ES已经停下后,开始Full Start。整个过程比较顺利,所有结点正常启动,数据恢复完成后,集群重新回到正常服务状态。

周末两天运行,未发现有任何的异样,CPU利用率也降了不少,看起来很靠谱……直到周一


踏坑

周一早上,随着用户访问量高峰来临,马上浮现出一个诡异的现象: 索引速率遇到了瓶颈,数据开始在前置的消息队列(Kafka)里堆积。 从监控数据看,尽管所有的数据结点CPU消耗都比上周同期低,磁盘IO也很低,但索引速率却低了很多。反复对比查看升级前后各类监控指标后,终于发现一个可疑点,所有结点的网络流量比升级前高了好几倍!  在集群架构上,我们是单独架设了几台client node做为数据写入和分发的入口,现在这几个node的网络流量已经饱和,成为数据写入的瓶颈。一开始,怀疑是否2.4启用了tcp压缩,而5.0取消了,但翻查官方文档后发现transport.tcp.compress在2.4和5.0里默认都是关闭的! 这时候只有两个解决办法了,要么启用tcp压缩,要么扩容client node。 先考虑了一下tcp压缩的方案,快速扒了一下ES源码,在transport.TcpTransport这个类里,sendRequest和sendResponse两个方法会根据transport.tcp.compress设置来决定发送的消息是否要经过压缩,而在messageReceived方法则会读取消息头部的状态信息,探测消息是否经过压缩以及压缩的方法,而后决定是否需要解压,以及采用的解压方式。 这样看起来,ES是允许tcp压缩和不压缩的结点之间通讯的,那么只对client node启用压缩应该就可以了。测试环境测试过后,验证了想法的可行性。于是对生产的client node开启tcp压缩,同时在数据发送端(hangout的ES output)也启用tcp压缩,重启client node后入口网络流量降到和之前2.4差不多的程度,问题得到规避。 针对这个问题在Github上提交了issue https://github.com/elastic/ela ... 21612, 但未得到官方合理的解释。

解决好这个问题,另外一个问题来了,很多执行大量历史数据搜索的用户反映出不了结果。 从监控数据看,这类查询的搜索耗时非常久,直到网关300秒超时(查询api前置的nginx代理)。我们之前对集群设置过Global Search timeout为60s,用来保护集群资源过多被超高代价的查询消耗,在2.4版本是有效果的,现在看来不起作用了。手动测试了一下,这个参数果然失效! 于是向官方报告了第2个问题:https://github.com/elastic/ela ... 21595 。 这个问题很快被官方确认为Bug,修复也很快加入到了5.0.2。 为了规避这个问题,我们只好临时修改了一下Kibana以及第三方API访问要经过的nginx proxy,默认为所有的search request加入一个超时选项。此后,问题有一些缓解,但仍然发现用户查询大范围历史数据时,部分用于存储历史数据的结点响应很慢。

我们的集群是做了冷热分离的结构的,热节点主要承担写入和存放过去24小时数据,冷结点没有写入,查询频率也低,所以为了最大化利用硬件资源,一台物理机上跑了3个实例,这样一台128GB内存的机器可以存放下近30TB的索引。查看冷结点的监控数据,看到用户查询期间磁盘的read IO非常高,直接将磁盘IO Util%撑到100%,并且可持续数小时,同时search thread pool有大量的active thread处于无法完成状态,search queue不断攀升直至饱和、开始reject。 表象上看search thread似乎一直在尝试从磁盘大量读取数据,一次search甚至可以持续几十分钟至一个小时,耗尽了所有的搜索线程,导致拒绝后续的搜索服务。 于是Github上报了第3个issue: https://github.com/elastic/ela ... 21611  这个问题找到解决办法之前,我们只能通过反复重启有问题的冷结点来缓解。 和官方讨论过程中,得知5.0在Lucene文件访问方式上有一个比较大的改动,2.4使用mmapfs读取索引文件的部分,而5.0以后改为用mmapfs读取索引文件的全部。怀疑问题和这个变动有关,尝试将所有索引文件的设置改为NIOFS后,问题迎刃而解。 搜索性能一下回到了2.4时代,再也没出现搜索线程超长时间执行的问题。之后找时间复现了这个问题,并抓取了线程栈,看到长时间执行的搜索线程一直在做Global Ordinal的构造工作。 至于为何会这样,还不清楚。 从官方给出的信息看,底层索引文件的访问模式是没有变化的,仅仅是将文件读取方式全部改成了mmapfs,理论上应该性能更好,但是看起来在我们这种一台机器跑多个ES实例,所有分配的heap为系统缓存3倍的极端用例下,大范围的数据搜索可能造成过高的磁盘读IO,集群性能指数级下降。

以上问题前后耗了4天才完全规避掉,支持团队连续熬夜后集群总算回复到平稳状态。然而好景不长,运行一段时间以后,数据结点出现疑似内存泄漏现象。结点总数据没怎么增加、甚至还有减少的情况下,heap使用率一只呈攀升趋势,Old GC无法回收内存。这个问题对用户影响较小,通过监控我们可以及时发现内存即将用尽的结点,做一次重启很快就恢复了。 为排查根源,我们对一个有问题的结点做了dump,通过MAT工具分析,看到meta data相关的一个alias对象被实例化了有6600万次之多! 在Github上提交了第四个issue: https://github.com/elastic/ela ... 22013,不多久被确认为已知问题https://github.com/elastic/ela ... 21284 ,在5.0.1已经修复。

最后还存在一个master node内存泄漏的问题,这个问题在2.4.0时代就存在了,升级到5.0.0以后依然没有修复。由于我们的master node和data node是分离的,所以这个问题比较容易通过监控发现,解决方式也很简单和迅速,重启master node即可,对用户完全无影响。之后不久,5.0.2版本正式发布,release notes里提到了对这个问题的修复 https://github.com/elastic/ela ... 21578

上周周末我们将集群rolling upgrade到了5.0.2,global search timeout失效和两个内存泄漏的问题从根源上解决掉了。 网络流量增大的问题依然存在,仍然需要通过启用client结点的transport.tcp.compress规避。 冷结点搜索性能的问题没看到有提及,估计没解决,安全起见,还是保持索引的文件系统为NIOFS。升级完成运行一段时间后,可以肯定,5.0.2已经比较稳定。


心得

升到5.0.2后,对于其中一组数据结点这两天特意加了点索引负载,通过监控数据将v5.0.2与2.4.0做实际运行环境的索引吞吐量对比。

2.4_.png


5.0_.png

 
在近似的CPU使用率和load情况下,5.0.2能够支撑更大的吞吐量。另外5.0带来的Instant aggregation功能,对于跨多个索引的时序类型数据的聚合也可以有效Cache了,在使用Kibana的时候提速感觉非常明显。

升级过程虽然遇到很多波折,但由于集群架构上做了角色分离(client,master,data)和冷热分离,因而Bug引起的故障比较容易被限定在一个较小的范围而不至于影响所有的功能和所有的用户。 故障点定位更加容易,规避措施也更容易实施。 部分规避措施实施过程中甚至对用户是完全无影响的,比如: 重启内存泄漏的master node)。详尽的监控为问题的发现和诊断提供了有力的支持。

Elasticsearch是非常复杂的系统,官方的测试无法覆盖所有的用例场景和数据规模,一些极端的应用场景可能触发某个深藏的Bug或者缺陷而陷入困境。 因此对于稳定性要求极高的应用,最好还是采用经过长时间考验的版本,比如v2.4.2。 收起阅读 »

可定制的 elasticsearch 数据导入工具 ——mysql_2_elasticsearch

A customizable importer from mysql to elasticsearch.
可定制的 elasticsearch 数据导入工具 ——基于 elasticsearch 的 JS API
 
 
【github 项目地址】
https://github.com/parksben/mysql_2_elasticsearch 


【主要功能】
1. 完全使用 JS 实现数据从 MySQL 到 elasticsearch 的迁移;
2. 可批量导入多张 MySQL 表;
3. 可自定义的数据迁移规则(数据表/字段关系、字段过滤、使用正则进行数据处理);
4. 可自定义的异步分片导入方式,数据导入效率更高。


【一键安装】
npm install mysql_2_elasticsearch


【快速开始(简单用例)】
var esMysqlRiver = require('mysql_2_elasticsearch');

var river_config = {
mysql: {
host: '127.0.0.1',
user: 'root',
password: 'root',
database: 'users',
port: 3306
},
elasticsearch: {
host_config: { // es客户端的配置参数
host: 'localhost:9200',
// log: 'trace'
},
index: 'myIndex'
},
riverMap: {
'users => users': {} // 将数据表 users 导入到 es 类型: /myIndex/users
}
};


/*
** 以下代码内容:
** 通过 esMysqlRiver 方法进行数据传输,方法的回调参数(一个JSON对象) obj 包含此次数据传输的结果
** 其中:
** 1. obj.total => 需要传输的数据表数量
** 2. obj.success => 传输成功的数据表数量
** 3. obj.failed => 传输失败的数据表数量
** 4. obj.result => 本次数据传输的结论
*/

esMysqlRiver(river_config, function(obj) {
/* 将传输结果打印到终端 */
console.log('\n---------------------------------');
console.log('总传送:' + obj.total + '项');
console.log('成功:' + obj.success + '项');
console.log('失败:' + obj.failed + '项');
if (obj.result == 'success') {
console.log('\n结论:全部数据传送完成!');
} else {
console.log('\n结论:传送未成功...');
}
console.log('---------------------------------');
console.log('\n(使用 Ctrl + C 退出进程)');
/* 将传输结果打印到终端 */
});


【最佳实现(完整用例)】
var esMysqlRiver = require('mysql_2_elasticsearch');

/*
** mysql_2_elasticsearch 的相关参数配置(详情见注释)
*/

var river_config = {

/* [必需] MySQL数据库的相关参数(根据实际情况进行修改) */
mysql: {
host: '127.0.0.1',
user: 'root',
password: 'root',
database: 'users',
port: 3306
},

/* [必需] es 相关参数(根据实际情况进行修改) */
elasticsearch: {
host_config: { // [必需] host_config 即 es客户端的配置参数,详细配置参考 es官方文档
host: 'localhost:9200',
log: 'trace',
// Other options...
},
index: 'myIndex', // [必需] es 索引名
chunkSize: 8000, // [非必需] 单分片最大数据量,默认为 5000 (条数据)
timeout: '2m' // [非必需] 单次分片请求的超时时间,默认为 1m
//(注意:此 timeout 并非es客户端请求的timeout,后者请在 host_config 中设置)
},

/* [必需] 数据传送的规则 */
riverMap: {
'users => users': { // [必需] 'a => b' 表示将 mysql数据库中名为 'a' 的 table 的所有数据 输送到 es中名为 'b' 的 type 中去
filter_out: [ // [非必需] 需要过滤的字段名,即 filter_out 中的设置的所有字段将不会被导入 elasticsearch 的数据中
'password',
'age'
],
exception_handler: { // [非必需] 异常处理器,使用JS正则表达式处理异常数据,避免 es 入库时由于类型不合法造成数据缺失
'birthday': [ // [示例] 对 users 表的 birthday 字段的异常数据进行处理
{
match: /NaN/gi, // [示例] 正则条件(此例匹配字段值为 "NaN" 的情况)
writeAs: null // [示例] 将 "NaN" 重写为 null
},
{
match: /(\d{4})年/gi, // [示例] 正则表达式(此例匹配字段值为形如 "2016年" 的情况)
writeAs: '$1.1' // [示例] 将 "2015年" 样式的数据重写为 "2016.1" 样式的数据
}
]
}
},
// Other fields' options...
}

};


/*
** 将传输结果打印到终端
*/

esMysqlRiver(river_config, function(obj) {
console.log('\n---------------------------------');
console.log('总传送:' + obj.total + '项');
console.log('成功:' + obj.success + '项');
console.log('失败:' + obj.failed + '项');
if (obj.result == 'success') {
console.log('\n结论:全部数据传送完成!');
} else {
console.log('\n结论:传送未成功...');
}
console.log('---------------------------------');
console.log('\n(使用 Ctrl + C 退出进程)');
});


【注意事项及参考】
1. elasticsearch数据导入前请先配置好数据的 mapping;
2. "host_config" 更多参数设置详见 [es官方API文档] https://www.elastic.co/guide/e ... .html
3. mysql 表的自增 id 将自动替换为 "表名+_id" 的格式,如:"users_id";
4. 如出现数据缺失情况,请注意查看 elasticsearch 终端进程或日志,找出未成功导入的数据,通过设置 exception_handler 参数处理它。
继续阅读 »
A customizable importer from mysql to elasticsearch.
可定制的 elasticsearch 数据导入工具 ——基于 elasticsearch 的 JS API
 
 
【github 项目地址】
https://github.com/parksben/mysql_2_elasticsearch 


【主要功能】
1. 完全使用 JS 实现数据从 MySQL 到 elasticsearch 的迁移;
2. 可批量导入多张 MySQL 表;
3. 可自定义的数据迁移规则(数据表/字段关系、字段过滤、使用正则进行数据处理);
4. 可自定义的异步分片导入方式,数据导入效率更高。


【一键安装】
npm install mysql_2_elasticsearch


【快速开始(简单用例)】
var esMysqlRiver = require('mysql_2_elasticsearch');

var river_config = {
mysql: {
host: '127.0.0.1',
user: 'root',
password: 'root',
database: 'users',
port: 3306
},
elasticsearch: {
host_config: { // es客户端的配置参数
host: 'localhost:9200',
// log: 'trace'
},
index: 'myIndex'
},
riverMap: {
'users => users': {} // 将数据表 users 导入到 es 类型: /myIndex/users
}
};


/*
** 以下代码内容:
** 通过 esMysqlRiver 方法进行数据传输,方法的回调参数(一个JSON对象) obj 包含此次数据传输的结果
** 其中:
** 1. obj.total => 需要传输的数据表数量
** 2. obj.success => 传输成功的数据表数量
** 3. obj.failed => 传输失败的数据表数量
** 4. obj.result => 本次数据传输的结论
*/

esMysqlRiver(river_config, function(obj) {
/* 将传输结果打印到终端 */
console.log('\n---------------------------------');
console.log('总传送:' + obj.total + '项');
console.log('成功:' + obj.success + '项');
console.log('失败:' + obj.failed + '项');
if (obj.result == 'success') {
console.log('\n结论:全部数据传送完成!');
} else {
console.log('\n结论:传送未成功...');
}
console.log('---------------------------------');
console.log('\n(使用 Ctrl + C 退出进程)');
/* 将传输结果打印到终端 */
});


【最佳实现(完整用例)】
var esMysqlRiver = require('mysql_2_elasticsearch');

/*
** mysql_2_elasticsearch 的相关参数配置(详情见注释)
*/

var river_config = {

/* [必需] MySQL数据库的相关参数(根据实际情况进行修改) */
mysql: {
host: '127.0.0.1',
user: 'root',
password: 'root',
database: 'users',
port: 3306
},

/* [必需] es 相关参数(根据实际情况进行修改) */
elasticsearch: {
host_config: { // [必需] host_config 即 es客户端的配置参数,详细配置参考 es官方文档
host: 'localhost:9200',
log: 'trace',
// Other options...
},
index: 'myIndex', // [必需] es 索引名
chunkSize: 8000, // [非必需] 单分片最大数据量,默认为 5000 (条数据)
timeout: '2m' // [非必需] 单次分片请求的超时时间,默认为 1m
//(注意:此 timeout 并非es客户端请求的timeout,后者请在 host_config 中设置)
},

/* [必需] 数据传送的规则 */
riverMap: {
'users => users': { // [必需] 'a => b' 表示将 mysql数据库中名为 'a' 的 table 的所有数据 输送到 es中名为 'b' 的 type 中去
filter_out: [ // [非必需] 需要过滤的字段名,即 filter_out 中的设置的所有字段将不会被导入 elasticsearch 的数据中
'password',
'age'
],
exception_handler: { // [非必需] 异常处理器,使用JS正则表达式处理异常数据,避免 es 入库时由于类型不合法造成数据缺失
'birthday': [ // [示例] 对 users 表的 birthday 字段的异常数据进行处理
{
match: /NaN/gi, // [示例] 正则条件(此例匹配字段值为 "NaN" 的情况)
writeAs: null // [示例] 将 "NaN" 重写为 null
},
{
match: /(\d{4})年/gi, // [示例] 正则表达式(此例匹配字段值为形如 "2016年" 的情况)
writeAs: '$1.1' // [示例] 将 "2015年" 样式的数据重写为 "2016.1" 样式的数据
}
]
}
},
// Other fields' options...
}

};


/*
** 将传输结果打印到终端
*/

esMysqlRiver(river_config, function(obj) {
console.log('\n---------------------------------');
console.log('总传送:' + obj.total + '项');
console.log('成功:' + obj.success + '项');
console.log('失败:' + obj.failed + '项');
if (obj.result == 'success') {
console.log('\n结论:全部数据传送完成!');
} else {
console.log('\n结论:传送未成功...');
}
console.log('---------------------------------');
console.log('\n(使用 Ctrl + C 退出进程)');
});


【注意事项及参考】
1. elasticsearch数据导入前请先配置好数据的 mapping;
2. "host_config" 更多参数设置详见 [es官方API文档] https://www.elastic.co/guide/e ... .html
3. mysql 表的自增 id 将自动替换为 "表名+_id" 的格式,如:"users_id";
4. 如出现数据缺失情况,请注意查看 elasticsearch 终端进程或日志,找出未成功导入的数据,通过设置 exception_handler 参数处理它。 收起阅读 »

Day6:《记一次es性能调优》


一.前言
应medcl写es文章的时候,其实这段时间es研究的不多,感觉没什么新东西可写。
考虑只有这次调优心得可与大家分享,文笔有限,见谅!
二.背景
先交代一下背景,调优的项目是某电商类搜索项目,流量来自于前端的app和h5。
搜索主要是根据用户的地理位置和关键字等条件搜索附近的商家和商品。
商品数据大概在5000w左右,商品更新很频繁,更新量大概是每天2000w条左右,(因商家经常会促销、或者调上下架状态、改价格等)查询也相当频繁。
集群有2个集群,一个主一个备,用于有问题的时候随时切换。主集群有8个节点,配置是32核,
32g内存的docker的机器。给es jvm分配20g内存,jdk 版本是1.7,gc 是使用parnew/cms gc。
这个项目我是后期加入的,来的时候项目已上线。由于参与进来的时候es跑的也还是比较稳定,所以也一直
没调过es的参数。程序,参数基本上也就保持上线的时候那个样子。
es上线的时候是用的1.5版本,后期没升过级。
三.问题
项目大概跑了一年多,时间来到大概16年的9月份。搜索请求响应时间开始出现几秒才完成的情况,
我就被拉过来调优了。通过我们自己内部的调用方法监控,tp99和avg这些值还好,维持在200ms以下。max 最大有5,6s的情况,而且次数有点多。
这里没怎么折腾,很快定位就是es gc导致的。翻了一下es gc日志,就是cms remark这个阶段时间特别长,
而且 这个阶段是stop the world的。
四.解决
为什么remark阶段这么长时间? 直接上结论,就是一次cms 周期内,并发标记后到remark这个期间jvm 堆内存对象
变化很大。说白了对应我们的场景就是一大波 es bulk操作。对应Bigdesk观察,几秒的卡顿基本都出现在一大波 es bulk操作之后。
这里解释一下,引用网上文章的说法:
remark如果耗时较长,通常原因是在cms gc已经结束了concurrent-mark步骤后,旧生代的引用关系仍然发生了很多的变化,旧生代的引用关系发生变化的原因主要是:
* 在这个间隔时间段内,新生代晋升到旧生代的对象比较多;
* 在这个间隔时间段内,新生代没怎么发生ygc,但创建出来的对象又比较多,这种通常就只能是新生代比较大的原因;
原文地址:
http://hellojava.info/?tag=cms-gc-remark

调整一:
加cms gc 的 线程
直接从根源入手,你remark 慢,我就让你跑快点。
因为我们是32 核的cpu ,cpu 利用率用bigdesk观察还是很低的,5%左右。这么低,那就加点线程呗。
-XX:ParallelGCThreads= N
-XX:ParallelCMSThreads= M
调整这2个参数都可以,它们的关系:ParallelCMSThreads = (ParallelGCThreads + 3)/4)
调整后情况缓解了一些,remark还是有3,4秒的情况。
调整二:
关于这点是我们自己的问题。一次bulk 操作就写1条数据。
是的,你没有看错,我们这边的工程师就是这么干的。
一年以前提过这里最好是能合并写,但现在还是这个样子。
这里有一些业务上的原因,合并写会导致一些字段值不准确。
合并写暂时没办法,只能调整es 了。(这里说明一下,其实合并写应该是本次优化比较有效果的办法,可惜这招不让我用。)
通过bigdesk观察,bulk线程池有reject的情况。
但就增加bulk线程池的消费线程,加快数据的消费速度,减少对象驻留在jvm 的时间。
调整后情况没有明显的好转,
但肯定有用,能优化一点是一点嘛。

调整三:
再次从gc入手, -XX:+CMSParallelRemarkEnabled -XX:+CMSScavengeBeforeRemark
这个是网上找的办法:
为了减少第二次暂停的时间,开启并行remark: -XX:+CMSParallelRemarkEnabled。 如果remark还是过长的话,可以开启-XX:+CMSScavengeBeforeRemark选项,强制 remark之前开始一次minor gc,减少remark的暂停时间,但是在remark之后也将立即开始又一次minor gc。调整后情况也没有特别的好转。
以上都是从减小单次cms gc的开销的方向去解决问题,然后我就换了个方向,降低cms gc发生的次数,让它少发生或者不发生。
调整四:
这里调整了一共5个参数,
Xmn10g ==> 8g
CMSInitiatingOccupancyFraction=70 ==>80

index.cache.filter.max_size 2g==>1g
index.cache.filter.expire 2m==>40s
index.refresh_interval 20s==>30s

前2个参数没什么好说的,提高cms gc 被触发的条件,降低cms gc 触发几率。
后3个参数是本文的重点,这里大概讲下es 对于filter cache的管理。
这部分是通过阅读源码分析出来的,涉及代码还挺多,有点复杂,还有很大一部分还是lucene的代码。
这里就不贴大段代码了。
es 对于 filter cache管理是内部维护了一个map的结构(实际是利用com.google.common.cache实现的),关键是这个map 的key 是个它自己定义的类
叫 FilterCacheKey,它override了equals方法
public FilterCacheKey(Object readerKey, Object filterKey) {
    this.readerKey = readerKey;
    this.filterKey = filterKey;
}
...
@Override
public boolean equals(Object o) {
    if (this == o) return true;
//            if (o == null || getClass() != o.getClass()) return false;
    FilterCacheKey that = (FilterCacheKey) o;
    return (readerKey().equals(that.readerKey()) && filterKey.equals(that.filterKey));
}
从这里可以看出,filter cache 能否被再次利用到就跟readerKey和filterKey 有关。
filterkey如果你build 查询语句的时候什么都没设置的话,就是filter对象本身。
举个例子,TermFilter,如果term一样,那前后2次查询filterKey是一致的。
关键是这个readerKey是否一致呢?这个readerKey其实就是lucene 的 indexReader,如果前后2次查询有数据更新并且
index.refresh_interval 这个参数设置的比较小,es 会去刷新indexReader,那么很有可能readerKey不一致。
对应我们的场景,数据更新频繁,而且index.refresh_interval 比较小,那么缓存利用率就不太高。
后一次查询的filter cache 会重新put 到上面提到的map里,然后这个index.cache.filter.max_size 2g 
就迅速占满(我们程序代码里很多地方使用到了filter),配多大都有可能占满。那这个filter cache什么时候被移除呢,index.cache.filter.expire 2m管移除这个事,当然应该还有size满了移除的策略。
这就造成了缓存没什么用,还占这么大地方,还占那么久。
然后这个filter cache就很可能跑到 old gen去了。
那么就让它占少点,不干活就快点走:
index.cache.filter.max_size 2g==>1g
index.cache.filter.expire 2m==>40s
index.refresh_interval 20s==>30s
这些调整完毕,重启es ,通过bigdesk ,gc a线图好看多了,cms gc 基本没了,monitor gc 也顺畅多了。
五.总结和备注
总结:
1.优化这个事,我认为是业务上的优化要比技术上优化有效的多
2.日志和监控是你最好的朋友,仔细的看,你总会看出什么
3.es 缓存要利用好,还是需要好好去设计,回头考虑单独写一篇。
备注:
因为这次优化后,我就离开了原来的公司,没有了原来的环境。所以本文
部分参数和数字可能不准确,我仅凭记忆完成。
 
继续阅读 »

一.前言
应medcl写es文章的时候,其实这段时间es研究的不多,感觉没什么新东西可写。
考虑只有这次调优心得可与大家分享,文笔有限,见谅!
二.背景
先交代一下背景,调优的项目是某电商类搜索项目,流量来自于前端的app和h5。
搜索主要是根据用户的地理位置和关键字等条件搜索附近的商家和商品。
商品数据大概在5000w左右,商品更新很频繁,更新量大概是每天2000w条左右,(因商家经常会促销、或者调上下架状态、改价格等)查询也相当频繁。
集群有2个集群,一个主一个备,用于有问题的时候随时切换。主集群有8个节点,配置是32核,
32g内存的docker的机器。给es jvm分配20g内存,jdk 版本是1.7,gc 是使用parnew/cms gc。
这个项目我是后期加入的,来的时候项目已上线。由于参与进来的时候es跑的也还是比较稳定,所以也一直
没调过es的参数。程序,参数基本上也就保持上线的时候那个样子。
es上线的时候是用的1.5版本,后期没升过级。
三.问题
项目大概跑了一年多,时间来到大概16年的9月份。搜索请求响应时间开始出现几秒才完成的情况,
我就被拉过来调优了。通过我们自己内部的调用方法监控,tp99和avg这些值还好,维持在200ms以下。max 最大有5,6s的情况,而且次数有点多。
这里没怎么折腾,很快定位就是es gc导致的。翻了一下es gc日志,就是cms remark这个阶段时间特别长,
而且 这个阶段是stop the world的。
四.解决
为什么remark阶段这么长时间? 直接上结论,就是一次cms 周期内,并发标记后到remark这个期间jvm 堆内存对象
变化很大。说白了对应我们的场景就是一大波 es bulk操作。对应Bigdesk观察,几秒的卡顿基本都出现在一大波 es bulk操作之后。
这里解释一下,引用网上文章的说法:
remark如果耗时较长,通常原因是在cms gc已经结束了concurrent-mark步骤后,旧生代的引用关系仍然发生了很多的变化,旧生代的引用关系发生变化的原因主要是:
* 在这个间隔时间段内,新生代晋升到旧生代的对象比较多;
* 在这个间隔时间段内,新生代没怎么发生ygc,但创建出来的对象又比较多,这种通常就只能是新生代比较大的原因;
原文地址:
http://hellojava.info/?tag=cms-gc-remark

调整一:
加cms gc 的 线程
直接从根源入手,你remark 慢,我就让你跑快点。
因为我们是32 核的cpu ,cpu 利用率用bigdesk观察还是很低的,5%左右。这么低,那就加点线程呗。
-XX:ParallelGCThreads= N
-XX:ParallelCMSThreads= M
调整这2个参数都可以,它们的关系:ParallelCMSThreads = (ParallelGCThreads + 3)/4)
调整后情况缓解了一些,remark还是有3,4秒的情况。
调整二:
关于这点是我们自己的问题。一次bulk 操作就写1条数据。
是的,你没有看错,我们这边的工程师就是这么干的。
一年以前提过这里最好是能合并写,但现在还是这个样子。
这里有一些业务上的原因,合并写会导致一些字段值不准确。
合并写暂时没办法,只能调整es 了。(这里说明一下,其实合并写应该是本次优化比较有效果的办法,可惜这招不让我用。)
通过bigdesk观察,bulk线程池有reject的情况。
但就增加bulk线程池的消费线程,加快数据的消费速度,减少对象驻留在jvm 的时间。
调整后情况没有明显的好转,
但肯定有用,能优化一点是一点嘛。

调整三:
再次从gc入手, -XX:+CMSParallelRemarkEnabled -XX:+CMSScavengeBeforeRemark
这个是网上找的办法:
为了减少第二次暂停的时间,开启并行remark: -XX:+CMSParallelRemarkEnabled。 如果remark还是过长的话,可以开启-XX:+CMSScavengeBeforeRemark选项,强制 remark之前开始一次minor gc,减少remark的暂停时间,但是在remark之后也将立即开始又一次minor gc。调整后情况也没有特别的好转。
以上都是从减小单次cms gc的开销的方向去解决问题,然后我就换了个方向,降低cms gc发生的次数,让它少发生或者不发生。
调整四:
这里调整了一共5个参数,
Xmn10g ==> 8g
CMSInitiatingOccupancyFraction=70 ==>80

index.cache.filter.max_size 2g==>1g
index.cache.filter.expire 2m==>40s
index.refresh_interval 20s==>30s

前2个参数没什么好说的,提高cms gc 被触发的条件,降低cms gc 触发几率。
后3个参数是本文的重点,这里大概讲下es 对于filter cache的管理。
这部分是通过阅读源码分析出来的,涉及代码还挺多,有点复杂,还有很大一部分还是lucene的代码。
这里就不贴大段代码了。
es 对于 filter cache管理是内部维护了一个map的结构(实际是利用com.google.common.cache实现的),关键是这个map 的key 是个它自己定义的类
叫 FilterCacheKey,它override了equals方法
public FilterCacheKey(Object readerKey, Object filterKey) {
    this.readerKey = readerKey;
    this.filterKey = filterKey;
}
...
@Override
public boolean equals(Object o) {
    if (this == o) return true;
//            if (o == null || getClass() != o.getClass()) return false;
    FilterCacheKey that = (FilterCacheKey) o;
    return (readerKey().equals(that.readerKey()) && filterKey.equals(that.filterKey));
}
从这里可以看出,filter cache 能否被再次利用到就跟readerKey和filterKey 有关。
filterkey如果你build 查询语句的时候什么都没设置的话,就是filter对象本身。
举个例子,TermFilter,如果term一样,那前后2次查询filterKey是一致的。
关键是这个readerKey是否一致呢?这个readerKey其实就是lucene 的 indexReader,如果前后2次查询有数据更新并且
index.refresh_interval 这个参数设置的比较小,es 会去刷新indexReader,那么很有可能readerKey不一致。
对应我们的场景,数据更新频繁,而且index.refresh_interval 比较小,那么缓存利用率就不太高。
后一次查询的filter cache 会重新put 到上面提到的map里,然后这个index.cache.filter.max_size 2g 
就迅速占满(我们程序代码里很多地方使用到了filter),配多大都有可能占满。那这个filter cache什么时候被移除呢,index.cache.filter.expire 2m管移除这个事,当然应该还有size满了移除的策略。
这就造成了缓存没什么用,还占这么大地方,还占那么久。
然后这个filter cache就很可能跑到 old gen去了。
那么就让它占少点,不干活就快点走:
index.cache.filter.max_size 2g==>1g
index.cache.filter.expire 2m==>40s
index.refresh_interval 20s==>30s
这些调整完毕,重启es ,通过bigdesk ,gc a线图好看多了,cms gc 基本没了,monitor gc 也顺畅多了。
五.总结和备注
总结:
1.优化这个事,我认为是业务上的优化要比技术上优化有效的多
2.日志和监控是你最好的朋友,仔细的看,你总会看出什么
3.es 缓存要利用好,还是需要好好去设计,回头考虑单独写一篇。
备注:
因为这次优化后,我就离开了原来的公司,没有了原来的环境。所以本文
部分参数和数字可能不准确,我仅凭记忆完成。
  收起阅读 »

求一个Kibana5能用的高德URL

kibana到5.0后可以直接在kibana.yml上面使用tilemap.url进行配置,
但是之前使用的url貌似都不行,有伙伴能提供个K5能用的url吗?
kibana到5.0后可以直接在kibana.yml上面使用tilemap.url进行配置,
但是之前使用的url貌似都不行,有伙伴能提供个K5能用的url吗?

Day5: 《PacketBeat奇妙的OOM小记》

Beats这个项目的确很好用,几行命令下来,一个成型的Agent就出来了。使用者只需要关注采集什么数据就好,后续的事情libbeat基本都处理完了。不过值得吐槽的是,Beat太散了,管理起来东一个西一个的,产品化的时候对客户说,我们要在机器上放n个Agent不知道客户会是什么样的表情。


d7d0a529244b57acb6ce3796da132df8.jpg



不过轻量级、已部署的特点还是极大的吸引了我,于是就有了后面的事情了。

PacketBeat不明原因的OOM

某天我把PacketBeat放到了我的服务器上面,这台服务器上面有个MongoDB,MongoDB主要是拿来存放ES的元数据的。ES2.x的时候并没有很好的元数据管理,为了能让ES的索引分配的比较均匀,并且有元数据辅助查询,设计好一个元数据管理的仓库是必要的。然后我打开了对MongoDB的抓包功能,恩,一切都很好,接着我打开了日志管理页面,看到了一条一条的MongoDB的包被抓回来,解码,然后塞到了ES。可是第二天一看,咦??Packet跪了?不是吧,ElasticSearch做的产品这么不稳定么。我不信。


06170826_dLgU.png



然后我又启动了第二次,紧接着熟练的top了一下,观察了PacketBeat半个多小时,在被观察的这段时间里面,PacketBeat的表现非常的正常,看不出有什么异样。好吧,那上一次的OOM可能只是个意外,Windows也经常蓝屏嘛,OOM一次也正常。结果第二天我再次打开终端,发现这货居然又OOM了!!


06170909_irst.png



好吧好吧,我感觉我已经踩到Bug了,拿了开源社区这么多东西,总得贡献一下的,好吧,提个Issue去 https://github.com/elastic/beats/issues/2867

真相只有一个

微信群里面聊起这个奇妙的OOM,Medcl大神问是不是因为采集了ES的日志,(我的这台服务器和日志服务器有关系)然后导致滚雪球把PacketBeat给滚死了。咦?说不定真的是这个原因耶!但是看了看PacketBeat,我并没有抓ES的包,而且假如我采集了ES的包,应该一下就OOM掉了,不应该等那么久。不过这么一说,却仿佛打开了新世界的大门


06171040_FVEG.png



我把这台服务器在日志服务器中的角色重新梳理了下,终于发现了这次OOM的原因了。。

由于2.X的ES没有比较好的元数据信息,所以当日志送到LogServer的时候,我做了些额外的操作,让LogServer持久化到ES一定量的时候就会往Mongo写一下元数据信息(当然也有其他服务会往里面做CRUD啦),开始的时候访问Mongo的次数其实是很少的,假设按1W来算。那么问题来了,由于我们的PacketBeat抓了Mongo的包,那么LogServer往ES的CRUD操作都会被PacketBeat给抓走,然后再送回给LogServer


06171248_tcdl.png



那么一个隐藏的滚雪球事件就产生了,刚开始的那段时间,Mongo被抓包的次数只有1W,然后就往LogServer多送了1W条日志,不。。应该多很多,毕竟网络包嘛,然后就导致LogServer因为要管理元数据的频率开始逐渐地提高,逐渐提高CRUD的频率后抓包的内容也越来越多,紧接着到这发生到LogServer的频率也越来越高。。。。。每次PacketBeat崩掉的时候,都送了80W左右的日志量出去,然后它就OOM掉了(因为我那台机器就只剩下2G的空闲内存给它用,被系统给干掉了)。。我居然发现了这样的场景


06171336_PbWI.png



结论

使用PacketBeat的时候,记得要留意一下有没这种反馈型滚雪球的情况,多发生在自己的日志服务器上面。当然那种直接抓ES的就没什么好说了,估计启动了之后没多久就崩溃掉了
继续阅读 »
Beats这个项目的确很好用,几行命令下来,一个成型的Agent就出来了。使用者只需要关注采集什么数据就好,后续的事情libbeat基本都处理完了。不过值得吐槽的是,Beat太散了,管理起来东一个西一个的,产品化的时候对客户说,我们要在机器上放n个Agent不知道客户会是什么样的表情。


d7d0a529244b57acb6ce3796da132df8.jpg



不过轻量级、已部署的特点还是极大的吸引了我,于是就有了后面的事情了。

PacketBeat不明原因的OOM

某天我把PacketBeat放到了我的服务器上面,这台服务器上面有个MongoDB,MongoDB主要是拿来存放ES的元数据的。ES2.x的时候并没有很好的元数据管理,为了能让ES的索引分配的比较均匀,并且有元数据辅助查询,设计好一个元数据管理的仓库是必要的。然后我打开了对MongoDB的抓包功能,恩,一切都很好,接着我打开了日志管理页面,看到了一条一条的MongoDB的包被抓回来,解码,然后塞到了ES。可是第二天一看,咦??Packet跪了?不是吧,ElasticSearch做的产品这么不稳定么。我不信。


06170826_dLgU.png



然后我又启动了第二次,紧接着熟练的top了一下,观察了PacketBeat半个多小时,在被观察的这段时间里面,PacketBeat的表现非常的正常,看不出有什么异样。好吧,那上一次的OOM可能只是个意外,Windows也经常蓝屏嘛,OOM一次也正常。结果第二天我再次打开终端,发现这货居然又OOM了!!


06170909_irst.png



好吧好吧,我感觉我已经踩到Bug了,拿了开源社区这么多东西,总得贡献一下的,好吧,提个Issue去 https://github.com/elastic/beats/issues/2867

真相只有一个

微信群里面聊起这个奇妙的OOM,Medcl大神问是不是因为采集了ES的日志,(我的这台服务器和日志服务器有关系)然后导致滚雪球把PacketBeat给滚死了。咦?说不定真的是这个原因耶!但是看了看PacketBeat,我并没有抓ES的包,而且假如我采集了ES的包,应该一下就OOM掉了,不应该等那么久。不过这么一说,却仿佛打开了新世界的大门


06171040_FVEG.png



我把这台服务器在日志服务器中的角色重新梳理了下,终于发现了这次OOM的原因了。。

由于2.X的ES没有比较好的元数据信息,所以当日志送到LogServer的时候,我做了些额外的操作,让LogServer持久化到ES一定量的时候就会往Mongo写一下元数据信息(当然也有其他服务会往里面做CRUD啦),开始的时候访问Mongo的次数其实是很少的,假设按1W来算。那么问题来了,由于我们的PacketBeat抓了Mongo的包,那么LogServer往ES的CRUD操作都会被PacketBeat给抓走,然后再送回给LogServer


06171248_tcdl.png



那么一个隐藏的滚雪球事件就产生了,刚开始的那段时间,Mongo被抓包的次数只有1W,然后就往LogServer多送了1W条日志,不。。应该多很多,毕竟网络包嘛,然后就导致LogServer因为要管理元数据的频率开始逐渐地提高,逐渐提高CRUD的频率后抓包的内容也越来越多,紧接着到这发生到LogServer的频率也越来越高。。。。。每次PacketBeat崩掉的时候,都送了80W左右的日志量出去,然后它就OOM掉了(因为我那台机器就只剩下2G的空闲内存给它用,被系统给干掉了)。。我居然发现了这样的场景


06171336_PbWI.png



结论

使用PacketBeat的时候,记得要留意一下有没这种反馈型滚雪球的情况,多发生在自己的日志服务器上面。当然那种直接抓ES的就没什么好说了,估计启动了之后没多久就崩溃掉了 收起阅读 »

ES5.0.0 安装记录


创建用户:adduser elasticsearch
可查看创建结果:
##########/etc/passwd
##########/etc/shadow
##########/etc/group
配置环境变量
修改文件:/home/elasticsearch/.profile
追加内容:
export JAVA_HOME=/home/elasticsearch/java/jdk1.8.0_73
export PATH=$JAVA_HOME/bin:$PATH
export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$CLASSPATH
export PATH
配置elasticsearch5.0.0
tar -xf elasticsearch-5.0.0.tar.gz -C /home/elasticsearch/
cd /home/elasticsearch/
ln -sv elasticsearch-5.0.0 elasticsearch
mkdir -pv /esdata/elasticsearch/{data,logs}
chown -R elasticsearch.elasticsearch /esdata/elasticsearch
修改ES配置文件
/home/elasticsearch/elasticsearch-5.0.0/config/elasticsearch.yml
http.cors.enabled: true
http.cors.allow-origin: "*"
path.data: /esdata/elasticsearch/data
path.logs: /esdata/elasticsearch/logs
network.host: 192.168.25.57
http.port: 8201
transport.tcp.port: 8301
bootstrap.memory_lock: true
/home/elasticsearch/elasticsearch-5.0.0/config/jvm.options
-Xms8g
-Xmx8g
修改系统参数
/etc/security/limits.conf
elasticsearch soft nproc 65536
elasticsearch hard nproc 65536
elasticsearch soft nofile 65536
elasticsearch hard nofile 65536
elasticsearch - memlock unlimited
/etc/sysctl.conf
vm.max_map_count = 262144
加载更新:sysctl -p
启动ES服务
su - elasticsearch -c "/home/elasticsearch/elasticsearch/bin/elasticsearch &"
 
继续阅读 »

创建用户:adduser elasticsearch
可查看创建结果:
##########/etc/passwd
##########/etc/shadow
##########/etc/group
配置环境变量
修改文件:/home/elasticsearch/.profile
追加内容:
export JAVA_HOME=/home/elasticsearch/java/jdk1.8.0_73
export PATH=$JAVA_HOME/bin:$PATH
export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$CLASSPATH
export PATH
配置elasticsearch5.0.0
tar -xf elasticsearch-5.0.0.tar.gz -C /home/elasticsearch/
cd /home/elasticsearch/
ln -sv elasticsearch-5.0.0 elasticsearch
mkdir -pv /esdata/elasticsearch/{data,logs}
chown -R elasticsearch.elasticsearch /esdata/elasticsearch
修改ES配置文件
/home/elasticsearch/elasticsearch-5.0.0/config/elasticsearch.yml
http.cors.enabled: true
http.cors.allow-origin: "*"
path.data: /esdata/elasticsearch/data
path.logs: /esdata/elasticsearch/logs
network.host: 192.168.25.57
http.port: 8201
transport.tcp.port: 8301
bootstrap.memory_lock: true
/home/elasticsearch/elasticsearch-5.0.0/config/jvm.options
-Xms8g
-Xmx8g
修改系统参数
/etc/security/limits.conf
elasticsearch soft nproc 65536
elasticsearch hard nproc 65536
elasticsearch soft nofile 65536
elasticsearch hard nofile 65536
elasticsearch - memlock unlimited
/etc/sysctl.conf
vm.max_map_count = 262144
加载更新:sysctl -p
启动ES服务
su - elasticsearch -c "/home/elasticsearch/elasticsearch/bin/elasticsearch &"
  收起阅读 »

Day4: 《将sql转换为es的DSL》

es现在几乎已经是开源搜索引擎的事实标准了,搭建简易,使用方便。不过在很多公司里(包括我司的部分部门),并不是把它当搜索引擎来用,而是当db来用。因为本身查询/搜索原理的区别,使es在千万或者亿级的数据中进行逻辑筛选相对高效。例如一些wms、工单查询系统,单表几十个甚至上百个字段,如果在数据库里为每种类型的查询都建立合适的索引,成本比较高,更不用说索引建多了还会影响到插入速度,后期的索引优化也是比较麻烦的问题。

不过如果把es当db来使的话,始终会有一个绕不过去的坎。就是es的DSL。让所有业务开发去学习dsl的话也不是不可以,但DSL真的有点反人类(不要打我)。简单的a and b或者a or b还比较容易写,如果我要的是a and (b and (c or d) and e)的查询逻辑,那我觉得谁写都会晕。即使是用官方或者第三方提供的client,如果需求多种多样的话,想要灵活地实现`需求=>DSL`的过程还是比较痛苦。

对于业务开发来说,当然是sql更平易近人(毕竟写了这么多年CRUD)。所以还有一种歪门邪道的流派,直接把sql转成DSL。要做sql和DSL转换的工作,需要进行sql的解析,先不要怵,这个年代找一个靠谱的sql parser还是比较容易的。比如阿里开源的druid连接池里的sql模块:
 
https://github.com/alibaba/dru ... d/sql

因为笔者的实现是用的下面这个golang版的parser:

https://github.com/xwb1989/sqlparser

所以用这个来举例吧~

这个是其作者从youtube/vitness里提取并进行改进的一个parser,我们能用到的是一部分子集功能,只需要解析select类的sql。

先举个简单的sql的例子:
select * from x_order where userId = 1 order by id desc limit 10,1;

解析之后会变成golang的一个struct,来看看具体的定义:

&sqlparser.Select{
Comments:sqlparser.Comments(nil),
Distinct:"",
SelectExprs:sqlparser.SelectExprs{(*sqlparser.StarExpr)(0xc42000aee0)},
From:sqlparser.TableExprs{(*sqlparser.AliasedTableExpr)(0xc420016930)},
Where:(*sqlparser.Where)(0xc42000afa0),
GroupBy:sqlparser.GroupBy(nil),
Having:(*sqlparser.Where)(nil),
OrderBy:sqlparser.OrderBy{(*sqlparser.Order)(0xc42000af20)},
Limit:(*sqlparser.Limit)(0xc42000af80),
Lock:""
}


sql的select语句在被解析之后生成一个Select的结构体,如果我们不关心使用者需要的字段的话,可以先把SelectExprs/Distinct/Comments/Lock里的内容忽略掉。如果不是分组统计类的需求,也可以先把GroupBy/Having忽略掉。这里我们关心的就剩下From、Where、OrderBy和Limit。

From对应的TableExprs实际上可以认为是简单的字符串,这里的值其实就是`x_order`。

OrderBy实际上是一个元素为
type Order struct {
Expr ValExpr
Direction string
}\

的数组。

Limit也很简单,
type Limit struct {
Offset, Rowcount ValExpr
}

其实就是俩数字。

那么剩下的就是这个Where结构了。where会被解析为AST(`https://en.wikipedia.org/wiki/Abstract_syntax_tree`),中文是抽象语法树。在不说子查询之类的情况下,这个AST也不会太复杂,毕竟where后面的情况比起编译原理里的程序语言来说情况还是要少得多的。以上述的sql为例,这里解析出来的Where结构是这样的:
&sqlparser.Where{
Type:"where",
Expr:(*sqlparser.ComparisonExpr)(0xc420016a50)
}


只有一个节点,一个ComparisonExpr表达式,这个ComparisonExpr,中文比较表达式,指代的就是我们sql里的`user_id = 1`。实际上我们可以认为这个"比较表达式"即是所有复杂AST的叶子节点。叶子结点在AST遍历的时候一般也就是递归的终点。因为这里的查询比较简单,整棵AST只有一个节点,即根节点和叶子节点都是这个ComparisonExpr。

再来一个复杂点的例子。
select * from users where user_id = 1 and product_id =2

=>

&sqlparser.Where{
Type:"where",
Expr:(*sqlparser.AndExpr)(0xc42000b020)
}

AndExpr有Left和Right两个成员,分别是:

Left:
&sqlparser.ComparisonExpr{
Operator:"=",
Left:(*sqlparser.ColName)(0xc4200709c0),
Right:sqlparser.NumVal{0x31}
}

Right:
&sqlparser.ComparisonExpr{
Operator:"=",
Left:(*sqlparser.ColName)(0xc420070a50),
Right:sqlparser.NumVal{0x32}
}


稍微有一些二叉树的样子了吧。把这棵简单的树画出来:


Untitled1.png



回到文章开头的那个复杂的例子:
a and (b and (c or d) and e)

=>

select * from user_history where user_id = 1 and (product_id = 2 and (star_num = 4 or star_num = 5) and banned = 1)


看着真够麻烦的,我们把这棵树画出来:


Untitled.png



这样看着就直观多了。我们有了AST的结构,那要怎么对应到es的查询DSL呢?少安毋躁。

我们知道es的bool query是可以进行嵌套的,所以实际上我们可以同样可以构造出树形结构的bool query。这里把bool嵌套must和bool嵌套should简化一下,写成boolmust和boolshould:

例如a and (b and c)
query {
boolmust {
a,
boolmust {
b,
c
}
}
}


我们把query内部的第一个boolmust当作根节点,内部嵌套的a和另一个boolmust当作它的两个子节点,然后b和c又是这个boolmust的子节点。可以看出来,实际上这棵树和AST的节点可以一一对应。

再回到文章开头的例子,a and (b and (c or d) and e):
query {
boolmust {
a,
boolmust {
b,
boolshould {
c,
d
},
e
}
}
}

和前文中ast来做个简单的结构对比~


dsl和ast对比.png




和前文中sql的where解析后的AST树也是完全匹配的。思路来了,只要对sql解析生成的AST进行递归,即可得到这棵树。当然了,这里还可以进行一些优化,如果子节点的类型和父
节点的类型一致,例如都是and表达式或者都是or表达式,我们可以在生成dsl的时候将其作为并列的节点进行合并,这里不再赘述。


在递归中有这么几种情况:
AndExpr => bool must [{left}, {right}]
OrExpr => bool should [{left}, {right}]
ComparisonExpr => 一般是叶子节点
ParenBoolExpr => 指代括号表达式,其实内部是上述三种节点的某一种,所以直接取出内部节点按上述方法来处理


这样问题就变成了如何处理AST的叶子节点。前面提到了叶子节点实际上就是Comparison Expression。只要简单进行一些对应即可,下面是我们的项目里的一些对应关系,仅供参考:


convert.png

最后再附上demo
 
https://github.com/cch123/elasticsql
继续阅读 »
es现在几乎已经是开源搜索引擎的事实标准了,搭建简易,使用方便。不过在很多公司里(包括我司的部分部门),并不是把它当搜索引擎来用,而是当db来用。因为本身查询/搜索原理的区别,使es在千万或者亿级的数据中进行逻辑筛选相对高效。例如一些wms、工单查询系统,单表几十个甚至上百个字段,如果在数据库里为每种类型的查询都建立合适的索引,成本比较高,更不用说索引建多了还会影响到插入速度,后期的索引优化也是比较麻烦的问题。

不过如果把es当db来使的话,始终会有一个绕不过去的坎。就是es的DSL。让所有业务开发去学习dsl的话也不是不可以,但DSL真的有点反人类(不要打我)。简单的a and b或者a or b还比较容易写,如果我要的是a and (b and (c or d) and e)的查询逻辑,那我觉得谁写都会晕。即使是用官方或者第三方提供的client,如果需求多种多样的话,想要灵活地实现`需求=>DSL`的过程还是比较痛苦。

对于业务开发来说,当然是sql更平易近人(毕竟写了这么多年CRUD)。所以还有一种歪门邪道的流派,直接把sql转成DSL。要做sql和DSL转换的工作,需要进行sql的解析,先不要怵,这个年代找一个靠谱的sql parser还是比较容易的。比如阿里开源的druid连接池里的sql模块:
 
https://github.com/alibaba/dru ... d/sql

因为笔者的实现是用的下面这个golang版的parser:

https://github.com/xwb1989/sqlparser

所以用这个来举例吧~

这个是其作者从youtube/vitness里提取并进行改进的一个parser,我们能用到的是一部分子集功能,只需要解析select类的sql。

先举个简单的sql的例子:
select * from x_order where userId = 1 order by id desc limit 10,1;

解析之后会变成golang的一个struct,来看看具体的定义:

&sqlparser.Select{
Comments:sqlparser.Comments(nil),
Distinct:"",
SelectExprs:sqlparser.SelectExprs{(*sqlparser.StarExpr)(0xc42000aee0)},
From:sqlparser.TableExprs{(*sqlparser.AliasedTableExpr)(0xc420016930)},
Where:(*sqlparser.Where)(0xc42000afa0),
GroupBy:sqlparser.GroupBy(nil),
Having:(*sqlparser.Where)(nil),
OrderBy:sqlparser.OrderBy{(*sqlparser.Order)(0xc42000af20)},
Limit:(*sqlparser.Limit)(0xc42000af80),
Lock:""
}


sql的select语句在被解析之后生成一个Select的结构体,如果我们不关心使用者需要的字段的话,可以先把SelectExprs/Distinct/Comments/Lock里的内容忽略掉。如果不是分组统计类的需求,也可以先把GroupBy/Having忽略掉。这里我们关心的就剩下From、Where、OrderBy和Limit。

From对应的TableExprs实际上可以认为是简单的字符串,这里的值其实就是`x_order`。

OrderBy实际上是一个元素为
type Order struct {
Expr ValExpr
Direction string
}\

的数组。

Limit也很简单,
type Limit struct {
Offset, Rowcount ValExpr
}

其实就是俩数字。

那么剩下的就是这个Where结构了。where会被解析为AST(`https://en.wikipedia.org/wiki/Abstract_syntax_tree`),中文是抽象语法树。在不说子查询之类的情况下,这个AST也不会太复杂,毕竟where后面的情况比起编译原理里的程序语言来说情况还是要少得多的。以上述的sql为例,这里解析出来的Where结构是这样的:
&sqlparser.Where{
Type:"where",
Expr:(*sqlparser.ComparisonExpr)(0xc420016a50)
}


只有一个节点,一个ComparisonExpr表达式,这个ComparisonExpr,中文比较表达式,指代的就是我们sql里的`user_id = 1`。实际上我们可以认为这个"比较表达式"即是所有复杂AST的叶子节点。叶子结点在AST遍历的时候一般也就是递归的终点。因为这里的查询比较简单,整棵AST只有一个节点,即根节点和叶子节点都是这个ComparisonExpr。

再来一个复杂点的例子。
select * from users where user_id = 1 and product_id =2

=>

&sqlparser.Where{
Type:"where",
Expr:(*sqlparser.AndExpr)(0xc42000b020)
}

AndExpr有Left和Right两个成员,分别是:

Left:
&sqlparser.ComparisonExpr{
Operator:"=",
Left:(*sqlparser.ColName)(0xc4200709c0),
Right:sqlparser.NumVal{0x31}
}

Right:
&sqlparser.ComparisonExpr{
Operator:"=",
Left:(*sqlparser.ColName)(0xc420070a50),
Right:sqlparser.NumVal{0x32}
}


稍微有一些二叉树的样子了吧。把这棵简单的树画出来:


Untitled1.png



回到文章开头的那个复杂的例子:
a and (b and (c or d) and e)

=>

select * from user_history where user_id = 1 and (product_id = 2 and (star_num = 4 or star_num = 5) and banned = 1)


看着真够麻烦的,我们把这棵树画出来:


Untitled.png



这样看着就直观多了。我们有了AST的结构,那要怎么对应到es的查询DSL呢?少安毋躁。

我们知道es的bool query是可以进行嵌套的,所以实际上我们可以同样可以构造出树形结构的bool query。这里把bool嵌套must和bool嵌套should简化一下,写成boolmust和boolshould:

例如a and (b and c)
query {
boolmust {
a,
boolmust {
b,
c
}
}
}


我们把query内部的第一个boolmust当作根节点,内部嵌套的a和另一个boolmust当作它的两个子节点,然后b和c又是这个boolmust的子节点。可以看出来,实际上这棵树和AST的节点可以一一对应。

再回到文章开头的例子,a and (b and (c or d) and e):
query {
boolmust {
a,
boolmust {
b,
boolshould {
c,
d
},
e
}
}
}

和前文中ast来做个简单的结构对比~


dsl和ast对比.png




和前文中sql的where解析后的AST树也是完全匹配的。思路来了,只要对sql解析生成的AST进行递归,即可得到这棵树。当然了,这里还可以进行一些优化,如果子节点的类型和父
节点的类型一致,例如都是and表达式或者都是or表达式,我们可以在生成dsl的时候将其作为并列的节点进行合并,这里不再赘述。


在递归中有这么几种情况:
AndExpr => bool must [{left}, {right}]
OrExpr => bool should [{left}, {right}]
ComparisonExpr => 一般是叶子节点
ParenBoolExpr => 指代括号表达式,其实内部是上述三种节点的某一种,所以直接取出内部节点按上述方法来处理


这样问题就变成了如何处理AST的叶子节点。前面提到了叶子节点实际上就是Comparison Expression。只要简单进行一些对应即可,下面是我们的项目里的一些对应关系,仅供参考:


convert.png

最后再附上demo
 
https://github.com/cch123/elasticsql 收起阅读 »

Day3: 《创建一个你自己的 Beat》

Elastic Advent 第三篇, 手头上事情实在太多,这两天正在进行权威指南翻译的冲刺阶段,临时填下坑,翻译官网的一篇文章吧(原文:https://www.elastic.co/blog/build-your-own-beat),Advent 规则很自由的,没说不能翻译文章啊,嘿嘿嘿,另外号召大家踊跃报名,大家一起玩才有意思。
 
活动地址:http://elasticsearch.cn/article/107
 
言归正传!
 Beat 是一个开源的用来构建轻量级数据汇集的平台,可用于将各种类型的数据发送至Elasticsearch 与 Logstash。我们有 Packetbeat 用于监控局域网内服务器之间的网络流量信息,有 Filebeat 收集服务器上的日志信息,还有新推出的 Metricbeat 可以定期获取外部系统的监控指标信息,除此以外,你还可以非常方便的基于 libbeat 框架来构建你属于自己的专属 Beat,目前 beas 社区已经有超过25个 Community Beats 了。

Elastic 还提供一个 Beat generator(Beat 生成器)来帮你快速构建属于你自己的 Beat。通过这篇博客你将会看到如何通过 Beat 生成器来快速创建一个你自己的 Beat。今天我们创建的是一个叫做 lsbeat 的 Beat,lsbeat 非常类似 Unix 系统下的命令行 ls,我们用 lsbeat 来索引目录和文件信息。本篇文章环境基于 Unix 系统,如果你是 Windows 或是其它系统,相关操作可能需要根据实际情况进行调整。

第一步 – 配置 Golang 环境

Beats 是用 Golang写的,显然,要创建和开发一个 beat,Golang 环境必不可少,关于这方面的文章很多,建议查看这篇 Golang 的安装向导: install Golang。当前 Beats 需要的最低版本是 Golang 1.6。另外请确保正确设置了你的 $GOPATH 环境变量。

另外 Golang Glide 被用来进行包的依赖管理,所以也需要确保正确安装,最低版本是 Glide 0.10.0,安装说明点这里。

让我们先来看看 lsbeat 将会用到的一段代码吧,这是一个简单的 golang 程序,通过命令行接收一个目录参数,然后列出该目录下的文件和子目录信息。
package main

import (
"fmt"
"io/ioutil"
"os"
)

func main() {
//apply run path "." without argument.
if len(os.Args) == 1 {
listDir(".")
} else {
listDir(os.Args[1])
}
}

func listDir(dirFile string) {
files, _ := ioutil.ReadDir(dirFile)
for _, f := range files {
t := f.ModTime()
fmt.Println(f.Name(), dirFile+"/"+f.Name(), f.IsDir(), t, f.Size())

if f.IsDir() {
listDir(dirFile + "/" + f.Name())
}
}
}

后面我们将使用到这段代码和 listDir 函数。

第二步 – 生成项目

要生成一个你自己的 Beat,就要用到 beat-generator 了,首先你必须安装 cookiecutter。安装的详细说明看这里。安装好 cookiecutter 之后,我们要给自己的 Beat 起一个好听的名字,最好是小写的英文字母,我们今天这个例子就叫 lsbeat 吧。

生成项目模板之前,我们需要下载 Beats generator 包文件,就在 beats 仓库。安装好 GoLang 之后,你就可以很方便的使用 go get 命令来下载 Beats generator 包文件了。 当你执行下面的这个命令后,所有的源码文件都会下载到 $GOPATH/src 目录。
$ go get github.com/elastic/beats

在 GOPATH 下创建一个以你自己github账号名称命名的目录,并切换过去,然后执行 cookiecutter 命令并指向 Beat Generator 源码路径。
$ cd $GOPATH/src/github.com/{user}
$ cookiecutter $GOPATH/src/github.com/elastic/beats/generate/beat

Cookiecutter 接下来会问你几个问题,比如项目名称,我们输入:lsbeat;你的 github 用户名,输入你自己的 github 账户;还有两个关于beat和beat_path应该会自动识别,默认回车就好;最后的问题,你可以输入你的姓和名。
project_name [Examplebeat]: lsbeat
github_name [your-github-name]: {username}
beat [lsbeat]:
beat_path [github.com/{github id}]:
full_name [Firstname Lastname]: {Full Name}


现在应该已经创建好了一个名为 lsbeat 的目录,并且里面应该会生成一些文件,让我们一起来看一下吧,结构如下:
$ cd lsbeat
$ tree
.
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── README.md
├── beater
│ └── lsbeat.go
├── config
│ ├── config.go
│ └── config_test.go
├── dev-tools
│ └── packer
│ ├── Makefile
│ ├── beats
│ │ └── lsbeat.yml
│ └── version.yml
├── docs
│ └── index.asciidoc
├── etc
│ ├── beat.yml
│ └── fields.yml
├── glide.yaml
├── lsbeat.template.json
├── main.go
├── main_test.go
└── tests
└── system
├── config
│ └── lsbeat.yml.j2
├── lsbeat.py
├── requirements.txt
└── test_base.py


我们刚刚已经生成好了一个原始的 Beat 模板了,但是你还需要获取相关的依赖和设置好 git 仓库。

首先,你需要拉取依赖的相关包信息,我们的这个例子是 lsbeat,我们先做一些的基本的配置,回头再看看详细看看其它的模板和配置文件,只需要执行 make setup 就可以自动获取依赖。
$ make setup


当你创建好了自己的 Beat 之后,记得上传到 github 仓库,并和社区进行分享哦,如下:


beats.png



要 push lsbeat 到你的 git 仓库,只需要执行如下命令:
$ git remote add origin git@github.com:{username}/lsbeat.git
$ git push -u origin master


恭喜你,现在你已经完成了一个 Beat ,并且发布了第一个版本到了 Github,不过里面还没有什么具体内容,现在让我们进一步看看里面的代码吧。

第四步 – 配置

执行过上面一系列命令之后,项目里将会自动创建名为 lsbeat.yml 和 lsbeat.template.json 的配置文件。所有的基本配置项都已经生成在了里面。
lsbeat.yml
lsbeat:
# Defines how often an event is sent to the output
period: 1s


Period 参数包含在每一个生成的 Beats 里面,它表示 lsbeat 将会每隔 1 秒钟轮询一次,这里我们修改 period 时间间隔为 10 秒。还可以在修改 etc/ 目录下面的 beat.yml 文件,这里新增一个 path 参数表示我们具体要监听哪个目录。
lsbeat:
# Defines how often an event is sent to the output
period: 10s
path: "."


参数添加好了之后,我们只需要运行 make update 命令就能让这些修改应用到配置文件lsbeat.yml。
$ make update
$ cat lsbeat.yml

################### Lsbeat Configuration Example #########################

############################# Lsbeat ######################################

lsbeat:
# Defines how often an event is sent to the output
period: 10s
path: "."
###############################################################################


修改完 yml 文件,记得修改 config/config.go文件,添加一个path 参数。
package config

import "time"

type Config struct {
Period time.Duration `config:"period"`
Path string `config:"path"`
}

var DefaultConfig = Config{
Period: 10 * time.Second,
Path: ".",
}


同时我们修改 period 默认时间间隔为 10 秒,默认监听的是当前目录 (.) 。.

第五步 – 添加代码

每一个 Beat 需要实现 Beater 接口,里面定义了 Run() 和 Stop() 函数。. 

我们可以定义一个名为 Lsbeat 的结构体,然后用这个对象实现 Beater 接口。然后添加字段 lastIndexTime 来保存最后运行的时间戳信息。
type Lsbeat struct {
done chan struct{}
config config.Config
client publisher.Client

lastIndexTime time.Time
...
}


另外,每个 Beat 还需要实现 New() 方法来接收 Beat 配置信息和返回 Lsbeat 的具体实例。
func New(b *beat.Beat, cfg *common.Config) (beat.Beater, error) {
config := config.DefaultConfig
if err := cfg.Unpack(&config); err != nil {
return nil, fmt.Errorf("Error reading config file: %v", err)
}

ls := &Lsbeat{
done: make(chan struct{}),
config: config,
}
return ls, nil
}


在我们的 lsbeat 例子中,我们要做的就是扩展默认的 Run() 函数来导出指定目录的文件和子目录信息。

在修改 Run() 函数之前,我们先在 lsbeat.go 增加 listDir() 函数,就是我们前面最开始测试的那段代码,用于收集目录和文件信息的简单例子稍微修改一下。另外我们还要生成以下字段信息:
"@timestamp": common.Time(time.Now()),
"type": beatname,
"modtime": common.Time(t),
"filename": f.Name(),
"path": dirFile + "/" + f.Name(),
"directory": f.IsDir(),
"filesize": f.Size(),


第一次运行的时候我们将索引所有的文件和目录信息,然后我们再定期检查是否有新文件被创建或者修改,再索引这些新创建的文件和目录。每次定期检查的时间戳都会保存在 lasIndexTime 变量,完整代码如下:
func (bt *Lsbeat) listDir(dirFile string, beatname string, init bool) {
files, _ := ioutil.ReadDir(dirFile)
for _, f := range files {
t := f.ModTime()

event := common.MapStr{
"@timestamp": common.Time(time.Now()),
"type": beatname,
"modtime": common.Time(t),
"filename": f.Name(),
"path": dirFile + "/" + f.Name(),
"directory": f.IsDir(),
"filesize": f.Size(),
}
if init {
// index all files and directories on init
bt.client.PublishEvent(event)
} else {
// Index only changed files since last run.
if t.After(bt.lastIndexTime) {
bt.client.PublishEvent(event)
}
}

if f.IsDir() {
bt.listDir(dirFile+"/"+f.Name(), beatname, init)
}
}
}


记住在最开始需要导入 “io/ioutil” 包。
import (
"fmt"
"io/ioutil"
"time"
)


现在,让我们看看如何在 Run() 函数里面调用 listDir() 函数,并且保存时间戳到 lasIndexTime 变量。
func (bt *Lsbeat) Run(b *beat.Beat) error {
logp.Info("lsbeat is running! Hit CTRL-C to stop it.")

bt.client = b.Publisher.Connect()
ticker := time.NewTicker(bt.config.Period)
counter := 1
for {

select {
case <-bt.done:
return nil
case <-ticker.C:
}

bt.listDir(bt.config.Path, b.Name, true) // call lsDir
bt.lastIndexTime = time.Now() // mark Timestamp

logp.Info("Event sent")
counter++
}

}


函数 Stop() 用来中断 run 的循环执行,保持默认生成的就行。
func (bt *Lsbeat) Stop() {
bt.client.Close()
close(bt.done)
}


到这里,编码部分基本就完成了。我们接下来添加新字段到 mapping 中,修改文件 etc/fields.yml。.
- key: lsbeat
title: LS Beat
description:
fields:
- name: counter
type: integer
required: true
description: >
PLEASE UPDATE DOCUMENTATION
#new fiels added lsbeat
- name: modtime
type: date
- name: filename
type: text
- name: path
- name: directory
type: boolean
- name: filesize
type: long


重新应用新的配置。

$ make update

字段 file_name 将使用 nGram 分词,我们还需要在文件 lsbeat.template.json 的 “settings” 节点添加一个自定义的 analyzer。
{
"mappings": {
...
},
"order": 0,
"settings": {
"index.refresh_interval": "5s",
"analysis": {
"analyzer": {
"ls_ngram_analyzer": {
"tokenizer": "ls_ngram_tokenizer"
}
},
"tokenizer": {
"ls_ngram_tokenizer": {
"type": "ngram",
"min_gram": "2",
"token_chars": [
"letter",
"digit"
]
}
}
}
},
"template": "lsbeat-*"
}


第六步 – 编译和运行

现在我们可以编译和运行了,只需要执行 make 命令就可以编译出可执行文件 lsbeat (lsbeat.exe on windows) 。

$ make

修改 lsbeat.yml 文件,设置需要监听的目录,如: “/Users/ec2-user/go”,记住是全路径。
lsbeat:
# Defines how often an event is sent to the output
period: 10s
path: "/Users/ec2-user/go"

同时确保你的 elasticsearch 和 kibana 正常运行。现在运行一下 lsbeat 命令看看会发生什么事情吧。

$ ./lsbeat

打开Kibana,通过调用 _cat 接口我们看看的索引是不是创建了。

beats-1.png


可以看到创建了一个名为 lsbeat-2016.06.03 的索引,并且看到已经有了一些文档了。现在对 filename 字段查询一下,由于使用的是 nGram 分词,支持模糊匹配,我们使用 lsbe 关键字搜一下。

beats-2.png


大功告成! 恭喜你,你已经完成了第一个属于你自己的 beat。
继续阅读 »
Elastic Advent 第三篇, 手头上事情实在太多,这两天正在进行权威指南翻译的冲刺阶段,临时填下坑,翻译官网的一篇文章吧(原文:https://www.elastic.co/blog/build-your-own-beat),Advent 规则很自由的,没说不能翻译文章啊,嘿嘿嘿,另外号召大家踊跃报名,大家一起玩才有意思。
 
活动地址:http://elasticsearch.cn/article/107
 
言归正传!
 Beat 是一个开源的用来构建轻量级数据汇集的平台,可用于将各种类型的数据发送至Elasticsearch 与 Logstash。我们有 Packetbeat 用于监控局域网内服务器之间的网络流量信息,有 Filebeat 收集服务器上的日志信息,还有新推出的 Metricbeat 可以定期获取外部系统的监控指标信息,除此以外,你还可以非常方便的基于 libbeat 框架来构建你属于自己的专属 Beat,目前 beas 社区已经有超过25个 Community Beats 了。

Elastic 还提供一个 Beat generator(Beat 生成器)来帮你快速构建属于你自己的 Beat。通过这篇博客你将会看到如何通过 Beat 生成器来快速创建一个你自己的 Beat。今天我们创建的是一个叫做 lsbeat 的 Beat,lsbeat 非常类似 Unix 系统下的命令行 ls,我们用 lsbeat 来索引目录和文件信息。本篇文章环境基于 Unix 系统,如果你是 Windows 或是其它系统,相关操作可能需要根据实际情况进行调整。

第一步 – 配置 Golang 环境

Beats 是用 Golang写的,显然,要创建和开发一个 beat,Golang 环境必不可少,关于这方面的文章很多,建议查看这篇 Golang 的安装向导: install Golang。当前 Beats 需要的最低版本是 Golang 1.6。另外请确保正确设置了你的 $GOPATH 环境变量。

另外 Golang Glide 被用来进行包的依赖管理,所以也需要确保正确安装,最低版本是 Glide 0.10.0,安装说明点这里。

让我们先来看看 lsbeat 将会用到的一段代码吧,这是一个简单的 golang 程序,通过命令行接收一个目录参数,然后列出该目录下的文件和子目录信息。
package main

import (
"fmt"
"io/ioutil"
"os"
)

func main() {
//apply run path "." without argument.
if len(os.Args) == 1 {
listDir(".")
} else {
listDir(os.Args[1])
}
}

func listDir(dirFile string) {
files, _ := ioutil.ReadDir(dirFile)
for _, f := range files {
t := f.ModTime()
fmt.Println(f.Name(), dirFile+"/"+f.Name(), f.IsDir(), t, f.Size())

if f.IsDir() {
listDir(dirFile + "/" + f.Name())
}
}
}

后面我们将使用到这段代码和 listDir 函数。

第二步 – 生成项目

要生成一个你自己的 Beat,就要用到 beat-generator 了,首先你必须安装 cookiecutter。安装的详细说明看这里。安装好 cookiecutter 之后,我们要给自己的 Beat 起一个好听的名字,最好是小写的英文字母,我们今天这个例子就叫 lsbeat 吧。

生成项目模板之前,我们需要下载 Beats generator 包文件,就在 beats 仓库。安装好 GoLang 之后,你就可以很方便的使用 go get 命令来下载 Beats generator 包文件了。 当你执行下面的这个命令后,所有的源码文件都会下载到 $GOPATH/src 目录。
$ go get github.com/elastic/beats

在 GOPATH 下创建一个以你自己github账号名称命名的目录,并切换过去,然后执行 cookiecutter 命令并指向 Beat Generator 源码路径。
$ cd $GOPATH/src/github.com/{user}
$ cookiecutter $GOPATH/src/github.com/elastic/beats/generate/beat

Cookiecutter 接下来会问你几个问题,比如项目名称,我们输入:lsbeat;你的 github 用户名,输入你自己的 github 账户;还有两个关于beat和beat_path应该会自动识别,默认回车就好;最后的问题,你可以输入你的姓和名。
project_name [Examplebeat]: lsbeat
github_name [your-github-name]: {username}
beat [lsbeat]:
beat_path [github.com/{github id}]:
full_name [Firstname Lastname]: {Full Name}


现在应该已经创建好了一个名为 lsbeat 的目录,并且里面应该会生成一些文件,让我们一起来看一下吧,结构如下:
$ cd lsbeat
$ tree
.
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── README.md
├── beater
│ └── lsbeat.go
├── config
│ ├── config.go
│ └── config_test.go
├── dev-tools
│ └── packer
│ ├── Makefile
│ ├── beats
│ │ └── lsbeat.yml
│ └── version.yml
├── docs
│ └── index.asciidoc
├── etc
│ ├── beat.yml
│ └── fields.yml
├── glide.yaml
├── lsbeat.template.json
├── main.go
├── main_test.go
└── tests
└── system
├── config
│ └── lsbeat.yml.j2
├── lsbeat.py
├── requirements.txt
└── test_base.py


我们刚刚已经生成好了一个原始的 Beat 模板了,但是你还需要获取相关的依赖和设置好 git 仓库。

首先,你需要拉取依赖的相关包信息,我们的这个例子是 lsbeat,我们先做一些的基本的配置,回头再看看详细看看其它的模板和配置文件,只需要执行 make setup 就可以自动获取依赖。
$ make setup


当你创建好了自己的 Beat 之后,记得上传到 github 仓库,并和社区进行分享哦,如下:


beats.png



要 push lsbeat 到你的 git 仓库,只需要执行如下命令:
$ git remote add origin git@github.com:{username}/lsbeat.git
$ git push -u origin master


恭喜你,现在你已经完成了一个 Beat ,并且发布了第一个版本到了 Github,不过里面还没有什么具体内容,现在让我们进一步看看里面的代码吧。

第四步 – 配置

执行过上面一系列命令之后,项目里将会自动创建名为 lsbeat.yml 和 lsbeat.template.json 的配置文件。所有的基本配置项都已经生成在了里面。
lsbeat.yml
lsbeat:
# Defines how often an event is sent to the output
period: 1s


Period 参数包含在每一个生成的 Beats 里面,它表示 lsbeat 将会每隔 1 秒钟轮询一次,这里我们修改 period 时间间隔为 10 秒。还可以在修改 etc/ 目录下面的 beat.yml 文件,这里新增一个 path 参数表示我们具体要监听哪个目录。
lsbeat:
# Defines how often an event is sent to the output
period: 10s
path: "."


参数添加好了之后,我们只需要运行 make update 命令就能让这些修改应用到配置文件lsbeat.yml。
$ make update
$ cat lsbeat.yml

################### Lsbeat Configuration Example #########################

############################# Lsbeat ######################################

lsbeat:
# Defines how often an event is sent to the output
period: 10s
path: "."
###############################################################################


修改完 yml 文件,记得修改 config/config.go文件,添加一个path 参数。
package config

import "time"

type Config struct {
Period time.Duration `config:"period"`
Path string `config:"path"`
}

var DefaultConfig = Config{
Period: 10 * time.Second,
Path: ".",
}


同时我们修改 period 默认时间间隔为 10 秒,默认监听的是当前目录 (.) 。.

第五步 – 添加代码

每一个 Beat 需要实现 Beater 接口,里面定义了 Run() 和 Stop() 函数。. 

我们可以定义一个名为 Lsbeat 的结构体,然后用这个对象实现 Beater 接口。然后添加字段 lastIndexTime 来保存最后运行的时间戳信息。
type Lsbeat struct {
done chan struct{}
config config.Config
client publisher.Client

lastIndexTime time.Time
...
}


另外,每个 Beat 还需要实现 New() 方法来接收 Beat 配置信息和返回 Lsbeat 的具体实例。
func New(b *beat.Beat, cfg *common.Config) (beat.Beater, error) {
config := config.DefaultConfig
if err := cfg.Unpack(&config); err != nil {
return nil, fmt.Errorf("Error reading config file: %v", err)
}

ls := &Lsbeat{
done: make(chan struct{}),
config: config,
}
return ls, nil
}


在我们的 lsbeat 例子中,我们要做的就是扩展默认的 Run() 函数来导出指定目录的文件和子目录信息。

在修改 Run() 函数之前,我们先在 lsbeat.go 增加 listDir() 函数,就是我们前面最开始测试的那段代码,用于收集目录和文件信息的简单例子稍微修改一下。另外我们还要生成以下字段信息:
"@timestamp": common.Time(time.Now()),
"type": beatname,
"modtime": common.Time(t),
"filename": f.Name(),
"path": dirFile + "/" + f.Name(),
"directory": f.IsDir(),
"filesize": f.Size(),


第一次运行的时候我们将索引所有的文件和目录信息,然后我们再定期检查是否有新文件被创建或者修改,再索引这些新创建的文件和目录。每次定期检查的时间戳都会保存在 lasIndexTime 变量,完整代码如下:
func (bt *Lsbeat) listDir(dirFile string, beatname string, init bool) {
files, _ := ioutil.ReadDir(dirFile)
for _, f := range files {
t := f.ModTime()

event := common.MapStr{
"@timestamp": common.Time(time.Now()),
"type": beatname,
"modtime": common.Time(t),
"filename": f.Name(),
"path": dirFile + "/" + f.Name(),
"directory": f.IsDir(),
"filesize": f.Size(),
}
if init {
// index all files and directories on init
bt.client.PublishEvent(event)
} else {
// Index only changed files since last run.
if t.After(bt.lastIndexTime) {
bt.client.PublishEvent(event)
}
}

if f.IsDir() {
bt.listDir(dirFile+"/"+f.Name(), beatname, init)
}
}
}


记住在最开始需要导入 “io/ioutil” 包。
import (
"fmt"
"io/ioutil"
"time"
)


现在,让我们看看如何在 Run() 函数里面调用 listDir() 函数,并且保存时间戳到 lasIndexTime 变量。
func (bt *Lsbeat) Run(b *beat.Beat) error {
logp.Info("lsbeat is running! Hit CTRL-C to stop it.")

bt.client = b.Publisher.Connect()
ticker := time.NewTicker(bt.config.Period)
counter := 1
for {

select {
case <-bt.done:
return nil
case <-ticker.C:
}

bt.listDir(bt.config.Path, b.Name, true) // call lsDir
bt.lastIndexTime = time.Now() // mark Timestamp

logp.Info("Event sent")
counter++
}

}


函数 Stop() 用来中断 run 的循环执行,保持默认生成的就行。
func (bt *Lsbeat) Stop() {
bt.client.Close()
close(bt.done)
}


到这里,编码部分基本就完成了。我们接下来添加新字段到 mapping 中,修改文件 etc/fields.yml。.
- key: lsbeat
title: LS Beat
description:
fields:
- name: counter
type: integer
required: true
description: >
PLEASE UPDATE DOCUMENTATION
#new fiels added lsbeat
- name: modtime
type: date
- name: filename
type: text
- name: path
- name: directory
type: boolean
- name: filesize
type: long


重新应用新的配置。

$ make update

字段 file_name 将使用 nGram 分词,我们还需要在文件 lsbeat.template.json 的 “settings” 节点添加一个自定义的 analyzer。
{
"mappings": {
...
},
"order": 0,
"settings": {
"index.refresh_interval": "5s",
"analysis": {
"analyzer": {
"ls_ngram_analyzer": {
"tokenizer": "ls_ngram_tokenizer"
}
},
"tokenizer": {
"ls_ngram_tokenizer": {
"type": "ngram",
"min_gram": "2",
"token_chars": [
"letter",
"digit"
]
}
}
}
},
"template": "lsbeat-*"
}


第六步 – 编译和运行

现在我们可以编译和运行了,只需要执行 make 命令就可以编译出可执行文件 lsbeat (lsbeat.exe on windows) 。

$ make

修改 lsbeat.yml 文件,设置需要监听的目录,如: “/Users/ec2-user/go”,记住是全路径。
lsbeat:
# Defines how often an event is sent to the output
period: 10s
path: "/Users/ec2-user/go"

同时确保你的 elasticsearch 和 kibana 正常运行。现在运行一下 lsbeat 命令看看会发生什么事情吧。

$ ./lsbeat

打开Kibana,通过调用 _cat 接口我们看看的索引是不是创建了。

beats-1.png


可以看到创建了一个名为 lsbeat-2016.06.03 的索引,并且看到已经有了一些文档了。现在对 filename 字段查询一下,由于使用的是 nGram 分词,支持模糊匹配,我们使用 lsbe 关键字搜一下。

beats-2.png


大功告成! 恭喜你,你已经完成了第一个属于你自己的 beat。 收起阅读 »

开源一组ES工具包,请大家帮忙宣传下,谢谢

https://github.com/DataSays/wES/ 

wES 是一组开源的Java ElasticSearch客户端和工具; 简洁但是很勥 :)
wES = Java Retrofit2/OkHttp版本的客户端(不依赖Json类库,高度可定制) + 工具包 + spring-boot demo + 常用的ElasticSearch环境Dockerfile

wES 模块
wES 分割成许多模块, 可以按需选择.
wUtil: 一些有用的帮助类和工具类.
wES-client: 一个基于Retrofit2/OkHttp的Java客户端, 她是基于官方的ElasticSearch Rest API规范生成的. 她包含两种访问ElasticSearch的实现: OkHttp3版本的和Retrofit2版本. 而且她只依赖okhttp3/retrofit2,并且可以通过实现org.datasays.wes.core.IConvert接口支持任意一种Java Json类库. 你可以按照你的想法使用她.
wES-toolkit: 一些使用wES-client和ElasticSearch的工具包. 她包含了一套基于Gson的标准IConvert实现及封装代码库.
wES-demo: 一个Spring-boot + Vue.js的web应用, 用于展示一些通用的ElasticSearch使用场景.
wES-docker: 一些构建ElasticSearch开发/生产环境的常用Dockerfiles和shell脚本.

 
继续阅读 »
https://github.com/DataSays/wES/ 

wES 是一组开源的Java ElasticSearch客户端和工具; 简洁但是很勥 :)
wES = Java Retrofit2/OkHttp版本的客户端(不依赖Json类库,高度可定制) + 工具包 + spring-boot demo + 常用的ElasticSearch环境Dockerfile

wES 模块
wES 分割成许多模块, 可以按需选择.
wUtil: 一些有用的帮助类和工具类.
wES-client: 一个基于Retrofit2/OkHttp的Java客户端, 她是基于官方的ElasticSearch Rest API规范生成的. 她包含两种访问ElasticSearch的实现: OkHttp3版本的和Retrofit2版本. 而且她只依赖okhttp3/retrofit2,并且可以通过实现org.datasays.wes.core.IConvert接口支持任意一种Java Json类库. 你可以按照你的想法使用她.
wES-toolkit: 一些使用wES-client和ElasticSearch的工具包. 她包含了一套基于Gson的标准IConvert实现及封装代码库.
wES-demo: 一个Spring-boot + Vue.js的web应用, 用于展示一些通用的ElasticSearch使用场景.
wES-docker: 一些构建ElasticSearch开发/生产环境的常用Dockerfiles和shell脚本.

  收起阅读 »

Day2:《Kibana 系漫游指南》

大家好,欢迎你们来到 ELK 三体星系的第二天。昨天,wood 送给大家一本脚踏实地的生存指南,今天让我们仰望星空,由我给大家介绍一下围绕在 Kibana 身边的诸多行星们~

Kibana Plugin 类型简介

我们最熟悉的 Kibana Plugin,其实就是 Kibana 本身~ Kibana 提供了一整套框架,我们可以在此基础上,开发诸多不同类型的插件,包括:
  • app
  • visTypes
  • spyModes
  • fieldFormatter


列这么几个源码里的名词出来可能大家觉得比较晦涩。其实呢,app 就是同时具有前后端实现的应用,在 Kibana 5 里,默认分发的 app 有四个:实现日志查询和可视化的 kibana app、实现时序指标统计和可视化的 timelion app、实现和 ES 接口交互命令的 console app、在有异常的时候才看得到的状态页面 status_page app。

而 visTypes 则是在 kibana 中具体可用的可视化效果。默认分发的有:kbn_vislib_vis_types、metric、table、markdown。我们常用的那些由 D3.js 完成的饼图线图地图,都是在 kbn_vislib_vis_types 中完成的。

fieldFormatter 则用来定义在 ES 中相同类型的数据,根据其实际含义,可以有不同的展示方式。比如说:URL 肯定是一个字符串,但是可以用 fieldFormatter 把它在页面展示的时候,加上 `<a href></a>` 的样式,让人一键点击;同理,还可以过滤判断一下图片类 URL,加上 `<img src></img>` 的样式,直接在 Kibana 界面上就看图片内容~~

官方的我们会看手册啦~

好啦好啦,我也不会真的去抄一把官方手册假冒《Kibana 系漫游指南》来骗你们流量的。下面给大家介绍一些社区开源的,让你绝对眼前一亮的各种新奇扩展:

1. logtrail
    这是一个 app 插件,创意来自 papertrail 公司的产品。完全的满足了 Geeker 们喜欢黑底白字终端的癖好~不过其实实现非常简单:每隔 10 秒请求一次最近 500 条日志就是啦!

2. vectormap
    这是一个 visType 插件,也就是我们在 Kibana3 里曾经用过的 map panel 效果。这个插件不被官方直接采用的一个原因是版权许可问题。不做商用的情况下,这个插件还是可以极大方便我们做行政区域的访问情况统计和展示的。
    
3. kbn_network
    这也是一个 visType 插件,酷毙了的网状图效果!通过不同的 aggs 数据展示 node 和 relational。
    注意这个跟 Elastic 的 graph 并不是完全一致的东西。该插件要求你本身的数据已经有直接的关联可用。
    
4. sentinl
    这是一个同时带有 spyMode 和 app 双插件的项目。其基础思路是参照 Elastic 的 Watcher 接口,但是将监控告警的进程从 ES 挪到 Kibana 里。同时还可以通过 phantomjs 做到截图报表。
    
    这个项目最大的特点,是通过 spyMode 插件,大大降低了配置告警规则的复杂度。这个扩展让你可以在 Kibana 上配置任意聚合效果之后,就地点击定义当前聚合语句为告警规则!
    
5. kibana-keynote
    这是另一个剑走偏锋的 app 插件,出自 Kibana 作者本人之手。它的作用是:播放 keynote 演讲稿!事实上项目里放的演讲稿就是作者本人在 ELastic{ON} 2016 上用的。让我们猜一猜下周的大会上,他会不会就用这个插件给我们分享呢?

今天就先讲这几颗最闪亮的星了~有兴趣了解更多 Kibana 行星的游客,欢迎阅读全本《Kibana系漫游指南》

也欢迎观看 Kibana 行星的《探索·发现》节目哟~
继续阅读 »
大家好,欢迎你们来到 ELK 三体星系的第二天。昨天,wood 送给大家一本脚踏实地的生存指南,今天让我们仰望星空,由我给大家介绍一下围绕在 Kibana 身边的诸多行星们~

Kibana Plugin 类型简介

我们最熟悉的 Kibana Plugin,其实就是 Kibana 本身~ Kibana 提供了一整套框架,我们可以在此基础上,开发诸多不同类型的插件,包括:
  • app
  • visTypes
  • spyModes
  • fieldFormatter


列这么几个源码里的名词出来可能大家觉得比较晦涩。其实呢,app 就是同时具有前后端实现的应用,在 Kibana 5 里,默认分发的 app 有四个:实现日志查询和可视化的 kibana app、实现时序指标统计和可视化的 timelion app、实现和 ES 接口交互命令的 console app、在有异常的时候才看得到的状态页面 status_page app。

而 visTypes 则是在 kibana 中具体可用的可视化效果。默认分发的有:kbn_vislib_vis_types、metric、table、markdown。我们常用的那些由 D3.js 完成的饼图线图地图,都是在 kbn_vislib_vis_types 中完成的。

fieldFormatter 则用来定义在 ES 中相同类型的数据,根据其实际含义,可以有不同的展示方式。比如说:URL 肯定是一个字符串,但是可以用 fieldFormatter 把它在页面展示的时候,加上 `<a href></a>` 的样式,让人一键点击;同理,还可以过滤判断一下图片类 URL,加上 `<img src></img>` 的样式,直接在 Kibana 界面上就看图片内容~~

官方的我们会看手册啦~

好啦好啦,我也不会真的去抄一把官方手册假冒《Kibana 系漫游指南》来骗你们流量的。下面给大家介绍一些社区开源的,让你绝对眼前一亮的各种新奇扩展:

1. logtrail
    这是一个 app 插件,创意来自 papertrail 公司的产品。完全的满足了 Geeker 们喜欢黑底白字终端的癖好~不过其实实现非常简单:每隔 10 秒请求一次最近 500 条日志就是啦!

2. vectormap
    这是一个 visType 插件,也就是我们在 Kibana3 里曾经用过的 map panel 效果。这个插件不被官方直接采用的一个原因是版权许可问题。不做商用的情况下,这个插件还是可以极大方便我们做行政区域的访问情况统计和展示的。
    
3. kbn_network
    这也是一个 visType 插件,酷毙了的网状图效果!通过不同的 aggs 数据展示 node 和 relational。
    注意这个跟 Elastic 的 graph 并不是完全一致的东西。该插件要求你本身的数据已经有直接的关联可用。
    
4. sentinl
    这是一个同时带有 spyMode 和 app 双插件的项目。其基础思路是参照 Elastic 的 Watcher 接口,但是将监控告警的进程从 ES 挪到 Kibana 里。同时还可以通过 phantomjs 做到截图报表。
    
    这个项目最大的特点,是通过 spyMode 插件,大大降低了配置告警规则的复杂度。这个扩展让你可以在 Kibana 上配置任意聚合效果之后,就地点击定义当前聚合语句为告警规则!
    
5. kibana-keynote
    这是另一个剑走偏锋的 app 插件,出自 Kibana 作者本人之手。它的作用是:播放 keynote 演讲稿!事实上项目里放的演讲稿就是作者本人在 ELastic{ON} 2016 上用的。让我们猜一猜下周的大会上,他会不会就用这个插件给我们分享呢?

今天就先讲这几颗最闪亮的星了~有兴趣了解更多 Kibana 行星的游客,欢迎阅读全本《Kibana系漫游指南》

也欢迎观看 Kibana 行星的《探索·发现》节目哟~ 收起阅读 »