사용자 도구

사이트 도구


scrap:serverperformanceandscalabilitykiller

from http://msdn.microsoft.com/ko-kr/library/ms951773.aspx

Server Performance and Scalability Killers

서버 성능에 관한 이슈는 데스크탑 애플리케이션을 제작해온 프로그래머들도 이제는 신경써야할 부분이다. 뒤에서 쓰이는 백본 프레임웍이 아무리 잘 되어있다고 해도, 그것을 이용해 실제 애플리케이션을 제작하는 사람이 아무 생각이 없다면, 소용없다. 데스크탑 애플리케이션과 서버 애플리케이션 사이에는 성능에 중요한 영향을 주는 커다란 차이점이 존재한다.

서버 vs 데스크탑 애플리케이션

데스크탑 애플리케이션의 성능에 영향을 주는 요소들은 잘 알려져있다. 긴 코드는 느린 속도를 의미한다. 리소스를 많이 사용하는 것은 무거운 애플리케이션을 만들어낸다. 긴 시작 시간은 사용자를 분노하게 한다. 대량의 데이터를 한 순간에 다루는 것은 페이지폴트가 발생할 확률을 높이고 이는 시스템을 버벅거리게 만든다. 서버 애플리케이션에 영향을 주는 요소들은 위에서 말한 요소들 뿐 아니라, 다음과 같은 것들이 있다.

  • 서버 애플리케이션은 적게는 수십명에서 많게는 수백명의 클라이언트들을 동시에 처리해야한다. 데스크탑 애플리케이션은 10분의 1초 정도 안에 반응하면, 반응이 빠른 것으로 취급받는다. 하지만 이는 하나의 작업이 100ms 정도의 시간을 소요한다는 말이고, 결국 초당 10개 정도 밖에 처리할 수 없다는 것을 의미한다. 대부분의 서버 애플리케이션은 초당 10개보다는 훨씬 높은 처리량이 요구된다. 게다가 CPU 속도에 비해 훨씬 느린 네트워크를 고려하면, 서버 애플리케이션의 반응 속도는 훨씬 빨라져야한다.
  • 서버 애플리케이션은 대부분 대량의 데이터를 다룬다. 비효율적인 알고리즘, 특히 O(N^2)나 그 이상의 시간을 요구하는 알고리즘들은 백만개 단위의 데이터를 다루는 데 있어서는 적당하지 않다.
  • 서버 머신은 데스크탑에 비해서 강력하다. 보통 더 많은 메모리를 가지고 있으며, 더 큰 디스크와 빠른 CPU, 그리고 종종 여러 개의 CPU를 가진다. 하지만 이외에도 더 있다. 데스크탑 머신은 대부분의 시간을 놀다가 어느 한 순간 사용량이 치솟는 데 비해, 서버 머신은 대부분 일정 이상의 사용량을 계속 유지한다. 서버는 데스크탑에 비해서 비싸고, 이런 사항들을 잘 지킬 거라고 가정되어진다. -_-;
  • 서버 애플리케이션은 실행 시간이 짧게는 몇일에서 길게는 몇달까지 이어진다. 이는 애플리케이션의 성능이 메모리 누수 같은 이유로 인해 시간이 지남에 따라 떨어지면 안 된다는 말이다.
  • 서버 애플리케이션은 대부분의 경우 다중 스레드 구조가 필요하다. 단독 스레드 서버는 한 순간에 하나의 작업 밖에 처리하지 못하는 데다가 대부분의 시간을 IO에 블로킹되어 보내게 된다. 이는 받아들일 수 없을 정도의 성능 저하를 야기한다. 스레드풀을 사용하게 되면, 하나의 스레드가 노는 동안에 다른 스레드가 작업을 처리할 수 있으므로 낫다. 사실 멀티 프로세서를 제대로 활용하려면 다중 스레드는 필수다. 하지만 다중 스레드 프로그램은 제작하기도 어렵고, 디버그하기도 어렵다. 하지만 제대로만 한다면, 단독 스레드 애플리케이션보다는 훨씬 나은 성능을 얻을 수 있다.

서버 프로그램의 성능을 떨어뜨리기 위한 십계명

아래의 “십계명”은 서버의 성능과 확장성을 떨어뜨리기 위한 것들이다. 가능하다면 반드시 깨어야할 것들이란 말이다.

대량의 오브젝트를 생성하고, 삭제하라.

메모리 할당은 생각보다 시간이 오래 걸리는 작업이기 때문에, 너무 잦은 메모리 할당은 피하는 것이 좋다. 대부분의 메모리 할당자가 메모리 해제시, 메모리의 조각화를 막기 위해 내부적인 처리를 하기 때문에, 메모리 해제는 더욱 더 시간이 오래 걸린다.

프로세서 캐시에 대해서 생각하지 마라.

대부분의 사람들이 가상 파일 시스템에 의해 일어나는 페이지 폴트가 상당히 높은 비용을 요구하고, 될 수 있는 한 피하는 것이 상책이라는 것을 알고 있지만, 다른 종류의 메모리 액세스는 무조건 좋은 것으로 알고 있다. 그러나 80486이 나온 이후로부터 메모리 액세스가 무조건 좋다는 명제는 거짓이 되었다. 현재 나와있는 CPU들은 워낙 빨라서 적어도 두 레벨의 메모리 캐시가 필요하다. 수 KB 정도 되는 L1 캐시는 CPU 바로 옆에 붙어있기 때문에, 내용을 읽어오는 데 1 사이클이면 되지만, 수백 KB 정도 되는 L2 캐시는 읽어오는데 4~7 사이클을 필요로 한다. 그리고 실제 RAM을 읽어오는 작업은 수십 사이클을 필요로 한다. RAM에서 데이터를 읽어오는 작업은 곧 100 사이클 이상이 걸리게 될 것이다. CPU 캐시는 여러 면에서, 조그맣고 빠른 가상 파일 시스템과 비슷하다.

캐시 메모리에서 관심있게 다루는 가장 작은 단위는 하나의 바이트가 아니라, 캐시 라인이다. 펜티엄의 경우에는 32 바이트, 알파 같은 경우에는 64 바이트다. 즉 8KB 크기의 L1 캐시의 경우, 겨우 512개의 코드와 데이터가 존재할 수 있다는 말이다. 같이 사용되는 데이터가 캐시 메모리에 같이 존재하지 않을 경우, 성능을 상당히 떨어뜨릴 수 있다. 이는 배열(데이터가 거의 인접해있는…)과 리스트(데이터가 상당히 떨어져 있을 수 있는…)의 속도 차이가 꽤 커질 수 있음을 의미한다.

데이터가 같은 캐시 라인에 존재할 수 있도록 압축하는 것은 보통 성능 향상에 도움이 된다. 하지만 이는 CPU가 여러개인 상황에서는 오히려 성능을 떨어뜨릴 수 있다. 각각의 CPU 캐시에 있는 메모리를 동기화하기 위해서, 로우 레벨의 메모리 시스템이 작동해야 하기 때문이다. 예를 들어 임의의 데이터를 다른 CPU들은 읽기 전용으로 사용하고 있는데, 하나의 CPU가 그 데이터를 계속 변경한다면, 그 변경 사항이 다른 CPU들에게도 전파되어야하므로 느려질 수 밖에 없는 것이다. 즉 이런 경우 오히려 하나의 캐시 라인에 데이터가 압축되어 있는 것보다는 퍼져있는 것이 도움이 될 수 있다.

속도를 위한 최적화보다는 공간을 위한 최적화가 더 효율적이다. 작은 코드는 적은 수의 페이지를 차지하고, 페이지 폴트의 횟수를 줄이며, 적은 캐시 라인에 들어갈 수 있게 된다. 하지만 위에서도 언급했듯이 어떤 경우, 속도 쪽에 관심을 기울이는 것이 나은 경우가 있다. 이 둘을 구별하기 위해서는 프로파일러를 사용하라.

자주 사용되는 데이터를 절대 캐싱하지 마라.

캐싱이란 개념은 어떤 애플리케이션에서도 사용할 수 있다. 결과값을 자주 사용하지만, 시간이 오래 걸리는 계산이 있다면 그 결과값을 메모리 어딘가에 저장해두는 것이다. 고전적인 시간과 공간의 타협(tradeoff)이다. 메모리를 희생해 속도를 얻는 것이다. 잘만 사용하면 굉장히 효율적으로 성능을 끌어올릴 수 있다.

하지만 캐시할 데이터를 판단할 때는 냉정해야한다. 별 필요없는 데이터를 캐싱하는 것은 메모리만 낭비하는 것이다. 캐시를 너무 많이 사용하면, 다른 작업을 위한 메모리가 줄어든다. 캐시를 너무 적게 잡으면 캐시를 쓴 이유가 불분명해진다. 서버의 경우 대부분 공간보다는 시간을 우선시하므로, 데스크탑 애플리케이션보다 캐시를 사용하는 빈도가 잦다. 캐시를 갱신하는 것을 잊지 않도록 해라. 잘못된 내용을 가진 캐시는 없느니만 못하다.

많은 스레드를 생성하라. 많을수록 좋다.

스레드 숫자를 튜닝하는 것은 서버 성능에 엄청난 영향을 끼친다. 만일 스레드가 IO 작업과 밀접하게 관련된 스레드라면, 그 스레드들은 대부분의 시간을 블러킹 상태로 보낼 것이다. 이를 개선하기 위해 스레드의 숫자를 늘리면 단위 시간당 처리량을 늘리는 데는 도움이 될 것이다. 그러나 이런 식으로 스레드 숫자를 너무 늘리게 되면, 컨텍스트 스위치 문제 때문에 오히려 서버 성능을 떨어뜨리게 된다. 컨텍스트 스위치는 말 그대로 오버헤드(overhead)로서 애플리케이션이 처리하는 일에 도움 주는 것이 아무 것도 없다. 무엇보다도 컨텍스트 스위치가 일어날 경우, CPU 캐시에 있는 내용들이 쓸모없어진다. 이들을 교체하는 것은 결코 비용이 적게 드는 일이 아니다.

스레드 숫자는 애플리케이션 구조에 따라 달라진다. 클라이언트 하나당 스레드 하나는 대부분의 경우 쓸모가 없다. 클라이언트 숫자가 많아질 경우, 성능이 기하급수적으로 떨어진다. 스레드 풀링 모델이 이보다는 낫다. Windows 2000 이상에서는 QueueUserWorkItem 스레드 풀링 관련 API를 제공하기 때문에 이를 이용하면 스레드 풀을 자체 제작하는 수고를 덜 수 있다.

데이터에다가 전역 락을 사용하라.

데이터를 스레드 세이프하게 만드는 가장 쉬운 방법은 락을 단 하나만 두고, 데이터를 액세스하는 모든 함수에서 그 락을 통해 데이터에 접근하게 하는 것이다. 하지만 이 방법은 스레드의 실행을 직렬화시켜버리기 때문에 문제가 있다. 모든 스레드가 같은 데이터를 사용하는 상황에서 이런 식으로 락을 두게 되면, 전체 프로그램이 단 하나의 스레드만 사용하는 꼴이 되어버리는 것이다.

락에 의한 성능 저하를 막는 방법은 여러 가지가 있다.

  • 너무 방어적인 코드를 짜지 마라. 즉 필요하지 않은 데이터에다가 락을 사용하지 말라. 데이터에다가 락을 걸 때도 필요할 때 걸고, 바로 풀어야 한다. 길다란 코드 제일 위에서 락을 걸고, 그 코드의 제일 밑부분에서 락을 푸는 것은 좋은 코드가 못 된다.
  • 데이터를 여러 개로 분할하고, 락도 여러 개를 사용하라. 예를 들어 데이터를 알파벳 첫글자 같은 것으로 분할하고, 그 수에 많게 락을 두면, 하나의 스레드가 A 영역에서 작업을 하는 동안 다른 스레드가 Z 영역에서 작업할 수 있는 것이다.
  • InterlockedXXX 계열의 API를 사용하라. 이 함수들은 CPU 벤더가 제공하는 연산을 사용하기 때문에 다른 락 연산보다 훨씬 빠르다. 그러므로 락을 필요로 하는 데이터가 간단한 것일 경우, 이들을 사용하는 것이 낫다.
  • 데이터가 자주 변경되지 않는다면 multi-reader/single-write 락을 사용하라. writer가 락을 얻지 못하는 현상(starvation)에만 주의하면 좀 더 나은 성능을 얻을 수 있을 것이다.
  • 크리티컬 섹션을 사용하는 경우, 스핀 카운트를 같이 사용하다. Windows NT 4.0 서비스팩 3 이후로 소개된 SetCriticalSectionSpinCount API를 참고하라.
  • TryEnterCriticalSection 함수를 사용해서 락을 얻지 못하는 경우, 뭔가 유용한 일을 하라. 잦은 병목은 프로그램을 직렬화시켜버린다. 그리고 이는 CPU 사용률을 떨어뜨린다. 이를 보고 아무 생각 없이 스레드를 추가하면, 오히려 문제를 더 심화시키게 된다.

다중 프로세서 머신에 눈독들이지 마라.

당신이 만든 프로그램이 다중 프로세서 환경에서 더 느리게 돌아가는 것을 보는 것은 기분 나쁜 일이다. CPU가 N개라면 1개인 시스템보다 N배의 성능이 나와야한다고 생각하는데 말이다. 성능이 떨어지는 이유는 주로 병목 현상 때문이다. 락에 의한 병목, 버스에 의한 병목, 캐시 라인에 의한 병목 등등. 프로세서가 실제 작업을 처리하는 것이 아니라, 리소스를 차지하기 위한 싸움만을 처절하게 하고 있는 것이다.

멀티 스레드 애플리케이션을 진지하게 개발하고자 한다면, 다중 프로세서 환경에서 스트레스 테스트나 성능 테스트를 해야 한다. 프로세서가 1개인 상황에서의 동시성(concurrency)은 환영일 뿐이기 때문이다.

블로킹 함수만을 사용하라. 재미있다.

IO를 행하는 블로킹 함수들은 대부분의 데스크탑 애플리케이션에는 적당하다. 하지만 서버의 경우 블로킹 함수를 사용하면, 성능을 떨어뜨리는 주요한 요인이 된다. 보통 IO는 끝나는 데까지 수백만 사이클을 필요로 하고, 이 시간 동안 다른 클라이언트들은 아무 것도 할 일 없이 기다리게 되기 때문이다. 프로그램의 구조가 좀 더 복잡해지겠지만, 비동기 IO를 사용하는 것이 나은 방법이다.

블로킹 함수를 사용할 수 밖에 없다면 스레드를 사용하라. 대부분 너무 많은 스레드를 생성하는 것은 좋지 않고, 적은 수의 제한된 스레드를 사용하는 것이 낫다. 그 중에 하나는 IO 작업을 스케쥴링하고, 나머지 스레드들이 실제 IO 작업을 처리하게 하는 것이다.

측정하지 마라.

당신이 말하는 것을 측정할 수 있다면, 그리고 그것을 숫자로 표현할 수 있다면, 당신은 그것에 대해서 뭔가 아는 것이다. 하지만 숫자로 표현할 수 없다면 당신의 지식은 빈약하며 불충분한 것이다. 무언가를 숫자로 표현하는 것은 지식의 시작일 뿐만 아니라, 당신의 생각이 과학에 한 걸음 다가갔다는 것을 의미한다. - Lord Kelvin (William Thomson)

측정을 하지 않고는 애플리케이션의 성능을 알 수 없다. 어디가 느리고 빠른지, 대충 짐작해서 더듬을 뿐이다. 이래서야 제대로 된 성능 개선 계획을 세울 수 있을 리가 없다.

측정에는 블랙 박스 테스팅과 프로파일이 있다. 블랙 박스 테스팅으로는 메모리 사용량, 컨텍스트 스위치, CPU 사용량 등 외부적으로 드러나는 사항을 알 수 있고, 프로파일링으로는 실행 시간, 함수 호출 빈도 등을 알 수 있다.

분석 없는 측정은 별로 쓸데가 없다. 측정은 어디에 문제가 있고, 그 문제를 고립시키는 것까지는 해주지만, 왜 문제가 생겼는지 알려면 분석을 해야한다.

애플리케이션을 변경했다면, 새로 측정을 해라. 변경이 효과가 있었는지 알 수 있다. 변경은 다른 문제를 발생시킬 수도 있기 때문이다. 변경했을 때 말고도 정기적으로 측정을 하는 것이 좋다.

하나의 클라이언트가 보내는 하나의 요청만으로 테스트하라.

서버 프로그램을 제작하는 경우 하나의 클라이언트만을 붙여서 테스팅하는 것은 좋지 않다. 여러 클라이언트가 있어야만 발생하는 문제가 허다하기 때문이다. 또한 성능과 관련된 부분에 있어서는 거의 아무 것도 테스트할 수가 없다. 서버 프로그램을 테스트할 때는 반드시 가상 클라이언트를 만들어 동시에 여러 개를 붙여놓고 테스트하라.

실제 세계에서 일어나는 일들을 생각하지 마라.

몇개 안되는 인공적인 시나리오만을 보고, 예를 들어 어떤 벤치마크 같은 것을 보고 프로그램을 튜닝하는 것은 자주 빠질 수 있는 함정이다. 그런 시나리오를 넓게 보고, 실제 세계에 적용할 수 있는 부분만을 뽑아내는 것이 좋다.

scrap/serverperformanceandscalabilitykiller.txt · 마지막으로 수정됨: 2014/11/11 14:50 저자 excel96