Skip to content
isdnetworks
Go back

폴더를 옮겼는데 옛 경로가 코드에 남아 있었다

수집 장비에 쌓이는 파일이 한 폴더에 뒤섞여 있었다.

/data/
  dcu01_20161010.csv
  dcu01_20161010.log
  dcu02_20161010.csv
  raw_20161010.bin
  ...

csvlogbin 이 장비 구분 없이 한 자리에 섞여서 날짜별·장비별로 나누기로 했다.

/data/2016/10/dcu01/
/data/2016/10/dcu02/
/archive/2016/10/

옮기고 이틀이 지난 뒤에 수집이 멈춰 있는 것을 발견했다.

Table of contents

Open Table of contents

옮긴 뒤에 조용히 멈췄다

로그가 안 쌓이는데 collector 프로세스는 살아 있었다.

$ ps -ef | grep collector
root  2841  ... /usr/local/bin/collector -c /etc/collector.conf

죽은 것이 아니라 돌고는 있고 출력만 없는 상태였다.

$ ls -l /data/2016/10/dcu01/
total 0

새로 만든 자리는 비어 있었고 옮기기 전 자리를 보니 답이 나왔다.

$ ls -l /data/
dcu01_20161016.csv
dcu01_20161017.csv

옛 경로에 새 파일이 계속 쌓이고 있었다. 이사한 것을 프로그램이 모르고 있었던 것이다. 오류가 안 났으니 ps 로 봐도 정상이고 로그에도 아무 말이 없었다.

멈춘 것이 아니라 엉뚱한 자리에 쌓는 것이라서 감시 항목에 아무것도 안 걸렸다. 프로세스는 살아 있고 디스크도 줄고 있으니 겉으로는 평소와 같았다.

새 자리가 비어 있는 것을 사람이 눈으로 볼 때까지 아무도 이상을 알리지 않았다. 조용한 고장은 요란한 고장보다 늦게 드러난다.

원인 — 설정을 거치지 않고 조립하는 줄

설정 파일에는 새 경로를 제대로 넣어 뒀다.

data_dir=/data/2016/10

코드에서 그 값을 읽는 자리도 멀쩡했다.

#define DEFAULT_DATA_DIR "/data"

static const char *data_dir(void) {
    const char *d = cfg_get("data_dir");
    return d ? d : DEFAULT_DATA_DIR;
}

cfg_get 이 값을 돌려주면 그것을 쓰고 없으면 DEFAULT_DATA_DIR 로 떨어진다. 여기까지는 맞는데 문제는 다른 파일에 있었다.

snprintf(path, sizeof(path), "/data/%s_%s.csv", dev_id, datestr);

data_dir() 을 안 거치고 snprintf 에 경로를 직접 박아 문자열을 만든다. 설정을 아무리 고쳐도 이 줄은 안 바뀐다. 한 군데를 고쳤다고 전부 고친 것이 아니었다.

설정을 읽는 통로와 경로를 만드는 통로가 둘로 갈려 있었던 것이다. 통로가 둘이면 고친 쪽만 바뀌고 안 고친 쪽은 옛 값을 그대로 쓴다.

기본값이 옛 경로였던 것도 상황을 가렸다. 설정이 안 읽혀도 실패하지 않고 옛 자리로 조용히 돌아가기 때문이다.

남은 자리를 세어 봤다

grep 으로 트리 전체에서 옛 경로 문자열을 찾았다.

$ grep -rn '"/data' --include=*.c --include=*.h .
./collector/main.c:44:   #define DEFAULT_DATA_DIR "/data"
./collector/writer.c:118: snprintf(path, ..., "/data/%s_%s.csv", ...)
./collector/writer.c:203: snprintf(path, ..., "/data/%s_%s.log", ...)
./tools/rotate.c:31:      const char *base = "/data";
./tools/pack.c:77:        system("find /data -name '*.bin' -mtime +7 ...")

writer.c 에 둘, rotate.cpack.c 에 하나씩이라 코드에만 다섯 군데다. 그리고 코드 밖에도 있었다.

$ grep -rn '/data' /etc/cron.d/ /etc/init.d/ /usr/local/sbin/*.sh
/etc/cron.d/collector:2:   30 3 * * * root /usr/local/sbin/pack.sh /data
/etc/init.d/collector:18:  DATADIR=/data
/usr/local/sbin/backup.sh:9: rsync -a /data/ /mnt/nas/backup/

cron.d 항목과 init.d 스크립트와 backup.sh 에 세 군데가 더 있었다. 소스만 훑고 끝냈으면 이 셋이 그대로 남았을 것이다.

경로는 코드에만 있는 것이 아니라 그 코드를 부르는 쪽에도 있다. 주기 실행 등록과 기동 스크립트와 백업 스크립트가 전부 같은 문자열을 들고 있었다.

찾을 자리를 소스 트리로 한정하면 절반만 본 것이 된다. 여덟 군데를 다 세고 나서야 작업 크기가 정해졌다.

백업이 더 위험했다

셋 중에 backup.sh 가 가장 무거운 문제였다. rsync 가 옛 경로를 통째로 밀어 넣고 있으니 거기 남은 찌꺼기는 계속 백업되는데 정작 새 경로는 대상에서 빠져 있었다.

$ ls /mnt/nas/backup/ | head -3
dcu01_20161010.csv
dcu01_20161011.csv
dcu01_20161012.csv

이틀치 새 자료가 백업에 없었다. 수집이 멈춘 것은 이틀이면 알아채는데 백업에서 빠지는 것은 복구가 필요한 날까지 모른다. 같은 누락이어도 드러나는 시점이 다르면 무게가 달랐다.

수집이 멈춘 것은 화면을 보면 이틀 안에 드러난다. 백업에서 빠지는 것은 복구가 필요해지는 날까지 아무 신호도 안 준다.

같은 누락이어도 드러나는 시점이 다르면 무게가 달라진다. 뒤늦게 아는 쪽을 먼저 고치는 것이 맞았다.

해결 — 경로를 만드는 한 자리

경로를 조립하는 코드를 함수 하나에 몰았다.

/* 경로를 만드는 곳은 여기 하나뿐이다. 다른 곳에서 문자열로 조립하지 않는다. */
int data_path(char *buf, size_t n, const char *dev, const char *date, const char *ext)
{
    char y[5], m[3];
    memcpy(y, date, 4); y[4] = 0;
    memcpy(m, date + 4, 2); m[2] = 0;
    return snprintf(buf, n, "%s/%s/%s/%s/%s_%s.%s",
                    data_dir(), y, m, dev, dev, date, ext);
}

date 앞 네 글자를 연도로 두 글자를 월로 잘라 data_dir() 뒤에 붙인다. writer.crotate.cdata_path 를 부르게 고쳤다.

셸 쪽은 함수를 공유하기 어려워서 환경 파일 한 장을 만들었다.

# /etc/collector.env
DATA_ROOT=/data
ARCHIVE_ROOT=/archive
. /etc/collector.env
rsync -a "$DATA_ROOT/" /mnt/nas/backup/

backup.shpack.shcollector.envsource 로 읽고 DATA_ROOT 를 쓴다. 흩어져 있으면 아무리 잘 찾아도 하나는 남는다.

한 자리로 모으는 작업 자체는 구조를 바꾸지 않는다. 만드는 방법만 바꾸고 만들어지는 값은 그대로 두는 것이다.

그렇게 해 두면 다음에 자리를 옮길 때 고칠 곳이 두 군데로 줄어든다. 코드는 함수 하나고 셸은 환경 파일 하나다.

재발 방지 — 옛 자리를 못 쓰게 막기

고치고도 옛 경로가 쓸 수 있는 상태로 남아 있으면 빠뜨린 코드가 조용히 거기에 쓴다. 오류가 안 나니까 이번에도 이틀이 지나서야 알았다. 심볼릭 링크로 새 자리를 가리키게 뒀으면 더 오래 못 찾았을 것이다.

$ mv /data/dcu01_2016* /data/2016/10/dcu01/
$ chmod 000 /data/legacy_marker

남은 파일을 새 자리로 옮기고 chmod 000 으로 옛 자리를 막았다. 빠뜨린 코드가 거기 쓰려 하면 그 자리에서 실패하고 로그에 남는다. 조용히 성공하는 것보다 낫다.

순서를 다시 짜면 이렇게 된다.

# 1) 경로 문자열이 몇 군데 있는지 먼저 센다
grep -rn '"/data' --include=*.c --include=*.h .
grep -rn '/data' /etc/cron.d/ /etc/init.d/ /usr/local/sbin/

# 2) 한 자리로 모은다 (구조는 아직 안 바꾼다)
# 3) 그 한 자리만 새 경로로 바꾼다
# 4) 옛 자리를 못 쓰게 막는다

구조 변경과 코드 정리를 같이 하면 무엇이 원인인지 못 가른다. 정리를 먼저 하고 구조를 나중에 바꾼다.

두 가지를 한 번에 하면 문제가 생겼을 때 어느 쪽 탓인지 가릴 수 없다. 정리가 끝난 뒤에 옮기면 바뀐 것이 값 하나뿐이라 확인이 간단하다.

이번에는 그 순서를 거꾸로 밟아서 이틀치 자료를 백업에서 잃을 뻔했다.

정리


Share this post on:

Previous Post
핀 하나를 나눠 쓴 대가
Next Post
처음 감시 체계를 세우며