Skip to content

필사 모드: systemd 서비스 관리 완벽 가이드: Unit 파일 작성부터 트러블슈팅까지

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

들어가며

현대 리눅스 배포판에서 서비스 관리의 핵심은 **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 시스템이 아니라 **서비스 관리의 전체 생명주기를 아우르는 프레임워크**다. 핵심 원칙을 정리한다.

1. **Unit 파일은 코드다**: 버전 관리하고, 코드 리뷰를 받고, 테스트하라. `systemd-analyze verify`로 문법 오류를 사전에 잡아라.

2. **Type을 정확히 선택하라**: 서비스의 실제 동작 방식에 맞는 Type을 설정하지 않으면 systemd가 상태를 올바르게 추적할 수 없다.

3. **Restart 정책에는 반드시 제한을 두어라**: `StartLimitIntervalSec`과 `StartLimitBurst` 없는 `Restart=always`는 시한폭탄이다.

4. **리소스 제한을 기본으로 설정하라**: `MemoryMax`, `CPUQuota`, `TasksMax`는 프로덕션 서비스의 필수 설정이다. 한 서비스의 폭주가 전체 시스템을 마비시키는 것을 방지한다.

5. **보안 샌드박싱을 적극 활용하라**: `NoNewPrivileges=true`, `ProtectSystem=strict`, `PrivateTmp=true`는 최소한의 기본 보안이다.

6. **journalctl을 마스터하라**: 구조화된 로그 조회, 시간 범위 필터, 우선순위 필터를 활용하면 트러블슈팅 시간을 크게 단축할 수 있다.

7. **드롭인 오버라이드를 사용하라**: 패키지 유닛 파일을 직접 수정하지 말고 `systemctl edit`을 사용하라.

systemd를 잘 다루는 것은 리눅스 서버 운영의 기본기다. 이 글의 예시를 자신의 환경에 맞게 적용하고, 공식 문서를 꾸준히 참조하여 더 안정적인 서비스 운영 환경을 구축하자.

참고 자료

- [systemd 공식 매뉴얼 (freedesktop.org)](https://www.freedesktop.org/software/systemd/man/)

- [systemd.service(5) - Service Unit Configuration](https://www.freedesktop.org/software/systemd/man/systemd.service.html)

- [systemd.unit(5) - Unit Configuration](https://www.freedesktop.org/software/systemd/man/systemd.unit.html)

- [systemd.exec(5) - Execution Environment Configuration](https://www.freedesktop.org/software/systemd/man/systemd.exec.html)

- [systemd.resource-control(5) - Resource Control](https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html)

- [systemd.timer(5) - Timer Unit Configuration](https://www.freedesktop.org/software/systemd/man/systemd.timer.html)

- [systemd-journald(8) - Journal Service](https://www.freedesktop.org/software/systemd/man/systemd-journald.html)

- [Arch Wiki - systemd](https://wiki.archlinux.org/title/Systemd)

- [Red Hat Documentation - Managing Services with systemd](https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/configuring_basic_system_settings/managing-system-services-with-systemctl_configuring-basic-system-settings)

- [DigitalOcean - Understanding Systemd Units and Unit Files](https://www.digitalocean.com/community/tutorials/understanding-systemd-units-and-unit-files)

현재 단락 (1/395)

현대 리눅스 배포판에서 서비스 관리의 핵심은 **systemd**다. PID 1으로 시스템 초기화를 담당하며, 서비스 시작/중지, 의존성 해결, 리소스 제한, 로깅까지 하나의 프레...

작성 글자: 0원문 글자: 18,204작성 단락: 0/395