Mongoに一連のレコードがコミットされると、オフセットを手動でコミットするコンシューマーを作成しています。
Mongoエラーまたはその他のエラーの場合、後日再生するために、エラー処理コレクションにレコードを永続化する試みが行われます。 Mongoがダウンしている場合、コンシューマーが一定期間処理を停止してから、Kakfaからのコミットされていないオフセットからレコードを読み取ろうとします。
以下のサンプルは機能しますが、このシナリオのベストプラクティスを教えてください。
while (true) {
boolean commit = false;
try {
ConsumerRecords<K, V> records = consumer.poll(consumerTimeout);
kafkaMessageProcessor.processRecords(records);
commit = true;
}
catch (Exception e) {
logger.error("Unable to consume closing consumer and restarting", e);
try {
consumer.close();
}
catch (Exception consumerCloseError) {
logger.error("Unable to close consumer", consumerCloseError);
}
logger.error(String.format("Attempting recovery in [%d] milliseconds.", recoveryInterval), e);
Thread.sleep(recoveryInterval);
consumer = createConsumer(properties);
}
if (commit) {
consumer.commitSync();
}
}
private KafkaConsumer<K, V> createConsumer(Properties properties) {
KafkaConsumer<K, V> consumer = new KafkaConsumer<K, V>(properties);
consumer.subscribe(topics);
return consumer;
}
コンシューマーを再作成しないと、次のエラーが発生します。
o.a.k.c.c.internals.AbstractCoordinator : Marking the coordinator 2147483647 dead.
o.a.k.c.c.internals.ConsumerCoordinator : Error ILLEGAL_GENERATION occurred while committing offsets for group test.consumer
オフセットをコミットせず、auto.commit.enableプロパティがfalseの場合、Mongoの呼び出しが失敗したときは、必要と思われる時間だけ待機し、poll()を再試行します。
表示されている問題は、新しいコンシューマがpoll()をハートビートメカニズムとして使用しているため、タイムアウトリクエストよりも長く待機すると、トピックのコーディネータがコンシューマを強制終了します。グループを再調整します。したがって、mongoを待機しますが、しばらくするとpoll()を実行したい場合があります。
編集:回避策として、このプロパティをより高いrequest.timeout.msに置くことができます
それが役に立てば幸い!
これは、クライアントバージョン0.10.0を使用したコードです。
あなたが要求するように思えます。
import Java.util.Collections;
import Java.util.HashMap;
import Java.util.Map;
import Java.util.Properties;
import Java.util.concurrent.ExecutorService;
import Java.util.concurrent.Executors;
import Java.util.concurrent.Future;
import Java.util.concurrent.TimeUnit;
import Java.util.concurrent.atomic.AtomicBoolean;
import org.Apache.kafka.clients.consumer.ConsumerRecord;
import org.Apache.kafka.clients.consumer.ConsumerRecords;
import org.Apache.kafka.clients.consumer.KafkaConsumer;
import org.Apache.kafka.clients.consumer.OffsetAndMetadata;
import org.Apache.kafka.clients.consumer.OffsetCommitCallback;
import org.Apache.kafka.common.TopicPartition;
import org.Apache.kafka.common.serialization.StringDeserializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MessageProcesser implements Runnable {
private static Logger logger = LoggerFactory.getLogger(MessageProcesser.class);
private final ExecutorService pool = Executors.newFixedThreadPool(4);
private final KafkaConsumer<String, String> consumer;
private final String topic;
private final AtomicBoolean closed = new AtomicBoolean(false);
public MessageProcesser(String groupId, String topic, String kafkaServer) {
this.topic = topic;
Properties props = new Properties();
props.put("bootstrap.servers", kafkaServer);
props.put("group.id", groupId);
props.put("key.deserializer", StringDeserializer.class.getName());
props.put("value.deserializer", StringDeserializer.class.getName());
props.put("enable.auto.commit", "false");
this.consumer = new KafkaConsumer<>(props);
}
@Override
public void run() {
try {
consumer.subscribe(Collections.singleton(topic));
while (true) {
if (closed.get()) {
consumer.close();
}
ConsumerRecords<String, String> records = consumer.poll(1000 * 60);
for (ConsumerRecord<String, String> record : records) {
String value = record.value();
if (null == value) {
continue;
}
boolean processResult = false;
try {
Future<Object> f = pool.submit(new ProcessCommand(value));
processResult = (boolean) f.get(100, TimeUnit.MILLISECONDS);
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
if (!processResult) {
//here if process fail, seek to current offset
consumer.seek(new TopicPartition(record.topic(), record.partition()), record.offset());
} else {
this.commitAsyncOffset(record);
}
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
if (!closed.get()) {
try {
Thread.sleep(100);
} catch (InterruptedException e1) {
// ignore
}
}
}
}
public void shutdown() {
closed.set(true);
}
public void commitAsyncOffset(ConsumerRecord<String, String> record) {
Map<TopicPartition, OffsetAndMetadata> offsets = new HashMap<>();
offsets.put(new TopicPartition(record.topic(), record.partition()), new OffsetAndMetadata(record.offset() + 1));
consumer.commitAsync(offsets, new OffsetCommitCallback() {
@Override
public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception e) {
if (e != null) {
logger.error("kafka offset commit fail. {} {}", offsets, PushUtil.getStackString(e.getStackTrace()));
}
}
});
}
}
私が理解しているように、(新しい)クライアントは、消費されたオフセットを保持するクライアントです。コミットはオフセットをサーバーに送信しますが、クライアントがサーバーに「そのオフセットの次のメッセージを送ってください」と言うため、そのクライアントからの次のポーリングには影響しません。オフセットがサーバーに送信されるのはなぜですか?次のリバランスのために。したがって、サーバーがコミットされたオフセットを使用する唯一の状況は、一部のクライアントが停止または切断したときです。パーティションが再調整され、クライアントはサーバーからオフセットを取得します。
したがって、オフセットをコミットせずにpoll()を呼び出すと、メッセージが再度読み取られることは期待できません。これには、クライアントでオフセットをロールバックする可能性がなければなりません。私は試しませんでしたが、失敗したメッセージのオフセットに対してKafkaConsumer.seekを呼び出すとうまくいくはずです。
ところで、このようにして、最後に正常に処理されたメッセージをコミットし、最初の失敗にシークすることもできます。そのため、途中でエラーが発生したときに、レコードリスト全体を繰り返す必要はありません。