<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://imucoding.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://imucoding.github.io/" rel="alternate" type="text/html" /><updated>2026-04-27T13:12:54+00:00</updated><id>https://imucoding.github.io/feed.xml</id><title type="html">IMU TECH BLOG</title><subtitle>Write an awesome description for your new site here. You can edit this line in _config.yml. It will appear in your document head meta (for Google search results) and in your feed.xml site description.</subtitle><author><name>GitHub User</name></author><entry><title type="html">Timecode-driven Multi-source Synchronization</title><link href="https://imucoding.github.io/media/2026/04/25/timecode-driven-multi-source-synchronization.html" rel="alternate" type="text/html" title="Timecode-driven Multi-source Synchronization" /><published>2026-04-25T00:00:00+00:00</published><updated>2026-04-25T00:00:00+00:00</updated><id>https://imucoding.github.io/media/2026/04/25/timecode-driven-multi-source-synchronization</id><content type="html" xml:base="https://imucoding.github.io/media/2026/04/25/timecode-driven-multi-source-synchronization.html"><![CDATA[<h2 id="environment">Environment</h2>
<p><img src="https://imucoding.github.io/assets/images/multi-sync.png" alt="img.png" /></p>
<ul>
  <li>4개의 테라덱은 각 소스 스트림을 송출함</li>
  <li>각 테라덱은 부팅 시점에 pts를 0으로 초기화하므로 4개의 테라덱이 정확히 동시에 켜지지 않는 이상 동일 프레임에 대해 pts가 상이함</li>
  <li>타임 코드는 동일 ntp 서버 기준으로 동일 프레임에 대해 같은 타임코드로 삽입됨</li>
  <li>네트워크를 통해 오는 각 스트림은 도착 속도가 다 제각각이므로 동일 프레임이 동일 타이밍에 수신되지 않음, 즉 수신하는 프레임의 타임코드가 정렬되어 있지 않음</li>
</ul>

<h2 id="troubleshooting">Troubleshooting</h2>
<p><img src="https://imucoding.github.io/assets/images/multi-sync2.png" alt="img.png" /></p>
<ol>
  <li>모든 스트림을 동시에 로컬 .ts 파일로 녹화 시작 (버퍼링)</li>
  <li>녹화와 병렬로 각 스트림의 첫 SEI timecode를 추출</li>
  <li>가장 느린(timecode가 큰) 스트림 기준 + 5초 = target_sec 결정</li>
  <li>각 버퍼 파일에서 TS 패킷 레벨로:
    <ul>
      <li>PAT/PMT 파싱하여 비디오/오디오 PID 식별</li>
      <li>SEI timecode가 target_sec:fnum=0 인 프레임 탐색</li>
      <li>해당 프레임의 PTS를 base_pts로 설정</li>
      <li>base_pts를 빼서 PTS/DTS/PCR을 0부터 리맵</li>
      <li>Named FIFO → ffmpeg → SRT caller MPTS 출력</li>
    </ul>
  </li>
</ol>

<p>SEI는 h26x 코덱의 NALU type이므로 비디오 스트림에 대해서 SEI 패킷을 찾고 타임코드를 파싱하면 됨</p>
<ul>
  <li>TS 패킷(188byte)을 이어 붙여서 PES를 만드는데, TS 헤더의 PUSI(Payload Unit Start Indicator) 비트가 1이면 그때부터 새로운 PES 시작 (2번째 바이트 내 존재)</li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[ 188 Bytes TS Packet ]
│
├─▶ 1. TS Header (기본 4 Bytes)
│    ├─ [0] Sync Byte: 항상 0x47 (알파벳 'G')
│    ├─ [1] Error(1bit) | ★PUSI(1bit) | Priority(1bit) | PID 상위 5bits
│    ├─ [2] PID 하위 8bits
│    └─ [3] Scrambling(2bits) | ★AFC(2bits) | Continuity Counter(4bits)
│
├─▶ 2. Adaptation Field (옵션, 가변 길이)
│    └─ AFC(Adaptation Field Control) 값에 따라 존재 여부 결정됨.
│       (PCR 클럭 동기화 데이터나 패킷 패딩을 위해 쓰임)
│
└─▶ 3. TS Payload (실제 데이터, 최대 184 Bytes)
     │
     ▼ ★ PUSI가 1일 때, 페이로드의 첫 바이트부터 무조건 PES 헤더 시작!
     [ PES Header ]
     ├─ [0] 0x00 \
     ├─ [1] 0x00  &gt; Packet Start Code Prefix (고정값)
     ├─ [2] 0x01 /
     ├─ [3] Stream ID (예: 0xE0 = 비디오)
     ├─ [4~5] PES Packet Length (비디오는 보통 0x0000)
     ├─ [6~8] Optional Header (여기에 PTS/DTS 플래그가 있음)
     ├─ [9~13] ★ PTS (5 Bytes, 90kHz 하드웨어 틱)
     └─ [14~18] ★ DTS (존재할 경우 5 Bytes)
</code></pre></div></div>

<p>그리고 PES 역시 헤더와 페이로드로 구성되고 구조는 아래와 같음</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[ PES Packet ]
 ├── [ PES Header ] (운송장: 언제 재생할 것인가?)
 │    ├── Start Code Prefix (0x000001) : "여기서부터 PES 시작이다"
 │    ├── Stream ID : 이 데이터가 비디오인지 오디오인지 식별 (예: 0xE0 = 비디오)
 │    ├── PES Packet Length : 패킷 길이 (비디오는 보통 0으로 세팅됨)
 │    └── Optional Header : ★ PTS와 DTS가 기록되는 핵심 구역
 │
 └── [ PES Payload ] (내용물: 무엇을 그릴 것인가?)
      │
      └── [ Access Unit (AU) ] ★ 1개의 PES 페이로드 = 보통 1개의 AU (프레임 1장)
           ├── 1. AUD NALU (Access Unit Delimiter) : "프레임 조립 시작!"
           ├── 2. SPS / PPS NALU : 해상도, 프로파일 등 코덱 메타데이터
           ├── 3. SEI NALU : 앞서 다뤘던 타임코드 등 부가 정보 메타데이터
           └── 4. VCL NALU : 실제 화면을 구성하는 픽셀 데이터 (I/P/B 슬라이스)
</code></pre></div></div>

<ul>
  <li>이렇게 PES안에 다양한 NALU가 들어있음 이 NALU 들의 집합으로 1프레임을 이루는 데이터가 AU이고, PES 페이로드를 AU로 봐도 무방</li>
  <li>PTS/DTS는 PES 헤더에 기록되어있고, PES 헤더는 오직 PUSI가 켜진 패킷에만 존재하므로 PTS추출을 위해서는 기타 패킷에서 페이로드를 볼 필요 없고, PUSI를 확인해야함</li>
</ul>

<h3 id="스트림-정보-추출">스트림 정보 추출</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ffprobe -v error -select_streams v:0 -show_frames -show_entries frame=pts,tags -print_format json -read_intervals %+#[프레임수] -i [파일경로]
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">-select_streams</code> : 멀미플렉싱된 TS 안에서 특정 PID로 디먹싱 고정</li>
  <li><code class="language-plaintext highlighter-rouge">-show_frames</code> : deep parsing 모드로 패킷 헤더만 보는 것이 아니라 AU 내부로 진입해서 NALU도 확인</li>
  <li><code class="language-plaintext highlighter-rouge">-show_entries frame=pts,tags</code> : 데이터 필터링으로 PES 헤더의 pts와 SEI 파싱 결과가 담길 tags 바구니 가져옴</li>
  <li><code class="language-plaintext highlighter-rouge">-print_format json</code> : json 포맷으로 직렬화</li>
  <li><code class="language-plaintext highlighter-rouge">-read_intervals %+#[N]</code> : 파일 전체 스캔하지 않고, 시작점부터 딱 N개 프레임만 읽음</li>
</ul>

<p>tags는 모든 메타데이터를 담는 바구니이므로, SEI뿐만아니라 다른 데이터도 담김</p>

<h3 id="phase-1-로컬-버퍼-녹화-시작">Phase 1: 로컬 버퍼 녹화 시작</h3>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">def</span> <span class="nf">start_recording</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="n">log</span><span class="p">.</span><span class="nf">info</span><span class="p">(</span><span class="sh">"</span><span class="s">Phase 1: Recording all streams to local buffer...</span><span class="sh">"</span><span class="p">)</span>

        <span class="k">for</span> <span class="n">st</span> <span class="ow">in</span> <span class="n">self</span><span class="p">.</span><span class="n">states</span><span class="p">:</span>
            <span class="n">srt_url</span> <span class="o">=</span> <span class="nf">_srt_in_url</span><span class="p">(</span><span class="n">st</span><span class="p">.</span><span class="n">port</span><span class="p">)</span>
            <span class="n">cmd</span> <span class="o">=</span> <span class="p">[</span>
                <span class="sh">"</span><span class="s">ffmpeg</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">-y</span><span class="sh">"</span><span class="p">,</span>
                <span class="sh">"</span><span class="s">-loglevel</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">warning</span><span class="sh">"</span><span class="p">,</span>
                <span class="sh">"</span><span class="s">-fflags</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">+igndts</span><span class="sh">"</span><span class="p">,</span>
                <span class="sh">"</span><span class="s">-rtbufsize</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">2G</span><span class="sh">"</span><span class="p">,</span>
                <span class="sh">"</span><span class="s">-thread_queue_size</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">8192</span><span class="sh">"</span><span class="p">,</span>
                <span class="sh">"</span><span class="s">-i</span><span class="sh">"</span><span class="p">,</span> <span class="n">srt_url</span><span class="p">,</span>
                <span class="sh">"</span><span class="s">-c</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">copy</span><span class="sh">"</span><span class="p">,</span>                <span class="c1"># 디코딩 없이 비트스트림 그대로 복사
</span>                <span class="sh">"</span><span class="s">-f</span><span class="sh">"</span><span class="p">,</span> <span class="n">PASSTHROUGH_FORMAT</span><span class="p">,</span>
                <span class="n">st</span><span class="p">.</span><span class="n">buffer_path</span><span class="p">,</span>              <span class="c1"># 로컬 버퍼 파일 (예: buffer_0.ts)
</span>            <span class="p">]</span>
            <span class="n">rec</span> <span class="o">=</span> <span class="nc">FFmpegProc</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">rec-</span><span class="si">{</span><span class="n">st</span><span class="p">.</span><span class="n">name</span><span class="si">}</span><span class="sh">"</span><span class="p">,</span> <span class="n">cmd</span><span class="p">)</span>
            <span class="n">rec</span><span class="p">.</span><span class="nf">start</span><span class="p">()</span>
            <span class="n">self</span><span class="p">.</span><span class="n">_recorders</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">rec</span><span class="p">)</span>

        <span class="c1"># 파일이 생기도록 잠시 대기
</span>        <span class="n">time</span><span class="p">.</span><span class="nf">sleep</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>
</code></pre></div></div>

<h3 id="phase-2-첫-번째-프레임의-timecode-추출">Phase 2: 첫 번째 프레임의 timecode 추출</h3>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">def</span> <span class="nf">probe_timecodes</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">max_retries</span><span class="o">=</span><span class="mi">20</span><span class="p">,</span> <span class="n">interval</span><span class="o">=</span><span class="mf">2.0</span><span class="p">):</span>
        <span class="n">log</span><span class="p">.</span><span class="nf">info</span><span class="p">(</span><span class="sh">"</span><span class="s">Phase 2: Extracting timecodes from buffered streams...</span><span class="sh">"</span><span class="p">)</span>

        <span class="n">threads</span> <span class="o">=</span> <span class="p">[]</span>
        <span class="k">for</span> <span class="n">st</span> <span class="ow">in</span> <span class="n">self</span><span class="p">.</span><span class="n">states</span><span class="p">:</span>
            <span class="c1"># 4개의 스트림을 동시에 찔러보기 위해 스레드 생성
</span>            <span class="n">t</span> <span class="o">=</span> <span class="n">threading</span><span class="p">.</span><span class="nc">Thread</span><span class="p">(</span>
                <span class="n">target</span><span class="o">=</span><span class="n">self</span><span class="p">.</span><span class="n">_probe_one</span><span class="p">,</span> <span class="n">args</span><span class="o">=</span><span class="p">(</span><span class="n">st</span><span class="p">,</span> <span class="n">max_retries</span><span class="p">,</span> <span class="n">interval</span><span class="p">),</span>
                <span class="n">daemon</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
            <span class="p">)</span>
            <span class="n">threads</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">t</span><span class="p">)</span>
            <span class="n">t</span><span class="p">.</span><span class="nf">start</span><span class="p">()</span>
            
        <span class="k">for</span> <span class="n">t</span> <span class="ow">in</span> <span class="n">threads</span><span class="p">:</span>
            <span class="n">t</span><span class="p">.</span><span class="nf">join</span><span class="p">()</span> <span class="c1"># 모든 스트림의 타임코드를 찾을 때까지 대기
</span>
    <span class="k">def</span> <span class="nf">_probe_one</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">st</span><span class="p">:</span> <span class="n">StreamState</span><span class="p">,</span> <span class="n">retries</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">interval</span><span class="p">:</span> <span class="nb">float</span><span class="p">):</span>
        <span class="k">for</span> <span class="n">attempt</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">retries</span> <span class="o">+</span> <span class="mi">1</span><span class="p">):</span>
            <span class="c1"># ffprobe를 통해 SEI 타임코드 추출 (tags.timecode)
</span>            <span class="n">tc</span> <span class="o">=</span> <span class="nf">extract_timecode</span><span class="p">(</span><span class="n">st</span><span class="p">.</span><span class="n">buffer_path</span><span class="p">,</span> <span class="n">st</span><span class="p">.</span><span class="n">name</span><span class="p">)</span> 
            <span class="k">if</span> <span class="n">tc</span><span class="p">:</span>
                <span class="n">st</span><span class="p">.</span><span class="n">timecode</span> <span class="o">=</span> <span class="n">tc</span>  <span class="c1"># 찾은 절대 시간(Timecode) 저장
</span>                <span class="n">st</span><span class="p">.</span><span class="n">fps</span> <span class="o">=</span> <span class="nf">get_fps</span><span class="p">(</span><span class="n">st</span><span class="p">.</span><span class="n">buffer_path</span><span class="p">)</span>
                <span class="n">st</span><span class="p">.</span><span class="n">ready</span> <span class="o">=</span> <span class="bp">True</span>
                <span class="k">return</span>
            <span class="n">time</span><span class="p">.</span><span class="nf">sleep</span><span class="p">(</span><span class="n">interval</span><span class="p">)</span>
</code></pre></div></div>

<h3 id="phase-3-동기화-타겟-계산">Phase 3: 동기화 타겟 계산</h3>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>   <span class="k">def</span> <span class="nf">compute_alignment</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="n">log</span><span class="p">.</span><span class="nf">info</span><span class="p">(</span><span class="sh">"</span><span class="s">Phase 3: Computing alignment target...</span><span class="sh">"</span><span class="p">)</span>

        <span class="c1"># 가장 느린(타임코드가 큰) 스트림을 Reference로 선정
</span>        <span class="n">ref</span> <span class="o">=</span> <span class="nf">max</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">states</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">s</span><span class="p">:</span> <span class="n">s</span><span class="p">.</span><span class="n">timecode</span><span class="p">.</span><span class="nf">total_seconds</span><span class="p">())</span>
        
        <span class="c1"># Reference 시간에 TARGET_MARGIN_SEC (예: 5초)를 더해 타겟 설정
</span>        <span class="n">self</span><span class="p">.</span><span class="n">_target_sec</span> <span class="o">=</span> <span class="n">ref</span><span class="p">.</span><span class="n">timecode</span><span class="p">.</span><span class="nf">total_seconds</span><span class="p">()</span> <span class="o">+</span> <span class="n">TARGET_MARGIN_SEC</span>

        <span class="k">for</span> <span class="n">st</span> <span class="ow">in</span> <span class="n">self</span><span class="p">.</span><span class="n">states</span><span class="p">:</span>
            <span class="n">skip</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">_target_sec</span> <span class="o">-</span> <span class="n">st</span><span class="p">.</span><span class="n">timecode</span><span class="p">.</span><span class="nf">total_seconds</span><span class="p">()</span>
            <span class="n">log</span><span class="p">.</span><span class="nf">info</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">[</span><span class="si">{</span><span class="n">st</span><span class="p">.</span><span class="n">name</span><span class="si">}</span><span class="s">] must fast-forward ~</span><span class="si">{</span><span class="n">skip</span><span class="si">}</span><span class="s">s to reach target</span><span class="sh">"</span><span class="p">)</span>
</code></pre></div></div>

<h3 id="phase-4-타겟-프레임-탐색-및-패킷-조작">Phase 4: 타겟 프레임 탐색 및 패킷 조작</h3>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">_tail_aligned</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">st</span><span class="p">:</span> <span class="n">StreamState</span><span class="p">,</span> <span class="n">fifo_path</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">ready_event</span><span class="p">:</span> <span class="n">threading</span><span class="p">.</span><span class="n">Event</span><span class="p">):</span>
        <span class="c1"># 1. ffprobe로 타겟 타임코드(예: 12:00:07:00) 프레임이 버퍼에 쓰여질 때까지 기다렸다가 PTS 추출
</span>        <span class="n">target_pts</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">_find_target_pts_ffprobe</span><span class="p">(</span><span class="n">st</span><span class="p">)</span> 
        <span class="n">base_pts</span> <span class="o">=</span> <span class="n">target_pts</span>
        <span class="n">st</span><span class="p">.</span><span class="n">base_pts</span> <span class="o">=</span> <span class="n">base_pts</span>

        <span class="n">reader</span> <span class="o">=</span> <span class="nc">TSPacketReader</span><span class="p">(</span><span class="n">st</span><span class="p">.</span><span class="n">buffer_path</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">_shutdown</span><span class="p">)</span>
        <span class="n">reader</span><span class="p">.</span><span class="nf">open</span><span class="p">()</span>

        <span class="c1"># 2. 버퍼를 읽으며 정확히 target_pts를 가진 프레임까지 건너뜀(Skip)
</span>        <span class="k">for</span> <span class="n">pkt</span> <span class="ow">in</span> <span class="n">reader</span><span class="p">:</span>
            <span class="c1"># ... (PAT/PMT 파싱으로 비디오 PID 찾는 로직 생략) ...
</span>            <span class="k">if</span> <span class="n">pid</span> <span class="o">==</span> <span class="n">v_pid</span> <span class="ow">and</span> <span class="n">pusi</span><span class="p">:</span>
                <span class="n">pts</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="nf">pes_get_ts</span><span class="p">(</span><span class="nf">ts_payload</span><span class="p">(</span><span class="n">pkt</span><span class="p">))</span>
                <span class="k">if</span> <span class="n">pts</span> <span class="o">==</span> <span class="n">target_pts</span><span class="p">:</span>
                    <span class="n">target_pkt</span> <span class="o">=</span> <span class="n">pkt</span>
                    <span class="k">break</span> <span class="c1"># 동기화 시작점 발견!
</span>
        <span class="n">ready_event</span><span class="p">.</span><span class="nf">set</span><span class="p">()</span> <span class="c1"># 메인 스레드에 준비 완료 알림
</span>
        <span class="c1"># 3. FIFO를 열고, 이후의 패킷들을 리맵핑하여 기록
</span>        <span class="k">with</span> <span class="nf">open</span><span class="p">(</span><span class="n">fifo_path</span><span class="p">,</span> <span class="sh">"</span><span class="s">wb</span><span class="sh">"</span><span class="p">,</span> <span class="n">buffering</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span> <span class="k">as</span> <span class="n">dst</span><span class="p">:</span>
            <span class="n">dst</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="nf">remap_pkt</span><span class="p">(</span><span class="n">target_pkt</span><span class="p">,</span> <span class="n">base_pts</span><span class="p">))</span> <span class="c1"># 첫 프레임 기록
</span>            
            <span class="k">for</span> <span class="n">pkt</span> <span class="ow">in</span> <span class="n">reader</span><span class="p">:</span>
                <span class="c1"># 비디오 관련 PID(PAT, PMT, Video)만 통과
</span>                <span class="k">if</span> <span class="n">pid</span> <span class="ow">in</span> <span class="n">allowed_pids</span><span class="p">:</span>
                    <span class="c1"># 핵심: pkt의 타임스탬프에서 base_pts를 깎아내어 0으로 만듦
</span>                    <span class="n">remapped_pkt</span> <span class="o">=</span> <span class="nf">remap_pkt</span><span class="p">(</span><span class="n">pkt</span><span class="p">,</span> <span class="n">base_pts</span><span class="p">)</span> 
                    <span class="n">dst</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="n">remapped_pkt</span><span class="p">)</span>
</code></pre></div></div>
<ul>
  <li>TS 스트림은 PAT, PMT, stream 으로 분류
    <ul>
      <li>PAT를 통해 PMT를 찾아야 그 안에서 오디오, 비디오 스트림 분류 가능</li>
    </ul>
  </li>
</ul>

<h3 id="phase-5-mpegts-멀티플렉싱-및-최종-송출">Phase 5: MPEGTS 멀티플렉싱 및 최종 송출</h3>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">start_forwarding</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="n">log</span><span class="p">.</span><span class="nf">info</span><span class="p">(</span><span class="sh">"</span><span class="s">Phase 4: TS-level alignment + PTS remap via FIFO...</span><span class="sh">"</span><span class="p">)</span>

        <span class="c1"># 1. 프로세스 간 통신을 위한 Named FIFO 생성
</span>        <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="nf">len</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">states</span><span class="p">)):</span>
            <span class="n">os</span><span class="p">.</span><span class="nf">mkfifo</span><span class="p">(</span><span class="n">fifo_paths</span><span class="p">[</span><span class="n">i</span><span class="p">])</span>

        <span class="c1"># 2. 4개의 각 스트림마다 Tailer Thread(위의 phase 4) 실행
</span>        <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">st</span> <span class="ow">in</span> <span class="nf">enumerate</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">states</span><span class="p">):</span>
            <span class="n">t</span> <span class="o">=</span> <span class="n">threading</span><span class="p">.</span><span class="nc">Thread</span><span class="p">(</span><span class="n">target</span><span class="o">=</span><span class="n">self</span><span class="p">.</span><span class="n">_tail_aligned</span><span class="p">,</span> <span class="n">args</span><span class="o">=</span><span class="p">(</span><span class="n">st</span><span class="p">,</span> <span class="n">fifo_paths</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="p">...))</span>
            <span class="n">t</span><span class="p">.</span><span class="nf">start</span><span class="p">()</span>

        <span class="c1"># 3. 모든 스트림이 타겟 PTS를 찾을 때까지 메인 스레드 대기
</span>        <span class="k">for</span> <span class="n">evt</span> <span class="ow">in</span> <span class="n">self</span><span class="p">.</span><span class="n">_ready_events</span><span class="p">:</span>
            <span class="n">evt</span><span class="p">.</span><span class="nf">wait</span><span class="p">()</span>

        <span class="c1"># 4. 0으로 정렬된 FIFO 데이터를 입력받아 MPTS로 합치는 거대 FFmpeg 기동
</span>        <span class="n">cmd</span> <span class="o">=</span> <span class="p">[</span><span class="sh">"</span><span class="s">ffmpeg</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">-y</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">-copyts</span><span class="sh">"</span><span class="p">]</span>
        
        <span class="c1"># 4개의 리맵핑 완료된 FIFO 파이프를 입력으로 연결
</span>        <span class="k">for</span> <span class="n">fifo</span> <span class="ow">in</span> <span class="n">self</span><span class="p">.</span><span class="n">_fifos</span><span class="p">:</span>
            <span class="n">cmd</span> <span class="o">+=</span> <span class="p">[</span><span class="sh">"</span><span class="s">-f</span><span class="sh">"</span><span class="p">,</span> <span class="n">PASSTHROUGH_FORMAT</span><span class="p">,</span> <span class="sh">"</span><span class="s">-i</span><span class="sh">"</span><span class="p">,</span> <span class="n">fifo</span><span class="p">]</span>

        <span class="c1"># 코덱 복사 및 프로그램 맵핑 설정
</span>        <span class="n">cmd</span> <span class="o">+=</span> <span class="p">[</span><span class="sh">"</span><span class="s">-c:v</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">copy</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">-c:a</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">copy</span><span class="sh">"</span><span class="p">]</span>
        <span class="n">cmd</span> <span class="o">+=</span> <span class="p">[</span><span class="sh">"</span><span class="s">-program</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">program_num=1:st=0:st=1:st=2:st=3</span><span class="sh">"</span><span class="p">]</span>
        <span class="n">cmd</span> <span class="o">+=</span> <span class="p">[</span><span class="sh">"</span><span class="s">-f</span><span class="sh">"</span><span class="p">,</span> <span class="n">PASSTHROUGH_FORMAT</span><span class="p">,</span> <span class="n">srt_out_url</span><span class="p">]</span>

        <span class="n">self</span><span class="p">.</span><span class="n">_forwarder</span> <span class="o">=</span> <span class="nc">FFmpegProc</span><span class="p">(</span><span class="sh">"</span><span class="s">fwd-mpts</span><span class="sh">"</span><span class="p">,</span> <span class="n">cmd</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">_forwarder</span><span class="p">.</span><span class="nf">start</span><span class="p">()</span> <span class="c1"># 최종 SRT 송출 시작
</span></code></pre></div></div>

<h3 id="remap-pcrptsdts">Remap PCR,PTS,DTS</h3>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">PTS_WRAP</span> <span class="o">=</span> <span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="mi">33</span>              <span class="c1"># 90kHz PTS wrap-around (33-bit, 약 85899초)
</span>
<span class="k">def</span> <span class="nf">remap_pkt</span><span class="p">(</span><span class="n">pkt</span><span class="p">:</span> <span class="nb">bytes</span><span class="p">,</span> <span class="n">base90</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bytes</span><span class="p">:</span>
    <span class="sh">"""</span><span class="s">TS 패킷의 PCR / PES PTS / PES DTS에서 base90을 빼서 반환.</span><span class="sh">"""</span>
    <span class="c1"># 1. TS 패킷 유효성 검사 (188바이트, Sync Byte 0x47)
</span>    <span class="k">if</span> <span class="nf">len</span><span class="p">(</span><span class="n">pkt</span><span class="p">)</span> <span class="o">!=</span> <span class="n">TS_SZ</span> <span class="ow">or</span> <span class="n">pkt</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">!=</span> <span class="mh">0x47</span><span class="p">:</span>
        <span class="k">return</span> <span class="n">pkt</span>

    <span class="c1"># ----------------------------------------------------
</span>    <span class="c1"># 2. PCR (Program Clock Reference) 리맵핑
</span>    <span class="c1"># ----------------------------------------------------
</span>    <span class="n">pcr</span> <span class="o">=</span> <span class="nf">pcr_read</span><span class="p">(</span><span class="n">pkt</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">pcr</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">:</span>
        <span class="c1"># PCR은 27MHz 기반이므로 300을 나누어 90kHz(base)로 맞춘 뒤 연산하고 다시 복원
</span>        <span class="n">new_pcr</span> <span class="o">=</span> <span class="p">((</span><span class="n">pcr</span> <span class="o">//</span> <span class="mi">300</span> <span class="o">-</span> <span class="n">base90</span><span class="p">)</span> <span class="o">%</span> <span class="n">PTS_WRAP</span><span class="p">)</span> <span class="o">*</span> <span class="mi">300</span> <span class="o">+</span> <span class="p">(</span><span class="n">pcr</span> <span class="o">%</span> <span class="mi">300</span><span class="p">)</span>
        <span class="n">pkt</span> <span class="o">=</span> <span class="nf">pcr_write</span><span class="p">(</span><span class="n">pkt</span><span class="p">,</span> <span class="n">new_pcr</span><span class="p">)</span>

    <span class="c1"># ----------------------------------------------------
</span>    <span class="c1"># 3. PES Header의 PTS / DTS 리맵핑
</span>    <span class="c1"># ----------------------------------------------------
</span>    <span class="k">if</span> <span class="nf">ts_pusi</span><span class="p">(</span><span class="n">pkt</span><span class="p">):</span> <span class="c1"># PUSI가 1일 때만 (즉, PES 헤더가 존재할 때만)
</span>        <span class="n">afc</span> <span class="o">=</span> <span class="nf">ts_afc</span><span class="p">(</span><span class="n">pkt</span><span class="p">)</span>
        
        <span class="c1"># Adaptation Field 존재 여부에 따라 페이로드(PES 헤더) 시작 오프셋 계산
</span>        <span class="k">if</span> <span class="n">afc</span> <span class="o">==</span> <span class="mi">1</span><span class="p">:</span>
            <span class="n">off</span> <span class="o">=</span> <span class="mi">4</span>
        <span class="k">elif</span> <span class="n">afc</span> <span class="o">==</span> <span class="mi">3</span><span class="p">:</span>
            <span class="n">off</span> <span class="o">=</span> <span class="mi">5</span> <span class="o">+</span> <span class="n">pkt</span><span class="p">[</span><span class="mi">4</span><span class="p">]</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="k">return</span> <span class="n">pkt</span>

        <span class="n">pl</span> <span class="o">=</span> <span class="n">pkt</span><span class="p">[</span><span class="n">off</span><span class="p">:]</span> <span class="c1"># 여기서부터가 순수 PES 구역
</span>        
        <span class="c1"># PES Start Code (0x000001) 확인 및 헤더 파싱
</span>        <span class="k">if</span> <span class="nf">len</span><span class="p">(</span><span class="n">pl</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="mi">14</span> <span class="ow">and</span> <span class="n">pl</span><span class="p">[:</span><span class="mi">3</span><span class="p">]</span> <span class="o">==</span> <span class="sa">b</span><span class="sh">"</span><span class="se">\x00\x00\x01</span><span class="sh">"</span><span class="p">:</span>
            <span class="n">pts</span><span class="p">,</span> <span class="n">dts</span> <span class="o">=</span> <span class="nf">pes_get_ts</span><span class="p">(</span><span class="n">pl</span><span class="p">)</span>
            
            <span class="k">if</span> <span class="n">pts</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">:</span>
                <span class="c1"># 핵심 연산: 현재 값에서 base_pts를 빼고 33비트 모듈러 연산
</span>                <span class="n">new_pts</span> <span class="o">=</span> <span class="p">(</span><span class="n">pts</span> <span class="o">-</span> <span class="n">base90</span><span class="p">)</span> <span class="o">%</span> <span class="n">PTS_WRAP</span>
                <span class="n">new_dts</span> <span class="o">=</span> <span class="p">(</span><span class="n">dts</span> <span class="o">-</span> <span class="n">base90</span><span class="p">)</span> <span class="o">%</span> <span class="n">PTS_WRAP</span> <span class="k">if</span> <span class="n">dts</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span> <span class="k">else</span> <span class="bp">None</span>
                
                <span class="c1"># 조작된 값으로 PES 헤더 바이트를 다시 생성 (Bitwise 인코딩)
</span>                <span class="n">new_pl</span> <span class="o">=</span> <span class="nf">pes_set_ts</span><span class="p">(</span><span class="n">pl</span><span class="p">,</span> <span class="n">new_pts</span><span class="p">,</span> <span class="n">new_dts</span><span class="p">)</span>
                
                <span class="c1"># 원래 TS 패킷의 앞부분 + 조작된 PES 헤더 + 뒷부분을 이어붙임
</span>                <span class="n">pkt</span> <span class="o">=</span> <span class="n">pkt</span><span class="p">[:</span><span class="n">off</span><span class="p">]</span> <span class="o">+</span> <span class="n">new_pl</span> <span class="o">+</span> <span class="n">pkt</span><span class="p">[</span><span class="n">off</span> <span class="o">+</span> <span class="nf">len</span><span class="p">(</span><span class="n">new_pl</span><span class="p">):]</span>
                
    <span class="k">return</span> <span class="n">pkt</span>
</code></pre></div></div>
<ul>
  <li>MPEGTS에서는 90kHz를 PTS/DTS의 단위로 사용하는 것이 규격에서 갖게하는 표준
    <ul>
      <li>모든 비디오/오디오 프레임 레이트를 소수점 없이 딱 떨어지는 정수로 나누기 위한 최소공배수이기 때문
        <ul>
          <li>24fps : 3750 tick</li>
          <li>25fps : 3600 tick</li>
          <li>30fps : 3000 tick</li>
          <li>60fps : 1500 tick</li>
          <li>어떤 fps든 pts가 90000 증가했다면 1초 증가</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>PTS는 프레임을 화면에 뿌리는 시간이지만 PCR은 수신 단말기(TV)의 PCR은 하드웨어 마스터 클럭을 인코더와 똑같이 맞추기 위한 신호
    <ul>
      <li>90kHz보다 훨씬 정밀한 27MHz를 사용</li>
      <li>따라서 90kHz의 1틱에서 PCR은 300틱을 올림</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">new_pcr = ((pcr // 300 - base90) % PTS_WRAP) * 300 + (pcr % 300)</code>
    <ul>
      <li>base_pts는 90kHz 단위이므로 pcr을 300으로 나눈 몫 대상으로 빼주고 ts에서의 pts범위인 PTS_WRAP으로 모듈러 연산을 진행</li>
      <li>그리고 <code class="language-plaintext highlighter-rouge">//</code>연산으로 잘려나간 300미만의 값은 다시 더해줘서 보정</li>
    </ul>
  </li>
</ul>

<p><br /></p>
<hr />

<p><br /></p>

<h3 id="송수신-sync-원리">송수신 sync 원리</h3>
<ul>
  <li>PCR은 송출에서 보내는 신호고 수신기 내부에는 STC(System Time Clock)이라고 하는 내부 타이머가 존재함</li>
  <li>만약에 STC를 PCR과 동기화된 상태라면 PTS대로 재생했을 때 싱크가 완벽히 맞음을 보장함</li>
  <li>PCR 12:00:00을 수신했는데 STC가 11:59:58이었다면 2초 딜레이시켜서 STC를 12:00:00으로 맞춤</li>
</ul>

<h3 id="오디오-비디오-타임라인">오디오 비디오 타임라인</h3>
<p><img src="https://imucoding.github.io/assets/images/multi-sync3.png" alt="img.png" /></p>

<ul>
  <li>오디오 프레임 1개당 비디오 프레임 1개씩 매핑되는것이 아니라 각각으로 동작</li>
  <li>수신기 STC는 1개를 공통으로 쓰지만 오디오 비디오가 각각의 PTS tick을 가지고 정해진 시점에 재생됨</li>
</ul>

<p>ex)</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[ STC 시간에 따른 디코더 출력 시나리오 ]
STC = 0 : 비디오 1번 프레임 출력, 오디오 1번 프레임 출력 (동시 시작)
STC = 1500 : 비디오 2번 프레임 출력 (오디오는 아직 대기 중)
STC = 1920 : 오디오 2번 프레임 출력 (비디오는 그대로 유지)
STC = 3000 : 비디오 3번 프레임 출력
STC = 3840 : 오디오 3번 프레임 출력
STC = 4500 : 비디오 4번 프레임 출력
</code></pre></div></div>]]></content><author><name>Hyunjune</name></author><category term="media" /><category term="timecode" /><category term="sync" /><category term="multi-source" /><category term="playout" /><summary type="html"><![CDATA[Environment 4개의 테라덱은 각 소스 스트림을 송출함 각 테라덱은 부팅 시점에 pts를 0으로 초기화하므로 4개의 테라덱이 정확히 동시에 켜지지 않는 이상 동일 프레임에 대해 pts가 상이함 타임 코드는 동일 ntp 서버 기준으로 동일 프레임에 대해 같은 타임코드로 삽입됨 네트워크를 통해 오는 각 스트림은 도착 속도가 다 제각각이므로 동일 프레임이 동일 타이밍에 수신되지 않음, 즉 수신하는 프레임의 타임코드가 정렬되어 있지 않음]]></summary></entry><entry><title type="html">KSQLDB Stream &amp;amp; Table</title><link href="https://imucoding.github.io/kafka/2024/10/09/stream.html" rel="alternate" type="text/html" title="KSQLDB Stream &amp;amp; Table" /><published>2024-10-09T00:00:00+00:00</published><updated>2024-10-09T00:00:00+00:00</updated><id>https://imucoding.github.io/kafka/2024/10/09/stream</id><content type="html" xml:base="https://imucoding.github.io/kafka/2024/10/09/stream.html"><![CDATA[<h3 id="stream-개요">Stream 개요</h3>
<ul>
  <li>stream은 메시지 streaming 처리를 위해서 추상화한 layer</li>
</ul>

<p><img src="https://imucoding.github.io/assets/images/stream1.png" alt="img.png" /></p>

<ul>
  <li>변경 불가하며 append만 가능한 collection</li>
  <li>연속된 historical 정보 표현</li>
  <li>insert된 row는 변경 불가하며 (update, delete 불가) 새로 insert된 row는 stream의 마지막에 추가됨</li>
  <li>stream은 key를 가질 수도 그렇지 않을 수도 있으며 동일한 key 값을 가진 row는 동일한 파티션에 저장됨</li>
</ul>

<h4 id="stream--table-연속-처리">Stream / Table 연속 처리</h4>
<ul>
  <li>stream과 table은 topic을 source로 할 수 있으며, 다른 stream 또는 table을 source로 입력 받아 연속적인 processing 가능</li>
</ul>

<p><br /></p>

<h3 id="stream과-table-차이">Stream과 Table 차이</h3>
<ul>
  <li>stream은 key 값의 중복 여부와 상관 없이 연속적으로 메시지 관리 (stateless)</li>
  <li>table은 primary key로 중복을 허용하지 않으며 동일 primary key로 메시지가 입력될 경우 해당 값을 업데이트 함 (stateful)</li>
</ul>

<p><img src="https://imucoding.github.io/assets/images/streamtable.png" alt="img.png" /></p>

<h4 id="실습">실습</h4>
<p>토픽 생성</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kafka-topics --bootstrap-server localhost:9092 --create --topic simple_user_stream
</code></pre></div></div>
<p>토픽에 메시지 전송</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kafka-console-producer --bootstrap-server localhost:9092 --topic simple_user_stream

&gt; {"id":1, "name":"name1", "email":"email1"}
{"id":2, "name":"name2", "email":"email2"}
{"id":3, "name":"name3", "email":"email3"}
{"id":4, "name":"name4", "email":"email4"}
</code></pre></div></div>
<p>stream 생성 및 select로 스트림 조회</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>create stream simple_user_stream
(
  id int,
  name varchar,
  email varchat
) with
(
  kafka_topic = 'simple_user_stream',
  value_format = 'JSON'
);

select * from simple_user_stream;
</code></pre></div></div>
<ul>
  <li>with 절은 topic과 format에 대한 설정 부과 가능</li>
  <li>만약 기존 토픽이 없는 경우 새롭게 생성하는데 이 때 반드시 with 절에 partitions 옵션으로 파티션 개수 지정해야함
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>show streams;  // stream 목록 확인
drop stream simple_user_stream delete topic; // stream 삭제하며 topic 함께 삭제
</code></pre></div>    </div>
  </li>
</ul>

<h4 id="key를-가지는-stream-생성">key를 가지는 stream 생성</h4>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>create stream simple_user_stream
(
  id int key,
  name varchar,
  email varchar
) with (
  kafka_topic = 'simple_user_stream',
  key_format = 'KAFKA',
  value_format = 'JSON',
  partitions = 1
);
</code></pre></div></div>
<ul>
  <li>ksqlDB는 여러가지 직렬화 포맷으로 topic 메시지를 읽고 쓸 수 있음</li>
  <li>직렬화 포맷은 key, value 각각에 별도로 지정 가능, 만약 동일하다면 <code class="language-plaintext highlighter-rouge">format</code>으로 지정 가능</li>
</ul>

<p><img src="https://imucoding.github.io/assets/images/serializationformat.png" alt="img.png" /></p>
<ul>
  <li>직렬화 포맷 유형
    <ul>
      <li><code class="language-plaintext highlighter-rouge">KAFKA</code> : number(Integer, Float 등)과 String 등의 primitive 직렬화에 사용되며 보통 key 직렬화 포맷으로 사용</li>
      <li><code class="language-plaintext highlighter-rouge">JSON</code> : json 포맷 직렬화</li>
      <li><code class="language-plaintext highlighter-rouge">AVRO</code> : avro 포맷 직렬화. schema registry 사용 필요</li>
      <li><code class="language-plaintext highlighter-rouge">DELIMITED</code> : csv 포맷 직렬화</li>
      <li><code class="language-plaintext highlighter-rouge">PROTOBUF</code> : protocol buffer 형태 직렬화</li>
    </ul>
  </li>
</ul>

<p><br /></p>

<h3 id="pull-쿼리와-push-쿼리">Pull 쿼리와 Push 쿼리</h3>
<h4 id="pull-쿼리">Pull 쿼리</h4>
<ul>
  <li>쿼리를 실행한 시점에, 쿼리 조건에 맞는 데이터를 KSQLDB에서 가져옴. 한번 수행 후 쿼리 종료됨
<code class="language-plaintext highlighter-rouge">select * from stream;</code></li>
</ul>

<h4 id="push-쿼리">Push 쿼리</h4>
<ul>
  <li>쿼리 실행 이후 지속적으로 stream / table을 모니터링 하면서 쿼리 조건에 맞는 데이터가 추가 (변경) 되었을 때마다 데이터를 계속 가져옴</li>
  <li>종료 시그널을 주지 않는 이상 쿼리가 종료되지 않음
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> select * from stream emit changes;
</code></pre></div>    </div>
  </li>
</ul>

<h4 id="pull-쿼리-제약">Pull 쿼리 제약</h4>
<ul>
  <li>stream에 group by 적용 시 pull 쿼리 수행 불가
    <ul>
      <li>stream은 stateful한 처리 없이 append만 수행하여 데이터가 엄청 많은데 쿼리 날릴 때마다 group by (stateful)한 처리를 수행하면 성능 이슈 발생</li>
    </ul>
  </li>
  <li>topic을 source로 하는 table에 pull 쿼리 수행 불가
    <ul>
      <li>table이 source를 topic으로 하는 경우 mview가 아니므로 쿼리 수행 시점에 rocksdb 인스턴스가 새로 떠서 작업 처리함. 따라서 pull 쿼리와 같이 빈도가 잦은 쿼리는 rocksdb에 부담</li>
    </ul>
  </li>
  <li>pull 쿼리는 수행 시 마다 새로운 consumer group과 consumer가 생성되고 종료됨</li>
  <li>push 쿼리는 쿼리가 유지되는 동안 계속 consumer group과 consumer가 유지됨</li>
</ul>

<h4 id="ksql-cli">KSQL CLI</h4>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>show topics;
set 'auto.offset.reset` = 'earliest';
print user_stream from beginning;
</code></pre></div></div>
<ul>
  <li>토픽 및 메시지 확인
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>describe user_stream extended;
</code></pre></div>    </div>
  </li>
  <li>stream / table의 DDL, 데이터 타입 등 주요 메타 정보 확인 가능</li>
</ul>

<h4 id="query와-consumer-동작">Query와 Consumer 동작</h4>
<p><img src="https://imucoding.github.io/assets/images/queryconsumer.png" alt="img.png" /></p>

<ul>
  <li>쿼리는 수행 상태를 가짐</li>
  <li>동일한 쿼리더라도 select / insert로 수행되는 쿼리는 서로 다른 ID를 가짐</li>
  <li>mview에서 사용되는 CSAS, CTAS 쿼리는 동일한 쿼리일 경우 동일한 ID를 가짐</li>
</ul>

<h4 id="pull-쿼리와-push-쿼리-활용도">Pull 쿼리와 Push 쿼리 활용도</h4>
<ul>
  <li>pull 쿼리는 ksql CLI나 application에서 최종 데이터 추출하는데 사용</li>
  <li>push 쿼리는 materialized view에 변경된 데이터를 반영하는 데 주로 사용 (CLI 기반 조회도 가능)</li>
</ul>

<h4 id="alter-ddl">ALTER DDL</h4>
<ul>
  <li>ksqlDB는 컬럼의 변경, 삭제를 허용하지 않고 오직 컬럼의 추가만 허용</li>
  <li>ksqlDB의 컬럼은 무조건 nullable이며 not null 제약 조건 부여 불가하지만 table의 경우 primary key에 대해서는 not null 묵시적 적용
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>alter stream user_stream add column phone varchar;
</code></pre></div>    </div>
  </li>
</ul>

<p><br /></p>

<h3 id="table-생성하기">Table 생성하기</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>create table user_table
(
  id int primary key,
  name varchar,
  email varchar
) with
(
  kafka_topic = 'user_table',
  key_format = 'KAFKA',
  value_format = 'JSON',
  partitions = 1
);
</code></pre></div></div>

<h4 id="topic을-소스로-가지는-table에서-rocksdb-메커니즘">Topic을 소스로 가지는 Table에서 RocksDB 메커니즘</h4>
<p><img src="https://imucoding.github.io/assets/images/tablerocksdb.png" alt="img.png" /></p>

<ul>
  <li>push 쿼리 수행 시 rocksDB가 기동되어 stateful SQL 수행</li>
  <li>rocksDB instance는 쿼리 생성 시 생성되었다가 쿼리가 끝나면 종료됨</li>
  <li>change log는 push 쿼리 수행 중에 데이터 변경 사항을 내부 토픽으로 가지며 rocksDB의 복구 시 사용</li>
  <li>rocksDB의 기본 storage 디렉토리는 <code class="language-plaintext highlighter-rouge">/tmp/kafka-streams</code>이며  <code class="language-plaintext highlighter-rouge">ksql.streams.state.dir</code> 설정으로 변경 가능</li>
  <li>쿼리 종료 시 change 내부 토픽 및 rocksDB 파일 모두 삭제됨</li>
</ul>

<p><br /></p>

<h3 id="date--time--timestamp-타입-설명">DATE / TIME / TIMESTAMP 타입 설명</h3>
<ul>
  <li><code class="language-plaintext highlighter-rouge">DATE</code> : 시분초를 제외한 월일년만 가지는 temporal 타입 (2023-02-01)</li>
  <li><code class="language-plaintext highlighter-rouge">TIME</code> : 월일년을 제외한 시분초만 가지는 temporal 타입 (11:59:59)</li>
  <li><code class="language-plaintext highlighter-rouge">TIMESTAMP</code> : 월일년 시분초, 밀리세컨드까지 가지는 temporal 타입 (2023-02-01 11:59:59.010)</li>
  <li><code class="language-plaintext highlighter-rouge">Time Units</code> : temporal 관련 함수나 windows 절에서 사용되는 time unit
    <ul>
      <li>DAYS / HOURS / MINUTES / SECONDS / MILLISECONDS 등</li>
    </ul>
  </li>
  <li>RDBMS와 달리 datetime은 없음</li>
</ul>

<h4 id="문자열-변환-함수">문자열 변환 함수</h4>
<ul>
  <li>temporal 타입을 문자열로 변경할 때는 FORMAT_XXX() 함수를, 문자열을 temporal 타입으로 변경할 떄는 PARSE_XXX() 함수를 사용</li>
  <li>temporal에서 문자열 변환
    <ul>
      <li>FORMAT_DATE(Date, ‘yyyy-MM-dd’)</li>
      <li>FORMAT_TIME(Time, ‘HH:mm:ss’)</li>
      <li>FORMAT_TIMESTAMP(Timestamp, ‘yyyy-MM-dd HH:mm:ss’)</li>
    </ul>
  </li>
  <li>문자열에서 temporal 변환
    <ul>
      <li>PARSE_DATE(‘2023-02-01’, ‘yyyy-MM-dd’)</li>
      <li>PARSE_TIME(‘11:59:59’, ‘HH:mm:ss’)</li>
      <li>PARSE_TIMESTAMP(‘2023-02-01 11:59:59’, ‘yyyy-MM-dd HH:mm:ss’)</li>
    </ul>
  </li>
</ul>

<h4 id="unix-epoch-일자--milliseconds-값-변환">Unix epoch 일자 / Milliseconds 값 변환</h4>
<ul>
  <li>Date / Timestamp 값을 int / big int 형 unix epoch 일자 / Milliseconds 값으로 변환할 경우 UNIX_XXX() 함수를 사용</li>
  <li>int / big int 형 unix epoch 일자 / Milliseconds 값을 Date / Time 값으로 변환할 때는 FROM_XXX() 사용</li>
  <li>unix epoch 일자 / milliseconds 값으로 변환
    <ul>
      <li>UNIX_DATE(Date)</li>
      <li>UNIX_TIMESTAMP(Timestamp)</li>
    </ul>
  </li>
  <li>Date / Timestamp 값으로 변환
    <ul>
      <li>FROM_DAYS(unix epoch 일자)</li>
      <li>FROM_UNIXTIME(unix epoch milliseconds)</li>
    </ul>
  </li>
</ul>

<h4 id="더하기--빼기-연산">더하기 / 빼기 연산</h4>
<ul>
  <li>temporal 값에 일정 time unit을 더하고 빼기 위해서 DATEADD() 등의 함수가 지원됨
    <ul>
      <li>time unit : DAYS / HOURS / MINUTES / SECONDS / MILLISECONDS</li>
    </ul>
  </li>
  <li>DATEADD(time units, 기간, Date 컬럼명)과 같이 사용될 수 있으며 DATEADD는 DAYS만 사용 가능</li>
  <li>더하기
    <ul>
      <li>DATEADD(DAYS, 7, Date)</li>
      <li>TIMEADD(HOURS, 7 Time)</li>
      <li>TIMESTAMPADD(MINUTES, 10, Timestamp)</li>
    </ul>
  </li>
  <li>빼기
    <ul>
      <li>DATESUB(DAYS, 7, Date)</li>
      <li>TIMESUB(HOURS, 7 Time)</li>
      <li>TIMESTAMPSUB(MINUTES, 10, Timestamp)</li>
    </ul>
  </li>
</ul>]]></content><author><name>Hyunjune</name></author><category term="kafka" /><category term="ksqldb" /><category term="stream" /><category term="table" /><category term="pull query" /><category term="push query" /><summary type="html"><![CDATA[Stream 개요 stream은 메시지 streaming 처리를 위해서 추상화한 layer]]></summary></entry><entry><title type="html">KSQLDB 개요</title><link href="https://imucoding.github.io/kafka/2024/10/08/ksqldb-1.html" rel="alternate" type="text/html" title="KSQLDB 개요" /><published>2024-10-08T00:00:00+00:00</published><updated>2024-10-08T00:00:00+00:00</updated><id>https://imucoding.github.io/kafka/2024/10/08/ksqldb-1</id><content type="html" xml:base="https://imucoding.github.io/kafka/2024/10/08/ksqldb-1.html"><![CDATA[<h3 id="ksqldb">KSQLDB</h3>
<p>SQL 만으로 쉽게 streaming 데이터의 가공/ 변환/ 분석 수행</p>

<h4 id="적용-사례">적용 사례</h4>
<ul>
  <li>고객 360</li>
  <li>데이터 웨어하우스</li>
  <li>사기 탐지</li>
  <li>IoT</li>
  <li>스트리밍 데이터 파이프라인</li>
  <li>고객 충성도 시스템 구축</li>
  <li>개인 맞춤형 프로모션 생성</li>
  <li>실시간 주문 배송 추적</li>
  <li>지리적 위치 기반 알림 및 프로모션 생성</li>
</ul>

<p><br /></p>

<h3 id="streams-api-기반-데이터-처리">Streams API 기반 데이터 처리</h3>
<p><img src="https://imucoding.github.io/assets/images/streamsapi.png" alt="img.png" /></p>
<ul>
  <li>streams 라이브러리를 통해 제공하는 단 한번의 데이터 처리, 장애 허용 시스템 등의 특징들은 컨슈머와 프로듀서 조합만으로 완벽하게 구현 어려움</li>
  <li>source topic, sink topic이 다른 카프카 클러스터인 경우 streams 지원하지 않으므로 producer, consumer 사용</li>
</ul>

<p><img src="https://imucoding.github.io/assets/images/streamsscaleout.png" alt="img.png" /></p>
<ul>
  <li>스트림즈 애플리케이션은 내부적으로 스레드 1개 이상 생성 가능하고, 스레드는 1개 이상의 태스크를 가짐</li>
  <li>task는 스트림즈 애플리케이션을 실행하면 생기는 데이터 처리 최소 단위</li>
  <li>만약 3개의 파티션으로 이뤄진 토픽을 처리하는 스트림즈 애플리케이션을 실행하면 내부에 3개의 태스크가 생김</li>
  <li>스트림즈는 컨슈머 스레드를 늘리는 바업과 동일하게 병렬처리를 위해 토픽 파티션과 스트림즈 thread 개수를 늘림으로써 처리량 늘림</li>
  <li>스트림즈 애플리케이션은 장애에도 안정적으로 운영 가능하도록 스케일 아웃 가능</li>
</ul>

<h4 id="토폴로지">토폴로지</h4>
<ul>
  <li>2개 이상의 노드와 선으로 이루어진 집합</li>
  <li>
    <p>스트림즈에서는 트리형과 비슷한 토폴로지 사용
<img src="https://imucoding.github.io/assets/images/topology.png" alt="img.png" /></p>
  </li>
  <li>토폴로지를 이루는 노드를 하나의 프로세서라고 부름</li>
  <li>노드와 노드를 이은 선을 스트림이라고 부름</li>
  <li>스트림은 토픽의 데이터를 의미하는 레코드와 동일한 개념</li>
</ul>

<h4 id="스트림즈-dsl과-프로세서-api">스트림즈 DSL과 프로세서 API</h4>
<ul>
  <li>스트림즈 DSL과 프로세서 API 2가지 방법으로 개발 가능</li>
  <li>스트림즈 DSL은 KTable, KStream 활용</li>
  <li>프로세서  API는 스트림즈 DSL에서 제공하지 않는 기능, 로직을 Processor 차원의 투박한 코드로 구현 가능
    <ul>
      <li>스트림즈 DSL의 KTable, KStream 등 개념이 없음</li>
    </ul>
  </li>
</ul>

<h4 id="streams-api-기반의-ksqldb">Streams API 기반의 KSQLDB</h4>
<p><img src="https://imucoding.github.io/assets/images/streamsbased.png" alt="img.png" /></p>
<ul>
  <li>KsqlDB 기반으로 실시간 Streaming 데이터 처리 중심 축 이동</li>
  <li>대부분의 Streams API 기능을 KsqlDB로 수행 가능하며 실시간 Streaming 데이터 처리 중심축이 KsqlDB로 급격히 이동</li>
</ul>

<h3 id="rocksdb-개요">RocksDB 개요</h3>
<ul>
  <li>facebook에서 만든 key-value 기반의 embedded database</li>
  <li>mysql, mongodb의 storage engine으로도 제공됨</li>
  <li>경량 DB이지만 대용량 데이터에서도 빠른 처리</li>
  <li>뛰어난 압축 효율, read 뿐만 아니라 특히 write 성능이 뛰어남</li>
  <li>c++로 짜여졌으며 Java로도 interface 제공</li>
  <li>분산 DB가 아니며 single 노드 DB 엔진임</li>
</ul>

<h4 id="kafka-에서의-rocksdb">Kafka 에서의 RocksDB</h4>
<ul>
  <li>kafka의 분산 처리 핵심은 topic의 partition임</li>
  <li>rocksDB는 분산 DB가 아니기 때문에 여러 개의 partition 들이 있을 경우 rocksDB는 개별 partition 별로 구동됨</li>
  <li>streams / ksqlDB 작업 시 partition 별로 rocksDB instance가 생성되어 구동되며 개별 rocksDB는 서로 관여하지 않음</li>
</ul>

<p><br /></p>

<h3 id="process-task-thread-및-병렬-처리">Process, Task, Thread 및 병렬 처리</h3>
<p><img src="https://imucoding.github.io/assets/images/parellel.png" alt="img.png" /></p>
<ul>
  <li>단일 ksqlDB는 하나의 process로 구성됨</li>
  <li>CLI 등으로 SQL이 ksqlDB에서 수행될 떄 해당 SQL이 topology로 재구성됨</li>
  <li>topology는 ksqlDB가 수행해야 할 task로 부여되어 thread가 할당되어 수행됨</li>
  <li>topology는 입력 topic의 partition 수에 따라 sub-topology로 만들어지고 이들이 개별 task로 할당됨
    <ul>
      <li>partition의 개수가 3개인 경우 task도 3개</li>
    </ul>
  </li>
  <li>ksqlDB에서 일반적으로 task별로 stream thread가 할당되어 작업되나 task 개수가 최대 thread 개수를 초과 시 1개의 stream thread가 여러 개의 task를 수행할 수 있음</li>
  <li>개별 stream task를 수행하는 개별 stream thread들은 서로 데이터를 주고 받지 않음</li>
  <li><code class="language-plaintext highlighter-rouge">ksql.streams.num.stream.threads</code>로 SQL 수행 시 할당되는 threads의 최대 개수를 지정</li>
</ul>

<p><img src="https://imucoding.github.io/assets/images/streamthread.png" alt="img.png" /></p>
<ul>
  <li>topic A와 B는 모두 3개의 파티션을 가지므로 3개의 sub topology를 만들고 이를 3개의 task로 생성</li>
  <li>최대 사용할 수 있는 stream thread가 2개일 경우 3개의 task를 2개의 stream thread에 할당</li>
</ul>

<p><br /></p>

<h3 id="ksqldb-환경-설정-및-기동">KsqlDB 환경 설정 및 기동</h3>
<p>1) zookeeper 기동 <br />
2) kafka 기동 <br />
3) schema registry <br />
4) ksqlDB 기동 <br /></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bootstrap.servers = localhost:9092
ksql.schema.registry.url = http://localhost:8081
</code></pre></div></div>
<ul>
  <li>ksql-server.properties 수정</li>
</ul>]]></content><author><name>Hyunjune</name></author><category term="kafka" /><category term="ksqldb" /><category term="streams api" /><category term="rocksdb" /><summary type="html"><![CDATA[KSQLDB SQL 만으로 쉽게 streaming 데이터의 가공/ 변환/ 분석 수행]]></summary></entry><entry><title type="html">Schema Registry</title><link href="https://imucoding.github.io/kafka/2024/09/09/schemaregistry.html" rel="alternate" type="text/html" title="Schema Registry" /><published>2024-09-09T00:00:00+00:00</published><updated>2024-09-09T00:00:00+00:00</updated><id>https://imucoding.github.io/kafka/2024/09/09/schemaregistry</id><content type="html" xml:base="https://imucoding.github.io/kafka/2024/09/09/schemaregistry.html"><![CDATA[<h3 id="schema-registry">Schema Registry</h3>
<ul>
  <li>confluent kafka는 schema registry를 통해 schema 정보를 별도로 관리하는 기능 제공</li>
  <li>토픽으로 전송되는 data의 schema는 schema registry에서 ID + version 별로 중앙 관리 되므로 레코드 별로 schema를 중복해서 전송할 필요가 없음</li>
</ul>

<p><img src="https://imucoding.github.io/assets/images/schemaregistry.png" alt="img.png" /></p>

<h4 id="schema-registry-역할">Schema Registry 역할</h4>
<ul>
  <li>schema 전송 없이 레코드 값만 kafka로 전송할 수 있게 해줌</li>
  <li>kafka 메시지의 schema 제약 조건을 유지하면서 schema 변화에 대한 일정 수준 호환성을 제공</li>
</ul>

<p><br /></p>
<hr />

<h3 id="avro-개요-및-장점">Avro 개요 및 장점</h3>
<ul>
  <li>데이터 직렬화를 수행하는 시스템, 프레임워크</li>
  <li>보다 컴팩트하고 빠른 binary 데이터 포맷 제공</li>
  <li>json 포맷으로 쉽게 스키마 메시지를 정의</li>
  <li>다양한 데이터 타입을 제공 (string, bytes, long, float, double, boolean 뿐만 아니라 enum, arrays, maps 등의 복잡한 데이터 타입도 지원)</li>
</ul>

<h4 id="메시지-포맷-비교">메시지 포맷 비교</h4>
<p>Json (schemaless)</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"user_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">53</span><span class="p">,</span><span class="w">
  </span><span class="nl">"timestamp"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="mi">1232144232</span><span class="p">,</span><span class="w">
  </span><span class="nl">"address"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"경기도 성남시 ..."</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Schema + Avro payload</p>

<p><img src="https://imucoding.github.io/assets/images/avro.png" alt="img.png" /></p>
<ul>
  <li>avro payload는 바이너리 포맷이며 필드, 타입 정보 등이 포함되지 않음</li>
  <li>json + schema 보다 훨씬 빠르며 스키마 레지스트리에 최적화</li>
</ul>

<p>Schema ID + Avro payload</p>

<p><img src="https://imucoding.github.io/assets/images/schemaid.png" alt="img.png" /></p>

<p><br /></p>
<hr />

<h3 id="avro-메시지를-kafka-utility로-보내고-읽기">Avro 메시지를 Kafka Utility로 보내고 읽기</h3>
<ul>
  <li>avro 메시지는 avro 이진 파일 형태로 변환 된 후 직렬화되므로 별도의  class인 AvroSerializer 이용해야 함</li>
  <li><code class="language-plaintext highlighter-rouge">kafka-avro-console-producer</code>, <code class="language-plaintext highlighter-rouge">kafka-avro-console-consumer</code> 유틸리티를 이용하면 손쉽게 avro 메시지를 kafka 토픽 메시지로 전송하고 읽을 수 있음</li>
  <li><code class="language-plaintext highlighter-rouge">kafka-avro-console-producer</code>, <code class="language-plaintext highlighter-rouge">kafka-avro-console-consumer</code>는 모두 <code class="language-plaintext highlighter-rouge">schema registry</code> 기반 동작함</li>
</ul>

<h4 id="avro와-json의-차이">Avro와 Json의 차이</h4>
<ul>
  <li>avro는 json 포맷의 schema 정보를 가지고 payload를 byte 값으로 가진 형태이고, json은 그 자체는 schema가 없지만 JsonSerializer 사용에 스키마 설정을 하면 record에 schema와 payload가 모두 json 형식으로 붙어서 생성됨</li>
  <li>avro 사용 시 schema registry를 통해 schema를 따로 보관하고 schema ID + payload(byte)의 compact한 데이터로 전송이 가능</li>
</ul>

<h4 id="avro-메시지-전송-및-읽기">Avro 메시지 전송 및 읽기</h4>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>avro 메시지 보내기

kafka-avro-console-producer --broker-list localhost:9092 --topic avro-test --property value.schema=
'
 {
    "type": "record",
    "name": "customer_short",
    "fields": [
                {"name": "customer_id", "type": "int"},
                {"name": "customer_name", "type": "string"}
              ]
 }
'
--property schema.registry.url=http://localhost:8081      

# enter 이후 메시지 전송
{ "customer_id" : 1, "customer_name" : "min"}
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>avro 메시지 읽기

kafka-avro-console-consumer --bootstrap-server localhost:9092 --topic avro_test 
--property schema.registry.url=http://localhost:8081 --from-beginning

# 출력
{ "customer_id" : 1, "customer_name" : "min"}
</code></pre></div></div>

<p><br /></p>
<hr />

<h3 id="schema-registry-기반의-connector-설정">Schema Registry 기반의 Connector 설정</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>source connector 설정

"key.converter" : "io.confluent.connect.avro.AvroConverter",
"value.converter" : "io.confluent.connect.avro.AvroConverter",
"key.converter.schema.registry.url" : "http://localhost:8081",
"value.converter.schema.registry.url" : "http://localhost:8081",
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sink connector 설정

"key.converter" : "io.confluent.connect.avro.AvroConverter",
"value.converter" : "io.confluent.connect.avro.AvroConverter",
"key.converter.schema.registry.url" : "http://localhost:8081",
"value.converter.schema.registry.url" : "http://localhost:8081",
</code></pre></div></div>

<ul>
  <li>source connector에서 database.server.name으로 지정한 이름으로 topic이 만들어지고 여기에는 source DB의 DDL 정보가 저장됨
    <ul>
      <li>sink connector의 소비 대상 아님</li>
    </ul>
  </li>
</ul>

<h4 id="schema-registry-등록-주요-정보">Schema Registry 등록 주요 정보</h4>
<ul>
  <li><code class="language-plaintext highlighter-rouge">subjects</code>
    <ul>
      <li>schema registry에 등록된 토픽 별 (또는 토픽 레코드 유형 별) 기준 정보
        <ul>
          <li>schema 정보가 subject 안에 있음</li>
        </ul>
      </li>
      <li>subject 내의 스키마 호환성 체크</li>
      <li>version은 subject 내에서 변경</li>
      <li>schema가 진화할 때 동일한 subject에서 새로운 schema id와 version을 가짐</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">schemas</code> : schema 정보</li>
  <li><code class="language-plaintext highlighter-rouge">config</code> : 전역 또는 subject 별 스키마 호환성 관련 정보 저장</li>
</ul>

<p><br /></p>
<hr />

<h3 id="rest-api를-통한-schema-registry-관리">REST API를 통한 Schema Registry 관리</h3>
<ul>
  <li>subject 및 config의 정보 추출 및 주요 속성에 대한 수정 / 변경 / 삭제 가능</li>
  <li>가급적 delete는 자제. delete 수행 시 soft delete 이후 hard delete를 수행해야 완전 삭제</li>
  <li>config는 subject 별로 별도 설정하지 않으면 전역 config를 그대로 따름</li>
</ul>

<h4 id="내부-토픽-_schemas-조회">내부 토픽 _schemas 조회</h4>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kafkacat -b localhost:9092 -C -t _schemas -u -q | jq '.'
</code></pre></div></div>
<ul>
  <li>토픽 별 key, value에 대한 스키마 정보가 저장됨</li>
</ul>

<h4 id="schema-registry에-등록된-모든-schema-정보-확인">schema registry에 등록된 모든 schema 정보 확인</h4>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http GET http://localhost:8081/schemas
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">_schemas</code>에 저장된 데이터 기반으로 가져옴</li>
</ul>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
  </span><span class="nl">"schema"</span><span class="w"> </span><span class="p">:</span><span class="w"> 
  </span><span class="p">{</span><span class="w">
        </span><span class="nl">"type"</span><span class="p">:</span><span class="s2">"record"</span><span class="p">,</span><span class="w"> 
        </span><span class="nl">"name"</span><span class="p">:</span><span class="s2">"Key"</span><span class="p">,</span><span class="w"> 
        </span><span class="nl">"namespace"</span><span class="p">:</span><span class="s2">"mysqlavro.oc.products"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"fields"</span><span class="p">:[{</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"product_id"</span><span class="p">,</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="s2">"int"</span><span class="p">}],</span><span class="w">
        </span><span class="nl">"connect.name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"mysqlavro.oc.products-key"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="w">
</span><span class="p">}</span><span class="err">,</span><span class="w">

</span><span class="p">{</span><span class="w">
  </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w">
  </span><span class="nl">"schema"</span><span class="w"> </span><span class="p">:</span><span class="w">
  </span><span class="p">{</span><span class="w">
    </span><span class="nl">"type"</span><span class="p">:</span><span class="s2">"record"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"name"</span><span class="p">:</span><span class="s2">"Value"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"namespace"</span><span class="p">:</span><span class="s2">"mysqlavro.oc.products"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"fields"</span><span class="p">:[{</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"product_id"</span><span class="p">,</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="s2">"int"</span><span class="p">},</span><span class="w">
              </span><span class="p">{</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"product_name"</span><span class="p">,</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:[</span><span class="s2">"null"</span><span class="p">,</span><span class="s2">"string"</span><span class="p">],</span><span class="w"> </span><span class="nl">"default"</span><span class="p">:</span><span class="kc">null</span><span class="p">},</span><span class="w">
              </span><span class="p">{</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"product_category"</span><span class="p">,</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:[</span><span class="s2">"null"</span><span class="p">,</span><span class="w"> </span><span class="s2">"string"</span><span class="p">],</span><span class="w"> </span><span class="nl">"default"</span><span class="p">:</span><span class="kc">null</span><span class="p">}],</span><span class="w">
    </span><span class="nl">"connect.name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"mysqlavro.oc.products-value"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<ul>
  <li>만약 특정 토픽에서 스키마 정보가 변경된 경우 새로운 스키마 id 할당</li>
  <li>해당 토픽 subject 명은 유지 되고, 그 안에서 버전이 바뀜</li>
</ul>

<h4 id="schema-registry에-등록된-모든-subjects-정보-확인">schema registry에 등록된 모든 subjects 정보 확인</h4>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http GET http://localhost:8081/subjects
</code></pre></div></div>
<ul>
  <li>subject 명은 topic 명 + ‘-key’ 또는 ‘-value’로 명명됨</li>
</ul>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="s2">"mysqlavro-key"</span><span class="p">,</span><span class="w">
  </span><span class="s2">"mysqlavro-value"</span><span class="p">,</span><span class="w">
  </span><span class="s2">"mysqlavro.oc.customers-key"</span><span class="p">,</span><span class="w">
  </span><span class="s2">"mysqlavro.oc.customers-value"</span><span class="w">
          </span><span class="err">...</span><span class="w">
</span><span class="p">}</span><span class="w">

</span></code></pre></div></div>

<h4 id="특정-id를-가지는-schema-정보-확인">특정 id를 가지는 schema 정보 확인</h4>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http GET http://localhost:8081/schemas/ids/8
</code></pre></div></div>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"schema"</span><span class="w"> </span><span class="p">:</span><span class="w">
  </span><span class="p">{</span><span class="w">
    </span><span class="nl">"type"</span><span class="p">:</span><span class="s2">"record"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"name"</span><span class="p">:</span><span class="s2">"Value"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"namespace"</span><span class="p">:</span><span class="s2">"mysqlavro.oc.products"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"fields"</span><span class="p">:[{</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"product_id"</span><span class="p">,</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="s2">"int"</span><span class="p">},</span><span class="w">
      </span><span class="p">{</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"product_name"</span><span class="p">,</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:[</span><span class="s2">"null"</span><span class="p">,</span><span class="s2">"string"</span><span class="p">],</span><span class="w"> </span><span class="nl">"default"</span><span class="p">:</span><span class="kc">null</span><span class="p">},</span><span class="w">
      </span><span class="p">{</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"product_category"</span><span class="p">,</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:[</span><span class="s2">"null"</span><span class="p">,</span><span class="w"> </span><span class="s2">"string"</span><span class="p">],</span><span class="w"> </span><span class="nl">"default"</span><span class="p">:</span><span class="kc">null</span><span class="p">}],</span><span class="w">
    </span><span class="nl">"connect.name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"mysqlavro.oc.products-value"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h4 id="특정-subject의-버전-별-schema-정보-확인">특정 subject의 버전 별 schema 정보 확인</h4>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http GET http://localhost:8081/subjects/mysqlavro.oc.customers-value/versions/1
</code></pre></div></div>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">4</span><span class="p">,</span><span class="w">
  </span><span class="nl">"schema"</span><span class="w"> </span><span class="p">:</span><span class="w">
  </span><span class="p">{</span><span class="w">
    </span><span class="nl">"type"</span><span class="p">:</span><span class="s2">"record"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"name"</span><span class="p">:</span><span class="s2">"Value"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"namespace"</span><span class="p">:</span><span class="s2">"mysqlavro.oc.customers"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"fields"</span><span class="p">:[{</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"customers_id"</span><span class="p">,</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="s2">"int"</span><span class="p">},</span><span class="w">
      </span><span class="p">{</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"name"</span><span class="p">,</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="s2">"string"</span><span class="p">},</span><span class="w">
      </span><span class="p">{</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"email"</span><span class="p">,</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"string"</span><span class="p">}],</span><span class="w">
    </span><span class="nl">"connect.name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"mysqlavro.oc.customers-value"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h4 id="전역으로-등록된-config-정보-확인">전역으로 등록된 config 정보 확인</h4>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http GET http://localhost:8081/config
</code></pre></div></div>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"compatibilityLevel"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"BACKWARD"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h4 id="개별-subject-별-config-정보-확인">개별 subject 별 config 정보 확인</h4>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http GET http://localhost:8081/config/mysqlavro.ac.customers-value
</code></pre></div></div>

<p><br /></p>
<hr />

<h3 id="schema-개요와-필요성">Schema 개요와 필요성</h3>
<ul>
  <li>schema란 데이터의 구조와 제약 조건에 대한 규칙 정보를 기술한 메타 정보</li>
  <li>정해진 규칙대로 데이터를 입력 / 수정하지 않으면 잘못된 로직으로 데이터를 사용할 수 있음</li>
</ul>

<h4 id="오픈소스-분산-db-아키텍처의-특징">오픈소스 분산 DB 아키텍처의 특징</h4>
<ul>
  <li>데이터 처리 성능 극대화를 위해 자유로운 확장이 가능한 분산 DB</li>
  <li>초기 오픈 소스 분산 DB의 경우 schema가 없는 데이터 저장 구조 채택</li>
  <li>현재도 NoSQL의 경우는 schema가 없는 데이터 구조로 발전</li>
</ul>

<h4 id="schemaless-이슈">Schemaless 이슈</h4>
<ul>
  <li>너무 자유로운 스키마 허용과 묵시적인 schema 추론은 복잡한 비즈니스 처리나 여러 사용자 / 팀이 개입된 데이터 처리 시 문제 발생시키기 쉬움</li>
  <li>sink에서 스키마 정보 없이 데이터 포맷 처리 어려움</li>
</ul>

<h4 id="nosql에서-스키마-변경에-따른-데이터-저장">NoSQL에서 스키마 변경에 따른 데이터 저장</h4>

<p><img src="https://imucoding.github.io/assets/images/nosql.png" alt="img.png" /></p>
<ul>
  <li>schemaless : 비슷한 유형의 데이터 그룹 들을 묶는 동적 스키마로 발전 필요</li>
</ul>

<p><br /></p>
<hr />

<h3 id="avro-읽기-스키마와-쓰기-스키마">Avro 읽기 스키마와 쓰기 스키마</h3>
<ul>
  <li>쓰기 스키마 : 애플리케이션이 데이터 전송 시 데이터를 부호화하기 위해 사용하는 스키마</li>
  <li>읽기 스키마 : 애플리케이션이 데이터를 복화하하여 읽어들일 시 사용하는 스키마</li>
</ul>

<p><img src="https://imucoding.github.io/assets/images/readwriteschema.png" alt="img.png" /></p>
<ul>
  <li>별도의 애플리케이션에서 쓰기 스키마와 읽기 스키마를 따로 가져갈 필요 있는가?</li>
  <li>쓰기 스키마와 읽기 스키마가 다룰 수 있는가?</li>
</ul>

<p><img src="https://imucoding.github.io/assets/images/schemachange.png" alt="img.png" /></p>
<ul>
  <li>v1에서 v2로 스키마 변경된다면 수신 App은 당연히 동일 스키마로 변경되어야 하는가?</li>
  <li>기존 수신 App은 새로운 스키마 변경을 어떻게 반영해서 프로그램 변경?</li>
  <li>변경될 때마다 수신 App 적용하면 스키마 일관성은 어떻게 유지?</li>
</ul>

<h4 id="호환성-없는-스키마-발전에-따른-메시지-변화">호환성 없는 스키마 발전에 따른 메시지 변화</h4>

<p><img src="https://imucoding.github.io/assets/images/messagechange.png" alt="img.png" /></p>
<ul>
  <li>수신 스키마가 변경되는 경우 호환이 가능하다면 읽으면서 차이 해소</li>
</ul>

<h4 id="avro의-읽기-스키마와-쓰기-스키마-호환성">Avro의 읽기 스키마와 쓰기 스키마 호환성</h4>
<ul>
  <li>avro는 읽기 스키마와 쓰기 스키마가 동일하지 않아도 되며 호환 가능하기만 하면됨</li>
  <li>데이터를 복호화 (읽기) 할 때 avro는 쓰기 스키마와 읽기 스키마가 호환 가능하다면 읽어들이면서 그 차이를 해소</li>
  <li>속성의 순서가 달라도 상관 없음</li>
  <li>쓰기 스키마에는 있지만 읽기 스키마에는 없는 속성은 읽어 들일 때 속성 무시</li>
  <li>쓰기 스키마에는 없지만 읽기 스키마에는 있는 속성은 읽어 들일 때 읽기 스키마에 선언된 기본 값으로 읽어들임</li>
</ul>

<p><br /></p>
<hr />

<h3 id="스키마-호환성과-전체-메시지-일관성">스키마 호환성과 전체 메시지 일관성</h3>
<ul>
  <li>스키마 호환성 지원을 통해 유연하게 상황 대처 가능 vs 스키마 호환성을 통해 전체 메시지는 데이터 일관성이 깨질 수 있음</li>
</ul>

<h4 id="하위-호환성-backward">하위 호환성 (BACKWARD)</h4>
<ul>
  <li>새로운 버전의 읽기 스키마는 예전 버전의 쓰기 스키마를 처리할 수 있음</li>
</ul>

<p>case 1) 예전 버전에서 속성이 추가된 새로운 버전의 읽기 스키마는 기본 값으로 해당 속성을 읽음 <br />
따라서 읽기 스키마에 새로운 버전으로 속성이 추가될 경우 반드시 기본 값 설정이 필요</p>

<p><img src="https://imucoding.github.io/assets/images/backward1.png" alt="img.png" /></p>

<p>case 2) 예전 버전에서 속성이 삭제된 새로운 버전의 읽기 스키마는 해당 속성을 무시 <br /></p>

<p><img src="https://imucoding.github.io/assets/images/backward2.png" alt="img.png" /></p>

<h4 id="상위-호환성-forward">상위 호환성 (FORWARD)</h4>
<ul>
  <li>예전 버전의 읽기 스키마는 새로운 버전의 쓰기 스키마를 처리할 수 있음</li>
</ul>

<p>case 1) 새로운 버전 쓰기 스키마에 신규 속성이 추가되는 경우 예전 버전의 읽기 스키마는 해당 속성을 무시 <br /></p>

<p><img src="https://imucoding.github.io/assets/images/forward1.png" alt="img.png" /></p>

<p>case 2) 새로운 버전 쓰기 스키마에 기존 속성이 삭제되는 경우 예전 버전의 읽기 스키마는 해당 속성의 기본 값으로 읽어들임 <br />
따라서 삭제되는 기존 속성은 반드시 예전 버전에서 기본 값을 가지고 있어야 함</p>

<p><img src="https://imucoding.github.io/assets/images/forward2.png" alt="img.png" /></p>

<h4 id="avro-스키마-호환성-체크">Avro 스키마 호환성 체크</h4>
<ul>
  <li>새로 추가되는 컬럼이 기본 값을 가지고 있지 않거나 (하위 호환성 오류) 삭제되는 컬럼이 기본 값을 가지고 있지 않은 경우 (상위 호환성 오류) 호환성 오류 발생</li>
  <li>기존 컬럼 타입 변경, 기존 컬럼 명 변경은 스키마 호환성 체크가 제대로 되지 않음</li>
</ul>

<p><br /></p>
<hr />

<h3 id="subject-개요">Subject 개요</h3>
<ul>
  <li>schema registry에 등록된 토픽 별 기준 정보 (schema namespace)</li>
  <li>subject 내의 스키마 호환성 체크</li>
  <li>version은 subject 내에서 변경</li>
  <li>schema가 진화할 때 동일한 subject에서 새로운 schema id와 version을 가짐</li>
</ul>

<h4 id="subject의-naming-유형">Subject의 Naming 유형</h4>
<ul>
  <li><code class="language-plaintext highlighter-rouge">TopicNameStrategy</code>
    <ul>
      <li>subject의 이름을 정하는 기본 설정</li>
      <li>개별 토픽 별 토픽 key / value에 따라 스키마를 등록
        <ul>
          <li>ex) oc.customer의 토픽 명인 경우 oc.customer-key, oc.customer-value</li>
        </ul>
      </li>
      <li>토픽 내의 메시지 레코드가 동일한 스키마를 따르도록 설정</li>
      <li>스키마 호환성이 설정된 경우 해당 호환성에 적합하지 않은 스키마를 가지는 메시지는 토픽에 저장되지 않음</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">RecordNameStrategy</code>
    <ul>
      <li>특정 레코드 타입별로 스키마를 따르도록 설정</li>
      <li>하나의 토픽 내 여러 타입의 이벤트가 있을 경우 이벤트 유형별로 스키마를 지정할 때 사용
        <ul>
          <li>event라는 토픽에 click, page_view 두 가지 이벤트 타입에 대한 레코드가 있을 수 있음</li>
        </ul>
      </li>
      <li>kafka 전체에서 특정 레코드 타입별로 스키마를 설정하므로 서로 다른 토픽에서 동일한 레코드 타입들이 있을 경우에도 동일 스키마가 적용됨</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">TopicRecordNameStrategy</code>
    <ul>
      <li>RecordNameStrategy와 유사하지만 토픽 내의 특정 레코드 타입 별로 스키마를 따르도록 설정</li>
      <li>개별 토픽의 특정 레코드 타입별로 스키마를 설정하므로 서로 다른 토픽에서 동일한 레코드 타입들이 있을 경우에는 서로 다른 스키마가 적용됨</li>
    </ul>
  </li>
</ul>

<h4 id="subject-스키마-호환성">Subject 스키마 호환성</h4>
<ul>
  <li>subject 내의 스키마 호환성 (compatibility) 체크</li>
</ul>

<p><img src="https://imucoding.github.io/assets/images/subjectevolve.png" alt="img.png" /></p>
<ul>
  <li>id는 key, value 따로 관리하지 않음, 스키마 저장 / 변경 시 새로 할당</li>
  <li>value의 schema 변경 되어 새로운 id가 할당되고 version이 업데이트 됨</li>
  <li>스키마 호환성은 schema id 레벨로 체크되지 않고 subject 레벨로 체크됨</li>
</ul>

<p><br /></p>
<hr />

<h3 id="스키마-발전과-호환성">스키마 발전과 호환성</h3>
<ul>
  <li>업무가 계속 변함에 따라 스키마도 변경</li>
  <li>schema registry는 subject 단위로 변경된 스키마의 버전과 호환성을 관리</li>
  <li>schema registry에 subject 별로 호환성이 설정되어 있다면 해당 호환성에 맞지 않는 스키마 변경은 허용되지 않음</li>
</ul>

<h4 id="schema-registry-기반의-producer와-consumer">Schema Registry 기반의 Producer와 Consumer</h4>
<p><img src="https://imucoding.github.io/assets/images/schemaproducerconsumer.png" alt="img.png" /></p>
<ul>
  <li>schema registry를 이용한 producer와 consumer 애플리케이션은 별도의 스키마를 개별적으로 가지고 있음</li>
  <li>스키마가 변경됨에 따라 producer 또는 consumer가 가지고 있는 스키마를 개별적으로 update 할 수 있음</li>
  <li>schema registry에 스키마에 대해 호환성이 설정되면 해당 호환성에 부합하는 스키마 변경이 되었을 때만 producer와 consumer 애플리케이션 스키마 update 가능</li>
</ul>]]></content><author><name>Hyunjune</name></author><category term="kafka" /><category term="schema registry" /><category term="backward" /><category term="forward" /><summary type="html"><![CDATA[Schema Registry confluent kafka는 schema registry를 통해 schema 정보를 별도로 관리하는 기능 제공 토픽으로 전송되는 data의 schema는 schema registry에서 ID + version 별로 중앙 관리 되므로 레코드 별로 schema를 중복해서 전송할 필요가 없음]]></summary></entry><entry><title type="html">Debezium CDC Connector</title><link href="https://imucoding.github.io/kafka/2024/09/07/debezium.html" rel="alternate" type="text/html" title="Debezium CDC Connector" /><published>2024-09-07T00:00:00+00:00</published><updated>2024-09-07T00:00:00+00:00</updated><id>https://imucoding.github.io/kafka/2024/09/07/debezium</id><content type="html" xml:base="https://imucoding.github.io/kafka/2024/09/07/debezium.html"><![CDATA[<h3 id="cdc-change-data-capture-개요">CDC (Change Data Capture) 개요</h3>
<ul>
  <li>CDC는 RDBMS오 NoSql 등의 data store 시스템의 변경 데이터를 기록하는 내부 트랜잭션 로그 파일로부터 변경 데이터를 capture하는 설계, sw를 지칭</li>
  <li>DB 내부 트랜잭션 로그 파일로부터 변경 데이터를 추출하므로 소스 DB 성능에 큰 영향 없이 대용량 변경 데이터를 매우 빠르게 추출하고 준실시간으로 타겟 DB 연동 가능</li>
  <li>DBMS 복제 수준의 안정적이고 정확한 데이터 추출 가능</li>
  <li>Debezium은 이러한 CDC 기반의 분산 플랫폼으로 기본적으로는 카프카 커넥터로 제공됨</li>
  <li>JDBC source connector의 경우 DB에 select 쿼리를 주기적으로 전송하고 변경 사항 추적은 특정 컬럼 (key, timestamp) 값에 대해 기존에 없던 새로운 값이 추가된 경우만 capture 가능</li>
</ul>

<p><br /></p>
<hr />

<h3 id="db-복제-기반-cdc-솔루션">DB 복제 기반 CDC 솔루션</h3>
<ul>
  <li>대부분의 CDC 솔루션은 DB 내의 트랜잭션 log를 이용하는 DB replication에 기반함</li>
</ul>

<p><img src="https://imucoding.github.io/assets/images/cdc.png" alt="img.png" /></p>

<h4 id="dbms-별-트랜잭션-로그-파일">DBMS 별 트랜잭션 로그 파일</h4>
<ul>
  <li>Oracle : Redo log</li>
  <li>Mysql : Bin log</li>
  <li>PostgreSQL : WAL (Write-Ahead Log)</li>
</ul>

<p><br /></p>
<hr />

<h3 id="mysql-복제-개요">Mysql 복제 개요</h3>
<ul>
  <li>bin log 기반 구현</li>
  <li>master bin log를 slave로 전송하고, 전송된 bin log를 SQL로 적용</li>
  <li>복제 cluster 내의 master와 slave DB는 고유의 server_id 설정 값을 가짐</li>
</ul>

<p><img src="https://imucoding.github.io/assets/images/binlog.png" alt="img.png" /></p>

<h4 id="binary-log-데이터-포맷">Binary Log 데이터 포맷</h4>
<ul>
  <li>statement 방식 : 실행된 SQL을 기록. row 방식보다 처리속도가 느리고 덜 안정적</li>
  <li>row 방식 : 변경된 데이터를 기록. InnoDB binary log의 기본 데이터 포맷. 많은 데이터 변경 시 용량이 커질 수 있음</li>
</ul>

<h4 id="row-포맷-유형">Row 포맷 유형</h4>
<ul>
  <li><code class="language-plaintext highlighter-rouge">mysqld.cnf</code>에서 <code class="language-plaintext highlighter-rouge">binlog_row_image</code> 설정</li>
  <li>full : 변경이 발생한 레코드의 모든 컬럼의 변경 전 / 후 값을 bin log에 기록
    <ul>
      <li>insert는 변경 후만 기록</li>
      <li>update는 변경 전 / 후 기록</li>
      <li>delete는 변경 전만 기록</li>
    </ul>
  </li>
  <li>minimal : 변경이 적용되어 필요한 컬럼만 기록
    <ul>
      <li>insert는 SQL에 명시된 컬럼 값만 기록</li>
      <li>update는 변경 전 pk와 SQL에 명시된 컬럼 값만 기록</li>
      <li>delete는 변경 전 pk만 기록</li>
    </ul>
  </li>
  <li>noblob : full과 동일하지만 blob, text 컬럼의 변경 값은 기록하지 않음</li>
</ul>

<h4 id="mysql-복제-환경-확인">Mysql 복제 환경 확인</h4>
<p><img src="https://imucoding.github.io/assets/images/showmaster.png" alt="img.png" /></p>
<ul>
  <li>position 정보가 있는지 확인
    <ul>
      <li>binlog는 파일 위치 기반이 default</li>
    </ul>
  </li>
  <li>mysql binlog 복제 방법
    <ul>
      <li>파일 위치 기반 복제 : DB 변경 사항을 binlog 파일 명과 파일 내 offset 조합 식별</li>
      <li>GTID 기반 복제 : 복제 cluster 구성 여러 DB 간에 소스 DB 변경 사항 이벤트를 동일 고유 식별자로 식별</li>
    </ul>
  </li>
</ul>

<p><img src="https://imucoding.github.io/assets/images/showbin.png" alt="img.png" /></p>
<ul>
  <li>log_bin 값이 <code class="language-plaintext highlighter-rouge">ON</code>으로 되어있으면 됨</li>
  <li>설정이 되어 있지 않은 경우
    <ul>
      <li><code class="language-plaintext highlighter-rouge">/etc/mysql/mysql.conf.d/mysql.conf</code> 에 설정 추가 후 재기동
        <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>server_id = 223344
log_bin = binlog
binlog_format = Row
binlog_row_image = Full
expire_logs_days = 0    
</code></pre></div>        </div>
      </li>
    </ul>
  </li>
</ul>

<p><br /></p>
<hr />

<h3 id="debezium-cdc-source-connector-이벤트-메시지">Debezium CDC Source Connector 이벤트 메시지</h3>
<p><img src="https://imucoding.github.io/assets/images/transaction.png" alt="img.png" /></p>

<p><br /></p>
<hr />

<h3 id="kafka-연계-시-유의-사항">Kafka 연계 시 유의 사항</h3>
<ul>
  <li>debezium은 source connector만 지원하므로 sink는 JDBC sink connector로 수행
    <ul>
      <li>소스 DB의 DDL 변경을 타겟 DB에 정확히 반영 어려움</li>
      <li>타겟 DB 데이터 입력이 JDBC sink connector의 기능에 제약</li>
    </ul>
  </li>
  <li>소스 데이터 truncate table, drop table 타겟 적용되지 않음
    <ul>
      <li>v2.1에서 truncate 반영 예정</li>
    </ul>
  </li>
  <li>debezium은 kafka로 메시지 전송까지만 담당하므로 기존의 사용 CDC 솔루션과 차이 존재
    <ul>
      <li>상용 솔루션은 타겟 DB에 복제까지 담당</li>
    </ul>
  </li>
</ul>

<p><br /></p>
<hr />

<h3 id="connector-parameter-특징">Connector Parameter 특징</h3>
<ul>
  <li>하나의 source connector로 여러 개의 source 테이블 데이터를 유연하게 추출 가능하며 하나의 source 테이블은 하나의 topic으로 생성</li>
  <li>source 테이블의 pk는 자동으로 kafka 토픽의 key 값으로 생성됨</li>
  <li>토픽 명은 기본으로 <code class="language-plaintext highlighter-rouge">{database server name}.{database 명}.{table 명}</code>으로 생성되고, SMT를 통해 변경 가능</li>
  <li>source table DDL 변경을 kafka 토픽으로 저장 가능. topic 메시지는 JDBC source connector와 다르게 구성되어 있으며 (befor / after 구성) JDBC sink connector에서 데이터를 입력하기 위해서는 SMT를 이용하여 메시지 재변경 필요</li>
  <li>delete를 위한 tombstone 메시지 적용을 위해 SMT 적용</li>
  <li>data, datetime, timezone의 일자 / 시간 관련 타입, numeric, decimal 등의 precision / scale 관련 타입을 JDBC sink connector와 호환될 수 있는 타입으로 만들어져야 함</li>
</ul>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"name"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"mysql_cdc_oc_source"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"config"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"connector.class"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"io.debezium.connector.mysql.MysqlConnector"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"tasks.max"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"1"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"database.hostname"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"192.168.56.101"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"database.port"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"3306"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"database.password"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"connect_dev"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"database.allowPublickeyRetrieval"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"true"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"database.server.name"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"mysql01"</span><span class="p">,</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">connector</span><span class="w"> </span><span class="err">명칭</span><span class="w"> </span><span class="err">해당</span><span class="w"> </span><span class="err">값</span><span class="w"> </span><span class="err">접두어로</span><span class="w"> </span><span class="err">토픽</span><span class="w"> </span><span class="err">명</span><span class="w"> </span><span class="err">생성</span><span class="w">
    </span><span class="nl">"database.include.list"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"oc"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"table.include.list"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"oc.customers.oc.orders"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"poll.interval.ms"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"1000"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"include.schema.changes"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"true"</span><span class="p">,</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">source</span><span class="w"> </span><span class="err">table</span><span class="w"> </span><span class="err">DDL</span><span class="w"> </span><span class="err">변경</span><span class="w"> </span><span class="err">반영.</span><span class="w"> </span><span class="err">기본</span><span class="w"> </span><span class="kc">true</span><span class="w">
    </span><span class="nl">"database.history.kafka.bootstrap.servers"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"192.168.56.101:9092"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"database.history.kafka.topic"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"schema_changes.mysql.oc"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"key.converter"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"org.apache.kafka.connect.json.JsonConverter"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"value.converter"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"org.apache.kafka.connect.json.JsonConverter"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"tombstones.on.delete"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"true"</span><span class="p">,</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">delete</span><span class="w"> </span><span class="err">발생</span><span class="w"> </span><span class="err">시</span><span class="w"> </span><span class="err">tombstone</span><span class="w"> </span><span class="err">발생</span><span class="w"> </span><span class="err">여부.</span><span class="w"> </span><span class="err">기본</span><span class="w"> </span><span class="kc">true</span><span class="w">
    </span><span class="nl">"transforms"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"unwrap"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"transforms.unwrap.type"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"io.debezium.transforms.ExtractNewRecordState"</span><span class="p">,</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">after</span><span class="w"> </span><span class="err">메시지만</span><span class="w"> </span><span class="err">생성하기</span><span class="w"> </span><span class="err">위한</span><span class="w"> </span><span class="err">SMT</span><span class="w">
    </span><span class="nl">"transforms.unwrap.drop.tombstones"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"false"</span><span class="p">,</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">after</span><span class="w"> </span><span class="err">메시지</span><span class="w"> </span><span class="err">생성</span><span class="w"> </span><span class="err">시</span><span class="w"> </span><span class="err">tombstone</span><span class="w"> </span><span class="err">메시지의</span><span class="w"> </span><span class="err">경우</span><span class="w"> </span><span class="err">삭제</span><span class="w"> </span><span class="err">여부</span><span class="w">
    </span><span class="nl">"time.precision.mode"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"connect"</span><span class="p">,</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">기본은</span><span class="w"> </span><span class="err">adaptive_time_microseconds.</span><span class="w"> </span><span class="err">time</span><span class="w"> </span><span class="err">관련</span><span class="w"> </span><span class="err">단위</span><span class="w"> </span><span class="err">기본</span><span class="w"> </span><span class="err">레벨로</span><span class="w"> </span><span class="err">변경</span><span class="w"> </span><span class="err">(microseconds</span><span class="w"> </span><span class="err">-&gt;</span><span class="w"> </span><span class="err">milliseconds)</span><span class="w">
    </span><span class="nl">"database.connectionTimeZone"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"Asia/Seoul"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<ul>
  <li>tomestone 데이터 발생 위해 sink connector에서도 <code class="language-plaintext highlighter-rouge">delete.enabled</code>도 true 이어야 함</li>
</ul>

<p><br /></p>
<hr />

<h3 id="debezium-source-connector-생성-메시지-포맷">Debezium Source Connector 생성 메시지 포맷</h3>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"schema"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="err">...</span><span class="p">},</span><span class="w">  </span><span class="err">//</span><span class="w"> </span><span class="err">key</span><span class="w"> </span><span class="err">필드의</span><span class="w"> </span><span class="err">schema</span><span class="w"> </span><span class="err">정보</span><span class="w">
  </span><span class="nl">"payload"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="err">...</span><span class="p">},</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">key</span><span class="w"> </span><span class="err">값</span><span class="w">
  </span><span class="nl">"schema"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="err">...</span><span class="p">},</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">value</span><span class="w"> </span><span class="err">필드의</span><span class="w"> </span><span class="err">schema</span><span class="w"> </span><span class="err">정보</span><span class="w"> 
  </span><span class="nl">"payload"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="err">...</span><span class="p">}</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">value</span><span class="w"> </span><span class="err">값</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<ul>
  <li>value 필드에서는 before과 after에 대한 schema 정보 및 값을 모두 가짐</li>
</ul>

<p><br /></p>
<hr />

<h3 id="topic-명의-dot을-dash-로-변경">Topic 명의 dot(.)을 dash(-)로 변경</h3>
<ul>
  <li>토픽 명을 직접 지정해주지 않은 경우, debezium은 topic 명을 기본적으로 <code class="language-plaintext highlighter-rouge">database.server.name + "." + database.include.list + "." + table.include.list</code>를 조합해서 만듦
    <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">"transforms"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"rename_topic"</span><span class="err">,</span><span class="w">
</span><span class="nl">"transforms.rename_topic.type"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"org.apache.kafka.connect.transforms.RegexRouter"</span><span class="err">,</span><span class="w">
</span><span class="nl">"transforms.rename_topic.regex"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"(.*)</span><span class="se">\\</span><span class="s2">.(.*)</span><span class="se">\\</span><span class="s2">.(.*)"</span><span class="w">
</span><span class="nl">"transforms.rename_topic.replacement"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"$1-$2-$3"</span><span class="w">
</span></code></pre></div>    </div>
  </li>
</ul>

<h3 id="동일-source-table에-여러-개의-connector-사용-시-문제점">동일 Source Table에 여러 개의 Connector 사용 시 문제점</h3>
<ul>
  <li>여러 개 connector를 동일 테이블에 적용은 가능하지만 이들 중 단 하나의 connector에만 topic 메시지가 생성됨</li>
</ul>

<p><br /></p>
<hr />

<h3 id="snapshot-모드">Snapshot 모드</h3>
<ul>
  <li>스냅샷은 connector 생성 시점의 binlog와 binlog position, DB와 읽어들일 테이블의 schema (DDL), 대상 테이블의 초기 레코드를 읽어서 카프카에 저장 등의 작업 수행</li>
  <li><code class="language-plaintext highlighter-rouge">connect-offsets</code>에 해당 connector 명과 서버 명으로 offset 정보가 없을 때 수행</li>
  <li><code class="language-plaintext highlighter-rouge">snapshot.mode = initial</code>로 default 설정 시 connector를 생성하기 이전에 소스 테이블에 생성되어 있는 레코드를 모두 카프카로 보내어서 동기화를 시킴
    <ul>
      <li>오래된 테이블의 경우 초기 binlog 가 없어서 오류 발생할 수 있음</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">snapshot.mode = schema_only</code>로 설정 시 connector 등록 후 변경 데이터만 메시지로 생성
    <ul>
      <li>초기 데이터는 sink 테이블로 수동 마이그레이션 필요</li>
    </ul>
  </li>
</ul>

<h4 id="connector-최초-생성-시-수행-프로세스">Connector 최초 생성 시 수행 프로세스</h4>
<ol>
  <li>MySql DB에 Global read lock(DB 내 모든 테이블 lock)을 획득. 다른 client 들이 테이블에 write할 수 없음 <br />
    <ul>
      <li>connector가 Global read lock 획득 권한이 없으면 Table Lock (개별 테이블 lock)을 획득함. 이 경우 테이블에 snapshot이 완료될 때까지 write 불가능 <br /></li>
    </ul>
  </li>
  <li>현재 binlog의 position을 읽음 <br /></li>
  <li>connector가 읽어들일 테이블의 스키마 정보 가져옴 <br /></li>
  <li>Global read lock 해제 (다른 클라이언트가 테이블 write 가능) <br />
    <ul>
      <li>DDL 변경 사항을 schema change topic에 기록 <br /></li>
    </ul>
  </li>
  <li>connector가 대상 테이블 들의 기존 레코드를 topic에 전송 <br /></li>
  <li>topic에 기록된 레코드 offset을 <code class="language-plaintext highlighter-rouge">connect-offset</code> 토픽에 기록 <br /></li>
</ol>

<h4 id="오래된-binlog-purge-시-connector-기동-이슈">오래된 binlog purge 시 connector 기동 이슈</h4>
<ul>
  <li>connector는 자신이 읽어서 kafka로 보낸 offset 정보를 binlog 명과 binlog position으로 connect-offsets 토픽에 기록</li>
  <li>오랫 동안 connector를 기동하지 않는다면, binlog가 mysql expire log 기간 이상 저장될 경우 삭제될 수 있음</li>
  <li>connector는 connect-offsets에 기록된 binlog를 binlog 디렉토리에서 찾지 못해서 기동 불가</li>
  <li>새롭게 connector를 생성하거나 connect-offsets 재설정 필요</li>
</ul>

<p><br /></p>
<hr />

<h3 id="numeric과-decimal-data-type">Numeric과 Decimal Data Type</h3>
<ul>
  <li>precision(전체 자리 수)과 scale(소수점 이하 자리 수) 값을 가지는 숫자 data type</li>
  <li>Mysql은 Numeric과 Decimal이 동일한 타입임</li>
  <li>Decimal(7,3) 이면 9999.999 형태로 DB 저장</li>
  <li>Decimal(10,0) 이면 100.1을 입력해도 100으로 저장됨</li>
  <li>Decimal(10)은 precision 10, scale 0 임</li>
  <li>precision 또는 scale 설정하지 않으면 기본적으로 precision 10, scale 0</li>
</ul>

<h4 id="numeric과-decimal-데이터-타입-변환">Numeric과 Decimal 데이터 타입 변환</h4>
<ul>
  <li>debezium default 설정인 <code class="language-plaintext highlighter-rouge">decimal.handling.mode=precision</code> 일 때 numeric / decimal 모두 bytes 형태 변환</li>
  <li><code class="language-plaintext highlighter-rouge">org.apache.kafka.connect.data.Decimal</code>을 이용하여 변환되며 schema에 parameters로 scale과 precision 값 표현</li>
</ul>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">컬럼명</span><span class="w"> </span><span class="err">:</span><span class="w"> </span><span class="err">unit_price</span><span class="w">
</span><span class="err">타입</span><span class="w"> </span><span class="err">:</span><span class="w"> </span><span class="err">Decimal(</span><span class="mi">10</span><span class="err">,</span><span class="mi">2</span><span class="err">)</span><span class="w">
</span><span class="err">rkqt</span><span class="w"> </span><span class="err">:</span><span class="w"> </span><span class="mf">100.00</span><span class="w">


</span><span class="err">//</span><span class="w"> </span><span class="err">schema</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"type"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"bytes"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"name"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"org.apache.kafka.connect.data.Decimal"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"parameters"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"scale"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"2"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"connect.decimal.precision"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"10"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"field"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"unit_price"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><br /></p>
<hr />

<h3 id="mysql-temporal-type-변환-시-이슈">Mysql Temporal Type 변환 시 이슈</h3>
<ul>
  <li>date / time / datetime 타입 변환 시 debezium source connect의 변환 클래스 명을 JDBC sink connector가 인지하지 못하므로 오류 발생</li>
  <li>timestamp 타입은 문자열로 변환되며 소스 DB의 timezone에 관계 없이 UTC timezone으로 변경됨</li>
  <li>debezium source connector는 date / time / datetime 변환 모드를 설정할 수 있는 <code class="language-plaintext highlighter-rouge">time.precision.mode</code> 파라미터 제공</li>
  <li><code class="language-plaintext highlighter-rouge">time.precision.mode = connect</code>는 connect의 기본 변환 타입 (이것을 사용해야 함!)</li>
  <li><code class="language-plaintext highlighter-rouge">adaptive_time_microseconds</code> 설정 시 debezium 변환 클래스를 JDBC sink connector가 인지할 수 없음
    <ul>
      <li>adaptive_time_microseconds 설정 시에 microsecond 단위로 변환된 unix epoch time으로 millisecond 단위 변환을 처리하는 JDBC sink connector 오류 발생</li>
    </ul>
  </li>
</ul>

<h4 id="adaptive_time_microseconds">adaptive_time_microseconds</h4>
<p><img src="https://imucoding.github.io/assets/images/adaptivetime.png" alt="img.png" /></p>

<h4 id="connect">connect</h4>
<p><img src="https://imucoding.github.io/assets/images/connecttime.png" alt="img.png" /></p>

<p><br /></p>
<hr />

<h3 id="mysql-datetime과-timestamp">Mysql DATETIME과 TIMESTAMP</h3>
<ul>
  <li>timestamp는 datetime과 달리 timezone 정보를 가짐</li>
  <li>DB는 기본적으로 timezone 설정 필요</li>
  <li>datetime은 클라이언트에서 입력한 값 그대로 DB에 저장되지만 timestamp는 입력 값이 UTC로 변환 되어 DB 저장
    <ul>
      <li>ex) 한국에서 11:00를 입력 시 datetime은 그대로 11:00이 저장되지만 timestamp는 02:00가 저장됨 (저장 시점의 UTC)</li>
    </ul>
  </li>
  <li>DB 저장 값을 클라이언트에서 출력할 때는 datetime은 DB 저장된 값을 그대로 출력하지만, timestamp는 DB 값을 timezone 설정에 따라 변환한 뒤 출력</li>
</ul>

<h4 id="debezium에서-timestamp-타입-변환">Debezium에서 timestamp 타입 변환</h4>
<ul>
  <li>debezium은 datetime의 경우 int64 타입으로 변환하지만 timestamp는 문자열로 변환</li>
  <li>문자열로 적용 시 UTC timezone을 적용하여 변환됨</li>
  <li>debezium connector 설정 중 <code class="language-plaintext highlighter-rouge">database.connectionTimezone = Asia/Seoul</code>은 UTC를 KST로 변환하지 못함
    <ul>
      <li>한국 시간 2022년 11월 22일 13시 30분 37초 timestamp를 debezium에서 ‘2022-11-22T04:30:47Z’와 같은 문자열로 변환함</li>
    </ul>
  </li>
  <li>해당 문자열을 JDBC sink connector에서 인식하기 위해서는 timestamp 형 변환이 필요하므로 SMT 적용 필요</li>
</ul>

<h4 id="문자열-timestamp-처리를-위한-smt">문자열 timestamp 처리를 위한 SMT</h4>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CREATE TABLE orders_timestamp_tab(
  order_id int NOT NULL PRIMARY KEY,
  order_datetime datetime NOT NULL,
  order_timestamp timestamp NOT NULL,
  customer_id int NOT NULL,
  order_status varchar(10) NOT NULL,
  store_id int NOT NULL) ENGINE=InnoDB;

CREATE TABLE orders_timestamp_tab_sink(
  order_id int NOT NULL PRIMARY KEY,
  order_datetime datetime NOT NULL,
  order_timestamp timestamp NOT NULL,
  customer_id int NOT NULL,
  order_status varchar(10) NOT NULL,
  store_id int NOT NULL) ENGINE=InnoDB;
</code></pre></div></div>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">//</span><span class="w"> </span><span class="err">debezium</span><span class="w"> </span><span class="err">source</span><span class="w"> </span><span class="err">connector</span><span class="w">

</span><span class="p">{</span><span class="w">
  </span><span class="nl">"name"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"mysql_cdc_oc_source_timestamp_tab"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"config"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"connector.class"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"io.debezium.connector.mysql.MysqlConnector"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"task.max"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"1"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"database.hostname"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"localhost"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"database.port"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"3306"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"database.user"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"connect_dev"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"database.password"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"connect_dev"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"database.allowPublicKeyRetrieval"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"true"</span><span class="p">,</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">mysql</span><span class="w"> </span><span class="mf">8.0</span><span class="err">부터는</span><span class="w"> </span><span class="kc">true</span><span class="err">여야</span><span class="w"> </span><span class="err">접속</span><span class="w"> </span><span class="err">가능</span><span class="w"> </span><span class="err">(TLS</span><span class="w"> </span><span class="err">설정)</span><span class="w">
    </span><span class="nl">"database.server.id"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"10022"</span><span class="p">,</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">DB</span><span class="w"> </span><span class="err">서버</span><span class="w"> </span><span class="err">별로</span><span class="w"> </span><span class="err">다르게</span><span class="w"> </span><span class="err">지정해야</span><span class="w"> </span><span class="err">함</span><span class="w">
    </span><span class="nl">"database.server.name"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"test01"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"database.include.list"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"oc"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"table.include.list"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"oc.orders_timestamp_tab"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"database.history.kafka.bootstrap.servers"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"192.168.56.101:9092"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"database.history.kafka.topic"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"schema_changes.mysql.oc"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"key.converter"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"org.apache.kafka.connect.json.JsonConverter"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"value.converter"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"org.apache.kafka.connect.json.JsonConverter"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"time.precision.mode"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"connect"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"database.connectionTimezone"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"Asia/Seoul"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"transforms"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"unwrap"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"transforms.unwrap.type"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"io.debezium.transforms.ExtractNewRecordState"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"transforms.unwrap.drop.tombstones"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"false"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">//</span><span class="w"> </span><span class="err">jdbc</span><span class="w"> </span><span class="err">sink</span><span class="w"> </span><span class="err">connector</span><span class="w">

</span><span class="p">{</span><span class="w">
  </span><span class="nl">"name"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"mysql_jdbc_sink_timestamp_tab"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"config"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"connector.class"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"io.confluent.connect.jdbc.JdbcSinkConnector"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"tasks.max"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"1"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"topics"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"oc.orders_timestamp_tab"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"connection.url"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"jdbc:mysql://localhost:3306/oc_sink"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"connection.user"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"connect_dev"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"connection.password"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"connect_dev"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"insert.mode"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"upsert"</span><span class="p">,</span><span class="w"> 
    </span><span class="nl">"pk.mode"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"record_key"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"pk.fields"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"order_id"</span><span class="p">,</span><span class="w"> 
    </span><span class="nl">"delete.enabled"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"true"</span><span class="p">,</span><span class="w"> 
    </span><span class="nl">"key.converter"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"org.apache.kafka.connect.json.JsonConverter"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"value.converter"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"org.apache.kafka.connect.json.JsonConverter"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"db.timezone"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"Asia/Seoul"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"transforms"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"conevertTs"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"transforms.convertTs.type"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"org.apache.kafka.connect.transforms.TimestampConvert$Value"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"transforms.convertTs.field"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"order_timestamp"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"transforms.convertTs.format"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"yyyy-MM-dd'T'HH:mm:ss'Z'"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"transforms.convertTs.target.type"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"Timestamp"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><br /></p>
<hr />

<h3 id="배치-메시지-관련-파라미터">배치 메시지 관련 파라미터</h3>
<ul>
  <li><code class="language-plaintext highlighter-rouge">poll.interval.ms</code> : source connector가 배치 메시지를 전송하기 전까지 기다리는 시간 (기본 1000ms)</li>
  <li><code class="language-plaintext highlighter-rouge">max.batch.size</code> : batch message의 최대  record 건 수 (기본 2048)</li>
  <li><code class="language-plaintext highlighter-rouge">max.queue.size</code> : producer에 넘기기 전 대기하는 내부 queue의 최대 레코드 크기 (기본 8192), max.batch.size 보다 커야함</li>
  <li><code class="language-plaintext highlighter-rouge">max.queue.size.in.bytes</code> : producer에 넘기기 전 대기하는 내부 queue의 최대 메모리 크기 (기본 0)
    <ul>
      <li>max.queue.size와 max.queue.size.in.bytes가 동시에 설정되어 있으면 어느 한 조건만 만족해도 되는 크기로 queue 크기가 정해짐</li>
    </ul>
  </li>
</ul>

<p><img src="https://imucoding.github.io/assets/images/debeziumparameter.png" alt="img.png" /></p>

<p><br /></p>
<hr />

<h3 id="source-테이블의-스키마-변경-유형에-따른-자동-스키마-반영">Source 테이블의 스키마 변경 유형에 따른 자동 스키마 반영</h3>
<ul>
  <li>스키마 호환성보다 우선 시 됨</li>
</ul>

<h4 id="컬럼-추가">컬럼 추가</h4>
<ul>
  <li>source 테이블의 null 가능한 숫자형, date, datetime, timestamp 컬럼들은 정상적으로 자동 반영, 다만 datetime의 경우는 datetime(3)으로 변형되어 반영</li>
  <li>source 테이블의 not null 컬럼은 반드시 default 값을 명시적으로 선언해주어야 함 (schema registry 적용 시 문제 발생)</li>
  <li>varchar 컬럼 추가 시 sink 테이블은 text로 변환됨</li>
  <li>varchar 컬럼을 not null로 default 값 설정해서 추가하면 sink 테이블은 text를 default 값으로 변경하지 못해서 장애 발생 -&gt; 수동 변경 권장
    <ul>
      <li>default는 저장 공간을 미리 할당해야 하는데, text의 경우 가변 길이 데이터 타입이므로 필드가 차지하는 저장 공간을 미리 알 수 없음</li>
    </ul>
  </li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>정수형 컬럼 추가 
alter table customers add column (age int);

Not Null 컬럼 추가 (default 필수)
alter table customers add column (age int not null default 0);
alter table customers add column (date date not null default '2022-11-25');
alter table customers add column (datetime datetime not null default '2022-11-25 13:00:00');

Varchar Null 컬럼 추가
alter table customers add column (address varcher(100));

Varchar Not Null 컬럼 추가
alter table customers add column (address varcher(100) not null default 'test address');
- insert 시점에 sink connector 오류 발생
- __connect_offsets 확인 시 위 메시지를 읽지도 못함
</code></pre></div></div>

<h4 id="컬럼-타입-변경">컬럼 타입 변경</h4>
<ul>
  <li>source 테이블의 컬럼 타입 변경은 sink 테이블에서 반영되지 못함</li>
  <li>수동 변경 필요</li>
</ul>

<h4 id="컬럼-타입-삭제">컬럼 타입 삭제</h4>
<ul>
  <li>source 테이블의 컬럼 삭제는 sink 테이블에서 반영되지 못함</li>
  <li>수동 변경 필요</li>
</ul>

<h4 id="컬럼명-변경">컬럼명 변경</h4>
<ul>
  <li>source 테이블의 컬럼명 변경은 sink 테이블에서 이상 반영
    <ul>
      <li>컬럼이 새로 추가 됨</li>
    </ul>
  </li>
  <li>수동 변경 권장</li>
</ul>

<p><br /></p>
<hr />

<h3 id="mysql에서-jdbc-sink-postgresql-연동">MySQL에서 JDBC Sink PostgreSQL 연동</h3>
<p><img src="https://imucoding.github.io/assets/images/datatype.png" alt="img.png" /></p>

<h4 id="mysql과-postgresql간-date와-time-관련-데이터-타입-변환">MySQL과 PostgreSQL간 Date와 Time 관련 데이터 타입 변환</h4>
<ul>
  <li>mysql의 date / time / datetime 변환을 위해서는 debezium mysql source connector에서 <code class="language-plaintext highlighter-rouge">time.precision.mode=connect</code>를 반드시 설정</li>
  <li>date, time, datetime은 int로 변환되지만 timestamp는 문자열로 변환
    <ul>
      <li>mysql의 date, time, timestamp는 postgresql로 정상 입력됨</li>
      <li>mysql의 timestamp는 JDBC postgresql sink connector에서 SMT를 이용하여 timestamp 형태로 변환되어야 함</li>
      <li>이 때 sink connector의 <code class="language-plaintext highlighter-rouge">db.timezone = Asia/Seoul</code> 을 설정하지 않아도 자동으로 postgreSQL의 DB timezone을 반영하여 입력됨</li>
    </ul>
  </li>
</ul>

<h4 id="환경-구축">환경 구축</h4>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo apt update / sudo apt -y upgrade
sudo apt install postgresql postgresql-client
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo su - postgres
cd /etc/postgresql/12/main
</code></pre></div></div>
<ul>
  <li>postgres os 사용자로 로그인</li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>listen_addresses=`*`
wal_level = logical
</code></pre></div></div>
<ul>
  <li>postgresql.conf의 설정 변경</li>
  <li>wal_level
    <ul>
      <li>minimal : 가장 적은 양의 정보를 로그에 기록</li>
      <li>replica : 주로 스트리밍 복제에 사용</li>
      <li>logical : 논리적 복제를 위한 모든 정보 로그에 기록</li>
    </ul>
  </li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># "logic" is for unix domain socket connections only
  local     all     all     md5
  
# IPv3 local connections:
  host      all     all     0.0.0.0/0     md5
</code></pre></div></div>
<ul>
  <li>pg_hba.conf의 아래 IPv4 address를 0.0.0.0/0으로 변경</li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo systemctl restart postgresql
sudo su - postgres
psql
</code></pre></div></div>
<ul>
  <li>postgresql 재기동 및 psql 접속</li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>create user connect_dev password 'connect_dev';
alter user connect_dev createdb;
grant all privileges on database postgres to connect_dev;
psql -h localhost -U connect_dev -d postgres
</code></pre></div></div>
<ul>
  <li>새로운 user 생성 및 postgres DB 접속
```
create schema ops_sink; // mysql에서 db에 해당
set search_path = ops_sink; // default schema 설정</li>
</ul>

<p>CREATE TABLE customers_sink(
  customer_id int NOT NULL PRIMARY KEY,
  email varchar(255) NOT NULL,
  name varchar(255) NOT NULL);</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- 신규 스키마 생성 

```json
// postgresql DB 대상 jdbc sink connector

{
  "name" : "postgres_jdbc_sink_timestamp_tab",
  "config" : {
    "connector.class" : "io.confluent.connect.jdbc.JdbcSinkConnector",
    "tasks.max" : "1",
    "topics" : "mysqlavro_ops_customers",
    "connection.url" : "jdbc:postgresql://localhost:5432/postgres",
    "connection.user" : "connect_dev",
    "connection.password" : "connect_dev",
    "insert.mode" : "upsert",
    "pk.mode" : "record_key",
    "pk.fields" : "customer_id",
    "delete.enabled" : "true",
    "key.converter" : "io.confluent.connect.avro.AvroConverter",
    "value.converter" : "io.confluent.connect.avro.AvroConverter",
    "key.converter.schema.registry.url" : "http://localhost:8081",
    "value.converter.schema.registry.url" : "http://localhost:8081",
    "transforms" : "conevertTs",
    "transforms.convertTs.type" : "org.apache.kafka.connect.transforms.TimestampConvert$Value",
    "transforms.convertTs.field" : "order_timestamp",
    "transforms.convertTs.format" : "yyyy-MM-dd'T'HH:mm:ss'Z'",
    "transforms.convertTs.target.type" : "Timestamp"
  }
}
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">"db.timezone" : "Asia/Seoul"</code> 설정 필요 없음 (postgreSQL DB 설정 따름)</li>
</ul>

<h4 id="스키마-변경-유형에-따른-스키마-반영">스키마 변경 유형에 따른 스키마 반영</h4>
<ul>
  <li>Mysql to Mysql
    <ul>
      <li>source 테이블의 null 가능한 숫자형, date, datetime 컬럼들은 정상 반영</li>
      <li>datetime인 경우 datetime(3)으로 반영</li>
      <li>varchar인 경우 text로 반영</li>
      <li>timestamp의 경우 문자열로 변경됨</li>
      <li>source 테이블의 not null은 sink 테이블에서 제대로 인지하지 못해서 null로 적용됨</li>
      <li>varcahr 컬럼 not null로 default 값 추가 시 sink에서 반영하지 못하고 장애 발생</li>
    </ul>
  </li>
  <li>Mysql to PostgreSQL
    <ul>
      <li>source 테이블의 null 가능한 숫자형, date, datetime 컬럼들은 정상 반영</li>
      <li>datetime인 경우 timestamp로 반영</li>
      <li>varchar인 경우 text로 반영</li>
      <li>timestamp의 경우 문자열로 변경됨</li>
      <li>source 테이블의 not null은 sink 테이블에서 제대로 인지하지 못해서 null로 적용됨</li>
      <li>varcahr 컬럼 not null로 default 값 추가 시 postgresql sink에서 정상적으로 default 값을 반영</li>
    </ul>
  </li>
  <li>컬럼 타입 변경 / 삭제 / 컬럼명 변경은 두 경우 동일함</li>
</ul>

<p><br /></p>
<hr />

<h3 id="debezium-postgresql-cdc-source-connector">Debezium PostgreSQL CDC Source Connector</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>DML -&gt; WAL (트랜잭션 로그파일) -&gt; Datafile (Redolog)
</code></pre></div></div>

<h4 id="replication-메커니즘-비교">Replication 메커니즘 비교</h4>
<ul>
  <li><code class="language-plaintext highlighter-rouge">mysql</code> : binary log를 dump로 slave에 전송해서 slave는 log dump 바탕으로 sql을 생성하여 DB 반영 (logical 복제)</li>
  <li><code class="language-plaintext highlighter-rouge">postgresql</code> : WAL file을 chunk 단위로 실시간으로 보내고 받는 쪽에서는 받은 WAL file 그대로 복제함 (dump가 아니며 SQL 변환도 하지 않은 physical 복제)</li>
</ul>

<h4 id="postgresql-logical-replication">PostgreSQL Logical Replication</h4>
<ul>
  <li>postgresql도 mysql 처럼 publisher-subscriber 모델의 logical 복제 도입</li>
</ul>

<p><img src="https://imucoding.github.io/assets/images/replication.png" alt="img.png" /></p>
<ol>
  <li>복제 대상 테이블을 선정해서 <code class="language-plaintext highlighter-rouge">Publication</code>에 등록</li>
  <li>WAL을 decoding 해서 <code class="language-plaintext highlighter-rouge">WAL sender</code>로 전송 (pgoutput plugin이 WAL을 디코딩하는 용도)</li>
  <li><code class="language-plaintext highlighter-rouge">Apply Worker</code>가 SQL로 변환하여 테이블에 반영
    <ul>
      <li><code class="language-plaintext highlighter-rouge">Logical replication Slot</code>에서 WAL이 어디까지 보내졌는지 등의 관리 수행</li>
    </ul>
  </li>
</ol>

<p><img src="https://imucoding.github.io/assets/images/postgresconnect.png" alt="img.png" /></p>
<ul>
  <li>기존 WAL Sender를 통해 subscriber에 전송 대신 source connector가 붙어서 처리</li>
  <li>pgoutput에서 적정 데이터를 뽑아서 전송</li>
</ul>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"name"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"postgres_cdc_oc_source"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"config"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"connection.class"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"io.debezium.connector.postgresql.PostgresConnector"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"tasks.max"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"1"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"database.hostname"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"192.168.56.101"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"database.port"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"5432"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"database.user"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"connect_dev"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"database.password"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"connect_dev"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"database.dbname"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"oc"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"database.server.name"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"pg01"</span><span class="p">,</span><span class="w"> 
    </span><span class="nl">"plugin.name"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"pgoutput"</span><span class="p">,</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">decoding</span><span class="w"> </span><span class="err">plugin</span><span class="w"> </span><span class="err">유형으로</span><span class="w"> </span><span class="err">반드시</span><span class="w"> </span><span class="err">pgoutput으로</span><span class="w"> </span><span class="err">설정</span><span class="w">
    </span><span class="nl">"slot.name"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"debezium_01"</span><span class="p">,</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">없으면</span><span class="w"> </span><span class="err">새로</span><span class="w"> </span><span class="err">생성</span><span class="w">
    </span><span class="nl">"publication.name"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"dbz_publication"</span><span class="p">,</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">기본</span><span class="w"> </span><span class="err">명은</span><span class="w"> </span><span class="err">dbz_publication</span><span class="w"> </span><span class="err">이며</span><span class="p">,</span><span class="w"> </span><span class="err">없으면</span><span class="w"> </span><span class="err">자동</span><span class="w"> </span><span class="err">생성</span><span class="w"> </span><span class="err">모드에</span><span class="w"> </span><span class="err">따라</span><span class="w"> </span><span class="err">생성하거나</span><span class="w"> </span><span class="err">오류</span><span class="w"> </span><span class="err">발생</span><span class="w"> 
    </span><span class="nl">"publication.autocreate.mode"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"all_tables"</span><span class="p">,</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">all_tables</span><span class="w"> </span><span class="err">:</span><span class="w"> </span><span class="err">DB</span><span class="w"> </span><span class="err">내</span><span class="w"> </span><span class="err">모든</span><span class="w"> </span><span class="err">테이블</span><span class="w"> </span><span class="err">등록</span><span class="p">,</span><span class="w"> </span><span class="err">filtered:</span><span class="w"> </span><span class="err">include</span><span class="w"> </span><span class="err">list</span><span class="w"> </span><span class="err">따라</span><span class="w"> </span><span class="err">테이블</span><span class="w"> </span><span class="err">등록</span><span class="p">,</span><span class="w"> </span><span class="err">disabled:</span><span class="w"> </span><span class="err">오류</span><span class="w"> </span><span class="err">발생</span><span class="w">
    </span><span class="nl">"schema.include.list"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"public"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"table.include.list"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"public.customers, public.orders"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"key.converter"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"org.apache.kafka.connect.json.JsonConverter"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"value.converter"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"org.apache.kafka.connect.json.JsonConverter"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"transforms"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"unwrap"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"transforms.unwrap.type"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"io.debezium.transforms.ExtractNewRecordState"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"transforms.unwrap.drop.tombstones"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"false"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><br /></p>

<p>** 부록 **</p>
<ul>
  <li>publication, replication slot 삭제
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>drop publication dbz_publication;
SELECT pg_drop_replication_slot('debezium_01');
</code></pre></div>    </div>
  </li>
  <li>publication, replication slot 확인
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>select * from pg_replication;
select * from pg_replication_slots;
</code></pre></div>    </div>
  </li>
</ul>]]></content><author><name>Hyunjune</name></author><category term="kafka" /><category term="connector" /><category term="debezium" /><category term="cdc" /><summary type="html"><![CDATA[CDC (Change Data Capture) 개요 CDC는 RDBMS오 NoSql 등의 data store 시스템의 변경 데이터를 기록하는 내부 트랜잭션 로그 파일로부터 변경 데이터를 capture하는 설계, sw를 지칭 DB 내부 트랜잭션 로그 파일로부터 변경 데이터를 추출하므로 소스 DB 성능에 큰 영향 없이 대용량 변경 데이터를 매우 빠르게 추출하고 준실시간으로 타겟 DB 연동 가능 DBMS 복제 수준의 안정적이고 정확한 데이터 추출 가능 Debezium은 이러한 CDC 기반의 분산 플랫폼으로 기본적으로는 카프카 커넥터로 제공됨 JDBC source connector의 경우 DB에 select 쿼리를 주기적으로 전송하고 변경 사항 추적은 특정 컬럼 (key, timestamp) 값에 대해 기존에 없던 새로운 값이 추가된 경우만 capture 가능]]></summary></entry><entry><title type="html">파드와 디플로이먼트로 컨테이너 시작</title><link href="https://imucoding.github.io/kubernetes/2024/09/07/pod-deployment.html" rel="alternate" type="text/html" title="파드와 디플로이먼트로 컨테이너 시작" /><published>2024-09-07T00:00:00+00:00</published><updated>2024-09-07T00:00:00+00:00</updated><id>https://imucoding.github.io/kubernetes/2024/09/07/pod-deployment</id><content type="html" xml:base="https://imucoding.github.io/kubernetes/2024/09/07/pod-deployment.html"><![CDATA[<h3 id="쿠버네티스-이해">쿠버네티스 이해</h3>
<ul>
  <li>마스터 노드 (control plain)
    <ul>
      <li>클러스터 노드, 파드 관리</li>
    </ul>
  </li>
  <li>워커 노드 (data plane)
    <ul>
      <li>컨테이너 실행</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">kubeadm init</code> 으로 클러스터 초기화하여 새로운 마스터 노드 생성</li>
  <li>마스터 노드는 기본적으로 컨테이너를 실행할 수 없지만 설정을 통해 가능하도록 할 수 있음</li>
  <li>kubectl은 마스터 노드의 api server에 요청하고 api server는 각 워커 노드에 설치된 <code class="language-plaintext highlighter-rouge">kubelet</code>과 통신하여 각 worker 노드에서 pod를 실행하도록 함</li>
  <li>마스터 노드의 scheduler가 어떤 워커노드에 pod를 배정할 지 결정</li>
</ul>

<p><br /></p>
<hr />

<h3 id="컨테이너-실행">컨테이너 실행</h3>
<ul>
  <li>간단한 pod는 yaml 파일 없이 직접 실행 가능
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl run hello-kiamol --image=kiamol/ch02-hello-kiamol
kubectl wait --for=condition=Ready pod hello-kiamol  // Ready 될 때까지 I/O 대기
kubectl get pods
kubectl describe pod hello-kiamol // 상세 정보 확인 가능
</code></pre></div>    </div>
  </li>
</ul>

<h4 id="포트-포워딩">포트 포워딩</h4>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl port-forward pod/hello-kiamol 8080:80
</code></pre></div></div>
<ul>
  <li>host PC port -&gt; pod port</li>
</ul>

<p><br /></p>
<hr />

<h3 id="컨트롤러-객체">컨트롤러 객체</h3>
<ul>
  <li>파드가 여러 노드에서 실행되다가 어떤 노드가 고장 발생 시 파드를 복구해주지 않음</li>
  <li>또한 파드가 여러 노드에 고르게 흩어져서 실행된다는 보장이 없으므로 사람이 직접 개입해야하고 오케스트레이션 도구 사용의 의미가 없음</li>
  <li>따라서 컨트롤러 객체를 사용해야 하고 파드를 관리하는 컨트롤러 객체는 디플로이먼트
    <ul>
      <li><code class="language-plaintext highlighter-rouge">컨트롤러 객체</code> : 다른 리소스를 관리하는 쿠버네티스 리소스</li>
    </ul>
  </li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl create deployment hello-kiamol --image=kiamol/ch02-hello-kiamol
</code></pre></div></div>
<ul>
  <li>pod와 달리 run이 아닌 create로 생성</li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get pods

Name                      Ready      Status      Restarts      Age
hello-kiamol-21562342      1/1       Running         0         10s
</code></pre></div></div>
<ul>
  <li>k8s가 디플로이먼트 이름 뒤 무작위 문자열을 붙여서 pod 생성</li>
</ul>

<p><br /></p>
<hr />

<h3 id="deployment의-pod-관리">Deployment의 Pod 관리</h3>
<ul>
  <li>모든 쿠버네티스 리소스 (deployment, pod …) 은 key-value 쌍의 label을 가짐</li>
  <li>deployment는 자신의 selector와 label이 일치하는 pod를 관리함</li>
  <li><code class="language-plaintext highlighter-rouge">kubectl describe deploy heelo-kiamol</code>에서 label과 selector를 확인해보면 둘다 <code class="language-plaintext highlighter-rouge">app:hello-kiamol</code>임을 알 수 있음
    <ul>
      <li>본인의 label과 동일하게 selector를 지정하였고 이 selector를 pod에 부여함</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">kubectl get pods -l app=hello-kiamol</code>
    <ul>
      <li>app:hello-kiamol의 label을 가진 pod 조회</li>
    </ul>
  </li>
  <li>만약 pod의 label을 변경한다면 deployment의 관리에서 벗어남
    <ul>
      <li><code class="language-plaintext highlighter-rouge">kubectl label pods -l app-hello-kiamol --overwrite app=x</code></li>
    </ul>
  </li>
  <li>pod를 다시 조회해보면 새로운 pod가 생성되었음이 확인되는데 deployment가 label selector와 일치하는 pod가 사라졌기 때문에 새로운 pod 생성
    <ul>
      <li>만약 관리가 벗어난 pod의 app:x 레이블을 원래대로 복원 시 새로 생성한 pod가 삭제됨 (1개 pod 유지)</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">kubectl port-forward deploy/hello-kiamol 8080:80</code>
    <ul>
      <li>deployment를 port forwarding 시키면 deployment는 자신이 가진 pod 중 1개를 트래픽 전달 대상으로 삼음</li>
    </ul>
  </li>
</ul>

<p><br /></p>
<hr />

<h3 id="애플리케이션-manifest에-배포-정의">애플리케이션 manifest에 배포 정의</h3>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># pod.yaml</span>

<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span> <span class="c1"># k8s API version</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Pod</span> <span class="c1"># 리소스 유형</span>
<span class="na">metadata</span><span class="pi">:</span> <span class="c1"># 리소스에서 메타데이터는 name(필수)과 label(비필수)가 존재</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">hello-kiamol</span>
<span class="na">spec</span><span class="pi">:</span> <span class="c1"># 리소스의 실제 정의 내용</span>
  <span class="na">containers</span><span class="pi">:</span> <span class="c1"># pod는 컨테이너 정의 필요</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">web</span> <span class="c1"># 컨테이너는 이름과 이미지로 정의</span>
      <span class="na">image</span><span class="pi">:</span> <span class="s">kiamol/ch02-hello-kiamol</span>
</code></pre></div></div>
<ul>
  <li>리소스에 대한 버전 확인
    <ul>
      <li><code class="language-plaintext highlighter-rouge">kubectl explain pod</code></li>
      <li>group / version 형식으로 apiVersion에 작성</li>
    </ul>
  </li>
</ul>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># deployment.yaml</span>

<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">hello-kiamol</span> <span class="c1"># deployment 이름 지정</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">selector</span><span class="pi">:</span> <span class="c1"># 관리 대상 레이블 셀렉터 설정</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
        <span class="s">app:hello-kiamol</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">3</span>
  <span class="na">template</span><span class="pi">:</span> <span class="c1"># pod 생성 시 사용</span>
    <span class="na">metadata</span><span class="pi">:</span> <span class="c1"># pod 정의</span>
      <span class="na">labels</span><span class="pi">:</span> <span class="c1"># pod 이름 대신 label을 지정하는데 selector와 일치해야함</span>
        <span class="s">app:hello-kiamol</span>
      <span class="na">spec</span><span class="pi">:</span> <span class="c1"># pod 컨테이너 정의</span>
        <span class="na">containers</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">web</span>
            <span class="na">image</span><span class="pi">:</span> <span class="s">kiamol/ch02-hello-kiamol</span>
</code></pre></div></div>

<p><br /></p>
<hr />

<h3 id="pod에서-실행-중인-애플리케이션에-접근">Pod에서 실행 중인 애플리케이션에 접근</h3>
<p>1) pod 접속 <br />
  <code class="language-plaintext highlighter-rouge">kubectl exec -it hello-kiamol sh</code> <br />
2) pod log 출력 <br />
  <code class="language-plaintext highlighter-rouge">kubectl logs --tail=2 hello-kiamol</code> <br />
3) deployment를 통해서 pod에서 명령 실행 <br />
  <code class="language-plaintext highlighter-rouge">kubectl exec deploy/hello-kiamol --sh -c 'wget -O - http://localhost &gt; /dev/null</code> <br />
4) pod 속 컨테이너에서 local로 파일 복사 <br />
  <code class="language-plaintext highlighter-rouge">kubectl cp hello-kiamol:usr/share/nginx/html/index.html /tmp/index.html</code> <br /></p>

<p><br /></p>
<hr />

<h3 id="쿠버네티스-리소스-관리-이해">쿠버네티스 리소스 관리 이해</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl delete pods -all
</code></pre></div></div>
<ul>
  <li>모든 pod 삭제</li>
  <li>pod 조회 시 다시 pod가 생성됨, deploy가 존재하기 때문</li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get all
</code></pre></div></div>
<ul>
  <li>모든 리소스 확인</li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl delete deploy -all
</code></pre></div></div>
<ul>
  <li>deployment 삭제</li>
</ul>]]></content><author><name>Hyunjune</name></author><category term="kubernetes" /><category term="pod" /><category term="deployment" /><summary type="html"><![CDATA[쿠버네티스 이해 마스터 노드 (control plain) 클러스터 노드, 파드 관리 워커 노드 (data plane) 컨테이너 실행 kubeadm init 으로 클러스터 초기화하여 새로운 마스터 노드 생성 마스터 노드는 기본적으로 컨테이너를 실행할 수 없지만 설정을 통해 가능하도록 할 수 있음 kubectl은 마스터 노드의 api server에 요청하고 api server는 각 워커 노드에 설치된 kubelet과 통신하여 각 worker 노드에서 pod를 실행하도록 함 마스터 노드의 scheduler가 어떤 워커노드에 pod를 배정할 지 결정]]></summary></entry><entry><title type="html">네트워크를 통해 서비스에 파드 연결</title><link href="https://imucoding.github.io/kubernetes/2024/09/07/service.html" rel="alternate" type="text/html" title="네트워크를 통해 서비스에 파드 연결" /><published>2024-09-07T00:00:00+00:00</published><updated>2024-09-07T00:00:00+00:00</updated><id>https://imucoding.github.io/kubernetes/2024/09/07/service</id><content type="html" xml:base="https://imucoding.github.io/kubernetes/2024/09/07/service.html"><![CDATA[<p>서로 다른 두 파드가 통신하려면 서로의 IP 주소를 먼저 알아야 함</p>
<ul>
  <li>pod 간 통신을 위해서 IP 주소를 사용하는데 IP 주소는 pod 대체 시 변경됨</li>
  <li>service address discovery를 통해 문제 해결</li>
  <li>pod의 교체된 IP 주소는 kubernetes API를 통해서만 찾을 수 있음</li>
</ul>

<p><br /></p>

<h3 id="쿠버네티스-내부의-네트워크-트래픽-라우팅">쿠버네티스 내부의 네트워크 트래픽 라우팅</h3>
<ul>
  <li>언제든지 pod IP 주소는 바뀔 수 있으므로 고정된 도메인 네임 도입</li>
  <li>쿠버네티스 내부에는 전용 DNS 서버가 있음</li>
</ul>

<p><img src="https://imucoding.github.io/assets/images/service.png" alt="img.png" /></p>
<ul>
  <li>서비스 생성 시 서비스의 IP 주소가 DNS 서버에 등록되고 서비스 삭제 전까지 변경되지 않음</li>
  <li>서비스는 label selector를 이용하여 파드와 느슨한 연결</li>
  <li>sleep-1 파드는 sleep-2 파드에 통신 시 도메인 네임을 가지고 수행하며 DNS 조회는 DNS 서버에서 처리됨. DNS 서버가 서비스의 IP 주소를 반환</li>
  <li>sleep-1 파드는 서비스가 없으므로 도메인 네임으로 접근 불가</li>
</ul>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># sleep2-service.yaml</span>

<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">sleep-2</span> <span class="c1"># 서비스 이름이 도메인명으로 사용됨</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="s">app:sleep-2</span> <span class="c1"># 대상 pod label</span>
  <span class="na">ports</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">port</span><span class="pi">:</span> <span class="m">80</span> <span class="c1"># 80번 포트를 주시하다가 pod의 80번 포트로 트래픽 전달</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply -f sleep2-service.yaml
kubectl get svc sleep-2
kubectl exec deploy/sleep-1 -- ping -c | sleep-2
</code></pre></div></div>
<ul>
  <li>ping은 k8s에서 지원하는 TCP/UDP 프로토콜이아닌 ICMP 이므로 실패함</li>
</ul>

<p><br /></p>
<hr />

<h3 id="파드와-파드-간-통신">파드와 파드 간 통신</h3>
<ul>
  <li>서비스 유형 중 가장 기본은 <code class="language-plaintext highlighter-rouge">ClusterIP</code></li>
  <li>ClusterIP는 클러스터 전체에서 통용되는 IP 주소를 생성하며 파드가 어느 노드에 있더라도 접근이 가능</li>
  <li>ClusterIP는 클러스터 내에서만 유효하므로 파드와 파드간 통신에만 사용</li>
</ul>

<h4 id="웹-사이트와-api를-담당할-두-개의-디플로이먼트-실행">웹 사이트와 API를 담당할 두 개의 디플로이먼트 실행</h4>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply -f api.yaml -f web.yaml
kubectl port-forward deploy/numbers-web 8080:80
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">http://localhost:8080</code> 접속 시 deploy/numbers-web의 80 port로 포워딩 되고 web 페이지가 출력됨</li>
  <li>web에서는 <code class="language-plaintext highlighter-rouge">http://numbers-api/...</code> api를 사용하지만 현재 등록된 서비스가 없으므로 도메인을 찾지 못해서 오류 발생</li>
</ul>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># api-service.yaml</span>

<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">numbers-api</span> <span class="c1"># 도메인 등록</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="s">app:numbers-api</span>
  <span class="na">ports</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">port</span><span class="pi">:</span> <span class="m">80</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">ClusterIP</span> <span class="c1"># 클러스터 내부에서만 사용</span>

</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply -f api-service.yaml
kubectl get svc numbers-api

    NAME      TYPE      CLUSTER-IP    EXTERNAL-IP   PORTS   AGE
numbers-api ClusterIP   10.109.24.16    &lt;none&gt;        80    25s
</code></pre></div></div>
<ul>
  <li>api 파드를 삭제 후 deploy에서 재생성하여 IP 주소가 변경되어도 web 파드는 서비스 도메인을 통해 api 파드에 요청하므로 정상 수행됨</li>
</ul>

<p><br /></p>
<hr />

<h3 id="외부-트래픽을-파드로-전달">외부 트래픽을 파드로 전달</h3>

<h4 id="loadbalancer">LoadBalancer</h4>
<ul>
  <li>로컬 개별 환경과 운영 환경 모두 적용 가능 방법</li>
  <li>트래픽을 받은 노드가 아닌 다른 노드에서 실행되는 pod에도 트래픽 전달 가능</li>
</ul>

<p><img src="https://imucoding.github.io/assets/images/loadbalancer.png" alt="img.png" /></p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># web-service.yaml</span>

<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">numbers-web</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="s">app:numbers-web</span> <span class="c1"># 대상 pod label</span>
  <span class="na">ports</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">port</span><span class="pi">:</span> <span class="m">8080</span> <span class="c1"># 서비스에서 주시하는 포트</span>
      <span class="na">targetPort</span><span class="pi">:</span> <span class="m">80</span> <span class="c1"># 트래픽이 전달된 파드의 포트</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">LoadBalancer</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply -f web-service.yaml 
kubectl get svc numbers-web
</code></pre></div></div>
<ul>
  <li>로드 밸런서 등록 시 external IP를 부여 받음</li>
  <li>EKS 클러스터에서 수행 시 공인 IP 주소 부여 받음</li>
  <li>port forwarding 필요 없음</li>
</ul>

<h4 id="nodeport">NodePort</h4>
<ul>
  <li>외부 로드밸런서가 필요 없음</li>
  <li>클러스터의 모든 노드가 서비스에 지정된 port를 오픈하고 트래픽을 파드에 전달</li>
</ul>

<p><img src="https://imucoding.github.io/assets/images/nodeport.png" alt="img.png" /></p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># web-service-nodeport.yaml</span>

<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">numbers-web-node</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="s">app:numbers-web</span>
  <span class="na">ports</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">port</span><span class="pi">:</span> <span class="m">8080</span> <span class="c1"># 파드가 서비스에 접근하기 위해 사용</span>
      <span class="na">targetPort</span><span class="pi">:</span> <span class="m">80</span>  <span class="c1"># 파드에 트래픽을 전달하는 포트</span>
      <span class="na">nodePort</span><span class="pi">:</span> <span class="m">30080</span> <span class="c1"># 서비스 외부 공개 포트</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">NodePort</span> <span class="c1"># 노드의 IP 주소를 통해 직접 클러스터 접속</span>
</code></pre></div></div>

<p><br /></p>
<hr />

<h3 id="쿠버네티스-외부로-트래픽-전달">쿠버네티스 외부로 트래픽 전달</h3>
<ul>
  <li>쿠버네티스 클러스터 내에서 실행하는 서비스가 아닌 경우 (ex DB) 외부 서비스와 연동 필요성 존재</li>
</ul>

<h4 id="externalname-서비스">ExternalName 서비스</h4>

<p><img src="https://imucoding.github.io/assets/images/externalname.png" alt="img.png" /></p>
<ul>
  <li>서비스는 db-service라는 명으로 도메인 등록 그리고 externalName으로 외부 도메인 지정</li>
  <li>pod에서 db-service 도메인으로 접근 시 외부 도메인에 연결됨</li>
</ul>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># api-service-externalName.yaml</span>

<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">db-service</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">ExternalName</span>
  <span class="na">externalName</span><span class="pi">:</span> <span class="s">app.mydb.io</span>
</code></pre></div></div>
<ul>
  <li>하지만 externalName은 app이 가리키는 주소를 치환할 뿐 요청 내용 자체를 바꾸지는 못함</li>
  <li>HTTP 요청의 경우 요청 헤더에 대상 호스트 명이 들어가는데 이 호스트 명이 externalName과 다른 경우 요청 실패함
    <ul>
      <li>app에서 host명을 바꾸는 작업 수행 필요</li>
    </ul>
  </li>
</ul>

<h4 id="헤드리스-서비스">헤드리스 서비스</h4>
<ul>
  <li>ExternalName과 유사하게 로컬 도메인 네임을 치환하지만 도메인 네임 대신 IP 주소를 대체함</li>
  <li>헤드리스 서비스는 ClusterIP 형태로 정의되고 레이블 셀렉터가 없으므로 대상 파드가 없음</li>
  <li>대신 자신이 제공해야 할 IP 목록이 담긴 엔드 포인트 리소스와 함께 배포됨</li>
</ul>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># api-service-headless.yaml</span>

<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">numbers-api</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">ClusterIP</span>
  <span class="na">ports</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">port</span><span class="pi">:</span> <span class="m">80</span>

<span class="nn">---</span>

<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Endpoints</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">numbers-api</span>
<span class="na">subsets</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">addresses</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">ip</span><span class="pi">:</span> <span class="s">192.168.124.234</span> <span class="c1"># IP 주소가 유효하지 않아도 서비스 등록됨</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">ports</span><span class="pi">:</span> <span class="m">80</span> <span class="c1"># 각 IP 주소에서 주시할 포드</span>
</code></pre></div></div>

<ul>
  <li>headless 서비스 사용 시나리오
    <ul>
      <li>만약 같은 서비스에 속하는 pod A, B가 통신하고 싶다면?
        <ul>
          <li>서비스를 이용 시 서비스는 본인에게 속한 pod 중 1개로 트래픽을 보내므로 부적합</li>
        </ul>
      </li>
      <li>클러스터에 속한 클라이언트가 여러 pod와 통신하고 싶다면?
        <ul>
          <li>모든 엔드포인트 IP 주소가 필요</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply -f api-service-headless.yaml
kubectl get svc numbers-api
kubectl get endpoints numbers-api
</code></pre></div></div>

<p><br /></p>
<hr />

<h3 id="쿠버네티스-서비스의-해소-과정">쿠버네티스 서비스의 해소 과정</h3>

<p><img src="https://imucoding.github.io/assets/images/dnsserver.png" alt="img.png" /></p>

<ul>
  <li>파드 속 컨테이너가 요청한 도메인 네임 조회는 DNS 서버가 응답함</li>
  <li>조회 대상이 서비스라면 클러스터 내 IP 주소 혹은 외부 도메인 네임 반환</li>
  <li>파드에서 나온 모든 통신은 네트워크 프록시가 라우팅 담당</li>
  <li>각 노드에서 모든 서비스의 엔드포인트에 대한 최신 정보 유지하고 OS의 네트워크 패킷 필터(iptables)를 사용해서 트래픽 라우팅함</li>
  <li>ClusterIP는 실재하지 않은 가상 IP 이므로 네트워크 프록시가 패킷 필터링을 통해 이를 실제 엔트포인트에 연결</li>
</ul>

<h4 id="네임스페이스">네임스페이스</h4>
<ul>
  <li>리소스는 모든 네임스페이스 안에 존재함</li>
  <li>네임스페이스는 다른 리소스를 묶기 위한 리소스</li>
</ul>

<p><img src="https://imucoding.github.io/assets/images/namespace.png" alt="img.png" /></p>

<ul>
  <li>같은 네임스페이스 안에서는 도메인 네임 number-api로 접근 가능</li>
  <li>다른 네임스페이스에서 접근하려면 <code class="language-plaintext highlighter-rouge">number-api.default.svc.cluster.local</code>이라는 도메인 네임으로 접근 가능</li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get svc --namespace default
</code></pre></div></div>

<p>** 주의</p>
<ul>
  <li>리소스 삭제 시 pod나 deployment와 달리 서비스는 default namespace에 쿠버네티스 API가 존재하므로 주의해야 함</li>
  <li><code class="language-plaintext highlighter-rouge">kubectl delete svc --all</code> 대신 지정해서 삭제 필요!</li>
</ul>]]></content><author><name>Hyunjune</name></author><category term="kubernetes" /><category term="network" /><category term="service" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">JDBC Connector</title><link href="https://imucoding.github.io/kafka/2024/09/05/jdbc-connector.html" rel="alternate" type="text/html" title="JDBC Connector" /><published>2024-09-05T00:00:00+00:00</published><updated>2024-09-05T00:00:00+00:00</updated><id>https://imucoding.github.io/kafka/2024/09/05/jdbc-connector</id><content type="html" xml:base="https://imucoding.github.io/kafka/2024/09/05/jdbc-connector.html"><![CDATA[<h3 id="jdbc-source-connector">JDBC Source Connector</h3>
<ul>
  <li>JDBC source connector는 RDBMS 별 JDBC driver가 별도로 설치되어야 함</li>
  <li>JDBC source connector는 JDBC driver를 이용하여 DB 접속 및 데이터 추출을 수행 후 producer를 이용하여 broker로 데이터를 전송함</li>
  <li>JDBC source connector는 source 시스템에 주기적으로 query를 날려 변경된 데이터를 추출하는 방식</li>
</ul>

<p><img src="https://imucoding.github.io/assets/images/driver.png" alt="img.png" /></p>

<p><br /></p>
<hr />

<h3 id="jdbc-source-connector-모드-유형">JDBC Source Connector 모드 유형</h3>
<ul>
  <li><code class="language-plaintext highlighter-rouge">incremental query mode</code> : 이전 데이터 추출 이후 새롭게 생성된 데이터나 업데이트 된 데이터를 kafka 전송. 모드 별로 대상 컬럼을 지정해 주어야 함
    <ul>
      <li><code class="language-plaintext highlighter-rouge">incrementing</code> : insert만 가능. auto incremental pk 컬럼 필요.</li>
      <li><code class="language-plaintext highlighter-rouge">timestamp</code> : insert / update 가능</li>
      <li><code class="language-plaintext highlighter-rouge">timestamp + incrementing</code> : insert / update 가능. 가장 안정적</li>
      <li><code class="language-plaintext highlighter-rouge">bulk</code> : 테이블 데이터 한번에 모두 kafka 전송. 이후 테이블 데이터가 모두 삭제되어야 불필요한 재전송 안함</li>
    </ul>
  </li>
</ul>

<h4 id="incrementing-모드">Incrementing 모드</h4>
<p><img src="https://imucoding.github.io/assets/images/incremental.png" alt="img.png" /></p>

<h4 id="timestamp-모드">Timestamp 모드</h4>
<p><img src="https://imucoding.github.io/assets/images/timestamp.png" alt="img.png" /></p>

<p><br /></p>
<hr />

<h3 id="jdbc-source-connector-문제점">JDBC Source Connector 문제점</h3>
<ul>
  <li>source DB의 데이터 삭제 이벤트를 kafka로 전송 불가능</li>
  <li>CDC 기반 connector보다 source DB 성능에 상대적으로 더 큰 영향을 미칠 수 있음</li>
  <li>source 데이터의 모든 변경 이벤트가 제대로 kafka로 전송되었다고 보장되기 어려움</li>
</ul>

<p><br /></p>
<hr />

<h3 id="jdbc-connector-환경-구성">JDBC Connector 환경 구성</h3>
<ol>
  <li>JDBC source / sink connector download <br /></li>
  <li>Mysql JDBC driver download <br /></li>
  <li>JDBC connector, Mysql jdbc driver를 <code class="language-plaintext highlighter-rouge">plugin.path</code> 이동 <br /></li>
  <li>connect 재기동 <br /></li>
  <li>plugin 정상 등록 확인  <br />
<code class="language-plaintext highlighter-rouge">
http http://localhost:8083/connector-plugins
</code></li>
</ol>

<h4 id="mysql-설치-및-환경-구성">Mysql 설치 및 환경 구성</h4>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo apt-get update &amp;&amp; sudo apt-get upgrade
sudo apt-get install mysql-server mysql-client
sudo ufw allow mysql
</code></pre></div></div>

<p>mysql 접속하여 root password 변경</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo mysql
create user 'root'@'%' identified with mysql_native_password by 'root'; 
grant all privileges on *.* to 'root'@'%' with grant option;
flush privileges;
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">mysql_native_password</code> : 암호화된 연결 플러그인</li>
</ul>

<p>시스템 기동 시 mysql 함께 기동</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo systemctl enable mysql
</code></pre></div></div>

<p>DB 생성</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo mysql -u root -p
create database om;
show databases;
</code></pre></div></div>

<p>새로운 사용자 추가 및 DB 사용 권한 할당</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>create user 'connect_dev'@'%' identified by 'connect_dev';
grant all privillegs on om.* to 'connect_dev'@'%' with grant option;
</code></pre></div></div>

<p>테이블 생성</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mysql -u connect_dev -p
use om;
CREATE TABLE customers(
  customer_id int NOT NULL AUTO_INCREMENT PRIMARY_KEY,
  email varchar(255) NOT NULL,
  name varchar(255) NOT NULL,
  system_upd timestamp NOT NULL)
  ENGINE=InnoDB;
create index idx_customers on customers(system_upd);
</code></pre></div></div>
<ul>
  <li>update 용 system_upd 칼럼에 인덱스 생성</li>
</ul>

<p>한국 표준시를 mysqld 환경에 적용</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cd /etc/mysql/mysql.conf.d
sudo vi mysqld.cnf
default_timze_zon=`09:00`
</code></pre></div></div>
<p>mysql 재기동</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo systemctl restart mysql
</code></pre></div></div>
<p>timezone 확인</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>show variables like `%time_zone%`;
</code></pre></div></div>

<p><br /></p>
<hr />

<h3 id="incrementing-mode-용-jdbc-source-connector-생성-및-등록">Incrementing mode 용 JDBC Source Connector 생성 및 등록</h3>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"name"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"mysql_jdbc_on_source"</span><span class="p">,</span><span class="w">  </span><span class="err">//</span><span class="w"> </span><span class="err">connector</span><span class="w"> </span><span class="err">명</span><span class="w">
  </span><span class="nl">"config"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"connector.class"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"io.confluent.connect.jdbc.Jdbc.SourceConnector"</span><span class="p">,</span><span class="w">  </span><span class="err">//</span><span class="w"> </span><span class="err">connector</span><span class="w"> </span><span class="err">class</span><span class="w"> </span><span class="err">명</span><span class="w">
    </span><span class="nl">"tasks.max"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"1"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"connection.url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"jdbc:mysql://localhost:3306/om"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"connection.user"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"connect_dev"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"connection.password"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"connect_dev"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"topic.prefix"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"mysql_om_"</span><span class="p">,</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">topic</span><span class="w"> </span><span class="err">명은</span><span class="w"> </span><span class="err">topic.prefix</span><span class="w"> </span><span class="err">+</span><span class="w"> </span><span class="err">table</span><span class="w"> </span><span class="err">명으로</span><span class="w"> </span><span class="err">생성</span><span class="w">
    </span><span class="nl">"topic.creation.enable"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"true"</span><span class="p">,</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">지정된</span><span class="w"> </span><span class="err">topic</span><span class="w"> </span><span class="err">명의</span><span class="w"> </span><span class="err">topic</span><span class="w"> </span><span class="err">없을</span><span class="w"> </span><span class="err">경우</span><span class="w"> </span><span class="err">자동</span><span class="w"> </span><span class="err">생성</span><span class="w">
    </span><span class="nl">"topic.creation.default.replication.factor"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
    </span><span class="nl">"topic.creation.default.partitions"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
    </span><span class="nl">"catalog.pattern"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"om"</span><span class="p">,</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">postgres</span><span class="p">,</span><span class="w"> </span><span class="err">oracle</span><span class="w"> </span><span class="err">경우에는</span><span class="w"> </span><span class="err">schema.pattern</span><span class="w">
    </span><span class="nl">"table.whitelist"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"om.customers"</span><span class="p">,</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">여러</span><span class="w"> </span><span class="err">개의</span><span class="w"> </span><span class="err">경우</span><span class="w"> </span><span class="err">콤마로</span><span class="w"> </span><span class="err">분리</span><span class="w">
    </span><span class="nl">"poll.interval.ms"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="mi">10000</span><span class="p">,</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">조회</span><span class="w"> </span><span class="err">주기</span><span class="w">
    </span><span class="nl">"incrementing.column.name"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"customer_id"</span><span class="p">,</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">mode가</span><span class="w"> </span><span class="err">incrementing일</span><span class="w"> </span><span class="err">경우</span><span class="w"> </span><span class="err">PK</span><span class="w"> </span><span class="err">컬럼</span><span class="w">
    </span><span class="nl">"mode"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"incrementing"</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">incrementing</span><span class="p">,</span><span class="w"> </span><span class="err">timestamp</span><span class="p">,</span><span class="w"> </span><span class="err">timestamp</span><span class="w"> </span><span class="err">+</span><span class="w"> </span><span class="err">incrementing</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">catalog.pattern</code> : 지정된 스키마에 소속된 모든 테이블 연동</li>
  <li>여러 개 테이블 지정 시 개별 테이블 별 서로 다른 환경 설정을 적용할 수 없음</li>
</ul>

<h4 id="timestamp-mode">timestamp mode</h4>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">"mode"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"timestamp"</span><span class="err">,</span><span class="w">
</span><span class="nl">"timestamp.column.name"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"system_upd"</span><span class="w">
</span></code></pre></div></div>
<h4 id="timestamp--incrementing-mode">timestamp + incrementing mode</h4>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">"mode"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"timestamp + incrementing"</span><span class="err">,</span><span class="w">
</span><span class="nl">"timestamp.column.name"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"system_upd"</span><span class="err">,</span><span class="w">
</span><span class="nl">"incrementing.column.name"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"customer_id"</span><span class="err">,</span><span class="w">
</span></code></pre></div></div>

<p><br /></p>
<hr />

<h3 id="schema를-가지는-jdbc-source-connector-생성-메시지">Schema를 가지는 JDBC Source Connector 생성 메시지</h3>

<h4 id="생성-메시지-포맷">생성 메시지 포맷</h4>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"topic"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="err">...</span><span class="p">,</span><span class="w">
  </span><span class="nl">"partition"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="err">...</span><span class="p">,</span><span class="w">
  </span><span class="nl">"offset"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="err">...</span><span class="p">,</span><span class="w">
  </span><span class="nl">"tstype"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="err">...</span><span class="p">,</span><span class="w">
  </span><span class="nl">"ts"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="err">...</span><span class="p">,</span><span class="w">
  </span><span class="nl">"key"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
  </span><span class="nl">"payload"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="err">'</span><span class="p">{</span><span class="nl">"schema"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nl">"type"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="err">...</span><span class="w"> </span><span class="p">,</span><span class="w"> </span><span class="nl">"fields"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="err">...</span><span class="w"> </span><span class="p">,</span><span class="w">
                           </span><span class="nl">"optional"</span><span class="p">:</span><span class="w"> </span><span class="err">...</span><span class="w"> </span><span class="p">,</span><span class="w"> </span><span class="nl">"name"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="err">...</span><span class="p">},</span><span class="w">
                </span><span class="nl">"payload"</span><span class="w"> </span><span class="p">:</span><span class="w">  </span><span class="p">{</span><span class="err">...</span><span class="p">}}</span><span class="err">'</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<ul>
  <li>기본적으로 토픽에 저장되는 key 값은 자동 입력되지 않음 -&gt; SMT를 이용하여 key 값 설정해야 함</li>
  <li>컬럼 추가 / 삭제 / 타입 변경 등에 대응하기 위해 개별 레코드마다 자신의 schema (컬럼 타입, 컬럼 명 등)의 정보를 별도로 가지고 있으며 이를 메시지로 생성함</li>
  <li>schema 정보를 source connector에서 생성하지 않을 수 있지만 그럴 경우 sink connector에서 DB로 데이터 입력이 불가능</li>
  <li>하나의 메시지는 개별 레코드의 schema 정보와 실제 값 payload로 구성됨</li>
  <li>schema 정보로 인하여 개별 레코드에 대한 토픽 메시지 크기가 매우 커짐</li>
</ul>

<h4 id="topic-메시지-전송-시-schema-출력-끄기">Topic 메시지 전송 시 Schema 출력 끄기</h4>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">"key.converter.schemas.enable"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"false"</span><span class="err">,</span><span class="w">
</span><span class="nl">"value.converter.schemas.enable"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"false"</span><span class="w">
</span></code></pre></div></div>

<p><br /></p>
<hr />

<h3 id="jdbc-source-connector의-offset-메커니즘">JDBC Source Connector의 offset 메커니즘</h3>

<h4 id="incrementing">incrementing</h4>
<p><img src="https://imucoding.github.io/assets/images/incrementingoffset.png" alt="img.png" /></p>

<h4 id="timestamp">timestamp</h4>
<p><img src="https://imucoding.github.io/assets/images/timestampoffset.png" alt="img.png" /></p>

<h4 id="connect-offsets-topic-삭제로-reset-하기">connect-offsets topic 삭제로 reset 하기</h4>
<ol>
  <li>기존 connector 삭제 <br /></li>
  <li>connect 내리기 <br /></li>
  <li>source connector topic 삭제 <br /></li>
  <li>connect-offsets, connect-configs, connect-status topic 삭제 <br /></li>
  <li>connect 재가동 <br /></li>
</ol>

<p><br /></p>
<hr />

<h3 id="smt-single-message-transform">SMT (Single Message Transform)</h3>
<ul>
  <li>SMT를 이용하여 테이블의 PK를 key값으로 설정 가능</li>
  <li>ValueToKey는 value에서 메시지 key 값을 변환</li>
  <li>ExtractField는 메시지에서 특정 필드만 추출</li>
</ul>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"name"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"mysql_jdbc_om_source"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"config"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"connector.class"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"io.confluent.connect.jdbc.JdbcSourceConnector"</span><span class="p">,</span><span class="w">
    </span><span class="err">...</span><span class="w">
    </span><span class="nl">"transforms"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"create_key, extract_key"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"transforms.create_key.type"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"org.apache.kafka.connect.transforms.ValueToKey"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"transforms.create_key.fields"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"customer_id"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"transforms.extract_key.type"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"org.apache.kafka.connect.transforms.ExtractField$KEY"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"transforms.extract_key.field"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"customer_id"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">

</span></code></pre></div></div>

<h4 id="valuetokey">ValueToKey</h4>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="err">...</span><span class="w">
  </span><span class="nl">"key"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"schema"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"struct"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"fields"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="w">
          </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"int32"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"optional"</span><span class="p">:</span><span class="w"> </span><span class="s2">"false"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"field"</span><span class="p">:</span><span class="w"> </span><span class="s2">"customer_id"</span><span class="w">
        </span><span class="p">}</span><span class="w">
      </span><span class="p">],</span><span class="w">
      </span><span class="nl">"optional"</span><span class="p">:</span><span class="w"> </span><span class="s2">"false"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"payload"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"customer"</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"payload"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="err">...</span><span class="w">
  </span><span class="err">...</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<ul>
  <li>ValueToKey 적용 시 value에서 지정한 fields에 대해 struct type으로 key 추출함</li>
</ul>

<h4 id="valuetokey--extractfield">ValueToKey + ExtractField</h4>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="err">...</span><span class="w">
  </span><span class="nl">"key"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"schema"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"int32"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"optional"</span><span class="p">:</span><span class="w"> </span><span class="s2">"false"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"payload"</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"payload"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="err">...</span><span class="w">
  </span><span class="err">...</span><span class="w">
</span><span class="p">}</span><span class="w">

</span></code></pre></div></div>
<ul>
  <li>ExtractField 적용 시 key의 type이 단일 type이 되며 payload가 컬럼 값으로 적용됨</li>
</ul>

<h4 id="여러-개의-컬럼으로-구성된-pk를-key-값으로-설정">여러 개의 컬럼으로 구성된 PK를 Key 값으로 설정</h4>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"name"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"mysql_jdbc_om_source"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"config"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"connector.class"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"io.confluent.connect.jdbc.JdbcSourceConnector"</span><span class="p">,</span><span class="w">
    </span><span class="err">...</span><span class="w">
    </span><span class="nl">"transforms"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"create_key"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"transforms.create_key.type"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"org.apache.kafka.connect.transforms.ValueToKey"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"transforms.create_key.fields"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"customer_id, order_id"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"mode"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"timestamp"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"timestamp.column.name"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"system_upd"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<ul>
  <li>ValueToKey에 pk가 되는 컬럼명들을 fields로 적용</li>
  <li>ExtractField는 적용하지 말아야 함</li>
  <li>일반적으로 incrementing mode로 설정이 어려움. timestamp mode 설정 필요</li>
</ul>

<h4 id="topic-명-변경">topic 명 변경</h4>
<ul>
  <li>SMT에서 정규 표현식을 사용하여 topic 명 변경</li>
  <li>주요 속성
    <ul>
      <li><code class="language-plaintext highlighter-rouge">regex</code> : topic 명 매칭을 위한 정규 표현식</li>
      <li><code class="language-plaintext highlighter-rouge">replacement</code> : 매칭된 표현식에 대체될 문자열</li>
    </ul>
  </li>
</ul>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"name"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"mysql_jdbc_om_source"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"config"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"connector.class"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"io.confluent.connect.jdbc.JdbcSourceConnector"</span><span class="p">,</span><span class="w">
    </span><span class="err">...</span><span class="w">
    </span><span class="nl">"topic.prefix"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"mysql_om_smt_key_"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"table.whitelist"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"om.customers"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"transforms"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"create_key, extract_key, rename_topic"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"transforms.rename_topic.type"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"org.apache.kafka.connect.transforms.RegexRouter"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"transforms.rename_topic.regex"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"mysql_on_smt_key_(.*)"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"transforms.rename_topic.replacement"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"mysql_$1"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"transforms.create_key.type"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"org.apache.kafka.connect.transforms.ValueToKey"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"transforms.create_key.fields"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"customer_id"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"transforms.extract_key.type"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"org.apache.kafka.connect.transforms.ExtractField$KEY"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"transforms.extract_key.field"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"customer_id"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<ul>
  <li>정규 표현식
    <ul>
      <li><code class="language-plaintext highlighter-rouge">.</code> : 임의 단일 문자</li>
      <li><code class="language-plaintext highlighter-rouge">*</code> : 0번 이상 반복 가능</li>
      <li><code class="language-plaintext highlighter-rouge">.*</code>는 임의의 문자열을 의미하지만 <code class="language-plaintext highlighter-rouge">(.*)</code>으로 그룹핑하면 매칭 결과를 반환 가능. 이를 replacement의 <code class="language-plaintext highlighter-rouge">$1</code>에서 받아서 적용</li>
    </ul>
  </li>
</ul>

<p><br /></p>
<hr />

<h3 id="jdbc-sink-connector">JDBC Sink Connector</h3>
<ul>
  <li>카프카 토픽에서 메시지를 읽어들여서 타겟 DB로 데이터 입력 / 수정 / 삭제를 수행</li>
  <li>connect의 consumer가 주기적으로 카프카 토픽 메시지를 읽어서 타겟 DB로 데이터 연동</li>
  <li>RDBMS에서 데이터 추출은 JDBC source connector, CDC source connector 등을 사용하지만 RDBMS로 데이터 입력은 주로 JDBC sink connector를 사용</li>
</ul>

<p><img src="https://imucoding.github.io/assets/images/sink-connector.png" alt="img.png" /></p>

<p><br /></p>
<hr />

<h3 id="환경-구성">환경 구성</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>create database om_sink;
grant all privileges on om_sink.* to 'connect_dev'@'%' with grant option;
use om_sink;
create table customer_sink(
  customer_id int NOT NULL AUTO INCREMENT PRIMARY KEY,
  email varchar(255) NOT NULL,
  name varchar(255) NOT NULL,
  system_upd timestamp NOT NULL) ENGINE=InnoDB;
</code></pre></div></div>

<p><br /></p>
<hr />

<h3 id="jdbc-sink-connector-config">JDBC Sink Connector Config</h3>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"name"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"mysql_jdbc_sink"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"config"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"connector.class"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"io.confluent.connect.jdbc.JdbcSinkConnector"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"tasks.max"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"1"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"topics"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"mysql_jdbc_customers"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"connection.url"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"jdbc:mysql://localhost:3306/om_sink"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"connection.user"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"connect_dev"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"connection.password"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"connect_dev"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"insert.mode"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"upsert"</span><span class="p">,</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">upsert는</span><span class="w"> </span><span class="err">insert+update</span><span class="w">
    </span><span class="nl">"pk.mode"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"record_key"</span><span class="p">,</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">pk가</span><span class="w"> </span><span class="err">key에</span><span class="w"> </span><span class="err">있음.</span><span class="w"> </span><span class="err">record_value는</span><span class="w"> </span><span class="err">pk가</span><span class="w"> </span><span class="err">value에</span><span class="w"> </span><span class="err">있음</span><span class="w">
    </span><span class="nl">"pk.fields"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"customer_id"</span><span class="p">,</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">pk</span><span class="w"> </span><span class="err">컬럼명</span><span class="w"> </span><span class="err">(테이블의</span><span class="w"> </span><span class="err">컬럼)</span><span class="w">
    </span><span class="nl">"delete.enabled"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"true"</span><span class="p">,</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">value가</span><span class="w"> </span><span class="err">NULL인</span><span class="w"> </span><span class="err">경우</span><span class="w"> </span><span class="err">데이터</span><span class="w"> </span><span class="err">삭제</span><span class="w"> </span><span class="err">여부.</span><span class="w"> </span><span class="err">pk</span><span class="w"> </span><span class="err">mode는</span><span class="w"> </span><span class="err">record_key여야</span><span class="w"> </span><span class="err">함</span><span class="w">
    </span><span class="nl">"table.name.format"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"om_sink.customers_sink"</span><span class="p">,</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">입력</span><span class="w"> </span><span class="err">테이블</span><span class="w"> </span><span class="err">명</span><span class="p">,</span><span class="w"> </span><span class="err">없으면</span><span class="w"> </span><span class="err">topic</span><span class="w"> </span><span class="err">명으로</span><span class="w"> </span><span class="err">생성됨</span><span class="w">
    </span><span class="nl">"auto.create"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"true"</span><span class="p">,</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">테이블</span><span class="w"> </span><span class="err">자동</span><span class="w"> </span><span class="err">생성.</span><span class="w"> </span><span class="err">컬럼</span><span class="w"> </span><span class="err">타입이</span><span class="w"> </span><span class="err">멋대로</span><span class="w"> </span><span class="err">만들어지므로</span><span class="w"> </span><span class="err">비권장</span><span class="w">
    </span><span class="nl">"auto.evolve"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"true"</span><span class="p">,</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">소스</span><span class="w"> </span><span class="err">테이블</span><span class="w"> </span><span class="err">변경</span><span class="w"> </span><span class="err">사항</span><span class="w"> </span><span class="err">자동</span><span class="w"> </span><span class="err">반영</span><span class="w"> </span><span class="err">여부</span><span class="w">
    </span><span class="nl">"key.converter"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"org.apache.kafka.connect.json.JsonConverter"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"value.converter"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"org.apache.kafka.connect.json.JsonConverter"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<ul>
  <li>pk 컬럼이 여러 개인 경우 <code class="language-plaintext highlighter-rouge">pk.fields</code>에 여러 개 지정 가능</li>
</ul>

<h4 id="jdbc-sink-connector의-성능-향상-방법">JDBC Sink Connector의 성능 향상 방법</h4>
<ul>
  <li>topic의 partition 수를 늘리고 거기에 맞게 <code class="language-plaintext highlighter-rouge">task.max</code> 증가</li>
  <li>task.max 수는 topic partition 수와 cpu core 수에 맞게 설정</li>
  <li>task.max를 증가시켜도 target 테이블에 인덱스가 많이 있는 경우는 수행 성능이 증가하지 않음</li>
</ul>

<h4 id="source-테이블의-스키마-변경을-sink-테이블에서-자동-반영">Source 테이블의 스키마 변경을 Sink 테이블에서 자동 반영</h4>
<ul>
  <li><code class="language-plaintext highlighter-rouge">auto.evolve = true</code> 설정은 source 테이블의 스키마 변경을 JDBC sink connector가 자동으로 인지하여 sink 테이블에 반영</li>
  <li>하지만 스키마 변경 유형에 따라 제대로 반영이 되지 않는 경우가 있으므로 유의 필요</li>
</ul>

<p><br /></p>
<hr />

<h3 id="jdbc-sink-connector의-update-로직">JDBC Sink Connector의 Update 로직</h3>
<p><img src="https://imucoding.github.io/assets/images/update.png" alt="img.png" /></p>
<ul>
  <li>sink에서 update를 캡처하려면 timestamp도 함께 갱신 필요</li>
  <li>sink에서 update로 kafka 메시지 event 발생</li>
  <li>sink connector는 target 테이블의 해당 pk로 기존 레코드가 있으면 update 적용</li>
  <li>incrementing인 경우는 같은 key로 토픽에 데이터가 생성되지 않으므로 update 불가능</li>
</ul>

<h3 id="jdbc-sink-connector의-delete-로직">JDBC Sink Connector의 Delete 로직</h3>
<p><img src="https://imucoding.github.io/assets/images/delete.png" alt="img.png" /></p>
<ul>
  <li>debezium cdc source connector는 delete도 kafka에 이벤트로 발생시키며 key 값은 pk, value는 null 값으로 발생</li>
  <li>sink connector는 해당 pk로 기존 레코드가 있는 경우 delete 적용</li>
</ul>

<h3 id="upsert-기반의-jdbc-batch-처리">Upsert 기반의 JDBC Batch 처리</h3>
<ul>
  <li>connector 설정인 <code class="language-plaintext highlighter-rouge">insert.mode = upsert</code> 이면 insert 시와 update 시 모두 동일하게 upsertSQL이 수행됨
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>insert into 테이블 명 values (...) on duplicate key update ... ;
</code></pre></div>    </div>
  </li>
</ul>

<p><br /></p>
<hr />

<h3 id="jdbc-source-connector의-날짜--시간-데이터-변환">JDBC Source Connector의 날짜 / 시간 데이터 변환</h3>
<p><img src="https://imucoding.github.io/assets/images/date.png" alt="img.png" /></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>create table datetime_tab(
  id int NOT NULL PRIMARY KEY,
  order_date date NOT NULL,
  order_datetime datetime NOT NULL,
  order_timestamp timestamp NOT NULL,
  system_upd timestamp NOT NULL)
  ENGINE=InnoDB;
  
create table datetime_tab_sink(
  id int NOT NULL PRIMARY KEY,
  order_date date NOT NULL,
  order_datetime datetime NOT NULL,
  order_timestamp timestamp NOT NULL,
  system_upd timestamp NOT NULL)
  ENGINE=InnoDB;
</code></pre></div></div>
<ul>
  <li>정수형으로 전송된 시간 데이터를 JDBC sink connector가 알아서 포맷팅 수행 (yyyy-MM-dd HH:mm:ss)</li>
</ul>]]></content><author><name>Hyunjune</name></author><category term="kafka" /><category term="connector" /><category term="jdbc" /><category term="smt" /><summary type="html"><![CDATA[JDBC Source Connector JDBC source connector는 RDBMS 별 JDBC driver가 별도로 설치되어야 함 JDBC source connector는 JDBC driver를 이용하여 DB 접속 및 데이터 추출을 수행 후 producer를 이용하여 broker로 데이터를 전송함 JDBC source connector는 source 시스템에 주기적으로 query를 날려 변경된 데이터를 추출하는 방식]]></summary></entry><entry><title type="html">Kafka Connect 개요</title><link href="https://imucoding.github.io/kafka/2024/08/31/kafka-connect.html" rel="alternate" type="text/html" title="Kafka Connect 개요" /><published>2024-08-31T00:00:00+00:00</published><updated>2024-08-31T00:00:00+00:00</updated><id>https://imucoding.github.io/kafka/2024/08/31/kafka-connect</id><content type="html" xml:base="https://imucoding.github.io/kafka/2024/08/31/kafka-connect.html"><![CDATA[<h3 id="kafka-connect-개요">Kafka Connect 개요</h3>
<ul>
  <li>Kafka 메시지 시스템 (Broker/Producer/Consumer)를 기반으로 다양한 데이터 소스 시스템 (ex: RDBMS)에서 발생한 데이터 이벤트를 다른 데이터 타겟 시스템으로</li>
  <li>별도의 client 코딩 없이 seamless하게 실시간 전달하기 위해 만들어진 kafka component’</li>
</ul>

<h4 id="kafka-connect-활용">Kafka Connect 활용</h4>
<ul>
  <li>Data Pipeline</li>
  <li>DW ETL 활용</li>
  <li>Micro Service</li>
  <li>DB offloading</li>
</ul>

<h4 id="kafka-connect-주요-구성요소">Kafka Connect 주요 구성요소</h4>
<ul>
  <li>Connector
    <ul>
      <li>JDBC Source / Sink</li>
      <li>Debezium CDC Source</li>
      <li>Elasticsearch Sink</li>
      <li>File Connector</li>
      <li>MongoDB Source / Sink</li>
    </ul>
  </li>
  <li>Transformation
    <ul>
      <li>SMT</li>
    </ul>
  </li>
  <li>Converter
    <ul>
      <li>JsonConverter</li>
      <li>AvroConverter</li>
    </ul>
  </li>
</ul>

<p><img src="https://imucoding.github.io/assets/images/connector.png" alt="img.png" /></p>

<p><br /></p>
<hr />

<h3 id="connect-cluster-아키텍처">Connect Cluster 아키텍처</h3>
<p><img src="https://imucoding.github.io/assets/images/connect-cluster.png" alt="img.png" /></p>
<ul>
  <li>connect cluster는 동일 group id를 지정하는 것으로 클러스터를 구축하고 kafka cluster 내의 internal topic들을 통해 서로의 상태 정보를 공유함</li>
</ul>

<h4 id="connect-connector-worker-task-정의">Connect, Connector, Worker, Task 정의</h4>
<ul>
  <li>connect는 connector를 기동시키기 위한 framework를 갖춘 JVM process 모델</li>
  <li>connect process를 <code class="language-plaintext highlighter-rouge">worker process</code>로 지칭함</li>
  <li>connect는 서로 다른 여러 개의 connector instance를 자신의 framework 내부로 로딩하고 호출 및 수행 가능</li>
  <li>connector instance 실제 수행은 thread 레벨로 수행되며 이를 <code class="language-plaintext highlighter-rouge">task</code>라고 한다</li>
  <li>connector가 병렬 thread에서 수행이 가능한 경우 여러 개의 task thread들로 해당 connector 수행 가능</li>
  <li>connect는 connect cluster로 구성될 수 있으며, 1개의 노드에서 여러 개의 worker process 또는 여러 개의 노드에서 여러 개의 worker process 들로 connect cluster 구성 가능</li>
  <li>connect 유형은 <code class="language-plaintext highlighter-rouge">standalone</code>과 <code class="language-plaintext highlighter-rouge">distributed mode</code>로 나뉨
    <ul>
      <li>standalone : 단일 worker cluster만 가능</li>
      <li>distributed mode : 여러 worker로 가능</li>
    </ul>
  </li>
</ul>

<p><br /></p>
<hr />

<h3 id="connector-설치">Connector 설치</h3>
<ul>
  <li>connect 설정의 <code class="language-plaintext highlighter-rouge">plugin.path</code>에 connector 설치</li>
  <li>confluent-hub 사용</li>
</ul>

<h4 id="spool-source-connector">Spool Source Connector</h4>
<ul>
  <li>특정 디렉토리에 위치한 csv, json 포맷 등의 파일들을 event message로 만들어서 kafka로 전송하는 source connector</li>
  <li>해당 디렉토리를 주기적으로 모니터링 수행하면서 새로운 파일이 생성될 때마다 kafka로 메시지 전송</li>
</ul>

<h4 id="kafka-connect에-새로운-connector-생성-순서">Kafka Connect에 새로운 Connector 생성 순서</h4>
<ol>
  <li>Connector를 다운로드 받음. Connector는 여러 개의 jaf file로 구성 <br /></li>
  <li>Connector를 plugin.path로 지정된 디렉토리에 별도의 서브 디렉토리를 만들어 jar file을 이동시켜야함 <br /></li>
  <li>Connect는 기동 시에 plugin.path로 지정된 디렉토리 밑의 서브 디렉토리들에 위치한 모든 jar 파일을 로딩함. 신규로 connector를 connect로 올릴 시에는 반드시 connect를 재기동해야 반영됨 <br /></li>
  <li>Connect는 Connector 명, Connector 클래스 명, Connector 고유 환경 설정 등을 REST API를 통해 Connect에 전달하여 새롭게 생성</li>
  <li>REST API 성공 response와 별개로 connect log 메시지를 반드시 확인해야 함</li>
</ol>

<h4 id="connector-로딩-확인">Connector 로딩 확인</h4>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl -X GET -H "Content-Type : application/json" http://localhost:8083/connector-plugins
</code></pre></div></div>

<h4 id="spooldir_sourcejson">spooldir_source.json</h4>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">{</span>
  <span class="s2">"</span><span class="s">name"</span> <span class="pi">:</span> <span class="s2">"</span><span class="s">csv_spooldir_source"</span><span class="pi">,</span>
  <span class="s2">"</span><span class="s">config"</span> <span class="pi">:</span> <span class="pi">{</span>
    <span class="s2">"</span><span class="s">tasks.max"</span> <span class="pi">:</span> <span class="s2">"</span><span class="s">3"</span><span class="pi">,</span>
    <span class="s2">"</span><span class="s">connector.class"</span> <span class="pi">:</span> <span class="s2">"</span><span class="s">com.github.jcustenborder.kafka.connect.spooldir.SpoolDirCsvSourceConnector"</span><span class="pi">,</span>
    <span class="s2">"</span><span class="s">input.path"</span> <span class="pi">:</span> <span class="s2">"</span><span class="s">/home/ubuntu/spool-test-dir"</span><span class="pi">,</span>
    <span class="s2">"</span><span class="s">input.file.pattern"</span> <span class="pi">:</span> <span class="s2">"</span><span class="s">/</span><span class="err">\</span><span class="s">.*</span><span class="se">\\</span><span class="s">.csv"</span><span class="pi">,</span>
    <span class="s2">"</span><span class="s">error.path"</span> <span class="pi">:</span> <span class="s2">"</span><span class="s">/home/ubuntu/spooldir-test-dir/error"</span><span class="pi">,</span>
    <span class="s2">"</span><span class="s">finished.path"</span> <span class="pi">:</span> <span class="s2">"</span><span class="s">/home/ubuntu/spooldir-test-dir/finished"</span><span class="pi">,</span>
    <span class="s2">"</span><span class="s">empty.poll.wait.ms"</span> <span class="pi">:</span> <span class="nv">30000</span><span class="pi">,</span> <span class="c1"># 모니터링 주기</span>
    <span class="s2">"</span><span class="s">halt.on.error"</span> <span class="pi">:</span> <span class="s2">"</span><span class="s">false"</span><span class="pi">,</span> <span class="c1"># 파싱 오류 시 중지 여부</span>
    <span class="s2">"</span><span class="s">topic"</span> <span class="pi">:</span> <span class="s2">"</span><span class="s">spooldir-topic"</span><span class="pi">,</span>
    <span class="s2">"</span><span class="s">csv.first.row.as.header"</span> <span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span><span class="pi">,</span>
    <span class="s2">"</span><span class="s">schema.generation.enabled"</span> <span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>
  <span class="pi">}</span>
<span class="pi">}</span>
</code></pre></div></div>

<h4 id="spool-dir-source-connector-생성">Spool Dir Source Connector 생성</h4>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl -X POST -H "Content-Type : application/json" http://localhost:8083/connectors --data @spooldir_source.json
</code></pre></div></div>

<h4 id="connector-status-확인">Connector Status 확인</h4>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl -X GET http://localhost:8083/connectors
curl -X GET http://localhost:8083/connectors/csv_spooldir_source/status | jq '.'
</code></pre></div></div>
<h4 id="connector-수행-thread-및-task-thread-모니터링">Connector 수행 Thread 및 Task Thread 모니터링</h4>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ps -ef | grep java
jstack pid
</code></pre></div></div>

<h4 id="source-connect에서-connector-등록-시-수행-프로세스">Source Connect에서 Connector 등록 시 수행 프로세스</h4>
<p><img src="https://imucoding.github.io/assets/images/spool.png" alt="img.png" /></p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">tasks.max</code> 개수만큼 thread 생성</li>
  <li>connector 별 멀티 thread 지원 여부 다름</li>
</ul>

<p><br /></p>
<hr />

<h3 id="rest-api-기반의-connect-관리">REST API 기반의 Connect 관리</h3>
<ul>
  <li>GET : 기동 중인 모든 connector 들의 리스트, 개별 connector의 config와 현재 상태</li>
  <li>POST : connector 생성 등록, 개별 connector 재기동</li>
  <li>PUT : connector 일시 정지 및 재시작, connector의 새로운 config 등록, config validation</li>
  <li>DELETE : connector 삭제</li>
</ul>

<h4 id="httpie-이용-rest-api-호출">httpie 이용 REST API 호출</h4>
<ul>
  <li>httpie 설치
    <ul>
      <li><code class="language-plaintext highlighter-rouge">sudo apt-get install httpie</code></li>
    </ul>
  </li>
  <li>connector 리스트 확인
    <ul>
      <li><code class="language-plaintext highlighter-rouge">http GET http://localhost:8083/connectors</code></li>
    </ul>
  </li>
  <li>connector 등록하기
    <ul>
      <li><code class="language-plaintext highlighter-rouge">http POST http://localhost:8083/connectors @spooldir_source.json</code></li>
    </ul>
  </li>
  <li>connector 삭제
    <ul>
      <li><code class="language-plaintext highlighter-rouge">http DELETE http://localhost:8083/connectors/csv_spooldir_source</code></li>
    </ul>
  </li>
  <li>connector 일시 중지
    <ul>
      <li><code class="language-plaintext highlighter-rouge">http PUT http://localhost:8083/connectors/csv_spooldir_source/pause</code></li>
    </ul>
  </li>
  <li>connector 재시작
    <ul>
      <li><code class="language-plaintext highlighter-rouge">http PUT http://localhost:8083/connectors/csv_spooldir_source/resume</code></li>
    </ul>
  </li>
</ul>

<h4 id="전송-데이터의-스키마-필요성">전송 데이터의 스키마 필요성</h4>
<ul>
  <li>csv 문자열을 콤마 기준으로 파싱한 후, DB insert SQL을 만들기 위해서 파싱한 값을 타입에 따라 변환 해야 함</li>
  <li>consumer 입장에서 타입을 알 수 있는 방법은?</li>
  <li>문자열 자체를 Serialization 해서 전송하지 않고 custom 객체를 생성해서 이를 직렬화해서 전송</li>
</ul>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">OrderModel</span> <span class="kd">implements</span> <span class="nc">Serializable</span><span class="o">{</span>
    <span class="kd">public</span> <span class="nc">String</span> <span class="n">orderId</span><span class="o">;</span>
    <span class="kd">public</span> <span class="nc">String</span> <span class="n">shopId</span><span class="o">;</span>
            <span class="o">...</span>
    <span class="kd">public</span> <span class="nc">LocalDateTime</span> <span class="n">orderTime</span><span class="o">;</span>
<span class="o">}</span>

</code></pre></div></div>
<ul>
  <li>custom 자바 객체를 이용하여 schema가 있는 정보 전송 시 문제
    <ul>
      <li>custom 자바 객체를 customer에서 타겟 DB에 맞춰 매번 생성하기 어려움</li>
    </ul>
  </li>
  <li>connector에서 schema 설정 시 파싱 후 객체 생성 필요 없이 schema 정보 포함 직렬화</li>
</ul>

<p><img src="https://imucoding.github.io/assets/images/converter.png" alt="img.png" /></p>

<h4 id="converter-지원-포맷">Converter 지원 포맷</h4>
<ul>
  <li>Json, Avro, Protobuf, String, ByteArray
    <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">{</span>
<span class="s2">"</span><span class="s">schema"</span> <span class="pi">:</span> <span class="pi">{</span><span class="s2">"</span><span class="s">type"</span> <span class="pi">:</span> <span class="s2">"</span><span class="s">struct"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">fields"</span> <span class="pi">:</span> <span class="pi">[{</span><span class="nv">..</span><span class="pi">},{</span><span class="nv">..</span><span class="pi">}],</span>
            <span class="s2">"</span><span class="s">optional"</span> <span class="pi">:</span> <span class="nv">false</span><span class="pi">,</span> <span class="s2">"</span><span class="s">name"</span> <span class="pi">:</span> <span class="nv">...</span><span class="pi">},</span>
<span class="s2">"</span><span class="s">payload"</span> <span class="pi">:</span> <span class="pi">{</span> <span class="nv">...</span> <span class="pi">}</span>
<span class="pi">}</span>
</code></pre></div>    </div>
  </li>
</ul>

<h4 id="converter의-json-schema">Converter의 Json Schema</h4>
<ul>
  <li>connect의 기본 설정은 json schema 설정</li>
  <li>converter 설정은 key, value 각각 해주어야 함</li>
  <li>(key or value).converter.schemas.enable = true 이면 schema 정보를 json 형태로 embedding하여 생성</li>
</ul>

<h4 id="schema-registry를-이용한-schema-정보-중복-전송-제거">Schema Registry를 이용한 Schema 정보 중복 전송 제거</h4>
<ul>
  <li>confluent kafka는 schema registry를 통해 schema 정보 별도로 관리하는 기능 제공</li>
  <li>토픽으로 전송되는 data의 schema는 schema registry에서 ID + version 별로 중앙 관리되므로 레코드 별로 schema를 중복해서 전송할 필요 없음</li>
</ul>

<p><img src="https://imucoding.github.io/assets/images/registry.png" alt="img.png" /></p>

<p><br /></p>
<hr />

<h3 id="connect-내부-정보-topic-들">Connect 내부 정보 Topic 들</h3>
<ul>
  <li>connector의 메시지 처리 offset 및 connector 별 config와 상태 정보를 내부 토픽에 저장</li>
  <li>connect-offsets
    <ul>
      <li>source connector 별로 메시지를 전송한 offset 정보 저장</li>
      <li>한 번 전송한 메시지를 중복 전송 하지 않기 위함</li>
      <li>기본 25개의 partition으로 구성</li>
    </ul>
  </li>
  <li>connect-configs
    <ul>
      <li>connector의 config 정보를 가짐</li>
      <li>connect 재기동 시 설정된 connector를 기동</li>
    </ul>
  </li>
  <li>connect-status
    <ul>
      <li>connector의 상태 정보를 가짐</li>
      <li>connect 재기동 시 설정된 connector를 기동</li>
    </ul>
  </li>
  <li>__consumer_offsets
    <ul>
      <li>consumer가 읽어들인 메시지의 offset 정보를 가짐</li>
      <li>한 번 읽어들인 메시지를 중복해서 읽지 않기 위함</li>
      <li>기본 50개의 partition으로 구성</li>
    </ul>
  </li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kafka-console-consumer --bootstrap-server localhost:9092 --topic connect-offsets --from-beginning --property print.key=true | jq '.'
</code></pre></div></div>

<p><br /></p>
<hr />

<h3 id="kafkacat--kcat-개요">kafkacat / kcat 개요</h3>
<ul>
  <li><code class="language-plaintext highlighter-rouge">sudo apt-get install kafkacat</code></li>
  <li>하나의 command로 producer, consumer 모두 사용</li>
  <li>키 값을 보다 쉽게 출력</li>
  <li>토픽 메시지의 포맷팅을 보다 다양하게 출력</li>
  <li>메시지 헤더 정보 볼 수 있음</li>
  <li>내부 offsets 값을 reset 하는데 주로 사용</li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kafkacat -b localhost:9092 -t 토픽명 -C -k# 
kafkacat -b localhost:9092 -t 토픽명 -C -J -u -q | jq '.'
kafkacat -b localhost:9092 -L
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">-C</code> : consumer</li>
  <li><code class="language-plaintext highlighter-rouge">-J</code> : Json</li>
  <li><code class="language-plaintext highlighter-rouge">-u</code> : unbuffered</li>
  <li><code class="language-plaintext highlighter-rouge">-q</code> : verbosity 제거</li>
  <li><code class="language-plaintext highlighter-rouge">-L</code> : 전체 토픽 메타 정보 확인</li>
</ul>

<h3 id="-내부-offsets-rest-과정">※ 내부 offsets rest 과정</h3>

<h4 id="1-connect-offsets-토픽에서-connect-별-key-partition-확인">1. connect-offsets 토픽에서 connect 별 key, partition 확인</h4>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kafkacat -b localhost:9092 -t connect-offsets -C -f `Key : %k, Value : %s, Partition : %p, Offset : %o '
</code></pre></div></div>
<h4 id="2-최종-offset-메시지가-있는-partition을-찾아서-null-적용">2. 최종 offset 메시지가 있는 partition을 찾아서 NULL 적용</h4>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>echo `["csv_spooldir_source", {"filename" : "spool..csv"}]#' | kafkacat -b loaclhost:9092 -t
connect-offsets -P -Z -k# -p 17
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">-P</code> : producer</li>
  <li><code class="language-plaintext highlighter-rouge">-Z</code> : null 가능</li>
  <li><code class="language-plaintext highlighter-rouge">-p</code> : partition</li>
  <li><code class="language-plaintext highlighter-rouge">-k#</code> : key 포함 (separator #)</li>
  <li>echo로 ‘#’ separator 포함해서 파이프를 통해 producer로 전송, value는 NULL</li>
</ul>]]></content><author><name>Hyunjune</name></author><category term="kafka" /><category term="connect" /><category term="spooldir source connector" /><category term="schema registry" /><category term="kafkacat" /><category term="internal topics" /><summary type="html"><![CDATA[Kafka Connect 개요 Kafka 메시지 시스템 (Broker/Producer/Consumer)를 기반으로 다양한 데이터 소스 시스템 (ex: RDBMS)에서 발생한 데이터 이벤트를 다른 데이터 타겟 시스템으로 별도의 client 코딩 없이 seamless하게 실시간 전달하기 위해 만들어진 kafka component’]]></summary></entry><entry><title type="html">Kafka Cluster</title><link href="https://imucoding.github.io/kafka/2024/08/25/kafka-cluster.html" rel="alternate" type="text/html" title="Kafka Cluster" /><published>2024-08-25T00:00:00+00:00</published><updated>2024-08-25T00:00:00+00:00</updated><id>https://imucoding.github.io/kafka/2024/08/25/kafka-cluster</id><content type="html" xml:base="https://imucoding.github.io/kafka/2024/08/25/kafka-cluster.html"><![CDATA[<h3 id="multi-broker-cluster-구축">Multi Broker Cluster 구축</h3>
<p><img src="https://imucoding.github.io/assets/images/cluster.png" alt="img.png" /></p>

<ul>
  <li>각 인스턴스 모두 동일 VPC에 속해야 함</li>
  <li>보안 그룹 설정
    <ul>
      <li>포트 범위 (0-65535) 소스 (172.31.0.0/16)</li>
      <li>포트 범위 (0-65535) 소스 (허가 IP 지정)</li>
    </ul>
  </li>
  <li>local에서 admin 접근은 public IP로 접근</li>
  <li>DNS 설정
    <ul>
      <li>각 노드의 <code class="language-plaintext highlighter-rouge">/etc/hosts</code> 파일 수정
        <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>172.31.3.209 client.foo.bar client
172.31.0.186 kafka1.foo.bar kafka1
172.31.12.195 kafka2.foo.bar kafka2
172.31.3.173 kafka3.foo.bar kafka3
172.31.5.59 zookeeper1.foo.bar zookeeper1
172.31.11.46 zookeeper2.foo.bar zookeeper2
172.31.8.78 zookeeper2.foo.bar zookeeper2
</code></pre></div>        </div>
      </li>
    </ul>
  </li>
  <li>ping으로 확인
    <ul>
      <li><code class="language-plaintext highlighter-rouge">ping -c 2 kafka1.foo.bar</code></li>
    </ul>
  </li>
</ul>

<hr />

<h3 id="zookeeper-설정-파일-수정">Zookeeper 설정 파일 수정</h3>
<ul>
  <li>모든 주키퍼 노드의 <code class="language-plaintext highlighter-rouge">zookeeper.properties</code> 파일에 아래 추가
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>server.1 = zookeeper1.foo.bar:2888:3888
server.2 = zookeeper2.foo.bar:2888:3888
server.3 = zookeeper3.foo.bar:2888:3888
tickTime = 3000
initLimit = 10 
syncLimit = 10
</code></pre></div>    </div>
  </li>
  <li><code class="language-plaintext highlighter-rouge">tickTime</code> : 주키퍼 시간 단위 (ms), <code class="language-plaintext highlighter-rouge">initLimit</code> : 주키퍼에 연결하는 시간, <code class="language-plaintext highlighter-rouge">syncLimit</code> : 주키퍼 동기화 하는시간</li>
  <li>zookeeper.properties 설정 중 <code class="language-plaintext highlighter-rouge">dataDir</code>에 적힌 경로에 myid라는 파일 생성</li>
  <li>각 노드에서 노드 1은 1, 노드 2는 2, 노드 3은 3 저장</li>
</ul>

<hr />

<h3 id="kafka-설정-파일-수정">Kafka 설정 파일 수정</h3>

<ul>
  <li>모든 노드에서 <code class="language-plaintext highlighter-rouge">server.properties</code> 각각 수정
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>broker.id = 1
listeners = PLAINTEXT://:9092
advertiesd.listeners = PLAINTEXT://kafka1.foo.bar:9092
zookeeper.connect = zookeeper1.foo.bar:2181,zookeeper2.foo.bar:2181,zookeeper3.foo.bar:2181
</code></pre></div>    </div>
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>broker.id = 2
listeners = PLAINTEXT://:9092
advertiesd.listeners = PLAINTEXT://kafka2.foo.bar:9092
zookeeper.connect = zookeeper1.foo.bar:2181,zookeeper2.foo.bar:2181,zookeeper3.foo.bar:2181
</code></pre></div>    </div>
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>broker.id = 3
listeners = PLAINTEXT://:9092
advertiesd.listeners = PLAINTEXT://kafka3.foo.bar:9092
zookeeper.connect = zookeeper1.foo.bar:2181,zookeeper2.foo.bar:2181,zookeeper3.foo.bar:2181
</code></pre></div>    </div>
  </li>
  <li>kafka-ui를 docker로 실행 시 docker는 /etc/hosts를 host에서 읽어가지 않으므로 <code class="language-plaintext highlighter-rouge">--add-host</code> 옵션으로 붙여줘야 함</li>
</ul>]]></content><author><name>Hyunjune</name></author><category term="kafka" /><category term="cluster" /><category term="aws" /><summary type="html"><![CDATA[Multi Broker Cluster 구축]]></summary></entry></feed>