续单表7000w数据的一些问题及思路

2024/02/11

续单表7000w数据的一些问题及思路

这篇文章还是记录了上一篇文章中做设计、实验遇到的一些问题及解决方案,主要是内存相关的问题

大量对象造成的OOM

这个问题是由于每次从数据库查出一批数据,再由 dao 层转为 pojo 后,交由 java 代码进行处理,java 代码在处理的过程中,会把大量 pojo 的引用持有着不释放,并且会创建许多日期时间对象,导致内存占用过多,最终导致 OOM

分两步解决这个问题,一是解决 pojo 引用不释放,二是解决创建了大量对象

第一个问题还是从数据处理流程的角度来解决,分析了一下持有这么多 pojo 对象引用的原因,原来是每处理掉一条用户的数据,都要在内存中创建一条新记录,记录一下这条数据对应的用户具体信息,在编码时图方便,直接引用了 pojo 对象作为记录用户信息的载体。经过分析后发现,其实很多记录的用户信息都是一样的,系统一共也就几万个用户,大量重复的用户信息都可以共用同一个对象,这样复用对象可以有效降低大量重复对象引用

第二个问题也是类似,虽然日期对象并不是内存占用最多的,占了大概百分之十左右的内存,于是顺带着优化了

综上所述,这个问题最终其实是通过享元模式,共享了大量细粒度的对象,从而降低了内存消耗

磁盘碎片导致的IO速度下降

先说一下这个问题的症状:对某一个盘进行顺序读或者顺序写,当读了一定量或者写了一定量之后,读写速度都会急剧下降,有每秒几十 M 降低到一两 M

换了其他盘,并不会出现这个症状。于是怀疑可能是磁盘碎片导致的顺序IO速度下降,顺序 IO 速度快就是得益于机械磁盘的物理特性,所以如果磁盘出现碎片,导致后半部分的速度下降,磁盘碎片假说是符合逻辑的。由于测试机是 windows 操作系统,当时并没有找到合适的工具来查看是否存在磁盘碎片,于是直接通过磁盘碎片清理后,再次进行测试,没有出现该问题了

综上所述,这个问题最终是通过 发现症状-假设原因-验证 的这样一个流程来进行解决的

跳出这个问题谈谈性能问题排查的方法论,一般都是出现了什么症状,影响了业务的运行,我们才会先去根据症状进行粗略的排查,是 CPU、IO、内存哪个大块出问题了,定位到大模块之后,再通过一些工具定位到异常指标,此时就需要根据工程师的经验根据得到的这些信息提出假设并进行验证了,根据得到的信息提出假设,是最难的一个地方,十分考验工程师的知识与经验,提出一个假设并不难,但若想要能提出更多的假设,除了知识是否丰富之外,是否有足够踩各种坑的经验也是重要的灵感来源

使用桶排序处理无序数据

这个问题还是需要交代一下背景:我们需要对一批用户行为数据做基于滑动窗口的分析,如果数据有序还好,可以通过比较符合直觉的方式使用队列来模拟,类似于流处理滑动窗口的实现

但由于我们的系统这批用户行为数据并不是有序的,所以如何实现针对无序数据的滑动窗口,就需要转变个思路了

为了保证数据的处理速度,也不能对数据进行排序,排序由于数据量比较大,所以 MySQL 需要使用文件排序的方式,速度实在不能接受,我们最终是通过 id 的方式进行顺序读,来保证读入速度,经过测试,这种顺序读一秒读个几万条不是问题,然后计算每条数据所处的时间窗,实时更新每个时间窗的状态

最终的实现逻辑如下

 while(var data = next()) {
  range = getWindowRange(data) // 获取这条数据的日期所处的那个时间窗
  updateRangeState(range) // 更新该时间窗的状态
 }

这个算法其实就是桶排序,但当时在使用的时候不自知这是桶排序,只是因为之前在 leetcode 中刷题中经常使用的 bitmap 技巧,就想到了这个办法

数据结构与算法是一门需要持续学习,持续精进,非常有价值的课程,就算工作好几年了,我也仍在持续学习算法与数据结构

select太多记录导致内存占用过多

这个问题其实也有很多人遇到过,一条 select 语句查出的数据可能会一下把内存堆满,为了解决这个问题,因为实在没法降低每次查询的数据量,大部分人采用的,也是我最终采用的,就是采取流式查询

这个流式查询其实是需要 JDBC 的驱动支持的,所以不同数据库实现是有差异的。因为驱动干的,就是连接数据库服务器,按照既定协议,发送请求并接收响应,除了 MySQL 是通过 setFetchSize(Integer.MIN_VALUE) 来实现的之外,因为我们的产品还做了国产化数据库适配,我发现国产数据库的行为有一些差异

人大金仓如果 setFetchSize(5000),那它就会老老实实把这 5000 条数据读完了,才会向服务器请求下一批数据,但是达梦就不太听用户的话,它每次请求的数据量并不是由用户来指定,而是会比用户设定的量大一部分,这部分可能是由其服务器根据数据分布情况计算出来的活或者它觉得比较合适的,等到用户本地的数据消费完了,再去请求下一批的数据

由于我们的产品为私有化部署,所以低运维成本是至关重要的,对于一些参数,最好可以动态根据用户的环境进行调整,而不是写死。达梦给了我一个启发就是其实是可以充分利用好一些隐藏信息,或者通过动态尝试如 TCP 的拥塞控制算法来得到一个较合适的动态参数,来替代这种硬配置或者硬编码的参数

Post Directory