Flink 大状态存储 & 状态TTL

什么是Flink大状态存储?

举个栗子。现有用户访问流数据,需统计每个用户PV,用户量级为3亿。如何计算?

假定每个用户ID为50字节。那么3亿用户ID的存储需要:50 b * 3 亿 ≈ 13 G ,那么可以直接存在job内存中,如果担心job重启,内存数据丢失,可以放在redis中,或者Aerospike(一种用磁盘的kv存储)。

那如果状态再大一些呢?再举个栗子:某广告场景下,点击数据需要根据请求ID 到 请求日志中取相关维度信息。假设请求发生后,24小时之内,都有可能发生点击,那么就需要将请求日志保存24小时。

假定每条请求日志500字节,每天请求量20亿,那么请求日志保存24小时需要:500 b * 20亿 ≈ 931 G,那么此时如果hash之后存redis,代价就比较大了,怎么办呢?存磁盘吧!

 

RocksDB就是以磁盘为存储的KV存储引擎,Flink也支持以RocksDB作为状态存储。

 

那么Flink用RocksDB做状态存储需要注意什么问题那?

1、与RocksDB读写的延迟是否能满足?

众所周知(那你要是不知道就去问山聚聚,山聚聚啥都懂)RocksDB采用LSM的写入方式,因此顺序写磁盘的速度还是非常快的,随机读也能在毫秒级别,而且Flink是与本地磁盘做交互,少了一层网络传输的消耗,在读放大不严重的情况下,随机读的性能也还不错。

 

2、数据的清理方式?

众所周知(同上)RocksDB的数据删除是标记删除,已经落到磁盘中的数据会等待compaction时清除。

数据的清理分为两种方式:主动删除和过期清理。

上述场景使用哪种方式会比较好呢?

对于想要及时清理过期的key来说,可以通过TimerServer回调的方式去主动删除,但是不适用于大数据量的情况,为什么呢?因为频繁的回调很浪费cpu,24小时后相当于数据量翻倍(一遍处理新来的数据,一遍回调清理24小时之前的数据)。(PS:Flink 的 TimerServer也是保存在State中的)

主动调用指定Key的删除,需要保存24小时内所有的key,然后遍历顺序删除,可以挑选夜间一个时间,触发range清理操作。但这种做法显然也不太方便。

那么来看下TTL过期清理了。

Flink的TTL清理方式简单介绍下:

cleanupFullSnapshot() 当对状态做全量快照时清理过期数据,对开启了增量检查点(incremental checkpoint)的RocksDB状态后端无效。

cleanupIncrementally(int cleanupSize, boolean runCleanupForEveryRecord) 增量清理过期数据,默认在每次访问状态时进行清理,仅适用于状态存储方式为堆的方式。

cleanupInRocksdbCompactFilter(long queryTimeAfterNumEntries)当RocksDB做compaction操作时,通过Flink定制的过滤器(FlinkCompactionFilter)过滤掉过期状态数据。参数queryTimeAfterNumEntries用于指定在compaction处理多少条状态数据后,通过状态时间戳来判断是否过期。该策略仅对RocksDB状态后端有效。

所以针对RocksDB建议使用哪个清理策略就不用多说了吧~ 

 

说完了清理策略,再来看下TTL的设置:

StateTtlConfig requestTtlConfig = StateTtlConfig
                .newBuilder(Time.seconds(60 * 60 * 24)) // ttl 24小时
                .setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)
                .setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired)
                .setTtlTimeCharacteristic(StateTtlConfig.TtlTimeCharacteristic.ProcessingTime)
                .cleanupInRocksdbCompactFilter(10000)
                .build();

首先是TTL时间。

StateTtlConfig.UpdateType.OnCreateAndWrite 指的是对于一个key,更新其时间戳的方式是读时还是写时,这个时间戳用来判断是否过期。

StateTtlConfig.StateVisibility.NeverReturnExpired 指的是读到过期key时返回的策略。(此处是不返回)

StateTtlConfig.TtlTimeCharacteristic.ProcessingTime 指的是处理时间。

cleanupInRocksdbCompactFilter(long queryTimeAfterNumEntries) 上面介绍过了。

 

那么,TTL更具体的细节是什么样子呢?在请教了山聚聚后,我得到了答案。

对存入state的每一个value进行一次包装,在其尾部写入一个时间戳,然后每次读取时根据尾部时间戳判断一个key是否过期。其中DecodeFixed32()便是解析尾部时间戳的操作。

更加详细的请看 https://github.com/dataArtisans/frocksdb/blob/FRocksDB-5.17.2/utilities/ttl/db_ttl_impl.cc

注:FRocksDB是为Flink打造的分支。所以有些RocksDB的新特性,Flink中可能暂时不支持。

// Checks if the string is stale or not according to TTl provided
bool DBWithTTLImpl::IsStale(const Slice& value, int32_t ttl, Env* env) {
  if (ttl <= 0) {  // Data is fresh if TTL is non-positive
    return false;
  }
  int64_t curtime;
  if (!env->GetCurrentTime(&curtime).ok()) {
    return false;  // Treat the data as fresh if could not get current time
  }
  int32_t timestamp_value =
      DecodeFixed32(value.data() + value.size() - kTSLength);
  return (timestamp_value + ttl) < curtime;
}

那RocksDB是如何清理这些过期的数据呢?

这里是官方的解释:https://github.com/facebook/rocksdb/wiki/Compaction-Filter 

意思是说,RocksDB在每次compaction的时候,通过 compaction_filter 进行过滤数据,这些被过滤的数据不会写入到新生成的sst文件中,那么自然就被删除啦~

而Flink内部实现了 FlinkCompactionFilter,定义了数据的过滤逻辑。https://github.com/dataArtisans/frocksdb/blob/FRocksDB-5.17.2/java/src/main/java/org/rocksdb/FlinkCompactionFilter.java

简单来说,RocksDB compaction时调用FlinkCompactionFilter进行数据的过滤清理。

 

FlinkCompactionFilter中定义了一些config,但是暴露给用户层面的只有一个:long queryTimeAfterNumEntries,

官方的解释:

/**
 * Cleanup expired state while Rocksdb compaction is running.
 *
 * <p>RocksDB compaction filter will query current timestamp,
 * used to check expiration, from Flink every time after processing {@code queryTimeAfterNumEntries} number of state entries.
 * Updating the timestamp more often can improve cleanup speed
 * but it decreases compaction performance because it uses JNI call from native code.
 *
 * @param queryTimeAfterNumEntries number of state entries to process by compaction filter before updating current timestamp
 */

你看明白了吗,我反正没看懂。(翻译成中文我也知道啥意思,可还是隐隐约约感觉到一丝不对劲)哈哈哈。

好吧,带着这个疑问,再看看。

我的理解是:RocksDB对每个CF维护一个current_timestamp_  但是这个current_timestamp_ 不是由RocksDB根据本机时间来决定的,而是通过JIN方式对Flink调用获取到的,这个JIN详细的我不了解,知道是一种跨语言的调用,是有损耗的。

所以RocksDB并不是处理每条数据都调用Flink获取current_timestamp_,而是累计处理queryTimeAfterNumEntries条数据后更新时间戳。(在注释写清楚不就完了,代码还是C++的,嗨,看半天看不懂哇。)详见代码:https://github.com/dataArtisans/frocksdb/blob/49bc897d5d768026f1eb816d960c1f2383396ef4/utilities/flink/flink_compaction_filter.h#L140

有关TTL的更多细节请看提交:https://github.com/dataArtisans/frocksdb/commit/01dca02244522e405c9258000903fee81496f72c

 

3、Flink端关于RocksDB的优化

上面说到State的清理只会在RocksDB compaction时进行,默认情况下,根据level层数,从上至下进行compaction,所以一般会观察到这种现象:我的State总共只有100G,为什么看到磁盘使用了200G呢?

就是因为有很多数据已经过期了,但是对应的sst文件还没发生compaction,因此磁盘空间得不到回收,那么此时会发生什么事情呢?磁盘空间浪费暂且不谈,这个无关紧要,主要是会在读取的时候多扫描sst文件,会拖慢读取操作。

还有就是关于RocksDB调参的玄学问题,辣么多参数,应该调整哪个呢?

这些都不是问题!因为Flink帮你预设好啦!

那么来看下如何从Flink层面对RocksDB进行优化吧~

Flink预定义了四种对于RocksDB的参数:

DEFAULT:啥也没干
SPINNING_DISK_OPTIMIZED:常规旋转磁盘
SPINNING_DISK_OPTIMIZED_HIGH_MEM:常规旋转磁盘,消耗更多的内存来提升性能(有可能会oom,如果发生oom可以调整TaskManger的内存分配或者改为常规磁盘)
FLASH_SSD_OPTIMIZED:SSD存储

可以根据自身条件去选择对应的预定义参数。(每个预定义选项的详细参数就不展开说了,涉及到RocksDB的一些内容,后面再写一篇关于RocksDB的)

 

总的来说,关于大状态这个东西,还是要有业务场景才能发现问题,实践出真知~

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值