<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【RocketMQ源碼分析】深入消息存儲(3)

          共 3756字,需瀏覽 8分鐘

           ·

          2021-04-12 23:34

          前文回顧

          【RocketMQ源碼分析】深入消息存儲(1)

          【RocketMQ源碼分析】深入消息存儲(2)

          文章評論可以掃描下方知識星球二維碼或者點擊底部閱讀原文。

          前面兩篇已經(jīng)說過了消息如何存儲到CommitLog,以及ConsumeQueue的構(gòu)建流程,到了第三篇,我們有一個不得不跨過的坎兒,MappedFile —— 內(nèi)存文件映射。

          MappedFile的存在是RocketMQ選擇將消息直接存儲到磁盤的關(guān)鍵因素,在第一篇CommitLog存儲流程開篇中,我就寫過一個思路。

          1. 即用到內(nèi)存又用到本地磁盤

          2. 填充和交換

          3. 文件映射到內(nèi)存

          4. 隨機讀接口去訪問

          這里出現(xiàn)的幾個關(guān)鍵句,都離不開本篇要說的MappedFile。

          RocketMQ既然要去與磁盤交互存儲文件,不同IO方法在性能差距上都是千差萬別的,怎么高效的與磁盤/內(nèi)存進行交互,是很多涉及存儲的中間件強大與否的重要標志。

          https://tianchi.aliyun.com/markets/tianchi/aliware2019

          這是幾年前天池中間件大賽的題目,目標就是設(shè)計一個利用有限內(nèi)存、較多磁盤空間來實現(xiàn)一個消息隊列,這樣看其實思路在第一篇就已經(jīng)說過了,重點是他要求這個隊列支持聚合操作。

          這讓我想到ElasticSearch的聚合場景,如果要實現(xiàn)那么復(fù)雜的聚合功能,也太南了吧。

          不過好在題目只是要求做指定時間段的消息加和,這無非就是維護一個消息存儲的偏移量與時間的存儲就好了。

          為了深入了解內(nèi)存文件映射,我們可以來讀讀它的源碼,這里相對于CommitLog、ConsumeQueue更加底層,更多涉及的是IO、Buffer、PageCache等知識。

          從頁表談到零拷貝

          在我過去學(xué)習(xí)匯編語言的時候,有兩個尋址相關(guān)的寄存器。

          段寄存器、變址寄存器。

          在8086的年代,地址總線是20位,但寄存器16位,尋址能力有限,為了保證1M的尋址能力,是將兩個16位寄存器一起使用,以段基址和偏移地址的形式,達到1M尋址能力。

          這個思想在操作系統(tǒng)保護模式下也是一樣的,假如我們有一臺32位操作系統(tǒng),內(nèi)存4GB。

          我們來思考一下它的內(nèi)存布局,內(nèi)核空間和用戶空間這是我們熟知的概念了,假如內(nèi)存空間不做任何操作,按順序性讓我們?nèi)ピL問,首先一個大問題就是內(nèi)存隔離,兩個進程之間如何做到內(nèi)存互不污染,這也引出了Java虛擬機內(nèi)存分配的一個問題,分配之后的內(nèi)存空間被垃圾回收器清理,剩下的空間大大小小可能不連續(xù),后續(xù)一個需要占據(jù)大內(nèi)存的對象可能無法存儲,JVM可以選擇回收-清理的方式保證沒有碎片,這是因為有棧上的引用指向堆,一個大對象就算被移動也不用擔(dān)心,但操作系統(tǒng)不同,如果想用類似JVM回收-清理的方式減少碎片內(nèi)存,首先一個要面對的問題就是地址變更,后續(xù)進程在尋址時可能找不到目標。

          此處需要注意地址變更,因為后面我們也會提到,操作系統(tǒng)的PageCache操作不當(dāng)也會引起這個問題。

          還有一個問題是,這種循序的空間并不安全,所有進程之間都可以互相訪問到對方的地址,這是一些修改器的常用手段。

          基于以上問題,操作系統(tǒng)入了保護模式,基于頁表將內(nèi)存空間調(diào)整為虛擬內(nèi)存,與實際的物理內(nèi)存區(qū)分開。

          現(xiàn)在的頁表通常是二級頁表,所謂兩級頁表就是對頁表再進行分頁,一個頁表內(nèi)的所有頁表項是連續(xù)存放的,頁表本質(zhì)上是一堆數(shù)據(jù),也是以頁為單位存放在內(nèi)存。

          第一級稱為頁目錄表。每個頁表的物理地址在頁目錄表中都以頁目錄項(PDE)的形式來存儲,4MB的頁表再次分頁可以分為1K(4MB/4KB)個頁,對每個頁的描述需要4個字節(jié),所以頁目錄表占用4K大小,正好是一個標準頁的大小,其指向第二級表。線性地址的高10位產(chǎn)生第一級的索引,由索引得到的表項中,指定并選擇了1K個二級表中的一個頁表。

          第二級稱為頁表,存放在一個4K大小的頁面中,包含1K個表項,每個表項包含一個頁的物理基地址。線性地址的中間10位產(chǎn)生第二級索引,可以獲得包含頁的物理地址的頁表項。這個物理地址的高20位與線性地址的低12位形成了最終的物理地址。

          有了頁表就能很好的劃分進程空間,以及減少碎片空間了,對于一個進程而言,理論上最大可使用空間為4GB。基于此,操作系統(tǒng)的內(nèi)存操作大多都是基于頁(4KB).

          虛擬內(nèi)存的映入使得操作系統(tǒng)管理劃分內(nèi)存更加方便,實際進行虛擬地址映射到物理地址的單元是MMU,mmap內(nèi)存文件映射也是一樣,通過MMU映射到文件。

          為了解決磁盤IO效率低下的問題,操作系統(tǒng)在進程空間內(nèi)增加了一片空間,用于與磁盤文件進行地址映射,這部分內(nèi)存也是虛擬內(nèi)存地址,通過指針操作這部分內(nèi)存,系統(tǒng)會自動將處理過的頁寫回對應(yīng)的磁盤文件位置,就不需要去調(diào)用系統(tǒng)read、write等函數(shù),內(nèi)核空間對這段區(qū)域的修改也直接反映用戶空間,從而可以實現(xiàn)不同進程間的文件共享。

          這部分內(nèi)存映射需要維護一份頁表,用于管理內(nèi)存——文件地址的映射關(guān)系,如果當(dāng)前虛擬內(nèi)存地址找不到對應(yīng)的物理地址,就會發(fā)生所謂的缺頁,缺頁時系統(tǒng)會根據(jù)地址偏移量在PageCache中查看目標地址是否已經(jīng)緩存過了,如果有就直接指向該PageCache地址,如果沒有就需要將目標文件加載入PageCache中。

          通過mmap的映射功能,就能避免IO操作,直接去操作內(nèi)存,這就是所謂的零拷貝技術(shù)。

          下面將要從幾幅圖說起IO到零拷貝。

          這是最普通的文件服務(wù)器傳輸文件過程,首先在內(nèi)核態(tài)將文件從物理設(shè)備讀取到內(nèi)核空間,這是一次直接直接內(nèi)存拷貝,然后用戶進程需要從內(nèi)核中將數(shù)據(jù)讀取到用戶進程空間,完成讀的流程,這是一次CPU拷貝,至此,讀的過程完成了,進程需要將數(shù)據(jù)發(fā)送給客戶端,這時有需要將數(shù)據(jù)放到內(nèi)核空間的socket處,之后通過協(xié)議層發(fā)送出去。

          這整個流程需要兩次CPU拷貝、兩次直接內(nèi)存拷貝,還需要不斷在內(nèi)核態(tài)用戶態(tài)切換。(第一種:四次)

          第二種模型是引入了mmap,在內(nèi)核空間與用戶空間建立映射關(guān)系,就可以讓socket空間直接操作內(nèi)核空間就能完成拷貝功能,還不需要在內(nèi)核態(tài)用戶態(tài)之間切換,write系統(tǒng)調(diào)用使內(nèi)核將數(shù)據(jù)從原始內(nèi)核緩沖區(qū)復(fù)制到與套接字關(guān)聯(lián)的內(nèi)核緩沖區(qū)中。

          這個方式使用mmap代替了read,雖然看上去減少了拷貝,但是存在風(fēng)險。當(dāng)映射一個文件到內(nèi)存,然后調(diào)用write,在另一個進程write同一個文件時,就會發(fā)生系統(tǒng)錯誤。(第二種:三次)

          第三種模型,基于Linux新增引入的sendfile系統(tǒng)調(diào)用,不僅能減少文件拷貝,還能減少系統(tǒng)切換,sendfile可以直接完成內(nèi)核空間的拷貝流程,從內(nèi)核空間拷貝到套接字空間,由此跳過了用戶空間。(第三種:三次)

          第四種模型,在內(nèi)核版本2.4中,對sendfile進行了優(yōu)化,可以直接從內(nèi)核空間將數(shù)據(jù)發(fā)送到協(xié)議器,還消除了到套接字區(qū)域的數(shù)據(jù)拷貝,對于用戶級應(yīng)用程序沒有任何變化。(第四種:兩次)

          綜上,數(shù)據(jù)發(fā)送的流程中數(shù)據(jù)不會結(jié)果多余的拷貝,內(nèi)核與用戶態(tài)空間內(nèi)都不會有多余的備份,這就是所謂的零拷貝技術(shù),基于sendfile與mmap。

          說回RocketMQ

          MQ是IO使用的大戶,MMap、FileChannel、RandomAccessFile是MQ文件操作最常使用的方法。

          RocketMQ支持MMap與FileChannel,默認使用MMap,在PageCache繁忙時,會使用FileChannel,同樣也可以避免PageCache競爭鎖。


          在MappedFile類中,可以看到FileChannel與MappedByteBuffer兩個變量,在Java代碼中可以通過FileChannel的map方法將文件映射到虛擬內(nèi)存。

          在MappedFile的init方法中也可以看到mmap初始化的過程。

          在實際的寫入流程中,操作的buffer可能是mmap也可能是TransientStorePool申請來的直接內(nèi)存,避免頁面被換出到交換區(qū)。

          TransientStorePool是否啟用根據(jù)TransientStorePoolEnable確定,當(dāng)開啟時,表示優(yōu)先使用堆外內(nèi)存存儲數(shù)據(jù),通過Commit線程刷到內(nèi)存映射Buffer中。

          TransientStorePool是一個簡易的池化類,其中包含了池的大小,每個單元存儲的大小,存儲單元的隊列以及存儲配置類。具體的初始化操作可以在init方法中看到有循環(huán)使用allocateDirect申請JVM外的內(nèi)存空間,相比于allocate申請到的JVM內(nèi)的內(nèi)存,堆外內(nèi)存操作更加迅速,免去了數(shù)據(jù)從堆外再次拷貝到堆內(nèi)的流程。

          申請到內(nèi)存后,取到了申請的內(nèi)存地址。

          Pointer pointer = new Pointer(address);LibC.INSTANCE.mlock(pointer, new NativeLong(fileSize));

          拿到地址后,創(chuàng)建一個指向該處的指針,調(diào)用本地鏈接庫的方法,將該地址的內(nèi)存鎖住,防止釋放。

          綜上,相信你已經(jīng)對頁表、文件系統(tǒng)IO操作有了一定的認識了。

          瀏覽 62
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美三级韩国三级日本三斤在线观看en | 欧洲亚洲激情 | 操逼片看看 | 在线午夜福利 | 免费看一级一级人妻片 |