카프카 프로덕션 환경에서 운용하기 위해서나, 카프카를 사용하는 애플리케이션을 개발하기 위해 꼭 내부 메커니즘을 알아야 하는 것은 아님. 하지만 트러블 슈팅, 혹은 실행되는 방식을 이해하는 데에 도움이 될 것. 또한 카프카 튜닝 시 명확한 의도를 가지고 설정값을 잡아주는 등 특히 도움이 될 것으로 봄.
이 장에서는 특히 아래 4가지 주제를 중점으로 다뤄볼 것.
- 카프카 컨트롤러
- 카프카에서 복제(replication)가 작동하는 방식
- 카프카가 프로듀서와 컨슈머의 요청을 처리하는 방식
- 카프카가 저장을 처리하는 방식(파일 형성, 인덱스 등)
6.1 클러스터 멤버십
기존 버전 카프카 : 주키퍼 사용.
각 브로커는 고유한 식별자(브로커 설정 파일에 정의되었거나, 자동으로 생성된)를 가짐
브로커 프로세스 시작 때마다 : 주키퍼에 Ephemeral 노드의 형태로 ID를 등록.
- 컨트롤러와 몇몇 생태계 툴들은 주키퍼의 /brokers/ids 경로를 구독함으로써 브로커가 추가되거나 제거될 때마다 알림을 받음.
- 만약 동일한 ID를 가진 다른 브로커를 시작한다면 에러가 발생
- 즉, 새 브로커는 자신의 ID를 등록하려 했지만, 이미 동일 ID를 가진 브로커가 Znode에 등록되어 있기 때문에 실패.
브로커와 주키퍼 간의 연결이 끊어진 경우, 브로커가 시작될 때 생성한 Ephemeral 노드는 자동으로 주키퍼에서 삭제.
- 해당 목록을 지켜보고있던 카프카 컴포넌트들은 해당 브로커가 내려갔음을 알 수 있음.
브로커가 정지 → Znode 삭제. but, 브로커의 ID는 다른 자료구조에 남아있게 됨.
ex) 각 토픽의 레플리카 목록에는 해당 레플리카를 저장하는 브로커의 ID가 포함됨.
따라서, 브로커가 완전히 유실되어 동일한 ID를 가진 새로운 브로커가 투입되는 경우, 곧바로 클러스터에서 유실된 브로커의 자리를 대신해서 이전 브로커의 토픽과 파티션들을 할당받게 됨.
6.2 컨트롤러
컨트롤러 : 일반적인 카프카 브로커 기능 + 파티션 리더를 선출하는 역할
클러스터에서 가장 먼저 시작되는 브로커가, 주키퍼의 /controller에 Ephemeral 노드를 생성함으로써 컨트롤러가 됨
- 다른 브로커 역시 시작 시에 주키퍼에 노드를 생성하고자 하지만, ‘노드가 이미 존재함’이라는 예외를 받기 때문에 컨트롤러가 있다는 것을 알아차리게 됨
- 클러스터 안에 한 번에 한 개의 컨트롤러만 존재하도록 보장할 수 있음
- 브로커들은 주키퍼의 컨트롤러 노드에 변동이 생겼을 때 알림을 받기 위해, 해당 노드에 와치를 설정
컨트롤러 브로커가 멈추거나, 연결이 끊기는 경우 -> Ephemeral 노드는 삭제됨
- zookeeper.session.timeout.ms에 설정된 값보다 더 오랫동안 주키퍼 클라이언트가 주키퍼에 하트비트를 전송하지 않는 것도 여기에 해당.
Ephemeral 노드가 삭제됨
→ 클러스터 안의 다른 브로커들은 와치를 통해 컨트롤러가 없어진 것을 알아차림.
→ 주키퍼에 컨트롤러 노드를 생성하려고 시도
→ 주키퍼에 가장 먼저 컨트롤러 노드를 생성한 브로커가 컨트롤러로 설정됨(나머지 : 노드 이미 존재 예외)
- 브로커는 새로운 컨트롤러가 선출 될 때 마다 주키퍼의 조건적 증가 연산에 의해 증가된 에포크epoch값 을 전달받게 됨.
- 브로커는 현재 에포크 값을 알고 있기 때문에, 만약 더 낮은 에포크 값을 가진 컨트롤러부터 메시지를 받은 경우 무시함.
- 이것은 ‘가비지 컬렉션 때문에 컨트롤러 브로커가 멈춘 사이 주키퍼 사이의 연결이 끊어질 수 있기 때문’에 중요(그 사이 새로운 컨트롤러가 선정 될 수 있기 때문). 작업을 재개한 컨트롤러는 좀비 컨트롤러라고 불리움.
브로커가 컨트롤러가 되면,
- 클러스터 메타데이터 관리와 리더 선출을 시작하기 전, 주키퍼로부터 최신 레플리카 상태맵을 받아옴.
- 해당 적재 작업은 비동기 API를 사용해서 수행됨.
- 지연을 줄이기 위해, 읽기 요청을 여러 단계로 나눠 주키퍼로 보냄.
- 하지만, 그럼에도 불구하고 파티션 수가 많은 클러스터에서는 적재 작업이 몇 초씩 걸릴 수 있음.
브로커가 클러스터를 나갔다는 사실을 컨트롤러가 알아차리면,
- 컨트롤러는 해당 브로커가 리더를 담당하던 모든 파티션에 대해 새로운 브로커를 할당해 줌.
- 컨트롤러는 새로운 리더가 필요한 모든 파티션을 순회하며 새로운 리더가 될 브로커를 결정.
- 단순히 해당 레플리카 목록에서 바로 다음 레플리카가 새 리더브로커가 됨.
- 이후, 새로운 상태를 주키퍼에 쓴 뒤(비동기, 여러 개로 나눠 요청)
- 새로 리더가 할당된 파티션의 레플리카를 포함하는 모든 브로커에 LeaderAndISR 요청 전송.
- 해당 요청은 해당하는 파티션들에 대한 새로운 리더와 팔로우 정보를 포함함.
- 이러한 요청은 배치 단위로 묶여서 전송됨(효율성을 위해)
- 즉, 각각의 요청은 같은 브로커에 레플리카가 있는 다수의 파티션에 대한 새 리더십 정보를 포함함.
새로 리더가 된 브로커 각각은,
- 클라이언트로부터 쓰기 혹은 읽기 요청을 처리하기 시작.
팔로워들은,
- 새 리더로부터 메시지를 복제하기 시작.
클러스터 안의 모든 브로커 : 클러스터 내 전체 브로커와 레플리카의 맵을 포함하는 MetadataCache를 가지고 있음
- 따라서 컨트롤러는 모든 브로커에 리더십 변경 정보를 포함하는 UpdateMetadata요청을 보내서 각각의 캐시를 업데이트하도록 함.
브로커가 백업을 시작할 때에도 상기 과정이 반복됨.
- 브로커에 속한 모든 레플리카들은 팔로워로 시작하며, 리더로 선출될 자격을 얻기 위해서는 그전에 리더에 쓰여진 메시지를 따라잡아야 한다는 차이가 있음.
[요약]
- 컨트롤러는 브로커가 클러스터에 추가되거나 제거 될 때 파티션과 레플리카 중에서 리더를 선출할 책임이 있음
- 컨트롤러는 서로 다른 2개의 브로커가 자신이 현재 컨트롤러라고 생각하는 스플릿 브레인 현상을 방지하기 위해 에포크 번호를 사용함.
6.2.1 KRaft : 카프카의 새로운 래프트 기반 컨트롤러
KRaft : 주키퍼 기반 컨트롤러로부터 탈피하기 위한 새로운 래프트 기반 컨트롤러 쿼럼
- 2.8 버전에서 프리뷰 버전으로 포함
- 3.3 버전부터 정식으로 프로덕션 환경에서 사용 가능
카프카 컨트롤러 교체 이유
- 주키퍼 모델이 우리가 카프카에 원하는 파티션 수까지 확장될 수 없다는 사실이 명백해짐.
- 컨트롤러가 주키퍼에 메타데이터를 쓰는 작업은 동기적으로 이루어지지만 또한 주키퍼로부터 업데이트를 받는 과정 역시 비동기적으로 이루어짐.
- 따라서 브로커, 컨트롤러, 주키퍼간에 메타데이터 불일치 발생 가능.
- 브로커 메시지를 보내는 작업은 비동기적으로 이루어짐
- 컨트롤러가 재시작될 때마다 주키퍼로부터 모든 브로커와 파티션에 대한 메타데이터를 읽어와야 함
- 해당 부분은 병목현상이 많이 발생.
- 즉, 파티션과 브로커의 수가 증가함에 따라 브로커의 재시작은 더욱 느려짐.
- 이후, 메타데이터를 모든 브로커로 전송함.
- 메타데이터 소유권 관련 내부 아키텍처가 좋지 못함.(브로커, 주키퍼, 컨트롤러가 나눠서 함)
- 주키퍼 그 자체로 분산 시스템(카프카를 운영하려면 2개의 분산시스템을 이해해야 함)
기존 주키퍼의 주요한 기능
- 컨트롤러 선출
- 메타데이터(현재 운영 중 브로커, 설정, 토픽, 파티션, 레플리카 정보) 저장
새로운 컨트롤러의 핵심 아이디어
- 로그 기반 아키텍처 도입 : 카프카 그 자체에 사용자가 상태를 이벤트 스크림으로 나타 낼 수 있도록 함.
- 해당 방법은 카프카 커뮤니티에 익숙함
- 즉, 다수의 컨슈머를 사용해서 이벤트를 재생 replay 함으로써 최신 상태를 빠르게 따라잡을 수 있음
- Q) 이게 대체 무슨말이여..
- 로그 : 이벤트 사이에 명확한 순서 부여→ 컨슈머들이 항상 하나의 타임라인을 따라 움직이도록 보장함.
- 메타데이터를 관리하는 데에도 장점이 적용됨.
- 컨트롤러 노드들은 메타데이터 이벤트 로그를 관리하는 레프트 쿼럼이 됨. 이 로그는 클러스터 메타데이터의 변경내역을 저장. 현재 주키퍼에 저장되어 있는 모든 정보들이 여기에 저장될 것.
래프트 알고리즘 사용 → 컨트롤러 노드들은 외부 시스템에 의존하지 않고 자체적으로 리더를 선출
액티브 컨트롤러 : 메타데이터 로그의 리더 역할을 맡고 있는 컨트롤러
- 액티브 컨트롤러는 브로커가 보내온 모든 RPC 호출(**Remote Procedure Call, 원격 프로시저 호출)**을 처리함
- 팔로워 컨트롤러들은 액티브 컨트롤러에 쓰여진 데이터를 복제.
- 액티브 컨트롤러에 장애 발생 시 즉시 투입될 수 있도록 준비 상태 유지
모든 컨트롤러들이 최신 상태를 가지고 있음으로, 컨트롤러 장애 복구는 모든 상태를 새 컨트롤러로 이전하는 리로드 기간을 필요로 하지 않음.
컨트롤러는 다른 브로커게 변경사항을 push 하지 않음.
- 대신, 다른 브로커들이 새로 도입된 MetadataDetch API를 사용해 액티브 컨르롤러로부터 pull 해옴
- 컨슈머의 읽기 요청과 유사하게, 브로커는 마지막으로 가져온 메타데이터 변경사항의 오프셋을 추적하고 그보다 나중에 업데이트만 컨트롤러에 요청.
- 브로커는 추후 시동 시간을 줄이기 위해 메타데이터를 디스크에 저장.
브로커 프로세스는 시작 시 주키퍼가 아닌, 컨트롤러 쿼럼(의사결정에 필요한 정족수)에 등록
그리고 운영자가 이를 해지하지 않는 한, 이를 유지함.
- 즉, 브로커가 종료되면 오프라인 상태롤 들어가는 것일 뿐, 등록은 여전히 유지됨.
- 온라인 상태지만 최신 메타데이터로 최신 상태를 유지하고 있지 않은 브로커의 경우, 펜스 된 상태가 되어 클라이언트 요청을 처리할 수 없음.
- 브로커에 새로 도입된 ‘펜스된 상태’는 클라이언트가 더이상 리더가 아닌, 하지만 최신상태에서 너무 떨어지는 바람에 자신이 리더가 아니라는것 조차 인식을 못하는 브로커에 쓰는 것을 방지함.
기존 주키퍼와 직접 통신하던 모든 클라이언트 , 브로커 작업들은 이제 컨트롤러로 보내짐
→ 브로커 쪽에는 아무것도 바꾸지 않으면서, 컨트롤러만을 바꿔주는 것으로 매끄러운 마이그레이션 가능.
KRaft로 옮겨가는 과정
- 적어도 1회는 클러스터 정지가 일어남
- 브릿지 릴리즈 버전으로 변경한 뒤, 브로커별로 KRaft를 지원하는 버전으로 변경
- 중지를 일으킬 수 없는 클러스터의 경우 미러링을 통한 버전 변경을 추천함.
6.3 복제
복제 : 카프카의 핵심.
- 카프카 : 분산되고, 분할되고 복제된 커밋 로그 서비스라고 표현되기도 함.
- 복제가 중요한 이유 : 개별적인 노드에 필연적으로 장애가 발생할 수밖에 없기 때문
- 카프카가 신뢰성과 지속성을 지원하는 방안
카프카에 저장되는 모든 데이터 : 토픽 단위로 조직화됨
- 각 토픽은 1개 이상의 파티션으로 분할됨
- 각 파티션은 다시 다수의 레플리카를 가짐
- 각각의 레플리카는 브로커에 저장됨.
- 대개 하나의 브로커는 서로 다른 토픽과 파티션에 속하는 수백 개에서 수천 대의 레플리카를 저장함.
레플리카의 종류 : 리더, 팔로워
리더 레플리카
일관성 보장을 위해 모든 쓰기 요청은 리더 레플리카로 주어짐.
클라이언트들은 리더 레플리카나 팔로워로부터 레코드를 ‘읽어’ 올 수 있음.
팔로워 레플리카
파티션에 속한 레플리카 중 리더를 제외한 모든 레플리카
주로 하는 일 : 리더 레플리카로 들어온 최근 메시지들을 복제함으로써 최신 상태를 유지
별도로 설정을 잡아주지 않는 한, 팔로워는 클라이언트의 요청을 처리할 수 없음
<aside> 💡 팔로워로부터 읽기 KIP-392부터 추가된 기능.
클라이언트가 리더 레플리카 대신 가장 가까이에 있는 인싱크 레플리카로부터 읽을 수 있게 함. 관련 설정 : replica.selector.class, client.rack, rack.id
주의 : 인싱크에서 읽을 때에도 커밋된 메시지만 읽을 수 있음(하이워터마크에 나온 인덱스까지). 하이워터마크 전파에도 시간이 걸리기 때문에, 리더에서 읽을 때보다 팔로워에서 읽을때 더 느릴 수 있음.
</aside>
리더 레플리카 : 어느 팔로워 레플리카가 최신 상태를 유지하고 있는지 확인함.
팔로워 레플리카 : 리더 레플리카와 동기화를 유지하기 위해 읽기 요청을 보냄.
- 컨슈머가 메시지를 읽어오기 위해 사용하는 요청과 같음
- 팔로워 레플리카가 10초 이상 메시지 요청을 보내지 않거나, 10초(replica.lag.time.max.ms) 이상 가장 최근의 메시지를 가져가지 않는 경우 아웃 오브 싱크 레플리카로 간주됨. → 해당 레플리카는 장애 상황에서 리더가 될 수 없음
즉, 인싱크 레플리카만이 장애상황에서 파티션 리더로 선출될 수 있음.
‘현재 리더’에 대해 각 파티션은 ‘선호 리더’를 가짐.
선호리더 : 파티션 생성 시 처음 선정된 리더 레플리카.
- 레플리카 목록의 최상단 레플리카
- 선호 리더가 현재 리더와 동기화되고 있을 경우 리더 선출을 실행시켜 선호 리더를 현재 리더로 변경함. Q) 왜 굳이..?
- 브로커에 가장 골고루 분산된 상태(파티션 할당 목표에 맞춰진 분산이 이루어진 상태)이기 때문
- auto.leader.reblance.enable=true(default)
6.4 요청 처리
카프카 브로커가 하는 일 : 대부분 클라이언트, 파티션 레플리카, 컨트롤러가 파티션 리더에게 보낸 요청 처리
모든 요청은 표준 헤더를 가짐
- 요청 유형 : API 키
- 요청 버전 : 브로커는 서로 다른 버전의 클라이언트로부터 요청을 받아 각각의 버전에 맞는 응답을 할 수 있음
- Correlation ID : 각각의 요청에 부여되는 고유 식별자(응답, 에러로그에도 포함 - 트러블 슈팅에 사용)
- 클라이언트 ID : 요청을 보낸 애플리케이션을 식별함
브로커는 각 포트별로 acceptor 스레드를 하나씩 실행함
→ 억셉트 스레드는 요청을 프로세서 스레드에 넘겨 처리하도록 함
- 프로세스 스레드의 수는 설정 가능함.
→ 프로세서 스레드는 클라이언트 연결로부터 들어온 요청을 받아서 요청 큐에 넣음
→ 일단 요청이 요청큐에 들어오면, I/O 스레드가 요청을 가져와서 처리하는 일을 담당
→ 프로세서 스레드는 응답 큐에서 응답을 가져다가 클라이언트로 보냄.
- 가끔 응답에 지연이 필요한 경우가 있음 → 지연된 응답은 완료될 때까지 퍼거토리에 저장
- 컨슈머의 경우 브로커 쪽에 데이터가 준비되었을 때에만 응답을 보낼 수 있음
- 어드민 클라이언트의 경우 토픽 삭제가 진행 중엔 상황에서만 DeleteTopicrequest를 보낼 수 있음
쓰기 요청
카프카 브로커로 메시지를 쓰고 있는 프로듀서가 보낸 요청
읽기 요청
카프카 브로커로부터 메시지를 읽어오고 있는 컨슈머나, 팔로워 레플리카가 보내는 요청
어드민 요청
토픽 생성이나, 삭제같이 메타데이터 작업을 수행 중인 어드민 클라이언트가 보내는 요청
클라이언트는 어디로 요청을 보내야 하는지 어떻게 알 까?
- 카프카 클라이언트는 메타데이터 요청이라 하는 또 다른 유형의 요청을 사용함
- 해당 요청으로 클라이언트가 다루고자 하는 토픽 관련 정보를 받음
- 해당 요청은 어느 브로커로 보내도 상관없음
- 모든 브로커가 관련 정보를 담은 메타데이터 캐시를 지니고 있기 때문
- 클라이언트는 이러한 정보를 캐싱해 두었다가 파티션의 리더 역할을 맡고 있는 브로커에 바로 쓰거나 읽음
- 클라이언트는 최신 값을 유지하기 위해 메타데이터 요청을 보냄
- Not a Leader 응답을 받은 경우에도 재요청을 보냄
6.4.1 쓰기 요청
acks 설정을 통해 어느 시점에 메시지가 성공적으로 쓰였는지 판별
파티션의 리더 레플리카를 가지고 있는 브로커는, 해당 파티션에 대한 쓰기 요청이 주어진 경우 아래 유효성 검증
- 데이터를 보내는 사용자가 토픽에 관한 사용권을 가지고 있는가
- 요청에 지정된 acks 값이 올바른가
- acks가 all인 경우 메시지를 안전하게 쓸 수 있을 만큼의 인싱크 레플리카를 보유하고 있는가.
- 해당 조건 미충족시 메시지를 쓰지 않도록 설정 가능
유효성 검증 이후 새 메시지들을 로컬디스크에 작성
쓰여지고 난 뒤 acks에 따라 응답을 내려보냄(all일 경우 퍼거토리에 저장해 두었다가 싱크 완료 시 응답)
6.4.2 읽기 요청
클라이언트는 각 파티션에 대해 브로커가 리턴 할 수 있는 최대 데이터 양을 지정함
- 클라이언트는 브로커가 되돌려준 응답을 담을 수 있을 정도로 큰 메모리를 할당해야 하기 때문
요청을 받은 파티션 리더는, 우선 요청이 유효한지 확인
- 오프셋이 유효한지 등등
오프셋이 존재한다면 브로커는 파티션으로부터 클라이언트가 요청에 지정한 크기 한도만큼의 메시지를 읽어서 보내줌.
카프카는 클라이언트에게 보내는 메시지를 제로카피 최적화를 적용하는 것으로 유명함.
- 즉, 파일에서 읽어온 메시지들을 중간 버퍼를 거치지 않고 바로 네트워크 채널로 보냄
- 데이터를 복사하고 메모리상에 버퍼 하기 위한 오버헤드가 사라짐
브로커가 리턴할 수 있는 데이터 양의 상한을 지정 가능
클라이언트는 리턴될 데이커 양의 하한도 지정할 수 있음.
- 관련해서 응답을 대기할 타임아웃도 설정 가능
파티션 리더에 있더라도, 모든 인싱크 레플리카에 저장되지 않은 정보는 읽어갈 수 없음
6.4.3 기타 요청
카프카 프로토콜은 62개의 서로 다른 요청 유형을 지님
6.5 물리적 저장소
카프카의 기본 저장 단위 : 파티션 레플리카
파티션은 서로 다른 브로커들 사이에 분리될 수 없음→같은 브로커의 서로 다른 디스크에 분할저장되는 것도 불가
운영자가 파티션들이 저장될 디렉터리 목록을 정의(log.dirs)
카프카가 데이터를 저장하기 위해 사용 가능한 디렉터리를 어떻게 활용하는가
- 데이터가 클러스터 안의 브로커, 브로커 안의 디렉터리에 할당되는 방식
- 브로커가 파일을 관리하는 방법(보존기한 등)
- 파일 내부에 데이터가 저장되는 파일과 인덱스 현식에 대해 살펴봄
- 카프카를 장시간용 데이터 저장소로 사용가능하게 해주는 로그 압착기능을 살표봄
6.5.1 계층화된 저장소
계층화된 저장을 위해 시도 중(3.4.0 기준 미탑재 → 3.6.0에서 얼리 액세스모드로 탑재됨)
사유
- 파티션별로 저장 가능한 데이터에는 한도가 있음.
- 결과적으로 최대 보존 기한과 파티션 수는 물리적 디스크 크기에도 제한받음
- 디스크와 클러스터 크기는 저장소 요구 조건에 의해 결정됨
- 지연과 처리량이 주 고려사항일 경우 클러스터는 필요 이상으로 커지는 경우가 많음→비용 up
- 클러스터의 크기를 키우거나 줄일 때 파티션의 위치를 다른 브로커로 옮기는 데에 걸리는 시간은 파티션의 수에 따라 결정됨
- 파티션의 크기가 클수록 클러스터의 탄력성 하락
계층화된 저장소 기능에서 카프카 클러스터의 저장소를 로컬과 원격 두 계층으로 나눔
계층화된 저장소 기능의 이중화된 구조 덕분에 카프카 클러스터의 메모리와 CPU에 상관없이 저장소를 확장할 수 있음. → 장기간용 저장 솔루션 역할 수행 가능
KIP-405에서 계층화된 저장소 설계에 대해 알아볼 수 있음
6.5.2 파티션 할당
레플리카들은 가능한 브로커간에 고르게 분산시킨다
각 파티션에 대해 가각의 레플리카는 서로 다른 브로커에 배치되도록 한다
만약 브로커에 렉 정보가 설정되어 있다면, 가능한 각 파티션의 레플리카들은 서로 다른 렉에 할당한다
6.5.3 파일 관리
카프카 운영자는 토픽에 대해 보존 기한을 설정할 수 있음
기본적으로 각 세그먼트는 1G의 데이터 혹은 최근 1주일 치의 데이터 중 적은 쪽을 저장함
현재 쓰여지고 있는 세그먼트를 액티브 세그먼트라고 부름
- 액티브 세그먼트는 어떠한 경우에도 삭제되지 않음.
카프카 브로커는 각 파티션의 모든 세그먼트에 대해 파일 핸들을 엶
- 사용 중인 파일 핸들 수가 매우 높게 유지될 수 있기 때문에 운영체제에 맞춘 튜닝 필요
6.5.4 파일 형식
6.5.5 인덱스
카프카는 컨슈머가 임의의 사용 가능한 오프셋(인덱스)에서부터 메시지를 읽어 올 수 있도록 함
카프카는 타임스탬프와 메시지 오프셋을 매핑하는 또 다른 인덱스를 지니고 있음.
- 해당 인덱스는 타임스탬프를 기준으로 메시지를 찾을 때 사용됨
6.5.6 압착
6.5.7 압착 원리
6.5.8 삭제된 이벤트
tombstone : soft delete
'Web_Backend > Kafka' 카테고리의 다른 글
[Kafka] 8장 ‘정확히 한 번’의미구조(멱등성) (1) | 2023.12.26 |
---|---|
[Kafka] 4장. 카프카 컨슈머 (0) | 2023.11.02 |
[Kafka] 3장. 카프카 프로듀서 : 카프카에 메시지 쓰기 (1) | 2023.11.02 |
[Kafka] 1. 카프카 시작하기 (0) | 2023.11.02 |
야나의 코딩 일기장 :) #코딩블로그 #기술블로그 #코딩 #조금씩,꾸준히
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!