《关于我因为flink成为spark源码贡献者这件小事》
各位读者老爷请放下手上的板砖,我可真没有标题党,且容老弟慢慢道来。
spark和flink本身相信我不用做过多的介绍,后端同学不管搞没搞过大数据,应该都多多少少听过。
如果没听过,简单说,spark和flink之于大数据,就好比vue和react之于前端,就好比spring家族之于java。
从2015年开始接触大数据,2016年开始使用spark,到2022年初能够为spark社区贡献一点微博的贡献,成为spark项目的contributor,对我来说是一段奇特的经历。
这段经历来源于一次spark窗口计算,由于我觉得并不能完全满足要求,想通过源码改造一下。
这一下子进了源码就误入歧途了。
什么要求之类的按不表,进入正题。
什么是窗口计算
在大数据领域,基于窗口的计算是非常常见的场景,特别在于流式计算(flink只不叫实时和离线,只区分有界无界)。
如果这说起来还是有点抽象,那举个例子,相信你很快就能明白。
比如微博热搜,我需要每分钟计算过去半小时的热搜词取top50;
比如新能源车,行驶过程中每秒或每两秒钟上报各信号项,如果30秒钟内没有收到该车的信号项,我们认为该车出现故障,便进行预警(假设场景);
等等。
前者微博热搜是一个典型的时间窗口,后者新能源车是一个典型的会话窗口。
时间窗口(timewindow)
时间窗口又分为滑动窗口
(sliding window)和滚动窗口
(tumbling window)。反正意思就是这么个意思,在不同的大数据引擎里叫法略有不同,在同一个引擎里不同的API里叫法也略有区别(比如,flink 滑动窗口在DataSet&DataStream api和Table(sql) api里分别叫作sliding window 和HOP window)。
总之时间窗口有一个长度距离(m)和滑动距离(n),当m=n时,这就是一个滚动窗口,相邻窗口两两并不相交重叠。
当m>n时,称为滑动窗口,这时相邻的两个窗口就有了重叠部份。
在多数场景下,m为n的正整数倍。即m%n=0;除非产品经理认为我们应该每61秒统计过去7.3分钟的微博热搜(???)。
这个例子可能极端了些,但m%n != 0的实际应用场景肯定是有的。
会话窗口(sessionwindow)
session窗口相对抽象一点。大家可以把session对应到web应用上,理解为一个连接session。
当大数据引擎接收到一条数据相当于一个连接session,当在设定的时间范围内连续没有接收到数据,相当于session会话已断开,这里触发窗口结束。
因此会话窗口长度是不固定的,没有固定的开始和结束。而且相邻的窗口也不会相交重叠。
到这里,大家对大数据的窗口计算应该有了一个简单的感性认识,我们今天讨论的重点是时间窗口,而且只是时间窗口下的一个小小的切点。
今天的主题
即当大数据引擎是怎样划分窗口的,当接收到一条数据的时候,数据的时间戳会落到哪些窗口?
先来简单看一点源码。不多,就一点点。
spark获取窗口的主要代码逻辑:
一时看不懂没关系,我第一次看到spark这段代码的时候也有点懵。借助spark的注释来梳理一下。
为了不水字数把spark源代码注释折叠
* The windows are calculated as below:
* maxNumOverlapping <- ceil(windowDuration / slideDuration)
* for (i <- 0 until maxNumOverlapping)
* windowId <- ceil((timestamp - startTime) / slideDuration)
* windowStart <- windowId * slideDuration + (i - maxNumOverlapping) * slideDuration + startTime
* windowEnd <- windowStart + windowDuration
* return windowStart, windowEnd
然后,我们再假设一个简单的场景,将原伪代码进行微调,并配合注释讲解一下。
假设窗口长度(windowDuration )为10
,滑动距离(slideDuration)为5
,即每5分钟计算过去10分钟的数据。简单化流程,窗口偏移时间为0。
现在spark集群收到一条数据,它的事件时间戳为13
,然后需要计算13会落到哪些窗口里面。
// `获取窗口个数,窗口长度(m)/滑动长度(n),当两者相等时,就1个窗口;
// 当m%n=0时,窗口长度为除数;当m%n!=0时,窗口长度为除数向下的最小整数
// 这里为2个窗口
maxNumOverlapping <- ceil(windowDuration / slideDuration)
// 循环获取当前时间戳在每个窗口的边界,即开始时间和结束时间
for (i <- 0 until maxNumOverlapping)
// 13/5 -> 2.6 通过ceil向下取整得到2,再+1 = 3
windowId <- ceil(timestamp / slideDuration)
// 第1次循环时,计算第1个窗口开始时间 :3 * 5 +(0 - 2)* 5 = 5
// 第2次循环时,计算第2个窗口开始时间: 3 * 5 + (1 - 2) * 5 = 10
windowStart <- windowId * slideDuration + (i - maxNumOverlapping) * slideDuration
// 第1次循环时,计算第1个窗口结束时间:5+10 = 15
// 第2次循环时,计算第2个窗口结束时间:10+10 = 20
windowEnd <- windowStart + windowDuration
return windowStart, windowEnd
通过上面的代码,我们知道,时间戳13
最终会落到[5-15]
,[10-20]
两个窗口区间。
我们再来看看flink的实现逻辑。
可以看到其实原理类似,先求得窗口个数,略有区别的是,spark是先求得窗口编号windowId
,再根据窗口编号求得每一个窗口的开始结束时间。
而spark是直接得到一个窗口开始时间lastWindowStart
,然后根据窗口开始时间+滑动距离=窗口结束时间。
再然后,窗口开始时间-窗口长度=另一个窗口的开始时间,再求得窗口的结束时间。
而不管是哪种方法,都有一个线头。
spark是windowId
windowId <- ceil((timestamp - startTime) / slideDuration)
flink是lastWindowStart
.
timestamp - (timestamp - offset + windowSize) % windowSize;
大家发现上面两边代码对比有问题了吗?
spark的两个问题
===========================================5秒钟思考线
点击查看问题答案
问题1:重复计算。`windowId`只需要计算一次就够了。
乃至于`windowStart`也只需要计算一次,根据它,可以计算出当次windowEnd,同样也可以计算出其它的窗口边界。
问题2:ceil和mod(%模运算)的差异。
这两个问题都不是BUG,是性能问题。
第1个问题,直接观察代码就可以得出结论。
第2个问题,需要通过代码测试一下。
因为scala本身也是JVM生态语言,底层都一样。所以我直接使用java写了一个基准测试,内容为ceil和求模的性能差异。
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 3, time = 4)
@Threads(1)
@Fork(1)
@State(value = Scope.Benchmark)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MathTest {
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(MathTest.class.getSimpleName())
.mode(Mode.All)
.result("MathTest.json")
.resultFormat(ResultFormatType.JSON).build();
new Runner(opt).run();
}
@Benchmark
public void ceil() {
Math.ceil((double)1000/20);
Math.ceil((double)1000/34);
}
@Benchmark
public void mod() {
int a = 1000 % 20;
int b = 1000 % 34;
}
}
大家感兴趣的话,也可以将代码复制到本地,引入JMH就可以运行。
得到的结果:
上图表示的是执行耗时,柱图越高性能越低。更多可以参考我的另一篇文章hashmap的一些性能测试。
那么可以从图中明显看到使用ceil的spark比使用mod的flink在这获取窗口这块功能的性能上肯定要差一些。
不管是第1还是第2个问题,都会随着窗口长度放大。如果我需要每1分钟计算过去60分钟的数据
那么每1条数据进来,它都会进行这样的60次无效计算。
如果一个窗口批次有一万条数据,它就会进行60万次无效计算。
大数据场景下,它的性能损耗是多少呢?
是吧?
既然有性能损耗,它必然可以优化。对于我这种开源爱(投)好(机)者来说,这都是一个大好的机会啊。
原创是不可能原创的,这辈子都不可能,只能靠抄抄flink代码才能成为spark contributor什么的。
好了,我们现在已经学会了两个主流大数据引擎的窗口计算基本原理了,现在我们来写一个大数据引擎吧。
不是,来重(抄)构(袭)spark获取时间窗口的代码吧。
第一个PR
怀着忐忑的心情,给spark社区提了一个PR。想想还有点小激动。
第一次给这种级别的开源社区提交PR。两眼一摸黑,比如PR要怎样写才规范,要不要写test case。要不要@社区大佬等等。
等了两天后,终于有大佬理我了。
我这点渣渣英语,借助翻译软件才完成了PR描述。自然不懂cc是啥,FYI是啥,nit.是啥?看到不认识的自然复制粘贴到翻译软件。
嗯?
现在翻译软件都这样先进了吗?它居然看穿了我是个废物!
第1次提交犯了很多小错误,这是没看源码贡献指南的后果。(其实是看不太懂)
这里面很详细,怎样拉下源码编译,PR标题格式怎样,PR描述规范,代码stylecheck插件等等,事无巨细,是立志于成为spark社区大佬的新手启航必备。
如果英语老师没有骗我, could you please
表示的应该是委婉,客气。
社区大佬都很有礼貌,说话又好听,我超喜欢的。
If you don't mind, could you please
在这位大佬发现我没做性能测试后,(真的,现在想想,性能改进的代码没有基准测试你敢信),温柔提醒我,在看穿我是个新(废)手(物)后,亲自写了benchmark基准测试代码。并得出新的计算逻辑比原有的性能提升30%到60%。
然后经过一番沟通修改代码注释什么的,最终合并到了master.
尾声
以上就是我的第一次spark PR之旅了。
如果你要问我感受的话。短暂的兴奋过后就是空虚。
不玩梗地说,spark社区氛围真的很好。在后面陆陆续续又给sparkT和flink提交了几个PR。没有对比就没有伤害,比起flink,spark真的对新手非常友好了。
这过程中,踩了很多坑。也收获很多。比如,这次30%到60%的性能提高,对于一个比较成熟的大数据产品来说,应该算是比较大的提升了吧?
但在后面的PR中,我做出远超此次的性能提升,而且不是借助flink的既有逻辑,完全独立完成。
后面有时间也可以把这些写出来,水水文章。
彩蛋
本来到这里就结束。但是,偶然在flink社区PR区看到一个熟悉timewindow
,哟呵,这个我熟啊。
点进去一看,尴尬了。
大家看一下,这次PR提交的主要代码更改逻辑就知道了。
没错,就是之前我给spark 提交的代码的借(抄)鉴(袭)来源。flink时间窗口分配窗口的核心代码。而且这不是优化,而是修复BUG。
哦,原来这特么的是彩蛋,这特么的是惊喜啊!
这就好比,照着隔壁班里第一名抄作业,老师给了个高分,然后被高手自爆,老师,我写得有问题。
它是怎炸的呢?
原文已经说得非常清楚,我在这里长话短说。
简单画了个图:
假设时间戳13
在一个长度15
,滑动长度5
的窗口逻辑里,我们要知道它会分配到哪2个窗口里,只需求得最后一个窗口开始的长度即可。
它最后一个窗口的开始长度为 13%5 = 3
,为时间戳对滑动距离求模。即把上图中红色部份减去
,或者向左偏移
余数部份,就是它最后一个窗口的开始长度。
不管怎样,时间戳必须得落到开始时间后面,窗口必须包含时间戳。
好,很好,很有精神。没有问题!no problem!
但是!如果时间戳是负数呢?比如-1
呢?
我们开始求它的最后一个窗口开始时间,时间戳对滑动距离求模,即-1%5 = -1
。
-1 - (-1) = 0
这样就导致不管是-1还是13都应该向左偏移的,结果跑向右边了。
13:???
开始时间大于了时间戳本身,时间戳跑到窗口外面去了
,这肯定是不正常的。
其实不仅仅是负的时间戳,是(timestamp - window.starttime)% window.slideduration <0
的情况下都会有这种问题。
只说负的时间戳有问题,就显得我的上个PR很无脑。显得我无脑没关系,这其实也是小看了spark,flink这种大范围流行的开源框架。
通过spark的测试案例也能很清楚的看到,肯定是考虑到了时间戳落到1970-01-01
之前的。
随手截一个测试案例
只不过它的时间戳都在1970-01-01
前后几秒钟范围,落在了滑动距离之内。所以这个问题没有及时暴露出来。
而且在我提交优化的PR之前,spark本身的代码是不会出现这种问题的。所以这个锅完全是我的,必须背了。
然后给社区提交了一个fix
由于种种原因,我倒是放了鸽子了。后来,被另一哥们重新提交合并。
https://github.com/apache/spark/pull/39843#issuecomment-1418436041
(完)
《关于我因为flink成为spark源码贡献者这件小事》的更多相关文章
- 简单物联网:外网访问内网路由器下树莓派Flask服务器
最近做一个小东西,大概过程就是想在教室,宿舍控制实验室的一些设备. 已经在树莓上搭了一个轻量的flask服务器,在实验室的路由器下,任何设备都是可以访问的:但是有一些限制条件,比如我想在宿舍控制我种花 ...
- 利用ssh反向代理以及autossh实现从外网连接内网服务器
前言 最近遇到这样一个问题,我在实验室架设了一台服务器,给师弟或者小伙伴练习Linux用,然后平时在实验室这边直接连接是没有问题的,都是内网嘛.但是回到宿舍问题出来了,使用校园网的童鞋还是能连接上,使 ...
- 外网访问内网Docker容器
外网访问内网Docker容器 本地安装了Docker容器,只能在局域网内访问,怎样从外网也能访问本地Docker容器? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动Docker容器 ...
- 外网访问内网SpringBoot
外网访问内网SpringBoot 本地安装了SpringBoot,只能在局域网内访问,怎样从外网也能访问本地SpringBoot? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装Java 1 ...
- 外网访问内网Elasticsearch WEB
外网访问内网Elasticsearch WEB 本地安装了Elasticsearch,只能在局域网内访问其WEB,怎样从外网也能访问本地Elasticsearch? 本文将介绍具体的实现步骤. 1. ...
- 怎样从外网访问内网Rails
外网访问内网Rails 本地安装了Rails,只能在局域网内访问,怎样从外网也能访问本地Rails? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动Rails 默认安装的Rails端口 ...
- 怎样从外网访问内网Memcached数据库
外网访问内网Memcached数据库 本地安装了Memcached数据库,只能在局域网内访问,怎样从外网也能访问本地Memcached数据库? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装 ...
- 怎样从外网访问内网CouchDB数据库
外网访问内网CouchDB数据库 本地安装了CouchDB数据库,只能在局域网内访问,怎样从外网也能访问本地CouchDB数据库? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动Cou ...
- 怎样从外网访问内网DB2数据库
外网访问内网DB2数据库 本地安装了DB2数据库,只能在局域网内访问,怎样从外网也能访问本地DB2数据库? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动DB2数据库 默认安装的DB2 ...
- 怎样从外网访问内网OpenLDAP数据库
外网访问内网OpenLDAP数据库 本地安装了OpenLDAP数据库,只能在局域网内访问,怎样从外网也能访问本地OpenLDAP数据库? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动 ...
随机推荐
- Conda 环境移植 (两种方式)
------------------------方法一------------------------ 优点: 在原机器上需要进行的操作较少,且除了conda不需要其余的库来支撑:需要传输的文件小,操 ...
- EluxJS-让你像切蛋糕一样拆解前端巨石应用
大家好,EluxJS是一套基于"微模块"和"模型驱动"的跨平台.跨框架『同构方案』,欢迎了解... 可怕的巨石怪 工作中最可怕的是什么?是遇到业务复杂且乱作一团 ...
- 大前端系统学-了解html
标签: 使用尖括号包起来的就是标签,例如我们看到的 <html></html> 一对标签 <head> 开始标签 </head> 结束标签 < ...
- Objects.requireNonNull的意义是什么
Objects.requireNonNull方法的源码是这样: public static <T> T requireNonNull(T obj) { if (obj == null) t ...
- VH6501模板工程介绍(一)
VH6501硬件结构 1.式样 1.正向有5个灯,用来指示干扰的触发状态,干扰类型(数字或模拟),通道通信以及设备状态. 2.两个DB9接口(公头male和母头female),这是CAN或CANFD通 ...
- js-day01-商品订单信息
学会表格表单(html+css) 表格的默认CSS属性 *{ margin: 0; padding: 0; } tabl ...
- JavaEE Day14 Servlet&HTTP&Request
今日内容 1.Servlet 2.HTTP协议 3.Request 一.Servlet 1.概念 2.步骤 3.执行原理 4.生命周期 5.Servlet 3.0注解配置 6.Servlet体系结构 ...
- 【Java SE进阶】Day03 数据结构、List、Set、Collections
一.数据结构 1.红黑树 根黑子黑红子黑 接近平衡树(左右孩子数量相同),查询叶子快慢次数不超过2倍 二.List 1.概述 元素有序 线性存储 带有索引 可以重复 2.常用方法 增:add(I,E) ...
- 【大数据-课程】高途-天翼云侯圣文-Day2:离线数仓搭建分解
一.内容介绍 昨日福利:大数据反杀熟 今日:数据看板 离线分析及DW数据仓库 明日:实时计算框架及全流程 一.数仓定义及演进史 1.概念 生活中解答 2.数据仓库的理解 对比商品仓库 3.数仓分层内容 ...
- 持续发烧,聊聊Dart语言的静态编译,能挑战Go不?
前言 前两天写了几篇文章,谈了谈Dart做后端开发的优势,比如: <Dart开发服务端,我是不是发烧(骚)了?> <持续发烧,试试Dart语言的异步操作,效率提升500%> & ...