1. Poll Messages
在Kafka Consumer 中消費messages時,使用的是poll模型,也就是主動去Kafka端取數據。其他消息管道也有的是push模型,也就是服務端向consumer推送數據,consumer僅需等待即可。
Kafka Consumer的poll模型使得consumer可以控制從log的指定offset去消費數據、消費數據的速度、以及replay events的能力。
Kafka Consumer 的poll模型工作如下圖:

- · Consumer 調用.poll(Duration timeout) 方法,向broker請求數據
- · 若是broker端有數據則立即返回;否則在timeout時間后返回empty
我們可以通過參數控制 Kafka Consumer 行為,主要有:
- · Fetch.min.bytes(默認值是1)
o 控制在每個請求中,至少拉取多少數據
o 增加此參數可以提高吞吐並降低請求的數目,但是代價是增加延時
- · Max.poll.records(默認是500)
o 控制在每個請求中,接收多少條records
o 如果消息普遍都比較小而consumer端又有較大的內存,則可以考慮增大此參數
o 最好是監控在每個請求中poll了多少條消息
- · Max.partitions.fetch.bytes(默認為1MB)
o Broker中每個partition可返回的最多字節
o 如果目標端有100多個partitions,則需要較多內存
- · Fetch.max.bytes(默認50MB)
o 對每個fetch 請求,可以返回的最大數據量(一個fetch請求可以覆蓋多個partitions)
o Consumer並行執行多個fetch操作
默認情況下,一般不建議手動調整以上參數,除非我們的consumer已經達到了默認配置下的最高的吞吐,且需要達到更高的吞吐。
2. Consumer Offset Commit 策略
在一個consumer 應用中,有兩種常見的committing offsets的策略,分別為:
- · (較為簡單)enable.auto.commit = true:自動commit offsets,但必須使用同步的方式處理數據
- · (進階)enable.auto.commit = false:手動commit offsets
在設置enable.auto.commit = true時,考慮以下代碼:
while(true) {
List<Records> batch = consumer.poll(Duration.ofMillis(100));
doSomethingSynchronous(batch);
}
一個Consumer 每隔100ms poll一次消息,然后以同步地方式處理這個batch的數據。此時offsets 會定期自動被commit,此定期時間由 auto.commit.interval.ms 決定,默認為 5000,也就是在每次調用 .poll() 方法 5 秒后,會自動commit offsets。
但是如果在處理數據時用的是異步的方式,則會導致“at-most-once”的行為。因為offsets可能會在數據被處理前就被commit。
所以對於新手來說,使用 enable.auto.commit = true 可能是有風險的,所以不建議一開始就使用這種方式 。
若設置 enable.auto.commit = false,考慮以下代碼:
while(true) {
List<Records> batch = consumer.poll(Duration.ofMillis(100));
if isReady(batch){
doSomethingSynchronous(batch);
consumer.commitSync();
}
}
此例子明確指示了在同步地處理了數據后,再主動commit offsets。這樣我們可以控制在什么條件下,去commit offsets。一個比較典型的場景為:將接收的數據讀入緩存,然后flush 緩存到一個數據庫中,最后再commit offsets。
3. 手動Commit Offset 示例
首先我們關閉自動commit offsets :
// disable auto commit of offsets
properties.setProperty(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
指定每個請求最多接收10條records,便於測試:
properties.setProperty(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, "10");
添加以下代碼邏輯:
public static void main(String[] args) throws IOException {
Logger logger = LoggerFactory.getLogger(ElasticSearchConsumer.class.getName());
RestHighLevelClient client = createClient();
// create Kafka consumer
KafkaConsumer<String, String> consumer = createConsumer("kafka_demo");
// poll for new data
while(true){
ConsumerRecords<String, String> records =
consumer.poll(Duration.ofMinutes(100));
logger.info("received " + records.count() + "records");
for(ConsumerRecord record : records) {
// construct a kafka generic ID
String kafka_generic_id = record.topic() + "_" + record.partition() + "_" + record.offset();
// where we insert data into ElasticSearch
IndexRequest indexRequest = new IndexRequest(
"kafkademo"
).id(kafka_generic_id).source(record.value(), XContentType.JSON);
IndexResponse indexResponse = client.index(indexRequest, RequestOptions.DEFAULT);
String id = indexResponse.getId();
logger.info(id);
try {
Thread.sleep(10); // introduce a small delay
} catch (InterruptedException e) {
e.printStackTrace();
}
}
logger.info("Committing offsets...");
consumer.commitSync(); // commit offsets manually
logger.info("Offsets have been committed");
}
}
這里我們在處理每次獲取的10條records后(也就是for 循環完整執行一次),手動執行一次offsets commit。打印日志記錄為:
手動停止consumer 程序后,可以看到最后的committed offsets為165:
使用consumer-group cli 也可以驗證當前committed offsets為165:

4. Performance Improvement using Batching
在這個例子中,consumer 限制每次poll 10條數據,然后每條依次處理(插入elastic search)。此方法效率較低,我們可以通過使用 batching 的方式增加吞吐。這里實現的方式是使用 elastic search API 提供的BulkRequest,基於之前的代碼,修改如下:
public static void main(String[] args) throws IOException {
Logger logger = LoggerFactory.getLogger(ElasticSearchConsumer.class.getName());
RestHighLevelClient client = createClient();
// create Kafka consumer
KafkaConsumer<String, String> consumer = createConsumer("kafka_demo");
// poll for new data
while(true){
ConsumerRecords<String, String> records =
consumer.poll(Duration.ofMinutes(100));
// bulk request
BulkRequest bulkRequest = new BulkRequest();
logger.info("received " + records.count() + "records");
for(ConsumerRecord record : records) {
// construct a kafka generic ID
String kafka_generic_id = record.topic() + "_" + record.partition() + "_" + record.offset();
// where we insert data into ElasticSearch
IndexRequest indexRequest = new IndexRequest(
"kafkademo"
).id(kafka_generic_id).source(record.value(), XContentType.JSON);
IndexResponse indexResponse = client.index(indexRequest, RequestOptions.DEFAULT);
// add to our bulk request (takes no time)
bulkRequest.add(indexRequest);
//String id = indexResponse.getId();
//logger.info(id);
try {
Thread.sleep(10); // introduce a small delay
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// bulk response
BulkResponse bulkItemResponses = client.bulk(bulkRequest, RequestOptions.DEFAULT);
logger.info("Committing offsets...");
consumer.commitSync(); // commit offsets manually
logger.info("Offsets have been committed");
}
}
可以看到,consumer在poll到記錄后,並不會一條條的向elastic search 發送,而是將它們放入一個BulkRequest,並在for循環結束后發送。在發送完畢后,再手動commit offsets。
執行結果為:

