天天看點

MyRocks TTL特性介紹概述RocksDB TTLMyRocks TTL 實作MyRocks TTL 清理MyRocks TTL 讀過濾MyRocks TTL 潛在問題最後

概述

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 還不夠成熟,還有許多需要改進的地方。