서버를 위한 socket 프로그래밍을 할 때, listen API를 사용해 socket에 binding 된 host와 port로 들어오는 커넥션 요청을 받겠다고 선언할 수 있다. listen API의 첫번째 인자는 socketfd
이며, socket의 file descriptor을 의미한다. 두 번째 인자는 backlog
인데, 이번 글에서는 이 backlog 인자가 정확히 어떤 역할을 하는지 알아보자.
리눅스 메뉴얼의 listen API를 보면, backlog 인자에 대해 다음과 같이 정의되어 있다.
The backlog argument defines the maximum length to which the
queue of pending connections for sockfd may grow.
구글 변역기의 힘을 빌려보자면
무슨 말인지 모르겠다 (아직도 먼 기계학습...). 마음가는대로 해석해보면
socketfd의 커넥션 중, 허용 가능한 pending된 커넥션 큐의 최대 길이
정도가 될 것이다.
그러나 정의 만으로도 정확히 이해하기가 힘들었다. 그래서 무식하게 간단한 프로그램을 만들어 실험을 해보기로 결정했다.
필자는 python을 주로 사용해와서, python이 편하기 때문에 python으로 간단한 프로그램을 짜보았다.
클라이언트에서 bytes를 보내면, 그 값을 그대로 돌려주는 간단한 서버 프로그램이다
# echo_server.py
import socket
import sys
if __name__ == '__main__':
host = sys.argv[1]
port = int(sys.argv[2])
num_of_backlog = int(sys.argv[3])
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.bind((host, port))
s.listen(num_of_backlog)
print(f'[INFO] listens to {host}:{port} with backlog {num_of_backlog}')
while True:
conn, addr = s.accept()
with conn:
print(f'[INFO] connected to {addr[0]}:{addr[1]}')
while True:
data = conn.recv(1024)
if not data:
break
print(f'[INFO] received data: {data.decode("utf-8")}')
# echo to client
conn.sendall(data)
finally:
print('[INFO] socket closed')
s.close()
실행은 다음과 같이 하면 된다.
python echo_server.py HOST PORT BACKLOG
Backlog를 테스트 해보기 위해서, BACKLOG
를 인자로 넣었다.
이 프로그램은 입력값을 키보드로 부터 받아, 서버로 넘겨주는 간단한 클라이언트 프로그램이다.
# echo_client.py
import socket
import sys
if __name__ == '__main__':
host = sys.argv[1]
port = int(sys.argv[2])
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.connect((host, port))
print(f'[INFO] connected to {host}:{port}')
while True:
# make prompts
print('>>', end=' ')
# send input data to server
input_data = input()
s.sendall(input_data.encode(encoding='utf-8'))
# receive data from server
received_data = s.recv(1024)
print(f'[INFO] received data: {received_data.decode("utf-8")}')
finally:
print('[INFO] socket closed')
s.close()
실행은 다음과 같이 하면 된다.
python echo_server.py 127.0.0.1 10000 2
로 backlog 값은 2로 주었다.python echo_client.py 127.0.0.1 10000
이다. 각각 1번, 2번, 3번, 4번 클라이언트라 부르자.상기 이미지를 보면, 4개의 클라이언트 프로그램을 실행했고, 실행 순서는 위에서 부터 차례로 실행했다. 3개까지는 커넥션이 잘 맺어졌고, 값을 입력할 수 있는 promps로 넘어간걸 볼 수 있다.
netstat 명령어를 사용해, 실제 연결된 커넥션을 봐보자. netstat -anlt | grep 10000
명령어를 치면 결과가 다음과 같이 나온다.
tcp4 0 0 127.0.0.1.60240 127.0.0.1.10000 SYN_SENT
tcp4 0 0 127.0.0.1.10000 127.0.0.1.60237 ESTABLISHED
tcp4 0 0 127.0.0.1.60237 127.0.0.1.10000 ESTABLISHED
tcp4 0 0 127.0.0.1.10000 127.0.0.1.60236 ESTABLISHED
tcp4 0 0 127.0.0.1.60236 127.0.0.1.10000 ESTABLISHED
tcp4 0 0 127.0.0.1.10000 127.0.0.1.60235 ESTABLISHED
tcp4 0 0 127.0.0.1.60235 127.0.0.1.10000 ESTABLISHED
tcp4 0 0 127.0.0.1.10000 *.* LISTEN
3개의 클라이언트는 TCP connection이 ESTABLISHED
된 상태이고, 하나의 클라이언트는 SYN_SENT
상태로 대기 중이다. 이 클라이언트가 4번 클라이언트인 것을 추측할 수 있다. 60초가 지난 후, 4번 클라이언트는 TimeoutError와 함께 종료된다. (4번 클라이언트는 이미지 캡처에서 아웃 시키겠다.)
[INFO] socket closed
Traceback (most recent call last):
File "echo_client.py", line 11, in <module>
s.connect((host, port))
TimeoutError: [Errno 60] Operation timed out
ESTABLISHED
인 1번, 2번, 3번의 클라이언트에 입력을 해보면, 실제로 작동하는 클라이언트는 1번 클라이언트고, 2번, 3번 클라이언트는 아무런 응답이 없다. 서버의 스레드가 하나여서, 실제로 서버 프로그램과 데이터를 주고 받을 수 있는 클라이언트는 1개인 것이다. 즉, 2번, 3번의 연결 상태는 ESTABLISHED
이미지만, listen 정의에서 언급된 큐에 대기중인 상태인 것을 알 수 있다.
1번 클라이언트를 강제 종료해보자 (Ctrl+C를 주어, KeyboardInterrupt를 발생시켰다). 대기하고 있던 2번 클라이언트가 서버로 부터 응답을 받게 된다.
이를 통해 backlog 정의에서 언급한 queue는 FIFO인것을 확인할 수 있다. (큐가 맞다!)
만약 서버 프로그램이 동시에 응답할 수 있는 수가 3개라 가정하고, backlog 값을 5라 가정하자. 10개의 요청이 차례로 들어왔을 때, 상황을 그림을 그려보면 다음과 같다. (숫자는 요청을 의미하고, 가장 먼저 들어온 요청이 1번이다)
1~3 요청을 보낸 클라이언트는 TCP connection이 established되고, 서버와 데이터를 주고 받을 수 있을 것이다.
listen backlog queue에 있는 4~8번 요청은 TCP connection은 established 되었지만, 서버와 데이터를 주고 받지는 못한다. 1~3번 요청 중 커넥션이 끊어지면, 4번 부터 차례로 데이터를 주고 받을 수 있도록 backlog에서 큐를 이동할 것이다.
tcp max sync backlog라는 곳에 저장된 9, 10번 요청은 SYNC_SENT
상태를 유지하게 된다. 실제로는 유지한다기 보다는 클라이언트에서 지속적으로 SYNC
요청을 시도하고, 서버 쪽에서는 계속 패킷을 drop 시킨다. tcp connection timeout이 값 이상으로 계속 시도하게되면, 클라이언트 쪽에서 커넥션에 대한 timeout을 발생키시고, 연결 시도를 중단한다.
이 backlog는 OS 종류에 따라 없을 수도 있고, 다르게 동작할 수도 있다. 예를 들어 어떤 OS에서는 이 backlog가 존재하지 않고 바로 요청을 refuse 할수도 있다. 이 내용은 listen 매뉴얼에도 잘 나와있다.
net.core.somaxconn
커널 파라미터 값보다 크다면, 실제 적용은 net.core.somaxconn
값이 된다.gunicorn은 python의 WSGI 인터페이스를 지원하는 unix를 위한 http 서버이다. 설정값중에 backlog
라는 값이 있다. gunicorn의 코드를 보았을 때, 이 backlog
값은 실제 listen API의 backlog 값과 동일한 것을 알 수 있었다. 다음 코드는 gunicorn 20.1.0의 gunicorn/sock.py
파일의 내용이다.
그렇다. gunicorn의 backlog 설정은, 말그대로 listen API의 backlog 인자와 동일하다.
사실 이 블로그는 gunicorn의 backlog 값이 어떤 역할을 하는지 궁금해서 시작되었다. 처음에는 gunicorn을 가지고 실험을 해보다, 결국 gunicorn의 코드를 까보게 되었고, listen API를 사용하는 것을 알고, listen API도 무식하게 backlog 인자를 테스트해보면서 여기까지 오게 되었다. 여러 OS 환경(특히 Window)에서 테스트하지 못했다는 점이 아쉽지만, 이 정도 정보면 실무에서 실제로 값을 설정할 때, 내가 뭔짓을 하고 있는지 아는데 부족하지 않을 것이다.
좋은 글 감사합니다~