<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>차니</title>
    <link>https://chaaany.tistory.com/</link>
    <description>DevOps 엔지니어이자 준희 아빠인 차니가 기술, 육아, 일상에서 배우고 만든 것들을 기록합니다.</description>
    <language>ko</language>
    <pubDate>Sat, 8 Aug 2026 12:21:52 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Chann._.y</managingEditor>
    <image>
      <title>차니</title>
      <url>https://tistory1.daumcdn.net/tistory/5203857/attach/10991fcc151846c99e9733be22e1cc54</url>
      <link>https://chaaany.tistory.com</link>
    </image>
    <item>
      <title>Linux 네트워크 성능 분석 순서: TCP 큐&amp;middot;오프로드&amp;middot;qdisc 확인하기</title>
      <link>https://chaaany.tistory.com/500</link>
      <description>&lt;p&gt;Linux 네트워크가 느릴 때는 용어를 많이 아는 것보다 &lt;strong&gt;데이터가 어느 큐에서 기다리고 어느 계층에서 다시 보내지는지&lt;/strong&gt;를 순서대로 확인하는 편이 유용하다. 애플리케이션 → 소켓 → TCP → qdisc → NIC → 네트워크 경로로 내려가며 같은 시각의 지표를 모으면 “네트워크가 느리다”는 증상을 구체적인 후보로 바꿀 수 있다.&lt;/p&gt;
&lt;p&gt;브렌던 그렉의 시스템 성능 관점으로 네트워크 용어를 정리하다 보니 ICMP, CIDR, 쿠키, backlog와 오프로드가 한 글에 섞였다. 다시 보니 이 개념들은 같은 진단 단계에 있지 않았다. 여기서는 Linux 호스트의 지연·손실·연결 실패를 좁히는 흐름에 필요한 내용만 남긴다.&lt;/p&gt;
&lt;h2&gt;증상을 먼저 네 종류로 나눈다&lt;/h2&gt;
&lt;p&gt;측정 전에 사용자가 겪는 실패를 구분한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;연결 자체가 느리거나 실패한다&lt;/strong&gt;: SYN queue, accept queue, 방화벽, DNS와 경로를 본다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;연결 뒤 응답이 느리다&lt;/strong&gt;: 애플리케이션 off-CPU, socket buffer, RTT와 재전송을 본다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;처리량만 낮다&lt;/strong&gt;: cwnd·rwnd, 손실, RTT, NIC와 qdisc를 본다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;부하가 있을 때만 지연이 튄다&lt;/strong&gt;: queueing delay와 bufferbloat를 의심한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;평균 응답 시간 하나만 보면 이 네 문제가 섞인다. 요청 시작 시각, 연결 시간, 첫 바이트와 전체 전송 시간을 나눠 기록한다.&lt;/p&gt;
&lt;h2&gt;전체 데이터 경로를 한 번에 그린다&lt;/h2&gt;
&lt;p&gt;송신 경로를 단순화하면 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;애플리케이션 write()
→ TCP 송신 버퍼
→ 혼잡·수신 윈도우
→ qdisc
→ NIC 드라이버와 하드웨어 큐
→ 링크와 라우터
→ 상대 호스트
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;수신 쪽은 반대 방향으로 NIC에서 커널 socket receive buffer를 거쳐 애플리케이션의 &lt;code&gt;read()&lt;/code&gt;로 올라온다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;계층&lt;/th&gt;
&lt;th&gt;대표 질문&lt;/th&gt;
&lt;th&gt;먼저 볼 도구&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;애플리케이션&lt;/td&gt;
&lt;td&gt;CPU를 쓰는가, I/O·락을 기다리는가&lt;/td&gt;
&lt;td&gt;profiler, tracing, off-CPU 분석&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;listen socket&lt;/td&gt;
&lt;td&gt;연결을 accept하지 못하고 쌓였는가&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ss -lntp&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;established TCP&lt;/td&gt;
&lt;td&gt;RTT·재전송·윈도우가 어떻게 변하는가&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ss -tin&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qdisc&lt;/td&gt;
&lt;td&gt;송신 큐에 backlog·drop이 생기는가&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tc -s qdisc&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NIC&lt;/td&gt;
&lt;td&gt;error·drop·offload 상태가 어떤가&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ip -s link&lt;/code&gt;, &lt;code&gt;ethtool&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;경로&lt;/td&gt;
&lt;td&gt;손실·MTU·홉 변화가 있는가&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ping&lt;/code&gt;, &lt;code&gt;tracepath&lt;/code&gt;, packet capture&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;한 도구의 한 시점 출력만으로 원인을 확정하지 않는다. 같은 부하 구간에서 애플리케이션 지연과 호스트·네트워크 지표의 변화가 맞물리는지 본다.&lt;/p&gt;
&lt;h2&gt;연결 실패는 listen queue부터 본다&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ss -lntp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;LISTEN socket에서 queue가 설정된 한계에 계속 가까워지면 애플리케이션이 완성된 연결을 충분히 빨리 &lt;code&gt;accept()&lt;/code&gt;하지 못하는지 확인한다. &lt;code&gt;listen()&lt;/code&gt;의 backlog는 Linux에서 주로 완성된 연결의 대기열에 영향을 받고 &lt;code&gt;net.core.somaxconn&lt;/code&gt;의 상한을 받는다. SYN이 아직 완성되지 않은 연결은 별도의 정책과 &lt;code&gt;tcp_max_syn_backlog&lt;/code&gt; 영향을 받는다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sysctl net.core.somaxconn
sysctl net.ipv4.tcp_max_syn_backlog
sysctl net.ipv4.tcp_syncookies
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;queue가 찬다고 설정값만 늘리면 대기 공간이 커질 뿐 처리율 병목은 남을 수 있다. 다음을 함께 확인한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;accept loop와 worker가 CPU·락·GC에 막히는가&lt;/li&gt;
&lt;li&gt;프로세스 file descriptor 제한에 닿았는가&lt;/li&gt;
&lt;li&gt;upstream load balancer의 재시도와 연결 폭주가 있는가&lt;/li&gt;
&lt;li&gt;SYN flood 또는 비정상적인 연결 패턴인가&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SYN cookie는 SYN queue 고갈을 완화하는 방어 수단이지 완성된 연결 폭주나 느린 애플리케이션을 해결하는 기능은 아니다.&lt;/p&gt;
&lt;h2&gt;연결된 TCP는 ss -tin으로 읽는다&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ss -tin dst 203.0.113.10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ss -i&lt;/code&gt;는 Linux TCP 내부 정보에서 다음 값을 보여 줄 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;rtt&lt;/code&gt;와 &lt;code&gt;rttvar&lt;/code&gt;: 왕복 시간과 변동&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rto&lt;/code&gt;: 재전송 timeout&lt;/li&gt;
&lt;li&gt;&lt;code&gt;retrans&lt;/code&gt;: 재전송 관련 상태&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cwnd&lt;/code&gt;: 혼잡 윈도우&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ssthresh&lt;/code&gt;: slow start threshold&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wscale&lt;/code&gt;: 송·수신 윈도우 scale&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pmtu&lt;/code&gt;: 추정한 path MTU&lt;/li&gt;
&lt;li&gt;&lt;code&gt;send&lt;/code&gt;: 추정 송신률&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;송신자가 새로 보낼 수 있는 양은 수신자가 광고한 윈도우와 혼잡 제어가 허용하는 윈도우의 제약을 모두 받는다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;실제 전송 가능량 ≈ min(rwnd, cwnd)에서 이미 미확인인 데이터 제외
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;cwnd&lt;/code&gt;가 작다고 혼잡 제어만 탓하지 않는다. 애플리케이션이 데이터를 충분히 쓰지 않는지, 수신 윈도우가 닫히는지, RTT·손실과 재전송이 같이 변하는지 봐야 한다. TCP 혼잡 제어의 단계는 &lt;a href=&quot;/424&quot;&gt;cwnd·slow start·fast recovery&lt;/a&gt;에서 더 자세히 정리했다.&lt;/p&gt;
&lt;h2&gt;qdisc에서 대기와 drop을 확인한다&lt;/h2&gt;
&lt;p&gt;Linux 송신 경로의 queueing discipline(qdisc)은 패킷을 언제 NIC로 보낼지 결정한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;tc -s qdisc show dev eth0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;부하 전후의 &lt;code&gt;backlog&lt;/code&gt;, &lt;code&gt;dropped&lt;/code&gt;, &lt;code&gt;overlimits&lt;/code&gt;, &lt;code&gt;requeues&lt;/code&gt;를 비교한다. 출력 필드와 의미는 qdisc 종류에 따라 다르므로 숫자 하나를 모든 환경에서 같은 뜻으로 해석하지 않는다.&lt;/p&gt;
&lt;p&gt;부하가 없을 때 RTT가 낮은데 업로드나 다운로드 중 RTT가 크게 오르면 bufferbloat 후보가 된다. 큰 unmanaged queue는 손실을 늦추는 대신 패킷을 오래 기다리게 할 수 있다.&lt;/p&gt;
&lt;p&gt;FQ-CoDel은 flow queueing과 CoDel active queue management를 결합한다. CoDel은 queue 길이만이 아니라 패킷이 머문 시간을 이용해 지속적인 지연을 감지한다. Linux의 BQL(Byte Queue Limits)은 더 아래의 device driver ring에 과도한 바이트가 쌓이지 않도록 돕는다.&lt;/p&gt;
&lt;p&gt;그렇다고 모든 서버에 &lt;code&gt;fq_codel&lt;/code&gt;을 즉시 설정하면 되는 것은 아니다. 실제 병목 링크가 호스트 바깥에 있거나, 클라우드 가상 NIC·traffic shaping 구조가 다르면 효과와 설정 지점이 달라진다. 먼저 어느 인터페이스와 계층에서 queue가 생기는지 측정한다.&lt;/p&gt;
&lt;h2&gt;NIC error와 drop을 분리한다&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ip -s link show dev eth0
ethtool -S eth0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;확인할 항목은 드라이버와 장치마다 이름이 다르지만 다음 범주로 나눌 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;RX/TX packet과 byte 증가량&lt;/li&gt;
&lt;li&gt;error, dropped, missed, timeout&lt;/li&gt;
&lt;li&gt;ring buffer 또는 queue별 drop&lt;/li&gt;
&lt;li&gt;link speed·duplex와 carrier 변화&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;counter는 누적값이다. 값이 0이 아닌 사실보다 문제 구간에서 증가하는 속도가 중요하다. 컨테이너·bond·bridge·가상 NIC를 쓰면 애플리케이션이 보는 인터페이스와 실제 물리 NIC를 모두 따라가야 한다.&lt;/p&gt;
&lt;h2&gt;오프로드 때문에 packet capture가 다르게 보일 수 있다&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ethtool -k eth0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;TSO(TCP Segmentation Offload)는 큰 TCP payload를 NIC가 실제 송신 직전에 여러 frame으로 나누도록 맡긴다. GSO는 소프트웨어에서 비슷한 분할을 늦추고, GRO는 수신한 여러 패킷을 커널에서 큰 단위로 합친다.&lt;/p&gt;
&lt;p&gt;이 때문에 호스트에서 캡처한 &lt;code&gt;tcpdump&lt;/code&gt;에 MTU보다 큰 TCP segment처럼 보이는 데이터가 있어도 wire에 그 크기 그대로 나갔다고 단정할 수 없다. 캡처 지점이 분할 전이거나 병합 후일 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;tcpdump -ni eth0 host 203.0.113.10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;오프로드를 끈 비교는 진단에 도움이 될 수 있지만 처리량과 CPU 사용에 영향을 준다. 운영 인터페이스에서 무심코 변경하지 말고 검증 환경이나 짧고 통제된 창에서 적용한다.&lt;/p&gt;
&lt;h2&gt;MTU와 경로 문제는 ICMP까지 본다&lt;/h2&gt;
&lt;p&gt;일반 Ethernet MTU 1500에서 IPv4·옵션 없는 TCP header를 빼면 흔히 MSS 1460을 떠올린다. 하지만 IPv6, TCP option, tunnel과 VPN encapsulation이 있으면 실제 값이 달라진다. 고정된 1460만 기준으로 삼지 않는다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;tracepath example.com
ping -c 10 example.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Path MTU Discovery는 ICMP 메시지에 의존할 수 있다. ICMP를 일괄 차단하면 큰 패킷에서만 통신이 멈추는 black hole 문제가 생길 수 있다. &lt;code&gt;ping&lt;/code&gt; 성공이 TCP 서비스의 정상 동작을 보장하지도 않는다. 실제 서비스의 주소·포트와 packet size를 기준으로 본다.&lt;/p&gt;
&lt;p&gt;traceroute 결과도 경로의 힌트다. ECMP, 정책 라우팅, ICMP rate limit과 비대칭 경로 때문에 요청마다 또는 방향마다 다를 수 있다. IP 경로 선택 자체는 &lt;a href=&quot;/426&quot;&gt;라우팅과 포워딩 차이&lt;/a&gt;에서 이어서 볼 수 있다.&lt;/p&gt;
&lt;h2&gt;on-CPU와 off-CPU를 네트워크 지표와 맞춘다&lt;/h2&gt;
&lt;p&gt;요청이 200ms이고 CPU time이 5ms라면 남은 시간을 모두 네트워크라고 부를 수는 없다. socket read, DNS, TLS, connection pool, lock, scheduler run queue와 downstream 응답 대기가 섞일 수 있다.&lt;/p&gt;
&lt;p&gt;trace에서 다음 구간을 나눈다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;DNS
→ TCP connect
→ TLS handshake
→ connection pool 대기
→ 요청 전송
→ first byte 대기
→ body 수신
→ 애플리케이션 처리
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;호스트의 &lt;code&gt;ss&lt;/code&gt;, &lt;code&gt;tc&lt;/code&gt;, NIC counter를 같은 시간축에 놓았을 때 RTT·재전송·queue 증가가 보이지 않으면 애플리케이션이나 downstream 처리부터 다시 본다. 시스템 성능의 관측·실험 순서는 &lt;a href=&quot;/475&quot;&gt;시스템 성능 엔지니어링 1장 노트&lt;/a&gt;와 연결된다.&lt;/p&gt;
&lt;h2&gt;최소 진단 체크리스트&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# listen queue와 프로세스
ss -lntp

# 연결별 RTT, RTO, cwnd, 재전송
ss -tin

# qdisc 상태
tc -s qdisc show dev eth0

# 인터페이스·드라이버 통계
ip -s link show dev eth0
ethtool -S eth0

# offload 상태
ethtool -k eth0

# 경로와 MTU 힌트
tracepath example.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 명령은 원인을 자동으로 알려 주는 체크박스가 아니다. 문제 전후의 변화, 애플리케이션 trace와 요청 특성을 함께 모을 때 의미가 생긴다. 네트워크 성능 분석의 핵심은 모든 용어를 한 번에 외우는 것이 아니라 &lt;strong&gt;기다림이 시작되는 첫 계층을 찾는 것&lt;/strong&gt;이다.&lt;/p&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/networking/segmentation-offloads.html&quot;&gt;Linux 커널 문서: Segmentation Offloads&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://man7.org/linux/man-pages/man8/ss.8.html&quot;&gt;ss(8) Linux manual&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://man7.org/linux/man-pages/man8/tc.8.html&quot;&gt;tc(8) Linux manual&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc8290.html&quot;&gt;RFC 8290: FQ-CoDel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc9293.html&quot;&gt;RFC 9293: Transmission Control Protocol&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>배움과 성장/네트워크&amp;middot;보안</category>
      <category>Linux 네트워크 성능 분석</category>
      <category>Linux네트워크성능</category>
      <category>qdisc</category>
      <category>TCP큐</category>
      <category>네트워크성능분석</category>
      <author>Chann._.y</author>
      <guid isPermaLink="true">https://chaaany.tistory.com/500</guid>
      <comments>https://chaaany.tistory.com/500#entry500comment</comments>
      <pubDate>Wed, 3 Jun 2026 10:02:16 +0900</pubDate>
    </item>
    <item>
      <title>MySQL 복합 인덱스 설계: 컬럼 순서&amp;middot;범위 조건&amp;middot;EXPLAIN ANALYZE</title>
      <link>https://chaaany.tistory.com/499</link>
      <description>&lt;p&gt;MySQL 복합 인덱스는 여러 단일 인덱스를 합친 목록이 아니라 &lt;strong&gt;왼쪽 컬럼부터 정렬된 하나의 키&lt;/strong&gt;다. 따라서 “선택도가 높은 컬럼을 항상 앞에 둔다”거나 “등호·정렬 컬럼을 차례대로 붙인다”는 규칙만으로 설계하면 실제 쿼리와 어긋날 수 있다.&lt;/p&gt;
&lt;p&gt;아래 예제는 MySQL 8.4와 InnoDB의 B-tree 인덱스를 기준으로 한다. 먼저 자주 실행되는 쿼리를 고정하고, 후보 인덱스마다 읽는 행과 정렬 비용을 실행 계획으로 비교하는 것이 출발점이다.&lt;/p&gt;
&lt;h2&gt;복합 인덱스는 어떻게 정렬되는가&lt;/h2&gt;
&lt;p&gt;다음 인덱스는 &lt;code&gt;(user_id, status, created_at)&lt;/code&gt; 튜플 순서로 정렬된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE INDEX idx_orders_user_status_created
ON orders (user_id, status, created_at);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;먼저 &lt;code&gt;user_id&lt;/code&gt;로 정렬하고, 같은 사용자 안에서 &lt;code&gt;status&lt;/code&gt;, 두 값이 모두 같을 때 &lt;code&gt;created_at&lt;/code&gt; 순서가 정해진다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;(1, CANCELLED, 2026-04-01)
(1, PAID,      2026-04-01)
(1, PAID,      2026-04-02)
(2, PAID,      2026-04-01)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 때문에 다음 왼쪽 접두사는 검색 경로가 된다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;(user_id)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;(user_id, status)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;(user_id, status, created_at)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;반면 &lt;code&gt;status&lt;/code&gt;만 보면 인덱스 전체가 상태값 순서로 모여 있지 않다. MySQL은 이 인덱스를 lookup에 쓰지 않거나 다른 접근 방식을 택할 수 있다.&lt;/p&gt;
&lt;h2&gt;왼쪽 접두사 규칙을 과하게 단순화하지 않기&lt;/h2&gt;
&lt;p&gt;다음 쿼리는 인덱스의 앞에서부터 조건을 사용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT id, user_id, status, created_at
FROM orders
WHERE user_id = 10
  AND status = &amp;#39;PAID&amp;#39;
  AND created_at &amp;gt;= &amp;#39;2026-04-01&amp;#39;
  AND created_at &amp;lt;  &amp;#39;2026-05-01&amp;#39;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;user_id&lt;/code&gt;와 &lt;code&gt;status&lt;/code&gt;의 동등 조건으로 검색 구간을 좁힌 뒤 &lt;code&gt;created_at&lt;/code&gt; 범위를 읽을 수 있다.&lt;/p&gt;
&lt;p&gt;“범위 조건 뒤 컬럼은 전혀 사용되지 않는다”는 설명도 정확하지 않다. 뒤쪽 컬럼은 인덱스에서 조건을 평가해 테이블 행 접근을 줄이는 데 쓰일 수 있다. 다만 앞의 동등 조건과 첫 범위 조건처럼 &lt;strong&gt;스캔해야 할 인덱스 구간 자체를 줄이지는 못할 수 있다&lt;/strong&gt;. &lt;code&gt;EXPLAIN&lt;/code&gt;에서 key parts와 필터 조건을 구분해 봐야 한다.&lt;/p&gt;
&lt;h2&gt;컬럼 순서는 쿼리 묶음으로 결정한다&lt;/h2&gt;
&lt;p&gt;주문 테이블에서 다음 두 쿼리가 있다고 하자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- A: 특정 사용자의 결제 완료 주문을 최근 순으로
SELECT id, created_at
FROM orders
WHERE user_id = ?
  AND status = &amp;#39;PAID&amp;#39;
ORDER BY created_at DESC
LIMIT 20;

-- B: 전체에서 특정 기간의 결제 완료 주문 집계
SELECT COUNT(*)
FROM orders
WHERE status = &amp;#39;PAID&amp;#39;
  AND created_at &amp;gt;= ?
  AND created_at &amp;lt; ?;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;(user_id, status, created_at)&lt;/code&gt;은 A에 잘 맞을 가능성이 크지만 B의 &lt;code&gt;status&lt;/code&gt;부터 바로 찾는 인덱스는 아니다. B의 빈도와 비용이 크다면 &lt;code&gt;(status, created_at)&lt;/code&gt; 같은 별도 후보를 비교해야 한다.&lt;/p&gt;
&lt;p&gt;여기서 한 인덱스로 모든 쿼리를 해결하려고 하지 않는다. 반대로 쿼리마다 인덱스를 하나씩 만들지도 않는다. 다음 항목을 쿼리 묶음별로 확인한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;동등 조건과 범위 조건&lt;/li&gt;
&lt;li&gt;필요한 정렬과 &lt;code&gt;LIMIT&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;반환하는 열&lt;/li&gt;
&lt;li&gt;호출 빈도와 허용 지연&lt;/li&gt;
&lt;li&gt;기존 인덱스와의 접두사 중복&lt;/li&gt;
&lt;li&gt;삽입·갱신 부하&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;ORDER BY까지 인덱스로 처리하는 조건&lt;/h2&gt;
&lt;p&gt;쿼리 A에서 &lt;code&gt;user_id&lt;/code&gt;와 &lt;code&gt;status&lt;/code&gt;는 상수다. 그 뒤의 &lt;code&gt;created_at&lt;/code&gt;이 인덱스 순서와 맞으므로 MySQL이 인덱스를 역방향으로 읽어 별도 정렬을 피할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;EXPLAIN FORMAT=TREE
SELECT id, created_at
FROM orders
WHERE user_id = 10
  AND status = &amp;#39;PAID&amp;#39;
ORDER BY created_at DESC
LIMIT 20;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그러나 &lt;code&gt;ORDER BY&lt;/code&gt; 컬럼의 순서·방향이 인덱스와 맞지 않거나, 앞쪽 컬럼이 상수로 고정되지 않거나, 테이블 행을 읽는 비용이 더 크면 별도 정렬이 생길 수 있다. “정렬 컬럼을 인덱스 마지막에 붙이면 된다”가 아니라 실제 계획의 &lt;code&gt;Sort&lt;/code&gt;와 읽은 행 수를 확인한다.&lt;/p&gt;
&lt;h2&gt;커버링 인덱스의 이득과 비용&lt;/h2&gt;
&lt;p&gt;쿼리에 필요한 열이 모두 인덱스에 있으면 InnoDB가 클러스터드 인덱스의 전체 행을 다시 읽지 않고 결과를 만들 수 있다. MySQL 문서에서는 이를 covering index라고 설명한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT user_id, status, created_at
FROM orders
WHERE user_id = 10
  AND status = &amp;#39;PAID&amp;#39;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 쿼리는 세 열이 모두 후보 인덱스에 있다. 하지만 커버링을 만들겠다고 큰 문자열이나 자주 바뀌는 열을 계속 붙이면 인덱스가 커진다. 더 많은 페이지를 읽고, 캐시 효율이 떨어지고, 쓰기 비용도 증가할 수 있다.&lt;/p&gt;
&lt;p&gt;커버링은 테이블 접근이 없어진다는 사실만으로 확정하지 않고 &lt;strong&gt;중요한 읽기 경로에서 절약하는 비용이 인덱스 비대화보다 큰지&lt;/strong&gt;로 판단한다.&lt;/p&gt;
&lt;h2&gt;단일 인덱스 여러 개와 무엇이 다른가&lt;/h2&gt;
&lt;p&gt;다음 두 구성은 같은 의미가 아니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 단일 인덱스 두 개
CREATE INDEX idx_orders_user   ON orders (user_id);
CREATE INDEX idx_orders_status ON orders (status);

-- 복합 인덱스 하나
CREATE INDEX idx_orders_user_status ON orders (user_id, status);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MySQL은 별도 인덱스를 Index Merge로 조합하거나 더 선택적인 하나를 고를 수 있다. 복합 인덱스는 두 조건이 결합된 정렬 구조에서 바로 범위를 좁힐 수 있다. 어느 쪽이 빠른지는 데이터 분포, 조건 조합과 반환 열에 달려 있다.&lt;/p&gt;
&lt;p&gt;자주 함께 쓰는 조건이라는 이유만으로 복합 인덱스를 확정하지 말고 두 계획을 대표 데이터에서 비교한다.&lt;/p&gt;
&lt;h2&gt;EXPLAIN ANALYZE로 후보를 비교하는 순서&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;EXPLAIN ANALYZE
SELECT id, created_at
FROM orders
WHERE user_id = 10
  AND status = &amp;#39;PAID&amp;#39;
ORDER BY created_at DESC
LIMIT 20;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;다음 순서로 읽으면 된다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Table scan&lt;/code&gt;, &lt;code&gt;Index lookup&lt;/code&gt;, &lt;code&gt;Index range scan&lt;/code&gt; 중 무엇을 선택했는가&lt;/li&gt;
&lt;li&gt;어떤 인덱스와 키 부분을 사용했는가&lt;/li&gt;
&lt;li&gt;예상 행 수와 실제 행 수가 얼마나 다른가&lt;/li&gt;
&lt;li&gt;각 iterator가 몇 번 반복됐는가&lt;/li&gt;
&lt;li&gt;별도 &lt;code&gt;Sort&lt;/code&gt;와 테이블 row lookup이 있는가&lt;/li&gt;
&lt;li&gt;후보 인덱스가 쓰기 부하와 저장 공간을 얼마나 늘리는가&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;는 문장을 실제로 실행한다. 운영 데이터에서 바로 시험하기보다 읽기 전용 복제본이나 안전한 검증 환경을 사용하고, 대상 문장이 변경을 일으키지 않는지 확인해야 한다.&lt;/p&gt;
&lt;p&gt;복합 인덱스 설계의 결론은 “등호 → 범위 → 정렬”이라는 암기 문장이 아니다. &lt;strong&gt;중요한 쿼리가 적은 페이지와 행을 읽도록 정렬 구조를 만들고, 그 가정을 실행 계획으로 검증하는 일&lt;/strong&gt;이다. B+Tree 탐색 자체는 &lt;a href=&quot;/497&quot;&gt;InnoDB 인덱스와 풀 스캔&lt;/a&gt;, 단일 컬럼의 선택도는 &lt;a href=&quot;/498&quot;&gt;MySQL 단일 인덱스 판단 기준&lt;/a&gt;에서 이어서 볼 수 있다.&lt;/p&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/multiple-column-indexes.html&quot;&gt;MySQL 8.4: Multiple-Column Indexes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/mysql-indexes.html&quot;&gt;MySQL 8.4: How MySQL Uses Indexes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/order-by-optimization.html&quot;&gt;MySQL 8.4: ORDER BY Optimization&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/explain.html&quot;&gt;MySQL 8.4: EXPLAIN Statement&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/index-statistics.html&quot;&gt;MySQL 8.4: Index Statistics Collection&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>배움과 성장/백엔드&amp;middot;데이터</category>
      <category>EXPLAINANALYZE</category>
      <category>MySQL 복합 인덱스</category>
      <category>sql튜닝</category>
      <category>복합인덱스</category>
      <category>인덱스컬럼순서</category>
      <author>Chann._.y</author>
      <guid isPermaLink="true">https://chaaany.tistory.com/499</guid>
      <comments>https://chaaany.tistory.com/499#entry499comment</comments>
      <pubDate>Fri, 1 May 2026 11:30:25 +0900</pubDate>
    </item>
    <item>
      <title>MySQL 단일 인덱스가 유효한 조건: 선택도&amp;middot;범위 검색&amp;middot;정렬</title>
      <link>https://chaaany.tistory.com/498</link>
      <description>&lt;p&gt;MySQL 단일 인덱스는 컬럼 하나를 기준으로 검색 범위를 빠르게 줄일 때 유효하다. &lt;code&gt;WHERE&lt;/code&gt;에 등장한 컬럼이라고 모두 인덱스를 만들 필요는 없다. &lt;strong&gt;조건을 적용한 뒤 남는 행의 수, 실제로 반환할 열, 정렬 방식과 쓰기 비용&lt;/strong&gt;을 함께 봐야 한다.&lt;/p&gt;
&lt;p&gt;아래 설명은 MySQL 8.4와 InnoDB의 일반 B-tree 보조 인덱스를 기준으로 한다.&lt;/p&gt;
&lt;h2&gt;단일 인덱스는 어떤 일을 하는가&lt;/h2&gt;
&lt;p&gt;사용자를 이메일로 자주 찾는다면 다음 인덱스를 생각할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE INDEX idx_users_email ON users (email);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 인덱스는 &lt;code&gt;email&lt;/code&gt; 값을 정렬된 검색 키로 저장한다. InnoDB의 보조 인덱스 엔트리에는 행을 찾기 위한 기본 키도 포함된다. 다음 쿼리는 보조 인덱스에서 이메일을 찾은 뒤 기본 키로 전체 행을 읽을 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT *
FROM users
WHERE email = &amp;#39;chaaany@example.com&amp;#39;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이메일이 업무 규칙상 유일해야 한다면 성능과 별개로 &lt;code&gt;UNIQUE&lt;/code&gt; 제약을 검토해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE UNIQUE INDEX uk_users_email ON users (email);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;인덱스는 데이터 무결성 제약을 대신 결정해 주지 않는다. 중복 허용 여부는 도메인 규칙으로 먼저 정한다.&lt;/p&gt;
&lt;h2&gt;동등 검색과 범위 검색&lt;/h2&gt;
&lt;p&gt;B-tree 인덱스는 &lt;code&gt;=&lt;/code&gt;, &lt;code&gt;IN&lt;/code&gt;, 비교 연산과 &lt;code&gt;BETWEEN&lt;/code&gt; 같은 조건에서 특정 키나 키 범위를 찾는 데 사용할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE INDEX idx_users_created_at ON users (created_at);

SELECT id, created_at
FROM users
WHERE created_at &amp;gt;= &amp;#39;2026-04-01&amp;#39;
  AND created_at &amp;lt;  &amp;#39;2026-05-01&amp;#39;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;날짜 구간은 끝을 포함하는 &lt;code&gt;BETWEEN&lt;/code&gt;보다 반열린 구간으로 적으면 시간 정밀도가 바뀌어도 경계를 다루기 쉽다. 인덱스는 4월 1일의 시작 위치를 찾고 5월 1일 전까지 연속된 엔트리를 읽을 수 있다.&lt;/p&gt;
&lt;p&gt;하지만 한 달치가 테이블의 1%인지 70%인지에 따라 비용이 달라진다. 반환 범위가 넓고 &lt;code&gt;SELECT *&lt;/code&gt;로 모든 열을 읽는다면 보조 인덱스와 클러스터드 인덱스를 오가는 비용이 커질 수 있다.&lt;/p&gt;
&lt;h2&gt;카디널리티보다 선택도를 본다&lt;/h2&gt;
&lt;p&gt;카디널리티는 서로 다른 값의 개수다. 이메일처럼 값 종류가 많은 컬럼은 일반적으로 한 조건이 적은 행을 가리키기 쉽다. 상태값처럼 종류가 적은 컬럼은 그렇지 않을 가능성이 크다.&lt;/p&gt;
&lt;p&gt;다만 “카디널리티가 낮으면 인덱스가 쓸모없다”는 규칙은 정확하지 않다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;ACTIVE   999,000행
DELETED    1,000행
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;값 종류는 두 개뿐이지만 &lt;code&gt;status = &amp;#39;DELETED&amp;#39;&lt;/code&gt;는 전체의 0.1%만 고른다. 이 조건만 자주 조회한다면 인덱스가 후보가 될 수 있다. 반대로 &lt;code&gt;status = &amp;#39;ACTIVE&amp;#39;&lt;/code&gt;가 거의 전부를 반환하면 같은 인덱스의 이득이 작을 수 있다.&lt;/p&gt;
&lt;p&gt;실무에서는 다음 질문이 더 직접적이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;이 조건으로 전체 행의 몇 퍼센트가 남는가&lt;/li&gt;
&lt;li&gt;값별 분포가 한쪽으로 치우쳐 있는가&lt;/li&gt;
&lt;li&gt;결과에서 몇 개의 열을 읽는가&lt;/li&gt;
&lt;li&gt;이 쿼리는 얼마나 자주 실행되는가&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;옵티마이저는 테이블과 인덱스 통계를 바탕으로 행 수를 추정한다. 실제 분포와 추정치가 크게 다르면 통계 갱신과 데이터 편향을 함께 확인한다.&lt;/p&gt;
&lt;h2&gt;ORDER BY는 인덱스만 있으면 사라지는가&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;created_at&lt;/code&gt; 인덱스는 정렬된 순서를 제공하므로 다음 쿼리의 별도 정렬을 피할 가능성이 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT id, created_at
FROM users
ORDER BY created_at DESC
LIMIT 50;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그러나 인덱스가 있다고 항상 &lt;code&gt;filesort&lt;/code&gt;가 사라지는 것은 아니다. 옵티마이저는 인덱스 순서대로 읽은 뒤 테이블 행을 찾는 비용과, 테이블을 읽어 별도로 정렬하는 비용을 비교한다. &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;ORDER BY&lt;/code&gt;, 선택한 열, &lt;code&gt;LIMIT&lt;/code&gt;과 정렬 방향이 함께 계획을 결정한다.&lt;/p&gt;
&lt;p&gt;실행 계획에서 다음을 본다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;EXPLAIN FORMAT=TREE
SELECT id, created_at
FROM users
ORDER BY created_at DESC
LIMIT 50;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;어떤 인덱스를 읽는가&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Sort&lt;/code&gt; 단계가 따로 있는가&lt;/li&gt;
&lt;li&gt;몇 행을 읽은 뒤 50행을 반환할 것으로 보는가&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;인덱스가 있어도 사용하지 않는 이유&lt;/h2&gt;
&lt;p&gt;다음은 오류라기보다 비용 기반 선택일 수 있다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;조건이 전체의 큰 비율을 반환한다.&lt;/li&gt;
&lt;li&gt;테이블이 매우 작다.&lt;/li&gt;
&lt;li&gt;함수나 암시적 형 변환 때문에 인덱스 검색 조건을 만들지 못한다.&lt;/li&gt;
&lt;li&gt;비교하는 문자열 컬럼의 타입·길이·문자 집합이 맞지 않는다.&lt;/li&gt;
&lt;li&gt;통계가 실제 데이터 분포를 충분히 반영하지 못한다.&lt;/li&gt;
&lt;li&gt;다른 인덱스나 풀 스캔이 더 저렴하다고 추정한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;예를 들어 컬럼에 함수를 적용한 다음 조건은 일반 B-tree 인덱스를 그대로 활용하기 어려울 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;WHERE DATE(created_at) = &amp;#39;2026-04-01&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;가능하다면 동일한 의도를 범위 조건으로 표현한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;WHERE created_at &amp;gt;= &amp;#39;2026-04-01&amp;#39;
  AND created_at &amp;lt;  &amp;#39;2026-04-02&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;함수 기반 검색이 핵심이라면 생성 컬럼과 그 인덱스 같은 MySQL 기능을 별도로 검토해야 한다.&lt;/p&gt;
&lt;h2&gt;읽기 이득과 쓰기 비용을 같이 측정한다&lt;/h2&gt;
&lt;p&gt;보조 인덱스가 하나 늘면 &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;와 인덱스 키를 바꾸는 &lt;code&gt;UPDATE&lt;/code&gt;에서도 해당 구조를 유지해야 한다. 디스크 공간과 버퍼 풀도 사용한다. 그러므로 “조회가 빨라질 것 같다”만으로 추가하지 않는다.&lt;/p&gt;
&lt;p&gt;안전한 확인 순서는 다음과 같다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;느린 쿼리와 호출 빈도를 확인한다.&lt;/li&gt;
&lt;li&gt;현재 &lt;code&gt;EXPLAIN&lt;/code&gt;과 &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;를 남긴다.&lt;/li&gt;
&lt;li&gt;대표 데이터 분포에서 후보 인덱스를 비교한다.&lt;/li&gt;
&lt;li&gt;읽은 행 수와 지연뿐 아니라 쓰기 지연·인덱스 크기도 본다.&lt;/li&gt;
&lt;li&gt;운영 적용 뒤 실제 쿼리 지연 분포를 다시 확인한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이 흐름을 거치면 “카디널리티가 높으니 생성” 같은 단일 규칙보다 데이터에 맞는 결정을 할 수 있다. 인덱스 탐색 구조는 &lt;a href=&quot;/497&quot;&gt;InnoDB B+Tree와 풀 스캔&lt;/a&gt;, 여러 조건을 함께 쓰는 경우는 &lt;a href=&quot;/499&quot;&gt;MySQL 복합 인덱스 설계&lt;/a&gt;에서 이어진다.&lt;/p&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/column-indexes.html&quot;&gt;MySQL 8.4: Column Indexes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/mysql-indexes.html&quot;&gt;MySQL 8.4: How MySQL Uses Indexes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/order-by-optimization.html&quot;&gt;MySQL 8.4: ORDER BY Optimization&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/optimization-indexes.html&quot;&gt;MySQL 8.4: Optimization and Indexes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/explain.html&quot;&gt;MySQL 8.4: EXPLAIN Statement&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>배움과 성장/백엔드&amp;middot;데이터</category>
      <category>MySQL 단일 인덱스</category>
      <category>sql튜닝</category>
      <category>단일인덱스</category>
      <category>범위검색</category>
      <category>인덱스선택도</category>
      <author>Chann._.y</author>
      <guid isPermaLink="true">https://chaaany.tistory.com/498</guid>
      <comments>https://chaaany.tistory.com/498#entry498comment</comments>
      <pubDate>Thu, 30 Apr 2026 11:30:28 +0900</pubDate>
    </item>
    <item>
      <title>MySQL 인덱스는 어떻게 찾는가: InnoDB B+Tree와 풀 스캔</title>
      <link>https://chaaany.tistory.com/497</link>
      <description>&lt;p&gt;MySQL 인덱스가 빠른 이유를 한 문장으로 줄이면 &lt;strong&gt;조건에 맞는 행을 찾기 위해 읽어야 할 페이지를 줄이기 때문&lt;/strong&gt;이다. 다만 “인덱스가 있으면 B+Tree를 몇 번 내려가 바로 한 행을 찾는다”는 설명만으로는 부족하다. 실제 비용은 저장 엔진, 인덱스 종류, 조건이 반환하는 행 수, 버퍼 풀 적중 여부와 테이블 행을 다시 읽는 횟수에 따라 달라진다.&lt;/p&gt;
&lt;p&gt;이 글의 예제는 MySQL 8.4의 기본 저장 엔진인 InnoDB와 일반 B-tree 인덱스를 기준으로 한다. 전문 검색·공간 인덱스·MEMORY 엔진의 해시 인덱스는 동작 방식이 다르다.&lt;/p&gt;
&lt;h2&gt;풀 테이블 스캔은 언제 일어나는가&lt;/h2&gt;
&lt;p&gt;다음 테이블에서 &lt;code&gt;email&lt;/code&gt;에 별도 인덱스가 없다고 가정해 보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE users (
    id BIGINT PRIMARY KEY,
    name VARCHAR(50) NOT NULL,
    email VARCHAR(255) NOT NULL,
    created_at DATETIME NOT NULL
) ENGINE = InnoDB;

SELECT *
FROM users
WHERE email = &amp;#39;chaaany@example.com&amp;#39;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;옵티마이저가 사용할 만한 인덱스를 찾지 못하면 테이블을 훑으며 각 행에 조건을 적용할 수 있다. MySQL 실행 계획에서는 보통 &lt;code&gt;Table scan&lt;/code&gt; 또는 전통 형식의 &lt;code&gt;type=ALL&lt;/code&gt;로 확인한다.&lt;/p&gt;
&lt;p&gt;풀 스캔 자체가 언제나 잘못은 아니다. 테이블이 작거나, 조건에 맞는 행이 전체의 대부분이거나, 읽어야 할 열이 많다면 인덱스를 오간 뒤 테이블 행을 다시 찾는 것보다 순차적으로 읽는 편이 저렴할 수 있다. 중요한 질문은 “스캔을 했는가”가 아니라 &lt;strong&gt;예상한 것보다 많은 행과 페이지를 읽었는가&lt;/strong&gt;다.&lt;/p&gt;
&lt;h2&gt;InnoDB에서 테이블과 인덱스의 관계&lt;/h2&gt;
&lt;p&gt;InnoDB 테이블은 기본 키를 기준으로 조직된 클러스터드 인덱스다. 별도의 보조 인덱스는 보조 키 값과 해당 행의 기본 키를 저장한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;보조 인덱스(email)
email 값 → 기본 키 id
              ↓
클러스터드 인덱스(PRIMARY KEY)
id → 전체 행
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;따라서 &lt;code&gt;email&lt;/code&gt; 보조 인덱스로 &lt;code&gt;SELECT *&lt;/code&gt;을 처리하면 대체로 두 단계가 필요하다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;email&lt;/code&gt; 인덱스에서 일치하는 엔트리를 찾는다.&lt;/li&gt;
&lt;li&gt;엔트리에 들어 있는 기본 키로 클러스터드 인덱스의 전체 행을 찾는다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이 두 번째 접근을 흔히 테이블 조회 또는 row lookup이라고 부른다. 반환 행이 매우 많으면 이 반복 접근이 비싸져 옵티마이저가 풀 스캔을 선택할 수 있다.&lt;/p&gt;
&lt;h2&gt;B+Tree라는 설명에서 꼭 남길 것&lt;/h2&gt;
&lt;p&gt;MySQL 문서는 일반적으로 인덱스 구조를 B-tree라고 표현한다. InnoDB의 구현을 이해할 때는 키를 정렬된 페이지에 저장하고, 내부 페이지를 따라 리프 페이지로 내려가며, 리프 페이지가 순서대로 연결된 B+Tree 계열 구조로 생각하면 유용하다.&lt;/p&gt;
&lt;p&gt;핵심은 세 가지다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;한 노드에 키 하나만 두는 이진 트리가 아니라, 한 페이지에 여러 키를 담는다.&lt;/li&gt;
&lt;li&gt;루트와 내부 페이지는 어느 자식 페이지로 갈지 좁힌다.&lt;/li&gt;
&lt;li&gt;정렬된 리프 페이지 덕분에 시작점을 찾은 뒤 범위를 이어서 읽을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;email = ?&lt;/code&gt; 같은 동등 조건은 목표 키가 있을 법한 리프 페이지까지 탐색한다. &lt;code&gt;created_at &amp;gt;= ? AND created_at &amp;lt; ?&lt;/code&gt; 같은 범위 조건은 시작 키를 찾은 뒤 범위가 끝날 때까지 리프 엔트리를 읽는다.&lt;/p&gt;
&lt;p&gt;여기서 “1,000만 행도 루트·브랜치·리프 세 번이면 끝난다”처럼 횟수를 고정하면 안 된다. 트리 높이와 페이지 접근 비용은 키 길이, 페이지 크기, 데이터량, 캐시 상태에 따라 달라진다. 리프를 찾은 뒤 몇 개의 행을 반환하는지도 전체 비용에 큰 영향을 준다.&lt;/p&gt;
&lt;h2&gt;인덱스를 추가한 뒤 무엇이 달라지는가&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;email&lt;/code&gt; 조회가 충분히 자주 발생하고 결과가 적다면 다음 인덱스가 후보가 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE INDEX idx_users_email ON users (email);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;실행 계획은 추측하지 말고 직접 비교한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;EXPLAIN FORMAT=TREE
SELECT *
FROM users
WHERE email = &amp;#39;chaaany@example.com&amp;#39;;

EXPLAIN ANALYZE
SELECT *
FROM users
WHERE email = &amp;#39;chaaany@example.com&amp;#39;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;EXPLAIN&lt;/code&gt;은 옵티마이저가 고른 계획과 추정치를 보여 준다. &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;는 쿼리를 실제로 실행해 반복 횟수, 반환 행 수와 시간을 함께 보여 준다. 변경을 일으키는 문장에 사용할 때는 실제 실행된다는 점을 먼저 확인해야 한다.&lt;/p&gt;
&lt;p&gt;비교할 항목은 다음과 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Table scan&lt;/code&gt;이 &lt;code&gt;Index lookup&lt;/code&gt; 또는 &lt;code&gt;Index range scan&lt;/code&gt;으로 바뀌었는가&lt;/li&gt;
&lt;li&gt;추정 행 수와 실제 행 수의 차이가 큰가&lt;/li&gt;
&lt;li&gt;인덱스에서 찾은 뒤 테이블 행을 몇 번 다시 읽는가&lt;/li&gt;
&lt;li&gt;첫 행까지 걸린 시간과 전체 반복 시간이 어디에서 커지는가&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;실행 계획 하나의 시간이 곧 운영 성능은 아니다. 콜드 캐시와 웜 캐시, 동시 요청, 데이터 분포가 다르면 결과도 달라진다. 같은 데이터와 조건에서 여러 번 재고, 운영 쿼리의 빈도와 지연 분포를 함께 봐야 한다.&lt;/p&gt;
&lt;h2&gt;풀 스캔과 인덱스 스캔을 고르는 판단 기준&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상황&lt;/th&gt;
&lt;th&gt;먼저 의심할 계획&lt;/th&gt;
&lt;th&gt;확인할 것&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;유일한 이메일 한 건 조회&lt;/td&gt;
&lt;td&gt;인덱스 lookup&lt;/td&gt;
&lt;td&gt;실제 반환 행 수, unique 제약 필요 여부&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;한 달치 가입자 범위 조회&lt;/td&gt;
&lt;td&gt;인덱스 range scan&lt;/td&gt;
&lt;td&gt;기간이 전체에서 차지하는 비율&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;전체 사용자의 80% 조회&lt;/td&gt;
&lt;td&gt;table scan 가능&lt;/td&gt;
&lt;td&gt;테이블 재조회 비용과 읽는 열 수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;아주 작은 기준 테이블 조회&lt;/td&gt;
&lt;td&gt;table scan 가능&lt;/td&gt;
&lt;td&gt;인덱스 유지 비용보다 이득이 있는지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;통계와 실제 분포가 크게 다름&lt;/td&gt;
&lt;td&gt;비효율적인 계획 가능&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ANALYZE TABLE&lt;/code&gt;, 추정 행 수&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;이 판단을 “인덱스가 있느냐”만으로 끝내지 않는 것이 중요하다. 인덱스는 가능한 접근 경로를 늘려 주지만, 최종 계획은 옵티마이저가 비용을 비교해 선택한다.&lt;/p&gt;
&lt;p&gt;단일 컬럼 인덱스의 선택도와 정렬 활용은 &lt;a href=&quot;/498&quot;&gt;MySQL 단일 인덱스가 유효한 조건&lt;/a&gt;, 여러 조건의 컬럼 순서는 &lt;a href=&quot;/499&quot;&gt;MySQL 복합 인덱스 설계 기준&lt;/a&gt;에서 이어서 볼 수 있다.&lt;/p&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/mysql-indexes.html&quot;&gt;MySQL 8.4: How MySQL Uses Indexes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/innodb-introduction.html&quot;&gt;MySQL 8.4: InnoDB 소개&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/column-indexes.html&quot;&gt;MySQL 8.4: Column Indexes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/explain.html&quot;&gt;MySQL 8.4: EXPLAIN Statement&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>배움과 성장/백엔드&amp;middot;데이터</category>
      <category>InnoDB</category>
      <category>MySQL B+Tree 인덱스</category>
      <category>MySQLBTree</category>
      <category>mySQL인덱스</category>
      <category>풀테이블스캔</category>
      <author>Chann._.y</author>
      <guid isPermaLink="true">https://chaaany.tistory.com/497</guid>
      <comments>https://chaaany.tistory.com/497#entry497comment</comments>
      <pubDate>Wed, 29 Apr 2026 11:30:04 +0900</pubDate>
    </item>
    <item>
      <title>리눅스 메모리 분석 순서: RSS 증가부터 smaps&amp;middot;할당 경로까지</title>
      <link>https://chaaany.tistory.com/496</link>
      <description>&lt;p&gt;RSS 증가, cgroup OOM, latency spike가 함께 보이면 memory leak부터 의심하기 쉽다. 하지만 RSS 하나만으로 원인을 정할 수는 없다. 먼저 관측 범위와 memory pressure를 확인하고, mapping의 backing을 나눈 뒤, application allocation 경로로 내려가야 한다.&lt;/p&gt;
&lt;p&gt;분석 순서는 다음 다섯 질문으로 정리할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;증상이 실제인가
  → 어떤 memory 종류가 늘었나
  → 어느 mapping·cgroup에서 늘었나
  → 어떤 code 경로가 만들고 유지하나
  → 수정 후 같은 지표가 좋아졌나
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;0단계: host·container·process 범위를 고정한다&lt;/h2&gt;
&lt;p&gt;같은 ‘memory 사용량’이라도 host &lt;code&gt;MemAvailable&lt;/code&gt;, cgroup &lt;code&gt;memory.current&lt;/code&gt;, process RSS는 서로 다른 범위를 측정한다. container service라면 pod·container와 host PID의 대응, cgroup version, memory limit부터 기록한다.&lt;/p&gt;
&lt;p&gt;증상 시간대도 고정한다. deploy, traffic, batch, cache warm-up, OOM event가 언제 발생했는지 하나의 timeline에 놓아야 서로 다른 snapshot을 잘못 비교하지 않는다.&lt;/p&gt;
&lt;h2&gt;1단계: 사용량과 pressure를 분리한다&lt;/h2&gt;
&lt;p&gt;RSS가 높아도 workload가 안정적이고 reclaim pressure가 낮을 수 있다. 반대로 limit에 가까운 cgroup에서는 작은 증가도 OOM으로 이어질 수 있다.&lt;/p&gt;
&lt;p&gt;read-only로 시작할 수 있는 최소 확인은 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pid=1234

grep -E &amp;#39;^(VmRSS|RssAnon|RssFile|RssShmem|VmSwap):&amp;#39; \
  &amp;quot;/proc/$pid/status&amp;quot;

cat /proc/pressure/memory
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;cgroup v2 경로를 알고 있다면 해당 group의 &lt;code&gt;memory.current&lt;/code&gt;, &lt;code&gt;memory.stat&lt;/code&gt;, &lt;code&gt;memory.events&lt;/code&gt;도 같은 시점에 읽는다. cgroup path는 runtime과 배포 방식에 따라 다르므로 추측한 경로에 command를 적용하지 않는다.&lt;/p&gt;
&lt;p&gt;PSI는 task가 memory resource를 기다리며 stall한 시간을 보여 준다. 사용 byte와 pressure가 같은 지표는 아니다. OOM이 있었다면 &lt;code&gt;memory.events&lt;/code&gt;의 &lt;code&gt;oom&lt;/code&gt;, &lt;code&gt;oom_kill&lt;/code&gt;과 kernel log 접근 정책을 확인한다.&lt;/p&gt;
&lt;h2&gt;2단계: smaps로 증가 영역을 나눈다&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;/proc/&amp;lt;pid&amp;gt;/smaps_rollup&lt;/code&gt;은 process 전체의 PSS, anonymous, shared·private clean/dirty 같은 합계를 제공한다. 어떤 mapping이 늘었는지는 개별 &lt;code&gt;smaps&lt;/code&gt;를 시간 간격을 두고 비교해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cat &amp;quot;/proc/$pid/smaps_rollup&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서도 한 항목을 원인과 바로 등치하지 않는다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;anonymous 증가: heap뿐 아니라 stack, anonymous mmap, runtime·JIT 영역일 수 있다.&lt;/li&gt;
&lt;li&gt;file-backed 증가: executable·shared library·mapped file이 될 수 있으며 항상 무해하지 않다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mmap&lt;/code&gt; 증가: mapping 생성 방식이지 anonymous·file backing 자체를 뜻하지 않는다.&lt;/li&gt;
&lt;li&gt;shared memory 증가: tmpfs, IPC, shared mapping의 owner와 accounting을 따로 확인해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;mapping별 path, permission, &lt;code&gt;Anonymous&lt;/code&gt;, &lt;code&gt;Private_Dirty&lt;/code&gt;, &lt;code&gt;Pss&lt;/code&gt;의 변화로 후보를 좁힌다. &lt;code&gt;smaps&lt;/code&gt; 읽기는 process map이 큰 환경에서 비용이 있으므로 짧은 주기로 계속 polling하지 않는다. 세부 필드 해석은 &lt;a href=&quot;https://chaaany.tistory.com/491&quot;&gt;/proc/smaps로 메모리를 분석하는 방법&lt;/a&gt;에 정리되어 있다.&lt;/p&gt;
&lt;h2&gt;3단계: allocation과 lifetime을 찾는다&lt;/h2&gt;
&lt;p&gt;anonymous resident memory가 실제로 application allocation과 함께 증가한다면 heap profiler, runtime profiler, allocator statistic으로 live allocation과 retained object를 찾는다. 중요한 것은 allocation 횟수만이 아니라 &lt;strong&gt;얼마나 오래 살아 있고 누가 참조하는가&lt;/strong&gt;다.&lt;/p&gt;
&lt;p&gt;native process에서 eBPF uprobe로 &lt;code&gt;malloc&lt;/code&gt;·&lt;code&gt;free&lt;/code&gt; symbol을 관측할 수도 있다. 그러나 다음 한계가 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;custom allocator나 static linking에서는 target symbol이 다를 수 있다.&lt;/li&gt;
&lt;li&gt;compiler inlining·wrapper·symbol versioning 때문에 기대한 호출을 놓칠 수 있다.&lt;/li&gt;
&lt;li&gt;호출량이 많으면 probe와 stack collection overhead가 커질 수 있다.&lt;/li&gt;
&lt;li&gt;allocation과 free를 정확히 짝지으려면 pointer, PID/TID, 실패 반환을 신중히 다뤄야 한다.&lt;/li&gt;
&lt;li&gt;stack과 symbol에는 민감한 code·request 정보가 포함될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;production에서는 먼저 같은 binary의 test 환경에서 overhead와 누락률을 확인하고, PID·시간·size threshold·sampling을 제한한다. &lt;a href=&quot;https://chaaany.tistory.com/492&quot;&gt;eBPF로 메모리를 추적하는 방법&lt;/a&gt;은 이 단계의 도구이지 분석의 출발점은 아니다.&lt;/p&gt;
&lt;h2&gt;4단계: 패턴을 원인 후보로 바꾼다&lt;/h2&gt;
&lt;p&gt;관측 패턴과 가능한 설명을 분리해 기록한다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;관측&lt;/th&gt;
&lt;th&gt;원인 후보&lt;/th&gt;
&lt;th&gt;추가 확인&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;live allocation이 계속 증가&lt;/td&gt;
&lt;td&gt;leak, unbounded cache, queue backlog&lt;/td&gt;
&lt;td&gt;owner·reference·eviction policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;live bytes는 감소하지만 RSS 유지&lt;/td&gt;
&lt;td&gt;allocator retention, fragmentation&lt;/td&gt;
&lt;td&gt;allocator stats·재할당 패턴&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;file PSS가 증가&lt;/td&gt;
&lt;td&gt;mapped file working set&lt;/td&gt;
&lt;td&gt;mapping path·access pattern&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;latency와 PSI가 함께 증가&lt;/td&gt;
&lt;td&gt;reclaim·cgroup pressure&lt;/td&gt;
&lt;td&gt;PSI, fault, cgroup event timeline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;allocation 호출 tail만 증가&lt;/td&gt;
&lt;td&gt;lock contention·slow path&lt;/td&gt;
&lt;td&gt;allocator latency·thread·size 분포&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;“free가 안 보인다”만으로 leak을 확정하지 않는다. sampling 누락, process 종료, 다른 allocator API, ownership transfer도 설명 후보다. 반대로 cache라는 이름이 붙었다고 무제한 증가가 정상인 것도 아니다. 용량 상한과 eviction 조건을 확인한다.&lt;/p&gt;
&lt;h2&gt;5단계: 수정 후 같은 조건으로 검증한다&lt;/h2&gt;
&lt;p&gt;수정 전 baseline과 같은 workload·시간 구간에서 다시 측정한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;process RSS·RssAnon과 cgroup &lt;code&gt;memory.current&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;application live bytes 또는 object 수&lt;/li&gt;
&lt;li&gt;PSI·page fault·OOM event&lt;/li&gt;
&lt;li&gt;CPU와 request p50·p95·p99 latency&lt;/li&gt;
&lt;li&gt;기능 정확성과 cache hit rate 같은 guardrail&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;RSS만 낮추려고 cache를 비우거나 trim을 반복하면 latency가 나빠질 수 있다. memory limit을 올리면 OOM 시점만 늦출 수도 있다. 변경은 hypothesis 하나씩 적용하고, 되돌릴 기준을 미리 둔다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;free()&lt;/code&gt; 이후 allocator retention을 판단하는 법은 &lt;a href=&quot;https://chaaany.tistory.com/490&quot;&gt;free와 RSS의 관계&lt;/a&gt;, allocator 호출 자체의 지연은 &lt;a href=&quot;https://chaaany.tistory.com/489&quot;&gt;malloc 빠른 경로와 느린 경로&lt;/a&gt;로 이어진다.&lt;/p&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/filesystems/proc.html&quot;&gt;Linux kernel: The /proc Filesystem&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://man7.org/linux/man-pages/man5/proc_pid_smaps.5.html&quot;&gt;Linux man-pages: proc_pid_smaps(5)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/admin-guide/cgroup-v2.html#memory&quot;&gt;Linux kernel: Control Group v2 — Memory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/accounting/psi.html&quot;&gt;Linux kernel: PSI — Pressure Stall Information&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/trace/uprobetracer.html&quot;&gt;Linux kernel: Uprobe tracer&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>배움과 성장/시스템&amp;middot;성능</category>
      <category>proc smaps</category>
      <category>RSS 증가</category>
      <category>리눅스 메모리 분석</category>
      <category>메모리 누수 분석</category>
      <category>시스템 성능</category>
      <author>Chann._.y</author>
      <guid isPermaLink="true">https://chaaany.tistory.com/496</guid>
      <comments>https://chaaany.tistory.com/496#entry496comment</comments>
      <pubDate>Fri, 10 Apr 2026 22:00:36 +0900</pubDate>
    </item>
    <item>
      <title>eBPF 관측 Hook 선택: tracepoint&amp;middot;kprobe&amp;middot;fentry&amp;middot;uprobe 차이</title>
      <link>https://chaaany.tistory.com/495</link>
      <description>&lt;p&gt;eBPF 프로그램은 혼자 실행되지 않고 커널이나 사용자 공간에서 발생하는 특정 event에 연결된다. 이 연결 지점을 흔히 hook이라고 부른다. 같은 함수 호출을 관찰하더라도 &lt;strong&gt;tracepoint, kprobe, fentry, uprobe는 대상과 안정성, 인자 해석 방식이 다르다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;“커널이면 kprobe, 애플리케이션이면 uprobe”만으로 고르면 배포 뒤 symbol이 사라지거나 context 구조가 달라지는 문제를 놓치기 쉽다. 먼저 관찰하려는 event의 의미와 유지해야 할 kernel·binary 범위를 정한다.&lt;/p&gt;
&lt;h2&gt;Hook을 고르는 기준&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hook&lt;/th&gt;
&lt;th&gt;관찰 대상&lt;/th&gt;
&lt;th&gt;연결 기준&lt;/th&gt;
&lt;th&gt;변경에 민감한 지점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;tracepoint&lt;/td&gt;
&lt;td&gt;커널이 미리 정의한 event&lt;/td&gt;
&lt;td&gt;category와 event name&lt;/td&gt;
&lt;td&gt;event 제공 여부와 context field&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;raw tracepoint&lt;/td&gt;
&lt;td&gt;커널 tracepoint&lt;/td&gt;
&lt;td&gt;raw argument&lt;/td&gt;
&lt;td&gt;typed tracepoint보다 인자 해석 책임이 큼&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kprobe·kretprobe&lt;/td&gt;
&lt;td&gt;커널 instruction·함수 진입/반환&lt;/td&gt;
&lt;td&gt;symbol과 offset&lt;/td&gt;
&lt;td&gt;함수명, inline, prototype, kernel 내부 구현&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fentry·fexit&lt;/td&gt;
&lt;td&gt;BTF가 있는 커널 함수 진입/반환&lt;/td&gt;
&lt;td&gt;BTF function type&lt;/td&gt;
&lt;td&gt;BTF·trampoline 지원, target function 변경&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;uprobe·uretprobe&lt;/td&gt;
&lt;td&gt;사용자 executable·shared library&lt;/td&gt;
&lt;td&gt;binary path, symbol·offset&lt;/td&gt;
&lt;td&gt;build별 symbol·offset, stripped binary, library version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;USDT&lt;/td&gt;
&lt;td&gt;애플리케이션이 정의한 static probe&lt;/td&gt;
&lt;td&gt;provider와 probe name&lt;/td&gt;
&lt;td&gt;애플리케이션의 probe 계약과 배포 여부&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;일반적인 출발점은 &lt;strong&gt;의미가 맞는 tracepoint 또는 USDT가 있는지 먼저 찾고&lt;/strong&gt;, 더 깊은 내부 상태가 필요할 때 fentry·kprobe 또는 uprobe로 내려가는 것이다. 다만 tracepoint라는 이유만으로 영구적인 ABI가 보장되는 것은 아니며, 배포 대상 kernel에서 context를 검증해야 한다.&lt;/p&gt;
&lt;h2&gt;tracepoint: 커널이 이름 붙인 event&lt;/h2&gt;
&lt;p&gt;Linux tracepoint는 커널 소스의 전략적인 위치에 정의된 static probe point다. process scheduling이나 system call event처럼 관찰 의미가 이미 정해져 있다.&lt;/p&gt;
&lt;p&gt;배포 중인 host에서 실제 event를 확인한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;find /sys/kernel/tracing/events -mindepth 2 -maxdepth 2 -type d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;특정 event의 field 형식은 &lt;code&gt;format&lt;/code&gt; 파일에서 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cat /sys/kernel/tracing/events/sched/sched_switch/format
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;libbpf ELF section은 다음 형식을 사용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;SEC(&amp;quot;tracepoint/sched/sched_switch&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;tracepoint는 내부 kernel 함수 이름보다 event 의미가 분명하고 context가 노출돼 있어 관측 도구의 첫 후보로 좋다. 반면 필요한 위치에 event가 없거나 원하는 내부 변수가 context에 포함되지 않을 수 있다.&lt;/p&gt;
&lt;p&gt;raw tracepoint는 같은 event에 더 낮은 수준으로 연결하며 raw argument를 직접 해석한다. typed context의 편의보다 낮은 수준의 접근이 꼭 필요한지 먼저 판단한다.&lt;/p&gt;
&lt;h2&gt;kprobe와 kretprobe: 커널 구현에 동적으로 붙는다&lt;/h2&gt;
&lt;p&gt;kprobe는 대부분의 kernel instruction 주소에 동적으로 probe를 설치할 수 있고, kretprobe는 함수 반환 시점을 관찰한다. libbpf에서는 다음과 같은 section name을 쓴다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;SEC(&amp;quot;kprobe/tcp_sendmsg&amp;quot;)
SEC(&amp;quot;kretprobe/tcp_sendmsg&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;자유도가 높은 대신 target이 kernel 내부 구현이라는 점이 가장 큰 비용이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;함수 이름이 kernel version이나 configuration에 따라 달라질 수 있다.&lt;/li&gt;
&lt;li&gt;compiler가 함수를 inline하면 기대한 모든 호출을 잡지 못할 수 있다.&lt;/li&gt;
&lt;li&gt;함수 prototype이 바뀌면 register에서 인자를 읽는 코드가 틀릴 수 있다.&lt;/li&gt;
&lt;li&gt;kprobe blacklist와 architecture별 제약으로 연결할 수 없는 위치가 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;따라서 “등록이 성공했다”만 확인하지 말고 예상 event rate와 field 값이 합리적인지 검증한다. 여러 kernel version을 지원한다면 각 build의 symbol과 prototype을 CI 또는 별도 호환성 검사에 포함한다.&lt;/p&gt;
&lt;h2&gt;fentry와 fexit: BTF type을 이용한 함수 관찰&lt;/h2&gt;
&lt;p&gt;fentry·fexit는 &lt;code&gt;BPF_PROG_TYPE_TRACING&lt;/code&gt;과 BPF trampoline을 사용해 함수 진입·반환에 연결한다. BTF function type을 이용하므로 kprobe에서 architecture별 register를 직접 해석하는 방식보다 typed argument를 다루기 쉽다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;SEC(&amp;quot;fentry/tcp_sendmsg&amp;quot;)
SEC(&amp;quot;fexit/tcp_sendmsg&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;먼저 target host에 kernel BTF가 있는지 확인한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;test -r /sys/kernel/btf/vmlinux &amp;amp;&amp;amp; echo &amp;quot;kernel BTF available&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;fentry가 항상 kprobe의 완전한 대체는 아니다. kernel과 loader가 필요한 BPF attach type·BTF를 지원해야 하고, target function이 BTF에 노출되어야 한다. 함수 자체가 내부 구현인 이상 version 변경으로 사라질 수도 있다. 지원 조건이 맞으면 함수 entry·exit 관찰의 우선 후보로 검토하는 편이 좋다.&lt;/p&gt;
&lt;h2&gt;uprobe와 uretprobe: 사용자 binary 내부를 본다&lt;/h2&gt;
&lt;p&gt;uprobe는 executable이나 shared library의 instruction에 연결되고 uretprobe는 함수 반환을 관찰한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;SEC(&amp;quot;uprobe//usr/lib/libexample.so:parse_request&amp;quot;)
SEC(&amp;quot;uretprobe//usr/lib/libexample.so:parse_request&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;실제 자동 연결 API와 section 문법은 loader version에 맞춰 확인한다. 운영에서 더 중요한 것은 어느 binary를 관찰하는지 식별하는 일이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;container image와 host의 binary path가 다를 수 있다.&lt;/li&gt;
&lt;li&gt;같은 library name이어도 build ID와 symbol offset이 다를 수 있다.&lt;/li&gt;
&lt;li&gt;stripped binary에는 기대한 symbol이 없을 수 있다.&lt;/li&gt;
&lt;li&gt;JIT runtime은 코드 주소가 동적으로 바뀌어 별도 runtime 지원이 필요할 수 있다.&lt;/li&gt;
&lt;li&gt;process 전체가 아니라 특정 PID·cgroup만 볼지 범위를 정해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;source code를 고칠 수 있고 장기적인 관측 계약이 필요하다면 애플리케이션이 이름과 인자를 정의하는 USDT probe가 더 나은 선택일 수 있다. uprobe는 제공되지 않은 내부 지점을 관찰하는 강력한 fallback이지만 binary 구현과 함께 versioning해야 한다.&lt;/p&gt;
&lt;h2&gt;선택 순서&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;관찰하려는 질문을 event와 필요한 field로 적는다.&lt;/li&gt;
&lt;li&gt;target host의 tracepoint·USDT에 그 의미가 이미 있는지 찾는다.&lt;/li&gt;
&lt;li&gt;커널 함수 내부가 필요하고 BTF 조건이 맞으면 fentry·fexit를 검토한다.&lt;/li&gt;
&lt;li&gt;BTF attach가 불가능하거나 instruction offset이 필요하면 kprobe·kretprobe를 검토한다.&lt;/li&gt;
&lt;li&gt;사용자 binary 내부는 USDT를 먼저 찾고, 없으면 uprobe·uretprobe를 사용한다.&lt;/li&gt;
&lt;li&gt;연결 성공, event 누락, field 해석, probe overhead를 target kernel·binary 조합별로 검증한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;운영에 붙이기 전 확인할 것&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;kernel version, configuration, architecture와 BTF 존재 여부&lt;/li&gt;
&lt;li&gt;symbol·tracepoint·USDT provider가 실제 host에 존재하는지&lt;/li&gt;
&lt;li&gt;고빈도 event에서 sampling이나 aggregation이 필요한지&lt;/li&gt;
&lt;li&gt;BPF map 크기와 ring buffer가 밀릴 때 drop을 관측하는지&lt;/li&gt;
&lt;li&gt;PID, cgroup, namespace filter가 데이터 범위를 제대로 제한하는지&lt;/li&gt;
&lt;li&gt;command line, path, payload에 개인정보·secret이 섞이지 않는지&lt;/li&gt;
&lt;li&gt;detach와 program cleanup이 실패했을 때 남은 link를 어떻게 찾는지&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;system call 경계를 먼저 좁히는 방법은 &lt;a href=&quot;/336&quot;&gt;Linux 시스템 콜과 strace 추적&lt;/a&gt;, 네트워크 성능 event를 계층별로 나누는 방법은 &lt;a href=&quot;/500&quot;&gt;Linux 네트워크 성능 분석&lt;/a&gt;에서 이어서 볼 수 있다.&lt;/p&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/core-api/tracepoint.html&quot;&gt;Linux kernel: Tracepoint API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/trace/kprobes.html&quot;&gt;Linux kernel: Kprobes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/trace/uprobetracer.html&quot;&gt;Linux kernel: Uprobe-based Event Tracing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/bpf/libbpf/program_types.html&quot;&gt;Linux kernel: libbpf Program Types and ELF Sections&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/bpf/btf.html&quot;&gt;Linux kernel: BPF Type Format&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>배움과 성장/시스템&amp;middot;성능</category>
      <category>ebpf</category>
      <category>eBPF Hook 선택</category>
      <category>fentry</category>
      <category>kprobe</category>
      <category>tracepoint</category>
      <author>Chann._.y</author>
      <guid isPermaLink="true">https://chaaany.tistory.com/495</guid>
      <comments>https://chaaany.tistory.com/495#entry495comment</comments>
      <pubDate>Thu, 9 Apr 2026 22:00:01 +0900</pubDate>
    </item>
    <item>
      <title>eBPF 동작 원리: bytecode&amp;middot;verifier&amp;middot;map&amp;middot;helper&amp;middot;JIT 흐름</title>
      <link>https://chaaany.tistory.com/494</link>
      <description>&lt;p&gt;eBPF C source를 작성했다고 해서 그 C code가 그대로 kernel에서 실행되는 것은 아니다. compiler가 BPF instruction으로 변환하고, loader가 필요한 relocation을 처리해 kernel에 전달한다. kernel verifier의 검사를 통과한 program만 hook에 attach할 수 있으며, 실행은 interpreter 또는 architecture별 JIT compiler가 만든 native code로 이뤄질 수 있다.&lt;/p&gt;
&lt;p&gt;전체 흐름을 먼저 연결하면 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;restricted C source
      ↓ clang/LLVM
BPF instructions in ELF (+ BTF)
      ↓ libbpf: open · relocate · load
kernel verifier
      ↓ accept
attach to hook
      ↓ event / packet / syscall path
interpreter or optional JIT execution
      ↕
BPF maps · helpers · kfuncs
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;BPF instruction set과 register&lt;/h2&gt;
&lt;p&gt;eBPF는 register 기반 instruction set을 사용한다. ABI에는 &lt;code&gt;R0&lt;/code&gt;부터 &lt;code&gt;R10&lt;/code&gt;까지 11개의 64-bit register가 정의되어 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;R0&lt;/code&gt;: function return value&lt;/li&gt;
&lt;li&gt;&lt;code&gt;R1&lt;/code&gt;~`R5`: function call arguments&lt;/li&gt;
&lt;li&gt;&lt;code&gt;R6&lt;/code&gt;~`R9`: callee가 보존하는 register&lt;/li&gt;
&lt;li&gt;&lt;code&gt;R10&lt;/code&gt;: read-only frame pointer&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;program type에 따라 entry context의 형태가 다르고, helper를 호출하면 argument register 규칙을 따라야 한다. “R1은 언제나 PID”처럼 고정된 의미를 붙이면 안 된다.&lt;/p&gt;
&lt;p&gt;stack과 instruction은 제한된 실행 환경 안에서 사용된다. arbitrary kernel function을 일반 C program처럼 자유롭게 호출하는 모델이 아니다.&lt;/p&gt;
&lt;h2&gt;libbpf가 load 전후를 연결한다&lt;/h2&gt;
&lt;p&gt;clang은 BPF target의 object file을 만들 수 있고, ELF section에는 program과 map definition, BTF type 정보가 담길 수 있다. libbpf는 object를 열고, CO-RE relocation 같은 조정을 수행한 뒤 BPF system call interface를 통해 map과 program을 load한다.&lt;/p&gt;
&lt;p&gt;load와 attach는 같은 단계가 아니다. verifier를 통과해 kernel에 load된 program도 적절한 tracepoint, kprobe, network hook 등에 attach해야 실제 경로에서 호출된다. hook 선택은 &lt;a href=&quot;https://chaaany.tistory.com/495&quot;&gt;kprobe·tracepoint·uprobe의 차이&lt;/a&gt;와 연결된다.&lt;/p&gt;
&lt;h2&gt;verifier는 안전성 gate이지 정답 증명기가 아니다&lt;/h2&gt;
&lt;p&gt;verifier는 instruction과 control flow를 분석하며 register·stack state, pointer range, memory access, helper call 규칙 등을 추적한다. 안전하다고 증명할 수 없는 경로가 있으면 load를 거부한다.&lt;/p&gt;
&lt;p&gt;하지만 verifier 통과가 business logic의 정확성이나 낮은 overhead를 보장하지는 않는다. 잘못된 key로 통계를 집계하거나 지나치게 많은 event를 내보내는 program도 memory safety 조건을 만족할 수 있다. verifier는 &lt;strong&gt;kernel에서 허용 가능한 실행인지&lt;/strong&gt;를 판단하는 gate로 이해해야 한다.&lt;/p&gt;
&lt;p&gt;검증 규칙은 kernel version과 program type에 따라 발전한다. 특정 error log를 만났다면 일반적인 우회 code를 복사하기보다 실제 kernel의 verifier log와 context type을 확인한다.&lt;/p&gt;
&lt;h2&gt;map은 kernel object다&lt;/h2&gt;
&lt;p&gt;BPF map은 key-value data를 저장하는 kernel object이며 file descriptor로 참조할 수 있다. user space에서 syscall을 통해 읽고 쓸 수 있고, BPF program도 helper 등을 이용해 접근한다. program 사이에서 state를 공유하거나 event를 user space로 전달하는 데도 쓰인다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;BPF program ── lookup/update ── BPF map
                                  ↑
user process ── fd / libbpf ──────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;map을 단순히 “kernel과 user space 사이의 pipe”라고만 설명하면 부족하다. counter·LRU cache·per-CPU state·program array처럼 kernel 안의 program끼리 사용하는 상태도 map으로 구성할 수 있다. map type마다 concurrency, memory accounting, eviction 의미가 다르다.&lt;/p&gt;
&lt;h2&gt;helper와 kfunc는 허용된 kernel 기능의 경계다&lt;/h2&gt;
&lt;p&gt;BPF program은 등록된 helper function을 호출할 수 있다. 어떤 helper가 허용되는지는 program type에 따라 다르다. 최근 kernel에서는 BTF 정보를 사용하는 kfunc도 제공되지만, 이 역시 임의의 kernel function 호출이 아니며 허용 집합과 lifetime 규칙을 따라야 한다.&lt;/p&gt;
&lt;p&gt;따라서 sample code를 다른 hook에 옮겼을 때 verifier가 거부할 수 있다. target kernel version, program type, attach point가 모두 호환되는지 확인해야 한다.&lt;/p&gt;
&lt;h2&gt;JIT는 선택 사항이다&lt;/h2&gt;
&lt;p&gt;verifier를 통과한 BPF instruction은 interpreter가 실행할 수 있고, kernel configuration과 architecture가 지원하면 JIT compiler가 native instruction으로 변환할 수 있다. JIT 사용 여부와 hardening 설정은 kernel build와 sysctl policy에 달려 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 읽기 전용 확인. 파일이 없으면 해당 kernel/config에서 제공되지 않을 수 있다.
sysctl net.core.bpf_jit_enable 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;값 하나만으로 program별 실제 실행 상태나 성능을 단정하지 않는다. production에서 JIT 설정을 바꾸는 것은 system-wide security·performance policy 변경이므로, 읽기 확인과 변경 작업을 분리하고 kernel 문서와 조직 기준을 먼저 검토한다.&lt;/p&gt;
&lt;p&gt;eBPF가 허용하는 경계를 더 자세히 보려면 &lt;a href=&quot;https://chaaany.tistory.com/493&quot;&gt;eBPF verifier와 안전성&lt;/a&gt;, memory allocation 추적 사례는 &lt;a href=&quot;https://chaaany.tistory.com/492&quot;&gt;eBPF로 메모리를 추적하는 방법&lt;/a&gt;으로 이어진다.&lt;/p&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/bpf/standardization/abi.html&quot;&gt;Linux kernel: eBPF Instruction Set Specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/bpf/verifier.html&quot;&gt;Linux kernel: eBPF verifier&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/bpf/maps.html&quot;&gt;Linux kernel: BPF maps&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/bpf/kfuncs.html&quot;&gt;Linux kernel: BPF Kernel Functions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/bpf/libbpf/libbpf_overview.html&quot;&gt;Linux kernel: libbpf Overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/admin-guide/sysctl/net.html#bpf-jit-enable&quot;&gt;Linux kernel: BPF JIT sysctl&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>배움과 성장/시스템&amp;middot;성능</category>
      <category>BPF JIT</category>
      <category>BPF verifier</category>
      <category>eBPF 내부 구조</category>
      <category>eBPF 동작 원리</category>
      <category>리눅스 eBPF</category>
      <author>Chann._.y</author>
      <guid isPermaLink="true">https://chaaany.tistory.com/494</guid>
      <comments>https://chaaany.tistory.com/494#entry494comment</comments>
      <pubDate>Wed, 8 Apr 2026 22:00:37 +0900</pubDate>
    </item>
    <item>
      <title>eBPF verifier가 보장하는 것과 보장하지 않는 것</title>
      <link>https://chaaany.tistory.com/493</link>
      <description>&lt;p&gt;&lt;a href=&quot;https://chaaany.tistory.com/492&quot;&gt;malloc 호출을 eBPF로 추적한 실습&lt;/a&gt; 뒤에는 자연스럽게 이런 질문이 남았다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;kernel 안에서 실행되는 program을 사용자가 올릴 수 있다면, system을 망가뜨리지 않는다는 보장은 어디에서 오는가?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;짧게 답하면 eBPF program은 load 단계에서 verifier의 검사를 받아야 하고, 허용된 program type·attach point·helper의 경계 안에서 실행된다. 하지만 이를 “verifier가 모든 위험을 막는다”거나 “eBPF는 관찰만 하므로 안전하다”로 줄이면 중요한 부분이 빠진다.&lt;/p&gt;
&lt;p&gt;eBPF의 안전성은 &lt;strong&gt;정적 검증, 권한, kernel이 제공한 interface, 운영 통제&lt;/strong&gt;가 함께 만드는 결과다.&lt;/p&gt;
&lt;h2&gt;load부터 실행까지의 경로&lt;/h2&gt;
&lt;p&gt;일반적인 흐름을 단순화하면 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;eBPF bytecode
      ↓ bpf() system call
kernel verifier
      ├─ 거부 → verifier log와 error
      └─ 승인
           ↓ interpreter 또는 JIT compilation
program type에 맞는 hook에 attach
           ↓
event가 발생할 때 kernel context에서 실행
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;libbpf를 사용하면 ELF object를 열고, map을 만들고, program을 load하고, hook에 attach하는 userspace 작업을 도와준다. CO-RE를 쓰는 경우에는 실행 중인 kernel의 BTF 정보를 이용해 type layout 차이에 맞춘 relocation도 수행한다.&lt;/p&gt;
&lt;p&gt;여기서 verifier 통과는 “이 program이 의도대로 동작한다”는 인증이 아니다. kernel이 허용한 실행 model 안에서 실행 가능한지를 판정하는 gate에 가깝다.&lt;/p&gt;
&lt;h2&gt;verifier가 확인하는 대표적인 것&lt;/h2&gt;
&lt;p&gt;Linux kernel verifier 문서는 register와 stack slot의 상태를 추적하며 instruction path를 분석하는 방식을 설명한다. 실제 허용 범위는 kernel version, program type, helper와 instruction에 따라 달라지지만 핵심은 다음과 같다.&lt;/p&gt;
&lt;h3&gt;초기화되지 않은 값을 읽지 않는가&lt;/h3&gt;
&lt;p&gt;register와 stack 값은 사용 전에 초기화되어야 한다. verifier는 각 register가 scalar인지 pointer인지, 어떤 범위를 가질 수 있는지 추적한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;R1: context pointer
R2: known scalar range
R3: map value pointer or NULL
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;map lookup처럼 실패할 수 있는 helper의 반환값은 &lt;code&gt;NULL&lt;/code&gt; 여부를 확인한 뒤 역참조해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;struct value *value = bpf_map_lookup_elem(&amp;amp;counts, &amp;amp;key);
if (!value)
    return 0;

value-&amp;gt;count++;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;허용된 memory 범위 안에서 접근하는가&lt;/h3&gt;
&lt;p&gt;verifier는 stack, map value, packet data, context 같은 pointer의 type과 offset 범위를 추적한다. 증명할 수 없는 out-of-bounds 접근은 load 단계에서 거부한다.&lt;/p&gt;
&lt;p&gt;packet parser에서는 &lt;code&gt;data&lt;/code&gt;와 &lt;code&gt;data_end&lt;/code&gt;를 비교하는 code가 필요한 이유다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void *data_end = (void *)(long)ctx-&amp;gt;data_end;
void *data = (void *)(long)ctx-&amp;gt;data;
struct ethhdr *eth = data;

if ((void *)(eth + 1) &amp;gt; data_end)
    return XDP_ABORTED;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;helper와 context를 program type에 맞게 쓰는가&lt;/h3&gt;
&lt;p&gt;모든 eBPF program이 같은 helper와 kernel context에 접근하는 것은 아니다. XDP, tracing, cgroup, LSM 등 program type마다 attach 위치와 가능한 return action, helper 집합이 다르다.&lt;/p&gt;
&lt;p&gt;helper call에서는 argument type과 반환 type도 검사한다. 임의의 kernel function을 아무 signature로나 호출하는 model이 아니다. kfunc도 kernel이 BTF와 metadata를 통해 노출한 범위에서 사용한다.&lt;/p&gt;
&lt;h3&gt;실행이 끝난다는 것을 증명할 수 있는가&lt;/h3&gt;
&lt;p&gt;초기 eBPF 설명에는 “loop를 쓸 수 없다”는 문장이 자주 등장한다. 현재 kernel에서는 verifier가 종료와 complexity를 증명할 수 있는 bounded loop를 사용할 수 있다. helper 기반 iteration이나 open-coded iterator 같은 mechanism도 있다.&lt;/p&gt;
&lt;p&gt;중요한 것은 loop의 존재 자체가 아니라 가능한 path가 유한하고 verifier가 이를 분석할 수 있는지다. 범위가 지나치게 크거나 path가 폭발하면 논리상 끝나는 code도 verifier complexity 제한 때문에 거부될 수 있다.&lt;/p&gt;
&lt;h2&gt;verifier가 주는 보장을 정확히 읽는다&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;verifier가 막으려는 것&lt;/th&gt;
&lt;th&gt;verifier만으로 보장하지 않는 것&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;증명할 수 없는 pointer·범위 접근&lt;/td&gt;
&lt;td&gt;business logic의 정답 여부&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;초기화되지 않은 register·stack 사용&lt;/td&gt;
&lt;td&gt;관측 대상과 field가 정확한지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;program type에서 허용하지 않은 helper·context 사용&lt;/td&gt;
&lt;td&gt;CPU·memory·network 비용이 적절한지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;종료를 증명할 수 없는 control flow&lt;/td&gt;
&lt;td&gt;개인정보와 secret을 수집하지 않는지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;일부 잘못된 helper argument와 type 사용&lt;/td&gt;
&lt;td&gt;map key cardinality가 안전한지&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;따라서 verifier 승인 메시지는 memory-safety와 실행 model에 대한 중요한 방어선이지만 다음을 뜻하지 않는다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;program이 성능 저하를 만들지 않는다.&lt;/li&gt;
&lt;li&gt;수집한 data가 정확하다.&lt;/li&gt;
&lt;li&gt;race나 logical bug가 없다.&lt;/li&gt;
&lt;li&gt;security policy가 올바르다.&lt;/li&gt;
&lt;li&gt;kernel, JIT, verifier, helper 구현에 취약점이 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;eBPF는 관찰만 하는 기술이 아니다&lt;/h2&gt;
&lt;p&gt;tracing program으로 event를 읽고 map이나 ring buffer에 기록하는 use case가 익숙해서 eBPF를 read-only 관측 도구로 오해하기 쉽다. 실제 효과는 program type과 helper가 정한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;XDP와 traffic-control program은 packet을 drop, redirect하거나 허용된 범위에서 수정할 수 있다.&lt;/li&gt;
&lt;li&gt;cgroup networking program은 socket·packet policy에 관여할 수 있다.&lt;/li&gt;
&lt;li&gt;BPF LSM program은 security hook에서 접근을 허용하거나 거부할 수 있다.&lt;/li&gt;
&lt;li&gt;map update는 kernel 안의 state를 바꾼다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;eBPF program이 임의의 kernel memory를 자유롭게 덮어쓰도록 허용되는 것은 아니다. 그렇다고 system behavior를 바꿀 수 없다는 뜻도 아니다. &lt;strong&gt;kernel이 program type과 helper로 허용한 효과&lt;/strong&gt; 안에서는 실제 traffic과 policy에 영향을 줄 수 있다.&lt;/p&gt;
&lt;h2&gt;권한은 root 하나로 설명되지 않는다&lt;/h2&gt;
&lt;p&gt;과거에는 많은 BPF operation이 포괄적인 &lt;code&gt;CAP_SYS_ADMIN&lt;/code&gt;에 묶여 있었다. Linux 5.8부터 &lt;code&gt;CAP_BPF&lt;/code&gt;가 privileged BPF operation을 분리하기 위해 추가됐다. 하지만 실제 program을 load하고 attach하는 데 필요한 capability는 use case에 따라 더 있을 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;tracing·performance event 접근: &lt;code&gt;CAP_PERFMON&lt;/code&gt;과 관련 설정&lt;/li&gt;
&lt;li&gt;networking configuration: &lt;code&gt;CAP_NET_ADMIN&lt;/code&gt;이 필요한 operation&lt;/li&gt;
&lt;li&gt;BPF object load·map 관리: &lt;code&gt;CAP_BPF&lt;/code&gt; 또는 호환을 위한 &lt;code&gt;CAP_SYS_ADMIN&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;또한 다음 환경 차이가 작동한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;kernel.unprivileged_bpf_disabled&lt;/code&gt; 값&lt;/li&gt;
&lt;li&gt;kernel build configuration과 version&lt;/li&gt;
&lt;li&gt;distribution의 security policy&lt;/li&gt;
&lt;li&gt;LSM, lockdown mode와 container security context&lt;/li&gt;
&lt;li&gt;namespace와 capability bounding set&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;현재 값은 다음처럼 읽을 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sysctl kernel.unprivileged_bpf_disabled
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;kernel 문서에서 값 &lt;code&gt;0&lt;/code&gt;은 unprivileged &lt;code&gt;bpf()&lt;/code&gt; call 허용, &lt;code&gt;1&lt;/code&gt;은 되돌릴 수 없는 비활성화, &lt;code&gt;2&lt;/code&gt;는 administrator가 다시 변경할 수 있는 비활성화를 뜻한다. default는 kernel configuration에 따라 달라질 수 있으므로 숫자를 확인하지 않고 정책을 가정하면 안 된다.&lt;/p&gt;
&lt;p&gt;container에 &lt;code&gt;privileged: true&lt;/code&gt;를 주는 것은 필요한 capability만 설계하는 것보다 훨씬 넓은 권한을 연다. 도구가 실제로 필요로 하는 program type, attach point와 capability를 먼저 확인하는 편이 안전하다.&lt;/p&gt;
&lt;h2&gt;현실에서 남는 위험&lt;/h2&gt;
&lt;h3&gt;verifier·JIT·helper도 kernel code다&lt;/h3&gt;
&lt;p&gt;verifier는 복잡한 static analyzer이고 JIT compiler와 helper도 kernel의 일부다. 이 구성요소의 취약점은 security issue가 될 수 있다. production kernel과 distribution security update를 적용해야 하는 이유다.&lt;/p&gt;
&lt;h3&gt;너무 뜨거운 hook은 작은 code도 비싸다&lt;/h3&gt;
&lt;p&gt;packet마다, system call마다, allocation마다 실행되는 program은 호출 빈도가 매우 높다. event 하나당 작은 비용도 전체 CPU 사용량에 영향을 줄 수 있다.&lt;/p&gt;
&lt;p&gt;다음을 함께 관찰해야 한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;program run count와 run time&lt;/li&gt;
&lt;li&gt;ring buffer drop&lt;/li&gt;
&lt;li&gt;map memory와 entry count&lt;/li&gt;
&lt;li&gt;user-space consumer backlog&lt;/li&gt;
&lt;li&gt;target workload의 latency와 CPU 변화&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;kernel의 &lt;code&gt;kernel.bpf_stats_enabled&lt;/code&gt;를 켜면 program runtime 통계 수집 자체에도 overhead가 있다고 문서에 명시돼 있다. 진단 기능도 비용이 없는 것은 아니다.&lt;/p&gt;
&lt;h3&gt;map은 memory와 lifecycle을 가진다&lt;/h3&gt;
&lt;p&gt;BPF map은 kernel과 userspace 사이에서 data를 공유하는 핵심 primitive다. 동시에 type, maximum entries, key cardinality와 eviction policy를 잘못 잡으면 memory 압박이나 stale state를 만들 수 있다.&lt;/p&gt;
&lt;p&gt;pinning된 map과 program은 loader process가 종료된 뒤에도 reference가 남을 수 있다. 배포·rollback 때 누가 생성하고 교체하고 제거하는지 lifecycle을 정해야 한다.&lt;/p&gt;
&lt;h3&gt;관측 data도 민감정보가 될 수 있다&lt;/h3&gt;
&lt;p&gt;argument, path, network payload, user identifier를 수집하면 debugging에는 도움이 되지만 privacy와 security 위험이 커진다. 가능한 field만 allowlist로 수집하고, kernel에서 user space로 보내기 전에 redaction·aggregation할 수 있는지 검토한다.&lt;/p&gt;
&lt;h2&gt;load 실패는 verifier log부터 읽는다&lt;/h2&gt;
&lt;p&gt;verifier가 program을 거부하면 무작정 instruction을 줄이기보다 log에서 어느 invariant를 증명하지 못했는지 본다.&lt;/p&gt;
&lt;p&gt;자주 만나는 원인은 다음과 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;map lookup 뒤 &lt;code&gt;NULL&lt;/code&gt; check 누락&lt;/li&gt;
&lt;li&gt;packet의 &lt;code&gt;data_end&lt;/code&gt; boundary check 누락&lt;/li&gt;
&lt;li&gt;scalar range를 verifier가 충분히 좁히지 못함&lt;/li&gt;
&lt;li&gt;program type에서 사용할 수 없는 helper 호출&lt;/li&gt;
&lt;li&gt;stack limit 또는 instruction-path complexity 초과&lt;/li&gt;
&lt;li&gt;kernel version과 BTF·helper 지원 차이&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;libbpf의 debug output이나 loader가 제공하는 verifier log를 함께 남기고, 실행 중인 kernel release와 object의 build 정보를 기록해야 재현이 쉽다.&lt;/p&gt;
&lt;h2&gt;production 적용 전 체크리스트&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;지원할 kernel·distribution version 범위를 명시했는가?&lt;/li&gt;
&lt;li&gt;program type과 attach point, return action을 검토했는가?&lt;/li&gt;
&lt;li&gt;필요한 capability만 부여했는가?&lt;/li&gt;
&lt;li&gt;verifier log를 build·test 환경에서 보존하는가?&lt;/li&gt;
&lt;li&gt;hot path의 CPU overhead와 event loss를 부하 조건에서 측정했는가?&lt;/li&gt;
&lt;li&gt;map의 maximum entries, memory와 eviction policy를 정했는가?&lt;/li&gt;
&lt;li&gt;수집 field에 credential·개인정보가 포함되지 않는가?&lt;/li&gt;
&lt;li&gt;program·link·pinned object의 upgrade와 rollback 절차가 있는가?&lt;/li&gt;
&lt;li&gt;kernel security update와 eBPF toolchain version을 추적하는가?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;eBPF를 안전하다고 부를 수 있는 이유는 kernel module처럼 아무 code나 바로 실행하기 때문이 아니라, verifier와 type별 interface가 실행 가능 범위를 좁히기 때문이다. 동시에 그 범위 안에서도 packet을 바꾸고 policy를 적용하며 많은 resource를 쓸 수 있다.&lt;/p&gt;
&lt;p&gt;다음 글인 &lt;a href=&quot;https://chaaany.tistory.com/494&quot;&gt;eBPF 내부 구조&lt;/a&gt;와 &lt;a href=&quot;https://chaaany.tistory.com/495&quot;&gt;eBPF hook 종류&lt;/a&gt;를 볼 때도 “어디에 attach하고 무엇을 허용받는가”를 함께 보면 기능과 위험을 더 정확히 읽을 수 있다.&lt;/p&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/bpf/verifier.html&quot;&gt;Linux kernel: eBPF verifier&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/bpf/bpf_iterators.html&quot;&gt;Linux kernel: BPF iterators&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/torvalds/linux/blob/master/tools/bpf/bpftool/feature.c&quot;&gt;Linux bpftool source: bounded-loop feature probe&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/bpf/bpf_design_QA.html&quot;&gt;Linux kernel: BPF Design Q&amp;amp;A&lt;/a&gt; — 일부 답변은 작성 당시 제약을 담고 있으므로 현재 verifier·kernel 문서와 함께 읽어야 한다.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/bpf/maps.html&quot;&gt;Linux kernel: BPF maps&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/bpf/libbpf/libbpf_overview.html&quot;&gt;Linux kernel: libbpf overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/admin-guide/sysctl/kernel.html#unprivileged-bpf-disabled&quot;&gt;Linux kernel: kernel sysctl documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://man7.org/linux/man-pages/man7/capabilities.7.html&quot;&gt;Linux capabilities manual&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>배움과 성장/시스템&amp;middot;성능</category>
      <category>BPF보안</category>
      <category>ebpf</category>
      <category>eBPF verifier</category>
      <category>LinuxKernel</category>
      <category>시스템관측</category>
      <author>Chann._.y</author>
      <guid isPermaLink="true">https://chaaany.tistory.com/493</guid>
      <comments>https://chaaany.tistory.com/493#entry493comment</comments>
      <pubDate>Tue, 7 Apr 2026 22:00:25 +0900</pubDate>
    </item>
    <item>
      <title>eBPF로 사용자 공간 메모리 할당 추적하기: malloc 호출과 누수 구분</title>
      <link>https://chaaany.tistory.com/492</link>
      <description>&lt;p&gt;&lt;code&gt;/proc/&amp;lt;PID&amp;gt;/smaps&lt;/code&gt;로 익명 메모리가 늘어나는 매핑을 찾았다면 다음 질문은 “어느 호출 경로가 할당을 만들었는가?”다. 이때 eBPF 기반 도구로 사용자 공간 allocator 함수에 uprobe를 붙여 호출 스택과 요청 크기를 관찰할 수 있다.&lt;/p&gt;
&lt;p&gt;중요한 경계부터 말하면, &lt;strong&gt;&lt;code&gt;malloc&lt;/code&gt; 호출 횟수만 세어서는 메모리 누수를 증명할 수 없다.&lt;/strong&gt; 반환된 주소와 크기를 기록하고 대응하는 &lt;code&gt;free&lt;/code&gt;를 제거한 뒤, 일정 시간 남아 있는 outstanding allocation을 봐야 누수 후보에 가까워진다.&lt;/p&gt;
&lt;h2&gt;smaps와 할당 추적이 답하는 질문&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;도구&lt;/th&gt;
&lt;th&gt;답하기 좋은 질문&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/proc/PID/smaps&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;어떤 매핑과 메모리 성격이 커졌는가?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;heap profiler&lt;/td&gt;
&lt;td&gt;언어·런타임이 아는 객체와 할당 지점은 어디인가?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;eBPF uprobe&lt;/td&gt;
&lt;td&gt;실행 중인 프로세스에서 어떤 allocator 함수와 호출 스택이 관찰되는가?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;smaps에서 Anonymous가 늘었다고 곧바로 &lt;code&gt;malloc&lt;/code&gt;만 추적하는 것도 성급하다. Java·Go처럼 자체 런타임과 GC를 쓰거나 jemalloc·tcmalloc을 쓰는 프로세스는 glibc &lt;code&gt;malloc&lt;/code&gt;이 핵심 경로가 아닐 수 있다.&lt;/p&gt;
&lt;h2&gt;먼저 대상과 allocator를 확인한다&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pid=1234
readlink -f &amp;quot;/proc/$pid/exe&amp;quot;
cat &amp;quot;/proc/$pid/maps&amp;quot; | grep -E &amp;#39;libc|jemalloc|tcmalloc&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;확인할 것은 다음과 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;정확한 PID와 실행 파일&lt;/li&gt;
&lt;li&gt;glibc, jemalloc, tcmalloc 또는 언어 런타임 중 무엇이 할당하는지&lt;/li&gt;
&lt;li&gt;심볼이 제거됐는지와 사용자 스택을 해석할 디버그 심볼이 있는지&lt;/li&gt;
&lt;li&gt;컨테이너·PID namespace에서 어느 호스트 PID에 붙어야 하는지&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;bpftrace로 요청한 할당량을 빠르게 보기&lt;/h2&gt;
&lt;p&gt;bpftrace는 라이브러리 이름을 &lt;code&gt;/etc/ld.so.cache&lt;/code&gt;에서 해석할 수 있다. 다음 예시는 특정 PID의 &lt;code&gt;malloc&lt;/code&gt; 진입점에서 호출 스택별 &lt;strong&gt;요청 바이트 합계&lt;/strong&gt;를 집계한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pid=1234
sudo bpftrace -p &amp;quot;$pid&amp;quot; -e &amp;#39;
uprobe:libc:malloc
{
  @[ustack] = sum(arg0);
}&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;arg0&lt;/code&gt;는 &lt;code&gt;malloc(size_t size)&lt;/code&gt;의 첫 번째 인자다. 이 수치는 호출자가 요청한 누적 크기이지 현재 살아 있는 메모리도, RSS 증가량도 아니다. allocator가 내부적으로 반올림한 크기, 재사용한 arena, 실제 page fault와도 다를 수 있다.&lt;/p&gt;
&lt;p&gt;호출 수가 필요하면 &lt;code&gt;sum(arg0)&lt;/code&gt; 대신 &lt;code&gt;count()&lt;/code&gt;를 쓸 수 있다. 그러나 높은 빈도의 할당에서 모든 사용자 스택을 수집하면 부하가 커질 수 있으므로 짧은 시간과 한 PID로 범위를 제한한다.&lt;/p&gt;
&lt;h2&gt;outstanding allocation은 BCC memleak으로 보기&lt;/h2&gt;
&lt;p&gt;BCC의 &lt;code&gt;memleak&lt;/code&gt; 도구는 사용자 공간 할당과 해제 함수를 함께 추적하고, 아직 해제되지 않은 주소를 스택별로 집계한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo memleak -p 1234 5 6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 형태는 PID 1234를 추적해 5초 간격으로 6회 보고하는 예시다. 배포판에 따라 실행 파일이 &lt;code&gt;/usr/share/bcc/tools/memleak&lt;/code&gt;에 있을 수 있다.&lt;/p&gt;
&lt;p&gt;할당이 매우 잦다면 샘플링과 최소 크기를 사용해 관찰 비용을 줄일 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo memleak -p 1234 --sample-rate 10 --min-size 4096 5 6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;남아 있는 할당은 어디까지나 &lt;strong&gt;후보&lt;/strong&gt;다. 정상적으로 오래 사는 캐시나 초기화 데이터도 출력될 수 있다. 같은 부하를 반복해 오래된 할당량이 계속 증가하는지, 서비스의 heap 지표와 함께 확인한다.&lt;/p&gt;
&lt;h2&gt;malloc과 free를 직접 짝지을 때 필요한 것&lt;/h2&gt;
&lt;p&gt;직접 추적기를 만든다면 다음 상태가 모두 필요하다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;malloc&lt;/code&gt; 진입 시 스레드별 요청 크기를 임시 저장한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;uretprobe&lt;/code&gt;에서 반환 주소와 크기, 호출 스택을 기록한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;free(ptr)&lt;/code&gt;가 호출되면 해당 주소를 맵에서 제거한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;calloc&lt;/code&gt;, &lt;code&gt;realloc&lt;/code&gt;, &lt;code&gt;posix_memalign&lt;/code&gt; 같은 다른 경로도 처리한다.&lt;/li&gt;
&lt;li&gt;프로세스 종료, 실패한 할당과 주소 재사용을 처리한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;bpftrace 문서에서 &lt;code&gt;uprobe&lt;/code&gt;는 인자를, &lt;code&gt;uretprobe&lt;/code&gt;는 반환값을 읽는 사용자 공간 probe로 구분한다. 런타임에 따라 return probe가 안전하지 않거나 스택 해석이 제한될 수 있으므로 대상별 검증이 필요하다.&lt;/p&gt;
&lt;h2&gt;운영 환경에서의 안전 체크&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;먼저 스테이징이나 동일한 부하 재현 환경에서 오버헤드를 측정한다.&lt;/li&gt;
&lt;li&gt;호스트 전체가 아니라 PID, 함수, 크기와 시간으로 범위를 제한한다.&lt;/li&gt;
&lt;li&gt;스택·주소·문자열에 민감 정보가 포함될 수 있어 출력 접근과 보존 기간을 제한한다.&lt;/li&gt;
&lt;li&gt;심볼이 보이지 않는 결과를 추측으로 함수 이름에 연결하지 않는다.&lt;/li&gt;
&lt;li&gt;관찰을 끝내면 probe가 해제됐는지 확인한다.&lt;/li&gt;
&lt;li&gt;eBPF 실행 권한은 강력하므로 최소 인원과 감사 가능한 절차로 관리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;분석 흐름을 한 줄로 연결하면&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;RSS 증가 확인
  → smaps로 증가 매핑과 Anonymous/PSS 확인
  → 런타임·allocator 확인
  → profiler 또는 eBPF로 호출 경로 추적
  → outstanding allocation 추세와 코드 수명주기 대조
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;eBPF는 코드를 수정하지 않고 관찰 지점을 추가할 수 있다는 장점이 있다. 하지만 항상 낮은 오버헤드이거나 자동으로 누수를 찾아 주는 도구는 아니다. &lt;strong&gt;무엇을 추적했고 무엇은 보지 못했는지&lt;/strong&gt; 함께 기록해야 결과를 믿을 수 있다.&lt;/p&gt;
&lt;p&gt;관련 글: &lt;a href=&quot;https://chaaany.tistory.com/491&quot;&gt;/proc/PID/smaps로 RSS·PSS를 나누는 법&lt;/a&gt;, &lt;a href=&quot;https://chaaany.tistory.com/493&quot;&gt;eBPF가 안전한 이유와 위험해지는 조건&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;참고 문서&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://bpftrace.org/docs/release_024/language#uprobe-uretprobe&quot;&gt;bpftrace: uprobe와 uretprobe&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/iovisor/bcc/blob/master/tools/memleak.py&quot;&gt;iovisor/bcc: memleak.py&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/bpf/bpf_design_QA.html&quot;&gt;Linux kernel: BPF design Q&amp;amp;A&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>배움과 성장/시스템&amp;middot;성능</category>
      <category>BCC memleak</category>
      <category>bpftrace uprobe</category>
      <category>eBPF 메모리 추적</category>
      <category>eBPF 메모리 할당 추적</category>
      <category>malloc 추적</category>
      <author>Chann._.y</author>
      <guid isPermaLink="true">https://chaaany.tistory.com/492</guid>
      <comments>https://chaaany.tistory.com/492#entry492comment</comments>
      <pubDate>Mon, 6 Apr 2026 22:00:39 +0900</pubDate>
    </item>
    <item>
      <title>/proc/PID/smaps 메모리 분석: RSS&amp;middot;PSS&amp;middot;Anonymous를 구분하는 법</title>
      <link>https://chaaany.tistory.com/491</link>
      <description>&lt;p&gt;프로세스 RSS가 늘었다고 바로 메모리 누수라고 결론 내릴 수는 없다. RSS에는 익명 메모리, 실행 파일과 공유 라이브러리의 파일 매핑, 공유 메모리 등 현재 RAM에 올라온 여러 페이지가 함께 잡힌다.&lt;/p&gt;
&lt;p&gt;Linux의 &lt;code&gt;/proc/&amp;lt;PID&amp;gt;/smaps&lt;/code&gt;는 이 합계를 &lt;strong&gt;가상 메모리 매핑별로&lt;/strong&gt; 나눠 보여 준다. 원인을 확정하는 도구라기보다, 다음 조사 대상을 좁히는 도구에 가깝다.&lt;/p&gt;
&lt;h2&gt;smaps와 smaps_rollup의 차이&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pid=1234
sudo cat &amp;quot;/proc/$pid/smaps_rollup&amp;quot;
sudo less &amp;quot;/proc/$pid/smaps&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;smaps_rollup&lt;/code&gt;: 프로세스 전체의 주요 항목을 합산해 빠르게 본다. 커널에서 제공하는 경우에 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;smaps&lt;/code&gt;: 주소 범위와 권한, 파일 경로를 포함해 매핑 하나씩 본다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;다른 사용자의 프로세스는 권한이나 &lt;code&gt;ptrace&lt;/code&gt; 정책 때문에 읽지 못할 수 있다. 컨테이너 안에서는 PID namespace가 달라 호스트 PID와 컨테이너 PID도 구분해야 한다.&lt;/p&gt;
&lt;h2&gt;먼저 읽을 핵심 필드&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;필드&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;th&gt;해석할 때의 주의점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;해당 가상 주소 매핑의 크기&lt;/td&gt;
&lt;td&gt;실제 RAM 사용량이 아니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Rss&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 RAM에 상주한 페이지 크기&lt;/td&gt;
&lt;td&gt;공유 페이지가 프로세스마다 중복 집계될 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Pss&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;공유 페이지를 공유 프로세스 수에 비례해 나눈 값&lt;/td&gt;
&lt;td&gt;여러 프로세스의 합산 비용을 비교할 때 유용하다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Private_Clean&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;이 프로세스에만 귀속되고 수정되지 않은 상주 페이지&lt;/td&gt;
&lt;td&gt;파일 재로딩 가능 여부를 함께 봐야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Private_Dirty&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;이 프로세스에만 귀속되고 수정된 상주 페이지&lt;/td&gt;
&lt;td&gt;크다고 곧바로 누수라는 뜻은 아니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Shared_Clean/Dirty&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;다른 프로세스와 공유되는 깨끗한·수정된 페이지&lt;/td&gt;
&lt;td&gt;라이브러리·공유 매핑의 성격을 함께 확인한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Anonymous&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;파일에 속하지 않는 상주 메모리&lt;/td&gt;
&lt;td&gt;힙만이 아니라 익명 &lt;code&gt;mmap&lt;/code&gt;과 copy-on-write 페이지도 포함한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Swap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;해당 매핑에서 swap으로 나간 메모리&lt;/td&gt;
&lt;td&gt;지연과 메모리 압력의 맥락을 같이 본다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;PSS가 RSS보다 무조건 ‘더 정확한 단 하나의 값’인 것은 아니다. 질문이 다르다. 한 프로세스가 접근하는 상주 페이지 총량을 보려면 RSS가, 공유 비용을 나눠 여러 프로세스의 메모리 기여도를 합산하려면 PSS가 더 적합하다.&lt;/p&gt;
&lt;h2&gt;매핑 헤더부터 읽기&lt;/h2&gt;
&lt;p&gt;각 블록의 첫 줄은 대략 다음 모양이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;7f2c1a000000-7f2c1a200000 rw-p 00000000 00:00 0 [heap]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;확인할 내용은 네 가지다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;주소 범위&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rwx&lt;/code&gt; 접근 권한과 private/shared 표시&lt;/li&gt;
&lt;li&gt;파일 경로 또는 &lt;code&gt;[heap]&lt;/code&gt;, &lt;code&gt;[stack]&lt;/code&gt; 같은 이름&lt;/li&gt;
&lt;li&gt;그 아래의 RSS·PSS·Anonymous·Dirty 값&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이름이 &lt;code&gt;[heap]&lt;/code&gt;이라고 표시된 영역만 동적 할당 메모리인 것도 아니다. 현대 할당자는 큰 요청이나 arena를 익명 &lt;code&gt;mmap&lt;/code&gt;으로 확보할 수 있다. 반대로 &lt;code&gt;mmap&lt;/code&gt;은 파일 기반일 수도, 익명일 수도 있다. 따라서 &lt;strong&gt;heap·file·mmap을 서로 배타적인 세 종류로 나누면 안 된다.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;합계에서 큰 매핑으로 내려가기&lt;/h2&gt;
&lt;p&gt;전체 합계를 먼저 확인한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pid=1234
sudo grep -E &amp;#39;^(Rss|Pss|Private_|Shared_|Anonymous|Swap):&amp;#39; &amp;quot;/proc/$pid/smaps_rollup&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그다음 &lt;code&gt;smaps&lt;/code&gt;에서 매핑별 PSS와 Anonymous가 큰 구간을 찾는다. 단일 시점보다 같은 부하 구간의 스냅샷을 반복해서 비교하는 것이 중요하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pid=1234
sudo awk &amp;#39;
  /^[0-9a-f]+-[0-9a-f]+/ { mapping=$0 }
  /^Pss:/ &amp;amp;&amp;amp; $2 &amp;gt; 10240 { print $2 &amp;quot; kB\t&amp;quot; mapping }
&amp;#39; &amp;quot;/proc/$pid/smaps&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 예시는 PSS가 10MiB보다 큰 매핑을 표시한다. 임계값은 워크로드에 맞춰 바꾼다.&lt;/p&gt;
&lt;h2&gt;증가 패턴을 어떻게 해석할까&lt;/h2&gt;
&lt;h3&gt;Anonymous와 Private_Dirty가 함께 증가&lt;/h3&gt;
&lt;p&gt;애플리케이션이 실제로 쓰는 익명 페이지가 늘고 있을 가능성이 있다. 정상 캐시, 런타임 heap 성장, allocator fragmentation, 해제되지 않은 객체 모두 후보다. &lt;strong&gt;누수 확정이 아니라 할당 경로 조사 신호&lt;/strong&gt;로 본다.&lt;/p&gt;
&lt;h3&gt;파일 경로가 있는 Shared_Clean 증가&lt;/h3&gt;
&lt;p&gt;공유 라이브러리나 읽기 전용 파일 매핑이 상주하기 시작했을 수 있다. 파일 기반 페이지는 필요할 때 회수될 수 있으므로 메모리 압력과 재사용 패턴을 함께 확인한다.&lt;/p&gt;
&lt;h3&gt;가상 &lt;code&gt;Size&lt;/code&gt;만 크게 증가&lt;/h3&gt;
&lt;p&gt;주소 공간을 예약했지만 아직 페이지가 실제로 fault-in되지 않았을 수 있다. RSS·PSS가 함께 늘었는지 확인한다.&lt;/p&gt;
&lt;h3&gt;Swap이 증가&lt;/h3&gt;
&lt;p&gt;프로세스의 익명 페이지 일부가 swap으로 이동했다. 단순 용량뿐 아니라 major fault, PSI memory pressure와 지연 변화를 같이 본다.&lt;/p&gt;
&lt;h2&gt;누수 판단까지 가는 순서&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;같은 워크로드에서 RSS·PSS의 시간 추세를 기록한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;smaps_rollup&lt;/code&gt;으로 Anonymous·Private·Shared·Swap 변화를 나눈다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;smaps&lt;/code&gt;에서 증가를 주도하는 매핑을 찾는다.&lt;/li&gt;
&lt;li&gt;애플리케이션 캐시, 런타임 GC와 allocator 동작을 확인한다.&lt;/li&gt;
&lt;li&gt;재현 가능한 경우 heap profiler나 할당 추적으로 호출 경로를 찾는다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;Private_Dirty&lt;/code&gt;가 크다는 사실만으로 “진짜 내가 쓰는 메모리”나 “누수”라고 부르면 한 단계가 빠져 있다. smaps는 &lt;strong&gt;무엇이 상주하고 있는지&lt;/strong&gt; 보여 주고, 왜 남았는지는 다음 도구로 검증해야 한다.&lt;/p&gt;
&lt;p&gt;할당 호출 경로를 좁히는 단계는 &lt;a href=&quot;https://chaaany.tistory.com/492&quot;&gt;eBPF로 사용자 공간 메모리 할당 추적하기&lt;/a&gt;에서 이어서 다룬다.&lt;/p&gt;
&lt;h2&gt;참고 문서&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://man7.org/linux/man-pages/man5/proc_pid_smaps.5.html&quot;&gt;Linux man-pages: proc_pid_smaps(5)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/filesystems/proc.html&quot;&gt;Linux kernel: The /proc Filesystem&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>배움과 성장/시스템&amp;middot;성능</category>
      <category>Anonymous 메모리</category>
      <category>Linux 메모리 분석</category>
      <category>proc smaps</category>
      <category>proc smaps 메모리 분석</category>
      <category>RSS PSS</category>
      <author>Chann._.y</author>
      <guid isPermaLink="true">https://chaaany.tistory.com/491</guid>
      <comments>https://chaaany.tistory.com/491#entry491comment</comments>
      <pubDate>Sun, 5 Apr 2026 22:00:36 +0900</pubDate>
    </item>
  </channel>
</rss>