서버를 재부팅해도 PM2가 살아남게 하기
서버가 재부팅됐는데 사이트가 돌아오지 않았습니다. 한 시간 뒤 누군가 배포를 돌리자 그제야 살아났습니다. 배포 문제처럼 보이지만 아닙니다. 그 배포가 한 일은 pm2 start를 손으로 한 번 더 실행한 것뿐입니다.
앱이 알아서 뜨게 하려면 서로 독립된 두 가지가 필요하고, 하나만 있으면 아무 소용이 없습니다.
두 가지
pm2 save 는 현재 프로세스 목록을 ~/.pm2/dump.pm2에 씁니다. PM2가 되살리는 것은 그 파일에 든 것뿐입니다. pm2 delete와 pm2 start는 하면서 저장은 하지 않는 배포 스크립트는 오래된 덤프를 남깁니다. 한 번도 저장한 적이 없다면 아예 비어 있습니다.
pm2 startup 은 부팅할 때 pm2 resurrect를 실행하는 systemd 유닛을 설치합니다. 그 덤프를 읽는 쪽이 이 유닛입니다.
하나만 있으면 아무것도 띄우지 않는 부팅이 되거나, 아무도 읽지 않는 저장 목록이 됩니다.
함정
권한 없는 사용자로 pm2 startup을 실행하면 아무것도 설치되지 않습니다. systemd를 감지하고, 유닛을 설치할 명령을 만들어 화면에 출력한 다음 끝납니다. 출력은 길고 초록색이며, 마지막에 이 명령을 복사해 붙여넣으라는 줄이 붙습니다. 성공한 것처럼 읽힙니다. 실제로는 아무 일도 없었습니다.
그러니 root로 실행하고, 확인은 PM2가 출력한 내용이 아니라 systemd로 하세요.
올바른 순서
저장하기 전에 앱이 떠 있어야 합니다. 그렇지 않으면 빈 목록을 저장하게 됩니다.
# <user> 로 — 앱이 돌고 있는지 확인
pm2 status
# root 로 — 그 사용자용 유닛 설치
env PATH=$PATH:/usr/bin pm2 startup systemd -u <user> --hp /home/<user>
앞에 붙는 env PATH=$PATH:/usr/bin은 장식이 아닙니다. root의 PATH에는 보통 Node가 없고, 유닛은 생성될 때의 경로를 그대로 기록합니다. 이게 없으면 유닛은 존재하지만 부팅 때 node를 찾지 못합니다.
# 다시 <user> 로 — 덤프 쓰기
pm2 save
각각 따로 확인하기
따로 망가지니 따로 확인해야 합니다.
systemctl is-enabled pm2-<user>
enabled가 나와야 합니다. disabled든 Failed to get unit file state든, 다른 결과는 유닛이 설치된 적이 없다는 뜻입니다. PM2가 뭐라고 했는지는 상관없습니다.
grep -o '"name":"[^"]*"' /home/<user>/.pm2/dump.pm2
여기에 <app>이 나와야 합니다. 파일이 없거나 grep이 아무것도 출력하지 않으면, 앱이 떠 있는 상태에서 pm2 save를 돌린 적이 한 번도 없는 것입니다.
재부팅 없이 시험하기
이게 되는지 알아보자고 운영 서버를 재부팅할 필요는 없습니다.
# <user> 로
pm2 kill
# root 로
systemctl start pm2-<user>
# <user> 로
pm2 status
curl -sf http://127.0.0.1:3000/health
<app>이 다시 온라인이면 두 가지 다 제대로 걸린 것입니다. 부팅할 때 일어나는 일이 정확히 이겁니다.
pm2 kill 뒤에는 절대 pm2 save를 실행하지 마세요. 데몬을 죽인 상태에서는 프로세스 목록이 비어 있고, 저장하면 그 빈 상태가 덤프에 그대로 들어가서 방금 시험하던 것을 스스로 지워버립니다. 습관적으로 쳐버렸다면 앱을 다시 띄우고 pm2 save를 다시 실행하세요.
배포 스크립트에 pm2 save 넣기
배포가 프로세스를 건드리는 순간부터 덤프는 어긋나기 시작합니다. 스크립트가 지우고 다시 띄우는 방식이라면 그다음에 저장해야 합니다.
pm2 delete <app> || true
pm2 start ecosystem.config.cjs --only <app>
pm2 save
|| true는 첫 실행에서 의미가 있습니다. 지울 것이 없으면 pm2 delete가 0이 아닌 코드로 끝나고, set -e 아래에서는 스크립트 전체가 거기서 멈춥니다.
마지막 줄이 없어도 위의 모든 게 계속 잘 돌아갑니다. 앱 이름을 바꾸거나 인스턴스 수를 바꾸거나 진입점을 갈아끼우는 배포가 오기 전까지는요. 그 뒤로 덤프는 더 이상 존재하지 않는 프로세스를 가리키고, 다음 재부팅은 그걸 되살립니다.
부팅 순서에 대해 한 가지
생성되는 유닛은 After=network.target입니다. 데이터베이스 뒤에 놓여 있지 않습니다. 콜드 부팅에서는 MariaDB나 PostgreSQL이 연결을 받기 전에 앱이 먼저 떠서, 첫 쿼리에서 실패하고 종료될 수 있습니다.
보통은 저절로 해결됩니다. PM2가 프로세스를 다시 띄우고, 두세 번째 시도쯤에는 데이터베이스가 준비돼 사이트가 올라옵니다. 알아보는 표시는 그 밖에는 멀쩡한 프로세스의 재시작 카운터가 0보다 큰 것으로, pm2 status의 ↺ 열에 나옵니다.
정말로 복구되지 않는다면 그때 순서를 명시하세요.
sudo systemctl edit pm2-<user>
[Unit]
After=network.target mariadb.service
직접 실패하는 것을 본 경우에만 넣으세요. 느린 서비스나 다른 장비에 있는 서비스 뒤에 유닛을 세우면, 재시도해서 살아나던 부팅이 그냥 멈춰 서 있는 부팅으로 바뀝니다.