遲到的Kudu設計要點面面觀(之更加遲到的后篇)

前篇傳送門:http://www.lxweimin.com/p/5ffd8730aad8

目錄

事務與數據一致性

Kudu支持單行事務,但不支持多行事務(Kudu中對多行操作不滿足ACID原則中的原子性),也不支持事務回滾,這點與HBase是相同的。

前面已經提到過,Kudu采用與關系數據庫類似的多版本并發控制(MVCC)機制來實現事務隔離,通過為數據添加時間戳的方式實現。該時間戳不能在寫入時由用戶添加,但可以在執行讀?。⊿can)操作時指定,這樣就可以讀取到歷史數據(UndoFile中的數據)。Kudu提供兩種讀模式:read-latest和read-at-snapshot,分別對應讀取當前的快照以及按時間戳讀取歷史快照。

對于寫操作而言,Kudu也提供了兩種一致性模型:快照一致性(snapshot consistency)和外部一致性(external consistency)。下面來分析一下它們。

快照一致性比較簡單,只保證當前執行寫操作的客戶端能看到自己提交的最新數據,而不保障跨客戶端的可見性。它是Kudu默認的一致性模型,一般情況下都夠用。但是特殊情況也同樣存在:考慮用Kudu作為點擊流數倉的情景,客戶端A在某時刻寫入了點擊事件x,客戶端B緊隨其后寫入事件y,并且這兩個事件之間具有關聯性。要想讓所有客戶端都能達到外部一致性(及時取到最新數據),必須手動將寫操作完成后產生的時間戳傳播(propagate)到其他客戶端上,這種方式在Kudu中叫client-propagated。

很顯然,client-propagated方案需要頻繁地交換時間戳,其overhead比較高,所以Kudu也借鑒了Google Spanner的思路,實現了commit-wait一致性。

我們已經可以發現,保證外部一致性的重點在于事務的版本號(時間戳)必須足夠準,并且每臺服務器的時間都要保持精確的同步。Google Spanner提出的時間同步方案叫做TrueTime,需要原子鐘等硬件的支持,可以將對時間的認知誤差控制在±4ms之內。但Kudu集群都是建立在普通商用服務器上的,所以只能靠NTP和算法近似實現,該算法名為HybridTime,不詳細展開了,看官可以參考論文《Technical Report: HybridTime - Accessible Global Consistency with High Clock Uncertainty》。

下圖粗淺地示出commit-wait機制的原理。

當一個事務獲取到鎖并開始執行時,它會先生成自己的時間戳,再開始事務操作。當事務執行完之后,還必須要保證后發生的事務時間戳不能比自己的時間戳小,因此最終要等待2倍的誤差時間,才能結束本次事務并釋放鎖。

與Impala、Spark集成

Kudu本身并沒有SQL外殼,僅僅提供了Java和C++ API。但是Kudu和查詢引擎Impala可以近乎無縫地結合在一起,為Kudu提供SQL能力。下面的簡圖示出用Impala SQL對Kudu表執行簡單查詢的流程。

可見,在Impala端會解析SQL語句并生成查詢計劃,然后作為客戶端去連接Kudu集群,執行增刪改查操作。關于Kudu與Impala的集成和查詢方法,官方文檔已經寫得非常詳細,不再贅述。

相對而言,我們更多地是編寫Spark程序來執行一些對Kudu表數據的復雜分析任務。Maven上已經有Kudu與Spark的connector包,其坐標如下。

    <!-- scala.bin.version: 2.11, kudu.version: 1.5.0 -->
    <dependency>
        <groupId>org.apache.kudu</groupId>
        <artifactId>kudu-spark2_${scala.bin.version}</artifactId>
        <version>${kudu.version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.kudu</groupId>
        <artifactId>kudu-spark2-tools_${scala.bin.version}</artifactId>
        <version>${kudu.version}</version>
   </dependency>

引入依賴之后,就可以用Spark SQL以及KuduContext來操作Kudu表了,一個簡單的示例代碼如下。

import org.apache.kudu.client._
import collection.JavaConverters._

// Read a table from Kudu
val df = spark.read
  .options(Map("kudu.master" -> "kudu.master:7051", "kudu.table" -> "kudu_table"))
  .format("kudu").load

// Query using the Spark API...
df.select("id").filter("id >= 5").show()

// ...or register a temporary table and use SQL
df.createOrReplaceTempView("kudu_table")
val filteredDF = spark.sql("select id from kudu_table where id >= 5").show()

// Use KuduContext to create, delete, or write to Kudu tables
val kuduContext = new KuduContext("kudu.master:7051", spark.sparkContext)

// Create a new Kudu table from a DataFrame schema
// NB: No rows from the DataFrame are inserted into the table
kuduContext.createTable(
    "test_table", df.schema, Seq("key"),
    new CreateTableOptions()
        .setNumReplicas(1)
        .addHashPartitions(List("key").asJava, 3))

// Insert data
kuduContext.insertRows(df, "test_table")

// Delete data
kuduContext.deleteRows(filteredDF, "test_table")

// Upsert data
kuduContext.upsertRows(df, "test_table")

// Update data
val alteredDF = df.select("id", $"count" + 1)
kuduContext.updateRows(filteredRows, "test_table")

// Data can also be inserted into the Kudu table using the data source, though the methods on
// KuduContext are preferred
// NB: The default is to upsert rows; to perform standard inserts instead, set operation = insert
// in the options map
// NB: Only mode Append is supported
df.write
  .options(Map("kudu.master"-> "kudu.master:7051", "kudu.table"-> "test_table"))
  .mode("append")
  .format("kudu").save

// Check for the existence of a Kudu table
kuduContext.tableExists("another_table")

// Delete a Kudu table
kuduContext.deleteTable("unwanted_table")

需要注意的是,Spark on Kudu不支持有大寫字母和非ASCII字符的表名、列名,必須預先處理。另外,不等于(<>)和or謂詞不會下推給Kudu,而是由Spark任務來處理。like謂詞同理,當有通配符時,只有以通配符結尾的語句(如like 'some%')才會下推給Kudu。

Benchmarking

在TPC-H數據集上進行測試,Impala on Kudu的查詢時間比Impala on HDFS (Parquet) 平均縮短了三成。

使用TPC-H中的lineitem表(原始數據大小約62GB)進行Impala on Kudu與Phoenix on HBase的對比測試,包括數據的載入與4種查詢。Phoenix on HBase的表劃分為100個哈希分區,Kudu表劃分為100個Tablet。

測試結果如下。

可見,Phoenix on HBase的方案只有在基于RowKey的查詢時有性能優勢,并且領先幅度不大。而Impala on Kudu在執行基于列的查詢和全表掃描時,效率遠遠高于HBase。當然,這與HBase偏OLTP的設計思想有關,并不能說明Kudu可以完全取代HBase。

另外,論文中還用了雅虎的YCSB數據集測試隨機讀寫能力。

結果如下,整體上看,Kudu的隨機讀寫與HBase相比都或多或少地落后,其中zipfian數據集(符合Zipf's Law,即長尾分布)上的差距比較大,而uniform數據集(符合均勻分布)上的差距比較小。這也是自然的,要想兼顧OLAP的效率,必然要在OLTP方面做出一些犧牲。Kudu也在持續優化隨機讀寫,不過那是新版的事情了。

當前的主要不足

Kudu現在可以基本滿足我們對于OLTP+OLAP混合型分析的需求,但是它畢竟還年輕,采用的設計方案也較新,因此不可避免地還存在一些短板,在實際使用時需要提前避開一些坑。以我們生產環境中部署的1.5版本舉例如下:

  • 一行的主鍵組的值不能修改。如果想修改主鍵,就必須把該行刪掉并新插入一行,但這樣就無法保證原子性。
  • 數據類型相對稀少,不支持所有復雜結構(map、struct等)。數據類型、是否允許為空、壓縮編碼等屬性在列創建后都不能更改。
  • 無法像HBase一樣手動觸發Compaction過程,無法在TServer間做數據均衡,表中已有的數據無法重新分區。
  • 不能隨意添加或者刪除Kudu數據的存儲目錄,想要更改的話必須格式化所有目錄,再進行遷移。
  • 不支持像ElasticSearch一樣的滾動重啟。如果要從單個Master的部署切換到多個Master,必須手動操作,步驟非常復雜,容易出錯。
  • TServer的總數據量和Tablet的數量都不能過大,官方給出的單節點最大承受值是8TB、2000個Tablet。但在我們的實踐中,數據量只達到上述的一半,整個集群重啟就幾乎起不來了。

簡單調優方法

我們的Kudu服務與Hadoop基礎服務和Impala一起部署在10個節點上(每個節點雙路E5 12C/24T,256G RAM,6TB SAS HDD),3個Master,10個TServer。以下是我們根據集群實際情況對一些主要參數進行的調優:

  • memory_limit_hard_bytes
    該參數是單個TServer能夠使用的最大內存量。如果寫入量很大而內存太小,會造成寫入性能下降。如果集群資源充裕,可以將它設得比較大,比如單臺服務器內存總量的一半。我們設定為32GB。
    官方也提供了一個近似估計的方法,即:每1TB實際存儲的數據約占用1.5GB內存,每個副本的MemRowSet和DeltaMemStore約占用128MB內存,(對多讀少寫的表而言)每列每CPU核心約占用256KB內存,另外再加上塊緩存,最后在這些基礎上留出約25%的余量。

  • block_cache_capacity_mb
    Kudu中也設計了BlockCache,不管名稱還是作用都與HBase中的對應角色相同。默認值512MB,經驗值是設置1~4GB之間,我們設了4GB。

  • memory.soft_limit_in_bytes/memory.limit_in_bytes
    這是Kudu進程組(即Linux cgroup)的內存軟限制和硬限制。當系統內存不足時,會優先回收超過軟限制的進程占用的內存,使之盡量低于閾值。當進程占用的內存超過了硬限制,會直接觸發OOM導致Kudu進程被殺掉。我們設為-1,即不限制。

  • maintenance_manager_num_threads
    單個TServer用于在后臺執行Flush、Compaction等后臺操作的線程數,默認是1。如果是采用普通硬盤作為存儲的話,該值應與所采用的硬盤數相同。

  • max_create_tablets_per_ts
    創建表時能夠指定的最大分區數目(hash partition * range partition),默認為60。如果不能滿足需求,可以調大。

  • follower_unavailable_considered_failed_sec
    當Follower與Leader失去聯系后,Leader將Follower判定為失敗的窗口時間,默認值300s。

  • max_clock_sync_error_usec
    NTP時間同步的最大允許誤差,單位為微秒,默認值10s。如果Kudu頻繁報時間不同步的錯誤,可以適當調大,比如15s。

The End

謝謝食用~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。