방화벽이나 NAT 게이트웨이, 혹은 커넥션이 많은 웹 서버를 운영하다 보면 어느 순간부터 간헐적으로 커넥션이 끊기고 응답이 느려지는 증상을 만난다. 애플리케이션 로그에는 아무것도 안 남는데 dmesg를 열어 보면 커널이 패킷을 버리고 있던 경우가 많다. 범인은 대개 conntrack 테이블이다. 평소엔 존재조차 의식하지 않다가, 꽉 차는 순간에만 티를 낸다.
결론부터 말하자면, net.netfilter.nf_conntrack_count가 nf_conntrack_max에 거의 붙어 있으면 테이블 포화다. 급한 불은 sysctl -w net.netfilter.nf_conntrack_max를 키워서 끄고, 지속적으로 커넥션이 많은 서버라면 해시 버킷 크기(hashsize)를 함께 늘리고 TCP timeout을 줄여 엔트리가 빨리 회수되게 한다. 커넥션 추적이 필요 없는 구간이라면 아예 추적을 끄는 선택도 있다.
1. conntrack이 무엇이고 왜 꽉 차는가
conntrack은 커널 netfilter의 커넥션 추적 기능이다. 커널은 통과하는 각 커넥션(출발지, 목적지 IP, 포트, 프로토콜 조합)을 해시 테이블에 엔트리로 올려 두고 상태를 기억한다. 이 기억이 있어야 stateful 방화벽 규칙(-m state --state ESTABLISHED,RELATED)과 NAT가 동작한다. 응답 패킷이 돌아왔을 때 "이건 아까 그 커넥션의 회신"이라고 판단하는 근거가 바로 이 엔트리다.
문제는 이 테이블에 상한이 있다는 점이다. 상한이 nf_conntrack_max이고, 엔트리 수가 여기에 도달하면 커널은 새 엔트리를 만들지 못해 해당 패킷을 버린다. 그때 찍히는 로그가 아래다.
kernel: nf_conntrack: table full, dropping packet
버려지는 건 "새 커넥션"의 첫 패킷인 경우가 많아서, 기존 연결은 멀쩡한데 신규 접속만 간헐적으로 실패하는 형태로 나타난다. 그래서 원인 짚기가 까다롭다.
2. 지금 테이블이 얼마나 찼는지 확인
현재 엔트리 수(nf_conntrack_count)와 상한(nf_conntrack_max)을 나란히 본다.
$ sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max net.netfilter.nf_conntrack_count = 128742 net.netfilter.nf_conntrack_max = 131072
count가 max의 90%를 넘기 시작하면 위험 구간이다. 위 예시처럼 거의 붙어 있으면 이미 드롭이 나고 있을 가능성이 높다. /proc에서도 같은 값을 읽을 수 있다.
$ cat /proc/sys/net/netfilter/nf_conntrack_count 128742
드롭 로그가 실제로 찍혔는지는 커널 메시지에서 확인한다.
$ dmesg -T | grep -i conntrack [Mon Oct 19 14:02:11 2026] nf_conntrack: nf_conntrack: table full, dropping packet
conntrack-tools 패키지(RHEL 계열 dnf install conntrack-tools, 데비안 계열 apt install conntrack)가 있으면 통계를 CPU별로 볼 수 있다. insert_failed와 drop 값이 올라가고 있으면 포화가 맞다.
$ conntrack -S cpu=0 found=0 invalid=12 insert=0 insert_failed=331 drop=331 ... cpu=1 found=0 invalid=9 insert=0 insert_failed=287 drop=287 ...
엔트리가 어떤 커넥션으로 채워졌는지 분포를 보고 싶으면 아래처럼 목적지 포트별로 집계한다. 특정 포트가 엔트리를 독식하고 있으면 그쪽 애플리케이션이나 스캔성 트래픽을 의심한다.
$ conntrack -L 2>/dev/null | awk '{print $1, $4}' | sort | uniq -c | sort -rn | head
3. 급한 불 끄기 - nf_conntrack_max 올리기
드롭이 진행 중이라면 먼저 상한부터 올린다. 런타임에 즉시 반영되고 재부팅 전까지 유지된다.
$ sudo sysctl -w net.netfilter.nf_conntrack_max=524288 net.netfilter.nf_conntrack_max = 524288
어느 값으로 잡을지는 메모리와 상관있다. 커널 문서 기준으로 엔트리 하나는 수백 바이트를 쓴다. 64비트 시스템에서 흔히 쓰는 어림식은 nf_conntrack_max = 총 메모리(바이트) / 16384 / 2다. 예를 들어 4GB 머신이면 대략 13만 수준이 나오는데, 실제 메모리 여유를 보고 2배, 4배로 키우는 식으로 조정한다. 무턱대고 수천만으로 올리면 커널 메모리를 그만큼 점유하므로 count 추이를 보면서 단계적으로 늘린다.
재부팅 후에도 유지하려면 /etc/sysctl.d/에 파일로 남긴다.
$ sudo tee /etc/sysctl.d/99-conntrack.conf <<'EOF' net.netfilter.nf_conntrack_max = 524288 net.netfilter.nf_conntrack_tcp_timeout_established = 86400 net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30 EOF $ sudo sysctl --system
4. 해시 버킷(hashsize) 함께 늘리기
여기서 한 번 걸린다. nf_conntrack_max만 키우고 해시 버킷을 그대로 두면 테이블은 커졌는데 해시 버킷 하나에 매달린 체인이 길어진다. 조회할 때마다 긴 체인을 훑어야 해서 CPU가 더 든다. 버킷 수는 nf_conntrack_buckets(읽기)와 모듈 파라미터 hashsize로 제어한다.
$ cat /sys/module/nf_conntrack/parameters/hashsize 32768
통용되는 비율은 nf_conntrack_max = hashsize * 4 정도다. max를 524288로 잡았다면 hashsize는 131072 안팎이 적당하다. 실행 중에는 /sys에 직접 쓰는 방법도 있다.
$ echo 131072 | sudo tee /sys/module/nf_conntrack/parameters/hashsize
재부팅 후에도 적용되게 하려면 modprobe 설정으로 남긴다. 모듈이 로드될 때 이 값으로 테이블이 할당된다.
$ sudo tee /etc/modprobe.d/nf_conntrack.conf <<'EOF' options nf_conntrack hashsize=131072 EOF
참고로 nf_conntrack_max의 기본값은 커널에서 nf_conntrack_buckets와 같게 잡힌다. 버킷 기본값은 메모리를 16384로 나눠 계산하며, 4GB 이상 머신이면 대개 버킷 65536, max도 그 근처에서 시작한다. 그래서 둘 중 하나만 손대면 균형이 깨진다.
5. timeout 줄여 엔트리 빨리 회수하기
상한을 키우는 것만이 답은 아니다. 커넥션이 끝나도 엔트리는 timeout 기간 동안 테이블에 남는다. 이 기간이 길면 이미 닫힌 커넥션이 자리를 오래 차지한다. 특히 established TCP의 기본 timeout은 432000초, 즉 5일이다.
$ sysctl net.netfilter.nf_conntrack_tcp_timeout_established net.netfilter.nf_conntrack_tcp_timeout_established = 432000
단명 커넥션이 쏟아지는 서버에서 5일은 과하다. 애플리케이션 keepalive 주기를 고려해 하루(86400초) 안팎으로 줄이면 좀비 엔트리가 빠르게 빠진다. TIME_WAIT timeout(nf_conntrack_tcp_timeout_time_wait, 기본 120초)도 30초 수준으로 낮추면 회전이 빨라진다. 두 값 모두 위 3절의 sysctl 파일에 넣어 두면 된다.
다만 timeout을 너무 짧게 잡으면 실제로 살아 있는 저빈도 커넥션이 중간에 추적에서 빠질 수 있다. 끊기면 안 되는 장시간 유휴 세션(예: 일부 DB 커넥션 풀)이 있다면 established timeout을 과하게 줄이지 않는다.
6. 아예 추적하지 않는 선택 - NOTRACK
모든 트래픽을 추적할 필요는 없다. 상태 추적이 의미 없는 트래픽, 예를 들어 외부로 나가는 DNS 질의나 특정 고빈도 포트는 raw 테이블에서 NOTRACK으로 지정해 테이블에 아예 안 올릴 수 있다.
$ sudo iptables -t raw -A PREROUTING -p udp --dport 53 -j NOTRACK $ sudo iptables -t raw -A OUTPUT -p udp --dport 53 -j NOTRACK
엔트리 자체를 만들지 않으므로 포화 압력을 근본적으로 덜어 준다. 단, 해당 트래픽은 stateful 규칙의 보호를 못 받으니 방화벽 정책에 구멍이 생기지 않는지 확인하고 적용한다.
한 걸음 더 나가면, 이 서버가 NAT나 stateful 방화벽을 전혀 안 쓰는데 다른 이유로 nf_conntrack 모듈이 로드돼 테이블을 채우는 경우도 있다. 그럴 땐 모듈 의존성을 확인하고 불필요한 규칙을 걷어내는 쪽이 맞다.
7. 적용 후 확인
튜닝을 반영했으면 다시 count와 max의 간격, 그리고 드롭 통계가 더 안 올라가는지 본다.
$ watch -n 2 'sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max'
count가 max 대비 충분히 여유를 두고 움직이고, conntrack -S의 drop과 insert_failed가 더 이상 증가하지 않으면 안정화된 것이다. 실측 엔트리 수는 트래픽에 따라 환경마다 크게 다르니, 평상시 count의 최고점을 며칠간 관찰해 그 2배 이상을 max로 잡아 두는 것이 안전하다.