편집

다음을 통해 공유


성능 튜닝 - 이벤트 스트리밍

Azure 기능
Azure IoT Hub
Azure Cosmos DB

이 문서에서는 개발 팀이 메트릭을 사용하여 병목 상태를 찾고 분산 시스템의 성능을 개선하는 방법을 설명합니다. 이 문서는 샘플 애플리케이션에 대해 수행한 실제 부하 테스트를 기반으로 합니다.

이 문서는 시리즈의 일부입니다. 여기에서 첫 번째 부분을 읽어보세요.

시나리오: Azure Functions를 사용하여 이벤트 스트림을 처리합니다.

이벤트 스트리밍 아키텍처의 다이어그램

이 시나리오에서는 드론 집합이 위치 데이터를 Azure IoT Hub에 실시간으로 보냅니다. Functions 앱은 이벤트를 수신하고, 데이터를 GeoJSON 형식으로 변환하고, 변환된 데이터를 Azure Cosmos DB에 씁니다. Azure Cosmos DB는 지리 공간적 데이터를 기본적으로 지원하며 효율적인 공간 쿼리를 위해 Azure Cosmos DB 컬렉션을 인덱싱할 수 있습니다. 예를 들어 클라이언트 애플리케이션은 지정된 위치에서 1km 이내에 있는 모든 드론을 쿼리하거나 특정 영역 내의 모든 드론을 찾을 수 있습니다.

이러한 처리 요구 사항은 본격적인 스트림 처리 엔진이 필요하지 않을 정도로 간단합니다. 특히 처리는 스트림을 조인하거나 데이터를 집계하거나 기간에 걸쳐 처리하지 않습니다. 이러한 요구 사항에 따라 Azure Functions는 메시지 처리에 적합합니다. Azure Cosmos DB는 매우 높은 쓰기 처리량을 지원하도록 스케일링할 수도 있습니다.

처리량 모니터링

이 시나리오는 흥미로운 성능 과제를 제시합니다. 디바이스당 데이터 속도는 알려져 있지만 디바이스 수는 변동될 수 있습니다. 이 비즈니스 시나리오의 경우 대기 시간 요구 사항은 특별히 엄격하지 않습니다. 드론의 보고된 위치는 1분 이내에만 정확하면 됩니다. 즉, 함수 앱은 시간이 지남에 따라 평균 수집 속도를 따라잡아야 합니다.

IoT Hub는 메시지를 로그 스트림에 저장합니다. 들어오는 메시지는 스트림의 꼬리에 추가됩니다. 스트림의 판독기(이 경우 함수 앱)는 스트림을 트래버스하는 자체 속도를 제어합니다. 이러한 읽기 및 쓰기 경로의 분리는 IoT Hub를 매우 효율적으로 만들지만 느린 판독기가 뒤쳐질 수 있음을 의미합니다. 이 조건을 감지하기 위해 개발 팀은 메시지 지연을 측정하는 사용자 지정 메트릭을 추가했습니다. 이 메트릭은 메시지가 IoT Hub에 도착하는 시간과 함수가 처리를 위해 메시지를 수신하는 시간 사이의 차이를 기록합니다.

var ticksUTCNow = DateTimeOffset.UtcNow;

// Track whether messages are arriving at the function late.
DateTime? firstMsgEnqueuedTicksUtc = messages[0]?.EnqueuedTimeUtc;
if (firstMsgEnqueuedTicksUtc.HasValue)
{
    CustomTelemetry.TrackMetric(
                        context,
                        "IoTHubMessagesReceivedFreshnessMsec",
                        (ticksUTCNow - firstMsgEnqueuedTicksUtc.Value).TotalMilliseconds);
}

TrackMetric 메서드는 Application Insights에 사용자 지정 메트릭을 씁니다. Azure 함수 내에서 TrackMetric을 사용하는 방법에 대한 자세한 내용은 C# 함수의 사용자 지정 원격 분석을 참조하세요.

함수가 메시지 양을 따라잡는다면 이 메트릭은 낮은 안정 상태를 유지해야 합니다. 약간의 대기 시간은 피할 수 없으므로 값은 0이 되지 않습니다. 그러나 함수가 뒤처지면 큐에 넣은 시간과 처리 시간 사이의 델타가 올라가기 시작합니다.

테스트 1: 기준

첫 번째 부하 테스트는 즉각적인 문제를 보여 줍니다. 함수 앱은 Azure Cosmos DB에서 지속적으로 HTTP 429 오류를 수신하여 Azure Cosmos DB가 쓰기 요청을 제한하고 있음을 나타냅니다.

Azure Cosmos DB 제한된 요청 그래프

이에 대응하여 팀은 컬렉션에 할당된 RU 수를 늘려 Azure Cosmos DB를 스케일링했지만 오류는 계속 발생했습니다. 봉투 뒤 계산 결과 Azure Cosmos DB가 쓰기 요청 볼륨을 따라잡는 데 문제가 없어야 했으므로 이는 이상해 보였습니다.

그날 늦게 개발자 중 한 명이 팀에 다음 메일을 보냈습니다.

웜 경로에 대해 Azure Cosmos DB를 살펴보았습니다. 한 가지 이해가 가지 않는 점이 있습니다. 파티션 키는 deliveryId이지만 deliveryId를 Azure Cosmos DB로 보내지 않습니다. 제가 무엇인가를 놓치고 있나요?

그것이 단서였습니다. 파티션 열 지도를 보면 모든 문서가 동일한 파티션에 있는 것으로 나타났습니다.

Azure Cosmos DB 파티션 열 지도 그래프

열 지도에서 보고 싶은 것은 모든 파티션에 고르게 분포된 것입니다. 이 경우 모든 문서가 동일한 파티션에 쓰여졌으므로 RU를 추가하는 것은 도움이 되지 않았습니다. 문제는 코드의 버그로 밝혀졌습니다. Azure Cosmos DB 컬렉션에 파티션 키가 있었지만 Azure Function은 실제로 문서에 파티션 키를 포함하지 않았습니다. 파티션 열 지도에 대한 자세한 내용은 파티션 간 처리량 분포 결정을 참조하세요.

테스트 2: 분할 문제 해결

팀에서 코드 수정 사항을 배포하고 테스트를 다시 실행했을 때 Azure Cosmos DB는 제한을 중지했습니다. 잠시 동안 모든 것이 좋아 보였습니다. 그러나 특정 부하에서 원격 분석 결과 함수가 작성해야 하는 문서보다 적은 수의 문서를 작성하고 있는 것으로 나타났습니다. 다음 그래프는 IoT Hub에서 받은 메시지와 Azure Cosmos DB에 기록된 문서를 보여줍니다. 노란색 선은 일괄 처리당 받은 메시지 수이고 녹색은 일괄 처리당 작성된 문서 수입니다. 이는 비례해야 합니다. 그런데 일괄 처리당 데이터베이스 쓰기 작업의 수는 약 07:30에 크게 감소합니다.

삭제된 메시지 그래프

다음 그래프는 메시지가 디바이스에서 IoT Hub에 도착하는 시간과 함수 앱이 해당 메시지를 처리하는 시간 사이의 대기 시간을 보여 줍니다. 동시에 지연이 극적으로 급증하고, 안정되고, 감소하는 것을 볼 수 있습니다.

메시지 지연 그래프

값이 5분에 정점에 도달한 다음 0으로 떨어지는 이유는 함수 앱이 5분 이상 늦은 메시지를 삭제하기 때문입니다.

foreach (var message in messages)
{
    // Drop stale messages,
    if (message.EnqueuedTimeUtc < cutoffTime)
    {
        log.Info($"Dropping late message batch. Enqueued time = {message.EnqueuedTimeUtc}, Cutoff = {cutoffTime}");
        droppedMessages++;
        continue;
    }
}

지연 메트릭이 다시 0으로 떨어지면 그래프에서 이를 볼 수 있습니다. 그 동안 함수가 메시지를 버렸으므로 데이터가 손실되었습니다.

무슨 일이 있었나요? 이 특정 부하 테스트의 경우 Azure Cosmos DB 컬렉션에 여유 RU가 있었으므로 데이터베이스에 병목 상태가 나타나지 않았습니다. 오히려 문제는 메시지 처리 루프에 있었습니다. 간단히 말해서 함수는 들어오는 메시지 양을 따라잡을 수 있을 만큼 빠르게 문서를 작성하지 못했습니다. 시간이 지날수록 점점 뒤쳐졌습니다.

테스트 3: 병렬 쓰기

메시지를 처리하는 시간이 병목 상태인 경우 한 가지 해결 방법은 더 많은 메시지를 병렬로 처리하는 것입니다. 이 시나리오에서는

  • IoT Hub 파티션 수를 늘립니다. 각 IoT Hub 파티션에는 한 번에 하나의 함수 인스턴스가 할당되므로 파티션 수에 따라 처리량이 선형으로 스케일링될 것으로 예상됩니다.
  • 함수 내에서 문서 쓰기를 병렬화합니다.

두 번째 옵션을 탐색하기 위해 팀은 병렬 쓰기를 지원하도록 함수를 수정했습니다. 함수의 원래 버전은 Azure Cosmos DB 출력 바인딩을 사용했습니다. 최적화된 버전은 Azure Cosmos DB 클라이언트를 직접 호출하고 Task.WhenAll을 사용하여 병렬로 쓰기를 수행합니다.

private async Task<(long documentsUpserted,
                    long droppedMessages,
                    long cosmosDbTotalMilliseconds)>
                ProcessMessagesFromEventHub(
                    int taskCount,
                    int numberOfDocumentsToUpsertPerTask,
                    EventData[] messages,
                    TraceWriter log)
{
    DateTimeOffset cutoffTime = DateTimeOffset.UtcNow.AddMinutes(-5);

    var tasks = new List<Task>();

    for (var i = 0; i < taskCount; i++)
    {
        var docsToUpsert = messages
                            .Skip(i * numberOfDocumentsToUpsertPerTask)
                            .Take(numberOfDocumentsToUpsertPerTask);
        // client will attempt to create connections to the data
        // nodes on Azure Cosmos DB clusters on a range of port numbers
        tasks.Add(UpsertDocuments(i, docsToUpsert, cutoffTime, log));
    }

    await Task.WhenAll(tasks);

    return (this.UpsertedDocuments,
            this.DroppedMessages,
            this.CosmosDbTotalMilliseconds);
}

접근 방식을 사용하면 경합 상태가 가능합니다. 동일한 드론에서 두 개의 메시지가 동일한 메시지 일괄 처리에 도착한다고 가정합니다. 병렬로 작성하면 이전 메시지가 이후 메시지를 덮어쓸 수 있습니다. 이 특정 시나리오의 경우 애플리케이션은 간헐적인 메시지 손실을 허용할 수 있습니다. 드론은 5초마다 새 위치 데이터를 보내므로 Azure Cosmos DB의 데이터가 지속적으로 업데이트됩니다. 그러나 다른 시나리오에서는 메시지를 순서대로 엄격하게 처리하는 것이 중요할 수 있습니다.

이 코드 변경 사항을 배포한 후 애플리케이션은 파티션이 32개인 IoT Hub를 사용하여 초당 2,500개 이상의 요청을 수집할 수 있었습니다.

클라이언트 쪽 고려 사항

전반적인 클라이언트 환경은 서버 쪽의 공격적인 병렬화로 인해 저하될 수 있습니다. Azure Cosmos DB 컨테이너에 할당된 처리량을 포화하는 데 필요한 클라이언트 쪽 컴퓨팅 리소스를 크게 줄이는 Azure Cosmos DB 대량 실행기 라이브러리(이 구현에는 표시되지 않음)를 사용하는 것이 좋습니다. 대량 가져오기 API를 사용하여 데이터를 쓰는 단일 스레드 애플리케이션은 클라이언트 시스템의 CPU를 포화하면서 병렬로 데이터를 쓰는 다중 스레드 애플리케이션과 비교할 때 거의 10배 더 큰 쓰기 처리량을 달성합니다.

요약

이 시나리오에서는 다음과 같은 병목 상태가 확인되었습니다.

  • 핫 쓰기 파티션(기록 중인 문서의 파티션 키 값이 누락되었기 때문).
  • IoT Hub 파티션당 직렬로 문서 작성.

이러한 문제를 진단하기 위해 개발 팀은 다음 메트릭을 사용했습니다.

  • Azure Cosmos DB에서의 제한된 요청.
  • 파티션 열 지도 - 파티션당 최대 사용 RU.
  • 수신한 메시지와 만들어진 문서.
  • 메시지 지연.

다음 단계

성능 안티패턴 검토