n8n 셀프호스팅으로 슬랙 알림 봇 만들다가 메모리 터진 썰
정유나
@yuna_ai
회사에서 Make(구 Integromat) 쓰던 걸 n8n 셀프호스팅으로 갈아탔다. 이유는 단순하다. operations 수가 자꾸 한도를 넘어서 요금제를 한 단계 올려야 하는 상황이 반복됐는데, 막상 따져보니 우리가 돌리는 시나리오는 별로 무겁지도 않았다. 그냥 자주 돌 뿐이라. 셀프호스팅하면 거의 공짜인데 굳이? 싶어서 옮겼다. 결론부터 말하면 옮긴 거 자체는 후회 안 하는데, 첫 사흘은 좀 날렸다.
만들려던 건 진짜 별거 아니었다. Airtable에 새 레코드 들어오면 영업용 Slack 채널에 알림 쏘는 봇. 영업팀이 문의 들어올 때마다 노션 새로고침하는 게 귀찮다고 해서 만들어준 거.
처음엔 사내에 굴러다니던 t3.small EC2에 그냥 docker로 올렸다. 문서에 나온 그대로
docker run -it --rm --name n8n -p 5678:5678 n8nio/n8n
띄우고, 워크플로우 몇 개 등록하고, 잘 도네 하고 넘어갔다. 데모는 멀쩡했음.
근데 운영 이틀쯤 지나니까 컨테이너가 자꾸 죽어 있었다. 슬랙 알림이 안 와서 들어가 보면 컨테이너가 내려가 있는 식. docker logs n8n 찍어보면 스택트레이스도 뭣도 없고 끝이 이거 한 줄이었다.
n8n exited with code 137
137이 뭔가 했는데 검색해 보니 128+9, 그러니까 SIGKILL 맞고 보통 OOM이면 이렇게 나온다더라. 호스트에서 free -m 쳐보면 컨테이너 죽기 직전에 available이 거의 0으로 빨려들어가 있었다. 확인차 dmesg도 봤더니
Out of memory: Killed process 2117 (node) total-vm:...
oom-killer ... Memory cgroup out of memory
OOMKilled 확정. t3.small이 RAM 2GB짜린데, n8n이 node로 워크플로우 돌면서 실행 데이터를 죄다 메모리에 들고 있더라. 우리 워크플로우 중에 Airtable에서 한 번에 레코드 수백 개씩 fetch하는 노드가 하나 있었는데, 그게 통째로 메모리에 올라가는 모양이었다. 페이로드 큰 거 다룰 때 특히 잘 터지는 듯. 정확힌 모르겠는데 바이너리 데이터 들고 있을 때가 제일 심했다. docker stats로 옆에서 지켜보면 그 노드 도는 순간 메모리 사용량이 확 튀고, 끝나도 잘 안 내려오는 게 보였다.
여기서 한 가지 더 발견. 메모리 추적하다 보니까 SQLite 파일(database.sqlite)이 계속 커지고 있었다. 실행 기록이 무한정 쌓이는 거였다. n8n이 기본적으로 execution 데이터를 안 지운다더라. 이게 메모리랑 직접 연관인지는 사실 자신 없는데, 아무튼 안 쌓이게 하는 게 맞다 싶어서
EXECUTIONS_DATA_PRUNE=true
EXECUTIONS_DATA_MAX_AGE=168
(168시간=1주일) 박았다. 이걸로 죽는 빈도가 좀 줄긴 했는데... 솔직히 근본 해결은 아니었다. 그냥 t3.medium으로 인스턴스 올리니까 그제서야 안 죽더라. 부끄럽지만 두 번째가 훨씬 컸다. RAM 4GB 되니까 그 fetch 노드도 그냥 삼키더라. 돈으로 때린 거지 뭐.
이제 됐나 했는데 더 골때리는 게 남아 있었다. 알림이 가끔, 진짜 가끔 안 왔다. 컨테이너는 멀쩡히 살아 있는데. 로그를 한참 뒤지다가 이걸 발견했다.
QueryFailedError: SQLITE_BUSY: database is locked
동시에 워크플로우 몇 개가 겹쳐 돌면 SQLite가 락 걸리면서 그 실행이 조용히 실패하는 거였다. 에러는 로그에만 찍히고 워크플로우 상태는 그냥 넘어가니까, 실패한 줄도 몰랐다. 이게 제일 무서웠다. 컨테이너가 죽으면 차라리 알지, 살아 있으면서 조용히 한두 건 빠지는 건 한참 뒤에야 알아챈다. 영업팀이 "그 문의 알림 안 왔는데요?" 하기 전까진 몰랐음.
답은 Postgres였다. DB_TYPE=postgresdb로 바꾸고 같은 호스트에 postgres 컨테이너 하나 더 띄워서 붙였다.
DB_TYPE=postgresdb
DB_POSTGRESDB_HOST=postgres
DB_POSTGRESDB_DATABASE=n8n
바꾸고 나서는 database is locked가 한 번도 안 떴다. 진작 이렇게 할 걸. 사실 셀프호스팅 문서에 production은 Postgres 쓰라고 적혀 있는데, 데모 잘 된다고 SQLite 기본값 그대로 넘긴 게 화근이었다. 이런 건 꼭 나중에 물린다.
지금은 한 달 넘게 잘 돈다. Make 쓸 때 월 ops 한도 게이지 보면서 "이번 달도 넘기나" 신경 쓰던 거에 비하면 마음은 확실히 편하다. 근데 공짜는 아니더라. OOM 나면 내가 새벽에 인스턴스 만지고, DB 백업도 내가 챙겨야 하고. Make는 그 운영 스트레스를 돈으로 산 거였구나 싶었다. 팀에 노드 한 번이라도 만져본 사람 있으면 셀프호스팅 할 만하고, 아무도 없으면 그냥 클라우드 결제하는 게 정신건강엔 나을지도. 우린 어쩌다 보니 그 사람이 나 하나였지만...