502 Bad Gateway 오류 원인과 해결법
서버를 운영하다가 브라우저에 502 Bad Gateway라는 에러가 뜨면 심장이 덜컥합니다. 방금까지 잘 되던 사이트가 갑자기 이 화면을 보여주면 뭐부터 해야 할지 막막해집니다. 502 에러는 서버가 완전히 죽은 건 아닌데 뭔가 내부에서 꼬인 상태라서, 원인을 모르면 한참 헤매게 됩니다. 이번 글에서는 502 Bad Gateway가 정확히 무슨 뜻인지, 어떤 상황에서 발생하는지, 각 원인별로 어떻게 해결하는지를 정리해보겠습니다.
502 Bad Gateway가 의미하는 것
502 에러를 이해하려면 먼저 서버 구조를 떠올려야 합니다. 이전 글에서 다뤘던 배포 구성을 생각해보면, 사용자의 요청은 Nginx가 먼저 받고 뒤쪽의 앱 서버로 전달합니다. Nginx가 앞단에서 리버스 프록시 역할을 하고, Node.js나 Python 같은 앱 서버가 뒤에서 실제 처리를 하는 구조입니다.
502 Bad Gateway는 Nginx가 뒤쪽의 앱 서버에 요청을 전달하려고 했는데 유효한 응답을 받지 못했을 때 발생합니다. Nginx 자체는 정상적으로 동작하고 있지만 뒤쪽 서버가 응답을 주지 않는 상황입니다. 그래서 502 에러가 뜨면 Nginx는 살아있고 문제는 뒤쪽에 있다는 뜻입니다.
비유하자면 식당의 홀 직원은 정상 출근해서 주문을 받고 있는데, 주방에서 요리가 안 나오는 상태입니다. 손님한테 “주방에 문제가 있습니다”라고 알리는 게 502 에러입니다.
가장 흔한 원인 — 앱 서버가 꺼져 있는 경우
502 에러의 가장 흔한 원인은 앱 서버가 돌아가고 있지 않은 겁니다. PM2로 관리하던 앱이 에러로 종료됐거나, 서버를 재시작한 후에 앱이 자동으로 뜨지 않은 경우입니다.
가장 먼저 앱이 실행 중인지 확인합니다.
pm2 list
상태가 online이 아니라 stopped나 errored로 나오면 앱이 꺼져 있는 겁니다. 로그를 확인해서 왜 종료됐는지 파악합니다.
pm2 logs 앱이름
로그에 에러 메시지가 나오면 해당 에러를 수정해야 합니다. 환경 변수가 빠져 있거나, 데이터베이스 접속이 실패하거나, 코드에 문법 에러가 있는 경우가 많습니다.
앱을 다시 시작합니다.
pm2 restart 앱이름
시작 후에 다시 pm2 list로 상태가 online인지 확인합니다. 시작하자마자 바로 errored로 바뀐다면 앱 자체에 문제가 있는 것이니 로그를 꼼꼼히 봐야 합니다.
PM2를 사용하지 않는 환경이라면 앱 프로세스가 실행 중인지 직접 확인합니다.
ps aux | grep node
Node.js 프로세스가 목록에 없으면 앱이 꺼져 있는 겁니다.
포트 불일치 문제
Nginx 설정에서 proxy_pass에 적힌 포트와 앱이 실제로 실행되는 포트가 다르면 502 에러가 납니다.
Nginx 설정을 확인합니다.
sudo cat /etc/nginx/sites-enabled/today-play
proxy_pass 부분을 봅니다.
proxy_pass http://localhost:3000;
여기서 3000번 포트로 요청을 보내도록 되어 있다면, 앱도 3000번에서 돌아가고 있어야 합니다. 앱이 실제로 어떤 포트에서 실행 중인지 확인합니다.
ss -tlnp | grep node
이 명령어는 node 프로세스가 어떤 포트에서 리스닝하고 있는지 보여줍니다. 앱이 8080 포트에서 돌아가고 있는데 Nginx가 3000으로 보내고 있다면 당연히 연결이 안 됩니다.
해결 방법은 둘 중 하나입니다. 앱의 포트를 Nginx 설정에 맞추거나, Nginx 설정의 포트를 앱에 맞추면 됩니다. 앱의 포트는 보통 환경 변수로 설정하니까 .env 파일에서 PORT 값을 확인합니다.
Nginx 설정을 수정했으면 반드시 문법 확인 후 리로드합니다.
sudo nginx -t
sudo nginx -s reload
앱 서버가 과부하인 경우
앱이 실행 중이고 포트도 맞는데 502가 발생한다면 앱 서버가 요청을 처리할 여유가 없는 상태일 수 있습니다. 요청이 갑자기 몰렸거나, 특정 요청이 너무 오래 걸려서 다른 요청까지 밀리는 경우입니다.
이전 글에서 다뤘던 htop으로 서버 상태를 확인합니다.
htop
CPU가 100%에 가깝거나 메모리가 거의 다 찼다면 과부하 상태입니다. 어떤 프로세스가 리소스를 많이 사용하는지 확인하고, 이전 글에서 다뤘던 서버 느려짐 진단 방법을 따라가면 됩니다.
Node.js는 기본적으로 싱글 스레드이기 때문에 CPU를 많이 사용하는 작업이 하나라도 있으면 다른 요청이 전부 대기하게 됩니다. 이런 경우 PM2의 클러스터 모드를 사용하면 CPU 코어 수만큼 프로세스를 띄워서 부하를 분산할 수 있습니다.
pm2 start app.js -i max
-i max 옵션은 CPU 코어 수에 맞춰서 프로세스를 자동으로 생성합니다.
데이터베이스 연결 실패
앱이 데이터베이스에 접속하지 못하면 요청을 처리할 수 없고, 결과적으로 Nginx에 응답을 주지 못해서 502가 발생합니다.
데이터베이스가 실행 중인지 확인합니다.
sudo systemctl status mysql
active 상태가 아니면 데이터베이스가 꺼져 있는 겁니다. 다시 시작합니다.
sudo systemctl start mysql
데이터베이스가 실행 중인데도 앱에서 연결이 안 된다면 몇 가지를 확인해야 합니다. 앱의 환경 변수에 적힌 데이터베이스 접속 정보가 맞는지, 데이터베이스 사용자에게 접속 권한이 있는지, 최대 연결 수에 도달하지 않았는지 확인합니다.
MySQL의 현재 연결 수를 확인하려면 이렇게 합니다.
mysql -u root -p -e "SHOW STATUS LIKE 'Threads_connected';"
최대 허용 연결 수와 비교합니다.
mysql -u root -p -e "SHOW VARIABLES LIKE 'max_connections';"
현재 연결 수가 최대 연결 수에 가깝다면 앱에서 연결을 제대로 반환하지 않고 있거나, 커넥션 풀 설정이 맞지 않는 겁니다.
메모리 부족으로 앱이 죽는 경우
서버 메모리가 부족하면 리눅스의 OOM Killer(Out of Memory Killer)가 메모리를 많이 사용하는 프로세스를 강제 종료합니다. 앱 서버가 OOM Killer에 의해 죽으면 502 에러가 발생합니다.
OOM Killer가 동작했는지 확인하려면 시스템 로그를 봅니다.
sudo dmesg | grep -i "oom\|killed"
“Out of memory: Killed process”라는 메시지가 보이면 메모리 부족으로 프로세스가 종료된 겁니다. 어떤 프로세스가 죽었는지도 로그에 나옵니다.
이 경우 해결 방법은 몇 가지가 있습니다. 서버의 메모리를 늘리는 게 가장 확실합니다. 당장 메모리를 늘릴 수 없다면 이전 글에서 다뤘던 스왑 파일을 추가해서 임시로 대응할 수 있습니다. 앱에서 메모리 누수가 발생하고 있다면 코드를 수정해야 합니다.
Node.js의 메모리 사용량을 제한하는 방법도 있습니다. PM2에서 max_memory_restart 옵션을 설정하면 앱이 지정한 메모리를 넘었을 때 자동으로 재시작합니다.
pm2 start app.js --max-memory-restart 512M
512MB를 넘으면 앱을 재시작해서 메모리를 반환합니다. 근본적인 해결은 아니지만 OOM Killer에 의해 갑자기 죽는 것보다는 나은 대응입니다.
Nginx 버퍼 크기 문제
앱 서버가 보내는 응답이 Nginx의 버퍼 크기보다 크면 502 에러가 발생할 수 있습니다. 응답 헤더가 매우 크거나 응답 본문이 큰 경우에 생기는 문제입니다.
Nginx 에러 로그에 “upstream sent too big header” 같은 메시지가 있으면 이 문제입니다.
sudo tail -50 /var/log/nginx/error.log
해결하려면 Nginx 설정에서 버퍼 크기를 늘려줍니다.
server {
...
location / {
proxy_pass http://localhost:3000;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
...
}
}
proxy_buffer_size는 응답 헤더를 읽을 때 사용하는 버퍼 크기이고, proxy_buffers는 응답 본문을 읽을 때 사용하는 버퍼입니다. 기본값이 작아서 문제가 생기는 경우 위처럼 늘려주면 해결됩니다.
설정을 변경한 후에는 반드시 확인하고 리로드합니다.
sudo nginx -t
sudo nginx -s reload
타임아웃 문제
앱 서버가 응답을 보내기는 하는데 너무 오래 걸리면 Nginx가 기다리다가 포기합니다. 이때는 502가 아니라 504 Gateway Timeout이 나오는 경우가 더 많지만, 상황에 따라 502가 나올 수도 있습니다.
Nginx의 타임아웃 설정을 확인하고 필요하면 늘려줍니다.
server {
...
location / {
proxy_pass http://localhost:3000;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
...
}
}
proxy_connect_timeout은 앱 서버에 연결을 맺기까지의 대기 시간이고, proxy_read_timeout은 앱 서버가 응답을 보내기까지의 대기 시간입니다. 기본값은 60초인데, 특별히 오래 걸리는 작업이 있다면 늘려줄 수 있습니다.
다만 타임아웃을 무작정 늘리는 건 좋지 않습니다. 응답이 60초 이상 걸린다는 건 근본적으로 뭔가 문제가 있는 겁니다. 쿼리가 너무 무겁거나 외부 API 호출이 느리거나 코드에 비효율적인 부분이 있을 수 있습니다. 타임아웃을 늘리는 건 임시 대응이고, 원인을 찾아서 응답 시간 자체를 줄이는 게 맞습니다.
소켓 파일 문제
앱 서버와 Nginx가 TCP 포트 대신 유닉스 소켓으로 통신하는 경우, 소켓 파일의 권한이나 경로 문제로 502가 발생할 수 있습니다.
Nginx 설정에서 proxy_pass가 이런 형태라면 소켓 통신을 사용하고 있는 겁니다.
proxy_pass http://unix:/var/run/myapp.sock;
이 경우 해당 소켓 파일이 존재하는지, Nginx 프로세스가 소켓 파일에 접근할 수 있는 권한이 있는지 확인해야 합니다.
ls -l /var/run/myapp.sock
파일이 없으면 앱이 소켓을 생성하지 못한 겁니다. 파일은 있는데 권한이 맞지 않으면 Nginx가 읽을 수 없습니다. 이전 글에서 다뤘던 chmod와 chown으로 권한을 조정합니다.
502 에러 진단 순서 정리
502 에러가 발생했을 때 확인하는 순서를 정리하면 이렇습니다.
첫째, 앱 서버가 실행 중인지 확인합니다. pm2 list 또는 ps aux로 프로세스가 살아있는지 봅니다. 꺼져 있으면 재시작하고 로그에서 종료 원인을 확인합니다.
둘째, 앱이 올바른 포트에서 실행 중인지 확인합니다. ss -tlnp로 리스닝 포트를 확인하고 Nginx 설정의 proxy_pass 포트와 일치하는지 봅니다.
셋째, Nginx 에러 로그를 확인합니다. sudo tail -50 /var/log/nginx/error.log에서 구체적인 에러 메시지를 찾습니다.
넷째, 서버 리소스를 확인합니다. htop으로 CPU와 메모리 상태를 보고, dmesg로 OOM Killer가 동작했는지 확인합니다.
다섯째, 데이터베이스 등 외부 의존성을 확인합니다. 앱이 의존하는 서비스들이 전부 정상인지 점검합니다.
이 순서대로 확인하면 대부분의 502 에러 원인을 찾을 수 있습니다.
마무리
502 Bad Gateway는 Nginx와 앱 서버 사이의 통신에 문제가 생겼다는 신호입니다. 대부분의 경우 앱 서버가 꺼져 있거나, 포트가 안 맞거나, 서버 리소스가 부족한 게 원인입니다. Nginx 에러 로그가 가장 중요한 단서를 제공하기 때문에 502가 보이면 가장 먼저 에러 로그를 확인하는 습관을 들이면 좋습니다. 원인만 알면 해결은 대부분 간단합니다. 다음 글에서는 서버의 기본적인 방화벽 설정 도구인 UFW 사용법을 정리해보겠습니다.
Leave a Reply