대규모 IoT 디바이스 배포에 대한 모범 사례

IoT 솔루션 하나를 수백만 대의 디바이스로 확장하는 것은 어려울 수 있습니다. 대규모 솔루션은 서비스 및 구독 한도에 따라 설계되어야 하는 경우가 많습니다. Azure IoT Device Provisioning Service를 사용하는 고객은 IoT Hub 및 Azure IoT 디바이스 SDK와 같은 다른 Azure IoT 플랫폼 서비스 및 구성 요소도 함께 사용합니다. 이 문서에서는 이러한 서비스를 활용하고 배포를 확장할 수 있도록 디자인에 통합할 수 있는 모범 사례, 패턴 및 샘플 코드에 대해 설명합니다. 프로젝트의 디자인 단계부터 이러한 패턴 및 사례를 따르면 IoT 디바이스의 성능을 최대화할 수 있습니다.

새 디바이스 프로비전

첫 번째 프로비전은 IoT 솔루션의 일부로 디바이스를 처음으로 온보딩하는 프로세스입니다. 대규모 배포로 작업할 때는 모든 디바이스가 동시에 연결을 시도하여 발생하는 오버로드 상황을 방지하기 위해 프로비전 프로세스를 예약하는 것이 중요합니다.

엇갈린 프로비전 일정 사용

수백만 규모의 디바이스를 배포하는 경우 모든 디바이스를 한 번에 등록하면 제한(HTTP 응답 코드 429, Too Many Requests)과 디바이스 등록 실패로 인해 DPS 인스턴스 과부하가 발생할 수 있습니다. 이러한 제한을 방지하려면 디바이스에 대해 엇갈린 등록 일정을 사용합니다. DPS 할당량 및 한도에 따라 디바이스 등록 일괄 처리 크기를 구성합니다. 예를 들어 등록 속도가 분당 200개 디바이스인 경우 온보딩의 일괄 처리 크기는 일괄 처리당 200개 디바이스가 됩니다.

작업 다시 시도

서비스가 사용 중이라서 일시적인 오류가 발생하는 경우 재시도 논리를 사용하면 디바이스가 IoT 클라우드에 성공적으로 연결할 수 있습니다. 그러나 재시도 횟수가 너무 많을 경우 용량이 거의 또는 꽉 찬 사용 중인 서비스의 성능이 더욱 저하될 수 있습니다. 모든 Azure 서비스와 마찬가지로 지수 백오프를 사용하여 지능형 재시도 메커니즘을 구현해야 합니다. 다른 재시도 패턴에 대한 자세한 내용은 재시도 디자인 패턴일시적인 오류 처리에서 확인할 수 있습니다.

제한에 걸렸을 때 배포를 즉시 다시 시도하는 것보다는 retry-after 헤더에 지정된 시간까지 기다립니다. 서비스에서 사용할 수 있는 재시도 헤더가 없는 경우 이 알고리즘으로 더 원활한 디바이스 온보딩 환경을 구현할 수 있습니다.

min_retry_delay_msec = 1000
max_retry_delay_msec = (1.0 / <load>) * <T> * 1000
max_random_jitter_msec = max_retry_delay_msec

이 논리를 사용하면 디바이스는 min_retry_delay_msec~max_retry_delay_msec 사이의 임의 시간 동안 다시 연결이 지연됩니다. 최대 재시도 지연은 다음 변수를 사용하여 계산됩니다.

  • <load>는 값이 0보다 큰 구성 가능한 요소입니다. 이는 로드가 평균 로드 시간에 초당 연결 수를 곱하여 수행됨을 나타냅니다.
  • <T>는 디바이스를 콜드 부팅하는 절대 최소 시간으로, T = N / cps로 계산됩니다. 여기서 N은 총 디바이스 수이고 cps는 초당 연결 수에 대한 서비스 한도입니다.

재시도 작업의 타이밍에 대한 자세한 내용은 재시도 타이밍을 참조하세요.

디바이스 다시 프로비전

다시 프로비전은 이전에 성공적으로 연결한 디바이스를 IoT Hub에 프로비전하는 프로세스입니다. 디바이스가 IoT Hub에 다시 연결해야 하는 이유는 다음과 같이 여러 가지가 있을 수 있습니다.

  • 디바이스는 정전, 네트워크 연결 끊김, 지역 재배치, 펌웨어 업데이트, 공장 초기화 또는 인증서 키 회전으로 인해 다시 부팅될 수 있습니다.
  • 계획되지 않은 IoT Hub 중단으로 인해 IoT Hub 인스턴스를 사용하지 못할 수 있습니다.

디바이스가 다시 부팅될 때마다 프로비전 프로세스를 진행할 필요가 없습니다. 다시 프로비전된 디바이스는 대부분 동일한 IoT Hub에 연결됩니다. 대신 디바이스는 이전에 성공한 연결에서 캐시된 정보를 사용하여 IoT Hub에 직접 연결을 시도해야 합니다.

연결 문자열을 저장할 수 있는 디바이스

초기 프로비전 후 연결 문자열을 저장할 수 있는 디바이스는 다시 부팅한 후 IoT Hub에 직접 다시 연결해야 합니다. 이 패턴에서는 적절한 IoT Hub에 성공적으로 연결하는 동안 대기 시간이 줄어듭니다. 다음과 같은 두 가지 가능한 원인이 있습니다.

  • 디바이스 재부팅 시 연결할 IoT Hub가 이전에 연결된 IoT Hub와 동일합니다.

    캐시에서 검색된 연결 문자열이 제대로 작동해야 하며 디바이스가 동일한 엔드포인트에 다시 연결할 수 있습니다. 프로비전 프로세스를 새로 시작할 필요가 없습니다.

  • 디바이스 재부팅 시 연결할 IoT Hub가 이전에 연결된 IoT Hub와 다릅니다.

    메모리에 저장된 연결 문자열이 정확하지 않습니다. 동일한 엔드포인트에 대한 연결 시도가 실패하므로 IoT Hub 연결에 대한 재시도 메커니즘이 트리거됩니다. IoT Hub 연결 실패에 대한 임계값에 도달하면 재시도 메커니즘이 프로비전 프로세스의 새로운 시작을 자동으로 트리거합니다.

연결 문자열을 저장할 수 없는 디바이스

일부 디바이스에는 과거에 성공한 IoT Hub 연결의 연결 문자열 캐싱을 수용하기에 충분한 공간 또는 메모리가 없습니다. 이러한 디바이스는 다시 부팅한 후 DPS를 통해 다시 프로비전해야 합니다. DPS 등록 API를 사용하여 다시 등록합니다. 분당 재등록 수는 DPS 디바이스 등록 한도에 따라 제한됩니다.

다시 프로비전 샘플

이 섹션의 코드 예제에서는 디바이스 캐시에서 읽고 쓰는 클래스를 보여 줍니다. 그리고 연결 문자열이 발견되면 디바이스를 IoT Hub에 다시 연결하려고 시도하고 그렇지 않은 경우에는 DPS를 통해 다시 프로비전하는 코드를 보여 줍니다.

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

namespace ProvisioningCache
{
  public class ProvisioningDetailsFileStorage : IProvisioningDetailCache
  {
    private string dataDirectory = null;

    public ProvisioningDetailsFileStorage()
    {
      dataDirectory = Environment.GetEnvironmentVariable("ProvisioningDetailsDataDirectory");
    }

    public ProvisioningResponse GetProvisioningDetailResponseFromCache(string registrationId)
    {
      try
        {
          var provisioningResponseFile = File.ReadAllText(Path.Combine(dataDirectory, registrationId));

          ProvisioningResponse response = JsonConvert.DeserializeObject<ProvisioningResponse>(provisioningResponseFile);

          return response;
        }
      catch (Exception ex)
      {
        return null;
      }
    }

    public void SetProvisioningDetailResponse(string registrationId, ProvisioningResponse provisioningDetails)
    {
      var provisioningDetailsJson = JsonConvert.SerializeObject(provisioningDetails);

      File.WriteAllText(Path.Combine(dataDirectory, registrationId), provisioningDetailsJson);
    }
  }
}

다음과 유사한 코드를 사용하여 캐시에 연결 정보가 있는지 확인한 후 디바이스 다시 연결을 진행하는 방법을 결정할 수 있습니다.

IProvisioningDetailCache provisioningDetailCache = new ProvisioningDetailsFileStorage();

var provisioningDetails = provisioningDetailCache.GetProvisioningDetailResponseFromCache(registrationId);

// If no info is available in cache, go through DPS for provisioning
if(provisioningDetails == null)
{
  logger.LogInformation($"Initializing the device provisioning client...");
  using var transport = new ProvisioningTransportHandlerAmqp();
  ProvisioningDeviceClient provClient = ProvisioningDeviceClient.Create(dpsEndpoint, dpsScopeId, security, transport);
  logger.LogInformation($"Initialized for registration Id {security.GetRegistrationID()}.");
  logger.LogInformation("Registering with the device provisioning service... ");

  // This method will attempt to retry in case of a transient fault
  DeviceRegistrationResult result = await registerDevice(provClient);
  provisioningDetails = new ProvisioningResponse() { iotHubHostName = result.AssignedHub, deviceId = result.DeviceId };
  provisioningDetailCache.SetProvisioningDetailResponse(registrationId, provisioningDetails);
}

// If there was IoT Hub info from previous provisioning in the cache, try connecting to the IoT Hub directly
// If trying to connect to the IoT Hub returns status 429, make sure to retry operation honoring
//   the retry-after header
// If trying to connect to the IoT Hub returns a 500-series server error, have an exponential backoff with
//   at least 5 seconds of wait-time
// For all response codes 429 and 5xx, reprovision through DPS
// Ideally, you should also support a method to manually trigger provisioning on demand
if (provisioningDetails != null)
{
  logger.LogInformation($"Device {provisioningDetails.deviceId} registered to {provisioningDetails.iotHubHostName}.");
  logger.LogInformation("Creating TPM authentication for IoT Hub...");
  IAuthenticationMethod auth = new DeviceAuthenticationWithTpm(provisioningDetails.deviceId, security);
  logger.LogInformation($"Testing the provisioned device with IoT Hub...");
  DeviceClient iotClient = DeviceClient.Create(provisioningDetails.iotHubHostName, auth, TransportType.Amqp);
  logger.LogInformation($"Registering the Method Call back for Reprovisioning...");
  await iotClient.SetMethodHandlerAsync("Reprovision",reprovisionDirectMethodCallback, iotClient);

  // Now you should start a thread into this method and do your business while the DeviceClient is still connected
  await startBackgroundWork(iotClient);
  logger.LogInformation("Wait until closed...");

  // Wait until the app unloads or is cancelled
  var cts = new CancellationTokenSource();
  AssemblyLoadContext.Default.Unloading += (ctx) => cts.Cancel();
  Console.CancelKeyPress += (sender, cpe) => cts.Cancel();

  await WhenCancelled(cts.Token);
  await iotClient.CloseAsync();
  Console.WriteLine("Finished.");
}

IoT Hub 연결 관련 고려 사항

모든 단일 IoT 허브는 100만 대의 디바이스와 모듈로 제한됩니다. 100만 대가 넘는 디바이스를 사용하려는 경우 허브당 디바이스 수를 100만 대로 제한하고 배포 규모를 늘릴 때 필요에 따라 허브를 추가합니다. 자세한 내용은 IoT Hub 할당량을 참조하세요. 100만 대가 넘는 디바이스에 대한 플랜이 있고 특정 지역에서 지원해야 하는 경우(예: 데이터 보존 요구 사항을 위해 EU 지역에서 지원해야 하는 경우) 배포하려는 지역에 현재 및 향후 규모를 지원할 수 있는 용량이 있는지 문의할 수 있습니다.

DPS를 통해 IoT Hub에 연결하는 경우 디바이스는 연결할 때 오류 코드에 대한 응답으로 다음 논리를 사용해야 합니다.

  • 500 시리즈 서버 오류 응답을 수신하는 경우 캐시된 자격 증명 또는 디바이스 등록 상태 조회 API 호출 결과를 사용하여 연결을 다시 시도합니다.
  • 401, Unauthorized, 403, Forbidden 또는 404, Not Found가 수신될 경우 DPS 등록 API를 호출하여 전체 다시 등록을 수행합니다.

언제든지 디바이스는 사용자가 시작한 다시 프로비전 명령에 응답할 수 있어야 합니다.

디바이스는 IoT Hub와 연결이 끊어지면 DPS로 돌아가기 전에 15~30분 동안 동일한 IoT Hub에 직접 다시 연결하려고 시도해야 합니다.

DPS를 사용하는 다른 IoT Hub 시나리오:

  • IoT Hub 장애 조치(failover): 연결 정보가 변경되지 않고 허브를 다시 사용할 수 있게 되면 연결을 다시 시도하는 논리가 적용되므로 디바이스는 계속 작동합니다.
  • IoT Hub 변경: 사용자 지정 할당 정책을 사용하여 디바이스를 다른 IoT Hub에 할당해야 합니다.
  • IoT Hub 연결 다시 시도: 적극적인 재시도 전략을 사용하면 안 됩니다. 대신, 다시 시도하기 전에 최소 1분의 간격을 허용합니다.
  • IoT Hub 파티션: 디바이스 전략이 원격 분석에 크게 의존하는 경우 디바이스-클라우드 파티션 수를 늘려야 합니다.

장치 모니터링

전체 배포의 중요한 부분은 시스템이 적절하게 작동하고 있는지 확인하기 위해 솔루션에 대한 엔드투엔드 모니터링을 수행하는 것입니다. IoT 디바이스의 대규모 배포에서 서비스의 상태를 모니터링하는 방법에는 여러 가지가 있습니다. 다음 패턴은 서비스 모니터링에 효과적인 것으로 입증되었습니다.

  • DPS 인스턴스에서 각 등록 그룹을 쿼리하고, 해당 그룹에 등록된 총 디바이스를 가져와서 다양한 등록 그룹의 숫자를 집계하는 애플리케이션을 만듭니다. 이 숫자는 현재 DPS를 통해 등록된 디바이스의 정확한 개수를 제공하며 서비스 상태를 모니터링하는 데 사용할 수 있습니다.
  • 특정 기간 동안 디바이스 등록을 모니터링합니다. 예를 들어 이전 5일 동안의 DPS 인스턴스 등록 속도를 모니터링합니다. 이 접근 방법은 대략적인 수치만 제공하며 특정 기간으로 제한됩니다.

다음 단계