背景介绍
发钱啦:笔者所在公司22年的年终奖是23年2月底发放,在国内互联网公司来说应该是比较靠前了。在整个业务场景中,部门奖金池包的计算是核心。系统要确保部门的年终奖不能超包。在整个业务场景中,每个部门都有一个基础的奖金包。该包是基于部门员工计算,大体的公式为 每位员工的基本工资出勤系数目标年终奖倍数然后求和。每个部门除了这个基础包可能还会有一部份直接分配给部门的奖金。同时为了起到更大的激励作用,允许上级部门调整下级部门包的金额。具体可以按照金额、比例进行调整。调整的页面包含:
- 本部门及下级部门一体调整
- 本部门预留
- 本部门调增/调减
举例:一个三级部门C 有10名员工,月薪为6w,签订年终奖为4个月。出勤系数都是1,则C部门的年终奖的初始值为:6w41*10 = 240W
- C部门的上级部门B,进行了“本部门及下级部门一体调整 ”的调整,假设10%
- B部门的上级A部门也进行了调整 “本部门及下级部门一体调整 ”,假设5%
这样在计算C部门可用年终奖时,需要综合考虑A对C的影响,A对B的影响,B对c的影响。这些影响都计算完成后才能计算出C部门可用的年终奖。
基于上面的场景,需要提供一个接口,入参是部门ID,结果是部门年终奖信息(包含总额、已用、剩余、上级调整影响),因为很多场景都会用到这个接口(特别是在分配页面,管理者没每改动员工的年终奖信息都要快速返回已用和剩余信息),要保证接口的响应时长,当时定的目标是 999线<200ms
线上数据量预估
- 员工数量15w+
- 部门薪酬数据都是加密存储,计算之前要进行解密处理
- 部门数量2000+,(末级)部门最大人数 3000+。最大部门人数8w+
系统配置
- ORACL 数据库 16C/128G
- WEB 服务器 ,云服务 随时扩容
先期考虑的问题
- 原始数据需要解密,解密是否会消耗CPU?笔者前期碰到过因解密导致CPU打满的情况
- 大部门的员工数据加载的内存是否会大量占用内存?
- 考虑到系统计算量大,能否先期计算部门的奖金池?
开发阶段碰到的问题
-
sql查询竟然超过一秒 进入开发后碰到的第一个问题就是从数据库中查询部门的薪资信息,笔者在测试环境找了1000人左右的部门,查询员工的薪资信息,发现一条简单的sql查询竟然然需要1s。sql大体如下:select 月薪,出勤系数,年终奖倍数 from 人员表 a inner join 部门表 b on a.部门ID=b.部门ID and b.父部门ID like ‘%部门ID%’。有经验的同学可能一眼看出问题,模糊查询有问题。会进行全表扫描,好,优化掉。查询出部门的全路径 ,将sql改为 :
select 月薪,出勤系数,年终奖倍数 from 人员表 a inner join 部门表 b on a.部门ID=b.部门ID and b.父部门ID like ‘部门全路径%’。然并卵,耗时依然1S,看了下测试环境不到10W的数据,优化器应该是走了全表扫描。但是性能还是要优化 -
fectchSize :oracle 数据库默认的fectchSize默认为10,如果大数据量查询时,客户端和oracle之间会进行多次网络交互,增加sql的执行时长。于是将fectChSize 设置成了3000(最大部门人数
<select id="selectEmpl" fectchSize=3000> select 月薪,出勤系数,年终奖倍数 from 人员表 a inner join 部门表 b on a.部门ID=b.部门ID and b.父部门ID like ‘部门全路径%’。 </select>
果然:sql执行时长 70ms左右
虽然sql的执行时长大大降低,但是再压测的时候发现JVM占用大量内存(因使用了fectchSize,后续说明)
-
计算部门的年终奖
虽然sql已经执行的足够快,但是在实际计算奖金包时要考虑到上级部门对本部门的影响,可能需要计算从根节点到当前部门的所有部门的奖金包。再加上接口需要计算已用信息,因此代码线性执行肯定不能满足性能需求。采用多任务并行执行
FutureTask<CalculateTask> futureList = Lists.newArrayList();
改进后的接口,在5级部门计算奖金包时,能保证在200ms返回。功能开发完成。QA同事开始对接口进行压测。
-
压测碰到的问题
- 开始时接口很快,180ms就能返回,但是随着压测的进行接口响应速度慢慢降下来。最后系统假死,重启后系统恢复正常。
当时本能反应是:肯定是内存泄漏了,开始看代码。找了很多地方也没看出哪里有问题(中间非常耗时和郁闷)。看了下GC日志,发现确认JVM内存占用很多,公司是Docker 集群部署,当时压测机器的物理内存为12G,给JVM分配了8G。通过查询GC日志发现JVM内存占用6G(事后才知道是因为fetchSize 设置的原因),更肯定了是内存泄漏导致的想法。最后仔细看了GC日志,发现虽然JVM内存占用大,但是GC的频率和耗时并不高。并且内存在接近8G时可以进行正常的回收,不应该导致系统假死。换思路:用jstack 看了下几百个NZJ_CALCULATE 开头的线程。当时的反应是这么多线程不应该呀(未留下案发现场的日志),线程池应该当时就释放了。不应该这样。回去看代码。果然ExecuteService 忘记了shutDown。加上shutdown后。系统的响应时长并没有随着压测时间增加
2、系统占用大量内存
- 开始时接口很快,180ms就能返回,但是随着压测的进行接口响应速度慢慢降下来。最后系统假死,重启后系统恢复正常。
压测是发现8G的内存配置,系统会占用到将近8G,因计算是需要将大量数据加载到内存,当时也感觉不正常。看了下GC(回收器是G1)日志,垃圾回收时长也没有不正常。当时没有多想。诡异的问题是:QA同事压测结束后,系统的内存占用不正常。总感觉有2G多的内存一直被占用不释放。决定将内存dump下看看里面到底是什么。
Leak Suspects One instance of **"com.alibaba.druid.pool.DruidDataSource"** loaded by **"org.apache.catalina.loader.ParallelWebappClassLoader @ 0x5ece40be0"** occupies **2,735,776,520 (98.08%)**
上面是用MAT分析内存镜像的结果,发现内存中有很多DruidDataSource这个对象。google可一下发现github 上确实有人反馈过druid 占用内存的情况。链接如下:github.com/alibaba/dru… 看来这锅应该是druid 的了,开始按照网上给的建议修改druid配置(主要是关闭druid的sql统计功能),折腾的半天发现没有任何作用,内存占用一直没变。又回头仔细看了下dump文件发现内存中 oracle.jdbc.driver.T4CConnection 这个对象非常多。最后在oracle官网看了这个文档:www.oracle.com/technetwork…
意思大概是在设置较大的fectchSize 会导致oracle的驱动占用较大的内存。 看了这个锅应该是oracle驱动的了。
PS:其实最终也没有解决内存占用过高的问题。为了防止线上出现OOM的问题。在系统上线后 服务器就行了double。 从后续的运行结果看,系统未出现问题。如果有童鞋碰到过类似问题麻烦一起讨论下
最后系统运行基本满足了业务需求,接口性能监控显示999线在200ms 下。部门场景会出现400-500ms,没找到原因。推测应该是大部门导致