【完整指南】打造无伺服器架构severless 应用
很多人都说「无伺服器架构」是云端运算的下一阶段发展,而「无伺服器架构」是来自云端运算中两个服务的整合:后端即服务(BaaS) 和功能即服务(FaaS )。
藉由BaaS,我们将应用程式拆解成更小的元件,并使用外部服务来完全部署这些元件。通常是透过API调用(或gRPC调用)所完成,Google其中一项最受欢迎的后端即服务就是Firebase,它是一款适用于行动装置和网页应用程式的即时资料库(具有许多额外厉害的功能)。
另一方面,功能即服务则是云端运算服务的另一种形式:FaaS 是一种构建和部署伺服器端代码的方法,只需在供应商提供的FaaS 平台上部署各种功能(hence the name)。
介绍完无伺服器架构的定义之后,接着来建构一个完整的「无伺服器应用程式」吧。
我们要建构的应用程式是一个聊天机器人,它能够从图片中读取文字内容(可选择将其翻译为不同的语言),并通过SMS 讯息(或电话) 将结果发送回用户。这种应用程式可以从图像甚至视频流中提取可用的讯息,并将SMS 通知发送给用户或群组。
1 – 建立 Chatbot
对于我们的使用案例,我们希望与我们的聊天机器人开始对话并提供一些包含文字讯息的内容以供后续分析(范例:书籍或报纸的其中一页)。
a-为我们的代理商创建“对话流”
由于「聊天机器人」实作的部分不是我们在这篇文章中的主要内容,因此我们将在DialogFlow 中设计一个最简单快速的dialog,如下所示:
- 建立一个intent “Read”。
- 添加几个用户的句子,例如:“读取文字内容” 或“摘录文字内容”
- 添加一个“read” 动作
- 启用webhook (请参阅下方的实行过程)。
b-实作聊天机器人逻辑
现在让我们编写聊天机器人的逻辑,依此将实际拍摄照片。首先,我们需要两个实用功能:
- captureImage 使用用户相机拍摄图像的功能。
- uploadImage 将该图像上传到Google Cloud Storage (GCS) 的功能。
首先是captureImage 功能函数的实作。此函数使用MacOS 上的系统函数imagesnap 来实际使用相机,捕捉图像并将图像文件存储于下方路径/tmp/google-actions-reader-${Date.now()}.png 。接着函数会将档案名称和内容以以base64 格式传回:
const fs = require('fs');
const child_process = require('child_process');
const Buffer = require('safe-buffer').Buffer;
/**
* Capture the image from the user computer's camera.
*/
function captureImage() {
return new Promise((res, rej) => {
const file = `/tmp/google-actions-reader-${Date.now()}.png`;
try {
child_process.execSync(`imagesnap -w 1 ${file}`);
const bitmap = fs.readFileSync(file);
res({
base64: new Buffer(bitmap).toString('base64'),
file
});
} catch (err) { rej(err); }
});
}
下一个功能函数uploadImage 则将该图像简单地上传到GCS 中的储存分区(bucket)cloud-function-ocr-demo__image:
const child_process = require('child_process');
/**
* Uploads the file to GCS.
*
* @param {object} data The GCP payload metadata.
* @param {object} data.file The filename to read.
*/
function uploadImage(data) {
child_process.execSync(
`gsutil cp ${data.file} gs://cloud-function-ocr-demo__image`
);
return data.file.split('/').pop();
}
请注意该储存分区(bucket)的名称cloud-function-ocr-demo__image,我们稍后将需要它。
现在我们准备好了两个功能函数captureImage 并uploadImage,让我们在intent 中的读取(READ) 逻辑使用它们(请记住上面dialog 中的这个intent):
/**
* The "read" intent that will trigger the capturing and uploading
* the image to GSC.
*
* @param {object} app DialogflowApp instance object.
*/
function readIntent(app) {
captureImage()
.then(uploadImage)
.then(content => {
app.tell(`I sent you an SMS with your content.`);
})
.catch(e => app.ask(`[ERROR] ${e}`) );
}
readIntent 将会截取照片并且上传到GCS。
现在我们已经完成了聊天机器人的所有逻辑,接下来我们要建立一个主要的 Cloud Functions 来处理DialogFlow的请求:
const aog = require('actions-on-google');
const DialogflowApp = aog.DialogflowApp;
/**
* Handles the agent (chatbot) logic. Triggered from an HTTP call.
*
* @param {object} request Express.js request object.
* @param {object} response Express.js response object.
*/
module.exports.assistant = (request, response) => {
const app = new DialogflowApp({ request, response });
const actions = new Map();
actions.set('read', readIntent);
app.handleRequest(actions);
};
上面程式码有个function 名称为assistant,会需要透过HTTP 请求来触发。当使用者在在聊天室输入文字时,HTTP 请求用将由DialogFlow 执行,例如:「阅读文字」(如上所述) 是读取intent 中定义的表达式。
c-部署辅助的Cloud Functions
本章节将为本指南提供其他的范例。
为了部署Cloud Functions,我们可以使用gcloud 命令附带有以下的参数:
gcloud beta functions
deploy [function-label]
[trigger-type]
--source [source-code]
--entry-point [function-name]
- <function-label> 是一个功能函数标签,和<function-name> 相似但有些许不同。
- <trigger-type> 是你的功能将如何被触发(topic,http,storage 等)。
- <source-code> 是指原始码托管放置于Google Cloud 代码库的位置。此处不得为其他公开的Git 代码库的位置!
- <function-name> 是实际输出的功能函数名称(在您的代码中)。
*注1:您还可以使用Google 云端储存分区(bucket) 托管您的功能函数原始码。但我们不会在此指南介绍。
*注2:如果您的组织中有持续部署方式(continuous delivery),那么将您的原始码托管在Google云端代码库(a Git repo) 中是个不错的主意。
以下是我们范例中完整的命令:
gcloud beta functions
deploy ocr-assistant
--source https://source.developers.google.com/projects/...
--trigger-http
--entry-point assistant
如果您想知道更详细,Google Cloud 代码库中的原始码位址格式如下:
https://source.developers.google.com/projects//repos//moveable-aliases/
一旦部署完毕,您的功能函数应该准备完成等待触发:
您还将获得一个公开的URL,如下所示:
https://us-central1-.cloudfunctions.net/ocr-assistant
这是我们将在DialogFlow 项目中使用的URL。
如果你有仔细阅读,你可能会注意到captureImage功能函数需要使用相机,这意味着我们无法将此特定功能部署到Google云端平台。相反,我们会将其托管在我们的特定硬体上,例如Raspberry PI (to make it simple),并使用不同的URL。您可以使用Google Cloud Function Emulator在本地运行您的云端函数。请记住,这仅用于开发目的。不要将其用于实际的应用程式。
d-添加实际运行网址
接着添加实际运行的URL,它指向Cloud Functions中的 assistant 将处理聊天机器人的请求:
现在,我们完成了应用程式的第一部分,主要内容包括将图像上传到GCS。
2-处理图像
到目前为止,我们只讨论了Cloud Functions – FaaS 的部分。让我们跳转到后端即服务(BaaS) 的部分。
在我们的范例中,我们希望能够从图像中摘录出一些文字内容。我们有大量的开源函式库来做到这一点,简单举几个例子,如:OpenCV 或 Tensorflow。但是运行这些开源函式库,我们会需要拥有机器学习和图像(或声音)处理方面的专家,而这并不容易。此外,在理想情况下,我们并不想管理这个功能,不希望维护此代码,也希望能够在应用程式受欢迎后有能力自动扩展。幸运的是,Google云端平台给了我们以下支援:
- 在 Google Vision API 中允许我们摘录内容。
- 使用Google Translation API可以协助我们翻译内容。
以下是此功能的子结构:
a-从图像中摘录内容
为了能够处理图像,我们需要两个功能:
- processImage 每当新图像上传到GCS 中的储存分区(bucket) cloud-function-ocr-demo__image 时,都会触发Cloud Functions。
- detectText 该功能将使用Google Vision API 从图像中实际摘录文字内容。
processImage 执行过程如下:
/**
* Cloud Function triggered by GCS when a file is uploaded.
*
* @param {object} event The Cloud Functions event.
* @param {object} event.data A Google Cloud Storage File object.
*/
exports.processImage = function processImage(event) {
let file = event.data;
return Promise.resolve()
.then(() => {
if (file.resourceState === 'not_exists') {
// This was a deletion event, we don't want to process this
return;
}
return detectText(file.bucket, file.name);
})
.then(() => {
console.log(`File ${file.name} processed.`);
});
};
detectText功能函数的实作很简单(我们稍后会改进它):
const vision = require('@google-cloud/vision')();
/**
* Detects the text in an image using the Google Vision API.
*
* @param {string} bucketName Cloud Storage bucket name.
* @param {string} filename Cloud Storage file name.
*/
function detectText(bucketName, filename) {
let text;
return vision
.textDetection({
source: {
imageUri: `gs://${bucketName}/${filename}`
}
})
.then(([detections]) => {
const annotation = detections.textAnnotations[0];
text = annotation ? annotation.description : '';
return Promise.resole(text);
});
}
我们现在需要部署processImage Cloud Function,并且希望在新图像上传到GCS 中的储存分区(bucket) cloud-function-ocr-demo__image 时触发它。
gcloud beta functions
deploy ocr-extract
--source https://source.developers.google.com/projects/...
--trigger-bucket cloud-function-ocr-demo__image
--entry-point processImage
接着,我们需要加入一些翻译。
b-翻译文字内容
翻译摘录出的文字将由特定的Google Cloud Pub/Sub 主题触发TRANSLATE_TOPIC,它将包含两项操作:
- 检测摘录出的内容所属的语言。我们将于先前的processImage 功能中执行此操作。我们可以为此建立另一个Cloud Functions,但不要让我们的架构过于复杂!
- translateText :将该内容转换为给定的语言。
使用语言检测功能来改进我们现有的processImage Cloud Functions:
const vision = require('@google-cloud/vision')();
const translate = require('@google-cloud/translate')();
const config = require('./config.json');
/**
* Detects the text in an image using the Google Vision API.
*
* @param {string} bucketName Cloud Storage bucket name.
* @param {string} filename Cloud Storage file name.
* @returns {Promise}
*/
function detectText(bucketName, filename) {
let text;
return vision
.textDetection({
source: {
imageUri: `gs://${bucketName}/${filename}`
}
})
.then(([detections]) => {
const annotation = detections.textAnnotations[0];
text = annotation ? annotation.description : '';
return translate.detect(text);
})
.then(([detection]) => {
if (Array.isArray(detection)) {
detection = detection[0];
}
// Submit a message to the bus for each language
// we're going to translate to
const tasks = config.TO_LANG.map(lang => {
let topicName = config.TRANSLATE_TOPIC;
if (detection.language === lang) {
topicName = config.RESULT_TOPIC;
}
const messageData = {
text: text,
filename: filename,
lang: lang,
from: detection.language
};
return publishResult(topicName, messageData);
});
return Promise.all(tasks);
});
}
让我们来解释我们额外新添加的代码:
我们首先添加了对Google Translation API 的调用,用translate.detect(text);检测摘录文字的主要语言。然后,在下一个区块中,基本上遍访配置文件中的config.TO_LANG 阵列中的每一个元素,并发布一个TRANSLATE_TOPIC 包含文字内容(text),来源语言(from) 和我们想要翻译的目标语言的特定有效内容(lang) 。如果来源语言与目标语言相同,我们只发布RESULT_TOPIC。
Google Cloud Pub / Sub 的备注
为了方便起见,我们还包括一个新的实用功能,publishResult 它负责发布Pub / Sub主题(topic)。它主要使用Google Cloud Pub / Sub API 建立(如果需要) 并发布给定主题:
const pubsub = require('@google-cloud/pubsub')();
/**
* Publishes the result to the given pub-sub topic.
*
* @param {string} topicName Name of the topic on which to publish.
* @param {object} data The message data to publish.
*/
function publishResult(topicName, data) {
return pubsub
.topic(topicName)
.get({ autoCreate: true })
.then(([topic]) => topic.publish(data));
}
接下来我们建立一个translateText Cloud Function 来翻译摘录出的文字:
const translate = require('@google-cloud/translate')();
const Buffer = require('safe-buffer').Buffer;
const config = require('./config.json');
/**
* Translates text using the Google Translate API.
* Triggered from a message on a Pub/Sub topic.
*
* @param {object} event The Cloud Functions event.
* @param {object} event.data The Cloud Pub/Sub Message object.
* @param {string} event.data.data The "data" property of
* the Cloud Pub/Sub Message.
* This property will be a base64-encoded string that
* you must decode.
*/
exports.translateText = function translateText(event) {
const pubsubMessage = event.data;
const jsonString = Buffer.from(
pubsubMessage.data, 'base64'
).toString();
const payload = JSON.parse(jsonString);
return Promise.resolve()
.then(() => {
const options = {
from: payload.from,
to: payload.lang
};
return translate.translate(payload.text, options);
})
.then(([translation]) => {
const messageData = {
text: translation,
filename: payload.filename,
lang: payload.lang
};
return publishResult(config.RESULT_TOPIC, messageData);
});
};
这个函数的实作是一目了然的:我们会调用translation.translate(payload.text, options); 一旦我们得到结果,我们就发布翻译后的内容为RESULT_TOPIC。
现在是时候使用与以前相同的命令部署translateText Cloud Function 了。该功能将由TRANSLATE_TOPIC 主题触发,因此我们需要使用「主题」作为触发型态:
gcloud beta functions
deploy ocr-translate
--source https://source.developers.google.com/projects/...
--trigger-topic TRANSLATE_TOPIC
--entry-point translateText
c-保存翻译后的文字
到目前为止,我们现在已经设法捕捉图像,将其上传到GCS,处理它并摘录出文字且进行翻译。最后一步是将翻译后的文字存回GCS。
以下为此功能函数的实作:
const storage = require('@google-cloud/storage')();
const Buffer = require('safe-buffer').Buffer;
const config = require('./config.json');
/**
* Saves the data packet to a file in GCS.
* Triggered from a message on a Pub/Sub topic.
*
* @param {object} event The Cloud Functions event.
* @param {object} event.data The Cloud Pub/Sub Message object.
* @param {string} event.data.data The "data" property of
* the Cloud Pub/Sub Message.
* This property will be a base64-encoded string that
* you must decode.
*/
exports.saveResult = function saveResult(event) {
const pubsubMessage = event.data;
const jsonString = Buffer.from(
pubsubMessage.data, 'base64'
).toString();
const payload = JSON.parse(jsonString);
return Promise.resolve()
.then(() => {
const bucketName = config.RESULT_BUCKET;
// Appends a .txt suffix to the image name.
const filename = renameFile(payload.filename, payload.lang);
const file = storage.bucket(bucketName).file(filename);
return file.save(payload.text)
.then(_ => publishResult(config.READ_TOPIC, payload));
});
};
saveResult 是由RESULT_TOPIC 持有翻译文本的主题触发的。我们只需使用该有效内容并调用Google Cloud Storage API 将内容储存在名为config.RESULT_BUCKET (即cloud-functions-orc-demo) 的储存分区(bucket) 中。一旦完成,我们发布该READ_TOPIC 主题并触发下一个Cloud Functions (请参阅下一节)。
来到部署saveResult Cloud Function 的时间,使用与先前相同的命令来部署。该功能将由TRANSLATE_TOPIC 主题触发,因此我们也需要使用「主题」作为触发型态:
gcloud beta functions
deploy ocr-save
--source https://source.developers.google.com/projects/...
--trigger-topic RESULT_TOPIC
--entry-point saveResult
3-发送SMS 讯息通知
最后,现在我们准备从GCS 读取翻译后的文字内容,并通过SMS 发送至用户的手机。
a-从GCS 读取翻译后的文字内容
从GCS 读取文件同样是个简单的操作:
const Buffer = require('safe-buffer').Buffer;
/**
* Reads the data packet from a file in GCS.
* Triggered from a message on a Pub/Sub topic.
*
* @param {object} event The Cloud Functions event.
* @param {object} event.data The Cloud Pub/Sub Message object.
* @param {string} event.data.data The "data" property of
* the Cloud Pub/Sub Message.
* This property will be a base64-encoded string that
* you must decode.
*/
exports.readResult = function readResult(event) {
const pubsubMessage = event.data;
const jsonString = Buffer.from(
pubsubMessage.data, 'base64'
).toString();
const payload = JSON.parse(jsonString);
return Promise.resolve()
.then(() => readFromBucket(payload))
.then(content => sendSMS(content).then(_ => call(content)));
};
在readResult 功能函数中,我们会用到另一个工具程式的函数readFromBucket,顾名思义,它从给定的GCS 储存分区(bucket) 中读取内容。以下是详细的实作过程:
const storage = require('@google-cloud/storage')();
const config = require('./config.json');
/**
* Reads the data packet from a file in GCS.
* Triggered from a message on a Pub/Sub topic.
*
* @param {object} payload The GCS payload metadata.
* @param {object} payload.filename The filename to read.
*/
function readFromBucket(payload) {
// Appends a .txt suffix to the image name.
const filename = renameFile(payload.filename, payload.lang);
const bucketName = config.RESULT_BUCKET;
const file = storage.bucket(bucketName).file(filename);
const chunks = [];
return new Promise((res, rej) => {
file
.createReadStream()
.on('data', chunck => {
chunks.push(chunck);
})
.on('error', err => {
rej(err);
})
.on('response', response => {
// Server connected and responded with
// the specified status and headers.
})
.on('end', () => {
// The file is fully downloaded.
res(chunks.join(''));
});
});
}
接着让我们部署readResult Cloud Function 并使其从READ_TOPIC 主题中触发:
gcloud beta functions
deploy ocr-read
--source https://source.developers.google.com/projects/...
--trigger-topic READ_TOPIC
--entry-point readResult
b-发送SMS 讯息通知
当来到发送SMS讯息至用户手机上的阶段时,我们采用一款厉害的服务Twilio。
*注:欲使用Twilio服务,需要您建立一个开发者帐户。
const Twilio = require('twilio');
const TwilioClient = new Twilio(
config.TWILIO.accountSid,
config.TWILIO.authToken
);
/**
* Sends an SMS using Twilio's service.
*
* @param {string} body The content to send via SMS.
*/
function sendSMS(body) {
return TwilioClient.messages
.create({
to: '+33000000000',
from: '+33000000000',
body: body || 'MESSAGE NOT FOUND'
});
}
c-拨打电话(BONUS)
通过电话向用户回送翻译的内容有点棘手,因为您需要提供两个function:
- 「call」这个function 用于实际上拨打电话呼叫用户。
- 「twilloCalls」这个function 为一个HTTP 接口, 负责处理由“call” function 所发送请求。
为了演示这个过程如何进行,我们先来看看twilioCalls 实行如下:
const Twilio = require('twilio');
const VoiceResponse = Twilio.twiml.VoiceResponse;
/**
* Handles the incoming Twilio call request.
* Triggered from an HTTP call.
*
* @param {object} request Express.js request object.
* @param {object} response Express.js response object.
*/
module.exports.twilioCall = function(request, response) {
return readFromBucket({
filename: 'twilio_user_33000000000.txt'
}).then(content => {
const twiml = new VoiceResponse();
twiml.say(`
Hi, this is your extracted text:
${content}
`);
res.writeHead(200, { 'Content-Type': 'text/xml' });
res.end(twiml.toString());
});
};
twilioCall功能函数负责从储存分区(bucket)中读取文件,并经由Twilio Markup Language ( TwilioML )建立XML格式的回应。
然后,您需要部署此Cloud Function 以获取该call 功能函数所需的公开URL :
gcloud beta functions
deploy ocr-twilio-call
--source https://source.developers.google.com/projects/...
--trigger-http
--entry-point twilioCall
部署完成后,您将获得如下所示的公开URL:
https://us-central1-.cloudfunctions.net/ocr-twilio-call
接下来,我们将在该call 函数中使用该URL:
/**
* Triggers a call using Twilio's service.
*/
function call() {
return TwilioClient.api.calls
.create({
url: 'https://the-url-from-above/ocr-twilio-call',
to: '+33000000000',
from: '+33000000000'
});
}
完成!现在,您的Twilio HTTP 端已准备好接听来电。
总结!在本指南中,我们实行了一组Cloud Functions执行不同任务:
- assistant 处理来自DialogFlow 的聊天机器人请求。
- processImage 从上传的图像中摘录出文字内容。
- translateText 将摘录出的文字翻译成不同的语言。
- saveResult 将翻译后的文字内容保存到GCS。
- readResult 从储存在GCS 中的文件里读取翻译后的文字内容。
- twilioCall 处理来电请求。
以下为所有已部署的Cloud Functions 重点总结:
试试看
为了测试应用程式,首先我们需要部署DialogFlow 聊天机器人。我们选择将其部署到Google Assistant,因为我们的assistant Cloud Function 主要在处理Google 智能助理请求。如果你想部署到其他服务(Slack,Facebook,Twitter 等),你就需要提供和部署其他Google Assistant。
于按钮选项列中选择Google Assistant,接着点击TEST 按钮
这将在Google 模拟器上打开Actions,允许您直接在浏览器中测试您的聊天机器人。或者,您也可以使用手机或Google Home 设备:
同时给我们的聊天机器人一个名字吧,范例:莎士比亚。从模拟器的总览面板中可以完成这项工作。
做为演示范例,我们将加入以下的引用(由 Ziad K. Abdelnour 撰写):
而且这是由我们的readResult 功能发送的SMS 讯息:
以下是完整的原始码:https://github.com/manekinekko/serverless-application-demo
恭喜您刚刚建构完毕一个真正的“无伺服器架构”应用程式!
(原文翻译此。)