2016年我负责了一个搜索系统的架构设计用Elasticsearch做搜索引擎要求高可用、高并发。

说起来这个项目是公司的一个核心业务做的是一个电商平台商品搜索功能。每天有几百万的搜索请求高峰期每秒有几千的并发对可用性和性能要求都很高。如果搜索挂了整个网站就基本瘫痪了用户找不到商品就没法下单影响很大。

之前公司的搜索是用MySQL的LIKE查询做的数据量小的时候还能用后来商品越来越多有几百万的商品LIKE查询就很慢了而且支持不了复杂的搜索功能比如分词相关性排序过滤等用户体验很差。

于是公司决定重构搜索系统用Elasticsearch做搜索引擎我负责这个项目的架构设计和实现。

我花了一个月时间进行架构设计和调优最终系统稳定运行可用性达到99.99%,搜索响应时间平均50毫秒以内满足了业务的需求。

今天就来聊聊这次Elasticsearch搜索架构设计的经历以及搜索架构设计的一些经验。

一、需求分析

做架构设计之前首先要明确需求知道要做什么达到什么目标。

这个项目的主要需求如下:

1. 功能需求

  • 商品搜索:用户输入关键词搜索相关的商品。
  • 分词支持:支持中文分词英文分词能正确处理各种语言。
  • 相关性排序:搜索结果按相关性排序最相关的商品排在前面。
  • 多维度过滤:支持按分类品牌价格区间属性等多维度过滤。
  • 排序支持:支持按销量价格上架时间评价等多种方式排序。
  • 搜索建议:输入关键词的时候给出搜索建议提升用户体验。
  • 热门搜索:展示热门搜索词引导用户搜索。
  • 搜索统计:统计搜索词搜索量转化率等数据用于运营分析。

2. 性能需求

  • 搜索响应时间:平均50毫秒以内99%的请求100毫秒以内。
  • 并发支持:高峰期支持5000 QPS以上。
  • 数据量:支持千万级的商品数据并且持续增长。
  • 实时性:商品信息更新之后1秒内能搜索到。

3. 可用性需求

  • 系统可用性:99.99%,以上全年, downtime不超过5分钟。
  • 故障恢复:单节点故障不影响系统正常运行自动恢复。
  • 数据安全:数据不丢即使节点故障数据也能恢复。
  • 扩容能力:支持在线扩容不影响系统正常运行。

4. 运维需求

  • 监控告警:完善的监控和告警系统出了问题及时发现及时处理。
  • 日志记录:完善的日志记录出了问题能快速定位原因。
  • 备份恢复:定期备份数据出了问题能快速恢复。
  • 易于运维:系统易于运维日常操作简单方便。

明确了需求之后就开始架构设计。

二、技术选型

首先是技术选型为什么选择Elasticsearch而不是其他的搜索引擎。

当时主流的搜索引擎有以下几种:

  1. Elasticsearch:基于Lucene的分布式搜索引擎功能强大性能好易用性好社区活跃生态丰富支持RESTful API和多种客户端。
  2. Solr:也是基于Lucene的搜索引擎功能也很强大但是易用性不如Elasticsearch分布式支持也不如Elasticsearch方便。
  3. Sphinx:高性能的全文搜索引擎性能很好但是功能相对简单易用性一般社区也不如Elasticsearch活跃。
  4. 自研:基于Lucene自研搜索引擎灵活度高但是开发成本高维护成本也高不适合快速上线。

综合比较之后我们选择了Elasticsearch原因如下:

  • 功能强大:支持全文搜索分词过滤排序聚合等各种搜索功能满足我们的需求。
  • 性能,好:基于Lucene性能很好支持高并发低延迟。
  • 分布式支持,好:天生支持分布式集群分片副本高可用扩容都很方便。
  • 易用性,好:RESTful API和丰富的客户端上手容易开发效率,高。
  • 社区活跃生态丰富:有很多插件工具文档遇到问题容易找到解决方案。
  • 实时性,好:支持近实时搜索数据写入之后1秒内就能搜索到。

所以最终我们选择了Elasticsearch作为搜索引擎。

三、集群架构设计

接下来是集群架构设计。

Elasticsearch天生支持分布式集群我们设计了一个多节点的集群保证高可用和高并发。

1. 节点规划

我们规划了以下几种节点:

  • 主节点(Master Node):负责集群管理比如索引创建删除分片分配节点加入退出,等。主节点不处理搜索请求只负责集群管理避免被搜索请求影响稳定性。我们设置了3个主节点防止脑裂保证高可用。
  • 数据节点(Data Node):负责存储数据和处理搜索请求是集群的核心节点。我们设置了多个数据节点根据数据量和并发量调整数量。初期设置了6个数据节点后续根据需求扩容。
  • 协调节点(Coordinating Node):负责接收客户端请求转发到数据节点收集结果返回给客户端。协调节点不存储数据只负责请求转发和结果合并减轻数据节点的压力。我们设置了2个协调节点做负载均衡和高可用。
  • 摄取节点(Ingest Node):负责数据写入之前的预处理比如数据清洗转换丰富,等。我们设置了2个摄取节点处理数据写入的预处理。

这样不同的节点职责清晰各司其职避免一个节点做太多事情影响性能和稳定性。

2. 分片和副本设计

Elasticsearch把索引分成多个分片每个分片可以有多个副本分布在不同的节点上保证高可用和高并发。

我们的分片和副本设计如下:

  • 主分片(Primary Shard):每个索引设置12个主分片。主分片的数量在索引创建之后就不能改了所以要提前规划好根据数据量预估未来3年的数据量设置合适的分片数量。12个主分片能支持千万级到亿级的数据量满足我们的需求。
  • 副本分片(Replica Shard):每个主分片设置2个副本也就是每个分片有1个主分片,+,2个副本共3份数据。这样即使2个节点故障数据也不会丢而且副本能处理搜索请求提升并发能力。
  • 分片分配:主分片和副本分片分布在不同的节点上同一个分片的主分片和副本不会在同一个节点上保证节点故障的时候数据不丢服务不中断。

这样分片和副本的设计保证了数据的高可用和搜索的高并发。

3. 集群高可用设计

为了保证集群的高可用我们做了以下设计:

  • 多节点:集群有多个节点单节点故障不影响集群正常运行。
  • 多副本:每个分片有多个副本节点故障的时候副本能提升为主分片继续提供服务。
  • 主节点高可用:3个主节点防止脑裂单主节点故障其他主节点能选举新的主节点继续管理集群。
  • 协调节点高可用:2个协调节点做负载均衡单协调节点故障另一个能继续处理请求。
  • 跨机架部署:节点分布在不同的机架上避免同一个机架故障导致多个节点同时故障。
  • 监控告警:完善的监控和告警节点故障性能异常等及时告警及时处理。

这样集群的高可用有了保障可用性能达到99.99%,以上。

四、索引设计

接下来是索引设计好的索引设计能提升搜索性能和用户体验。

1. 索引规划

我们根据数据的特点和查询模式规划了以下索引:

  • 商品主索引:存储商品的主要信息比如商品ID名称描述分类品牌价格属性等是搜索的核心索引。
  • 搜索建议索引:存储搜索建议的数据用于输入关键词的时候给出搜索建议。
  • 搜索日志索引:存储用户搜索的日志用于搜索统计和分析。
  • 热门搜索索引:存储热门搜索词的数据用于展示热门搜索。

不同的索引分开存储避免一个索引太大影响性能也便于分别优化和管理。

2. Mapping设计

Mapping定义了索引中字段的类型和属性好的Mapping设计能提升搜索性能和准确性。

我们的商品主索引的Mapping设计如下:

  • 商品ID:long类型不分词用于精确匹配。
  • 商品名称:text类型使用IK分词器分词用于全文搜索。同时设置keyword子字段不分词用于聚合和排序。
  • 商品描述:text类型使用IK分词器分词用于全文搜索。
  • 分类ID:long类型不分词用于过滤。
  • 分类名称:keyword类型不分词用于过滤和聚合。
  • 品牌ID:long类型不分词用于过滤。
  • 品牌名称:keyword类型不分词用于过滤和聚合。
  • 价格:double类型不分词用于过滤和排序。
  • 销量:integer类型不分词用于排序。
  • 上架时间:date类型不分词用于过滤和排序。
  • 评价数:integer类型不分词用于排序。
  • 评分:double类型不分词用于排序。
  • 商品属性:nested类型存储商品的各种属性比如颜色尺寸材质等用于多维度过滤。
  • 商品图片:keyword类型不分词用于展示不搜索。

这样每个字段的类型和属性都根据查询模式设计既能支持全文搜索又能支持过滤排序聚合等功能。

3. 分词器选择

中文分词是中文搜索的关键好的分词器能提升搜索的准确性和用户体验。

我们选择了IK分词器作为中文分词器原因如下:

  • 分词准确:IK分词器对中文的分词效果很好能正确处理各种中文词汇。
  • 支持自定义词典:可以添加自定义的词典比如品牌词商品词行业术语等提升分词准确性。
  • 支持两种分词模式:iksmart和ikmaxword,iksmart是智能分词会做最粗粒度的拆分ikmaxword是最细粒度的拆分会把文本拆成最细的粒度。我们索引的时候用ikmaxword搜索的时候用ik_smart兼顾召回率和准确性。
  • 性能,好:IK分词器性能很好分词速度快不影响索引和搜索性能。
  • 社区活跃:IK分词器社区活跃更新及时遇到问题容易找到解决方案。

除了IK分词器我们还用了Elasticsearch自带的standard分词器处理英文和数字用了keyword分词器处理不需要分词的字段。

4. 索引优化

为了提升索引性能我们做了以下优化:

  • 合理设置分片数量:根据数据量设置合适的分片数量避免分片太多或者太少影响性能。
  • 合理设置副本数量:根据可用性和并发需求设置合适的副本数量副本太多浪费资源太少可用性不够。
  • 关闭不需要的功能:比如不需要的字段不索引不需要的功能关闭减少资源消耗。
  • 使用压缩:开启索引压缩减少存储空间也能提升IO性能。
  • 合理设置刷新间隔:调整refresh_interval平衡实时性和索引性能。我们设置了1秒兼顾实时性和性能。
  • 使用批量索引:数据写入的时候用批量接口提升索引性能。
  • 异步索引:商品信息更新的时候异步更新索引不影响主业务的性能。

五、查询优化

接下来是查询优化好的查询设计能提升搜索性能和用户体验。

1. 查询设计

我们的搜索查询设计如下:

  • 多字段搜索:在商品名称和商品描述上进行多字段搜索商品名称的权重更高因为名称更重要。
  • 布尔查询:用bool查询组合多个条件must,should,must_not,filter等灵活组合查询条件。
  • 过滤查询:分类品牌价格属性等过滤条件用filter上下文不计算相关性能利用缓存性能更好。
  • 相关性排序:默认按相关性排序用TF-IDF算法计算相关性最相关的商品排在前面。
  • 自定义排序:支持按销量价格上架时间评价等多种方式排序用户可以选择排序方式。
  • 分页:用from和size做分页但是深度分页性能差我们限制用户最多翻到100页避免深度分页影响性能。
  • 高亮:搜索结果中匹配的关键词高亮显示提升用户体验。

2. 查询优化

为了提升查询性能我们做了以下优化:

  • 使用filter上下文:过滤条件用filter上下文不计算相关性能利用缓存性能更好。
  • 避免深度分页:限制分页深度最多100页避免深度分页性能,差。如果需要翻更多用search_after或者scroll,API。
  • 合理设置超时:查询设置超时时间避免慢查询影响整个集群。
  • 使用查询缓存:开启查询缓存缓存常用的查询结果提升性能。
  • 避免昂贵的查询:比如wildcard,regexp,script等昂贵的查询尽量避免或者优化使用。
  • 预计算聚合:常用的聚合数据预计算缓存不用每次查询都计算提升性能。
  • 慢查询监控:监控慢查询找到性能差的查询优化它们。

3. 搜索建议

搜索建议是提升用户体验的重要功能用户输入关键词的时候给出相关的搜索建议帮助用户快速找到想要的商品。

我们用了Elasticsearch的completion,suggester做搜索建议原因如下:

  • 性能,好:completion,suggester是专门为搜索建议设计的性能非常好能达到毫秒级响应。
  • 支持前缀匹配:用户输入前缀就能给出建议符合搜索建议的场景。
  • 支持权重:可以给不同的建议设置不同的权重热门的搜索词权重高排在前面。
  • 支持模糊匹配:可以支持一定程度的模糊匹配用户输入有小错误也能给出建议。

我们的搜索建议数据来自用户的搜索日志统计热门的搜索词和商品名称定期更新搜索建议索引保证建议的准确性和时效性。

六、高可用保障

接下来是高可用保障除了集群本身的高可用设计我们还做了其他的高可用保障。

1. 数据备份

数据备份是数据安全的重要保障即使集群出了大问题也能从备份恢复数据。

我们的备份策略如下:

  • 快照备份:用Elasticsearch的snapshot,API定期给索引做快照备份存储在共享存储,上。每天全量备份每小时增量备份。
  • 备份验证:定期验证备份的可用性确保备份能正常恢复不要等需要恢复的时候才发现备份不可用。
  • 异地备份:备份数据存储在异地的存储上避免同一个机房故障导致备份也丢了。
  • 保留策略:备份保留最近30天的超过30天的自动删除节省存储空间。

2. 故障恢复

故障恢复是高可用的重要环节出了故障能快速恢复减少影响。

我们的故障恢复策略如下:

  • 自动恢复:单节点故障集群自动恢复副本提升为主分片重新分配分片不用人工干预。
  • 手动恢复:如果是大的故障比如整个集群挂了或者数据损坏用备份手动恢复数据。
  • 恢复演练:定期做故障恢复演练确保恢复流程可行恢复时间在可接受范围,内。
  • 降级方案:如果搜索集群暂时不可用有降级方案比如用缓存的热门搜索结果或者用MySQL的简单搜索保证基本的搜索功能可用不完全瘫痪。

3. 监控告警

监控告警是高可用的眼睛出了问题及时发现及时处理避免小问题变成大故障。

我们的监控告警体系如下:

  • 集群监控:监控集群的状态节点状态分片状态索引状态等集群异常及时告警。
  • 性能监控:监控搜索响应时间吞吐量并发数CPU内存磁盘IO网络等性能指标性能异常及时告警。
  • 资源监控:监控磁盘使用率内存使用率CPU使用率等资源指标资源不足及时告警提前扩容。
  • 慢查询监控:监控慢查询找到性能差的查询及时优化。
  • 错误监控:监控查询错误率索引错误率等错误指标错误率升高及时告警。
  • 告警方式:邮件短信微信电话等多种告警方式确保运维人员能及时收到告警。
  • 告警分级:告警分级别紧急重要一般不同级别的告警用不同的方式通知避免告警轰炸。

4. 容量规划

容量规划是高可用的前置保障提前预估资源需求提前扩容避免资源不足导致故障。

我们的容量规划策略如下:

  • 预估数据量:根据业务增长预估未来1-3年的数据量提前规划存储容量。
  • 预估并发量:根据业务增长预估未来1-3年的并发量提前规划计算容量。
  • 定期评估:定期评估容量使用情况根据实际使用情况调整容量规划。
  • 提前扩容:资源使用率达到70%,的时候就开始扩容不要等资源满了才扩容避免影响业务。
  • 在线扩容:Elasticsearch支持在线扩容添加节点之后自动重新分配分片不影响系统正常运行。

七、踩坑经验

在这个项目的过程中也踩了一些坑分享一下。

1. 分片数量设置不当

刚开始设计索引的时候分片数量设得太少只有3个后来数据量增长之后单个分片太大查询性能下降而且扩容也不方便因为主分片数量不能,改。

后来我们重新设计了索引把主分片数量改成了12个重新索引数据才解决了这个问题。

经验:主分片数量一定要提前规划好根据未来3年的数据量预估设置合适的数量不要设得太少也不要太多。一般单个分片的大小控制在20-50GB比较合适。

2. 深度分页性能差

刚开始我们没有限制分页深度用户可以随便翻页后来发现有用户翻到几千页查询非常慢影响了整个集群的性能。

后来我们限制了用户最多翻到100页超过100页就提示用户缩小搜索范围或者用更精确的关键词搜索才解决了这个问题。

经验:深度分页性能很差一定要限制分页深度或者用search_after或者scroll,API替代from/size分页。

3. 分词器词典更新

刚开始我们用了IK分词器的默认词典后来发现很多品牌词商品词行业术语分词不准确影响了搜索的准确性。

后来我们添加了自定义的词典把品牌词商品词行业术语等都加到自定义词典里定期更新词典分词准确性才提升了。

经验:中文分词自定义词典很重要一定要根据自己的业务添加自定义词典定期更新提升分词准确性。

4. 集群脑裂

有一次网络波动导致集群出现了脑裂两个主节点同时存在集群状态混乱影响了搜索服务。

后来我们检查了配置发现discovery.zen.minimummasternodes设置不对应该设置为主节点数量/2 + 1也就是3个主节点的话设置为2我们之前设置为1所以容易脑裂。

调整了配置之后就再也没有出现脑裂,了。

经验:主节点数量一定要是奇数比如3个5个7个而且discovery.zen.minimummasternodes一定要设置为主节点数量/2 + 1防止脑裂。

5. JVM内存设置不当

刚开始我们给Elasticsearch的JVM内存设得太大32GB后来发现性能反而不好而且内存浪费严重。

后来我们查了资料发现Elasticsearch的JVM内存不要超过32GB因为超过32GB之后JVM的指针压缩就失效了反而性能下降而且内存应该留一半给操作系统做文件系统缓存因为Elasticsearch大量依赖文件系统缓存提升性能。

后来我们把JVM内存改成了16GB留16GB给操作系统做缓存性能反而提升了很多。

经验:Elasticsearch的JVM内存不要超过32GB一般设置为服务器内存的一半比较合适留一半给操作系统做文件系统缓存。

八、写在最后

Elasticsearch搜索架构设计:高可用高并发。

这个项目从需求分析到架构设计到实现到调优花了一个月时间最终系统稳定运行可用性达到99.99%,搜索响应时间平均50毫秒以内满足了业务的需求老板和业务方都很满意。

搜索是很多系统的核心功能特别是电商内容等平台搜索的体验直接影响用户的体验和业务的转化。好的搜索架构能提升用户体验和系统性能这需要我们从多个层面综合设计和优化。

Elasticsearch是一个非常优秀的搜索引擎功能强大性能好易用性好社区活跃生态丰富是做搜索系统的好选择。但是要用好Elasticsearch也需要掌握它的原理和最佳实践合理设计架构索引查询等才能发挥它的最大性能。

希望我的这些经历和经验能对大家有所帮助特别是做搜索系统和Elasticsearch的朋友。

最后用一句话结尾:

"好的架构不是一蹴而就的而是不断思考不断实践不断优化的结果。"

愿大家都能设计出高可用高并发的优秀架构。