关于架构设计:微电平台高并发实战经验奇葩问题解决之旅

5次阅读

共计 5561 个字符,预计需要花费 14 分钟才能阅读完成。

作者:京东科技 孙亮

微电平台

微电平台是集电销、企业微信等于一体的综合智能 SCRM SAAS 化零碎,涵盖多渠道治理、全客户生命周期治理、私域营销经营等次要性能,目前曾经有 60+ 京东各业务线入驻,专一于为业务提供职场外包式的一站式客户治理及一体化私域经营服务。

导读

本文介绍电销零碎在遇到【客户名单离线打标】问题时,从排查、重复验证到最终解决问题并额定 晋升 50% 吞吐 的过程,适宜所有服务端 研发同学,提供生产环遇到一些简单问题时排查思路及解决方案,正确应用京东外部例如 sgm、jmq、jsf 等工具抓到问题根因并彻底解决,用技术为业务倒退保驾护航~

上面开始介绍电销零碎理论生产环境遇到的离线回绝营销打标流程吞吐问题。

案例背景

1、概述

每日凌晨 1~8 点会应用 80 台机器为电销零碎上亿客户名单进行回绝营销打标,均匀 95 万名单 / 分钟,回绝营销 jsf 服务总 tps 约 2 万,tp99 在 100~110ms,若夜间没有实现标记加工操作,会导致白天职场无奈失常作业,并且存在客户骚扰隐患、升高职场经营效率的问题,因内部接口 依赖数量较多 打标程序只能凌晨启动和完结。

2、复杂度

面向业务提供 千人千面 的配置性能,底层基于规定引擎设计实现,调用链路中蕴含泛滥内部接口,例如金融刷单标记、风控标记、人群画像标记、商城风控标记、商城实名标记等,蕴含的 维度多、复杂度较高

3、问题

2023 年 2 月 24 日早上通过监控发现回绝营销打标没有执行实现,体现为 jmq 生产端 tp99 过高、进而升高了打标程序吞吐,通过 长期扩容、下掉“问题机器(上帝视角:其实是程序导致的问题机器)进步吞吐,减速实现回绝营销打标。

但,为什么会频繁有机器问题?为什么机器有问题会升高 40% 吞吐?后续如何防止此类情况?

带着上述问题,上面开启问题根因定位及解决之旅~

抓出幕后黑手

1、为什么几台机器出问题就会导致吞吐急剧下降?

如上图所示,每有一条音讯生产报错(在本案例中是打到“问题机器”上),会本地尝试 sleep 并从新生产拉下来的所有音讯(本案例中 jmq 的 batchSize=10),即每次报错产生的总耗时至多会 减少一千毫秒 ,一共 80 台机器,jsf 应用 默认负载平衡 算法,服务申请打到 4 台问题机器的概率是 5%,jmq 一次拉下来 10 条音讯,只有有一条音讯命中“问题机器”就会极大升高吞吐。

综上所述,大量机器有问题吞吐就会急剧升高的起因是 jsf 随机负载平衡算法下每个实例的命中率雷同以及报错后 jmq consumer 重试时默认休眠 1 秒的机制导致。

解决:当然 consumer 不报错是齐全能够躲避问题,那如果保障不了不报错,能够通过:

1)批改 jmq 的重试次数、重试延迟时间来尽可能的缩小影响

<jmq:consumer id="cusAttributionMarkConsumer" transport="jmqTransport"> 
<jmq:listener topic="${jmq.topic}" listener="jmqListener" retryDelay="10" maxRetryDelay="20" maxRetrys="1"/> 
</jmq:consumer>

2)批改 jsf 负载平衡算法

配置样例:

<jsf:consumer loadbalance="shortestresponse"/>

原理图:

上图中的 consumer 图是从 jsf wiki 里摘取,下面的红字是看 jsf 代码提取的要害信息,总而言之就是:默认的 random 是齐全随机算法,最快响应工夫是基于服务申请体现进行负载平衡,所以应用最快响应算法能够很大水平上躲避此类问题,相似于熔断的作用(本次解决过程中也应用了 jsf 的实例熔断、预热能力,具体看 jsf wiki,在此不过多介绍)。

2、如何断定是实例问题、找出有问题的实例 ip?

通过监控察看,耗时高的景象只存在于 4 台机器上,第一反馈的确是认为机器问题,但联合之前(1 月份有过相似景象)的状况,感觉此事必有蹊跷。

下图是第一反馈认为是机器问题的日志(对应 sgm 监控这台机器耗时也是间断高),下掉此类机器的确能够 长期解决 问题:

综上所述,当 时间段内耗时高或失败的都是某个 ip,此时能够断定该 ip 对应的实例有问题(例如网络、硬件等),如果是大量 ip 存在相似景象,断定不是机器自身的问题,本案例波及到的问题不是机器自身问题而是程序导致的景象,持续往下看揭晓答案。

3、是什么导致机器频繁假死、成为故障机器?

通过上述剖析能够得悉,问题机器报错为jsf 线程池打满,机器出问题期间tps 简直为 0 ,期间有申请过去就会报 jsf 线程池(非业务线程池)打满,此时有两种可能,一是 jsf 线程池有问题,二是 jsf 线程池的线程始终被占用着,抱着信赖中间件的思路,抉择可能性二持续排查。

通过行云进行如下操作:

1)dump 内存对象

无显著问题,内存占用也不大,合乎监控上的大量 gc 景象,持续排查堆栈

2)jstack 堆栈

从此来看与问题机器表象统一了,根本得出结论:所有jsf 线程都在 waiting,所以有流量进来就会报 jsf 线程池满谬误,并且与机器 cpu、内存都很低景象相符,持续看具体栈信息,抽取两个比拟有代表的 jsf 线程。

线程编号 JSF-BZ-22000-92-T-200:

stackTrace:
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000007280021f8> (a java.util.concurrent.FutureTask)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.FutureTask.awaitDone(FutureTask.java:429)
at java.util.concurrent.FutureTask.get(FutureTask.java:191)
at com.jd.jr.scg.service.common.crowd.UserCrowdHitResult.isHit(UserCrowdHitResult.java:36)
at com.jd.jr.scg.service.impl.BlacklistTempNewServiceImpl.callTimes(BlacklistTempNewServiceImpl.java:409)
at com.jd.jr.scg.service.impl.BlacklistTempNewServiceImpl.hit(BlacklistTempNewServiceImpl.java:168)

线程编号 JSF-BZ-22000-92-T-199:

stackTrace:
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000007286c9a68> (a java.util.concurrent.FutureTask)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.FutureTask.awaitDone(FutureTask.java:429)
at java.util.concurrent.FutureTask.get(FutureTask.java:191)
at com.jd.jr.scg.service.biz.BlacklistBiz.isBlacklist(BlacklistBiz.java:337)
````

** 推断 **:线程编号 JSF-BZ-22000-92-T-200 在 UserCrowdHitResult 的 36 行 ** 期待 **,线程编号 JSF-BZ-22000-92-T-199 在 BlacklistBiz 的 337 行 ** 期待 **,查到这,根本能推断出问题起因是线程期待,** 造成问题的相似代码场景 ** 是 1)main 线程让线程池 A 去执行一个工作 X;2)工作 X 中又让同一个线程池去执行另一个工作,当第二步获取不到线程时就会始终等,而后第一步又会始终等第二步执行实现,就是造成线程相互期待的假死景象。** 小结 **:查到这根本能够确认问题,但因代码保护人到职以及程序盘根错节,此时为验证论断先批改业务线程池 A 线程数:50->200(此处是为了验证没有线程期待景象时的吞吐体现),再进行验证,论断是 **tps 会有小范畴抖动 **,但不会呈现 tps 到 0 或是大幅升高的景象。单机 tps300~500,流量失常了,即 ** 未产生线程期待问题时能够失常提供服务 **,如图:![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/261c1e2d0bf74403ab25a953182804f5~tplv-k3u1fbpfcp-zoom-1.image)

** 印证推断 **:通过堆栈定位到具体代码,代码逻辑如下:

BlacklistBiz->【线程池 A】blacklistTempNewService.hit
blacklistTempNewService.hit->callTimes
callTimes->userCrowdServiceClient.isHit->【线程池 A】crowdIdServiceRpc.groupFacadeHit



** 小结 **:BlacklistBiz 作为主线程通过线程池 A 执行了 blacklistTempNewService.hit 工作,而后 blacklistTempNewService.hit 中又应用线程池 A 执行了 crowdIdServiceRpc.groupFacadeHit 造成了 ** 线程期待、假死景象 **,与上述推断统一,至此,问题已实现定位。** 解决 **:方法很简略,额定新增一个线程池 ** 防止线程池嵌套 ** 应用。## 4、意外播种,发现一个影响回绝营销服务性能的问题点

查看堆栈信息时发现存在大量 **waiting to lock** 的信息:![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5c6336fa9fff4a44b5eefcca26e75db3~tplv-k3u1fbpfcp-zoom-1.image)

** 问题 **:通过上述堆栈进而排查代码发现一个服务链路中的 3 个办法应用了 ** 同一把锁 **,性能不升高都怪了 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/157d2e94214f414992fd4a5bab6e889c~tplv-k3u1fbpfcp-zoom-1.image) ** 解决 **:通过引入 caffeine 本地缓存替换掉原来应用同步锁保护的手写本地缓存。## 5、额定播种,你晓得 jsf 线程池满的时候报 RpcException 客户端不会进行重试吗?这个让我挺意外的,之前看 jsf 代码以及和 jsf 架构师交换失去的信息是:所有 RpcException 都会进行重试,重试的时候通过负载平衡算法从新找 provider 进行调用,但在理论验证过程中发现若服务端报:handlerRequest error msg:\[JSF-23003\]Biz thread pool of provider has bean exhausted, the server port is 22001,** 客户端不会发动重试 **,此论断最终与 jsf 架构师达成统一,所以此类场景想要重试,须要在客户端程序中想方法,实现比较简单,这里不贴样例了。# 事件回顾

![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/77d21de841d84f2d9c788f72dd9db423~tplv-k3u1fbpfcp-zoom-1.image) 解决问题后对以前的景象和相似问题做了进一步开掘,梳理出了如上图的整个链路(问题代码同学早已不在、大可疏忽问题人,此处从上帝视角复盘整个事件),通过 2 月 24 号的问题,岂但彻底解决了问题,还对影响性能的因素做了优化,最终成果有:1、解决回绝营销 jsf 服务线程期待安全隐患、去掉同步锁晋升吞吐,tps 从 ** 2 万晋升至 3 万 ** 的状况下,tp99 从 **100ms 升高至 65ms**;2、jmq 重试期待及延迟时间调优,躲避重试时吞吐大幅升高:tp99 从 **1100ms 升高至 300ms**;3、jsf 负载平衡算法调优,躲避机器故障时依然大量申请打到机器上,成果是服务绝对 ** 稳固 **;** 最终从 8 点多执行完提前至 5 点前实现,整体工夫缩减了 57%,并且即便机器呈现“问题”也不会大幅升高整体吞吐,收益比拟显著。**

优化后的运行图如下:![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1690b5abf7ef4e5cb1e956b72d1e1263~tplv-k3u1fbpfcp-zoom-1.image)

# 写在最初

微电平台虽说不在黄金链路,但场景复杂度(业务复杂度、rpa 等机器人用户复杂度)以及流量量级使咱们常常面临各种挑战,好在咱们都解决了,这里共勉一句话:“** 在后退的路上总会有各种意想不到的状况,然而,都会拨云见日 **”。
正文完
 0