郑昀 基于胡耀华和王超的设计文档 最后更新于2014/12/3
关键词:
ElasticSearch
、Lucene、solr、搜索、facet、高可用、可伸缩、mongodb、SearchHub、商品中心
提纲:
- 曾经的基于MongoDB的筛选+排序解决方案
- MongoDB方案的缺陷
- 看中了搜索引擎的facet特性
- 看中了ES的简洁
- 看中了ES的天生分布式设计
- 窝窝的ES方案
- ES的几回事故和教训
- ES自身存在的问题
首先要感谢王超和胡耀华两位研发经理以严谨治学的研究精神和孜孜以求的工做态度给咱们提供了高可用、可伸缩、高性能的ES方案。
一,曾经的基于 MongoDB 的筛选+排序解决方案
电商的商品展现无非“List(列表页)-Detail(详情页)”模式。生活服务电商更特殊一点,不一样开站城市下的用户看到的团购/旅游/酒店/抽奖/电影订座/外卖…等商品集合以及排序也不同。
起初窝窝的 List 需求比较简单,因此用 memcached+mysql 也就解决了,但随着在 List 页作多级筛选,根据排序公式计算商品得分来作自动排序等需求的提出,咱们把视线转向了 MongoDB。
2012年,咱们针对窝窝当时的 MongoDB 实现方案进一步提出,商品中心的改造思路为“
持久化缓存模式,尽可能减小接口调用”:
- 商品中心小组对外提供的其实是一个存储介质
- 把本需作复杂关联查询的商品数据(base属性集合、ext属性集合、BLOB集合)组装成一个 Document 放入 MongoDB 等持久化存储介质中
- 容许不一样商品具备不一样属性的可扩展性
- 商品中心要作的是维护好这个存储介质,保证:
- 商品数据的准确性:
- 如商品天然下线,从介质中清除;
- 如商品紧急下线,默认保留一段时间如6小时;
- 如商品base/ext/blob属性发生变动,有不一样的时间策略来更新,如base属性改变,则须要第一时间更新;
- 商品可按常见规则快速抽取:
- 如view层按频道+城市抽取商品,
- 如view层按城市+区县+前台分类抽取商品,
- view层可由各个系统自行开发
|
这样,MongoDB 里不只仅存储了一份份 documents,还存储了不一样开站城市、不一样频道、不一样排列组合下商品列表的 Goods IDs 清单。排序基本靠 MongoDB 排。ids 清单定时更新。
这以后,商品中心分拆为:泰山和 GoodsCenter 两部分。
二,MongoDB 方案的缺陷
随着网站业务的不断发展,网站商品搜索筛选的粒度愈来愈细,维度也就愈来愈多,多维度的 count 和 select 查询,业务上各类排序需求,使 MongoDB 集群压力山大,以致于屡屡拖累商品中心和泰山的性能。
2012年下半年,咱们意识到:
因为频道页流量小于首页,尤为是用户不多点击到的深度筛选条件组合查询,因此下图中的全部枚举项商品数量都容易缓存失效或缓存挤出:
图2 筛选愈来愈复杂,标题数字却要保持准确性
一旦缓存失效后,但凡我从上图的“20元如下”点击切换到“51-80元”或作更深层次筛选,那么程序就要针对上面全部组合条件对 MongoDB 商品记录逐一作 count 计算。
虽然每个 count 计算都很快不属于慢查询,但也架不住多啊,尤为是配上区县和商圈等动辄六、7层深的筛选组合,点击一次轻易就涉及成百次的 count 计算,代价仍是很大的。
因为在商城模式下,不一样频道极可能不断增长新筛选条件,致使筛选组合愈来愈复杂,最终可能要求咱们从基于 NoSQL 的排序和筛选方案,尽快转变为基于搜索引擎的排序和筛选方案。
|
2012年时,不一样筛选维度的组合筛选形成
MongoDB 的索引命中率不高,MongoDB一旦没有命中索引,其查询效率会直线降低,从而形成整个MongoDB的压力增大响应变慢(MongoDB 的索引策略基本和 MySQL 的差很少)。有段时间,咱们不止一次遇到因为 MongoDB 的慢查,拖挂全部前台工程的状况,焦头烂额。
商品中心须要升级。
技术选型主要集中在 solr 和 ES 这两个均构建于 Lucene 之上的搜索引擎。
这时,咱们也注意到了外界对新生事物 Elastic Search 的各类溢美之辞,系统运维部此前也用 Logstash+ElasticSearch+Kibana 方案替代了 Splunk,也算是对 ES 的搭建有了必定了解。
三,看中了搜索引擎的 facet 特性
借用腾讯一篇博文来说解 facet search:
介绍分面
分面是指事物的多维度属性。例如一本书包含主题、做者、年代等分面。而分面搜索是指经过事物的这些属性不断筛选、过滤搜索结果的方法。能够将分面搜索当作搜索和浏览的结合。
灵活使用分面 分面不但能够用来筛选结果,也能够用来对结果排序。电商网站中经常使用风格、品牌等分面筛选搜索结果,而价格、信誉、上架时间等分面则用来排序。 html
有时用户并不明确本身的目的,所以提供宽松的筛选方式更符合这部分用户的预期。Bing 的旅行搜索中选择航班时,用户能够经过滑块来选择某个时间段起飞的航班。 前端
|
facet 的字段必须被索引,无需分词,无需存储。无需分词是由于该字段的值表明了一个总体概念,无需存储是由于通常而言用户所关心的并非该字段的具体值,而是做为对查询结果进行分组的一种手段,用户通常会沿着这个分组进一步深刻搜索。
facet 特性对咱们最大优势是,查询结果里自带 count 信息,无需咱们单独计算不一样排列组合的 count 信息,一举扫清性能瓶颈。
solr 里 facet search 分为三种类型:
- Field Facet:若是须要对多个字段进行Facet查询,那么将 facet.field 参数声明屡次,Facet字段必须被索引;
- Date Facet:时间字段的取值有无限性,用户每每关心的不是某个时间点而是某个时间段内的查询统计结果,譬如按月份查;
- Facet Query:利用相似于filter query的语法提供了更为灵活的Facet,譬如根据价格字段查询时,可设定不一样价格区间;
四,看中了 ES 的简洁
2012年下半年很多人倾倒于 ES 的简洁之美:
图3
图4
ES 的优势:
- 简单
- RESTful
- json 格式 Response
- 天生分布式
- Querydsl 风格查询
五,看中了 ES 的天生分布式
ES 毕竟是后来者,因此能够说为分布式而生。它的处理能力上,支持横向扩展,理论上无限制;存储能力上,取决于磁盘空间(根据提取字段的数量,索引后的数据量 是原始数据量的几倍,譬如咱们的 Logstash+ES 方案中对 nginx 访问日志提取了17个字段(都创建了索引),存储数据量8倍于原始日志)。
好比在高峰期,咱们能够采用调配临时节点的方式,来分解压力,在不须要的时候咱们能够停掉多余的节点来节省资源。
还有ES的高可用性,在集群节点出现一个节点或者多个节点出现故障时,主要数据完整,依然能够正常提供服务。
这里有一个数据,大概是2013年时,有一个
访谈说起 Github 是如何使用 ES:1, 用了 40~50 个索引,包括库、用户、问题、pull请求、代码、用户安全日志、系统异常日志等等;2,44台 EC2 主机处理 30T 的数据,每台机器配备 2T SSD 存储;3,8台机器仅仅用于搜索,不保存数据。固然,Github 也曾经在 ES 升级上栽过大跟头,那是2013年1月17日的事儿了,参考《2013,GitHub使用elasticsearch遇到的一些问题及解决方法,
中文译稿,
英文原文》。
六,窝窝的 ES 方案
6.1>架出一层 SearchHub
全部数据查询均经过 SearchHub 工程完成,以下图所示:
图5 SearchHub
6.2>经过 NotifyServer 来异步更新各个系统
6.3>索引设计方案
6.3.1>商品索引设计
商品维度是咱们主要的查询维度,其业务复杂度也比较高。针对网站查询特性,咱们的商品主索引方案为:每一个城市创建一个 index,因此一共有400多个 index,每一个城市仅有1个主 shard(不分片)。这样作的好处是之后咱们根据热点城市和非热点城市,能够将各个 index 手工分配到不一样的 node 上,能够作不少优化。 node
其结构为: mysql
图7 goodsinfo nginx
为了减小索引量和功能拆分,减小商品索引的内存占用,因此咱们把全文检索单独建为一个索引。 git
每一个城市索引或者商品索引按频道分为几个type,以下图所示。 github
图9 type sql
商品频道映射到es的type是很容易理解的,由于每一个频道的模型不一样:有的频道特有“用餐人数”属性,有的频道特有“出发城市”和“目的地城市”属性 因此每一个频道对应一个es的type,每一个type绑定一种特定的mapping(这个mapping里面能够指定该频道各自的特殊属性如何储存到ES)。
6.3.2>门店索引设计
门店索引方案,采用了默认的形式,就是一个索引叫作 shop_index, 5 shard 的形式。
6.4>集群节点设计方案
按照业务拆分,咱们将ES拆分为两大集群:商品索引集群(商品分城市索引和全文检索索引)和非商品索引集群(或叫通用集群,目前主要是门店索引和关键词提示索引)。这样分的主要缘由是,商品索引数据量较大,并且它是主站主要业务逻辑,因此将其单独设立集群。
网络拓扑以下图所示:
图10 集群网络拓扑
6.5>分词设计
中文检索最主要的问题是分词,可是分词有一个很大的弊端:当我增长一个新的词库后,须要从新索引现有数据,致使咱们重建索引代价较大。因此在牺牲一些查询效率的状况下,窝窝采起了在创建索引时作单字索引,在查询时控制分词索引的方案。
具体方案以下所示:
图11 分词设计
6.6>高可用和可伸缩方案
看一下窝窝商品索引,窝窝采用的方案是一个城市一个索引,全部索引的“副本(replicas)”都设为 1, 这样好比 shop_index,它有 5 个 shard,每一个 shard (只)有一个副本。(注:1个副本一方面能够省空间,另外一方面是为了效率,在 ES 0.90版本下,ES 的副本更新是全量备份的方案,多个副本就会有更新效率的问题。ES 1.0 后有改进,王超认为在增长服务器后,能够考虑多增长副本。)
ES 会保证全部 shard 的主副本不在同一个 node 上面,但咱们是 ES 服务器集群,每台服务器上有多个 node,一个 shard 的主副本不在同一个节点仍是不够的,咱们还须要一个 shard 的主副本不在同一台服务器,甚至在多台物理机的状况下保证要保证不在同一个机架上,才能够保证系统的高可用性。 mongodb
因此ES提供了一个配置:cluster.routing.allocation.awareness.attributes: rack_id。 json
这个属性保证了主副 shard 会分配到名称不一样的 rack_id 上面。
当咱们中止一个节点时,如中止 174_node_2,则 ES 会自动从新平衡数据,以下图所示:
图13 从新分布
即便一台物理机彻底 down 掉,咱们能够看到其余物理机上的数据是完整的,ES 依然能够保证服务正常。
七,ES 的几回事故和教训
7.1>误删数据
ES 的 Web 控制台权限很大,能够删数据。
有一天,一个开发者须要查询索引 mapping,他用 firefox 的插件访问,结果 Method 默认竟然为 DELETE,以下图所示:
图14 delete
没有注意,因而悲剧发生了。
教训:
1)后来耀华咨询了长期运维ES的一些人,大部分都建议前置一个 ngnix,经过 ngnix 禁用 delete 和 put 的 HTTP 请求,借此来限制开放的ES接口服务。
2)此次的误操做,实际上
是在没有给定索引的状况下,误执行了DELETE 操做,结果删除了所有索引。其实配一下 ES 是能够避免的,加入这个配置:
action.disable_delete_all_indices=true
这样会禁止删除全部索引的命令,删除索引的话,必需要给定一个索引 ,稍微安全一些。
7.2>mvel 脚本引起的ES事故
ES集群表象:
一天,ES 各个节点负载升高,JVM Full GC 频繁。
查看其内存使用情况发现,ES 各个节点的 JVM perm 区均处于满或者将要满的状态,以下图所示:
图15 当时perm的容量
注1:jstat -gc <pid>命令返回结果集中,上图红色方框中字段的含义为:
PC Current permanent space capacity (KB). 当前perm的容量 ;
PU Permanent space utilization (KB). perm的使用。
你们能够看到图1中的PU值基本等于PC值了。
问题缘由:
头一天上线了商品搜索的一个动态排序功能,它采用 ES 的 mvel 脚本 来动态计算商品的排序分值。而 mvel 的原理是基于 JIT,动态字节码生成的,因而颇有可能形成 perm 区持续升高,缘由是它不断地加载和生成动态 class。
因为 ES 各个节点的 perm 区接近饱和状态,因此形成了服务器负载升高,GC 频繁,并进一步形成 ES 集群出现了相似于“脑裂”的状态。
经验教训:
- 引入新技术,仍是要谨慎,毕竟若是真是 mevl 脚本引发的问题,其实线下作压力测试就能提早发现。
- 增强ES的监控。
- 虽然如今回过头来看,若是在第一时间重启全部 nodes,损失应该是最小的——可是王超认为当时采用的保守策略依然是有意义的,由于在弄清楚问题缘由以前,直接重启 nodes 有可能反而形成更大的数据破坏。
7.3>mark shard as failed 的 ES事故
问题现象:
一天,打算对 JVM 参数和 ES 配置作了小幅度的谨慎调整。
凌晨 00:10 左右,维护者开始按照计划对 ES 集群的各个 node 依次进行重启操做,以便使新配置的参数生效(这类操做以前进行过不少次,比较熟练)。
1, 使用 http 正常的关闭接口,通知 174_0 节点进行关闭,成功。
2, 观察其他 node 的状态,几十秒后,ES 剩余的9个节点恢复了 green 状态。
3, 启动 174_0 节点。
——至此仅仅完成了 174_0 节点的重启工做,但紧接着就发现了问题:174_0 节点没法加入集群!
此时的状态是:174_0 报告本身找不到 master,剩余9个节点的集群依然运行良好。
因而用 jstat 查看了 174_0 的内存占用状况,发现其 ParNew 区在正常增加,因此他认为此次重启只是比往常稍慢而已,决定等待。
可是在 00:16 左右,主节点 174_4 被踢出集群,174_3 被选举为主节点。
以后,日志出现了 shard 损坏的状况:
[WARN ][cluster.action.shard ] [174_node_2] sending failed shard for [goods_city_188][0], node[eVxRF1mzRc-ekLi_4GvoeA], [R], s[STARTED], reason [master [174_node_4][6s7o-Yr-QXayxeXRROnFPg][inet[/174:9354]]{rack_id=rack_e_14}
marked shard as started, but shard has not been created, mark shard as failed]
更糟的是,不但损坏的 shard 没法自动回复,并且损坏的 shard 数量愈来愈多,最终在将近 01:00 的时候,集群由 yellow 状态转为 red 状态,数据再也不完整(此时损坏的主 shard 不到 20%,大部分城市仍是能够访问的)。
临时解决办法:
首先对主站作业务降级,关闭了来自前端工程的流量。
维护者开始采用第一套方案:依次关闭全部 node,而后再依次启动全部 node。此时上面新增的 gateway 系列参数开始起做用,集群在达到 6 个 node 以上才开始自动恢复,而且在几分钟后自动恢复了全部的 shard,集群状态恢复 green。
随后打开了前端流量,主站恢复正常。
接着补刷了过去2小时的数据到 ES 中。
至此,故障彻底恢复。
经验教训:
1, 这次事故发生时,出问题的 nodes 都是老配置;而事故修复以后的全部 nodes 都是采用的新配置,因此
能够肯定这次问题并非新配置致使的。
2,
ES 自身的集群状态管理(至少在 0.90.2 这个版本上)是有问题的——先是从正常状态逐渐变为“愈来愈多的 shard 损坏”,重启以后数据又彻底恢复,全部 shard 都没有损坏。
3, 因为是深夜且很短期就恢复了服务,所幸影响范围较小。
4,
故障缘由不明,因此随后安排 ES 从 0.90 版本升级到 1.3 版本。
八,ES 存在的问题以及改进
Elastic Search 在窝窝运行几年来基本稳定,可靠性和可用性也比较高,可是也暴露了一些问题:
- ES 的更新效率,做为基于 lucene 的分布式中间件,受限于底层数据结构,因此其更新索引的效率较低,lucene 一直在优化;
- ES 的可靠性的前提是保证其集群的总体稳定性,但咱们遇到的状况,每每是当某个节点性能不佳的状况下,可能会拖累与其同服务器上的全部节点,从而形成整个集群的不稳定。
- 增长服务器,让节点尽量地散开;
- 当某个节点出现问题的时候,须要咱们及早发现处理,不至于拖累整个集群。其实监控一个节点是否正常的方法不难,ES 是基于 JVM 的服务,它出现问题,每每和 GC、和内存有关,因此只要监控其内存达到某个上限就报警便可;
- 没有一个好的客户端可视化集群管理工具,官方或者主流的可视化管理工具,基本都是基于 ES 插件的,不符合咱们的要求,因此须要一款可用的客户端可视化集群管理工具;
- ES 的升级问题,因为 ES 是一个快速发展的中间件系统,每一次新版本的更新,更改较大,甚至致使咱们没法兼容老版本,因此 ES 升级问题是个不小的问题,再加上咱们数据量较大,迁移也比较困难。
——END——