2018 年 12 月

33 卷,第 12

容器-使用 Azure 容器提供按需 R Server

通过Will Stott |2018 年 12 月

在 2018 年 11 月发行的 MSDN 杂志 》,"网站后台处理与 Azure 服务总线队列"文章中 (msdn.com/magazine/mt830371),我介绍了如何使用 Azure 函数和服务总线队列来处理长时间运行后台处理您的网站。在此第二篇文章,我将说明如何使用此类处理来自 Web 应用的流量到达时启动服务器,然后使用服务器的 Web 流量均已停止后自动关闭服务器之前执行分类任务规定的时间。因为它作为 Docker 容器实现为一个黑色框处理服务器。

在本例中服务器的统计语言 R 提供 ultrasound 的质量控制数据在运行 Linux 和逻辑回归分类器开发扫描我研究的一部分为 United Kingdom 协作试用版的卵巢癌症屏蔽 (UKCTOCS).但是,你的服务器,可能会运行 Windows 和 Python,以确定套汇外汇货币交易记录的机会。这并不重要服务器的操作,但前提是它可以构成的 Docker 映像并具有可以访问的 API。

在本文中,才会遇到的关键技术是 Azure.Management.Fluent API,用于以编程方式创建和删除你的服务器的 Azure 容器实例 (ACI)。此外,您将基于早期工作,如之前的文章,以使用计时器触发器,以安排这些操作创建一个 Azure 函数中所述。你将还扩展具有服务总线队列触发器的现有 Azure 函数以便它可以将传递到在 ACI 中运行的分类器已排队的消息引用的输入的数据使用其 OpenCPU API。以这种方式,您将使用 ASP.NET Core 2.1 MVC Web 应用执行有意义的长时间运行后台任务的 Azure 无服务器函数。整个系统的概述所示图 1

系统概述
图 1 系统概述

重新创建特色这篇文章中的项目需要只尚不完善的 Web 和C#开发技能,但假定你已经构建了前一篇文章中所述的 Azure Functions 项目。它还假定你构建了随附的联机资源中所述的简单 web 应用和数据库项目。您可以找到相关资源 PDF,源代码,并生成解决方案中所示的说明图 1github.com/wpqs/MSDNOvaryVis。此外,包含服务器的 R 分类器的 Docker 映像是可以从hub.docker.com作为 r/wpqs/ovaryclassifier。根据工具,你将需要使用.NET Core 2.1 SDK 的 Visual Studio 2017 版本 15.7 和 Web 开发工作负载,以及 v15.0.404240 的 Azure 函数和 web 作业工具。请注意,免费的 Visual Studio Community 版本是可用。您还需要一个 Azure 订阅,但同样,您可以获取所需内容免费如果你是新客户。

Azure 容器实例中运行你的服务器

已预配虚拟机 (VM) 的任何人都非常熟悉的概念从某种形式的文件加载其映像。此映像包含 OS、 设置、 数据、 应用程序软件和其他所需的 VM。Docker,可高效地完成几乎相同的工作,但的详细信息,因为在同一台计算机上运行的映像的实例之间更好地共享资源。

使用 R 统计软件的 Linux 服务器的基础 Docker 映像用于我的研究和 OpenCPU 安装。OpenCPU (opencpu.org) 向我提供了一个 API,因此无法使用通过主计算机的端口 80 的 HTTP 消息传送调用我的 R 的分类器函数。在 OpenCPU 人还还为您提供作为我的服务器的基础提供服务的公共 Docker 映像。我需要做的就是添加 R 函数和我的分类器。

此能够生成 Docker 映像层在层上移和重复使用现有的工作是什么,它为开发人员最具吸引力的重要组成部分。Azure 通过允许你创建 Azure 容器实例 (ACI),这类似于 Docker 容器,因为它是 Docker 映像的实例,为 Docker 提供支持。因此,通过从 Docker 中心映像创建 ACI,Azure 平台上启动的服务器实例并且删除 ACI 时此服务器将停止。

准备工作

在实施之前管理 ACI 所需的代码,它才可进行一些准备工作由安装在前一篇文章中所描述的工作的一部分开发的 Azure 函数应用项目中的几个其他包。此外需要创建此 Azure 函数的安全主体,并更新其应用程序设置。

检查现有的 Azure 函数应用项目和包是一个不错的主意或打开 Visual Studio MSDNOvaryVis 解决方案,从下载后创建时我的上一篇文章中的说明。

要检查使用 NuGet 包管理器的关键包是 Microsoft.EntityFrameworkCore.SqlServer v2.1.2 和 Microsoft.NET.Sdk.Functions v1.0.19。这些包使用本文中,但你可能想要尝试为您自己的工作的更高版本。相关资源 PDF 我已托管在 GitHub 存储库中可以找到有关使用 NuGet 包管理器的详细信息。

添加控制 Azure 容器,只需将消息发送到服务总线队列所需的包,需要从包管理器提供以下命令:

Install-Package Microsoft.Azure.Management.Fluent
  -Project OvaryVisFnApp
  -Version 1.14.0
Install-Package Microsoft.Azure.WebJobs.ServiceBus
   -Project OvaryVisFnApp   -Version 3.0.0-beta8
Install-Package Microsoft.Azure.ServiceBus
  -Project OvaryVisFnApp
  -Version 3.1.0

添加这些包,则意味着需要项目的目标框架设置为 netstandard2.0。

授予它需要创建和删除 Azure 容器实例的颁发机构的 Azure 函数,需要为你的 Azure 订阅创建一个安全主体。在 PowerShell 控制台内置到 Azure 门户网站中, 所示图 2,提供了执行此操作的简单办法使用以下命令,不过,需要使用合适的密码和 MsdnOvaryVis 的 EEE 替换的名称在拥有 Azure 订阅:

az account set --subscription MsdnOvaryVis
$password = ConvertTo-SecureString "EEE" -AsPlainText -Force
$sp = New-AzureRmADServicePrincipal -DisplayName "MSDNOvaryVisApp"
  -Password $password
New-AzureRmRoleAssignment -ServicePrincipalName $sp.ApplicationId
  -RoleDefinitionName Contributor
$sp | Select DisplayName, ApplicationId
Get-AzureRmSubscription -SubscriptionName MsdnOvaryVis

Cloud Shell 中的 Azure 门户的 PowerShell 控制台
图 2 在 Azure 门户的 PowerShell 控制台在 Cloud Shell

到记事本 (或类似) 应该复制到响应和密码以便您可以保存信息。要保留的重要值是你的密码 (EEE) 应用程序 Id 和租户 id。但是,而非硬编码它们到代码中,则最好从你的 Azure 函数应用服务的应用程序设置中引用它们。在生产系统中,您可以考虑使用新的 Azure 托管服务标识 (MSI) 作为创建安全主体,并将其添加到你的 Azure 函数的应用程序设置,如前面所述的替代方法。

正在更新你的 Azure 函数应用的应用程序设置,可避免到你的代码进行硬编码值可更改或敏感值。您可以在 Azure 门户中打开其应用程序设置边栏选项卡,然后再中所示的红色框中添加项图 3。或者,可以通过发出命令从 Cloud Shell 添加设置。例如,假设我函数的应用程序称为 MSDNOvaryVisFnApp 并且位于 resMSDNOvaryVis 资源组中,我可以给出其安全主体 ID,将值设置为 DDD 中,按如下所示:

az functionapp config appsettings set --name MSDNOvaryVisFnApp
  --resource-group resMSDNOvaryVis --settings 'SecPrincipalId=DDD'

图 3 Azure 函数应用服务应用程序设置

可以在 AzureResCmds.txt 文件随本文提供的下载中找到所需应用所需应用程序设置的命令的完整列表。

创建服务器类

若要将一个类添加到你的 Azure 函数应用项目,这样才可以管理你的服务器在一个位置,具体而言所需的所有方法意义:

  • GetAzure 返回初始化与安全主体之前创建的接口。
  • GetContainerGroup 查找现有 ACI 中 Azure 资源运行。
  • IsRunning 返回一个布尔值,以指示你的服务器当前是否正在运行。
  • StartAsync 创建使用指定的 Docker 映像 ACI
  • GetResult Async 的输入的数据传递到你的服务器,并返回结果。
  • StopAsync 删除 ACI。

可以使用 Visual Studio 来添加一个空C#类适用于这项工作选择 OvaryVisFnApp 项目,右键单击并选择添加 |类。这将打开添加新项对话框中使用C#选择类。只需键入作为其文件名的 Server.cs 创建相应的服务器类。让我们通过实现每种方法。

GetAzure 支持更早版本,创建的安全主体中的初始化,使你的代码具有它来管理你的 Azure 订阅的资源所需的权限。按如下所示实现它:

private static IAzure GetAzure(IConfigurationRoot config)
{
  AzureCredentials credentials = SdkContext.AzureCredentialsFactory
    .FromServicePrincipal(config["SecPrincipalId"],
    config["SecPrincipalKey"], config["TenantId"],
    AzureEnvironment.AzureGlobalCloud);
  IAzure rc = Azure.Configure().WithLogLevel(
    HttpLoggingDelegatingHandler.Level.Basic)
    .Authenticate(credentials).WithDefaultSubscription();
  return rc;
}

GetContainerGroup 可以查找已创建在 Azure 中,,因此请勿尝试一个已存在时创建新 ACI 任何容器组。如果找不到,它是只需要列出资源组中当前存在的容器组,并返回以容器名称的一个,指定在应用程序设置中,则为 null。按如下所示实现它:

private static async Task<IContainerGroup>GetContainerGroup(IConfigurationRoot config)
{
  IContainerGroup rc = null;
  var classifierResoureGroup = config["ClassifierResourceGroup"];
  var list = await azure.ContainerGroups.ListByResourceGroupAsync(
    classifierResoureGroup);
  foreach (var container in list){
    if((rc = await GetAzure(config).ContainerGroups
        .GetByResourceGroupAsync(
          classifierResoureGroup,config["ClassifierContainerName"])))
      break;
  }
  return rc;
}

IsRunning 提供了方便地确定你的服务器当前是否正在运行。它还将设置变量 _classifierIP 为服务器的 IP 地址如果运行的。您实现如下所示:

private static string _classifierIP = "";
public static string GetIP() { return _classifierIP; }
public static async Task<bool> IsRunning(IConfigurationRoot config)
{
  _classifierIP = (await GetContainerGroup(config)).IPAddress ?? "";
  return (_classifierIP.Length > 0) ? true : false;
}

StartAsync,可以创建 Azure 容器实例使用 r/wpqs/ovaryclassifier Docker 映像来预配并启动按需的服务器。如中所示的服务器类中实现图 4。你将看到该服务器 TcpPort 80 处于打开入门,以便您可以使用 HTTP 消息传送 OpenCPU API 使用的与其进行通信。您还会看到多个参数传递给方法从 Azure 函数的应用程序设置在顶部的红色框中所示图 3。它们的名称和密码 Docker 帐户、 容器映像和最小数量的处理器内核和内存大小方面 ACI 所需的规范。可以在 Microsoft 文档中找到用于创建 Azure 容器实例的选项的详细信息bit.ly/2CDKrJg

图 4 实现的 Server.StartAsync 可以启动按需的服务器

public static async Task<string> StartAsync(IConfigurationRoot config)
{
  string rc = "server start";
  if (await Server.IsRunning(config) == true )
    rc += " completed, already running";
  else
  {
    var region = config["ClassifierRegion"];
    var resourceGroupName = config["ClassifierResourceGroup"];
    var containerName = config["ClassifierContainerName"];
    var classifierImage = config["ClassifierImage"];
    var cpus = config["ClassifierCpus"];
    var memory = config["ClassifierMemoryGB"];
    var dockeruser = config["DockerHubUserName"];
    var dockerpwd = config["DockerHubPassword"];
    double.TryParse(cpus, out double cpuCount);
    double.TryParse(memory, out double memoryGb);
    var containerGroup =
      await GetAzure(config).ContainerGroups.Define(containerName)
        .WithRegion(Region.Create(region)).WithExistingResourceGroup(
          resourceGroupName)
        .WithLinux().WithPrivateImageRegistry("index.docker.io",
          dockeruser, dockerpwd)
        .WithoutVolume().DefineContainerInstance(containerName)
        .WithImage(classifierImage).WithExternalTcpPort(80)
        .WithCpuCoreCount(cpuCount).WithMemorySizeInGB(memoryGb)
        .Attach().WithRestartPolicy(
          ContainerGroupRestartPolicy.OnFailure).CreateAsync();
      _classifierIP = containerGroup?.IPAddress ?? “”;
    if ((_classifierIP == null) || (_classifierIP.Length == 0))
      rc += " failed";
    else
      rc += " completed ok";
  }
  return rc;
}

GetResultAsync,可在 Azure 函数和服务器之间传递数据。确切的实现将取决于你的服务器已实现其 API 的方式。在这种情况下使用 OpenCPU API 向分类器传递 ovary 维度并获取返回的结果 — ovary 或不将其可视化。此特定 API 旨在允许 HTTP 消息传送与服务器承载 R 统计函数,因此它需要以下类型的方法:

public static async Task<int> GetResultAsync(int D1mm, int D2mm, int D3mm)
{
  int rc = -99;
  var ip = Server.GetIP();
  if (ip != "")
  {
  // Post message to server with content formed
  // from KeyValue pairs from input params
  // Check response and then query the server for the result
  // Process the result to get the return value:
  // 0 is not visualized, 1 is visualized
  }
  return rc;
}

GetResultAsync 的完整实现本文中,在源代码下载中提供,但它不太可能对您有用的除非你的服务器还实现 OpenCPU API。

StopAsync,可删除 ACI,从而停止关联的 Azure 服务器。这实现通过将更多方法添加到服务器类,如下所示:

public static async Task<string> StopAsync(IConfigurationRoot config)
{
  _classifierIP = "";
  IContainerGroup containerGroup = await GetContainerGroup(config);
  if (containerGroup == null)
    return  "server stop completed already";
  else
  {
    await GetAzure(config).ContainerGroups.DeleteByIdAsync(containerGroup.Id);
    return " server stop completed ok";
  }
}

实现具有计时器触发器的 Azure 函数

带定期计时器触发器的 Azure 函数是好的方法来管理服务器上,启动此操作可能需要数分钟才能完成,这可能会导致消息在 Azure 服务总线队列触发器中执行的及时处理函数。上个月的文章介绍了如何创建 Visual Studio 项目需要包含此类函数。如果你遵循与代码一起,您只需选择 OvaryVisFnApp 项目,右键单击以选择新的 Azure Function,然后为 OvaryVisMonitor.cs 指定新文件的名称。这将打开所示的对话框图 5,它可让你选择一个计时器触发器并设置其频率使用 CRON 表达式,在此情况下,每隔一分钟。当单击确定时,关闭该对话框并使用适用于所选内容的运行方法调用 OvaryVisMonitor,类中创建新的 Azure 函数。

新建 Azure 函数对话框
图 5 新建 Azure 函数对话框

创建此类后应以初始化客户端为服务总线队列添加 Run 方法的正上方的以下代码:

private static IQueueClient _queueClient = null;
private static readonly object _accesslock = new object();
private static void SetupServiceBus(string connection, string queueName)
{
  lock (_accesslock) {
    if (_queueClient == null)
      _queueClient = new QueueClient(connection, queueName);
  }
}

可以看到,无论此 SetupServiceBus 方法调用多少次它仅一次创建服务总线队列客户端,且使用锁来保证其线程安全。因此,您可以安全地执行此操作中所示图 6,知道这一点,即使调用 SetupServiceBus 每次你定期计时器的函数运行时,仅在首次调用实例化客户端。

图 6 实现 OvaryVisMonitor 计时器触发 Azure 函数

[FunctionName(“OvaryVisMonitor”)]
public static async Task Run([TimerTrigger(“0 */1 * * * *”)]TimerInfo myTimer,
  TraceWriter log, Microsoft.Azure.WebJobs.ExecutionContext exeContext)
{
  using (Mutex mutex = new Mutex(true, “MSDNOvaryVisMonitorMutuex”, out bool doRun))
  {
    if (doRun == true)
    {
      var config = new ConfigurationBuilder().SetBasePath(
        exeContext?.FunctionAppDirectory)
        .AddJsonFile(“local.settings.json”, optional: true, reloadOnChange: true)
        .AddEnvironmentVariables().Build();
      var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();
      optionsBuilder.UseSqlServer(
        config[“ConnectionStrings:DefaultConnection”]);
      ApplicationDbContext dbContext =
        new ApplicationDbContext(optionsBuilder.Options);
      DateTime expiry = DateTime.UtcNow.AddMinutes(-10);
      var pendingJobs = await dbContext.OvaryVis
        .Where(a => (a.ResultVis == -1) && (a.JobSubmitted > expiry)).ToListAsync();
      if (pendingJobs.Count > 0)
      {
        if (await Server.IsRunning(config) == false)
          await Server.StartAsync(config);
        else
        {
         SetupServiceBus(config[“AzureWebJobsServiceBus”],
                          config[“AzureServiceBusQueueName”]);
          foreach (var job in pendingJobs)
          {
            var message = new Message(Encoding.UTF8.GetBytes(job.Id));
            await _queueClient.SendAsync(message);
          }
        }
      }
      else
      {
        if (await Server.IsRunning(config) == true)
        {
          var recentJobs = await dbContext.OvaryVis
            .Where(a => a.JobSubmitted > expiry).ToListAsync();
          if (recentJobs.Count == 0)
            await Server.StopAsync(config);
        }
      }
    }
  }
}

您应该注意 Run 方法,需要添加 exeContext 参数,如中所示图 6。它用于初始化配置变量,它允许访问 Azure 函数应用程序设置,如前面的准备工作。您还应注意使用互斥体表达式,它将停止正在应运行函数再次调用其前一次调用完成之前重新输入该函数的正文。这可以防止任何尝试创建第二个 Azure 容器实例应你的服务器的启动需要多个一分钟时间。

运行函数的操作取决于获取尚未处理的 OvaryVis 数据库记录的列表 (即,具有其 ResultVis 字段的记录设置为-1)。如果此列表不为空,并且该服务器没有运行,则调用 Server.StartAsync,可阻止将长时间; 运行方法代码的任何进一步进度可能是几分钟时间。Server.StartAsync 返回后,Run 方法完成并释放互斥体。因此,在其下一个调用后, 一个或多个消息将发送到服务总线队列-一个用于 pendingJobs 列表中每个记录。这允许您挂起的作业要处理如果他们已发送的 OvaryVisWebApp,第一篇文章中所述。

计时器函数的最后一部分涉及到在处于非活动状态 10 分钟后停止服务器。您可以看到当 OvaryVis 表中的所有记录是否已处理或不都是旧,至少 10 分钟,该代码调用 Server.StopAsync。

更新使用 OvaryVisSubmitProc Azure 函数的 FormSubmittedProc 方法完成您的代码实现。您只需编辑为前一篇文章中,以便更改 (通过运行调用) 的 FormSubmittedProc 方法,创建的代码中所示图 7

图 7 更新现有的 Azure 函数中的 FormSubmittedProc 方法

private static async Task<string> FormSubmittedProc(IConfigurationRoot config,
  ApplicationDbContext dbContext, string queueItem)
{
  string rc = "FormSubmittedProc: ";
  if (queueItem != null)
  {
    var rec = await dbContext.OvaryVis.SingleOrDefaultAsync(a => a.Id == queueItem);
    if (rec == null)
      rc += string.Format("record not found: Id={0}", queueItem);
    else
    {
      if ((rec.ResultVis != -1) || (rec.ResultVis == -99))
        rc += string.Format("already processed: result={0}", rec.ResultVis);
      else
      {
        if (await Server.IsRunning(config) == false)
          rc += "server not running, wait for job to be resubmitted";
        else
        {
          rec.ResultVis = await Server.GetResultAsync(
            rec.D1mm, rec.D2mm, rec.D3mm);
          if (rec.ResultVis < 0)
            rc += string.Format("server running result={0} - error",
              rec.ResultVis);
          else
          {
            rc += string.Format("server running result={0} - success (ovary {1})",
                  rec.ResultVis, (rec.ResultVis == 1) ? "found" : "not found");
          }
        }
        rec.StatusMsg = rc;
        dbContext.Update(rec);
        await dbContext.SaveChangesAsync();
       }
     }
  }
  return rc;
}

查看中的代码图 7,可以看到,FormSubmittedProc 检查是否正在运行服务器,以及如果是这样,调用 GetResultAsync 分类器中获取的结果。然后更新数据库记录,因此在 ResultVis 字段中设置的分类器结果。0 值表示不进行可视化的 ovary,1 表示 ovary 可视化。因此,可以显示操作的进度,也会更新 StatusMsg 字段每当用户刷新浏览器 (F5)。如果服务器没有运行,使用户知道要等待的时间要重新提交 OvaryVisMonitor 通过前面所述的事件,并显示相应的消息被更新记录的 StatusMsg 字段。

生成和发布更新的 OvaryVisFnApp 项目完成前一篇文章中所述:右键单击,选择发布并单击发布按钮。但是,请确保事先用于在 Azure 门户通过从 Cloud Shell 中,发出以下命令停止你的 Azure Function App 服务,如中所示图 2:

az functionapp stop --name MSDNOvaryVisFnApp
  --resource-group resMSDNOvaryVis

一旦发布您需要重新启动服务,然后在功能上测试它,并且特别注意观看函数服务器应用程序记录,以及检查服务器运行时容器实例将出现在 Azure 资源组边栏选项卡.

可以通过打开浏览器的 web 应用的 URL 显示在 Azure 门户的概述边栏选项卡中执行基本功能测试。主页出现后,请输入一组 ovary 维度,然后单击提交。你将立即重定向到结果页上,其中你会看到与你的提交内容相关的数据库记录的值,如左侧和右侧的中所示图 8。重复按 F5 将导致页后,可以使用此记录的当前值更新。最终的这些 ovary 维度的分类器的评估结果将显示,如右侧所示图 8。如果服务器已在运行,这将需要仅在几秒钟,否则它可能要花几分钟时间。10 分钟后处于不活动状态,在网站上,服务器将自动关闭,演示其实现的按需性质。

功能测试
图 8 功能测试

总结

此处开发网站的功能进行了一些无关紧要。它只是为了生成二进制响应收集三个维度从窗体: ovary 或不将其可视化。但是,其实现是与普通相差甚远。在上一篇文章中,我介绍了如何实现一种方法处理使用 Azure 服务总线队列和 Azure Function App 服务在后台中的数据。在本文中,我已扩展此后台处理机制来创建 ACI 从 Docker Hub 上发布 Docker 映像。此映像包含托管 R 和自定义逻辑回归分类器,以及 OpenCPU API 允许使用 HTTP 消息传送与服务器通信的 Linux 服务器。更重要的是,我实现的逻辑来创建此 ACI 的按需启动服务器只在需要时处于不活动状态 10 分钟后关闭它。

您可以轻松地提高通过添加 Azure SignalR 服务来自动更新结果 Web 页面而不是依靠用户定期按 f5 键刷新此处介绍的系统。此外可以考虑扩展,以便时网站上出现高负载情况下,会创建多个 ACIs 处理消息。但是,此额外的复杂性并不保证演示如何实现成本只为每个月 (相比 50 美元月可能支付虚拟服务器运行 7 月 24 日) 1.00 美元的服务器的项目。即使使用的数据库服务成本,您正在寻找支付不到为每个月 5.00 美元。这是考虑您要采用的该技术的正确值,但更重要的是,它是实际的问题,可以轻松生成可靠的解决方案。


Dr.Will Stott有超过 25 年的工作范围广泛的英国和欧洲地区的公司的合同工/顾问的经验。过去 10 年里博士的大部分时间已花费 UCL 在对卵巢癌症屏蔽的研究。Dr.Stott 曾在很多会议和在各种日志,以及本书中发布的文章作者"Visual Studio Team System:Better Software Development for Agile Teams”(Visual Studio Team System:帮助敏捷团队更好地开发软件)(Addison-Wesley Professional,2007 年)一书的作者。

衷心感谢以下 Microsoft 技术专家对本文的审阅:Srikantan Sankaran


在 MSDN 杂志论坛讨论这篇文章