ElasticSearch大集群管理维护的难题与个推GProxy解决方案

 

 

作者| 个推平台研发总监 逍遥 &高级Java工程师 南洋

 

前言

 

用户搜索组件和日志管理平台是个推推送服务的重要组成部分。ElasticSearch(简称ES)作为一个开源的分布式搜索引擎,能较好地满足上述要求。个推在ES的使用上经过了多年迭代,积累了丰富的经验,特别是在数据量不断增大时,如何管理集群、维护集群稳定、优化集群性能,我们进行了许多实践。

 

本文将从三部分讲述个推ElasticSearch架构的演变过程:大集群的挑战、GProxy如何支持多集群以及当前运行情况。

 

个推ES服务概述

个推使用ES的业务场景主要是对用户信息增删改查和日志存储,是支持个推推送的核心服务之一,其特点是:

  • 数据更新快:需要实时更新用户画像等信息
  • 查询条件复杂:支持多种维度交并补,组合条件搜索
  • 查询数据量大:一个推送任务可能会需要查询千万级的数据

 

我们选择ES的原因主要有三:

  • 提供近实时索引,最短可以在1s内搜索到写入的文档
  • 能够支持复杂条件的查询,满足我们各种检索条件的需求
  • 分布式特性能够较好地满足水平扩容和高可用的要求

此外,ES官方和社区都非常活跃,周边的生态产品也多,遇到问题基本能够很好地得到解决。

 

大集群的挑战

个推集群演进

 

 

上图展示的是个推ES集群演进的过程。个推使用ES时间较早,起初从0.20.x版本开始用,当时集群规模较小,后来为了scroll查询结果不带source,进行了index-source的分离,升级到了0.90.x版本。接着官方推出了支持无source查询功能的1.2.0版本,我们又进行了集群合并。随后,为了使用官方的一些新特性,我们升级到了1.4.x和1.5.x版本。此时,集群规模已经很大了,升级一次版本比较不方便,而且后续的新特性对我们没有很强的吸引力,于是很长时间没升级,跳过了官方推出的2.x版本。但集群由于规模大,开始出现很多难以解决的问题,需要进行拆分升级,并且官方版本也更新到了5.x版本,不支持直接从1.x升级5.x,于是我们考虑用数据网关的方式解决升级、重启的困难,集群也演化到了上图的最后一版架构。

 

大集群的问题

大集群还带来很多问题,以下是我们在使用过程中遇到相对比较棘手的几个问题。

 

  • 第一个问题是JVM内存容易居高不下。

ES内存的占用主要有几个部分:Segment Memory、Filter Cache、Field Data cache 、Bulk Queue、Indexing Buffer和Cluster State Buffer 。Segment Memory会随着段文件增长而增长,大集群这块内存占用没法避免。Filter Cache和Field Data cache个推用到的场景不多,我们便通过参数配置予以禁用。Bulk Queue和Indexing Buffer都比较固定,内存不会不断增长,也没必要调整参数。Cluster State Buffer是当时比较棘手的一个问题,我们设置mapping的方式是在config目录下配置default_mapping.json文件,该文件会匹配所有写入的文档,进行格式解析处理,ES处理解析过之后会在内存中缓存一份ParserContext。而由于type数不断增加,这部分内存占用会越来越多,直到耗尽,如果不索引新type,则不会增长。在出现内存居高不下的问题时,我们分析了它的dump文件,如下图,发现是ParserContext占用导致的。当时没有很好的办法彻底解决,只能采用重启的方式清空ParserContext,暂时缓解一段时间。但随着文档的不断写入,这部分内存占用还是会重新变大。

 

 

  • 第二个问题是超大分片和超大段。

 

我们默认使用docid作为routing的依据,通过hash算法将文档散列到不同分片上。这种方式在文档数少的时候分片大小还比较均匀,但当文档数涨到了一定程度时,分片的大小差距会变大。在每个分片平均100g大小的时候,差距最大可达20g。此时,达到最大段阈值限制的段数量也非常多,这导致在段合并的时候比较耗时。

 

  • 第三个问题是磁盘IO高。

 

我们经常使用scroll查询文档,超大的段文件会导致文件磁盘查找效率降低。并且机器内存被ES节点的JVM占用了大半,致使文件系统很难使用内存缓存段文件页。这就导致scroll查询会直接读取磁盘文件,IO被打满。从监控看,集群的IO基本是时刻处于满的状态。

 

 

此外还有许多隐患。

 

首先是扩展瓶颈,集群预设的分片数原本是较充裕的,然而在经过多次扩容后,实例数已经和分片数相等了,扩容实例后,集群也不会将分片分配到新实例。其次是原先机器的磁盘空间逐渐不足,ES默认水位线是85%,到达后分片就会开始乱跳难以恢复。

 

然后是调整难,重启恢复非常慢,如果升级重建,索引也很慢。

 

最后集群的健壮性也会受到影响。文档数变大,压力也会变大,更容易出现故障,某个实例故障后压力会分摊给其他节点,业务会产生感知。

 

 

GProxy 的解决方案

 

对于上面描述的这些问题,我们希望找到一个解决方案能够提供如下功能:

  1. 能够平滑升级集群版本,并且升级期间不影响业务使用
  2. 通过拆分大集群为小集群,使业务分流,减轻集群压力
  3. 提供集群数据的多IDC热备,为异地多活提供数据层支持

 

但是之前的架构中,业务服务直接访问ES集群,耦合严重,要实现这些需求就变得比较困难。于是,我们选择了proxy-based架构,通过增加中间层proxy来隔离存储集群和业务服务集群,为更灵活的运维存储集群提供支持。

 

 

上图是GProxy的整体架构,它是一个三层架构:

  • 最上层是业务层,只需和 proxy 交互,通过etcd实现proxy的服务发现
  • 中间是GProxy层,提供请求转发和集群的管理
  • 最下方是多个ES集群

 

GProxy包含了多个组件:

  • etcd:高可用的元信息存储数据库,包括路由规则、集群信息、proxy服务地址、迁移任务等
  • sdk:扩展了es原生sdk,通过其sniffer机制,封装了对proxy服务发现和熔断等功能
  • proxy是一个轻量级的代理服务,扩容很方便,启动后可以将自己的地址注册到etcd中
  • dashboard 是整个集群的管理服务,并提供 web 界面,方便运维人员对集群进行管理和监控
  • migrate服务提供不同集群间的迁移功能

 

服务发现和路由规则

 

有了上面的总体架构后,还需要解决两个问题:

  1. 业务服务如何发现proxy,也即服务发现问题
  2. proxy将请求转发给哪一个集群,也即路由问题

 

服务发现

etcd是一个高可用的分布式键值数据库,且通过http api进行交互,操作简便,因此我们选择etcd来实现服务发现和元数据存储。

 

 

proxy是一个无状态的服务,启动初始化完成之后,将自己的地址注册到etcd中。通过etcd的lease机制,系统可以监控proxy的存活状态。当proxy服务出现异常而不能定时续期lease时,etcd会将其摘除,避免其影响正常的业务请求。

 

ElasticSearch提供的sdk预留了sniffer接口,sdk可以通过sniffer接口来获取后端地址。我们实现了sniffer接口,其定期从etcd获取proxy列表,并通过etcd的watch机制,监听服务的上下线,及时更新内部的连接列表。业务方还是可以按原来的方式使用原生的sdk,不需要过多的改动,只需要将sniffer注入到SDK就行。

 

路由规则

 

在个推推送业务场景中,每个app推送需要的数据都可以视为是一个整体,因此我们选择按照app的维度进行请求的路由,每个app的数据都存储在一个集群内。

 

路由信息保存在etcd中,格式是 appid->clusterName 这样一个对应关系。如果没有这样一个对应关系,proxy会将appid归属到一个默认集群。

 

proxy启动时会拉取最新的路由表,并通过etcd的watch机制,监控路由表的变更。

 

 

路由关系的变更通过迁移操作实现,以下是迁移流程的介绍。

 

迁移流程

 

每个app都属于一个集群。当集群的负载不均衡时,管理员可以按照app维度,通过迁移服务进行集群间的数据迁移。

 

 

迁移流程包含两个步骤:数据同步和路由规则的修改。数据同步需要同步两份数据:全量数据和增量数据。

  1. 全量数据通过ElasticSearch的scroll api导出
  2. 因为ElasticSearch没有提供增量数据的获取方式(类似mysql的binlog协议来实现增量数据的获取),因此我们通过proxy双写来实现增量数据的获取。

 

迁移服务负责数据的同步,并在数据同步完成后通知dashboard,dashboard更新etcd的路由关系。proxy通过watch机制,得到新的路由关系,更新内部的路由表,此时app新的请求就会路由到新的集群。

 

多IDC数据热备

 

在个推实际业务场景中,推送作为企业级服务,对服务的可用性要求很高,个推有多个机房对外提供服务,每个app归属到一个机房。为了应对机房级故障,我们需要对数据进行多IDC的热备,这样才能在机房发生故障后,将客户的请求路由到非故障机房,从而不影响客户的正常使用。

 

 

我们对数据的热备采用集群维度进行,每个集群的数据会备份到另一个机房。proxy收到请求后,根据集群的热备信息,实时将增量数据写入到MQ中,另一个机房的consumer服务不断消费MQ的增量数据,并写入至对应的集群中。dashboard服务负责管控所有IDC的热备任务的状态。

 

性能

 

引入一个中间层后,不可避免地会带来一定的性能损失。我们选择 GO 开发的原因,就是希望损失尽可能减小。最终性能结果如下:

 

 

由上图可知,QPS降低10%左右,平均延时约等于ES调用和proxy本身平均延时之和。虽然有了10%的性能下降,但是带来了更灵活的运维能力。

 

当前运行情况

 

GProxy服务上线之后,顺利完成了ES版本的升级(从1.5升级到6.4),并将原来大集群拆分为多个小集群。整个升级和拆分过程对于业务方无感,并且GProxy提供的无损回滚功能可以让操作更放心(数据的迁移需要十分谨慎)。

 

有了GProxy的支持,DBA日常对ES运维操作,如参数的优化、集群间的压力平衡,变得更加方便。

 

总结

 

个推通过使用Go语言,自主研发了Gproxy,成功解决ElasticSearch大集群存在的问题,为上层业务提供了稳定可靠的数据存储服务。此外,个推也将持续打磨自身技术,在搜索和数据存储领域不断探索,不断拓宽ElasticSearch的应用场景,与开发者一起分享关于如何保证数据存储高可用的最新实践。

  • 在线咨询
  • 技术咨询
  • 业务咨询
  • 电话咨询