人工智能即服务-全-

人工智能即服务(全)

原文:AI as a Service

译者:飞龙

协议:CC BY-NC-SA 4.0

前置材料

前言

在过去的二十年中,AI 在我们的生活中扮演了越来越重要的角色。它一直在幕后默默地发挥作用,因为全球各地的公司都在使用 AI 技术来改善搜索结果、产品推荐和广告,甚至帮助医疗工作者提供更好的诊断。AI 技术无处不在,不久的将来,我们都会乘坐自动驾驶汽车!

随着这种知名度的提升,对相关技能的需求也增加了。在机器学习或深度学习方面有专长的工程师通常以高薪被大型科技公司吸引。与此同时,地球表面上的每个应用程序都想使用 AI 来改善用户体验。但是,聘请相关技能集和获取训练这些 AI 模型所需的数据量的能力仍然是一个重大的进入障碍。

幸运的是,云服务提供商正在提供越来越多的 AI 服务,这些服务消除了您需要深入掌握收集和清理数据以及训练 AI 模型的艺术的需求。例如,AWS 让您可以使用为 Amazon.com 的产品推荐提供动力的相同技术,通过 Amazon Personalize,或者通过 Amazon Transcribe 为 Alexa 提供语音识别技术。其他云服务提供商(如 GCP、Azure、IBM 等)也提供类似的服务,我们将通过这些服务看到 AI 在日常应用程序中的功能。随着这些服务的日益完善和易于访问,人们自己训练 AI 模型的需求将越来越少,除非是更专业的负载。

看到一本专注于利用这些 AI 服务而不是 AI 模型训练细节的书真是太好了。这本书用通俗易懂的语言解释了 AI 和机器学习的重要概念,并准确地描述了它们,没有像 AI 相关对话中经常出现的所有炒作和夸张。这本书的美丽之处在于,它不仅仅是“如何使用 AWS 的这些 AI 服务”,还包括如何以无服务器的方式构建应用程序。它涵盖了从项目组织到持续部署,再到有效的日志策略以及如何使用服务和应用程序指标来监控您的应用程序的各个方面。这本书的后期章节也是集成模式和将一些 AI 魔法融入现有应用程序的实际案例的宝库。

无服务器是一种心态,是一种思考软件开发的方式,它将业务及其客户的需求放在首位,并旨在通过利用尽可能多的托管服务来以最小的努力创造最大的商业价值。这种思维方式导致开发人员生产力和功能速度的提高,并且通常通过在 AWS 等巨人的肩膀上构建,产生更可扩展、更弹性、更安全的应用程序。

无服务器不是我们围绕软件构建业务的未来;它是现在,并且它将留下来。这本书将帮助你开始无服务器开发,并展示如何将 AI 服务集成到无服务器应用程序中,以增强用户体验。这真是一石二鸟!

Yan Cui

AWS 无服务器英雄

独立顾问

前言

第四次工业革命已经到来!未来十年可能会在基因编辑、量子计算和当然还有人工智能(AI)等领域取得巨大进步。我们中的大多数人已经每天都在与 AI 技术互动。这不仅仅意味着自动驾驶汽车或自动割草机。AI 比这些明显的例子更为普遍。考虑一下当你访问亚马逊网站时,亚马逊刚刚做出的产品推荐,或者你与航空公司进行的在线聊天对话,重新预订航班,或者你的银行刚刚发送给你的文本,警告你的账户上可能存在欺诈交易。所有这些例子都是由 AI 和机器学习技术驱动的。

越来越多的开发者将需要将“智能”AI 功能界面添加到他们构建的产品和平台中。当然,AI 和机器学习的早期采用者已经这样做了一段时间;然而,这通常需要大量研发投资,通常需要一个数据科学家团队来训练、测试、部署和运行定制的 AI 模型。由于商品化的强大力量,这一局面正在迅速改变。

在他 2010 年畅销书《大切换》中,尼古拉斯·卡尔将云计算比作电力,预测我们最终将像使用公共事业一样消费计算资源。尽管我们还没有达到真正的公共事业计算阶段,但这一消费模式正在迅速成为现实。

你可以在云原生服务的范围和能力的爆炸性增长中看到这一点。云堆栈的商品化催生了无服务器计算范式。我们相信,无服务器计算将成为未来构建软件平台和产品的既定标准架构。

随着更广泛的应用堆栈的商品化,AI 也正在迅速成为商品。看看主要云提供商在图像识别、自然语言处理和聊天机器人界面等领域提供的 AI 服务数量。这些 AI 服务每月都在数量和能力上增长。

在我们公司 fourTheorem,我们每天都在使用这些技术,帮助我们的客户通过应用 AI 服务扩展和改进他们现有的系统。我们帮助客户采用无服务器架构和工具来加速他们的平台开发工作,并利用我们的经验帮助重构遗留系统,以便它们在云上运行得更高效。

正是这两项技术——无服务器和 AI 服务的快速增长和商品化,以及我们将它们应用于实际项目的经验,促使我们写这本书。我们希望提供一本工程师指南,帮助你在 AI as a Service 中取得成功,并祝愿你在开始掌握这个软件开发的新世界时一切顺利!

致谢

询问任何技术书籍的作者,他们都会告诉你完成一本书需要大量的时间和精力。这也需要他人的出色支持。我们非常感激那些使这本书得以完成的人们。

首先,我们想感谢我们的家人在我们努力完成这本书的过程中给予的支持、理解和耐心。Eóin 想感谢他惊人的妻子 Keelin,感谢她无尽的耐心、道德支持和不可或缺的技术审阅。他还要感谢 Aoife 和 Cormac,他们是世界上最好的孩子。Peter 想感谢他的女儿 Isobel 和 Katie,仅仅因为她们很棒。

Eóin 和 Peter 想感谢 fourTheorem 的联合创始人 Fiona McKenna 对这本书的信任,以及她在许多领域的持续支持和专业知识。没有你,我们无法完成这项工作。

开始这样一个项目是最困难的部分,我们感谢在开始时帮助我们的所有人。Johannes Ahlmann 贡献了想法、写作和讨论,这些都帮助塑造了这本书的内容。James Dadd 和 Robert Paulus 提供了宝贵的支持和反馈。

我们还要感谢 Manning 出版社的出色团队,是他们使这本书成为可能。特别是,我们想感谢我们的开发编辑 Lesley Trites,感谢她的耐心和支持。我们还要感谢我们的技术发展编辑 Palak Mathur 和 Al Krinker,感谢他们的审阅和反馈。感谢我们的项目编辑 Deirdre Hiam;我们的校对员 Ben Berg;我们的校对员 Melody Dolab,以及我们的审阅编辑 Ivan Martinović。

我们想感谢闫翠为这本书撰写前言。闫翠是一位杰出的架构师,也是无服务器技术的倡导者,我们非常感激他的支持。

感谢所有审阅者对文本和示例的反馈和建议改进:Alain Couniot、Alex Gascon、Andrew Hamor、Dwight Barry、Earl B. Bingham、Eros Pedrini、Greg Andress、Guillaume Alleon、Leemay Nassery、Manu Sareena、Maria Gemini、Matt Welke、Michael Jensen、Mykhaylo Rubezhanskyy、Nirupam Sharma、Philippe Vialatte、Polina Keselman、Rob Pacheco、Roger M. Meli、Sowmya Vajjala、Yvon Vieville,

特别感谢技术校对员 Guillaume Alleon,他为代码示例的仔细审阅和测试。

最后,我们想承认更广泛的开源社区,我们自豪地参与其中。我们确实站在巨人的肩膀上!

关于这本书

AI as a Service》作为工程师构建具备人工智能功能的平台和服务的指南被撰写。本书的目标是帮助您快速上手,并能够迅速产生结果,而不会陷入细节的泥潭。人工智能和机器学习是庞大的主题,如果您希望掌握这些学科,那么有很多东西需要学习。我们并不打算阻止任何人去这样做,然而如果您需要快速得到结果,这本书将帮助您迅速掌握相关知识。

本书探讨了两个正在增长且日益重要的技术:无服务器计算和人工智能。我们从开发者的角度来审视这些技术,以提供实用的、动手操作的指南。

所有主要的云服务提供商都在竞相提供相关的 AI 服务,例如

  • 图像识别

  • 语音转文本,文本转语音

  • 聊天机器人

  • 语言翻译

  • 自然语言处理

  • 推荐

这个列表将随着时间的推移而不断扩展!

好消息是,您不需要成为人工智能或机器学习专家就能使用这些服务。本书将指导您作为开发者将这些服务应用于日常工作中。

随着人工智能服务的增长,现在使用无服务器方法可以以最小的运营开销构建和部署应用程序。我们相信,在未来几年内,本书中描述的工具、技术和架构将成为企业平台开发的标准化工具包的一部分。本书将帮助您快速掌握相关知识,并指导您使用无服务器架构构建新的系统,并将人工智能服务应用于现有的平台。

适合阅读本书的人群

AI as a Service》是为全栈和后端开发者编写的,他们负责实现增强人工智能的平台和服务。本书对希望了解如何通过人工智能增强和改进其系统的解决方案架构师和产品所有者也很有价值。DevOps 专业人士将获得关于构建和部署系统的“无服务器方式”的宝贵见解。

本书组织结构:路线图

本书分为三个部分,共涵盖九个章节。

第一部分提供了一些背景信息,并检查了一个简单的无服务器 AI 系统:

  • 第一章讨论了过去几年无服务器计算的发展,解释了为什么无服务器代表了真正的实用云计算。在此之后,它提供了一个关于人工智能的简要概述,以便让对这一主题没有经验的读者也能迅速掌握相关知识。

  • 第二章和第三章快速构建了一个使用现成图像识别技术的无服务器 AI 系统。读者可以部署并实验这个系统,以探索如何使用图像识别。

第二部分深入探讨了开发者需要了解的个别工具和技术,以便在无服务器和现成 AI 方面变得有效:

  • 第四章探讨了如何构建和部署一个简单的无服务器 Web 应用程序,然后,也许更重要的是,如何以无服务器的方式确保应用程序的安全。

  • 第五章探讨了如何将 AI 驱动的界面添加到无服务器 Web 应用程序中,包括语音到文本、文本到语音和会话聊天机器人界面。

  • 第六章提供了一些具体建议,说明如何利用这一新技术集成为一个有效的开发者,包括项目结构、CI/CD 和可观察性——大多数刚开始接触这项技术的开发者都需要在他们的工具箱中具备这些。

  • 第七章详细探讨了如何将无服务器 AI 应用于现有或遗留平台。在这里,我们提供了可以应用的通用模式建议,并查看了一些点解决方案来展示这些模式的应用。

第三部分探讨了如何在全规模 AI 驱动系统的背景下将前两部分学到的内容结合起来:

  • 第八章探讨了大规模收集数据,以无服务器 Web 爬虫为例。

  • 第九章探讨了如何利用 AI as a Service 从大型数据集中提取价值,使用从无服务器网络爬虫收集的数据。

读者应复习第一章的内容,以获得对该主题的基本了解,并密切关注第二章的内容,其中我们描述了如何设置开发环境。本书最好按顺序阅读,因为每一章都是基于前一章的例子和学习内容。

关于代码

本书包含许多源代码示例,无论是编号列表还是与普通文本内联。在两种情况下,源代码都以fixed-width font like this的格式进行格式化,以将其与普通文本区分开来。有时代码也会加粗,以突出显示与章节中先前步骤相比有所改变的代码,例如当新功能添加到现有代码行时。

在许多情况下,原始源代码已被重新格式化;我们添加了换行符并重新整理了缩进,以适应书籍中的可用页面空间。在极少数情况下,即使这样也不够,列表中还包括了行续符()。此外,当代码在文本中描述时,源代码中的注释通常也会从列表中删除。代码注释伴随着许多列表,突出显示重要概念。本书中的示例源代码可以从出版商的网站上下载。

本书的所有源代码都可在以下存储库中找到:github.com/fourTheorem/ai-as-a-service

liveBook 讨论论坛

购买 AI as a Service 服务包括免费访问由 Manning Publications 运营的私有网络论坛,您可以在论坛上对书籍发表评论、提出技术问题,并从作者和其他用户那里获得帮助。要访问论坛,请访问livebook.manning.com/#!/book/ai-as-a-service/discussion。您还可以在livebook.manning.com/#!/discussion了解更多关于 Manning 的论坛和行为准则。

Manning 对我们读者的承诺是提供一个平台,让读者之间以及读者与作者之间可以进行有意义的对话。这不是对作者参与特定数量活动的承诺,作者对论坛的贡献仍然是自愿的(且未付费)。我们建议您尝试向作者提出一些挑战性的问题,以免他的兴趣转移!只要书籍在印刷中,论坛和先前讨论的存档将可通过出版商的网站访问。

关于作者

Peter Elger 是 fourTheorem 的联合创始人和 CEO。Peter 在英国的 JET 联合企业开始了他的职业生涯,在那里他花了七年时间构建用于核聚变研究的数据采集、控制和数据分析系统。他在软件行业的多个领域担任过技术领导角色,包括研究部门和商业部门,包括软件灾难恢复、电信和社交媒体。在创立 fourTheorem 之前,Peter 是两家公司的联合创始人兼 CTO:Stitcher Ads,一个社交广告平台;以及 nearForm,一家 Node.js 咨询公司。Peter 目前的重点是通过对尖端无服务器技术、云架构和机器学习的应用为客户创造商业价值。他的经验涵盖了从架构大型分布式软件系统到领导实施这些系统的国际团队。Peter 拥有物理学和计算机科学学位。
Eóin Shanaghy 很幸运,在 20 世纪 80 年代中期就能开始使用 Sinclair ZX Spectrum 编程。这是他第一次没有尝试拆解的电子产品。如今,他试图拆解软件系统。Eóin 是技术咨询服务公司 fourTheorem 的 CTO 和联合创始人,同时也是一位有经验的架构师和开发者,曾为初创公司和大型企业构建和扩展系统。Eóin 在许多不同的技术时代工作过,从 2000 年的基于 Java 的分布式系统到近年来全栈多语言容器和无服务器应用程序。Eóin 在都柏林三一学院获得了计算机科学学士学位。

关于封面插图

《AI as a Service》封面上的插图标题为“Homme de la Forêt Noire”,或称“黑森林的人”。这幅插图取自雅克·格拉塞·德·圣索沃尔(1757-1810)的作品集,名为《Costumes civils actuels de tous les peuples connus》,1788 年于法国出版。每一幅插图都是手工精心绘制和着色的。格拉塞·德·圣索沃尔的收藏中丰富的多样性生动地提醒我们,200 年前世界的城镇和地区在文化上有多么不同。人们彼此孤立,说着不同的方言和语言。在街道或乡村,仅凭他们的服饰就能轻易识别他们居住的地方以及他们的职业或社会地位。

自那以后,我们的着装方式已经改变,当时如此丰富的地区多样性已经消失。现在很难区分不同大陆、不同城镇、地区或国家的人们。也许我们用文化多样性换取了更加多样化的个人生活——当然,是为了更加多样化且节奏更快的技术生活。

在难以区分一本计算机书与另一本计算机书的今天,曼宁通过基于两百年前丰富多样的地区生活,并由格拉塞·德·圣索沃尔的图画使之重现的书封面,庆祝了计算机行业的创新精神和主动性。

第一部分. 第一步

在第一部分,我们为您提供了基础知识,帮助您了解作为服务的 AI。在第一章中,我们探讨了人工智能和服务器无计算的发展和历史。我们回顾了当前的技术水平,并将 AWS 上可用的服务分类到标准的架构结构中。在第二章和第三章中,我们直接深入,构建了一个无服务器图像识别系统,作为我们的第一个 AI as a Service 平台。

1 两种技术的故事

本章涵盖

  • 云景观

  • 什么是无服务器?

  • 人工智能是什么?

  • 摩尔定律的民主化力量

  • 典型的 AI 即服务架构

  • 亚马逊网络服务上的典型架构

欢迎来到我们的书籍!在这些页面中,我们将探讨两种爆炸性技术:无服务器计算人工智能。我们将从工程的角度来探讨。当我们说工程角度时,我们的意思是这本书将为你提供一本实用的动手指南,帮助你快速上手 AI 即服务,而不会陷入大量理论的泥潭。

我们想象,像大多数人一样,你已经听说过这些话题,可能会想知道为什么我们将这两个看似不同的主题结合在一本书中。正如我们将在接下来的章节中看到的那样,这些技术的结合有可能成为企业和对消费者平台开发的既定标准。这是一个将为软件开发人员——以及他们为之工作的企业——提供巨大力量来增强和改进现有系统,并快速开发和部署新的 AI 赋能平台的组合。

世界正变得越来越数字化——你可能听说过“数字化转型”这个短语。这通常指的是将现有的手动业务流程(目前使用电子表格、本地数据库,甚至完全没有软件)转变为运行在云上的平台的过程。无服务器为我们提供了一个工具链来加速数字化转型,并且越来越,人工智能成为这些转型的一个核心部分,用计算机取代所有或部分这些由人驱动的业务流程。

软件开发人员将越来越多地需要实施这些平台;我们中大多数参与软件行业的人将需要掌握设计、开发和维护这些类型系统的技能,如果我们还没有的话。

你现在在想,“我对人工智能一无所知!我需要成为人工智能专家吗?这听起来真的很困难?”别慌!你不需要成为数据科学家或机器学习专家来构建无服务器 AI 系统。正如我们将在整本书中看到的那样,大部分艰苦的工作已经以“现成”的云 AI 服务的形式为你完成了。作为软件专业人士,我们的工作是利用这些组件来设计解决方案。

让我们通过一个简单的例子来说明这个概念。想象一家连锁酒店。为了使经营酒店的公司在盈利和运营方面取得成功,必须发生许多过程。以确定某一天房间价格的问题为例。如果价格定得太高,没有人会预订,如果价格定得太低,公司就会失去收入。操作这个流程的人类依赖他们的经验来设定房间价格,并会考虑诸如当地竞争、季节、预期的天气以及可能在该地区发生的任何感兴趣的事件等因素。一旦决定,这些价格将被宣传,但随着当地条件的变化和房间被预订,这些价格将不断变化。

这个过程非常适合 AI 即服务平台,因为它是一个优化问题。使用云原生服务,我们可以想象快速开发服务来摄取和存储适当的数据,无论是通过 API 访问还是抓取有关当地事件信息的网站。我们可以使用现成的 AI 模型来解释抓取的数据,并且可以交叉训练现有的神经网络来计算为我们提供的最佳房间价格。这些价格可以通过另一个服务自动发布。今天,仅通过连接云原生 AI 和数据服务,就可以实现这一点,而无需对 AI 有非常有限的知识。

如果你的主要兴趣是开发简单的网站或低级通信协议,AI 即服务可能不会引起你的兴趣。然而,对于绝大多数软件专业人士来说,AI 即服务将是你职业生涯中产生重大影响的事物,而且很快就会到来!

1.1 云计算格局

任何参与软件行业的人至少对 云计算 有一个基本了解。云计算最初是一种在他人硬件上运行虚拟服务器的机制——通常被称为 基础设施即服务 (IaaS)。它已经演变成为一个更加丰富的按需服务套件,可以满足各种计算负载。目前有三家主要玩家:亚马逊、谷歌和微软。亚马逊网络服务 (AWS) 一直并且继续是云基础设施的主要提供商,提供了一系列令人眼花缭乱的解决方案。

截至 2020 年 3 月,三大平台提供的服务范围非常相似。表 1.1 列出了亚马逊、谷歌和微软在其产品页面上列出的常见类别下的可用服务数量。1

表 1.1 2020 年 3 月云服务数量

服务类型 AWS Google Azure
人工智能与机器学习 24 20 42
计算 10 7 20
容器 4 8 10
开发者 12 16 11
数据库 12 6 12
存储 10 6 17
物联网 12 2 22
网络 11 11 20
安全 18 28 10
其他 85 119 115
总计 198 223 279

需要尝试理解的服务有很多,每个服务都附带其特定的 API。鉴于我们永远无法详细了解整个领域,我们如何最好地理解所有这些内容,并成为有效的工程师?这个领域也在不断变化,因为新的服务被添加和更新。

我们的目标应该是理解架构原则,以及我们如何从这些服务中组合系统以实现特定的业务目标。我们应该努力保持对可用服务类型的心理清单,并深入探讨一个子集,这样我们就可以根据我们想要实现的结果快速吸收和利用新的服务。

图 1.1 有效的 AI as a Service 工程

图 1.1 概述了将 AI as a Service 平台视为思考框架的心理参考。

该模型建立在理解四个支柱之上:

  • 架构 --采用无服务器计算的有效架构模式是什么?

  • 开发 --最好的开发工具、框架和技术是什么?

  • AI --有哪些可用的机器学习和数据处理服务,以及它们如何最好地应用于解决业务问题?

  • 运维 --我们如何有效地将这些服务投入生产并管理它们的运营?

在这本书中,我们将通过构建一个包含机器学习服务(如聊天机器人和语音转文本)的示例软件系统来探索每个 AI 子主题的应用。我们将探索有效的无服务器开发框架和工具,并提供如何在无服务器环境中有效调试的帮助和建议。在本书的后期,我们将发现如何将 AI 工具和技术应用于平台运营,以及如何确保无服务器平台的安全。

我们还将看到我们现有的软件架构经验如何转移到无服务器领域,并开发一个适用于 AI as a Service 平台的规范架构,这将帮助我们将每个可用的云服务置于适当的背景中。我们将在这本书中,将此架构作为我们开发的示例系统的参考模型。

在本章的剩余部分,我们将探讨无服务器和 AI 的发展,并为每个主题提供简略的历史。这个重要的背景信息将帮助我们理解我们作为一个行业如何到达今天的位置,以及看似复杂的 AI 和云计算领域是如何演变的。大部分的理论都在这一章;从第二章开始,我们将直接进入代码!

1.2 什么是无服务器?

由于没有官方的无服务器术语定义,我们提供以下作为工作解决方案。

无服务器计算是一种云效用计算形式,其中云服务提供商动态管理服务用户的底层资源。它为底层基础设施提供了一层抽象,从最终用户那里移除了管理的负担。

无服务器软件是一种云软件,它避免了显式创建和管理基础设施;例如,服务器或容器。这些传统的计算资源被云提供商管理和运行的函数所取代。这被称为函数即服务(FaaS)。无服务器应用程序还避免了创建重型、专用资源,如数据库、文件存储或消息队列。相反,它们依赖于云提供商提供的托管服务,这些服务可以自动扩展以处理大量工作负载。无服务器应用程序的定价模型也非常重要。与无论资源是否在使用或空闲都要付费的传统模式不同,云提供商通常在函数被调用和管理服务被消耗时收费。这可以实现大量成本节约,并确保基础设施成本与使用量同步增长。

无服务器计算的原则可以总结如下:

  • 服务器和容器被按需执行的云函数所取代。

  • 与自定义资源相比,更倾向于使用托管服务和第三方 API。

  • 架构主要是事件驱动和分布式的。

  • 开发者专注于构建核心产品,而不是底层基础设施。

术语“无服务器”有点误导,因为当然,在链中某处总是涉及服务器!重点是,在无服务器的情况下,作为技术的用户,我们不再需要关心底层基础设施。云服务提供商通过 FaaS 和其他托管服务,为底层基础设施提供了一层抽象。

计算机历史在某种程度上一直是在创造抽象层次。早期的用户必须关注物理磁盘扇区和寄存器,直到操作系统的抽象被创建。语言已经从汇编语言这样的低级语言,发展到通过创建一系列越来越复杂的抽象,变成了动态的现代方言,如 Python。Serverless 也是如此。

任何从事软件开发工艺的人,无论是作为开发者、DevOps 专家、经理还是高级技术专家,都明白我们行业的变革速度是前所未有的。想象一下,如果你愿意,其他职业,如医生、牙医、律师或土木工程师,必须以软件行业那样的狂热速度更新他们的知识库。如果你觉得难以想象,我们同意你的看法!

这既是福也是祸。我们中许多人喜欢使用最新和最先进的技术栈,但我们也常常遭受选择的悖论,因为可供选择的语言、平台和技术可能太多。

选择悖论

选择悖论:为什么更多意味着更少是心理学家 Barry Schwartz 写的一本书,他在书中提出了这样一个论点:更多的选择实际上会导致消费者焦虑。他认为,一个成功的产品应该将选择限制在几个不同的类别中。在编程语言、框架和平台方面,情况也类似,我们实际上有太多的选项可供选择!

许多在业界工作了一段时间的人对最新的技术趋势或框架变得合理地怀疑。然而,我们相信 Serverless 和当前的 AI 浪潮代表了一次真正的范式转变,而不仅仅是一个短期趋势。

要理解 Serverless 真正意味着什么,我们首先需要了解行业是如何发展到今天的,而要做到这一点,我们需要探索推动行业的核心驱动力:速度!

1.3 对速度的需求

计算机工业的历史和发展是一个迷人的话题,关于这个主题已经写了许多值得阅读的书籍。虽然我们在这里无法对这个主题进行充分的公正评价,但了解一些关键的历史趋势和背后的力量是很重要的。这将帮助我们认识到,Serverless 实际上是这一历史进程中的下一个逻辑步骤。

1.3.1 早期阶段

计算机的历史可以追溯到古代,有算盘这样的设备。历史学家将第一个已知的计算机算法归功于 Ada Lovelace 在 19 世纪为 Charles Babbage 实现的。计算机的早期发展关注的是单一目的、难以驾驭的系统,旨在完成单一目标。现代软件时代真正开始于 1964 年第一个多任务操作系统的开发,即 MULTICS,随后 Unix 操作系统的开发。

1.3.2 Unix 哲学

Unix 操作系统是在 20 世纪 70 年代由 Ken Thompson 和 Dennis Ritchie 在 Bell Labs 创建的。原始的 AT&T 版本衍生出许多作品;最著名的大概是 Linux 内核及其相关发行版。对于那些对计算机历史感兴趣的人来说,图 1.2 描述了 Unix 家族树的主要分支。正如你所看到的,原始系统衍生出许多成功的衍生品,包括 Linux、Mac OS X 和 BSD 操作系统家族。

图 1.2 Unix 家族树。来源:mng.bz/6AGR

也许比操作系统更重要的是围绕原始 Unix 文化发展起来的哲学,可以概括为

  • 编写只做一件事并且做得很好的程序。

  • 编写可以协同工作的程序。

  • 编写处理文本流的程序,因为这是一个通用的接口。

提示:要全面了解这个主题,请参阅 Brian Kernighan 和 Rob Pike 所著的《Unix 编程环境》(Prentice-Hall,1983 年)。

这种系统设计方法首先将模块化的概念引入到软件开发中。我们应该注意,这些原则可以应用于底层操作系统或语言无关。例如,在 Windows 编程环境中使用 C#应用 Unix 哲学是完全有效的。

这里要理解的关键点是单一职责原则——编写具有单一关注点的程序或模块。

单一关注点

为了说明程序单一关注点的概念,考虑以下 Unix 命令行工具:

  • ls 知道如何在目录中列出文件。

  • find 在目录树中搜索文件。

  • grep 知道如何在文本中搜索字符串。

  • wc 知道如何计算文本中的行或单词数。

  • netstat 列出打开的网络连接。

  • sort> 对数字或字母进行排序。

  • head 返回输入的前 n 行。

这些工具本身相当简单,但我们可以将它们组合起来完成更复杂的任务。例如,以下代码给出了系统上监听 TCP 套接字的数量:

$ netstat -an | grep -i listen | grep -i tcp | wc -l

这个例子显示了目录树中的五个最大文件:

find . -type f -exec ls -s {} \; | sort -n -r | head -5

做一件事做好的哲学是软件中一股强大的正能量。它允许我们编写更小的代码单元,这些单元更容易推理,更容易正确实现,而不是大型互联的单一巨块。

1.3.3 面向对象和模式

这种原始的干净和模块化方法在很大程度上被行业遗忘,转而采用面向对象的范式。在 20 世纪 80 年代末和 90 年代初,C++等语言越来越受欢迎。受软件模式运动推动,面向对象的承诺是代码可以通过继承和多态等机制在对象级别上重用。结果证明,这种愿景从未实现,正如在著名的“香蕉、猴子、丛林”问题中所讽刺的那样。

香蕉、猴子、丛林

“香蕉、猴子、丛林”问题指的是现实世界中面向对象代码库中的重用问题。它可以表述为:“我想得到一个香蕉,但当我伸手去拿时,我得到了一个拿着香蕉的猴子。不仅如此,这只猴子还抓着树,所以我得到了整个丛林。”

以下片段说明了这个问题:

public class Banana {
    public Monkey Owner {get;}
}

public class Monkey {
    public Jungle Habitat {get;}
}

public class Jungle {
}

为了使用 Banana 类,我首先需要向它提供一个 Monkey 实例。为了使用 Monkey 类,我需要向它提供一个 Jungle 实例,依此类推。这种耦合是作者遇到的大多数面向对象代码库中的真正问题。

在这个时期,在互联网出现之前,系统往往被开发和构建为单体,大型系统拥有超过一百万行代码,构成一个单一的可执行文件,这种情况并不罕见。

1.3.4 Java, J2EE, .NET,

面向对象的趋势从 20 世纪 90 年代延续到 21 世纪,Java 和 C#等语言获得了显著地位。然而,这些系统的本质开始从桌面交付转向更分布式、网络感知的应用。这个时期见证了应用服务器模型的兴起,其特点是大型的单体代码库、庞大的关系型数据库以及大量的存储过程,以及 CORBA/COM 用于分布式通信和互操作性。部署通常每三到六个月一次,需要数周的计划和停机时间。

CORBA 和 COM

常见对象请求代理架构(Common Object Request Broker Architecture)是一种在 2000 年代初非常流行的遗留二进制通信协议。通用对象模型(COM)是微软特有的 CORBA 替代品。这两种技术现在都幸运地被 RESTful API 所取代。

图片

图 1.3 2000 年左右的企业软件开发(或耕牛耕作,乔治·H·哈维,1881 年)。来源:http://mng.bz/oRVD。(链接

反思起来,21 世纪初的发展可以与农业的早期阶段相提并论。在当时,它是一场革命。与后来的发展相比,它显得缓慢、笨拙、缺乏灵活性,且劳动密集。

1.3.5 XML 和 SOA

从这里,行业转向采用XML(可扩展标记语言)作为配置和通信一切的手段,随着 SOAP 的出现和所谓的面向服务的架构(SOA)的推动而达到顶峰。这由对解耦和互操作性的需求所驱动,并建立在开始理解开放标准益处的认识之上。

SOAP

简单对象访问协议(Simple Object Access Protocol)是一种基于 XML 的文本协议,被誉为优于 CORBA 和 COM 的替代品。由于其基于文本的特性,SOAP 比 CORBA 或 COM 具有更好的跨平台互操作性;然而,与基于现代 JSON 的 RESTful API 相比,它仍然显得笨重且使用不便。

1.3.6 网速

与企业软件开发的变化并行,受到互联网泡沫(及其随后的崩溃)的推动,软件即服务(SaaS)模式开始获得势头。行业正转向网络作为主要的应用交付机制,最初是用于外部客户 facing 的用途,但越来越多地用于内部企业交付。这一时期,对软件的快速交付需求日益增加,包括初始部署以及现在可以立即部署到服务器上的功能添加。在这一时期,主要的 SaaS 托管模式是将部署到本地服务器或数据中心内共置的机器上。

因此,行业面临两个主要问题。首先,需要提前预测所需的容量,以便能够分配足够的资本支出购买处理预期负载所需的硬件。其次,大型、单体、面向对象的代码库并不真正适合以网络速度的开发模式。

显然,重量级、封闭的企业模式不适合以网络速度进行交付。这导致了基于开放标准的方法的日益采用,以及开源技术的使用增加。这一运动由 FSF(自由软件基金会)、Apache 和 GNU/Linux 等组织领导。

向开源的转变导致了企业软件架构定义方式的一个关键、不可逆转的变化。最佳实践、标准和工具曾经是由像 Sun Microsystems、Oracle 和 Microsoft 这样的企业领导者所决定的。开源赋予了业余爱好者、初创企业和学者快速创新、以前所未有的频率分享和迭代的权力。在行业曾经等待大玩家就复杂的标准文档达成一致的时候,模式转变为了利用社区的联合力量和敏捷性来展示既实用又有效的解决方案,这些解决方案不仅立即就能工作,而且以惊人的速度持续改进。

1.3.7 云计算

云计算首次在 2006 年引起关注,当时亚马逊推出了他们的弹性计算云产品,现在被称为亚马逊 EC2。随后在 2008 年,谷歌推出了 App Engine,在 2010 年,微软推出了 Azure。说云计算从根本上改变了软件行业并不夸张。到 2017 年,亚马逊网络服务(AWS)报告的收入为 174.6 亿美元。

按需计算能力的可用性使得个人和资金紧张的初创企业能够构建真正创新的项目,并对行业产生不成比例的影响。关键的是,行业开始寻找软件工具的最具创新性的最终用户作为领导,而不是企业软件供应商。对于企业来说,云计算的兴起导致了几个地震般的变化。其中最重要的是

  • 成本模型从大额前期资本支出转向较低的持续运营支出。

  • 弹性扩展——资源现在可以按需使用和付费。

  • DevOps 和基础设施即代码——随着云 API 的成熟,开发出了工具来捕获整个部署栈作为代码。

1.3.8 微服务(重新发现)

开源软件的广泛应用加上向运营支出和弹性扩展的转变,导致在企业平台开发方面重新发现了 Unix 哲学,并有助于推动所谓的微服务架构的采用。尽管对于微服务没有达成一致的形式定义,但大多数行业从业者都会同意以下描述:

  • 微服务是小型、细粒度的,执行单一功能。

  • 组织文化必须接受测试和部署的自动化。这减轻了管理和运营的负担,并允许不同的开发团队独立工作于可部署的代码单元。

  • 文化设计和原则必须接受失败和故障,类似于反脆弱系统。

  • 每个服务都是弹性的、有弹性的、可组合的、最小化和完整的。

  • 服务可以单独和水平扩展。

微服务的理念并非新颖。分布式系统自 20 世纪 70 年代以来就存在。Erlang 在 20 世纪 80 年代就进行了微服务,从 CORBA 到 SOA 的所有事物都试图实现分布式、网络化组件的目标。微服务大规模采用的推动者是云、容器和社区:

  • 云基础设施如 AWS 使我们能够快速且低成本地部署和销毁具有高可用性的安全集群。

  • 容器(Docker)使我们能够构建、打包和部署包含软件的不可变单元,这些软件以微服务单元的规模存在。以前,将几百行代码作为一个单一单元部署是不可行或不理解的!

  • 社区提供的工具使我们能够管理在开始处理众多、小型部署单元时出现的新形式复杂性。这包括以 Kubernetes 形式出现的编排,以ELK(Elasticsearch、Logstash 和 Kibana)或 Zipkin 形式出现的监控,以及大量工具,例如 Netflix 工程团队开源的工作。

微服务模型非常适合现代云基础设施,因为每个组件都可以单独扩展。此外,每个组件也可以单独部署。这使开发周期更快,确实导致了所谓的持续部署的发展,即开发者的代码提交立即进入生产,无需人工干预,当然,前提是它通过了严格的自动化测试。

对于微服务的全面介绍,我们可以推荐理查德·罗杰的《微服务之道》,该书也由 Manning 出版社出版。

1.3.9 云原生服务

如亚马逊的 EC2 这样的服务通常被称为“基础设施即服务”或“IaaS”。虽然这是一个强大的概念,但它仍然将操作和管理负担放在了服务的最终用户身上。大多数系统将需要某种形式的数据库和其他基础设施才能运行。如果我们基于 IaaS 构建系统,那么我们就需要安装和维护数据库服务器集群,并处理备份、地理冗余和扩展集群以处理所需负载等问题。我们可以通过使用云原生服务来避免所有这些开销。在这个模型下,云服务提供商为我们管理数据库;我们只需通过配置或使用 API 告诉系统要做什么。

为了提供一个更具体的例子,让我们看看亚马逊的 DynamoDB 服务。这是一个全托管的高规模键值存储。要使用 DynamoDB,我们只需在 AWS 控制台的 DynamoDB 设置页面上输入一些配置信息,不到 60 秒就可以有一个可以读写的数据表。与此相对比的是,在 EC2 实例上安装自己的键值存储所需的设置,这将需要数小时的设置和持续维护。

云服务中最令人兴奋的发展之一是能够在云上运行托管代码单元,而不必关心底层服务器。这通常被称为“函数即服务”或“FaaS”。在 AWS 上,FaaS 是通过Lambda服务实现的,而谷歌的提供物被称为Cloud Functions

1.3.10 趋势:速度

如果我们拉紧所有这些线索,就会很明显,由于一些失误,主要的驱动力是速度的需求。毕竟,时间就是金钱!这意味着我们需要尽快将代码投入生产,并能够快速管理和扩展它。这推动了微服务和云原生服务的采用,因为这些技术为功能快速开发和部署提供了一条途径。

随着技术格局的变化,行业在软件开发中应用的方法论也发生了演变。这些趋势总结在图 1.4 中。

图片

图 1.4 迭代时间和代码量的变化。由 Clarke, Paul. 2017. 计算机科学讲义。都柏林城市大学和 Lero,爱尔兰软件研究中心。

如上图所示,迭代次数已经迅速减少。在 20 世纪 80 年代和 90 年代初,使用瀑布式方法,所谓的迭代可能就是整个项目的长度——对于大多数项目来说可能是长达一年或更久。当我们进入 20 世纪 90 年代中期,随着早期敏捷方法如 Rational Unified Process(RUP)的采用,迭代时间缩短。到了大约 2000 年左右,敏捷方法如极限编程(XP)的出现,迭代时间缩短到两周左右,而现在一些更超敏捷的实践甚至使用一周左右的迭代时间。

软件伴随的发布周期——生产发布之间的时间——一直在减少,从 20 世纪 80 年代和 90 年代的一年多到现在的远更快发布计划。实际上,使用持续部署技术的最高效的组织每天可以向生产发布软件多次。

以如此快的速度发布的能力得益于另一个趋势:即尺度的减小。每个部署单元的代码量正在减少。在 20 世纪 80 年代和 90 年代,大型、单体代码库是常态。由于这些代码库的耦合性质,测试和部署是一个困难和耗时的过程。随着 20 世纪 90 年代末和 21 世纪初服务导向架构的出现,部署单元的规模减小。随后,随着微服务的采用,出现了更显著的下降。

图 1.5 展示了随着尺度的减小和部署周期的加速,抽象级别逐渐提高,远离底层硬件。

行业在很大程度上已经从安装和运行物理硬件,通过虚拟服务器到基于容器的部署转变。当前最先进的状态通常是部署为容器的小型服务到某种编排平台,如 Kubernetes。服务通常会消耗 IaaS 配置的数据库或云原生数据服务。然而,如果增加部署速度和减少单位规模的趋势要继续下去——经济激励表明这是一个可取的目标——那么这一进步的下一个逻辑步骤就是转向完全无服务器系统。

图 1.5 尺度单位变化

图 1.6 展示了导致行业发展到无服务器计算的技术路径和技术里程碑。

图 1.6 计算概念的历史,这些概念导致了无服务器计算的出现。E. van Eyk 等人,“无服务器更多:从 PaaS 到现在的云计算。”IEEE 互联网计算 22,第 5 期(2018 年 9/10 月):8-17,doi: 10.1109/MIC.2018.053681358。

总结来说,对软件快速开发和部署的需求导致了规模单位的减少。这一进程的下一个逻辑步骤是采用完全无服务器架构。

1.4 什么是人工智能?

人工智能(AI)是一个术语,它已经涵盖了计算机科学中一系列技术和算法方法。对许多人来说,它常常会让人联想到失控的杀手机器人,这主要是由于《黑客帝国》和《终结者》电影中所描绘的反乌托邦未来!

对这个术语的一个更加清醒和合理的定义可能是这样的:

人工智能指的是计算机展现学习和决策能力的能力,正如人类和其他动物物种所展现的那样。

它只是代码

虽然现代人工智能系统所展现的一些能力可能看起来神奇,但我们始终应该记住,最终它只是代码。例如,一个能够识别图像的算法可能是一个非常强大的工具,但在基本层面上,它只是一系列非常简单的单元的互联。人工智能算法中发生的“学习”过程实际上只是根据训练数据调整数值的非常简单的事情。正是这些数值的大量涌现行为产生了显著的结果。

1.4.1 人工智能的历史

为什么突然对人工智能感兴趣?为什么人工智能和机器学习的需求日益增加?人工智能的发展是一个迷人的话题,本身就可以写几本书,而且全面论述显然超出了本书的范围。

人类一直对创造自身的复制品感到着迷,但直到 17 世纪,哲学家如莱布尼茨和笛卡尔才开始探索这样一个观点:人类思维可以用系统、数学的方式描述,因此可能被非人类机器所复制。

从这些最初的哲学探索开始,直到 20 世纪初,像罗素和布尔这样的思想家才对这些想法进行了更正式的定义。这些发展,加上数学家库尔特·哥德尔杰出的工作,导致了艾伦·图灵的基础性工作。图灵的关键洞察是,任何在哥德尔不完全性定理的范围内可以形式定义的数学问题,在理论上都可以由计算设备,即所谓的图灵机来解决。

图灵和弗劳尔斯在英国布莱切利公园开发 Bombe 和 Colossus 系统的工作具有开创性,最终导致了 1956 年夏天的著名达特茅斯学院会议,该会议通常被认为是人工智能学科的正式创立。

早期充满了乐观情绪,导致了一些非常乐观的预测,例如

  • H.A.西蒙和 A.纽厄尔,1958 年:“在十年内,一台数字计算机将成为世界象棋冠军。”

  • H.A.西蒙,1965 年:“在二十年内,机器将能够完成人类能做的任何工作。”

  • M.明斯基,1967 年:“在一代人……内,创造人工智能的问题将得到实质性解决。”

20 世纪 70 年代初该领域的进展并没有达到这些预期。随着时间的推移,没有取得实质性进展,资金来源枯竭,该领域的研究放缓。这一时期被称为第一次 AI 寒冬

20 世纪 80 年代见证了专家系统的兴起:基于规则的解决问题语言如 Prolog 在商业界获得了关注和兴趣。历史重演,到了 20 世纪 80 年代末,专家系统早期承诺的实现变得不再可能。这一事实,加上商品 PC 硬件的兴起,意味着公司将不再投资于这些系统所需的昂贵定制硬件,第二次 AI 寒冬开始了。

在幕后,研究人员在神经网络发展的领域取得了进展,包括网络架构和改进的训练算法,如反向传播。该领域缺少一个关键因素:计算能力。

在 20 世纪 90 年代和 21 世纪初的整个时期,摩尔定律(计算能力的指数增长)继续快速发展。这种能力的增长使得研究人员能够构建越来越复杂的神经网络,并缩短训练周期,为该领域的发展和创新提供了动力,速度大大加快。从 IBM 的 Deep Blue 在 1997 年击败加里·卡斯帕罗夫开始,AI 已经扩展到许多领域,并且正在迅速商品化,这意味着这项技术现在以低成本在许多商业环境中可用,无需专家研究团队。

1.4.2 现实世界 AI

自 20 世纪 50 年代以来,人们一直在努力创造能够展现人类能力的机器,能够接受一个目标并找出实现它的方法。在过去的几年里,出现了现实世界的 AI 解决方案,这些解决方案每天都在被使用。无论你是观看最新的电视剧,听音乐,在线购物,还是获取最新的新闻更新,你使用的技术很可能是由 AI 的最新进展所驱动的。让我们来看看人工智能技术对哪些领域产生了重大影响。

零售和电子商务

在线上零售和实体零售店中,AI 被应用于推荐顾客最有可能购买的产品。在电子商务的早期,我们看到了简单的推荐器示例(“购买这个的人还购买了……”)。如今,许多零售商正在详细监控用户的浏览行为,并使用这些数据与实时 AI 算法相结合,突出展示更有可能被购买的产品。

娱乐

在线电影、电视和音乐消费的显著增长为提供商提供了大量的消费数据。这些数据正被所有主要提供商用于进一步推动消费。Netflix 表示,80% 的订阅者选择来自平台的推荐算法。Spotify 是另一个从用户行为中学习并进一步提供音乐推荐的流媒体平台的例子。2

新闻与媒体

人工智能在社交媒体和在线新闻中的使用已经广为人知。Facebook 和 Twitter 都广泛使用人工智能来选择出现在用户时间线中的帖子。大约三分之二的美国成年人从社交媒体网站获取新闻,因此人工智能对我们看到新闻产生了重大影响(来源:www.journalism.org/2018/09/10/news-use-across-social-media-platforms-2018/)。

广告

广告是一个受到人工智能巨大影响的领域。人工智能被用于根据在线行为和偏好将广告与用户匹配。广告商在移动和网页上争夺消费者注意力的过程是实时并由人工智能自动化的。谷歌和 Facebook 都拥有庞大的 AI 研究部门,在这个过程中广泛使用人工智能。2017 年,Facebook 和谷歌占据了 90% 的新广告业务(来源:www.marketingaiinstitute.com/blog/ai-in-advertising-what-you-need-to-know)。

客户联系

随着在线世界的演变,消费者与企业的互动方式正在改变。许多人习惯于使用自动电话应答系统,通过按键选择选项来引导我们,或者使用准确性可疑的语音识别。客户支持运营现在正在使用各种先进技术来降低成本并改善客户体验。一个例子是使用情感分析来检测语气并优先考虑某些互动的重要性。另一个例子是使用聊天机器人来回答常见问题,而不需要任何工作人员。

语音识别和语音合成在这些场景中也极为有用,因为这些系统的能力在不断提高。2018 年谷歌 Duplex 演示是一个很好的例子,说明了这些能力已经变得多好(mng.bz/v9Oa)。每天都有越来越多的人使用 Alexa、Siri 或 Google Assistant 作为通向在线世界的接口,获取信息、组织生活并进行购物。

数据和安全

企业、消费者和监管机构越来越意识到数据隐私和安全的重要性。这体现在围绕数据存储、保留和处理形成的法规中。此外,安全漏洞也是一个日益令人担忧的问题。人工智能在解决这两个方面都发挥着作用。文档处理、分类和个人数据的识别已经在 Amazon Macie 等服务中实现并实施。在威胁和漏洞检测领域,人工智能被用于预防和警报。Amazon GuardDuty 是这方面的一个好例子。

除了信息安全之外,人工智能在物理安全领域也找到了许多实际应用。最近在图像处理和面部识别方面的重大改进正在应用于城市、建筑和机场安全。人工智能还可以有效地应用于检测来自爆炸物、枪支和其他武器等物体的物理威胁。

金融

通常,金融应用中的数据是时间序列数据。想想一个包含特定年份每天某种产品销售数量的数据集。这种性质的时间序列数据适合于预测人工智能模型。这些模型可用于预测和资源规划。

医疗保健

医疗保健领域的人工智能发展主要集中在诊断工具上,尤其是在放射学和微生物学领域的图像解释。最近对这一领域的深度学习研究进行的调查表明,该领域兴趣的激增和近年来性能的显著提升。虽然一些工作已经声称超越医学专家,但人工智能更典型地预期将被用作检测和测量细微异常的助手。3

在许多发展中国家,医疗专业知识短缺,这使得人工智能的应用更加有价值。例如,结核病的检测是通过人工智能自动解释胸部 X 光图像来进行的。

图 1.7 人工智能在发展中国家使用移动 X 光机辅助诊断结核病。(经 Delft Imaging Systems 许可复制。)

1.4.3 人工智能服务

表 1.3 展示了人工智能的一些常见应用,AWS(以及其他云服务提供商)为这些用例中的许多提供了基于预训练模型的服务。

表 1.2 人工智能应用和服务

应用 用途 服务
自然语言处理 机器翻译 AWS Translate
文档分析 AWS Textract
关键短语 AWS Comprehend
情感分析
主题建模
文档分类
实体提取
对话式界面 聊天机器人 AWS Lex
语音 语音到文本 AWS Transcribe
文本到语音 AWS Polly
机器视觉 物体、场景和活动检测 AWS Rekognition
面部识别
面部分析
图像中的文本
其他 时间序列预测 AWS Forecast
实时个性化推荐 AWS Personalize

我们将在后面的章节中使用这些服务的大部分,因此它们将非常熟悉,但我们将在此处总结每个服务以供参考:

  • AWS Translate 是一种神经机器翻译服务。这意味着它使用深度学习模型来提供比传统基于统计和规则的翻译算法更准确、更自然的翻译。

  • AWS Textract 通过结合光学字符识别(OCR)和文本分类模型,自动从扫描的文档中提取文本和数据。

  • AWS Comprehend 是一种自然语言处理(NLP)服务,它使用机器学习来在文本中找到洞察力和关系。

  • AWS Lex 是一种用于构建语音和文本对话界面的服务,也称为 聊天机器人。它通过使用深度学习模型进行自然语言理解(NLU)和自动语音识别(ASR)来实现这一点。

  • AWS Transcribe 使用深度学习模型将音频文件中的语音转换为文本。

  • AWS Polly 使用先进的深度学习模型将文本转换为逼真的语音。

  • AWS Rekognition 是一种图像识别服务,它使用深度学习模型在图像和视频中识别对象、人物、文本、场景和活动,以及检测任何不适当的内容。

  • AWS Forecast 基于 Amazon.com 使用的相同技术。它使用机器学习将时间序列数据与附加变量相结合,以构建预测。

  • AWS Personalize 提供个性化的产品和建议内容。它基于 Amazon.com 上使用的推荐技术。

1.4.4 人工智能和机器学习

过去 10 年在人工智能领域的关注和进步主要集中在机器学习领域,这是“通过经验自动改进的计算机算法的研究”(Tom Mitchell,《机器学习》,麦格劳-希尔,1991 年)。关于人工智能和机器学习的具体含义以及它们之间的细微差别存在一些争议。在这本书中,当我们谈论软件系统中的人工智能应用时,我们是在谈论机器学习。

机器学习的实践通常涉及一个训练阶段,随后是一个测试阶段。无论算法如何,机器学习算法都是在数据集上训练的。这可能是一组图像,例如图像识别算法的情况,或者是一组结构化记录,例如金融预测模型的情况。算法的目的是根据从训练数据中“学习”到的特征对测试数据进行判断。

机器学习可以划分为这些类别,如图 1.8 所示。

图 1.8 机器学习类型(来源:Analytics Vidhya)

注意特征是机器学习中的一个重要概念。为了在图像中识别一只猫,你可能会寻找三角形耳朵、胡须和尾巴等特征。选择正确的特征集对于算法的有效性能至关重要。

在传统的机器学习算法中,特征是通过手工创建的。在神经网络中,特征是由网络自动选择的。

机器学习可以分为以下这些类别:

  • 监督学习

  • 无监督学习

  • 强化学习

监督学习

监督学习是指算法被提供了一组标注的训练数据。例如,一组被标注为分类的文档。这些标签可能代表每个文档的主题。通过使用这个数据集训练一个特定的算法,你期望算法能够预测未标注测试文档的主题。在有足够、良好标注的训练数据的情况下,这可以非常有效。当然,缺点是可能很难找到或创建足够数量的这种标注训练数据。

无监督学习

无监督机器学习试图在没有任何标注训练数据(标签)的情况下从数据中提取相关模式。无监督算法的例子包括聚类、降维和异常检测。当我们想要从数据集中提取模式而没有具体期望的结果时,我们可以使用无监督学习。无监督方法有一个明显的优势,即它不需要标注数据。另一方面,结果可能难以被人类解释,并且学习到的模式可能与期望的不同。

强化学习

强化学习从直接经验中学习。它被提供了一个环境和奖励函数,目标是最大化其奖励。我们允许算法采取行动并观察这些行动的结果。然后我们尝试计算一个衡量结果期望的奖励函数。强化学习最可能的应用是合成计算机模拟环境,这些环境允许在短时间内进行数百万或数十亿次的探索性交互。

1.4.5 深度学习

深度学习基于人工神经网络(ANNs),这种网络最早在 20 世纪 50 年代被研究。人工神经网络由连接的节点层组成,或称为感知器。输入作为一组数字在输入层提供,结果通常作为输出层的数字提供。输入和输出之间的层被称为“隐藏”层。人工神经网络的目标是通过迭代学习每个感知器的权重,以便在输出层产生对期望结果的近似。单词“深度”指的是网络中有许多层(至少 7-8 层,但可能是数百层)。深度学习网络如图 1.9 所示。

使用神经网络模拟人脑的概念自人工智能研究之初就存在。然而,当时的原始计算能力根本无法实现这些方法潜力。在 2000 年代后期和 2010 年代初期,随着更强大的处理能力的出现,神经网络和深度学习开始成为人工智能的主要方法。深度学习的发展还得到了算法的进步以及来自互联网的大量训练数据的可用性的帮助。标记训练数据的任务通常通过众包(例如,亚马逊机械师)来解决。

图片

图 1.9 深度神经网络层

Alpha Go

一个展示了深度学习巨大进步的关键事件是 Alpha Go 胜过最优秀的人类围棋大师。Alpha Go 最初由英国公司 DeepMind Technologies 开发。该公司于 2014 年被谷歌收购。

关于这一点,关键的是网络必须“学习”围棋。这与 Deep Blue 在击败加里·卡斯帕罗夫时能够采取的方法有显著不同。这是由于可能的棋局状态数量。在围棋中,大约有 10⁴⁵ 种棋局状态,而在围棋中大约有 10¹⁷⁰ 种。正因为如此,可以使用游戏知识和算法机器学习技术相结合的方法,基本上编程 Deep Blue 成为围棋专家。然而,当我们考虑到可观测宇宙中原子数的估计值为 10⁸⁰ 时,我们就能理解围棋游戏的复杂性以及尝试使用类似专家系统的方法的不可行性。相反,Alpha Go 使用了一个通过观察数百万场比赛来训练的深度神经网络。

图 1.10 尝试将前面机器学习工具和技术的总结分类到一张单独的图表中。尽管对这一领域所有细微差别的详细描述超出了本章的范围,但图 1.10 应该在讨论高级机器学习时提供一个基本的参考框架。

图片

图 1.10 人工智能和机器学习算法与应用。深度强化学习:概述。李宇熙。arxiv.org/abs/1701.07274.

1.4.6 人工智能挑战

目前,人工智能主要由监督学习主导,这需要用于训练的数据。一个挑战是拥有代表网络要学习所有场景的标记数据。因此,无监督模型的发展成为研究的热点。对于许多希望利用人工智能的用户来说,通常无法获得足够的数据。使用有限的数据集进行训练往往会使算法产生偏差,导致对与训练集不相似的数据做出错误的判断。

当人工智能被应用于法律和伦理领域时,也存在挑战。如果一个机器学习算法做出了不希望的判断,很难知道哪个当事人应承担责任。如果一家银行决定某人无权获得抵押贷款,可能不清楚为什么做出这个决定,以及谁可以被追究责任。

1.5 计算能力和人工智能的民主化

观察摩尔定律几十年来所具有的民主化力量是很有趣的。考虑到今天仍然有程序员记得在穿孔卡片上提交程序以供执行!在大型机和迷你计算机时代,计算机时间是一种稀缺资源,只有少数特权人士才能获得。当然,现在我们大多数人都在口袋里携带比那些早期系统更强大的计算能力,那就是智能手机。

云计算也产生了类似的影响。在互联网泡沫时期,需要专业的硬件工程师在共址设施中构建服务器机架。如今,我们只需编写脚本就能构建整个数据中心,同样也可以轻松地将其拆除——前提是我们有足够的资金!

因此,人工智能也是如此。以前,为了构建一个具有语音识别等功能的系统,我们需要使用高度专业的定制硬件和软件,甚至可能需要自己对该主题进行研究。今天,我们只需要连接到云本地的语音识别服务之一,我们就可以为我们的平台添加语音界面。

1.6 标准化人工智能即服务架构

当以人工智能即服务这样广泛的新主题为出发点时,构建一个了解各个部分如何组合的图景非常重要。图 1.11 是典型无服务器人工智能平台结构的概览:一个架构参考框架。我们将在整本书中参考这个标准架构,作为一个共同的参考心理模型。

关于这个架构的关键点包括

  • 它可以完全使用云本地的服务来实现——不需要物理或虚拟机,也不需要容器。

  • 它可以在主要供应商提供的多个云平台上实现。

让我们逐一回顾这个架构的各个元素。

图 1.11 标准化人工智能即服务平台架构

1.6.1 网络应用程序

典型的平台将通过网络应用程序层提供功能,也就是说使用 HTTP(S)协议。这一层通常由多个元素组成,包括

  • 静态资源,如图像、样式表和客户端 JavaScript

  • 一些形式的内容分发网络或页面缓存

  • RESTful API

  • GraphQL 接口

  • 用户注册/登录/注销

  • 移动 API

  • 应用程序防火墙

这一层充当客户端请求进入主平台的大门。

1.6.2 实时服务

这些服务通常由 Web 应用层消费,以便对客户端请求立即做出响应。这些服务代表了平台各部分之间的通用粘合层。例如,一个服务可能负责获取图像并将其传递给 AI 服务进行分析,然后将结果返回给客户端。

1.6.3 批处理服务

通常这些服务是用于长时间运行、异步的任务,并将包括像ETL(提取、转换、加载)过程、长时间运行的数据加载和传统分析等。批处理服务通常使用如 Hadoop 或 Spark 等知名分析引擎,作为云原生服务消费,而不是自行管理和维护。

1.6.4 通信服务

大多数平台将需要某种形式的异步通信,通常是在某种消息传递基础设施或事件总线之上实现。在这个通信结构中,我们也期待找到诸如服务注册和发现等功能。

1.6.5 工具服务

工具服务包括安全服务,如单点登录和联合身份,以及网络和配置管理服务,如VPC(虚拟私有云)和证书处理。

1.6.6 AI 服务

这构成了无服务器 AI 平台的核心智能,并且可以根据平台关注的重点包含一系列 AI 服务。例如,在这里你可能找到聊天机器人实现、自然语言处理或图像识别模型和服务。在许多情况下,这些服务是通过预制的现成云原生 AI 服务连接到平台上的;在其他情况下,在部署到平台之前可能需要对模型进行一些交叉训练。

1.6.7 数据服务

我们的无服务器 AI 堆栈的基础是数据服务。这些服务通常将使用关系数据库、NoSQL 数据库、云文件存储以及介于两者之间的任何东西。与其他系统区域一样,数据层是通过消费云原生数据服务而不是自行安装和维护的实例来实现的。

1.6.8 运营支持

这个分组包含了平台成功运行所需的管理工具,如日志记录、日志分析、消息跟踪、警报等。与其他系统部分一样,运营支持服务可能无需安装和管理基础设施即可实现。值得注意的是,这些运营支持服务本身可能使用 AI 服务来帮助警报和异常检测。我们将在后面的章节中更详细地介绍这一点。

1.6.9 开发支持

这个分组关注平台的部署,并将包含创建其他服务分组所需云织物的脚本。它还将为其他每个服务组提供持续集成/持续交付管道的支持,以及平台的端到端测试。

1.6.10 离平台

我们包括了一个离平台元素分组。这些元素可能存在也可能不存在,这取决于平台操作模型。

AI 支持

这包括数据科学类型的调查、定制模型训练和调查。我们将在后面看到,训练机器学习系统的过程与实际使用它非常不同。对于许多用例,训练不是必需的。

内部数据源

企业平台通常会有与内部或遗留系统的接触点。这可能包括 CRM(客户关系管理)和 ERP(企业资源规划)类型系统或连接到遗留内部 API。

外部数据源

大多数平台都不是孤立存在的,可能会从第三方 API 中获取数据和服务;这些作为外部数据源服务于我们的无服务器 AI 平台。

1.7 在亚马逊网络服务(AWS)上的实现

为了使这一点更加具体,图 1.12 展示了标准架构可能如何映射到亚马逊网络服务(AWS)。

这当然不是 AWS 平台上所有可用云原生服务的详尽列表;然而,它确实说明了这些服务可以如何组合成一个连贯的架构结构。

为什么选择 AWS?

在整本书中,我们将提供针对 AWS 平台的代码和示例。我们选择这样做有两个原因:

  • AWS 在云计算领域的市场渗透率遥遥领先,是市场领导者。在撰写本文时,AWS 占据了 48%的市场份额。这意味着书中的例子将使更广泛的读者感到熟悉。

  • AWS 在创新方面处于领先地位。我们最近比较了 AWS 和其他云服务提供商在多个类别中服务的发布日期。我们发现,平均而言,AWS 比竞争对手提前 2.5 年发布服务。这也意味着 AWS 的服务提供更加成熟和完整。

当然,在三个不同的云上构建示例系统会带来很多工作!

图 1.12 在 AWS 上实现的 AI as a Service 平台

从这个映射练习中可以得出的关键点是

  • 在这个系统中,我们不需要在任何地方安装和管理服务器。这减少了大量与管理和扩展、容量规划等相关联的操作开销。

  • 所有这些服务的创建和部署都通过一组部署脚本进行控制,这些脚本可以像代码资产一样进行版本控制和管理工作。

  • 我们的人工智能服务可以现成使用;换句话说,不需要机器学习专家来构建系统。

我们希望第一章已经为你提供了足够关于行业趋势的背景信息,以使你相信 AI 作为服务和无服务器将在未来几年内成为平台开发的实际标准。

在本书的剩余部分,我们将专注于实用的配方和示例,以帮助你直接进入无服务器人工智能开发的尖端。我们将涵盖构建一系列越来越复杂的 AI 赋能系统,同时始终参考我们在本章中开发的规范架构。

我们要强调的是,虽然本书将使用 AWS,但架构、原则和实践可以轻松地转移到其他云上。Azure 和 GCP 都提供了并行服务,可以以与本书中 AWS 示例类似的方式组合。

接下来,我们将直接深入构建你的第一个 AI 作为服务系统!

1.8 摘要

  • 规模的单位正在减小。下一个合乎逻辑的进步是函数即服务或 FaaS。

  • 无服务器在很大程度上消除了对复杂 IT 基础设施管理的需求。

  • 服务扩展由云提供商处理,消除了容量规划或复杂自动扩展设置的必要性。

  • 无服务器允许企业更多地关注开发平台功能,而不是基础设施和运营。

  • 随着数据量和复杂性的增加,无论是商业还是技术分析,对人工智能服务的需求将会增加。

  • 云原生人工智能服务正在使人工智能技术民主化,现在非 AI 专家也可以使用。在接下来的周期中,可用的产品范围将只会增长。

  • 所有这些力量都使得以工程驱动的方式构建无服务器平台以及消费 AI 作为服务成为可能。


1.来源:aws.amazon.com/products/cloud.google.com/products/,和 azure.microsoft.com/en-us/services/

2.来源:mng.bz/4BvQmng.bz/Qxg4

3.来源:(1)Geert Litjens,Thijs Kooi,Babak Ehteshami Bejnordi,Arnaud Arindra Adiyoso Setio,Francesco Ciompi,Mohsen Ghafoorian,Jeroen A.W.M. van der Laak,Bram van Ginneken,Clara I. Sánchez. “医学图像分析中的深度学习综述。” 医学图像分析,第 42 卷,2017 年,第 60-88 页。(2)和(3)Esteva 等人(2017 年)和 Gulshan 等人(2016 年)在皮肤病学和眼科领域的工作。

2 构建无服务器图像识别系统,第一部分

本章节涵盖

  • 构建简单的 AI as a Service 系统

  • 设置云环境

  • 设置本地开发环境

  • 实现简单的异步服务

  • 部署到云端

在本章和第三章中,我们将专注于构建我们的第一个具有 AI 功能的无服务器系统。到结束时,你将配置并部署到云端一个能够从网页读取和识别图像并显示结果以供审查的小型系统。这听起来可能是一个章节中要做的大量工作,确实如此,在无服务器和现成 AI 出现之前,我们将在本章取得的成绩需要一个小团队工程师数月的工作量来完成。正如艾萨克·牛顿所说,我们站在巨人的肩膀上!在本章中,我们将站在无数软件工程师和 AI 专家的肩膀上,快速组装我们的“hello world”系统。

如果你刚开始接触 AWS 和无服务器技术,在这两个章节中,你将需要吸收大量的信息。我们的目标是慢慢来,并提供很多细节,以便让每个人都能跟上进度。我们将采取“按数字绘画”的方法,所以如果你仔细遵循代码和部署说明,你应该会做得很好。

随着你翻阅这些页面,无疑会有几个问题出现在你的脑海中,比如“我该如何调试这个?”或者“我应该如何进行单元测试?”请放心,我们将在后续章节中提供更多细节;现在,请拿一些咖啡,系好安全带!

2.1 我们的第一个系统

我们的第一个无服务器 AI 系统将使用 Amazon Rekognition 来分析网页上的图像。通过对这些图像的分析,系统将生成一个词云并为每个图像提供标签列表。我们将开发这个系统作为一个由多个离散、解耦的服务组成的系统。完成的用户界面截图显示在图 2.1 中。

图片

图 2.1 完成的 UI

在这种情况下,我们将系统指向包含猫的图片的网页。图像识别 AI 正确识别了猫,并允许我们从这个分析中构建一个词云和检测标签频率的直方图。然后系统向我们展示了每个被分析的图像,以及分析结果和每个标签的置信度。

2.2 架构

在我们深入实施之前,让我们看看这个简单系统的架构,看看它如何映射到我们在第一章中开发的规范架构,以及服务如何协作以提供此功能。图 2.2 描述了系统的整体结构。

图片

图 2.2 系统架构。系统由使用 AWS Lambda 和 API Gateway 构建的自定义服务组成。SQS 用于消息通信。这里使用的管理服务是 S3 和 Rekognition。

系统架构显示了系统的层级:

  • 从前端开始,由 S3(简单存储服务)提供,通过 API 网关调用 API。

  • 异步 Lambda 函数由 SQS(简单队列服务)消息触发。

  • 同步 Lambda 函数由来自 API 网关的事件触发。

  • AWS Rekognition 是一个完全管理的 AI 图像分析服务。

2.2.1 网络应用程序

系统的前端是一个单页应用程序,由 HTML、CSS 和一些简单的 JavaScript 组成,用于渲染 UI,如图 2.3 所示。您将在本章中多次看到此图,因为我们将介绍我们系统的基础构建块。

图 2.3 网络应用程序

前端部署到一个 S3 存储桶中。在这个层级中,我们使用 API 网关提供进入同步服务的路由,这些同步服务为前端提供渲染数据。

2.2.2 同步服务

有三个同步服务作为 Lambda 函数实现,如图 2.4 所示。

图 2.4 同步服务

这些服务作为通过 API 网关访问的 RESTful 端点提供:

  • POST /url/analyze--此端点接收一个包含 URL 的请求体,并将其提交到 SQS 队列以进行分析。

  • GET /url/list--由前端使用,以获取系统已处理的 URL 列表。

  • GET /image/list--返回给定 URL 已处理的图像和分析结果集。

要触发分析,我们的系统用户在 UI 顶部的输入字段中输入一个 URL,然后点击分析按钮。这将向/url/analyze发送一个 POST 请求,这将导致一个 JSON 消息被发送到一个形式为 SQS 队列的消息:

{body: {action: "download", msg: {url: "http://ai-as-a-service.s3-website-eu-west-1.amazonaws.com"}}}

2.2.3 异步服务

异步服务构成了系统的主处理引擎。有两个主要服务,如图 2.5 所示。

图 2.5 异步服务

爬虫服务从 HTML 页面中提取图像。分析服务提供了一个 AWS Rekognition 的接口,提交图像进行分析并汇总结果。

在收到“下载”消息后,爬虫服务将从提供的 URL 中获取 HTML。爬虫将解析此 HTML,并提取页面中每个内联图像标签的源属性。然后,爬虫将下载每个图像并将其存储在 S3 存储桶中。一旦所有图像都下载完毕,爬虫将向分析 SQS 队列发送一个分析消息:

{body: {action: "analyze", msg: {domain: "ai-as-a-service.s3-website-eu-west-1.amazonaws.com"}}}

此消息将被分析服务获取,该服务将为每个下载的图像调用图像识别 AI,收集结果,并将它们写入存储桶以供前端稍后显示。

2.2.4 通信服务

内部,系统使用简单队列服务(SQS)作为消息管道,如图 2.6 所示。

图 2.6 通信和数据服务

正如我们将在整本书中看到的那样,这种消息传递方法是一种强大的模式,它允许我们在几乎不影响整个系统的情况下向系统中添加服务或从系统中移除服务。它还迫使我们保持服务解耦,并为单独扩展服务提供了一个清晰的模型。

对于这个系统,我们使用 SQS 作为我们的主要通信机制,但我们使用术语 通信服务 来涵盖任何可以用来促进消费者和服务之间通信的基础设施技术。通常这需要某种形式的服务发现和一个或多个通信协议。图 2.7 描述了我们系统通信服务的隔离视图。

图 2.7 Elger

图 2.7 通信服务

所示的通信服务包括用于服务发现的 Route 53 DNS(域名系统)以及 HTTP 和 SQS 作为通信协议。通常,我们将使用 JSON 数据格式来编码双方之间的消息。这与底层通信协议无关。

消息传递技术

消息传递系统、队列和相关技术是一个大主题,我们不会在本书中详细讨论。然而,如果你还没有意识到这些概念,你应该了解它们。简而言之,消息传递系统通常支持两种模型之一--点对点或发布/订阅:

  • 点对点--在这个模型下,放入队列的消息仅被发送给一个消费者,仅此一个消费者。

  • 发布/订阅--在这个模型下,所有已注册对某种消息类型感兴趣的所有消费者都将接收到该消息。

队列系统在通知消费者新消息的方式上也存在差异。总的来说,这可以通过以下三种方式之一发生:

  • 推送--队列系统将消息推送到消费者(们)。

  • 轮询--消费者将轮询队列以获取消息。

  • 长轮询--消费者将进行一段较长时间的轮询。

在本章中,SQS 将将消息推送到我们的消费 Lambda 函数。

对于这个主题的入门,我们推荐 Gregor Hohpe 和 Bobby Woolf 的《企业集成模式》(Addison-Wesley Professional,2003 年)。

2.2.5 人工智能服务

这个系统只使用一个人工智能服务,即 Amazon Rekognition。这个人工智能服务提供多种不同的图像识别模式,包括对象和场景检测、面部识别、面部分析、名人识别以及图像中的文本检测。对于这个第一个系统,我们使用默认的对象和场景检测 API。

2.2.6 数据服务

在数据服务层,我们只使用简单存储服务(S3)。这对于我们在这个初始平台的需求来说是足够的;我们将在后续章节中探索其他数据服务。

2.2.7 开发支持和运营支持

我们使用无服务器框架作为我们的主要开发支持系统。所有日志数据都使用 CloudWatch 收集。我们将在接下来的章节中更详细地讨论这些内容。

2.3 准备工作

现在我们已经看到了最终目标,让我们深入其中,将系统组合起来。您需要一个活跃的 AWS 账户。如果您还没有 AWS 账户,您需要创建一个。如果您是 AWS 的新手,请参阅附录 A,其中包含了帮助您设置的说明。

对于熟悉 AWS 的各位,我们建议您创建一个独立的子账户,以保持本书中的示例不受您可能正在运行的任何其他系统的影响。

附录 A 还包含了创建 API 密钥和配置命令行及 API 访问的说明,因此我们建议即使是经验丰富的 AWS 开发者也应该回顾这些材料,以确保正确的开发环境。

小贴士:所有示例代码已在eu-west-1区域进行了测试;我们建议您在代码部署时也使用此区域。

警告:使用 AWS 需要付费!请确保您完成使用后,任何云基础设施都被销毁。我们已在每章末尾提供了帮助资源移除的脚本。

2.3.1 DNS 域名和 SSL/TLS 证书

本章的示例以及本书其他部分的示例都需要一个 DNS 域名及其相关证书。这些可以在 AWS 上轻松设置,如何在附录 D 中提供了完整的说明。在尝试运行示例之前,请确保您已根据附录 D 中提供的说明设置了您的 AWS 环境。

Node.js

我们在本书中使用 Node.js 作为主要开发平台。如果您还没有安装它,您需要安装。

为什么选择 Node.js?

我们选择 Node.js 作为本书的开发平台,因为 JavaScript 的普遍性,它不仅可在每个主要网络浏览器中使用,还可以在 Node.js 平台上作为服务器端使用。此外,JavaScript 还是所有主要 FaaS(无服务器功能即服务)提供的实现语言,所有这些都使其成为自然的选择。

如果您之前没有使用过 Node.js,请不要担心。如果您甚至只了解一点 JavaScript,您也会做得很好。如果您想复习 Node(甚至 JavaScript),我们强烈推荐 Node School 的教程系列。前往nodeschool.io/开始学习。

在撰写本文时,当前的长期支持(LTS)版本的 Node.js 是 10.x 和 12.x。可以从nodejs.org/下载二进制安装程序,为您的开发机器安装适当的二进制文件。

注意:我们将构建此系统的 AWS 上支持的最新 Node.js 版本是 12.x。为了保持一致性,最好在您的本地开发环境中选择最新的 12.x LTS 版本。

安装程序运行后,请打开控制台窗口并使用以下命令检查一切是否正常:

$ node -v
$ npm -v

NPM

NPM 是 Node.js 的包管理系统。对于我们的每个示例系统,我们将使用 NPM 来管理称为 node 模块 的依赖软件单元。如果您不熟悉 NPM,我们可以在 Node School 的 NPM 教程中推荐您:nodeschool.io/#workshopper-list

Serverless Framework

接下来,我们需要安装 Serverless Framework。这个框架在基础 AWS API 之上提供了一层抽象和配置,帮助我们更轻松地创建和消费云服务。在这本书中,我们将广泛使用 Serverless Framework,因此它应该变得熟悉。我们使用 NPM 安装 Serverless。打开控制台窗口并运行

$ npm install -g serverless

NPM 全局安装

使用 -g 标志运行 npm install 告诉 NPM 全局安装一个模块。这使得模块可在路径上可用,因此可以作为系统命令执行。

通过运行以下命令来检查 Serverless 是否成功安装

$ serverless -v

Serverless Framework

有几个框架可用于帮助支持无服务器开发。在撰写本文时,领先的框架是 Serverless Framework,它由 Node.js 实现。在底层,该框架使用 Node.js AWS API 来完成其工作,并且对于 AWS,它大量依赖于 CloudFormation。在本章中,我们将仅使用该框架,而不会详细介绍其工作原理。目前,我们需要理解的关键点是,该框架允许我们将基础设施和 Lambda 函数定义为代码,这意味着我们可以以类似管理系统其他源代码的方式管理我们的操作资源。

注意 如果您想了解更多关于 Serverless 的信息,我们已在附录 E 中提供了框架操作的深入了解。

提示 第六章涵盖了某些高级无服务器主题,并为您的项目提供了生产级别的模板。

2.3.2 设置清单

在我们继续编写代码之前,请查看此清单以确保一切就绪:

  • 附录 A

    • AWS 账户已创建

    • AWS 命令行已安装

    • AWS 访问密钥已创建

    • 开发外壳配置了访问密钥并已验证

  • 附录 D

    • Route 53 域已注册

    • SSL/TLS 证书已创建

  • 本章

    • Node.js 已安装

    • Serverless Framework 已安装

如果所有这些都已经就绪,我们就可以开始了!

警告 请确保完成此清单中的所有项目;否则,在尝试运行示例代码时可能会遇到问题。特别是,请确保已设置环境变量 AWS_REGIONAWS_DEFAULT_REGION,并且它们指向附录 A 中描述的同一 AWS 区域。

2.3.3 获取代码

现在我们已经完成了基本设置,我们可以继续获取系统的代码。本章的源代码存储在本仓库的chapter2-3子目录中:github.com/fourTheorem/ai-as-a-service。要开始,请先克隆此仓库:

$ git clone https://github.com/fourTheorem/ai-as-a-service.git

代码映射到您可能预期的架构。每个定义的服务都有一个顶级目录,如下所示。

列表 2.1 仓库结构

???  analysis-service
???  crawler-service
???  frontend-service
???  resources
???  ui-service

2.3.4 设置云资源

除了我们的服务文件夹外,我们还有一个名为resources的顶级目录。我们的系统依赖于许多云资源,在我们能够部署任何服务元素之前,我们需要这些资源已经就绪。对于我们的简单系统,我们需要一个 SQS 队列用于异步通信和一个 S3 存储桶来存储下载的图像。我们将使用专门的 Serverless Framework 配置文件来部署这些资源。让我们看看这是如何完成的。进入chapter2-3/resources资源目录,查看下一个列表中的serverless.yml文件。

列表 2.2 资源的服务器无配置

service: resources                                             ❶
 frameworkVersion: ">=1.30.0"
custom:                                                        ❷
   bucket: ${env:CHAPTER2_BUCKET}
  crawlerqueue: Chap2CrawlerQueue
  analysisqueue: Chap2AnalysisQueue
  region: ${env:AWS_DEFAULT_REGION, 'eu-west-1'}
  accountid: ${env:AWS_ACCOUNT_ID}

provider:                                                      ❸
   name: aws
  runtime: nodejs12.x
  stage: dev
  region: ${env:AWS_DEFAULT_REGION, 'eu-west-1'}

resources:
  Resources:
    WebAppS3Bucket:                                            ❹
       Type: AWS::S3::Bucket
      Properties:
        BucketName: ${self:custom.bucket}
        AccessControl: PublicRead
        WebsiteConfiguration:
          IndexDocument: index.html
          ErrorDocument: index.html
    WebAppS3BucketPolicy:                                      ❺
       Type: AWS::S3::BucketPolicy
      Properties:
        Bucket:
          Ref: WebAppS3Bucket
        PolicyDocument:
          Statement:
            - Sid: PublicReadGetObject
              Effect: Allow
              Principal: "*"
              Action:
                - s3:GetObject
              Resource: arn:aws:s3:::${self:custom.bucket}/*
    Chap2CrawlerQueue:                                         ❻
       Type: "AWS::SQS::Queue"
      Properties:
        QueueName: "${self:custom.crawlerqueue}"
    Chap2AnalysisQueue:
      Type: "AWS::SQS::Queue"
      Properties:
        QueueName: "${self:custom.analysisqueue}"

❶ 服务名称

❷ 自定义定义

❸ 特定提供者

❹ 存储桶定义

❺ 存储桶策略

❻ 队列定义

提示:Serverless 使用YAML文件格式进行配置。YAML 代表YAML Ain’t Markup Language;您可以在本网站上找到有关 YAML 的更多信息:yaml.org/

如果一开始看起来令人不知所措,请不要担心。我们将在这本书中一直使用 Serverless Framework,因此这些配置文件将变得非常熟悉。让我们看看这个文件的总体结构。

提示:Serverless Framework 及其配置的完整文档可以在项目的主要网站上找到:serverless.com/framework/docs/

服务器无配置被分解为几个顶级部分。其中关键的部分是

  • custom--定义在配置中其他地方使用的属性。

  • provider--定义框架中特定提供者的配置。在本例中,我们使用 AWS 作为提供者;然而,该框架支持多个云平台

  • functions--定义服务实现的功能端点。在本例中,我们没有要定义的功能,因此本例中不存在此部分。

  • resources--定义云平台上的支持资源。在本例中,我们定义了两个 SQS 队列和一个 S3 存储桶。当我们部署此配置时,Serverless Framework 将为我们创建队列和存储桶。

注意:还有许多其他工具可以用来部署云资源,例如 AWS CloudFormation 或 Hashicorp 的 Terraform,这两者都是管理基础设施代码的出色工具。如果你有一个基础设施密集型项目,我们建议调查这些工具。对于这本书,我们将几乎完全使用无服务器框架。我们还注意到,无服务器框架在 AWS 底层使用 CloudFormation;我们将在附录 E 中更详细地介绍这一点。

在我们继续部署资源之前,我们需要确定一个存储桶名称。AWS 存储桶名称空间是全球性的,因此你应该选择一个可用的名称,并在你的 shell 中添加一个额外的环境变量CHAPTER2_BUCKET,就像我们设置 AWS 环境变量一样:

export CHAPTER2_BUCKET=<YOUR BUCKET NAME>

<YOUR BUCKET NAME>替换为你选择的唯一名称。现在我们已经准备好了,让我们继续部署资源。在chapter2-3/resources目录的命令行中运行

$ serverless deploy

无服务器将自动部署我们的资源,你应该会看到类似以下列表的输出。

列表 2.3 无服务器部署输出

Serverless: Packaging service...
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
.....
Serverless: Stack create finished...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
..............
Serverless: Stack update finished...
Service Information
service: resources
stage: dev
region: eu-west-1
stack: resources-dev
api keys:
  None
endpoints:
  None
functions:
  None

无服务器已经为我们创建了一个 S3 存储桶和一个 SQS 队列。现在我们已经有了支持的基础设施,我们可以继续进行实际实现了!

2.4 实现异步服务

在完成基本设置后,我们可以继续编写我们的第一个服务。在本节中,我们将组合爬虫和分析异步服务,并在隔离状态下测试它们。

2.4.1 爬虫服务

首先,让我们看看crawler-service的代码。图 2.8 展示了该服务内部的流程。

图片

图 2.8 爬虫服务

当消息被放置在crawler队列中时,会调用crawler-service。消息包含服务要爬取的目标 URL。一旦被调用,爬虫会抓取指定 URL 的 HTML 页面,并解析出图像标签。然后,对于每个图像依次,它会将图像下载到 S3 文件夹中。最后,一旦所有图像都下载完毕,它会向analysis队列发送一个analyze消息,包括分析 URL 的域名以进行进一步处理。

爬虫服务的代码位于chapter2-3/crawler-service。进入这个目录,你应该会看到以下列出的文件:

handler.js
images.js
package.json
serverless.yml

为了了解此服务使用的资源及其整体结构,我们首先应该查看文件serverless.yml,其中包含下一列表中显示的配置。

列表 2.4 爬虫服务的serverless.yml

service: crawler-service
frameworkVersion: ">=1.30.0"
custom:
  bucket: ${env:CHAPTER2_BUCKET}                              ❶
  crawlerqueue: Chap2CrawlerQueue                             ❷
  analysisqueue: Chap2AnalysisQueue
  region: ${env:AWS_DEFAULT_REGION, 'eu-west-1'}
  accountid: ${env:AWS_ACCOUNT_ID}                            ❸

provider:
  name: aws
  runtime: nodejs12.x
  stage: dev
  region: ${env:AWS_DEFAULT_REGION, 'eu-west-1'}
  iamRoleStatements:
    - Effect: Allow                                           ❹
      Action:
        - s3:PutObject
      Resource: "arn:aws:s3:::${self:custom.bucket}/*"
    - Effect: Allow
      Action:
        - sqs:ListQueues
      Resource: "arn:aws:sqs:${self:provider.region}:*:*"
    - Effect: Allow                                           ❺
      Action:
        - sqs:ReceiveMessage
        - sqs:DeleteMessage
        - sqs:GetQueueUrl
      Resource: "arn:aws:sqs:*:*:${self:custom.crawlerqueue}"
    - Effect: Allow                                           ❻
      Action:
        - sqs:SendMessage
        - sqs:DeleteMessage
        - sqs:GetQueueUrl
      Resource: "arn:aws:sqs:*:*:${self:custom.analysisqueue}"

functions:
  crawlImages:                                                ❼
    handler: handler.crawlImages
    environment:
      BUCKET: ${self:custom.bucket}
      ANALYSIS_QUEUE: ${self:custom.analysisqueue}
      REGION: ${self:custom.region}
      ACCOUNTID: ${self:custom.accountid}
    events:
      - sqs:                                                  ❽
          arn: "arn:aws:sqs:${self:provider.region}:${env:AWS_ACCOUNT_ID} \
          :${self:custom.crawlerqueue}"

❶ S3 存储桶名称

❷ SQS 队列名称

❸ 本地环境中的账户 ID

❹ S3 权限

❺ 允许从爬虫队列接收

❻ 允许向分析队列发送

❼ 定义处理函数的入口点

❽ 由爬虫队列触发的函数

此配置的效果是定义并部署我们的爬虫服务函数到 AWS,并允许它通过资源配置中部署的爬虫 SQS 队列触发。关键部分如下

  • custom--定义在配置中其他地方使用的属性。

  • provider--在此配置中的提供者部分设置 AWS 权限,允许服务访问 SQS 队列,并授予它写入我们 S3 存储桶的权限。

  • functions--此部分定义了服务 Lambda。处理器设置引用了实现,我们将在稍后查看。事件条目将函数连接到我们之前部署的 SQS 爬虫队列。最后,环境块定义了将可用于我们的函数的环境变量。

注意:iamRoleStatements块中定义的权限直接映射到 AWS 身份和访问管理(IAM)模型。关于这方面的完整文档可以在 AWS 上找到,网址为aws.amazon.com/iam

与我们资源之前的serverless.yml文件不同,此文件没有定义任何资源。这是因为我们选择在服务范围之外定义我们的资源。一般来说,一个很好的经验法则是,全局或共享资源应该部署在公共资源堆栈中;用于单个服务的资源应该与该特定服务一起部署。

在无服务器 YAML 文件中的“资源”部分定义了在部署时将创建的资源。依赖于此资源的其他服务必须在资源创建后部署。我们发现将全局资源放在单独的配置中是最好的做法。

让我们现在看看爬虫的主要实现文件handler.js。在文件顶部,我们包含了一些模块,如下所示。

列表 2.5 爬虫 handler.js 所需模块

const request = require('request') 1((CO3-1))          ❶
const urlParser = require('url')                       ❷
const AWS = require('aws-sdk')                         ❸
const s3 = new AWS.S3()
const sqs = new AWS.SQS({region: process.env.REGION})
const images = require('./images')()                   ❹

❶ request 是一个实现功能齐全的 HTTP 客户端的 node 模块。

❷ url 是一个核心 node 模块,它理解如何解析 URL。

❸ 包含 AWS SDK 节点模块。在这种情况下,我们实例化了 S3 和 SQS 对象,以便分别与我们的 S3 存储桶和队列进行接口。

❹ ./images 指的是文件 images.js 中的我们自己的模块。

进入此服务的主要入口点是crawlImages。此函数接受三个参数:eventcontextcb。下面的代码展示了这一部分。

列表 2.6 爬虫服务入口点

module.exports.crawlImages = function (event, context, cb) {
  asnc.eachSeries(event.Records, (record, asnCb) => {           ❶
    let { body } = record

    try {
      body = JSON.parse(body)
    } catch (exp) {
      return asnCb('message parse error: ' + record)
    }

    if (body.action === 'download' && body.msg && body.msg.url) {
      const udomain = createUniqueDomain(body.msg.url)
      crawl(udomain, body.msg.url, context).then(result => {    ❷
        queueAnalysis(udomain, body.msg.url, context).
        then(result => {                                        ❸
          asnCb(null, result)
        })
      })
    } else {
      asnCb('malformed message')
    }
  }, (err) => {
    if (err) { console.log(err) }
    cb()
  })
}

❶ 遍历消息

❷ 爬取图像的 URL。

❸ 向 SQS 发送消息以触发分析

函数接受以下三个参数:

  1. event--提供有关正在处理的当前事件的详细信息。在这种情况下,事件对象包含从 SQS 队列中取出的记录数组。

  2. context--由 AWS 用于提供调用上下文信息,例如可用内存量、执行时间和客户端调用上下文。

  3. cb--回调函数。处理完成后,应由处理程序调用此函数并传递结果。

回调和异步 I/O

回调函数是 JavaScript 的一个基本组成部分,允许代码异步执行并通过执行传入的回调参数返回结果。回调对于异步 I/O(与同步 I/O 相比)是一个自然的语法匹配,这也是 Node.js 平台成功的原因之一。如果你需要复习 JavaScript 函数和回调,我们可以推荐 Node School 的“Javascripting”教程,该教程可在 nodeschool.io/ 找到。

最后,对于捕获服务,让我们简要看看下一个列表中所示的 package.json 文件。此文件提供了一组 Node 模块依赖项。

列表 2.7 捕获服务 package.json

{
  "name": "crawler-service",
  "version": "1.0.0",              ❶
  "description": "",
  "main": "handler.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "async": "³.2.0",
    "aws-sdk": "².286.2",        ❷
    "htmlparser2": "³.9.2",
    "request": "².87.0",
    "shortid": "².2.15",
    "uuid": "³.3.2"
  }
} 

❶ 设置模块版本号

❷ 设置 aws-sdk 模块版本

package.json

虽然 package.json 文件的格式相对简单,但也有一些细微差别,例如语义版本支持脚本。在这里描述全部细节超出了本书的范围。关于这个主题的深入覆盖可以在 NPM 的 docs.npmjs.com/files/package.json 找到。

这个入口函数相当简单。它只是调用 crawl 函数从事件对象中提供的 URL 下载图像,一旦捕获完成,它将一条消息排队到 SQS,表明下载的图像已准备好进行分析。

crawl 函数在下面的列表中展示。

列表 2.8 crawl 函数

function crawl (url, context) {
  const domain = urlParser.parse(url).hostname                         ❶

  return new Promise(resolve => {
    request(url, (err, response, body) => {                            ❷
      if (err || response.statusCode !== 200) {
        return resolve({statusCode: 500, body: err})
      }
      images.parseImageUrls(body, url).then(urls => {                  ❸
        images.fetchImages(urls, domain).then(results =>               ❹
          writeStatus(url, domain, results).then(result => {           ❺
            resolve({statusCode: 200, body: JSON.stringify(result)})
          })
        })
      })
    })
  })
}

❶ 从请求的 URL 中提取域名部分。

❷ 使用请求模块获取给定 URL 的 HTML。

❸ 解析的 HTML 内容被传递给 parseImageUrls 函数,该函数返回一个用于下载的图像列表。

❹ 将图像列表传递给 fetchImages 函数,该函数将每个图像下载到指定的存储桶。

❺ 最后,该函数将状态文件写入存储桶,以便下游服务在解析承诺之前消费。

Promises 和箭头函数

如果你稍微有些 JavaScript 实践不足,你可能想知道构造 .then(result => {...的含义。箭头函数操作符是function` 关键字的替代(略有变化)。出于实用目的,你可以将以下内容视为等价:

result => { console.log(result) }
function (result) { console.log(result) }

.then 构造定义了一个在承诺解析时被调用的处理函数。承诺为异步 I/O 提供了替代回调的机制。许多人更喜欢使用承诺而不是回调,因为它有助于使代码更干净,并避免俗称的“回调地狱”。如果你不熟悉承诺,可以在 www.promisejs.org/ 找到全部细节。

下一个列表中显示的queueAnalysis函数使用 AWS SQS 接口向分析队列发送消息,该消息稍后将被分析服务获取。

列表 2.9 queueAnalysis函数

function queueAnalysis (url, context) {
  let domain = urlParser.parse(url).hostname
  let accountId = process.env.ACCOUNTID
  if (!accountId) {
    accountId = context.invokedFunctionArn.split(':')[4]
  }
  let queueUrl = `https://sqs.${process.env.REGION}.amazonaws.com/
    ${accountId}/
    ${process.env.ANALYSIS_QUEUE}`                 ❶

  let params = {                                   ❷
    MessageBody: JSON.stringify({action: 'analyze', msg: {domain: domain}}),
    QueueUrl: queueUrl
  }

  return new Promise(resolve => {
    sqs.sendMessage(params, (err, data) => {       ❸
    ...
    })
  })
}

❶ 构建 SQS 端点 URL。

❷ 构建消息正文。

❸ 将消息发布到 SQS。

现在我们已经理解了爬虫的代码,让我们部署这个服务。首先我们需要安装支持节点模块。为此,cdcrawler-service目录并运行

$ npm install

我们现在可以通过运行 Serverless Framework 的deploy命令来部署我们的服务:

$ serverless deploy

一旦此命令完成,我们可以通过检查 AWS Lambda 控制台来确认一切正常,它应该类似于图 2.9。

图片

图 2.9 爬虫服务 Lambda

在我们继续分析函数之前,让我们通过向 SQS 发送消息来测试爬虫。打开 AWS 控制台,转到 SQS 服务页面,并在相应区域中选择Chap2CrawlerQueue。然后从队列操作下拉菜单中选择发送消息。

图片

图 2.10 发送 SQS 消息

将此处显示的 JSON 粘贴到消息窗口中,然后点击发送消息:

{
  "action": "download",
  "msg": {
    "url": "http://ai-as-a-service.s3-website-eu-west-1.amazonaws.com"
  }
}

注意 我们为了测试目的创建了一个简单的静态网站,使用 S3,并在测试消息中的 URL 上放置了一些示例图片,但如果你更喜欢,可以使用不同的 URL--例如,谷歌图片搜索的结果。

消息将被附加到 SQS 队列,并由爬虫服务获取。我们可以查看爬虫日志以确认这一点。打开 AWS 控制台,然后打开 CloudWatch。点击左侧的日志菜单项,然后选择爬虫服务,列出的为crawler-service -dev-crawlimages,以检查日志。你应该会看到类似于图 2.11 的输出。

图片

图 2.11 爬虫的 CloudWatch 日志

最后让我们检查我们的图片是否正确下载。打开 AWS 控制台,转到 S3 服务。选择您的存储桶。您应该看到一个名为ai-as-a-service.s3-website-eu-west-1.amazonaws.com的文件夹。点击进入以查看下载的图片,如图 2.12 所示。

图片

图 2.12 下载的图片

在下一章中,我们将把注意力转向分析服务,并完成异步服务的部署,然后再部署系统的其余部分。现在,好好休息一下,并祝贺自己到目前为止的辛勤工作!

摘要

  • AWS 提供了一系列不断增长的云原生服务,我们可以利用。在本章中,我们使用了 S3、Route53、Lambda 和 SQS。

  • AWS 提供了一个基于 Web 的控制台,我们可以使用它来设置账户并配置 API 访问密钥

  • 无服务器框架用于部署云基础设施,包括 S3 存储桶、SQS 队列和 Route53 DNS 记录。一个serverless.yml文件允许我们以可预测和逻辑的方式定义和部署我们的基础设施。

  • 一个 SQS 队列连接到一个爬虫 Lambda 函数。

  • 爬虫服务是一个 Lambda 函数,它下载图片并将它们放置在 S3 存储桶中。

警告 第三章将继续构建这个系统,并在第三章末尾提供如何删除已部署资源的说明。如果你在一段时间内不打算处理第三章,请确保你完全删除本章中部署的所有云资源,以避免额外费用!

3 构建无服务器图像识别系统,第二部分

本章涵盖了

  • 构建简单的 AI as a Service 系统

  • 消费人工智能图像识别服务

  • 实现同步和异步服务

  • 部署用户界面

  • 部署到云端

在本章中,我们将继续构建我们在第二章开始的服务器端无服务器图像识别系统。我们将添加我们的图像识别服务,该服务将调用 AWS Rekognition 来为我们完成繁重的工作。一旦完成,我们将为我们的系统构建一个简单的前端,以便我们可以测试我们的图像识别能力。

如果你还没有完成第二章的内容,你应该在继续本章之前返回并完成它,因为我们将直接基于那里开始的工作进行构建。如果你对第二章的内容感到满意,我们可以直接从我们离开的地方继续,并部署分析服务。

3.1 部署异步服务

在第二章中,我们设置了我们的开发环境并部署了爬虫服务。在本章中,我们将继续部署系统的其余部分,从分析服务开始。

3.1.1 分析服务

让我们来看看 analysis-service。与 crawler-service 类似,当我们的 S3 存储桶中有可供分析图像时,该服务会由 Analysis SQS 队列的消息触发。该服务的逻辑概述如图 3.1 所示。

图 3.1 分析服务

从本质上讲,analysis-service 在下载的图像和亚马逊 Rekognition 服务之间形成了一座桥梁。crawler-service 下载的每一张图像都会输入到 Rekognition 中,并返回一组标签。每个标签都是一个描述图像中由模型识别的对象的词,以及一个置信度水平(一个介于 0 和 100 之间的数字,其中 100 表示对图像标签的完全置信)。

在此分析之后,该服务处理返回的数据以创建一组词频,这些词频可以输入到词云生成器中。背后的想法是尝试在给定 URL 上可用的图像之间视觉上确定一个共同的主题。

让我们来看看 analysis-service 中的代码,从 serverless.yml 配置开始,看看这是如何实现的。这将在下一列表中展示。

图 3.1 分析服务 serverless.yml

service: analysis-service
custom:
  bucket: ${env:CHAPTER2_BUCKET}
  ...

provider:
  ...
  iamRoleStatements:
    - Effect: "Allow"               ❶
      Action:
        - "rekognition:*"
      Resource: "*"
  ...

functions:
  analyzeImages:                    ❷
    handler: handler.analyzeImages
    ...

❶ 允许访问 Rekognition API。

❷ 定义主入口点

我们应该注意,这个服务的 serverless.yml 配置文件与之前的非常相似。主要区别在于它允许 Lambda 函数访问 Rekognition API。让我们看看这个接口是如何工作的。analysis-service 代码中的 handler.js 文件实现了这个接口。

以下列表显示了 analysis-servicerequire 语句。

图 3.2 分析服务 require

const AWS = require('aws-sdk')       ❶
const s3 = new AWS.S3()              ❷
const rek = new AWS.Rekognition()    ❸

❶ AWS SDK 模块已加载。

❷ S3 接口是为了处理存储桶及其对象而创建的。

❸ 创建 Rekognition 接口。

下一个列表显示了如何在analyzeImageLabels函数中使用 Rekognition 对象。

图 3.3 使用 Rekognition API

function analyzeImageLabels (imageBucketKey) {
  const params = {                                ❶
    Image: {
      S3Object: {
        Bucket: process.env.BUCKET,
        Name: imageBucketKey
      }
    },
    MaxLabels: 10,
    MinConfidence: 80
  }
  return new Promise((resolve, reject) => {
    rek.detectLabels(params, (err, data) => {    ❷
      if (err) {
        return resolve({image: imageBucketKey, labels: [], err: err})
      }
      return resolve({image: imageBucketKey,
        labels: data.Labels})                    ❸
    })
  })
}

❶ 创建 Rekognition 调用参数。

❷ 调用 Rekognition 的 detectLabel API。

❸ 承诺解析为结果。

这个简单的函数实现了很多功能!它触发了一个图像识别 AI 服务对存储在 S3 存储桶中的图像文件进行运行,然后返回一组结果以供进一步处理。所有这些都在相当少的代码中完成!

应该注意的是,我们可以用 Rekognition 做很多事情;然而,出于这个代码的目的,我们只是使用了默认设置。我们将在后面的章节中更详细地探讨这一点。

小贴士:Rekognition 不仅适用于视频,也适用于静态图像,并且可以用于检测图像中的各种特征,如微笑或皱眉的脸,图像中的文本,以及知名人士。你能想到哪些对你的最终用户有益的应用?例如,我们最近用它来检测图像中的地址和邮政编码。

analysis-service的最终列表显示了wordCloudList函数。该函数计算所有检测到的标签中单词出现的次数。

图 3.4 wordCloudList计算

function wordCloudList (labels) {            ❶
  let counts = {}
  let wcList = []

  labels.forEach(set => {                    ❷
    set.labels.forEach(lab => {
      if (!counts[lab.Name]) {
        counts[lab.Name] = 1
      } else {
        counts[lab.Name] = counts[lab.Name] + 1
      }
    })
  })

  Object.keys(counts).forEach(key => {       ❸
    wcList.push([key, counts[key]])
  })
  return wcList
}

❶ 函数接受一个标签对象的数组。

❷ 遍历每个标签对象中设置的标签集以计算标签出现次数。

❸ 将计数映射转换为表示为两个元素数组的单词-计数对的数组。

让我们继续使用 Serverless Framework 部署分析服务:

$ cd analysis-service
$ npm install
$ serverless deploy

一旦部署成功完成,我们可以通过 AWS 控制台将一个测试消息排队到 SQS 中,重新运行我们的系统。请继续这样做,发送与之前相同的 JSON 消息:

{
  "action": "download",
  "msg": {
    "url": "http://ai-as-a-service.s3-website-eu-west-1.amazonaws.com"
  }
}

这将导致爬虫服务运行。一旦完成,爬虫将向分析 SQS 队列发送一条消息,请求对下载的图像进行分析,这将触发分析服务。最终结果将是一组标签添加到 S3 中的status.json文件中。如果你打开这个文件,你应该会看到以下类似的列表。

图 3.5 wordCloudList计算结果

{
  "url": "http://ai-as-a-service.s3-website-eu-west-1.amazonaws.com",
  "stat": "analyzed",
  "downloadResults": [         ❶
    {
      "url": "http://ai-as-a-service.s3-website-eu-west-1.amazonaws.com/cat1.png",
      "stat": "ok"
    },
    ...
  ],
  "analysisResults": [         ❷
    {
      "image": "ai-as-a-service.s3-website-eu-west-1.amazonaws.com/cat1.png",
      "labels": [
        {
          "Name": "Cat",
          "Confidence": 99.03962707519531
        }
      ]
      ...
  ],
  "wordCloudList": [           ❸
    [ "Cat", 3 ],
    [ "Dog", 3 ],
    ....
  ]
}

❶ 图像下载结果

❷ 图像分析结果

❸ 单词云计算

对于一个更完整的系统,我们可能会考虑将此信息存储在数据库或键/值存储中;然而,对于这个第一个演示,S3 就足够好了。此状态文件用于驱动前端和 UI 服务,我们现在将关注这些服务。

3.2 实现同步服务

在这个系统中,同步服务由 UI 服务和前端组成。前端在浏览器中渲染和执行,而 UI 服务作为三个 Lambda 函数执行。

3.2.1 UI 服务

图 3.2 概述了 UI 服务的操作。

如上图所示,UI 服务公开了三个端点:

  • url/list 列出已提交进行分析的所有 URL。

  • image/list 列出针对特定 URL 已分析的所有图像。

  • url/analyze 提交 URL 以进行分析。

Serverless 框架允许我们在单个配置文件中定义多个 Lambda 函数,我们已经在 UI 服务的配置中使用了这一点。让我们看一下 UI 服务的 serverless.yml 文件,它将在下一个列表中展示。

图 3.2 UI 服务

图 3.6 UI 服务 serverless.yml

service: ui-service
frameworkVersion: ">=1.30.0"
plugins:
  - serverless-domain-manager                        ❶
custom:
  bucket: ${env:CHAPTER2_BUCKET}
  queue: Chap2Queue
  region: ${env:AWS_DEFAULT_REGION, 'eu-west-1'}
  domain: ${env:CHAPTER2_DOMAIN}
  accountid: ${env:AWS_ACCOUNT_ID}
  customDomain:                                      ❷
    domainName: 'chapter2api.${self:custom.domain}'
    stage: dev
    basePath: api
    certificateName: '*.${self:custom.domain}'
    createRoute53Record: true
    endpointType: regional

provider:
  name: aws
  runtime: nodejs12.x
  region: ${env:AWS_DEFAULT_REGION, 'eu-west-1'}
  iamRoleStatements:                                 ❸
  ...

functions:
  analyzeUrl:                                        ❹
    handler: handler.analyzeUrl
    environment:
     ...
    events:
      - http:
          path: url/analyze
          method: post
  listUrls:                                          ❺
    handler: handler.listUrls
    ...
  listImages:                                        ❻
     handler: handler.listImages
    ....

❶ 域插件

❷ 自定义域名设置

❸ 角色权限

❹ 分析 URL Lambda HTTP POST

❺ 列出 URL Lambda

❻ 列出图像 Lambda

此配置在之前的配置文件之上引入了一些新元素。首先,我们使用了一个自定义插件--serverless-domain-manager。我们使用它来帮助我们为我们的服务设置一个自定义域名。如果您还记得,在第二章的开始,我们在 Route53 中设置了一个域名并创建了一个通配符证书。一会儿,我们将使用此域名来为我们的 UI 服务。

在此阶段,配置中的权限部分应该看起来很熟悉。函数部分略有不同,因为它有三个条目。请注意,每个条目都是相似的,因为它与一个 HTTP 事件相关联。这告诉 Serverless 将函数绑定到 API Gateway,并通过给定的路由使函数可用。自定义域名条目用于为服务创建 DNS 条目,并将其连接到 API Gateway。我们将在一会儿部署此服务,但首先让我们看看实现,它在 handler.js 文件中。

在现在应该已经熟悉的模式中,我们引入 AWS SDK 并创建服务所需的对象,在这种情况下是 S3 和 SQS。这将在以下列表中展示。

图 3.7 UI 服务 require

const urlParser = require('url')    ❶
const AWS = require('aws-sdk')      ❷
const s3 = new AWS.S3()
const sqs = new AWS.SQS({region: process.env.REGION})

❶ 加载 url 节点模块以进行 URL 解析。

❷ 加载 AWS SDK 并实例化 S3 和 SQS 接口。

该服务定义了三个入口点,它们将被部署为三个单独的 Lambda 函数。listUrl 函数将在下一个列表中提供。

图 3.8 listUrls 函数

module.exports.listUrls = (event, context, cb) => {     ❶
  const params = {
    Bucket: process.env.BUCKET,
    Delimiter: '/',
    MaxKeys: 1000
  }

  s3.listObjectsV2(params, (err, data) => {             ❷
    let promises = []
    if (err) { return respond(500, {stat: 'error', details: err}, cb) }

    data.CommonPrefixes.forEach(prefix => {
      promises.push(readStatus(prefix.Prefix))
    })
    Promise.all(promises).then(values => {
      let result = []
      values.forEach(value => {
        result.push({url: value.url, stat: value.stat})
      })
      respond(200,
        {stat: 'ok', details: result}, cb)              ❸
    })
  })
}

❶ 入口点

❷ 列出 S3 对象

❸ 返回 URL 列表

注意,此函数的入口点与我们的所有其他服务完全相同,尽管在这种情况下,该函数将通过 API Gateway 作为 HTTP GET 请求执行。该函数相当简单,它只是列出我们 S3 存储桶顶层的一组文件夹,并将列表作为 JSON 数组返回。

我们的 listImages 函数更加简单,因为它从 S3 读取 status.json 文件并返回用于显示的内容,所以我们在这里不会详细说明它。相反,让我们看一下以下代码中的 analyzeUrl 函数。

图 3.9 analyzeUrl

module.exports.analyzeUrl = (event, context, cb) => {
  let accountId = process.env.ACCOUNTID
  if (!accountId) {
    accountId = context.invokedFunctionArn.split(':')[4]
  }
  const queueUrl = `https://sqs.${process.env.REGION}.amazonaws.com/
    ${accountId}/
    ${process.env.QUEUE}`                    ❶

  const body = JSON.parse(event.body)

  const params = {
    MessageBody: JSON.stringify({action: 'download', msg: body}),
    QueueUrl: queueUrl
  }

  sqs.sendMessage(params, (err, data) => {   ❷
     if (err) { return respond(500, {stat: 'error', details: err}, cb) }
    respond(200,
      {stat: 'ok', details: {queue: queueUrl, msgId: data.MessageId}}, cb)
  })
}

❶ 构建队列 URL

❷ 发送 SQS 消息

再次,这个函数相当直接。它将 URL 作为事件体,并将此 URL 作为消息有效负载的一部分发布到我们的 SQS 队列,由爬虫服务处理。

单一职责原则

单一职责原则SRP是一个强大的理念,它帮助我们保持代码的解耦和良好的维护。希望你能看到,到目前为止的所有代码都遵循了 SRP。我们喜欢将 SRP 视为适用于多个层面的东西:

  • 在架构层面,每个服务应该有一个单一的目的。

  • 在实现层面,每个函数应该有一个单一的目的。

  • 在“代码行”级别,每一行只应该做一件事。

我们所说的“代码行”级别是什么意思呢?嗯,以下代码在一行中做了多件事,因为它获取了bar的值并对其与foo进行了测试:

if (foo !== (bar = getBarValue())) {

一个更清晰的实现是将代码拆分为两行,这样每行只做一件事:

bar = getBarValue()
if (foo !== bar) {

现在我们已经了解了 UI 服务代码,让我们继续部署它。首先,我们需要创建自定义域名条目。serverless.yml文件使用环境变量CHAPTER2_DOMAIN作为ui-service部署的基础域名。如果你还没有设置此变量,你应该现在设置它,通过将以下列表的内容添加到你的 shell 启动脚本中。

图 3.10 设置基础域的环境变量

export CHAPTER2_DOMAIN=<MY CUSTOM DOMAIN>

<MY CUSTOM DOMAIN>替换为章节开头创建的域名。

接下来我们需要安装支持节点模块。为此,请使用cd命令进入ui-service目录,并安装以下依赖项:

$ npm install

这将在本地安装package.json中的所有依赖项,包括serverless-domain-manager。要创建我们的自定义域名,请运行

$ serverless create_domain

此命令将导致域名管理器插件在Route53中创建域名。例如,如果你的自定义域名是example.com,那么这将创建一个指向chapter2api.example.comA记录,如serverless.yml中的customDomain部分所述。此部分将在下一列表中展示。

图 3.11 serverless.ymlui-service的定制部分

custom:
  bucket: ${env:CHAPTER2_BUCKET}
  queue: Chap2Queue
  region: ${env:AWS_DEFAULT_REGION, 'eu-west-1'}
  domain: ${env:CHAPTER2_DOMAIN}                      ❶
  accountid: ${env:AWS_ACCOUNT_ID}
  customDomain:
    domainName: 'chapter2api.${self:custom.domain}'   ❷
    stage: dev
    basePath: api
    certificateName: '*.${self:custom.domain}'        ❸
    createRoute53Record: true
    endpointType: regional

❶ 自定义域环境变量

❷ 完整域名

❸ 证书引用

注意,你需要APIGatewayAdministrator权限才能成功执行此操作。如果你创建了一个新的 AWS 账户,那么这应该默认启用。最后,我们还需要以通常的方式部署服务:

$ serverless deploy

这将部署我们的 UI 端点作为 Lambda 函数,配置 API Gateway 以调用这些函数,并将我们的自定义域名绑定到 API Gateway。最终结果是,我们的函数现在可以通过 HTTP 进行调用,地址为chapter2api.<YOUR CUSTOM DOMAIN>/api/url/list。要测试这一点,请打开网页浏览器并将它指向该 URL。你应该会看到以下输出:

{"stat":"ok","details":[{"url":"http://ai-as-a-service.s3-website-eu-west-1.amazonaws.com","stat":"analyzed"}]}

这是因为我们迄今为止只提交了一个用于下载和分析的单个 URL,并且 UI 服务正在返回一个元素列表。

3.2.2 前端服务

我们系统的最后一部分是前端服务。它与系统其他部分的不同之处在于,它纯粹是一个前端组件,并在用户的浏览器中完全执行。图 3.3 概述了前端服务的结构。

图片

图 3.3 前端

我们将把这个服务作为一组静态文件部署到 S3。首先让我们看看代码。cdfrontend-service目录。您应该会看到下一列表中显示的结构。

列表 3.12 前端结构

???  app
???  code.js
???  index.html
???  templates.js
???  wordcloud2.js

在这种情况下,我们不需要 serverless.yml 配置,因为我们只是将前端部署到 S3 存储桶。前端代码包含在app目录中,该目录包含我们应用的 HTML 和 JavaScript。在这个例子中,我们的应用是一个所谓的单页应用(SPA)的例子。有许多框架可以帮助构建大规模 SPA 应用,如 Angular、React 或 Vue。对于我们的简单应用,我们只是使用了 jQuery,因为它提供了一个简单的最低共同点,并且我们的应用足够简单,不需要前端框架的支持。

单页应用

单页应用架构的特点是动态地将内容重写为单页,而不是重新加载网页以渲染新内容。这种方法越来越受欢迎,并被广泛应用于大多数现代网络应用中。实际上,正是这种应用模式的兴起,部分推动了您可能遇到的前端 JavaScript 框架(如 Angular、React、Vue 等)的发展。

如果您不熟悉这种方法,我们建议您阅读这本电子书来提高对这个主题的了解:github.com/mixu/singlepageappbook

注意:对于这个示例系统,我们将直接使用 S3 来提供我们的应用。为了实现规模化的操作,通常的做法是将 S3 存储桶作为 Amazon CloudFront CDN(内容分发网络)的源。

实现前端代码相对简单,仅由一个 HTML 页面和一些 JavaScript 组成。让我们快速查看以下列表中的索引页面。

图 3.13 前端index.html

<html>
<head>
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/
bootstrap/4.1.3/css/bootstrap.min.css">                                ❶
  <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/
bootstrap.min.js"></script>
  <script src="/templates.js"></script>                                ❷
  <script src="/code.js"></script>
  <script src="/wordcloud2.js"></script>
</head>
<body>
                                                                       ❸
<div class="navbar navbar-expand-lg navbar-light bg-light">
  ...
</div>

<div id="content"></div>                                               ❹

</body>
</html>

❶ CDN 库

❷ 应用代码

❸ 定义导航栏

❹ 主要内容区域

在页面的 head 部分中,我们加载了一些标准库,如 jQuery 和 Bootstrap,来自共享 CDN。这只是方便一下。对于生产级 Web 应用程序,我们通常会自己重新分发这些库以确保它们的完整性。该页面的主要标记定义了一个简单的导航栏,然后声明一个内容区域,该区域将由应用程序代码填充,其中大部分代码在 code.js 文件中。

jQuery、Bootstrap 和 CDN

如果你对前端 JavaScript 开发不熟悉,你可能想知道 HTML 文件中 Bootstrap 和 jQuery 的链接。为了方便用户,这两个项目都提供了它们库的主要版本的主机、压缩版本,并在快速内容分发网络上提供,以便外部应用程序使用。

为了清晰起见,我们从下一列表中的代码中删除了一些细节。完整的代码可在 Git 仓库中找到。

图 3.14 前端应用程序的主要 JavaScript

const BUCKET_ROOT = '<YOUR BUCKET URL>'                            ❶
const API_ROOT = 'https://chapter2api.<YOUR CUSTOM DOMAIN>/api/'   ❷

function renderUrlList () {
  $.getJSON(API_ROOT + 'url/list', function (body) {
    ...                                                            ❸
  })
}

function renderUrlDetail (url) {
  let list = ''
  let output = ''
  let wclist = []

  $.getJSON(API_ROOT + 'image/list?url=' + url, function (data) {
    ...                                                            ❹
  })
}

$(function () {
  renderUrlList()

  $('#submit-url-button').on('click', function (e) {
    e.preventDefault()
    $.ajax({url: API_ROOT + 'url/analyze',                         ❺
      type: 'post',
      data: JSON.stringify({url: $('#target-url').val()}),
      dataType: 'json',
      contentType: 'application/json',
      success: (data, stat) => {
      }
    })
  })
})

❶ 定义存储桶 URL 根。

❷ 定义 UI API 根。

❸ 获取并渲染 URL。

❹ 获取并渲染图像。

❺ 发送一个 URL 进行分析。

代码使用标准的 jQuery 函数向刚刚部署的 UI 服务发送 AJAX 请求。它在页面加载时渲染已分析的 URL 列表,以及针对特定 URL 分析的图像列表。最后,它允许用户提交新的 URL 进行分析。在我们部署前端之前,你应该编辑 code.js 文件并替换以下行:

  • const BUCKET_ROOT = '<YOUR BUCKET URL>' 应替换为你的特定存储桶的 URL;例如,s3-eu-west-1.amazonaws.com /mybucket

  • const API_ROOT = 'https://chapter2api.<YOUR CUSTOM DOMAIN>/api/' 应替换为你的特定自定义域名。

现在已经完成了,我们可以继续部署前端。为此,我们将使用本章开头设置的 AWS 命令行。运行以下命令:

$ cd frontend-service
$ aws s3 sync app/ s3://$CHAPTER2_BUCKET

注意:对于这个例子,我们将前端部署到与我们的抓取数据相同的存储桶中。我们不建议你在生产系统中这样做!

我们现在已经构建并部署了一个完整的无服务器 AI 系统;在下一节中,我们将对其进行测试!

3.3 运行系统

现在我们已经完全部署了我们的系统,是时候尝试一下了。为此,打开一个网页浏览器并将其指向 <YOURBUCKETNAME>.s3.amazonaws.com/index.html。索引页面应该加载并显示在测试部署期间分析的单个 URL,如图 3.4 所示。

图 3.4 带有一个 URL 的默认着陆页

让我们看看我们的图像分析系统在其他图像上的表现。鉴于互联网上充斥着猫图片,请将你的浏览器指向 Google 并搜索“猫图片”——然后点击“图片”标签。它应该看起来像图 3.5。

图片

图 3.5 Google Images 上的猫图片

从地址栏复制 URL,回到着陆页,将其粘贴到目标 URL 字段中。然后点击分析按钮。几秒钟后,刷新页面:你应该在列表中看到一个状态为已分析的www.google.com条目,如图 3.6 所示。

图片

图 3.6 带有 Google Images 分析的着陆页

点击新分析数据集的链接,系统将显示由 Rekognition 分析过的图像列表,以及我们在前端生成的词云。这如图 3.7 所示。

图片

图 3.7 带有 Google Images 分析的着陆页

该系统在识别我们的猫图像方面相当成功;然而,在某些情况下,它完全失败了。每个成功处理的图像都将有一个相关的标签列表。每个标签有两个组成部分:一个单词和一个 100 分中的分数。这个数字是置信度,是 AI 认为单词与图像匹配准确性的度量。看看无法识别的图像很有趣;例如,图 3.8 中的图像。对于猫背部的图像无法做出准确判断可能并不令人惊讶!

图片

图片

恭喜!你现在已经部署并运行了你的第一个无服务器 AI 系统!

在本章中,我们从零开始,构建了一个完全工作的无服务器 AI 系统,该系统能够从任意网页中识别图像。虽然这只是一场旋风般的游览,但我们希望这能说明,复杂的 AI 功能现在对没有专业知识的人来说也是可用的。

请记住,我们只是触及了 Rekognition 和图像识别技术所能做到的一小部分。希望你现在正在思考如何在你的工作中使用这项功能。我们遇到的一些用例包括

  • 从图像中提取姓名和邮政编码信息

  • 验证上传的个人信息照片是否为有效的人类面部

  • 通过描述当前视野中的物体来帮助盲人或视力受损的人

  • Google 反向图像搜索,允许人们搜索视觉上相似的图像

  • 通过拍摄瓶子上标签的图片来识别酒的类型、价格和价值

可能性是无限的,我们肯定会在这一技术周围看到新的商业出现。

希望你能同意,我们能够用相对较少的代码实现了很多功能,让云基础设施为我们承担重担。我们还应该指出,这个图像识别系统可以不使用神经网络或深度学习的专业知识来构建。我们将在本书的其余部分扩展这一 AI 工程方法。

3.4 移除系统

一旦你完成对系统的测试,它应该被完全移除,以避免产生额外费用。这可以通过使用 Serverless 的remove命令非常简单地实现。在chapter2-3代码目录中有一个名为remove.sh的脚本,它会为你完成移除工作。这将从 S3 中移除前端,并拆除所有相关资源。要使用它,请运行

$ cd chapter2-3
$ bash remove.sh

如果你希望在任何时候重新部署系统,同一文件夹中有一个名为deploy.sh的关联脚本。这将通过自动化我们在本章中工作的步骤为你重新部署整个系统。

摘要

  • 一个分析服务消费一个图像识别 AI 服务。我们使用 AWS Rekognition 的detectLabels API 来检测每张图像中的标记对象。

  • 创建了一个简单的 API 来与我们的分析系统交互。我们使用 API 网关为无服务器服务提供外部端点。

  • 前端单页应用程序可以作为无服务器应用程序的一部分进行部署。我们的单页应用程序被复制到公开可访问的 S3 存储桶中。

  • 该系统的所有基础设施都定义为代码;我们从未需要使用 AWS 网页控制台来部署我们的应用程序。

  • 部署和移除可以通过触发 Serverless 框架的脚本实现完全自动化。

警告 请确保你完全移除本章中部署的所有云资源,以避免产生额外费用!

第二部分. 行业工具

在第四章中,我们将创建一个完全无服务器的待办事项应用,我们将使用 AWS Cognito 来确保其安全性。在第五章中,我们将继续添加由人工智能驱动的界面,例如语音转文本和交互式聊天机器人。

在第六章中,我们将更详细地探讨您为了有效地使用人工智能即服务(AI as a Service)所需掌握的关键工具和技术。这包括如何创建构建和部署管道,如何将可观察性构建到我们的系统中,以及如何有效地监控和调试我们所构建的系统。

从零开始构建系统相对容易。在现实世界中,我们大多数人被分配的任务是维护和扩展现有的平台,因此我们将在第七章中结束这一部分,探讨如何将我们所学应用到现有系统中。

4 以无服务器方式构建和保障 Web 应用程序

本章涵盖

  • 创建无服务器待办事项列表

  • 使用 DynamoDB,一个无服务器数据库

  • 以无服务器方式实现登录

在本章中,我们将基于第二章和第三章的教训,构建我们的第二个、更强大的无服务器 AI 系统。大多数编程教材都使用标准的待办事项列表应用作为教学示例。在这方面,本书也不例外。然而,这绝对不是你祖父母的待办事项列表:这是强化版的待办事项列表!在本章中,我们将构建的待办事项列表应用将从一个足够简单的 CRUD(创建、读取、更新、删除)类型的应用程序开始,利用云原生数据库。在通过登录和注销屏幕保护应用程序后,我们将添加自然语言语音接口来记录和转录文本,并让系统从我们的待办事项列表中告诉我们日常日程。最后,我们将向系统中添加对话接口,使我们能够完全通过自然语音而不是键盘进行交互。

在本章中,我们将构建无服务器待办事项列表。我们将在第五章中添加 AI 功能,正如我们将看到的,这些功能可以通过利用云 AI 服务来完成快速构建。

4.1 待办事项列表

我们下一代待办事项列表将消耗多个 AI 服务。与之前一样,这将遵循我们在第一章中开发的、在第二章和第三章中使用的标准无服务器 AI 系统架构模式。最终产品如图 4.1 所示。

图 4.1 最终目标

在这张图中,用户正在通过与我们待办事项聊天机器人的对话创建一个新的待办事项,正处于中途。

4.2 架构

在我们开始组装系统之前,让我们看看架构,并花点时间理解它如何映射回我们在第一章中开发的标准无服务器 AI 架构。图 4.2 描述了系统的整体结构。

系统架构显示了服务之间的清晰分离。每个服务都有一个单一的责任和定义良好的接口:

  • Web 应用程序 --客户端应用程序的静态内容从 S3 存储桶中提供。API 网关提供了一个 API,该 API 触发我们的同步和异步服务中的事件处理器。我们的 Web 应用程序客户端使用 AWS Amplify 客户端 SDK 来处理认证的复杂性。

  • 同步和异步服务 --这些自定义服务是 AWS Lambda 函数,它们处理我们的 API 请求并执行应用程序的主要业务逻辑。

    图 4.2 系统架构。系统由自定义服务和托管服务组成。使用 AWS 提供的许多托管服务,我们可以快速构建和部署可扩展的生产级应用程序。

  • 通信架构 --AWS Route53 用于 DNS 配置,因此我们的服务可以通过自定义域名访问。

  • 实用服务 --AWS Cognito 用于身份验证和授权。

  • AI 服务 --我们使用三个托管的 AWS AI 服务:Transcribe、Polly 和 Lex。

  • 数据服务 --使用 DynamoDB 作为强大且可扩展的数据库。S3 用于文件存储。

在我们处理系统时,我们将更详细地描述每个部分,并解释它是如何构建和部署的。

4.2.1 网络应用

应用程序的结构如图 4.3 所示,其中突出显示了网络应用部分。

图 4.3 网络应用

所示的结构与我们第二章和第三章中的系统相似。系统的前端是一个单页应用程序,由 HTML、CSS 和 JavaScript 组成,用于渲染 UI,部署到 S3 存储桶中。我们将在本章中重复使用此图像,在构建完整应用程序时突出相关部分。与之前一样,我们使用 API Gateway 提供对服务的路由。

对于我们的待办事项应用程序,我们在前端使用了一个额外的库;AWS Amplify。Amplify 是一个 JavaScript 客户端库,它提供了对指定 AWS 服务的安全访问。在我们的案例中,我们使用它来提供 Cognito 的客户端接口以进行登录和注销,并且还可以访问存储在 S3 中的语音转文本数据。

4.2.2 同步服务

图 4.4 再次显示了我们的应用程序架构,这次突出显示了同步服务部分。

图 4.4 同步服务

展示了一个主要同步服务。这是to-do服务,它提供了以下简单的 CRUD 接口:

  • POST /todo/--创建一个新的项目。

  • GET /todo/{id}--读取一个特定的项目。

  • PUT /todo/{id}--更新项目。

  • DELETE /todo/{id}--删除一个项目。

  • GET /todo--列出所有项目。

4.2.3 异步服务

我们应用程序架构的异步服务部分在图 4.5 中突出显示。

图 4.5 异步服务

有两种异步服务,分别涉及语音转文本和文本转语音翻译。这些服务如下。

注意服务

提供了一个将录音笔记转换为文本的接口:

  • POST /note--启动一个新的异步笔记转录作业。

  • GET /note/{id}--轮询获取有关异步转录的信息。

调度服务

提供了一个创建计划并将其转换为语音录音的接口:

  • POST /schedule--启动一个新的异步调度作业。

  • GET /schedule/{id}--轮询获取有关计划的信息。

4.2.4 通信架构

我们选择使用基于轮询的机制来构建待办事项列表以保持简单,并且选择不使用任何队列。我们主要使用 HTTP 和 DNS 作为我们的通信架构技术。

4.2.5 实用服务

我们使用 Amazon Cognito 作为用户登录和身份验证的机制。用户管理是一个“已解决的问题”,我们不想为每个我们开发的平台都自己构建。对于这个系统,我们使用 Cognito 来为我们做繁重的工作。

4.2.6 AI 服务

我们架构中下一个突出的部分,如图 4.6 所示,涵盖了我们在本系统中使用的 AI 和数据存储服务。

图 4.6 AI 和数据服务

此图显示我们正在使用几个 AI 服务:

  • Transcribe 用于提供语音到文本转换,并从 S3 读取其输入。

  • Polly 将文本转换为语音,并将其输出音频文件写入 S3。

  • Lex 用于创建交互式聊天机器人。我们将使用 Lex Web UI 系统直接集成到我们的前端应用中。

4.2.7 数据服务

在数据服务层,我们使用简单存储服务(S3)和 DynamoDB。DynamoDB 是一个高度可扩展的云原生 NoSQL 数据库,我们使用它来存储我们的待办事项。

4.2.8 开发支持和运营支持

与之前一样,我们使用 Serverless Framework 作为我们的主要开发支持系统。所有日志数据都使用 CloudWatch 收集。

4.3 准备工作

现在我们已经看到了最终目标,让我们深入其中,将系统组合起来。作为本章的先决条件,您需要具备以下条件:

  • AWS 账户

  • AWS 命令行已安装并配置

  • Node.js 已安装

  • Serverless Framework 已安装

如何设置 Node.js 和 Serverless Framework 的说明在第 2 和第三章中提供。AWS 的设置说明在附录 A 中提供。如果您尚未这样做,您需要在继续本章的代码之前遵循这些说明。

警告:使用 AWS 需要付费!请确保您完成使用后,任何云基础设施都被销毁。我们将在每个章节的末尾提供拆解说明,以帮助您完成这项工作。

4.3.1 获取代码

本章的源代码可在github.com/fourTheorem/ai-as-a-service仓库的code/chapter4目录中找到。如果您尚未这样做,您可以继续克隆该仓库:

$ git clone https://github.com/fourTheorem/ai-as-a-service.git

该系统的代码被分解为多个简单步骤,因为我们将会逐步构建系统。在本章中,我们构建基本应用,然后在第五章中添加 AI 功能。如果您查看chapter4chapter5目录,您将找到以下分解:

  • chapter4/step-1-basic-todo

  • chapter4/step-2-cognito-login

  • chapter5/step-3-note-service

  • chapter5/step-4-schedule-service

  • chapter5/step-5-chat-interface

我们将按顺序处理这些目录。每个逻辑步骤都将为我们的待办事项应用添加额外的功能。让我们从第一步开始,我们的基本待办事项应用。

4.4 步骤 1:基本应用

我们的基本待办事项应用程序对大多数程序员来说应该相当熟悉,他们可能在某个时候遇到过标准的待办事项应用程序。图 4.7 展示了部署后的应用程序运行情况。

图片

图 4.7 基本待办事项列表

完整的应用程序显示了一个待办事项列表,以及一个用于添加新待办事项的表单。

为什么还需要另一个待办事项应用程序?

在整理本书的内容时,我们确实质疑过世界是否还需要另一个待办事项应用程序。然而,经过反思,我们决定以下原因将是有价值的:

  • 待办事项应用程序需要涵盖所有基本的 CRUD 操作。

  • 这对大多数程序员来说是一个熟悉的起点。

  • 大多数待办事项应用程序都停留在 CRUD 部分;我们的目标是探索如何通过 AI 服务来保护和扩展应用程序。

在这个起点,系统由一组小的组件组成,如图 4.8 所示。

图片

图 4.8 步骤 1 架构

如您所见,我们的系统目前相当简单。它使用单个 API Gateway 部署、一些简单的 Lambda 函数、一个 DynamoDB 表,以及从 S3 提供的前端代码。这个第一步的源代码在 chapter4/step-1-basic-todo 目录中,下一列表中只列出了关键文件,以保持清晰。

列表 4.1 代码结构

???  frontend
?    ???   package.json
?    ???   serverless.yml
?    ???   webpack.config.js
???  resources
?    ???   package.json
?    ???   serverless.yml
???  todo-service
     ???   dynamodb.yml
     ???   handler.js
     ???   package.json
     ???   serverless.yml

让我们逐一查看这些组件。

4.4.1 资源

与我们之前的应用程序一样,我们在 resources 目录中定义了一组全局云资源。需要注意的是,我们在这里只配置全局资源。针对特定服务的云资源配置应与该服务一起保留。例如,待办事项服务“拥有”待办事项 DynamoDB 表;因此,此资源被配置为 to-do 服务定义的一部分。

小贴士:作为一般规则,将特定服务的资源定义与服务代码一起保留。只有全局访问的资源才应在服务目录之外进行配置。

我们为资源定义的 serverless.yml 文件定义了一个用于前端的前端 S3 存储桶,设置了权限,并启用了 CORS。在完成第二章和第三章的学习后,这个 serverless.yml 的格式和结构应该非常熟悉,所以我们在这里不会详细说明,只是指出,在这个配置中我们使用了一个新的插件:serverless-dotenv-plugin。这个插件从 .env 文件中读取环境变量,该文件包含系统特定的变量,如我们的存储桶名称。我们将在本节稍后部署系统时编辑此文件。

CORS

CORS代表跨源资源共享。这是一种安全机制,允许网页从与原始网页加载的域不同的域请求资源。使用 CORS,Web 服务器可以选择性地允许或拒绝来自不同源域的请求。有关 CORS 的完整解释,请参阅此处:en.wikipedia.org/wiki/Cross-origin_resource_sharing

在我们的系统中,唯一的共享资源是数据存储桶。这将在后续部分的服务中使用。

4.4.2 待办服务

对于第一步,我们只实现了基本的待办 CRUD 服务和最小的前端。待办服务使用 DynamoDB,亚马逊的云原生 NoSQL 数据库。图 4.9 说明了构成待办服务的各个路由,每个路由都执行相应的读取或写入操作。

图 4.9 待办服务

图像的展开部分显示了添加、更新和删除待办记录的 POST、PUT 和 DELETE 路由。显示了两个 GET 路由:一个用于检索所有待办事项,另一个用于使用其 ID 检索单个待办事项。

CRUD

如果你对这个术语不熟悉,CRUD代表创建、读取、更新、删除。有时你会听到“基于 CRUD 的应用程序”这个术语。这个术语只是意味着一个在某个数据存储上执行这些标准操作的应用程序。通常,CRUD 应用程序使用 RESTful HTTP 接口实现。这意味着以下 HTTP 动词和路由被使用:

  • POST /widget--发布数据以创建和存储新的小部件。

  • GET /widget/{id}--读取具有提供 ID 的小部件的数据。

  • PUT /widget/{id}--更新具有提供 ID 的小部件。

  • DELETE /widget/{id}--删除具有提供 ID 的小部件。

  • GET /widget--获取所有小部件的列表。

如下所示,serverless.yml文件的主要部分配置了 AWS 提供者并定义了 API 网关路由及其相关的 Lambda 函数事件处理程序。

列表 4.2 为待办服务配置的serverless.yml

provider:
  name: aws
  runtime: nodejs12.x
  stage: ${opt:stage, 'dev'}
  region: ${env:AWS_DEFAULT_REGION, 'eu-west-1'}
  environment:                                            ❶
    TODO_TABLE: '${self:service}-${self:provider.stage}'
  iamRoleStatements:                                      ❷
    - Effect: Allow
      Action:
        - dynamodb:DescribeTable
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource: "arn:aws:dynamodb:${self:custom.region}:${self:custom.accountid}:*"

functions:                                                ❸
  create:
    handler: handler.create
    events:
      - http:
          method: POST
          path: todo
          cors: true
  ...

resources:
  - ${file(./dynamodb.yml)}                               ❹

❶ 为 DynamoDB 定义环境变量

❷ 为 Lambda 函数访问 DynamoDB 的 IAM 访问角色

❸ CRUD 路由和处理程序

❹ 包含资源

尽管这个配置文件比我们之前的示例要大一些,但其结构与第二章和第三章中的ui-service非常相似,即我们

  • 为我们的处理程序函数配置对 DynamoDB 的访问

  • 定义路由和处理程序函数

我们在提供者部分使用环境定义来为我们的处理程序代码提供 DynamoDB 表名:

environment:
  TODO_TABLE: '${self:service}-${self:provider.stage}'

这很重要,因为我们不希望将表名硬编码到我们的处理程序函数中,因为这会违反 DRY 原则。

提示 DRY 代表“不要重复自己”。在软件系统的上下文中,这意味着我们应该努力只为系统中的每条信息保留一个定义或真相来源。

为了使无服务器定义更易于管理,我们选择将我们的 DynamoDB 表定义放在单独的文件中,并将其包含在我们的主 serverless.yml 文件中:

resources:
  - ${file(./dynamodb.yml)}

这可以帮助我们使配置更短、更易读。我们将在剩余的章节中一直使用这种模式。我们包含的文件,如下一列表所示,为系统配置了 DynamoDB 资源。

列表 4.3 无服务器 DynamoDB 配置

Resources:
  TodosDynamoDbTable:
    Type: 'AWS::DynamoDB::Table'
    DeletionPolicy: Retain         ❶
    Properties:
      AttributeDefinitions:        ❷
        - AttributeName: id
          AttributeType: S
      KeySchema:
        - AttributeName: id
          KeyType: HASH
      ProvisionedThroughput:       ❸
        ReadCapacityUnits: 1
        WriteCapacityUnits: 1
      TableName: '${self:service}-${self:provider.stage}'

❶ 我们指定,当删除 CloudFormation 堆栈时,不应删除表格。这有助于避免意外数据丢失。CloudFormation 堆栈是我们运行无服务器部署时创建或更新的资源集。

❷ 对于这个表格,我们指定了一个键属性,一个类型为 S(字符串)的 ID。这个属性是一个分区键,因此值必须是唯一的。

❸ 设置吞吐量的容量单位为最低可能值。这将限制可以发生的读取和写入次数,但对于这个应用程序,将确保成本保持在最低。

这是一个非常简单的配置,在 DynamoDB 表上定义了一个单一的 id 键。

如果我们现在查看 to-do 服务的处理程序代码,应该会清楚系统是如何使用 DynamoDB 存储数据的。代码在文件 handler.js 中,如下所示列表。

列表 4.4 需求和创建待办服务处理程序

const uuid = require('uuid')
const AWS = require('aws-sdk')                       ❶
const dynamoDb = new AWS.DynamoDB.DocumentClient()   ❷
const TABLE_NAME = {
  TableName: process.env.TODO_TABLE                  ❸
}

function respond (err, body, cb) {                   ❹
  ...
}

module.exports.create = (event, context, cb) => {    ❺
  const data = JSON.parse(event.body)
  removeEmpty(data)

  data.id = uuid.v1()
  data.modifiedTime = new Date().getTime()

  const params = { ...TABLE_NAME, Item: data }
  dynamoDb.put(params, (err, data) => {              ❻
    respond(err, {data: data}, cb)
  })
}

❶ 需求 AWS SDK

❷ 创建 DynamoDB 客户端

❸ 使用表名环境变量

❹ 响应样板

❺ 创建处理程序

❻ 在数据库中创建待办事项

如果你在第二章和第三章中工作过,处理程序的实现应该也很熟悉。这里的模式是包含 AWS SDK 并创建一个接口,以便访问我们想要访问的特定服务,在这种情况下是 DynamoDB。其余的代码然后使用这个资源对服务执行操作,并将结果返回给服务的调用者。在列表 4.4 中,我们展示了 create 端点。这映射到我们的 POST /to-do 路由。此代码的客户端将在 POST 请求中包含待办信息,格式为 JSON 数据。在这种情况下,使用的 JSON 格式如以下列表所示。

列表 4.5 待办 POST 的示例 JSON 内容

{
  dueDate: '2018/11/20',
  action: 'Shopping',
  stat: 'open',
  note: 'Do not forget cookies'
}

create 方法在将待办事项写入数据库之前添加了一个 timestamp 字段和一个 id 字段。handler.js 中的其他方法实现了对数据库的其他 CRUD 操作。

4.4.3 前端

我们这个第一步的前端应用程序也很简单,如图 4.10 所示。

前端应用程序在 S3 上构建和存储。当浏览器加载 index.html 页面时,代码和其他资源,如样式表和图像,也会被加载。内部前端应用程序使用 JQuery 构建。由于这个应用程序将比第二章和第三章中的示例做得更多,我们在代码中引入了一些结构,如图 4.10 所示,稍后将进行描述。

代码位于 frontend 目录中,结构如下所示列表。

列表 4.6 前端目录结构

???    assets
???    src
?      ???    index.html
?      ???    index.js
?      ???    templates.js
?      ???    todo-view.js
?      ???    todo.js
???    webpack.config.js
???    package.json
???    serverless.ym

应用程序的根页面是 src/index.html,如下所示列表。这提供了一些初始的 DOM(文档对象模型)结构和加载主要应用程序代码。

图 4.10 前端

列表 4.7 index.html

<html>
<head>
  <title>Chapter 4</title>
</head>
<body>
  <script src='main.js'></script>      ❶

    <nav class="navbar navbar-expand-lg navbar-light bg-light">
  .
  .                                    ❷
  .
    </nav>

    <div id="content">                 ❸
    </div>

    <div id="footer">
        <div id="error"></div>
    </div>

</body>
</html>

❶ 加载应用程序代码

❷ 导航栏代码省略

❸ 主要应用程序内容区域

应用程序的主要代码位于 src 目录中。它由以下内容组成:

  • index.js--应用程序入口点

  • todo.js--待办事项“模型”和“控制器”代码

  • todo-view.js--待办事项 DOM 操作

  • templates.js--通用渲染模板

如下所示列表的 index.js 文件,简单地加载了所需资源。

列表 4.8 index.js

import $ from 'jquery'                          ❶
import 'bootstrap/dist/css/bootstrap.min.css'
import 'webpack-jquery-ui/css'
import {todo} from './todo'                     ❷

$(function () {
  todo.activate()                               ❸
})

❶ 加载 jQuery 和样式

❷ 加载待办事项代码

❸ 页面加载后激活待办事项

主要工作在我们下面的待办事项模块中完成。

列表 4.9 todo.js

import $ from 'jquery'
import {view} from './todo-view'     ❶

const todo = {activate}              ❷
export {todo}

const API_ROOT = `https://chapter4api.${process.env.CHAPTER4_DOMAIN}/api/todo/`

function create (cb) {               ❸
  $.ajax(API_ROOT, {
  ...
  })
}

function list (cb)                   ❹
  $.get(API_ROOT, function (body) {
  ...
  })
}

function activate () {
  list(() => {                       ❺
    bindList()
    bindEdit()
  })
  $('#content').bind('DOMSubtreeModified', () => {
    bindList()
    bindEdit()
  })
}

❶ 导入待办事项视图

❷ 导出激活函数

❸ 创建待办事项

❹ 列出待办事项

❺ 加载时调用列表

为了清晰起见,我们省略了列表 4.9 中的一些代码。大多数读者都会熟悉模型视图控制器(MVC)模式。我们的待办事项模块可以被视为在前端应用程序中充当待办事项的模型和控制器,而视图功能由 todo-view.js 处理。

我们正在使用环境变量构建待办事项 API 的 URL:

API_ROOT = `https://chapter4api.${process.env.CHAPTER4_DOMAIN}/api/todo/`

我们将在本节稍后部署前端时设置 CHAPTER4_DOMAIN 变量。

为什么没有使用框架?

熟悉前端开发的读者可能会想知道为什么我们不使用某种前端框架,如 React、Vue 或 Angular。答案是,尽管我们了解有许多流行的框架可用,但它们需要时间来学习。本书的目标是教授 AI 即服务而不是前端框架,因此我们选择使用 JQuery 结合 Webpack 的最低共同分母方法。这样我们希望减少认知学习负担。

我们的处理功能由 todo-view.jstemplates.js 处理。我们将其留作练习,让读者查看这些文件,这些文件本质上执行了一些非常简单的 DOM 操作来渲染待办事项列表。

在我们的 frontend 目录根目录中,我们有三个控制文件:package.jsonwebpack.config.jsserverless.yml。这些文件允许我们安装和管理 JavaScript 依赖项,构建用于部署的前端版本,并创建用于部署构建的 S3 存储桶。

前端 serverless.yml 与我们资源目录中的类似,所以我们在这里不会详细说明。它只是定义了一个具有适当权限的 S3 存储桶,以公开提供我们的前端。

我们在第二章和第三章中介绍了 package.json 的结构,所以这部分应该是熟悉的。我们应该注意,webpack 本身在 package.json 中被管理为一个开发依赖项。我们还在脚本块下定义了一个构建任务,该任务运行 webpack 以构建用于部署的应用程序。

Webpack

Webpack 是现代 JavaScript 应用程序的静态模块打包器。Webpack 处理 JavaScript、CSS 以及其他源文件,以创建一个紧凑的输出 JavaScript 文件,该文件可以包含在 Web 应用程序中。Webpack 通过构建依赖图而不是逐文件工作来实现这一点。这有几个好处:

  • 依赖图意味着只有我们需要的资源被包含在输出中。

  • 结果输出更加高效,因为网络应用程序只下载一个压缩后的 JavaScript 文件。

  • 我们的流程现在干净且高效,因为我们可以使用 npm 模块系统进行依赖管理。

  • Webpack 还会管理其他静态资源,如 CSS、图像等,作为依赖图的一部分。

Webpack 的完整文档可在此处查看:webpack.js.org/.

我们的 webpack 配置如下所示。

列表 4.10 webpack.config.js

const Dotenv = require('dotenv-webpack')
const path = require('path')

module.exports = {
  entry: {
    main: './src/index.js'       ❶
   },
  devtool: 'eval-source-map',    ❷
   devServer: {
    contentBase: './dist',
    port: 9080
  },
  output: {                      ❸
     filename: '[name].js',
    path: path.resolve(__dirname, 'dist'),
    publicPath: 'dist/'
  },
  mode: 'development',           ❹
   module:                       ❺
     rules: [{
    ...
    }]
  },
  plugins: [                     ❻
     new Dotenv({
      path: path.resolve(__dirname, '..', '.env'),
      systemvars: false,
      silent: false
    })
  ]
}

❶ 定义依赖图入口点

❷ 启用源映射以进行调试

❸ 定义输出映射

❹ 开发模式

❺ CSS 和图像模块

❻ .env 文件插件

我们的 webpack 配置将把 src/index.js 中的所有依赖项构建到 dist 文件夹中。这包括我们所有的源代码和相关模块,包括 JQuery。然后我们可以简单地部署 dist 目录到 S3,以拥有一个功能性的应用程序。

serverless-dotenv-plugin 类似,我们在这里使用 dotenv-webpack 插件。这允许我们在所有代码区域使用单个环境配置文件,有助于保持我们的系统 DRY(Don't Repeat Yourself)。

4.4.4 部署步骤 1

现在我们已经了解了待办事项系统,让我们继续将其部署到 AWS。如果您还没有设置账户,您需要查看附录 A 来进行设置。

设置环境变量

你可能还记得,在审查代码时,前端项目创建了一个 S3 存储桶来存放我们的 Web 应用程序,并且它使用了一个环境变量CHAPTER4_BUCKET。你需要为你的存储桶选择一个全局唯一的名称。记住,我们通过环境变量CHAPTER4_DOMAIN使用了一个自定义域名来为待办事项 API。

在附录 A 的设置之后,你应该在你的 shell 中定义以下环境变量:

  • AWS_ACCOUNT_ID

  • AWS_DEFAULT_REGION

  • AWS_ACCESS_KEY_ID

  • AWS_SECRET_ACCESS_KEY

这些是全局变量,你应该在系统的一个地方保持它们。为了部署我们的待办应用程序,我们需要提供系统特定的变量。为此,我们将使用一个.env文件。使用任何文本编辑器创建一个名为.env的文件,并将其放置在chapter4/step1-basic-todo目录中。该文件应包含以下列表中的内容。

列表 4.11 步骤 1 的环境变量

# environment definiton for Chapter 4
TARGET_REGION=eu-west-1                          ❶
CHAPTER4_BUCKET=<your bucket name>               ❷
CHAPTER4_DATA_BUCKET=<your data bucket name>
CHAPTER4_DOMAIN=<your development domain>        ❸

❶ 我们指定所有示例的区域为 eu-west-1。

❷ 指定你选择的全球唯一存储桶名称。

CHAPTER4_DOMAIN的值可以与第二章和第三章部署中使用的值完全相同,并且应引用使用 AWS Route53 创建的域名。

用你选择的名称替换CHAPTER4_BUCKETCHAPTER4_DATA_BUCKETCHAPTER4_DOMAIN。请参考第二章和第三章,以获取设置域的完整说明。

部署资源

首先,我们将部署我们的资源项目。为此,进入resources目录并运行:

$ npm install
$ serverless deploy

这将创建我们的 S3 数据存储桶,供以后使用。我们可以通过使用 AWS Web 控制台来确认存储桶的创建。

部署待办服务

接下来,我们将部署待办服务。进入todo-service目录,通过运行以下命令安装依赖项:

$ npm install

在部署之前,我们需要为我们的应用程序创建一个自定义域名。在serverless.yml中的配置如下所示。

列表 4.12 serverless.yml中的自定义域名配置

custom:
  region: ${env:AWS_DEFAULT_REGION, 'eu-west-1'}
  accountid: ${env:AWS_ACCOUNT_ID}
  domain: ${env:CHAPTER4_DOMAIN}                       ❶
  customDomain:
    domainName: 'chapter4api.${self:custom.domain}'    ❷
    stage: ${self:provider.stage}
    basePath: api
    certificateName: '*.${self:custom.domain}'         ❸
    createRoute53Record: true
    endpointType: regional
  serverless-offline:
    port: 3000

❶ 这定义了父域名。

❷ 子域名由前缀 chapter4api 和父域名组成。

❸ 指定了一个通配符证书。

我们这个部分的域名将由CHAPTER4_DOMAIN的设置和一个子域名chapter4api组成。也就是说,如果我们使用example.com作为变量CHAPTER4_DOMAIN的值,那么这一章的完整自定义域名将是chapter4api.example.com

让我们继续创建这个域名

$ serverless create_domain

我们现在可以通过运行以下命令来部署我们的待办事项 API:

$ serverless deploy

部署前端

最后,在这一节中,我们需要部署我们的前端。首先,为了安装依赖项,进入frontend目录并运行:

$ npm install

接下来,我们需要通过运行以下命令创建我们的应用程序的存储桶:

$ serverless deploy

我们现在可以通过我们的npm脚本部分使用 Webpack 构建前端:

$ source ../.env
$ npm run build

这将在我们的dist目录中创建一个名为main.js的文件,以及一个包含一些额外图像的assets目录。为了部署前端,我们将使用 AWS 命令行,就像我们在第二章和第三章中所做的那样:

$ cd frontend
$ source ../.env
$ aws s3 sync dist/ s3://$CHAPTER4_BUCKET

这会将dist目录的内容推送到我们刚刚创建的第四章存储桶。请注意,我们需要将环境文件的内容source到 shell 中,以提供CHAPTER4_BUCKET环境变量。

测试它

如果所有前面的步骤都进行顺利,我们现在应该已经将一个完全功能性的系统部署到了 AWS。为了测试这一点,请在浏览器中打开此 URL

https://<CHAPTER4_BUCKET>.s3-eu-west-1.amazonaws.com/index.html

<CHAPTER4_BUCKET>替换为你的特定存储桶名称。你应该能够通过浏览器中的前端创建和更新待办事项。

为什么从存储桶提供服务?

一些读者可能会想知道为什么我们要直接从 S3 存储桶提供内容。我们为什么不使用像 CloudFront 这样的 CDN?答案是,对于这样一个教学系统,CloudFront 是过度的。我们同意,对于一个完整的生产系统,S3 存储桶应该用作 CDN 的源服务器;然而,在开发模式下,CDN 缓存和更新只会碍手碍脚。

我们现在有一个工作的待办事项系统。只有一个小问题。系统是公开可访问的,这意味着任何互联网上的随机人员都可以读取和修改我的待办事项列表。这显然不是我们系统所期望的特性,所以我们最好迅速解决这个问题。幸运的是,我们可以使用一个云原生服务来为我们处理这项工作。在下一节中,我们将使用 Cognito 来保护我们的待办事项列表。

4.5 步骤 2:使用 Cognito 进行安全保护

用户管理是那些看似简单的问题之一,因为它看起来应该很容易,但通常结果却非常困难!许多程序员在天真地认为“这不可能那么难”的假设下,长时间熬夜自己编写用户身份验证和管理系统。

幸运的是,用户登录和管理是一个已解决的问题,所以我们再也不需要编写这类代码了。我们可以直接使用云原生服务来为我们完成这项工作。有几种选项可供选择,但对我们这个系统来说,我们将使用 AWS Cognito。Cognito 为我们提供了一个完整的身份验证服务,包括

  • 密码复杂性策略

  • 与 Web 和移动应用程序的集成

  • 多种登录策略

  • 用户管理

  • 密码复杂性规则

  • 单点登录

  • 通过 Facebook、Google、Amazon 等社交登录

  • 安全最佳实践和针对最新已知安全漏洞的防御

这对于一个小型的开发工作来说功能非常强大。所以,让我们将 Cognito 应用到我们的待办事项系统中,并保护它免受坏人的侵害!

图 4.11 展示了添加了 Cognito 身份验证的系统。

图片

图 4.11 步骤 2 架构

我们已经将 AWS Amplify 库添加到前端。Amplify 是一个提供对各种 AWS 服务进行认证访问的 JavaScript 库。目前,我们只会用它来进行认证和访问控制。在成功登录后提供的令牌将被传递到 API Gateway 的 API 调用中,这些调用反过来由 AWS Lambda 处理函数处理。

AWS Amplify

Amplify 最初是一个提供客户端访问 AWS API 的 JavaScript 库。该库支持桌面浏览器,以及 iOS 和 Android 设备。库的最新增补是 Amplify CLI,旨在提供类似于我们一直在使用的 Serverless Framework 的工具链。在撰写本文时,Amplify 工具链不如 Serverless Framework 成熟,并且缺乏插件生态系统支持。然而,这绝对是一个值得关注的项目。

Amplify 的完整文档可以在以下链接找到:aws-amplify.github.io/docs/js/start

如图 4.11 所示,我们将登录任务委托给 Cognito。一旦用户认证成功,就会分配一个会话令牌,由 Amplify 库管理。然后我们在 API Gateway 设置中添加一个认证步骤,要求用户在请求继续之前提供有效的 JSON Web Token (JWT)。任何没有有效网络令牌的请求都会在这一层被拒绝。

JSON Web Tokens

JSON Web Token (JWT) 是一个定义了安全传输声明的方法的 (RFC 7519) 标准,它将声明作为 JSON 对象进行传输。JWT 由三个部分组成:

<header>.<payload>.<signature>
  • header--标识令牌使用的哈希算法。

  • payload--包含一组声明。一个典型的声明可能是用户 ID。

  • signature--是 header、payload 和一个秘密使用 header 中定义的算法进行单向哈希的结果。

通常,JWT 会在登录时由认证服务器颁发,然后由客户端用于安全地访问资源。JWT 通常具有较短的生存期,并在预定义的时间后过期,迫使客户端定期重新认证以生成新的令牌。

关于 JWT 的详细信息可以在以下链接找到:en.wikipedia.org/wiki/JSON_Web_Token

4.5.1 获取代码

这个步骤的代码在目录 chapter4/step-2-cognito-login 中,包含了步骤 1 的代码以及 Cognito 的更新。我们将依次介绍这些更新,然后部署更改以保护我们的系统。

4.5.2 用户服务

首先,有一个新的服务目录,user-service。这个文件夹只包含 Cognito 的无服务器配置。这里有三个文件:

  • identity-pool.yml

  • user-pool.yml

  • serverless.yml

我们的serverless.yml很短,此时你应该熟悉大多数模板条目。它导入其他两个文件,这些文件包含 Cognito 资源。下一个列表中的user-pool.yml配置了我们的 Cognito 用户池。用户池正是其名称所暗示的,即用户池。

列表 4.13 Cognito 用户池配置

Resources:
  CognitoUserPool:                             ❶
    Type: AWS::Cognito::UserPool
    Properties:
      UserPoolName: ${self:service}${self:provider.stage}userpool
      UsernameAttributes:
        - email
      AutoVerifiedAttributes:
        - email
      EmailVerificationSubject: 'Your verification code'
      EmailVerificationMessage: 'Your verification code is {####}.'
      Schema:
        - Name: email
          AttributeDataType: String
          Mutable: true
          Required: true
      AdminCreateUserConfig:
        InviteMessageTemplate:
          EmailMessage: 'Your username is {username} and\
temporary password is {####}.'
          EmailSubject: 'Your temporary password'
        UnusedAccountValidityDays: 2
        AllowAdminCreateUserOnly: true
  CognitoUserPoolClient:                      ❷
    Type: AWS::Cognito::UserPoolClient
    Properties:
      ClientName: ${self:service}${self:provider.stage}userpoolclient
      GenerateSecret: false
      UserPoolId:
        Ref: CognitoUserPool

❶ 用户池

❷ 客户端集成

Cognito 提供了大量的选项。我们将保持简单,仅为其配置电子邮件和密码登录。列表 4.13 中的代码创建了一个用户池和一个用户池客户端。用户池客户端提供了一个用户池和外部应用程序之间的集成桥梁。Cognito 支持单个用户池针对多个用户池客户端。

要使用 Cognito 授权访问 AWS 资源,我们还需要一个身份池。这已在identity-pool.yml中配置,如下所示。

列表 4.14 Cognito 身份池配置

Resources:
  CognitoIdentityPool:                                  ❶
    Type: AWS::Cognito::IdentityPool
    Properties:
      IdentityPoolName: ${self:service}${self:provider.stage}identitypool
      AllowUnauthenticatedIdentities: false
      CognitoIdentityProviders:
        - ClientId:
            Ref: CognitoUserPoolClient                  ❷
          ProviderName:
            Fn::GetAtt: [ "CognitoUserPool", "ProviderName" ]

  CognitoIdentityPoolRoles:                             ❸
    Type: AWS::Cognito::IdentityPoolRoleAttachment
    Properties:
      IdentityPoolId:
        Ref: CognitoIdentityPool
      Roles:
        authenticated:
          Fn::GetAtt: [CognitoAuthRole, Arn]

❶ 定义身份池

❷ 连接到用户池

❸ 将策略附加到身份池

在列表 4.14 中,我们将身份池连接到了我们的用户池,还连接到了一个角色,CognitoAuthRole。该角色也在identity-pool.yml中定义。关于这个角色的关键部分包含在下一个列表中的策略声明中。

列表 4.15 身份池策略声明

Statement:
    - Effect: 'Allow'
      Action:                    ❶
        - 'cognito-sync:*'
        - 'cognito-identity:*'
        - 'S3:*'
        - 'transcribe:*'
        - 'polly:*'
        - 'lex:*'
      Resource: '*'
    - Effect: 'Allow'
      Action:                    ❷
        - 'execute-api:Invoke'
      Resource:

❶ 策略授予 Cognito、S3、Transcribe、Polly 和 Lex 的所有操作。

❷ 策略授予调用我们的 API 网关路由的访问权限。

此策略将与所有经过身份验证的用户相关联,并表示具有此角色的用户可以

  • 访问 S3

  • 调用 Transcribe 服务

  • 调用 Polly 服务

  • 使用 Lex 服务

  • 执行 API 网关功能

对于此角色,将拒绝访问任何其他服务。

超时!

好的,如果你觉得所有关于用户池和身份池的讨论有点令人困惑,我们同意!一开始可能会感到有些压倒性,所以让我们花点时间来解释。要理解的关键概念是身份验证和授权之间的区别。

身份验证是“谁”。换句话说,我能证明我就是我所说的那个人吗?通常这是通过证明我知道一个秘密信息片段——密码来完成的。用户池处理身份验证。

授权是“什么”。鉴于我已经证明了我的身份,我允许访问哪些资源?通常这通过某种类型的权限模型来实现。例如,在文件系统中,有用户和组级别的访问控制,实现了基本的权限模型。我们刚刚创建的 AWS 策略是我们登录用户的权限模型。身份池处理授权。

身份池也被称为联合身份。这是因为身份池可以有多个身份来源。这如图 4.12 所示。

图 4.12 用户和身份池

如上图所示,用户池可以被视为一个验证身份的来源。其他来源包括 Facebook、Google、Twitter 等。可以配置身份池以使用多个身份来源。对于每个验证过的身份,身份池允许我们为授权访问我们的 AWS 资源配置策略。

对于这个系统,我们将仅使用 Cognito 用户池作为我们的认证用户来源;我们不会启用社交登录。

4.5.3 To-do 服务

现在我们有了认证用户的来源,我们需要更新我们的服务以确保我们已将其锁定以防止未经授权的访问。这很容易实现,并且需要对我们的 to-do 服务 serverless.yml 进行少量更新,如下列表所示。

列表 4.16 对 serverless.yml 的更改

custom:
  poolArn: ${env:CHAPTER4_POOL_ARN}         ❶

functions:
  create:
    handler: handler.create
    events:
      - http:
          method: POST
          path: todo
          cors: true
          authorizer:
            arn: '${self:custom.poolArn}'   ❷
  list:
    handler: handler.list
    events:
      - http:
          method: GET
          path: todo
          cors: true
          authorizer:
            arn: '${self:custom.poolArn}'   ❷

❶ 包含用户池 ARN

❷ 声明授权者

我们只需为希望保护的每个端点声明一个授权者。我们还需要更新我们的环境以包括用户池标识符 CHAPTER4_POOL_ARN

4.5.4 前端服务

我们对前端所做的最后一系列更改提供了登录、注销和令牌管理功能。我们已将 AWS Amplify 添加到前端 package.json 中作为依赖项。Amplify 需要我们提供一些配置参数。这通过 index.js 完成,如下列表所示。

列表 4.17 index.js 中的 Amplify 配置

const oauth = {                                  ❶
  domain: process.env.CHAPTER4_COGNITO_DOMAIN,
  scope: ['email'],
  redirectSignIn: `https://s3-${process.env.TARGET_REGION}.amazonaws.com/${process.env.CHAPTER4_BUCKET}/index.html`,
  redirectSignOut: `https://s3-${process.env.TARGET_REGION}.amazonaws.com/${process.env.CHAPTER4_BUCKET}/index.html`,
  responseType: 'token'
}

Amplify.configure({                              ❷
  Auth: {
    region: process.env.TARGET_REGION,
    userPoolId: process.env.CHAPTER4_POOL_ID,
    userPoolWebClientId: process.env.CHAPTER4_POOL_CLIENT_ID,
    identityPoolId: process.env.CHAPTER4_IDPOOL,
    mandatorySignIn: false,
    oauth: oauth
  }
})

❶ 配置 OAuth 流程。

❷ 配置 Amplify。

我们的配置分为两个独立的部分。首先,我们通过提供域名和重定向 URL 来配置 OAuth。这些必须与我们的 Cognito 配置匹配,我们将在部署这些更改时设置。其次,我们使用我们的池标识符配置 Amplify;我们将在部署期间获取这些 ID,并在下一节中相应地调整我们的环境文件。

登录实现由 auth.jsauth-view.js 处理。以下列表显示了 auth.js 的代码。

列表 4.18 auth.js

...
function bindLinks () {
  ...
  $('#login').on('click', e => {
    const config = Auth.configure()
    const { domain, redirectSignIn, responseType } = config.oauth
    const clientId = config.userPoolWebClientId
    const url = 'https://' + domain                ❶
      + '/login?redirect_uri='
      + redirectSignIn
      + '&response_type='
      + responseType
      + '&client_id='
      + clientId
    window.location.assign(url)
  })
}

function activate () {
  return new Promise((resolve, reject) => {
    Auth.currentAuthenticatedUser()               ❷
       .then(user => {
        view.renderLink(true)                     ❸
        bindLinks()
        resolve(user)
      })
      .catch(() => {
        view.renderLink(false)                    ❹
        bindLinks()
        resolve(null)
      })
  })
}

❶ 重定向到 Cognito 登录页面

❷ 检查是否已登录

❸ 渲染注销链接

❹ 否则渲染登录链接

auth.js 将大部分工作委托给 Amplify。在 activate 函数中,它检查用户是否已经登录,然后调用视图来渲染登录或注销链接。它还提供了一个登录处理程序,该处理程序将重定向到 Cognito 登录页面。

最后,在前端,我们需要更新我们对 to-do API 的调用,包括我们的授权令牌;否则,我们将被拒绝访问。这显示在列表 4.19 中。

列表 4.19 更新的 create 方法

function create (cb) {
  auth.session().then(session => {                  ❶
    $.ajax(API_ROOT, {
      data: JSON.stringify(gather()),
      contentType: 'application/json',
      type: 'POST',
      headers: {
        Authorization: session.idToken.jwtToken     ❷
      },
      success: function (body) {
      ...
      }
    })
  }).catch(err => view.renderError(err))
}

❶ 获取会话。

❷ 通过授权头提供 JWT

我们已更新 to-do.js 中的每个函数,以包含一个 Authorization 头,该头用于将 Cognito 获取的 JWT 传递到我们的 API。

4.5.5 部署步骤 2

现在我们已经了解了 Cognito,让我们部署更改并确保我们的应用程序安全。

部署 Cognito 池

首先,进入 step-2-cognito-login/user-service 目录,通过运行以下命令部署池:

$ serverless deploy

这将创建一个用户和身份池。我们还需要通过 AWS 控制台提供一些额外的配置。打开浏览器,登录到 AWS 控制台,并转到 Cognito 部分。选择管理用户池选项,并选择池 chapter4usersdevuserpool。我们需要为我们用户提供一个域名。从应用集成部分选择域名选项,并提供一个新域名,如图 4.13 所示。

图 4.13 用户和身份池

对于我们的用户池,我们使用了域名 chapter4devfth。您可以使用任何可用的唯一域名。

接下来,我们需要配置我们的 OAuth 流。选择应用客户端设置选项,并按图 4.14 所示提供设置。

图 4.14 OAuth 流配置

对于登录和注销回调 URL,您应该使用我们在步骤 1 中创建的定制域名提供您的前端存储桶的 URL。这些应该以以下形式提供:s3-eu-west-1.amazonaws.com/<YOUR BUCKET NAME>/index.html

OAuth

OAuth 是一种广泛实施的认证和授权标准协议。对 OAuth 2.0 协议的全面讨论需要一本完整的书籍。实际上,我们会将对此感兴趣的读者推荐到曼宁出版社出版的由 Justin Richer 和 Antonio Sanso 编写的 OAuth 2 in Action 一书(www.manning.com/books/oauth-2-in-action)。

关于 OAuth 2.0 协议的更多详细信息,请参阅此处:oauth.net/2/

最后,对于用户池,我们需要创建一个账户来登录。为此,选择用户和组,然后点击创建用户按钮。在这里,您可以使用您的电子邮件地址作为用户名,并选择一个临时密码。在电子邮件字段中也输入您的电子邮件地址。不需要输入电话号码,因此取消选中“标记电话号码为已验证”。所有其他字段的默认选择可以保持不变。

更新环境变量

现在我们已经配置了我们的池,我们需要更新我们的 .env 文件。进入 chapter4/step-2-cognito-login 目录并编辑文件 .env 以匹配以下列表。

列表 4.20 更新的 .env 文件

# environment definition for Chapter 4
TARGET_REGION=eu-west-1                                      ❶
CHAPTER4_BUCKET=<your bucket name>
CHAPTER4_DATA_BUCKET=<Your data bucket name>
CHAPTER4_DOMAIN=<your development domain>
CHAPTER4_COGNITO_BASE_DOMAIN=<your cognito domain>
CHAPTER4_COGNITO_DOMAIN=<your cognito domain>.auth.eu-west-1.amazoncognito.com                                     ❷
CHAPTER4_POOL_ARN=<your user pool ARN>
CHAPTER4_POOL_ID=<your user pool ID>
CHAPTER4_POOL_CLIENT_ID=<your app integration client ID>
CHAPTER4_IDPOOL=<your identity pool ID>

❶ 环境变量的第一块保留自列表 4.11。

❷ 新的环境变量引用了我们创建的 AWS Cognito 资源。

您可以在 AWS 管理控制台的 Cognito 部分找到这些 ID。用户池 ID 位于用户池视图中,如图 4.15 所示。

图 4.15 用户池 ID 和 ARN

客户端 ID 可以在 Cognito 用户池视图的 应用客户端设置 部分找到。这如图 4.16 所示。

图 4.16 池客户端 ID

身份池 ID 可以在“联合身份”视图中找到。只需选择已创建的身份池,然后在右上角选择编辑身份池。编辑视图如图 4.17 所示。从这里,您可以查看并复制身份池 ID。

图 4.17 身份池 ID

注意,您可能会看到一个警告,表明没有指定未认证的角色。这可以忽略,因为所有用户都必须对我们的应用程序进行认证。

一旦您在 AWS 控制台中找到了所需值,请使用相关值填充.env文件。

更新待办事项 API

现在我们已经更新了我们的环境,我们可以将更改部署到我们的待办事项服务。请进入step-2-cognito-login/todo-service目录并运行

$ npm install
$ serverless deploy

这将推送 API 的新版本,其中包括我们的 Cognito 授权器。

更新前端

现在我们已经安全地保护了我们的 API,我们需要更新我们的前端以允许访问。为此,请进入step-2-cognito-login/frontend目录并运行

$ source ../.env
$ npm install
$ npm run build
$ aws s3 sync dist/ s3://$CHAPTER4_BUCKET

这将构建我们应用程序的新版本,包括认证代码,并将其部署到我们的存储桶。如果您将浏览器指向我们的应用程序,您应该看到一个空白页面和页面顶部的登录链接。点击此链接将弹出 Cognito 登录对话框。一旦登录,应用程序应该像以前一样运行。

虽然设置 Cognito 需要一点努力,但带来的好处远远超过了成本。让我们回顾一下您使用此服务可以获得的内容:

  • 用户注册

  • 安全 JWT 登录

  • 集成到 AWS IAM 安全模型中

  • 密码重置

  • 联邦身份,包括企业和社交(如 Facebook、Google、Twitter...)

  • 密码策略控制

那些之前必须处理这些问题的用户将欣赏到实现这些功能所带来的大量开销,即使使用第三方库也是如此。使用 Cognito 的关键原因是将保持用户账户安全的大部分工作责任转移到这个服务上。当然,我们仍然必须注意我们应用程序的安全性;然而,知道 Cognito 服务正在为我们积极管理和更新,这让人感到安慰。

为了使我们的安全无服务器应用程序上线,我们覆盖了很多内容。关于这一点,重要的是我们能够以非常少的努力快速以安全的方式部署我们的应用程序。在下一章中,我们将向我们的待办事项列表添加一些 AI 服务。

摘要

  • 从客户端到数据库的端到端无服务器平台可以在代码中定义并使用 Serverless Framework 进行部署。

  • 可以在serverless.yml文件的资源部分创建 DynamoDB 表。

  • 在我们的 Lambda 函数中使用 AWS SDK 将数据从事件传递到我们的数据库读/写调用。

  • 认证和授权使用 AWS Cognito 进行配置。我们配置了一个用户池、身份池和自定义域名,以及一个策略来保护特定资源。

  • AWS Amplify 与 Cognito 结合使用,以创建一个带有 Cognito 的登录界面。Amplify 是 AWS 提供的一个易于使用的客户端 SDK,它集成了 Cognito 以启用强大的安全功能。

  • 可以创建 API Gateway CRUD 路由来触发 Lambda 函数。API Gateway 路由是通过我们在 serverless.yml 中定义的事件创建的,与相关的 Lambda 函数或 handler 相关联。

警告:第五章将继续构建此系统,并在第五章末尾提供如何移除已部署资源的说明。如果您暂时不打算处理第五章,请确保您完全移除本章中部署的所有云资源,以避免产生额外费用!

5 将人工智能界面添加到 Web 应用程序

本章涵盖

  • 使用 Transcribe 说话音符

  • 使用 Polly 回读日程

  • 使用 Lex 添加聊天机器人界面

在本章中,我们将基于第四章的待办事项应用程序进行构建,向系统中添加现成的 AI 功能。我们将添加自然语言语音接口来记录和转录文本,并让系统从我们的待办事项列表中告诉我们日常日程。最后,我们将向系统中添加一个对话界面,使我们能够完全通过自然语言界面进行交互。正如我们将看到的,这可以通过利用云 AI 服务来完成,从而快速构建。

如果您还没有完成第四章的内容,您应该在继续本章之前返回并完成它,因为我们将直接基于该章末尾部署的待办事项应用程序进行构建。如果您对第四章的内容感到满意,我们可以直接进入并添加我们的笔记服务。我们将从我们上次离开的地方开始,从步骤 3 开始。

5.1 步骤 3:添加语音到文本界面

现在我们已经部署并安全了一个基本的无服务器应用程序,是时候添加一些人工智能功能了。在本节中,我们将添加一个语音到文本界面,使我们能够将笔记口述到系统中而不是键入。我们将使用 AWS Transcribe 来实现这一点。正如我们将看到的,添加语音到文本实际上对于这样一个高级功能来说并不太难。

图 5.1 显示了该功能的实现方式。

图片

图 5.1 步骤 3 架构。AWS Transcribe 服务从笔记服务中调用。前端应用程序使用 Amplify 将 Transcribe 处理后的文件上传到 S3。

系统将使用浏览器捕获语音音频并将其保存到 S3,使用 Amplify 库。一旦音频文件上传,就会调用笔记服务。这将启动一个 Transcribe 任务将音频转换为文本。客户端将定期轮询笔记服务以确定转换何时完成。最后,前端将使用转换后的文本填充笔记字段。

5.1.1 获取代码

此步骤的代码位于 chapter5/step-3-note-service 目录中。此目录包含步骤 2 的所有代码,以及我们的音频转录更改。与之前一样,我们将依次介绍更新,然后部署更改。

5.1.2 笔记服务

我们的笔记服务遵循现在应该已经熟悉的模式:代码位于 note-service 目录中,包含一个 serverless.yml 配置文件和实现。大部分都是样板代码:主要区别在于我们配置服务以访问 S3 数据桶和访问 Transcribe 服务。这位于我们的配置中的 iamRoleStatements 部分,如下所示。

列表 5.1 笔记服务角色声明

provider:
  ...
  iamRoleStatements:
    - Effect: Allow
      Action:              ❶
        - s3:PutObject
        - s3:GetObject
      Resource: "arn:aws:s3:::${self:custom.dataBucket}/*"
    - Effect: Allow
      Action:              ❷
        - transcribe:*
      Resource: "*"

❶ 音频文件数据桶

❷ 允许此服务访问转录。

笔记服务定义了两个路由:POST /noteGET /note/{id},分别用于创建和获取笔记。与待办事项 CRUD 路由一样,我们使用我们的 Cognito 池来锁定对笔记 API 的访问,并且我们使用相同的自定义域名结构,只是基础路径为 noteapi。我们的处理器代码使用 AWS SDK 创建转录任务,如下所示。

列表 5.2 笔记服务处理器

const AWS = require('aws-sdk')
var trans = new AWS.TranscribeService()                   ❶

module.exports.transcribe = (event, context, cb) => {
  const body = JSON.parse(event.body)

  const params = {
    LanguageCode: body.noteLang,
    Media: { MediaFileUri: body.noteUri },
    MediaFormat: body.noteFormat,
    TranscriptionJobName: body.noteName,
    MediaSampleRateHertz: body.noteSampleRate,
    Settings: {
      ChannelIdentification: false,
      MaxSpeakerLabels: 4,
      ShowSpeakerLabels: true
    }
  }

  trans.startTranscriptionJob(params, (err, data) => {   ❷
    respond(err, data, cb)
  })
}

❶ 创建转录服务对象。

❷ 开始异步转录任务。

如列表所示,代码相当简单,因为它只是调用单个 API 来启动任务,传递我们音频文件的链接。代码向客户端返回一个转录作业 ID,该 ID 在 poll 函数中使用。详细检查代码以查看 poll 的实现,它使用 getTranscriptionJob API 检查我们正在运行的作业的状态。

5.1.3 前端更新

为了提供转录功能,我们对前端进行了一些更新。首先,我们在 index.js 中添加了一些对 Amplify 库的配置。如下所示。

列表 5.3 更新的 Amplify 配置

Amplify.configure({
  Auth: {
    region: process.env.TARGET_REGION,
    userPoolId: process.env.CHAPTER4_POOL_ID,
    userPoolWebClientId: process.env.CHAPTER4_POOL_CLIENT_ID,
    identityPoolId: process.env.CHAPTER4_IDPOOL,
    mandatorySignIn: false,
    oauth: oauth
  },
  Storage: {                                     ❶
    bucket: process.env.CHAPTER4_DATA_BUCKET,
    region: process.env.TARGET_REGION,
    identityPoolId: process.env.CHAPTER4_IDPOOL,
    level: 'public'
  }
})

❶ 配置 Amplify 存储接口使用的 S3 桶。

此配置告诉 Amplify 使用我们在步骤 1 中设置的我们的数据桶。因为我们已经使用我们的 Cognito 设置配置了 Amplify,所以一旦登录,我们就可以从客户端访问此桶。

我们在 frontend/src/audio 目录中添加了一些音频处理代码。它使用浏览器媒体流录制 API 将音频录制到缓冲区。为了本书的目的,我们将此代码视为黑盒。

注意 更多关于媒体流录制 API 的信息可以在此处找到:mng.bz/X0AE

主要的笔记处理代码位于 note.jsnote-view.js 文件中。视图代码向用户界面添加了两个按钮:一个用于开始录音,另一个用于停止录音。这些按钮分别对应 note.js 中的 startRecordstopRecord 函数。stopRecord 函数的代码如下所示。

列表 5.4 stopRecord 函数

import {Storage} from 'aws-amplify'
...
function stopRecord () {
  const noteId = uuid()

  view.renderNote('Thinking')
  ac.stopRecording()
  ac.exportWAV((blob, recordedSampleRate) => {    ❶
    Storage.put(noteId + '.wav', blob)            ❷
      .then(result => {
        submitNote(noteId, recordedSampleRate)    ❸
      })
      .catch(err => {
        console.log(err)
      })
    ac.close()
  })
}

❶ 将录制缓冲区导出为 WAV 格式

❷ 使用 Amplify 将 WAV 文件保存到 S3。

❸ 提交 WAV 文件进行处理。

stopRecord 函数使用 Amplify 的 Storage 对象将 WAV(波形音频文件格式)文件直接写入 S3。然后调用 submitNote 函数,该函数调用我们的笔记服务 API /noteapi/note 来启动转录任务。submitNote 函数的代码如下所示。

列表 5.5 submitNote 函数

const API_ROOT = `https://chapter4api.${process.env.CHAPTER4_DOMAIN}
/noteapi/note/`
...
function submitNote (noteId, recordedSampleRate) {
  const body = {
    noteLang: 'en-US',
    noteUri: DATA_BUCKET_ROOT + noteId + '.wav',
    noteFormat: 'wav',
    noteName: noteId,
    noteSampleRate: recordedSampleRate
  }

  auth.session().then(session => {
    $.ajax(API_ROOT, {                    ❶
      data: JSON.stringify(body),
      contentType: 'application/json',
      type: 'POST',
      headers: {
        Authorization: session.idToken.jwtToken
      },
      success: function (body) {
        if (body.stat === 'ok') {
          pollNote(noteId)                ❷
        } else {
          $('#error').html(body.err)
        }
      }
    })
  }).catch(err => view.renderError(err))
}

❶ 调用笔记服务。

❷ 进入轮询状态

我们的轮询函数在后台调用笔记服务以检查转录任务的进度。轮询函数的代码如下所示。

列表 5.6 note.js pollNote 函数

function pollNote (noteId) {
  let count = 0
  itv = setInterval(() => {
    auth.session().then(session => {                                  ❶
      $.ajax(API_ROOT + noteId, {                                     ❷
        type: 'GET',
        headers: {
          Authorization: session.idToken.jwtToken
        },
        success: function (body) {
          if (body.transcribeStatus === 'COMPLETED') {
            clearInterval(itv)
            view.renderNote(body.results.transcripts[0].transcript)   ❸
          } else if (body.transcribeStatus === 'FAILED') {
            clearInterval(itv)
            view.renderNote('FAILED')
          } else {
            count++
            ...
          }
        }
      })
    }).catch(err => view.renderError(err))
  }, 3000)
}

❶ 使用 Cognito 获取认证会话。

❷ 调用 API 检查笔记状态。

❸ 如果转录完成,渲染转录的笔记。

作业完成后,生成的文本将被渲染到页面上的笔记输入字段中。

投票

轮询通常不是处理事件的有效方式,并且肯定不适合扩展。我们在这里使用轮询确实暴露了 AWS Lambda 的一个缺点,即函数通常预期在短时间内执行。这使得它们不适合可能需要长期连接的应用程序。当作业完成时,建立 WebSocket 连接并向下推送更新是一个更好的接收更新方式。这更有效率,并且可以很好地扩展。

在这里,有几个更好的选项可以用来代替轮询,例如

  • 使用 AWS API Gateway 和 WebSockets——mng.bz/yr2e

  • 使用第三方服务,如 Fanout——fanout.io/

当然,最佳方法将取决于具体的系统。这些方法的描述超出了本书的范围,这就是为什么我们为笔记服务使用了简单的基于轮询的方法。

5.1.4 部署步骤 3

让我们部署笔记功能。首先,我们需要设置我们的环境。为此,只需将step-2-cognito-login中的.env文件复制到step-3-note-service

接下来,我们将部署我们的新笔记服务。在step-3-note-service/note-service目录下使用cd命令,然后运行

$ npm install
$ serverless deploy

这将在 API Gateway 中创建我们的笔记服务端点并安装我们的两个 Lambda 函数。接下来,部署前端更新。在step-3-note-service/frontend目录下使用cd命令并运行

$ source ../.env
$ npm install
$ npm run build
$ aws s3 sync dist/ s3://$CHAPTER4_BUCKET

5.1.5 测试步骤 3

让我们尝试一下我们新的语音转文字功能。在浏览器中打开待办事项应用并像之前一样登录。点击创建新待办事项的按钮,并输入一个动作和一个日期。你应该会看到如图 5.2 所示的额外两个按钮:一个录音按钮和一个停止按钮。

图 5.2 录音笔记

点击录音按钮开始说话!说完后,点击停止按钮。几秒钟后,你应该会看到你刚才口述的笔记被渲染成文本,出现在笔记字段中,这样你就可以继续保存新的待办事项,包括转录的笔记。

将音频转录成文本的时间是可变的,这取决于当前正在进行的全球转录作业数量。在最坏的情况下,转录完成可能需要 20 到 30 秒。虽然待办事项上的笔记是展示 AWS Transcribe 的一种方式,但请注意,我们使用的 API 是针对批量处理优化的,并且可以转录包含多个说话者的大型音频文件——例如,董事会会议或采访。我们将在本章后面的步骤 5 中介绍一个更快的对话界面。然而,我们应该指出,在最近的服务更新中,AWS Transcribe 现在也支持实时处理以及批量模式。

5.2 步骤 4:添加文本到语音

我们将要添加到待办事项列表中的下一个 AI 功能是笔记服务的逆过程。我们的调度服务将从我们的待办事项列表中构建每日日程,然后将其读给我们听。我们将使用 AWS Polly 来实现这一点。Polly 是 AWS 语音到文本服务。我们可以通过 API 的方式将其连接到我们的系统,类似于我们的笔记服务。图 5.3 描述了调度服务的架构结构。

图 5.3 记录笔记

当我们的系统用户请求日程时,会调用我们的调度服务,该服务创建文本形式的日程并将其提交给 Amazon Polly。Polly 解释文本并将其转换为音频。音频文件被写入我们的 S3 数据存储桶,一旦可用,我们就将其播放给我们的用户。再次强调,这对于一个高级功能来说工作量很小!

5.2.1 获取代码

此步骤的代码位于chapter5/step-4-schedule-service目录中。此目录包含步骤 3 的所有代码以及我们的调度服务。与之前一样,我们将依次介绍更新,然后部署更改。

5.2.2 调度服务

我们的调度服务与笔记服务类似,因为它使用与之前相同的域管理器结构提供两个 API 端点:

/schedule/day--为今天创建日程并提交文本到语音任务给 Polly

/schedule/poll--检查作业状态,一旦完成返回音频文件的引用

这种结构反映在serverless.yml配置中,在这个阶段应该非常熟悉。这两个端点daypoll的实现位于handler.js中。首先,让我们看看day处理程序使用的buildSchedule函数。如下列表所示。

列表 5.7 调度服务day处理程序中的buildSchedule函数

const dynamoDb = new AWS.DynamoDB.DocumentClient()
const polly = new AWS.Polly()                                ❶
const s3 = new AWS.S3()
const TABLE_NAME = { TableName: process.env.TODO_TABLE }     ❷
...
function buildSchedule (date, speakDate, cb) {               ❸
  let speech = '<s>Your schedule for ' + speakDate + '</s>'
  let added = false
  const params = TABLE_NAME

  dynamoDb.scan(params, (err, data) => {                     ❹
    data.Items.forEach((item) => {
      if (item.dueDate === date) {
        added = true
        speech += '<s>' + item.action + '</s>'
        speech += '<s>' + item.note + '</s>'
      }
    })
    if (!added) {
      speech += '<s>You have no scheduled actions</s>'
    }
    const ssml = `<speak><p>${speech}</p></speak>`
    cb(err, {ssml: ssml})
  })
}

❶ 创建 SDK Polly 对象。

❷ 从环境中获取待办事项表。

❸ 定义构建 SSML 日程的函数。

❹ 从 DynamoDB 读取日程事项并创建到期事项的 SSML。

我们已经看到buildSchedule函数如何读取给定日期的待办事项并创建 SSML。这被调度服务中的day处理程序使用。下一列表显示了此处理程序的代码。

列表 5.8 调度服务day处理程序

module.exports.day = (event, context, cb) => {
  let date = moment().format('MM/DD/YYYY')
  let speakDate = moment().format('dddd, MMMM Do YYYY')
  buildSchedule(date, speakDate, (err, schedule) => {
    if (err) { return respond(err, null, cb) }

    const params = {                                          ❶
      OutputFormat: 'mp3',
      SampleRate: '8000',
      Text: schedule.ssml,
      LanguageCode: 'en-GB',
      TextType: 'ssml',
      VoiceId: 'Joanna',
      OutputS3BucketName: process.env.CHAPTER4_DATA_BUCKET,
      OutputS3KeyPrefix: 'schedule'
    }

    polly.startSpeechSynthesisTask(params, (err, data) => {   ❷
      ...
      respond(err, result, cb)
    })
  })
}

❶ 配置 Polly 的语音和输出存储桶参数。

❷ 启动 Polly 语音合成任务。

buildSchedule函数创建了一个 SSML 块,传递给 Polly,Polly 将将其转换为输出mp3文件。我们的day函数设置了一个参数块,指定输出格式,以及 Polly 应将输出放置的 S3 存储桶。下一列表中的代码显示了poll处理程序。

列表 5.9 调度服务的poll处理程序

module.exports.poll = (event, context, cb) => {
  polly.getSpeechSynthesisTask({TaskId: event.pathParameters.id}, (err, data) => {  ❶
    // Create result object from data
    ...
    respond(err, result, cb)                                        ❷
  })
}

❶ 检查任务状态。

❷ 向 API 调用者提供任务状态。

检查处理器代码显示了 Lambda 函数调用 Polly 服务以检索语音合成任务。这包含在 API 响应中。

SSML

语音合成标记语言 (SSML) 是一种用于文本到语音任务的 XML 方言。虽然 Polly 可以处理纯文本,但 SSML 可以用于为语音合成任务提供额外的上下文。例如,以下 SSML 使用了耳语效果:

<speak>
  I want to tell you a secret.
  <amazon:effect name="whispered">I am not a real human.</amazon:effect>.
  Can you believe it?
</speak>

更多关于 SSML 的信息可以在这里找到:mng.bz/MoW8

一旦我们的语音到文本任务启动,我们使用 poll 处理器来检查状态。这调用 polly.getSpeechSynthesisTask 来确定任务的状态。一旦我们的任务完成,我们使用 s3.getSignedUrl 生成一个临时 URL 来访问生成的 mp3 文件。

5.2.3 前端更新

要访问我们的调度服务,我们在应用程序的导航栏中放置一个“调度”按钮,如图 5.4 所示。

图片

图 5.4 更新后的用户界面

这与文件 frontend/src/schedule.js 中的前端处理器相连,如下所示。

列表 5.10 schedule.js

import $ from 'jquery'
import {view} from './schedule-view'
...
const API_ROOT = `https://chapter4api.${process.env.CHAPTER4_DOMAIN}
/schedule/day/`
let itv
let auth

function playSchedule (url) {                    ❶
  let audio = document.createElement('audio')
  audio.src = url
  audio.play()
}

function pollSchedule (taskId) {                 ❷
  itv = setInterval(() => {
    ...
    $.ajax(API_ROOT + taskId, {                  ❷
      ...
      playSchedule(body.signedUrl)               ❸
      ...
  }, 3000)
}

function buildSchedule (date) {
  const body = { date: date }

  auth.session().then(session => {
    $.ajax(API_ROOT, {                           ❹
      ...
      pollSchedule(body.taskId)
      ...
    })
  }).catch(err => view.renderError(err))
}

❶ 播放调度文件。

❷ 检查调度状态。

❸ 将签名 URL 传递给播放器。

❹ 启动调度任务。

使用来自 S3 的临时签名 URL 允许前端代码使用标准的音频元素播放调度,而不会损害我们数据桶的安全性。

5.2.4 部署步骤 4

到现在为止,这一步骤的部署应该非常熟悉。首先,我们需要从上一个步骤复制我们的环境。将文件 step-3-note-service/.env 复制到 step-4-schedule-service

接下来,通过执行以下命令部署调度服务:

$ cd step-4-schedule-service/schedule-service
$ npm install
$ serverless deploy

最后,像之前一样部署前端更新:

$ cd step-4-schedule-service/frontend
$ source ../.env
$ npm install
$ npm run build
$ aws s3 sync dist/ s3://$CHAPTER4_BUCKET

5.2.5 测试步骤 4

让我们现在让我们的待办事项列表读出我们当天的日程。在浏览器中打开应用程序,登录,并为今天的日期创建一些待办事项。一旦你输入了一到两个条目,点击调度按钮。这将触发调度服务构建并发送我们的日程到 Polly。几秒钟后,应用程序将为我们读出我们的日程!

我们现在有一个可以与之交谈的待办事项系统,并且它可以对我们说话。我们的待办事项存储在数据库中,系统通过用户名和密码进行安全保护。所有这些都不需要启动服务器或深入了解文本/语音转换的细节!

在我们对待办事项系统的最终更新中,将通过构建聊天机器人来为系统添加一个更会话式的界面。

5.3 步骤 5:添加会话式聊天机器人界面

在我们对待办事项应用程序的最终更新中,我们将实现一个聊天机器人。聊天机器人将允许我们通过基于文本的界面或通过语音与系统交互。我们将使用 Amazon Lex 来构建我们的机器人。Lex 使用与 Amazon Alexa 相同的 AI 技术。这意味着我们可以使用 Lex 为我们的系统创建一个更自然的人机界面。例如,我们可以要求我们的应用程序为“明天”或“下周三”安排待办事项。虽然这对人类来说是一种表达日期的自然方式,但实际上对计算机来说理解这些模糊的命令非常复杂。当然,通过使用 Lex,我们可以免费获得所有这些功能。图 5.5 展示了我们的聊天机器人如何集成到我们的系统中。

图 5.5 更新后的用户界面

用户可以通过聊天窗口或通过说话来提供命令。这些命令被发送到由 Lex 托管的我们的聊天机器人,并返回一个响应。在对话结束时,机器人将收集创建或更新待办事项所需的所有信息。然后前端将此信息发布到待办事项 API,就像之前一样。

在这一点上需要注意的是,我们不需要更改我们底层的待办事项 API,以便向其添加对话界面。这可以在现有代码的最小干扰上叠加。

5.3.1 获取代码

此步骤的代码位于 chapter5/step-5-chat-bot 目录中。此目录包含步骤 4 的所有代码以及与我们的聊天机器人交互的代码。

5.3.2 创建机器人

我们已经创建了一个用于创建我们的 todo 机器人的命令行脚本。此代码位于 chapter5/step-5-chat-bot/bot 目录中。文件 create.sh 使用 AWS 命令行设置机器人,如下所示。

列表 5.11 创建聊天机器人的脚本

#!/bin/bash
ROLE_EXISTS=`aws iam get-role \
--role-name AWSServiceRoleForLexBots \
| jq '.Role.RoleName == "AWSServiceRoleForLexBots"'`

if [ ! $ROLE_EXISTS ]                             ❶
then
  aws iam create-service-linked-role --aws-service-name lex.amazonaws.com
fi

aws lex-models put-intent \                       ❷
--name=CreateTodo \
--cli-input-json=file://create-todo-intent.json

aws lex-models put-intent \                       ❸
--name=MarkDone \
--cli-input-json=file://mark-done-intent.json

aws lex-models create-intent-version --name=CreateTodo
aws lex-models create-intent-version --name=MarkDone

aws lex-models put-bot --name=todo \              ❹
--locale=en-US --no-child-directed \
--cli-input-json=file://todo-bot.json

❶ 如有必要,创建服务角色

❷ 定义创建待办事项的意图。

❸ 定义标记完成的意图。

❹ 定义机器人。

注意:create.sh 脚本使用 jq 命令,这是一个用于处理 JSON 数据的命令行工具。如果您的开发环境中没有安装它,您需要使用系统包管理器进行安装。

此脚本使用一些 JSON 文件来定义我们聊天机器人的特性。请运行 create.sh 脚本。创建我们的机器人可能需要几秒钟的时间;我们可以通过运行以下命令来检查进度:

$ aws lex-models get-bot --name=todo --version-or-alias="\$LATEST"

一旦此命令的输出包含 "status": "READY",我们的机器人就可以使用了。在网页浏览器中打开 AWS 控制台,并从服务列表中选择 Lex。点击 todo 机器人的链接。

注意:您在首次创建机器人时可能会看到错误消息,例如:“无法找到名为 AWSServiceRoleForLexBots 的角色”。这是因为 Lex 在账户中首次创建机器人时创建此服务角色。

图 5.6 更新后的用户界面

您的控制台应该看起来像图 5.6 所示。一开始这可能看起来有点复杂,但一旦我们理解了三个关键概念:意图、话语和插槽,配置实际上非常简单。

意图

一个意图是我们想要实现的目标;例如,“订购披萨”或“预约”。将意图视为机器人的整体任务,它将需要收集额外数据以完成任务。一个机器人可以有多个意图,但通常这些都与某个中心概念相关。例如,一个订购披萨的机器人可能有“订购披萨”、“检查配送时间”、“取消订单”、“更新订单”等意图。

todo机器人的情况下,我们有两个意图:CreateTodoMarkDone

话语

一个话语是用来识别意图的短语。对于我们的CreateTodo意图,我们定义了创建待办事项新待办事项这两个话语。重要的是要理解,话语不是一组必须精确提供的关键词。Lex 使用多种 AI 技术来匹配话语和意图。例如,我们的创建意图可以通过以下任何一种方式被识别:

  • “初始化待办事项”

  • “获取待办事项”

  • “我想要一个新的待办事项,请”

  • “为我创建待办事项”

话语为 Lex 提供示例语言,而不是需要精确匹配的关键词。

插槽

一个插槽可以被视为 Lex 对话的输出变量。Lex 将使用对话来获取插槽信息。对于我们的CreateTodo意图,我们定义了两个插槽:dueDateaction。我们为这些插槽使用了内置的插槽类型AMAZON.DATEAMAZON.EventType。在大多数情况下,内置的插槽类型提供了足够的信息;然而,根据机器人的需求,可以定义自定义的插槽类型。

Lex 将使用插槽类型作为帮助理解响应的手段。例如,当 Lex 提示我们输入日期时,它可以处理大多数合理的响应,例如

  • 明天

  • 星期四

  • 下周三

  • 圣诞节

  • 2019 劳动节

  • 从今天起一个月

这允许通过文本或语音实现灵活的对话界面

尝试一下

让我们测试一下我们的机器人!点击右上角的构建按钮,等待构建完成。然后选择测试聊天机器人链接,在右侧弹出一个消息面板,并尝试创建一些待办事项。图 5.7 展示了一个示例会话。

图片

图 5.7 更新后的用户界面

除了向机器人输入命令外,您还可以使用麦克风按钮对机器人说出语音命令,并让它以音频形式回复。需要注意的是,Lex 已从松散结构的对话中提取了结构化信息。然后我们可以将这些提取的结构化数据用于我们的代码中。

5.3.3 前端更新

现在我们已经有一个工作的机器人,是时候将其集成到我们的应用程序中了。更新前端的代码位于 chapter5/step-5-chat-bot/frontend 目录中。主要的机器人集成在 src/bot.js 中。首先,让我们看看以下列表中所示的 activate 函数。

列表 5.12 bot.js activate 函数

import $ from 'jquery'
import * as LexRuntime from 'aws-sdk/clients/lexruntime'             ❶
import moment from 'moment'
import view from 'bot-view'

const bot = {activate}
export {bot}

let ac
let auth
let todo
let lexruntime
let lexUserId = 'chatbot-demo' + Date.now()
let sessionAttributes = {}
let recording = false

...

function activate (authObj, todoObj) {
  auth = authObj
  todo = todoObj
  auth.credentials().then(creds => {
    lexruntime = new LexRuntime({region: process.env.TARGET_REGION,
      credentials: creds})                                          ❷
    $('#chat-input').keypress(function (e) {                        ❸
      if (e.which === 13) {
        pushChat()                                                  ❹
        e.preventDefault()
        return false
      }
    })
    bindRecord()
  })
}

❶ 导入 Lex API

❷ 配置 Lex 的区域和凭证

❸ 获取输入的文本。

❹ 使用输入的文本调用 pushChat。

LexRuntime 是 AWS SDK 服务的接口,用于处理 Lex 聊天机器人服务。它有两个方法用于将用户输入发送到 Lex。一个方法 postContent 支持音频和文本流。更简单的方法 postText 仅支持发送文本形式的用户输入。在这个应用程序中,我们将使用 postText。下一个列表显示了将前端捕获的输入文本传递给 Lex 的代码。

列表 5.13 bot.js pushChat 函数

function pushChat () {
  var chatInput = document.getElementById('chat-input')

  if (chatInput && chatInput.value && chatInput.value.trim().length > 0) {
    var input = chatInput.value.trim()
    chatInput.value = '...'
    chatInput.locked = true

    var params = {                                         ❶
      botAlias: '$LATEST',
      botName: 'todo',
      inputText: input,
      userId: lexUserId,
      sessionAttributes: sessionAttributes
    }

    view.showRequest(input)
    lexruntime.postText(params, function (err, data) {     ❷
      if (err) {
        console.log(err, err.stack)
        view.showError('Error:  ' + err.message + ' (see console for details)')
      }
      if (data) {
        sessionAttributes = data.sessionAttributes
        if (data.dialogState === 'ReadyForFulfillment') {
          todo.createTodo({                                ❸
            id: '',
            note: '',
            dueDate: moment(data.slots.dueDate).format('MM/DD/YYYY'),
            action: data.slots.action,
            stat: 'open'
          }, function () {})
        }
        view.showResponse(data)
      }
      chatInput.value = ''
      chatInput.locked = false
    })
  }
  return false
}

❶ 配置参数

❷ 向机器人发送文本

❸ 创建新的待办事项

bot.js,以及 bot-view.js 中的某些显示函数,通过 postText API 实现了一个简单的文本消息接口到我们的机器人。这会将用户的文本输入发送到 Lex 并引发响应。一旦我们的两个槽位 dueDateaction 被填充,Lex 将将响应数据 dialogState 设置为 ReadyForFulfillment。此时,我们可以从 Lex 响应中读取槽位数据,为我们的 to-do 项创建一个 JSON 结构,并将其发布到我们的待办事项 API。

我们还有一个 pushVoice 函数,我们已经将其连接到浏览器音频系统。这个函数与 pushChat 函数类似,但它会将音频推送到机器人。如果我们向机器人推送音频(即语音命令),它将像以前一样以文本形式响应,但还会在附加到响应数据对象的 audioStream 字段中包含音频响应。playResponse 函数接受这个音频流并简单地播放它,这样我们就可以与机器人进行语音激活的对话。

5.3.4 部署步骤 5

由于我们已经部署了我们的机器人,我们只需要更新前端。像以前一样,将步骤 4 中的 .env 文件复制到步骤 5 目录中,并运行以下列表中的命令以部署新版本。

列表 5.14 将部署命令更新前端

$ cd step-5-chat-bot/frontend
$ source ../.env
$ npm install                                 ❶
$ npm run build )                             ❷
$ aws s3 sync dist/ s3://$CHAPTER4_BUCKET     ❸

❶ 安装依赖项

❷ 创建前端静态资源的生产构建。

❸ 将静态网站复制到 S3 桶中。

更新后的前端现在已经部署完成。

5.3.5 测试步骤 5

打开浏览器并加载最新更改。登录后,你应该能在页面右侧看到聊天机器人界面,如图 5.8 所示。

图 5.8 更新后的 UI

你现在应该能够在与待办事项应用程序的上下文中与机器人交互。一旦对话完成,待办事项列表中将会创建一个新的待办事项!

虽然我们编写了大量代码来实现这一点,但代码实现起来相当简单。大多数时候我们只是在调用外部 API,这是大多数在职程序员熟悉的工作。通过调用这些 API,我们能够在待办事项列表中添加高级人工智能功能,而无需了解任何自然语言处理或语音到文本翻译的科学。

语音和聊天机器人界面变得越来越普遍,尤其是在移动应用程序中。我们最近遇到的一些很好的用例包括

  • 集成在网页中的第一行客户支持和销售咨询

  • 用于安排会议的个人助理

  • 旅行助手,帮助预订航班和酒店

  • 电子商务网站的个人购物助手

  • 医疗保健和激励型机器人,以促进生活方式的改变

希望这一章能激发你将这项技术应用到自己的工作中!

5.4 移除系统

一旦您完成对系统的测试,应完全移除,以避免产生额外费用。这可以通过使用 serverless remove 命令手动完成。我们还在 chapter5/step-5-chat-bot 目录中提供了一个脚本,用于删除第四章和第五章中部署的所有资源。在 bot 子目录中还有一个单独的 remove.sh 脚本。要使用这些脚本,请执行以下命令:

$ cd chapter5/step-5-chat-bot
$ bash ./remove.sh
$ cd bot
$ bash ./remove.sh

如果您想在任何时候重新部署系统,同一文件夹中有一个名为 deploy.sh 的相关脚本。这将通过自动化第四章和本章中我们已执行的步骤为您重新部署整个系统。

摘要

  • AWS Transcribe 用于将语音转换为文本。Transcribe 允许我们指定一个文件、文件格式和语言参数,并启动转录作业。

  • 使用 AWS Amplify 将数据上传到 S3 桶。我们可以通过使用 Amplify 的 Storage 接口将浏览器捕获的音频保存为 WAV 文件。

  • 语音合成标记语言 (SSML) 用于定义对话式语音。

  • AWS Polly 用于将文本转换为语音。

  • AWS Lex 用于创建强大的聊天机器人。

  • Lex 的语句、意图和槽位是构建 Lex 聊天机器人的组件。

警告 请确保您完全移除本章中部署的所有云资源,以避免产生额外费用!

6 如何有效地使用 AI 即服务

本章涵盖

  • 为快速和有效开发构建无服务器项目结构

  • 构建无服务器持续交付管道

  • 通过集中式、结构化日志实现可观察性

  • 监控生产中的无服务器项目指标

  • 通过分布式跟踪理解应用程序行为

到目前为止,我们已经构建了一些非常吸引人的基于 AI 的无服务器应用程序。这些系统具有非凡的功能,代码量却很少。然而,你可能已经注意到,我们的无服务器 AI 应用程序有很多组成部分。我们坚持单一职责原则,确保每个应用程序由许多小型单元组成,每个单元都有其特定的目的。本章是关于有效的 AI 即服务。这意味着我们不仅超越了简单的应用原型,还开发出了能够为真实用户服务的生产级应用程序。为此,我们需要考虑的不仅仅是如何让基础知识工作,还要考虑何时可能会出现问题。

我们已经清楚地阐述了小型代码单元和现成、托管服务的优势。让我们退一步,从建筑师和开发人员从更传统的软件开发方式转变的角度来思考这种方法的优缺点。

我们将概述主要挑战如何与结构化、监控和部署应用程序相关,以确保您在保证质量和可靠性的同时继续快速交付。这包括拥有清晰的项目布局、一个有效的持续交付管道,以及在出现问题时能够快速了解应用程序行为的能力。

本章将提供克服每个挑战的实际解决方案,并帮助您建立有效的无服务器开发实践。

6.1 应对无服务器的全新挑战

由于本书中我们已经成功部署了出色的无服务器 AI 应用程序,很容易被误导,认为一切都会一帆风顺!就像任何软件开发方式一样,都有其缺点和需要注意的陷阱。通常,这些问题只有在构建并将系统投入生产后才会遇到。为了帮助您预见潜在问题并在问题出现之前解决它们,我们将列出无服务器开发的优缺点。然后,我们将展示一个模板项目,您可以用它作为自己私有项目的起点。目的是节省您在这些问题出现时可能花费的时间和挫折。

6.1.1 无服务器的优势和挑战

表 6.1 列出了使用托管 AI 服务开发无服务器应用程序的主要优势和挑战。

表 6.1 无服务器应用程序的优势和挑战

优势 挑战
按需计算允许你快速开始并扩展,无需管理任何基础设施。 你依赖于云服务提供商的环境来准确运行你的代码。
较小的部署单元允许你遵守单一责任原则。这些单元开发速度快,维护相对容易,因为它们有明确的目的和接口。维护这些组件的团队不需要考虑整个系统的微妙细节。 要真正实现无服务器,有一个显著的学习曲线。理解有效的无服务器架构、学习可用的管理服务以及建立有效的项目结构都需要时间。
管理计算、通信、存储和机器学习的服务,只需最小的设计和编程努力,就能大幅提升你的能力。同时,你将不再需要承担如果要在自己的组织中构建这种能力所必须承担的维护和基础设施负担。 无服务器-微服务架构的分布式和碎片化特性使得整体系统行为的可视化和推理变得更加困难。
在无服务器系统中,你只需为所使用的部分付费,消除了浪费,并允许你根据业务成功进行扩展。 虽然无服务器减少了你需要考虑的安全责任中的系统数量,但它与传统方法相当不同。例如,一个恶意攻击者通过使用过权限的 IAM 策略访问 AWS Lambda 执行环境,可能会允许攻击者访问你的资源和数据,以及消耗可能无限的 AWS 资源,如更多的 Lambda 执行、EC2 实例或数据库。这可能会从你的云服务提供商那里产生一笔巨额账单。
无服务器方法允许你选择多个管理数据库服务,确保任何工作都有合适的工具。这种“多语言持久性”与过去尝试为大多数情况选择一个数据库的经验截然不同,这导致了沉重的维护负担,并且不适合某些数据访问需求。 当你的团队需要具备正确使用它们的技能和理解时,处理多个数据库可能是一个挑战。虽然像 DynamoDB 这样的服务容易上手,但管理变更和确保最佳性能是一项必须通过学习和经验获得的新技能。
无服务器项目创建成本低廉,因此可以针对不同的环境多次重建。 动态创建的云资源通常会被赋予生成的名称。允许服务被其他组件发现是确保松散耦合、服务可用性和部署便捷性之间平衡必须解决的问题。

这些挑战和好处被提出,以提供一个清晰和诚实的无服务器软件在生产中的现实情况。现在,你已经意识到了陷阱以及潜在的收益,我们准备讨论如何避免陷阱并最大化你项目的效果。我们将借助一个包含许多现成解决方案的参考项目来完成这项工作。

6.1.2 一个生产级无服务器模板

本书作者在构建无服务器应用程序和体验所有好处与挑战方面投入了大量的时间。因此,我们建立了一套最佳实践。我们决定将这些实践整合到一个模板中,以便我们可以用它来快速启动新的无服务器项目。我们还决定开源这个项目,使其对任何构建生产级无服务器应用程序的人开放。它旨在作为一个学习资源,并允许我们从更广泛的社区中收集想法和反馈。

这个名为 SLIC Starter 的项目免费使用,并欢迎贡献。SLIC 代表无服务器、精益、智能和持续。你可以在 GitHub 上找到它:github.com/fourTheorem/slic-starter。从头开始创建生产级无服务器应用程序可能会令人望而却步。有许多选择和决策需要做出。SLIC Starter 的目的是回答 80% 的这些问题,以便我们能够尽可能快地开始构建有意义的业务功能。需要做出决策的领域在图 6.1 中显示。

图片

图 6.1 需要做出决策的无服务器项目方面。SLIC Starter 旨在为这些主题中的每一个提供模板,以便采用者能够更快地进入生产阶段。

SLIC Starter 是一个模板,可以应用于任何行业中的任何应用程序。它附带一个用于管理清单的示例应用程序。这个名为 SLIC Lists 的应用程序故意设计得简单,但具有足够的需求,使我们能够应用许多无服务器最佳实践。一旦你熟悉了 SLIC Starter,你可以用你自己的应用程序功能替换 SLIC Lists 应用程序。示例 SLIC Lists 应用程序具有以下功能:

  • 用户可以注册和登录。

  • 用户可以创建、编辑和删除清单。

  • 用户可以在清单中创建条目并将它们标记为已完成。

  • 任何清单都可以通过提供他们的电子邮件地址与其他用户共享。收件人必须接受邀请并登录或创建账户以查看和编辑列表。

  • 当用户创建清单时,他们会收到一封“欢迎邮件”,通知他们已创建该列表。

我们系统的组件在图 6.2 中展示。显示的主要组件或 服务 如下:

  • 清单服务 负责存储和检索列表及其条目。它由数据库支持,并为授权用户提供公共 API。

    图 6.2 SLIC 列表应用的 SLIC 启动服务。该应用由五个后端服务组成。还有一个前端组件,以及处理证书和域的附加服务。

  • 邮件服务 负责发送邮件。邮件通过入站队列传递到这项服务。

  • 用户服务 管理用户和账户。它还提供了一个内部 API,用于访问用户数据。

  • 欢迎服务 在用户创建清单时向用户发送欢迎通知消息。

  • 共享服务 处理向新合作者发送共享列表的邀请。

  • 前端 负责前端 Web 应用的构建、部署和分发。它通过配置与公共、后端服务相连。

此外,我们还提供支持证书部署和创建面向公众的 API 域的服务。

这个应用所做的事情可能与你自己的应用不太相关,但如何构建这个应用应该非常相关。图 6.1 已经展示了你在构建成熟、生产级软件应用时最终需要考虑的基础性考虑因素。清单应用为每个这些考虑因素提供了一个模板,并作为学习资源,帮助你应对挑战,而无需花费太多时间停下来研究所有可能的解决方案。我们首先考虑的是如何结构化项目代码库和仓库。

6.2 建立项目结构

在项目快速扩展之前,建立清晰的项目和源代码库结构是一个好主意。如果你不这样做,团队成员在做出更改和添加新功能时可能会感到困惑,尤其是在新成员加入项目时。这里有许多选择,但我们希望优化在许多开发者共同构建、部署和运行新功能和修改的协作环境中的快速、高效开发。

6.2.1 源代码库--单代码库或多代码库

你如何组织团队的代码似乎是一个微不足道的话题。但正如我们在许多项目后发现的那样,关于如何进行简单决策会对你能够多快地做出更改并发布它们,以及开发者如何进行沟通和协作产生重大影响。这部分原因在于你是否选择多仓库或单仓库。多仓库是指在一个应用程序中,每个服务、组件或模块使用多个源代码控制仓库。在一个具有多个前端(如网页、移动端等)的微服务项目中,这可能导致数百或数千个仓库。单仓库是指所有服务和前端代码库都保存在单个仓库中。

谷歌、Facebook 和推特因在极其大规模上使用单仓库而闻名。当然,仅仅因为谷歌/Facebook/推特这么说就采取某种方法从来不是一个好主意。相反,就像其他所有事情一样,衡量这种方法对你产生的影响,并做出对你的组织有利的决策。图 6.3 展示了两种方法之间的差异。

图片

图 6.3 单仓库与多仓库对比。单仓库包含多个服务、支持库和基础设施即代码(IaC)在一个仓库中。多仓库倾向于为每个单独的组件使用单独的仓库。

多仓库(polyrepo)方法有一定的优势。例如,每个模块可以单独版本控制,并且可以拥有细粒度的访问控制。然而,根据我们的经验,在多个仓库之间进行协调管理会花费太多时间。随着更多服务、库和依赖项的增加,开销会迅速失控。通常,需要使用定制工具来管理跨仓库的依赖项。新开发者应该能够尽快开始为你的产品工作。避免不必要的仪式和任何仅适用于你团队或公司的独特学习曲线。

在单仓库(monorepo)模式下,当修复错误或添加功能影响多个模块/微服务时,所有更改都在同一个仓库中进行。单个仓库只有一个分支。不再需要在多个仓库之间进行跟踪。每个功能都有一个单独的拉取请求(pull request)。不会存在功能部分合并的风险。

通过坚持使用单个仓库,你的外部测试(端到端或 API 测试)也属于测试代码的一部分。同样适用于基础设施即代码(Infrastructure as Code)。任何需要更改的基础设施都会与应用程序代码一起捕获。如果你有被微服务使用的通用代码、实用工具和库,将它们保存在同一个仓库中可以非常容易地进行共享。

6.2.2 项目文件夹结构

SLIC Starter 仓库遵循单仓库(monorepo)方法。应用程序的布局与我们在这本书中已经描述的许多应用程序类似。每个服务都有自己的文件夹,包含一个 serverless.yml 文件。SLIC Starter 单仓库仓库中的项目结构在下一列表中展示。

列表 6.1 SLIC Starter 项目结构

???  certs/                Hosted zone and HTTPS Certificates (ACM)
???  api-service/          API Gateway custom domain
???  checklist-service/    API Gateway for checklists, DynamoDB
???  welcome-service/      Event handler to send emails on checklist creation
???  sharing-service/      API Gateway list sharing invitations
???  email-service/        SQS, SES for email sending
???  user-service/         Internal API Gateway and Cognito for user accounts
???  frontend/             S3, CloudFront, ACM for front-end distribution
???  cicd/                 Dynamic pipelines and cross account roles
???  e2e-tests/            End-to-end tests using TestCafe
???  integration-tests/    API tests

6.2.3 获取代码

为了探索这个具有项目结构和为本章剩余部分做准备,请从 SLIC Starter GitHub 仓库获取代码。如果你想在章节的后面自动构建和部署应用程序,你需要将此代码放在你控制的仓库中。为了实现这一点,在克隆之前先对 SLIC Starter 仓库进行分支(github.com/fourTheorem/slic-starter):

$ git clone https://github.com/<your_user_or_organization>/slic-starter.git

你现在应该对有效项目结构的意义有了稳固的理解。你也有权访问一个体现这种结构的模板项目。我们接下来的考虑是关于自动化部署项目组件。

6.3 持续部署

到目前为止,我们所有的无服务器应用程序都是手动部署的。我们依赖 Serverless Framework 的 serverless deploy 命令将每个服务部署到特定的目标环境中。这对于早期开发和原型设计来说是可以的,特别是当我们的应用程序规模较小时。但是,当真实用户依赖于我们的应用程序,并且预期功能开发将频繁且快速时,手动部署就太慢且容易出错。

当你的应用程序由数百个独立可部署的组件组成时,你能想象手动部署应用程序的场景吗?现实世界的无服务器应用程序本质上都是复杂的分布式系统。你不能,也不应该依赖对它们如何组合在一起有一个清晰的思维模型。相反,你应该依靠自动化部署和测试的力量。

有效的无服务器应用程序需要持续部署。持续部署意味着我们的源代码仓库中的更改会自动传递到目标生产环境中。当触发持续部署时,受代码更改影响的任何组件都会被构建和测试。还有一个系统用于集成测试我们更改的组件,作为整个系统的一部分。一个合适的持续部署解决方案让我们有信心快速做出更改。持续部署的原则同样适用于数据集和机器学习模型的部署。

让我们从高层次的角度来看一个无服务器持续部署系统的设计。

6.3.1 持续部署设计

我们已经讨论了我们的服务器端应用程序方法如何倾向于存储在单仓库中的源代码。这影响了持续部署过程的触发方式。如果每个模块或服务都存储在其自己的独立仓库中,该仓库的更改可能会触发该服务的构建。那么挑战就变成了如何在多个仓库之间协调构建。对于单仓库方法,我们希望避免在只有少数提交影响一到两个模块时构建一切。请查看图 6.4 中展示的高级持续部署流程。

部署管道的阶段如下:

  1. 一个变更检测作业确定哪些模块受到源代码提交的影响。

  2. 管道随后触发每个模块的并行构建。这些构建作业也将为相关模块运行单元测试。

    图 6.4 我们的单仓库方法要求我们在触发每个受影响模块的并行构建和单元测试作业之前,检测哪些模块已更改。一旦成功,模块将被部署到预发布环境,在那里可以运行集成测试。成功的测试执行将触发向生产环境的部署。

  3. 当所有构建都成功时,模块将被部署到预发布环境。预发布环境是生产环境的副本,不对真实用户公开。

  4. 我们运行一系列自动化、端到端的测试,这让我们有信心在可预测的测试条件下,新的更改不会破坏系统中的基本功能。当然,在不可预测的生产条件下出现破坏性更改始终是可能的,你应该为此做好准备。

  5. 如果所有测试都成功,新模块将被部署到生产环境。

在我们的管道中,我们假设有两个目标环境——一个用于在上线前测试新更改的预发布环境,以及一个用于最终用户的生产环境。预发布环境完全是可选的。实际上,尽快将更改投入生产并采取有效措施来减轻风险是理想的。这些措施包括快速回滚的能力、蓝/绿或金丝雀部署模式 1,以及良好的可观察性实践。可观察性将在本章后面讨论。

现在我们已经了解了持续部署流程,让我们来看看我们如何使用自身也是无服务器的托管云构建服务来实现它!

6.3.2 使用 AWS 服务实现持续部署

对于托管您的持续构建和部署环境,有许多优秀的选项。这些包括从不朽的 Jenkins 到 SaaS 服务,如 CircleCI (circleci.com) 和 GitHub Actions (github.com/features/actions)。选择取决于对你和你的团队来说什么最有效。对于本章,我们将使用 AWS 构建服务,以保持选择云托管服务的主题。这种方法的优点是,我们将使用与应用程序本身相同的 Infrastructure-as-Code 方法。持续部署管道将使用 CloudFormation 构建,并驻留在 SLIC Starter 中的其他服务相同的单仓库中。

多账户和单账户部署

SLIC Starter 支持开箱即用的多账户部署。这允许我们为我们的预发布和生产环境使用单独的 AWS 账户,从而提供更高的隔离性和安全性。我们还可以使用一个单独的“工具”账户,其中将驻留持续部署管道和工件。这种方法需要时间来设置,并且对于许多用户来说,创建多个账户可能不可行。因此,单账户部署也是可能的。这是我们将在本章中介绍的选择。

构建持续部署管道

我们将为管道使用的 AWS 服务是 AWS CodeBuild 和 AWS CodePipeline。CodeBuild 允许我们执行构建步骤,如安装、编译和测试。通常会产生一个构建工件作为其输出。CodePipeline 允许我们将多个操作组合成阶段。操作可以包括源获取、CodeBuild 执行、部署和人工批准步骤。操作可以按顺序或并行运行。

在我们的仓库的master分支上每次提交或合并时,我们将并行构建和部署受影响的模块。为了实现这一点,我们将为每个模块创建一个单独的管道。这些管道将由一个单一的、整体的协调管道执行和监控。所有这些都可以在图 6.5 中看到。

图 6.5

图 6.5 SLIC Starter 的典型无服务器 CI/CD 架构是其一部分。它使用每个模块的 CodePipeline 管道。这些管道的并行执行由协调管道协调。构建、部署和测试阶段作为 CodeBuild 项目实现。

由于我们正在使用 AWS 服务构建管道,因此我们将使用 CloudFormation 堆栈进行部署,就像我们的无服务器应用程序一样。到目前为止,我们已经使用 Serverless Framework 构建了这些堆栈。对于部署堆栈,我们将使用 AWS Cloud Development Kit (CDK)代替。

CDK 提供了一种程序化的方式来构建 CloudFormation 模板。使用标准编程语言进行基础设施即代码(IaC)有其优缺点。我们更喜欢这种方式,因为它与我们构建应用程序的方式相似,但对于许多人来说,使用 JSON 或 YAML 之类的配置语言来定义基础设施可能更好。在这种情况下,它允许我们动态创建项目和管道,而不是依赖于静态配置。随着我们向应用程序添加新的模块,CDK 将自动生成新的资源。CDK 支持 JavaScript、Python、Java 和 TypeScript。我们正在使用 TypeScript,它是 JavaScript 的超集,为我们提供了类型安全。类型安全在创建具有复杂配置语法的资源时是一种强大的辅助工具。它允许我们利用自动完成功能并获得即时的文档提示。CDK 和 TypeScript 的详细覆盖超出了本书的范围。如果您对探索如何构建管道感兴趣,请探索cicd文件夹中的 CDK TypeScript 代码。我们将直接进入并部署我们的 CI/CD 管道!

部署和运行 CI/CD 管道的最新文档位于 SLIC Starter 存储库中的QUICK_START.md文档中。一旦您运行了所有步骤,您的管道就准备好了。对存储库的每次提交都将触发源 CodeBuild 项目,并导致编排管道的执行。图 6.6 显示了在 AWS CodePipeline 控制台中该管道的外观。

在这里,我们可以清楚地看到已经运行的管道步骤。当前的执行处于“审批”阶段。这是一个特殊的阶段,需要用户进行审查并点击“批准”才能推进管道。这给了我们检查和取消任何生产部署的机会。显示的执行已成功部署到预发布环境,并且我们的测试作业已成功完成。在 SLIC Starter 中,针对公共 API 和前端,并行运行自动化的 API 集成测试和用户界面端到端(E2E)测试。

一旦我们的系统已部署到生产环境,我们需要了解那里发生了什么。当事情出错时,我们需要能够进行故障排除并回答有关应用程序状态的许多问题。这让我们想到了可观察性,这可能是有效生产无服务器部署中最重要的一部分!

6.4 可观察性和监控

在本章的开头,我们描述的挑战之一是无服务器系统的碎片化特性。这是由许多小部分组成的分布式系统的常见问题。它可能导致对系统运行行为的理解不足,使得解决问题和进行更改变得困难。随着微服务架构的更广泛采用,这个问题已经得到了更好的理解。利用第三方托管服务的无服务器应用程序,这个问题尤其普遍。这些托管服务在某种程度上是黑盒。

图 6.6 典型的无服务器 CI/CD 架构是 SLIC Starter 的一部分。它为每个模块使用一个 CodePipeline 管道。这些管道的并行执行由一个编排管道协调。构建、部署和测试阶段作为 CodeBuild 项目实现。

我们能理解多少取决于这些服务提供报告其状态的接口。系统报告其状态的程度被称为可观察性。这个术语越来越多地被用来代替传统的监控术语。

监控与可观察性

监控通常指的是使用工具来检查系统的已知指标。监控应允许你检测问题何时发生,并推断出关于系统的某些知识。如果一个系统没有发出正确的输出,监控的效果将受到限制。

可观察性,2,是控制理论中的一个术语,它是指系统的一个属性,允许你通过查看其输出了解其内部情况。可观察性的目标是能够通过检查输出理解任何给定的问题。例如,如果我们必须更改系统并重新部署以了解正在发生的事情,那么该系统缺乏可观察性。

考虑这两个术语之间的区别的一种方式是,监控允许你检测已知问题何时发生,而可观察性旨在在未知问题发生时提供理解。

例如,假设你的应用程序有一个经过良好测试、正常工作的注册功能。有一天,用户抱怨他们无法完成注册。通过查看系统的视觉图,你确定注册模块中的错误是由于发送注册确认电子邮件失败造成的。通过进一步调查电子邮件服务中的错误,你注意到已经达到了电子邮件发送限制,这阻止了电子邮件的发送。模块之间的依赖关系和错误的视觉图引导你到电子邮件服务日志,其中提供了根本原因的详细信息。这些可观察性功能帮助解决了意外问题。

实现可观察性的方法有很多。对于我们这个清单应用程序,我们将探讨我们想要观察的内容以及如何使用 AWS 管理服务来实现这一点。我们将探讨可观察性的四个实际领域:

  • 结构化、集中的日志记录

  • 服务和应用程序指标

  • 当出现异常或错误条件时,会发出警报来提醒我们

  • 跟踪以让我们能够看到整个系统中消息的流动

6.5 日志

可以从许多 AWS 服务中收集日志。使用 AWS CloudTrail,甚至可以收集通过 AWS SDK 或管理控制台进行的资源更改相关的日志。在这里,我们将关注我们的应用程序日志,即由我们的 Lambda 函数创建的日志。我们的目标是创建应用程序中有意义事件的日志条目,包括信息日志、警告和错误。当前趋势使我们倾向于采用结构化日志方法,这是有充分理由的。非结构化的纯文本日志难以搜索。它们也难以被日志分析工具解析。基于结构的、基于 JSON 的日志可以轻松解析、过滤和搜索。结构化日志可以被视为应用程序的操作数据

在传统的、非无服务器环境中,日志通常收集在文件中或使用日志代理。使用 Lambda,这些选项实际上并不适用,因此方法变得更加简单。我们 Lambda 函数的任何控制台输出(无论是输出到标准输出还是标准错误)都会显示为日志输出。AWS Lambda 自动收集这些输出并将它们存储在 CloudWatch 日志中。这些日志存储在以 Lambda 函数名称命名的日志组中。例如,如果我们的 Lambda 函数名为checklist-service-dev-get,其日志将被收集在名为/aws/lambda/checklist-service-dev-get的 CloudWatch 日志组中。

CloudWatch 日志概念

CloudWatch 日志组织成日志组。日志组是一组相关的日志,通常与特定服务相关。在每一个日志组中有一组日志流。流是从同一来源的一组日志。对于 Lambda 函数,每个配置的容器只有一个日志流。日志流由一系列日志事件组成。日志事件只是记录到流中并关联时间戳的记录。

可以使用 API 或 AWS 管理控制台将日志存储在 CloudWatch 日志中以供检查。可以配置日志组以保留期来控制它们保留的时间。默认情况下,日志会永久保留。这通常不是最佳选择,因为与存档或删除日志相比,CloudWatch 中的日志存储要昂贵得多。

可以使用订阅过滤器将日志转发到其他服务。每个日志组可以设置一个订阅过滤器,允许设置一个过滤器模式和目的地。过滤器模式可以用来可选地提取仅与字符串匹配的消息。目的地可以是以下任何一种:

  • 一个 Lambda 函数。

  • 一个 Kinesis 数据流。

  • 一个 Kinesis Data Firehose 交付流。交付流可以用来收集存储在 S3、Elasticsearch 或 Splunk 中的日志。

存储集中日志的第三方选项有很多,包括流行的 Elasticsearch、Logstash 和 Kibana 组合,通常称为ELK 栈。ELK 解决方案经过测试,非常强大,能够执行复杂的查询并生成日志数据的可视化。为了简单起见,也因为它是许多应用程序的充分解决方案,我们将保留日志在 CloudWatch 中,并使用 CloudWatch 日志洞察来查看和查询它们。设置它比基于 Elasticsearch 的解决方案要少得多工作。首先,让我们处理我们如何生成结构化日志。

6.5.1 编写结构化日志

在选择如何编写日志时,目标应该是尽可能让开发者容易操作,并最小化对应用程序性能的影响。在 Node.js 应用程序中,Pino 日志记录器(getpino.io)完美地符合这一要求。其他选项包括 Bunyan(www.npmjs.com/package/bunyan)和 Winston(www.npmjs.com/package/winston)。我们使用 Pino,因为它专门为高性能和最小开销而设计。要在你的无服务器模块中安装它,请按照以下方式将其添加为依赖项:

npm install pino --save

还值得安装pino-pretty,这是一个辅助模块,它从 Pino 接收结构化日志输出并将其转换为人类可读的格式。这在通过命令行查看日志时非常理想:

npm install pino-pretty -g

要在我们的代码中生成结构化日志,我们创建一个新的 Pino 日志记录器并调用一个用于所需日志级别的日志函数--可以是tracedebuginfowarningerrorfatal中的任何一个。以下列表演示了如何使用 Pino 日志记录器生成结构化日志。

列表 6.2 带有上下文、结构化数据的 Pino 日志消息。

const pino = require('pino')
const log = pino({ name: 'pino-logging-example' })    ❶

log.info({ a: 1, b: 2 }, 'Hello world')               ❷
const err = new Error('Something failed')
log.error({ err })                                    ❸

❶ 创建一个具有特定名称的日志记录器,以标识日志的来源。

❷ 信息消息与一些数据一起记录。数据作为第一个参数以对象的形式传递。

❸ 使用属性 err 记录错误。这是一个特殊的属性,它会导致错误被序列化为对象。该对象包括错误类型和字符串形式的堆栈跟踪。

第一条日志记录的 JSON 日志看起来像这样:

{"level":30,"time":1575753091452,"pid":88157,"hostname":"eoinmac","name":"pino-logging-example","a":1,"b":2,"msg":"Hello world","v":1}

错误日志作为 JSON 难以阅读。如果我们将其输出通过pino-pretty管道,结果将更容易理解。这将在下一个列表中展示。

列表 6.3 使用pino-pretty使结构化 JSON 日志可读。

[1575753551571] INFO  (pino-logging-example/90677 on eoinmac): Hello world
    a: 1
    b: 2
[1575753551572] ERROR (pino-logging-example/90677 on eoinmac):
    err: {
      "type": "Error",
      "message": "Something failed",
      "stack":
          Error: Something failed
              at Object.<anonymous> (/Users/eoin/code/chapter5/           pino-logging-example/index.js:9:13)
              at Module._compile (internal/modules/cjs/loader.js:689:30)
              at Object.Module._extensions..js (internal/modules/cjs/           loader.js:700:10)
              at Module.load (internal/modules/cjs/loader.js:599:32)
              at tryModuleLoad (internal/modules/cjs/loader.js:538:12)
              at Function.Module._load (internal/modules/cjs/loader.js:530:3)
              at Function.Module.runMain (internal/modules/cjs/           loader.js:742:12)
              at startup (internal/bootstrap/node.js:283:19)
              at bootstrapNodeJSCore (internal/bootstrap/node.js:743:3)
    }

6.5.2 检查日志输出

我们可以通过使用 SLIC 启动器应用程序来触发一些日志输出。访问已部署的 SLIC 列表前端 URL。如果你遵循了 SLIC 启动器的快速入门指南,你应该已经有了这个。在这个例子中,我们将使用持续部署的开源存储库的测试环境,stg.sliclists.com

您需要注册并创建账户。从那里,您可以登录并创建一个清单。您首先会看到一个登录屏幕,如图 6.7 所示。遵循该屏幕上的链接注册并创建您的账户,然后再登录。

图片

图 6.7 当您首次启动 SLIC 列表时,您可以注册创建账户并登录。

一旦您登录,您就可以创建一个列表,如图 6.8 所示。

图片

图 6.8 SLIC 列表允许您创建和管理清单。在这里,我们通过输入标题和可选的描述来创建一个清单。在无服务器后端,这会在 DynamoDB 中创建一个项。它还会触发一个事件驱动的流程,结果是通过电子邮件发送欢迎信息给列表创建者。

最后,您可以为清单添加一些条目。这如图 6.9 所示。

图片

图 6.9 在这里,我们向清单中添加了一些项目。这一步将条目添加到我们刚刚创建的清单项中。如果您对如何使用 DynamoDB 数据建模实现这一点感兴趣,请查看 checklist-service 文件夹中的 services/checklists/entries/entries.js

一旦创建了清单记录,您就可以检查日志。请注意,SLIC Starter 生成的日志比您在类似系统中通常预期的要多。特别是,以 INFO 级别记录的信息,您合理地期望在 DEBUG 级别日志中看到。CloudWatch 日志的成本在这里是一个真正的考虑因素。在真实的生产系统中,您应该考虑减少日志输出,删除任何可识别的个人用户信息,并为调试日志实施采样 3。

我们检查 CloudWatch 日志的第一种方法是使用 Serverless Framework CLI。在这里,我们将使用 serverless logs 来查看 create 函数的最新日志。输出再次通过 pino-pretty 进行管道传输以提高可读性:

cd checklist-service
serverless logs -f create --stage <STAGE> | pino-pretty
  # STAGE is one of dev, stg or prod

下一个列表显示了显示 INFO 级别日志的日志输出。

列表 6.4 serverless logs 获取日志事件并将它们打印到控制台

[1576318523847] INFO  (checklist-service/7 on 169.254.50.213): Result received
    result: {
      "entId": "4dc54f8e-e28b-4de2-9456-f30caef781e4",
      "title": "Entry 2"
    }
END RequestId: fa02f8b1-2a42-46a8-83b4-a8834483fa0a
REPORT RequestId: fa02f8b1-2a42-46a8-83b4-a8834483fa0a  Duration: 74.44 ms
      Billed Duration: 100 ms Memory Size: 1024 MB
    Max Memory Used: 160 MB

START RequestId: 0e56603b-50f1-4581-b208-18139e85d597 Version: $LATEST
[1576318524826] INFO
  (checklist-service/7 on 169.254.50.213): Result received
    result: {
      "entId": "279f106f-469d-4e2d-9443-6896bc70a2d5",
      "title": "Entry 4"
    }
END RequestId: 0e56603b-50f1-4581-b208-18139e85d597
REPORT RequestId: 0e56603b-50f1-4581-b208-18139e85d597  Duration: 25.08 ms
      Billed Duration: 100 ms Memory Size: 1024 MB
    Max Memory Used: 160 MB

除了结构化的 JSON 日志,这些日志由 pino-pretty 格式化以供阅读外,我们还可以看到 Lambda 容器本身生成的日志条目。这些包括 STARTENDREPORT 记录。REPORT 记录打印有关使用的内存和函数持续时间的有用记录。这两者在优化内存配置以实现性能和成本方面都至关重要。

选择最佳的 Lambda 内存配置 Lambda 函数按请求和每 GB-秒计费。与许多服务一样,有一个免费层——在撰写本文时,每月有 100 万次请求和 40 万 GB-秒。这意味着在您开始收费之前,您可以进行相当多的计算。一旦在生产应用程序中用完了免费层,选择每个函数的正确大小在成本和性能方面都至关重要。

当你配置 Lambda 函数时,你可以选择为其分配多少内存。内存加倍将使每秒执行成本加倍。然而,分配更多内存也会线性增加函数的 vCPU 分配。

假设你有一个函数,在 960MB 内存的 Lambda 函数中执行需要 212ms,但在 1024MB 内存的函数中执行只需要 190ms。更高内存配置的 GB-秒定价将大约高 6%,但由于执行是按 100ms 单位计费,较低内存配置将使用 50%更多的单位(3 个而不是 2 个)。出人意料的是,更高内存配置将显著更便宜,并带来更好的性能。

类似地,如果你有一个通常在 10ms 内执行完成的函数,且延迟不是那么关键,你可能更倾向于使用较低的内存配置,减少 CPU 分配,并让它执行时间接近 100ms。

6.5.3 使用 CloudWatch 日志洞察搜索日志

我们已经看到了如何在命令行中检查单个函数的日志。同样,你还可以在 AWS 管理控制台中查看单个日志流。这在开发期间很有用,但在你部署了许多函数且在生产系统中频繁执行时,作用就较小了。为此,你需要能够搜索 TB 级日志数据的大规模、集中式日志记录。CloudWatch 日志洞察是一个方便的服务,且无需预先设置。它可以在 AWS 管理控制台的 CloudWatch 服务下的洞察部分找到。图 6.10 显示了有关标题中包含“启动”短语清单的日志查询。

图 6.11

图 6.10 CloudWatch 日志洞察允许你在多个日志组中运行复杂的查询。

这里显示的查询是一个简单的示例。查询语法支持许多函数和操作。你可以执行算术和统计运算,以及提取字段、排序和过滤。图 6.11 展示了我们如何通过从每个执行的REPORT日志中提取数据来使用统计函数分析 Lambda 的内存使用量和持续时间。

图 6.10

图 6.11 使用 Lambda REPORT日志进行统计和算术运算,可以分析函数是否配置了最优的内存量以实现成本和性能。在此,我们展示了内存使用情况,并将最大内存使用量与配置的内存容量进行比较。我们还展示了函数持续时间的 95%,98%和 99.9 百分位数,以获得性能的直观感受。

在示例中,我们分配了比所需更多的内存。这可能意味着可以将容器的内存大小减少到 256MB。由于正在分析的功能仅调用 DynamoDB 写入操作,它比 CPU 密集型更偏向 I/O 密集型。因此,减少其内存和 CPU 分配不太可能对执行持续时间产生重大影响。

您现在应该已经很好地理解了如何通过集中式、结构化的日志以及 CloudWatch 日志洞察来为您的应用程序添加可观察性。接下来,我们将探讨您可以观察和创建的指标,以进一步了解应用程序的行为。

6.6 监控服务和应用指标

作为实现可观察性目标的一部分,我们希望能够创建和查看指标。指标可以是特定于服务的,例如并发执行的 Lambda 函数的数量。它们也可以是特定于应用的,例如清单中的条目数量。AWS 提供了一个名为 CloudWatch 指标的指标存储库。此服务收集单个指标并允许您查看它们的聚合。请注意,一旦收集了单个指标数据点,就无法查看。相反,您可以请求给定时间段内的统计数据,例如每分钟计数指标的求和。

CloudWatch 指标的默认最小周期为 1 分钟。可以添加具有 1 秒分辨率的自定义高分辨率指标。保留 3 小时后,高分辨率指标将聚合到 1 分钟间隔。

6.6.1 服务指标

许多 AWS 服务默认为大多数服务发布指标。无论您是使用 CloudWatch 指标还是其他指标解决方案,了解发布的指标以及您应该监控哪些指标都至关重要。表 6.2 列出了 AWS 服务样本的一些指标。我们选择了与第 2-5 章中构建的 AI 应用特别相关的示例。

表 6.2 AWS 服务发布 CloudWatch 指标,可以监控以深入了解系统行为。理解和观察与您使用的服务相关的指标非常重要。

服务 示例指标
Lex4 未接收到的话语数量, Polly 运行时错误
Textract5 用户错误数量, 响应时间
Rekognition6 检测到的面孔数量, 检测到的标签数量
Polly7 请求字符数, 响应延迟
DynamoDB8 返回字节, 消耗的写入容量单元
Lambda9 调用次数, 错误, 迭代器年龄, 并发执行数

对我们所使用的所有服务的所有指标的彻底覆盖超出了本书的范围。我们建议您在阅读本书时,探索您迄今为止构建的应用程序的 AWS 管理控制台中的 CloudWatch Metrics 部分。有关服务和它们的指标的综合列表可以在 AWS 文档中找到。10

6.6.2 应用指标

除了 AWS 服务发布的内置指标外,CloudWatch Metrics 还可以用作自定义应用指标的存储库。在本节中,我们将探讨添加指标需要哪些步骤。让我们回顾一下 SLIC Starter 项目中的清单应用程序。我们可能想要收集一些特定于应用程序的指标,这些指标可以告诉我们产品是如何进一步开发的。假设我们正在考虑为应用程序开发一个 Alexa 技能。一个 Alexa 技能 是一个 AWS 中的无服务器应用程序,允许用户通过智能扬声器设备与一项服务进行交互。这将与第五章中 Lex 驱动的待办事项聊天机器人非常相似!为了设计这个技能,我们的用户体验部门想要收集有关用户当前如何使用 SLIC 列表 的统计数据。具体来说,我们想要了解以下内容:

  • 用户在清单中添加了多少条目?

  • 一个典型的清单条目中有多少个单词?

使用 CloudWatch Metrics,我们可以有两种方法来添加这些指标:

  • 使用 AWS SDK 并调用 putMetricData API11

  • 使用根据 嵌入式指标格式 特别格式化的日志

使用 putMetricData API 有一个缺点。进行此类 SDK 调用将导致底层 HTTP 请求。这会给我们的代码添加不必要的延迟。我们将改用嵌入式指标格式日志。这种方法要求我们创建一个特别格式化的日志消息,其中包含我们想要生成的指标的所有详细信息。由于我们使用 CloudWatch 日志,CloudWatch 将自动检测、解析并将此日志消息转换为 CloudWatch 指标。编写此日志消息的开销将对代码的性能产生微乎其微的影响。此外,原始指标将在我们保留日志的时间内可用。

让我们看看我们是如何生成这些指标日志以及结果看起来像什么。日志消息格式的概述如下所示。

列表 6.5 嵌入式指标格式日志的结构

{
  "_aws": {                            ❶
    "Timestamp": 1576354561802,
    "CloudWatchMetrics": [
      {
        "Namespace": "namespace"       ❷
        "Dimensions": [["stagej"]]     ❸
        "Metrics": [
          {
            "Name": "Duration",        ❹
            "Unit": "Milliseconds"12
          }
        ],
        ...
      }
    ]
  },
  "stage": "prod",                     ❺
  "Duration": 1                        ❻
}

_aws 属性定义了我们指标的元数据。

❷ 指标的命名空间是此指标所属的分组。

❸ 每个指标可以指定多达十个维度。维度是一个将指标分类的名称-值对。

❹ 在这里定义了一个单一指标,给它一个名称和单位。AWS 文档中定义了一个支持的指标单位列表。5

❺ 在元数据中命名的维度的值在这里给出。

❻ 在元数据中命名的指标的值在这里提供。

这些符合 JSON 结构的日志消息会自动被 CloudWatch 识别,并导致创建 CloudWatch 指标,同时最小化性能开销。我们可以在 Lambda 函数代码中使用console.log创建此 JSON 结构并将其记录到 CloudWatch 日志中。另一种方法是使用aws-embedded-metrics Node.js 模块。13 此模块为我们提供了一系列用于记录指标的函数。在这种情况下,我们将使用createMetricsLogger函数。我们将在checklist-service/services/checklists/entries/entries.js中添加指标记录代码。请参阅以下列表,以获取addEntry函数的相关摘录。

列表 6.6 符合嵌入式指标格式的结构化日志

const metrics = createMetricsLogger()                                      ❶
metrics.putMetric('NumEntries', Object.keys(entries).length, Unit.Count)   ❷
metrics.putMetric('EntryWords', title.trim().split(/s/).length, Unit.Count)❸
await metrics.flush()                                                      ❹

createMetricsLogger创建了一个我们可以显式调用的记录器。aws-embedded-metrics模块还提供了一个包装器或“装饰器”函数,以避免显式调用刷新。

❷ 将清单中的条目数量记录为计数指标。

❸ 记录清单条目中的单词数量。

❹ 我们刷新指标以确保它们被写入控制台输出。

要生成一些指标,我们需要用不同的输入调用此函数。SLIC Starter 端到端集成测试包括一个创建具有条目计数和单词计数的清单的测试,这些计数根据现实分布。我们可以多次运行此测试以在 CloudWatch 中获得一些合理的指标。

SLIC Starter 的集成测试中有一些设置步骤。查看integration-tests文件夹中的README.md文件。一旦您已准备好测试并验证可以运行一次,我们就可以运行一批集成测试来模拟一些负载:

cd integration-tests
./load.sh

load.sh脚本并行运行随机数量的集成测试执行,并重复此过程,直到完成 100 次。现在,我们可以进入 AWS 管理控制台的 CloudWatch 指标部分,以可视化清单条目的统计数据。

当您在控制台中选择 CloudWatch 指标时,视图应该类似于图 6.12。

图 6.12 在 AWS 管理控制台中浏览到 CloudWatch 指标视图,允许您从自定义命名空间和 AWS 服务的命名空间中选择。

从这里,选择aws-embedded-metrics命名空间。这会带您到一个表格,您可以在其中看到所选命名空间内的维度集。如图 6.13 所示。

图 6.13 一旦选择了命名空间,下一步就是选择维度集。

通过唯一选项进行点击以显示可查看的指标。从addEntry函数中选择两个指标,如图 6.14 所示。

图 6.14 CloudWatch 指标控制台呈现所选命名空间和维度内的所有指标。点击每个旁边的复选框将指标添加到显示的图表中。

我们现在想自定义这些指标的展示方式。首先,让我们添加到默认的平均统计信息。这可以通过切换到“图形指标”选项卡来完成。在每个指标旁边选择复制图标。对于 NumEntries 和 EntryWords 指标,都这样做两次。这将创建平均指标的副本。将每个副本中的一个更改为使用最大值和 p95 统计信息。最后,将图表类型从折线图更改为数字图。最终视图应该看起来像图 6.15。

图 6.15

图 6.15 切换到“图形指标”选项卡允许您自定义和复制指标。在这里,我们为相同的两个指标选择了新的统计信息。将图表从折线图切换到数字图也为我们提供了所需统计信息的简单直观视图。

在这种情况下,使用数字可视化而不是折线图更有用,因为随着时间的推移在折线图上查看的值的变化对这些指标来说并不有趣。我们最终得到了一些简单的数字,可以帮助我们的用户体验团队设计 Alexa 技能!我们知道大多数条目少于五个单词,平均为两个单词。平均列表大约有 8 个条目,95%的条目少于 16.6。

6.6.3 使用指标创建警报

在这个阶段,你已经看到了理解和监控 AWS 服务指标以及自定义应用指标的价值。当你遇到无法解释的系统行为时,这些知识应该能帮助你开始揭示一些未知因素。然而,等到出现问题才开始寻找答案并不是一个好主意。最好是思考正常系统行为是什么,并为系统行为偏离这一正常状态时创建警报。警报是在达到指定条件时对系统操作员的通知。通常,我们会为以下偏差设置警报:

  1. 一个计数 AWS 服务内错误数量的指标,当值大于给定数字时触发。例如,我们可能希望在五分钟内所有函数的 Lambda 调用次数超过 10 次时收到警报。

  2. 我们对最终用户的服务水平已经达到了不可接受的程度。一个例子是,对于关键 API 端点的 API 网关延迟指标的 99 百分位数超过 500ms。

  3. 商业指标对于创建警报来说非常有价值。从终端用户的角度来看,通常更容易创建与交互相关的阈值。例如,在我们的 SLIC Starter 应用程序中,我们可能知道每小时通常会有 50 到 60 个清单被创建。如果这个数字远远低于这个阈值,我们就可以收到警报并调查。这可能是活动中的偶然变化,也可能是我们可能没有检测到的潜在技术问题。

在 AWS 的背景下,像这样的警报可以通过 CloudWatch Alarms 实现。警报始终基于 CloudWatch 指标。可以定义使用的周期和统计量(例如,5 分钟内的平均延迟)。警报的阈值可以是基于数值,也可以是基于标准差带进行异常检测。CloudWatch Alarms 的警报机制是通过 SNS 主题实现的。SNS(或简单通知服务)是一种用于发送事件的发布/订阅服务。SNS 允许通过电子邮件、短信或 webhook 将警报发送到另一个服务,包括 SQS 和 Lambda。

创建警报的全面示例超出了本章的范围。使用 AWS 管理控制台进行实验和创建一些警报是值得的。一旦你熟悉了 CloudWatch Alarms 的配置选项,你就可以在serverless.yml文件中将它们作为资源创建。以下资源还可以让我们以更少的配置创建警报:

  • Serverless 应用程序仓库提供了可以包含在您自己的应用程序中的托管 CloudFormation 堆栈。其他组织已经发布了简化创建合理警报集的堆栈。一个例子是SAR-cloudwatch-alarms-macro应用程序。14 它为 AWS Lambda、API Gateway、AWS Step Functions 和 SQS 中的常见错误创建警报。

  • 适用于 Serverless Framework 的插件,如 AWS Alerts Plugin (mng.bz/jVre),使创建警报的过程更加简单。

6.7 使用跟踪来理解分布式应用程序

在本章的开头,我们提到,无服务器开发中的一个挑战是系统的分布式和碎片化特性。这一方面使得可视化或推理整个系统的行为变得更加困难。集中式日志、指标和警报的实践可以帮助解决这个问题。分布式追踪是另一个工具,它使得理解无服务器系统中数据流变得可能。在 AWS 生态系统中,分布式追踪由 X-Ray 和 CloudWatch ServiceLens 提供。X-Ray 是底层追踪服务,ServiceLens 是 CloudWatch 控制台中提供与日志和指标集成的追踪可视化的区域。还有商业替代方案,如 Datadog、Lumigo 和 Epsagon。尽管这些方案确实值得探索,但我们将使用托管 AWS 服务,因为它们足以演示和学习可观察性和追踪的概念。

6.7.1 启用 X-Ray 追踪

分布式追踪的目的是监控和评估请求在系统中通过多个服务时的性能。最好的说明方式是使用一个视觉示例。考虑在 SLIC Starter 应用程序中创建清单的场景。从用户在前端点击保存按钮的那一刻起,图 6.16 中所示的序列发生。

图片

图 6.16 服务器无服务器系统的一次典型请求会在多个服务之间产生多条消息。

  1. 请求通过 API Gateway 到达清单服务中的 Lambda。

  2. 这个 Lambda 调用 DynamoDB。

  3. Lambda 将“列表创建”事件发布到 Amazon EventBridge。

  4. 事件被欢迎服务捕获。

  5. 欢迎服务调用用户服务 API 来查找清单所有者的电子邮件地址。

  6. 欢迎服务将 SQS 消息放入电子邮件服务的队列中。

  7. 电子邮件服务接受传入的 SQS 消息,并使用简单电子邮件服务(SES)发送电子邮件。

这是一个相对简单的分布式工作流,但已经可以看出,这种事件链式反应对于开发者来说理解起来有多困难。想象一下在拥有数百或数千个服务的系统中的情况!通过捕获整个流程的轨迹,我们可以在 ServiceLens 中查看序列和时序。其中一部分序列在图 6.17 中显示。

图片

图 6.17 CloudWatch ServiceLens 显示了每个段的时序的单独轨迹。

图中的轨迹显示了分布式请求的段,包括它们的时序。请注意,这张图片与单个请求相关。使用 X-Ray,轨迹被采样。默认情况下,每秒采样一个请求,之后每 5% 的请求进行采样。这可以通过 X-Ray 控制台中的规则进行配置。

X-Ray 通过生成跟踪 ID 并将这些 ID 在请求得到满足时从一个服务传播到另一个服务来工作。为了启用此行为,开发者可以使用 AWS X-Ray SDK 来添加 AWS SDK 调用的自动跟踪仪器。这样做的影响是,包含跟踪和分段标识符的跟踪头被添加到请求中。请求数据,包括时间信息,也由 X-Ray SDK 发送到收集跟踪样本的守护进程。以下代码显示了我们在 Node.js Lambda 函数代码中初始化 X-Ray SDK 的方式:

const awsXray = require('aws-xray-sdk')
const AWS = awsXray.captureAWS(require('aws-sdk'))

此代码片段来自 SLIC Starter 中的slic-tools/aws.js,在加载标准 AWS SDK 之前加载 X-Ray SDK。X-Ray SDK 的captureAWS函数被调用以拦截所有 SDK 请求并在跟踪中创建新的分段。15 要启用 X-Ray 跟踪,还需要在 API Gateway 和 Lambda 配置中将其打开。当使用 Serverless Framework 时,这涉及到对serverless.ymlprovider部分的添加,如下面的代码所示:

tracing:
    apiGateway: true
    lambda: true

这是在 SLIC Starter 中的所有服务中完成的,所以您已经拥有了查看分布式跟踪结果所需的一切。

6.7.2 探索跟踪和映射

除了我们之前看到的单个跟踪时间线之外,X-Ray 控制台和较新的 CloudWatch ServiceLens 控制台都有显示您服务完整映射的能力。这是一个极其强大的可视化工具。图 6.18 展示了 SLIC Starter 服务映射的一个示例。

图片

图 6.18 在 CloudWatch ServiceLens 中可以显示服务之间请求传播的映射。尽管这个图表显示了如此多的服务以至于难以阅读,但您在使用 AWS 控制台时可以选择放大和过滤。

所有可视化,包括映射和跟踪,都会显示捕获到的任何错误。映射视图显示了每个节点的错误百分比。选择映射中的任何节点将显示请求速率、延迟和错误数量。图 6.19 显示了清单服务中deleteEntry函数的服务映射选择,错误率为 50%。

图片

在服务映射中选择任何节点以选择“查看连接”将过滤视图以仅显示连接的服务。在这里,我们可以看到可以通过在 CloudWatch 日志中使用相关请求 ID 进一步调查的错误事件。

我们可以选择选择“查看跟踪”或“查看日志”来进一步诊断。选择“查看日志”将带我们进入 CloudWatch 日志洞察来查看此请求和时间的日志。

6.7.3 使用注释和自定义指标的高级跟踪

我们无法涵盖 X-Ray 和 ServiceLens 的所有用例。然而,有一些特性值得提及,因为它们在尝试寻找大规模真实生产场景的答案时特别有用:

  • 注释 是您可以使用 X-Ray SDK 分配到跟踪段的索引键值对。这些由 X-Ray 索引,因此您可以在 X-Ray 控制台中根据它们进行筛选。16 还可以向跟踪段添加自定义元数据。这些不是索引的,但可以在控制台中查看。

  • X-Ray 分析控制台和 AWS SDK 支持创建由过滤器表达式定义的 。过滤器表达式可以包括使用 X-Ray SDK 在您的代码中创建的自定义注释。

  • 当定义组时,X-Ray 将创建自定义指标并将它们发布到 CloudWatch 指标。这些包括延迟、错误和节流速率。

我们建议您花些时间通过 AWS 管理控制台实验 X-Ray 的功能。这将帮助您为您的无服务器应用程序创建正确的注释、元数据和组。

摘要

  • 可以使用 CodePipeline 和 CodeBuild 创建无服务器持续部署管道。

  • 单一仓库方法是一种有效的策略,用于构建可扩展的无服务器应用程序。

  • 使用可观测性最佳实践可以解决分布式无服务器应用程序架构中的挑战。

  • 可以使用结构化 JSON 日志和 AWS CloudWatch 日志实现集中式日志记录。

  • 使用 CloudWatch 日志洞察查看并深入日志。

  • 可以使用 CloudWatch 查看服务指标。

  • 可以创建特定于应用程序的自定义指标。

  • 使用 X-Ray 和 ServiceLens 进行分布式跟踪使我们能够理解高度分布式的无服务器系统。

在下一章中,我们将继续探讨现实世界的 AI 即服务,重点关注将其集成到基于截然不同技术的现有系统中。

警告 请确保您完全删除本章中部署的所有云资源,以避免额外收费!


1.有关这些和其他部署策略的更多信息,请参阅 Etienne Tremel 的文章“应用程序部署的六种策略”,2017 年 11 月 21 日,thenewstack.io,thenewstack.io/deployment-strategies/

2.可观测性简介,honeycomb.io,http://mng.bz/aw4X.

3.“您需要在生产中采样调试日志,” 焉桂,2018 年 4 月 28 日,hackernoon.com/you-need-to-sample-debug-logs-in-production-171d44087749

4.参见 使用 Amazon CloudWatch 监控 Amazon Lex, http://mng.bz/emRq.

5.参见 Amazon Textract 的 CloudWatch 指标, http://mng.bz/pzEw.

6.参见 Amazon Rekognition 的 CloudWatch 指标, http://mng.bz/OvAa.

7.参见 将 CloudWatch 与 Amazon Polly 集成, http://mng.bz/YxOa.

8.参见 DynamoDB 指标和维度, http://mng.bz/Gd2J.

9.查看 AWS Lambda 指标mng.bz/zrgA.

10.发布云监控指标的 AWS 服务,mng.bz/0Z5v.

11.AWS JavaScript SDK 的 putMetricData 方法,docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CloudWatch.html#putMetricData-property.

12.云监控指标支持的单位在 MetricDatum 中介绍,mng.bz/9Azr.

13.aws-embedded-metrics 在 GitHub 上,github.com/awslabs/aws-embedded-metrics-node.

14.Lumigo 的 SAR-cloudwatch-alarms-macro,mng.bz/WqeW.

15.使用 Node.js 的 X-Ray SDK 查看 AWS SDK 调用的跟踪,mng.bz/8GyD.

16.使用 Node.js 的 X-Ray SDK 为段添加注释和元数据,mng.bz/EEeR.

7 将 AI 应用于现有平台

本章涵盖

  • 无服务器 AI 的集成模式

  • 使用 Textract 改进身份验证

  • 基于 Kinesis 的 AI 赋能数据处理管道

  • 即时翻译使用 Translate

  • 使用 Comprehend 进行情感分析

  • 使用 Comprehend 训练自定义文档分类器

在第 2-5 章中,我们从零开始创建系统,并从一开始就应用 AI 服务。当然,现实世界并不总是这么干净和简单。我们几乎所有人都要处理遗留系统和技术债务。在本章中,我们将探讨将 AI 服务应用于现有系统的一些策略。我们将首先查看一些适用于此的架构模式,然后从那里开发一些从实际经验中汲取的具体示例。

7.1 无服务器 AI 的集成模式

不可避免的事实是,现实世界的企业计算是“混乱”的。对于中型到大型企业,技术资产通常是庞大的、分散的,并且随着时间的推移通常是有机增长的。

一个组织的计算基础设施可以根据财务、人力资源、市场营销、业务线系统等域线进行分解。这些域中的每一个都可能由来自不同供应商的许多系统组成,包括自建软件,并且通常会将遗留系统与现代软件即服务(SaaS)交付的应用程序混合在一起。

与此同时,各种系统通常以混合模式运行,混合本地、共址和基于云的部署。此外,这些操作元素通常必须与域内和域外的其他系统进行集成。这些集成可以通过批量 ETL 作业、点对点连接或通过某种形式的企业服务总线(ESB)来实现

ETL、点对点和 ESB

企业系统集成是一个大主题,我们在这里不会涉及,只是指出,有几种方式可以将系统连接在一起。例如,一家公司可能需要从其人力资源数据库中导出记录以与费用跟踪系统匹配。提取、转换和加载(ETL)指的是从数据库(通常为 CSV 格式)导出记录、转换,然后加载到另一个数据库的过程。

连接系统的另一种方法是使用点对点集成。例如,可以创建一些代码来调用一个系统的 API 并将数据推送到另一个系统的 API。当然,这取决于提供合适的 API。随着时间的推移,ETL 和点对点集成的使用可能会积累成一个非常复杂且难以管理的系统。

企业服务总线(ESB)试图通过提供一个中心系统来管理这种复杂性,这些连接可以在该系统中进行。ESB 方法有其自身的特定病理,并且常常造成的问题与解决的问题一样多。

图 7.1 说明了典型中型组织的技术地产。在这个例子中,通过一个中央总线将不同的领域连接在一起。在每一个领域内,都有独立的 ETL 和批量处理过程将系统连接起来。

不言而喻,对所有这种复杂性的描述超出了本书的范围。我们将关注的问题是,我们如何在这个环境中采用和利用无服务器 AI。幸运的是,有一些简单的模式我们可以遵循来实现我们的目标,但首先让我们简化这个问题。

图片

图 7.1 典型企业技术地产,按逻辑领域分解。此图像旨在说明典型技术地产的复杂性质。架构的细节并不重要。

在接下来的讨论中,我们将使用图 7.2 来表示我们的“企业地产”,以便我们将其余的基础设施视为一个黑盒。在下一节中,我们将检查四种常见的连接 AI 服务模式。然后,我们将构建一些具体的例子来展示如何使用 AI 服务在企业内部增强或替换现有的业务流程。

例如,如果一家公司的业务流程需要通过水电费账单或护照证明身份,这可以作为一个 AI 赋能的服务提供,从而减少人工工作量。

另一个例子是预测。许多组织需要提前规划,以预测在特定时间段内所需的库存或员工水平。AI 服务可以集成到这一过程中,以构建更复杂和准确的模型,为公司节省金钱或机会成本。

图片

图 7.2 简化的企业表示

我们将检查四种方法:

  • 同步 API

  • 异步 API

  • VPN 流入

  • VPN 全连接流

请记住,这些方法仅仅代表将适当的数据放入所需位置以使我们能够执行 AI 服务以实现业务目标的方式。

7.1.1 模式 1:同步 API

第一种也是最简单的方法是创建一个小的系统,就像我们在前几章中所做的那样,与企业其他部分隔离。功能通过安全的 API 暴露,并通过公共互联网访问。如果需要更高层次的安全性,可以建立 VPN 连接,通过该连接调用 API。这种简单的模式在图 7.3 中得到了说明。

图片

图 7.3 集成模式 1:同步 API

为了使用该服务,必须创建一小段桥接代码来调用 API 并消费服务的输出。当结果可以快速获得,并且 API 以请求/响应方式调用时,此模式是合适的。

7.1.2 模式 2:异步 API

我们的第二个模式非常相似,即我们通过 API 公开功能;然而,在这种情况下,API 是异步的。这种模式适用于运行时间较长的 AI 服务,如图 7.4 所示。

图片

图 7.4 集成模式 2:异步 API

在这个“点火后即忘”模型下,桥梁代码调用 API 但不立即接收结果,除了状态信息。一个例子可能是一个处理大量文本的文档分类系统。该系统的输出可以通过多种方式被更广泛的企业所消费:

  • 通过构建一个用户可以与之交互以查看结果的 Web 应用程序

  • 通过系统通过电子邮件或其他渠道发送结果消息

  • 通过系统调用外部 API 以转发任何分析详情

  • 通过桥梁代码轮询 API 以获取结果

7.1.3 模式 3:VPN 流入

第三种方法是通过 VPN 将企业连接到云服务。一旦建立了安全连接,桥梁代码可以更直接地与云服务交互。例如,而不是使用 API 网关来访问系统,桥梁代码可以直接将数据流入 Kinesis 管道。

结果可以通过多种方式访问:通过 API、通过出站消息、通过 Web GUI 或通过输出流。如图 7.5 所示。

图片

图 7.5 集成模式 3:流入

VPN

虚拟专用网络(VPN)可以用来在设备或网络之间提供安全的网络连接。VPN 通常使用 IPSec 协议套件来提供身份验证、授权和安全的加密通信。使用 IPSec 可以让您使用不安全的协议,例如用于远程节点之间文件共享的协议。

VPN 可以用来为远程工作者提供对企业的安全访问,或者将企业网络安全地连接到云中。尽管有几种设置和配置 VPN 的方法,但我们建议使用 AWS VPN 服务来实现无服务器方法。

7.1.4 模式 4 VPN:完全连接的流式传输

我们的最后一个模式涉及企业与云 AI 服务之间更深层次的连接。在这个模型下,我们像以前一样建立 VPN 连接,并使用它来双向流数据。尽管有几种流式传输技术可用,但我们使用 Apache Kafka 取得了良好的效果。如图 7.6 所示

图片

图 7.6 集成模式 4:全流式传输

这种方法涉及在 VPN 两端运行 Kafka 集群,并在集群之间复制数据。在云环境中,服务通过从适当的 Kafka 主题中拉取数据来消费数据,并将结果放回不同的主题,以便更广泛的企业消费。

Kafka

Apache Kafka 是一个开源的分布式流式平台。Kafka 最初是在 LinkedIn 开发的,后来捐赠给了 Apache 基金会。尽管有其他流式技术可用,但 Kafka 的设计独特之处在于它被实现为一个分布式提交日志。

Kafka 越来越多地被 Netflix 和 Uber 等公司用于高吞吐量数据流场景。当然,您可以安装、运行和管理自己的 Kafka 集群;然而,我们建议您采用无服务器方法,并采用 AWS Managed Streaming for Kafka (MSK)等系统。

对这种方法以及 Kafka 的一般优点的全面讨论超出了本书的范围。如果您不熟悉 Kafka,我们建议您阅读 Dylan Scott 所著的 Manning 出版社的《Kafka in Action》一书,以了解相关知识。

7.1.5 哪种模式?

与所有架构决策一样,采取哪种方法实际上取决于用例。我们的指导原则是尽可能保持简单。如果简单的 API 集成可以实现您的目标,那么就选择它。如果随着时间的推移,外部 API 集开始增长,那么考虑将集成模型改为流式解决方案,以避免 API 的过度扩散。关键点是持续审查 AI 服务的集成,并准备好根据需要重构。

表 7.1 总结了上下文以及何时应用每种模式。

表 7.1 AI as Service 传统集成模式适用性

模式 上下文 示例
1: 同步 API 单个服务,快速响应 从文档中提取文本
2: 异步 API 单个服务,运行时间更长 文档转录
3: VPN 流入 多个服务,人类消费的结果 情感分析管道
4: VPN 完全连接 多个服务,机器消费的结果 文档批处理翻译

在本章中,我们将构建两个示例系统:

  • 模式 1:同步 API 方法

  • 模式 2:异步 API

虽然我们不会详细探讨流式处理方法,但请记住,我们的两个示例系统也可以通过这种方法与企业连接,只需用适当的技术(如 Apache Kafka)替换 API 层即可。

7.2 使用 Textract 改进身份验证

对于我们的第一个示例,我们将通过创建一个小的、自包含的 API 来扩展现有平台,该 API 可以直接调用。让我们想象一个组织需要验证身份。我们中的大多数人都在某个时候经历过这个过程;例如,在申请抵押贷款或汽车贷款时。

这通常需要扫描几份文件以证明您的身份和地址给贷款人。尽管需要有人查看这些扫描件,但从这些扫描件中提取信息并将信息手动输入到贷款人的系统中既耗时又容易出错。这是我们现在可以通过应用 AI 来实现的事情。

我们的小型、自包含的服务如图 7.7 所示。它使用 AWS Textract 从扫描的文档中提取详细信息。在这个例子中,我们将使用护照,但其他身份证明文件也可以同样工作,例如水电费账单或银行对账单。

图 7.7 文档识别 API

我们的 API 有两部分。首先,我们需要将扫描的图像上传到 S3。最简单的方法是使用预签名的 S3 URL,我们的 API 提供了一个生成此类 URL 并将其返回给客户端的功能。一旦我们的图像在 S3 中,我们将使用 Lambda 函数调用 Textract,它将分析扫描的图像,并以文本格式返回数据。我们的 API 将将这些数据返回给客户端以进行进一步处理。

个人可识别信息

任何个人可识别信息都必须非常小心地处理。每当系统必须处理用户提供的个人信息时,尤其是身份证明文件时,它必须遵守信息收集地的所有法定法律要求。

在欧盟,这意味着系统必须遵守通用数据保护条例(GDPR)。作为开发人员和系统架构师,我们需要了解这些法规并确保合规。

7.2.1 获取代码

API 的代码位于 chapter7/text-analysis 目录中。这包含两个目录:text-analysis-api,其中包含我们的 API 服务的代码,以及 client,其中包含一些用于测试 API 的代码。在部署和测试之前,我们将通过一些示例数据来了解整个系统。

7.2.2 文本分析 API

我们的 API 代码库包括一个 serverless.yml 配置文件,package.json 用于我们的节点模块依赖项,以及包含 API 逻辑的 handler.jsserverless.yml 是相当标准的,定义了两个 Lambda 函数:uploadanalyze,它们可以通过 API Gateway 访问。它还定义了一个用于 API 的 S3 桶,并设置了分析服务的 IAM 权限,如下所示。

列表 7.1 Textract 权限

iamRoleStatements:
    - Effect: Allow                     ❶
      Action:
        - s3:GetObject
        - s3:PutObject
        - s3:ListBucket
      Resource: "arn:aws:s3:::${self:custom.imagebucket}/*"
    - Effect: Allow
      Action:
        - textract:AnalyzeDocument      ❷
      Resource: "*"

❶ 启用桶对 Lambda 的访问

❷ 启用 Textract 权限

除了为 Textract 允许访问上传的文档外,还需要桶权限以使我们的 Lambda 函数生成有效的预签名 URL。

下一个列表显示了调用 S3 API 生成预签名 URL 的调用。URL 以及桶密钥被返回给客户端,客户端执行 PUT 请求上传相关的文档。

列表 7.2 获取预签名 URL

const params = {
    Bucket: process.env.CHAPTER7_IMAGE_BUCKET,
    Key: key,
    Expires: 300                                 ❶
  }
  s3.getSignedUrl('putObject', params, function (err, url) {
    respond(err, {key: key, url: url}, cb)
  })

❶ 设置五分钟的过期时间

预签名的 URL 仅限于为该特定键和文件指定的特定操作——在本例中为 PUT 请求。注意,我们还在 URL 上设置了 300 秒的过期时间。这意味着如果在五分钟内未启动文件传输,预签名 URL 将失效,并且没有生成新的 URL 的情况下无法进行传输。

一旦文档上传到存储桶,我们就可以调用 Textract 来执行分析。下一列表显示了这是如何在 handler.js 中完成的。

列表 7.3 调用 Textract

const params = {
    Document: {
      S3Object: {                                   ❶
        Bucket: process.env.CHAPTER7_IMAGE_BUCKET,
        Name: data.imageKey
      }
    },
    FeatureTypes: ['TABLES', 'FORMS']               ❷
  }

  txt.analyzeDocument(params, (err, data) => {      ❸
    respond(err, data, cb)
  })

❶ 指向上传的文档

❷ 设置特征类型

❸ 调用 Textract

Textract 可以执行两种类型的分析,TABLESFORMSTABLES 分析类型指示 Textract 在其分析中保留表格信息,而 FORMS 类型则请求 Textract 在可能的情况下将信息提取为键值对。如果需要,这两种分析类型可以在同一调用中执行。

分析完成后,Textract 返回一个包含结果的 JSON 块。结果结构如图 7.8 所示。

图 7.8 Textract 输出 JSON

结构应该是相当直观的,因为它由一个根 PAGE 元素组成,该元素链接到子 LINE 元素,每个子 LINE 元素又链接到多个子 WORD 元素。每个 WORDLINE 元素都有一个相关的置信区间:一个介于 0 和 100 之间的数字,表示 Textract 认为每个元素的分析有多准确。每个 LINEWORD 元素还有一个 Geometry 部分;这包含围绕元素边界框的坐标信息。这可以用于需要一些额外人工验证的应用程序。例如,一个 UI 可以显示带有叠加边界框的扫描文档,以确认提取的文本与预期的文档区域相匹配。

7.2.3 客户端代码

测试 API 的代码位于 client 目录中。主要的 API 调用代码在 client.js 中。有三个函数:getSignedUrluploadImageanalyze。这些函数与 API 的一对一映射,如前所述。

以下列表显示了 analyze 函数。

列表 7.4 调用 API

function analyze (key, cb) {
    req({                                     ❶
      method: 'POST',
      url: env.CHAPTER7_ANALYZE_URL,
      body: JSON.stringify({imageKey: key})
    }, (err, res, body) => {
      if (err || res.statusCode !== 200) {
        return cb({statusCode: res.statusCode,
          err: err,
          body: body.toString()})
      }
      cb(null, JSON.parse(body))             ❷
    })
  }

❶ 向 API 发送 POST 请求

❷ 返回结果

代码使用 request 模块向 analyze API 发送 POST 请求,该请求将 Textract 的结果块返回给客户端。

7.2.4 部署 API

在我们部署 API 之前,我们需要配置一些环境变量。API 和客户端都从 chapter7/text-analysis 目录中的 .env 文件中读取其配置。打开您最喜欢的编辑器并创建此文件,内容如下一列表所示。

列表 7.5 Textract 示例的 .env 文件

TARGET_REGION=eu-west-1
CHAPTER7_IMAGE_BUCKET=<your bucket name>

<your bucket name> 替换为您选择的全球唯一存储桶名称。

为了部署 API,我们需要像之前一样使用 Serverless Framework。打开命令行,cdchapter7/text-analysis/text-analysis-api目录,并运行

$ npm install
$ serverless deploy

这将创建文档图像存储桶,设置 API 网关,并部署我们的两个 Lambda 函数。一旦部署,Serverless 将输出两个函数的网关 URL,这些 URL 将类似于下一条列表中所示。

列表 7.6 端点 URL

endpoints:
GET - https://63tat1jze6.execute-api.eu-west-1.amazonaws.com/dev/upload    ❶
POST - https://63tat1jze6.execute-api.eu-west-1.amazonaws.com/dev/analyze  ❷
functions:
upload: c7textanalysis-dev-upload
analyze: c7textanalysis-dev-analyze

❶ 上传 URL

❷ 分析 URL

我们将使用这些 URL 来调用我们的文本分析 API。

7.2.5 测试 API

现在我们已经部署了我们的 API,是时候用一些真实数据来测试它了。我们刚刚部署的服务能够读取和识别文档中的文本字段,例如账单或护照。我们在data子目录中提供了一些示例护照图像,其中之一如图 7.9 所示。当然,这些是由模拟数据组成的。

图片

图 7.9 示例护照

为了测试 API,我们首先需要更新我们的.env文件。在文本编辑器中打开文件,并添加两个 URL 和存储桶名称,如下所示,使用您特定的名称。

列表 7.7 环境文件

TARGET_REGION=eu-west-1
CHAPTER7_IMAGE_BUCKET=<your bucket name>
CHAPTER7_ANALYZE_URL=<your analyze url>       ❶
CHAPTER7_GETUPLOAD_URL=<your upload url>      ❷

❶ 替换为分析 URL

❷ 替换为上传 URL

接下来,cdchapter7/text-analysis/client目录。在data子目录中有一些示例图像。在index.js中有练习客户端的代码。要运行代码,打开命令行并执行

$ npm install
$ node index.js

客户端代码将使用 API 将示例文档上传到图像存储桶,然后调用我们的analyze API。analyze API 将调用 Textract 来分析图像并将结果返回给我们的客户端。最后,客户端代码将解析输出 JSON 结构并挑选出几个关键字段,将它们显示在控制台上。你应该看到类似以下列表的输出。

列表 7.8 客户端输出

{
  "passportNumber": "340020013 (confidence: 99.8329086303711)",
  "surname": "TRAVELER (confidence: 75.3625717163086)",
  "givenNames": "HAPPY (confidence: 96.09229278564453)",
  "nationality": "UNITED STATES OF AMERICA (confidence: 82.67759704589844)",
  "dob": "01 Jan 1980 (confidence: 88.6818618774414)",
  "placeOfBirth": "WASHINGTON D.C. U.S.A. (confidence: 84.47944641113281)",
  "dateOfIssue": "06 May 2099 (confidence: 88.30438995361328)",
  "dateOfExpiration": "05 May 2019 (confidence: 88.60911560058594)"
}

重要的是要注意,Textract 正在应用多种技术来为我们提取这些信息。首先,它执行一个光学字符识别(OCR)分析来识别图像中的文本。作为分析的一部分,它保留了识别字符的坐标信息,将它们分组到块和行中。然后,它使用坐标信息将表单字段关联为名称-值对。

为了准确起见,我们需要向 Textract 提供高质量的图像:图像质量越好,分析结果越好。你可以通过创建或下载自己的低质量图像并将这些图像传递给 API 来测试这一点。你应该会发现 Textract 在低质量图像中难以识别相同的字段。

列表 7.8 显示了 Textract 识别的字段,以及一个置信度级别。大多数人工智能服务都会返回一些相关的置信度级别,而作为服务的消费者,我们则需要弄清楚我们应该如何处理这个数字。例如,如果我们的用例对错误非常敏感,那么可能只接受 99% 或更好的置信度级别是正确的。置信度较低的成果应发送给人类进行验证或修正。然而,许多商业用例可以容忍较低的准确性。这种判断非常特定于领域,应涉及业务和技术利益相关者。

考虑你自己的组织中的业务流程:是否有可以通过此类分析自动化的领域?你是否需要从客户提供的文档中收集和输入信息?也许你可以通过调整此示例以满足自己的需求来改进该流程。

7.2.6 删除 API

在进入下一节之前,我们需要删除 API 以避免产生额外费用。为此,请使用 cd 命令进入 chapter7/text-analysis/text-analysis -api 目录,并运行

$ source ../.env && aws s3 rm s3://${CHAPTER7_IMAGE_BUCKET} --recursive
$ serverless remove

这将删除桶中所有上传的图像并拆除堆栈。

7.3 带有 Kinesis 的 AI 驱动数据处理管道

对于我们的第二个示例,我们将构建一个数据处理管道。这个管道将通过异步 API 公开,并作为我们的模式 2 示例。在构建这个示例时,我们将详细探讨包括 Kinesis、Translate 和 Comprehend 在内的一系列新服务和技术的使用:

  • Kinesis 是亚马逊的实时流服务,用于创建数据和处理视频的管道。

  • Translate 是亚马逊的机器驱动语言翻译服务。

  • Comprehend 是亚马逊的自然语言处理(NLP)服务,可用于执行情感分析或关键词检测等任务。

考虑零售和电子商务领域。大型零售店可能有多个产品部门,如“户外”、“汽车”、“宠物”等。客户服务是零售贸易的重要组成部分。特别是,快速有效地回应客户投诉非常重要,因为如果处理得当,它可以把一个不满的客户转变为品牌倡导者。问题是客户有多个渠道可以投诉,包括网站产品评论、电子邮件、Twitter、Facebook、Instagram、博客文章等等。

不仅有许多渠道可以放置产品反馈,全球零售商还必须处理多种语言的反馈。尽管需要人类来处理客户,但检测所有这些渠道和地理区域的负面反馈是适合人工智能驱动解决方案的。

我们的示例系统将是一个具有 AI 功能的管道,可以用于过滤来自多个渠道和多种语言的反馈。我们管道的目标是在检测到关于他们产品之一的负面反馈时,向适当的部门发出警报。

这个具有 AI 功能的管道增强了零售商的数字能力,同时不会直接干扰业务线系统。

我们在图 7.10 中展示了我们的管道。管道的起始处,原始数据被发送到一个收集 API;这些数据可以来自多个来源,例如 Twitter 流、Facebook 评论、入站电子邮件以及其他社交媒体渠道。API 将原始文本输入到 Kinesis 流中。

图 7.10 处理管道

AWS 提供了两种关键的流技术:管理的 Kafka(MSK)和 Kinesis。在这些技术中,Kinesis 是最容易使用的,因此我们将专注于它来构建这个系统。流中的数据会触发下游的 Lambda 函数,该函数使用 Comprehend 来确定入站文本的语言。如果语言不是英语,Lambda 函数将使用 AWS Translate 进行即时翻译,然后再将其发送到管道中。下一个下游的 Lambda 函数将对翻译后的文本进行情感分析,使用 Comprehend。如果检测到积极情感,则不会对消息进行进一步处理。然而,如果情感非常负面,文本将被发送到使用 AWS Comprehend 构建的客户分类器。该分类器分析文本并尝试确定与消息相关的产品部门。一旦确定了部门,消息就可以被发送到适当的团队,以便他们处理负面评论。在这种情况下,我们将输出结果到 S3 存储桶。

通过这种方式结合使用 AI 服务,这样的管道可以为企业节省巨大的成本,因为反馈的过滤和分类是自动完成的,无需团队人员。

Kinesis 与 Kafka 的比较

直到最近,选择 Kinesis 而不是 Kafka 的一个原因是 Kafka 需要在 EC2 实例上安装、设置和管理。随着 AWS 管理的 Kafka(MSK)的发布,这种情况已经改变。尽管关于 Kafka 优点的全面讨论超出了本书的范围,但我们想指出,这项技术具有高度可扩展性和多功能性。我们建议,如果您正在构建一个需要大量流处理的系统,您应该更深入地研究 Kafka。

即使考虑到 MSK,Kinesis 仍然更完全地集成到 AWS 堆栈中,并且更容易快速启动,因此我们将使用它作为示例系统。Kinesis 可以用几种方式使用:

  • Kinesis Video Streams--用于视频和音频内容

  • Kinesis Data Streams--用于通用数据流

  • Kinesis Data Firehose--支持将 Kinesis 数据流式传输到 S3、Redshift 或 Elasticsearch 等目标

  • Kinesis Analytics--支持使用 SQL 进行实时流处理

在本章中,我们使用 Kinesis Data Streams 构建我们的管道。

7.3.1 获取代码

我们管道的代码位于书籍仓库中chapter7/pipeline目录下。该目录包含以下子目录,它们对应于处理过程中的每个阶段:

  • pipeline-api--包含系统的 API Gateway 设置

  • translate--包含语言检测和翻译服务

  • sentiment--包含情感分析代码

  • training--包含帮助训练自定义分类器的实用脚本

  • classify--包含触发我们的自定义分类器的代码

  • driver--包含用于测试管道的代码

与前面的示例一样,在部署之前,我们将简要描述每个服务的代码。一旦我们所有的单元都已部署,我们将对管道进行端到端测试。让我们从简单的第一步开始,部署 API。

7.3.2 部署 API

API 的代码位于chapter7/pipeline/pipeline-api目录中,包括一个serverless.yml文件和一个简单的 API。Serverless 配置定义了一个单一的ingest方法,该方法将 API 发布的数据推送到 Kinesis。Kinesis 流也在 Serverless 配置中定义,如下所示。

列表 7.9 serverless.yml Kinesis 定义

resources:
  Resources:
    KinesisStream:                    ❶
      Type: AWS::Kinesis::Stream
      Properties:
        Name: ${env:CHAPTER7_PIPELINE_TRANSLATE_STREAM}
        ShardCount: ${env:CHAPTER7_PIPELINE_SHARD_COUNT}

❶ 定义 Kinesis 流

API 的代码非常简单,它只是将传入的数据转发到 Kinesis 流。API 接受传入的 JSON POST请求,并期望格式如下一列表所示。

列表 7.10 管道 API 的 JSON 数据格式

{
  originalText: ...                      ❶
  source: 'twitter' | 'facebook'...      ❷
  originator: '@pelger'                  ❸
}

❶ 原始文本

❷ 反馈来源

❸ 反馈发起者的 ID

在部署 API 之前,我们需要设置我们的环境。我们在chapter7/pipeline目录中提供了一个名为default-environment.env的模板.env文件。在chapter7/pipeline目录中创建此文件的副本,文件名为.env。该文件应包含下一列表中概述的内容。

列表 7.11 管道的环境文件

TARGET_REGION=eu-west-1
CHAPTER7_PIPELINE_SHARD_COUNT=1                          ❶
CHAPTER7_PIPELINE_TRANSLATE_STREAM=c7ptransstream        ❷
CHAPTER7_PIPELINE_SENTIMENT_STREAM=c7psentstream         ❸
CHAPTER7_PIPELINE_CLASSIFY_STREAM=c7pclassifystream      ❹
CHAPTER7_PIPELINE_TRANSLATE_STREAM_ARN=...
CHAPTER7_PIPELINE_SENTIMENT_STREAM_ARN=...
CHAPTER7_PIPELINE_CLASSIFY_STREAM_ARN=...
CHAPTER7_CLASSIFIER_NAME=chap7classifier
CHAPTER7_CLASSIFIER_ARN=...
...

❶ Kinesis 分片数量

❷ Kinesis 翻译流名称

❸ Kinesis 情感流名称

❹ Kinesis 分类流名称

接下来,我们可以在chapter7/pipeline/pipeline-api目录中打开命令行,并执行以下操作来部署 API。

$ npm install
$ serverless deploy

这将创建我们的第一个 Kinesis 流,以及我们的摄取 API。图 7.11 说明了 API 部署后的管道状态。高亮部分表示到目前为止已部署的内容。

图 7.11

图 7.11 API 部署后的管道

在部署后,框架将输出我们 API 的 URL。在进入下一阶段之前,请将其添加到.env文件中,如下所示,并用您的具体值替换。

列表 7.12 API 部署后的.env文件中的附加条目

CHAPTER7_PIPELINE_API=<your API url>

7.4 使用 Translate 进行即时翻译

在数据摄取后的我们管道的第一个阶段是检测语言,如果需要则翻译成英语。这些任务由我们的翻译服务处理,其代码位于chapter8/pipeline/translate目录中。Serverless 配置相当标准,除了主要处理函数是由我们在 API 部署中定义的 Kinesis 流触发的。这将在下面的列表中展示。

列表 7.13 由 Kinesis 触发的处理程序

functions:
  translate:
    handler: handler.translate
    events:
      - stream:                 ❶
          type: kinesis
          arn: ${env:CHAPTER7_PIPELINE_TRANSLATE_STREAM_ARN}
          batchSize: 100
          startingPosition: LATEST
          enabled: true
          async: true

❶ 连接到流。

该配置定义了一个第二个 Kinesis 流,我们的情感服务将连接到它,并设置了适当的权限来发布到流和调用所需的翻译服务。这将在下面的列表中展示。

列表 7.14 处理程序 IAM 权限

- Effect: Allow
      Action:
        - comprehend:DetectDominantLanguage      ❶
         - translate:TranslateText               ❷
         - kinesis:PutRecord                     ❸
         - kinesis:PutRecords
      Resource: "*"

❶ Comprehend 权限

❷ 翻译权限

❸ Kinesis 权限

我们翻译服务的代码在handler.js中,由我们的 API 定义的 Kinesis 流中的数据触发。这作为事件参数中 Base64 编码记录的一个块。下一个列表展示了我们的服务如何消费这些记录。

列表 7.15 翻译服务

module.exports.translate = function (event, context, cb) {
  let out = []
  asnc.eachSeries(event.Records, (record, asnCb) => {     ❶
    const payload = new Buffer(record.kinesis.data,
      'base64').toString('utf8')                          ❷
    let message
    try {
      message = JSON.parse(payload)                       ❸
    } catch (exp) {
  ...
  })

❶ 遍历每个记录。

❷ 解码记录。

❸ 转换为对象

我们的服务结合使用 Comprehend 和 Translate。Comprehend 用于检测我们的消息中的语言,Translate 用于在检测到的语言需要时将其转换为英语。下一个列表展示了源代码中的相关调用。

列表 7.16 检测语言和翻译

...
let params = {
  Text: message.originalText
}
comp.detectDominantLanguage(params, (err, data) => {     ❶
...
  params = {
    SourceLanguageCode: data.Languages[0].LanguageCode,
    TargetLanguageCode: 'en',
    Text: message.originalText
  }
  trans.translateText(params, (err, data) => {           ❷
  ...

❶ 检测语言

❷ 翻译成英语

一旦服务翻译了文本,如果需要,它将更新后的消息发布到第二个 Kinesis 流。这将稍后被我们的情感检测服务获取,我们将在不久后部署它。

要部署翻译服务,请在chapter7/pipeline/translate目录中打开命令行并运行

$ npm install
$ serverless deploy

这将在我们的管道中创建第二个阶段。图 7.12 展示了最新部署后我们的管道状态。

图 7.12 Elger

图 7.12 API 部署后的管道

我们已经完成了管道部署的一半。在下一节中,我们将检查到目前为止一切是否正常工作。

7.5 测试管道

现在我们已经部署了管道的一部分,让我们通过一些数据来检查它是否正常工作。为此,我们将利用一个免费的开源公共数据集。让我们现在获取一些这些数据并用来测试我们的管道。

首先cdchapter7/pipeline/testdata目录。这个目录包含一个脚本,它将下载并解压一些测试数据,你可以使用以下命令运行

$ bash ./download.sh

我们使用存储在snap.stanford.edu/data/amazon/productGraph/的亚马逊产品评论数据的一个子集。具体来说,我们使用汽车、美容、办公和宠物类别的数据。一旦脚本完成,你将在testdata/data目录中拥有四个 JSON 文件。每个文件包含一定数量的评论,包括评论文本和总体评分。你可以用文本编辑器打开这些文件,浏览它们以了解数据。

testdata目录中还有一个名为preproc.sh的脚本。它将下载的评论数据处理成用于训练和测试我们自定义分类器的格式。我们将在下一节中查看分类器,但现在让我们通过运行此脚本来处理我们的数据:

$ cd pipeline/testdata
$ bash preproc.sh

这将在data目录中创建多个附加文件。对于每个下载的文件,它创建一个新的 JSON 文件,其结构如下一列表所示。

列表 7.17 亚马逊评论数据格式

{
  train: [...],    ❶
  test: {
    all: [...],
    neg: [...],    ❷
    pos: [...]     ❸
  }
}

❶ 训练数据

❷ 负面测试数据

❸ 正面测试数据

脚本所做的是将输入数据分成两个集合,一个用于训练,一个用于测试,其中大部分记录在训练集中。在测试集中,我们使用原始数据中的overall字段来确定这些评论数据是正面还是负面。这将允许我们稍后测试我们的情感过滤器。脚本还创建了一个 CSV(逗号分隔值)文件,data/final/training.csv。我们将在下一节中使用此文件来训练我们的分类器。

现在我们已经下载并准备好了数据,我们可以检查我们的管道到目前为止是否正常工作。在pipeline/driver目录中有一个用于此目的的测试工具。这包含两个小的 Node.js 程序:driver.js,它使用测试数据调用我们的 API,以及streamReader.js,它从指定的 Kinesis 流中读取数据,这样我们就可以看到该流中存在哪些数据。我们不会在这里详细介绍代码。

首先,让我们向我们的 API 发送一些数据。在pipeline/driver目录中打开命令行,安装依赖项,然后运行驱动程序:

$ npm install
$ node driver.js office pos
$ node driver.js office neg
$ node driver.js beauty neg

这将使用三个随机评论调用 API:两个来自办公产品数据集,一个来自美容数据集。驱动程序还允许我们指定数据应该是正面还是负面。接下来,让我们检查数据是否确实在我们的 Kinesis 流中。首先运行

$ node streamReader.js translate

这将从我们的翻译流中读取数据并在控制台上显示。流读取器代码每秒轮询 Kinesis 以显示最新数据。要停止读取器,请按 Ctrl-C。接下来,为情感流重复此练习:

$ node streamReader.js sentiment

你应该会在控制台上看到相同的数据显示,还有一些由翻译服务添加的额外字段。

7.6 使用 Comprehend 进行情感分析

现在我们已经测试了我们的管道,是时候实施下一阶段了,即检测传入文本的情感。这个代码位于pipeline/sentiment目录中,并使用 AWS Comprehend 来确定情感。无服务器配置与之前的服务非常相似,所以我们在这里不会详细说明,只是要注意配置创建了一个 S3 存储桶来收集负面评论数据以供进一步处理。

情感分析

情感分析是一个复杂的过程,涉及自然语言处理(NLP)、文本分析和计算语言学。对于计算机来说,这是一个困难的任务,因为从根本上讲,它涉及到在一定程度上检测文本中表达的情感。考虑以下可能由评论者关于他们刚刚入住的酒店所写的句子:

我们讨厌离开酒店,回家时感到很悲伤。

虽然这实际上是在表达对酒店的正面情感,但如果单独考虑这个句子中的所有单词,它们都是负面的。随着深度学习技术的应用,情感分析变得越来越准确。然而,有时仍然需要人工进行判断。

通过使用 AWS Comprehend,我们不必担心所有这些复杂性;我们只需处理结果,并在 API 无法做出准确判断时调用人工。

服务的代码位于handler.js中,并在下一列表中展示。

列表 7.18 情感分析处理器

{
module.exports.detect = function (event, context, cb) {
  asnc.eachSeries(event.Records, (record, asnCb) => {
    const payload = new Buffer(record.kinesis.data,
      'base64').toString('utf8')                             ❶
    let message = JSON.parse(payload)
    ...
    let params = {
      LanguageCode: 'en',
      Text: message.text
    }
    comp.detectSentiment(params, (err, data) => {            ❷
      ...

      if (data.Sentiment === 'NEGATIVE' ||
          data.Sentiment === 'NEUTRAL' ||
          data.Sentiment === 'MIXED') {                      ❸
        writeNegativeSentiment(outMsg, (err, data) => {
          asnCb(err)
        })
      } else {
        if (data.SentimentScore.Positive < 0.85) {           ❹
          writeNegativeSentiment(outMsg, (err, data) => {
          ...
      }
    })
  ...
}

❶ 从 Kinesis 中提取消息。

❷ 检测情感

❸ 将负面、中性或混合消息写入 S3

❹ 即使是正面的,也要根据置信度来写

在提取消息后,代码调用 Comprehend 来检测消息的情感。任何负面消息都被写入 S3 存储桶以供进一步处理。正面消息被丢弃。然而,你可以在这一点上进行进一步的计算;例如,监控正面与负面情感的比例,并在异常条件下发出警报。

就像所有 AI 服务一样,正确解释返回的置信度对于业务问题非常重要。在这种情况下,我们决定采取谨慎的态度。这意味着

  • 任何整体负面、中性或混合消息被视为负面情感,并继续进行分类。

  • 任何整体正面消息,如果置信度超过 85%,则被丢弃。

  • 任何整体正面消息,如果置信度低于 85%,则被视为负面,并继续进行分类。

记住,在这个场景中,一旦分类,未丢弃的消息将被发送给人工处理。我们可以轻松地更改这些规则以适应我们的业务流程——例如,如果我们不太关心收集所有投诉,只想关注强烈负面结果,我们可以丢弃中性和正面消息,无论置信度如何。关键是理解我们的结果都伴随着一个相关的置信度,并据此进行解释。

现在让我们部署情感分析服务。切换到pipeline/sentiment目录并运行以下命令:

$ npm install
$ serverless deploy

一旦服务部署完成,我们可以通过再次运行驱动程序来重新测试我们的管道,发送一些正面和负面的消息,如下一列表所示。

列表 7.19 Amazon 评论数据格式

$ cd pipeline/driver
$ node driver.js office pos      ❶
 $ node driver.js beauty pos
$ node driver.js beauty neg      ❷
 $ node driver.js auto neg

❶ 发送正面消息

❷ 发送负面消息

为了检查我们的管道是否正常工作,在driver目录中运行streamReader实用程序,这次告诉它从分类流中读取:

$ node streamReader.js classify

这将从我们的分类器流中读取数据并显示在控制台上。流读取器代码每秒轮询 Kinesis 以显示最新数据。要停止读取器,请按 Ctrl-C。你应该看到消息输出,以及一些来自情感分析的其他数据。注意,强烈正面消息将被丢弃,因此并非所有由驱动程序发送的消息都会到达分类器流。

在此部署之后,我们管道的当前状态如图 7.13 所示。

提示:尽管我们正在将翻译和情感分析服务作为数据管道的一部分使用,但当然也可以单独使用。也许你可以想想在你的当前工作或组织中可以应用这些服务的实例。

图片

图 7.13 情感服务部署后的管道

7.7 训练自定义文档分类器

在我们管道的最终阶段,我们将使用一个自定义分类器。从传入的消息文本中,我们的分类器将能够确定消息属于哪个部门:汽车、美容、办公室或宠物。从头开始训练分类器是一个复杂的任务,通常需要一定程度的机器学习深入知识。幸运的是,AWS Comprehend 使这项工作变得容易得多。图 7.14 说明了训练过程。

图片

图 7.14 使用 Comprehend 训练自定义分类器的流程

所有训练自定义分类器的代码都在pipeline/training目录中。为了训练我们的分类器,我们需要做以下几步:

  • 创建一个数据存储桶。

  • 将训练数据上传到存储桶。

  • 为分类器创建一个 IAM 角色。

  • 运行训练数据以创建分类器。

  • 创建一个端点以使分类器可用。

文档分类模型

文档分类是将一个或多个类别或类型分配给文档的问题。在这个上下文中,文档可以是从大型手稿到单个句子的任何内容。这通常使用两种方法之一来完成:

  • 无监督分类--根据文本分析将文档聚类成类型

  • 监督分类--为训练过程提供标记数据以构建针对我们需求的定制模型

在本章中,我们使用监督分类来训练模型。通过使用 Comprehend,我们不需要深入了解训练过程;我们只需要为 Comprehend 提供一个标记的数据集进行训练。

7.7.1 创建训练存储桶

在我们创建训练存储桶之前,我们需要更新我们的.env文件。像之前一样在文本编辑器中打开它,并添加下一个列表中指示的行,用您自己的唯一存储桶名称替换。

列表 7.20 管道环境文件

CHAPTER7_PIPELINE_TRAINING_BUCKET=<your training bucket name>

要创建存储桶,在pipeline/training目录下使用cd命令进入该目录并运行以下命令:

$ cd pipeline/training
$ npm install
$ cd resources
$ serverless deploy

7.7.2 上传训练数据

如您从上一节中测试我们的管道时回忆的那样,我们的数据处理脚本创建了一个用于训练的 CSV 文件。我们现在需要将其上传到我们的训练存储桶。在pipeline/testdata目录下使用cd命令进入该目录并运行

$ source ../.env &&  aws s3 sync ./data/final s3://${CHAPTER7_PIPELINE_TRAINING_BUCKET}

这将把训练数据集推送到 S3。请注意,训练文件大约有 200MB,因此根据您的出站连接速度,上传可能需要一段时间。

训练数据文件只是一个包含一组标签和相关文本的csv文件,如下所示。

列表 7.21 训练数据文件结构

<LABEL>, <TEXT>

在我们的案例中,标签是AUTOBEAUTYOFFICEPET之一。Comprehend 将使用此文件来构建一个自定义分类器,使用文本数据来训练模型并将其与适当的标签匹配。

7.7.3 创建 IAM 角色

接下来,我们必须为分类器创建一个身份和访问管理(IAM)角色。这将限制分类器可以访问的 AWS 云服务。要创建该角色,在pipeline/training目录下使用cd命令进入该目录并运行

$ bash ./configure-iam.sh

这将创建角色并将新创建的角色 ARN 写入控制台。将角色 ARN 添加到.env文件中,如下所示。

列表 7.22 使用角色 ARN 更新管道环境

CHAPTER7_DATA_ACCESS_ARN=<your ARN>

注意 AWS 身份和访问管理(IAM)功能在 AWS 中无处不在。AWS IAM 定义了整个平台上的角色和访问权限。完整的描述超出了本书的范围,但您可以在以下链接中找到完整的 AWS IAM 文档:mng.bz/NnAd

7.7.4 运行训练

现在我们已经准备好开始训练分类器了。执行此操作的代码位于pipeline/training/train-classifier.js中。此代码简单地调用 Comprehend 的createDocumentClassifier API,传入数据访问角色、分类器名称和训练存储桶的链接。这将在下一个列表中展示。

列表 7.23 训练分类器

const params = {                                                  ❶
   DataAccessRoleArn: process.env.CHAPTER7_DATA_ACCESS_ARN,
  DocumentClassifierName: process.env.CHAPTER7_CLASSIFIER_NAME,
  InputDataConfig: {
    S3Uri: `s3://${process.env.CHAPTER7_PIPELINE_TRAINING_BUCKET}`
  },
  LanguageCode: 'en'
}

comp.createDocumentClassifier(params, (err, data) => {           ❷
 ...

❶ 设置训练参数。

❷ 开始训练。

要开始训练,请使用cd命令进入pipeline/training目录并运行

$ bash ./train.sh

需要注意的是,训练过程可能需要一段时间才能完成,通常超过一小时,所以现在可能是一个休息的好时机!您可以通过在同一目录中运行脚本status.sh来检查训练过程的状态。一旦分类器准备好使用,它将输出状态TRAINED

7.8 使用自定义分类器

现在我们已经训练了我们的分类器,我们可以在管道中完成最后阶段:部署一个分类服务来调用我们新训练的自定义分类器。回想一下,我们已经确定了消息的语言,如果需要,翻译成英语,并在处理存储桶中过滤出只有负面消息。现在我们需要通过运行我们新训练的分类器来确定这些消息与哪个部门相关。

要使分类器可用,我们需要创建一个端点。通过在pipeline/training目录中运行脚本endpoint.sh来完成此操作:

$ cd pipeline/training
$ bash ./endpoint.sh

警告:一旦创建分类器的端点,您将按小时计费,因此请确保您完成此章节后删除所有资源!

在我们部署分类服务之前,我们需要更新.env文件以提供输出存储桶的名称。在文本编辑器中打开它,并编辑下一列表中指示的行,用您自己的唯一存储桶名称替换。

列表 7.24 管道处理存储桶

CHAPTER7_PIPELINE_PROCESSING_BUCKET=<your processing bucket name>

我们分类服务的代码位于pipeline/classify目录中。该目录包含服务的serverless.ymlhandler.js文件。以下列表显示了如何从服务的主处理函数中执行分类器。

列表 7.25 调用自定义分类器端点

...
  let params = {                                    ❶
    EndpointArn: process.env.CHAPTER7_ENDPOINT_ARN,
    Text: message.text
  }
  comp.classifyDocument(params, (err, data) => {    ❷
    if (err) { return asnCb(err) }
    let clas = determineClass(data)                 ❸
    writeToBucket(clas, message, (err) => {         ❹
      if (err) { return asnCb(err) }
      asnCb()
    })
  })
  ...

❶ 将端点 ARN 添加到参数中。

❷ 通过端点调用分类器。

❸ 处理结果。

❹ 将消息写入输出存储桶。

虽然我们已经训练了自己的自定义分类器,但其消耗模式与其他我们之前遇到的服务相似,因此代码应该看起来很熟悉。在列表 7.25 中调用的determineClass函数如下所示。

列表 7.26 解释自定义分类结果

function determineClass (result) {
  let clas = classes.UNCLASSIFIED
  let max = 0
  let ptr

  result.Classes.forEach(cl => {   ❶
    if (cl.Score > max) {
      max = cl.Score
      ptr = cl
    }
  })
  if (ptr.Score > 0.95) {          ❷
    clas = classes[ptr.Name]
  }
  return clas
}

❶ 找到分数最高的分类。

❷ 只接受大于 95%的分数。

该函数返回分数最高的分类类别,前提是分数大于 95%。否则,它将返回UNCLASSIFIED的结果。需要注意的是,与其他我们遇到的服务一样,置信水平的解释是特定领域的。在这种情况下,我们选择了高精度(大于 95%)。未分类的结果需要由人工处理,而不是直接发送到部门。

要部署分类服务,请使用cd命令进入pipeline/classify目录并运行

$ npm install
$ serverless deploy

我们现在已经完全部署了我们的管道!在本章的最后一步,让我们进行端到端测试。

7.9 端到端测试管道

为了测试我们的完整管道,让我们首先向其中推送一些数据。我们可以通过使用之前的测试驱动器来完成此操作。进入目录 pipeline/driver,并通过运行以下命令推送一些数据

$ node driver.js [DEPT] [POS | NEG]

这样做几次,用随机的部门名称替换:autobeautyofficepet。同时,随机使用正负值。信息应该通过管道流动,负面信息最终会出现在处理桶中的五个可能路径之一下:auto、beauty、office、pet 或未分类。我们提供了一个脚本来帮助检查结果。进入 pipeline/driver 目录并运行

$ node results.js view

这将获取桶中的输出结果并将其打印到控制台。你应该会看到类似以下输出:

beauty
I'm not sure where all these glowing reviews are coming from...
NEGATIVE
{
  Positive: 0.0028411017265170813,
  Negative: 0.9969773292541504,
  Neutral: 0.00017945743456948549,
  Mixed: 0.0000021325695342966355
}

office
I bought this all in one HP Officejet for my son and his wife...
NEGATIVE
{
  Positive: 0.4422852396965027,
  Negative: 0.5425800085067749,
  Neutral: 0.015050739049911499,
  Mixed: 0.00008391317533096299
}

unclassified
didnt like it i prob will keep it and later throw it out...
NEGATIVE
{
  Positive: 0.00009981004404835403,
  Negative: 0.9993864297866821,
  Neutral: 0.0005127472686581314,
  Mixed: 9.545062766846968e-7
}

请记住,只有负面信息会出现在结果桶中;正面值应该已经被情感过滤器丢弃。花些时间来审查结果。一些信息将未被分类,这意味着分类步骤的置信水平低于 95%。

在此过程中的下一个逻辑步骤可能是根据管道输出发送警报电子邮件到适当的部门。这可以很容易地使用 Amazon 的 SES(简单电子邮件服务)服务完成,我们将此作为练习留给读者来完成!

作为进一步练习,你可以编写一个脚本将大量数据推送到管道中,并观察系统如何表现。你也可以尝试编写自己的评论或“推文”并发送到管道中,以确定系统在呈现不同数据项时的准确性。

7.10 移除管道

一旦你完成了管道,重要的是要将其移除,以避免从 AWS 负担额外的费用。为此,我们提供了一些脚本,这些脚本将移除目录 chapter7/pipeline 中管道的所有元素。进入此目录并运行

$ bash ./remove-endpoint.sh
$ bash ./check-endpoint.sh

这将移除端点,这可能需要几分钟才能完成。你可以重新运行 check-endpoint.sh 脚本;这将显示针对我们的端点的状态为 DELETING。一旦脚本不再列出我们的端点,你可以通过运行以下命令来继续移除系统的其余部分

$ bash ./remove.sh

这将移除自定义分类器以及本节中部署的所有其他资源。请确保所有资源确实已被脚本移除!

7.11 自动化的好处

让我们花点时间思考一下这种类型的处理如何使组织受益。截至 2019 年 4 月,Amazon.com 拥有一个包含数亿个条目的产品目录。让我们考虑一个较小的零售商,该零售商在多个不同的部门中列出,例如,500,000 个项目。让我们假设客户在以下五个渠道中提供反馈:

  • Twitter

  • Facebook

  • 网站评论

  • 邮件

  • 其他

让我们再假设在平均每天,2% 的产品将在这些渠道中的每一个都获得一些关注。这意味着公司每天大约有 50,000 项反馈需要审查和处理。按年度计算,相当于 18,250,000 项单独的反馈。

假设一个人平均需要两分钟来处理每条反馈,那么在标准八小时工作日中,一个人只能处理 240 条这样的反馈。这意味着需要超过 200 人的团队来手动处理所有反馈项。

我们的人工智能管道可以轻松处理这种负载,每天 24 小时,每年 365 天,大幅降低成本和繁琐。

希望这一章能够激发您进一步探索如何在日常工作中应用人工智能作为服务来解决这些问题。

摘要

  • 将人工智能作为服务应用于现有系统有多种架构模式:

    • 同步 API

    • 异步 API

    • 流入

    • 全连接流

  • 可以使用 AWS Textract 从文档中提取关键文本字段。我们在从护照扫描中提取信息的特定案例中演示了示例。

  • 以现有的电子商务/零售平台为例,我们可以使用 Kinesis 和 Lambda 构建一个人工智能数据处理的管道。

  • AWS Translate 可以用于即时翻译语言。

  • 使用亚马逊的产品评论数据,可以构建一个情感分析服务。

  • 使用 Comprehend 构建了一个文档分类器,通过将亚马逊评论数据分为训练集和测试集来实现。

  • 将所有这些技术结合到一个数据处理管道中,结果是一个能够翻译、过滤和分类数据的系统。这是如何结合几个人工智能服务以实现商业目标的一个示例。

警告 请确保您完全删除了本章中部署的所有云资源,以避免产生额外费用!

第三部分. 将一切整合

在第八章中,我们构建了一个无服务器网络爬虫,并探讨了与数据收集相关的一些挑战。在第九章中,我们使用 AI as a Service 来分析我们使用无服务器爬虫抓取的数据,并探讨如何有效地编排和控制我们的分析任务。如果你已经掌握了到目前为止的所有内容,那么这部分内容将具有挑战性,但希望也是有益和启发性的。

一旦你掌握了这一部分,你将能够跟上当前技术的最新状态,并准备好在自己的工作中应用工具、服务和技巧。祝你好运!

8 在真实世界人工智能中大规模收集数据

本章涵盖

  • 选择人工智能应用的数据来源

  • 构建一个无服务器网络爬虫以寻找大规模数据来源

  • 使用 AWS Lambda 从网站中提取数据

  • 理解大规模数据收集的合规性、法律方面和礼貌考虑

  • 使用 CloudWatch Events 作为事件驱动无服务器系统的总线

  • 使用 AWS Step Functions 进行服务编排

在第七章中,我们讨论了自然语言处理(NLP)技术在产品评论中的应用。我们展示了如何使用 AWS Comprehend 在无服务器架构中使用流数据实现情感分析和文本分类。在本章中,我们关注数据收集。

根据一些估计,数据科学家花费 50-80%的时间在收集和准备数据。1 2 许多数据科学家和机器学习实践者会说,在执行分析和机器学习任务时,找到高质量的数据并正确准备它是面临的最大挑战。显然,应用机器学习的价值仅取决于输入算法的数据质量。在我们直接开发任何 AI 解决方案之前,有一些关键问题需要回答,这些问题涉及到将要使用的数据:

  • 需要哪些数据以及数据格式是什么?

  • 可用的数据来源有哪些?

  • 数据将如何被清洗?

对数据收集概念的良好理解是功能机器学习应用的关键。一旦你学会了根据应用需求获取和调整数据,你产生预期结果的机会将大大增加!

8.1 场景:寻找事件和演讲者

让我们考虑一个许多软件开发者都会遇到的问题——寻找要参加的相关会议。想象一下,如果我们想要构建一个解决这个问题的系统。用户将能够搜索感兴趣主题的会议,并查看会议的演讲者、地点以及会议时间。我们还可以想象扩展这个功能,向搜索过或“喜欢”过其他活动的用户推荐会议。3

图 8.1 我们的数据收集应用程序将爬取会议网站并提取事件和演讲者信息。

构建这样一个系统的第一个挑战在于收集和编目会议事件的数据。目前没有现成的、完整的、结构化的数据来源。我们可以使用搜索引擎找到相关事件的网站,但接下来就是找到和提取事件地点、日期以及演讲者和主题信息的问题。这是一个应用网络爬虫和抓取的绝佳机会!让我们用图 8.1 总结我们的需求。

8.1.1 确定所需数据

识别你的数据的第一个步骤是从你正在解决的问题开始。如果你对你要实现的目标有一个清晰的了解,就从这个地方开始工作,并确定所需的数据及其属性。所需数据类型受到两个因素的影响:

  • 训练和验证是否必要?

  • 如果是这样,你的数据是否需要标注?

在本书中,我们一直在使用托管 AI 服务。这种方法的一个主要优势是它通常消除了训练的需要。不需要使用自己的数据进行训练的服务附带预训练模型,这些模型可以与你的测试数据集一起使用。在其他情况下,你可能需要执行训练步骤。

训练、验证和测试数据 在机器学习模型开发过程中,通常将数据集分为三个集合,如图 8.2 所示。

图片

图 8.2 模型开发和测试中的训练、验证和测试数据

更大的数据比例,即训练集,用于训练算法。验证集(或开发集)用于选择算法并衡量其性能。最后,测试集是一个独立的集合,用于检查算法在未用于训练的数据上的泛化能力。

你可能还记得第一章中提到的监督学习和无监督学习。了解你使用的方法很重要,因为监督学习需要将数据标注上标签。

在第一章中,我们展示了一个托管 AWS AI 服务的表格。这个表格在附录 A 中得到了扩展,显示了每个服务的数据要求和训练支持。你可以在规划你的 AI 赋能应用程序时使用这个作为参考。

如果你没有使用托管 AI 服务,而是选择一个算法并训练一个自定义模型,那么收集和准备数据所需的工作量很大。有许多考虑因素需要确保数据能够产生准确的结果,并在测试数据集的领域内良好工作。

选择代表性数据

在选择用于训练机器学习模型的数据时,确保数据能够代表真实世界中的数据至关重要。当你的数据做出导致偏见结果的假设时,就会出现问题。选择良好的训练数据对于减少过拟合至关重要。当模型对训练数据集过于特定,无法泛化时,就会发生过拟合。

华盛顿大学的一组研究人员通过训练一个机器学习模型来检测图片中是否包含狼或哈士奇狗,说明了选择偏差的问题。他们故意选择有雪背景的狼图片和有草背景的哈士奇狗图片来训练算法,实际上这个算法只对检测草与雪有效。当他们将结果展示给一组测试对象时,人们仍然报告说他们相信算法检测哈士奇和狼的能力4

我们还知道,使用来自具有人类偏见输出的系统的数据进行训练可能会导致算法继承现有的有害社会偏见。当微软的“Tay”推特机器人开始生成种族主义、仇恨言论后被迫关闭时,这一情况被臭名昭著地展示出来。5

在选择好的数据时,可以应用以下几条规则:

  • 数据应该有所有可能遇到的场景的表示(比如除了雪以外的背景上的哈士奇!)。

  • 对于分类,你应该有足够,并且最好是所有类别的近似相等,的表示。

  • 对于标签,考虑是否可以无歧义地分配标签,或者如果不可以,如何处理这种情况。你可能会有这样的情况,要分配的标签并不明确(“那是一只哈士奇还是一只狗?”)。

  • 定期手动检查你数据的一个合理大小的随机样本,以验证是否发生了意外的情况。花些时间做这件事是值得的,因为坏数据永远不会产生好的结果!

在这本书中,我们主要关注使用预训练的、托管的服务。对于机器学习训练优化、数据整理和特征工程的更深入理解,我们推荐 Brink、Richards 和 Fetherolf 所著的《Real-World Machine Learning》,由 Manning Publications 于 2017 年出版。Real-World Machine Learning

8.1.2 数据来源

第一章讨论的关键点之一是,人工智能领域的近期成功是如何通过大量数据的可用性得以实现的。互联网本身就是数据的一个公共来源,通过在我们的日常生活中使用互联网,我们不断为不断增长的大量极其详细的数据做出贡献。大型科技公司(如谷歌、Facebook、亚马逊)在人工智能领域取得了巨大成功。这其中的一个重要因素是他们可以访问数据,并且在数据收集方面有专业知识。6 对于其他人来说,有许多方法可以为人工智能应用获取数据。附录 C 包含了一列可能非常适合你应用需求的公共数据集和其他数据来源。

8.1.3 准备训练数据

一旦你收集了用于训练的数据,还有很多工作要做:

  • 处理缺失数据。你可能需要删除记录、插值或外推数据,或使用其他方法避免缺失字段的问题。在其他情况下,最好将缺失字段留空,因为这可能是算法的重要输入。关于这个话题的更多信息,请参阅 John Mount 和 Nina Zumel 所著的《探索数据科学》第一章“探索数据”。7

  • 获取正确的数据格式。这可能意味着为日期或货币值应用一致的格式。在图像识别中,这可能意味着裁剪、调整大小和更改颜色格式。许多预训练的网络是在 224x224 RGB 数据上训练的,所以如果你想要分析非常高分辨率的图像数据(如果调整大小,则可能会丢失太多信息),那么这些网络可能在不修改的情况下不适用。

我们简要介绍了机器学习工程师可用的数据源。应该很明显,互联网是大规模数据量的主要来源。大量互联网数据无法通过 API 或结构化文件访问,而是发布在旨在用网络浏览器消费的网站上。从这些宝贵的资源中收集数据需要爬取、抓取和提取。这是我们接下来要讨论的主题。

8.2 从网络收集数据

本章的剩余部分将更详细地探讨从网站收集数据。尽管一些数据可能以预包装、结构化的格式提供,可以通过平面文件或 API 访问,但网页并非如此。

网页是产品数据、新闻文章和财务数据等非结构化信息来源。找到合适的网页、检索它们并提取相关信息是非平凡的。完成这些所需的过程被称为网络爬取网络抓取

  • 网络爬取是根据特定策略抓取网页内容并导航到链接页面的过程。

  • 网络抓取跟随爬取过程,从已抓取的内容中提取特定数据。

图 8.3 展示了两个过程如何结合以产生有意义的、结构化的数据。

图 8.3

图 8.3 网页爬取和抓取过程概述。在本章中,我们关注的是这幅图中的爬取器部分及其产生的输出页面。

回想一下本章开头提到的会议演讲者信息收集场景。为这个场景创建解决方案的第一步将是构建一个无服务器网络爬取系统。

8.3 网络爬取简介

我们场景中的爬取器将是一个通用爬取器。通用爬取器可以爬取任何结构未知的网站。特定网站的爬取器通常是为大型网站创建的,具有用于查找链接和内容的特定选择器。一个特定网站爬取器的例子可能是编写来爬取 amazon.com 特定产品或 ebay.com 拍卖的爬取器。

一些知名爬虫的例子包括

  • 搜索引擎如 Google、Bing、Yandex 或 Baidu

  • GDELT 项目(www.gdeltproject.org),一个关于人类社会和全球事件的开源数据库

  • OpenCorporates(opencorporates.com),世界上最大的开放公司数据库

  • 互联网档案馆(archive.org),一个包含互联网网站和其他数字形式的文化遗产的数字图书馆

  • CommonCrawl(commoncrawl.org/),一个开放的网页爬取数据存储库

网络爬取的一个挑战是访问和分析的网页数量庞大。当我们执行爬取任务时,我们可能需要任意大的计算资源。一旦爬取过程完成,我们的计算资源需求就会下降。这种可扩展的、突发性的计算需求非常适合按需、云计算和无服务器!

8.3.1 典型网络爬虫过程

要了解网络爬虫可能的工作方式,可以考虑网页浏览器如何允许用户手动导航网页:

  1. 用户将网页 URL 输入到网页浏览器中。

  2. 浏览器获取网页的第一个 HTML 文件。

  3. 浏览器解析 HTML 文件以找到其他所需内容,如 CSS、JavaScript 和图片。

  4. 链接会被渲染。当用户点击链接时,过程会为新的 URL 重复。

以下列表显示了非常简单的示例网页的 HTML 源代码。

列表 8.1 示例网页 HTML 源代码

<!DOCTYPE html>
<html>
  <body>
    <a href="https://google.com">Google</a>           ❶
    <a href="https://example.com/about">About</a>     ❷
    <a href="/about">About</a>                        ❸

    <img src="/logo.png" alt="company logo"/>         ❹

    <p>I am a text paragraph</p>                      ❺

    <script src="/script.js"></script>                ❻
  </body>
</html>

❶ 外部链接

❷ 绝对内部链接

❸ 相对内部链接

❹ 图片资源

❺ 段落文本

❻ JavaScript 源代码

我们已经展示了非常基础的页面结构。实际上,一个单独的 HTML 页面可以包含数百个超链接,包括内部和外部链接。为特定应用需要爬取的页面集合被称为爬取空间。让我们讨论典型网络爬虫的架构以及它是如何构建以处理不同大小的爬取空间的。

8.3.2 网络爬虫架构

典型的网络爬虫架构如图 8.4 所示。在描述如何使用无服务器方法实现之前,让我们先了解架构的每个组件以及它们如何与我们的会议网站场景相关。

图 8.4 网络爬虫的组件。每个组件都有明确的职责,这可以指导我们在软件架构中的工作。

  • 前端维护一个待爬取 URL 的数据库。这最初由会议网站填充。从这里,网站上单个页面的 URL 被添加到这里。

  • 获取器接受一个 URL 并检索相应的文档。

  • 解析器获取获取到的文档,解析它,并从中提取所需信息。在此阶段,我们不会寻找特定的演讲者详细信息或任何会议特定的内容。

  • 策略工作器或生成器是网络爬虫最关键组件之一,因为它决定了爬取空间。策略工作器生成的 URL 被反馈到前沿。策略工作器决定

    • 应该跟随哪些链接

    • 要爬取的链接的优先级

    • 爬取深度

    • 如果需要,何时重新访问/重新爬取页面

  • 项目存储是提取的文档或数据存储的地方。

  • 调度器接受一组 URL,最初是种子 URL,并安排获取器下载资源。调度器负责确保爬虫对 Web 服务器表现得礼貌,不抓取重复的 URL,并且 URL 是规范化的。

爬取是否真的适合无服务器架构?

如果你现在在怀疑无服务器架构是否真的是实现网络爬虫的有效选择,你有一个很好的观点!大规模运行的爬虫需要快速、高效的存储;缓存;以及大量的计算能力来处理多个资源密集型的页面渲染过程。另一方面,无服务器应用程序通常以短期、事件驱动的计算和缺乏快速、本地磁盘存储为特征。

那么,本章中的系统是否值得在生产中使用,或者我们是在进行一项疯狂的实验,看看我们能将我们的云原生理念推进多远?!使用更传统的“农场”服务器,如亚马逊弹性计算云(EC2)实例,确实有明显的优势。如果你的爬取需求需要大量持续运行的工作负载,你可能更适合选择传统方法。

我们必须记住维护和运行此基础设施、操作系统以及任何底层框架的隐藏成本。此外,我们的爬取场景是针对特定会议网站的数据按需提取。这种“突发”行为适合弹性、实用计算范式。从缓存的角度来看,无服务器实现可能不是最优的,但就我们的场景而言,这不会产生重大影响。我们对此方法非常满意,因为我们不需要支付任何费用,当系统不运行时,我们也不必担心操作系统的补丁、维护或容器编排和服务发现。

对于我们的网络爬虫,我们处理的是会议。由于这些构成了所有网页中的少数,因此没有必要为这些网站爬取整个网络。相反,我们将为爬虫提供一个“种子”URL。

在会议网站本身,我们将抓取本地超链接。我们不会跟随指向外部域的超链接。我们的目标是找到包含所需数据的页面,例如演讲者和日期。我们不对爬取整个会议网站感兴趣,因此我们还将使用深度限制来在达到链接图中的给定深度后停止爬取。爬取深度是从种子 URL 开始跟随的链接数量。深度限制阻止过程超出指定的深度。

基本爬虫与渲染爬虫

基本爬虫只会抓取 HTML 页面,而不会评估 JavaScript。这导致爬虫过程更加简单和快速。然而,这可能会导致我们排除有价值的数据。

现在非常常见的是,网页由 JavaScript 在浏览器中动态渲染。使用 React 或 Vue.js 等框架的单页应用程序(SPA)是此类示例。一些网站使用这些框架的服务器端渲染,而其他网站则执行预渲染,以将完全渲染的 HTML 作为搜索引擎优化(SEO)技术返回给搜索引擎爬虫。我们不能依赖这些在普遍应用。因此,我们选择采用完整的网页渲染,包括 JavaScript 评估。

当没有用户或屏幕可用时,渲染网页有多种选项:

  • Splash (scrapinghub.com/splash),一个专为网络爬虫应用设计的浏览器。

  • 使用 Puppeteer API 的 Headless Chrome (mng.bz/r2By)。这简单地运行了流行的 Chrome 浏览器,并允许我们以编程方式控制它。

  • 使用 Selenium (www.seleniumhq.org)的 Headless Firefox (mng.bz/V8qG)。这是 Puppeteer 的基于 Firefox 的替代方案。

对于我们的解决方案,我们将使用无头 Chrome。我们选择这个选项是因为有现成的 Serverless Framework 插件可用于 AWS Lambda。

网络爬虫的法律和合规性考虑

网络爬虫的合法性可能是一个有争议的领域。一方面,网站所有者使内容公开可用。另一方面,过度的爬取可能会对网站的可用性和服务器负载产生不利影响。不用说,以下内容不构成法律建议。这里只是几项被视为礼貌行为的最佳实践:

  • 使用User-Agent字符串识别您的爬虫。为网站所有者提供一种联系方式,例如AIaaSBookCrawler/1.0; +https://aiasaservicebook.com)

  • 尊重网站的robots.txt。此文件允许网站所有者说明您可以和不可以爬取的页面。8

  • 如果可用,请使用网站的 API 而不是网络爬虫。

  • 限制每个域每秒的请求数量。

  • 如果网站所有者要求,立即停止爬取网站。

  • 只爬取公开可访问的内容。永远不要使用登录凭证。

  • 使用缓存以减少目标服务器的负载。不要在短时间内重复检索同一页面。

  • 从网站上收集的材料通常属于版权和知识产权法规的范畴。请确保您尊重这一点。

尤其是我们需要确保我们限制每个域名/IP 地址的并发性,或者选择在请求之间选择合理的延迟。这些要求将是我们无服务器爬虫架构的考虑因素。

在撰写本文时,AWS 可接受使用政策禁止“监控或爬取损害或干扰被监控或爬取的系统的行为”(aws.amazon.com/aup/)。

还请注意,一些网站实施了防止网络爬虫的机制。这可以通过检测 IP 地址或用户代理来实现。像 CloudFlare(www.cloudflare.com/products/bot-management/)或 Google reCaptcha(developers.google.com/recaptcha/docs/invisible)这样的解决方案使用了更复杂的方法。

8.3.3 无服务器网络爬虫架构

让我们首先看看我们将如何将我们的系统映射到第一章中开发的规范架构。图 8.5 为我们提供了系统层级的分解以及服务如何协作以提供解决方案。

图片

图 8.5 无服务器网络爬虫系统架构。系统由使用 AWS Lambda 和 AWS Step Functions 实现的定制服务组成。SQS 和 CloudWatchEvents 服务用于异步通信。内部 API 网关用于同步通信。S3 和 DynamoDB 用于数据存储。

系统架构显示了所有服务中系统的各个层级。请注意,在这个系统中,我们没有前端 Web 应用程序:

  • 前端和抓取服务中的同步任务使用 AWS Lambda 实现。首次引入 AWS Step Functions 来实现调度器。它将负责根据前端的数据协调抓取器。

  • 策略服务是异步的,并响应事件总线上的事件,表明已发现新的 URL。

  • 我们系统中内部服务之间的同步通信由 API 网关处理。我们选择了 CloudWatch Events 和 SQS 进行异步通信。

  • 共享参数发布到系统管理器参数存储。IAM 用于管理服务之间的权限。

  • DynamoDB 用于前端 URL 存储。一个 S3 存储桶用作我们的项目存储。

提示:如果您想了解更多关于网络爬虫的信息,请参阅 Satnam Alag 所著的《Collective Intelligence in Action》第六章,“智能网络爬虫”。9

建造还是购买?评估第三方托管服务

写一本宣扬托管服务优点、强调关注核心业务逻辑重要性的书,同时还在书中专门用一章来从头构建网络爬虫,这有一定的讽刺意味。

我们的爬虫相当简单,并且具有特定领域。这是编写我们自己的实现的一些理由。然而,我们知道从经验中,简单的系统会随着时间的推移而变得复杂。因此,实现你自己的任何东西应该是你的最后选择。以下是现代应用开发的两个经验法则:

  • 最小化你编写的代码量!你编写的绝大多数代码应该关注独特的业务逻辑。在可能的情况下,避免编写任何系统中的代码,这些代码是平凡的,并且在许多其他软件系统中实现,通常被称为无差别的重劳动。

  • 使用云托管服务。虽然你可以通过使用库、框架和组件来遵循第 1 条规则,但这些可能有自己的维护负担,而且你仍然需要维护它们运行的基础设施。与云托管服务集成可以减轻你这一重大负担。

这些服务可能超出了你选择的云提供商的范围。即使亚马逊网络服务没有现成的网络爬虫和抓取服务,也要超越 AWS,评估第三方提供的服务。这对于你打算构建的任何服务来说都是一项值得做的练习。例如,如果你想在你应用程序中实现搜索功能,你可能评估一个完全托管的 Elasticsearch 服务,如 Elastic (www.elastic.co),或者一个托管的搜索和发现 API,如 Algolia (www.algolia.com/)).

如果你感兴趣于评估第三方网络爬虫服务,请查看以下内容:10

8.4 实现项目存储

我们将开始对爬虫实现过程的讲解,从所有服务中最简单的一个,即项目存储。作为我们会议网站爬取过程的一部分,项目存储将存储每个会议网站上爬取的每一页的副本。首先,获取代码以便你可以更详细地探索。

8.4.1 获取代码

项目存储的代码位于chapter8-9/item-store目录中。类似于我们之前的示例,这个目录包含一个serverless.yml文件,声明我们的 API、函数和其他资源。在我们部署和测试项目存储之前,我们将逐步解释其内容。

8.4.2 项目存储存储桶

在项目存储中查看serverless.yml文件,我们看到的是一个 S3 存储桶,没有其他内容!我们正在实现最简单的存储方案。

其他服务可以直接写入我们的存储桶或列出对象,并使用 AWS SDK S3 API 抓取对象。所需的一切是他们 IAM 角色和策略中具有正确的权限。

8.4.3 部署项目存储

部署项目存储非常简单。鉴于我们正在部署一个 S3 存储桶,我们将在 chapter8-9 目录中的 .env 文件中定义其全局唯一名称:

ITEM_STORE_BUCKET=<your bucket name>

无需进一步配置。默认区域为 eu-west-1。如果您想指定不同的区域,请使用 serverless deploy 命令的 --region 参数提供:

npm install
serverless deploy

就这样!项目存储已部署并准备就绪。让我们继续到爬虫应用程序中的下一个服务。

8.5 创建一个用于存储和管理 URL 的前沿

我们的前沿将存储所有用于会议网站的种子 URL 以及在爬取过程中发现的新 URL。我们正在使用 DynamoDB 进行存储。我们的目标是利用 DynamoDB 的 API 进行插入和查询,并在其上添加最小层级的抽象。

8.5.1 获取代码

前沿服务的代码位于 chapter8-9/frontier-service 目录中。此目录包含一个 serverless.yml 文件,声明我们的 API、函数和其他资源。我们将在部署和测试前沿服务之前解释其内容。

8.5.2 前沿 URL 数据库

前沿 URL 数据库存储所有打算抓取、已抓取或未能抓取的 URL。服务需要有一个支持以下操作的接口:

  • 插入一个种子 URL

  • 将 URL 的状态更新为 PENDINGFETCHEDFAILED

  • 插入一批被认为适合抓取的新发现 URL(链接)。

  • 根据状态参数和最大记录限制获取给定种子 URL 的 URL 集合。

我们前沿数据库的数据模型通过表 8.1 中的示例进行说明。

表 8.1 前沿 URL 数据库示例

种子 URL 状态 深度
http://microxchg.io http://microxchg.io FETCHED 0
http://microxchg.io http://microxchg.io/2019/index.html FETCHED 1
http://microxchg.io http://microxchg.io/2019/all-speakers.html PENDING 2
https://www.predictconference.com https://www.predictconference.com PENDING 0

在这种情况下,我们的“主键”是种子和 URL 的组合。种子属性是分区键哈希值,而url属性是排序键范围键。这确保我们不会将重复项插入数据库中。

除了我们的表键之外,我们还将定义一个二级索引。这允许我们根据种子 URL 和状态快速搜索。

从表 8.1 中的示例数据中我们可以看到,完整的 URL 包含在url字段中,而不仅仅是相对路径。这允许我们在将来支持从种子链接的外部 URL,并避免了在获取内容时需要重建 URL 的不便。

前沿表的 DynamoDB 资源定义可以在服务的serverless.yml文件中找到,如下所示。

列表 8.2 前沿 DynamoDB 表定义

frontierTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:provider.environment.FRONTIER_TABLE}            ❶
         AttributeDefinitions:
          - AttributeName: seed
            AttributeType: S
          - AttributeName: url
            AttributeType: S
          - AttributeName: status
            AttributeType: S
        KeySchema:                                                        ❷
           - AttributeName: seed
            KeyType: HASH
          - AttributeName: url
            KeyType: RANGE
        LocalSecondaryIndexes:
          - IndexName: ${self:provider.environment.FRONTIER_TABLE}Status  ❸
             KeySchema:
              - AttributeName: seed
                KeyType: HASH
              - AttributeName: status
                KeyType: RANGE
            Projection:
              ProjectionType: ALL
        ProvisionedThroughput:                                            ❹
           ReadCapacityUnits: 5
          WriteCapacityUnits: 5

❶ 表名定义为 frontier。

❷ 表的键由 seed 和 url 属性组成。

❸ 定义了一个二级索引,frontierStatus,允许使用 seed 和 status 属性进行查询。

❹ 在这种情况下,我们选择配置吞吐量,具有五个读写容量单位。或者,我们可以指定 BillingMode: PAY_PER_REQUEST 来处理不可预测的负载。

无服务器数据库

我们已经介绍了一些与 DynamoDB 一起工作的示例。DynamoDB 是一种 NoSQL 数据库,适用于非结构化文档存储。在 DynamoDB 中建模关系数据是可能的,有时也是非常有必要的。一般来说,当您对数据的访问方式有清晰的了解,并且可以设计键和索引来适应访问模式时,DynamoDB 更合适。当您存储结构化数据,但希望在将来支持任意访问模式时,关系型数据库更合适。这正是结构化查询语言(SQL),关系型数据库管理系统支持的接口,非常擅长的。1112

关系型数据库针对从服务器发出的少量长时间运行的连接进行了优化。因此,来自 Lambda 函数的大量短暂连接可能会导致性能不佳。作为对全服务器关系型数据库管理系统(RDBMS)的替代方案,Amazon Aurora Serverless 是一种无服务器关系型数据库解决方案,避免了需要配置实例的需求。它支持自动扩展和按需访问,允许您按秒计费。您还可以使用 Lambda 函数中的 AWS SDK 通过 Data API 对 Aurora Serverless 运行查询(mng.bz/ZrZA)。此解决方案避免了创建短暂数据库连接的问题。

8.5.3 创建前沿 API

我们已经描述了前沿服务中至关重要的 DynamoDB 表。我们需要一种方法将会议网站的 URL 引入系统。现在让我们看看 API Gateway 和 Lambda 函数,这些函数允许外部服务与前沿交互,以实现这一目标。

前沿服务支持的 API 列于表 8.2 中。

表 8.2 前沿服务 API

路径 方法 Lambda 函数 描述
frontier-url/{seed}/{url} POST create 为种子添加 URL
frontier-url/{seed} POST create 添加新的种子
frontier-url/{seed}/{url} PATCH update 更新 URL 的状态
frontier-url PUT bulkInsert 创建一批 URL
frontier-url/{seed} GET list 根据状态列出种子 URL,最多指定记录数

每个 API 的定义可以在frontier-serviceserverless.yml配置文件中找到。此配置还定义了一个用于服务 API 的 Systems Manager Parameter Store 变量。我们没有使用 DNS 来配置 API,因此它不能通过已知名称被其他服务发现。相反,API Gateway 生成的 URL 已注册在参数存储中,以便具有正确 IAM 权限的服务可以找到。

为了简单起见,我们所有的 Lambda 代码都实现在了handler.js中。它包括创建和执行 DynamoDB SDK 调用的逻辑。如果您查看此代码,您会发现其中很多与第四章和第五章中的处理器相似。一个显著的区别是我们引入了一个名为Middy的库来减少模板代码。Middy 是一个中间件库,允许您在 Lambda 被调用之前和之后拦截调用,以执行常见操作(middy.js.org)。中间件简单来说是一组函数,它们会钩入您的事件处理器的生命周期。您可以使用 Middy 的任何内置中间件或任何第三方中间件,或者编写自己的。

对于我们的前端处理器,我们设置了如下一列表中的 Middy 中间件。

列表 8.3 前端处理器中间件初始化

const middy = require('middy')
...

const { cors, jsonBodyParser, validator, httpEventNormalizer, httpErrorHandler } = require('middy/middlewares')

const loggerMiddleware = require('lambda-logger-middleware')
const { autoProxyResponse } = require('middy-autoproxyresponse')
...

function middyExport(exports) {
  Object.keys(exports).forEach(key => {
    module.exports[key] = middy(exports[key])                   ❶
      .use(loggerMiddleware({ logger: log }))                   ❷
      .use(httpEventNormalizer())                               ❸
      .use(jsonBodyParser())                                    ❹
      .use(validator({ inputSchema: exports[key].schema }))     ❺
      .use(cors())                                              ❻
      .use(autoProxyResponse())                                 ❼
      .use(httpErrorHandler())                                  ❽
  })
}

middyExport({
  bulkInsert,
  create,
  list,
  update
})

❶ Middy 包装了普通的 Lambda 处理器。

❷ lambda-logger-middleware 在开发环境中记录请求和响应。13 我们与第六章中引入的 Pino 日志记录器一起使用它。

❸ httpEventNormalizer 为 queryStringParameters 和 pathParameters 添加默认空对象。

❹ jsonBodyParser 自动解析体并提供一个对象。

❺ validator 验证输入体和参数与我们定义的 JSON 模式。

❻ cors 自动向响应中添加 CORS 头。

❼ middy-autoproxy response 将简单的 JSON 对象响应转换为 Lambda Proxy HTTP 响应。14

图 8.6 检索器实现与参数存储、前沿 API、嵌入式无头网络浏览器、项目存储和事件总线集成。

此服务接受对一批 URL 的fetch请求。对于每个 URL,依次执行页面检索、渲染和解析步骤。

注意:我们尚未为检索处理程序定义任何 Lambda 触发器。我们不会使用 API 网关或异步事件,而是将允许此处理程序直接通过 AWS Lambda SDK 调用。这是一个特殊情况,因为我们的检索器实现导致 Lambda 长时间运行,检索多个页面。API 网关最多 30 秒就会超时。基于事件的触发器不合适,因为我们希望从调度器有同步调用。

8.6.1 配置和控制无头浏览器

服务的配置(serverless.yml)包括一个新插件,serverless-plugin-chromegithub.com/adieuadieu/serverless-chrome),如下所示。

列表 8.4 检索服务serverless.yml加载和配置 Chrome 插件

service: fetch-service

plugins:
...
  - serverless-plugin-chrome       ❶
...
custom:
  chrome:
    flags:                         ❷
      - --window-size=1280,1696
      - --hide-scrollbars
...

❶ 在 serverless.yml 的插件部分指定了插件。它导致在调用处理程序之前打开浏览器。

❷ 提供浏览器命令行参数。为了创建有用的截图,我们提供分辨率并隐藏任何滚动条。

此插件在 Lambda 函数加载时自动以无头模式(即没有用户界面)安装 Google Chrome 网络浏览器。然后我们可以使用chrome-remote-interface模块(github.com/cyrus-and/chrome-remote-interface)以编程方式控制浏览器。

8.6.2 捕获页面输出

我们的主要目标是收集 HTML 和链接。链接将由策略工作器处理,以确定是否应该检索它们。我们捕获页面截图,以便我们有开发前端应用并更好地可视化检索内容的选项。

在图 8.5 中,我们在爬虫架构中展示了解析器组件。在我们的实现中,解析器作为 fetcher 的一部分实现。这既是一种简化也是一种优化。在我们的 fetcher 中,我们已经承担了加载网络浏览器并使其解析和渲染页面的开销。使用浏览器的 DOM API 查询页面并提取链接是一个非常简单的步骤。

所有浏览器交互和提取代码都被封装在 Node.js 模块browser.js中。以下列表显示了提取示例。

列表 8.5 Browser 模块加载函数

return initBrowser().then(page =>
    page.goto(url, { waitUntil: 'domcontentloaded' }).then(() =>     ❶
      Promise.all([
        page.evaluate(`
JSON.stringify(Object.values([...document.querySelectorAll("a")]     ❷
  .filter(a => a.href.startsWith('http'))
  .map(a => ({ text: a.text.trim(), href: a.href }))
  .reduce(function(acc, link) {
    const href = link.href.replace(/#.*$/, '')
    if (!acc[href]) {
        acc[href] = link
    }
    return acc
  }, {})))
`),
        page.evaluate('document.documentElement.outerHTML'),         ❸
        page.evaluate(`
function documentText(document) {                                    ❹
  if (!document || !document.body) {
    return ''
  }
  return document.body.innerText + '\\n' +
    [...document.querySelectorAll('iframe')].map(iframe => documentText(iframe.contentDocument)).join('\\n')
}
documentText(document)
`),
        page.screenshot()                                            ❺
      ]).then(([linksJson, html, text, screenshotData]) => ({
        links: JSON.parse(linksJson).reduce(
          (acc, val) =>
            acc.find(entry => entry.href === val.href) ? acc : [...acc, val],
          []
        ),
        html,
        text,
        screenshotData
      }))
    )
  )
}

❶ 加载正确的 URL 并等待文档加载。

❷ 使用 JavaScript 查询页面的文档对象模型(DOM)以提取链接。

❸ 捕获页面生成的 HTML。

❹ 从页面和任何<iframe>中获取文本。

❺ 创建页面的截图图像。

browser模块的load函数被一个 URL 调用时,它执行以下操作。

8.6.3 获取多个页面

fetch 服务的 Lambda 处理程序接受多个 URL。我们的想法是允许我们的 Lambda 函数尽可能多地加载和处理页面。我们优化这些调用,使得发送给fetch调用的所有 URL 都来自同一个种子 URL。这增加了它们具有相似内容并从浏览器中执行的缓存中受益的可能性。我们的 Lambda 函数按顺序获取 URL。这种行为很容易改变,添加对并行获取器的支持,以进一步优化过程。

页面上找到的所有链接都会发布到我们的系统事件总线。这允许任何订阅这些事件的其它服务异步地做出反应。对于我们的事件总线,我们使用 CloudWatch Events。fetch 服务以最多 10 批(CloudWatch 的限制)的形式发布发现的链接,如下面的列表所示。

列表 8.6 为发现的 URL 生成 CloudWatch 事件

const cwEvents = new AWS.CloudWatchEvents({...})
...
function dispatchUrlDiscoveredEvents(item, links) {
  if (links.length > 0) {
    if (links.length > 10) {
      return dispatchUrlDiscoveredEvents(item, links.splice(0, 10))   ❶
        then(() => dispatchUrlDiscoveredEvents(item, links))
    }

    const eventEntries = links.map(link => ({
      Detail: JSON.stringify({ item, link }),                         ❷
      Source: 'fetch-service',                                        ❸
      DetailType: 'url.discovered'                                    ❹
    }))

    return cwEvents.putEvents({ Entries: eventEntries })              ❺
      promise().then(() => {})
  }
  return Promise.resolve()
}

❶ 我们一次只能使用 CloudWatch Events API 发送 10 个事件,所以我们提取 10 个,然后递归地处理剩余的。

❷ Detail 属性是事件的 JSON 有效负载。

❸ 我们识别事件的来源。

❹ 使用事件类型在 CloudWatch 规则中匹配接收事件。

❺ 使用 AWS SDK 中的 CloudWatch Events API 发送事件批。

CloudWatch Events 作为事件总线

在第二章中,我们描述了消息传递技术和队列系统与 pub/sub 系统之间的区别。对于我们的“URL 发现”消息,我们希望有一个 pub/sub 模型。这允许多个订阅者对这类事件做出反应,并且不对他们做什么做出任何假设。这种方法有助于我们减少服务之间的耦合。

在 AWS 中,我们有几种 pub/sub 选项:

  • 简单通知服务(SNS)

  • 第七章中使用的 Kinesis Streams

  • 已使用 Kafka 的用户可用的托管流式传输 Kafka(MSK)

  • DynamoDB Streams,一个发布到 DynamoDB 数据的系统,建立在 Kinesis Streams 之上

  • CloudWatch Events,一个几乎不需要设置的简单服务

CloudWatch Events 的优势在于需要很少的设置。我们不需要声明任何主题或配置分片。我们只需使用 AWS SDK 发送事件。任何希望对这些事件做出反应的服务都需要创建一个 CloudWatch 规则来匹配传入的事件并触发目标。可能的目标包括 SQS、Kinesis,当然还有 Lambda。

对于每次成功的页面抓取,都会调用 Frontier URL 更新 API 来标记 URL 为FETCHED。任何失败的页面加载都会导致 URL 被标记为FAILED

8.6.4 部署和测试 fetcher

要将 fetcher 部署到 AWS,先在本地测试。首先,我们安装模块依赖项:

npm install

接下来,我们使用无服务器本地调用。我们的本地调用将尝试将内容复制到我们的项目存储 S3 桶中。它还将发布与抓取的页面中发现的链接相关的 CloudWatch 事件。因此,请确保您的 AWS 凭证已通过AWS_环境变量或使用 AWS 配置文件进行配置。运行invoke local命令,传递 Fetch 服务代码中提供的测试事件:

source ../.env
serverless invoke local -f fetch --path test-events/load-request.json

您应该看到 Google Chrome 运行并加载了一个网页(fourtheorem.com)。在某些平台上,即使调用完成后,调用也可能不会退出,可能需要手动终止。当调用完成后,您可以在 AWS 管理控制台中导航到项目存储的 S3 桶。在那里您将找到一个包含 HTML 文件和屏幕截图的单个文件夹。下载它们并查看您迄今为止的优秀工作成果!我们现在准备部署到 AWS:

serverless deploy

8.7 策略服务中确定爬取空间

在任何网络爬虫中确定爬取空间的过程是特定于领域和应用的。在我们的场景中,我们做出了一些假设,这将简化我们的爬取策略:

  • 爬虫只遵循本地链接。

  • 每个种子策略的爬取策略是独立的。我们不需要处理通过爬取不同种子找到的链接之间的重复内容。

  • 我们的爬取策略遵循爬取深度限制。

让我们探索爬虫服务实现。您可以在chapter8-9/strategy-service中找到代码。图 8.7 展示了该服务的物理结构。

图 8.7 策略服务实现通过 SQS 与 CloudWatch Events 相关联。它还集成了参数存储和前沿 API。

您可以看到这个服务非常简单。它处理列表 8.7 中显示的事件批次。handler.js的摘录可以在chapter8-9/strategy-service中找到。

列表 8.7 页面爬取策略

const items = event.Records.map(({ body }) => {
    const { item, link } = JSON.parse(body)                        ❶
    return {                                                       ❷
       seed: item.seed,
      referrer: item.url,
      url: link.href,
      label: link.text,
      depth: item.depth + 1
    }
  }).filter(newItem => {
    if (newItem.depth > MAX_DEPTH) {                               ❸
      log.debug(`Rejecting ${newItem.url} with depth (${newItem.depth})  beyond limit`)
    } else if (!shouldFollow(newItem.seed, newItem.url)) {         ❹
      log.debug(`Rejecting ${newItem.url}  from a different domain to seed ${newItem.seed}`)
    } else {
      return true
    }
    return false
  })
  log.debug({ items }, 'Sending new URLs to Frontier')
  return items.length > 0
    ? signedAxios({method: 'PUT', url: frontierUrl, data: items})  ❺
        .then(() => ({}))
        .catch(err => {
          const { data, status, headers } = err.response || {}
          if (status) {
            log.error({ data, status, headers }, 'Error found')
          }
          throw err
        })
    : Promise.resolve({})

❶ 事件中的每条记录都会被解析以提取链接和发现它的页面。

❷ 为新页面创建一个前沿记录。它包含引用页面的 URL、链接文本标签和增加的爬取深度。

❸ 超过最大爬取深度的项目将被排除。

❹ 来自不同域的项目将被排除。

❺ 使用 Axios HTTP 库通过 Frontier 的 Bulk Insert API 调用合格的项目。15

我们刚刚处理的事件是由 fetch 服务使用 CloudWatch Events API 发送的。要了解它们是如何被策略服务接收的,请参考图 8.7 和strategy-serviceserverless.yml摘录,如列表 8.8 所示。

列表 8.8 通过 SQS 接收到的策略服务 CloudWatch 事件

Resources:
    strategyQueue:
      Type: AWS::SQS::Queue                               ❶
       Properties:
        QueueName: ${self:custom.strategyQueueName}

    strategyQueuePolicy:
      Type: AWS::SQS::QueuePolicy
      Properties:
        Queues:
          - !Ref strategyQueue
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
            - Effect: Allow
              Action:
                - sqs:SendMessage
              Principal:
                Service: events.amazonaws.com             ❷
              Resource: !GetAtt strategyQueue.Arn

    discoveredUrlRule:
      Type: AWS::Events::Rule                             ❸
      Properties:
        EventPattern:
          detail-type:
            - url.discovered                              ❹
        Name: ${self:provider.stage}-url-discovered-rule
        Targets:
          - Arn: !GetAtt strategyQueue.Arn                ❺
            Id: ${self:provider.stage}-url-discovered-strategy-queue-target
            InputPath: '$.detail'                         ❻

❶ 我们定义了一个 SQS 队列。这是 handleDiscoveredUrls Lambda 处理器的触发器。

❷ 给定 SQS 队列的资源策略,授予 CloudWatch Events 服务向队列发送消息的权限。

❸ 定义了一个 CloudWatch 规则来匹配给定模式的事件。

❹ 规则匹配 DetailType: url-discovered 类型的事件。

❺ 将 SQS 队列指定为规则的靶标。

❻ 发送到目标的消息体是消息负载。

让我们将策略服务直接部署到 AWS:

npm install
serverless deploy

我们现在可以构建爬虫的最后一部分了。

8.8 使用调度器编排爬虫

网络爬虫的最后一个组件,调度器,是爬取网站流程开始的地方,并一直跟踪到结束。对于习惯于更大、单体架构的人来说,以无服务器的心态设计这类流程具有挑战性。特别是,对于任何给定的网站,我们需要强制执行以下要求:

  • 必须强制执行每个站点的最大并发抓取数。

  • 在执行下一批抓取之前,流程必须等待指定的时间。

这些要求与流量控制相关。使用纯事件驱动方法实现流量控制是可能的。然而,为了努力将同一 Lambda 函数内的同一站点的请求进行聚类,架构本身就已经相当复杂且难以理解。

在我们解决流量控制挑战之前,确保你有这个服务的代码准备探索。

8.8.1 获取代码

调度服务代码可以在chapter8-9/scheduler-service中找到。在serverless.yml中,你会找到一个新插件,serverless-step-functions。这引入了一个新的 AWS 服务,将帮助我们编排爬取过程。

8.8.2 使用 Step Functions

我们的调度器将使用 AWS Step Function 实现流程控制和过程编排。Step Functions 具有以下功能:

  • 它们可以运行长达一年。

  • Step Functions 与许多 AWS 服务集成,包括 Lambda。

  • 支持等待步骤、条件逻辑、并行任务执行、故障和重试。

Step Functions 使用称为 Amazon States Language (ASL) 的特定语法在 JSON 中定义。serverless-step-function 插件允许我们在 stepFunctions 部分下的 serverless 配置文件中定义我们的函数的 ASL。我们在 Serverless Framework 配置中使用 YAML。在作为底层 CloudFormation 堆栈的一部分创建资源之前,此格式将转换为 JSON。图 8.8 阐述了调度器 Step Function 的流程。

我们已经学习了如何构建其他组件服务。我们介绍了它们用于与系统其他部分交互的 API 和事件处理。由调度器管理的端到端爬取过程也已经展示。特别是,过程中的 WaitCheck Batch Count 步骤展示了如何使用 Step Function 容易地管理控制流。Step Function 状态机的 ASL 代码列表显示在列表 8.9 中。

图 8.8 调度器作为 AWS Step Function 实现。它对调度器服务内定义的 Lambda 以及抓取服务中的抓取 Lambda 进行同步调用。

列表 8.9 调度服务状态机的 ASL

StartAt: Seed URL
States:
  Seed URL:
    Type: Task
    Resource: !GetAtt PutSeedLambdaFunction.Arn                  ❶
    Next: Get URL Batch
    InputPath: '$'
    ResultPath: '$.seedResult'
    OutputPath: '$'
  Get URL Batch:
    Type: Task
    Resource: !GetAtt GetBatchLambdaFunction.Arn                 ❷
    Next: Check Batch Count
    InputPath: '$'
    ResultPath: '$.getBatchResult'
    OutputPath: '$'
  Check Batch Count:
    Type: Choice                                                 ❸
    Choices:
      - Not:
          Variable: $.getBatchResult.count
          NumericEquals: 0
        Next: Fetch
    Default: Done
  Fetch:
    Type: Task
    Resource: ${ssm:/${self:provider.stage}/fetch/lambda-arn}    ❹
    InputPath: $.getBatchResult.items
    ResultPath: $.fetchResult
    Next: Wait
  Wait:
    Type: Wait                                                   ❺
    Seconds: 30
    Next: Get URL Batch
  Done:
    Type: Pass
    End: true

❶ 状态机调用 putSeed Lambda 以启动爬取过程。

❷ 使用 getBatch Lambda 函数检索一批 URL。

❸ 在 Choice 状态中检查批次的 URL 数量。这是在 Step Function 中实现简单流程控制的示例。如果计数为零,状态机将使用完成状态终止。否则,它将前进到 Fetch 状态。

❹ 使用参数存储发现抓取服务的 Lambda,并使用来自前沿的 URL 批次进行调用。

❺ 一旦抓取完成,状态机等待 30 秒以确保礼貌的爬取行为。然后状态机返回到 Get URL Batch 状态以处理更多页面。

8.8.3 部署和测试调度器

一旦我们部署了调度器,你应该能够为任何给定的种子 URL 启动爬取过程。让我们开始吧!

npm install
serverless deploy

现在我们已经部署了所有服务以运行爬取过程。通过启动 Step Function 执行来启动爬取过程。这可以通过使用 AWS 命令行完成。首先,我们使用 list-state-machines 命令来查找 CrawlScheduler Step Function 的 ARN:

aws stepfunctions list-state-machines --output text

下面的示例显示了返回的输出:

STATEMACHINES 1561365296.434 CrawlScheduler arn:aws:states:eu-west-1:123456789123:stateMachine:CrawlScheduler

接下来,我们通过提供 ARN 并传递包含种子 URL 的 JSON 来启动状态机执行:

aws stepfunctions start-execution \
 --state-machine-arn arn:aws:states:eu-west-1:1234567890123:stateMachine:CrawlScheduler \
 --input '{"url": "https://fourtheorem.com"}'

作为使用 CLI 的替代方案,我们可以在 AWS 管理控制台中启动 Step Function 执行。在浏览器中导航到 Step Functions 服务并选择 CrawlScheduler 服务。你应该会看到类似于图 8.9 中的屏幕。

图 8.9 AWS 管理控制台中的步骤函数视图允许您启动新的执行并查看现有执行的进度。您还可以从这里检查或编辑 ASL JSON。

选择“启动执行”。从这里,您可以输入要传递给起始状态的 JSON 对象。在我们的例子中,JSON 对象需要一个属性--要爬取的网站的 URL。这如图 8.10 所示。

图片

图 8.10 通过在步骤函数控制台上的“启动执行”选项中提供网站 URL,可以启动爬取过程。

一旦执行开始,控制台将带您进入执行视图。从这里,您可以查看步骤函数执行进度的非常有用的可视化,如图 8.11 所示。通过点击任何状态,您可以看到输入数据、输出数据和任何错误。

图片

图 8.11 步骤函数控制台中的可视化工作流程允许用户监控执行的进度。

当步骤函数执行完成后,查看项目存储 S3 存储桶的内容。您应该找到与种子 URL 链接的最重要页面相关的一组文件。一个页面的内容示例如图 8.12 所示。

图片

图 8.12 可以从 S3 控制台浏览项目存储。这允许我们检查生成的 HTML 并直观地检查页面的截图。

这种类型的数据,从许多会议网站上收集而来,将构成第九章中智能数据提取的基础。然而,在继续之前,花些时间在 AWS 控制台中导航爬虫的组件。从步骤函数开始,依次跟随每个阶段。查看 fetch 服务、策略服务和前哨站的 CloudWatch 日志。数据和事件的流程应与图 8.9 中的图示相匹配,这个练习应该有助于巩固我们在本章中描述的所有内容。

在下一章中,我们将深入探讨使用命名实体识别从文本数据中提取特定信息。

摘要

  • AI 算法类型或托管服务决定了 AI 应用所需的数据类型。

  • 如果您还没有合适的数据,考虑寻找公开可用的数据源或生成自己的数据集。

  • 网络爬虫和抓取器从网站上查找和提取数据。

  • 可以使用 DynamoDB 辅助索引执行额外的查询。

  • 可以设计和构建一个用于网络爬取的无服务器应用程序,特别是针对特定的网站小集合。

  • 需要控制流的过程可以使用 AWS 步骤函数进行编排。

  • 需要控制流的过程可以使用 AWS 步骤函数进行编排。

警告第九章将继续构建这个系统,并在第九章末尾提供如何移除已部署资源的说明。如果您计划在一段时间内不处理第九章,请确保您完全移除本章中部署的所有云资源,以避免额外收费!


1.Gil Press,《清理大数据:调查称这是耗时最长、最不愉快的数据科学任务》,福布斯,2016 年 3 月 23 日,www.forbes.com/sites/gilpress/2016/03/23/data-preparation-most-time-consuming-least-enjoyable-data-science-task-survey-says.

2.史蒂夫·洛尔,《对于大数据科学家来说,“清洁工作”是洞察力的关键障碍》,纽约时报,2014 年 8 月 17 日,www.nytimes.com/2014/08/18/technology/for-big-data-scientists-hurdle-to-insights-is-janitor-work.html.

3.这对读者来说是一个有趣的挑战。AWS Personalize 服务(在撰写本文时处于开发者预览版)是一个托管机器学习推荐服务,应该适合这个应用。

4.Ribeiro, Singh 和 Guestrin, “'为什么我应该相信你?'解释任何分类器的预测”,华盛顿大学,2016 年 8 月 9 日,https://arxiv.org/pdf/1602.04938.pdf.

5.Ashley Rodriguez,“微软的 AI 千禧年聊天机器人上线不到一天就在 Twitter 上变成了种族主义者”,Quarts,2016 年 3 月 24 日,http://mng.bz/BED2.

6.Tom Simonite,《AI 和“大量数据”可能使科技巨头更难被颠覆》,Wired,2017 年 7 月 13 日,mng.bz/dwPw.

7.*《探索数据科学》,John Mount 和 Nina Zumel,Manning Publications,2016 年,www.manning.com/books/exploring-data-science.

8.有关 robots.txt 的更多信息,请参阅 http://www.robotstxt.org.

9.*《集体智慧实践》,Satnam Alag,Manning Publications,2019 年,mng.bz/xrJX.

10.术语“无差别的重负荷”的起源尚不明确。然而,亚马逊首席执行官杰夫·贝索斯在 2006 年 Web 2.0 峰会上的演讲中提到了它。他说:“在想法和成功产品之间存在着大量的无差别的重负荷,我们称之为‘垃圾’,我们认为今天创造新产品有 70%是垃圾,30%是新想法的实施。我们希望逆转这个比例。”(来源:Dave Kellogg,Web 2.0 峰会:杰夫·贝索斯,2006 年 11 月 8 日,http://mng.bz/Az2x.](http://mng.bz/Az2x)

11.亚马逊在其博客上有一个 DynamoDB 关系模型的示例:http://mng.bz/RMGv.](http://mng.bz/RMGv)

12.许多高级 DynamoDB 主题,包括关系建模,都在 Rick Houlihan 在 AWS re:Invent 2018 上的演讲中有所涉及,“Amazon DynamoDB 深入探讨:DynamoDB 的高级设计模式 (DAT401),”视频链接:https://www.youtube.com/watch?v=HaEPXoXVf2k.

13.github.com/eoinsha/lambda-logger-middleware.

14.www.npmjs.com/package/middy-autoproxyresponse.

15.github.com/axios/axios.

9 使用 AI 从大型数据集中提取价值

本章涵盖了

  • 使用 Amazon Comprehend 进行命名实体识别(NER)

  • 理解 Comprehend 的操作模式(异步、批量、同步)

  • 使用异步 Comprehend 服务

  • 使用 S3 通知触发 Lambda 函数

  • 使用死信队列处理 Lambda 中的错误

  • 处理 Comprehend 的结果

第八章处理了从网站收集非结构化数据用于机器学习分析的问题。本章在第八章的无服务器网页爬虫基础上进行构建。这次,我们关注的是使用机器学习从我们收集的数据中提取有意义的见解。如果你没有完成第八章的内容,你应该现在回去完成它,然后再继续本章,因为我们将在网页爬虫的基础上直接构建。如果你已经熟悉了这些内容,我们可以直接进入并添加信息提取部分。

9.1 使用 AI 从网页中提取重要信息

让我们回顾一下第八章场景的宏伟愿景——寻找要参加的相关开发者会议。我们希望建立一个系统,让人们能够搜索他们感兴趣的和会议演讲者。在第八章的网页爬虫中,我们构建了一个系统,解决了这个场景的第一部分——收集会议数据。

然而,我们不想让用户手动搜索我们收集的所有非结构化网站文本。相反,我们希望向他们展示会议、事件地点和日期以及可能在这些会议上发表演讲的人名单。

从非结构化文本中提取这些有意义的资料是一个非同小可的问题——至少,在最近管理人工智能服务进步之前是这样的。

让我们再次回顾第八章场景的需求概述图。这次,我们突出显示本章的相关部分。

图片

图 9.1 本章处理从我们已收集的数据中提取事件和演讲者信息。

9.1.1 理解问题

从非结构化文本中提取重要信息的问题被称为 命名实体识别 (NER)。一个 命名实体 可以是人、地点或组织。它也可以指日期和数值。NER 是一个具有挑战性的问题,也是许多研究的话题。这绝对不是一个完全解决的问题。由于其结果不能保证达到 100% 的准确性,我们必须考虑这一点。根据应用情况,可能需要人工结果检查。例如,假设你有一个需要检测文本中位置的系统。现在假设文本中的一句话提到了阿波罗 11 号指令舱,“哥伦比亚”。NER 可能将其识别为地点而不是航天器的一部分!每个命名实体识别系统都会为每个识别结果提供一个可能性分数,而这个值永远不会达到 100%。

对于我们的会议事件信息提取场景,我们的目标是从网站数据中提取人名、地点和日期。然后这些信息将被存储并供用户访问。

9.1.2 扩展架构

我们即将设计和部署一个无服务器系统,从会议网页中提取所需信息。让我们看看本章的架构组件,使用第一章中提到的标准无服务器架构中的类别。这如图 9.2 所示。

图 9.2 服务器端无实体提取系统架构。系统由使用步骤函数编排的同步 Lambda 函数组成。数据存储在第八章中介绍的项存储 S3 存储桶中。S3 的存储桶通知触发我们的异步服务。

与前几章相比,服务和通信渠道的种类较少。从 AWS 服务角度来看,本章将相对简单。新引入的方面包括 Amazon Comprehend 功能和 S3 事件通知作为数据处理触发器。

9.2 理解 Comprehend 的实体识别 API

Amazon Comprehend 支持多种实体识别接口。在我们进一步详细说明数据如何通过系统流动之前,让我们花些时间来了解 Comprehend 的工作原理以及它可能对我们架构产生的影响。

Amazon Comprehend 中的三个实体识别接口在表 9.1 中概述。

表 9.1 Amazon Comprehend 操作模式

API 描述 限制
按需实体识别 分析单个文本片段。结果同步返回。 仅支持最多 5,000 个字符。
批量实体识别 分析多个文本片段。结果同步返回。 每个文档最多 25 个,每个文档最多 5,000 个字符。
异步实体识别 多个大型文本被分析。文本从 S3 读取,结果异步写入 S3。 每秒仅一个请求,每份文档 100 KB,所有文档最大 5 GB。

有关 Comprehend 限制的完整详细信息,请参阅亚马逊 Comprehend 的 指南和限制 文档。1

为了我们的目的,我们希望分析超过 5,000 个字符的文档,因此我们必须选择异步操作模式。此模式要求我们使用两个 API:StartEntitiesDetectionJob 用于启动分析,以及 DescribeEntitiesDetectionJob 如果我们希望轮询作业状态。

Comprehend 以数组的形式返回实体识别结果。每个数组元素包含以下属性:

Type--已识别的实体类型:PERSONLOCATIONORGANIZATIONCOMMERCIAL_ITEMEVENTDATEQUANTITYTITLEOTHER

  • Score--分析结果中的置信度分数。这是一个介于 01 之间的值。

  • Text--已识别的实体文本。

  • BeginOffset--实体在文本中的起始偏移量。

  • EndOffset--实体在文本中的结束偏移量。

为了了解 Comprehend 的工作方式,让我们使用 AWS 命令行界面运行一次性的测试。使用 shell 是熟悉任何新 AWS 服务的一种有用方式。

提示:第二章和附录 A 介绍了 AWS CLI。除了正常的 AWS CLI 外,亚马逊还发布了一个名为 AWS Shell 的交互式版本(github.com/awslabs/aws-shell)。它支持交互式帮助和命令自动完成。如果您使用 AWS CLI 来学习和探索新服务,查看 AWS Shell 是值得的。

我们将分析代码仓库中 chapter8-9/sample-text/apollo.txt 下的样本文本。文本段落取自维基百科上的 阿波罗 11 号 页面。2 样本文本将在下一列表中展示。

列表 9.1 实体识别样本文本:apollo.txt

Apollo 11 was the spaceflight that first landed humans on the Moon. 
Commander Neil Armstrong and lunar module pilot Buzz Aldrin formed the 
American crew that landed the Apollo Lunar Module Eagle on July 20, 1969, 
at 20:17 UTC. Armstrong became the first person to step onto the lunar 
surface six hours and 39 minutes later on July 21 at 02:56 UTC; Aldrin 
joined him 19 minutes later. They spent about two and a quarter hours 
together outside the spacecraft, and they collected 47.5 pounds (21.5 kg) 
of lunar material to bring back to Earth. Command module pilot Michael 
Collins flew the command module Columbia alone in lunar orbit while they 
were on the Moon's surface. Armstrong and Aldrin spent 21 hours 31 minutes 
on the lunar surface at a site they named Tranquility Base before lifting 
off to rejoin Columbia in lunar orbit.

我们可以使用以下命令在 CLI 上运行按需实体识别:

export INPUT_TEXT=`cat apollo.txt`

aws comprehend detect-entities --language-code=en --text $INPUT_TEXT > results.json

此命令的输出,保存到 results.json,表明 Comprehend 如何为实体识别任务提供分析结果。表 9.2 以表格格式显示了此命令的一些结果。

表 9.2 Comprehend 实体识别样本结果

类型 文本 分数 起始偏移量 结束偏移量
ORGANIZATION 阿波罗 11 号 0.49757930636405900 0 9
LOCATION 月球 0.9277622103691100 62 66
PERSON 尼尔·阿姆斯特朗 0.9994082450866700 78 92
PERSON 巴兹·奥尔德林 0.9906044602394100 116 127
OTHER 美国 0.6279735565185550 139 147
ORGANIZATION 阿波罗 0.23635128140449500 169 175
COMMERCIAL_ITEM 月球模块鹰 0.7624998688697820 176 194
DATE "1969 年 7 月 20 日" 0.9936476945877080 198 211
QUANTITY 第一人称 0.8917713761329650 248 260
QUANTITY 大约两个半小时 0.9333438873291020 395 424
QUANTITY 21.5 公斤 0.995818555355072 490 497
LOCATION 地球 0.9848601222038270 534 539
PERSON 迈克尔·柯林斯 0.9996771812438970 562 577
LOCATION 哥伦比亚 0.9617793560028080 602 610

很明显,通过 Comprehend 实体识别可以以非常小的努力获得非常准确的结果。

为了从会议网站爬取的每个网页中获得这些结果,我们将使用异步实体识别 API。这意味着我们必须处理以下 API 的特性:

  • 在 Comprehend 上进行的实体识别作业在异步模式下运行时间较长。每个作业可能需要 5 到 10 分钟。这比同步作业要长得多,但权衡是异步作业可以处理更大的文档。

  • 为了避免触碰到 API 的限速限制,我们将避免每秒发送超过一个请求,并将多个网页提交给每个任务。

  • 亚马逊 Comprehend 中的异步 API 将结果写入配置的 S3 存储桶。我们将通过 S3 存储桶上的通知触发器来处理结果。

第八章中的网络爬虫为每个网页在 S3 存储桶中写了一个文本文件(page.txt)。为了开始实体识别,我们将在 S3 的单独的临时文件夹中创建这个文件的副本。这样,我们可以检查临时文件夹中的新文本文件以供处理。当处理开始时,我们将从临时区域删除文件。原始文件(page.txt)将永久保留在sites文件夹中,因此如果以后需要,它仍然可用于进一步处理。

让我们继续实现一个简单的服务,该服务将在临时区域创建文本文件的副本。

9.3 准备信息提取数据

包含准备处理文件的临时区域将是项目存储 S3 存储桶中的一个目录,称为incoming-texts。我们将使用 S3 通知触发器来响应从网络爬虫到达存储桶中的新page.txt文件。然后,每个文件将被复制到incoming-texts/

9.3.1 获取代码

准备服务的代码位于chapter8-9/preparation-service目录中。此目录包含一个serverless.yml文件。在部署和测试准备服务之前,我们将逐步解释其内容。

9.3.2 创建 S3 事件通知

准备服务主要由一个具有事件通知的简单函数组成。让我们详细探索serverless.yml,看看它是如何工作的。接下来的列表显示了该文件的摘录,其中我们遇到了第一个 S3 存储桶事件通知。

列表 9.2 准备服务 serverless.yml 文件摘录

service: preparation-service
frameworkVersion: '>=1.47.0'                       ❶

plugins:
  - serverless-prune-plugin                        ❷
  - serverless-pseudo-parameters                   ❸
  - serverless-dotenv-plugin                       ❹
...

provider:
  ...
  iamRoleStatements:
    - Effect: Allow
      Action:
        - s3:GetObject
        - s3:PutObject
        - s3:ListBucket                             ❺

      Resource:
        - arn:aws:s3:::${env:ITEM_STORE_BUCKET}/*
...
functions:
  prepare:
    handler: handler.prepare                        ❻
    events:
      - s3:
          bucket: ${env:ITEM_STORE_BUCKET}
          event: s3:ObjectCreated:*
          rules:
            - suffix: page.txt                      ❼
          existing: true                            ❽

❶ 要使用 S3 现有的存储桶作为事件触发器,您必须使用 Serverless Framework 1.47.0 或更高版本。3 这一行强制执行了该要求。

❷ 每次我们部署时,都会为每个 Lambda 函数创建一个新的版本。serverless-prune-plugin 负责在版本积累时移除旧的 Lambda 函数版本。4

❸ 我们希望在配置中使用带有伪参数(如${AWS::AccountId})的 CloudFormation 子函数,5,但此语法与 Serverless Framework 的变量语法冲突。6 serverless-pseudo-parameters7 通过允许我们使用更简单的语法(#{AWS::AccountId})来解决此问题。

❹ 正如前几章所述,我们使用 serverless-dotenv-plugin8 从.env 文件中加载环境变量。

❺ 我们为函数授予读取和写入项目存储桶的权限。

❻ S3 事件处理函数在 handler.js 中定义。函数名称被导出为 prepare。

❼ Lambda 触发器被定义为 S3 通知。通知将匹配在桶中创建的任何带有 page.txt 后缀的对象(文件)。

❽ 这确保了 Serverless Framework 不会尝试创建桶。相反,它将在现有的项目存储桶上创建一个通知触发器。

我们刚刚声明了函数、其资源以及准备处理器的触发器。现在我们可以继续实现这个函数。

CloudFormation 和 S3 通知触发器

CloudFormation 是一种定义基础设施即代码的绝佳方式,它支持逻辑上分组资源,并在任何失败事件发生时提供回滚功能。然而,一个缺点是,与 AWS SDK 创建所有资源类型相比,CloudFormation 不够灵活。

这的一个例子是与桶通知一起使用。使用 CloudFormation,只有在创建桶资源时才能添加通知。9 我们更希望能够在我们的系统中为任何服务添加现有桶的通知。

Serverless Framework 为这个问题提供了一个很好的解决方案。通过使用具有属性existing: trues3事件类型,框架在底层使用 AWS SDK 向现有桶添加一个新的通知。这是通过使用CloudFormation 自定义资源实现的,这是一种有用的解决方案,当官方 CloudFormation 支持不足以满足您的需求时。有关自定义资源的更多信息,请参阅 AWS 文档。10

9.3.3 实现准备处理程序

准备服务handler模块的目标是执行任何必要的处理,以便文本准备好进行实体识别。在我们的案例中,这仅仅是把文本放入正确的文件夹,并使用正确的文件名进行处理。准备服务的handler模块如下所示。

列表 9.3 准备服务handler.js摘录

...
const s3 = new AWS.S3({ endpoint: process.env.S3_ENDPOINT_URL })

function prepare(event) {
  const record = event.Records[0]                                 ❶
  const bucketName = record.s3.bucket.name
  const key = decodeURIComponent(record.s3.object.key)            ❷
  const object = { Bucket: bucketName, Key: key }
  ...
  return s3
    .getObject(object)
    .promise()
    .then(({ Body: body }) => body)
    .then(body => body.toString())
    .then(text => {
      const textObjectKey = `incoming-texts/${key.substring(KEY_PREFIX.length).replace(/page.txt$/, 'pending.txt')}`     ❸
      ...
      return s3                                                   ❹
        .putObject({ Body: text, Bucket: bucketName, Key: textObjectKey })
        .promise()
    })
}

❶ 每个 S3 通知事件都是一个长度为 1 的数组。

❷ 当对象键到达 S3 事件时,它们会被 URL 编码。

❸ 阶段区域副本的键是通过替换传入键字符串中的前缀和文件名来创建的。

❹ 将 S3 对象的内写入目标键。

9.3.4 使用死信队列(DLQ)增加弹性

在我们部署准备服务之前,让我们处理弹性和重试的问题。如果我们的事件处理程序无法处理事件,我们可能会丢失事件。Lambda 将重试我们的函数两次。11 如果我们的函数在任何这些调用尝试中未能成功处理事件,将不会有进一步的自动重试。

幸运的是,我们可以为任何 Lambda 函数配置一个死信队列(DLQ)。在自动重试失败后,未处理的事件将进入这里。一旦它们在 DLQ 中,就取决于我们决定如何重新处理它们。

DLQ 可能是一个 SQS 队列或 SNS 主题。SNS(简单通知服务)用于 pub/sub 消息,这是第二章中介绍的内容。SQS(简单队列服务)用于点对点消息。我们将使用 SQS 队列作为我们的 DLQ,因为我们只需要一个消费者。DLQ 交互如图 9.3 所示。

图 9.3 一个 DLQ 有助于检查和重新处理导致 Lambda 执行失败的的事件。

这是我们处理未处理消息的方式:

  • 我们为prepare Lambda 函数设置了一个 SQS 队列作为 DLQ。

  • 在所有重试尝试失败后,未处理的消息将被发送到我们的队列。

  • 我们可以间歇性地检查 AWS 控制台中的 SQS 队列。在生产场景中,我们理想情况下会设置一个 CloudWatch 警报,在队列中的消息数量超过零时提醒我们。为了简化,我们不会在本章中创建 CloudWatch 警报。12

  • 我们将创建第二个 Lambda 函数,其唯一目的是从 DLQ 检索消息并将它们传递回原始的prepare Lambda 函数。当我们注意到未处理的消息并已采取措施解决根本问题时,可以手动调用此函数。

9.3.5 创建 DLQ 和重试处理程序

在第二章和第三章中,我们使用 SQS 队列来触发 Lambda 函数。在 DLQ 的情况下,我们不希望我们的重试 Lambda 函数自动触发。由于我们将手动调用重试 Lambda,重试处理程序必须手动从 SQS 队列中读取消息。让我们看看serverless.yml的添加部分。以下列表显示了相关摘录。您可以在chapter8-9/preparation-service中找到完整的配置文件。

列表 9.4 准备服务serverless.yml DLQ 摘录

custom:
  ...
  dlqQueueName: ${self:provider.stage}PreparationDlq     ❶
  ...
provider:
  ...
  iamRoleStatements:
  ...
    - Effect: Allow
      Action:
        - sqs:GetQueueUrl                                ❷
        - sqs:DeleteMessage
        - sqs:SendMessage
        - sqs:ReceiveMessage
      Resource:
        - !GetAtt preparationDlq.Arn
functions:
  prepare:
    ...
    onError: !GetAtt preparationDlq.Arn                  ❸

  ...
  retryDlq:
    handler: dlq-handler.retry                           ❹
    environment:
      DLQ_QUEUE_NAME: ${self:custom.dlqQueueName}
...
resources:
  Resources:
    preparationDlq:
      Type: AWS::SQS::Queue
      Properties:
        QueueName: ${self:custom.dlqQueueName}
        MessageRetentionPeriod:                          ❺

❶ DLQ 队列名称对于每个部署阶段都不同,以避免命名冲突。

❷ Lambda 需要四个权限来读取和处理 DLQ 中的消息

❸ 使用onError将准备 Lambda 函数的 DLQ 设置为 SQS 队列的 ARN。

❹ 重试 Lambda 函数配置时没有事件触发器。DLQ 队列使用环境变量配置。

❺ 我们将 DLQ 的消息保留期设置为一天。这应该足够长,以便可以手动恢复未投递的消息。最大消息保留值是 14 天。

Lambda 处理器在dlq-handler.js文件中的retry函数中实现。当被调用时,其目标是执行以下一系列操作:

  1. 从 DLQ 中检索一批消息。

  2. 对于每条消息,从消息中提取原始事件。

  3. 通过加载handler模块并直接使用事件调用prepare函数来调用prepare函数,并等待成功或失败响应。

  4. 如果事件已成功,则从 DLQ 中删除该消息。

  5. 继续处理下一条消息,直到批次中的所有消息都已被处理。

DLQ 处理是我们希望应用于多个 Lambda 函数的常见模式,因此我们将其提取到一个独立的、开源的 NPM 模块中,即lambda-dlq-retry。13 使用此模块简化了重试实现。让我们看看下面的dlq-handler.js,如下所示。

列表 9.5 准备服务 DLQ 处理器

const lambdaDlqRetry = require('lambda-dlq-retry')          ❶
const handler = require('./handler')                        ❷
const log = require('./log')

module.exports = {
  retry: lambdaDlqRetry({ handler: handler.prepare, log })  ❸
}

❶ 导入 lambda-dlq-retry 模块。

❷ 需要包含准备服务的准备函数的模块。

❸ 我们导出 DLQ 重试处理器,这是 lambda-dlq-retry 模块为我们创建的,使用指定的处理器。您可以传递一个日志记录实例。如果启用了调试日志记录,这将产生与 DLQ 重试相关的日志条目。

值得注意的是,lambda-dlq-retry以最多 10 条消息的批次处理消息。这可以通过在环境变量中设置一个替代值来配置,即DLQ_RETRY_MAX_MESSAGES

9.3.6 部署和测试准备服务

到目前为止,我们已经在本章中创建了四个 Lambda 函数。在部署和运行它们之前回顾这些函数是很有价值的,这样我们就可以开始清楚地了解它们是如何协同工作的。图 9.4 回顾了本章开头我们的服务架构。我们已经涵盖的部分被突出显示。

在我们部署准备服务之前,请确保您已在chapter8-9目录中设置了.env,如第八章所述。这包含项目存储桶名称环境变量。一旦完成,我们就可以继续进行构建和部署的常规步骤!

npm install
sls deploy

图 9.4 到目前为止,我们已经实现了用于文本准备、获取一批文本文件、启动实体识别和检查识别进度的 Lambda 函数。

为了测试我们的函数,我们可以手动将文件上传到带有后缀page.txt的项目存储桶中。然后我们可以检查它是否已复制到incoming-texts的预演区域。我们可以使用我们从简单的 Comprehend 测试中已有的示例文本:

source ../.env
aws s3 cp ../sample-text/apollo.txt \
  s3://${ITEM_STORE_BUCKET}/sites/test/page.txt

要检查 prepare 函数的日志,我们可以使用 Serverless 的 logs 命令。这将打印函数的 CloudWatch 日志到控制台。由于我们在第八章中使用了 pino 模块进行日志记录,我们可以通过将输出管道传输到 pino-pretty 模块来格式化它们,以便于阅读输出:

npm install -g pino-pretty

sls logs -f prepare | pino-pretty

您应该看到类似于以下列表的输出。

列表 9.6 准备服务日志输出

START RequestId: 259082aa-27ec-421f-9caf-9f89042aceef Version: $LATEST
[1566803687880] INFO (preparation-service/1 on 169.254.238.253): Getting S3 Object
    object: {
      "Bucket": "item-store-bucket",
      "Key": "sites/test/page.txt"
    }
[1566803687922] INFO (preparation-service/1 on 169.254.238.253): Uploading extracted text
    bucketName: "item-store-bucket"
    key: "sites/test/page.txt"
    textObjectKey: "incoming-texts/test/pending.txt"

然后,您可以检查 S3 桶中暂存区域中文件的包含内容:

aws s3 ls s3://${ITEM_STORE_BUCKET}/incoming-texts/test/pending.txt

最后,我们将测试 DLQ 重试功能。如果没有经过测试和验证以确保其工作,那么拥有一个处理失败恢复的过程是没有意义的!为了模拟错误,我们将撤销对 S3 桶的读取权限。在 serverless.yml 中注释掉 Lambda IAM 角色策略中的 GetObject 权限,如下所示:

...
    - Effect: Allow
      Action:
#        - s3:GetObject
        - s3:PutObject
...

使用修改后的 IAM 角色部署更新的准备服务:

sls deploy

我们可以使用不同的 S3 密钥(路径)再次运行相同的测试:

aws s3 cp ../sample-text/apollo.txt s3://${ITEM_STORE_BUCKET}/sites/test2/page.txt

这次,我们应该在 prepare 函数日志中观察到错误:

START RequestId: dfb09e2a-5db5-4510-8992-7908d1ac5f13 Version: $LATEST
...
[1566805278499] INFO (preparation-service/1 on 169.254.13.17): Getting S3 Object
    object: {
      "Bucket": "item-store-bucket",
      "Key": "sites/test2/page.txt"
    }
[1566805278552] ERROR (preparation-service/1 on 169.254.13.17): Error in handler
    err: {
      "type": "Error",
      "message": "Access Denied",

您将看到此错误出现两次:一次在一分钟后,再次在额外两分钟后。这是因为 AWS Lambda 自动重试。在三次尝试失败后,您应该看到消息到达 DLQ。

我们将使用 AWS 控制台在尝试重发之前检查错误:

  1. 浏览到 SQS 控制台,并从队列列表中选择准备服务 DLQ。您会注意到消息计数设置为 1。

  2. 在列表中的队列上右键单击,并选择查看/删除消息选项。选择开始轮询消息,然后在我们未投递的 S3 事件消息出现后选择立即停止。

  3. 要查看完整消息,请选择更多详情。我们现在看到了导致 prepare Lambda 函数出现错误的完整 S3 事件文本。

  4. 这是解决原始消息问题的宝贵信息。通过选择第二个标签页,消息属性,我们还可以看到错误消息以及请求 ID。此 ID 与 Lambda 函数调用匹配,可以用来将错误关联回 CloudWatch 中的日志。您可能会注意到“错误代码”在这里显示为 200。此值可以忽略,因为它总是设置为 200 用于 DLQ 消息。

接下来,通过在 serverless.yml 中恢复正确的权限来测试重发。取消注释 s3:GetObject 行,并使用 sls deploy 重新部署。我们可以选择通过 AWS 控制台、AWS CLI 或使用 Serverless Framework 的 invoke 命令来触发重试 Lambda。以下命令使用 AWS CLI:

aws lambda invoke --function-name preparation-service-dev-retryDlq /tmp/dlq-retry-output

如果您运行此命令并检查 /tmp/dlq-retry-output 中的输出,您应该看到一个简单的 JSON 对象({"count": 1})。这意味着一条消息已被处理并投递!我们可以像之前一样使用 sls logs 命令检查重试 Lambda 的输出:

sls logs -f retryDlq | pino-pretty

这将表明这次 S3 事件已成功处理。

9.4.1 使用文本批次管理吞吐量

现在我们有一个单独的临时区域,以及一个准备服务,用于在网页爬虫创建文件时填充它。我们还决定使用异步 Comprehend API 并批量处理文本。我们的下一步是创建一个简单的 Lambda 函数,用于检索要处理的文本文件批次。

9.4.1 获取代码

getTextBatch函数可以在extraction-servicehandler模块中找到。提取服务包括本章其余的功能,因为它处理提取和提取结果的报告:

cd ../extraction-service

9.4.2 获取用于提取的文本批次

getTextBatch函数的源代码如下所示。此函数使用 S3 的listObjectsV2 API 读取临时区域(incoming-texts)中的文件,直到达到指定的限制。

列表 9.7 getTextBatch函数

const MAX_BATCH_SIZE = 25
const INCOMING_TEXTS_PREFIX = 'incoming-texts/'
...

function getTextBatch() {
  ...
  return s3
    .listObjectsV2({
      Bucket: itemStoreBucketName,
      Prefix: INCOMING_TEXTS_PREFIX,                                       ❶
      MaxKeys: MAX_BATCH_SIZE
    })
    .promise()
    .then(({ Contents: items }) =>
      items.map(item => item.Key.substring(INCOMING_TEXTS_PREFIX.length))  ❷
    )
    .then(paths => {
      log.info({ paths }, 'Text batch')
      return {                                                             ❸
        paths,
        count: paths.length
      }
    })
}

❶ 从临时区域(incoming-texts)读取最多 25 个键。

❷ 修改文件名,以从批处理结果中移除 incoming-texts/前缀。

❸ 返回转换后的文件名批次,并附带一个表示批次大小的计数。

我们将等待完全部署提取服务,所以让我们使用sls invoke local命令进行测试。请注意,尽管我们是在本地执行函数,但它正在调用 S3。因此,你的AWS_环境变量应该在这里设置,以确保你有权执行这些 SDK 调用。

我们如下本地运行该函数:

sls invoke local -f getTextBatch

你应该看到类似于以下列表的输出。

列表 9.8 getTextBatch的示例输出

{
    "paths": [
        "test/pending.txt",
        "test2/pending.txt"
    ],
    "count": 2
}

9.5 异步命名实体抽象

我们已经有一种方法可以从会议网页中获取一批文本。现在,让我们构建一个函数,用于从一组文本文件中启动实体识别。记住,我们在 Comprehend 中使用异步实体识别。使用这种方法,输入文件存储在 S3 中。我们可以轮询 Comprehend 以检查识别作业的状态,并将结果写入 S3 桶中指定的路径。

9.5.1 获取代码

提取服务的代码位于chapter8-9/extraction-service目录中。我们的startBatchProcessingcheckActiveJobs函数可以在handler.js中找到。

9.5.2 启动实体识别作业

AWS Comprehend SDK 为我们提供了startEntitiesDetectionJob函数。14 它要求我们指定一个输入路径,用于处理 S3 中所有文本文件。我们希望确保没有文本文件被遗漏处理。为了实现这一点,我们将要处理的文件复制到批处理目录中,并且只有在startEntitiesDetectionJob调用成功后才会删除源文件。

这可以在提取服务的handler.js中的startBatchProcessing Lambda 函数中看到,如下面的列表所示。

列表 9.9 提取服务处理程序startBatchProcessing函数

function startBatchProcessing(event) {
  const { paths } = event                                             ❶
  const batchId = new Date().toISOString().replace(/[⁰-9]/g, '')     ❷

  return (
    Promise.all(                                                      ❸
      paths
        .map(path => ({
          Bucket: itemStoreBucketName,
          CopySource: encodeURIComponent(
            `${itemStoreBucketName}/${INCOMING_TEXTS_PREFIX}${path}`  ❹
          ),
          Key: `${BATCHES_PREFIX}${batchId}/${path}`
        }))
        .map(copyParams => s3.copyObject(copyParams).promise())
    )
      // Start Processing
      .then(() => startEntityRecognition(batchId))                    ❺
      // Delete the original files so they won't be reprocessed
      .then(() =>
        Promise.all(                                                  ❻
          paths.map(path =>
            s3
              .deleteObject({
                Bucket: itemStoreBucketName,
                Key: `${INCOMING_TEXTS_PREFIX}${path}`
              })
              .promise()
          )
        )
      )
      .then(() => log.info({ paths }, 'Batch process started'))
      .then(() => ({ batchId }))
  )
}

❶ 事件传递一个路径数组。这些路径相对于 incoming_texts 前缀。这些路径集构成了批处理。

❷ 我们基于当前时间生成一个批处理 ID。这用于在 S3 中创建批处理目录。

❸ 在处理之前,批处理中的所有文件都复制到批处理目录中。

❹ S3 copyObject API 需要 URL 编码的 CopySource 属性。

❺ 我们将批处理 ID 传递给我们的 startEntityRecognition 函数,以便批处理中的所有文件可以一起分析。

❻ 批处理识别开始后,我们继续删除 incoming_texts 目录中的所有输入路径。

现在我们可以看到,通过将文件复制到批处理目录中,我们确保incoming_texts中的每个文件都将被处理。启动批处理识别作业时出现的任何错误都会使文件留在incoming_texts中,以便可以使用后续的批处理重新处理。

我们刚刚看到了对startEntityRecognition函数的引用。这个函数负责为 Comprehend 的startEntitiesDetectionJob API 创建参数。列表 9.10 显示了该函数的代码。

列表 9.10 提取服务startBatchProcessing Lambda 函数

function startEntityRecognition(batchId) {
  return comprehend
    .startEntitiesDetectionJob({
      JobName: batchId,                                                    ❶
      DataAccessRoleArn: dataAccessRoleArn,                                ❷
      InputDataConfig: {
        InputFormat: 'ONE_DOC_PER_FILE',                                   ❸
        S3Uri: `s3://${itemStoreBucketName}/${BATCHES_PREFIX}${batchId}/`  ❹
      },
      LanguageCode: 'en',
      OutputDataConfig: {
        S3Uri: `s3://${itemStoreBucketName}/${ENTITY_RESULTS_PREFIX}${batchId}`                                   ❺
      }
    })
    .promise()
    .then(comprehendResponse =>
      log.info({ batchId, comprehendResponse }, 'Entity detection started')
    )
}

❶ 为了便于手动故障排除,我们使用生成的批处理 ID 作为作业名称。

❷ 作业需要一个具有读取和写入 S3 存储桶权限的 IAM 角色。角色定义可以在 extraction-service/serverless.yml 中找到。

❸ 我们需要告诉 Comprehend,S3 文件夹中的每个文件代表一个单独的文档。另一种选择是 ONE_DOC_PER_LINE。

❹ 批处理中文件的路径是我们刚刚复制文件的路径。

❺ 将 Comprehend 结果写入由批处理 ID 指定的输出文件夹。

startBatchProcessing函数是本章功能的核心。它将提取的文本传递到 AWS Comprehend,这是一个托管 AI 服务,负责提取重要数据。

9.6 检查实体识别进度

在我们尝试我们的实体识别作业处理之前,我们将查看checkActiveJobs。这是一个简单的 Lambda 函数,它将使用 Comprehend API 来报告正在进行的作业的状态。对于手动进度检查,您还可以查看 AWS 管理控制台的 Comprehend 部分。当我们知道有多少作业正在进行时,我们可以知道何时开始更多作业并控制并发 Comprehend 作业执行的数量。checkActiveJobs的代码在下一列表中显示。

列表 9.11 提取服务checkActiveJobs Lambda 函数

function checkActiveJobs() {
  return comprehend
    .listEntitiesDetectionJobs({                                      ❶
      Filter: { JobStatus: 'IN_PROGRESS' },
      MaxResults: MAX_COMPREHEND_JOB_COUNT
    })
    .promise()
    .then(({ EntitiesDetectionJobPropertiesList: jobList }) => {
      log.debug({ jobList }, 'Entity detection job list retrieved ')
      return {                                                        ❷
        count: jobList.length,
        jobs: jobList.map(
          ({ JobId: jobId, JobName: jobName, SubmitTime: submitTime }) => ({
            jobId,
            jobName,
            submitTime
          })
        )
      }
    })
}

❶ 调用 listEntitiesDetectionJobs API,根据正在进行的作业进行过滤。为了限制可能返回的数量,我们将结果数量限制在最大值。我们选择了 10 作为这个值。

❷ 结果被转换,以给我们一个包含正在进行作业总数(不超过我们的最大作业计数值 10)以及每个作业摘要的输出。

现在我们有三个 Lambda 函数可以一起使用,以对文件批次执行实体识别:

  1. 使用 getTextBatch 选择要处理的有限数量的文件。

  2. 使用 startBatchProcessing 启动文件批次的实体识别执行。

  3. checkActiveJobs 用于报告正在进行中的识别作业数量。这将在我们整合所有实体提取逻辑时非常有用。

我们已经使用 sls invoke local 测试了 getTextBatch。接下来,我们将部署提取服务并开始处理一批样本文本文件,以了解这些函数在实际应用中的配合情况。

9.7 部署和测试批量实体识别

为了测试我们的函数,我们首先部署提取服务。这与其他所有 Serverless Framework 部署的方式相同:

cd extraction-service
npm install
sls deploy

我们现在可以使用 Serverless Framework CLI 来调用我们的远程函数。我们将传递一个简单的 JSON 编码的路径数组给 startBatchProcessing Lambda 函数。在这个例子中,我们将使用 incoming-texts S3 目录中已有的两个文件。这些文件包含阿波罗 11 的样本文本。稍后,我们将对真实的会议网页数据进行实体识别!

sls invoke -f startBatchProcessing --data \
  "{\"paths\":[\"test/pending.txt\", \"test2/pending.txt\"]}"

如果执行成功,你应该会看到以下类似的输出——一个包含批次 ID 的 JSON 对象:

{
    "batchId": "20190829113049287"
}

接下来,我们将运行 checkActiveJobs 来报告正在进行的 Comprehend 作业数量。

列表 9.12 checkActiveJobs 输出

{
    "count": 1,                                           ❶
    "jobs": [
        {
            "jobId": "acf2faa221ee1ce52c3881e4991f9fce",  ❷
            "jobName": "20190829113049287",               ❸
            "submitTime": "2019-08-29T11:30:49.517Z"
        }
    ]
}

❶ 正在进行的作业总数

❷ 作业 ID 由 Comprehend 生成。

❸ 作业名称与我们所生成的批次 ID 匹配。

经过 5-10 分钟后,再次运行 checkActiveJobs 将报告零个正在进行的作业。此时,你可以检查作业的输出。

extraction-service 目录包含一个 shell 脚本,可以方便地查找、提取和输出批量作业的结果。要运行它,请执行以下命令:

./scripts/get-batch_results.sh <BATCH_ID>

<BATCH_ID> 占位符可以被替换为在执行 startBatchProcessing 时看到的批次 ID 值。运行此脚本将打印出代表每个样本文本的 Comprehend 实体识别结果的 JSON。在我们目前的例子中,批次中的两个文件都包含关于阿波罗 11 的相同样本文本。

9.8 持久化识别结果

我们已经看到如何从命令行手动运行实体提取功能并验证命名实体识别(NER)的输出。对于我们的端到端应用,用于会议网站爬取和分析,我们希望持久化我们的实体提取结果。这样,我们可以使用 API 为寻找会议的观众提供提取的人名、地点和日期!

实体结果处理将由 Comprehend 结果到达我们在启动实体识别作业时配置的输出文件夹驱动。就像准备服务一样,我们将使用 S3 桶通知。你将在提取服务的serverless.yml中找到processEntityResults函数的配置。相关部分在下一列表中重现。

列表 9.13 processEntityResultsserverless.yml提取

processEntityResults:
    handler: handler.processEntityResults
    events:
      - s3:
          bucket: ${env:ITEM_STORE_BUCKET}     ❶
          event: s3:ObjectCreated:*
          rules:
            - prefix: entity-results/          ❷
            - suffix: /output.tar.gz           ❸
          existing: true

❶ 通知配置与准备服务 S3 桶通知位于同一桶中。这次,键后缀/前缀不同。

❷ 所有 Comprehend 结果都持久化到实体结果中,正如我们在启动实体检测作业时指定的。

❸ Comprehend 还写入其他临时文件。我们只对存储在 output.tar.gz 中的最终结果感兴趣。

当结果到达时,我们将使用我们的通知 Lambda 函数提取结果并将它们持久化到前沿服务。由于前沿服务维护所有 URL 的状态,因此将结果与爬取/提取状态一起存储很方便。让我们分解所有必需的步骤:

  1. S3 通知触发processEntityResults函数。

  2. 对象作为流从 S3 中检索。

  3. 流被解压并提取。

  4. 输出中的每一行 JSON 都被解析。

  5. 每个 Comprehend 结果条目的结构被转换为一个更易于访问的数据结构。结果按实体类型(人物、地点等)分组。

  6. 网页的种子和 URL 是从 S3 对象的路径(键)中派生的。

  7. 转换后的识别结果被发送到前沿服务。

Lambda 函数和相关内部函数(handleEntityResultLinesstoreEntityResults)可以在提取服务的handler.js模块中找到。

9.9 整合所有内容

我们的大会网站爬取和识别应用中的最后一个任务是整合所有功能,以便在爬虫提供新页面数据时自动分析所有网站。

正如我们在第八章中所做的那样,我们将为此作业使用 AWS 步骤函数。

9.9.1 协调实体提取

图 9.5 显示了步骤函数中实现的控制逻辑以及它与我们所构建的 Lambda 函数之间的关系。

图片

图 9.5 提取服务中的逻辑步骤是通过 AWS 步骤函数编排的。这确保了我们能够控制同时执行多少机器学习作业。它也可以扩展以支持高级错误恢复场景。

我们的大会数据提取过程是一个连续循环,检查新抓取的页面文本,并根据配置的并发作业限制启动异步实体识别。正如我们所见,结果处理是一个独立的、异步的过程,由 S3 桶中 Comprehend 结果的到达驱动。

图 9.5 是对步进函数的轻微简化。步进函数实际上不支持连续执行事件;最大执行时间为一年。在函数中必须有可到达的End状态。为了处理这种情况,我们在步进函数中添加了一些额外的逻辑。我们将终止函数的执行,在 100 次迭代后。这是一个安全措施,以避免忘记长时间运行的任务,可能造成令人惊讶的 AWS 费用!以下列表显示了步进函数 YAML 的压缩视图。完整版本包含在提取服务的serverless.yml文件中。

列表 9.14 简化实体提取步进函数配置

StartAt: Initialize
States:
  Initialize:                               ❶
    Type: Pass
    Result:
      iterations: 100
    ResultPath: $.iterator
    Next: Iterator
  Iterator:                                 ❷
    Type: Task
    Resource: !GetAtt IteratorLambdaFunction.Arn
    ResultPath: $.iterator
    Next: ShouldFinish
  ShouldFinish:                             ❸
    Type: Choice
    Choices:
      - Variable: $.iterator.iterations
        NumericEquals: 0
        Next: Done
    Default: Check Comprehend
  Check Comprehend:
    Type: Task
    Resource: !GetAtt CheckActiveJobsLambdaFunction.Arn
    ...

  Check Job Limit:                          ❹
    Type: Choice
    Choices:
      - Variable: $.activeJobsResult.count
        NumericGreaterThanEquals: 10
        Next: Wait
    Default: Get Text Batch
  Get Text Batch:
    Type: Task
    Resource: !GetAtt GetTextBatchLambdaFunction.Arn
    ...
  Check Batch Size:                          ❺
    Type: Choice
    Choices:
      - Variable: $.textBatchResult.count
        NumericEquals: 0
        Next: Wait
    Default: Start Batch Processing
  Start Batch Processing:
    Type: Task
    Resource: !GetAtt StartBatchProcessingLambdaFunction.Arn
    ...

  Wait:                                      ❻
    Type: Wait
    Seconds: 30
    Next: Iterator
  Done:                 
    Type: Pass
    End: true

❶ 初始状态将迭代计数初始化为 100。

❷ 迭代任务是循环的起点。它调用一个 Lambda 函数来减少计数。

❸ 检查迭代次数。当循环执行了 100 次后,状态机终止。

❹ 现在我们已经运行了 checkActiveJobs 函数,我们可以将活动作业的数量与限制(10)进行比较。

❺ 获取传入文本的批次。如果没有可用的文本,我们等待。如果至少有一个项目,我们开始一个实体识别作业。

❻ 30 秒的等待期是控制数据吞吐量的一个变量。我们还可以增加最大批次大小和并发 Comprehend 作业的数量。

简单迭代函数提供在extraction-service中的handler.js模块。

9.9.2 端到端数据提取测试

我们已经完成了最终的无服务器 AI 应用程序的构建!您已经覆盖了大量无服务器架构,学习了众多非常强大的 AI 服务,并构建了一些相当惊人的 AI 赋能系统。恭喜您达到这一里程碑!现在是时候通过运行我们的端到端会议数据爬取和提取应用程序来奖励自己了。让我们用会议网站的 URL 启动网络爬虫。然后,坐下来观察我们的自动化提取逻辑开始行动,随着使用 AI 检测到的会议和演讲者的详细信息开始出现。

正如我们在第八章末所做的那样,我们将使用种子 URL 启动网络爬虫。这次,我们将选择一个真实的会议网站!

aws stepfunctions start-execution \
 --state-machine-arn arn:aws:states:eu-west-1:1234567890123:stateMachine:CrawlScheduler \
 --input '{"url": "https://dt-x.io"}'

我们也将以相同的方式启动实体提取步进函数。此命令不需要 JSON 输入:

aws stepfunctions start-execution \
  --state-machine-arn arn:aws:states:eu-west-1:1234567890123:stateMachine:EntityExtraction

在这两种情况下,您都需要将步进函数的 ARN 替换为您的部署的正确值。回想一下第八章中,检索这些值所需的 AWS CLI 命令是

aws stepfunctions list-state-machines --output text

一旦状态机开始运行,您可以在 AWS 控制台步骤函数部分查看它们,通过点击状态来监控它们的进度,状态转换发生时。图 9.6 显示了实体提取状态机的进度。

图片

图 9.6 监控实体提取状态机的进度

9.9.3 查看会议数据提取结果

为应用程序构建前端 UI 超出了本章的范围,因此提供了一个方便的脚本scripts/get_extracted_entities.js来检查结果。通过运行此脚本,将在前沿表中执行 DynamoDB 查询,以找到给定种子 URL 提取的实体。然后,这些结果将汇总以生成一个 CSV 文件,总结每个实体的出现次数,以及使用机器学习过程找到的每个实体的平均分数。脚本执行方式如下:

scripts/get_extracted_entities.js https://dt-x.io

脚本使用 AWS SDK,因此必须在 shell 中配置 AWS 凭证。脚本将打印生成的 CSV 文件名称。对于此示例,它将是https-dt-x-io.csv。使用 Excel 等应用程序打开 CSV 文件以检查结果。图 9.7 显示了我们对该会议网站的结果。

图 9.7

图 9.7 监控实体提取状态机的进度

我们已过滤仅显示此案例中的 PERSON 实体。结果包括爬取网站所有页面中提到的每个人!这次会议有一些杰出的演讲者,包括本书的作者!

随意尝试其他会议网站以测试我们会议爬虫和提取器的限制。一如既往,请记住您的 AWS 使用成本。随着数据量的增长,Comprehend 的成本可能会很高 15,尽管有免费层可用。如有疑问,停止任何正在运行的步骤函数状态机,并在测试完成后尽快删除已部署的应用程序。chapter8-9代码目录中包含一个clean.sh脚本,可以帮助您完成此操作!

9.10 总结

您已经到达了最后一章的结尾。恭喜您坚持下来并走这么远!在这本书中,我们构建了

  • 一个具有物体检测功能的图像识别系统

  • 一个语音驱动的任务管理应用

  • 一个聊天机器人

  • 一个自动化的身份文件扫描器

  • 一个用于电子商务系统的 AI 集成,用于确定客户产品评论背后的情感,使用自定义分类器对它们进行分类,并将它们转发到正确的部门

  • 一个使用实体识别来查找会议信息的事件网站爬虫,包括演讲者资料和活动地点

我们还涵盖了许多想法、工具、技术和架构实践。尽管无服务器和 AI 是快速发展的主题,但这些基础原则旨在在您构建令人惊叹的 AI 赋能无服务器系统时保持其适用性。

我们很感激您抽出时间关注AI as a Service。想了解更多信息,请查看 fourTheorem 博客(fourtheorem.com/blog),在那里您可以找到更多关于 AI、无服务器架构等方面的文章。

关注我们 Twitter 和 LinkedIn 上的所有这些主题更新:

  • 彼得·埃尔格--@pelger--linkedin.com/in/peterelger

  • 埃欧恩·沙纳基 - @eoins - linkedin.com/in/eoins

摘要

  • 使用 S3 通知和 AWS Lambda 实现事件驱动计算。

  • 死信队列捕获未投递的消息。它可以与 AWS Lambda 和 SQS 一起实现,以防止数据丢失。

  • 命名实体识别是自动识别文本中如名称、地点和日期等实体的过程。

  • 根据要分析文本的数量,Amazon Comprehend 有多种操作模式可供选择。

  • Comprehend 可用于执行异步批量实体识别。

  • 步进函数可用于控制异步人工智能分析作业的并发性和吞吐量。

  • Comprehend 产生的机器学习分析数据可以根据应用程序的业务需求进行提取和转换。

警告 请确保您完全删除本章中部署的所有云资源,以避免额外收费!


  1. Amazon Comprehend 指南和限制,mng.bz/2WAa

  2. 阿波罗 11 号,维基百科,根据 Creative Commons Attribution-ShareAlike 许可重新发布,en.wikipedia.org/wiki/Apollo_11

  3. 无服务器框架,使用现有存储桶,mng.bz/1g7q

  4. 无服务器修剪插件,github.com/claygregory/serverless-prune-plugin

  5. 子函数和 CloudFormation 变量,mng.bz/P18R

  6. 无服务器框架变量,mng.bz/Jx8Z

  7. 无服务器伪参数插件,mng.bz/wpE5

  8. 无服务器 Dotenv 插件,mng.bz/qNBx

  9. CloudFormation AWS::S3::NotificationConfiguration, http://mng.bz/7GeQ.

  10. AWS CloudFormation 模板自定义资源,http://mng.bz/mNm8.

  11. Lambda 异步调用,mng.bz/5pN7

  12. 有关根据 SQS 队列消息计数创建 CloudWatch 警报的详细信息,请参阅mng.bz/6AjR

  13. lambda-dlq-retry 可在github.com/eoinsha/lambda-dlq-retry找到。

  14. startEntitiesDetectionJob,AWS JavaScript SDK,mng.bz/oRND

  15. Amazon Comprehend 费用,aws.amazon.com/comprehend/pricing/

附录

附录 A. AWS 账户设置和配置

本附录是为不熟悉亚马逊网络服务的读者准备的。它解释了如何在 AWS 上设置账户以及如何配置您的环境以适应书中的示例。

A.1 设置 AWS 账户

在您开始使用 AWS 之前,您需要创建一个账户。您的账户是您所有云资源的篮子。如果多人需要访问它,您可以将多个用户附加到账户上;默认情况下,您的账户将有一个根用户。要创建账户,您需要以下信息:

  • 一个用于验证身份的电话号码

  • 一张信用卡来支付您的账单

注册过程包括五个步骤:

  1. 提供您的登录凭证。

  2. 提供您的联系信息。

  3. 提供您的付款详情。

  4. 验证您的身份。

  5. 选择您的支持计划。

将您喜欢的网络浏览器指向 aws.amazon.com,然后点击创建免费账户按钮。

A.1.1 提供您的登录凭证

创建 AWS 账户首先需要定义一个唯一的 AWS 账户名称,如图 A.1 所示。AWS 账户名称必须在所有 AWS 客户中全局唯一。除了账户名称外,您还必须指定用于验证您的 AWS 账户根用户的电子邮件地址和密码。我们建议您选择一个强大的密码,以防止账户被滥用。使用至少 20 个字符的密码。保护您的 AWS 账户免受未经授权的访问对于避免数据泄露、数据丢失或代表您的不当资源使用至关重要。花些时间研究如何使用您的账户进行多因素认证(MFA)也是值得的。

图 A.1 创建 AWS 账户:注册页面

下一步,如图 A.2 所示,是添加您的联系信息。填写所有必填字段,然后继续。

图 A.2 创建 AWS 账户:提供联系信息

A.1.2 提供您的付款详情

接下来,如图 A.3 所示的屏幕会要求您提供付款信息。提供您的信用卡信息。如果您更方便,可以选择稍后从 USD 更改货币设置到 AUD、CAD、CHF、DKK、EUR、GBP、HKD、JPY、NOK、NZD、SEK 或 ZAR。如果您选择此选项,则月底将美元金额转换为您的当地货币。

图 A.3 创建 AWS 账户:提供付款详情

A.1.3 验证您的身份

下一步是验证您的身份。图 A.4 显示了该过程的第一个步骤。在您完成表单的第一部分后,您将收到 AWS 的电话。一个机器人声音会要求您输入 PIN 码。四位数 PIN 码在网站上显示,您必须使用电话输入它。在您的身份得到验证后,您就可以继续最后一步。

图 A.4 创建 AWS 账户:验证身份

A.1.4 选择您的支持计划

最后一步是选择一个支持计划;见图 A.5。在这种情况下,选择免费的基本计划。如果您后来为您的业务创建 AWS 账户,我们建议选择商业支持计划。您甚至可以在以后切换支持计划。您可能需要等待几分钟,直到您的账户准备就绪。如图 A.6 所示,点击“控制台登录”,以首次登录您的 AWS 账户!

APPA_F05_Elger.png

图 A.5 创建 AWS 账户:选择您的支持计划

APPA_F06_Elger.png

图 A.6 创建 AWS 账户:您已成功创建 AWS 账户。

A.2 登录

您现在拥有了一个 AWS 账户,并准备好登录 AWS 管理控制台。管理控制台是一个基于 Web 的工具,您可以使用它来检查和控制 AWS 资源;它使 AWS API 的大部分功能对您可用。图 A.7 显示了console.aws.amazon.com上的登录表单。输入您的电子邮件地址,点击“下一步”,然后输入您的密码以登录。

APPA_F07_Elger.png

图 A.7 创建 AWS 账户:登录控制台

成功登录后,您将被转发到控制台的开始页面,如图 A.8 所示。

APPA_F08_Elger.png

图 A.8 AWS 控制台

A.3 最佳实践

在前面的章节中,我们已经介绍了设置 AWS 根账户。如果您打算仅为此账户进行实验,这将是足够的;然而,请注意,对于生产工作负载,不建议使用根账户。这个主题的全面讨论超出了本书的范围,但我们强烈建议您使用 AWS 账户最佳实践,例如,根据本 AWS 文章中概述的设置 IAM 用户、组和角色:mng.bz/nzQd。我们还推荐 AWS 安全博客,作为了解 AWS 相关安全主题的绝佳资源:aws.amazon.com/blogs/security/

A.4 AWS 命令行界面

当您需要创建、编辑或检查 AWS 云资源时,您有许多选择:

  • 手动,使用网络浏览器中的 AWS 控制台。

  • 以编程方式,使用您选择的编程语言的 AWS SDK。许多语言都受到支持,包括 JavaScript 和 Python。

  • 使用第三方工具,如 Serverless Framework。这些工具通常在底层使用 AWS SDK。

  • 使用 AWS 命令行界面(CLI)。

在本书中,我们将尽可能使用 Serverless Framework。在某些情况下,我们将使用 AWS CLI 执行命令。我们的目标是避免使用 AWS 控制台。AWS 控制台对于实验和熟悉 AWS 产品来说已经足够好。它也是最易用的。然而,随着你对 AWS 的了解不断深入,了解 AWS CLI 和 SDK 确实是值得的。你应该旨在使用程序化选项,以下是一些原因:

  • 您的代码(包括 CLI 命令)记录了您所做的更改。

  • 您可以将代码置于版本控制之下(例如,Git)并有效地管理更改。

  • 可以快速重做操作,而无需执行许多手动步骤。

  • 与点对点界面常见的错误相比,人为错误的可能性较小。

让我们设置 AWS CLI,以便在需要时可以运行 CLI 命令。

安装方法将取决于您的平台。对于基于 Windows 的安装,只需下载 64 位(s3.amazonaws.com/aws-cli/AWSCLI64PY3.msi)或 32 位(s3.amazonaws.com/aws-cli/AWSCLI32PY3.msi)安装程序。

A.4.1 在 Linux 上安装 AWS CLI

大多数 Linux 软件包管理器都为 AWS CLI 提供了快速安装选项。对于基于 Ubuntu 或 Debian 的系统,使用 apt

sudo apt install awscli

对于使用 yum 的发行版,如 CentOS 和 Fedora,输入以下命令:

sudo yum install awscli

A.4.2 在 MacOS 上安装 AWS CLI

对于使用 Homebrew 的 MacOS 用户,最简单的安装方法是使用 Homebrew:

brew install awscli

A.4.3 在其他平台上安装 AWS CLI

如果您的系统与已描述的选项不同,您可以尝试其他方法,例如使用 pip 通过 Python 安装 AWS CLI。有关详细信息,请参阅 AWS CLI 安装文档(mng.bz/X0gE)。

A.4.4 配置本地 AWS 环境

要从本地开发系统访问 AWS 服务,我们需要创建一个 API 访问密钥对,并将其提供给我们的开发外壳。为此,首先登录到您的 AWS 账户,然后从 AWS 用户菜单中选择“我的安全凭证”,如图 A.9 所示。

图片 A.9

图 A.9 AWS 安全凭证菜单

接下来,从 AWS 用户列表中选择您的用户名,然后从用户摘要屏幕中选择创建访问密钥,如图 A.10 所示。

图片 A.10

图 A.10 AWS 用户摘要屏幕

AWS 将创建一个 API 访问密钥对。要使用这些密钥,请继续下载如图 A.11 所示的 CSV 文件。

图片 A.11

图 A.11 创建密钥对话框

将此 CSV 文件存储在安全的地方以供以后参考。CSV 文件包含两个标识符:访问密钥 ID 和秘密访问密钥。内容应类似于以下列表。

列表 A.1 AWS 凭据 CSV 文件

Access key ID,Secret access key
ABCDEFGHIJKLMNOPQRST,123456789abcdefghijklmnopqrstuvwxyz1234a

要使用这些密钥进行访问,我们需要将它们添加到我们的开发外壳中。对于类 UNIX 系统,这可以通过将环境变量添加到您的 shell 配置中来实现。例如,Bash 外壳用户可以将这些添加到他们的.bash_profile文件中,如下一个列表所示。

列表 A.2 bash_profile中用于 AWS 凭证的条目

export AWS_ACCOUNT_ID=<your aws account ID>
export AWS_ACCESS_KEY_ID=<your access key ID>
export AWS_SECRET_ACCESS_KEY=<your secret access key>
export AWS_DEFAULT_REGION=eu-west-1
export AWS_REGION=eu-west-1

注意:我们已经设置了AWS_REGIONAWS_DEFAULT_REGION环境变量。这是由于 JavaScript SDK 和 CLI 之间不幸的不匹配造成的。AWS CLI 使用AWS_DEFAULT_REGION,而 SDK 使用AWS_REGION。我们预计这将在未来的版本中得到修正,但到目前为止,简单的修复方法是设置这两个变量到相同的区域。

Windows 用户需要在控制面板的系统配置对话框中设置这些环境变量。请注意,为了使这些环境变量生效,您需要重新启动您的开发外壳。

管理您的密钥

有多种其他方法可以通过使用配置文件来配置 AWS API 访问。为了方便,我们已使用环境变量进行本地开发。

在管理访问密钥时,您应该谨慎行事,以确保它们不会意外泄露。例如,将访问密钥添加到公共 Git 仓库是一个非常糟糕的想法!

注意,我们建议仅在本地开发环境中使用环境变量来存储 AWS API 密钥。我们不推荐您在生产环境中这样做。有服务可以帮助管理密钥,例如 AWS 密钥管理服务(KMS)。这个主题的全面讨论超出了本书的范围。

A.4.5 检查设置

为了确认设置良好,请运行以下命令:

$ aws --version
$ aws s3 ls s3://

这两个都应该无错误地完成。如果情况不是这样,那么请回顾本附录中所有前面的步骤。

附录 B. AWS 托管 AI 服务的数据需求

第一章介绍了一张 AWS 托管 AI 服务的表格。本附录在此基础上扩展,展示了每个服务的数据需求。这体现在表 B.1 中。它还指出了每个服务是否支持训练。您可以使用此指南,结合第七章中关于数据收集的所有学习内容,以确保您拥有正确的数据并已进行了充分的数据准备。

表 B.1 AI 服务的数据需求

应用 服务 需要的数据 训练支持
机器翻译 AWS Translate 源语言文本 机器翻译不支持或需要自定义训练。然而,您可以定义特定于您领域的自定义术语。
文档分析 AWS Textract 文档的高质量图像 无需训练。
关键短语 AWS Comprehend 文本 无需训练。
情感分析 AWS Comprehend 文本 无需训练。
主题建模 AWS Comprehend 文本 无需训练。
文档分类 AWS Comprehend 带有分类标签的文本 需要训练。我们在第六章中介绍了自定义分类器。
实体提取 AWS Comprehend 文本。对于自定义实体训练,需要标记过的实体。 标准实体(名称、日期和地点)可以不经过训练进行提取。也可以通过提供带有实体标签的文本集来训练 AWS Comprehend 以使用自定义实体。
聊天机器人 AWS Lex 文本语句 无需训练。AWS Lex 基于样本语句和配置的槽位构建模型。这已在第四章中介绍。
语音转文本 AWS Transcribe 音频文件或流式音频 无需训练,但可以添加自定义词汇和发音以细化结果。
文本转语音 AWS Polly 文本,可选地使用 SSML 进行标注 无需训练。AWS Polly 在第四章中有所介绍。
目标、场景和活动检测 AWS Rekognition 图像或视频 无需训练。
面部识别 AWS Rekognition 图像或视频 无需训练,但可添加自定义面部。
面部分析 AWS Rekognition 图像或视频 无需训练。
图像中的文本 AWS Rekognition 图像 无需训练。
时间序列预测 AWS Forecast 时间序列数据和项目元数据 需要训练。AWS Forecast 根据您提供的历 史数据和元数据训练模型。
实时个性化推荐 AWS Personalize 项目目录和用户数据 需要训练。AWS Personalize 可以根据提供的数据训练模型并选择最佳算法。

如您所见,大多数服务不需要训练阶段。对于这些情况,数据收集和学习过程得到了极大的简化。无论是进行训练还是使用预训练模型,AWS 都对所需数据的类型和格式有明确的规范。

附录 C. 人工智能应用的数据源

第七章概述了在构建人工智能应用时良好数据收集和准备的重要性。本附录列出了你可能利用的一些数据源,以确保你有适合人工智能成功的正确数据。

C.1 公共数据集

  1. AWS 上的开放数据注册处(registry.opendata.aws)包括其他数据集,如 PB 级的 Common Crawl 数据(commoncrawl.org/the-data)。

  2. 公共 API,如 Twitter API,提供了大量数据。我们在第六章中看到了社交媒体帖子如何用于执行分类和情感分析。

  3. Google 有一个公共数据集的搜索引擎(toolbox.google.com/datasetsearch)和公共数据集列表(ai.google/tools/datasets/)。

  4. Kaggle 有一个包含数千个数据集的目录(www.kaggle.com/datasets)。

  5. 许多政府数据源现在都可以使用。一个例子是美国开放政府数据在data.gov

  6. 如果你是一个觉得第二章中猫的内容太多的狗爱好者,你将会被斯坦福狗数据集中可用的 20,000 张狗图片所安慰(vision.stanford.edu/aditya86/ImageNetDogs/)!

提示:许多公共数据集都受到许可的限制。做你的作业,了解在你的工作中使用数据集的法律影响。

C.2 软件分析和日志

除了公共、预包装的数据之外,还有许多收集机器学习应用数据的方法。现有的软件系统具有分析和日志数据,这些数据可以准备和优化以用于机器学习算法:

  • 收集来自 Web 和移动应用程序的最终用户交互数据的分析平台是用户行为和交互的原始数据的宝贵来源。Google Analytics 就是这样一个例子。

  • 网络服务器和后端应用程序日志或审计日志也可能是系统内外交互的全面来源。

C.3 人类数据收集

当数据不易获得且需要大规模收集或转换时,有几种方式可以众包这项工作:

  • 数据收集公司提供收集(通过调查或其他方式)或转换数据的服务。

  • 存在着由 API 驱动的众包服务。Amazon Mechanical Turk (MTurk) 是一个著名的例子(www.mturk.com/)。

  • 我们中的许多人已经进行了无数次的 Captcha 检查,作为验证我们不是机器人的手段!这项服务实际上提供了两个好处。像 reCAPTCHA 这样的服务也充当了为图像识别算法收集标记训练数据的一种手段。1

C.4 设备数据

根据您的应用,您可能可以从现有系统中收集遥测数据,无论是使用软件监控工具还是硬件传感器:

  • 传感器不再仅限于工业自动化设备。物联网(IoT)设备正在许多环境中变得普遍,并生成可能庞大的数据集。

  • 静态图像或视频摄像头可以用来收集用于训练和分析的图像数据。例如,想想谷歌街景所需的图像捕捉规模,以及 reCAPTCHA 如何作为大规模标注这些图像的手段。


  1. James O’Malley, “Captcha if you can: how you’ve been training AI for years without realising it.” TechRadar 12 January 2018, www.techradar.com/news/captcha-if-you-can-how-youve-been-training-ai-for-years-without-realising-it.

附录 D. 设置 DNS 域和证书

本书介绍的一些系统需要通过 AWS 管理控制台进行常见的 AWS 设置,而不是通过编程方式。这是因为需要一些手动验证。请在运行示例系统之前确保你已经完成了以下设置。

D.1 设置域名

当你为 AWS 资源如 S3 存储桶和 API 网关创建动态 HTTP 端点时,AWS 将为这些端点生成一个 URL。当你不是在构建生产应用程序时,可以使用这些生成的名称。然而,这很快就会变得令人沮丧。每次你移除和销毁这些资源时,URL 可能会改变。它们也很长,难以记住。为了避免这些问题,我们将注册一个域名。通过使用 AWS 中的 Route 53 服务,这个过程变得简单。如果你已经有一个域名并希望使用它,或者你希望使用已注册域名的子域名,请查阅 Route 53 文档(mng.bz/Mox8)。

D.1.1 注册域名

我们将使用 Route 53 从头开始介绍注册新域名的流程。

如果你在这个 AWS 账户上还没有任何与域名相关的资源,点击主 AWS 控制台网络部分中的 Route 53 链接(假设所有服务控制已展开)将带你到一个介绍屏幕。如果你已经创建了资源,你将被发送到 Route 53 仪表板。

图片

图 D.1 显示了 Amazon Route 53 介绍页面,展示了该服务的四个不同元素

图 D.1 显示了 Route 53 介绍页面。正如你所见,Route 53 是为了提供四个不同但紧密相关的服务而构建的:域名注册(Amazon 是一个域名注册商);DNS 管理,这是你用来将流量引导到你的域的工具;流量管理,用于处理流量重定向;以及可用性监控,用于确认你的目标资源正在按预期运行。我们只关注域名注册和 DNS 管理。

点击域名注册下方的“立即开始”按钮,然后点击注册域名。输入名称的主要部分--例如,如果您想注册acme-corporation.com,则输入acme-corporation。一个下拉菜单显示包括.com、.org、.net 等在内的域名,以及它们的年度注册费用。选择一个,然后点击检查。Route 53 将在网上搜索记录,查看该组合是否目前可用。当您找到一个符合您需求的域名时,将其添加到购物车,并完成结账流程以提交第一年的注册费用。域名注册通常每年花费 10 到 15 美元的美元,并且不包括免费层使用。在短时间内,您的新域名将出现在 Route 53 仪表板中。在您的域名注册完成之前可能需要一段时间。到那时,您就可以继续配置您的域名,并使用它来为您的全新开发的 Serverless AI 应用程序服务了!

注意 并没有强制您必须使用 Route 53 进行域名注册。实际上,您可能会发现其他提供商提供更便宜的替代方案。即使是通过其他公司注册的域名,您也可以使用 Route 53 的其他功能。

D.1.2 配置您的托管区域

您的域名现在已注册,但您还没有告诉它如何处理传入的请求。Route 53 将自动为您注册的域名创建一个托管区域。在控制台的网络部分点击托管区域,然后点击新托管区域的链接。您将发现自己在一个带有两个预先创建的记录集的页面:

  • 授权开始(SOA)--标识您域的基本 DNS 配置信息。

  • NS--列出可以查询您的域名主机的权威性名称服务器。这些是提供域名翻译请求答案的公共服务。

注意 记录集--一组数据记录,定义了域行为的特定方面。

不要随意更改这两个记录集。仅凭它们本身不足以使您的新域名完全可用。稍后,我们将使用 Serverless Framework 自动添加一个新的记录,告诉任何使用您的域名服务器(通过将他们的浏览器指向您的域名)请求我们应用程序使用的 IP 地址。

D.2 设置证书

网络安全是一个广泛的话题,远远超出了本书的范围。我们仍然想要确保我们为所有网络流量使用 HTTPS。使用纯 HTTP 的日子已经一去不复返了,尽早考虑安全最佳实践是明智的。为了便于管理证书的生成和更新,我们将使用 AWS 证书管理器。

D.2.1 预配新证书

在 AWS 控制台中,在网络部分点击证书管理器的链接。这将带您到证书管理器仪表板,如图 D.2 所示。

图 D.2 证书管理器介绍页面

在“提供证书”部分下选择“开始”,并选择“请求公共证书”选项。如图 D.3 所示的“请求证书”页面允许我们指定证书的域名。我们将请求一个通配符证书,用于我们注册域名的所有子域名。例如,如果我们注册了 stuff.org,通配符证书将保护 api.stuff.orgwww.stuff.org

添加 *.stuff.org(通配符域名)和 stuff.org。然后,点击下一步以选择验证方法。这将显示类似于图 D.3 的页面。

AWS 控制台将要求验证您已添加的域名,以确保您是所有者,如图 D.4 所示。

图片

图 D.3 选择要使用证书保护的域名

图片

图 D.4 选择您的证书验证方法

选择 DNS 验证并确认此选择。由于我们在 Route 53 中注册了域名,我们有自动在托管区域中创建特殊验证 DNS 条目的选项。展开如图 D.5 所示的每个域的章节。

图片

图 D.5 使用 Route 53 创建验证 DNS 记录

在 Route 53 中点击创建记录。在选择继续之前,为每个域名确认此步骤。

您可能需要等待长达 30 分钟,直到您的域名验证完成并且证书配置完成。一旦完成,证书管理器将显示您的证书状态为 已验证,如图 D.6 所示。

图片

图 D.6 在 AWS 控制台的证书管理部分显示的已验证证书

这是非常棒的工作!您已注册了一个域名,并创建了一个相关的 SSL/TLS 证书以确保流量加密。您将稍后使用此域名来访问您新部署的应用程序。

附录 E. Serverless Framework 内部机制

在本附录中,我们将更详细地探讨 AWS 上的无服务器技术,特别是 Serverless Framework,它是本书中许多示例系统所使用的框架。

如第一章所述,术语 serverless 并非指没有服务器的系统;它的意思是我们可以构建不需要关注底层服务器基础设施的系统。通过使用无服务器技术,我们能够提升抽象层次,更多地关注应用程序逻辑,而不是技术上的“重活”。

无服务器架构的一个关键概念是基础设施即代码(IaC)。IaC 允许我们将系统的整个基础设施视为源代码。这意味着我们可以将其存储在版本控制系统(如 Git)中,并对其创建和维护应用软件开发的最佳实践。

所有主要的云服务提供商都支持某种基础设施即代码(IaC)机制。在 AWS 上,支持 IaC 的服务称为 CloudFormation。

CloudFormation 可以通过创建 JSON 或 YAML 格式的模板文件进行配置。虽然可以直接使用文本编辑器编写模板,但随着系统规模的扩大,模板可能会变得难以管理,因为模板相当冗长。有许多工具可以帮助开发者与 CloudFormation 一起工作,例如 SAM、AWS CDK 和 Serverless Framework。还有其他一些工具,如 HashiCorp 的 Terraform,它们针对多个云服务,这里不会进行介绍。

虽然 Serverless Framework 可以用于部署任何 AWS 资源,但它主要面向管理和部署无服务器 Web 应用程序。通常这意味着 API Gateway、Lambda 函数以及像 DynamoDB 表这样的数据库资源。Serverless 配置文件可以被视为一种轻量级的领域特定语言(DSL),用于描述这些类型的应用程序。

图 E.1 展示了 Serverless Framework 如何与 CloudFormation 协作。

图 E.1 CloudFormation 工作流程

在部署时,Serverless 配置文件(serverless.yml)被“编译”成 CloudFormation 模板。创建一个部署存储桶,并将定义的每个 Lambda 函数的代码工件上传。为每个 Lambda 函数计算哈希值并将其包含在模板中。然后 Serverless 调用 CloudFormation 的 UpdateStack 方法,将部署工作委托给 CloudFormation。CloudFormation 然后继续查询现有基础设施。如果发现差异(例如,如果定义了新的 API Gateway 路由),CloudFormation 将进行必要的基础设施更新,以使部署与新的编译模板保持一致。

E.1 演示

让我们通过一个简单的 Serverless 配置文件详细说明部署过程。首先创建一个名为 hello 的新空目录。cd 到这个目录并创建一个文件 serverless.yml。将下一列表中显示的代码添加到该文件中。

列表 E.1 简单的 serverless.yml

service: hello-service

provider:
  name: aws
  runtime: nodejs10.x
  stage: dev
  region: eu-west-1

functions:
  hello:
    handler: handler.hello
    events:
      - http:
          path: say/hello
          method: get

接下来,在同一个目录中创建一个名为 handler.js 的文件,并将下一列表中的代码添加到其中。

列表 E.2 简单的处理程序函数

'use strict'

module.exports.hello = async event => {
  return {
    statusCode: 200,
    body: JSON.stringify({
      message: 'Hello!',
      input: event
    },
    null, 2)
  }
}

现在,让我们将此处理程序部署到 AWS。在部署之前,你需要设置一个 AWS 账户并配置你的命令行。如果你还没有这样做,请参阅附录 A,其中介绍了设置过程。

要部署此简单应用程序,请运行

$ serverless deploy

让我们看看在部署过程中创建的组件。在部署此应用程序时,框架在应用程序目录中创建了一个名为 .serverless 的本地工作目录。如果你查看这个目录,你应该会看到下一列表中列出的文件。

列表 E.3 Serverless 工作目录

cloudformation-template-create-stack.json
cloudformation-template-update-stack.json
hello-service.zip
serverless-state.json

这些文件具有以下用途:

  • 如果尚未存在,cloudformation-template-create-stack.json 用于创建用于代码组件的 S3 部署存储桶。

  • cloudformation-template-update-stack.json 包含用于部署的编译后的 CloudFormation 模板。

  • hello-service.zip 包含我们 Lambda 函数的代码包。

  • serverless-state.json 存储当前已部署状态的本地副本。

登录 AWS 网络控制台以查看框架实际部署了什么。首先转到 S3 并搜索包含字符串 'hello' 的存储桶;你应该会找到一个名为类似 hello-service-dev-serverlessdeploymentbucket-zpeochtywl7m 的存储桶。这是框架用于将代码推送到 AWS 的部署存储桶。如果你查看这个存储桶,你会看到类似于以下列表的结构。

列表 E.4 Serverless 部署存储桶

serverless
  hello-service
    dev
      <timestamp>
        compiled-cloudformation-template.json
        hello-service.zip

<timestamp> 被替换为你运行部署的时间。随着对服务的更新,框架会将更新的模板和代码推送到此存储桶以进行部署。

接下来,使用 AWS 控制台导航到 API Gateway 和 Lambda 网络控制台。点击右上角的“服务”链接,然后搜索lambdaapi gateway,如图 E.2 所示。

图片

图 E.2 在 AWS 网络控制台中搜索服务。

在 Lambda 和 Api Gateway 控制台中,你会看到服务的已部署实例,如图 E.3 和 E.4 所示。

图片

图 E.3 已部署的 Lambda 函数

图片

图 E.4 已部署的 API Gateway

最后,如果你打开 CloudFormation 网络控制台,你会看到服务的已部署模板。这应该看起来与图 E.5 类似。

理解框架的部署流程有助于在出现问题时进行问题诊断。关键是要记住,serverless deploy 将部署委托给 CloudFormation 的 UpdateStack,并且如果出现问题,我们可以使用 AWS 控制台查看堆栈更新历史和当前状态。

图 E.5 已部署的 CloudFormation 堆栈

E.2 清理

一旦完成示例堆栈,请确保通过运行

$ serverless remove

确保框架已移除此处描述的所有相关工件。

posted @ 2025-11-22 09:03  绝不原创的飞龙  阅读(0)  评论(0)    收藏  举报