• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            陳碩的Blog

            Muduo 設(shè)計與實現(xiàn)之一:Buffer 類的設(shè)計

            陳碩 (giantchen_AT_gmail)

            Blog.csdn.net/Solstice  t.sina.com.cn/giantchen

            Muduo 全系列文章列表: http://blog.csdn.net/Solstice/category/779646.aspx

             

            本文介紹 Muduo 中輸入輸出緩沖區(qū)的設(shè)計與實現(xiàn)。

            本文中 buffer 指一般的應(yīng)用層緩沖區(qū)、緩沖技術(shù),Buffer 特指 muduo::net::Buffer class。

            本文前兩節(jié)的內(nèi)容已事先發(fā)表在 muduo 英文博客 http://muduo.chenshuo.com/2011/04/essentials-of-non-blocking-tcp-network.html

            Muduo 的 IO 模型

            UNPv1 第 6.2 節(jié)總結(jié)了 Unix/Linux 上的五種 IO 模型:阻塞(blocking)、非阻塞(non-blocking)、IO 復(fù)用(IO multiplexing)、信號驅(qū)動(signal-driven)、異步(asynchronous)。這些都是單線程下的 IO 模型。

            C10k 問題的頁面介紹了五種 IO 策略,把線程也納入考量。(現(xiàn)在 C10k 已經(jīng)不是什么問題,C100k 也不是大問題,C1000k 才算得上挑戰(zhàn))。

            在這個多核時代,線程是不可避免的。那么服務(wù)端網(wǎng)絡(luò)編程該如何選擇線程模型呢?我贊同 libev 作者的觀點:one loop per thread is usually a good model。之前我也不止一次表述過這個觀點,見《多線程服務(wù)器的常用編程模型》《多線程服務(wù)器的適用場合》。

            如果采用 one loop per thread 的模型,多線程服務(wù)端編程的問題就簡化為如何設(shè)計一個高效且易于使用的 event loop,然后每個線程 run 一個 event loop 就行了(當然、同步和互斥是不可或缺的)。在“高效”這方面已經(jīng)有了很多成熟的范例(libev、libevent、memcached、varnish、lighttpd、nginx),在“易于使用”方面我希望 muduo 能有所作為。(muduo 可算是用現(xiàn)代 C++ 實現(xiàn)了 Reactor 模式,比起原始的 Reactor 來說要好用得多。)

            event loop 是 non-blocking 網(wǎng)絡(luò)編程的核心,在現(xiàn)實生活中,non-blocking 幾乎總是和 IO-multiplexing 一起使用,原因有兩點:

            • 沒有人真的會用輪詢 (busy-pooling) 來檢查某個 non-blocking IO 操作是否完成,這樣太浪費 CPU cycles。
            • IO-multiplex 一般不能和 blocking IO 用在一起,因為 blocking IO 中 read()/write()/accept()/connect() 都有可能阻塞當前線程,這樣線程就沒辦法處理其他 socket 上的 IO 事件了。見 UNPv1 第 16.6 節(jié)“nonblocking accept”的例子。

            所以,當我提到 non-blocking 的時候,實際上指的是 non-blocking + IO-muleiplexing,單用其中任何一個是不現(xiàn)實的。另外,本文所有的“連接”均指 TCP 連接,socket 和 connection 在文中可互換使用。

            當然,non-blocking 編程比 blocking 難得多,見陳碩在《Muduo 網(wǎng)絡(luò)編程示例之零:前言》中“TCP 網(wǎng)絡(luò)編程本質(zhì)論”一節(jié)列舉的難點。基于 event loop 的網(wǎng)絡(luò)編程跟直接用 C/C++ 編寫單線程 Windows 程序頗為相像:程序不能阻塞,否則窗口就失去響應(yīng)了;在 event handler 中,程序要盡快交出控制權(quán),返回窗口的事件循環(huán)。

            為什么 non-blocking 網(wǎng)絡(luò)編程中應(yīng)用層 buffer 是必須的?

            Non-blocking IO 的核心思想是避免阻塞在 read() 或 write() 或其他 IO 系統(tǒng)調(diào)用上,這樣可以最大限度地復(fù)用 thread-of-control,讓一個線程能服務(wù)于多個 socket 連接。IO 線程只能阻塞在 IO-multiplexing 函數(shù)上,如 select()/poll()/epoll_wait()。這樣一來,應(yīng)用層的緩沖是必須的,每個 TCP socket 都要有 stateful 的 input buffer 和 output buffer。

            TcpConnection 必須要有 output buffer

            考慮一個常見場景:程序想通過 TCP 連接發(fā)送 100k 字節(jié)的數(shù)據(jù),但是在 write() 調(diào)用中,操作系統(tǒng)只接受了 80k 字節(jié)(受 TCP advertised window 的控制,細節(jié)見 TCPv1),你肯定不想在原地等待,因為不知道會等多久(取決于對方什么時候接受數(shù)據(jù),然后滑動 TCP 窗口)。程序應(yīng)該盡快交出控制權(quán),返回 event loop。在這種情況下,剩余的 20k 字節(jié)數(shù)據(jù)怎么辦?

            對于應(yīng)用程序而言,它只管生成數(shù)據(jù),它不應(yīng)該關(guān)心到底數(shù)據(jù)是一次性發(fā)送還是分成幾次發(fā)送,這些應(yīng)該由網(wǎng)絡(luò)庫來操心,程序只要調(diào)用 TcpConnection::send() 就行了,網(wǎng)絡(luò)庫會負責(zé)到底。網(wǎng)絡(luò)庫應(yīng)該接管這剩余的 20k 字節(jié)數(shù)據(jù),把它保存在該 TCP connection 的 output buffer 里,然后注冊 POLLOUT 事件,一旦 socket 變得可寫就立刻發(fā)送數(shù)據(jù)。當然,這第二次 write() 也不一定能完全寫入 20k 字節(jié),如果還有剩余,網(wǎng)絡(luò)庫應(yīng)該繼續(xù)關(guān)注 POLLOUT 事件;如果寫完了 20k 字節(jié),網(wǎng)絡(luò)庫應(yīng)該停止關(guān)注 POLLOUT,以免造成 busy loop。(Muduo EventLoop 采用的是 epoll level trigger,這么做的具體原因我以后再說。)

            如果程序又寫入了 50k 字節(jié),而這時候 output buffer 里還有待發(fā)送的 20k 數(shù)據(jù),那么網(wǎng)絡(luò)庫不應(yīng)該直接調(diào)用 write(),而應(yīng)該把這 50k 數(shù)據(jù) append 在那 20k 數(shù)據(jù)之后,等 socket 變得可寫的時候再一并寫入。

            如果 output buffer 里還有待發(fā)送的數(shù)據(jù),而程序又想關(guān)閉連接(對程序而言,調(diào)用 TcpConnection::send() 之后他就認為數(shù)據(jù)遲早會發(fā)出去),那么這時候網(wǎng)絡(luò)庫不能立刻關(guān)閉連接,而要等數(shù)據(jù)發(fā)送完畢,見我在《為什么 muduo 的 shutdown() 沒有直接關(guān)閉 TCP 連接?》一文中的講解。

            綜上,要讓程序在 write 操作上不阻塞,網(wǎng)絡(luò)庫必須要給每個 tcp connection 配置 output buffer。

            TcpConnection 必須要有 input buffer

            TCP 是一個無邊界的字節(jié)流協(xié)議,接收方必須要處理“收到的數(shù)據(jù)尚不構(gòu)成一條完整的消息”和“一次收到兩條消息的數(shù)據(jù)”等等情況。一個常見的場景是,發(fā)送方 send 了兩條 10k 字節(jié)的消息(共 20k),接收方收到數(shù)據(jù)的情況可能是:

            • 一次性收到 20k 數(shù)據(jù)
            • 分兩次收到,第一次 5k,第二次 15k
            • 分兩次收到,第一次 15k,第二次 5k
            • 分兩次收到,第一次 10k,第二次 10k
            • 分三次收到,第一次 6k,第二次 8k,第三次 6k
            • 其他任何可能

            網(wǎng)絡(luò)庫在處理“socket 可讀”事件的時候,必須一次性把 socket 里的數(shù)據(jù)讀完(從操作系統(tǒng) buffer 搬到應(yīng)用層 buffer),否則會反復(fù)觸發(fā) POLLIN 事件,造成 busy-loop。(Again, Muduo EventLoop 采用的是 epoll level trigger,這么做的具體原因我以后再說。)

            那么網(wǎng)絡(luò)庫必然要應(yīng)對“數(shù)據(jù)不完整”的情況,收到的數(shù)據(jù)先放到 input buffer 里,等構(gòu)成一條完整的消息再通知程序的業(yè)務(wù)邏輯。這通常是 codec 的職責(zé),見陳碩《Muduo 網(wǎng)絡(luò)編程示例之二:Boost.Asio 的聊天服務(wù)器》一文中的“TCP 分包”的論述與代碼。

            所以,在 tcp 網(wǎng)絡(luò)編程中,網(wǎng)絡(luò)庫必須要給每個 tcp connection 配置 input buffer。

             

            所有 muduo 中的 IO 都是帶緩沖的 IO (buffered IO),你不會自己去 read() 或 write() 某個 socket,只會操作 TcpConnection 的 input buffer 和 output buffer。更確切的說,是在 onMessage() 回調(diào)里讀取 input buffer;調(diào)用 TcpConnection::send() 來間接操作 output buffer,一般不會直接操作 output buffer。

            btw, muduo 的 onMessage() 的原型如下,它既可以是 free function,也可以是 member function,反正 muduo TcpConnection 只認 boost::function<>。

            void onMessage(const TcpConnectionPtr& conn, Buffer* buf, Timestamp receiveTime);

            對于網(wǎng)絡(luò)程序來說,一個簡單的驗收測試是:輸入數(shù)據(jù)每次收到一個字節(jié)(200 字節(jié)的輸入數(shù)據(jù)會分 200 次收到,每次間隔 10 ms),程序的功能不受影響。對于 Muduo 程序,通常可以用 codec 來分離“消息接收”與“消息處理”,見陳碩《在 muduo 中實現(xiàn) protobuf 編解碼器與消息分發(fā)器》一文中對“編解碼器 codec”的介紹。

            如果某個網(wǎng)絡(luò)庫只提供相當于 char buf[8192] 的緩沖,或者根本不提供緩沖區(qū),而僅僅通知程序“某 socket 可讀/某 socket 可寫”,要程序自己操心 IO buffering,這樣的網(wǎng)絡(luò)庫用起來就很不方便了。(我有所指,你懂得。)

             

            Buffer 的要求

            http://code.google.com/p/muduo/source/browse/trunk/muduo/net/Buffer.h

            Muduo Buffer 的設(shè)計考慮了常見的網(wǎng)絡(luò)編程需求,我試圖在易用性和性能之間找一個平衡點,目前這個平衡點更偏向于易用性。

            Muduo Buffer 的設(shè)計要點:

            • 對外表現(xiàn)為一塊連續(xù)的內(nèi)存(char*, len),以方便客戶代碼的編寫。
            • 其 size() 可以自動增長,以適應(yīng)不同大小的消息。它不是一個 fixed size array (即 char buf[8192])。
            • 內(nèi)部以 vector of char 來保存數(shù)據(jù),并提供相應(yīng)的訪問函數(shù)。

            Buffer 其實像是一個 queue,從末尾寫入數(shù)據(jù),從頭部讀出數(shù)據(jù)。

            誰會用 Buffer?誰寫誰讀?根據(jù)前文分析,TcpConnection 會有兩個 Buffer 成員,input buffer 與 output buffer。

            • input buffer,TcpConnection 會從 socket 讀取數(shù)據(jù),然后寫入 input buffer(其實這一步是用 Buffer::readFd() 完成的);客戶代碼從 input buffer 讀取數(shù)據(jù)。
            • output buffer,客戶代碼會把數(shù)據(jù)寫入 output buffer(其實這一步是用 TcpConnection::send() 完成的);TcpConnection 從 output buffer 讀取數(shù)據(jù)并寫入 socket。

            其實,input 和 output 是針對客戶代碼而言,客戶代碼從 input 讀,往 output 寫。TcpConnection 的讀寫正好相反。

            以下是 muduo::net::Buffer 的類圖。請注意,為了后面畫圖方便,這個類圖跟實際代碼略有出入,但不影響我要表達的觀點。

            bc

            這里不介紹每個成員函數(shù)的作用,留給《Muduo 網(wǎng)絡(luò)編程示例》系列。下文會仔細介紹 readIndex 和 writeIndex 的作用。

            Buffer::readFd()

            我在《Muduo 網(wǎng)絡(luò)編程示例之零:前言》中寫道

            • 在非阻塞網(wǎng)絡(luò)編程中,如何設(shè)計并使用緩沖區(qū)?一方面我們希望減少系統(tǒng)調(diào)用,一次讀的數(shù)據(jù)越多越劃算,那么似乎應(yīng)該準備一個大的緩沖區(qū)。另一方面,我們系統(tǒng)減少內(nèi)存占用。如果有 10k 個連接,每個連接一建立就分配 64k 的讀緩沖的話,將占用 640M 內(nèi)存,而大多數(shù)時候這些緩沖區(qū)的使用率很低。muduo 用 readv 結(jié)合棧上空間巧妙地解決了這個問題。

            具體做法是,在棧上準備一個 65536 字節(jié)的 stackbuf,然后利用 readv() 來讀取數(shù)據(jù),iovec 有兩塊,第一塊指向 muduo Buffer 中的 writable 字節(jié),另一塊指向棧上的 stackbuf。這樣如果讀入的數(shù)據(jù)不多,那么全部都讀到 Buffer 中去了;如果長度超過 Buffer 的 writable 字節(jié)數(shù),就會讀到棧上的 stackbuf 里,然后程序再把 stackbuf 里的數(shù)據(jù) append 到 Buffer 中。

            代碼見 http://code.google.com/p/muduo/source/browse/trunk/muduo/net/Buffer.cc#36

            這么做利用了臨時棧上空間,避免開巨大 Buffer 造成的內(nèi)存浪費,也避免反復(fù)調(diào)用 read() 的系統(tǒng)開銷(通常一次 readv() 系統(tǒng)調(diào)用就能讀完全部數(shù)據(jù))。

            這算是一個小小的創(chuàng)新吧。

            線程安全?

            muduo::net::Buffer 不是線程安全的,這么做是有意的,原因如下:

            • 對于 input buffer,onMessage() 回調(diào)始終發(fā)生在該 TcpConnection 所屬的那個 IO 線程,應(yīng)用程序應(yīng)該在 onMessage() 完成對 input buffer 的操作,并且不要把 input buffer 暴露給其他線程。這樣所有對 input buffer 的操作都在同一個線程,Buffer class 不必是線程安全的。
            • 對于 output buffer,應(yīng)用程序不會直接操作它,而是調(diào)用 TcpConnection::send() 來發(fā)送數(shù)據(jù),后者是線程安全的。

            如果 TcpConnection::send() 調(diào)用發(fā)生在該 TcpConnection 所屬的那個 IO 線程,那么它會轉(zhuǎn)而調(diào)用 TcpConnection::sendInLoop(),sendInLoop() 會在當前線程(也就是 IO 線程)操作 output buffer;如果 TcpConnection::send() 調(diào)用發(fā)生在別的線程,它不會在當前線程調(diào)用 sendInLoop() ,而是通過 EventLoop::runInLoop() 把 sendInLoop() 函數(shù)調(diào)用轉(zhuǎn)移到 IO 線程(聽上去頗為神奇?),這樣 sendInLoop() 還是會在 IO 線程操作 output buffer,不會有線程安全問題。當然,跨線程的函數(shù)轉(zhuǎn)移調(diào)用涉及函數(shù)參數(shù)的跨線程傳遞,一種簡單的做法是把數(shù)據(jù)拷一份,絕對安全(不明白的同學(xué)請閱讀代碼)。

            另一種更為高效做法是用 swap()。這就是為什么 TcpConnection::send() 的某個重載以 Buffer* 為參數(shù),而不是 const Buffer&,這樣可以避免拷貝,而用 Buffer::swap() 實現(xiàn)高效的線程間數(shù)據(jù)轉(zhuǎn)移。(最后這點,僅為設(shè)想,暫未實現(xiàn)。目前仍然以數(shù)據(jù)拷貝方式在線程間傳遞,略微有些性能損失。)

             

            Muduo Buffer 的數(shù)據(jù)結(jié)構(gòu)

            Buffer 的內(nèi)部是一個 vector of char,它是一塊連續(xù)的內(nèi)存。此外,Buffer 有兩個 data members,指向該 vector 中的元素。這兩個 indices 的類型是 int,不是 char*,目的是應(yīng)對迭代器失效。muduo Buffer 的設(shè)計參考了 Netty 的 ChannelBuffer 和 libevent 1.4.x 的 evbuffer。不過,其 prependable 可算是一點“微創(chuàng)新”。

            Muduo Buffer 的數(shù)據(jù)結(jié)構(gòu)如下:

            圖 1buffer0

            兩個 indices 把 vector 的內(nèi)容分為三塊:prependable、readable、writable,各塊的大小是(公式一):

            prependable = readIndex

            readable = writeIndex - readIndex

            writable = size() - writeIndex

            (prependable 的作用留到后面討論。)

            readIndex 和 writeIndex 滿足以下不變式(invariant):

            0 ≤ readIndex ≤ writeIndex ≤ data.size()

            Muduo Buffer 里有兩個常數(shù) kCheapPrepend 和 kInitialSize,定義了 prependable 的初始大小和 writable 的初始大小。(readable 的初始大小為 0。)在初始化之后,Buffer 的數(shù)據(jù)結(jié)構(gòu)如下:括號里的數(shù)字是該變量或常量的值。

            圖 2buffer1

            根據(jù)以上(公式一)可算出各塊的大小,剛剛初始化的 Buffer 里沒有 payload 數(shù)據(jù),所以 readable == 0。

            Muduo Buffer 的操作

            1. 基本的 read-write cycle

            Buffer 初始化后的情況見圖 1,如果有人向 Buffer 寫入了 200 字節(jié),那么其布局是:

            圖 3buffer2

            圖 3 中 writeIndex 向后移動了 200 字節(jié),readIndex 保持不變,readable 和 writable 的值也有變化。

             

            如果有人從 Buffer read() & retrieve() (下稱“讀入”)了 50 字節(jié),結(jié)果見圖 4。與上圖相比,readIndex 向后移動 50 字節(jié),writeIndex 保持不變,readable 和 writable 的值也有變化(這句話往后從略)。

            圖 4buffer3

             

            然后又寫入了 200 字節(jié),writeIndex 向后移動了 200 字節(jié),readIndex 保持不變,見圖 5

            圖 5buffer4

             

            接下來,一次性讀入 350 字節(jié),請注意,由于全部數(shù)據(jù)讀完了,readIndex 和 writeIndex 返回原位以備新一輪使用,見圖 6,這和圖 2 是一樣的。

            圖 6buffer5

             

            以上過程可以看作是發(fā)送方發(fā)送了兩條消息,長度分別為 50 字節(jié)和 350 字節(jié),接收方分兩次收到數(shù)據(jù),每次 200 字節(jié),然后進行分包,再分兩次回調(diào)客戶代碼。

             

            自動增長

            Muduo Buffer 不是固定長度的,它可以自動增長,這是使用 vector 的直接好處。

            假設(shè)當前的狀態(tài)如圖 7 所示。(這和前面圖 5 是一樣的。)

            圖 7buffer4

             

            客戶代碼一次性寫入 1000 字節(jié),而當前可寫的字節(jié)數(shù)只有 624,那么 buffer 會自動增長以容納全部數(shù)據(jù),得到的結(jié)果是圖 8。注意 readIndex 返回到了前面,以保持 prependable 等于 kCheapPrependable。由于 vector 重新分配了內(nèi)存,原來指向它元素的指針會失效,這就是為什么 readIndex 和 writeIndex 是整數(shù)下標而不是指針。

            圖 8buffer6

             

            然后讀入 350 字節(jié),readIndex 前移,見圖 9

            圖 9buffer7

             

            最后,讀完剩下的 1000 字節(jié),readIndex 和 writeIndex 返回 kCheapPrependable,見圖 10。

            圖 10buffer8

            注意 buffer 并沒有縮小大小,下次寫入 1350 字節(jié)就不會重新分配內(nèi)存了。換句話說,Muduo Buffer 的 size() 是自適應(yīng)的,它一開始的初始值是 1k,如果程序里邊經(jīng)常收發(fā) 10k 的數(shù)據(jù),那么用幾次之后它的 size() 會自動增長到 10k,然后就保持不變。這樣一方面避免浪費內(nèi)存(有的程序可能只需要 4k 的緩沖),另一方面避免反復(fù)分配內(nèi)存。當然,客戶代碼可以手動 shrink() buffer size()。

            size() 與 capacity()

            使用 vector 的另一個好處是它的 capcity() 機制減少了內(nèi)存分配的次數(shù)。比方說程序反復(fù)寫入 1 字節(jié),muduo Buffer 不會每次都分配內(nèi)存,vector 的 capacity() 以指數(shù)方式增長,讓 push_back() 的平均復(fù)雜度是常數(shù)。比方說經(jīng)過第一次增長,size() 剛好滿足寫入的需求,如圖 11。但這個時候 vector 的 capacity() 已經(jīng)大于 size(),在接下來寫入 capacity()-size() 字節(jié)的數(shù)據(jù)時,都不會重新分配內(nèi)存,見圖 12

            圖 11buffer6

            圖 12buffer9

             

            細心的讀者可能會發(fā)現(xiàn)用 capacity() 也不是完美的,它有優(yōu)化的余地。具體來說,vector::resize() 會初始化(memset/bzero)內(nèi)存,而我們不需要它初始化,因為反正立刻就要填入數(shù)據(jù)。比如,在圖 12 的基礎(chǔ)上寫入 200 字節(jié),由于 capacity() 足夠大,不會重新分配內(nèi)存,這是好事;但是 vector::resize() 會先把那 200 字節(jié)設(shè)為 0 (圖 13),然后 muduo buffer 再填入數(shù)據(jù)(圖 14)。這么做稍微有點浪費,不過我不打算優(yōu)化它,除非它確實造成了性能瓶頸。(精通 STL 的讀者可能會說用 vector::append() 以避免浪費,但是 writeIndex 和 size() 不一定是對齊的,會有別的麻煩。)

             

            圖 13buffer9a

            圖 14buffer9b

            google protobuf 中有一個 STLStringResizeUninitialized 函數(shù),干的就是這個事情。

            內(nèi)部騰挪

            有時候,經(jīng)過若干次讀寫,readIndex 移到了比較靠后的位置,留下了巨大的 prependable 空間,見圖 14

            圖 14buffer10

             

            這時候,如果我們想寫入 300 字節(jié),而 writable 只有 200 字節(jié),怎么辦?muduo Buffer 在這種情況下不會重新分配內(nèi)存,而是先把已有的數(shù)據(jù)移到前面去,騰出 writable 空間,見圖 15

            圖 15buffer11

             

            然后,就可以寫入 300 字節(jié)了,見圖 16

            圖 16buffer12

            這么做的原因是,如果重新分配內(nèi)存,反正也是要把數(shù)據(jù)拷到新分配的內(nèi)存區(qū)域,代價只會更大。

            prepend

            前面說 muduo Buffer 有個小小的創(chuàng)新(或許不是創(chuàng)新,我記得在哪兒看到過類似的做法,忘了出處),即提供 prependable 空間,讓程序能以很低的代價在數(shù)據(jù)前面添加幾個字節(jié)。

            比方說,程序以固定的4個字節(jié)表示消息的長度(即《Muduo 網(wǎng)絡(luò)編程示例之二:Boost.Asio 的聊天服務(wù)器》中的 LengthHeaderCodec),我要序列化一個消息,但是不知道它有多長,那么我可以一直 append() 直到序列化完成(圖 17,寫入了 200 字節(jié)),然后再在序列化數(shù)據(jù)的前面添加消息的長度(圖 18,把 200 這個數(shù) prepend 到首部)。

             

            圖 17buffer13

             

            圖 18buffer14

            通過預(yù)留 kCheapPrependable 空間,可以簡化客戶代碼,一個簡單的空間換時間思路。

            其他設(shè)計方案

            這里簡單談?wù)勂渌赡艿膽?yīng)用層 buffer 設(shè)計方案。

            不用 vector<char>?

            如果有 STL 潔癖,那么可以自己管理內(nèi)存,以 4 個指針為 buffer 的成員,數(shù)據(jù)結(jié)構(gòu)見圖 19。

            圖 19alternative

            說實話我不覺得這種方案比 vector 好。代碼變復(fù)雜,性能也未見得有 noticeable 的改觀。

            如果放棄“連續(xù)性”要求,可以用 circular buffer,這樣可以減少一點內(nèi)存拷貝(沒有“內(nèi)部騰挪”)。

            Zero copy ?

            如果對性能有極高的要求,受不了 copy() 與 resize(),那么可以考慮實現(xiàn)分段連續(xù)的 zero copy buffer 再配合 gather scatter IO,數(shù)據(jù)結(jié)構(gòu)如圖 20,這是 libevent 2.0.x 的設(shè)計方案。TCPv2介紹的 BSD TCP/IP 實現(xiàn)中的 mbuf 也是類似的方案,Linux 的 sk_buff 估計也差不多。細節(jié)有出入,但基本思路都是不要求數(shù)據(jù)在內(nèi)存中連續(xù),而是用鏈表把數(shù)據(jù)塊鏈接到一起。

            圖 20evbuf0

            當然,高性能的代價是代碼變得晦澀難讀,buffer 不再是連續(xù)的,parse 消息會稍微麻煩。如果你的程序只處理 protobuf Message,這不是問題,因為 protobuf 有 ZeroCopyInputStream 接口,只要實現(xiàn)這個接口,parsing 的事情就交給 protobuf Message 去操心了。

            性能是不是問題?看跟誰比

            看到這里,有的讀者可能會嘀咕,muduo Buffer 有那么多可以優(yōu)化的地方,其性能會不會太低?對此,我的回應(yīng)是“可以優(yōu)化,不一定值得優(yōu)化。”

            Muduo 的設(shè)計目標是用于開發(fā)公司內(nèi)部的分布式程序。換句話說,它是用來寫專用的 Sudoku server 或者游戲服務(wù)器,不是用來寫通用的 httpd 或 ftpd 或 www proxy。前者通常有業(yè)務(wù)邏輯,后者更強調(diào)高并發(fā)與高吞吐。

            以 Sudoku 為例,假設(shè)求解一個 Sudoku 問題需要 0.2ms,服務(wù)器有 8 個核,那么理想情況下每秒最多能求解 40,000 個問題。每次 Sudoku 請求的數(shù)據(jù)大小低于 100 字節(jié)(一個 9x9 的數(shù)獨只要 81 字節(jié),加上 header 也可以控制在 100 bytes 以下),就是說 100 x 40000 = 4 MB per second 的吞吐量就足以讓服務(wù)器的 CPU 飽和。在這種情況下,去優(yōu)化 Buffer 的內(nèi)存拷貝次數(shù)似乎沒有意義。

            再舉一個例子,目前最常用的千兆以太網(wǎng)的裸吞吐量是 125MB/s,扣除以太網(wǎng) header、IP header、TCP header之后,應(yīng)用層的吞吐率大約在 115 MB/s 上下。而現(xiàn)在服務(wù)器上最常用的 DDR2/DDR3 內(nèi)存的帶寬至少是 4GB/s,比千兆以太網(wǎng)高 40 倍以上。就是說,對于幾 k 或幾十 k 大小的數(shù)據(jù),在內(nèi)存里邊拷幾次根本不是問題,因為受以太網(wǎng)延遲和帶寬的限制,跟這個程序通信的其他機器上的程序不會覺察到性能差異。

            最后舉一個例子,如果你實現(xiàn)的服務(wù)程序要跟數(shù)據(jù)庫打交道,那么瓶頸常常在 DB 上,優(yōu)化服務(wù)程序本身不見得能提高性能(從 DB 讀一次數(shù)據(jù)往往就抵消了你做的全部 low-level 優(yōu)化),這時不如把精力投入在 DB 調(diào)優(yōu)上。

            專用服務(wù)程序與通用服務(wù)程序的另外一點區(qū)別是 benchmark 的對象不同。如果你打算寫一個 httpd,自然有人會拿來和目前最好的 nginx 對比,立馬就能比出性能高低。然而,如果你寫一個實現(xiàn)公司內(nèi)部業(yè)務(wù)的服務(wù)程序(比如分布式存儲或者搜索或者微博或者短網(wǎng)址),由于市面上沒有同等功能的開源實現(xiàn),你不需要在優(yōu)化上投入全部精力,只要一版做得比一版好就行。先正確實現(xiàn)所需的功能,投入生產(chǎn)應(yīng)用,然后再根據(jù)真實的負載情況來做優(yōu)化,這恐怕比在編碼階段就盲目調(diào)優(yōu)要更 effective 一些。

            Muduo 的設(shè)計目標之一是吞吐量能讓千兆以太網(wǎng)飽和,也就是每秒收發(fā) 120 兆字節(jié)的數(shù)據(jù)。這個很容易就達到,不用任何特別的努力。

            如果確實在內(nèi)存帶寬方面遇到問題,說明你做的應(yīng)用實在太 critical,或許應(yīng)該考慮放到 Linux kernel 里邊去,而不是在用戶態(tài)嘗試各種優(yōu)化。畢竟只有把程序做到 kernel 里才能真正實現(xiàn) zero copy,否則,核心態(tài)和用戶態(tài)之間始終是有一次內(nèi)存拷貝的。如果放到 kernel 里還不能滿足需求,那么要么自己寫新的 kernel,或者直接用 FPGA 或 ASIC 操作 network adapter 來實現(xiàn)你的高性能服務(wù)器。

            (待續(xù))

            posted on 2011-04-17 12:24 陳碩 閱讀(9821) 評論(28)  編輯 收藏 引用

            評論

            # re: Muduo 設(shè)計與實現(xiàn)之一:Buffer 類的設(shè)計[未登錄] 2011-04-17 16:54 by

            我用的定長BUFFER組來做的這個BUFFER。
            另外你提到的ZEROCOPY的那個,我以前在群里也提過,就是底層分配一個STREAM,讓應(yīng)用層來進行讀寫操作。
            不過你的BUFFER的prepend確實是個很好的東西。我這邊的這個,暫時是放在應(yīng)用層的。應(yīng)用層數(shù)據(jù)進入底層,全部做了一次拷貝,成為底層的BUFFER。  回復(fù)  更多評論   

            # re: Muduo 設(shè)計與實現(xiàn)之一:Buffer 類的設(shè)計[未登錄] 2011-04-17 16:58 by

            關(guān)于協(xié)議解析的問題,我直接把CODEC做成底層插件,應(yīng)用層實現(xiàn)后,就插進去,而且目前是靠單條消息緩沖實現(xiàn)的。
            以后要做的事情,就是結(jié)合到零拷貝那里面,做一個ZEROCOPY的STREAM出來,讓應(yīng)用層全部使用這種消息解析和處理的方式。
              回復(fù)  更多評論   

            # re: Muduo 設(shè)計與實現(xiàn)之一:Buffer 類的設(shè)計[未登錄] 2011-04-17 21:14 vincent

            有木有實際的測試數(shù)據(jù)可供參考啊

            另外我個人覺得對于這個input buffer,我覺得它可一直擴展不見得是好事
            如果應(yīng)用層真處理的很慢,造成buffer增長到內(nèi)存吃不消的時候還是很尷尬的
            個人覺得還是有一個上限,把大于上限的阻塞于TCP的好  回復(fù)  更多評論   

            # re: Muduo 設(shè)計與實現(xiàn)之一:Buffer 類的設(shè)計 2011-04-17 21:33 陳碩

            @vincent
            實際的測試數(shù)據(jù):
            http://www.shnenglu.com/Solstice/archive/2010/09/04/muduo_vs_asio.html
            http://www.shnenglu.com/Solstice/archive/2010/09/05/muduo_vs_libevent.html  回復(fù)  更多評論   

            # re: Muduo 設(shè)計與實現(xiàn)之一:Buffer 類的設(shè)計 2011-04-18 09:26 zuhd

            想問下 你寫這樣一篇博客是如何排版的?用了哪些工具?大概需要多長時間?謝謝  回復(fù)  更多評論   

            # re: Muduo 設(shè)計與實現(xiàn)之一:Buffer 類的設(shè)計 2011-04-18 16:37 城市男人

            還是很不錯的  回復(fù)  更多評論   

            # re: Muduo 設(shè)計與實現(xiàn)之一:Buffer 類的設(shè)計 2011-04-18 19:25 陳碩

            @zuhd
            Live Writer 和 gpic。
            畫圖一天,碼字一天。  回復(fù)  更多評論   

            # re: Muduo 設(shè)計與實現(xiàn)之一:Buffer 類的設(shè)計 2011-04-19 09:02 打擊裝B犯

            樓主雖然長的憨了一點, 但水平還是不錯  回復(fù)  更多評論   

            # re: Muduo 設(shè)計與實現(xiàn)之一:Buffer 類的設(shè)計 2011-04-20 00:26 so

            在之前項目中,也有你在prepend提到的問題,我是pop一個引用(會移動寫位置),然后寫入數(shù)據(jù)。最后再最之前pop的那個引用進行賦值。  回復(fù)  更多評論   

            # re: Muduo 設(shè)計與實現(xiàn)之一:Buffer 類的設(shè)計[未登錄] 2011-04-24 23:49 楊粼波

            自己先好好學(xué)一學(xué)STL。
            還capacity()機制,寒……
            先仔細看看std::vector吧,
            調(diào)用reserve()才會預(yù)分配內(nèi)存。
            這些都是STL的內(nèi)存分配機制的問題。
            自己先多學(xué)點東西吧。  回復(fù)  更多評論   

            # re: Muduo 設(shè)計與實現(xiàn)之一:Buffer 類的設(shè)計 2011-04-25 07:30 陳碩

            @楊粼波
            “調(diào)用reserve()才會預(yù)分配內(nèi)存”?!這是哪家的 STL?
            我原文說了,“vector 的 capacity() 以指數(shù)方式增長,讓 push_back() 的平均復(fù)雜度是常數(shù)。”
            如果“調(diào)用reserve()才會預(yù)分配內(nèi)存”如何達到 push_back 的平均復(fù)雜度要求?

            幾行代碼就能驗證的事情:

            vector<char> vec;
            printf("%zd %zd\n", vec.size(), vec.capacity());
            vec.resize(1024);
            printf("%zd %zd\n", vec.size(), vec.capacity());
            vec.resize(1300);
            printf("%zd %zd\n", vec.size(), vec.capacity());

            運行結(jié)果:

            0 0
            1024 1024
            1300 2048


            原話奉還:“自己先好好學(xué)一學(xué)STL。這些都是STL的內(nèi)存分配機制的問題。
            自己先多學(xué)點東西吧。”
              回復(fù)  更多評論   

            # re: Muduo 設(shè)計與實現(xiàn)之一:Buffer 類的設(shè)計 2011-04-25 08:01 halida

            “vector 的 capacity() 以指數(shù)方式增長,讓 push_back() 的平均復(fù)雜度是常數(shù)。”

            這是可變長vector的典型實現(xiàn)方式, 讓push操作變成O(1)的, 算法導(dǎo)論上面有寫.  回復(fù)  更多評論   

            # re: Muduo 設(shè)計與實現(xiàn)之一:Buffer 類的設(shè)計[未登錄] 2011-04-25 14:11 路人甲

            看了一下@楊粼波的博客,醒目處寫著 “嚴以律己,寬以待人. 三思而后行....每日自省,慎言敏行...", 這讓我想起了0bug老師,技術(shù)對錯無關(guān)緊要(雖技術(shù)高低可以一眼看出),可以討論,惡語相向,實不可取也!  回復(fù)  更多評論   

            # re: Muduo 設(shè)計與實現(xiàn)之一:Buffer 類的設(shè)計[未登錄] 2011-04-25 14:23 路人甲

            如果output buffer堆積的數(shù)據(jù)太多,在remalloc或者平臺根本不提供remalloc的情況下, append操作開銷還是比較大的,可以考慮采用一個隊列,隊列保存的基本單元是 memory block或者vector, 但這無形中會增加小內(nèi)存的分配,也導(dǎo)致更多的 send操作,即更多的系統(tǒng)調(diào)用,采用混合的方法可能好一點,根據(jù) Queue.last_in_vector_item的長度來決定是對 Queue.last_in_vector_item執(zhí)行 append操作,還是對Queue執(zhí)行 putItem操作。  回復(fù)  更多評論   

            # re: Muduo 設(shè)計與實現(xiàn)之一:Buffer 類的設(shè)計 2011-04-25 14:58 陳碩

            @路人甲
            確實,output buffer 不必是連續(xù)的,反正復(fù)雜性丟給 TcpConnection 唄,可以用 writev 來減少系統(tǒng)調(diào)用次數(shù)。  回復(fù)  更多評論   

            # re: Muduo 設(shè)計與實現(xiàn)之一:Buffer 類的設(shè)計[未登錄] 2011-04-26 00:13 楊粼波

            std::vector進行一次內(nèi)存分配的成本是很高的,需要銷毀掉原有的內(nèi)存塊,創(chuàng)建一塊新的內(nèi)存塊,然后還有進行一次拷貝。啟動的時候,使用std::vector::reserve()進行預(yù)先分配,成本將會低上許多,即使是超支了,再分配的次數(shù)也是很微小的。這是一種空間換時間的做法。

            像Buffer這種可以預(yù)先知道大致大小的場景,可以使用std::vector::reserve()預(yù)先分配好內(nèi)存塊。而非在使用時實時的進行多次的動態(tài)分配,雖然時間復(fù)雜度不大,但是代價還是是很大的。

            std::vector::capacity() 返回vector所能容納的元素數(shù)量(在不重新分配內(nèi)存的情況下)

            std::vector::reserve() 設(shè)置Vector最小的元素容納數(shù)量。

            做內(nèi)存分配的是STL內(nèi)置的allocator,capacity()本身所取得的是allocator所預(yù)先分配的大小。

            vector::push_back導(dǎo)致vector大小增長過程是這樣的,0 -> 1 -> 2 -> 4 -> 8 -> 16 -> 32 ...
            既是以2的指數(shù)增長,雖然時間復(fù)雜度是o(1),但是還是比較費,因為有內(nèi)存銷毀,因為有內(nèi)存拷貝,如果是原生數(shù)據(jù)還稍微好一點,如果是struct或者class,這是一個惡夢。

            另,std::vector::append()這個方法是不存在的。

            @路人甲
            我并非惡語相向,而是如此的誤導(dǎo)性的東西,是在誤人子弟。此類型文章是面向初學(xué)者的,如果一個引路者誤導(dǎo)他們,這是一件很糟糕的事情。
            還有,0bug是誰?我還真不認識。  回復(fù)  更多評論   

            # re: Muduo 設(shè)計與實現(xiàn)之一:Buffer 類的設(shè)計 2011-04-26 17:18 jigsaw

            陳碩所展示的編程能力全都是toy級的。

            從難易程度上來說,可以給初學(xué)者當入門資料;但從正確性上來說,充其量只能敝帚自珍了。

            不過以前陳碩跟我辯論的時候,我發(fā)現(xiàn)他有一特長。就是會悄悄地把我說的話嫁接到他的話里;本來是反駁他的話,結(jié)果變成支持他。并且還一而再再而三的重復(fù),以至于好像變成了他的原創(chuàng)。 吐血~~  回復(fù)  更多評論   

            # re: Muduo 設(shè)計與實現(xiàn)之一:Buffer 類的設(shè)計 2011-04-26 17:24 jigsaw

            當然濫竽充數(shù)的人很多,比如0bug老師,也不多陳碩一個了。

            我只是覺得對0bug老師不公平啊。都是南郭先生,為什么0bug被釘上了恥辱柱,而陳碩(還有其他一些了,就不一一點名了)卻成了偶像呢?

            嗯,看來還是因為0bug的廣告效果不如陳碩的好。現(xiàn)在的市場,會包裝就行,管你裝的是酒還是醋呢。  回復(fù)  更多評論   

            # re: Muduo 設(shè)計與實現(xiàn)之一:Buffer 類的設(shè)計 2011-04-27 09:53 陳碩

            @楊粼波
            思考題:為什么 muduo::net::Buffer 不需要 reserve()?  回復(fù)  更多評論   

            # re: Muduo 設(shè)計與實現(xiàn)之一:Buffer 類的設(shè)計 2011-05-16 11:21 yrj

            "prepend' 有 Pascal String 的味道  回復(fù)  更多評論   

            # re: Muduo 設(shè)計與實現(xiàn)之一:Buffer 類的設(shè)計 2012-02-13 15:08 fztcjjl@gmail.com

            這個函數(shù)好像有個BUG
            void shrink(size_t reserve)
            {
            std::vector<char> buf(kCheapPrepend+readableBytes()+reserve);
            std::copy(peek(), peek()+readableBytes(), buf.begin()+kCheapPrepend);
            buf.swap(buffer_);
            }

            buf.swap(buffer_)進行交換,將first last end指針交換到buffer_對象中,但是函數(shù)返回的時候buf對象被析構(gòu)了,這些指針所指向的內(nèi)存也跟著被釋放了。  回復(fù)  更多評論   

            # re: Muduo 設(shè)計與實現(xiàn)之一:Buffer 類的設(shè)計 2012-02-13 15:34 solstice

            @fztcjjl@gmail.com
            No. swap 是安全的,析構(gòu)的是原來 buffer_ 里的內(nèi)容。
            你實驗一下就知道。  回復(fù)  更多評論   

            # re: Muduo 設(shè)計與實現(xiàn)之一:Buffer 類的設(shè)計 2012-02-13 16:17 fztcjjl@gmail.com

            可能是我對這個函數(shù)的功能理解錯了  回復(fù)  更多評論   

            # re: Muduo 設(shè)計與實現(xiàn)之一:Buffer 類的設(shè)計 2012-02-13 16:36 solstice

            @fztcjjl@gmail.com
            retrieveAsString() 會清空 buffer,第二次調(diào)用自然就沒有內(nèi)容了。  回復(fù)  更多評論   

            # re: Muduo 設(shè)計與實現(xiàn)之一:Buffer 類的設(shè)計 2012-02-13 18:13 fztcjjl@gmail.com

            @solstice
            終于明白這句話的意思了  回復(fù)  更多評論   

            # re: Muduo 設(shè)計與實現(xiàn)之一:Buffer 類的設(shè)計[未登錄] 2015-08-29 15:02 name

            求大神,libevent的evbuffer 接收大數(shù)據(jù) 怎么接收? 如果接收N兆的數(shù)據(jù) 是分n次,每次調(diào)用回調(diào)去接收,這樣我就做一個狀態(tài)機去讀取,可是,到最后一點數(shù)據(jù),又要用while把每次回調(diào)的buffer數(shù)據(jù)全部讀出來(因為不會在次調(diào)用回調(diào)了,所謂的回調(diào),是在每次buffer添加數(shù)據(jù)才進行回調(diào),所以在回調(diào)函數(shù)一定要全部讀取完數(shù)據(jù)),這樣感覺太不好了,有沒有更好的方案  回復(fù)  更多評論   


            只有注冊用戶登錄后才能發(fā)表評論。
            網(wǎng)站導(dǎo)航: 博客園   IT新聞   BlogJava   博問   Chat2DB   管理


            <2011年4月>
            272829303112
            3456789
            10111213141516
            17181920212223
            24252627282930
            1234567

            導(dǎo)航

            統(tǒng)計

            常用鏈接

            隨筆分類

            隨筆檔案

            相冊

            搜索

            最新評論

            閱讀排行榜

            評論排行榜

            国产精品99久久精品爆乳| 久久精品国产国产精品四凭| 久久精品一区二区| 欧洲人妻丰满av无码久久不卡| 久久精品无码一区二区三区日韩 | 18禁黄久久久AAA片| 91久久精品无码一区二区毛片| 久久精品aⅴ无码中文字字幕不卡| 亚洲人成无码网站久久99热国产| 国产日韩久久免费影院| 久久99精品国产麻豆不卡| 99久久精品免费看国产| 国产精品免费久久久久久久久| 久久这里只有精品久久| 草草久久久无码国产专区| 国产叼嘿久久精品久久| 久久婷婷人人澡人人| 三级韩国一区久久二区综合| 奇米影视7777久久精品人人爽| 18禁黄久久久AAA片| 久久综合给合久久狠狠狠97色| 久久久无码一区二区三区| 国产午夜久久影院| 久久AⅤ人妻少妇嫩草影院| 久久综合伊人77777麻豆| 国内精品伊人久久久久777| 久久亚洲私人国产精品vA| 美女写真久久影院| 伊人久久成人成综合网222| 亚洲AV日韩AV永久无码久久| 97久久精品无码一区二区| 久久久久18| 久久久女人与动物群交毛片| 99久久国产综合精品五月天喷水| 欧洲国产伦久久久久久久 | 亚洲AV无码久久| 久久国产亚洲精品无码| 久久久久婷婷| 国产精品久久久久影院色| 久久午夜福利电影| 国产精品一久久香蕉国产线看 |