Linux多線程服務器端編程

Linux多線程服務器端編程

  • 源碼連接
  • muduo的編譯安裝.
  • 陳碩的編譯教程
  • bazel編譯文件不能有中文路徑。
  • 安裝到指定目錄:
    • /usrdata/usingdata/studying-coding/server-development/server-muduo/build/release-install-cpp11/lib/libmuduo_base.a.
  • 這本書前先後後看了三四遍,寫得很是有深度,值得推薦。
  • 編譯和安裝.

線程安全的對象生命期管理

  • 利用shared_ptr和weak_ptr避免對象析構時存在的競爭條件(race conditon).
  • 當一個對象被多個線程同時看到,那麼對象的銷燬時機就會變得模糊不清,可能出現多種競爭條件(race condition).
  • 用RAII(Resource Acquire Is Initialization, 資源申請即初始化)封裝互斥量的建立和銷燬, MutexLock封裝臨界區(critical section), 資源管理類。
  • MutexLockGuard封裝臨界區的進入和退出,即加鎖和解鎖,MutexLockGuard通常是個棧上對象,它的做用域恰好等於臨界區域。
  • 不可拷貝類.
    • 把copy構造函數和複製操做符聲明爲私有函數並不聲明。
    • 在C++11中使用delete關鍵字,muduo採用了這種方式。
    namespace muduo
      {
    
      class noncopyable
      {
          public:
          noncopyable(const noncopyable&) = delete;
          void operator=(const noncopyable&) = delete;
    
          protected:
          noncopyable() = default;
          ~noncopyable() = default;
      };
    
      }  // namespace muduo
  • Linux的capability機制.
  • 對象構造要作到線程安全,惟一的要求是在構兆期間不要泄漏this指針。
    • 不要在構造函數中註冊任何回調;(利用二段式構造(構造函數+initialization()),或直接調用register_函數)
    • 不要把在構造函數中把this傳給跨線程的對象;
    • 即使在構造函數的最後一行也不行。(構造函數執行期間對象尚未完成初始化。)

對象的銷燬線程比較難

  • 單線程對象析構要注意避免空懸指針和野指針。多線線程每一個成員函數的臨界區域不得重疊,並且成員函數用來保護臨界區的互斥器自己必須是有效的。
  • 在析構函數中直接調用互斥器進行多線程的同步是不可取的,沒有徹底達到線程安全的效果。
  • 做爲數據成員的mutex不能保護析構, 由於成員的生命週期最多與對象同樣長,而析構動做能夠發生在對象死亡以後。(調用基類析構函數時,派生類的析構函數已經被調用)
  • 析構函數自己不須要保護,由於只有別的線程都訪問不到這個對象時,析構纔是安全的。
  • 若是要鎖住相同類型的多個對象,爲了保證始終按相同的順序加鎖,能夠比較mutex對象的地址,始終先加鎖地址較小的mutex.(防止死鎖)
  • 判斷一個指針是否是合法指針沒有高效的辦法,這是C/C++指針問題的根源。
  • 調用正在析構對象的任何非靜態成員函數都是不安全的,更況且是虛函數。
  • 指向對象的原始指針(raw pointer)最好不要暴露給別的線程。--- 通常用智能指針
  • 解決空懸指針的辦法是,引入一層間接性。(handle/body慣用技法)
  • shared_ptr指針源碼分析.
    • shared_ptr控制對象的生命期,是強引用,只要有一個指向x對象的shared_ptr存在,該x對象就不會析構,當指向對象x的最後一個shared_ptr析構或reset()調用時,x保證會被銷燬。
    • weak_ptr不控制對象的生命期,但它知道對象是否還或者;
      • 若是對象還活着,weak_ptr能夠提高爲有效的shared_ptr;
      • 若是對象已經死了,提高失敗,返回一個空的shared_ptr;
      • 提高函數lock()行爲是線程安全的。
    • shared_ptr/weak_ptr的計數在主流平臺上是原子操做,沒有用鎖,性能不俗。
  • 資源(包括複雜對象本省)都是經過對象(只能指針或容器)來管理,不要直接調用delete來刪除資源。
  • shared_ptr自己的引用計數自己是線程安全的,可是讀寫操做不是原子化的。
  • shared_ptr技術與陷阱:
    • 若是不當心多進行了拷貝或賦值就會意外延長對象的生命週期。
    • 析構動做在建立時被捕獲;
      • shared_ptr 能夠持有任何對象,並且能安全地釋放。(析構動做能夠定製)
    • 爲了避免影響關鍵線程的速度,能夠用一個單獨的線程來作shared_ptr對象的析構。
    • 要避免shared_ptr管理共享資源時引發的循環引用,一般作法是owner持有指向child的shared_ptr,child持有指向owner的weak_ptr.
    • shared_ptr的析構函數能夠有一個額外的模板類參數,傳入一個函數指針或反函數d,在析構對象時執行d(ptr),其中ptr是shared_ptr保存的對象指針。
    • 弱回調技術會在事件通知中會很是有用。

線程同步精要

  • 線程同步的四原則:
    • 首要原則是儘可能最低限度地共享對象,減小須要同步的場合。
    • 其次使用高級的併發編程構建(TaskQueue, Producer-Consumer Queue, CountDownLatch(倒計時)).
    • 最後不得已必須使用底層同步原語(promitives)時,只用非遞歸的互斥器和條件變量,慎用讀寫鎖,不要用信號量。
      • 使用非遞歸(non-recursive)互斥量能夠把程序錯誤儘早地暴露出來。
    • 除了使用atomic整數以外,不本身編寫lock-free代碼,也不要用內核級的同步原語。
  • 若是堅持Scoped Locking,那麼出現死鎖的時候就很容易定位。
    • gdb ./self_deadlock core --- 調試定位死鎖。
    • __attribute__能夠用來防止函數inline內聯展開。
  • 條件變量(condition variable): 一個或多個線程等待某個布爾表達式爲真,即等待別的線程喚醒它。
    • 條件變量的學名叫做管程(monitor);
    • 必須和mutex一塊兒使用, 該布爾表達式的讀寫受此mutex保護。
    • 在mutex已上鎖的時候才能調用wait().
    • 把判斷布爾條件和wait()放到while循環中。
    • 虛假喚醒(spurious wakeup):
  • 倒計時(CountDownLatch)是一種經常使用且易用的同步手段,主要用途:
    • 主線程等待多個子線程完成初始化;
    • 多個子線程等待主線程發起起跑命令。
    • 使用CountDownLatch使程序邏輯更清楚。
  • pthread_onece保證函數只執行一次。
  • sleep並非同步原語。
    • 若是須要等待一段已知的時間,應該往event loop裏註冊一個timer,而後在timer的回調函數裏接着幹活,由於線程是個珍貴的資源,不能輕易浪費(阻塞也是浪費)。

借shared_ptr實現寫時拷貝(copy-on-write)

  • 寫時若是引用計數大於1,該如何處理?
  • 用普通的mutex替換讀寫鎖。
  • 大多數狀況下更新都是在原來數據上進行的,拷貝的比例還不到1%.

多線程服務器的適用場合與經常使用編程模型

  • 進程(process)是操做系統裏最重要的兩個概念之一(另外一個是文件),一個進程就是內存中正在運行的程序。
  • 每一個進程有本身獨立的地址空間(adress space), 在同一個進程仍是不在同一個進程是系統功能劃分的重要決策。
  • 把一個進程比喻成一我的,週期性的心跳判斷對方是否還活着。
    • 容錯 --- 萬一有人忽然死了。
    • 擴容 --- 新人中途加進來。
    • 負載均衡 --- 把甲的活挪給乙作。
    • 退休 --- 甲要修復Bug, 先別派新任務,等他作完手上的活就把他重啓。
  • 線程的特色是共享地址空間,從而能夠高效地共享數據。
    • 多進程能夠高效地共享代碼段,可是不能共享數據。
    • 多線程能夠高效地發揮多核的效能。(單核,按照狀態機編程思想比較高效)

單線程服務器的經常使用編程模型

  • 「nonblocking IO + IO multiplexing(非阻塞IO + IO 多路複用)」, 即Reactor模式(反應堆模式)。
    • lighttpd, 單線程服務器(Nginx與之相似,每一個工做進程有一個事件循環(event loop)).
    • libevent, libev.
    • ACE, Poco C++ Libaries.
    • Java NIO, 包括Aache Mina 和 Netty.
    • POE(Perl).
    • Twisted(Python).
  • 「nonblocking IO + IO multiplexing(非阻塞IO + IO 多路複用)」, 即Reactor模式(反應堆模式)的基本結構是一個事件循環:
    • 以事件驅動和事件回調的方式實現業務邏輯。
    • Reactor模式(Linux採用epoll機制)對於IO密集的應用是一個不錯的選擇。
    • 巧妙地使用fdevent結構。
    • 基於事件驅動的編程模式的本質缺點是: 要求事件回調函數必須是非阻塞的,容易割裂業務邏輯,將其散佈於各個回調函數中。

多線程服務器的經常使用編程模型

  • 每一個請求建立一個線程,使用阻塞式IO操做(可伸展性不佳)。
  • 使用線程池。
    • 阻塞的任務隊列(blocking queue TaskQueue)。
    • Intel Threading Building Blocks的concurrent_queue 性能比較好。
    • 線程池(thread pool)用來作計算, 能夠用任務隊列或者是生產者消費者隊列實現。
  • 使用nonblocking IO + IO multiplexing(非阻塞IO + IO 多路複用)。
    • 每一個IO線程有一個event loop(或者叫Reactor)用於處理讀寫和定時事件。
    • 對實時性要求高的鏈接(connection)能夠單獨使用一個線程。
    • 數據量大的鏈接能夠獨佔一個線程,並把數據處理任務分攤到另幾個計算線程中(用線程池)。
    • 其餘輔助性的鏈接能夠共享一個線程。
  • Leader/Follower等高級模式。java

  • 進程間通訊:
    • 匿名管道(pipe);
      • 用來異步喚醒select(或等價的poll或epoll_wait)調用。
    • 具名管道(FIFO);
    • POSIX消息隊列;
    • 共享內存;
      • 共享內存是消息協議,a進行填好一塊內存讓b進程來讀,基本上是停等方式(stop wait).
    • 信號(signals);
    • 套接字(socket),通常用TCP, 不考慮domain協議和UDP(能夠跨主機,具備伸縮性)。
      • TCP是雙向的,管道pipe是單向的(進程間雙向通訊須要打開兩個文件描述符,父子進程才能用pipe).
      • 套接字由一個進程獨佔,且操做系統會自動回收(關閉文件描述符時)。
      • 套接字是端口是獨佔的,能夠防止程序重複啓動。
      • 能夠用tcpcopy工具進行壓力測試。
      • TCP是字節流協議,只能順序讀取,有寫緩衝區;
      • RPC/HTTP/SOAP上層通訊協議都是用的TCP網絡層協議。
  • 同步原語(synchronization primitives):
    • 互斥器(mutex);
    • 條件變量(condition variable);
    • 讀寫鎖(reader-writer lock);
    • 文件鎖(recode locking);
    • 信號量(semaphore)

分佈式系統中使用TCP長鏈接通訊

  • 分佈式系統是由運行在多臺機器上的多個進程組成的,進程間採用TCP長鏈接通訊(創建鏈接後不會當即關閉)。
  • 在實現每一類服務器進程時,在必要時能夠經過多線程提升性能。
  • 對整個分佈式系統,要作到能scale out, 即享受增長機器帶來的好處。
  • TCP長鏈接的好處:
    • 容易定位分佈式系統中服務之間的依賴關係。 --- netstat -tpna | grep :port
      • 客戶端用netstat或lsof找到那個進程發起的鏈接。
    • 經過接收和發送隊列的長度也較容易定位網絡或程序故障。 --- netstat -tn觀察Recv-Q和Send-Q的變化狀況。
  • Event Loop(事件循環): 事件循環的最明顯的缺點是非搶佔的(non-preemptive), 可能會發生優先級發轉(經過多線程克服)。
  • 多線程的使用適用場合:
    • 提升響應速度,讓IO和計算相互重疊,下降latency.
  • 多線程不能提升併發度(併發鏈接數)。
  • 多線程也不能提升吞吐量,但多線程可以下降響應時間。
  • 線程池的經驗公式 T=C/P(一個有C個CPU, 密集計算所佔的時間比重爲P( 0 < P <= 1)).
  • 若是一次請求響應中要和別的進程打屢次交道,那麼Proactor模型每每能作到更高的併發度。
  • Proactor模式依賴操做系統或庫來高效地調度這些子任務,每一個子任務都不會阻塞,所以能用比較少的線程達到很高的IO併發度。
  • Proactor能提升吞吐量,但不能下降延遲。

C++多線程系統編程精要

  • 多線程編程面臨的最大思惟方式的轉變有兩點:
    • 當前線程可能隨時會被切換出去,或者說被搶佔(preempt)了。
    • 多線程程序中,事件的發生順序再也不有全局同喜的前後關係。
  • 多線程程序的正確性不能依賴於任何一個線程的執行速度,不能經過原地等待(sleep())來假定其餘線程的時間已經發生,而必須經過適當的同步來讓當前線程能看到其餘線程的時間的結果
  • 線程(thread),互斥量(mutex),條件變量(condition)這三個線程原語能夠完成任何多線程任務。
  • 內存序(memory ordering),內存模型(memory model),內存能見度(memory visibility)。
    • Linux系統自己是能夠被搶佔的(preemptive).
    • errno是一個全局變量。
    • 不用擔憂系統調用的線程安全性,由於系統調用對用戶態程序來講是原子的。
      • 但系統調用對於內核態的改變可能影響其餘線程
  • C++中的泛型函數通常都是線程安全的。C++ iostream不是線程安全的。
  • pthreads只能保證同一進程內,同一時刻的各個線程的id不一樣;不能保證同一進程前後過個線程具備不一樣的id.
  • 若是程序中不止一個線程,就很難安全地fork()了。
  • 在main()函數以前不該該啓動線程,由於這會影響全局對象的安全構造。
    • 全局對象不能建立線程。
  • kill一個進程比殺死本地進程內的線程要安全得多。
  • 不要用共享內存和跨進程的互斥器等IPC, 由於這樣仍然有死鎖的可能。
    • Thread的析構不會等待線程結束。
    • 若是Thread對象的生命期短於線程,那麼析構時會自動detach線程(僵死線程的感受),避免資源泄漏。
    • 程序中的線程建立最好能在初始化階段所有完成,則程序是沒必要銷燬的。
    • 最好不要經過外部殺死線程。
  • exit()可能致使析構對象時形成死鎖。
  • 善用__thread關鍵字,但只能用於內置類型。
    • __thread變量是每一個線程有一份獨立實體,各個線程的變量值都互不干擾。
  • 多線程磁盤IO的一個思路是每一個磁盤配一個線程,把全部針對此磁盤的IO挪到同一個線程,能夠避免或者減小內核中的鎖競爭。
    • 每一個文件描述符只由一個線程操做。
  • Linux/Unix中, 信號(signal)與多線程可謂是勢不兩立,信號打斷了正在運行的線程控制權。
  • fork()以後, 子進程幾乎繼承父進程的全部狀態,但子進程不會繼承:
    • 父進程的內存鎖: mlock,mlockall.
    • 父進程的文件鎖: fcntl.
    • 父進程的某些定時器,setitimer,alarm,timer_create等(man 2 fork).
  • 多線程和fork協做不好,fork通常不能在多線程程序中調用,由於Linux中的fork只會克隆當前線程的線程控制權,不克隆其餘線程。
    • fork以後,除了當前線程以外,其餘線程都消失了。
    • 調用fork後,當即調用exec()執行另外一個程序,完全隔斷子進程與父進程的聯繫。

高效的多線程日誌

  • 日誌能夠分爲兩類:
    • 診斷日誌(diagnostic log), 用於故障診斷和追蹤(trace), 也可用於性能分析。
      • 每條日誌都要有對應的時間戳。
      • 生產者-消費者模型: 對生產者(前端)而言,要儘可能作到低延遲、低CPU開銷、無阻塞;對消費者(後端)而言,要作到足夠大的吞吐量,並佔用較少的資源。
      • 整個程序最好使用相同的日誌庫(庫程序和主程序)。 --- 日誌庫最好是一個單例(singleton).
    • 交易日誌(transaction log), 用於記錄狀態變動,經過回放日誌能夠逐步恢復每一次修改的狀態。

日誌功能的需求

  1. 日誌消息有多種級別(level): TRACE, DEBUG, INFO, WARN, ERROR,FATAL等。
  2. 日誌消息的格式可配置(layout)。
  3. 日誌消息可能有多個目的地(appender),如文件、socket,SMTP等。
  4. 能夠設置運行時過濾器(filter),控制不一樣組件的日誌消息的級別和目的地。
  5. 日誌的輸出級別須要在運行時進行動態調整(不須要從新編譯,也不要從新啓動進程)。
  • muduo庫只要調用muduo::Logger::setLogLevel()就能即時生效。
  1. 分佈式系統中,日誌的目的地(destination)只有一個: 本地文件。
    • 由於診斷日誌的功能之一就是診斷網絡故障:
      • 連接斷開(網卡或交換機故障);
      • 網絡暫時不通(若干秒以內沒有心跳消息);
      • 網絡擁塞(消息延遲明顯加大)等。
    • 也應該避免往網絡文件系統(NFS)上寫日誌。
  • 日誌回滾(rolling):
    • 回滾(rolling)一般具備兩個條件:
      • 文件大小(如寫滿1GB就換下一個文件);
      • 時間(如天天零點新建一個日誌文件,不論前一個文件有沒有寫滿)。
  • 日誌文件壓縮和歸檔(archive)不是日誌庫應有的功能,應該交給專門的腳本去作。
  • 按期(默認3秒)將緩衝區內的日誌消息flush到硬盤;
  • 每條內存中的日誌消息都帶有cookie(或者叫哨兵值/sentry),其值爲某個函數地址,經過core dump查找cookie就能找到還沒有來得及寫入磁盤的消息。react

  • muduo庫的優化措施:
    • 時間戳字符串中的日期和時間兩部分是緩存的,一秒內的多條日誌只須要從新格式化微妙部分。
    • 日誌消息的前4個字段是定長的,避免在運行期求字符串長度。
    • 線程id是預格式化爲字符串,在輸出日誌消息時只需簡單拷貝幾個字節。
    • 文件名basename採用編譯期計算。

多線程異步日誌

  • 多線程寫多個文件也不必定會提速,因此儘可能一個進程的多線程寫一個文件。
    • 用一個背景線程負責收集日誌消息,並寫入日誌文件,其餘線程只管往這個日誌線程發送日誌消息,這稱爲"異步消息"("非阻塞日誌")。
  • 在正常的實時業務處理流程中應該測底避免磁盤IO。
  • muduo日誌庫採用雙緩衝技術(double buffering):
    1. 準備兩塊buffer: A和B, 前端負責往A填數據(日誌消息),後端負責將buffer B的數據寫入文件;
    2. 當buffer A寫滿以後,交換A和B,讓後端將buffer A的數據寫入文件,而前端則往buffer B填入新的日誌消息,如此往復。
    • 這麼作的好處是:
      • 新建日誌消息的時候沒必要等待磁盤文件操做,也避免每條新日誌消息都觸發(喚醒)後端日誌線程。
      • 即使buffer A未填滿,日誌庫也會每3秒執行一次交換寫入操做。
    • 壞處是: 前端消息速度(前端buffer寫速度)要和buffer大小作好平衡,不然會出現後端寫入磁盤尚未寫完,前端的buffer就已經填滿。
  • Java的ConcurrentHashmap那樣用多個筒子(bucket),前端寫日誌的時候再按線程id哈希到不一樣的bucket, 以減小競爭。
  • Linux默認會把core dump寫到當前目錄,並且文件名是固定的core。(sysctl能夠進行設置core dump的一些參數)

muduo網絡庫簡介

  • 高級語言(Java, Python等)Socket庫並無對Sockets API提供更高層的封裝,直接調用很容易掉入到陷阱中;網絡庫的價值在於能方便地處理併發鏈接。
  • muduo使用了較新的系統調用(主要是timefd和eventfd),要求linux內核的版本大於2.6.28.
  • muduo是基於Reactor模式的網絡庫,其核心是個時間循環EventLoop, 用於響應計時器和IO事件。
  • muduo採用基於對象而非面向對象的設計風格,其事件回調接口多以boost::function+boost::bind表達。
  • muduo主要掌握關鍵的5個類: Buffer, EventLoop, TcpConnection, TcpClient, TcpSever.
    網絡核心頭文件.
    muduo的簡化類圖.
  • 一個文件描述符(file descriptor)只能由一個線程讀寫。
  • muduo支持非併發阻塞TCP網絡編程,它的核心是每一個IO線程一個事件循環,把IO事件分發到回調函數上。減小網絡編程中的偶發複雜性(accidental complexity).
  • muduo擅長的領域是TCP長鏈接(創建鏈接後一直收發、處理數據)。

TCP網絡編程最本質的是處理三個半事件:

  1. 鏈接的創建: 服務端成功接受(accept)新鏈接和客戶端成功發起(connect)鏈接。
  2. 鏈接的斷開: 包括主動斷開(close、shutdown)和被動斷開(read(2) 返回0)。
  3. 消息到達,文件描述符可讀: 對它的處理決定了網絡編程的風格(阻塞仍是非阻塞,如何處理分包,應用層的緩衝如何設計等)。
  4. 消息發送完畢,這算半個: 發送完畢是指數據寫入操做系統的緩衝區,將由TCP協議棧負責數據的發送與重傳,不表明對方已經收到了數據。

在一個端口上提供服務,而且要發揮多核處理器的計算能力

  • 高性能httpd(httpd是一個開源軟件,且通常用做web服務器來使用)廣泛採用的是單線程Reactor方式。
    12種併發模型
  • 推薦的C++多線程服務端編程模式: one loop per thread + threadpool.
    • event loop 用做non-blocking IO和定時器。
    • threadpool用來作計算,具體能夠是任務隊列或生產者消費者隊列。
  • 實用的5種方案,muduo支持後4種:
    實用的5種網絡編程方案

muduo編程示例

  • daytime是短鏈接協議,在發送完當前時間後,由服務端主動斷開鏈接。
  • 非阻塞網絡編程必須在用戶態使用接收緩衝區。
  • TcpConnection對象表示一次TCP鏈接,鏈接斷開後不能重建,重試後鏈接的會是另外一個TcpConnection對象。
  • Chargen協議很特殊,它只發送數據,不接收數據,並且,它發送數據的速度不能快過客戶端接收的速度。
  • 在非阻塞網絡編程種,發送消息一般是由網絡庫完成,用戶代碼不會直接調用write或send等系統調用。
  • TCP是一個全雙工協議,同一個文件描述符便可讀又可寫,shutdownWrite()關閉了"寫"方向的鏈接,保留了"讀方向"的鏈接,這稱爲TCP半關閉(half-close).
    • 若是直接close(socket_fd), 那麼sock_fd就不能讀或寫了。
      muduo關閉鏈接.
  • 在TCP這種字節流協議上作應用層分包是網絡編程的基本需求。
    • 分包指的是在發生一個消息(message)或一幀(frame)數據時,經過必定的處理,讓接收方能從字節流種識別並截取(還原)一個個消息。
    • 對於短鏈接,只要發送方主動關閉鏈接,分包不是一個問題。
    • 對於長鏈接,分包有四種方法:
      1. 消息長度固定。
      2. 使用特殊字符或者字符串做爲消息的邊界(如'\r\n')。
      3. 在每條消息的頭部加一個長度字段(最經常使用的作法)。
      4. 利用消息自己的格式來分包(XML, JSON, 但歇息這種消息格式一般會用到狀態機(state machine))。
  • non-blocking(非阻塞)IO的核心思想是避免阻塞在read()或write()或其餘IO調用上,這樣能夠最大限度地複用thread-of-control, 讓一個線程能服務於多個socket鏈接。
    • IO線程只阻塞在IO multiplexing(複用)函數上(如select/poll/epoll_wait等)。
  • muduo庫採用的是水平觸發(level trigger), 而不是邊沿觸發(edge trigger)。
    1. 爲了與傳統的poll兼容,在文件描述符較少,活動文件描述符比例較高時,epoll不見得比poll更高效。
    • 必要時能夠在進程中切換Poller.
    1. 水平觸發(level trigger)編程更加容易,不可能發生漏掉事件的bug.
    2. 讀寫的時候沒必要等候出現EAGAIN, 能夠節省系統調用次數,下降延遲。
  • muduo全部的IO都是帶有緩衝的IO(buffered IO)。
    • 在棧上準備一個65536字節的額外緩存(extrabuf), 利用readv來讀取數據,iovec有兩塊,第一塊指向muduo的Buffer中的可寫字節,另外一塊指向extrabuf。
      • 數據很少,直接存到Buffer中,若是較多,剩餘的放到extrabuf中進行緩存,而後再存到Buffer中。
  • send()是線程安全原子的,多個線程能夠同時調用send(),消息之間不會混疊或交織。
  • FILE是一個在stdio.h中預先定義的一個文件類型.linux

    typedef struct{
      short level;              /*緩衝區「滿/空」的程度*/
      unsigned flags;           /*文件狀態標誌字*/
      char fd;
      unsigned char hold;
      short bsize;              /*緩衝區大小*/
      unsigned char *buffer;    /*數據緩衝區的位置*/
      unsigned char *curp;      /*當前讀寫位置指針*/
      unsigned istemp;
      short token;
    }FILE;
    • boost::any能夠表示任意類型,因此boost::any用不了多態的特性。
  • pintf()函數是線程安全的,但std::cout<<不是線程安全的。
  • 解析數據每每比生成數據更加複雜。
  • 非阻塞讀(nonblocking read)必須和input buffer一塊兒使用,在接收方(decoder)必定要在收到完整的消息以後再retrieve(取出)整條消息。
  • Buffer其實就像是一個隊列(queue), 從末尾寫入數據,從頭部讀出數據。
    • Buffer內部是一個std::vector ,它是一塊連續的內存,參考netty的ChannelBuffer(prependable是微創新)。
      • 若是readIndex太靠右,就不會從新分配內存,而是把已有數據移動到前面,騰出writable空間。
      • 前方添加(prepend):提供prependable空間,讓程序可以以很低的代價在數據前面添加傑哥字節。
        buffer結構
    • muduo Buffer不是固定長度的,它能夠自動增加(使用vector的好處)。
    • readIndex和writeIndex是整數(由於指針的話,在新建數組時會失效)。
    • Buffer沒有自動shink, 數組只會愈來愈大。
      vector的好處
    • libevent 2.0.x的設計方案:
      • 實現分段連續的zero copy buffer再配合gather scatter IO(mbuf方案, Linux的sk_buff方案),基本思路是不要求數據在內存中是連續的,而是用鏈表把數據鏈接到一塊兒。
        zero_copy_buffer
    • muduo的設計目標之一是吞吐量能讓千兆以太網飽和,每秒收發120MB的數據。

一種自動反射消息類型的Google Protobuf網絡傳輸方案

  • Google Protocol Buffers(簡稱Protobuf)是一款很是優秀的庫,它定義了一種緊湊的可擴展二進制消息格式,特別適合網絡數據傳輸。
    protobuf反射自動建立message對象
    • 拿到Message*指針,不用知道它的具體類型,就能建立和其餘類型同樣的具體Message type的對象。
    • 經過DescriptorPool能夠根據type name查到Descriptor*, 再調用DescriptorPool::findMessageTypeByName(const string& type_name)便可。
  • 使用步驟:
    1. 用DescriptorPool::generated_pool()找到一個DescriptorPool對象(它包含了編譯時所鏈接的所有Protobuf Message types).
    2. 根據type name用DescriptorPool::FindMessageTypeByName()查找Descriptor.
    3. 再用MessageFactory::generated_factory()找到MessageFactory對象,它能建立程序編譯的時候所連接的所有Protobuf Message types.
    4. 而後用MessageFactory::GetPrototype()找到具體Message type的default instance(默認實例)。
    5. 最後用prototype->New()建立對象。
      • 返回的是動態對象,調用方須要釋放它,可使用智能指針管理資源。(消息分發器dispatcher)
        protobuf的傳輸格式
  • java沒有unsigned類型。protobuf通常用於打包小於1MB的數據。
    • adler32校驗算法,計算量小,速度比較快,強度和CRC-32差很少。
    • Protobuf Message的一個突出有點是用optional fields來避免協議的版本號.
  • 只有再使用TCP長鏈接,而且在一個鏈接上傳遞不知一種消息的狀況下,才須要打包方案。(還須要一個分發器,把不一樣類型的消息分給各個消息小狐狸函數)。
  • non-trivial的網絡服務常旭一般會以消息爲單位來通訊,每條消息有明確的長度與界限。
    • codec(編解碼器)的基本功能是TCP分包: 肯定每條消息的長度,爲消息劃分界限。
      protobuf codec消息流程圖
    • Protobuf RPC.
  • ProtobufCodec攔截了TcpConnection的數據,把它轉換爲Message, ProtobufDispatcher攔截了ProtobufCodec的回調函數(callback). 按照消息具體類型把它分派給多個callbacks.
    protobuf RPC
  • filedescriptor是稀缺資源,若是出現文件描述符(filedescriptor)耗盡,很棘手。
  • 處理空閒鏈接超時: 若是一個長鏈接長時間(若干秒)沒有輸入數據,就踢掉此鏈接。--- 用timing wheel解決。
  • 定時器(時間):
    • 計時只使用gettimeofday(2)來獲取當前時間。 --- 精度爲1微妙。
    • 定時只使用timerfd_*系列函數來處理定時任務。
  • Netty是一個很是好的Java NIO網絡庫,帶有流量統計功能的echo和discard服務端。
  • 兩臺機器的網絡延遲和時間差(簡單網絡程序roundtrip).
    • NTP協議進行時間校準。
  • 應該用心跳消息來判斷對方進程是否能正常工做,timing wheel(時間輪盤)避免空閒鏈接佔用資源。
    • timing wheel只用檢查第一個桶中的鏈接。
    • 層次化的timing wheel與hash timing wheel.
    • timing wheel中的每一個格子是hash set,能夠容納不止一個鏈接。
    • 不會真的把一個鏈接從一個格子移動到另外一個格子,而是採用引用計數的辦法,用shared_ptr來管理Entry.
      • 若是鏈接接收到了數據,就把對應的EntryPtr放到這個格子裏,引用計數爲零,那麼就析構掉(斷開鏈接)。
  • 簡單的消息廣播服務:
    簡單的消息廣播服務
    • 能夠增長多個Subscriber而不用修改Subscriber(分佈式的觀察者模式Observer pattern).
    • 應用層廣播在分佈式系統中用處很大。 --- 消息應該是snapshot, 而不是delta(如今比分是幾比幾,而不是誰剛纔又得分了)。
    • ·sub<topic>\r\n, 表示訂閱 , 之後該topic有任何更新都會發給這個TCP鏈接。
      • Hub會把 上最近的消息發給此Subscriber.
    • unsub<topic>\r\n, 表示退訂 .
    • pub<topic>\r\n<content>\r\n, 表示往 發送消息,內容爲 .
    • 利用thread local的辦法解決多線程廣播的鎖競爭。
  • 數據串並轉換:
    • 鏈接服務器把多個客戶鏈接匯聚爲一個內部TCP鏈接,起到數據串並轉換的做用,後端(backend)的邏輯服務器專心處理業務。
    • 分爲四步:
      1. 當client connection到達或斷開時,向backend發出通知。
      2. 當從client connection收到數據時,把數據連同connection id一同發給backend.
      3. 當從backend connection收到數據時,辨別數據是發給那個client connection,並執行相應的轉發操做.
      4. 若是backend connection斷開鏈接,則斷開全部client connections(假設client會自動重試).
    • multiplexer的功能與proxy頗爲類似。
  • 中繼器(relay)主要把client與Server之間的通訊內容記錄下來(tcpdump的功能)。
    relay功能框圖
    • Sockets API來實現TcpRelay,須要splice系統調用。
    • 須要考慮的問題:
      須要考慮的問題

短址服務

  • muduo HTTP服務器能夠處理簡單的HTTP請求,也能夠用來實現一個簡單的短URL轉發服務。
  • 一種真正高效的優化手段是修改Linux內核,例如Google的SO_REUSEPORT內核補丁。ios

  • muduo的Channel class類,能夠把其餘一些現成的網絡庫融入muduo的event loop中。
    • Channel class是IO事件回調的分發器(dispatcher), 它在handleEvent()中根據事件的具體類型分別回調ReadCallback, WriteCallback等。
    • 每一個Channel對象服務於一個文件描述符,但並不擁有fd, 在析構函數中也不會close(fd).
    • Channel與EventLoop的內部交互有兩個函數:
      • EventLoop::updateChannel(Channel*);
      • EventLoop::removeChannel(Channel*).
      • 客戶須要在Channel析構前本身調用Channel::remove().
  • libcurl是一個經常使用的HTTP客戶端庫,能夠方便地下載HTTP和HTTPS數據。
  • muduo提供Channel::tie(const boost::shared_ptr &)這個函數,用於延長某些對象的生命期,使其壽命長過Channel::handleEvent()函數。
  • POSIX操做系統老是選用當前最小可用的文件描述符。git

muduo庫設計與實現

  • EventLoop的析構函數會記住本對象所屬的線程(threadId_), 建立了EventLoop的線程是IO線程。
    • 其主要同能時運行事件循環EventLoop::loop().
    • EventLoop對象的生命期一般和其所屬的線程同樣長,沒必要是heap對象。
  • Reactor的關鍵結構: Reactor 最核心的事件分發機制,即將IO multiplexing拿到的IO事件分發給各個文件描述符(fd)的事件處理函數。
  • Channel的成員函數都只能在IO線程調用,所以更新數據成員都沒必要加鎖。
    reactor 流程圖
    poll框架
  • TcpConnection簡單的狀態圖:
    TcpConnection簡單的狀態圖
  • SIGPIPE的默認行爲時終止進程,在網絡編程中,意味着若是對方斷開鏈接而本地繼續寫入的話,會形成服務進程意外退出。
  • TCPNoDelay和TCPkeepalive都是經常使用的TCP選項:
    • TCPNoDelay的做用是禁用Nagle算法,避免連續發包出現延遲,對編寫低延遲網絡服務很重要。
    • TCPkeepalive是按期檢查TCP鏈接是否還存在,若是有應用層心跳,TCPkeepalive不是必須的,但通常須要暴露其接口。
  • 用one loop per thread的思想多線程TcpServer的關鍵步驟是在新建TcpConnection時從event loop pool裏挑選一個loop給TcpConnection用.
  • 在併發鏈接較大而活動鏈接比例不高時, epoll比poll更有效.
  • 右值引用(rvalue reference)有助於提升性能與資源管理的便利性。

分佈式系統工程實踐

  • 分佈式系統設計以進程爲基本單位.
  • 不要把時間浪費在解決錯誤的問題,應集中精力應付更本質的業務問題。
  • 只用TCP爲進程間通訊,由於進程一退出,鏈接與端口自動關閉;並且不管鏈接的哪一方斷連,均可以重建TCP鏈接,恢復通訊。
  • 分佈式系統中心跳協議的設計:
    • 心跳除了說明應用程序還活着(進程還在,網絡暢通), 更重要的是代表應用程序還能正常工做。
    • TCP keepalive由操做系統負責探查,即使進程死鎖或阻塞,操做系統也會如常收發TCP keepalive消息。對方沒法得知這一異常。
    • 通常是服務端向客戶端發送心跳。
    • Sender和Receiver的計時器是獨立的。
    • 心跳協議的內在矛盾: 高置信度與低反應時間不可兼得。
    • timeout的選擇要能容忍網絡消息延時波動和定時器的波動。
      心跳協議
      • 發送週期和檢查週期均爲\(T{_C}\), 一般可取\(timeout=2T{_C}\).
    • 心跳應該包含發送方的標識符,也應包含當前負載,便於客戶端作負載均衡。
    • 考慮閏秒的影響,尤爲在考慮容錯協議的時候。
    • 心跳協議的實現上有兩個關鍵的點(防止僞心跳):
      • 要在工做線程中發送,不要單獨起一個心跳線程。
      • 與業務消息用同一個鏈接,不要單獨用心跳鏈接。
  • 分佈式系統沒有全局瞬時的狀態,不存在馬上判斷對方故障的方法,這是分佈式系統的本質困難。
  • 端口只有6萬多個,是稀缺資源,在公司內部也有分配完的一天,通常到高級階段會採用動態分配端口號。
  • 若是客戶端與服務端之間用某種消息中間件來回轉發消息,那麼客戶端必須經過進程標識符才能識別服務端。
    • 設置SO_REUSEADDR, 爲了快速重啓。
    • linux的pid的最大默認值是32768。
    • 用四元組ip::port::tart_time::pid做爲分佈式系統中進程的gpid, 其中start_time是64-bit整數,表示進程的啓動時刻(UTC時區)。
  • 在服務程序內置監控接口的必要性;HTTP協議的便利性。
  • Hadoop有四種主要services:NameNode,DataNode, JobTracker和TaskTracker.
    • 每種service都內置了HTTP狀態頁面。
  • 在本身辯詞額分佈式程序的時候,提供一個維修通道是頗有必要的,它能幫助平常運維,並且在出現故障的時候幫助排查。
  • 若是不在程序開發的時候統一預留一些維修通道,那麼運維起來就抓瞎了 --- 每一個進程都是黑盒子,出點什麼狀況都得拼命查log試圖(猜測)進程的狀態,工做效率不高。
  • 使用跨語言的可擴展消息格式。
    • 可擴展消息格式的第一條原則是避免協議的版本號。
  • protobuf的可選字段(optional fields)就解決了服務端和客戶端升級的難題。
    • proto文件就像C/C++動態庫的頭文件,其中定義的消息就是庫(分佈式服務)的接口,一單發佈就不能作有損二進制兼容性的修改。
  • 分佈式程序的自動化迴歸測試:
    • 自動化測試的必要性:
      • 自動化測試的做用是把程序已經事項的features以test case的形式固話下來,未來任何代碼改動若是破壞了現有功能需求就會觸發測試failure.
      • 單元測試(unit testing): 主要測試一個函數、一個類(class)活着相關的幾個class。
        • 單元測試的缺點:
          • 阻礙大型重構,單元測試是白盒測試,測試代碼直接調用被測試代碼,測試代碼和被測試代碼緊耦合。
          • java有動態代理,還能夠用cglib來操做字節碼以實現注入。
          • 網絡鏈接不上,數據庫超時,系統資源不足等都沒法測試。
          • 單元測試對多線程程序無能爲力
    • 分佈式系統測試的要點:
      • 測試進程間交互。
      • 用腳原本模擬客戶,自動化地測試系統的總體運做狀況,做爲系統的冒煙測試。
      • 一個分佈式系統就是一堆機器,每臺機器的"屁股"上拖着兩根線:電源線和網線(不考慮SAN等存儲設備)。
        分佈式架構
  • 千兆網的吞吐量不太於125MB/S. --- 只要能讓千兆網的吞吐量飽和或接近飽和,那麼編程語言就無所謂了。
  • Hadoop的分佈式文件系統HDFS的架構簡圖:
    HDFS
    • HDFS有四個角色參與其中: NameNode(保存元數據)、DataNode(存儲節點,多個)、Secondary NameNode(按期寫check point)、Client(客戶,系統的使用者)。
    • 這些進程運行在多態機器上,之間經過TCP協議互聯。但一個程序其實不知道與本身打交道的究竟是什麼。
  • Test harness(獨立的進程),模冒(mock)了與被測進程打交道的所有程序。
    迴歸測試方案
  • 壓力測試,test harness少加改進還能夠變功能測試爲壓力測試, 公程序員profiling用。
    • 發覆不間斷髮送請求,向被測程序加壓,用C++寫一個負載生成器。
  • 單獨的進程做爲test harness對於開發分佈式程序至關有幫助,它能達到單元測試的自動化程度和細緻程度,又避免了單元測試對功能代碼結構的侵入與依賴。

分佈式系統部署、監控與進程管理的幾重境界

  • 以Host指代服務器硬件。
  1. 境界1: 全手工操做,過家家級別,系統時靈時不靈,能夠跑跑測試,發發parper.
  2. 境界2: 使用零散的自動化腳本和第三方組件
  • 公司的開發中心放在實現核心業務,天健新功能方面,暫時還顧不上高效的運維。
  • host的IP地址由DHCP配置,公司的軟硬件配置比較統一。
  • 使用cron、at、logrotate、rrdtool等標準的Linux工具來將部分運維任務自動化.
    • QA簽署後部署(md5), md5sum檢查拷貝以後的文件是否與源文件相同。
    • Monit開源工具進行監控(內存、CPU、磁盤空間、網絡帶寬等)。
    • netstart-tpn | grep port(端口號)查詢哪些用到了程序。
  1. 境界3: 自制機制管理系統,幾種化配置
    境界3
  2. 境界4: 機羣管理與nameing service結合:
  • naming service的功能是把一個service_name解析成list of ip:port,比方說,查詢"sudo_solver",返回host1:998一、host2:998一、host3:9981.
  • naming_servive與DNS最大的不一樣在於它能把新的地址信息推送給客戶端。
  • gethostbyname()和getaddrinfo()解析DNS是阻塞的(除非使用UDNS等異步DNS庫),在大規模分佈式系統中DNS的做用不大,寧願花時間實現一個naming service,併爲它編寫name resolve library.

C++編譯連接模型精要

  • C++語言的三大約束: 與C兼容、零開銷(zero overhead)原則、值語義。
  • 查看編譯時打開的文件命令: strace -f -e open cpp hello.cc -o /dev/null 2>&1 |grep -v ENONT|awk {'print $3'}
  • C++也繼承了單遍編譯的約束,Java編譯器不受單遍編譯的約束,調整成員函數的順序不會影響代碼語義。
  • 按照C++模板的侷限話規則,編譯期會爲每個用到的類模板成員函數具現化一份實例。
  • 在如今的C++實現中,虛函數的動態調用(動態綁定、運行期決議)是經過虛函數表(vtable)進行的,每一個多態class都應該有一根vtable.
    • 定義或繼承了虛函數的對象中會有一個隱含成員:指向vtable的指針,即vptr。
    • 在構造和析構對象的時候,編譯期生成的代碼會修改這個vptr成員,這就要用到vtable的定義(使用其地址)。
  • 源碼編譯纔是王道。
  • 實用當頭,樸實爲貴,好用纔是王道。
  • 避免使用虛函數做爲庫的接口。
  • 以boost::function和boost::bind取代虛函數。
相關文章
相關標籤/搜索