The Log-Structured Merge-Tree(譯):上
http://duanple.blog.163.com/blog/static/7097176720120391321283/
說明:轉載請保留全部資訊
作者:Patrick O’Neil &Edward Cheng etc. 1996
原文:http://www.springerlink.com/content/rfkpd5yej9v5chrp/
譯者:phylips@bmy 2011-12-25
譯文:http://duanple.blog.163.com/blog/static/7097176720120391321283/
【隨著NoSql系統尤其是類BigTable系統的流行,LSM-Tree這個名詞也開始變得不再陌生。相信大多數了解NoSql系統的人,基本上都會聽到過LSM-Tree這個名詞,但是讀過其原始論文的人估計就不是很多了。在我看來,LSM-Tree之於BigTable的重要性就像一致性hash之於Dynamo。溯本求源一向是本人的追求,希望可以從最初的文字中找到蘊含在結構之下的更多思考。老實說,這篇論文也算是很長的了,原文共30頁,涉及了不少公式,因此翻起來也不會那麼簡單。
不管怎樣,言必談LSM-Tree,如果沒有讀過它的原始論文,就太虛了,因此還是堅持把它讀懂,並譯了下來。近來也在弄LevelDB相關的東西,就當理論與實踐相結合了。之後,會再放出一篇關於LevelDB的長文,雖然那篇文章九月份就寫完了,但是還是在譯完LSM-Tree之後再潤色一番吧,相信看過LSM-Tree之後,會有些新的理解和想法。
此處的Log-Structured這個詞源於Ousterhout和Rosenblum在1991年發表的經典論文<<The Design and Implementation of a Log-Structured File System >>,這篇論文提出了一種新的磁碟儲存管理方式,在這種結構下,針對磁碟內容的所有更新將會被順序地寫入一個類日誌的結構中,從而加速檔案寫入和回收速度。該日誌包含了一些索引資訊以保證檔案可以快速地讀出。日誌會被劃分為多個段來進行管理。這種方式非常適合於存在大量小檔案寫入的場景。
Log-Structureed檔案系統實際上源自於日誌(logging or journaling)檔案系統,大概在1988年,Ousterhout等人覺得可以對現有日誌型檔案系統進行改進,不再是將資料和操作日誌分成獨立兩部分,也不再只將日誌作為一種暫存方式,而是將整個磁碟就看做是一個log,在log中存放永久性資料,每次都寫入到log的末尾,並且log中包含索引,之後他們實現了一個稱作Sprite LFS的系統原型,並與FFS進行了對比。Unix FFS使用inode資料結構來儲存諸如型別,owner,許可權,磁碟地址,indirect blocks之類的檔案資料元資訊,它本身是索引中的一個entry,並通過indirect blocks指向下一個inode,inode儲存於磁碟的固定位置,而且是非連續的。這樣在存取檔案時,就會有大量的磁碟seek,同時需要同步更新檔案對應的後設資料(inode,directories),在大量小檔案存取中會有效能問題。而在Log-Structureed檔案系統中,寫入是以log追加的方式進行的,因此只需一次seek,讀取時,因為具有索引,因此也很快。磁碟空間以segment為單位進行管理,並通過segment cleaner查詢空閒空間。傳統Unix檔案系統對磁碟頻寬的使用率只有5%-10%,但是LFS通常可以達到65%-75%。在故障恢復時,傳統Unix系統需要掃描整個磁碟,而LFS只需要檢查最近crash之後的log部分即可。
LFS的主要優點就是:通過將很多小檔案的存取轉換為連續的大批量傳輸,使得對於檔案系統的大多數存取都是順序性的,從而提高磁碟頻寬利用率;故障恢復速度快。其背後也有諸如clean,copy and compact,checkpoint這樣的機制。{!關於Log-Structureed的更詳細資訊可以參考這兩篇文章:<<Log-structured file systems: There's one in every SSD>>,<<The Design and Implementation of a Log-Structured File System >>}】
摘要
高效能事務系統通常會通過向一個歷史表中插入記錄以追蹤各項活動行為;與此同時事務系統也會因系統恢復的需要而生成日誌記錄。這兩種型別的生成資訊都可以從高效的索引方式中獲益。眾所周知的一個例子,TPC-A benchmark{!TPC是Transcation Processing Performance Council的簡稱,是一個專門針對資料庫系統效能benchmark的非盈利性組織,TPC-A是其中的一個測試集合,主要關注於事務處理的吞吐量,更具體來說是每秒的事務處理能力,通過TPS(transactions per second)測量}中的測試程式,修改下就可以在給定的History表上對給定account的活動記錄進行高效的查詢支援。但這需要在快速增長的History表上建立一個針對account id的索引。不幸的是,標準的基於磁碟的索引結構,比如B-樹,要實時性地維護這樣的一個索引,會導致事務的IO開銷加倍,從而會導致系統的整體開銷增加50%{!通過例1.1可知,磁碟成本大概佔系統總成本的50%}。很明顯,這就需要一種維護這種實時性索引的低成本的方式。The Log-Structured Merge-Tree (LSM-Tree)就是設計用於來為那些長期具有很高記錄更新(插入或刪除)頻率的檔案來提供低成本的索引機制。LSM-Tree通過使用某種演算法,該演算法會對索引變更進行延遲及批量處理,並通過一種類似於歸併排序的方式聯合使用一個基於記憶體的元件和一個或多個磁碟元件。在處理過程中,所有的索引值對於所有的查詢來說都是可以通過記憶體元件或者某個磁碟元件來進行訪問的(除了很短暫的加鎖期外)。與傳統訪問方式比如B-樹相比,該演算法大大降低了磁碟磁臂的移動,同時也會提高那些使用傳統訪問方式進行插入時,磁碟磁臂開銷{!尋道+轉動}遠大於儲存空間花費的情況的價效比。LSM-Tree方式也可以支援除插入和刪除外的其他操作。但是,對於那些需要立即響應的查詢操作來說,某些情況下,它也會損失一些IO效率,因此LSM-Tree最適用於那些索引插入比查詢操作更常見的情況。比如,對於歷史記錄表和日誌檔案來說,就屬於這種情況。
1. 導引
隨著activity flow(活動流)管理系統中的long-lived(長生命期)事務的商業化使用,針對事務日誌記錄提供索引化的訪問需求也在逐步增長。傳統的,事務日誌機制主要專注於失敗和恢復,需要系統能夠在偶然的事務回滾中可以回退到一個相對近期的正常歷史狀態,而恢復的執行則通過批量化的順序讀取完成。然而,隨著系統需要管理越來越多的複雜行為,組成單個長生命期活動的事件的持續時間和個數也會增長到某種情況,在該情況下,需要實時檢視已經完成的事務步驟以通知使用者目前已經完成了哪些。與此同時,一個系統的處於活動狀態的事件總數,也會增長到某種情況,在該情況下,用於記錄活動日誌的基於記憶體的資料結構開始無法工作,儘管記憶體價格的不斷下降是可以預計的。這種對於過去的行為日誌的查詢需求,意味著索引化的日誌訪問將會越來越重要。
即使是對於當前的事務系統來說,如果對在具有高插入頻率上的歷史記錄表上的查詢提供了索引支援,其價值也是很明顯的。網路應用,電子郵件,和其他的近事務系統通常會產生不利於它們的主機系統的大量日誌。為了便於理解,還是從一個具體的大家都熟知的例子開始,在下面的例1.1和1.2中我們使用了一個修改版的TPC-A benchmark。需要注意的是,為了便於表述,本文中的例子都採用了一些特定的引數值,當然這些結果應該都很容易進行推廣。還要指出的是,儘管歷史記錄表和日誌都是一些時間序列相關的資料,LSM-Tree中的索引節點並不一定具有與之相同的key值順序。唯一的假設就是與查詢頻率相比的高更新率。
5分鐘法則
下面的兩個例子都依賴於5分鐘法則。該法則是說當頁面訪問頻率超過每60秒就會被訪問一次的頻率後,可以通過加大記憶體來將頁面儲存到記憶體,以避免磁碟訪問來降低系統總體開銷。60秒在這裡只是個近似值,實際上是磁碟提供每秒單次IO的平攤開銷與每秒快取4K bytes的磁碟頁的記憶體開銷的比值。用第3節的術語來說,就是COSTp/COSTm。在這裡,我們會從經濟學的角度上簡單看下如何在磁碟訪問和快取在記憶體之間進行權衡。需要注意的是,由於記憶體價格與磁碟相比下降地更快,60秒這個值實際應該會隨著時間而變大。但是在1995年的今天它是60秒,與1987年的5分鐘相比,它卻變小了,部分是因為技術性的(不同的快取假設)原因,部分是因為介於二者之間的廉價量產磁碟的引入。
例1.1 考慮TPC-A benchmark中描述的每秒執行1000個事務(該頻率可以被擴充套件,但是此處我們只考慮1000TPS)的多使用者應用程式。每個事務會更新一個列值,從一個Balance列中取出數目為Delta的款項,對應的記錄(row)是隨機選定的並且具有100位元組大小,涉及到三個表:具有1000條記錄的Branch(分公司)表,具有10000條記錄的Teller(出納員)表,以及具有100,000,000條記錄的Account表;更新完成之後,該事務在提交之前會向一個歷史記錄表中插入一條50位元組的記錄,該記錄具有如下列:Account-ID,Branch-ID,Teller-ID,Delta和時間戳。
根據磁碟和記憶體成本計算下,可以看出Account表在未來的很多年內都不可能是記憶體駐留的,而Branch和Teller表目前是可以全部存入記憶體中的。在給定的假設下,對於Account表的同一個磁碟頁的兩次訪問大概要間隔2500秒{!磁碟頁是4K bytes,Account表每行是100bytes,這樣每次讀會涉及到4k/100=40條記錄,TPS是1000,這樣2500秒內讀到的行數就是40*1000*2500=100,000,000。2500秒就是這麼算出來的},很明顯這個值還未達到5分鐘法則中快取駐留所需要的訪問頻率。現在每次事務都需要大概兩次磁碟IO,一次用於讀取所需的Account記錄(我們忽略那種頁面已經被快取的特殊情況),一次用於將之前的一個髒的Account頁寫出來為讀取騰出快取空間(necessary for steady-status behavior)。因此1000TPS實際上對應著大概每秒2000個IO。如果磁碟的標稱速率是25IO/s,那麼這就需要80個磁碟驅動器,從1987年到如今(1995)的8年間磁碟速率(IOPS)每年才提高不到10%,因此現在IOPS大概是40IO/s,這樣對於每秒2000次IO來說,就需要50個磁碟。對於TPC應用來說,磁碟的花費大概佔了整個系統成本的一半,儘管在IBM的大型機系統中這個比例要低一些。然而,用於支援IO的開銷很明顯在整個系統成本中正在不斷增長,因為記憶體和CPU成本下降地比磁碟要快。
例1.2 現在來考慮一個在具有高插入量的歷史記錄表上的索引,可以證明這樣的一個索引將會使TPC應用的磁碟開銷加倍。一個在“Account-ID+Timestamp”上的聯合索引,是歷史記錄表能夠對最近的account活動進行高效查詢的關鍵,比如:
Select * from History
Where History.Acct-ID = %custacctid
And History.Timestamp > %custdatetime;
如果Acct-ID||Timestamp索引不存在,這樣的一個查詢將需要搜尋歷史記錄表中的所有記錄,因此基本上是不可行的。如果只是在Acct-ID上建立索引,可以得到絕大部分的收益,但是即使將Timestamp排除,我們下面的那些開銷考慮也不會發生變化{!即去掉Timestamp也不會省掉什麼開銷},因此我們這裡假設使用的是最有效的聯合索引。那麼實時地維護這樣的一個B-樹索引需要多少資源呢?可以知道,B樹中的節點每秒會生成1000個,考慮一個20天的週期,每天8小時,每個節點16位元組,這意味著將會有576,000,000{!1000*20*8*3600}個節點,佔據的磁碟空間是9.2GBytes,即使是在沒有浪費空間的情況下,整個索引的葉節點都大概需要2.3million個磁碟頁。因為事務的Acct-ID值是隨機選擇的,每個事務至少需要一次讀取,此外基本上還需要一次頁面寫入。根據5分鐘法則,這些索引頁面也不應該是記憶體駐留的(磁碟頁大概每隔2300秒被讀一次),這樣所有的IO都是針對磁碟的。這樣針對Account表的更新,除了現有的2000IO/s就還需要額外的2000IO/s,也就需要再購買50個磁碟,這樣磁碟的需求就加倍了。同時,這還是假設用於將日誌檔案索引維持在20天的長度上的刪除操作,可以作為一個批處理任務在空閒時間執行。
現在我們已經分析了使用B-樹來作為Acct-ID||Timestamp索引的情況,因為它是當前商業系統中使用的最通用的基於磁碟的訪問方法。事實上,也沒有什麼其他經典的磁碟索引結構可以提供更好的IO價效比。在第5節中我們還會討論下如何得出這樣的結論的。
本文提出的LSM-Tree訪問方法使得我們可以使用更少的磁碟運動來執行在Acct-ID||Timestamp上的頻繁插入操作。LSM-Tree通過使用某種演算法,該演算法會對索引變更進行延遲及批量處理,並通過一種類似於歸併排序的方式高效地將更新遷移到磁碟。正如我們將在第5節看到的,將索引節點放置到磁碟上的這一過程進行延遲處理,是最根本的,LSM-Tree結構通常就是包含了一系列延遲放置機制。LSM-Tree結構也支援其他的操作,比如刪除,更新,甚至是那些具有long latency的查詢操作。只有那些需要立即響應的查詢會具有相對昂貴的開銷。LSM-Tree的主要應用場景就是像例1.2那樣的,查詢頻率遠低於插入頻率的情況(大多數人不會像開支票或存款那樣經常檢視自己的賬號活動資訊)。在這種情況下,最重要的是降低索引插入開銷;與此同時,也必須要維護一個某種形式的索引,因為順序搜尋所有記錄是不可能的。
在第2節,我們會引入2-元件LSM-Tree演算法。在第3節,我們會分析下LSM-Tree的效能,並提出多元件LSM-Tree。在第4節,我們會描述下LSM-Tree的併發和恢復的概念。在第5節,我們會討論下其他的一些訪問方式,以及它們的效能。第6節是總結,我們會指出LSM-Tree的一些問題,並提出一些擴充套件建議。
2. 兩元件LSM-Tree演算法
LSM-Tree由兩個或多個類樹的資料結構元件構成。本節,我們只考慮簡單的兩個元件的情況,同時假設LSM-Tree索引的是例1.2中的歷史記錄表中的記錄。如下圖2.1
在每條歷史記錄表中的記錄生成時,會首先向一個日誌檔案中寫入一個用於恢復該插入操作的日誌記錄。然後針對該歷史記錄表的實際索引節點會被插入到駐留在記憶體中的C0樹,之後它將會在某個時間被移到磁碟上的C1樹中。對於某條記錄的檢索,將會首先在C0中查詢,然後是C1。在記錄從C0移到C1中間肯定存在一定時間的延遲,這就要求能夠恢復那些crash之前還未被移出到磁碟的記錄。恢復機制將會在第4節討論,現在我們只是簡單地認為那些用於恢復插入的歷史記錄資料的日誌記錄可以被看做邏輯上的日誌;在恢復期間我們可以重構出那些已經被插入的歷史記錄,同時可以重建出需要的那些記錄並將這些記錄進行索引以恢復C0丟失的內容。
向駐留在記憶體中的C0樹插入一個索引條目不會花費任何IO開銷。但是,用於儲存C0的記憶體的成本要遠高於磁碟,這就限制了它的大小。這就需要一種有效的方式來將記錄遷移到駐留在更低成本的儲存裝置上的C1樹中。為了實現這個目的,在當C0樹因插入操作而達到接近某個上限的閾值大小時,就會啟動一個rolling merge過程,來將某些連續的記錄段從C0樹中刪除,並merge到磁碟上的C1樹中。圖2.2描述了這樣的一個過程。
C1樹具有一個類似於B-樹的目錄結構,但是它是為順序性的磁碟訪問優化過的,所有的節點都是100%滿的,同時為了有效利用磁碟,在根節點之下的所有的單頁面節點都會被打包(pack)放到連續的多頁面磁碟塊(multi-page block)上;類似的優化也被用在SB-樹中。對於rolling merge和長的區間檢索的情況將會使用multi-page block io,而在匹配性的查詢中會使用單頁面節點以最小化快取需求。對於root之外的節點使用256Kbytes的multi-page block大小,對於root節點根據定義通常都只是單個的頁面。
Rolling merge實際上由一系列的merge步驟組成。首先會讀取一個包含了C1樹中葉節點的multi-page block,這將會使C1中的一系列記錄進入快取。之後,每次merge將會直接從快取中以磁碟頁的大小讀取C1的葉節點,將那些來自於葉節點的記錄與從C0樹中拿到的葉節點級的記錄進行merge,這樣就減少了C0的大小,同時在C1樹中建立了一個新的merge好的葉節點。
merge之前的老的C1樹節點被儲存在快取中的稱為emptying block{!掏空ing,即該block中的那些節點正在被掏空}的multi-page block中,而新的葉節點會被寫入到另一個稱為filling block{!填充ing,即該block正在被不斷地用新節點填充}的快取中的multi-page block。當C1中新merge的節點填滿filling block後,該block會被寫入到磁碟上的新空閒區域中。如果從圖2.2中看的話,包含了merge結果的新的multi-page block位於圖中老節點的右側。後續的merge步驟會隨著C0和C1的索引值的增加而發生,當達到閾值時,就又會從最小值開始啟動rolling merge過程。
新的merge後的blocks會被寫入到新的磁碟位置上,這樣老的blocks就不會被覆蓋,這樣在crash發生後的恢復中就是可用的。C1中的父目錄節點也會被快取在記憶體中,此時也會被更新以反映出葉節點的變動,同時父節點還會在記憶體中停留一段時間以最小化IO;當merge步驟完成後,C1中的老的葉節點就會變為無效狀態,之後會被從C1目錄結構中刪除。通常,每次都是C1中的最左邊的葉節點記錄參與merge,因為如果老的葉節點都是空的那麼merge步驟也就不會產生新的節點,這樣也就沒有必要進行。除了更新後的目錄節點資訊外,這些最左邊的記錄在被寫入到磁碟之前也會在記憶體中快取一段時間。用於提供在merge階段的併發訪問和從crash後的記憶體丟失中進行恢復的技術將會在第4節詳細介紹。為了減少恢復時的重構時間,merge過程需要進行週期性的checkpoints,強制將快取資訊寫入磁碟。
2.1 How a Two Component LSM-Tree Grows
為了追蹤LSM-tree從誕生那一刻開始的整個變化過程,我們從針對C0的第一次插入開始。與C1樹不同,C0樹不一定要具有一個類B-樹的結構。首先,它的節點可以具有任意大小:沒有必要讓它與磁碟頁面大小保持一致,因為C0樹永不會位於磁碟上,因此我們就沒有必要為了最小化樹的深度而犧牲CPU的效率{!如果看下B-樹,就可以知道實際上它為了降低樹的高度,犧牲了CPU效率。在當整個資料結構都是在記憶體中時,與二分查詢相比,B-樹在查詢時,在節點內部的比較,實際上退化成了順序查詢,這樣它查詢一個節點所需的比較次數實際上要大於AVL的比較次數}。這樣,一個2-3樹或者是AVL樹就可以作為C0樹使用的一個資料結構。當C0首次增長到它的閾值大小時,最左邊的一系列記錄將會從C0中刪除(這應是以批量處理的模式完成,而不是一次一條記錄),然後被重新組織成C1中的一個100%滿的葉子節點。後續的葉節點會按照從左到右的順序放到快取中的一個multi-page block的初始頁面中,直到該block填滿為止;之後,該block會被寫到磁碟中,成為C1樹的磁碟上的葉級儲存的第一部分。隨著後續的葉節點的加入,C1樹會建立出一個目錄節點結構,具體細節如下。
C1樹的葉節點級的後續multi-page block會按照鍵值遞增的順序被寫入到磁碟中,以防止C0樹大小超過閾值。C1樹的上級目錄節點被存放在獨立的multi-page block buffers或者是單頁面快取中,無論存在哪裡,都是為了更好地利用記憶體和磁碟;目錄節點中的記錄包含一些分隔點,通過這些分隔點可以將使用者訪問導引到單個的singe-page節點中,像B-樹那樣。通過這種指向葉級節點的single-page索引節點可以提供高效的精確匹配訪問,避免了multi-page block的讀取,這樣就最小化了快取需求。這樣在進行rolling merge或者按range檢索時才會讀寫multi-page block,對於索引化的查詢(精確匹配)訪問則讀寫singe-page節點。[22]中提出了一種與之類似但又稍有不同的結構。在一系列葉級節點blocks被寫出時,那些由C1的目錄節點組成的還未滿的multi-page block可以保留在快取中。在如下情況下,C1的目錄節點會被強制寫入磁碟:
l 由目錄節點組成的某個multi-page block被填滿了
l 根節點發生了分裂,增加了C1樹的深度(成了一個大於2的深度)
l 執行Checkpoint
對於第一種情況,只有被填滿的那個block會被寫出到磁碟。對於後兩個情況,所有的multi-page block buffers和目錄節點buffers都會被flush到磁碟。
當C0樹的最右邊的葉節點記錄首次被寫出到C1樹後,整個過程就又會從兩個樹的最左端開始,只是從現在開始,需要先把C1中的葉子級別的multi-page block讀入到buffer,然後與C0樹中的記錄進行merge,產生出需要寫入到磁碟的新的C1的multi-page leaf block。
一旦merge過程開始,情況就變地更復雜了。我們可以把整個兩元件LSM-tree的rolling merge過程想象成一個具有一定步長的遊標迴圈往復地穿越在C0和C1的鍵值對上,不斷地從C0中取出資料放入到磁碟上才C1中。該rolling merge遊標在C1樹的葉節點和更上層的目錄級都會有一個邏輯上的位置。在每個層級上,所有當前正在參與merge的multi-page blocks將會被分成兩個blocks:”emptying block”-它內部的記錄正在搬出,但是還有一些資訊是merge遊標所未到達的,”filling block”-反映了此刻的merge結果。類似地,該遊標也會定義出”emptying node”和”filling node”,這兩個節點此刻肯定是已在快取中。為了可以進行併發訪問,每個層級上的”emptying block”和”filling block”包含整數個的page-sized C1樹節點。(在對執行節點進行重組的merge步驟中,針對這些節點的內部記錄的其他型別的並行訪問將會被阻塞)。當所有被快取的節點需要被flush到磁碟時,每個層級的所有被快取的資訊必須被寫入到磁碟上的新的位置上(同時這些位置資訊需要反映在上層目錄資訊中,同時為了進行恢復還需要產生一條日誌記錄)。此後,當C1樹某一層級的快取中的filling block被填滿及需要再次flush時,它會被放到新的磁碟位置上。那些可能在恢復過程中需要的老的資訊永不會被覆蓋,只有當後續的寫入提供了足夠資訊時它們才可以宣告失效。第4節來還會進行一些關於roling merge過程的更細節的解釋,在那一節裡還會考慮關於併發訪問和恢復機制的設計。
在C1的某個層級上的rolling merge過程,需要很高的節點傳輸速率時,所有的讀寫都是以multi-page blocks為單位進行的,對於LSM-tree來說,這是一個很重要的效率上的優化。通過減少尋道時間和旋轉延遲,我們認為與普通的B-樹節點插入所產生的隨機IO相比,這樣做可以得到更大的優勢(我們將會在3.2節討論其中的優勢)。總是以multi-page blocks為單位進行寫入的想法源自於由Rosenblum和Ousterhout發明的Log-Structured File System,Log-Structured Merge-tree的叫法也源於此。需要注意的是,對於新的multi-page blocks的寫入使用連續的新的磁碟空間,這就意味著必須對磁碟區域進行包裝管理,舊的被丟棄的blocks必須能被重用。使用記錄可以通過一個記憶體表來管理;舊的multi-page blocks作為單個單元被置為無效和重用,通過checkpoint來進行恢復。在Log-Structured File System中,舊的block的重用會引入顯著的IO開銷,因為blocks通常是半空的,這樣重用就需要針對該block的一次讀取和寫入。在LSM-tree中,blocks是完全空的,因此不需要額外的IO。
2.2 Finds in the LSM-tree index
當在LSM-tree index上執行一個需要理解響應的精確匹配查詢或者range查詢時,首先會到C0中查詢所需的那個或那些值,然後是C1中。這意味著與B-樹相比,會有一些額外的CPU開銷,因為現在需要去兩個目錄中搜尋。對於那些具有超過兩個元件的LSM-tree來說,還會有IO上的開銷。先稍微講一下第3章的內容,我們將一個具有元件C0,C1,C2…Ck-1和Ck的多元件LSM-tree,索引樹的大小伴隨著下標的增加而增大,其中C0是駐留在記憶體中的,其他則是在磁碟上。在所有的元件對(Ci-1,Ci)之間都有一個非同步的rolling merge過程負責在較小的元件Ci-1超過閾值大小時,將它的記錄移到Ci中。一般來說,為了保證LSM-tree中的所有記錄都會被檢查到,對於一個精確匹配查詢和range查詢來說,需要訪問所有的Ci元件。當然,也存在很多優化方法,可以使搜尋限制在這些元件的一個子集上。
首先,如果生成邏輯可以保證索引值是唯一的,比如使用時間戳來進行標識時,如果一個匹配查詢已經在一個早期的Ci元件中找到時那麼它就可以宣告完成了。再比如,如果查詢條件裡使用了最近時間戳,那麼我們可以讓那些查詢到的值不要向最大的元件中移動。當merge遊標掃描(Ci,Ci+1)對時,我們可以讓那些最近某個時間段(比如τi秒)內的值依然保留在Ci中,只把那些老記錄移入到Ci+1。在那些最常訪問的值都是最近插入的值的情況下,很多查詢只需要訪問C0就可以完成,這樣C0實際上就承擔了一個記憶體緩衝區的功能。[23]中也使用了這一點,同時這也是一種重要的效能優化。比如,用於短期事務UNDO日誌的索引訪問模式,在中斷事件發生時,通常都是針對相對近期的資料的訪問,這樣大部分的索引就都會是仍處在記憶體中。通過記錄每個事務的啟動時間,就可以保證所有最近的τ0秒內發生的事務的所有日誌都可以在C0中找到,而不需要訪問磁碟元件。
2.3 Deletes,Updates and Long-Latency Finds in the LSM-tree
需要指出的是刪除操作可以像插入操作那樣享受到延遲和批量處理帶來的好處。當某個被索引的行被刪除時,如果該記錄在C0樹中對應的位置上不存在,那麼可以將一個刪除標記記錄(delete node entry)放到該位置,該標記記錄也是通過相同的key值進行索引,同時指出將要被刪除的記錄的Row ID(RID)。實際的刪除可以在後面的rolling merge過程中碰到實際的那個索引entry時執行:也就是說delete node entry會在merge過程中移到更大的元件中,同時當碰到相關聯的那個entry,就將其清除。與此同時,查詢請求也必須在通過該刪除標記時進行過濾,以避免返回一個已經被刪除的記錄。該過濾很容易進行,因為刪除標記就是位於它所標識的那個entry所應在的位置上,同時在很多情況下,這種過濾還起到了減少判定記錄是否被刪除所需的開銷{!比如對於一個實際不存在的記錄的查詢,如果沒有該刪除標記,需要搜尋到最大的那個Ci元件為止,但是如果存在一個刪除標記,那麼在碰到該標記後就可以停止了}。對於任何應用來說,那些會導致索引值發生變化{!比如一條記錄包含了ID和name,同時是以ID進行索引的,那麼如果是name更新了,很容易,只需要對該記錄進行一個原地改動即可,但是如果是ID該了,那麼該記錄在索引中的位置就要調整了,因此是很棘手的}的更新都是不平凡的,但是這樣的更新卻可以被LSM-tree一招化解,通過將該更新操作看做是一個刪除操作+一個插入操作。
還可以提供另一種型別的操作,可以用於高效地更新索引。一個稱為斷言式刪除(predicate deletion)的過程,提供了一種通過簡單地宣告一個斷言,就可以執行批量刪除的操作方式。比如這樣的一個斷言,刪除那些時間戳超過20天的所有的索引值。當位於最大元件裡的受斷言影響的記錄,通過日常的rolling merge過程進入到記憶體時,就可以簡單地將他們丟棄來實現刪除。另一種型別的操作,long-latency find,對於那些可以等待很長時間(所需等待的時間實際上是由最慢的那個merge遊標的速度決定的)的查詢來說,它提供了一種高效地響應方式。通過向C0中插入一個find note entry,它被移入到更大的元件的過程實際上也就是該查詢執行的過程,一旦該find note entry到達了LSM-tree中最大的那個元件的對應位置,該long-latency find所對應的那些匹配的RID列表也就生成完畢了。
3 Cost-Performance and the Multi-Component LSM-Tree
本節我們會從一個兩元件LSM-tree開始分析下LSM-tree的價效比。同時會將LSM-tree與具有與之類似的索引規模的B-樹進行對比,比較下它們在大量插入操作下的IO資源利用情況。正如我們將在第5節所述的那樣,其他的基於磁碟的訪問方式在插入新索引節點所需的IO開銷上都基本上與B-樹類似。我們在此處對LSM-tree和B-樹進行比較的最重要的原因是這兩個結構很容易比較,它們都在葉子節點上為每個以特定順序索引的記錄行儲存了一個entry,同時上層目錄資訊可以沿著一系列頁面大小的節點將各種訪問操作進行指引。通過與低效但是很好理解的B-樹的對比分析,對於LSM-tree在新節點插入上的所具有的IO優勢的分析,可以更好地進行表達。
在3.2節中,我們會比較IO開銷,並將證明兩元件LSM-tree的開銷與B-樹的開銷的比值實際上兩個因子的乘積{!即3.2節中的公式3.4}。第一個因子, ,代表了LSM-tree通過將所有的IO以multi-page blocks進行得到的優勢,這樣通過節省大量的尋道和旋轉延遲可以更有效地利用磁碟磁臂。COSTπ代表了磁碟以multi-page blocks為單位讀寫一個page時的開銷,COSTp則代表了隨機讀寫一個page時的開銷。第二個因子是1/M,代表了在merge過程中的批量處理模式帶來的效率提升。M是從C0中merge到C1中的一個page-sized的葉節點中的記錄的平均數目。對於B樹來說,每條記錄的插入通常需要對該記錄所屬的節點進行兩次IO(一次讀出一次寫入),與此相比,可以向每個葉子中一次插入多條記錄就是一個優勢。根據5分鐘法則,例1.2中的葉子節點在從B樹中讀入後之後短暫地在記憶體中停留,在它被再一次使用時它已不在記憶體了。因此對於B樹索引來說就沒有一種批量處理的優勢:每個葉節點被讀入記憶體,然後插入一條記錄,然後寫出去。但是在一個LSM-tree中,只要與C1元件相比C0元件足夠大,總是會有一個批量處理效果。比如,對於16位元組的索引記錄大小來說,在一個4Kbytes的節點中將會有250條記錄。如果C0元件的大小是C1的1/25,那麼在每個具有250條記錄的C1節點的Node IO中,將會有10條是新記錄{!也就是說在此次merge產生個node中有10條是在C0中的,而C0中的記錄則是使用者之前插入的,這相當於將使用者的插入先暫存到C0中,然後延遲到merge時寫入磁碟,這樣這一次的Node IO實際上消化了使用者之前的10次插入,的確是將插入批量化了}。很明顯,由於這兩個因素,與B-樹相比LSM-tree效率更高,而rolling merge過程則是其中的關鍵。
用來代表multi-page block比single-page的優勢之處的 實際上是個常量,為了使它生效我們無需對LSM-tree的結構進行任何處理。但是merge中的1/M的批量模式效率是跟C0和C1的大小之比成比例的;與C1相比,C0越大,效果越好;某種程度上說,這意味著我們可以通過使用更大的C0來節省額外的磁碟磁臂開銷,但是這也需要更大的記憶體開銷來容納下C0元件。這也是在使用LSM-tree時需要考慮的,會在3.3節中對此進行研究。一個三元件LSM-tree具有一個記憶體元件C0和兩個基於磁碟的元件C1和C2,並且隨著元件大小隨下標增加而增大。這樣,除了C0和C1之間會有一個rolling merge過程,在C1和C2之間也會存在一個rolling merge過程,來負責在小的元件達到閾值大小時,將記錄從小的元件中移到大的元件中。三元件LSM-tree的優勢在於,它可以通過選擇C1的大小來實現C0和C1以及C1和C2之間的比率大小來提高批處理效率。這樣C0的大小就可以變得更小,可以大大地降低開銷。{!因為在只有C0和C1的情況下,C1的大小有一個硬性要求,它必須能夠容得下所有的記錄,這樣C0的大小選擇就沒有多少自由,而引人C2後,我們可以利用C2來保證可以儲存下所有的記錄,而C1就可以用來調整與C0的比例,而它就可以小點,這樣由於目標是為了讓C0/(C1+C0)儘量小,那麼C0也可以變得小點就可以達到兩元件下的效果}。
3.1 The Disk Model
與B-樹相比LSM-tree的優勢主要在於降低IO開銷方面(儘管與其他的已知的磁碟結構相比,它的磁碟元件都是100%慢,這也降低了容量方面的開銷)。LSM-tree在IO開銷上的優勢,部分是因為一個page IO可以平攤到一個multi-page block中的多個page上。
定義3.1.1 IO開銷和資料熱度 當我們將某種特定資料儲存在磁碟上時,比如表中的行或者是索引中的記錄,會發現隨著資料量的增加,在給定的應用環境下,磁碟磁臂的利用率會越來越高。當我們購買了一塊磁碟時,我們實際上為兩樣東西付了款:首先是磁碟容量,其次是磁碟IO能力。對於任意型別應用來說,通常其中的一個會成為限制因素。如果容量是瓶頸,我們會發現磁碟填滿時,應用程式只使用了磁碟磁臂所提供的IO能力的一部分;反過來,如果我們發現在新增資料時,磁碟磁臂已經被充分使用,但是磁碟還有剩餘空間,這就意味著IO能力是瓶頸。
設峰值期間的一次隨機的page IO使用所具有的開銷為:COSTp,它是基於對磁碟臂的使用算出的,同時作為multi-page block的一部分的一次磁碟page IO的開銷我們用COSTπ表示,該值要小很多,因為它將尋道時間和旋轉延時平攤到了多個page上。我們使用下面的這些名詞來表示各項儲存開銷:
COSTd= 1Mbytes磁碟儲存的開銷
COSTm= 1Mbytes記憶體儲存的開銷
COSTp=disk arm cost to provide 1 page/second IO rate, for random pages
COSTπ=disk arm cost to provide 1 page/second IO rate,as part of multi-page block IO
假設一個應用程式需要S Mbytes的資料儲存以及每秒H個random page訪問的IO傳輸需求(假設資料不會被快取),那麼磁碟臂的費用就是H·COSTp,磁碟儲存的費用就是S·COSTd。取決於哪個是瓶頸,剩下的那個就是免費可得的,這樣磁碟資料的訪問開銷COST-D就是:
COST-D=max(H·COSTp, S·COSTd)
在資料不會被快取的假設下,COST-D也就是該應用程式用於支援資料訪問的總開銷,COST-TOT。在這種情況下,如果總的磁碟儲存需求S是個常量,那麼總開銷就是隨隨機IO率H線性增長的。在磁碟IO上升到與磁碟儲存S相同的開銷時{!如果磁碟IO還未上升到與儲存S相同開銷時,它就是免費的,也就不需要考慮用記憶體支援它},就可以考慮使用記憶體快取來取代磁碟IO。假設在這些情況下,可以用記憶體快取來支援隨機IO請求,那麼磁碟的開銷就又只取決於所需的磁碟儲存空間,那麼訪問快取資料的開銷COST-B,就可以簡單地表示為記憶體的開銷加上磁碟儲存的開銷:
COST-B= S·COSTm+ S·COSTd
那麼現在用於支援該應用程式的資料訪問的總開銷就是由min(COST-D, COST-B)決定的:
COST-TOT=min(max(H·COSTp, S·COSTd), S·COSTm+ S·COSTd)
這樣隨著針對給定大小的資料S的頁面訪問頻率H的增長,COST-TOT就由三部分組成。如圖3.1,我們畫出了COST-TOT/MByte與H/S的變化關係。在S比較小的情況下,COST-TOT由磁碟儲存開銷S·COSTd決定,對於固定的S它是個常量。隨著H/S的值的增長,開銷逐漸由磁碟磁臂的使用開銷H·COSTp所控制,同時對於固定的S來說,它與H/S成正比。最後,在五分鐘法則所指出的記憶體駐留點上,主要因素變成了S·COSTm+ S·COSTd,這就主要由記憶體開銷來決定,因為COSTm >> COSTd。師從論文[6],我們將資料的熱度定義為H/S,同時我們命名出三個區域:cold,warm和hot。Hot資料足夠高的訪問頻率H,因此它的熱度H/S,表明它應該被放入記憶體。另一個區域,cold資料受限於磁碟空間:它所佔據的磁碟空間所能提供的IO能力已足夠使用。居於兩者之間的是warm資料,對於它來說磁碟磁臂是限制因素。它們之間的邊界劃分如下:
Tf= COSTd/ COSTp=cold和warm資料之間的分界點(freezing-冰點)
Tb= COSTm/ COSTp=warm和hot資料之間的分界點(boiling-沸點)
{!首先資料熱度是用H/S來定義的,而Tf和Tb不過是一種特殊的熱度,因此它們不過是代表了H/S= COSTd/ COSTp 和H/S= COSTm/ COSTp 的情況。簡單分析下H/S= COSTm/ COSTp的情況,此時H·COSTp=S·COSTm ,意味著該份資料所承受的隨機訪問開銷已經等於將其完全放入記憶體所需的記憶體開銷,可見其著實是很hot了}
通過使用COSTπ替代COSTp我們可以得出multi-page block訪問模式下的類似邊界。Warm和hot區域間的分界實際上就是五分鐘法則[13]的通用化。
[注:注意圖中橫座標的單位的理解,accesses/sec/Mbyte->( accesses/sec)/( Mbyte),其中accesses/sec即是H的單位,Mbyte則對應著S的單位]
正如[6]所說,在訪問很均勻的情況下,可以很容易地計算出一個資料庫表的熱度來。然而,熱度還依賴於具體的訪問方式:熱度實際上是與實際的磁碟訪問頻率相關,而不是邏輯上的插入頻率。可以這樣說,LSM-tree的成功之處就在於它減少了實際的磁碟訪問次數,因此就減低了索引的資料的熱度。該第6節中我們會重新討論下該觀點。
Multi-page block IO Advantage
通過採用multi-page block IO獲取的優勢對於幾個早期的訪問方式來說是至關重要的,比如Bounded Disorder files,SB-tree,和Log Structured files。一個1989年的IBM出版物針對DB2在IBM 3380磁碟上的效能進行了分析,給出了下面的結果:“…用於完成一次單個page讀取的時間大約是20ms(10ms用於尋道,8.3毫秒的旋轉延遲,1.7ms用於讀取)…用於執行一次順序式預讀[以64個連續頁面大小為單位]大約是125ms(10ms用於尋道,8.3毫秒的旋轉延遲,106.9ms用於讀取64條記錄[page]),這樣每個page只需2ms”。因此multi-page block IO情況下的2ms比上隨機IO下的20ms,也就是COSTπ/COSTp,大概等於1/10。最近的一個關於SCSI-2磁碟的分析,提到讀取4Kbyte頁面大小大概需要9ms的尋道時間,5.5ms的旋轉延遲,1.2ms的讀取時間,總共是16ms。而讀取64個連續的4Kbyte頁面,需要9ms的尋道時間,5.5ms的旋轉延遲,80ms的讀取時間,總共是95ms,算下來單個頁面需要95/64=1.5ms。COSTπ/COSTp仍還是大概等於1/10。
我們來分析一個具有1GByte(它的開銷大概是1000$)的SCSI-2磁碟的工作站,IO峰值大概是每秒60-70個IO請求。通常情況下的IO頻率要更低一些,大概是每秒40個IO請求。multi-page block IO的優勢是很明顯的。
1995年的一個典型工作站的成本
COSTm=$100/Mbytes
COSTd=$1/Mbytes
COSTp=$25/(IOs/sec)
COSTπ=$2.5/(IOs/sec)
Tf= COSTd/ COSTp=0.04IOs/(sec·MBytes) (freezing point)
Tb= COSTm/ COSTp=4IOs/(sec·MBytes) (boiling point)
我們通過Tb可以推匯出五分鐘法則所對應的時間間隔值τ,該值表明資料所維持的每秒的單個page的IO訪問開銷已經達到了用來儲存它所需的記憶體開銷,即:
(1/τ)·COSTp=pagesize·COSTm。
對τ進行求解,可得τ=(1/ pagesize)·(COSTp/ COSTm)=1/( pagesize·Tb),根據上面所給出的值,及pagesize=0.004Mbytes,可得τ=1/(0.004*4)=62.5seconds/IO。
例3.1 為了達到例1.1中TPC-A應用的1000TPS,首先這意味著針對Account表的H=2000 IOs/sec,它本身由100,000,000個100位元組的行組成,總大小S=10GBytes。此處的磁碟儲存開銷就是S·COSTd=$10,000,而磁碟IO開銷將是H·COSTp=$50,000。資料熱度T=H/S=2000/10,000=0.2,在冰點之上(是它的5倍,冰點是0.04,0.2/0.04=5),同時也還在浮點之下。該warm資料只用到了磁碟儲存能力的1/5,這樣瓶頸就是磁碟磁臂。例1.2中歷史記錄表的20天的Acct-ID||Timestamp索引也是類似的情況。正如我們在例1.2中的計算結果,這樣的一個B-樹索引大概需要9.2GBytes的葉級節點。如果樹只有70%的full,整個樹大概需要13.8GBytes的儲存,但是它具有與account表相同的IO請求率,這也意味著它們具有類似的資料熱度{!H值相同,S值一個是9.2,一個是13.8,因此H/S值相差不大,都屬於warm資料}。
3.2 LSM-tree與B-樹的IO開銷對比分析
我們將會分析下如下那些mergeable的索引操作的IO開銷:插入,刪除,更新,和long-latency find。下面的討論將會提供LSM-tree與B-樹的對比分析結果。
B-樹的插入開銷公式
考慮執行一次B-樹插入操作的磁碟磁臂開銷。首先需要對該記錄所需要放置到的樹中的位置進行訪問,這將會產生沿著樹節點的自上而下的搜尋過程。假設針對樹的後續插入是針對葉子上的某個隨機的位置,這樣就不能保證訪問路徑上的節點所在的頁面會因為之前的插入操作而進入記憶體。如果後續的插入是在一系列遞增的key-values,即insert-on-the-right的情況,這種情況就是一種不滿足上述假設的常見情況。需要指出的是,這種insert-on-the-right的情況是可以高效地被B-樹資料結構所處理的,因為如果B-樹一直是往右增長的話,只需要很少地IO開銷;事實上這也是B-樹所擅長處理的情況。現在已有很多其他類似的結構可以用來作為這種值不斷增長的日誌記錄的索引機制。
[21]提出了B-樹的實際深度(effective depth)概念,用De表示。它代表了在B樹的一次隨機查詢中,不在快取中的page的平均個數。對於例1.2中的用來索引Account-ID||timestamp的B-樹大小來說,De的值大約是2。
為了執行B-樹的一次插入,首先我們需要對葉級page執行一次key-value搜尋(需要De個IO操作),進行更新,然後寫出一個對應的髒頁(1次IO)。我們忽略相對不那麼頻繁的節點分裂帶來的影響。在這個過程中的page的讀寫都是開銷為COSTp的隨機訪問,這樣一次B-樹插入的總的IO開銷COST(B-ins)就是:
(3.1) COST(B-ins)= COSTp·(De+1)
LSM-tree的插入開銷公式
為了計算出LSM-tree的一次插入的開銷,我們需要對多次插入進行平攤分析,因為針對記憶體元件C0的單個插入只是不定期的對IO產生影響。正如我們在本節開始所解釋的,LSM-tree比B-樹相比,它的效能上的優勢基於兩種批處理模式的影響。第一個就是前面提到將單頁面IO的開銷降低為COSTπ。第二個就是,將新插入的記錄merge到C1中的延遲效果,這就允許被插入的記錄可以積累在C0中,這樣在C1的葉級page從磁碟讀入到記憶體在寫回到磁碟的過程中,可以一次將幾條記錄同時merge進來。與此相比,我們已經假設對於B-樹來說,很少會一次向一個葉級page中插入多於一條的記錄。
定義3.2.1 Batch-Merge引數M. 為了對這種multiple-entries-per-leaf的批量模式的影響進行量化,對於給定的一個LSM-tree,我們定義M為在rolling merge過程中,C1樹的每個單頁面葉子節點中來自於C0樹的記錄的平均個數。對於特定的LSM-tree來說,M基本上是一個很穩定的值,M的值實際上是由索引entry的大小和C1與C0的葉級資料大小比例來決定的。我們定義如下幾個大小引數:
Se=entry(index entry)size in bytes
Sp=page size in bytes
S0=size in Mbytes of C0 component leaf level
S1=size in Mbytes of C1 component leaf level
那麼,一個page裡的entries個數就是Sp/Se,LSM-tree中位於C0中的entries比例是S0/(S0+S1),那麼引數M可以表示為:
(3.2) M=(Sp/Se)·(S0/(S0+S1))
可以看出,C0比C1大的越多,M也越大。典型情況下,S1=40·S0,同時每個磁碟頁的記錄數,Sp/Se=200,此時M=5。給定M的情況下,我們就能夠給出LSM-tree的一次插入所具有的開銷的嚴格的公式化表示。我們簡單地將C1樹的葉級節點讀入和寫出的開銷,2·COSTπ,平攤到被merge到C1樹的葉級節點中的M次插入中。
(3.3) COST(LSM-ins)=2·COSTπ/M
需要注意的是,我們忽略了LSM-tree和B-樹中那些與索引更新產生的IO相關的但又無關緊要的開銷。
{!理解上面這些內容的關鍵在於理解平攤分析的思想,在LSM-tree中,插入操作實際上不是來了就真正執行地,而是會被儲存在C0中,這樣C0中的每條記錄實際上就對應著使用者的一次插入操作,然後這些插入操作實際上回被延遲到後面的rolling merge過程中,之後才會被反映到磁碟上,我們具體到rolling merge過程中的一個節點來看,該節點會首先從C1中讀入到記憶體,然後會與C0做merge,就看它能從C0中帶出幾條記錄,帶出多少條記錄也就意味著它將之前的多少個插入操作寫入到了磁碟,因此這一個節點的讀寫實際上就包含了之前的多個插入操作,也就是插入操作達到了批量化處理的目的。而C0中有多少條記錄會落在該節點內,則是與C0與C1的記錄總條數相關的,基本上C1中應該有佔S0/(S0+S1)是在C0中的。}
LSM-tree與B-樹插入開銷的比較
如果我們將這兩種資料結構所對應的開銷公式(3.1)和(3.3)進行比較,我們可以得到如下比率:
(3.4) COST(LSM-ins)/ COST(B-ins)=K1·(COSTπ/COSTp)·(1/M)
此處,K1是一個(near)常數,2/(De+1),對於我們所考慮的索引大小來說大概是0.67。上述公式表明,LSM-tree和B-樹的一次插入的IO開銷的比值與我們之前討論過的兩個批量處理模式是直接相關的:COSTπ/COSTp,一個很小的分數,代表了以multi-page block為單位的page IO和隨機的page IO之間的開銷之比,而1/M,M是rolling merge期間每個page所批量匯出的C0中的記錄數。通常,這兩個因素的乘積可以帶來接近兩個數量級的成本降低。當然,這種改進只有在資料熱度相對比較高的情況下才能體現出來,這樣將資料從B-樹遷移到LSM-tree將會大大減少所需的磁碟數。
例3.2 如果我們假設例1.2中的索引需要1GBytes的磁碟空間,但是需要存放到10GB的磁碟空間上以獲取必需的磁碟IO能力來支撐所需的磁碟訪問頻率。那麼其中對於磁碟磁臂開銷來說肯定存在提升空間。如果公式(3.4)給出的插入開銷比率是0.02=1/50,那麼這就意味著我們可以減少索引和磁碟開銷:LSM-tree只需要0.7GBytes的磁碟空間,因為它採用了被packed的記錄儲存方式,同時降低了磁碟所需的IO能力。但是,需要注意的是無論多麼有效的LSM-tree最多也只能將它降低到磁碟容量所需的那個開銷水平上。如果我們是針對從一個需要存放到35GB磁碟上所提供的IO能力的具有1GBytes大小的B-樹的話,那麼就完全達到上面所提到的那個1/50的成本提升。{!也就是說在10GB的情況下,如果按照上面1/50的比率來算,如果使用LSM-tree按理來說只需要10/50GB=0.2GB,但是另一方面為了滿足硬性的儲存需求,LSM-tree所需的磁碟空間不能小於0.7GBytes,因此實際上沒有完全發揮出LSM-tree所帶來的IO上的降低,也就是說這種情況下對於0.7 GBytes 的磁碟來說,它的IO能力還有空閒。但是對於35GB的情況來說,按照上面1/50的比率來算就剛好是0.7GB,這樣因使用LSM-tree所帶來的IO開銷上的降低就被全部利用了。}
相關文章
- The Log-Structured Merge-Tree(譯):中Struct
- The Log-Structured Merge-Tree(譯):下Struct
- mac上編譯FFmpegMac編譯
- 反編譯系列教程(上)編譯
- 譯文:影象優化(上)優化
- [譯]前端離線指南(上)前端
- 在CentOS 7上編譯QtumCentOS編譯QT
- windows上使用clang編譯程式Windows編譯
- 如何在Windows上編譯Docker?Windows編譯Docker
- 程式碼線上編譯器(上)- 編輯及編譯編譯
- [譯] GitHub 上的 12 個騷操作Github
- 深入剖析Java即時編譯器(上)Java編譯
- Hadoop - macOS 上編譯 Hadoop 3.2.1HadoopMac編譯
- [譯] 在 iOS 上使用 Carthage 建立依賴iOS
- 國慶,帶上 Google 翻譯探索城市Go
- LSM 優化系列(三)-- SILK- Preventing Latency Spikes in Log-Structured Merge Key-Value Stores ATC‘19優化Struct
- 給Android開發者的Flutter指南 (上) [翻譯]AndroidFlutter
- [譯] ProGuard 在 Android 上的使用姿勢Android
- [譯] TypeScript:擁有超能力的 JavaScript (上)TypeScriptJavaScript
- 在Ubuntu X64上編譯HadoopUbuntu編譯Hadoop
- [譯] 基於 Metal 的 ARKit 使用指南(上)
- SDL Guide 中文譯版(三上) (轉)GUIIDE
- Ubuntu上編譯多個版本的fridaUbuntu編譯
- Ubuntu上的pycrypto給出了編譯器錯誤Ubuntu編譯
- [譯] Flutter — 五個你會愛上它的原因Flutter
- [譯] iOS App 上一種靈活的路由方式iOSAPP路由
- CentOS64位上編譯Hadoop2.6.0CentOS編譯Hadoop
- 《千萬別上魔術的當》精彩譯文分享
- 0909 編譯原理 第1次上機編譯原理
- iOS15上線圖片翻譯功能,能取代專業翻譯軟體嗎?iOS
- mac上Apk反編譯工具合集整理與資源MacAPK編譯
- [譯] 構建世界上最快的會議網站網站
- [譯] 常用 Phpstorm tips (上/共3部分)PHPORM
- Vue 原始碼解讀(8)—— 編譯器 之 解析(上)Vue原始碼編譯
- Linux上安裝GCC編譯器過程(轉)LinuxGC編譯
- CentOS 8上安裝GCC實現開發編譯功能CentOSGC編譯
- [譯] Android 上一次編寫,隨處測試Android
- ES6 系列之 Babel 是如何編譯 Class 的(上)Babel編譯