MyRocksTTL特性介紹

zysql發表於2018-04-18

概述

MyRocks TTL(Time To Live) 特性允許使用者指定表資料的自動過期時間,表資料根據指定的時間在compact過程中進行清理。

MyRocks TTL 簡單用法如下,

在comment中通過ttl_duration指定過期時間,ttl_col指定過期時間列

CREATE TABLE t1 (
  a bigint(20) NOT NULL,
  b int NOT NULL,
  ts bigint(20) UNSIGNED NOT NULL,
  PRIMARY KEY (a),
  KEY kb (b)
) ENGINE=rocksdb
COMMENT=`ttl_duration=1;ttl_col=ts;`;

也可以不指定過期時間列ttl_col,插入資料時會隱式將當前時間做為過期時間列儲存到記錄中。

CREATE TABLE t1 (
  a bigint(20) NOT NULL,
  PRIMARY KEY (a)
) ENGINE=rocksdb
COMMENT=`ttl_duration=1;`;

分割槽表也同樣支援TTL

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

RocksDB TTL

介紹MyRocks TTL實現之前,先來看看RocksDB TTL。
RocksDB 本身也支援TTL, 通過DBWithTTL::Open介面,可以指定每個column_family的過期時間。

每次put資料時,會呼叫DBWithTTLImpl::AppendTS將過期時間append到value最後。

在Compact時通過自定義的TtlCompactionFilter , 去判斷資料是否可以清理。具體參考DBWithTTLImpl::IsStale

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 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中,

 if (m_index_type == INDEX_TYPE_SECONDARY &&
     m_total_index_flags_length > 0) {
   // Reserve space for index flag fields
   unpack_info->allocate(m_total_index_flags_length);

   // Insert TTL timestamp
   if (has_ttl() && ttl_bytes) {
     write_index_flag_field(unpack_info,
                            reinterpret_cast<const uchar *const>(ttl_bytes),
                            Rdb_key_def::TTL_FLAG);
   }
 }

二級索引ttl_col同主鍵保持一致。 對於更新顯式指定的ttl_col列時,所有的二級索引都需要更新,即使此列不在二級索引列中

MyRocks TTL 清理

MyRocks TTL 清理也發生在compact時,由Rdb_compact_filter定義清理動作, 具體參考should_filter_ttl_rec

RocksDB TTL中過期時間和當前時間做比較,而MyRocks TTL 的過期時間是和最老的快照時間(m_snapshot_timestamp )做比較(當沒有快照時,也取當前時間)。

  bool should_filter_ttl_rec(const rocksdb::Slice &key,
                             const rocksdb::Slice &existing_value) const {
    uint64 ttl_timestamp;
    Rdb_string_reader reader(&existing_value);
    if (!reader.read(m_ttl_offset) || reader.read_uint64(&ttl_timestamp)) {
      std::string buf;
      buf = rdb_hexdump(existing_value.data(), existing_value.size(),
                        RDB_MAX_HEXDUMP_LEN);
      // NO_LINT_DEBUG
      sql_print_error("Decoding ttl from PK value failed in compaction filter, "
                      "for index (%u,%u), val: %s",
                      m_prev_index.cf_id, m_prev_index.index_id, buf.c_str());
      abort();
    }

    /*
      Filter out the record only if it is older than the oldest snapshot
      timestamp.  This prevents any rows from expiring in the middle of
      long-running transactions.
    */
    return ttl_timestamp + m_ttl_duration <= m_snapshot_timestamp;
  }

MyRocks TTL 讀過濾

前面講到, RocksDB TTL 過期時間並不嚴格,取決於compaction速度。MyRocks TTL也有類似問題,因此MyRocks引入引數rocksdb_enable_ttl_read_filtering, 當開啟此引數時,過期時間是嚴格的。
每次讀取記錄會呼叫should_hide_ttl_rec判斷此記錄是否過期,當compact操作不及時而沒有清理的過期記錄,在讀取時會被過濾掉。

bool ha_rocksdb::should_hide_ttl_rec(const Rdb_key_def &kd,
                                     const rocksdb::Slice &ttl_rec_val,
                                     const int64_t curr_ts) {
  DBUG_ASSERT(kd.has_ttl());
  DBUG_ASSERT(kd.m_ttl_rec_offset != UINT_MAX);

  /*
    Curr_ts can only be 0 if there are no snapshots open.
    should_hide_ttl_rec can only be called when there is >=1 snapshots, unless
    we are filtering on the write path (single INSERT/UPDATE) in which case
    we are passed in the current time as curr_ts.

    In the event curr_ts is 0, we always decide not to filter the record. We
    also log a warning and increment a diagnostic counter.
  */
  if (curr_ts == 0) {
    update_row_stats(ROWS_HIDDEN_NO_SNAPSHOT);
    return false;
  }

  if (!rdb_is_ttl_read_filtering_enabled() || !rdb_is_ttl_enabled()) {
    return false;
  }

  Rdb_string_reader reader(&ttl_rec_val);

  /*
    Find where the 8-byte ttl is for each record in this index.
  */
   uint64 ts;
   if (!reader.read(kd.m_ttl_rec_offset) || reader.read_uint64(&ts)) {
     /*
       This condition should never be reached since all TTL records have an
       8 byte ttl field in front. Don`t filter the record out, and log an error.
     */
     std::string buf;
     buf = rdb_hexdump(ttl_rec_val.data(), ttl_rec_val.size(),
                       RDB_MAX_HEXDUMP_LEN);
     const GL_INDEX_ID gl_index_id = kd.get_gl_index_id();
     // NO_LINT_DEBUG
     sql_print_error("Decoding ttl from PK value failed, "
                     "for index (%u,%u), val: %s",
                     gl_index_id.cf_id, gl_index_id.index_id, buf.c_str());
     DBUG_ASSERT(0);
     return false;
   }

   /* Hide record if it has expired before the current snapshot time. */
   uint64 read_filter_ts = 0;
 #ifndef NDEBUG
   read_filter_ts += rdb_dbug_set_ttl_read_filter_ts();
 #endif
   bool is_hide_ttl =
       ts + kd.m_ttl_duration + read_filter_ts <= static_cast<uint64>(curr_ts);
   if (is_hide_ttl) {
     update_row_stats(ROWS_FILTERED);
   }
   return is_hide_ttl;
 }

MyRocks TTL 潛在問題

Issue#683 中談到了MyRocks TTL 有個潛在問題, 當更新顯式指定的ttl_col列值時,compact時有可能將新的記錄清理掉,而老的記錄仍然保留,從而有可能讀取到本該不可見的老記錄。此問題暫時還沒有close.

最後

MyRocks TTL 是一個不錯的特性,可以應用在歷史資料清理的場景。相比傳統的Delete資料的方式,更節約空間和CPU資源,同時傳統的Delete還會影響查詢的效率。目前MyRocks TTL 還不夠成熟,還有許多需要改進的地方。


相關文章