서버 프로세스를 관리하는 올바른 방법

웹사이트 개발을 완료하고 서비스할 프로덕션(production) 서버에 배치(deploy)할 때 고민하게 되는 문제 중 하나가 서버에서 띄워야 하는 여러 가지 프로세스들을 어떻게 띄울 것인가 하는 문제다. 프로세스가 죽으면 다시 띄우는 것도 물론 필요하다. monit이나 supervisord, god 등은 이런 목적으로 나온 것이다(몇 가지 부가적인 목적이 더 있긴 하다). 하지만 이건 OS에 대한 이해 부족에서 나온 것이다. 프로세스 관리는 OS의 핵심적인 기반이 되는 기능이다. 본 연구에서는 왜 monit 등이 잘못된 선택인지, 올바른 방법은 무엇인지에 대해 다룰 것이다.

프로세스 No. 1 Init

올바른 방법은 OS의 init 시스템, 혹은 그와 유사한 대안 시스템을 사용하는 것이다. init은 유닉스에서 부팅될 때 첫번째로 만들어지는 프로세스다. 그래서 프로세스 번호가 1이고, 이후의 모든 프로세스는 init 프로세스의 자손이 되고, 시스템이 부팅될 때 뜨는 모든 프로세스는 init이 띄우게 된다. respawn이 설정된 프로세스는 죽었을 때 다시 띄우는 역할도 한다. 그러니 서버에 프로세스를 띄워야 한다면 init에 맡기는 것이 자연스럽지 않겠는가.

물론 init 시스템은 낡고 문제점이 있지만, 그건 부팅 과정의 문제점이 크고, 프로세스를 관리하는 데는 별 문제가 없다. 다만, 조금 번거롭긴 하다. 사용법은 Managing Linux daemons with init scripts를 참조하라. 보면 알겠지만 좀 귀찮다.

하지만, 이제 이 init 시스템은 역사 속으로 사라질 예정이다. 아니, 많은 리눅스 배포판에서 이미 사라졌다. 우분투는 이미 수년 전 init을 upstart로 대체했고, 다른 배포판들은 systemd로 방향을 잡았으며 최근에 우분투도 systemd에 합류하기로 했다. 그러니까, 미래는 systemd에 있고, 서버에서 프로세스를 띄우고 관리해야 한다면 systemd를 쓰는 게 정답이다.

monit에서 시작된 흑역사

그런데 왜 init이 있는데 monit 같은 툴들이 등장했을까? 여기에는 약간의 역사가 있다. 수년 전, Ruby on Rails 개발자들 사이에서 monit이 유행을 타기 시작했다. Rails가 뜨기 전까지 웹 개발의 주류는 PHP와 자바였는데, PHP는 아파치에 모듈로 붙으니 따로 프로세스 관리를 할 필요가 없고, 자바 WAS들은 자체적인 프로세스 관리를 탑재했었다. 그러다보니 아마도 웹 개발자가 서버의 프로세스를 어떻게 띄우고 내리고 리스타트시키는지에 대해 고민할 기회가 별로 없었을 것이다. 당시 웹 개발의 시대를 이끈 것은 그 이전에 유닉스/리눅스에 빠져 있던 해커들이 아니었다.

이런 상황에서 Rails가 탄생했는데 Rails를 돌릴 마땅한 애플리케이션 서버가 없어서 mongrel 같은 서버를 루비 개발자들이 직접 만들었다. 그런데 자바 WAS들처럼 프로세스 관리 기능을 통째로 개발해서 탑재하기는 쉽지 않았고(물론 좋은 방법도 아니고) 아직 Rails가 충분히 안정화되지 않은 시기다보니 mongrel 프로세스가 죽는 일이 잦았다. 그러니 프로세스가 죽는지 모니터링하다가 죽으면 살려주자는 발상을 하게 되었고, 그 결과가 monit이다. monit이 Rails + mongrel 조합의 불안정성을 개선해주면서 인기를 끌게 된 것이다.

문제는 앞서서 본 것처럼 이런 목적에는 이미 init 시스템이 있었기 때문에 monit이 필요 없었다는 것이다. 명백한 ReinventTheWheel이다. 물론 바퀴를 더 좋게 개선하면 재발명할 수도 있다. 하지만 monit은 init보다 더 느리고, 더 리소스도 많이 먹었고, 더 불안정했다. running-processes에서는 다음과 같이 표현하고 있다.

“Wow. You reinvented init and cron, but managed to make them both less reliable and consume more CPU than I could’ve imagined.”

monit을 위한 변명

하지만 monit도 할 말이 있었을 것이다. 당시 upstart는 초기였고, systemd는 없었으며, init의 문제점은 오래 전부터 지적되어 왔었다(그게 프로세스 관리와 상관이 있든 없든). 게다가 init의 설정법은 루비 개발자들의 취향과 거리가 멀다. Managing Linux daemons with init scripts에서 보듯 /etc/init.d의 스크립트들은 별로 아름답지 않다. 이런 점들이 ReinventTheWheel을 좋아하는 루비 개발자들의 성향과 맞물렸을 것이다.

그리고 monit에는 init에서 소화하지 않는 기능이 하나 있었다. 프로세스가 죽었을 때 메일 등으로 알림을 보내주는 것이다(물론 이것도 불가능한 것은 아니었지만, 적어도 init의 기능은 아니었다). 이 두 가지가 그나마 monit을 위한 변명이 될 것이다.

유닉스의 철학

하지만, 이 변명으로는 충분하지 않다. 유닉스의 소프트웨어는 일반적인 사용자가 사용하는 소프트웨어와 다른 철학이 반영되어 있다. 여러 개의 목적을 수행하는 사용자 애플리케이션과 달리 유닉스의 프로그램은 여러 목적을 수행하지 않고 하나의 일만 담당하되, 그 일을 가장 잘하도록 설계한다. 그리고 파이프, 리다이렉션, 시그널 등의 기본적인 수단을 통해 이런 프로그램들을 조합해서 복잡한 일을 해낸다. 그런데 monit은 프로세스가 죽으면 다시 살려주는 일과 그것을 개발자에게 알려주는 일을 합쳐놓았다. 당시에 이미 유닉스에 더 좋은 모니터링 도구가 많았기 때문에 monit의 모니터링 기능이 그리 좋은 편은 아니었다. 그러니까 두 일을 묶어놓았는데, 두 일 다 원래 있던 다른 도구들보다 못하는 것이다. 그래서 두 가지 목적을 대충 때우고 싶은 상황에서는 약간의 이점이 있지만, 그에 대한 대가로 시스템 자원도 많이 쓰고, 더 안 좋은 모니터링 서비스를 받게 된 것이다.

그렇다고 monit이 유닉스의 철학에 집중해서 프로세스를 관리하는 일에만 초점을 맞췄다면, 이젠 앞서 이야기한 것처럼 완전한 ReinventTheWheel이 되어 쓸데 없는 짓이 된다. supervisord가 바로 그렇다. supervisord는 init이 하는 일의 부분집합인데 성능이 더 떨어지니 그야말로 티없이 맑고 순수한 잉여인 것이다.

god는 monit보다 훨씬 더 나아갔다. 여러 가지 막강한 기능을 갖추고 있어서 사실 monit이나 supervisord와 같이 묶기 애매할 정도. 그래서 프로세스 관리보다 다른 목적들에 초점을 맞춘다면 그럭저럭 쓸만할 수도 있지만, 여전히 그 분야에 더 좋은 도구가 많이 있기 때문에 의문이 생기는 건 마찬가지다.

현 시점의 정답

upstart

systemd로의 이행이 결정되었으나, 아직도 많은 배포판은 upstart를 기본으로 제공한다. 그래도 systemd를 쓸 수 있긴 하나, 일단은 기본인 upstart를 그냥 계속 쓰다가 systemd가 기본으로 바뀐 배포판을 적용하면서 systemd를 적용해도 된다. 그래서, upstart의 사용법을 간략하게 알아보겠다.

upstart는 /etc/init에 스크립트를 넣으면 실행할 수 있다. 태스크 큐로 널리 쓰이는 celery의 예를 보자. celery 서버는 보통 다음과 같은 명령으로 띄운다.

celery -A myapp.tasks worker -l INFO -E

이러면 작업을 처리하는 worker 프로세스를 띄우게 된다. 이걸 upstart로 관리를 하고 싶으면 /etc/init/celery.conf 파일을 다음과 같이 만든다.

# Celery

description "Celery Worker"
start on runlevel [2345]
stop on runlevel [06]
respawn limit 10 5
script chdir /home/ubuntu/myproject exec sudo -uwebuser celery -A myapp.tasks worker -l INFO -E > /home/ubuntu/logs/celery.log 2>&1 end script

init과는 달리 실행 권한을 줄 필요는 없다. 그러면 서버 띄우기는 다음과 같이 한다.

sudo start celery

종료, 재시작은 다음과 같다. 우분투의 경우 tab을 누르면 어떤 서비스를 띄울 수 있는지 자동 완성이 되므로 편리하다.

sudo stop celery
sudo resart celery

위의 설정 파일에서 respawn 부분은 죽었을 때 다시 실행시키는 설정이다. 죽었을 때 10번까지 5초 간격으로 시작 재시도를 한다. script 부분에 실제로 실행할 코드를 적는다.

한 가지 주의해야 할 점은 cron과 비슷하게 환경 변수가 로드되지 않는다는 것이다. 로케일 등을 환경변수에 의존하고 있다면 LC_ALL 같은 환경 변수를 지정해두어야 한다.

systemd

upstart가 현재라면 systemd는 미래다. 앞으로 대부분의 리눅스 배포판이 systemd로 통합될 것이므로 systemd를 미리 배워두면 좋을 것이다. 물론 지금도 최신 배포판들은 systemd가 기본은 아닐지언정 설치는 대부분 되어 있을 것이고, 설치가 안되어 있더라도 패키지로 설치할 수 있을 것이다. sshd의 예를 보면 이해하기 쉬울 것이다. 우분투 배포판에는  /etc/systemd/system/sshd.service 파일이 다음과 같이 작성되어 있다.

[Unit]
Description=OpenBSD Secure Shell server
After=network.target auditd.service
ConditionPathExists=!/etc/ssh/sshd_not_to_be_run

[Service]
EnvironmentFile=-/etc/default/ssh
ExecStart=/usr/sbin/sshd -D $SSHD_OPTS
ExecReload=/bin/kill -HUP $MAINPID
KillMode=process
Restart=on-failure

[Install]
WantedBy=multi-user.target
Alias=sshd.service

시작, 재시작 등은 다음과 같이 한다.

systemctl start sshd.service
systemctl stop sshd.service
systemctl restart sshd.service

커뮤니티가 systemd를 선택한 이유

그런데 왜 upstart라는 init의 대안이 이미 나왔고, 수년 간 검증도 되었는데 굳이 또 systemd라는 게 나와서 대체하게 되었을까? 물론 upstart를 밀었던 우분투는 systemd로 전환하고 싶어하지 않았는데, 우분투의 부모라고 할 수 있는 데비안 커뮤니티에서 systemd로 전환하기로 결정하는 바람에 우분투도 대세를 따랐다. 그럼 upstart는 init의 어떤 점들을 개선하려 했으며, 또 systemd는 upstart의 어떤 점이 마음에 들지 않았을까?

init의 가장 큰 단점은 태스크가 병렬로 실행된다는 것이다. 그래서 I/O가 있는 작업은 많으면 부팅 속도가 느려지게 된다. 그래서 upstart는 이벤트 기반으로 작성되어 비동기로 작업을 처리한다. 그리고 sysv 방식의 init에 비해 설정이 간편하다. 필자가 체감하기에는 systemd보다도 간편한데, 이 점에서는 의견이 갈리는 듯 하다. 

그런데 이 upstart가 여러 배포판에 퍼져가던 중에 비판을 받기 시작했다. 그 중에 가장 상세하게 설명이 된 글이 Rethinking PID 1이다. 이 글에서는 먼저 좋은 init 시스템은 다음과 같은 특성을 가져야 한다고 정의했다.

그런데 upstart는 의존성을 설정할 수 있어서 서비스 간에 의존성이 설정되어 있으면 의존성 순서대로 실행이 된다. 기껏 비동기 로직을 적용했는데 결국 직렬화를 해야 하니 성능상의 이득이 줄어든다. 그리고, 당장 필요하지 않은 서비스도 올려두게 되므로 처음부터 많은 프로세스가 뜨게 된다. 반면, systemd는 많은 서비스가 on-demand로 뜰 수 있다. 예를 들어 sshd는 부팅할 때 띄우지 않고 있다가 22번 포트로 요청이 오면 그 때 띄우는 것이다. 의존성도 이런 식으로 설정된다. 그러니까 부팅 속도도 빠르고, 불필요한 서비스는 올라가지 않아서 시스템 자원도 아낀다. 시스템 자원을 아낀다는 점은 애플리케이션 서버의 프로세스를 관리하는 입장에서도 유익하다. 그리고, 그런 관점에서도 여전히 monit이나 supervisord는 잉여다.

Debate init system에 init, systemd, upstart에 대한 상세한 비교가 올라와 있으니 이것도 읽어보면 재미있을 것이다.

올바른 방법의 추구

앞서 monit이 왜 등장했는지에 대한 역사적 배경은 설명했다. 그런데, 여기서 조금 비약해서 태도의 문제까지 거론해보고자 한다. 왜 monit이나 supervisord 개발자는 init을 놔두고 이런 걸 개발했을까? 나는 그것이 문제해결 우선주의에 있다고 본다. 현재 눈앞의 상황을 해결하는 것에만 초점을 맞추면 저런 솔루션들이 떠오를 수 있다. monit 개발자가 접했던 상황에서 그는 어떤 질문들을 던졌을까? 아마도 what is the right way to start unix process는 아니었을 것이다. 그보다는 how to figure out a process running or not, 혹은 how to respawn process 같은 질문이 아니었을까? 그래서 그런 질문들에 대한 개별적인 답을 모으면 monit을 개발할 수 있었을 것이다. 그리고 당면과제는 그걸로 해결된다. 만세.

필자는 이것을 엔지니어의 자세 문제라고 본다. 엔지니어는 일단 본능적으로 눈앞의 문제를 해결하는데 집중하게 마련이다. 이것은 좋은 자세다. 그러나, 여기에 그치면 안된다. 그러면서도 계속 의심을 품어야 하고, 올바른 방법이 무엇인가를 고민해야 한다. monit이나 supervisord 관련해서 올라오는 질문들을 보면 재미있는 사실을 하나 알게 된다. monit으로 띄우려는 프로세스 이전에 monit 자체 프로세스가 떠 있어야 한다는 것이다. 그래서 supervisord를 어떻게 떠 있게 하나요? 같은 질문이 올라오곤 한다. supervisord를 supervisord로 떠 있게 만들 수는 없으니까. 이 점을 곰곰히 생각해보면 init을 몰랐더라도 아, OS에 프로세스를 띄우는 역할을 하는 무언가가 있겠구나 하는 것을 짐작할 수 있다. 그리고 답변에는 어김없이 init이나 upstart가 달리는데, 그러면 그 답변들을 보면서 init이나 upstart로 supervisord의 일을 하게 하면 되지 않을까 하는 생각도 할 법 하다. 그런데 왜 그런 생각에 이르지 못할까? 너무 당면 과제에만 초점이 맞춰져 있기 때문이다. 한 발 물러서서 잠깐 생각할 여유가 있었다면 supervisord는 나오지 않았을 것이다. 

린 스타트업을 주장하고 당면과제에 집중하는 것을 강조하는 평소 필자의 생각을 알고 있는 사람들에게는 이런 올바른 방법의 추구가 어색하게 느껴질 수도 있을 것이다. 하지만 올바른 방법이 올바른 이유는 더 효율적이기 때문이다. supervisord 개발자가 자신의 당면과제를 직접 개발해서 해결하려는 대신, 올바른 방법이 무엇인지 잠깐 고민해봤다면 supervisord를 개발할 시간에 간단한 설정 파일만 만들면 되었을 테니 훨씬 빠른 시간 안에 문제를 해결했을 것이고, 그 해결책의 품질도 훨씬 좋았을 것이다.

당면 과제 해결에만 급급해온 개발자에겐 이상한 노하우가 축적되어 있어서 간단한 방법을 놔두고 어려운 방법을 쓰거나, 자신만의 라이브러리로 문제를 해결해서 유지보수가 어렵게 되는 경우가 많다. 반면, 올바른 방법을 추구해온 개발자는 플랫폼의 특성을 이해하고 관례를 존중하기 때문에 대체로 더 간결하고 효율적인 방법을 선택하게 된다. monit이나 supervisord, god는 이제 역사 속으로 흘려보내고 타산지석으로 삼도록 하자.


연구 기사 | OS리눅스