<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>성장을 꾸준히 기록하는 공간</title>
    <link>https://downfa11.tistory.com/</link>
    <description>무소의 뿔처럼 나아가자</description>
    <language>ko</language>
    <pubDate>Thu, 9 Apr 2026 05:31:50 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>downfa11</managingEditor>
    <image>
      <title>성장을 꾸준히 기록하는 공간</title>
      <url>https://tistory1.daumcdn.net/tistory/7464342/attach/6f8ea62ec57b44dbaa78e1b060256e81</url>
      <link>https://downfa11.tistory.com</link>
    </image>
    <item>
      <title>8억 명의 사용자를 지원하기 위한 OpenAI의 PostgreSQL 확장</title>
      <link>https://downfa11.tistory.com/118</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;나는 PostgreSQL 안쓰는데도 무려 8억 사용자의 수백만 QPS를 어떻게 해결하는지 궁금하잖아 다들&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;수백만 QPS를 처리하는데 Azure PostgreSQL을 쓰고, 단일 쓰기 노드와 50 여개의 복제본으로 수용한다는건 직접 안해보면 알 방법이 없는 견적이다. 그 쓰기 처리량을 단일 노드로&amp;hellip;. 놀랍다.&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;b&gt;2만~5만 QPS&lt;/b&gt; 정도를 분담하는 구조로, Azure에서 제공하는 가장 높은 하드웨어 사양(96 vCore, 672GiB RAM, 32TiB)로 추측했다.&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;East US 리전 기준으로 최대 스펙의 Azure 서버는 수백만 QPS의 네트워크 IO 비용을 제외하고서도, 인스턴스 비용 $10,000 * 50, 스토리지 공간 $5,000 * 50 으로 한화로 월 10억 원씩 쓰는 셈이다.&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;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 1년간 OpenAI의 PostgreSQL 사용량은 10배 이상 증가했으며, 계속해서 빠르게 증가하고 있다.&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;이러한 성장을 지속하기 위해 생산 인프라를 발전시키려는 노력은 새로운 인사이트를 얻었는데, PostgreSQL은 &lt;b&gt;생각보다 훨씬 더 큰 읽기 중심 워크로드를 신뢰성 있게 지원하도록 확장할 수 있다&lt;/b&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;California 대학교의 Berkeley 캠퍼스 연구에서 처음 시작한 이 시스템은 &lt;b&gt;단일 Primary로도 대규모 글로벌 트래픽을 지원&lt;/b&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;OpenAI는 전 세계 여러 지역에 걸쳐서 약 50개의 읽기 복제본이 존재한다. 여기서는 그 과정에서 배운 교훈을 다룬다.&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;단일 아키텍처로 OpenAI의 규모 요구를 충족한다는 점이 놀랍게 들릴 수 있지만, 실제로 이를 실현하는건 쉽지 않았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;596&quot; data-origin-height=&quot;387&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGD5cB/dJMcafelpoJ/VSsgTjc03gbytDMwlvVSik/tfile.svg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGD5cB/dJMcafelpoJ/VSsgTjc03gbytDMwlvVSik/tfile.svg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGD5cB/dJMcafelpoJ/VSsgTjc03gbytDMwlvVSik/tfile.svg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGD5cB%2FdJMcafelpoJ%2FVSsgTjc03gbytDMwlvVSik%2Ftfile.svg&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;596&quot; height=&quot;387&quot; data-origin-width=&quot;596&quot; data-origin-height=&quot;387&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 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;PostgreSQL 과부하로 발생하는 SEV는 종종 upstream 이슈로 인한 DB 부하 급증 패턴을 따른다:&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;복잡한 조인 쿼리 급증으로 CPU 포화&lt;/li&gt;
&lt;li&gt;신규 피처 구현으로 인한 쓰기 과부하(write storm)&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;리소스 사용량이 증가하면서 쿼리 지연도 증가하고, 요청이 timeout되기 시작한다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이때 재시도 메커니즘은 부하를 더 증폭시켜서 서비스 전체를 저하시키는 악순환을 초래할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 PostgreSQL은 다중 버전 동시성 제어(MVCC) 구현 때문에 쓰기 중심의 워크로드에 비해 효율이 떨어진다. 그래서 읽기 중심 작업에는 잘 확장되지만 쓰기 트래픽이 많은 시기에는 어려움을 겪는다.&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;이에 대한 추가적인 문제 논의는 PostgreSQL 위키에 포스트 작성자가 함께 작성한게 있다.&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1769225483960&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;The Part of PostgreSQL We Hate the Most&quot; data-og-description=&quot;As much as Andy loves PostgreSQL, there is one part that is terrible and causes many headaches for people. Learn what it is and why it sucks.&quot; data-og-host=&quot;www.cs.cmu.edu&quot; data-og-source-url=&quot;https://www.cs.cmu.edu/~pavlo/blog/2023/04/the-part-of-postgresql-we-hate-the-most.html&quot; data-og-url=&quot;https://www.cs.cmu.edu/~pavlo/blog/2023/04/the-part-of-postgresql-we-hate-the-most.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/r8ja3/dJMb86OV4wV/tVS3mFsQ5V2VnJIywtVWt1/img.jpg?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400&quot;&gt;&lt;a href=&quot;https://www.cs.cmu.edu/~pavlo/blog/2023/04/the-part-of-postgresql-we-hate-the-most.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.cs.cmu.edu/~pavlo/blog/2023/04/the-part-of-postgresql-we-hate-the-most.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/r8ja3/dJMb86OV4wV/tVS3mFsQ5V2VnJIywtVWt1/img.jpg?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400');&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;The Part of PostgreSQL We Hate the Most&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;As much as Andy loves PostgreSQL, there is one part that is terrible and causes many headaches for people. Learn what it is and why it sucks.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.cs.cmu.edu&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;Scaling DB to millions of QPS&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 한계를 완화하고 쓰기 부담을 줄이기 위해서 수평적으로 분할(Sharding) 가능한 쓰기 중심의 워크로드를 Azure Cosmos DB로 마이그레이션 중이다.&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;그리고 불필요한 쓰기를 최소화하도록 애플리케이션 로직을 최적화했다. 이제 기존 PostgreSQL에는 새로운 테이블 추가를 허용하지 않고 신규 시스템에 할당된다.&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;PostgreSQL을 샤드화하지 않는 이유는 뭘까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 쓰기를 하나의 Primary 인스턴스로 처리하는 이유는 그 샤딩 과정이 매우 복잡하고 시간이 많이 소요되기 때문이다.&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;/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;/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;/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;b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;지연된 쓰기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;중복 쓰기 오류&lt;/b&gt;를 애플리케이션 수준에서 수정하거나 지연된 쓰기를 도입해서 트래픽 급증을 완화하거나 쓰기 속도 제한을 통해 쓰기 pressure를 방지하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;복잡한 쿼리의 급증&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;12개 테이블을 조인하는 &lt;b&gt;복잡한 쿼리의 급증&lt;/b&gt;이 심각한 SEV 원인이었고, 대다수가 ORM이 생성한 쿼리였기 때문에 신중히 쿼리문을 검토하고 기대하는대로 동작하는지 확인하는 것이 중요하다.&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;해당 쿼리들은 분해하고 조인 과정을 애플리케이션 계층으로 옮겨서 진행하고 idle_in_transaction_session_timeout처럼 timeout 설정, 장시간 유휴 쿼리를 모니터링한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;단일 장애 지점(SPOF)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 요청은 읽기에 치중되어 있기 때문에 &lt;b&gt;Primary의 SPOF 문제&lt;/b&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;Primary 장애를 줄이기 위해서 고가용성(HA) 모드로 운영하며 항상 트래픽을 대신할 synced 복제본의 stand-by 구조를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;noisy neighbor&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 기능 출시할때 비효율적인 쿼리 도입으로 PostgreSQL CPU를 크게 소모해서 기존 시스템의 요청이 느려질 수 있다.&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;ldquo;noisy neighbor&amp;rdquo; 문제를 완화하기 위해서 워크로드를 &lt;b&gt;리소스 집약적 요청의 우선순위별로 나눠서 라우팅&lt;/b&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;덜 중요한 작업이 CPU를 크게 소모하더라도 중요한 작업의 요청 성능이 저하되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;커넥션 풀&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 DB 인스턴스의 &lt;b&gt;커넥션이 고갈되거나 유휴 상태가 많아서&lt;/b&gt; statement나 transaction polling 모드를 통해 활성 커넥션 수를 크게 줄일 수 있었다.&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;연결 설정의 지연 시간(50ms&amp;rarr;5ms)도 감소했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;캐시&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시 계층 장애로 인한 &lt;b&gt;캐시 미스 급증&lt;/b&gt;이 데이터베이스 부하로 이어진 사례다. 특정 Key를 놓친 단일 Leader만 fallback하는 캐시 잠금 및 lease 구현으로 해결했다.&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;하나의 요청만 락 걸고 캐시를 적재하고, 나머지 요청들은 대기하기 때문에 PostgreSQL를 보호할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;복제 지연의 완화&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;50 여개의 읽기 복제본은 모두 Primary의 WAL(Write Ahead Log)를 스트리밍한다.&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;이 복제본 개수가 증가할수록 더 많은 복제본에게 WAL을 전달해야 해서 네트워크 대역폭, CPU 모두에 부담을 준다.&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;이로 인해서 복제 지연(replication log)가 더 크고 불안정해져서 시스템의 신뢰성 있는 확장을 해친다.&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;이를 해결하기 위해서 Azure PostgreSQL 팀과 협력하여 계단식 복제(cascading replication)를 도입했다.&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 alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;596&quot; data-origin-height=&quot;345&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SRuZn/dJMcacomXJh/JZnsj8q9SqJRw1K4adaRj0/tfile.svg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SRuZn/dJMcacomXJh/JZnsj8q9SqJRw1K4adaRj0/tfile.svg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SRuZn/dJMcacomXJh/JZnsj8q9SqJRw1K4adaRj0/tfile.svg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSRuZn%2FdJMcacomXJh%2FJZnsj8q9SqJRw1K4adaRj0%2Ftfile.svg&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;596&quot; height=&quot;345&quot; data-origin-width=&quot;596&quot; data-origin-height=&quot;345&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;중간(intermediate) 복제본이 WAL을 하위(downstream) 복제본으로 중계하는 방식인데, 테스트중이라고만 하고 실제 지연을 어느 정도 해결하는지 궁금하다.&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;최적화의 결과&lt;/h2&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;p data-ke-size=&quot;size16&quot;&gt;이 컬럼은 적절한 설계와 최적화를 통해 Azure PostgreSQL이 어느 정도 규모의 워크로드까지 처리하도록 확장 가능한지 보여준다.&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;OpenAI는 거의 50개의 읽기 복제본을 추가하면서도 복제 지연(replication lag)를 거의 0에 가깝게 유지하고, 지리적으로 분산된 지역에 대해서 저지연 읽기를 보장하고 미래의 성장을 위한 여유 용량까지 확보했다.&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;이를 통해서 클라이언트측에서 두 자릿수 ms의 낮은 지연시간(p99)와 5-9 가용성을 제공하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 12개월동안 SEV-0 PostgreSQL 사고는 단 한 건뿐이었는데, 이는 ChatGPT ImageGen의 트래픽이 10배 이상 급증하여 일주일만에 1억 명 이상의 신규 사용자가 가입했다.&lt;/p&gt;
&lt;/blockquote&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;p data-ke-size=&quot;size16&quot;&gt;앞으로도 인프라 수요가 증가함에 따라 PostgreSQL Sharding이나 대체할만한 분산 시스템 등의 추가적인 확장 접근법을 모색한다고 하니 기대된다.&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;출처 및 인용.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://openai.com/index/scaling-postgresql/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://openai.com/index/scaling-postgresql/&lt;/a&gt;&lt;/p&gt;</description>
      <category>tech</category>
      <category>Database</category>
      <category>OpenAI</category>
      <category>PostgreSQL</category>
      <category>RDBMS</category>
      <category>Sharding</category>
      <author>downfa11</author>
      <guid isPermaLink="true">https://downfa11.tistory.com/118</guid>
      <comments>https://downfa11.tistory.com/118#entry118comment</comments>
      <pubDate>Sat, 24 Jan 2026 12:44:14 +0900</pubDate>
    </item>
    <item>
      <title>Kubernetes 리소스 최적화: CPU Throttling은 왜 Limits보다 낮은데 발생할까?</title>
      <link>https://downfa11.tistory.com/117</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 토스 Slash의 'Kubernetes CPU 알뜰하게 사용하기' 섹션을 주제로 작성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/WdikCm_CYms&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&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;code&gt;resources.requests.cpu&lt;/code&gt; 설정을 접할때 'CPU Core를 몇 개 요청한다'라고 막연히 이해했었다.&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;예를 들어 cpu: 0.3이라고 적혀 있으면 물리적인 CPU Core를 30% 점유한다거나, 코어의 일부를 고정적으로 할당받는다고 오해하기 쉽다. 그리고 이런 접근 방식은 사실 절반 정도 틀렸다.&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;Kubernetes &lt;code&gt;requests.cpu&lt;/code&gt;의 오해&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;576&quot; data-origin-height=&quot;210&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpsz5A/dJMcadnaOs8/fLTvVGugOOGXRzD32nfhM0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpsz5A/dJMcadnaOs8/fLTvVGugOOGXRzD32nfhM0/img.jpg&quot; data-alt=&quot;kubernetes&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpsz5A/dJMcadnaOs8/fLTvVGugOOGXRzD32nfhM0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbpsz5A%2FdJMcadnaOs8%2FfLTvVGugOOGXRzD32nfhM0%2Fimg.jpg&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;521&quot; height=&quot;190&quot; data-origin-width=&quot;576&quot; data-origin-height=&quot;210&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;kubernetes&lt;/figcaption&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;Kubernetes에서 CPU는 Core 개념이 아니라 &lt;b&gt;CPU Time이라는 상대적인 시간 점유율&lt;/b&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;code&gt;requests.cpu=0.3&lt;/code&gt;의 의미는 실제로 Core를 0.3개 쓴다는게 아니라 필요할 때 CPU Time을 30%만큼 보장받는다는 의미에 가깝다.&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;Kubernetes는 컨테이너의 런타임을 어느 정도 보장하기 위해서 이 requests 값을 기준으로 스케줄링하고, 동시에 Linux Kernel의 CFS(Completely Fair Scheduler)와 cgroup 설정으로 실제 CPU의 사용 비율을 조정한다.&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;code&gt;requests.cpu=1.5&lt;/code&gt;인 Pod가 하나 실행중이라면 어떤 일이 벌어질까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노드에 CPU Core가 하나뿐이라면 물리적으로 1Core 이상 쓸 수 없지만, 2 Core 이상이라면 이 Pod는 &quot;최대&quot; 1.5 Core에 해당하는 CPU Time을 보장받는다.&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;code&gt;requests.cpu=0.5&lt;/code&gt;인 Pod를 하나 더 띄우면 서로 1.5:0.5, 즉 3:1 비율로 CPU Time을 나눠 쓰게 된다.&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;그렇다고 해서 항상 이 비율대로 통용되는 것은 아니다. CFS는 필요해지는 순간에만 CPU를 분배하기 때문에 모든 Pod가 idle 상태라면 CPU Time을 쪼갤 수 없기 때문에 비율이 무의미해진다.&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;결국 이 상대적인 CPU Time의 조정은 Linux cgroup이 담당하고, 실제로도 특정 경로 안에서 이 가중치에 대해서 확인할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;/sys/fs/cgroup&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/sys/fs/cgroup/cpu.weight&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Kubernetes의 시스템 리소스 최적화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes는 여러 서버에 걸쳐 컨테이너를 실행하고, 그 상태를 관찰하고 제어하는 도구이다. 당연히 시스템 리소스의 할당량에 대해서도 설정할 수 있고 requests, limits를 제공하고 있다.&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;CPU Requests는 &lt;b&gt;최소한 이정도쯤의 CPU Time을 보장&lt;/b&gt;하는 선언이다. requests같은 경우는 서버의 리소스가 충분하다면 더 쓸 수 있겠지만 반면 limits은 말 그대로 &lt;b&gt;상한선&lt;/b&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;이 한계를 넘는 순간 컨테이너는 CPU를 더이상 받지 못하고 대기 상태에 들어선다. 흔히 말하는 CPU Throttling이 여기서 발생한다.&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;b&gt;항상 서비스의 안정성을 유지해야한다&lt;/b&gt;는 점이다. 비용 부담때문에 리소스를 줄인다고 만지다가 응답 지연이나 장애가 늘어난다면 그야말로 본말전도이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;그래서 우리는 &quot;불필요한&quot; 할당을 줄여서 비용을 낮추고, &quot;꼭 필요한&quot; 리소스의 사용량을 줄이거나 &quot;분산&quot;시키는 토끼들을 모두 잡아야 한다.&lt;/blockquote&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;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;CPU Throttling을 어떻게 피할 것인가?&lt;/li&gt;
&lt;li&gt;Requests, Limits를 어디까지 줄일 수 있는가?&lt;/li&gt;
&lt;li&gt;CPU Usages 자체를 줄일 수 있는가?&lt;/li&gt;
&lt;li&gt;그 사용량을 어떻게 각 노드별로 고르게 분산시킬 것인가?&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CPU Throttling을 어떻게 피할 것인가?&lt;/h2&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;p data-ke-size=&quot;size16&quot;&gt;CPU Usages 그래프는 Limits보다 낮게 나오는데, Throttling이 계속 발생한다. 모순적이다.&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;이를 이해하기 위해선 먼저 Linux CFS의 동작 방식을 보아야 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너는 Worker Node의 CPU를 공유하고, CFS는 quota 방식으로 CPU를 관리한다.&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;기본적으로 kubelet은 100ms interval (&lt;code&gt;cpu-cfs-quota-period&lt;/code&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;예를 들어, CPU 할당량이 50%인 컨테이너라면 100ms중 이미 50ms를 써버린 순간 나머지 50ms 동안 멍청하게 기다려야 한다.&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;전체적인 Avg Usages는 50%를 넘지 않더라도 한순간에 CPU를 집중적으로 쓰면 Throttling이 발생할 수 있는 구조이다.&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;code&gt;quota period&lt;/code&gt;를 줄이는 방법도 있지만, 근본적으로 해결하려면 CPU Limits를 두지 말거나 아예 &lt;code&gt;cpu-cfs-quota&lt;/code&gt;를 비활성화하는 선택지도 존재한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 인사이트는 토스 Slash에서나 얻을 수 있는거지만.. 안정성이 중요한 서비스라면 '더더욱 Limits이 필요한가?'라는 질문을 던져볼 필요가 있다.&lt;/p&gt;
&lt;p&gt;&lt;del&gt;토스니까 할만하긴 하네&lt;/del&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;JVM의 &lt;code&gt;ActiveProcessorCount&lt;/code&gt;나 Java 11 이후의 &lt;code&gt;container-awareness&lt;/code&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;컨테이너는 Worker Node의 커널을 공유하기 때문에 사용 가능한 CPU Core 수가 노드 전체의 Core 수와 동일하다.&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;이러니까 자기들끼리 '헉 이렇게 많은 CPU를 제가요??' 하면서 자기 혼자 다 쓰는줄 알고 신나서 쓰레드를 잔뜩 만드는 상황이 발생할 수도 있다.&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;Right Sizing: Requests, Limits를 어디까지 줄일 수 있는가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리소스의 할당량이 부족하면 서비스가 불안정해지고, 반대로 과하게 사용하면 지갑이 눈물을 흘린다. 결국 안정성과 효율성 사이의 균형점(trade-off)를 찾아야 한다.&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;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고객 서비스와 직접 연관된 경우에는 requests를 일별 최대 사용량의 2배로 잡고서 아예 Limits를 두지 않는다. 두배씩이나???&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;하지만 데이터 센터가 2곳이라 한쪽이 장애가 나는 경우 다른 한 쪽에서 2곳치의 트래픽을 감당해야 한다는 전제가 있었다.&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;반대로 안정성 요구가 상대적으로 낮은 서비스는 하루의 최대 사용량 정도로 Requests를 잡고 Limits를 그보다 더 높게 둔다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 센터가 없는 우리들은 클라우드의 다중 AZ를 쓰거나 토스뱅크의 후자 사례를 도입하면 되겠다.&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;IDC 환경에서 발생하는 AutoScaling시 장애 위험:&lt;/h3&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;p data-ke-size=&quot;size16&quot;&gt;토스처럼 IDC 환경에서 운영하는 경우는 특히 문제가 되기 쉬웠는데, Projection 방식으로 미리 최대 부하를 추산하여 맞춰두고 CPU Usages에 따라 사전에 조정하는 방식을 따른다.&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;그러니 Grafana, Prometheus, Thanos같은 메트릭을 모니터링하는 것이 운영시 중요해진다.&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;Kotlin이나 Spring 애플리케이션은 &lt;code&gt;Warm-up&lt;/code&gt; 과정에서 CPU가 튀는데, 이런 초기 구간까지 다 Alert로 잡으면 그 자체로 Noise가 된다. 이때 Alert의 기준 메트릭을 명확하게 잡아야 이 노이즈를 제거할 수 있다. (etc. requests per usages rate)&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;클라우드 환경에서 발생하는 오버프로비저닝 (feat. 리소스 파편화):&lt;/h3&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;p data-ke-size=&quot;size16&quot;&gt;여기에 리소스 Fragmentation 문제까지 이어진다. 예를 들어서 CPU 4Core, Memory 16GB 스펙의 노드를 운영한다고 가정해보자.&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;CPU를 다 썼는데도 메모리가 10GB 이상 남아 있다면 Pod가 안들어가니 그거 쓰지도 못하고, 두 눈뜨고서 리소스 노는거 보고만 있어야 한다.&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;/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;b&gt;노드 수를 줄이고 깡스펙을 올리는 방법&lt;/b&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;code&gt;DaemonSet&lt;/code&gt;은 모든 노드마다 하나씩 뜨기 때문에 노드 수가 줄어들면 자연스럽게 그 비율이 줄어들기도 하고, 그만큼 기존 노드들의 CPU가 올라가니 파편화를 줄이고 더 많은 Pod를 띄울 수 있다.&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;실제로 토스 Core팀에서 노드별 스펙을 4배로 뻥튀기해서 전체 클러스터의 사이즈를 40% 줄였다는 사례가 증명해준다.&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;CPU Usages 자체를 줄일 수 있는가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MAU가 늘어나면 트래픽과 CPU Usages가 늘어나는데, 이 증가는 선형적이지 않고 지수적으로 늘어난다고 체감할 때가 있다.&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;혼자 써먹기 부담스러운 Istio, Thanos, Mimir, Loki 등과 같은 클러스터 운영 컴포넌트들은 그 이름처럼 무거운 리소스를 잡아먹는다.&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;CNCF나 클라우드를 대상으로 하는 오픈소스가 주류를 이루고 대부분 Go로 이루어져서 직접 튜닝하기도 쉽지 않다.&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;Prometheus나 Thanos에서의 사례:&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; - REST API에 userID같은 값이 그대로 들어가면 메트릭의 Cardinality 폭증, 메트릭 하나만 제거해도 CPU Usages가 15% 감소&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Prometheus 버전 업데이트&lt;/b&gt; - CPU Usages 30~40% 개선&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;써볼 일이 있을까 싶은 Service Mesh 사례 (Istio):&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;토스는 서비스마다 붙는 Envoy 사이드카만으로 전체 클러스터 CPU의 15% 비중을 차지했었다.&lt;/li&gt;
&lt;li&gt;Istio 버전 올라갈수록 Envoy 리소스 사용량이 감당안되자 &lt;s&gt;직접 만든다&lt;/s&gt;는 극단적인 선택을 해버렸다..&lt;/li&gt;
&lt;/ul&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;특히 다중 노드를 사용하는 Kubernetes 생태계에서 리소스의 분산은 중요하다.&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;Pod 수를 아무리 균등하게 나눴다고 해서 CPU 사용량까지 고르게 분산되는 것은 아니다. 실제로 노드간 CPU 사용량 차이가 40%p까지 벌어지는 사례도 있다.&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;이게 다 Kubernetes Control Plane에서 이뤄지는 스케줄러가 기본적으로 Pod 수 기반의 분산만 보장하기 때문이다.&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;Topology Spread Constraint를 사용해도 결국 같은 Label의 Pod들을 같은 개수로 나누는 수준에 머무르는데, 서비스별 트래픽 차이가 심할수록 이런 형식적인 분산은 비효율을 낳는다.&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;CPU를 실제로 어느 정도 사용하는지에 대한 인식 없이 Pod 수만 나눠놓으면 결국 특정 노드에 몰리는 과부하로 운영중에 큰 코 다칠 수 있다는 점을 명심해야 한다.&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;오늘의 최적화 글 요약&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 Kubernetes에서 CPU Resources를 다룬다는건 단순히 숫자를 줄이고 늘리는 딸깍이 아니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;requests나 limits를 통해서 오버 프로비저닝을 막아야 한다.&lt;/li&gt;
&lt;li&gt;노드별 스펙을 조정해서 리소스 파편화를 최소화해야 한다.&lt;/li&gt;
&lt;li&gt;불필요한 연산과 메트릭을 제거하여 CPU Usages를 근본적으로 낮춰야 한다.&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;p data-ke-size=&quot;size16&quot;&gt;출처 및 인용.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=WdikCm_CYms&quot;&gt;https://www.youtube.com/watch?v=WdikCm_CYms&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #777777; text-align: center;&quot;&gt;&lt;a href=&quot;http://appdata.hungryapp.co.kr/data_file/data_img/201702/21/W148766798003569241.jpg&quot;&gt;http://appdata.hungryapp.co.kr/data_file/data_img/201702/21/W148766798003569241.jpg&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>kubernetes</category>
      <category>CFS</category>
      <category>cpu throttling</category>
      <category>Fragmentation</category>
      <category>kubernetes</category>
      <category>right sizing</category>
      <category>리소스 최적화</category>
      <author>downfa11</author>
      <guid isPermaLink="true">https://downfa11.tistory.com/117</guid>
      <comments>https://downfa11.tistory.com/117#entry117comment</comments>
      <pubDate>Thu, 15 Jan 2026 21:24:23 +0900</pubDate>
    </item>
    <item>
      <title>JVM Metaspace가 단순히 이름만 바꾼건 아닐텐데요</title>
      <link>https://downfa11.tistory.com/116</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Java 8부터 JVM 메모리 구조에서 PermGen 영역이 제거되고 Metaspace가 도입되었다.&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;code&gt;OutOfMemoryError: PermGen space&lt;/code&gt; 오류를 사라지게 만들었고, Heap 영역에서 관리받던 친구를 바깥으로 빼버리면서 JVM 메모리 관리가 한층 단순해진 것처럼 보였다.&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;513&quot; data-origin-height=&quot;300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Cr4wn/dJMcaiWj798/2JZXTiFlQrcRRCoE8xaeL0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Cr4wn/dJMcaiWj798/2JZXTiFlQrcRRCoE8xaeL0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Cr4wn/dJMcaiWj798/2JZXTiFlQrcRRCoE8xaeL0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCr4wn%2FdJMcaiWj798%2F2JZXTiFlQrcRRCoE8xaeL0%2Fimg.jpg&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;261&quot; data-origin-width=&quot;513&quot; data-origin-height=&quot;300&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;하지만 이 변경은 메모리 사용량 자체를 줄이거나, 클래스 로딩 문제를 근본적으로 해결한 결정은 아니었다.&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;PermGen이 담당하던 클래스 metadata를 Heap 내부에서 관리하던 방식이, Native memory로 이동했을 뿐이다. 즉, JVM이 직접 통제하던 메모리 영역을 OS의 메모리 관리에 위임한 것이다.&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;이 지점부터 Java 8 이후의 메모리 문제는 Heap이 아니라 &lt;b&gt;프로세스 전체 메모리 관점&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;이름만 Metaspace로 바꾼건 아닐거잖아요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Metaspace는 Heap의 확장도, PermGen의 단순한 이름 변경도 아니다.&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;Heap과 완전히 분리된 Native 영역에 위치하며, &lt;code&gt;-Xmx&lt;/code&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;기본 설정에서 Metaspace는 사실상 제한이 없다. 클래스를 계속 로드하는 한, 운영체제가 허용하는 범위까지 메모리를 소비할 수 있다.&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;이 때문에 Java 8 이후에는 Heap 사용량은 안정적인데, JVM 프로세스가 갑자기 OOM으로 종료되는 상황이 발생할 수 있다.&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;원인은 Heap이 아니라 &lt;b&gt;Metaspace의 무제한 확장&lt;/b&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;code&gt;-XX:MaxMetaspaceSize&lt;/code&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;Metaspace에는 무엇이 저장되는가?&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Metaspace에는 &lt;b&gt;클래스 실행에 필요한 metadata가 저장&lt;/b&gt;된다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에는 단순한 class name만 있는 것이 아니라, method signature, bytecode, constant pool, annotation, runtime metadata 등 JVM 실행에 직접적으로 필요한 정보들이 포함된다.&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;이 중에서도 가장 중요한 구성 요소는 Klass 구조체다.&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;Klass는 JVM 내부에서 하나의 클래스를 대표하는 네이티브 구조체이며, Metaspace가 단순한 &amp;ldquo;정보 저장소&amp;rdquo;가 아니라 실행과 직결된 영역임을 보여준다.&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;Klass와 java.lang.Class는 왜 다른가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름 때문에 자주 혼동되지만, Klass와 &lt;code&gt;java.lang.Class&lt;/code&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;code&gt;java.lang.Class&lt;/code&gt;는 Heap에 존재하는 일반적인 Java 객체다. 반면 Klass는 C++로 구현된 네이티브 구조체이며 Metaspace에 위치한다.&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;JVM이 객체의 타입을 판단하고, 메서드 호출 시 어떤 구현을 실행할지 결정하며, 상속 관계와 인터페이스 구조를 해석할 때 기준으로 삼는 것은 &lt;code&gt;java.lang.Class&lt;/code&gt;가 아니라 Klass다.&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;모든 Java 객체는 Heap에 생성되지만, 객체 헤더에는 자신이 속한 Klass를 가리키는 포인터가 포함된다.&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;이 포인터를 통해 JVM은 동적 바인딩과 다형성을 구현한다. 즉, Klass는 JVM 객체 모델의 중심이며, Metaspace는 단순한 보조 영역이 아니라 실행의 핵심 축이다.&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;Compressed Class Pointer가 만드는 제약&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;64bit HotSpot JVM은 메모리 사용을 줄이기 위해 CompressedOops와 Compressed Class Pointer를 기본적으로 사용한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;292&quot; data-origin-height=&quot;220&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ddWXoj/dJMcahiQSNN/zEWYqmAkKqkcBsFQyHFkAk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ddWXoj/dJMcahiQSNN/zEWYqmAkKqkcBsFQyHFkAk/img.jpg&quot; data-alt=&quot;What is Compressed Class Space&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ddWXoj/dJMcahiQSNN/zEWYqmAkKqkcBsFQyHFkAk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FddWXoj%2FdJMcahiQSNN%2FzEWYqmAkKqkcBsFQyHFkAk%2Fimg.jpg&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;292&quot; height=&quot;220&quot; data-origin-width=&quot;292&quot; data-origin-height=&quot;220&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;What is Compressed Class Space&lt;/figcaption&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;객체 헤더에 들어 있는 Klass 포인터는 32bit로 압축되어 있으며, 이 포인터로 접근 가능한 Klass 구조체들은 32GB 미만의 연속된 가상 주소 공간에 위치해야 한다.&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;이 제약 때문에 Metaspace는 내부적으로 두 영역으로 나뉜다.&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;Klass 구조체만 저장하는 &lt;b&gt;Class space&lt;/b&gt;와, 그 외 메타데이터를 담는 &lt;b&gt;Non-class space&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;class part: Klass 구조체를 32G가 넘지 않는 연속된 영역으로 할당 (*클래스 자체를 압축하는게 아니라 포인터를 압축)&lt;/li&gt;
&lt;li&gt;non-class part: 나머지 모든 객체들 포함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;compressed object pointers에 대한 좋은 설명: &lt;a href=&quot;https://shipilev.net/jvm/anatomy-quarks/23-compressed-references&quot;&gt;JVM Anatomy Quark #23: Compressed References&lt;/a&gt;​&lt;/p&gt;
&lt;figure id=&quot;og_1768377916606&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;JVM Anatomy Quark #23: Compressed References&quot; data-og-description=&quot;Java specification is silent on the storage size for the data types. Even for primitives, it only mandates the ranges the primitive types should definitely support and their behavior of operations, but not the actual storage size. This, for example, allows&quot; data-og-host=&quot;shipilev.net&quot; data-og-source-url=&quot;https://shipilev.net/jvm/anatomy-quarks/23-compressed-references&quot; data-og-url=&quot;https://shipilev.net/jvm/anatomy-quarks/23-compressed-references/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://shipilev.net/jvm/anatomy-quarks/23-compressed-references&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://shipilev.net/jvm/anatomy-quarks/23-compressed-references&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;JVM Anatomy Quark #23: Compressed References&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Java specification is silent on the storage size for the data types. Even for primitives, it only mandates the ranges the primitive types should definitely support and their behavior of operations, but not the actual storage size. This, for example, allows&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;shipilev.net&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;Compressed Class Space&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Class space는 JVM 시작 시 &lt;code&gt;-XX:CompressedClassSpaceSize&lt;/code&gt;로 지정된 크기만큼 가상 주소 공간을 미리 예약한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본값은 1GB이며, HotSpot 구현상 최대 3GB로 제한되어 있다.&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;ldquo;예약&amp;rdquo;만 되어 있을 뿐, 즉시 물리 메모리를 모두 점유하지는 않는다. 하지만 한 번 가득 차면 여유가 없다.&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;중요한 점은 Class space가 먼저 소진될 경우, &lt;code&gt;MaxMetaspaceSize&lt;/code&gt;에 여유가 있더라도 즉시 &lt;code&gt;OutOfMemoryError: Compressed Class Space&lt;/code&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;code&gt;MaxMetaspaceSize&lt;/code&gt;가 아니라 Compressed Class Space인 경우가 적지 않다.&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;클래스는 왜 마음대로 해제되지 않는가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'더이상 참조되지 않는 클래스는 GC되겠지'라고 생각했는데 그게 아니었지롱.&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;JVM에는 클래스 단위의 GC 개념이 없다.&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;클래스는 static 필드, 상수 풀, JIT 코드, 리플렉션 캐시 등과 깊게 얽혀 있기 때문에, JVM은 개별 클래스가 아니라 &lt;b&gt;Class Loader 단위&lt;/b&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;하나의 클래스 로더가 로드한 클래스들은 하나의 묶음으로 취급되며, 해당 클래스로더 자체가 GC 대상이 되었을 때에만 자신이 로드한 모든 클래스 metadata가 함께 해제된다.&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;Class Loader 단위 Garbage Collection&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;783&quot; data-origin-height=&quot;590&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bq3aAQ/dJMcaaxfYsm/CYZgrU7ykaX27n0mHEDPl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bq3aAQ/dJMcaaxfYsm/CYZgrU7ykaX27n0mHEDPl1/img.png&quot; data-alt=&quot;What is Metaspace?&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bq3aAQ/dJMcaaxfYsm/CYZgrU7ykaX27n0mHEDPl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbq3aAQ%2FdJMcaaxfYsm%2FCYZgrU7ykaX27n0mHEDPl1%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;556&quot; height=&quot;419&quot; data-origin-width=&quot;783&quot; data-origin-height=&quot;590&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;What is Metaspace?&lt;/figcaption&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;Poof!!!!!!!&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클래스 로더 객체가 더 이상 참조되지 않음 &amp;rArr; Garbage&lt;/li&gt;
&lt;li&gt;GC cycle 중에서 클래스 unload 발생&lt;/li&gt;
&lt;li&gt;클래스 로더가 갖던 metadata들이 해제&lt;/li&gt;
&lt;li&gt;metaspace에 할당된 메모리가 반환된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Metaspace의 메모리는 GC 없이는 절대 반환되지 않는다.&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;Heap 사용량이 낮더라도, 메타데이터 정리를 위해 GC가 수행되는 상황은 충분히 발생할 수 있다.&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;JVM은 Metaspace 사용량이 특정 임계점에 도달하면 오래된 클래스 로더를 회수할 수 있는지 확인하기 위해 GC를 트리거한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;즉, metaspace가 클래스 로더로 인한 GC를 시작하는 임계 지점(threshold) 역할&lt;/blockquote&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;code&gt;MaxMetaspaceSize&lt;/code&gt;나 Compressed Class Space 한계에 근접했을 때도 마지막 시도로 GC를 수행한다. 이 GC가 실패하면, 그제서야 OOM이 발생한다.&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;MaxMetaspaceSize와 CompressedClassSpaceSize의 차이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;MaxMetaspaceSize&lt;/code&gt;와 &lt;code&gt;CompressedClassSpaceSize&lt;/code&gt;는 메타스페이스의 크기를 제어하는 knobs인데, 미묘하게 다르면서 서로에게 영향을 주니 헷갈릴만하다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;-XX:MaxMetaspaceSize&lt;/code&gt;,&lt;code&gt;-XX:MetaspaceSize&lt;/code&gt;: 최대/최소 metaspace size.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;non-class 영역과 class 영역을 모두 포함하는 &amp;ldquo;soft&amp;rdquo; 제한의 의미로, 기본적으로 꺼져 있어서 선택 사항&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-XX:CompressedClassSpaceSize&lt;/code&gt;: Compressed Class size의 가상 사이즈를 지정한다. (default=1G, max=3G)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;compressed class space의 가상 크기를 정의하는 &amp;ldquo;hard&amp;rdquo; 제한으로, VM 런타임중에 절대 변경할 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;490&quot; data-origin-height=&quot;209&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JxPnt/dJMcad1LyCi/U6tlEQ5SnEVUIlqkwKbP51/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JxPnt/dJMcad1LyCi/U6tlEQ5SnEVUIlqkwKbP51/img.jpg&quot; data-alt=&quot;Sizing Metaspace&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JxPnt/dJMcad1LyCi/U6tlEQ5SnEVUIlqkwKbP51/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJxPnt%2FdJMcad1LyCi%2FU6tlEQ5SnEVUIlqkwKbP51%2Fimg.jpg&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;490&quot; height=&quot;209&quot; data-origin-width=&quot;490&quot; data-origin-height=&quot;209&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Sizing Metaspace&lt;/figcaption&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;&lt;code&gt;MaxMetaspaceSize&lt;/code&gt;는 Metaspace 전체에 대한 상한선이다.&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;Class space와 Non-class space를 모두 포함하는 빨간색 영역이 &lt;code&gt;MaxMetaspaceSize&lt;/code&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;이 영역 이상으로 메모리를 commit하려 하거나, 초과 시 GC를 시도한 뒤 실패하면 &lt;code&gt;OutOfMemoryError: Metaspace&lt;/code&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;code&gt;MaxMetaspaceSize&lt;/code&gt;의 의도는 단순하게 최대 메모리 제한을 두는 거지만, &lt;code&gt;CompressedClassSpaceSize&lt;/code&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;code&gt;CompressedClassSpaceSize&lt;/code&gt;는 Class space만을 대상으로 하며 JVM 시작 시 고정되며 런타임 중 변경할 수 없다.&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;code&gt;MaxMetaspaceSize&lt;/code&gt;는 기본적으로 무제한인 반면, &lt;code&gt;CompressedClassSpaceSize&lt;/code&gt;는 1G로 설정되어 있다. 즉, 우리가 도달할 수 있는 한계는 &lt;code&gt;CompressedClassSpaceSize&lt;/code&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;실전 운영시 Metaspace 크기는 어떻게 잡아야 하는가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Metaspace 설정의 목적은 메모리 절약이 아니다. 비정상적인 클래스 로딩을 &lt;b&gt;조기에 감지하기 위한 안전 장치&lt;/b&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;경험적으로 클래스 하나당 Class space는 약 1KB, Non-class space는 약 8KB 정도를 사용한다. (&lt;i&gt;출처: Sizing Metaspace)&lt;/i&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;예를 들어 10,000개의 클래스를 로드한다면 대략 90MB 정도의 Metaspace가 필요하다.&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;본문 글에서는 단편화와 런타임 변동성을 고려해 보통 2배 수준의 안전 계수를 적용하는데, 이 경우 &lt;code&gt;-XX:MaxMetaspaceSize=180M&lt;/code&gt; 정도가 첫 설정값의 근거가 될 수 있다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;PermGen은 사라졌지만, 클래스 metadata의 관리 문제도 같이 사라진 것은 아니다.&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;Metaspace는 관리 포인트가 줄어든게 아니라, 관리하는 책임자가 바뀐거 뿐이다.&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;특히 동적 클래스 로딩하는 시스템에서는 Metaspace는 조용히 시스템을 암살할 수 있으니 Heap만 보고 메모리는 안정적이라고 자만하지 말자.&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;CompressedClassSpaceSize 인위적으로 jvm이 3G로 제한하는데 그 이유는 모른다고 함.. 기술적으로는 32G까지 된다는데&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.programmersought.com/article/4905216600/&quot;&gt;https://www.programmersought.com/article/4905216600/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stuefe.de/posts/metaspace/what-is-metaspace/&quot;&gt;https://stuefe.de/posts/metaspace/what-is-metaspace/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stuefe.de/posts/metaspace/what-is-compressed-class-space/&quot;&gt;https://stuefe.de/posts/metaspace/what-is-compressed-class-space/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stuefe.de/posts/metaspace/sizing-metaspace/&quot;&gt;https://stuefe.de/posts/metaspace/sizing-metaspace/&lt;/a&gt;&lt;/p&gt;</description>
      <category>spring</category>
      <category>compressed class</category>
      <category>heap</category>
      <category>JVM</category>
      <category>Klass</category>
      <category>Metaspace</category>
      <category>permgen</category>
      <author>downfa11</author>
      <guid isPermaLink="true">https://downfa11.tistory.com/116</guid>
      <comments>https://downfa11.tistory.com/116#entry116comment</comments>
      <pubDate>Wed, 14 Jan 2026 17:14:21 +0900</pubDate>
    </item>
    <item>
      <title>운영중 장애 징후 분석: MySQL fsync 최적화를 통한 Commit 지연 해결</title>
      <link>https://downfa11.tistory.com/115</link>
      <description>&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;11월에 행사를 진행하면서 트래픽 조금 경험한걸 아직도 곱씹어 먹고 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;트래픽이 많았다고 말하긴 애매하지만, 운영 환경에서 실제 사용자가 동시에 몰리는 상황을 처음을 제대로 겪은 경험이었다. 그고 항상 성능 테스트를 진행해도 spike 몰리는 상황을 염두해왔지 8시간이나 되는 장시간 트래픽 집중은 그 자체로 신선했다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1768224365321&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignLeft&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Hack Playground: 부산대-부경대 해킹캠프 회고&quot; data-og-description=&quot;11월 8일자로 진행된 부산대-부경대 연합 해킹캠프에서 'Hack-Playground' 서비스 내의 CTF 대회를 시범적으로 도입했다. 서비스 자체는 KOREN망 안에서 호화롭게 실험하고 성능 걱정없이 트래픽 때려박&quot; data-og-host=&quot;downfa11.tistory.com&quot; data-og-source-url=&quot;https://downfa11.tistory.com/106&quot; data-og-url=&quot;https://downfa11.tistory.com/106&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bl33kh/hyZQKWXYkb/nKZgjYk0TgceX8sQrppdc1/img.png?width=800&amp;amp;height=529&amp;amp;face=0_0_800_529,https://scrap.kakaocdn.net/dn/u6Okj/hyZQHMJp8u/qO9WKhlFpxmitwOEwvyVrK/img.png?width=800&amp;amp;height=529&amp;amp;face=0_0_800_529,https://scrap.kakaocdn.net/dn/hxRVy/hyZQSnaQxc/yb73zsk6P9efaloSgWtci0/img.png?width=1774&amp;amp;height=1322&amp;amp;face=0_0_1774_1322&quot;&gt;&lt;a href=&quot;https://downfa11.tistory.com/106&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://downfa11.tistory.com/106&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bl33kh/hyZQKWXYkb/nKZgjYk0TgceX8sQrppdc1/img.png?width=800&amp;amp;height=529&amp;amp;face=0_0_800_529,https://scrap.kakaocdn.net/dn/u6Okj/hyZQHMJp8u/qO9WKhlFpxmitwOEwvyVrK/img.png?width=800&amp;amp;height=529&amp;amp;face=0_0_800_529,https://scrap.kakaocdn.net/dn/hxRVy/hyZQSnaQxc/yb73zsk6P9efaloSgWtci0/img.png?width=1774&amp;amp;height=1322&amp;amp;face=0_0_1774_1322');&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;Hack Playground: 부산대-부경대 해킹캠프 회고&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;11월 8일자로 진행된 부산대-부경대 연합 해킹캠프에서 'Hack-Playground' 서비스 내의 CTF 대회를 시범적으로 도입했다. 서비스 자체는 KOREN망 안에서 호화롭게 실험하고 성능 걱정없이 트래픽 때려박&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;downfa11.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;대회 기간동안 발생한 슬로우 쿼리를 분석해서 인덱스 설계가 제대로 이뤄졌는지 평가하고 쿼리 튜닝을 진행할 목적이었다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;슬로우 쿼리 로그만으로는 부족하다&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;long_query_time을 1(sec)로 설정했다고 가정하자.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;1초 이상 걸린다고 해서 다 문제가 되는 쿼리인가? 극단적으로 트래픽이 많은 테이블에서 0.9초 쿼리가 10만번 일어나는 경우는 감지할 수 없다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;즉,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;슬로우 쿼리만으로 어디가 원인인지 왈가왈부 평가하기 어렵다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;performance_schema도 같이 확인하자&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;아래 내용들은 전부 slow log만으로는 알 수 없는 정보들이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지연 원인이 락인가? IO인가? CPU 연산인가?&lt;/li&gt;
&lt;li&gt;왜&amp;nbsp;같은&amp;nbsp;쿼리여도&amp;nbsp;시간대별로&amp;nbsp;차이가&amp;nbsp;나는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;MySQL Performance Schema 검사를 통해서 다양한 실시간성 정보를 수집할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쿼리의 히스토리, 평균/최대 지연시간, wait event&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;아래와 같이 쿼리 형태로 상세한 정보를 확인할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1768224867767&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT *
FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;좀 더 정제해서 보면&lt;/p&gt;
&lt;pre id=&quot;code_1768224971716&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT DIGEST_TEXT, COUNT_STAR,
  ROUND(SUM_TIMER_WAIT/1e12, 2) AS total_sec,
  ROUND(AVG_TIMER_WAIT/1e6, 2)  AS avg_ms
FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;실험1: 호출 빈도가 제일 많았던 Commit&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;운영 MySQL 스냅샷을 로컬에서 performance_schema 돌려보면서 분석하던 중 사실을 발견할 수 있었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;누적 시간(sum_timer_wait) 기준으로 정렬한 결과.&lt;/p&gt;
&lt;pre id=&quot;code_1768225113693&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mysql&amp;gt; SELECT
    -&amp;gt;   LEFT(DIGEST_TEXT, 60) AS digest_short,
    -&amp;gt;   COUNT_STAR,
    -&amp;gt;   ROUND(SUM_TIMER_WAIT/1e12, 2) AS total_sec,
    -&amp;gt;   ROUND(AVG_TIMER_WAIT/1e6, 2) AS avg_ms
    -&amp;gt; FROM performance_schema.events_statements_summary_by_digest
    -&amp;gt; ORDER BY SUM_TIMER_WAIT DESC
    -&amp;gt; LIMIT 10;
+-----------------------------------------------------------+------------+-----------+-----------+
| digest_short                                              | COUNT_STAR | total_sec |  avg_ms   |
+-----------------------------------------------------------+------------+-----------+-----------+
| COMMIT                                                    |     130051 |    702.32 |   5400.34 |
| SET `autocommit` = ?                                      |     268293 |     38.32 |    142.84 |
| SELECT COUNT(*) FROM `problems`                           |        139 |     19.54 | 372260.89 |
| SELECT `u1_0` . `user_id` ...                             |         35 |     25.03 |  319400.3 |
| ...                                                       |        ... |       ... |       ... |
+-----------------------------------------------------------+------------+-----------+-----------+
10 rows in set (0.00 sec)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;가장 문제가 된 쿼리는 비즈니스가 아니라 Commit이었다. 네?&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;뭔가 잘못 됐다. commit 지연이면 &lt;b&gt;쿼리 문제라기보단 DB 병목의 신호&lt;/b&gt;이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;autocommit 역시도 같은 맥락으로, 어디가 원인인지 분석해야한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;쿼리 자체는 빠르지만 Flush나 IO 작업으로 꼬인다면 스케줄러 전체가 느려지기 때문에 지연 발생시 염두해둬야할 지점이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;몰랏는데 새로 알았다 여기도 보자 이제!!!&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;부수적으로 발견한 쿼리 튜닝의 비효율 개선&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;아 그 과정에서 간단한 쿼리 튜닝도 했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;점수(score)가 가장 높은 사용자 조회&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;mysql 8에서는 desc 인덱스가 의미 있음. 이전 버전처럼 * (-1) 안그런다.&lt;/li&gt;
&lt;li&gt;index(score desc)가 없으면 filesort, limit이 있어도 full table scan&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1768225266793&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN                                                                                                                                                                                                                                                                                                                                                                                                               |
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| -&amp;gt; Limit: 20 row(s)  (cost=10.9 rows=20) (actual time=0.395..0.395 rows=0 loops=1)
    -&amp;gt; Sort: u.score DESC, limit input to 20 row(s) per chunk  (cost=10.9 rows=107) (actual time=0.391..0.391 rows=0 loops=1)
        -&amp;gt; Filter: (u.score &amp;gt; 1000)  (cost=10.9 rows=107) (actual time=0.36..0.36 rows=0 loops=1)
            -&amp;gt; Table scan on u  (cost=10.9 rows=107) (actual time=0.0585..0.236 rows=120 loops=1)
 |
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;DESC 인덱스를 추가한 후, LIMIT 연산이 Index range scan 단계에서 적용되어서 정렬 과정을 생략할 수 있었다.&lt;/p&gt;
&lt;pre id=&quot;code_1768225278381&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN                                                                                                                                                                                                                                                                 |
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| -&amp;gt; Limit: 20 row(s)  (cost=0.71 rows=1) (actual time=0.0419..0.0419 rows=0 loops=1)
    -&amp;gt; Index range scan on u using idx_user_score_desc over (score &amp;lt; 1000), with index condition: (u.score &amp;gt; 1000)  (cost=0.71 rows=1) (actual time=0.0386..0.0386 rows=0 loops=1)
 |
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;실행계획상 &lt;span data-token-index=&quot;1&quot;&gt;불필요한 filesort는 제거&lt;/span&gt;했지만, 근본 원인을 해소하지 않는 이상 증상 완화에 불과하다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;슬로우 쿼리 로그를 통해 발견한 DB 병목&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;슬로우 쿼리가 발생했던 원인이 어느 정도 좁혀졌다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;분석하는 과정에서 비효율적인 쿼리를 찾고, 인덱스를 추가하기도 했지만 근본적인 원인이 아니였다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;인덱스가 없어서 해당 쿼리만 느린게 아니라, 모든 쿼리에 대한 commit 자체가 수백 초 걸린다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;즉, DB 자체가 잠깐 지연된 구간이 있었다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;예상하기로 아마 대학 동아리간 연합 CTF 대회때 트래픽이 몰리면서.. 열악한 성능을 버티지 못하고 DB 자체가 지연 &amp;rarr; 인덱스 효율이 낮은(없는) 조회성 쿼리들이 슬로우 쿼리로 노출된 것으로 보인다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;DB 병목이 발생했다는 것으로 유추할 순 있었지만 그 병목 원인을 파악할 순 없다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;DB 병목의 원인은 그래서 뭐였을까?&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;막연하게 트래픽이 몰리던 대회날 수집된 거라고 추측하는 것을 넘어서 신뢰할만한 데이터로 문제를 확정하고 싶었다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;특정 테이블만의 문제가 아니였고, 전체적으로 SELECT, COUNT, UPDATE가 느려지는 현상이 있었다. (commit avg latency 5sec, max 수백 sec)&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;쓰기 작업에 대한 문제가 보이니 disk flush 과정에서 발생하는 fsync 지연을 의심했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;먼저 운영 환경은 따로 DB 튜닝을 진행한 적이 없어서 날것 그대로의 순수한 default 세팅을 갖고 있었다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;주의해서 봐야할 튜닝 옵션은 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1768225325483&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mysql&amp;gt; SHOW VARIABLES LIKE 'sync_binlog';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| sync_binlog   | 1     |
+---------------+-------+
1 row in set (0.05 sec)

mysql&amp;gt; SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';
+--------------------------------+-------+
| Variable_name                  | Value |
+--------------------------------+-------+
| innodb_flush_log_at_trx_commit | 1     |
+--------------------------------+-------+
1 row in set (0.00 sec)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;innodb_flush_log_at_trx_commit&lt;/b&gt;: commit시 뭘 어디까지 보장할건가
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;0: 응난몰라. 메모리에만 ack&lt;/li&gt;
&lt;li&gt;1: default. commit마다 fsync 1번 (정합선이 최우선인 phase로 동시 트랜잭션이 많으면 병목이 바로 생긴다!!)&lt;/li&gt;
&lt;li&gt;2: fsync는 1초마다 백그라운드에서 진행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;sync_binlog&lt;/b&gt;: N번의 COMMIT마다 binlog를 fsync 할 것인가?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;commit시 실제 흐름은 InnoDB redo log 기록 &amp;rarr; binlog 기록 &amp;rarr; commit 반환이다.&lt;/li&gt;
&lt;li&gt;0: commit시 binlog 메모리에 기록만 하고 fsync 안하니까 disk flush도 os 마음대로 진행함. binlog 유실 가능&lt;/li&gt;
&lt;li&gt;1: commit마다 fsync 1회씩 하면 binlog가 100% disk에 기록된다.&lt;/li&gt;
&lt;li&gt;N: n번 commit마다 1번식 fsync해서 n개 단위로 유실될 수는 있지만&amp;hellip; commit 지연 급감&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;더 자세한 설명은 여기 참조&lt;/p&gt;
&lt;figure id=&quot;og_1768287803291&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignLeft&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[MySQL/MariaDB] innodb_flush_log_at_trx_commit 파라미터 / 개념도&quot; data-og-description=&quot;innodb_flush_log_at_trx_commit 란 ? - 트랜잭션이 commit 될 때 log buffer를 flush하고 disk 연산이 flush 되는 시점을 설정하는 파라미터 - default 값은 1 Innodb_flush_log_at_trx_commit 개념도 1. commit 2. InnoDB의 log buffer에&quot; data-og-host=&quot;jione-e.tistory.com&quot; data-og-source-url=&quot;https://jione-e.tistory.com/128&quot; data-og-url=&quot;https://jione-e.tistory.com/128&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dmyTEU/dJMb8ZvuEeG/Axa6n0f4gnvndihOw2QGc1/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/cDECKy/dJMb8QL5hDV/RfEVHbVNQXl2uwQyjKjkV0/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/dgcfiW/dJMb87fZzC7/sQnhYPPNxtlkRw0sbQnxhk/img.png?width=960&amp;amp;height=540&amp;amp;face=0_0_960_540&quot;&gt;&lt;a href=&quot;https://jione-e.tistory.com/128&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://jione-e.tistory.com/128&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dmyTEU/dJMb8ZvuEeG/Axa6n0f4gnvndihOw2QGc1/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/cDECKy/dJMb8QL5hDV/RfEVHbVNQXl2uwQyjKjkV0/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/dgcfiW/dJMb87fZzC7/sQnhYPPNxtlkRw0sbQnxhk/img.png?width=960&amp;amp;height=540&amp;amp;face=0_0_960_540');&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;[MySQL/MariaDB] innodb_flush_log_at_trx_commit 파라미터 / 개념도&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;innodb_flush_log_at_trx_commit 란 ? - 트랜잭션이 commit 될 때 log buffer를 flush하고 disk 연산이 flush 되는 시점을 설정하는 파라미터 - default 값은 1 Innodb_flush_log_at_trx_commit 개념도 1. commit 2. InnoDB의 log buffer에&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;jione-e.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;모든 commit마다 redo log와 binlog를 디스크에 sync flush하고 있는 셈이다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;wait/io/file/innodb/innodb_log_file은 InnoDB redo log 파일에 대한 IO wait를 수집하고 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;여기에는 redo log의 쓰기나 flush(fsync) 와 같은 파일 작업에서 기다린 시간을 보여준다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그럼 이제 wait event를 확인해서 대기 시간을 보자.&lt;/p&gt;
&lt;pre id=&quot;code_1768287601520&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mysql&amp;gt; SELECT *
    -&amp;gt; FROM performance_schema.events_waits_summary_global_by_event_name
    -&amp;gt; WHERE EVENT_NAME LIKE 'wait/io/file/innodb/innodb_log_file';
+-------------------------------------+------------+------------------+----------------+----------------+----------------+
| EVENT_NAME                          | COUNT_STAR | SUM_TIMER_WAIT   | MIN_TIMER_WAIT | AVG_TIMER_WAIT | MAX_TIMER_WAIT |
+-------------------------------------+------------+------------------+----------------+----------------+----------------+
| wait/io/file/innodb/innodb_log_file |    1269680 | 1112217551109336 |              0 |      875982562 |   353111843406 |
+-------------------------------------+------------+------------------+----------------+----------------+----------------+
1 row in set (0.72 sec)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;AVG_TIMER_WAIT가 거의 0.87s, MAX_TIMER_WAIT 는 353s 나온 적이 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;아직 이 redo log 대기 시간의 원인이 fsync인지 write인지 확정 지을 수는 없지만, 적어도 IO가 멈춘게 확실해진다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 앞서 Commit이 fync 빈도가 잦으며, 지연 시간 digest 1위라는 점도 함께 고려하면 디스크 flush시 발생한 지연이 원인이 된다는 것을 알 수 있었다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;innodb_flush_log_at_trx_commit=1, sync_binlog=1으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;commit당 fsync 2번씩 하면서 트래픽이 몰리는 순간 병목 지점이 된 것이다. (innodb쪽 fsync + binlog fsync)&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;자, 이제 commit 지연이 fsync 빈도로 인한 병목이라는 논리적 근거를 실제 실험을 통해서 검증하겠다.&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;실험 환경&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이전에 구축해둔 실험 환경을 그대로 가져와서 대시보드나 비교군만 다시 설계했다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;낑낑거리면서 어떻게든 선언적으로 관리해볼려고 고민한게 안아까울만큼 알차게 써먹고 있다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;Apache JMeter 기준 thread: 1000, 10sec, 10 count 상황을 고정해서 문제 풀이이력 등록하는 쓰기 작업 테스트를 진행했다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;일부러 DB의 병목을 시각적으로 확인하고자 짧은 시간내에 트래픽을 쑤셔박았다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 mysql-exporter를 통해서 쓰기 작업에 대한 세부 메트릭을 수집하기 위해서 죄다 긁어왔다.&lt;/p&gt;
&lt;pre id=&quot;code_1768288031799&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;--collect.perf_schema.eventsstatements
--collect.perf_schema.eventswaits
--collect.perf_schema.indexiowaits
--collect.perf_schema.tableiowaits
--collect.perf_schema.file_events
--collect.engine_innodb_status
--collect.info_schema.innodb_metrics
--collect.info_schema.processlist      
--collect.info_schema.tables
--collect.global_status&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;아, 당연하다면 당연하겠지만 mysql-exporter 접속에도 DB 커넥션을 사용하니 실험 환경에서 미리 염두해서 max_connections를 설정해야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 접속하는 방법도 좀 특이했다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;mysqld.address=&amp;lt;URL&amp;gt;:&amp;lt;Port&amp;gt;&lt;/li&gt;
&lt;li&gt;mysqlid.username=&amp;lt;root&amp;gt;:&amp;lt;password&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;튜닝전&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;먼저 눈에 띄는 패턴을 설명하고 가겠다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;12.png&quot; data-origin-width=&quot;1253&quot; data-origin-height=&quot;460&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcAv2U/dJMcadOc8xu/2XuKFukVdiDkdyuPcX1P9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcAv2U/dJMcadOc8xu/2XuKFukVdiDkdyuPcX1P9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcAv2U/dJMcadOc8xu/2XuKFukVdiDkdyuPcX1P9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcAv2U%2FdJMcadOc8xu%2F2XuKFukVdiDkdyuPcX1P9k%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;1253&quot; height=&quot;460&quot; data-filename=&quot;12.png&quot; data-origin-width=&quot;1253&quot; data-origin-height=&quot;460&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;commit 빈도와 Query per seconds가 비례하고 있다. 아래도 같이 보자&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-01-10 220751.png&quot; data-origin-width=&quot;1367&quot; data-origin-height=&quot;448&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIZeEo/dJMcac9CfP1/6ONbjpGZXrfsDikJZNEj70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIZeEo/dJMcac9CfP1/6ONbjpGZXrfsDikJZNEj70/img.png&quot; data-alt=&quot;fsync Rate: 160 ops/s&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIZeEo/dJMcac9CfP1/6ONbjpGZXrfsDikJZNEj70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIZeEo%2FdJMcac9CfP1%2F6ONbjpGZXrfsDikJZNEj70%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;1367&quot; height=&quot;448&quot; data-filename=&quot;스크린샷 2026-01-10 220751.png&quot; data-origin-width=&quot;1367&quot; data-origin-height=&quot;448&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;fsync Rate: 160 ops/s&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;fync rate까지도 commit, qps와 함께 선형적으로 같이 증가하고 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;QPS가 증가하는 만큼 Commit수가 비례하는데, DB 설정으로 인해서 fsync가 비례해서 증가하는 증거이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; 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-filename=&quot;스크린샷 2026-01-10 220740.png&quot; data-origin-width=&quot;1250&quot; data-origin-height=&quot;452&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dtDh3X/dJMcac9CfQb/ZV11y82zrwiKOXfqMSd1wk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dtDh3X/dJMcac9CfQb/ZV11y82zrwiKOXfqMSd1wk/img.png&quot; data-alt=&quot;commit avg latency: 20.7ms&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dtDh3X/dJMcac9CfQb/ZV11y82zrwiKOXfqMSd1wk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdtDh3X%2FdJMcac9CfQb%2FZV11y82zrwiKOXfqMSd1wk%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;1250&quot; height=&quot;452&quot; data-filename=&quot;스크린샷 2026-01-10 220740.png&quot; data-origin-width=&quot;1250&quot; data-origin-height=&quot;452&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;commit avg latency: 20.7ms&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이를 통해서 디스크 flush 대기열이 쌓이면서 전체적인 commit latency를 발생하고 있는 것을 관측할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;튜닝후&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;사실 첫 실험에서는 binlog 30개당 fsync 1회으로 fsync 빈도를 극단적으로 줄이려고 했었다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1211&quot; data-start=&quot;1173&quot;&gt;innodb_flush_log_at_trx_commit = 2&lt;/li&gt;
&lt;li data-end=&quot;1232&quot; data-start=&quot;1212&quot;&gt;sync_binlog = 30&lt;/li&gt;
&lt;li data-end=&quot;1267&quot; data-start=&quot;1233&quot;&gt;innodb_flush_method = O_DIRECT&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 되면 redo log는 commit마다 fsync를 하지 않고 백그라운드에서 초당 1회씩 진행한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;binlog 역시도 30개 단위로 syscall을 호출해서 fsync 호출 자체를 물리적으로 줄일 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;flush 방식에서 Direct IO를 도입했는데 이에 관한 설명도 이미 블로그에 공부한 내용을 올린 적이 있다.&lt;/p&gt;
&lt;figure id=&quot;og_1768288582789&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignLeft&quot; data-og-type=&quot;article&quot; data-og-title=&quot;InnoDB Flush 계층에서 본 Direct IO (feat. Zero Copy와의 차이점)&quot; data-og-description=&quot;처음에는 MySQL의 디스크 쓰기 작업에 대해서 정리하다가 O_DIRECT 옵션을 보고 이런 생각이 들었다.zero copy랑 비슷한 얘기 아닌가? 둘 다 불필요한 복사를 줄여서 성능을 개선하는건데. 궁금해져서 &quot; data-og-host=&quot;downfa11.tistory.com&quot; data-og-source-url=&quot;https://downfa11.tistory.com/112&quot; data-og-url=&quot;https://downfa11.tistory.com/112&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/c5dUgL/dJMb8WesPAZ/TUZKDpUUdgCR6ghVGJfFk0/img.png?width=799&amp;amp;height=355&amp;amp;face=0_0_799_355,https://scrap.kakaocdn.net/dn/bJYE7q/dJMb8XRYM76/0ZLXob4bYvkrA1fWSmdPz0/img.png?width=799&amp;amp;height=355&amp;amp;face=0_0_799_355,https://scrap.kakaocdn.net/dn/4597q/dJMb9jOggQ7/JHc74Mev8hhVoCpGpMwx31/img.png?width=799&amp;amp;height=355&amp;amp;face=0_0_799_355&quot;&gt;&lt;a href=&quot;https://downfa11.tistory.com/112&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://downfa11.tistory.com/112&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/c5dUgL/dJMb8WesPAZ/TUZKDpUUdgCR6ghVGJfFk0/img.png?width=799&amp;amp;height=355&amp;amp;face=0_0_799_355,https://scrap.kakaocdn.net/dn/bJYE7q/dJMb8XRYM76/0ZLXob4bYvkrA1fWSmdPz0/img.png?width=799&amp;amp;height=355&amp;amp;face=0_0_799_355,https://scrap.kakaocdn.net/dn/4597q/dJMb9jOggQ7/JHc74Mev8hhVoCpGpMwx31/img.png?width=799&amp;amp;height=355&amp;amp;face=0_0_799_355');&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;InnoDB Flush 계층에서 본 Direct IO (feat. Zero Copy와의 차이점)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 MySQL의 디스크 쓰기 작업에 대해서 정리하다가 O_DIRECT 옵션을 보고 이런 생각이 들었다.zero copy랑 비슷한 얘기 아닌가? 둘 다 불필요한 복사를 줄여서 성능을 개선하는건데. 궁금해져서&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;downfa11.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 이 상태를 운영에 그대로 가져가도 되는가 생각해보면 영 글쎄올시다..&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;특히 sync_binlog를 N으로 설정해버리면 장애 발생시 최대 N개의 commit까지 binlog를 유실해버린다. binlog를 잃으면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;복구나 백업도 못하고 영영 사라져버리는 셈&lt;/b&gt;이다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; 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;innodb_flush_method=O_DIRECT&lt;/li&gt;
&lt;li&gt;innodb_flush_log_at_trx_commit=2&lt;/li&gt;
&lt;li&gt;sync_binlog = 1 (rollback)&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;binlog를 원래 값으로 복원하여 장애가 발생해도 commit 단위로 복구를 보장하되, redo log의 flush 압박만 줄이는 쪽으로 선택했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-01-10 224925.png&quot; data-origin-width=&quot;2519&quot; data-origin-height=&quot;459&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pau7K/dJMcajnmvfT/LzFEmAf6cAE2uMTLPmjDVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pau7K/dJMcajnmvfT/LzFEmAf6cAE2uMTLPmjDVK/img.png&quot; data-alt=&quot;fsync Rate: 5 ops/s, Commit avg latency: 1.47ms&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pau7K/dJMcajnmvfT/LzFEmAf6cAE2uMTLPmjDVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fpau7K%2FdJMcajnmvfT%2FLzFEmAf6cAE2uMTLPmjDVK%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;2519&quot; height=&quot;459&quot; data-filename=&quot;스크린샷 2026-01-10 224925.png&quot; data-origin-width=&quot;2519&quot; data-origin-height=&quot;459&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;fsync Rate: 5 ops/s, Commit avg latency: 1.47ms&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; 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;fsync Rate: 160 &amp;rarr; 5 (ops/s)&lt;/li&gt;
&lt;li&gt;commit avg latency: 20.7 &amp;rarr; 1.47(ms)&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1516&quot; data-origin-height=&quot;670&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/whmMz/dJMb996bG9R/tSmZ9RJsovOhHRc9RzB6IK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/whmMz/dJMb996bG9R/tSmZ9RJsovOhHRc9RzB6IK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/whmMz/dJMb996bG9R/tSmZ9RJsovOhHRc9RzB6IK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwhmMz%2FdJMb996bG9R%2FtSmZ9RJsovOhHRc9RzB6IK%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;1516&quot; height=&quot;670&quot; data-origin-width=&quot;1516&quot; data-origin-height=&quot;670&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;슬로우 쿼리 로그만으로 fsync, flush, IO 병목을 파악할 수 없다.&amp;nbsp;&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;왜 Commit이 가장 느린 쿼리로 나타났는지&lt;/li&gt;
&lt;li&gt;왜 인덱스를 붙여도 해결되지 않는지&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이 경우, 슬로우 쿼리는 쿼리 지연의 원인이 아니라 DB 병목의 결과일 수 도 있다.&lt;/p&gt;</description>
      <category>mysql</category>
      <category>binlog</category>
      <category>db 튜닝</category>
      <category>Direct IO</category>
      <category>flush</category>
      <category>fsync</category>
      <category>mysql</category>
      <category>slow query</category>
      <category>성능 개선</category>
      <author>downfa11</author>
      <guid isPermaLink="true">https://downfa11.tistory.com/115</guid>
      <comments>https://downfa11.tistory.com/115#entry115comment</comments>
      <pubDate>Tue, 13 Jan 2026 16:47:29 +0900</pubDate>
    </item>
    <item>
      <title>가상 쓰레드 도입 전후 DB 병목의 이동 (feat. HikariCP 튜닝 전략)</title>
      <link>https://downfa11.tistory.com/114</link>
      <description>&lt;p data-end=&quot;283&quot; data-start=&quot;157&quot; data-ke-size=&quot;size16&quot;&gt;컨테이너를 동적으로 생성하거나 현황을 조회하는 CTF 문제 관리 기능은 Hack Playground 서비스의 핵심 비즈니스 중 하나다.&lt;/p&gt;
&lt;p data-end=&quot;283&quot; data-start=&quot;157&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;283&quot; data-start=&quot;157&quot; data-ke-size=&quot;size16&quot;&gt;이 기능은 Kubernetes API와 빈번하게 통신하며, 성격상 IO bound 작업에 가까워서 자연스럽게 Java 가상 쓰레드 도입을 검토했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;418&quot; data-start=&quot;285&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;418&quot; data-start=&quot;285&quot; data-ke-size=&quot;size16&quot;&gt;가상 쓰레드의 개념부터 소개해야 하나 고민했지만 다행히 예전에 공부한 내용이 있다.&lt;/p&gt;
&lt;figure id=&quot;og_1768221442183&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;Java도 한다 경량 쓰레드 (Virtual Thread)&quot; data-og-description=&quot;봄(Spring)은 왔는가?Java의 위대한 산물인 가상 쓰레드는 분명 JVM 생테계에 엄청난 열풍을 일으켰음에 의심할 여지가 없다.많은 개발자들이 그 패러다임에 발맞춰서 프레임워크를 개선하고 있듯&quot; data-og-host=&quot;downfa11.tistory.com&quot; data-og-source-url=&quot;https://downfa11.tistory.com/76&quot; data-og-url=&quot;https://downfa11.tistory.com/76&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/P5XAH/hyZQL9pwnp/ROZhXadalOxhRx4GBwYfn0/img.png?width=800&amp;amp;height=484&amp;amp;face=0_0_800_484,https://scrap.kakaocdn.net/dn/iw9Op/hyZRkYJdcD/rKKYOZoXsuEh8Zpt7eMNI1/img.png?width=800&amp;amp;height=484&amp;amp;face=0_0_800_484,https://scrap.kakaocdn.net/dn/cWUeNf/hyZRgaZYbN/ZpcVtN2blpYdgAxPk9fNZ0/img.png?width=1784&amp;amp;height=1081&amp;amp;face=0_0_1784_1081&quot;&gt;&lt;a href=&quot;https://downfa11.tistory.com/76&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://downfa11.tistory.com/76&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/P5XAH/hyZQL9pwnp/ROZhXadalOxhRx4GBwYfn0/img.png?width=800&amp;amp;height=484&amp;amp;face=0_0_800_484,https://scrap.kakaocdn.net/dn/iw9Op/hyZRkYJdcD/rKKYOZoXsuEh8Zpt7eMNI1/img.png?width=800&amp;amp;height=484&amp;amp;face=0_0_800_484,https://scrap.kakaocdn.net/dn/cWUeNf/hyZRgaZYbN/ZpcVtN2blpYdgAxPk9fNZ0/img.png?width=1784&amp;amp;height=1081&amp;amp;face=0_0_1784_1081');&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;Java도 한다 경량 쓰레드 (Virtual Thread)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;봄(Spring)은 왔는가?Java의 위대한 산물인 가상 쓰레드는 분명 JVM 생테계에 엄청난 열풍을 일으켰음에 의심할 여지가 없다.많은 개발자들이 그 패러다임에 발맞춰서 프레임워크를 개선하고 있듯&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;downfa11.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;p data-end=&quot;418&quot; data-start=&quot;285&quot; data-ke-size=&quot;size16&quot;&gt;IO 대기 비용을 줄일 수 있는지 데이터로 확인하고 도입의 근거로 뒷받침하고자 실험을 준비했다.&amp;nbsp;실험 과정에서 K8s 통신과 별개로, DB 쪽 로직도 함께 테스트하며 다양한 성능 데이터를 쌓는 목적을 두었다.&lt;/p&gt;
&lt;p data-end=&quot;418&quot; data-start=&quot;285&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그 결과, 의도하지 않았던 흥미로운 현상을 발견할 수 있었다.&lt;/p&gt;
&lt;p data-end=&quot;436&quot; data-start=&quot;420&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;479&quot; data-start=&quot;471&quot; data-ke-size=&quot;size26&quot;&gt;YAML 기반 환경 구축으로 선언적 관리&lt;/h2&gt;
&lt;p data-end=&quot;575&quot; data-start=&quot;481&quot; data-ke-size=&quot;size16&quot;&gt;운영 중인 서비스에서 performance_schema를 기준으로 &lt;b&gt;가장 요청 빈도가 높은 쿼리&lt;/b&gt;를 대상으로 가상 쓰레드 도입 전&amp;middot;후 성능 테스트를 진행했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;626&quot; data-start=&quot;577&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;595&quot; data-start=&quot;577&quot;&gt;Apache JMeter 기준 Thread 수: 1000, 시간: 30초, 반복 횟수: 10회&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실험 과정에서 Grafana 대시보드를 항상 켜둘 수 없고, 매번 초기화할 때마다 소스를 등록하고 ID로 대시보드를 가져오는 번거로움이 있었다.&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그 과정에서 모든 Grafana 대시보드를 직접 구축했으며, 파일 구조는 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1768222664667&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Folder PATH listing
Volume serial number is 707A-1D92
C:.
│   docker-compose.yml
│   prometheus.yml
│   README.md
└───grafana
    ├───dashboards
    │       grafana-2.json
    │       grafana.json
    │
    └───provisioning
        ├───dashboards
        │       dashboard.yml
        │
        └───datasources
                datasource.yml&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;681&quot; data-start=&quot;628&quot;&gt;grafana/dashboards/grafana.json: 가상 쓰레드 전후 결과 시각화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;681&quot; data-start=&quot;628&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1001&quot; data-start=&quot;911&quot; data-ke-size=&quot;size16&quot;&gt;이 구조 덕분에 모든 대시보드를 선언적으로 관리할 수 있었고, 매번 Grafana를 초기화해도 환경 재사용과 테스트 일관성을 확보할 수 있었다.&lt;/p&gt;
&lt;p data-end=&quot;1001&quot; data-start=&quot;911&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1165&quot; data-start=&quot;1003&quot; data-ke-size=&quot;size16&quot;&gt;테스트 환경은 서로 영향을 주지 않도록 독립된 클라우드 환경에서 격리했으며, 실험 시 차이는 가상 쓰레드 여부의 환경변수만 적용했다.&lt;/p&gt;
&lt;p data-end=&quot;1165&quot; data-start=&quot;1003&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;모든 실험은 동일 환경에서 2~3회 정도의 warm-up 후 진행하여 안정적인 측정을 확보했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;718&quot; data-start=&quot;688&quot; data-ke-size=&quot;size26&quot;&gt;튜닝 전 관찰: 의도한대로 IO는 빨라졌지만... DB가 무너진다&lt;/h2&gt;
&lt;p data-end=&quot;741&quot; data-start=&quot;720&quot; data-ke-size=&quot;size16&quot;&gt;관측된 사실부터 정리하면 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;831&quot; data-start=&quot;743&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;792&quot; data-start=&quot;743&quot;&gt;Kubernetes API(&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;IO bound&lt;/span&gt;)의 응답 속도는 의미 있게 감소&lt;/li&gt;
&lt;li data-end=&quot;831&quot; data-start=&quot;793&quot;&gt;&lt;b&gt;DB 로직의 성능은 캐리어 쓰레드 대비 현저히 악화&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;852&quot; data-start=&quot;833&quot; data-ke-size=&quot;size16&quot;&gt;별 생각없이 k8s api 로직만 보고 '흠 나아졌군' 넘어갈 뻔했지만 환경 구축해둔게 아까워서 여러 데이터를 쌓으려다 얻어걸렸다.&lt;/p&gt;
&lt;p data-end=&quot;852&quot; data-start=&quot;833&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;852&quot; data-start=&quot;833&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;852&quot; data-start=&quot;833&quot; data-ke-size=&quot;size16&quot;&gt;특히 눈에 띈 지표는 다음과 같다.&lt;/p&gt;
&lt;h3 data-end=&quot;1000&quot; data-start=&quot;928&quot; data-ke-size=&quot;size23&quot;&gt;커넥션 대기 쓰레드 및 처리량&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-01-11 012128.png&quot; data-origin-width=&quot;2522&quot; data-origin-height=&quot;452&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/smzyu/dJMcagK0mce/rceFjrpMlEKuDrVG4UOil1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/smzyu/dJMcagK0mce/rceFjrpMlEKuDrVG4UOil1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/smzyu/dJMcagK0mce/rceFjrpMlEKuDrVG4UOil1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fsmzyu%2FdJMcagK0mce%2FrceFjrpMlEKuDrVG4UOil1%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;2522&quot; height=&quot;452&quot; data-filename=&quot;스크린샷 2026-01-11 012128.png&quot; data-origin-width=&quot;2522&quot; data-origin-height=&quot;452&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;1000&quot; data-start=&quot;928&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1000&quot; data-start=&quot;928&quot; data-ke-size=&quot;size16&quot;&gt;처리량 자체는 큰 차이가 없었다. 하지만 &lt;b&gt;DB 커넥션을 기다리는 pending 쓰레드 수&lt;/b&gt;에서 압도적인 차이가 발생했다.&lt;/p&gt;
&lt;p data-end=&quot;1000&quot; data-start=&quot;928&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot; data-start=&quot;854&quot; data-end=&quot;882&quot;&gt;HikariCP 커넥션 획득 시간 (P95)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-01-11 012028.png&quot; data-origin-width=&quot;1365&quot; data-origin-height=&quot;399&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJAjZe/dJMcafrMLUs/E3W92JJhTLTcyxFJ4uLcgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJAjZe/dJMcafrMLUs/E3W92JJhTLTcyxFJ4uLcgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJAjZe/dJMcafrMLUs/E3W92JJhTLTcyxFJ4uLcgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJAjZe%2FdJMcafrMLUs%2FE3W92JJhTLTcyxFJ4uLcgk%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;1365&quot; height=&quot;399&quot; data-filename=&quot;스크린샷 2026-01-11 012028.png&quot; data-origin-width=&quot;1365&quot; data-origin-height=&quot;399&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;884&quot; data-end=&quot;926&quot;&gt;
&lt;li data-start=&quot;884&quot; data-end=&quot;906&quot;&gt;캐리어 쓰레드: 2.63초&lt;/li&gt;
&lt;li data-start=&quot;907&quot; data-end=&quot;926&quot;&gt;가상 쓰레드: 30.1초&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1018&quot; data-start=&quot;1002&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1018&quot; data-start=&quot;1002&quot; data-ke-size=&quot;size16&quot;&gt;여기서 분석 결과가 드러난다.&lt;/p&gt;
&lt;blockquote data-end=&quot;1188&quot; data-start=&quot;1020&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;1188&quot; data-start=&quot;1022&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;병목 지점이 애플리케이션 쓰레드에서 DB Connection Pool로 이동&lt;/b&gt;했다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 쓰레드의 생성 비용이 저렴하다 보니, 제한된 DB 커넥션을 차지하기 위한 경쟁이 더욱 치열해졌다.&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;1225&quot; data-start=&quot;1195&quot; data-ke-size=&quot;size26&quot;&gt;튜닝 방향: 빠르게 결과를 내서 DB 병목을 최소화하자&lt;/h2&gt;
&lt;p data-end=&quot;1316&quot; data-start=&quot;1227&quot; data-ke-size=&quot;size16&quot;&gt;가상 쓰레드 환경에서 중요한 건 동시 요청을 최대한 처리하는 것이 아니라, DB에 부담을 주지 않으면서 핵심 요청만 빠르게 처리하는 것이라 생각했다.&lt;/p&gt;
&lt;p data-end=&quot;1419&quot; data-start=&quot;1411&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1419&quot; data-start=&quot;1411&quot; data-ke-size=&quot;size16&quot;&gt;튜닝의 핵심은 하나다.&lt;/p&gt;
&lt;blockquote data-end=&quot;1447&quot; data-start=&quot;1421&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;1447&quot; data-start=&quot;1423&quot; data-ke-size=&quot;size16&quot;&gt;더 느려지기 전에 얼른 멈춰야한다. 즉, 커넥션을 오래 기다리게 하지 않는다.&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&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 style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;1318&quot; data-end=&quot;1349&quot; data-ke-size=&quot;size16&quot;&gt;이를 위해 HikariCP 설정을 다음과 같이 조정했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;1351&quot; data-end=&quot;1409&quot;&gt;
&lt;li data-start=&quot;1351&quot; data-end=&quot;1364&quot;&gt;최대 커넥션 수 제한&amp;nbsp;&lt;/li&gt;
&lt;li data-start=&quot;1365&quot; data-end=&quot;1381&quot;&gt;&lt;b&gt;커넥션 획득 타임아웃 축소&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(hikari-connection-timeout)&lt;/li&gt;
&lt;li data-start=&quot;1382&quot; data-end=&quot;1409&quot;&gt;&lt;b&gt;일정 시간 이상 응답이 없으면 즉시 실패 처리&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(hikari-leak-detection-threshold)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 쓰레드쪽은 일정 시간 지나도록 응답이 오지 않으면 timeout 처리한 결과:&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;30sec_2.png&quot; data-origin-width=&quot;2519&quot; data-origin-height=&quot;459&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7alPx/dJMb99SEdMj/Gn4eKAOBcz28xqs3g81bbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7alPx/dJMb99SEdMj/Gn4eKAOBcz28xqs3g81bbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7alPx/dJMb99SEdMj/Gn4eKAOBcz28xqs3g81bbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7alPx%2FdJMb99SEdMj%2FGn4eKAOBcz28xqs3g81bbk%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;2519&quot; height=&quot;459&quot; data-filename=&quot;30sec_2.png&quot; data-origin-width=&quot;2519&quot; data-origin-height=&quot;459&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;p95: 5.56sec(carrier) - 1.77sec(virtual)&lt;/li&gt;
&lt;li&gt;latency: 667ms(virtual) - 1.3sec(carrier)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;1817&quot; data-start=&quot;1814&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1843&quot; data-start=&quot;1819&quot; data-ke-size=&quot;size26&quot;&gt;결론: 병목 이동과 가상 쓰레드의 한계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;솔직히&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;별 고민 없이&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;도입할 정도로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;막연하게&lt;span&gt;&lt;span&gt;&amp;nbsp;&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 style=&quot;color: #333333; text-align: start;&quot;&gt;&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 style=&quot;color: #333333; text-align: start;&quot;&gt;결국 성능 개선의 핵심은 가상 쓰레드 자체가 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;HikariCP Timeout을 통한 DB 부하 제어였다.&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 alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1494&quot; data-origin-height=&quot;478&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RqCVa/dJMcaivfkBO/c8bAMQskWsZxQjKMEOL860/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RqCVa/dJMcaivfkBO/c8bAMQskWsZxQjKMEOL860/img.png&quot; data-alt=&quot;carrier thread vs tunned vthread&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RqCVa/dJMcaivfkBO/c8bAMQskWsZxQjKMEOL860/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRqCVa%2FdJMcaivfkBO%2Fc8bAMQskWsZxQjKMEOL860%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;1494&quot; height=&quot;478&quot; data-origin-width=&quot;1494&quot; data-origin-height=&quot;478&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;carrier thread vs tunned vthread&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;HikariCP Connection Acquire Time(p95): 30.1 &amp;rarr; 1.77 (sec)&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;1713&quot; data-end=&quot;1812&quot; data-ke-size=&quot;size16&quot;&gt;DB 앞에서 무제한 대기열이 형성되던 상황이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;timeout 기반의 빠른 실패 전략&lt;/b&gt;으로 완화되면서,&lt;span&gt;&amp;nbsp;&lt;/span&gt;DB 부하는 줄고 핵심 비즈니스 요청의 응답성은 오히려 개선됐다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;1713&quot; data-end=&quot;1812&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1865&quot; data-start=&quot;1845&quot; data-ke-size=&quot;size16&quot;&gt;이번 실험에서 얻은 결론은 단순하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2093&quot; data-start=&quot;1867&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1894&quot; data-start=&quot;1867&quot;&gt;가상 쓰레드는 IO bound 작업에 강력하다&lt;/li&gt;
&lt;li data-end=&quot;1965&quot; data-start=&quot;1895&quot;&gt;하지만 DB 커넥션 풀과 결합되면 &lt;b&gt;애플리케이션 쓰레드에서 DB Connection Pool로 병목이 이동&lt;/b&gt;한다&lt;/li&gt;
&lt;li data-end=&quot;2024&quot; data-start=&quot;1966&quot;&gt;저렴한 쓰레드들이 제한된 커넥션을 놓고 경쟁하면서 &lt;b&gt;커넥션 고갈과 대기 지연을 심화&lt;/b&gt;시킨다&lt;/li&gt;
&lt;/ul&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;</description>
      <category>tech</category>
      <category>HikariCP</category>
      <category>java</category>
      <category>jdbc</category>
      <category>mysql</category>
      <category>virtual thread</category>
      <category>vthread</category>
      <category>가상 쓰레드</category>
      <category>경량 쓰레드</category>
      <author>downfa11</author>
      <guid isPermaLink="true">https://downfa11.tistory.com/114</guid>
      <comments>https://downfa11.tistory.com/114#entry114comment</comments>
      <pubDate>Mon, 12 Jan 2026 22:11:43 +0900</pubDate>
    </item>
    <item>
      <title>ArgoWorkflows: 대용량 워크플로우 아카이브 조회시 Out of sort memory 기여</title>
      <link>https://downfa11.tistory.com/113</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&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 style=&quot;color: #333333;&quot;&gt;성숙도가 높은 오픈소스에 기여하다보니 굵직한 기여도 못했다고 생각하고, 대부분 기술적으로 블로그에 작성할 분량이 나오지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;325&quot; data-end=&quot;340&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그런데 이번 이슈는 좀 달랐다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;342&quot; data-end=&quot;476&quot;&gt;
&lt;li data-start=&quot;342&quot; data-end=&quot;367&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;장애 상황이 명확하게 재현 가능했다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;368&quot; data-end=&quot;412&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;장애 원인이 애플리케이션 수준이 아니라 DB 수준까지 이어졌다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;413&quot; data-end=&quot;476&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;v3.7 업데이트 과정에서 생긴 성능 개선이 운영 환경에서 새로운 장애를 만들어냈다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;478&quot; data-end=&quot;525&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;단순한 버그 수정이라기보단 '최적화가 운영 환경에서 안전한가?'라는 고민을 해볼 수 있었기에 기록할 가치가 있다고 생각했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;478&quot; data-end=&quot;525&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;478&quot; data-end=&quot;525&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Argo Workflows란?&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;576&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhzSxL/dJMcaaxfppj/9KrpGm7ftPaw3M2Z5kzDwk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhzSxL/dJMcaaxfppj/9KrpGm7ftPaw3M2Z5kzDwk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhzSxL/dJMcaaxfppj/9KrpGm7ftPaw3M2Z5kzDwk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhzSxL%2FdJMcaaxfppj%2F9KrpGm7ftPaw3M2Z5kzDwk%2Fimg.jpg&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;380&quot; height=&quot;214&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;576&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;478&quot; data-end=&quot;525&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Kubernetes 위에서 배치 작업과 파이프라인을 선언적으로 실행, 관리하기 위한 워크플로우 엔진이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1745&quot; data-origin-height=&quot;535&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v7HMy/dJMcac2SezU/cb2P0XkggAkHJlFkyU87E1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v7HMy/dJMcac2SezU/cb2P0XkggAkHJlFkyU87E1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v7HMy/dJMcac2SezU/cb2P0XkggAkHJlFkyU87E1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv7HMy%2FdJMcac2SezU%2Fcb2P0XkggAkHJlFkyU87E1%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;1745&quot; height=&quot;535&quot; data-origin-width=&quot;1745&quot; data-origin-height=&quot;535&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;478&quot; data-end=&quot;525&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;478&quot; data-end=&quot;525&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;k8s native로 모든 워크플로우를 CRD로 정의해서 작업 단계와 의존성을 선언한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;478&quot; data-end=&quot;525&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;478&quot; data-end=&quot;525&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;DAG나 복잡한 작업 흐름을 표현하여&amp;nbsp;각 워크로드의 단계(step)가 컨테이너로 실행되기 때문에 재현성이 좋다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;대용량 워크플로우 환경에서 발생한 'Out of sort memory' 장애&lt;/span&gt;&lt;/h2&gt;
&lt;figure id=&quot;og_1768376517443&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;UI shows out of sort memory &amp;middot; Issue #14240 &amp;middot; argoproj/argo-workflows&quot; data-og-description=&quot;Pre-requisites I have double-checked my configuration I have tested with the :latest image tag (i.e. quay.io/argoproj/workflow-controller:latest) and can confirm the issue still exists on :latest. ...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/argoproj/argo-workflows/issues/14240&quot; data-og-url=&quot;https://github.com/argoproj/argo-workflows/issues/14240&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/oHD63/dJMb8RjVhrA/jTuFvDmATObNHfhdrthUb1/img.png?width=3022&amp;amp;height=1270&amp;amp;face=0_0_3022_1270&quot;&gt;&lt;a href=&quot;https://github.com/argoproj/argo-workflows/issues/14240&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/argoproj/argo-workflows/issues/14240&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/oHD63/dJMb8RjVhrA/jTuFvDmATObNHfhdrthUb1/img.png?width=3022&amp;amp;height=1270&amp;amp;face=0_0_3022_1270');&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;UI shows out of sort memory &amp;middot; Issue #14240 &amp;middot; argoproj/argo-workflows&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Pre-requisites I have double-checked my configuration I have tested with the :latest image tag (i.e. quay.io/argoproj/workflow-controller:latest) and can confirm the issue still exists on :latest. ...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.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;span style=&quot;color: #333333;&quot;&gt;Argo workflows 오픈소스에 제기한 오류(#14240)는 50개 이상의 대용량 워크플로우를 생성한 경우 발생하는 'Out of sort memory' 장애이다.&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 style=&quot;color: #333333;&quot;&gt;아래의 스크린샷처럼 워크플로우가 종료되어 아카이브된 이후 &lt;b&gt;ListWorkflows UI 자체가 로드되지 않는 현상&lt;/b&gt;이 발생한 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2116&quot; data-origin-height=&quot;491&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lh5pj/dJMcahQGBC5/v5jlPlwrU1hlmFMsMZzCI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lh5pj/dJMcahQGBC5/v5jlPlwrU1hlmFMsMZzCI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lh5pj/dJMcahQGBC5/v5jlPlwrU1hlmFMsMZzCI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flh5pj%2FdJMcahQGBC5%2Fv5jlPlwrU1hlmFMsMZzCI0%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;2116&quot; height=&quot;491&quot; data-origin-width=&quot;2116&quot; data-origin-height=&quot;491&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;&lt;span style=&quot;color: #333333;&quot;&gt;서버 로그:&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1768152528677&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;rpc error: code = Internal desc = 
Error 1038 (HY001): Out of sort memory, consider increasing server sort buffer size&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 style=&quot;color: #333333;&quot;&gt;OOM도 아니고&amp;nbsp;MySQL sort buffer이 문제가 되고 있다. UI상에서 리스트 하나 조회하는데 DB 메모리가 부족하다는 소리다. 왜 그럴까?&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;1. 원인 파악&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;눈여겨볼 점은&amp;nbsp;&lt;/span&gt;v3.6.4 버전에서는 잘 작동했지만 최신 버전에서 문제가 발생한다는 것이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;환경도 동일하고, DB도 동일한데도&amp;nbsp;&lt;b&gt;버전 업그레이드 후에만 장애가 발생&lt;/b&gt;한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;965&quot; data-end=&quot;981&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그래서 해당 ListWorkflows 메서드에 어떤 변경점이 있었는지 &lt;span style=&quot;text-align: start;&quot;&gt;커밋 이력을 따라가봤는데, 쿼리의 성능 개선이 있었다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;v3.6 &lt;/span&gt;ListWorkflows 메서드의 쿼리 최적화 (&lt;span style=&quot;text-align: start;&quot;&gt;PR &lt;/span&gt;#&lt;span style=&quot;text-align: start;&quot;&gt;13819)&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이전 버전까지의 Listworkflows 요청은 몇몇 최적화 작업 이후에도 워크플로우 수가 매우 많거나(100,000개 이상) 워크플로 평균 크기가 큰(100KB) 경우 느려질 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;여기서 MySQL에서 약 90%, PostgreSQL에서 약 50%까지 쿼리 속도를 높이는 추가적인 최적화를 진행한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;MySQL 최적화&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;옵티마이저가 argo_archived_workflows_i4 인덱스 말고 훨씬 더 비용이 많이 드는 PK를 사용하는 것이 원인이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;기존 인덱스(argo_archived_workflows_i4)를 삭제하고, (clustername, startedat) 복합 인덱스 재생성해서 해결&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;20&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;ListWorkflows: 약 96% 성능 향상 (43.51ms -&amp;gt; 1.65ms)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;ListWorkflows_with_label_selector: 약 93% 성능 향상 (69.64ms -&amp;gt; 4.51ms)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;PostgreSQL 최적화&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;병목 현상은 detoasting(압축 해제 및 읽기) 오버헤드였고 workflow가 detoast 되어야 하는 횟수를 줄이기 위해서 CTF으로 해결&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;17&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;ListWorkflows: 약 61% 성능 향상 (25.11ms -&amp;gt; 9.69ms)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;ListWorkflows_with_label_selector: 약 57% 성능 향상 (26.14ms -&amp;gt; 11.06ms)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;분명 테스트에서 괄목할만한 성능 개선이 이뤄졌다. 벤치마크는 성공적이지만 MySQL 운영 환경에서 sort_buffer_size에 관한 고려가 없었다.&amp;nbsp;&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 style=&quot;color: #333333;&quot;&gt;쿼리 시간은 확연히 빨라졌지만 Sorting 단계에서 JSON payload를 다루게 되면서 그 비용은 고스란히 MySQL sort buffer의 몫이 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;2. 장애 상황의 재현&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;argo workflows에서 아카이브 기능을 활성화하면 다음 테이블들이 생성된다.&lt;/span&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;span style=&quot;color: #333333;&quot;&gt;argo_workflows: 워크플로우 상태 저장&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;argo_archived_workflows: 아카이브된 워크플로우 저장&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;argo_archived_workflows_labels: 아카이브된 워크플로우 레이블&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;schema_history:&amp;nbsp;데이터베이스 마이그레이션 기록&lt;/span&gt;&lt;/li&gt;
&lt;/ul&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 style=&quot;color: #333333;&quot;&gt;개발 환경의 열악한 리소스 때문에(...) sort_buffer_size=64KB로 줄여서 더 작은 크기에서 테스트를 진행하고 싶었다. &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 style=&quot;color: #333333;&quot;&gt;그리고 다른 기여자가 이미 workflow template을 짜놓은게 있어서 재현하기 편할 거라고 생각했다.&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 style=&quot;color: #333333;&quot;&gt;mysql depoyment에서 글로벌 변수를 변경해보려 했는데, spce.image같은 템플릿이 맞지 않아서 워크플로우 아카이브가 이뤄지지 않았다.&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 style=&quot;color: #333333;&quot;&gt;결국 default인 256KB를 유지하되, 워크플로우를 직접 실행하지 않고 아카이브 테이블에 직접 대용량의 JSON을 기록해서 'Out of sort memory' 상황을 재현했다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재현된 데이터 히스토그램:&lt;/p&gt;
&lt;pre id=&quot;code_1768376675494&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mysql&amp;gt; SELECT FLOOR(LENGTH(workflow)/10240) * 10 AS size_range_kb, COUNT(*) AS count FROM argo_archived_workflows GROUP BY 1 ORDER BY 1;
+---------------+-------+
| size_range_kb | count |
+---------------+-------+
|             0 |    25 |
|           190 |    10 |
+---------------+-------+
2 rows in set (0.00 sec)&lt;/code&gt;&lt;/pre&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;span style=&quot;color: #333333;&quot;&gt;2-1. 아카이빙된 워크플로우의 크기&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1768152299423&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT name, LENGTH(workflow) FROM argo_archived_workflows WHERE namespace = 'argo';
+---------------------+------------------+
| name                | LENGTH(workflow) |
+---------------------+------------------+
| giant-workflow-test |           316188 |
+---------------------+------------------+
1 row in set (0.00 sec)&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 style=&quot;color: #333333;&quot;&gt;sort_buffer_size(256KB)보다 큰 309KB쯤 된다.&lt;/span&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;span style=&quot;color: #333333;&quot;&gt;2-2. 문제가 발생했던 v3.7 style 쿼리 재현&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1768152333223&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mysql&amp;gt; explain SELECT `name`, `namespace`, `uid`, `phase`, `startedat`, `finishedat`, `creationtimestamp`,
    -&amp;gt;        coalesce(workflow-&amp;gt;'$.metadata.labels', '{}') as labels,
    -&amp;gt;        coalesce(workflow-&amp;gt;'$.metadata.annotations', '{}') as annotations,
    -&amp;gt;        coalesce(workflow-&amp;gt;&amp;gt;'$.status.progress', '') as progress,
    -&amp;gt;        workflow-&amp;gt;&amp;gt;'$.spec.suspend',
    -&amp;gt;        coalesce(workflow-&amp;gt;&amp;gt;'$.status.message', '') as message,
    -&amp;gt;        coalesce(workflow-&amp;gt;&amp;gt;'$.status.estimatedDuration', '0') as estimatedduration,
    -&amp;gt;        coalesce(workflow-&amp;gt;'$.status.resourcesDuration', '{}') as resourcesduration
    -&amp;gt; go_archived_workFROM `argo_archived_workflows`
    -&amp;gt; WHERE ((`clustername` = 'default' AND `instanceid` = '') AND `namespace` = 'test'
    -&amp;gt;       AND not exists (select 1 from argo_archived_workflows_labels
    -&amp;gt;                       where clustername = argo_archived_workflows.clustername
    -&amp;gt;                         and uid = argo_archived_workflows.uid
    -&amp;gt;                         and name = 'workflows.argoproj.io/controller-instanceid'))
    -&amp;gt; ORDER BY `startedat` DESC
    -&amp;gt; LIMIT 50;
+----+--------------+--------------------------------+------------+--------+---------------------------------------------------------------------------------------------------------------------+----------------------------+---------+---------------------------------------------------------------------------+------+----------+--------------------------+
| id | select_type  | table                          | partitions | type   | possible_keys                                                                                                       | key                        | key_len | ref                                                                       | rows | filtered | Extra                    |
+----+--------------+--------------------------------+------------+--------+---------------------------------------------------------------------------------------------------------------------+----------------------------+---------+---------------------------------------------------------------------------+------+----------+--------------------------+
|  1 | SIMPLE       | argo_archived_workflows        | NULL       | ref    | PRIMARY,argo_archived_workflows_i1,argo_archived_workflows_i2,argo_archived_workflows_i3,argo_archived_workflows_i4 | argo_archived_workflows_i1 | 1542    | const,const,const                                                         |   10 |   100.00 | Using filesort           |
|  1 | SIMPLE       | &amp;lt;subquery2&amp;gt;                    | NULL       | eq_ref | &amp;lt;auto_distinct_key&amp;gt;                                                                                                 | &amp;lt;auto_distinct_key&amp;gt;        | 774     | argo.argo_archived_workflows.clustername,argo.argo_archived_workflows.uid |    1 |   100.00 | Using where; Not exists  |
|  2 | MATERIALIZED | argo_archived_workflows_labels | NULL       | ref    | PRIMARY,argo_archived_workflows_labels_i1                                                                           | PRIMARY                    | 258     | const                                                                     |    1 |   100.00 | Using where; Using index |
+----+--------------+--------------------------------+------------+--------+---------------------------------------------------------------------------------------------------------------------+----------------------------+---------+---------------------------------------------------------------------------+------+----------+--------------------------+
3 rows in set, 3 warnings (0.00 sec)&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 style=&quot;color: #333333;&quot;&gt;Sorting 단계에서 &lt;b&gt;row 전체(JSON payload 포함)&lt;/b&gt;를 다루면서 Out of sort memory 상황을 재현할 수 있었다.&lt;/span&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;span style=&quot;color: #333333;&quot;&gt;2-3. 이전 버전(v3.6) style 쿼리 재현&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1768152369743&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mysql&amp;gt; SELECT name, namespace, uid, phase, startedat, finishedat,
    -&amp;gt;   COALESCE(JSON_EXTRACT(workflow, '$.metadata.labels'), '{}') AS labels,
    -&amp;gt;   COALESCE(JSON_EXTRACT(workflow, '$.metadata.annotations'), '{}') AS annotations,  
    -&amp;gt;   COALESCE(JSON_UNQUOTE(JSON_EXTRACT(workflow, '$.status.progress')), '') AS progress,
      (JSON_UNQUOTE(JS -&amp;gt; COALESCE(JSON_UNQUOTE(JSON_EXTRACT(workflow, '$.metadata.creationTimestamp')), '') AS creationtimestamp,
    -&amp;gt;   JSON_UNQUOTE(JSON_EXTRACT(workflow, '$.spec.suspend')) AS suspend,
    -&amp;gt;   COALESCE(JSON_UNQUOTE(JSON_EXTRACT(workflow, '$.status.message')), '') AS message,
    -&amp;gt;   COALESCE(JSON_UNQUOTE(JSON_EXTRACT(workflow, '$.status.estimatedDuration')), '0') AS estimatedduration,
    -&amp;gt;   COALESCE(JSON_EXTRACT(workflow, '$.status.resourcesDuration'), '{}') AS resourcesduration
    -&amp;gt; FROM argo_archived_workflows
    -&amp;gt; WHERE namespace = 'argo'
    -&amp;gt; AND uid IN (
    -&amp;gt;   SELECT * FROM (
    -&amp;gt;     SELECT uid FROM argo_archived_workflows
    -&amp;gt;     WHERE namespace = 'argo'
    -&amp;gt;     ORDER BY startedat DESC LIMIT 100 OFFSET 0
    -&amp;gt;   ) AS x
    -&amp;gt; );
+---------------------+-----------+--------------+-----------+---------------------+---------------------+--------+-------------+----------+-------------------+---------+---------+-------------------+-------------------+
| name                | namespace | uid          | phase     | startedat           | finishedat          | labels | annotations | progress | creationtimestamp | suspend | message | estimatedduration | resourcesduration |
+---------------------+-----------+--------------+-----------+---------------------+---------------------+--------+-------------+----------+-------------------+---------+---------+-------------------+-------------------+
| giant-workflow-test | argo      | test-uid-999 | Succeeded | 2026-01-10 08:58:09 | 2026-01-10 08:58:09 | {}     | {}          |          |                   | NULL    |         | 0                 | {}                |
+---------------------+-----------+--------------+-----------+---------------------+---------------------+--------+-------------+----------+-------------------+---------+---------+-------------------+-------------------+
1 row in set (0.00 sec)&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 style=&quot;color: #333333;&quot;&gt;그에 반면 이전 버전의 쿼리를 실행하면 먼저 UID만 정렬하고서 필요한 row들만 새로 조회한다. 정렬 과정에서 짱큰 JSON payload를 다루지 않아서 안정적이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;첨언) 옵티마이저가 시간순 인덱스 대신 네임스페이스 인덱스를 선택하면 Using filesort가 발생한다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;이때 정렬을 위해 SELECT 절의 모든 컬럼을 sort_buffer에 올리는데, 문제는 SELECT 절에 무거운 JSON 추출 함수를 대거 포함했다는 점이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;20&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;20,0,0&quot;&gt;워크플로우 한 행의 크기:&lt;/b&gt; ~200KB 이상&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;20,1,0&quot;&gt;sort_buffer_size:&lt;/b&gt; 256KB&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;21&quot; data-ke-size=&quot;size16&quot;&gt;sort_buffer는 64~256KB인데, 올려야 할 row가 200KB라면? &lt;b&gt;row 하나도 buffer에 담지 못하고 쿼리는 즉시 터진다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;21&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;3. 해결 방안에 대한 고민&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;3-1. 인덱스 튜닝 시도 (feat. 커버링 인덱스)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;사실 쿼리 개선 과정에서 발생한 문제라, 튜닝으로 해결해야 한다는 고정관념에 잡혀서 시간을 좀 잡아먹었다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;인덱스를 통해서 정렬 과정을 생략할 수는 없을까?&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;사용하는 인덱스 목록:&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1768158954130&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mysql&amp;gt; SHOW INDEX FROM argo_archived_workflows;
+-------------------------+------------+----------------------------+--------------+-------------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| Table                   | Non_unique | Key_name                   | Seq_in_index | Column_name       | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Visible | Expression |
+-------------------------+------------+----------------------------+--------------+-------------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| argo_archived_workflows |          0 | PRIMARY                    |            1 | clustername       | A         |           0 |     NULL |   NULL |      | BTREE      |         |               | YES     | NULL       |
| argo_archived_workflows |          0 | PRIMARY                    |            2 | uid               | A         |           0 |     NULL |   NULL |      | BTREE      |         |               | YES     | NULL       |
...
| argo_archived_workflows |          1 | argo_archived_workflows_i4 |            1 | clustername       | A         |           0 |     NULL |   NULL |      | BTREE      |         |               | YES     | NULL       |
| argo_archived_workflows |          1 | argo_archived_workflows_i4 |            2 | startedat         | A         |           0 |     NULL |   NULL |      | BTREE      |         |               | YES     | NULL       |
+-------------------------+------------+----------------------------+--------------+-------------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
14 rows in set (16.00 sec)&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 style=&quot;color: #333333;&quot;&gt;v3.7 쿼리는 ORDER BY startedat DESC LIMIT 100으로 실행하면 (namespace, startedat) 인덱스 잘 타고 있다.&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 style=&quot;color: #333333;&quot;&gt;정렬 자체의 문제가 아니라 &lt;b&gt;정렬시 다뤄야 하는 row 데이터(JSON payload) 크기가 커서 sort buffer에 올라가다 터진거&lt;/b&gt;다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그렇다고 커버링 인덱스(covering index)로 우회한다?&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1768159441676&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE INDEX idx_cover_json
ON argo_archived_workflows (
  namespace,
  startedat DESC,
  (JSON_EXTRACT(workflow, '$.metadata.labels')),
  (JSON_EXTRACT(workflow, '$.metadata.annotations')),
  (JSON_UNQUOTE(JSON_EXTRACT(workflow, '$.status.progress'))),
  (JSON_UNQUOTE(JSON_EXTRACT(workflow, '$.status.message'))),
  (JSON_UNQUOTE(JSON_EXTRACT(workflow, '$.status.estimatedDuration')))
);&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 style=&quot;color: #333333;&quot;&gt;이런 식으로 json 필드까지 인덱스에 넣어버리겠다는 소리인데 인덱스가 payload를 고대로 복제해서 스토리지 낭비가 발생한다.&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 style=&quot;color: #333333;&quot;&gt;앞서 재현하려고 만든 워크플로우 크기가 약 300KB이다. 단순 계산으로 10,000개 있다고 치면 테이블만 3GB, 커버링 인덱스로도 그쯤 쓰인다. &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 style=&quot;color: #333333;&quot;&gt;정렬 한번 빠르게 해보겠다고 스토리지 3GB를 낭비하게 된다.&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 style=&quot;color: #333333;&quot;&gt;이러면 워크플로우 schema 변경할때마다 인덱스도 재설계해줘야하는 불편함도 한 몫한다. 즉, &lt;b&gt;이거 인덱스로 해결할 수 있는 문제가 아니였다.&lt;/b&gt;&lt;/span&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;span style=&quot;color: #333333;&quot;&gt;3-2. 워크플로우 크기별 테이블 분리, 파티셔닝 시도&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;사실 가장 무식한 방법은 사용자가 DB sort_buffer_size를 직접 조절하도록 강제하는 것이다. 권장하지 않는 해결책이다.&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 style=&quot;color: #333333;&quot;&gt;워크플로우 크기별로 테이블 분리하거나 파티셔닝도 생각해봤지만, 이 역시도 사용자에게 DB 튜닝을 강제한다. &lt;s&gt;&lt;i&gt;너가해&lt;/i&gt;&lt;/s&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 style=&quot;color: #333333;&quot;&gt;사용자에게 책임을 떠넘기지 않기 위해서는 v3.6의 복잡하고 느리지만 안전한 쿼리와 v3.7의 sort buffer 장애 가능성 간의 trade-off를 고려해야 했다.&lt;/span&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;span style=&quot;color: #333333;&quot;&gt;3-3. 워크플로우 크기와 sort_buffer_size 비교를 통한 쿼리 전략 선택&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음 제시한 해결책은 다음과 같다.&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;워크플로우 크기에 따라 다른 쿼리를 사용하도록 조건부 로직 수정&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;쉽게 말해서 워크플로우의 크기가 작으면 v3.7 쿼리를 사용하고 대용량의 경우는 이전 쿼리를 사용한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;문제가 된 MySQL에서만&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;sort buffer와 워크플로우 크기를 비교해서 안전한 쿼리를 선택&lt;/b&gt;하도록 구현했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;대부분의 상황에서는 v3.7의 성능 개선을 그대로 누리면서도, 대용량 워크플로우 상황에서 장애를 막을 수 있다.&lt;/li&gt;
&lt;li&gt;sort_buffer_size를 사용자가 직접 튜닝하도록 강제하지 않는다.​&lt;/li&gt;
&lt;li&gt;MySQL의 로직 변경이 PostgreSQL 설계에는 영향을 주지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ListWorkflows 흐름을 다음과 같이 리팩토링했다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;ListWorkflows (MySQL, Postgres&lt;span style=&quot;color: #333333;&quot;&gt;별 로직 분리)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;listWorkflowsMySQL, listWorkflowsPostgres&amp;nbsp;&lt;/li&gt;
&lt;li&gt;convertToWorkflows&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1768153388209&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func (r *workflowArchive) listWorkflowsMySQL(ctx context.Context, options sutils.ListOptions) (wfv1.Workflows, error) {
	sortBufferSize, err := r.getSortBufferSize(ctx)
	if err != nil {
		return nil, err
	}

	avgSize, err := r.getAverageWorkflowSize(ctx)
	if err != nil {
		return nil, err
	}

	if avgSize &amp;gt; sortBufferSize-1024 { // margin
		return r.listWorkflowsV36(ctx, options)
	}
	return r.listWorkflowsV37(ctx, options)
}

// listWorkflowsV36 uses the subquery approach for large workflows
func (r *workflowArchive) listWorkflowsV36(_ context.Context, options sutils.ListOptions) (wfv1.Workflows, error) {
	...
	return r.convertToWorkflows(archivedWfs)
}

// listWorkflowsV37 uses the direct JSON extraction for small workflows
func (r *workflowArchive) listWorkflowsV37(_ context.Context, options sutils.ListOptions) (wfv1.Workflows, error) {
	...
	return r.convertToWorkflows(archivedWfs)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;getSortBufferSize(ctx context.Context): MySQL sort_buffer_size 조회&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;getAverageWorkflowSize(ctx context.Context): 워크플로우들의 평균 크기 연산&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(26.1.14) 아직 메인테너와 해결책 논의중인데, 쿼리 튜닝쪽으로 다시 가닥을 잡고 있다. 나중에 PR 올리면 다시 수정하겠음&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;&lt;span style=&quot;color: #333333;&quot;&gt;4. 현황 및 새로 배운 점&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;안정적이지만 느린 쿼리(v3.6), 쿼리를 개선했지만 불안정한 쿼리(v3.7)를 어떻게 해결할지 고민했었다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;빠른 쿼리와 안전한 쿼리 사이의 트레이드오프를 사용자에게 떠넘길 것인가, 아니면 코드에서 감당할 것인가?&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;#13819 PR의 쿼리 성능 개선은 벤치마크 기준으로 훌륭했지만, 대용량 워크플로우 운영시 장애가 발생한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;성능을 90% 올리는 것보다 중요한 것은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b data-path-to-node=&quot;34&quot; data-index-in-node=&quot;23&quot;&gt;시스템이 죽지 않는 것&lt;/b&gt;이다. 이번 기여를 통해 최적화가 실제 운영 데이터와 만났을 때 어떤 사이드 이펙트를 낼 수 있는지 깊게 배울 수 있었던 값진 경험이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;현재 문제가 되는 쿼리 성능 개선을 진행한 메인테너가 등판해서 직접 재현 과정에 대해서 논의하고 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;메인테너가 주어진 장애 환경이 어떤지 정보가 부족해서 재현에 어려움을 겪고 있었는데, 그걸 내가 해냈다ㅋ&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;단순히 row 수만 늘린다고 터지는 문제가 아니라 재현의 핵심은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;데이터 분포&lt;/b&gt;에 있었다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot; data-path-to-node=&quot;9&quot;&gt;
&lt;li&gt;장애 재현을 위해 filesort를 타도록&lt;span&gt;&amp;nbsp;&lt;/span&gt;최신 데이터(dummy)는 가볍게, 과거 데이터(target)는 무겁게 배치해야했다.&lt;/li&gt;
&lt;li&gt;sort_buffer는 정렬할 전체 데이터 크기가 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;단 한 행을 담을 수 있느냐가 관건이었다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;장애 상황의 원인 파악은 옵티마이저가 시간순 기반의 argo_archived_workflows_i4 인덱스를 타고 내려가는게 손해라고 판단해, namespace 기반 argo_archived_workflows_i1를 선택하여 filesort한 것이었고 재현을 통해서 검증했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>tech</category>
      <category>Argo-CD</category>
      <category>argo-workflows</category>
      <category>CNCF</category>
      <category>mysql</category>
      <category>oss</category>
      <category>out of sort buffer</category>
      <category>sort_buffer_size</category>
      <category>오픈소스</category>
      <author>downfa11</author>
      <guid isPermaLink="true">https://downfa11.tistory.com/113</guid>
      <comments>https://downfa11.tistory.com/113#entry113comment</comments>
      <pubDate>Mon, 12 Jan 2026 03:38:52 +0900</pubDate>
    </item>
    <item>
      <title>InnoDB Flush 계층에서 본 Direct IO (feat. Zero Copy와의 차이점)</title>
      <link>https://downfa11.tistory.com/112</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 MySQL의 디스크 쓰기 작업에 대해서 정리하다가 O_DIRECT 옵션을 보고 이런 생각이 들었다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;zero copy랑 비슷한 얘기 아닌가? 둘 다 불필요한 복사를 줄여서 성능을 개선하는건데.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;궁금해져서 그 차이가 궁금해져서 좀 더 파고들어봤다.&amp;nbsp; 본 내용은 MySQL 8.0을 기준으로 작성했다.&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MySQL의 디스크 쓰기(Flush)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 튜닝시 귀에 피나도록 'disk IO를 최소화해야해'라는 말을 들었을 것이다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;Full table scan이 아니라 Index range scan을 그렇게 강조하고 buffer_pool에 가능한 많은 데이터를 둘려는 이유이다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;여기서 말하는 디스크 접근, 즉 &lt;b&gt;메모리에 적재된 데이터를 디스크에 영속적으로 기록하는 행위&lt;/b&gt;를 Flush라고 한다. 문제는 그 피나던 말처럼 Flush 비용이 생각보다 비싸다는 점이다..&lt;br /&gt;&amp;nbsp;&lt;br /&gt;일반적으로 시스템은 데이터를 바로 디스크에 기록하지 않는다. 메모리에 두면서 쓰기 작업들을 모아서 한번에 하는걸 본 적이 있을 것이다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;이른바 지연된 쓰기(delayed write)라고 하는데, 보통 버퍼 크기(batch_size)를 채우던가 일정 시간(linger_ms)이 지나면 Flush하는 식으로 disk IO를 줄이는 '배치 쓰기'로 이해하면 된다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;우리는 데이터를 어느정도 크기, 시간동안 메모리에 적재한 채로 두는가에 따라서 장애 발생시 유실 가능성을 점칠 수 있다. 그래서 Flush 주기나 조건은 항상 데이터베이스 튜닝시 민감한 영역이 된다.&lt;br /&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;다들 쓰기 부하가 발생하면 가장 먼저 commit 지연이 발생하는걸 performance_schema 로그를 통해 본 경험이 있을 것이다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;기본적으로 durability를 위해서 커밋시 binlog와 undo log flush하는데 2번의 fsync가 이뤄지기 때문에, 인덱스를 타지 않는 쿼리부터 시작해서 전체적으로 쿼리들이 느려지기 시작하는 거다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;MySQL의 innodb_flush_method&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;옵션&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 튜닝시 InnoDB 스토리지 엔진에서 Flush 작업을 어떻게 수행할 건지 결정하는 innodb_flush_method 옵션이 존재한다.&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;fsync&lt;/b&gt;: 데이터와 metadata 로그를 모두 디스크에 sync하는 syscall&lt;/li&gt;
&lt;li&gt;&lt;b&gt;O_DIRECT&lt;/b&gt;: metadat 로그의 읽기 작업시는 Direct IO를 이용해서 빠르게 가져오고, 쓰기 작업은 안전하게 fsync하는 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Buffered IO&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 OS 수준의 캐시로 읽기 속도를 올린다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;그러니까, DB에 저장된 내용을 디스크에 기록하기 전에 커널 영역(OS Page Cache)에 캐싱하는 과정이 발생한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;795&quot; data-origin-height=&quot;198&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tIIms/dJMb99ZoR0s/TiykbX5w4vsONaMriCEji1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tIIms/dJMb99ZoR0s/TiykbX5w4vsONaMriCEji1/img.jpg&quot; data-alt=&quot;fsync 방식의 flush 동작&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tIIms/dJMb99ZoR0s/TiykbX5w4vsONaMriCEji1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtIIms%2FdJMb99ZoR0s%2FTiykbX5w4vsONaMriCEji1%2Fimg.jpg&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;795&quot; height=&quot;198&quot; data-origin-width=&quot;795&quot; data-origin-height=&quot;198&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;fsync 방식의 flush 동작&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;하지만 InnoDB는 훨씬 고도화된 Buffer Pool이 존재하기 때문에, OS 캐싱 과정이 불필요하다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;Page Cache가 보조 캐시 역할을 한다면 시너지가 나겠지만 그렇지 않은 경우에는 메모리 낭비로 이어진다. (double buffering)&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Direct IO&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해서 커널 영역에 캐싱하는 중간 과정을 생략한 채로 디스크와 직접 접근하는 Direct IO 기술이 사용된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;795&quot; data-origin-height=&quot;258&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRb4TV/dJMcabCTFHd/U8PMkuiHKYBnHK0f1EnTik/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRb4TV/dJMcabCTFHd/U8PMkuiHKYBnHK0f1EnTik/img.jpg&quot; data-alt=&quot;O_DIRECT 방식의 flush 동작&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRb4TV/dJMcabCTFHd/U8PMkuiHKYBnHK0f1EnTik/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRb4TV%2FdJMcabCTFHd%2FU8PMkuiHKYBnHK0f1EnTik%2Fimg.jpg&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;795&quot; height=&quot;258&quot; data-origin-width=&quot;795&quot; data-origin-height=&quot;258&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;O_DIRECT 방식의 flush 동작&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;user space의 InnoDB가 데이터를 캐시하면서 OS cache를 무시하는 셈이다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;플랫폼에 따라서 fdatasync()을 지원하는 경우가 있는데, 얘는 metadat만 필요할때 fsync하는 식으로 디스크 IO를 최소화한다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;여담으로 O_DSYNC라는 친구도 있긴 한데.. 몰라도 될거 같다. metadata 로그만 O_SYNC, 데이터는 fsync 하는 방식으로 대부분의 UNIX 계통 InnoDB에서 더이상 지원하지 않고 있다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. fdatasync() vs O_DIRECT&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 설명만 보면 자연스럽게 O_DIRECT가 항상 더 좋아 보인다. &lt;span style=&quot;color: #333333;&quot;&gt;불필요한 캐시 과정이 생략되면서 메모리 이점을 볼 수 있으면 좋은거 아닌가 싶지만 &lt;/span&gt;&lt;b&gt;역시 은탄환은 없다&lt;/b&gt;.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;아래 블로그에서 직접 flush_method에 따라서 벤치마크를 진행한 결과가 있다.&lt;/p&gt;
&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;MySQL innodb_flush_method 튜닝 포인트 | Mimul Tech blog&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;MySQL InnoDB 스토리지 엔진을 사용하면 매개 변수 innodb_flush_method가 있는데 이 설정 값의 의미와 테스트를 통해 튜닝 포인트를 검토.&quot; data-og-host=&quot;www.mimul.com&quot; data-og-source-url=&quot;https://www.mimul.com/blog/sysvar_innodb_flush_method/&quot; data-og-image=&quot;https://blog.kakaocdn.net/dna/4iKb0/hyZRqRh27g/AAAAAAAAAAAAAAAAAAAAALOorC9OM7uwB-dLoBmad88t6AGVG83UDOnYPV96f66L/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1769871599&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=HDpmpgfEBtPq2R6tOxviAiJboWQ%3D&quot; data-og-url=&quot;https://www.mimul.com/blog/sysvar_innodb_flush_method/&quot;&gt;&lt;a href=&quot;https://www.mimul.com/blog/sysvar_innodb_flush_method/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.mimul.com/blog/sysvar_innodb_flush_method/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://blog.kakaocdn.net/dna/4iKb0/hyZRqRh27g/AAAAAAAAAAAAAAAAAAAAALOorC9OM7uwB-dLoBmad88t6AGVG83UDOnYPV96f66L/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1769871599&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=HDpmpgfEBtPq2R6tOxviAiJboWQ%3D');&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;MySQL innodb_flush_method 튜닝 포인트 | Mimul Tech blog&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;MySQL InnoDB 스토리지 엔진을 사용하면 매개 변수 innodb_flush_method가 있는데 이 설정 값의 의미와 테스트를 통해 튜닝 포인트를 검토.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.mimul.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;br /&gt;buffer_pool의 크기가 128M 이하일때 fdatasync 방식이 우세하고, 128M 이후부터는 O_DIRECT 방식이 더 높은 결과를 보여주고 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;291&quot; data-origin-height=&quot;173&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/r3Yqf/dJMcabXc8vy/ZKDyhnlSLqHvT8LSMXMaRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/r3Yqf/dJMcabXc8vy/ZKDyhnlSLqHvT8LSMXMaRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/r3Yqf/dJMcabXc8vy/ZKDyhnlSLqHvT8LSMXMaRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fr3Yqf%2FdJMcabXc8vy%2FZKDyhnlSLqHvT8LSMXMaRK%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;291&quot; height=&quot;173&quot; data-origin-width=&quot;291&quot; data-origin-height=&quot;173&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;작성자분께서는 buffer pool의 크기가 더 작은 경우에서 fsync 계열이 성능이 더 잘 나온 이유로 Page Cache가 제 역할(여기서는 보조 캐시)을 해준 것이라 보셨다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;모든 데이터가 buffer pool에 들어갈 크기는 되어야 O_DIRECT의 성능적 이점이 나온다고 설명하셨다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #4a4a4a;&quot;&gt;'추측하지 말라, 데이터를 보고 계산된 예측을 하라' 라고 하시는데 진짜 DBA같아서 심금을 울린다.... &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #4a4a4a;&quot;&gt;너무멋있으니 다들 보고 가삼&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Zero Copy와의 차이점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 &lt;b&gt;불필요한 복사 과정을 최소화&lt;/b&gt;해서 성능을 개선하는 기술이라는 점에서 비슷하다고 생각했다. 그럼 어떤 차이점이 있을까?&lt;br /&gt;&amp;nbsp;&lt;br /&gt;비교를 위해서 직접 그렸다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;799&quot; data-origin-height=&quot;355&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XNvn4/dJMcadgnA3A/ATNKS3HXzFJ1ixl64YYubk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XNvn4/dJMcadgnA3A/ATNKS3HXzFJ1ixl64YYubk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XNvn4/dJMcadgnA3A/ATNKS3HXzFJ1ixl64YYubk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXNvn4%2FdJMcadgnA3A%2FATNKS3HXzFJ1ixl64YYubk%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;799&quot; height=&quot;355&quot; data-origin-width=&quot;799&quot; data-origin-height=&quot;355&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;direct IO같은 경우는 Page Cache(kernel)를 건너뛰고 디스크에 바로 접근한다. (OS 캐싱 과정을 생략)&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;/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;metadata 로그의 읽기는 direct IO, 쓰기는 안전하게 fsync한다고만 설명했었다. &lt;b&gt;데이터 쓰기/읽기시에는 모두 Direct IO 처리&lt;/b&gt;한다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;652&quot; data-origin-height=&quot;355&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjt41i/dJMcahpA0wj/HGBmLkHyWAkCyDMXOMZEp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjt41i/dJMcahpA0wj/HGBmLkHyWAkCyDMXOMZEp1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjt41i/dJMcahpA0wj/HGBmLkHyWAkCyDMXOMZEp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcjt41i%2FdJMcahpA0wj%2FHGBmLkHyWAkCyDMXOMZEp1%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;652&quot; height=&quot;355&quot; data-origin-width=&quot;652&quot; data-origin-height=&quot;355&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;zero copy는 커널 영역에서 fs의 종류에 따라서 다른 Page Cache나 Socket Buffer로 전달한다. (User Buffer 복사를 생략)&lt;br /&gt;&amp;nbsp;&lt;br /&gt;이 둘은 겉보기에 비슷해보여도 &lt;b&gt;서로 생략하는 복사의 영역도 다르고 그 목적도 다르다&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;Direct IO: 중간자 역할인 커널모드 캐싱을 건너뛰고 전달&lt;/li&gt;
&lt;li&gt;Zero copy: 유저모드로의 복사를 건너뛰고 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;zero copy 자체가 비동기 통신쪽에서 자주 언급되다 보니 소켓 네트워크 기술 내지는 SKB로만 가는걸로 오해할 수 있다. 하지만 파일도 소켓도 모두 file descriptor라는 점!!&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;출처 및 인용.&lt;br /&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/innodb-parameters.html#sysvar_innodb_flush_method&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;https://dev.mysql.com/doc/refman/8.0/en/innodb-parameters.html#sysvar_innodb_flush_method&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://medium.com/@nuwanwe/innodb-flush-method-balancing-performance-and-data-integrity-d39d8ac50766&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;https://medium.com/@nuwanwe/innodb-flush-method-balancing-performance-and-data-integrity-d39d8ac50766&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;</description>
      <category>mysql</category>
      <category>buffer_pool</category>
      <category>Direct IO</category>
      <category>flush</category>
      <category>fsync</category>
      <category>InnoDB</category>
      <category>innodb_flush_method</category>
      <category>mysql</category>
      <category>O_DIRECT</category>
      <category>zero copy</category>
      <author>downfa11</author>
      <guid isPermaLink="true">https://downfa11.tistory.com/112</guid>
      <comments>https://downfa11.tistory.com/112#entry112comment</comments>
      <pubDate>Fri, 9 Jan 2026 21:54:04 +0900</pubDate>
    </item>
    <item>
      <title>Cursus: BloomFilter를 이용한 컨슈머 벤치마크의 정확성 검사 확장</title>
      <link>https://downfa11.tistory.com/111</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;현재 컨슈머측 벤치마크 측정은 단순하게 전체 TPS와 발행된 메시지 수와 비교해서 유실된 메시지 수만 표시한다.&lt;/p&gt;
&lt;pre id=&quot;code_1767424259540&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;broker-consumer   | Total messages consumed      : 100000
broker-consumer   | Actually processed messages  : 100000
broker-consumer   | Duplicate messages filtered  : 0
broker-consumer   | Consume elapsed time         : 398.445467ms
broker-consumer   | Consume Throughput           : 250975.37 msg/s
broker-consumer   | Processed Throughput         : 250975.37 msg/s

broker-consumer   | --- Partition Message Counts ---
broker-consumer   | Partition [4] : 8333 messages
broker-consumer   | Partition [11] : 8333 messages
broker-consumer   | Partition [8] : 8333 messages
broker-consumer   | Partition [1] : 8334 messages
broker-consumer   | Partition [6] : 8333 messages
broker-consumer   | Partition [10] : 8333 messages
broker-consumer   | Partition [7] : 8333 messages
broker-consumer   | Partition [5] : 8333 messages
broker-consumer   | Partition [3] : 8334 messages
broker-consumer   | Partition [2] : 8334 messages
broker-consumer   | Partition [0] : 8334 messages
broker-consumer   | Partition [9] : 8333 messages&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;이를 파티션 모델에 특화된 지표를 수집하는 벤치마크로 개선하는 과정에서, 성능 측정을 위한 벤치마크와 정확도 검사를 명확히 분리할 필요가 있다고 판단했다. 이제 enable_correctness, enable_benchmark 옵션으로 나눠서 메시지 소비가 얼마나 정확하게 이뤄지는지 검사한다.&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;이미 producer측에서 멱등성(idempotency) 처리나 브로커측에서 중복에 대한 처리를 수행하지만, 그 처리 결과를 검증하는 구간은 종단간(end-to-end) 테스트에서 메시지를 최종적으로 소비할 컨슈머가 맡아야 한다고 생각했다.&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;메시지의 중복 발행 문제는 동일한 partition-offset 꼴을 갖기 때문에 필터링하기 쉬운 편이다. 하지만 브로커측의 오류로 제때 발행에 대한 ACK를 주지 못한 경우는 같은 메시지를 다른 offset으로 멱등하게 재처리(retry)한다.&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;이러면 같은 내용의 메시지여도 partition-offset 꼴이 달라지기 때문에 중복을 판단하는게 불가능하다. 그럼 우째요?&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;블룸 필터(Bloom Filter)를 통한 메시지 중복 검사&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;블룸 필터란?&lt;/h3&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;p data-ke-size=&quot;size16&quot;&gt;요소가 존재하지 않는 경우는 확실하게 No라고 판별할 수 있지만, 존재하는 경우에는 오판(False Positive)의 가능성이 있다.&lt;/p&gt;
&lt;p data-end=&quot;337&quot; data-start=&quot;276&quot; data-ke-size=&quot;size16&quot;&gt;즉, &amp;ldquo;존재하지 않는다&amp;rdquo;는 사실은 정확히 알 수 있지만, &amp;ldquo;존재한다&amp;rdquo;는 판단은 일정 확률로 틀릴 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;337&quot; data-start=&quot;276&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;489&quot; data-start=&quot;339&quot; data-ke-size=&quot;size16&quot;&gt;블룸 필터는 여러 개의 해시 함수를 사용해 데이터를 비트 배열(bit array)에 매핑한다.&lt;/p&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;블룸 필터의 동작 원리&lt;/b&gt;:&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;180&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/enAw1j/dJMcad1KLY3/I5uDPWnr68OryT9tO6N701/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/enAw1j/dJMcad1KLY3/I5uDPWnr68OryT9tO6N701/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/enAw1j/dJMcad1KLY3/I5uDPWnr68OryT9tO6N701/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FenAw1j%2FdJMcad1KLY3%2FI5uDPWnr68OryT9tO6N701%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;500&quot; height=&quot;180&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;180&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;549&quot; data-start=&quot;509&quot;&gt;메시지 키를 k개의 해시 함수로 변환 &amp;rarr; k개의 비트 위치 지정&lt;/li&gt;
&lt;li data-end=&quot;567&quot; data-start=&quot;550&quot;&gt;해당 위치를 1로 세팅&lt;/li&gt;
&lt;li data-end=&quot;612&quot; data-start=&quot;568&quot;&gt;검사 시 모든 위치가 1이면 존재 가능, 하나라도 0이면 존재하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;그럼 왜 블룸 필터처럼 오판 가능성이 있는 근사 자료구조를 쓰는가?&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사실 &lt;b&gt;정확하게 관리&lt;/b&gt;할려면 Hash Table(map, set) 쓰면 된다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;평균적으로 시간 복잡도 O(1)이 나오지만 최악의 경우 O(N)이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;참고로 Set도 내부적으로 map과 동일하게 해시 테이블을 사용하되 Value값에 더미를 넣는 식으로 구현하는 편이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기에 GC 부담이나 락 관리까지 신경쓰면 된다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;나도 concurrent map이나 set쪽 쓰는게 더 코드 직관적이긴 하지만&lt;span&gt;&amp;nbsp;&lt;/span&gt;벤치마크의 락 관리로 오버헤드 생기는건 너무 &lt;b&gt;본말전도&lt;/b&gt;다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그에 반면, 블룸 필터는 O(k)의 비트 연산으로 아주 적은 메모리로 처리할 수 있다. (사실 해시 연산에 대해서 O(value length)만큼 더 해야하지만 넘어가자)&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;자료구조&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;메모리 사용량&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;비고&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Bloom filter&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;m (bits, -n&amp;nbsp;*&amp;nbsp;ln(fp)&amp;nbsp;/&amp;nbsp;(ln2)^2)&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;n=100,000, fp=0.001 &amp;rarr; m &amp;asymp; 180 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Hash Table(map/set)&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;약 24byte + key size/entry (offset + partition, 16byte)&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;100,000(n) * 40~50byte &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;rarr; m &amp;asymp;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;4~5 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;메모리 사용량만 놓고 봐도 &lt;b&gt;약 23~28배 정도&lt;/b&gt;나 차이난다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;오판만 고려한다면 블룸 필터쪽이 락 관리나 GC 부담도 필요 없고, 비용적으로 수십 만 msg/s의 TPS에 적합하다고 볼 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;분산 시스템에서의 활용&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;특히 '존재하지 않는지 판단'하는 비용이 상대적으로 큰 분산 시스템에서 그 효용을 발휘한다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Cassandra는 데이터가 존재하는지 확인하는 Disk IO를 최대한 줄이기 위해서 내부적으로 블룸 필터를 사용하고 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이처럼 블룸 필터는 분산 시스템에서 &lt;b&gt;대규모 메시지 처리, 높은 TPS 환경&lt;/b&gt;에서 메모리 효율과 성능을 동시에 잡는 단골 손님이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;나 역시도 메시지 발행시 설정한 고유값 messageID를 블룸 필터에 넣는 식으로 적은 비용과 성능 유지라는 두 마리 토끼를 잡고자 했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;cursus는 설계 과정부터 Kafka를 보고 공부하면서 진행했기 때문에, 메시지마다 고유값(producerID-seqNum-epoch) 역시 어느정도 이를 따른다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;간단하게 필요한 부분만 가져왔다. 블룸 필터의 해시 함수로 murmur쪽이 Go 표준에 없어서 안썼다.&lt;/p&gt;
&lt;pre id=&quot;code_1767424864392&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package bench

import (
	&quot;encoding/binary&quot;
	&quot;hash/fnv&quot;
	&quot;math&quot;
	&quot;sync/atomic&quot;
)

type BloomFilter struct {
	bits []uint64
	m    uint64
	k    uint64
}

func NewBloomFilter(expected uint64, fpRate float64) *BloomFilter {
	m := uint64(-1 * float64(expected) * math.Log(fpRate) / (math.Ln2 * math.Ln2))
	k := uint64(float64(m) / float64(expected) * math.Ln2)
	size := (m + 63) / 64

	return &amp;amp;BloomFilter{
		bits: make([]uint64, size),
		m:    m,
		k:    k,
	}
}

func hashf(data []byte) (uint64, uint64) {
	h1 := fnv.New64a()
	h1.Write(data)
	sum1 := h1.Sum64()

	var buf [8]byte
	binary.BigEndian.PutUint64(buf[:], sum1)
	h2 := fnv.New64()
	h2.Write(buf[:])
	sum2 := h2.Sum64()

	return sum1, sum2
}

func (bf *BloomFilter) Add(data []byte) bool {
	h1, h2 := hashf(data)

	var seen = true
	for i := uint64(0); i &amp;lt; bf.k; i++ {
		idx := (h1 + i*h2) % bf.m
		word := idx / 64
		bit := uint64(1) &amp;lt;&amp;lt; (idx % 64)

		old := atomic.LoadUint64(&amp;amp;bf.bits[word])
		if old&amp;amp;bit == 0 {
			seen = false
			atomic.OrUint64(&amp;amp;bf.bits[word], bit)
		}
	}

	return seen
}&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;벤치마크는 메시지 소비시마다 Add 메서드를 통해서 넣고, 이미 블룸 필터에 존재하는 경우(seen)는 True를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 이미 k개의 해시 함수에 대해서 bits offset을 전부 차지하고 있다면 중복 메시지로 카운트한다.&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;오판 비율(fpRate)를 0.001로 설정해서 공식에 의해 비트수(m)와 해시 함수 개수(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;아, 참고로 메시지의 크기를 1KB로 설정한 벤치마크 테스트이다.&lt;/p&gt;
&lt;pre id=&quot;code_1767425062795&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Total Messages       : 100000
Elapsed Time         : 0.35s
Overall TPS          : 289589.07 msg/s
Duplicate Detected    : 32 (Bloom filter, fp possible)
Message Loss          : 0

  Phase Total TPS        : 282253.00
  p95 Partition Avg TPS : 327776.59
  p99 Partition Avg TPS : 327776.59
    #10 total=8333    avgTPS=302138.2
    #0  total=8334    avgTPS=305570.5
    #9  total=8333    avgTPS=295242.9
    #7  total=8333    avgTPS=297891.1
    #2  total=8334    avgTPS=321543.7
    #1  total=8334    avgTPS=332686.7
    #5  total=8333    avgTPS=313307.9
    #8  total=8333    avgTPS=310533.2
    #3  total=8334    avgTPS=327776.6
    #4  total=8333    avgTPS=279992.8
    #11 total=8333    avgTPS=297874.3
    #6  total=8333    avgTPS=304979.1&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;전체 발행한 100,000 메시지 중에서 최대 ~100건 수준의 오판(false positive)까지 허용 범위 이내이니, 32개의 중복이 나온 것 같지만 오차 범위 이내로 실제 중복은 아니었다.&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;False Negative는 없기 때문에 존재하지 않는 경우(데이터 유실)에 대해서 결과를 보장한다.&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;p data-ke-size=&quot;size16&quot;&gt;출처 및 인용.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Bloom_filter&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://en.wikipedia.org/wiki/Bloom_filter&lt;/a&gt;&lt;/p&gt;</description>
      <category>tech</category>
      <author>downfa11</author>
      <guid isPermaLink="true">https://downfa11.tistory.com/111</guid>
      <comments>https://downfa11.tistory.com/111#entry111comment</comments>
      <pubDate>Sat, 3 Jan 2026 16:57:09 +0900</pubDate>
    </item>
    <item>
      <title>Cursus: Tabellarius CDC(Change Data Capture) 설계</title>
      <link>https://downfa11.tistory.com/110</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 브로커로 시작한 오픈소스 프로젝트 cursus는 작업해온 양이 방대해서 문서화하는데도 오래 걸리고 있다.&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;플랫폼화해서 생태계를 이루는 것을 다음 목표로 생각하고 있던 터라, cursus connector에 해당하는 CDC 사이드 프로젝트를 함께 시작했다.&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;a href=&quot;https://github.com/downfa11-org/tabellarius&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/downfa11-org/tabellarius&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1767073217993&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - downfa11-org/tabellarius: Change data capture source&quot; data-og-description=&quot;Change data capture source. Contribute to downfa11-org/tabellarius development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/downfa11-org/tabellarius&quot; data-og-url=&quot;https://github.com/downfa11-org/tabellarius&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bDPb91/hyZQDD82Gw/uhoHPgZBksXZlVwuBoXaX1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/Qs4sG/hyZQ9Bte3Q/KIS04HXtVWtKRZruvwr830/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/downfa11-org/tabellarius&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/downfa11-org/tabellarius&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bDPb91/hyZQDD82Gw/uhoHPgZBksXZlVwuBoXaX1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/Qs4sG/hyZQ9Bte3Q/KIS04HXtVWtKRZruvwr830/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&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;GitHub - downfa11-org/tabellarius: Change data capture source&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Change data capture source. Contribute to downfa11-org/tabellarius development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.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;이번 글은 CDC 프로젝트의 시작과 함께 코어 시스템 설계에 대한 논의와 고민해온 과정을 소개하고자 한다.&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;CDC(Change Data Capture&lt;span&gt;)&lt;/span&gt;란 무엇인가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CDC(Change Data Capture)는 &lt;b&gt;DB에 발생한 변경 사항을 감지하고 이를 이벤트로 외부에 전달&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;누가, 언제, 어떤 테이블의, 어떤 row를, 어떻게 변경했는지 감지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에서는 가장 활발하게 사용되면서 자료도 풍부한 CDC, Debezium을 참고하되, 초기 단계에서는 최대한 가볍게 구현하고자 했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;634&quot; data-origin-height=&quot;245&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DSkEt/dJMcab3UnWK/FdFltKz5nr8Rok0Qm5laeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DSkEt/dJMcab3UnWK/FdFltKz5nr8Rok0Qm5laeK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DSkEt/dJMcab3UnWK/FdFltKz5nr8Rok0Qm5laeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDSkEt%2FdJMcab3UnWK%2FFdFltKz5nr8Rok0Qm5laeK%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;385&quot; height=&quot;149&quot; data-origin-width=&quot;634&quot; data-origin-height=&quot;245&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그럼 CDC 기술은 어디서 활용되는가?&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;300&quot; data-end=&quot;397&quot; data-ke-size=&quot;size16&quot;&gt;데이터의 변경 정보는 DB 안에는 분명히 존재하지만,&lt;span&gt;&amp;nbsp;&lt;/span&gt;애플리케이션 바깥에서는 쉽게 접근할 수 없다. 그리고 대부분의 시스템은 이 &amp;ldquo;변경 사실&amp;rdquo;을 다른 시스템과 공유해야 한다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;399&quot; data-end=&quot;466&quot;&gt;
&lt;li data-start=&quot;399&quot; data-end=&quot;411&quot;&gt;검색 인덱스 동기화&lt;/li&gt;
&lt;li data-start=&quot;412&quot; data-end=&quot;419&quot;&gt;캐시 갱신&lt;/li&gt;
&lt;li data-start=&quot;420&quot; data-end=&quot;427&quot;&gt;통계 집계&lt;/li&gt;
&lt;li data-start=&quot;428&quot; data-end=&quot;447&quot;&gt;이벤트 기반 마이크로서비스 연동&lt;/li&gt;
&lt;li data-start=&quot;448&quot; data-end=&quot;466&quot;&gt;감사 로그(Audit Log)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;1066&quot; data-start=&quot;1034&quot; data-ke-size=&quot;size23&quot;&gt;트랜잭션 아웃박스(Transaction Outbox) 패턴&lt;/h3&gt;
&lt;p data-end=&quot;1081&quot; data-start=&quot;1068&quot; 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 data-end=&quot;1081&quot; data-start=&quot;1068&quot;&gt;(Kafka 장애) DB 트랜잭션은 성공적으로 처리했지만, 그 결과 이벤트를 발행하는 과정에서 장애가 발생한 경우&lt;/li&gt;
&lt;li data-end=&quot;1081&quot; data-start=&quot;1068&quot;&gt;(DB 장애) 이벤트 발행에는 성공했지만 DB의 트랜잭션은 실패해서 롤백된 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1081&quot; data-start=&quot;1068&quot; data-ke-size=&quot;size16&quot;&gt;다른 마이크로 서비스들에게 전파되지 않거나, 롤백된 실제값과 다른 내용을 전파받아서 전체 시스템이 불일치한다.&lt;/p&gt;
&lt;p data-end=&quot;1210&quot; data-start=&quot;1166&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1210&quot; data-start=&quot;1166&quot; data-ke-size=&quot;size16&quot;&gt;이 문제의 본질은 &lt;b&gt;DB 작업과 이벤트 발행이 원자적으로 이뤄지지 않기&lt;/b&gt; 때문이다.&lt;/p&gt;
&lt;p data-end=&quot;1210&quot; data-start=&quot;1166&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1210&quot; data-start=&quot;1166&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1210&quot; data-start=&quot;1166&quot; data-ke-size=&quot;size16&quot;&gt;이걸 Spring에서 제공하는 TransactionalEventListener같은 걸로 처리하면 일부면 몰라도 본질적인 해결이 되진 않는다.&lt;/p&gt;
&lt;pre id=&quot;code_1767076553416&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void createOrder() {
    orderRepository.save(order);
    applicationEventPublisher.publishEvent(new OrderCreatedEvent(order));
}

@TransactionalEventListener(phase = AFTER_COMMIT)
public void handle(OrderCreatedEvent event) {
    kafkaTemplate.send(...);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;1210&quot; data-start=&quot;1166&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1210&quot; data-start=&quot;1166&quot; data-ke-size=&quot;size16&quot;&gt;phase = AFTER_COMMIT가 전송의 성공을 보장하는게 아니다.&lt;/p&gt;
&lt;p data-end=&quot;1210&quot; data-start=&quot;1166&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1210&quot; data-start=&quot;1166&quot; data-ke-size=&quot;size16&quot;&gt;메시지 발행의 성공 여부는 이미 스프링 트랜잭션의 제어 범위 밖이다.&lt;/p&gt;
&lt;p data-end=&quot;1210&quot; data-start=&quot;1166&quot; data-ke-size=&quot;size16&quot;&gt;이미 여기 DB Commit은 완료했지만 메시지 발행에 실패한 시점에서 스프링은 아무것도 해주지 않는다.&lt;/p&gt;
&lt;p data-end=&quot;1210&quot; data-start=&quot;1166&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-end=&quot;1327&quot; data-start=&quot;1324&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;1350&quot; data-start=&quot;1329&quot; data-ke-size=&quot;size23&quot;&gt;Outbox 패턴의 핵심 아이디어&lt;/h3&gt;
&lt;p data-end=&quot;1366&quot; data-start=&quot;1352&quot; data-ke-size=&quot;size16&quot;&gt;이 불일치 문제에 대한 해결책은 의외로 단순하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1475&quot; data-start=&quot;1368&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1404&quot; data-start=&quot;1368&quot;&gt;비즈니스 데이터와 이벤트를 같은 DB 트랜잭션으로 저장&lt;/li&gt;
&lt;li data-end=&quot;1441&quot; data-start=&quot;1405&quot;&gt;이벤트는 직접 발행하지 않고 outbox 테이블에 기록&lt;/li&gt;
&lt;li data-end=&quot;1475&quot; data-start=&quot;1442&quot;&gt;별도의 프로세스가 outbox 테이블을 읽어 외부로 발행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1581&quot; data-start=&quot;1574&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 되면 DB 트랜잭션이 성공하면 비즈니스 데이터와 이벤트 기록이 항상 함께 존재하고, 롤백된 경우에는 이벤트도 존재하지 않는다.&lt;/p&gt;
&lt;p data-end=&quot;1699&quot; data-start=&quot;1672&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1699&quot; data-start=&quot;1672&quot; data-ke-size=&quot;size16&quot;&gt;여기서 이 outbox 테이블을 안정적으로 감지해줄 CDC가 필요해진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1699&quot; data-start=&quot;1672&quot;&gt;CDC가 outbox 테이블의 INSERT만 감지하면서, 이벤트를 외부 메시지 브로커로 전달해준다.&lt;/li&gt;
&lt;li data-end=&quot;1699&quot; data-start=&quot;1672&quot;&gt;이때 발행의 성공 여부에 따라서 상태를 관리하면 재처리 등의 후대응이 가능해진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1699&quot; data-start=&quot;1672&quot; data-ke-size=&quot;size16&quot;&gt;애플리케이션이 직접 polling 하는 경우는 성능상의 문제나 중복 처리, 장애 복구에 대한 복잡성만 올라간다.&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;설계1. Trigger를 통한 Polling 방식의 변경 감지&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 사실 이걸로 초기 MVP를 이미 작성했다. 너무 직관적이고 쉽기 때문에 누구나 생각해볼법 했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기본 골자는 감지하고자 하는 DB와 테이블 정보를 미리 config.yaml로 수집하고 CDC 시작시 초기화 과정을 거친다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;config.yaml&lt;/p&gt;
&lt;pre id=&quot;code_1767074004038&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;database:
  schema: mydb
  user: root
  password: root
  host: mysql
  port: 3306

cdc_log:
  table: cdc_log

tables:
  - name: orders
    pk: id
  - name: users
    pk: id

cdc_server:
  offset_file: offset.txt
  publisher_addr: localhost:9092&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;실제 DB에 접속이 이뤄지는가(ping)?&lt;/li&gt;
&lt;li&gt;감지할 테이블이 존재하는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;감지할 테이블이 존재한다면 재시작한 경우를 감안해서 트리거가 미리 존재하는지 확인한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;트리거의 생성같은 경우를 자동화하는건 &lt;b&gt;DB 운영하는 입장에서 굉장히 불쾌한 경험&lt;/b&gt;이다. 나도 모르는 사이에 운영 DB의 깊숙하게 성능 저하나 장애의 원인이 될지도 모르는 소모가 발생하는 것이기 때문이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 탐지만 초기화 과정에서 자동으로 처리하고, 실제 트리거 생성은 CLI를 통해서 사용자가 명시적으로 실행하도록 처리했다.&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;MySQL Trigger를 사용해 INSERT / UPDATE / DELETE 발생시 별도의 테이블이나 큐에 변경 내용을 기록한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;CREATE TRIGGER after_insert_user
AFTER INSERTONuser
FOREACH ROW
INSERT INTO user_cdc_log (...)
VALUES (...);&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;br /&gt;이를 통해서 감지할 테이블에 새로 변경사항이 생기면 Trigger를 통해서 cdc-table에 기록되게 된다. 우리는 Polling 방식으로 이 cdc-table만 매번 조회하면 간단하게 CDC를 구현할 수 있다.&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;detect&lt;/p&gt;
&lt;pre id=&quot;code_1767073848198&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func (c *CDCLogInspector) Detect(from uint64, limit int) ([]model.TriggerEvent, error) {
	rows, err := c.db.Query(`
		SELECT seq, table_name, op, row_id, payload
		FROM cdc_log
		WHERE seq &amp;gt; ?
		ORDER BY seq ASC
		LIMIT ?
	`, from, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var events []model.TriggerEvent

	for rows.Next() {
		var (
			seq   uint64
			table string
			op    string
			key   []byte
			value []byte
		)

		if err := rows.Scan(&amp;amp;seq, &amp;amp;table, &amp;amp;op, &amp;amp;key, &amp;amp;value); err != nil {
			return nil, err
		}

		events = append(events, model.TriggerEvent{
			Seq:   seq,
			Table: table,
			Op:    op,
			Key:   key,
			Value: value,
		})
	}

	return events, nil
}&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;polling&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1767073471858&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func (e *Engine) Run(ctx context.Context, out chan&amp;lt;- model.TriggerEvent) error {
	for {
		select {
		case &amp;lt;-ctx.Done():
			return nil
		default:
			events, err := e.inspector.Detect(e.from, e.batchSize)
			if err != nil {
				return err
			}
			if len(events) == 0 {
				time.Sleep(200 * time.Millisecond)
				continue
			}
			log.Printf(&quot;fetched %d events (from=%d)&quot;, len(events), e.from)
			for _, evt := range events {
				out &amp;lt;- evt
				e.from = evt.Seq
			}
			if e.offsetFile != &quot;&quot; {
				offset.Save(e.offsetFile, int64(e.from))
			}
		}
	}
}&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;비어 있는 경우, 단순하게 200ms만큼 대기하도록 했지만 좀 더 구체화하면 지수 백오프로 long-polling 하도록 할 생각까지 미리 염두했었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;하지만, 문제는 생각보다 많았다&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 트랜잭션 경계 문제&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Trigger는 트랜잭션 내부에서 실행되기 때문에, origin 트랜잭션이 영향을 받게 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CDC 로직이 느리면?&lt;/li&gt;
&lt;li&gt;로그 테이블 insert가 병목이면?&lt;/li&gt;
&lt;li&gt;외부 시스템과 연동하면?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 서비스의 로직은 정상적으로 작동했지만, CDC의 로직이 느리거나 cdc-table에 insert하는 과정에서 병목이 생긴다고 상상해보자.&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;운영자 입장에서는 비즈니스 트랜잭션이 CDC 때문에 느려지고 있다.&lt;/p&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;p data-ke-size=&quot;size16&quot;&gt;이건 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;정말 최악이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. DB schema와의 강한 결합&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테이블 추가 &amp;rarr; Trigger 추가&lt;/li&gt;
&lt;li&gt;컬럼 변경 &amp;rarr; Trigger 수정&lt;/li&gt;
&lt;li&gt;모든 환경(dev/stage/prod)에 반영&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 그 불쾌한 경험으로 설명했지만, 외부 시스템(CDC)에서 &lt;b&gt;DB schema와 강하게 결합&lt;/b&gt;된다.&lt;/p&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;p data-ke-size=&quot;size16&quot;&gt;그렇다고 공짜인가? &lt;b&gt;DB 부하가 더 심해진다. &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 DB는 소중하고 귀중하게 다뤄야하는데 외부 시스템에 의해서 자꾸 쓰기/조회 비용이 추가적으로 든다.&lt;/p&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;h4 data-ke-size=&quot;size20&quot;&gt;3. 어차피 모든 변경을 모두 수집하지도 못한다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CDC에서 가장 중요한 건 &lt;b&gt;모든 변경 이벤트를 감지&lt;/b&gt;인데, Trigger 방식은 이를 보장하기 어렵다.&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;서비스적으로 soft delete를 구현한 경우는 감지할 수 있겠지만 아예 삭제(hard)해버리면 트리거로도 감지할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통의 운영 서비스는 바로 삭제해버리지 않고 soft delete 방식을 이용하겠지만.. 그렇다고 해서 외부 시스템이 soft delete 방식을 강제하는 구조는 절대적으로 잘못된 설계라고 생각했다.&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;/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;거기다가 Trigger가 실수로 테이블을 빠트릴 수도 있고, 장애가 발생하는 경우는 어떻게 처리할 것인가? 재처리는?&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설계2. Binlog 구독 방식의 변경 감지 (like Debezium)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RDBMS Binlog란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDBMS의 binary log는 데이터의 변경 사항을 저장하며 복제, 복구 목적으로 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적으로 MySQL replication 구성시 binlog가 사용된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;binlog, transaction log, redo log 등..&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커밋되지 않은 트랜잭션은 binlog에 쌓이지 않는다. 변경사항이 없기 때문이다!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;binlog format&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;statement&lt;/b&gt;: 쿼리문을 평문 기록. now() 그대로 저장해서 데이터 일관성 유지X&lt;/li&gt;
&lt;li&gt;&lt;b&gt;row&lt;/b&gt;(default): 변경 사항이 발생한 row를 base64 인코딩해서 기록&lt;/li&gt;
&lt;li&gt;&lt;b&gt;mixed&lt;/b&gt;: 필요에 따라서 row, statement를 혼합해서 사용&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;binlog commands&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;binlog에 관한 설정 변경은 SET GLOBAL 변수를 변경하거나, cnf 파일을 수정하고 재시작하는 방법이 존재한다.&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;실제 binlog 내용 조회:&lt;/p&gt;
&lt;pre id=&quot;code_1767078245493&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SHOW BINLOG_EVENTS IN '&amp;lt;binlog-file-name&amp;gt;';&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;하지만 쿼리로 바이너리 로그를 조회하면 로그의 시간이 조회되지 않는다. mysqlbinlog 유틸리티를 다운받아서 지정된 경로(/var/lib/mysql)에 기록된 raw를 직접 디코딩해야한다.&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;h3 data-ke-size=&quot;size23&quot;&gt;외부 CDC가 binlog를 읽어오는 방식&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Binlog는 복제(replication)를 위해 설계된 Binlog를 외부의 CDC가&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;일단 이 방식을 채택할려면 RDBMS Binlog format에 대한 이해나 RowEvent 처리에 대한 구현상 어려움이 발목을 잡는다.&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;/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;at-least-once 확보&lt;/b&gt;: DDL을 포함한 누락이 전혀 없으며, 트랜잭션 단위의 이벤트들도 모두 묶어서 한꺼번에 기록된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;장애시 특정 시점에서 재처리 관리&lt;/b&gt;: binlog pos 기반으로 offset을 관리할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DB 의존성 분리&lt;/b&gt;: 이미 쓰고 있는 binlog를 읽기만 해서 origin 트랜잭션에 아무 영향을 주지 않는다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;당연히 DB Schema 변경에도 아무 영향을 받지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Trigger 기반이 갖는 단점들이 너무 치명적인 반면, binlog 구독 방식이 시원하게 해결해주는 모습에 사실 이 쯤에서 도입이 결정되었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Tabellarius에서의 설계 방향&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 안정성과 확장성을 고려하면 문제는 이걸 어떻게 DB에 부담 없이 운영하면서도, 안정적으로, 빠뜨리지 않고 가져오는지 여부를 중점적으로 비교해야 했다.&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;CDC는 편의 기능따위가 아니라 데이터 신뢰성에 기반한 인프라이기 때문에 선택지는 binlog로 좁혀질 수 밖에 없었다.&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;Binlog 방식을 채택하면서 다음 원칙을 세웠다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CDC는 DB 외부 프로세스로, Source(DB)와 Sink(Cursus) 분리&lt;/li&gt;
&lt;li&gt;Offset 기반 재처리 기능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여담으로, 감지할 DB는 stand-alone으로 제한했다.&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;운영 DB가 복제본(replication)을 갖는 구조라면 binlog의 형태나 권한에 대한 설정 소요가 발생할 수 있고, 초기 프로젝트에서 고려할 사항은 아니라고 판단했다.&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;Debezium과 유사하지만 더 단순한 구조의 독립 CDC Source를 목표로 했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 서비스는 DB를 직접 바라보지 않는다.&lt;/li&gt;
&lt;li&gt;변경은 이벤트로 전파된다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>tech</category>
      <category>binlog</category>
      <category>CDC</category>
      <category>Change Data Capture</category>
      <category>cursus</category>
      <category>debezium</category>
      <category>message broker</category>
      <category>mysql</category>
      <category>OpenSource</category>
      <category>RDBMS</category>
      <category>tabellarius</category>
      <author>downfa11</author>
      <guid isPermaLink="true">https://downfa11.tistory.com/110</guid>
      <comments>https://downfa11.tistory.com/110#entry110comment</comments>
      <pubDate>Tue, 30 Dec 2025 16:12:39 +0900</pubDate>
    </item>
    <item>
      <title>오픈소스 입문자를 위한 Kubernetes 지역화: 누락된 문서 탐지 스크립트 구현</title>
      <link>https://downfa11.tistory.com/109</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;쿠버네티스 문서 생태계의 지역화(localization)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;한번쯤은 모두들 들어가봤을 공식 문서다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a href=&quot;https://kubernetes.io/ko/&quot;&gt;https://kubernetes.io/ko/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2527&quot; data-origin-height=&quot;1287&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAR6Hb/dJMcagD7cwR/70kBUHIwOCutYiypcJ19hK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAR6Hb/dJMcagD7cwR/70kBUHIwOCutYiypcJ19hK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAR6Hb/dJMcagD7cwR/70kBUHIwOCutYiypcJ19hK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAR6Hb%2FdJMcagD7cwR%2F70kBUHIwOCutYiypcJ19hK%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;2527&quot; height=&quot;1287&quot; data-origin-width=&quot;2527&quot; data-origin-height=&quot;1287&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;645&quot; data-start=&quot;585&quot; data-ke-size=&quot;size23&quot;&gt;kubernetes-sigs/docs&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;쿠버가 오픈소스인만큼 문서도 사용자들의 자유로운 참여로 추가, 수정되고 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;585&quot; data-end=&quot;645&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;585&quot; data-end=&quot;645&quot; data-ke-size=&quot;size16&quot;&gt;이 문서의 저장소인 kubernetes-sigs/docs는 kubernetes.io에 배포되는 모든 문서나 번역본을 모두 포함하고 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;585&quot; data-end=&quot;645&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;645&quot; data-start=&quot;585&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;지역화(localization)는&lt;span&gt; &lt;/span&gt;&lt;/span&gt;영문 문서를 진실의 원천(Source of Truth)로 두고 관리하는 SIG-DOCS팀의 산하 서브 프로젝트이다.&lt;/p&gt;
&lt;p data-end=&quot;645&quot; data-start=&quot;585&quot; data-ke-size=&quot;size16&quot;&gt;이 공식 문서 생태계를 지역별로 번역&amp;middot;관리하는 활동으로, 영문 문서를&amp;nbsp; 두고 다국어 번역, 동기화하는 downstream 작업이다.&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;오픈소스 입문으로 굉장히 매력적이라 생각한다. 이거만한게 없다.&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;1. 번역 작업을 통해서 &lt;b&gt;공식 문서를 학습할 기회&lt;/b&gt;도 마련되고 CNCF Graduated인 만큼 &lt;b&gt;성숙한 생태계를 경험&lt;/b&gt;할 수 있다.&lt;/p&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;p data-ke-size=&quot;size16&quot;&gt;2. &lt;b&gt;한국어&lt;/b&gt;로 리뷰하거나 Slack에서 소통할 수 있기 때문에 편리하다.&lt;/p&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;p data-ke-size=&quot;size16&quot;&gt;3. 특히 입문자들에게 막연히 어렵고 힘든, 미지의 영역으로 보이는 &lt;b&gt;오픈소스 기여라는 영역을 접근하기 쉽게 해준다.&lt;/b&gt;&lt;/p&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;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;h3 data-ke-size=&quot;size23&quot;&gt;1. 시작하기에 앞서, CLA 인증 설정하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스 기여를 시작하기 전에 반드시 CNCF CLA(Contributor License Agreement) 서명이 필요하다.&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;서명하고 인증해야하는데 안하고 PR 올리면 댓글로 봇이 잔뜩 화내는데..&lt;/p&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;p data-ke-size=&quot;size16&quot;&gt;해버리자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/kubernetes/community/blob/master/CLA.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/kubernetes/community/blob/master/CLA.md&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1766409884494&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;community/CLA.md at master &amp;middot; kubernetes/community&quot; data-og-description=&quot;Kubernetes community content. Contribute to kubernetes/community development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/kubernetes/community/blob/master/CLA.md&quot; data-og-url=&quot;https://github.com/kubernetes/community/blob/master/CLA.md&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/co2DG0/hyZP9o5LCo/xwIKmyzy6kaP1EgPeksfrk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/MwzXl/hyZP24yztz/Fj8s2XiQIUexAO2R4KxaF1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/kubernetes/community/blob/master/CLA.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/kubernetes/community/blob/master/CLA.md&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/co2DG0/hyZP9o5LCo/xwIKmyzy6kaP1EgPeksfrk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/MwzXl/hyZP24yztz/Fj8s2XiQIUexAO2R4KxaF1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&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;community/CLA.md at master &amp;middot; kubernetes/community&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Kubernetes community content. Contribute to kubernetes/community development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.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;쿠버는 아니지만, DCO 설정같은 것도 다른 오픈소스에서 요구하는 경우가 많으니 미리 해두면 좋다.&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;2. 작업할 문서를 찾자 (+ 쉘스크립트 구현)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 영문(en)과 한글(ko) 문서를 비교하며 작업거리를 찾는다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;content/en/*, content/ko/*&lt;/li&gt;
&lt;/ul&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;/p&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;find-works.sh&lt;/p&gt;
&lt;pre id=&quot;code_1766057513141&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;find content/en -name &quot;*.md&quot; | sed 's|content/en/||' | sort &amp;gt; /tmp/en.txt  
find content/ko -name &quot;*.md&quot; | sed 's|content/ko/||' | sort &amp;gt; /tmp/ko.txt  

echo &quot;최소 요구 사항(상위 50개):&quot;  
comm -23 /tmp/en.txt /tmp/ko.txt | grep -E &quot;^(docs/home/|docs/setup/|docs/tutorials/|releases/|docs/concepts/)&quot; | head -50  

echo &quot;전체 문서 (상위 10개):&quot; &amp;amp;&amp;amp; comm -23 /tmp/en.txt /tmp/ko.txt | head -10  
rm /tmp/en.txt /tmp/ko.txt&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;결과적으로 아래와 같은 형식으로 작업할 리스트를 확인할 수 있다!&lt;/p&gt;
&lt;pre id=&quot;code_1766057539571&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;./find-works.sh

docs/concepts/cluster-administration/compatibility-version.md
docs/concepts/cluster-administration/coordinated-leader-election.md
docs/concepts/cluster-administration/kube-state-metrics.md
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 이슈 할당&lt;/h3&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;p data-ke-size=&quot;size16&quot;&gt;Assign을 통해 일감을 누가 맡을지 지정해야 각기다른 기여자들간 충돌이 발생하지 않는다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1903&quot; data-origin-height=&quot;855&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dLQ9EG/dJMcaaqlILp/ETsc8fsGTWtlyTdMZRBo7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dLQ9EG/dJMcaaqlILp/ETsc8fsGTWtlyTdMZRBo7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dLQ9EG/dJMcaaqlILp/ETsc8fsGTWtlyTdMZRBo7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdLQ9EG%2FdJMcaaqlILp%2FETsc8fsGTWtlyTdMZRBo7K%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;1903&quot; height=&quot;855&quot; data-origin-width=&quot;1903&quot; data-origin-height=&quot;855&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;라벨링을 한국어로 설정해서 검색하면 찾기 편하다!&lt;/p&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;p data-ke-size=&quot;size16&quot;&gt;이슈가 존재하지 않는다면 직접 작성해서 공식적으로 해당 문서를 할당받아야 한다. &lt;s&gt;이건 이제 내꺼니까 아무도 건들지마라&lt;/s&gt;&lt;s&gt;&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1405&quot; data-origin-height=&quot;1067&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2J09R/dJMcahXkgjc/WxqgfClTzzGu1Bd91zaKp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2J09R/dJMcahXkgjc/WxqgfClTzzGu1Bd91zaKp0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2J09R/dJMcahXkgjc/WxqgfClTzzGu1Bd91zaKp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2J09R%2FdJMcahXkgjc%2FWxqgfClTzzGu1Bd91zaKp0%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;1405&quot; height=&quot;1067&quot; data-origin-width=&quot;1405&quot; data-origin-height=&quot;1067&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;k8s-ci-robot 이 친구 앞으로 자주 보게 될 친구인데 주로 테스트나 라벨링하는데 쓰인다.&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;이슈는 템플릿이 잘 나와있어서 그대로 따라 적으면 되지만, 영문으로 적어야해서 다른 PR이나 이슈를 참고해서 적으면 된다.&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;3. 영문 문서의 번역&lt;/h3&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;p style=&quot;color: #333333; text-align: start;&quot; 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;쿠버네티스 한글화 가이드:&amp;nbsp;&lt;a href=&quot;https://kubernetes.io/ko/docs/contribute/localization_ko/&quot;&gt;https://kubernetes.io/ko/docs/contribute/localization_ko/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;쿠버네티스 한글화의 모범 사례:&amp;nbsp;&lt;a href=&quot;https://kubernetes.io/ko/docs/contribute/localization_ko_best_practice/&quot;&gt;https://kubernetes.io/ko/docs/contribute/localization_ko_best_practice/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. PR 제출전에 미리 고려해야할 검사 항목&lt;/h3&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;p data-ke-size=&quot;size16&quot;&gt;3-1. i18n 문자열 검사&lt;/p&gt;
&lt;pre id=&quot;code_1766057749262&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cat i18n/en/en.toml
cat i18n/ko/ko.toml 

comm -23 &amp;lt;(grep &quot;^\[&quot; i18n/en/en.toml | sort) \  
         &amp;lt;(grep &quot;^\[&quot; i18n/ko/ko.toml | sort)&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;3-2. 문서 내의 하이퍼링크 검사&lt;/p&gt;
&lt;pre id=&quot;code_1766057740341&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;scripts/linkchecker.py -f content/ko/docs/tutorials/configuration/pod-sidecar-containers.md&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;3-3. PR에서 Netlify로 확인하기 or 로컬에서 테스트하기&lt;/p&gt;
&lt;pre id=&quot;code_1766057763110&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;make container-serve # http://localhost:1313&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;2540&quot; data-origin-height=&quot;1301&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lP7xA/dJMcah328mQ/trUlL3k1KQKldvB6n9Hz61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lP7xA/dJMcah328mQ/trUlL3k1KQKldvB6n9Hz61/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lP7xA/dJMcah328mQ/trUlL3k1KQKldvB6n9Hz61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlP7xA%2FdJMcah328mQ%2FtrUlL3k1KQKldvB6n9Hz61%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;2540&quot; height=&quot;1301&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;2540&quot; data-origin-height=&quot;1301&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;다음과 같이 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>tech</category>
      <category>CNCF</category>
      <category>kubernetes</category>
      <category>localization</category>
      <category>OpenSource</category>
      <category>오픈소스 기여</category>
      <category>지역화</category>
      <category>쿠버네티스</category>
      <author>downfa11</author>
      <guid isPermaLink="true">https://downfa11.tistory.com/109</guid>
      <comments>https://downfa11.tistory.com/109#entry109comment</comments>
      <pubDate>Mon, 22 Dec 2025 22:30:37 +0900</pubDate>
    </item>
  </channel>
</rss>