altair의 프로젝트 일기
SQLite 기반 set과 queue(나무위키 크롤링) 본문
개요
파이썬으로 나무위키를 크롤링하다가 문제가 발생했다.
서버에서 크롤러가 돌아가는 컨테이너의 메모리 사용량이다. alpine 리눅스를 사용했음에도 불구하고 512메가바이트를 가볍게 채우고 1기가바이트로 바꾸어도 결국 모두 채운다. 크롤러는 대상 서버를 배려하기위해 시간 간격을 두고 요청을 보낸다. 그럼에도 불구하고 메모리에 계속 데이터가 쌓이는 모습이다.
결국 크롤러는 굉장히 느려졌다.
원인
사용 중이었던 크롤러 클래스의 __init__ 코드를 작성하면 다음과 같다.
class Crawler:
def __init__(self, base_url):
self.visited = set()
self.queue = Queue()
self.base_url = base_url
방문했던 url을 visited라는 set에 넣고, 다음에 탐색할 url을 queue에 넣는 방식이다. 좀 더 자세히 표현하면 다음과 같다.
- queue에서 url을 하나를 빼온다.
- 그 url을 visited라는 set에 저장한다.
- url에 GET 요청을 날려 html을 처리한다.
- html에서 얻은 자식 url들을
- 해당 자식 url이 visited에 있다면 넘어간다.
- 없다면 queue에 저장한다.
- queue가 빌 때까지 반복한다.
언뜻 보기에는 별 문제가 없다. 그러나 페이지가 몇 만 개라면? 실제로 512메가바이트의 메모리를 모두 채웠을 때 탐색한 페이지 수는 19000개가 넘어갔고, 1기가바이트를 채웠을 때는 75000개가 넘어갔다. visited와 queue가 모두 메모리에 저장되는 것을 고려하면 이 두개가 램 부족의 유력한 용의자다.
set
일단 visited라는 set의 크기가 매우 커질 것으로 예상한다. 파이썬의 set은 C++의 unordered_map과 같이 해쉬맵으로 구현되어 있다.
(하지만 C++의 그냥 set은 레드블랙트리로 구현된 경우가 많다. 때문에 set은 순회가 가능하다)
문제는 방문하는 페이지가 매우 많기 때문에 해쉬테이블 특성상 공간이 크게 늘어날 가능성이 있다. 어차피 크롤러는 빠르게 요청을 할 필요가 없기 때문에 속도가 빠른 해쉬테이블은 의미가 없다. 메모리 효율성이 더 좋은 자료구조를 찾아야 했다.
Queue
그러나 가장 큰 문제는 Queue였다. 하나의 html을 읽을 때마다 발견한 모든 문서 링크를 저장하지만, 반대로 하나씩 시간 간격까지 두고 천천히 소비하기 때문이다. 페이지 몇 개만 탐색해도 queue는 몇 천개의 링크를 저장해야 했다. 역시 메모리 효율성이 매우 나빴다. 가장 주요한 요인은 바로 이 큐에 있을 것으로 예상했다.
해결 시도1: 블룸필터
블룸필터로 방문한 링크인지 확인하는 방법을 생각해보았다. 문제는 블룸필터가 방문한 적 없는 링크에 대해 방문했다고 결과를 줄 확률이 있다는 것이다(긍정오류, False positive). 그렇다면 다음과 같은 순서가 된다.
- 블룸필터로 링크를 검사한다.
- 거짓일 경우 항상 방문하지 않은 url이다. 크롤링을 진행한다.
- 참일 경우, FP의 가능성이 존재한다.
- 해당 url의 데이터가 파일 시스템에 존재하는지 확인한다.
- 존재한다면 방문한 url이다.
- 존재하지 않는다면 방문하지 않은 url이다. 크롤링을 진행한다.
- 해당 url의 데이터가 파일 시스템에 존재하는지 확인한다.
이런 경우 다음과 같은 단점이 존재한다.
- FP의 가능성 때문에 어차피 파일 시스템에서 조회를 해야한다. 문제는 url을 파일 이름으로 삼을 수 없기 때문에 html의 타이틀을 파일 이름으로 삼고 있다는 점이다. 링크의 타이틀을 알아내려면 어차피 GET 요청으로 html을 받아와야한다. 선후관계가 역전되어 버린다.
- 당연하게도 파일 시스템에서 수 만개의 파일을 조회하는 것은 비효율적이다.
이런 이유로 블룸필터는 사용하지 못했다.
해결 시도 2: SQLite3
파이썬은 SQLite3를 내장하고 있다. 이를 사용해 SQLite 기반의 set과 queue를 구현할 수 있다. 이런 방식으로 queue를 구현한 패키지는 이미 나와있다.
램이 아닌 파일시스템을 사용할 뿐만 아니라 중간에 중단되었을 때도 중단된 부분부터 다시 로드할 수 있으니 매우 편리하다.
set도 SQLite로 쉽게 구현할 수 있는데, 단지 PK로 TEXT 컬럼 하나만 갖는 테이블을 선언하고 여기에 링크들을 저장하면 된다. PK는 중복될 수 없으니 하나만 저장하고, 조회를 통해 존재 여부를 검사할 수 있다. 또한 SQLite는 파일 하나만 생성하므로 관리도 편하고 비교적 높은 성능을 얻을 수 있다.
램보다 파일 시스템에 저장되므로 속도는 느려지지만 크롤러 특성 상 조회 속도는 크게 문제가 되지 않을 것이다.
이로써 메모리 효율성이 좋아지고 갑작스런 종료에도 진행상황을 잃어버리지 않는 크롤러을 만들 수 있었다.
개선 방향
일단은 이대로 돌려보려 하지만, 걱정되는 부분이 있다. 같은 종류의 페이지들에서 링크들을 얻다보면 유난히 높은 빈도로 나타나는 같은 url들이 많을 것이다. 예를 들어 홈으로 가는 url이나 로그인 url 등 말이다. 이것들이 쿼리의 많은 부분을 차지할 것이다.
SQLite로 구현한 set을 조금 더 개선한다면 이를 원활히 처리할 수 있을 것이다. 예를 들어 조회 빈도 컬럼을 추가해, 빈도가 높은 레코드를 램에 올려 캐시로 사용할 수 있을 것이다.
이 부분은 쿼리들을 기록해 분석하면 더 확실하게 알 수 있을 것이다.
Github
'IT > 파이썬' 카테고리의 다른 글
운영체제의 캐시 정책 비교 (0) | 2023.01.13 |
---|