DataHub cost saving guide
This topic describes the new batch processing feature in DataHub, including how it works and how to implement it. Switching to batch mode improves performance and reduces costs by decreasing server-side resource consumption and significantly lowering usage fees.
Upgrade details
Support for zstd compression
DataHub now supports the Zstandard (zstd) compression algorithm. Compared to the lz4 and deflate algorithms that DataHub also supports, zstd delivers superior performance.
Zstandard (zstd) is a high-performance compression algorithm that was developed by Facebook and open-sourced in 2016. The zstd algorithm excels in both compression speed and compression ratio, which makes it well-suited for DataHub scenarios.
Serialization transformation
DataHub introduces batch serialization, which is a method for organizing data for transmission. The term 'batch' does not refer to a specific serialization method. Instead, it refers to a secondary encapsulation of serialized data. For example, to send 100 records at once, the 100 records are first serialized into a buffer. A compression algorithm is then applied to this buffer to create a compressed buffer. Finally, a header is added to the compressed buffer to store metadata, such as its size, the number of data records, the compression algorithm, and a CRC checksum. This process creates a complete batch buffer.
This addresses the following problems:
Effectively prevents dirty data at the business layer.
Reduces server-side CPU overhead and improves data processing performance.
Reduces simultaneous read and write latency.
Because the client performs thorough data validity checks before sending a batch buffer, the server only needs to verify the CRC checksum to confirm that the buffer is valid. The buffer can then be written directly to disk. This process eliminates the need for server-side serialization, deserialization, compression, decompression, and validation operations. As a result, server-side performance is improved by over 80%. Because multiple data records are compressed together, the compression ratio is also improved, which reduces storage costs.
Cost comparison
The following test was conducted to verify the effects of batch processing. The test scenario is as follows:
The test data consists of ad delivery information with approximately 200 columns. Approximately 20% to 30% of the data values are null.
1,000 data records per batch.
Avro is used for serialization within the batch.
The zstd compression algorithm is used to replace lz4, which is the default compression algorithm in previous versions.
The test produced the following results:
Data source size (bytes) | lz4 compressed size (bytes) | zstd compressed size (bytes) | |
protobuf serialization | 11,506,677 | 3,050,640 | 1,158,868 |
batch serialization | 11,154,596 | 2,931,729 | 1,112,693 |
DataHub billing is based on two main dimensions: storage and traffic. Other billable items are primarily intended as penalties to prevent resource abuse. Therefore, the test results are analyzed from the perspectives of storage and traffic.
Storage costs: When using protobuf serialization, DataHub does not compress data for storage. Data is compressed only during HTTP transmission. If you replace this method with batch processing and zstd compression, the storage size decreases from 11,506 KB to 1,112 KB. This reduces storage costs by approximately 90%.
Traffic costs: When using protobuf and lz4, the data size is 3,050 KB. When using batch processing and zstd, the data size is 1,112 KB. This reduces traffic costs by approximately 60%.
The preceding results are based on sample data. Your actual results may vary depending on your data. We recommend that you run your own tests as needed.
Use batch processing
Notes
The primary benefit of batch writing is maximizing the number of records in each batch. If the client cannot batch records, or if the number of records per batch is small, the performance improvement may not be significant.
To ensure a smooth transition, batch mode is compatible with the original read and write methods. Data written in batch mode can be read in the original mode, and vice versa. However, for optimal performance, you should write and consume data in the same mode. Mismatched writing and consumption degrades performance.
The latest version of DataHub no longer requires you to enable the multi-version schema for a topic to use the batch protocol. However, you must update your client to a supported version. The following table lists the supported clients and their required versions.
Language | Support status | Version requirement |
Java SDK | Supported | 1.5.1 or later |
Go SDK | Supported | 1.1.0 or later |
Python | Not supported | - |
C++ | Not supported | - |
Java example
In versions 1.5.1 and later, the default serialization protocol is Batch. No special configuration is needed. You can write data as usual, using either synchronous or asynchronous writes.
Maven dependency
<dependency>
<groupId>com.aliyun.datahub</groupId>
<artifactId>datahub-client-library</artifactId>
<version>1.5.1</version>
</dependency>Code example
public static void main(String[] args) throws InterruptedException {
// Obtain AccessKey information from an environment variable.
EnvironmentVariableCredentialProvider provider = EnvironmentVariableCredentialProvider.create();
String endpoint ="https://dh-cn-hangzhou.aliyuncs.com";
String projectName = "test_project";
String topicName = "test_topic";
// Initialize the producer. The default configurations are used.
ProducerConfig config = new ProducerConfig(endpoint, provider);
DatahubProducer producer = new DatahubProducer(projectName, topicName, config);
RecordSchema schema = producer.getTopicSchema();
// If a multi-version schema is enabled, you can also get the schema of a specified version.
// RecordSchema schema = producer.getTopicSchema(3);
// For an asynchronous write, register a callback function as needed.
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);
// If you do not need to confirm whether the data was sent successfully, do not register a callback. Send the data directly.
// producer.sendAsync(recordEntry, null);
} catch (DatahubClientException e) {
// TODO: Handle exceptions, which are typically non-retriable errors or errors that occur after the maximum number of retries.
Thread.sleep(1000);
}
}
// Ensure that all data is sent before the program exits.
producer.flush(true);
producer.close();
}Go example
go.mod dependency
require (
github.com/aliyun/aliyun-datahub-sdk-go v1.1.0
)Code example
In versions 1.1.0 and later, the default serialization protocol is Batch. No special configuration is needed. You can write data as usual, using either synchronous or asynchronous writes.
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()
// Obtain AccessKey information from an environment variable.
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)
}
// Handle the success channel.
go handleSuccessRun(producer)
// Handle the error channel.
go handleFailedRun(producer)
// Generate data in a loop.
for i := 0; i < 1000; i++ {
record := datahub.NewTupleRecord(schema)
// Each time you set a value, check if the operation was successful.
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)
}
}
Support
If you have any questions or issues, you can submit a ticket for support or join our user group. The group number is 33517130.