Memory) 크롤링중 메모리 이슈

박우영·2023년 7월 9일
0

트러블 슈팅

목록 보기
16/19

1회 크롤링시 약 3000개 정도의 데이터를 크롤링하는데 메모리 이슈가 발생 하였다.
쿠버네티스로 오토스케일링을 하여도 지속적인 크롤링으로 서버가 다운되는 현상

제가 생각한 원인은 코드 레벨에서의 실수로 메모리 누수가 발생 한 것이라고 생각 하였습니다.

먼저 로컬환경에서 테스트를 진행 -> 배포환경에서 모니터링을 통해 문제를 식별 하고자 합니다.

실행 전

실행전에는 약 5700 의 쓰레드가 실행되어있습니다.

실행 후

11시 40분 30초 부터 Schedule 이 작동되어 실행된 모습입니다.

진행 중

실행후 2분 경과입니다. 39running, 6500 threads 가 눈에 보입니다.

나중엔 threads 7000 ~ 7500정도를 유지하였고 이러한 부분때문에 jvm heap 을 초과하여 서버가 종료된것이라고 생각했습니다. 물론 인프라적으로 스케일업을 통하여 해결하는 방법도 있겠지만 제가 작성한 코드에서 성능개선할 부분이 충분히 있다고 생각하고 접근하였습니다.

코드를 확인 해 보겠습니다.

Before code

        WebDriver driver = setDriver().get();
        driver.get(checkDto.url());
        try {
            scrollDown(driver);
        }catch (Exception e){
            e.printStackTrace();
        }

기존의 코드입니다. 새로운 페이지를 들어가고 스크롤을 내리는 부분입니다. 크롤링 클래스에서 가장많이 호출되고 가장많이 에러가 발생하는 곳 이기도 합니다.
이부분에 대하여 try catch 는 진행 하였지만 driver 의 종료문이 없었습니다.

먼저 driver.close 와 quit에 대해 간단하게 알아보면

  • driver.quit() : quit() 메서드는 드라이버를 종료하고 관련된 모든 창을 닫습니다.
  • driver.close() : close() 메서드는 현재 포커스가 있는 창을 닫고 현재 창이 유일한 열린 창인 경우 드라이버를 종료합니다. 열려 있는 창이 없으면 오류가 발생합니다.

우리는 비동기로 여러창이 띄워지고 해당 드라이버가 사용했던 창만 종료하면 됩니다. 만약 모든 창을 종료한다면 에러가 발생할 것입니다.

이러한 점을 모르고 quit() 를 사용했으니 여러창들이 띄워져있을때 에러가 발생하는 경우도 있었습니다.

수정 1

            WebDriver driver = setDriver().get();
            driver.get(checkDto.url());

        try {
            scrollDown(driver);
            WebDriverWait xpathWait = new WebDriverWait(driver, Duration.ofSeconds(10));
            WebElement deadlineElement = xpathWait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//*[@id=\"__next\"]/div[3]/div[1]/div[1]/div/div[2]/section[2]/div[1]/span[2]")));
            WebElement workingAreaElement = xpathWait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//*[@id=\"__next\"]/div[3]/div[1]/div[1]/div/div[2]/section[2]/div[2]/span[2]")));

            // 웹 요소로부터 텍스트 추출
            String deadLine = deadlineElement.getText();
            String place = workingAreaElement.getText();
            log.debug("{}", deadLine);
            log.debug("{}", place);

            JobResponseDto jobResponseDto = new JobResponseDto
                    (
                            checkDto.company(), checkDto.subject(), checkDto.url(),
                            checkDto.sector(), checkDto.sectorCode(), checkDto.createDate(),
                            deadLine, checkDto.career(), place
                    );
            System.out.println(jobResponseDto.getUrl());
//        producer.batchProducer(objectMapper.writeValueAsString(jobResponseDto));
        }catch (Exception e){
            e.printStackTrace();
            throw new CrawlingException("detailPage 스크롤 에러");
        }finally {
            driver.close();
        }

try catch finally 로 driver 를 종료하게 만들었습니다.

outofMemoryerror

Exception in thread "Exec Default Executor" 
java.lang.OutOfMemoryError: unable to create native thread:
possibly out of memory or process/resource limits reached

기존의 quit -> close 로 되어 있던게 크롬 메모리 에러를 일으켰습니다.

chrome driver option 에 추가

chromeOptions.addArguments("--disk-cache-size=0");

추가적으로 JVM 구조에 대해 공부해보니 Stack Area 는 각 쓰레드 별로 생성 되기때문에
쓰레드 가 종료될때 close 가 아닌 quit 로 전부 종료를 해주었습니다.

이렇게 진행하면 바로바로 종료가 되기때문에 GC 가 처리되지않아도 메모리 성능을 최적화 시킬 수 있습니다.

//TODO:

        for (JobCheckDto checkDto : checkDtos) {
            try {
                detailPage(checkDto);
            } catch (CrawlingException e) {
                continue;
            }
        }

변경

@Slf4j
@Configuration
public class SeleniumTest {
    private static ConcurrentLinkedQueue<WebDriver> driverPool;

    @Bean
    public ApplicationRunner test() {
        return args -> {
            initializeDriverPool(30);
            ExecutorService executorService = Executors.newFixedThreadPool(20);
            // System Property 설정
            System.setProperty("webdriver.chrome.driver", "/Users/myunghan/Desktop/test/spring-Crawling/driver/chromedriver");
            Integer[] ids = {
            //TODO: 들어갈 페이지 id 목록 
            };

            for (Integer id: ids) {
                executorService.submit(() -> {
                    ChromeOptions options = new ChromeOptions();
                    options.addArguments("headless", "disable-gpu", "window-size=1920x1080",
                            "user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36",
                            "blink-settings=imagesEnabled=false"
                            );
                    WebDriver driver = getDriverFromPool();
                    driver.get("https://www.wanted.co.kr/wd/" + id);

                    WebElement element = driver.findElement(By.className("JobDescription_JobDescription__VWfcb"));
                    WebDriverWait wait1 = new WebDriverWait(driver, Duration.ofSeconds(10));
                    wait1.until(ExpectedConditions.visibilityOfElementLocated(By.className("JobDescription_JobDescription__VWfcb")));

                    JavascriptExecutor js = (JavascriptExecutor) driver;
                    js.executeScript("arguments[0].scrollIntoView({block: 'end', behavior: 'auto'});", element);
                    js.executeScript("window.scrollBy(0, window.innerHeight);");

                    // 특정 웹 요소가 로드될 때까지 대기
                    WebDriverWait wait2 = new WebDriverWait(driver, Duration.ofSeconds(10));
                    WebElement deadlineElement = wait2.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//*[@id=\"__next\"]/div[3]/div[1]/div[1]/div/div[2]/section[2]/div[1]/span[2]")));
                    WebElement workingAreaElement = wait2.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//*[@id=\"__next\"]/div[3]/div[1]/div[1]/div/div[2]/section[2]/div[2]/span[2]")));

                    // 웹 요소로부터 텍스트 추출
                    String deadline = deadlineElement.getText();
                    String workingArea = workingAreaElement.getText();
                    log.info("{}", deadline);
                    log.info("{}", workingArea);

                    returnDriverToPool(driver);
                });
            }

            executorService.shutdown();

            new Thread(() -> {
                try {
                    if (executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS)) {
                        driverPool.forEach(WebDriver::quit);
                    }
                } catch (InterruptedException e) {
                    System.err.println("Interrupted while waiting for tasks to complete.");
                }
            }).start();
        };


    }

    private static void initializeDriverPool(int size) {
        driverPool = new ConcurrentLinkedQueue<>();

        for (int i = 0; i < size; i++) {
            ChromeOptions options = new ChromeOptions();
            options.addArguments("headless", "disable-gpu", "window-size=1920x1080",
                    "user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36",
                    "blink-settings=imagesEnabled=false"
            );
            WebDriver driver = new ChromeDriver(options);
            driverPool.add(driver);
        }
    }

    private static WebDriver getDriverFromPool() {
        return driverPool.poll();
    }

    private static void returnDriverToPool(WebDriver driver) {
        driverPool.add(driver);
    }
}

참조


oreilly

0개의 댓글