key-value 多線程server的Linux C++實現

項目需求

設計一個基於Socket或基於HTTP的server,服務內容是提供一種簡單的key/value映射關係的管 理與查詢
如下的所有操做都是經過結構體Node來傳遞的:
struct Node {
char key[KEY_SIZE];
char value[VALUE_SIZE];
};
本場景中需要client和server兩個程序
client端僅僅有兩種操做:
int AddNode(const struct Node *node); // 將指定的node保存到server上,需要key和value都 完整
int GetNode(struct Node *node); // 輸入的node需要有完整的key。server負責將這個key相應 的value填到node中。或返回不存在html

server: server端就是接收client的兩種請求,而後要麼保存node要麼查詢node並返回值。
server端怎麼保存node不做要求,但要達到如下的幾點:
1. 假設將所有node都保存在內存中。那麼當內存中的數據太多時,需要按期將node保存成磁 盤文件。
2. client端在運行AddCell時,假設相應的key已經存在於server端。那麼覆蓋原有的值。node

1. 總體思路

首先咱們要開發一個基於網絡的server,那麼網絡的知識是必須的,咱們要開發的server是一種要求端對端的可靠的server。所以將採用基於TCP協議和套接字Socket做爲服務端和client之間的通訊方式。進,而咱們可以觀察到咱們操做的數據是一個(key,value)形式。而TCP是一個以字節流傳輸的鏈接,所以需要一個字符串(char*)和數據結構(key,value)之間的相互轉換函數。來將服務端和client之間的傳輸字節流解析成命令和數據節點(以及將命令和數據節點打包成字符串進行通訊)。而後咱們必須考慮server上數據的存儲和查詢問題,這是項目實現的關鍵。最後,server確定要支持多client的同一時候鏈接和通訊,所以還必須增長多線程或多進程。
本項目已實現的功能linux

  • 基於Socket的TCP網絡傳輸
  • 對網絡傳輸字符的解析與打包
  • LRU Page 緩存
  • Hash-map 查找
  • 多線程多clients 同一時候put。get操做

如下將從四個方面進行闡述項目思想:網絡通訊,字符解析。數據存儲與查詢,多線程實現。git

2. 網絡通訊

基於本server的功能特性考慮。咱們要求的是可靠性,所以選擇TCP。
而在TCP的鏈接前。
- server首先要作的準備是:建立一個鏈接套接字socket。而後socket綁定(bind)在一個IP和Port上以供client尋找。監聽(listen)client的請求,而後accept一直堵塞等待client的鏈接。github


- client需要作的準備:client的套接字附上服務端的ip地址和本身的port號。
當服務端和client作好通訊準備後,由client發起,服務端響應。通過三次握手,創建相互之間的鏈接。
當一個client通訊完畢後, client會主動向服務端請求釋放鏈接。算法


關於三次握手以及當一個client通訊完畢,與服務端釋放鏈接時的四次揮手的具體知識,請參考wireshark抓包圖解 TCP三次握手/四次揮手具體解釋編程

創建鏈接後,服務端調用accept()函數。堵塞服務端進程。直至收到clientsend()過來的信息。
關於網絡編程中的Socket接口函數的具體解說。參考Linux的SOCKET編程具體解釋緩存

3. 字符解析

由於網絡通訊中傳輸的是字符,咱們必須將字符解析成 命令(put,get,…etc.)和數據(key,value)。
由上述得知,傳輸字符包括有:命令,key,value。咱們可依據習慣。設立切割字符,如空格,分號等。markdown

需要注意的是每次傳輸的字符串切割後,獲得的子字符串的個數多是不一樣的,如」put 12:quinn」 分爲3段,而」get 12」分爲2段,」exit」僅僅有一段。網絡

所以依據client發信給client的命令不一樣,字符解析函數將它解析成不一樣的命令。

而服務端首先推斷第一段字符的含義(put,get,exit,save等)。來決定本身要實現的動做。


源代碼查看:https://github.com/qzxin/key-value-server/blob/master/convert.cpp

4. 數據存儲與查詢

4.1 存儲管理

由於內存空間有限。當數據量到達上限時,必須把數據轉存到磁盤文件裏。
在server實現過程當中,服務端需要接受client的get和put兩種操做,
- put(key, value): 在接收必定數量的數據後需要將數據保存到磁盤上,並且需要檢查是否存在一樣的key。
- get(key): 向服務端查詢是否存在該key;

由於項目需求一樣的key僅僅能有一個值,因此不管是put和get都必須遍歷內存和磁盤文件裏的數據,查找是否含有該key。假設每一次操做都需要訪問磁盤,那麼效率將是極低的,由於訪問數據的時間局部性,近期訪問過的數據在近期內有更大的可能再次被訪問,所以想到了引入內存緩存系統,即將近期訪問過的節點保留在內存中。又由訪問數據的空間局部性,近期訪問過的數據周圍的數據有更大的可能被訪問,想到了引入分頁機制。

  • 緩存系統:當查找節點時,首先在緩存中查找。查找成功則對該節點操做。

    緩存查找失敗。即缺頁中斷。由於內存空間限制。緩存的大小是必定的,當發生缺頁中斷時,要從磁盤中載入新數據,即需要不斷的用近期訪問成功的新數據替換緩存中的舊數據。

  • 分頁機制:當待查找數據不在緩存,即缺頁中斷時,假設每次都從磁盤中載入一個數據,那效率是不可接受的。所以,將頁(包括N個數據節點)做爲緩存和磁盤數據之間操做的基本單位。

如上提到的缺頁中斷是操做系統中內存管理中的概念。

在請求分頁存儲管理系統中。由於使用了虛擬存儲管理技術,使得所有的進程頁面不是一次性地所有調入內存,而是部分頁面裝入。
這就有可能出現如下的狀況:要訪問的頁面不在內存。這時系統產生缺頁中斷。操做系統在處理缺頁中斷時,要把所需頁面從外存調入到內存中。假設這時內存中有空暇塊,就可以直接調入該頁面。假設這時內存中沒有空暇塊,就必須先淘汰一個已經在內存中的頁面,騰出空間。再把所需的頁面裝入。即進行頁面置換。
當缺頁中斷時,需要進行頁面置換。而常見的頁面置換算法有:FIFO。LRU和時鐘算法。
(1)FIFO是淘汰內存中存在時間最長的頁,而最長的頁多是最常被訪問的。所以性能差。
(2)LRU是淘汰內存中最久沒有被訪問的頁。
(3)時鐘算法是,將頁連成一個環形鏈表,當缺頁中斷時。指針指向最老的頁,當該頁的訪問位爲0。則刪除該頁。若該頁訪問位爲1,則將訪問位置0。遍歷它的下一頁,直至遇到一個訪問位爲0的頁。用新數據替換它,並把指針指向它的下一頁。

注意,本文中假設」數據緩存「存在於內存中,即內存緩存,而」磁盤中的數據文件「模擬現代OS中的虛擬內存。即本文將緩存放在內存,將磁盤文件當作緩存頁。

本文選用easy實現且性能尚可的LRU頁面置換。具體實現過程已在還有一篇博文基於文件頁的 LRU Cache:磁盤緩存實現中具體描寫敘述,本文再也不贅述。

本文思想是,爲了更便利的對頁數據進行置換。將磁盤文件的大小設置爲頁的大小,造成映射。

當缺頁中斷時,調入新的一頁時,即讀一個新文件到內存中。而怎樣定位文件,下文分析;而被替換掉的頁。假設頁的dirty位爲1。則又一次寫入到它所屬的文件,爲了實現這一點,在頁的數據結構中應該包括該頁所屬文件的編號。(這是OS中虛擬緩存的思想

怎樣定位key所在的文件?(2015/07/19更新)
創建(key, file)映射的hash表。put操做時,將每一個新key和該key將要存入的文件序號壓入一個hash-map;get操做或put操做。search(key)時。假設該key不在緩存中,那麼查key-file映射表,假設存在該key相應的文件,則將該文件載入進緩存中,不然返回不存在該key。注意,在server啓動時,應該載入(key,file)的映射表(它們存儲在一個文件裏)。

class HashCache::Page {
public:
    int file_num_; // 頁所相應的文件序號
    bool lock_;
    bool dirty_;  // 標記page是否被改動
    class Node data_[PAGE_SIZE];  // 頁包括的節點數據
    class Page* next;
    class Page* prev;
    Page() {
        lock_ = false;
        dirty_ = false;
    }
};

4.2 數據查詢

4.1存儲管理 攻克了數據的存儲和載入問題,那麼怎樣能高速的索引到一個數據呢?兩種辦法,平衡二叉樹O(lgn)和hash表O(1);咱們知道hash表的缺點是不能有效解決衝突,而本項目中的key,value惟一。所以採用更快的hash-map實現數據的索引,固然採用時間複雜度爲O(lgn)的map實現也是可以的。


在實際操做中,當每次缺頁中斷,載入一頁時,將新頁的數據都插入到hash-map中,同一時候將被替換頁的數據從hash-map中釋放。而爲了保證這點,在構建數據結構時,每一個數據節點必須包括它所屬的頁號

class HashCache::Node {
public:
    std::string key_;
    std::string value_;
    class Page* page_;  // 該數據節點所屬頁
};

總結:由操做系統中的內存頁面置換和虛擬緩存中的理論,遷移獲得本項目server數據的存儲和查詢的實現思想。
本節思想的具體實現步驟,已再還有一篇博文中描寫敘述點擊此處查看

5. 多線程

2015/09/30 更新
對於多線程實現,由於線程的建立和銷燬耗費時間和資源,所以對於大量的短的傳輸任務可以用線程池的方式實現。

一個server確定是要支持多client通訊的。那麼應該使用多進程仍是多線程呢?
由上文可知,所有的數據都是先存儲到內存中。而後再轉存到文件裏,那麼爲了內存數據(緩存)的共享。選用多線程實現。
每當有一個client和server鏈接成功後,新建一個線程,將鏈接套接字傳入線程處理函數。而後分離(detach)該線程。由該線程處理該client的所有通訊。

由於是經過」共享內存「的方式實現線程之間的通訊,可能存在多個client同一時候針對一個key的value作改動。同一時候有client在讀取該key的value,形成數據的不一樣步?那應該怎樣解決線程同步問題呢?

線程同步的方式:臨界區。相互排斥鎖。信號量,事件

本項目,採用相互排斥鎖解決數據之間的同步問題,引入2個鎖:寫入鎖和讀取鎖。當有一個正在put時,所有的put和get操做等待;當有get操做時,可以再有get操做,put操做等待。(讀者寫者問題的經典思想)
C++多線程編程,詳情請參閱:C++11 編寫 Linux 多線程程序
C++線程信號量和鎖,詳情請參閱:C++11 併發指南三(std::mutex 具體解釋)

2015/09/30 更新
如上述所述,每一次操做緩存都要鎖定整個緩存部分,可以作出例如如下改進:使用兩個相互排斥量進行加鎖,當讀取或者寫入一個頁的數據時,對該頁進行加鎖。其它頁可以正常訪問。但是,當將剛剛操做的頁放到雙向鏈表的頭部時,需要對整個鏈表(整個緩存)進行加鎖。這樣粒度更小。效率更高。

6. 待改進。未實現的想法

  • 怎樣增長斷電緩存重建機制?
  • 怎樣增長查詢超時推斷?
  • 需不需要線程調度?
  • 是否能把所有的key全放到一個set裏,當cache中不存在該key時,去set裏查找。假設存在而後纔去遍歷文件。不存在則直接返回。

  • 是否是還可以。將key。page_num對存入一個hash-map,依據key直接索引到其所屬的頁(相應文件號)。
  • 其它參考信息:淘寶自主開發的一個分佈式key/value存儲系統Tair,開發本項目時沒有發現~~~

7. GitHub源代碼

本項目開發環境Linux GCC4.8.4 ,C++ 11
源代碼:https://github.com/qzxin/key-value-server
原文:key-value 多線程server的Linux C++實現http://blog.csdn.net/quzhongxin/article/details/46927785

相關文章
相關標籤/搜索