概述
數(shù)據(jù)庫(kù)系統(tǒng)一般采用WAL(write ahead log)技術(shù)來實(shí)現(xiàn)原子性和持久性,MySQL也不例外。WAL中記錄事務(wù)的更新內(nèi)容,通過WAL將隨機(jī)的臟頁(yè)寫入變成順序的日志刷盤,可極大提升數(shù)據(jù)庫(kù)寫入性能,因此,WAL的寫入能力決定了數(shù)據(jù)庫(kù)整體性能的上限,尤其是在高并發(fā)時(shí)。
在MYSQL 8以前,寫日志被保護(hù)在一把大鎖之下,本來并行事務(wù)日志寫入被人為串行化處理。雖簡(jiǎn)化了邏輯,但也極大限制了整體的性能表現(xiàn)。8.0很大的一部分工作便是將日志系統(tǒng)并行化。
日志并行化
日志并行化的思路也很簡(jiǎn)單:將寫日志拆分為兩個(gè)過程:
- 從內(nèi)存log buffer中為日志預(yù)留空間
2. 將日志內(nèi)容拷貝至1預(yù)留的空間
而在這兩個(gè)步驟中,只需要步驟1保證在多并發(fā)并發(fā)預(yù)留空間時(shí)的正確性即可,確保并發(fā)線程預(yù)留的日志空間不會(huì)交叉。一旦預(yù)留成功,步驟2各并發(fā)線程可互不干擾地執(zhí)行拷貝至自己的預(yù)留空間即可,這天然可并發(fā)。
而在步驟1中也可以使用原子變量來代替代價(jià)較高鎖實(shí)行預(yù)留,在mysql 8實(shí)現(xiàn)中,其實(shí)就兩行代碼:
Log_handle log_buffer_reserve(log_t &log, size_t len) { ... const sn_t start_sn = log.sn.fetch_add(len); const sn_t end_sn = start_sn + len; ...}
可以看到,只需要一個(gè)原子變量log.sn記錄當(dāng)前分配的位置信息,下次分配時(shí)更新該log.sn即可,非常簡(jiǎn)潔優(yōu)雅。
8.0中引入的并行日志系統(tǒng)雖然很美好,但是也會(huì)帶來一些小麻煩,我們下面會(huì)詳細(xì)描述其引入的日志空洞問題并闡述其解決方案。
Log Buffer空洞問題
Mysql 8.0中使用了無鎖預(yù)分配的方式可以使MTR并行地將WAL日志寫入到Log Buffer,提升性能。但這樣勢(shì)必會(huì)帶來Redo Log Buffer的空洞問題,如下:
上圖中,3個(gè)線程分別分配了對(duì)應(yīng)的redo buffer,線程1和3已經(jīng)完成了wal日志內(nèi)容的拷貝,而線程2則還在拷貝中,此時(shí)寫入線程最多只能將thread-1的redo log寫入日志文件。 為此,MySQL 8.0中引入了 Link_buf 。
Link_buf原理
Link_buf用于輔助表示其他數(shù)據(jù)結(jié)構(gòu)的使用情況,在Link_buf中,如果一個(gè)索引位置index處存儲(chǔ)的是非0值n,則表示Link_buf輔助標(biāo)記的那個(gè)數(shù)據(jù)結(jié)構(gòu),從index開始后面n個(gè)元素已被占用。
template <typename Position = uint64_t>class Link_buf { private: ... size_t m_capacity; std::atomic<Distance> *m_links; alignas(INNOBASE_CACHE_LINE_SIZE) std::atomic<Position> m_tail;};
Link_buf是一個(gè)定長(zhǎng)數(shù)組,且保證數(shù)組的每個(gè)元素的更新是原子操作的。以環(huán)形的方式復(fù)用已經(jīng)釋放的空間。
同時(shí)Link_buf內(nèi)部維護(hù)了一個(gè)變量 m_tail 表示當(dāng)前最大可達(dá)的LSN。
Innodb日志系統(tǒng)中為L(zhǎng)og Buffer維護(hù)了兩個(gè)Link_buf類型的變量 recent_written 和 recent_closed 。示意圖如下:
上圖中,共有兩處日志空洞,起始的LSN為lsn1與lsn3,均有4個(gè)字節(jié)。而lsn2處的redo log已經(jīng)寫入,共3個(gè)字節(jié)。在 recent_written 中,lsn1開始處的4個(gè)atomic均是0,lsn3同樣如此,而lsn2處開始的存儲(chǔ)的則是3,0,0表示從該位置起的3個(gè)字節(jié)已經(jīng)成功寫入了redo日志。
接下來當(dāng)lsn1處的空洞被填充后,Link_buf中該處對(duì)應(yīng)的內(nèi)容就會(huì)被設(shè)置,如下:
同理,當(dāng)lsn3處的空洞也被填充后,狀態(tài)變成下面這樣:
Link_buf實(shí)現(xiàn)
初始化
bool log_sys_init(...){ ... log_allocate_recent_written(log); ...}?constexpr ulong INNODB_LOG_RECENT_WRITTEN_SIZE_DEFAULT = 1024 * 1024;ulong srv_log_recent_written_size = INNODB_LOG_RECENT_WRITTEN_SIZE_DEFAULT;?static void log_allocate_recent_written(log_t &log) { // 默認(rèn)值為1MB log.recent_written = Link_buf<lsn_t>{srv_log_recent_written_size};}?// Link_buf構(gòu)造template <typename Position>Link_buf<Position>::Link_buf(size_t capacity) : m_capacity(capacity), m_tail(0){ ... m_links = UT_NEW_ARRAY_NOKEY(std::atomic<Distance>, capacity); for (size_t i = 0; i < capacity; ++i) { m_links[i].store(0); }}
從構(gòu)造函數(shù)中可以看到,LinkBuf內(nèi)核心成員是一維數(shù)組,數(shù)組的成員類型是原子類型的Distance(uint64_t),數(shù)組成員個(gè)數(shù)則由創(chuàng)建者決定,如Innodb中為recent_written創(chuàng)建的LinkBuf的數(shù)組成員個(gè)數(shù)為1MB,而為recent_closed創(chuàng)建的LinkBuf的數(shù)組成員個(gè)數(shù)為2MB。
同時(shí),創(chuàng)建完成后會(huì)將數(shù)組的每個(gè)成員初始化為0。
mtr log拷貝完成
mtr在commit時(shí)會(huì)將其運(yùn)行時(shí)產(chǎn)生的所有redo log拷貝至Innodb全局的redo log buffer,這借助了 mtr_write_log_t 對(duì)象來完成,且每次拷貝按照block為單位進(jìn)行。需要說明的是:一個(gè)mtr中可能存在多個(gè)block來存儲(chǔ)mtr運(yùn)行時(shí)產(chǎn)生的redo log,每個(gè)block拷貝完成后均觸發(fā)一次Link_buf的更新。
struct mtr_write_log_t { bool operator()(const mtr_buf_t::block_t *block) { ... // 拷貝完成后觸發(fā)LinkBuf更新 log_buffer_write_completed(*log_sys, m_handle, start_lsn, end_lsn); }}?void log_buffer_write_completed(log_t &log, const Log_handle &handle, lsn_t start_lsn, lsn_t end_lsn) { ... // 更新本次寫入的內(nèi)容范圍對(duì)應(yīng)的LinkBuf內(nèi)特定的數(shù)組項(xiàng)值 log.recent_written.add_link(start_lsn, end_lsn);}?template <typename Position>inline size_t Link_buf<Position>::slot_index(Position position) const { return position & (m_capacity - 1);}?template <typename Position>inline void Link_buf<Position>::add_link(Position from, Position to) { // 定位本次寫入的內(nèi)容范圍所在數(shù)組項(xiàng)index // 算法是將起始lsn(@from)對(duì)數(shù)組容量取模,即from % capacity const auto index = slot_index(from); auto &slot = m_links[index]; slot.store(to - from);}
在這里會(huì)找到start_lsn對(duì)應(yīng)的slot,并在該slot內(nèi)設(shè)置值為end_lsn - start_lsn,記錄該位置處已寫入的內(nèi)容數(shù)量。
log_advance_ready_for_write_lsn
Innodb將redo log buffer內(nèi)容寫入日志文件時(shí)需要保證不能存在空洞,即在寫入前需要獲得當(dāng)前最大的無空洞lsn。這同樣依賴LinkBuf。在后臺(tái)寫日志線程 log_writer 的 log_advance_ready_for_write_lsn 函數(shù)中完成。
void log_writer(log_t *log_ptr) { ... for (uint64_t step = 0;; ++step) { (void)log_advance_ready_for_write_lsn(log); }}?bool log_advance_ready_for_write_lsn(log_t &log) { const lsn_t write_lsn = log.write_lsn.load(); const auto write_max_size = srv_log_write_max_size;? auto stop_condition = [&](lsn_t prev_lsn, lsn_t next_lsn) { return (next_lsn - write_lsn >= write_max_size); }; const lsn_t previous_lsn = log_buffer_ready_for_write_lsn(log);? if (log.recent_written.advance_tail_until(stop_condition)) { const lsn_t previous_lsn = log_buffer_ready_for_write_lsn(log); return (true); } else { return (false); }}
這里的關(guān)鍵在于函數(shù) Link_buf::advance_tail_until ,即推進(jìn)Link_buf::m_tail。
bool Link_buf<Position>::next_position(Position position, Position &next) { const auto index = slot_index(position); auto &slot = m_links[index]; const auto distance = slot.load(); next = position + distance; return distance == 0;}?bool Link_buf<Position>::advance_tail_until(Stop_condition stop_condition) { auto position = m_tail.load(); while (true) { Position next; bool stop = next_position(position, next); if (stop || stop_condition(position, next)) { break; } /* 回收slot */ claim_position(position); position = next; } if (position > m_tail.load()) { m_tail.store(position); return true; } else { return false; }}
這里的原理也比較簡(jiǎn)單,可以用下面的圖來表示:
簡(jiǎn)單來說,就是從上次尾部位置(m_tail)開始,順序遍歷數(shù)組,如果該項(xiàng)不為0,則推進(jìn)m_tail,否則意味著出現(xiàn)了空洞,就不能再往下推進(jìn)了。






