之前负责搜索系统的时候花了些时间在搜索中间件上,沉淀了一下拿出来跟大家分享。
一、背景
相比之下大家对数据库中间件的项目背景会比较熟悉,其实搜索中间件的项目背景也类似,搜索系统总的来说可以分两种,一种是业务为主的搜索推荐系统,另一种是以提供基础搜索服务能力为主的泛化的数据检索系统。
搜索中间件的服务目标就是这种泛化的平台化数据检索系统。
二、总体目标
根据前期的搜索平台运营经验,中间件的核心目标主要有三个:
三、需要实现的功能
制定了大目标之后,剩下就是任务拆解,具体来说,搜索中间件需要实现的功能如下:
1. 统一接入
搜索发展初期存在各种域名,对应不同的集群,也对应不同的业务:
在统一接入之后,将会只有:
所有的业务线,对搜索的访问,只有一个入口,由搜索中间件进行鉴权和路由。
业务到索引的访问,统一到一个入口,通过中间件进行路由控制,这样对后端集群进行扩容、迁移、拆分等操作对业务来说透明,免去了很大的运维成本,也让我们在控制流量上有了更大的空间。
这里也可以根据实际需求分步实现,比如先统一单业务的访问域名,再统一全部业务的访问域名。
2. 保持访问
因为历史原因,访问搜索集群的方式多种多样,有通过原生DSL访问的,也有通过内部封装的业务协议请求的,甚至还有不同版本的协议样式。
保持访问,就是指让上游业务对搜索服务的访问接口尽量不用改变,中间件对上游业务访问的引擎进行无缝替。
3. 屏蔽多索引
根据业务需要或者性能上的考虑,后端集群会将流量导入到多个索引中,比如冷热隔离、或者类目/关键字检索隔离,中间件需要对业务屏蔽此类细节,由自己来对不同请求进行路由。
4. 支持索引拆分
我们支持容量伸缩的方式基本是通过索引拆分实现的(比通过索引内部的 _routing 容易控制,比如重建和迁移),db 的分库方式是在不同的实例中存在同样的表,db => db1.table / db2.table,索引拆分目前是分表的方式 es.index => es.index1 / es.index2。
这样对业务层来说访问的始终是同一个索引,而不必感知实际细节,方便许多。
按照现有的业务使用方式,我们需要支持的拆分规则只需要下述四项即可:
5. 熔断限流
为了保护搜索集群,在遇到流量抖动或者流量洪峰情况下,中间件需要对其进行必要的熔断或者限流处理,将问题的影响面控制到最低。
熔断和限流是不同的两种保护机制,熔断:保护集群高负载情况下在中间层将流量导流或者拒绝,可行的比如通过监测后端请求失败率,在失败率高时触发熔断。
限流是限制不同请求能使用的资源,避免流量之间的相互影响,具体的又可以分为两个方面:
6. 高可用
高可用需要考虑中间件自身和搜索集群两部分。
中间件部分:
搜索集群部分:
7. 可运维
这里目标是能够让开发和运维感知到中间件的运行数据,能够通过管理命令在运行时进行变更,当然能够提供界面化的操作最佳。
四、模块设计
1. 总体架构
为了透明切换业务访问到中间件,必须在中间件支持原生的server协议,并转换query到目标dsl发送到集群,之所以不是透传,主要是考虑到之后引擎升级可能产生与现有query不兼容的语法,总体架构如下:
es对外提供transport和rest两种协议的服务接口,transport协议私有,并没有对外公开,如果要兼容这个协议,只能通过查找源代码中的序列化/反序列化模块,考虑到大部分场景下对协议的敏感度并没有我们想象的那么大,这里建议是舍弃transport访问协议,只支持rest部分,在server部分的处理上会简化不少。
2. 架构细节和组件划分
架构细节和内部的组件划分大致如下:
总的分为业务层、中间层、数据层三部分,其中中间层又可以分为管理子系统和业务子系统两部分。
上游业务可以使用rest风格的http协议、原生es http client或者通过我们封装的search-client调用搜索中间件,当然也可以支持监控任务或者开发、运维通过管理接口管理集群配置或者采集统计数据。
Es集群,作为承载数据查询和存储的介质,位于整个系统的最下游。
配置管理初期可以通过本地的配置文件,但是当中间件集群扩大时,仍然使用本地文件的方式带来的维护成本太大,通常会考虑通过配置中心的方式,可以自行实现,也可以集成现有的如Apollo等开源配置管理中间件。
配置形式上不再继续展开,可以自行设计合适的组织形式。
这部分负责对客户端的交互,back queue 作为请求队列除了扩大系统吞吐量,也是削峰限流的一部分,protocol analyzer用于解析客户端的http request为结构化的查询请求,这里还有几个问题需要处理:
负责与 es 集群交互的部分,初期可以使用原生的low level http client,如果有多个版本 es 集群存在,可以考虑直接通过http client封装,可以更加方便的控制DSL拼接,也适合对 client 连接池的管理和监控。
解析查询,分解为原子条件,比如 term / terms / range / match 等,用来进行查询校验,可以分为非拆分索引和拆分索引两种:
优化查询,是查询过程中比较重要的一环,可以分为两类:
查询优化器最后还需要将条件组合为具体的DSL。
这部分负责路由转发,非拆分索引和只有单路由规则的拆分索引可以根据索引名选择对应的客户端连接发送请求即可。
比较复杂的是组合路由规则的拆分索引,为了尽可能提高查询执行效率,假设查询命中多个子索引,需要考虑将查询并行化,并通过 result merger 模块合并结果返回,这里的处理规则比较复杂,后续单独开文展开讨论。
为了提高集群性能考虑,这里会加入一个短时间的result cache,用于补齐引擎不提供的功能。