Kafka索引設計、MySQL緩存池設計與算法的結合

前言

你好,我是yes。node

這篇文章從Kafka索引入手,來說述算法在工程上基於場景的靈活運用。面試

順帶也提了提 MySQL 緩存的設計細節。算法

這些精妙的細節設計,就是這些源碼的精華所在。數據庫

看源碼,就是取其精華,再模仿之,超越之。緩存

索引的重要性

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

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

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

索引在Kafka中的實踐

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

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

與之相關的源碼以下:

一、AbstractIndex.scala:抽象類,封裝了全部索引的公共操做

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

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

四、TransactionIndex.scala:事務索引,啓用Kafka事務以後纔會出現這個索引(本文暫不涉及事務相關內容)

索引類圖 先來看看AbstractIndex的定義

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的值的差值確定在整型的範圍內。

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

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

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

互相轉化的源碼以下,就這麼個簡單的操做:

上述解釋了位移值是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 的數據)

註釋還友好的給出了例子

簡單的來說,假設某索引佔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新增的時候緩存的訪問固定,或者只須要遷移少部分數據

好了,讓咱們先看看源碼是如何作的

實現並不難,可是爲什麼是把尾部的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,用於二分查找的頁面都能命中

> 2. 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淘汰機制 MySQL的緩衝池分區是爲了解決預讀失效緩存污染問題。

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

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

小結

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

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

不說了,頭有點冷。

歡迎關注個人公衆號【yes的練級攻略】,更多硬核文章等你來讀。

相關文章
相關標籤/搜索