内容简介:背景随着二手车市场的不断扩增,58二手车作为全网的重要入口之一,经历着越来越多的流量考验,业务需求的快速迭代,需要我们在列表页承载更多类型的信息,这使我们面临着更加严峻的性能考验。情景描述
导语
并发编程,在提高我们程序性能的同时,也会带来种种不可未知的问题,如何优雅地在高并发场景下解决实际业务问题呢?本文从五八二手车真实业务出发,逐步刨析,希望能给读者一些启示。
背景
随着二手车市场的不断扩增,58二手车作为全网的重要入口之一,经历着越来越多的流量考验,业务需求的快速迭代,需要我们在列表页承载更多类型的信息,这使我们面临着更加严峻的性能考验。
获取数据列表的服务接口是我们这块耗时最为严重的接口,曾一度达到200ms+,如何优化此接口的耗时是我们要解决的核心问题。
情景描述
1、数据查询
二手车列表页最多包含近10种信息类型需要查询,其中这些信息可以分为两大类,一类是无依赖的信息,一类是信息间存在补足策略的有依赖信息列表。这里的依赖指的是是否需要查询count接口,来确定下面的信息是否需要再次查询。每种信息都是从第三方服务中拉取列表,不同的服务方接口响应也是差别很大的。
2、数据转换
拉取信息列表后,要经过合并 排序 去重,然后在对每种信息进行打标转换,以便输出统一的实体模型,转换期间也有可能会调用第三方服务,来处理不同的实体字段。
问题分析
通过阿里的arthas 工具可以监控到列表服务接口getListResult 中两个比较耗时的操作:数据查询 search (150ms)和数据转换 transfer(50ms),接下来我们对此进行分析。
1、数据查询
针对已有的业务逻辑,我们线上优化前采用的是比较便于理解的方法,来处理这块并发,把有依赖的信息列表和无依赖的信息列表区分开,创建不同的线程去执行。下面是执行流程,以及我们监控的每一块的耗时情况。
上面A、B、C三种类型的信息没有任何依赖,我们采用三个线程并发执行,由于它们只需要获取Result(R)列表数据,且取决于耗时最长的执行子线程,平均耗时40ms左右。
下面的七种信息存在着依赖,首先获取D信息的Result(R)列表数据(此调用接口也会同时返回总量Count),根据其总量和页长可以计算该信息最多可以展示的页数,最后一页不足时,由E信息补足到指定的页长,这期间的页码需要获取E信息的Count(C)列表总量接口,以便用于分页的计算,其他的信息依次和E信息一样进行补足策略的处理。由于存在依赖,这里是一个子线程循环执行多种信息,基于页长的考虑,大部分的列表数据查询Result(R)会执行到 F信息,平均耗时取决于前三种信息的Result(R)的耗时和之后信息Count(C)的耗时:(40+10+60)+(5+15+20) = 150ms左右。
上下两类信息查询之间是并发执行的,所以数据查询执行耗时大概150ms左右。
2、数据转换
对于合并去重后的信息列表, 由于实体转换时存在对绝对位置的要求,线上优化前这一块没有进行并发处理。页面平均转换50条数据,由于每条数据转换时可能需要调用第三方的服务,平均每条转换需要1ms左右,所以数据转换执行耗时大概50ms左右。
优化方案
1、优化一:数据查询
重新设计信息间的补足策略,将数据的 count 和 result 查询彻底分开,执行流程如下。
由上图可知,将count(C)和result(R)完全分开,Count中多个子线程并发执行,耗时取决于最高的是50ms,然后执行统一的补足策略,来确定 result 查询的集合,最后多个子线程并发执行,耗时取决于最高的是60ms,平均耗时:50+60=110ms。
此优化应用于线上后, 接口耗时有40ms左右的性能提升。
2、优化二:数据转换
数据转换这块重新考虑并发处理,借助中间实体对象,保留合并排序去重后的绝对位置。将信息列表进行集合拆分,并发转换后再次放入原来排序后的位置。
拆分集合的方式如下:
在现有线程池资源的范围内,灵活合理的设置单个线程执行的子任务数 handlerCount 和允许创建的最大线程数MaxThreadSize。鉴于我们的系统场景,我们设置了每个线程执行5条信息转换,平均耗时5ms左右。
此优化应用于线上后,接口耗时又有40ms左右的性能提升。
3、 优化三:线程池调参:
涉及线程池,势必要考虑线程池参数问题,要想正确合理的设置线程池的大小,必须综合考虑计算环境、资源预算和任务的特性。《Java并发编程实战》的作者Doug Lea给我们介绍了关于计算密集型和IO密集型线程池设置的基本思路,如下:
然后实际生产环境中,一个系统中既存在计算型任务,又存在IO型任务,此时我们该如何合理的设置呢?
针对于我们的应用场景,线程池中存在三个并发场景,如下图所示:
场景描述:单台机器的 QPS大概15~20,一个请求进入需要执行count、result、transfer 三次并发调度,每次都开启了10个线程,任务数计算为:(15~20) * (10+10+10) ~500个任务,考虑流量翻倍情况,任务数最大约1000,再计算每个任务的平均耗时:(40+60+10)/ 3 = 40ms 即为0.04秒,系统预设每个任务最大容忍耗时300ms,即为0.3s(任务执行await超时时间)。
线程计算:单线程时,每个任务平均耗时0.04s,则每秒可以执行(1/0.04)= 25个任务,500个任务则需要(500/25)= 20秒,则20个线程可一秒内执行500个任务,
即corePoolSize可设为20;1秒内执行500个任务,任务最大容忍耗时0.3s,则 workQueue 可设为 (500 * 0.3)= 150个任务;此时每秒可以执行650个任务不会有抛弃,当任务达到1000个时,剩下的(1000-650)= 350 个任务,需要重新创建线程,同上述计算一样需要创建(350/25)= 14个线程,即 maxPoolSize 为(20+14)= 34 个线程。
基于以上计算方式,总结公式如下:
针对本次优化,我们上线前对单机50 QPS 进行 TcpCopy 和 压测处理,数据如下:
最大线程数 |
CPU负载 |
队列情况 |
耗时情况 |
300 |
88% |
无等待 |
183ms |
180 |
54% |
无等待 |
128ms |
50 |
28% |
无等待 |
125ms |
34 |
21% |
无等待 |
120ms |
24 |
18% |
有等待 |
135ms |
12 |
18% |
有等待 |
148ms |
多次调整线程数,耗时情况如下图所示:
可见,线程数过大会导致CPU负载高,影响耗时情况,过小会导致队列存在等待,也会使耗时增高,针对不同的业务场景,合理的计算线程数 + 实验 Copy压测,才是最佳的实践选择。
总结规划
列表服务并发优化后,提升了80ms以上,性能提升的同时,也带来了开发调试的困难,多场景的并发引入,线程池的参设对于合理利用资源显的尤为重要,后续我们考虑动态设置线程池参数,以便最优化的利用系统资源,提高整体机器的利用率。
参考文献
1. Arthas:阿里 Java 诊断工具,https://alibaba.github.io/arthas/
2. 《Java并发编程实战》作者:Doug Lea,童云兰(译)
作者简介
王杰,58同城ABG资深研发工程师,负责58二手车主站业务架构设计,包括ToC业务架构、业务数据服务、业务监控平台。
杨俊南,58同城ABG资深开发工程师,负责58二手车列表详情系统、二手车用户画像设计与开发。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 我的二手车究竟值多少钱?
- 数据智能在二手车业务场景中的探索与沉淀-Part2业务标签的挖掘
- Java并发系列—并发编程基础
- [Java并发-17-并发设计模式] Immutability模式:如何利用不变性解决并发问题?
- JAVA并发编程之并发模拟工具
- Java并发系列—并发编程的挑战
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Mission Python
Sean McManus / No Starch Press / 2018-9-18 / GBP 24.99
Launch into coding with Mission Python, a space-themed guide to building a complete computer game in Python. You'll learn programming fundamentals like loops, strings, and lists as you build Escape!, ......一起来看看 《Mission Python》 这本书的介绍吧!