brunch

You can make anything
by writing

C.S.Lewis

by doz Nov 16. 2020

6. 웹 서비스의 성능 극대화

클라이언트와 서버 구간에서..

지금까지 배운 것들 모두 사실 웹 서비스의 성능 향상을 위한 것이라고 봐도 무방하다. 이러한 내용을 다시한번 버무려서 웹 서비스 성능에 영향을 미치는 요소를 파악해보자.



CPU 성능을 최대한 활용하자


특정 서비스를 제공해주는 WAS(Web Application Server)가 있다고 하자. 그리고 서버의 환경은 멀티프로세서 환경이라고 하자.

그런데 이 was 가 동작 방식이 싱글 스레드라면 ?

CPU 코어는 남아도는데 하나의 코어에서만 열심히 프로그램을 돌리며, 서버는 한번에 하나의 요청만 처리할 것이다. 즉 첫번째 요청에 대한 작업이 끝나기 전에 두번째 요청이 들어온다면 앞선 작업이 끝날때까지 대기하게 된다. 만약 서버가 훨씬 더 많은 요청을 받게 된다면, 처리는 점점 더 늦어지게 된다. 나중에 들어온 요청은 앞선 요청보다 더 빠르게 처리 될 수 있더라도 긴 시간을 기다려야 할 것이다.


그러므로, 멀티 스레드 또는 멀티 프로세스 환경으로 변화시킴으로써 스케쥴링을 통한 CPU 코어를 전체적으로 사용하게 하는 것이 가장 좋다. 


이렇게 was가 CPU 리소스를 최대로 사용할 수 있도록 다수의 워커를 통해 서비스 할 수 있도록 설정해야 한다. 대부분의 경우 옵션을 통해서 워커의 수를 조절할 수 있으며, 최소한 CPU 코어 수와 같은 수의 값을 설정해서 사용한다.


네트워크 소켓을 최적화 하자


우선 현재 운영체제의 소켓 상태를 모니터링해봐야 한다.

netstat과 ss 명령어를 사용해서 확인해보자. (ss는 s 옵션을 사용하면 요약된다 summary)

[현재 서버는 ELK가 구축된 서버이며, HAProxy와 logstash, ELK가 있다]

[centos@ip-172-31-27-12 ~]$ ss -s

Total: 928 (kernel 0)

TCP:   1237 (estab 3, closed 1179, orphaned 0, synrecv 0, timewait 1179/0), ports 1009

Transport Total     IP        IPv6

*  0         -         -        

RAW  0         0         0        

UDP  7         4         3        

TCP  58        26        32       

INET  65        30        35       

FRAG  0         0         0        


[centos@ip-172-31-27-12 ~]$ netstat -napo | grep -i time_wait

tcp        0      0 172.24.0.1:46850        172.24.0.2:5044         TIME_WAIT   -      timewait (32.14/0/0)

tcp        0      0 172.24.0.1:47000        172.24.0.2:5044         TIME_WAIT   -      timewait (48.57/0/0)

tcp        0      0 172.24.0.1:46946        172.24.0.2:5044         TIME_WAIT   -      timewait (43.55/0/0)

<생략>

항상 netstat을 읽을때는 왼쪽을 기준으로 상태를 보여준다고 생각하면 된다.


ss 명령어 결과를 보면 TIME_WAIT 소켓이 많은 것이 뭔가 찜찜하다. 좀더 자세하게 보기 위해 netstat 명령어를 수행하니 다음과 같이 5044(logstash) 서버에 HAProxy가 연결을 맺고 전송한 뒤 종료하는 과정에서 계속해서 timewait 소켓이 발생하는 문제다. 즉, 잦은 연결 맺음/끊음이 있을 수 있는 것으로 보이며 이로 인해 서비스 성능 저하가 있을 수 있다. 그러므로 TIME_WAIT 소켓을 줄이는 방법을 선택해야한다. (로컬 포트의 고갈 현상은 보이지 않으니 tw_reuse 옵션은 설명하지 않겠다)


즉, 클라이언트 입장에서의 TIME_WAIT 소켓을 줄이는 방법인 Connection Pool 방식을 사용한다. 


HAProxy와 뒷단에 위치한 logstash 서버와 미리 연결된 커넥션 풀을 생성하고 해당 커넥션 풀을 사용하는 방식으로 적용한다. 


자, 이제 다시 확인해보자.

[centos@ip-172-31-27-12 ~]$ ss -s

Total: 71 (kernel 87)

TCP:   827 (estab 104, closed 712, orphaned 0, synrecv 0, timewait 712/0), ports 10

Transport Total     IP        IPv6

*  0         -         -        

RAW  0         0         0        

UDP  7         4         3        

TCP  58        26        32       

INET  65        30        35       

FRAG  0         0         0        


줄긴 줄었는데 아직도 완벽하진 않다. 자, 그럼 이제 어떤 이유로 time_wait이 발생하는지 확인해보자.

(예를 드는 것이므로 참고만 하도록 하자..)

[centos@ip-172-31-27-12 ~]$ netstat -napo | grep -i time_wait

tcp        0      0 172.24.0.1:5601        172.24.0.2:40233         TIME_WAIT   -      timewait (32.14/0/0)

tcp        0      0 172.24.0.1:5601        172.24.0.2:40532        TIME_WAIT   -      timewait (48.57/0/0)

tcp        0      0 172.24.0.1:5601        172.24.0.2:40212         TIME_WAIT   -      timewait (43.55/0/0)

<생략>


보는 것 처럼 서버의 입장에서 먼저 연결을 끊기 때문에 발생한 것으로 확인된다. curl 을 통해 확인하면 Header에 Connection:close 가 담겨서 오는 것을 확인할 수 있다. 


[centos@ip-172-31-27-12 ~]$ curl -v -X POST server

 < HTTP/1.1 200 OK 

< Connection: close


이러한 헤더는 was가 먼저 연결을 끊었다는 의미로, was 측에서 time_wait을 낮출 수 있는 KeepAlive를 사용해야 한다. 그러면 해당 웹서버의 keepalive 옵션을 활성화 시킨 뒤 다시 시도해보자.


[centos@ip-172-31-27-12 ~]$ ss -s

Total: 71 (kernel 87)

TCP:   827 (estab 268, closed 0, orphaned 0, synrecv 0, timewait 0/0), ports 201

Transport Total     IP        IPv6

*  0         -         -        

RAW  0         0         0        

UDP  7         4         3        

TCP  58        26        32       

INET  65        30        35       

FRAG  0         0         0        


keepalive를 활성화 시키니 현재 활성화 된 EASTBLISHED 상태의 소켓이 많으며, timewait은 0개에 도달했다. wrk를 통해서 확인을 해보면 응답속도도 향상된 것을 확인할 수 있다.


위 작업들을 통해 서버 내 불필요한 작업을 간소화 시킬 수 있었으며 서비스를 개선시킬 수 있었다. 하지만 직접적으로 클라이언트와 맞붙는 서버가 과연 안전할까? 그렇다면 이번엔 앞단에 웹서버를 사용해보자



필자는 주로 웹서버를 사용할때 Apache를 사용 했었다. 하지만 Nginx가 좀더 가볍고 분산환경에 좋다 보니 본 글에서는 Nginx를 다루도록 한다. 


우선 앞단에 웹서버가 있는게 왜 좋을까?

앞단에 웹서버를 두는 것은 보안적인 측면 뿐만아니라 다양한 라우팅, 헤더를 컨트롤하는 등의 옵션을 단순히 설정만 바꿈으로서 가능하기 때문이다.


속도는 어떨까?

속도는 당연히 느려질 수 밖에 없다. 절차가 하나 더 추가됐기 때문에 예상한 결과이다. 하지만 Trade off로 그만큼 얻을 수 있는 다양한 설정과 옵션이 존재한다. 그러므로 사용하는 것이 더 좋다.


그렇다면 이러한 Trade off를 가장 이점만 챙기기 위해서는 어떻게 해야할까?


바로 속도를 최대한으로 끌어내는 것이다.

웹서버를 앞단에 두고 뒷단에 was 를 배치한 뒤, 이를 Proxy Pass 로 넘기게 될 경우 다음과 같은 작업을 진행해 줘야 가장 최적의 이점을 끌어낼 수 있다.


1. net.ipv4.tcp_tw_reuse = 1

[    클라이언트    ] ---- [    Nginx    ] ----proxypass----- [    was    ]
       (클라) -----------------(서버)
                                    (클라) -------------(서버)

위와 같이 존재한다고 했을 때, Nginx는 ProxyPass 하는 과정에서 클라이언트로 역할하게 된다. 이때 너무 많은 클라이언트들이 몰리게 될 경우 Nginx는 그만큼의 로컬 포트를 할당하여 WAS와 연결을 하게 된다. 이 과정에서 Nginx가 was로 연결을 성사 후 데이터를 보내고 연결을 끊는 구조(nginx가 Active closer)가 될텐데, 이렇게 되면 time_wait 소켓이 다수 생성되어 로컬 포트가 고갈되는 문제가 발생할 수 있다. (실제로 위와 같이 구성 후 netstat -napo를 수행하면 다수의 time_wait 소켓이 생성되어 있다.) 그러므로 tw_reuse 옵션을 통하여 time_wait 소켓이 사용되고 있으면 이를 재사용할 수 있도록 한다.(재사용은 부족할때 이루어진다)


2. Nginx의 upstream의 keepalive 설정

nginx에서의 upstream은 proxypass를 할 대상을 지정하고 keepalive 옵션을 지정할 수 있다. Nginx 와 was는 클라이언트 수 만큼 계속해서 연결을 성사해야한다. 그만큼 맺고 끊음이 잦을 것이며, 이때 keepalive 옵션을 사용하면 handshake 과정을 감소시킴으로써 time_wait 현상을 낮출 수 있다. 


3. Nginx의 worker 수 및 다른 옵션을 사용

worker_connections 수를 늘리고, epoll을 사용하며 multi_accept를 on 함으로써 퍼포먼스적으로 더 높은 성능으로 이끌어낼 수 있다.



네트워크 카드는 안녕한가?

ethtool을 사용하면 네트워크 카드의 속도 등을 알 수 있다. 이때 g 옵션을 사용하면 RingBuffer의 크기를 알 수 있다. 이러한 RingBuffer 크기도 네트워크 성능에 영향을 미친다.


RingBuffer?


케이블을 통해서 들어온 패킷 정보는 제일 먼저 네트워크 카드 내에 있는 RingBuffer라는 공간에 복사된다. 이후, 커널에게 패킷 도착을 알린 다음 패킷 정보를 다시 커널로 복사한다. 그렇게 때문에 만일 RingBuffer의 크기가 작다면 네트워크 성능 저하가 있을 수 있는 것이다. 그렇다면 어떻게 해야하나 ?


root@www:/home# ethtool -g eno1

Ring parameters for eno1:

Pre-set maximums:

RX:4096

RX Mini:0

RX Jumbo:0

TX:4096

Current hardware settings:

RX:2048

RX Mini:0

RX Jumbo:0

TX:2048


바로 위에 보이는 Maximum에 Current Hardware settings 값을 맞춰주는 것이다. 즉 Rx / Tx 링 버퍼 크기를 늘리면 스케줄링 지연 중에 네트워크 카드의 패킷 폐기 확률을 줄일 수 있다.




마지막으로 추후 있을 사태를 위한 대비를 하자


kdump-tools 를 설치한다.

커널 패닉은 다양한 이유로 커널 패닉 상태에 빠지게 된다. 리눅스는 커널 패닉상태가 되면 사용자의 입력을 받아들일 수 없으므로 원인 파악이 불가능하다. 이때, crashkernel을 설정 해 두면 커널 패닉에 빠질 때 crasahkernel을 로딩해서 패닉에 빠진 커널의 디버깅 정보를 저장한다. 이를 통해 원인 추적이 가능하다.


sysstat 을 설치한다.

sysstat은 다음과 같은 툴을 제공한다.

I/O 통계를 위한 “iostat“

r/s, w/s rkB/s, wkB/s: read 요청과 write 요청, read kB/s, write kB/s를 나타낸다. 어떤 요청이 가장 많이 들어오는지 확인해볼 수 있는 중요한 지표다. 성능 문제는 생각보다 과도한 요청때문에 발생하는 경우도 있기 때문이다.

await: I/O처리 평균 시간을 밀리초로 표현한 값이다. application한테는 I/O요청을 queue하고 서비스를 받는데 걸리는 시간이기 때문에 application이 이 시간동안 대기하게 된다. 일반적인 장치의 요청 처리 시간보다 긴 경우에는 블럭장치 자체의 문제가 있거나 장치가 포화된 상태임을 알 수 있다.

프로세스 별 정보를 위한 "pidstat"

pidstat은 프로세스 당 CPU 사용 율을 보기에 좋다. 지속적으로 변화하는 상황을 띄워주기 떄문에 상황변화를 기록하기 좋다.


프로세스별 메모리 통계를 위한 “mpstat“

이 커멘드는 CPU time을 CPU 별로 측정할 수 있다. 이 방법을 통하면 각 CPU별로 불균형한 상태를 확인할 수 있는데, 한 CPU만 일하고 있는것은 application이 single thread로 동작한다는 이야기다.


각종 시스템 리소스 정보 통계를 위한 “sar“

이 값은 TCP 통신량을 요약해서 보여준다.  

active/s: 로컬에서부터 요청한 초당 TCP 커넥션 수를 보여준다 (예를들어, connect()를 통한 연결).

passive/s: 원격으로부터 요청된 초당 TCP 커넥션 수를 보여준다 (예를들어, accept()를 통한 연결).

retrans/s: 초당 TCP 재연결 수를 보여준다.

active와 passive 수를 보는것은 서버의 부하를 대략적으로 측정하는데에 편리하다. 

위 설명을 보면 active를 outbound, passive를 inbound 연결로 판단할 수 있는데 꼭 그렇지만은 않다. (예를들면 localhost에서 localhost로 연결같은 connection)

retransmits은 네트워크나 서버의 이슈가 있음을 이야기한다. 신뢰성이 떨어지는 네트워크 환경이나(공용인터넷), 서버가 처리할 수 있는 용량 이상의 커넥션이 붙어서 패킷이 드랍되는것을 이야기한다. 위 예제에서는 초당 하나의 TCP 서버가 들어오는것을 알 수 있다.


하지만 여기서 가장 중요한 것은 sar 이다.


sar (system activity reporter) 

리눅스 시스템의 cpu, memory, network, disk io 등의 지표 정보를 수집하여 sar command을 통해 실시간으로 지표를 보여 주며, 파일로 저장한다. sar을 구성하는 요소는 아래와 같다.


 (1) sadc : system activity data collector 

   지표 데이터를 collect 하며 이를 /var/log/sa/sa~ 형태의 이진 데이터 파일로 저장하는 도구이다.


 (2) sadf : Display data collected by sar in multiple formats

  지표 데이터 파일은 이진 파일 형식이라 sar 로만 report 할 수 있다. sadf는 csv, xml, svg 등의 포맷으로 변환해주는 도구이다. 주로 sar의 데이터를 다른 모니터링 리소스 지표의 데이터로 활용할 때 쓰이며, 최신 버전에선 svg 포맷으로 웹페이지에서 GUI 형식으로도 볼 수 있다. 


 (3) sa1 : Collect and store binary data in the system activity daily data file

 sadc로 추출한 모든 지표 데이터를 /var/log/sa날자 파일에 바이너리 형식으로 저장하는 bash 스크립트.


 (4) sa2 : Create a report from the current standard system activity daily data file

  s1으로 생성된 데이터 파일을 기반으로, 원하는 지표 옵션을 선택해 사람이 읽을 수 있는 파일 형태로 1벌 더 저장 (/var/log/sar날자 파일) 하는 bash 스크립트.

 (sa1으로 추출한 데이터만으로도 분석이 가능하여, 굳이 sa2를 사용할 필요는 없다. 디스크 용량만 잡아먹고... 그냥 이런 기능이구나 정도만 알자.)


sysstat 설치 후 crontab에서 보면 기본 설정이 되어있다.

[root@ip-172-31-27-12 ~]# cat /etc/cron.d/sysstat

# Run system activity accounting tool every 10 minutes

*/10 * * * * root /usr/lib64/sa/sa1 1 1

# 0 * * * * root /usr/lib64/sa/sa1 600 6 &

# Generate a daily summary of process accounting at 23:53

53 23 * * * root /usr/lib64/sa/sa2 -A


sysstat으로 서버의 각종 지표를 살펴보는 일은 매우 중요하다. 특히 서버가 이상 현상을 일으킬 때 원인을 파악하는데 많은 도움이 된다. 하지만 sysstat을 기본으로 설치하면 10분에 한 번 시스템 지표를 수집하게 되는데, 이 마저도 평균값이 아닌 순간의 값이기 때문에 큰 도움이 안 될 때도 있다. 그래서 대부분은 sysstat의 주기를 변경해서 사용하는데, 일부 서버 중 세부적으로 지표 변화를 살펴봐야 할 경우 sysstat을 1초 단위까지도 수집할 수 있게 변경할 수 있다.

* * * * * root /usr/lib64/sa/sa1 10 1

위를 아래로 변경함으로써 1분 간격으로 59번을 호출 하도록 한다. 즉 매 초마다 설정하는 것

* * * * * root /usr/lib64/sa/sa1 1 59


하지만 1초 단위로 바뀐 만큼 sar 파일의 크기가 커지게 된다. 반드시 적용 전에 예상 크기 변화와 디스크 가용량을 감안해서 적용해야한다.



tcpdump 지속적으로 걸어 놓기


애플리케이션에서 타임아웃이 발생한다면 추적하기 위해 가장 좋은 방법은 tcpdump를 통한 패킷 덤프 분석이다. 하지만 타임아웃이 주기적으로 발생하는 것이 아니고 간헐적으로 발생하게 된다면?

계속해서 걸어 놓을 수도 없고, 하루 종일 지켜보고 있을 수도 없다. 

그럴 때는 아래와 같은 옵션을 사용하면 1시간 단위로 덤프 파일을 새로 만들어 줄 수 있다.

tcpdump -vvv -nn -A -G 3600 -w /var/log/tcpdump/파일이름.pcap -Z root host <추적하고자 하는 dest ip> and port <추적하고자 하는 dest 포트> &


보통은 /var/log 밑에 tcpdump라는 디렉터리를 만들어 놓고 위 명령을 실행한다. -G 옵션에 의해서 3600초에 한 번 파일을 갱신하게 된다. 이렇게 하면 계속해서 서버에 들어가서 덤프 파일의 크기가 얼마나 커졌는지 확인할 필요도 없고 다른 업무를 하다가 타임아웃이 발생한 순간의 덤프 파일만 가져다가 분석하면 된다.



swap 영역 날려 버리기


서버를 운영하다 보면 긴급한 순간이 있다. 메모리 사용률이 쭉쭉 올라가고 스왑을 사용하기 시작하는 그런 순간. 문제의 원인을 파악해서 메모리 릭을 발생시키는 프로세스를 죽이고 사용 패턴을 정상화시켜도 찝찝하게 남아 있는 스왑 메모리가 있다.

긴급한 순간을 넘기고 난 후의 메모리 모습

메모리 사용률은 정상 패턴이 되었지만 긴박할 때 사용했던 스왑 영역이 훈장처럼 남아 있다. 가장 좋은 방법은 리부팅이겠지만, 굳이 저 스왑 영역을 날려 버리기 위해 리부팅을 할 필요는 없다. 

바로 아래와 같이 명령을 입력

swapoff -a
swapon -a

말 그대로 스왑 영역을 아예 날렸다가 다시 붙이는 명령.

아마 이 명령이 서비스 중인 시스템에 다른 사이드 이펙트를 불러일으키진 않을까에 대해서는 시스템 엔지니어마다 약간씩 견해가 다를 수 있다. 하지만 사이즈가 그리 크지 않고 (MB 단위) 대부분 서비스와 상관없는 프로세스들이 점유하고 있다면 큰 영향은 있지 않다. 스왑 영역에 보관되어 있는 메모리가 없어진다고 해도, 커널에서는 해당 영역을 다시 메모리에 올릴 만큼의 충분한 Free 영역을 보유하고 있기 때문이다.



브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari