本文介紹了DataHub新版本帶來的改動,介紹batch的原理和實現,以及使用batch後所帶來的效能的提升和費用的減少。切換為batch後,對於DataHub而言,服務端的資源消耗會明顯降低,同時,效能會明顯提升,使用費用也會大幅降低。
升級內容
支援zstd壓縮
DataHub在新版本中對zstd壓縮演算法做了支援,相較於DataHub支援的lz4和deflate壓縮演算法,效果卓越。
zstd是一種高效能壓縮演算法,由Facebook開發,於2016年開源,zstd在壓縮速度和壓縮比兩方面都有不俗的表現,非常契合DataHub的使用情境。
序列化改造
DataHub引入了batch序列化,batch序列化本質上就是DataHub對資料轉送中資料的定義的一種組織方式,batch並不是特指某種序列化的方式,而是對序列化的資料做了二次封裝。例如:一次發送100條資料,將100條資料序列化後得到一個buffer,給這個buffer選擇一個壓縮演算法得到壓縮後的buffer,這個時候給這個壓縮後的buffer添加一個header記錄這個buffer大小、資料條數、壓縮演算法、crc等資訊,從而獲得一條完整batch buffer。
解決的問題:
可以有效避免業務層面的髒資料。
減少服務端CPU開銷,提高資料處理效能。
較少同時讀寫延遲。
batch的buffer發送到服務端後,因用戶端已經做了充分的資料有效性的校正,所以服務端只需檢驗資料中的crc確認為有效buffer後,便可以直接落盤,省去了序列化、還原序列化、加解壓以及校正的操作,服務端效能提升超過80%,因為是多條資料一起壓縮的,所以壓縮率也提高了,儲存成本也降低了。
費用對比
驗證batch所帶來的實際效果,進行以下測試,假設情境如下:
測試資料為廣告投放相關的資料,大約200列,資料中null比例大約20%~30%。
1000條資料一個batch。
batch內部的序列化使用的是avro。
lz4是之前版本預設的壓縮演算法,壓縮使用zstd來替代lz4。
測試結果如下:
資料來源大小(Byte) | lz4壓縮(Byte) | zstd壓縮(Byte) | |
protobuf序列化 | 11,506,677 | 3,050,640 | 1,158,868 |
batch序列化 | 11,154,596 | 2,931,729 | 1,112,693 |
Datahub的收費項主要是這兩個維度為儲存與流量,其他主要是為了防止濫用而設定的懲罰性質的收費項。因此,以上測試結果從儲存與流量兩個角度進行分析。
從儲存成本上來看,DataHub的protobuf序列化是沒有儲存壓縮的(只是HTTP傳輸環節壓縮),如果替換為batch+zstd,那麼儲存會由11506KB降為1112KB,也就是說,儲存成本下降幅度達到約90%。
從流量成本上來看,DataHub的protobuf+lz4後的大小為3050KB,batch+zstd的大小為1112KB,也就是說,流量成本會降低約60%。
以上為樣本資料測試結果,不同資料測試效果有差異,請您根據您業務實際情況進行測試。
使用batch
注意事項
batch寫入最大的優勢需要充分攢批,如果用戶端無法攢批,或者攢批的資料較少,可能帶來的效果提升並不顯著。
為了讓使用者移轉更加方便,DataHub在各種讀寫方式之間做了相容,保證使用者中間狀態可以更平滑地過渡,即batch寫入依舊可以使用原方式讀取,原方式寫入依舊可以使用batch讀取。因此,在寫入端更新為batch寫入之後,最好消費端也更新為batch,寫入和消費不對應反而會降低效能。
DataHub新版本已經取消只有開啟多version的Topic 才可以使用batch協議,無需開啟Topic 多version開關就可以使用batch協議,但需要使用者更新到最新版的用戶端,目前支援的用戶端以及所需版本如下所示:
語言 | 支援狀態 | 版本要求 |
Java SDK | 支援 | 1.5.1 及以上 |
Go SDk | 支援 | 1.1.0 及以上 |
Python | 暫不支援 | - |
C++ | 暫不支援 | - |
Java 樣本:
在 1.5.1 及以後的版本中,預設序列化協議已經設定為 Batch,使用者無需做任何特殊配置,只需要正常的寫入即可,同步和非同步寫入均可以。
Maven依賴
<dependency>
<groupId>com.aliyun.datahub</groupId>
<artifactId>datahub-client-library</artifactId>
<version>1.5.1</version>
</dependency>程式碼範例:
public static void main(String[] args) throws InterruptedException {
// 通過環境變數擷取AK資訊
EnvironmentVariableCredentialProvider provider = EnvironmentVariableCredentialProvider.create();
String endpoint ="https://dh-cn-hangzhou.aliyuncs.com";
String projectName = "test_project";
String topicName = "test_topic";
// 初始化Producer,這裡直接使用預設配置
ProducerConfig config = new ProducerConfig(endpoint, provider);
DatahubProducer producer = new DatahubProducer(projectName, topicName, config);
RecordSchema schema = producer.getTopicSchema();
// 如果開啟了多version schema,這裡也可以擷取指定version的schema
// RecordSchema schema = producer.getTopicSchema(3);
// 對於非同步寫入,可以根據需要來選擇是否註冊回呼函數
WriteCallback callback = new WriteCallback() {
@Override
public void onSuccess(String shardId, List<RecordEntry> records, long elapsedTimeMs, long sendTimeMs) {
System.out.println("write success");
}
@Override
public void onFailure(String shardId, List<RecordEntry> records, long elapsedTimeMs, DatahubClientException e) {
System.out.println("write failed");
}
};
for (int i = 0; i < 10000; ++i) {
try {
// generate data by schema
TupleRecordData data = new TupleRecordData(schema);
data.setField("field1", "hello");
data.setField("field2", 1234);
RecordEntry recordEntry = new RecordEntry();
recordEntry.setRecordData(data);
producer.sendAsync(recordEntry, callback);
// 如果不需要關心資料是否發送成功,那麼就不需要註冊回調,直接發送
// producer.sendAsync(recordEntry, null);
} catch (DatahubClientException e) {
// TODO 處理異常,一般是不可重試錯誤或者超過重試次數;
Thread.sleep(1000);
}
}
// 保證退出前,資料全部被發送完
producer.flush(true);
producer.close();
}Go 樣本:
go.mod依賴
require (
github.com/aliyun/aliyun-datahub-sdk-go v1.1.0
)程式碼範例:
在 1.1.0 及以後的版本中,預設序列化協議已經設定為 Batch,無需使用者做任何特殊配置,只需要正常的寫入即可,同步和非同步寫入均可以。
func handleSuccessRun(producer datahub.AsyncProducer) {
for suc := range producer.Successes() {
// handle request success
fmt.Printf("shard:%s, rid:%s, records:%d, latency:%v\n",
suc.ShardId, suc.RequestId, len(suc.Records), suc.Latency)
}
}
func handleFailedRun(producer datahub.AsyncProducer) {
// handle request failed
for err := range producer.Errors() {
fmt.Printf("shard:%s, records:%d, latency:%v, error:%v\n",
err.ShardId, len(err.Records), err.Latency, err.Err)
}
}
func main() {
cfg := datahub.NewProducerConfig()
// 通過環境變數擷取AK資訊
credential, err := credentials.NewCredential(nil)
if err != nil {
fmt.Println(err)
// TODO: handle error
}
cfg.Account = datahub.NewCredentialAccount(credential)
cfg.Endpoint = "https://dh-cn-hangzhou.aliyuncs.com"
cfg.Project = "test_project"
cfg.Topic = "test_topic"
producer := datahub.NewAsyncProducer(cfg)
err = producer.Init()
if err != nil {
// TODO: handle error
fmt.Println(err)
}
schema, err := producer.GetSchema()
if err != nil {
// TODO: handle error
fmt.Println(err)
}
// 處理success channle
go handleSuccessRun(producer)
// 處理error channle
go handleFailedRun(producer)
// 迴圈產生資料
for i := 0; i < 1000; i++ {
record := datahub.NewTupleRecord(schema)
// 每次設定值都需要檢查設定是否成功
err = record.SetValueByName("f1", "val1")
if err != nil {
fmt.Println(err)
return
}
err = record.SetValueByName("f2", 1234)
if err != nil {
fmt.Println(err)
return
}
producer.Input() <- record
}
err = producer.Close()
if err != nil {
fmt.Println(err)
}
}
後續支援
如果您在使用中有任何問題或者疑問,歡迎提工單諮詢或加入使用者群諮詢,群號:33517130。