LevelDB 源码剖析(三)公共基础:内存管理、数值编码、Env家族、文件操作
文章目錄
- 內存管理
- Arena
- 結構
- 內存分配
- 內存使用率統計
- TCMalloc
- Env家族
- PosixEnv
- EnvWrapper
- InMemoryEnv
- 文件操作
- SequentialFile
- WritableFile
- RandomAccessFile
- 數值編碼
- 字節序
- 定長編碼
- 變長編碼
內存管理
Arena
內存頻繁創建/釋放的地方就會有內存池的出現,LevelDB 也不例外。在 Memtable 組件中,會有大量內存創建(數據持續 put)和釋放(dump 到磁盤后內存結束),于是 LevelDB 通過 Arena 來管理內存。
它有什么好處呢?
-
提升性能:內存申請本身就需要占用一定的資源,消耗空間與時間。而 Arena 內存池的基本思路就是預先申請一大塊內存,然后多次分配給不同的對象,從而減少 malloc 或 new 的調用次數。
-
提高內存利用率:頻繁進行內存的申請與釋放易造成內存碎片。即內存余量雖然夠用,但由于缺乏足夠大的連續空閑空間,從而造成申請一段較大的內存不成功的情況。而 Arena 具有整塊內存的控制權,用戶可以任意操作這段內存,從而避免內存碎片的產生。
結構
// https://github.com/google/leveldb/blob/master/util/arena.hclass Arena {public:Arena();Arena(const Arena&) = delete;Arena& operator=(const Arena&) = delete;~Arena();char* Allocate(size_t bytes); malloc.char* AllocateAligned(size_t bytes);size_t MemoryUsage() const {return memory_usage_.load(std::memory_order_relaxed);}private:char* AllocateFallback(size_t bytes);char* AllocateNewBlock(size_t block_bytes);char* alloc_ptr_;size_t alloc_bytes_remaining_;std::vector<char*> blocks_;std::atomic<size_t> memory_usage_; };其組成如下:
- 成員變量
- alloc_ptr_ :當前已使用內存的指針
- blocks_ :實際分配的內存池_
- alloc_bytes_remaining_ :剩余內存字節數
- memory_usage_ :記錄內存的使用情況
- kBlockSize :一個塊大小(默認4k)
- 成員函數
-
AllocateFallback :按需分配內存,可能會有內存浪費。
-
AllocateAligned :分配偶數大小的內存,主要是 skiplist 節點時,目的是加快訪問。
-
MemoryUsage:統計內存使用量。
-
內存分配
Arena 中與內存分配有關的兩個接口函數:Allocate 與 AllocateAligned。
首先我們來看看 Allocate 與它依賴的兩個函數 AllocateFallback 與 AllocateNewBlock。代碼如下:
// https://github.com/google/leveldb/blob/master/util/arena.hinline char* Arena::Allocate(size_t bytes) {assert(bytes > 0);if (bytes <= alloc_bytes_remaining_) {char* result = alloc_ptr_;alloc_ptr_ += bytes;alloc_bytes_remaining_ -= bytes;return result;}return AllocateFallback(bytes); }首先,對于 Allocate 來說,它存在兩種情況
AllocateFallback 用于申請一個新 Block 內存空間,然后分配需要的內存并返回。因此當前 Block 剩余空閑內存就不可避免地浪費了。接著看看 AllocateFallback 的邏輯
// https://github.com/google/leveldb/blob/master/util/arena.ccchar* Arena::AllocateFallback(size_t bytes) {if (bytes > kBlockSize / 4) {char* result = AllocateNewBlock(bytes);return result;}alloc_ptr_ = AllocateNewBlock(kBlockSize);alloc_bytes_remaining_ = kBlockSize;char* result = alloc_ptr_;alloc_ptr_ += bytes;alloc_bytes_remaining_ -= bytes;return result; }AllocateFallback 的使用也包括兩種情況:
對于 Block 的分配,這里又調用了 AllocateNewBlock,其邏輯如下:
// https://github.com/google/leveldb/blob/master/util/arena.ccchar* Arena::AllocateNewBlock(size_t block_bytes) {char* result = new char[block_bytes];blocks_.push_back(result);memory_usage_.fetch_add(block_bytes + sizeof(char*),std::memory_order_relaxed);return result; }這里的邏輯非常簡單,其通過 new,申請了一段大小為 block_bytes 的內存空間,并將這塊空間的地址存儲到 blocks_ 中,之后更新當前可用的總空間大小后將空間首地址返回。
講解完了 Allocate ,我們接著再來看看 AllocateAligned。雖然它也用于內存分配,但不同點在于它考慮了內存分配時的內存對齊問題(進行內存分配所返回的起始地址應為 b / 8 的倍數,在這里 b 代表操作系統平臺的位數)。代碼如下:
// https://github.com/google/leveldb/blob/master/util/arena.ccchar* Arena::AllocateAligned(size_t bytes) {const int align = (sizeof(void*) > 8) ? sizeof(void*) : 8;static_assert((align & (align - 1)) == 0,"Pointer size should be a power of 2");size_t current_mod = reinterpret_cast<uintptr_t>(alloc_ptr_) & (align - 1);size_t slop = (current_mod == 0 ? 0 : align - current_mod);size_t needed = bytes + slop;char* result;if (needed <= alloc_bytes_remaining_) {result = alloc_ptr_ + slop;alloc_ptr_ += needed;alloc_bytes_remaining_ -= needed;} else {// AllocateFallback always returned aligned memoryresult = AllocateFallback(bytes);}assert((reinterpret_cast<uintptr_t>(result) & (align - 1)) == 0);return result; }由于主流的服務器平臺采用 64 位的操作系統,64 位操作系統的指針同樣為 64 位(即 8 個字節),因此這里的對齊就需要使分配的內存起始地址必然為 8 的倍數。要滿足這一條件,采用的主要辦法就是判斷當前空閑內存的起始地址是否為 8 的倍數:如果是,則直接返回;如果不是,則對 8 求模,然后向后尋找最近的 8 的倍數的內存地址并返回。
由于計算機進行乘除或求模運算的速度遠慢于位操作運算,因此這里巧妙的用了位運算來進行優化。
用位運算判斷某個數值是否為2的正整數冪:static_assert((align & (align - 1)) == 0, "Pointer size should be a power of 2");
用位運算進行求模: size_t current_mod = reinterpret_cast<uintptr_t>(alloc_ptr_) & (align - 1);
位運算的細節這里就不提了,大家可以自行百度了解。
內存使用率統計
memory_usage_ 用于存儲當前 Arena 所申請的總共的內存空間大小,為了保證線程安全,其為 atomic<size_t> 變量。
// https://github.com/google/leveldb/blob/master/util/arena.hsize_t MemoryUsage() const {return memory_usage_.load(std::memory_order_relaxed); }// https://github.com/google/leveldb/blob/master/util/arena.ccchar* Arena::AllocateNewBlock(size_t block_bytes) {char* result = new char[block_bytes];blocks_.push_back(result);memory_usage_.fetch_add(block_bytes + sizeof(char*),std::memory_order_relaxed);return result; }在 AllocateNewBock 申請內存的過程中,其會通過以下的公式來更新當前總大小
memory_usage_ = memory_usage_ + block_bytes + sizeof(char*)
為什么這里還需要加上一個sizeof(char*)呢?
因為在申請完 Block 后,Block 的首地址需要存儲在一個 vector<char*> 的動態數組 blocks_ 中,因而需要額外占用一個指針的空間。
TCMalloc
LevelDB 中針對一些需要調用 new 或 malloc 方法進行堆內存操作的情況(即非內存池的內存分配),其使用 TCMalloc 進行優化。
TCMalloc(Thread-Caching Malloc)是 google-perftool 中一個管理堆內存的內存分配器工具,可以降低內存頻繁分配與釋放所造成的性能損失,并有效控制內存碎片。默認 C/C++ 在編譯器中主要采用 glibc 的內存分配器 ptmalloc2。同樣的 malloc 操作,TCMalloc 比 ptmalloc2 更具性能優勢。
TCMalloc 的詳細介紹可以參見 TCMalloc。
Env家族
Env 是一個抽象接口類,用純虛函數的形式定義了一些與平臺操作的相關接口,如文件系統、多線程、時間操作等。接口定義如下:
// https://github.com/google/leveldb/blob/master/include/leveldb/env.hclass LEVELDB_EXPORT Env {public:Env();Env(const Env&) = delete;Env& operator=(const Env&) = delete;virtual ~Env();static Env* Default();virtual Status NewSequentialFile(const std::string& fname,SequentialFile** result) = 0;virtual Status NewRandomAccessFile(const std::string& fname,RandomAccessFile** result) = 0;virtual Status NewWritableFile(const std::string& fname,WritableFile** result) = 0;virtual Status NewAppendableFile(const std::string& fname,WritableFile** result);virtual bool FileExists(const std::string& fname) = 0;virtual Status GetChildren(const std::string& dir,std::vector<std::string>* result) = 0;virtual Status RemoveFile(const std::string& fname);virtual Status DeleteFile(const std::string& fname);virtual Status CreateDir(const std::string& dirname) = 0;virtual Status RemoveDir(const std::string& dirname);virtual Status DeleteDir(const std::string& dirname);virtual Status GetFileSize(const std::string& fname, uint64_t* file_size) = 0;virtual Status RenameFile(const std::string& src,const std::string& target) = 0;virtual Status LockFile(const std::string& fname, FileLock** lock) = 0;virtual Status UnlockFile(FileLock* lock) = 0;virtual void Schedule(void (*function)(void* arg), void* arg) = 0;virtual void StartThread(void (*function)(void* arg), void* arg) = 0;virtual Status GetTestDirectory(std::string* path) = 0;virtual Status NewLogger(const std::string& fname, Logger** result) = 0;virtual uint64_t NowMicros() = 0;virtual void SleepForMicroseconds(int micros) = 0; };Env 作為抽象類,有 3 個派生子類:PosixEnv、EnvWrapper 與 InMemoryEnv。
Env與派生類這里就不過多的介紹它們的實現原理,只是大概的描述一下概念,方便理解。
PosixEnv
PosixEnv 是 LevelDB 中默認的 Env 實例對象。從字面意思上看,PosixEnv 就是針對 POSIX 平臺的 Env 接口實現。
EnvWrapper
EnvWrapper 也是 Env 的一個派生類,與 PosixEnv 不同的是,EnvWrapper 中并沒有定義眾多純虛函數接口的具體實現,而是定義了一個私有成員變量 Env* target_,并在構造函數中通過傳遞預定義的 Env 實例對象,從而實現對 target_ 的初始化操作。基于 EnvWrapper 的派生類,易于實現用戶在某一個 Env 派生類的基礎上改寫其中一部分接口的需求。
InMemoryEnv
InMemoryEnv 就是 EnvWrapper 的一個子類,主要對 Env 中有關文件的接口進行了重寫。InMemoryEnv 主要是將所有的操作都置于內存中,從而提升文件I/O的讀取速度。
文件操作
在 LevelDB 中,主要有三種文件 I/O 操作:
- SequentialFile:順序讀,如日志文件的讀取、Manifest 文件的讀取。
- WritableFile:順序寫,用于日志文件、SSTable 文件、Manifest 文件的寫入。
- RandomAccessFile:隨機讀,如 SSTable 文件的讀取。
SequentialFile
SequentialFile 定義了文件順序讀抽象接口,其接口定義如下:
// https://github.com/google/leveldb/blob/master/include/leveldb/env.hclass LEVELDB_EXPORT SequentialFile {public:SequentialFile() = default;SequentialFile(const SequentialFile&) = delete;SequentialFile& operator=(const SequentialFile&) = delete;virtual ~SequentialFile();virtual Status Read(size_t n, Slice* result, char* scratch) = 0;virtual Status Skip(uint64_t n) = 0; };其主要有兩個接口方法,即 Read 與 Skip:
- Read:用于從文件當前位置順序讀取指定的字節數。
- Skip:用于從當前位置,順序向后忽略指定的字節數。
無論是 Read 方法還是 Skip 方法,對于多線程環境而言均不是線程安全的訪問方法,需要開發者在調用過程中采用外部手段進行線程同步操作。
PosixSequentialFile,是在符合POSIX標準的文件系統上對順序讀的實現。這里就不過多介紹,感興趣的可以自己去了解。
WritableFile
WritableFile定義了文件順序寫抽象接口,其定義下:
// https://github.com/google/leveldb/blob/master/include/leveldb/env.hclass LEVELDB_EXPORT WritableFile {public:WritableFile() = default;WritableFile(const WritableFile&) = delete;WritableFile& operator=(const WritableFile&) = delete;virtual ~WritableFile();virtual Status Append(const Slice& data) = 0;virtual Status Close() = 0;virtual Status Flush() = 0;virtual Status Sync() = 0; };WritableFile 主要有4個純虛函數接口:Append、Close、Flush 與 Sync:
- Append:用于以追加的方式對文件順序寫入。
- Close:用于關閉文件。
- Flush:用于將 Append 操作寫入到緩沖區的數據強制刷新到內核緩沖區。
- Sync:用于將內存緩沖區的數據強制保存到磁盤。
PosixWritableFile 是對符合 POSIX 標準平臺的 WritableFile 的派生實現。
RandomAccessFile
隨機讀就是指可以定位到文件任意某個位置進行讀取。RandomAccessFile 是文件隨機讀的抽象接口,其代碼如下:
// https://github.com/google/leveldb/blob/master/include/leveldb/env.hclass LEVELDB_EXPORT RandomAccessFile {public:RandomAccessFile() = default;RandomAccessFile(const RandomAccessFile&) = delete;RandomAccessFile& operator=(const RandomAccessFile&) = delete;virtual ~RandomAccessFile();virtual Status Read(uint64_t offset, size_t n, Slice* result,char* scratch) const = 0; };與順序讀接口相比,隨機讀沒有 Skip 操作,只有一個 Read 方法:
- Read:可從文件指定的任意位置讀取一段指定長度數據。
在 LevelDB 中,RandomAccessFile 有兩個派生類:PosixRandomAccessFile 與 PosixMmapReadableFile。這兩個派生類是兩種對隨機文件操作的實現形式:一種是基于 pread() 方法的隨機訪問;另一種是基于 mmap()方法的隨機訪問。
數值編碼
LevelDB 是一個嵌入式的存儲庫,其存儲的內容可以是字符,也可以是數值。LevelDB 為了減少數值型內容對內存空間的占用,分別針對不同的需求定義了兩種編碼方式:一種是定長編碼,另一種是變長編碼。
字節序
在講編碼之前,先聊聊字節序。字節序是處理器架構的特性,比如一個16 位的整數,他是由 2 個字節組成。內存中存儲 2 個字節有兩種方法:
-
將低序字節存儲在起始地址,稱為小端。
-
將高序字節存儲在起始地址,稱為大端。
大端、小端存儲模式
LevelDB 為了便于操作,編碼的數據統一采用小端模式,并存放到對應的字符串中,即數據低位存在內存低地址,數據高位存在內存高地址。
具體的關于大小端的信息可以參考往期博客 大端小端存儲解析以及判斷方法。
定長編碼
定長的數值編碼比較簡單,主要是將原有的 uint64 或 uint32 的數據直接存儲在對應的 8 字節或 4 字節中。而在直接存儲的過程中,會基于大小端的不同,采取不同的執行邏輯(新版代碼簡化邏輯,統一按照大端邏輯處理):
- 大端:在復制過程中調換字節順序,以小端模式進行編碼保存。
- 小端:直接利用 memcpy 進行內存間的復制。
具體實現代碼如下:
// https://github.com/google/leveldb/blob/master/util/coding.h//32位數據定長編碼 void EncodeFixed32(char* buf, uint32_t value) {if (port::kLittleEndian) {memcpy(buf, &value, sizeof(value));} else {buf[0] = value & 0xff;buf[1] = (value >> 8) & 0xff;buf[2] = (value >> 16) & 0xff;buf[3] = (value >> 24) & 0xff;} }//64位數據定長編碼 void EncodeFixed64(char* buf, uint64_t value) {if (port::kLittleEndian) {memcpy(buf, &value, sizeof(value));} else {buf[0] = value & 0xff;buf[1] = (value >> 8) & 0xff;buf[2] = (value >> 16) & 0xff;buf[3] = (value >> 24) & 0xff;buf[4] = (value >> 32) & 0xff;buf[5] = (value >> 40) & 0xff;buf[6] = (value >> 48) & 0xff;buf[7] = (value >> 56) & 0xff;} }//32位數據定長解碼 inline uint32_t DecodeFixed32(const char* ptr) {if (port::kLittleEndian) {// Load the raw bytesuint32_t result;memcpy(&result, ptr, sizeof(result)); // gcc optimizes this to a plain loadreturn result;} else {return ((static_cast<uint32_t>(static_cast<unsigned char>(ptr[0])))| (static_cast<uint32_t>(static_cast<unsigned char>(ptr[1])) << 8)| (static_cast<uint32_t>(static_cast<unsigned char>(ptr[2])) << 16)| (static_cast<uint32_t>(static_cast<unsigned char>(ptr[3])) << 24));} }//64位數據定長解碼 inline uint64_t DecodeFixed64(const char* ptr) {if (port::kLittleEndian) {// Load the raw bytesuint64_t result;memcpy(&result, ptr, sizeof(result)); // gcc optimizes this to a plain loadreturn result;} else {uint64_t lo = DecodeFixed32(ptr);uint64_t hi = DecodeFixed32(ptr + 4);return (hi << 32) | lo;} }變長編碼
對于 4 字節或 8 字節表示的無符號整型數據而言,數值較小的整數的高位空間基本為 0,如 uint32 類型的數據 128,高位的 3 個字節都是 0。為了更好的利用這些高位空間,如果能基于某種機制,將為 0 的高位數據忽略,有效地保留其有效位,從而減少所需字節數、節約存儲空間。于是 LevelDB 就采用了 Google 的另一個開源項目 Protobuf 中提出的 varint 變長編碼。
varint 將實際的一個字節分成了兩個部分,最高位定義為 MSB(mostsignificant bit),后續低 7 位表示實際數據。 MSB 是一個標志位,用于表示某一數值的字節是否還有后續的字節,如果為 1 表示該數值后續還有字節,如果為 0 表示該數值所編碼的字節至此完畢。每一個字節中的第 1 到第 7 位表示的是實際的數據,由于有 7 位,則只能表示大小為 0~127 的數值。
下圖給出了 127、300、2^28 - 1 的編碼結果
變長編碼示例具體實現如下:
// https://github.com/google/leveldb/blob/master/util/coding.cc//32位數據變長編碼 char* EncodeVarint32(char* dst, uint32_t v) {// Operate on characters as unsignedsuint8_t* ptr = reinterpret_cast<uint8_t*>(dst);static const int B = 128;if (v < (1 << 7)) {*(ptr++) = v;} else if (v < (1 << 14)) {*(ptr++) = v | B;*(ptr++) = v >> 7;} else if (v < (1 << 21)) {*(ptr++) = v | B;*(ptr++) = (v >> 7) | B;*(ptr++) = v >> 14;} else if (v < (1 << 28)) {*(ptr++) = v | B;*(ptr++) = (v >> 7) | B;*(ptr++) = (v >> 14) | B;*(ptr++) = v >> 21;} else {*(ptr++) = v | B;*(ptr++) = (v >> 7) | B;*(ptr++) = (v >> 14) | B;*(ptr++) = (v >> 21) | B;*(ptr++) = v >> 28;}return reinterpret_cast<char*>(ptr); }//32位數據變長解碼 const char* GetVarint32PtrFallback(const char* p, const char* limit,uint32_t* value) {uint32_t result = 0;for (uint32_t shift = 0; shift <= 28 && p < limit; shift += 7) {uint32_t byte = *(reinterpret_cast<const uint8_t*>(p));p++;if (byte & 128) {// More bytes are presentresult |= ((byte & 127) << shift);} else {result |= (byte << shift);*value = result;return reinterpret_cast<const char*>(p);}}return nullptr; }總結
以上是生活随笔為你收集整理的LevelDB 源码剖析(三)公共基础:内存管理、数值编码、Env家族、文件操作的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: LevelDB 源码剖析(一)准备工作:
- 下一篇: LevelDB 源码剖析(六)WAL模块