写点什么

苏宁 11.11:搜索引擎 Solr 在苏宁易购商品评价系统中的应用

胡正林

  • 2018-11-05
  • 本文字数:5027 字

    阅读完需:约 16 分钟

苏宁11.11:搜索引擎Solr在苏宁易购商品评价系统中的应用

背景说明

苏宁易购商品评价系统主要提供商品维度评价数量聚合、评价列表展示功能,并为其他业务系统提供商品评价数据支撑服务。功能涉及对亿级数据的数量聚合、排序、多维度查询等复杂的业务场景,关系型数据库的索引为 B-Tree 结构,适合数值区分度或离散度高的数据,而评价系统中单个商品评价可以达到数十万条,相同星级的评价数则为亿级,故不适宜使用关系型数据库。解决此类海量数据准实时聚合的技术选型有以倒排表作为索引结构的 Solr 和 Elasticsearch(底层都是 Apache Lucene)搜索引擎服务,还有以 bitmap 作为底层索引结构的实时分析统计数据库 Druid,但 Druid 只是支持数据统计功能并不保存原始数据,无法满足商品评价类的功能需求。系统建设之初对团队对 Solr 更为熟悉,故在 Solr 和 Elasticsearch 两者之间选择的 Solr 作为商品评价索引数据存储服务。

评价系统架构

苏宁易购商品评价系统架构如下:

image
图 2-1

应用服务模块

根据苏宁易购技术规范要求,应用系统架构划分为前中后台三个主模块:

  • 前台:为 web、app 提供页面或接口服务,接入苏宁统一认证系统 Passport;
  • 中台:通过苏宁自研 RSF 远程服务框架对前台服务和其他业务系统提供功能接口服务;
  • 后台:业务系统运营管理功能和任务批处理。

数据存储层

  • MySQL:存储和查询登录用户发表的评价,具备 ACID 特性,按照用户 ID 分库分表,根据业务要求和存储容量限制只保留指定时间内的评价数据
  • Redis 集群:苏宁在开源 Redis 中间件基础上自研的支持分布式集群、熔断、横向扩容的高可用、海量缓存服务架构,缓存商品评价数量和列表数据,承载用户端的数据访问请求,降低对业务系统的访问压力
  • HBase:海量评价内容存储
  • Solr:评价索引存储和查询服务

外围系统

  • 商品中心:通过 MQ(异步) 和 RSF(同步)接收保存商品主数据如商品信息、类目信息、店铺信息、商品关系信息等
  • 订单中心:待评商品主要来源于订单中心通过 MQ 下发的订单信息和订单状态通知,并由评价系统提供订单待评状态查询 RSF 接口,在订单页面展示商品评价发表入口
  • 搜索产品线:通过 MQ 下发商品评价数量到搜索产品线,为商品搜索排序计算提供数据支持

基础设施

  • 分布式任务调度系统 (UTS):对各业务系统中的定时任务进行集中管理和调度,解决了集群环境下任务并发执行的控制,让业务系统从繁琐的技术细节中释放出来,并提供了对任务调度执行情况的监控和异常短信告警、重试机制,且提供跨业务系统的后继任务调度。
  • 集中配置管理 (SCM):基于 Zookeeper 自研的应用配置信息集中统一管理和实时推送服务框架。
  • 消息中间件:由于历史原因,苏宁内部有多套 MQ 中间件,包括 IBM MQ,Kafka 和自研的 WindQ。IBM MQ 已经不建议使用,正在逐渐废弃;Kafka 在大数据场景下使用较多;业务系统推荐使用 WindQ,解决了解决多活场景下消息路由,提供消息发送重试熔断、在线动态扩缩容、顺序消费、异构 MQ 桥接等特性。

监控

  • 日志:改造 Solr 日志模块,接入苏宁统一日志框架,便于异常问题分析
  • 主机:接入 Zabbix 监控体系,监控主机和应用健康状态
  • 系统:接入苏宁云迹系统,监控 Solr 请求响应 TP90/99 等指标

Solr 介绍

搜索引擎基础原理

搜索引擎的索引称为反向索引,俗称倒排表,把文本分词得到字典,保存字典项与文档 ID(Lucene 自身 doc ID 而非应用端文档 ID)的关系,在查询时根据字典查询到倒排表文档 ID 集合,再进行交并集操作即可得到结果,其主要结构是字典域、索引域和字段存储域。反向索引如下图:

image
图 3-1

在 Solr4.0 之后为了满足排序和关键字聚合的需求场景,Lucene 提供了 DocValues 特性,又被称为正排表,使用列式存储保存文档 ID 和字典项的关系,不再使用之前 FieldValue Cache 机制,提升性能并降低对内存的使用和虚拟机 Full GC 的风险。在 Elasticsearch 中所有字段都是默认开启此特性,但在 Solr 中需要使用者进行显式配置生效。DocValues 存储结构可分为两种,一种是原值,一种是字典项 ID。

  • 单数值型和原始字节型字段,其基本结构包含字段原值的 long[] 数组,如带有三个数值的文档 DocValues 结构:
复制代码
doc[0] = 1005
doc[1] = 1006
doc[2] = 1005
  • 其他可索引类型字段,其结构也是一个 long[] 数组,只不过数组中的值为字典项 ID(可能有多个),例如有 3 个字符型字段的文档:
复制代码
doc[0] = "aardvark"
doc[1] = "beaver"
doc[2] = "aardvark"

假如“aardvark”在字典表中 ID 为 0,”beaver”在字典中 ID 为 1,则实际结构为:

复制代码
doc[0] = 0
doc[1] = 1
doc[2] = 0

字典表数据为:

复制代码
term[0] = "aardvark"
term[1] = "beaver"

注:字典表所有 term 都是有序的,故 DocValues 可以直接用于排序。

Solr&Lucene 架构体系

Solr 是一个高性能、基于 Lucene 的开源全文搜索服务,提供丰富的查询语言,同时实现了可配置、可扩展并对查询性能提供了优化,提供完善的功能管理界面;在 Lucene 基础上对易用性和可用性进行了大量封装如 Schema 配置化、请求分发处理机制、插件化机制、数据导入、分布式、监控指标采集等,架构体系如下:

image
图 3-2

苏宁商品评价系统结合自身业务特点采用了 Solr 主从节点和聚合查询节点的组合架构,而不是通用的 Solr Cloud 架构,主要考虑到两点:

  • 横向扩容机制完全可控,根据商品编码单调递增特性,可以随时扩充新节点,节点路由机制由业务系统控制。
  • 单独搭建聚合查询节点,在跨多个 Solr 节点查询时由聚合节点实现聚合查询功能,特别是在聚合商品评价列表查询场景下,降低对数据节点的性能压力。

Solr 特性应用

多维度数量聚合

商品详情页展示商品 / 供应商维度审核通过的好中差评数量、标签数量、个性化评价项等数量,使用的是 Solr facet 机制。

image
图 4-1

  • 单个字段 facet 如好中差评数据量 (上图方框处) 涉及星级字段直接设置 facet.field 参数
复制代码
/solr /select?q=*:*&wt=json&indent=true&facet=true&facet.field=qualityStar
  • 多条件 facet 如有图评价和已追评(上图圆圈处)评价数量,应使用 facet.query 机制把每个 facet.qery 查询当做单独 facet 值,一次性查询出结果而不是发起多次普通总数查询
复制代码
/solr/select?q=*:*&wt=json&indent=true&facet=true&facet.query=picVideoFlag:1&facet.query=againReviewFlag:1

多维度列表查询

根据查询条件直接查询评价 ID 即可,限制查询字段并支持分页,因商品评价无需计算文档相似度且开启缓存可提升查询性能,故使用 filter query 替代 query 作为查询条件:

复制代码
/solr/select?q=*:*&fq=(auditStat:0+OR+auditStat:1)&start=0&rows=10&fl=commodityReviewId

需要注意的是 filter cache 是以单个 filter query 为键缓存结果,可以根据业务需求要求拆成多个 filter query,设置不同缓存策略以提升缓存利用率。

分组查询

需要说明的是 Solr 分组关键字 group 的含义与关系型数据库的 group by 中的 group 并不相同,在 Solr 中指的是对查询的结果文档根据指定的字段进行分组,而非关系型数据库对字段分组,如苏宁商品评价需要展示每个评价的第一条商家回复内容,通过 Solr 查询商家回复并根据评价 ID 进行分组后取第一条记录即可(查询条件为评价 ID 列表),参数说明如下:

image
表 4-1

自定义排序

在使用 Solr 对结果集排序一般有两种方式:

  • 一是在写入索引时单独字段保存数值,在查询时使用 sort 字段直接排序即可,优点是简单,但调整排序规则时需要重建所有索引。

  • 二是函数查询机制,在 Solr 标准查询解析器中使用 solr 内置的函数, 指定 sort 字段为函数内容或在 DisMax 中指定 bf 参数都可以满足业务需求,例如 sort=div(popularity,price)desc,score desc 格式,div 函数表示 popularity 和 price 两个字段相除。

大部分排序都使用第二种方式,但此方式对性能会有影响,特别是在涉及到多个字段时且参与排序的文档数较多时,其内部执行过程需要获取每个匹配 doc 的字段值进行计算,即使字段开启 docValues 特性对存储、IO 和内存空间也有一定压力。

提升函数排序的性能也有两种方式:

  • 一是粗略的计算排序值,或写索引时即有一定顺序,查询时进行截断,只返回一定数量的文档,再进行二次排序,即通常电商搜索中粗排和精排过程。
  • 二是把涉及到的排序字段合并到一个字段并开启 docValues 特性,例如【评价时间|会员等级|评价星级|内容长度|图片个数|内容质量分|图片质量分|人工审核权重】此种格式,并编写自定义函数解析计算这个组合字段的排序值;继承 org.apache.Lucene.queries.function.ValueSource 实现排序值计算,继承 org.apache.solr.search.ValueSourceParser 实现此函数的解析创建即可。这也是苏宁商品评价系统准备采用的方式。

facet.method 参数的选择

Lucene 字段级 facet 有三种机制:

  • enum:遍历字段下所有的字典项,对所有字典项的倒排文档 ID 和查询结果文档 ID 进行交集运算得到结果
  • fc:根据查询结果的每个文档 ID 从 Field Cache 中查询字段值,计算每个字段值的次数得到结果
  • fcs:类似于 fc,不同点是基于 Lucene 中每个段单独的 Field Cache

facet.method 参数指定在对字段进行聚合时使用上述哪种算法,根据上述实现机制说明可知对于值区分度低的字段,适合选择 enum 机制;对于有大量不同值的字段合适使用 fc 机制,若 Solr 开启了近实时搜索 (NRT) 特性,fcs 机制则是更好的选择,因其在生成新索引段时旧索引段缓存不用重新加载。

分布式聚合查询

苏宁易购商品电商模型中 SPU 和 SKU 可以在商品上架时根据产品特性和销售情况动态调整组合,查询 SPU 商品评价时就会出现跨多个节点的场景,需要支持跨节点的分布式查询并对聚合结果集进行二次处理,如数量的累加、列表二次排序和分页等,为此搭建空数据的 Solr 节点作为聚合查询节点。业务方根据 SPU 和 SKU 关系得出节点编号和节点的地址,改写查询请求添加 shards 参数转发给聚合节点即可,示例如下:

复制代码
/solr/select?q=*:*&wt=json&indent=true&facet=true&facet.query=picVideoFlag:1&facet.query=againReviewFlag:1&shards=solr1:8080/,solr2:8080/,solr3:8080/

此特性原理是使用多线程查询多个 Solr 节点,在内存中对结果进行合并,故使用此特性也有一定限制:

  • 列表分页须限制大页码,防止列表合并分页时内存溢出或产生 Full GC 问题,性能急剧下降并可能拖垮服务节点
  • 相关性排序和 facet 特性可能与单节点查询结果可能不同
  • 注意设置线程池数量和 HTTP 超时时长
  • 文档在所有节点必须唯一,不允许重复
  • 不支持 pivot facet 和 join 特性

性能调优

  • 开启 filter cache,以查询条件为 key,文档 ID 列表为值保存在 cache 中,可以大幅提升数量聚合性能,但需要注意不适合字段值离散度较高的查询,否则产生大量 key 会把 cache 中的热点缓存替换出去,缓存命中率下降反而影响性能,可以通过设置 cache=false 或在函数查询中设置 fq={! cache=false}参数屏蔽当前请求的 cache 机制

  • 文档数较多且命中率低,需要关闭 documentCache 和 queryResultCache

  • 设置 newSearcher 和 firstSearcher 两个 Listener 的查询语句,对重新打开的 IndexSearcher 进行预热

  • JVM 使用 CMS GC 策略,并设置 CMSInitiatingOccupancyFraction 值为 60,保证在主从同步时 Solr 有足够的堆内存,降低因 CMS GC 内存碎片导致 Full GC 的风险。

配置示例如下:

复制代码
<query>
<maxBooleanClauses>2000</maxBooleanClauses>
<filterCache class="solr.FastLRUCache" size="8192" initialSize="4096" autowarmCount="80%" />
<enableLazyFieldLoading>true</enableLazyFieldLoading>
<listener event="newSearcher" class="solr.QuerySenderListener">
<arr name="queries">
<!-- 预加载查询 -->
</arr>
</listener>
<listener event="firstSearcher" class="solr.QuerySenderListener">
<arr name="queries">
<!-- 预加载查询 -->
</arr>
</listener>
<useColdSearcher>false</useColdSearcher>
<maxWarmingSearchers>1</maxWarmingSearchers>
</query>

展望

目前在 CDN 缓存和 Redis 缓存失效情况部分请求的压力还是回源到 Solr,对于 Solr 的直接依赖较大,且更新索引时偶尔还是会触发 Full GC,影响业务的稳定性。下一步需要对缓存架构重新设计,设计缓存异步更新和熔断机制,限制用户请求直接落到 Solr 服务上,提升接口 QPS、系统稳定性和评价展示生效实时性。

另外还在规划引入 Elasticsearch 作为后台业务索引,前后台索引进行分离,降低前后台耦合度和系统风险,更便于业务开发。

作者介绍

胡正林,苏宁易购 IT 总部消费者平台研发中心高级架构师,十余年软件开发经验,熟悉大型分布式高并发系统架构和开发,目前主要负责易购各系统架构优化与大促保障工作。

2018-11-05 15:363331

评论 1 条评论

发布
用户头像
学习了
2018-11-07 19:51
回复
没有更多了
发现更多内容

真的强!借助阿里技术博主分享的Android面试笔记,我拿到了字节跳动的offer

android 程序员 移动开发

程序员30岁+又怎么了呢?找准定位,kotlin可选参数

android 程序员 移动开发

知乎3000多人关注!27w+阅读开发者进阶难题,2021安卓开发社招面试解答之性能优化

android 程序员 移动开发

种草Dribbble:从未见过如此Q弹的Switcher,腾讯架构师深入讲解Android开发

android 程序员 移动开发

值得收藏的 Eureka 控制台详解

悟空聊架构

Eureka 源码解析 注册中心 悟空聊架构 11月日更

疫情下中年IT的焦虑,轻松拿下offer

android 程序员 移动开发

疫情让“灵活用工”浮出水面,一线互联网公司面经总结

android 程序员 移动开发

百度阿里腾讯头条面试Android高级岗必问!Handler源码解析!

android 程序员 移动开发

看我一波,Android获取进程名函数,代码优化到极致的操作

android 程序员 移动开发

真的已经讲烂了!想彻底掌握-binder-机制,必须先搞懂这几个关键类

android 程序员 移动开发

移动应用遗留系统重构(7)- 解耦重构演示篇(一),android开发环境的搭建视频

android 程序员 移动开发

移动开发的罗曼蒂克消亡史 _ InfoQ,网易云的朋友给我这份339页的Android面经

android 程序员 移动开发

移动端视频开发通过什么方式实现直播?十分钟带你快速了解

android 程序员 移动开发

疫情下,中年IT的焦虑,Android面试总结

android 程序员 移动开发

疫情下,每个职场人都要思考这4点,有了这些中高端面试专题-大厂还会远吗

android 程序员 移动开发

疫情之下,这真是一份完美的程序员宅家指南,androidsdk开发实例

android 程序员 移动开发

看完此文,你对Gradle的理解又升级了!,万字总结

android 程序员 移动开发

知乎热榜第一破10w阅读-Android不行了?2021程序员的风口又要来了,你还不抓紧机会

android 程序员 移动开发

疫情过后打算跳槽阿里,本以为凉了没想到居然是它“救,2021年Android大厂面试分享

android 程序员 移动开发

百万级日活 App 的屏幕录制功能是如何实现的,flutter小程序的onshow

android 程序员 移动开发

破解 Kotlin 协程(1) - 入门篇,35岁以后的Android程序员出路在哪里

android 程序员 移动开发

移动应用遗留系统重构(9)- 路由篇,移动开发者大会

android 程序员 移动开发

程序员35岁之后该怎样规划自己的职业?,Android开发手册

android 程序员 移动开发

移动应用遗留系统重构(9)- 路由篇(1),kotlin版权

android 程序员 移动开发

移动开发的罗曼蒂克消亡史,android系统开发书籍

android 程序员 移动开发

程序员中年了40岁,公司里熬到中层年薪30w+,最新高频Android面试题目分享

android 程序员 移动开发

程序员你所害怕的中年危机,恰恰是人生的转机!,kotlin高阶函数源码讲解

android 程序员 移动开发

程序员别只顾着敲代码了,老板们不喜欢聘请40岁程序员真相原来是

android 程序员 移动开发

移动开发程序员怎么优雅迈过30K+这道坎?成功入职字节跳动

android 程序员 移动开发

疫情结束后,会影响程序员年后找工作吗?,安卓开发面试题自定义view

android 程序员 移动开发

程序员为什么越老贬值的越厉害?,面试官6个灵魂拷问

android 程序员 移动开发

苏宁11.11:搜索引擎Solr在苏宁易购商品评价系统中的应用_架构_InfoQ精选文章