spring-kafka について
Spring Kafka 開発者向けのクイックリファレンススキルです。プロデューサー、コンシューマー、エラー処理、テストの例に加え、信頼性設定とトラブルシューティングを扱います。
主な機能
- 01KafkaTemplate によるメッセージ送信
- 02リスナーとバッチ受信
- 03リトライトピックとデッドレター処理
- 04EmbeddedKafka と Testcontainers によるテスト
- 05本番チェックリストとトラブルシューティング
活用シーン
- 01Spring アプリケーションでの Kafka メッセージ送信
- 02単一またはバッチコンシューマーの構築
- 03リトライとデッドレタートピックの設定
- 04Kafka 連携のテストと受信問題の診断
MCP 対応
README では、Kafka の詳細ドキュメント取得先として `mcp__documentation__fetch_docs` を案内しています。
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ claude-dev-suite/ claude-dev-suite --skill spring-kafkaインストール後、Claude Code で/spring-kafkaを入力して読み込まれたか確認します。
詳細
Spring Kafka - クイックリファレンス
詳細な知識:テクノロジーに
kafkaを指定してmcp__documentation__fetch_docsを使用すると、包括的なドキュメントを参照できます。
依存関係
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
設定
application.yml
spring:
kafka:
bootstrap-servers: localhost:9092
consumer:
group-id: my-group
auto-offset-reset: earliest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
properties:
spring.json.trusted.packages: "com.example.dto"
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
acks: all
properties:
enable.idempotence: true
listener:
ack-mode: manual
concurrency: 3
プロデューサーパターン
KafkaTemplate
@Service
@RequiredArgsConstructor
public class OrderProducer {
private final KafkaTemplate<String, OrderEvent> kafkaTemplate;
public void sendOrder(OrderEvent event) {
kafkaTemplate.send("orders", event.getOrderId(), event)
.whenComplete((result, ex) -> {
if (ex != null) {
log.error("Failed to send order: {}", event.getOrderId(), ex);
} else {
log.info("Order sent: {} to partition {}",
event.getOrderId(),
result.getRecordMetadata().partition());
}
});
}
// With headers
public void sendWithHeaders(OrderEvent event, String correlationId) {
ProducerRecord<String, OrderEvent> record = new ProducerRecord<>(
"orders", event.getOrderId(), event);
record.headers()
.add("correlation-id", correlationId.getBytes())
.add("source", "order-service".getBytes());
kafkaTemplate.send(record);
}
}
トランザクショナルプロデューサー
@Configuration
public class KafkaConfig {
@Bean
public ProducerFactory<String, Object> producerFactory() {
Map<String, Object> config = new HashMap<>();
config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
config.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "tx-");
config.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
return new DefaultKafkaProducerFactory<>(config);
}
@Bean
public KafkaTemplate<String, Object> kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}
@Bean
public KafkaTransactionManager<String, Object> kafkaTransactionManager() {
return new KafkaTransactionManager<>(producerFactory());
}
}
@Service
@Transactional("kafkaTransactionManager")
public class TransactionalProducer {
public void sendMultiple(List<OrderEvent> events) {
events.forEach(e -> kafkaTemplate.send("orders", e.getId(), e));
}
}
コンシューマーパターン
基本的な @KafkaListener
@Service
@RequiredArgsConstructor
public class OrderConsumer {
@KafkaListener(topics = "orders", groupId = "order-processor")
public void consume(
@Payload OrderEvent event,
@Header(KafkaHeaders.RECEIVED_KEY) String key,
@Header(KafkaHeaders.RECEIVED_PARTITION) int partition,
@Header(KafkaHeaders.OFFSET) long offset,
Acknowledgment ack) {
log.info("Received order: {} from partition {} offset {}",
event.getOrderId(), partition, offset);
try {
processOrder(event);
ack.acknowledge();
} catch (Exception e) {
log.error("Failed to process order: {}", event.getOrderId(), e);
throw e; // Will trigger retry
}
}
}
バッチコンシューマー
@KafkaListener(
topics = "orders",
groupId = "batch-processor",
containerFactory = "batchKafkaListenerContainerFactory"
)
public void consumeBatch(
List<OrderEvent> events,
@Header(KafkaHeaders.RECEIVED_PARTITION) List<Integer> partitions,
Acknowledgment ack) {
log.info("Received batch of {} orders", events.size());
processBatch(events);
ack.acknowledge();
}
@Bean
public ConcurrentKafkaListenerContainerFactory<String, OrderEvent> batchKafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, OrderEvent> factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
factory.setBatchListener(true);
factory.getContainerProperties().setAckMode(AckMode.MANUAL);
return factory;
}
クラスレベルのリスナー
@KafkaListener(topics = "orders", groupId = "order-handler")
@Service
public class OrderHandler {
@KafkaHandler
public void handleCreated(OrderCreatedEvent event) {
// Handle order created
}
@KafkaHandler
public void handleUpdated(OrderUpdatedEvent event) {
// Handle order updated
}
@KafkaHandler(isDefault = true)
public void handleDefault(Object event) {
log.warn("Unknown event type: {}", event.getClass());
}
}
リトライトピック(Spring Kafka 3.x)
@RetryableTopic
@RetryableTopic(
attempts = "3",
backoff = @Backoff(delay = 1000, multiplier = 2.0, maxDelay = 10000),
dltStrategy = DltStrategy.FAIL_ON_ERROR,
autoCreateTopics = "true",
topicSuffixingStrategy = TopicSuffixingStrategy.SUFFIX_WITH_INDEX_VALUE
)
@KafkaListener(topics = "orders", groupId = "retry-consumer")
public void consumeWithRetry(OrderEvent event, Acknowledgment ack) {
processOrder(event);
ack.acknowledge();
}
@DltHandler
public void handleDlt(OrderEvent event,
@Header(KafkaHeaders.RECEIVED_TOPIC) String topic,
@Header(KafkaHeaders.EXCEPTION_MESSAGE) String errorMessage) {
log.error("DLT received: {} from {} - error: {}",
event.getOrderId(), topic, errorMessage);
// Store in database for manual review
failedOrderRepository.save(new FailedOrder(event, errorMessage));
}
手動リトライ設定
@Configuration
@EnableKafka
public class KafkaRetryConfig {
@Bean
public RetryTopicConfiguration retryTopicConfiguration(KafkaTemplate<String, Object> template) {
return RetryTopicConfigurationBuilder
.newInstance()
.maxAttempts(4)
.fixedBackOff(3000)
.includeTopic("orders")
.doNotAutoCreateRetryTopics()
.create(template);
}
}
エラー処理
カスタムエラーハンドラー
@Bean
public DefaultErrorHandler errorHandler(KafkaTemplate<String, Object> template) {
// Send to DLT after 3 retries
DeadLetterPublishingRecoverer recoverer = new DeadLetterPublishingRecoverer(template,
(record, ex) -> new TopicPartition(record.topic() + ".DLT", record.partition()));
DefaultErrorHandler handler = new DefaultErrorHandler(recoverer,
new FixedBackOff(1000L, 3L));
// Don't retry for these exceptions
handler.addNotRetryableExceptions(
ValidationException.class,
DeserializationException.class
);
return handler;
}
@Bean
public ConcurrentKafkaListenerContainerFactory<String, Object> kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, Object> factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
factory.setCommonErrorHandler(errorHandler(kafkaTemplate()));
return factory;
}
テスト
@EmbeddedKafka
@SpringBootTest
@EmbeddedKafka(partitions = 1, topics = {"orders"})
class OrderProducerTest {
@Autowired
private EmbeddedKafkaBroker embeddedKafka;
@Autowired
private OrderProducer orderProducer;
@Test
void shouldSendOrder() throws Exception {
OrderEvent event = new OrderEvent("123", "CREATED");
Map<String, Object> consumerProps = KafkaTestUtils.consumerProps(
"test-group", "true", embeddedKafka);
ConsumerFactory<String, OrderEvent> cf = new DefaultKafkaConsumerFactory<>(consumerProps);
Consumer<String, OrderEvent> consumer = cf.createConsumer();
embeddedKafka.consumeFromAnEmbeddedTopic(consumer, "orders");
orderProducer.sendOrder(event);
ConsumerRecord<String, OrderEvent> record = KafkaTestUtils.getSingleRecord(consumer, "orders");
assertThat(record.value().getOrderId()).isEqualTo("123");
}
}
CountDownLatch を使用したコンシューマーテスト
@SpringBootTest
@EmbeddedKafka(partitions = 1, topics = {"orders"})
class OrderConsumerTest {
@SpyBean
private OrderConsumer orderConsumer;
@Autowired
private EmbeddedKafkaBroker embeddedKafka;
@Test
void shouldConsumeAndProcessOrder() throws Exception {
CountDownLatch latch = new CountDownLatch(1);
doAnswer(inv -> { inv.callRealMethod(); latch.countDown(); return null; })
.when(orderConsumer).consume(any(), any());
Map<String, Object> props = KafkaTestUtils.producerProps(embeddedKafka);
KafkaTemplate<String, String> template = new KafkaTemplate<>(
new DefaultKafkaProducerFactory<>(props));
template.send("orders", "{\"orderId\":\"456\",\"status\":\"CREATED\"}");
assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue();
verify(orderConsumer).consume(any(), any());
}
}
Testcontainers
@SpringBootTest
@Testcontainers
class OrderIntegrationTest {
@Container
@ServiceConnection
static KafkaContainer kafka = new KafkaContainer(
DockerImageName.parse("apache/kafka-native:3.8.0"));
@Autowired
private KafkaTemplate<String, OrderEvent> kafkaTemplate;
@Autowired
private OrderRepository orderRepository;
@Test
void shouldProcessOrderEndToEnd() throws Exception {
kafkaTemplate.send("orders", "key-1",
new OrderEvent("789", "CREATED")).get(10, TimeUnit.SECONDS);
await().atMost(Duration.ofSeconds(10))
.untilAsserted(() -> {
Optional<Order> order = orderRepository.findById("789");
assertThat(order).isPresent();
assertThat(order.get().getStatus()).isEqualTo("CREATED");
});
}
}
詳細:MockConsumer/MockProducer、@EmbeddedKafka の高度なパターン、Node.js/Python での Kafka テストについては、
messaging-testing-kafkaスキルを参照してください。
ベストプラクティス
| 推奨事項 | 非推奨事項 |
|---|---|
耐久性のために acks=all を使用する | 本番環境で acks=0 を使用する |
| 冪等性を有効にする | 重複メッセージを無視する |
| 失敗に備えて DLT を設定する | 失敗したメッセージを黙って破棄する |
| 手動確認応答を使用する | 処理せずに自動コミットする |
| 適切なデシリアライザーの信頼設定を行う | すべてのパッケージを信頼する |
本番環境チェックリスト
-
acks=allを設定済み - 冪等性を有効化済み
- コンシューマーグループ ID を設定済み
- 手動確認応答モードを設定済み
- リトライトピックを設定済み
- DLT ハンドラーを実装済み
- エラーハンドラーを設定済み
- 適切なシリアライザーを設定済み
- 信頼するパッケージを設定済み
- 監視メトリクスを公開済み
このスキルを使用しない場合
- Raw Kafka - ブローカー設定には
kafkaスキルを使用してください - RabbitMQ - 代わりに
spring-amqpを使用してください - シンプルなメッセージング - Spring Events を検討してください
- Kafka Streams - 追加のスキルが必要な場合があります
アンチパターン
| アンチパターン | 問題 | 解決策 |
|---|---|---|
| 自動コミット | メッセージの損失 | 手動 ack を使用する |
| エラーハンドラーなし | エラーが通知されない | エラーハンドラーを設定する |
| DLT なし | 失敗したメッセージの損失 | デッドレタートピックを追加する |
| リスナー内でブロッキング | コンシューマーの遅延 | 非同期処理を使用する |
| 誤ったデシリアライザー | 消費時のエラー | プロデューサーのシリアライザーに合わせる |
| 冪等性なし | 重複処理 | 冪等性のあるコンシューマーを実装する |
トラブルシューティング
| 問題 | 診断 | 対処方法 |
|---|---|---|
| コンシューマーが受信しない | group.id を確認する | コンシューマーグループを確認する |
| シリアライズエラー | 値の型を確認する | 正しいデシリアライザーを設定する |
| 頻繁なリバランス | session.timeout を確認する | タイムアウトを増やす |
| コンシューマーの遅延 | 処理時間を確認する | コンシューマーを最適化するか、スケールする |
| DLT にメッセージがある | エラーログを確認する | 処理エラーを修正する |
リファレンスドキュメント
コメント
Markdown 対応