개요
커널을 수정하면 기존 OS의 임계점을 변경하는 것이 가능하다. 서버 상에서 최적의 효율을 끌어낼 수 있는 환경을 구축할 수 있게 되는 것이다(어떤 설정은 신중하게 선택해야 하겠지만). 본 내용은 철저하게 웹 서버의 관점에서 작성되었으며, 모든 글이 철저히 주관적인 관점에서 쓰였음을 염두에 두어야 한다!
무엇을 할 수 있을까?
TCP 요청을 위한 튜닝
웹 서버를 위해 사용되는 환경이므로 TCP를 위한 환경 설정 일부는 제법 좋은 선택이 되어줄 것입다.
TCP의 수락 대기열 큐 크기나 파일 연결 수 등을 늘리면 커넥션 상에서 발생하는 에러를 줄일 수 있을 것.
시스템 세션을 위한 튜닝
커널 세션과 파일 시스템 세션을 위한 주요 항목을 통해 공유 메모리나 메시지 큐의 크기를 결정하고, 패닉 시 재부팅 등의 기능을 수행할 수 있을 것이다.
그 외에도
캐시, 덤프, 세마포어 및 세그먼트 등 시스템적인 요소들을 변경하는 것이 가능하다. 보안 등을 위한 설정도 가능하다.
환경 구성하기
TCP 및 통신
TIME_WAIT 소켓 수 제한
sysctl net.ipv4.tcp_max_tw_buckets
net.ipv4.tcp_max_tw_buckets = 262144
사실 TIME_WAIT은 자연스러운 현상으로 오히려 이 수가 작은 건 그다지 좋은 형태는 아닐 것이다. 얼마를 설정하든 65536개를 넘을 수는 없다(총 포트의 개수로 사실, 실 사용은 이보다도 더 적을 것이다). TCP 소켓의 안전하고 우아한(흠..) 셧다운을 위해 적당히 상향시켜주면 좋다고 한다...🤨️
근데 내 건 추천 상향 내역(180000)보다 훨씬 더 높길래 디폴트로 설정되어 있길래 그냥 냅두기로 했다.
TIME_WAIT 소켓 재활용
$ sysctl net.ipv4.tcp_tw_reuse
net.ipv4.tcp_tw_reuse = 2
다음의 명령어를 통해 활성화한다.
사용 가능한 소켓인지 확인하는 로직상에 TCP_TIMESTAMP 기능을 활용하므로 이또한 활성화해야한다.
$ sysctl -w ipv4.tcp_timestamps="1"
$ sysctl -w net.ipv4.tcp_tw_reuse="1"
그러나 이는 통신 양측 모두에서 TCP_TIMESTAMP 옵션이 있을 때만 가능하다. (근데 요샌 그냥 활성화되어있는 것 같긴 함)
그냥 TIME_WAIT 상태에 있을 수 있는 시간 자체를 줄여버리는 방법도 있지만, 이렇게 되면 커넥션 상태일 때 SYN이 드랍되면서 일부 패킷이 유실되는 경우가 생길 수 있다고 하므로 레퍼런스에서도 제안한 대로 설정하지는 않겠다.
개별 소켓에 대한 크기 상향
우선 소켓당 버퍼 크기에 대한 커널 파라미터는 다음과 같다.
$ sysctl net.core.rmem_default
net.core.rmem_default = 212992
$ sysctl net.core.wmem_default
net.core.wmem_default = 212992
$ sysctl net.core.rmem_max
net.core.rmem_max = 212992
$ sysctl net.core.wmem_max
net.core.wmem_max = 212992
$ sysctl net.ipv4.tcp_rmem
net.ipv4.tcp_rmem = 4096 131072 6291456
$ sysctl net.ipv4.tcp_wmem
net.ipv4.tcp_wmem = 4096 16384 4194304
# 4096은 4kb이다.
r은 read(즉, receive buffer), w는 write(즉, send buffer)에 해당한다.
또한 마지막 두 설정은 ipv6에도 적용이 된다(이는 일부 ipv4 커널 파라미터가 ipv6까지 확장되어 적용되는 경우가 있기 때문이다 → net.ipv4.tcp , net.ipv4.icmp , etc......
각 커널 파라미터가 세 정수로 구성되어 있음을 볼 수 있는데, 이는 각각 min / default / max 값을 뜻한다.
수틀리면 밀어버린다는 마인드로 철저하게 레퍼런스를 준수해 설정하겠다.
$ sysctl -w net.core.rmem_default="253952"
$ sysctl -w net.core.wmem_default="253952"
$ sysctl -w net.core.rmem_max="16777216"
$ sysctl -w net.core.wmem_max="16777216"
$ sysctl -w net.ipv4.tcp_rmem="253952 253952 16777216"
$ sysctl -w net.ipv4.tcp_wmem="253952 253952 16777216"
이렇게 한다고 실제로 sender buffer가 데이터를 네트워크로 보낼 수 있게 되는 것은 아니다.
네트워크는 연결된 모든 노드를 공유하는 공유 자원이므로 개개 노드가 이를 독식해버리는 것은 네트워크 마비의 우려가 있기 때문이다.
때문에 각 노드는 일반적으로 Congestion Avoidance Algorithm을 사용해 보낼 데이터 양을 자체적으로 조정한다. 리눅스에서는 reno, vegas, bic, cubic 등의 Congestion Avoidance Algorithm을 사용할 수 있는데, 최근 리눅스의 대부분에는 cubic이 기본적으로 설치되어 있다.
ss명령어를 사용하면 각 소켓별로 현재의 congestion window 크기를 확인하는 것이 가능하다.
$ ss -n -i
...
tcp ESTAB 0 0 192.168.0.158:56516 3.233.54.64:443
cubic wscale:12,7 rto:408 rtt:197.859/1.011 ato:40 mss:1448 pmtu:1500 rcvmss:1448 advmss:1448 cwnd:10 bytes_sent:19385 bytes_acked:19386 bytes_received:5425 segs_out:98 segs_in:80 data_segs_out:37 data_segs_in:21 send 585467bps lastsnd:995220 lastrcv:995020 lastack:26240 pacing_rate 1170928bps delivery_rate 292888bps delivered:38 app_limited busy:2552ms rcv_space:14480 rcv_ssthresh:64088 minrtt:197.108
이 Congestion Window Size는 TCP 혼잡 제어 전략에 따라 slow start 방법으로 최초 연결시 정해진 Initial Congestion Window Size(CWND)부터 어느 정도 선까지 지속적으로 증가하게 된다.
그리고 통신이 지속적으로 진행되면서 receiver로 부터 ACK 패킷을 받으면, congestion window size를 현재 크기의 2배만큼씩 증가시킨다.
도중에 패킷이 유실되는 등의 케이스가 생긴다면 Congestion Window Size를 감소시키는데, 이는 Congestion Avoidance Algorithm에 따라 경감되는 사이즈가 달라진다.
아무튼, Initial Congestion Window Size를 변경하거나 확인하려면 ip route 명령어를 쓰면 된다.
$ ip route show
default via 192.168.0.1 dev enp3s0 proto dhcp metric 100
169.254.0.0/16 dev enp3s0 scope link metric 1000
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown
192.168.0.0/24 dev enp3s0 proto kernel scope link src 192.168.0.158 metric 100
192.168.49.0/24 dev br-a5b94e9ce78d proto kernel scope link src 192.168.49.1 linkdow
근데 뭔가 이상한게 디폴트도 kernel이 아니라 dhcp로 잡혀있기도 하고...실제로 명령어를 입력해도 이상한 명령어가 뜨는게 뭔가 석연찮기도 하고 문서 자체도 7년이나 된 거다보니 일단 이 설정까지는 만지지 않고 그냥 두도록 하겠다.
$ sudo ip route change default via 192.168.1.1 dev enp3s0 proto kernel initcwnd 10
Error: Nexthop has invalid gateway.
파일 / 소켓 최대 오픈 개수 관리
$ sysctl fs.file-max
일반적인 리눅스와 유닉스 환경에서 소켓과 파일은 같은 취급을 받는다.
전체 시스템에서 가질 수 있는 파일 개수의 제한이 소켓에도 영향을 준다는 것이다.
그래서 사용자에 대한 관리를 하는 것이 맞다.
이건 여기(커널 파라미터 변경)에서 확인할 수 있으니 여기에서 재언급하지는 않겠다.
유실되는 패킷 방지를 위한 backlog 설정
네트워크 패킷 내부의 처리 과정을 파이프로 보고, 처리 과정 앞에 queue가 존재한다고 하자.
갑자기 처리량이 급증할 때 이 queue의 크기가 패킷의 양보다 작다면 넘치는 패킷들은 처리되지 않고 버려진다. 이렇게 되면 애플리케이션 측에서는 버려지는 패킷은 알 수 없다.
때문에 대규모 처리를 위해서는 어느 정도의 in-bound-queue를 증가시킬 필요가 있다.
다음의 명령어로 확인이 가능하다.
$ sysctl net.core.netdev_max_backlog
net.core.netdev_max_backlog = 1000
다음의 명령어로 증가시킬 수 있다.
레퍼런스에 나온대로 증가시켜주자. 고려할 사항은 메모리 정도이므로 괜찮을 것이라고 본다(아마도)...
$ sysctl -w net.core.netdev_max_backlog="30000"
listen backlog 개념도 있는데, 이건 클라이언트가 서버에 연결될 때에 수락되지 않은 클라이언트들이 대기할 수 있는 최대량을 의미한다. 다음의 명령어로 확인할 수 있다.
$ sysctl net.core.somaxconn
net.core.somaxconn = 4096
웹 서버의 backlog 기본값이 이보다 더 높다고 하더라도 이 커널 파라미터는 hard limit이기 때문에 실제로는 커널 파라미터의 값을 준수하게 된다고 한다.
비슷한 경우로 netdev_max_backlog가 있는데, 커널은 패킷을 받으면 ring buffer라고 하는 큐에 쌓아놓는데, 이 값이 작으면 패킷이 드랍될 수 있으므로 이 값도 키워주는 게 좋다고 한다.
근데 마찬가지로 레퍼런스에서 제시한 상향값보다 이미 6배나 높아서 일단 내버려뒀음...
$ sysctl net.core.netdev_max_backlog
net.core.netdev_max_backlog = 30000
손실된 패킷은 softnet_stat 파일에서 확인할 수 있다고 한다.
TCP 세션에서 불필요한 자원낭비 막기
sysctl net.ipv4.tcp_syncookies
net.ipv4.tcp_syncookies = 1
이 값이 1이면 더 이상 TCP 세션 시에 정상적인 ACK 패킷이 오지 않는다고 재연결을 하는 불필요한 낭비를 막을 수 있다고 한다.
근데 이미 1이긴 함
다만 이 값이 1이 되면 발생하는 문제는 클라이언트에서 오는 ACK 패킷의 손실인데, 첫번째 요청의 ACK 패킷이 손실된 상태에서 두번째, 세번째 요청이 오면 첫번째 패킷은 서버가 인식하지 못하고 있을 수 있기 때문에 문제가 발생한다.
비슷한 기능으로 클라이언트가 서버측으로 SYN 패킷을 보냈을 때 응답값으로 SYN+ACK가 오지 않으면 재시도하는 숫자도 정할 수 있다. 다음 명령어로 확인 가능하다.
일반적으로 retry는 처음 대기시간이 exponential backoff로 설정한다. 기본값 5일때, 타임아웃의 기준은 1, 3, 7, 15, 31 초에 한번씩 요청을 보내고 최대 대기 시간은 63초이다. 그 시간 동안 5번의 retry를 했기 때문이다(즉, 총 요청은 6번이다).
연결을 대기할 때 실제로 그 대기 시간이 63초가 넘으면 타임아웃으로 이어진다.
$ sysctl net.ipv4.tcp_synack_retries
net.ipv4.tcp_synack_retries = 5
SYN+ACK 같은 TCP 헤더는 7월에 1~4계층 때 한 번 했었다.
소켓의 포트 선점 범위 확장하기
프록시 서버는 클라이언트에게서 요청을 받아 다른 백엔드 서버에 전달하는 방식으로 동작하는데, 이를 위해서는 다른 백엔드 서버에 연결하기 위한 클라이언트 소켓을 필요로 한다. 이 소켓은 지정된 범위 내에서 사용되지 않는 포트를 할당하는 방식으로 동작한다. 다음의 명령어로 그 범위를 확인할 수 있다.
$ sysctl net.ipv4.ip_local_port_range
net.ipv4.ip_local_port_range = 32768 60999
최대한 넓은 포트 범위를 위해서는 다음과 같이 설정하면 된다.
웰노운 포트를 제외하고 선정해야하므로 1023번까지는 지정하지 않는 모양이다?
$ sysctl -w net.ipv4.ip_local_port_range="1024 65535"
keep-alive 설정
TCP 연결 상태가 되었을 때, keepalive로 상대편이 살아있는지를 확인한다(매 연결, 즉 3-way handshake 는 성능 저하로 이어지기 때문이다).
관련 설정이 몇 가지 있는데, 별도의 지침 없이 명시만 되어있어서 참고만 하기로 했다.
$ sysctl net.ipv4.tcp_keepalive_time # 얼마나 자주 keepalive 메시지를 보낼 것인가?
net.ipv4.tcp_keepalive_time = 7200 # default 2시간
$ sysctl net.ipv4.tcp_keepalive_probes # 연결이 끊겼다고 판단하기 위한 메시지는 몇 번 보낼 것인가?
net.ipv4.tcp_keepalive_probes = 9 # default 9번
$ sysctl net.ipv4.tcp_keepalive_intvl # 위 메시지를 보내는 간격은 몇 초로 할 것인가?
net.ipv4.tcp_keepalive_intvl = 75 # default 75초
커널
공유 메모리 최대값
$ sysctl kernel.shmmax
kernel.shmmax = 18446744073692774399
세마포어 값
배열당 최대 세마포어 수 / 최대 세마포어 시스템 넓이 / SEMOP?? 호출당 최대 OP수 / 최대 배열수
$ sysctl kernel.sem
kernel.sem = 32000 1024000000 500 32000
메시지 큐 값
메시지 큐 시스템 크기
sysctl kernel.msgmni
kernel.msgmni = 32000
보안
이긴 한데 이걸 설정하는게 맞을까? 싶어서 일단 두기로 했다.
ip주소를 스푸핑한다고 예상되는 경우를 로그에 기록
$ sysctl net.ipv4.conf.all.log_martians
net.ipv4.conf.all.log_martians = 0
$ sudo sysctl -w net.ipv4.conf.all.log_martians=1
net.ipv4.conf.all.log_martians = 1
DoS 공격 source IP 로 사용되는 것을 차단(IP 스푸핑 방지하기)
sysctl net.ipv4.conf.default.send_redirects
net.ipv4.conf.default.send_redirects = 1
Source Route 패킷허용 막기(신뢰받는 호스트로 위장하는 것 막기)
sysctl net.ipv4.conf.default.accept_source_route
net.ipv4.conf.default.accept_source_route = 1
# ICMP redirect 의 변조된 패킷 차단(accept 패킷 차단설정)
[root@localhost /]# cat /proc/sys/net/ipv4/conf/default/accept_redirects // 기본값 1 -> 0 설정
1
# ICMP redirect 의 변조된 패킷 차단(send 패킷 차단설정)
[root@localhost /]# cat /proc/sys/net/ipv4/conf/default/send_redirects // 기본값 1 -> 0 설정
1
# DoS 공격 source IP 로 사용되는 것을 차단(IP 스푸핑 방지하기)
[root@localhost /]# cat /proc/sys/net/ipv4/conf/default/rp_filter // 기본값 0 -> 1 설정
1
참고 자료
리눅스 서버의 TCP 네트워크 성능을 결정짓는 커널 파라미터 이야기 - 1편 : NHN Cloud Meetup
리눅스 서버의 TCP 네트워크 성능을 결정짓는 커널 파라미터 이야기 - 2편 : NHN Cloud Meetup
리눅스 서버의 TCP 네트워크 성능을 결정짓는 커널 파라미터 이야기 - 3편 : NHN Cloud Meetup
Kernel Parameter (TCP Parameter 정복하기)
웹 서버등 네트워크 서버를 위한 리눅스 커널 튜닝(linux kernel tuning for network server)