当前位置: 首页 > 后端技术 > Java

vivo评论平台流量与数据隔离实践

时间:2023-04-02 10:40:30 Java

一、背景vivo评论平台提供评论发布、点赞、举报、自定义评论排序等常用能力,帮助前端业务快速构建评论功能,并提供评论操作能力,避免前端业务重复建设和数据孤岛问题。目前已有vivo短视频、vivo浏览器、负一屏、vivo商城等10+业务接入。这些业务的流量大小和波动幅度各不相同。如何保证每个前端业务的高可用,避免某个业务的流量突然暴增导致其他业务不可用?所有商家的评论数据都由中台存储。他们的数据量不同,DB压力不同。作为中台,各个业务的数据应该如何隔离,才能保证整个中台系统的高可用?本文将与大家分享vivo点评平台的解决方案,主要从流量隔离和数据隔离两部分。2.流量隔离2.1流量分组vivo浏览器业务日活亿级,实时热点新闻全网推送。对于这种用户量大、流量大的重要业务,我们提供单独的集群为其提供服务,避免受到其他业务的影响。影响。vivo点评平台通过Dubbo接口对外提供服务。我们通过Dubbo标签路由对整个服务集群进行逻辑划分。Dubbo调用可以根据请求中携带的标签智能选择标签对应的服务提供者。转移。如下图所示:1)Providerlabeling:目前有两种方式完成实例分组,分别是动态规则标记和静态规则标记,其中动态规则的优先级高于静态规则,当这两种规则同时存在时同时发生冲突时,动态规则占上风。公司内部运维系统非常支持动态标记,只需标记指定ip的机器即可(不是docker容器,机器ip是固定的)。2)前端消费者指定的服务标签:发起请求时设置,如下;前端在中台指定路由标签RpcContext.getContext().setAttachment(Constants.REQUEST_TAG_KEY,"browser");请求标签的范围是每次调用,只需要在评论中调用平台服务前设置标签,前台业务调用其他业务的提供者,不受路由标签的影响。2.2多租户限流大流量业务通过独立集群隔离。但是独立部署集群的成本较高,无法为每个前端业务独立部署一套集群。大多数情况下,多个服务还是需要共享一套集群,那么当共享集群的服务遇到突发流量时,如何处理突发流量呢?没错,限流!但是目前很多限流都是采用一刀切的方式来限制接口的整体QPS。这样,某个前端业务的流量突然增加,就会导致所有的前端业务请求都被限制。这就需要多租户限流的出现(这里的一个租户可以理解为前端业务),支持对同一接口不同租户的流量进行限流处理。特点,使用服务ID码作为热点参数,为每个服务配置不同的流量控制大小。那么什么是热点参数限流呢?首先,什么是热点?热点是经常访问的数据。很多时候我们想统计某个热点数据中访问频率最高的前n条数据,限制其访问。例如:商品ID为参数,统计一段时间内最常购买的商品ID。用户标识是一个参数,针对一段时间内频繁访问的用户标识进行限制。热点参数限流会统计传入参数中的热点参数,根据配置的节流阈值和模式限制包含热点参数的资源调用。热点参数限流可以看作是一种特殊的流量控制,只对包含热点参数的资源调用生效。Sentinel采用LRU策略统计最近访问的热点参数,并结合令牌桶算法进行参数级流控。下图是一个注释场景的例子:使用Sentinel做资源保护主要分为几个步骤:定义资源、定义规则、处理规则生效。1)定义资源:这里可以理解为各个中台的API接口路径。2)定义规则:Sentienl支持QPS流控、自适应限流、热点参数限流、集群限流等多种规则,这里我们使用单机热点参数限流。热点参数限流配置{"resource":"com.vivo.internet.comment.facade.comment.CommentFacade:comment(com.vivo.internet.comment.facade.comment.dto.CommentRequestDto)",//需要限流interface"grade":1,//QPS限流模式"count":3000,//接口默认限流大小为3000"clusterMode":false,//单机模式"paramFieldName":"clientCode",//specifiedhotspot参数名称为业务方代码字段。这里我们优化了sentinel组件,增加了这个配置属性,指定参数对象的属性名作为热点参数key"paramFlowItemList":[//热点参数限流规则{"object":"vivo-community",//当clientCode为该值时,匹配限流规则"count":1000,//当前限流大小为1000"classType":"java.lang.String"},{"object":"vivo-shop",//当clientCode为该值时,匹配限流规则"count":2000,//限流大小为2000"classType":"java.lang.String"}]}3)规则生效处理:Sentinel触发限流规则时会抛出ParamFlowException异常,直接把异常抛给前端业务处理并不优雅。Sentinel为我们提供了统一的异常回调处理入口DubboAdapterGlobalConfig,支持我们将异常转化为业务定义的结果并返回。自定义流量限制返回结果;DubboAdapterGlobalConfig.setProviderFallback((invoker,invocation,ex)->AsyncRpcResult.newDefaultAsyncResult(FacadeResultUtils.returnWithFail(FacadeResultEnum.USER_FLOW_LIMIT),invocation));我们做了哪些额外的优化:1)内部限流控制台暂不支持热点参数限流配置,所以我们新增了一个限流配置控制器,支持通过配置中心动态下发限流配置。整体流程如下:动态下发限流配置;公共类VivoCfgDataSourceConfig实现InitializingBean{privatestaticfinalStringPARAM_FLOW_RULE_PREFIX="sentinel.param.flow.rule";@OverridepublicvoidafterPropertiesSet(){//自定义配置分析对象VivoCfgDataSource>paramFlowRuleVivoDataSource=newVivoCfgDataSource<>(PARAM_FLOW_RULE_PREFIX,sources->sources.stream().map(source->JSON.parseObject(source,ParamFlowRule.class)).collect(Collectors.toList()));//注册配置有效监听器ParamFlowRuleManager.register2Property(paramFlowRuleVivoDataSource.getProperty());//初始化限流配置paramFlowRuleVivoDataSource.init();//监听配置中心VivoConfigManager.addListener(((item,type)->{if(item.getName().startsWith(PARAM_FLOW_RULE_PREFIX)){paramFlowRuleVivoDataSource.updateValue(item,type);}}));}}2)原生sentinel指定限流热点参数的方式有两种:第一种是指定接口方法的第n个参数;二是方法参数继承自ParamFlowArgument实现ParamFlowKey方法,该方法的返回值为热点参数的值。这两种方法都不灵活,第一种方法不支持指定对象属性。;第二种方法需要我们修改代码。如果某个接口参数没有继承ParamFlowArgument,上线后想配置热点参数限流,只能修改代码,发布版本。因此,我们对Sentinel组件的热点参数限流源码做了一些优化,增加了“指定参数对象的某个属性”作为热点参数,并支持对象级嵌套。小的代码改动大大方便了热点参数的配置。修改热点参数校验逻辑;publicstaticbooleanpassCheck(ResourceWrapperresourceWrapper,/*@Valid*/ParamFlowRulerule,/*@Valid*/intcount,Object...args){//忽略部分代码//获取参数值。如果值为空,则通过。对象值=args[paramIdx];如果(值==null){返回真;}//使用paramFlowKey方法的结果赋值if(valueinstanceofParamFlowArgument){value=((ParamFlowArgument)value).paramFlowKey();}else{//根据classFieldName指定的热点参数获取热点参数值if(StringUtil.isNotBlank(rule.getClassFieldName())){//通过反射获取参数对象中的classFieldName属性Valuevalue=getParamFieldValue(value,规则.getClassFieldName());}}//忽略部分代码}3、MongoDB数据隔离为什么需要数据隔离?有两个原因。第一点:中台存储了不同前端业务的数据,数据查询时各个业务的数据不能相互影响,业务A无法查询到业务B的数据。第二点:数据每个业务的级别不同,对db运行的压力也不同。比如流量隔离,我们为浏览器业务单独提供了一套服务集群,那么浏览器业务使用的db也需要单独配置,从而可以完全隔离其他业务的服务压力。vivo评测平台使用MongoDB作为存储介质(对数据库选型和Mongodb应用细节感兴趣的同学可以看我们之前的介绍《MongoDB 在评论中台的实践》)。为了隔离来自不同业务方的数据,点评平台提供了两种数据隔离方案:物理隔离和逻辑隔离。3.1物理隔离不同业务方的数据存储在不同的数据库集群中,这就需要我们的系统支持MongoDB的多种数据源。实现过程如下:1)寻找合适的入口通过分析spring-data-mongodb的执行过程源码,发现在执行所有语句之前,会执行一个getDB()动作,获取数据库连接实例,如下。spring-data-mongodbdb操作源码;privateTexecuteFindOneInternal(CollectionCallbackcollectionCallback,DbObjectCallbackobjectCallback,StringcollectionName){try{//关键代码getDb()Tresult=objectCallback.doWith(collectionCallback.doInCollection(getAndPrepareCollection(getDb(),collectionName))));返回结果;}catch(RuntimeExceptione){throwpotentiallyConvertRuntimeException(e,exceptionTranslator);}}getDB()会执行org.springframework.data.mongodb.MongoDbFactory接口的getDb()方法,默认情况下,使用MongoDbFactory的SimpleMongoDbFactory实现。看到这里,我们很自然地可以想到使用“代理模式”,将SimpleMongoDbFactory替换为SimpleMongoDbFactory代理对象,在代理对象实例内部为每个MongoDB集合创建一个SimpleMongoDbFactory。在执行db操作时,执行代理对象的getDb()操作,只需要做两件事;找到集群对应的SimpleMongoDbFactory对象,执行SimpleMongoDbFactory.getdb()操作。类图如下。整体执行流程如下:3.1.2核心代码实现dubbofilter获取业务标识并设置到上下文中;privatebooleansetCustomerCode(Objectargument){//从字符串类型参数中获取业务标识信息if(argumentinstanceofString){if(!Pattern.matches("client.*",(String)argument)){returnfalse;}//将业务身份信息设置到上下文CustomerThreadLocalUtil.setCustomerCode((String)argument);返回真;}else{//从列表类型中获取参数对象if(argumentinstanceofList){ListlistArg=(List)argument;如果(CollectionUtils.isEmpty(listArg)){返回false;}argument=((List)argument).get(0);}//从object对象获取业务标识信息try{Methodmethod=argument.getClass().getMethod(GET_CLIENT_CODE_METHOD);对象object=method.invoke(argument);//验证商家身份是否合法?ClientParamCheckServiceclientParamCheckService=ApplicationUtil.getBe一个(ClientParamCheckService.class);clientParamCheckService.checkClientValid(String.valueOf(object));//设置业务身份信息到上下文CustomerThreadLocalUtil.setCustomerCode((String)object);返回真;}catch(NoSuchMethodException|IllegalAccessException|InvocationTargetExceptione){log.debug("反射获取clientCode失败,入参为:{}",argument.getClass().getName(),e);返回假;}}}MongoDB集群路由代理类;publicclassMultiMongoDbFactoryextendsSimpleMongoDbFactory{//不同集群的数据库实例缓存:key为MongoDB集群配置名,value为对应的业务MongoDB集群实例privatefinalMapmongoDbFactoryMap=newConcurrentHashMap<>();//添加创建的MongoDBCluster实例publicvoidaddDb(StringdbKey,SimpleMongoDbFactorymongoDbFactory){mongoDbFactoryMap.put(dbKey,mongoDbFactory);}@OverridepublicDBgetDb()throwsDataAccessException{//从上下文中获取前台业务代码StringcustomerCode=CustomerThreadLocalUtil.getCustomerCode();//获取业务对应的MongoDB配置名StringdbKey=VivoConfigManager.get(ConfigKeyConstants.USER_DB_KEY_PREFIX+customerCode);//从连接缓存中获取对应的SimpleMongoDbFactory实例if(dbKey!=null&&mongoDbFactoryMap.get(dbKey)!=null){//执行SimpleMongoDbFactory.getDb()操作returnmongoDbFactoryMap.get(dbKey).getDb();}返回super.getDb();}}自定义MongoDB操作模板;@BeanpublicMongoTemplatecreateIgnoreClass(){//生成MultiMongoDbFactory代理MultiMongoDbFactorymultiMongoDbFactory=multiMongoDbFactory();如果(multiMongoDbFactory==null){返回null;}MappingMongoConverter转换器=newMappingMongoConverter(newDefaultDbRefResolver(multiMongoDbFactory),newMongoMappingContext());转换器。setTypeMapper(新的DefaultMongoTypeMapper(空));//使用multiMongoDbFactory代理生成MongoDB操作模板returnnewMongoTemplate(multiMongoDbFactory,converter);}3.2逻辑隔离物理隔离是最彻底的数据隔离,但是我们不可能为每个业务都搭建一个独立的MongoDB集群。当多个业务共享一个数据库时,需要做数据逻辑隔离。逻辑隔离一般分为两种:一种是表隔离:不同业务方的数据存储在同一个数据库的不同表中,不同的业务操作使用不同的数据表。一是行隔离:不同业务方的数据存储在同一张表中,表中存储冗余的业务方代码。在读取数据时,通过业务代码过滤条件达到隔离数据的目的。综合考虑实现成本和业务场景,我们选择了表隔离的方式。实现过程如下:1)初始化数据表每次有新的业务连接,我们都会为业务分配一个唯一的标识码。我们直接使用这个标识码作为业务表名的后缀,对表进行初始化,例如:商城评论表comment\_info\_vshop,社区评论表comment\_info\_community。2)自动查表直接使用spring-data-mongodb的@Document注解支持Spel的能力,结合我们的业务身份信息上下文,实现自动查表。自动查表@Document(collection="comment_info_#{T(com.vivo.internet.comment.common.utils.CustomerThreadLocalUtil).getCustomerCode()}")publicclassComment{//表字段忽略}两种隔离方式的结合最终的整体效果:4.最后,通过上述实践,我们很好的支持了不同量级的前端业务,并且做到了不侵入业务代码,更好的解耦了技术和业务之间的复杂度。此外,我们还针对不同的业务,对项目中使用的Redis集群和ES集群进行了隔离。大体思路类似于MongoDB的隔离。它们都是充当一层代理,这里就不一一介绍了。作者:vivo官网商城开发组-孙道明