<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>코딩수집</title>
    <link>https://westlife0615.tistory.com/</link>
    <description>와주셔서 감사합니다. 좋은 글을 많이 쓰겠습니다.</description>
    <language>ko</language>
    <pubDate>Thu, 7 May 2026 02:11:22 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>코딩수집가</managingEditor>
    <image>
      <title>코딩수집</title>
      <url>https://tistory1.daumcdn.net/tistory/3192971/attach/c361da78164a4984996d5a7696b4c566</url>
      <link>https://westlife0615.tistory.com</link>
    </image>
    <item>
      <title>[Kubernetes] Validating Admission Webhook 알아보기</title>
      <link>https://westlife0615.tistory.com/1186</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes에서 리소스 생성이나 변경 요청은 API Server를 통해 처리됩니다. 예를 들어 사용자가 kubectl create, kubectl apply, kubectl replace 와 같은 명령을 실행하면, 해당 요청은 모두 API Server로 전달됩니다. 이 과정에서 기본적인 스키마 검증이나 권한 검사는 자동으로 수행되지만, &lt;b&gt;도메인 규칙까지 보장해 주지는 않습니다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 요구사항은 Kubernetes 기본 기능만으로는 표현하기 어렵습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 필드 조합이 함께 존재하면 안 되는 경우&lt;/li&gt;
&lt;li&gt;리소스 간 관계를 고려한 제약 조건&lt;/li&gt;
&lt;li&gt;운영 정책에 따른 생성&amp;middot;수정 제한&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제를 해결하기 위해 Kubernetes는 &lt;b&gt;Admission Webhook&lt;/b&gt;이라는 확장 지점을 제공하며, 그중 하나가 &lt;b&gt;Validating Admission Webhook&lt;/b&gt;입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Validating Admission Webhook 이란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Validating Admission Webhook은 Kubernetes API Server가 리소스를 &lt;b&gt;etcd에 저장하기 직전&lt;/b&gt;, 해당 요청이 유효한지 외부 로직으로 검증할 수 있도록 해주는 메커니즘입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리소스 생성(Create), 수정(Update) 요청이 들어오면 API Server는 Validating Admission Webhook을 호출하고, Webhook의 응답에 따라 요청을 **허용(Allow)**하거나 **거부(Deny)**합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 Validating Admission Webhook이 &lt;b&gt;리소스를 변경하지 않고&lt;/b&gt;, 오직 검증만 수행한다는 점입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Admission 과정에서의 위치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes의 Admission 단계는 크게 다음 순서로 동작합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;Authorization&lt;/li&gt;
&lt;li&gt;Mutating Admission&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Validating Admission&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;etcd 저장&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Validating Admission Webhook은 이 중 &lt;b&gt;Validating Admission 단계&lt;/b&gt;에서 호출되며, 이 시점에서는 리소스의 최종 형태가 이미 확정된 상태입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Validating Admission Webhook이 필요한 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes API Server는 &lt;b&gt;Built-in Resource&lt;/b&gt;에 대해서는 비교적 상세한 규칙을 알고 있습니다. 예를 들어 Deployment, Service와 같은 리소스는 스키마뿐 아니라 내부 동작 방식과 제약 조건이 API Server 및 Controller에 이미 정의되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 **Custom Resource(CR)**의 경우, API Server는 해당 리소스의 도메인 의미나 상세한 운영 규칙을 알지 못합니다. API Server가 이해하는 것은 CRD에 정의된 OpenAPI 스키마 수준에 한정되며, 그 이상의 규칙은 알 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로 인해 CRD를 사용하는 환경에서는 다음과 같은 한계가 자연스럽게 발생합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;필드 간 상호 의존성 검증&lt;/li&gt;
&lt;li&gt;다른 리소스 상태를 고려한 검증&lt;/li&gt;
&lt;li&gt;조직 또는 서비스별 운영 정책 반영&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Validating Admission Webhook은 이러한 &lt;b&gt;API Server가 알 수 없는 Custom Resource의 도메인 규칙&lt;/b&gt;을 코드로 구현할 수 있게 해주는 메커니즘입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Validating Admission Webhook의 동작 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Validating Admission Webhook은 다음과 같은 흐름으로 동작합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 Kubernetes API로 리소스 생성 또는 수정 요청&lt;/li&gt;
&lt;li&gt;API Server가 Admission 단계에서 Validating Admission Webhook 호출&lt;/li&gt;
&lt;li&gt;Webhook 서버가 요청 내용을 검증&lt;/li&gt;
&lt;li&gt;Webhook 응답에 따라 요청 허용 또는 거부&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Webhook 응답에는 허용 여부와 함께, 거부 시 사용자에게 전달할 메시지를 포함할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ValidatingWebhookConfiguration&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Validating Admission Webhook은 ValidatingWebhookConfiguration 리소스를 통해 등록됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 리소스에는 다음과 같은 정보가 포함됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 리소스에 대해 Webhook을 호출할지&lt;/li&gt;
&lt;li&gt;어떤 이벤트(Create/Update/Delete)에 반응할지&lt;/li&gt;
&lt;li&gt;Webhook 서버의 위치(Service 또는 URL)&lt;/li&gt;
&lt;li&gt;실패 시 동작 정책(failurePolicy)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;failurePolicy의 의미&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Validating Admission Webhook 호출이 실패했을 때의 동작은 매우 중요합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Fail : Webhook 호출 실패 시 요청 거부&lt;/li&gt;
&lt;li&gt;Ignore : Webhook 호출 실패 시 요청 허용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 정책에 따라 안정성과 안전성 사이의 트레이드오프를 고려해 선택해야 합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Validating Admission Webhook의 특성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Validating Admission Webhook은 다음과 같은 특성을 가집니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청 경로의 &lt;b&gt;동기적 처리&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;API Server의 응답 지연에 직접적인 영향&lt;/li&gt;
&lt;li&gt;Webhook 서버의 가용성이 클러스터 동작에 영향&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Validating Admission Webhook은 반드시 빠르고 안정적으로 동작해야 합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Validating Admission Webhook은 Kubernetes 리소스의 생명주기 초입에서 &lt;b&gt;잘못된 요청을 차단하기 위한 마지막 방어선&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스키마 검증을 넘어서는 도메인 규칙을 강제하고 싶다면, Validating Admission Webhook은 매우 강력한 도구가 될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 Mutating Webhook과의 차이점, 그리고 실제 구현 시 주의해야 할 점들을 더 자세히 살펴보겠습니다.&lt;/p&gt;</description>
      <category>Kubernetes</category>
      <category>Admission Webhook</category>
      <category>CR</category>
      <category>crd</category>
      <category>Custom Resource</category>
      <category>k8s</category>
      <category>kubernetes</category>
      <category>Mutating Admission Webhook</category>
      <category>Validating Admission Webhook</category>
      <category>webhook</category>
      <category>쿠버네티스</category>
      <author>코딩수집가</author>
      <guid isPermaLink="true">https://westlife0615.tistory.com/1186</guid>
      <comments>https://westlife0615.tistory.com/1186#entry1186comment</comments>
      <pubDate>Mon, 17 Feb 2025 20:35:48 +0900</pubDate>
    </item>
    <item>
      <title>[Kubernetes] Operator Pattern 과 Lease 알아보기</title>
      <link>https://westlife0615.tistory.com/1185</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes의 &lt;b&gt;Lease&lt;/b&gt;는 Operator Pattern에서 &lt;b&gt;여러 Controller 중 하나의 Active Controller를 선출하기 위해 사용되는 리소스&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Operator나 Controller는 보통 고가용성을 위해 여러 replica로 실행되지만, 실제로 중요한 판단과 상태 변경은 &lt;b&gt;단 하나의 Controller만 수행&lt;/b&gt;해야 합니다. 이때 어떤 Controller가 그 역할을 맡을지를 결정하는 장치가 바로 Lease입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Operator Pattern 에서 K8s&amp;nbsp;&lt;b&gt;Lease 가 왜 필요해졌는지&lt;/b&gt;, 그리고 &lt;b&gt;Kubernetes 에서 Lease가 어떻게 Active Controller를 보장하는지&lt;/b&gt;에 대해서 알아보도록 하겠습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Operator Pattern 이란 ?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Operator Pattern은 &lt;b&gt;Custom Resource에 대한 처리를 자동화하기 위한 패턴&lt;/b&gt;을 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Operator는 Reconcile Loop를 실행하는 Controller를 기반으로 동작하며, 사용자가 생성한 Custom Resource를 지속적으로 관찰하면서 해당 리소스에 필요한 실제 Kubernetes 리소스의 생성과 관리를 수행합니다. 즉, 하나의 Controller가 Custom Resource의 상태를 반복적으로 확인하고, 선언된 상태와 실제 상태를 일치시키는 역할을 담당합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 고가용성(HA)을 위해 여러 Controller가 동시에 실행될 수 있습니다. 다만, 동일한 Custom Resource를 여러 Controller가 동시에 처리하면 상태 충돌이 발생할 수 있기 때문에, &lt;b&gt;하나의 Active Controller와 여러 Standby Controller 구조&lt;/b&gt;를 유지해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 특정 Custom Resource가 생성되면 Controller는 해당 CR의 정의에 따라 필요한 Pod를 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Spark Operator의 경우, 사용자가 Spark Application이나 Spark Job을 생성하면 Controller가 이를 감지하고, 해당 애플리케이션을 실행하기 위한 Driver Pod와 Executor Pod들을 적절한 설정으로 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 흐름을 안정적으로 유지하기 위해, Kubernetes에서는 단 하나의 Active Controller만이 이 역할을 수행하도록 &lt;b&gt;Lease 리소스&lt;/b&gt;를 사용합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Lease란 무엇인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes에서 &lt;b&gt;Lease&lt;/b&gt;는 특정 역할을 수행할 주체를 하나로 제한하기 위해 사용되는 &lt;b&gt;경량 Coordination 리소스&lt;/b&gt;입니다. 주로 여러 Controller 또는 Operator가 동시에 실행되는 환경에서, 그중 &lt;b&gt;단 하나의 Active Controller를 식별하고 유지하기 위한 목적&lt;/b&gt;으로 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념적으로 Lease는 ZooKeeper나 Etcd와 같은 &lt;b&gt;Coordination Service가 제공하는 Lease 또는 Lock 메커니즘과 매우 유사한 역할&lt;/b&gt;을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ZooKeeper를 예로 들면, 특정 Ephemeral ZNode는 하나의 client와 연결되어 소유권을 가지며, 해당 client는 주기적으로 heartbeat를 전송함으로써 이 소유권을 유지합니다. 만약 heartbeat가 중단되거나 connection이 종료되면, 해당 Ephemeral ZNode는 자동으로 제거되고 소유권은 해제됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시점에서 ZooKeeper의 Watch 메커니즘을 통해 대기 중이던 다른 client들은 소유권 해제를 감지하고, 새로운 소유권을 획득하기 위한 경쟁을 시작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes Lease 역시 동일한 개념 위에 설계되어 있습니다. Lease를 보유한 Controller는 주기적으로 갱신을 수행하며 자신의 소유권을 유지하고, 이 갱신이 중단되면 Lease는 더 이상 유효하지 않은 것으로 간주됩니다. Standby Controller들은 Lease 상태 변화를 감지하고, 새로운 Active Controller가 되기 위한 경쟁을 수행하게 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Lease의 핵심 필드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lease는 다음과 같은 필드를 통해 Active Controller를 표현합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lease는 다음과 같은 필드를 통해 Active Controller를 표현합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;holderIdentity&lt;br /&gt;현재 Lease를 보유하고 있는 주체를 식별합니다. 일반적으로 Controller Pod의 이름이나 고유 식별자가 사용됩니다.&lt;/li&gt;
&lt;li&gt;leaseDurationSeconds&lt;br /&gt;Lease가 유효하다고 간주되는 최대 시간입니다. 이 시간 동안 Lease가 갱신되지 않으면, 해당 Lease는 만료된 것으로 판단됩니다.&lt;/li&gt;
&lt;li&gt;renewTime&lt;br /&gt;Lease가 마지막으로 갱신된 시점을 나타냅니다. Active Controller는 주기적으로 이 값을 갱신함으로써 자신이 여전히 정상 동작 중임을 알립니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 세 필드의 조합을 통해 Kubernetes는 &lt;b&gt;현재 누가 Active Controller인지&lt;/b&gt;, 그리고 &lt;b&gt;해당 Controller가 여전히 유효한 상태인지&lt;/b&gt;를 판단할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Kubernetes에서 Lease가 어떤 문제를 해결하기 위해 등장했는지, 그리고 Leader Election 과정에서 어떤 역할을 수행하는지 살펴봤습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Lease는 다중 Controller 환경에서 논리적 단일성을 보장하기 위한 장치&lt;/li&gt;
&lt;li&gt;주기적인 갱신 구조를 통해 장애 감지를 단순화&lt;/li&gt;
&lt;li&gt;복잡한 합의 알고리즘 없이 안정적인 리더 선출 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;script&gt;

    setTimeout(function() {
      var elementList = document.querySelectorAll(&quot;h2&quot;);
      var indexer = null;
      for (var i = 0; i &lt; elementList.length; i++) {
          if (elementList[i].innerText.indexOf(&quot;- 목차&quot;) &gt; -1) {
              indexer = elementList[i]    
          }
          
      }
      var headerList = document.querySelectorAll(&quot;h2,h3,h4&quot;);
	  var allHeaderList = [...headerList]
      var indexBox = document.createElement(&quot;ul&quot;);
      for (var i = 0; i &lt; allHeaderList.length; i++) {
        var header = allHeaderList[i]; 
        var headerText = header.innerText
        if (headerText != null &amp;&amp; (headerText.indexOf(&quot;*&quot;) &gt; -1 || headerText.indexOf(&quot;.&quot;) &gt; -1 || headerText.indexOf(&quot;;&quot;) &gt; -1 || headerText.indexOf(&quot;?&quot;) &gt; -1 || headerText.indexOf(&quot;!&quot;) &gt; -1)) {
          header.setAttribute(&quot;id&quot;, i);
          
          var indexItem = document.createElement(&quot;li&quot;)
          var indexLink = document.createElement(&quot;a&quot;)
          indexLink.setAttribute(&quot;style&quot;, &quot;color: black; line-height: 2;&quot;);
          indexLink.setAttribute(&quot;href&quot;, &quot;#&quot; + i);

          indexItem.appendChild(indexLink);
          indexLink.innerText = headerText;
		  if (header.tagName == 'H2') {
			indexItem.setAttribute(&quot;style&quot;, &quot;margin-left: 12px; list-style: circle;&quot;);
		  }
		  if (header.tagName == 'H3') {
			indexItem.setAttribute(&quot;style&quot;, &quot;margin-left: 24px; list-style: disc;&quot;);
		  }          
		  if (header.tagName == 'H4') {
			indexItem.setAttribute(&quot;style&quot;, &quot;margin-left: 36px; list-style: disclosure-closed;&quot;);
		  }                    
          indexBox.appendChild(indexItem)

        }

      }

      indexer.parentElement.insertBefore(indexBox, indexer);
      indexer.parentElement.removeChild(indexer)
      indexBox.parentElement.insertBefore(indexer, indexBox)


      var progressBar = document.createElement(&quot;div&quot;)
	  progressBar.setAttribute(&quot;class&quot;, &quot;progress&quot;)
      progressBar.setAttribute(&quot;style&quot;, &quot;height: 5px; width: 0px; position: fixed; top: 0; left : 0; background-color: red; z-index: 100000;&quot;)
      document.body.insertBefore(progressBar, document.body.firstChild);
      $(document).scroll(function (e) {
		var scrollAmount = $(window).scrollTop();
		var documentHeight = $(document).height();
		var windowHeight = $(window).height();
		var scrollPercent = (scrollAmount / (documentHeight - windowHeight)) * 100;
		var roundScroll = Math.round(scrollPercent);
        progressBar.setAttribute(&quot;style&quot;, &quot;height: 5px; width: &quot; + scrollPercent + &quot;vw; position: fixed; top: 0; left : 0; background-color: red;  z-index: 100000;&quot;)
	  });
      
      
      $(&quot;div[data-ke-type='moreLess']&quot;)
      .css(&quot;margin&quot;, &quot;20px 0px&quot;)
      .css(&quot;background-color&quot;, &quot;#fafafa&quot;)
      .css(&quot;border&quot;, &quot;1px dashed #c5c5c5&quot;)
      .css(&quot;color&quot;, &quot;#333333&quot;)
      
      $(&quot;.btn-toggle-moreless&quot;)
      .css(&quot;width&quot;, &quot;100%&quot;).css(&quot;height&quot;, &quot;100%&quot;).css(&quot;display&quot;, &quot;block&quot;).css(&quot;padding&quot;, &quot;20px 20px 22px&quot;)
	  .text(&quot;열어보기&quot;)
  
      $(&quot;.btn-toggle-moreless&quot;).each((i, tag) =&gt; {
	    var text = $(tag).next().text().substring(0, 50).replaceAll(&quot;\n&quot;, &quot;&quot;)
        $(tag).text(&quot; 열어보기 &quot; + text + &quot; ... ... &quot;);
      });
      
      
    }, 500);  
  
&lt;/script&gt;</description>
      <category>Kubernetes</category>
      <category>etcd</category>
      <category>kubernetes</category>
      <category>lease</category>
      <category>lock</category>
      <category>operator</category>
      <category>Operator Pattern</category>
      <category>Zookeeper</category>
      <category>리스</category>
      <category>오퍼레이터 패턴</category>
      <category>쿠버네티스</category>
      <author>코딩수집가</author>
      <guid isPermaLink="true">https://westlife0615.tistory.com/1185</guid>
      <comments>https://westlife0615.tistory.com/1185#entry1185comment</comments>
      <pubDate>Mon, 17 Feb 2025 06:52:42 +0900</pubDate>
    </item>
    <item>
      <title>[대규모 데이터 모델링] Dimensional Modeling (Fact &amp;amp; Dimension)</title>
      <link>https://westlife0615.tistory.com/1184</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 다루는 일을 하다 보면, 결국 &lt;b&gt;어떻게 저장하고 어떻게 바라볼 것인가&lt;/b&gt;라는 질문으로 귀결됩니다. 특히 분석과 리포팅을 목적으로 하는 시스템에서는 단순히 데이터를 쌓는 것보다, &lt;b&gt;사람이 이해하기 쉬운 구조로 모델링하는 것&lt;/b&gt;이 훨씬 중요해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 데이터 모델링 기법 중에서도 분석 시스템에서 가장 널리 사용되는 &lt;b&gt;Dimensional Modeling&lt;/b&gt;에 대해 정리해보려고 합니다. 데이터 웨어하우스나 데이터 마트 설계 과정에서 한 번쯤은 반드시 만나게 되는 개념이기 때문에, 전체적인 흐름을 차분히 짚어보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Dimensional Modeling이란 무엇인가요?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dimensional Modeling은 &lt;b&gt;분석과 조회를 최우선 목표로 하는 데이터 모델링 기법&lt;/b&gt;입니다. 트랜잭션 처리(OLTP)를 위한 정규화 모델과 달리, 분석용 쿼리가 단순하고 직관적으로 작성될 수 있도록 구조를 설계하는 것이 핵심입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 모델링 방식의 가장 큰 특징은 데이터를 크게 두 가지로 나눈다는 점입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Fact&lt;/b&gt;: 측정 가능한 수치 데이터&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Dimension&lt;/b&gt;: Fact를 설명해주는 기준 정보&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 두 가지를 중심으로 데이터 구조를 구성하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Fact 테이블이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fact 테이블은 &lt;b&gt;분석의 대상이 되는 수치 데이터&lt;/b&gt;를 담고 있습니다. 매출 금액, 주문 수량, 클릭 수, 노출 수처럼 집계와 계산의 대상이 되는 값들이 여기에 해당합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fact 테이블의 주요 특징은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;숫자 형태의 **측정값(measure)**을 중심으로 구성됩니다.&lt;/li&gt;
&lt;li&gt;여러 개의 **Dimension 키(Foreign Key)**를 함께 가집니다.&lt;/li&gt;
&lt;li&gt;데이터의 &lt;b&gt;그레인(Grain)&lt;/b&gt;, 즉 한 행이 의미하는 기준이 명확해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 일자-사용자-이벤트 단위의 행동 로그라는 그레인을 가진 Fact 테이블이라면, 한 행은 특정 날짜에 특정 사용자가 특정 행동(조회, 클릭, 구매 등)을 수행했음을 의미하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 그레인을 설계 단계에서 명확히 정의하지 않으면, 이후 집계 결과가 어긋나거나 해석이 어려워지는 문제가 발생하기 쉽습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사용자 행동 데이터 예시 (Click / View)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 설명한 개념을 조금 더 구체적으로 이해하기 위해, 실제 서비스에서 흔히 수집하는 &lt;b&gt;사용자 행동 데이터&lt;/b&gt;를 기준으로 예시를 들어보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 사용자가 콘텐츠를 &lt;b&gt;조회(view)&lt;/b&gt; 하거나 &lt;b&gt;클릭(click)&lt;/b&gt; 하는 이벤트를 수집하고 있다고 가정해보겠습니다. 이 경우 Fact 테이블의 그레인은 다음과 같이 정의할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일자 &amp;ndash; 사용자 &amp;ndash; 콘텐츠 &amp;ndash; 행동(Event) 단위&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 한 행은 특정 날짜에 특정 사용자가 특정 콘텐츠에 대해 특정 행동을 수행했음을 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 그레인을 기준으로 한 Fact 테이블에는 아래와 같은 측정값이 포함될 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;view_count&lt;/li&gt;
&lt;li&gt;click_count&lt;/li&gt;
&lt;li&gt;dwell_time(체류 시간)&lt;/li&gt;
&lt;li&gt;is_clicked(클릭 여부 플래그)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 Dimension 테이블은 이 행동을 해석하기 위한 기준 정보를 담게 됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자 Dimension: 사용자 ID, 가입일, 국가, 플랫폼&lt;/li&gt;
&lt;li&gt;콘텐츠 Dimension: 콘텐츠 ID, 콘텐츠 유형, 카테고리&lt;/li&gt;
&lt;li&gt;행동 Dimension: view, click 등의 행동 타입&lt;/li&gt;
&lt;li&gt;날짜 Dimension: 연, 월, 일, 요일&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 같은 구조를 사용하면, &quot;특정 기간 동안 특정 카테고리의 콘텐츠를 많이 조회한 사용자 그룹&quot;과 같은 분석 쿼리를 비교적 단순하게 작성할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Dimension 테이블이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dimension 테이블은 &lt;b&gt;Fact를 바라보는 기준과 맥락을 제공하는 역할&lt;/b&gt;을 합니다. 다시 말해, 숫자에 의미를 부여해주는 정보라고 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 Dimension 예시는 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;날짜(Date)&lt;/li&gt;
&lt;li&gt;사용자(User)&lt;/li&gt;
&lt;li&gt;행동(Event)&lt;/li&gt;
&lt;li&gt;콘텐츠(Content)&lt;/li&gt;
&lt;li&gt;디바이스(Device)&lt;/li&gt;
&lt;li&gt;유입 채널(Channel)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 사용자 Dimension에는 사용자 ID, 가입일, 국가, 플랫폼 정보가 포함될 수 있고, 콘텐츠 Dimension에는 콘텐츠 ID, 유형, 카테고리 정보 등이 함께 들어갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금 더 구체적으로 사용자 행동 데이터를 기준으로 살펴보면, 다음과 같은 형태의 Dimension 구성을 생각해볼 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자 Dimension: 사용자 ID, 가입일, 국가, 플랫폼, 앱 버전&lt;/li&gt;
&lt;li&gt;콘텐츠 Dimension: 콘텐츠 ID, 콘텐츠 유형, 카테고리, 작성자&lt;/li&gt;
&lt;li&gt;행동 Dimension: view, click과 같은 행동 타입&lt;/li&gt;
&lt;li&gt;디바이스 Dimension: OS, 디바이스 종류, 브라우저&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 같은 Dimension 정보가 Fact 테이블과 함께 연결될 경우, 단순한 클릭 수 집계를 넘어 &quot;어떤 사용자가 어떤 환경에서 어떤 콘텐츠에 반응했는가&quot;와 같은 분석이 가능해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Star Schema 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dimensional Modeling에서 가장 일반적으로 사용되는 형태는 &lt;b&gt;Star Schema&lt;/b&gt;입니다. Fact 테이블을 중심에 두고, 여러 개의 Dimension 테이블이 방사형으로 연결된 구조를 가집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조의 장점은 비교적 명확합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조인 구조가 단순해집니다.&lt;/li&gt;
&lt;li&gt;쿼리 가독성이 좋아집니다.&lt;/li&gt;
&lt;li&gt;분석가나 비즈니스 사용자 입장에서 이해하기 쉽습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 마트 설계 시, 하나의 Fact 테이블과 여러 Dimension 테이블만으로 대부분의 분석 요구를 충족할 수 있어 자주 활용됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Dimensional Modeling의 장점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dimensional Modeling은 다음과 같은 환경에서 특히 강점을 가집니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리포트와 대시보드 중심의 분석 시스템&lt;/li&gt;
&lt;li&gt;반복적인 집계 쿼리가 많은 경우&lt;/li&gt;
&lt;li&gt;비즈니스 사용자가 직접 데이터를 조회하는 환경&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 구조 자체가 의미를 담고 있기 때문에, 협업과 유지보수 측면에서도 장점이 큽니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리하며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dimensional Modeling은 단순한 테이블 설계 기법이라기보다, &lt;b&gt;데이터를 어떻게 해석하고 전달할 것인가에 대한 사고 방식&lt;/b&gt;에 가깝다고 생각합니다. Fact와 Dimension을 명확히 구분하고, 데이터의 그레인을 신중하게 정의하는 것만으로도 모델의 완성도는 크게 달라집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분석용 데이터 모델을 설계하실 때, 이번 글이 하나의 기준점이 되었으면 합니다. 이후에는 Fact 테이블의 유형이나 Slowly Changing Dimension과 같은 주제로 조금 더 깊이 있게 이어서 정리해보겠습니다.&lt;/p&gt;</description>
      <category>Data Warehouse</category>
      <category>data</category>
      <category>Data Warehouse</category>
      <category>Dimension</category>
      <category>Dimensional Model</category>
      <category>Dimensional Modeling</category>
      <category>DW</category>
      <category>fact</category>
      <category>grain</category>
      <category>star schema</category>
      <category>warehouse</category>
      <author>코딩수집가</author>
      <guid isPermaLink="true">https://westlife0615.tistory.com/1184</guid>
      <comments>https://westlife0615.tistory.com/1184#entry1184comment</comments>
      <pubDate>Thu, 13 Feb 2025 18:38:13 +0900</pubDate>
    </item>
    <item>
      <title>[Kubernetes] kube-scheduler 에 대해 알아보기</title>
      <link>https://westlife0615.tistory.com/1183</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;들어가며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kubectl create 또는 kubectl apply를 통해 생성된 Pod 리소스는 Kubernetes API Server를 거쳐 &lt;b&gt;etcd에 저장&lt;/b&gt;됩니다. 이 시점에서 Pod는 아직 어떤 Node에도 할당되지 않은 상태이며, 스케줄링을 기다리는 단계에 놓이게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kube-scheduler는 API Server를 통해 etcd에 저장된 리소스 상태를 &lt;b&gt;watch&lt;/b&gt;하고 있으며, 이 중 아직 Node가 지정되지 않은 Pod를 감지하면 스케줄링 대상 Pod로 인식합니다. 이후 kube-scheduler는 클러스터의 현재 상태와 다양한 제약 조건을 기반으로 해당 Pod를 실행할 Node를 결정하고, 그 결과를 다시 API Server에 반영합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kube-scheduler는 Pod의 Node를 지정한 이후, 해당 정보를 API Server를 통해 반영합니다. 이후 각 Node에서 동작 중인 kubelet은 자신에게 할당된 Pod를 감지하고, 그 시점부터 비로소 Pod 및 Container 생성 과정을 시작하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;kube-scheduler의 역할&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kube-scheduler의 핵심 역할은 간단합니다. &lt;b&gt;어떤 Pod를 어떤 Node에 배치할 것인지 결정하는 것&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kube-scheduler는 다음과 같은 정보들을 기반으로 스케줄링 결정을 수행합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 Node의 리소스 사용량 및 가용 리소스&lt;/li&gt;
&lt;li&gt;Pod가 요청한 CPU, 메모리 등의 리소스 요구사항&lt;/li&gt;
&lt;li&gt;NodeSelector, Node Affinity, Pod Affinity / Anti-Affinity 설정&lt;/li&gt;
&lt;li&gt;Taints와 Tolerations&lt;/li&gt;
&lt;li&gt;이미 배치된 다른 Pod들과의 관계&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 요소들을 종합하여, 실행 가능한 Node 후보를 선별하고 그중에서 가장 적절한 Node를 선택합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;default-scheduler&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes 클러스터에는 기본적으로 &lt;b&gt;default-scheduler&lt;/b&gt;가 동작하고 있습니다. Pod를 별도로 지정하지 않는 한, 대부분의 Pod는 이 default-scheduler에 의해 스케줄링됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pod 리소스에는 spec.schedulerName 필드가 존재하며, 이 값이 설정되지 않은 경우 기본값으로 default-scheduler가 사용됩니다. kube-scheduler는 API Server를 통해 Pod를 watch할 때, 이 schedulerName 값이 자신과 일치하는 Pod만을 스케줄링 대상으로 인식합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조 덕분에 Kubernetes에서는 하나의 클러스터 내에서 여러 개의 스케줄러를 동시에 운영할 수 있습니다. 예를 들어 특정 워크로드에 대해 별도의 스케줄링 정책을 적용하고 싶다면, custom scheduler를 구현하고 schedulerName을 통해 해당 스케줄러가 Pod를 처리하도록 구성할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면, default-scheduler는 Kubernetes에서 제공하는 &lt;b&gt;기본 스케줄링 로직을 담당하는 표준 스케줄러&lt;/b&gt;이며, 대부분의 일반적인 워크로드는 이 default-scheduler의 판단에 따라 Node에 배치됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;schedulerName 및 nodeName 지정 Pod의 동작&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pod spec에는 schedulerName과 nodeName이라는 두 가지 중요한 필드가 존재합니다. 이 두 필드는 kube-scheduler의 동작 여부에 직접적인 영향을 미칩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 schedulerName이 명시적으로 설정된 경우, 해당 Pod는 지정된 스케줄러만이 처리 대상이 됩니다. 예를 들어 다음과 같이 schedulerName을 지정하면, default-scheduler는 해당 Pod를 스케줄링 대상으로 인식하지 않습니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Pod
metadata:
  name: custom-scheduler-pod
spec:
  schedulerName: custom-scheduler
  containers:
  - name: nginx
    image: nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Pod spec에 nodeName이 이미 설정되어 있는 경우, 해당 Pod는 스케줄링 대상에서 제외됩니다. 이 경우 kube-scheduler는 해당 Pod를 처리하지 않으며, 지정된 Node의 kubelet이 Pod를 감지한 뒤 곧바로 실행 절차를 시작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, kube-scheduler의 역할은 &lt;b&gt;nodeName이 비어 있고, 자신의 schedulerName과 일치하는 Pod에 대해서만 Node를 선택하는 것&lt;/b&gt;이라고 정리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스케줄링 과정 개요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kube-scheduler의 스케줄링 과정은 크게 다음과 같은 단계로 나뉩니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;스케줄링 대상 Pod 감지&lt;/b&gt;&lt;br /&gt;API Server를 통해 아직 Node에 바인딩되지 않은 Pod를 감지합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;필터링(Filter) 단계&lt;/b&gt;&lt;br /&gt;Pod가 실행될 수 없는 Node를 제외합니다. 리소스 부족, Taint/Toleration 불일치, Affinity 조건 위반 등이 이 단계에서 걸러집니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스코어링(Score) 단계&lt;/b&gt;&lt;br /&gt;필터링을 통과한 Node들을 대상으로 우선순위를 계산합니다. 리소스 여유, 균형 분산 여부 등의 기준이 적용됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Node 선택 및 바인딩&lt;/b&gt;&lt;br /&gt;가장 높은 점수를 받은 Node를 선택하고, 해당 Node로 Pod를 바인딩합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정은 매우 짧은 시간 안에 반복적으로 수행되며, 클러스터 규모가 커질수록 스케줄러의 성능이 중요해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Filter 단계에서 고려되는 요소들&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Filter 단계에서는 Pod가 &lt;b&gt;실행 자체가 불가능한 Node&lt;/b&gt;를 제거하는 데 집중합니다. 대표적으로 다음과 같은 조건들이 적용됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Node의 가용 CPU, 메모리가 Pod의 요청량을 충족하는지 여부&lt;/li&gt;
&lt;li&gt;Pod의 NodeSelector 및 Node Affinity 조건 충족 여부&lt;/li&gt;
&lt;li&gt;Node에 설정된 Taint를 Pod가 Toleration으로 허용하는지 여부&lt;/li&gt;
&lt;li&gt;Pod 간 Anti-Affinity 조건 위반 여부&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 단계에서 하나라도 만족하지 못하면 해당 Node는 후보군에서 제외됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Score 단계에서의 우선순위 계산&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Filter 단계를 통과한 Node들은 Score 단계에서 점수화됩니다. kube-scheduler는 여러 스코어링 플러그인을 통해 각 Node에 점수를 부여하고, 그 결과를 종합하여 최종 우선순위를 계산합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 기준들이 사용됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리소스 사용률이 균형 잡혀 있는 Node&lt;/li&gt;
&lt;li&gt;특정 Pod가 몰리지 않도록 분산이 잘 이루어지는 Node&lt;/li&gt;
&lt;li&gt;선호 Affinity 조건을 가장 잘 만족하는 Node&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Score 단계는 **&quot;가능한 Node 중 가장 적합한 Node&quot;**를 선택하기 위한 과정이라고 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스케줄링 결과와 바인딩&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 선택된 Node는 API Server를 통해 Pod와 바인딩됩니다. 이 과정에서 kube-scheduler의 실제 동작은 &lt;b&gt;Pod spec의 nodeName 필드에 선택된 Node의 이름을 설정하는 것&lt;/b&gt;입니다. 이 시점부터 각 Node에서 동작 중인 kubelet은 &lt;b&gt;자신이 속한 Node에 nodeName으로 지정된 Pod를 API Server를 통해 watch하고&lt;/b&gt;, 해당 Pod 정보를 가져옵니다. 이후 kubelet은 해당 Pod를 실제로 생성하고 컨테이너를 실행하게 됩니다. kubelet은 Container Runtime Interface(CRI)를 통해 containerd와 같은 Container Runtime과 통신하며, 이 과정에서 실제 컨테이너 생성 및 실행이 이루어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 kube-scheduler가 &lt;b&gt;컨테이너 실행을 직접 수행하지는 않는다&lt;/b&gt;는 것입니다. kube-scheduler는 어디에 배치할지를 결정할 뿐이며, 실제 실행은 kubelet의 역할입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;FailedScheduling 이벤트 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Filter 단계에서 모든 Node가 조건을 만족하지 못하는 경우, kube-scheduler는 Pod를 어느 Node에도 배치하지 못하고 스케줄링을 보류합니다. 이때 해당 Pod는 Pending 상태로 유지되며, 이벤트(Event)에는 FailedScheduling 메시지가 기록됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 클러스터 내 어떤 Node도 Pod가 요청한 리소스를 충족하지 못하는 경우, 다음과 같은 이벤트를 확인할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;kubectl describe pod example-pod
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Events:
  Type     Reason             Age    From               Message
  ----     ------             ----   ----               -------
  Warning  FailedScheduling   10s    default-scheduler  0/3 nodes are available: 3 Insufficient cpu.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 메시지는 클러스터에 존재하는 모든 Node가 CPU 리소스 부족 상태이기 때문에, 해당 Pod를 배치할 수 없음을 의미합니다. 이와 유사하게 Affinity 조건 불일치, Taint/Toleration 미충족 등의 이유로도 FailedScheduling 이벤트가 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 이벤트를 통해 사용자는 스케줄링이 실패한 원인을 파악하고, 리소스 확장이나 Pod 스펙 조정을 통해 문제를 해결할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마무리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kube-scheduler는 쿠버네티스에서 Pod의 배치를 책임지는 핵심 컴포넌트입니다. 단순히 Node를 하나 고르는 역할처럼 보일 수 있지만, 내부적으로는 다양한 제약 조건과 우선순위를 고려하는 복잡한 의사결정 과정을 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클러스터 규모가 커질수록 스케줄링 전략은 성능과 안정성에 큰 영향을 미치게 됩니다. kube-scheduler의 동작 원리를 이해하고 있다면, Node Affinity, Taints/Tolerations, 리소스 설계 등을 보다 의도적으로 구성할 수 있을 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;
&lt;script&gt;

    setTimeout(function() {
      var elementList = document.querySelectorAll(&quot;h2&quot;);
      var indexer = null;
      for (var i = 0; i &lt; elementList.length; i++) {
          if (elementList[i].innerText.indexOf(&quot;- 목차&quot;) &gt; -1) {
              indexer = elementList[i]    
          }
          
      }
      var headerList = document.querySelectorAll(&quot;h2,h3,h4&quot;);
	  var allHeaderList = [...headerList]
      var indexBox = document.createElement(&quot;ul&quot;);
      for (var i = 0; i &lt; allHeaderList.length; i++) {
        var header = allHeaderList[i]; 
        var headerText = header.innerText
        if (headerText != null &amp;&amp; (headerText.indexOf(&quot;*&quot;) &gt; -1 || headerText.indexOf(&quot;.&quot;) &gt; -1 || headerText.indexOf(&quot;;&quot;) &gt; -1 || headerText.indexOf(&quot;?&quot;) &gt; -1 || headerText.indexOf(&quot;!&quot;) &gt; -1)) {
          header.setAttribute(&quot;id&quot;, i);
          
          var indexItem = document.createElement(&quot;li&quot;)
          var indexLink = document.createElement(&quot;a&quot;)
          indexLink.setAttribute(&quot;style&quot;, &quot;color: black; line-height: 2;&quot;);
          indexLink.setAttribute(&quot;href&quot;, &quot;#&quot; + i);

          indexItem.appendChild(indexLink);
          indexLink.innerText = headerText;
		  if (header.tagName == 'H2') {
			indexItem.setAttribute(&quot;style&quot;, &quot;margin-left: 12px; list-style: circle;&quot;);
		  }
		  if (header.tagName == 'H3') {
			indexItem.setAttribute(&quot;style&quot;, &quot;margin-left: 24px; list-style: disc;&quot;);
		  }          
		  if (header.tagName == 'H4') {
			indexItem.setAttribute(&quot;style&quot;, &quot;margin-left: 36px; list-style: disclosure-closed;&quot;);
		  }                    
          indexBox.appendChild(indexItem)

        }

      }

      indexer.parentElement.insertBefore(indexBox, indexer);
      indexer.parentElement.removeChild(indexer)
      indexBox.parentElement.insertBefore(indexer, indexBox)


      var progressBar = document.createElement(&quot;div&quot;)
	  progressBar.setAttribute(&quot;class&quot;, &quot;progress&quot;)
      progressBar.setAttribute(&quot;style&quot;, &quot;height: 5px; width: 0px; position: fixed; top: 0; left : 0; background-color: red; z-index: 100000;&quot;)
      document.body.insertBefore(progressBar, document.body.firstChild);
      $(document).scroll(function (e) {
		var scrollAmount = $(window).scrollTop();
		var documentHeight = $(document).height();
		var windowHeight = $(window).height();
		var scrollPercent = (scrollAmount / (documentHeight - windowHeight)) * 100;
		var roundScroll = Math.round(scrollPercent);
        progressBar.setAttribute(&quot;style&quot;, &quot;height: 5px; width: &quot; + scrollPercent + &quot;vw; position: fixed; top: 0; left : 0; background-color: red;  z-index: 100000;&quot;)
	  });
      
      
      $(&quot;div[data-ke-type='moreLess']&quot;)
      .css(&quot;margin&quot;, &quot;20px 0px&quot;)
      .css(&quot;background-color&quot;, &quot;#fafafa&quot;)
      .css(&quot;border&quot;, &quot;1px dashed #c5c5c5&quot;)
      .css(&quot;color&quot;, &quot;#333333&quot;)
      
      $(&quot;.btn-toggle-moreless&quot;)
      .css(&quot;width&quot;, &quot;100%&quot;).css(&quot;height&quot;, &quot;100%&quot;).css(&quot;display&quot;, &quot;block&quot;).css(&quot;padding&quot;, &quot;20px 20px 22px&quot;)
	  .text(&quot;열어보기&quot;)
  
      $(&quot;.btn-toggle-moreless&quot;).each((i, tag) =&gt; {
	    var text = $(tag).next().text().substring(0, 50).replaceAll(&quot;\n&quot;, &quot;&quot;)
        $(tag).text(&quot; 열어보기 &quot; + text + &quot; ... ... &quot;);
      });
      
      
    }, 500);  
  
&lt;/script&gt;
&lt;/p&gt;</description>
      <category>Kubernetes</category>
      <category>controlplane</category>
      <category>default-scheduler</category>
      <category>k8s</category>
      <category>kube-scheduler</category>
      <category>kubectl</category>
      <category>kubernetes</category>
      <category>nodeName</category>
      <category>scheduler</category>
      <category>schedulerName</category>
      <category>쿠버네티스</category>
      <author>코딩수집가</author>
      <guid isPermaLink="true">https://westlife0615.tistory.com/1183</guid>
      <comments>https://westlife0615.tistory.com/1183#entry1183comment</comments>
      <pubDate>Thu, 13 Feb 2025 07:26:22 +0900</pubDate>
    </item>
    <item>
      <title>Kubernetes Metrics Server 알아보기</title>
      <link>https://westlife0615.tistory.com/1180</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Metrics Server는 쿠버네티스 클러스터 내에서 &lt;/span&gt;&lt;b&gt;&lt;span&gt;Node, Pod, Container의 리소스 사용량 메트릭을 제공하는 API 서버&lt;/span&gt;&lt;/b&gt;&lt;span&gt;입니다. CPU와 메모리 사용량과 같은 기본적인 리소스 메트릭을 수집하여, Kubernetes API를 통해 조회할 수 있도록 제공합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;쿠버네티스 클러스터의 각 Node에는 kubelet이라는 구성 요소가 존재하며, Metrics Server는 이 kubelet으로부터 노드에 실행 중인 Container의 리소스 사용 정보를 주기적으로 수집합니다. 이렇게 수집된 메트릭은 가공 과정을 거쳐 API 형태로 제공되며, 클러스터 내부 컴포넌트나 사용자가 조회할 수 있도록 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;특히 Kubernetes의 HPA(Horizontal Pod Autoscaler) Controller는 Metrics Server로부터 Pod 또는 Container 단위의 CPU, 메모리 사용량 정보를 전달받아 자동 스케일링을 수행합니다. 이러한 점에서 Metrics Server는 HPA를 구성하기 위해 사실상 필수적인 컴포넌트이며, 가장 대표적인 활용 사례 또한 자동 스케일링 시나리오라고 할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;cAdvisor - Metrics Server 가 정보를 수집하는 방법&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;쿠버네티스에서 컨테이너 단위의 리소스 메트릭은 **kubelet 내부에 포함된 cAdvisor(Container Advisor)**를 통해 수집됩니다. cAdvisor는 각 노드에서 실행 중인 컨테이너의 CPU, 메모리 사용량과 같은 리소스 정보를 주기적으로 수집하고 이를 kubelet의 메트릭 엔드포인트로 노출합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Metrics Server는 각 Node에 존재하는 kubelet의 &lt;/span&gt;&lt;span&gt;/metricmes/resource&lt;/span&gt;&lt;span&gt; 엔드포인트를 주기적으로 호출하여, cAdvisor가 수집한 컨테이너 및 파드 수준의 리소스 메트릭을 가져옵니다. 이렇게 수집된 메트릭은 노드별로 집계된 이후, Kubernetes API Server를 통해 &lt;/span&gt;&lt;span&gt;metrics.k8s.io&lt;/span&gt;&lt;span&gt; API 형태로 제공됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;정리하면, &lt;/span&gt;&lt;b&gt;&lt;span&gt;cAdvisor는 실제 리소스 사용량을 수집하는 역할&lt;/span&gt;&lt;/b&gt;&lt;span&gt;을 담당하고, &lt;/span&gt;&lt;b&gt;&lt;span&gt;Metrics Server는 이 정보를 중앙에서 수집&amp;middot;집계하여 API로 제공하는 역할&lt;/span&gt;&lt;/b&gt;&lt;span&gt;을 수행한다고 이해하시면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;Kind로 Kubernetes 클러스터 구성하기&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Metrics Server의 동작을 로컬 환경에서 확인하기 위해서는 **kind(Kubernetes IN Docker)**를 사용하여 쿠버네티스 클러스터를 구성하는 방법이 유용합니다. kind는 Docker 컨테이너 기반으로 쿠버네티스 노드를 구성하기 때문에, 별도의 인프라 없이도 Metrics Server 및 HPA 동작을 간단히 실습해 볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;아래는 Control Plane 1대와 Worker Node 2대로 구성된 kind 클러스터 설정 예시입니다. Kubernetes v1.31.0 이미지를 사용하며, containerd에서 systemd cgroup을 사용하도록 설정되어 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1769466061263&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: metrics-server-cluster
nodes:
- role: control-plane
  image: kindest/node:v1.31.0
- role: worker
  image: kindest/node:v1.31.0
- role: worker
  image: kindest/node:v1.31.0
containerdConfigPatches:
- |
  [plugins.&quot;io.containerd.grpc.v1.cri&quot;.containerd.runtimes.runc]
    runtime_type = &quot;io.containerd.runc.v2&quot;
    [plugins.&quot;io.containerd.grpc.v1.cri&quot;.containerd.runtimes.runc.options]
      SystemdCgroup = true
      Platform = &quot;linux/amd64&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;위 설정을 예를 들어 &lt;/span&gt;&lt;span&gt;kind-config.yaml&lt;/span&gt;&lt;span&gt; 파일로 저장한 뒤, 다음 명령어를 실행하면 클러스터를 생성할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1769466073305&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kind create cluster --config kind-config.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;클러스터 생성이 완료되더라도, &lt;/span&gt;&lt;b&gt;&lt;span&gt;Metrics Server가 설치되어 있지 않은 상태에서&lt;/span&gt;&lt;/b&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;kubectl top&lt;/span&gt;&lt;span&gt; 명령어를 실행하면 메트릭을 조회할 수 없습니다. 이 경우 다음과 같은 오류 메시지가 출력됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1769466085432&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;error: Metrics API not available&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이는 kind로 구성한 Kubernetes 클러스터에 메트릭을 제공하는 API 서버가 아직 존재하지 않기 때문입니다. 따라서 노드나 파드의 리소스 사용량을 확인하거나, 이후 HPA와 같은 기능을 사용하기 위해서는 Metrics Server를 추가로 설치해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;클러스터 생성 이후에는 Metrics Server를 설치한 뒤 &lt;/span&gt;&lt;span&gt;kubectl top&lt;/span&gt;&lt;span&gt; 명령어 및 HPA 동작을 확인하실 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;Metrics Server 설치 방법&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;가장 간단한 방법은 공식에서 제공하는 매니페스트를 그대로 적용하는 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1769466113113&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;위 명령어를 실행하면 Metrics Server와 관련된 ServiceAccount, RBAC 리소스, Service, Deployment, APIService 등이 함께 생성됩니다. 예를 들어, &lt;/span&gt;&lt;span&gt;kubectl apply&lt;/span&gt;&lt;span&gt; 실행 결과는 다음과 같이 출력됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1769466127297&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;serviceaccount/metrics-server created
clusterrole.rbac.authorization.k8s.io/system:aggregated-metrics-reader created
clusterrole.rbac.authorization.k8s.io/system:metrics-server created
rolebinding.rbac.authorization.k8s.io/metrics-server-auth-reader created
clusterrolebinding.rbac.authorization.k8s.io/metrics-server:system:auth-delegator created
clusterrolebinding.rbac.authorization.k8s.io/system:metrics-server created
service/metrics-server created
deployment.apps/metrics-server created
apiservice.apiregistration.k8s.io/v1beta1.metrics.k8s.io created&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Metrics Server 는 Aggregated API 형태로 &lt;/span&gt;&lt;span&gt;metrics.k8s.io&lt;/span&gt;&lt;span&gt; API를 등록합니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;설치 이후에는 다음 명령어를 통해 해당 API가 정상적으로 등록되었는지 확인하실 수 있습니다.&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769466163394&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kubectl get apiservice | grep metrics
v1beta1.metrics.k8s.io                 kube-system/metrics-server   True        14m&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;설치가 완료되면 &lt;/span&gt;&lt;span&gt;kube-system&lt;/span&gt;&lt;span&gt; 네임스페이스에 &lt;/span&gt;&lt;span&gt;metrics-server&lt;/span&gt;&lt;span&gt; Deployment 가 생성됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1769466191894&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kubectl get deployment metrics-server -n kube-system

NAME             READY   UP-TO-DATE   AVAILABLE   AGE
metrics-server   1/1     1            1           15m&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;READY 상태가 &lt;/span&gt;&lt;span&gt;1/1&lt;/span&gt;&lt;span&gt;로 표시된다면 정상적으로 기동된 상태입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;추가로, kind와 같은 로컬 Kubernetes 환경이나 자체 인증서 구성을 사용하는 환경에서는 kubelet TLS 인증 문제로 인해 Metrics Server가 정상 동작하지 않는 경우가 있습니다. 이 경우 &lt;/span&gt;&lt;span&gt;metrics-server&lt;/span&gt;&lt;span&gt; Deployment의 &lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;span&gt; 섹션에 &lt;/span&gt;&lt;span&gt;--kubelet-insecure-tls&lt;/span&gt;&lt;span&gt; 옵션을 추가하여 문제를 해결할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어 &lt;/span&gt;&lt;span&gt;metrics-server&lt;/span&gt;&lt;span&gt; Deployment 매니페스트의 일부는 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769466212868&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spec:
  template:
    spec:
      containers:
      - name: metrics-server
        args:
          - --kubelet-insecure-tls&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;해당 옵션은 kubelet 인증서 검증을 생략하는 설정이므로, 테스트 환경이나 로컬 실습 용도로 사용하는 것이 적절합니다. 운영 환경에서는 kubelet 인증서를 정상적으로 구성한 뒤 사용하는 것이 바람직합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;설치 후 동작 확인&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Metrics Server가 정상적으로 동작하는지 확인하기 위해서는 &lt;/span&gt;&lt;span&gt;kubectl top&lt;/span&gt;&lt;span&gt; 명령어를 사용하시면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;nsis&quot;&gt;&lt;code&gt;kubectl top nodes
kubectl top pods --all-namespaces&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;설치 직후에는 메트릭 수집이 시작되지 않아 결과가 바로 출력되지 않을 수 있습니다. 이 경우 잠시 기다린 후 다시 실행해 보시면 CPU와 메모리 사용량이 표시되는 것을 확인하실 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어 Metrics Server가 정상적으로 동작하는 경우, &lt;/span&gt;&lt;span&gt;kubectl top pods --all-namespaces&lt;/span&gt;&lt;span&gt; 명령어를 실행하면 다음과 같이 각 파드의 CPU 및 메모리 사용량이 출력됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;kubectl top pod --all-namespaces
NAMESPACE            NAME                                                           CPU(cores)   MEMORY(bytes)
kube-system          coredns-6f6b679f8f-lr7pv                                       2m           17Mi
kube-system          coredns-6f6b679f8f-n2qps                                       2m           15Mi
kube-system          etcd-metrics-server-cluster-control-plane                      23m          33Mi
kube-system          kindnet-f57kj                                                  1m           12Mi
kube-system          kindnet-krrgz                                                  1m           11Mi
kube-system          kindnet-t9t4c                                                  1m           10Mi
kube-system          kube-apiserver-metrics-server-cluster-control-plane            35m          211Mi
kube-system          kube-controller-manager-metrics-server-cluster-control-plane   20m          58Mi
kube-system          kube-proxy-fj5mm                                               1m           15Mi
kube-system          kube-proxy-tw2dz                                               1m           17Mi
kube-system          kube-proxy-vhs5g                                               1m           16Mi
kube-system          kube-scheduler-metrics-server-cluster-control-plane            3m           24Mi
kube-system          metrics-server-bf688598-ksgvs                                  5m           19Mi
local-path-storage   local-path-provisioner-ccc7bf7fc-6qkmg                         1m           9Mi&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이와 같이 네임스페이스별 파드의 리소스 사용량이 정상적으로 출력된다면 Metrics Server가 올바르게 동작하고 있다고 판단할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;script&gt;

    setTimeout(function() {
      var elementList = document.querySelectorAll(&quot;h2&quot;);
      var indexer = null;
      for (var i = 0; i &lt; elementList.length; i++) {
          if (elementList[i].innerText.indexOf(&quot;- 목차&quot;) &gt; -1) {
              indexer = elementList[i]    
          }
          
      }
      var headerList = document.querySelectorAll(&quot;h2,h3,h4&quot;);
	  var allHeaderList = [...headerList]
      var indexBox = document.createElement(&quot;ul&quot;);
      for (var i = 0; i &lt; allHeaderList.length; i++) {
        var header = allHeaderList[i]; 
        var headerText = header.innerText
        if (headerText != null &amp;&amp; (headerText.indexOf(&quot;*&quot;) &gt; -1 || headerText.indexOf(&quot;.&quot;) &gt; -1 || headerText.indexOf(&quot;;&quot;) &gt; -1 || headerText.indexOf(&quot;?&quot;) &gt; -1 || headerText.indexOf(&quot;!&quot;) &gt; -1)) {
          header.setAttribute(&quot;id&quot;, i);
          
          var indexItem = document.createElement(&quot;li&quot;)
          var indexLink = document.createElement(&quot;a&quot;)
          indexLink.setAttribute(&quot;style&quot;, &quot;color: black; line-height: 2;&quot;);
          indexLink.setAttribute(&quot;href&quot;, &quot;#&quot; + i);

          indexItem.appendChild(indexLink);
          indexLink.innerText = headerText;
		  if (header.tagName == 'H2') {
			indexItem.setAttribute(&quot;style&quot;, &quot;margin-left: 12px; list-style: circle;&quot;);
		  }
		  if (header.tagName == 'H3') {
			indexItem.setAttribute(&quot;style&quot;, &quot;margin-left: 24px; list-style: disc;&quot;);
		  }          
		  if (header.tagName == 'H4') {
			indexItem.setAttribute(&quot;style&quot;, &quot;margin-left: 36px; list-style: disclosure-closed;&quot;);
		  }                    
          indexBox.appendChild(indexItem)

        }

      }

      indexer.parentElement.insertBefore(indexBox, indexer);
      indexer.parentElement.removeChild(indexer)
      indexBox.parentElement.insertBefore(indexer, indexBox)


      var progressBar = document.createElement(&quot;div&quot;)
	  progressBar.setAttribute(&quot;class&quot;, &quot;progress&quot;)
      progressBar.setAttribute(&quot;style&quot;, &quot;height: 5px; width: 0px; position: fixed; top: 0; left : 0; background-color: red; z-index: 100000;&quot;)
      document.body.insertBefore(progressBar, document.body.firstChild);
      $(document).scroll(function (e) {
		var scrollAmount = $(window).scrollTop();
		var documentHeight = $(document).height();
		var windowHeight = $(window).height();
		var scrollPercent = (scrollAmount / (documentHeight - windowHeight)) * 100;
		var roundScroll = Math.round(scrollPercent);
        progressBar.setAttribute(&quot;style&quot;, &quot;height: 5px; width: &quot; + scrollPercent + &quot;vw; position: fixed; top: 0; left : 0; background-color: red;  z-index: 100000;&quot;)
	  });
      
      
      $(&quot;div[data-ke-type='moreLess']&quot;)
      .css(&quot;margin&quot;, &quot;20px 0px&quot;)
      .css(&quot;background-color&quot;, &quot;#fafafa&quot;)
      .css(&quot;border&quot;, &quot;1px dashed #c5c5c5&quot;)
      .css(&quot;color&quot;, &quot;#333333&quot;)
      
      $(&quot;.btn-toggle-moreless&quot;)
      .css(&quot;width&quot;, &quot;100%&quot;).css(&quot;height&quot;, &quot;100%&quot;).css(&quot;display&quot;, &quot;block&quot;).css(&quot;padding&quot;, &quot;20px 20px 22px&quot;)
	  .text(&quot;열어보기&quot;)
  
      $(&quot;.btn-toggle-moreless&quot;).each((i, tag) =&gt; {
	    var text = $(tag).next().text().substring(0, 50).replaceAll(&quot;\n&quot;, &quot;&quot;)
        $(tag).text(&quot; 열어보기 &quot; + text + &quot; ... ... &quot;);
      });
      
      
    }, 500);  
  
&lt;/script&gt;</description>
      <category>Kubernetes</category>
      <category>cadvisor</category>
      <category>HPA</category>
      <category>kubectl</category>
      <category>kubectl top</category>
      <category>Kubelet</category>
      <category>kubernetes</category>
      <category>metrics</category>
      <category>Metrics Server</category>
      <category>쿠버네티스</category>
      <author>코딩수집가</author>
      <guid isPermaLink="true">https://westlife0615.tistory.com/1180</guid>
      <comments>https://westlife0615.tistory.com/1180#entry1180comment</comments>
      <pubDate>Tue, 11 Feb 2025 06:42:32 +0900</pubDate>
    </item>
    <item>
      <title>[Flink] SplitFetcher 알아보기</title>
      <link>https://westlife0615.tistory.com/1174</link>
      <description>&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;- 목차&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Flink Source Level 에서 사용되는 SplitFetcher 에 대해서 알아봅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SplitFetcher 는 Split 을 생성하는 SplitEnumerator 와 Split 을 실행하는 SplitReader 사이를 간접적으로 연결하는 역할을 수행합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SplitEnumerator 는 JobManager 에 위치하고, SplitReader 는 TaskManager 에 존재하여 이 둘이 Split 을 공유하는 과정은 다소 복잡합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SplitEnumerator 가 Split 들을 생성하게 되고, RPC 기반의 통신을 수행하여 JobManager 에서 TaskManager 로 Split 들이 전달됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Split 을 전달받은 TaskManager 는 SourceReader 를 통해서 이를 Split 를 SplitReader 에게 전달하는데요.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 SplitFetch 가 동작하게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 동작은 AddSplitsTask 라고 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그외 RemoveSplitsTask, SplitsFetcherTask 등이 존재하며, 해당 Split 관련 Task 를 수행하는 것이 SplitFetcher 입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이어지는 내용에서 SplitFetcher 에 대해서 상세히 알아보는 시간을 가져보도록 하겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AddSplitsTask.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 SplitEnumerator 에 의해서 생성된 Split 들을 SplitReader 에게 전달하기 위해서 사용되는 Split Task 인 AddSplitsTask 에 대해서 알아봅니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 Class 는 AddSplitsTask 에 해당하는 내용이며, Runnable 타입의 클래스입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 SplitFetcher 에 의해서 비동기적으로 실행되는 하나의 작업에 대한 정의입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1755930542094&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package org.apache.flink.connector.base.source.reader.fetcher;

import org.apache.flink.annotation.Internal;
import org.apache.flink.api.connector.source.SourceSplit;
import org.apache.flink.connector.base.source.reader.splitreader.SplitReader;
import org.apache.flink.connector.base.source.reader.splitreader.SplitsAddition;

import java.util.List;
import java.util.Map;

/** The task to add splits. */
@Internal
class AddSplitsTask&amp;lt;SplitT extends SourceSplit&amp;gt; implements SplitFetcherTask {

    private final SplitReader&amp;lt;?, SplitT&amp;gt; splitReader;
    private final List&amp;lt;SplitT&amp;gt; splitsToAdd;
    private final Map&amp;lt;String, SplitT&amp;gt; assignedSplits;

    AddSplitsTask(
            SplitReader&amp;lt;?, SplitT&amp;gt; splitReader,
            List&amp;lt;SplitT&amp;gt; splitsToAdd,
            Map&amp;lt;String, SplitT&amp;gt; assignedSplits) {
        this.splitReader = splitReader;
        this.splitsToAdd = splitsToAdd;
        this.assignedSplits = assignedSplits;
    }

    @Override
    public boolean run() {
        for (SplitT s : splitsToAdd) {
            assignedSplits.put(s.splitId(), s);
        }
        splitReader.handleSplitsChanges(new SplitsAddition&amp;lt;&amp;gt;(splitsToAdd));
        return true;
    }

    @Override
    public void wakeUp() {
        // Do nothing.
    }

    @Override
    public String toString() {
        return String.format(&quot;AddSplitsTask: [%s]&quot;, splitsToAdd);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 간단히 AddSplitsTask 가 어떤 순서로 SplitEnumerator 에서 SplitReader 에게 전달되는지 알아봅니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 요약된 순서가 Split 의 전파 순서를 나타냅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일차적으로 SplitEnumerator 가 생성한 Split 은 JobManager -&amp;gt; TaskManager 로 이어지는 RPC 통신을 통해서 SplitReader 에게로 전파됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Split 를 전달받은 SplitReader 는 SplitFetcher 에게 Split 을 전달합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SplitFetcher 는 Background 에서 별도의 Thread 에서 동작하며, 내부적인 Queue 자료구조를 통해서 SplitTask 들을 전달받습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AddSplitsTask 는 특정 SplitReader 에게 Split 을 넘기는 동작이 정의된 Runnable 를 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SplitFetcher 가 별도의 Thread 에서 SplitReader 에게 Split 을 전달하는 방식으로 Split 이 전달되게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1755930673849&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 1. SplitEnumerator에서 Split 할당
SplitEnumerator.assignSplits()
    &amp;darr;
// 2. SourceCoordinatorContext를 통한 Event 전송
SourceCoordinatorContext.assignSplits()
    &amp;darr;
// 3. AddSplitEvent 생성 및 전송
AddSplitEvent 생성 &amp;rarr; RPC 전송
    &amp;darr;
// 4. SourceOperator에서 Event 수신
SourceOperator.handleAddSplitsEvent()
    &amp;darr;
// 5. SourceReader에 Split 전달
SourceReader.addSplits()
    &amp;darr;
// 6. SplitFetcherManager에 Split 전달
SplitFetcherManager.addSplits()
    &amp;darr;
// 7. SplitFetcher에 Split 전달
SplitFetcher.addSplits()
    &amp;darr;
// 8. AddSplitsTask 생성 및 enqueue
new AddSplitsTask() &amp;rarr; taskQueue에 추가
    &amp;darr;
// 9. SplitFetcher Thread에서 Task 실행
AddSplitsTask.run()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RemoveSplitsTask.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 RemoveSplitsTask 의 Runnable Class 정의입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AddSplitsTask 와 반대로 SplitReader 가 처리하는 Split 들 중에서 특정 Split 을 제거하는 역할을 수행합니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1755930917880&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package org.apache.flink.connector.base.source.reader.fetcher;

import org.apache.flink.annotation.Internal;
import org.apache.flink.api.connector.source.SourceSplit;
import org.apache.flink.connector.base.source.reader.splitreader.SplitReader;
import org.apache.flink.connector.base.source.reader.splitreader.SplitsRemoval;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Collectors;

/** The task to finish reading some splits. */
@Internal
public class RemoveSplitsTask&amp;lt;SplitT extends SourceSplit&amp;gt; implements SplitFetcherTask {
    private static final Logger LOG = LoggerFactory.getLogger(RemoveSplitsTask.class);

    private final SplitReader&amp;lt;?, SplitT&amp;gt; splitReader;
    private final List&amp;lt;SplitT&amp;gt; removedSplits;
    private final Map&amp;lt;String, SplitT&amp;gt; assignedSplits;
    private final Consumer&amp;lt;Collection&amp;lt;String&amp;gt;&amp;gt; splitFinishedCallback;

    RemoveSplitsTask(
            SplitReader&amp;lt;?, SplitT&amp;gt; splitReader,
            List&amp;lt;SplitT&amp;gt; removedSplits,
            Map&amp;lt;String, SplitT&amp;gt; assignedSplits,
            Consumer&amp;lt;Collection&amp;lt;String&amp;gt;&amp;gt; splitFinishedCallback) {
        this.splitReader = splitReader;
        this.removedSplits = removedSplits;
        this.assignedSplits = assignedSplits;
        this.splitFinishedCallback = splitFinishedCallback;
    }

    @Override
    public boolean run() {
        for (SplitT s : removedSplits) {
            assignedSplits.remove(s.splitId());
        }
        splitReader.handleSplitsChanges(new SplitsRemoval&amp;lt;&amp;gt;(removedSplits));

        List&amp;lt;String&amp;gt; splitIds =
                removedSplits.stream().map(SourceSplit::splitId).collect(Collectors.toList());
        splitFinishedCallback.accept(splitIds);
        LOG.info(&quot;RecordEvaluator triggers splits {} to finish reading.&quot;, splitIds);
        return true;
    }

    @Override
    public void wakeUp() {
        // Do nothing.
    }

    @Override
    public String toString() {
        return String.format(&quot;RemoveSplitsTask: [%s]&quot;, removedSplits);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KafkaPartitionSplit 의 경우에 특정 Topic 에서 특정 Partition 이 제거되었고, 이를 Discovery 하는 KafkaSourceEnumerator 가 이를 감지하고 RemoveSplitsTask 를 전파하게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관련된 전파의 순서는 AddSplitsTask 와 동일합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SplitsFetcherTask.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SplitsFetchTask 는 실질적인 데이터 조회를 수행하는 역할을 담당합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 클래스는 SplitsFetcherTask Interface 를 구현한 FetchTask 의 내용입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내용은 단순합니다. SplitReader 의 fetch 함수를 주기적으로 호출하는 역할을 수행합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1755931137185&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package org.apache.flink.connector.base.source.reader.fetcher;

import org.apache.flink.annotation.Internal;
import org.apache.flink.api.connector.source.SourceSplit;
import org.apache.flink.connector.base.source.reader.RecordsWithSplitIds;
import org.apache.flink.connector.base.source.reader.splitreader.SplitReader;
import org.apache.flink.connector.base.source.reader.synchronization.FutureCompletingBlockingQueue;

import java.io.IOException;
import java.util.Collection;
import java.util.function.Consumer;

/** The default fetch task that fetches the records into the element queue. */
@Internal
class FetchTask&amp;lt;E, SplitT extends SourceSplit&amp;gt; implements SplitFetcherTask {
    private final SplitReader&amp;lt;E, SplitT&amp;gt; splitReader;
    private final FutureCompletingBlockingQueue&amp;lt;RecordsWithSplitIds&amp;lt;E&amp;gt;&amp;gt; elementsQueue;
    private final Consumer&amp;lt;Collection&amp;lt;String&amp;gt;&amp;gt; splitFinishedCallback;
    private final int fetcherIndex;
    private volatile RecordsWithSplitIds&amp;lt;E&amp;gt; lastRecords;
    private volatile boolean wakeup;

    FetchTask(
            SplitReader&amp;lt;E, SplitT&amp;gt; splitReader,
            FutureCompletingBlockingQueue&amp;lt;RecordsWithSplitIds&amp;lt;E&amp;gt;&amp;gt; elementsQueue,
            Consumer&amp;lt;Collection&amp;lt;String&amp;gt;&amp;gt; splitFinishedCallback,
            int fetcherIndex) {
        this.splitReader = splitReader;
        this.elementsQueue = elementsQueue;
        this.splitFinishedCallback = splitFinishedCallback;
        this.lastRecords = null;
        this.fetcherIndex = fetcherIndex;
        this.wakeup = false;
    }

    @Override
    public boolean run() throws IOException {
        try {
            if (!isWakenUp() &amp;amp;&amp;amp; lastRecords == null) {
                lastRecords = splitReader.fetch();
            }

            if (!isWakenUp()) {
                if (elementsQueue.put(fetcherIndex, lastRecords)) {
                    if (!lastRecords.finishedSplits().isEmpty()) {
                    }
                    lastRecords = null;
                }
            }
        } catch (InterruptedException e) {
            throw new IOException(&quot;Source fetch execution was interrupted&quot;, e);
        } finally {
            if (isWakenUp()) {
                wakeup = false;
            }
        }
        return true;
    }

    @Override
    public void wakeUp() {
        wakeup = true;
        if (lastRecords == null) {
            splitReader.wakeUp();
        } else {
            elementsQueue.wakeUpPuttingThread(fetcherIndex);
        }
    }

    private boolean isWakenUp() {
        return wakeup;
    }

    @Override
    public String toString() {
        return &quot;FetchTask&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SplitFetcher 는 내부적으로 그리고 자동으로 SplitFetcherTask 를 생성하고 실행합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AddSplitsTask 와 RemoveSplitsTask 가 외부에서 전달되는 형식과는 다릅니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부적으로 While Loop 을 반복하여 SplitsFetcherTask 를 생성하고 실행하기를 반복하게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1755931659002&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// SplitFetcher 생성자에서 FetchTask 자동 생성
SplitFetcher(...) {
    // FetchTask 자동 생성
    this.fetchTask = new FetchTask&amp;lt;&amp;gt;(
        splitReader,                    // SplitReader 인스턴스
        elementsQueue,                  // 데이터 전달용 Queue
        ids -&amp;gt; {                        // Split 완료 콜백
            ids.forEach(assignedSplits::remove);
            splitFinishedHook.accept(ids);
            LOG.info(&quot;Finished reading from splits {}&quot;, ids);
        },
        id                              // Fetcher ID
    );
}

boolean runOnce() {
    // 1. Task 선택
    SplitFetcherTask task;
    lock.lock();
    try {
        if (closed) {
            return false;
        }
        task = getNextTaskUnsafe();
        if (task == null) {
            return true;
        }
        this.runningTask = task;
    } finally {
        lock.unlock();
    }

    // 2. Task 실행 (Lock 외부에서 실행)
    boolean taskFinished;
    try {
        taskFinished = task.run();
    } catch (Exception e) {
        throw new RuntimeException(&quot;SplitFetcher thread received unexpected exception&quot;, e);
    }

    // 3. 결과 처리
    lock.lock();
    try {
        this.runningTask = null;
        processTaskResultUnsafe(task, taskFinished);
    } finally {
        lock.unlock();
    }
    return true;
}

@Override
public void run() {
    LOG.info(&quot;Starting split fetcher {}&quot;, id);
    try {
        while (runOnce()) {
            // nothing to do, everything is inside #runOnce.
        }
        if (recordsProcessedLatch.getCount() &amp;gt; 0) {
            // Put an empty synchronization batch to the element queue.
            // When this batch is recycled, all the records emitted earlier
            // must have already been processed.
            elementsQueue.put(
                    fetcherId(),
                    new RecordsBySplits&amp;lt;E&amp;gt;(Collections.emptyMap(), Collections.emptySet()) {
                        @Override
                        public void recycle() {
                            super.recycle();
                            recordsProcessedLatch.countDown();
                        }
                    });
        }
    } catch (Throwable t) {
        errorHandler.accept(t);
    } finally {
        try {
            recordsProcessedLatch.await();
            splitReader.close();
        } catch (Exception e) {
            errorHandler.accept(e);
        } finally {
            LOG.info(&quot;Split fetcher {} exited.&quot;, id);
            // This executes after possible errorHandler.accept(t). If these operations bear
            // a happens-before relation, then we can checking side effect of
            // errorHandler.accept(t)
            // to know whether it happened after observing side effect of shutdownHook.run().
            shutdownHook.run();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;script&gt;

    setTimeout(function() {
      var elementList = document.querySelectorAll(&quot;h2&quot;);
      var indexer = null;
      for (var i = 0; i &lt; elementList.length; i++) {
          if (elementList[i].innerText.indexOf(&quot;- 목차&quot;) &gt; -1) {
              indexer = elementList[i]    
          }
          
      }
      var headerList = document.querySelectorAll(&quot;h2,h3,h4&quot;);
	  var allHeaderList = [...headerList]
      var indexBox = document.createElement(&quot;ul&quot;);
      for (var i = 0; i &lt; allHeaderList.length; i++) {
        var header = allHeaderList[i]; 
        var headerText = header.innerText
        if (headerText != null &amp;&amp; (headerText.indexOf(&quot;*&quot;) &gt; -1 || headerText.indexOf(&quot;.&quot;) &gt; -1 || headerText.indexOf(&quot;;&quot;) &gt; -1 || headerText.indexOf(&quot;?&quot;) &gt; -1 || headerText.indexOf(&quot;!&quot;) &gt; -1)) {
          header.setAttribute(&quot;id&quot;, i);
          
          var indexItem = document.createElement(&quot;li&quot;)
          var indexLink = document.createElement(&quot;a&quot;)
          indexLink.setAttribute(&quot;style&quot;, &quot;color: black; line-height: 2;&quot;);
          indexLink.setAttribute(&quot;href&quot;, &quot;#&quot; + i);

          indexItem.appendChild(indexLink);
          indexLink.innerText = headerText;
		  if (header.tagName == 'H2') {
			indexItem.setAttribute(&quot;style&quot;, &quot;margin-left: 12px; list-style: circle;&quot;);
		  }
		  if (header.tagName == 'H3') {
			indexItem.setAttribute(&quot;style&quot;, &quot;margin-left: 24px; list-style: disc;&quot;);
		  }          
		  if (header.tagName == 'H4') {
			indexItem.setAttribute(&quot;style&quot;, &quot;margin-left: 36px; list-style: disclosure-closed;&quot;);
		  }                    
          indexBox.appendChild(indexItem)

        }

      }

      indexer.parentElement.insertBefore(indexBox, indexer);
      indexer.parentElement.removeChild(indexer)
      indexBox.parentElement.insertBefore(indexer, indexBox)


      var progressBar = document.createElement(&quot;div&quot;)
	  progressBar.setAttribute(&quot;class&quot;, &quot;progress&quot;)
      progressBar.setAttribute(&quot;style&quot;, &quot;height: 5px; width: 0px; position: fixed; top: 0; left : 0; background-color: red; z-index: 100000;&quot;)
      document.body.insertBefore(progressBar, document.body.firstChild);
      $(document).scroll(function (e) {
		var scrollAmount = $(window).scrollTop();
		var documentHeight = $(document).height();
		var windowHeight = $(window).height();
		var scrollPercent = (scrollAmount / (documentHeight - windowHeight)) * 100;
		var roundScroll = Math.round(scrollPercent);
        progressBar.setAttribute(&quot;style&quot;, &quot;height: 5px; width: &quot; + scrollPercent + &quot;vw; position: fixed; top: 0; left : 0; background-color: red;  z-index: 100000;&quot;)
	  });
      
      
      $(&quot;div[data-ke-type='moreLess']&quot;)
      .css(&quot;margin&quot;, &quot;20px 0px&quot;)
      .css(&quot;background-color&quot;, &quot;#fafafa&quot;)
      .css(&quot;border&quot;, &quot;1px dashed #c5c5c5&quot;)
      .css(&quot;color&quot;, &quot;#333333&quot;)
      
      $(&quot;.btn-toggle-moreless&quot;)
      .css(&quot;width&quot;, &quot;100%&quot;).css(&quot;height&quot;, &quot;100%&quot;).css(&quot;display&quot;, &quot;block&quot;).css(&quot;padding&quot;, &quot;20px 20px 22px&quot;)
	  .text(&quot;열어보기&quot;)
  
      $(&quot;.btn-toggle-moreless&quot;).each((i, tag) =&gt; {
	    var text = $(tag).next().text().substring(0, 50).replaceAll(&quot;\n&quot;, &quot;&quot;)
        $(tag).text(&quot; 열어보기 &quot; + text + &quot; ... ... &quot;);
      });
      
      
    }, 500);  
  
&lt;/script&gt;</description>
      <category>Flink</category>
      <category>AddSplitsTask</category>
      <category>FetchTask</category>
      <category>flink</category>
      <category>RemoveSplitsTask</category>
      <category>split</category>
      <category>SplitFetcher</category>
      <category>SplitFetcherManager</category>
      <category>SplitsFetcherTask</category>
      <author>코딩수집가</author>
      <guid isPermaLink="true">https://westlife0615.tistory.com/1174</guid>
      <comments>https://westlife0615.tistory.com/1174#entry1174comment</comments>
      <pubDate>Tue, 4 Feb 2025 05:58:04 +0900</pubDate>
    </item>
    <item>
      <title>[Flink] KafkaSource 의 KafkaPartitionSplit 알아보기</title>
      <link>https://westlife0615.tistory.com/1173</link>
      <description>&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;- 목차&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Flink 의 KafkaSource Connector 내부에서 어떠한 방식으로 KafkaPartitionSplit 이 생성되고 소비되는지 자세한 동작과 라이프사이클을 알아봅니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;KafkaPartitionSplit&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(org.apache.flink.connector.kafka.source.split.KafkaPartitionSplit)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KafkaPartitionSplit 은 Flink KafkaSource 에서 사용되는 SourceSplit 입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SourceSplit 은 Flink 에서 데이터 조회를 위해서 쓰이는 하나의 단위입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flink 의 병렬 처리를 위해서 각각의 읽어들일 데이터들을 하나의 단위 또는 묶음으로써 표현할 수 있어야하며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KafkaPartitionSplit Class 가 KafkaSource 에서 사용되는 단위입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KafkaPartitionSplit 클래스의 Field 와 Method Signiture 목록은 아래와 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KafkaPartitionSplit 은 Topic/Partition 단위로써 구분됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 아래와 같이 KafkaPartitionSplit 은 Topic 과 Partition 정보를 내장하며, Start/End Offset 정보를 포함합니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1755919211405&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// org.apache.flink.connector.kafka.source.split.KafkaPartitionSplit

// 상수 (Constants)
public static final long NO_STOPPING_OFFSET = Long.MIN_VALUE;
@Deprecated public static final long LATEST_OFFSET = -1;
public static final long EARLIEST_OFFSET = -2;
public static final long COMMITTED_OFFSET = -3;

public static final Set&amp;lt;Long&amp;gt; VALID_STARTING_OFFSET_MARKERS;
public static final Set&amp;lt;Long&amp;gt; VALID_STOPPING_OFFSET_MARKERS;

// 인스턴스 필드 (Instance Fields)
private final TopicPartition tp;
private final long startingOffset;
private final long stoppingOffset;

public KafkaPartitionSplit(TopicPartition tp, long startingOffset)
public KafkaPartitionSplit(TopicPartition tp, long startingOffset, long stoppingOffset)
public String getTopic()
public int getPartition()
public TopicPartition getTopicPartition()

public long getStartingOffset()
public Optional&amp;lt;Long&amp;gt; getStoppingOffset()

@Override
public String splitId()

private static void verifyInitialOffset(TopicPartition tp, Long startingOffset, long stoppingOffset)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 생성된 KafkaPartitionSplit 은 다음과 같은 모습으로 표현됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FromOffset 와 EndOffset 의 Range 를 적용하여 범위 읽기 방식과 Endless 방식의 범위를 설정할 수가 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 일반적으로 Earliest, Latest, 또는 From Committed Offset 과 같은 세가지 방식이 사용되고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EndOffset 없이 무한히 Kafka Topic 를 조회하는 방식으로 KafkaPartitionSplit 이 사용됩니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1755919557860&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    public KafkaPartitionSplit(TopicPartition tp, long startingOffset) {
        this(tp, startingOffset, NO_STOPPING_OFFSET);
    }

    public KafkaPartitionSplit(TopicPartition tp, long startingOffset, long stoppingOffset) {
        verifyInitialOffset(tp, startingOffset, stoppingOffset);
        this.tp = tp;
        this.startingOffset = startingOffset;
        this.stoppingOffset = stoppingOffset;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;KafkaPartitionSplitState.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KafkaPartitionSplitState 는 KafkaSource 가 TopicPartition 으로부터 조회한 데이터의 Offset 을 저장하기 위한 State 클래스입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관련된 Field 와 Method 들은 아래와 같으며, 가장 중요한 Field 는 currentOffset 입니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1755921125382&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 상속받은 필드들 (KafkaPartitionSplit에서)
private final TopicPartition tp;           // Kafka Topic과 Partition 정보
private final long startingOffset;        // 시작 offset
private final long stoppingOffset;        // 끝 offset

// 추가된 필드
private long currentOffset;               // 현재 읽고 있는 offset (가변)

public long getCurrentOffset()
public void setCurrentOffset(long currentOffset)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KafkaPartitionSplitState 는 Polling 되는 Kafka Record 의 Offset 을 기록하기 위해서 사용되며, Record by Record 기준으로 레코드가 처리될 때마다 KafkaPartitionSplitState 의 currentOffset 은 1씩 단조 증가하게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flink 의 DataStreamSource 는 내부적으로 RecordEmitter 가 동작하게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RecordEmitter 는 개별 레코드를 역직렬화하고, State 를 업데이트하는 역할을 수행합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KafkaSource 는 KafkaRecordEmitter 라는 클래스가 RecordEmitter 를 구현하여 활용되며,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fully Qualified Name 은 org.apache.flink.connector.kafka.source.reader.KafkaRecordEmitter 와 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 emitRecord 함수는 KafkaRecordEmitter 클래스의 emitRecord 함수입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 역직렬화와 KafkaPartitionSplitState 의 CurrentOffset 의 갱신이 이루어집니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1755922150915&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// org.apache.flink.connector.kafka.source.reader.KafkaRecordEmitter

    @Override
    public void emitRecord(
            ConsumerRecord&amp;lt;byte[], byte[]&amp;gt; consumerRecord,
            SourceOutput&amp;lt;T&amp;gt; output,
            KafkaPartitionSplitState splitState)
            throws Exception {
        try {
            sourceOutputWrapper.setSourceOutput(output);
            sourceOutputWrapper.setTimestamp(consumerRecord.timestamp());
            deserializationSchema.deserialize(consumerRecord, sourceOutputWrapper);
            splitState.setCurrentOffset(consumerRecord.offset() + 1);
        } catch (Exception e) {
            throw new IOException(&quot;Failed to deserialize consumer record due to&quot;, e);
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;KafkaSourceEnumerator.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KafkaPartitionSplit 은 Flink 의 KafkaSource 가 데이터를 조회하기 위한 단위를 의미하죠.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 KafkaPartitionSplit 은 Topic/Partition 과 일대일로 매칭되는 개념으로 볼 수도 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KafkaSourceEnumerator 는 이러한 KafkaPartitionSplit 을 생성하는 Flink Source 의 구성요소 중의 하나입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 initializePartitionSplits 함수는 KafkaSourceEnumerator 가 최초로 KafkaPartitionSplit 을 생성하는 함수입니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1755922416373&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 4. KafkaPartitionSplit 생성
private PartitionSplitChange initializePartitionSplits(PartitionChange partitionChange) {
    Set&amp;lt;TopicPartition&amp;gt; newPartitions = partitionChange.getNewPartitions();
    
    // Starting Offset 결정
    Map&amp;lt;TopicPartition, Long&amp;gt; startingOffsets = startingOffsetInitializer
        .getPartitionOffsets(newPartitions, offsetsRetriever);
    
    // Stopping Offset 결정
    Map&amp;lt;TopicPartition, Long&amp;gt; stoppingOffsets = stoppingOffsetInitializer
        .getPartitionOffsets(newPartitions, offsetsRetriever);
    
    // KafkaPartitionSplit 생성
    Set&amp;lt;KafkaPartitionSplit&amp;gt; partitionSplits = new HashSet&amp;lt;&amp;gt;();
    for (TopicPartition tp : newPartitions) {
        Long startingOffset = startingOffsets.get(tp);
        long stoppingOffset = stoppingOffsets.getOrDefault(tp, KafkaPartitionSplit.NO_STOPPING_OFFSET);
        
        // 여기서 실제 KafkaPartitionSplit 생성!
        partitionSplits.add(new KafkaPartitionSplit(tp, startingOffset, stoppingOffset));
    }
    
    return new PartitionSplitChange(partitionSplits, partitionChange.getRemovedPartitions());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;KafkaSourceEnumerator 는 Push 방식으로 SourceReader 에게 Split 을 제공.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flink 의 DataSource 의 SplitEnumerator 와 SplitReader 는 Push 와 Pull 방식으로 Split 을 주고 받을 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 KafkaSourceEnumerator 는 Push 방식으로 KafkaPartitionSplitReader 에게 KafkaPartitionSplit 을 제공합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 이유의 배경에는 KafkaPartitionSplit 은 그 갯수가 적고 KafkaSourceEnumerator 의 초기화 시점에 생성된 KafkaPartitionSplit 을 바로 Reader 에게 제공할 수 있기 때문입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KafkaPartitionSplit 은 기본적으로 Endless 방식 동작하기 때문에 하나의 Split 이 종료 제한없이 사용됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 KafkaPartitionSplitReader 가 KafkaSourceEnumerator 에게 Split 을 요구하는 상황이 없습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 KafkaSourceEnumerator 는 Push 방식으로 SourceReader 에게 KafkaPartitionSplit 을 제공하게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 handlePartitionSplitChanges 함수는 KafkaSourceEnumerator 가 초기화 시점에 호출하는 함수이며,&amp;nbsp;&lt;br /&gt;assignPendingPartitionSplits 함수를 호출하여 SourceReader 에게 Pending Split 들을 Push 하게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1755923024927&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    private void handlePartitionSplitChanges(
            PartitionSplitChange partitionSplitChange, Throwable t) {
        if (t != null) {
            throw new FlinkRuntimeException(&quot;Failed to initialize partition splits due to &quot;, t);
        }
        if (partitionDiscoveryIntervalMs &amp;lt;= 0) {
            LOG.debug(&quot;Partition discovery is disabled.&quot;);
            noMoreNewPartitionSplits = true;
        }
        // TODO: Handle removed partitions.
        addPartitionSplitChangeToPendingAssignments(partitionSplitChange.newPartitionSplits);
        assignPendingPartitionSplits(context.registeredReaders().keySet());
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;KafkaPartitionSplitReader.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KafkaPartitionSplitReader 는 Enumerator 에게서 전달받은 KafkaPartitionSplit 에 작성된 메타데이터를 기반으로 실질적인 데이터를 조회하는 동작을 수행합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 동작을 위해서 내부적으로 KafkaConsumer 를 가지며, Polling 을 수행하게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KafkaPartitionSplitReader 의 Fully Qualified Name 은 org.apache.flink.connector.kafka.source.reader.KafkaPartitionSplitReader 입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 KafkaPartitionSplitReader 의 Field 목록입니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1755923182714&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// org.apache.flink.connector.kafka.source.reader.KafkaPartitionSplitReader
private final KafkaConsumer&amp;lt;byte[], byte[]&amp;gt; consumer;           // Kafka Consumer 인스턴스
private final Map&amp;lt;TopicPartition, Long&amp;gt; stoppingOffsets;        // 각 Partition의 종료 Offset
private final String groupId;                                   // Consumer Group ID
private final int subtaskId;                                    // Flink Subtask ID
private final KafkaSourceReaderMetrics kafkaSourceReaderMetrics; // 메트릭 수집기&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 KafkaPartitionSplitReader 의 handleSplitsChanges 함수에서 Enumerator 로부터 전달받은 KafkaPartitionSplit 을 통해 Kafka Consumer 를 초기화하게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1755923480628&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Override
    public void handleSplitsChanges(SplitsChange&amp;lt;KafkaPartitionSplit&amp;gt; splitsChange) {
        // Get all the partition assignments and stopping offsets.
        if (!(splitsChange instanceof SplitsAddition)) {
            throw new UnsupportedOperationException(
                    String.format(
                            &quot;The SplitChange type of %s is not supported.&quot;,
                            splitsChange.getClass()));
        }

        // Assignment.
        List&amp;lt;TopicPartition&amp;gt; newPartitionAssignments = new ArrayList&amp;lt;&amp;gt;();
        // Starting offsets.
        Map&amp;lt;TopicPartition, Long&amp;gt; partitionsStartingFromSpecifiedOffsets = new HashMap&amp;lt;&amp;gt;();
        List&amp;lt;TopicPartition&amp;gt; partitionsStartingFromEarliest = new ArrayList&amp;lt;&amp;gt;();
        List&amp;lt;TopicPartition&amp;gt; partitionsStartingFromLatest = new ArrayList&amp;lt;&amp;gt;();
        // Stopping offsets.
        List&amp;lt;TopicPartition&amp;gt; partitionsStoppingAtLatest = new ArrayList&amp;lt;&amp;gt;();
        Set&amp;lt;TopicPartition&amp;gt; partitionsStoppingAtCommitted = new HashSet&amp;lt;&amp;gt;();

        // Parse the starting and stopping offsets.
        splitsChange
                .splits()
                .forEach(
                        s -&amp;gt; {
                            newPartitionAssignments.add(s.getTopicPartition());
                            parseStartingOffsets(
                                    s,
                                    partitionsStartingFromEarliest,
                                    partitionsStartingFromLatest,
                                    partitionsStartingFromSpecifiedOffsets);
                            parseStoppingOffsets(
                                    s, partitionsStoppingAtLatest, partitionsStoppingAtCommitted);
                            // Track the new topic partition in metrics
                            kafkaSourceReaderMetrics.registerTopicPartition(s.getTopicPartition());
                        });

        // Assign new partitions.
        newPartitionAssignments.addAll(consumer.assignment());
        consumer.assign(newPartitionAssignments);

        // Seek on the newly assigned partitions to their stating offsets.
        seekToStartingOffsets(
                partitionsStartingFromEarliest,
                partitionsStartingFromLatest,
                partitionsStartingFromSpecifiedOffsets);
        // Setup the stopping offsets.
        acquireAndSetStoppingOffsets(partitionsStoppingAtLatest, partitionsStoppingAtCommitted);

        // After acquiring the starting and stopping offsets, remove the empty splits if necessary.
        removeEmptySplits();

        maybeLogSplitChangesHandlingResult(splitsChange);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 KafkaConsumer 와 group.id 를 기준으로 Record Polling 을 수행하게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1755923297356&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
public RecordsWithSplitIds&amp;lt;ConsumerRecord&amp;lt;byte[], byte[]&amp;gt;&amp;gt; fetch() throws IOException {
    ConsumerRecords&amp;lt;byte[], byte[]&amp;gt; consumerRecords;
    try {
        consumerRecords = consumer.poll(Duration.ofMillis(POLL_TIMEOUT));
    }
    // .. 생략
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;script&gt;

    setTimeout(function() {
      var elementList = document.querySelectorAll(&quot;h2&quot;);
      var indexer = null;
      for (var i = 0; i &lt; elementList.length; i++) {
          if (elementList[i].innerText.indexOf(&quot;- 목차&quot;) &gt; -1) {
              indexer = elementList[i]    
          }
          
      }
      var headerList = document.querySelectorAll(&quot;h2,h3,h4&quot;);
	  var allHeaderList = [...headerList]
      var indexBox = document.createElement(&quot;ul&quot;);
      for (var i = 0; i &lt; allHeaderList.length; i++) {
        var header = allHeaderList[i]; 
        var headerText = header.innerText
        if (headerText != null &amp;&amp; (headerText.indexOf(&quot;*&quot;) &gt; -1 || headerText.indexOf(&quot;.&quot;) &gt; -1 || headerText.indexOf(&quot;;&quot;) &gt; -1 || headerText.indexOf(&quot;?&quot;) &gt; -1 || headerText.indexOf(&quot;!&quot;) &gt; -1)) {
          header.setAttribute(&quot;id&quot;, i);
          
          var indexItem = document.createElement(&quot;li&quot;)
          var indexLink = document.createElement(&quot;a&quot;)
          indexLink.setAttribute(&quot;style&quot;, &quot;color: black; line-height: 2;&quot;);
          indexLink.setAttribute(&quot;href&quot;, &quot;#&quot; + i);

          indexItem.appendChild(indexLink);
          indexLink.innerText = headerText;
		  if (header.tagName == 'H2') {
			indexItem.setAttribute(&quot;style&quot;, &quot;margin-left: 12px; list-style: circle;&quot;);
		  }
		  if (header.tagName == 'H3') {
			indexItem.setAttribute(&quot;style&quot;, &quot;margin-left: 24px; list-style: disc;&quot;);
		  }          
		  if (header.tagName == 'H4') {
			indexItem.setAttribute(&quot;style&quot;, &quot;margin-left: 36px; list-style: disclosure-closed;&quot;);
		  }                    
          indexBox.appendChild(indexItem)

        }

      }

      indexer.parentElement.insertBefore(indexBox, indexer);
      indexer.parentElement.removeChild(indexer)
      indexBox.parentElement.insertBefore(indexer, indexBox)


      var progressBar = document.createElement(&quot;div&quot;)
	  progressBar.setAttribute(&quot;class&quot;, &quot;progress&quot;)
      progressBar.setAttribute(&quot;style&quot;, &quot;height: 5px; width: 0px; position: fixed; top: 0; left : 0; background-color: red; z-index: 100000;&quot;)
      document.body.insertBefore(progressBar, document.body.firstChild);
      $(document).scroll(function (e) {
		var scrollAmount = $(window).scrollTop();
		var documentHeight = $(document).height();
		var windowHeight = $(window).height();
		var scrollPercent = (scrollAmount / (documentHeight - windowHeight)) * 100;
		var roundScroll = Math.round(scrollPercent);
        progressBar.setAttribute(&quot;style&quot;, &quot;height: 5px; width: &quot; + scrollPercent + &quot;vw; position: fixed; top: 0; left : 0; background-color: red;  z-index: 100000;&quot;)
	  });
      
      
      $(&quot;div[data-ke-type='moreLess']&quot;)
      .css(&quot;margin&quot;, &quot;20px 0px&quot;)
      .css(&quot;background-color&quot;, &quot;#fafafa&quot;)
      .css(&quot;border&quot;, &quot;1px dashed #c5c5c5&quot;)
      .css(&quot;color&quot;, &quot;#333333&quot;)
      
      $(&quot;.btn-toggle-moreless&quot;)
      .css(&quot;width&quot;, &quot;100%&quot;).css(&quot;height&quot;, &quot;100%&quot;).css(&quot;display&quot;, &quot;block&quot;).css(&quot;padding&quot;, &quot;20px 20px 22px&quot;)
	  .text(&quot;열어보기&quot;)
  
      $(&quot;.btn-toggle-moreless&quot;).each((i, tag) =&gt; {
	    var text = $(tag).next().text().substring(0, 50).replaceAll(&quot;\n&quot;, &quot;&quot;)
        $(tag).text(&quot; 열어보기 &quot; + text + &quot; ... ... &quot;);
      });
      
      
    }, 500);  
  
&lt;/script&gt;</description>
      <category>Flink</category>
      <category>flink</category>
      <category>kafka</category>
      <category>Kafka consumer</category>
      <category>KafkaPartitionSplitReader</category>
      <category>KafkaPartitionSplits</category>
      <category>KafkaPartitionSplitState</category>
      <category>KafkaRecordEmitter</category>
      <category>KafkaSource</category>
      <category>KafkaSourceEnumerator</category>
      <category>SplitEnumerator</category>
      <author>코딩수집가</author>
      <guid isPermaLink="true">https://westlife0615.tistory.com/1173</guid>
      <comments>https://westlife0615.tistory.com/1173#entry1173comment</comments>
      <pubDate>Tue, 4 Feb 2025 05:23:35 +0900</pubDate>
    </item>
    <item>
      <title>[Ai-ML] Transformer Multi-Head Attention 알아보기</title>
      <link>https://westlife0615.tistory.com/1170</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;- 목차&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Multi-head Attention 이란 ?&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Multi-Head Attention 은 Transformer 의 핵심 구성 요소 중의 하나입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Self-Attention 만으로도 시퀀스 내에서 각 Time Step 이 다른 Time Step 사이에 어떠한 영향력이 있는지 계산할 수 있지만,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 Self-Attention 은 하나의 고정된 시각에서만 학습을 수행하게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 다양한 패턴을 포착하는데에 한계가 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 여러 개의 서로 다른 Attention Head 를 구성해서 병렬적으로 사용하는 구조입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 Attention Head 는 서로 다른 Query, Key, Value 를 생성합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 각 Attention head 가 시퀀스 내에서 서로 다른 관점으로 Attention 을 수행하기를 기대합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 Head 는 a,b,c feature 를 잘 대변한다던지 또 다른 Head 는 가까운 Time Step 간의 관계를 잘 파악한다던지 와 같이&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다양한 Multi-Head Attention 을 통해서 이러한 다양성을 잘 포착할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Multi-Head Attention 과 Concatenation.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러개의 Attention Head 는 서로 독립적인 Attention Output 를 생성합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 되면 Head 의 갯수만큼 Attention Output 이 생성되는데, Transformer 의 출력 Shape 와는 맞지 않습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Transformer 는 Multi-Head Attention 의 각 Attention Output 을 Concat 을 취하고,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Output Size 만큼 DenseNet Layer 로 Projection 처리하여 Output Size 로 출력하게 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bagging Model 처럼 여러 Sub Model 이나 Layer 들이 사용되는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Transformer 는 Bagging 과 같이 평균을 내는 Aggregation 을 취하지 않습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 대신 모든 Attention Head 의 Output 들을 한데 모아 선형 변환을 적용합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 함으로써 정보의 손실이나 희석되는 상황이 발생하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 Multi-Head Attention 과 Concatenation, Projection 구조를 통해서 다양한 패턴을 효과적으로 학습할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;각 Head 의 독립적인 Weight (W_q, W_k, W_v).&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Attention Head 는 3가지 가중치를 가집니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;익히 아시다시피 Query, Key, Value 을 Projection 하기 위한 Weight 가 존재합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 Weight 들은 Input Data 를 각기 다른 시각에서 입력 데이터를 바라보고 새로운 Query, Key, Value vector 를 생성합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 각 Head 에 해당하는 가중치들은 서로 다른 값으로 초기화되어야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 모든 Head 들이 동일한 W_q, W_k, W_v 로 초기화된다면, 각 Head 는 동일한 Attention Map 을 계산하게 되어 Multi-Head Attention 효과가 사라집니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Attention Head 는 서로 다른 초기값을 가지게 되고, 이에 따라서 서로 다른 Query, Key, Value 로 선형변환되게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Tree 의 Bagging 이나 Boost 계열의 모델을 살펴보면 각 Sub Tree Model 이 서로 다른 Tree 구성을 가지게끔 트릭을 활용합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 Multi-Head Attention 또한 각 Head 별 가중치가 다양할 수 있도록 설정하는 것이 Transformer 초기화의 첫걸음이 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Head 별 병렬 처리.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Attention Head 는 개별적으로 또는 병렬적으로 학습이 가능합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유는 동일한 Input Data 를 대상으로 독립적인 가중치로 선형 변환을 취할 수 있으니, 이는 현실적인 병렬 처리가 가능합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 각 Head 별 서로 간섭없이 동시에 계산을 할 수 있는거죠. 이는 Transformer 가 효율적인 처리가 가능한 이유입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로는 각 Head 의 Attention Output 이 단순히 사용되는 것이 아니라 모든 Head 의 출력들을 Concat 처리한 후에&amp;nbsp;다시 Output Size 로 Projection 을 취하게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;script&gt;

    setTimeout(function() {
      var elementList = document.querySelectorAll(&quot;h2&quot;);
      var indexer = null;
      for (var i = 0; i &lt; elementList.length; i++) {
          if (elementList[i].innerText.indexOf(&quot;- 목차&quot;) &gt; -1) {
              indexer = elementList[i]    
          }
          
      }
      var headerList = document.querySelectorAll(&quot;h2,h3,h4&quot;);
	  var allHeaderList = [...headerList]
      var indexBox = document.createElement(&quot;ul&quot;);
      for (var i = 0; i &lt; allHeaderList.length; i++) {
        var header = allHeaderList[i]; 
        var headerText = header.innerText
        if (headerText != null &amp;&amp; (headerText.indexOf(&quot;*&quot;) &gt; -1 || headerText.indexOf(&quot;.&quot;) &gt; -1 || headerText.indexOf(&quot;;&quot;) &gt; -1 || headerText.indexOf(&quot;?&quot;) &gt; -1 || headerText.indexOf(&quot;!&quot;) &gt; -1)) {
          header.setAttribute(&quot;id&quot;, i);
          
          var indexItem = document.createElement(&quot;li&quot;)
          var indexLink = document.createElement(&quot;a&quot;)
          indexLink.setAttribute(&quot;style&quot;, &quot;color: black; line-height: 2;&quot;);
          indexLink.setAttribute(&quot;href&quot;, &quot;#&quot; + i);

          indexItem.appendChild(indexLink);
          indexLink.innerText = headerText;
		  if (header.tagName == 'H2') {
			indexItem.setAttribute(&quot;style&quot;, &quot;margin-left: 12px; list-style: circle;&quot;);
		  }
		  if (header.tagName == 'H3') {
			indexItem.setAttribute(&quot;style&quot;, &quot;margin-left: 24px; list-style: disc;&quot;);
		  }          
		  if (header.tagName == 'H4') {
			indexItem.setAttribute(&quot;style&quot;, &quot;margin-left: 36px; list-style: disclosure-closed;&quot;);
		  }                    
          indexBox.appendChild(indexItem)

        }

      }

      indexer.parentElement.insertBefore(indexBox, indexer);
      indexer.parentElement.removeChild(indexer)
      indexBox.parentElement.insertBefore(indexer, indexBox)


      var progressBar = document.createElement(&quot;div&quot;)
	  progressBar.setAttribute(&quot;class&quot;, &quot;progress&quot;)
      progressBar.setAttribute(&quot;style&quot;, &quot;height: 5px; width: 0px; position: fixed; top: 0; left : 0; background-color: red; z-index: 100000;&quot;)
      document.body.insertBefore(progressBar, document.body.firstChild);
      $(document).scroll(function (e) {
		var scrollAmount = $(window).scrollTop();
		var documentHeight = $(document).height();
		var windowHeight = $(window).height();
		var scrollPercent = (scrollAmount / (documentHeight - windowHeight)) * 100;
		var roundScroll = Math.round(scrollPercent);
        progressBar.setAttribute(&quot;style&quot;, &quot;height: 5px; width: &quot; + scrollPercent + &quot;vw; position: fixed; top: 0; left : 0; background-color: red;  z-index: 100000;&quot;)
	  });
      
      
      $(&quot;div[data-ke-type='moreLess']&quot;)
      .css(&quot;margin&quot;, &quot;20px 0px&quot;)
      .css(&quot;background-color&quot;, &quot;#fafafa&quot;)
      .css(&quot;border&quot;, &quot;1px dashed #c5c5c5&quot;)
      .css(&quot;color&quot;, &quot;#333333&quot;)
      
      $(&quot;.btn-toggle-moreless&quot;)
      .css(&quot;width&quot;, &quot;100%&quot;).css(&quot;height&quot;, &quot;100%&quot;).css(&quot;display&quot;, &quot;block&quot;).css(&quot;padding&quot;, &quot;20px 20px 22px&quot;)
	  .text(&quot;열어보기&quot;)
  
      $(&quot;.btn-toggle-moreless&quot;).each((i, tag) =&gt; {
	    var text = $(tag).next().text().substring(0, 50).replaceAll(&quot;\n&quot;, &quot;&quot;)
        $(tag).text(&quot; 열어보기 &quot; + text + &quot; ... ... &quot;);
      });
      
      
    }, 500);  
  
&lt;/script&gt;</description>
      <category>AI-ML</category>
      <category>Attention</category>
      <category>attention map</category>
      <category>bagging</category>
      <category>concatenation</category>
      <category>Key</category>
      <category>multi-head attention</category>
      <category>Query</category>
      <category>self-attention</category>
      <category>Transformer</category>
      <category>어텐션</category>
      <author>코딩수집가</author>
      <guid isPermaLink="true">https://westlife0615.tistory.com/1170</guid>
      <comments>https://westlife0615.tistory.com/1170#entry1170comment</comments>
      <pubDate>Sun, 2 Feb 2025 08:34:28 +0900</pubDate>
    </item>
    <item>
      <title>[Ai-ML] Attention Query 와 Key Vector 알아보기</title>
      <link>https://westlife0615.tistory.com/1168</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;- 목차&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Attention 은 Input Data 를 3개의 서로 다른 공간으로 투영시키는 과정이 수행됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기하학적으로 바라본다면 Input Dimension 에서 Query Dim, Key Dim, Value Dim 에 해당하는 다른 공간으로 Projection 되게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 행위를 압축이라고 볼 수도 있을 것 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무튼 Query, Key, Value 라는 이름붙인 새로운 형태로 Input Data 는 새로운 Vector 로 변환되게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 변환된 Query 와 Key Vector 는 서로 간의 유사도를 계산하는데 사용됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Self-Attention 에서는 각 Query Vector 가 모든 Sequence 의 Key Vector 들과 얼마나 유사한지를 측정하게 되는데요.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 과정을 통해서 각 Time Step 별 유사도를 표현하는 Matrix 를 만들 수 있게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Input Data 가 어떻게 Key 와 Query 공간으로 Projection 되는지 그리고 어떻게 유사도를 계산하고,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Attention 내부에서 이 유사도를 의미를 직관적으로 알아보도록 하겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Query 와 Key 의 직관적인 의미.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러가지 문서들을 확인해보면 Query Vector 와 Key Vector 는 Input Time-series Data 의 특성 Time Step 의 &quot;어떤 정보를 필요로하는가?&quot;, &quot;어떤 정보를 포함하는가?&quot; 와 같이 인위적인 의미를 부여하는데,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 표현은 잘 와닿지 않고, 그냥 Input Data 를 선형적으로 투영시킨 결과입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Input Data 를 두가지 방식으로 Linear Projection 시키는데, 첫번째 투영 결과를 Query 로 부르고 두번째 투영의 결과를 Key 라고 부른다고 이해하면 좋을 것 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Input Time-series Data 의 특정 Time Step 을 두가지 공간으로 투영시켜서 다양하게 표현한다고 생각할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습 과정에서 Input Time-series Data 는 Query 와 Key 에 해당하는 두가지 Vector 로 변환되는데, Query 와 Key 가 Input Data 의 특정 Time Step 을 적절히 표현할 수 있도록 학습되게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Query 와 Key Vector 의 유사도를 계산하여 Score Matrix 를 만들며, 이 결과는 Time Step 과 Time Step 사이의 유사도를 표현하게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Attention 은 시계열 데이터에서 어떤 시점이 어떠한 다른 시점들에게 영향을 주는지를 Score Matrix 를 통해서 확인할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것은 여러 feature 사이의 Correlation 를 측정하듯이 Time Step 간의 유사도를 표현합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Query 와 Key 의 유사도 계산.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Query 와 Key Vector 는 Dot Product 를 기반으로 유사도를 계산합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구체적인 수식은 아래와 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$\text{score}(i, j) = \frac{1}{\sqrt{d_k}} \, Q_i \cdot K_j^\mathrm{T}$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Q_i 는 i 번째 Time Step 의 Query Vector 이고,&amp;nbsp;K_j 는 j 번째 Time Step 의 Key Vector 입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 d_k 는 Key Vector 의 차원수를 의미합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cosine Similarity 가 Inner Product 를 사용하듯이 Query 와 Key 사이의 유사도 계산에서 Inner Product 가 활용됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 Dot Product 결과를 나누는 스케일의 값이 다르다는 차이가 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 각 Time Step 별 유사도가 계산되면 아래의 도표와 같이 표현될 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 도표에서 Query 4 와 Key 9 의 유사도가 가장 높이 측정되는데,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 &quot;I saw a cute cat sitting on the mat&quot; 이라고 할때에 4 번 Query 인 &lt;b&gt;&quot;cat&quot;&lt;/b&gt; 이 9번째 Key 인 &lt;b&gt;&quot;mat&quot; &lt;/b&gt;이 큰 관련이 있다는 것을 의미합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1154&quot; data-origin-height=&quot;1101&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ymxd7/btsOJjw28Sv/frDRbBp4J6A3qYmZdbin1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ymxd7/btsOJjw28Sv/frDRbBp4J6A3qYmZdbin1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ymxd7/btsOJjw28Sv/frDRbBp4J6A3qYmZdbin1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYmxd7%2FbtsOJjw28Sv%2FfrDRbBp4J6A3qYmZdbin1k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;446&quot; height=&quot;1101&quot; data-origin-width=&quot;1154&quot; data-origin-height=&quot;1101&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 방식으로 Key 와 Query Vector 를 통해서 Sequence Time Step 간의 관계를 파악할 수 있고,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 시계열 데이터 관계를 Attention 방식을 통해서 파악할 수 있게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Softmax Activation.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Query 와 Key Vector 사이의 계산된 유사도를 기반으로 Softmax Activation 을 거처 확률로써 표현됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Softmax 특성상 0 ~ 1 사이의 값으로 변환되게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$\alpha_{i,j} = \text{softmax}\left( \text{score}(i, j) \right)$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 Query i 가 Key j 에 얼마나 주의할지에 대한 확률로 바뀌게 되고, 이는 Attention 이라고 부르게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 각 Time Step 들 사이의 Attention 을 학습하게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;script&gt;

    setTimeout(function() {
      var elementList = document.querySelectorAll(&quot;h2&quot;);
      var indexer = null;
      for (var i = 0; i &lt; elementList.length; i++) {
          if (elementList[i].innerText.indexOf(&quot;- 목차&quot;) &gt; -1) {
              indexer = elementList[i]    
          }
          
      }
      var headerList = document.querySelectorAll(&quot;h2,h3,h4&quot;);
	  var allHeaderList = [...headerList]
      var indexBox = document.createElement(&quot;ul&quot;);
      for (var i = 0; i &lt; allHeaderList.length; i++) {
        var header = allHeaderList[i]; 
        var headerText = header.innerText
        if (headerText != null &amp;&amp; (headerText.indexOf(&quot;*&quot;) &gt; -1 || headerText.indexOf(&quot;.&quot;) &gt; -1 || headerText.indexOf(&quot;;&quot;) &gt; -1 || headerText.indexOf(&quot;?&quot;) &gt; -1 || headerText.indexOf(&quot;!&quot;) &gt; -1)) {
          header.setAttribute(&quot;id&quot;, i);
          
          var indexItem = document.createElement(&quot;li&quot;)
          var indexLink = document.createElement(&quot;a&quot;)
          indexLink.setAttribute(&quot;style&quot;, &quot;color: black; line-height: 2;&quot;);
          indexLink.setAttribute(&quot;href&quot;, &quot;#&quot; + i);

          indexItem.appendChild(indexLink);
          indexLink.innerText = headerText;
		  if (header.tagName == 'H2') {
			indexItem.setAttribute(&quot;style&quot;, &quot;margin-left: 12px; list-style: circle;&quot;);
		  }
		  if (header.tagName == 'H3') {
			indexItem.setAttribute(&quot;style&quot;, &quot;margin-left: 24px; list-style: disc;&quot;);
		  }          
		  if (header.tagName == 'H4') {
			indexItem.setAttribute(&quot;style&quot;, &quot;margin-left: 36px; list-style: disclosure-closed;&quot;);
		  }                    
          indexBox.appendChild(indexItem)

        }

      }

      indexer.parentElement.insertBefore(indexBox, indexer);
      indexer.parentElement.removeChild(indexer)
      indexBox.parentElement.insertBefore(indexer, indexBox)


      var progressBar = document.createElement(&quot;div&quot;)
	  progressBar.setAttribute(&quot;class&quot;, &quot;progress&quot;)
      progressBar.setAttribute(&quot;style&quot;, &quot;height: 5px; width: 0px; position: fixed; top: 0; left : 0; background-color: red; z-index: 100000;&quot;)
      document.body.insertBefore(progressBar, document.body.firstChild);
      $(document).scroll(function (e) {
		var scrollAmount = $(window).scrollTop();
		var documentHeight = $(document).height();
		var windowHeight = $(window).height();
		var scrollPercent = (scrollAmount / (documentHeight - windowHeight)) * 100;
		var roundScroll = Math.round(scrollPercent);
        progressBar.setAttribute(&quot;style&quot;, &quot;height: 5px; width: &quot; + scrollPercent + &quot;vw; position: fixed; top: 0; left : 0; background-color: red;  z-index: 100000;&quot;)
	  });
      
      
      $(&quot;div[data-ke-type='moreLess']&quot;)
      .css(&quot;margin&quot;, &quot;20px 0px&quot;)
      .css(&quot;background-color&quot;, &quot;#fafafa&quot;)
      .css(&quot;border&quot;, &quot;1px dashed #c5c5c5&quot;)
      .css(&quot;color&quot;, &quot;#333333&quot;)
      
      $(&quot;.btn-toggle-moreless&quot;)
      .css(&quot;width&quot;, &quot;100%&quot;).css(&quot;height&quot;, &quot;100%&quot;).css(&quot;display&quot;, &quot;block&quot;).css(&quot;padding&quot;, &quot;20px 20px 22px&quot;)
	  .text(&quot;열어보기&quot;)
  
      $(&quot;.btn-toggle-moreless&quot;).each((i, tag) =&gt; {
	    var text = $(tag).next().text().substring(0, 50).replaceAll(&quot;\n&quot;, &quot;&quot;)
        $(tag).text(&quot; 열어보기 &quot; + text + &quot; ... ... &quot;);
      });
      
      
    }, 500);  
  
&lt;/script&gt;</description>
      <category>AI-ML</category>
      <category>Attention</category>
      <category>dot product</category>
      <category>Key</category>
      <category>Query</category>
      <category>sequence</category>
      <category>similarity</category>
      <category>Transformer</category>
      <category>유사도</category>
      <author>코딩수집가</author>
      <guid isPermaLink="true">https://westlife0615.tistory.com/1168</guid>
      <comments>https://westlife0615.tistory.com/1168#entry1168comment</comments>
      <pubDate>Sat, 1 Feb 2025 09:22:21 +0900</pubDate>
    </item>
    <item>
      <title>[Ai-ML] Attention Weight 와 Value vector 알아보기</title>
      <link>https://westlife0615.tistory.com/1167</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;- 목차&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://westlife0615.tistory.com/1165&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://westlife0615.tistory.com/1165&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1750399810053&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Ai-ML] Transformer 와 Positional Encoding 알아보기&quot; data-og-description=&quot;- 목차 Positional Encoding 이란 ? Attention 에서 Input Data 는 Query, Key, Value 에 해당하는 3가지 형태로 Projection 됩니다. 일반적으로 Input Data 는 Sequence x Input Size 로 구성된 데이터이며, 이는 Sequence x Attn_Size&quot; data-og-host=&quot;westlife0615.tistory.com&quot; data-og-source-url=&quot;https://westlife0615.tistory.com/1165&quot; data-og-url=&quot;https://westlife0615.tistory.com/1165&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/hQ91L/hyY8YoWBrH/0HdTqsTokN8vV0BpGfMMIK/img.png?width=800&amp;amp;height=763&amp;amp;face=0_0_800_763,https://scrap.kakaocdn.net/dn/cRKl8F/hyY8QEqQpK/dvyQUu7UCijZnyab65Xx30/img.png?width=800&amp;amp;height=763&amp;amp;face=0_0_800_763,https://scrap.kakaocdn.net/dn/bbkSLH/hyY8WxT7hB/zxeNCbXKwlq2SV65rBWuPK/img.png?width=2317&amp;amp;height=1979&amp;amp;face=0_0_2317_1979&quot;&gt;&lt;a href=&quot;https://westlife0615.tistory.com/1165&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://westlife0615.tistory.com/1165&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/hQ91L/hyY8YoWBrH/0HdTqsTokN8vV0BpGfMMIK/img.png?width=800&amp;amp;height=763&amp;amp;face=0_0_800_763,https://scrap.kakaocdn.net/dn/cRKl8F/hyY8QEqQpK/dvyQUu7UCijZnyab65Xx30/img.png?width=800&amp;amp;height=763&amp;amp;face=0_0_800_763,https://scrap.kakaocdn.net/dn/bbkSLH/hyY8WxT7hB/zxeNCbXKwlq2SV65rBWuPK/img.png?width=2317&amp;amp;height=1979&amp;amp;face=0_0_2317_1979');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Ai-ML] Transformer 와 Positional Encoding 알아보기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;- 목차 Positional Encoding 이란 ? Attention 에서 Input Data 는 Query, Key, Value 에 해당하는 3가지 형태로 Projection 됩니다. 일반적으로 Input Data 는 Sequence x Input Size 로 구성된 데이터이며, 이는 Sequence x Attn_Size&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;westlife0615.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://westlife0615.tistory.com/1168&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://westlife0615.tistory.com/1168&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1750399809475&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Ai-ML] Attention Query 와 Key Vector 알아보기&quot; data-og-description=&quot;- 목차 들어가며.Attention 은 Input Data 를 3개의 서로 다른 공간으로 투영시키는 과정이 수행됩니다. 기하학적으로 바라본다면 Input Dimension 에서 Query Dim, Key Dim, Value Dim 에 해당하는 다른 공간으로 Pr&quot; data-og-host=&quot;westlife0615.tistory.com&quot; data-og-source-url=&quot;https://westlife0615.tistory.com/1168&quot; data-og-url=&quot;https://westlife0615.tistory.com/1168&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/M0ICZ/hyY8XcufXt/q5x0J3znUrOTEJbkJJLGyK/img.png?width=800&amp;amp;height=763&amp;amp;face=0_0_800_763,https://scrap.kakaocdn.net/dn/m0BER/hyY76N9BDR/kcRYcS4Tg60RNrCc2dBKUk/img.png?width=800&amp;amp;height=763&amp;amp;face=0_0_800_763,https://scrap.kakaocdn.net/dn/csQDNY/hyY8a37jFe/fkumzQpnfZjsbOg7jSpw3K/img.png?width=1154&amp;amp;height=1101&amp;amp;face=0_0_1154_1101&quot;&gt;&lt;a href=&quot;https://westlife0615.tistory.com/1168&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://westlife0615.tistory.com/1168&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/M0ICZ/hyY8XcufXt/q5x0J3znUrOTEJbkJJLGyK/img.png?width=800&amp;amp;height=763&amp;amp;face=0_0_800_763,https://scrap.kakaocdn.net/dn/m0BER/hyY76N9BDR/kcRYcS4Tg60RNrCc2dBKUk/img.png?width=800&amp;amp;height=763&amp;amp;face=0_0_800_763,https://scrap.kakaocdn.net/dn/csQDNY/hyY8a37jFe/fkumzQpnfZjsbOg7jSpw3K/img.png?width=1154&amp;amp;height=1101&amp;amp;face=0_0_1154_1101');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Ai-ML] Attention Query 와 Key Vector 알아보기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;- 목차 들어가며.Attention 은 Input Data 를 3개의 서로 다른 공간으로 투영시키는 과정이 수행됩니다. 기하학적으로 바라본다면 Input Dimension 에서 Query Dim, Key Dim, Value Dim 에 해당하는 다른 공간으로 Pr&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;westlife0615.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Attention Weight.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Attention Weight 는 Attention 의 Query 와 Key vector 사이의 유사도를 확률적으로 표현한 가중치입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Attention Score, Weight 등의 다양한 표현이 있는 것으로 알고 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 편의상 Attention Weight 라고 지칭하도록 하겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Attention Weight 는 Query 와 Key vector 의 유사도를 Dot Product 를 통해서 계산한 후에 Softmax Activation 을 거쳐 출력되는 결과입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 시계열 데이터에서 각 Time Step 이 서로 다른 Time Step 에 어떠한 영향력을 끼치는지, 주목하는지 정도를 나타내는 가중치라고 생각할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 관련된 수식을 정리해보면 아래와 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$\text{score}(i, j) = Q_i \cdot K_j^\mathrm{T}$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Q_i 는 i 번째 Query Vector, K_j 는 j 번째 Key Vector 에 해당하며, Dot Product 연산을 통해서 각 Time Step 별 Score 또는 유사도를 표현하는 Matrix 를 얻을 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Key Vector 의 Dimension 크기만큼 스케일링을 취하게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 Cosine Simiarity 의 경우에는 벡터의 크기를 나눠주는 것과 달리 Attention Weight 에서는 차원의 크기의 제곱근 크기만큼 스케일링하게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$\text{ScaledScore} = \frac{Q K^\mathrm{T}}{\sqrt{d_k}}$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Softmax Activation 을 거쳐 최종적인 Attention Weight 가 만들어집니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$\alpha_{i,j} = \text{softmax}_j \left( \frac{Q_i \cdot K_j^\mathrm{T}}{\sqrt{d_k}} \right)$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Attention Weight 의 직관적인 해석은 각 Time Step 간의 유사함, 기여도, 또는 주목할 정도 (Attention) 을 나타내게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Value vector.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Value vector 는 Attention 의 Query 와 Key vector 와 함께 Linear Projection 을 거쳐서 생성되는 결과입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Query 와 Key 처럼 Input Data 를 변환한 또 다른 형태이며, Value vector 는 Attention Weight 와 결합하여 최종적인 결론을 도출합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$\alpha_{i,j} = \mathrm{softmax}_j \left( \frac{Q_i \cdot K_j^T}{\sqrt{d_k}} \right )$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$\mathrm{Output}_i = \sum_j \alpha_{i,j} \cdot V_j$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Attention Weight 는 seq_num x seq_num 의 shape 를 가지는 행렬로 표현됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 각 Time Step 의 관계 또는 주목할 정도를 행렬 형식으로 표현하는거죠.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 Value vector 가 함께 연산이 되는데요.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Value vector 는 단일 Time Step 의 상태를 Attn_Dim 의 크기만큼 표현됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 시퀀스의 Value Matrix 관점에서 본다면, Attention Weight 와 Value Matrix 는 Sequence 를 기준으로 함께 Dot Product 연산이 될 수 있으며,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과로써 각 Time Step 의 Context 를 표현하는 Hidden State 를 출력할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RNN 계열의 모델과 Attention 의 차이점.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RNN, LSTM, GRU 와 같은 RNN 계열의 모델에서 생성하는 Hidden State 의 경우에는 과거의 데이터를 기반으로 Context 를 형성합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 현 시점의 Hidden State 는 과거의 상태들이 누적된 결론에 해당합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 Attention 은 과거의 상태에만 의존하지 않고, Sequence 내의 모든 Time Step 을 고려합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거와 미래에 제한을 두지 않고, 모든 Time Step 간의 관계를 기반으로 현 시점의 Hidden State 를 생성하게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;script&gt;

    setTimeout(function() {
      var elementList = document.querySelectorAll(&quot;h2&quot;);
      var indexer = null;
      for (var i = 0; i &lt; elementList.length; i++) {
          if (elementList[i].innerText.indexOf(&quot;- 목차&quot;) &gt; -1) {
              indexer = elementList[i]    
          }
          
      }
      var headerList = document.querySelectorAll(&quot;h2,h3,h4&quot;);
	  var allHeaderList = [...headerList]
      var indexBox = document.createElement(&quot;ul&quot;);
      for (var i = 0; i &lt; allHeaderList.length; i++) {
        var header = allHeaderList[i]; 
        var headerText = header.innerText
        if (headerText != null &amp;&amp; (headerText.indexOf(&quot;*&quot;) &gt; -1 || headerText.indexOf(&quot;.&quot;) &gt; -1 || headerText.indexOf(&quot;;&quot;) &gt; -1 || headerText.indexOf(&quot;?&quot;) &gt; -1 || headerText.indexOf(&quot;!&quot;) &gt; -1)) {
          header.setAttribute(&quot;id&quot;, i);
          
          var indexItem = document.createElement(&quot;li&quot;)
          var indexLink = document.createElement(&quot;a&quot;)
          indexLink.setAttribute(&quot;style&quot;, &quot;color: black; line-height: 2;&quot;);
          indexLink.setAttribute(&quot;href&quot;, &quot;#&quot; + i);

          indexItem.appendChild(indexLink);
          indexLink.innerText = headerText;
		  if (header.tagName == 'H2') {
			indexItem.setAttribute(&quot;style&quot;, &quot;margin-left: 12px; list-style: circle;&quot;);
		  }
		  if (header.tagName == 'H3') {
			indexItem.setAttribute(&quot;style&quot;, &quot;margin-left: 24px; list-style: disc;&quot;);
		  }          
		  if (header.tagName == 'H4') {
			indexItem.setAttribute(&quot;style&quot;, &quot;margin-left: 36px; list-style: disclosure-closed;&quot;);
		  }                    
          indexBox.appendChild(indexItem)

        }

      }

      indexer.parentElement.insertBefore(indexBox, indexer);
      indexer.parentElement.removeChild(indexer)
      indexBox.parentElement.insertBefore(indexer, indexBox)


      var progressBar = document.createElement(&quot;div&quot;)
	  progressBar.setAttribute(&quot;class&quot;, &quot;progress&quot;)
      progressBar.setAttribute(&quot;style&quot;, &quot;height: 5px; width: 0px; position: fixed; top: 0; left : 0; background-color: red; z-index: 100000;&quot;)
      document.body.insertBefore(progressBar, document.body.firstChild);
      $(document).scroll(function (e) {
		var scrollAmount = $(window).scrollTop();
		var documentHeight = $(document).height();
		var windowHeight = $(window).height();
		var scrollPercent = (scrollAmount / (documentHeight - windowHeight)) * 100;
		var roundScroll = Math.round(scrollPercent);
        progressBar.setAttribute(&quot;style&quot;, &quot;height: 5px; width: &quot; + scrollPercent + &quot;vw; position: fixed; top: 0; left : 0; background-color: red;  z-index: 100000;&quot;)
	  });
      
      
      $(&quot;div[data-ke-type='moreLess']&quot;)
      .css(&quot;margin&quot;, &quot;20px 0px&quot;)
      .css(&quot;background-color&quot;, &quot;#fafafa&quot;)
      .css(&quot;border&quot;, &quot;1px dashed #c5c5c5&quot;)
      .css(&quot;color&quot;, &quot;#333333&quot;)
      
      $(&quot;.btn-toggle-moreless&quot;)
      .css(&quot;width&quot;, &quot;100%&quot;).css(&quot;height&quot;, &quot;100%&quot;).css(&quot;display&quot;, &quot;block&quot;).css(&quot;padding&quot;, &quot;20px 20px 22px&quot;)
	  .text(&quot;열어보기&quot;)
  
      $(&quot;.btn-toggle-moreless&quot;).each((i, tag) =&gt; {
	    var text = $(tag).next().text().substring(0, 50).replaceAll(&quot;\n&quot;, &quot;&quot;)
        $(tag).text(&quot; 열어보기 &quot; + text + &quot; ... ... &quot;);
      });
      
      
    }, 500);  
  
&lt;/script&gt;</description>
      <category>AI-ML</category>
      <category>Attention</category>
      <category>attention weight</category>
      <category>Key</category>
      <category>Query</category>
      <category>RNN</category>
      <category>sequence</category>
      <category>Transformer</category>
      <category>value</category>
      <author>코딩수집가</author>
      <guid isPermaLink="true">https://westlife0615.tistory.com/1167</guid>
      <comments>https://westlife0615.tistory.com/1167#entry1167comment</comments>
      <pubDate>Tue, 28 Jan 2025 16:50:29 +0900</pubDate>
    </item>
  </channel>
</rss>