ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 14장. 고성능 로깅 및 메시징
    DevBook/Optimizing Java 2024. 7. 31. 08:24

    개요

    • 지연에 민감한 고성능 어플리케이션을 다룰 때 개발자가 해결해야 하는 일반적인 이슈 몇가지를 살펴보고, 이런 시스템에는 어떤 요건이 있고 어떠한 방식으로 설계해야 하는지 안내함
    • 저지연, 고성능 시스템에서 핵심적인 고려사항 2가지는 로깅과 메시징

     

    14.1 로깅

    • 다른 라이브러리와 마찬가지로 로깅 라이브러리도 프로젝트 도입 전 신중한 확인이 필요하다.

     

    14.1.1 로깅 벤치마크

    • 가장 많이 쓰는 세가지 로거(Logback, Log4j, java.util.logging)의 성능을 비교하는 벤치마크를 살펴보자

     

    방식

    • 해당 자료를 사용해 벤치마크 수행
    • 해당 프로젝트는 여러 가지 설정으로 다중 로거를 실행 가능한 벤치마크 제공함

     

    로깅 포맷

    1) Logback 포맷

    14:18:17.635 [Name Of Thread] INFO  c.e.NameOfLogger - Log message

     

    2) java.util.logging 포맷

    Feb 08, 2017 2:09:19 PM com.example.NameOfLogger nameOfMethodContainingLogStatement
    INFO: Log message

     

    3) Log4j 포맷

    2017-02-08 14:16:29,651 [Name Of Thread] INFO com.example.NameOfLogger - Log message

     

    측정

    1) 아이맥

    • Logback이 전반적으로 성능이 가장 우수하고 로깅 포맷이 Log4j 일 때 최고임

     

    2) AWS EC2 t2.2xlarge

    • Logback이 Log4j보다 약간 더 빠르게 나옴

     

    로거 결과

    • 실행 시간 측면에서 대체로 Logback 성능이 가장 좋았고 자바 유틸 로거가 제일 나빴음
    • Log4j 포맷은 전반적으로 가장 일관된 결과를 보였음
    • 실제 시스템에서는, 결과 수치가 엇비슷할 경우, 운영 장비에서 직접 실행 성능을 테스트 해보는 게 좋음
    • 로깅 프레임워크가 생성하는 가비지도 잘 따져봐야 함 (로깅하느라 소비한 CPU 시간만큼 핵심 업무를 병렬 처리할 기회를 잃어버리기 때문). 로깅 라이브러리의 설계와 작동 원리 역시 직선적인 마이크로벤치마크 실행 결과만큼 중요함

     

    14.2 성능에 영향이 적은 로거 설계하기

    • Log4j 2.6 버전은 정상 상태(steady-state)의 가비지-프리(garbage-free)한 로거로 해결하는 것을 목표로 출시됨
      • 정상 상태 : 어플리케이션이 실행되는 동안 일반적인 운영 상태를 의미함. 시스템이 장기적으로 안정적인 상태에서 동작하는 것
      • 가비지-프리 : GC의 필요성을 최소화하거나 없애는 것
    • Log4j 2.6은 임시 배열을 생성해 로그문에 전달되는 매개변수를 담고 가변인수(varargs)를 사용해 할당 횟수를 줄임
      • 모든 매개변수를 담는 단일 배열을 생성해 로그 메시지를 생성하는 과정에서 불필요한 객체 생성을 줄임
      • 가변인수를 사용해 필요한 만큼의 메모리만 할당하여 메모리 사용을 효율적으로 관리함
    • Log4j를 SLF4j로 감싸면 퍼사드가 매개변수를 2개만 지원하기 때문에, 가비지-프리한 방식을 응용하거나 Log4j2 라이브러리를 직접 사용해 코드 베이스를 리팩터링할 필요가 없음

     

    Log4j 2.5와 2.6 비교

     

    1) Log4j 2.5 샘플 실행

    • 141번 수집 중 평균 중단 시간이 약 7ms, 최대 중단 시간이 52ms로 GC 사이클 정보가 표시돼 있음

     

    2) Log4j 2.6 샘플 실행

    • 같은 기간 동안 GC 사이클이 발생하지 않았음
    • 특징
      • Log4j 2.6에서 성능이 향상된 비결은, 각 로그 메시지마다 임시 객체를 생성했던 로직을 객체를 재사용하는 방향으로 수정한 것 (오브젝트 풀 패턴 적용)
      • Log4j 2.6은 ThreadLocal 필드를 이용해 스트림 -> 바이트 변환 시 버퍼를 재사용하는 식으로 객체를 재사용함
    • 단점
      • ThreadLocal 객체는 웹 컨테이너에서 문제가 될 수 있음
        • ThreadLocal 객체 : Java에서 각 스레드가 독립적으로 값을 저장하고 관리할 수 있게 함
        • 문제점
          • 스레드 풀의 사용
            • 웹 컨테이너는 스레드 풀을 사용해 클라이언트 요청을 처리하고, 요청이 올 때마다 스레드 풀의 스레드를 재사용함
            • ThreadLocal 객체 사용 시 해당 스레드에 저장된 데이터가 명시적으로 제거되지 않는 한, 새로운 요청이 이전 요청의 데이터를 볼 수 있게 됨
          • 메모리 누수
            • ThreadLocal에 저장된 객체는 명시적으로 제거되지 않으면 스레드가 생존하는 동안 메모리에서 제거되지 않음
      • 특히, 웹 어플리케이션과 웹 컨테이너 사이에 로드/언로드하는 시점이 문제가 됨
      • Log4j 2.6은 웹 컨테이너 내부에서 실행 시 ThreadLocal을 안 쓰지만, 성능 향상을 도모하고자 일부 공유된/캐시된 구조체를 사용함

     

    14.3 리얼 로직 라이브러리를 이용해 지연 줄이기

    • 리얼 로직(Real Logic)은 저수준 세부의 이해가 고성능 설계에 영향을 미친다는 기계 공감 접근 방식을 주장한 마틴 톰슨이 설립한 영국 회사
    • 리얼 로직 깃허브 페이지 에는 다양한 유명 오픈소스 프로젝트가 존재함
      • 아그로나(agrona) : 자바용 고성능 자료구조 및 유틸리티 메서드
      • 단순 바이너리 인코딩(simple-binary-encoding) : 고성능 메시지 코덱(codec)
      • 에어론(aeron) : 효율적/안정적인 UDP 유니캐스트, UDP 멀티캐스트, IPC 메시지 전송
      • 아티로(artio) : 탄력적인 고성능 FIX 게이트웨이
    • 이 책에서 해당 부분을 언급한 이유는 성능을 최대한 얻어내고자 라이브러리가 이 정도로 세부 수준까지 파고들었다~ 기존 자바의 성능보다 개선된 형태의 구현이 필요할 때 잘 구현된 오픈소스 프로젝트를 먼저 찾아보자~ 를 말하고 싶어서 인 듯함

     

    14.3.1 아그로나

    Buffer

    • 기존 자바 버퍼의 문제점
      • 자바에는 다이렉트/논다이렉트 버퍼를 추상화한 ByteBuffer 클래스가 있음
      • 다이렉트 버퍼는 자바 힙 밖에 있기 때문에 온-힙(on-heap) 버퍼(논다이렉트 버퍼)보다 할당/해제율은 낮은 편임
      • 다이렉트 버퍼의 장점은 중간 단계의 매핑 없이 직접 구조체에 명령어를 실행하는 것
      • ByteBuffer는 일반화한 유스케이스가 가장 큰 문제로, 버퍼 타입별로 최적화를 적용할 수 없음
        • 아토믹 연산을 지원하지 않아 생산자/소비자 방식의 버퍼 구축 시 제약 발생
        • 매번 다른 구조체를 감쌀 때마다 하부 버퍼를 새로 할당해야 함
    • 아그로나 방식
      • 복사를 지양하며 저마다 독특한 특성을 지닌 버퍼 4가지 지원함
      • 각 버퍼 객체마다 있음 직한 상호작용을 정의/제어할 수 있음
        • DirectBuffer 인터페이스 : 버퍼에서 읽기만 가능하며 최상위 상속 계층에 위치
        • MutableDirectBuffer 인터페이스 : DirectBuffer를 상속하며 버퍼 쓰기도 가능
        • AtomicBuffer 인터페이스 : MutableDirectBuffer를 상속하며 메모리 액세스 순서까지 보장
        • UnsafeBuffer 클래스 : Unsafe를 이용해 AtomicBuffer를 구현한 클래스

    아그로나 버퍼 클래스의 상속 계층도

     

    List, Map, Set

    • 아그로나 ArrayListUtil을 이용하며 리스트 순서는 안 맞지만 ArrayList에서 신속하게 원소를 제거할 수 있음
    • 아그로나 Map, Set 구현체는 키/값을 해시 테이블 자료 구조에 나란히 저장함
    • 키가 충돌하며 다음 값은 해시 테이블의 해당 위치 바로 다음에 저장됨. 동일한 캐시 라인에 있는 기본형 매핑을 재빠르게 액세스할 때 적합한 자료구조임

     

    Queue

    • 아그로나의 동시성 패키지에 있음
    • java.util.Queue 인터페이스를 준수하므로 표준 큐 구현체 대신 쓸 수 있고, 순차 처리용 컨테이너 지원 기능이 부가된 org.agrona.concurrent.Pipe 인터페이스도 함께 구현되어 있음
      • Pipe는 원소를 카운팅하고, 수용 가능한 최대 원소 개수를 반환하고, 원소를 비우는 작업을 지원하므로 큐를 소비하는 코드와 원활하게 상호작용할 수 있음
    • 큐는 모두 락-프리하고 Unsafe를 사용하므로 저지연 시스템에 적합함
      • 큐 자료구조 구현할 때 락-프리 메커니즘과 Unsafe 클래스를 사용해 높은 성능을 달성함
    • 구현체는 아래 3가지. 이런 식으로 구현체를 분리하면 필요할 때에만 코드에서 조정하며 쓸 수 있음

     

    1) OneToOneConcurrentArrayQueue

    • 하나의 생산자, 하나의 소비자는, '유일한 동시 액세스는 생산자, 소비자가 자료구조에 동시 액세스할 때에만 발생한다'는 것을 가정함
    • 즉, 생산자와 소비자가 1개씩이므로 한번에 하나의 스레드에 의해서만 큐의 헤드, 테일의 위치가 업데이트됨
    • 헤드는 큐에서 poll() or drain() 할 때에만, 테일은 put() 할 때에만 업데이트 할 수 있음
    • 이 모드를 선택하면 아래 2가지 큐에서 꼭 필요한, 부수적인 조정 체크를 하느라 쓸데없이 성능 누수를 유발할 일이 없음

     

    2) ManyToOneConcurrentArrayQueue

    • 다수의 생산자, 하나의 소비자
    • 생산자가 다수일 경우, 테일의 위치를 업데이트할 때 부가적인 제어 로직이 필요함 (다른 생산자가 업데이트했을 수도 있으므로)
    • while 루프에서 Unsafe.compareAndSwapLong을 사용하면 테일이 업데이트될 때까지 큐 테일을 안전하게, 락-프리하게 업데이트할 수 있음
    • 소비자는 하나이므로 소비자 쪽에는 이런 경합이 없음

     

    3) ManyToManyConcurrentArrayQueue

    • 생산자, 소비자가 모두 다수인 경우
    • 이 정도 수준으로 조정/제어하려면 CAS 연산이 성공할 때까지 compareAndSwap을 감싼 while 루프가 필요함
    • 셋 중 조정 과정이 가장 복잡하기 때문에 그만큼 안전이 보장돼야 할 경우에만 사용하는게 좋음

     

    RingBuffer

    • 프로세스 간 통신용 바이너리 인코딩 메시지를 교환하는 인터페이스
    • DirectBuffer를 이용해 메시지 오프-힙(off-heap) 저장소를 관리함
    • 아그로나에 내장된 링 버퍼 구현체는 OneToOneRingBuffer, ManyToOneRingBuffer 2가지
      • 쓰기 작업은 소스 버퍼를 전달받아 그 메시지를 별도의 버퍼에 써넣고, 읽기 작업은 메시지 핸들러의 onMessage() 메서드로 콜백됨
      • ManyToOneRingBuffer에서 여러 생산자가 쓰기하고 있는 상황에서 Unsafe.storeFence() 메서드를 호출하면 수동으로 메모리 동기화를 통제할 수 있음

     

    14.3.2 단순 바이너리 인코딩 (SBE)

    • 저지연 성능에 알맞게 개발된 바이너리 인코딩 방식으로, 금융 시스템에서 쓰이는 FIX 프로토콜에 특화되어 있음
    • SBE는 메시지를 인코딩/디코딩하는 어플리케이션 계층의 관심사이고, 버퍼는 아그로나에서 빌려 사용함
    • SBE는 GC를 유발하지 않고 메모리 액세스 같은 문제를 최적화하지 않고도 효율적인 자료구조를 통해 저지연 메시지를 전달할 수 있어 고빈도 거래 환경에 맞춤 설계되어 있음

     

    SBE의 설계 원리와 저지연 시스템 설계와의 연관성

    https://github.com/real-logic/simple-binary-encoding/wiki/Design-Principles

     

    1) 카피-프리

    • 카피-프리(복사 하지 않는) 기술은 중간 버퍼를 쓰지 않고 하부 버퍼에 바로 접근해 메시지를 인코딩/디코딩하도록 설계됨
    • 버퍼에 집어넣지 못할 정도로 큰 메시지는 지원할 수 없어 메시지를 나누어 다시 조립하는 프로토콜을 구축해야 함

     

    2) 네이티브 타입 매핑

    • 어셈블리 명령어에 네이티브하게 매핑되는 타입도 카피-프리하게 작업하는 게 좋음
    • 어셈블리 연산을 잘 선택해 매핑하면 필드 검색 성능이 향상됨

     

    3) 정상 상태 할당

    • 하부 버퍼에 플라이웨이트 패턴을 사용하여 할당-프리(allocation-free)함
      • 객체들이 공유할 수 있는 데이터를 사용해 불필요한 메모리 할당을 줄이거나 없애 메모리 효율성을 높였음

     

    3) 스트리밍/단어 정렬 액세스

    • 자바 배열은 보통 레퍼런스 배열 형태로 메모리 순차 읽기가 불가능함
    • SBE는 메시지를 진행 방향으로 인코딩/디코딩하도록 설계되어 있어 정확하게 단어를 정렬할 수 있는 틀이 잡혀 있음

     

    14.3.3 에어론

    • SBE와 아그로나에 기반한 툴
    • 자바 및 C++ 용도로 개발된, UDP(User Datagram Protocol) 유니캐스트, 멀티캐스트, IPC(프로세스간 통신) 메시지를 전송하는 수단
    • 어플리케이션이 같은 머신에서, 또는 네트워크를 넘나들며 IPC를 통해 서로 소통할 수 있게 해주는 것들을 망라한, 일반적인 메시지 프로토콜
    • 지연을 예측 가능한 방향으로 가장 낮게 유지하는 것을 목표로 함
    • 처음부터 컴포넌트 라이브러리 형태로 제작되어 저수준 자료구조만 필요한 경우 다른 디펜던시를 갖다 쓰지 않고 아그로나에 탑재된 기능을 그대로 이용하면 됨

     

    에어론의 구성 컴포넌트

    • 미디어(media) : 에어론이 통신하는 매개체 (ex: UDP, IPC 등)
    • 미디어 드라이버(media driver) : 미디어와 에어론 사이의 연결 통로. 원하는 전송 구성을 세팅해 통신할 수 있음
    • 감독자(conductor) : 전체 흐름을 관장함
      • 버퍼를 설정하거나 새 구독자/발행자 요청을 리스닝, NAK(부정 응답 문자)를 감지해 재전송 준비하기 등의 일 수행
      • 송신자/수신자는 바이트를 주고받는 일만 집중해서 최대 처리율을 높일 수 있음
    • 송신자(sender) : 생산자로부터 데이터를 읽어 소켓으로 전송함
    • 수신자(receiver) : 소켓에서 데이터를 읽고 해당 채널/세션으로 내보냄

     

    14.3.4 에어론의 설계 개념

    전송 요건

    • 에어론은 OSI4 전송 계층에서 메시징 하므로 반드시 준수해야 할 요건들이 있음
      • 정렬 : 전송 계층은 저수준에서 순서 없이 무작위로 패킷을 받기 때문에 순서가 뒤섞인 메시지는 다시 정렬해야 함
      • 신뢰성 : 유실된 데이터는 재전송을 요청해야 함
      • 배압 : 부하가 높아지면 구독자는 압박을 받음. 따라서 흐름 제어 및 배압 측정 서비스가 지원돼야 함
      • 혼잡 : 네트워크 포화 시 혼잡이 일어날 수 있고, 에어론은 혼잡 제어 기능을 옵션으로 제공함
      • 다중화 : 전체 성능 떨어뜨리지 않고 단일 채널에서 다중 정보 스트림을 처리할 수 있어야 함

     

    지연 및 어플리케이션 원칙

    • 에어론은 8개 설계 원칙을 따름
      • 정상 상태에서 가비지-프리 실현
        • GC 중단 방지를 위해 정상 상태를 보장하도록 설계되어 있어 이와 동일한 설계 결정을 따르는 어플리케이션에 에어론을 포함시킬 수 있음
      • 메시지 경로에 스마트 배칭 적용
        • 스마트 배칭은 수신 메시지가 폭주하는 상황을 감안해 설계된 알고리즘
        • 메시지 처리 도중 다른 메시지가 들어오면 용량이 허락하는 한도 내에서 같은 네트워크 패킷 안에 집어넣을 수 있음
        • 적절한 자료구조를 이용해 생산자가 공유 리소스에 쓰는 걸 지연시키지 않고 이렇게 배칭을 수행함
      • 메시지 경로의 락-프리 알고리즘
      • 메시지 경로의 논블로킹 I/O
      • 메시지 경로의 비예외 케이스
        • 특이 케이스 뿐 아니라 기본 시나리오의 실행 속도 신경써야 함
      • 단일 출력기 원칙 적용
        • 다중 출력기는 큐 액세스를 고도로 정교하게 제어/조정하는 작업이 수반됨
        • 단일 출력기를 쓰면 이 과정이 단순해지고 쓰기 경합이 줄어듦
      • 공유 안하는 상태가 더 좋다
        • 단일 출력기는 큐의 경합 문제를 해결하지만 가변 데이터를 공유해야 하는 문제를 유발하기 때문에 데이터는 프라이빗 또는 로컬 상태를 유지해야 함
      • 쓸데없이 데이터를 복사하지 말라
        • 복사를 줄여 우발적인 메모리 변경을 방지해야 함

     

    내부 작동 원리

    • 에어론은 단순한 방식으로 자료구조에 메시지 시퀀스를 생성함
    • 파일 개념을 폭넓게 활용함
      • 파일은 서로 연관된 프로세스끼리 공유 가능한 매개체
      • 리눅스의 메모리 맵 파일 아키텍처 덕분에 모든 파일 호출은 실제 물리적인 파일을 쓰는 게 아닌 메모리로 전달됨

     

    • 테일 포인터는 최종 메시지가 쓰인 지점을 찾아가는 용도로 사용됨
    • 위의 사진은 현재 파일에 헤더를 지닌 단일 메시지를 쓰는 장면
    • 이벤트 시퀀싱 과정
      • 테일 포인터는 파일 내부에 메시지 공간을 예약함
      • 테일 증분 작업은 아토믹하므로 출력기는 자기 영역의 처음과 끝이 어디인지 알고 있음
      • 덕분에 다중 출력기가 락-프리하게 파일을 업데이트할 수 있고 파일 쓰기 프로토콜을 효율적으로 작동시킬 수 있음

     

    14.4 마치며

    • 로깅
      • 로깅은 필수 요소이지만 어떤 종류의 로거를 사용할지에 따라 전체 어플리케이션 성능이 영향을 받을 수 있음
      • 로깅은 어플리케이션을 전체적으로 바라보며 스레드 사용, 가비지 수집 등의 다른 JVM 서브시스템에 미칠 영향까지 감안하여 설계해야 함
    • 메시징
      • 저지연 시스템에서는 가장 저수준의 큐부터 고수준의 소프트웨어 스택에 이르기까지 그 목표와 원리가 일관되게 적용돼야 함
      • 저지연 시스템을 새로 구축할 때, 잘 만들어진 오픈소스 프로젝트를 고려해보자
    • 자바/JVM은 추상화가 잘된 언어라서 저지연, 고처리율 어플리케이션을 개발하기 위해 관리를 잘 해야 하고 문제를 우회해야 함
    • 하드웨어, JVM 성능, 저수준의 관심사도 반드시 고려해야 함

    'DevBook > Optimizing Java' 카테고리의 다른 글

    CH10. JIT 컴파일의 세계로  (0) 2024.06.24
    CH2. JVM 이야기  (0) 2024.04.14
    CH1 성능과 최적화  (0) 2024.04.09

    댓글

Designed by Tistory.