概述

MyRocks TTL(Time To Live) 特性允许用户指定表数据的自动过期时间,表数据根据指定的时间在compact过程中进行清理。

MyRocks TTL 简单用法如下,

在comment中通过ttl_duration指定过期时间,ttl_col指定过期时间列

  1. CREATE TABLE t1 (
  2. a bigint(20) NOT NULL,
  3. b int NOT NULL,
  4. ts bigint(20) UNSIGNED NOT NULL,
  5. PRIMARY KEY (a),
  6. KEY kb (b)
  7. ) ENGINE=rocksdb
  8. COMMENT='ttl_duration=1;ttl_col=ts;';

也可以不指定过期时间列ttl_col,插入数据时会隐式将当前时间做为过期时间列存储到记录中。

  1. CREATE TABLE t1 (
  2. a bigint(20) NOT NULL,
  3. PRIMARY KEY (a)
  4. ) ENGINE=rocksdb
  5. COMMENT='ttl_duration=1;';

分区表也同样支持TTL

  1. CREATE TABLE t1 (
  2. c1 BIGINT,
  3. c2 BIGINT UNSIGNED NOT NULL,
  4. name VARCHAR(25) NOT NULL,
  5. event DATE,
  6. PRIMARY KEY (`c1`) COMMENT 'custom_p0_cfname=foo;custom_p1_cfname=bar;custom_p2_cfname=baz;'
  7. ) ENGINE=ROCKSDB
  8. COMMENT="ttl_duration=1;custom_p1_ttl_duration=100;custom_p1_ttl_col=c2;custom_p2_ttl_duration=5000;"
  9. PARTITION BY LIST(c1) (
  10. PARTITION custom_p0 VALUES IN (1, 2, 3),
  11. PARTITION custom_p1 VALUES IN (4, 5, 6),
  12. PARTITION custom_p2 VALUES IN (7, 8, 9)
  13. );

RocksDB TTL

介绍MyRocks TTL实现之前,先来看看RocksDB TTL。
RocksDB 本身也支持TTL, 通过DBWithTTL::Open接口,可以指定每个column_family的过期时间。

每次put数据时,会调用DBWithTTLImpl::AppendTS将过期时间append到value最后。

在Compact时通过自定义的TtlCompactionFilter , 去判断数据是否可以清理。具体参考DBWithTTLImpl::IsStale

  1. bool DBWithTTLImpl::IsStale(const Slice& value, int32_t ttl, Env* env) {
  2. if (ttl <= 0) { // Data is fresh if TTL is non-positive
  3. return false;
  4. }
  5. int64_t curtime;
  6. if (!env->GetCurrentTime(&curtime).ok()) {
  7. return false; // Treat the data as fresh if could not get current time
  8. }
  9. int32_t timestamp_value =
  10. DecodeFixed32(value.data() + value.size() - kTSLength);
  11. return (timestamp_value + ttl) < curtime;
  12. }

RocksDB TTL在compact时才清理过期数据,所以,过期时间并不是严格的,会有一定的滞后,取决于compact的速度。

MyRocks TTL 实现

和RocksDB TTL column family级别指定过期时间不同,MyRocks TTL可表级别指定过期时间。
MyRocks TTL表过期时间存储在数据字典INDEX_INFO中,表中可以指定过期时间列ttl_col, 也可以不指定, 不指定时会隐式生成ttl_col.

对于主键,ttl_col的值存储在value的头8个字节中,对于指定了过期时间列ttl_col的情况,value中ttl_col位置和valule的头8个字节都会存储ttl_col值,这里有一定的冗余。具体参考convert_record_to_storage_format

读取数据会自动跳过ttl_col占用的8个字节,参考convert_record_from_storage_format

对于二级索引,也会存储ttl_col同主键保持一致,其ttl_col存储在value的unpack_info中,

  1. if (m_index_type == INDEX_TYPE_SECONDARY &&
  2. m_total_index_flags_length > 0) {
  3. // Reserve space for index flag fields
  4. unpack_info->allocate(m_total_index_flags_length);
  5. // Insert TTL timestamp
  6. if (has_ttl() && ttl_bytes) {
  7. write_index_flag_field(unpack_info,
  8. reinterpret_cast<const uchar *const>(ttl_bytes),
  9. Rdb_key_def::TTL_FLAG);
  10. }
  11. }

二级索引ttl_col同主键保持一致。 对于更新显式指定的ttl_col列时,所有的二级索引都需要更新,即使此列不在二级索引列中

MyRocks TTL 清理

MyRocks TTL 清理也发生在compact时,由Rdb_compact_filter定义清理动作, 具体参考should_filter_ttl_rec

RocksDB TTL中过期时间和当前时间做比较,而MyRocks TTL 的过期时间是和最老的快照时间(m_snapshot_timestamp )做比较(当没有快照时,也取当前时间)。

  1. bool should_filter_ttl_rec(const rocksdb::Slice &key,
  2. const rocksdb::Slice &existing_value) const {
  3. uint64 ttl_timestamp;
  4. Rdb_string_reader reader(&existing_value);
  5. if (!reader.read(m_ttl_offset) || reader.read_uint64(&ttl_timestamp)) {
  6. std::string buf;
  7. buf = rdb_hexdump(existing_value.data(), existing_value.size(),
  8. RDB_MAX_HEXDUMP_LEN);
  9. // NO_LINT_DEBUG
  10. sql_print_error("Decoding ttl from PK value failed in compaction filter, "
  11. "for index (%u,%u), val: %s",
  12. m_prev_index.cf_id, m_prev_index.index_id, buf.c_str());
  13. abort();
  14. }
  15. /*
  16. Filter out the record only if it is older than the oldest snapshot
  17. timestamp. This prevents any rows from expiring in the middle of
  18. long-running transactions.
  19. */
  20. return ttl_timestamp + m_ttl_duration <= m_snapshot_timestamp;
  21. }

MyRocks TTL 读过滤

前面讲到, RocksDB TTL 过期时间并不严格,取决于compaction速度。MyRocks TTL也有类似问题,因此MyRocks引入参数rocksdb_enable_ttl_read_filtering, 当开启此参数时,过期时间是严格的。 每次读取记录会调用should_hide_ttl_rec判断此记录是否过期,当compact操作不及时而没有清理的过期记录,在读取时会被过滤掉。

  1. bool ha_rocksdb::should_hide_ttl_rec(const Rdb_key_def &kd,
  2. const rocksdb::Slice &ttl_rec_val,
  3. const int64_t curr_ts) {
  4. DBUG_ASSERT(kd.has_ttl());
  5. DBUG_ASSERT(kd.m_ttl_rec_offset != UINT_MAX);
  6. /*
  7. Curr_ts can only be 0 if there are no snapshots open.
  8. should_hide_ttl_rec can only be called when there is >=1 snapshots, unless
  9. we are filtering on the write path (single INSERT/UPDATE) in which case
  10. we are passed in the current time as curr_ts.
  11. In the event curr_ts is 0, we always decide not to filter the record. We
  12. also log a warning and increment a diagnostic counter.
  13. */
  14. if (curr_ts == 0) {
  15. update_row_stats(ROWS_HIDDEN_NO_SNAPSHOT);
  16. return false;
  17. }
  18. if (!rdb_is_ttl_read_filtering_enabled() || !rdb_is_ttl_enabled()) {
  19. return false;
  20. }
  21. Rdb_string_reader reader(&ttl_rec_val);
  22. /*
  23. Find where the 8-byte ttl is for each record in this index.
  24. */
  25. uint64 ts;
  26. if (!reader.read(kd.m_ttl_rec_offset) || reader.read_uint64(&ts)) {
  27. /*
  28. This condition should never be reached since all TTL records have an
  29. 8 byte ttl field in front. Don't filter the record out, and log an error.
  30. */
  31. std::string buf;
  32. buf = rdb_hexdump(ttl_rec_val.data(), ttl_rec_val.size(),
  33. RDB_MAX_HEXDUMP_LEN);
  34. const GL_INDEX_ID gl_index_id = kd.get_gl_index_id();
  35. // NO_LINT_DEBUG
  36. sql_print_error("Decoding ttl from PK value failed, "
  37. "for index (%u,%u), val: %s",
  38. gl_index_id.cf_id, gl_index_id.index_id, buf.c_str());
  39. DBUG_ASSERT(0);
  40. return false;
  41. }
  42. /* Hide record if it has expired before the current snapshot time. */
  43. uint64 read_filter_ts = 0;
  44. #ifndef NDEBUG
  45. read_filter_ts += rdb_dbug_set_ttl_read_filter_ts();
  46. #endif
  47. bool is_hide_ttl =
  48. ts + kd.m_ttl_duration + read_filter_ts <= static_cast<uint64>(curr_ts);
  49. if (is_hide_ttl) {
  50. update_row_stats(ROWS_FILTERED);
  51. }
  52. return is_hide_ttl;
  53. }

MyRocks TTL 潜在问题

Issue#683 中谈到了MyRocks TTL 有个潜在问题, 当更新显式指定的ttl_col列值时,compact时有可能将新的记录清理掉,而老的记录仍然保留,从而有可能读取到本该不可见的老记录。此问题暂时还没有close.

最后

MyRocks TTL 是一个不错的特性,可以应用在历史数据清理的场景。相比传统的Delete数据的方式,更节约空间和CPU资源,同时传统的Delete还会影响查询的效率。目前MyRocks TTL 还不够成熟,还有许多需要改进的地方。