Post

고성능 로깅 및 메시징

14. 고성능 로깅 및 메시징

14.1 로깅

제품급 로깅 시스템을 선정 시 바람직하지 않은 안티 패턴

  • 10년짜리 로거: 누군가 이미 로거를 잘 설정해놨다. 뭐 하러 다시 만드나? 그냥 편하게 갖다 쓰면 되지
  • 프로젝트 전체 로거: 누군가 프로젝트 각 파트마다 다로 로거를 재구성하지 않아도 되게끔 로거를 감싸놓았다.
  • 전사 로거: 누군가 전사적으로 사용 가능한 로거를 만들었다.

이렇게 대충 판단하고 방관하다가 전체 조직에 고루 영향을 미치는 기술 부채(technical debt)로 이어지기도 함

대부분의 고성능 환경에서는 처리 정확도와 리포팅이 속도만큼이나 중요함

로그는 운연팀이 이슈의 단서를 찾는 데 도움이 되고 사후 조사를 할 수 있을 정도로 충분한 로그를 남겨야 함

14.1.1 로깅 벤치마크

가장 많이 쓰는 세 로거(Logback, log4j, java.util.logging) 비교

같은 코드를 여러 가지 애플리케이션에서 실행할 경우, 마이크로벤치마킹을 하면 그 코드가 얼마나 많은 성능을 내눈지 추정 가능

로깅 없음

로깅 없음은 현재 로거가 켜져 있고 어떤 한계치 이하로 메시지가 로깅되고 있느 상태에서 무동작 로그의 비용을 측정하는 벤치마크 테스트

Logback 포맷

1
14:18:17.365 [Name Of Thread] INFO c.e.NameOfLogger - Log meesage

Logback 1.2.1 버전을 사용한 벤치마크

java.util.logging 포맷

1
2
Feb 08, 2017 2:00:19 PM com.example.NameOfLogger nameOfThread
INFO: Log message

Log4j 포맷

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

측정

아이맥 벤치마크 결과를 보면 Logback이 전반적으로 성능이 가장 우수하고 로깅 포맷이 Log4j일 때 최고

AWS 벤치마크 결과를 보면 Logback이 Log4j보다 약간 더 빠르게 나옴

로거 결과

대체로 Logback이 성능이 가장 좋았고, 자바 유틸 로거가 제일 나쁨

실제 시스템에서는 결과 수치가 엇비슷할 경우, 운영 장비에서 직접 실행 성능을 테스트해보는 것이 좋다

로깅 프레임워크가 생성하는 엄청난 양의 가비지도 잘 따져보아야 함

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

로깅은 모든 애플리케이션의 필수 컴포넌트이지만, 저지연 애플리케이션에서 로거는 비즈니스 로직 성능에 병목 현상을 초래해선 안 됨

log4j 2.6 버전은 정상 상태의 가비지-프리한 로거로 해결하는 것을 목표로 출시

log4j 2.6에서 성능이 향상된 비결은, 각 로그 메시지마다 임시 객체를 생성했던 로직을 객체를 재사용하는 방향으로 수정한 것

객체 풀 패턴(object pool pattern, 필요한 객체를 바로 생성하지 않고 풀에 요청을 해서 반환받는 식으로 작업을 수행하는 패턴)을 실천

Log4j 2.6은 ThreadLocal 필드을 이용해 스트링 -> 바이트 변환 시 버퍼를 재사용하느 식으로 객체를 재사용

ThreadLocal 객체는 웹 컨테이너에서 문제가 될 수 있음

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

리얼 로직(real logic)은 저수준 세부의 이해가 고성능 설계에 영향을 미친다는 기계 공감 접근 방식을 주장

14.3.1 아그로나

저지연 애플리케이션 전용 구성 요소를 담아놓은 라이브러리

버퍼

자바에는 다이렉트/논다이렉트 버퍼를 추상화한 ByteBuffer 클래스가 있음

다이렉트 버퍼는 자바 힙 밖에 있기 때문에 온-힙 버퍼보다 할당/해제율은 낮은 편

다이렉트 버퍼의 잘점은 중간 단계의 매핑 없이 직접 구조체에 명령어를 실행하는 것

ByteBuffer는 버퍼 타입별로 최적화를 사용할 수 없다

ByteBuffer는 아토믹 연산을 지원하지 않으므로 생산자/소비자 방식의 버퍼를 구축할 때 제약이 따름

아그로나는 복사를 지양하며 저마다 독특한 특성을 지닌 버퍼를 네 가지 지원

  • DirectBuffer 인터페이스: 버퍼에서 읽기만 가능하며 최상위 상속 계층에 위치
  • MutableDirectBuffer: DirectBuffer를 상속하며 버퍼 쓰기도 가능
  • AtomicBuffer: MutableDirectBuffer를 상속하며 메모리 액세스 순서까지 보장
  • UnsafeBuffer: Unsafe를 이용해 AtomicBuffer를 구현한 클래스

아그로나 버퍼를 이용하면 다양한 get 메서드를 통해 하부 데이터를 가져올 수 있음

버퍼 타입은 어느 한 가지로 고정된 게 아니고, 가장 적합한 자료 구조를 선택/관리하는 일은 개발자의 몫

리스트, 맵, 세트

자바는 배열 안에서 객체를 나란히 배치하는 장치가 따로 없으며, 표준 컬렉션의 결과물은 항상 레퍼런스의 배열

표준 컬렉션에서 기본형 아닌 객체를 사용하라고 강요하다 보니 객체 자체의 크기 오버헤드도 있지만 자동박싱/언박싱도 발생

아그로나 ArrayListUtii을 이용하면 리스트 순서는 안 맞지만 신속하게 원소를 제거 가능

아그로나 맵, 세트 구현체는 키/값을 해시 테이블 자료 구조에 나란히 저장

키가 충돌하면 다음 값은 해시 테이블의 해당 위치 바로 다음에 저장됨

아그로나의 동시성 패키지에는 큐, 링 버퍼를 비롯해 쓸만한 자료 구조 및 동시성 유틸리가 있다.

아그로나 큐는 표준 인터페이스를 준수하므로 표준 큐 구현체 대신 쓸 수 있고, 순차 처리용 컨테이너 지원 기능이 부가된 org.agrona.concurrent.Pipe 인터페이스도 함께 구현되어 있다.

생산자, 소비자가 큐를 동시에 액세스할 경우 잘못된 공유를 방지하고자 큐 메모리를 영리하게 배치

  • OneToOneConcurrentArrayQueue

    ‘유일한 동시 액세스는 생산자, 소비자가 자료구조에 동시 액세스할 때에만 발생함’

    한번에 하나의 스레드에 의해서면 업데이트되는 해드, 테일의 위치

  • ManyToManyConcurrentArrayQueue

    생산자다 다수일 경우에는, 테일 위치를 업데이트할 때 부가적인 제어 로직이 필요

    while 루프에서 Unsafe.compareAndSwapLong을 사용하면 꼬리가 업데이트될 때까지 큐 테일을 안전하게, 락-프리하게 업데이트 가능

  • ManyToOneConcurrentArrayQueue

    생산자, 소비자가 모두 다수일 경우, 머리/테일 양쪽을 업데이트해야 함

링 버퍼

프로세스 간 통신용 바이너리 인코딩 메시지를 교환하는 인터페이스

RingBuffer는 DirectBuffer를 이용해 메시지 오프-힙 저장소를 관리

아그로나에 내장된 링 버퍼 구현체는 OneToOneRingBuffer, ManyToOneRingBuffer

쓰기 작업은 소스 버퍼를 전달받아 그 메시지를 별도의 버퍼에 써 넣는 반면, 읽기 작업은 메시지 핸들러의 onMessage() 메서드로 콜백됨

ManyToOneRingBuffer에서 여러 생산자가 쓰기하고 있는 상황에서 Unsafe.storeFence() 메서드를 호출하면 수동으로 메모리 동기화를 통제 가능

storeFence()는 ‘펜스를 치기 전의 스토어를, 펜스 친 이후의 로드 또는 스토어와 순서를 바꾸지 못하게 하는’ 메서드

14.3.2 단순 바이너리 인코딩

단순 바이너리 인코딩(SBE)는 저지연 성능에 알맞게 개발된 바이너리 인코딩 방식

SBE는 메시지를 인코딩/디코딩하는 애플리케이션 계층의 관심사

SBE는 GC를 유발하지 않고 메모리 액세스 같은 문제를 치적화하지 않고도 효율적인 자료 구조를 통해 저지연 메시지를 전달 가능

카피-프리, 네이티브 타입 매핑

복사는 비용이 듬

SBE의 카피-프리(복사를 하지 않는) 기술은 중간 버퍼를 쓰지 않고 메시지를 인코딩/디코딩하도록 설계됨

정상 상태 할당

SBE는 하부 버퍼에 플라이트웨이트 패턴(flyweight pattern, 동일하거나 유사한 객체들 사이에 가능한 많은 데이터를 서로 공유하여 사용하도록 하여 메모리 사용량을 최소화하는 소프트웨어 디자인 패턴)을 사용하므로 할당-프리

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

SBE는 메시지를 진행 방향으로 인코딩/디코딩하도록 설계되어 있어서 정확하게 단어를 정렬할 수 있는 틀이 잡혀 있다

SBE 써보기

SBE 메시지는 메시지 레이아웃을 특정한 XML 스키마 파일로 나타냄

14.3.3 에어론

에이론은 SBE와 아그로나에 기반한 툴

에어론은 자바 및 C++ 용도로 개발된, UDP(User Datagram Protocol) 유니캐스트, 멀티캐스트, IPC(Inter-Process Communication) 메시지를 전송하는 수단

최고의 처리율을 지향하는 에어론은 지연을 예측 가능한 방향으로 가장 낮게 유지하는 것을 목표함

굳이 새로운 걸 만든 이유는?

시장에 나와 있는 제품들이 점점 더 일반화되기 때문

발행자

  • 미디어: 에어론이 통긴하는 매개체(ex. UDP, IPC, 인피니밴드 등). 클라이언트 에오린이 이 매체들을 모두 추상화함
  • 미디어 드라이버: 미디어와 에어론 사이의 연결 통로
  • 감독자(conductor): 전체 흐름을 관장, 버퍼를 설정하거나 새 구독자/발행자 요청을 리스닝하는 등의 일을 함
  • 송신자(sender): 생산자로부터 데이터를 읽어 소켓으로 전송
  • 수신자(receiver): 소켓에서 데이터를읽고 해당 채널/세션으로 내보냄

보통 미디어 드라이버는 메시지 송수신에 사용하는 버퍼를 제공하는 별개 프로세스로 떠 있다

1
2
3
4
5
6
7
8
Final MediaDriver driver = MediaDriver.launch(); //임베디드 미디어 드라이버를 시작하는 코드
Final Aeron.Context ctx = new Aeron.Context(); // 설정값 구성

try(Publication publication = aeron.addPublication(CHANNEL, STREAM_ID)) { // publication에 접속해서 주어진 채널/스트림으로 통신
  ...
  Final long result = publication.offer(BUFFER, 0, messageByutes.length); // 발행자에게 버퍼를 제공하고 그 결뫄 메시지 상태값을 반환받음
}

구독자

1
2
3
4
5
6
7
// 메시지 수신 시 작동시킬 콜백 함수를 등록
Final FragmentHandler fragmentHandler = SamplesUtil.printStiringMessage(STREAM_ID);

try(Aeron aeron = Aeron.connect(ctx);
   Subscription subscription = aeron.addSubscription(CHANNEL, STREAM_ID)) {
  	SamplesUtil.subscriberLoop(fragmentHandler, FRAGMENT_COUNT_LIMIT, running).accept(subscription);
}

14.3.4 에어론의 설계 개념

전송 요건

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

지연 및 애플리케이션 원칙

  • 정상 상태에서 가비지-프리 실현: 에어론은 GC 중단을 방지하기 위해 정상 상태를 보장하도록 설계되어 있기 때문에 이와 동일한 설계 결정을 따르는 애플리케이션에 에어론을 포함 가능
  • 메시지 경로에 스마트 배칭 적용: 스마트 배칭(Smart Bathcing)은 수신 메시지가 폭주하는 상황을 감안하여 설계된 알고리즘. 에어로은 적절한 자료 구조를 이용해 생산자가 공유 리소스에 쓰는 걸 지연시키지 않고도 이렇게 배칭 수행
  • 메시지 경로의 락-프리 알고리즘
  • 메시지 경로의 논블로킹 I/O
  • 메시지 경로의 비예외 케이스
  • 단일 출력기 원칙을 적용: 다중 출력기는 큐 액세르르 고도로 정교하게 제어/조정하는 작업이 수반됨. 에어론 발행 객체는 스레드-안전하면 다중 출력기르 지원. 그러나 구독자 객체는 구독하려는 스레드마다 하나씩 필요하므로 스레드-안전하지 않음
  • 공유 안 하는 상태가 더 좋다
  • 쓸데없이 데이터를 복사하지 마라

내부 작동 원리

에어론은 기본적으로 복제된 영구 로그 메시지를 생성

에어론은 파일 개념을 폭넓게 활용

에어론은 기본적으로 tmpfs(파일처럼 마운팅된 휘발성 메모리)에 매핑

테일 포인터는 최종 메시지가 씌여진 지점을 찾아가는 용도로 사용

테일 포인터는 파일 내부에 메시지 공간을 예약

테일 증분 작업은 아토믹하므로 출력기는 자기 영역의 처음과 끝이 어디인지 알고 있음

덕분에 다중 출력기가 락-프리하게 파일을 업데이트할 수 있고 파일 쓰기 프로토콜을 아주 효율적으로 작동 가능

헤더는 제일 마지막으로 아토믹하게 파일에 출력되므로 그 존재 여부로 메시지가 완성됐음을 알 수 있다

파일은 쓰면 점점 커지기기만 함

방치하면 메모리 맵 파일에서 페이지 폴트같은 별의별 문제 발생

이 문제는 파일을 액티브, 더티, 클린 세 파일로 두어 해결

액티브는 현재 쓰고 있는 파일, 더티는 이전에 씌여진 파일, 클린은 바로 다음에 쓸 파일

어떤 경우에도 메시지는 여러 파일을 넘나들 수 없다.

메시지 헤더 자체에 메시지 순서를 이용해 누락된 메시지 처리

일체의 간극이나 다른 자료 구조 없이 계속 증가하는 메시지 시리즈를 만들 수 있다

워터마크는 최종 수신 메시지의 현재 위치

워터마크 및 테일이 일정 기간 동안 상이하다면 그건 메시지가 누락됐음을 의미

모든 수신 메시지가 각 메시지의 바이트를 식별하는 고유한 방식으로 가진다

이 고유한 방식과 아카이브를 결합하면 역사상 모든 메시지를 유일하게 식별 가능

로그파일은 속도 및 상태를 유지하는 에어론의 핵심 기능으로, 단순하고 우아하게 실행하도록 설계됨

참조

  1. Optimizing Java(자바 최적화)
  2. https://github.com/stephenc/java-logging-benchmarks
This post is licensed under CC BY 4.0 by the author.