八、使用 Docker 和 Azure Kubernetes 服务的负载平衡订单处理微服务

不久前,当浏览招聘广告时,术语 n 层架构会突然出现,这是潜在求职者需要熟悉的东西。这种架构范例背后的原则是,有一个数据存储,通常保存在公司拥有的服务器上;然后,有一个服务(或几个服务)会在客户端应用或另一个服务的要求下询问该数据,您会有一个客户端应用(桌面或 web 客户端)与该服务通信。该体系结构通常如下所示:

术语 n 层取代了三层,因为该图中的服务实际上可以调用另一个服务,因此在客户端和服务器之间可能存在几层。

这种方法有很多优点:您可以将用户界面和业务逻辑分开,并且它可以(如果做得正确的话)与数据存储无关(或者至少不可撤销地绑定到数据存储)。您的所有代码都可以存在于一个解决方案中(如果您正在使用。你知道数据库中的任何东西都是任何给定时间的情况。

然而,这种架构方法也有不利的一面,这与某种规模以及可靠性有关。

第一个是使用:如果你增加流量,那么在某一点上,你的架构不能处理那个级别的流量。

我们前面提到的数据存储有一个定义的限制,因为最终它是读写磁盘。这是可以改进的:数据可以分布在多个磁盘上,这大大提高了性能。还有一个问题是,当数据库中的数据被更新时,记录必须被锁定,最后,插入新记录、更新索引等需要花费物理时间。

该服务有一个类似的问题:它一次可以接收的流量是有限制的,尤其是当它与数据库通信并执行事务时。当然,您可以增加服务的数量,但是它们都在争夺同一个数据存储。

下一个规模问题是开发问题:想象一下,您希望同时处理应用的几个部分。当然,有了现代源代码控制,这在机械上是非常容易的;但是,您的销售订单系统可能依赖于您的库存检查系统。您可能会陷入销售订单团队无法取得进展的情况,因为库存检查团队仍在处理给定的功能。

此外,如果你想更新这些系统中的一个,那么你需要测试它们并同时改变它们。这意味着对于每个部署,系统的每个部分都会改变(可能有些部分只是因为重新编译而改变,但它们确实会改变)。

那么,解决办法是什么?很多人最近一直在推广分布式系统的想法(至少在撰写本文时是这样)。这个想法是将整个系统分解成松散耦合的部分或服务。系统的各个部分通常通过某种形式的消息传递进行通信。

For the purpose of this chapter, we're going to work on the premise that our system is being designed for a large DIY store chain.

在本章中,我们将探索使用 Azure Kubernetes 服务来扩展和协调服务的微服务的使用。我们将涵盖以下主题:

  • 在 Azure 中创建存储队列
  • 创建新服务并将其部署到 Azure 容器注册中心 ( ACR )然后部署到 Azure Kubernetes 服务 ( AKS )
  • 通过杀死一个吊舱来证明系统是负载平衡的
  • 使用 JMeter 在负载下测试我们的服务

微服务和容器是当下的流行语:

技术要求

我们将使用 Azure Kubernetes 服务 ( AKS ),所以您首先需要的是 Azure CLI。可以去https://aka.ms/installazurecliwindows安装。

您还需要一个 Azure 帐户;如果你没有,那么你可以在这里创建一个:https://azure.microsoft.com/

在写这篇文章的时候,你可以免费注册,第一个月可以获得 150/$200 的积分。

注册后,请访问 https://portal.azure.com。在这里,您可以管理您的 Azure 资源并检查您的余额。

测试工具

这是一个可选步骤,稍后我将解释如何跳过这一步。如果您选择跳过这一部分,那么您也可以跳到这一部分的结尾。

为了在负载下测试我们的微服务,我们将使用一个名为 JMeter 的工具;可以从http://jmeter.apache.org/download_jmeter.cgi下载。

此外,您还需要安装 Java SE 开发工具包;你可以在这里这样做:http://www . Oracle . com/tech network/Java/javase/downloads/index . html

安装 JRE 超出了本章的范围,尽管有许多关于这方面的在线教程;如果安装正确,您应该能够在命令提示符下键入以下内容,并查看您安装的版本:

java -version

微服务

很多年前,我曾在一个电子数据交换 ( EDI )系统上工作。该系统由销售订单系统工作,将当天的销售汇总写入文本文件;然后,另一个进程将获取该文本文件并处理这些订单。本质上,这是一个分布式系统:系统的每个部分都独立于其他部分运行。销售订单系统可以关闭,它已经写的任何文件仍然会被处理。

如今,这种数据交换形式已经转变为使用消息代理:要么在内部维护,要么在云中维护。稍后我会解释为什么会这样。

At the time of writing, RabbitMQ and ActiveMQ are two well-known message brokers that are typically run on-premises. All of the big cloud providers offer a message broker of one form or another, including Azure, Google, and AWS (which use ActiveMQ).

微服务只是一种分布式系统,它们非常适合使用容器。想法是将最小级别的功能封装在一个独立的流程中。这里的关键是流程是独立的——虽然似乎没有一个正式的文档来声明什么是微服务,但是如果您只是有一个直接依赖于另一个流程的流程,那么称某个东西为微服务是错误的。

It's worth considering the term dependency here. In my EDI example, clearly, a dependency exists in a logical process sense. The test here is whether you can turn one of these processes off and the other(s) continue (obviously with limited effect).

我们已经介绍了创建分布式系统对我们有什么好处,但是让我们考虑一下这种方法的缺点,因为有很多缺点:除非您需要一个,否则您绝对不应该创建分布式系统。我在这里整理了一个(非详尽的)负面清单(每个负面清单都可以在一定程度上减轻,但这个清单的目的是说明这不是一个万灵药):

  • 数据完整性:在一个只有单一数据存储的系统中,你可以简单地依靠你选择的 DBMS 的事务模型来确保,例如,当你创建一个销售订单时,你的库存会减少。在真正分布式的系统中,这些可能是独立的服务,您不能将它们作为单个事务来执行;这样做的最终结果是,你不能保证在任何特定的时间,你的数据是正确的。
  • 系统复杂性:一旦你开始创建许多服务,你会发现你有一个逻辑依赖的网络(类似于你可能在一个单片或 n 层系统中的物理依赖,正如我们之前提到的)。

  • 速度:将一组数据写入数据库的最快方法是在单个事务中完成。一旦在这个过程中引入服务总线,它的速度就会大大降低。

  • 维护:虽然使用这种架构风格的优点之一是可以独立更新单个组件,但也是缺点之一。如果你有 50 个不同的组件,那么你需要跟踪 50 个不同的版本。

在本章中,我们将执行以下操作:

  • 创建一个非常简单的销售订单处理系统。不会有前端,但我们会给它一个销售订单列表。
  • 将我们的系统包装在 Docker 容器中,并将其添加到Azure Kubernetes Service(AKS上托管的 Kubernetes 集群中。
  • 证明即使我们可能会杀死该服务的一个实例,或者它可能会崩溃,Kubernetes 也可以自我修复,我们的订单将继续被处理。

我提到了 Docker 和 Kubernetes,但是这些技术到底是什么?从探索 Docker 开始最容易。

探索码头工人

Docker 是一个容器引擎:这意味着您可以将流程(可执行文件)的所有依赖项封装到一个实体中。我经常认为它有助于分析以容器结束(或目前已经到达)的旅程。

曾经,在微软开发界(之前.NET),我们将编写一个仍然依赖于 dll 的应用。Windows 提供了一个注册表,告诉程序 DLL 将在哪里。当产品发货的时候,您必须创建一个安装程序,将您的依赖项添加到注册表中;然而,因为所有的动态链接库都集中保存,所以您就遇到了一个问题,即另一个程序可能依赖于同一个动态链接库,但是该动态链接库的版本不同。不用说,这导致了数不清的问题,我们根本不知道现场运行的是什么代码,也不相信会安装任何东西。注册表清理器和安装工具如雨后春笋般出现在左侧、右侧和中央,最终微软脱离了注册表模式。

什么时候.NET 发布后,有两种方法可以发布您的依赖项:您可以将它们安装到全局程序集缓存 ( 广汽)中,或者将它们放在与可执行文件相同的目录中。广汽也有自己的问题(它非常接近注册管理机构模型),但是打包了我需要的依赖项,并且使用非常有效;但是,当您想要使用系统依赖项时会发生什么.NET 框架为例)?微软还是很好的,他们允许你把这个添加到一个安装程序中,但是我们又回到了原则上,我们拿了一个软件,在一个不同于测试环境的环境中运行它。例如,您针对哪个版本的 SQL Server 编写数据访问,目标计算机上的版本是否是这个版本?

开始出现的是创建一个虚拟机,然后将整个虚拟机复制到目的地的原理;这样,您可以绝对确定正在运行的是什么,因为它本质上是在同一台机器上进行的测试。这是一个很好的解决方案,但它(可以说)走得太远了。现在有许多虚拟机的操作系统和许可证需要更新和维护。需要的是一个应用的容器,您可以在其中存储应用及其依赖关系,但是整个事情需要从操作系统中抽象出来。

这就是 Docker 的本质——一个这样的容器。

忽必烈与管弦乐团

实际上,Kubernetes 是在 2016 年左右问世的。这是受内部谷歌产品的启发,他们已经在内部使用了几年。为了说明 Kubernetes 允许我们做的事情,我邀请你走过一个小场景。

你在超市排队。不幸的是,收银机很有问题,而且经常坏。有一个在超市工作的人,他的工作就是让队伍保持畅通:他的名字叫温斯顿,所以每次钱柜坏了,他都会把队伍中的下一个人重定向到另一个钱柜,修好钱柜,然后再打开它。

突然,队列变得很大:20 万人一起排队。温斯顿得到指示,如果发生这种情况,他应该“横向扩展”,因此他很快又建立了 500 台收银机来应对压力;这需要几分钟的时间,但是收银机很快上线,队列再次流动:

Kubernetes 充当我们的温斯顿:它可以创建新的容器,升级现有的容器而不会停机,并路由流量。

创建我们的微服务

我们应该考虑我们的微服务是什么,它不是什么:这里最重要的是服务需要是自治的。如果我们创建了一个需要其他服务才能运行的服务,那么它就不是微服务。

We shouldn't confuse this with creating a service that requires other services to function. For example, our sales order processing system must have sales orders, and these come from an external source. This is fine because we're not dependent on that external source: if we don't get a list of sales orders, then our service will still run; it just won't do anything.

让我们从创建新服务开始!

Remember that what we mean here by service is a process that performs a task: it should not be confused with specific usages of the word service, such as a REST service. What we want is simply an independent process.

请记住,像我们忙碌的超市员工温斯顿一样,Kubernetes 可以通过复制容器来扩展您的应用。您只能复制一个容器或进程,如果该进程的编写方式允许的话。这对我们来说意味着,我们需要使用一种机制与我们的服务进行通信,该机制允许多个进程从单个队列中读取,这需要维护。

如果我们回到涉及超级超市雇员温斯顿的例子,队列中的每个人只有在被叫时才接近收银台;如果他们都试图一起接近钱柜,那么钱柜操作员将无法为他们服务。或者,设想这样一种情况,收银员来到队列,扫描前面那个人的物品,然后把那个人留在那里:下一个可用的收银员也可以这样做。温斯顿创造新的收银机只有在有一个机制让顾客以有序的方式接近收银机时才起作用。

我们的容器也是如此:如果你用错误的方式设计了一个微服务,那么它就不能被扩展。

行列

为了避免这种情况,我们将使用 Azure 服务总线为我们进入系统的提要提供一个队列;因为我们不需要保证交付顺序,所以我们可以使用存储队列,而不是服务总线队列。

It's worth bearing in mind that, in the case of sales order processing, the order that processes the messages is probably not too important; however, this is not always the case. You should carefully consider whether the order of delivery is critical in your own case.

让我们从配置队列开始:

  1. 我们需要做的第一件事是创建一个新的存储帐户。在 Azure 门户中,从菜单中选择存储帐户,或简单地搜索。创建存储帐户刀片出现后,选择添加新存储帐户的选项:

  1. 选择“队列”后,您将看到一个当前已设置的队列列表。最初这里很孤独,所以让我们创建一个新队列:

  1. 您将被要求在此处命名您的队列:

  1. 创建新队列后,记下网址,因为我们稍后会用到它:

  1. 接下来,导航到访问键并复制连接字符串(我们稍后还会用到):

我们已经配置了队列,现在是时候研究如何模拟销售订单的创建了。

销售订单生成器

因为我们没有一个有成千上万订单的网站,我们将创建一个新的流程来生成新的销售订单消息。然后,我们将使用一个名为 JMeter 的工具来调用这个过程足够多次,以模拟一个重负载。让我们首先构建应用来创建新消息;第一步是创建一个新的.NET Core 3 控制台应用。

If you have decided to not use JMeter, then follow through anyway, and I'll explain which sections you should skip.

因为我们要对存储队列进行读写,所以我们将把代码放在一个单独的帮助程序库中。现在,我们将简单地编写代码,就像助手存在一样。让我们从控制台的主要方法开始:

static void Main(string[] args)
{
    // How many to create?
    int salesOrderCount = int.Parse(args[0]);

    // Set-up Helpers and Dependencies
    var serviceBusHelper = new ServiceBusHelper();
    var productRepository = new ProductRepository();
    var productService = new ProductService(productRepository);
    var salesOrderRepository = new SalesOrderRepository();

    // Process sales orders - will run forever
    var generateSalesOrders = new GenerateSalesOrders(serviceBusHelper, productService);
    generateSalesOrders.Run(salesOrderCount);
}

如您所见,我们遵循手动构建依赖关系的标准,而不是使用第三方库。

public class GenerateSalesOrders
{
    private readonly IServiceBusHelper _serviceBusHelper; 
    private readonly IProductService _productService;
    private Random _rnd = new Random();
    public GenerateSalesOrders(IServiceBusHelper serviceBusHelper, IProductService productService)
    {
        _serviceBusHelper = serviceBusHelper; 
        _productService = productService;
    }
}

我们只是在这里接受一些基本的依赖关系:serviceBusHelperproductService。我们将很快讨论这些课程的细节。

For now, I'd like to draw your attention to the _rnd variable. Outside of writing games, it's rare that you'll find random numbers in code. I felt that it made sense here because we're generating example data. Clearly, any test that uses a random number doesn't adhere to FIRST testing principles.

课程的下一部分是Run方法:

public void Run(int salesOrderCount)
{ 
    for (int i = 0; i < salesOrderCount; i++)
    {
        var newOrder = CreateSalesOrder();
        _serviceBusHelper.SendToSalesOrderMessageQueue(newOrder);
    }
}

这只是调用一个方法来创建销售订单,然后使用服务总线助手将消息放入队列。我们将在下一节回到服务总线,但是现在,让我们看一下CreateSalesOrder()方法,看看它是做什么的:

private SalesOrder.Models.SalesOrder CreateSalesOrder()
{
    var products = _productService.GetProductData();
    var product = products.ElementAt(_rnd.Next(products.Count() - 1));
    var salesOrder = new SalesOrder.Models.SalesOrder()
    {
        ProductCode = product.ProductCode,
        UnitPrice = product.UnitPrice,
        Quantity = _rnd.Next(1, 5)
    };

    return salesOrder;
}

这个方法还有更多,但本质上我们所做的就是调用产品服务来检索产品列表,然后随机挑选一个。最后,我们创建一个随机数量的销售订单。

产品服务从产品存储库中读取产品列表;为了完整起见,我将在这里列出产品服务及其接口。但是,因为它只调用产品存储库,所以我不会对代码做任何进一步的解释:

public interface IProductService
{
    IEnumerable<Models.Product> GetProductData();
}

具体实现如下:

public class ProductService : IProductService
{
    private readonly IProductRepository _productRepository;
    public ProductService(IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }

    public IEnumerable<Models.Product> GetProductData()
    {
        return _productRepository.GetProductData();
    }
}

同样,我将列出存储库接口,但它不保证任何解释:

public interface IProductRepository
{
    IList<SalesOrder.Models.Product> GetProductData();
}

这里有趣的部分是在实现中。但是,在深入讨论之前,我将向您展示这些组件在我的解决方案中的位置,以及我将用于产品数据的数据:

ProductList.csv文件作为内容文件包含在内。我不会在这里列出全部内容,但是如果你想看,我会鼓励你从 GitHub 存储库中下载它。不过,我会列出前几行,让你感受一下文件的格式(毕竟,我相信你和我一样有能力编造产品代码和价格!).该文件应采用以下格式:

SCREW025MMX50,1.72
HARDWOODSQMT,0.45
SLEDGEHAMMER,34.56

产品存储库方法如下所示:

public IList<SalesOrder.Models.Product> GetProductData()
{
    string data = File.ReadAllText($"img/ProductList.csv");

    var products = new List<Models.Product>();
    string[] productLines = data.Split(Environment.NewLine); 
    foreach(var productLine in productLines)
    {
        string[] productData = productLine.Split(",");
        var product = new Models.Product()
        {
            ProductCode = productData[0],
            UnitPrice = decimal.Parse(productData[1])
        };

        products.Add(product);
    }

    return products;
}

这个文件确实有很多内容,从调用从数据文件中读取所有文本开始。首先,我们把它分成几行,然后依次用逗号隔开。最后,我们将创建一个新的产品类对象,并读取其中的数据。

这段代码不是很有弹性:你应该能够很容易地用一些格式不良的数据打破它。如果您选择扩展这个项目,您可能希望从这里开始,或者通过添加一些防御检查来改进代码,或者甚至将数据移动到数据库中;对于任何严重的延期,建议后者。

Should you wish to improve the reliability of the code, you may choose to use the following NuGet package, which I created: https://www.nuget.org/packages/Castr/.

现在我们已经完成了服务和存储库,让我们来看看服务总线助手。

服务总线帮助程序库

正如您在前面部分看到的,我们指的是服务总线助手。对于生成例程,我们在服务总线帮助器中只有一个方法。我强烈建议您将这段代码放在自己的项目中:这样,您就可以将对 Azure 的依赖限制在一个项目中。以下是我目前的解决方案概述:

如您所见,我们已经分离了服务总线交互。让我们看看目前为止使用的界面:

public interface IServiceBusHelper
{
    Task SendToSalesOrderMessageQueue(SalesOrder.Models.SalesOrder salesOrderData);
}

在我们的新项目中,我们需要引用一个微软 NuGet 库:

Install-Package WindowsAzure.Storage

一旦我们做到了这一点,我们就可以实现我们的助手方法:

public async Task SendToSalesOrderMessageQueue(Models.SalesOrder salesOrderData)
{
    CloudStorageAccount storageAccount = CloudStorageAccount.Parse("DefaultEndpointsProtocol=https;AccountName=salesorderqueue;AccountKey=...;EndpointSuffix=core.windows.net");

    CloudQueueClient queueClient = storageAccount.CreateCloudQueueClient();
    CloudQueue queue = queueClient.GetQueueReference("salesorder");
    CloudQueueMessage message = new CloudQueueMessage(JsonConvert.SerializeObject(salesOrderData));

    await queue.AddMessageAsync(message);
}

Before we delve into an explanation of what this is doing, note that the string that's passed to CloudStorageAccount.Parse() is the connection string that we made a note of earlier. As you can see, I've put the connection string directly into the code. Needless to say, this is bad practice; however, as in previous chapters, I feel that it illustrates, the software that we are actually writing better, and more directly.

那么,这段代码在做什么?首先,我们创建一个CloudStorageAccount实例;这有效地连接到 Azure 存储帐户,并作为我们未来通话的基础。接下来,我们需要一个CloudQueueClient,然后使用GetQueueReference获取对队列本身的引用。最后,我们通过简单地序列化类数据并将其添加到队列中来构建一条新消息。

A message should contain all the information that's necessary to deal with that message. You should think carefully about the size of your message; however, on storage queues, you have a limit of 200 GB!

测试我们的销售订单生成器和 JMeter

在我们继续之前,让我们给我们的发电机做一个快速测试。有两种方法可以做到这一点;第一种方法是在命令行中编译和运行软件,但是您也可以简单地在项目属性中提供一个参数:

如您所见,我选择创建一个订单。最好先尝试创建一个单独的项目,只是为了检查它是否正确运行。一旦你知道这个有效,你可以把它提高到,比如说,10 或 20。

你怎么知道它有效?有益的是,微软有存储资源管理器应用,你可以免费下载;你可以在这里找到它:https://azure . Microsoft . com/en-us/features/storage-explorer/。

此外,在撰写本文时,微软已经在门户中发布了相同工具的预览版:

无论您决定以哪种方式查看队列,一旦您运行了销售订单生成器,您应该会在队列中看到一条消息(以下截图来自桌面版本):

尝试运行该软件 10 条甚至 100 条消息,只是为了检查它是否如预期那样生成消息。

我们的下一步将是从 JMeter 调用我们的程序,这样我们就可以模拟同时下许多订单。

测试工具

要模拟高负载,一种选择是简单地编译应用并一起运行它的几个实例。这就是 JMeter 这样的工具为你做的:我在技术需求 s 部分提到 JMeter 是可选的;如果您不想使用 JMeter,那么只需运行应用几次,将数字设置为 100 左右。以下 DOS 命令将为您完成此操作:

for /l %x in (1, 1, 10) do start SalesOrder.Generate.exe 10

在这种情况下,我们调用Generate例程 10 次。这将几乎立即向队列中添加 100 条消息。您将在 GitHub 存储库中找到一个执行相同操作的批处理脚本。如果您不希望使用 JMeter,请直接跳到下一部分,标题为登录

回到 JMeter,假设您已经成功安装了 JDK(参见技术要求部分),您应该能够简单地将 ZIP 文件提取到本地驱动器的某个地方并运行jmeter.bat。您可以在提取的以下子目录中找到这个:apache-jmeter-5.1\bin

如果您已成功运行该软件,您将看到类似如下的屏幕:

It's worth bearing in mind that, depending on when you are reading this, the user interface for JMeter may have changed.

第一步是添加线程组:

线程组是一种表示执行操作的一组用户的方式。让我们设置 10 个用户,每个用户将执行我们的世代应用 10 次。

10 users running the app 10 times and the app generating 10 orders for each invocation means that 1,000 orders will be generated.

下面是我们如何在线程屏幕上配置前面的场景:

最后,我们将添加一个新的采样器来调用我们的可执行文件:

一旦我们添加了它,我们可以要求它从新的采样器调用我们的可执行文件,我们就完成了:

我们准备好出发了。只需按下播放按钮。10 个用户应该立即开始以 10 节的速度下单!

记录

在我们取得任何进展之前,我们应该考虑记录。使用单个应用,日志记录是一个非常简单的过程——您将所有内容记录到一个日志文件中(通常位于服务器上),并且由于所有调用都是针对该单个服务器上的服务,所以所有内容都记录在一个地方。在我们迁移到微服务时产生的其他问题中,我们会产生一个日志问题。到底是哪个服务在记录日志,它记录到哪里?在容器系统中,您不能简单地登录到本地文件系统,因为它不一定是持久的。

创建聚合日志系统远远超出了本章的范围;但是,如果您希望这样做,我强烈建议您查看主要云提供商的一些产品。微软的应用洞察提供了日志功能。

另一种选择是简单地创建一个日志队列,并将所有日志消息写入一个队列(然后,您可以选择编写一个使用者,而不是将其转换为一个文件)。请记住,您可能会同时写入多个日志消息,因此您应该有一种方法来分离这些进程。

创建新的微服务

现在我们有了一个队列和一些数据,我们可以创建新的微服务。

Our microservice will be technology-agnostic. It will be written in .NET Core 3.0 and it will be running inside a Linux container; however, it will not have any hard dependencies on any external hosting, so we should be able to take it out of AKS and drop it into Google-hosted Kubernetes. If you are developing microservices (or anything), you should ask yourself why you are using the technology (and architecture) that you've chosen. An answer such as to gain exposure to that technology is an acceptable reason if it's a personal project, but if you are being paid to produce a piece of software, saying this may not be acceptable to the person paying you. Kubernetes is an excellent container orchestrator, but bear in mind that if you simply write an Azure WebJob or function, you may get all of the benefits that you need with less of the overhead.

我们的服务本身非常简单。我们只需监控队列,在发现消息的地方,我们会对其进行处理。显然,一个完整的、功能齐全的销售订单处理系统对于单个章节来说代码太多了;但是,我们将产生一个完整的流程,该流程将执行以下操作:

  • 向数据库添加新的销售订单
  • 将消息添加到另一个队列,以便调度系统接收

您可能已经从架构中意识到这个过程是一个脱节的过程;也就是说,当用户下订单时,检查是否有足够的库存来完成订单的过程会离线进行。这是有意为之的:这是像这样的系统如何处理突然激增的流量;然而,这意味着你必须重新思考你的用户体验。假设用户已经下了订单,然后在未来的某个时候,订单被拒绝。有办法处理这个;例如,如果库存是您唯一的潜在问题,您可以简单地将重新进货所需的时间计入提前期,这样每个订单都有 10 天的交货窗口,但大多数订单都在 2 天内交货。简而言之,这是一个业务逻辑问题,但也许这是一个更传统的架构所没有的问题。

现在,我们必须创建我们的微服务。这个的逻辑其实比较简单;该服务应执行以下操作:

  • 检查销售订单队列中的新请求;如果没有请求,那么服务应该在一段时间内再次检查(我们将进行 1 分钟)
  • 在发现消息的地方,服务应该将销售订单添加到数据库中
  • 服务应该在一个新的队列中放置一条消息,表明订单成功

Clearly, this flow is abridged, but it is fully functional in and of itself.

创建我们新的 Docker 容器

让我们从创建一个新的控制台应用开始;我们将在一个 Linux 容器中编译并运行它。控制台应用的创建与任何其他控制台应用完全一样,尽管因为我们处理的是 Linux,所以命名必须是小写的,不能包含点。这使得我们的解决方案看起来有些混乱,但是,让我们创建salesorder-process:

我们的下一步是添加 Docker 支持。在这里,您可以简单地手动创建一个 Docker 文件,但是 Visual Studio 很乐意帮助我们。通过右键单击项目,现在有一个添加| Docker 支持选项:

您可以选择创建一个窗口或 Linux 容器;我们将选择 Linux。这将为您创建您的 Dockerfile。

Docker for Windows either runs Linux or Windows containers, but at the time of writing, it cannot run both (you can switch). It looks like Microsoft is going all-in on Linux containers, so my advice would be going for Linux and staying with them. Of course, this is just my personal opinion.

一旦 Docker 文件出现,新的右键单击上下文菜单就可用了。选择构建 Docker 映像:

经过相当长的构建过程后,您应该会得到一个报告,表明映像已经成功创建。为了检查这一点,让我们运行一个 PowerShell 实例,看看当前可用的图像列表;列出图像的命令如下:

docker images

您的新图像应列在顶部:

现在,我们可以运行这个图像,只是为了检查它是否构建正确。为此,请使用以下命令:

docker run salesorderprocess

应该向您呈现控制台应用的输出;既然我们还没有改变什么,这就是默认Hello World

现在我们已经成功地创建了容器,让我们从添加将组成微服务的逻辑开始。

创建微服务逻辑

逻辑本身相对简单;但是,您会注意到这里有一些空白,我们使用的方法和类要么不存在,要么不可访问。我们将从把我们的逻辑放入一个单独的类开始;为了使这个类易于理解,我们将创建一个名为Run的公共函数:

public async Task Run()
{
    while (true)
    {
        if (!await ProcessEachMessage())
        { 
            await Task.Delay(60000);
            continue;
        }
    }
}

这里的逻辑是,我们试图从服务总线获取下一条消息;当我们不成功时,我们等待一分钟,然后再试一次。让我们看看ProcessEachMessage是什么样子的:

public async Task<bool> ProcessEachMessage()
{
    SalesOrder.Models.SalesOrder? salesOrder = await _serviceBusHelper.GetNextOrderFromMessageQueue();

    if (salesOrder == null) return false;

    _salesOrderService.Create(salesOrder);
    await _serviceBusHelper.ConfirmSalesOrderToMessageQueue(salesOrder);

    return true;
}

如果我们确实从队列中获得了销售订单,我们调用一个服务方法来创建销售订单;然后,我们向队列中添加一条确认消息。

This is the basis of this kind of design; where you need to communicate with other parts of the system, you need to do so via a mechanism that doesn't require that part of the system to respond immediately (or, indeed, at all). In this case, if we stopped this service and then ran the generator, the generator would not fail because this service was not running.

我们使用一个单独的类,这样我们就可以注入我们的依赖项;正如您所看到的,我们在这里使用了一些,它们目前大多是不存在的。我们将很快介绍这些,但我们先介绍一下新类的构造函数:

private readonly IServiceBusHelper _serviceBusHelper;
private readonly IProductService _productService;
private readonly ISalesOrderService _salesOrderService;

public SalesOrderProcessor(IServiceBusHelper serviceBusHelper,
                           IProductService productService,
                           ISalesOrderService salesOrderService)
{
    _serviceBusHelper = serviceBusHelper;
    _productService = productService;
    _salesOrderService = salesOrderService;
}

在这个阶段,您需要添加对SalesOrder.ServiceBusSalesOrder.Models项目的引用。我们现在已经到了这样一个阶段,即SalesOrder.Generate内部的大部分代码都应该被引入到自己的项目中,以便可以从其他地方访问。因为这大部分是将代码从一个项目移动到另一个项目,所以我不会详细说明每个类,而只是简单地显示新项目和旧项目的结构;以下是搬迁后的SalesOrder.Generate项目:

正如你所看到的,我们已经将几乎所有东西从SalesOrder.Generate中移出,并将其转移到一个新项目中:SalesOrder.Data。让我们看看我们在SalesOrder.Data有什么:

如您所见,我们只是从一个项目中取出文件,并将其粘贴到另一个项目中。唯一需要的其他步骤是重命名名称空间;例如,ProductService的命名空间现在将是SalesOrder.Data.Products

In cases like this, Ctrl + Shift + H is your best friend and will allow you to replace all the instances of the old namespace with the new one.

您还需要添加一些项目引用;SalesOrder.Generate项目现在应该参考SalesOrder.Data项目。新的SalesOrder.Data项目反过来又依赖于SalesOrder.Models

既然我们已经解决了这个问题,我们就可以开始填充存根函数了;让我们从SalesOrder服务开始。

For the sake of illustration, we grouped the service and data repository classes inside a single project. Generally, this should be avoided; should you decide to extend this project, separating the repository and the services may be a good idea.

创建新的 Azure SQL 数据库

当您为容器编写时,尤其是当您想要一个流畅的开发体验时,能够记住 Docker 命令变得越来越不重要;事实上,在本章的过程中,直到我们开始将容器移动到 AKS 中,才有可能不接触命令行。因此,很容易忘记您是在一个容器中运行的,而 Linux 容器也是如此。EF Core 允许您连接到一个 SQL 实例,因此很容易连接到(LocalDB)。但是,您将无法从您的容器中访问它,因为它是一个仅限于 Windows 的概念,并且当然存在于主机上,而不是容器中。

那么,我们可以从一个容器中访问 SQL Server 吗?如果是,怎么做?答案是你可以访问它,但是你需要记住你在哪里:当你在容器里面的时候,你的本地主机不是你的机器——它是你的容器。如果您想托管自己的 SQL Server 实例,当然可以这样做,但是您需要通过外部 IP 地址进行连接。

在我们的例子中,我们打算将它转移到 AKS 上,所以让我们的数据库托管在 Azure 中是有意义的。让我们快速创建一个新的 SQL Azure 数据库和服务器。从 Azure 中,选择 SQL 数据库刀片:

和往常一样,你也可以简单地搜索这个。选中后,单击添加创建新数据库:

这个屏幕现在看起来可能很熟悉。所有部分大致相同:订阅和资源组决定了资源的位置。每个服务器的数据库名称应该是唯一的。

除非已经设置了服务器,否则请选择标记为“创建新的 SQL 服务器”的链接。这将在屏幕右侧打开一个窗口,允许您创建一个新的服务器。一旦您这样做了,它将为您填充该值。

You'll be given a default Standard S0 compute and storage model. The compute and storage model that you choose is something that only you will know; however, if you are simply following along to learn how this works, I would recommend selecting the hyperlink to configure the database and selecting the Basic tier. At the time of writing, this comes in at under £5/month.

创建资源后,从概述中选择连接字符串,并保存一份连接字符串的副本——我们很快就会需要它。

实体框架核心

在这个阶段,我们需要引入一个数据持久层。为此,我们将使用实体框架核心 ( EF 核心)。第一步是向数据项目添加 EF Core:

Install-Package Microsoft.EntityFrameworkCore -ProjectName SalesOrder.Data
Install-Package Microsoft.EntityFrameworkCore.Tools -ProjectName SalesOrder.Data
Install-Package Microsoft.EntityFrameworkCore.SqlServer -ProjectName SalesOrder.Data
Install-Package Microsoft.EntityFrameworkCore.Design -ProjectName salesorder-process

一旦我们安装了必要的包,我们需要几个类,这样 EF Core 就可以创建我们的数据库;让我们将它们都放在SalesOrder.Data项目内部一个名为Entities的新文件夹中。事实上,我们首先需要的是一个SalesOrder实体:

public class SalesOrderEntity : SalesOrder.Models.SalesOrder
{
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }
}

正如您将看到的,我们只是从我们的模型中继承。拥有模型类的过程并不少见,模型类可以在数据库不可知的情况下传递,然后继承它们来创建数据库实体。您可以简单地告诉 EF 使用模型类,但是这个方法提供了一个抽象,这样您就可以屏蔽对模型类的更新,而不会让它们扩散到数据库中。我们还在这里声明了一个自动生成的主键,这显然在我们的模型中没有位置。

接下来,我们需要创建一个数据库上下文:

public class SalesOrderContext : DbContext
{
    public SalesOrderContext(DbContextOptions<SalesOrderContext> options) : base(options)
    { 
    }

    public DbSet<SalesOrderEntity> SalesOrders { get; set; }
}

在这里,我们只是简单地说,我们想要一个单一的表来保存销售订单。其余的功能由英孚核心为我们提供(注意我们继承自DbContext)。最后,我们需要一个设计环境,以便英孚核心知道在哪里创建我们的数据库:

public class SalesOrderContextFactory : IDesignTimeDbContextFactory<SalesOrderContext>
{
    public SalesOrderContext CreateDbContext(string[] args)
    {
        var optionsBuilder = new DbContextOptionsBuilder<SalesOrderContext>();
        optionsBuilder.UseSqlServer(@"Data Source=[ConnectionString]");

        return new SalesOrderContext(optionsBuilder.Options);
    }
}

这只是为了让英孚核心可以连接到我们的数据库,并创建或更新它。您需要用之前记录的连接字符串替换[ConnectionString]占位符。

The connection string has absolutely no place in this file; once you're familiar with how this works, I would encourage you to put this into a configuration file.

这就是我们需要的所有基础设施。下一步是添加迁移:

Add-Migration InitialCreation -Project SalesOrder.Data -StartupProject salesorder-process

You can call this command from PowerShell, or wherever you choose, but using the Package Manager Console inside Visual Studio gives you a nice little PowerShell interface directly inside the IDE.

这将在SalesOrder.Data项目中创建一个Migrations文件夹,并提供一个将创建数据库的迁移。要让它真正创建数据库,我们需要另一个命令:

Update-Database -Project SalesOrder.Data -StartupProject salesorder-process

在 Visual Studio 中快速查看 SQL Server 对象资源管理器,应该会验证数据库实际上已经创建。我们现在可以继续我们的代码,并从消息队列中填充它。

There are ways that you can have your program automatically call the migration when it runs. There are advantages to doing this either way; this method provides more control (you won't be able to run the software if the database isn't up to date), however, having to manually run an update script might not make sense in your deployment scenario.

创建销售订单

在创建销售订单时,我们实际上有两个抽象层:服务和存储库类。这里的想法是,存储库负责数据访问和业务概念的服务。在我们的例子中,这些是相同的,因为它是一个如此简单的应用,但是如果您开始扩展这个项目,记住这些差异将有助于代码保持干净和简洁。

Abstraction, for the sake of it, is the enemy of simplicity. If you know that your application will always be just a UI over a database and that you will always use Entity Framework, then access the data context directly. Although this does reduce your ability to create unit tests that don't include data access, it could be argued that, in such a scenario, those tests were unnecessary anyway.

从我们之前在Main方法中创建的代码来看,我们在销售订单服务中有两个新的创建方法;我们将从创建销售订单开始。SalesOrderService需要一种叫做Create 的单一方法。修改后的界面如下:

public interface ISalesOrderService
{
    void Create(SalesOrder.Models.SalesOrder salesOrder);
}

没什么可看的,我承认。这一点的实现并不复杂:

public void Create(SalesOrder.Models.SalesOrder salesOrder)
{
    var salesOrderEntity = new SalesOrderEntity()
    {
        ProductCode = salesOrder.ProductCode,
        Quantity = salesOrder.Quantity,
        Reference = salesOrder.Reference,
        UnitPrice = salesOrder.UnitPrice
    };
    _salesOrderRepository.Create(salesOrderEntity);
}

我们的存储库方法接受一个SalesOrderEntity类型的实体(我们在前面已经讨论过了),这意味着我们必须在这里转换对象。我们将存储库注入到这个类中;为了完整起见,下面是构造函数和类变量:

public class SalesOrderService : ISalesOrderService
{
    private readonly ISalesOrderRepository _salesOrderRepository;        

    public SalesOrderService(ISalesOrderRepository salesOrderRepository)
    {
        _salesOrderRepository = salesOrderRepository;
    }

    . . . 

我们下一步是填写StorageQueueHelper中的方法。

存储队列

我们有两种方法需要从存储队列中完成。我们的第一个方法是ConfirmSalesOrderToMessageQueue;这负责将销售订单的副本发布到新队列中,以表明我们已经处理了它。您可能开始注意到这与我们之前编写的首先向队列发送消息的方法之间的相似之处:

public async Task ConfirmSalesOrderToMessageQueue(Models.SalesOrder salesOrderData)
{
    CloudStorageAccount storageAccount = CloudStorageAccount.Parse("DefaultEndpointsProtocol=https;AccountName=salesorderqueue;AccountKey=aa8eED0s25wezSDCyj0BmukVq2zE9puEFRVq4jIR++n8L1NNSUyZZaJXZHVN91BgsQQ9sPE2gnlsb5MWC1TsVw==;EndpointSuffix=core.windows.net");

    CloudQueueClient queueClient = storageAccount.CreateCloudQueueClient();
    CloudQueue queue = queueClient.GetQueueReference("salesorderconfirm");
    await queue.CreateIfNotExistsAsync();
    CloudQueueMessage message = new CloudQueueMessage(JsonConvert.SerializeObject(salesOrderData));

    await queue.AddMessageAsync(message);
}

Remember to replace the connection details with your own (these are not real account details).

事实上,这两种方法几乎完全相同。队列名称是不同的,但是很明显,如果您愿意,这两个方法可以重构为一个。现在,我将把它们分开,因为我认为这更好地说明了它们的不同功能;然而,我不会重新解释代码。

最后,我们有从队列中获取下一个销售订单的代码,即GetNextOrderFromMessageQueue:

public async Task<Models.SalesOrder?> GetNextOrderFromMessageQueue()
{
    CloudStorageAccount storageAccount = CloudStorageAccount.Parse("DefaultEndpointsProtocol=https;AccountName=salesorderqueue;AccountKey=aa8eED0s25wezSDCyj0BmukVq2zE9puEFRVq4jIR++n8L1NNSUyZZaJXZHVN91BgsQQ9sPE2gnlsb5MWC1TsVw==;EndpointSuffix=core.windows.net"); 
    CloudQueueClient queueClient = storageAccount.CreateCloudQueueClient(); 
    CloudQueue queue = queueClient.GetQueueReference("salesorder"); 
    var message = await queue.GetMessageAsync();
    if (message == null) return null; 
    string data = message.AsString; 
    var salesOrder = JsonConvert.DeserializeObject<Models.SalesOrder>(data);

    await queue.DeleteMessageAsync(message.Id, message.PopReceipt); 
    return salesOrder;
}

同样,这是一组非常熟悉的方法;重点如下:

var message = await queue.GetMessageAsync();
...
string data = message.AsString;

这只是返回一条消息。现在,我们可以使用AsString属性提取其内容。然后,我们将其反序列化回一个SalesOrder对象:

await queue.DeleteMessageAsync(message.Id, message.PopReceipt);

此命令从队列中删除消息。值得记住的是,如果事情在这一点之后崩溃,信息将会丢失。您可能希望考虑维护这个标识,甚至可能只在订单创建后删除消息。但是,我们仍然可以识别订单,因为我们可以确定销售订单没有确认。

如果您现在运行这个项目,您应该会发现它工作正常,并开始处理队列中的任何消息。

最后一步是把这个上传到 Azure 的 AKS 系统中,这样项目就完成了。

现在我们已经创建了我们的项目,让我们看看如何在微软的 Azure Kubernetes 服务中托管这个项目。

蓝色忽必烈服务

Azure Kubernetes 服务(或 AKS ,通常缩写为)只是几种云 Kubernetes 产品中的一种。它充当了创建和托管自己的 Kubernetes 集群的中间点,并使用特定的云服务,如 AWS Lambda 和 Azure Functions。这里的主要优势是控制(您可以确定部署扩展的原因和方式)和可移植性(您应该能够从微软升级,将完全相同的设置放入谷歌,并从您停止的地方继续)。

构建 Docker 映像

让我们从代码中创建一个 Docker 映像。在项目中找到 Docker 文件,并从上下文菜单中选择“构建 Docker 映像”:

Azure 容器注册表

现在,您应该能够打开 PowerShell 并键入以下内容:

docker images

这将为您提供一个列表,列出您机器上当前拥有的所有 Docker 映像。容器注册表本质上是它的远程版本;也就是说,您创建一个映像并将其上传到注册表。当我们上传到 AKS 时,我们需要我们的图像在一个容器注册表中。虽然 Azure 确实有自己的容器注册表,但是您可以使用任何一个;Docker 有一个容器注册中心,大多数(如果不是全部的话)大型云提供商也有。在这一部分,我们将上传到蔚蓝集装箱登记处 ( ACR )。

到目前为止,我们几乎一直依赖于 Visual Studio 来管理我们的 Docker 映像;在撰写本文时,这是一个非常新的特性,以前需要在命令行中完成所有工作。现在,我们需要离开 IDE 的舒适环境,深入探究 PowerShell 和 Azure CLI 的黑暗世界。要确保安装了 CLI,请打开 PowerShell 窗口并输入以下命令:

az --version

Mine is version 2.0.57, although anything later than 2.0.55 should be fine. If you have a lower version than that, then please return to the Technical requirements section and reinstall the CLI. If you don't have this installed, then jump back to the Technical requirements section, where you'll find some instructions on how to install this.

我们的首要任务是将我们的形象推向 ACR。

蔚蓝集装箱登记处

既然我们已经建立了 CLI 的有效版本,我们就可以创建新的注册表;以下命令将为您完成此操作:

az acr create --resource-group netcode-projects --name salesorderregistry --sku Basic

netcore-projects是我的资源组,所以你需要用你自己的替换它。我已经调用了我的注册表salesorderregistry,但是只要你遵守命名规则,你可以随意调用你的注册表。sku决定可用的性能和规模。

You would need to look into the specifics at the time of using this, but broadly, the more you pay, the better the performance and the easier it will scale. This is a common pattern with cloud utilization.

对前面命令的响应应该类似于这样:

记下loginServer的回答,因为这决定了从我们的登录开始,您将如何从这里开始查阅您的注册表:

az acr login --name salesorderregistry

在前面的命令中,salesorderregistry实际上是我们刚才创建 ACR 时返回的值(为了这个命令的目的,我们只需要这个值的第一部分)。您应该会收到对该命令的响应,告诉您登录成功。

之前,我们查看了 Docker 图像,以识别本地系统中存在的图像;我们将再次需要该命令,但这一次,我们需要记下图像名称和标签。提醒一下,此命令如下:

docker images

响应应该是这样的:

首先,我们需要标记我们的图像;下面的命令应该可以帮我们做到这一点:

docker tag salesorderprocess salesorderregistry.azurecr.io/salesorderprocess:v1

如果我们再次列出图像,我们应该会看到新的标签:

现在我们有了一个标签,我们可以将它推送到我们的存储库中:

docker push salesorderregistry.azurecr.io/salesorderprocess:v1

这一次,我们应该看到一些行动;也就是说,当图像被推入存储库时,您应该会看到一些状态栏在移动:

要确认这一点,请使用以下命令,它应该会告诉您,您的图像现在确实在 ACR 中:

az acr repository list --name salesorderregistry --output table

现在我们在容器注册表中有了一个图像,我们可以创建我们的 Kubernetes 集群并设置我们的编排。

蓝色忽必烈服务

我们将在 PowerShell 中度过这一整节。为了使用 Kubernetes,您需要做的第一件事就是安装 CLI。为此,请登录 Azure:

az login

这将打开一个窗口,允许您输入您的 Azure 凭据。一旦通过身份验证,您应该能够安装 Kubernetes 命令行界面:

az aks install-cli

这可能需要一点时间来下载;一旦有了,我们就可以创建一个新的 Kubernetes 集群。本质上,集群就是当你说 Kubernetes 的时候你可能想到的。集群中的节点由主节点管理。这意味着您与主节点交互,它将工作负载委托给节点。想到这一点的好方法就像建筑工地上的工作人员:你可以接近工头,让他建一堵墙,但他自己不太可能这么做;相反,他会找到他的一些工人,他们有空并且有能力建造一堵墙,并要求他们开始建造。

以下命令应该会创建我们的集群:

az aks create --resource-group netcode-projects --name salesorder-cluster --generate-ssh-keys

It's worth pointing out that creating a Kubernetes cluster creates a number of virtual machines, all of which you pay for whether or not you're actually doing anything with them. In the grand scheme of things, you're looking at maybe a pound or two by the time you've completed this chapter, but remember to clean up afterward; otherwise, you may get an unexpectedly large bill!

同样,您应该将资源组替换为此项目使用的资源组。您也可以将名称替换为...好吧,不管你想要哪个名字。

The SSH keys that you've asked for will allow you to access the cluster.

完成后,您应该会得到一个包含大量集群细节的 JSON 响应。

下一步是将kubectl工具指向您的新集群:

az aks get-credentials --resource-group netcode-projects --name salesorder-cluster

kubectl is the main tool for interfacing with Kubernetes via the command line.

我们可以通过要求它列出我们的节点来验证这是否对我们的集群有效:

kubectl get nodes

你应该得到一个包含三个节点的列表,所有节点都准备好了。接下来我们需要做的是指定要在集群中使用的 Docker 映像;我们可以通过创建一个 Kubernetes 部署文件(在 YAML!).

库比涅斯部署

现代部署技术的巨大优势之一是,您不再需要公司中有人进行部署。我记得在我早期的职业生涯中,你要么自己手动将软件部署到一个真实的环境中,要么认识办公室里的某个人可以做到这一点。不用说,这项技术充满了危险!输入错误的命令,您可能会部署错误的软件,或者部署正确的软件不正确或不完整;这使得现场诊断问题变得极其困难,因为你永远也不能确定到底发生了什么。

*如今,有许多工具可以帮助解决这个问题;但是,Kubernetes(以及一般的容器生态系统)是用自动化部署构建的,作为您如何使用它的一部分。因此,为了将任何东西部署到 Kubernetes,您必须创建一个脚本(在 YAML 或 JSON 中)来告诉 Kubernetes 部署什么以及如何部署。

YAML is white-space sensitive, that is, the indentation levels matter.

让我们从创建 YAML 文件开始:

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: salesorderservice
spec:
  replicas: 2
  template:
    metadata:
      labels:
        app: salesorderservice
    spec:
      containers:
      - name: salesorder-process
        image: salesorderregistry.azurecr.io/salesorderprocess:v1
        ports:
        - containerPort: 80

在这里,我们宣布打算将我们的形象部署到 Kubernetes。一旦部署了这个,Kubernetes 将不断尝试保持这个声明的正确性;例如,如果一个节点死亡,它将尝试创建另一个节点;如果我们推出一个新的图像,它会试图得到那个图像,以此类推。

It is possible to simply ask Kubernetes to return the latest image (:latest), but this is generally considered to be bad practice, especially in production, as this could result in changes to a production environment that weren't planned.

现在,我们可以使用kubectl工具来应用我们的部署(您将需要在部署文件发生变化时执行此操作–我们稍后将回到这一点):

kubectl apply -f "C:\Dev\Packt\C-8-and-.NET-Core-3.0-Projects-Second-Edition\Chapter 8 - Sales Order Processor\deployment.yaml"

Remember to replace the path with the path to your own YAML file.

当您运行这个时,它应该看起来工作正常。但是,这一切其实告诉你的是,文件本身是有效的;你要求它做的事情可能是不可能的。例如,让我们键入以下内容:

kubectl describe pod

由此可见,部署远未成功:

问题是 Kubernetes 没有权限从 ACR 中提取图像。通过查看 Kubernetes 仪表板,我们可以对正在发生的事情有一个稍微好一点的了解。首先,我们需要让仪表板访问我们的集群:

kubectl create clusterrolebinding kubernetes-dashboard --clusterrole=cluster-admin --serviceaccount=kube-system:kubernetes-dashboard

然后,以下命令将启动仪表板:

az aks browse --resource-group netcode-projects --name salesorder-cluster

The dashboard is an invaluable tool when you're trying to diagnose issues with Kubernetes!

事实上,为了从 ACR 中拉出,AKS 需要明确的许可。我们可以使用以下命令来实现这一点:

az ad sp create-for-rbac --skip-assignment

由此,您应该会得到类似以下内容的响应:

突出显示的值是受让人的值(记下来);我们一会儿就称之为value_a。我们还需要从我们的容器注册表中获得一些详细信息:

az acr show --resource-group netcode-projects --name salesorderregistry --query "id" --output tsv

这应该会给你一个类似如下的回答:

我们在这里将高亮显示的值称为范围(我们称之为value_b)。

要实际分配角色,我们需要使用以下命令:

az role assignment create --assignee value_a --scope value_b --role acrpull

在我的例子中,这看起来如下:

az role assignment create --assignee 8a970d1b-7b69-4d2e-a084-0087c9cbf5d0 --scope /subscription
s/16a7fc79-9bea-4d04-83a1-09f3b260adb0/resourceGroups/netcode-projects/providers/Microsoft.ContainerRegistry/registries/salesorderregistry --role acrpull

现在,这个应该可以正常工作了;但是,如果您遇到任何问题,开始诊断这些问题的好地方是仪表板内。选择以下屏幕截图中突出显示的按钮将显示容器的日志文件,包括任何崩溃报告:

如果您发现您有任何错误,您可以修复它们并重新创建图像(跳转到构建 Docker 图像部分了解更多信息)。然后,您可以通过标记图像来跟随;但是,这一次,将图像标记为v2。最后,更新并重新应用deployment.yaml文件。现在,您的新 YAML 文件可能如下所示:

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: salesorderservice
spec:
  replicas: 2
  template:
    metadata:
      labels:
        app: salesorderservice
    spec:
      containers:
      - name: salesorder-process
        image: salesorderregistry.azurecr.io/salesorderprocess:v2
        ports:
        - containerPort: 80

当您的吊舱正常运行时,您应该能够在吊舱状态中看到以下内容:

kubectl describe pod

这会给你一个关于豆荚的健康报告:

最后,仪表板应该显示绿色:

现在我们已经成功配置了 Kubernetes 集群,让我们回到本章的标题,研究 Kubernetes 的负载平衡方面。

负载平衡

本章的标题是使用 Docker 和 Azure Kubernetes 服务的负载平衡订单处理微服务。事实上,您已经看到了这一点,因为工人舱正在根据可用的物品从队列中挑选物品。然而,让我们看看我们能否设计出一个更具戏剧性的演示,来说明这实际上意味着什么。让我们进入仪表板,删除其中一个吊舱;选择窗格右侧的省略号并将其删除:

删除 pod 后,您应该(几乎立即)注意到屏幕更改如下:

如你所见,一旦库本内特斯意识到你杀死了其中一个豆荚,它会立即启动新的豆荚进行补偿!它怎么知道要这么做?嗯,在我们的deployment.yaml文件中,我们已经将副本设置为2。为了证明这一点,让我们把副本增加到3:

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: salesorderservice
spec:
  replicas: 3
  template:
    metadata:
      labels:
        app: salesorderservice
    spec:
      containers:
      - name: salesorder-process
        image: salesorderregistry.azurecr.io/salesorderprocess:v2
        ports:
        - containerPort: 80

如你所见,除了那一个领域,我们什么也没改变。现在,让我们重新应用部署:

kubectl apply -f "C:\Dev\Packt\C-8-and-.NET-Core-3.0-Projects-Second-Edition\Chapter 8 - Sales Order
Processor\deployment.yaml"

Kubernetes 开始行动,并启动一个新节点:

现在,我们已经成功创建了我们的 AKS 集群,并看到它处理我们的订单。AKS 在不必要的情况下运行会很昂贵,所以我们将在下一节中拆除我们的资源。

清除

Kubernetes 集群可能是一项昂贵的业务。要明确的是,这通常比自己购买和运行服务要便宜得多;然而,如果你在玩这些东西,你很快就会欠下一大笔钱。要删除我们在本章中创建的资源,让我们从集群开始:

  1. 首先,打开 Kubernetes 服务刀片并选择您的集群:

这肯定不是一个快速的过程,但是一旦完成,它也应该删除创建的虚拟机。

  1. 接下来,打开存储帐户刀片,找到您为存储队列创建的帐户:

  1. 我们的下一站是容器注册刀片。我们需要找到我们的注册表并删除它:

  1. 最后但同样重要的是,我们的数据库。在 SQL 数据库刀片中,找到并删除我们创建的数据库:

现在,我们已经清理了我们创建的资源,让我们来谈谈本章中介绍的内容。

摘要

在本章中,我们已经了解了什么是微服务,以及如何创建微服务。我们已经讨论了如何使用队列等技术来分离服务,这意味着一个微服务不依赖于另一个微服务。通过使用 Kubernetes 来编排我们的服务,我们已经看到了这是一个自我修复的系统。

一路走来,我们看到了 Docker,并建立了 Docker 的形象;我们已经探索了容器注册中心,以及如何在其中存储 Docker 映像;我们还创建并配置了一个 AKS 集群。我们已经探索了服务总线和存储队列在 Azure 中的使用,以及它们如何在分布式系统中使用。

正如我在介绍中提到的,这是一个非常诱人的建筑模式;但是,我们也讨论了它如何不是没有成本的,而这个成本就是复杂性。在用几个软件替换一个软件的过程中,它们之间的交互、日志记录机制以及轻松维护整个系统的能力突然变得更加困难。

在下一章中,我们将朝着一个非常不同的方向前进,从设计一个大规模的云托管系统到运行在手机上的应用。然而,我们不会完全离开云世界,因为我们将利用一些预先构建的机器学习模型。*