로컬서버를 지킬 때

재부팅·서비스·예약 작업

서버를 세운 뒤 계속 돌게 지키는 일에서 넘어진 자리를 모았습니다. 서버를 세울 때의 함정은 로컬서버를 세울 때에, 백업이 실제로 되고 있는지 확인하는 법은 백업이 진짜 백업인지에 있습니다. 사례는 품앗이 자체 서버와 함께 일한 교육원·연합회 현장의 서버에서 나왔고, 기계 이름과 주소는 지웠습니다.

요지

서버는 아무도 보지 않는 동안에도 돌아야 하고, 대부분의 사고는 사람이 보지 않는 그 시간에 조용히 납니다. 상시 프로그램은 서비스로 올리고, 고쳤으면 재시작까지 하고, 재시작 설정이 기계를 먹지 않게 상한을 겁니다. 재부팅 뒤의 판정은 기다렸다가 이름 목록으로 하고, 새 예약 작업은 이미 도는 예약을 세 곳 다 본 뒤에 겁니다.

1. 상시 프로그램은 서비스로, 재시작까지가 수정이다

손으로 띄운 프로그램은 재부팅에서 죽습니다. 한 영농형 태양광 연합회의 상담 AI가 쓰던 지식 그래프 데이터베이스는 터미널을 닫아도 계속 돌게 하는 명령으로 손으로 띄워 둔 상태였습니다. 서버가 재부팅되자 그 데이터베이스는 죽었고 아무도 다시 띄우지 않았습니다. 상담 AI는 그 데이터베이스가 응답하지 않으면 조용히 빈손으로 넘어가도록 짜여 있어서, 열흘 동안 그 지식 없이 답했고 오류도 경보도 없었습니다. 이 사례는 현장 AX 사례 5번에도 짧게 적혀 있습니다.

  • 늘 떠 있어야 하는 프로그램은 운영체제의 서비스로 올립니다. 손으로 띄운 것을 발견하면 그 자리에서 서비스로 옮깁니다.
  • 조용한 대체 동작은 장애를 숨깁니다. 대체 경로가 있는 기능은 「대답은 한다」로 건강을 판단하지 말고, 어느 출처에서 답이 나왔는지를 따로 잽니다.

고쳤으면 재시작까지 해야 고친 것입니다. 늘 떠 있는 프로그램의 코드를 고치고 재시작을 빠뜨린 일이 있습니다. 메모리에 올라 있던 옛 코드가 나흘 더 돌면서 같은 문제를 다시 냈고, 그 경위는 라이브를 실험장으로 쓰지 마라에 있습니다. 확진하려면 파일의 수정 시각과 그 프로세스의 시작 시각을 나란히 봅니다. 파일이 더 새롭다면 그 프로세스는 옛 코드로 돌고 있습니다. 수정이 됐는지는 「파일에 들어 있나」가 아니라 다음 실행이 낸 산출물로 확인합니다.

2. 자동 재시작 설정이 기계를 먹통으로 만든다

품앗이 자체 서버 한 대가 어느 날 먹통이 됐습니다. 같은 포트를 쓰는 서비스가 사용자 단위와 시스템 단위로 두 번 등록돼 있었고, 뒤늦게 뜬 쪽이 「포트가 이미 쓰이고 있다」며 3초마다 재시작하고 있었습니다. 3.4일 동안 90,647번 재시작했고, 로그가 3.2GB 쌓이면서 디스크 입출력이 포화돼 기계 전체가 굼떠졌고, 재부팅해도 풀리지 않았습니다.

「무한 재시작은 운영체제가 알아서 멈춘다」는 통념이 여기서 깨집니다. 기본 재시작 제한은 「10초 안에 5번」을 막는 문인데, 3초 간격은 10초에 세 번이라 그 문을 영원히 통과합니다.

  • 늘 재시작하게 설정할 때는 재시작 간격과 재시작 제한 창을 같이 정합니다. 간격 × 허용 횟수가 제한 창 안에 들어가야 제한이 실제로 걸립니다.
  • 포트를 쓰는 서비스는 만들기 전에 그 포트를 누가 쓰고 있는지 봅니다. 같은 서비스를 사용자 단위와 시스템 단위에 두 번 등록하지 않습니다.
  • 로그 용량에 상한을 걸어 두면 루프가 나도 디스크는 죽지 않습니다.

이 구성이 사이트 다운을 가린 일도 있습니다. 한 교육원 서버를 새로 세우면서 앱이 기대던 실행 환경이 빠졌는데, 늘 재시작하는 설정 때문에 그 앱들이 「실패」 상태로 떨어지지 않고 계속 기동만 시도했습니다. 실패 목록에 아무것도 없으니 두 사이트가 내려가 있는 것을 하루 동안 아무도 몰랐습니다. 사람이 자주 보지 않는 사이트일수록 재시작 제한을 걸어, 끝내 안 뜨면 실패로 드러나게 합니다.

반대쪽 함정으로, 무거운 데이터베이스 서비스의 재시작 제한을 너무 빡빡하게 잡았더니, 메모리 압박이 몇 분 이어지는 사이 허용 횟수가 바닥났고 서비스가 실패 상태로 굳어 백업이 이틀 끊겼습니다. 무거운 서비스는 재시작 간격을 길게, 제한 창을 넉넉하게 잡습니다.

3. 재부팅 복구 검증의 착시 셋

품앗이 자체 서버를 실제로 재부팅해 자동 복구를 실측했습니다. 결과는 통과였는데, 첫 판정에서 두 항목이 어긋나게 나왔고 둘 다 고장이 아니었습니다. 그대로 고장으로 읽었다면 멀쩡한 시스템을 파헤쳤을 것입니다.

재부팅 직후의 응답 확인은 판정에 쓰지 않습니다. 무거운 라이브러리를 올리는 한 앱은 운영체제가 「시작됨」이라고 한 뒤 3분 50초가 지나서야 응답하기 시작했습니다. 서비스가 「켜져 있음」이라는 것은 프로세스가 떴다는 뜻이지 응답한다는 뜻이 아닙니다. 4~5분 기다린 뒤 실제 주소로 다시 잽니다.

서비스 「개수」가 아니라 이름 목록을 기준선으로 남깁니다. 재부팅 전 15개였던 사용자 서비스가 재부팅 뒤 13개로 보였습니다. 처음에는 5분마다 도는 짧은 작업이 기준선을 잴 때 섞였을 것이라고 짐작했는데, 실제로 재 보니 그럴 수 없는 시간이었습니다. 이전 부팅 기록을 세어 보니 빠진 둘은 누군가 접속해 있을 때만 켜지는 서비스였고, 기준선을 잴 때 우리가 접속해 있었기 때문에 더 잡혔던 것입니다. 숫자만 남기면 차이가 났을 때 원인을 짚지 못하고, 짐작으로 덮기 쉽습니다.

자동 로그인 기계에서는 「사람 손 0」을 접속자 목록으로 판정하지 않습니다. 자동 로그인이 켜진 기계는 아무도 건드리지 않아도 로그인 세션이 뜹니다. 증거는 부팅 시각에서 서비스 기동 시각까지의 차이와, 그 서비스가 로그인 단계에 기대지 않는다는 의존 관계입니다.

판정 스크립트는 본 게임인 재부팅 전에 한 번 돌려 봅니다. 위의 세 번째 착시도 재부팅 전 시운전에서 잡았습니다. 재부팅은 되돌릴 수 없고, 세션이 끊기면 고칠 기회도 없습니다. 기준선, 판정 스크립트, 되살리는 명령은 재부팅으로 끊기지 않는 자리에 미리 두고, 그 자리를 사람이 볼 수 있게 적어 둡니다.

4. 야간 작업을 걸기 전에 예약 세 곳을 다 본다

품앗이 자체 서버에 야간 자료 수집 작업을 걸면서, 사용자 예약 목록과 사용자 타이머만 보고 비어 있는 시각을 골랐습니다. 그 시각에는 시스템 타이머에 걸린 백업이 이미 돌고 있었습니다. 백업이 홈 폴더를 훑는 도중에 새 작업이 파일을 쓰면 반쯤 쓰인 파일이 백업됩니다. 시스템 타이머는 사용자 쪽 목록에 나오지 않습니다.

새 예약 작업을 걸기 전에 사용자 예약 목록, 시스템 타이머, 사용자 타이머 세 곳을 다 봅니다. 백업이 무거운 작업과 겹쳐 서버가 메모리 부족으로 죽은 일은 백업이 진짜 백업인지 5절에 있습니다.

5. 옮길 때 깨지는 것들

파이썬 가상환경은 파이썬 버전을 넘어 복사할 수 없습니다. 서버를 옮길 때 홈 폴더를 통째로 복사하면 가상환경도 딸려 갑니다. 그런데 컴파일된 확장 모듈은 파일 이름에 파이썬 버전 표시가 박혀 있어서, 새 기계의 파이썬 버전이 다르면 조용히 불러오기에 실패합니다. 옮길 때는 가상환경을 빼고, 원래 기계에서 패키지 목록을 떠 둔 다음, 새 기계에서 다시 만듭니다. 노드 패키지 폴더도 네이티브 모듈이 들어 있으면 같은 이유로 새로 설치합니다.

서버 사양을 올리거나 재시작한 뒤에는 전부 점검합니다. 클라우드 서버 사양을 올린 뒤 주소만 바꾸고 끝냈다가, 실행 환경이 사라지고 메신저 봇이 멈춘 것을 뒤늦게 알았습니다. 서비스 상태, 떠 있는 프로세스, 예약 작업, 실행 환경 버전, 봇 응답까지 봅니다.

데이터베이스 파일이 든 폴더를 복사했으면 옆 파일까지 확인합니다. 한 교육원의 임대 서버를 철거하기 전에 앱 폴더를 복사했는데, 사본의 SQLite 데이터베이스 파일은 4KB짜리 껍데기였습니다. 실제 데이터는 그 옆의 쓰기 기록 파일에 있었고, 그 파일이 첫 복사에 따라오지 않았는데, 원인은 끝내 밝히지 못했습니다. 복사한 뒤에는 데이터베이스 옆 파일이 같이 왔는지, 무결성 검사가 통과하는지, 행 수가 원본과 같은지를 봅니다. 더 안전한 길은 원본에서 데이터베이스의 백업 기능으로 일관된 사본을 뜨는 것입니다.

6. 기계가 늘면 대장과 화면 한 장

기계 정보는 대장 한 곳에 모읍니다. 품앗이의 기계가 여러 대로 늘고 이사가 잦아지자 「어느 기계 이야기인가」가 늘 헷갈리는 자리가 됐습니다. 기계 정보를 여기저기 메모에 흩어 적으면 서로 어긋납니다. 기계를 새로 들이거나, 역할을 바꾸거나, 운영체제를 다시 깔면 그 자리에서 대장을 고칩니다.

감시 스크립트가 각자 알림만 쏘면, 조용할 때 무슨 뜻인지 알 수 없습니다. 괜찮아서 조용한 것인지, 감시기까지 죽어서 조용한 것인지 구분이 안 됩니다. 품앗이는 여러 조직의 서버와 백업 상태를 한 번에 당겨 보는 화면 한 장을 만들었고, 그 화면에는 원칙을 박았습니다.

  • 재지 못한 것을 0이나 정상으로 적지 않습니다. 「측정 안 됨」을 따로 두고 화면에도 빗금으로 그립니다. 디스크를 못 읽었는데 0%로 적으면 녹색으로 떠서, 이런 화면이 낼 수 있는 가장 위험한 거짓말이 됩니다.
  • 일부만 성공한 것은 정상이 아닙니다.
  • 색만으로 뜻을 전하지 않고 글자 표시를 같이 씁니다.
  • 남의 조직 공개 주소는 한 번만, 간격을 두고, 하나씩 확인합니다.
  • 알림은 이미 다른 감시기들이 쏘고 있으니 이 화면은 새 알림을 만들지 않습니다.
  • 읽기만 하고, 남의 기계에 설치나 설정 변경을 하지 않습니다.

측정 실패와 「값이 0」을 가르는 이야기는 측정의 함정 2절에, 남의 서버를 두드리는 상한은 남의 서버를 두드릴 때에, 알림이 소음이 되지 않게 하는 법은 알림과 타이머가 소음이 되지 않게에 있습니다.

7. 무엇을 직접 세울지는 비어 있는 자리만 본다

품앗이가 클라우드 드라이브 유료 요금제를 끊으면서 쓰임새를 셋으로 갈랐습니다. 신청서·보고서 같은 문서 배포와 PDF·이미지 첨부는 다른 길로 그대로 되고 있었고, 영상·자료집·데이터 같은 큰 파일만 빈자리였습니다. 그래서 그 자리만 자체 클라우드 저장소로 채웠습니다. 기존 파일은 옮기지 않고, 앞으로 주고받을 것만 담기로 했습니다. 드라이브는 267GiB에서 1.7GiB로 줄었습니다.

그룹웨어를 대신할 자체 메일을 세울 때 기존 서비스를 1차로 두고 병행하는 순서와, 넘어오는 날짜를 그 조직이 정한다는 원칙은 로컬서버에 메일 세우기 9절에 있습니다. 상시 켜진 서버가 있으면 시간 제한 없는 화상회의의 추가 비용이 0원이라는 이야기는 로컬서버에 화상회의 세우기 1절에 있습니다.

일곱 줄로 줄이면

  1. 늘 떠 있어야 하는 프로그램은 서비스로 올리고, 조용한 대체 동작은 출처별로 따로 잰다.
  2. 코드를 고쳤으면 재시작까지 하고, 파일 수정 시각과 프로세스 시작 시각을 비교한다.
  3. 늘 재시작하게 할 때는 간격과 제한 창을 같이 정하고 로그에 상한을 건다.
  4. 재부팅 뒤 판정은 4~5분 기다렸다가, 개수가 아니라 이름 목록으로 한다.
  5. 새 예약 작업 전에 사용자 예약, 시스템 타이머, 사용자 타이머 세 곳을 본다.
  6. 옮길 때는 가상환경을 다시 만들고, 데이터베이스는 옆 파일과 행 수까지 맞춘다.
  7. 재지 못한 것은 「측정 안 됨」으로 두고, 녹색으로 칠하지 않는다.

도구 메모

  • 예약 세 곳: crontab -l · systemctl list-timers --all · systemctl --user list-timers --all
  • 옛 코드 판별: ls -l --time-style=full-iso <파일> 과 ps -o lstart= -p <PID> 를 비교한다.
  • 직전 부팅의 재시작 실패 순위: journalctl -b -1 | grep -oE '[a-z0-9@._-]+\.service: Failed with result' | sort | uniq -c | sort -rn | head
  • SQLite 일관 사본: sqlite3 원본.db ".backup 사본.db" 뒤 PRAGMA integrity_check;

관련 노드


작성: 품앗이 · 2026-10-22 · 품앗이 자체 서버와 교육원·연합회 현장 서버의 운영 기록 기준