引言
在實際的kafka開發中,我們會發現,無論是生產者還是消費者,都需要構建一個Properties對象,里面設置了很多參數。對于很多初學者來說,會看不懂這些參數分別代表什么含義。
在本篇文章我們就來詳細地了解一下這些參數的作用,并探討下如何使用合理的配置去優化提高生產/消費效率。
正文
1.kafka消費者參數
我們先來看一段消費者的構建代碼。
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "test");
props.put("enable.auto.commit", "true");
props.put("auto.commit.interval.ms", "1000");
props.put("auto.offset.reset", "earliest");
props.put("session.timeout.ms", "30000");
props.put("fetch.min.bytes", "1048576");
props.put("fetch.max.wait.ms", "2000");
props.put("max.partition.fetch.bytes", "2097152");
props.put("max.poll.records", "10000");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);
在這段代碼中有很多常用的參數配置,在線上使用時,我們要根據實際的數據量和數據大小來決定這些配置的具體值。下面來挑出其中比較重要的幾個參數來詳細解析一下。
1.1 enable.auto.commit
指定了消費者是否自動提交偏移量,默認值是true,為了盡量避免重復數據和數據丟失,可以把它設置為false,有自己控制合適提交偏移量,如果設置為true, 可以通過設置 auto.commit.interval.ms屬性來控制提交的頻率。
詳細地來說:
當一個consumer因某種原因退出Group時,進行重新分配partition后,同一group中的另一個consumer在讀取該partition時,怎么能夠知道上一個consumer該從哪個offset的message讀取呢?也是是如何保證同一個group內的consumer不重復消費消息呢?上面說了一次走網絡的fetch請求會拉取到一定量的數據,但是這些數據還沒有被消息完畢,Consumer就掛掉了,下一次進行數據fetch時,是否會從上次讀到的數據開始讀取,而導致Consumer消費的數據丟失嗎?
為了做到這一點,當使用完poll從本地緩存拉取到數據之后,需要client調用commitSync方法(或者commitAsync方法)去commit 下一次該去讀取 哪一個offset的message。
而這個commit方法會通過走網絡的commit請求將offset在coordinator中保留,這樣就能夠保證下一次讀取(不論進行了rebalance)時,既不會重復消費消息,也不會遺漏消息。
對于offset的commit,Kafka Consumer Java Client支持兩種模式:由KafkaConsumer自動提交,或者是用戶通過調用commitSync、commitAsync方法的方式完成offset的提交。
自動提交的例子:
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "test");
props.put("enable.auto.commit", "true");
props.put("auto.commit.interval.ms", "1000");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("foo", "bar"));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records)
System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
}
手動提交的栗子:
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "test");
props.put("enable.auto.commit", "false");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("foo", "bar"));
final int minBatchSize = 200;
List<ConsumerRecord<String, String>> buffer = new ArrayList<>();
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records) {
buffer.add(record);
}
if (buffer.size() >= minBatchSize) {
insertIntoDb(buffer);
consumer.commitSync();
buffer.clear();
}
}
在手動提交單個partition的offset時,需要注意的一點是:要提交的是下一次要讀取的offset,例如:
try {
while(running) {
// 取得消息
ConsumerRecords<String, String> records = consumer.poll(Long.MAX_VALUE);
// 根據分區來遍歷數據:
for (TopicPartition partition : records.partitions()) {
List<ConsumerRecord<String, String>> partitionRecords = records.records(partition);
// 數據處理
for (ConsumerRecord<String, String> record : partitionRecords) {
System.out.println(record.offset() + ": " + record.value());
}
// 取得當前讀取到的最后一條記錄的offset
long lastOffset = partitionRecords.get(partitionRecords.size() - 1).offset();
// 提交offset,記得要 + 1
consumer.commitSync(Collections.singletonMap(partition, new OffsetAndMetadata(lastOffset + 1)));
}
}
} finally {
consumer.close();
}
1.2 auto.offset.reset
該屬性指定了消費者在讀取一個沒有偏移量后者偏移量無效(消費者長時間失效當前的偏移量已經過時并且被刪除了)的分區的情況下,應該作何處理,默認值是latest,也就是從最新記錄讀取數據(消費者啟動之后生成的記錄),另一個值是earliest,意思是在偏移量無效的情況下,消費者從起始位置開始讀取數據。
1.3 session.timeout.ms
該屬性指定了當消費者被認為已經掛掉之前可以與服務器斷開連接的時間。默認是3s,消費者在3s之內沒有再次向服務器發送心跳,那么將會被認為已經死亡。此時,協調器將會出發再均衡,把它的分區分配給其他的消費者,該屬性與heartbeat.interval.ms緊密相關,該參數定義了消費者發送心跳的時間間隔,也就是心跳頻率,一般要同時修改這兩個參數,heartbeat.interval.ms參數值必須要小于session.timeout.ms,一般是session.timeout.ms的三分之一,比如,session.timeout.ms設置成3min,那么heartbeat.interval.ms一般設置成1min,這樣,可以更快的檢測以及恢復崩潰的節點,不過長時間的輪詢或垃圾收集可能導致非預期的再均衡(有一種情況就是網絡延遲,本身消費者是沒有掛掉的,但是網絡延遲造成了心跳超時,這樣本不該發生再均衡,但是因為網絡原因造成了非預期的再均衡),把該屬性的值設置得大一些,可以減少意外的再均衡,不過檢測節點崩憤-需要更長的時間。
1.4 max.partition.fetch.bytes
該屬性指定了服務器從每個分區里返回給消費者的最大字節數。它的默認值是lMB , 也
就是說,kafkaConsumer.poll() 方法從每個分區里返回的記錄最多不超max.partitions.fetch.bytes 指定的字節。如果一個主題有20 個分區和5 個消費者,那么每個消費者需要至少4MB 的可用內存來接收記錄。在為消費者分配內存時,可以給它們多分配一些,因為如果群組里有消費者發生奔潰,剩下的消費者需要處理更多的分區。max.partition.fetch.bytes 的值必須比broker 能夠接收的最大消息的字節數(通過max.message.size 屬性配置)大, 否則消費者可能無法讀取這些消息,導致消費者一直掛起重試,例如,max.message.size設置為2MB,而該屬性設置為1MB,那么當一個生產者可能就會生產一條大小為2MB的消息,那么就會出現問題,消費者能從分區取回的最大消息大小就只有1MB,但是數據量是2MB,所以就會導致消費者一直掛起重試。
在設置該屬性時,另一個需要考慮的因素是消費者處理數據的時間。消費者需要頻繁調用poll()方法
來避免會話過期和發生分區再均衡,如果單次調用poll()返回的數據太多,消費者需要更多的時間來處理,可能無怯及時進行下一個輪詢來避免會話過期。如果出現這種情況, 可以把max.partitioin.fetch.bytes 值改小,或者延長會話過期時間。
1.5 fetch.min.bytes
消費者從服務器獲取記錄的最小字節數,broker收到消費者拉取數據的請求的時候,如果可用數據量小于設置的值,那么broker將會等待有足夠可用的數據的時候才返回給消費者,這樣可以降低消費者和broker的工作負載。
因為當主題不是很活躍的情況下,就不需要來來回回的處理消息,如果沒有很多可用數據,但消費者的CPU 使用率卻很高,那么就需要把該屬性的值設得比默認值大。如果消費者的數量比較多,把該屬性的值設置得大一點可以降低broker 的工作負載。
1.6 fetch.max.wait.ms
fetch.min.bytes設置了broker返回給消費者最小的數據量,而fetch.max.wait.ms設置的則是broker的等待時間,兩個屬性只要滿足了任何一條,broker都會將數據返回給消費者,也就是說舉個例子,fetch.min.bytes設置成1MB,fetch.max.wait.ms設置成1000ms,那么如果在1000ms時間內,如果數據量達到了1MB,broker將會把數據返回給消費者;如果已經過了1000ms,但是數據量還沒有達到1MB,那么broker仍然會把當前積累的所有數據返回給消費者。
1.7 max.poll.records
控制單次調用call方法能夠返回的記錄數量,幫助控制在輪詢里需要處理的數據量。
1.8 receive.buffer.bytes + send.buffer.bytes
socket 在讀寫數據時用到的TCP 緩沖區也可以設置大小。如果它們被設為-1 ,就使用操作系統的默認值。如果生產者或消費者與broker 處于不同的數據中心內,可以適當增大這些值,因為跨數據中心的網絡一般都有比較高的延遲和比較低的帶寬。
1.9 partition.assignment.strategy
分區分配策略,kafka有兩個默認策略:
- Range:該策略會把主題的若干個連續的分區分配給消費者
- Robin:該策略把主題的所有分區逐個分配給消費者
分區策略默認是:org.apache.kafka.clients.consumer.RangeAssignor=>Range策略
org.apache.kafka.clients.consumer.RoundRobinAssignor=>Robin策略
1.10 client.id
Consumer進程的標識。如果設置一個人為可讀的值,跟蹤問題會比較方便。