背景说明

苏宁易购商品评价系统主要提供商品维度评价数量聚合、评价列表展示功能,并为其他业务系统提供商品评价数据支撑服务。功能涉及对亿级数据的数量聚合、排序、多维度查询等复杂的业务场景,关系型数据库的索引为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总部消费者平台研发中心高级架构师,十余年软件开发经验,熟悉大型分布式高并发系统架构和开发,目前主要负责易购各系统架构优化与大促保障工作。

Comments are closed.