记录一次系统计算逻辑优化

由于项目二期增加了一个维度,做了代码重构
由于一期设计仓促,有一些设计的不合理的地方,比如: public_ sku_site_data(sku网站数据)表
大数据推送的表将数据逻辑有许多不同的服装,GB的网站信息合在了一个表中推送过来
在这里插入图片描述
其中服装的网站数据不到site_code,terminal,stock_code维度 故这三个字段都是" "值(源数据就是如此)
下面的价格如同字段注释所示,有些数据只有GB有,有些只有服装有,后来加入的产品等级也是只有服装的数据才有
但是由于GB相关的指标需要到仓库维度,GB和服装又是混在一起计算的,为了统一,设计索引的时候将服装这些不到仓库维度的数据也存入了仓库的子type中,这样后期维护起来会增加难度,也给新接触项目的人造成逻辑混乱

且服装在这个表中的本店售价,是否促销,上架时间需要参与父type的计算,这些值就重复取了两次
在2.0之前由于出现了一个漏洞,就是受先前三个准入条件所限制,导致了如果更新的数据在变更后恰好不符合这3个条件,则将无法进入IPS进行数据更新,已将该表拆分成为服装和GB分别推送
在这里插入图片描述
在这里插入图片描述
服装移除空值字段,每日全量推送,GB移除产品等级字段,每日增量推送
但是由于二期设计时未考虑到服装数据不到仓库维度,若要改动,牵扯到接口,以及前端的改动,后期发现时想要改变设计有风险,为了项目如期上线,还是维持了原来的仓库子type设计,此项优化将放在后续需求中重构.

设计时二期拆分后,将预处理的中间表也拆分为FZ和GB
服装部分预处理的中间表为:
tmp_stock_information_fz (仓库表,包括在途/可用库存以及网站信息表中的上架时间状态等)
tmp_goods_site_fz (基础信息表,包括sku,spu,cat_id,网站标题等基础信息,代表这个网站有售这款产品)
tmp_order_sale_fz (订单表,包括到website,site,terminal,country维度的1/7/15/30天 销量,订单量,销售金额)
tmp_count_chain_fz(流量/环比表,包括到website,site,terminal维度的1/7/15/30天 UV,加购,加收,以及计算环比用的周期销量)

tmp_trend_7 (7天销量趋势,因为趋势要用数组存入索引,到每天的维度,需要另外预计算)

由于二期扩展的国家维度只有订单表相关的数据,一期是将订单表和流量相关表full outer join成一个宽表一期计算,由于二期这两个表维度不同,后面聚合逻辑处理的时候会比较麻烦,所以设计时没有将两表合并
分别和基础信息表left outer join,做起来后发现筛选器中只有销量是需要输出到结果的,也就是说只有销量相关的数据才到国家维度,但是订单量又需要用来计算实际转化率 (订单量/UV),而UV是不到国家维度的,所以实际转化率不到国家维度,
所以在筛选器中,订单量也只需要计算不到国家维度的值
故将上面两个划线的表改为
tmp_order_sale_fz (订单表,包括到website,site,terminal,country维度的1/7/15/30天销量)
tmp_dimension_website_site_terminal_fz (三维度表,包括到website,site,terminal维度的1/7/15/30天 UV,加购,加收,订单量 以及计算环比用的周期销量)

于是高高兴兴开始码第一版计算逻辑的代码

在这里插入图片描述
大概流程就是这样
①趋势数据左关联进销量数据表
②销量/趋势左关联进基础信息表
③将流量/订单/环比关联进②
④计算SPU销量关联进③
⑤计算SPU流量/订单/环比关联进④
由于服装网站的基础信息中没有到站点,终端,国家维度,需要在关联订单流量的时候给他关联赋值进去.

从维度计算上就是分为web_site_t_cy、web_site_t、web_site_cy、web_t_cy、web_site、web_t、web_cy、web、full九个维度

在这里插入图片描述

tmp_order_sale_fz 和 tmp_dimension_website_site_terminal_fz 表从hive拉入Spark后缓存在内存中,各个维度从初始最细粒度数据根据不同维度分组聚合计算,运营助手时遇到了数据在经过几次聚合计算后出现了数据误差,所以就先按这样都按源头数据计算了
但是不幸的是… 仍然出现了那个问题,并且只有web_site,web_t两个维度的数据出现了误差,检查了3遍计算逻辑,也没发现什么错误…
于是去看一期的代码,发现好像不要每个维度从初始的数据往下计算就不会出问题,然后将计算逻辑改为:
在这里插入图片描述
较粗维度的计算从上面细粒度的计算结果再次分组聚合计算(其实web_cy的维度也能从web_site_cy计算后的结果往下算,但是当时写乱了没想到,就也从初始数据计算了)
一跑,数据一对,诶 数据正确了 高高兴兴送测

随着进入测试阶段,发现第一版设计的计算逻辑有一个重大失误…
由于服装的基础数据没有站点和端口维度,而计算逻辑里最后数据的站点和端口都是由order表赋值进去的

在这里插入图片描述
这样的逻辑,在关联了销量相关数据后,那些只有点击量而没有销量的数据在关联的时候就丢了…
一开始为了图省事,不将这个两个表full outer join起来还是不行
由于这是比较源头数据就出错了,没办法只能重新将这两个表join起来,改造计算逻辑,基本整套逻辑都要改造
于是第二版的计算逻辑修改为:
先将需要计算的指标表full outer join在一起,缓存起来

在这里插入图片描述
修改好之后一对数,发现web_site和web_t维度的数据又错了… 并且在合并了表后 只有销量的数据是错的 = =
这就超出了我的理解范围了啊… 都已经改成这样了 还错 而且错的这么奇怪 我还能怎么改呢
于是灵机一动,把web_cy用web_stie_cy计算后的结果来计算,计算逻辑改为:
在这里插入图片描述
跑完数一查… 龟龟 连web_cy维度的数据也一起错了
在这里插入图片描述
于是又去对比一期的代码,发现一期这些复用的RDD并没有做缓存操作,死马当活马医,将全关联后的RDD的缓存操作注释掉,再次跑数
一查结果,数据对了

Spark在执行的时候只有遇到action算子操作才会真正的去执行,其它的transform算子是懒加载的
当遇到一个action算子后会向前追溯整条链路的执行算子,遇到shuffle操作就会将两个shuffle操作之间的算子组成一个stage,而一个stage内的逻辑也会进行底层流水线优化,重复刚才的步骤,以此类推
最终形成一个有向无环图(DAG),它有可能长这样:
在这里插入图片描述
以前在使用Spark累加器Accumulator时也遇到过一个坑
在这里插入图片描述
解决办法是为了不使这个逻辑被执行两次,在中间将结果调用cache缓存起来,这样就可以切断DAG图的依赖关系
我认为这个问题也是类似的原因导致的,或者在对全关联后的RDD调用了cache缓存操作后底层优化的时候有什么bug,实际导致的原因可能还要对源码进行更深入的研究才能知道,以后遇到类似情况的时候要持续关注进行研究.

随之完成了第二版的计算逻辑修改,继续测试…
测试过程中又发现了新的问题:一个sku在web(只选择了网站)维度的加购率是有值的,但是在web_cy(选择了网站,国家)维度的值为0,但是按照道理加购人数的值不应该因国家变化
然后去查询了一下表中的数据,发现数据中还有端口为’OTHER’的值,而加购人数全部存在于下面的第一条数据中,
这样当只根据sku,website_code(203050201,YS)分组聚合时,加购数据就会被被聚合再一起,而当根据sku,website_code,country聚合时,(203050201,YS,SA)和(203050201,YS,null)就不会被聚合在一起
也就是说基础表和指标表关联后会出现两条数据:country为空的只有加购人数数据,country有值不包含加购数据造成数据丢失.

在这里插入图片描述

于是又要开始进行计算逻辑修改…
由于之前接下选品两周就开始做运营助手,对选品里面的数据还不是特别熟悉,很难考虑到这些特殊数据的逻辑
然后就开始了第三版的计算逻辑改造
对数据的情况有了更深的了解之后发现,实际上数据的下单国家维度都是由order表中赋值进去的,也就是说如果这个sku在这个维度下是没有销量信息的话,选到了国家维度请求时一定会带某个国家来请求数据,
这样国家为空的数据永远不会被搜索出来,而全关联后的数据(使用的20190122的数据)查询共有6824183条 而其中6042194条都是空的 ,也就是有88.5%的数据会经过关联,计算,并存下来,而这部分数据完全是无用的
同理,服装网站的网站基础信息全是不到,站点,端口维度的,只有在有销量或者流量的sku数据才能到这些维度去计算,也就是说在需要选择站点,端口,国家的维度下,这些需要选择的值为空的话,里面都是"死数据"
这些数据有多少呢?
在未过滤掉这些"死数据",且不含有国家维度的现在线上的数据服装有7KW左右,而增加了国家维度后,服装有1.5E左右
在这里插入图片描述在这里插入图片描述
GB的线上数据是3.2E左右,增加了国家维度后有6.4E左右
在这里插入图片描述在这里插入图片描述
于是就考虑到了,对于服装的数据来说,在选到了国家的维度下,order表和count表全关联后的数据国家为空的数据都可以过滤掉

在这里插入图片描述
过滤掉后的服装数据从1.53E减少到了1.38E 少了1.5KW条
在这里插入图片描述
由于计算逻辑是从开始一直改下来的,重新review一下发现这种逻辑计算还是不够好的
服装改成全关联后 第一步先算web_site_t_cy维度时,要将销量带上国家维度算一遍,再去掉国家维度计算流量/订单/环比数据,
而这部分数据在web_site_t的维度下其实是统一的,所有全关联后宽表内的数据进行一次去维度聚合就行了,相当于在第一个维度计算时
计算流量/订单/环比是重复计算,于是计算逻辑可以这样优化:

在这里插入图片描述
优先计算不带国家的维度,只有销量数据新增维度,其它值不变,所以可以从不到国家维度计算的结果将销量数据关联覆盖旧结果,不到国家维度的数据不需要重新计算
这样计算的好处
1.一期中产品基本信息只跟sku有关,所以在所有计算完后进行关联,而增加的国家维度只有销量相关数据改变,不需要重新遍历赋值产品基本信息
2.由于一些不只是累加而需要计算的指标,是在最后遍历赋值产品基本信息是计算的,这部分数据也都不到国家维度,可以减少这部分的重复计算
3.减少了上面所说的重复计算
4.通过结果数据inner join需要改变的order相关数据,第一次只过滤country为空时用的是left outer join,还过滤了在order表中没有数据的sku

同时也继续将包含站点、端口的维度中,站点端口为空的"死数据"过滤掉之后
在这里插入图片描述
服装数据从一开始的1.5E条减少为4.7KW条,比现在线上的7KW条还少

GB由于网站信息表里到了站点,终端维度 有这个维度的信息就代表在这个网站有售,即使没有销量,流量数据也能够筛选出来
但是有一部分在基础商品表中有sku,但网站信息表中没有数据的也无法获取到站点,终端,也可以将这部分数据过滤掉
并且GB的下单国家也必须从销量表中关联进来,所以选择到国家维度时,不存在于销量表中的数据也是"死数据",而扩展了国家维度后翻倍的数据量大部分都是这些"死数据",也可以在计算包含国家维度时将这些数据过滤掉

重构后GB的结果数据为
在这里插入图片描述
3.2E条,与扩展国家维度前差不多

而跑数时间FZ从开始的28min左右减少至12min左右
在这里插入图片描述
GB从43min减少至28min左右
在这里插入图片描述
因为GB过滤掉的比例不及服装,且这些数据其实在spark中计算可能时间提升的不是这么明显
这些数据经过落盘,IO交换同步进ES的提升就比较明显了
FZ的同步ES数据时间从27min左右减少至9min左右

在这里插入图片描述
而GB的同步时间从1.7H减少为53min
在这里插入图片描述
至此第三次的计算逻辑重构就算完成了
将"死数据"过滤掉,以及计算逻辑优化后 节省了大约100min,现在线上跑数任务IPS选品为2个小时左右,将原本预估需要翻倍的时间减少为仍然2个小时左右
且现在将服装和GB数据拆分串行跑数之后,若先跑数据量较少的服装数据,且大数据能在八点半前推数完成的话,一般在上班时间服装数据已经能跑完
但是GB数据还是要等到十点多才能跑完

其它优化:

关于Kryo:

使用Spark的开发人员基本都会知道一条最省事的优化方法,就是使用Kryo序列化替代Java本身的序列化(这也是官方推荐的做法,而Spark默认使用Java序列化)
设置也很简单,创建Spark上下文对象时指定:
在这里插入图片描述
但是很多人会忽略一点,那就是虽然Spark在使用Kryo序列化是会默认注册一些常用的类,而自定义类需要自己去注册否则在序列化时使用的是全限定类名而占用一些内存空间
开启Kryo的情况下,被序列化的类型没有被注册,那么类型名会被写入到序列中极大的影响序列化性能,占用空间会下降甚至会不如直接使用java serialization

使用自定义注册类:
在这里插入图片描述
实现自定义注册类的接口注册自定义的类:
在这里插入图片描述
对于FZ跑数通过WebUI界面可以看到可以节省约2716.8M的内存空间(蚊子再小也是肉啊!)
在这里插入图片描述

关于同步ES:

由于Spark在写入HDFS文件时是根据并行度(分区),一个分区就会写出一个文件,为了打散任务提高任务并行度,我们将GB跑数的默认并行度设置为4000
这样计算的时候可以减少每个分区分到的数据,否则可能造成可使用的内存不够而增加GC时间,但是由于最后计算的结果可能不及源数据这么大量的数据,就会造成输出4000个小文件存储在HDFS中
这样不仅增大了NameNode在写数时的压力,也不符合HDFS的设计,所以在测试跑出全量数据后 根据数据量的大小,在写入HDFS之前,将结果数据进行repartition 重分区的操作,这样根据不同维度的结果数据重新分区,让HDFS存储时存的是一个大小适中的文件
同时在同步ES时读取HDFS文件时,也是根据一个文件一个分区读回来的
这时我就观察到,当减小了文件数量之后,在同步ES时由于调用的是foreachPartition,就是一个分区建立一个TCP连接去向ES写入数据,而文件数量过小导致连接数也很少,反而写入ES是变慢了
这样我就采取了在同步ES时将HDFS读取回来之后的数据再次重分区,将数据再次打散,从而增加连接数,增加并发写入ES的速度
从下面的日志时间可以看到 这个重分区的数量也是需要权衡的,并不是越大越好,且连接太多也会增加ES集群的压力,可能导致查询变慢,所以我折中将分区数设置为1000

关于同步ES:

由于Spark在写入HDFS文件时是根据并行度(分区),一个分区就会写出一个文件,为了打散任务提高任务并行度,我们将GB跑数的默认并行度设置为4000
这样计算的时候可以减少每个分区分到的数据,否则可能造成可使用的内存不够而增加GC时间,但是由于最后计算的结果可能不及源数据这么大量的数据,就会造成输出4000个小文件存储在HDFS中
这样不仅增大了NameNode在写数时的压力,也不符合HDFS的设计,所以在测试跑出全量数据后 根据数据量的大小,在写入HDFS之前,将结果数据进行repartition 重分区的操作,这样根据不同维度的结果数据重新分区,让HDFS存储时存的是一个大小适中的文件
同时在同步ES时读取HDFS文件时,也是根据一个文件一个分区读回来的
这时我就观察到,当减小了文件数量之后,在同步ES时由于调用的是foreachPartition,就是一个分区建立一个TCP连接去向ES写入数据,而文件数量过小导致连接数也很少,反而写入ES是变慢了
这样我就采取了在同步ES时将HDFS读取回来之后的数据再次重分区,将数据再次打散,从而增加连接数,增加并发写入ES的速度
从下面的日志时间可以看到 这个重分区的数量也是需要权衡的,并不是越大越好,且连接太多也会增加ES集群的压力,可能导致查询变慢,所以我折中将分区数设置为1000
在这里插入图片描述

关于缓存:

由于Spark的执行特性,是遇到action算子操作时才真正执行,而一些可以复用的RDD,一种比较简单粗暴的优化方法就是将这些复用的RDD都调用cache缓存操作将其缓存起来,否则第二个action操作追溯生成DAG图时,还会将这个RDD重复计算一遍
缓存默认的缓存级别都是缓存在内存中的,而内存有时也是一种宝贵的资源,虽然大部分时间缓存下来都会比重新计算性能要好,但是有时候在资源紧张的时候还需权衡一下缓存带来的收益
Spark 1.6之后引入了动态内存分配机制,缓存内存和执行内存共同分配在了一块内存区域中,这块区域的大小也是根据所申请的executor内存大小相关
我观察到GB跑数时,每个exector默认分配的缓存区域大小为2.1G 而在跑数过程中 一些executor的缓存大小已经达到2G,这样一是减少了执行内存的大小,可用内存小了会造成GC更加频繁的情况,而且可能数据量更大之后它的缓存淘汰也许更加消耗性能
所以我还将缓存过的RDD都记录下来,在它们使用完后将这部分缓存调用 unpersist 手动释放出来
在这里插入图片描述
但是需要注意的是 unpersist 不是一个惰性操作,也就是说代码执行到这里会立即执行,将缓存释放,而其他算子是根据action操作执行才去执行的,所以调用 unpersist 一定要在使用了这个缓存RDD的action操作之后调用 否则缓存就提前失效了.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值