Linux Network Internal
지난번 글에 이어서.. 오늘 다루고자 하는 내용은 syn_backlog, somaxconn과 관련된 내용입니다. 이 값들이 어떤 역할을 하며 값을 변경할 때 서버에 어떤 영향을 끼치는지 살펴보겠습니다.
혹시 잘못 정리된 내용이 있다면 댓글로 꼭 알려 주세요.
바쁘신 분들을 위해 요약하자면,
syn_backlog, somaxconn 파라미터는 다른 파라미터들과는 달리 변경 즉시 서버에 영향을 끼치지 않으며 실제 SYN Backlog 의 크기는 애플리케이션에서 Listen() 시스템 콜을 할 때 결정됩니다.
그럼 천천히 살펴보겠습니다. 좀 깁니다. ^^;;
다시 한 번 기억을 떠올려 보겠습니다. 모든 TCP 커넥션은 3-way handshake로 이루어지며, 맨 처음 클라이언트로부터 SYN 패킷을 받으면 해당 커넥션은 half-open 상태라 불리는 SYN_RECV 상태가 됩니다. 그리고 syn backlog에 해당 커넥션에 대한 정보가 저장됩니다.
이 과정에서 클라이언트로부터 ACK 패킷을 받지 못해 SYN_RECV 상태의 세션 정보들이 쌓여서 SYN Backlog 가 가득 차게 되면 SYN Drop 이 발생하게 되고 이게 SYN Flood 공격이라고 했었습니다.
그럼 이번엔 테스트를 통해 서버에 SYN_RECV 상태의 소켓들을 만들어 보겠습니다.
클라이언트와 서버를 준비하고 서버에서는 nginx를 이용해 80 포트를 열어 두겠습니다. 그리고 클라이언트에서는 hping3 툴을 이용해서 SYN Flood 공격을 재현합니다.
클라이언트에서 hping3는 아래와 같이 입력합니다.
hping3 -c 100 --flood -d 120 -S -w 64 -p 80 --rand-source 서버 IP주소
명령어를 입력한 후 서버에서 netstat을 통해 확인해 보면 SYN_RECV 소켓들이 생성되어 있는 것을 볼 수 있습니다.
--rand-source 명령어 때문에 서버에서는 다양한 IP에서 SYN이 들어오는 것으로 확인이 됩니다. grep 명령을 통해서 SYN_RECV 소켓이 몇 개인지 확인해 볼까요?
네.. 그렇습니다. 이상하죠? 512개.. synbacklog 파라미터는 몇 개로 되어 있을까요?
30000이라는 상당히 큰 사이즈로 설정이 되어 있는데도 불구하고 nginx의 80 포트는 512개까지 밖에 저장을 못하고 있습니다. 그럼.. syn_backlog 값은 무의미한 걸까요??
테스트에서 알아 봤듯이 syn_backlog 값이 설정되어 있음에도 불구하고 nginx는 SYN_RECV 소켓 개수를 512개까지 밖에 저장할 수 없었습니다. 왜 이런 일이 발생하게 되는 걸까요? 그럼 syn_backlog 값은 왜 있는 걸까요?
결론부터 말씀드리자면,
syn_backlog 값은 설정시마다 모든 포트에 적용되는 dynamic 한 값이 아닙니다.
대부분의 커널 파라미터들은 적용 즉시 효과가 나타나는 dynamic 값이지만, syn_backlog 값은 변경한다고 해도 즉시 효과가 나타나지 않습니다.
이 과정을 이해하기 위해 커널 소스 코드의 일부인 tcp_v4_conn_request 함수를 살펴보겠습니다. 이 함수는 모든 TCP 커넥션이 맺어질 때 호출되는 함수입니다.
빨갛게 네모 표시된 저 부분이 SYN Backlog 가 가득 찼는지를 확인하는 부분입니다. 가득 차 있는 상태에서 tcp_syncookies 값이 Enable 되어 있으면 want_cookie라는 변수를 세팅하고 진행합니다. 그럼 저 함수를 따라가 볼까요?
inet_csk_reqsk_queue_is_full 은 reqsk_queue_is_full 함수를 호출합니다. 그대로 따라가 보겠습니다.
인자로 받은 queue 구조체의 listen_opt-> qlen 값을 max_qlen_log 값만큼 뒤로 shift 연산을 합니다. qlen은 현재 backlog queue의 크기, max_qlen_log는 최대 backlog queue를 2의 제곱수로 표현한 수입니다. 따라서 qlen이 512, max_qlen_log가 10 (=1024) 라면 512 값을 오른쪽으로 10 bit shift 하게 되고 결국 0을 리턴 하게 됩니다. 즉 full 이 되지 않았다는 의미가 되겠죠.
그럼 여기서 핵심은 바로 max_qlen_log 값이 됩니다. 과연 어떻게 설정되는지 한 번 살펴볼까요?
max_qlen_log는 include/net/request_sock.h 파일에 listen_sock이라는 구조체에서 찾아볼 수 있습니다.
Listen 상태에 있는 소켓과 관련된 구조체 이기 때문에, Listen 시스템 콜을 살펴보겠습니다.
Listen 시스템 콜을 호출할 때 넘겨주는 인자 중 backlog 인자에 관한 처리 과정이 나타나 있습니다. 이 곳에서 somaxconn의 의미를 찾을 수 있는데요, Listen 상태의 소켓의 Listen Queue의 크기를 지정할 때 사용하게 됩니다. 그래서 backlog 값이 somaxconn 보다 커지게 된다면 somaxconn 의 크기로 지정이 됩니다. Listen 큐는 지난 글에서도 언급했던 것처럼 클라이언트로부터 ACK을 받은 후 accept() 시스템 콜이 불려지기 전까지 대기하는 큐입니다.
핵심은 sock-> ops-> listen 함수인데요, 이 함수가 어떤 역할을 하는지 살펴보겠습니다. 이 함수는 실제로는 inet_listen 함수로 연결이 됩니다.
inet_listen 함수를 찾아가 보면, 아래와 같이 정의되어 있는 것을 볼 수 있습니다.
생각보다 여정이 길어지는데요.. ^^;; 이제 거의 다 왔습니다. 아주 핵심적인 함수가 보입니다. inet_csk_listen_start 함수입니다.
이제 마지막으로 이 함수에서 호출하는 reqsk_queue_alloc 함수를 살펴보겠습니다.
초반 nr_table_entries 값을 가지고 연산하는 부분을 주목하시면 됩니다. min_t 값은 두 개의 넘어온 값 중에 작은 값을 사용하게 되는데요, 함수를 따라와 보셔서 아시겠지만 인자로 받은 nr_table_entries 값은 Listen() 호출 시 넘어온 backlog 값이 됩니다. 즉, backlog 값과 max_syn_backlog 값 중 작은 값을 사용하게 됩니다. 그리고, 그 다음 max_t 함수를 통해 큰 값을 사용하게 되기 때문에 backlog 값이 max_syn_backlog 값보다 작다면 결과적으로 여기서 얻게 되는 최종 값은 Listen() 호출 시 넘어온 backlog 값이 되게 됩니다. roundup_pow_of_tow 함수는 인자로 받은 수에서 반올림된 2의 제곱수를 리턴으로 주기 때문에, backlog 값이 511이라면, 511 + 1 = 512 가 됩니다. 이후 for 문을 만나 nr_table_entries 값인 512까지 lopt-> max_qlen_log는 왼쪽으로 1비트씩 쉬프트 되기 때문에 결과적으로 9라는 값을 가지게 됩니다.
위 여정을 통해 확인한 결과들을 종합해 보면, Listen() 시스템 콜 호출 수 받게 되는 backlog 의 값과 somaxconn, syn_backlog 값들이 syn backlog의 크기를 결정하는데 큰 영향을 끼치는 것을 알 수 있습니다. 결과적으로 세 값 중 가장 작은 값 + 1의 값을 2의 제곱수로 올림 해서 결정이 됩니다.
간단한 테스트를 통해 확인해 보겠습니다. 서버에서 somaxconn을 128로 수정하고 nginx를 재시작해 줍니다. 그리고 똑같이 클라이언트에서 hping3를 통해서 syn flooding을 일으킨 후의 SYN_RECV 개수를 세어 봅니다.
256개가 됩니다. somaxconn이 128이기 때문에 nginx의 기본 backlog 값인 511이 무시가 되었고요, 이후 reqsk_queue_alloc 함수까지 쭉 로직이 넘어 오면서 128 + 1 = 129이고, 129를 올림 해서 가장 가까운 2의 제곱수는 256이 되기 때문에 syn backlog queue의 최댓값이 256으로 설정이 됩니다.
이번엔 somaxconn을 늘리고 nginx에서 backlog 값을 수정해서 올려 보겠습니다.
그리고 somaxconn 값은 10000 정도로 수정합니다. 그리고 다시 테스트를 하면 SYN_RECV가 1024개 인 것을 볼 수 있습니다.
소켓의 SYN Backlog 크기는 이미 Listen() 시스템 콜을 호출하는 과정에서 결정이 되기 때문에 syn_backlog와 somaxconn 값을 수정하는 것은 현재 열려있는 포트의 SYN Backlog 크기에는 아무런 영향을 주지 않습니다.
그렇기 때문에, syn_backlog, somaxconn 값은 충분히 크게 주고, 애플리케이션에서 backlog 값을 수정해 가면서 재시작을 해주어야 효과를 볼 수 있습니다.
한 개의 SYN_RECV 소켓은 약 80 bytes 정도 되기 때문에 1000개의 SYN_RECV 소켓을 유지하고 있어도 약 78 Mbyes 정도밖에 차지하지 않으며, 요즘과 같이 대용량의 메모리를 사용하는 시대에는 성능에 큰 영향을 끼칠만한 양은 아닙니다. (물론 어느 정도로 SYN Backlog 크기를 유지하느냐는 서비스 성격에 따른 선택의 문제입니다.)
반드시 기억하셔야 할 것은 syn_backlog, somaxconn 값은 SYN Backlog 크기를 결정하는데 영향을 끼치지만, 설정된 값으로 SYN Backlog 크기가 바로 변경되는 것은 아니며 Listen() 시스템 콜을 호출하는 순간 인자로 받은 backlog 값으로 크기가 결정된다 라는 것입니다. SYN Backlog 크기에 문제가 있어서 성능 저하가 발생한다면 커널 파라미터 수정 후 애플리케이션도 재시작해 주어야 합니다.
긴 글 읽어 주셔서 감사합니다. ^^
이번 글을 작성하는 동안 http://www.piao2010.com/linux%E8%AF%A1%E5%BC%82%E7%9A%84%E5%8D%8A%E8%BF%9E%E6%8E%A5syn_recv%E9%98%9F%E5%88%97%E9%95%BF%E5%BA%A6%E4%B8%80 에서 많은 도움을 받았습니다.