天天看點

Linux系統中的page cache和buffer cache的概念、機制及kafka、redis等産品如何利用page cache1 page cache & buffer cache2 大資料産品針對page cache的利用3 page cache相關的參數參考

1 page cache & buffer cache

1.1 概念

Page cache緩存檔案的頁以優化檔案IO。

Buffer cache緩存塊裝置的塊以優化塊裝置IO。

頁是邏輯上的概念,是以page cache是與檔案系統同級的;塊是實體上的概念,是以buffer cache是與塊裝置驅動程式同級的。

  • 在Linux 2.4版本的核心之前,page cache與buffer cache是完全分離的。但是,塊裝置大多是磁盤,磁盤上的資料又大多通過檔案系統來組織,這種設計導緻很多資料被緩存了兩次,浪費記憶體。
  • 在2.4版本核心之後,兩塊緩存近似融合在了一起:如果一個檔案的頁加載到了page cache,那麼同時buffer cache隻需要維護塊指向頁的指針就可以了。隻有那些沒有檔案表示的塊,或者繞過了檔案系統直接操作(如dd指令)的塊,才會真正放到buffer cache裡。是以,我們現在提起page cache,基本上都同時指page cache和buffer cache兩者,本文之後也不再區分,直接統稱為buffer cache。

無論如何核心要對塊裝置執行基于塊的IO而不是虛拟記憶體page。由于絕大部分的塊表示檔案的資料部,是以大部分的buffer cache都可由page cache表達,但是有少量的比如檔案中繼資料仍然是保留在buffer cache中來緩存。

1.1.1 Page Cache(頁緩存)

  • 由記憶體中的實體page組成,其内容對應磁盤上的block。
  • page cache的大小是動态變化的。
  • backing store:cache緩存的儲存設備
  • 一個page通常包含多個block, 而block不一定是連續的。

讀Cache

  • 當核心發起一個讀請求時, 先會檢查請求的資料是否緩存到了page cache中。
    • 如果有,那麼直接從記憶體中讀取,不需要通路磁盤, 此即 cache hit(緩存命中)
    • 如果沒有, 就必須從磁盤中讀取資料, 然後核心将讀取的資料再緩存到cache中, 如此後續的讀請求就可以命中緩存了。
  • page可以值緩存一個檔案的部分内容, 而不需要把整個檔案都緩存進來。

寫Cache

  • 當核心發起一個寫請求時, 也是直接往cache中寫入, 後備存儲中的内容不會直接更新。
  • 核心會将被寫入的page标記為dirty, 并将其加入到dirty list中。
  • 核心會周期性地将dirty list中的page寫回到磁盤上, 進而使磁盤上的資料和記憶體中緩存的資料一緻。

Cache回收

  • Page cache的另一個重要工作是釋放page, 進而釋放記憶體空間。
    • cache回收的任務是選擇合适的page釋放
    • 如果page是dirty的, 需要将page寫回到磁盤中再釋放。

LRU算法

LRU(Least Recently used): 最近最少使用算法, Redis中也有此政策, 該算法在Java中可以使用LinkedHashMap進行實作。

Two-List政策

  • Two-List政策維護了兩個list: active list && inactive list
    • 在active list上的page被認為是hot的,不能釋放。
    • 隻有inactive list上的page可以被釋放的。
  • 首次緩存的資料的page會被加入到inactive list中,已經在inactive list中的page如果再次被通路,就會移入active list中。
  • 兩個連結清單都使用了僞LRU算法維護,新的page從尾部加入,移除時從頭部移除
  • 如果active list中page的數量遠大于inactive list,那麼active list頭部的頁面會被移入inactive list中,進而實作兩個表的平衡。

1.1.2 Buffer Cache

  • buffer cache就是一塊含有許多資料塊的記憶體區域,這些資料塊主要都是資料檔案裡的資料塊内容的拷貝。
  • 從buffer cache中讀取一個資料塊一般需要100ns左右,從一般的存儲硬碟中讀取一個資料塊需要10ms;是以大概算一下,從記憶體中讀取資料塊比從硬碟中快近十萬倍。
  • 每一個資料塊在被讀入buffer cache時,都會先在buffer cache中構造一個buffer header,buffer header與資料塊一一對應(buffer header 中有指定buffer 具體記憶體位址的資訊)。
  • 值得注意的是在Linux2.4中,buffer cache和 page cache之間是獨立的
    • 前者使用老版本的buffer_head進行存儲,這導緻了一個磁盤block可能在兩個cache中同時存在,造成了記憶體的浪費。
    • 2.6核心中将兩者合并到了一起,使buffer_head隻存儲buffer-block的映射資訊,不再存儲block的内容, 進而減少了記憶體浪費。

1.1.3 Flusher Threads

  • Page Cache推遲了檔案寫入後備存儲的時間, 但是dirty page最終還是要被寫回磁盤的。
  • 核心會在以下三種情況下将dirty page 寫回磁盤:
    • 使用者程序調用sync() 和 fsync()系統調用
    • 空閑記憶體低于特定的門檻值(threshold)
    • Dirty資料在記憶體中駐留的時間超過一個特定的門檻值
  • 線程群的特點是讓一個線程負責一個儲存設備(比如一個磁盤驅動器),多少個儲存設備就用多少個線程, 進而避免阻塞或者競争的情況,提高效率。
  • 當空閑記憶體低于門檻值時,核心就會調用wakeup_flusher_threads()來喚醒一個或者多個flusher線程,将資料寫回磁盤。
  • 為了避免dirty資料在記憶體中駐留過長時間(避免在系統崩潰時丢失過多資料),核心會定期喚醒一個flusher線程,将駐留時間過長的dirty資料寫回磁盤。

1.2 作用

page cache與buffer cache的共同目的都是加速資料I/O:

  • 寫資料時首先寫到緩存,将寫入的頁标記為dirty,然後向外部存儲flush,也就是緩存寫機制中的write-back(另一種是write-through,Linux未采用);
  • 讀資料時首先讀取緩存,如果未命中,再去外部存儲讀取,并且将讀取來的資料也加入緩存。

作業系統總是積極地将所有空閑記憶體都用作page cache和buffer cache,當記憶體不夠用時也會用LRU等算法淘汰緩存頁。

緩存(cache)與緩沖(buffer)的主要差別

  • Buffer的核心作用是用來緩沖,緩和沖擊。比如你每秒要寫100次硬碟,對系統沖擊很大,浪費了大量時間在忙着處理開始寫和結束寫這兩件事嘛。用個buffer暫存起來,變成每10秒寫一次硬碟,對系統的沖擊就很小,寫入效率高了。
  • Cache的核心作用是加快取用的速度。比如一個很複雜的計算做完了,下次還要用結果,就把結果放手邊一個好拿的地方存着,下次不用再算了。加快了資料取用的速度。
  • page cache用于優化檔案系統的I/O,buffer cache用于優化磁盤的I/O
  • page cache常用于讀操作的時候,将常常讀取的file緩存起來;buffer cache則是将要寫入磁盤的内容緩沖(零存整取)。

1.3 緩存大小如何檢視

執行free指令,注意到會有一列名為buff/cache【在centos 7.2上執行】,

buff/cache即表示目前頁、塊緩存的大小,如截圖,占用了43GB

Linux系統中的page cache和buffer cache的概念、機制及kafka、redis等産品如何利用page cache1 page cache & buffer cache2 大資料産品針對page cache的利用3 page cache相關的參數參考

1.4 IO流

如下圖:

  • IO控制流就是帶着中繼資料的流轉,driver中會管理着buffer cache、檔案的中繼資料資訊;
  • 資料直接按照資料流從page cache刷入disk的cache(磁盤也是有自己的cache的)。
    Linux系統中的page cache和buffer cache的概念、機制及kafka、redis等産品如何利用page cache1 page cache & buffer cache2 大資料産品針對page cache的利用3 page cache相關的參數參考
    對于具體的Linux檔案系統,會以block(磁盤塊)的形式組織檔案,為了減少對實體塊裝置的通路,在檔案以塊的形式調入記憶體後,使用塊高速緩存進行管理。每個緩沖區由兩部分組成,第一部分稱為緩沖區首部,用資料結構buffer_head表示,第二部分是真正的存儲的資料。由于緩沖區首部不與資料區域相連,資料區域獨立存儲。因而在緩沖區首部中,有一個指向資料的指針和一個緩沖區長度的字段。
    Linux系統中的page cache和buffer cache的概念、機制及kafka、redis等産品如何利用page cache1 page cache & buffer cache2 大資料産品針對page cache的利用3 page cache相關的參數參考

1.5 兩類緩存的邏輯關系

從linux-2.6.18的核心源碼來看, Page Cache和Buffer Cache是一個事物的兩種表現:對于一個Page而言,對上,它是某個File的一個Page Cache,而對下,它同樣是一個Device上的一組Buffer Cache 。

Linux系統中的page cache和buffer cache的概念、機制及kafka、redis等産品如何利用page cache1 page cache & buffer cache2 大資料産品針對page cache的利用3 page cache相關的參數參考

File在位址空間上,以4K(page size)為機關進行切分,每一個4k都可能對應到一個page上(這裡 可能 的含義是指,隻有被緩存的部分,才會對應到page上,沒有緩存的部分,則不會對應),而這個4k的page,就是這個檔案的一個Page Cache。

而對于落磁盤的一個檔案而言,最終這個4k的page cache,還需要映射到一組磁盤block對應的buffer cache上,假設block為1k,那麼每個page cache将對應一組(4個)buffer cache,而每一個buffer cache,則有一個對應的buffer cache與device block映射關系的描述符:buffer_head,這個描述符記錄了這個buffer cache對應的block在磁盤上的具體位置。

Linux系統中的page cache和buffer cache的概念、機制及kafka、redis等産品如何利用page cache1 page cache & buffer cache2 大資料産品針對page cache的利用3 page cache相關的參數參考

2 大資料産品針對page cache的利用

2.1 Kafka對page cache的利用

Kafka為什麼不自己管理緩存,而非要用page cache?原因有如下三點:

  • JVM中一切皆對象,資料的對象存儲會帶來所謂object overhead,浪費空間;
  • 如果由JVM來管理緩存,會受到GC的影響,并且過大的堆也會拖累GC的效率,降低吞吐量;
  • 一旦程式崩潰,自己管理的緩存資料會全部丢失。

    Kafka三大件(broker、producer、consumer)與page cache的關系可以用下面的簡圖來表示。

    Linux系統中的page cache和buffer cache的概念、機制及kafka、redis等産品如何利用page cache1 page cache & buffer cache2 大資料産品針對page cache的利用3 page cache相關的參數參考

    producer生産消息發到Server端時,會調用writeFullyTo【會調用Java NIO中是FileChannel.write() API】按偏移量寫入資料,并且都會先寫入page cache裡。consumer消費消息時,會使用sendfile()系統調用【對應FileChannel.transferTo() API】,零拷貝地将資料從page cache傳輸到broker的Socket buffer,再通過網絡傳輸。

    具體參見 org.apache.kafka.common.record.FileRecords中的

public int append(MemoryRecords records) throws IOException {
        if (records.sizeInBytes() > 2147483647 - this.size.get()) {
            throw new IllegalArgumentException("Append of size " + records.sizeInBytes() + " bytes is too large for segment with current file position at " + this.size.get());
        } else {
            int written = records.writeFullyTo(this.channel);
            this.size.getAndAdd(written);
            return written;
        }
    }

 public int writeFullyTo(GatheringByteChannel channel) throws IOException {
        this.buffer.mark();

        int written;
        for(written = 0; written < this.sizeInBytes(); written += channel.write(this.buffer)) {
        }

        this.buffer.reset();
        return written;
    }
 @Override
    public long writeTo(GatheringByteChannel destChannel, long offset, int length) throws IOException {
        long newSize = Math.min(channel.size(), end) - start;
        int oldSize = sizeInBytes();
        if (newSize < oldSize)
            throw new KafkaException(String.format(
                    "Size of FileRecords %s has been truncated during write: old size %d, new size %d",
                    file.getAbsolutePath(), oldSize, newSize));

        long position = start + offset;
        int count = Math.min(length, oldSize);
        final long bytesTransferred;
        if (destChannel instanceof TransportLayer) {
            TransportLayer tl = (TransportLayer) destChannel;
            bytesTransferred = tl.transferFrom(channel, position, count);
        } else {
            bytesTransferred = channel.transferTo(position, count, destChannel);
        }
        return bytesTransferred;
    }
           

圖中沒有畫出來的還有leader與follower之間的同步,這與consumer是同理的:隻要follower處在ISR【同步的replica,相對的就有out of sync replica,也就是跟不上同步節奏的replica】中,就也能夠通過零拷貝機制将資料從leader所在的broker page cache傳輸到follower所在的broker。

同時,page cache中的資料會随着核心中flusher線程的排程以及對sync()/fsync()的調用寫回到磁盤,就算程序崩潰,也不用擔心資料丢失。另外,如果consumer要消費的消息不在page cache裡,才會去磁盤讀取,并且會順便預讀出一些相鄰的塊放入page cache,以友善下一次讀取。

如果Kafka producer的生産速率與consumer的消費速率相差不大,那麼就能幾乎隻靠對broker page cache的讀寫完成整個生産-消費過程,磁盤通路非常少。這個結論俗稱為“讀寫空中接力”。并且Kafka持久化消息到各個topic的partition檔案時,是隻追加的順序寫,充分利用了磁盤順序通路快的特性,效率高。

2.2 redis AOF對page cache的利用

當 Redis 開啟 AOF 時,需要配置 AOF 的刷盤政策。

基于性能和資料安全的平衡,你肯定會采用 append fsync everysec 這種方案。

這種方案的工作模式為,Redis 的背景線程每間隔 1 秒,就把 AOF page cache 的資料,刷到磁盤(fsync)上。

這種方案的優勢在于,把 AOF 刷盤的耗時操作,放到了背景線程中去執行,避免了對主線程的影響。

AOF everysec 真的不會阻塞主線程嗎?

Redis 背景線程在執行 AOF page cache 刷盤(fysnc)時,如果此時磁盤 IO 負載過高,那麼調用 fsync 就會被阻塞住。

此時,主線程仍然接收寫請求進來,那麼此時的主線程會先判斷,上一次背景線程是否已刷盤成功。

如何判斷呢?

背景線程在刷盤成功後,都會記錄刷盤的時間。

主線程會根據這個時間來判斷,距離上一次刷盤已經過去多久了。整個流程是這樣的:

  • 主線程在寫 AOF page cache(write系統調用)前,先檢查背景 fsync 是否已完成?
    • fsync 已完成,主線程直接寫 AOF page cache
    • fsync 未完成,則檢查距離上次 fsync 過去多久?
      • 如果距離上次 fysnc 成功在 2 秒内,那麼主線程會直接傳回,不寫 AOF page cache
      • 如果距離上次 fysnc 成功超過了 2 秒,那主線程會強制寫 AOF page cache(write系統調用)

由于磁盤 IO 負載過高,此時,背景線程 fynsc 會發生阻塞,那主線程在寫 AOF page cache 時,也會發生阻塞等待(操作同一個 fd,fsync 和 write 是互斥的,一方必須等另一方成功才可以繼續執行,否則阻塞等待)

Linux系統中的page cache和buffer cache的概念、機制及kafka、redis等産品如何利用page cache1 page cache &amp; buffer cache2 大資料産品針對page cache的利用3 page cache相關的參數參考

通過分析可以發現,即使你配置的 AOF 刷盤政策是 appendfsync everysec,也依舊會有阻塞主線程的風險。

其實,産生這個問題的重點在于,磁盤 IO 負載過高導緻 fynsc 阻塞,進而導緻主線程寫 AOF page cache 也發生阻塞。

是以,你一定要保證磁盤有充足的 IO 資源,避免這個問題。

3 page cache相關的參數

page cache中的資料會随着核心中flusher線程的排程寫回磁盤。與它相關的有以下4個參數,必要時可以調整。

  • /proc/sys/vm/dirty_writeback_centisecs:flush檢查的周期。機關為0.01秒,預設值500,即5秒。每次檢查都會按照以下三個參數控制的邏輯來處理。
  • /proc/sys/vm/dirty_expire_centisecs:如果page cache中的頁被标記為dirty的時間超過了這個值,就會被直接刷到磁盤。機關為0.01秒。預設值3000,即半分鐘。
  • /proc/sys/vm/dirty_background_ratio:如果dirty page的總大小占空閑記憶體量的比例超過了該值,就會在背景排程flusher線程異步寫磁盤,不會阻塞目前的write()操作。預設值為10%。
  • /proc/sys/vm/dirty_ratio:如果dirty page的總大小占總記憶體量的比例超過了該值,就會阻塞所有程序的write()操作,并且強制每個程序将自己的檔案寫入磁盤。預設值為20%。

由此可見,調整空間比較靈活的是參數2、3,而盡量不要達到參數4的門檻值,代價太大了。

使用sysctl -a | frep dirty指令可以檢視預設配置

Linux系統中的page cache和buffer cache的概念、機制及kafka、redis等産品如何利用page cache1 page cache &amp; buffer cache2 大資料産品針對page cache的利用3 page cache相關的參數參考

參考

Page Cache(頁緩存)

Linux檔案系統(五)—三大緩沖區之buffer塊緩沖區

【kafka】源碼分析-Producer過程全解

Kafka 源碼解析:Server 端的運作過程

KAFKA進階:【十一】總結一下,KAFKA的高并發、高吞吐等特性?

颠覆認知——Redis會遇到的15個“坑”,你踩過幾個?

繼續閱讀