3 minute read

여기 하나의 웹 서버가 있다.이 웹 서버는 요청을 처리할 때마다 로그 파일에 한 줄을 추가한다.

예를 들어 nginx의 기본 액세스 로그 형식을 사용하면 로그의 한 줄은 다음과 같이 나타난다.

216.58.210.78 - - [27/Feb/2015:17:55:11 +0000] "GET /css/typography.css HTTP/1.1"
200 3377 "http://martin.kleppmann.com/" "Mozilla/5.0 (Macintosh; Intel Mac OS X
10_9_5) AppleWebKit/537.36 (KHTML, Gecko 등) Chrome/40.0.2214.115 Safari/537.36"

이 한 줄에는 많은 정보가 담겨 있다.

다음과 같은 로그 형식의 정의가 이 정보들을 해석하기 위해 필요하다.

$remote_addr - $remote_user [$time_local] "$request"
$status $body_bytes_sent "$http_referer" "$http_user_agent"
  • 2015년 2월 27일 17:55:11 UTC에 서버가 클라이언트 IP 주소 216.58.210.78에서 /css/typography.css 파일 요청을 수신했음을 나타낸다.
  • 사용자가 인증되지 않았으므로 $remote_user가 하이픈(-)으로 설정되었다.
  • 응답 상태는 200(즉, 요청이 성공했음)이었고 응답의 크기는 3,377바이트였다.
  • 웹 브라우저는 Chrome 40 버전이었으며, http://martin.kleppmann.com/ 이라는 URL로 참조되었다.

간단한 로그 분석

다양한 도구가 위와 같은 로그 파일을 가져와 웹사이트 트래픽에 대한 보고서를 생성할 수 있다. 기본적인 Unix 도구를 사용하여 직접 작성해 보자. 예를 들어 웹사이트에서 가장 인기 있는 페이지 5개를 찾고 싶다고 가정하자. 다음과 같이 Unix 셸에서 이 작업을 수행할 수 있다.

cat /var/log/nginx/access.log |
  awk '{print $7}' |
  sort             |
  uniq -c          |
  sort -r -n       |
  head -n 5
  • 첫 번째 줄은 로그 파일을 읽는 명령이다.
  • 두 번째는 각 줄을 공백으로 구분하여 필드로 나누고 각 줄의 일곱 번째 필드(요청 URL)만 출력한다. 예제 줄에서 이 요청 URL은 /css/typography.css 이다.
  • 세 번째는 요청된 URL 목록을 알파벳순으로 정렬한다. 특정 URL이 n번 요청된 경우 정렬 후 파일에는 동일한 URL이 연속으로 n번 반복된다.
  • ` uniq`` 명령은 인접한 두 줄이 동일한지 확인하여 입력에서 반복되는 줄을 필터링한다. c 옵션은 카운터도 출력하도록 지시한다. 모든 고유 URL에 대해 해당 URL이 입력에 몇 번이나 나타났는지 보고한다.
  • 정렬은 각 줄의 시작 부분에 있는 숫자(-n)를 기준으로 정렬하는데, 이는 URL이 요청된 횟수다. 그런 다음 역(-r) 순서로, 즉 가장 큰 숫자부터 결과를 반환한다.
  • 마지막으로, head는 입력의 처음 다섯 줄(-n 5)만 출력하고 나머지는 버립니다.

위 명령어의 결과는 다음과 같다.

4189 /favicon.ico
3631 /2013/05/24/improving-security-of-ssh-private-keys.html
2124 /2012/12/05/schema-evolution-in-avro-protocol-buffers-thrift.html
1369 /
915 /css/typography.css

유닉스 명령줄 단순하지만 매우 유용하다. 몇 초 만에 GB 용량의 로그 파일을 처리할 수 있으며 필요에 따라 분석을 쉽게 수정할 수 있다. 예를 들어, 보고서에서 CSS 파일을 생략하려면 awk 인수를 '$7 !~ /\.css$/ {print $7}'로 변경하면 된다. 상위 페이지 대신 상위 클라이언트 IP 주소를 계산하려면 awk 인수를 '{print $1}'로 변경한다. 이외에도 awk, sed, grep, sort, uniq, xargs를 조합하여 몇 분 안에 놀랍도록 많은 데이터 분석을 수행할 수 있으며, 그 성능도 놀라울 정도로 뛰어나다.

Unix 명령어 vs 사용자 프로그램

Unix 명령 체인 대신 동일한 작업을 수행하는 간단한 프로그램을 Ruby라는 프로그래밍 언어로 작성해보자.

counts = Hash.new(0)
File.open('/var/log/nginx/access.log') do |file| file.each do |line|
        url = line.split[6]
counts[url] += 1 end
end
    top5 = counts.map{|url, count| [count, url] }.sort.reverse[0...5]
    top5.each{|count, url| puts "#{count} #{url}" }
  • counts는 각 URL을 방문한 횟수가 카운터로 보관되는 해시 테이블이다.
  • counts는 기본적으로 0이다.
  • 로그의 각 줄에서 URL은 공백으로 구분된 일곱 번째 필드로 간주한다.
  • 로그의 현재 줄에 있는 URL에 대한 카운터를 증가시킨다.
  • 해시 테이블 내용을 카운터 값(내림차순)으로 정렬하고 상위 5개 항목을 가져온다.
  • 상위 5개 항목을 출력한다.

이 프로그램은 유닉스 파이프 체인만큼 간결하지는 않지만 가독성이 좋다. 두 가지 중 어떤 것을 선호할지는 취향의 문제다. 그러나 두 프로그램 간에는 표면적인 구문상의 차이 외에도 실행 흐름에 큰 차이가 있으며, 이는 대용량 파일에서 이 분석을 실행하면 분명해진다.

정렬과 인메모리 집계 비교

Ruby 스크립트는 URL의 인메모리 해시 테이블을 유지하며, 각 URL은 조회된 횟수에 매핑된다. Unix 파이프라인 예제에는 이러한 해시 테이블이 없지만, 동일한 URL이 여러 번 반복되는 URL 목록을 정렬하는 데 의존한다.

어떤 접근 방식이 더 낫다고 볼 수 있을까? 이것은 서로 다른 URL이 얼마나 많이 존재하는지에 따라 다르다. 대부분의 소규모 웹사이트라면, 1GB의 메모리에 모든 고유 URL과 각 URL에 대한 카운터를 모두 넣을 수 있다. 위 예제에서 작업에 랜덤 액세스가 필요한 메모리 양은 고유 URL의 수에만 의존한다. 단일 URL에 대한 로그 항목이 백만 개인 경우에도 해시 테이블에 필요한 공간은 여전히 하나의 URL에 카운터 크기를 더한 값이다. 이 작업 세트가 충분히 작은 경우, 인메모리 해시 테이블은 노트북에서도 잘 작동한다.

반면에 작업할 양이 사용 가능한 메모리보다 큰 경우, 정렬 방식은 디스크를 효율적으로 사용할 수 있다는 장점이 있다. 이는 SSTables과 LSM-Tree와 같은 원리다. 즉, 데이터 청크를 메모리에서 정렬하여 세그먼트 파일로 디스크에 기록하고, 여러 개의 정렬된 세그먼트를 더 큰 정렬된 파일로 병합할 수 있다. 병합에는 디스크에서 잘 작동하는 순차 액세스 패턴이 있다.

GNU Coreutils(Linux)의 정렬 유틸리티는 메모리보다 큰 데이터세트를 디스크에 흘려보내 자동으로 처리하고, 여러 CPU 코어에 걸쳐 정렬을 자동으로 병렬화한다. 즉, 앞서 살펴본 간단한 유닉스 명령 체인은 메모리 부족 없이 대규모 데이터 집합으로 쉽게 확장할 수 있다. 병목 현상은 디스크에서 입력 파일을 읽을 수 있는 속도 자체가 된다.