RM新时代网站-首页

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

nutsdb的研究以及一些心得體會(huì)分享

冬至子 ? 來源:陪計(jì)算機(jī)走過漫長歲月 ? 作者:陪計(jì)算機(jī)走過漫長 ? 2022-11-16 11:40 ? 次閱讀

背景

有一天nutsdb學(xué)習(xí)交流群中有一位老哥說nutsdb重新恢復(fù)的速度太慢了,他那邊大概有一個(gè)G左右的數(shù)據(jù)。我對(duì)這個(gè)問題還是比較感興趣的,當(dāng)場就接下了這個(gè)任務(wù)。開啟了這段時(shí)間的優(yōu)化之路,歷時(shí)還蠻長時(shí)間的,因?yàn)槠綍r(shí)上班,還有其他的一些興趣愛好什么,所以開源上面的投入倒是有細(xì)水長流的感覺。好了,閑話不多說了,下面把這段時(shí)間在這個(gè)問題上面的研究以及一些心得體會(huì)分享給大家。

分析問題

俗語有云,能復(fù)現(xiàn)的問題都不是什么大問題。根據(jù)學(xué)習(xí)老哥的描述,我首先要做的就是準(zhǔn)備1G的數(shù)據(jù),然后benchmark+pprof測試重啟恢復(fù)db的cpu和內(nèi)存情況。最終問題定位到了這段代碼上面:

// ReadAt returns entry at the given off(offset).
func (df *DataFile) ReadAt(off int) (e *Entry, err error) {

   // 讀取header
   buf := make([]byte, DataEntryHeaderSize)


   if _, err := df.rwManager.ReadAt(buf, int64(off)); err != nil {
      return nil, err
   }


   meta := readMetaData(buf)


   e = &Entry{
      crc:  binary.LittleEndian.Uint32(buf[0:4]),
      Meta: meta,
   }


   if e.IsZero() {
      return nil, nil
   }


   // read bucket
   off += DataEntryHeaderSize
   bucketBuf := make([]byte, meta.BucketSize)
   _, err = df.rwManager.ReadAt(bucketBuf, int64(off))
   if err != nil {
      return nil, err
   }


   e.Meta.Bucket = bucketBuf


   // read key
   off += int(meta.BucketSize)
   keyBuf := make([]byte, meta.KeySize)


   _, err = df.rwManager.ReadAt(keyBuf, int64(off))
   if err != nil {
      return nil, err
   }
   e.Key = keyBuf


   // read value
   off += int(meta.KeySize)
   valBuf := make([]byte, meta.ValueSize)
   _, err = df.rwManager.ReadAt(valBuf, int64(off))
   if err != nil {
      return nil, err
   }
   e.Value = valBuf


   crc := e.GetCrc(buf)
   if crc != e.crc {
      return nil, ErrCrc
   }


   return
}

這段代碼的作用是給定一個(gè)文件的偏移位置,從這個(gè)位置開始往后讀取一條數(shù)據(jù)。在db重啟恢復(fù)的過程中這個(gè)函數(shù)會(huì)被頻繁的調(diào)用,為什么呢?我們知道nutsdb是基于bitcask模型實(shí)現(xiàn)的,本質(zhì)上來說是hash索引。在重啟恢復(fù)的過程中會(huì)加載所有db的數(shù)據(jù)來重新構(gòu)建索引。所以理論上來說,重啟的時(shí)候當(dāng)前db有多少數(shù)據(jù),這個(gè)函數(shù)就會(huì)被調(diào)用多少次,由于這個(gè)函數(shù)性能不大行,所以這里成為了瓶頸。為什么說這個(gè)函數(shù)性能不太行呢?且看我娓娓道來。

一條nutsdb中的數(shù)據(jù)是以這樣的形式被存儲(chǔ)在磁盤中的:

圖片

首先我們需要獲取這條數(shù)據(jù)的元數(shù)據(jù)(header),也就是描述真實(shí)數(shù)據(jù)的數(shù)據(jù),這里會(huì)記錄bucket,key,value的長度,我們拿到這些信息之后會(huì)再次發(fā)起系統(tǒng)調(diào)用去獲取真實(shí)bucket,key,value。由這段代碼我們可以看到,這個(gè)函數(shù)沒執(zhí)行一次,就會(huì)發(fā)起四次系統(tǒng)調(diào)用,分別在磁盤中獲取meta,bucket,key,value。而且每次都需要申請(qǐng)與數(shù)據(jù)長度匹配字節(jié)數(shù)組去承接這些數(shù)據(jù)。這個(gè)現(xiàn)狀有兩個(gè)問題,其一是過多的系統(tǒng)調(diào)用會(huì)是性能的瓶頸,其次是過多的申請(qǐng)內(nèi)存,在這些對(duì)象被使用完之后會(huì)在內(nèi)存中堆積起來,觸發(fā)gc之后才會(huì)被清除回收。但是如果db數(shù)據(jù)過多,在這個(gè)時(shí)候也會(huì)觸發(fā)gc造成卡頓。

如何解決?

在定位到問題之后,就開始了解決問題的旅程,這段旅程異常的漫長,這段過程翻閱了很多資料,嘗試了很多方法。且看我一一道來。

1. 減少系統(tǒng)調(diào)用

在上面的分析中,我們知道讀一條數(shù)據(jù)需要發(fā)起四次系統(tǒng)調(diào)用。我覺得這里其實(shí)大可不必,兩次就足夠了。因?yàn)榈谝淮巫x取meta之后,我們就知道bucket,key,value這些數(shù)據(jù)的長度和位置了加上這些數(shù)據(jù)都是連續(xù)存儲(chǔ)的,直接一次全部拿出來然后分別解析就好。

所以在第一次優(yōu)化中,將上面的四次系統(tǒng)調(diào)用優(yōu)化成了2次,帶來了一倍速度上的提升。不僅僅是重啟速度上的,因?yàn)樵谶\(yùn)行中讀取數(shù)據(jù)用的也是這個(gè)函數(shù)。所以在數(shù)據(jù)的讀取上同樣也是優(yōu)化了一倍的讀取速度。下圖是我提交的pr的截圖,我構(gòu)造了大小分別是50B, 256B,1024B的數(shù)據(jù),db總數(shù)據(jù)1G,做并發(fā)讀取性能測試,實(shí)驗(yàn)結(jié)果表明此次優(yōu)化在讀取速度上提升了接近一倍,但是在內(nèi)存的使用上并沒有減少。

圖片

2. 批量讀取,然后解析數(shù)據(jù)

在做完系統(tǒng)調(diào)用的優(yōu)化之后的一個(gè)星期的時(shí)間里,這項(xiàng)優(yōu)化工作其實(shí)陷入了沉寂,有時(shí)候會(huì)腦爆一下這個(gè)該怎么做,翻越了bitcask論文和DDIA對(duì)hash索引恢復(fù)操作的描述,書中所說都是要將數(shù)據(jù)一條條的拿出來,沒有可參考的資料感覺有點(diǎn)寸步難行。不過天無絕人之路,有一天打游戲的時(shí)候突然想到了,為什么不一次性讀取文件的一部分,比如4KB,然后在這4KB中解析出里面的數(shù)據(jù),解析到解析不下去的時(shí)候再拿下一個(gè),直到文件解析完畢。下面讓我們展開一下這個(gè)思路。

我們知道磁盤底層會(huì)把自己存儲(chǔ)空間劃分成一個(gè)個(gè)扇區(qū),在磁盤之上的文件系統(tǒng)會(huì)把磁盤的扇區(qū)組成一個(gè)個(gè)的block,我們的應(yīng)用程序每次去讀取數(shù)據(jù)都是按照block去讀取的。打個(gè)比方,一個(gè)block是4KB,如果我要讀1KB的數(shù)據(jù),那么會(huì)加載4KB的數(shù)據(jù)到pagecache,然后返回1KB給我們的應(yīng)用程序。所以當(dāng)我們一次次發(fā)起系統(tǒng)調(diào)用的去磁盤回去數(shù)據(jù)的時(shí)候,實(shí)際上數(shù)據(jù)已經(jīng)從磁盤里拿出來并且放到pagecache中了。所以我們直接把整個(gè)block拿出來就完事了。

圖片

一開始想的是批量獲取一批完整的數(shù)據(jù)。這個(gè)時(shí)候其實(shí)不太好控制,因?yàn)槲覀儾恢酪粭l數(shù)據(jù)有多大,所以如果要要批量獲取數(shù)據(jù)還需要額外的數(shù)據(jù)結(jié)構(gòu)去描述,我把這個(gè)東西叫做斷點(diǎn),也就是說一個(gè)文件分成若干個(gè)斷點(diǎn),斷點(diǎn)之間存儲(chǔ)的是一批數(shù)據(jù)。如果朝這個(gè)方向改會(huì)衍生出很多問題,其一是在什么樣的標(biāo)準(zhǔn)下面去寫這個(gè)斷點(diǎn),比如按照數(shù)據(jù)量來記呢,還是按照一個(gè)固定的空間大小來記錄。其二是再增加這個(gè)邏輯去額外記錄還需要在數(shù)據(jù)寫入過程中做添加。一來二去感覺太復(fù)雜了。

那么我們按照一個(gè)個(gè)固定的塊來搞。由于我們不知道數(shù)據(jù)的大小是怎么樣的,所以按照固定塊讀取會(huì)出現(xiàn)以下的情況。

圖片

我們看上圖,上圖中的e1和e2代表著在一個(gè)存儲(chǔ)文件中一條條連續(xù)著存儲(chǔ)的數(shù)據(jù),header是元數(shù)據(jù),payload是對(duì)bucket,key,value這三個(gè)數(shù)據(jù)的整體描述,即有效數(shù)據(jù)部分。圖中的箭頭代表按塊讀取數(shù)據(jù)的時(shí)候有可能讀到的塊的邊界。其實(shí)也很好理解,在讀一個(gè)塊的時(shí)候可能會(huì)出現(xiàn)的情況是:

這個(gè)塊就包含了若干條完完整整的數(shù)據(jù)。這個(gè)時(shí)候不需要額外的處理。

這個(gè)塊前面包含了若干條完整的數(shù)據(jù),但是最后的一條是殘缺的,缺失了header。這個(gè)時(shí)候我們需要讀入header殘余的數(shù)據(jù),解析出一整個(gè)header。為什么不讀一整個(gè)block然后在在block中獲取解析header所需的剩余數(shù)據(jù)呢?因?yàn)槲覀儾恢肋@條數(shù)據(jù)長什么樣子,如果他的大小超過了一個(gè)block的size,那么還需要在后續(xù)做額外的處理。

最后一種情況是缺失了payload,那么我們需要根據(jù)缺失數(shù)據(jù)的大小來判斷要讀多少個(gè)block進(jìn)來。

經(jīng)過這一分析之后,其實(shí)一整個(gè)解析數(shù)據(jù)的流程就是在這三個(gè)狀態(tài)之間來回轉(zhuǎn)換。解析的流程就是一個(gè)狀態(tài)機(jī)的算法。等到文件讀完,那么也就解析完了。

圖片

激動(dòng)的心顫抖的手,代碼寫完之后做了一版benchmark。

BenchmarkRecovery-10           1 2288482750 ns/op 1156858488 B/op 14041579 allocs/op
BenchmarkRecovery-10           2 701933396 ns/op 600666340 B/op 3947879 allocs/op

我們分析一下這個(gè)思路下的優(yōu)化,理論上來說db中一條數(shù)據(jù)越小,優(yōu)化效果越明顯,因?yàn)檫@就意味著單次獲取的block中蘊(yùn)含著更多的數(shù)據(jù),就可以減少更多的系統(tǒng)調(diào)用。但是我們可以看到的是,雖然啟動(dòng)的時(shí)間變短了,申請(qǐng)內(nèi)存的次數(shù)變少了,但是申請(qǐng)的內(nèi)存還是不少。

執(zhí)行兩次啟動(dòng)恢復(fù)1G左右的數(shù)據(jù)大概花了4G的內(nèi)存。也就是說一次啟動(dòng)花了2G,里面有1G消耗不知道是在哪里來的。一開始我以為是讀取數(shù)據(jù)的時(shí)候會(huì)在用戶空間往內(nèi)核空間拷貝,會(huì)存在讀取放大的情況。不過轉(zhuǎn)念一想覺得不大可能,因?yàn)閜agecache是類似LRU的緩存機(jī)制,不會(huì)緩存你所有的數(shù)據(jù),他也有數(shù)據(jù)的淘汰策略,不太可能1比1放大的。

所以問題出在哪里呢?我百思不得其解。不過源碼之下無秘密,我反復(fù)一次又一次看我寫下的代碼,終于讓我在一個(gè)地方發(fā)現(xiàn)了貓膩。一個(gè)我意想不到的地方,那就是append操作。

我的文件數(shù)據(jù)恢復(fù)恢復(fù)操作所代表的結(jié)構(gòu)體是這樣的:

FileRecovery struct {
   fd    *os.File
   rest      []byte
   state     state
   blockSize uint32
   finish    bool
   entry     *handlingEntry
   entries   []*Entry
   off       int64
}

我的解析流程是把文件讀完,把里面的數(shù)據(jù)一個(gè)個(gè)append進(jìn)entries字段里面,文件解析完了再把entries返回。要是在平時(shí)我不會(huì)覺得這個(gè)操作是有什么問題的,不過我準(zhǔn)備的數(shù)據(jù)量太大了(兩百萬條),append應(yīng)該會(huì)觸發(fā)多次擴(kuò)容操作,然后反復(fù)申請(qǐng)內(nèi)存。去翻看了一下append操作的擴(kuò)容機(jī)制,當(dāng)數(shù)據(jù)量小于1024的時(shí)候,會(huì)擴(kuò)成原來的2倍,數(shù)據(jù)量大于1024時(shí),會(huì)變成原來的1.25倍??吹竭@里其實(shí)基本驗(yàn)證了我的猜想,不過謹(jǐn)慎起見還是計(jì)算了一下, nutsdb數(shù)據(jù)有segment設(shè)置,當(dāng)一個(gè)文件大于這個(gè)參數(shù)的大小的時(shí)候會(huì)保存成只讀文件。兩百萬條數(shù)據(jù)寫了兩個(gè)文件,這個(gè)結(jié)構(gòu)體是解析單個(gè)文件的,我們按照每個(gè)文件100萬條數(shù)據(jù)算。100萬條數(shù)據(jù)大概要擴(kuò)容幾次呢:

圖片

1024前面的過程就忽略不計(jì)了,直接從1024開始,掏出計(jì)算器,這里x的值是30次,那么擴(kuò)容30次總共需要多少內(nèi)存呢?

圖片

為什么是乘8呢,因?yàn)槲沂?4位機(jī)器,entries里面的元素是指針,那么指針的長度和字長是一樣的,也就是8個(gè)byte。這里的結(jié)果大概是3000萬,3000萬個(gè)比特大概是220MB的內(nèi)存,兩個(gè)文件加起來大概就是440MB的內(nèi)存了。驚不驚喜,意不意外?

那么如何優(yōu)化這里呢,其實(shí)一個(gè)個(gè)往外傳就可以了,不要等到解析完了堆在數(shù)組里再往外丟。如果要在原來的基礎(chǔ)上改的話,我需要每次處理完一條數(shù)據(jù)的時(shí)候記錄下當(dāng)前這個(gè)block處理到哪里,也就是要把block改成ring buffer的形式,這個(gè)時(shí)候我隱隱約約想到了go的標(biāo)準(zhǔn)庫里有一個(gè)東西做了類似的事情,沒錯(cuò),就是bufio.Reader.他不僅僅會(huì)幫我們看到記錄ringbuffer的位置,還會(huì)預(yù)讀block。整體上看是完美契合這個(gè)需求的。

3. bufio.Reader

bufio.Reader會(huì)維護(hù)一個(gè)默認(rèn)大小4KB的ring buffer,當(dāng)讀取的內(nèi)容大于它所能承載的緩存時(shí),他會(huì)直接在讀取而不使用緩存,如果讀取的是比較小的數(shù)據(jù),會(huì)在他的緩存中copy出來一份返回。這個(gè)就完美的契合了我們多次讀取的場景??匆幌赂某蛇@樣子的性能提升吧:

1.jpg

這下無論是速度上,還是內(nèi)存的使用上,都達(dá)到了一個(gè)比較理想的狀態(tài)。這次優(yōu)化歷程就到此結(jié)束了。

圖片

總結(jié)

做性能優(yōu)化的感覺就像和計(jì)算機(jī)對(duì)話,依照自己現(xiàn)有的知識(shí)去想方案, 然后寫出來之后做實(shí)驗(yàn)求證。做這個(gè)的思路是讓他慢慢的變好,而不是上來就追求完美主義,完美主義是不靠譜的,反而會(huì)讓你陷入到糾結(jié)之中,能優(yōu)化一點(diǎn)是一點(diǎn),我們要看到一個(gè)變好的趨勢,然后在這個(gè)趨勢上面不斷的基于上一次的結(jié)果去猜想下一次怎么優(yōu)化,也就是所謂的“小步快跑”。在做這個(gè)的過程中會(huì)往各個(gè)方向去腦爆,一些背景知識(shí)不是很清楚的時(shí)候需要翻越各種資料。整體來說是一次很不錯(cuò)的成長體驗(yàn)。

審核編輯:劉清

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • 磁盤
    +關(guān)注

    關(guān)注

    1

    文章

    375

    瀏覽量

    25201
  • 狀態(tài)機(jī)
    +關(guān)注

    關(guān)注

    2

    文章

    492

    瀏覽量

    27528
  • Hash
    +關(guān)注

    關(guān)注

    0

    文章

    32

    瀏覽量

    13195
收藏 人收藏

    評(píng)論

    相關(guān)推薦

    VHDL編程心得體會(huì)o

    VHDL編程心得體會(huì)o
    發(fā)表于 08-20 19:04

    學(xué)習(xí)fpga心得體會(huì)

    學(xué)習(xí)fpga心得體會(huì)
    發(fā)表于 09-14 09:02

    Linux鏈表操作心得體會(huì)

    研究linux內(nèi)核自帶的dmatest.c驅(qū)動(dòng)程序過程中發(fā)現(xiàn)有部分的鏈接操作,非常迷惑,故在此記錄下來一些查閱資料后的心得體會(huì)。
    發(fā)表于 07-26 08:15

    求大神分享學(xué)習(xí)51單片機(jī)的心得體會(huì)

    學(xué)習(xí)51單片機(jī)心得體會(huì)51單片機(jī)上拉電阻的心得體會(huì)
    發(fā)表于 03-10 07:32

    C語言編程的學(xué)習(xí)經(jīng)驗(yàn)和心得體會(huì)概括

    C語言編程的學(xué)習(xí)經(jīng)驗(yàn)和心得體會(huì)有哪些?
    發(fā)表于 11-03 06:03

    學(xué)習(xí)STM32控制蜂鳴器的心得體會(huì)分

    以下是學(xué)習(xí)STM32控制蜂鳴器時(shí)的一些心得體會(huì),我也是綜合各種資料寫出來的。蜂鳴器是種很常見的電子元件,般也就發(fā)出滴滴的聲音。但自從在網(wǎng)上看到各種用蜂鳴器播放音樂的實(shí)例,我就對(duì)蜂鳴
    發(fā)表于 12-07 07:44

    Ucos2/3系統(tǒng)和FreeRtos系統(tǒng)心得體會(huì)記錄

    這段時(shí)間學(xué)習(xí)了Ucos2/3系統(tǒng)和FreeRtos系統(tǒng),有一些心得體會(huì),寫下來方面作為筆記,另方面作為小伙伴們學(xué)習(xí)的資料吧!
    發(fā)表于 12-22 08:08

    改造電烙鐵的心得體會(huì)

    改造電烙鐵的心得體會(huì),30W的電烙鐵在焊接大元件的過程中就覺得要加熱好長時(shí)間,這樣就容易將焊接的元件損壞。
    發(fā)表于 02-09 11:17 ?9716次閱讀
    改造電烙鐵的<b class='flag-5'>心得體會(huì)</b>

    飛機(jī)電子狗的正確使用方法-心得體會(huì)

    飛機(jī)電子狗的正確使用方法-心得體會(huì),感興趣的小伙伴可以看看。
    發(fā)表于 07-28 10:21 ?26次下載

    單片機(jī)應(yīng)用研發(fā)暑期實(shí)習(xí)小結(jié)_第周焊接部分心得體會(huì)

    暑期實(shí)習(xí)小結(jié),新手可以看看,這個(gè)是焊接部分,一些心得體會(huì),給大家分享了4個(gè)資源,因第三周是公司的一些產(chǎn)品心得,所以不能上傳。
    發(fā)表于 08-17 11:54 ?0次下載

    單片機(jī)應(yīng)用研發(fā)暑期實(shí)習(xí)小結(jié)_第二周PCB心得體會(huì)

    暑期實(shí)習(xí)小結(jié),新手可以看看,這個(gè)是PCB部分,一些心得體會(huì),給大家分享了4個(gè)資源,因第三周是公司的一些產(chǎn)品心得,所以不能上傳。
    發(fā)表于 08-17 11:54 ?1次下載

    單片機(jī)應(yīng)用研發(fā)暑期實(shí)習(xí)小結(jié)_第四周STM32心得體會(huì)

    暑期實(shí)習(xí)小結(jié),新手可以看看,這個(gè)是STM32部分,一些心得體會(huì),給大家分享了4個(gè)資源,因第三周是公司的一些產(chǎn)品心得,所以不能上傳。
    發(fā)表于 08-17 11:54 ?1次下載

    VHDL編程心得體會(huì)

    VHDL編程心得體會(huì),感興趣的小伙伴們可以瞧瞧。
    發(fā)表于 11-11 17:17 ?3次下載

    Linux內(nèi)核閱讀心得體會(huì)

    Linux內(nèi)核閱讀心得體會(huì)
    發(fā)表于 10-24 08:55 ?8次下載
    Linux內(nèi)核閱讀<b class='flag-5'>心得體會(huì)</b>

    marantz7K?9K試聽會(huì)心得體會(huì)

    marantz7K?9K試聽會(huì)心得體會(huì)
    發(fā)表于 07-31 15:43 ?0次下載
    RM新时代网站-首页