从平台到中台 | Elasticsearch在蚂蚁金服的实践经验

善仁 2019-05-10 11:31:26
作者介绍

善仁,蚂蚁金服通用搜索产品负责人,通用搜索目前拥有上万亿文档,服务了上百个业务方,是蚂蚁内部最大的搜索产品。目前专注于抽象各种复杂场景下的搜索解决方案。

 

今天的主题是《Elasticsearch在蚂蚁金服的中台实践经验》,主要会从四个方面来分享我们的经验:

 

  • 源动力

  • ES平台

  • 回看业务

  • 搜索中台

 

基于 Elasticsearch 的通用搜索是蚂蚁内部最大的搜索产品,目前拥有上万亿文档,服务了上百个业务方。而通用搜索的发展主要分为两个阶段:平台化和中台化。

 

我们在这两个阶段的发展中,为业务解决了哪些痛点?又是如何去解决这些痛点的?

 

一、源动力:架构复杂、运维艰难

 

和大多数大型企业一样,蚂蚁内部也有一套自研的搜索系统,我们称之为主搜。但是由于这种系统可定制性高,所以一般业务接入比较复杂,周期比较长。

 

而对于大量新兴的中小业务而言,迭代速度尤为关键,因此难以用主搜去满足。

 

主搜不能满足,业务又实际要用,怎么办呢?那就只能自建了。在前几年蚂蚁内部有很多小的搜索系统,有 ES,也有 solr,甚至还有自己用 Lucene 的。

 

1、业务痛点
 

 

 

然而业务由于自身迭代速度很快,去运维这些搜索系统成本很大。就像 ES,虽然搭建一套很是简单,但是用在真实生产环境中还是需要很多专业知识的。作为业务部门很难去投入人力去运维维护。

 

并且由于蚂蚁自身的业务特性,很多业务都是需要高可用保证的。而我们都知道 ES 本身的高可用目前只能跨机房部署了,先不谈跨机房部署时的分配策略,光是就近访问一点,业务都很难去完成。

 

因为这些原因,导致这类场景基本都没有高可用,业务层宁愿写两套代码,准备一套兜底方案。觉得容灾时直接降级也比高可用简单。

 

2、架构痛点
 

 

 

从整体架构层面看,各个业务自行搭建搜索引擎造成了烟囱林立,各种重复建设。

 

并且这种中小业务一般数据量都比较小,往往一个业务一套三节点集群只有几万条数据,造成整体资源利用率很低,而且由于搜索引擎选用的版本,部署的方式都不一致,也难以保证质量,在架构层面只能当做不存在搜索能力。

 

二、低成本,高可用,少运维的Elasticsearch平台

 

基于以上痛点,我们产生了构建一套标准搜索平台的想法,将业务从运维中解放出来,也从架构层面统一基础设施,提供一种简单可信的搜索服务。『低成本,高可用,少运维』的 Elasticsearch 平台应运而生。

 

1、架构
 

 

如何做低成本,高可用,少运维呢?我们先来一起看一下整体架构,如图:

 

 

首先说明一下,图中中部的两个黑色框框代表两个机房,我们整体就是一种多机房的架构,用来保证高可用:

 

  • 最上层是用户接入层,有 API、Kibana、Console 三种方式,用户和使用 ES 原生的 API 一样可以直接使用我们的产品;

  • 中间为路由层(Router),负责将用户请求真实发送到对应集群中,负责一些干预处理逻辑;

  • 下面每个机房中都有队列(Queue),负责削峰填谷和容灾多写。

  • 每个机房中有多个 ES 集群,用户的数据最终落在一个真实的集群中,或者一组对等的高可用集群中;

  • 右边红色的是 Meta,负责所有组件的一站式自动化运维和元数据管理;

  • 最下面是 Kubernetes, 我们所有的组件均是以容器跑在 k8s 上的,这解放了我们很多物理机运维操作,使得滚动重启这些变得非常方便;

 

2、低成本:多租户
 

 

看完了整体,下面就逐点介绍下我们是怎么做的。第一个目标是低成本。

 

在架构层面,成本优化是个每年必谈的话题。那么降低成本是什么意思?实际上就是提高资源利用率。提高资源利用率方法有很多,比如提高压缩比,降低查询开销。但是在平台上做简单有效的方式则是多租户。

 

今天我就主要介绍下我们的多租户方案:

 

多租户的关键就是租户隔离。租户隔离分为逻辑隔离和物理隔离:

 

1)逻辑隔离

 

也就是让业务还是和之前自己搭ES一样的用法,也就是透明访问,但是实际上访问的只是真实集群中属于自己的一部分数据,而看不到其他人的数据,也就是保证水平权限。

 

而 ES 有一点很适合用来做逻辑隔离,ES 的访问实际上都是按照 index 的。因此我们逻辑隔离的问题就转化为如何让用户只能看到自己的表了。

 

我们是通过 console 保存用户和表的映射关系,然后在访问时通过 router,也就是前面介绍的路由层进行干预,使得用户只能访问自己的 index。

 

具体而言,我们路由层采用 OpenResty+Lua 实现,将请求过程分为了下图右侧的四步:

 

 

Dispatch

 

在 Dispatch 阶段我们将请求结构化,抽出其用户、app、index、action 数据。

 

Filter

 

然后进入 Filter 阶段,进行写过滤和改写,filter 又分为三步:

 

  • Access 进行限流和验权这类的准入性拦截;

  • Action 对具体的操作进行拦截处理,比如说 DDL ,也就是建表、删表、修改结构这些操作,我们将其转发到 Console 进行处理,一方面方便记录其 index 和 app 的对应信息,另一方面由于建删表这种还是很影响集群性能的,我们通过转发给 console 可以对用户进行进一步限制,防止恶意行为对系统的影响。

  • Params 则是请求改写,在这一步我们会根据具体的 index 和 action 进行相应的改写。比如去掉用户没有权限的 index,比如对于 kibana 索引将其改为用户自己的唯一 kibana 索引以实现 kibana 的多租户,比如对ES不同版本的简单兼容。

    在这一步我们可以做很多,不过需要注意的有两点:

    一是尽量不要解析 body, 解 body是一种非常影响性能的行为,除了特殊的改写外应该尽力避免,比如 index 就应该让用户写在 url 上,并利用ES本身的参数关闭 body 中指定 index 的功能,这样改写速度可以快很多。

    二是对于_all 和 getMapping 这种对所有 index 进行访问的,如果我们替换为用户所有的索引会造成 url 过长,我们采用的是创建一个和应用名同名的别名,然后将其改写成这个别名。

 

Router

 

进行完 Filter 就到了真实的 router 层,这一层就是根据 filter 的结果做真实的路由请求,可能是转发到真实集群也能是转发到我们其他的微服务中。

 

Reprocess

 

最后是 Reprocess,这是拿到业务响应后的最终处理,我们在这边会对一些结果进行改写,并且异步记录日志。

 

上面这四步就是我们路由层的大致逻辑,通过 app 和 index 的权限关系控制水平权限,通过 index 改写路由进行共享集群。

 

2)物理隔离

 

做完了逻辑隔离,我们可以保证业务的水平权限了,那么是否就可以了呢?显然不是的,实际中不同业务访问差异还是很大的,只做逻辑隔离往往会造成业务间相互影响。这时候就需要物理隔离。

 

不过物理隔离我们目前也没有找到非常好的方案,这边给大家分享下我们的一些尝试:

 

首先我们采用的方法是服务分层,也就是将不同用途,不同重要性的业务分开,对于关键性的主链路业务甚至可以独占集群。

 

对于其他的,我们主要分为两类,写多查少的日志型和查多写少的检索型业务,按照其不同的要求和流量预估将其分配在我们预设的集群中。不过需要注意的是申报的和实际的总会有差异的,所以我们还有定期巡检机制,会将已上线业务按照其真实流量进行集群迁移。

 

做完了服务分层,我们基本可以解决了低重要性业务影响高重要性业务的场景,但是在同级业务中依旧会有些业务因为比如说做营销活动这种造成突发流量。对于这种问题怎么办?

 

一般而言就是全局限流,但是由于我们的访问都是长连接,所以限流并不好做。

 

如下图右侧所示,用户通过一个 LVS 访问了我们多个 Router,然后我们又通过了 LVS 访问了多个 ES 节点,我们要做限流,也就是要保证所有 Router 上的令牌总数:

 

 

一般而言全局限流有两种方案:

 

  • 第一种方案是以限流维度将所有请求打在同一实例上,也就是将同一表的所有访问打在一台机器上,但是在 ES 访问量这么高的场景下,这种并不合适,并且由于我们前面已经有了一层lvs 做负载均衡,再做一层路由会显得过于复杂。

  • 第二种方案就是均分令牌,但是由于长连接的问题,会造成有些节点早已被限流,但是其他节点却没有什么流量。

 

那么怎么办呢?

 

 

既然是令牌使用不均衡,那么我们就让其分配也不均衡就好了呗。所以我们采用了一种基于反馈的全局限流方案。什么叫基于反馈呢?就是我们用巡检去定时采集用量,用的多就多给一些,用的少就少给一点。

 

那么多给一些少给一点到底是什么样的标准呢?

 

这时我们就需要决策单元来处理了,目前我们采取的方案是简单的按比例分配。

 

这边需要注意的一点是当有新机器接入时,不是一开始就达到终态的,而是渐进的过程。所以需要对这个收敛期设置一些策略,目前因为我们机器性能比较好,不怕突发毛刺,所以我们设置的是全部放行,到稳定后再进行限流。

 

这里说到长连接就顺便提一个 nginx 的小参数,keepalive_timeout。用过 nginx 的同学应该都见过,表示长连接超时时间,默认有75s。

 

但是这个参数实际上还有一个可选配置,表示写在响应头里的超时时间,如果这个参数没写的话,就会出现“在服务端释放的瞬间,客户端正好复用了这个连接,造成 Connection Reset 或者 NoHttpResponse 的问题。

 

出现频率不高,但是真实影响用户体验,因为随机低频出现,我们之前一直以为是客户端问题,后来才发现是原来是这个释放顺序的问题。

 

至此服务分层,全局限流都已经完成了,是不是可以睡个好觉了呢?

 

很遗憾,还是不行。

 

因为ES语法非常灵活,并且有许多大代价的操作,比如上千亿条数据做聚合,或者是用通配符做个中缀查询,写一个复杂的script都有可能造成拖垮我们整个集群,那么对于这种情况怎么办呢?

 

我们目前也是处于探索阶段,目前看比较有用的一种方式是事后补救,也就是我们通过巡检去发现一些耗时大的 Task,然后对其应用的后继操作进行惩罚,比如降级,甚至熔断。这样就可以避免持续性的影响整个集群。

 

但是一瞬间的rt上升还是不可避免的,因此我们也在尝试事前拦截,不过这个比较复杂,感兴趣的同学可以一起线下交流一下。

 

3、高可用:对等多集群
 

 

 

讲完了低成本,那么就来到了我们第二个目标,高可用。

 

正如我之前提到那样,ES 本身其实提供了跨机房部署的方案,通过打标就可以进行跨机房部署,然后通过preference可以保证业务就近查询。我这里就不再详细说了。

 

但是这种方案需要两地三中心,而我们很多对外输出的场景出于成本考虑,并没有三中心,只有两地两中心,因此双机房如何保证高可用就是我们遇到的一个挑战。

 

我们提供了两种类型、共三种方案分别适用于不同的业务场景。

 

类型:

 

1)单写多读

 

单写多读我们采用的是跨集群复制的方案,通过修改 ES,我们增加了利用 translog 将主集群数据推送给备的能力。就和 6.5 的 ccr 类似,但是我们采用的是推模式,而不是拉模式,因为我们之前做过测试,对于海量数据写入,推比拉的性能好了不少。

 

容灾时进行主备互换,然后恢复后再补上在途数据。由上层来保证单写,多读和容灾切换逻辑。

 

这种方案通过 ES 本身的t ranslog 同步,部署结构简单,数据也很准确,类似与数据库的备库,比较适合对写入 rt 没有过高要求的高可用场景。

 

 

2)多写多读

 

我们提供了两种方案:

 

第一种方案比较取巧,就是因为很多关键链路的业务场景都是从DB同步到搜索中的,因此我们打通了数据通道,可以自动化的从DB写入到搜索,用户无需关心。

 

那么对于这类用户的高可用,我们采用的就是利用DB的高可用,搭建两条数据管道,分别写入不同的集群。这样就可以实现高可用了,并且还可以绝对保证最终一致性。

 

第二种方案则是在对写入rt有强要求,有没有数据源的情况下,我们会采用中间层的多写来实现高可用。我们利用消息队列作为中间层,来实现双写。

 

就是用户写的时候,写成功后保证队列也写成功了才返回成功,如果一个不成功就整体失败。然后由队列去保证推送到另一个对等集群中。用外部版本号去保证一致性。

 

但是由于中间层,对于Delete by Query的一致性保证就有些无能为力了。所以也仅适合特定的业务场景。

 

最后,在高可用上我还想说的一点是对于平台产品而言,技术方案有哪些,怎么实现的业务其实并不关心,业务关心的仅仅是他们能不能就近访问降低rt,和容灾时自动切换保证可用。

 

因此我们在平台上屏蔽了这些复杂的高可用类型和这些适用的场景,完全交由我们的后端去判断,让用户可以轻松自助接入。并且在交互上也将读写控制,容灾操作移到了我们自己系统内,对用户无感知。

 

只有用户可以这样透明拥有高可用能力了,我们的平台才真正成为了高可用的搜索平台。

 

4、少运维
 

 

最后一个目标,少运维。简单介绍一下我们在整体运维系统搭建过程中沉淀出的四个原则:自包含、组件化、一站到底、自动化。

 

  • 自包含ES做的就很不错了,一个jar就可以启动,而我们的整套系统也都应该和单个ES一样,一条很简单的命令就能启动,没有什么外部依赖,这样就很好去输出。

  • 组件化是指我们每个模块都应该可以插拔,来适应不同的业务场景,比如有的不需要多租户,有的不需要削峰填谷。

  • 一站到底是指我们的所有组件,router、queue、es,还有很多微服务的管控都应该在一个系统中去管控,万万不能一个组件一套自己的管控。

  • 自动化就不说了,大家都懂。

  • 下图右侧就是我们的一个大盘页面,展现了router,es和queue的访问情况。当然,这是mock的数据:

 

 

三、回看业务:无需运维,却依旧不爽

 

至此我们已经拥有了一套『低成本,高可用,少运维』的 Elasticsearch 平台了,也解决了之前谈到的业务痛点,那么用户用的是否就爽了呢?

 

我们花了大半个月的时间,对我们的业务进行了走访调研,发现业务虽然已经从运维中解放了出来,但是身上还是有不少搜索的枷锁。

 

 

我们主要分为两类用户:

 

数据分析用户主要觉得配置太复杂,他只是想导入一个日志数据,要学一堆的字段配置,而且很久才会用到一次,每次学完就忘,用到再重学,很耽误事情。

 

其次,无关逻辑重,因为数据分析类的一般都是保留多天的数据,过期的数据就可以删除了,为了实现这一个功能,数据分析的同学要写很多代码,还要控制不同的别名,很是麻烦。

 

而全文检索类的用户主要痛点有三个:

 

  • 一是分词配置复杂;

  • 二是难以修改字段,reindex 太复杂,还要自己先创建别名,再控制无缝切换;

  • 第三点是Debug艰难,虽然现在有 explain,但是用过的同学应该都懂,想要整体梳理出具体的算分原因还是需要自己在脑中开辟很大的一块缓存的。对于不熟悉 ES 的同学就太痛苦了。

 

整理一下,这些痛点归类起来就两个痛点,学习成本高和接口过于原子。

 

四、搜索中台:抽象逻辑,解放业务

 

 

学习成本高和接口过于原子,虽然是业务的痛点,但是对ES本身而言却反而是优点,为什么学习成本高呢?因为功能丰富。而为什么接口原子呢?为了让上层可以灵活使用。

 

这些对于专家用户而言,非常不错,但是对于业务而言,的确很是麻烦。

 

因此我们开始了我们第二个阶段,搜索中台。什么叫中台呢,就是把一些通用的业务逻辑下移,来减少业务的逻辑,让业务专注于业务本身。

 

而为什么业务不能做这些呢?当然也能做。但是俗话说『天下武功,唯快不破』,前台越轻,越能适应这变化极快的业务诉求。

 

因此我们的搜索中台的主要目标就是两点:

 

  • 降低业务学习成本,加快上手速度。我们这次介绍的主要是如何降低对于配置类这种低频操作的学习成本;

  • 抽象复杂逻辑来加速业务迭代,我们这次主要会介绍抽象了哪两种业务逻辑。

 

1、降低学习成本
 

 

降低学习成本,这个怎么做呢?众所周知,黑屏变白屏,也就是白屏化。但很多的白屏化就是把命令放在了 web 上,回车变按钮。这样真的可以降低用户学习成本么?

 

我想毋庸置疑,这样是不行的。

 

我们在可视化上尝试了许多方案,也走了许多弯路,最后发现要想真正降低用户学习成本,需要把握三个要点:

 

 

1)用户分层

 

区分出小白用户和专家用户,不要让专家用户的意见影响整体产品的极简设计,对于小白用户一定是越少越好。选择越少,路径越短,反馈越及时,效果越好。

 

正如所谓的沉默的大多数,很多小白用户并不会去主动发声,只会随着复杂的配置而放弃使用。

 

下图就是我们对于专家用户和小白用户在配置表结构时不同的页面:

 

 

对于专家用户,基本就是 ES 所有的功能可视化,加快使用速度。对于小白用户而言,则是完全屏蔽这些功能点,让其可以直接使用。

 

2)引导式配置

 

引导式配置其实也就是加上限制,通过对用户的上一步输入决定下一步的可选。

 

要避免一个页面打开一堆配置项,这样用户就会无从下手,更不要谈学习成本了。通过引导式配置来减少用户的选择,降低用户的记忆成本。限制不一定就意味着约束用户,合适的限制更可以降低用户的理解成本。

 

比如下图就是我们的一个分词器配置,很简单的引导,用户选择了中文字典后才可以选择相应的词典:

 

 

 

3)深层次结构打平

 

什么叫深层次结构打平?就是指像现在的分词器,相似度这些都是在 index 级别下的,我们将其抽象出来,变为全局的。用户可以自行创建全局的分词器、相似度,并且还可以共享给其他人,就像一个资源一样。然后在 index 中则是引用这个分词器。

 

虽然这边做的仅仅是将分词器从 index 级别变为了全局,但是缺真正的减少了很多业务操作,因为在一个业务场景中,往往存在多张表,而多张表往往会使用同一套分词器。通过这种全局性的分词器用户仅需修改一处即可作用于所有位置。

 

2、抽象复杂逻辑
 

 

 

好的,说完了白屏化的一些经验,这边给大家分享我们对于复杂逻辑的抽象封装的两种新型表结构

 

这两种分别是数据分析类场景,我们抽象出了日志型表,另一种是全文检索类场景,我们抽象出了别名型表。

 

日志型表的作用顾名思义就是存日志,也就是之前说的对于数据分析类业务,往往只保留几天。

 

比如我们现在有个业务场景,有张es的日志表,只想保留3天,于是我们就给他按天创建索引,然后写入索引挂载到今天,查询索引挂载所有的,用 router 去自动改写别名,用户还是传入 es,但是执行写入操作时实际就是在 es_write 执行,查询就是在 es_read 执行。

 

当然实际中我们并不是按天建的索引,我们会利用 Rollover 创建很多的索引来保证海量写入下的速度。但是整体逻辑还是和这个是一样的。

 

 

而对于全文检索类场景,主要的痛点就是表结构的变更和分词器,字典类的变更,需要重建索引。

 

所以我们则抽象了一个叫别名表的表结构,用户创建一张表es,实际创建的是一个 es 的别名,我们会把他和我们真实的 index 一一对应上。这样利用这个别名,我们就可以自动帮用户完成索引重建的操作。

 

而索引重建,我们有两种方式:

 

一是用户配置了数据源的,我们会直接从数据源进行重建,重建完成后直接切换。

 

另外对于没有数据源,直接 api 写入的,目前我们是利用了 ES 的 reindex 再配合我们消息队列的消息回放实现的。

 

具体而言,我们就是首先提交 reindex,同时数据开始进 queue 转发,然后待 reindex 完成后,queue 再从 reindex 开始时进行回放,追平时切别名即可。

 

五、总结

 

总结一下这次分享的内容,我们首先构建了一个『低成本,高可用,少运维』的 ES 平台将业务从运维中解脱出来,然后又进一步构建了搜索中台,通过降低业务学习成本,下沉通用业务逻辑来加速业务迭代,赋能业务。

 

作者:善仁

来源:金融级分布式架构订阅号(ID:Antfin_SOFA)

dbaplus社群欢迎广大技术人员投稿,投稿邮箱:editor@dbaplus.cn

最新评论
访客 2019年09月18日

请问下为什么不用logstash同步呢?

访客 2019年09月16日

写的太好了,我这就开始学

访客 2019年09月16日

Discretized Streams 就要过时了??

访客 2019年09月07日

写的就跟屎一样

访客 2019年09月01日

没看懂啊,PK的时候,没懂,怎就就从 (1,1)变成了…

活动预告