高并发分布式计算-生产实践
Posted jacobus
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了高并发分布式计算-生产实践相关的知识,希望对你有一定的参考价值。
简介
记录一个项目的技术实现,主要谈这个项目的大请求的高并发处理这一块。这个项目最终通过多种技术组合,达到削峰填谷地秒级分布式计算大请求的能力,且各服务/接口间的熔断和自动恢复避免了某1个挂起的服务使得其他服务挂起的单点故障。这个项目申请了多份专利
背景
其实项目整体的技术选型本身是一次很有意思的经历,里面有太多的故事(技术访谈、技术调研、多套方案的POC、小组讨论、确认技术选型、异地出差做项目调研、跨部门确认技术选型、向各技术部门领导通报技术可行性研究、协助兄弟部门技术提升等),最有意思的是兄弟部门架构师(已离职)的一席话:这个项目已经好多波人找我调研了,其实有方案一直不能落地,希望你们能搞定!当时,我的头顶有几只乌鸦飞过……。几百人IT团队的架构师都推不动,我们能量有那么大吗。最后生产实践证明:相比之下,我们的方案更加轻快、更加流程化、更加既见既所得、且一处配置处处一致
为了控制文章的篇幅,此文只说明大请求高并发的技术实现方案
业务场景
- 在1次业务请求里:需要处理1~10+个对象。这1~10+个对象在被处理时,会共使用同1张300~700K的图片
- 请求里的1个对象被处理的步骤:
1)获取对应的html模板,每分模板大概70K左右
2)Html模板的velocity引擎参数替换
3)Html转PDF
4)向第三方上传PDF和图片,作电子签名
5)将签名的PDF上传到云存储平台,得到fileid
6)将签名的fileid + 由业务同事配置的印章信息,调用第三方接口,作电子印章 - 业务同事会在页面上新增、修改Html模板。换言之,模板不能是项目的资源文件
技术说明
- 如果需要使用MQ,需要使用集团基于RocketMq封装的MQ,消息体最大支持128K
- 业务高峰时300+tps。后面测试同事反馈:压测时他们更狠,用了1w+的瞬时并发压测4台机器,依然ok
- 1次业务请求需在5秒内处理完毕并返回结果。生产数据表明:1个对象被处理的总时间基本落在300毫秒~1.5秒之间,而由于是分布式计算,因此1次请求的总耗时≈1个对象的总耗时(木桶效应:请求中最耗时的那1个对象)
- 当时这个项目所属的系统没有跑在微服务,而是跑在云主机上
换言之,这个系统的集群会run其他的业务功能
再换言之,不能因为这个高并发的项目让集群里的机器瘫痪,影响到其它同样重要的业务逻辑的运行
方案
服务 = 请求持久化 + 分布式计算 + MQ异步通知
实现
请求持久化
示意图
说明
- 流程说明(为避免并发因素,下文的执行顺序不能调换):
- 对请求A里的对象A、B做必要性的参数检查。检验失败立马返回错误信息
- 在分布式环境下生成唯一的请求编号reqId。参见之前写的一篇文章: 分布式UUID的生成
- 将当前请求信息(主要是reqId、请求的初始处理状态)写入Oracle
- 在Redis中记录基于reqId的分布式锁,控制后面多实例间的并发。hset-->reqId(key):1(value)
- 分别给对象A、B定义3个核心属性:
1)当前对象所属请求的reqId
2)对象编号。生成编号的方法和生成reqId的一致
3)对象持久化的时间(当前时间) - 通过上一步,得到reqId和所有对象编号的对应关系,将对应关系持久化到Redis中。hset-->reqId(key):List<对象编号>(value)
- 将对象A、B(含图片)持久化到Redis队列中,或则数据库中。说明:
- 配置Redis的Queue深度为2000(可动态调整)
- 若发现队列满了,则将当前对象持久化到Oracle中作二级缓存,并在Redis中累加二级缓存的对象数: redis.incrby("SL_CACHE",1);
- 若Redis记录的二级缓存对象数大于0:后续请求的对象直接写Oracle,确保Redis队列和Oracle二级缓存队列保持顺序的一致性
- 若发现队列未满且二级缓存对象数不大于0,则将当前对象持久化到Redis的Queue中
- 返回调用方reqId
其他说明:
Redis读的速度是11万次/s,写的速度是8.1万次/s。每1次写的速度小于0.0125毫秒,如果把应用层和Redis之间的网络消耗算在内,最大应该是1个毫秒级。如果1次请求有10个对象的持久化,最大也就是10个毫秒左右。加上Oracle的1次请求(不是请求里的对象)的持久化时间,整个持久化时间大概20~50ms左右,调用方可以快速得到reqId
分布式计算、MQ通知
示意图
说明
- 流程说明:
- 集群里的每台实例开启4个无限循环的线程。下面的步骤都是在1个线程中执行
- 当前线程判断是否需要将二级缓存的对象补充到Redis队列(需控制并发,避免重复载入二级缓存)。判断依据:
1) 队列深度小于50%
2) 二级缓存对象数大于0 - 从Redis队列中获取的对象若为空,睡300毫秒后,再继续从第2步开始
- 和当前时间比较,若对象持久化的时间已经超过120秒了,则立马跳到第2步开始
- 处理对象(执行业务逻辑),这一步涉及熔断。在标准web应用中通过AOP实现了一版,切换到微服务后,利用Hystrix组件实现了熔断。这两版虽然是不同的实现,但是控制逻辑一致,示意图中对熔断逻辑做了说明
- 将当前对象的处理结果(Map),回写到Redis中。对象编号(key):处理结果(value)
- 通过当前对象绑定的reqId,从Redis中取得reqId和所有对象编号的对应关系,在Redis中查询每个对象编号的处理结果(Map)。若某个对象编号没有处理结果,表示该对象处理中或则没有处理,则跳到第2步开始
- 当前请求的所有对象处理完成后,就可以将所有对象的处理结果(Map)封装成MQ,异步通知调用方。同1个请求的不同对象,可能会被不同实例的线程处理,因此只能让一个线程获得上文提到的基于reqId的分布式锁来发送MQ消息,方法:若某1个线程执行redis.del(reqId)的返回值大于0,则获取到分布式锁
FAQ
汇总一下之前小伙伴的疑问:
分别对1次业务请求的n个对象,发起n次请求?
不行。1次请求拆成n次Http请求调用,每次调用又带着300~700K的图片的话,在高并发下,千兆网卡可能吃不消(何况很多实例都是虚拟化的,多台实例共用同1台物理机器,多台实例共用同1张网卡,更会加重这种影响)。我的猜想没有错:有一次UIOC事件(其他兄弟的另1个项目)就是因为并发过高,网卡挂起,网卡间断性的发网络请求
用MQ的方式通知集群处理业务请求?
不行。 集团的1条MQ消息,容量最大是128K,而请求中的图片就有300~600K
同步接口来处理业务请求?
不行。1个对象的处理时间若是700ms,1个请求的10个对象的处理时间是7秒,超出了1个请求2秒得到结果的要求
同步接口 + ExecutorService线程池并行处理业务请求里的各个对象?
不行。主要是两个方面:
1)由于涉及PDF流的运算,会消耗大量内存,为了不影响其他的业务逻辑的运行,必须限定线程池的个数和队列深度。若不限定,那么大的图片存储在内存中很快会OOM
2)在高并发下,线程数和队列深度一旦限定,有些请求因达到线程池的限制会被直接拒绝。同时,同1个请求10+左右的对象,可能会分m批运行(m=10/线程数),若每批运行时间是700ms,会有超过整个请求的处理时间在2秒内的限制的可能这个项目为什么一定要用异步接口呢?
作为服务方,尽量不成为公司整个业务流程的性能瓶颈节点。 调用方与服务方的网络连接越快结束越好,避免服务方运算速度变慢(比如YGC、OGC、FGC、系统中其他耗性能的功能running中等),而导致调用方HTTP连接的等待,如果此时调用方没有配置超时时间,会发生灾难性的贯穿性的雪崩效应。同时鉴于单个对象的处理链条长,PDF流运算较耗性能,如果几千个对象同时同步的在服务方处理,服务方必然会资源耗尽。所以这个项目需要通过异步队列的形式,削峰填谷
不考虑用MongoDB来替代Oracle的写操作?
有考虑过,不过这个项目所属的系统没有MongoDB,需要预算审批。如果替代过后的好处:
1)减轻Oracle的写压力
2)由于磁盘以bson存储,易于扩展监控类字段
3)通过MongoDB优秀的写性能,可以减少单个对象的处理时间
由于1次请求整体响应时间在1秒左右,已经达到5秒内的要求,性能过得去,所以没有申请这笔预算为什么不直接将对象持久化到Redis中,而要用数据库做二级缓存呢?
主要是担心海量并发时,Redis的队列深度不控制,会将Redis撑爆。毕竟为了读写效率,每个对象都会存储300~700K的图片。如果1个请求有10个对象,则会存7M,48G的Redis总共存储7021个请求,若按照300tps且来不及消费Redis队列的话,最快23秒就会将Redis爆掉。因此,队列深度是2000的话,最大占用Redis容量是1.3G=2000*700/1024/1024
为什么每台实例只开启4个线程?
当时测试环境压测的主要的环境指标:申请和生产环境同样的CPU和内存,每次压测的处理对象一样。线程数从1配置到10,主要考核2个核心指标:每秒处理的对象数、每个对象处理的耗时。不难看出这2个指标是互为倒数关系,数学上分别是直线和双曲线,他们一定在第一象限有个交点,这个交点就是4。4个线程时,这两个指标是最好的。其实也不难理解,在内存足够的情况下,影响线程性能的就是CPU,而服务器的CPU是四核的,每个核处理1个用户线程是最佳的
为什么Redis队列里的对象超过120秒后丢弃?二级缓存只取60秒内的数据?
1)文中提到的各种阈值,它们之间是有联系的,是通过计算得到的。比如:这里的60、120:队列深度(2000)低于50%时,取1000个对象补充;而每个对象120秒就会被丢弃,所以只用取60秒内的数据;当然这也和集群中的实例数有关。因每家公司集群环境不一样,阈值配置的具体值不深入讨论
2) 对象之所以要有过期时间而被丢弃,主要是为了当数据积压发生时(队列生产者的生产的速度比消费者消费的速度快),避免新的请求迟迟得不到消费
3) 生产事件:截止到2019.06,这种情况生产环境一共出现3次,第1次出现这个情况时没有这个特性,运维同事又迟迟没有Redis的操作权限,也不敢flush数据导致加重了业务影响----集群一直消费很早之前(半小时、1个小时之前)的请求。其实这个超时丢弃的特性早已报备并计划上线,只不过计划赶不上变化。第2、3次出现这个情况时,因为有了这个特性,集群进行了自我修复,基本无感知。至于这3次情况产生的根源性原因各不相同,都不是因我们的服务自身而起,虽不是我们的锅,但毕竟我们可以做到更好,减轻别人黑锅对我们的影响,你说呢?丢弃的对象后面会补偿处理吗?
不会,业务上不需要。调用我们服务的属于App交互类的请求,接口上保持幂等性即可
以上是关于高并发分布式计算-生产实践的主要内容,如果未能解决你的问题,请参考以下文章