网页客服,欢迎咨询

线程复用助力之家出行平台运力提升的应用实践

2025-01-06 22:20:02 87
  • 收藏
  • 管理
    总篇第143篇 2022年第18篇背景之家出行平台起源2020年疫情开始,为响应国家号召减少公共交通,避免人多聚集,虽出行人数减少,但网约车用户不降反增;为了更好的保障用户安心到店,同时打通用户线上询价与线下到店看车行为关联,更好的服务之家用户,之家出行平台诉求迫在眉睫;系统构建既然需求已经很明确,接下来就使围绕着怎么构建之家出行,经过调查发现网约车细分如下: (网约车市场细分,摘自:2022年网约车行业研究报告)经过分析,以下两种方式更适合之家出行平台要求,但依然各有千秋:1)网约车 S2B2C优点:服务、数据安全有保证;缺点:需网约车运营资质,成本高,运营难度大; 2)聚合平台 S2B2C优点:依赖流量优势,低成本,易运营;缺点:存在数据安全风险,打车成功率以及打车体验受限; 之家构建出行平台初衷是为了保障用户安心到店,对抢占目前网约车市场并无兴趣,在保证数据安全,用户用车到店安全的前提下,用车成本是必须考虑的因素;相比网约车的高成本高运营,聚合平台的优势相对突出,无需关心具体运力公司的成本以及运营,只需使用他们的服务,同时之家的用车流量也是各运力公司拓展渠道的香饽饽;出行平台系统要求100+主要城市全部运力覆盖,叫车成功率在60%以上;保证之家业务覆盖到的城市均可以让用户使用出行平台打车到店,平均叫车成功率达到60%以上,这样才能保证用户的叫车体验,太低会使用户体验降低,失去信任,提升叫车成功率是用车体验重要一步;要实现该目标需要:(一)统一运力接入流程,配置优化管理,快速接入;① 分析接入流程: ② 针对运力公司和聚合平台接入特点,分别针对运力公司以及聚合平台进行流程细化,寻找共同点以及差异点,针对共同点进行功能设计,抽取,减少重复造轮子,针对差异点进行功能定制;通过模板方法设计模式、适配器模式、装饰器模式、工厂模式、单例模式、策略模式等设计模式提升运力接入功能抽象,功能统一同时数据隔离,保证服务安全可靠;(二)无论同时选择多少个运力叫车,都不能影响预估以及下单性能;经过上一步我们使用了统一的接入流程,虽然能够通过功能抽象、抽取,实现运力公司以及聚合平台两种快速接入。但用户预估时每选择一个运力,对应的就会多一套运力接口调用,以下用户预估时分析: ① 运力下单前,需要先对用车(时间、车型、起点、终点等)行程进行预估,使用获取到的有效预估信息,才能调用运力下单; ② 预估时需要调用C端选择的每个运力的预估接口,由于木桶效应,最长耗时的运力拉长整个预估接口响应时长; ③ 随着运力接入数量的增多,每次预估调用运力预估接口也成倍增加,没有上限,对服务器资源要求高;分析之后有解决办法如下运力预估返回数据是存在时效(基本在10分钟左右),可以对预估数据进行缓存,针对同一个用户,服务类型、用车类型以及上下车地点等信息不变的情况下可以对运力返回预估数据进行缓存,这样可以减少用户频繁预估时无效调用,提升性能,但解决同时请求多套运力接口并保证总体RT的问题主要解决办法如下:一 线程复用,减少创建、销毁、调度线程的开销,譬如JDK自带的J.U.C包线程池就是一个不错的选择;线程池解决方案线程池解决的核心问题就是资源管理问题。在并发环境下,系统不能够确定在任意时刻中,有多少任务需要执行,有多少资源需要投入,通过对资源的统一管理(池化)实现风险最小的情况下收益最大;线程池在内部实际上构建了一个生产者消费者模型,将线程和任务两者解耦,并不直接关联,从而良好的缓冲任务,复用线程。线程池的运行主要分成两部分:任务管理、线程管理。任务管理部分充当生产者的角色,当任务提交后,线程池会判断该任 务后续的流转:(1)直接申请线程执行该任务;(2)缓冲到队列中等待线程执行;(3) 拒绝该任务。线程管理部分是消费者,它们被统一维护在线程池内,根据任务请求进 行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取 不到任务的时候,线程就会被回收。 JDK1.8的线程池ThreadPoolExecutor实现的顶层接口是Executor,接口Executor将任务提交和任务执行进行解耦。用户无需关注如何创建线程,如何调度线程来执行任务,用户只需提供 Runnable 对象,将任务的运行逻辑提交 到执行器 (Executor) 中,由 Executor 框架完成线程的调配和任务的执行部分。ExecutorService 接口增加了一些能力:(1)扩充执行任务的能力,补充可以为一 个或一批异步任务生成 Future 的方法;(2)提供了管控线程池的方法,比如停止线 程池的运行。AbstractExecutorService 则是上层的抽象类,将执行任务的流程 串联了起来,保证下层的实现只需关注一个执行任务的方法即可。最下层的实现类 ThreadPoolExecutor 实现最复杂的运行部分,ThreadPoolExecutor 将会一方面 维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务。线程池Worker实现的是AQS(AbstractQueuedSynchronizer),主要是为了Worker在空闲的时候可以被响应中断,而在执行任务的时候不能中断;具体实现为,中断之前首先尝试获取锁,tryLock,worke具有不可重入特性,执行任务中的worker已经获取得了lock,tryLock失败,无法再中断work对象的thread;详细关于AQS的可以查询源码这里不再展开;jdk1.8对于Worker类的官方注释?/**?*?Class?Worker?mainly?maintains?interrupt?control?state?for?*?threads?running?tasks,?along?with?other?minor?bookkeeping.?*?This?class?opportunistically?extends?AbstractQueuedSynchronizer?*?to?simplify?acquiring?and?releasing?a?lock?surrounding?each?*?task?execution.This?protects?against?interrupts?that?are?*?intended?to?wake?up?a?worker?thread?waiting?for?a?task?from?*?instead?interrupting?a?task?being?run.We?implement?a?simple?*?non-reentrant?mutual?exclusion?lock?rather?than?use?*?ReentrantLock?because?we?do?not?want?worker?tasks?to?be?able?to?*?reacquire?the?lock?when?they?invoke?pool?control?methods?like?*?setCorePoolSize.Additionally,?to?suppress?interrupts?until?*?the?thread?actually?starts?running?tasks,?we?initialize?lock?*?state?to?a?negative?value,?and?clear?it?upon?start?(in?*?runWorker).?*/ 线程池配置由于本身业务为IO密集型的,并且使用之家云容器Docker配置:4核心8G内存;详细配置:coreSize:10,maxSize:20,rejectedExecutionHandler:ThreadPoolExecutor.CallerRunsPolicy()摘自:Java Concurrency In Practice具体实现/**?价格预估伪代码;**/public?JSONArray?estimatePrice(EstimateQuery?estimateQuery)?{/*?..?*//*?查询可用运力?*/List?channelIdList?=?channelMapper.getAllChannelIds();/**?构建多个异步调用任务;**/CompletableFuture[]?cfs?=?channelIdList.stream().map(i->?CompletableFuture.supplyAsync(()->?request(i),?taskExecutor?).whenComplete((u,e)?->?{if(null?!=?u){array.add(u);}})).toArray(CompletableFuture[]::new);/**?开始执行任务,阻塞主线程,等待各任务全部处理完成;?**/CompletableFuture.allOf(cfs).join();/**?后续处理?..?*/}/**?*?调用运力对应的预估接口获取预估数据;?*/private?JSONObject?request(int?channeId){switch?(channelId){case?Constant.SQ_CHANNEL_ID:/**?首汽预估;*/return?sqService.getEstimatePrice(estimateQuery);case?xx:/*?其他预估;省略.. */}return?null;} 通过设置HTTP请求的connectTime,requestTime控制接口调用时长,以及使用Alibaba Sentinel对各运力封装接口进行熔断处理,从而保证接口整体响应时长;/**?*通过?@SentinelResource?为运力的预估接口增加?熔断机制,一旦接口达到熔断条件(访问超时占比超过设定阈值)则不在调用运力http接口,以提升性能;?*通过 CONNECT_TIMEOUT、REQUEST_TIMEOUT 设置http接口请求的超时时间,接口超时直接放弃结果,以提升接口整体性能;?*//**sqService**/@SentinelResource(value?=?"sqEstimate",?blockHandler?=?"sqEstimateBlockHandler",?fallback?=?"orderFallback")public?JSONObject?getEstimatePrice(EstimateQuery?estimateQuery){?/**?调用接口数据请求转换?**/?Map?requestMap?=?this.convertData(estimateQuery);?/**?调用接口请求数据,通过?CONNECT_TIMEOUT,REQUEST_TIMEOUT限制请求时间?**/?String?result?=?HttpUtils.post(this.REQUEST_URL,CONNECT_TIMEOUT,REQUEST_TIMEOUT);?/**?对结果数据进行封装处理?**/?return?this.convertResult(result);}用户在下单时,会尽可能的勾选更多运力以及车型,这样能够保证下单的成功率。但每次价格预估需要同步调用的运力接口是不同,可以对齐每次预估调用时申请线程的数量,复用已有线程,但线程池的资源是有限的,仍然出现线程池线程全部占用,等待的情况;设置每个预估请求的运力数为channelCount,预估最大允许的线程数量为maxThread,一个请求周期为requestPeriod; ① 预估请求运力数channelCount maxThread,则对运力按照设置的优先级进行分组排序,对运力分成maxThread个组,每个组内的运力按照优先级再次划分梯队,第一梯队中的运力调用优先级最高,首选获取线程并且执行,当一个梯队中的调用超时或者数据返回失败之后线程并不会释放,会执行当前分组中的第二个梯队中的运力调用,对数据进行补全,同时也能实现调用的优先级;每个预估都会创建一个数组,数组存放的是单链表结构运力调用任务,任务节点数据结构://数据结构public?class?ChannelTaskNode{private?boolean?header;?//头节点private?int?priority;?//优先级private?int?groupIndex;?//groupprivate?Long?channelId;?//?运力IDprivate?ChannelTaskNode?next;?//?下一个节点}对任务的初始化以及分组处理如下://每次预估对应的任务分组ChannelTaskNode[]?channelTaskArray?=?new?ChannelTaskNode[maxThread];//按照优先级获取对应的运力列表;int[]?channelArray?=?queryAllOpenChannelSortedByPriority();//初始化for(int?i=0;i?service.estimatePrice(channelId));}else{tempFuture?=?tempFuture.handle((obj,?error)?->?{if(null?!=?error?){return?service.estimatePrice(channelId);}return?obj;});}temp?=?node.next;}allTasks[node.groupIndex]?=?tempFuture;}/**开始调用?**/CompletableFuture.allOf(allTasks).join();/**?对结果进行处理?**///省略 运行结果对比(横轴为预估运力数量,纵轴为响应时间毫秒); 优化多线程方案,虽然会随着预估时运力增多带来总体耗时增加,但在总RT控制的前提下,又能保证线程池资源的合理了分配,又能使得各运力能够按照概率的配置被调用到,总体效果还是显而易见;总结本文主要讲解了,出行平台的来源,系统构建时的思考,以及在设计时遇到的问题;前期主要是以接入更多的运力,来保证系统的覆盖以及用户用车体验,由于网约车系统的规范性每个运力的接入基本都是设计到数据适配、接口对接、订单系统集成、费用结算集成等节点;为了降低开发成本,快速接入,提供了一套接入的统一的接入流程,并对接入做统一处理,这样单运力接入工作缩短一倍以上;由于出行平台为聚合型平台,用车功能基本都是需要调用各接入的运力公司的接口,预估接口又是整个叫车环节中前置的重要入口,在对接口性能以及用车体验双重考验之下,我们运用了线程池的方式在预估时对多个运力做并发调用,但由于预估时选择运力的不确定性,为了保证每个预估调用的公平,预估多个运力时,按照运力进行分组,复用当前从线程池中获取的线程,同时也实现运力的优先级;为了保证整个预估接口的RT,每个运力调用时均存在connectTimeOut以及requestTimeOut,同时为了避免接口性能问题对整个接口影响,还引入了Alibaba Sentinel对各运力封装接口进行熔断处理,从而保证接口整体响应时长;经过以上处理,价格预估阶段不再受限与接入运力的多少,不再成为整个用车环节中的瓶颈,但HTTP请求的超时限制并不严格准确,后续会使用时间轮算法对该部分功能进行持续完善;相关参考2022年网约车行业研究报告Java Concurrency In PracticeJavaDoc Guide作者简介汽车之家张武杰2020年加入汽车之家,目前任职于主机厂事业部-技术部-广告技术及系统团队,负责之家出行平台的架构设计以及研发工作,致力于为之家用户安心到店提供安全,稳定,便捷的出行平台服务阅读更多:一劳永逸 搞定Flex布局(子元素篇)Go语言项目实践及新特性解读汽车之家基于 Flink 的实时计算平台 3.0 建设实践 线程复用助力之家出行平台运力提升的应用实践
    上一页:细粒度分类算法在汽车之家车系识别业务中的应用 下一页:精简CSS代码,提升前端性能实战优化技巧
    全部评论(0)