ceph的數據存儲之路(4) ----- rbd client 端的數據請求處理

2016-11-01更新 start:--------------------------------------------------------------------------------------python

最近比較多的人私下問我,改了ceph的源碼,從新編譯了,可是在使用本節提供的python腳本測試librbd的時候出現了錯誤,怎麼解決出現的這個錯誤。這應該是個好現象,不少人都深刻到代碼層級了,這個也是開始代碼之旅的重要環節,今天在這裏更新說明下。以前在第二節https://my.oschina.net/u/2460844/blog/515353中講述了怎麼編譯源碼,可是在使用本節提供的腳本時會出現一些問題,這些問題來自於使用了源碼編譯後,在python腳本中調用失敗。問題的緣由是我忘記添加上了一個環節,從新編譯後的代碼要替換一些庫文件,首先來看下提示的錯誤有哪些,錯誤以下:c++

1.ImportError: No module named rados算法

root@cephmon:~/ceph/ceph-0.94.2/python# python create_rbd.py 
Traceback (most recent call last):
  File "create_rbd.py", line 2, in <module>
    import sys,rados,rbd
ImportError: No module named rados

2.OSError: librados.so.2: cannot open shared object file: No such file or directorysession

root@cephmon:~/ceph/ceph-0.94.2/python# python create_rbd.py
Traceback (most recent call last):
  File "create_rbd.py", line 18, in <module>
    connectceph()
  File "create_rbd.py", line 4, in connectceph
    cluster = rados.Rados(conffile = '/root/ceph/ceph-0.94.2/src/ceph.conf')
  File "/usr/lib/python2.7/rados.py", line 215, in __init__
    self.librados = CDLL(library_path if library_path is not None else 'librados.so.2')
  File "/usr/lib/python2.7/ctypes/__init__.py", line 365, in __init__
    self._handle = _dlopen(self._name, mode)
OSError: librados.so.2: cannot open shared object file: No such file or directory

出現這兩個問題的緣由是 源碼編譯後,python 腳本沒法找到對應的庫文件(或者python文件)。數據結構

針對問題1.拷貝源碼包下面的python腳本 cp  ../ceph-0.94.2/src/pybind/*  /usr/lib/python2.7/python2.7

針對問題2.拷貝最新編譯出來的的librados到/usr/lib/ 目錄下便可, 這個最新編譯出來的librados在目錄../ceph-0.94.2/src/.lib/目錄中,該目錄是一個隱藏目錄,容易被忽略。在該目錄下找到librados.so.2 和librbd.so.1 拷貝到 /usr/lib/ 下。socket

解決了問題1和問題2,腳本就能夠正常的運行了。函數

針對問題2,OSError: librados.so.2: cannot open shared object file: No such file or directory 網上常常有人在用源碼部署ceph的時候都出現了這個問題,好像沒人特別準確的回答這個問題。緣由就是最新編譯出的librados沒有拷貝到/usr/lib下,腳本或者程序找不到這個庫因此報錯。解決辦法如上便可。測試

2016-11-01更新 end:--------------------------------------------------------------------------------------ui

講ceph的文章有不少,可是都是從高大尚的理論出發,看了不少這樣的文章收穫不少,可是總有一種不能實際抓住ceph的命門,不能切脈,不少時候可能看完就忘了。從這篇博客開始拋開高大尚的理論,從最接地氣的方式開始,能夠幫助那些須要開發ceph童鞋們,或者想深刻了解ceph實現的童鞋們。這裏用最接地氣的方式講述ceph背後的故事。

首先明白ceph就是用來存儲的,這個系列的博客就講述如何ceph的讀寫請求的一輩子,本節講述數據寫操做的生命開始。

首先看一下咱們用python調用librbd 寫rbd設備的測試代碼:

#!/usr/bin/env python
import sys,rados,rbd
def connectceph():
      cluster = rados.Rados(conffile = '/root/xuyanjiangtest/ceph-0.94.3/src/ceph.conf')
      cluster.connect()
      ioctx = cluster.open_ioctx('mypool')
      rbd_inst = rbd.RBD()
      size = 4*1024**3 #4 GiB
      rbd_inst.create(ioctx,'myimage',size)
      image = rbd.Image(ioctx,'myimage')
      data = 'foo'* 200
      image.write(data,0)
      image.close()
      ioctx.close()
        cluster.shutdown()
 
if __name__ == "__main__":
        connectceph()

 

1、寫操做數據request的孕育過程

在write request 請求開始以前,它須要準備點旅行的用品,往返的機票等。下面先看看前期準備了什麼。

1. 首先cluster = rados.Rados(conffile = 'XXXX/ceph.conf'),用當前的這個ceph的配置文件去建立一個rados,這裏主要是解析ceph.conf中寫明的參數。而後將這些參數的值保存在rados中。

2. cluster.connect() ,這裏將會建立一個radosclient的結構,這裏會把這個結構主要包含了幾個功能模塊:消息管理模塊Messager,數據處理模塊Objector,finisher線程模塊。這些模塊具體的工做後面講述。

3. ioctx = cluster.open_ioctx('mypool'),爲一個名字叫作mypool的存儲池建立一個ioctx ,ioctx中會指明radosclient與Objector模塊,同時也會記錄mypool的信息,包括pool的參數等。

4. rbd_inst.create(ioctx,'myimage',size) ,建立一個名字爲myimage的rbd設備,以後就是將數據寫入這個設備。

5. image = rbd.Image(ioctx,'myimage'),建立image結構,這裏該結構將myimage與ioctx 聯繫起來,後面能夠經過image結構直接找到ioctx。這裏會將ioctx複製兩份,分爲爲data_ioctx和md_ctx。見明知意,一個用來處理rbd的存儲數據,一個用來處理rbd的管理數據。

經過上面的操做就會造成這樣的結構(以下圖)

 

圖1-1 request孕育階段

 

過程描述,首先根據配置文件建立一個rados,接下來爲這個rados建立一個radosclient,radosclient包含了3個主要模塊(finisher,Messager,Objector)。再根據pool建立對應的ioctx,ioctx中可以找到radosclient。再對生成對應rbd的結構image,這個image中複製了兩個ioctx,分別成爲了md_ioctx與data_ioctx。這時徹底能夠根據image入口去查找到前期準備的其餘數據結構。接下來的數據操做徹底從image開始,也是rbd的具體實例。

 

2、request的出生和成長。

1. image.write(data,0),經過image開始了一個寫請求的生命的開始。這裏指明瞭request的兩個基本要素 buffer=data 和 offset=0。由這裏開始進入了ceph的世界,也是c++的世界。

由image.write(data,0)  轉化爲librbd.cc 文件中的Image::write() 函數,來看看這個函數的主要實現

ssize_t Image::write(uint64_t ofs, size_t len, bufferlist& bl)
  {  
      //…………………
   ImageCtx *ictx = (ImageCtx *)ctx;
    int r = librbd::write(ictx, ofs, len, bl.c_str(), 0);
    return r;     
  }

 

2. 該函數中直接進行分發給了librbd::wrte的函數了。跟隨下來看看librbd::write中的實現。該函數的具體實如今internal.cc文件中。

ssize_t write(ImageCtx *ictx, uint64_t off, size_t len, const char *buf, int op_flags)
 {
      ……………
    Context *ctx = new C_SafeCond(&mylock, &cond, &done, &ret);   //---a
    AioCompletion *c = aio_create_completion_internal(ctx, rbd_ctx_cb);//---b
   r = aio_write(ictx, off, mylen, buf, c, op_flags);  //---c
     ……………     
    while (!done)
           cond.Wait(mylock);  // ---d
      ……………
}

 

---a.這句要爲這個操做申請一個回調操做,所謂的回調就是一些收尾的工做,信號喚醒處理。

---b。這句是要申請一個io完成時 要進行的操做,當io完成時,會調用rbd_ctx_cb函數,該函數會繼續調用ctx->complete()。

---c.該函數aio_write會繼續處理這個請求。

---d.當c句將這個io下發到osd的時候,osd還沒請求處理完成,則等待在d上,直到底層處理完請求,回調b申請的 AioCompletion, 繼續調用a中的ctx->complete(),喚醒這裏的等待信號,而後程序繼續向下執行。

3.再來看看aio_write 拿到了 請求的offset和buffer會作點什麼呢?

int aio_write(ImageCtx *ictx, uint64_t off, size_t len, const char *buf,
           AioCompletion *c, int op_flags)
  {
      ………
      //將請求按着object進行拆分
      vector<ObjectExtent> extents;
     if (len > 0) 
      {
         Striper::file_to_extents(ictx->cct, ictx->format_string,
                        &ictx->layout, off, clip_len, 0, extents);   //---a
      } 
      //處理每個object上的請求數據
      for (vector<ObjectExtent>::iterator p = extents.begin(); p != extents.end(); ++p) 
      {
               ……..
           C_AioWrite *req_comp = new C_AioWrite(cct, c); //---b
           ……..
           AioWrite *req = new AioWrite(ictx, p->oid.name, p->objectno, p- >offset,bl,….., req_comp);     //---c
           r = req->send();    //---d
           …….
      }
      ……
}

 

根據請求的大小須要將這個請求按着object進行劃分,由函數file_to_extents進行處理,處理完成後按着object進行保存在extents中。file_to_extents()存在不少同名函數注意區分。這些函數的主要內容作了一件事兒,那就對原始請求的拆分。

一個rbd設備是有不少的object組成,也就是將rbd設備進行切塊,每個塊叫作object,每一個object的大小默認爲4M,也能夠本身指定。file_to_extents函數將這個大的請求分別映射到object上去,拆成了不少小的請求以下圖。最後映射的結果保存在ObjectExtent中。

 

 

本來的offset是指在rbd內的偏移量(寫入rbd的位置),通過file_to_extents後,轉化成了一個或者多個object的內部的偏移量offset0。這樣轉化後處理一批這個object內的請求。

4. 再回到 aio_write函數中,須要將拆分後的每個object請求進行處理。

---b.爲寫請求申請一個回調處理函數。

---c.根據object內部的請求,建立一個叫作AioWrite的結構。

---d.將這個AioWrite的req進行下發send().

5. 這裏AioWrite 是繼承自 AbstractWrite ,AbstractWrite 繼承自AioRequest類,在AbstractWrite 類中定義了send的方法,看下send的具體內容.

int AbstractWrite::send() 

 {  ………………

    if (send_pre())           //---a

      ……………

}

#進入send_pre()函數中

bool AbstractWrite::send_pre()

{

      m_state = LIBRBD_AIO_WRITE_PRE;   // ----a

      FunctionContext *ctx =    //----b

           new FunctionContext( boost::bind(&AioRequest::complete, this, _1));

      m_ictx->object_map.aio_update(ctx); //-----c

}

 

---a.修改m_state 狀態爲LIBRBD_AIO_WRITE_PRE。

---b.申請一個回調函數,實際調用AioRequest::complete()

---c.開始下發object_map.aio_update的請求,這是一個狀態更新的函數,不是很重要的環節,這裏再也不多說,當更新的請求完成時會自動回調到b申請的回調函數。

6. 進入到AioRequest::complete() 函數中。

void AioRequest::complete(int r)
 {
    if (should_complete(r))   //---a
        …….
}

 

---a.should_complete函數是一個純虛函數,須要在繼承類AbstractWrite中實現,來7. 看看AbstractWrite:: should_complete()

bool AbstractWrite::should_complete(int r)
{
    switch (m_state) 
  {
          case LIBRBD_AIO_WRITE_PRE:  //----a

      {

                     send_write(); //----b

 

----a.在send_pre中已經設置m_state的狀態爲LIBRBD_AIO_WRITE_PRE,因此會走這個分支。

----b. send_write()函數中,會繼續進行處理,

7.1.下面來看這個send_write函數

void AbstractWrite::send_write()
{
      m_state = LIBRBD_AIO_WRITE_FLAT;   //----a

      add_write_ops(&m_write);    // ----b

      int r = m_ictx->data_ctx.aio_operate(m_oid, rados_completion, &m_write);

}

 

---a.從新設置m_state的狀態爲 LIBRBD_AIO_WRITE_FLAT。

---b.填充m_write,將請求轉化爲m_write。

---c.下發m_write  ,使用data_ctx.aio_operate 函數處理。繼續調用io_ctx_impl->aio_operate()函數,繼續調用objecter->mutate().

8. objecter->mutate()

ceph_tid_t mutate(……..) 
  {
    Op *o = prepare_mutate_op(oid, oloc, op, snapc, mtime, flags, onack, oncommit, objver);   //----d
    return op_submit(o);
  }

 

---d.將請求轉化爲Op請求,繼續使用op_submit下發這個請求。在op_submit中繼續調用_op_submit_with_budget處理請求。繼續調用_op_submit處理。

8.1 _op_submit 的處理過程。這裏值得細看

ceph_tid_t Objecter::_op_submit(Op *op, RWLock::Context& lc)
{
    check_for_latest_map = _calc_target(&op->target, &op->last_force_resend); //---a
    int r = _get_session(op->target.osd, &s, lc);  //---b
    _session_op_assign(s, op); //----c
    _send_op(op, m); //----d
}

 

----a. _calc_target,經過計算當前object的保存的osd,而後將主osd保存在target中,rbd寫數據都是先發送到主osd,主osd再將數據發送到其餘的副本osd上。這裏對於怎麼來選取osd集合與主osd的關係就再也不多說,在《ceph的數據存儲之路(3)》中已經講述這個過程的原理了,代碼部分不難理解。

----b. _get_session,該函數是用來與主osd創建通訊的,創建通訊後,能夠經過該通道發送給主osd。再來看看這個函數是怎麼處理的

9. _get_session

int Objecter::_get_session(int osd, OSDSession **session, RWLock::Context& lc)
{
    map<int,OSDSession*>::iterator p = osd_sessions.find(osd);   //----a
    OSDSession *s = new OSDSession(cct, osd); //----b
    osd_sessions[osd] = s;//--c
    s->con = messenger->get_connection(osdmap->get_inst(osd));//-d
    ………
}

 

----a.首先在osd_sessions中查找是否已經存在一個鏈接能夠直接使用,第一次通訊是沒有的。

----b.從新申請一個OSDSession,而且使用osd等信息進行初始化。

---c. 將新申請的OSDSession添加到osd_sessions中保存,以備下次使用。

----d.調用messager的get_connection方法。在該方法中繼續想辦法與目標osd創建鏈接。

10. messager 是由子類simpleMessager實現的,下面來看下SimpleMessager中get_connection的實現方法

ConnectionRef SimpleMessenger::get_connection(const entity_inst_t& dest)
{
    Pipe *pipe = _lookup_pipe(dest.addr);     //-----a
    if (pipe) 
    {
        ……
    } 
    else 
    {
      pipe = connect_rank(dest.addr, dest.name.type(), NULL, NULL); //----b
    }
}

 

----a.首先要查找這個pipe,第一次通訊,天然這個pipe是不存在的。

----b. connect_rank 會根據這個目標osd的addr進行建立。看下connect_rank作了什麼。

11. SimpleMessenger::connect_rank

Pipe *SimpleMessenger::connect_rank(const entity_addr_t& addr,  int type, PipeConnection *con,    Message *first)

{
      Pipe *pipe = new Pipe(this, Pipe::STATE_CONNECTING, static_cast<PipeConnection*>(con));      //----a
      pipe->set_peer_type(type); //----b
      pipe->set_peer_addr(addr); //----c
      pipe->policy = get_policy(type); //----d
      pipe->start_writer();  //----e
      return pipe; //----f
}

 

----a.首先須要建立這個pipe,而且pipe同pipecon進行關聯。

----b,----c,-----d。都是進行一些參數的設置。

----e.開始啓動pipe的寫線程,這裏pipe的寫線程的處理函數pipe->writer(),該函數中會嘗試鏈接osd。而且創建socket鏈接通道。

目前的資源統計一下,寫請求能夠根據目標主osd,去查找或者創建一個OSDSession,這個OSDSession中會有一個管理數據通道的Pipe結構,而後這個結構中存在一個發送消息的處理線程writer,這個線程會保持與目標osd的socket通訊。

12. 創建而且獲取到了這些資源,這時再回到_op_submit 函數中

ceph_tid_t Objecter::_op_submit(Op *op, RWLock::Context& lc)
{
    check_for_latest_map = _calc_target(&op->target, &op->last_force_resend); //---a
    int r = _get_session(op->target.osd, &s, lc);  //---b
    _session_op_assign(s, op); //----c
    MOSDOp *m = _prepare_osd_op(op); //-----d
    _send_op(op, m); //----e
}

 

---c,將當前的op請求與這個session進行綁定,在後面發送請求的時候能知道使用哪個session進行發送。

--d,將op轉化爲MOSDop,後面會以MOSDOp爲對象進行處理的。

---e,_send_op 會根據以前創建的通訊通道,將這個MOSDOp發送出去。_send_op 中調用op->session->con->send_message(m),這個方法會調用SimpleMessager-> send_message(m), 再調用_send_message(),再調用submit_message().在submit_message會找到以前的pipe,而後調用pipe->send方法,最後經過pipe->writer的線程發送到目標osd。

 

自此,客戶就等待osd處理完成返回結果了。

總結客戶端的全部流程和數據結構,下面來看下客戶端的全部結構圖。

 

 

經過這個所有的結構圖來總結客戶端的處理過程。

1.看左上角的rados結構,首先建立io環境,建立rados信息,將配置文件中的數據結構化到rados中。

2.根據rados建立一個radosclient的客戶端結構,該結構包括了三個重要的模塊,finiser 回調處理線程、Messager消息處理結構、Objector數據處理結構。最後的數據都是要封裝成消息 經過Messager發送給目標的osd。

3.根據pool的信息與radosclient進行建立一個ioctx,這裏麪包好了pool相關的信息,而後得到這些信息後在數據處理時會用到。

4.緊接着會複製這個ioctx到imagectx中,變成data_ioctx與md_ioctx數據處理通道,最後將imagectx封裝到image結構當中。以後全部的寫操做都會經過這個image進行。順着image的結構能夠找到前面建立而且可使用的數據結構。

5.經過最右上角的image進行讀寫操做,當讀寫操做的對象爲image時,這個image會開始處理請求,而後這個請求通過處理拆分紅object對象的請求。拆分後會交給objector進行處理查找目標osd,固然這裏使用的就是crush算法,找到目標osd的集合與主osd。

6.將請求op封裝成MOSDOp消息,而後交給SimpleMessager處理,SimpleMessager會嘗試在已有的osd_session中查找,若是沒有找到對應的session,則會從新建立一個OSDSession,而且爲這個OSDSession建立一個數據通道pipe,把數據通道保存在SimpleMessager中,能夠下次使用。

7.pipe 會與目標osd創建Socket通訊通道,pipe會有專門的寫線程writer來負責socket通訊。在線程writer中會先鏈接目標ip,創建通訊。消息從SimpleMessager收到後會保存到pipe的outq隊列中,writer線程另外的一個用途就是監視這個outq隊列,當隊列中存在消息等待發送時,會就將消息寫入socket,發送給目標OSD。

8. 等待OSD將數據消息處理完成以後,就是進行回調,反饋執行結果,而後一步步的將結果告知調用者。

上面是就rbd client處理寫請求的過程,那麼下面會在分析一個OSD是如何接到請求,而且怎麼來處理這個請求的。請期待下一節。

相關文章
相關標籤/搜索