systemd 가이드
systemd의 PID 1 설계 철학, Unit 시스템, 부팅 순서, 서비스 관리, 커널 인터페이스 통합, 주요 구성 요소(journald, udevd, networkd, resolved, logind, oomd, nspawn), 리소스 제어, 보안 강화, 디버깅 기법까지 리눅스 사용자 공간(User Space) 초기화의 핵심을 심층 분석합니다.
핵심 요약
- PID 1 — 커널이 실행하는 최초 사용자 프로세스, 모든 고아 프로세스의 부모
- Unit 시스템 — 서비스, 소켓, 타이머 등 12가지 유닛 타입의 의존성 그래프(DAG) 기반 관리
- 병렬 부팅 — 소켓/D-Bus/장치 활성화로 의존성 대기 없이 병렬 시작
- cgroup v2 통합 — 모든 서비스에 자동 cgroup 할당, 리소스 제어와 감시
- 에코시스템 — journald, udevd, networkd, resolved, logind, oomd 등 통합 구성 요소
단계별 이해
- PID 1 역할 이해
커널에서 사용자 공간으로의 전환점과 init 프로세스의 책임을 파악합니다. - Unit 의존성 학습
서비스 간 Requires/Wants/After 관계로 부팅 순서가 결정되는 원리를 이해합니다. - 서비스 관리 실습
systemctl 명령어로 서비스 상태 조회, 시작, 중지, 활성화를 실습합니다. - 디버깅 도구 활용
journalctl, systemd-analyze로 문제 진단과 부팅 최적화를 수행합니다.
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은 특별한 책임을 갖습니다.
- 고아 프로세스(Orphan Process) 수거 — 부모가 종료된 프로세스는 PID 1에 재배정(reparenting)되며, PID 1이
waitpid()로 수거하지 않으면 좀비(Zombie) 프로세스가 누적됩니다. - 시그널 처리 특수성 — PID 1은 명시적으로 핸들러를 등록하지 않은 시그널을 무시합니다. 따라서
kill -9 1은 커널이 차단하며, PID 1 종료는 곧 시스템 패닉(Panic)을 의미합니다. - 마운트 네임스페이스(Mount Namespace) 루트 — 초기 마운트 테이블의 소유자이며,
/proc,/sys,/dev등 가상 파일시스템(Virtual Filesystem)을 마운트합니다. - cgroup 트리 초기화 — systemd는 부팅 직후 cgroup v2 통합 계층(Unified Hierarchy)을 마운트하고, 모든 서비스에 개별 cgroup을 할당합니다.
SysVinit/Upstart 비교
| 항목 | SysVinit | Upstart | systemd |
|---|---|---|---|
| 부팅 모델 | 순차(Sequential) | 이벤트(Event) | 의존성 그래프(DAG) |
| 병렬 시작 | 제한적 | 이벤트 기반 부분 병렬 | 소켓/D-Bus 활성화로 완전 병렬 |
| 서비스 감시 | PID 파일 기반 | expect fork/daemon | cgroup 기반 정확한 추적 |
| 의존성 표현 | LSB 헤더(Header) 숫자 순서 | start on/stop on | Requires/Wants/After/Before |
| 소켓 활성화 | inetd 별도 | 미지원 | 네이티브 지원 |
| 로그 | syslog 텍스트 | syslog 텍스트 | journald 바이너리 구조화 |
| 리소스 제어 | ulimit 수동 | 제한적 cgroup | cgroup v2 네이티브 통합 |
| 설정 형식 | 셸 스크립트 | Upstart job 파일 | 선언적 INI 형식 유닛 파일 |
설계 원칙
systemd의 핵심 설계 원칙은 다음과 같습니다.
- 의존성 기반 병렬화(Dependency-based Parallelization) — 유닛 간 의존성을 DAG(Directed Acyclic Graph)로 모델링하고, 의존성이 충족된 유닛은 즉시 병렬로 시작합니다.
- 소켓 활성화(Socket Activation) — 서비스가 사용할 소켓을 systemd가 미리 열어 두면, 실제 연결이 들어올 때까지 서비스 프로세스 시작을 지연할 수 있습니다. 이는 서비스 간 순서 의존성을 제거하는 핵심 메커니즘입니다.
- D-Bus 활성화(D-Bus Activation) — D-Bus 버스 이름을 요청하는 클라이언트가 있을 때 해당 서비스를 자동으로 시작합니다.
- 장치 활성화(Device Activation) — 특정 하드웨어 장치가 연결되면 관련 서비스를 자동으로 시작합니다. udev 이벤트와 연동됩니다.
- 경로 활성화(Path Activation) — 특정 파일이나 디렉터리의 변경을 inotify로 감시하여 서비스를 시작합니다.
- cgroup 기반 프로세스 추적 — PID 파일에 의존하지 않고, 각 서비스의 모든 프로세스를 cgroup으로 정확히 추적합니다. fork 폭탄(Fork Bomb)이나 데몬화(Daemonization) 과정에서도 프로세스를 놓치지 않습니다.
- 선언적 설정 — 셸 스크립트 대신 선언적 INI 형식의 유닛 파일을 사용하여 파싱이 쉽고 검증이 가능합니다.
에코시스템 구성 요소
| 구성 요소 | 역할 | 커널 인터페이스 |
|---|---|---|
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-resolved | DNS 리졸버(Resolver) | AF_INET/AF_INET6 sockets |
systemd-oomd | PSI 기반 OOM 관리 | cgroup v2 memory.pressure |
systemd-timedated | 시간/시간대(Timezone) 관리 | settimeofday, adjtimex |
systemd-tmpfiles | 임시 파일/디렉터리 관리 | 파일시스템 API |
systemd-sysctl | 커널 파라미터 설정 | /proc/sys |
systemd-boot | UEFI 부트로더 | EFI Boot Services |
systemd-nspawn | 경량 컨테이너(Container) | namespaces, cgroups, seccomp |
systemd-homed | 휴대용 홈 디렉터리(Home Directory) | LUKS, FUSE, btrfs |
systemd의 역사와 채택
systemd는 2010년 4월 Lennart Poettering이 "Rethinking PID 1"이라는 블로그 포스트에서 설계 철학을 공개하면서 시작되었습니다. 기존 SysVinit의 순차적 셸 스크립트 모델과 Upstart의 이벤트 기반 모델의 한계를 분석하고, macOS의 launchd에서 영감을 받은 새로운 init 시스템을 제안했습니다.
- 2010년 — Fedora 15에 최초 도입
- 2011년 — openSUSE, Arch Linux 채택
- 2014년 — Debian Technical Committee에서 systemd 채택 결정
- 2015년 — Ubuntu가 Upstart에서 systemd로 전환 (15.04)
- 2015년 — RHEL 7에서 공식 채택
- 현재 — Debian, Ubuntu, Fedora, RHEL, SUSE, Arch 등 대부분의 주요 배포판이 기본 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)로 처리합니다.
- I/O 이벤트 — 소켓 연결, D-Bus 메시지 수신, 저널 로그 수신
- 시그널 이벤트 — SIGCHLD(자식 종료), SIGTERM(종료 요청), SIGHUP(재로드)
- 타이머 이벤트 — 유닛 타이머, watchdog, 주기적 정리 작업
- 파일시스템 이벤트 — cgroup 이벤트, 마운트 변경, 유닛 파일 변경
이 단일 루프 설계 덕분에 PID 1은 락(Lock) 없이 동작하며, 데드락(Deadlock) 위험이 없습니다. 다만 이벤트 핸들러가 블로킹(Blocking)되면 전체 시스템 관리가 멈출 수 있으므로, 시간이 걸리는 작업은 별도 워커(Worker) 프로세스에서 처리합니다.
D-Bus 통신
systemd의 모든 제어 인터페이스는 D-Bus(Desktop Bus) 프로토콜을 통해 노출됩니다. systemctl이 systemctl 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 등이 대상 프로세스에 유출되지 않습니다.
| 항목 | sudo | run0 |
|---|---|---|
| 동작 방식 | 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은 자동으로 터미널 배경색을 빨간 계열로 변경하여
# 루트 세션임을 시각적으로 알림
Unit 시스템
systemd의 관리 대상은 모두 유닛(Unit)이라는 추상 단위로 표현됩니다. 유닛 파일은 선언적 INI 형식으로 작성되며, systemd는 이 파일들의 의존성 관계를 DAG(Directed Acyclic Graph)로 구성하여 시작/정지 순서를 결정합니다. 유닛은 시스템 리소스의 논리적 표현이며, 하나의 유닛은 하나의 관리 대상에 대응합니다.
유닛의 생명주기(Lifecycle)는 다음과 같습니다.
- 로드(Load) — 유닛 파일을 읽어 메모리에 적재.
systemctl daemon-reload로 재로드 - 의존성 해석(Dependency Resolution) — Requires, Wants, After 등의 관계를 DAG로 구성
- 활성화(Activation) — 의존성이 충족되면 유닛 시작. 타입별로 시작 완료 판단 기준이 다름
- 실행(Running) — 서비스 프로세스 실행 중. cgroup으로 추적
- 비활성화(Deactivation) — 중지 요청 시 ExecStop 실행 후 프로세스 종료
- 정리(Cleanup) — cgroup 제거, 임시 파일 정리
12가지 유닛 타입
| 유닛 타입 | 확장자 | 설명 | 예시 |
|---|---|---|---|
| 서비스(Service) | .service | 데몬 프로세스 관리 | nginx.service |
| 소켓(Socket) | .socket | IPC/네트워크 소켓 관리, 활성화 트리거 | cups.socket |
| 타이머(Timer) | .timer | 주기적/일정 기반 서비스 실행 (cron 대체) | logrotate.timer |
| 경로(Path) | .path | 파일/디렉터리 변경 감시 (inotify) | cups.path |
| 장치(Device) | .device | udev 장치 노출 | dev-sda1.device |
| 마운트(Mount) | .mount | 파일시스템 마운트 포인트 | home.mount |
| 자동 마운트(Automount) | .automount | 접근 시 자동 마운트 | home.automount |
| 스왑(Swap) | .swap | 스왑 영역 관리 | dev-sda2.swap |
| 스코프(Scope) | .scope | 외부 생성 프로세스 그룹 (API 전용) | session-1.scope |
| 슬라이스(Slice) | .slice | cgroup 계층 노드 (리소스 분배) | 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.target과 Requires=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
유닛 인스턴스화 (템플릿 유닛)
반복적인 유닛 설정을 효율적으로 관리하기 위해 템플릿 유닛(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 |
%m | Machine ID | abc123... |
%b | Boot ID | def456... |
%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.target | 0 | 시스템 종료 |
rescue.target | 1 (single) | 단일 사용자 모드, 기본 파일시스템만 마운트 |
multi-user.target | 3 | 다중 사용자 모드, 네트워크 활성화, GUI 없음 |
graphical.target | 5 | GUI 환경(Display Manager 포함) |
reboot.target | 6 | 시스템 재부팅 |
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-generator | GPT 파티션 타입 GUID | 자동 마운트 유닛 |
systemd-cryptsetup-generator | /etc/crypttab | systemd-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.target | systemd-tmpfiles-setup-dev-early | /dev 내 최소 노드 생성 |
local-fs.target | *.mount, systemd-fsck@ | 로컬 파일시스템 마운트, fsck 실행 |
sysinit.target | systemd-journald, systemd-udevd, systemd-tmpfiles-setup, systemd-sysctl | 저널링, 장치 관리, 임시 파일, 커널 파라미터 |
basic.target | systemd-logind, dbus, systemd-resolved | 기본 시스템 인프라 (D-Bus, 로그인, DNS) |
network.target | systemd-networkd, NetworkManager | 네트워크 인터페이스 구성 |
network-online.target | systemd-networkd-wait-online | 네트워크 연결 완료 대기 |
multi-user.target | sshd, nginx, postgresql 등 | 사용자 서비스, 데몬 |
graphical.target | display-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-reboot | kexec |
|---|---|---|---|
| 커널 재시작 | 예 (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로 기록됨
서비스 관리 (systemctl)
systemctl은 systemd의 주요 제어 인터페이스(CLI)입니다. 내부적으로 D-Bus를 통해 PID 1에 명령을 전달합니다. 서비스의 시작, 중지, 상태 조회, 활성화/비활성화, 마스킹(Masking) 등 모든 관리 작업을 수행합니다. systemctl 없이 직접 D-Bus 메시지를 보내거나 busctl을 사용해도 동일한 작업이 가능하지만, systemctl이 가장 간편합니다.
.service 확장자는 생략할 수 있습니다. systemctl start nginx와 systemctl 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는 부모 프로세스가 종료된 것을 서비스 실패로 판단합니다.
- 서비스가 전경(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)에서 실행되는 데몬 |
exec | ExecStart 바이너리 exec() 성공 후 | simple과 유사, exec 실패 감지 개선 |
forking | 부모 프로세스 종료 후 (fork → exit 패턴) | 전통적 데몬 (PIDFile= 함께 사용) |
oneshot | ExecStart 프로세스 종료 후 | 일회성 초기화 스크립트 |
dbus | D-Bus 버스 이름 획득 후 | D-Bus 서비스 (BusName= 필수) |
notify | sd_notify(READY=1) 수신 후 | 준비 상태를 명시적으로 알리는 데몬 |
idle | simple과 동일 (단, 다른 작업 완료 후 시작) | 콘솔 출력이 필요한 서비스 |
Restart 정책
서비스 프로세스가 종료되었을 때 systemd의 재시작 동작을 제어합니다.
| Restart 값 | 재시작 조건 |
|---|---|
no | 재시작하지 않음 (기본값) |
on-success | 정상 종료(exit code 0) 시에만 |
on-failure | 비정상 종료, 시그널, 타임아웃 시 |
on-abnormal | 시그널, 타임아웃, watchdog 시 |
on-watchdog | watchdog 타임아웃 시에만 |
on-abort | 시그널에 의한 종료 시에만 |
always | 모든 종료 상황에서 재시작 |
[Service]
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=300
StartLimitBurst=5
위 설정은 서비스가 비정상 종료되면 5초 후 재시작하되, 300초(5분) 안에 5회 이상 재시작 시도가 발생하면 더 이상 재시작하지 않습니다. 재시작 제한에 도달하면 서비스는 failed 상태가 되며, systemctl reset-failed로 재설정할 수 있습니다.
Restart=always를 사용할 때는 반드시 StartLimitIntervalSec과 StartLimitBurst를 함께 설정하세요. 설정 오류로 서비스가 즉시 종료되는 경우, 제한 없이 재시작을 반복하면 시스템 리소스를 소진할 수 있습니다.
서비스 종료 동작
systemd가 서비스를 중지할 때의 프로세스는 다음과 같습니다.
ExecStop=명령 실행 (있는 경우)- 메인 프로세스에
KillSignal=전송 (기본:SIGTERM) TimeoutStopSec=대기 (기본: 90초)- 타임아웃 후
FinalKillSignal=전송 (기본:SIGKILL) - cgroup 내 모든 잔여 프로세스에
SIGKILL전송 (SendSIGKILL=yes시) 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;
}
부팅 성능 분석 실전
부팅 시간이 느린 시스템을 진단하는 구체적인 절차를 살펴봅니다.
# 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.target | initramfs 단계의 기본 타깃 |
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 v2 | 4.15+ | 리소스 제어, 프로세스 추적 |
| PSI (Pressure Stall Information) | 4.20+ | systemd-oomd |
| pidfd | 5.3+ | 프로세스 관리 개선 |
| BPF (cgroup 연동) | 4.15+ | IP 필터링, 장치 접근 제어 |
| io_uring | 5.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 관리 흐름은 다음과 같습니다.
- 부팅 시 —
/sys/fs/cgroup에 cgroup2 마운트, 루트 cgroup에서 컨트롤러 위임 - 유닛 시작 시 — 유닛 타입에 따라 적절한 cgroup 생성 (service →
/system.slice/유닛이름) - 프로세스 배치 — fork한 프로세스를 cgroup의
cgroup.procs에 기록 - 리소스 제한 적용 — 유닛 파일의 리소스 지시어를 cgroup 컨트롤러 파일에 기록
- 유닛 종료 시 — cgroup 내 잔여 프로세스 정리 후 cgroup 삭제
- 단일 작성자 규칙(Single Writer Rule) — cgroup 트리는 계층적으로 관리되며, 각 cgroup의 속성은 단일 관리자만 수정해야 합니다. systemd가 최상위 관리자이며, 하위 트리를 컨테이너 런타임 등에 위임(Delegation)할 수 있습니다.
- 위임(Delegation) —
Delegate=yes설정으로 특정 서비스에 하위 cgroup 트리 관리 권한을 부여합니다. 컨테이너 런타임(Docker, Podman)이 대표적인 위임 대상입니다. - 커널 부트 파라미터 —
systemd.unified_cgroup_hierarchy=1로 cgroup v2 전용 모드를 강제할 수 있습니다.
# 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 NS | ProtectSystem=, ProtectHome=, PrivateTmp= | 파일시스템 뷰 |
| PID NS | PrivatePIDs= (실험적) | 프로세스 ID 공간 |
| Network NS | PrivateNetwork= | 네트워크 인터페이스 |
| User NS | PrivateUsers= | UID/GID 매핑 |
| UTS NS | ProtectHostname= | 호스트명 |
| IPC NS | PrivateIPC= | 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_SERVICE | 1024 미만 포트 바인드 | nginx, Apache (80/443) |
CAP_NET_RAW | RAW 소켓, ICMP | ping, tcpdump |
CAP_NET_ADMIN | 네트워크 설정 변경 | networkd, iptables |
CAP_SYS_ADMIN | 다양한 관리 작업 (과도하게 넓음) | 마운트, cgroup 관리 |
CAP_DAC_READ_SEARCH | 파일 읽기 권한 검사 우회 | 백업 서비스 |
CAP_CHOWN | 파일 소유자 변경 | 파일 서버 |
CAP_SETUID/CAP_SETGID | UID/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 | 이벤트 루프 핵심 | 소켓, 시그널, 타이머 감시 |
signalfd | SIGCHLD 등 시그널 수신 | 자식 프로세스 종료 감지 |
timerfd | 타이머 유닛 구현 | monotonic/realtime 타이머 |
inotify | 경로(Path) 유닛 구현 | 파일/디렉터리 변경 감시 |
netlink | udevd, networkd | 커널 이벤트(uevent, route) 수신 |
/proc/self/mountinfo | 마운트 유닛 감시 | 마운트 이벤트 추적 |
/sys/fs/cgroup | 리소스 제어 | CPU/메모리/IO 제한 |
/dev/kmsg | journald 커널 로그 수집 | 커널 메시지 읽기 |
리소스 위임(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=1 | cgroup v2 전용 모드 강제 |
systemd.log_level=debug | PID 1 로그 레벨 설정 |
systemd.log_target=journal | PID 1 로그 출력 대상 |
systemd.unit=rescue.target | 기본 타깃 오버라이드 |
systemd.mask=unit | 특정 유닛 부팅 시 마스킹 |
systemd.wants=unit | 부팅 시 추가 유닛 요청 |
systemd.debug_shell=1 | early-boot 디버그 셸 (/dev/tty9) |
rd.systemd.unit= | initramfs 내 기본 타깃 |
systemd.setenv=KEY=VALUE | PID 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= | SMACK | SMACK 레이블 지정 |
[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-boot | GRUB2 |
|---|---|---|
| 펌웨어 지원 | UEFI 전용 | BIOS + UEFI |
| 설정 형식 | INI (BLS 표준) | grub.cfg (스크립트) |
| 파일시스템 | ESP(FAT32)만 | ext4/Btrfs/XFS/ZFS 등 |
| 네트워크 부팅 | 미지원 | 지원 (HTTP/TFTP) |
| 암호화 지원 | LUKS 비지원 (UKI로 우회) | LUKS1 직접 지원 |
| UKI 지원 | 네이티브 | 제한적 |
| Secure Boot | 네이티브 + shim | shim 필요 |
| 부트 메뉴 | 최소 (텍스트) | 커스터마이징 가능 (테마) |
| 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
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
원격 전송
분산 환경에서 여러 서버의 로그를 중앙으로 수집하기 위해 systemd는 세 가지 컴포넌트를 제공합니다.
| 컴포넌트 | 역할 | 프로토콜 |
|---|---|---|
systemd-journal-upload | 로컬 저널을 원격으로 전송 (Push) | HTTPS POST |
systemd-journal-remote | 원격 저널 데이터 수신 (Pull/Accept) | HTTPS |
systemd-journal-gatewayd | HTTP/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_DATA | 1 | 단일 FIELD=VALUE 쌍 저장. 해시값과 역참조 엔트리 배열 오프셋 포함 |
OBJECT_FIELD | 2 | 필드 이름만 저장 (예: MESSAGE, _PID). 필드 해시 테이블에서 참조 |
OBJECT_ENTRY | 3 | 하나의 로그 항목. realtime/monotonic 타임스탬프, boot ID, seqnum, Data Object 오프셋 배열 |
OBJECT_DATA_HASH_TABLE | 4 | Data Object 조회용 해시 테이블. 파일 생성 시 크기 고정 |
OBJECT_FIELD_HASH_TABLE | 5 | Field Object 조회용 해시 테이블. 파일 생성 시 크기 고정 |
OBJECT_ENTRY_ARRAY | 6 | Entry 오프셋의 연결 리스트. 순차 탐색과 이진 검색 지원 |
OBJECT_TAG | 7 | FSS 봉인 태그. 에포크별 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 순서를 따릅니다.
- Data Hash Table →
FIELD=VALUE의 해시로 Data Object를 O(1) 조회 - Data Object → 이 데이터를 포함하는 모든 Entry의 오프셋 배열(Entry Array) 역참조
- Entry Object → 이 로그 항목을 구성하는 모든 Data Object 오프셋 배열 포함
- Entry Array → Entry 오프셋의 연결 리스트, 시간순 정렬로 이진 검색 가능
- Field Hash Table → 필드 이름으로 Field Object를 조회, 필드 열거에 사용
커널 로그 수집 메커니즘
journald는 커널 로그를 /dev/kmsg 캐릭터 디바이스를 통해 수집합니다. 이 디바이스는 커널의 printk 링 버퍼(log_buf)에 대한 사용자 공간 인터페이스로, journald는 이를 read()와 poll()로 지속적으로 모니터링합니다.
부팅 시 로그 수집 타임라인
- 커널 부팅 —
printk()호출이 커널 링 버퍼(log_buf)에 메시지 축적 - initramfs/initrd — 초기 사용자 공간. 커널 메시지는 여전히 링 버퍼에만 존재
- systemd PID 1 시작 —
systemd-journald.service가 초기에 실행됨 - journald 시작 —
/dev/kmsg를 열어 축적된 모든 커널 메시지를 한 번에 읽음 - 정상 운영 —
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 | 의미 |
|---|---|---|---|
| 0 | KERN_EMERG | 0 (emerg) | 시스템 사용 불가 |
| 1 | KERN_ALERT | 1 (alert) | 즉시 조치 필요 |
| 2 | KERN_CRIT | 2 (crit) | 치명적 조건 |
| 3 | KERN_ERR | 3 (err) | 오류 조건 |
| 4 | KERN_WARNING | 4 (warning) | 경고 조건 |
| 5 | KERN_NOTICE | 5 (notice) | 정상이지만 중요 |
| 6 | KERN_INFO | 6 (info) | 정보 메시지 |
| 7 | KERN_DEBUG | 7 (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.service | systemd-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 |
| 조회 | journalctl | journalctl --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
저널 트러블슈팅
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의 핵심 역할을 정리하면 다음과 같습니다.
- 장치 노드 관리 — 커널의 devtmpfs에 생성된 장치 노드에 권한(MODE), 소유자(OWNER/GROUP)를 설정
- 심볼릭 링크 생성 —
/dev/disk/by-id/,/dev/disk/by-uuid/등 안정적 경로 제공 - 환경 설정 — 장치별 환경 변수(ENV) 설정으로 다른 서비스에 정보 전달
- 프로그램 실행 — 장치 이벤트 시 외부 프로그램(RUN) 실행
- systemd 연동 — TAG+="systemd"로 .device 유닛 자동 생성, 장치 기반 서비스 활성화
- 네트워크 인터페이스 명명 — Predictable Network Interface Names 규칙 적용
커널 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
Predictable Network Interface Names
systemd-udevd는 네트워크 인터페이스에 예측 가능한 이름(Predictable Names)을 부여합니다. 전통적인 eth0, wlan0 방식은 하드웨어 검색 순서에 따라 이름이 바뀔 수 있어 안정성이 떨어집니다.
| 접두사 | 명명 규칙 | 예시 | 설명 |
|---|---|---|---|
en | 이더넷 | eno1 | 온보드(Onboard) 인덱스 |
en | 이더넷 | enp3s0 | PCI 버스 + 슬롯 |
en | 이더넷 | enx001122334455 | MAC 주소 기반 |
wl | 무선 LAN | wlp2s0 | PCI 버스 + 슬롯 |
ww | WWAN | wwp0s29f7u2i3 | USB 장치 경로 |
# 현재 네트워크 인터페이스 명명 정보
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-networkd | NetworkManager |
|---|---|---|
| 설계 목표 | 서버/컨테이너 | 데스크탑/노트북 |
| 설정 방식 | 선언적 INI 파일 | D-Bus API + 다양한 프론트엔드 |
| Wi-Fi 지원 | 제한적 (iwd 연동) | wpa_supplicant 통합 |
| VPN 플러그인 | WireGuard만 네이티브 | OpenVPN, IPsec 등 다수 |
| Hotplug | 기본 지원 | 고급 지원 (Dispatcher) |
| GUI | 없음 (networkctl CLI) | nmtui, nm-applet, GNOME 설정 |
| 메모리 | 낮음 (~5MB) | 높음 (~30MB) |
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.53 | resolved를 통한 모든 DNS 질의 |
| 정적 모드 | nameserver 업스트림IP | resolved 우회, 직접 업스트림 질의 |
| 호환 모드 | 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.conf | 127.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
| 모듈 | 역할 |
|---|---|
mymachines | systemd-machined에 등록된 컨테이너/VM 이름 해석 |
resolve | systemd-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
/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가 관리하는 핵심 기능은 다음과 같습니다.
- 세션 추적 — 모든 로그인 세션(SSH, 콘솔, GUI)을 cgroup(scope)으로 정확히 추적
- 좌석 관리 — 멀티시트(Multi-Seat) 환경에서 입력/출력 장치를 좌석별로 분리
- 전원 관리 — 종료, 재부팅, 절전, 최대 절전의 접근 제어 (polkit 연동)
- inhibitor locks — 특정 작업(예: 백업) 중 종료/절전 방지
- 사용자 리소스 관리 — 로그인/로그아웃 시 cgroup 할당/해제
- VT(Virtual Terminal) 전환 — Ctrl+Alt+F1~F12 가상 터미널 관리
- Linger 관리 — 로그아웃 후에도 사용자 서비스 유지 여부 제어
세션/좌석/사용자 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초) 지연시킵니다.
| 시나리오 | 차단 대상 | 모드 | 명령 예제 |
|---|---|---|---|
| 백업 진행 중 | shutdown | block | systemd-inhibit --what=shutdown --mode=block rsync ... |
| 프레젠테이션 중 | idle:sleep:handle-lid-switch | block | 화면 꺼짐/절전/덮개 닫기 방지 |
| CD 굽기 | shutdown:sleep | block | 굽기 중 시스템 상태 변경 방지 |
| 세션 관리자 | shutdown | delay | 종료 전 저장 유도 (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_ID | 1 | 세션 고유 번호 |
XDG_SESSION_TYPE | tty, x11, wayland | 세션 유형 |
XDG_SESSION_CLASS | user, greeter | 세션 클래스 |
XDG_SEAT | seat0 | 할당된 좌석 |
XDG_VTNR | 1 | 가상 터미널 번호 |
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 Killer | systemd-oomd |
|---|---|---|
| 동작 시점 | alloc 실패 시 (매우 늦음) | PSI 압력 임계값 도달 시 (선제적) |
| 동작 계층 | 커널 내부 | 사용자 공간 |
| Kill 대상 선택 | oom_score 기반 프로세스 | memory.current 기반 cgroup |
| 설정 방법 | /proc/PID/oom_score_adj | 유닛 파일 ManagedOOM* 지시어 |
| 로깅 | dmesg | journald |
일반적으로 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-oomd | earlyoom | 커널 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, omit | avoid: 가능하면 제외, 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이 설정되어 있는지 확인합니다.
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.service | udev 초기화 직후 | /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.service가 sysinit.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.service | sysinit.target 직전 (매우 이른 시점) | 글로벌 커널 파라미터 (vm.*, kernel.*, fs.*) |
systemd-networkd | network-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-sysctl | sysctl.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 # 값 확인
드롭인 충돌 해결
여러 패키지가 같은 파라미터를 서로 다른 값으로 설정할 때 충돌이 발생합니다. 해결 원칙은 다음과 같습니다.
- 파일명 순서 — 같은 디렉터리 내에서 파일명 알파벳순으로 마지막에 읽힌 값이 승리합니다.
99-custom.conf는50-default.conf를 오버라이드합니다. - 경로 우선순위 —
/etc/가/usr/lib/보다 우선합니다. 배포판 기본값을 오버라이드하려면 같은 파일명으로/etc/sysctl.d/에 생성하면 됩니다. - 무효화 — 배포판 설정을 완전히 무시하려면
/etc/sysctl.d/에 같은 이름의 빈 파일을 생성합니다 (예:touch /etc/sysctl.d/50-default.conf).
# 충돌 파라미터 추적
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.max | CPU 시간 상한 비율 (예: 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
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:00 | 1월, 7월 1일 | 0 0 1 1,7 * |
Sat *-*-1..7 00:00:00 | 매월 첫 번째 토요일 | (복잡한 스크립트 필요) |
*-*-* 02:00:00 UTC | UTC 기준 새벽 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
cron 대체 비교
| 항목 | cron | systemd 타이머 |
|---|---|---|
| 로그 | 메일 또는 별도 설정 | journalctl 통합 |
| 의존성 | 없음 | After=, Requires= 지원 |
| 리소스 제한 | 없음 | cgroup 기반 제어 가능 |
| 놓친 실행 | anacron 별도 | Persistent= 내장 |
| 무작위 지연 | 수동 sleep | RandomizedDelaySec= 내장 |
| 정밀도 | 분 단위 | 마이크로초 단위 (AccuracySec) |
| 보안 | 셸 실행 | 전체 서비스 보안 강화 적용 가능 |
# 활성 타이머 목록
systemctl list-timers --all
# 타이머 활성화
sudo systemctl enable --now backup.timer
소켓 활성화(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
보안 강화(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=yes | setuid/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=yes | W+X 메모리 매핑 방지 (JIT 차단) |
LockPersonality=yes | 실행 도메인(Personality) 변경 방지 |
RestrictSUIDSGID=yes | setuid/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: 위험
보안 강화 실전 적용 절차
새 서비스에 보안을 적용하는 권장 절차는 다음과 같습니다.
- 기본 보안 측정 —
systemd-analyze security myservice.service로 현재 점수 확인 - 기본 격리 적용 —
ProtectSystem=strict,ProtectHome=yes,PrivateTmp=yes,NoNewPrivileges=yes추가 - 필요한 쓰기 경로 열기 —
ReadWritePaths=로 서비스가 쓰는 경로만 허용 - Capability 최소화 —
CapabilityBoundingSet=에 필요한 능력만 나열 - Seccomp 적용 —
SystemCallFilter=@system-service부터 시작, 필요하면 그룹 추가 - 재측정 및 반복 — 점수가 원하는 수준이 될 때까지 반복
# 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-veth | veth 쌍 생성 (호스트-컨테이너 연결) | 기본 격리 네트워크 |
--network-bridge=br0 | veth를 호스트 브리지에 연결 | LAN 직접 접속 |
--network-macvlan=eth0 | macvlan 인터페이스 생성 | 별도 MAC 주소 할당 |
--network-zone=myzone | 같은 zone 컨테이너끼리 L2 연결 | 컨테이너 간 통신 |
--private-network | 격리된 네트워크 (loopback만) | 완전 네트워크 격리 |
nspawn vs Docker/Podman 비교
| 항목 | systemd-nspawn | Docker/Podman |
|---|---|---|
| 이미지 형식 | 디렉터리 / raw 이미지 | OCI/Docker 이미지 레이어 |
| 레지스트리 | 없음 (수동 관리) | Docker Hub, 사설 레지스트리 |
| 오버레이 FS | 없음 (--overlay 옵션) | overlay2 기본 |
| init 시스템 | 컨테이너 내 systemd 자연 실행 | 보통 PID 1 = 애플리케이션 |
| cgroup 위임 | 네이티브 (machine.slice) | shim 계층 필요 |
| machinectl 통합 | 네이티브 | 미지원 |
| Kubernetes 연동 | 미지원 | CRI 준수 |
| 주 용도 | OS 컨테이너, 테스트, CI/CD | 애플리케이션 컨테이너, 프로덕션 |
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 유닛 파일 작성의 실전 패턴을 다섯 가지 시나리오로 정리합니다. 각 예제는 실제 프로덕션(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에서 도입된 run0는 sudo의 대안입니다. 기존 sudo는 setuid 바이너리로 권한 상승하지만, run0는 systemd에 요청하여 새로운 서비스 유닛으로 명령을 실행합니다. 이를 통해 setuid 바이너리의 보안 위험을 제거하고, 모든 실행이 journald에 기록됩니다.
# sudo와 동일한 사용법
run0 apt update
run0 -u postgres psql
# 실행은 별도 서비스 유닛으로 처리
# 모든 명령이 journald에 로깅됨
주요 버전별 변경사항
systemd는 약 4~6개월 주기로 주요 버전을 릴리스하며, 매 버전마다 커널 연동 기능이 확대되어 왔습니다. 새 버전은 일반적으로 최신 리눅스 커널의 새 기능을 활용하는 방향으로 발전합니다. 다음은 핵심 기능이 도입된 주요 버전입니다.
| 버전 | 시기 | 주요 기능 |
|---|---|---|
| v1 | 2010-03 | 최초 릴리스. Lennart Poettering이 PID 1 재설계 발표. 소켓 활성화, cgroup 기반 프로세스 추적 도입 |
| v38 | 2011-07 | Fedora 15 기본 init으로 채택 — systemd를 기본 init으로 채택한 최초의 주요 배포판 |
| v44 | 2012-01 | journald 도입, 바이너리 구조화 로그 형식 최초 구현. 기존 syslog 호환 유지 |
| v183 | 2013-02 | journald 안정화, 바이너리 저널 형식 확립. 대규모 배포판 채택 가속 |
| v197 | 2013-04 | udevd가 systemd 프로젝트로 통합. 기존 udev 단독 프로젝트에서 합병 |
| v205 | 2013-12 | 소켓 활성화 안정화, 부팅 병렬화 완성. KDBUS 실험 시작 (이후 폐기) |
| v208 | 2014-02 | networkd 초기 구현 — 네트워크 인터페이스 관리 데몬 도입 |
| v213 | 2014-07 | resolved 도입 — DNS 해석 데몬. 로컬 캐싱, mDNS/LLMNR 통합 |
| v215 | 2014-09 | Debian 8(Jessie)용 안정화. TimerSlackNSec, KillMode=mixed 도입 |
| v220 | 2015-05 | networkd/resolved 안정화, DNS 관리 통합. 첫 LTS 배포판(Ubuntu 16.04)에서 채택 |
| v226 | 2015-10 | cgroup v2 최초 지원 (실험적). unified hierarchy 인식, 커널 4.2+ 필요 |
| v229 | 2016-03 | Type=exec 도입 — exec() 실패를 즉시 감지하여 서비스 시작 실패 보고 개선 |
| v230 | 2016-05 | RestartKillSignal=, WatchdogSignal= 추가. 서비스 종료 신호 세밀 제어 |
| v232 | 2017-03 | DynamicUser= 도입 — 서비스별 동적 UID/GID 할당, StateDirectory/CacheDirectory 자동 관리 |
| v233 | 2017-06 | PrivateUsers=, ProtectKernelModules=, ProtectKernelTunables= 등 샌드박싱 강화 |
| v236 | 2017-12 | systemd-coredump 안정화, coredumpctl info/debug/gdb 연동 |
| v238 | 2018-04 | IPAddressAllow=/IPAddressDeny= — eBPF 기반 서비스별 네트워크 접근 제어 |
| v240 | 2018-12 | cgroup v2 기본 지원. 통합 계층 구조 권장. PSI 연동 기반 마련. 커널 4.15+ 필요 |
| v243 | 2019-09 | systemd-oomd 초기 구현 (PSI 기반). cgroup v2 전용. 커널 4.20+ PSI 필요 |
| v245 | 2020-05 | systemd-homed 도입 — LUKS/fscrypt 기반 휴대용 홈 디렉터리, 사용자 레코드 JSON |
| v246 | 2020-07 | systemd-repart — 디스크 파티션 선언적 관리. A/B 업데이트 시나리오 지원 |
| v247 | 2020-12 | LoadCredential= 도입 — 서비스에 비밀 정보를 안전하게 전달하는 Credential 시스템 |
| v248 | 2021-03 | systemd-oomd 안정화. ManagedOOMMemoryPressure/ManagedOOMSwap 지시어 확립 |
| v249 | 2021-07 | ImportCredential=, SetCredential= 추가. TPM2 기반 credential 암호화 초기 지원 |
| v250 | 2021-12 | Portable Services — systemd-portabled, 이미지 기반 서비스 배포. systemd-sysext 도입 |
| v251 | 2022-04 | systemd-sysupdate — 이미지 기반 시스템 업데이트 매니저. A/B 파티션 전환 |
| v252 | 2022-10 | UKI(Unified Kernel Image) 지원. ukify 빌드 도구. systemd-measure로 PCR 사전 계산 |
| v253 | 2023-02 | TPM2 자동 등록(systemd-cryptenroll --tpm2-device=auto). systemd-pcrphase measured boot |
| v254 | 2023-07 | soft-reboot — 커널을 유지한 채 사용자 공간만 재시작. systemd-bsod 도입 (부팅 실패 시 화면 표시) |
| v255 | 2023-12 | tpm2-pcrlock — 부팅 무결성 정책 잠금. systemd-vmspawn으로 VM 빠른 시작 |
| v256 | 2024-06 | run0 도입 — sudo 대체 도구 (PTY 기반 권한 상승, 자격 증명 유출 방지). varlink IPC 확대 |
| v257 | 2024-12 | BPF 토큰 위임 개선, AF_UNIX 피어 인증, mkosi 통합 강화, ssh 기반 머신 관리 |
주요 마일스톤 타임라인
systemd 역사에서 특히 중요한 전환점은 다음 세 가지입니다.
| 마일스톤 | 버전 | 영향 |
|---|---|---|
| cgroup v2 전환 | v226→v240 | v226에서 실험적 지원 시작, v240에서 기본 전환. 커널 cgroup unified hierarchy 채택이 주요 배포판으로 전파되는 계기. RHEL 9/Fedora 31+/Ubuntu 21.10+ 기본 v2 |
| UKI 도입 | v252 | 커널+initrd+cmdline+splash를 단일 PE 바이너리로 번들. Secure Boot 서명이 단일 파일로 단순화. 클라우드/엣지 환경에서 이미지 기반 부팅 표준화 추진 |
| soft-reboot | v254 | 커널 재부팅 없이 userspace만 재시작. 업데이트 적용 시간 90%+ 단축. 컨테이너 오케스트레이터와 유사한 빠른 롤아웃 가능 |
배포판별 기본 systemd 버전 (2024 기준)
| 배포판 | 버전 | systemd 버전 | cgroup v2 기본 |
|---|---|---|---|
| Ubuntu 24.04 LTS | Noble | v255 | 예 |
| Ubuntu 22.04 LTS | Jammy | v249 | 예 (하이브리드) |
| Debian 12 | Bookworm | v252 | 예 |
| Fedora 40 | - | v255 | 예 |
| RHEL 9 | - | v252 | 예 |
| RHEL 8 | - | v239 | 아니오 (v1) |
| SUSE 15 SP5 | - | v254 | 예 |
| Arch Linux | Rolling | 최신 | 예 |
| Alpine Linux | - | 미사용 (OpenRC) | - |
| Gentoo | - | 선택 (OpenRC/systemd) | - |
systemctl --version 명령으로 현재 설치된 systemd 버전을 확인할 수 있습니다. 배포판별 기본 버전은 상이하므로, 특정 기능 사용 시 버전 호환성을 반드시 확인하세요. 예를 들어 DynamicUser=는 v232+, LoadCredential=은 v247+, UKI 지원은 v252+ 버전이 필요합니다.
주요 커밋과 설계 결정
systemd의 주요 설계 결정과 그 배경을 이해하면 현재 동작 방식을 더 깊이 파악할 수 있습니다.
- cgroup 단일 작성자 규칙(Single Writer Rule) — cgroup v2 설계에서 systemd가 최상위 cgroup 관리자로 동작하고, 하위 트리를 위임하는 모델은 여러 프로세스가 동시에 cgroup을 수정할 때 발생하는 경쟁 상태(Race Condition)를 방지하기 위한 것입니다.
- 소켓 활성화 — macOS의 launchd에서 영감을 받았습니다. 소켓을 미리 열어 두면 서비스 간 순서 의존성이 제거되어 부팅 시간이 단축됩니다.
- 바이너리 저널 — 텍스트 로그의 파싱 비용과 필드 검색 비효율성을 해결하기 위해 도입되었습니다. 대신
journalctl이라는 전용 도구가 필요합니다. - 선언적 유닛 파일 — 셸 스크립트 기반 init 스크립트의 취약점(파싱 오류, 비결정적 동작, 보안 문제)을 해결하고, 기계적 분석과 검증을 가능하게 합니다.
- 통합 에코시스템 — journald, udevd, networkd 등을 별도 프로젝트가 아닌 단일 프로젝트에 포함시킨 것은 구성 요소 간 일관된 인터페이스와 버전 호환성을 보장하기 위한 의도적 결정입니다.
자주 묻는 질문 (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/grub의 GRUB_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: 부팅 시간을 최적화하려면?
다음 단계로 접근합니다.
systemd-analyze blame으로 가장 느린 서비스를 식별합니다.systemd-analyze critical-chain으로 임계 경로의 병목을 파악합니다.- 불필요한 서비스를
systemctl disable또는systemctl mask합니다. - 느린 서비스에 소켓 활성화를 적용하여 지연 시작(Lazy Start)합니다.
NetworkManager-wait-online.service같은 동기 대기 서비스의 타임아웃을 줄입니다.- 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=1 | Watchdog 갱신 | 주기적 (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 |
| 8 | cgroup 이벤트 | cat /sys/fs/cgroup/.../memory.events |
| 9 | OOM 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 start | systemctl start service |
| 부팅 등록 | update-rc.d service defaults | systemctl enable service |
| 로그 확인 | cat /var/log/syslog | journalctl -u service |
| PID 추적 | PID 파일 (/var/run/service.pid) | cgroup 자동 추적 |
| 환경 설정 | /etc/default/service | EnvironmentFile=/etc/default/service |
| 데몬화 | 셸 스크립트 + start-stop-daemon | 불필요 (systemd가 관리) |
| 종료 처리 | 수동 PID kill | cgroup 전체 정리 |
마이그레이션 체크리스트
- 포그라운드 실행 — 데몬화 코드를 제거하고 전경에서 실행하도록 수정 (
Type=simple또는Type=notify) - PID 파일 제거 — systemd가 cgroup으로 추적하므로 PID 파일 불필요 (단,
Type=forking시PIDFile=사용 가능) - 로그 출력 변경 — 파일 로깅 대신 stdout/stderr 출력으로 변경 (journald가 수집)
- sd_notify() 통합 — 초기화 완료 알림으로 정확한 상태 추적
- 리소스 제한 추가 — MemoryMax, CPUQuota 등 리소스 가드레일 설정
- 보안 강화 — ProtectSystem, NoNewPrivileges 등 기본 보안 적용
- 의존성 정의 — After=, Requires= 로 명시적 순서/의존성 선언
systemd 비사용 환경과의 호환성
systemd에 의존하는 서비스를 systemd가 없는 환경(Alpine Linux, Gentoo OpenRC 등)에서도 실행해야 하는 경우, 다음 전략을 고려합니다.
- sd_notify() 조건부 사용 —
$NOTIFY_SOCKET환경 변수 존재 여부로 systemd 환경 감지. 없으면 호출 건너뛰기 - sd_listen_fds() 조건부 사용 —
$LISTEN_FDS환경 변수 존재 여부로 소켓 활성화 감지. 없으면 직접 소켓 생성 - OCI 컨테이너 이미지 — 컨테이너 환경에서는 init 시스템에 무관하게 서비스 실행 가능
- 멀티 init 지원 패키징 — Debian은 .service 파일과 init.d 스크립트를 모두 포함할 수 있음
/* 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 서비스가 즉시 inactive | RemainAfterExit=yes 추가 |
| User= 지정 후 권한 부족 | 바인드, 파일 접근 실패 | Capability 추가 또는 디렉터리 권한 조정 |
| 너무 제한적인 SystemCallFilter | 서비스 SIGSYS로 사망 | SystemCallErrorNumber=EPERM 설정 후 로그로 필요 콜 파악 |
참고 링크
- systemd 공식 사이트 — 프로젝트 홈페이지 (systemd.io)
- freedesktop.org: systemd — 프로젝트 위키 및 문서 모음
- GitHub: systemd/systemd — 소스 코드 및 이슈 트래커
- systemd man 페이지 온라인 — 전체 지시어 레퍼런스
- systemctl(1) man 페이지 — 서비스 제어 명령 레퍼런스
- journalctl(1) man 페이지 — 저널 로그 조회 명령 레퍼런스
- systemd.unit(5) man 페이지 — 유닛 파일 공통 옵션 레퍼런스
- Rethinking PID 1 — Lennart Poettering의 systemd 설계 철학 원문
- The Biggest Myths — systemd에 대한 오해와 진실 (Lennart Poettering)
- systemd for Administrators, Part XII — 보안 강화 지시어 해설
- LWN.net: Portable services with systemd — 이식 가능한 서비스 이미지 (2019)
- LWN.net: A systemd-homed future — 홈 디렉터리 관리의 미래 (2016)
- LWN.net: Pid namespaces and systemd — PID 네임스페이스와 systemd (2018)
- LWN.net: Control group v2 — systemd의 cgroup v2 통합 (2015)
- Arch Wiki: systemd — 실전 활용 정보가 풍부한 커뮤니티 위키
- Arch Wiki: systemd/Timers — 타이머 유닛 활용 가이드
- 커널 소스: init/main.c — kernel_init()에서 /sbin/init 실행 코드 (Bootlin Elixir)
- 커널 소스: kernel/cgroup/cgroup.c — cgroup 코어 구현 (Bootlin Elixir)
관련 문서
systemd와 관련된 다른 리눅스 커널 문서를 참고하여 더 깊이 이해할 수 있습니다. systemd는 커널의 cgroup, 네임스페이스, seccomp, 부팅 프로세스 등 다양한 하위 시스템과 밀접하게 연동되므로, 각 주제에 대한 상세 설명은 개별 문서를 참조하세요.