2017 年 2 月

第 32 卷,第 2 期

此文章由机器翻译。

Azure - 深入探究 Azure App Service 体系结构

通过Yochay Kiriaty | 2017 年 2 月

Azure 应用程序服务被视为极好平台即服务 (PaaS) 产品/服务开发人员能够构建 Web、 移动和 API 应用程序的应用程序平台。从简单的市场营销和数字其产品范围内是否存在应用程序扩展性电子商务解决方案到和缩放性超高的、 可自定义应用程序。

App Service 进行全面管理,这意味着管理下划线计算基础结构 (服务器) 在其运行您的应用程序所需的任何管理任务。您不必担心下划线服务器维护,如平台为您修补操作系统和框架。您的应用程序在虚拟化服务器上运行,但您应只考虑过要在其运行您的应用程序的服务器实例的最大数目设置。缩放您的应用程序需要更多的平台句柄计算资源,并在同一时间,它处理流量进行负载平衡应用程序的多个实例。

虽然应用程序服务团队所做的事情了最大努力隐藏任何下划线实现细节,则应该知道的事情在封面下的工作方式。本文介绍了应用程序服务 (服务如何生成和运行) 的基本内部体系结构,并提供几种最佳做法,对于某些方案。

全局和地理上分散的体系结构

云计算快速进行缩放,并且具有无限容量。云级可以被解释为在计算机屏幕上查找。远程查看并了解清晰而又圆润图片;当您深入研究一下,您注意到屏幕上的图像组成许多小的像素为单位。云,如图像、 由多台服务器。应用程序服务群集到一个单元称为"缩放单位"(或"戳记") 的服务器部分。有许多此类扩展单元在全球范围内的 Azure 数据中心中。

作为 Azure 的一部分,应用程序服务具有全局需求量。在 Azure 支持每个区域中,您会发现应用程序服务运行客户的工作负荷 (应用程序) 和区域控制单元集的扩展单元。控制单元 (除非它们正常工作) 是透明的客户,并考虑平台的一部分。没有被用作所有管理 API 调用一个网关的一个特殊控制单元。例如,当客户发出一个请求来创建新的应用程序,通过门户中,命令行界面或直接通过 Azure REST API,该请求被发送到中央的 Azure 终结点 (management.azure.com)。Azure 资源管理器或 ARM (bit.ly/2i6UD07),可让您视为单个组应用程序中使用不同的 Azure 资源。定义由 ARM API 允许你管理 Azure 资源。ARM 不会实际管理单个资源。在 Azure 中的每个服务都有自己是由 ARM 代理的管理 API 实现。在应用程序服务的情况下 ARM 将转发到应用程序服务地域的主数据的任何应用程序服务 API 调用。

地域母版具有全球范围内有关所有扩展单元的上下文。例如,当您创建新应用程序服务应用程序 (或网站),地域 Master 查找您的应用程序的最合适的缩放单位,并随后将创建请求转发到合适的缩放单位。缩放单位现在任务是设置新的应用程序和任何所需的资源分配。图 1显示工作流的创建新的应用程序。

全球分布的应用程序服务扩展单元
图 1 全局通讯组的应用程序服务扩展单元

下面是用于创建新的应用程序的过程︰

  1. 用户发出请求以创建新网站。
  2. ARM 可确保用户有权访问的资源,以允许给定的操作 (创建这种情况下),并将请求转发到应用程序服务地域的主数据。
  3. 地域主查找最佳的合适的缩放单位的用户的请求,并将请求转发。
  4. 缩放单元创建新的应用程序。
  5. 地域主机上创建请求报告成功。

必须了解应用程序服务具有多个扩展单元时,您的应用程序通常运行在单一的应用程序服务缩放单位内。使用 Azure Traffic Manager 在多个区域中运行,即使在两个或多个单独的扩展单元中运行你的应用程序。但是,从单个缩放单位的角度来看,您的应用程序限制到单个缩放单位。

应用程序服务缩放单位是什么?

App Service 缩放单位是托管并运行您的应用程序的服务器的集合。典型的缩放单位可以有 1000 个以上的服务器。服务器群集使规模经济和重复使用的共享的基础结构。App Service 缩放单元的基本构造块是 Azure 云服务部署 (请记住,作为在 2012 年 6 月预览首次发布应用程序服务)。任何给定的缩放单位是自治,并可自行运行。

缩放单元主要构建基块

缩放单位的主要功能是托管并运行客户应用程序。应用程序在 Windows 服务器上运行,并被称为 Web 辅助进程或工作人员简称。给定的缩放单位中的服务器的大多数都是工作人员。但是,扩展单元包含实现应用程序服务所提供的功能所需的几个额外的支持服务器。支持服务器具有角色,以及针对冗余性和小数位数的多个实例上部署的每个角色。

前端

前端是层七个负载平衡器充当分发传入的 HTTP 请求不同的应用程序和他们各自的工作人员之间的代理。目前,应用程序服务负载平衡算法是一组给定的应用程序的已分配的服务器之间简单轮循机制。

Web 工作线程

工作进程为 App Service 缩放单位的主干。它们运行您的应用程序。

使用 App Service,您可以选择想要运行您的应用程序。您可以选择共享或专用服务器上运行你的应用程序。通过选择 App Service 计划中做到这样。App Service 计划定义功能、 功能和服务器分配一的组。共享的工作进程承载来自保证专用工作线程来运行的单个客户的一个或多个应用程序的多个不同客户的应用程序。有几种专用的服务器类型和大小,您可以从中选择。服务器大小越大,适用于已分配的应用程序将更多的 CPU 和内存资源。App Service 计划定义了您的应用程序的预分配服务器的量。

App Service 缩放单位有多个池的工作人员预先设置并可承载您的应用程序,如中所示图 2,第 1 部分。在定义时将专用的 App Service 计划为两个服务器的大小,应用程序服务会将分配两个服务器,如中所示图 2,第 2 部分。接下来,在横向扩展 App Service 计划 — 例如,添加两个更多的工作进程 — 可用辅助进程分配池中的线程的已准备就绪的辅助进程中所示图 2、 第 3 部分。由于工作人员已预先设置和感兴趣,所有需要发生这种情况是您的应用程序以获取部署在工作线程上。一旦部署该应用程序,工作线程已插入旋转以及前端可以分配给它的流量。整个过程通常需要几秒钟。

应用程序服务缩放单元中的服务器应用程序进程
图 2 应用程序服务缩放单元中的服务器应用程序进程

图 2、 第 4 部分显示多个 App Service 计划,标识多彩色矩形,为每个代表可以属于多个客户 App Service 计划。

文件服务器

任何应用程序需要存储区来存放内容,例如 HTML、.js 文件、 图像或代码文件和应用程序工作所需的任何其他内容。文件服务器装 Azure 存储 blob,并将它们公开为工作人员的网络驱动器。工作线程,作为回报,映射为本地,此类网络驱动器允许在任何给定的辅助进程上运行任何应用程序能够使用"local"的驱动器中,就像您期望的应用程序已在使用其本地磁盘的服务器上运行一样。通过文件服务器传递应用程序执行任何与文件相关的读/写操作。

API 控制器

API 控制器可以视为扩展到应用程序服务地域的主数据。虽然地域母版的所有应用程序服务应用程序感知到所有扩展单元,它是实际执行将影响您的应用程序的管理操作的 API 控制器。地域 Master 委托到给定的缩放单元通过 API 控制器的 API 执行。例如,当地域 Master 传递要创建新的应用程序的 API 调用,API 控制器协调的缩放单位,在创建应用程序所需的步骤。当你使用 Azure 门户重置您的应用程序时,它是通知所有当前分配给您的应用程序重新启动您的应用程序的 Web 辅助进程的 API 控制器。

发布服务器

Azure 应用程序服务支持对任何应用程序的 FTP 访问。应用程序内容存储在 Azure 存储 blob,并由文件服务器映射,因为应用程序服务都有一个发布服务器角色,以公开 FTP 功能。发布服务器角色允许客户使用 FTP 来访问其应用程序内容和日志。

请务必注意,有许多其他方式来部署应用程序而不是 FTP。常见的部署方法之一是 Web 部署 (从 Visual Studio) 或任何 Visual Studio 发行经理或 GitHub 等支持将继续部署选项。

SQL Azure

每个应用程序服务缩放单位使用 Azure SQL Database 保持应用程序元数据。分配给给定单位遵从性要求每个应用程序具有 SQL Database 中的表示形式。SQL Database 还用于保存有关应用程序的运行时信息。

数据角色

所有角色都需要在要运行的数据库中找到的数据。作为示例︰ 一个 Web 辅助进程在启动应用程序; 需要站点配置信息前端需要知道哪些服务器分配用于运行特定的应用程序中,以便正确转发 HTTP 请求路由到相应的服务器;和控制器中读取并更新基于所做的客户的 API 调用数据库中的数据。数据角色可以被描述为 SQL Database 和小数位数的给定单位中的所有其他角色之间的缓存层。它的角色,从而提高规模和性能,以及简化软件开发和维护的其余部分中提取数据层 (SQL Database)。

不太比明显的最佳做法

现在,您了解如何构建 Azure App Service,我们将回顾几个提示和技巧从 App Service 团队。这些是实际操作经验教训由应用程序服务与 numerus 客户互动的工程团队。

控制密度

大多数客户使用运行的数量较少 (小于 10) App Service 计划的每个应用程序。但是,有许多客户正在许多的多个应用程序的方案。请务必防止意外地通过饱和的基础服务器的计算能力。

我们开始应用程序和它们的计算资源的基本层次结构。有两个 Web 应用程序和与此 App Service 计划相关联的一个移动后端应用程序。计划设置为两个服务器。

默认情况下,给定的 App Service 计划中包含的所有应用程序运行的所有可用计算资源 (服务器) 分配给该服务计划。在这两台服务器上运行所有三个应用程序。在其中 App Service 计划都有一台服务器的简单情况下,很容易理解︰ 所有 App Service 计划中的应用程序在一台服务器上运行。

它是关于多个计算资源分配给 App Service 计划时,会发生什么情况有点不太直观的。作为示例,如果单个 App Service 计划具有 10 计算资源,然后在应用程序服务中的每个应用程序将在每个计算资源上运行。如果有 50 种应用程序在 App Service 计划中的,将在第一个服务器上运行所有 50 和将在第二台服务器上都运行同一 50 并且等操作时,到第 10 个服务器,它将还都运行所有 50 应用程序。

在某些情况下,其中您的应用程序需要大量计算资源,通常用于处理传入的 HTTP 请求,增加您需要在所有可用的服务器上运行的应用程序。但是,有时这是 App Service 计划向外扩展从一台服务器到许多时发生产生的意外的后果。如果 App Service 计划是由于在该类中运行的应用程序有大量的 CPU 和/或内存压力之下,App Service 计划不会解决该问题增加服务器的数目。

相反,请考虑每个应用程序的流量分布,分隔到单独的 App Service 计划的低容量应用长尾。请考虑在单独的 App Service 计划运行大量应用程序。使用 50,应用程序示例更早版本,分析流量模式后您可能会得到以下分配给 App Service 计划和计算资源︰

  • 40 低容量应用程序保持在一个计算资源上运行单个 App Service 计划中。
  • 五个 mid 到低容量应用程序使用对计算资源运行第二个 App Service 计划。
  • 她发现剩余的五个应用程序都具有高容量使用率。每个应用程序部署到单独的 App Service 计划中。  每个 App Service 计划至少包含一个计算资源,用于规则,以扩展输入/输出根据的 CPU 和内存利用率上进行设置的自动缩放规则。

这种方法的最终结果是,50 应用最低保护措施,与每个都有必要的空间来进行独立扩展按需基于负载的五个大容量应用程序使用七个计算资源。

按应用程序缩放

用于在更高效的方式运行的应用程序的较大数字的另一种方法是使用 Azure App Service 的每个应用程序缩放功能。在文档bit.ly/2iQUm1S涵盖每个应用的详细信息中的缩放。按应用程序扩展允许您控制分配给给定的应用程序的服务器的最大数目,就可以打开每个应用程序。在这种情况下,应用程序将运行的服务器定义的最大数量而不是在所有可用的服务器。

使用早期 50 应用程序的示例中,其中包含每个应用程序扩展为 App Service 计划,启用所有 50 应用程序可以分配给相同 App Service 计划。然后,您可以修改单个应用程序的缩放特征︰

  • 40 低容量应用程序设置为运行在一台服务器每个具有最大。
  • 五个 mid-到低容量应用程序设置为最多两个服务器上运行。
  • 五个剩余的大量应用程序设置为最多 10 个服务器上运行。

基础的 App Service 计划可以启动最少的五个服务器。然后再设置自动缩放规则以所需的基于的内存压力与横向扩展。CPU。

Azure 应用程序服务将自动处理应用程序来计算资源的分配。该服务还会自动将处理限制正在运行的辅助进程数所基于的应用程序实例的最大数量设置为每个应用程序。结果是,越来越多的 App Service 计划中的工作人员不会导致每个新的可用虚拟机上正常运转的 50 个应用程序实例。 

总之,按应用程序缩放"打包"到与 App Service 计划相关联的基础服务器上的应用程序。按应用程序缩放不会导致在与 App Service 计划相关联,如前面所述的每个计算资源上运行每个应用程序。

应用程序槽

应用程序服务都有一种称为"部署槽"功能 (bit.ly/2iJzv3f)。简而言之,部署槽使你能够生产应用程序之外的另一个应用程序 (槽)。它是另一个应用程序可用于测试新代码,然后再交换到生产环境。

应用程序插槽也显示在应用程序服务中的最常用功能。但是,它是一定要了解应用程序的每个插槽也是自己的权限中的应用程序。这意味着应用程序槽可以具有与它们、 不同的 SSL 证书,不同的应用程序设置等相关联的自定义域。这还意味着与 App Service 计划与主生产槽关联可以分开管理的应用程序插槽到 App Service 计划分配。

默认情况下,应用程序的每个插槽相同 App Service 计划与生产槽中创建。对于低容量应用程序,并且/或者具有较低的 CPU/内存使用率的应用程序,这种方法是很好。

但是,由于 App Service 计划中的所有应用程序运行在同一服务器上,这意味着默认情况下,所有的应用程序的槽都完全相同的基础生产服务器上运行。如果您决定在生产应用程序槽所在的同一服务器运行的非生产槽对运行压力测试,则可能导致问题,例如 CPU 或内存约束。

资源争用只限于如运行负载测试方案,如果然后暂时将一个槽移到不同 App Service 计划,并将其自己的服务器,设置将执行以下操作︰

  • 创建其他 App Service 计划适用于非生产槽。重要说明︰ 每个 App Service 计划需要能够在同一个资源组和生产槽 App Service 计划与同一区域中。
  • 将非生产槽移到不同的 App Service 计划,因此单独的池的计算资源。
  • 以单独的 App Service 计划运行时执行占用大量资源 (或危险) 的任务。例如,负载测试可以不能运行针对非生产槽造成负面影响的生产槽,因为不会有任何资源争用。
  • 非生产槽准备好交换到生产环境时,将其移回到同一个 App Service 计划运行生产槽。然后槽交换操作可以执行。

将部署到生产环境中随没有停机时间

已成功在 App Service 计划上运行的应用程序,并且具有出色的团队对您的应用程序在每日基础上进行更新。在这种情况下,您不想直接在生产中部署 bits。你想要控制部署过程并尽量减少停机时间。为此,您可以使用您的应用程序段。将您的部署设置为"预生产"槽位,可以使用生产设置中,配置和部署最新代码。现在可以安全地测试您的应用程序。一旦您感到满意,您可以交换新位到生产环境。交换操作不会重新启动您的应用程序,并返回控制器通知前端负载平衡器将流量重定向到最新的槽。

某些应用程序需要进行预热,它们可以安全地处理生产负载之前 — 例如,如果您的应用程序需要将数据加载到缓存中,或.NET 应用程序以允许你的程序集的 JIT 的.NET 运行时。在这种情况下,您还需要使用应用程序槽预热您的应用程序之前将其交换到生产环境。

我们经常看见的客户各遇到用于同时测试和准备好应用程序的预生产槽。可以使用连续部署工具如 Visual Studio 版本管理器来设置您的代码以获取部署到生产槽,运行测试以进行验证延迟和热您的应用程序之前将其交换到生产环境中的所有所需的路径的前一个管道。

缩放单元网络配置

在云服务部署的应用程序服务缩放单位。在这种情况下,这意味着某些网络配置和功能,您可能熟悉要全面了解您的应用程序对任何网络影响。

缩放单位有向外界公开一个单一虚拟 IP (VIP) 地址。分配给给定单位遵从性要求的所有应用程序是通过此 VIP 提供服务。VIP 是云服务在其上部署的应用程序服务缩放单位表示。

App Service 应用程序只提供 HTTP (端口 80) 和 HTTPS (端口 443) 流量。每个应用程序服务应用程序具有内置的 HTTPS 支持 azurewebsites.net 域名称的默认值。应用程序服务支持服务器名称指示 (SNI) 和基于 IP 的安全套接字层 (SSL) 证书。对于基于 IP 的 SSL,给定的应用程序的专用的 IP 地址为分配仅入站通信,这是与云服务部署相关联。请注意: 前端将终止 SSL 连接对所有 HTTPS 请求的所有应用程序和任何类型的证书。前端将请求转发给给定应用程序的指定工作。

公共 VIP

默认情况下,没有对所有入站 HTTP 流量的单个公用 VIP。任何应用程序进行寻址到一个 VIP。如果应用程序对应用程序服务,请尝试 (从 Windows 或 PowerShell 控制台) 运行 nslookup 命令并查看结果。例如:

#1 PS C:\> nslookup awesomewebapp.azurewebsites.net
#2 Server:  UnKnown
#3 Address:  10.221.0.3
#4 Non-authoritative answer:
#5 Name:    waws-prod-bay-001.cloudapp.net
#6 Address:  168.62.20.37
#7 Aliases:  awesomewebapp.azurewebsites.net

下面的输出的 awesomewebapp.azurewebsites.net 概述了︰

  • 行 #1 运行 nslookup 查询的 awseomwebapp.azurewebsites.net 解析。
  • 第 #5 行显示运行 awseomwebapp 应用程序的缩放单位的域名。您会注意到,App Service 缩放单位部署在 Azure 云服务 (通过 cloudapp.net 后缀)。WAWS 代表 Windows Azure (当 Azure 仍被称为 Windows) 网站 (应用程序服务的原始名称)。
  • 第 #6 行显示的缩放单位的 VIP。将托管并运行在上 waws-prod-托架-001 (#5 行) 的所有应用程序是在指定的公共 VIP 上可寻址的。
  • 第 #7 行显示所有域别名映射到相同的 IP 地址。

出站 Vip

很可能你的应用程序连接到其他 Azure 及非 Azure 服务。在这种情况下,您的应用程序不在您的应用程序的缩放单位进行到终结点的出站网络调用。这包括到 Azure SQL Database 和 Azure 存储空间等服务对外调用。有最多五个 Vip (一个公共 VIP 和四个出站专用的 Vip) 用于出站通信。你不能选择该 VIP 您的应用程序使用,并且来自缩放单元中的所有应用程序的所有出站调用使用五个已分配的 Vip。如果您的应用程序使用的服务,要求您将有权执行此类服务 API 调用的 Ip 白名单,您将需要注册的扩展单元的所有五个 Vip。若要查看哪些 Ip 将分配给出站 Vip 给定单元的小数位数 (或您的应用程序在您看来) 转到 Azure 门户中,如中所示图 3

在 Azure 门户中的 app Service 应用程序出站 IP 地址视图
图 3 App Service 应用程序出站 IP 地址在 Azure 门户中的视图

如果您正在寻找一组专用入站和出站 Ip,你可以通过使用在完全隔离和专用的 App Service 环境浏览这bit.ly/2hVRSlR

IP 和 SNI SSL

应用程序服务支持基于 IP 的 SSL 证书。使用 IP SSL 时,应用程序服务会分配到您的应用程序的专用的 IP 地址对于仅入站 HTTP 流量。

与不同的是 Azure 的专用 IP 地址的其余部分,通过 IP SSL 的应用程序服务的 IP 地址分配,只要您倾向于使用它。您自己的 IP 地址和删除 IP SSL 时,(因为它可能会分配给不同的应用程序),您可能会丢失的 IP 地址。

App Service 还支持 SNI SSL 不需要专用的 IP 地址和现代浏览器都支持。 

出站网络调用的网络端口容量

针对应用程序的常见要求是能够进行到其他网络终结点的出站网络调用。这包括对外调用到 Azure 的内部服务,例如 SQL Database 和 Azure 存储空间。它还包括用例,其中应用程序会对 HTTP/HTTPS API 终结点调用 — 例如,调用 Bing Search API 或调用的 API 实现的 Web 应用程序的后端业务逻辑的"application"。

在几乎所有这些情况下,在 Azure App Service 上运行的调用应用程序正在隐式打开网络套接字和终结点被认为是"远程"从 Azure 网络的角度对出站调用。这是一点很重要,因为从在 Azure App Service 上运行的通往远程终结点的应用程序中进行调用依赖于 Azure 网络设置和管理的网络地址转换 (NAT) 映射表。

此 NAT 映射中创建新条目需要时间,最终有一定的限制可以对单个 Azure App Service 缩放单位建立的 NAT 映射的总数。因此,应用程序服务强制出站连接数的时间都可能在任意给定时间未处理的数量限制。

最大连接限制如下所示︰

  • 每个 B1/S1/P1 实例 1920 连接
  • 每个 B2/S2/P2 实例 3,968 连接
  • 每个 B3/S3/P3 实例 8,064 连接
  • 每 App Service 环境的 64k 的最大上限值

应用程序"会泄漏在"连接总是会遇到这些连接限制。应用程序将启动间歇性地发生故障的原因到远程终结点的调用失败,并于更高版本的应用程序负载期间有时密切相关的失败。您会经常看到如下所示的错误︰ "试图访问其访问权限 aaa.bbb.ccc.ddd 禁止的方式在套接字。"

遇到了此问题的可能性可以显著缓解与几种最佳做法︰

  • 对于使用 ADO.NET/EF.NET 应用程序,使用数据库连接池。
  • 对于 php/mySql,使用持久的数据库连接。
  • 对于出站 HTTP/HTTPS 调用 Node.js 应用程序,以便将重用出站连接配置 keep-alive。在介绍了此配置bit.ly/2iGrcoo
  • 对于出站 HTTP/HTTPS 调用的.NET 应用程序,池和重用的 System.Net.Http.HttpClient 实例或使用 System.Net.HttpWebRequest 保持活动状态的连接。注意: 请记住 System.Net.ServicePointManager.DefaultConnectionLimit 会增加,因为您将会限制为两个并发的出站连接到同一个终结点。

有一些其他应用程序服务沙箱规定的限制。这些是较低级别的限制和约束的 Azure App Service 并且您可以阅读有关这些更详细地介绍bit.ly/2hXJ6lL

总结

Azure App Service Web、 移动和 API 应用程序提供丰富的 PaaS 产品/服务。虽然该服务有大量的移动的内部部件,这些都抽象出来,以便开发人员可以专注于编写优秀的应用程序,而该服务来处理在全球范围内运行这些应用程序的复杂性。 

许多应用程序服务的最佳实践都围绕应用程序的伸缩。很好地了解应用程序如何映射到 Web 工作进程在 App Service 计划很重要,因为增加您在服务上,同时仍保持最佳缩放配置状态运行的应用程序的数目。

给定首个基于云世界中我们开展业务,Azure 和 Azure App Service 总是不断演进在较快的速度。许多新的创新都应在 2017年中。

缩放单元组件

这可能看起来就像缩放单元组件是高度依赖于彼此。但是,根据设计,它们是松散耦合的。当前在给定的 Web 辅助进程运行 (主动提供 HTTP 通信) 的应用程序可以继续提供 HTTP 流量,即使的缩放单位中的其他角色不能正常工作。

例如,发布服务器不能正常工作可能会妨碍 FTP 访问,但是,不会影响应用程序的 HTTP 流量或其他部署选项。如果控制器具有阻止新的应用程序创建一个 bug,它并不意味着应用程序已分配给的缩放单位将停止工作。


Yochay Kiriaty是在 Microsoft Azure 团队,特别推动 Web、 移动、 API 的首席项目经理,函数会将视为 Azure App Service 平台的一部分。 Kiriaty 后期 90 年代以来一直致力于 Web 技术和热衷于规模和性能。您可以通过yochay@microsoft.com和关注他的 Twitter: @yochayk


Stefan Schackow是一种在 Azure 应用程序服务团队的项目经理,他曾在 Web 应用程序云自从其最早的产品/服务。 在 Azure 中,他领导团队的项目经理,从事开发和部署 Azure 应用程序服务,以及 Microsoft 的本地/云混合产品 (Azure Pack 和 Azure 堆栈) 的开发工作。他可以致电stefsch@microsoft.com


衷心感谢以下 Microsoft 技术专家对本文的审阅: Eduardo Laureano 和 Nir Mashkowski