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