[Linux ] systemd 서비스 관리

2026. 5. 22. 22:21·Linux
SMALL
시나리오

서버에 올린 앱이 재부팅 후 자동으로 다시 뜨지 않아서 매번 수동으로 켜야 했습니다. nohup으로 실행한 프로세스는 서버가 재시작되면 사라지고, 크래시가 나도 아무도 모릅니다. 이런 상황에서 새벽에 서버가 재부팅됐다면? 아침에 출근해서야 서비스가 죽어있음을 발견하게 됩니다.

systemd 서비스로 등록하면 부팅 시 자동 시작, 크래시 시 자동 재시작, 로그 통합 관리가 모두 해결됩니다. 운영 서버에 앱을 배포할 때 반드시 거치는 과정입니다.

systemd 서비스 관리

1. Unit 파일 구조와 서비스 타입

이번 챕터에서 배울 것
1systemd Unit 파일 구조([Unit]/[Service]/[Install])를 이해하고 서비스를 직접 작성할 수 있다
2systemctl로 서비스를 시작·중지·활성화하고 상태를 확인할 수 있다
3journalctl로 서비스 로그를 조회하고 필터링할 수 있다
4Override 파일로 패키지 기본 unit 설정을 덮어쓰고 Timer Unit으로 cron을 대체할 수 있다
5크래시 자동 재시작 정책과 보안 샌드박스 옵션을 설정할 수 있다
실습 환경 준비
systemd 버전 확인
systemctl --version
실패한 서비스 현황 파악
systemctl --failed
journald 디스크 사용량 확인
journalctl --disk-usage
실습용 unit 파일 디렉토리 확인
ls /etc/systemd/system/
개념
systemd Unit 파일 구조

앱을 systemd 서비스로 등록하려면 어디에 무엇을 써야 하는지 알아야 합니다. 설정 파일 하나의 위치와 내용이 "부팅 시 자동 시작되는 서비스"와 "수동으로 켜야 하는 프로세스"의 차이를 만듭니다. Unit 파일은 크게 세 섹션([Unit], [Service], [Install])으로 구성되고, 각 섹션이 서비스의 다른 측면을 정의합니다. 이 구조를 한 번 이해하면 nginx, postgresql 같은 패키지가 설치한 unit 파일을 읽고 의도를 파악할 수 있고, 직접 서비스를 작성할 때도 막히지 않습니다.

서비스는 Unit 파일로 정의됩니다. 위치: /etc/systemd/system/ (운영자 정의) 또는 /lib/systemd/system/ (패키지 기본값)

systemd Unit 파일 구조와 서비스 생명주기 — [Unit]/[Service]/[Install] 섹션, systemctl 명령
ini[Unit]
Description=My App Service
After=network.target postgresql.service
Requires=postgresql.service

[Service]
Type=simple
User=myapp
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/bin/server --port 8080
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
RestartSec=5
Environment=NODE_ENV=production
EnvironmentFile=/etc/myapp/env

[Install]
WantedBy=multi-user.target

핵심 디렉티브:

  • After: 이 서비스가 시작되기 전에 시작되어야 할 유닛 (순서만, 의존성 아님)
  • Requires: 강한 의존성 — 해당 유닛이 실패하면 이 서비스도 중단
  • Wants: 약한 의존성 — 해당 유닛이 실패해도 이 서비스는 계속 시작
  • Restart=always: 어떤 이유로든 종료 시 자동 재시작
  • EnvironmentFile: 환경 변수를 파일에서 로드 (시크릿 관리에 유용)

서비스 타입 선택 기준:

Type=은 systemd가 서비스의 "준비 완료" 신호를 어떻게 판단하는지를 지정합니다. 대부분의 현대 앱은 simple이 맞고, 포크 방식 데몬이나 D-Bus 서비스는 별도 타입이 필요합니다.

Type 동작 사용 예
simple ExecStart가 메인 프로세스 대부분의 현대 앱
forking ExecStart가 fork 후 종료 전통적인 데몬
notify 준비 완료를 sd_notify()로 알림 systemd-aware 앱
oneshot 단발성 실행, 종료 후 active 유지 초기화 스크립트
dbus D-Bus 버스 이름 획득 시 준비 완료 D-Bus 서비스

— — —

2. 기본 제어 명령

실습 전 디렉토리와 예제 파일을 먼저 준비합니다.

bash# 실습 디렉토리 준비
mkdir -p /tmp/linux/part2/exam_4 && cd /tmp/linux/part2/exam_4

# 실습용 애플리케이션 스크립트 생성
cat > /tmp/linux/part2/exam_4/myapp.sh << 'EOF'
#!/bin/bash
echo "MyApp PID: $$"
while true; do
  echo "$(date): running" >> /tmp/linux/part2/exam_4/app.log
  sleep 5
done
EOF
chmod +x /tmp/linux/part2/exam_4/myapp.sh

이제 실습을 진행합니다.

Step 1. 서비스 상태 확인 및 기본 제어
bash# 서비스 상태 확인 (가장 자주 쓰는 명령)
systemctl status nginx

# 시작 / 중지 / 재시작 / 재로드
systemctl start nginx
systemctl stop nginx
systemctl restart nginx
systemctl reload nginx   # 설정만 재로드 (무중단)

# 부팅 시 자동 시작 설정
systemctl enable nginx
systemctl disable nginx

# 활성화 + 즉시 시작 (가장 편한 방법)
systemctl enable --now nginx

# 서비스 완전히 멈추고 재시작 금지 (마스킹)
systemctl mask nginx
systemctl unmask nginx
bashsystemctl status nginx
Step 2. 유닛 목록 탐색과 의존성 파악
bash# 실행 중인 모든 서비스 목록
systemctl list-units --type=service

# 모든 유닛 (실패 포함)
systemctl list-units --type=service --all

# 실패한 서비스만
systemctl --failed

# 부팅 시 활성화된 서비스 목록
systemctl list-unit-files --type=service --state=enabled

# 특정 서비스의 의존성 트리 확인
systemctl list-dependencies nginx

# 역방향: 이 서비스에 의존하는 유닛
systemctl list-dependencies --reverse nginx

# unit 파일 내용 바로 확인 (편집기 없이)
systemctl cat nginx

출력 예시:

UNIT                        LOAD   ACTIVE SUB     DESCRIPTION
nginx.service               loaded active running A high performance web server
postgresql.service          loaded active running PostgreSQL RDBMS
myapp.service               loaded failed failed  My Application Service

LOAD 컬럼이 not-found이면 unit 파일이 없는 것입니다. ACTIVE가 failed이면 journalctl -u 서비스명 -n 50으로 원인을 확인하세요.

bashsystemctl list-units --type=service

— — —

3. 로그 조회 (journalctl)

Step 3. 서비스 로그 확인
bash# 특정 서비스 전체 로그
journalctl -u nginx

# 실시간 로그 스트리밍 (tail -f와 동일)
journalctl -u nginx -f

# 최근 50줄
journalctl -u nginx -n 50

# 오늘 로그만
journalctl -u nginx --since today

# 특정 시간대
journalctl -u nginx --since "2026-03-25 10:00" --until "2026-03-25 12:00"

# 에러 레벨 이상만 (0=emerg ~ 7=debug)
journalctl -u nginx -p err

# JSON 출력 (로그 파이프라인에 유용)
journalctl -u nginx -o json-pretty | head -30

# 부팅 이후 로그만
journalctl -u nginx -b

# 이전 부팅 로그 (-1 = 직전 부팅)
journalctl -u nginx -b -1
bashjournalctl -u nginx -f

— — —

4. 나만의 서비스 Unit 파일 작성

Step 4. 서비스 unit 파일 작성 및 등록
bash# 1. Unit 파일 작성
sudo vim /etc/systemd/system/myapp.service
ini[Unit]
Description=My Application Server
Documentation=https://github.com/myorg/myapp
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/bin/server --port 8080
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=60
StartLimitBurst=3

# 환경 변수
Environment=NODE_ENV=production
EnvironmentFile=-/etc/myapp/env   # 앞의 -는 파일 없어도 오류 안 냄

# 표준 출력/에러를 journald로
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp

[Install]
WantedBy=multi-user.target
bash# 2. systemd에 변경사항 반영 (파일 수정 후 항상 실행)
sudo systemctl daemon-reload

# 3. 서비스 시작 및 활성화
sudo systemctl enable --now myapp

# 4. 동작 확인
systemctl status myapp
journalctl -u myapp -f

StartLimitBurst=3 + StartLimitIntervalSec=60: 60초 안에 3번 재시작 실패하면 더 이상 재시작하지 않습니다. 무한 재시작 루프를 방지하는 중요한 설정입니다.

bashsudo systemctl daemon-reload

— — —

5. Drop-in Override 파일

개념
Override 파일로 패키지 기본값 덮어쓰기
systemd Drop-in Override — /lib 기본값 + /etc override.conf 병합 구조

패키지 관리자가 설치한 unit 파일(/lib/systemd/system/)을 직접 수정하면 패키지 업데이트 시 덮어써집니다. Drop-in override를 사용하면 원본을 건드리지 않고 특정 설정만 재정의할 수 있습니다.

bash# 권장 방법: systemctl edit (자동으로 경로 생성)
sudo systemctl edit nginx

# 직접 생성하는 방법
sudo mkdir -p /etc/systemd/system/nginx.service.d/
sudo vim /etc/systemd/system/nginx.service.d/override.conf

override 파일 예시 — nginx에 메모리 제한과 재시작 정책 추가:

ini[Service]
# 기존 값을 초기화하려면 빈 값으로 먼저 선언 후 재정의
Restart=
Restart=always
RestartSec=3

# 리소스 제한 추가
MemoryMax=512M
CPUQuota=50%
bash# 변경 적용
sudo systemctl daemon-reload
sudo systemctl restart nginx

# override 파일 확인 (원본 + override 합쳐진 결과)
systemctl cat nginx

systemctl cat은 적용된 모든 설정을 파일 출처와 함께 보여줍니다. 설정이 어디서 왔는지 추적할 때 필수입니다.

— — —

6. 리소스 제한과 서비스 하드닝

개념
프로덕션 서비스 보안 설정
systemd 보안 옵션 — PrivateTmp·NoNewPrivileges·ProtectSystem·ProtectHome·ReadWritePaths 효과 비교

컨테이너 없이 일반 systemd 서비스로 운영하던 API 서버가 취약점을 통해 침해되면, 프로세스는 루트 권한으로 파일 시스템 전체를 읽고 쓸 수 있는 상태가 됩니다. /etc/passwd, SSH 키, 데이터베이스 설정 파일까지 노출됩니다. systemd unit 파일에 보안 설정 몇 줄만 추가했더라면, 서비스가 접근 가능한 경로와 권한이 처음부터 제한되어 피해 범위를 크게 줄일 수 있었습니다. Docker나 Kubernetes 없이도 커널의 네임스페이스와 capability 제한 기능을 systemd가 직접 활성화해줍니다.

systemd는 Linux 커널의 보안 기능을 unit 파일에서 직접 활성화할 수 있습니다. 컨테이너 없이도 서비스 격리가 가능합니다.

실제 프로덕션 unit 파일 — 웹 API 서버:

ini[Unit]
Description=Production API Server
After=network-online.target postgresql.service
Requires=postgresql.service

[Service]
Type=notify
User=apiserver
Group=apiserver
WorkingDirectory=/opt/apiserver

ExecStart=/opt/apiserver/bin/api-server
ExecReload=/bin/kill -USR1 $MAINPID
Restart=on-failure
RestartSec=5
TimeoutStartSec=30
TimeoutStopSec=30

# 환경 변수 (시크릿은 별도 파일로)
EnvironmentFile=/etc/apiserver/secrets.env

# ── 리소스 제한 ──────────────────────────────
# CPU: 최대 2코어 분량
CPUQuota=200%
# 메모리: 소프트 한도 512M, 하드 한도 768M
MemoryHigh=512M
MemoryMax=768M
# 오픈 파일 디스크립터 한도
LimitNOFILE=65536
# 프로세스 수 제한
LimitNPROC=512
# 파일 크기 제한 (코어 덤프 방지)
LimitCORE=0

# ── 보안 하드닝 ──────────────────────────────
# setuid/setgid 비트 실행 금지
NoNewPrivileges=yes
# /usr, /boot, /etc를 읽기 전용으로
ProtectSystem=strict
# /home, /root 접근 차단
ProtectHome=yes
# 서비스 전용 /tmp (다른 서비스와 격리)
PrivateTmp=yes
# /dev 접근 최소화
PrivateDevices=yes
# 커널 튜닝 파라미터 변경 금지
ProtectKernelTunables=yes
# 커널 모듈 로드 금지
ProtectKernelModules=yes
# 시스템 콜 필터 (화이트리스트 방식)
SystemCallArchitectures=native
# 쓰기 가능한 경로 명시적 허용
ReadWritePaths=/var/lib/apiserver /var/log/apiserver

[Install]
WantedBy=multi-user.target

각 하드닝 디렉티브의 효과:

각 설정이 어떤 공격 경로를 차단하는지 알면, 서비스 특성에 맞게 취사선택할 수 있습니다.

디렉티브 차단하는 공격
NoNewPrivileges=yes SUID 바이너리를 통한 권한 상승
ProtectSystem=strict 시스템 파일 변조
PrivateTmp=yes /tmp를 통한 다른 서비스 공격
ProtectHome=yes 홈 디렉토리 내 시크릿 파일 접근
PrivateDevices=yes 원시 디바이스 파일 접근
ProtectKernelTunables=yes /proc/sys 변조
Step 5. 보안 설정 적용 및 검증
bash# 서비스 보안 점수 분석 (0에 가까울수록 격리 강함)
systemd-analyze security myapp

# 출력 예시:
# NAME                                                        DESCRIPTION                              EXPOSURE
# ✗ RootDirectory=/RootImage=                                 Service runs within the host's root directory               0.1
# ✓ NoNewPrivileges=                                          Service processes cannot acquire new privileges              0.0
# ✓ PrivateTmp=                                              Service has access to private /tmp                           0.0
# ...
# → Overall exposure level for myapp.service: 4.2 MEDIUM

# unit 파일 문법 검증 (배포 전 항상 실행)
systemd-analyze verify /etc/systemd/system/myapp.service

# 서비스에 실제 적용된 리소스 제한 확인
systemctl show myapp | grep -E 'Memory|CPU|Limit'
bashsystemd-analyze security myapp

— — —

7. systemd Timer (cron 대체)

개념
Timer Unit으로 cron 대체하기
systemd Timer vs cron — 로그·의존성·보완 실행·가독성 비교

cron으로 등록한 배치 작업이 실패했는데 로그를 찾을 수 없어서, "실행이 됐는지조차 모르는" 상황이 있습니다. cron은 출력을 메일로 보내거나 직접 파일에 리다이렉트하지 않으면 로그가 없습니다. systemd Timer는 일반 서비스처럼 journalctl로 실행 로그를 볼 수 있고, 놓친 실행 보완(Persistent=yes), 의존성 지정, 다음 실행 시간 조회(systemctl list-timers) 등 cron에 없는 기능을 제공합니다. 기존 cron 작업을 Timer로 전환하면 운영 가시성이 크게 높아집니다.

cron 대비 systemd Timer의 장점:

  • journalctl로 실행 로그 통합 관리
  • 의존성 지정 가능 (네트워크 연결 후 실행 등)
  • 놓친 실행 보완 (Persistent=yes)
  • systemctl list-timers로 다음 실행 시간 한눈에 확인

Timer unit은 항상 동명의 .service unit과 쌍으로 동작합니다.

OnCalendar 문법:

OnCalendar=는 cron 표현식과 유사하지만 더 읽기 쉬운 형식을 씁니다. 단축어(daily, weekly)로도 쓸 수 있고, 날짜/시각을 정밀하게 지정할 수도 있습니다.

*-*-* HH:MM:SS        매일 특정 시각
Mon-Fri *-*-* 09:00   평일 오전 9시
*-*-1 00:00            매월 1일 자정
weekly                 매주 월요일 자정 (단축어)
daily                  매일 자정 (단축어)
hourly                 매시간 정각 (단축어)

모노토닉 타이머 (상대 시간):

OnCalendar 대신 모노토닉 타이머를 쓰면 절대 시각 대신 "부팅 후" 또는 "마지막 실행 후" 기준으로 간격을 지정할 수 있습니다.

iniOnBootSec=15min        # 부팅 후 15분
OnUnitActiveSec=1h     # 마지막 실행 후 1시간
OnActiveSec=10min      # 타이머 활성화 후 10분
Step 6. 백업 타이머 유닛 작성 — 완전한 예제

백업 작업을 매일 새벽 2시에 실행하는 타이머 예제입니다.

1단계: 서비스 유닛 작성

bashsudo vim /etc/systemd/system/db-backup.service
ini[Unit]
Description=PostgreSQL Daily Backup
After=postgresql.service
Requires=postgresql.service
# Timer에서 실행될 때는 [Install] 섹션 불필요

[Service]
Type=oneshot
User=backup
Group=backup

# 백업 스크립트 실행
ExecStart=/usr/local/bin/db-backup.sh

# 보안 하드닝
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ReadWritePaths=/var/backups/postgresql

# 실패 시 알림 (선택)
OnFailure=notify-admin@.service

2단계: 타이머 유닛 작성

bashsudo vim /etc/systemd/system/db-backup.timer
ini[Unit]
Description=Daily PostgreSQL Backup Timer
Requires=db-backup.service

[Timer]
# 매일 새벽 2시 실행
OnCalendar=*-*-* 02:00:00

# 서버가 꺼진 동안 놓친 실행을 부팅 후 즉시 보완
Persistent=yes

# 정확히 2시가 아닌 ±30분 내 랜덤 지연 (서버 부하 분산)
RandomizedDelaySec=30min

# 함께 시작할 서비스 명시
Unit=db-backup.service

[Install]
WantedBy=timers.target

3단계: 활성화 및 확인

bash# 타이머 활성화 (.service가 아닌 .timer를 enable)
sudo systemctl daemon-reload
sudo systemctl enable --now db-backup.timer

# 모든 타이머 목록 및 다음 실행 시간 확인
systemctl list-timers

# 출력 예시:
# NEXT                         LEFT       LAST                         PASSED  UNIT
# Thu 2026-03-27 02:00:00 KST  9h left    Wed 2026-03-26 02:00:12 KST  13h ago db-backup.timer

# 타이머 대기하지 않고 즉시 테스트 실행
sudo systemctl start db-backup.service

# 실행 로그 확인
journalctl -u db-backup.service -n 30

Persistent=yes 없이는 서버가 새벽 2시에 꺼져 있었다면 그날 백업이 영구 누락됩니다. 백업 타이머에서는 항상 설정하세요.

bashsystemctl list-timers

— — —

8. 부팅 시간 분석 (systemd-analyze)

Step 7. 부팅 성능 분석 및 병목 찾기
bash# 총 부팅 시간 요약
systemd-analyze

# 출력 예시:
# Startup finished in 1.842s (kernel) + 3.291s (initrd) + 8.103s (userspace) = 13.237s
# graphical.target reached after 7.812s in userspace

# 각 서비스별 시작 소요 시간 (느린 순)
systemd-analyze blame

# 출력 예시:
#   3.201s postgresql.service
#   1.893s cloud-init.service
#   1.241s NetworkManager-wait-online.service
#    892ms snapd.service
#    ...

# 직렬 의존성 체인 (실제 병목 구간)
systemd-analyze critical-chain

# 특정 서비스까지의 체인
systemd-analyze critical-chain nginx.service

# SVG 타임라인 차트 생성 (브라우저로 열기)
systemd-analyze plot > /tmp/boot-timeline.svg

# unit 파일 검증 (문법 오류 탐지)
systemd-analyze verify /etc/systemd/system/myapp.service

# 보안 점수 (앞서 소개)
systemd-analyze security myapp.service

critical-chain은 총 부팅 시간에 직접 영향을 주는 의존성 체인만 보여줍니다. blame에서 느린 서비스를 발견해도 critical-chain에 없으면 병렬 실행 중이라 실제 병목이 아닐 수 있습니다.

bashsystemd-analyze blame

— — —

9. 소켓 활성화 (Socket Activation)

개념
소켓 활성화 — 연결이 올 때만 서비스 시작
소켓 활성화 — 전통적 상시 실행 vs 연결 시 지연 시작 비교

평소에는 거의 사용되지 않는 백오피스 API나 내부 관리 툴을 항상 메모리에 올려둘 필요가 없을 때가 있습니다. 소켓 활성화는 서비스를 항상 켜두는 대신, 첫 번째 연결이 들어올 때 systemd가 자동으로 서비스를 시작하는 방식입니다. 부팅 시간을 단축할 수 있고, 서비스 재시작 중에도 소켓이 연결을 큐잉해서 요청을 잃지 않는 장점도 있습니다. Docker Desktop, DBus, SSH를 포함한 많은 시스템 서비스가 이 방식으로 동작합니다.

소켓 활성화는 서비스를 항상 실행하는 대신, 소켓에 첫 연결이 들어올 때 systemd가 서비스를 자동으로 깨우는 방식입니다.

클라이언트 요청 → 소켓 (.socket 유닛이 리스닝) → 서비스 자동 시작 → 처리

장점:

  • 메모리 절약 (유휴 상태에서 서비스 미실행)
  • 부팅 시간 단축 (필요할 때 지연 시작)
  • 서비스 재시작 중에도 연결 큐잉 (무중단)
ini# myapp.socket
[Unit]
Description=My App Socket

[Socket]
ListenStream=8080
Accept=no

[Install]
WantedBy=sockets.target
ini# myapp.service — [Install] 섹션 없음 (소켓이 활성화하므로)
[Unit]
Description=My App Service

[Service]
ExecStart=/opt/myapp/bin/server
StandardInput=socket
bash# 소켓만 활성화 (서비스는 연결 시 자동 시작)
sudo systemctl enable --now myapp.socket

SSH, DBus, 많은 시스템 서비스가 이 방식으로 동작합니다.

— — —

10. journald 로그 보존 설정

개념
journald 보존 정책 및 크기 제한
journald 저장소 — /run/log/journal(휘발성) vs /var/log/journal(영구), SystemMaxUse·RuntimeMaxUse 설정

디스크 사용률 경보가 울려 확인해보면 /var/log/journal이 수십 GB를 차지하고 있는 경우가 있습니다. 설정을 하지 않은 journald는 가용 디스크의 10%까지 로그를 계속 쌓고, 어느 날 갑자기 서비스가 로그를 쓰지 못해 오동작하거나 디스크 풀로 SSH 접속조차 안 되는 상황이 생깁니다. 로그 보존 정책은 서비스 초기 세팅 시 빠트리기 쉬운 항목이지만, 미리 한도를 잡아두면 이런 상황을 완전히 막을 수 있습니다.

기본 설정으로는 디스크가 가득 찰 때까지 로그가 쌓입니다. 프로덕션 서버에서는 명시적으로 한도를 설정해야 합니다.

bashsudo vim /etc/systemd/journald.conf
ini[Journal]
# 저장 방식: persistent(디스크), volatile(메모리), auto(기본)
Storage=persistent

# 최대 디스크 사용량 (전체 로그)
SystemMaxUse=2G

# 파일 하나의 최대 크기
SystemMaxFileSize=200M

# 보존 기간 (오래된 로그 자동 삭제)
MaxRetentionSec=30day

# 압축 여부
Compress=yes

# Rate limiting: 30초에 서비스당 최대 10000 메시지
RateLimitIntervalSec=30s
RateLimitBurst=10000
bash# 설정 적용
sudo systemctl restart systemd-journald

# 현재 로그 디스크 사용량 확인
journalctl --disk-usage

# 수동으로 오래된 로그 삭제
sudo journalctl --vacuum-size=1G    # 1G 이하로 줄이기
sudo journalctl --vacuum-time=14d   # 14일 이상 된 로그 삭제

# 특정 부팅 ID 목록
journalctl --list-boots

로그 수집 스택(ELK, Loki 등)을 운영한다면 MaxRetentionSec을 짧게 유지해도 됩니다. 로컬 로그는 장애 초기 대응용으로만 사용하기 때문입니다.

— — —

11. 실패 디버깅

트러블슈팅
서비스가 active(running)인데 실제로 죽어있음

원인: Type=simple로 설정된 서비스에서 fork 후 부모 프로세스가 바로 종료되는 경우. systemd는 원래 PID를 추적하므로 자식 프로세스가 죽어도 active 상태를 유지합니다.

확인:

bashsystemctl status myapp
# 상태는 active(running)이지만 실제 동작 없음

# 실제 프로세스 확인
ps aux | grep myapp

# PID 추적
systemctl show myapp --property=MainPID

해결:

ini[Service]
# fork하는 데몬의 경우
Type=forking
PIDFile=/var/run/myapp.pid

# 또는 앱이 sd_notify를 지원하면
Type=notify

# 헬스체크로 실제 상태 보장
ExecStartPost=/usr/bin/curl -sf http://localhost:8080/health
트러블슈팅
ExecStart 경로가 맞는데 서비스가 시작 안 됨

흔한 원인 3가지:

  1. 환경 변수 없음: systemd 서비스는 /etc/environment와 ~/.bashrc를 읽지 않음
  2. 현재 디렉토리 없음: WorkingDirectory 지정 필요
  3. 권한 문제: User= 지정한 사용자가 실행 파일이나 로그 경로에 접근 불가

디버깅 순서:

bash# 1. 상세 에러 확인
journalctl -u myapp.service -n 50 --no-pager

# 2. 해당 사용자로 직접 실행 테스트
sudo -u myapp /opt/myapp/bin/server

# 3. 적용된 환경 변수 확인
systemctl show myapp | grep -E 'Exec|Environment|WorkingDir'

# 4. unit 파일 문법 검증
systemd-analyze verify /etc/systemd/system/myapp.service

# 5. SELinux/AppArmor 거부 여부
ausearch -m avc -ts recent | grep myapp
# 또는
journalctl -k | grep apparmor
트러블슈팅
타이머가 등록됐는데 실행이 안 됨

증상: systemctl list-timers에 나타나지만 LAST 실행이 없거나, 예상 시각에 실행되지 않음.

확인 체크리스트:

bash# 1. 타이머 유닛 상태 확인 (.service가 아닌 .timer!)
systemctl status db-backup.timer

# 2. 연결된 서비스 유닛 상태
systemctl status db-backup.service

# 3. OnCalendar 문법 검증
systemd-analyze calendar "*-*-* 02:00:00"
# 출력:
#   Original form: *-*-* 02:00:00
# Normalized form: *-*-* 02:00:00
#     Next elapse: Thu 2026-03-27 02:00:00 KST
#        (in UTC): Wed 2026-03-26 17:00:00 UTC

# 4. 서비스 직접 실행으로 스크립트 오류 확인
sudo systemctl start db-backup.service
journalctl -u db-backup.service -n 20

# 5. Persistent=yes 설정 여부 확인 (놓친 실행 보완)
systemctl cat db-backup.timer | grep Persistent

systemd-analyze calendar 명령으로 OnCalendar 문자열을 검증하면 잘못된 문법을 배포 전에 잡을 수 있습니다. 타임존도 함께 확인하세요.

트러블슈팅
MemoryMax 설정 후 서비스가 OOM으로 죽음

증상: journalctl -u myapp에서 Memory limit exceeded 또는 Killed 메시지.

bash# 현재 메모리 사용량 확인
systemctl show myapp | grep -i memory

# cgroup 레벨에서 실제 사용량
cat /sys/fs/cgroup/system.slice/myapp.service/memory.current

# OOM 이벤트 커널 로그
journalctl -k | grep -i oom | tail -20

# 실시간 모니터링
systemd-cgtop

해결 접근:

ini[Service]
# 1. 한도 자체를 올림
MemoryMax=1G

# 2. 소프트/하드 구분하여 여유 확보
MemoryHigh=768M    # 이 값 초과 시 메모리 회수 압력
MemoryMax=1G       # 이 값 초과 시 OOM 킬

# 3. 스왑 사용 허용
MemorySwapMax=256M

— — —

12. 실무 맥락

실무 맥락
서비스 무중단 배포 설계

실무에서 새 버전을 배포할 때 다운타임 없이 서비스를 교체하는 패턴입니다.

bash# 1. 새 바이너리 배치
cp /tmp/myapp-v2 /opt/myapp/bin/server.new
chmod +x /opt/myapp/bin/server.new
chown myapp:myapp /opt/myapp/bin/server.new

# 2. unit 파일 변경이 있으면 반드시 reload
sudo systemctl daemon-reload

# 3. 설정에 ExecReload가 있으면 reload (SIGHUP으로 graceful reload)
systemctl reload myapp 2>/dev/null || systemctl restart myapp

# 4. 배포 후 헬스체크 (실패 시 롤백)
for i in 1 2 3 4 5; do
    sleep 2
    if curl -sf http://localhost:8080/health; then
        echo "배포 성공"
        exit 0
    fi
done

# 5. 헬스체크 실패 시 롤백
echo "헬스체크 실패, 롤백 시작"
cp /opt/myapp/bin/server.bak /opt/myapp/bin/server
systemctl restart myapp
exit 1

핵심: Restart=on-failure + RestartSec=3 조합으로 크래시 자동 복구. StartLimitBurst=3으로 무한 재시작 루프 방지. Drop-in override로 환경별 설정 분리 (개발/스테이징/프로덕션).

실무 맥락
레거시 cron 작업을 systemd Timer로 마이그레이션

팀에서 crontab으로 관리하던 작업들이 분산되어 추적이 어려운 상황에서 systemd Timer로 일원화하는 과정입니다.

bash# 현재 cron 목록 파악
crontab -l
sudo crontab -l
cat /etc/cron.d/*

# 예시 cron 항목:
# 0 2 * * * /usr/local/bin/db-backup.sh >> /var/log/backup.log 2>&1

# 마이그레이션 후 검증
systemctl list-timers --all

# cron과 timer 동시 실행 방지: cron 항목 주석 처리 후
# timer가 안정적으로 실행됨을 1주일 확인 후 cron 삭제

마이그레이션 이점:

  • 로그가 journalctl -u db-backup으로 중앙 집중
  • 실행 실패 시 OnFailure= 디렉티브로 알림 자동화
  • systemctl list-timers로 다음 실행 시간 즉시 확인
  • 서비스에 리소스 제한과 보안 하드닝 적용 가능

주의: Persistent=yes를 설정하지 않으면 서버가 실행 시각에 꺼져 있던 경우 그 실행은 영구 누락됩니다. 백업, 정산 같은 중요 작업에는 반드시 설정하세요.

— — —

다음 모듈에서는 텍스트 처리(Text Processing)를 다룹니다 — grep, awk, sed, sort, uniq로 로그를 분석하고 파이프라인을 구성하는 방법을 배웁니다.

반응형
LIST

'Linux' 카테고리의 다른 글

[Linux] Bash 스크립팅 기초  (0) 2026.05.22
[Linux] 텍스트 처리 (grep/awk/sed)  (0) 2026.05.22
[Linux ] 시그널 & 프로세스 종료  (0) 2026.05.22
[Linux] 프로세스 관리 (Process Management)  (0) 2026.05.22
[Linux] 환경변수 & dotfiles  (0) 2026.05.22
'Linux' 카테고리의 다른 글
  • [Linux] Bash 스크립팅 기초
  • [Linux] 텍스트 처리 (grep/awk/sed)
  • [Linux ] 시그널 & 프로세스 종료
  • [Linux] 프로세스 관리 (Process Management)
cumo
cumo
  • cumo
    이것저것
    cumo
    • 분류 전체보기 (147)
      • 이것저것 (1)
      • 보안뉴스 (15)
      • Project (12)
      • wargame (1)
      • Cloud (25)
      • DevOps (21)
      • Linux (43)
      • 네트워크 (23)
      • AWS Developer BootCamp (1)
      • WEB&WAS (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • 도구모음 사이트
    • 참고 기술 블로그
  • 공지사항

  • 인기 글

  • 태그

    인프라
    리눅스
    포트진단
    부팅서비스
    텍스트편집기
    눅스네트워킹
    서버편집
    디렉토리구조
    터미널멀티플렉서
    ubuntu
    vi
    nano
    데몬
    Linux
    논리볼륨
    Volume Group
    bash입문
    vim
    스토리지확장
    서버관리
  • 최근 댓글

  • 최근 글

  • 반응형
  • hELLO· Designed By정상우.v4.10.3
cumo
[Linux ] systemd 서비스 관리
상단으로

티스토리툴바