성능, 확장성

성능

  • 성능을 높인다는 것은 적은 자원을 사용하면서 더 많은 작업을 하도록 한다는 것이다.

  • 단일 스레드가 아니라 여러 스레드를 사용하는 경우 스레드 간의 작업을 조율하거나 컨텍스트 스위칭이 발생하는 등 성능 상의 비용이 발생한다.

  • 하지만 여러 스레드를 효율적으로 잘 활용한다면 성능, 응답성을 높이고 처리량도 늘릴 수 있다.

  • 보통 성능을 고려할 때 확장성과 처리량과 용량이라는 세 가지 측면을 중요시한다.

    • 단일 구조의 애플리케이션에 비해 여러 계층으로 나뉜 애플리케이션이 티어 간 통신으로 인해 자원을 더 많이 사용할 수 있다.

    • 하지만 단일 구조 애플리케이션이 처리할 수 있는 양보다 훨씬 많은 요청이 들어오는 경우, 시스템 자원을 더 많이 사용하더라도 많은 처리량을 감당할 수 있어야 한다.

  • 일단 제대로 동작하게 만들고 난 다음 빠르게 동작하도록 최적화해야 한다. 예상한 것보다 심각하게 성능이 떨어지는 경우에만 최적화를 하는 것으로 충분하다.

  • 좀 더 최적화 되어있는 코드는 보통 복잡하기 때문에 코드의 가독성과 유지보수의 용이성이 떨어진다.

  • "빠르다"의 정의를 내리고, 어떤 조건일 경우에 이 방식이 빠른지, 얼마나 많이 해당 조건이 발생하는 건지, 개발 비용이나 유지보수 비용이 얼마나 증가하며 그를 감수할 만 한지 고민해봐야 한다.

  • 성능 튜닝 시에는 항상 성능 목표에 대한 명확한 요구사항이 있어야 하며 실제 환경과 유사한 성능 측정 도구가 필요하다.

암달의 법칙

  • 병렬 작업과 순차 작업의 비율에 따라 하드웨어 자원을 추가 투입했을 때 얼마나 속도가 증가하는지 예측할 수 있다.

  • 순차적으로 실행되어야 하는 작업의 비율이 F이고 프로세서(코어) 개수가 N일 경우, 1 / F + ((1 - F) / N) 정도까지 속도를 증가시킬 수 있다.

  • 단순히 계산하면 N이 무한대로 증가할 수 있을 때 속도 증가량은 최대 1/F까지 증가한다. 예를 들어 F가 0.1인 경우에는 속도가 10배 증가할 수 있다는 의미이다.

  • 프로세서가 계속해서 증가한다 해도 처리량이 계속해서 늘어나는 것이 아니라 오히려 떨어지거나 일정 수치 이상 오르지 않을 수 있다. 따라서 이 법칙을 통해 적절한 프로세서 개수를 정해야 한다.

  • 작업을 멀티 스레드로 병렬 처리하는 경우, 큐에 있는 작업을 꺼낼 때와 여러 작업 결과를 합칠 때 순차적으로 처리하게 된다.

스레드와 비용

컨텍스트 스위칭

  • CPU 개수보다 많은 양의 스레드를 사용하는 경우, 스레드가 돌아가면서 실행되기 때문에 컨텍스트 스위칭이 발생한다.

  • 컨텍스트 스위칭으로 인해 스레드가 사용하던 데이터가 프로세스의 캐시 메모리에서 사라지면 다음에 다시 로드해야 하기 때문에 처리 속도가 느려질 수 있다.

  • 락을 비롯한 리소스를 획득하기 위해 대기 상태에 들어가는 요청이 많은 경우 컨텍스트 스위칭 횟수와 스케줄링 부하가 늘어나면서 전체적인 처리량이 줄어든다.

메모리 동기화

  • 메모리 배리어는 캐시를 플러시하거나 무효화하고, 하드웨어와 관련된 쓰기 버퍼를 플러시하고, 실행 파이프라인을 늦출 수 있는 명령어이다. 자바에서는 synchronized, volatile 키워드를 통해 해당 기능을 이용할 수 있다.

  • 메모리 배리어를 사용하면 컴파일러가 제공하는 최적화 기법을 제대로 사용하지 못해 성능 저하가 발생할 수 있다.

  • JVM은 락을 거는 대상 객체가 특정 스레드 내에 한정되어 있다면 다른 스레드와의 경쟁 조건이 발생할 수 없기 때문에 락을 걸지 않는 등 최적화를 하기도 한다.

  • 또한 연달아 붙어 있는 락을 하나로 묶는 락 확장 방식을 사용해 락을 확보하고 해제하는 시간을 줄이기도 한다.

락 경쟁 줄이기

  • 락을 점유하는 동안 수행하는 작업을 최소화해야 한다.

  • 락 분할

    • 락이 두 개 이상의 독립적인 상태 변수를 한 번에 묶어 동기화하고 있다면, 각각의 락으로 동기화하도록 분할하여 확장성을 높일 수 있다.

  • 락 스트라이핑

    • 독립적인 객체를 여러 크기의 단위로 묶고 락을 나누는 방법을 통해 락 경쟁이 발생할 확률을 줄이고 병렬성을 높일 수 있다.

    • 모든 연산을 수행할 때 마다 한 번씩 접근해야 하는 카운터 변수와 같은 필드를 핫 필드라고 칭하는데, 이 필드에 대해 여러 스레드가 접근해야 하는 경우 병목이 될 수 있다.

    • 따라서 이러한 핫 필드를 가급적 줄이고, 줄이는게 어렵다면 단일 연산 변수(AtomicXXX) 타입을 사용하는 것이 좋다. ConcurrentHashMap의 경우, 전체 카운트를 하나의 변수에 두지 않고 락으로 분배된 각 부분마다 카운터 변수를 둔 후, 전체 카운터가 필요할 때 변수들의 합을 구해 사용한다.

  • CPU 모니터링

    • vmstat, mpstat 등의 도구를 사용해 CPU 활용도를 모니터링하면서 충분히 활용하고 있는지 확인해야 한다.

    • CPU를 충분히 활용하고 있지 않다면, 부하가 충분히 가해지지 않았거나 I/O 또는 외부 작업으로 인한 제약이 있거나 락 경쟁이 발생했을 수 있다.

Last updated