스페이스로 띄어쓰기 된 단어들의 리스트가 주어질때, 단어들을 반대 순서로 뒤집어라. 각 라인은 w개의 영단어로 이루어져 있으며, 총 L개의 알파벳을 가진다. 각 행은 알파벳과 스페이스로만 이루어져 있다. 단어 사이에는 하나의 스페이스만 들어간다.
입력
첫 행은 N이며, 전체 케이스의 개수이다. N개의 케이스들이 이어지는데, 각 케이스는 스페이스로 띄어진 단어들이다. 스페이스는 라인의 처음과 끝에는 나타나지 않는다. N과 L은 다음 범위를 가진다. N = 51 ≤ L ≤ 25
앞서 그냥 받은 문자열을 뒤집는것이 아닌 항해99에서 내준 문제는 스택/큐 를 사용해서 문제를 풀라고 하여서 Stack을 사용했습니다.
public class backjoon12605 {
public static void main(String[] args) throws IOException {
//BufferedReader로 입력받기
//BufferedReader를 사용해 입력을 받아 n에 저장합니다. n은 테스트 케이스의 수입니다.
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int n = Integer.parseInt(br.readLine());
//for문이 각 테스트 케이스마다 실행됩니다.
//Stack<String> q = new Stack<>();가 for문 안에 있어, 각 테스트 케이스마다 새로운 Stack이 생성됩니다.
for(int i =1; i<=n; i++){
Stack<String> q = new Stack<>();
//입력된 문장을 split(" ")으로 단어별로 나누어 String 배열 que에 저장합니다.
String[] que = br.readLine().split(" ");
//for-each 루프를 사용해 각 단어를 Stack에 push()로 추가합니다.
for(String a : que){
q.push(a);
}
//System.out.print("Case #" + i + ":");를 출력해 현재 테스트 케이스 번호를 출력합니다.
//while (!q.isEmpty()) 루프에서 Stack이 비어있지 않을 때까지 pop()을 사용해 단어를 출력합니다. Stack은 LIFO(Last In, First Out) 구조이므로 마지막에 push()된 단어부터 출력되어 단어 순서가 역순이 됩니다.
//System.out.println();로 각 테스트 케이스 출력을 마치고 줄바꿈을 합니다.
System.out.print("Case #"+i+": ");
while(!q.isEmpty()){
System.out.print(" "+ q.pop());
}
System.out.println();
}
}
}
TIP :
각 테스트 케이스마다 Stack을 새로 생성하므로, 이전 테스트 케이스의 데이터가 현재 테스트 케이스에 영향을 주지 않습니다. Stack의 내용을 pop()하면서 단어를 출력할 때, 단어가 역순으로 출력됩니다.
stack이 for문 밖에 선언이 되어있어도 잘 작동되고 답변 똑같이 나옵니다. 그러나 그럴 경우, 모든 테스트 케이스가 동일한 Stack 인스턴스를 공유하기 때문에 예상치 못한 결과를 초래할 수 있습니다. 다만, 현재 코드 구조에서는 매 반복문 안에서 Stack 이 비워질 때까지 pop() 을 호출하므로 동작할 수 있습니다.
윤수와 정환은 「전주 듣고 노래 맞히기」라는 게임을 할 예정이다. 「전주 듣고 노래 맞히기」는 주어진 노래의 전주를 듣고 먼저 제목을 맞히는 사람이 점수를 얻어 최종적으로 점수가 더 많은 사람이 이기는 게임이다. 절대 음감을 가진 윤수는 노래의 첫 네 음만 듣고도 어떤 노래든 바로 맞힐 수 있다. 따라서, 정환은 윤수를 이기기 위해 첫 세 음만으로 노래를 맞히게 해주는 프로그램을 만들려고 한다. 우선 정환이 알고 있는 노래 제목, 음이름 등을 데이터로 만든 뒤 프로그램을 구현하기 시작했다. 예를 들어, 다음은TwinkleStar(반짝반짝 작은 별)의 악보 중 일부이다.
위 악보를 박자와 관계없이 음이름으로 표현하면CCGGAAG가 된다.
윤수를 이기기 위해서는 이 프로그램이 첫 세 음인CCG만으로 노래 제목인TwinkleStar를 출력할 수 있어야 한다. 또한, 세상의 모든 노래를 아는 윤수와 다르게 정환은 음을 아는 노래가N개뿐이다. 그래서 프로그램에N$N$개의 노래의 정보를 저장해 놓을 것이다. 만약 저장된 노래 중 입력한 첫 세 음으로 시작하는 노래가 여러 개 있어 무슨 노래인지 정확히 알 수 없는 경우?를 출력하고, 입력한 첫 세 음에 맞는 저장된 노래가 없을 경우!를 출력한다.
정환을 도와서 첫 세 음만으로 본인이 음을 아는 노래를 맞히는 프로그램을 완성하자. 이 프로그램은 대문자와 소문자를 구분한다.
입력 :
첫 번째 줄에 정환이 음을 아는 노래의 개수N$N$, 정환이 맞히기를 시도할 노래의 개수M$M$이 공백으로 구분되어 주어진다.
두 번째 줄부터N$N$개의 줄에 걸쳐 노래 제목의 길이T$T$, 영어 대소문자로 이루어진 문자열 노래 제목S$S$, 해당 노래에서 처음 등장하는 일곱 개의 음이름a1,a2,a3,a4,a5,a6,a7$a_1, a_2, a_3, a_4, a_5, a_6, a_7$이 공백으로 구분되어 주어진다.
N+2$N+2$번째 줄부터M$M$개의 줄에 걸쳐 정환이 맞히기를 시도할 노래의 첫 세 음의 음이름b1,b2,b3$b_1, b_2, b_3$가 공백으로 구분되어 주어진다.
주어지는 음이름은 각각C,D,E,F,G,A,B중 하나이다. 같은 제목이 두 번 이상 주어지지 않는다.
출력 :
정환이 맞히기를 시도할 각 노래에 대하여 프로그램에 저장된 노래와 첫 세 음이 동일한 노래가 하나만 있다면 해당 노래의 제목을, 두 개 이상이면?을, 없다면!을 한 줄에 하나씩 출력한다.
예제 :
4 4
11 TwinkleStar C C G G A A G
8 Marigold E D E F E E D
23 DoYouWannaBuildASnowMan C C C G C E D
12 Cprogramming C C C C C C C
E D E
C G G
C C C
C C G
JAVA
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String[] NM = br.readLine().split(" ");
int N = Integer.parseInt(NM[0]); // 음을 아는 노래의 개수 N
int M = Integer.parseInt(NM[1]); // 맞출 노래의 개수 M
String[][] musicInfo = new String[N][]; // [제목 길이, 제목, 음이름]이 담긴 이차원 배열 선언
for(int i = 0; i < N; i++) {
musicInfo[i] = br.readLine().split(" "); // 음악 정보 초기화
}
for(int j = 0; j < M; j++) {
int cnt = 0;
String code = br.readLine().replaceAll(" ", "");
ArrayList<String> list = new ArrayList<>();
for(int i = 0; i < N; i++) {
// code와 가장 맨앞부터 3개의 음이름 같으면 list에 저장
if((musicInfo[i][2] + musicInfo[i][3] + musicInfo[i][4]).equals(code)) {
list.add(musicInfo[i][1]);
}
}
int listSize = list.size();
if(listSize == 1) { // size 1이면 제목 출력
System.out.println(list.get(0));
}
else if(listSize > 1) { // size 1보다 크면 ? 출력
System.out.println("?");
}
else { // size 0이면 ! 출력
System.out.println("!");
}
}
}
JAVA 2
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
StringBuilder sb = new StringBuilder();
StringTokenizer st = new StringTokenizer(br.readLine());
int n = Integer.parseInt(st.nextToken()), m = Integer.parseInt(st.nextToken());
Map<String, String> map = new HashMap<>();
while (n-- > 0) {
String[] arr = br.readLine().split(" ", 3);
String key = arr[2].substring(0, 5);
map.put(key, map.containsKey(key) ? "?\n" : arr[1] + "\n");
}
while (m-- > 0) sb.append(map.getOrDefault(br.readLine(), "!\n"));
bw.write(sb.toString());
bw.flush();
}
둘의 장단점 :
첫 번째 코드 (JAVA)
장점:
명확한 흐름: 입력을 받고 처리하는 과정이 명확하게 나누어져 있어 이해하기 쉽습니다.
직관적인 비교: 코드가 음악 정보를 직접 비교하여 일치 여부를 판단하므로, 쿼리의 결과를 이해하기 쉽게 출력합니다.
단점:
비효율성: 모든 쿼리에 대해 음악 정보를 매번 순회하여 비교하므로, 시간이 오래 걸릴 수 있습니다. 쿼리 수가 많아지면 성능이 저하될 수 있습니다.
중복 출력 처리: 중복된 제목이 있을 때, 명시적으로 "?"를 출력하는 로직이 필요하여 코드가 다소 복잡해질 수 있습니다.
두 번째 코드 (JAVA2)
장점:
해시맵 사용: 음악 코드와 제목을 해시맵에 저장하여 O(1) 시간 복잡도로 쿼리를 처리할 수 있습니다. 이로 인해 성능이 크게 향상됩니다.
중복 처리 간편화: 해시맵에 키가 이미 존재할 경우 "?"로 처리하므로, 코드가 간결해지고 중복 제목 처리가 쉽게 이루어집니다.
효율적인 입출력: BufferedWriter와 StringBuilder를 사용하여 입출력을 효율적으로 처리합니다.
단점:
코드 이해의 어려움: 해시맵 사용과 관련된 부분이 초보자에게는 직관적이지 않을 수 있습니다. 코드의 흐름이 첫 번째 코드에 비해 다소 복잡하게 느껴질 수 있습니다.
고정된 키 길이: 코드의 처음 5글자만 사용하므로, 만약 코드가 5글자 이하로는 구분할 수 없는 경우가 생길 수 있습니다.
결론
성능을 중시한다면 두 번째 코드(Main)가 더 좋습니다. 해시맵을 사용하여 쿼리 처리 속도가 빠르고, 중복 처리가 간편하여 전체적인 효율성이 높습니다.
코드의 직관성과 간단함을 중시한다면 첫 번째 코드(bak_31562)가 더 나을 수 있습니다. 읽기 쉬운 구조로 인해 이해하기 쉽습니다.
따라서 사용자의 요구에 따라 선택할 수 있습니다. 성능이 더 중요한 경우에는 두 번째 코드를 추천합니다.
카프카의 짧은 스토리와 왜 많은 기업들이 카프카를 선택했는지에 대해서 짧게 알아보았습니다.
그렇다면, 이제 간단하게 카프카의 기능들에 대해서 알아보도록 하겠습니다.
기본기
★ Producer
메시지를 주제(topic)에 게시하는 클라이언트는 프로듀서라고 합니다. 프로듀서는 특정 파티션에 메시지를 기록합니다. 이는 메시지 키와 파티셔너를 사용하여 키의 해시를 생성하고 이를 특정 파티션에 매핑함으로써 이루어집니다. 기본적으로 프로듀서는 메시지를 모든 주제의 파티션에 고르게 분배합니다. 그러나 경우에 따라 프로듀서는 특정 파티션으로 메시지를 직접 보낼 수 있습니다. 이는 메시지 키에 특정 파티션 스킴을 적용하여 달성됩니다.
★ Consumer
소비자는 구독한 주제에서 메시지를 끌어와 읽습니다. 소비자는 파티션에 기록된 순서대로 메시지를 읽습니다. 소비자는 메시지 오프셋을 사용하여 자신의 소비를 추적합니다. 소비자가 특정 메시지 오프셋을 확인하면, 이는 소비자가 해당 파티션에서 그 오프셋 이전의 모든 메시지를 수신했음을 의미합니다.
소비자는 소비자 그룹의 일원으로 작동하며, 하나 이상의 소비자가 함께 주제를 소비합니다. 이 그룹은 각 파티션에 대해 하나의 소비자만 책임지도록 설계되었습니다. 예를 들어, 다음 그림에서는 네 개의 파티션(p0, p1, p2, p3)과 세 개의 소비자(c0, c1, c2)를 가진 메시지 주제가 있습니다. 가능한 파티션-소비자 매핑은 다음과 같습니다: c0는 p0, c1은 p1과 p2, c2는 p3입니다.
이 둘의 특징 :
Producer 와 Consumer는 서로 알지 못하며, Producer와 Consumer는 각각 고유의 속도로 Commit Log에 Write및 Read를 수행합니다.
다른 Consumer Group 에 속한 Consumer들은 서로 관련이 없으며, Commit Log에 있는 Event(Message)를 동시에 다른 위치에서Read 할수 있습니다.
여기서! Commit Log는 무엇인가?
Commit Log : 추가만 가능하고 변경이 불가능한 데이터 structure, 데이터(event)는 항상 로그 끝에 추가 되고 변경되지 않습니다.
Offset : Commit Log에서 Event의 위치 122번째 의 offset을 볼수 있습니다.
카프카도 commit Log 라고 하는 개념으로 만들어져있습니다.
Producer 가 Write하는 LOG-END-OFFSET과 Consumer Group의 Consumer가 Read 하고 처리한 후에 Commit한 CURRENT-OFFSET과 차이(Consumer Lag)가 발생 할 수 있습니다.
★ Topic
카프카 안에서 메시지가 저장되는 장소, 논리적 표현
Partition : Commit Log, 하나의 Topic은 하나의 파티션으로 구성, 병렬처리(Throughput 향상)를 위해서 다수의 partition 사용
segment : 메시지(데이터) 물리파일 - 저장되는 실제 물리 File , segment file이 지정된 크기보다 크거나 지정된 기간보다 오래되면 새 파일이 열리고 메시지는 새 파일에 추가됨
Rolling Strategy : log.segment.bytes(default 1 GB), log.roll.hours(default 168 hours)
용량을 정하거나 시간을 가지고 파일을 시간에 맞춰 분리 시키면서 롤링하는것이 카프카의 구조입니다.
★ broker
게시된 메시지는 브로커라고 불리는 일련의 서버에 저장됩니다. 브로커는 프로듀서로부터 메시지를 수신하고, 오프셋을 할당한 후 이를 디스크에 기록합니다. 또한 소비자의 메시지 가져오기 요청에 응답하여 서비스를 제공합니다.
★ cluster
카프카 브로커는 클러스터의 일원으로 작동합니다. 클러스터 내에서 한 브로커는 클러스터 컨트롤러 역할을 하며, 이 컨트롤러는 관리 작업을 담당합니다.
카프카에서는 복제를 통해 파티션 내의 메시지에 대한 중복성을 제공합니다. 따라서 브로커가 실패할 경우, 팔로워 중 하나가 리더십을 맡을 수 있습니다. 모든 프로듀서는 메시지를 게시하기 위해 리더에 연결해야 하지만, 소비자는 리더 또는 팔로워 중 하나에서 메시지를 가져올 수 있습니다.
클러스터 내에는 파티션을 소유한 단일 브로커가 있습니다. 이 브로커를 파티션의 리더라고 합니다. 복제된 파티션은 추가 브로커에 할당되며, 이들은 파티션의 팔로워라고 불립니다.
안녕하세요, 카프카에 관련된 글을 적기 위해서 고민을 하던차에 미국의 대기업 중 하나인 월마트의 카프카 사용에 대한 좋은 글을 읽게 되어서 먼저 공유를 하고 점차적으로 글을 공유하려고합니다.
먼저 카프카에 대해서 어느정도 알고 있다면 큰 어려움 없이 읽을수 있을거같습니다.
요약
Walmart는 여러 클라우드(퍼블릭 및 프라이빗)에 걸쳐 25,000명 이상의 Kafka 소비자와 함께 Apache Kafka를 배포하고 있습니다. 데이터 이동, 이벤트 기반 마이크로서비스, 스트리밍 분석 등 비즈니스에 중요한 사용 사례를 지원합니다. 이러한 사용 사례는 99.99의 가용성을 요구하며 갑작스러운 트래픽 급증으로 인해 발생하는 백로그를 신속하게 처리해야 합니다. Walmart 규모에서는 여러 언어로 작성된 다양한 Kafka 소비자 애플리케이션을 보유하고 있습니다. 이러한 다양성과 안정성 요구 사항이 결합되어 소비자 애플리케이션은 고가용성 SLO를 보장하기 위해 모범 사례를 채택해야 합니다. 카프카 소비자 리밸런싱으로 인한 높은 소비자 지연은 카프카 소비자를 대규모로 운영할 때 가장 흔한 문제입니다. 이 글에서는 저렴한 비용과 탄력성으로 하루에 수조 개의 메시지를 안정적으로 처리하는 Apache Kafka 메시지의 안정적 처리 방법을 중점적으로 설명합니다.
소비자 리밸런싱
Kafka의 프로덕션 배포에서 자주 발생하는 문제는 소비자 리밸런싱과 관련이 있습니다. 카프카 리밸런싱은 각 소비자가 거의 동일한 수의 파티션을 처리하도록 하기 위해 카프카가 소비자 간에 파티션을 재분배하는 프로세스입니다. 이를 통해 데이터 처리가 소비자 간에 균등하게 분산되고 각 소비자가 최대한 효율적으로 데이터를 처리하도록 보장합니다. Kafka 애플리케이션은 컨테이너 또는 VM(가상 머신이라고도 함)에서 실행됩니다. 이 글에서는 현재 업계에서 널리 사용되고 있는 컨테이너에 초점을 맞춥니다. 컨테이너 이미지로 구축된 Kafka 소비자 애플리케이션은 Kubernetes를 기반으로 구축된 엔터프라이즈급 멀티클라우드 컨테이너 오케스트레이션 프레임워크인 WCNP(Walmart Cloud Native Platform)에서 실행됩니다. 따라서 다음과 같은 다양한 원인으로 인해 소비자 리밸런싱이 트리거될 수 있습니다:
소비자 그룹을 떠나는 소비자 파드: 이는 K8 배포 또는 롤링 재시작 또는 자동/수동 스케일-인으로 인해 발생할 수 있다. 소비자 그룹에 진입하는 소비자 파드: 이는 K8 배포 또는 롤링 재시작 또는 자동/수동 스케일아웃으로 인해 발생할 수 있다. 소비자가 실패한 것으로 판단하는 Kafka 브로커(예: 브로커가 세션.timeout.ms 내에서 소비자로부터 하트비트를 수신하지 못한 경우): JVM이 종료되거나 장기간 가비지 수집이 일시 중지되는 경우 트리거됩니다. 소비자가 멈췄다고 판단하는 경우(예: 소비자가 소비할 다음 레코드 배치를 폴링하는 데 max.poll.interval.ms보다 더 오래 걸리는 경우) Kafka 브로커가 트리거됩니다: 이전에 폴링된 레코드의 처리가 이 간격을 초과하면 트리거됩니다.
소비자 리밸런싱은 계획된 유지보수(예: 코드 릴리즈), 표준 운영 관행(예: 최소 포드/최대 포드 설정 수동 변경), 자동 자가 복구(예: 포드 충돌, 자동 확장) 모두에서 복원력을 달성하지만 지연 시간에는 부정적인 영향을 미칩니다. 오늘날 커머스의 거의 실시간에 가까운 특성을 고려할 때, 많은 Kafka 사용 사례에는 엄격한 전송 SLA가 적용됩니다. 이러한 애플리케이션은 프로덕션에서 빈번하고 예측할 수 없는 재조정으로 인해 지속적인 지연 경보로 인해 어려움을 겪었습니다.
현재로서는 카프카에서 리밸런싱을 피하도록 소비자를 구성하는 깔끔한 방법이 없습니다. 커뮤니티에서 정적 소비자 멤버십과 협업을 통한 점진적 리밸런싱을 제공하지만, 이러한 접근 방식에는 나름의 어려움이 따릅니다.
포이즌 필
HOL(헤드 오브 라인) 차단은 네트워킹 및 메시징 시스템에서 발생할 수 있는 성능 제한 현상입니다. Kafka 소비자가 성공적으로 처리될 수 없는 메시지를 접할 때 발생합니다. 메시지 처리 결과 Kafka 소비자 스레드에 잡히지 않은 예외가 발생하면 소비자는 브로커의 다음 폴링에서 동일한 메시지 배치를 다시 소비하게 되며, “포이즌 필” 메시지가 포함된 동일한 배치가 동일한 예외를 발생시킬 것으로 예상됩니다. 이 루프는 문제가 있는 메시지를 건너뛰거나 올바르게 처리하는 코드 수정이 Kafka 소비자 애플리케이션에 배포되거나 소비자 오프셋을 변경하여 문제가 있는 메시지를 건너뛸 때까지 무기한 계속됩니다. 이 포이즌 필 문제는 분할된 데이터 스트림의 순서 내 처리와 관련된 또 다른 문제입니다. 아파치 카프카는 포이즌 필 메시지를 자동으로 처리하지 않습니다.
비용
토픽의 파티션과 이를 읽는 소비자 스레드 사이에는 강력한 결합이 있습니다. 토픽의 최대 소비자 수는 해당 토픽의 파티션 수를 초과할 수 없습니다. 소비자가 토픽 흐름을 따라갈 수 없는 경우(즉, 지속적으로 낮은 소비자 지연을 유지하는 경우) 모든 파티션이 전용 소비자 스레드에 할당될 때까지만 소비자를 더 추가하는 것이 도움이 됩니다. 이 시점에서는 최대 소비자 수를 늘리기 위해 파티션 수를 늘려야 합니다. 좋은 아이디어처럼 들릴 수 있지만, 브로커 노드를 다음으로 큰 크기(4000개의 파티션/브로커)까지 수직 확장하기 전에 브로커에 추가할 수 있는 파티션의 수에 대한 일반적인 규칙이 있습니다.) 보시다시피, 소비자 지연이 증가하면 브로커 자체에 충분한 물리적 리소스(예: 메모리, CPU, 스토리지)가 있음에도 불구하고 파티션이 증가하고 잠재적으로 더 큰 규모의 브로커로 확장해야 하는 문제가 발생합니다. 파티션과 소비자 간의 이러한 강력한 결합은 카프카의 트래픽 증가에도 불구하고 낮은 지연 시간을 유지하고자 하는 많은 엔지니어들의 오랜 골칫거리였습니다.
Kafka 파티션 확장성
수천 개의 파이프라인이 있는 경우, 파티션 수를 늘리면 생산자, 소비자, 플랫폼 팀 간의 조정이 필요하고 가동 중단 시간이 짧아지기 때문에 운영상 부담이 됩니다. 갑작스러운 트래픽 급증과 대규모 백로그 소진은 모두 파티션과 소비자 포드의 증가를 필요로 합니다.
솔루션
위의 몇 가지 문제(예: 카프카 소비자 리밸런싱)를 해결하기 위해, 카프카 커뮤니티는 다음과 같은 카프카 개선 제안을 제안했습니다: KIP-932: Kafka용 대기열.
메시징 프록시 서비스(MPS)는 사용 가능한 다른 경로입니다. MPS는 HTTP를 통해 메시지를 소비자가 대기하는 REST 엔드포인트로 프록시하여 파티션의 제약으로부터 Kafka 소비를 분리합니다. MPS 접근 방식을 통해 Kafka 소비는 더 이상 리밸런싱으로 인한 문제를 겪지 않으며, 더 적은 수의 파티션으로 더 많은 처리량을 처리할 수 있습니다.
MPS 접근 방식의 또 다른 이점은 애플리케이션 팀이 더 이상 Kafka 소비자 클라이언트를 사용할 필요가 없다는 것입니다. 따라서 모든 Kafka 팀은 애플리케이션 팀을 쫓아다니며 Kafka 클라이언트 라이브러리를 업그레이드할 필요가 없습니다.
디자인
MPS Kafka 소비자는 두 개의 독립적인 스레드 그룹, 즉 Kafka message_reader 스레드(즉, 1개 스레드 그룹)와 message_processing_writer 스레드로 구성됩니다. 이러한 스레드 그룹은 표준 버퍼링 패턴(pendingQueue)으로 구분됩니다. 리더 스레드는 (폴링 중에) 바운드 버퍼에 쓰고, 쓰기 스레드는 이 버퍼에서 읽습니다.
또한 바운드 버퍼는 읽기 및 쓰기 스레드의 속도를 제어할 수 있습니다. pendingQueue가 최대 버퍼 크기에 도달하면 message_reader 스레드가 소비자를 일시 중지합니다.
이렇게 리더 스레드와 쓰기 스레드를 분리하면 리더 스레드가 매우 가벼워지고 max.poll.interval.ms
를 초과하여 재밸런싱 작업이 트리거되지 않습니다. 이제 작성자 스레드는 메시지를 처리하는 데 필요한 시간을 확보할 수 있습니다. 다음 다이어그램은 구성 요소와 디자인에 대한 그림 보기를 제공합니다.
위의 아키텍처는 다음과 같은 주요 구성 요소로 이루어져 있습니다:
리더 스레드
리더 스레드의 역할은 인바운드 토픽을 진행하면서 PendingQueue가 가득 차면 역압박을 가하는 것입니다.
순서 이터레이터
순서 이터레이터는 키가 지정된 메시지가 순서대로 처리되도록 보장합니다. 이 반복기는 pendingQueue에 있는 모든 메시지를 반복하고 동일한 키를 가진 메시지가 이미 전송 중인 경우 메시지를 남겨둡니다(즉, 일시적으로 건너뜁니다). 건너뛴 메시지는 동일한 키를 가진 이전 메시지가 처리되면 후속 폴링 호출에서 처리됩니다. MPS는 키당 1개의 메시지만 전송되도록 함으로써 키별 순서대로 전달되도록 보장합니다.
작성자 스레드
작성자 스레드는 병렬 처리를 통해 더 많은 처리량을 제공하는 풀의 일부입니다. 재시도가 모두 소진되거나 재시도할 수 없는 HTTP 응답 코드가 수신되는 경우 REST 엔드포인트에 데이터를 안정적으로 쓰고 메시지를 DLQ하는 것이 이 스레드가 하는 일입니다.
Deal Letter Queue(DLQ)
DLQ 토픽은 모든 Kafka 클러스터에서 생성할 수 있습니다. message_processing_writer 스레드는 처음에 기하급수적으로 백오프하면서 정해진 횟수만큼 메시지를 재시도합니다. 실패하면 메시지는 DLQ 토픽에 저장됩니다. 애플리케이션은 나중에 이러한 메시지를 처리하거나 삭제할 수 있습니다. 소비자 서비스에 중단(예: 시간 초과)이 발생하거나 소비자 서비스에 포이즌 필(예: 500 HTTP 응답)이 발생하는 경우 이 대기열에 메시지를 배치할 수 있습니다.
Comsumer Service
소비자 서비스는 애플리케이션이 메시지를 처리하기 위한 상태 비저장 REST 서비스입니다. 이 서비스에는 원래 Kafka 소비자 애플리케이션에서 사용할 수 있었던 처리의 일부였던 비즈니스 로직이 포함되어 있습니다. 이 새로운 모델을 사용하면 Kafka 소비(MPS)를 메시지 처리(소비자 서비스)에서 분리할 수 있습니다. 아래에서 모든 소비자 서비스에서 구현해야 하는 REST API 사양을 확인할 수 있습니다:
Kafka 오프셋 커밋 스레드
Kafka 오프셋 커밋은 별도의 스레드(즉, offset_commit 스레드)로 구현됩니다. 이 스레드는 일정한 간격(예: 1분)으로 깨어나서 쓰기 스레드가 성공적으로 처리한 가장 최근의 연속된 오프셋을 커밋합니다.
위 그림에서, offset_commit 스레드는 파티션 0과 1에 대해 각각 오프셋 124와 150을 커밋합니다.
구현 세부 정보 MPS는 카프카 커넥트에서 싱크 커넥터로 구현되었습니다. Kafka Connect 프레임워크는 다음과 같은 여러 가지 이유로 적합합니다:
멀티테넌시: 여러 개의 커넥터를 단일 Kafka Connect 클러스터에 배포할 수 있습니다. DLQ 처리: Kafka Connect는 이미 DLQ 처리를 위한 기본 프레임워크를 제공합니다. 커밋 흐름: Kafka Connect는 커밋을 위한 편리한 방법을 제공합니다. 기본 제공 NFR(비기능 요구 사항): Kafka Connect는 많은 비기능적 기능(예: 확장성, 안정성)을 제공합니다.
결론
MPS는 리더 스레드가 할당된 시간인 max.poll.interval.ms 5분 이내에 폴링 목록의 모든 메시지를 pendingQueue에 넣도록 보장하기 때문에 다운스트림 시스템의 속도 저하로 인한 리밸런싱을 제거했습니다. 우리가 보는 유일한 리밸런싱은 Kubernetes POD 재시작과 극히 드물게 발생하는 Kafka 클러스터와 MPS 간의 네트워크 속도 저하로 인한 것입니다. 그러나 소규모 소비자 그룹에서는 이러한 주기의 지속 시간이 무시할 수 있을 정도로 짧으며 처리 SLA(서비스 수준 계약)를 초과하지 않습니다. MPS 서비스와 Kafka 클러스터는 동일한 클라우드와 지역에서 호스팅되어야 두 서비스 간의 네트워크 관련 문제를 줄일 수 있습니다.
애플리케이션이 포이즌 필을 감지하고 반환 코드 600 및 700을 통해 MPS에 알리는 포이즌 필의 협력적 처리는 계획대로 작동합니다.
이 솔루션의 비용 이점은 두 가지 영역에서 실현됩니다. 첫째, 상태 비저장 소비자 서비스는 Kubernetes 환경에서 빠르게 확장할 수 있으며 명절이나 캠페인 이벤트를 위해 미리 확장할 필요가 없습니다. 둘째, Kafka 클러스터 크기는 더 이상 파티션 크기에 의존하지 않으며, 파티션당 약 5~10MB로 처리량에 맞게 실제로 확장할 수 있습니다.
매년 연말연시에 Kafka 클러스터를 확장하면서 발생하는 사이트 관련 문제와 Kafka 파이프라인의 운영 요청을 재조정하는 데 있어 큰 개선이 있었습니다.
상태 비저장 소비자 서비스는 메시지 버스트를 처리하기 위해 Kubernetes 환경에서 쉽게 자동 확장되기 때문에 트래픽이 갑자기 급증해도 더 이상 Kafka 파티션을 확장할 필요가 없습니다.