本文共 6945 字,大约阅读时间需要 23 分钟。
苏宁易购商品评价系统主要提供商品维度评价数量聚合、评价列表展示功能,并为其他业务系统提供商品评价数据支撑服务。功能涉及对亿级数据的数量聚合、排序、多维度查询等复杂的业务场景,关系型数据库的索引为B-Tree结构,适合数值区分度或离散度高的数据,而评价系统中单个商品评价可以达到数十万条,相同星级的评价数则为亿级,故不适宜使用关系型数据库。解决此类海量数据准实时聚合的技术选型有以倒排表作为索引结构的Solr和Elasticsearch(底层都是Apache Lucene)搜索引擎服务,还有以bitmap作为底层索引结构的实时分析统计数据库Druid,但Druid只是支持数据统计功能并不保存原始数据,无法满足商品评价类的功能需求。系统建设之初对团队对Solr更为熟悉,故在Solr和Elasticsearch两者之间选择的Solr作为商品评价索引数据存储服务。
苏宁易购商品评价系统架构如下:
图2-1根据苏宁易购技术规范要求,应用系统架构划分为前中后台三个主模块:
搜索引擎的索引称为反向索引,俗称倒排表,把文本分词得到字典,保存字典项与文档ID(Lucene自身doc ID而非应用端文档ID)的关系,在查询时根据字典查询到倒排表文档ID集合,再进行交并集操作即可得到结果,其主要结构是字典域、索引域和字段存储域。反向索引如下图:
图3-1在Solr4.0之后为了满足排序和关键字聚合的需求场景,Lucene提供了DocValues特性,又被称为正排表,使用列式存储保存文档ID和字典项的关系,不再使用之前FieldValue Cache机制,提升性能并降低对内存的使用和虚拟机Full GC的风险。在Elasticsearch中所有字段都是默认开启此特性,但在Solr中需要使用者进行显式配置生效。DocValues存储结构可分为两种,一种是原值,一种是字典项ID。
doc[0] = 1005doc[1] = 1006doc[2] = 1005
doc[0] = \u0026quot;aardvark\u0026quot;doc[1] = \u0026quot;beaver\u0026quot;doc[2] = \u0026quot;aardvark\u0026quot;
假如“aardvark”在字典表中ID为0,”beaver”在字典中ID为1,则实际结构为:
doc[0] = 0doc[1] = 1doc[2] = 0
字典表数据为:
term[0] = \u0026quot;aardvark\u0026quot;term[1] = \u0026quot;beaver\u0026quot;
注:字典表所有term都是有序的,故DocValues可以直接用于排序。
Solr是一个高性能、基于Lucene的开源全文搜索服务,提供丰富的查询语言,同时实现了可配置、可扩展并对查询性能提供了优化,提供完善的功能管理界面;在Lucene基础上对易用性和可用性进行了大量封装如Schema配置化、请求分发处理机制、插件化机制、数据导入、分布式、监控指标采集等,架构体系如下:
图3-2苏宁商品评价系统结合自身业务特点采用了Solr主从节点和聚合查询节点的组合架构,而不是通用的Solr Cloud架构,主要考虑到两点:
商品详情页展示商品/供应商维度审核通过的好中差评数量、标签数量、个性化评价项等数量,使用的是Solr facet机制。
图4-1/solr /select?q=*:*\u0026amp;wt=json\u0026amp;indent=true\u0026amp;facet=true\u0026amp;facet.field=qualityStar
/solr/select?q=*:*\u0026amp;wt=json\u0026amp;indent=true\u0026amp;facet=true\u0026amp;facet.query=picVideoFlag:1\u0026amp;facet.query=againReviewFlag:1
根据查询条件直接查询评价ID即可,限制查询字段并支持分页,因商品评价无需计算文档相似度且开启缓存可提升查询性能,故使用filter query替代query作为查询条件:
/solr/select?q=*:*\u0026amp;fq=(auditStat:0+OR+auditStat:1)\u0026amp;start=0\u0026amp;rows=10\u0026amp;fl=commodityReviewId
需要注意的是filter cache是以单个filter query为键缓存结果,可以根据业务需求要求拆成多个filter query,设置不同缓存策略以提升缓存利用率。
需要说明的是Solr分组关键字group的含义与关系型数据库的group by中的group并不相同,在Solr中指的是对查询的结果文档根据指定的字段进行分组,而非关系型数据库对字段分组,如苏宁商品评价需要展示每个评价的第一条商家回复内容,通过Solr查询商家回复并根据评价ID进行分组后取第一条记录即可(查询条件为评价ID列表),参数说明如下:
表4-1在使用Solr对结果集排序一般有两种方式:
一是在写入索引时单独字段保存数值,在查询时使用sort字段直接排序即可,优点是简单,但调整排序规则时需要重建所有索引。
二是函数查询机制,在Solr标准查询解析器中使用solr内置的函数,指定sort字段为函数内容或在DisMax中指定bf参数都可以满足业务需求,例如sort=div(popularity,price)desc,score desc格式,div函数表示popularity和price两个字段相除。
大部分排序都使用第二种方式,但此方式对性能会有影响,特别是在涉及到多个字段时且参与排序的文档数较多时,其内部执行过程需要获取每个匹配doc的字段值进行计算,即使字段开启docValues特性对存储、IO和内存空间也有一定压力。
提升函数排序的性能也有两种方式:
Lucene字段级facet有三种机制:
facet.method参数指定在对字段进行聚合时使用上述哪种算法,根据上述实现机制说明可知对于值区分度低的字段,适合选择enum机制;对于有大量不同值的字段合适使用fc机制,若Solr开启了近实时搜索(NRT)特性,fcs机制则是更好的选择,因其在生成新索引段时旧索引段缓存不用重新加载。
苏宁易购商品电商模型中SPU和SKU可以在商品上架时根据产品特性和销售情况动态调整组合,查询SPU商品评价时就会出现跨多个节点的场景,需要支持跨节点的分布式查询并对聚合结果集进行二次处理,如数量的累加、列表二次排序和分页等,为此搭建空数据的Solr节点作为聚合查询节点。业务方根据SPU和SKU关系得出节点编号和节点的地址,改写查询请求添加shards参数转发给聚合节点即可,示例如下:
/solr/select?q=*:*\u0026amp;wt=json\u0026amp;indent=true\u0026amp;facet=true\u0026amp;facet.query=picVideoFlag:1\u0026amp;facet.query=againReviewFlag:1\u0026amp;shards=solr1:8080/,solr2:8080/,solr3:8080/
此特性原理是使用多线程查询多个Solr节点,在内存中对结果进行合并,故使用此特性也有一定限制:
开启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的风险。
配置示例如下:
\u0026lt;query\u0026gt;\t\u0026lt;maxBooleanClauses\u0026gt;2000\u0026lt;/maxBooleanClauses\u0026gt;\t\u0026lt;filterCache class=\u0026quot;solr.FastLRUCache\u0026quot; size=\u0026quot;8192\u0026quot; initialSize=\u0026quot;4096\u0026quot; autowarmCount=\u0026quot;80%\u0026quot; /\u0026gt;\t\u0026lt;enableLazyFieldLoading\u0026gt;true\u0026lt;/enableLazyFieldLoading\u0026gt;\t\u0026lt;listener event=\u0026quot;newSearcher\u0026quot; class=\u0026quot;solr.QuerySenderListener\u0026quot;\u0026gt;\t\t\u0026lt;arr name=\u0026quot;queries\u0026quot;\u0026gt;\t\t\t\u0026lt;!--预加载查询--\u0026gt;\t\t\u0026lt;/arr\u0026gt;\t\t\u0026lt;/listener\u0026gt;\t\u0026lt;listener event=\u0026quot;firstSearcher\u0026quot; class=\u0026quot;solr.QuerySenderListener\u0026quot;\u0026gt;\t\t\u0026lt;arr name=\u0026quot;queries\u0026quot;\u0026gt;\t\t\t\u0026lt;!--预加载查询--\u0026gt;\t\t\u0026lt;/arr\u0026gt;\t\u0026lt;/listener\u0026gt;\t\u0026lt;useColdSearcher\u0026gt;false\u0026lt;/useColdSearcher\u0026gt;\t\u0026lt;maxWarmingSearchers\u0026gt;1\u0026lt;/maxWarmingSearchers\u0026gt;\u0026lt;/query\u0026gt;
目前在CDN缓存和Redis缓存失效情况部分请求的压力还是回源到Solr,对于Solr的直接依赖较大,且更新索引时偶尔还是会触发Full GC,影响业务的稳定性。下一步需要对缓存架构重新设计,设计缓存异步更新和熔断机制,限制用户请求直接落到Solr服务上,提升接口QPS、系统稳定性和评价展示生效实时性。
另外还在规划引入Elasticsearch作为后台业务索引,前后台索引进行分离,降低前后台耦合度和系统风险,更便于业务开发。
胡正林,苏宁易购IT总部消费者平台研发中心高级架构师,十余年软件开发经验,熟悉大型分布式高并发系统架构和开发,目前主要负责易购各系统架构优化与大促保障工作。
转载地址:http://msghx.baihongyu.com/