분산 시스템은 네트워크상에서 연결된 컴퓨터들의 그룹, 단일 시스템이 갖지 못한 높은 성능이 목표
성능이 높다는 장점 외에도 하나의 서버 또는 노드 등에 장애 발생 시 다른 서버 또는 노드가 대신 처리하는 고가용성이 있으며, 부하가 높은 경우 시스템 확장이 용이하다는 장점
카프카 또한 분산 시스템이므로 더 높은 처리량이 요구되는 경우, 브로커를 추가하는 방식으로 확장 가능하며, 온라인 상태에서도 쉽게 확장 가능
페이지 캐시
카프카는 높은 처리량을 얻기 위해 OS의 페이지 캐시를 활용하는 방식으로 설계됨
페이지 캐시는 직접 디스크에 읽고 쓰는 대신 물리 메모리 중 애플리케이션이 사용하지 않는 일부 잔여 메모리를 활용해 디스크 I/O에 대한 접근을 줄여 성능을 향상시킴
카프카, 페이지 캐시, 디스크의 읽고 쓰기를 표현한 이미지, 카프카가 OS의 페이지 캐시를 이용해 카프카가 직접 디스크에 읽고 쓰기르 ㄹ 하지 않고, 페이지 캐시를 통해 읽고 쓰기를 한다
배치 전송 처리
카프카는 프로듀서, 컨슈머 클라이언트들과 서로 통신하며, 메시지를 주고 받는데 이때 발생하는 수많은 통신을 묶어서 처리해, 네트워크 오버헤드를 줄이고, 더욱 빠르고 효율적으로 메시지를 처리
압축 전송
카프카는 메시지 전송 시 좀 더 성능이 높은 압축 전송을 사용하는 것을 권장하는데, 이때 gzip, snappy, lz4, zstd 압축 타입을 지원
압축만으로도 네트워크 대역폭이나 회선 비용 등을 줄일 수 있으며, 배치 전송과 결합해 사용하면 더욱 높은 효과를 얻음
일반적으로 높은 압축률이 필요한 경우라면 gzip, zstd, 빠른 응답 속도가 필요하다면 lz4, snappy 권장
토픽, 파티션, 오프셋
카프카는 토픽에 데이터를 저장하고, 토픽은 이메일 주소 정도의 개념
토픽은 병렬 처리를 위해 여러 개의 파티션 단위로 나뉘며, 파티셔닝을 통해 단 하나의 토픽이라도 높은 처리량 수행이 가능
이 파티션의 메시지가 저장되는 위치를 오프셋 이라 하며, 오프셋은 순차적으로 증가하는 숫자 형태(64비트 정수)
3개의 파티션을 가진 이미지이며, 전송되는 메시지들의 쓰기 동작이 각 파티션별로 이뤄짐
파티션에서의오프셋은 고유한 숫자로, 오프셋을 통해 메시지의 순서를 보장하고 컨슈머에서는 마지막까지 읽은 위치를 알 수 있다,./
고가용성 보장
카프카는 분산 시스템이기에 하나의 서버나 노드가 다운되어도 다른 서버 또는 노드가 장애가 발생한 서버의 역할을 대신해 안정적인 서비스 가능
고가용성을 보장하기 위해 카프카에서는 리플리케이션 기능 제공
카프카에서 제공하는 리플리케이션 기능은 토픽 자체를 복제하는게 아니라, 토픽의 파티션을 복제하는 것
원본과 리플리케이션을 구분하기 위해 카프카에서는 리더와 팔로워라고 부름
팔로워가 많을 수록 브로커의 디스크 공간도 소비되므로 이상적인 리플리케이션 팩터 수 유지가 중요, 일반적으로는 3이 권장값
리더는 프로듀서, 컨슈머로부터 오는 모든 읽기와 쓰기 요청을 처리하며, 팔로워는 오직 리더로부터 리플리케이션만 받음
주키퍼의 의존성
주키퍼는 여러 대의 서버를 앙상블(클러스터)로 구성하고, 살아 있는 노드 수가 과반수 이상 유지된다면 지속적인 서비스가 가능한 구조
주키퍼는 반드시 홀수로 구성
지노드(znode)를 이용해 카프카의 메타 정보가 주키퍼에 기록되며, 주키퍼는 이러한 지노드를 이용해 브로커의 노드 관리, 토픽 관리, 컨트롤러 관리 등의 역할 수행
프로듀서 기본 동작과 예제 맛보가ㅣ
프로듀서 디자인
ProducerRecord : 카프카로 전송하기 위한 실제 데이터이며, 토픽, 파티션, 키, 밸류로 구성
프로듀서가 카프카로 레코드 전송 시, 카프카의 특정 토픽으로 메시지를 전송하기 때문에 토픽과 밸류는 필숫값이며, 특정 파티션을 지정하기 위한 레코드의 파티션과 특정 파티션에 레코드들을 정렬하기 위한 레코드의 키는 옵셔널
각 레코드들은 프로듀서의 send() 메소드를 통해 Serializer, Partitioner를 거치며, 프로듀서 레코드의 선택사항인 파티션을 지정했다면, 파티셔너는 아무 동작도 하지 않고 지정된 파티션으로 레코드 전달하며, 기본적으로는 Round Robin 방식으로 동작
send() 메소드 동작 이후 배치 전송을 위해 레코드들을 파티션별로 잠시 모아두고, 전송 실패 시 재시도 동작이 이뤄지고, 전송이 성공하면 메타데이터를 리턴
프로듀서의 주요 옵션
옵션
설명
bootstrap.server
클라이언트가 카프카 클러스터에 처음 연결하기 위한 호스트&포트 정보
client.dns.lookup
하나의 호스트에 여러 IP를 매핑해 사용하는 일부 환경에서 클라이언트가 하나의 IP와 연결하지 못할 경우에 다른 IP로 시도하는 설정
acks
프로듀서가 카프카 토픽의 리더 측에 메시지를 전송한 후 요청을 완료하기를 결정하는 옵션.0,1,all(-1)로 표현하며, 0은 리더 측에서 메시지를 받았는지 확인하지 않는다. 따라서 전송이 빠르지만, 일부 메시지가 유실될 가능성이 있다. 1은 리더가 메시지를 받았는지 확인하지만, 모든 팔로워 전부를 확인하진 않는다. 대부분 기본값으로 1을 사용한다. all은 팔로워가 메시지를 받았는지 여부를 확인한다.
buffer.memory
카프카 서버로 데이터를 보내기 위해 잠시 대기(배치 전송이나 딜레이 등)할 수 있는 전체 메모리 바이트
compression.type
프로듀서가 메시지 전송 시 선택할 수 있는 압축 타입. (none, gzip, snappy, lz4, zstd)
enable.idempotence
설정을 true로 하면 중복 없는 전송이 가능함(중복 없는 전송은 max.flight.requests.per.connection은 5이하, retries는 0이상, acks는 all로 설정해야함)
max.in.flight.request.per.connection
하나의 커넥션에서 프로듀서가 최대한 ACK 없이 전송할 수 있는 요청 수. 메시지 순서가 중요하다면 1로 설정할 것을 권장하지만, 성능은 다소 떨어진다.
retries
전송에 실패한 데이터를 다시 보내는 횟수
batch.size
배치 전송할 배치 크기
linger.ms
배치 크기에 도달하지 못한 상황에서 linger.ms 제한 시간에 도달했을 때 메시지를 전송한다
transactional.id
'정확히 한번 전송'을 위해 사용하는 옵션이며, 동일한 TransactionalId에 한해 정확히 한 번을 보장한다. 옵션을 사용하기 전 enable.idempotence를 true로 설정해야 함
프로듀서의 전송 방법은 메시지를 보내고 확인하지 않기, 동기 전송, 비동기 전송 존재
메시지를 보내고 확인하지 않기 예제
public class ProducerFireForgot {
public static void main(String[] args) {
Properties props = new Properties(); // Properties 객체 생성
props.put("bootstrap.servers", "peter-kafka01.foo.bar:9092,peter-kafka02.foo.bar:9092,peter-kafka03.foo.bar:9092"); // 브로커 리스트 정의
props.put("key.serializer",
"org.apache.kafka.common.serialization.StringSerializer"); //메시지 키와 벨류에 문자열을 지정하므로 내장된 StringSerializer를 지정합니다.
props.put("value.serializer",
"org.apache.kafka.common.serialization.StringSerializer");
Producer<String, String> producer = new KafkaProducer<>(props); //Properties 오브젝트를 전달해 새 프로듀서를 생성합니다.
try {
for (int i = 0; i < 3; i++) {
ProducerRecord<String, String> record = new ProducerRecord<>("peter-basic01", "Apache Kafka is a distributed streaming platform - " + i); //ProducerRecord 오브젝트를 생성합니다.
producer.send(record); //send()메소드를 사용하여 메시지를 전송 후 Java Future Ojbect로 RecordMetadata를 리턴 받지만, 리턴값을 무시하므로 메시지가 성공적으로 전송되었는지 알 수 없습니다.
}
} catch (Exception e){
e.printStackTrace(); //카프카 브로커에게 메시지를 전송한 후의 에러는 무시하지만, 전송 전 에러가 발생하면 예외를 처리할 수 있습니다.
} finally {
producer.close(); // 프로듀서 종료
}
}
}
동기 전송 예제
public class ProducerSync {
public static void main(String[] args) {
Properties props = new Properties(); //Properties 오브젝트를 시작합니다.
props.put("bootstrap.servers", "peter-kafka01.foo.bar:9092,peter-kafka02.foo.bar:9092,peter-kafka03.foo.bar:9092"); //브로커 리스트를 정의합니다.
props.put("key.serializer",
"org.apache.kafka.common.serialization.StringSerializer"); //메시지 키와 벨류에 문자열을 지정하므로 내장된 StringSerializer를 지정합니다.
props.put("value.serializer",
"org.apache.kafka.common.serialization.StringSerializer");
Producer<String, String> producer = new KafkaProducer<>(props); //Properties 오브젝트를 전달해 새 프로듀서를 생성합니다.
try {
for (int i = 0; i < 3; i++) {
ProducerRecord<String, String> record = new ProducerRecord<>("peter-basic01", "Apache Kafka is a distributed streaming platform - " + i); //ProducerRecord 오브젝트를 생성합니다.
RecordMetadata metadata = producer.send(record).get(); //get() 메소드를 이용해 카프카의 응답을 기다립니다. 메시지가 성공적으로 전송되지 않으면 예외가 발생하고, 에러가 없다면 RecordMetadata를 얻게 됩니다.
System.out.printf("Topic: %s, Partition: %d, Offset: %d, Key: %s, Received Message: %s\n", metadata.topic(), metadata.partition()
, metadata.offset(), record.key(), record.value());
}
} catch (Exception e){
e.printStackTrace(); //카프카로 메시지를 보내기 전과 보내는 동안 에러가 발생하면 예외가 발생합니다.
} finally {
producer.close(); // 프로듀서 종료
}
}
}
주요 차이점은 프로듀서는 메시지를 보내고 send() 메소드의 Future 객체를 리턴하며, get() 메소드를 이용해 Future를 기다린 후 send()가 성공했는지 실패했는지 여부를 확인
ProducerRecord 전송이 성공하고 나면 RecordMetadata를 읽어 들여 파티션과 오프셋 정보 확인이 가능해 메시지 전달의 성공 여부를 파악할 수 있다.
비동기 전송을 위한 콜백
public class PeterProducerCallback implements Callback { //콜백을 사용하기 위해 org.apache.kafka.clients.producer.Callback를 구현하는 클래스가 필요합니다.
private ProducerRecord<String, String> record;
public PeterProducerCallback(ProducerRecord<String, String> record) {
this.record = record;
}
@Override
public void onCompletion(RecordMetadata metadata, Exception e) {
if (e != null) {
e.printStackTrace(); //카프카가 오류를 리턴하면 onCompletion()은 예외를 갖게 되며, 실제 운영환경에서는 추가적인 예외처리가 필요합니다.
} else {
System.out.printf("Topic: %s, Partition: %d, Offset: %d, Key: %s, Received Message: %s\n", metadata.topic(), metadata.partition()
, metadata.offset(), record.key(), record.value());
}
}
}
비동기 예제
public class ProducerAsync {
public static void main(String[] args) {
Properties props = new Properties(); //Properties 오브젝트를 시작합니다.
props.put("bootstrap.servers", "peter-kafka01.foo.bar:9092,peter-kafka02.foo.bar:9092,peter-kafka03.foo.bar:9092"); //브로커 리스트를 정의합니다.
props.put("key.serializer",
"org.apache.kafka.common.serialization.StringSerializer"); //메시지 키와 벨류에 문자열을 지정하므로 내장된 StringSerializer를 지정합니다.
props.put("value.serializer",
"org.apache.kafka.common.serialization.StringSerializer");
Producer<String, String> producer = new KafkaProducer<>(props); //Properties 오브젝트를 전달해 새 프로듀서를 생성합니다.
try {
for (int i = 0; i < 3; i++) {
ProducerRecord<String, String> record = new ProducerRecord<>("peter-basic01", "Apache Kafka is a distributed streaming platform - " + i); //ProducerRecord 오브젝트를 생성합니다.
producer.send(record, new PeterProducerCallback(record)); //프로듀서에서 레코드를 보낼 때 콜백 오브젝트를 같이 보냅니다.
}
} catch (Exception e){
e.printStackTrace();
} finally {
producer.close(); // 프로듀서 종료
}
}
}
프로듀서는 send() 메소드와 콜백을 함께 호출
동기 전송보다 빠른 전송이 가능하고, 메시지 전송이 실패한 경우에도 예외 처리할 수 있다.
컨슈머의 기본 동작과 예제 맛보기
컨슈머는 카프카의 토픽에 저장되어 있는 메시지를 가져오는 역할을 담당
내부적으로는 컨슈머 그룹, 리밸런싱 등 여러 동작을 수행
프로듀서가 카프카의 토픽으로 메시지 전송 시 메시지들은 브로커들의 로컬 디스크에 저장되며, 컨슈머는 로컬 디스크에 저장된 메시지를 컨슈밍
컨슈머 그룹은 하나 이상의 컨슈머들이 모여 있는 그룹이며, 컨슈머는 반드시 컨슈머 그룹에 속한다.
이 컨슈머 그룹은 각 파티션의 리더에게 카프카 토픽에 저장된 메시지를 가져오기 위한 요청을 보내며, 이때 파티션 수와 컨슈머 수(하나의 컨슈머 그룹 내 컨슈머 수)는 1:1로 매핑하는 것이 이상적이다.
컨슈머 수가 파티션 수보다 많다고 더 빠르게 토픽의 메시지를 가져오거나 처리량이 높아지는 것이 아님 (더 많은 수의 컨슈머들이 그냥 대기 상태로만 존재)
Active/Standby 개념으로 추가 컨슈머가 있으면 좋을 것이라고 생각할 수 있지만, 컨슈머 그룹 내에서 리밸런싱 동작을 통해 장애가 발생한 컨슈머의 역할을 동일한 그룹에 있는 다른 컨슈머가 그 역할을 대신 수행하므로 굳이 추가 컨슈머 리소스 할당은 필요없음
컨슈머의 주요 옵션
컨슈머 옵션
설명
bootstrap.servers
브로커의 정보
fetch.min.bytes
한 번에 가져올 수 있는 최소 데이터 크기. 만약 지정한 크기보다 작은 경우, 요청에 응답하지 않고 데이터가 누적될 때까지 기다린다.
group.id
컨슈머가 속한 컨슈머 그룹을 식별하는 식별자. 동일한 그룹 내의 컨슈머 정보는 모두 공유된다.
heartbeat.interval.ms
하트비트가 있다는 것은 컨슈머의 상태가 active임을 의미. 일반적으로 session.timeout.ms의 1/3로 설정한다.
max.partition.fetch.bytes
파티션당 가져올 수 있는 최대 크기
session.timeout.ms
이 시간을 이용해, 컨슈머가 종료된 것인지를 파악한다. 컨슈머는 주기적으로 하트비트를 보내야 하고, 만약 이 시간 전까지 하트비트를 보내지 않았다면 해당 컨슈머는 종료된 것으로 간주하고 컨슈머 그룹에서 제외하고 리밸런싱을 시작한다.
enable.auto.commit
백그라운드로 주기적으로 오프셋을 커밋한다.
auto.offset.reset
카프카에서 초기 오프셋이 없거나 현재 오프셋이 더 이상 존재하지 않는 경우에 다음 옵션으로 reset한다.- earliest: 가장 초기의 오프셋값으로 설정- latest: 가장 마지막의 오프셋값으로 설정.- none: 이전 오프셋값을 찾지 못하면 에러 발생
fetch.max.bytes
한 번의 가져오기 요청으로 가져올 수 있는 최대 크기
group.instance.id
컨슈머의 고유한 식별자. 만약 설정한다면 static 멤버로 간주되어, 불필요한 리밸런싱을 하지 않는다
isolation.level
트랜잭션 컨슈머에서 사용되는 옵션으로, read_uncommitted는 기본값으로 모든 메시지를 읽고, read_committed는 트랜잭션이 완료된 메시지만 읽는다.
max.poll.records
한 번의 poll() 요청으로 가져오는 최대 메시지의 수
partition.assignment.strategy
파티션 할당 전략이며, 기본값은 range
fetch.max.wait.ms
fetch.min.bytes에 의해 설정된 데이터보다 적은 경우 요청에 대한 응답을 기다리는 최대 시간
컨슈머 예제
컨슈머에서 메시지를 가져오는 방법은 크게 세 가지
오토 커밋
동기 가져오기
비동기 가져오기
오토 커밋 예제
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import java.util.Arrays;
import java.util.Properties;
public class ConsumerAuto {
public static void main(String[] args) {
Properties props = new Properties(); //Properties 오브젝트를 시작합니다.
props.put("bootstrap.servers", "peter-kafka01.foo.bar:9092,peter-kafka02.foo.bar:9092,peter-kafka03.foo.bar:9092"); //브로커 리스트를 정의합니다.
props.put("group.id", "peter-consumer01"); //컨슈머 그룹 아이디 정의합니다.
props.put("enable.auto.commit", "true"); //자동 커밋을 사용합니다.
props.put("auto.offset.reset", "latest"); //컨슈머 오프셋을 찾지 못하는 경우 latest로 초기화 합니다. 가장 최근부터 메시지를 가져오게 됩니다.
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); //문자열을 사용했으므로 StringDeserializer 지정합니다.
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props); //Properties 오브젝트를 전달하여 새 컨슈머를 생성합니다.
consumer.subscribe(Arrays.asList("peter-basic01")); //구독할 토픽을 지정합니다.
try {
while (true) { //무한 루프 시작입니다. 메시지를 가져오기 위해 카프카에 지속적으로 poll()을 하게 됩니다.
ConsumerRecords<String, String> records = consumer.poll(1000); //컨슈머는 폴링하는 것을 계속 유지하며, 타임 아웃 주기를 설정합니다.해당 시간만큼 블럭합니다.
for (ConsumerRecord<String, String> record : records) { //poll()은 레코드 전체를 리턴하고, 하나의 메시지만 가져오는 것이 아니므로, 반복문 처리합니다.
System.out.printf("Topic: %s, Partition: %s, Offset: %d, Key: %s, Value: %s\n",
record.topic(), record.partition(), record.offset(), record.key(), record.value());
}
}
} catch (Exception e){
e.printStackTrace();
} finally {
consumer.close(); //컨슈머를 종료합니다.
}
}
}
컨슈머 애플리케이션들의 기본값으로 가장 많이 사용되고 있는 것이 오토 커밋
오토 커밋은 오프셋을 주기적으로 커밋하므로 관리자가 오프셋을 따로 관리하지 않아도 된다는 장점이 있지만, 컨슈머 종료 등이 빈번히 일어나면 일부 메시지를 못 가져오거나 중복으로 가져오는 경우가 발생
동기 가져오기 예제
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import java.util.Arrays;
import java.util.Properties;
public class ConsumerSync {
public static void main(String[] args) {
Properties props = new Properties(); //Properties 오브젝트를 시작합니다.
props.put("bootstrap.servers", "peter-kafka01.foo.bar:9092,peter-kafka02.foo.bar:9092,peter-kafka03.foo.bar:9092"); //브로커 리스트를 정의합니다.
props.put("group.id", "peter-consumer01"); //컨슈머 그룹 아이디 정의합니다.
props.put("enable.auto.commit", "false"); //자동 커밋을 사용하지 않습니다.
props.put("auto.offset.reset", "latest"); //컨슈머 오프셋을 찾지 못하는 경우 latest로 초기화 합니다. 가장 최근부터 메시지를 가져오게 됩니다.
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); //문자열을 사용했으므로 StringDeserializer 지정합니다.
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props); //Properties 오브젝트를 전달하여 새 컨슈머를 생성합니다.
consumer.subscribe(Arrays.asList("peter-basic01")); //구독할 토픽을 지정합니다.
try {
while (true) { //무한 루프 시작입니다. 메시지를 가져오기 위해 카프카에 지속적으로 poll()을 하게 됩니다.
ConsumerRecords<String, String> records = consumer.poll(1000); //컨슈머는 폴링하는 것을 계속 유지하며, 타임 아웃 주기를 설정합니다.해당 시간만큼 블럭합니다.
for (ConsumerRecord<String, String> record : records) { //poll()은 레코드 전체를 리턴하고, 하나의 메시지만 가져오는 것이 아니므로, 반복문 처리합니다.
System.out.printf("Topic: %s, Partition: %s, Offset: %d, Key: %s, Value: %s\n",
record.topic(), record.partition(), record.offset(), record.key(), record.value());
}
consumer.commitSync(); //현재 배치를 통해 읽은 모든 메시지들을 처리한 후, 추가 메시지를 폴링하기 전 현재의 오프셋을 커밋합니다.
}
} catch (Exception e){
e.printStackTrace();
} finally {
consumer.close(); //컨슈머를 종료합니다.
}
}
}
메시지를 가져온 후 처리까지 완료하고 현재의 오프셋을 직접 커밋
동기 방식으로 가져오는 경우 속도는 느리지만, 메시지 손실은 거의 발생하지 않음
동기 방식도 메시지 중복 이슈는 피할 수 없다.
메시지 손실 : 실제로 토픽에는 메시지가 존재하지만 잘못된 오프셋 커밋으로 인한 위치 변경으로 컨슈머가 메시지를 가져오지 못하는 경우
비동기 가져오기 예제
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import java.util.Arrays;
import java.util.Properties;
public class ConsumerAsync {
public static void main(String[] args) {
Properties props = new Properties(); //Properties 오브젝트를 시작합니다.
props.put("bootstrap.servers", "peter-kafka01.foo.bar:9092,peter-kafka02.foo.bar:9092,peter-kafka03.foo.bar:9092"); //브로커 리스트를 정의합니다.
props.put("group.id", "peter-consumer01"); //컨슈머 그룹 아이디 정의합니다.
props.put("enable.auto.commit", "false"); //자동 커밋을 사용하지 않습니다.
props.put("auto.offset.reset", "latest"); //컨슈머 오프셋을 찾지 못하는 경우 latest로 초기화 합니다. 가장 최근부터 메시지를 가져오게 됩니다.
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); //문자열을 사용했으므로 StringDeserializer 지정합니다.
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props); //Properties 오브젝트를 전달하여 새 컨슈머를 생성합니다.
consumer.subscribe(Arrays.asList("peter-basic01")); //구독할 토픽을 지정합니다.
try {
while (true) { //무한 루프 시작입니다. 메시지를 가져오기 위해 카프카에 지속적으로 poll()을 하게 됩니다.
ConsumerRecords<String, String> records = consumer.poll(1000); //컨슈머는 폴링하는 것을 계속 유지하며, 타임 아웃 주기를 설정합니다.해당 시간만큼 블럭합니다.
for (ConsumerRecord<String, String> record : records) { //poll()은 레코드 전체를 리턴하고, 하나의 메시지만 가져오는 것이 아니므로, 반복문 처리합니다.
System.out.printf("Topic: %s, Partition: %s, Offset: %d, Key: %s, Value: %s\n",
record.topic(), record.partition(), record.offset(), record.key(), record.value());
}
consumer.commitAsync(); //현재 배치를 통해 읽은 모든 메시지들을 처리한 후, 추가 메시지를 폴링하기 전 현재의 오프셋을 비동기 커밋합니다.
}
} catch (Exception e){
e.printStackTrace();
} finally {
consumer.close(); //컨슈머를 종료합니다.
}
}
}
commitAsync() 메소드는 commitSync()와 달리 오프셋 커밋을 실패하더라도 재시도하지 않는다.
재시도하지 않는 이유 (총 10개 메시지 있고 오프셋 1~10번까지 순차적으로 커밋한다는 상황 가정 + 비동기)
1번 오프셋 메시지 읽은 뒤 1번 오프셋 비동기 커밋(현재 마지막 오프셋 1)
2번 오프셋 메시지 읽은 뒤 2번 오프셋 비동기 커밋하지만 실패(현재 마지막 오프셋 1)
3번 오프셋 메시지 읽은 뒤 3번 오프셋 비동기 커밋하지만 실패(현재 마지막 오프셋 1)
4번 오프셋 메시지 읽은 뒤 4번 오프셋 비동기 커밋하지만 실패(현재 마지막 오프셋 1)
5번 오프셋 메시지 읽은 뒤 5번 오프셋 비동기 커밋(현재 마지막 오프셋 5)
비동기 커밋 재시도로 2번 오프셋 비동기 커밋이 성공하면 마지막 오프셋이 2로 변경
현재 컨슈머 종료되어 리밸런싱 발생
다른 컨슈머 작업 진행하면 3번 오프셋부터 메시지를 가져오게 되고, 5번 메시지 중복 컨슈밍됨
컨슈머 그룹의 이해
컨슈머는 컨슈머 그룹 안에 속한 것이 일반적인 구조이고, 하나의 컨슈머 그룹 안에 여러 개의 컨슈머가 구성됨
데이터의 일관성을 보장해야할때에는 commitSync()를 사용하는게 좋다. commitSync()를 사용하게되면 오프셋 커밋이 성공했는지 실패했는지를 명확히 알 수 있지만, 성능과 비용 그리고 높은 지연이 발생할 수 있다.
데이터의 일관성을 확실하게 보장하지 않을때, 그리고 낮은 지연이 필요할때는 commitAsync()를 사용하는게 좋다.
비동기 커밋의 재시도에 대한 처리
위 책에서 비동기 커밋은 자동으로 재시도를 처리하지 않는다고 했는데, 콜백 메서드 구현을 통해 재시도를 처리할 수 있다.
비동기 커밋 재시도: 비동기 커밋의 재시도에 대해 커밋 순서를 올바르게 설정하는 간단한 패턴은 단조롭게 증가하는 시퀀스 번호를 사용하는 것이다. 커밋할 때마다 시퀀스 번호를 늘리고 커밋 시 시퀀스 번호를 commitAsync 콜백에 추가한다. 재시도를 보낼 준비가 되면 콜백이 받은 커밋 시퀀스 번호가 인스턴스 변수와 동일한지 확인합니다. 일치하는 경우 새 커밋이 없으므로 재시도해도 무방합니다. 인스턴스 시퀀스 번호가 더 큰 경우 새 커밋이 이미 전송되었으므로 다시 시도하지 못하도록 한다.
비동기 커밋은 ElasticSearch 등으로 로그를 보내는 등 어느정도 유실되어도 괜찮을때 사용하는 것이 좋다.
카프카를 구성하는 주요 요소
고유
병렬 처리
및고성능
을 얻기 위해 하나의 토픽을여러 개로 나눈 것
브로커의 로컬 디스크에 저장되는 파일
리플리케이션
replication-factor
옵션이 카프카 내 몇 개의 리플리케이션을 유지할지에 대한 옵션파티션
하나의 토픽이 한 번에 처리할 수 있는 한계를 높이기 위해 토픽 하나를 여러 개로 나눠 병렬 처리가 가능하게 만든 것이
파티션
토픽과 파티션의 관계
파티션 번호는 0부터 시작
파티션 수는 초기 생성 후 언제든지 늘릴 수 있지만, 반대로 한 번 늘린 파티션 수는
절대로 줄일 수 없다
초기에 토픽 생성 시 파티션 수를 2에서 4정도로 생성 후, 메시지 처리량이나 컨슈머의 LAG 등을 모니터링하면서 조금식 늘려가는 방법이 좋다.
LAG
이라는 지표를 통해 컨슈머에 지연이 없는지 확인세그먼트
프로듀서에 의해 브로커로 전송된 메시지는
토픽의 파티션에 저장
되며, 각 메시지들은세그먼트
라는 로그 파일의 형태로 브로커의로컬 디스크에 저장
파티션과 세그먼트의 관계
각 파티션마다 N개의 세그먼트 로그 파일들이 존재
실습 내용
아래 파일 중
peter-overview01-0
디렉토리가 있는걸 확인이 디렉토리의 이름은 글자 그대로
peter-overview01
이라는 토픽의 0번 파티션 디렉토리를 의미, 파티션이 두개였다면peter-overview01-1
도 존재했을 것임토픽, 파티션, 세그먼트의 관계
프로듀서는 카프카의 토픽으로 메시지를 전송
토픽은 프로듀서로부터 받은 메시지를 파티션의 세그먼트 로그 파일에 저장
브로커의 세그먼트 로그 파일에 저장된 메시지를 컨슈머가 컨슈밍
카프카 핵심 개념
분산 시스템
고가용성
이 있으며, 부하가 높은 경우시스템 확장이 용이하다는 장점
페이지 캐시
배치 전송 처리
압축 전송
gzip
,snappy
,lz4
,zstd
압축 타입을 지원배치 전송
과 결합해 사용하면 더욱 높은 효과를 얻음gzip
,zstd
, 빠른 응답 속도가 필요하다면lz4
,snappy
권장토픽, 파티션, 오프셋
토픽
에 데이터를 저장하고, 토픽은이메일 주소
정도의 개념파티션
단위로 나뉘며, 파티셔닝을 통해 단 하나의 토픽이라도 높은 처리량 수행이 가능오프셋
이라 하며, 오프셋은 순차적으로 증가하는 숫자 형태(64비트 정수)고가용성 보장
카프카는 분산 시스템이기에 하나의 서버나 노드가 다운되어도 다른 서버 또는 노드가 장애가 발생한 서버의 역할을 대신해 안정적인 서비스 가능
고가용성을 보장하기 위해 카프카에서는
리플리케이션
기능 제공카프카에서 제공하는 리플리케이션 기능은 토픽 자체를 복제하는게 아니라,
토픽의 파티션을 복제하는 것
원본과 리플리케이션을 구분하기 위해 카프카에서는
리더
와팔로워
라고 부름팔로워가 많을 수록 브로커의 디스크 공간도 소비되므로 이상적인 리플리케이션 팩터 수 유지가 중요, 일반적으로는 3이 권장값
리더는 프로듀서, 컨슈머로부터 오는
모든 읽기와 쓰기 요청을 처리
하며, 팔로워는 오직 리더로부터리플리케이션
만 받음주키퍼의 의존성
지노드(znode)
를 이용해 카프카의 메타 정보가 주키퍼에 기록되며, 주키퍼는 이러한지노드
를 이용해 브로커의 노드 관리, 토픽 관리, 컨트롤러 관리 등의 역할 수행프로듀서 기본 동작과 예제 맛보가ㅣ
프로듀서 디자인
ProducerRecord
: 카프카로 전송하기 위한 실제 데이터이며, 토픽, 파티션, 키, 밸류로 구성send()
메소드를 통해Serializer
,Partitioner
를 거치며, 프로듀서 레코드의 선택사항인 파티션을 지정했다면, 파티셔너는 아무 동작도 하지 않고 지정된 파티션으로 레코드 전달하며, 기본적으로는Round Robin
방식으로 동작send()
메소드 동작 이후 배치 전송을 위해 레코드들을 파티션별로 잠시 모아두고, 전송 실패 시 재시도 동작이 이뤄지고, 전송이 성공하면 메타데이터를 리턴프로듀서의 주요 옵션
프로듀서의 전송 방법은
메시지를 보내고 확인하지 않기
,동기 전송
,비동기 전송
존재메시지를 보내고 확인하지 않기 예제
동기 전송 예제
주요 차이점은 프로듀서는 메시지를 보내고
send()
메소드의Future
객체를 리턴하며,get()
메소드를 이용해Future
를 기다린 후send()
가 성공했는지 실패했는지 여부를 확인ProducerRecord
전송이 성공하고 나면RecordMetadata
를 읽어 들여 파티션과 오프셋 정보 확인이 가능해 메시지 전달의 성공 여부를 파악할 수 있다.비동기 전송을 위한 콜백
비동기 예제
send() 메소드와 콜백을 함께 호출
컨슈머의 기본 동작과 예제 맛보기
컨슈머 그룹
,리밸런싱
등 여러 동작을 수행컨슈머 그룹
은 하나 이상의 컨슈머들이 모여 있는 그룹이며,컨슈머는 반드시 컨슈머 그룹에 속한다.
Active/Standby
개념으로 추가 컨슈머가 있으면 좋을 것이라고 생각할 수 있지만, 컨슈머 그룹 내에서 리밸런싱 동작을 통해 장애가 발생한 컨슈머의 역할을 동일한 그룹에 있는 다른 컨슈머가 그 역할을 대신 수행하므로 굳이 추가 컨슈머 리소스 할당은 필요없음컨슈머의 주요 옵션
컨슈머 예제
컨슈머에서 메시지를 가져오는 방법은 크게 세 가지
오토 커밋 예제
컨슈머 애플리케이션들의 기본값으로 가장 많이 사용되고 있는 것이 오토 커밋
오토 커밋은 오프셋을 주기적으로 커밋하므로 관리자가 오프셋을 따로 관리하지 않아도 된다는 장점이 있지만, 컨슈머 종료 등이 빈번히 일어나면 일부 메시지를 못 가져오거나 중복으로 가져오는 경우가 발생
동기 가져오기 예제
메시지를 가져온 후 처리까지 완료하고 현재의 오프셋을 직접 커밋
동기 방식으로 가져오는 경우 속도는 느리지만, 메시지 손실은 거의 발생하지 않음
동기 방식도 메시지 중복 이슈는 피할 수 없다.
비동기 가져오기 예제
commitAsync()
메소드는commitSync()
와 달리 오프셋 커밋을 실패하더라도 재시도하지 않는다.컨슈머 그룹의 이해
컨슈머는 컨슈머 그룹 안에 속한 것이 일반적인 구조이고, 하나의 컨슈머 그룹 안에 여러 개의 컨슈머가 구성됨
컨슈머는 토픽의 파티션과 1:1 매핑되어 메시지를 컨슈밍한다.
컨슈머들은 하나의 컨슈머 그룹 안에 속하며, 그룹 내 컨슈머들은 서로의 정보를 공유한다.