你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

大规模 IoT 设备部署的最佳做法

将 IoT 解决方案扩展到数百万台设备可能具有挑战性。 大规模解决方案通常需要根据服务和订阅限制来设计。 当客户使用 Azure IoT 设备预配服务时,他们会将其与其他 Azure IoT 平台服务和组件(例如 IoT 中心和 Azure IoT 设备 SDK)结合使用。 本文介绍了可融入到设计中的最佳做法、模式和示例代码,以利用这些服务并允许部署横向扩展。通过从项目设计阶段开始遵循这些模式和做法,可以最大程度地提高 IoT 设备的性能。

预配新设备

首次预配是作为 IoT 解决方案的一部分首次加入设备的过程。 在进行大规模部署时,请务必计划预配过程,以避免由于所有设备同时尝试连接而导致的过载情况。

使用交错预配计划

对于百万规模的设备部署,一次注册所有设备可能会导致 DPS 实例由于限制(HTTP 响应代码 429, Too Many Requests)而不能正常工作,以及注册设备失败。 若要防止此类限制,请对设备使用交错注册计划。 根据 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_msecmax_retry_delay_msec 之间)。 使用以下变量计算最大重试延迟:

  • <load> 是一个可配置因素,其值 > 0,指示负载将以平均负载时间乘以每秒连接数表示
  • <T> 是冷启动设备的绝对最短时间(计算方式为 T = N / cps,其中 N 是设备总数,cps 是每秒连接数的服务限制)。

有关重试操作计时的详细信息,请参阅重试计时

重新预配设备

重新预配是设备在以前成功连接后需要预配到 IoT 中心的过程。 有多种原因可能会导致设备需要重新连接到 IoT 中心,例如:

  • 设备可能会因停电、网络连接中断、地理位置迁移、固件更新、出厂重置或证书密钥轮换而重新启动。
  • IoT 中心实例可能会由于计划外 IoT 中心服务中断而不可用。

无需在每次设备重新启动时都执行预配过程。 重新预配的大多数设备最终都会连接到同一个 IoT 中心。 设备应尝试使用从先前的成功连接中缓存的信息直接连接到其 IoT 中心。

可以存储连接字符串的设备

在初始预配后能够存储其连接字符串的设备应该这样做,并在重新启动后尝试直接重新连接到 IoT 中心。 此模式减少了成功连接到相应 IoT 中心时的延迟。 这里有两种可能的情况:

  • 设备重启时要连接的 IoT 中心就是以前连接的 IoT 中心。

    从缓存中检索的连接字符串应正常起作用,设备可以重新连接到同一终结点。 无需重新启动预配过程。

  • 设备重启时要连接的 IoT 中心不是以前连接的 IoT 中心。

    内存中存储的连接字符串不正确。 尝试连接到同一终结点不会成功,因此会触发 IoT 中心连接的重试机制。 达到 IoT 中心连接失败的阈值后,重试机制会自动触发重新启动预配过程。

无法存储连接字符串的设备

一些设备没有足够的空间或内存来容纳来自过去成功 IoT 中心连接的连接字符串的缓存。 重新启动后,这些设备需要通过 DPS 重新预配。 使用 DPS 注册 API 重新注册。 请记住,根据 DPS 设备注册限制,每分钟重新注册次数是受限的。

重新预配示例

本节中的代码示例显示了一个用于在设备缓存中读取和写入信息的类,该类后跟一段代码,用于在找到连接字符串时尝试将设备重新连接到 IoT 中心,并在未找到连接字符串时通过 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 中心连接注意事项

任何单个 IoT 中心都限制为 100 万台设备和模块。 如果计划拥有 100 万台以上的设备,请将每个中心的设备数限制为 100 万台,并在增大部署规模时根据需要添加中心。 有关详细信息,请参阅 IoT 中心配额。 如果你具有针对 100 万台以上的设备的计划,并且需要在特定区域为它们提供支持(例如在欧洲区域为它们提供支持以满足数据驻留要求),可以与我们联系,以确保要部署到的区域有能力支持你当前和未来的规模。

通过 DPS 连接到 IoT 中心时,设备应在连接时使用以下逻辑来响应错误代码:

  • 收到任何 500 系列的服务器错误响应时,请使用缓存的凭据或设备注册状态查找 API 调用的结果重试连接。
  • 收到 401, Unauthorized403, Forbidden404, Not Found 时,通过调用 DPS 注册 API 执行完全重新注册。

设备应随时能够响应用户启动的重新预配命令。

如果设备与 IoT 中心断开连接,设备应尝试直接重新连接到同一个 IoT 中心 15-30 分钟,然后再尝试返回到 DPS。

使用 DPS 时的其他 IoT 中心情况:

  • IoT 中心故障转移:设备应继续工作,因为连接信息不会更改,并且逻辑已到位,以在该中心再次可用后重试连接。
  • IoT 中心更改:应使用自定义分配策略将设备分配到其他 IoT 中心。
  • 重试 IoT 中心连接:不应使用主动重试策略。 相反,请在重试前至少留出一分钟。
  • IoT 中心分区:如果设备策略严重依赖于遥测,则应增加设备到云分区的数量。

监控设备

整体部署的一个重要部分是端到端地监视解决方案,以确保系统正常运行。 可通过多种方式监视服务的运行状况,以便大规模部署 IoT 设备。 以下模式已被证明在监视服务方面是有效的:

  • 创建应用程序以查询 DPS 实例上的每个注册组,获取注册到该组的全部设备,然后聚合来自各个注册组的数字。 此数字提供当前通过 DPS 注册的设备的确切计数,可用于监视服务的状态。
  • 监视特定时段内的设备注册。 例如,监视 DPS 实例在前五天的注册率。 请注意,此方法仅提供近似数字,并且还限制为一个时间段。

后续步骤