背景
我所负责的业务核心链路主要是通过定时任务,处理接入到平台产品的批量的离线数据,通过http接口发送给下游系统。虽然逻辑比较简单,但是随着业务的不断扩展,数据量不断增长,每天处理的数据从开始的十万级,增长到千万级甚至亿级。业务逻辑的升级刻不容缓。
业务描述
我们是通过xxl-job实现任务的调度,最开始使用的是xxl-job的轮询模式,因为需要处理的数据量比较小,这样做的好处是任务只会在一个节点执行,不需要考虑数据冲突,但是在集群架构中,只有一个节点被使用到,集群的资源被严重浪费。所以我考虑使用xxl-job的分片广播模式,这样解决了集群使用的问题。xxl-job提供了获取当前分片和总分片的方法,我们在查询处理数据的时候,只需要用 id%总分片=当前分片 即可解决数据的冲突问题。看似解决了分片处理和数据冲突的问题,但是随着接入的产品增多,需要处理的数据量增大,且不同产品所的数据量不一致,如果给所有产品都分配同样的资源,那显然是不合理的。
思考过程
首先,我需要处理的是批量生成的离线数据,且需要同时处理多个产品(每个产品是分表的),不同产品的数据量级不一样。对于大产品来说,会增大大产品的处理速度,对于小产品来说,又存在资源的浪费,那么应该如何分配呢,我想到的是通过给不同产品分配权重的方式进行资源的分配。这样能保证不同产品的资源能合理的分配。对于大数据量的产品来说,又存在一个问题,数据库查询速度较慢,一般我们都会想到使用索引,但是这里我发现,由于数据都是批量插入的,并且我只需要处理前一个周期批量插入的数据即可,那么我想到了hive中的分区,如果使用分区的话,那么我使用到的就只扫描上一个分区中的数据即可。对于大数据量的产品,网络io也是一个不小的开销,如果我每条走一次网络io的话,那千万条的数据,也会有很大的性能问题。从这几个角度出发,我开始了我们的优化过程。
MySQL优化
对于亿级的数据来说,sql查询是个很大的开销,之前我们使用索引进行处理即可,但是在数据量达到亿级的情况下只简单走索引已经显得有些吃力,在处理5000条数据的情况下,处理速度依然需要10s左右。在仔细分析业务场景后我发现,实际上我每次只需要上一个周期离线所生成的批量数据即可,所以我的mysql查询时也只需要扫描上一个周期的数据即可,如何实现这点呢?我想到了离线处理的hive中所用到的分区概念,如果mysql也支持分区的话,我通过createTime对上一个周期的数据进行分区,每次查询只查询上一个周期内的数据就可以了,通过调研发现,mysql5.0已经支持了分区的概念,只是用的确实比较少,但是对于我这个业务场景来说,我觉得是个非常不错的选择。所以我选择了对createTime进行分区,分区粒度与离线任务生成的粒度一致,如果离线任务一天一次,那么我就按天分区。如果离线任务一个月一次,那么我就按月分区,在查询时,携带分区字段即可。这样做显然提高了我的查询效率。在亿级数据查询5000条数据的情况下,查询基本保持在2s左右。
分片优化
由于我们所需要处理的产品数据是不一致的,有的产品亿级,有的产品十万级。如果分配相同的资源,那么显然是不合理的,所以我需要根据不同的产品分配不同的资源,这里使用了权重的方式。
首先,在xxl-job执行期间,我能拿到所有需要执行的产品,以及有多少个分片,如果我给不同的产品,赋值不同的权重,那么我就能根据这些数据,计算出每个节点需要分配给每个产品多少权重。
例如:我有三个产品A,B,C,A权重5,B权重3,C权重2。我有四个分片。那么我可以计算出节点数量和权重的最小公倍数,也就是20,那么就可以理解为,我一共有20个资源来处理这些产品,每个节点有5个资源,产品A可以获得 20*5/(5+3+2)=10 的资源,那么节点1和节点2都分配给A处理即可,后续的产品B需要6个资源,那么节点3完全分配给B后,还剩1个资源需要分配,那么节点4再分配给B 1个资源即可。产品C也就需要分配给C 4个资源即可分配完毕。这样做,即可解决不同数据量产品的资源分配。
处理逻辑优化
刚才我们已经解决了不同产品的资源分配,那么如何使用这些分配好的资源呢?那么肯定是不同产品分配不同的线程数量。
例如 当前节点1,为产品A独占,那么节点1的资源全部分配给A处理即可。而节点4为B 20%,C 80%,那么20%的线程给B使用,80%的线程给C使用即可,而线程数量的限定,我们可以使用juc的Sempre。
sql查询优化
同时,在查询时,由于大产品的数据量大,但是我们并不能一次查询过多的数据,这会导致大对象很快进入jvm老年代,造成频繁的full fc,而我们的对象生命周期实际比较短,只存在与一次发送请求的流程种,按理来说一次minor gc就会清理掉。所以我使用了分页查询,一次查询5000条数据出来并提交线程池进行处理,但是这样又会深度分页的问题。由于数据时批量生成的,也不存在修改等场景,所以他们的id一定是连续且递增的,那么我们可以使用id进行分页,这样即可解决深度分页的问题。当然,在第一次查询的时候,需要先查询我们的id起始位置。
在上面的逻辑中,我们也已经拿到了产品的分片权重等信息,所以在sql查询时,我们可以 右边处理的权重 > id% >= 左边的权重,这样来避免多个节点间的数据冲突问题。
网络优化
由于数据量巨大,如果每个请求都走一次http调用的话,那么显然网络的开销是很大的。因此这里我采用了批量发送的逻辑,由之前的一条数据发送一次,修改为100条批量发送一次,把之前的100次请求合并为1次请求。显著提高了查询效率。
优化结果
经过上述的优化以后,我的数据处理速度得到了显著的提升,在8核16G服务器,mysql 32核64G,单分区内千万数据量的场景下,单机能达到1300/s左右的数据处理速度。在集群部署4台机器的情况下,能满足5000/s的处理速度,满足每天亿级数据的处理场景。