- Authors
- Name
- 들어가며
- 1. 공식 문서 출처
- 2. SysVinit vs systemd 비교
- 3. Unit 파일 기본 구조
- 4. Service Type 비교
- 5. 프로덕션 수준 Unit 파일 예시
- 6. 소켓 활성화 (Socket Activation)
- 7. systemd 타이머 (cron 대체)
- 8. cgroup 리소스 제어
- 9. journalctl 로그 관리
- 10. 의존성 관리
- 11. systemctl 핵심 명령어
- 12. 운영 주의사항과 실패 사례
- 13. 보안 강화 (Sandboxing)
- 14. 트러블슈팅 체크리스트
- 15. 드롭인 오버라이드 (Drop-in Override)
- 16. 실전 디버깅 시나리오
- 마무리
- 참고 자료
들어가며
현대 리눅스 배포판에서 서비스 관리의 핵심은 systemd다. PID 1으로 시스템 초기화를 담당하며, 서비스 시작/중지, 의존성 해결, 리소스 제한, 로깅까지 하나의 프레임워크로 통합한다.
SysVinit의 순차적 쉘 스크립트 기반 초기화와 달리, systemd는 병렬 시작, 소켓 활성화, on-demand 서비스 로딩, cgroup 기반 리소스 제어를 기본으로 제공한다. 하지만 이 강력한 기능들을 제대로 활용하려면 Unit 파일의 구조와 디렉티브를 정확히 이해해야 한다.
이 글에서는 systemd의 핵심 개념부터 프로덕션 수준의 Unit 파일 작성, 타이머, 소켓 활성화, 리소스 제한, 저널 로깅, 그리고 실제 운영에서 마주치는 트러블슈팅 사례까지 체계적으로 정리한다.
1. 공식 문서 출처
systemd를 깊이 이해하려면 공식 문서를 참조하는 것이 필수다. 주요 레퍼런스는 다음과 같다.
| 문서 | URL | 설명 |
|---|---|---|
| systemd 공식 매뉴얼 | https://www.freedesktop.org/software/systemd/man/ | 모든 디렉티브의 공식 레퍼런스 |
| systemd.service(5) | https://www.freedesktop.org/software/systemd/man/systemd.service.html | 서비스 유닛 파일 상세 명세 |
| systemd.unit(5) | https://www.freedesktop.org/software/systemd/man/systemd.unit.html | 유닛 파일 공통 섹션 및 디렉티브 |
| systemd.exec(5) | https://www.freedesktop.org/software/systemd/man/systemd.exec.html | 실행 환경 설정 (보안, 환경변수 등) |
| systemd.resource-control(5) | https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html | cgroup 기반 리소스 제한 디렉티브 |
| systemd.timer(5) | https://www.freedesktop.org/software/systemd/man/systemd.timer.html | 타이머 유닛 명세 |
| systemd-journald(8) | https://www.freedesktop.org/software/systemd/man/systemd-journald.html | 저널 로깅 데몬 설정 |
2. SysVinit vs systemd 비교
systemd로의 전환이 왜 필요했는지, 두 시스템의 핵심 차이를 비교한다.
| 항목 | SysVinit | systemd |
|---|---|---|
| 초기화 방식 | 순차적 (쉘 스크립트) | 병렬 (의존성 그래프 기반) |
| 서비스 정의 | /etc/init.d/ 스크립트 | INI 형식 Unit 파일 |
| 의존성 관리 | 숫자 접두사 순서 (S01, S99) | After=, Requires=, Wants= 선언적 |
| 프로세스 추적 | PID 파일 기반 (불안정) | cgroup 기반 (확실한 추적) |
| 리소스 제한 | 별도 도구 필요 (cgroups 직접 설정) | 유닛 파일에 통합 (MemoryMax= 등) |
| 로깅 | syslog 의존 | journald 내장 (구조화된 로그) |
| 소켓 활성화 | inetd/xinetd 별도 | 네이티브 소켓 활성화 |
| 타이머 | cron 별도 | systemd.timer 내장 |
| 부팅 속도 | 느림 (순차 실행) | 빠름 (병렬 + 지연 로딩) |
| 런레벨 | 0-6 숫자 | target 유닛 (multi-user.target 등) |
3. Unit 파일 기본 구조
3.1 유닛 파일 위치
systemd는 여러 경로에서 유닛 파일을 로드하며, 우선순위가 있다.
| 경로 | 우선순위 | 용도 |
|---|---|---|
/etc/systemd/system/ | 높음 | 관리자 커스텀 유닛 (오버라이드) |
/run/systemd/system/ | 중간 | 런타임 생성 유닛 |
/usr/lib/systemd/system/ | 낮음 | 패키지 설치 기본 유닛 |
핵심 원칙: 패키지가 제공하는 유닛 파일을 직접 수정하지 말고,
systemctl edit명령으로/etc/systemd/system/아래에 오버라이드 파일을 생성하라.
3.2 유닛 파일 섹션
모든 유닛 파일은 세 가지 주요 섹션으로 구성된다.
[Unit]
# 유닛의 메타데이터와 의존성을 정의
Description=서비스 설명
Documentation=https://example.com/docs
After=network.target # 이 유닛 이후에 시작 (순서)
Requires=postgresql.service # 이 유닛이 필수 (의존성)
Wants=redis.service # 이 유닛이 있으면 좋음 (약한 의존성)
[Service]
# 서비스의 실행 방식과 동작을 정의
Type=notify
ExecStart=/usr/bin/myapp --config /etc/myapp/config.yaml
Restart=on-failure
[Install]
# systemctl enable 시 심볼릭 링크 생성 위치
WantedBy=multi-user.target
4. Service Type 비교
Type= 디렉티브는 systemd가 서비스의 시작 완료를 어떻게 판단하는지를 결정하는 핵심 설정이다.
| Type | 시작 완료 판단 | 적합한 서비스 | PID 추적 |
|---|---|---|---|
simple (기본) | ExecStart 프로세스 시작 즉시 | 포그라운드 실행 데몬 | MainPID = ExecStart PID |
exec | ExecStart 바이너리 exec() 성공 시 | simple과 유사, 더 정확 | MainPID = ExecStart PID |
forking | ExecStart 프로세스가 종료하고 자식이 남을 때 | 전통적 fork-daemon (nginx, Apache) | PIDFile= 필요 |
oneshot | ExecStart 프로세스가 완전히 종료될 때 | 초기화 스크립트, 일회성 작업 | 프로세스 없음 |
dbus | D-Bus 이름 등록 완료 시 | D-Bus 서비스 | BusName= 필요 |
notify | sd_notify() READY=1 수신 시 | 준비 완료를 직접 알리는 서비스 | MainPID = ExecStart PID |
idle | 모든 작업 디스패치 후 | 콘솔 출력 서비스 | simple과 동일 |
프로덕션 권장: 가능하면
Type=notify를 사용하라. 서비스가 실제로 요청을 처리할 준비가 된 시점을 정확히 알 수 있어 의존 서비스 시작이 안전해진다.
5. 프로덕션 수준 Unit 파일 예시
5.1 웹 애플리케이션 서비스
[Unit]
Description=My Web Application
Documentation=https://wiki.internal.example.com/myapp
After=network-online.target postgresql.service redis.service
Wants=network-online.target
Requires=postgresql.service
ConditionPathExists=/etc/myapp/config.yaml
[Service]
Type=notify
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
# 환경 변수
EnvironmentFile=-/etc/myapp/env
Environment=LANG=en_US.UTF-8
# 실행
ExecStartPre=/opt/myapp/bin/check-config --validate
ExecStart=/opt/myapp/bin/server --config /etc/myapp/config.yaml
ExecReload=/bin/kill -HUP $MAINPID
ExecStopPost=/opt/myapp/bin/cleanup
# 재시작 정책
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=300
StartLimitBurst=5
# 타임아웃
TimeoutStartSec=30
TimeoutStopSec=30
WatchdogSec=60
# 리소스 제한
MemoryMax=2G
MemoryHigh=1536M
CPUQuota=200%
TasksMax=512
LimitNOFILE=65535
# 보안 강화
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectControlGroups=true
ReadWritePaths=/var/lib/myapp /var/log/myapp
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true
# 로깅
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp
[Install]
WantedBy=multi-user.target
5.2 Oneshot 초기화 스크립트
[Unit]
Description=Initialize application database schema
After=postgresql.service
Requires=postgresql.service
ConditionPathExists=!/var/lib/myapp/.db-initialized
[Service]
Type=oneshot
User=myapp
Group=myapp
ExecStart=/opt/myapp/bin/migrate --apply
ExecStartPost=/usr/bin/touch /var/lib/myapp/.db-initialized
RemainAfterExit=true
TimeoutStartSec=120
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
6. 소켓 활성화 (Socket Activation)
소켓 활성화는 서비스를 미리 시작하지 않고, 요청이 들어올 때 자동으로 서비스를 기동하는 메커니즘이다. 부팅 속도를 높이고 리소스를 절약한다.
6.1 소켓 유닛 파일
# /etc/systemd/system/myapp.socket
[Unit]
Description=My Application Socket
[Socket]
ListenStream=8080
ListenStream=/run/myapp/myapp.sock
SocketUser=myapp
SocketGroup=myapp
SocketMode=0660
# 연결 큐 크기
Backlog=4096
# 동시 연결 제한
MaxConnections=256
MaxConnectionsPerSource=16
# 소켓 옵션
KeepAlive=true
NoDelay=true
# Accept 모드
# false: 서비스에 소켓 fd 전달 (권장)
# true: 연결당 서비스 인스턴스 생성 (inetd 방식)
Accept=false
[Install]
WantedBy=sockets.target
6.2 소켓과 연동되는 서비스
# /etc/systemd/system/myapp.service
[Unit]
Description=My Application
After=network.target
Requires=myapp.socket
[Service]
Type=notify
User=myapp
Group=myapp
ExecStart=/opt/myapp/bin/server
# 소켓 활성화 시 서비스는 systemd로부터 파일 디스크립터를 전달받음
# sd_listen_fds() 또는 환경변수 LISTEN_FDS로 확인
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
소켓 활성화를 사용하면 서비스 간 순서 의존성 문제를 해결할 수 있다. 소켓은 서비스보다 먼저 생성되므로, 의존 서비스가 아직 준비되지 않아도 연결 요청이 큐에 쌓인다.
# 소켓 활성화 설정
systemctl enable --now myapp.socket
# 소켓 상태 확인
systemctl status myapp.socket
systemctl list-sockets
# 서비스는 요청이 올 때 자동 시작됨
curl http://localhost:8080/health
systemctl status myapp.service # active (running)
7. systemd 타이머 (cron 대체)
systemd 타이머는 cron보다 정밀한 스케줄링과 로깅을 제공한다.
7.1 타이머 유닛 파일
# /etc/systemd/system/backup-database.timer
[Unit]
Description=Database Backup Timer
Documentation=https://wiki.internal.example.com/backup
[Timer]
# 매일 새벽 2시 실행
OnCalendar=*-*-* 02:00:00
# 서버가 꺼져 있었던 동안 놓친 실행을 부팅 후 수행
Persistent=true
# 실행 시간을 최대 15분 무작위 지연 (여러 서버 동시 실행 방지)
RandomizedDelaySec=900
# 정확한 시간 대신 분 단위 정확도 사용 (전력 절약)
AccuracySec=60
[Install]
WantedBy=timers.target
7.2 타이머가 실행하는 서비스
# /etc/systemd/system/backup-database.service
[Unit]
Description=Database Backup
After=postgresql.service
[Service]
Type=oneshot
User=backup
Group=backup
ExecStart=/opt/backup/bin/pg-backup --full --compress
ExecStartPost=/opt/backup/bin/upload-to-s3
# 백업 실패 시 알림
ExecStopPost=/opt/backup/bin/notify-on-failure
TimeoutStartSec=3600
StandardOutput=journal
StandardError=journal
SyslogIdentifier=db-backup
# 리소스 제한 (백업이 서비스에 영향 주지 않도록)
CPUQuota=50%
IOWeight=10
Nice=19
7.3 타이머 OnCalendar 문법
| 표현 | 의미 |
|---|---|
*-*-* 02:00:00 | 매일 02:00 |
Mon *-*-* 09:00:00 | 매주 월요일 09:00 |
*-*-01 00:00:00 | 매월 1일 00:00 |
*-01,07-01 00:00:00 | 1월, 7월 1일 00:00 |
hourly | 매 시간 정각 |
daily | 매일 00:00 |
weekly | 매주 월요일 00:00 |
# 타이머 표현식 테스트
systemd-analyze calendar "*-*-* 02:00:00"
systemd-analyze calendar "Mon..Fri *-*-* 09:00:00"
# 타이머 관리
systemctl enable --now backup-database.timer
systemctl list-timers --all
systemctl status backup-database.timer
# 즉시 실행 (테스트)
systemctl start backup-database.service
8. cgroup 리소스 제어
systemd는 cgroup v2를 활용하여 서비스별 리소스 사용량을 정밀하게 제한한다. 이는 한 서비스의 폭주가 전체 시스템에 영향을 주는 것을 방지하는 핵심 기능이다.
8.1 리소스 제한 디렉티브
[Service]
# === 메모리 제한 ===
MemoryMax=2G # 절대 상한 (초과 시 OOM Kill)
MemoryHigh=1536M # 소프트 상한 (초과 시 메모리 회수 압력 증가)
MemorySwapMax=0 # 스왑 사용 금지
MemoryMin=256M # 메모리 보장 (이 이하로 회수 불가)
# === CPU 제한 ===
CPUQuota=200% # 최대 CPU 사용량 (200% = 2코어)
CPUWeight=100 # 상대적 CPU 가중치 (기본 100, 범위 1-10000)
AllowedCPUs=0-3 # 허용 CPU 코어 지정
# === I/O 제한 ===
IOWeight=100 # 상대적 I/O 가중치 (기본 100, 범위 1-10000)
IOReadBandwidthMax=/dev/sda 100M # 읽기 대역폭 제한
IOWriteBandwidthMax=/dev/sda 50M # 쓰기 대역폭 제한
IOReadIOPSMax=/dev/sda 1000 # 읽기 IOPS 제한
# === 기타 제한 ===
TasksMax=512 # 최대 프로세스(스레드) 수
LimitNOFILE=65535 # 최대 열린 파일 수
LimitNPROC=4096 # 최대 프로세스 수
LimitCORE=0 # 코어 덤프 크기 제한 (0 = 비활성)
8.2 리소스 사용량 모니터링
# 서비스별 리소스 사용량 확인
systemctl status myapp.service
# cgroup 상세 정보
systemd-cgtop
# 특정 서비스 cgroup 경로 확인
systemctl show myapp.service -p ControlGroup
# 메모리 상세
cat /sys/fs/cgroup/system.slice/myapp.service/memory.current
cat /sys/fs/cgroup/system.slice/myapp.service/memory.max
cat /sys/fs/cgroup/system.slice/myapp.service/memory.events
# CPU 사용률
cat /sys/fs/cgroup/system.slice/myapp.service/cpu.stat
9. journalctl 로그 관리
systemd-journald는 모든 서비스의 stdout/stderr, syslog 메시지, 커널 로그를 구조화된 바이너리 형식으로 수집한다.
9.1 핵심 조회 명령
# 특정 서비스 로그
journalctl -u myapp.service
# 실시간 로그 추적 (tail -f 대체)
journalctl -u myapp.service -f
# 최근 N줄
journalctl -u myapp.service -n 100
# 시간 범위 필터
journalctl -u myapp.service --since "2026-03-14 09:00" --until "2026-03-14 12:00"
journalctl -u myapp.service --since "1 hour ago"
# 에러 레벨 이상만
journalctl -u myapp.service -p err
# JSON 출력 (파싱용)
journalctl -u myapp.service -o json-pretty
# 부팅 이후 로그만
journalctl -u myapp.service -b
# 커널 로그 (dmesg 대체)
journalctl -k
# 특정 PID
journalctl _PID=12345
# 디스크 사용량 확인
journalctl --disk-usage
# 오래된 로그 정리
journalctl --vacuum-time=30d # 30일 이전 삭제
journalctl --vacuum-size=1G # 1GB 초과 삭제
9.2 저널 영구 저장 설정
기본적으로 많은 배포판에서 저널은 /run/log/journal/에 저장되어 재부팅 시 사라진다. 영구 저장으로 변경하려면 다음과 같이 설정한다.
# 영구 저장 디렉토리 생성
mkdir -p /var/log/journal
systemd-tmpfiles --create --prefix /var/log/journal
# /etc/systemd/journald.conf 설정
# Storage=persistent
# Compress=yes
# SystemMaxUse=2G
# SystemKeepFree=4G
# MaxRetentionSec=90day
# MaxFileSec=1month
# ForwardToSyslog=yes
# 설정 적용
systemctl restart systemd-journald
10. 의존성 관리
10.1 의존성 디렉티브 비교
| 디렉티브 | 효과 | 순서 지정 여부 |
|---|---|---|
Requires=B | B가 실패하면 A도 중지 | 순서 미지정 (병렬 시작) |
Wants=B | B가 실패해도 A는 계속 | 순서 미지정 |
BindsTo=B | B가 중지되면 A도 즉시 중지 | 순서 미지정 |
Requisite=B | B가 이미 활성 상태가 아니면 A 시작 실패 | 순서 미지정 |
PartOf=B | B가 restart/stop되면 A도 함께 | 순서 미지정 |
After=B | B 시작 완료 후 A 시작 | 순서만 지정 (의존성 아님) |
Before=B | A 시작 완료 후 B 시작 | 순서만 지정 |
핵심 포인트:
Requires=와After=는 별개다.Requires=B만 있으면 A와 B가 동시에 시작된다. B가 준비된 후 A를 시작하려면 반드시After=B를 함께 사용해야 한다.
10.2 의존성 시각화
# 특정 유닛의 의존성 트리
systemctl list-dependencies myapp.service
# 역방향 의존성 (누가 나를 필요로 하는가)
systemctl list-dependencies myapp.service --reverse
# 전체 부팅 순서 시각화
systemd-analyze dot | dot -Tsvg > /tmp/systemd-deps.svg
# 부팅 시간 분석
systemd-analyze blame
systemd-analyze critical-chain myapp.service
11. systemctl 핵심 명령어
# === 서비스 제어 ===
systemctl start myapp.service # 시작
systemctl stop myapp.service # 중지
systemctl restart myapp.service # 재시작
systemctl reload myapp.service # 설정 리로드 (ExecReload)
systemctl reload-or-restart myapp # reload 가능하면 reload, 아니면 restart
# === 부팅 시 자동 시작 ===
systemctl enable myapp.service # 활성화 (심볼릭 링크 생성)
systemctl disable myapp.service # 비활성화
systemctl enable --now myapp # 활성화 + 즉시 시작
systemctl is-enabled myapp # 활성화 상태 확인
# === 상태 확인 ===
systemctl status myapp.service # 상태, 최근 로그, PID
systemctl is-active myapp # active/inactive
systemctl is-failed myapp # failed 여부
# === 유닛 파일 관리 ===
systemctl daemon-reload # 유닛 파일 변경 후 반드시 실행
systemctl cat myapp.service # 유닛 파일 내용 출력
systemctl show myapp.service # 모든 속성 출력
systemctl edit myapp.service # 드롭인 오버라이드 생성
systemctl edit --full myapp # 전체 유닛 파일 편집
# === 마스킹 (완전 비활성화) ===
systemctl mask myapp.service # 어떤 방법으로도 시작 불가
systemctl unmask myapp.service # 마스킹 해제
# === 전체 시스템 ===
systemctl list-units --type=service --state=failed # 실패한 서비스
systemctl list-unit-files --type=service # 설치된 서비스 목록
systemctl --failed # 실패한 유닛 요약
12. 운영 주의사항과 실패 사례
12.1 Restart=always 무한 루프
가장 흔한 실수 중 하나는 Restart=always를 설정하면서 시작 횟수 제한을 두지 않는 것이다.
문제 시나리오: 설정 파일 오류로 서비스가 시작 직후 크래시하면, systemd가 무한히 재시작을 시도한다. 이로 인해 CPU 사용률이 치솟고, 로그가 디스크를 가득 채운다.
해결: 반드시 StartLimitIntervalSec과 StartLimitBurst를 함께 설정하라.
[Service]
Restart=on-failure
RestartSec=5
[Unit]
# 300초(5분) 내에 5번 이상 재시작 시도하면 유닛을 failed 상태로 전환
StartLimitIntervalSec=300
StartLimitBurst=5
# 시작 제한에 도달했을 때의 동작
# none: 아무것도 안 함 (기본)
# reboot: 시스템 재부팅
# reboot-force: 강제 재부팅
# reboot-immediate: 즉시 재부팅
StartLimitAction=none
주의:
StartLimitIntervalSec과StartLimitBurst는[Unit]섹션에 속한다(Service 섹션이 아니다). 잘못된 섹션에 넣으면 무시된다.
12.2 의존성 순환 (Dependency Cycle)
문제 시나리오: A가 After=B, B가 After=A를 참조하면 순환 의존성이 발생한다. systemd는 이를 탐지하고 경고를 출력하지만, 유닛 중 하나의 순서를 무시하고 시작하므로 예측 불가능한 동작이 생긴다.
# 순환 의존성 탐지
systemd-analyze verify myapp.service
journalctl -b | grep "ordering cycle"
systemctl list-dependencies myapp.service
해결 전략:
- 소켓 활성화를 사용하여 순서 의존성을 해소
- 공통 의존성을 target 유닛으로 추출
After=를 제거하고 서비스가 자체적으로 의존성 대기를 구현
12.3 ExecStart에서 쉘 기능 사용
ExecStart=에서 파이프(|), 리다이렉션(>), 변수 치환 등 쉘 기능을 직접 사용할 수 없다. systemd는 쉘을 거치지 않고 직접 실행하기 때문이다.
# 잘못된 예 - 동작하지 않음
ExecStart=/opt/app/bin/server | tee /var/log/app.log
ExecStart=/opt/app/bin/server > /dev/null 2>&1
# 올바른 예 - 쉘을 명시적으로 호출
ExecStart=/bin/bash -c '/opt/app/bin/server | tee /var/log/app.log'
# 더 나은 예 - systemd의 로깅 기능 활용
ExecStart=/opt/app/bin/server
StandardOutput=journal
StandardError=journal
12.4 PIDFile 지정 오류
Type=forking 서비스에서 PIDFile= 경로가 실제 PID 파일 위치와 다르면, systemd는 서비스 상태를 올바르게 추적할 수 없다.
# 올바른 설정
[Service]
Type=forking
PIDFile=/run/nginx/nginx.pid
ExecStart=/usr/sbin/nginx
# nginx.conf의 pid 디렉티브가 같은 경로를 가리켜야 함
12.5 EnvironmentFile 누락
# EnvironmentFile 앞의 '-'는 파일이 없어도 에러를 무시한다는 의미
EnvironmentFile=-/etc/myapp/env
# '-' 없이 지정하면 파일이 없을 때 서비스 시작이 실패함
EnvironmentFile=/etc/myapp/env
13. 보안 강화 (Sandboxing)
systemd는 서비스별로 강력한 샌드박싱 옵션을 제공한다.
[Service]
# === 필수 보안 옵션 ===
NoNewPrivileges=true # 권한 상승 방지
PrivateTmp=true # /tmp 격리
ProtectSystem=strict # 파일시스템 읽기 전용 (ReadWritePaths로 예외)
ProtectHome=true # /home, /root, /run/user 접근 차단
# === 커널 보호 ===
ProtectKernelModules=true # 커널 모듈 로드/언로드 차단
ProtectKernelTunables=true # /proc/sys, /sys 쓰기 차단
ProtectKernelLogs=true # /dev/kmsg, /proc/kmsg 접근 차단
ProtectControlGroups=true # /sys/fs/cgroup 쓰기 차단
# === 네트워크 제한 ===
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX # 허용 소켓 타입
PrivateNetwork=false # true 시 네트워크 완전 차단
# === 시스템콜 필터링 ===
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM # 차단된 시스콜에 EPERM 반환
# === 기타 ===
PrivateDevices=true # /dev 최소화
ProtectClock=true # 시스템 시계 변경 차단
RestrictNamespaces=true # 네임스페이스 생성 차단
RestrictRealtime=true # 실시간 스케줄링 차단
RestrictSUIDSGID=true # SUID/SGID 파일 생성 차단
LockPersonality=true # 실행 도메인 변경 차단
MemoryDenyWriteExecute=true # W^X 정책 강제
# 서비스 보안 점수 확인
systemd-analyze security myapp.service
# 출력 예시:
# OVERALL EXPOSURE LEVEL: 2.1 OK
# 점수가 낮을수록 보안이 강화된 상태
14. 트러블슈팅 체크리스트
서비스가 시작되지 않거나 비정상 동작할 때 단계적으로 점검할 체크리스트다.
14.1 서비스 시작 실패
# 1단계: 상태 확인
systemctl status myapp.service -l --no-pager
# 2단계: 전체 로그 확인
journalctl -u myapp.service -n 50 --no-pager
# 3단계: 유닛 파일 문법 검증
systemd-analyze verify /etc/systemd/system/myapp.service
# 4단계: 유닛 파일 변경 후 reload 여부
systemctl daemon-reload
# 5단계: 의존성 확인
systemctl list-dependencies myapp.service
# 6단계: 실행 파일 존재 및 권한
ls -la /opt/myapp/bin/server
file /opt/myapp/bin/server
# 7단계: SELinux/AppArmor 차단 여부
ausearch -m AVC -ts recent # SELinux
journalctl -k | grep apparmor # AppArmor
# 8단계: 리소스 제한 확인
systemctl show myapp.service -p MemoryMax,CPUQuota,TasksMax,LimitNOFILE
14.2 주요 에러와 원인
| 에러 메시지 | 원인 | 해결 |
|---|---|---|
Main process exited, code=exited, status=203/EXEC | ExecStart 경로 오류 또는 실행 권한 없음 | 경로와 실행 권한 확인 |
Main process exited, code=exited, status=217/USER | User= 디렉티브의 사용자가 존재하지 않음 | 사용자 생성 또는 User= 수정 |
Failed to set up mount namespacing | PrivateTmp 등 네임스페이스 옵션 충돌 | ProtectSystem, PrivateTmp 설정 검토 |
Start request repeated too quickly | StartLimitBurst 초과 | 근본 원인 해결 후 systemctl reset-failed |
Dependency failed | Requires= 대상 유닛 시작 실패 | 의존 유닛 상태 확인 |
code=killed, signal=KILL | OOM Kill 또는 TimeoutStopSec 초과 | MemoryMax 증가 또는 타임아웃 조정 |
code=killed, signal=ABRT | WatchdogSec 타임아웃 | WatchdogSec 증가 또는 서비스 응답 개선 |
14.3 서비스 복구 절차
# 실패 카운터 리셋 (StartLimitBurst 도달 후)
systemctl reset-failed myapp.service
# 서비스 강제 중지 (정상 종료 실패 시)
systemctl kill myapp.service
systemctl kill -s SIGKILL myapp.service
# 유닛 파일 변경 사항 반영
systemctl daemon-reload
# 서비스 재시작
systemctl restart myapp.service
# cgroup에 남은 프로세스 확인
systemd-cgls /system.slice/myapp.service
15. 드롭인 오버라이드 (Drop-in Override)
패키지가 제공하는 유닛 파일을 직접 수정하면 패키지 업데이트 시 덮어써진다. 드롭인 오버라이드를 사용하면 원본을 유지하면서 특정 설정만 변경할 수 있다.
# 드롭인 파일 생성 (에디터 실행)
systemctl edit myapp.service
# /etc/systemd/system/myapp.service.d/override.conf 생성
# 예: 메모리 제한과 재시작 정책 추가
# [Service]
# MemoryMax=4G
# Restart=on-failure
# RestartSec=10
# /etc/systemd/system/myapp.service.d/override.conf
[Service]
# 기존 ExecStart를 변경하려면 먼저 빈 값으로 초기화해야 함
ExecStart=
ExecStart=/opt/myapp/bin/server --config /etc/myapp/production.yaml
# 리소스 제한 추가
MemoryMax=4G
CPUQuota=300%
중요:
ExecStart=를 오버라이드할 때는 반드시 빈 값으로 먼저 초기화한 후 새 값을 지정해야 한다. 그렇지 않으면 기존 값에 추가되어 두 번 실행된다.
16. 실전 디버깅 시나리오
시나리오 1: 서비스가 Active (running)인데 요청을 처리하지 않음
# 1. 프로세스가 살아있는지 확인
systemctl status myapp.service # PID 확인
ls -la /proc/PID_NUMBER/fd/ # 열린 파일 디스크립터
strace -p PID_NUMBER -e trace=network # 네트워크 시스콜 추적
# 2. 포트 리스닝 확인
ss -tlnp | grep myapp
# 3. 메모리/CPU 상태
systemd-cgtop -n 1
# 4. 로그에서 에러 검색
journalctl -u myapp.service -p warning --since "10 min ago"
시나리오 2: 부팅 후 서비스가 시작되지 않음
# 1. enable 상태 확인
systemctl is-enabled myapp.service
# 2. 의존성 타겟 확인
systemctl list-dependencies multi-user.target | grep myapp
# 3. 부팅 순서 분석
systemd-analyze critical-chain myapp.service
# 4. 조건 충족 여부
systemctl show myapp.service -p ConditionResult,AssertResult
journalctl -u myapp.service -b | grep -i condition
마무리
systemd는 단순한 init 시스템이 아니라 서비스 관리의 전체 생명주기를 아우르는 프레임워크다. 핵심 원칙을 정리한다.
- Unit 파일은 코드다: 버전 관리하고, 코드 리뷰를 받고, 테스트하라.
systemd-analyze verify로 문법 오류를 사전에 잡아라. - Type을 정확히 선택하라: 서비스의 실제 동작 방식에 맞는 Type을 설정하지 않으면 systemd가 상태를 올바르게 추적할 수 없다.
- Restart 정책에는 반드시 제한을 두어라:
StartLimitIntervalSec과StartLimitBurst없는Restart=always는 시한폭탄이다. - 리소스 제한을 기본으로 설정하라:
MemoryMax,CPUQuota,TasksMax는 프로덕션 서비스의 필수 설정이다. 한 서비스의 폭주가 전체 시스템을 마비시키는 것을 방지한다. - 보안 샌드박싱을 적극 활용하라:
NoNewPrivileges=true,ProtectSystem=strict,PrivateTmp=true는 최소한의 기본 보안이다. - journalctl을 마스터하라: 구조화된 로그 조회, 시간 범위 필터, 우선순위 필터를 활용하면 트러블슈팅 시간을 크게 단축할 수 있다.
- 드롭인 오버라이드를 사용하라: 패키지 유닛 파일을 직접 수정하지 말고
systemctl edit을 사용하라.
systemd를 잘 다루는 것은 리눅스 서버 운영의 기본기다. 이 글의 예시를 자신의 환경에 맞게 적용하고, 공식 문서를 꾸준히 참조하여 더 안정적인 서비스 운영 환경을 구축하자.
참고 자료
- systemd 공식 매뉴얼 (freedesktop.org)
- systemd.service(5) - Service Unit Configuration
- systemd.unit(5) - Unit Configuration
- systemd.exec(5) - Execution Environment Configuration
- systemd.resource-control(5) - Resource Control
- systemd.timer(5) - Timer Unit Configuration
- systemd-journald(8) - Journal Service
- Arch Wiki - systemd
- Red Hat Documentation - Managing Services with systemd
- DigitalOcean - Understanding Systemd Units and Unit Files