실제로 쓰는 104개 명령, 옵션을 하나씩 설명 — "무엇을 하려는지"로 분류
찾기, 일괄 처리, 압축 — 옵션을 잘못 기억하면 보통 파일을 잘못 지우거나 숨김 디렉터리를 조용히 놓친다
-l 긴 형식: 권한/소유자/크기/시간 표시
-a . 과 .. 를 포함한 모든 숨김 파일까지 함께 나열
-h 크기에 단위를 붙임(1234 대신 1.2K), 반드시 -l 과 함께 사용
-t 수정 시간 역순, 최신이 맨 위
-r 정렬 반전, -t 와 함께 쓰면 가장 오래된 것이 위
-S 파일 크기 역순 — "누가 자리를 차지했나"를 찾을 때 -t 보다 직접적
--time-style=long-iso 시간을 2026-10-29 09:18 형식으로 표시, 기본 형식보다 읽기 쉽다
# 최신 파일이 위에, 크기와 시간 포함 ls -laht --time-style=long-iso # 용량을 많이 차지하는 것 찾기(-S) ls -lhS /var/log | head
-name 파일 이름으로 매칭, 대소문자 구분; -iname 은 구분하지 않음
-type f 일반 파일만, -type d 디렉터리만, -type l 심볼릭 링크
-mtime +7 수정된 지 7일 초과; -mmin -30 30분 이내에 수정됨
-size +50M 50M 초과; -size -1k 1k 미만
-maxdepth 1 하위 디렉터리로 내려가지 않음, 원인 파악 시 먼저 붙이면 훨씬 빠르다
-ls 결과에 ls -dils 를 실행해 크기·inode·시간까지 바로 표시
-delete 즉시 삭제, 빈 디렉터리만 가능; -exec ... \; 결과마다 명령 실행
-prune 특정 디렉터리 건너뛰기, 반드시 -path 와 함께 사용
-o 는 "또는", 조건을 여러 개 이을 때 사용; -print 명시적 출력(-prune 과 함께면 필수)
# 30분 이내에 수정된 대용량 파일(디스크 급증 원인 파악) find /var -type f -mmin -30 -size +50M -ls # 7일 이전 로그 일괄 삭제 find /var/log -name '*.log.*' -mtime +7 -delete # .git과 node_modules 제외하고 검색 find . -path ./node_modules -prune -o -name '*.js' -print
-r 디렉터리 재귀; -R 재귀하며 심볼릭 링크도 따라감
-i 대소문자 무시; -n 줄 번호 표시; -v 반전, 일치하지 않는 줄만
-E 확장 정규식 사용(| 는 반드시 -E 가 필요, 아니면 리터럴로 취급)
-l 파일 이름만 출력, 먼저 위치를 좁힌 뒤 확인할 때 적합
-A3 / -B3 / -C3 일치 줄 뒤 3줄 / 앞 3줄 / 앞뒤 각 3줄 표시
--include='*.conf' 지정 확장자만 검색; --exclude-dir 디렉터리 건너뛰기
-w 단어 전체 일치, cat 을 검색해 category 까지 걸리는 것을 방지
-rn 은 가장 흔한 조합: 재귀 + 줄 번호, 없으면 오류 위치를 찾을 수 없다
# nginx 설정에서 특정 upstream 찾기, 앞뒤 3줄 포함 grep -rn -C3 'upstream' /etc/nginx --include='*.conf' # 여러 키워드 중 하나라도 일치(반드시 -E) grep -rnE 'timeout|refused|reset' /var/log/app/error.log # 설정이 포함된 파일명만 나열한 뒤 하나씩 확인 grep -rli 'proxy_pass' /etc/nginx
-c 생성; -x 해제; -t 내용만 확인하고 풀지 않음; -z gzip; -j bzip2
-f 뒤에 파일 이름이 오므로 -f 는 항상 맨 마지막
-v 처리 과정 출력, 큰 압축 파일에서는 매우 느려지므로 리다이렉트로 버린다
-C 지정 디렉터리에 해제, 디렉터리가 이미 있어야 함
--exclude 압축 시 제외, 상대 경로 표기가 실제 경로와 맞아야 함
-czf / -xzf / -tzf 가장 흔한 세 조합, 이 세 개만 기억하면 충분
해제 전에 -t 로 먼저 확인하면 tar 폭탄이 현재 디렉터리를 덮어쓰는 것을 막을 수 있다
# 압축, 캐시 디렉터리 제외 tar -czf app.tar.gz --exclude='node_modules' --exclude='.git' -C /srv app # 먼저 내용 확인 후 지정 디렉터리에 해제 tar -tzf app.tar.gz | head -20 tar -xzf app.tar.gz -C /tmp/restore
rm -rf 재귀 강제 삭제, 확인 없음 — 변수가 비어 있으면 rm -rf "$DIR/" 이 rm -rf / 가 된다
-f 강제, 프롬프트 없음; -r 디렉터리 재귀. 이 둘을 합친 것이 가장 사고 나기 쉬운 조합
cp -a 아카이브 모드, 권한·시간·심볼릭 링크 보존, 백업의 기본 선택
cp -n 기존 파일을 덮어쓰지 않음; mv -n 도 동일(-n 은 no-clobber)
mv 는 파일 시스템을 넘을 때 "복사 + 삭제"이며 원자적이지 않다, 중단되면 양쪽 모두 불완전해진다
rm 으로 지운 파일을 프로세스가 잡고 있으면 그 프로세스가 종료될 때까지 공간이 반환되지 않는다(디스크 점검 참고)
test -n "$DIR" 변수가 비어 있지 않은지 확인, rm 전에 반드시 해야 하는 보호
-- 이후는 옵션이 아니라고 알려줌, - 로 시작하는 파일 이름이 옵션으로 해석되는 것을 방지
# 안전 삭제: 변수가 비어 있지 않은지 확인 후 삭제 [ -n "$DIR" ] && rm -rf -- "$DIR" # 속성 유지하며 전체 백업 cp -a /srv/app /backup/app-$(date +%F)
df -h 각 마운트 지점 사용률 확인; -i inode 사용률 확인(inode 가 차도 파일을 쓸 수 없다)
du -sh 디렉터리 하나의 크기 요약; -s 총계만 출력, -h 사람이 읽기 쉬운 단위
du -sh * | sort -h 현재 디렉터리에서 무엇이 가장 큰지, sort -h 는 1K/2M/3G 를 제대로 정렬
--max-depth=1 한 단계만, 전체 재귀보다 훨씬 빠르다
du 와 df 수치가 크게 다르면 거의 "삭제됐지만 프로세스가 잡고 있는" 파일이다
2>/dev/null 권한 부족 오류를 버림, 아니면 출력이 오류 메시지로 가득 찬다
# 루트부터 층별로 탐색, 갑자기 커진 계층으로 들어가 확인 du -h --max-depth=1 / 2>/dev/null | sort -h | tail -10 # 삭제됐지만 프로세스가 점유 중인 파일(df는 가득, du는 작을 때 사용) lsof +L1 2>/dev/null | head
ln -s 대상 링크이름 — 인자 순서는 "대상이 먼저", 반대로 하면 깨진 링크가 만들어진다
-s 심볼릭 링크; -s 없이 쓰면 하드 링크(파일 시스템을 넘을 수 없고 디렉터리에는 사용 불가)
-f 기존 링크 덮어쓰기
-n 링크가 디렉터리를 가리킬 때 -f 와 함께 써야 디렉터리 안에 들어가지 않고 제대로 교체된다
-sfn 은 릴리스 디렉터리 전환의 표준 조합(current 가 새 release 를 가리킴)
심볼릭 링크는 경로 문자열을 저장하므로 대상이 이동하면 깨진다; 하드 링크는 같은 inode 를 가리켜 하나를 지워도 다른 하나는 유효하다
ln -sfn /srv/app/releases/20261029 /srv/app/current
stat 은 크기·inode·권한·세 가지 시간을 한 번에 준다 — "이 파일이 정말 수정됐나"를 볼 때 가장 직접적
-c 사용자 지정 형식: %y 수정 시간, %s 바이트 수, %U:%G 소유자, %a 8진 권한, %n 파일 이름
-L 심볼릭 링크를 따라가 대상 파일 자체를 봄(-L 이 없으면 링크라는 "파일"을 본다)
세 가지 시간: Access 읽음, Modify 내용 변경, Change 메타데이터(권한/소유자) 변경
-f 파일 시스템 계층 정보(블록 크기, inode 총수와 잔여), df 와 상호 보완
touch -d '2026-10-01 08:00' 파일 로 수정 시간 변경; find -newer 와 함께 변경 범위를 좁힌다
file 은 실제 타입을 판별, 확장자보다 신뢰할 수 있다 — 확장자는 마음대로 바꿀 수 있으니
stat -c '%a' 는 "권한이 정확히 몇 비트인가"를 확인하는 가장 빠른 방법
stat -c '%y %s %U:%G %a %n' /etc/nginx/nginx.conf stat -L -c '%y %s' /srv/app/current # 심볼릭 링크가 가리키는 실제 파일 확인 file /usr/local/bin/app
설정 수정, 열 단위 집계, 명령 조합 — 운영에서 "설정 200줄 손으로 고치기"는 모두 sed/awk 에 맡겨야 한다
-i 파일을 직접 수정(제자리 교체) — 운영에서는 먼저 -i 없이 한 번 실행해 출력을 확인한 뒤 적용
-i.bak 수정과 동시에 .bak 백업을 남겨 문제가 생기면 즉시 되돌릴 수 있다(습관으로 삼을 것을 강력히 권장)
-n 자동 출력하지 않음, p 와 함께 쓰면 지정한 줄만 출력 — "몇 줄만 보기"와 동일
-E 확장 정규식 사용(+ ? | ( ) 를 이스케이프하지 않음); 구형 시스템에서는 -r, 역할은 같다
-e 여러 표현식을 연결, 하나의 표현식 안에서 세미콜론으로 구분해도 된다
s/옛/새/g 의 g 는 "한 줄 안의 모든 일치를 치환", g 가 없으면 각 줄 첫 번째만
정규식 주소: '/^server/,/}/ s/a/b/' 는 특정 설정 블록 내부만 수정
\1 \2 는 캡처 그룹 참조, & 는 일치한 전체 내용
-i 뒤에는 반드시 -E 를 붙이거나 표현식 순서에 주의: sed -E -i.bak 's/x/y/' 파일
sed -n '1,20p' /etc/nginx/nginx.conf # 앞 20줄만 보기 sed -i.bak 's/listen 80;/listen 8080;/' /etc/nginx/sites-enabled/app sed -E -i.bak 's/^server_name[[:space:]]+([^;]+);/server_name \1.bak;/' app.conf sed '/^[[:space:]]*#/d;/^[[:space:]]*$/d' app.conf | head # 주석과 빈 줄 제거
-F 구분자 지정: -F: 콜론 기준, -F'\t' 탭 기준, 생략하면 연속 공백 기준
'{print $1,$3}' 1·3번째 열 출력; $0 은 전체 줄; NF 는 열 수; NR 은 줄 번호
조건 필터는 프로그램 앞에 그대로 쓴다: '$9==500' 은 9번째 열이 500 이라는 뜻
숫자 비교는 숫자로 변환해야 한다: $9+0 >= 500, 아니면 문자열 비교로 예상 밖의 결과가 나온다
'{s+=$7} END {print s}' 는 합계 템플릿; 개수는 '{c++} END {print c}'
배열 집계: {c[$1]++} END {for (k in c) print k, c[k]}
-v 외부 변수 전달: awk -v lim=500 '$3>lim', -v 는 프로그램 앞에 와야 함
-F 와 -v 는 awk 가 직접 파싱하므로 따옴표로 묶은 프로그램 앞에 둬야 한다
예시에서 sort -rn 은 숫자 역순, uniq -c 는 횟수 집계, ss -tn 은 TCP 연결(각 옵션은 해당 항목 참고)
awk -F: '$3>=1000 {print $1, $3}' /etc/passwd
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
awk -v lim=500 '{ if ($9+0 >= lim) print NR": "$7 }' access.log
ss -tn | awk 'NR>1 {c[$1]++} END {for (k in c) print k, c[k]}'sort -n 숫자 정렬(기본 사전순은 10 을 9 앞에 놓는다)
sort -h 1K/2M/3G 단위를 인식, du / df 출력 처리에는 반드시 필요
sort -r 역순; -u 정렬과 동시에 중복 제거
-k2 2번째 열 기준 정렬, -k3 도 동일; 여러 열은 -k1,1 -k2,2
-t: 구분자를 콜론으로 지정(기본은 공백 기준으로 자름)
uniq -c 연속 중복 횟수 집계 — 그래서 반드시 먼저 sort 해야 한다, 아니면 같은 값이 여러 구간으로 나뉜다
uniq -d 중복 항목만, uniq -u 유일한 항목만
Top N 의 정형 패턴: sort | uniq -c | sort -rn | head
예시의 du -sh 에서 -s 는 총계만 출력(du / df 항목 참고)
du -sh /var/log/* | sort -h | tail -5
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -10
sort -t: -k3 -n /etc/passwd | headcut -d: -f1,7 구분자 기준 열 추출: -d 는 구분자, -f 는 열 번호(여러 개 가능)
cut -c1-19 문자 위치 기준 자르기, 고정 길이 로그의 타임스탬프 부분에 적합; -c1-19 는 1~19번째 문자
cut 은 단일 구분자만 인식, 공백으로 정렬된 출력에는 잘못 적용되므로 awk 를 쓴다
tr 'a-z' 'A-Z' 문자 단위 매핑; tr 은 일대일 치환만 가능, 문자열 치환은 sed 를 쓴다
tr -d 문자 삭제, 가장 흔한 것은 tr -d '\r' 로 Windows 줄바꿈 제거
tr -s 연속 중복 문자를 하나로 압축, 연속 공백을 하나로 줄일 때 자주 사용
tr 은 표준 입력에서만 읽고 파일 이름을 받지 않으므로 < 로 리다이렉트
예시에서 df -h 는 사람이 읽는 단위, cut -f5 는 5번째 열(Use%) 추출, sort -u 는 중복 제거이며 각 항목에서 상세히 설명
cut -d: -f1,7 /etc/passwd | head cut -c1-19 /var/log/nginx/access.log | head -3 tr -d '\r' < crlf.txt > lf.txt df -h | tr -s ' ' | cut -d' ' -f5 | sort -u
xargs 는 표준 입력의 각 줄을 뒤 명령의 인자로 바꾼다, find | xargs rm 이 그렇게 만들어진 것이다
-0 은 find -print0 과 함께 사용: 파일 이름에 공백이나 줄바꿈이 있어도 분리되지 않는다(사용자 업로드 파일 처리 시 필수)
find -print0 은 \0 으로 끝나게 출력, xargs -0 전용
-n1 한 번에 인자 하나씩, 건별 실행이라 더 안전하고 어느 항목이 실패했는지도 보인다
-I {} 자리 표시자, 인자를 명령 중간에 넣는다: xargs -I {} cp {} /backup
-p 실행 전 확인, -t 먼저 출력 후 실행 — 어떤 명령이 만들어지는지 불확실하면 -t 로 먼저 확인
-r 입력이 비면 실행하지 않음(GNU 확장), 없으면 빈 입력에도 rm 이 한 번 실행된다
-P4 프로세스 4개 병렬, 대량 압축/다운로드 시 몇 배 빨라진다
cp --parents 디렉터리 구조까지 함께 복사, 백업 복원 시 유용
예시에서 find 의 -name / -mtime / -type 은 필터 조건, rm -f 의 -f 는 확인 없이 강제
find /var/log -name '*.log.*' -mtime +7 -print0 | xargs -0 -r rm -f
find /srv/uploads -type f -print0 | xargs -0 -I {} cp --parents {} /backup
printf '%s\n' a b c | xargs -n1 -t echotee 는 입력을 파일과 표준 출력에 동시에 쓴다, 설정을 바꾸기 전 증거를 남길 때 가장 유용
-a 덮어쓰지 않고 추가, 로그류에는 항상 사용
sudo tee 가 필요한 이유: sudo echo x > /etc/f 의 리다이렉션은 현재 셸이 수행하므로 권한 오류가 난다
-i 인터럽트 신호 무시, 긴 출력을 받아쓸 때 Ctrl-C 로 끊기지 않는다
파이프에서 받은 뒤에는 2>&1 로 stderr 를 합쳐야 오류 메시지가 tee 로 들어간다
명령에 타임스탬프 붙이기: ... | ts 또는 | awk '{print strftime("%F %T"), $0}'
./deploy.sh 2>&1 | tee -a /var/log/deploy-$(date +%F).log echo 'net.ipv4.ip_forward=1' | sudo tee -a /etc/sysctl.conf
jq 는 API 응답 JSON 을 처리, grep 보다 훨씬 믿을 만하다 — 줄이 아니라 구조로 값을 꺼낸다
. 필드 접근: .data .items[0].name; .[] 는 배열 순회
-r 생 문자열 출력(큰따옴표 제거), 변수에 넣거나 다시 파이프할 때 필수
-c 결과를 한 줄로 압축, sort / uniq 집계에 넘기기 편하다
'keys' 객체의 필드 목록; 'length' 배열 길이; '..' 모든 경로 나열(구조를 모를 때 형태부터 파악)
select() 필터: '.[] | select(.status != "ok")'
-C 컬러 출력(사람이 볼 때), -M 단색(파일이나 스크립트용)
jq 는 기본이 포맷 출력, -e 는 "결과가 참인가"를 종료 코드로 표현, 스크립트 판단에 사용
예시에서 curl -s 는 무음, 아니면 진행 표시가 JSON 파싱에 섞인다(curl 항목 참고)
curl -s https://api.example.com/v1/tools | jq -r '.data[].name'
curl -s 'https://api.example.com/v1/list?page=1' | jq -c '.items[] | {id, name}'
jq -r '.[] | select(.status != "ok") | .id' resp.jsondiff -u 통합 형식 출력, 곧 패치 형식이라 patch 가 바로 사용할 수 있다
diff -r 두 디렉터리 재귀 비교; -q 는 "어떤 파일이 다른지"만 보고하고 내용은 출력하지 않음
diff -y 나란히 표시, -W 200 으로 옆 칸 너비 조정, 눈으로 설정을 비교할 때 유용
wc -l 줄 수(로그 건수 집계에 가장 흔히 사용); -c 바이트 수; -w 단어 수
wc 출력에는 파일 이름이 붙으므로 스크립트에서 값을 꺼낼 때 열이 하나 더 있다, awk '{print $1}' 로 처리
comm 은 "두 정렬된 목록의 차집합"에 더 적합, 단 양쪽 모두 먼저 sort 해야 한다
comm -13 a b 는 b 에만 있는 줄 출력; -12 도 b 에만 있는 줄이므로 열 의미를 정확히 봐야 한다
비교 전에 정렬과 중복 제거를 통일, 아니면 "전부 다른 것처럼" 보이는 것이 실제로는 순서 차이일 뿐이다
diff -u /etc/nginx/nginx.conf.bak /etc/nginx/nginx.conf | head -40 diff -rq /srv/app/releases/20261028 /srv/app/releases/20261029 wc -l /var/log/nginx/access.log sort a.txt > a.s; sort b.txt > b.s; comm -13 a.s b.s # b에만 있는 줄
로그인, 무암호 접속, 파일 전송, 이어받기 — 실제로 시간을 가장 많이 잡아먹는 것은 "파일을 어떻게 옮길까" 단계다
ssh 사용자@호스트 가장 기본 로그인; 기본 포트 22
-p 2222 포트 지정(scp/sftp 는 대문자 -P, 이 둘을 헷갈리지 말 것)
-i 개인 키 파일 지정; 기본은 ~/.ssh/id_ed25519, id_rsa
-t 의사 터미널 강제 할당 — 원격에서 sudo, top 같은 대화형 명령을 실행하려면 필수
-v 핸드셰이크와 인증 과정 출력, 접속이 안 되면 "네트워크 문제"인지 "인증 실패"인지 먼저 확인; -vvv 는 더 상세
-o 설정 항목 전달: -o ConnectTimeout=5 로 멈춤 방지, -o BatchMode=yes 로 모든 대화형 입력 금지(스크립트 필수)
원격에서 명령 하나만 실행: ssh 호스트 '명령', 터미널을 할당하지 않아 일괄 점검에 적합
-J 점프 호스트: ssh -J bastion@1.2.3.4 deploy@10.0.0.5(구식 표기는 -o ProxyCommand)
예시에서 df -h 의 -h 는 사람이 읽는 단위(du / df 항목 참고)
ssh -p 2222 -i ~/.ssh/id_ed25519 deploy@10.0.0.5 ssh -o ConnectTimeout=5 -o BatchMode=yes 10.0.0.5 'uptime; df -h /' ssh -v -t 10.0.0.5 'sudo systemctl restart app' ssh -J bastion@1.2.3.4 deploy@10.0.0.5
ssh-keygen -t ed25519 키 쌍 생성(rsa 보다 짧고 빠르며 최신 시스템은 모두 지원)
-t 타입: ed25519 우선, rsa 는 구형 시스템에서만 불가피, -b 4096 으로 길이 지정
-C 주석에 용도/이메일을 적어 둔다, 키가 여러 개일 때 구분 기준
-f 출력 파일 이름 지정, 기본 키 덮어쓰기 방지(덮어쓰면 기존 무암호 접속이 깨진다)
-N '' 빈 암호, 자동화용; 대화형에서는 그냥 Enter
ssh-copy-id 는 공개 키를 상대의 ~/.ssh/authorized_keys 에 추가하고 디렉터리와 파일 권한도 자동으로 맞춘다
-i(ssh-copy-id 의 -i) 복사할 공개 키 지정, 기본은 id_*.pub 만 전송
-R known_hosts 에서 특정 호스트의 옛 지문 삭제 — 머신 재설치 후 반드시 실행, 아니면 REMOTE HOST IDENTIFICATION HAS CHANGED 오류
-lf 공개 키 지문과 타입 확인, 두 머신의 키가 같은지 대조
개인 키 권한은 반드시 600, 디렉터리는 700, 아니면 ssh 가 사용을 거부한다
ssh-keygen -t ed25519 -C 'deploy@icetools' -f ~/.ssh/id_deploy -N '' ssh-copy-id -i ~/.ssh/id_deploy.pub deploy@10.0.0.5 ssh-keygen -R 10.0.0.5 ssh-keygen -lf ~/.ssh/id_deploy.pub
설정해 두면 ssh app 이 ssh -p 2222 deploy@10.0.0.5 와 같아져 매번 긴 명령을 칠 필요가 없다
Host 는 별칭이며 이후 모든 설정이 적용되는 범위
HostName 실제 주소; User 로그인 사용자; Port 포트; IdentityFile 개인 키 경로
IdentitiesOnly yes 지정한 키만 사용, agent 에 키가 많아 순차 시도하는 것을 방지(너무 많이 시도하면 서버가 차단)
ProxyJump 점프 호스트, 명령행 -J 와 동일하며 scp / rsync 가 자동으로 상속
ServerAliveInterval 30 30초마다 하트비트 전송, NAT 나 클라우드 로드밸런서가 유휴 연결을 끊으므로 장기 작업에는 필수
ServerAliveCountMax 3 하트비트가 3회 실패해야 끊김, 위 간격과 함께 사용
Host * 는 파일 맨 끝에 두고 전역 기본값(하트비트, 타임아웃)을 작성
권한: config 600, ~/.ssh 700, 아니면 Bad owner or permissions 오류
# ~/.ssh/config (chmod 600)
Host app
HostName 10.0.0.5
User deploy
Port 2222
IdentityFile ~/.ssh/id_deploy
IdentitiesOnly yes
ServerAliveInterval 30
ServerAliveCountMax 3
Host db
HostName 10.0.0.6
ProxyJump app
Host *
ServerAliveInterval 30-L 로컬 포워딩: ssh -L 8080:127.0.0.1:80 호스트 — 원격 80 을 로컬 8080 에 매핑
-R 역방향 포워딩: 로컬 포트를 원격에 노출해 외부에서 내부 서비스에 접근
-D 동적 포워딩, SOCKS 프록시 생성(curl --socks5 나 브라우저와 함께 사용)
-N 포워딩만 하고 셸을 열지 않음; -f 백그라운드 실행, 둘은 거의 항상 함께 쓰인다
-g 다른 머신도 로컬 포워딩 포트에 접속 허용(기본은 127.0.0.1 만 수신)
-L 의 중간 주소는 "원격 관점"에서 해석되므로 127.0.0.1 은 원격 자신을 뜻한다
"DB 직접 접속 금지" 상황: 먼저 -L 로 포트를 로컬로 끌어온 뒤 127.0.0.1 로 접속
포트가 사용 중이면 로컬 포트를 바꾼다, lsof -i :8080 으로 누가 잡고 있는지 확인
ssh -N -f -L 15432:127.0.0.1:5432 deploy@10.0.0.5 ssh -N -L 8080:10.0.0.6:80 app ssh -D 1080 -N -f app ssh -g -L 8080:127.0.0.1:80 app
scp 원본 대상 — 방향이 업로드인지 다운로드인지 결정: 로컬이 앞이면 업로드, 원격이 앞이면 다운로드
-P 대문자로 포트 지정(ssh 는 소문자 -p, 가장 헷갈리는 부분)
-r 디렉터리 전체 재귀; -r 없이 디렉터리를 보내면 바로 오류
-i 개인 키 지정; -o ssh 옵션 전달, 예: -o ConnectTimeout=5
-C 전송 중 압축, 텍스트와 로그에서 효과가 크다; 이미 tar.gz 로 압축한 파일에는 의미 없음
-l 8000 속도 제한, 단위는 Kbit/s(8000 ≈ 1MB/s), 운영 대역폭을 모두 잡아먹지 않도록
-p 소문자는 타임스탬프와 권한 보존, -P 와 전혀 무관하니 혼동 금지
scp 에는 이어받기가 없다 — 큰 파일이 끊기면 처음부터, 몇 GB 는 rsync -P 로
scp -P 2222 app.tar.gz deploy@10.0.0.5:/srv/ scp -r -i ~/.ssh/id_deploy ./dist deploy@10.0.0.5:/srv/app/ scp -C -l 8000 backup.tar.gz deploy@10.0.0.5:/backup/ scp -p deploy@10.0.0.5:/var/log/app/error.log ./
sftp 사용자@호스트 대화형 세션 진입 — "ls 하면서 전송"이 scp 로 무작정 보내는 것보다 통제 가능
-P 대문자 포트, -i 개인 키 지정, scp 와 동일
get 원격파일 다운로드; put 로컬파일 업로드; get -r 디렉터리 전체 재귀
reget 이어받기 다운로드, reput 은 업로드 대응 — sftp 가 scp 보다 나은 핵심
-p(get/put 의 -p) 권한과 타임스탬프 보존
mget / mput 는 와일드카드 일괄 전송 지원, 기본은 파일마다 확인하므로 먼저 prompt off 실행
ls / lls 는 원격과 로컬, cd / lcd 는 각각 원격과 로컬 디렉터리 이동(l 로 시작하는 것이 로컬)
!df -h 는 ! 로 시작해 로컬 셸 명령 실행, 큰 파일 전송 전 로컬 공간 확인에 사용
bye / exit 종료; heredoc 으로 스크립트를 쓰면 무인 일괄 전송 가능
sftp -P 2222 -i ~/.ssh/id_deploy deploy@10.0.0.5 <<'EOF' get -r /srv/app/logs ./ reget backup.tar.gz put ./dist/app.tar.gz /srv/ EOF
-a 아카이브 모드(-rlptgoD 와 동일), 권한·시간·심볼릭 링크·그룹 보존, 일상 기본
-v 진행 과정 표시; -z 전송 압축, 공인망에서는 필수
-P 는 --partial --progress 와 같다: 반쯤 받은 파일 유지 + 진행률 표시, 이것이 "이어받기" 스위치
--partial 중단 후 받은 부분을 유지, 다음에 이어서; 없으면 매번 처음부터
--append 추가만, 늘기만 하는 로그에 적합하며 변경되는 파일에는 부적합
--delete 대상이 원본과 완전히 일치하게 만듦(대상에만 있는 파일 삭제) — 먼저 -n 으로 예행
-n 은 --dry-run 과 동일: 무엇을 할지만 출력하고 파일은 건드리지 않음
-e 'ssh -p 2222' 원격 셸과 포트 지정, rsync 자체에는 포트 옵션이 없다
--exclude 규칙 하나 제외, --exclude-from 은 파일에서 규칙 묶음 읽기
--bwlimit=2000 속도 제한, 단위 KB/s; --checksum 은 크기·시간이 아닌 내용으로 차이 판단
--info=progress2 총 진행률을 한 줄로 표시, -P 의 파일별 진행률보다 큰 디렉터리에 적합
【가장 큰 함정】원본 경로 끝의 / 는 디렉터리 내용 복사를, 없으면 디렉터리 자체 복사를 뜻한다
rsync -avzP ./dist/ deploy@10.0.0.5:/srv/app/dist/ rsync -avzP -e 'ssh -p 2222' deploy@10.0.0.5:/var/log/app/ ./logs/ rsync -avn --delete /srv/data/ /backup/data/ # 예행 실행, 충분히 확인 후 실제 실행 rsync -avz --bwlimit=2000 --exclude='*.tmp' ./ /mnt/nas/app/
wget URL 현재 디렉터리에 바로 다운로드; -O 출력 파일 이름 지정, -O - 는 표준 출력
-c 이어받기(curl 은 -C - 로 표기), 큰 파일이 끊기면 같은 명령을 다시 실행
-q 무음 모드; --progress=dot:giga 진행 표시를 간소화
--limit-rate=1m 속도 제한, k/m 접미사 지원; --tries=3 재시도 횟수
-T 30 연결 타임아웃(초), 멈춤 방지; --timeout 이 긴 표기
-r 사이트 전체 재귀 다운로드, -np 상위 디렉터리로 올라가지 않음(도메인 전체를 받는 사고 방지)
--no-check-certificate 인증서 검증 생략, 내부 사이트 진단 시에만 사용하고 스크립트에 넣지 말 것
사용자 지정 헤더, Cookie, 로그인 상태가 필요하면 wget 보다 curl 이 유연하다
wget -c https://example.com/big.iso wget -O - https://example.com/install.sh | head -40 # 내용 확인 후 실행 여부 결정 wget --limit-rate=1m --tries=3 -c -O app.tar.gz https://example.com/app.tar.gz
숫자 비트, 특수 비트, sudo — 권한을 잘못 주면 "비밀번호는 맞는데 들어갈 수 없다"와 "서비스가 뜨지 않는다"가 전형적인 증상이다
숫자 비트: 4 읽기 / 2 쓰기 / 1 실행, 세 자리는 소유자 / 그룹 / 기타
755 디렉터리에서 흔히 사용(rwxr-xr-x); 644 파일에서 흔히 사용(rw-r--r--)
600 소유자만 읽기·쓰기 가능, 개인 키는 반드시 이것
기호 비트: u/g/o/a 는 대상, +/-/= 는 동작, X 는 "디렉터리에만 실행 비트 추가"
-R 재귀; 디렉터리에 재귀적으로 x 를 줄 때는 a+X 가 안전, 일반 파일에 실행 비트가 붙지 않는다
go-w 는 그룹과 기타의 쓰기 권한 제거, a+X 와 함께 쓰면 두 종류를 한 번에 수정
재귀 변경 후 ls -l 로 파일 하나와 디렉터리 하나를 표본 확인, 권한 오류는 대개 재귀를 과하게 적용한 탓
chmod 600 ~/.ssh/id_rsa chmod -R a+X,go-w /srv/app/public ls -l /srv/app/public | head # 재귀 변경 후 일부 표본 점검
chown 사용자:그룹 경로, 콜론 대신 점(.)으로도 쓸 수 있다
그룹만 바꿀 때는 chown :그룹 또는 chgrp
-R 재귀; -h 와 함께 쓰면 링크 자체만 바꾸고 가리키는 대상은 그대로
--reference=원본파일 소유자와 권한을 그대로 복사, 일괄 동기화에 유용
root 만 소유자를 바꿀 수 있다; 일반 사용자는 자기 파일의 그룹만 변경 가능(그리고 대상 그룹 소속이어야 함)
컨테이너 마운트 볼륨에서 Permission denied 가 나면 호스트 디렉터리의 uid 가 컨테이너 내부 사용자와 같은지 먼저 확인
chown -R www-data:www-data /var/www/app chown --reference=/srv/app/a /srv/app/b
4000 SUID: 실행 시 파일 소유자 권한으로 동작(/usr/bin/passwd 가 그 예)
2000 SGID: 파일은 그룹 권한으로 실행; 디렉터리에 설정하면 새 파일이 디렉터리의 그룹을 상속
1000 Sticky: 디렉터리 안의 파일은 소유자만 삭제 가능(/tmp 가 그 예)
ls 에서 s 나 t 가 보이면 해당 비트가 설정된 것; 대문자 S/T 는 "설정됐지만 x 가 없다"는 뜻
-perm -4000 은 "권한 비트에 SUID 가 포함"을 뜻하며, -perm 은 find 의 권한 매칭 조건
-xdev 파일 시스템을 넘지 않음 — 없으면 NFS, 컨테이너 마운트까지 따라가 매우 느려진다
-ls 일치한 파일을 ls -dils 형식으로 출력, 크기와 시간을 함께 확인
-type f 일반 파일만, 디렉터리와 심볼릭 링크 제외
권한 상승 점검 시 SUID 부터 찾는다: find / -xdev -perm -4000 -type f
# 감사: 모든 SUID 프로그램 나열 find / -xdev -perm -4000 -type f -ls 2>/dev/null # 공유 디렉터리: 그룹 협업 + 본인만 삭제 가능 chmod 2770 /srv/share
umask 는 "빼낼 비트"이며 권한 그 자체가 아니다
022 → 새 디렉터리 755, 새 파일 644(파일은 원래 실행 비트가 없다)
077 → 새 디렉터리 700, 새 파일 600, 키를 두는 디렉터리에 적합
-S 기호 형식(rwxr-xr-x)으로 실제 기본 권한 표시, 숫자보다 직관적
임시 변경: umask 077 은 현재 셸에만 적용
영구 변경: /etc/profile 또는 사용자의 ~/.bashrc 에 기록
"왜 새로 만든 키를 남이 읽을 수 있나"를 볼 때는 umask 부터 확인
umask # 현재 값 확인 umask 077 # 현재 세션에 적용 umask -S # 기호 형식으로 실제 기본 권한 표시
sudo -i 완전히 새로운 root 로그인 셸 획득(root 의 환경 변수를 읽는다)
sudo -s 현재 사용자의 환경 변수로 root 셸 실행, PATH 가 바뀌지 않는다
sudo -u 사용자 지정 사용자로 실행, 서비스 구동 시 자주 사용
sudo -l 자신에게 허용된 명령 확인, "왜 실행이 거부되나"를 볼 때
su - 는 root 로 전환하며 환경을 함께 로드; 하이픈이 없으면 현재 환경을 그대로 가져가 PATH 가 틀릴 수 있다
visudo 로 sudoers 수정, 문법 검사가 있다; -c 는 검사만 하고 저장하지 않으므로 수정 후 먼저 실행
/etc/sudoers 를 직접 편집하다 틀리면 자신이 잠긴다, 운영에서 가장 고전적인 사고 지점 중 하나
sudo -l sudo -u www-data /usr/bin/env # 서비스 계정의 환경 확인 visudo -c # 문법 검사만 수행
useradd -m 홈 디렉터리 생성; -s 셸 지정; -G 보조 그룹에 추가
-m 이 없으면 홈 디렉터리가 없어 로그인 후 여러 도구가 원인 없이 실패한다
usermod -aG 그룹 사용자 — -a 가 없으면 다른 보조 그룹에서 사용자를 빼버린다(-a 는 append)
-G 는 보조 그룹 목록, -a 와 붙여 -aG 로 쓸 수 있다
id 사용자 uid/gid 와 전체 그룹 확인; groups 사용자 그룹만 확인
userdel -r 홈 디렉터리까지 삭제; -r 이 없으면 고아 파일이 남는다
그룹 변경 후 이미 로그인한 세션에는 자동 반영되지 않으므로 다시 로그인해야 한다
useradd -m -s /bin/bash deploy usermod -aG docker deploy id deploy
누가 CPU 를 쓰고, 누가 파일을 잡고 있고, 어떻게 정상 종료시킬까 — 여기서 잘못된 조작은 대개 kill -9 를 너무 일찍 쓰는 것이다
ps aux 는 BSD 스타일: %CPU, %MEM, STAT, COMMAND, 일상에서 가장 흔히 사용
ps -ef 는 System V 스타일: UID, PID, PPID, C, STIME, CMD, 부모 프로세스를 볼 수 있다
-e 모든 프로세스 표시(System V 스타일), -o 출력 열 지정
섞어서 ps -aux 로 써도 실행되지만 의미가 달라진다, 습관으로 삼지 말 것
--sort=-%cpu CPU 역순, 음수는 내림차순, 쉼표로 여러 단계 정렬 가능
--sort=-rss 실제 메모리 역순; STAT 의 Z 는 좀비, D 는 중단 불가, R 은 실행 중
RSS 는 실제 점유, VSZ 는 가상 주소 공간이라 거의 참고 가치가 없다
ps aux --sort=-%cpu | head ps -eo pid,ppid,rss,pcpu,cmd --sort=-rss | head
top 에서 P 는 CPU, M 은 메모리, T 는 누적 시간 기준 정렬
1 은 CPU 코어별로 펼침, 단일 코어 포화인지 전체가 높은지 판단
c 전체 명령행 표시, 기본은 잘린다
H 스레드 표시; u 특정 사용자만; k 프로세스 종료
-b 배치 모드, -n1 한 번만 출력, 스크립트나 로그에 넣기 적합(-n 은 횟수)
-p 지정 PID 만, 여러 개는 쉼표 구분; pgrep 의 -d, 는 여러 PID 를 쉼표로 잇는 옵션
첫 줄 load average 세 값은 1/5/15 분 평균, 코어 수를 넘으면 대기 중이라는 뜻
top -b -n1 | head -15 # 스냅샷 1회 수집 top -p $(pgrep -d, nginx) # 지정한 프로세스만 확인
기본 kill 은 TERM(15) 전송, 프로세스가 정리하고 파일을 기록한 뒤 종료할 수 있다
-15 와 -TERM 은 동일, -TERM 이 더 읽기 쉬움; 일상에서는 이것으로 먼저 정중히 요청
-9 는 KILL, 커널이 즉시 회수하며 프로세스에 정리 기회가 없다 — DB, 메시지 큐는 데이터가 깨질 수 있다
-1 또는 -HUP 은 흔히 "설정 재적용"에 사용(nginx, sshd 모두 지원)
-0 은 신호를 보내지 않고 프로세스 존재만 확인, 반환 코드를 그대로 판단에 사용
kill 은 PID 를 받고 프로세스 이름은 받지 않는다; 이름으로 하려면 pkill 이나 killall
pkill -HUP nginx 는 이름으로 모든 nginx 프로세스에 HUP 전송
먼저 TERM 으로 몇 초 기다리고, 살아 있으면 KILL — 이 순서는 운영 규정에 거의 항상 적혀 있다
kill -TERM 1234 && sleep 3 kill -0 1234 && kill -9 1234 pkill -HUP nginx # 설정 다시 로드
& 는 백그라운드로 보낼 뿐, 터미널을 닫으면 SIGHUP 을 받고 함께 종료된다
nohup 은 SIGHUP 을 무시하고 출력은 기본적으로 nohup.out 으로(명시적 리다이렉트 권장)
2>&1 stderr 를 stdout 에 합침, 아니면 오류 메시지가 어디로 갔는지 알 수 없다
< /dev/null 표준 입력을 빈 장치에 연결, 백그라운드 프로세스가 터미널 읽기로 멈추는 것을 방지
setsid 현재 세션에서 분리, nohup 보다 더 확실하다
disown 이미 백그라운드에 있는 작업을 셸 작업 목록에서 제거
진짜 장기 서비스는 systemd 를 써야 하고, nohup 은 임시 검증용에만 적합
nohup ./run.sh > /var/log/run.log 2>&1 & setsid ./worker.sh < /dev/null > /var/log/worker.log 2>&1 &
lsof -i :8080 누가 8080 포트를 잡고 있는지(-i 는 네트워크 주소 기준 필터)
-p PID 특정 프로세스가 연 모든 파일만
-u 사용자 사용자 기준 필터; -n 호스트 이름을 해석하지 않아 빠르고 DNS 로 멈추지 않는다
+D /path 특정 디렉터리를 연 프로세스를 재귀 조회(매우 느리므로 큰 디렉터리에서는 주의)
+L1 은 "링크 수가 1 미만" 즉 삭제됐지만 열려 있는 파일만 — 디스크가 반환되지 않는 주범
뒤에 붙는 sort -k7 -n 은 정렬 기법: -k7 은 7번째 열(SIZE/OFF), -n 은 숫자 비교
lsof 가 없으면 fuser -v 또는 /proc/PID/fd 로 대체
lsof -i :8080 lsof -n +L1 | sort -k7 -n | tail # 크기순으로 점유 중인 삭제된 파일 확인
nice 값 범위는 -20(최고 우선)부터 19(최저), 일반 사용자는 값을 올릴 수만 있고 내릴 수 없다
nice -n 19 명령 실행 시점부터 우선순위를 낮춤, 백업·압축 같은 배치 작업에 적합
-n 뒤에 오는 값이 nice 값이며 "횟수"가 아니다
renice -n 10 -p PID 실행 중인 프로세스 변경; -p PID 지정, -u 사용자 단위 일괄 변경
taskset -c 0-3 코어 고정: -c 로 CPU 목록 지정, 다중 인스턴스가 같은 코어를 다투는 문제 완화
taskset -cp 0-3 1234 의 -p 는 "이미 있는 프로세스 조작", 뒤에 PID
우선순위를 낮춘다고 자원 부족이 해결되지는 않는다, 핵심 업무의 방해만 줄일 뿐
예시에서 tar -czf 의 -c 생성, -z gzip, -f 파일 이름 지정(tar 항목 참고)
nice -n 19 tar -czf backup.tar.gz /srv/data renice -n 10 -p 1234 taskset -cp 0-3 1234
watch 는 기본 2초마다 명령을 다시 실행하고 화면을 지운다, 한 번의 스냅샷이 아니라 "변화"를 본다
-n 1 1초마다 갱신; -n 뒤에는 초 단위(0.5 도 가능)
-d 두 출력 사이에 달라진 부분을 강조, 어디가 움직이는지 한눈에 보인다
-d=cumulative 누적 강조, 변한 위치에 표시가 계속 남는다 — 간헐적 변동 추적에 유용
-t 제목 줄(상단의 명령과 간격)을 표시하지 않음, 출력이 더 깔끔해진다
명령은 반드시 작은따옴표로 감싸야 한다, 아니면 파이프를 현재 셸이 먼저 해석해 앞부분만 watch 에 전달된다
고빈도 지표(연결 수, QPS) 감시에는 쓰지 말 것, vmstat 1 / sar 를 쓰고 watch 는 중간 과정을 놓친다
예시에서 ss -s 는 연결 요약, wc -l 은 줄 수 집계(-l 이 줄 수), 각 항목 참고
watch -n 1 -d 'ss -tn state established | wc -l' watch -d=cumulative -t 'ss -s'
screen -S deploy 이름 있는 세션 시작; 터미널을 닫거나 네트워크가 끊겨도 작업은 계속 실행된다
-list 모든 세션 나열(-ls 로도 가능), Attached 상태의 옛 세션이 있는지 먼저 확인
-r deploy 지정 세션에 다시 접속; Attached 라면 다른 곳에 붙어 있으므로 -d -r 로 강제 인수
Ctrl-a d 는 screen 의 분리 키; Ctrl-a " 는 창 목록; Ctrl-a k 는 현재 창 종료
tmux new -s deploy 새로 생성, tmux attach -t deploy 다시 접속(-t 는 이름 지정)
tmux 에서 Ctrl-b d 로 분리, Ctrl-b [ 로 스크롤 모드에 들어가 오래된 출력까지 거슬러 볼 수 있다
마이그레이션, 백업, 대용량 전송 전에 세션을 먼저 띄운다 — 네트워크가 끊겨도 작업은 멈추지 않지만 출력은 볼 수 없다
screen -S deploy # 이후 Ctrl-a d 로 분리 screen -list screen -r deploy tmux new -s deploy
서비스가 뜨지 않을 때는 "수신하지 않음", "허용되지 않음", "연결 불가" 중 무엇인지 먼저 구분해야 한다 — 이 세 단계 순서를 틀리면 오래 헤맨다
-t TCP; -u UDP; -l 수신 대기만; -n 서비스 이름 해석 안 함(숫자라 더 빠름); -p 프로세스 표시
-tulnp 는 일상에서 가장 흔한 조합, 다섯 글자가 각각 한 가지를 담당
-tnp 는 l 이 빠진 형태로 "이미 연결된 프로세스까지" 확인, 연결 수 점검에 사용
-a 이미 연결된 연결까지 나열; -s 요약 통계, 연결 수가 급증할 때 먼저 확인
state established 상태 기준 필터, grep ESTAB 보다 정확(ESTAB 은 열 이름의 축약)
netstat 는 폐기되었고 최신 시스템에는 대개 없다, ss 를 바로 사용
127.0.0.1 과 0.0.0.0 수신은 전혀 다르다: 전자는 로컬만 접속 가능, "포트는 열렸는데 외부에서 안 된다"의 1순위 원인
ss -tulnp ss -tnp state established | head ss -s
-s 무음: 진행 표시와 오류 메시지를 숨김, 스크립트에서는 거의 필수
-S 는 -s 와 함께 쓰며 오류를 다시 표시 — -sS 는 "조용히, 그러나 오류는 알린다"
-i 응답 헤더까지 출력; -I 헤더만(HEAD 요청, body 는 보지 않음)
-v 핸드셰이크와 요청 과정 전체 출력, 어느 단계에서 막히는지 한눈에 보인다
-L 리다이렉트 따라가기; 301/302 체인 점검에는 필수
-X 메서드 지정; -d body 전송(기본 POST); -H 요청 헤더 추가
-o /dev/null body 버리기, -w 와 함께 쓰면 상태 코드와 소요 시간만 출력
-w '%{http_code} %{time_total}' 사용자 지정 출력, 눈으로 보는 것보다 빠르다
-m 10 전체 요청 최대 10초; --connect-timeout 3 은 연결 단계만
--resolve 도메인:443:IP IP 를 직접 묶어 DNS 우회, 해석 문제인지 판별
-x http://proxy:8080 프록시 경유; --noproxy '*' 강제 직접 연결(로컬 점검에서는 거의 항상 필요)
-k 인증서 검증 생략 — 진단 시에만 사용하고 스크립트에 넣지 말 것
-w 안의 \n 은 줄바꿈, 없으면 모두 한 줄로 붙는다
curl -sS -o /dev/null -w '%{http_code} %{time_total}s\n' https://example.com/
curl -m 10 --connect-timeout 3 -sS -o /dev/null \
-w 'dns:%{time_namelookup} conn:%{time_connect} ttfb:%{time_starttransfer} total:%{time_total}\n' \
https://example.com/api
curl -iL --resolve example.com:443:1.2.3.4 https://example.com/
curl -v -X POST -H 'Content-Type: application/json' -d '{"a":1}' http://127.0.0.1:8080/apinc -zv 호스트 포트 — -z 는 데이터 없이 탐지만, -v 는 결과 표시, 가장 빠르다
-w3 연결 타임아웃 3초, 명령이 멈추지 않도록(생략하면 오래 기다린다)
telnet 호스트 포트 는 구형 시스템에 반드시 있고, Connected 가 보이면 통한다
bash 내장 /dev/tcp 는 아무것도 설치할 필요 없다: echo > /dev/tcp/호스트/포트
refused 는 호스트까지 도달했지만 수신하는 프로세스가 없다는 뜻; 타임아웃은 방화벽이나 보안 그룹에 막힌 것 — 두 오류의 의미는 전혀 다르다
머신 간 점검은 항상 "로컬 ss → 로컬 curl 127.0.0.1 → 같은 서브넷 nc → 외부 nc" 순으로 좁혀 간다
nc -zv -w3 10.0.0.5 5432 echo > /dev/tcp/10.0.0.5/5432 && echo open || echo closed
ip a 전체 NIC 주소 확인(구 ifconfig 와 동일); -4 IPv4 만, -6 IPv6 만
ip r 라우팅 테이블 확인, default 줄이 기본 게이트웨이
ip neigh ARP 이웃 테이블 확인, 같은 서브넷이 안 되면 여기 항목이 있는지 본다
ip -s link 송수신 패킷과 오류 카운터 확인, -s 는 statistics; errors/dropped 가 0 이 아니면 링크 계층 문제
show eth0 특정 NIC 만 표시, 가상 NIC 들에 출력이 묻히지 않는다
add / del 임시 주소 추가·삭제, 재부팅하면 사라진다
영구 설정은 배포판에 따라: Debian 계열 netplan 또는 /etc/network/interfaces, RHEL 계열 nmcli
ip -4 a ip r ip -s link show eth0
+short 결과만 출력, 스크립트에 적합
+A / +AAAA / +MX 레코드 타입 지정(생략하면 A 조회)
@8.8.8.8 DNS 서버 지정, "로컬 해석 vs 공인 해석" 비교에 사용
+trace 루트부터 단계별 추적, 어느 계층의 위임이 틀렸는지 파악
조회가 안 되면 status 부터 본다: NXDOMAIN 은 레코드 없음, SERVFAIL 은 서버 문제
TTL 은 레코드 변경 후 얼마나 기다려야 하는지를 결정; ANSWER SECTION 의 남은 TTL 이 캐시 잔여 시간
dig 는 DNS 계층만 본다. 해석은 되는데 접속이 안 되면 원인은 그 이후의 네트워크나 포트다
dig +short example.com dig @8.8.8.8 example.com A dig +trace example.com
-i any 모든 NIC(구체적 NIC 이름도 가능); -n 도메인과 포트 이름을 해석하지 않아 빠르고 DNS 에 붙잡히지 않는다
-nn 은 "IP 와 포트를 모두 숫자로", 점검 시 -n 보다 명확
port 443 해당 포트만; host 1.2.3.4 해당 호스트만; 둘은 and / or 로 조합
-c 100 패킷 100개를 잡으면 중지, 화면이 넘치는 것을 방지
-w file.pcap 저장해 Wireshark 로 분석, 터미널에서 읽는 것보다 훨씬 낫다
-A ASCII 로 페이로드 확인, -X 16진수 + ASCII(평문 프로토콜에서만 의미)
tcp[tcpflags] & tcp-rst != 0 RST 패킷만 캡처, "누가 먼저 끊었나" 판단
root 가 필요; 운영 환경에서는 반드시 -c 와 필터 조건을 붙인다
tcpdump -i any -nn -c 50 'port 443 and host 1.2.3.4' tcpdump -i eth0 -nn -w /tmp/cap.pcap 'tcp[tcpflags] & tcp-rst != 0'
-connect 도메인:443 접속해 상대 인증서 확인, HTTP 가 아니라 TLS 계층만 본다
-servername 도메인 은 반드시 기입(SNI), 없으면 같은 IP 의 여러 도메인이 잘못된 인증서를 받는다
-showcerts 인증서 체인 전체 출력, 중간 인증서 누락 여부를 판별
-noout 인증서 원문은 출력하지 않고 뒤에서 요구한 필드만 출력
-dates notBefore / notAfter 확인, 만료 점검의 첫 단계
-subject 인증서가 누구에게 발급됐는지; -ext subjectAltName 어떤 도메인을 포함하는지
-checkend 2592000 30일(초) 내 만료 여부 확인, 반환 코드를 모니터링에 바로 사용
</dev/null 을 붙여 s_client 가 읽고 종료하게 한다, 없으면 입력을 기다리며 계속 붙어 있다
openssl s_client -connect www.icetools.cn:443 -servername www.icetools.cn </dev/null 2>/dev/null \ | openssl x509 -noout -dates -subject openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null | head -30 openssl s_client -connect example.com:443 -servername example.com -checkend 2592000 </dev/null >/dev/null \ && echo '30 天内不过期'
firewall-cmd --list-all 현재 zone 에서 열린 항목 확인(RHEL 계열)
--add-port=8080/tcp 규칙 추가; --permanent 여야 영구 적용, 아니면 재시작 시 사라진다
--reload 영구 규칙 재적용, 추가 후 반드시 실행해야 런타임에 반영된다
ufw status verbose 규칙 확인(Ubuntu)
iptables -L 필터 테이블 나열; -n 이름 해석 안 함, -v 패킷 카운터 표시(규칙이 실제로 맞았는지 증명)
--line-numbers 규칙 번호 표시, 규칙 삭제 시 iptables -D INPUT 3 처럼 번호로 지정
-I INPUT 1 맨 앞에 삽입, -A 는 끝에 추가 — 순서가 결과를 결정
규칙에서 허용했는데도 안 되면 클라우드 보안 그룹을 먼저 의심, 그것은 이 머신 밖에 있다
firewall-cmd --list-all firewall-cmd --add-port=8080/tcp --permanent && firewall-cmd --reload iptables -L INPUT -n -v --line-numbers
"디스크가 찼다"에는 원인이 네 가지로 완전히 다르다: 파일 수, 단일 파일 크기, inode 고갈, 삭제된 파일이 점유 중
1단계 df -h 로 어느 마운트 지점이 찼는지 확인, 서비스 재시작부터 하지 말 것
2단계 df -i 로 inode 확인. inode 가 차면 "장치에 공간 없음" 오류가 나지만 df -h 는 여유가 있다고 나온다
3단계 du -h --max-depth=1 로 단계별로 내려가며 큰 디렉터리 탐색(--max-depth 는 단계 수)
4단계 두 수치가 맞지 않으면 lsof +L1 로 프로세스가 잡고 있는 삭제된 파일을 찾고, 재시작하거나 그 프로세스를 kill 해야 해제된다
awk 문장은 7번째 열(바이트 수)을 누적해 MB 로 환산, 눈으로 어림하는 것보다 빠르다
로그 급증은 보통 1단계에서 드러난다, 급히 지우지 말고 어떤 오류가 도배하고 있는지 먼저 확인
df -h; df -i
du -h --max-depth=1 / | sort -h | tail
lsof +L1 2>/dev/null | awk '{s+=$7} END {print s/1024/1024" MB held"}'기본 출력은 트리 구조, LVM·파티션·마운트 지점의 종속 관계를 볼 수 있다
-f 파일 시스템 타입, UUID, 마운트 지점 표시 — fstab 작성 전 필수
-p 전체 장치 경로 표시, /dev/sda 와 /dev/sda1 을 혼동하지 않도록
-d 하위 장치를 펼치지 않고 디스크 전체만
-o 출력 열 지정, NAME,SIZE,FSTYPE,MOUNTPOINT,UUID 가 가장 실용적인 조합
증설 후 lsblk 는 새 용량인데 df 는 그대로라면 파일 시스템 resize 가 아직 안 된 것
lsblk -f lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINT,UUID
클라우드 디스크 증설은 두 단계: 파티션 확장 후 파일 시스템 확장, 하나를 빼면 df 는 변하지 않는다
growpart /dev/vda 1 파티션 확장(cloud-utils-growpart 패키지), 두 번째 인자는 파티션 번호
xfs_growfs / XFS 확장(확대만 가능, 축소 불가)
resize2fs /dev/vda1 ext4 확장(확대·축소 모두 가능, 축소 전에는 반드시 umount 후 점검)
LVM 은 lvextend 단계가 하나 더 필요하고 그다음 파일 시스템을 resize
마지막에 df -h 로 용량이 실제로 바뀌었는지 확인(-h 는 human-readable)
작업 전 스냅샷을 먼저, resize 도중 전원이 끊기면 데이터가 유실될 수 있다
growpart /dev/vda 1 xfs_growfs / # XFS resize2fs /dev/vda1 # ext4 df -h /
mount -o 자주 쓰는 옵션: ro 읽기 전용, remount 재마운트, noatime 쓰기 증폭 감소
remount 가 가장 흔하다: 마운트 옵션을 바꿔도 umount 없이 서비스 중단 없이 적용
umount 가 device is busy 를 보고하면 lsof +D 또는 fuser -vm 으로 점유자 탐색
fstab 에는 /dev/sdb 대신 UUID 사용을 권장, 장치 이름은 바뀔 수 있다
fstab 네 번째 열에 defaults,noatime; 다섯·여섯 번째 열은 dump 와 fsck 순서, 일반 데이터 디스크는 0 0
fstab 수정 후 반드시 mount -a 로 검증; 잘못 쓰면 다음 부팅에서 시스템에 들어가지 못한다
mount -a 는 fstab 기준으로 마운트되지 않은 것을 모두 마운트, 오류가 나면 어느 줄인지 바로 알려준다
mount -o remount,rw / fuser -vm /mnt/data mount -a # fstab 작성법 검증
파일마다 inode 하나를 차지하며 inode 총수는 포맷 시점에 정해진다
증상은 df -h 에는 공간이 있는데 파일 쓰기에서 No space left on device
df -i 로 IUse% 확인, 100% 에 가까우면 이것이 원인
-xdev 파일 시스템을 넘지 않아 다른 마운트 지점까지 집계되는 것을 방지
-type f 파일만 집계, 디렉터리는 제외(디렉터리도 inode 를 쓰지만 파일 분포를 먼저 보는 편이 효과적)
sed 는 경로의 마지막 / 이후를 잘라 디렉터리 이름을 얻는다; sort | uniq -c 로 디렉터리별 파일 수 집계
흔한 주범: 세션 파일, 업로드 임시 파일, mail spool, 수십만 개 작은 파일이 있는 캐시 디렉터리
근본 해결은 재포맷 후 -i 값을 키우는 것, 단기적으로는 작은 파일 정리만 가능
마지막 sort -rn 은 "숫자 내림차순": -r 역순, -n 숫자 비교
df -i find /var -xdev -type f | sed 's#/[^/]*$##' | sort | uniq -c | sort -rn | head
cron 이 안 도는 이유는 대개 PATH 와 출력 리다이렉션; 시간이 틀리면 인증서, JWT, 로그까지 연달아 문제가 된다
crontab -l 현재 사용자의 예약 작업 나열; -e 편집; -r 전체 삭제(재확인 없음, 손 조심)
-u 사용자 지정 사용자의 crontab 조작(root 로 실행해야 함)
다섯 필드는 순서대로 분 시 일 월 요일: 0 3 * * * 은 매일 3:00
*/5 5분마다; 1-5 월요일부터 금요일; 0,12 는 0시와 12시에만
cron 의 환경은 로그인 셸과 완전히 다르고 PATH 가 매우 짧다 — 스크립트에는 절대 경로를 쓴다
cron 은 ~/.bashrc 를 읽지 않는다, 환경 변수는 crontab 상단에 쓰거나 스크립트에서 source
출력은 반드시 리다이렉트: >> /var/log/x.log 2>&1, 아니면 오류는 메일로만 가서 영영 볼 수 없다
왜 안 돌았는지 확인: Debian 계열 journalctl -u cron, RHEL 계열 /var/log/cron
예시에서 curl -sS 는 조용하되 오류는 알림(-s 무음, -S 오류 표시), -o /dev/null 은 body 버리기
crontab -e crontab -l # 매일 3:30 백업, 출력은 로그로 30 3 * * * /srv/app/backup.sh >> /var/log/backup.log 2>&1 # 5분마다 헬스 체크 1회 */5 * * * * /usr/bin/curl -sS -o /dev/null http://127.0.0.1:8080/health
장점이 실질적이다: 로그가 journalctl 로, 다음 실행 시각 확인 가능, 놓친 실행 보충 가능
OnCalendar 는 달력 시각: *-*-* 03:30:00 매일 3:30; OnUnitActiveSec=5min 5분마다
Persistent=true 종료 중 놓친 작업을 부팅 후 한 번 보충 — cron 은 불가능
Unit= 실행할 service 지정, 생략하면 같은 이름의 .service
systemctl list-timers 전체 타이머와 다음 실행 시각 확인
enable --now 여야 "부팅 시 자동 시작 + 즉시 적용", enable 만 하면 바로 실행되지 않는다
테스트 시 start 대상은 .service 이지 .timer 가 아니다, timer 를 start 해도 작업이 즉시 실행되지는 않는다
마지막 줄 journalctl -u 로 unit 지정, -n 30 최근 30줄, --no-pager 페이징 없음
# /etc/systemd/system/backup.timer [Unit] Description=Daily backup [Timer] OnCalendar=*-*-* 03:30:00 Persistent=true [Install] WantedBy=timers.target # 활성화 및 확인 systemctl list-timers systemctl enable --now backup.timer journalctl -u backup -n 30 --no-pager
date 현재 시간과 시간대 확인; +%F 는 2026-10-29, +%T 는 시분초, +'%F %T' 가 가장 흔하다
-d '-1 hour' 한 시간 전 계산, -d 'next monday' 도 가능 — 스크립트의 시간 창 계산은 전부 -d
-s 시스템 시간 직접 변경; 운영에서는 수동 변경 금지, 로그 순서와 인증서 검증이 깨진다
timedatectl 시간대와 NTP 동기화 상태 확인, date 보다 정보가 많다
set-timezone Asia/Shanghai 시간대 변경(국내 장비의 흔한 문제는 UTC 를 그대로 둔 것)
set-ntp true 자동 시간 동기화 활성화; 컨테이너의 시간은 호스트에서 상속
시간이 틀리면 결과가 구체적이다: 인증서 not yet valid, JWT 즉시 만료, 로그 시간 불일치, 예약 작업 오작동
date '+%F %T' date -d '-1 hour' '+%F %T' timedatectl timedatectl set-timezone Asia/Shanghai
chronyc tracking 현재 오차와 계층 확인, System time 줄이 "실제 시간과의 차이"
오차가 수백 밀리초를 넘으면 점검: 소스 도달 불가, 방화벽의 UDP 123 차단, 가상 머신 시계 드리프트
chronyc sources -v 각 시간 소스 확인, -v 는 상세 파라미터(지연, 지터, 선택 여부)
줄 앞의 ^* 가 실제 사용 중인 소스, ^? 는 도달 불가
chronyc makestep 시간을 즉시 한 번 점프, 오차가 클 때만 수동 실행하며 로그가 자동 기록된다
ntpq -p 는 구형 시스템 ntpd 의 대응 명령, -p 는 피어 목록 출력
클라우드 서버는 벤더 내부 NTP 를 우선, 공인 NTP 는 속도 제한이나 거부가 흔하다
chronyc tracking chronyc sources -v chronyc makestep ntpq -p # 구형 시스템(ntpd)
명령보다 순서가 중요하다: 먼저 시간으로 범위를 잡고, 키워드로 좁히고, 마지막에 문맥을 본다
-u 서비스명 특정 unit 만
-f 계속 추적; --since '10 min ago' 또는 --since '2026-10-29 09:00' 으로 시간 범위 지정
-p err error 이상만; 우선순위는 emerg 부터 debug 까지
-n 100 마지막 100줄; -r 역순(최신이 위)
--no-pager 페이징 없음, 파일로 리다이렉트하거나 파이프로 넘기기 편하다
-o short-iso 타임스탬프에 시간대 오프셋 포함, 시간대가 다른 환경 점검 시 필수
--disk-usage 로그가 차지한 디스크 확인; --vacuum-time=7d 7일치만 유지
--vacuum-size=500M 은 용량 기준 정리, --vacuum-time 과 택일
journalctl -u nginx -p err --since '30 min ago' --no-pager journalctl -u app -f -o short-iso journalctl --disk-usage
-f 파일 디스크립터를 따라가므로 로그가 로테이트(rename)되면 옛 파일을 계속 보며 "로그가 갱신되지 않는" 것처럼 보인다
-F 파일 이름을 따라가므로 로테이트 후 자동으로 새 파일로 전환 — 장시간 로그를 볼 때는 반드시 이것
-n 100 먼저 마지막 100줄을 보여주고 추적; -n0 은 과거 한 줄도 보지 않고 새 내용만 본다
-Fn0 은 -F 와 -n0 을 붙여 쓴 것, 실시간 스트림 확인에 가장 흔한 조합
--pid=PID 프로세스가 종료되면 추적을 자동 중지
grep --line-buffered 는 grep 이 줄마다 즉시 출력하게 한다, 없으면 버퍼링되어 파이프에서 한참 보이지 않는다
조합: tail -Fn0 app.log | grep --line-buffered -i error
여러 파일: tail -F a.log b.log 는 ==> 로 구분, 하나만 따라가지 말 것
tail -F /var/log/app/error.log tail -Fn0 /var/log/nginx/access.log | grep --line-buffered -i 500
OOM, 디스크 IO 오류, NIC link down, 드라이버 이상이 모두 여기에 있다
-T 사람이 읽을 수 있는 시간 표시; -w 계속 추적
-l err,warn 지정 레벨만(-l 뒤에 레벨 목록, 쉼표 구분)
dmesg 링 버퍼는 덮어써지므로 중요한 정보는 즉시 확보; journalctl -k 는 같은 데이터의 영구 보관본
"Out of memory: Killed process" 가 보이면 OOM killer 에 의해 종료된 것이고 프로그램이 스스로 죽은 것이 아니다
grep -iE 와 연결할 때: -i 대소문자 무시, -E 확장 정규식(| 가 동작, grep 항목 참고)
dmesg -T -l err,warn | tail -30 dmesg -T | grep -iE 'oom|killed process'
확인: dmesg -T | grep -i -A2 'killed process', -A2 는 일치 줄 뒤 2줄까지 표시(종료된 프로세스의 메모리 정보가 그곳에 있다)
journalctl -k 에 정보가 더 완전하며 당시 메모리 상태도 포함
systemctl status 에 status=9/KILL 또는 signal=KILL 이 보이면 사실상 OOM 에 의한 종료
--no-pager 출력이 페이저에 넘어가지 않도록, 스크립트와 캡처에 필요
프로그램 크래시가 아니다: 프로그램 로그가 갑자기 끊기고 예외 스택이 없다
원인 찾기: free -h 로 가용 메모리 확인(-h human-readable), 이어서 RSS 기준 정렬로 누가 늘고 있는지 확인
컨테이너에서는 cgroup 제한도 확인: /sys/fs/cgroup/memory.max
dmesg -T | grep -i -A2 'killed process' systemctl status app --no-pager | head -20 free -h
설정은 /etc/logrotate.d/ 에 두고 서비스마다 파일 하나
daily / weekly 로테이트 주기; rotate 7 은 7개 보관
compress 오래된 로그 압축, delaycompress 최신 1개는 압축하지 않음(기록 중인 파일 압축 방지)
copytruncate 복사 후 비우기, 파일을 다시 열 수 없는 프로그램에 적합하지만 로그 유실 구간이 있다
postrotate 에서 신호를 보내 프로세스가 파일을 다시 열게 함, copytruncate 보다 깔끔
-d 실행 없이 예행, 수정 후 반드시 한 번 돌려 무엇을 할지 확인
-f 한 번 강제 실행, 로테이트 주기를 기다리지 않고 설정 적용을 즉시 검증
missingok 파일이 없어도 오류 없음, notifempty 빈 파일은 로테이트 안 함 — 거의 모든 설정에 있어야 하는 두 옵션
# /etc/logrotate.d/app
/var/log/app/*.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
postrotate
systemctl reload app > /dev/null 2>&1 || true
endscript
}
# 먼저 예행 실행, 이후 강제 실행으로 검증
logrotate -d /etc/logrotate.d/app
logrotate -f /etc/logrotate.d/app서비스가 뜨지 않는 이유는 대개 세 가지다: 경로 오류, 사용자 권한 부족, 의존성 미기동 — 모두 status 에 적혀 있다
start / stop / restart / reload — reload 는 프로세스를 재시작하지 않고 설정만 다시 읽음, 프로그램이 지원해야 의미가 있다
status 최근 몇 줄의 로그와 종료 코드를 보여줌, 추측보다 유용; --no-pager 페이저 회피
enable 부팅 시 자동 시작; --now 는 동시에 즉시 시작
disable 자동 시작만 해제하고 중지는 않음; 중지하려면 stop 을 따로 실행
list-units --failed 어떤 것이 죽었는지 한눈에; --all 은 시작되지 않은 것까지
is-active / is-enabled 반환 코드를 스크립트 판단에 바로 사용
cat systemd 가 실제로 읽은 unit 을 출력(drop-in 오버라이드 포함), 수정 후 먼저 이것으로 확인
systemctl status app --no-pager systemctl enable --now app systemctl list-units --failed
/etc/systemd/system/이름.service 에 두고, 작성 후 반드시 daemon-reload
ExecStart 는 절대 경로여야 하고 프로그램이 스스로 fork 하면 안 된다(그렇다면 Type=forking)
Restart=on-failure 재시작 정책, RestartSec=5 와 함께 써야 무한 재시작을 피한다
User / Group 이 권한을 결정, 비 root 로 바꾸면 홈 디렉터리와 파일 소유자도 함께 바꿔야 한다
EnvironmentFile 환경 변수를 별도 파일로, ExecStart 에 직접 쓰지 말 것
WorkingDirectory 상대 경로에 영향을 준다, 이 항목이 없어 설정 파일을 못 찾는 프로그램이 많다
StandardOutput=journal 로그를 journalctl 로 보내 통합 점검에 유리
After 는 순서만 보장하고 성공을 보장하지 않는다, 네트워크 준비는 Wants=network-online.target
# /etc/systemd/system/app.service [Unit] Description=App After=network-online.target Wants=network-online.target [Service] Type=simple User=deploy WorkingDirectory=/srv/app EnvironmentFile=/etc/app/env ExecStart=/srv/app/bin/app Restart=on-failure RestartSec=5 StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target
1단계 systemctl status 로 Active 와 마지막 오류 몇 줄 확인, 대부분 여기서 판명된다
2단계 journalctl -u 서비스 -n 50 --no-pager 로 전체 오류 스택 확인(-n 은 줄 수)
3단계 ExecStart 경로가 존재하고 실행 가능한지 확인: ls -l 그 경로
4단계 직접 실행: sudo -u 서비스사용자 로 ExecStart 명령을 돌려 보면 대개 진짜 원인이 즉시 나온다
5단계 권한 확인: SELinux 는 ausearch -m avc -ts recent(-m 은 레코드 타입 avc, -ts 는 시작 시간)
AppArmor 의 거부 기록은 dmesg 에 있다; 컨테이너에서는 docker logs 도 확인
status=203/EXEC 는 경로나 권한 문제; status=1 은 프로그램 자체의 종료 코드 1
systemctl status app --no-pager journalctl -u app -n 50 --no-pager sudo -u deploy /srv/app/bin/app # 직접 실행해 실제 오류 확인 ausearch -m avc -ts recent # SELinux 거부 기록
.service 파일을 수정 → 반드시 daemon-reload, 아니면 systemd 는 옛 정의를 그대로 사용
EnvironmentFile 내용만 수정 → reload 는 불필요하지만 서비스를 restart 해야 반영
새 unit 생성 → reload 후에야 enable 가능
이미 start 된 서비스는 reload 로 새 설정을 자동 사용하지 않는다, restart 가 필요
reload 를 잊은 전형적 증상: ExecStart 를 바꿨는데 아무 변화가 없고 status 에 옛 명령이 그대로
cat 은 systemd 가 보는 이 파일의 모습을 확인(drop-in 오버라이드 결과 표시)
systemctl daemon-reload && systemctl restart app systemctl cat app # systemd가 보는 이 파일의 내용 확인
After 는 시작 순서만 담당하고 상대의 성공을 보장하지 않는다
Requires 강한 의존, 상대가 실패하면 이 서비스도 실패
Wants 약한 의존, 상대가 실패해도 이 서비스는 정상 기동 — 대부분의 상황은 이것
네트워크 관련: network.target 은 네트워크 스택 초기화만 뜻하고, 주소 사용 가능까지 기다리려면 network-online.target
After=network-online.target 은 Wants=network-online.target 과 함께여야 실제로 기다린다
서비스가 너무 일찍 떠서 "주소 해석 실패"가 나는 경우는 십중팔구 이것
의존성 충족 여부 확인: systemctl list-dependencies 서비스명, 트리 구조로 확인
[Unit] Wants=network-online.target After=network-online.target # 확인 systemctl list-dependencies app
설치 전에는 출처를 확인하고, 삭제 전에는 무엇을 함께 가져가는지 확인 — 운영 서버에서는 특히 그렇다
apt update 인덱스만 갱신하고 패키지는 업그레이드하지 않는다 — 가장 헷갈리기 쉬운 부분
apt upgrade 인덱스 기준으로 설치된 패키지 업그레이드; full-upgrade 는 의존성 때문에 패키지를 제거할 수 있어 주의
install -y 확인 없이 진행, 스크립트에 적합; -y 가 없으면 입력을 기다린다
apt purge 설정 파일까지 삭제; remove 만 하면 설정이 남는다
apt list --installed 설치된 패키지; apt list --upgradable(--upgradable 은 필터 조건) 업그레이드 가능한 패키지
apt-cache show 패키지명 버전과 설명 확인
apt-mark hold 버전 고정, 일괄 업그레이드에 휩쓸리지 않도록
apt update && apt install -y nginx apt list --upgradable apt purge nginx-common
dnf 는 yum 의 후속, 최신 시스템에서 yum 은 보통 심볼릭 링크
install / remove / update 세 동사가 apt 와 대응되지 않으니 섞어 치지 말 것
install -y 확인 없이 진행, apt 와 동일
provides 파일이나 명령으로 패키지 역조회, "이 명령은 어느 패키지인가"를 찾을 때
history 로 트랜잭션 기록 확인, history undo last 로 직전 롤백 — 업그레이드가 망가뜨렸을 때 유용
--enablerepo 임시로 특정 저장소 활성화; --nogpgcheck 는 정말 필요할 때만
module 메커니즘(dnf module list)은 한 소프트웨어의 여러 버전이 공존할 때 지정 필요
dnf install -y nginx dnf provides /usr/sbin/ss dnf history undo last
RHEL 계열: rpm -qf /usr/bin/ss — -q 는 조회 모드, -f 는 파일로 패키지 역조회
Debian 계열: dpkg -S /usr/bin/ss; 찾지 못하면 apt-file update 후 apt-file search
전체 경로를 모르면 명령 이름으로 역조회: dnf provides '*/ss' 또는 apt-file search bin/ss
dpkg -L 패키지명 해당 패키지가 설치한 파일 목록, 설정 수정 전에 유용
rpm -qc 패키지명 설정 파일만 나열(-qc 는 -q 에 -c(config) 를 더한 것)
명령을 찾지 못하면 PATH 오류가 아니라 아예 설치되지 않았는지 먼저 확인
rpm -qf /usr/bin/ss dpkg -S /usr/bin/ss dpkg -L nginx-common | head -20
apt clean .deb 캐시 전체 삭제; apt autoclean 만료된 것만 삭제
dnf clean all 캐시와 메타데이터 삭제
apt autoremove "의존성으로 설치됐고 지금은 필요 없는" 패키지 삭제, --purge 를 붙이면 설정까지
autoremove 에도 -y 를 붙여야 한다, 아니면 확인 프롬프트에서 멈춘다
정리 전에 du -sh 로 실제로 공간을 쓰는지 확인(-s 요약, -h 사람이 읽는 단위)
컨테이너 이미지에서 apt install 후 clean 하지 않으면 이미지가 수십 MB 불필요하게 커진다
du -sh /var/cache/apt /var/cache/dnf 2>/dev/null apt clean && apt autoremove --purge -y dnf clean all
컨테이너 문제는 대개 "생각한 설정"과 "실제 적용된 설정"의 불일치 — 먼저 inspect 로 사실을 맞추고 그다음 로그를 본다
-a 종료된 컨테이너까지 나열 — 실행 중인 것만 보면 방금 죽은 것을 놓친다
-q 컨테이너 ID 만 출력, docker rm 이나 docker logs 에 넘기기 편하다
-s 쓰기 가능 계층 사용량 표시, "컨테이너가 디스크를 채웠다"를 볼 때
--format 열 지정, '{{.Names}} {{.Status}}' 가 기본 표보다 읽기 쉽고 스크립트 처리에도 편하다
--filter 조건 필터: status=exited 는 종료된 것, name=web 은 이름으로
--no-trunc 긴 ID 와 전체 명령을 자르지 않음
-n 5 최근 생성된 5개만(--last 의 축약)
Status 에 Up 3 seconds (restarting) 이 보이면 무한 재시작, docker logs --tail 로 이유 확인
# 모든 컨테이너(종료된 것 포함)
docker ps -a --format '{{.Names}} {{.Status}}'
# 종료/반복 재시작 중인 것 찾기
docker ps -a --filter status=exited -n 5
# 디스크를 가득 채운 주체
docker ps -s --format '{{.Names}} {{.Size}}'-f 계속 추적, tail -f 와 동일
--tail 200 마지막 200줄만 — 오래 돌면 로그가 수만 줄이라 없으면 화면이 넘쳐 문제가 안 보인다
-t 줄마다 타임스탬프 추가, 호스트 로그와 시간축 대조에 필수
--since 10m 최근 10분만; 2026-10-29T10:00:00 같은 절대 시각도 가능
--until 은 --since 와 함께 구간을 잘라낸다
-i 는 grep 에서 대소문자 무시, error / ERROR / Error 를 한 번에 잡는다
로그는 /var/lib/docker/containers/<ID>/<ID>-json.log 에 있고 컨테이너를 지우면 사라진다 —
중요 서비스는 journald 나 외부 수집으로 보내고 docker 안에만 두지 말 것
docker logs -f --tail 200 -t myapp docker logs --since 10m myapp | grep -i error
-it 대화형 터미널; -t 가 없으면 프롬프트가 없고 -i 가 없으면 입력이 안 되므로 둘을 함께 준다
-u root root 로 진입 — 이미지 기본 사용자가 설정 파일을 못 읽을 때
-w /app 작업 디렉터리 지정, 들어가서 cd 할 필요가 없다
-e KEY=val 환경 변수 임시 주입, "환경 변수가 잘못 전달됐나" 검증
-- 뒤 인자는 docker 가 해석하지 않는다, 명령 자체가 - 로 시작하면 반드시 필요
-c 는 bash 에서 뒤 문자열 전체를 명령으로 실행, bash -c 'a && b' 로 여러 단계를 한 번에
-l 은 ls 에서 긴 형식, 권한과 소유자를 확인
진입이 안 되면 컨테이너가 running 인지 먼저 확인: docker ps -a 에서 Exited 면 exec 는 반드시 실패하며,
그것은 exec 문제가 아니라 주 프로세스가 이미 종료된 것이다
이미지에 bash 가 없으면 sh 를 시도, alpine 기반은 보통 sh 만 있다
distroless 이미지는 sh 조차 없어 docker cp 로 파일을 꺼내거나 임시 디버그 컨테이너를 띄워야 한다
docker exec -it -u root myapp sh docker exec myapp env | grep -i database docker exec -it myapp bash -c 'ls -l /app'
--format Go 템플릿으로 필드 추출, -f 는 축약
자주 쓰는 경로: .State.Status 실행 상태, .State.ExitCode 종료 코드,
.NetworkSettings.Networks 컨테이너 IP, .Mounts 마운트, .Config.Env 환경 변수,
.HostConfig.RestartPolicy 재시작 정책
{{json .}} 원본 JSON 출력, jq 로 처리하면 편하다
--type image 컨테이너가 아닌 이미지 조회
compose 의 값은 명령행 -e 로 덮어써질 수 있다, inspect 로 본 것만 실제 적용값
.Config.Env 에는 비밀번호와 토큰이 흔히 있으므로 남에게 붙여넣기 전에 걸러낼 것
docker inspect -f '{{.State.Status}} exit={{.State.ExitCode}}' myapp
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' myapp
docker inspect --format '{{json .Mounts}}' myapp | jq--no-stream 한 번만 스냅샷하고 종료, 스크립트와 정기 기록에 적합
--format 출력 열 지정, grep 과 저장에 편하다
-a 중지된 컨테이너까지 집계(0 을 차지하지만 목록에 나온다)
MEM LIMIT 는 해당 컨테이너의 상한, MEM % 가 100 에 가까우면 곧 OOM
CPU % 는 코어 수로 환산, 다중 코어 머신에서 컨테이너 하나가 100% 를 넘는 것은 정상
컨테이너가 OOMKilled 되면 stats 로는 아무것도 볼 수 없다(프로세스가 이미 없다), docker inspect 의 ExitCode 137 과
dmesg 의 oom-kill 기록을 봐야 한다
docker stats --no-stream --format '{{.Name}} {{.CPUPerc}} {{.MemUsage}}'
docker stats --no-stream -a-t 태그 지정, 표준 표기는 이름:버전, latest 만 쓰지 말 것
-f Dockerfile 경로 지정(파일이 빌드 디렉터리에 없을 때)
--no-cache 캐시를 전혀 사용하지 않음 — "코드를 바꿨는데 이미지가 그대로"일 때 이것으로 배제
--build-arg 빌드 인자 전달, Dockerfile 에 ARG 로 선언된 것만 유효
--target 특정 단계까지만 빌드, 다단계 빌드의 중간 계층 디버깅
--progress=plain 전체 빌드 로그 출력; 기본 TTY 모드는 병렬 로그를 한 줄로 접어 오류 시 세부 내용이 안 보인다
마지막 . 은 빌드 컨텍스트이며 Dockerfile 경로가 아니다 — 컨텍스트가 크면 확실히 느려지므로 .dockerignore 로 node_modules 와 .git 을 제외
docker build -t myapp:1.2.3 . docker build -f docker/Dockerfile.prod --build-arg NODE_ENV=prod --progress=plain -t myapp:prod .
-f compose 파일 지정; 다중 환경은 -f 를 두 번 겹쳐 뒤가 앞을 덮어쓴다
-p 프로젝트 이름 지정 — 기본은 디렉터리 이름, 한 머신에 여러 환경이면 반드시 명시해야 컨테이너와 네트워크가 서로 덮어쓰지 않는다
-d 백그라운드 실행; 없으면 포그라운드로 붙어 Ctrl+C 로 중지
--build 시작 전 이미지 재빌드
up 생성 및 시작; down 컨테이너와 네트워크 중지 및 삭제
logs -f --tail 100 로그 추적; 끝에 서비스 이름을 쓰면 그것만 따라간다
config 문법 검증과 병합된 최종 설정 출력 — compose 수정 후 이것을 먼저 실행하는 편이 up 보다 안전
compose v2 는 docker compose(공백), v1 은 docker-compose(하이픈), 옵션이 미세하게 다르므로 스크립트에서 먼저 확인
docker compose -f docker-compose.yml -f docker-compose.prod.yml -p shop up -d --build docker compose -p shop logs -f --tail 100 api docker compose -p shop config
system df 는 이미지 / 컨테이너 / 볼륨 / 빌드 캐시 네 종류로 사용량 요약, -v 는 개별 객체까지
prune 사용하지 않는 객체 삭제; 기본은 댕글링 이미지(<none>)만, -a 를 붙여야 컨테이너가 참조하지 않는 모든 이미지 삭제
--volumes 데이터 볼륨까지 정리 — 볼륨에 DB 가 있을 수 있고 삭제하면 복구 불가
--filter until=24h 24시간 이전 것만 삭제, 사용 중인 것을 잘못 지우지 않도록
순서 권장: 먼저 system df -v 로 누가 차지했는지 확인하고 대상만 prune, 처음부터 system prune -a 를 쓰지 말 것
컨테이너 로그도 디스크를 채운다: /var/lib/docker/containers/*/*-json.log,
근본 해결은 daemon.json 의 log-opts 에 max-size 와 max-file 을 설정하는 것
docker system df -v docker image prune -a --filter until=24h docker system prune --volumes
K8s 장애 대응 순서는 거의 정해져 있다: 상태 → 이벤트 → 로그 → 컨테이너 진입. 한 단계를 건너뛰면 그것은 추측이다
-A 모든 네임스페이스 — 리소스를 못 찾으면 먼저 붙여 본다, 대개 ns 를 잘못 봤다
-n 네임스페이스 지정; 생략하면 현재 context 의 기본 ns
-o wide IP 와 노드 두 열이 추가, 네트워크와 스케줄링 점검에 필수
-o yaml 전체 정의 출력, 설정 수정 전에 내려받아 대조
--show-labels 레이블 표시 — Service 가 Pod 를 못 잡으면 대개 레이블 불일치
-l app=api 레이블 기준 필터, grep 이름보다 확실
--sort-by=.status.startTime 시작 시간 정렬, 최신이 맨 아래
리소스 이름에는 축약형이 있다: pod/po, svc, deploy, rs, no, cm, secret
kubectl get pod -A -o wide kubectl get pod -n shop -l app=api --show-labels kubectl get pod -A --sort-by=.status.startTime
끝부분의 Events 가 K8s 장애 대응에서 정보량이 가장 많은 곳 —
ImagePullBackOff, FailedScheduling, Unhealthy, OOMKilled 의 원인이 모두 여기에 적혀 있다
-n 을 빼고 네임스페이스를 잘못 보면 NotFound 만 나오고 유용한 힌트는 없다
Events 는 기본 최근 1시간만 보관, 사후 분석은 로그 수집에 의존하고 나중에 다시 볼 수 있다고 기대하지 말 것
describe node 하단의 Allocated resources 는 할당된 request / limit 비율을 보여준다,
Pod 가 계속 Pending 이면 노드가 request 로 가득 찼는지 먼저 확인(CPU 가 실제로 꽉 찬 것이 아니다)
describe 는 설정과 이벤트만 보고 애플리케이션 로그는 보여주지 않는다, 그것은 kubectl logs 의 역할
kubectl describe pod -n shop api-7d9f-abc12 kubectl describe node k8s-node-2 | tail -30
-f 계속 추적
-n 네임스페이스 지정, 생략하면 현재 context 의 기본 ns
--tail 200 마지막 200줄, Pod 가 오래 돌면 없을 경우 화면이 넘친다
-c 컨테이너 지정 — Pod 에 sidecar 가 있으면 필수, 아니면 ambiguous 와 사용 가능한 컨테이너 목록이 나온다
--previous 이전 컨테이너 로그 확인: CrashLoopBackOff 는 컨테이너를 반복 재시작하므로,
현재 컨테이너 로그는 "시작하자마자 죽은" 구간이고 진짜 사인은 이전 컨테이너에 있다
--since 10m 최근 10분만
--timestamps 줄마다 타임스탬프 추가
리소스는 deploy/api 처럼 "타입/이름" 형식으로 쓸 수 있어 특정 Deployment 의 Pod 로그를 바로 볼 수 있다
kubectl logs -f --tail 200 -n shop deploy/api kubectl logs -n shop api-7d9f-abc12 --previous kubectl logs -n shop api-7d9f-abc12 -c istio-proxy --since 10m
-it 대화형 터미널, 둘을 함께 줘야 한다
-n 네임스페이스 지정
-c 컨테이너 지정, 다중 컨테이너 Pod 에서 필수
-- 뒤는 kubectl 이 해석하지 않는다, 명령 자체가 - 로 시작하면 반드시 필요
cp 형식은 원본 대상, Pod 쪽은 pod:경로 로 쓴다
cp 는 컨테이너에 tar 가 있어야 하며 distroless 이미지에서는 바로 실패
kubectl debug 는 디버그 컨테이너를 임시 주입하고 네트워크 네임스페이스를 공유한다,
--image 디버그 이미지 지정(busybox 로 충분), --target 디버깅할 컨테이너 지정 —
운영 이미지에 셸이 없을 때 유일한 정답이며 이미지를 바꿔 재배포하는 것보다 훨씬 빠르다
kubectl exec -it -n shop api-7d9f-abc12 -- sh kubectl cp -n shop api-7d9f-abc12:/app/logs/app.log ./app.log kubectl debug -it -n shop api-7d9f-abc12 --image=busybox --target=api
describe 끝부분의 Events 와 같은 소스지만 리소스를 넘어 한 번에 볼 수 있어 "어느 객체가 문제인지 모를 때" 적합
-n 네임스페이스 지정
--sort-by=.lastTimestamp 시간 정렬, 최신이 마지막
--field-selector type=Warning 경고만 보기, 노이즈가 바로 한 자릿수 줄어든다
--field-selector involvedObject.name=api-7d9f-abc12 특정 객체로 좁히기
-A 모든 네임스페이스, 클러스터 수준 문제(노드 NotReady, 축출)는 붙여야 보인다
Warning 의 FailedScheduling 은 스케줄러의 원문을 담는다, 예를 들어
insufficient cpu, node(s) had taint — 스케줄이 안 되는 이유를 직접 알려주므로
자원인지 테인트인지 다시 추측할 필요가 없다
이벤트 기본 TTL 은 약 1시간, Pod 가 반복 재시작하면 즉시 기록해 둘 것
kubectl get events -A --sort-by=.lastTimestamp | tail -20 kubectl get events -n shop --field-selector type=Warning kubectl get events -n shop --field-selector involvedObject.name=api-7d9f-abc12
-f manifest 파일이나 디렉터리 지정; -f - 는 표준 입력에서 읽기
--dry-run=client 제출하지 않고 최종 YAML 만 출력 — 템플릿 생성에 사용, 직접 작성할 필요가 없다
--dry-run=server 서버에 제출해 검증(admission webhook 포함), client 보다 훨씬 엄격
-o yaml 은 dry-run 과 함께여야 출력이 의미 있다
rollout status 롤링 업데이트 완료를 기다림, 멈추면 어느 단계에서 걸렸는지 알려준다
rollout undo 이전 버전으로 롤백; --to-revision=3 지정 버전으로, history 로 버전 번호 확인
rollout restart 설정 변경 없이 Pod 만 재시작, ConfigMap 이나 Secret 수정 후 자주 사용
-n 네임스페이스 지정
apply 는 선언형이라 반복 실행이 멱등; create 는 명령형이라 두 번째에 AlreadyExists
kubectl apply -f deploy/api.yaml --dry-run=client -o yaml > api.gen.yaml kubectl rollout status deploy/api -n shop kubectl rollout undo deploy/api -n shop --to-revision=3 kubectl rollout restart deploy/api -n shop
형식은 리소스/이름 로컬포트:원격포트, NodePort 나 Ingress 가 필요 없다
-n 네임스페이스 지정
--address 0.0.0.0 같은 서브넷의 다른 머신도 접속 가능; 기본은 127.0.0.1 만 바인딩해 다른 곳에서는 붙을 수 없다
kube-apiserver 의 터널로 전달되며 Service 를 거치지 않는다 —
그래서 "서비스 자체가 통하는가"와 "네트워크 정책이 막았는가"는 별개 문제이고, 이것으로 전자를 먼저 배제한다
터미널을 끊으면 중지되므로 장기 사용은 tmux 나 screen 에 넣는다
점검 순서: port-forward 가 통하면 → 서비스는 정상이고 문제는 Ingress / Service / NetworkPolicy;
port-forward 가 안 되면 → Pod 나 포트 자체에 문제
kubectl port-forward -n shop svc/api 8080:80 kubectl port-forward -n shop --address 0.0.0.0 svc/api 8080:80 kubectl port-forward -n shop pod/api-7d9f-abc12 5432:5432
top node 와 top pod 는 metrics-server 에 의존, 없으면 Metrics API not available 오류
--sort-by=cpu 또는 --sort-by=memory 열 기준 정렬, 자원 소비 상위 탐색
--containers Pod 를 컨테이너 단위로 분해, 어느 sidecar 가 메모리를 먹는지 확인
-A 모든 네임스페이스
describe node 의 Allocated resources 는 request / limit 할당 비율 —
스케줄 실패 시 여기부터: 흔히 request 를 크게 써서 노드 "장부"가 가득 찼지만 실제 CPU 는 한가한 경우
cordon 노드를 스케줄 불가로 표시, 이미 실행 중인 Pod 에는 영향 없음
drain 유지보수를 위해 노드의 Pod 축출; --ignore-daemonsets DaemonSet 무시
(원래 축출 대상이 아니다), --delete-emptydir-data emptyDir 를 쓰는 Pod 삭제 허용(임시 데이터는 유실)
유지보수 후 uncordon 으로 스케줄 복구
kubectl top node kubectl top pod -A --sort-by=memory --containers kubectl drain k8s-node-2 --ignore-daemonsets --delete-emptydir-data
top 은 "이 프로세스가 CPU 를 쓴다"까지만 알려주고 "무엇을 기다리는가"에는 답하지 못한다. 이 그룹의 도구가 후자의 질문에 답한다
-p 실행 중인 프로세스에 부착; 명령을 바로 뒤에 붙여 시작부터 추적할 수도 있다
-f fork 로 생긴 자식 프로세스까지 추적 — 없으면 주 프로세스가 자식을 기다리는 것만 보이고 실제로 일하는 쪽은 안 보인다
-T 호출마다 소요 시간 표시(<0.000012>), 어느 단계가 느린지 찾는 근거
-tt 타임스탬프를 마이크로초까지
-s 200 문자열을 최대 200자까지 표시; 기본 32자는 핵심 경로를 ... 로 잘라 버린다
-o out.txt 파일로 출력, 수만 줄이 화면을 넘으면 분석이 불가능
-c 종료 시 시스템 콜 종류별 횟수와 총 소요 시간 집계 — 먼저 이것으로 어느 분류가 가장 비싼지 보고,
그다음 -T 로 개별 느린 호출을 집어낸다
-p 로 붙였는데 출력이 전혀 없으면 중단 불가 IO 나 커널 락에 걸린 것이므로,
다음은 /proc/PID/stack 과 /proc/PID/wchan 을 볼 차례
운영 서버에서는 주의: -f 로 전체 호출을 추적하면 프로세스가 몇 배 느려지므로 -c 나 필터로 완화
strace -p 1234 -T -tt -s 200 strace -f -p 1234 -o /tmp/st.txt strace -c -p 1234
-e trace= 는 strace 에서 가장 유용한 옵션이다, 이견의 여지가 없다
-e trace=file 파일 관련만(openat, stat, unlink), "파일을 못 찾음 / 권한 부족" 점검
-e trace=network 네트워크만(connect, sendto, recvfrom), "연결 불가 / 타임아웃" 점검
-e trace=openat,connect 쉼표로 구분해 특정 호출만 지정
-e trace=desc 파일 디스크립터 조작만, 어느 fd 에서 막히는지 판단
-e signal=SIGSEGV 신호만 추적
필터로 수만 줄을 수십 줄로 줄인다 — 올바른 순서는 먼저 -c 로 어느 분류가 가장 오래 걸리는지 보고,
그다음 -e trace= 로 그 분류만 따라가는 것
openat 이 ENOENT 를 반환하면 경로 없음, EACCES 는 권한 부족, EAGAIN 은 비블로킹에 데이터 없음,
세 오류 코드의 대응 방향이 완전히 다르니 error 만 보고 권한부터 뒤지지 말 것
나머지 옵션은 위의 strace -p 와 완전히 동일: -f 자식 추적, -p 프로세스 지정, -T 단일 소요 시간, -tt 마이크로초 타임스탬프, -o 파일 기록
strace -f -e trace=file -p 1234 -T strace -f -e trace=network -p 1234 -tt strace -o net.txt -e trace=openat,connect -f ./app
perf top 은 실시간 핫 함수 순위이며 PID 를 몰라도 전체 머신을 바로 본다
-p 특정 프로세스만
-F 99 샘플링 주기 99Hz, 100Hz 정수를 피해 타이머 공명으로 생기는 가짜 핫스팟을 회피
-g 호출 그래프 표시; top 에서 -g 는 시야가 제한적이라 실제 호출 체인은 record 로 봐야 한다
함수 이름이 unknown 이면 debuginfo 가 없거나 바이너리 심볼이 strip 된 것 —
perf 가 고장 난 것이 아니라 그 프로그램에 심볼 테이블이 없는 것이다
root 가 필요하거나 kernel.perf_event_paranoid 를 완화해야 하며, 컨테이너에서는 보통 --privileged 나 SYS_ADMIN 추가 필요
top 과의 차이: top 은 "어느 프로세스", perf top 은 "어느 함수"에 답하며 후자가 최적화의 출발점
perf top perf top -p 1234 -F 99 perf top -g
record 는 샘플 기록, report 는 대화형 확인, script 는 텍스트 출력 — 세 가지 조합
-F 99 초당 99회 샘플링
-g 호출 체인 기록, 플레임 그래프에 필수, 없으면 고립된 함수 이름만 남는다
-p 프로세스 지정; -a 모든 CPU 수집(전체 시스템)
--call-graph dwarf DWARF 정보로 스택을 펼침, 기본 fp 보다 정확하고
대가는 데이터 파일이 한 자릿수 커지는 것; 프로그램을 -fomit-frame-pointer 로 빌드했다면 반드시 이것
-o 출력 파일 지정, 기본은 현재 디렉터리의 perf.data
-i report / script 가 읽을 데이터 파일 지정
-- 뒤에 명령을 붙여 perf 로 프로그램을 실행하며 시작 단계부터 샘플링
플레임 그래프에는 script 의 접힌 출력이 필요하고, 이를 FlameGraph 의 stackcollapse 스크립트에 넘긴다
컨테이너의 perf_event_paranoid 기본값은 보통 2 라 커널 심볼이 보이지 않으며,
sysctl kernel.perf_event_paranoid=-1 이어야 완전하다
perf record -F 99 -g -p 1234 -o /tmp/perf.data perf report -i /tmp/perf.data perf script -i /tmp/perf.data > /tmp/perf.txt
pidstat 1 매초 출력하며 계속 갱신; 뒤에 숫자를 하나 더 쓰면 그 횟수만큼 출력하고 멈춘다
-p ALL 모든 프로세스 표시; -p 1234 하나만
-d 디스크 IO(kB_rd/s 읽기, kB_wr/s 쓰기, iodelay 대기 밀리초)
-r 메모리(페이지 폴트, RSS)
-u CPU 사용률(기본값)
-t 스레드 단위 분해, 어느 스레드가 CPU 를 쓰는지 — 다중 스레드 프로그램에는 필수
-w 컨텍스트 스위치, cswch/s 는 자발적(자원 대기), nvcswch/s 는 비자발적(선점됨)
sysstat 패키지에 포함, 최소 설치 시스템에는 sysstat 를 먼저 설치
top 보다 나은 점은 고정 간격으로 장시간 샘플링해 파일에 기록하고 사후에 평균과 곡선을 계산할 수 있다는 것
pidstat -p 1234 1 10 pidstat -d -p ALL 1 5 pidstat -t -p 1234 1
iostat -x 1 확장 통계, 매초 갱신
-x 확장 지표 출력, 없으면 다른 간이 표가 나온다
%util 이 100% 에 가까우면 디스크가 포화, await 는 IO 1회의 평균 대기 밀리초
rkB/s 와 wkB/s 는 초당 읽기·쓰기량 — await 가 크고 처리량이 작으면 디스크 자체가 느린 것이고,
await 가 크고 처리량도 크면 실제로 바쁜 것이며 두 경우의 증설 방향이 다르다
vmstat 1 매초 한 줄: r 실행 큐 길이(계속 코어 수보다 크면 CPU 부족),
b 블로킹 프로세스 수, si / so 스왑 인·아웃(0 이 아니면 메모리 부족으로 swap 사용 시작)
wa 는 CPU 시간 중 IO 대기 비율, 높으면 CPU 가 아니라 디스크에 걸린 것
-d 는 vmstat 에서 디스크 통계, 디스크 열만 표시
첫 출력은 부팅 이후 평균이므로 둘째 줄부터가 실시간 값 — 첫 줄로 결론 내지 말 것
iostat -x 1 5 vmstat 1 5 vmstat -d 1
프로세스가 hang 상태이고 strace 에 아무 출력도 없으면(중단 불가 상태) 사용자 공간 스택이 어디에 걸렸는지 봐야 한다
gdb -p 로 부착하면 프로세스가 일시 정지된다 — 캡처 후 반드시 detach 하고 종료해야 한다, 아니면 프로세스가 계속 멈춰 서비스를 세운 것과 같다
-ex 는 gdb 에 명령 하나를 실행하고 계속하게 하며 여러 번 쓸 수 있어 "부착 → 스택 출력 → 분리"를 한 번에 처리
thread apply all bt 모든 스레드의 전체 스택 출력, 단일 스레드면 bt 로 충분
gcore 는 core 파일을 생성해 사후에 분석, 온라인 디버깅보다 안전
-o core 출력 경로 지정(.PID 접미사 없이)
Java 프로세스에는 gdb 를 쓰지 말 것 — JVM 스택은 gdb 가 이해하지 못하므로 jstack 을 쓴다
perf 의 --call-graph 는 샘플 지점을 잡고, hang 은 전체 스택을 봐야 하므로 서로 대체할 수 없다
gdb -p 1234 -ex 'thread apply all bt' -ex detach gcore -o /tmp/core.1234 1234
서버에는 vi 만 있는 경우가 많아 설정 수정에서 피할 수 없다. 키 20개면 충분하고 전부 배울 필요는 없다
vim +120 파일 바로 120번째 줄로 이동 — 오류 로그가 알려준 줄 번호를 볼 때 가장 빠른 방법
-u NONE 설정과 플러그인을 전혀 읽지 않음, "vimrc 가 잘못됐나"를 배제할 때
-w /tmp/keys.log 모든 키 입력 기록, "내가 대체 무엇을 눌렀나" 확인
:w 저장; :w! 강제 저장(파일이 읽기 전용이거나 권한이 부족할 때)
:q 종료, 저장하지 않은 변경이 있으면 거부; :q! 변경을 버리고 강제 종료
:wq 또는 :x 저장 후 종료, 차이는 :x 가 변경이 없으면 타임스탬프를 갱신하지 않는 것
:e! 현재 변경을 버리고 다시 읽기 — 잘못 고쳐 처음부터 할 때
:saveas /tmp/a.conf 다른 이름으로 저장, 설정 수정 전 백업의 표준 동작
읽기 전용 파일을 잘못 고쳤다면 :q! 가 가장 빠른 복구, 저장을 누르지 말 것
vim +120 /etc/nginx/nginx.conf vim -u NONE /etc/nginx/nginx.conf :w 保存 :q! 수정 포기하고 종료 :wq 保存退出 :x 변경 있을 때만 저장 :e! 重新加载 :saveas /tmp/a.conf 다른 이름으로 저장
gg 파일 첫 줄; G 파일 마지막 줄; 42G 42번째 줄로 이동
0 줄 맨 앞; ^ 줄의 첫 비공백 문자(들여쓰기 정렬 시 이것); $ 줄 끝
w 다음 단어 시작; b 이전 단어 시작; e 현재 위치 단어의 끝
Ctrl+f 다음 페이지; Ctrl+b 이전 페이지; Ctrl+d 반 페이지 아래; Ctrl+u 반 페이지 위
% 짝이 맞는 괄호로 이동 — 설정 블록의 닫는 위치를 찾을 때 특히 빠르다
'' 직전 위치로 복귀(작은따옴표 두 개); 그냥 Enter 도 된다
큰 파일에서 방향키는 쓰지 말 것, hjkl 과 위 조합이 훨씬 빠르다
수정 전에 :set nu 로 줄 번호 표시, 그래야 오류의 줄 번호가 의미 있다
gg 文件首行 G 파일 마지막 줄 42G 第 42 行 0 줄 맨 앞 ^ 行首首个非空白 $ 줄 맨 끝 w / b / e 下词首 / 上词首 / 词尾 Ctrl+f 下一页 Ctrl+b 이전 페이지 % 跳到匹配的括号 '' 마지막 위치로 복귀
dd 한 줄 삭제 — 실제로는 잘라내기라 p 로 붙여넣을 수 있다
yy 한 줄 복사; 5yy 다섯 줄 복사
p 현재 줄 아래에 붙여넣기; P 위에 붙여넣기
u 실행 취소; Ctrl+r 다시 실행(취소를 과하게 했을 때)
x 문자 하나 삭제; r 현재 문자를 다른 문자 하나로 교체, 삽입 모드로 들어가지 않는다
cc 한 줄 전체를 다시 쓰기, 내용은 비우고 들여쓰기는 유지
d$ 줄 끝까지 삭제; d0 줄 맨 앞까지 삭제; dw 단어 하나 삭제
. 직전 수정 반복 — Vim 에서 가장 값진 키, 20줄 삭제는 dd 후 . 을 19번
i 커서 앞 삽입; a 커서 뒤 삽입; o 아래에 새 줄; O 위에 새 줄
Esc 일반 모드로 복귀, 잊으면 문자가 잔뜩 찍힌다
dd / yy / p 줄 삭제 / 줄 복사 / 붙여넣기 5yy 5줄 복사 u / Ctrl+r 실행 취소 / 다시 실행 x / r 문자 삭제 / 문자 하나 교체 cc 줄 전체 수정(들여쓰기 유지) . 마지막 수정 반복(가장 값진 키) i / a / o / O 앞 삽입 / 뒤 삽입 / 아래 새 줄 / 위 새 줄
/pattern 아래로 검색; ?pattern 위로 검색
n 다음 일치로 이동; N 이전 일치로 이동
* 커서 아래 단어를 입력 없이 바로 검색; # 는 역방향 검색
:set hlsearch 모든 일치 강조; :noh 강조 해제(검색 후 화면이 노랗게 덮였을 때)
:%s/old/new/g 전체 치환, g 는 각 줄의 모든 일치; 없으면 각 줄 첫 번째만
:%s/old/new/gc 치환마다 확인 — 설정 파일 수정에는 c 가 필수, 한 번의 잘못된 치환으로 서비스가 뜨지 않을 수 있다
:10,20s/old/new/g 10~20번째 줄만 치환
:.,$s/old/new/g 현재 줄부터 파일 끝까지
구분자는 바꿀 수 있다: :s#/usr/local/old#/usr/local/new#g, 경로에 / 가 있어도 계단식 이스케이프가 필요 없다
치환 전에 /pattern 으로 일치 수를 세어 두면 마음의 준비가 된다
:%s/old/new/g 전체 치환(각 줄의 모든 일치) :%s/old/new/gc 치환마다 확인(설정 변경 시 반드시 c 추가) :10,20s/old/new/g 10-20행만 치환 :.,$s/old/new/g 현재 줄부터 파일 끝까지 :s#/usr/local/old#/usr/local/new#g 구분자 변경, 경로 이스케이프 불필요 :noh 검색 하이라이트 해제
Ctrl+v 블록 선택 진입, 커서를 움직여 한 열을 상자 선택 — 일괄 주석과 정렬이 여기에 달려 있다
선택 후 I 는 각 줄 맨 앞에 동시 삽입, 입력 후 Esc 를 눌러야 실제 반영(안 누르면 반응이 없는 것처럼 보인다)
선택 후 A 는 각 줄 끝에 동시 추가
선택 후 c 는 블록 전체 교체, d 는 블록 즉시 삭제
:g/pattern/d 일치하는 모든 줄 삭제
:g/pattern/s/old/new/g 일치하는 줄에만 치환, 전체 치환보다 안전
:sort 선택한 줄 정렬; :sort u 정렬과 중복 제거
:normal @a 선택한 각 줄에 레지스터 a 의 매크로를 한 번 실행, 반복 작업에 사용
:set nu 로 줄 번호를 먼저 켜 두면 블록 작업에서 범위를 잘못 보기 어렵다
Ctrl+v I Esc 블록 선택 후 각 줄 맨 앞에 삽입(일괄 주석) Ctrl+v A Esc 블록 선택 후 각 줄 맨 끝에 추가 :g/^#/d #으로 시작하는 모든 줄 삭제 :g/ERROR/s/old/new/g ERROR가 포함된 줄에서만 치환 :sort u 선택한 줄 정렬 후 중복 제거
웹이나 Windows 에서 코드를 복사해 오면 들여쓰기가 점점 오른쪽으로 밀린다: 먼저 :set paste 후 붙여넣고, 끝나면 :set nopaste
(:set pastetoggle=<F2> 로 F2 를 토글키로 쓸 수도 있다)
Windows 에서 편집한 파일은 줄 끝에 ^M 이 붙는다: :set ff=unix 후 :w 로 저장
:set ff 를 단독 실행하면 현재가 dos 인지 unix 인지 확인
한글 깨짐: :set fileencoding=utf-8 후 저장; 표시만 깨지고 파일은 건드리고 싶지 않다면,
:e ++enc=gbk 로 GBK 기준으로 다시 읽는다
:w !sudo tee % 는 "sudo 없이 열어서 저장이 안 되는" 상황 해결 —
내용을 sudo 로 현재 파일 이름에 되쓰므로 다시 열어 고치는 것보다 빠르다
이 설정들은 모두 임시이며 영구 적용하려면 ~/.vimrc 에 기록
:set paste 붙여넣기 전 자동 들여쓰기 끄기 :set nopaste 붙여넣은 후 복원 :set ff=unix Windows의 ^M 제거 :set fileencoding=utf-8 인코딩 수정 후 저장 :w !sudo tee % sudo를 잊었을 때 강제 저장
"서버가 느리다"는 하나의 문제가 아니라 네 가지다: CPU, 메모리, IO, 네트워크. 명령 하나로 어느 것인지 먼저 가른다
uptime 으로 load average 세 값 확인: 1/5/15 분. 코어 수를 넘으면 대기 중이라는 뜻
load 는 높은데 CPU 가 한가하면(top 에서 %id 가 높음) 대개 IO 대기이므로 wa 열을 본다
free -h 에서 free 열이 아니라 available 열을 본다(-h 사람이 읽는 단위)
df -h 와 df -i 를 함께 본다: -h 는 용량, -i 는 inode, 디스크가 차면 갖가지 이상한 문제가 생긴다
vmstat 1 5 매초 1회씩 5회 샘플링, r(대기), b(블로킹), wa(IO 대기), si/so(스와핑) 확인
층을 먼저 가른 뒤 손대는 것이 서비스를 바로 재시작하는 것보다 훨씬 효과적
uptime free -h df -h; df -i vmstat 1 5
먼저 vmstat 의 wa 열: 계속 10 을 넘으면 IO 가 발목을 잡는 것
다음 b 열(블로킹 프로세스 수), 계속 코어 수보다 크면 다수 프로세스가 자원 대기로 막힌 것
어느 프로세스인지 확인: pidstat -d 1 5(-d 는 IO 통계만, 1초마다 총 5회)
ps -eo 의 -e 는 전체 프로세스, -o 는 열 지정, state 열에서 D(중단 불가 수면)를 볼 수 있다
awk '$1=="D"' 로 D 상태 프로세스만 걸러낸다 — IO 에 막힌 것들이다
흔한 원인: DB 전체 테이블 스캔, 로그 동기 쓰기, 백업 작업, NFS/클라우드 디스크 지연
D 상태 프로세스는 kill 로 죽지 않으며 IO 가 돌아오거나 머신을 재시작해야 한다
vmstat 1 5 pidstat -d 1 5 ps -eo state,pid,cmd | awk '$1=="D"'
free -h 의 available 이 "새 프로세스에 얼마나 더 줄 수 있나"이며 free 가 적은 것은 정상, Linux 는 유휴 메모리를 캐시로 쓴다
buff/cache 가 높은 것은 문제가 아니며 필요하면 자동으로 회수된다
swap 을 많이 쓰면 실제로 빠듯하다는 뜻, vmstat 의 si/so 가 계속 0 이 아니면 스래싱이다
메모리 기준 프로세스 정렬: ps -eo pid,rss,comm --sort=-rss(-e 전체, -o 열 선택, --sort 정렬 키 앞의 음수는 내림차순)
RSS 는 실제 점유, VSZ 는 가상 주소 공간이라 거의 참고 가치가 없다
컨테이너에서는 호스트 total 이 아니라 cgroup 제한(/sys/fs/cgroup/memory.max)을 본다
메모리 누수가 의심되면 두 번의 RSS 를 비교: 10분 간격으로 각각 수집해 값이 한 방향으로 늘어나는 프로세스를 찾는다
free -h vmstat 1 5 # si / so 확인 ps -eo pid,rss,comm --sort=-rss | head
1단계 curl -w 로 DNS / 연결 / 첫 바이트 / 총 소요 네 항목 분해를 얻는다
curl -o /dev/null 은 body 버리기, -s 무음으로 진행 표시 숨김, -w 사용자 지정 출력(셋을 합치면 숫자만 본다)
2단계 서버에서 curl 127.0.0.1, 네트워크와 게이트웨이를 배제
3단계 애플리케이션 로그의 요청 소요 시간 확인, nginx 의 $request_time 과 $upstream_response_time 비교
둘의 차이가 크면 중간 계층(게이트웨이, 프록시, 커넥션 풀)에서 막힌 것
DB 슬로우 쿼리 로그를 켜 두는 것도 잊지 말 것, "인터페이스가 느리다"의 상당수는 실제로 SQL 이 느린 것
urltime 만 높고 업스트림 시간이 낮으면 대개 속도 제한이나 대기열이다
curl -o /dev/null -s -w 'dns:%{time_namelookup} conn:%{time_connect} ttfb:%{time_starttransfer} total:%{time_total}\n' https://example.com/api
# nginx log_format에 추가:
# $request_time $upstream_response_timeping -c 50 패킷 50개를 보내 loss% 와 mdev(지터) 확인, 한 번의 ping 으로는 아무것도 증명되지 않는다
ping -M do -s 1472 경로 MTU 측정: -M do 는 단편화 금지, -s 는 패킷 크기, 통하지 않으면 MTU 문제
mtr -r -c 50: -r 은 화면 전환 없이 리포트 출력, -c 는 패킷 수, 홉별 손실률을 볼 수 있어 traceroute 보다 낫다
ICMP 가 속도 제한되면 손실처럼 위장되므로 TCP 로 탐지하는 편이 정확(nc -z 또는 curl)
ethtool -S NIC rx/tx 오류와 손실 카운터 확인(-S 는 statistics)
grep -iE 와 연결하면 대소문자 무시 + 확장 정규식
같은 데이터센터 내 손실은 NIC/케이블을, 리전 간 손실은 통신사 구간을 먼저 의심
ping -c 50 10.0.0.5 mtr -r -c 50 10.0.0.5 ethtool -S eth0 | grep -iE 'err|drop'
낯선 머신에 로그인하면 이 묶음을 먼저 돌려 전체 상황을 잡고 무엇을 볼지 정한다
각 구간이 어떤 질문에 답하는지 표시해 두었으니 블랙박스처럼 그대로 베끼지 말 것
옵션 요약(모두 위 항목에서 설명): --sort=-%cpu 는 CPU 역순, -h 사람이 읽는 단위, -i inode
grep -vE 는 "제외 + 확장 정규식", 여기서는 tmpfs 같은 가상 파일 시스템을 걸러낸다
ss -tulnp 를 풀면 t/TCP, u/UDP, l/수신 대기, n/숫자, p/프로세스
systemctl 의 --failed 실패한 unit 만, --no-pager 페이징 없음
dmesg 의 -T 읽기 쉬운 시간, -l 지정 레벨만
운영 서버에서는 어떤 쓰기 작업도 피한다 — 이 묶음은 전부 읽기 전용이라 출력을 그대로 티켓에 붙일 수 있다
echo "== 负载 =="; uptime echo "== CPU 前 5 =="; ps aux --sort=-%cpu | head -6 echo "== 内存前 5 =="; ps aux --sort=-%mem | head -6 echo "== 内存 =="; free -h echo "== 磁盘 =="; df -h | grep -vE 'tmpfs|overlay' echo "== inode =="; df -i | grep -vE 'tmpfs|overlay' echo "== 监听端口 =="; ss -tulnp echo "== 失败服务 =="; systemctl list-units --failed --no-pager echo "== 内核错误 =="; dmesg -T -l err | tail -10