elasticsearch服务开发总结

前几天在公司做了一次分享,其实是类似于工作汇报的东西。。。顺便记了一些东西,整理一下吧:

这个服务完成了的事情

1).独立的es集群

目前线上正在运行的版本是1.7.3

三台机器,总数据量大概在100G左右(算上分片的冗余)

集群上安装有head、bigdesk、ik分词器等插件

存在的问题:

es版本其实已经有点老了。。官方在2.1和2.2又加入了一些新特性,这些特性在老版本里我们享受不到,当然现在的接入功能也比较简单,也不一定用得到。但不升级自然就享受不到官方版本升级带来的红利:新功能,新bug

2).在es之前封装了一层web服务

用golang的web框架gin

https://github.com/gin-gonic/gin

和基于

https://github.com/walm/elasticsearch

这个客户端,做了一些bug修正,写了一些新bug

web服务前接了公司内部的inrouter来做负载均衡,这个inrouter也比较有意思,其实是在nginx里做了一次proxy_pass,然后又使用了nginx的第三方upstream模块来实现负载均衡和机器存活检测,有时间会在这里补一下我司的配置。不过这种配置明眼人也能一下就看出问题所在,因为存活检测需要一定的时间才能判断机器是否存活,所以机器如果真的挂了,那在那几秒~几十秒的时间范围内,你的请求还是会有一部分打到失效机器去。所以是有一些问题的,所以还是基于watch zk/etcd之类的服务发现靠谱呐,可惜php做不了。

存在的问题:

由于使用的es客户端的问题,接口定义死板,定制性不强,在年后用了功能更强大的client后才意思到自己去按照template拼字符串是多么的愚蠢

支持功能孱弱,目前只支持单表的多条件and查询,一点都不灵活

错误信息一点都不人性化,给鬼看的,之前接入的同学来找我吐过槽了。服务json decode直接看到cannot decode from string to int64,谁知道你是哪个字段的问题啊。。

3).接入了公司的kafka集群,消费mysql产生的binlog,用golang的

https://github.com/wvanbergen/kafka

客户端实现,从我们内部的监控系统来看,实际上整个工单系统的数据库的binlog数据量也就大概50条/30s30条/秒,所以消费者也没什么压力,在kafka里目前只有一个partition。消费程序写起来简直愉♂悦(其实用kafka的high level api怎么写都很愉快)。不过从这个数据量来看,其实本身这个系统的QPS也应该不怎么高(创建插入大概在每秒30左右,总qps应该也就100左右吧),那么目前整个系统还需要4台物理机器来运行就让人觉得有点匪夷所思了。当然了,也跟这个系统使用的是低效的php 5.3和落后的yii 1.1脱不了干系。平均下来一台机器qps才25。。。当然了,这种工单系统和京东之类的电商后台系统也有一些类似,查询非常多,这样也势必会造成很难通过优化mysql的索引来优化查询效率,总会有index miss。所以我们如果能把大部分慢查询迁移到es的话,也能一定程度上的提高整个系统的qps。

存在的问题:

因为公司业务量比较大,所以单表增长速度较快,为了提升mysql本身的查询效率,我们会在每天夜里做冷数据(三个月前的工单)迁出。也就是说每天会产生大概20-40w的delete的binlog(row format式的binlog,每删除一行数据都会产生一条binlog)以及同等数量的insert的binlog。其实是数据里最好把delete的binlog给过滤掉。。例如目前的工单数据,每晚20w条删除记录,集中产生的binlog会使得消费程序因为这些本来不需要处理的binlog而延迟需要处理的数据的速度。所以晚上2点左右会有一部分数据经过10分钟左右的延迟,才从kafka->elasticsearch。不过这个问题联系了一下负责的同学,可以把数据库的delete的binlog屏蔽掉,看起来效果就好多了~实际上在很多公司,数据的物理删除也确实都是不允许的,应该要在每一条记录上有删除位。只对数据进行更新和插入操作(然而在我们这里,规范什么的,呵呵)。

开发过程中的踩坑

1)-elasticsearch服务节点可以自动发现并加入到集群,

但前提是这些nodes必须在同一个网段,否则需要手动配置discovery的host名或ip

且配置的集群名相同

2)-es的mapping并不是强制约束,和mysql的schema不一样

比如我的字段customer_type是long,但在插入数据的时候只会做数据校验,而不会进行类型转换,这样会导致前后document同一字段的数据类型不一致,在强类型语言中做json解析是会有问题的

3)-虚拟机配置环境时,可能会遇到gc问题

es本身问题可能会因为gc停顿,jdbc脚本也会因为gc停顿

4)-1.7和2.0的各种兼容问题

不兼容2.0的插件本身只要打开就会一直发送ajax请求,会产生大量的错误日志

5)-index.refresh_interval

这个属性决定插入的数据什么时候可以被检索到,也就是搜索引擎near real-time的原因,如果你在插入数据后发现始终查询不到,那么去检查一下refresh_interval

同时refresh是代价比较高的操作,如果写入频率高,那么应该适当将值改大一些

6)-部署问题

分词之类的插件安装需要保证每个节点都有,因为本身数据分片分布在各台机器上,如果在mapping中指定了分词器而有些节点上没有插件,那么也会报错

7)-elasticsearch-jdbc

这个工具可真是坑惨了亲啊,
最早的时候按照工具官方提供的方案写sql,采用数据库里的update_time字段来判断是否有新数据需要导入到es,所以写出下面的sql:

select * from [业务表] where update_time > [脚本上一次启动时的时间]

写完之后去核对我们的业务数据,发现堂堂部门最重要的工单系统竟然大多数的数据根本就没有填update_time这个字段。但是看起来创建时间的字段有老老实实地按规矩填着,所以对sql进行了一些修改:

select * from [业务表] where update_time > [脚本上次启动时间] or create_time >[脚本上次启动时间]

写好sql之后去explain一发,然后傻眼了,根据数据库里的索引情况,实际会使用create_time索引或者update_time字段并不安定。也有可能会显示没有用上任何一个索引,所以这个sql显然是有问题的,可能会造成全表扫描。为了能同时用上create_time字段和update_time的索引,修改sql如下:

select * from [业务表] where update_time > [上次执行时间] union select * from [业务表] where create_time > [上次执行时间]

再来explain一次,看起来很完美了。实际上线之后发现还是有数据丢失,思考良久才明白大概是我司的主从同步延迟的问题。虽然dba声称延迟在秒级,但实际上呵呵呵。有了主从延迟这种客观条件,这个方案看起来就不是那么美好了,在征求dba同意后,我们在sql中加上了注释,让dbproxy在解析的时候把请求发往主库:

/* router m */ SQL和上面一样

这次还是遇到了莫名其妙少量数据丢失的问题,把jdbc工具的日志打开生成了几个GB的日志之后还是没有很好地排查出结果。但是每个阶段似乎逻辑都是正确的。经过一段时间的问题定位,发现工单系统是有可能在未来创建过去一段时间的工单的,这些工单数据来自于其它系统。但在这个项目启动阶段,我几乎问了所有的rd,没有任何人知道这件事情。算了,不吐槽了。

这时候想起了知乎上的一个段子,有个程序员为了保证写入成功,会用

for i := range (0,10):
    write_to_disk()

来确保写入成功。。把这个精神发扬光大一下,脚本每三十秒启动一次,那么我们就把最近十分钟的数据都写入一次:

select * from [业务表] where update_time >= date_sub(now(), interval 10 minute)

所以实际上一条数据最多可能被jdbc脚本写入到es 20次,考虑到写入的幂等性和我们部门的数据量级,这样做也暂时没什么问题。

上线经过了几天的验证,看起来问题不大了。

但老实说,这并不是一个靠谱的方案,因为这个es-jdbc的工具前前后后折腾了将近半个月才搞定。

那么为什么我要选用这样的一个方案呢?

我艹,还不是为了省开发时间啊。跟boss一说binlog同步方案需要一段时间开发boss就不乐意了。

为了节省五六天的开发时间,用了半个月去填坑。还不如一开始就用最靠谱的方案来做事情。但是在这样的部门,人微言轻,多说无益。