HandlerSocket簡介以及php使用handlersocket

HandlerSocket


作者:Eugene ,發佈於2012-11-9

 

目錄:

HandlerSocket的原理

HandlerSocket的優勢和缺陷闡述

HandlerSocket的性能測試

HandlerSocket的原理

HandlerSocket的應用場景:

MySQL自身的侷限性,很多站點都採用了MySQL+Memcached的經典架構,甚至一些網站放棄MySQL而採用NoSQL產品,比如Redis/MongoDB等。不可否認,在做一些簡單查詢(尤其是PK查詢)的時候,很多NoSQL產品比MySQL要快很多,而且前臺網站上的80%以上查詢都是簡潔的查詢業務。

MySQL通過HandlerSocket插件提供了API訪問接口,在我們的基準測試中,普通的R510服務器單實例Percona/XtraDB達到了72W+QPS(純讀),如果採用更強勁的CPU增加更多的網卡,理論上可以獲得更高的性能。而同等條件下Memcached僅有40W+QPS(純讀),並且在R510上Memcached單實例已經無法提升性能,因爲Memcached對內存的一把大鎖限制了它的併發能力。

HandlerSocket原理:

MySQL的架構是“數據庫管理”和“數據管理”分離,即MySQL Server+Storage Engine的模式。MySQL Server是直接與Client交互的一層,它負責管理連接線程,解析SQL生成執行計劃,管理和實現視圖、觸發器、存儲過程等這些與具體數據操作管理無關的事情,通過調用Handler API讓存儲引擎去操作具體的數據。Storage Engine通過繼承實現Handler API的函數,負責直接與數據交互,數據存取實現(必須實現),事務實現(可選),索引實現(可選),數據緩存實現(可選)。

(圖1-1 MySQL架構)

HandlerSocket是在MySQL的內部組件,以MySQL Daemon Plugin的形式提供類似NoSQL的網絡服務,它並不直接處理數據,只是偵聽配置好的某個端口方式,接收採用NoSQL/API的通訊協議,然後通過MySQL內部的Handler API來調用存儲引擎(例如InnoDB)處理數據。理論上,HanderSocket可以處理各種MySQL存儲引擎,但是用MyISAM時,會出現插入的數據查不出來,這個實際上是構造行時第一字節沒有初始化爲0xff,初始化以後就沒有問題,MyISAM也一樣可以支持,但是爲了更好地利用內存,用HandlerSocket都會搭配InnoDB存儲引擎一起使用。

圖1-2描述HandlerSocket具體做了哪些事情:

(圖1-2 HandlerSocket原理)

因爲HandlerSocket是以MySQL Daemon Plugin形式存在,所以在應用中,可把MySQL當NoSQL使用。它最大的功能是實現了與存儲引擎交互,比如InnoDB,而這不需要任何SQL方面的初始化開銷。訪問MySQL的TABLE時,當然也是需要open/close table的,但是它並不是每次都去open/close table,因爲它會將以前訪問過的table cache保存下來以重複使用,而opening/closing tables是最耗資源的,而且很容易引起互斥量的爭奪,這樣一來,對於提高性能非常有效。在流量變小時,HandlerSocket會close tables,所以它一般不會阻塞DDL。

HandlerSocket與MySQL+Memcached的區別在哪呢?對比圖1-2和圖1-3,可從中看出其不同點,圖1-3展示了典型的MySQL+Memecached的應用架構。因爲Memcached的get操作比MySQL的內存中或磁盤上的主鍵查詢要快很多,所以Memcached用於緩存數據庫記錄。若是HandlerSocket的查詢速度和相應時間能與Memcached媲美,我們就可以考慮替換Memcached緩存記錄的架構層。

(圖1-3 典型MySQL+Memcached架構)

HandlerSocket的優勢和缺陷闡述

HandlerSocket的優勢和特點:

1) 支持多種查詢模式

HandlerSocket目前支持索引查詢(主鍵索引和非主鍵的普通索引均可),索引範圍掃描,LIMIT子句,也即支持增加、刪除、修改、查詢完整功能,但還不支持無法使用任何索引的操作。另外支持execute_multi() 一次網絡傳輸多個Query請求,節省網絡傳輸時間。

2) 處理大量併發連接

HandlerSocket的連接是輕量級的,因爲HandlerSocket採用epoll() 和worker-thread/thread-pooling架構,而MySQL內部線程的數量是有限的(可以由my.cnf中的handlersocket_threads/handlersocket_threads_wr參數控制),所以即使建立上千萬的網絡連接到HandlerSocket,也不會消耗很多內存,它的穩定性不會受到任何影響(消耗太多的內存,會造成巨大的互斥競爭等其他問題,如bug#26590,bug#33948,bug#49169)。

3) 優秀的性能

HandlerSocket的性能見文章HandlerSocket的性能測試報告描述,相對於其它NoSQL產品,性能表現一點也不遜色,它不僅沒有調用與SQL相關的函數,還優化了網絡/併發相關的問題:

(1). 更小的網絡數據包:和傳統 MySQL 協議相比,HandlerSocket 協議更簡短,因此整個網絡的流量更小。

(2). 運行有限的MySQL內部線程數:參考上面的內容。

(3). 將客戶端請求分組:當大量的併發請求到達HandlerSocket時,每個工作線程儘可能多地聚集請求,然後同時執行聚集起來的請求和返回結果。這樣,通過犧牲一點響應時間,而大大地提高性能。例如,可以減少fsync()調用的次數,減少複製延遲。

4) 無重複緩存

當使用Memcached緩存MySQL/InnoDB記錄時,在Memcached和InnoDB Buffer Pool中均緩存了這些記錄,因此效率非常低(實際上有兩份數據,Memcached本身可能還需要做HA支持),而採用 HandlerSocket插件, 它直接訪問 InnoDB 存儲引擎,記錄緩存在InnoDB Buffer Pool,於是其它SQL語句還可以重複使用緩存的數據。

5) 無數據不一致的現象

由於數據只存儲在一個地方(InnoDB存儲引擎緩存區內),不像使用Memcached時,需要在Memcached和MySQL之間維護數據一致性。

6) 崩潰安全

後端存儲是InnoDB引擎,支持事務的ACID特性,能確保事務的安全性,即使設置innodb_flush_log_at_trx_commit=2,若數據庫服務器崩潰時,也只會丟掉<= 1s的數據。

7) SQL/NOSQL並存

在許多情況下,我們仍然希望使用SQL(例如複雜的報表查詢),而大多數NoSQL產品都不支持SQL接口,HandlerSocket僅僅是一個 MySQL 插件,我們依然可以通過MySQL客戶端發送SQL語句,但當需要高吞吐量和快速響應時,則使用 HandlerSocket。

8) 繼承MySQL的功能

因爲HandlerSocket運行於MySQL,因此所有MySQL的功能依然被支持,例如:SQL、在線備份、複製、HA、監控等等。

9) 不需要修改/重建MySQL

因爲HandlerSocket是一個插件並且開源,所以它支持從任何MySQL源碼、甚至是第三方版本(例如Percona)構建,而無需對MySQL做出任何修改。

10) 獨立於存儲引擎

雖然我們只測試了MySQL-EnterpriseInnoDB和Percona XtraDB插件,但HandlerSocket理論上可以和任何存儲引擎交互。MyISAM通過簡單的修改也是可以被支持的,但是從數據緩存而利用內存的角度看這個意義不大。

HandlerSocket的缺陷和注意事項

1) 協議不兼容

HandlerSocket API與Memcached API並不兼容,儘管它很容易使用,但仍然需要一點學習來學會如何與HandlerSocket交互。不過我們可以通過重載Memecached函數來翻譯到HandlerSocket API。

2) 沒有安全功能

與其它NoSQL數據庫類似,HandlerSocket不支持安全功能,HandlerSocket的工作線程以系統用戶權限運行,因此應用程序可以通過HandlerSocket協議訪問所有的表對象,但是可以通過簡單的修改協議,在my.cnf中增加一個配置項爲密碼,連接時通過這個配置的密碼驗證,當然也可以通過網絡防火牆來過濾數據包。

3) 對於磁盤IO密集的場景沒有優勢

對於IO密集的應用場景,數據庫每秒無法執行數千次查詢,通常只有1-10%的CPU利用率,在這種情況下,SQL解析不會成爲性能瓶頸,因此使用HandlerSocket沒有什麼優勢,應當只在數據完全裝載到內存的服務器上使用 HandlerSocket。但是對於PCI-E SSD(例如Fusion-IO)設備,每秒可以提供4w+ IOPS,並且IO設備本身消耗CPU比較大,使用HandlerSocket依然具有優勢。

HandlerSocket的性能測試

HandlerSocket Oprofile測試報告

(MySQL通過SQL執行K/V查詢的Oprofile信息)

MySQL執行SQL語句,首先要經過SQL解析階段,調用MYSQLparse() 和MYSQLlex() 進行語法和詞法解析;然後進入查詢優化階段,調用make_join_statistics() 和JOIN::optimize() 獲得統計信息和生成執行計劃,可以清洗第發現,主要耗資源的是SQL解析和優化層,而不是InnoDB存儲層,row_search_for_mysql只消耗了很少的時間。

因此我們對比Memcached/NoSQL,知道MySQL除了數據操作,還要很多額外的步驟需要完成:

1 Parsing SQL statements【解析SQL】

2 Opening, locking tables【打開並鎖定表】

3 Making SQL execution plans SQL【解析SQL並生成執行計劃】

4 Unlocking, closing tables【解鎖並關閉表】

另外,MySQL 還必須要做大量的併發控制,比如在發送/接收網絡數據包的時候,fcntl() 就要被調用很多次;Global mutexes比如LOCK_open,LOCK_thread_count也被頻繁地取得/釋放。所以在Oprofile的輸出中,排在第二位的是my_pthread_fastmutex_lock()。並且Mutex的競爭帶來的上下文切換,導致%system佔用CPU使用比例相當高(>20%)。

其實, MySQL 開發團隊和外圍的開發團體早已意識到大量併發控制對性能的影響,MySQL 5.5中已經解決了一些問題,Percona也對Mutex做了一些拆分處理,未來的MySQL版本中,也應該會越來越好。

在完全內存操作的情況時,CPU的效率非常重要。如果只有一小部分數據進入內存,那麼SQL語句帶來的消耗可以忽略不計。很簡單,因爲機械磁盤IO操作的時間消耗遠比CPU解析SQL語句的時間消耗多,這種情況下,就不需要過分考慮SQL語句所帶來的消耗。但是對於SSD盤,尤其是PCI-E SSD盤,響應時間在微秒級(Fusion I/O爲30us左右),就必須考慮SQL帶來的消耗了。

在大多數的MySQL 服務器中,大部分的熱點數據都緩存在內存中,因而訪問變得只受CPU的限制。Profiling 的結果就類似上所述的情況:SQL 層消耗了大量的資源。假設需要做大量的PK查詢(例如:SELECT x FROM t WHERE id=?)或者是做LIMIT的範圍查詢,即使有70-80%都是在同一張表中做PK查詢(僅僅只是查詢條件中給定的值不同,即value不同而已), MySQL 還是每次需要去做 parse/open/lock/unlock/close, 這對我們來說是非常影響效率的事情。

(MySQL通過HandlerSocket執行K/V查詢的Oprofile信息)

HandlerSocket性能測試報告:

【測試主機】

機型:R510

CPU:Intel(R) Xeon(R) CPU E5520 @ 2.27GHz

內存:4G*6

磁盤:146G*2(OS) + 300G*12 RAID10(data)

1) 完全隨機測試

測試場景描述:

單實例MySQL 5.1.48 InnoDB Plugin

測試SQL:INSERT INTO table (key, value) VALUES(#key#, #value#) / SELECT value FROM table WHERE key=#key#

HS API:execute_single

2) 重複獲取同一條數據

測試場景描述:

1 單實例Percona 5.1.57-12.8 XtraDB

2 測試SQL:SELECT value FROM table WHERE key=#key#

3 HS API:execute_single

4 MC API:get

轉自:http://www.uml.org.cn/sjjm/201211093.asp


php使用handlersocket詳解

有關於 HandlerSocket 的介紹、性能及其安裝,可參考Using SQL as NoSQL。而 PHP extension for interfacing with MySQL Handler Socket,實際上這裏php-handlersocket有整體的介紹,包括其安裝、使用方法。現在純粹是因爲自己測試時犯了一很基礎的錯誤,所以,罰自己多敲點字。
安裝
  
[root@localhost php-handlersocket]# /usr/local/php/bin/phpize
[root@localhost php-handlersocket]# ./configure --with-php-config=/usr/local/php/bin/php-config
[root@localhost php-handlersocket]# make
[root@localhost php-handlersocket]# make install
說明:
1 編譯時需要 libhsclient 庫(libhsclient – HandlerSocket client library)。
2 安裝成功時,在 PHP 的 extension dir 生成一名爲 handlersocket.so,將extension=handlersocket.so加入 php.ini, 重啓 PHP 服務。
HandlerSocket Class methods
HandlerSocket::construct
創建一 HandlerSocket Object。
  
HandlerSocket::__construct ( string $host, string $port [,    array $options ] )
參數:

        $host MySQL 服務器 host name。
        $port HandlerSocket 的端口地址。

返回值:
返回 HandlerSocket Object。
HandlerSocket::openIndex
在對數據庫表做任何的增刪改查操作前,必須先選擇一索引。
  
public bool HandlerSocket::openIndex ( int $id, string $db, string $table, string $index, string $fields )
參數:

        $id HandlerSocket ID; 1 SELECT, 2 UPDATE, 3 INSERT, 4 DELETE。
        $db 數據庫名
        $table 表名
        $index 索引名, 可以是手動創建的索引名。這個參數可爲空,一般指定時是用於 SELECT,eg: 指定爲主鍵:HandlerSocket::PRIMARY
        $fields 字段名(多個字段名,用逗號分隔),可爲空。

返回值:
成功時返回 TRUE, 反之亦然。
HandlerSocket::executeSingle
在表上做增刪改查操作。
  
public mixed HandlerSocket::executeSingle ( int $id, string $op, array $fields [, int $limit, int $skip, string $modop, array $values, array $filters, int $invalues_key, array $invalues ] )
參數:

        $id HandlerSocket ID; 1 SELECT, 2 UPDATE, 3 INSERT, 4 DELETE。
        $op 操作符,有如下可選項, ‘=’, ‘>=’, ‘<=’, ‘>’, ‘<’, ‘+’。
        $fields 查詢中所用到的字段,數組,其長度必須等於或小於指定的列數。
        $limit 最多影響的行數(最開始根據這個函數名稱有在懷疑這個參數,測試時發現,如果存在滿足條件的多條記錄時,會根據這個參數指定的值返回記錄數)。
        $skip 在檢索記錄前忽略掉的行數。
        $modop 指定修改操作,可選值:’U', ‘D’。
        $values 數組,用於做 UPDATE 操作時指定修改的值。
        $filters 過濾的選項。
        $invalues_key ? (enabled : 0 / disabled : -1).
        $invalues IN options

返回值:
返回做對應操作時的執行結果。
HandlerSocket::executeMulti
在一次調用中執行多個操作,即多個 HandlerSocket::executeSingle 的合併。
  
public mixed HandlerSocket::executeMulti ( array $requests )
參數:

        $requrest 多組 executeSingle 參數,用數組的形式體現。

注意:
等同於:HandlerSocket::executeSingle($requests00, $requests01, ...), HandlerSocket::executeSingle($requests10, ...) ...。
返回結果:
返回做對應操作時的執行結果。
HandlerSocket::executeUpdate
To update a record from a table using an index.
  
public mixed HandlerSocket::executeUpdate ( int $id, string$op, array $fields, array $values [, int $limit, int $skip, array $filters, int $invalues_key, array $invalues ] )
參數:

        $id HandlerSocket ID; 2 UPDATE 。
        $op 操作符,有如下可選項, ‘=’, ‘>=’, ‘<=’, ‘>’, ‘<’, ‘+’。
        $fields 查詢中所用到的字段,數組,其長度必須等於或小於指定的列數。
        $values UPDAET 時指定修改的值。
        $limit 最多影響的行數。
        $skip 在檢索記錄前忽略掉的行數。
        $filters 過濾的選項。
        $invalues_key ? (enabled : 0 / disabled : -1).
        $invalues IN options

注意:
等同於:HandlerSocket::executeSingle($id, $op, $fields, $limit, $skip, 'U', $values, $filters, $invalues_key, $invalues)。
返回值:
返回做對應操作時的執行結果。
HandlerSocket::executeDelete
To delete a record from a table using an index.
  
public mixed HandlerSocket::executeDelete ( int $id, string $op, array $fields [, int $limit, int $skip, array $filters, int $invalues_key, array $invalues ] )
參數:

        $id HandlerSocket ID; 4 DELETE 。
        $op 操作符,有如下可選項, ‘=’, ‘>=’, ‘<=’, ‘>’, ‘<’, ‘+’。
        $fields 查詢中所用到的字段,數組,其長度必須等於或小於指定的列數。
        $limit 最多影響的行數。
        $skip 在檢索記錄前忽略掉的行數。
        $filters 過濾的選項。
        $invalues_key ? (enabled : 0 / disabled : -1).
        $invalues IN options

注意:
等同於:HandlerSocket::executeSingle($id, $op, $fields, $limit, $skip, 'D', NULL, $filters, $invalues_key, $invalues)。
返回值:
返回做對應操作時的執行結果。
HandlerSocket::executeInsert
To insert a record from a table using an index.
  
public mixed HandlerSocket::executeInsert ( int $id, array $values )
參數:

        $id HandlerSocket ID; 3 INSERT 。
        $values HandlerSocket::openIndex 指定的字段參數所對應的值,但是以數組的形式體現。

注意:
等同於:HandlerSocket::executeSingle($id, '+', $values, 0, 0, NULL, NULL, NULL) ,第三個參數中指定的值必須和在此之前調用 HandlerSocket::openIndex 時第五個參數指定的字段對應。
返回值:
返回做對應操作時的執行結果。
HandlerSocket::getError
取得最近一次的錯誤信息。
  
public string HandlerSocket::getError ( void )
返回值:
返回最近的錯誤信息(時間上)。

Example

測試表 schema:

    
CREATE TABLE `hstesttbl` (        `id` int(11) NOT NULL AUTO_INCREMENT,    
        `k` char(6) DEFAULT NULL,
        `v` char(6) DEFAULT NULL,
        PRIMARY KEY (`id`),
        KEY `idx_hstesttbl_k` (`k`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;



    
$host                         = 'localhost';
$port                         = 9998;
$port_wr                = 9999;
$dbname                 = 'hstestdb';
$table                        = 'hstesttbl';

//GET
$hs = new HandlerSocket($host, $port);
if (!($hs->openIndex(1, $dbname, $table, HandlerSocket::PRIMARY, 'k,v'))) {
                echo $hs->getError(), PHP_EOL;
                die();
}

$retval = $hs->executeSingle(1, '=', array('k1'), 1, 0);
var_dump($retval);

$retval = $hs->executeMulti(
                array(
                                array(1, '=', array('k1'), 1, 0),
                                array(1, '=', array('k2'), 1, 0)
                )
);
var_dump($retval);
unset($hs);

//UPDATE
$hs = new HandlerSocket($host, $port_wr);
if (!($hs->openIndex(2, $dbname, $table, '', 'v'))) {
                echo $hs->getError(), PHP_EOL;
                die();
}

if ($hs->executeUpdate(2, '=', array('k1'), array('V1'), 1, 0) === false) {
                echo $hs->getError(), PHP_EOL;
                die();
}

unset($hs);

//INSERT
$hs = new HandlerSocket($host, $port_wr);
if (!($hs->openIndex(3, $dbname, $table, '', 'k,v'))) {
                echo $hs->getError(), PHP_EOL;
                die();
}

if ($hs->executeInsert(3, array('k2', 'v2')) === false) {
                echo $hs->getError(), PHP_EOL;
}
if ($hs->executeInsert(3, array('k3', 'v3')) === false) {
                echo 'A', $hs->getError(), PHP_EOL;
}
if ($hs->executeInsert(3, array('k4', 'v4')) === false) {
                echo 'B', $hs->getError(), PHP_EOL;
}

unset($hs);

//DELETE
$hs = new HandlerSocket($host, $port_wr);
if (!($hs->openIndex(4, $dbname, $table, '', ''))) {
                echo $hs->getError(), PHP_EOL;
                die();
}

if ($hs->executeDelete(4, '=', array('k2')) === false) {
                echo $hs->getError(), PHP_EOL;
                die();
}
  
PS: 因爲建立測試表時忘記指定存儲引擎爲 InnoDB, 測試 INSERT 操作時,怎樣都是失敗。後面爲了驗證問題的出處,用 perl 的 API 做同樣的測試操作,結果也是失敗。查看錶結構後,修改儲存引擎爲 InnoDB,才成功。只是這個問題的錯誤信息太難理解,就幾個數字,在沒找到答案之前,害我還去查看了下 HandlerSocket 的源代碼,當然,沒有從中得到任何的提示。
轉自http://flandycheng.blog.51cto.com/855176/628776/
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章