一、状态的底层存储——CopyOnWriteStateMap概述
不管是ValueState、ListState、MapState,它们的底层存储都是用的StateTable<K, N, S>,而StateTable的底层存储又是StateMap<K, N, S>的数组,每个StateMap代表一个keyGroup。在获取元素时,先根据key计算出来它所属的keyGroup,拿到对应的StateMap,再通过map.get(key)的方式获取到对应的元素。
K:keyBy时用的key
N:namespace, 用于区分窗口。
假设需要统计 app1 和 app2 每个小时的 pv 指标,则需要使用小时级别的窗口。状态引擎为了区分 app1 在 7 点和 8 点的 pv 值,就必须新增一个维度用来标识窗口。
Flink 用 Namespace 来标识窗口,这样就可以在状态引擎中区分出 app1 在 7 点和 8 点的状态信息。
S:存储的值,对于ValueState来说,里面就是一个值;对于ListState来说,里面是一个ArrayList;对于MapState来说,里面是一个HashMap。
所以,Flink的状态的真实数据其实是存储在StateMap中的,我们这里重点研究StateMap。
StateMap是一个抽象类,它有两个实现类:CopyOnWriteStateMap和NestedStateMap(目前还没看到NestedStateMap的用处,先重点介绍CopyOnWriteStateMap)
CopyOnWriteStateMap的实现类似于HashMap,数组+链表,每个元素是StateMapEntry。CopyOnWriteStateMap用的是数组+链表,而不像HashMap一样采用数组+链表+红黑树的结构,是因为CopyOnWriteStateMap在插入元素时只有头插法,无法构造树形结构。
在做checkpoint的时候把数据复制到新数组中,而在put数据的时候,可能会导致新数组中的数据被重写,所以不能直接修改,而要复制出来。
CopyOnWriteMap相比于普通的HashMap,它实现了两个功能:
-
渐进式rehash,即不是一下把数据全部迁移到新的hash表,而是慢慢去迁移数据到新的hash表中
渐进式rehash允许哈希表在进行扩展或收缩时,保持对现有数据的可用性。这意味着在进行rehash的过程中,哈希表可以同时处理新旧键值对的插入、查找和删除操作,而不会中断服务。这对于需要保持高可用性和持续操作的实时应用程序特别有用。
对于普通hash表的一次性rehash,它在rehash的时候,会把线程占住一直等它做完,如果hash表中的数据很大,这个过程会很长
-
Checkpoint 时 CopyOnWriteStateMap 支持异步快照,即Checkpoint在做快照的同时,用户仍然对CopyOnWriteStateMap中数据进行修改,那么需要解决的问题是:数据修改了,怎么保证快照数据的准确性呢。
这里同时涉及到checkpoint时的snapshot复制机制
二、渐进式rehash策略
2.1 简述
CopyOnWriteMap中有2个数组,一个是primaryTable,作为主数组;另一个是incrementalRehashTable(简称rehashTable),当primaryTable的元素大于theshhold时,就创建一个2*capacity的rehashTable,然后在每次增、删、改、查的时候都将primaryTable中的一部分元素rehash到rehashTable。
rehash时有一个规律,假设primaryTable的长度是4,rehashTable的长度为8,那么 primaryTable 中 0 位置上的元素会迁移到 rehashTable 的 0 和 4 位置上,同理 primaryTable 中 1 位置上的元素会迁移到 rehashTable 的 1 和 5 位置上,如下图所示。
这是因为当数组长度为2的倍数时,pos = hashcode & (length - 1),t = length - 1是一个二级制都为1的数,数组长度扩大2倍,相当于在t的二进制前面加了一个1,所以如果hashcode的二进制第3位如果是0,那么pos在rehash之后不变,如果是1,那么在rehash之后加4。
上面说到了“一部分”,那这个一部分到底是多少呢?
看源码可以看到,当数组中元素充足时,那就迁移MIN_TRANSFERRED_PER_INCREMENTAL_REHASH(默认是4)个桶中的元素;如果已经到头了,就退出。注意:这里说的是桶,而不是个,所以迁移过程的时间长短,也要取决于桶中的元素个数。
2.2 当同时存在2个桶时,在增删改查时要用哪个
依据于计算出来的位置pos与rehashIndex的大小进行比较,rehashIndex是记录的是当前在primarytTable中rehash到的位置,如果pos >= rehashIndex,说明该元素还未迁移;如果pos < rehashIndex,说明已迁移,该去rehashTable中去找。
三、CopyOnWrite机制
3.1 StateMap的Snapshot策略
-
如果Snapshot时不在扩容
如果在做snapshot时,map不在扩容阶段,那么就把primaryTable中的引用直接复制一份到snapshotData中,注意:这里复制的只是primaryTable中的引用,而不是实际的数据,且都是头节点的引用,因为用头节点就可以拿到后面的数据了
-
如果Snapshot时在扩容
是从三部分复制到snapshotData的
i. primaryTable的[rehashIndex, length),即没有被rehash的数
ii.rehashTable的 [0,rehashIndex)
iii.rehashTable的 [length >>> 1, length >>> 1 + rehashIndex)
ii 和 iii 加起来为全部已经被rehash的数,如上面的rehash策略所说,pos=0的数据被rehash到0和4上,pos=1的数据被rehash到1和5上
3.2 增删改查流程
3.2.1 修改元素
如果是头节点,就直接修改头节点的引用;如果是中间节点,就复制目标节点及目标节点以前的所有元素
解释:
修改头节点
修改中间节点
一个错误的思想:
如上图,如果我们想修改节点b,那么只把b复制出来一份,然后让a指向b copy,这样成立吗?显然是不成立的,a的指向只能有一个,要么是b,要么是b copy
正确的做法应该是把b及b以前的元素全部复制,如下图
3.2.2 查找元素
为了防止在查找以后把Value的引用修改,所以查找也要做深拷贝,所以查找和修改的处理方法是一样的,把目标节点以前的所有元素都复制出来
3.2.3 增加元素
看了上面的修改和查找流程,我们应该对CopyOnWrite有了理解,CopyOnWrite就是在只要Map中的元素有可能被修改,那么它就要进行复制,所以增加元素为了避免复制的开销,它只会在头节点添加元素。我理解这同时也是CopyOnWriteStateMap为什么只是数组+链表的结构,而不像HashMap一样还有数组+红黑树的优化。
3.2.4 删除元素
删除流程和修改流程其实一样,如果是头节点,就直接修改引用,如果是中间节点,复制该节点以前的所有元素
删除头节点,直接修改头节点所在桶的引用
删除中间节点,复制该节点以前的所有元素