我和一個大學的老朋友 Fedor Dzjuba ,創立了一家名為 Signal Analytics 的技術公司。我們通過構建自己的資料庫系統來打造一個現代的、基於雲的 OLAP 資料集(多維資料儲存和檢索)。
由於我主導技術層面並且我最熟悉 C++,所以決定用它來構建我們的 OLAP 引擎。雖然我最初的確用 Rust 來構建原型,但是那樣做風險太大了(我應該另外寫一篇文章來解釋更多關於這個決定的細節)。
我的很多同行覺得很奇怪,因為我用 C++ 而沒有用一種動態的語言(像 Ruby 或者 Python 那樣具有高生產效率、使產品可以快速上市的語言)來構建一個基於雲的服務。
我開始質疑自己對使用 C++ 的判斷。於是我決定調查一下,看看使用 C++ 是好是壞。
生產效率
雖然 C++ 不是一種動態語言,但是現代的 C++(C++11/14)有型別推斷。對於使用 C++ 編寫程式碼存在很多誤解,如,我們一定要使用原始指標來編寫程式碼,一定要輸入冗長的名稱空間或者型別,一定要手動管理記憶體。C++ 使我們感到更高生產效率的一個關鍵特性是 auto 特性。我們不一定要輸入冗長的名稱空間和類名,它利用型別推斷來推斷出變數的型別。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <chrono> int main() { //here's the verbose version std::chrono::time_point<std::chrono::system_clock> start = std::chrono::system_clock::now(); // benchmark something here std::chrono::time_point<std::chrono::system_clock> end = std::chrono::system_clock::now(); // now here's the concise version auto start = std::chrono::system_clock::now(); // benchmark something here auto end = std::chrono::system_clock::now(); } |
人工記憶體管理是對 C++ 最普遍的誤解。從 C++11 開始,推薦使用 std::shared_ptr 或者 std::unique_ptr 來實現自動記憶體管理。雖然自動記憶體管理需要小的運算代價來維護引用指標,但是這個代價是微乎其微的,並且它的安全性也值得付出這個代價。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <iostream> #include <memory> void testPointers() { PagedData* a = new PagedData(); //raw pointer, ideally shouldn't be managed manually std::unique_ptr<PagedData> b = std::make_unique<PagedData>(); // only 1 unique owner can own at a time auto c = std::make_unique<PagedData>(); // same, but less verbose... std::shared_ptr<PagedData> d = std::make_shared<PagedData>(); // can be passed around liberally and have multiple owners auto e = std::make_shared<PagedData>(); // same, but less verbose... // a will now leak as we forgot to call delete // yet b/c and d/e will be cleaned up for us // notice the lack of new and delete for b/c and d/e? modern c++ prefers the absence of new/delete calls; } int main() { testPointers(); } |
保證生產效率的最後一部分,是有可以快速構建一個服務或者產品的庫。Python、Ruby 等語言有很棒的庫來處理常見的基礎結構。在我看來,目前的 C++ 標準庫嚴重缺乏基礎功能,並且某些 API 效能低下(例如,用 iostreams 讀取檔案)。好在 Facebook 有高質量的開源庫幫助我們快速地釋出我們的 OLAP 雲服務。
這是一個很棒的 C++ 庫,它包含很多高效能的類以供使用。我在我們的引擎中使用它們的 fbvector 和 fbstring,因為它們分別提供了比 std::vector 和 std::string 更好的效能。我們還使用了它們的 futures 和原子無鎖資料結構。
Facebook 針對動態增長分配做了一個很聰明的舉動來避免平方階增長(在數學上可以簡單地證明並解釋為什麼平方階增長是不好的)。他們的容器使用 1.5x 而不是 2x 來增加記憶體尺寸,從而提高效能。
另外,閱讀 Folly 的程式碼也使得我變成更好的 C++ 開發者,所以我強烈建議大家去閱讀。
Proxygen 是一個非同步 HTTP 伺服器,也是由 Facebook 開發的。我們使用 Proxygen 作為我們的 HTTP 伺服器,以 JSON 的形式插入資料到我們的 OLAP 引擎和從我們的引擎中檢索資料。它使得我們可以僅用一天時間就開發出一個高效能的 HTTP 伺服器作為我們的引擎。我決定將它作為基準與用 Python 寫的 Tornado 伺服器進行對比。在一個 EC2 例項中測試 200 次 HTTP 連線,得出了下面的結果:
C++/Proxygen =每秒 1,990,130 次請求
Python/Tornado = 每秒 41,329 次請求
它的 API 更加底層並且你必須自己編寫 HTTP 路由,不過這是一項簡單的任務。我們的 HTTP 主體處理函式大致如下:
1 2 3 4 5 6 7 |
#include <folly/futures/Future.h> void OlapHandler::onBody(std::unique_ptr<folly::IOBuf> body) noexcept { folly::fbstring queryStr = processBody(std::move(body)); Future<OlapData> data = olapCube.query(queryStr); data.then(parseToJson).then(addToHttpBody); } |
我們的 OLAP 引擎基本上是一個用於儲存和查詢多維資料的分散式資料庫。該引擎使用 Wangle 作為一個應用伺服器的基礎框架。所有的邏輯被分層堆放在 Wangle 的處理函式中,鏈式地放到一個管道中或者從一個管道中取出。它與我們的 Proxygen HTTP 伺服器通訊,以處理資料查詢和節點之間的通訊。
它使用網狀的伺服器來共享相同的(對稱的)二進位制可執行檔案,所以沒有主從模式之分。每個節點既是主伺服器也是從伺服器,它們之間使用一套自定義的二進位制資料協議來傳遞資料或者訊息。
我們需要的庫中唯一缺少的是用於完成協助排程儲存器和引擎裡的查詢任務的 fibers。另外,雖然目前 Folly 和 Wangle 的開發者提供了實驗性的版本,但是這還不能用於產品。
硬體和人力成本
我通過量化得出,基於我們的 HTTP 標準,1 臺 C++ 伺服器的原始計算能力大概相當於 40 臺負載平衡的 Python 伺服器的原始計算能力。因此,使用 C++ 可以擠壓出基礎硬體的所有計算能力,從而以 1/40 的折扣節省伺服器成本。我想最初我們可以用 Python 來編寫引擎,但是,經濟上,它將會浪費人力成本和時間,因為,在某些階段,我們不得不拋棄 Python 而使用 C++ 版本來滿足我們對效能的需求。一旦被拋棄,用 Python 寫的程式碼將沒有任何經濟價值。
總而言之,C++ 可能不會成為初創公司最流行的選擇,但是我相信現代 C++ 提供的高階抽象和接近 C 的效能特點,可以使之成為一種切實可行的選擇。我擔心的是一旦程式碼庫顯著地增長時所需的構建時間,希望 C++17 模組可以緩解這個問題。
我希望這篇文章可以鼓勵其他人為自己的企業去研究一下 C++。
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!