一、背景
易企秀创意云矩阵下面运营了多个以内容发布为核心的互联网产品,每天都有大量企业与设计师在这些平台制作并传播他们的作品,每个产品又由不同部门及技术小组分管运营,底层的技术框架、存储以及数据结构差异性都比较大,产品要求整合各产品线数据做一个一站式搜索引擎,数据要求尽可能与业务线保持实时同步,并且允许用户对搜索结果按照实时阅读量进行排序。幸运的是,elasticsearch提供了丰富的功能帮我们实现了大部分的操作,大幅降低了系统的维护成本。
二、数据处理流程
业务线用到的数据库主要有3类,分别是MySQL、oracle、MongoDB,其中内容数据存储在MongoDB中,其它基础信息分别储存在MySQL与oracle中,如标题、作者、发布时间等信息。那么对应的数据处理流程包含:数据抽取>数据清洗>映射到es>提供query。
-
数据抽取
1、业务数据:由于需求中要求数据近实时同步,那么按照传统的方式在上亿数据表中近实时做全表迁移与增量迁移的方案将是一件多么可怕的事情,即便每小时同步一次,业务方也是不能接受的;针对以上问题我们对同步方式做了优化,采用解析MySQL binlog的方式进行数据同步,业务方新增、修改、删除都会产生对应的binlog日志(oracle同步类似),根据不同的CRUD操作解析为对应的es操作,并将数据实时更新到索引库;针对MongoDB中的内容数据同步,只需每小时聚合有过修改操作的作品id,使用spark脚本根据id批量拉取MongoDB中的作品内容同步到es(MongoDB中作品id要建索引)。
2、日志数据:作品的阅读量是基于日志计算的,前端通过sdk打点的,通过logserver(该项目会在后续实战篇进行讲解,高峰期每天可处理数十亿打点请求)采集日志发送到kafka。 -
数据清洗
1、业务数据:过滤标记为删除与发布时间为空的数据,因为这些数据已不允许在前端进行显示;这样的数据大约占全量数据的15%,滤除后可大幅提升后端存储与检索性能。
2、日志数据:根据日志中业务类型与事件类型过滤出作品PV打点数据,再对数据格式化处理后存入新的kafka topic,为后续的实时消费使用;如果不对日志清洗,会有大量不相关的数据流入(页面元素点击事件、广告曝光事件等),会使日志量翻好几倍,徒增后端计算的压力。 -
存储到ES
1、业务数据:由于es中的索引库是宽表结构,会存在根据作品id拉取业务库多张属性表到一个es索引库的情况,这种情况下当遇到有删除操作的binlog时需要特殊处理,不能因为其中一张表的数据删除而删除es中对应的文档数据。
2、日志数据:通过spark streaming实时消费kafka中的数据,每分钟形成一个batch,将计算好的作品pv数据通过es提供的spark connector存入对应的索引库,由于活跃作品比较多,每分钟对es的写操作大约是6-8w。 -
提供query
借助es提供的restful接口以及灵活的查询模板支持,使得接口层面的开发简单许多。
三、配置与优化
结合上述业务场景,如果不做任何优化,首次检索大约1s左右才能响应;由于es内部会对query和segment做缓存,当相似条件多次请求后,平均响应时间会降到500ms左右;经过对数据库、索引配置、查询语句三个层面简单配置后,首次检索降到了200ms以内,平均响应时间降了一个量级。
elasticsearch为满足多种不同的应用场景,底层提供多种数据结构支持并做了大量的默认配置优化,部分配置针对具体的用户使用场景可能是冗余的,甚至可能造成性能的下降,我们结合自身使用场景做了如下优化:
数据库
1、jvm内存,分配给es的内存不要超出系统内存的50%,预留一半给Lucene,因为Lucene会缓存segment数据提升检索性能;内存配置不要超过32g,如果你的服务器内存没有远远超过64g,那么不建议将es的jvm内存设置为32g,因为超过32g后每个jvm对象指针的长度会翻倍,导致内存与cpu的开销增大。
2、禁用swapping,开启服务器虚拟内存交换功能会对es产生致命的打击,如果某个segment数据被缓存到了磁盘上,会导致原来0.1毫秒的操作变成10毫秒,大大降低数据的检索性能,使得es在提供查询服务时出现木桶效应,拖跨整个集群的对外服务性能。
3、硬盘配置,建议使用SSD,如果你的服务器硬盘是一块200g的机械硬盘,那么建议分成两块100g的盘去使用,这样能大大提升es读写硬盘的能力,通过elasticsearch.yml中的path.data来实现。
4、其它配置:
bootstrap.memory_lock: true
#设置为true锁住内存,当服务混合部署了多个组件及服务时,应开启此操作,允许es占用足够多的内存。
indices.breaker.request.limit: 10%
#设置单个request请求的内存熔断限制,默认是jvm堆的60%(es7.0引入了新的内存熔断机制,会智能判断,规避OOM)。
index.merge.scheduler.max_thread_count: 1
#设置segment合并时占用的线程数,配置线程数越多对磁盘io消耗就越大(SSD忽略)。
indices.queries.cache.size:20%
#query请求可使用的jvm内存限制,默认是10%。
indices.requests.cache.size:2%
#查询request请求的DSL语句缓存,被缓存的DSL语句下次请求时不会被二次解析,可提升检索性能,默认值是1%。
indices.fielddata.cache.size:30%
#设置字段缓存的最大值,默认无限制。
索引配置
1、通过配置index_options优化text field的索引生成方式,es默认除生成倒排以外还会记录文档的词频与term的位置信息,如果只是用来对文档的检索不需要评分与高亮,可优化。
2、禁用field评分,如果该字段不需参与相关度打分那么可禁用,通过将norms设置为false来禁用评分提升检索性能。
3、Number类型的优化,如果该数字类型字段存储的是type信息 ,不会进行排序与范围查询操作,建议存储为keyword类型,keyword本身做了大量优化操作,对于检索与过滤性能更佳。
4、禁用doc_values,es为提升keyword类型下数据的排序与聚合性能,默认为每一个keyword field都开启了此功能,非必要场景禁用此操作可节省磁盘空间。
5、开启最优化压缩,通过配置"index.codec":"best_compression"开启最优压缩,可节省磁盘空间,但会增大cpu负载。
6、设置合适的分片数,一般不建议超过服务器的数量,过多的分片就意味着会有过多的segment产生,过多的segment消耗内存与系统IO。
7、设置合适的副本数,虽然多副本可以提升es的并发处理能力,但根据实际应用当中,这个值不建议配置太高,因为副本数据在进行同步时会带来过高的IO消耗。
8、降低刷新操作,过高的刷新会增大磁盘io负载,通过配置"index.refresh_interval":"-1"来控制刷新频率,es默认配置为1s,如果对数据时效性要求不高的话修改为30s-60s,-1表示禁用刷新。
9、通过forcemerge 强制合并索引段,定期清理过小的segment段,降低segment段的数量有助于减少内存消耗,过多的segment段也会增大底层os打开关闭文件的时间。
查询优化
1、尽量避免深度翻页场景,默认最大结果集深度为10000,可根据实际使用场景适当调大该值,如index.max_result_window:20000 ,该值设置过大,在高并发场景下会频繁触发es内部gc操作,影响整体性能。
2、使用sliced scroll翻页拉取大数据结果集,大家都知道使用scroll去做深度翻页读取大量数据,但这种操作不支持online,原因就是它的指针是全局的无法支撑高并发操作,这时需要用到sliced scroll。
3、尽可的使用filter操作,因为filter操作会使用缓存并且不会进行评分计算,性能高于query。
4、如果查询语句比较复杂且不需要翻页操作的话可增加timeout=1s&terminate_after=10000两个参数,在高并发场景下性能提升3-5倍。
四、小结
5个实例(8核16g)索引了上亿文档内容,优化后任意检索一条数据由原来的1s左右降低到200ms内 ,平均性能提升3-5倍。