1회 크롤링시 약 3000개 정도의 데이터를 크롤링하는데 메모리 이슈가 발생 하였다.
쿠버네티스로 오토스케일링을 하여도 지속적인 크롤링으로 서버가 다운되는 현상
제가 생각한 원인은 코드 레벨에서의 실수로 메모리 누수가 발생 한 것이라고 생각 하였습니다.
먼저 로컬환경에서 테스트를 진행 -> 배포환경에서 모니터링을 통해 문제를 식별 하고자 합니다.
실행전에는 약 5700 의 쓰레드가 실행되어있습니다.
11시 40분 30초 부터 Schedule 이 작동되어 실행된 모습입니다.
실행후 2분 경과입니다. 39running, 6500 threads 가 눈에 보입니다.
나중엔 threads 7000 ~ 7500정도를 유지하였고 이러한 부분때문에 jvm heap 을 초과하여 서버가 종료된것이라고 생각했습니다. 물론 인프라적으로 스케일업을 통하여 해결하는 방법도 있겠지만 제가 작성한 코드에서 성능개선할 부분이 충분히 있다고 생각하고 접근하였습니다.
코드를 확인 해 보겠습니다.
WebDriver driver = setDriver().get();
driver.get(checkDto.url());
try {
scrollDown(driver);
}catch (Exception e){
e.printStackTrace();
}
기존의 코드입니다. 새로운 페이지를 들어가고 스크롤을 내리는 부분입니다. 크롤링 클래스에서 가장많이 호출되고 가장많이 에러가 발생하는 곳 이기도 합니다.
이부분에 대하여 try catch 는 진행 하였지만 driver 의 종료문이 없었습니다.
먼저 driver.close 와 quit에 대해 간단하게 알아보면
우리는 비동기로 여러창이 띄워지고 해당 드라이버가 사용했던 창만 종료하면 됩니다. 만약 모든 창을 종료한다면 에러가 발생할 것입니다.
이러한 점을 모르고 quit() 를 사용했으니 여러창들이 띄워져있을때 에러가 발생하는 경우도 있었습니다.
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 를 종료하게 만들었습니다.
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);
}
}