리눅스 time 명령어 사용법 - 셸 빌트인과 /usr/bin/time 차이, real user sys 읽는 법
이 명령어를 언제 쓰는가
배치가 어제보다 느려졌다는 얘기를 들었을 때, 두 가지 구현 중 어느 쪽이 빠른지 확인할 때, 컨테이너 메모리 리밋을 얼마로 잡아야 할지 감을 잡을 때 time을 앞에 붙인다.
여기서 반드시 짚고 갈 것이 하나 있다. time은 두 개다. 하나는 bash·zsh에 내장된 셸 예약어이고, 다른 하나는 /usr/bin/time이라는 별도 실행 파일이다. 이름이 같아서 늘 헷갈리는데, 할 수 있는 일이 꽤 다르다. 메모리 사용량이 궁금하면 후자를 써야 한다.
기본 형식
time 명령 [인자...] # 셸 예약어
/usr/bin/time [옵션] 명령 [인자...] # 별도 바이너리
차이를 표로 정리하면 이렇다.
| 구분 | 셸 예약어 time | /usr/bin/time |
|---|---|---|
| 정체 | bash·zsh의 예약어. type time이 reserved word라고 답한다 | 실행 파일. Debian·Ubuntu는 time, RHEL은 time 패키지 |
| 파이프라인 측정 | 가능. time a | b가 전체를 잰다 | 불가. 첫 명령만 잰다 |
| 셸 함수·빌트인 측정 | 가능 | 불가(실행 파일이 아니라서) |
| 메모리·페이지폴트 | 불가 | 가능(-v, BSD는 -l) |
| 출력 서식 변경 | TIMEFORMAT(bash), TIMEFMT(zsh) | -f 옵션 |
| 파일로 저장 | { time 명령; } 2> 파일 | -o 파일 |
출력 읽는 법
bash에서 time sleep 0.2를 실행하면 이렇게 나온다.
real 0m0.206s
user 0m0.001s
sys 0m0.002s
- real — 벽시계 시간. 시작부터 끝까지 실제로 흐른 시간이다. 사용자가 체감하는 시간이 이것이다.
- user — 사용자 모드에서 CPU를 쓴 시간. 연산·문자열 처리 같은 순수 계산이 여기 들어간다.
- sys — 커널 모드에서 CPU를 쓴 시간. 파일 읽기·쓰기, 소켓, 메모리 할당 등 시스템 콜 비용이다.
해석의 핵심은 real과 user+sys의 관계다. real이 훨씬 크고 user+sys가 거의 0이면 CPU는 놀고 있었다는 뜻이므로 디스크·네트워크·락 대기를 의심한다. 반대로 user+sys가 real보다 크면 여러 코어를 동시에 쓴 것이다. 8코어에서 real 10초에 user 70초가 나오면 병렬화가 잘 되고 있다는 신호다.
자주 쓰는 옵션(/usr/bin/time)
| 옵션 | 의미 | 예시 |
|---|---|---|
-v | GNU. 최대 메모리, 페이지폴트, 컨텍스트 스위치까지 전부 | /usr/bin/time -v ./batch.sh |
-f 서식 | 필요한 항목만 골라 한 줄로 | /usr/bin/time -f '%e %M' ./batch.sh |
-o 파일 | 측정 결과를 파일에 저장 | /usr/bin/time -o t.log ./batch.sh |
-a | -o와 함께. 덮어쓰지 않고 이어쓰기 | /usr/bin/time -a -o t.log ./batch.sh |
-l | BSD·macOS. GNU의 -v에 해당 | /usr/bin/time -l ./batch.sh |
-f 서식에서 자주 쓰는 것은 %e(real 초), %U(user), %S(sys), %M(최대 상주 메모리 KB), %P(CPU 사용률), %x(종료 코드)다.
실전 예제
1) 파이프라인 전체 시간을 잰다. 이건 셸 예약어만 할 수 있다. /usr/bin/time을 쓰면 zcat만 측정된다.
time zcat access.log.gz | grep ' 500 ' | wc -l
2) 배치의 최대 메모리 사용량을 확인한다. 컨테이너 memory.limit이나 JVM 힙 크기를 정하기 전에 실측하는 용도다. GNU time의 Maximum resident set size가 KB 단위로 나온다.
/usr/bin/time -v ./nightly_aggregate.sh 2> timing.txt
grep -E 'Elapsed|Maximum resident' timing.txt
3) 매일 실행 시간을 한 줄씩 쌓아 추세를 본다. 크론 잡이 서서히 느려지는 것을 잡아내는 가장 싼 방법이다.
/usr/bin/time -a -o /var/log/batch_timing.log \
-f "$(date +%F) elapsed=%e user=%U sys=%S maxrss=%MKB exit=%x" \
./nightly_aggregate.sh
4) 두 구현을 비교한다. 캐시 영향을 줄이려면 순서를 바꿔 한 번씩 더 돌린다. 첫 실행은 페이지 캐시가 비어 있어 항상 느리므로 버린다.
time ./sort_v1.sh > /dev/null
time ./sort_v2.sh > /dev/null
5) 여러 명령을 묶어서 재고, 결과만 파일에 남긴다. 중괄호 블록과 2>의 조합이 필요하다.
{ time { ./extract.sh && ./transform.sh && ./load.sh ; } ; } 2> /tmp/etl_time.txt
6) 출력 서식을 바꾼다. bash는 TIMEFORMAT, zsh는 TIMEFMT다.
TIMEFORMAT='%3R real / %3U user / %3S sys'
time sleep 0.15
0.155 real / 0.001 user / 0.001 sys
함정과 주의점
time -v는 셸 예약어에서 동작하지 않는다. 가장 흔한 실수다. bash에서 time -v ./job.sh를 치면 셸이 -v를 명령 이름으로 해석해서 이런 결과가 나온다.
bash: -v: command not found
real 0m0.006s
실행 시간이 0.006초로 찍히니 명령이 엄청 빨랐다고 착각하기 쉽다. 실제로는 ./job.sh가 아예 실행되지 않았다. 해결책은 셋 중 하나다. /usr/bin/time -v로 절대 경로를 주거나, command time -v를 쓰거나, \time -v처럼 백슬래시를 붙인다.
출력이 표준 에러로 나간다. time ./job.sh > out.txt는 job.sh의 출력만 파일로 보내고 측정 결과는 화면에 남는다. 반대로 측정 결과만 파일로 보내려면 2>가 필요한데, 셸 예약어 time은 time ./job.sh 2> t.txt로도 잡히지 않는다. 그 2>는 job.sh에 붙기 때문이다. 반드시 { time ./job.sh; } 2> t.txt처럼 블록으로 감싸야 한다.
GNU와 BSD의 옵션이 다르다. 리눅스의 /usr/bin/time은 GNU time이라 -v와 -f를 쓰지만, macOS·FreeBSD의 것은 BSD time이라 -v가 없고 -l을 쓴다. macOS에서 /usr/bin/time -l sleep 0.1을 돌리면 이렇게 시작한다.
0.10 real 0.00 user 0.00 sys
1179648 maximum resident set size
여기서 최대 메모리는 바이트지만 GNU의 %M은 KB다. 스크립트를 두 환경에서 돌린다면 단위를 반드시 확인한다. macOS에서 GNU 쪽 동작이 필요하면 brew install gnu-time 후 gtime을 쓴다.
/usr/bin/time이 없는 서버가 흔하다. 최소 설치 이미지나 컨테이너에는 time 패키지가 빠져 있는 경우가 많다. /usr/bin/time: No such file or directory가 뜨면 apt install time 또는 dnf install time으로 넣는다. 셸 예약어는 항상 있으므로 시간만 필요하면 그쪽으로 충분하다.
단 한 번 측정한 값을 믿지 마라. 페이지 캐시, 다른 프로세스의 부하, CPU 주파수 스케일링 때문에 같은 명령도 편차가 크다. 최소 3~5회 돌려 중앙값을 보고, 디스크 I/O가 관건이면 캐시 상태를 통일한 뒤 비교한다.
함께 보면 좋은 명령어
- top — time은 끝난 뒤의 총합만 알려준다. 실행 중의 순간 CPU·메모리 변화는 top으로 본다.
- ps —
ps -o etime,times로 이미 돌고 있는 프로세스의 누적 시간을 확인할 수 있다. - strace — sys 시간이 유독 크면 어떤 시스템 콜에서 잡아먹는지
strace -c로 집계해 본다.