Skip to content

⚙️ Refactoring: 로그 저장을 한번에 하자!

Kim Minju edited this page Sep 5, 2024 · 21 revisions

Note

해당 개선은, Server측의 버퍼링만 적용된 상황으로 Client는 매 요청당 1개의 로그만 보내는 상황이기에 이후의 RPS보다는 현저하게 높게 나온 경향이 있습니다.

수많은 로그 저장 요청으로, 어플리케이션 내의 커넥션 풀이 부족하기 시작했습니다.

image

이를 해결하기 위해, 단순히 커넥션 풀을 증가하는 방법도 가능하겠지만, 근본적인 해결책이 되기에는 어려움이 있을 것이고,

커넥션 풀을 조금 더 효과적으로 쓸 수단이 필요하다고 생각했습니다. 따라서 로그를 저장하는 DB 요청을 N개씩 모아서 Bulk 처리하는 방향으로 리팩토링하고자 했습니다.

구조 설계하기

의도하는 동작 방식은 외부에서 온 로그 저장 요청이 Queue에 저장되고, Queue에서 임의의 Bulk Size만큼 모이면 꺼내서 DB에 저장 요청을 보내는 방식을 의도했습니다.

구상도 image

큐는 LinkedBlockingQueue를 이용하여 큐가 비어있을 때에도 busy waiting하지 않고, thread가 wait할 수 있도록 구상했습니다.

이 때, Queue에서 Bulk Size만큼 꺼내 DB에 저장요청을 하는 부분에 대한 설계가 필요했습니다.

1. Worker ThreadPool에서 Queue를 참조하여 Bulk만큼 꺼내가도록 반복하는 구조

Worker Thread들이 Queue를 참조하며 BulkSize만큼 DB에 저장요청을 보내도록 구상했습니다.

구조 image

하지만 이 구조는 여러 Worker Thread가 Queue에 대해 경쟁하게 되는 것으로 인해 부하가 생길 것으로 판단했습니다.

2. Queue를 참조하는 스레드는 1개로만 유지하고, Worker ThreadPool에게 저장 요청을 위임하는 구조

WAS 과제를 하는 도중, main 스레드에서 accept이후 만들어진 Socket을 ThreadPool을 통해 수행하는 방법에서 아이디어를 얻어

Queue를 확인하는 작업은 별도의 Single Thread에서만 하게 한 후, 실제 저장 작업은 Worker ThreadPool을 통해 수행하고자 했습니다.

구조 image

이처럼 설계하면 LinkedBlockingQueue내에서 queue가 비어있으면 await하기 때문에 busy waiting할 일이 없고, 의미없는 스레드간의 경쟁도 사라지기 때문에 이 방법으로 선택했습니다.


구현

Controller에서 Queue에 저장하기

image

Worker Thread 정의

image

Leader Thread 작업 정의 (Single Thread)

image

위 부분은 가장 최근 PR에서 bulk Size만큼 wait하는 코드로 변경됨에 따라 deprecated되었으나, 예시를 위해 남겨두었습니다. 리더 스레드는 queue를 참조하여 bulk size 이상인 경우 Log를 가져와서, followerExecutor(Worker ThreadPool)에 넘겨 저장을 위임하는 구조입니다.

부하 테스트

이 시기(MVP1)에는 람다로 동시에 어느정도의 순간 부하를 견딜 수 있는 지를 중점적으로 테스트했습니다.

기존 구현 + 람다 100개 * 개당 100회 요청

환경 설정

  • lambda 테스트 (인스턴스: 100, 반복횟수: 100)
  • HikariCP 10개 고정
image

결과

  • Connection Pool TimeOut

Bulk Insert 적용 + 람다 100개 * 개당 100회 요청

환경 설정

  • DB Insert를 Bulk 처리로 개선함
  • lambda 테스트 (인스턴스: 100, 반복횟수: 100)
  • HikariCP 10개 고정

결과

기존 DB Connection Pool이 부족하던 문제가 해결되어 모든 요청을 받아냄.


Bulk Insert 적용 + 람다 200개 * 개당 100회 요청

환경 설정

  • DB Insert를 Bulk 처리
  • lambda 테스트 (인스턴스: 200, 반복횟수: 100)
  • HikariCP 10개 고정

결과

  • 동시 요청 수가 많아져서 TPS가 소폭 상승했고, 평균 averageTime이 증가함
  • 20000개의 요청을 전부 받아냄

Bulk Insert 적용 + 람다 300개 * 개당 100회 요청

환경 설정

  • DB Insert를 Bulk 처리
  • lambda 테스트 (인스턴스: 300, 반복횟수: 100)
  • HikariCP 10개 고정

결과

  • 동시 요청 수가 많아져서 TPS가 소폭 상승했고, 평균 averageTime은 계속해서 증가함
  • 30000개의 요청을 전부 받아냄

Bulk Insert 적용 + 람다 400개 * 개당 100회 요청

환경 설정

  • DB Insert를 Bulk 처리
  • lambda 테스트 (인스턴스: 400, 반복횟수: 100)
  • HikariCP 10개 고정

결과

  • 동시 요청 수가 많아졌지만 TPS는 오히려 감소했고, 평균 averageTime이 대폭 증가함
  • 40000개의 요청이 손실되지는 않음

결론

Bulk Insert를 적용한 결과, 람다의 동시 요청 수가 증가함에도 불구하고 DB Connection Pool의 부족 문제를 해결하고 모든 요청을 성공적으로 처리할 수 있었습니다. 테스트 결과, 각 환경에서 TPS가 증가하며, 평균 처리 시간이 다소 상승했지만, 전체적으로 시스템이 더 많은 부하를 처리할 수 있게 되었음을 확인했습니다.

앞으로 나아가야 할 점들

  • Connection Pool 최적화: HikariCP의 크기를 10으로 고정한 상태에서 성능 테스트를 진행했지만, 요청 수가 증가함에 따라 Connection Pool의 크기를 동적으로 조정하거나 최적의 Pool 크기를 찾아내는 작업이 필요합니다.

  • TPS와 Average Time 개선: 람다 인스턴스 수가 증가할수록 TPS가 일정 수준에서 감소하고, 평균 처리 시간이 급격히 증가하는 현상이 관찰되었습니다. 이는 시스템의 한계점에 도달했음을 시사하므로, 더 높은 TPS를 유지하면서 평균 처리 시간을 줄일 수 있는 최적화 방안을 모색해야 합니다.

  • 안정성 : 현재까지는 요청이 손실되지 않았지만, 부하가 더욱 증가할 경우 시스템의 안정성이 저하될 수 있다는 것을 고려한 설계가 필요할 것으로 생각됩니다.

Clone this wiki locally