LODY/정리

Ubuntu 서버 운영 / 프로세스와 포트, 그리고 systemd

프로세스와 포트, 그리고 systemd

🟣 포트를 이미 누가 쓰고 있다

서버에서 서비스를 처음 띄울 때 가장 자주 만나는 오류가 이것이다. bind: address already in use. 8080 포트로 앱을 실행하려는데 이미 다른 프로세스가 그 포트를 붙잡고 있다는 뜻이다. 검색하면 대개 "그 프로세스를 찾아 죽여라"라고 나오고, 명령어를 복사해 붙이면 해결된다. 그런데 그 명령어가 실제로 무엇을 하는지 파고들면, 앞서 정리한 리눅스 시스템 프로그래밍이 그대로 깔려 있다.

포트를 붙잡는다는 것은 어떤 프로세스가 그 포트에 소켓을 열고 listen 상태로 두었다는 뜻이다. 소켓도 결국 파일 디스크립터이고, 커널은 어떤 프로세스가 어떤 포트를 열고 있는지 표로 들고 있다. 포트를 비우려면 그 소켓을 쥔 프로세스를 찾아 정리하면 된다.


🟣 누가 그 포트를 쥐고 있나

포트를 점유한 프로세스를 찾는 도구가 sslsof다. 예전에는 netstat을 많이 썼는데, 요즘 우분투에서는 ss가 기본이고 더 빠르다.

# 8080 포트를 LISTEN 중인 프로세스 찾기
ss -ltnp | grep :8080
 
# lsof로 같은 것 찾기 (포트를 연 프로세스의 PID가 나온다)
lsof -i :8080

ss -ltnp의 각 글자가 의미를 담는다. l은 listen 중인 것, t는 TCP, n은 포트를 이름 대신 숫자로, p는 그 소켓을 쥔 프로세스를 함께 보여 달라는 뜻이다. 출력에 나오는 PID가 그 포트를 붙잡은 프로세스다. 커널이 소켓과 프로세스의 연결을 표로 들고 있기 때문에 이 조회가 가능하다.


🟣 죽이는 것도 통신이다: 시그널

PID를 찾았으면 kill로 종료한다. 그런데 kill이라는 이름은 오해를 부른다. 이 명령은 프로세스를 강제로 없애는 게 아니라 시그널을 보내는 것이다. 리눅스 시스템 프로그래밍에서 시그널을 프로세스 간 통신의 한 형태로 봤던 그 개념이 여기서 실무 명령으로 나타난다.

kill 12345        # 기본 SIGTERM: "정리하고 끝내라"
kill -9 12345     # SIGKILL: "즉시 강제 종료"

기본값인 SIGTERM(15번)은 프로세스에게 "종료를 준비하라"고 정중히 요청하는 신호다. 프로세스는 이 신호를 받아 열어 둔 파일을 닫고, 처리 중인 요청을 마무리하고, 깨끗이 끝낼 수 있다. 반면 -9SIGKILL은 프로세스가 가로챌 수 없는 신호라 커널이 즉시 없앤다. 강력하지만 뒷정리를 못 하고 죽으니, 데이터를 쓰던 중이면 손상될 수 있다.

그래서 순서가 있다. 먼저 SIGTERM으로 정중히 요청하고, 그래도 안 끝나면 SIGKILL을 쓴다. 습관적으로 kill -9부터 쓰는 것은 정상 종료 절차를 건너뛰는 것이라, 앞 책에서 본 좀비 회수나 자원 정리 같은 마무리를 프로세스가 못 하게 만든다.

SIGTERM (15)정리하고 종료프로세스뒷정리 후 종료SIGKILL (9)안 끝날 때 강제정상 경로최후 수단

파랑이 정상 경로, 빨강이 최후 수단이다. 웬만하면 파랑으로 끝나야 하고 빨강은 어쩔 수 없을 때만 쓴다. 이 구분을 알고 나면 서비스를 재시작할 때 데이터가 왜 가끔 깨지는지가 설명된다.


🟣 계속 떠 있게 하기: 데몬과 systemd

포트를 비우고 서비스를 다시 띄웠다. 그런데 터미널을 닫으면 프로세스도 함께 죽는다. 서버에서 서비스는 사람이 로그아웃해도 계속 떠 있어야 하는데 말이다. 여기서 데몬이라는 개념이 필요해진다. 데몬은 터미널에 매이지 않고 백그라운드에서 상시 도는 프로세스다.

예전에는 프로세스를 직접 백그라운드로 돌리고(nohup, &) 죽으면 다시 살리는 스크립트를 짜야 했다. 지금 우분투는 이 일을 systemd가 맡는다. systemd는 부팅 직후 커널이 가장 먼저 실행하는 프로세스, 곧 앞 책에서 본 PID 1이다. 고아가 된 프로세스를 입양해 회수하던 그 init이, 현대 리눅스에서는 서비스 전체의 생명주기를 관리하는 systemd로 확장됐다.

서비스를 systemd에 등록하면 유닛 파일 하나로 상시 가동을 맡길 수 있다.

[Unit]
Description=My App Server
 
[Service]
ExecStart=/usr/bin/node /srv/app/server.js
Restart=on-failure
User=appuser
 
[Install]
WantedBy=multi-user.target

Restart=on-failure가 핵심이다. 프로세스가 비정상 종료되면 systemd가 자동으로 다시 띄운다. User=appuser는 앞 장에서 본 최소 권한 원칙의 실천이다. root가 아니라 전용 계정으로 서비스를 돌려, 혹시 뚫려도 피해 범위를 좁힌다. 이렇게 등록하면 서버가 재부팅되어도 서비스가 자동으로 살아난다.

sudo systemctl enable --now myapp   # 등록하고 지금 시작
systemctl status myapp              # 상태 확인
journalctl -u myapp -f              # 로그 실시간 확인

🟣 정리

  • 포트 점유 오류는 어떤 프로세스가 그 포트에 소켓을 열고 있다는 뜻이다. 소켓도 파일 디스크립터이고 커널이 소켓과 프로세스의 연결을 표로 들고 있다.
  • ss -ltnplsof -i로 포트를 쥔 프로세스를 찾는다. 각 플래그가 조회 조건을 정한다.
  • kill은 강제 종료가 아니라 시그널 전송이다. SIGTERM으로 정중히 요청하고, 안 되면 SIGKILL을 쓴다. 순서를 지켜야 뒷정리가 된다.
  • 서비스는 데몬으로 상시 떠 있어야 한다. systemd가 그 역할을 맡고, 이는 앞 책에서 본 PID 1(init)의 확장이다.
  • Restart=on-failure로 자동 복구, User=로 최소 권한 실행. 앞 장의 권한 원칙이 서비스 운영에서 그대로 쓰인다.

셸에서 시작해 권한을 지나 프로세스와 서비스까지, 서버에 접속한 순간부터 서비스가 상시 가동되기까지의 흐름을 따라왔다. 소재는 명령어 몇 개였지만, 그 아래에는 예외 없이 리눅스 시스템 프로그래밍에서 본 프로세스, 시그널, PID 1이 깔려 있었다. 짧은 팁을 그 CS 기반으로 설명할 수 있게 된 것이 이 책에서 남긴 것이다.