Kafka索引設計有什麼亮點?

前言

其實這篇文章只是從Kafka索引入手,來講述算法在工程上基於場景的靈活運用。單單是因爲看源碼的時候有感而寫之。

索引的重要性

索引對於我們來說並不陌生,每一本書籍的目錄就是索引在現實生活中的應用。通過寥寥幾頁紙就得以讓我等快速查找需要的內容。冗餘了幾頁紙,縮短了查閱的時間。空間和時間上的互換,包含着宇宙的哲學。

工程領域上數據庫的索引更是不可或缺,沒有索引很難想象如此龐大的數據該如何檢索。

明確了索引的重要性,咱再來看看索引在Kafka裏是如何實現的。

索引在Kafka中的實踐

首先Kafka的索引是稀疏索引,這樣可以避免索引文件佔用過多的內存,從而可以在內存中保存更多的索引。對應的就是Broker 端參數log.index.interval.bytes 值,默認4KB,即4KB的消息建一條索引。

Kafka中有三大類索引:位移索引、時間戳索引和已中止事務索引。分別對應了.index、.timeindex、.txnindex文件。

與之相關的源碼如下:

1、AbstractIndex.scala:抽象類,封裝了所有索引的公共操作

2、OffsetIndex.scala:位移索引,保存了位移值和對應磁盤物理位置的關係

3、TimeIndex.scala:時間戳索引,保存了時間戳和對應位移值的關係

4、TransactionIndex.scala:事務索引,啓用Kafka事務之後纔會出現這個索引(本文暫不涉及事務相關內容)
索引類圖
先來看看AbstractIndex的定義

image

AbstractIndex的定義在代碼裏已經註釋了,成員變量裏面還有個entrySize。這個變量其實是每個索引項的大小,每個索引項的大小是固定的。

entrySize

OffsetIndex中是override def entrySize = 8,8個字節。
TimeIndex中是override def entrySize = 12,12個字節。

爲何是8 和12?

OffsetIndex中,每個索引項存儲了位移值和對應的磁盤物理位置,因此4+4=8,但是不對啊,磁盤物理位置是整型沒問題,但是AbstractIndex的定義baseOffset來看,位移值是長整型,不是因爲8個字節麼?

因此存儲的位移值實際上是相對位移值,即真實位移值-baseOffset的值

相對位移用整型存儲夠麼?夠,因爲一個日誌段文件大小的參數log.segment.bytes是整型,因此同一個日誌段對應的index文件上的位移值-baseOffset的值的差值肯定在整型的範圍內。

爲什麼要這麼麻煩,還要存個差值?

1、爲了節省空間,一個索引項節省了4字節,想想那些日消息處理數萬億的公司。

2、因爲內存資源是很寶貴的,索引項越短,內存中能存儲的索引項就越多,索引項多了直接命中的概率就高了。這其實和MySQL InnoDB 爲何建議主鍵不宜過長一樣。每個輔助索引都會存儲主鍵的值,主鍵越長,每條索引項佔用的內存就越大,緩存頁一次從磁盤獲取的索引數就越少,一次查詢需要訪問磁盤次數就可能變多。而磁盤訪問我們都知道,很慢。

互相轉化的源碼如下,就這麼個簡單的操作:
image

上述解釋了位移值是4字節,因此TimeIndex中時間戳8個字節 + 位移值4字節 = 12字節。

_warmEntries

這個是幹什麼用的?

首先思考下我們能通過索引項快速找到日誌段中的消息,但是我們如何快速找到我們想要的索引項呢?一個索引文件默認10MB,一個索引項8Byte,因此一個文件可能包含100多W條索引項。

不論是消息還是索引,其實都是單調遞增,並且都是追加寫入的,因此數據都是有序的。在有序的集合中快速查詢,腦海中突現的就是二分查找了!

那就來個二分!

二分查找

這和_warmEntries有什麼關係?首先想想二分有什麼問題?

就Kafka而言,索引是在文件末尾追加的寫入的,並且一般寫入的數據立馬就會被讀取。所以數據的熱點集中在尾部。並且操作系統基本上都是用頁爲單位緩存和管理內存的,內存又是有限的,因此會通過類LRU機制淘汰內存。

看起來LRU非常適合Kafka的場景,但是使用標準的二分查找會有缺頁中斷的情況,畢竟二分是跳着訪問的。

這裏要說一下kafka的註釋寫的是真的清晰,咱們來看看註釋怎麼說的

when looking up index, the standard binary search algorithm is not cache friendly, and can cause unnecessary
page faults (the thread is blocked to wait for reading some index entries from hard disk, as those entries are not
cached in the page cache)

翻譯下:當我們查找索引的時候,標準的二分查找對緩存不友好,可能會造成不必要的缺頁中斷(線程被阻塞等待從磁盤加載沒有被緩存到page cache 的數據)

註釋還友好的給出了例子
image

簡單的來講,假設某索引佔page cache 13頁,此時數據已經寫到了12頁。按照kafka訪問的特性,此時訪問的數據都在第12頁,因此二分查找的特性,此時緩存頁的訪問順序依次是0,6,9,11,12。因爲頻繁被訪問,所以這幾頁一定存在page cache中。

當第12頁不斷被填充,滿了之後會申請新頁第13頁保存索引項,而按照二分查找的特性,此時緩存頁的訪問順序依次是:0,7,10,12。這7和10很久沒被訪問到了,很可能已經不再緩存中了,然後需要從磁盤上讀取數據。註釋說:在他們的測試中,這會導致至少會產生從幾毫秒跳到1秒的延遲。

基於以上問題,Kafka使用了改進版的二分查找,改的不是二分查找的內部,而且把所有索引項分爲熱區和冷區

這個改進可以讓查詢熱數據部分時,遍歷的Page永遠是固定的,這樣能避免缺頁中斷。

看到這裏其實我想到了一致性hash,一致性hash相對於普通的hash不就是在node新增的時候緩存的訪問固定,或者只需要遷移少部分數據

好了,讓我們先看看源碼是如何做的
image

實現並不難,但是爲何是把尾部的8192作爲熱區?

這裏就要再提一下源碼了,講的很詳細。

  1. This number is small enough to guarantee all the pages of the “warm” section is touched in every warm-section lookup. So that, the entire warm section is really “warm”.
    When doing warm-section lookup, following 3 entries are always touched: indexEntry(end), indexEntry(end-N), and indexEntry((end*2 -N)/2). If page size >= 4096, all the warm-section pages (3 or fewer) are touched, when we
    touch those 3 entries. As of 2018, 4096 is the smallest page size for all the processors (x86-32, x86-64, MIPS, SPARC, Power, ARM etc.).

大致內容就是現在處理器一般緩存頁大小是4096,那麼8192可以保證頁數小於等3,用於二分查找的頁面都能命中

  1. This number is large enough to guarantee most of the in-sync lookups are in the warm-section. With default Kafka settings, 8KB index corresponds to about 4MB (offset index) or 2.7MB (time index) log messages.

8KB的索引可以覆蓋 4MB (offset index) or 2.7MB (time index)的消息數據,足夠讓大部分在in-sync內的節點在熱區查詢

以上就解釋了什麼是_warmEntries,並且爲什麼需要_warmEntries

可以看到樸素的算法在真正工程上的應用還是需要看具體的業務場景的,不可生搬硬套。並且徹底的理解算法也是很重要的,例如死記硬背二分,怕是看不出來以上的問題。還有底層知識的重要性。不然也是看不出來對緩存不友好的。

從Kafka的索引冷熱分區到MySQL InnoDB的緩衝池管理

從上面這波冷熱分區我又想到了MySQL的buffer pool管理。MySQL的將緩衝池分爲了新生代和老年代。默認是37分,即老年代佔3,新生代佔7。即看作一個鏈表的尾部30%爲老年代,前面的70%爲新生代。替換了標準的LRU淘汰機制
image

MySQL的緩衝池分區是爲了解決預讀失效緩存污染問題。

1、預讀失效:因爲會預讀頁,假設預讀的頁不會用到,那麼就白白預讀了,因此讓預讀的頁插入的是老年代頭部,淘汰也是從老年代尾部淘汰。不會影響新生代數據。

2、緩存污染:在類似like全表掃描的時候,會讀取很多冷數據。並且有些查詢頻率其實很少,因此讓這些數據僅僅存在老年代,然後快速淘汰纔是正確的選擇,MySQL爲了解決這種問題,僅僅分代是不夠的,還設置了一個時間窗口,默認是1s,即在老年代被再次訪問並且存在超過1s,纔會晉升到新生代,這樣就不會污染新生代的熱數據。

小結

文章先從索引入手,這就是時間和空間的互換。然後引出Kafka中索引存儲使用了相對位移值,節省了空間,並且講述了索引項的訪問是由二分查找實現的,並結合Kafka的使用場景解釋了Kafka中使用的冷熱分區實現改進版的二分查找,並順帶提到了下一致性Hash,再由冷熱分區聯想到了MySQL緩衝池變形的LRU管理。

這一步步實際上都體現算法在工程中的靈活運用和變形實現。有些同學認爲算法沒用,刷算法題只是爲了面試,實際上各種中間件和一些底層實現都體現了算法的重要性。

不說了,頭有點冷。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章