회사에서 서비스를 하는 중에 로그 파일이 너무 커져서, 적당한 크기에서 나눌 방법이 필요해졌습니다. 이런 경우 리눅스에서 주로 쓰는 방법은 logrotate입니다. 그리고 이를 서비스에 적용하기 위해 필요한 것을 찾아봤습니다.

logrotate 설정에 나눌 로그 파일을 추가하면 설정된 상황(매일 또는 매주, 무조건 또는 주어진 크기를 넘었을 때등)에서 로그 파일을 나눕니다. 이때 하는 동작은 기존 로그 파일의 이름을 변경하고, 같은 이름으로 새 로그 파일을 만드는 것입니다.

만약 잠깐 실행되고 마는 프로그램이면 이것으로 잘 동작합니다. 매번 실행할 때마다 주어진 이름의 로그 파일을 새로 열어서 쓰니까요. 하지만 프로그램이 계속 떠 있는 경우 새 로그 파일에 쓰지 않고 계속 기존 로그 파일에 데이터가 쌓입니다.

간단하게 이 동작을 확인할 수 있습니다. shell에서 node -e "setInterval(function () { console.log('hello'); }, 1000)" > log를 실행한 후 다른 창에서 log 파일의 이름을 변경하고 새로 생성해도 계속 기존 파일에 로그가 추가됩니다. 이것은 한번 파일을 열면 그 파일의 이름이 아닌 descriptor에 의해서 접근하기 때문입니다.

이 문제를 해결하기 위해 logrotate는 copytruncate라는 옵션을 제공합니다. 이 옵션을 사용하면 파일의 이름을 변경하는 대신 새 파일로 내용을 복사하고 기존 파일의 크기를 0으로 만들어버립니다. 하지만 이 경우 복사하는 동안 추가된 로그가 사라지는 문제가 있습니다.

다른 방법으로는 logrotate가 파일 이름을 변경한 후에 프로그램이 로그 파일을 새로 열도록 하는 것이 있습니다. 이미 떠 있는 프로그램에 명령을 내리기 위해서는 주로 시그널을 사용합니다. 예를 들어 nginx는 SIGUSR1을 받으면 로그 파일을 다시 열도록 되어 있습니다. (Stopping or Restarting Nginx) logrotate에서는 postrotate 옵션으로 이를 표현합니다. 다음은 Ubuntu에 적용된 nginx를 위한 logrotate 설정입니다.

postrotate
  [ ! -f /run/nginx.pid ] || kill -USR1 `cat /run/nginx.pid`
endscript

다음은 이 방법이 적용된 서버 코드입니다. Node.js를 사용하고 CoffeeScript로 작성되어 있습니다.

logstream = undefined
openLogFile = ->
  logstream.close() if logstream
  logstream = fs.createWriteStream "log", flags: 'a+', mode: '0644', encoding: 'utf8'
openLogFile()

# 시그널을 받으면 로그 파일을 다시 연다
process.on 'SIGUSR2', ->
  openLogFile()

SIGUSR1은 Node.js가 디버그 모드 진입용으로 사용하고 있습니다. 그래서 대신 SIGUSR2를 사용했습니다.



comments powered by Disqus