티스토리 뷰

클러스터를 모니터링하는 서버에서 간혹 ssh 접속이 안되는 문제가 발생할 때가 있다. 모니터링 서버는 주기적으로 클러스터 서버에 접속해서 돌고 있어야하는 프로세스가 잘 돌고 있는지, 커널 로그에 에러 메시지가 남아있는지를 확인하는 동작을 하는데, 간혹 ssh 접속이 실패하면서 서버에 문제가 있다고 리포팅을 하고 있었다.

특이한 점은 특정 호스트만 접속오류를 보이는게 아니라 랜덤하게 ssh connection timed out 메시지를 보이는 호스트 주소가 바뀌는 것이었다. 에러 리포팅 이후 문제를 확인하기 위해 직접 문제의 서버로 ssh 접속을 하면 또 문제없이 접속이 잘 되었다. 귀신을 잡는 느낌으로 문제의 원인을 분석해봤다.

1. nf_conntrack: table full dropping packet 메시지

우선 접속 실패 로그에 찍혀있던 서버에 접속해서 동작하고 있는 프로세스들의 로그와 커널로그를 확인해봤다. 별다른 문제가 없었다. 최근 ssh 접속 실패로그에 찍혀있는 모든 서버에 접속을 해서 로그를 확인해봤지만 별다른 이상은 없었다.

클러스터 서버에 문제가 없다면 모니터링 서버에 문제가 있을 것이라는 생각에 모니터링 스크립트를 분석해봤다. 문제는 없었다. 결국 모니터링 서버의 커널 로그를 열어보기로 했다. 모니터링 서버에서 dmesg를 이용해 커널 로그를 열어봤다. 커널 로그에는 다음과 같은 메시지가 남아있었다.

[2538157.054528] nf_conntrack: table full, dropping packet.
[2538157.166357] nf_conntrack: table full, dropping packet.
[2538157.263534] nf_conntrack: table full, dropping packet.
[2538157.366837] nf_conntrack: table full, dropping packet.
[2538157.467305] nf_conntrack: table full, dropping packet.
[2538157.569270] nf_conntrack: table full, dropping packet.
[2538157.663836] nf_conntrack: table full, dropping packet.
[2538157.765348] nf_conntrack: table full, dropping packet.
[2538157.867338] nf_conntrack: table full, dropping packet.
[2538157.963828] nf_conntrack: table full, dropping packet.
[2538157.9639928] nf_conntrack: table full, dropping packet
[2538157.989528] nf_conntrack: table full, dropping packet
[2538162.214064] __ratelimit: 61 callbacks suppressed

nf_conntrack이라는 커널 모듈에서 관리하는 어떤 테이블의 엔트리가 꽉차서 패킷을 처리할 수 없었고 결국 패킷을 드랍했다는 의미로 보이는 메시지가 남아있었다. 아마 모니터링 서버의 어떤 Limit 값에 다다라서 ssh 접속을 처리할 수 없는 상태가 되어 문제가 발생한 것 같았다. nf_conntrack이라는 커널 모듈이 무엇인지 찾아봤다. 

2. nf_conntrack

nf_conntrack은 ip_conntrack의 후속 커널 모듈이다. netfilter라고 하는 커널 프레임워크가 네트워크 연결에 대한 내용을 기록하고 추적하기 위해 사용하는 모듈이다. 리눅스 커널에는 2.6 버전에서 정식으로 추가되었다.

nf_conntrack 모듈은 일반적으로 활성화되어 있지 않지만 다음의 경우 활성화 된다.

  • iptables -t nat -L 등의 NAT 테이블 확인 명령을 한번이라도 수행한 경우

  • docker 같은 iptables의 NAT 기능이 필요한 어플리케이션을 사용 할 경우

nf_conntrack 모듈의 기본 설정이 적절하게 되어 있어 평소에는 문제를 일으키지는 않지만 가끔 커넥션이 많은 서버나 모니터링 서버에서 문제가 발생하곤한다.

2.1 nf_conntrack table

nf_conntrack 모듈은 네트워크 연결 정보를 해시 테이블로 기록한다.

Connection Tracking Structure (credit : http://people.netfilter.org/pablo/docs/login.pdf)

해시 테이블의 각 해시버킷에 커넥션 트래킹 정보를 기록하는 노드가 이중 연결리스트로 구성이 되어 있다. 위 그림을 참고하면 nf_conntrack 모듈의 구조를 알 수 있다.(출처 :  http://people.netfilter.org/pablo/docs/login.pdf) 이 해시 테이블에 영향을 주는 파라미터로는 해시테이블의 버킷 개수를 지정하는 'nf_conntrack_buckets'과 해시테이블에 저장되는 노드의 최대 개수를 지정하는 'nf_conntrack_max'가 있다.

'nf_conntrack_buckets' 파라미터의 기본값은 16384이고, 'nf_conntrack_max'의 값은 65536인 경우가 많다. 이번 이슈에서 발생한 "nf_conntrack: table full, dropping packet." 에러 메시지는 'nf_conntrack_max' 값을 넘어선 개수의 네트워크 세션이 열렸기 때문에 발생한 것이다. 따라서 'nf_conntrack_max' 값을 손보면 된다. (nf_conntrack과 관련된 다른 파라미터들은 https://www.kernel.org/doc/Documentation/networking/nf_conntrack-sysctl.txt 페이지를 확인해보자.)

2.2 nf_conntrack_max & nf_conntrack_buckets

"nf_conntrack: table full, dropping packet." 에러 메시지는 단순히 'nf_conntrack_max' 파라미터 값을 증가시키는 것으로 해결할 수 있다. 하지만 nf_conntrack 모듈의 해시테이블 크기를 늘리지 않고, nf_conntrack_max 파라미터만 늘리면 원치않는 성능 저하를 발생시킬 수 있다.

이상적으로 해시 테이블은 버킷(Bucket)의 개수가 충분하면 O(1)의 시간 복잡도로 연산이 가능하다. 하지만 그 값이 작아서 Hash Collision이 발생하면 노드들이 버킷에 연결 리스트로 달리기 때문에 O(N/h)의 시간 복잡도의 비효율적인 연산으로 바뀌게 된다. (이 때, N은 노드의 개수, h는 해시 버킷의 개수를 의미)

nf_conntrack 모듈의 경우에도 해시 버킷의 개수를 증가시키기 않은 상태에서 최대 노드값만 늘리게 되면, 해시 버킷의 연결 리스트의 길이가 길어져 네트워크 성능 저하를 발생시킬 수도 있다. 따라서 nf_conntrack_max 값과 함께 nf_conntrack_buckets 값도 같이 조정해주는게 좋다. (물론 경우에 따라서 성능 저하가 큰 문제가 아닌 경우 nf_contrack_max 값만 올려도 되긴한다.) 

해시 버킷의 개수를 늘리는데 걸림돌이 되는 것 중 하나는 메모리 사용량이다. 해시 테이블의 버킷 헤더에 정보가 많다면 해시 테이블의 버킷 개수를 늘리는게 평소 메모리 사용량을 올려 부담스러워 질 수도 있다. 커널 소스를 잠깐 여러보자. (nf_conntrack_core.c 파일 참조)

void *nf_ct_alloc_hashtable(unsigned int *sizep, int nulls)
{
	struct hlist_nulls_head *hash;
	unsigned int nr_slots, i;

	if (*sizep > (UINT_MAX / sizeof(struct hlist_nulls_head)))
		return NULL;

	BUILD_BUG_ON(sizeof(struct hlist_nulls_head) != sizeof(struct hlist_head));

	nr_slots = *sizep = roundup(*sizep, PAGE_SIZE / sizeof(struct hlist_nulls_head));

	hash = kvmalloc_array(nr_slots, sizeof(struct hlist_nulls_head), GFP_KERNEL | __GFP_ZERO);

	if (hash && nulls)
		for (i = 0; i < nr_slots; i++)
			INIT_HLIST_NULLS_HEAD(&hash[i], i);
	
    return hash;
}

해시 테이블을 할당하는 함수다. 여기에서 해시 테이블은 다음 구조체(해시 버킷)의 배열이다.

struct hlist_nulls_head {
	struct hlist_nulls_node *first;
};

포인터를 하나 들고 있으니 64비트 아키텍처에서 해시버킷 하나당 8바이트를 소모한다. 'nf_conntrack_buckets'의 기본 값인 16384개의 해시 버킷을 유지하기위해 128kb의 메모리를 사용한다. 이를 64만개로 늘리면 약 5MB 정도의 메모리를 해시 버킷을 만드는데 사용하게 된다. 

nf_conntrack 해시 테이블에 기록되는 노드의 개수를 생각해보자. 하나의 커넥션 정보를 기록하는데 304바이트가 필요하다고 한다. (실제 빌드해서 찍어본건 아니고 웹 문서에 그렇게 나와있다. 실제로는 커널 내부에서 사용하는 SLAB Allocator에서 할당받은 SLAB의 내부 단편화와 페이지 사이즈, 커널 버전에 따라 약간 다를 수 있다.)

여튼 하나의 네트워크 세션당 304바이트의 메모리를 사용한다고 했을 때, nf_conntrack_max 의 기본 값인 65536개의 네트워크 커넥션 정보를 유지하려면 총 19MB의 메모리를 사용하게 된다. nf_conntrack_max 값을 1,000,000으로 늘리면 최대 289.92MB 가량을 커넥션 정보를 기록하는데 사용하게 된다. 여기에 해시 버킷으로 사용된 메모리의 용량까지 같이 계산해주면 된다.

원하는 커넥션 개수에 따라 메모리 사용량을 계산해보고, 부담스럽지 않은 선으로 올리면 되겠다. 만약 운영에 문제가 있다면 커넥션을 여러 서버로 분산시키는 등의 다른 방법들을 찾아봐야 한다.

3. nf_conntrack 모듈 모니터링

그렇다면 현재 접속중인 서버의 nf_conntrack 모듈의 상태를 모니터링해보자.

3.1 nf_conntrack 모듈 확인

우선 nf_conntrack 모듈이 활성화되어 있는지 확인해보는 명령어는 다음과 같다.

cat /proc/modules | grep nf_conntrack

/proc/modules 파일을 조회해보면 현재 활성화된 리눅스 모듈들의 정보를 확인할 수 있다. 그 중에서 nf_conntrack과 과련된 내용을 찾아보면 된다.

3.2 nf_conntrack_max 값 확인

nf_conntrack 모듈이 활성화되어 있다면 nf_conntrack_max 값을 확인해보자.

cat /proc/sys/net/nf_conntrack_max

 

nf_conntrack_buckets 값을 조회해보자.

cat /proc/sys/net/netfilter/nf_conntrack_buckets

nf_conntrack_buckets의 기본 값은  32 ~ 16384 사이 숫자가 할당된다.

3.3 nf_conntrack_count - 현재 기록중인 노드의 개수

그렇다면 현재 nf_conntrack 모듈이 기록중인 네트워크 커넥션의 개수를 확인해보자.

cat /proc/sys/net/netfilter/nf_conntrack_count

문제가 발생했다면 이 값이 순간적으로 nf_conntrack_max 값으로 올라가면서 커널 로그에 packet drop 메시지가 찍힐 것이다.

watch 명령으로 지속적인 모니터링도 가능하다.

watch -d cat /proc/sys/net/netfilter/nf_conntrack_count

4. 해결방안

4.1 nf_conntrack 모듈 언로드

nf_conntrack 모듈을 쓰지 않아도 되는 서버에서는 nf_conntrack 모듈을 언로드해버려서 문제를 해결할 수도 있다. 

우선 iptables를 멈춘다.

iptables stop

그리고 iptable rule 중에 state 구문이 들어가는 rule을 삭제한다.

그런 다음 다음 명령을 실행해서 nf_conntrack 모듈을 제거한다.

rmmod nf_conntrack

4.2 nf_conntrack_max, nf_conntrack_buckets 파라미터 조정

모듈을 언로드해도되는지 잘 모르겠거나 엮여 있는 모듈이 많은 경우 nf_conntrack_max와 nf_conntrack_buckets 파라미터를 조정하는 방법을 택해야한다.

우선 다음 파일을 생성한다.

/etc/modprobe.d/nf_conntrack.conf

그리고 다음 옵션을 추가한다.

options nf_conntrack hashsize={newValue}

newValue에 수정할 값을 변경하면 된다.

여기서는 해시 테이블의 사이즈(버킷의 개수)를 조정하여 nf_conntrack_max 값을 조정하도록 한다. nf_conntrack_max 값은 별도로 지정하지 않으면 nf_conntrack_max 값의 8배로 지정된다. nf_conntrack_buckets 값을 늘려 nf_conntrack_max 값을 증가시키는 방법이 해시 Collision으로 인한 모듈 성능 저하를 피할 수 있기 때문에 권장되는 방식이다.

새로운 값을 지정했으면 netfilter를 사용하는 iptables 같은 모듈을 재시작한다.

우분투의 경우 ufw를 재시작한다. (Ubuntu FireWall)

systemctl stop ufw
modprobe -rv nf_conntrack
systemctl start ufw

 

CentOS의 경우 다음 명령을 실행한다.

systemctl stop iptables
modprobe -rv nf_conntrack
systemctl start iptables

만약 modprob 명령에서 modprobe: FATAL: Module nf_Conntrack is in use 에러가 발생한다면, nf_conntract 모듈을 사용하는 다른 모듈이 있는지 확인한 다음 언로드하고 진행하면 된다.

4.3 적당한 값은?

nf_conntrack_max 값을 구하는 공식은 웹 페이지에서 어렵지 않게 찾아 볼 수 있다. 과거 ip_conntrack 모듈 시절에 계산법이 있는데 다음과 같다.

nf_conntrack_max = (MemorySize) / (16384 * (architecture bit / 32 ))
nf_conntrack_buckets = nf_conntrack_max / 8

 

적당한 값에 대한 근사치일 뿐, 실제 서버의 운영 환경에 따라 다양한 값으로 설정해 사용해도 무방하다. 특히 메모리를 많이 쓰는 서버인지, 커맨드 서버인지 등에 따라 적당한 값을 조정해가면서 사용하기 바란다.

 

Reference

댓글
댓글쓰기 폼
공지사항
최근에 달린 댓글
Total
531
Today
3
Yesterday
11
링크
«   2020/01   »
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31  
글 보관함