起因
我看 ClickHouse 有 C++ 客戶端(clickhouse-cpp),我又用過 PHP-CPP 寫擴充套件,於是就在國慶寫了 OrzClick ,一個 PHP 用的 ClickHouse 客戶端。
比較尷尬的是,我寫到一半才發現 SeasClick,它也是 clickhouse-cpp 的繫結, 而且是 C 寫的,感覺用 PHP-CPP 我就已經輸了一半呀,所以我的小目標就是效能超越 SeasClick。
效能測試
Select 結果:
- 使用 PDO 訪問 ClickHouse 的 MySQL 介面,查詢小量資料效能更好
- 小量資料時,OrzClick 和 SeasClick 效能相近,資料大時 OrzClick > SeasClick > MySQL 介面
Insert 結果:
- OrzClick-Indexed 對標的是 SeasClick,API 最相近(可看程式碼:1 2),算是達到了小目標
- SeasClick 和 OrzClick 都有提高插入效能的 API,SeasClick 的 startWrite-write-endWrite 效能非常好(圖上的 SeasClick-Block),OrzClick 的 InsertColumnar 只有資料量大於 5 千時才能超過它(圖上的 OrzClick-Columnar)
哪個 clickhouse-cpp ?
在 Github 搜尋 clickhouse-cpp, 你會發現有兩個相似的庫:
看 LICENSE 和開發人員的評論,可以得知 ClickHouse 官方的才是 fork。簡單對比了一下程式碼,兩者底層還是一樣的,只是功能特性有一點小小區別。
OrzClick 使用的是 ClickHouse/clickhouse-cpp 的 fork,而 SeasClick 是 artpaul/clickhouse-cpp 的 fork,所以大家還是同源的,效能差異就體現在使用方式和補丁了。
SeasClick 的優化
clickhouse-cpp 的資料插入介面非常簡單,就一個入口方法:
void Insert(const std::string& table_name, const Block& block);
而 SeasClick 把它拆分成:
void InsertQuery(const std::string& query, SelectCallback cb);
void InsertData(const Block& block);
void InsertDataEnd();
這個拆分對效能提升、擴充套件實現有很大幫助:
InsertQuery
可以拿到欄位的型別資訊,可以簡化 PHP 介面的使用,不像 OrzClick 一樣需要使用者指定欄位型別InsertQuery
+ 多次InsertData
+InsertDataEnd
可以實現連續插入,效能提升巨大(見圖上的 SeasClick-Block)
OrzClick 的優化
資料訪問模式
ClickHouse 是個列式儲存的資料庫,而它的介面也使用了同樣的設計,一次 select 會返回多個 Block,Block 裡有多個 Column,一個 Column 裡的資料是連續存放的,Column 間是相互獨立的。
應用層使用資料還是按行為主,所以這裡要重新組織一下資料,把列式資料轉成行式資料。 SeasClick 是按行處理,而 OrzClick 是按列處理,這是兩者的主要區別之一。
SeasClick 遍歷模式
Block
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Column A Column B Column C ┃
┃ ┃
┃ ┏━━━━━━━━━┓ ┏━━━━━━━━━┓ ┏━━━━━━━━━┓ ┃
Seas─╮──┃───>┃ 1 ┃──>┃ X ┃──>┃ 1.2 ┃ ┃
│ ┃ ┣━━━━━━━━━┫ ┣━━━━━━━━━┫ ┣━━━━━━━━━┫ ┃
╰──┃───>┃ 2 ┃──>┃ Y ┃──>┃ 2.3 ┃ ┃
┃ ┣━━━━━━━━━┫ ┣━━━━━━━━━┫ ┣━━━━━━━━━┫ ┃
┃ ┃ 3 ┃ ┃ Z ┃ ┃ 3.4 ┃ ┃
┃ ╏ ╏ ╏ ╏ ╏ ╏ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
OrzClick 遍歷模式
Block
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Column A Column B Column C ┃
╭───────────────────╮─────────────╮ ┃
Orz ─╯──┃─╮ ┏━━━━━━━━━┓ │ ┏━━━━━━━━━┓ │ ┏━━━━━━━━━┓ ┃
┃ │ ┃ 1 ┃ │ ┃ X ┃ │ ┃ 1.2 ┃ ┃
┃ │ ┣━━━━━━━━━┫ │ ┣━━━━━━━━━┫ │ ┣━━━━━━━━━┫ ┃
┃ │ ┃ 2 ┃ │ ┃ Y ┃ │ ┃ 2.3 ┃ ┃
┃ │ ┣━━━━━━━━━┫ │ ┣━━━━━━━━━┫ │ ┣━━━━━━━━━┫ ┃
┃ V ┃ 3 ┃ V ┃ Z ┃ V ┃ 3.4 ┃ ┃
┃ ╏ ╏ ╏ ╏ ╏ ╏ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
SeasClick 的實現類似這樣:
for (auto i = 0; i < block.GetRowCount(); i++) { // 外層按行遍歷
for (auto j = 0; j < block.GetColumnCount(); j++) { // 行內再按列遍歷
switch (block[i]->GetType().GetCode()) { // 每一列型別都不同,要相應處理
case clickhouse::Type::Int8:
add_assoc_long_ex(result, key, len, block[i]->As<clickhouse::ColumnInt8>()->At(j));
break;
case ...// 其他型別類似
}
}
}
OrzClick 的實現類似這樣:
for (auto i = 0; i < block.GetColumnCount(); i++) { // 外層按列遍歷
switch (block[i]->GetType().GetCode()) { // 每一列型別都不同,要相應處理
case clickhouse::Type::Int8:
auto col = block[i]->As<clickhouse::ColumnInt8>();
for (auto j = 0; j < block.GetRowCount(); j++) { // 列內再按行遍歷
add_assoc_long_ex(result, key, col->At(j));
}
break;
case ...// 其他型別類似
}
}
對比一下可以看到 SeasClick 的內層迴圈會有大量的 switch 分支跳轉,而 OrzClick
在外層判斷了型別,內層迴圈非常緊湊,沒有多餘的分支。
用 perf stat 分析一下,SeasClick 分支數(branches)、分支預測錯誤數(branch-misses)都在 OrzClick 的 2 倍以上:
# perf stat php select-orzclick.php 1000 1000
Performance counter stats for 'php select-orzclick.php 1000 1000':
496.85 msec task-clock:u # 0.340 CPUs utilized
0 context-switches:u # 0.000 K/sec
0 cpu-migrations:u # 0.000 K/sec
1,977 page-faults:u # 0.004 M/sec
1,761,248,425 cycles:u # 3.545 GHz
2,601,973,475 instructions:u # 1.48 insn per cycle
487,402,260 branches:u # 980.986 M/sec
2,879,008 branch-misses:u # 0.59% of all branches
# perf stat php select-seasclick.php 1000 1000
Performance counter stats for 'php select-seasclick.php 1000 1000':
896.48 msec task-clock:u # 0.482 CPUs utilized
0 context-switches:u # 0.000 K/sec
0 cpu-migrations:u # 0.000 K/sec
1,962 page-faults:u # 0.002 M/sec
3,316,728,038 cycles:u # 3.700 GHz
6,019,365,862 instructions:u # 1.81 insn per cycle
1,316,036,409 branches:u # 1468.000 M/sec (2.7x)
10,073,424 branch-misses:u # 0.77% of all branches (3.4x)
所以在 select 測試中,資料量少的時候 OrzClick 只比 SeasClick 略好,但資料量大了效能差距就拉開了。
當然也有退化到 OrzClick 不利的情況,就是 ClickHouse 返回多個Block,但每個 Block 都只有一行,目前只發現 Memory 引擎有這種情況。
TCP_NODELAY
在測試的時候,發現少量資料反而更慢,就是一位元組的區別:
$ time php insert-orzclick.php 8170 100
real 0m3.894s
user 0m0.030s
sys 0m0.061s
$ time php insert-orzclick.php 8171 100
real 0m0.422s
user 0m0.050s
sys 0m0.022s
看 ClickHouse 日誌,處理少量資料反而時多用了 40ms 左右的時間(大佬們看到 40 ms 就大概猜到了吧)。
對比兩者的火焰圖,雖然執行的總時間不同,但是各種函式的比例是接近的, 大頭都是 _zend_hash_find_known_hash
:
難道問題真在 PHP?我移除掉 clickhouse-cpp 的呼叫,發現兩種情況執行時間基本相同,這也就排除掉 PHP 的可能性,問題應該出在 clickhouse-cpp。
再用 strace 跟蹤,發現資料少的時候是隻有一個 send 系統呼叫,多的時候會分成兩個:
# 8170
sendto(3, "\2\0\1\0\2\377\377\377\377\0\1\352?\2u8"..., 8192, MSG_NOSIGNAL, NULL, 0) = 8192
# 8171
sendto(3, "\2\0\1\0\2\377\377\377\377\0\1\353?\2u8"..., 22, MSG_NOSIGNAL, NULL, 0) = 22
sendto(3, "\1\2\3\4\5\6\7\10\t\n\v\f\r\16\17\20"..., 8171, MSG_NOSIGNAL, NULL, 0) = 8171
8170 和 8171 這個臨界點,發現和 clickhouse-cpp 的緩衝區大小 8192 很接近。於是我試著調整 clickhouse-cpp 緩衝區大小,的確會影響 send 的次數,但只是臨界點有點變化,不能解決問題。
至此基本可以確定是核心和協議棧的影響,於是想有那些配置可能影響傳送、 接收延遲,然後就想到了 TCP_NODELAY
,於是我提了個 PR,給 clickhouse-cpp 加上了 TCP_NODELAY
選項,測試效能終於穩定了。
後來我又嘗試用 Off-CPU 火焰圖,只能看到在 recv 時有等待,還不能直接看出原因,這種問題沒經驗真不易處理(雖然搜尋 TCP 40ms
就有結果)。
PHP-CPP 損耗
PHP-CPP 封裝了 Zend API,開發擴充套件基本可以不考慮 Zend 引擎低層(zval、HashTable 等等),非常方便,代價就是更多額外操作和效能損耗。
優化方式非常暴力,直接修改 PHP-CPP,暴露出被封裝的 zval,然後直接用 Zend API 操作。過程就是先用 PHP-CPP 寫,然後用火焰圖發現熱點,然後替換成 Zend API。
例如在 nestedForeach
方法裡,需要獲取陣列的值,如果用 PHP-CPP 的 Value::get()
最後會複製一次:
Value::Value(struct _zval_struct *val, bool ref)
{
// do we have to force a reference?
if (!ref)
{
// we don't, simply duplicate the value
ZVAL_DUP(_val, val);
}
批量插入的時候,就會有不必要的陣列複製。所以這裡改成 zend_hash_find
拿到 *zval
,然後直接遍歷:
zval *item;
auto column = zend_hash_find(Z_ARRVAL_P(data._val), key);
auto ht = Z_ARRVAL_P(column);
ZEND_HASH_FOREACH_VAL(ht, item) {
callback(item);
}
ZEND_HASH_FOREACH_END();
結束語
國慶假期通過這個專案,每樣學到了一點點:
- ClickHouse
- PHP 擴充套件開發
- C++
- CMake
- 效能優化
也有沒做好的:
- 單元測試,本來想用 phpt,但沒寫,目前在 tests 目錄有幾個我開發時用的用例子
- CI,準備試試 GitHub Action
最後,從 OrzClick 這名字你就應該知道,這是出於玩和學習的目的寫的,生產環境還是建議用 SeasClick。
本作品採用《CC 協議》,轉載必須註明作者和本文連結