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是如何接到請求,而且怎麼來處理這個請求的。請期待下一節。