배치 프로그램 튜닝

devty·2023년 9월 4일
2

서론

도입하게 된 계기

  • 사내에서 사용하는 배치로 돌리는 작업이 기존에 4시간이 걸리는 작업이었다.
  • 4시간 동안 해당 서버는 그 배치로 인해 다른 작업이 불가능 상태이고 4시간은 너무 긴 시간으로 파악되어, 4시간에서 최소 1시간으로 줄여보기 위해 작업을 시작하게 되었다.

본론

배치 프로그램을 튜닝하기 위한 선택지

  1. 데이터 베이스 최적화
    • 데이터베이스 조회 최적화를 통해 배치 프로그램의 속도를 향상시킬 수 있다. 이러한 최적화에는 적절한 인덱싱, 쿼리 최적화 등이 포함된다.
  2. 코드 최적화
    • 병목 지점을 찾아내어 코드를 최적화해야한다. 종종 몇 가지 작은 변경만으로도 큰 성능 향상을 볼 수도 있다.
  3. 병렬 처리(멀티 스레드)
    • 가능하면 작업을 여러 부분으로 나누고 병렬로 실행해야한다. 이러한 방식은 멀티 코어나 멀티 스레드 환경에서 특히 효과적입니다.
  4. 비동기 처리
    • I/O 바운드 작업을 비동기 방식으로 수행하여 CPU 시간을 더 효율적으로 활용해야한다.
  5. 메모리 최적화 등이 존재하고 있다.
  6. 위 선택지 중 우리가 중점적으로 봐야할 부분은 병렬 처리(멀티 스레드)와 I/O 바운드이다.

CPU Bound vs I/O Bound

  • CPU Bound는 CPU의 연산 능력에 크게 의존한다. 이러한 프로그램은 주로 계산 집약적인 작업을 수행하며, 최적화되지 않은 알고리즘이나 복잡한 연산 과정이 포함될 수 있다.
    • 예를 들어 머신러닝에서 주로 사용한다.
  • I/O Bound 프로그램은 입력/출력 연산에 의존한다. 이러한 프로그램은 파일 읽기/쓰기, 네트워크 통신, 데이터베이스 쿼리 등의 작업을 수행하며, I/O 연산의 지연 시간이 프로그램의 전체 실행 시간에 큰 영향을 미친다.
    • 예를 들어 웹 서버에서 주로 사용한다.
    • CPU 성능보다 타 시스템과의 병목 부분(I/O Wating)에 큰 영향을 받기 때문에 스레드 개수를 늘리거나 동시성을 활용한다. 따라서 성능 향상을 위해 scale-out을 주로 사용한다.

병렬 프로그래밍 방법

  1. Multiprocessing (멀티프로세싱)
    • 정의 : 멀티프로세싱은 여러 개의 독립적인 프로세스(도서관)를 이용하여 각각 별도의 작업(책 읽기)을 수행하는 방식이다. 각 프로세스는 독립적인 메모리 공간(도서관)과 자원(책)을 가지고 작업을 수행하며, 한 프로세스에서 일어나는 일이 다른 프로세스에 영향을 미치지 않는다.
    • 비유 : 10개의 도서관, 10명의 독자, 10권의 책
    • 교착상태를 만들지는 않지만 리소스적인 측면에서 비용이 많이듬.
  2. Multithreading (멀티스레딩)
    • 정의 : 멀티스레딩은 한 프로세스(도서관) 내에서 여러 개의 스레드(독자)가 동시에 여러 작업(책 읽기)을 수행하는 방식이다. 모든 스레드는 메모리 공간(도서관)과 자원(책)을 공유하며, 스레드간의 협력과 자원 관리가 중요한 요소이다.
    • 비유 : 1개의 도서관, 10명의 독자, 10권의 책
    • 교착상태를 만들수도 있음.
  3. Async IO (비동기 입출력)
    • 정의 : 비동기 입출력은 한 프로세스(도서관)에서 한 스레드(독자)가 여러 작업(책 읽기)을 동시에 수행할 수 있는 방식이다. 이 스레드는 작업간에 대기 시간이 발생할 때, 다른 작업을 수행하며, 이를 통해 I/O 바운드 작업의 효율을 향상시킬 수 있다.
    • 비유 : 1개의 도서관, 1명의 독자, 10권의 책

병렬 프로그래밍 선택

  • 위 3가지중 나는 데이터 베이스에 관련된 배치 작업을 할 것이므로 I/O Bound 작업을 선택해줘야한다.
    • 멀티스레딩은 여러 스레드가 동시에 I/O 작업을 수행할 수 있어 대기 시간을 최소화할 수 있고, 비동기 입출력은 단일 스레드가 여러 I/O 작업을 효율적으로 관리할 수 있다.
    • 따라서, Multithreading (멀티스레딩)과 Async IO (비동기 입출력) 두가지 후보로 줄어들었다.
  • 나머지 두가지 중 배치작업에 API를 조회해 받은 Response을 DB에 적재해야하는 로직이 있는데, 해당 API 서버의 특성과 제한사항(예 : 동시 요청 수 제한)을 고려해야한다.
    • 만약 API 서버가 동시에 너무 많은 요청을 처리하지 못한다면, 스레드 또는 비동기 작업의 수를 적절히 조절해야 할 것이다.
    • 여러 스레드가 동시에 API 호출을 수행하고, 결과를 데이터베이스에 적재할 수 있어, 전체 배치 작업의 실행 시간을 줄일 수 있을 것 같아서 멀티 스레드를 사용하게 되었습니다.
    • 또한, 비동기 프로그래밍(Async IO)은 콜백, 프로미스, await/async 등 비교적 복잡한 프로그래밍 패턴을 사용할 수 있습니다. 반면, 멀티스레딩은 각 스레드가 독립적인 실행 흐름을 가지기 때문에 코드의 복잡성을 비교적 쉽게 관리할 수 있다.

코드는 어떻게 짜는가?

from concurrent.futures import ThreadPoolExecutor

def set_init_bldrgst(self):
    futures = []
    with ThreadPoolExecutor(max_workers = 10) as executor:
        for index, row in enumerate(query_job):
            stor_nm = row.STOR_NM
            stor_cd = row.STOR_CD
            addr = row.ADDR
            road_addr = row.ROAD_ADDR
            sigunguCd = row.SIGUNGUCD
            bjdongCd = row.BJDONGCD
            bun = row.BUN
            ji = row.JI
            future = executor.submit(self.BldRgst, stor_cd, stor_nm, addr, road_addr, sigunguCd, bjdongCd, bun, ji, index)
            futures.append(future)
    print('종료')

    for i, future in enumerate(futures):
        if not future.done():
            print("Unfinished task:", future)
  • 파이썬 코드에서는 멀티 스레딩을 지원해주는 라이브러리(ThreadPoolExecutor)가 있다.
  • with ThreadPoolExecutor(max_workers = 20) as executor:ThreadPoolExecutor 클래스의 인스턴스를 생성하며, 최대 20개의 스레드를 동시에 실행할 수 있도록 설정합니다. with 문은 컨텍스트 관리자를 사용하여, 작업이 완료되면 자원을 자동으로 정리해준다.
    • 즉, 스레드 풀을 종료한다,
  • future = executor.submit(self.BldRgst, stor_cd, stor_nm, addr, road_addr, sigunguCd, bjdongCd, bun, ji, index)self.BldRgst 함수를 비동기적으로 호출한.
  • if not future.done(): → 작업이 완료 되었는지 확인하기 위함.

스레드 풀 개수

  • 스레드 풀 개수 많이 늘린다고 능사는 아니다.
  • 왜냐하면 스레드 풀 개수를 무진장 늘린다고 가정한다면 생기는 여러가지 문제점이 있다.
    • 컨텍스트 스위칭 오버헤드
    • 메모리 과부하 → 서버가 Shut Down 되는 경우가 존재함.
    • 리소스 병목
  • 따라서 서버에 대한 자원 제약사항을 고려해야하고, 배치 작업의 특성을 고려해야한다.
  • 서버에 대한 자원 제약사항
    • 서버의 CPU 코어 수와 메모리를 고려하여 적절한 스레드 풀 크기를 설정해야 한다.
      • 예를 들어, CPU 코어 수보다 훨씬 많은 스레드를 생성하면, 컨텍스트 스위칭 비용이 증가하여 성능이 저하될 수 있다.
    • 배치 작업의 특성을 파악해서 스레드 풀 크기를 설정해야한다.
      • CPU 바운드 작업 : CPU 코어 수와 비슷하거나 조금 더 큰 수의 스레드를 사용하면 좋다.
      • I/O 바운드 작업 : I/O 작업은 CPU 리소스를 적게 사용하므로, CPU 코어 수보다 더 많은 스레드를 생성할 수 있다. → 우리의 배치 작업은 I/O 바운드 작업이므로 이걸 선택한다.
  • 코어 개수 확인하기
    • 작업관리자 → 성능 탭에 들어가면 코어와 논리 프로세서를 볼 수 있다.
      • 코어 : 물리적인 코어의 개수
      • 논리 프로세서 : 코어와 쓰레드에 따라 논리적으로 수행할 수 있는 코어의 수를 말합니다.
  • 우리는 CPU 바운드 작업보단 I/O 바운드 작업을 하기에 논리 프로세스 수인 8보다 더 많은 스레드 풀을 생성해도 된다. 따라서, 10개의 스레드 풀을 생성하였다.
  • 단, CPU에 병목 현상이 없는 경우에는 논리 프로세서 8개가 모두 활용이 안 될수도 있지만 CPU에 병목 현상이 있는 경우에는 논리 프로세서 8개 다 사용할 것이다.

실행

  • 그래서 스레드 풀을 10개로 늘렸을 때 정말 다이나믹한 결과가 나왔나?
  • 답은 그렇다이다.
    • 스레드 풀을 사용하지 않고 동기처리 했을 땐, 4시간 걸리는 것을 스레드 풀 10개로 늘려 5분만에 모든 배치 작업을 끝내게 되었다.
    • 1/48로 줄이게 된 계기가 되었다!!

결론

후기

  • 배치 작업을 튜닝한 경험이 처음이라 몰랐던 사실들이 정말 많았는데 이번 기회에 CPU, 멀티 스레드, I/O 바운드, 논리 프로세스 등을 알게되었다!
  • 분명 대학생 때 운영체제 시간에 배웠을텐데…다시금 기억하게 된 계기가 되었다.
  • 그리고 뿌듯하다. 직접 생각해서 시간을 다이나믹하게 줄일수 있다는 점에🙂
profile
지나가는 개발자

1개의 댓글

comment-user-thumbnail
2023년 9월 5일

이해하기 쉽게 설명이 되어있네요 ! 잘 보고 갑니다 🙂🙂

답글 달기