데이터 수집기
본글에서는 지난번 콘텐츠 유통 데이터 분석 시스템(이하, 코다스) Elastic Stack 프로젝트 글에 이어서, 다양한 데이터를 수집하는 역할을 수행하는 CODDAS Crawler 프로젝트 내용을 정리한다.
프로젝트 이력을 정리하는 글이기 때문에 구체적인 설치나 개발 가이드는 아니며, 필자의 프로젝트 구축 노하우(라고 쓰고, 개고생이라고 읽음)를 중심으로 경험담을 적어 볼까 한다.
CODDAS Crawler 프로젝트 개발 이유
Elastic Stack에서는 logstash, beats 등을 통해 다양한 데이터 수집이 가능하다. 그럼에도 불구하고 데이터를 제공하는 대상의 정책 또는 시스템의 제약조건으로 인해 수집이 불가능한 경우가 존재한다. DB 성능개선을 위한 모니터링 에이전트도 부담스러워하는 상황에서, 데이터 수집을 위한 에이전트는 언감생심이다. '메일로 보내드릴게요', '웹 페이지에 있으니 알아서...' 등등, 데이터를 제공받는 입장은 언제나 을이다.
때문에, "다양한" 데이터를 수집하는 Crawler 프로젝트는 선택이 아닌 필수였다.
CODDAS Cralwer 구성
메일 첨부파일을 통해 데이터를 제공하는 경우가 유독 많았다. 즉, 기존 리포팅의 재활용이 데이터를 제공하는 입장에서는 가장 수월했을게다.
CODDAS Crawler 팁 앤 테크
CODDAS Crawler 프로젝트는 NodeJs 기반이며, 주기적인 수집을 위해 OS 스케쥴러(crontab)로 동작한다. NodeJs 애플리케이션 스케쥴러 방식을 사용하지 않은 이유는(뒤에서 다시 언급하겠지만), 워낙 큰 데이터를 핸들링하다 보니 성능 및 메모리 제약이 많아서 OS 레벨로 처리했다.
OS 스케쥴러(crontab)와 NodeJs 애플리케이션 스케쥴러 방식을 결정하는 기준 아래와 같다.
> 스케쥴링 시간을 프로그래밍하여 사용하고 싶은 경우, NodeJs 애플리케이션 스케쥴러 방식
- 관련 모듈로는 node-schedule과 node-cron이 양대 산맥
- node-schedule이 보다 복잡한 스케쥴링 방식을 지원하지만 메모리를 더 많이 사용함
- node-cron 사용 시 주의점
* 코드 안에 exit() 존재할 경우, 아래와 같이 문제 발생함
node로 실행하면 cron이 동작하지 않고 프로세스가 종료됨
pm2를 이용해서 실행하면 무한 반복 실행되면서 cron이 동작하지 않음
* 정상적인 로직의 process.exit() 코드는 반드시 제거해야 함
> 지정된 시간에 실행하며 메모리에 민감하여 최적화가 필요한 경우, OS 스케쥴러 방식
웹 크롤링을 위해 HTML Request, Response를 직접 처리하는 모듈(crawler)과 Headless 브라우저 엔진을 통해 처리하는 모듈(puppeteer)을 모두 사용한다. 크롤링은 이 소재 하나만으로도 책 한 권을 쓸 수 있을 만큼의 방대한 내용이라 따로 시간을 내서 정리를 해 보려 한다.(할말하않)
이메일 관련 작업은 사내 메일 서비스가 제공하는 API(NAVER WORKS)를 통해 수신 및 첨부파일 다운로드 처리를 했다.
유튜브 데이터는 Youtube Data API, Youtube Analytics(Report) API를 사용하며, Report API로 받은 대용량 엑셀 파일을 기반으로 Data API를 통해 실시간 데이터를 추가하여 API 쿼터(Quota) 최적화했다.
유튜브 수익 데이터 중 파트너 세일즈 수익은 Ad Manager 360의 리포팅 메일 첨부파일을 사용했다. 물론, Ad Manager API로도 개발이 가능하다.(그... 그래 너무 귀찮았다... -_ -;; 가이드 문서에 NodeJs 샘플이 없다는 이유 하나만으로 기존에 개발된 코드를 활용해서 메일 방식으로 결정했다.)
Youtube Analytics API는 UTC 기준이며, 2일 전 데이터부터 확인이 가능하다.(11/30(월) 04:30 ~ 05:00 사이에 검색하면, 11/27(금) 데이터가 조회됨)
Youtube Analytics API의 일자의 기준은 유튜브 분석 페이지의 기준을 따른다. 예를 들어, 한국시간 11/27(금) 17:10에 업로드한 영상이 있다고 가정하자. 이 영상의 수집 데이터는 태평양시 11/27(금)으로 조회돼서 동일 날짜의 수집으로 인식되지만, 한국시간 11/27(금) 10:00에 업로드한 영상의 경우, 태평양시 11/26(목)으로 표시된다. 분석을 보는 사용자 입장에서는 하루치 데이터가 전일로 인식되는 상황이 생기는 셈. 즉, 둘 다 오차가 있지만 조회 결과 날짜에 +1일을 하면 17시간을 현재 날짜로 적용하는 셈이고, 그대로 간다면 7시간을 현재 날짜로 인식하는 셈이다. 시간의 양으로만 비교하면 +1일 하는 게 맞지만, 유튜브 분석 페이지에서는 7시간으로 선택하고 있기 때문에 유튜브의 기준을 따라 +1일은 하지 않기로 했다.
유튜브 채널의 모든 동영상 정보를 가져올 수 있는 API가 없다. 간단할 거 같은 이 작업이 생각보다 복잡하며 아래의 꼼수가 필요하다.
유튜브 수집 데이터처럼 큰 데이터를 DB(MySQL)에 저장할 때 패킷 사이즈 문제로 아래의 에러가 발생할 경우가 있다.
Packets out of order. Got: 5 Expected: 7
MySQL에서 한 번에 받을 수 있는 패킷 사이즈가 기본 64MB로 설정되어 있어서 발생하는 문제로, 설정 파일에서 패킷 사이즈를 1000M로 늘려서 해결했다.
max_allowed_packet=1000M
systemctl restart mysqld
쿼리 실행이 30초 이상 걸릴 경우 DB(MySQL)에서 강제로 연결 끊어 아래의 에러가 발생할 경우가 있다.
MySql Error Code 2013
Lost Connection
30초 이상 수행됐다는 게 본질적인 문제지만;;; 리인덱싱 작업처럼 특수한 경우에 수행하는 작업의 경우 설정 파일에서 최대 시간을 600초로 늘려서 해결했다.
net_read_timeout=600
systemctl restart mysqld
유튜브의 동영상 분석 리포트는 가이드에 따르면 3일 전 데이터가 생성된다고 나와있지만, 간혹 유튜브에서 1~2일 늦어지는 경우가 있다. (2021년 설날(2021/02/09, 2021/02/10) 데이터가 이틀 동안 생성 안됨)
NodeJs에서 힙 메모리 에러 발생할 경우, 명령행에서 옵션으로 할당 메모리를 늘린다.
node --max-old-space-size=4096 .\src\test.js
유튜브의 리포트 파일이 기가 단위로 커질 경우, NodeJs의 메모리 할당을 늘려서 대응은 가능하지만 처리 속도에 문제가 발생한다. 비단 속도뿐만 아니라 스트링 최대 길이나 배열, 객체의 최대 길이에 따른 다양한 변수가 존재하기 때문에 리포트 파일을 처리할 때는 Stream을 이용한 방식으로 처리했다.
유튜브의 리포트 파일(CSV 형식)을 라인 단위로 읽어서 콤마(,) 구분자로 처리하다 보면 라인 데이터에 포함된 줄 바꿈, 큰따옴표("), 홑 따옴표(') 때문에 문제가 발생한다. 특히 파일을 읽어서 객체화할 때 문제가 되는데, 이를 오픈소스(csvtojson)와 직접 개발한 CSV 라인 파서로 해결했다. 오픈소스로 처리하려면 반드시 Stream을 지원해야 하며, 파일 크기가 커질 경우 속도 및 성능에 영향을 미친다.
CSV 파일로 생성된 리포트 파일을 메모리에 로드할 때는 Escape 문자 처리를 반드시 해줘야 한다. 그렇지 않을 경우, backslash(\) 지옥에 빠질 수 있다.(csvtojson은 파라미터로 지원함)
HTTP의 비용이 워낙 크다 보니 동기로 개발할 경우 성능에 많은 영향을 미친다. 실행(처리) 속도가 중요한 작업의 경우, 비동기로 개발했다.(단, 요청 순서가 보장되어야 하는 경우는 제외)
CODDAS Crawler 소고
데이터를 수집해서 저장하는 서비스는 꽤나 날것(?)의 느낌이 나는 프로젝트다. 표준화가 안된 입력 덕분에 워낙 다양한 예외들이 존재하고, 바이트부터 기가까지 입력의 범위도 넓고 성능 또한 중요하다. 때문에 이를 해결하기 위한 정책적, 기술적 고민이 필요하고 원하는 정보를 얻기까지 꽤나 많은 노력이 들어간다. AI가 조금 더 보편화된 세상에서는 이런 고민들이 사라지길 희망한다.(과연 될까? ㅎㅎ)
입력 데이터의 다양성과 예외처리
세상에는 정말 다양한 형식의 입력 데이터가 존재한다. 그것을 고려하여 원본 데이터를 파싱 해야 하고 항상 예외처리를 고려해야 한다. 특히 사용자가 입력하는 비정형 데이터(제목, 내용 등)를 수집하는 경우 무엇을 상상하든 그 이상을 볼 수 있다. Bar에 그렇게 다양한 방식(|ㅣㅣ|│l)이 있을 거라곤 상상도 못 했다 @_@;;
한 가지 더. 모바일 시대로 넘어가면서 이모지의 활용이 늘어났기 때문에 텍스트 인코딩은 반드시 이를 고려하여 4바이트로 설정해야 한다.
쌓여가는 데이터에 대한 성능을 고려
수집 데이터는 지속적으로 쌓이고 저장 공간은 부족해진다. 부족한 디스크는 이후에도 늘릴 수 있지만, 데이터를 조회하는 서비스의 경우 시간이 흐를수록 성능이 떨어진다. 때문에, 적절한 쿼리 튜닝과 주기적인 인덱스 개선이 필요하며, 데이터 형식에 따른 물리적인 파티션 작업도 설계 초기에 고려해야 한다.
데이터 백업의 생활화
데이터는 소실되면 답이 없다. 백업은 필수다.
필자도 어느 날 갑자기(-_ -) DB 인스턴스가 정상적으로 올라오지 않는 문제가 발생했다.(이런 일은 나와는 상관없는, 책에서나 보단 이슈라고 생각했었는데...) 특정 날짜에 들어간 데이터가 깨진 듯 보였고, 서버 성능이 부족한 상황에서 대량의 유튜브 데이터(다양한 특수문자, 이모지 포함)가 동시간대에 입력되다 보니 생긴 문제라고 추측할 뿐, 결국 정확한 원인은 찾을 수 없었다. 결국 하루 전 백업 데이터로 원복을 했고 사라진 하루 동안의 데이터는 수작업(이라고 쓰고 개고생이라고 읽음)으로 재수집을 해서 해결할 수 있었다. 다시 한번 말하지만, 백업은 필수다.
수집 서버에 인색하지 않은 투자 필요
데이터를 수집하는 서버의 성능은 꽤나 중요하다. 웹 서버나 애플리케이션 서버와는 다르게 대량의 데이터를 빠르게 처리해야 하는 요구사항이 있기 때문에, CPU나 메모리는 다다/거거 익선이다. 필자의 경우, 일별 유튜브 동영상 분석 데이터를 수집할 때 최적화를 통해 제한된 서버 스펙의 한계를 뛰어넘겠노라 엔지니어의 자존심을 세워봤지만, 간헐적으로 발생하는 메모리 문제로 꽤나 고생을 했다. 서버 증설 이후 더 이상 에러가 발생하지 않는 프로그램을 바라보며 꽤나 멘붕이 왔던 기억이다. 괜히 불필요하게 자존심 부리면서 고생하지 말고 서버 스펙 증설에 노력하자 -_ -;;;
모니터링 기능은 선택이 아닌 필수
간혹 잘못되거나 미 수집된 데이터를 시각화 서비스를 통해 알게 되는 경우가 있다. 심지어 사용자를 통해 연락을 받는 최악의 경우도 존재한다.(그 사용자가 직속 상사더라... -_ -) 프로그램 수정 비용이 뒤로 갈수록 커지는 것처럼, 데이터 수집에 대한 비용 역시 시간이 흐를수록 커진다. 수집 프로세스에서 문제가 발생한 경우, 이를 실시간으로 다양한 방법(Mail, App Push 등)을 통해 알려줄 수 있는 모니터링 기능은 반드시 필요하다. (예산이 많아서 모니터링을 사람이 해준다면 금상첨화겠지만, 현실은 냉혹하다. 프로젝트 초기에 개발 목록에 포함시켜 놓자.)
재수집 방법은 필수
수집이 24/7 정상적으로 될 거라는 생각은 하지 않는 게 좋다. 예상하지 못한 상황으로 원하는 데이터가 미수집 되는 경우는 부지기수다. 나아가 잘못된 데이터가 수집되는 경우도 많다. 때문에 이런 상황에 데이터를 다시 수집할 수 있는 프로세스가 반드시 필요하다. 잘못된 데이터를 초기화하고 재수집할 수 있는 방법을 미리 준비해 놓자. (그렇지 않으면 지옥을 보게 된다 -_ -;;;;)
수집 데이터는 서로 트리거 되지 않도록
수집은 순수하게 원본 데이터를 저장하는 역할만 하고, 수집 데이터가 다른 데이터의 수집에 영향을 끼치는 프로세스는 지양해야 한다. 예를 들어, 01:00에 수집한 데이터가 있다고 가정할 때, 01:10에 수집하는 데이터를 01:00에 수집한 데이터로 계산해서 저장하면 안 된다. 01:00에 수집이 안될 경우, 01:10에 수집하는 데이터는 문제가 발생하고 재수집 작업도 복잡해진다. 수집 데이터는 그대로 저장하고 추후 작업을 통해 데이터를 가공하는 방법을 권장한다. 어쩔 수 없이 데이터가 서로 관련될 경우 해당 수집 프로세스가 길어지지 않도록 주의하자.