새 콘텐츠에 대한 알림을 받으시겠습니까?
우선적 로그 분석
대학 졸업 후 Amazon에 입사했을 때 제가 수행한 첫 번째 온보딩 실습 중 하나는 제 개발자 데스크톱에서 amazon.com 웹 서버를 시작하고 실행하는 것이었습니다. 첫 번째 시도에서 실패했지만 무엇을 잘못했는지 몰랐습니다. 친절한 동료가 로그를 보고 무엇이 잘못되었는지 살펴보라고 조언했습니다. 그러기 위해서는 “로그 파일을 캣(cat)”해야 한다고 말했습니다. 저는 그가 저에게 장난을 치거나 제가 이해하지 못하는 고양이에 대한 농담을 하고 있다고 생각했습니다. 저는 대학에서 컴파일하고, 소스 제어를 사용하고, 텍스트 편집기를 사용하는 데만 Linux를 사용했습니다. 저는 “캣”이 실제로는 패턴을 찾기 위해 다른 프로그램에 제공할 수 있는 파일을 터미널로 출력하는 명령인지 몰랐습니다.
제 동료는 cat, grep, sed, awk 등의 도구를 알려주었습니다. 이 새 도구 세트를 사용하여 제 개발자 데스크톱에서 amazon.com 웹 서버 로그를 살펴보았습니다. 웹 서버 애플리케이션은 모든 종류의 유용한 정보를 로그로 내보내도록 이미 계측되어 있었습니다. 로그를 통해 어디에서 작동이 중지되었거나 다운스트림 서비스에 연결하지 못했는지를 파악하고 웹 서버 시작을 방해하는 구성을 확인할 수 있었습니다. 웹 사이트는 많은 움직이는 부분으로 이루어져 있으며 이는 처음에는 저에게 본질적으로 내부 구조는 알려지지 않았지만 기능은 문서화되어 있는 블랙 박스처럼 느껴졌습니다. 그러나 시스템을 분석하면서 차츰 계측의 출력만 봐도 서버가 어떻게 작동하는지 알아내는 방법과 서버의 종속 항목과 상호 작용하는 방법을 익히게 되었습니다.
왜 계측인가
측정 대상
가용성과 지연 시간에 대한 Amazon의 높은 기준에 따라 서비스를 운영하기 위해서는 서비스 소유자로서 우리는 시스템이 어떻게 작동하는지 측정해야 합니다.
필요한 원격 분석을 얻기 위해 서비스 소유자는 여러 곳에서 운영 성능을 측정하여 종단 간 상황을 다각적으로 파악합니다. 이는 단순한 아키텍처에서도 복잡한 프로세스입니다. 고객이 로드 밸런서를 통해 호출하는 서비스가 있다고 합시다. 이 서비스는 원격 캐시와 원격 데이터베이스에 연결합니다. 우리는 각 구성 요소가 해당 동작에 대한 지표를 내보내게 하려고 합니다. 각 구성 요소가 다른 구성 요소의 동작을 인식하는 방법에 대한 지표도 필요합니다. 이러한 모든 관점의 지표를 취합하여 서비스 소유자가 문제의 원인을 빠르게 추적하고 파악할 수 있습니다.
많은 AWS 서비스가 자동으로 리소스에 대한 운영 분석을 제공합니다. 예를 들어, Amazon DynamoDB는 서비스에서 측정하는 성공률, 실패율 및 지연 시간에 대한 Amazon CloudWatch 지표를 제공합니다. 그러나 이러한 서비스를 사용하는 시스템을 빌드할 경우 시스템 작동 방식에 대해 더 자세히 알아봐야 합니다. 계측에는 태스크가 얼마나 오래 걸리는지, 특정 코드 경로가 얼마나 자주 실행되는지, 태스크 대상에 대한 메타데이터, 태스크의 어느 부분이 성공하거나 실패했는지를 기록하는 명시적 코드가 필요합니다. 팀에서 명시적 계측을 추가하지 않는 경우 자체 서비스를 블랙 박스로 운영할 수 밖에 없습니다.
예를 들어, 제품 ID로 제품 정보를 검색한 서비스 API 작업을 구현한 경우 코드는 다음 예와 같을 수 있습니다. 이 코드는 로컬 캐시, 원격 캐시 및 데이터베이스에서 차례로 제품 정보를 조회합니다.
public GetProductInfoResponse getProductInfo(GetProductInfoRequest request) {
// check our local cache
ProductInfo info = localCache.get(request.getProductId());
// check the remote cache if we didn't find it in the local cache
if (info == null) {
info = remoteCache.get(request.getProductId());
localCache.put(info);
}
// finally check the database if we didn't have it in either cache
if (info == null) {
info = db.query(request.getProductId());
localCache.put(info);
remoteCache.put(info);
}
return info;
}
제가 이 서비스를 운영한다면 프로덕션에서 서비스의 동작을 이해할 수 있도록 이 코드에 많은 계측이 필요할 것입니다. 실패하거나 느린 요청 문제를 해결하고 여러 종속 항목이 저평가되거나 잘못 동작하고 있다는 징후와 추세를 모니터링하는 기능이 필요했습니다. 다음은 전체적으로 또는 특정 요청과 관련하여 프로덕션 시스템에 대해 제가 대답할 수 있어야 하는 몇 가지 질문이 주석으로 추가된 동일한 코드입니다.
public GetProductInfoResponse getProductInfo(GetProductInfoRequest request) {
// Which product are we looking up?
// Who called the API? What product category is this in?
// Did we find the item in the local cache?
ProductInfo info = localCache.get(request.getProductId());
if (info == null) {
// Was the item in the remote cache?
// How long did it take to read from the remote cache?
// How long did it take to deserialize the object from the cache?
info = remoteCache.get(request.getProductId());
// How full is the local cache?
localCache.put(info);
}
// finally check the database if we didn't have it in either cache
if (info == null) {
// How long did the database query take?
// Did the query succeed?
// If it failed, is it because it timed out? Or was it an invalid query? Did we lose our database connection?
// If it timed out, was our connection pool full? Did we fail to connect to the database? Or was it just slow to respond?
info = db.query(request.getProductId());
// How long did populating the caches take?
// Were they full and did they evict other items?
localCache.put(info);
remoteCache.put(info);
}
// How big was this product info object?
return info;
}
이 모든 질문에 답하는 코드는 실제 비즈니스 로직보다 훨씬 더 깁니다. 몇몇 라이브러리는 계측 코드 양을 줄이는 데 도움이 될 수 있지만 개발자는 라이브러리에 필요한 가시성에 대한 질문에 여전히 답해야 하며 계측에서 연결에 대해 계획적이어야 합니다.
분산 시스템을 통해 흐르는 요청 문제를 해결할 때 하나의 상호 작용을 기반으로 하는 요청만 볼 경우 무슨 일이 발생했는지 파악하기 어려울 수 있습니다. 퍼즐을 맞추기 위해서는 이 모든 시스템에 대한 측정값을 모두 한 곳에 끌어모으는 것이 도움이 됩니다. 그러려면 각 서비스를 계측하여 각 태스크의 추적 ID를 기록하고 해당 태스크에 대해 협업하는 서비스 간에 추적 ID를 서로 전파해야 합니다. AWS X-Ray와 같은 서비스를 사용하여 실시간에 가깝게 또는 필요에 따라 문제 발생 후 지정된 추적 ID에 대해 전체 시스템에서 계측을 수집할 수 있습니다.
드릴다운
계측을 통해 지표를 한 눈에 보고 경보를 트리거하기에는 너무 작은 이상이 있는지 확인하는 것부터 조사를 수행하여 이러한 이상의 윈인을 찾아내는 것까지 여러 수준에서 문제를 해결할 수 있습니다.
가장 높은 수준에서는 경보를 트리거하고 대시보드에 표시될 수 있는 지표로 계측이 집계됩니다. 이러한 집계 지표를 통해 운영자는 전체 요청 빈도, 서비스 호출의 지연 시간 및 오류율을 모니터링할 수 있습니다. 이러한 경보와 지표를 사용하여 조사가 필요한 이상이나 변경 사항을 파악할 수 있습니다.
이상을 발견하면 왜 이상이 발생하고 있는지 알아내야 합니다. 이 질문에 답하기 위해 더 많은 계측으로 가능한 지표에 의존합니다. 요청 처리의 다양한 부분을 실행하는 데 걸리는 시간을 계측하여 처리의 어느 부분이 정상보다 더 느리거나 오류를 더 자주 트리거하는지 알 수 있습니다.
집계 타이머와 지표는 원인을 배제하거나 조사 영역을 강조하는 데 도움이 되지만 이들이 항상 모든 것을 설명하지는 않습니다. 예를 들어, 지표를 통해 오류가 특정 API 작업에서 발생함을 알 수 있지만 지표가 작업 실패 이유에 대한 충분한 설명을 제공하지 않을 수 있습니다. 이 시점에서 해당 기간 동안 서비스에서 내보내는 자세한 원시 로그 데이터를 봅니다. 원시 로그에는 문제의 원인 즉, 발생 중인 특정 오류나 어떤 엣지 케이스를 트리거하는 요청의 특정 부분이 표시됩니다.
계측 방법
계측에는 코딩이 필요합니다. 즉, 새로운 기능을 구현할 때 무슨 일이 있었는지, 성공했는지 아니면 실패했는지, 얼마나 걸렸는지를 나타내기 위해 다른 코드를 추가할 시간이 필요합니다. 계측은 일반 계측 라이브러리를 위한 표준화와 구조화된 로그 기반 지표 보고를 위한 표준화의 공통 패턴을 처리하기 위해 Amazon에서 수년 간 수행한 일반 코딩 태스크입니다.
라이브러리 소비자는 지표 계측 라이브러리의 표준화를 통해 라이브러리가 어떻게 작동하고 있는지 확인할 수 있습니다. 예를 들어, 일반적으로 사용되는 HTTP 클라이언트는 이러한 공용 라이브러리와 통합되므로 서비스 팀이 다른 서비스에 대한 원격 호출을 구현하는 경우 해당 호출에 대한 계측을 자동으로 받습니다.
계측된 애플리케이션이 작업을 실행하고 수행하면 결과 원격 분석 데이터가 구조화된 로그 파일에 기록됩니다. HTTP 서비스에 대한 요청인지, 대기열에서 끌어온 메시지인지에 관계없이 일반적으로 “작업 단위”당 하나의 로그 항목으로 데이터가 내보내집니다.
Amazon에서 애플리케이션의 측정값은 집계되지 않으며 지표 집계 시스템으로 가끔 플러시됩니다. 모든 작업에 대한 타이머와 카운터가 모두 로그 파일에 기록됩니다. 거기에서 다음 시스템에 의해 로그가 처리되고 집계 지표가 계산됩니다. 우리는 이러한 방식으로 높은 수준의 집계 운영 지표에서 요청 수준의 자세한 문제 해결 데이터까지 모든 정보를 얻습니다. 이들 모두 계측 코드에 대한 하나의 접근 방식을 통해 가능합니다. Amazon은 먼저 로깅하고 나중에 집계 지표를 생성합니다.
로깅을 통해 계측
우리를 일반적으로 서비스를 계측하여 요청 데이터와 디버깅 데이터, 이렇게 두 종류의 데이터를 내보냅니다. 요청 로그 데이터는 대개 각 작업 단위에 대한 하나의 구조화된 로그 항목으로 표시됩니다. 이 데이터에는 요청과 요청자에 대한 속성, 요청 대상, 발생 빈도에 대한 카운터 및 걸린 시간에 대한 타이머가 들어 있습니다. 요청 로그는 감사 로그와 서비스에서 발생한 모든 것에 대한 추적 역할을 합니다. 디버깅 데이터에는 애플리케이션이 내보내는 디버깅 라인의 구조화된 데이터나 느슨하게 구조화된 데이터가 들어 있습니다. 일반적으로 이러한 데이터는 Log4j 오류 또는 경고 로그 줄과 같은 구조화되지 않은 로그 항목입니다. Amazon에서 이러한 두 종류의 데이터는 대개 기록을 위해 별도의 로그 파일로 내보내집니다. 그러나 형식이 같은 로그 항목 형식으로 로그 분석을 수행하는 것이 편리할 수 있기 때문이기도 합니다.
CloudWatch Logs Agent와 같은 에이전트는 실시간으로 두 종류의 로그 데이터를 처리하고 CloudWatch Logs로 로그를 전송합니다. 그러면 CloudWatch Logs가 실시간에 가깝게 서비스에 대한 집계 지표를 생성합니다. Amazon CloudWatch Alarms가 이러한 집계 지표를 읽고 경보를 트리거합니다.
모든 요청에 대해 너무 자세한 정보를 로깅하는 데 비용이 많이 들 수 있지만 Amazon에서는 이것이 매우 중요함을 알게 되었습니다. 결국 가용성 블립, 지연 시간 증가, 고객이 보고한 문제를 조사해야 합니다. 자세한 로그 없이는 고객에게 답을 줄 수 없으며 고객의 서비스를 개선할 수 없습니다.
세부 정보 얻기
모니터링 및 경보 항목은 방대합니다. 이 문서에서는 경보 임계값 설정 및 조정, 서버 쪽 및 클라이언트 쪽 모두에서 성능 측정, “카나리아” 애플리케이션 계속 실행, 지표 집계 및 로그 분석을 위해 사용할 올바른 시스템 선택 등의 항목은 다루지 않습니다.
이 문서는 올바른 원시 측정 데이터 생성을 위해 애플리케이션을 계측해야 하는 필요성에 초점을 맞춥니다. 또한 Amazon의 팀에서 애플리케이션을 계측할 때 포함하거나 피하고자 하는 사항에 대해 설명합니다.
요청 로그 모범 사례
이 섹션에서는 Amazon에서 “작업 단위당” 데이터 로깅에 대해 습득한 좋은 습관을 설명하겠습니다. 이러한 기준을 충족하는 로그에는 발생 빈도를 나타내는 카운터, 걸린 시간을 포함한 타이머, 각 작업 단위에 대한 메타데이터를 포함한 속성이 들어 있습니다.
• 모든 작업 단위에 대해 하나의 요청 로그 항목을 내보냅니다. 작업 단위는 대개 서비스가 받은 요청이나 대기열에서 가져오는 메시지입니다. 우리는 서비스가 받는 각 요청마다 하나씩 서비스 로그 항목을 기록합니다. 여러 작업 단위를 하나로 결합하지 않습니다. 따라서 실패한 요청 문제를 해결할 때 하나의 로그 항목만 보면 됩니다. 이 항목에는 시도한 작업 보기 요청에 대한 관련 입력 매개 변수, 호출자가 누구였는지에 대한 정보, 모든 타이밍 및 카운터 정보가 들어 있습니다.
• 지정된 요청에 대해 하나의 요청 로그 항목만 내보냅니다. 비차단 서비스 구현에서는 처리 파이프라인의 각 단계마다 별도의 로그 항목을 내보내는 게 편리해 보일 수 있습니다. 대신 파이프라인의 단계 사이에서 단일 “지표 객체”로 핸들을 연결하고 모든 단계가 완료된 후 지표를 하나의 단위로 직렬화하여 이러한 시스템을 더 많이 해결할 수 있었습니다. 작업 단위당 로그 항목이 여러 개인 경우 로그 분석이 더 어려워지고 이미 비용이 많이 드는 로깅 오버헤드가 승수에 따라 증가합니다. 새로운 비차단 서비스를 작성할 경우 먼저 지표 로깅 수명주기를 계획하려고 합니다. 이는 나중에 리팩터링하고 수정하기 매우 어렵기 때문입니다.
• 오래 실행되는 태스크를 여러 로그 항목으로 나눕니다. 이전 권장 사항과 달리 수 분 또는 수 시간 동안 오래 실행되는 워크플로와 같은 태스크가 있는 경우 진행 상황과 속도가 느려지는 부분을 파악할 수 있도록 주기적으로 별도의 로그 항목을 내보낼 수 있습니다.
• 확인과 같은 작업을 수행하기 전에 요청에 대한 세부 정보를 기록합니다. 문제 해결과 감사 로깅을 위해 목표가 무엇인지 알 수 있도록 요청에 대한 충분한 정보를 로깅하는 것이 중요합니다. 또한 요청이 확인, 인증 또는 스로틀 논리에 의해 거부되기 전에 이 정보를 최대한 일찍 로깅하는 것이 중요합니다. 받는 요청의 정보를 로깅하는 경우 입력을 로깅하기 전에 삭제(인코드, 이스케이스 및 자르기)해야 합니다. 예를 들어, 호출자가 서비스 로그 항목을 전달한 경우 여기에 1MB 길이의 문자열을 포함하지 않으려고 합니다. 이렇게 하면 디스크가 가득 차고 로그 스토리지에 예상보다 더 많은 비용이 들 위험이 있습니다. 삭제의 또 다른 예는 로그 형식과 관련된 ASCII 제어 문자나 이스케이프 시퀀스를 필터링하는 것입니다. 호출자가 자신의 서비스 로그 항목을 전달하고 로그에 주입할 수 있는 경우 이는 혼란스러울 수 있습니다. 다음 문서도 참조하십시오. https://xkcd.com/327/
• 더 자세히 로깅하는 방법을 계획합니다. 로그에 문제가 있는 요청에 대한 세부 정보가 충분히 들어 있지 않으면 실패한 이유를 알아낼 수 없습니다. 해당 정보는 서비스에서 사용 가능하지만 상시 로깅을 정당화할 만큼 정보량이 많지 않습니다. 문제를 조사하면서 로그의 자세한 정도를 일시적으로 늘리기 위해 돌릴 수 있는 구성 노브가 있으면 도움이 될 수 있습니다. 개별 호스트에서, 개별 클라이언트에 대해 또는 플릿 전체에서 샘플링 비율로 노브를 돌릴 수 있습니다. 완료되면 잊지 말고 노브를 다시 돌려 놓아야 합니다.
• 지표 이름을 짧게(그러나 너무 짧지 않게) 유지합니다. Amazon은 15여 년간 동일한 서비스 로그 직렬화를 사용해 왔습니다. 이 직렬화에서 각 카운터와 타이머 이름이 모든 서비스 로그 항목에서 일반 텍스트로 반복됩니다. 로깅 오버헤드 최소화에 도움이 되도록 우리는 짧지만 설명을 포함하는 타이머 이름을 사용합니다. Amazon은 Amazon Ion이라는 이진 직렬화 프로토콜 기반의 새로운 직렬화 형식을 채택하기 시작했습니다. 궁극적으로는 로그 분석 도구에서 이해할 수 있으면서 최대한 효율적으로 직렬화, 역직렬화 및 저장할 수 있는 형식을 고르는 것이 중요합니다.
• 로그 볼륨이 최대 처리량의 로깅을 처리할 수 있을 만큼 충분히 큰지 확인합니다. 우리는 몇 시간 동안 최대의 지속적인 로드 또는 오버로드로 서비스에 대한 로드 테스트를 수행합니다. 서비스가 과다한 트래픽을 처리할 때 서비스가 새 로그 항목이 생성되는 속도로 로그를 전송하기 위한 리소스를 여전히 갖고 있는지 확인해야 합니다. 그렇지 않으면 디스크가 가득 차게 됩니다. 또한 과도한 로깅 발생 시 시스템이 고장 나지 않게 루트 파티션이 아닌 다른 파일 시스템 파티션에서 로깅이 수행되도록 구성할 수 있습니다. 처리량에 비례하는 동적 샘플링을 사용하는 등 나중에 이에 대한 다른 완화 방법에 대해 토론하겠습니다. 어느 전략이든 테스트가 중요합니다.
• 디스크가 가득 찰 경우 시스템 동작을 고려합니다. 서버의 디스크가 가득 차면 디스크에 로깅할 수 없습니다. 이 경우 서비스가 요청 수락을 중지하거나 로그를 삭제하고 모니터링 없이 작동을 계속해야 할까요? 로깅 없이 작동하는 것은 위험하므로 시스템을 테스트하여 디스크가 거의 가득 찬 서버를 탐지합니다.
• 클록을 동기화합니다. 분산 시스템에서 “시간”이라는 개념은 매우 복잡합니다. 우리는 분산 알고리즘에서 클록 동기화를 사용하지 않지만 이는 로그를 이해하는 데 필요합니다. 우리는 클록 동기화를 위해 Chrony나 ntpd와 같은 디먼을 실행하고 서버에서 클록 드리프트를 모니터링합니다. 이를 더 쉽게 수행하려면 Amazon Time Sync Service를 참조하십시오.
• 가용성 지표에 대해 0 카운트를 내보냅니다. 오류 개수는 유용하지만 오류 비율도 유용할 수 있습니다. “가용성 비율” 지표에 대해 계측하는 데 유용한 전략은 요청이 성공할 때는 1을 내보내고 실패할 때는 0을 내보내는 것입니다. 결과 지표의 “평균” 통계가 가용성 비율입니다. 의도적으로 0 데이터 요소를 내보내는 것은 다른 상황에서도 도움이 될 수 있습니다. 예를 들어, 애플리케이션이 리더 선택을 수행하는 경우 한 프로세서가 리더이면 주기적으로 1을 내보내고 프로세스가 리더가 아니면 0을 내보내는 방식을 사용하면 추적자의 상태를 모니터링하는 데 도움이 될 수 있습니다. 이 방법을 통해 프로세스가 0 내보내기를 중지할 경우 프로세스 안의 무엇인가가 손상되었음을 더 쉽게 알 수 있고 리더에게 무슨 일이 발생할 때 제어권을 가져올 수 없습니다.
• 모든 종속 항목의 가용성과 지연 시간을 로깅합니다. 이는 “요청이 왜 느립니까?” 또는 “요청이 왜 실패했습니까?”라는 질문에 답하는 데 특히 유용합니다 이 로그가 없다면 종속 항목의 그래프와 서비스의 그래프를 비교하고 종속 서비스의 지연 시간 증가가 현재 조사 중인 요청 실패로 이어졌는지 여부를 추측할 수만 있습니다. 많은 서비스 및 클라이언트 프레임워크가 자동으로 지표를 연결하지만 AWS SDK 등의 다른 프레임워크에는 수동 계측이 필요합니다.
• 호출당, 리소스당, 상태 코드당 등으로 종속 항목 지표를 나눕니다. 동일한 작업 단위에서 동일한 종속 항목과 여러 번 상호 작용하는 경우 각 호출에 대해 별도로 지표를 포함하고 각 리소스가 어느 리소스와 상호 작용하고 있는지를 분명히 합니다. 예를 들어, Amazon DynamoDB를 호출할 때 몇몇 팀에서 테이블, 오류 코드, 재시도 횟수당 타이밍 및 지연 시간 지표를 포함하는 것이 도움이 된다는 사실을 발견했습니다. 이를 통해 조건부 검사 실패에 따른 재시도로 서비스가 느린 문제를 더 쉽게 해결할 수 있습니다. 이러한 지표를 통해 클라이언트가 인지한 지연 시간 증가가 실제로는 패킷 손실이나 네트워크 지연 시간이 아닌 스로틀 재시도 또는 결과 세트 페이지 매김으로 인한 것인지도 확인할 수 있습니다.
• 액세스 시 메모리 대기열 깊이를 기록합니다. 요청이 대기열과 상호 작용하고, 대기열에서 객체를 끌어오거나, 대기열에 뭔가를 넣을 경우 지표 객체에 현재 대기열 깊이를 기록합니다. 인 메모리 대기열의 경우 이 정보는 매우 적은 비용으로 얻을 수 있습니다. 분산 대기열의 경우 API 호출에 대한 응답에 이 메타데이터를 무료로 사용할 수 있습니다. 이 로깅은 향후에 지연 시간의 원인과 백로그를 찾는 데 도움이 됩니다. 또한 대기열에서 객체를 꺼낼 때 대기열에 있었던 기간을 측정합니다. 즉, 대기열에 넣기 전에 먼저 메시지에 “대기열에 넣은 시간” 지표를 직접 추가해야 합니다.
• 모든 오류 원인에 대한 카운터를 추가합니다. 실패한 모든 요청에 대한 특정 오류 원인을 세는 코드 추가를 고려하십시오. 애플리케이션 로그에는 실패로 이어진 정보와 자세한 예외 메시지가 포함됩니다. 그러나 애플리케이션 로그에서 해당 정보를 찾을 필요도 없이 지표에서 오류 정보의 추세를 확인하면 도움이 됩니다. 각 실패 예외 클래스마다 별도의 지표로 시작하는 것이 편리합니다.
• 원인 범주별로 오류를 구성합니다. 모든 오류가 동일한 지표에 취합되면 지표가 복잡해져서 활용도가 떨어집니다. 최소한 “서버의 장애”인 오류와 “클라이언트의 장애”인 오류를 구분하는 것이 중요합니다. 그 외에 추가 분석도 도움이 됩니다. 예를 들어, DynamoDB에서 클라이언트는 수정 중인 항목이 요청의 사전 조건과 일치하지 않을 경우 오류를 반환하는 조건부 쓰기 요청을 만들 수 있습니다. 이러한 오류는 의도적이며 가끔 발생합니다. 반면 클라이언트의 “잘못된 요청” 오류는 대개 수정이 필요한 버그입니다.
• 작업 단위에 대한 중요한 메타데이터를 로깅합니다. 이후에 누가 요청을 했으며 요청이 무엇을 하려고 했는지 파악할 수 있도록 구조화된 지표 로그에 요청에 대한 충분한 메타데이터도 포함합니다. 여기에는 고객이 문제가 있어 연락할 때 로그에 포함되어 있을 것으로 기대하는 메타데이터가 포함됩니다. 예를 들어, DynamoDB는 요청이 상호 작용 중인 테이블의 이름과 읽기 작업이 일관된 읽기인지 여부와 같은 메타데이터를 로깅합니다. 그러나 데이터베이스에서 검색 중이거나 데이터베이스에 저장 중인 데이터를 로깅하지 않습니다.
• 액세스 제어와 암호화로 데이터를 보호하십시오. 로그에는 어느 정도의 민감한 데이터가 들어 있으므로 데이터 보호를 위한 조치를 취합니다. 이러한 조치에는 로그 암호화, 문제 해결 중인 운영자로 액세스 제한, 정기적으로 이러한 액세스의 기준 지정이 포함됩니다.
• 너무 민감한 정보는 로그에 넣지 마십시오. 민감한 정보를 포함하는 로그가 유용합니다. 지정된 요청의 출처를 알 수 있도록 충분한 정보를 로그에 포함하는 것이 중요합니다. 그러나 요청 처리 동작이나 라우팅에 영향을 주지 않는 요청 매개 변수와 같이 너무 민감한 정보는 제외합니다. 예를 들어, 코드의 고객 메시지 구문 분석이 실패할 경우 고객 개인 정보 보호를 위해 페이로드를 로깅하지 않는 것이 중요합니다. 그러면 나중에 문제를 해결하는 데 어려움을 겪을 수 있습니다. 우리는 나중에 추가된 민감한 새 매개 변수의 로깅을 방지하기 위해 도구를 사용하여 옵트아웃 방식 대신 옵트인 방식으로 무엇을 로깅할 수 있는지 결정합니다. Amazon API Gateway와 같은 서비스를 사용하여 자체 액세스 로그에 어느 데이터를 포함할지 구성할 수 있습니다. 이는 훌륭한 옵트인 메커니즘 역할을 합니다.
• 추적 ID를 로깅하고 백엔드 호출에서 전파합니다. 지정된 고객 요청에는 함께 작동하는 많은 서비스가 연관되어 있을 수 있습니다. 적게는 많은 AWS 요청을 위한 2 ~ 3개의 서비스가 연관되어 있을 수 있고, 많게는 amazon.com 요청을 위한 훨씬 더 많은 서비스가 연관되어 있을 수 있습니다. 분산 시스템 문제를 해결할 때 어떤 일이 있었는지 파악하기 위해 다양한 시스템의 로그를 정렬하여 어디에서 장애가 발생했는지 알 수 있도록 이러한 시스템 간에 동일한 추적 ID를 전파합니다. 추적 ID는 작업 단위의 시작 지점인 “프런트 도어” 서비스에서 분산 작업 단위에 찍히는 일종의 메타 요청 ID입니다. AWS X-Ray는 이러한 전파의 일부를 제공하는 유용한 서비스입니다. 종속 항목에 추적을 전달하는 것도 중요합니다. 다중 스레드 환경에서는 프레임워크가 우리를 대신하여 이 전파를 수행하는 것은 매우 어렵고 오류가 발생하기 쉬우므로 메서드 서명으로 추적 ID와 지표 객체 등의 기타 요청 콘텐츠를 전달하는 습관을 갖게 되었습니다. 또한 향후에 전달할 비슷한 패턴을 발견할 경우 리팩터링할 필요가 없도록 메서드 서명으로 Context 객체를 전달하는 것이 편리함을 알게 되었습니다. AWS 팀의 경우 시스템 문제 해결만이 아니라 고객 문제 해결도 중요합니다. 고객은 AWS 서비스가 고객을 대신하여 서로 상호 작용할 때 서비스 간에 전달되는 AWS X-Ray 추적을 사용합니다. 이를 위해서는 완전한 추적 데이터를 받을 수 있도록 서비스 간에 고객 AWS X-Ray 추적 ID를 전파해야 합니다.
• 상태 코드와 크기에 따라 다른 지연 시간 지표를 로깅합니다. 액세스 거부, 스로틀 및 확인 오류 응답과 같은 오류는 빠른 경우가 많습니다. 클라이언트가 높은 비율로 스로틀되기 시작하면 지연 시간이 언뜻 보기에는 괜찮아 보일 수 있습니다. 이러한 지표 오염을 방지하기 위해 우리는 성공적인 응답에 대해 별도의 타이머를 로깅하고 일반 Time 지표를 사용하는 대신 대시보드와 경보에서 해당 지표에 초점을 둡니다. 마찬가지로 입력 크기나 응답 크기에 따라 더 느려질 수 있는 작업이 있는 경우 SmallRequestLatency와 LargeRequestLatency처럼 범주화되는 지연 시간 지표를 내보내는 것을 고려합니다. 또한 복잡한 부분 정전과 장애 모드를 피하기 위해 요청과 응답을 적절히 제한합니다. 그러나 조심스럽게 설계된 서비스에서도 이러한 지표 버킷팅 기술을 통해 고객 동작이 격리되고 혼란을 주는 노이즈가 대시보드에서 차단될 수 있습니다.
애플리케이션 로그 모범 사례
이 섹션에서는 구조화되지 않은 디버그 로그 데이터 로깅에 대해 Amazon에서 습득한 좋은 습관에 대해 설명했습니다.
• 스팸 없이 깨끗하게 애플리케이션 로그를 유지합니다. 테스트 환경에서 개발 및 디버깅에 도움이 되도록 요청 경로에 INFO 및 DEBUG 수준의 로그 문이 있을 수 있지만 프로덕션에서는 이러한 로그 수준을 사용하지 않는 것을 고려합니다. 요청 추적 정보를 얻기 위해 애플리케이션 로그를 사용하는 대신 쉽게 지표를 생성하고 시간에 따른 집계 추세를 확인할 수 있는 추적 정보 위치로 서비스 로그를 생각합니다. 그러나 여기에는 흑백 규칙이 없습니다. 우리의 접근 방식은 로그를 계속 검토하여 로그에 노이즈가 많은지 또는 부족한지 확인하고 시간에 따라 로그 수준을 조정하는 것입니다. 예를 들어, 로그를 살펴보다가 노이즈가 너무 많은 로그 문이나 원하는 지표를 종종 발견하기도 합니다. 다행히 이러한 개선 작업은 쉬운 경우가 많으므로 로그를 깨끗하게 유지하기 위해 빠른 추가 작업 백로그 항목을 제출하는 습관을 갖게 되었습니다.
• 해당하는 요청 ID를 포함합니다. 애플리케이션 로그에서 오류 문제를 해결할 때 오류를 트리거한 호출자의 요청에 대한 세부 정보가 필요한 경우가 종종 있습니다. 두 로그 모두에 동일한 요청 ID가 들어 있으면 한 로그에서 다른 로그로 쉽게 건너뛸 수 있습니다. 애플리케이션 로깅 라이브러리는 적절히 구성된 경우 해당하는 요청 ID를 기록하고 요청 ID는 ThreadLocal로 설정됩니다. 애플리케이션이 다중 스레드된 경우 스레드가 새 요청을 처리하기 시작할 때 올바른 요청 ID를 설정하는 데 각별한 주의를 기울이십시오.
• 애플리케이션 로그의 오류 스팸 비율을 제한합니다. 일반적으로 서비스는 애플리케이션 로그에 오류를 많이 내보내지 않지만 갑자기 오류 양이 많아지기 시작하면 스택 추적이 포함된 매우 큰 로그 항목을 높은 비율로 기록하기 시작하는 것일 수 있습니다. 이를 방지하는 한 가지 방법은 지정된 로거의 로깅 빈도를 제한하는 것입니다.
• String#format 또는 문자열 연결보다 형식 문자열을 선호합니다. 더 오래된 애플리케이션 로그 API 작업은 log4j2의 varargs 형식 문자열 api 대신 단일 문자열 메시지를 받아들입니다. DEBUG 문으로 코드가 계측되지만 프로덕션이 ERROR 수준에서 구성된 경우 무시되는 DEBUG 메시지 문자열 형식을 지정하는 것은 무의미한 작업일 수 있습니다. 일부 로깅 API 작업에서는 로그 항목이 기록될 경우에만 toString() 메서드가 호출되게 하는 임의의 객체 전달을 지원합니다.
• 실패한 서비스 호출의 요청 ID를 기록합니다. 서비스가 호출되고 오류를 반환하는 경우 요청 ID가 반환되었을 수 있습니다. 해당 서비스 소유자에게 문의해야 하는 경우 소유자의 해당 서비스 로그 항목을 쉽게 찾을 수 있도록 로그에 요청 ID를 포함하면 유용합니다. 제한 시간 초과 오류는 이를 어렵게 만듭니다. 서비스가 요청 ID를 아직 반환하지 않았거나 클라이언트 라이브러리가 요청 ID를 구문 분석하지 않았을 수 있기 때문입니다. 그럼에도 불구하고 서비스에서 요청 ID를 반환하면 우리는 그것을 로깅합니다.
높은 처리량 서비스 모범 사례
Amazon에서 제공하는 대다수 서비스의 경우 모든 요청에서 로깅이 과도한 비용 오버헤드를 발생시키지는 않습니다. 더 높은 처리량 서비스는 더 회색인 영역에 들어가지만 여전히 모든 요청에서 로깅하는 경우가 많습니다. 예를 들어, Amazon 내부 트래픽만 해도 초당 최대 2천 개가 넘는 요청을 처리하는 DynamoDB는 그렇게 많이 로깅하지는 않는다고 대개 생각하지만 사실 감사와 규정 준수를 이유로 문제 해결을 위해 모든 요청을 로깅합니다. Amazon에서는 더 높은 호스트당 처리량에서 로깅을 더 효율적으로 만들기 위해 다음과 같은 고급 팁을 사용합니다.
• 로그를 샘플링합니다. 모든 항목을 기록하는 대신 N개 항목마다 기록하는 것도 좋은 방법입니다. 지표 집계 시스템이 계산하는 지표에서 실제 로그 양을 추정할 수 있도록 얼마나 많은 항목을 건너뛰었는지에 대한 정보도 각 항목에 들어 있습니다. 저장 장치 샘플링 등의 다른 샘플링 알고리즘은 더 대표적인 샘플을 제공합니다. 다른 알고리즘은 성공한 빠른 요청보다 오류나 느린 요청을 더 우선시합니다. 그러나 샘플링을 사용하면 고객을 지원하거나 특정 장애 문제를 해결할 수 없습니다. 몇몇 규정 준수 요구 사항으로 인해 불가능합니다.
• 별도의 스레드로 직렬화와 로그를 플러싱을 오프로드합니다. 쉬운 변경이며 많이 사용됩니다.
• 로그를 자주 회전합니다. 로그 파일 로그를 1시간마다 회전하면 처리할 파일 수가 더 적어서 편리해 보일 수 있지만 1분마다 회전하면 여러 가지 사항이 개선됩니다. 예를 들어, 로그 파일을 읽고 압축하는 에이전트가 디스크 대신 페이지 캐시에서 파일을 읽고, 항상 1시간이 끝날 때 트리거하는 대신 1시간에 걸쳐 압축 및 전송 로그가 배분됩니다.
• 미리 압축된 로그를 씁니다. 로그를 전송하는 에이전트가 로그를 아카이브 서비스로 보내기 전에 압축하는 경우 시스템 CPU와 디스크가 주기적으로 증가합니다. 압축된 로그를 디스크로 스트리밍하여 이 비용을 상각하고 디스크 IO를 반으로 줄일 수 있습니다. 그렇지만 몇 가지 위험이 따르기도 합니다. 애플리케이션 자동 중단 시 잘린 파일을 처리할 수 있는 압축 알고리즘을 사용하면 도움이 됩니다.
• ramdisk/tmpfs에 기록합니다. 서비스가 디스크에 로그를 기록하는 대신 서버에서 전송될 때까지 메모리에 로그를 기록하는 것이 더 쉬울 수 있습니다. 경험상 이는 1시간마다 로그 회전보다 1분마다 로그 회전에서 효과적입니다.
• 인 메모리 집계. 하나의 시스템에서 초당 수백, 수천 개의 트랜잭션을 처리해야 할 경우 요청당 단일 로그 항목을 기록하는 데 너무 많은 비용이 들 수 있습니다. 그러나 이를 건너뛰면 가시성이 많이 떨어지므로 중간에 최적화하지 않는 것이 좋습니다.
• 리소스 사용을 모니터링합니다. 우리는 조정 한도에 도달할 때까지 얼마나 남았는지에 주의를 기울입니다. 서버별 IO 및 CPU는 물론, 로깅하는 에이전트에서 얼마나 많은 리소스를 소비하고 있는지 측정합니다. 로드 테스트를 수행할 때 로그 전송 에이전트가 처리량을 따라갈 수 있음을 입증할 수 있도록 충분히 오래 테스트를 수행합니다.
적절한 로그 분석 도구를 사용하십시오.
Amazon에서 우리는 직접 작성하는 서비스를 운영하므로 해당 서비스의 문제 해결 전문가가 되어야 합니다. 즉, 손쉽게 로그 분석을 수행할 수 있어야 합니다. 상대적으로 적은 수의 로그를 보기 위한 로컬 로그 분석부터 방대한 양의 로그 결과를 살펴보고 집계하기 위한 분산 로그 분석까지 많은 도구를 마음대로 사용할 수 있습니다.
로그 분석을 위한 팀의 도구와 런북에 투자하는 것은 중요합니다. 지금은 로그가 작지만 이후에 서비스 증가가 예상되는 경우 분산 로그 분석 솔루션 채택에 투자할 수 있도록 언제 현재 도구가 더 이상 확장되지 않는지에 주의를 기울여야 합니다.
로그 분석 과정에는 다양한 Linux 명령줄 유틸리티에 대한 경험이 필요할 수 있습니다. 예를 들어, 일반적인 “로그에서 최고 토커 IP 주소 찾기”는 간단합니다.
cat log | grep -P "^RemoteIp=" | cut -d= -f2 | sort | uniq -c | sort -nr | head -n20
그러나 다음과 같이 로그에 대한 더 복잡한 질문에 답하는 데 유용한 다른 도구도 많습니다.
• jq: https://stedolan.github.io/jq/
• RecordStream: https://github.com/benbernard/RecordStream
Amazon EMR, Amazon Athena, Amazon Aurora, Amazon Redshift 등의 빅 데이터 분석 서비스를 사용하여 분산 로그 분석을 수행할 수 있습니다. 그러나 일부 서비스는 Amazon CloudWatch Logs 등의 로깅 시스템이 함께 제공됩니다.
• CloudWatch Logs Insights
• AWS X-Ray: https://aws.amazon.com/xray/
• Amazon Athena: https://aws.amazon.com/athena/
결론
서비스 소유자이자 소프트웨어 개발자로서 저는 대시보드의 그래프, 개별 로그 파일 등 계측 출력을 보고 CloudWatch Logs Insights와 같은 분산 로그 분석 도구를 사용하는 데 많은 시간을 보냅니다. 이들은 제가 좋아하는 일이기도 합니다. 어려운 태스크를 끝내고 휴식이 필요할 때 저 자신에게 주는 상으로 로그를 분석하면서 재충전합니다. “왜 여기에서 지표가 증가했지?”나 “이 작업의 지연 시간을 줄일 수 있나?” 등의 질문으로 시작합니다. 질문의 답을 찾지 못할 때 종종 코드에서 도움이 되는 측정값을 찾아내기도 합니다. 이런 경우 저는 계측을 추가하고, 테스트하고, 코드 검토를 팀 동료에게 보냅니다.
많은 지표가 우리가 사용하는 관리형 서비스와 함께 제공되지만 서비스를 효과적으로 운영하는 데 필요한 가시성을 확보하기 위해서는 자체 서비스 계측에 대해 많은 생각을 해야 합니다. 운영 이벤트 중에는 왜 문제가 있으며 해당 문제를 완화하기 위해 무엇을 할 수 있는지 빠르게 결정해야 합니다. 대시보드에 올바른 지표가 있어야 이러한 진단을 신속하게 수행할 수 있습니다. 또한 항상 서비스가 변경되고, 새로운 기능이 추가되고, 서비스와 종속 항목의 상호 작용 방식이 변경되고 있기 때문에 올바른 계측을 업데이트하고 추가하는 일은 영원히 현재 진행형입니다.
링크
• 전 Amazon 직원인 John Rauser의 “Look at your data”: https://www.youtube.com/watch?v=coNDCIMH8bk(13:22 부분에서 로그를 더 제대로 보려면 출력하라고 말함)
• 전 Amazon 직원인 John Rauser의 “Investigating anomalies”: https://www.youtube.com/watch?v=-3dw09N5_Aw
• 전 Amazon 직원인 John Rauser의 “How humans see data”: https://www.youtube.com/watch?v=fSgEeI2Xpdc
• https://www.akamai.com/uk/en/about/news/press/2017-press/akamai-releases-spring-2017-state-of-online-retail-performance-report.jsp
저자에 대하여
David Yanacek는 AWS Lambda 서비스의 선임 수석 엔지니어입니다. David는 2006년부터 Amazon에서 소프트웨어 개발자로 근무하고 있으며, 전에는 Amazon DynamoDB와 AWS IoT는 물론, 내부 웹 서비스 프레임워크와 플릿 운영 자동화 시스템도 다루었습니다. David가 회사에서 가장 즐겨하는 활동 중 하나는, 로그를 분석하고 운영 지표를 면밀히 조사하며 시간에 따라 시스템을 보다 더 원활하게 실행시키는 방법을 찾는 것입니다.