TiKV 源碼分析之 PointGet
一、背景介紹
TiDB是一款具有HTAP能力(同時(shí)支持在線事務(wù)處理與在線分析處理 )的融合型分布式數(shù)據(jù)庫產(chǎn)品,具備水平擴(kuò)容或者縮容等重要特性。TiDB 采用多副本+Multi-Raft 算法的方式將數(shù)據(jù)調(diào)度到不同的機(jī)器節(jié)點(diǎn)上,具備較高的可靠性和容災(zāi)能力。TiDB中的存儲(chǔ)層TiKV組件,能夠獨(dú)立于TiDB作為一款分布式KV數(shù)據(jù)庫使用,目前已經(jīng)捐贈(zèng)給CNCF并于2020年正式畢業(yè)。目前vivo公司內(nèi)部的磁盤KV產(chǎn)品采用了開源的TiKV作為存儲(chǔ)層實(shí)現(xiàn), 目前已經(jīng)在公司的不同業(yè)務(wù)產(chǎn)品中有深度實(shí)踐。
TiKV作為一款KV數(shù)據(jù)庫產(chǎn)品,同時(shí)提供了RawAPI和TxnAPI兩套接口:
- RawAPI僅支持最基本的針對(duì)單Key操作的Set/Get/Del及Scan語義
- TxnAPI提供了基于ACID事務(wù)標(biāo)準(zhǔn)的接口,支持多Key寫入的原子性
TxnAPI采用了分布式事務(wù)來保證多Key寫入的原子性,其適用的業(yè)務(wù)場(chǎng)景與RawAPI相比來說更為廣泛。本文后續(xù)內(nèi)容將重點(diǎn)對(duì)PointGet在TiKV側(cè)的執(zhí)行流程進(jìn)行分析,其內(nèi)容涉及到storage和txn模塊。閱讀本文后,讀者將會(huì)深入了解TiKV源碼中Get流程的實(shí)現(xiàn)細(xì)節(jié),包括如何處理讀請(qǐng)求、如何進(jìn)行數(shù)據(jù)定位和讀取、如何實(shí)現(xiàn)事務(wù)隔離級(jí)別等方面,并且能夠更好地理解TiKV的內(nèi)部工作原理和性能優(yōu)化。
二、PointGet介紹
2.1 TiDB視角中的PointGet
PointGet顧名思義即"點(diǎn)查", 它是TiDB中最為基本的幾種算子之一,以下列舉了兩個(gè)常見的PointGet算子的使用場(chǎng)景:
根據(jù)主鍵Id查詢
MySQL [test]> explain select * from user where id = 1024;
+-------------+---------+------+-------------------------------+---------------+
| id | estRows | task | access object | operator info |
+-------------+---------+------+-------------------------------+---------------+
| Point_Get_1 | 1.00 | root | table:user, index:PRIMARY(id) | |
+-------------+---------+------+-------------------------------+---------------+
根據(jù)唯一索引查詢
MySQL [test]> explain select * from users where name = "test";
+-------------+---------+------+-------------------------------+---------------+
| id | estRows | task | access object | operator info |
+-------------+---------+------+-------------------------------+---------------+
| Point_Get_1 | 1.00 | root | table:users, index:name(name) | |
+-------------+---------+------+-------------------------------+---------------+
2.2 純KV用戶視角中的PointGet
部分業(yè)務(wù)沒有完整地使用TiDB組件,而是使用官方提供的client-go/client-rust直接訪問PD和TiKV。
func testGet(k []byte) (error) {
txn, err := client.Begin()
if err != nil {
return err
}
v, err := txn.Get(context.TODO(), k)
if err != nil {
return err
}
fmt.Printf("value of key is: %+v", v)
return nil
}
三、PointGet在TiDB中的實(shí)現(xiàn)
TiDB層為計(jì)算層,其主要職能為MySQL協(xié)議的實(shí)現(xiàn)以及SQL優(yōu)化器和執(zhí)行器的構(gòu)建。客戶端發(fā)起的所有SQL, 都會(huì)經(jīng)過以下生命周期流程:
- Lexer/Parser解析后得到AST,并轉(zhuǎn)換為執(zhí)行計(jì)劃。
- 執(zhí)行計(jì)劃經(jīng)過RBO/CBO后得到優(yōu)化過后的執(zhí)行計(jì)劃。
- 基于執(zhí)行計(jì)劃構(gòu)建執(zhí)行器,其本質(zhì)是不同的算子"套娃",整體構(gòu)成一個(gè)樹型結(jié)構(gòu)。
TiDB的執(zhí)行器基于"火山模型"構(gòu)建,不同的操作算子具有不同的Executor實(shí)現(xiàn):
type Executor interface {
base() *baseExecutor
Open(context.Context) error
Next(ctx context.Context, req *chunk.Chunk) error
Close() error
Schema() *expression.Schema
}
Executor中最為核心的是三個(gè)函數(shù)分別是Open/Next/Close,分別對(duì)應(yīng)算子的初始化、迭代以及收尾邏輯。本文涉及的PointGet算子由PointGetExector實(shí)現(xiàn),其核心的查詢邏輯位于PointGetExector::Next()函數(shù)中。由于相關(guān)邏輯耦合了悲觀事務(wù),以及tikv/client-go中部分Percolator的實(shí)現(xiàn),且不屬于本文重點(diǎn)分析的主要內(nèi)容,這里不展開描述,感興趣的讀者可以自行閱讀。
四、PointGet在TiKV中的實(shí)現(xiàn)
4.1 PointGet接口定義
TiKV和TiDB使用gRPC進(jìn)行通信,其接口契約定義采用了protobuf,我們可以在pingcap/kvproto項(xiàng)目中找到與PointGet相關(guān)的接口定義KvGet如下:
// Key/value store API for TiKV.
service Tikv {
// Commands using a transactional interface.
rpc KvGet(kvrpcpb.GetRequest) returns (kvrpcpb.GetResponse) {}
// ... other api definations ...
}
其中入?yún)etRequest定義如下代碼片段,我們可以看到,TiKV的點(diǎn)查接口除了key之外,還額外需要一個(gè)名為version的參數(shù),即當(dāng)前事務(wù)的start_ts(事務(wù)開始時(shí)間戳),這個(gè)時(shí)間戳是由TiDB在啟動(dòng)事務(wù)時(shí)從Pd組件申請(qǐng)而來。與很多數(shù)據(jù)庫類似,TiKV也采用了MVCC機(jī)制,即同一個(gè)key在底層的存儲(chǔ)中在不同時(shí)刻擁有不同的值,因此要想進(jìn)行查詢,除了key之外,還需要帶上版本。
// A transactional get command. Lookup a value for `key` in the transaction with
// starting timestamp = `version`.
message GetRequest {
Context context = 1;
bytes key = 2;
uint64 version = 3;
}
4.2 TiKV側(cè)調(diào)用堆棧
TiKV作為gRPC的Server端,提供了KvGet接口的實(shí)現(xiàn),相關(guān)調(diào)用堆棧為:
+TiKV::kv_get (grpc-poll-thread)
+future_get
+Storage::get
+Storage::snapshot (readpool-thread)
+SnapshotStore::get
+PointGetterBuilder::build
+PointGetter::get
在一次KvGet調(diào)用中,函數(shù)執(zhí)行流程會(huì)在grpc-poll-thread和readpool-thread中切換,其中前者為gRPC的poll thread,請(qǐng)求在被路由到Storage層后,會(huì)根據(jù)讀寫屬性路由到不同的線程池中,只讀語義的Get/Scan請(qǐng)求都會(huì)被路由到ReadPool中執(zhí)行,這是一個(gè)特定用于處理只讀請(qǐng)求的線程池。
4.3 Read through locks介紹
在分析后續(xù)邏輯之前,我們需要對(duì)Read through locks機(jī)制先做個(gè)簡(jiǎn)單介紹。TiKV使用Percoaltor模型來實(shí)現(xiàn)分布式事務(wù),同時(shí)也引入了MVCC機(jī)制。然而其實(shí)現(xiàn)和傳統(tǒng)的MVCC實(shí)現(xiàn)略有差異:TiKV的讀取過程中若遇到其他事務(wù)提交時(shí)寫入的Lock, 則需要等待或者嘗試解鎖,這會(huì)阻塞讀取直到事務(wù)狀態(tài)確定,一定程度上會(huì)損失并發(fā)性能。
然而在一些場(chǎng)景(如SecondaryLocks),在Key對(duì)應(yīng)的鎖仍然存在的情況下,我們已經(jīng)知道相關(guān)事務(wù)的最終狀態(tài)(提交或回滾)。如果我們將這些事務(wù)的最終狀態(tài)與查詢請(qǐng)求一起發(fā)送給TiKV, 那么TiKV可以根據(jù)這些事務(wù)狀態(tài)來確定能否在有Lock的情況下安全讀取,避免不必要的等待, 即本小節(jié)提到的Read through lock機(jī)制。
Context是所有的TiKV請(qǐng)求都會(huì)攜帶的上下文信息,為了實(shí)現(xiàn)Read through lock,
https://github.com/pingcap/kvproto/pull/833 這個(gè)PR在Context中添加了如下字段:
message Context {
// Read requests can ignore locks belonging to these transactions because either
// these transactions are rolled back or theirs commit_ts > read request's start_ts.
repeated uint64 resolved_locks = 13;
// Read request should read through locks belonging to these transactions because these
// transactions are committed and theirs commit_ts <= read request's start_ts.
repeated uint64 committed_locks = 22;
}
其中resolved_locks用于記錄讀取時(shí)可以忽略的鎖,這些鎖對(duì)應(yīng)的事務(wù)可能已被回滾,或者已成功提交但CommitTS大于當(dāng)前的讀StartTS,直接忽略這些鎖也不影響快照一致性。
其中committed_locks則用于記錄邏輯上已被正確提交但物理上Lock還未被清理的、且CommitTS小于當(dāng)前讀取使用的StartTS的事務(wù)。由于事務(wù)本質(zhì)上已經(jīng)被提交,因此讀取時(shí)可以不需要返回等待,只需要通過Lock查詢DefaultCF中的數(shù)據(jù)即可。
通過Read through lock機(jī)制,TiKV可以在一些Lock尚未被清理的情況下直接返回正確的結(jié)果,避免了客戶端層面的Wait和ResolveLock,其具體實(shí)現(xiàn)在后續(xù)小節(jié)會(huì)涉及到。
4.4 Storage::get流程分析
下方代碼塊是經(jīng)過精簡(jiǎn)過后的偽代碼,主要標(biāo)注了get流程中一些比較關(guān)鍵的步驟。
pub fn get(&self, mut ctx: Context, key: Key, start_ts: TimeStamp) -> impl Future<Output = ... >> {
self.read_pool.spawn_handle(async move {
// 1. 創(chuàng)建創(chuàng)建快照需要的上下文
let snap_ctx = prepare_snap_ctx(...);
// 2. 申請(qǐng)一個(gè)快照
let snapshot = Self::with_tls_engine(|engine| Self::snapshot(engine, snap_ctx)).await?;
// 3. 創(chuàng)建SnapshotStore對(duì)象并執(zhí)行查詢
let snap_store = SnapshotStore::new(...);
let result = snap_store.get(key);
// 4. 更新Metrics和Stats統(tǒng)計(jì)信息
});
}
4.4.1 準(zhǔn)備快照上下文
prepare_snap_ctx顧名思義即準(zhǔn)備用于創(chuàng)建快照所需要的上下文對(duì)象,即SnapContext對(duì)象,其完整定義如下:
pub struct SnapContext<'a> {
pub pb_ctx: &'a Context,
pub read_id: Option<ThreadReadId>,
// When start_ts is None and `stale_read` is true, it means acquire a snapshot without any
// consistency guarantee.
pub start_ts: Option<TimeStamp>,
// `key_ranges` is used in replica read. It will send to
// the leader via raft "read index" to check memory locks.
pub key_ranges: Vec<KeyRange>,
// Marks that this snapshot request is allowed in the flashback state.
pub allowed_in_flashback: bool,
}
fn prepare_snap_ctx<'a>(...) -> Result<SnapContext<'a>> {
if !pb_ctx.get_stale_read() {
concurrency_manager.update_max_ts(start_ts);
}
if need_check_locks(isolation_level) {
concurrency_manager.read_key_check(...)
}
let mut snap_ctx = SnapContext {...};
if need_check_locks_in_replica_read(pb_ctx) {
snap_ctx.key_ranges = ...
}
}
prepare_snap_ctx只需要?jiǎng)?chuàng)建一個(gè)SnapContext對(duì)象,但目前實(shí)現(xiàn)中多出了如下判斷或操作,絕大部分都源于TiKV5.0中的AsyncCommit特性所需。
1.當(dāng)本次讀取非StaleRead時(shí),需要將當(dāng)前讀取請(qǐng)求的start_ts與CurrencyManager中的max_ts進(jìn)行比較,并將二者中的最大值更新為全局max_ts。這一操作用于保證異步提交事務(wù)計(jì)算出來的MinCommitTs不會(huì)破壞快照一致性。
2. 若當(dāng)前的隔離級(jí)別是SnapshotIsolation或者RcCheckTs時(shí), 則需要額外檢查CurrencyManager中的內(nèi)存鎖。如果存在鎖且當(dāng)前start_ts大于鎖中的MinCommitTs,TiKV會(huì)直接拒絕本次讀取請(qǐng)求。其原因在于AsyncCommit事務(wù)Prewrite結(jié)束之前需要暫時(shí)阻止使用更新的start_ts發(fā)起的快照讀,否則會(huì)導(dǎo)致正在異步提交的事務(wù)計(jì)算出的MinCommitTS無法滿足快照一致性。
4.4.2 向Engine申請(qǐng)Snapshot
Engine是TiKV中對(duì)上層存儲(chǔ)組件的一次抽象,所有實(shí)現(xiàn)了Engine Trait的具體實(shí)現(xiàn)都可以作為TiKV中的存儲(chǔ)層組件。目前TiKV中已經(jīng)實(shí)現(xiàn)了BTreeEngine/MockEngine/RocksEngine/RaftKV等多個(gè)實(shí)現(xiàn)。
pub trait Engine: Send + Clone + 'static {
// 獲取用于查詢的快照
fn async_snapshot(&mut self, ctx: SnapContext<'_>) -> Self::SnapshotRes;
// 提交寫入的Mutation
fn async_write(&self,ctx: &Context,batch: WriteData,subscribed: u8, on_applied: Option<OnAppliedCb>) -> Self::WriteRes;
// 其他接口...
}
Engine的接口定義中與讀寫相關(guān)的接口分別是async_snapshot和async_write。目前TiKV中的默認(rèn)Engine實(shí)現(xiàn)為RaftKV,即一個(gè)基于Raftstore的實(shí)現(xiàn)。在RaftKV中,所有的寫入都會(huì)通過Raft狀態(tài)機(jī)進(jìn)行propose/commit/apply流程,用戶可以基于訂閱機(jī)制獲得這3個(gè)事件的通知從而做出不同處理,默認(rèn)情況下,TiKV會(huì)在一次寫入請(qǐng)求被RaftLeader apply成功后返回用戶。而讀取操作則需要遵循先行一致性讀取,在早期版本中,一次讀取需要通過Raft狀態(tài)機(jī)進(jìn)行一次ReadIndex才能進(jìn)行,在新版中TiKV實(shí)現(xiàn)了基于租約的LeaseRead, 簡(jiǎn)化了讀取流程。本次介紹的PointGet讀取流程中,會(huì)涉及到使用async_snapshot獲取一個(gè)Engine在當(dāng)前時(shí)刻的快照,并基于快照進(jìn)行讀取。
TiKV按照KeyRange將Key拆分為不同的Region, 每個(gè)Region都是一個(gè)RaftGroup,且擁有獨(dú)立的狀態(tài)機(jī)推進(jìn)運(yùn)轉(zhuǎn)。因此,RaftKV-Engine中async_snapshot返回的是一個(gè)名為RegionSnapshot的對(duì)象,其定義如下:
pub struct RegionSnapshot<S: Snapshot> {
snap: Arc<S>,
region: Arc<Region>,
apply_index: Arc<AtomicU64>,
pub term: Option<NonZeroU64>,
pub txn_extra_op: TxnExtraOp,
// `None` means the snapshot does not provide peer related transaction extensions.
pub txn_ext: Option<Arc<TxnExt>>,
pub bucket_meta: Option<Arc<BucketMeta>>,
}
RegionSnapshot本質(zhì)是對(duì)底層的KV引擎RocksDB層面的快照的封裝,其邏輯視圖如下:
4.4.3 MVCC實(shí)現(xiàn)和快照隔離級(jí)別實(shí)現(xiàn)
前文提到的Engine::async_snapshot接口返回的快照本質(zhì)是Engine在當(dāng)下時(shí)刻的快照,并不等于事務(wù)層面的MVCC快照,因此在具體查詢時(shí),需要配合StartTS進(jìn)行使用。TiKV中封裝了一個(gè)SnapshotStore用于輔助MVCC層面的查詢。其定義如下:
pub struct SnapshotStore<S: Snapshot> {
snapshot: S,
start_ts: TimeStamp,
isolation_level: IsolationLevel,
fill_cache: bool,
bypass_locks: TsSet,
access_locks: TsSet,
check_has_newer_ts_data: bool,
point_getter_cache: Option<PointGetter<S>>,
}
SnapshotStore中集合了從Engine獲取的快照和客戶端請(qǐng)求附帶的StartTS, 因此可以被認(rèn)為是一個(gè)MVCC層面的快照。用戶對(duì)SnapshotStore發(fā)起的點(diǎn)查會(huì)被委托給內(nèi)部的PointGetter。
// PointGetter::get
pub fn get(&mut self, user_key: &Key) -> Result<Option<Value>> {
fail_point!("point_getter_get");
// 根據(jù)當(dāng)前請(qǐng)求使用的隔離級(jí)別判定是否需要檢查鎖
if need_check_locks(self.isolation_level) {
// 如果需要檢查鎖且鎖存在,則需要根據(jù)判定鎖
if let Some(lock) = self.load_and_check_lock(user_key)? {
return self.load_data_from_lock(user_key, lock);
}
}
// Percoaltor正常讀取流程:從WriteCF中找到<=start_ts中最大的commit_ts,并基于其存儲(chǔ)的start_ts到DefaultCF中讀取
self.load_data(user_key)
}
在執(zhí)行查詢前,TiKV需要根據(jù)當(dāng)前請(qǐng)求的隔離級(jí)別判定是否需要檢查鎖。
pub fn need_check_locks(iso_level: IsolationLevel) -> bool {
matches!(iso_level, IsolationLevel::Si | IsolationLevel::RcCheckTs)
}
TiKV支持SnapshotIsolation/ReadCommitted/ReadCommittedCheckTs三種隔離級(jí)別,其中前兩種需要檢查鎖。其原因在于LockCf中的鎖是由于事務(wù)在2PC的第一階段提交階段寫入的,事務(wù)的最終狀態(tài)無法確定,如果不檢查鎖直接讀取,那么可能導(dǎo)致快照讀取被破壞。
fn load_and_check_lock(&mut self, user_key: &Key) -> Result<Option<Lock>> {
// 從LockCf查詢?cè)揔ey的鎖信息
let lock_value = self.snapshot.get_cf(CF_LOCK, user_key)?;
if let Some(ref lock_value) = lock_value {
let lock = Lock::parse(lock_value)?;
// 如果存在鎖則檢查鎖是否沖突
if let Err(e) = Lock::check_ts_conflict(
Cow::Borrowed(&lock),
user_key,
self.ts,
&self.bypass_locks,
self.isolation_level,
)
// ...
}
其中Lock::check_ts_conflict的實(shí)現(xiàn)中會(huì)根據(jù)當(dāng)前的事務(wù)隔離級(jí)別進(jìn)行判定,不同的隔離級(jí)別的判定邏輯略有差異。由于本文篇幅有限,這里只分析我們常用的快照隔離級(jí)別的實(shí)現(xiàn)。
fn check_ts_conflict_si(lock: Cow<'_, Self>, key: &Key, ts: TimeStamp, bypass_locks: &TsSet ) -> Result<()> {
if lock.ts > ts
|| lock.lock_type == LockType::Lock
|| lock.lock_type == LockType::Pessimistic
{
return Ok(());
}
if lock.min_commit_ts > ts {
// Ignore lock when min_commit_ts > ts
return Ok(());
}
if bypass_locks.contains(lock.ts) {
return Ok(());
}
let raw_key = key.to_raw()?;
if ts == TimeStamp::max() && raw_key == lock.primary && !lock.use_async_commit {
// When `ts == TimeStamp::max()` (which means to get latest committed version
// for primary key), and current key is the primary key, we ignore
// this lock.
return Ok(());
}
// There is a pending lock. Client should wait or clean it.
Err(Error::from(ErrorInner::KeyIsLocked(
lock.into_owned().into_lock_info(raw_key),
)))
}
- 當(dāng)lock.ts > ts時(shí),當(dāng)前查詢請(qǐng)求可以直接忽略這個(gè)鎖。其原因在于當(dāng)前的lock是由具有更高start_ts的事務(wù)寫入,因此即便這個(gè)事務(wù)后續(xù)被提交,其commit_ts一定大于當(dāng)前的start_ts,其新寫入的數(shù)據(jù)是不可見的,不會(huì)破壞快照一致性。
- 當(dāng)lock_type==Lock時(shí),也可以直接忽略這個(gè)鎖突, 其原因在于LockType::Lock是由于創(chuàng)建索引產(chǎn)生,它只用于指示被鎖定但不會(huì)修改數(shù)據(jù),因此也可以直接被忽略。
- 當(dāng)lock_type==Pessistics時(shí),也可以直接忽略這個(gè)鎖突,LockType::Pessistics是由于悲觀事務(wù)執(zhí)行DML時(shí)寫入,并未進(jìn)行到事務(wù)提交階段,即使這個(gè)事務(wù)很快被提交,由于其commit_ts也一定大于當(dāng)前讀取的start_ts, 直接忽略并不會(huì)影響快照一致性。
- 當(dāng)lock.min_commit_ts > ts時(shí),也可以直接忽略這個(gè)鎖,其原因在于它能保證這個(gè)AsyncCommit事務(wù)的最終計(jì)算出的commit_ts一定大于ts,即使這個(gè)事務(wù)會(huì)被提交,也不會(huì)破壞快照一致性。
- 當(dāng)bypass_locks中包含了當(dāng)前鎖的start_ts時(shí), 也可以直接忽略這個(gè)鎖。bypass_locks即前面Read through locks小節(jié)中提到了resloved_locks,這些鎖雖然存在,但它們對(duì)應(yīng)事務(wù)要么已經(jīng)被回滾,要么使用了大于當(dāng)前讀取start_ts的commit_ts進(jìn)行提交,無論是哪種情況都不會(huì)破壞快照一致性。
- 其他情況則需要返回KeyIsLocked錯(cuò)誤給客戶端,客戶端收到這個(gè)錯(cuò)誤后則會(huì)檢查這個(gè)鎖的過期時(shí)間,如果鎖尚未過期則需要做wait,否則會(huì)嘗試進(jìn)行解鎖恢復(fù)這個(gè)事務(wù)的狀態(tài)。
若check_ts_conflict_si返回KeyIsLocked或其他錯(cuò)誤后,TiKV會(huì)額外檢查access_locks里是否包含該鎖,如果該鎖存在,則KeyIsLocked錯(cuò)誤則會(huì)被忽略,同時(shí)鎖會(huì)被直接返回,外層函數(shù)可以通過鎖找到start_ts從而直接讀取DefaultCF中的數(shù)據(jù)。這里的access_locks即Read through locks中的committed_locks,即已經(jīng)知曉被提交的且commit_ts小于當(dāng)前快照讀start_ts的事務(wù),在這種情況下,直接讀取DefaultCF是一個(gè)超前但安全的操作,原因在在于一旦這個(gè)Lock被Resolve,用戶通過新的commit_ts可以定位到同一個(gè)start_ts。
if let Err(e) = Lock::check_ts_conflict(Cow::Borrowed(&lock),user_key,self.ts,&self.bypass_locks,self.isolation_level) {
if self.access_locks.contains(lock.ts) {
return Ok(Some(lock));
}
Err(e.into())
}
在不存在Key被鎖定或沖突,且沒有使用Read through locks讀取后,TiKV則會(huì)進(jìn)行正常的Percolator讀取流程,即從WriteCF中找到<=start_ts中最大的commit_ts,并基于其存儲(chǔ)的start_ts到DefaultCF中讀取。
4.4.4 RegionSnapshot的Get實(shí)現(xiàn)
RegionSnapshot::get的實(shí)現(xiàn)相對(duì)比較簡(jiǎn)單,邏輯如下:
fn get_value_cf_opt(&self, opts: &ReadOptions, cf: &str, key: &[u8]) -> EngineResult<Option<Self::DbVector>> {
// 1. 檢查查詢的key是否在Region的范圍內(nèi), 如果不在則直接返回錯(cuò)誤。
check_key_in_range(key,self.region.get_id(),self.region.get_start_key(),self.region.get_end_key()).map_err(|e| EngineError::Other(box_err!(e)))?;
// 2. 基于查詢的key拼接出raftstore層面的DataKey (raftstore在寫入時(shí)會(huì)給用戶key前添加一個(gè)前綴'z')。
let data_key = keys::data_key(key);
// 3. 使用內(nèi)部的RocksSnapshot查詢RocksDB獲取key對(duì)應(yīng)的值。
self.snap.get_value_cf_opt(opts, cf, &data_key).map_err(|e| self.handle_get_value_error(e, cf, key))
}
4.4.5 RocksDB/Titan的Get實(shí)現(xiàn)
TiKV使用rust-rocksdb庫使用FFI實(shí)現(xiàn)與RocksDB C-API的交互,RocksSnapshot::get會(huì)通過crocksdb_get_pinned_cf將查詢接口委托給底層的RocksDB。值得注意的是,TiKV使用的并不是官方的RocksDB,而是自行維護(hù)的一個(gè)整合了Titan插件的版本。Titan是一個(gè)受WiscKey論文啟發(fā)而創(chuàng)建的項(xiàng)目,其主要目的是將存入RocksDB的大Value從LSM-Tree中分離出來,存儲(chǔ)到額外的Blob文件中,從而達(dá)到減小寫放大的目的。
本小節(jié)我們著重分析一下TitanDB中一次查詢的實(shí)現(xiàn)過程(做過大量精簡(jiǎn)):
Status TitanDBImpl::GetImpl(const ReadOptions& options,
ColumnFamilyHandle* handle, const Slice& key,
PinnableSlice* value) {
// 先查詢RocksDB
s = db_impl_->GetImpl(options, key, gopts);
// 如果Key的Value不存在或者不是BlobIndex, 則直接返回
if (!s.ok() || !is_blob_index) return s;
// Value是BlobIndex,說明這是一個(gè)索引,還需要額外查詢BlobStorage
BlobIndex index;
s = index.DecodeFrom(value);
assert(s.ok());
if (!s.ok()) return s;
BlobRecord record;
PinnableSlice buffer;
mutex_.Lock();
// 根據(jù)索引查詢BlobStorage
auto storage = blob_file_set_->GetBlobStorage(handle->GetID()).lock();
mutex_.Unlock();
if (s.ok()) {
value->Reset();
value->PinSelf(record.value);
}
return s;
}
五、總結(jié)
- TiKV對(duì)數(shù)據(jù)存儲(chǔ)層的職能進(jìn)行了非常合理的抽象,通過Engine/Snapshot/Iterator等trait定義實(shí)現(xiàn)了存儲(chǔ)層與上層的解耦。
- TiKV在RocksDB提供的多列族原子性寫入能力之上實(shí)現(xiàn)了Percolator模型,提供了分布式事務(wù)和MVCC等能力,并實(shí)現(xiàn)了AsyncCommit和1PC等改善了事務(wù)提交延遲。
- TiKV實(shí)現(xiàn)了一個(gè)基于RocksDB的KV分離插件titan, 借鑒了Wisckey的思想將大Value從LSM-Tree中分離,在大Value的業(yè)務(wù)場(chǎng)景下能夠通過降低寫放大改善性能。
- 從PointGet的實(shí)現(xiàn)我們可以看到在使用了MVCC的情況下,查詢時(shí)遇到前一事務(wù)Prewrite產(chǎn)生的Lock仍然需要等待Resolve,因此在AsyncCommit開啟的前提下,業(yè)務(wù)開發(fā)需要盡量避免設(shè)計(jì)事務(wù)提交后即刻發(fā)起查詢的場(chǎng)景,此外也要盡量避免由于大事務(wù)提交延遲高影響相關(guān)的查詢。
參考資料: