systemd 가이드

systemd의 PID 1 설계 철학, Unit 시스템, 부팅 순서, 서비스 관리, 커널 인터페이스 통합, 주요 구성 요소(journald, udevd, networkd, resolved, logind, oomd, nspawn), 리소스 제어, 보안 강화, 디버깅 기법까지 리눅스 사용자 공간(User Space) 초기화의 핵심을 심층 분석합니다.

전제 조건: 부팅 과정Control Groups (cgroups) 문서를 먼저 읽으세요. systemd는 커널이 PID 1으로 실행하는 최초의 사용자 공간 프로세스이며, cgroup v2를 리소스 관리의 핵심 인터페이스로 사용합니다.
일상 비유: 이 주제는 교통 통제 시스템과 비슷합니다. 도시의 교통 신호, 도로 안내, 응급 차량 우선 통과 등을 중앙에서 조율하듯이, systemd는 모든 서비스의 시작 순서, 의존성, 리소스 할당을 중앙에서 관리합니다.

핵심 요약

  • PID 1 — 커널이 실행하는 최초 사용자 프로세스, 모든 고아 프로세스의 부모
  • Unit 시스템 — 서비스, 소켓, 타이머 등 12가지 유닛 타입의 의존성 그래프(DAG) 기반 관리
  • 병렬 부팅 — 소켓/D-Bus/장치 활성화로 의존성 대기 없이 병렬 시작
  • cgroup v2 통합 — 모든 서비스에 자동 cgroup 할당, 리소스 제어와 감시
  • 에코시스템 — journald, udevd, networkd, resolved, logind, oomd 등 통합 구성 요소

단계별 이해

  1. PID 1 역할 이해
    커널에서 사용자 공간으로의 전환점과 init 프로세스의 책임을 파악합니다.
  2. Unit 의존성 학습
    서비스 간 Requires/Wants/After 관계로 부팅 순서가 결정되는 원리를 이해합니다.
  3. 서비스 관리 실습
    systemctl 명령어로 서비스 상태 조회, 시작, 중지, 활성화를 실습합니다.
  4. 디버깅 도구 활용
    journalctl, systemd-analyze로 문제 진단과 부팅 최적화를 수행합니다.
관련 표준: Linux Standard Base (LSB) 5.0 — init 스크립트 호환성 규격, D-Bus Specification 1.0 — 프로세스 간 통신(IPC) 규격. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

systemd 개요 및 설계 철학

systemd는 리눅스 시스템의 PID 1 init 시스템이자 시스템 관리자(System Manager)입니다. 2010년 Lennart Poettering과 Kay Sievers가 Fedora 프로젝트에서 처음 도입한 이후, 대부분의 주요 배포판이 기본 init 시스템으로 채택했습니다. systemd는 단순한 서비스 시작 도구가 아니라, 로그 관리, 장치 관리, 네트워크 설정, 컨테이너 관리까지 포괄하는 시스템 관리 프레임워크(System Management Framework)입니다.

PID 1의 역할

리눅스 커널은 초기화가 완료되면 /sbin/init(또는 커널 명령행의 init= 파라미터가 지정한 바이너리)를 PID 1으로 실행합니다. PID 1은 특별한 책임을 갖습니다.

SysVinit/Upstart 비교

항목SysVinitUpstartsystemd
부팅 모델순차(Sequential)이벤트(Event)의존성 그래프(DAG)
병렬 시작제한적이벤트 기반 부분 병렬소켓/D-Bus 활성화로 완전 병렬
서비스 감시PID 파일 기반expect fork/daemoncgroup 기반 정확한 추적
의존성 표현LSB 헤더(Header) 숫자 순서start on/stop onRequires/Wants/After/Before
소켓 활성화inetd 별도미지원네이티브 지원
로그syslog 텍스트syslog 텍스트journald 바이너리 구조화
리소스 제어ulimit 수동제한적 cgroupcgroup v2 네이티브 통합
설정 형식셸 스크립트Upstart job 파일선언적 INI 형식 유닛 파일

설계 원칙

systemd의 핵심 설계 원칙은 다음과 같습니다.

에코시스템 구성 요소

구성 요소역할커널 인터페이스
systemd (PID 1)서비스/유닛 관리, 부팅 조율cgroup, signalfd, epoll, timerfd
systemd-journald구조화 로그 수집/저장/dev/kmsg, AF_UNIX sockets
systemd-udevd장치 이벤트 처리, /dev 관리netlink (KOBJECT_UEVENT)
systemd-logind세션/좌석/사용자 관리evdev, VT, /sys/class/tty
systemd-networkd네트워크 설정netlink (NETLINK_ROUTE)
systemd-resolvedDNS 리졸버(Resolver)AF_INET/AF_INET6 sockets
systemd-oomdPSI 기반 OOM 관리cgroup v2 memory.pressure
systemd-timedated시간/시간대(Timezone) 관리settimeofday, adjtimex
systemd-tmpfiles임시 파일/디렉터리 관리파일시스템 API
systemd-sysctl커널 파라미터 설정/proc/sys
systemd-bootUEFI 부트로더EFI Boot Services
systemd-nspawn경량 컨테이너(Container)namespaces, cgroups, seccomp
systemd-homed휴대용 홈 디렉터리(Home Directory)LUKS, FUSE, btrfs
Linux Kernel cgroup v2 fs sysfs (/sys) devtmpfs netlink procfs (/proc) epoll/signalfd seccomp-bpf / namespaces / capabilities systemd (PID 1) Service Manager / Unit Engine D-Bus System Bus journald udevd logind tmpfiles networkd resolved oomd timedated systemctl journalctl loginctl networkctl syscalls / fs API Core Daemons Network/Resource User CLI Tools

systemd의 역사와 채택

systemd는 2010년 4월 Lennart Poettering이 "Rethinking PID 1"이라는 블로그 포스트에서 설계 철학을 공개하면서 시작되었습니다. 기존 SysVinit의 순차적 셸 스크립트 모델과 Upstart의 이벤트 기반 모델의 한계를 분석하고, macOS의 launchd에서 영감을 받은 새로운 init 시스템을 제안했습니다.

systemd의 기능 범위 확장에 대한 논의도 있습니다. init 시스템에서 시작하여 로그, 네트워크, DNS, 부트로더, 컨테이너, 홈 디렉터리 관리까지 확장되었기 때문입니다. 지지하는 측에서는 통합 에코시스템의 일관성과 재사용성을 강조하고, 비판하는 측에서는 유닉스 철학과 모듈성의 관점에서 우려를 표명합니다. 어느 쪽이든 systemd가 현대 리눅스 시스템 관리의 사실상 표준(de facto standard)이 되었다는 것은 부정하기 어렵습니다.

systemd 소스 코드 구조

systemd 프로젝트의 소스 코드는 기능별로 체계적으로 분리되어 있습니다. 핵심 실행 파일과 주요 라이브러리의 위치를 알면 동작 원리를 추적하는 데 도움이 됩니다.

소스 경로설명
src/core/PID 1 핵심 (manager.c, unit.c, service.c, socket.c, timer.c)
src/journal/journald 구현 (journald-server.c, journal-file.c)
src/udev/udevd 구현 (udevd.c, udev-rules.c)
src/login/logind 구현 (logind.c, logind-session.c)
src/network/networkd 구현 (networkd-manager.c)
src/resolve/resolved 구현 (resolved-manager.c)
src/oom/oomd 구현 (oomd-manager.c)
src/boot/systemd-boot UEFI 애플리케이션
src/nspawn/nspawn 컨테이너 구현
src/libsystemd/sd-bus, sd-event, sd-journal 등 공용 라이브러리
src/shared/내부 공용 유틸리티

이벤트 루프 아키텍처

systemd의 PID 1은 단일 스레드 이벤트 루프(Event Loop)로 동작합니다. sd-event 라이브러리를 기반으로 epoll, signalfd, timerfd, inotify 등의 커널 인터페이스를 통합하여 모든 이벤트를 비동기(Asynchronous)로 처리합니다.

이 단일 루프 설계 덕분에 PID 1은 락(Lock) 없이 동작하며, 데드락(Deadlock) 위험이 없습니다. 다만 이벤트 핸들러가 블로킹(Blocking)되면 전체 시스템 관리가 멈출 수 있으므로, 시간이 걸리는 작업은 별도 워커(Worker) 프로세스에서 처리합니다.

D-Bus 통신

systemd의 모든 제어 인터페이스는 D-Bus(Desktop Bus) 프로토콜을 통해 노출됩니다. systemctlsystemctl start nginx.service를 실행하면, 내부적으로 D-Bus 메시지를 PID 1에 보냅니다.

# D-Bus를 통해 직접 유닛 시작 (systemctl start와 동일)
busctl call org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager \
  StartUnit ss "nginx.service" "replace"

# systemd의 D-Bus 인터페이스 탐색
busctl tree org.freedesktop.systemd1 | head -20

systemd v253부터는 D-Bus와 병행하여 Varlink 프로토콜도 사용합니다. Varlink는 JSON 기반으로 D-Bus보다 가볍고, 컨테이너 환경에서 사용이 용이합니다.

run0 — sudo 대체 도구 (v256+)

systemd v256에서 도입된 run0은 전통적인 sudo를 대체하는 권한 상승(Privilege Escalation) 도구입니다. sudo와 달리 setuid 바이너리를 사용하지 않고, systemd-run의 래퍼(Wrapper)로 동작하여 별도의 PTY(Pseudo-Terminal)에서 새 서비스를 시작합니다. 이를 통해 호출자의 환경 변수, 파일 디스크립터, cgroup 등이 대상 프로세스에 유출되지 않습니다.

항목sudorun0
동작 방식setuid root 바이너리systemd-run + PTY 할당
환경 유출env_keep 설정 필요완전히 격리된 환경
FD 유출close-from 필요새 서비스이므로 FD 없음
cgroup호출자와 같은 cgroup별도 transient scope
보안 모델sudoers 파일 기반polkit 기반
감사sudo 로그journald + D-Bus 감사
# 기본 사용법 (sudo와 동일한 경험)
run0 systemctl restart nginx.service

# 특정 사용자로 실행
run0 --user=postgres psql

# 대화형 루트 셸
run0 --pty bash

# 배경색 변경으로 루트 세션 시각적 구분
# run0은 자동으로 터미널 배경색을 빨간 계열로 변경하여
# 루트 세션임을 시각적으로 알림
호환성: run0은 sudo를 완전히 대체하기보다는 보완적 도구입니다. sudoers 파일의 세밀한 명령 필터링이 필요하면 여전히 sudo가 적합하고, 환경 격리와 보안이 중요하면 run0이 유리합니다.

Unit 시스템

systemd의 관리 대상은 모두 유닛(Unit)이라는 추상 단위로 표현됩니다. 유닛 파일은 선언적 INI 형식으로 작성되며, systemd는 이 파일들의 의존성 관계를 DAG(Directed Acyclic Graph)로 구성하여 시작/정지 순서를 결정합니다. 유닛은 시스템 리소스의 논리적 표현이며, 하나의 유닛은 하나의 관리 대상에 대응합니다.

유닛의 생명주기(Lifecycle)는 다음과 같습니다.

  1. 로드(Load) — 유닛 파일을 읽어 메모리에 적재. systemctl daemon-reload로 재로드
  2. 의존성 해석(Dependency Resolution) — Requires, Wants, After 등의 관계를 DAG로 구성
  3. 활성화(Activation) — 의존성이 충족되면 유닛 시작. 타입별로 시작 완료 판단 기준이 다름
  4. 실행(Running) — 서비스 프로세스 실행 중. cgroup으로 추적
  5. 비활성화(Deactivation) — 중지 요청 시 ExecStop 실행 후 프로세스 종료
  6. 정리(Cleanup) — cgroup 제거, 임시 파일 정리

12가지 유닛 타입

유닛 타입확장자설명예시
서비스(Service).service데몬 프로세스 관리nginx.service
소켓(Socket).socketIPC/네트워크 소켓 관리, 활성화 트리거cups.socket
타이머(Timer).timer주기적/일정 기반 서비스 실행 (cron 대체)logrotate.timer
경로(Path).path파일/디렉터리 변경 감시 (inotify)cups.path
장치(Device).deviceudev 장치 노출dev-sda1.device
마운트(Mount).mount파일시스템 마운트 포인트home.mount
자동 마운트(Automount).automount접근 시 자동 마운트home.automount
스왑(Swap).swap스왑 영역 관리dev-sda2.swap
스코프(Scope).scope외부 생성 프로세스 그룹 (API 전용)session-1.scope
슬라이스(Slice).slicecgroup 계층 노드 (리소스 분배)user.slice
타깃(Target).target유닛 그룹 동기화 지점multi-user.target
스냅샷(Snapshot).snapshot런타임 상태 스냅샷 (임시)런타임 생성

유닛 파일 구조

유닛 파일은 크게 [Unit], 타입별 섹션(예: [Service]), [Install] 세 부분으로 구성됩니다.

[Unit]
Description=My Application Server
Documentation=https://example.com/docs
After=network.target postgresql.service
Requires=postgresql.service
Wants=redis.service

[Service]
Type=notify
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStartPre=/opt/myapp/bin/check-config
ExecStart=/opt/myapp/bin/server --config /etc/myapp/config.toml
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
WatchdogSec=30
TimeoutStartSec=90
TimeoutStopSec=30

[Install]
WantedBy=multi-user.target

의존성 키워드

유닛 간 관계를 정의하는 핵심 키워드를 이해하는 것이 systemd 운영의 기본입니다.

키워드방향효과실패 시 동작
Requires=의존 대상 → 본 유닛대상 유닛도 함께 시작대상 실패 시 본 유닛도 중지
Wants=의존 대상 → 본 유닛대상 유닛도 함께 시작대상 실패해도 본 유닛 유지
BindsTo=의존 대상 → 본 유닛Requires보다 강한 결합대상 중지 시 즉시 본 유닛도 중지
PartOf=본 유닛 → 대상대상 restart/stop 시 본 유닛도 동작대상 재시작 시 따라감
Conflicts=상호 배타한쪽 시작 시 다른 쪽 중지동시 활성화 불가
After=순서 지정대상이 시작된 후 본 유닛 시작시작 순서만 제어 (의존 아님)
Before=순서 지정본 유닛이 먼저 시작시작 순서만 제어 (의존 아님)
주의: After=/Before=순서만 정의하고, Requires=/Wants=의존성만 정의합니다. 일반적으로 두 가지를 함께 사용합니다. 예를 들어 After=network.targetRequires=network.target을 함께 지정해야 네트워크가 준비된 후에 서비스가 시작됩니다.

검색 경로 우선순위

systemd는 유닛 파일을 다음 순서로 검색하며, 먼저 발견된 파일이 우선합니다.

우선순위경로용도
1 (최고)/etc/systemd/system/관리자가 직접 작성하거나 수정한 유닛
2/run/systemd/system/런타임 생성 유닛 (재부팅 시 사라짐)
3 (최저)/usr/lib/systemd/system/패키지 관리자가 설치한 유닛

관리자가 패키지 유닛을 수정하려면 /etc/systemd/system/에 동일 이름의 파일을 만들거나(완전 덮어쓰기), systemctl edit 유닛이름으로 드롭인(Drop-in) 오버라이드 파일(/etc/systemd/system/유닛이름.d/override.conf)을 생성하는 방법이 권장됩니다.

# 드롭인 오버라이드 생성 (편집기가 열림)
sudo systemctl edit nginx.service

# 전체 유닛 파일을 복사하여 수정
sudo systemctl edit --full nginx.service

# 드롭인 파일 직접 확인
cat /etc/systemd/system/nginx.service.d/override.conf
multi-user.target network.target basic.target sshd.service networkd.service resolved.service sysinit.target sshd.socket dbus.socket systemd-journald.socket udevd.service Requires (필수 의존) Wants (선택 의존) After (순서) 유닛 의존성은 DAG(Directed Acyclic Graph) 형태로 순환이 허용되지 않음

유닛 인스턴스화 (템플릿 유닛)

반복적인 유닛 설정을 효율적으로 관리하기 위해 템플릿 유닛(Template Unit)을 사용할 수 있습니다. 파일명에 @를 포함하면 템플릿이 되며, %i로 인스턴스 이름을 참조합니다.

# /etc/systemd/system/container@.service
[Unit]
Description=Container %i
After=network.target

[Service]
Type=simple
ExecStart=/usr/bin/systemd-nspawn -M %i -D /var/lib/machines/%i --boot
ExecStop=/usr/bin/machinectl poweroff %i
Restart=on-failure

[Install]
WantedBy=multi-user.target
# 인스턴스별 활성화
sudo systemctl enable --now container@webserver.service
sudo systemctl enable --now container@database.service

# %i = "webserver" 또는 "database"로 치환됨
systemctl status container@webserver.service

템플릿 유닛에서 사용할 수 있는 주요 스펙시파이어(Specifier)는 다음과 같습니다.

스펙시파이어설명예시
%i인스턴스 이름 (이스케이프 해제)webserver
%I인스턴스 이름 (이스케이프 유지)webserver
%n전체 유닛 이름container@webserver.service
%N전체 유닛 이름 (이스케이프 해제)container@webserver.service
%p유닛 이름 접두사 (@ 이전)container
%H호스트명myserver
%mMachine IDabc123...
%bBoot IDdef456...
%t런타임 디렉터리/run (시스템), /run/user/1000 (사용자)

Condition/Assert 지시어

유닛 실행 전에 사전 조건을 검사할 수 있습니다. Condition*=이 실패하면 유닛을 건너뛰고(skip), Assert*=이 실패하면 에러를 발생시킵니다.

지시어설명
ConditionPathExists=경로 존재 여부 (! 접두사로 부정)
ConditionFileIsExecutable=실행 파일 존재 여부
ConditionDirectoryNotEmpty=디렉터리에 파일 존재 여부
ConditionKernelCommandLine=커널 부트 파라미터 포함 여부
ConditionKernelVersion=커널 버전 매칭
ConditionArchitecture=CPU 아키텍처 매칭 (x86-64, arm64 등)
ConditionVirtualization=가상화 환경 여부 (vm, container, kvm 등)
ConditionSecurity=보안 프레임워크 활성 여부 (selinux, apparmor)
ConditionACPower=AC 전원 연결 여부
ConditionMemory=최소 메모리 용량
ConditionCPUs=최소 CPU 수
[Unit]
# 가상 머신에서만 실행
ConditionVirtualization=vm
# 설정 파일이 존재할 때만 실행
ConditionPathExists=/etc/myapp/config.toml
# 커널 5.10 이상에서만 실행
ConditionKernelVersion=>=5.10
# 최소 2GB RAM
ConditionMemory=2G

Drop-in 오버라이드 메커니즘

패키지 관리자가 설치한 유닛 파일을 직접 수정하면 패키지 업데이트 시 덮어쓰여집니다. 대신 드롭인 디렉터리(Drop-in Directory)를 사용하여 특정 설정만 오버라이드합니다.

# systemctl edit으로 드롭인 파일 생성
sudo systemctl edit nginx.service
# → /etc/systemd/system/nginx.service.d/override.conf 생성

# 여러 드롭인 파일 가능 (알파벳순 로드)
# /etc/systemd/system/nginx.service.d/
# ├── 10-memory.conf    (MemoryMax 설정)
# ├── 20-security.conf  (보안 강화 설정)
# └── 30-logging.conf   (로깅 설정)

# 최종 적용된 유닛 설정 확인
systemctl cat nginx.service
# 원본 파일 + 모든 드롭인 내용이 순서대로 표시됨

systemd 부팅 순서

systemd가 PID 1로 실행되면, default.target을 최종 목표로 설정하고 그에 필요한 모든 유닛을 의존성 순서에 따라 활성화합니다. 이 과정은 전통적인 런레벨(Runlevel) 개념을 타깃(Target) 유닛으로 대체합니다.

Target 계층

주요 타깃은 다음과 같이 계층적으로 구성되며, 상위 타깃은 하위 타깃에 의존합니다.

Target전통 런레벨설명
poweroff.target0시스템 종료
rescue.target1 (single)단일 사용자 모드, 기본 파일시스템만 마운트
multi-user.target3다중 사용자 모드, 네트워크 활성화, GUI 없음
graphical.target5GUI 환경(Display Manager 포함)
reboot.target6시스템 재부팅
emergency.target-최소 환경, 루트 파일시스템 읽기 전용
# 현재 기본 타깃 확인
systemctl get-default
# graphical.target

# 기본 타깃 변경
sudo systemctl set-default multi-user.target

# 런타임에 타깃 전환 (현재 세션에서 즉시 적용)
sudo systemctl isolate rescue.target

Generator 메커니즘

systemd 제너레이터(Generator)는 부팅 초기에 실행되어 다른 설정 파일로부터 유닛 파일을 동적으로 생성하는 바이너리입니다. 이를 통해 /etc/fstab이나 GPT 파티션 정보 같은 기존 설정을 systemd 유닛으로 자동 변환합니다. 제너레이터는 3개의 디렉터리 경로를 인자로 받으며, 생성된 유닛은 일반 유닛과 동일하게 처리됩니다.

# 제너레이터 위치
ls /usr/lib/systemd/system-generators/
# systemd-fstab-generator
# systemd-gpt-auto-generator
# systemd-cryptsetup-generator
# systemd-sysv-generator
# ...

# 생성된 유닛 확인 (임시 디렉터리)
ls /run/systemd/generator/
# 제너레이터가 생성한 .mount, .swap 등 유닛

# 커스텀 제너레이터 작성 위치
# /etc/systemd/system-generators/ (관리자)
# /usr/lib/systemd/system-generators/ (패키지)
제너레이터입력 소스생성 유닛
systemd-fstab-generator/etc/fstab.mount, .swap 유닛
systemd-gpt-auto-generatorGPT 파티션 타입 GUID자동 마운트 유닛
systemd-cryptsetup-generator/etc/crypttabsystemd-cryptsetup@.service
systemd-sysv-generator/etc/init.d/ 스크립트호환 .service 유닛
systemd-veritysetup-generator커널 명령행 verity 파라미터dm-verity 설정 유닛

부팅 최적화 분석

systemd-analyze는 부팅 시간을 분석하는 핵심 도구입니다. 부팅 과정을 펌웨어(Firmware), 로더(Loader), 커널(Kernel), 사용자 공간(Userspace)의 네 단계로 분해하여 각 단계의 소요 시간을 보여줍니다.

# 전체 부팅 시간 요약
systemd-analyze
# Startup finished in 3.456s (firmware) + 1.234s (loader) + 2.345s (kernel) + 5.678s (userspace) = 12.713s

# 유닛별 시작 시간 (가장 느린 순서)
systemd-analyze blame

# 임계 경로(Critical Path) 분석
systemd-analyze critical-chain
# graphical.target @5.678s
# └─multi-user.target @5.600s
#   └─nginx.service @4.200s +1.400s
#     └─network-online.target @4.100s

# SVG 형태의 부팅 차트 생성
systemd-analyze plot > boot-chart.svg

부팅 단계별 실행 유닛

systemd 부팅은 여러 타깃을 순차적으로 거치며, 각 타깃에서 특정 종류의 유닛이 활성화됩니다.

타깃주요 실행 유닛역할
local-fs-pre.targetsystemd-tmpfiles-setup-dev-early/dev 내 최소 노드 생성
local-fs.target*.mount, systemd-fsck@로컬 파일시스템 마운트, fsck 실행
sysinit.targetsystemd-journald, systemd-udevd, systemd-tmpfiles-setup, systemd-sysctl저널링, 장치 관리, 임시 파일, 커널 파라미터
basic.targetsystemd-logind, dbus, systemd-resolved기본 시스템 인프라 (D-Bus, 로그인, DNS)
network.targetsystemd-networkd, NetworkManager네트워크 인터페이스 구성
network-online.targetsystemd-networkd-wait-online네트워크 연결 완료 대기
multi-user.targetsshd, nginx, postgresql 등사용자 서비스, 데몬
graphical.targetdisplay-manager (gdm, lightdm)GUI 환경

의존성 실패 전파

유닛의 의존성이 실패하면, 의존성 유형에 따라 결과가 달라집니다.

의존성 유형실패 시 동작용도
Requires=의존 유닛도 함께 중지필수 의존성 — 데이터베이스 ↔ 앱 서버
Wants=실패해도 무시, 계속 진행선택적 의존성 — 모니터링 에이전트
BindsTo=의존 유닛이 비활성화되면 함께 중지강한 바인딩 — 마운트 포인트 ↔ 서비스
PartOf=부모 유닛 restart/stop 시 함께 동작그룹 관리 — 하위 워커 프로세스
# FailureAction/SuccessAction — 유닛 실패/성공 시 시스템 동작
[Unit]
FailureAction=reboot-force    # 실패 시 강제 재부팅 (임베디드/키오스크)
StartLimitBurst=3             # 30초 내 3회 시작 실패 시...
StartLimitIntervalSec=30
StartLimitAction=reboot       # ...시스템 재부팅

DefaultDependencies

대부분의 유닛은 DefaultDependencies=yes(기본값)로 동작하며, 이 경우 systemd가 자동으로 sysinit.target, basic.target에 대한 After=Conflicts=shutdown.target을 추가합니다. 부팅 초기에 실행해야 하는 유닛(저널링, udev 등)은 DefaultDependencies=no로 설정하여 이 자동 의존성을 비활성화합니다.

rescue/emergency 모드

부팅 문제 발생 시 최소 환경으로 진입하여 복구 작업을 수행할 수 있습니다.

# 커널 명령행에서 직접 진입 (GRUB 편집)
# rescue 모드: systemd.unit=rescue.target
# emergency 모드: systemd.unit=emergency.target
# 또는 간단히: single (rescue), emergency

# 런타임에 전환 (SSH 접속 가능한 상태에서)
sudo systemctl isolate rescue.target

# rescue vs emergency 차이
# rescue: 기본 파일시스템 마운트, 네트워크 없음, 단일 사용자
# emergency: 루트 읽기 전용, 최소 서비스만, 비밀번호 프롬프트

# emergency 모드에서 루트 읽기/쓰기로 재마운트
mount -o remount,rw /

soft-reboot — 사용자 공간만 재시작 (v254+)

systemd v254에서 도입된 soft-reboot는 커널을 유지한 채 사용자 공간(Userspace)만 재시작하는 메커니즘입니다. 전체 재부팅 대비 시간을 90% 이상 단축할 수 있으며, 커널 업데이트가 아닌 사용자 공간 업데이트를 빠르게 적용하는 데 유용합니다.

항목전체 재부팅soft-rebootkexec
커널 재시작예 (BIOS/UEFI부터)아니오 (커널 유지)예 (BIOS 생략)
사용자 공간전체 재시작전체 재시작전체 재시작
하드웨어 초기화아니오아니오
소요 시간 (일반)30~120초2~10초10~30초
커널 업데이트 적용아니오
서비스 재시작
# soft-reboot 실행
sudo systemctl soft-reboot

# 다른 루트 이미지로 soft-reboot (A/B 업데이트)
sudo systemctl --root=/path/to/new-root soft-reboot

# soft-reboot 후 이전 부팅 로그 확인
journalctl --list-boots
# -1 soft-reboot 이전 세션도 별도 boot ID로 기록됨
활용 사례: IoT/엣지 환경에서 OTA 업데이트 시 soft-reboot으로 사용자 공간만 교체하면 다운타임을 최소화할 수 있습니다. A/B 파티션 스킴과 결합하면 실패 시 빠른 롤백도 가능합니다.
Firmware BIOS/UEFI POST Bootloader GRUB2/systemd-boot Kernel start_kernel() systemd PID 1 init= /sbin/init Generators 실행 (fstab, crypttab) 유닛 파일 로드 + DAG 구성 emergency.target rescue.target multi-user.target graphical.target default.target (symlink) Wants Requires Requires default.target은 symlink로 실제 target 중 하나를 가리킴 (isolate 명령으로 런타임 전환 가능)

서비스 관리 (systemctl)

systemctl은 systemd의 주요 제어 인터페이스(CLI)입니다. 내부적으로 D-Bus를 통해 PID 1에 명령을 전달합니다. 서비스의 시작, 중지, 상태 조회, 활성화/비활성화, 마스킹(Masking) 등 모든 관리 작업을 수행합니다. systemctl 없이 직접 D-Bus 메시지를 보내거나 busctl을 사용해도 동일한 작업이 가능하지만, systemctl이 가장 간편합니다.

팁: .service 확장자는 생략할 수 있습니다. systemctl start nginxsystemctl start nginx.service는 동일합니다. 단, 다른 유닛 타입(socket, timer 등)과 이름이 같을 경우 명시적으로 확장자를 지정해야 합니다.

핵심 명령어

명령어설명예시
systemctl start유닛 즉시 시작systemctl start nginx.service
systemctl stop유닛 즉시 중지systemctl stop nginx.service
systemctl restart중지 후 시작 (PID 변경)systemctl restart nginx.service
systemctl reload설정 재로드 (PID 유지)systemctl reload nginx.service
systemctl enable부팅 시 자동 시작 설정 (심볼릭 링크 생성)systemctl enable nginx.service
systemctl disable부팅 시 자동 시작 해제 (심볼릭 링크 제거)systemctl disable nginx.service
systemctl mask유닛 완전 차단 (/dev/null로 링크)systemctl mask bluetooth.service
systemctl unmask마스킹 해제systemctl unmask bluetooth.service
systemctl status유닛 상태, PID, 로그 조회systemctl status nginx.service
systemctl is-active활성 여부 (스크립트용)systemctl is-active nginx.service
systemctl is-enabled부팅 활성화 여부systemctl is-enabled nginx.service
systemctl list-units로드된 유닛 목록systemctl list-units --type=service
systemctl list-unit-files설치된 유닛 파일 목록systemctl list-unit-files --state=enabled
systemctl daemon-reload유닛 파일 변경 후 재로드systemctl daemon-reload
systemctl show유닛 속성 상세 조회systemctl show -p MainPID nginx.service

Service Type

서비스 유닛의 Type= 설정은 systemd가 서비스 시작 완료를 판단하는 방법을 결정합니다. 잘못된 타입 설정은 서비스가 시작 중에 타임아웃(Timeout)되는 가장 흔한 원인입니다. 예를 들어, fork하여 데몬화하는 서비스에 Type=simple(기본값)을 사용하면, systemd는 부모 프로세스가 종료된 것을 서비스 실패로 판단합니다.

Type 선택 가이드:
  • 서비스가 전경(foreground)에서 실행 → Type=simple 또는 Type=exec
  • 서비스가 fork() 후 부모 종료 → Type=forking + PIDFile=
  • 서비스가 sd_notify(READY=1) 호출 → Type=notify
  • 서비스가 D-Bus 이름 등록 → Type=dbus + BusName=
  • 초기화 스크립트 (한 번 실행 후 종료) → Type=oneshot + RemainAfterExit=yes
Type시작 완료 판단 기준적합한 서비스
simple (기본)ExecStart 프로세스 fork 직후전경(foreground)에서 실행되는 데몬
execExecStart 바이너리 exec() 성공 후simple과 유사, exec 실패 감지 개선
forking부모 프로세스 종료 후 (fork → exit 패턴)전통적 데몬 (PIDFile= 함께 사용)
oneshotExecStart 프로세스 종료 후일회성 초기화 스크립트
dbusD-Bus 버스 이름 획득 후D-Bus 서비스 (BusName= 필수)
notifysd_notify(READY=1) 수신 후준비 상태를 명시적으로 알리는 데몬
idlesimple과 동일 (단, 다른 작업 완료 후 시작)콘솔 출력이 필요한 서비스

Restart 정책

서비스 프로세스가 종료되었을 때 systemd의 재시작 동작을 제어합니다.

Restart 값재시작 조건
no재시작하지 않음 (기본값)
on-success정상 종료(exit code 0) 시에만
on-failure비정상 종료, 시그널, 타임아웃 시
on-abnormal시그널, 타임아웃, watchdog 시
on-watchdogwatchdog 타임아웃 시에만
on-abort시그널에 의한 종료 시에만
always모든 종료 상황에서 재시작
[Service]
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=300
StartLimitBurst=5

위 설정은 서비스가 비정상 종료되면 5초 후 재시작하되, 300초(5분) 안에 5회 이상 재시작 시도가 발생하면 더 이상 재시작하지 않습니다. 재시작 제한에 도달하면 서비스는 failed 상태가 되며, systemctl reset-failed로 재설정할 수 있습니다.

주의: Restart=always를 사용할 때는 반드시 StartLimitIntervalSecStartLimitBurst를 함께 설정하세요. 설정 오류로 서비스가 즉시 종료되는 경우, 제한 없이 재시작을 반복하면 시스템 리소스를 소진할 수 있습니다.

서비스 종료 동작

systemd가 서비스를 중지할 때의 프로세스는 다음과 같습니다.

  1. ExecStop= 명령 실행 (있는 경우)
  2. 메인 프로세스에 KillSignal= 전송 (기본: SIGTERM)
  3. TimeoutStopSec= 대기 (기본: 90초)
  4. 타임아웃 후 FinalKillSignal= 전송 (기본: SIGKILL)
  5. cgroup 내 모든 잔여 프로세스에 SIGKILL 전송 (SendSIGKILL=yes 시)
  6. ExecStopPost= 명령 실행 (있는 경우, 항상 실행)
[Service]
# 정상 종료 시그널 커스터마이징
KillSignal=SIGINT
# 종료 대기 시간 (데이터베이스 등 정리에 시간이 필요한 경우)
TimeoutStopSec=300
# cgroup 내 모든 프로세스에 시그널 전송
KillMode=mixed
# mixed = 메인 PID에 KillSignal, 나머지에 SIGKILL

KillMode 옵션

KillMode동작적합한 서비스
control-group (기본)cgroup 내 모든 프로세스에 시그널 전송대부분의 서비스
mixed메인 PID에 KillSignal, 나머지에 SIGKILL자식 프로세스가 있는 서비스
process메인 PID에만 시그널 전송Apache (자식 프로세스 자체 관리)
none프로세스를 죽이지 않음특수 상황 (LXC 컨테이너 등)

ExecStart 접두사 문자

ExecStart= 등의 명령행에는 특수 접두사 문자를 사용하여 동작을 제어할 수 있습니다.

접두사의미예시
-명령 실패해도 유닛을 실패로 표시하지 않음ExecStartPre=-/usr/bin/cleanup
+전체 권한(root)으로 실행 (User= 무시)ExecStartPre=+/usr/bin/chown ...
!권한 상승 후 실행 (ambient capabilities 유지)ExecStart=!/usr/bin/myapp
!!! 와 유사, 보안 컨텍스트 유지ExecStart=!!/usr/bin/myapp
@argv[0]을 별도 지정 (첫 인자가 실행 파일, 둘째부터 argv)ExecStart=@/usr/bin/python3 myapp
:환경 변수 치환 비활성화ExecStart=:/usr/bin/myapp $LITERAL
[Service]
# 접두사 조합 예제
# '-': 실패해도 계속, '+': root로 실행
ExecStartPre=-+/usr/bin/mkdir -p /var/run/myapp

# 일반 실행 (User=myapp 적용)
ExecStart=/usr/bin/myapp --config /etc/myapp.conf

ExecStart 관련 지시어

ExecStart= 외에도 서비스 생명주기의 각 단계에 명령을 지정할 수 있습니다.

지시어실행 시점용도
ExecStartPre=ExecStart 이전설정 검증, 디렉터리 생성
ExecStart=메인 프로세스 시작서비스 메인 바이너리
ExecStartPost=ExecStart 성공 후시작 후 알림, 상태 기록
ExecReload=reload 요청 시보통 /bin/kill -HUP $MAINPID
ExecStop=정상 종료 요청 시정리(Graceful Shutdown) 스크립트
ExecStopPost=프로세스 종료 후 (항상)임시 파일 정리, 상태 리셋

sd_notify() API

Type=notify 서비스는 sd_notify() 함수로 systemd에 상태를 보고합니다. 이 API는 유닉스 도메인 소켓($NOTIFY_SOCKET)을 통해 동작합니다.

#include <systemd/sd-daemon.h>

int main(void) {
    /* 초기화 작업 수행 */
    initialize_config();
    open_database();
    bind_socket();

    /* systemd에 준비 완료 알림 */
    sd_notify(0, "READY=1");

    /* 메인 루프 */
    while (running) {
        process_request();

        /* watchdog 갱신 (WatchdogSec= 설정 시) */
        sd_notify(0, "WATCHDOG=1");

        /* 상태 메시지 업데이트 (systemctl status에 표시) */
        sd_notifyf(0, "STATUS=Processing request %d", count);
    }

    /* 정지 중 상태 알림 */
    sd_notify(0, "STOPPING=1");
    cleanup();
    return 0;
}
inactive condition ConditionXxx 검사 activating active (running) active (exited) — Type=oneshot deactivating failed reloading ExecReload= start OK ready stop 정상 종료 실패 reload Restart=on-failure (RestartSec 후 재시작) 조건 실패

부팅 성능 분석 실전

부팅 시간이 느린 시스템을 진단하는 구체적인 절차를 살펴봅니다.

# 1단계: 전체 부팅 시간 분해
systemd-analyze
# Startup finished in 2.800s (firmware) + 1.200s (loader) + 3.500s (kernel) + 8.200s (userspace) = 15.700s
# → userspace 8.2초가 가장 큼 — 여기를 최적화

# 2단계: 가장 느린 유닛 식별
systemd-analyze blame | head -10
# 4.521s NetworkManager-wait-online.service
# 1.234s docker.service
# 0.890s systemd-udevd-settle.service
# ...

# 3단계: 임계 경로에 있는지 확인
systemd-analyze critical-chain
# blame 상위 유닛이 critical-chain에 없으면 병렬 실행 중이므로 실제 영향 적음

# 4단계: SVG 시각화로 병렬성 확인
systemd-analyze plot > /tmp/boot-chart.svg
# 브라우저에서 열어 유닛 간 겹침(병렬) 정도 확인

# 5단계: 불필요한 서비스 비활성화
sudo systemctl disable NetworkManager-wait-online.service
# 네트워크 필요 없는 서비스가 After=network-online.target을 쓰지 않으면 안전
팁: systemd-analyze blame의 시간은 해당 유닛 자체의 시작 소요 시간이지, 부팅 전체에 미치는 영향은 아닙니다. 병렬 실행되는 유닛은 시간이 길어도 실제 부팅 지연에 기여하지 않을 수 있습니다. critical-chain이 실제 지연 경로를 보여줍니다.

initrd(initramfs) 단계

최신 systemd는 initramfs 안에서도 실행될 수 있습니다. dracut이나 mkinitcpio가 initramfs에 systemd를 포함하면, 부팅 초기부터 유닛 기반 관리가 시작됩니다.

initramfs 내 Target역할
initrd.targetinitramfs 단계의 기본 타깃
initrd-root-fs.target루트 파일시스템 마운트 완료
initrd-switch-root.target실제 루트로 전환 직전
initrd-root-device.target루트 장치 노드 준비 완료

initramfs에서 systemd가 루트 파일시스템을 마운트하면, switch_root를 통해 실제 루트로 전환하고 PID 1을 다시 exec()합니다. 이때 PID 1의 바이너리는 바뀌지만 PID 번호는 유지됩니다.

커널-systemd 상호작용

systemd는 리눅스 커널이 제공하는 다양한 인터페이스를 적극적으로 활용합니다. 전통적인 init 시스템이 단순히 프로세스를 fork/exec하는 수준이었다면, systemd는 커널의 cgroup, 네임스페이스(Namespace), seccomp, capabilities 등을 네이티브로 통합하여 프로세스 격리(Isolation)와 리소스 제어를 수행합니다. 이 커널-systemd 통합은 단방향이 아니라 양방향입니다. 커널이 uevent, netlink, cgroup 이벤트를 systemd에 전달하고, systemd가 cgroup 파일, /proc/sys, namespace API를 통해 커널에 설정을 적용합니다.

systemd가 의존하는 핵심 커널 기능의 최소 버전 요구사항은 다음과 같습니다.

커널 기능최소 커널 버전systemd 활용
cgroup v24.15+리소스 제어, 프로세스 추적
PSI (Pressure Stall Information)4.20+systemd-oomd
pidfd5.3+프로세스 관리 개선
BPF (cgroup 연동)4.15+IP 필터링, 장치 접근 제어
io_uring5.1+비동기 I/O (향후 활용)
mount_setattr()5.12+마운트 속성 변경 (보안 강화)

cgroup v2 통합

systemd는 cgroup v2 통합 계층(Unified Hierarchy)의 핵심 관리자입니다. 부팅 시 /sys/fs/cgroup에 cgroup2 파일시스템을 마운트하고, 모든 서비스, 스코프, 슬라이스에 개별 cgroup을 할당합니다. cgroup v2에서는 모든 컨트롤러(CPU, memory, IO, pids)가 단일 계층에서 관리되며, 이전 v1의 다중 계층 모델에서 발생하던 불일치 문제가 해결되었습니다.

systemd의 cgroup 관리 흐름은 다음과 같습니다.

  1. 부팅 시/sys/fs/cgroup에 cgroup2 마운트, 루트 cgroup에서 컨트롤러 위임
  2. 유닛 시작 시 — 유닛 타입에 따라 적절한 cgroup 생성 (service → /system.slice/유닛이름)
  3. 프로세스 배치 — fork한 프로세스를 cgroup의 cgroup.procs에 기록
  4. 리소스 제한 적용 — 유닛 파일의 리소스 지시어를 cgroup 컨트롤러 파일에 기록
  5. 유닛 종료 시 — cgroup 내 잔여 프로세스 정리 후 cgroup 삭제
# cgroup v2 통합 계층 확인
mount | grep cgroup2
# cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot)

# 특정 서비스의 cgroup 경로 확인
systemctl show -p ControlGroup nginx.service
# ControlGroup=/system.slice/nginx.service

# cgroup 트리 조회
systemd-cgls

네임스페이스 활용

systemd는 커널 네임스페이스를 서비스 격리에 활용합니다. 유닛 파일의 보안 지시어로 네임스페이스를 선언적으로 설정할 수 있습니다.

네임스페이스systemd 지시어격리 대상
Mount NSProtectSystem=, ProtectHome=, PrivateTmp=파일시스템 뷰
PID NSPrivatePIDs= (실험적)프로세스 ID 공간
Network NSPrivateNetwork=네트워크 인터페이스
User NSPrivateUsers=UID/GID 매핑
UTS NSProtectHostname=호스트명
IPC NSPrivateIPC=IPC 객체

seccomp-bpf 필터링

seccomp(Secure Computing Mode)은 프로세스가 호출할 수 있는 시스템 콜을 제한하는 커널 메커니즘입니다. systemd는 BPF(Berkeley Packet Filter) 프로그램을 통해 서비스별로 허용 시스템 콜 목록을 설정합니다. 이를 통해 서비스가 침해(Compromise)되더라도 공격자가 사용할 수 있는 커널 인터페이스를 크게 제한할 수 있습니다.

SystemCallFilter= 지시어를 통해 서비스가 사용할 수 있는 시스템 콜을 제한합니다. systemd는 미리 정의된 시스템 콜 그룹을 제공합니다.

[Service]
# 기본 시스템 콜만 허용
SystemCallFilter=@system-service
# 네이티브 아키텍처만 허용 (32비트 호환 차단)
SystemCallArchitectures=native
# 차단된 시스템 콜 호출 시 EPERM 반환
SystemCallErrorNumber=EPERM

Linux Capabilities 세분화

전통적인 root/non-root 이분법 대신, 커널은 권한을 약 40개의 세분화된 능력(Capability)으로 분리합니다. 예를 들어 웹 서버가 80번 포트에 바인드하려면 전체 root 권한이 아닌 CAP_NET_BIND_SERVICE 능력만 있으면 됩니다. systemd는 서비스에 필요한 최소한의 능력만 부여하여 최소 권한 원칙(Principle of Least Privilege)을 구현합니다.

주요 Capabilities와 용도를 정리합니다.

Capability권한필요한 서비스 예
CAP_NET_BIND_SERVICE1024 미만 포트 바인드nginx, Apache (80/443)
CAP_NET_RAWRAW 소켓, ICMPping, tcpdump
CAP_NET_ADMIN네트워크 설정 변경networkd, iptables
CAP_SYS_ADMIN다양한 관리 작업 (과도하게 넓음)마운트, cgroup 관리
CAP_DAC_READ_SEARCH파일 읽기 권한 검사 우회백업 서비스
CAP_CHOWN파일 소유자 변경파일 서버
CAP_SETUID/CAP_SETGIDUID/GID 변경사용자 전환이 필요한 서비스
CAP_SYS_PTRACE프로세스 추적(ptrace)디버거, strace
CAP_SYS_TIME시스템 시계 변경NTP 서비스
[Service]
# 모든 능력 제거 후 필요한 것만 추가
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_DAC_READ_SEARCH
AmbientCapabilities=CAP_NET_BIND_SERVICE
# 새로운 권한 획득 방지
NoNewPrivileges=yes

커널 인터페이스 매핑

커널 인터페이스systemd 활용용도
epoll이벤트 루프 핵심소켓, 시그널, 타이머 감시
signalfdSIGCHLD 등 시그널 수신자식 프로세스 종료 감지
timerfd타이머 유닛 구현monotonic/realtime 타이머
inotify경로(Path) 유닛 구현파일/디렉터리 변경 감시
netlinkudevd, networkd커널 이벤트(uevent, route) 수신
/proc/self/mountinfo마운트 유닛 감시마운트 이벤트 추적
/sys/fs/cgroup리소스 제어CPU/메모리/IO 제한
/dev/kmsgjournald 커널 로그 수집커널 메시지 읽기
-.slice (root) system.slice user.slice machine.slice nginx.service postgresql.service sshd.service systemd-journald.service user-1000.slice user@1000.service session-1.scope session-2.scope mycontainer.scope docker-abcdef.scope system.slice: 시스템 서비스 user.slice: 사용자 세션 machine.slice: 컨테이너/VM 모든 프로세스는 반드시 하나의 cgroup(서비스/스코프)에 소속

리소스 위임(Delegation)

컨테이너 런타임(Docker, Podman, Kubernetes)이나 가상 머신 관리자(libvirt)가 cgroup을 직접 관리해야 할 경우, systemd가 해당 서비스에 cgroup 하위 트리를 위임(Delegate)합니다. 위임받은 서비스는 자신의 cgroup 아래에 자유롭게 하위 cgroup을 생성하고 관리할 수 있습니다.

[Service]
# cgroup 하위 트리 위임 활성화
Delegate=yes

# 특정 컨트롤러만 위임 (세밀한 제어)
# Delegate=cpu memory io pids

위임 규칙의 핵심은 단일 작성자 원칙(No-Internal-Process Rule)입니다. 위임된 cgroup에서는 내부 프로세스(Internal Process)와 하위 cgroup이 동시에 존재할 수 없습니다. 즉, 프로세스는 반드시 리프(Leaf) cgroup에 배치해야 합니다.

# Docker에서 cgroup v2 위임 확인
cat /sys/fs/cgroup/system.slice/docker.service/cgroup.controllers
# cpu memory io pids

# Docker가 생성한 하위 cgroup
ls /sys/fs/cgroup/system.slice/docker.service/
# docker-CONTAINER_ID.scope 디렉터리들

커널 명령행(Kernel Command Line) 파라미터

systemd는 커널 부트 파라미터를 통해 PID 1의 동작을 제어할 수 있습니다. GRUB 또는 systemd-boot의 엔트리에서 설정합니다.

파라미터설명
systemd.unified_cgroup_hierarchy=1cgroup v2 전용 모드 강제
systemd.log_level=debugPID 1 로그 레벨 설정
systemd.log_target=journalPID 1 로그 출력 대상
systemd.unit=rescue.target기본 타깃 오버라이드
systemd.mask=unit특정 유닛 부팅 시 마스킹
systemd.wants=unit부팅 시 추가 유닛 요청
systemd.debug_shell=1early-boot 디버그 셸 (/dev/tty9)
rd.systemd.unit=initramfs 내 기본 타깃
systemd.setenv=KEY=VALUEPID 1 환경 변수 설정
systemd.restore_state=0이전 상태 복원 비활성화
# 비상 복구: rescue 모드로 부팅
# GRUB 편집기에서 linux 행 끝에 추가:
# systemd.unit=rescue.target

# 더 최소 환경: emergency 모드 (루트 읽기 전용)
# systemd.unit=emergency.target

# 디버그 셸 활성화 (tty9에 root 셸)
# systemd.debug_shell=1
# Ctrl+Alt+F9로 전환

systemd와 SELinux/AppArmor 통합

systemd는 SELinux와 AppArmor 보안 프레임워크와 네이티브로 통합됩니다.

지시어보안 프레임워크설명
SELinuxContext=SELinux서비스 프로세스의 SELinux 컨텍스트 지정
AppArmorProfile=AppArmor서비스에 적용할 AppArmor 프로파일
SmackProcessLabel=SMACKSMACK 레이블 지정
[Service]
# SELinux: httpd_t 도메인으로 실행
SELinuxContext=system_u:system_r:httpd_t:s0

# AppArmor: nginx 프로파일 적용
AppArmorProfile=nginx

systemd-boot (UEFI 부트로더)

systemd-boot(이전 이름: gummiboot)는 systemd 프로젝트에 포함된 UEFI 부트 매니저(Boot Manager)입니다. GRUB2에 비해 기능은 적지만, UEFI 환경에 특화된 단순하고 빠른 부트로더입니다. Boot Loader Specification(BLS)을 따르는 선언적 설정을 사용합니다.

Boot Loader Specification

BLS는 부트로더와 OS 설치기 간의 표준 인터페이스를 정의합니다. 두 가지 타입의 부트 엔트리를 지원합니다.

타입설명위치
Type #1개별 설정 파일 (.conf)로 커널/initrd/cmdline 지정/loader/entries/*.conf
Type #2 (UKI)통합 커널 이미지(Unified Kernel Image) — 모든 요소를 단일 EFI 바이너리에 포함/EFI/Linux/*.efi

UKI (Unified Kernel Image)

UKI는 커널, initrd, 커널 명령행, 부팅 스플래시, sd-stub을 하나의 PE/COFF 바이너리로 번들링합니다. 이를 통해 Secure Boot 서명을 단일 파일에 적용할 수 있어 보안이 강화됩니다.

# UKI 생성 (ukify 도구 사용)
/usr/lib/systemd/ukify build \
    --linux=/boot/vmlinuz-6.6.0 \
    --initrd=/boot/initramfs-6.6.0.img \
    --cmdline="root=UUID=xxx ro quiet" \
    --os-release=@/etc/os-release \
    --output=/boot/efi/EFI/Linux/linux-6.6.0.efi

# bootctl로 systemd-boot 설치/관리
sudo bootctl install          # ESP에 systemd-boot 설치
sudo bootctl update           # 부트로더 바이너리 업데이트
bootctl list                  # 부트 엔트리 목록
sudo bootctl set-default "linux-6.6.0.conf"  # 기본 엔트리 설정
bootctl status                # 현재 부팅 상태 확인

ESP 레이아웃

/boot/efi/                       # EFI System Partition (ESP)
├── EFI/
│   ├── BOOT/
│   │   └── BOOTX64.EFI          # 기본 부트로더
│   ├── systemd/
│   │   └── systemd-bootx64.efi  # systemd-boot 바이너리
│   └── Linux/
│       ├── linux-6.6.0.efi      # Type #2 UKI
│       └── linux-6.5.0.efi      # 이전 버전 UKI
└── loader/
    ├── loader.conf               # 전역 설정
    └── entries/
        ├── linux-6.6.0.conf      # Type #1 엔트리
        └── linux-6.5.0.conf

Type #1 엔트리 파일

# /boot/efi/loader/entries/linux-6.6.0.conf
title    Linux 6.6.0
linux    /vmlinuz-6.6.0
initrd   /initramfs-6.6.0.img
options  root=UUID=abcd1234-ef56-7890-abcd-ef1234567890 ro quiet splash

loader.conf 전역 설정

# /boot/efi/loader/loader.conf
default      linux-6.6.0.conf
timeout      5
console-mode auto
editor       no
auto-entries yes
auto-firmware yes
beep         no
보안 참고: editor no를 설정하면 부팅 시 커널 명령행을 편집할 수 없습니다. Secure Boot와 함께 사용하면 부팅 과정의 무결성을 강화합니다.

Automatic Boot Assessment

systemd-boot는 부팅 시도 카운터를 통해 부팅 성공 여부를 자동으로 평가합니다. 이 기능은 커널 업데이트 후 부팅 실패 시 자동으로 이전 커널로 롤백하는 데 활용됩니다. 엔트리 파일명에 +N-M 형식(N: 총 시도 횟수, M: 남은 시도 횟수)으로 시도 카운터가 포함되면 활성화됩니다.

부팅이 성공하면 사용자 공간에서 systemd-bless-boot.service가 카운터를 제거하여 "good" 상태로 전환합니다. 모든 시도가 실패하면 해당 엔트리는 "bad"로 표시되어 다음 부팅 시 건너뜁니다.

상태의미파일명 예
good부팅 성공 확인됨linux-6.6.0.conf (카운터 없음)
indeterminate평가 중 (시도 남음)linux-6.6.0+3-2.conf (3회 중 2회 남음)
bad모든 시도 실패linux-6.6.0+3-0.conf (0회 남음)

systemd-boot vs GRUB2 비교

항목systemd-bootGRUB2
펌웨어 지원UEFI 전용BIOS + UEFI
설정 형식INI (BLS 표준)grub.cfg (스크립트)
파일시스템ESP(FAT32)만ext4/Btrfs/XFS/ZFS 등
네트워크 부팅미지원지원 (HTTP/TFTP)
암호화 지원LUKS 비지원 (UKI로 우회)LUKS1 직접 지원
UKI 지원네이티브제한적
Secure Boot네이티브 + shimshim 필요
부트 메뉴최소 (텍스트)커스터마이징 가능 (테마)
Boot Assessment네이티브미지원
코드 크기~120KB수 MB

systemd-stub와 UKI 동작 원리

systemd-stub(sd-stub)은 UKI의 핵심 구성 요소로, 커널을 실행하기 전에 PE 바이너리의 각 섹션에서 커널 이미지, initrd, 명령행 파라미터, OS release 정보를 추출합니다. UEFI 환경에서 EFI Stub 프로토콜을 사용하여 커널에 제어를 전달합니다.

# UKI 내 PE 섹션 확인
objdump -h /boot/efi/EFI/Linux/linux-6.6.0.efi
#  .linux    — 커널 이미지 (vmlinuz)
#  .initrd   — initramfs
#  .cmdline  — 커널 명령행
#  .osrel    — /etc/os-release 내용
#  .splash   — 부팅 스플래시 이미지 (선택)
#  .pcrpkey  — TPM2 PCR 공개 키 (선택)
#  .pcrsig   — PCR 서명 (선택)

# UKI 빌드 시 Secure Boot 서명 포함
/usr/lib/systemd/ukify build \
    --linux=/boot/vmlinuz-6.6.0 \
    --initrd=/boot/initramfs-6.6.0.img \
    --cmdline="root=UUID=xxx ro quiet" \
    --secureboot-private-key=db.key \
    --secureboot-certificate=db.crt \
    --output=/boot/efi/EFI/Linux/linux-6.6.0.efi

# PCR 측정값 사전 계산 (TPM2 기반 디스크 암호 해제에 사용)
/usr/lib/systemd/ukify pcr-calc \
    --linux=/boot/vmlinuz-6.6.0 \
    --initrd=/boot/initramfs-6.6.0.img
UEFI Firmware Boot Services systemd-boot ESP: EFI/systemd/ Type #1 Entry loader/entries/*.conf Type #2 UKI EFI/Linux/*.efi vmlinuz (커널) initrd (initramfs) kernel + initrd + cmdline + os-release + splash (단일 PE 바이너리) Linux Kernel ExitBootServices() → start_kernel() Boot Assessment good: 카운터 없음 indeterminate: +N-M bad: +N-0

systemd-journald

systemd-journald는 시스템 전체의 로그를 수집하고 저장하는 중앙 집중식 로깅 서비스입니다. 전통적인 syslog의 텍스트 기반 로그와 달리, journald는 바이너리 구조화 형식으로 로그를 저장하여 필드 기반 검색, 무결성 검증(Sealing), 효율적인 인덱싱을 제공합니다. journald는 커널 로그(/dev/kmsg), 서비스 stdout/stderr, syslog 소켓(/dev/log), 감사(audit) 로그, sd_journal_send() API 호출 등 다양한 소스의 로그를 통합 수집합니다.

journald의 핵심 설계 철학은 구조화(Structured)인덱싱(Indexed)입니다. 각 로그 항목에 풍부한 메타데이터를 자동 추가하고, 해시 테이블 기반 인덱스를 유지하여 대용량 로그에서도 빠른 필드 검색이 가능합니다. 또한 부팅 세션별 자동 분리, Forward Secure Sealing을 통한 무결성 보장, 자동 로그 회전과 크기 관리를 내장하고 있습니다.

syslog vs journald 비교

항목syslog (rsyslog)journald
저장 형식텍스트 파일바이너리 구조화 저널
메타데이터제한적 (facility/priority)풍부 (PID, UID, cgroup, unit, boot ID 등)
검색grep 기반필드 기반 인덱스 검색
무결성없음Forward Secure Sealing (FSS)
로그 소스syslog() 호출syslog, stdout/stderr, kmsg, audit 통합
회전(Rotation)logrotate 외부 도구내장 크기/시간 기반 관리
부팅 격리없음부팅 ID별 자동 분리

journalctl 필터링

# 유닛별 로그
journalctl -u nginx.service

# 우선순위 필터 (0=emerg ~ 7=debug)
journalctl -p err                      # error 이상만
journalctl -p warning..err             # warning~error 범위

# 시간 범위
journalctl --since "2024-01-01 00:00" --until "2024-01-02 00:00"
journalctl --since "1 hour ago"

# 부팅별 로그
journalctl -b              # 현재 부팅
journalctl -b -1           # 이전 부팅
journalctl --list-boots    # 부팅 목록

# 필드 매칭
journalctl _SYSTEMD_UNIT=sshd.service _PID=1234
journalctl _UID=1000
journalctl SYSLOG_IDENTIFIER=kernel

# 실시간 추적
journalctl -f              # tail -f 방식
journalctl -f -u nginx.service

# 출력 형식
journalctl -o json-pretty  # JSON 형식
journalctl -o verbose       # 모든 필드 표시
journalctl -o short-iso     # ISO 8601 타임스탬프

# 커서(Cursor) 기반 연속 읽기 (로그 수집기에서 활용)
journalctl --after-cursor="s=..." --show-cursor

구조화 필드

journald는 각 로그 항목에 다양한 메타데이터 필드를 자동으로 추가합니다. 필드 이름이 _(밑줄)로 시작하는 것은 커널/journald가 추가하는 신뢰 필드(Trusted Field)로, 프로세스가 위조할 수 없습니다.

필드유형설명
MESSAGE사용자로그 메시지 본문
MESSAGE_ID사용자메시지 타입 식별 UUID
PRIORITY사용자syslog 우선순위 (0~7)
ERRNO사용자관련 errno 값
_PID신뢰프로세스 ID
_UID신뢰사용자 ID
_GID신뢰그룹 ID
_SYSTEMD_UNIT신뢰소속 systemd 유닛
_SYSTEMD_CGROUP신뢰소속 cgroup 경로
_BOOT_ID신뢰부팅 세션 UUID
_TRANSPORT신뢰수신 경로 (stdout, syslog, journal, kernel, audit)
COREDUMP_EXE사용자코어 덤프 발생 실행 파일

메시지 카탈로그 (MESSAGE_ID)

systemd는 MESSAGE_ID 필드를 통해 잘 알려진 로그 메시지를 UUID로 분류합니다. 각 UUID에는 사람이 읽을 수 있는 설명과 해결 방법이 연결된 카탈로그(Catalog) 데이터베이스가 있어, 관리자가 특정 이벤트의 의미를 즉시 파악할 수 있습니다.

# 카탈로그 목록 조회
journalctl --list-catalog

# 특정 MESSAGE_ID로 필터링
journalctl MESSAGE_ID=39f53479d3a045ac8e11786248231fbf

# 카탈로그 설명 포함 출력
journalctl --catalog -p err
MESSAGE_ID (UUID)이벤트설명
39f53479d3a045ac...유닛 시작서비스/소켓 등 유닛이 시작됨
de5b426a63be47a7...유닛 중지유닛이 정상 또는 비정상 종료됨
d3505f78fd094e0f...유닛 실패유닛이 failed 상태로 전환됨
fc2e22bc6ee647b6...코어 덤프프로세스가 코어 덤프를 생성함
b07a249cd024414a...시스템 시작부팅 시 커널 버전과 호스트명 기록
98268866d1d54a26...시스템 종료시스템 셧다운 시작
# 커스텀 카탈로그 파일: /usr/lib/systemd/catalog/myapp.catalog
# 형식: UUID 한 줄 + 빈 줄 + 설명
-- a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
Subject: 데이터베이스 연결 실패
Defined-By: myapp

데이터베이스 서버에 연결할 수 없습니다.
네트워크 상태와 DB 서버 프로세스를 확인하세요.

# 카탈로그 업데이트 (파일 추가 후 실행)
# sudo journalctl --update-catalog

저장 정책

저널 저장 설정은 /etc/systemd/journald.conf에서 관리합니다.

[Journal]
# 저장 위치: persistent(디스크), volatile(메모리), auto(기본)
Storage=persistent

# 디스크 사용량 제한
SystemMaxUse=2G
SystemKeepFree=1G
SystemMaxFileSize=128M

# 속도 제한 (DoS 방지)
RateLimitIntervalSec=30s
RateLimitBurst=10000

# 압축
Compress=yes

# syslog 전달 (rsyslog 공존 시)
ForwardToSyslog=yes
# 저널 디스크 사용량 확인
journalctl --disk-usage
# Archived and active journals take up 1.2G in the file system.

# 수동 정리
sudo journalctl --vacuum-size=1G   # 1GB 이하로 축소
sudo journalctl --vacuum-time=30d  # 30일 이전 삭제
sudo journalctl --vacuum-files=10  # 최대 10개 파일 유지

커스텀 구조화 로그

서비스에서 journald에 구조화된 로그를 보내려면 sd_journal_send() API를 사용합니다. 이렇게 보낸 필드는 journalctl로 정확하게 검색할 수 있습니다.

#include <systemd/sd-journal.h>

/* 구조화 로그 전송 */
sd_journal_send(
    "MESSAGE=User login failed",
    "PRIORITY=4",                     /* warning */
    "MESSAGE_ID=1234abcd5678efgh",    /* 메시지 타입 UUID */
    "USER_NAME=john",                 /* 커스텀 필드 */
    "SOURCE_IP=192.168.1.100",        /* 커스텀 필드 */
    "FAILED_ATTEMPTS=3",              /* 커스텀 필드 */
    NULL);

/* errno 포함 로그 */
sd_journal_send(
    "MESSAGE=Failed to open database",
    "PRIORITY=3",                     /* error */
    "ERRNO=%d", errno,                /* 에러 코드 포함 */
    "DB_HOST=db.example.com",
    NULL);
# 커스텀 필드로 검색
journalctl USER_NAME=john
journalctl SOURCE_IP=192.168.1.100 PRIORITY=4

# Python에서 구조화 로그 전송
# from systemd import journal
# journal.send('User login failed',
#              USER_NAME='john',
#              SOURCE_IP='192.168.1.100')

journald 성능 튜닝

고부하 환경에서 journald의 성능을 최적화하는 설정입니다.

[Journal]
# 압축 임계값 (이보다 큰 메시지만 압축)
CompressThresholdBytes=512

# 속도 제한 완화 (고부하 서비스)
RateLimitIntervalSec=30s
RateLimitBurst=100000

# 동기 쓰기 비활성화 (성능 우선, 데이터 손실 가능)
SyncIntervalSec=5min

# 메모리 매핑 최대 크기
MaxFileSec=1month
kernel (/dev/kmsg) 서비스 stdout/stderr syslog socket (/dev/log) audit (af_unix) sd_journal_send() journald 구조화 + 인덱싱 속도 제한 + 압축 FSS 무결성 봉인 Persistent Journal /var/log/journal/MACHINE-ID/ Volatile Journal /run/log/journal/MACHINE-ID/ Forward to syslog (rsyslog) Forward to console/kmsg/wall journal-remote/upload HTTPS 기반 원격 전송 journalctl (CLI) query 모든 로그 소스가 journald로 통합되어 단일 인터페이스로 조회 가능

원격 전송

분산 환경에서 여러 서버의 로그를 중앙으로 수집하기 위해 systemd는 세 가지 컴포넌트를 제공합니다.

컴포넌트역할프로토콜
systemd-journal-upload로컬 저널을 원격으로 전송 (Push)HTTPS POST
systemd-journal-remote원격 저널 데이터 수신 (Pull/Accept)HTTPS
systemd-journal-gatewaydHTTP/JSON API로 저널 조회 제공HTTP REST
# 원격 서버에서 수신 (systemd-journal-remote)
sudo systemd-journal-remote --listen-https=0.0.0.0:19532 \
  --output=/var/log/journal/remote/

# 클라이언트 설정 (/etc/systemd/journal-upload.conf)
# [Upload]
# URL=https://logserver.example.com:19532

# HTTP API로 원격 저널 조회
curl -H "Accept: application/json" \
  "http://logserver:19531/entries?PRIORITY=3&boot"

Forward Secure Sealing (FSS)

journald는 FSS(Forward Secure Sealing)로 저널 무결성을 보장합니다. 키가 유출되더라도 과거 시점의 봉인을 위조할 수 없는 전방 보안(Forward Security) 속성을 가집니다.

# FSS 키 생성 (한 번만 실행)
sudo journalctl --setup-keys
# 표시되는 검증 키(Verification Key)를 안전한 곳에 보관

# 저널 무결성 검증
journalctl --verify
# PASS: /var/log/journal/.../system.journal

저널 저장 구조

저널 파일은 /var/log/journal/MACHINE-ID/ 디렉터리에 저장됩니다. 각 부팅 세션은 별도의 저널 파일로 분리되며, 파일 내부는 해시 테이블 기반의 인덱스 구조로 필드별 빠른 검색이 가능합니다.

# 저널 파일 목록
ls -la /var/log/journal/$(cat /etc/machine-id)/
# system.journal       ← 현재 부팅의 시스템 저널
# system@000...~000.journal  ← 이전 부팅(회전/보관됨)
# user-1000.journal    ← UID 1000 사용자 저널
# user-1000@000...~000.journal

# 저널 파일 통계
journalctl --header
# File path, File ID, Machine ID, Boot ID, Head/Tail timestamps 등
파일 패턴설명
system.journal현재 부팅의 시스템 로그 (활성)
system@SEQNUM-ID~SEQNUM.journal회전된 이전 시스템 로그
user-UID.journal특정 사용자의 현재 로그 (활성)
user-UID@SEQNUM-ID~SEQNUM.journal회전된 이전 사용자 로그

저널 파일 바이너리 내부 구조

저널 파일(.journal)은 mmap 기반의 바이너리 형식으로, 내부에 여러 유형의 객체(Object)가 연결된 구조입니다. 파일은 고정 크기 헤더로 시작하며, 이후 가변 길이 객체들이 64비트 오프셋으로 상호 참조됩니다. 이 구조 덕분에 별도의 파싱 없이 메모리 매핑만으로 빠른 필드 검색과 순차 읽기가 가능합니다.

객체 유형열거값설명
OBJECT_DATA1단일 FIELD=VALUE 쌍 저장. 해시값과 역참조 엔트리 배열 오프셋 포함
OBJECT_FIELD2필드 이름만 저장 (예: MESSAGE, _PID). 필드 해시 테이블에서 참조
OBJECT_ENTRY3하나의 로그 항목. realtime/monotonic 타임스탬프, boot ID, seqnum, Data Object 오프셋 배열
OBJECT_DATA_HASH_TABLE4Data Object 조회용 해시 테이블. 파일 생성 시 크기 고정
OBJECT_FIELD_HASH_TABLE5Field Object 조회용 해시 테이블. 파일 생성 시 크기 고정
OBJECT_ENTRY_ARRAY6Entry 오프셋의 연결 리스트. 순차 탐색과 이진 검색 지원
OBJECT_TAG7FSS 봉인 태그. 에포크별 HMAC-SHA256 값 저장

모든 객체는 공통 헤더(ObjectHeader)를 가집니다: 객체 유형(1바이트), 플래그(1바이트, 압축 여부 등), 객체 크기(8바이트). 이 뒤에 유형별 고유 데이터가 이어집니다.

File Header 구조

파일 헤더는 저널 파일의 첫 번째 영역으로, 파일 전체의 메타데이터와 주요 객체 위치를 기록합니다.

/* 저널 파일 헤더 주요 필드 (journal-def.h 기반) */
struct Header {
    uint8_t  signature[8];          /* "LPKSHHRH" 매직 시그니처 */
    le32_t   compatible_flags;      /* 호환 플래그 (SEALED 등) */
    le32_t   incompatible_flags;    /* 비호환 플래그 (COMPRESSED_XZ/LZ4/ZSTD) */
    sd_id128_t file_id;             /* 파일 고유 UUID */
    sd_id128_t machine_id;          /* 머신 ID (/etc/machine-id) */
    sd_id128_t boot_id;             /* 부팅 세션 UUID */
    sd_id128_t seqnum_id;           /* 시퀀스 번호 도메인 UUID */
    le64_t   header_size;           /* 헤더 크기 (바이트) */
    le64_t   arena_size;            /* 객체 영역 크기 */
    le64_t   data_hash_table_offset;/* 데이터 해시 테이블 위치 */
    le64_t   data_hash_table_size;  /* 데이터 해시 테이블 크기 */
    le64_t   field_hash_table_offset;/* 필드 해시 테이블 위치 */
    le64_t   field_hash_table_size; /* 필드 해시 테이블 크기 */
    le64_t   tail_object_offset;    /* 마지막 객체 오프셋 */
    le64_t   n_objects;             /* 총 객체 수 */
    le64_t   n_entries;             /* 총 엔트리(로그 항목) 수 */
    le64_t   tail_entry_realtime;   /* 마지막 엔트리의 실시간 타임스탬프 */
    le64_t   entry_array_offset;    /* 메인 Entry Array 시작 오프셋 */
};
# journalctl --header로 실제 헤더 정보 확인
journalctl --header
# File Path: /var/log/journal/abc123.../system.journal
# File ID: 7a8b9c...
# Machine ID: abc123...
# Boot ID: def456...
# Sequential Number ID: 111222...
# State: ONLINE
# Compatible Flags: SEALED
# Incompatible Flags: COMPRESSED-ZSTD KEYED-HASH
# Header size: 272
# Arena size: 8388368
# Data Hash Table Size: 233016
# Field Hash Table Size: 333
# Objects: 12345
# Entries: 6789

객체 간 참조 관계

저널 파일의 핵심은 객체 간의 오프셋 기반 상호 참조입니다. 필드 값을 검색할 때는 Data Hash Table → Data Object → Entry Array 순서로 탐색하고, 시간순 읽기에는 메인 Entry Array → Entry Object → Data Object 순서를 따릅니다.

File Header signature, machine-id, boot-id, offsets Data Hash Table hash(FIELD=VALUE) → offset Field Hash Table hash(FIELD_NAME) → offset Data Object MESSAGE=User login failed Data Object _PID=1234 Data Object PRIORITY=4 Entry Object realtime_ts, monotonic_ts boot_id, seqnum, data_offsets[] Entry Object realtime_ts, monotonic_ts boot_id, seqnum, data_offsets[] Entry Array entry_offsets[], next_array_offset Entry Array (next) 연결 리스트로 확장 entry_array_offset Field Object name: "MESSAGE", "PRIORITY" ... TAG Object (FSS) epoch, HMAC-SHA256 봉인 실선 화살표: 직접 참조 (오프셋) 점선 화살표: 헤더에서의 간접 참조 Entry ↔ Data: 상호 참조 관계

커널 로그 수집 메커니즘

journald는 커널 로그를 /dev/kmsg 캐릭터 디바이스를 통해 수집합니다. 이 디바이스는 커널의 printk 링 버퍼(log_buf)에 대한 사용자 공간 인터페이스로, journald는 이를 read()poll()로 지속적으로 모니터링합니다.

부팅 시 로그 수집 타임라인

  1. 커널 부팅printk() 호출이 커널 링 버퍼(log_buf)에 메시지 축적
  2. initramfs/initrd — 초기 사용자 공간. 커널 메시지는 여전히 링 버퍼에만 존재
  3. systemd PID 1 시작systemd-journald.service가 초기에 실행됨
  4. journald 시작/dev/kmsg를 열어 축적된 모든 커널 메시지를 한 번에 읽음
  5. 정상 운영poll()로 새 커널 메시지를 실시간 수신하며 저널에 기록

/dev/kmsg 메시지 형식

커널이 /dev/kmsg를 통해 전달하는 각 메시지는 다음 형식을 따릅니다.

priority,sequence_number,timestamp_usec,flags;message text
   │         │              │            │
   │         │              │            └─ '-' (일반) 또는 'c' (continuation)
   │         │              └─ 부팅 후 마이크로초 (monotonic)
   │         └─ 전역 시퀀스 번호 (누락 감지용)
   └─ facility*8 + level (syslog 인코딩)
# /dev/kmsg 직접 읽기 (디버깅용)
sudo head -5 /dev/kmsg
# 6,1234,5678901,-;Linux version 6.1.0 ...
# 6,1235,5678950,-;Command line: BOOT_IMAGE=...

# 사용자 공간에서 커널 링 버퍼에 메시지 주입
echo "<4>myapp: warning message" | sudo tee /dev/kmsg

printk 레벨과 journal PRIORITY 매핑

printk 레벨매크로journal PRIORITY의미
0KERN_EMERG0 (emerg)시스템 사용 불가
1KERN_ALERT1 (alert)즉시 조치 필요
2KERN_CRIT2 (crit)치명적 조건
3KERN_ERR3 (err)오류 조건
4KERN_WARNING4 (warning)경고 조건
5KERN_NOTICE5 (notice)정상이지만 중요
6KERN_INFO6 (info)정보 메시지
7KERN_DEBUG7 (debug)디버그 메시지

커널 로그 관련 설정

[Journal]
# /dev/kmsg 읽기 (기본값: yes)
ReadKMsg=yes

# 커널 메시지를 콘솔로도 전달
ForwardToConsole=no

# kmsg에 저널 메시지 쓰기 (컨테이너 환경에서 유용)
ForwardToKMsg=no

# 커널 메시지 최대 로그 레벨 (이 레벨 이하만 수집)
MaxLevelKMsg=notice

audit 서브시스템 연동

리눅스 감사(audit) 프레임워크의 로그도 journald가 수집합니다. 커널 audit 서브시스템은 AF_UNIX 소켓을 통해 auditd에 이벤트를 전달하며, journald는 이 경로를 모니터링하여 _TRANSPORT=audit 필드로 저널에 기록합니다. 이를 통해 journalctl _TRANSPORT=audit로 SELinux 위반, 파일 접근 감사, 사용자 인증 이벤트 등을 통합 조회할 수 있습니다.

# audit 로그만 조회
journalctl _TRANSPORT=audit

# _TRANSPORT 필드별 로그 소스 확인
journalctl -F _TRANSPORT
# stdout     ← 서비스 stdout/stderr
# syslog     ← /dev/log 소켓 (syslog API)
# journal    ← sd_journal_send() API
# kernel     ← /dev/kmsg
# audit      ← audit 서브시스템

접근 제어 및 권한

journald는 시스템 저널과 사용자 저널을 분리하여 관리하며, 각각에 대해 별도의 접근 제어를 적용합니다.

주체시스템 저널사용자 저널설정
root전체 읽기/쓰기전체 읽기-
systemd-journal 그룹읽기 가능읽기 가능usermod -aG systemd-journal USER
adm 그룹읽기 가능읽기 가능레거시 호환 (일부 배포판)
일반 사용자읽기 불가자신의 저널만 읽기-

SplitMode 설정

SplitMode= 설정은 사용자별 저널 파일 분리 방식을 결정합니다.

모드동작사용 시나리오
uid (기본)UID별로 별도의 user-UID.journal 파일 생성일반 서버/데스크톱
login로그인 세션이 있는 사용자만 별도 파일 생성다수의 시스템 사용자가 있는 환경
none모든 로그를 system.journal에 통합임베디드/컨테이너 (디스크 절약)
# 저널 파일별 ACL 확인
getfacl /var/log/journal/$(cat /etc/machine-id)/system.journal
# user::rw-
# group:systemd-journal:r--
# group:adm:r--

# systemd-journal 그룹에 사용자 추가
sudo usermod -aG systemd-journal myuser
# 다시 로그인 후 journalctl 실행 시 시스템 저널 읽기 가능

네임스페이스 저널 (LogNamespace)

systemd v245부터 저널 네임스페이스(Journal Namespace)를 지원합니다. 특정 서비스의 로그를 기본 저널과 완전히 분리하여 별도의 systemd-journald 인스턴스가 관리하게 합니다. 대량 로그를 생성하는 서비스 격리, 멀티테넌트 환경 로그 분리, 컴플라이언스 요구에 따른 로그 분류 등에 활용됩니다.

# 서비스에 네임스페이스 지정 (/etc/systemd/system/myapp.service.d/namespace.conf)
[Service]
LogNamespace=myapp

이 설정을 적용하면 systemd는 자동으로 systemd-journald@myapp.service 인스턴스를 시작합니다. 해당 서비스의 모든 로그는 기본 저널이 아닌 네임스페이스 전용 저널에 기록됩니다.

항목기본 저널네임스페이스 저널 (myapp)
데몬systemd-journald.servicesystemd-journald@myapp.service
저장 경로/var/log/journal/MACHINE-ID//var/log/journal/MACHINE-ID.myapp/
설정 파일/etc/systemd/journald.conf/etc/systemd/journald@myapp.conf
소켓/run/systemd/journal/socket/run/systemd/journal.myapp/socket
조회journalctljournalctl --namespace=myapp
# 네임스페이스 저널 조회
journalctl --namespace=myapp
journalctl --namespace=myapp -f           # 실시간 추적
journalctl --namespace=myapp --disk-usage  # 디스크 사용량

# 네임스페이스별 독립 설정
# /etc/systemd/journald@myapp.conf
# [Journal]
# SystemMaxUse=500M
# RateLimitBurst=50000
# Compress=yes
nginx.service sshd.service kernel (/dev/kmsg) myapp.service LogNamespace=myapp myapp-worker.service LogNamespace=myapp systemd-journald (기본 인스턴스) journald.conf journald@myapp (네임스페이스 인스턴스) journald@myapp.conf 기본 저널 저장소 /var/log/journal/MACHINE-ID/ journalctl (기본 조회) 네임스페이스 저널 저장소 /var/log/journal/MACHINE-ID.myapp/ journalctl --namespace=myapp 네임스페이스 격리 경계

저널 트러블슈팅

journald 운영 중 발생할 수 있는 일반적인 문제와 해결 방법입니다.

증상원인해결
journalctl --verify에서 FAIL 저널 파일 손상 (비정상 종료, 디스크 오류) 손상된 파일 삭제 후 systemctl restart systemd-journald. 기존 로그는 유실되므로 백업 후 진행
"No space left on device" 오류 저널이 디스크 공간 초과 journalctl --vacuum-size=500M로 즉시 축소, SystemMaxUse=로 상한 설정
"Suppressed N messages from ..." 속도 제한(Rate Limiting)에 의한 로그 드롭 RateLimitIntervalSec=RateLimitBurst= 상향. 특정 서비스만 해제: 유닛에 LogRateLimitIntervalSec=0
journald SIGBUS 크래시 mmap된 저널 파일이 외부에서 잘림(truncated) 해당 저널 파일 삭제 후 journald 재시작
커널 메시지가 저널에 없음 ReadKMsg=no 설정 또는 권한 부족 /etc/systemd/journald.conf에서 ReadKMsg=yes 확인, journald 재시작
재부팅 후 이전 로그 없음 Storage=volatile 또는 /var/log/journal/ 디렉터리 미존재 Storage=persistent 설정, mkdir -p /var/log/journal && systemd-tmpfiles --create --prefix=/var/log/journal
journalctl 출력이 비어 있음 사용자에게 읽기 권한 없음 sudo usermod -aG systemd-journal $USER 후 재로그인, 또는 sudo journalctl 사용
# 저널 상태 종합 점검
journalctl --verify                     # 파일 무결성 검사
journalctl --disk-usage                 # 디스크 사용량
systemctl status systemd-journald       # 데몬 상태 확인

# 속도 제한 드롭 확인
journalctl -u systemd-journald | grep -i suppress

# 저널 파일 강제 회전 (새 파일 시작)
sudo journalctl --rotate

# 손상된 저널 복구 절차
sudo systemctl stop systemd-journald
sudo rm /var/log/journal/$(cat /etc/machine-id)/system.journal  # 손상 파일 삭제
sudo systemctl start systemd-journald                           # 새 파일 자동 생성

systemd-udevd

systemd-udevd는 리눅스 커널의 장치 이벤트(uevent)를 수신하여 /dev 디렉터리의 장치 노드를 관리하고, 사용자 정의 규칙에 따라 권한 설정, 심볼릭 링크 생성, 외부 프로그램 실행 등을 수행하는 장치 관리자(Device Manager)입니다. udev는 원래 독립 프로젝트였지만, systemd v183에서 통합되었습니다.

udevd의 핵심 역할을 정리하면 다음과 같습니다.

커널 uevent 메커니즘

커널은 장치가 추가/제거/변경될 때 uevent를 netlink 소켓(NETLINK_KOBJECT_UEVENT)을 통해 사용자 공간으로 전달합니다. udevd는 이 이벤트를 수신하여 규칙(Rules)에 따라 처리합니다.

udev 규칙 문법

규칙 파일은 /etc/udev/rules.d//usr/lib/udev/rules.d/에 위치하며, 파일명의 숫자 접두사(prefix)로 실행 순서가 결정됩니다.

유형설명
KERNEL매칭커널 장치 이름 (예: sd*)
SUBSYSTEM매칭서브시스템 (예: block, net)
ATTR{key}매칭sysfs 속성 값
ACTION매칭이벤트 유형 (add, remove, change)
ENV{key}매칭/할당환경 변수 (== 매칭, = 할당)
SYMLINK할당심볼릭 링크 생성
NAME할당장치 노드 이름 변경 (네트워크 인터페이스)
MODE할당장치 파일 권한
OWNER, GROUP할당소유자/그룹
RUN{program}할당이벤트 발생 시 실행할 프로그램
TAG할당systemd 유닛 연결용 태그

커스텀 규칙 예제

# /etc/udev/rules.d/99-usb-storage.rules
# USB 저장 장치에 대해 고유 심볼릭 링크 생성
ACTION=="add", SUBSYSTEM=="block", KERNEL=="sd[b-z]1", \
  ATTRS{idVendor}=="0781", ATTRS{idProduct}=="5583", \
  SYMLINK+="myusbdisk", MODE="0660", GROUP="storage"

# /etc/udev/rules.d/70-persistent-net.rules
# MAC 주소 기반 네트워크 인터페이스 이름 고정
SUBSYSTEM=="net", ACTION=="add", ATTR{address}=="00:11:22:33:44:55", NAME="lan0"

# /etc/udev/rules.d/99-serial-port.rules
# 시리얼 장치 접근 권한 설정
SUBSYSTEM=="tty", KERNEL=="ttyUSB*", MODE="0666", TAG+="systemd"

udevadm 명령어

# 장치 정보 조회
udevadm info /dev/sda
udevadm info --attribute-walk /dev/sda  # 부모 장치 속성까지 표시

# 이벤트 모니터링 (실시간)
udevadm monitor --property

# 규칙 테스트 (실제 실행하지 않음)
udevadm test /sys/class/block/sda1

# 이벤트 재발생 (규칙 변경 후 적용)
sudo udevadm trigger --subsystem-match=net

# 대기 중인 이벤트 처리 완료 대기
sudo udevadm settle
Kernel kobject_uevent() Netlink KOBJECT_UEVENT udevd Worker Pool Rules Matching Serialization /dev 노드 생성/제거 SYMLINK 생성 (by-id, by-path) MODE/OWNER/GROUP 설정 RUN{program} 실행 ENV{} 설정, TAG 부여 systemd .device 유닛 생성 /etc/udev/rules.d/*.rules 규칙은 숫자 접두사(00-99)로 실행 순서가 결정되며, /etc가 /usr/lib보다 우선

Predictable Network Interface Names

systemd-udevd는 네트워크 인터페이스에 예측 가능한 이름(Predictable Names)을 부여합니다. 전통적인 eth0, wlan0 방식은 하드웨어 검색 순서에 따라 이름이 바뀔 수 있어 안정성이 떨어집니다.

접두사명명 규칙예시설명
en이더넷eno1온보드(Onboard) 인덱스
en이더넷enp3s0PCI 버스 + 슬롯
en이더넷enx001122334455MAC 주소 기반
wl무선 LANwlp2s0PCI 버스 + 슬롯
wwWWANwwp0s29f7u2i3USB 장치 경로
# 현재 네트워크 인터페이스 명명 정보
udevadm info /sys/class/net/enp3s0

# 예측 가능한 이름 비활성화 (전통 방식 복원)
# 커널 명령행에 추가: net.ifnames=0 biosdevname=0

udev 규칙 디버깅

# 1. 장치의 모든 속성 확인
udevadm info --attribute-walk /dev/sdb1

# 2. 규칙 시뮬레이션 (실행하지 않고 테스트)
sudo udevadm test /sys/class/block/sdb1 2>&1 | grep -E "^(RUN|SYMLINK|MODE)"

# 3. 실시간 이벤트 모니터링
sudo udevadm monitor --property --subsystem-match=block

# 4. 규칙 재로드
sudo udevadm control --reload-rules

# 5. 이벤트 재발생
sudo udevadm trigger --subsystem-match=block --action=change

systemd .device 유닛과의 연동

udevd가 장치를 감지하면, systemd는 자동으로 대응하는 .device 유닛을 생성합니다. udev 규칙에서 TAG+="systemd"를 부여하면 장치 연결/해제에 따라 서비스를 자동으로 시작/중지할 수 있습니다.

# /etc/systemd/system/backup-usb.service
[Unit]
Description=USB 백업 드라이브 감지 시 자동 백업
BindsTo=dev-disk-by\x2did-usb\x2dSandisk_Extreme.device
After=dev-disk-by\x2did-usb\x2dSandisk_Extreme.device

[Service]
Type=oneshot
ExecStart=/usr/local/bin/usb-backup.sh

[Install]
WantedBy=dev-disk-by\x2did-usb\x2dSandisk_Extreme.device

systemd-networkd

systemd-networkd는 네트워크 인터페이스 설정을 관리하는 시스템 데몬(Daemon)입니다. NetworkManager가 데스크탑과 노트북 환경에 최적화된 반면, networkd는 서버, 컨테이너, 임베디드 환경에 적합한 가벼운 네트워크 관리자입니다. 서버와 컨테이너 환경에 적합하며, NetworkManager에 비해 가볍고 선언적인 설정 방식을 사용합니다. 커널의 netlink 소켓(NETLINK_ROUTE)을 통해 네트워크 설정을 적용합니다.

설정 파일 형식

파일 타입확장자용도위치
Network.network인터페이스의 IP, 라우팅, DNS 설정/etc/systemd/network/
NetDev.netdev가상 네트워크 장치 생성 (bridge, VLAN, bond 등)/etc/systemd/network/
Link.link물리 인터페이스 속성 (이름, MTU, MAC, Wake-on-LAN)/etc/systemd/network/

설정 예제

# /etc/systemd/network/20-wired.network — 정적 IP 설정
[Match]
Name=eth0

[Network]
Address=192.168.1.100/24
Gateway=192.168.1.1
DNS=8.8.8.8
DNS=8.8.4.4
Domains=example.com

[Route]
Gateway=192.168.1.1
Metric=100
# /etc/systemd/network/20-dhcp.network — DHCP 설정
[Match]
Name=en*

[Network]
DHCP=yes

[DHCPv4]
UseDNS=yes
UseNTP=yes
UseDomains=yes
# /etc/systemd/network/10-bridge.netdev — 브리지 생성
[NetDev]
Name=br0
Kind=bridge

# /etc/systemd/network/11-bind-eth0.network — eth0을 br0에 연결
[Match]
Name=eth0

[Network]
Bridge=br0

# /etc/systemd/network/12-bridge-ip.network — 브리지에 IP 할당
[Match]
Name=br0

[Network]
Address=192.168.1.100/24
Gateway=192.168.1.1

networkctl 명령어

# 인터페이스 목록
networkctl list

# 특정 인터페이스 상태
networkctl status eth0

# 설정 재적용
sudo networkctl reconfigure eth0

# 전체 재로드
sudo networkctl reload

WireGuard VPN 설정

networkd는 WireGuard VPN을 네이티브로 설정할 수 있습니다.

# /etc/systemd/network/30-wireguard.netdev
[NetDev]
Name=wg0
Kind=wireguard

[WireGuard]
PrivateKey=BASE64_PRIVATE_KEY
ListenPort=51820

[WireGuardPeer]
PublicKey=PEER_BASE64_PUBLIC_KEY
AllowedIPs=10.0.0.0/24
Endpoint=vpn.example.com:51820
PersistentKeepalive=25
# /etc/systemd/network/30-wireguard.network
[Match]
Name=wg0

[Network]
Address=10.0.0.2/24

[Route]
Gateway=10.0.0.1
Destination=10.0.0.0/24

VLAN 설정

# /etc/systemd/network/10-vlan100.netdev
[NetDev]
Name=vlan100
Kind=vlan

[VLAN]
Id=100

# /etc/systemd/network/11-eth0-vlan.network
[Match]
Name=eth0

[Network]
VLAN=vlan100

# /etc/systemd/network/12-vlan100.network
[Match]
Name=vlan100

[Network]
Address=192.168.100.10/24

Bond 설정

# /etc/systemd/network/10-bond0.netdev
[NetDev]
Name=bond0
Kind=bond

[Bond]
Mode=802.3ad
TransmitHashPolicy=layer3+4
MIIMonitorSec=100ms
LACPTransmitRate=fast

# /etc/systemd/network/11-eth0-bond.network
[Match]
Name=eth0

[Network]
Bond=bond0

# /etc/systemd/network/11-eth1-bond.network
[Match]
Name=eth1

[Network]
Bond=bond0

# /etc/systemd/network/12-bond0.network
[Match]
Name=bond0

[Network]
Address=10.0.0.10/24
Gateway=10.0.0.1
DNS=8.8.8.8

networkd vs NetworkManager

항목systemd-networkdNetworkManager
설계 목표서버/컨테이너데스크탑/노트북
설정 방식선언적 INI 파일D-Bus API + 다양한 프론트엔드
Wi-Fi 지원제한적 (iwd 연동)wpa_supplicant 통합
VPN 플러그인WireGuard만 네이티브OpenVPN, IPsec 등 다수
Hotplug기본 지원고급 지원 (Dispatcher)
GUI없음 (networkctl CLI)nmtui, nm-applet, GNOME 설정
메모리낮음 (~5MB)높음 (~30MB)
.network 파일 .netdev 파일 .link 파일 networkd 인터페이스 매칭 DHCP 클라이언트 IPv6 RA/DHCPv6 Netlink NETLINK_ROUTE RTM_NEWADDR/LINK Kernel Network Stack routing table / neighbor cache / interface config bridge / VLAN / bond / wireguard modules networkctl (CLI) resolved (DNS 설정 전달) 파일명 숫자 접두사(10-xxx, 20-xxx)로 매칭 우선순위 결정

systemd-resolved

systemd-resolved는 로컬 DNS 스텁 리졸버(Stub Resolver)를 제공하는 서비스입니다. 전통적인 /etc/resolv.conf 기반 DNS 설정을 대체하며, DNS 캐싱, DNSSEC 검증, DNS-over-TLS, 인터페이스별 DNS 설정 등 현대적인 DNS 관리 기능을 제공합니다. 127.0.0.53:53에서 DNS 요청을 수신하고, 설정된 업스트림 DNS 서버로 전달합니다. 인터페이스별(per-link) DNS 설정, DNSSEC 검증, DNS-over-TLS(DoT), Split DNS 정책을 지원합니다.

동작 원리

resolved는 세 가지 모드로 동작할 수 있습니다.

모드/etc/resolv.conf 설정설명
스텁 모드 (권장)nameserver 127.0.0.53resolved를 통한 모든 DNS 질의
정적 모드nameserver 업스트림IPresolved 우회, 직접 업스트림 질의
호환 모드nameserver 127.0.0.54스텁과 유사, 원본 응답 IP 유지

Per-link DNS와 Split DNS

VPN 환경에서 회사 내부 도메인은 VPN DNS로, 나머지는 일반 DNS로 질의하는 Split DNS 정책을 설정할 수 있습니다. 이 기능은 기업 환경에서 매우 중요합니다.

resolved는 각 네트워크 인터페이스에 독립적인 DNS 서버와 검색 도메인(Search Domain)을 설정할 수 있습니다. 도메인 앞에 ~ 접두사를 붙이면 해당 도메인의 DNS 질의가 그 인터페이스의 DNS 서버로 라우팅됩니다.

도메인 설정의미
example.com검색 도메인 (DNS 서핑에 사용)
~example.com라우팅 도메인 (이 도메인의 질의를 해당 인터페이스 DNS로 라우팅)
~.기본 라우팅 (매칭되지 않는 모든 질의를 이 인터페이스로)
# 인터페이스별 DNS 설정
resolvectl dns eth0 8.8.8.8 8.8.4.4
resolvectl dns tun0 10.0.0.53

# 도메인 라우팅 (회사 도메인은 VPN DNS로)
resolvectl domain tun0 ~corp.example.com ~internal.example.com

# 현재 DNS 상태 확인
resolvectl status

# DNS 질의 테스트
resolvectl query www.example.com

# 캐시 통계 및 플러시
resolvectl statistics
resolvectl flush-caches

/etc/resolv.conf 관리 모드

resolved와 /etc/resolv.conf의 관계는 4가지 모드로 구성됩니다. 대부분의 배포판은 스텁 모드를 기본으로 설정합니다.

모드심링크 대상동작
스텁 (권장)/run/systemd/resolve/stub-resolv.conf127.0.0.53 → resolved 통과. 캐시/DNSSEC/DoT 모두 활성
정적/run/systemd/resolve/resolv.conf실제 업스트림 DNS IP 직접 노출. resolved 우회 가능
호환직접 작성127.0.0.54 사용. 스텁과 유사하나 원본 응답 IP 유지
외부 관리없음 (일반 파일)resolved와 무관하게 직접 관리 (NetworkManager 등)
# 현재 모드 확인
ls -la /etc/resolv.conf
# 심링크 → /run/systemd/resolve/stub-resolv.conf (스텁 모드)

# 스텁 모드로 설정 (권장)
sudo ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf

# 정적 모드로 전환 (컨테이너 내부 등)
sudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf

NSS 모듈 순서

glibc의 Name Service Switch 설정(/etc/nsswitch.conf)에서 resolved 관련 모듈의 순서가 중요합니다.

# /etc/nsswitch.conf (권장 설정)
hosts: mymachines resolve [!UNAVAIL=return] files myhostname dns
모듈역할
mymachinessystemd-machined에 등록된 컨테이너/VM 이름 해석
resolvesystemd-resolved D-Bus API로 질의 (캐시/DNSSEC/DoT 활용)
[!UNAVAIL=return]resolve가 사용 불가(서비스 중지)일 때만 다음으로 폴스루
files/etc/hosts 파일 참조
myhostname로컬 호스트명 해석 (localhost, 자신의 hostname)
dns전통적인 /etc/resolv.conf 기반 DNS (폴백)

Split DNS 실전: VPN + 회사 DNS

# 시나리오: eth0 (인터넷) + tun0 (VPN)
# eth0 → 공용 DNS (1.1.1.1)
# tun0 → 회사 DNS (10.0.0.53), 회사 도메인만 라우팅

# 1. 인터페이스별 DNS 설정
resolvectl dns eth0 1.1.1.1 1.0.0.1
resolvectl dns tun0 10.0.0.53

# 2. 도메인 라우팅 (회사 도메인은 tun0 DNS로)
resolvectl domain tun0 ~corp.example.com ~internal.example.com

# 3. eth0을 기본 라우팅으로 (나머지 모든 질의)
resolvectl domain eth0 ~.

# 4. 설정 확인
resolvectl status
# Link 2 (eth0): DNS=1.1.1.1 1.0.0.1, Domain=~.
# Link 3 (tun0): DNS=10.0.0.53, Domain=~corp.example.com ~internal.example.com

# 5. 테스트
resolvectl query gitlab.corp.example.com  # → 10.0.0.53으로 질의
resolvectl query www.google.com           # → 1.1.1.1로 질의

DNSSEC과 DNS-over-TLS

# /etc/systemd/resolved.conf
[Resolve]
# DNS 서버 (DoT용 서버명은 # 뒤에 지정)
DNS=1.1.1.1#cloudflare-dns.com 9.9.9.9#dns.quad9.net
FallbackDNS=8.8.8.8 8.8.4.4

# DNSSEC 설정
# yes: 엄격 (검증 실패 시 SERVFAIL)
# allow-downgrade: 지원하면 검증, 미지원 시 무시
# no: 비활성화
DNSSEC=allow-downgrade

# DNS-over-TLS 설정
# yes: 필수 (TLS 미지원 서버에 질의 불가)
# opportunistic: 가능하면 TLS, 불가능하면 평문
# no: 비활성화
DNSOverTLS=opportunistic

MulticastDNS=yes
LLMNR=yes
Cache=yes
CacheFromLocalhost=no
# DNSSEC 검증 상태 확인
resolvectl query --type=A sigok.verteiltesysteme.net
# sigok.verteiltesysteme.net: ... -- link: eth0 (authenticated)

# DNSSEC 실패 테스트
resolvectl query --type=A sigfail.verteiltesysteme.net
# sigfail.verteiltesysteme.net: resolve call failed: DNSSEC validation failed

# DNS-over-TLS 연결 확인
resolvectl statistics
# Transactions: ... Current/Max TLS sessions: 1/4
Application getaddrinfo() NSS (nss-resolve) Stub (127.0.0.53) resolved 캐시 / DNSSEC 검증 Split DNS 라우팅 DNS-over-TLS per-link DNS 선택 Upstream DNS (UDP/TCP) DNS-over-TLS (853/tcp) mDNS (5353/udp) LLMNR (5355/udp) resolvectl (CLI) ~domain 접두사로 특정 도메인을 해당 인터페이스의 DNS로 라우팅 (Split DNS)

/etc/resolv.conf 관리

resolved를 사용할 때 /etc/resolv.conf는 다양한 방식으로 관리될 수 있습니다. 올바른 설정을 위해 심볼릭 링크 상태를 확인해야 합니다.

# resolv.conf 상태 확인
ls -la /etc/resolv.conf
# 권장: /etc/resolv.conf → ../run/systemd/resolve/stub-resolv.conf

# 스텁 모드 설정 (권장)
sudo ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
# → nameserver 127.0.0.53 (resolved 경유)

# resolved의 실제 업스트림 DNS를 가리키는 파일
cat /run/systemd/resolve/resolv.conf
# → 실제 업스트림 DNS 서버가 나열됨 (resolved 우회 시 사용)

# DNS 캐시 통계
resolvectl statistics
# Current Transactions: 0
# Total Transactions: 12345
# Current Cache Size: 567
# Cache Hits: 8901
# Cache Misses: 3456

mDNS와 LLMNR

resolved는 mDNS(Multicast DNS)LLMNR(Link-Local Multicast Name Resolution)도 지원합니다. mDNS는 .local 도메인에서, LLMNR은 단일 레이블 이름 해석에서 사용됩니다. 이 프로토콜들은 로컬 네트워크에서 DNS 서버 없이 호스트 이름을 해석할 수 있게 합니다.

# /etc/systemd/resolved.conf
[Resolve]
MulticastDNS=yes     # .local 도메인 해석
LLMNR=yes            # 단일 레이블 이름 해석

systemd-logind

systemd-logind는 사용자 로그인 세션(Session), 좌석(Seat), 사용자(User) 계정의 3계층 구조를 관리하는 서비스입니다. 전통적인 utmp/wtmp 기반 세션 추적을 대체하고, polkit 통합을 통한 세밀한 권한 제어를 제공합니다.

logind가 관리하는 핵심 기능은 다음과 같습니다.

세션/좌석/사용자 3계층

개념설명예시
세션(Session)사용자의 개별 로그인 인스턴스SSH 세션, TTY 로그인, 그래픽 세션
좌석(Seat)물리적 접근 지점 (모니터+키보드+마우스)seat0 (기본 로컬 좌석)
사용자(User)UID 기반 사용자 엔티티user-1000

loginctl 명령어

# 활성 세션 목록
loginctl list-sessions

# 세션 상세 정보
loginctl show-session 1

# 사용자 정보
loginctl show-user 1000
loginctl list-users

# 좌석 정보
loginctl list-seats
loginctl show-seat seat0

# 세션 잠금/해제
loginctl lock-session 1
loginctl unlock-session 1

# 사용자 세션 종료 (주의: 해당 사용자의 모든 프로세스 종료)
loginctl terminate-user 1000

polkit 통합과 inhibitor locks

logind는 inhibitor locks를 통해 특정 작업(종료, 절전 등)을 일시적으로 차단할 수 있습니다. 예를 들어 대용량 파일 전송 중 시스템 종료를 방지하는 용도로 사용합니다.

# 현재 inhibitor 목록
systemd-inhibit --list

# 종료/절전 차단하며 명령 실행
systemd-inhibit --what=shutdown:sleep --who="backup" \
  --why="백업 진행 중" --mode=delay \
  /usr/local/bin/backup.sh

inhibitor lock 실전 시나리오

inhibitor에는 두 가지 모드가 있습니다. block은 해당 작업을 완전히 차단하고, delay는 일정 시간(기본 5초) 지연시킵니다.

시나리오차단 대상모드명령 예제
백업 진행 중shutdownblocksystemd-inhibit --what=shutdown --mode=block rsync ...
프레젠테이션 중idle:sleep:handle-lid-switchblock화면 꺼짐/절전/덮개 닫기 방지
CD 굽기shutdown:sleepblock굽기 중 시스템 상태 변경 방지
세션 관리자shutdowndelay종료 전 저장 유도 (5초 유예)
# 프레젠테이션 모드 — 화면 꺼짐/절전/덮개 닫기 모두 방지
systemd-inhibit --what=idle:sleep:handle-lid-switch \
  --who="presentation" --why="프레젠테이션 진행 중" --mode=block \
  sleep infinity &
INHIBIT_PID=$!

# 프레젠테이션 종료 시 해제
kill $INHIBIT_PID

# 프로그래밍 방식 — D-Bus API
# busctl call org.freedesktop.login1 /org/freedesktop/login1 \
#   org.freedesktop.login1.Manager Inhibit ssss \
#   "shutdown:sleep" "myapp" "백업 진행 중" "block"

전원 관리 정책

logind의 전원 관리는 /etc/systemd/logind.conf[Login] 섹션에서 설정합니다. 각 이벤트에 대해 poweroff, reboot, halt, kexec, suspend, hibernate, hybrid-sleep, suspend-then-hibernate, lock, ignore 중 하나를 지정합니다.

# /etc/systemd/logind.conf
[Login]
# 전원 버튼 동작
HandlePowerKey=poweroff
HandleRebootKey=reboot
HandleSuspendKey=suspend
HandleHibernateKey=hibernate

# 노트북 덮개 동작
HandleLidSwitch=suspend              # 배터리 사용 시: 절전
HandleLidSwitchExternalPower=ignore  # 외부 전원 시: 무시 (닫힌 채 사용)
HandleLidSwitchDocked=ignore         # 도킹 스테이션 시: 무시

# 유휴 동작
IdleAction=suspend
IdleActionSec=30min

# 사용자 프로세스 관리
KillUserProcesses=yes      # 로그아웃 시 사용자 프로세스 종료
KillExcludeUsers=root      # root는 제외
UserStopDelaySec=10        # 프로세스 종료 대기 시간

다중 좌석(Multi-Seat) 구성

하나의 물리 머신에 여러 모니터+키보드+마우스 세트를 연결하여 독립적인 사용자 워크스테이션으로 운영할 수 있습니다. logind가 입력/출력 장치를 좌석별로 분리합니다.

# 현재 좌석 목록과 할당된 장치
loginctl list-seats
loginctl seat-status seat0

# 장치를 새 좌석에 할당
loginctl attach seat1 /sys/devices/pci0000:00/0000:00:02.0/drm/card1

# 장치 할당 해제
loginctl flush-devices

XDG 환경 변수

pam_systemd를 통해 세션이 생성되면, logind는 다음 환경 변수를 자동으로 설정합니다.

변수값 예시설명
XDG_SESSION_ID1세션 고유 번호
XDG_SESSION_TYPEtty, x11, wayland세션 유형
XDG_SESSION_CLASSuser, greeter세션 클래스
XDG_SEATseat0할당된 좌석
XDG_VTNR1가상 터미널 번호
XDG_RUNTIME_DIR/run/user/1000사용자 런타임 디렉터리 (tmpfs)

사용자 서비스 (User Service Manager)

systemd는 시스템 레벨(--system) 외에도 사용자 레벨(--user)의 서비스 매니저를 제공합니다. 각 로그인 사용자에 대해 user@UID.service가 실행되며, 사용자는 root 권한 없이 자신만의 서비스를 관리할 수 있습니다.

# 사용자 서비스 상태 조회
systemctl --user status

# 사용자 서비스 시작/활성화
systemctl --user start myapp.service
systemctl --user enable myapp.service

# 사용자 서비스 파일 위치
# ~/.config/systemd/user/myapp.service
# /usr/lib/systemd/user/myapp.service

# 사용자 Linger 활성화 (로그아웃 후에도 서비스 유지)
sudo loginctl enable-linger $USER
# ~/.config/systemd/user/myapp.service
[Unit]
Description=My User Application

[Service]
Type=simple
ExecStart=%h/bin/myapp
Restart=on-failure
Environment=HOME=%h

[Install]
WantedBy=default.target

커널 인터페이스

logind는 다음 커널 인터페이스를 사용합니다.

인터페이스용도
/sys/class/tty/TTY 장치 열거 및 세션 연결
/dev/input/event*입력 장치 (키보드/마우스) 모니터링
VT ioctl가상 터미널(Virtual Terminal) 전환 제어
/sys/power/state절전/최대 절전 상태 전환
evdev전원 키, 덮개 스위치 이벤트 감지
PAM세션 시작/종료 알림 (pam_systemd)

pam_systemd

PAM(Pluggable Authentication Modules) 스택에 pam_systemd가 포함되면, 사용자 로그인 시 logind에 세션이 등록되고 적절한 cgroup(scope)에 프로세스가 배치됩니다. SSH, 콘솔, GDM 등 모든 로그인 경로에서 이 통합이 동작합니다.

# PAM 설정에서 pam_systemd 확인
grep pam_systemd /etc/pam.d/system-login
# -session   optional   pam_systemd.so

# 세션의 cgroup 확인
cat /proc/self/cgroup
# 0::/user.slice/user-1000.slice/session-1.scope

systemd-oomd

systemd-oomd는 커널의 PSI(Pressure Stall Information) 메트릭을 기반으로 메모리 부족 상황을 사전에 감지하고 대응하는 사용자 공간 OOM(Out-Of-Memory) 관리자입니다. v243에서 초기 구현되어 v248에서 안정화되었습니다. 커널 OOM Killer가 이미 심각한 메모리 부족 상태에서 동작하는 것과 달리, oomd는 압력(Pressure)이 임계값에 도달하면 선제적으로 리소스 소비가 큰 cgroup을 종료합니다.

커널 OOM Killer와의 관계

systemd-oomd와 커널 OOM Killer는 서로 다른 계층에서 동작하는 상호 보완적 메커니즘입니다.

구분커널 OOM Killersystemd-oomd
동작 시점alloc 실패 시 (매우 늦음)PSI 압력 임계값 도달 시 (선제적)
동작 계층커널 내부사용자 공간
Kill 대상 선택oom_score 기반 프로세스memory.current 기반 cgroup
설정 방법/proc/PID/oom_score_adj유닛 파일 ManagedOOM* 지시어
로깅dmesgjournald

일반적으로 oomd가 먼저 개입하여 메모리 압력을 해소하고, 그래도 해결되지 않으면 커널 OOM Killer가 최후의 수단으로 동작합니다. 두 메커니즘이 충돌하지 않도록 OOMPolicy= 지시어로 서비스별 정책을 조정할 수 있습니다.

[Service]
# OOM Kill 발생 시 서비스 동작
OOMPolicy=stop       # kill(기본): cgroup kill 시 유닛 실패
                     # stop: 유닛 중지
                     # continue: kill 후에도 유닛 유지

# 커널 OOM score 조정 (-1000 ~ 1000)
OOMScoreAdjust=-500  # 음수 = kill 우선순위 낮음 (보호)

PSI 기반 메모리 관리

PSI(Pressure Stall Information)는 커널 4.20부터 도입된 리소스 압력 측정 인터페이스입니다. 기존의 MemAvailable 기반 판단보다 정확하게 시스템의 리소스 부족 상황을 감지할 수 있습니다. /proc/pressure/memory와 각 cgroup의 memory.pressure 파일을 통해 메모리 지연 비율을 제공합니다.

# 시스템 전체 메모리 압력
cat /proc/pressure/memory
# some avg10=0.50 avg60=0.30 avg300=0.10 total=12345
# full avg10=0.10 avg60=0.05 avg300=0.02 total=6789

# 특정 cgroup의 메모리 압력
cat /sys/fs/cgroup/system.slice/nginx.service/memory.pressure

oomd 설정

# 서비스 유닛에서 oomd 관리 활성화
[Service]
ManagedOOMMemoryPressure=kill
ManagedOOMMemoryPressureLimit=60%
ManagedOOMSwap=kill

# 슬라이스 단위 설정
[Slice]
ManagedOOMMemoryPressure=kill
ManagedOOMMemoryPressureLimit=50%

earlyoom 비교

항목systemd-oomdearlyoom커널 OOM Killer
동작 계층cgroup 단위프로세스 단위프로세스 단위
감지 기준PSI 압력MemAvailable 비율alloc 실패
선제적 대응아니오 (사후 대응)
systemd 통합네이티브독립커널 내장
세분화 제어유닛별 정책전역 설정oom_score_adj

ManagedOOM* 지시어 상세

지시어설명
ManagedOOMMemoryPressure=auto, kill메모리 압력 기반 관리. kill이면 임계값 초과 시 cgroup 종료
ManagedOOMMemoryPressureLimit=백분율 (기본 60%)PSI avg10 임계값. 이 값 이상이면 kill 대상
ManagedOOMSwap=kill시스템 전체 스왑 사용량 기반. 90% 이상 사용 시 개입
ManagedOOMPreference=none, avoid, omitavoid: 가능하면 제외, omit: 절대 대상 아님
주의: ManagedOOMMemoryPressureLimit=의 기본값은 60%(avg10 기준)입니다. 메모리 집약적 워크로드(데이터베이스, 캐시 서버)에서는 이 값이 너무 낮아 불필요한 kill이 발생할 수 있습니다. 워크로드 특성에 따라 70~80%로 조정하세요.
# 데이터베이스 서비스 — kill 보호 + 높은 임계값
[Service]
ManagedOOMMemoryPressure=kill
ManagedOOMMemoryPressureLimit=80%
ManagedOOMPreference=avoid

# 중요 인프라 서비스 — 절대 kill 대상 아님
[Service]
ManagedOOMPreference=omit

# 배치 작업 슬라이스 — 적극적 관리
[Slice]
ManagedOOMMemoryPressure=kill
ManagedOOMMemoryPressureLimit=40%
ManagedOOMSwap=kill

oomd 운영과 트러블슈팅

# oomd 전체 상태 덤프
oomctl
# Monitored cgroups, swap usage, memory pressure 등 출력

# systemd-oomd에 의한 kill 이벤트 추적
journalctl -u systemd-oomd.service --since today
# "Killed /system.slice/myapp.service due to memory pressure"

# 실시간 모니터링
journalctl -u systemd-oomd.service -f

# oomd 디버그 로깅 활성화
sudo mkdir -p /etc/systemd/system/systemd-oomd.service.d
cat <<EOF | sudo tee /etc/systemd/system/systemd-oomd.service.d/debug.conf
[Service]
Environment=SYSTEMD_LOG_LEVEL=debug
EOF
sudo systemctl daemon-reload
sudo systemctl restart systemd-oomd.service

과도한 kill 발생 시: ManagedOOMMemoryPressureLimit을 높이거나, 특정 서비스에 ManagedOOMPreference=avoid를 설정합니다. kill이 부족할 때: 임계값을 낮추거나, 모니터링 대상 슬라이스에 ManagedOOMMemoryPressure=kill이 설정되어 있는지 확인합니다.

PSI Metrics memory.pressure avg10 / avg60 / avg300 (cgroup v2 인터페이스) systemd-oomd 주기적 폴링 임계값 비교 대상 cgroup 선택 압력 < 임계값 → 대기 압력 >= 임계값 cgroup SIGKILL memory.oom.group=1 Kill 대상 선택 기준 1. ManagedOOMPreference=avoid 는 제외 2. memory.current 가 가장 큰 cgroup 선택 3. ManagedOOMPreference=omit 은 절대 대상 아님

systemd-tmpfiles

systemd-tmpfiles는 임시 파일과 디렉터리의 생성, 정리, 삭제를 선언적으로 관리합니다. 리눅스 시스템에서 /run, /tmp, /var/tmp 등의 디렉터리는 부팅 시 비어 있거나 정기적으로 정리해야 하며, 서비스가 필요로 하는 런타임 디렉터리도 미리 생성해야 합니다. tmpfiles는 이러한 작업을 선언적 설정 파일로 자동화합니다. 부팅 시 systemd-tmpfiles-setup.service가 실행되어 필요한 디렉터리를 생성하고, 타이머(systemd-tmpfiles-clean.timer)가 주기적으로 오래된 파일을 정리합니다.

설정 형식

각 행은 Type Path Mode User Group Age Argument 형식으로 구성됩니다.

타입설명예시
d디렉터리 생성 (없으면 생성)d /run/myapp 0755 myapp myapp -
D디렉터리 생성 + 내용 삭제D /tmp/cache 1777 root root 7d
f파일 생성 (없으면 생성)f /run/myapp/pid 0644 myapp myapp -
F파일 생성/덮어쓰기 + 내용 기록F /sys/module/snd/parameters/... 0644 - - - Y
L심볼릭 링크 생성L /etc/localtime - - - - /usr/share/zoneinfo/Asia/Seoul
r파일/디렉터리 제거r /tmp/myapp-*
R재귀적 제거R /var/tmp/myapp
z권한/소유자 조정 (파일이 있을 때만)z /dev/kvm 0660 root kvm -
Z재귀적 권한 조정Z /var/lib/myapp 0750 myapp myapp -
w파일에 값 쓰기w /proc/sys/vm/swappiness - - - - 10
# /etc/tmpfiles.d/myapp.conf
# 런타임 디렉터리 생성
d /run/myapp 0755 myapp myapp -
# 캐시 디렉터리 (7일 이상 된 파일 자동 삭제)
D /var/cache/myapp 0755 myapp myapp 7d
# 로그 디렉터리
d /var/log/myapp 0750 myapp adm -
# PID 파일 위치
f /run/myapp/myapp.pid 0644 myapp myapp -
# 설정 적용 (디렉터리/파일 생성)
sudo systemd-tmpfiles --create /etc/tmpfiles.d/myapp.conf

# 오래된 파일 정리
sudo systemd-tmpfiles --clean

# 대상 제거
sudo systemd-tmpfiles --remove /etc/tmpfiles.d/myapp.conf

실행 타이밍: setup vs clean

tmpfiles는 두 가지 서로 다른 시점에 동작하며, 각각 다른 서비스에 의해 실행됩니다.

서비스실행 시점동작주요 옵션
systemd-tmpfiles-setup.service부팅 초기 (local-fs.target 이후)디렉터리/파일 생성, 권한 설정--create --prefix=/
systemd-tmpfiles-setup-dev-early.service부팅 매우 초기 (udev 이전)/dev 내 필수 노드 생성--create --prefix=/dev
systemd-tmpfiles-setup-dev.serviceudev 초기화 직후/dev 추가 설정--create --prefix=/dev
systemd-tmpfiles-clean.timer부팅 15분 후 + 매일Age 기반 파일 정리--clean
참고: --create는 규칙에 따라 파일/디렉터리를 생성하고 권한을 설정합니다. --clean은 Age 필드에 지정된 기간보다 오래된 파일만 삭제합니다. 두 동작은 독립적이며, Age 필드가 -인 항목은 정리 대상에서 제외됩니다.

실전: 부팅 시 /tmp 정리 정책

기본적으로 systemd는 systemd-tmpfiles-clean.timer에 의해 매일 /tmp의 오래된 파일을 정리합니다. 이 정책은 /usr/lib/tmpfiles.d/tmp.conf에 정의되어 있으며, 필요에 따라 오버라이드할 수 있습니다.

# 기본 정리 정책 확인
cat /usr/lib/tmpfiles.d/tmp.conf
# q /tmp 1777 root root 10d
# q /var/tmp 1777 root root 30d

# 정리 주기 확인
systemctl cat systemd-tmpfiles-clean.timer
# OnBootSec=15min (부팅 15분 후 첫 실행)
# OnUnitActiveSec=1d (이후 매일)

# 커스텀 정리 정책 (3일로 변경)
# /etc/tmpfiles.d/tmp.conf 생성 (동일 경로명이면 오버라이드)
# q /tmp 1777 root root 3d
# q /var/tmp 1777 root root 7d

와일드카드와 글로빙

경로 필드에서 셸 글로브 패턴을 사용하여 여러 파일을 한 번에 처리할 수 있습니다.

# /etc/tmpfiles.d/cleanup-logs.conf
# 30일 이상 된 로그 파일 삭제
r /var/log/myapp/*.log.gz - - - 30d

# /tmp 내 특정 패턴의 임시 파일 7일 후 정리
r /tmp/myapp-*.tmp - - - 7d

# 모든 사용자의 캐시 디렉터리 정리 (14일)
R /home/*/.cache/myapp - - - 14d

보안: SELinux 레이블 복원 (z/Z 타입)

z(단일 파일)와 Z(재귀) 타입은 파일 권한/소유자 뿐만 아니라 SELinux 보안 컨텍스트도 복원합니다. SELinux가 활성화된 시스템에서 파일의 레이블이 잘못된 경우 유용합니다.

# /etc/tmpfiles.d/selinux-fix.conf
# /var/lib/myapp의 SELinux 컨텍스트 재귀 복원
Z /var/lib/myapp 0750 myapp myapp -

# /dev/kvm에 올바른 그룹과 권한 설정
z /dev/kvm 0660 root kvm -

다중 파일 충돌 해결

같은 경로에 대해 여러 tmpfiles.d 파일에서 규칙이 정의된 경우, /etc/의 규칙이 /usr/lib/의 같은 이름 파일을 오버라이드합니다. 같은 디렉터리 내에서는 파일명 알파벳순으로 마지막 규칙이 우선합니다.

# 어떤 규칙이 적용되는지 확인 (드라이런)
systemd-tmpfiles --create --dry-run 2>&1 | head -20

# 특정 경로에 대한 규칙 추적
grep -r "/run/myapp" /etc/tmpfiles.d/ /usr/lib/tmpfiles.d/ /run/tmpfiles.d/ 2>/dev/null

# 배포판 규칙 무효화: 같은 이름의 빈 파일 생성
sudo touch /etc/tmpfiles.d/unwanted-package.conf

tmpfiles과 StateDirectory의 관계

유닛 파일의 StateDirectory=, CacheDirectory=, LogsDirectory= 지시어는 내부적으로 tmpfiles.d 규칙과 유사하게 동작합니다. 차이점은 유닛 지시어가 서비스 생명주기와 연동되어 DynamicUser와 함께 소유권이 자동 관리된다는 것입니다.

유닛 지시어실제 경로DynamicUser 시 경로
StateDirectory=myapp/var/lib/myapp/var/lib/private/myapp (심볼릭 링크)
CacheDirectory=myapp/var/cache/myapp/var/cache/private/myapp
LogsDirectory=myapp/var/log/myapp/var/log/private/myapp
RuntimeDirectory=myapp/run/myapp/run/myapp
ConfigurationDirectory=myapp/etc/myapp/etc/myapp (읽기 전용)

systemd-sysctl

systemd-sysctl은 부팅 초기에 /proc/sys/를 통해 커널 파라미터를 설정하는 서비스입니다. 리눅스 커널은 수천 개의 런타임 튜닝 파라미터를 /proc/sys/ 가상 파일시스템을 통해 노출합니다. 네트워크 스택 최적화, 메모리 정책, 보안 설정 등을 부팅 시 자동으로 적용할 수 있습니다. systemd-sysctl.servicesysinit.target 이전에 실행되어, 네트워크 스택이나 메모리 관리 등의 커널 튜닝을 적용합니다.

설정 파일

설정 파일은 INI 형식이 아닌 key = value 형식을 사용합니다. 검색 경로의 우선순위는 다른 systemd 설정과 동일합니다.

우선순위경로용도
1 (최고)/etc/sysctl.d/*.conf관리자 설정
2/run/sysctl.d/*.conf런타임 생성
3 (최저)/usr/lib/sysctl.d/*.conf배포판 기본값
# /etc/sysctl.d/99-custom.conf
# 네트워크 튜닝
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1

# 메모리 튜닝
vm.swappiness = 10
vm.dirty_ratio = 20
vm.dirty_background_ratio = 5

# 보안
kernel.dmesg_restrict = 1
kernel.kptr_restrict = 2
net.ipv4.conf.all.rp_filter = 1
# 설정 즉시 적용
sudo sysctl --system

# 개별 파라미터 적용
sudo sysctl -w net.ipv4.ip_forward=1

# 현재 값 확인
sysctl net.ipv4.ip_forward

커널 파라미터 카테고리

주요 커널 파라미터를 카테고리별로 정리합니다.

카테고리경로주요 파라미터
네트워크net.core.*somaxconn, rmem_max, wmem_max, netdev_max_backlog
TCP 튜닝net.ipv4.tcp_*max_syn_backlog, tw_reuse, keepalive_time, fin_timeout
IP 포워딩net.ipv4.*ip_forward, conf.all.rp_filter
메모리vm.*swappiness, dirty_ratio, overcommit_memory, min_free_kbytes
파일시스템fs.*file-max, nr_open, inotify.max_user_watches
보안kernel.*dmesg_restrict, kptr_restrict, randomize_va_space

부팅 타이밍과 실행 시점

systemd-sysctl.service는 부팅 초기 sysinit.target 직전에 실행됩니다. 이 시점에서는 네트워크 인터페이스가 아직 올라오지 않은 상태이므로, 네트워크 인터페이스별 sysctl 설정은 적용 시점에 주의해야 합니다.

서비스실행 시점적용 대상
systemd-sysctl.servicesysinit.target 직전 (매우 이른 시점)글로벌 커널 파라미터 (vm.*, kernel.*, fs.*)
systemd-networkdnetwork-pre.target 이후인터페이스별 sysctl (net.ipv4.conf.eth0.* 등)
sysctl --system수동 실행 시모든 .conf 파일 재적용
주의: net.ipv4.conf.eth0.rp_filter처럼 특정 인터페이스에 종속된 파라미터는 해당 인터페이스가 존재하기 전에 적용하면 무시됩니다. 이런 파라미터는 net.ipv4.conf.all.*이나 net.ipv4.conf.default.*를 대신 사용하거나, networkd의 [Network] 섹션에서 설정하세요.

sysctl.d와 sysctl.conf의 관계

전통적인 /etc/sysctl.conf는 procps-ng 패키지의 sysctl 명령이 읽는 파일이고, /etc/sysctl.d/는 systemd-sysctl이 읽는 디렉터리입니다. 두 시스템이 공존하면 적용 순서가 혼란스러울 수 있습니다.

도구설정 파일실행 시점우선순위
systemd-sysctlsysctl.d/*.conf부팅 시 자동/etc/ > /run/ > /usr/lib/
procps sysctl/etc/sysctl.conf수동 또는 init 스크립트단일 파일

충돌을 피하기 위해 sysctl.d/ 방식만 사용하는 것이 권장됩니다. 파일명의 숫자 접두사로 적용 순서가 결정되며, 같은 파라미터가 여러 파일에 있으면 마지막에 읽힌 값이 적용됩니다.

# 모든 sysctl.d 설정의 적용 순서와 최종 값 확인
sudo sysctl --system 2>&1 | grep "Applying"
# * Applying /usr/lib/sysctl.d/50-default.conf ...
# * Applying /etc/sysctl.d/99-custom.conf ...

# 특정 파라미터의 실제 출처 확인
grep -r "vm.swappiness" /etc/sysctl.d/ /usr/lib/sysctl.d/ /run/sysctl.d/ 2>/dev/null

마이그레이션: sysctl.conf에서 sysctl.d로

기존 /etc/sysctl.conf의 설정을 systemd 방식으로 마이그레이션하는 절차입니다.

# 1. 기존 sysctl.conf 내용을 sysctl.d로 복사
sudo cp /etc/sysctl.conf /etc/sysctl.d/99-sysctl.conf

# 2. 원본 파일을 빈 파일로 (주석만 남김)
echo "# Migrated to /etc/sysctl.d/99-sysctl.conf" | sudo tee /etc/sysctl.conf

# 3. 카테고리별로 분리 (선택사항, 관리 편의)
# /etc/sysctl.d/50-network.conf  — 네트워크 파라미터
# /etc/sysctl.d/50-security.conf — 보안 파라미터
# /etc/sysctl.d/50-memory.conf   — 메모리 파라미터

# 4. 적용 및 검증
sudo systemd-sysctl --no-pager
sysctl net.ipv4.ip_forward  # 값 확인

드롭인 충돌 해결

여러 패키지가 같은 파라미터를 서로 다른 값으로 설정할 때 충돌이 발생합니다. 해결 원칙은 다음과 같습니다.

# 충돌 파라미터 추적
for f in /usr/lib/sysctl.d/*.conf /run/sysctl.d/*.conf /etc/sysctl.d/*.conf; do
  [ -f "$f" ] && echo "=== $f ===" && grep -v '^#\|^$' "$f"
done | grep -A0 "vm.swappiness"

# 최종 적용 값 확인
sysctl vm.swappiness

실전 예제: 서버 환경 최적화

# /etc/sysctl.d/90-server-tuning.conf
# === 네트워크 성능 ===
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 65535
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_keepalive_probes = 5
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216

# === 보안 강화 ===
kernel.dmesg_restrict = 1
kernel.kptr_restrict = 2
kernel.yama.ptrace_scope = 1
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0

# === 메모리 관리 ===
vm.swappiness = 10
vm.dirty_ratio = 15
vm.dirty_background_ratio = 3
vm.min_free_kbytes = 65536
vm.overcommit_memory = 0

리소스 제어

systemd는 cgroup v2 컨트롤러를 통해 서비스별 리소스(CPU, 메모리, I/O, 태스크 수)를 선언적으로 제어합니다. 유닛 파일의 [Service] 또는 [Slice] 섹션에 리소스 제한을 설정하면, systemd가 해당 cgroup 파일에 값을 기록합니다. 이 방식의 장점은 서비스 개발자가 cgroup 파일시스템의 세부 구조를 알 필요 없이, 친숙한 INI 형식으로 리소스를 제어할 수 있다는 것입니다.

단위 표기: 메모리는 K, M, G, T(1024 기반) 또는 KB, MB, GB, TB(1000 기반)로, 시간은 s, min, h, d로, 대역폭은 K, M, G(바이트/초)로 표기합니다. 퍼센트 값은 %를 사용합니다 (예: CPUQuota=200% = 2코어 분량).

메모리 제어

지시어cgroup 파일설명
MemoryMin=memory.min절대 회수 불가 보장 (하드 보호)
MemoryLow=memory.low가능한 회수 안 함 (소프트 보호)
MemoryHigh=memory.high초과 시 회수 압력 적용 (소프트 제한, 스로틀링)
MemoryMax=memory.max절대 초과 불가 (하드 제한, OOM Kill 유발)
MemorySwapMax=memory.swap.max스왑 사용량 상한

CPU 제어

지시어cgroup 파일설명
CPUWeight=cpu.weight상대적 CPU 비중 (1~10000, 기본 100)
CPUQuota=cpu.maxCPU 시간 상한 비율 (예: 200% = 2코어)
AllowedCPUs=cpuset.cpus사용 가능한 CPU 코어 지정
AllowedMemoryNodes=cpuset.mems사용 가능한 NUMA 노드 지정

I/O 제어

지시어cgroup 파일설명
IOWeight=io.weight상대적 I/O 비중 (1~10000, 기본 100)
IODeviceWeight=io.weight장치별 I/O 비중
IOReadBandwidthMax=io.max읽기 대역폭 상한
IOWriteBandwidthMax=io.max쓰기 대역폭 상한
IOReadIOPSMax=io.max읽기 IOPS 상한
IOWriteIOPSMax=io.max쓰기 IOPS 상한

TasksMax와 기타

[Service]
# 최대 태스크(스레드+프로세스) 수
TasksMax=512

# 리소스 제한 종합 예제
MemoryMax=2G
MemoryHigh=1.5G
CPUQuota=150%
CPUWeight=200
IOWeight=200
IOReadBandwidthMax=/dev/sda 100M
IOWriteBandwidthMax=/dev/sda 50M

Slice 계층 구성

Slice를 통해 서비스 그룹에 계층적 리소스 제한을 적용할 수 있습니다.

# /etc/systemd/system/web.slice
[Slice]
Description=Web Services Slice
MemoryMax=8G
CPUQuota=400%

# 서비스에서 슬라이스 지정
# /etc/systemd/system/nginx.service.d/override.conf
[Service]
Slice=web.slice
# 리소스 사용량 실시간 모니터링
systemd-cgtop

# 일회성 명령에 리소스 제한 적용
systemd-run --scope -p MemoryMax=500M -p CPUQuota=50% stress-ng --vm 2 --vm-bytes 256M

# cgroup 트리 구조 조회
systemd-cgls
CPU CPUWeight= → cpu.weight CPUQuota= → cpu.max AllowedCPUs= → cpuset.cpus Memory MemoryMin= → memory.min MemoryLow= → memory.low MemoryHigh= → memory.high MemoryMax= → memory.max IO IOWeight= → io.weight IOReadBandwidthMax= → io.max IOWriteBandwidthMax= → io.max systemd Unit File [Service] / [Slice] 섹션 cgroup v2 filesystem /sys/fs/cgroup/{slice}/{service}/ write TasksMax= → pids.max systemd 유닛 지시어는 cgroup v2 컨트롤러 파일에 직접 매핑됨

Transient 리소스 제한

systemd-run을 사용하면 기존 프로세스나 일회성 명령에 즉시 cgroup 리소스 제한을 적용할 수 있습니다.

# 일회성 명령에 메모리 제한 적용
systemd-run --scope -p MemoryMax=1G -p MemoryHigh=800M \
  make -j$(nproc)

# 기존 PID를 스코프에 배치
systemd-run --scope --pid=12345 -p CPUQuota=50%

# 슬라이스 내에서 실행
systemd-run --scope --slice=batch.slice \
  -p IOWeight=10 -p Nice=19 \
  /usr/bin/heavy-batch-job

런타임 속성 변경

실행 중인 서비스의 리소스 제한을 재시작 없이 변경할 수 있습니다.

# 메모리 제한 즉시 변경
sudo systemctl set-property nginx.service MemoryMax=4G

# CPU 제한 즉시 변경
sudo systemctl set-property nginx.service CPUQuota=300%

# 임시 변경 (재부팅 후 원복)
sudo systemctl set-property --runtime nginx.service MemoryMax=8G

# 현재 적용된 값 확인
systemctl show nginx.service -p MemoryMax -p CPUQuota -p IOWeight

리소스 사용량 모니터링

# systemd-cgtop: top 방식 실시간 모니터링
systemd-cgtop -m             # 메모리 기준 정렬
systemd-cgtop -c             # CPU 기준 정렬
systemd-cgtop -i 500ms       # 갱신 간격 500ms

# 특정 서비스의 상세 리소스 사용량
systemctl show nginx.service \
  -p MemoryCurrent -p MemoryPeak -p CPUUsageNSec -p TasksCurrent \
  -p IOReadBytes -p IOWriteBytes

# cgroup 파일에서 직접 읽기
cat /sys/fs/cgroup/system.slice/nginx.service/memory.current
cat /sys/fs/cgroup/system.slice/nginx.service/cpu.stat
cat /sys/fs/cgroup/system.slice/nginx.service/io.stat
메모리 계층: MemoryLow < MemoryHigh < MemoryMax 순서로 설정합니다. MemoryLow는 시스템 전체가 메모리 부족일 때도 해당 서비스의 메모리를 보호합니다. MemoryHigh를 초과하면 커널이 적극적으로 메모리를 회수하지만 프로세스를 죽이지는 않습니다. MemoryMax를 초과하면 OOM Kill이 발생합니다.

타이머 유닛

systemd 타이머(Timer) 유닛은 전통적인 cron을 대체하는 시간 기반 서비스 활성화 메커니즘입니다. 모든 cron 작업은 systemd 타이머로 대체할 수 있으며, 추가적으로 journald 통합 로깅, cgroup 리소스 제어, 의존성 관리, 보안 강화 등의 이점이 있습니다. 타이머 유닛은 연결된 서비스 유닛을 지정된 시간이나 일정에 따라 실행합니다. cron 대비 journald 통합 로깅, 리소스 제어, 의존성 관리 등의 장점이 있습니다.

Monotonic 타이머

시스템 부팅 또는 유닛 활성화 시점 기준의 상대적 시간으로 동작합니다.

지시어기준 시점설명
OnBootSec=시스템 부팅부팅 후 지정 시간 경과 시 실행
OnStartupSec=systemd 시작systemd(PID 1) 시작 후 경과 시
OnUnitActiveSec=연결 유닛 마지막 활성화주기적 실행에 적합
OnUnitInactiveSec=연결 유닛 마지막 비활성화작업 완료 후 대기 시간
OnActiveSec=타이머 자체 활성화타이머 시작 후 경과 시

Realtime 타이머 (OnCalendar)

OnCalendar=는 cron과 유사한 캘린더(Calendar) 표현식을 사용합니다. 형식은 Dow Year-Month-Day Hour:Minute:Second입니다.

표현식의미cron 대응
*-*-* *:00:00매시간 정각0 * * * *
*-*-* *:0/15매 15분*/15 * * * *
*-*-* 00:00:00매일 자정0 0 * * *
Mon..Fri *-*-* 09:00:00평일 오전 9시0 9 * * 1-5
*-*-01 00:00:00매월 1일 자정0 0 1 * *
*-01,07-01 00:00:001월, 7월 1일0 0 1 1,7 *
Sat *-*-1..7 00:00:00매월 첫 번째 토요일(복잡한 스크립트 필요)
*-*-* 02:00:00 UTCUTC 기준 새벽 2시(타임존 설정 필요)
# 캘린더 표현식 검증 (다음 실행 시점 확인)
systemd-analyze calendar "Mon..Fri *-*-* 09:00:00"
#   Original form: Mon..Fri *-*-* 09:00:00
#  Normalized form: Mon..Fri *-*-* 09:00:00
#    Next elapse: Mon 2026-03-23 09:00:00 KST

# 반복 간격 테스트 (다음 N회 실행 시점)
systemd-analyze calendar --iterations=5 "*-*-* *:0/15"

# 자주 사용하는 사전 정의 표현식
systemd-analyze calendar daily         # *-*-* 00:00:00
systemd-analyze calendar weekly        # Mon *-*-* 00:00:00
systemd-analyze calendar monthly       # *-*-01 00:00:00
systemd-analyze calendar hourly        # *-*-* *:00:00
systemd-analyze calendar minutely      # *-*-* *:*:00
systemd-analyze calendar quarterly     # *-01,04,07,10-01 00:00:00
systemd-analyze calendar semiannually  # *-01,07-01 00:00:00

Persistent와 보상 실행

Persistent=true를 설정하면, 시스템이 꺼져 있던 동안 놓친 실행을 부팅 직후에 보상 실행합니다. 이 정보는 /var/lib/systemd/timers/에 타임스탬프 파일로 저장됩니다.

# Persistent 타임스탬프 확인
ls -la /var/lib/systemd/timers/
# stamp-backup.timer — 마지막 실행 시각 기록

# 타이머 다음 실행 시점 확인
systemctl list-timers backup.timer
# NEXT                        LEFT     LAST                        PASSED    UNIT
# Mon 2026-03-24 00:12:34 KST 5h left  Sun 2026-03-23 00:05:12 KST 18h ago   backup.timer

RandomizedDelaySec와 부하 분산

RandomizedDelaySec=는 지정된 범위 내에서 무작위 지연을 추가합니다. 동일한 타이머를 사용하는 수천 대의 서버가 같은 시각에 동시에 작업(예: 패키지 업데이트, 백업)을 시작하는 "thundering herd" 문제를 방지합니다.

[Timer]
OnCalendar=daily
Persistent=true
# 0~1800초(30분) 범위에서 무작위 지연
RandomizedDelaySec=1800
# 실행 정밀도 (기본 1분, 배터리 절약 시 높이면 타이머 합침)
AccuracySec=60
# 시스템이 깨어 있을 때만 실행 (절전 복귀 후 실행 방지)
WakeSystem=false

monotonic 타이머 시나리오

# 시나리오 1: 부팅 5분 후 한 번 실행 (시스템 초기화 후 상태 수집)
[Timer]
OnBootSec=5min

# 시나리오 2: 30초마다 주기적 실행 (헬스체크)
[Timer]
OnUnitActiveSec=30s

# 시나리오 3: 작업 완료 10분 후 다시 실행 (배치 처리)
# OnUnitActiveSec는 서비스 시작 기준, OnUnitInactiveSec는 완료 기준
[Timer]
OnUnitInactiveSec=10min

# 시나리오 4: 부팅 후 + 매 6시간 (복합)
[Timer]
OnBootSec=15min
OnUnitActiveSec=6h

타이머 유닛 예제

# /etc/systemd/system/backup.timer
[Unit]
Description=매일 자정 백업 타이머

[Timer]
OnCalendar=*-*-* 00:00:00
Persistent=true
RandomizedDelaySec=1800
AccuracySec=60

[Install]
WantedBy=timers.target
# /etc/systemd/system/backup.service
[Unit]
Description=백업 서비스

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
Nice=19
IOSchedulingClass=idle
Persistent=true는 시스템이 꺼져 있어서 놓친 실행을 부팅 직후에 보상 실행합니다. RandomizedDelaySec=는 여러 시스템이 동시에 같은 작업을 수행하는 것을 방지하는 무작위 지연입니다.

cron 대체 비교

항목cronsystemd 타이머
로그메일 또는 별도 설정journalctl 통합
의존성없음After=, Requires= 지원
리소스 제한없음cgroup 기반 제어 가능
놓친 실행anacron 별도Persistent= 내장
무작위 지연수동 sleepRandomizedDelaySec= 내장
정밀도분 단위마이크로초 단위 (AccuracySec)
보안셸 실행전체 서비스 보안 강화 적용 가능
# 활성 타이머 목록
systemctl list-timers --all

# 타이머 활성화
sudo systemctl enable --now backup.timer
OnBootSec= 부팅 후 N초 OnCalendar= 캘린더 표현식 OnUnitActiveSec= 마지막 실행 후 N초 backup.timer Persistent=true RandomizedDelaySec=1800 trigger backup.service Type=oneshot ExecStart=/usr/local/bin/backup.sh Persistent=true 보상 실행 시스템 꺼져서 놓친 실행 → 부팅 직후 즉시 실행 OnUnitInactiveSec 피드백 타이머 이름과 서비스 이름이 같으면 (backup.timer → backup.service) 자동 연결

소켓 활성화(Socket Activation)

소켓 활성화(Socket Activation)는 systemd의 가장 강력한 기능 중 하나입니다. systemd가 서비스 대신 소켓을 먼저 열어 두고, 실제 연결이 들어오면 그때 서비스 프로세스를 시작하는 메커니즘입니다. 이를 통해 서비스 간 순서 의존성을 제거하고 병렬 부팅을 극대화할 수 있습니다.

소켓 활성화의 원리는 다음과 같습니다. 서비스 A가 서비스 B의 소켓에 연결해야 할 때, 전통적인 방식에서는 B가 완전히 시작되어 소켓을 열 때까지 A가 기다려야 합니다. 소켓 활성화에서는 systemd가 B의 소켓을 미리 열어 두므로, A는 B의 시작 여부와 관계없이 소켓에 연결할 수 있습니다. 실제 데이터가 전송되면 그때 B가 시작됩니다. 이 방식은 inetd의 현대적 재해석이라고 할 수 있습니다.

소켓 유닛 설정

지시어설명예시
ListenStream=TCP 스트림 소켓 또는 Unix 소켓ListenStream=8080
ListenDatagram=UDP 데이터그램 소켓ListenDatagram=514
ListenSequentialPacket=Unix SEQPACKET 소켓ListenSequentialPacket=/run/myapp.sock
ListenFIFO=FIFO(Named Pipe)ListenFIFO=/run/myapp.fifo
Accept=연결별 인스턴스 생성 (inetd 방식)Accept=yes
MaxConnections=동시 연결 수 제한 (Accept=yes 시)MaxConnections=64
FileDescriptorName=FD 이름 지정 (다중 소켓 구분용)FileDescriptorName=http

Accept=no vs Accept=yes

모드서비스 인스턴스FD 전달적합한 서비스
Accept=no (기본)하나의 서비스가 모든 연결 처리리스닝 소켓 FD 전달대부분의 데몬 (nginx, PostgreSQL)
Accept=yes연결마다 새 서비스 인스턴스연결된 소켓 FD 전달간단한 per-connection 처리

sd_listen_fds() API

서비스 프로세스는 sd_listen_fds() 함수로 systemd가 전달한 소켓 파일 디스크립터(FD)를 받습니다. FD 번호는 SD_LISTEN_FDS_START (=3)부터 시작합니다.

#include <systemd/sd-daemon.h>

int main(void) {
    int n_fds = sd_listen_fds(0);  /* 전달받은 FD 개수 */

    if (n_fds < 0) {
        fprintf(stderr, "sd_listen_fds 실패: %s\n", strerror(-n_fds));
        return 1;
    }

    if (n_fds == 0) {
        /* 소켓 활성화 없이 직접 실행된 경우 — 직접 소켓 생성 */
        server_fd = create_socket(8080);
    } else {
        /* systemd가 전달한 소켓 사용 */
        server_fd = SD_LISTEN_FDS_START; /* fd = 3 */

        /* FD 이름으로 구분 (다중 소켓) */
        if (sd_is_socket_inet(SD_LISTEN_FDS_START, AF_INET, SOCK_STREAM, 1, 8080))
            http_fd = SD_LISTEN_FDS_START;
    }

    /* sd_notify로 준비 완료 알림 */
    sd_notify(0, "READY=1");

    /* accept() 루프 */
    while ((client_fd = accept(server_fd, NULL, NULL)) >= 0) {
        handle_client(client_fd);
        close(client_fd);
    }
    return 0;
}

실전 예제: TCP 서비스

# /etc/systemd/system/myhttp.socket
[Unit]
Description=My HTTP Socket

[Socket]
ListenStream=8080
Accept=no
ReusePort=true

[Install]
WantedBy=sockets.target
# /etc/systemd/system/myhttp.service
[Unit]
Description=My HTTP Service
Requires=myhttp.socket

[Service]
Type=notify
ExecStart=/usr/local/bin/myhttp-server
FileDescriptorStoreMax=16
NonBlocking=true

[Install]
WantedBy=multi-user.target
# 소켓만 활성화 (서비스는 연결 시 자동 시작)
sudo systemctl enable --now myhttp.socket

# 테스트 — 연결하면 서비스가 자동 시작됨
curl http://localhost:8080/

# 확인
systemctl status myhttp.socket
systemctl status myhttp.service
Client connect(:8080) systemd (PID 1) 소켓 FD 보유 중 연결 감지 → 서비스 시작 FD 전달 준비 SYN fork() + exec() 서비스 프로세스 생성 LISTEN_FDS=1 LISTEN_PID=서비스PID spawn Service sd_listen_fds() fd=3 사용 accept() + 처리 FD=3 1 2 3 4 소켓 활성화의 핵심 이점 1. 서비스가 실행되지 않아도 포트가 열려 있어 연결이 큐에 대기 (클라이언트는 지연만 경험, 연결 거부 없음) 2. 서비스 A가 서비스 B의 소켓에 의존할 때, B의 소켓은 이미 열려 있으므로 A는 B의 시작을 기다릴 필요 없음 → 병렬 부팅 FileDescriptorStoreMax= 서비스 재시작 시에도 연결 FD를 systemd에 저장하여 무중단 업그레이드 가능 (sd_pid_notify_with_fds)

보안 강화(Security Hardening)

systemd는 서비스별로 다양한 보안 격리(Isolation) 메커니즘을 선언적으로 적용할 수 있습니다. 이는 커널의 네임스페이스, seccomp-bpf, capabilities를 활용하여 서비스의 공격 표면(Attack Surface)을 최소화합니다. 전통적으로 이러한 보안 설정은 SELinux 정책이나 AppArmor 프로파일을 직접 작성해야 했지만, systemd는 간단한 INI 지시어만으로 동일한 수준의 격리를 달성할 수 있습니다.

보안 강화의 핵심 원칙은 기본 거부(Deny by Default)입니다. 먼저 모든 것을 차단하고, 서비스에 필요한 최소한의 접근만 허용하는 접근법입니다.

네임스페이스 격리

지시어효과
ProtectSystem=true/usr, /boot 읽기 전용
full+ /etc 읽기 전용
strict전체 파일시스템 읽기 전용 (ReadWritePaths로 예외 지정)
ProtectHome=true/home, /root, /run/user 접근 불가 (빈 디렉터리로 대체)
read-only읽기 전용
tmpfs빈 tmpfs로 대체
PrivateTmp=yes전용 /tmp, /var/tmp (다른 서비스와 공유 안 함)
PrivateDevices=yes전용 /dev (null, zero, random 등 기본 장치만)
PrivateNetwork=yes격리된 네트워크 네임스페이스 (loopback만)
PrivateUsers=yes격리된 사용자 네임스페이스
PrivateIPC=yes격리된 IPC 네임스페이스
PrivateMounts=yes격리된 마운트 네임스페이스
ProtectHostname=yes호스트명 변경 방지 (UTS 네임스페이스)

권한 제어

지시어설명
NoNewPrivileges=yessetuid/setgid 실행 파일 통해 권한 상승 방지
CapabilityBoundingSet=유지할 능력(Capability) 화이트리스트
AmbientCapabilities=비특권 프로세스에 부여할 능력
DynamicUser=yes서비스 실행 시 임시 사용자 동적 할당 (v232+)

Seccomp 시스템 콜 필터

지시어설명
SystemCallFilter=@system-service일반 시스템 서비스에 필요한 콜만 허용
SystemCallFilter=~@mount마운트 관련 콜 차단 (~ = 블랙리스트)
SystemCallArchitectures=native네이티브 아키텍처 콜만 허용 (32비트 호환 차단)
SystemCallErrorNumber=EPERM차단된 콜에 대한 에러 번호 (기본: SIGSYS kill)

systemd가 미리 정의한 시스템 콜 그룹(@ 접두사)은 다음과 같습니다.

그룹설명
@system-service일반 시스템 서비스에 적합한 기본 콜 집합
@network-io네트워크 I/O (socket, connect, send, recv 등)
@file-system파일시스템 작업 (open, read, write 등)
@process프로세스 관리 (fork, exec, kill 등)
@mount마운트 작업
@privileged특권 작업 (chroot, reboot, sethostname 등)
@clock시계 제어 (settimeofday, adjtimex)
@module커널 모듈 로드 (init_module, delete_module)
@raw-io원시 I/O (ioperm, iopl)
@obsolete폐기된 시스템 콜

파일시스템 접근 제어

[Service]
# 읽기 전용 경로
ReadOnlyPaths=/etc /usr

# 읽기/쓰기 허용 경로 (ProtectSystem=strict와 함께 사용)
ReadWritePaths=/var/lib/myapp /var/log/myapp

# 접근 불가 경로
InaccessiblePaths=/home /root

# 임시 tmpfs 오버레이
TemporaryFileSystem=/var:ro

커널 보호 지시어

지시어보호 대상
ProtectKernelTunables=yes/proc/sys, /sys 쓰기 방지
ProtectKernelModules=yes커널 모듈 로드/언로드 방지
ProtectKernelLogs=yes/dev/kmsg, /proc/kmsg 접근 방지
ProtectControlGroups=yes/sys/fs/cgroup 쓰기 방지
ProtectClock=yes시스템 시계 변경 방지
RestrictNamespaces=yes새 네임스페이스 생성 방지
RestrictRealtime=yes실시간 스케줄링 정책 사용 방지
MemoryDenyWriteExecute=yesW+X 메모리 매핑 방지 (JIT 차단)
LockPersonality=yes실행 도메인(Personality) 변경 방지
RestrictSUIDSGID=yessetuid/setgid 비트 설정 방지

systemd-analyze security

systemd-analyze security 명령은 모든 서비스의 보안 강화 수준을 자동으로 평가합니다.

# 전체 서비스 보안 점수 목록
systemd-analyze security

# 특정 서비스 상세 분석
systemd-analyze security nginx.service
# OVERALL EXPOSURE LEVEL: 4.2 OK
# 0.0~2.0: 우수, 2.1~4.0: 양호, 4.1~7.0: 보통, 7.1~10.0: 위험
Capabilities (CapabilityBoundingSet, NoNewPrivileges) Namespaces (ProtectSystem, PrivateTmp, PrivateNetwork, PrivateUsers) Seccomp-BPF (SystemCallFilter, SystemCallArchitectures) Filesystem (ReadOnlyPaths, InaccessiblePaths, ProtectHome) Kernel Protection (ProtectKernelTunables/Modules/Logs) Service Process 최소 권한 원칙(Least Privilege) DynamicUser, User=, Group= EXPOSURE: 0.0 (가장 안전) ← ────────────── → 10.0 (가장 노출) | systemd-analyze security 로 측정

보안 강화 실전 적용 절차

새 서비스에 보안을 적용하는 권장 절차는 다음과 같습니다.

  1. 기본 보안 측정systemd-analyze security myservice.service로 현재 점수 확인
  2. 기본 격리 적용ProtectSystem=strict, ProtectHome=yes, PrivateTmp=yes, NoNewPrivileges=yes 추가
  3. 필요한 쓰기 경로 열기ReadWritePaths=로 서비스가 쓰는 경로만 허용
  4. Capability 최소화CapabilityBoundingSet=에 필요한 능력만 나열
  5. Seccomp 적용SystemCallFilter=@system-service부터 시작, 필요하면 그룹 추가
  6. 재측정 및 반복 — 점수가 원하는 수준이 될 때까지 반복
# 1단계: 기본 점수 확인
systemd-analyze security nginx.service
# OVERALL EXPOSURE LEVEL for nginx.service: 9.6 UNSAFE 😨

# 2단계: 보안 지시어 추가 (systemctl edit)
sudo systemctl edit nginx.service
# 위의 "예제 5" 설정을 입력

# 3단계: 적용 및 재확인
sudo systemctl daemon-reload
sudo systemctl restart nginx.service
systemd-analyze security nginx.service
# OVERALL EXPOSURE LEVEL for nginx.service: 1.8 OK 🙂

# 4단계: 서비스 정상 동작 확인
systemctl status nginx.service
curl http://localhost/
주의: MemoryDenyWriteExecute=yes는 JIT 컴파일러(V8, JVM, .NET)를 사용하는 서비스와 호환되지 않습니다. Node.js, Java, Mono 서비스에는 이 옵션을 적용하지 마세요.

DynamicUser

DynamicUser=yes(v232+)를 사용하면 서비스 시작 시 임시 사용자/그룹이 동적으로 할당되고, 서비스 종료 시 자동으로 해제됩니다. User=/Group=을 수동으로 생성할 필요가 없어 컨테이너리스(containerless) 격리에 유용합니다.

[Service]
DynamicUser=yes
# 상태 디렉터리 자동 관리 (/var/lib/private/myapp → /var/lib/myapp 심볼릭 링크)
StateDirectory=myapp
# 캐시 디렉터리 자동 관리
CacheDirectory=myapp
# 로그 디렉터리 자동 관리
LogsDirectory=myapp

StateDirectory=, CacheDirectory=, LogsDirectory=는 DynamicUser와 함께 사용할 때 특히 유용합니다. systemd가 디렉터리를 자동으로 생성하고 소유권을 할당합니다.

Credentials

v247부터 도입된 LoadCredential=/SetCredential=은 시크릿(Secret) 데이터를 안전하게 서비스에 전달하는 메커니즘입니다. 환경 변수나 파일 시스템 경로에 비밀 정보를 노출하지 않고, 메모리 기반의 일시적 디렉터리를 통해 전달합니다.

[Service]
# 파일에서 Credential 로드
LoadCredential=db-password:/etc/myapp/secrets/db-password
LoadCredential=api-key:/etc/myapp/secrets/api-key

# 인라인 Credential 설정
SetCredential=log-level:debug

# 서비스 내에서 접근: $CREDENTIALS_DIRECTORY/db-password
ExecStart=/opt/myapp/bin/server \
  --db-password-file=${CREDENTIALS_DIRECTORY}/db-password

컨테이너 통합 (systemd-nspawn)

systemd-nspawn은 chroot의 강화 버전으로, 리눅스 네임스페이스(Mount, PID, UTS, IPC, Network, User)를 활용하여 프로세스를 격리하는 경량 컨테이너(Lightweight Container) 도구입니다. "chroot on steroids"라고도 불립니다. Docker나 Podman처럼 이미지 레지스트리나 오버레이 파일시스템을 사용하지 않고, 디렉터리나 디스크 이미지를 직접 컨테이너로 부팅할 수 있습니다.

기본 사용법

# 디렉터리를 컨테이너로 부팅
sudo systemd-nspawn -D /var/lib/machines/mycontainer

# 디스크 이미지를 컨테이너로 부팅
sudo systemd-nspawn -i /var/lib/machines/myimage.raw

# 임시(ephemeral) 컨테이너 — 종료 시 변경사항 폐기
sudo systemd-nspawn --ephemeral -D /var/lib/machines/mycontainer

# 호스트 디렉터리 바인드 마운트
sudo systemd-nspawn -D /var/lib/machines/mycontainer \
  --bind=/data:/mnt/data \
  --bind-ro=/etc/resolv.conf:/etc/resolv.conf

machinectl 관리

# 컨테이너 목록
machinectl list

# 컨테이너 시작/종료
sudo machinectl start mycontainer
sudo machinectl poweroff mycontainer

# 컨테이너 셸 접속
sudo machinectl shell mycontainer
sudo machinectl login mycontainer

# 이미지 다운로드 (raw/tar)
sudo machinectl pull-raw --verify=checksum \
  https://example.com/myimage.raw.xz myimage

# 이미지 목록
machinectl list-images

네트워크 모드

옵션설명용도
--network-vethveth 쌍 생성 (호스트-컨테이너 연결)기본 격리 네트워크
--network-bridge=br0veth를 호스트 브리지에 연결LAN 직접 접속
--network-macvlan=eth0macvlan 인터페이스 생성별도 MAC 주소 할당
--network-zone=myzone같은 zone 컨테이너끼리 L2 연결컨테이너 간 통신
--private-network격리된 네트워크 (loopback만)완전 네트워크 격리

nspawn vs Docker/Podman 비교

항목systemd-nspawnDocker/Podman
이미지 형식디렉터리 / raw 이미지OCI/Docker 이미지 레이어
레지스트리없음 (수동 관리)Docker Hub, 사설 레지스트리
오버레이 FS없음 (--overlay 옵션)overlay2 기본
init 시스템컨테이너 내 systemd 자연 실행보통 PID 1 = 애플리케이션
cgroup 위임네이티브 (machine.slice)shim 계층 필요
machinectl 통합네이티브미지원
Kubernetes 연동미지원CRI 준수
주 용도OS 컨테이너, 테스트, CI/CD애플리케이션 컨테이너, 프로덕션
실용 팁: nspawn은 배포판의 전체 OS 트리를 컨테이너로 실행하는 데 적합합니다. debootstrap이나 dnf --installroot로 최소 루트를 만들고, nspawn으로 부팅 테스트를 수행할 수 있습니다.

nspawn 설정 파일

/etc/systemd/nspawn/ 디렉터리에 컨테이너별 설정 파일을 배치하면 machinectl로 시작할 때 자동으로 적용됩니다.

# /etc/systemd/nspawn/webserver.nspawn
[Exec]
Boot=yes
Capability=CAP_NET_BIND_SERVICE
# 컨테이너 내에서 사용할 환경 변수
Environment=NODE_ENV=production

[Network]
VirtualEthernet=yes
Bridge=br0
Port=tcp:80:8080
Port=tcp:443:8443

[Files]
Bind=/data/web:/var/www:ro
BindReadOnly=/etc/ssl/certs
TemporaryFileSystem=/tmp
PrivateUsersChown=yes
# debootstrap으로 최소 Debian 루트 생성
sudo debootstrap stable /var/lib/machines/debian-test http://deb.debian.org/debian

# 첫 부팅 — 비밀번호 설정
sudo systemd-nspawn -D /var/lib/machines/debian-test
# passwd  (루트 비밀번호 설정)
# exit

# 정상 부팅 (machinectl 관리 등록)
sudo machinectl start debian-test

# 컨테이너 셸 접속
sudo machinectl shell debian-test /bin/bash

# 컨테이너 로그 확인
journalctl -M debian-test

Portable Services

systemd v250부터 도입된 Portable Services는 nspawn과 일반 서비스의 중간 형태입니다. 이미지 파일 안에 서비스 유닛과 바이너리를 묶어서 호스트 시스템에 "연결(attach)"하면, 호스트의 systemd가 직접 관리합니다. 컨테이너보다 가볍고, 일반 서비스보다 격리가 강합니다.

# 포터블 서비스 이미지 연결
sudo portablectl attach myapp.raw

# 연결된 서비스 시작
sudo systemctl start myapp.service

# 이미지 분리
sudo portablectl detach myapp.raw

디버깅 & 분석

systemd는 풍부한 디버깅 도구를 제공합니다. 시스템 관리자와 개발자가 문제를 체계적으로 진단할 수 있도록 각 도구가 특정 영역을 담당합니다. 부팅 시간 분석, 유닛 의존성 추적, 코어 덤프(Core Dump) 관리, D-Bus 인트로스펙션(Introspection) 등을 통해 시스템 문제를 체계적으로 진단할 수 있습니다.

systemd-analyze

# 부팅 시간 요약
systemd-analyze

# 유닛별 시작 시간 (느린 순서)
systemd-analyze blame

# 임계 경로 분석
systemd-analyze critical-chain graphical.target

# SVG 부팅 차트
systemd-analyze plot > boot-chart.svg

# DOT 의존성 그래프
systemd-analyze dot nginx.service | dot -Tsvg > nginx-deps.svg

# 보안 분석
systemd-analyze security
systemd-analyze security nginx.service

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

# 캘린더 표현식 테스트
systemd-analyze calendar "Mon *-*-* 03:00:00"

# 시간 간격 파싱
systemd-analyze timespan "2h 30min"

journalctl 고급

# JSON 형식 출력 (파이프라인 처리용)
journalctl -u nginx.service -o json-pretty --no-pager

# 커서 기반 연속 읽기 (로그 수집기)
journalctl --after-cursor="s=abc123..." --show-cursor -o export

# 복합 필드 매칭
journalctl _SYSTEMD_UNIT=sshd.service + _PID=5678
# '+' 는 OR 조건

# 커널 메시지만
journalctl -k
journalctl _TRANSPORT=kernel

# 특정 부팅의 에러 이상 로그
journalctl -b -1 -p err --no-pager

coredumpctl

systemd-coredump는 프로세스 비정상 종료 시 코어 덤프를 자동 수집하고 저널에 기록합니다.

# 코어 덤프 목록
coredumpctl list

# 특정 실행 파일의 최신 코어 덤프 정보
coredumpctl info /usr/bin/myapp

# gdb 연동
coredumpctl debug /usr/bin/myapp

# 코어 파일로 추출
coredumpctl dump /usr/bin/myapp -o /tmp/myapp.core

busctl (D-Bus 디버깅)

# D-Bus 서비스 트리
busctl tree org.freedesktop.systemd1

# 인터페이스 인트로스펙션
busctl introspect org.freedesktop.systemd1 /org/freedesktop/systemd1

# 메서드 호출
busctl call org.freedesktop.systemd1 /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager GetUnitByPID u $$

# 시그널 모니터링
busctl monitor org.freedesktop.systemd1

systemd-cgtop / systemd-cgls

# cgroup별 리소스 사용량 (top 방식)
systemd-cgtop

# cgroup 트리 구조
systemd-cgls

# 특정 슬라이스만
systemd-cgls /system.slice

서비스 디버깅 실전 시나리오

서비스가 시작되지 않거나 비정상 종료되는 경우의 진단 절차를 단계별로 살펴봅니다.

시나리오 1: 서비스 시작 실패

# 1. 상태 확인 — 실패 원인의 첫 번째 단서
systemctl status myapp.service
# ● myapp.service - My Application
#   Loaded: loaded (/etc/systemd/system/myapp.service; enabled)
#   Active: failed (Result: exit-code) since ...
#   Process: 12345 ExecStart=... (code=exited, status=1/FAILURE)
#    Main PID: 12345 (code=exited, status=1/FAILURE)

# 2. 전체 로그 확인
journalctl -u myapp.service -b --no-pager

# 3. 환경 변수 문제 의심 시
systemctl show myapp.service -p Environment
systemctl show myapp.service -p EnvironmentFiles

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

# 5. 보안 설정이 원인인지 확인 (보안 지시어 임시 제거 후 테스트)
# SystemCallFilter= 가 너무 제한적일 수 있음
sudo strace -f -e trace=process,network /opt/myapp/bin/server 2>&1 | head -50

# 6. 수동 실행으로 환경 차이 확인
sudo -u myapp /opt/myapp/bin/server --config /etc/myapp/config.toml

시나리오 2: 서비스가 주기적으로 재시작

# 1. 재시작 횟수 확인
systemctl show myapp.service -p NRestarts
# NRestarts=15

# 2. 종료 코드/시그널 확인
systemctl show myapp.service -p ExecMainStatus -p ExecMainCode
# ExecMainCode=signal   ← 시그널에 의한 종료
# ExecMainStatus=9      ← SIGKILL = OOM Kill 가능성

# 3. OOM Kill 확인
journalctl -u myapp.service -b | grep -i "oom\|kill\|memory"
dmesg | grep -i "oom\|kill" | tail -5

# 4. cgroup 메모리 이벤트 확인
systemctl show myapp.service -p MemoryCurrent -p MemoryPeak
cat /sys/fs/cgroup/system.slice/myapp.service/memory.events
# oom_kill 카운터가 증가했는지 확인

# 5. watchdog 타임아웃 확인
journalctl -u myapp.service -b | grep -i watchdog

시나리오 3: 부팅 시 서비스 시작 순서 문제

# 1. 의존성 트리 확인
systemctl list-dependencies myapp.service

# 2. 역방향 의존성 (누가 이 유닛에 의존하는가)
systemctl list-dependencies --reverse myapp.service

# 3. 순서 관계 시각화
systemd-analyze dot myapp.service | dot -Tsvg > /tmp/deps.svg

# 4. 특정 유닛 사이의 순서 확인
systemd-analyze critical-chain myapp.service

systemd 디버그 로깅

systemd 자체의 디버그 로그를 활성화하여 내부 동작을 추적할 수 있습니다.

# 런타임에 PID 1의 로그 레벨 변경
sudo systemctl log-level debug

# 디버그 로그 확인
journalctl -b -u init.scope -p debug

# 원래대로 복원
sudo systemctl log-level info

# 부팅 시 디버그: 커널 명령행에 추가
# systemd.log_level=debug systemd.log_target=journal

unitctl과 systemd-run 활용

# 일회성 명령을 systemd 서비스로 실행 (리소스 제한 적용)
systemd-run --scope -p MemoryMax=500M -p CPUQuota=50% -- \
  stress-ng --vm 2 --vm-bytes 256M --timeout 60

# 일회성 타이머 (5분 후 실행)
systemd-run --on-active=5min /usr/local/bin/cleanup.sh

# 일회성 캘린더 타이머
systemd-run --on-calendar="2026-03-24 03:00" /usr/local/bin/migration.sh

# 결과 확인
systemctl list-units --type=scope 'run-*'
journalctl -u 'run-*'

# 유닛 속성 실시간 변경 (재시작 불필요)
sudo systemctl set-property nginx.service MemoryMax=4G
# 영구 적용됨 (/etc/systemd/system.control/ 에 저장)
디버깅 도구 systemd-analyze journalctl coredumpctl busctl systemd-cgtop systemctl status 조사 대상 부팅 시간 / 의존성 시스템/서비스 로그 프로세스 크래시 D-Bus 통신 리소스 사용량 유닛 상태 / PID 각 도구는 주 조사 대상(실선)과 보조 연관(파선)을 갖음 — 문제 증상에 따라 적절한 도구 선택

실전 예제

이 섹션에서는 systemd 유닛 파일 작성의 실전 패턴을 다섯 가지 시나리오로 정리합니다. 각 예제는 실제 프로덕션(Production) 환경에서 바로 활용할 수 있는 수준의 설정을 포함합니다.

실무에서 빈번히 사용하는 다섯 가지 패턴을 완전한 유닛 파일로 제시합니다. 각 예제는 복사하여 바로 활용할 수 있도록 상세한 주석을 포함합니다.

예제 1: Type=notify 서비스 (sd_notify + watchdog)

데이터베이스나 메시지 큐처럼 초기화에 시간이 걸리는 서비스에 적합합니다. 서비스가 스스로 준비 완료를 알리고, watchdog으로 주기적 상태를 보고합니다.

[Unit]
Description=My Application Server (notify + watchdog)
Documentation=https://docs.example.com/myapp
After=network-online.target postgresql.service
Requires=postgresql.service
Wants=network-online.target

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

# 시작 전 설정 검증
ExecStartPre=/opt/myapp/bin/validate-config --config /etc/myapp/config.toml
ExecStart=/opt/myapp/bin/server --config /etc/myapp/config.toml

# Watchdog: 30초 간격으로 WATCHDOG=1 기대
WatchdogSec=30
# watchdog 실패 시 서비스 재시작
Restart=on-watchdog
# 비정상 종료 시에도 재시작
Restart=on-failure
RestartSec=5

# 시작 타임아웃 (초기화가 오래 걸리는 경우)
TimeoutStartSec=120
TimeoutStopSec=30

# 재시작 제한: 5분 이내 3회 초과 시 중단
StartLimitIntervalSec=300
StartLimitBurst=3

# 환경 변수 파일
EnvironmentFile=-/etc/myapp/env

# 상태 정보 (systemctl status에 표시)
NotifyAccess=main

[Install]
WantedBy=multi-user.target

예제 2: 타이머 기반 백업 서비스

매일 새벽 3시에 백업을 실행하되, 시스템이 꺼져 있었으면 부팅 후 보상 실행합니다.

# /etc/systemd/system/daily-backup.timer
[Unit]
Description=Daily Backup Timer

[Timer]
OnCalendar=*-*-* 03:00:00
# 놓친 실행 보상
Persistent=true
# 여러 서버가 동시에 백업하지 않도록 최대 30분 무작위 지연
RandomizedDelaySec=1800
# 정확도 (전력 절약을 위해 타이머를 합칠 수 있는 범위)
AccuracySec=60

[Install]
WantedBy=timers.target
# /etc/systemd/system/daily-backup.service
[Unit]
Description=Daily Backup
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
User=backup
Group=backup
ExecStart=/usr/local/bin/backup.sh --target s3://my-bucket/backups/

# 백업은 CPU/IO 우선순위 낮게
Nice=19
IOSchedulingClass=idle
CPUQuota=25%

# 보안 강화
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/var/backup /tmp
PrivateTmp=yes
NoNewPrivileges=yes

# 2시간 타임아웃
TimeoutStartSec=7200

예제 3: 소켓 활성화 HTTP 서비스

연결이 들어올 때만 서비스가 시작되는 on-demand 방식의 HTTP 서비스입니다.

# /etc/systemd/system/myhttp.socket
[Unit]
Description=My HTTP Socket Activation

[Socket]
ListenStream=0.0.0.0:8080
ListenStream=[::]:8080
ReusePort=true
NoDelay=true
# 최대 대기 연결
Backlog=128

# 소켓이 비활성화 되어도 서비스 유지
KeepAlive=true

[Install]
WantedBy=sockets.target
# /etc/systemd/system/myhttp.service
[Unit]
Description=My HTTP Service
Requires=myhttp.socket
After=myhttp.socket

[Service]
Type=notify
ExecStart=/usr/local/bin/myhttp --systemd
# FD 저장: 재시작 시 연결 유지
FileDescriptorStoreMax=4096
Restart=on-failure
RestartSec=1

# 보안
DynamicUser=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
NoNewPrivileges=yes
CapabilityBoundingSet=
SystemCallFilter=@system-service @network-io

[Install]
WantedBy=multi-user.target

예제 4: 리소스 제한 데이터베이스 서비스

# /etc/systemd/system/postgresql.service.d/resource-limits.conf
[Service]
# 메모리: 최대 4GB, 소프트 제한 3GB
MemoryMax=4G
MemoryHigh=3G
# 스왑 사용 제한
MemorySwapMax=1G

# CPU: 최대 4코어 분량
CPUQuota=400%
CPUWeight=200

# I/O: 높은 우선순위
IOWeight=500
IOReadBandwidthMax=/dev/nvme0n1 500M
IOWriteBandwidthMax=/dev/nvme0n1 200M

# 태스크 제한
TasksMax=4096

# OOM 보호 (oomd가 이 서비스를 마지막에 kill)
OOMPolicy=stop
ManagedOOMPreference=avoid

예제 5: 보안 강화 웹 서버

# /etc/systemd/system/nginx.service.d/hardening.conf
[Service]
# 네임스페이스 격리
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
PrivateIPC=yes
ProtectHostname=yes
ProtectClock=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes

# 읽기/쓰기 예외 경로
ReadWritePaths=/var/log/nginx /var/cache/nginx /run/nginx

# 권한 제어
NoNewPrivileges=yes
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_DAC_READ_SEARCH CAP_SETUID CAP_SETGID
AmbientCapabilities=CAP_NET_BIND_SERVICE

# seccomp
SystemCallFilter=@system-service @network-io
SystemCallArchitectures=native
SystemCallErrorNumber=EPERM

# 추가 보안
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
MemoryDenyWriteExecute=yes
LockPersonality=yes
RemoveIPC=yes

# 리소스 제한
MemoryMax=2G
TasksMax=1024
LimitNOFILE=65535
보안 점수 확인: 위 설정을 적용한 후 systemd-analyze security nginx.service를 실행하면 EXPOSURE 점수가 2.0 이하로 떨어지는 것을 확인할 수 있습니다.

유닛 파일 작성 모범 사례

실전에서 유닛 파일을 작성할 때 지켜야 할 모범 사례를 정리합니다.

원칙설명예시
적절한 Type 선택서비스 동작 방식에 맞는 Type 사용fork 데몬 → Type=forking
Restart 정책 설정프로덕션 서비스는 반드시 재시작 정책 포함Restart=on-failure
재시작 제한 설정무한 재시작 방지StartLimitBurst=5
보안 강화 기본 적용최소한의 보안 지시어 포함NoNewPrivileges=yes
환경 변수 파일 사용설정과 시크릿 분리EnvironmentFile=/etc/myapp/env
Documentation 명시관련 문서 URL 포함Documentation=https://...
적절한 타임아웃서비스 특성에 맞는 타임아웃 설정TimeoutStartSec=120
드롭인 방식 수정원본 파일 직접 수정 금지systemctl edit service
User/Group 지정root 실행 최소화User=myapp, Group=myapp
의존성 명확화After와 Requires/Wants 함께 사용After=network.target + Wants=network.target
# 유닛 파일 린팅(linting) — 문법 검증
systemd-analyze verify /etc/systemd/system/myapp.service

# 보안 점수 확인
systemd-analyze security myapp.service

# 의존성 확인
systemctl list-dependencies myapp.service

# 최종 적용 설정 확인 (드롭인 포함)
systemctl cat myapp.service

환경 변수와 시크릿 관리 상세

서비스에 환경 변수와 시크릿을 전달하는 방법은 여러 가지이며, 보안 수준에 따라 적절한 방법을 선택해야 합니다.

방법보안 수준설명
Environment=낮음유닛 파일에 직접 기록 (/proc/PID/environ에 노출)
EnvironmentFile=중간파일에서 로드 (파일 권한으로 보호)
LoadCredential=높음메모리 기반 임시 디렉터리로 전달 (v247+)
LoadCredentialEncrypted=매우 높음TPM2로 암호화된 시크릿 (v250+)
# 암호화된 Credential 생성 (TPM2 기반)
sudo systemd-creds encrypt --name=db-password \
  /etc/myapp/secrets/db-password \
  /etc/myapp/secrets/db-password.cred

# 유닛 파일에서 사용
# LoadCredentialEncrypted=db-password:/etc/myapp/secrets/db-password.cred

soft-reboot (v254+)

soft-reboot는 커널을 재부팅하지 않고 사용자 공간만 재시작하는 기능입니다. 커널과 하드웨어 초기화를 건너뛰므로 수 초 만에 서비스를 재시작할 수 있습니다. 커널 업데이트가 아닌 사용자 공간 변경(패키지 업데이트, 설정 변경)에 유용합니다.

# 소프트 리부트 실행
sudo systemctl soft-reboot

# 소프트 리부트 후 확인
# uptime은 커널 시작 시간 기준이므로 긴 값이 유지됨
# journalctl --list-boots에서 새 부팅 세션이 생성됨

Portable Services 상세

Portable Services는 애플리케이션과 유닛 파일을 하나의 이미지(raw 또는 디렉터리)에 묶어 배포하는 메커니즘입니다. 호스트에 "연결(attach)"하면 이미지 내의 유닛 파일이 호스트 systemd에 등록되고, 이미지의 바이너리가 적절히 마운트됩니다.

# 포터블 서비스 이미지 정보 확인
portablectl inspect myapp.raw

# 이미지 연결 (유닛 등록 + 프로파일 적용)
sudo portablectl attach --profile=trusted myapp.raw

# 연결된 서비스 시작
sudo systemctl start myapp.service

# 연결 목록
portablectl list

# 이미지 분리 (서비스 중지 + 유닛 제거)
sudo portablectl detach myapp.raw

run0 (sudo 대체, v256+)

systemd v256에서 도입된 run0sudo의 대안입니다. 기존 sudo는 setuid 바이너리로 권한 상승하지만, run0는 systemd에 요청하여 새로운 서비스 유닛으로 명령을 실행합니다. 이를 통해 setuid 바이너리의 보안 위험을 제거하고, 모든 실행이 journald에 기록됩니다.

# sudo와 동일한 사용법
run0 apt update
run0 -u postgres psql

# 실행은 별도 서비스 유닛으로 처리
# 모든 명령이 journald에 로깅됨

주요 버전별 변경사항

systemd는 약 4~6개월 주기로 주요 버전을 릴리스하며, 매 버전마다 커널 연동 기능이 확대되어 왔습니다. 새 버전은 일반적으로 최신 리눅스 커널의 새 기능을 활용하는 방향으로 발전합니다. 다음은 핵심 기능이 도입된 주요 버전입니다.

버전시기주요 기능
v12010-03최초 릴리스. Lennart Poettering이 PID 1 재설계 발표. 소켓 활성화, cgroup 기반 프로세스 추적 도입
v382011-07Fedora 15 기본 init으로 채택 — systemd를 기본 init으로 채택한 최초의 주요 배포판
v442012-01journald 도입, 바이너리 구조화 로그 형식 최초 구현. 기존 syslog 호환 유지
v1832013-02journald 안정화, 바이너리 저널 형식 확립. 대규모 배포판 채택 가속
v1972013-04udevd가 systemd 프로젝트로 통합. 기존 udev 단독 프로젝트에서 합병
v2052013-12소켓 활성화 안정화, 부팅 병렬화 완성. KDBUS 실험 시작 (이후 폐기)
v2082014-02networkd 초기 구현 — 네트워크 인터페이스 관리 데몬 도입
v2132014-07resolved 도입 — DNS 해석 데몬. 로컬 캐싱, mDNS/LLMNR 통합
v2152014-09Debian 8(Jessie)용 안정화. TimerSlackNSec, KillMode=mixed 도입
v2202015-05networkd/resolved 안정화, DNS 관리 통합. 첫 LTS 배포판(Ubuntu 16.04)에서 채택
v2262015-10cgroup v2 최초 지원 (실험적). unified hierarchy 인식, 커널 4.2+ 필요
v2292016-03Type=exec 도입 — exec() 실패를 즉시 감지하여 서비스 시작 실패 보고 개선
v2302016-05RestartKillSignal=, WatchdogSignal= 추가. 서비스 종료 신호 세밀 제어
v2322017-03DynamicUser= 도입 — 서비스별 동적 UID/GID 할당, StateDirectory/CacheDirectory 자동 관리
v2332017-06PrivateUsers=, ProtectKernelModules=, ProtectKernelTunables= 등 샌드박싱 강화
v2362017-12systemd-coredump 안정화, coredumpctl info/debug/gdb 연동
v2382018-04IPAddressAllow=/IPAddressDeny= — eBPF 기반 서비스별 네트워크 접근 제어
v2402018-12cgroup v2 기본 지원. 통합 계층 구조 권장. PSI 연동 기반 마련. 커널 4.15+ 필요
v2432019-09systemd-oomd 초기 구현 (PSI 기반). cgroup v2 전용. 커널 4.20+ PSI 필요
v2452020-05systemd-homed 도입 — LUKS/fscrypt 기반 휴대용 홈 디렉터리, 사용자 레코드 JSON
v2462020-07systemd-repart — 디스크 파티션 선언적 관리. A/B 업데이트 시나리오 지원
v2472020-12LoadCredential= 도입 — 서비스에 비밀 정보를 안전하게 전달하는 Credential 시스템
v2482021-03systemd-oomd 안정화. ManagedOOMMemoryPressure/ManagedOOMSwap 지시어 확립
v2492021-07ImportCredential=, SetCredential= 추가. TPM2 기반 credential 암호화 초기 지원
v2502021-12Portable Services — systemd-portabled, 이미지 기반 서비스 배포. systemd-sysext 도입
v2512022-04systemd-sysupdate — 이미지 기반 시스템 업데이트 매니저. A/B 파티션 전환
v2522022-10UKI(Unified Kernel Image) 지원. ukify 빌드 도구. systemd-measure로 PCR 사전 계산
v2532023-02TPM2 자동 등록(systemd-cryptenroll --tpm2-device=auto). systemd-pcrphase measured boot
v2542023-07soft-reboot — 커널을 유지한 채 사용자 공간만 재시작. systemd-bsod 도입 (부팅 실패 시 화면 표시)
v2552023-12tpm2-pcrlock — 부팅 무결성 정책 잠금. systemd-vmspawn으로 VM 빠른 시작
v2562024-06run0 도입 — sudo 대체 도구 (PTY 기반 권한 상승, 자격 증명 유출 방지). varlink IPC 확대
v2572024-12BPF 토큰 위임 개선, AF_UNIX 피어 인증, mkosi 통합 강화, ssh 기반 머신 관리

주요 마일스톤 타임라인

systemd 역사에서 특히 중요한 전환점은 다음 세 가지입니다.

마일스톤버전영향
cgroup v2 전환v226→v240v226에서 실험적 지원 시작, v240에서 기본 전환. 커널 cgroup unified hierarchy 채택이 주요 배포판으로 전파되는 계기. RHEL 9/Fedora 31+/Ubuntu 21.10+ 기본 v2
UKI 도입v252커널+initrd+cmdline+splash를 단일 PE 바이너리로 번들. Secure Boot 서명이 단일 파일로 단순화. 클라우드/엣지 환경에서 이미지 기반 부팅 표준화 추진
soft-rebootv254커널 재부팅 없이 userspace만 재시작. 업데이트 적용 시간 90%+ 단축. 컨테이너 오케스트레이터와 유사한 빠른 롤아웃 가능

배포판별 기본 systemd 버전 (2024 기준)

배포판버전systemd 버전cgroup v2 기본
Ubuntu 24.04 LTSNoblev255
Ubuntu 22.04 LTSJammyv249예 (하이브리드)
Debian 12Bookwormv252
Fedora 40-v255
RHEL 9-v252
RHEL 8-v239아니오 (v1)
SUSE 15 SP5-v254
Arch LinuxRolling최신
Alpine Linux-미사용 (OpenRC)-
Gentoo-선택 (OpenRC/systemd)-
버전 확인: systemctl --version 명령으로 현재 설치된 systemd 버전을 확인할 수 있습니다. 배포판별 기본 버전은 상이하므로, 특정 기능 사용 시 버전 호환성을 반드시 확인하세요. 예를 들어 DynamicUser=는 v232+, LoadCredential=은 v247+, UKI 지원은 v252+ 버전이 필요합니다.

주요 커밋과 설계 결정

systemd의 주요 설계 결정과 그 배경을 이해하면 현재 동작 방식을 더 깊이 파악할 수 있습니다.

자주 묻는 질문 (FAQ)

systemd를 운영하면서 자주 마주치는 질문과 그에 대한 실용적인 답변을 정리합니다. 각 질문은 실제 운영 환경에서 발생하는 문제 상황을 기반으로 구성했습니다.

Q1: systemd와 SysVinit의 근본적인 차이점은?

SysVinit는 순차적 셸 스크립트 실행 모델입니다. /etc/init.d/ 디렉터리의 스크립트를 숫자 순서로 하나씩 실행하므로, 서비스 A가 끝나야 서비스 B가 시작됩니다. 반면 systemd는 의존성 DAG 기반 병렬 실행 모델입니다. 서비스 간 의존성이 없으면 동시에 시작하고, 소켓 활성화를 통해 순서 의존성까지 제거할 수 있습니다. 또한 SysVinit는 프로세스를 PID 파일로 추적하는 반면, systemd는 cgroup으로 정확히 추적하여 fork 폭탄이나 데몬화 과정에서도 프로세스를 놓치지 않습니다.

Q2: journald 디스크 사용량을 제한하려면?

/etc/systemd/journald.conf에서 다음 설정을 조정합니다.

[Journal]
# 최대 디스크 사용량
SystemMaxUse=1G
# 최소 여유 공간 보장
SystemKeepFree=2G
# 개별 저널 파일 최대 크기
SystemMaxFileSize=128M
# 최대 보관 기간
MaxRetentionSec=1month

설정 변경 후 sudo systemctl restart systemd-journald로 적용합니다. 즉시 정리가 필요하면 sudo journalctl --vacuum-size=500M 또는 sudo journalctl --vacuum-time=7d를 사용합니다.

Q3: cgroup v1에서 v2로 마이그레이션하려면?

커널 부트 파라미터에 systemd.unified_cgroup_hierarchy=1을 추가합니다. GRUB의 경우 /etc/default/grubGRUB_CMDLINE_LINUX에 추가하고 update-grub 후 재부팅합니다. 주의할 점은 Docker, Kubernetes 등 컨테이너 런타임이 cgroup v2를 지원하는 버전인지 확인해야 합니다. Docker 20.10+, containerd 1.4+, crun 0.13+부터 cgroup v2를 지원합니다. 현재 cgroup 모드는 stat -fc %T /sys/fs/cgroup/으로 확인할 수 있습니다 (cgroup2fs면 v2).

Q4: systemd 없이 리눅스 부팅이 가능한가?

가능합니다. 리눅스 커널 자체는 init 시스템에 대해 중립적입니다. 커널은 PID 1으로 실행할 바이너리만 필요하며, 이것이 systemd일 필요는 없습니다. 대안으로 OpenRC(Gentoo, Alpine), runit(Void Linux), s6(skarnet), Busybox init(임베디드) 등이 있습니다. 커널 명령행에 init=/bin/bash를 전달하면 셸이 PID 1으로 실행됩니다(비상 복구용). 다만 현재 대부분의 주요 배포판(Debian, Ubuntu, Fedora, RHEL, SUSE, Arch)은 systemd를 기본 init으로 사용합니다.

Q5: 서비스 파일에서 환경 변수를 전달하려면?

여러 방법이 있습니다.

[Service]
# 방법 1: 직접 지정
Environment=DATABASE_URL=postgres://localhost/mydb
Environment=LOG_LEVEL=info

# 방법 2: 환경 파일에서 로드 (권장)
EnvironmentFile=/etc/myapp/env
# '-' 접두사: 파일이 없어도 에러 아님
EnvironmentFile=-/etc/myapp/env.local

# 방법 3: 생성기(Generator)를 통한 동적 생성
ExecStartPre=/usr/local/bin/generate-env.sh

환경 파일(/etc/myapp/env) 형식은 KEY=VALUE이며, #으로 주석을 작성합니다. 시크릿(Secret) 정보는 LoadCredential=(v247+) 지시어를 사용하는 것이 더 안전합니다.

Q6: 부팅 시간을 최적화하려면?

다음 단계로 접근합니다.

  1. systemd-analyze blame으로 가장 느린 서비스를 식별합니다.
  2. systemd-analyze critical-chain으로 임계 경로의 병목을 파악합니다.
  3. 불필요한 서비스를 systemctl disable 또는 systemctl mask합니다.
  4. 느린 서비스에 소켓 활성화를 적용하여 지연 시작(Lazy Start)합니다.
  5. NetworkManager-wait-online.service 같은 동기 대기 서비스의 타임아웃을 줄입니다.
  6. initramfs 크기를 최적화합니다 (dracut --hostonly 등).

Q7: mask와 disable의 차이는?

disable은 부팅 시 자동 시작 설정만 해제합니다. 수동으로 systemctl start하면 여전히 실행됩니다. mask는 유닛 파일을 /dev/null에 심볼릭 링크하여 어떤 방법으로도 시작할 수 없게 완전히 차단합니다. 다른 유닛의 의존성으로 인한 자동 시작도 차단됩니다. 해제하려면 systemctl unmask를 사용합니다.

# disable: 자동 시작 해제 (수동 시작 가능)
sudo systemctl disable bluetooth.service
sudo systemctl start bluetooth.service  # 성공

# mask: 완전 차단 (수동 시작도 불가)
sudo systemctl mask bluetooth.service
sudo systemctl start bluetooth.service  # 실패: Unit is masked

# 해제
sudo systemctl unmask bluetooth.service

Q8: journald와 rsyslog를 함께 사용할 수 있나?

가능합니다. journald의 ForwardToSyslog=yes(기본값) 설정이 활성화되어 있으면, journald는 수신한 로그를 /run/systemd/journal/syslog 소켓을 통해 rsyslog에 전달합니다. 이렇게 하면 journalctl의 구조화 검색 기능과 rsyslog의 원격 전송, 유연한 필터링을 모두 활용할 수 있습니다. 단, rsyslog의 imjournal 모듈을 사용하면 저널에서 직접 읽어 더 풍부한 메타데이터를 활용할 수 있습니다.

Q9: systemd-nspawn과 Docker의 차이는?

nspawn은 OS 컨테이너(전체 운영 체제 트리를 컨테이너로 실행)에 특화되어 있고, Docker는 애플리케이션 컨테이너(단일 프로세스 격리)에 특화되어 있습니다. nspawn은 컨테이너 안에서 systemd가 자연스럽게 PID 1로 실행되며, machinectl로 통합 관리됩니다. 반면 Docker는 이미지 레이어, 레지스트리, Dockerfile, 오버레이 파일시스템 등 풍부한 빌드/배포 에코시스템을 제공합니다. 프로덕션 마이크로서비스에는 Docker/Podman이, 시스템 테스트나 CI/CD에는 nspawn이 적합합니다.

Q10: sd_notify()를 사용하는 방법은?

서비스에서 Type=notify를 사용할 때, 프로세스는 sd_notify()로 systemd에 상태를 보고해야 합니다. 핵심 메시지는 다음과 같습니다.

메시지의미사용 시점
READY=1서비스 준비 완료초기화 완료 후
STOPPING=1서비스 정지 시작종료 처리 시작 시
RELOADING=1설정 재로드 중reload 처리 시작 시
WATCHDOG=1Watchdog 갱신주기적 (WatchdogSec 절반 간격)
STATUS=텍스트상태 메시지 업데이트수시 (systemctl status에 표시)
MAINPID=PID메인 PID 변경 알림프로세스 교체 시

C 라이브러리 없이도 환경 변수 $NOTIFY_SOCKET이 가리키는 유닉스 소켓에 직접 메시지를 보낼 수 있습니다. 셸 스크립트에서는 systemd-notify --ready 명령을 사용할 수 있습니다. 단, 셸에서 systemd-notify를 사용하려면 NotifyAccess=all을 설정해야 합니다. 기본값인 NotifyAccess=main에서는 메인 PID만 알림을 보낼 수 있는데, 셸에서 systemd-notify를 실행하면 자식 프로세스로 실행되기 때문입니다.

#!/bin/bash — Type=notify 셸 스크립트 예제
# 초기화 작업
setup_database
start_workers

# 준비 완료 알림
systemd-notify --ready
systemd-notify --status="Server started, accepting connections"

# 메인 루프
while true; do
    process_queue
    systemd-notify WATCHDOG=1
    sleep 10
done

systemd 트러블슈팅 체크리스트

서비스 문제 발생 시 순서대로 확인할 항목입니다.

순서확인 항목명령어
1유닛 상태 확인systemctl status myapp.service
2서비스 로그 확인journalctl -u myapp.service -b --no-pager
3유닛 파일 문법 검증systemd-analyze verify myapp.service
4적용된 설정 확인systemctl cat myapp.service
5속성값 확인systemctl show myapp.service
6의존성 확인systemctl list-dependencies myapp.service
7리소스 사용량systemctl show -p MemoryCurrent -p CPUUsageNSec myapp.service
8cgroup 이벤트cat /sys/fs/cgroup/.../memory.events
9OOM Kill 확인dmesg | grep -i oom
10코어 덤프 확인coredumpctl list myapp
11보안 설정 문제systemd-analyze security myapp.service
12수동 실행 테스트sudo -u myapp /path/to/binary

주요 systemctl status 출력 해석

systemctl status nginx.service
# ● nginx.service - A high performance web server
#      Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: enabled)
#      Active: active (running) since Mon 2026-03-23 09:00:00 KST; 2h ago
#    Main PID: 1234 (nginx)
#       Tasks: 5 (limit: 4096)
#      Memory: 48.0M (peak: 52.0M)
#         CPU: 1.234s
#      CGroup: /system.slice/nginx.service
#              ├─1234 "nginx: master process /usr/sbin/nginx"
#              ├─1235 "nginx: worker process"
#              └─1236 "nginx: worker process"
# 
# Loaded 행 해석:
#   loaded   = 유닛 파일 정상 로드됨
#   enabled  = 부팅 시 자동 시작 설정됨
#   preset: enabled = 배포판 기본값이 활성화
# 
# Active 행 가능한 값:
#   active (running)   = 프로세스 실행 중
#   active (exited)    = oneshot 완료 후 유지
#   inactive (dead)    = 중지됨
#   failed             = 비정상 종료
#   activating (start) = 시작 진행 중

systemd와 컨테이너 런타임 연동

Docker, Podman, containerd 등의 컨테이너 런타임이 systemd와 올바르게 연동되려면 cgroup v2 위임(Delegation)이 핵심입니다.

# Docker에 cgroup 위임 (드롭인 파일)
# /etc/systemd/system/docker.service.d/delegate.conf
[Service]
Delegate=yes
# Podman rootless 모드: 사용자 서비스로 관리
systemctl --user start podman.socket

# Podman 컨테이너를 systemd 서비스로 생성
podman generate systemd --new --name mycontainer > \
  ~/.config/systemd/user/mycontainer.service

# Kubernetes kubelet의 cgroup 드라이버 확인
# /var/lib/kubelet/config.yaml 에서 cgroupDriver: systemd 설정

systemd-sysext (시스템 확장, v250+)

systemd-sysext는 OS의 /usr 디렉터리를 확장 이미지로 오버레이하는 메커니즘입니다. 불변 OS(Immutable OS) 환경에서 패키지를 추가하거나 커스텀 바이너리를 배포하는 데 사용합니다.

# 확장 이미지 배치
ls /var/lib/extensions/
# myextension.raw

# 확장 목록 확인
systemd-sysext list

# 확장 적용 (오버레이 마운트)
sudo systemd-sysext merge

# 확장 해제
sudo systemd-sysext unmerge

systemd-repart (파티션 자동 관리)

systemd-repart는 부팅 시 GPT 파티션 테이블을 선언적 설정에 맞게 자동으로 조정합니다. 빈 디스크에 OS를 설치하거나, 파티션 크기를 자동으로 확장하는 데 사용합니다.

# /usr/lib/repart.d/10-root.conf
[Partition]
Type=root
Format=ext4
SizeMinBytes=5G
SizeMaxBytes=50G
MakeDirectories=/home /var

# /usr/lib/repart.d/20-swap.conf
[Partition]
Type=swap
SizeMinBytes=1G
SizeMaxBytes=4G

systemd 관련 참고 자료

자료설명
man systemd.service서비스 유닛 파일 레퍼런스
man systemd.exec실행 환경 및 보안 지시어 레퍼런스
man systemd.resource-control리소스 제어 지시어 레퍼런스
man systemd.directives모든 지시어 인덱스
systemd.io프로젝트 공식 사이트
GitHub: systemd/systemd소스 코드 및 이슈 트래커
Arch Wiki: systemd커뮤니티 위키 (실전 활용 정보 풍부)
"Rethinking PID 1" (Lennart Poettering)systemd 설계 철학 원문

systemd 도입 시 고려사항

systemd를 기반으로 서비스를 설계하거나 기존 SysVinit 스크립트를 마이그레이션할 때 고려해야 할 핵심 사항을 정리합니다.

항목SysVinit 방식systemd 방식
서비스 시작/etc/init.d/service startsystemctl start service
부팅 등록update-rc.d service defaultssystemctl enable service
로그 확인cat /var/log/syslogjournalctl -u service
PID 추적PID 파일 (/var/run/service.pid)cgroup 자동 추적
환경 설정/etc/default/serviceEnvironmentFile=/etc/default/service
데몬화셸 스크립트 + start-stop-daemon불필요 (systemd가 관리)
종료 처리수동 PID killcgroup 전체 정리

마이그레이션 체크리스트

  1. 포그라운드 실행 — 데몬화 코드를 제거하고 전경에서 실행하도록 수정 (Type=simple 또는 Type=notify)
  2. PID 파일 제거 — systemd가 cgroup으로 추적하므로 PID 파일 불필요 (단, Type=forkingPIDFile= 사용 가능)
  3. 로그 출력 변경 — 파일 로깅 대신 stdout/stderr 출력으로 변경 (journald가 수집)
  4. sd_notify() 통합 — 초기화 완료 알림으로 정확한 상태 추적
  5. 리소스 제한 추가 — MemoryMax, CPUQuota 등 리소스 가드레일 설정
  6. 보안 강화 — ProtectSystem, NoNewPrivileges 등 기본 보안 적용
  7. 의존성 정의 — After=, Requires= 로 명시적 순서/의존성 선언

systemd 비사용 환경과의 호환성

systemd에 의존하는 서비스를 systemd가 없는 환경(Alpine Linux, Gentoo OpenRC 등)에서도 실행해야 하는 경우, 다음 전략을 고려합니다.

/* systemd 환경 감지 패턴 */
#ifdef HAVE_SYSTEMD
#include <systemd/sd-daemon.h>

static int use_systemd(void) {
    return getenv("NOTIFY_SOCKET") != NULL;
}

static void notify_ready(void) {
    if (use_systemd())
        sd_notify(0, "READY=1");
}
#else
static void notify_ready(void) { /* no-op */ }
#endif

systemd에서 자주 발생하는 실수

실수증상해결
Type 불일치서비스 시작 타임아웃프로세스 동작에 맞는 Type 선택
daemon-reload 누락유닛 파일 변경이 반영 안 됨변경 후 systemctl daemon-reload
After= 없이 Requires=만의존 서비스 시작 전에 본 서비스 시작After= + Requires= 함께 사용
EnvironmentFile 미존재서비스 시작 실패EnvironmentFile=-/path (- 접두사)
ExecStart에 셸 문법 사용파이프, 리다이렉션 동작 안 함ExecStart=/bin/bash -c "cmd1 | cmd2"
RemainAfterExit 누락oneshot 서비스가 즉시 inactiveRemainAfterExit=yes 추가
User= 지정 후 권한 부족바인드, 파일 접근 실패Capability 추가 또는 디렉터리 권한 조정
너무 제한적인 SystemCallFilter서비스 SIGSYS로 사망SystemCallErrorNumber=EPERM 설정 후 로그로 필요 콜 파악

참고 링크

systemd와 관련된 다른 리눅스 커널 문서를 참고하여 더 깊이 이해할 수 있습니다. systemd는 커널의 cgroup, 네임스페이스, seccomp, 부팅 프로세스 등 다양한 하위 시스템과 밀접하게 연동되므로, 각 주제에 대한 상세 설명은 개별 문서를 참조하세요.