지난 글(AWS t3.micro에서 스프링 부트가 자꾸 죽던 이유)에서 OOM으로 서버가 죽었던 걸 다뤘다. 스왑을 추가하고 힙을 제한해서 메모리 부족 자체는 완화했지만, 근본적인 문제가 하나 더 있었다 — 그동안 이 앱을 nohup java -jar ... 같은 방식으로 SSH 세션에서 수동으로 띄우고 있었다는 것이다.
인스턴스가 재부팅되면 다시 켜지지 않는다 — 사람이 SSH로 접속해서 다시 실행해줘야 한다
프로세스가 죽으면 그걸로 끝 — 재시작해주는 사람이 없으면 계속 죽어있는 채로 방치된다
재배포할 때 실수하기 쉽다 — 실제로 OOM 당시 커널 로그를 뜯어보니 java 프로세스가 두 개 떠 있던 흔적이 있었다. 재배포하면서 이전 프로세스를 안 죽이고 새로 하나 더 띄운 것으로 추정된다. 메모리가 빠듯한 인스턴스에서 프로세스가 중복으로 떠 있으면 OOM을 자초하는 셈이다
이 세 가지를 한 번에 해결하는 방법이 systemd 서비스로 등록하는 것이다.
/etc/systemd/system/stock-backend.service
[Unit]
Description=Stock Disclosure Crawler Backend
After=network.target mariadb.service
Requires=mariadb.service
[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/stock/backend
ExecStart=/usr/bin/java -Xmx400m -jar /home/ubuntu/stock/backend/backend-0.0.1-SNAPSHOT.jar
EnvironmentFile=/home/ubuntu/stock/backend/app.env
Restart=always
RestartSec=10
SuccessExitStatus=143
[Install]
WantedBy=multi-user.target
항목별로 짚어보면:
After=network.target mariadb.service / Requires=mariadb.service: 네트워크와 MariaDB가 먼저 뜬 다음에 이 서비스를 시작하라는 뜻. DB 연결이 필요한 앱인데 DB보다 먼저 뜨면 초기 연결에 실패할 수 있어서 순서를 강제했다.ExecStart에 -Xmx400m: 지난 글에서 다룬 힙 제한을 여기서 적용한다.EnvironmentFile: DB 비밀번호, 텔레그램 봇 토큰 같은 값을 소스코드에 하드코딩하지 않고 별도 env 파일로 분리해서 여기서 주입한다. 스프링 부트의 application.properties에는 ${DB_PASSWORD:...} 형태로 환경변수를 우선 읽도록 설정해뒀다.Restart=always / RestartSec=10: 어떤 이유로든 프로세스가 죽으면 10초 뒤에 자동으로 다시 띄운다. 이제 OOM이 다시 발생해도 사람이 손대지 않아도 짧은 다운타임 후 스스로 복구된다.SuccessExitStatus=143: systemctl stop으로 정상 종료시킬 때 SIGTERM(종료 코드 143)을 보내는데, 이걸 "성공적인 종료"로 안 쳐주면 systemd가 이걸 "비정상 종료"로 착각해서 다시 살리려고 시도한다. 그러면 정상적으로 서비스를 내리고 싶어도 계속 재시작되는 웃긴 상황이 생긴다. 이 한 줄로 그걸 방지한다.
sudo cp stock-backend.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable stock-backend # 부팅 시 자동 시작
sudo systemctl enable mariadb # DB도 마찬가지로
sudo systemctl start stock-backend
sudo systemctl status stock-backend
enable을 빼먹으면 지금 당장은 잘 돌아가도, 인스턴스를 재부팅하는 순간 다시 예전처럼 수동으로 띄워줘야 하는 상황으로 돌아간다. 이번에 재부팅 뒤에 서버가 안 켜져서 놀랐던 게 바로 이걸 빼먹어서였다.
systemd로 전환한 뒤, 재배포할 때 FileZilla로 새 jar를 덮어쓰기만 하고 서비스는 재시작을 안 한 적이 있다. 그랬더니 백그라운드 스케줄러는 멀쩡히 도는데 API 요청만 오면 연결이 끊기는 애매한 장애가 났다. 파일만 덮어쓴다고 실행 중인 프로세스에 반영되는 게 아니라는 걸 몸으로 배웠다 — 이 디버깅 과정은 내용이 꽤 길어서 별도 글(재배포하다 서버를 반쯤 죽인 이야기)로 따로 정리하겠습니다.
교훈만 요약하면, 배포 순서를 이렇게 확정했다:
sudo systemctl stop stock-backend # 먼저 내리고
# (여기서 FileZilla로 새 jar 업로드)
sudo systemctl start stock-backend # 그다음 새로 띄우기
sudo systemctl status stock-backend
● stock-backend.service - Stock Disclosure Crawler Backend
Loaded: loaded (/etc/systemd/system/stock-backend.service; enabled; preset: enabled)
Active: active (running) since ...
Main PID: XXXX (java)
enabled와 active (running)이 같이 떠 있으면 재부팅에도, 크래시에도 살아남는 구조가 완성된 것이다.
.service 파일 하나) 재현 가능하고, 실수를 구조적으로 줄일 수 있다. 사람이 매번 명령어를 외워서 치는 것보다 훨씬 안전하다.다음 글에서는 이렇게 자동 복구 체계를 만들어놓고도 놓칠 수 있는 부분, "장애가 났다는 걸 사람이 어떻게 바로 알 수 있는가"를 CloudWatch 알람으로 해결한 과정을 정리한다.