배포 후에 파일 권한을 맞추는 스크립트가 있다. 예전부터 있던 것인데 언제부턴가 40분씩 걸린다. 작업 자체는 단순해서 파일은 644 디렉터리는 755로 맞추는 것이 전부다.
Table of contents
Open Table of contents
스크립트가 이랬다
find /var/www/game -type f -exec chmod 644 {} \;
find /var/www/game -type d -exec chmod 755 {} \;
문제없어 보인다. 실제로 파일이 몇천 개일 때는 몇 초에 끝났다.
지금 세어 봤다.
$ find /var/www/game -type f | wc -l
83412
wc -l 로 세니 8만 3천 개다. 리소스 파일인 이미지와 사운드가 계속 늘었다.
원인 — 세미콜론과 플러스의 차이
-exec ... \; 는 파일 하나마다 명령을 한 번씩 실행한다. 8만 3천 번 chmod 프로세스를 띄운다는 뜻이다. -exec ... + 는 여러 파일을 모아 한 번에 넘긴다.
# \; 는 이렇게 실행된다
chmod 644 /var/www/game/a.png
chmod 644 /var/www/game/b.png
chmod 644 /var/www/game/c.png
...8만 3천 번
# + 는 이렇게 실행된다
chmod 644 /var/www/game/a.png /var/www/game/b.png ... (수천 개)
chmod 644 ... (나머지)
인자 길이 제한 때문에 몇 번으로는 나뉘지만 8만 번이 수십 번이 된다. xargs 로 파이프해도 같다.
chmod 자체는 순식간이다. 오래 걸리는 것은 프로세스를 만들고 없애는 일 곧 fork 와 exec 이고 그것이 8만 번 반복됐다.
바꿔서 재 봤다
$ time find /var/www/game -type f -exec chmod 644 {} \;
real 38m41.203s
$ time find /var/www/game -type f -exec chmod 644 {} +
real 0m11.907s
38분이 12초가 됐다. 코드는 문자 하나 차이다.
대상을 2,000개로 줄여 따로 재 보면 차이가 더 또렷하다. \; 는 3.9초이고 + 는 0.02초로 한 번에 200배 가까이 갈린다.
이미 맞는 것은 대상에서 뺀다
한 발 더 갔다. 8만 개 중 대부분은 이미 644고 새로 올라온 것만 다르다.
find 에 권한 조건을 넣을 수 있다.
find /var/www/game -type f -not -perm 644 -exec chmod 644 {} +
find /var/www/game -type d -not -perm 755 -exec chmod 755 {} +
-not -perm 644 는 권한이 644가 아닌 것만 남기므로 대상이 8만 개에서 몇백 개로 줄었다.
$ time find /var/www/game -type f -not -perm 644 -exec chmod 644 {} +
real 0m3.114s
3초다. 파일 목록을 훑는 시간이 대부분이다.
다른 자리의 같은 패턴
같은 구조를 셸 스크립트에서 여러 번 봤다. grep -rn 으로 찾으니 for 문 안에서 외부 명령을 부르는 자리가 여럿 나왔다.
# 파일마다 프로세스 두 개(cat, grep)를 띄운다
for f in $(ls logs/*.log); do
cat $f | grep ERROR
done
grep 은 원래 파일을 여러 개 받는다.
grep ERROR logs/*.log
cat 도 필요 없고 반복도 필요 없다. 프로세스가 파일 수 × 2 에서 1이 된다.
# 파일마다 stat 을 부른다
for f in *; do
size=$(stat -c %s "$f")
...
done
stat 도 find 나 du 로 한 번에 얻을 수 있는 값이다. 루프 안에 외부 명령이 있으면 그 명령이 파일 수만큼 실행되는데 이것이 셸 스크립트가 느려지는 가장 흔한 이유다.
이 스크립트는 배포될 때부터 이랬고 코드는 안 바뀌었으며 데이터만 늘었다. 앞서 DB 에서 겪은 것과 같은 구조로 인덱스 없는 조회가 몇천 건일 때는 안 보이다가 몇십만 건에서 터진 것과 같다.
둘 다 건당 비용 × 건수인데 건수만 늘어난 경우다. 건당 비용이 작아 보여서 넘어갔던 것이 건수가 커지면서 전체를 지배한다. 그래서 이런 것을 볼 때 건당 무엇을 하는지와 건수가 늘어날 수 있는지 둘을 본다. 건당 비용이 0에 가까우면 건수가 늘어도 괜찮은데 프로세스 생성은 0이 아니다.
정리
-exec ... {} \;는 파일 수만큼fork와exec를 한다- 끝을
+로 바꾸거나xargs를 쓰면 몇 번으로 끝난다 - 8만 3천 개에서 38분이 12초가 됐다
- 2,000개로 재면 3.9초와 0.02초로 갈린다
- 작업이 무거운 것이 아니라 프로세스 생성이 비쌌다
-not -perm 644로 이미 맞는 것을 대상에서 빼면 3초가 된다for문 안의 외부 명령은 대상 수만큼 실행된다grep·chmod·chown은 여러 대상을 한 번에 받는다- 느려진 원인이 코드가 아니라 데이터 증가인 경우가 많다
- 건당 무엇을 하는지와 건수가 늘어날 수 있는지를 함께 본다