原文:
zh.annas-archive.org/md5/96e50944f4c1f57a68d39f9d5c19a2b2译者:飞龙
第八章:为失败而设计
在本章中,将涵盖以下食谱:
-
使用适当的超时和重试
-
实现背压和速率限制
-
处理故障
-
重新提交故障事件
-
使用反向 OpLock 实现幂等性
-
使用事件溯源实现幂等性
简介
处理失败是云原生的基础。我们构建自主服务,当它们失败时限制爆炸半径,并在其他服务失败时继续运行。我们将部署与发布解耦,并控制每次部署的批量大小,以便在部署出错时容易识别问题。我们将测试左移到持续部署管道中,以在部署之前捕获问题,以及一直移到生产环境中,在那里我们持续测试系统并在异常情况下发出警报,以最小化恢复时间。本章中的食谱演示了如何设计一个在失败面前具有弹性和宽容性的服务,以便正确处理瞬态故障,最小化其影响,并使服务能够自我修复。
使用适当的超时和重试
计算机网络的现实是它们不可靠。云计算的现实是它依赖于计算机网络,因此我们必须实施服务来正确处理网络异常。这个食谱演示了如何正确配置函数和 SDK 调用,使用适当的超时和重试。
如何做…
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch8/timeout-retry --path cncb-timeout-retry
-
使用
cd cncb-timeout-retry切换到cncb-timeout-retry目录。 -
查看名为
serverless.yml的文件,其内容如下:
service: cncb-timeout-retry
...
functions:
command:
handler: handler.command
timeout: 6
memorySize: 1024
...
- 查看名为
handler.js的文件,其内容如下:
module.exports.command = (request, context, callback) => {
const db = new aws.DynamoDB.DocumentClient({
httpOptions: { timeout: 1000 },
logger: console,
});
...
db.put(params, callback);
};
-
使用
npm install安装依赖项。 -
使用
npm test运行测试。 -
查看在
.serverless目录中生成的内容。 -
使用
npm run dp:lcl -- -s $MY_STAGE部署堆栈。 -
查看 AWS 控制台中的堆栈和资源。
-
使用以下命令调用函数:
$ sls invoke -f command -r us-east-1 -s $MY_STAGE -d '{"name":"thing one"}'
- 查看以下命令函数日志:
$ sls logs -f command -r us-east-1 -s $MY_STAGE
2018-07-14 23:41:14.229 (-04:00) ... [AWS dynamodb 200 0.063s 0 retries] putItem({ TableName: 'john-cncb-timeout-retry-things',
Item:
{ id: { S: 'c22f3ce3-551a-4999-b750-a20e33c3d53b' },
name: { S: 'thing one' } } })
- 完成后,使用
npm run rm:lcl -- -s $MY_STAGE删除堆栈。
它是如何工作的…
网络中断可能随时发生。一个请求可能无法通过,而下一个请求可能顺利通过;因此,我们应该设置较短的超时时间,以便我们能够尽快重试过程,但又不至于太短,以至于一个良好的请求在正常完成之前就超时了。我们还必须确保我们的超时和重试周期在函数超时之前有足够的时间完成。默认情况下,aws-sdk 配置为两分钟后超时,并执行三次带有递增延迟时间的重试。当然,两分钟太长了。将 timeout 设置为 1000(1 秒)通常足以让请求完成,并允许在 6 秒的函数超时之前完成三次重试。
如果请求频繁超时,这可能表明函数分配的资源过少。例如,函数分配的 memorySize 与使用的机器实例大小之间存在关联。较小的机器实例也有更少的网络 I/O 容量,这可能导致更频繁的网络中断。因此,增加 memorySize 将降低网络波动性。
实现背压和速率限制
云原生系统中的各种服务必须能够处理系统中的流量起伏。上游服务不应过载下游服务,下游服务必须能够处理峰值负载而不会落后或过载更下游的服务。本食谱展示了如何利用流处理器的自然背压并实现额外的速率限制来管理节流。
准备工作
在开始此食谱之前,您需要一个 AWS Kinesis Stream,例如在 创建事件流 食谱中创建的那个。
如何操作…
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch8/backpressure-ratelimit --path cncb-backpressure-ratelimit
-
使用以下命令导航到
cncb-backpressure-ratelimit目录:cd cncb-backpressure-ratelimit。 -
查看名为
serverless.yml的文件,其内容如下:
service: cncb-backpressure-ratelimit
...
functions:
listener:
handler: handler.listener
timeout: 240 # headroom for retries
events:
- stream:
batchSize: 1000 # / (timeout / 2) < write capacity
...
environment:
WRITE_CAPACITY_UNITS: 10
SHARD_COUNT: 1
...
resources:
Resources:
Table:
Type: AWS::DynamoDB::Table
Properties:
...
ProvisionedThroughput:
...
WriteCapacityUnits: ${self:functions.listener.environment.WRITE_CAPACITY_UNITS}
- 查看名为
handler.js的文件,其内容如下:
module.exports.listener = (event, context, cb) => {
_(event.Records)
.map(recordToUow)
.filter(forPurple)
.ratelimit(Number(process.env.WRITE_CAPACITY) /
Number(process.env.SHARD_COUNT) / 10, 100)
.flatMap(put)
.collect()
.toCallback(cb);
};
const put = uow => {
const params = { ... };
const db = new aws.DynamoDB.DocumentClient({
httpOptions: { timeout: 1000 },
// default values:
// maxRetries: 10,
// retryDelayOptions: {
// base: 50,
// },
logger: console,
});
return _(db.put(params).promise()
.then(() => uow)
);
};
-
使用以下命令安装依赖项:
npm install。 -
使用以下命令运行测试:
npm test -- -s $MY_STAGE。 -
查看在
.serverless目录中生成的内容。 -
使用以下命令部署堆栈:
npm run dp:lcl -- -s $MY_STAGE。 -
在 AWS 控制台中查看堆栈和资源。
-
使用以下命令调用
simulate函数:
$ sls invoke -f simulate -r us-east-1 -s $MY_STAGE
[
{
"total": 4775,
"blue": 1221,
"green": 1190,
"purple": 1202,
"orange": 1162
}
]
- 查看以下
trigger函数日志:
$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter 'event count' 2018-07-15 22:53:29 ... event count: 1000
2018-07-15 22:54:00 ... event count: 1000
2018-07-15 22:54:33 ... event count: 1000
2018-07-15 22:55:05 ... event count: 425
2018-07-15 22:55:19 ... event count: 1000
2018-07-15 22:55:51 ... event count: 350$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter 'Duration' REPORT ... Duration: 31011.59 ms ...
REPORT ... Duration: 33038.58 ms ...
REPORT ... Duration: 32399.91 ms ...
REPORT ... Duration: 13999.56 ms ...
REPORT ... Duration: 31670.86 ms ...
REPORT ... Duration: 12856.77 ms ...
$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter 'retries' ...
2018-07-15 22:55:49 ... [AWS dynamodb 200 0.026s 0 retries] putItem({ TableName: '...',
Item:
{ id: { S: '686dc03a-88a3-11e8-829c-67d049599dd2' },
type: { S: 'purple' },
timestamp: { N: '1531709604787' },
partitionKey: { S: '3' },
tags: { M: { region: { S: 'us-east-1' } } } },
ReturnConsumedCapacity: 'TOTAL' })
2018-07-15 22:55:50 ... [AWS dynamodb 200 0.013s 0 retries] putItem({ TableName: '...',
Item:
{ id: { S: '686d4b14-88a3-11e8-829c-67d049599dd2' },
type: { S: 'purple' },
timestamp: { N: '1531709604784' },
partitionKey: { S: '4' },
tags: { M: { region: { S: 'us-east-1' } } } },
ReturnConsumedCapacity: 'TOTAL' })
...
- 完成后,使用以下命令删除堆栈:
npm run rm:lcl -- -s $MY_STAGE。
它是如何工作的…
背压 是实现良好的流处理器的关键特性。当使用 命令式编程范式,例如在批处理记录数组中循环时,下游目标系统可能会轻易过载,因为循环会尽可能快地处理记录,而不考虑目标系统的吞吐量能力。另一方面,函数式响应式编程(FRP)范式,例如使用 Highland.js (highlandjs.org) 或 RxJS (github.com/ReactiveX/rxjs) 库,提供了自然的背压,因为数据只以下游步骤能够完成任务的速度被拉取。例如,吞吐量低的系统只会以其容量允许的速度快速拉取数据。
另一方面,像 DynamoDB 或 Kinesis 这样高度可扩展的系统,能够以极高的吞吐量处理数据,它们依赖于节流来限制容量。在这种情况下,FRP 的自然背压不足以应对;需要额外使用 ratelimit 功能。例如,当我编写这个菜谱时,我运行了一个没有速率限制的模拟,然后出去吃饭。当我几个小时后回来时,模拟生成的事件仍在尝试处理。这是因为 DynamoDB 节流了请求,而 aws-sdk 内置的指数退避和重试消耗了太多时间,导致函数 timeout 并重新尝试整个批量。这表明,虽然重试,如 Employing proper timeouts and retries 菜谱中讨论的那样,对于同步请求很重要,但不能仅依赖于异步流处理。相反,我们需要积极限制流量速率,以避免节流和指数退避,从而确保批量在函数超时内完成。
在这个菜谱中,我们使用一个简单的算法来计算流量速率——每 100 毫秒 WRITE_CAPACITY / SHARD_COUNT / 10。这确保了 DynamoDB 每秒接收的请求数量不会超过已分配的数量。我还使用了一个简单的算法来确定批量大小——batchSize / (timeout / 2) < WRITE_CAPACITY。这确保了在正常情况下应该有足够的时间来完成批量,但在有节流的情况下,将会有两倍于必要的时间。请注意,这只是一个逻辑起点;这是云原生系统中性能调优应该关注的领域。你的数据和目标系统的特性将决定最有效的设置。正如我们将在 Autoscaling DynamoDB 菜谱中看到的那样,自动扩展为背压、速率限制和性能调优增加了另一个维度。无论如何,你可以从这些简单且安全的算法开始,并在一段时间内调整它们。
处理故障
流处理器对瞬时错误具有天然的抗性和宽容性,因为它们使用背压并自动重试失败的批量。然而,如果处理不当,硬错误可能会导致交通堵塞,导致消息丢失。这个菜谱将向你展示如何将这些错误委派为故障事件,以便良好的消息可以继续流动。
如何操作…
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch8/handling-faults --path cncb-handling-faults
-
使用
cd cncb-handling-faults命令进入cncb-handling-faults目录。 -
检查名为
serverless.yml的文件。 -
检查包含以下内容的
handler.js文件:
module.exports.listener = (event, context, cb) => {
_(event.Records)
.map(recordToUow)
.filter(forThingCreated)
.tap(validate)
.tap(randomError)
.flatMap(save)
.errors(errors)
.collect().toCallback(cb);
};
const validate = uow => {
if (uow.event.thing.name === undefined) {
const err = new Error('Validation Error: name is required');
// handled
err.uow = uow;
throw err;
}
};
const randomError = () => {
if (Math.floor((Math.random() * 5) + 1) === 3) {
// unhandled
throw new Error('Random Error');
}
};
const save = uow => {
...
return _(db.put(uow.params).promise()
.catch(err => {
// handled
err.uow = uow;
throw err;
}));
};
const errors = (err, push) => {
if (err.uow) {
// handled exceptions are adorned with the uow in error
push(null, publish({
type: 'fault',
timestamp: Date.now(),
tags: {
functionName: process.env.AWS_LAMBDA_FUNCTION_NAME,
},
err: {
name: err.name,
message: err.message,
stack: err.stack,
},
uow: err.uow,
}));
} else {
// rethrow unhandled errors to stop processing
push(err);
}
};
-
使用
npm install安装依赖项。 -
使用
npm test -- -s $MY_STAGE运行测试。 -
检查
.serverless目录中生成的内容。 -
使用
npm run dp:lcl -- -s $MY_STAGE部署堆栈。 -
在 AWS 控制台中检查堆栈和资源。
-
使用以下命令调用
simulate函数:
$ sls invoke -f simulate -r us-east-1 -s $MY_STAGE
- 查看以下
trigger函数日志:
$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter 'publishing fault' ... {"type":"fault","timestamp":...,"tags":{"functionName":"cncb-handling-faults-john-listener"},"err":{"name":"Error","message":"Validation Error: name is required" ...
... {"type":"fault","timestamp":...,"tags":{"functionName":"cncb-handling-faults-john-listener"},"err":{"name":"ValidationException","message":"...Missing the key id..." ...
- 完成后,删除堆栈:
npm run rm:lcl -- -s $MY_STAGE。
它是如何工作的…
这个食谱实现了我在我的书云原生开发模式和最佳实践(www.packtpub.com/application-development/cloud-native-development-patterns-and-best-practices)中深入讨论的流断路器模式。遇到硬错误的流处理器将继续重新处理这些事件,直到它们从流中过期——除非流处理器被实现为将这些错误放在一边,将它们作为故障事件委托给其他地方处理,如重新提交故障事件食谱中所述。这缓解了交通堵塞,以便其他事件可以继续流动。
这个食谱模拟了在流处理器中不同阶段可能失败的事件。一些事件模拟了上游的 bug,这些 bug 将导致验证逻辑失败,该逻辑断言事件正在上游正确创建。其他事件在它们被插入到 DynamoDB 时将失败。逻辑还会随机失败一些事件来模拟不会产生故障的短暂错误,这些错误将自动重试。在日志中,您将看到发布了两条故障事件。当生成随机错误时,您将在日志中看到函数正在重试批次。如果模拟没有生成随机错误,那么您应该重新运行它,直到它生成错误。
为了隔离流中的事件,我们需要引入工作单元(UOW)的概念,它将一批中的一个或多个事件组合成一个原子单元,这个单元必须一起成功或失败。UOW 包含原始的 Kinesis 记录(uow.record)、从记录中解析的事件(uow.event)以及任何附加到 UOW 的中间处理结果(uow.params),这些结果在它通过流处理器时被附加。UOW 还用于标识错误是否被处理或未处理。当验证逻辑识别出预期的错误或从外部调用中捕获到错误时,UOW 将被装饰到错误上,并且错误将被重新抛出。装饰的工作单元(error.uow)作为指示器,告知errors处理器错误已在流处理器中被处理,并且应该发布错误作为故障事件。意外的错误,如随机生成的错误,不会被流处理器逻辑处理,因此不会有装饰的 UOW。错误处理器将推送这些错误到下游,导致函数失败并重试。在创建警报食谱中,我们讨论了对故障事件以及迭代器年龄的监控,以便团队可以及时收到关于流处理器错误的通知。
重新提交故障事件
我们已经设计我们的流处理器将错误作为故障事件进行委派,以便有效的事件可以继续流动。我们监控并警告故障事件,以便可以采取适当的行动来解决问题。一旦问题得到解决,可能需要重新处理失败的事件。本食谱演示了如何将故障事件重新提交给引发故障的流处理器。
如何操作…
- 从以下模板创建
monitor、simulator和cli项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch8/resubmitting-faults/monitor --path cncb-resubmitting-faults-monitor
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch8/resubmitting-faults/cli --path cncb-resubmitting-faults-cli
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch8/resubmitting-faults/simulator --path cncb-resubmitting-faults-simulator
-
检查
cncb-resubmitting-faults-monitor和cncb-resubmitting-faults-simulator目录中的serverless.yml文件。 -
按照以下方式部署
monitor和simulator堆栈:
$ cd ../cncb-resubmitting-faults-monitor
$ npm install
$ npm test -- -s $MY_STAGE
$ npm run dp:lcl -- -s $MY_STAGE
Stack Outputs
BucketName: cncb-resubmitting-faults-monitor-john-bucket-1llq835xdczd8
$ cd ../cncb-resubmitting-faults-simulator
$ npm install
$ npm test -- -s $MY_STAGE
$ npm run dp:lcl -- -s $MY_STAGE
-
在 AWS 控制台中检查堆栈和资源。
-
按照以下方式从
cncb-resubmitting-faults-simulator目录运行模拟器:
$ sls invoke -f simulate -r us-east-1 -s $MY_STAGE
-
确认故障文件已写入监控堆栈输出中指定的存储桶
cncb-resubmitting-faults-monitor-*-bucket-*并注意路径。如果没有生成故障,请再次运行模拟器,直到生成故障。 -
检查名为
./cli/lib/resubmit.js的文件,其内容如下:
exports.command = 'resubmit [bucket] [prefix]'
exports.desc = 'Resubmit the faults in [bucket] for [prefix]'
...
const invoke = (lambda, options, event) => {
const Payload = JSON.stringify({
Records: [event.uow.record],
});
const params = {
FunctionName: event.tags.functionName,
...
Payload: Buffer.from(Payload),
};
return _(lambda.invoke(params).promise());
}
- 使用以下命令重新提交故障:
$ cd ../cncb-resubmitting-faults-cli
$ npm install
$ node index.js resubmit -b cncb-resubmitting-faults-monitor-$MY_STAGE-bucket-<suffix> -p <s3-path> --dry false
- 手动清空
cncb-resubmitting-faults-monitor-*存储桶,并在完成npm run rm:lcl -- -s $MY_STAGE后移除monitor和simulator堆栈。
它是如何工作的…
在 处理故障 食谱中,我们看到了流处理器如何通过发布包含失败工作单元所有相关数据的故障事件来委派硬错误。在本食谱中,故障监控器消费这些故障事件并将它们存储在 S3 存储桶中。这使得团队能够审查特定的故障,以帮助确定问题的根本原因。故障包含捕获的具体异常、失败的事件以及附加到工作单元的所有上下文信息。
一旦解决了根本原因,原始事件可以提交回发布故障的流处理器。这是可能的,因为故障事件包含原始 Kinesis 记录 (event.uow.record) 和要调用的函数名称 (event.tags.functionName)。命令行实用程序从指定路径的存储桶中读取所有故障事件并调用特定函数。从函数逻辑的角度来看,这种直接调用函数与直接从 Kinesis 流调用没有区别。然而,流处理器必须设计为幂等的,并且能够处理乱序事件,正如我们将在 使用逆 OpLock 实现幂等性 和 使用事件溯源实现幂等性 食谱中讨论的那样。
使用逆 OpLock 实现幂等性
从业务规则的角度来看,事件必须恰好处理一次非常重要;否则,可能会出现问题,例如重复计数或根本不计数。然而,我们的云原生系统必须能够抵御故障并主动重试以确保不丢失任何消息。不幸的是,这意味着消息可能会被多次投递,例如当生产者重新发布事件或流处理器重试可能已部分处理的批次时。解决这个问题的方法是实现所有操作都具有幂等性。这个配方通过我所说的逆 OpLock 来实现幂等性。
如何做到这一点…
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch8/idempotence-inverse-oplock --path cncb-idempotence-inverse-oplock
-
使用
cd cncb-idempotence-inverse-oplock命令导航到cncb-idempotence-inverse-oplock目录。 -
检查名为
serverless.yml的文件。 -
检查名为
handler.js的文件,其内容如下:
module.exports.listener = (event, context, cb) => {
_(event.Records)
.map(recordToUow)
.filter(forThingSaved)
.flatMap(save)
.collect().toCallback(cb);
};
const save = uow => {
const params = {
TableName: process.env.TABLE_NAME,
Item: {
...uow.event.thing,
oplock: uow.event.timestamp,
},
ConditionExpression: 'attribute_not_exists(#oplock) OR #oplock < :timestamp',
...
};
const db = new aws.DynamoDB.DocumentClient({
httpOptions: { timeout: 1000 },
logger: console,
});
return _(db.put(params).promise()
.catch(handleConditionalCheckFailedException)
.then(() => uow)
);
}
const handleConditionalCheckFailedException = (err) => {
if (err.code !== 'ConditionalCheckFailedException') {
err.uow = uow;
throw err;
}
};
-
使用
npm install命令安装依赖项。 -
使用
npm test -- -s $MY_STAGE命令运行测试。 -
检查
.serverless目录中生成的内容。 -
使用
npm run dp:lcl -- -s $MY_STAGE命令部署栈。 -
在 AWS 控制台中检查栈和资源。
-
使用以下命令调用模拟函数:
$ sls invoke -f simulate -r us-east-1 -s $MY_STAGE
- 查看以下
listener函数的日志:
$ sls logs -f listener -r us-east-1 -s $MY_STAGE
... [AWS dynamodb 200 0.098s 0 retries] putItem({ TableName: '...',
Item:
{ id: { S: '3022eaeb-45b7-46d5-b0a1-696c0eb9aa25' },
oplock: { N: '1531628180237' } },
...
... [AWS dynamodb 400 0.026s 0 retries] putItem({ TableName: '...',
Item:
{ id: { S: '3022eaeb-45b7-46d5-b0a1-696c0eb9aa25' },
oplock: { N: '1531628180237' } },
...
... { ConditionalCheckFailedException: The conditionalrequest failed
at Request.extractError ...
...
message: 'The conditional request failed',
code: 'ConditionalCheckFailedException',
time: 2018-07-15T04:16:21.202Z,
requestId: '36BB14IGTPGM8DE8CFQJS0ME3VVV4KQNSO5AEMVJF66Q9ASUAAJG',
statusCode: 400,
retryable: false,
retryDelay: 20.791698133966396 }
... [AWS dynamodb 200 0.025s 0 retries] putItem({ TableName: '...',
Item:
{ id: { S: '3022eaeb-45b7-46d5-b0a1-696c0eb9aa25' },
oplock: { N: '1531628181237' } },
...
... [AWS dynamodb 400 0.038s 0 retries] putItem({ TableName: '...',
Item:
{ id: { S: '3022eaeb-45b7-46d5-b0a1-696c0eb9aa25' },
oplock: { N: '1531628180237' } },
...
... { ConditionalCheckFailedException: The conditionalrequest failed
at Request.extractError ...
...
message: 'The conditional request failed',
...
- 完成后,使用
npm run rm:lcl -- -s $MY_STAGE命令删除栈。
它是如何工作的…
传统的乐观锁定防止多个用户同时更新相同的记录。只有当用户检索数据后oplock字段没有变化时,记录才会被更新。如果数据已更改,则会抛出异常,并强制用户在继续更新之前重新检索数据。这迫使更新按顺序执行,并需要人工交互来解决任何潜在冲突。
逆 OpLock旨在为异步处理提供幂等性。我们不是强制事务重试,而是简单地做相反的事情——我们丢弃较旧或重复的事件。在上游 Backend For Frontend 服务中,可以使用传统的 OpLock 来序列化用户事务,其中下游服务实现逆 OpLock 以确保较旧或重复的事件不会覆盖最新的数据。在这个配方中,我们使用uow.event.timestamp作为oplock值。在某些场景中,如果多个事件在确切的同一毫秒发生,可能更倾向于使用序列号。ConditionalCheckFailedException被捕获并忽略。所有其他异常都会重新抛出,并附加工作单元以发布故障事件,如处理故障配方中所述。
这个配方中的模拟会发布一个thing-created事件,再次发布它,然后发布一个thing-updated事件,最后第三次发布thing-created事件。日志显示thing-created事件只被处理一次,重复的则被忽略。
使用事件溯源实现幂等性
从业务规则的角度来看,事件必须被精确处理一次非常重要;否则,可能会出现问题,例如重复计数或根本不计数。然而,我们的云原生系统必须能够抵御故障并主动重试以确保不丢失任何消息。不幸的是,这意味着消息可能会被多次投递,例如当生产者重新发布事件或流处理器重试可能已部分处理的事务批次时。解决这个问题的方法是实现所有操作的可幂等性。本菜谱演示了如何使用事件溯源和微事件存储来实现幂等性。
如何做到这一点…
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch8/idempotence-es --path cncb-idempotence-es
-
使用
cd cncb-idempotence-es命令导航到cncb-idempotence-es目录。 -
查看名为
serverless.yml的文件。 -
查看以下内容的
handler.js文件:
module.exports.listener = (event, context, cb) => {
_(event.Records)
.map(recordToUow)
.filter(forThingSaved)
.flatMap(saveEvent)
.collect().toCallback(cb);
};
const saveEvent = uow => {
const params = {
TableName: process.env.EVENTS_TABLE_NAME,
Item: {
id: uow.event.thing.id,
sequence: uow.event.id,
event: uow.event,
}
};
const db = new aws.DynamoDB.DocumentClient({
httpOptions: { timeout: 1000 },
logger: console,
});
return _(db.put(params).promise()
.then(() => uow)
);
}
-
使用
npm install安装依赖项。 -
使用
npm test -- -s $MY_STAGE运行测试。 -
查看在
.serverless目录中生成的内容。 -
使用
npm run dp:lcl -- -s $MY_STAGE部署堆栈。 -
在 AWS 控制台中查看堆栈和资源。
-
使用以下命令调用模拟函数:
$ sls invoke -f simulate -r us-east-1 -s $MY_STAGE
- 查看以下
trigger函数日志:
$ sls logs -f trigger -r us-east-1 -s $MY_STAGE
... record: { ... "Keys":{"sequence":{"S":"3fdb8c10-87ea-11e8-9cf5-0b6c5b83bdcb"},"id":{"S":"8c083ef9-d180-48b8-a773-db0f61815f38"}}, ...
... record: { ... "Keys":{"sequence":{"S":"3fdb8c11-87ea-11e8-9cf5-0b6c5b83bdcb"},"id":{"S":"8c083ef9-d180-48b8-a773-db0f61815f38"}}, ...
- 完成后,使用
npm run rm:lcl -- -s $MY_STAGE删除堆栈。
它是如何工作的…
事件溯源通过事件不可变来促进幂等性。具有相同唯一 ID 的相同事件可以被发布或处理多次,并产生相同的结果。微事件存储充当缓冲区,以过滤掉重复项。服务消费所需事件,并将它们存储在具有 hashkey 的微事件存储中,该 hashkey 用于分组相关事件,例如领域对象的 uow.event.thing.id,以及基于 uow.event.id 的范围键。这个主键也是不可变的。因此,相同的事件可以保存多次,但在数据库流上只产生一个事件。因此,在 创建微事件存储 或 实现分析 BFF 菜谱中讨论的业务逻辑只会触发一次。
本菜谱中的模拟发布了一个 thing-created 事件,再次发布它,然后发布一个 thing-updated 事件,最后第三次发布 thing-created 事件。日志显示,三个 thing-created 事件实例只在 DynamoDB 流上产生一个事件。
第九章:优化性能
在本章中,将涵盖以下食谱:
-
调优函数即服务
-
批量请求
-
利用异步非阻塞 IO
-
在流处理器中分组事件
-
自动扩展 DynamoDB
-
使用缓存控制
-
利用会话一致性
简介
云原生将性能测试、调优和优化颠倒过来。许多利用的完全管理的无服务器云服务具有隐式可伸缩性。这些服务按请求购买,并将自动扩展以满足峰值和意外的需求。对于这些资源,进行前期性能测试的必要性要小得多;相反,我们根据在生产环境中进行的持续测试收集到的信息进行优化,如第七章优化可观察性中所述,并基于收集到的信息持续调优。我们还利用持续部署来推动必要的改进。这种基于价值的开发方法有助于确保我们专注于最高价值的改进。
尽管如此,还有一些资源,如某些流处理器和数据存储,严重依赖于显式定义的批量大小和读写容量。对于关键服务,这些资源必须得到充分分配,以确保峰值数据处理量不会使其过载。因此,本章的食谱将专注于在设计和发展过程中值得提前应用的性能优化技术,以帮助确保服务不会相互对抗。
调优函数即服务
调优函数与传统服务调优非常不同,因为可调整的旋钮很少。函数的短生命周期也带来了许多影响,使得传统的技术和框架成为反模式。以下食谱解释了一个常见的内存错误,讨论了传统语言和库选择对冷启动的影响,并展示如何使用webpack打包 JavaScript 函数以最小化下载时间。
如何做…
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch9/tuning-faas --path cncb-tuning-faas
-
导航到
cncb-tuning-faas目录,cd cncb-tuning-faas。 -
查看以下内容的
serverless.yml文件:
service: cncb-tuning-faas
plugins:
- serverless-webpack
provider:
memorySize: 1024 # default
...
package:
individually: true
custom:
webpack:
includeModules: true
functions:
save:
memorySize: 512 # function specific
...
- 查看以下内容的
webpack.config.js文件:
const slsw = require('serverless-webpack');
const nodeExternals = require("webpack-node-externals");
const path = require('path');
module.exports = {
entry: slsw.lib.entries,
output: {
libraryTarget: 'commonjs',
path: path.join(__dirname, '.webpack'),
filename: '[name].js'
},
optimization: {
minimize: false
},
target: 'node',
mode: slsw.lib.webpack.isLocal ? "development" : "production",
externals: [
nodeExternals()
],
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: 'babel-loader',
}
],
include: __dirname,
exclude: /node_modules/
}
]
}
};
-
使用
npm install安装依赖项。 -
使用
npm test运行测试。 -
查看
.serverless目录中生成的内容,注意每个函数的 ZIP 文件,然后解压每个文件以查看其内容。 -
部署堆栈,
npm run dp:lcl -- -s $MY_STAGE。 -
在 AWS 控制台中查看堆栈和资源,并在 Lambda 控制台中注意代码大小。
-
最后,一旦完成
npm run rm:lcl -- -s $MY_STAGE,请删除堆栈。
它是如何工作的…
我们调整函数即服务(FaaS)的最明显旋钮是memorySize分配。这个设置也驱动着价格计算。不幸的是,价格的相关性往往具有反直觉性,可能会导致增加成本的同时降低性能的决策。AWS Lambda 的价格机制是这样的:如果你将内存分配加倍,但相应地将执行时间减半,价格保持不变。由此推论,如果你将内存分配减半,相应地将执行时间加倍,你将花费相同数量的钱,但性能却更低。这是因为内存分配实际上与函数执行的机器实例大小相关。更多的内存分配意味着函数也将拥有更快的 CPU 和更多的网络 IO 吞吐量。简而言之,不要在内存分配上节省。
函数即服务的主要担忧和混淆来源是冷启动时间。对于异步函数,例如处理 Kinesis 流,冷启动时间并不那么令人担忧。这是因为冷启动频率较低,因为函数通常在每个分片上重复使用数小时。然而,最小化 API 网关后面同步函数的冷启动时间非常重要,因为需要启动多个函数来适应并发负载。因此,冷启动时间可能会影响最终用户的使用体验。
影响冷启动时间的第一个因素是必须下载到容器中的函数包的大小。这也是为什么分配更多内存和更多的网络 IO 吞吐量可以提高函数性能的一个原因。同时,最小化必须下载的包的大小也很重要。我们将在稍后讨论如何使用 webpack 来优化 JavaScript 函数。
影响冷启动时间的下一个因素是语言或运行时的选择。脚本语言,如 JavaScript 和 Python,在启动时做的工作很少,因此对冷启动时间的影响很小。相反,Java 必须在启动时执行工作来加载类和准备 JVM。随着类数量的增加,对冷启动的影响也增加。这导致下一个影响冷启动时间的因素:库和框架的选择,例如对象关系映射(ORM)和依赖注入框架,以及连接池库。这些库和框架通常在启动时做大量工作,因为它们被设计成在长时间运行的服务器上工作。
在使用 Java 编写的函数中,FaaS 开发者面临的一个常见问题是提高 Spring 和 Hibernate 函数的冷启动时间;然而,这些工具最初并不是为 FaaS 设计的。我使用 Java 编程已经超过 20 年,从它在 1990 年代初出现时开始。起初我对转向 JavaScript 表示怀疑,但这个食谱证明了它与云原生和无服务器架构的契合度。然而,值得注意的是,多语言编程是最好的策略;为特定服务使用合适的编程语言,但了解在使用 Faas 时其带来的影响。
为了最小化 JavaScript 函数的包大小,我们利用 webpack,原因与我们在浏览器中用于最小化下载的原因相同。Webpack 执行摇树优化,移除未使用的代码以减小包大小。在 serverless.yml 文件中,我们包括 serverless-webpack 插件并配置它以单独打包函数。单独打包函数使我们能够最大化摇树优化的好处。webpack.config.js 文件进一步控制打包过程。serverless-webpack 插件提供了 slsw.lib.entries 工具,这样我们就不需要重复函数名称来定义所有的 entry 点。我们还关闭了 minimize 功能,这会使代码变得难以阅读。我们这样做是为了避免包含源映射进行调试,这会显著增加包大小。我们还排除了 node_modules 文件夹中的所有外部库,并配置插件为 includeModules,这包括那些实际用作运行时的库。一个特殊的例外是 aws-sdk 模块,因为它已经在函数容器中可用,所以永远不会被包含。最终结果是包含仅必要内容的精简函数包。
批处理请求
流处理器的设计必须考虑到它将接收的数据量。数据应实时处理,处理器不应落后。以下菜谱演示了如何使用 DynamoDB 批写入来确保足够的吞吐量。
准备工作
在开始此菜谱之前,您需要一个 AWS Kinesis Stream,例如在 创建事件流 菜谱中创建的那个。
如何做到这一点…
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch9/frp-batching --path cncb-frp-batching
-
导航到
cncb-frp-batching目录,cd cncb-frp-batching。 -
查看名为
serverless.yml的文件。 -
查看名为
handler.js的文件,其内容如下:
module.exports.listener = (event, context, cb) => {
_(event.Records)
.map(recordToUow)
.filter(forPurple)
.ratelimit(Number(process.env.WRITE_CAPACITY) /
Number(process.env.SHARD_COUNT) /
Number(process.env.WRITE_BATCH_SIZE) / 10, 100)
.batch(Number(process.env.WRITE_BATCH_SIZE))
.map(batchUow)
.flatMap(batchWrite)
.collect().toCallback(cb);
};
const batchUow = batch => ({ batch });
const batchWrite = batchUow => {
batchUow.params = {
RequestItems: {
[process.env.TABLE_NAME]: batchUow.batch.map(uow =>
({
PutRequest: {
Item: uow.event
}
})
)
},
};
...
return _(db.batchWrite(batchUow.params).promise()
.then(data => (
Object.keys(data.UnprocessedItems).length > 0 ?
Promise.reject(data) :
batchUow
))
.catch(err => {
err.uow = batchUow;
throw err;
})
);
};
-
使用
npm install安装依赖项。 -
使用
npm test -- -s $MY_STAGE运行测试。 -
查看在
.serverless目录中生成的内容。 -
使用以下命令部署堆栈,
npm run dp:lcl -- -s $MY_STAGE。 -
在 AWS 控制台中查看堆栈和资源。
-
使用以下命令调用
simulate函数:
$ sls invoke -f simulate -r us-east-1 -s $MY_STAGE
[
{
"total": 3850,
"orange": 942,
"purple": 952,
"blue": 1008,
"green": 948
}
]
- 查看以下
listener函数日志:
$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter 'event count' 2018-08-04 23:46:53 ... event count: 100
2018-08-04 23:46:57 ... event count: 1000
2018-08-04 23:47:23 ... event count: 250
2018-08-04 23:47:30 ... event count: 1000
2018-08-04 23:47:54 ... event count: 1000
2018-08-04 23:48:18 ... event count: 500$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter 'Duration' REPORT ... Duration: 3688.65 ms ...
REPORT ... Duration: 25869.08 ms ...
REPORT ... Duration: 7293.39 ms ...
REPORT ... Duration: 23662.65 ms ...
REPORT ... Duration: 24752.11 ms ...
REPORT ... Duration: 13983.72 ms ...
$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter 'retries' ...
2018-08-04 23:48:20 ... [AWS dynamodb 200 0.031s 0 retries] batchWriteItem({ RequestItems:
{ 'john-cncb-frp-batching-things':
[ { PutRequest:
{ Item:
{ id: { S: '320e6023-9862-11e8-b0f6-01e9feb460f5' },
type: { S: 'purple' },
timestamp: { N: '1533440814882' },
partitionKey: { S: '5' },
tags: { M: { region: { S: 'us-east-1' } } } } } },
{ PutRequest:
{ Item:
{ id: { S: '320e6025-9862-11e8-b0f6-01e9feb460f5' },
type: { S: 'purple' },
timestamp: { N: '1533440814882' },
partitionKey: { S: '1' },
tags: { M: { region: { S: 'us-east-1' } } } } } },
...
[length]: 10 ] },
ReturnConsumedCapacity: 'TOTAL',
ReturnItemCollectionMetrics: 'SIZE' })
...
- 最后,使用
npm run rm:lcl -- -s $MY_STAGE删除堆栈。
它是如何工作的…
如果流处理器接收一批 1,000 个事件,它是否需要向数据库发出 1,000 次请求或只需 100 次请求才能执行得更快?答案当然取决于许多变量,但一般来说,通过网络进行更少的调用更好,因为它最小化了网络延迟的影响。为此,像 DynamoDB 和 Elasticsearch 这样的服务提供了 API,允许在单个请求中提交命令批处理。在这个配方中,我们使用 DynamoDB 的batchWrite操作。为了准备一个批处理,我们只需在管道中添加一个batch步骤并指定WRITE_BATCH_SIZE。这种性能提升非常简单添加,但重要的是要记住,批处理请求会增加 DynamoDB 写入容量消耗的速度。因此,有必要在ratelimit计算中包含WRITE_BATCH_SIZE并相应地增加写入容量,正如在实现背压和速率限制配方中讨论的那样。
另一个需要注意的重要事项是,这些批处理请求不被视为单个事务。某些命令可能在单个请求中成功,而其他命令可能失败;因此,有必要检查响应中的UnprocessedItems,这些项目需要重新提交。在这个配方中,我们将每个批处理视为一个工作单元(uow)并引发整个批次的故障,正如在处理故障配方中讨论的那样。在调整逻辑以仅重试失败的命令之前,这是一个良好的、安全的地方。请注意,最终,你只有在尝试了最大重试次数后才会引发故障。
利用异步非阻塞 IO
流处理器的设计必须考虑到它将接收的数据量。数据应实时处理,处理器不应落后。以下配方演示了如何利用异步、非阻塞 IO 并行处理数据,以确保足够的吞吐量。
如何操作…
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch9/frp-async-non-blocking-io --path cncb-frp-async-non-blocking-io
-
导航到
cncb-frp-async-non-blocking-io目录,cd cncb-frp-async-non-blocking-io。 -
检查名为
serverless.yml的文件。 -
检查以下内容的
handler.js文件:
module.exports.listener = (event, context, cb) => {
_(event.Records)
.map(recordToUow)
.filter(forPurple)
.ratelimit(Number(process.env.WRITE_CAPACITY) /
Number(process.env.SHARD_COUNT) /
Number(process.env.PARALLEL) / 10, 100)
.map(put)
.parallel(Number(process.env.PARALLEL))
.collect().toCallback(cb);
};
-
使用
npm install安装依赖项。 -
使用
npm test -- -s $MY_STAGE运行测试。 -
检查
.serverless目录中生成的内容。 -
部署堆栈
npm run dp:lcl -- -s $MY_STAGE。 -
在 AWS 控制台中检查堆栈和资源。
-
使用以下命令调用
simulate函数:
$ sls invoke -f simulate -r us-east-1 -s $MY_STAGE
[
{
"total": 4675,
"blue": 1136,
"green": 1201,
"purple": 1167,
"orange": 1171
}
]
- 查看以下
listener函数日志:
$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter 'event count' 2018-08-05 00:03:05 ... event count: 1675
2018-08-05 00:03:46 ... event count: 1751
2018-08-05 00:04:34 ... event count: 1249$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter 'Duration' REPORT ... Duration: 41104.28 ms ...
REPORT ... Duration: 48312.47 ms ...
REPORT ... Duration: 31450.13 ms ...
$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter 'retries' ...
2018-08-05 00:04:58.034 ... [AWS dynamodb 200 0.024s 0 retries] ...
2018-08-05 00:04:58.136 ... [AWS dynamodb 200 0.022s 0 retries] ...
2018-08-05 00:04:58.254 ... [AWS dynamodb 200 0.034s 0 retries] ...
2018-08-05 00:04:58.329 ... [AWS dynamodb 200 0.007s 0 retries] ...
2018-08-05 00:04:58.430 ... [AWS dynamodb 200 0.007s 0 retries] ...
2018-08-05 00:04:58.540 ... [AWS dynamodb 200 0.015s 0 retries] ...
2018-08-05 00:04:58.661 ... [AWS dynamodb 200 0.035s 0 retries] ...
2018-08-05 00:04:58.744 ... [AWS dynamodb 200 0.016s 0 retries] ...
2018-08-05 00:04:58.843 ... [AWS dynamodb 200 0.014s 0 retries] ...
2018-08-05 00:04:58.953 ... [AWS dynamodb 200 0.023s 0 retries] ...
...
- 最后,一旦你完成了
npm run rm:lcl -- -s $MY_STAGE,请移除堆栈。
它是如何工作的…
异步非阻塞 I/O 对于最大化吞吐量至关重要。没有它,流处理器将阻塞并无所事事,直到外部调用完成。这个配方演示了如何使用 parallel 步骤来控制可以执行的并发调用数量。作为一个例子,我曾经有一个从 S3 读取的脚本,处理时间超过一个小时,但一旦我添加了一个设置为 16 的 parallel 步骤,脚本只用了五分钟就执行完毕。改进如此显著,Datadog 几乎立即联系我,看看我们是否有失控的进程。
为了允许并发调用,我们只需在外部调用步骤之后将 parallel 步骤添加到管道中,并指定 PARALLEL 数量。这种性能改进非常简单添加,但重要的是要记住,并行请求会增加 DynamoDB 写入容量消耗的速度。因此,有必要在 ratelimit 计算中包含 PARALLEL 数量,并相应地增加写入容量,正如在 实现背压和速率限制 配方中讨论的那样。通过结合并行执行、分组和批量,可以实现进一步的性能改进。
流处理器中的事件分组
流处理器的设计必须考虑到它将接收的数据量。数据应实时处理,处理器不应落后。以下配方演示了如何通过在流中将相关数据分组来确保足够的吞吐量。
如何操作…
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch9/frp-grouping --path cncb-frp-grouping
-
导航到
cncb-frp-grouping目录,cd cncb-frp-grouping。 -
查看名为
serverless.yml的文件。 -
查看以下内容的
handler.js文件:
module.exports.listener = (event, context, cb) => {
_(event.Records)
.map(recordToUow)
.filter(forPurple)
.group(uow => uow.event.partitionKey)
.flatMap(groupUow)
.ratelimit(Number(process.env.WRITE_CAPACITY) /
Number(process.env.SHARD_COUNT) / 10, 100)
.flatMap(put)
.collect().toCallback(cb);
};
const groupUow = groups => _(Object.keys(groups).map(key => ({ batch: groups[key]})));
const put = groupUow => {
const params = {
TableName: process.env.TABLE_NAME,
Item: groupUow.batch[groupUow.batch.length - 1].event, // last
};
...
return _(db.put(params).promise()
.then(() => groupUow)
);
};
-
使用
npm install安装依赖。 -
使用
npm test -- -s $MY_STAGE运行测试。 -
查看在
.serverless目录中生成的内容。 -
部署堆栈
npm run dp:lcl -- -s $MY_STAGE。 -
在 AWS 控制台中查看堆栈和资源。
-
使用以下命令调用
simulate函数:
$ sls invoke -f simulate -r us-east-1 -s $MY_STAGE
[
{
"total": 4500,
"blue": 1134,
"green": 1114,
"purple": 1144,
"orange": 1108
}
]
- 查看以下
listener函数日志:
$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter 'event count' 2018-08-05 00:28:19 ... event count: 1000
2018-08-05 00:28:20 ... event count: 1000
2018-08-05 00:28:21 ... event count: 650
2018-08-05 00:28:22 ... event count: 1000
2018-08-05 00:28:23 ... event count: 850$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter 'Duration' REPORT ... Duration: 759.50 ms ...
REPORT ... Duration: 611.70 ms ...
REPORT ... Duration: 629.91 ms ...
REPORT ... Duration: 612.90 ms ...
REPORT ... Duration: 623.11 ms ...
$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter 'retries' 2018-08-05 00:28:20.197 ... [AWS dynamodb 200 0.112s 0 retries] ...
2018-08-05 00:28:20.320 ... [AWS dynamodb 200 0.018s 0 retries] ...
...
2018-08-05 00:28:23.537 ... [AWS dynamodb 200 0.008s 0 retries] ...
2018-08-05 00:28:23.657 ... [AWS dynamodb 200 0.019s 0 retries] ...
- 最后,一旦完成,使用
npm run rm:lcl -- -s $MY_STAGE删除堆栈。
它是如何工作的…
批量请求 配方演示了如何通过将多个不相关的命令批量到一个请求中,以最小化网络 I/O 的开销。另一种减少网络 I/O 的方法是简单地减少需要执行的命令数量,通过分组相关事件,并且每组只执行一个命令。例如,我们可能对每个组进行计算,或者只是采样一些数据。在这个配方中,我们根据 partitionKey 对事件进行分组。我们可以根据事件中的任何数据进行分组,但最佳结果是在分组相对于分区键时实现;这是因为分区键确保相关事件被发送到同一个分片。
group 步骤使得根据事件的内容将相关事件归组变得简单。对于更复杂的逻辑,可以直接使用 reduce 步骤。接下来,我们将每个组映射到一个必须成功或失败一起的工作单元(groupUow),正如在“处理故障”配方中讨论的那样。最后,如前例所示,我们写入每个组的最后一个事件。从日志中注意,分组导致写入次数显著减少;对于这次特定的运行,模拟了 4,500 个事件,但只有 25 次写入。通过结合分组、批处理和并行调用,可以进一步提高性能。
自动扩展 DynamoDB
云原生,使用 FaaS 和无服务器架构,可以最小化支持服务层的基础设施扩展所需的努力。然而,我们现在需要关注调整流处理器并最小化对目标数据存储的任何限制。以下配方演示了如何使用 DynamoDB 自动扩展来确保分配足够的容量以提供足够的吞吐量并避免限制。
如何操作…
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch9/dynamodb-autoscaling --path cncb-dynamodb-autoscaling
-
导航到
cncb-dynamodb-autoscaling目录,cd cncb-dynamodb-autoscaling。 -
使用以下内容审查名为
serverless.yml的文件:
service: cncb-dynamodb-autoscaling
...
plugins:
- serverless-dynamodb-autoscaling-plugin
custom:
autoscaling:
- table: Table
write:
minimum: 5
maximum: 50
usage: 0.6
actions:
- name: morning
minimum: 5
maximum: 50
schedule: cron(0 6 * * ? *)
- name: night
minimum: 1
maximum: 1
schedule: cron(0 0 * * ? *)
read:
...
resources:
Resources:
Table:
Type: AWS::DynamoDB::Table
...
-
审查名为
handler.js的文件。 -
使用以下命令安装依赖项:
npm install。 -
使用以下命令运行测试:
npm test -- -s $MY_STAGE。 -
审查
.serverless目录中生成的内容。 -
使用以下命令部署堆栈:
npm run dp:lcl -- -s $MY_STAGE。 -
在 AWS 控制台中审查堆栈和资源。
-
使用以下命令多次调用
simulate函数以触发自动扩展:sls invoke -f simulate -r us-east-1 -s $MY_STAGE。 -
使用以下命令查看以下“监听器”函数日志:
$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter 'event count'
$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter 'Duration'
$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter '"1 retries"'$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter '"2 retries"'
$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter '"3 retries"'
$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter '"4 retries"'
$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter '"5 retries"'
$ sls logs -f listener -r us-east-1 -s $MY_STAGE --filter '"6 retries"'
-
在 AWS 控制台的 DynamoDB 指标选项卡上查看“写入容量”和“受限写入请求数量”指标,以查看自动扩展增量是否满足需求,然后在夜间缩小规模,并在早晨再次扩大。
-
最后,一旦完成,使用以下命令删除堆栈:
npm run rm:lcl -- -s $MY_STAGE。
它是如何工作的…
在“实现背压和速率限制”配方中,我们看到流处理器最小化限制以最大化吞吐量的重要性。在本章中,我们讨论了优化吞吐量的技术,例如批处理、分组和异步非阻塞请求,这些都增加了必须分配的数据存储容量。然而,虽然我们需要确保有足够的容量,但我们还希望最小化浪费的容量,自动扩展帮助我们实现这一点。自动扩展可以解决随着时间的推移而增长到预期峰值的需求,如已知事件的可预测需求,以及不可预测的需求。
在本配方中,我们使用 serverless-dynamodb-autoscaling-plugin 在每个表的基础上定义 autoscaling 策略。对于 read 和 write 容量,我们指定 minimum 和 maximum 容量和期望的 usage 百分比。这个 usage 百分比定义了我们希望拥有的额外空间量,这样我们就可以提前增加容量,以确保在达到 100% 利用率并开始节流之前分配额外的容量。我们还可以在特定时间安排自动扩展 actions。在本配方中,我们在夜间缩小规模以最小化浪费,然后在典型需求到来之前在早上恢复规模。
利用缓存控制
自主、云原生服务维护自己的物化视图,并将复制的数据存储在高度可用且性能极优的云原生数据库中。当与 API 网关和 FaaS 的性能结合时,通常没有必要添加传统的缓存机制来实现面向用户的 前端后端(BFF)服务的预期性能。但话虽如此,这并不意味着我们不应该利用已经包装服务的 CDN,例如 CloudFront。因此,以下配方将向您展示如何利用缓存控制头和利用 CDN 来提高最终用户性能,同时减少对服务的负载。
如何操作…
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch9/cache-control --path cncb-cache-control
-
导航到
cncb-cache-control目录,cd cncb-cache-control。 -
检查名为
serverless.yml的文件。 -
检查名为
handler.js的文件,其内容如下:
module.exports.get = (request, context, callback) => {
const response = {
statusCode: 200,
headers: {
'Cache-Control': 'max-age=5',
},
body: ...,
};
callback(null, response);
};
module.exports.save = (request, context, callback) => {
const response = {
statusCode: 200,
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
},
};
callback(null, response);
};
-
使用
npm install安装依赖项。 -
使用
npm test运行测试。 -
检查
.serverless目录中生成的内容。 -
按照以下步骤部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
...
Stack Outputs
ApiDistributionEndpoint: https://d2thj6o092tkou.cloudfront.net
...
部署 CloudFront 分发通常需要超过 20 分钟。
-
在 AWS 控制台中检查堆栈和资源。
-
使用以下
curl命令多次调用堆栈输出中显示的端点,以查看缓存结果的性能差异:
$ curl -s -D - -w "Time: %{time_total}" -o /dev/null https://<see stack output>.cloudfront.net/things/123 | egrep -i 'X-Cache|Time'
X-Cache: Miss from cloudfront
Time: 0.712
$ curl ...
X-Cache: Hit from cloudfront
Time: 0.145
$ curl -v -X POST -H 'Content-Type: application/json' -d '{"name":"thing 1"}' https://<see stack output>.cloudfront.net/things ...
< HTTP/1.1 200 OK
< Cache-Control: no-cache, no-store, must-revalidate
< X-Cache: Miss from cloudfront
...
- 最后,使用
npm run rm:lcl -- -s $MY_STAGE删除堆栈。
它是如何工作的…
在这个相关的配方中,我们关注等式的服务端。云原生数据库,如 DynamoDB,响应时间在低 10 毫秒,AWS API 网关和 AWS Lambda 对于 BFF 服务的整体延迟通常在低 100 毫秒。只要数据库容量设置得当且节流最小化,就很难从最终用户的角度观察到明显的性能提升。真正改善这一点的唯一方法就是根本不需要发出请求。
这是一个云原生可能真正反直觉的案例。传统上,为了提高性能,我们需要增加基础设施的数量,并在代码和数据库之间添加一个昂贵的缓存层。换句话说,我们需要花费更多的钱来提高性能。然而,在这个菜谱中,我们利用了一个极低成本的边缘缓存,既提高了性能,又降低了成本。通过向我们的响应添加 Cache-Control 头部,例如 max-age,我们可以告诉浏览器不要重复请求,并告诉 CDN 为其他用户重用响应。因此,我们减少了 API 网关和函数的负载,并减少了数据库所需的容量,从而降低了所有这些服务的成本。明确控制哪些操作应该存储 no-cache,例如 PUT、POST 和 DELETE 方法,也是一种良好的实践。
利用会话一致性
云原生前端应用程序的设计必须考虑到系统最终应该是一致的。例如,在一个传统的前端应用程序中,保存数据然后立即执行查询以检索相同的数据并不罕见。然而,在一个最终一致性的系统中,查询第一次尝试很可能找不到数据。相反,云原生前端利用了单页应用程序至少可以缓存数据的事实,以用户会话的持续时间为限。这种方法被称为会话一致性。以下菜谱演示了如何使用流行的 Apollo Client (www.apollographql.com/client) 与 ReactJS 结合使用,通过实现 会话一致性 来提高感知性能并减少系统负载。
如何做到这一点…
- 从以下模板创建
service和spa项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch9/session-consistency/spa --path cncb-session-consistency-spa
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch9/session-consistency/service --path cncb-session-consistency-service
- 使用以下命令部署
service:
$ cd ./cncb-session-consistency-service
$ npm install
$ npm run dp:lcl -- -s $MY_STAGE
-
导航到
cncb-session-consistency-spa目录,cd ../cncb-session-consistency-spa。 -
查看以下内容的
src/index.js文件,并根据service堆栈输出的值更新uri,如下所示:
...
const client = new ApolloClient({
link: new HttpLink({
// CHANGE ME
uri: 'https://*<API_ID>*.execute-api.us-east-1.amazonaws.com/*<STAGE>*/graphql',
}),
cache: new InMemoryCache(),
});
...
- 查看名为
src/App.js的文件,其内容如下:
...
const AddThing = () => {
...
return (
<Mutation
mutation={SAVE_THING}
update={(cache, { data: { saveThing } }) => {
const { things } = cache.readQuery({ query: GET_THINGS });
cache.writeQuery({
query: GET_THINGS,
data: { things: { items: things.items.concat([saveThing]) } }
});
}}
>
...
</Mutation>
);
};
...
-
使用
npm install安装依赖项。 -
使用
npm start在本地运行应用程序。 -
浏览到
http://localhost:3000。 -
添加和更新一些内容,注意查询结果是如何更新的。
-
最后,一旦完成,按照以下方式删除服务堆栈:
$ cd ../cncb-session-consistency-service
$ npm run rm:lcl -- -s $MY_STAGE
它是如何工作的…
在这个菜谱中,我们使用了一个类似于我们在 实现一个 GraphQL CRUD BFF 菜谱中创建的 GraphQL BFF。这里的重点是前端应用程序,我们使用 ReactJS 和 Apollo Client 创建它,并且特别关注如何缓存我们与服务的交互。首先,我们在 src/index.js 文件中创建 ApolloClient 并用服务的端点和最重要的 InMemoryCache 对象初始化它。
接下来,我们在src/App.js文件中实现用户界面。屏幕显示从things查询返回的项目列表。Apollo 客户端将自动缓存查询的结果。更新单个对象的突变将自动更新缓存并触发屏幕重新渲染。请注意,添加新数据需要更多努力。《AddThing》函数使用突变的update功能来保持缓存同步并触发重新渲染。update函数接收对缓存的引用和突变返回的对象。然后我们调用readQuery从缓存中读取查询,将新对象追加到查询结果中,并最终通过调用writeQuery来更新缓存。
最终结果是用户体验非常低延迟,因为我们正在优化执行请求的数量、传输的数据量以及使用的内存量。最重要的是,对于新数据和更新数据,我们不会丢弃客户端创建的任何内容,并用相同的检索值来替换它——毕竟,这只是不必要的劳动。我们已经有数据了,为什么还要丢弃它并再次检索呢?我们也无法确定服务端的数据是一致的——除非我们执行一个更慢、成本更高的连续读取,正如所述,这是不必要的。在区域故障的情况下,会话一致性对于多区域部署变得更加有价值。正如我们将在第十章“部署到多个区域”中讨论的,最终一致性的云原生系统对区域故障非常宽容,因为它们已经对最终一致性有容忍度,这在故障转移期间可能会更加持久。因此,会话一致性有助于使区域故障对最终用户透明。对于在区域故障期间必须保持可用的任何数据,我们可以将会话一致性进一步推进,并将用户会话持久化到本地存储。
第十章:部署到多个区域
在本章中,将涵盖以下食谱:
-
实现基于延迟的路由
-
创建区域健康检查
-
触发区域故障转移
-
使用 DynamoDB 实现区域复制
-
实现轮询复制
简介
并不是某个云提供商是否会经历一个值得报道的区域中断的问题,而是何时会发生。这是不可避免的。根据我的经验,这种情况大约每两年发生一次。当这种事件发生时,许多系统没有其他选择,在发生中断期间变得不可用,因为它们只设计为在单个区域内的多个可用区工作。与此同时,其他系统几乎不会在可用性方面出现任何波动,因为它们已经被设计为跨多个区域运行。底线是,真正的云原生系统利用区域隔舱,并在多个区域运行。幸运的是,我们利用了已经跨可用区运行的完全托管、增值云服务。这使团队能够将精力重新集中在创建一个活动-活动、多区域系统上。本章中的食谱从三个相互关联的视角涵盖了多区域主题——同步请求、数据库复制和异步事件流。
实现基于延迟的路由
许多系统会做出有意识的决策,不跨多个区域运行,因为这根本不值得额外的努力和成本。当以活动-被动模式运行时,这一点完全可以理解,因为额外的努力不会产生容易看到的效益,直到发生区域中断。当以活动-活动模式运行时,它将每月运行成本加倍,这也是可以理解的。相反,无服务器云原生系统可以轻松部署到多个区域,并且由于交易量成本在各个区域之间分散,成本的增加是微不足道的。本食谱演示了如何在多个区域运行 AWS API Gateway 和基于 Lambda 的服务,并利用 Route53 在这些活动-活动区域之间路由流量以最小化延迟。
准备工作
您需要一个已注册的域名和一个Route53 托管区域,您可以在本食谱中使用它来为将要部署的服务创建子域名,例如我们在将自定义域名与 CDN 关联食谱中讨论的那样。您还需要在us-east-1和us-west-2区域为您的域名获取通配符证书,正如我们在创建用于传输加密的 SSL 证书食谱中讨论的那样。
如何操作…
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch10/latency-based-routing --path cncb-latency-based-routing
-
使用
cd cncb-latency-based-routing导航到cncb-latency-based-routing目录。 -
查看名为
serverless.yml的文件,其内容如下:
service: cncb-latency-based-routing
plugins:
- serverless-multi-regional-plugin
provider:
...
endpointType: REGIONAL
custom:
dns:
hostedZoneId: ZXXXXXXXXXXXXX
domainName: ${self:service}.example.com
regionalDomainName: ${opt:stage}-${self:custom.dns.domainName}
us-east-1:
acmCertificateArn: arn:aws:acm:us-east-1:xxxxxxxxxxxx:certificate/...
us-west-2:
acmCertificateArn: arn:aws:acm:us-west-2:xxxxxxxxxxxx:certificate/...
cdn:
region: us-east-1
aliases:
- ${self:custom.dns.domainName}
acmCertificateArn: ${self:custom.dns.us-east-1.acmCertificateArn}
functions:
hello:
...
-
更新
serverless.yml文件中的以下字段:-
custom.dns.hostedZoneId -
custom.dns.domainName -
custom.dns.us-east-1.acmCertificateArn -
custom.dns.us-west-2.acmCertificateArn
-
-
审查名为
handler.js的文件。 -
使用
npm install安装依赖项。 -
使用
npm test运行测试。 -
审查
.serverless目录中生成的内容。 -
使用
npm run dp:lcl:**w** -- -s $MY_STAGE在us-west-2区域部署堆栈。 -
在 AWS 控制台中审查
us-west-2区域的堆栈和资源。 -
测试服务的
regional端点并注意负载中返回的区域:
$ curl -v https://$MY_STAGE-cncb-latency-based-routing.example.com/$MY_STAGE/hello
{"message":"Your function executed successfully in us-west-2!"}
- 使用
npm run dp:lcl:**e** -- -s $MY_STAGE在us-east-1区域部署堆栈。
部署 CloudFront 分发可能需要 20 分钟或更长时间。
-
在 AWS 控制台中审查
us-east-1区域的堆栈和资源。 -
测试服务的
global端点并注意负载中返回的区域,如果你不是最接近us-west-2区域,则应与上述不同:
$ curl -v https://cncb-regional-failover-service.example.com/hello
{"message":"Your function executed successfully in us-east-1!"}
- 完成后删除堆栈:
$ npm run rm:lcl:e -- -s $MY_STAGE
$ npm run rm:lcl:w -- -s $MY_STAG
它是如何工作的…
在这个菜谱中,我们正在将单个服务部署到两个区域——us-east-1和us-west-2。从 API 网关和 Lambda 函数的角度来看,我们只是在每个区域创建两个不同的 CloudFormation 堆栈。我们有两个脚本——dp:lcl:**e**和dp:lcl:**w**,这两个脚本之间的唯一区别是它们指定了不同的区域。因此,部署到两个区域的努力微乎其微,而且没有额外的成本,因为我们只按交易付费。在serverless.yml文件中需要注意的一点是,我们正在为 API 网关定义endpointType为REGIONAL,这将使我们能够利用 Route53 的区域路由功能:
如前图所示,我们需要配置 Route53 在两个区域之间执行基于延迟的路由。这意味着 Route53 将请求路由到请求者最近的位置。serverless-multi-regional-plugin封装了这些配置的大部分细节,因此我们只需要在custom.dns下指定变量。首先,我们提供托管顶级域名(例如example.com)的区域的hostedZoneId。接下来,我们定义将用作通过CloudFront访问服务的全局alias的domainName。为此,我们使用服务名称(即${self:service})作为顶级域名的子域名来唯一标识一个服务。
我们还需要定义一个 regionalDomainName,以便在所有区域提供一个通用名称,这样 CloudFront 就可以依赖 Route53 来选择最佳的访问区域。为此,我们使用阶段(即 ${opt:stage}-${self:custom.dns.domainName})作为前缀,并请注意,我们将其与破折号连接,以便与简单的通配符证书(如 *.example.com)一起使用。区域 acmCertificateArn 变量指向每个区域中你的通配符证书的副本,如 准备就绪 部分所述。API Gateway 要求证书位于与服务相同的区域。CloudFront 要求证书位于 us-east-1 区域。CloudFront 是一项全球服务,因此我们只需要从 us-east-1 区域部署 CloudFront 分发。
所有对全局端点(service.example.com)的请求都将路由到最近的 CloudFront 边缘位置。CloudFront 然后将请求转发到区域端点(stage-service.example.com),Route53 将请求路由到最近的区域。一旦请求进入一个区域,对服务(如 Lambda、DynamoDB 和 Kinesis)的所有请求都将保持在区域内,以最小化延迟。所有状态更改都将复制到其他区域,正如我们在 使用 DynamoDB 实现区域复制 和 实现轮询复制 配方中讨论的那样。
我强烈建议查看生成的 CloudFormation 模板,以了解所有创建的资源细节。
创建区域健康检查
云原生系统中的健康检查与传统健康检查的关注点不同。传统的健康检查在实例级别运行,以确定集群中何时需要替换特定的实例。然而,云原生系统使用完全托管、增值的云服务,因此没有实例需要管理。这些无服务器功能提供了特定区域内可用区的高可用性。因此,云原生系统可以专注于在区域之间提供高可用性。本配方演示了如何断言给定区域内使用的增值云服务的状态。
准备就绪
要完整完成此配方,您需要一个 Pingdom (www.pingdom.com) 账户。
如何操作…
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch10/regional-health-check --path cncb-regional-health-check
-
使用
cd cncb-regional-health-check命令导航到cncb-regional-health-check目录。 -
查看名为
serverless.yml的文件,其内容如下:
service: cncb-regional-health-check
provider:
name: aws
runtime: nodejs8.10
endpointType: REGIONAL
iamRoleStatements:
...
functions:
check:
handler: handler.check
events:
- http:
path: check
method: get
environment:
UNHEALTHY: false
TABLE_NAME:
Ref: Table
resources:
Resources:
Table:
Type: AWS::DynamoDB::Table
...
- 查看名为
handler.js的文件,其内容如下:
module.exports.check = (request, context, callback) => {
Promise.all([readCheck, writeCheck])
.catch(handleError)
.then(response(callback));
};
const db = new aws.DynamoDB.DocumentClient({
httpOptions: { timeout: 1000 },
logger: console,
});
const readCheck = () => db.get({
TableName: process.env.TABLE_NAME,
Key: {
id: '1',
},
}).promise();
const writeCheck = () => db.put({
TableName: process.env.TABLE_NAME,
Item: {
id: '1',
},
}).promise();
const handleError = (err) => {
console.error(err);
return true; // unhealthy
};
const response = callback => (unhealthy) => {
callback(null, {
statusCode: unhealthy || process.env.UNHEALTHY === 'true' ? 503 : 200,
body: JSON.stringify({
timestamp: Date.now(),
region: process.env.AWS_REGION,
}),
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
},
});
};
-
使用
npm install安装依赖项。 -
使用
npm test运行测试。 -
查看在
.serverless目录中生成的内容。 -
在
us-east-1和us-west-2区域部署堆栈:
$ npm run dp:lcl:e -- -s $MY_STAGE
...
GET - https://0987654321.execute-api.us-east-1.amazonaws.com/john/check
$ npm run dp:lcl:w -- -s $MY_STAGE
...
GET - https://1234567890.execute-api.us-west-2.amazonaws.com/john/check
-
在 AWS 控制台中查看两个区域的堆栈和资源。
-
对于每个区域,调用堆栈输出中显示的端点,如下所示:
$ curl -v https://0987654321.execute-api.us-east-1.amazonaws.com/$MY_STAGE/check
$ curl -v https://1234567890.execute-api.us-west-2.amazonaws.com/$MY_STAGE/check
-
在您的 Pingdom 账户中为每个区域端点创建一个 Uptime 检查,间隔为
1 分钟。 -
完成后,请删除堆栈:
$ npm run rm:lcl:e -- -s $MY_STAGE
$ npm run rm:lcl:w -- -s $MY_STAGE
它是如何工作的…
传统的健康检查通常断言实例能够访问它操作所需的全部资源。区域健康检查做的是同样的事情,但是从整个区域的角度来看。它断言系统使用的所有增值云服务(即资源)在给定区域内都运行正常。如果任何一个资源不可用,我们将根据 触发区域故障转移 菜谱中的讨论,将整个区域故障转移。
健康检查服务被实现为一个基于 REGIONAL API 网关的服务,并部署到每个区域。然后我们需要定期在每个区域调用健康检查,以确认该区域是健康的。我们可以让 Route53 ping 这些区域端点,但它会频繁地 ping,以至于健康检查服务可能会成为你整个系统中最昂贵的服务。作为替代方案,我们可以使用外部服务,例如 Pingdom,在每个区域每分钟调用一次健康检查。对于许多系统来说,每分钟一次已经足够,但对于流量极高的系统,可能从 Route53 提供的更高频率中受益。
健康检查需要确认所需资源可用。健康检查本身隐含地确认 API 网关和 Lambda 服务可用,因为它建立在那些服务之上。对于所有其他资源,它将需要执行某种 ping 操作。在这个菜谱中,我们假设 DynamoDB 是所需资源。健康检查服务定义了自己的 DynamoDB 表,并在每次调用时执行 readCheck 和 writeCheck,以确认服务仍然可用。如果任一请求失败,则健康检查服务将失败并返回 503 状态码。为了测试,该服务提供了一个 UNHEALTHY 环境变量,可以用来模拟失败,我们将在下一个菜谱中使用它。
触发区域故障转移
正如我们讨论的,在 创建区域健康检查 菜谱中,我们的区域健康检查断言系统使用的所有完全管理、增值云服务都正常运行。当这些服务中的任何一个出现故障或错误率足够高时,最好是整个区域故障转移到下一个最佳活动区域。这个菜谱演示了如何使用 CloudWatch Alarms 将区域健康检查连接到 Route53,以便 Route53 可以将流量导向健康的区域。
准备工作
您需要一个已注册的域名和一个Route53 托管区域,您可以使用此配方为要部署的服务创建子域名,例如我们在将自定义域名与 CDN 关联配方中讨论的那样。您还需要在us-east-1和us-west-2区域为您的域名获取通配符证书,例如我们在创建用于传输加密的 SSL 证书配方中讨论的那样。
如何做到这一点…
- 从以下模板创建服务并检查项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch10/regional-failover/check --path cncb-regional-failover-check
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch10/regional-failover/service --path cncb-regional-failover-service
-
使用
cd cncb-regional-failover-check进入cncb-regional-failover-check目录。 -
查看名为
serverless.yml的文件,其内容如下:
service: cncb-regional-failover-check
...
functions:
check:
...
resources:
Resources:
Api5xxAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
Namespace: AWS/ApiGateway
MetricName: 5XXError
Dimensions:
- Name: ApiName
Value: ${opt:stage}-${self:service}
Statistic: Minimum
ComparisonOperator: GreaterThanThreshold
Threshold: 0
Period: 60
EvaluationPeriods: 1
ApiHealthCheck:
DependsOn: Api5xxAlarm
Type: AWS::Route53::HealthCheck
Properties:
HealthCheckConfig:
Type: CLOUDWATCH_METRIC
AlarmIdentifier:
Name:
Ref: Api5xxAlarm
Region: ${opt:region}
InsufficientDataHealthStatus: LastKnownStatus
Outputs:
ApiHealthCheckId:
Value:
Ref: ApiHealthCheck
-
查看名为
handler.js的文件。 -
使用
npm install安装依赖项。 -
使用
npm test运行测试。 -
查看在
.serverless目录中生成的内容。 -
在
us-east-1和us-west-2区域部署堆栈:
$ npm run dp:lcl:e -- -s $MY_STAGE
...
GET - https://0987654321.execute-api.us-east-1.amazonaws.com/john/check
$ npm run dp:lcl:w -- -s $MY_STAGE
...
GET - https://1234567890.execute-api.us-west-2.amazonaws.com/john/check
-
在 AWS 控制台中查看两个区域的堆栈和资源。
-
使用
cd cncb-regional-failover-service进入cncb-regional-failover-service目录。 -
查看名为
serverless.yml的文件,其内容如下:
service: cncb-regional-failover-service
plugins:
- serverless-multi-regional-plugin
...
custom:
dns:
...
us-east-1:
...
healthCheckId: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
us-west-2:
...
healthCheckId: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
...
-
在
serverless.yml文件中更新以下字段:-
custom.dns.hostedZoneId -
custom.dns.domainName -
custom.dns.us-east-1.acmCertificateArn -
从
east健康检查堆栈的输出中获取custom.dns.us-east-1.healthCheckId -
custom.dns.us-west-2.acmCertificateArn -
从
west健康检查堆栈的输出中获取custom.dns.us-west-2.healthCheckId
-
-
使用
npm install安装依赖项。 -
使用
npm test运行测试。 -
查看在
.serverless目录中生成的内容。 -
在
us-west-2和us-east-1区域部署堆栈:
$ npm run dp:lcl:w -- -s $MY_STAGE
$ npm run dp:lcl:e -- -s $MY_STAGE
部署 CloudFront 分发可能需要 20 分钟或更长时间。
-
在 AWS 控制台中查看两个区域的堆栈和资源。
-
测试服务的
global端点并注意负载中返回的区域:
$ curl -v https://cncb-regional-failover-service.example.com/hello
{"message":"Your function executed successfully in us-east-1!"}
-
前往 AWS 控制台中路由到您请求的区域对应的
cncb-regional-failover-check-<stage>-checkLambda 函数,将UNHEALTHY环境变量更改为true并保存函数。例如,前面的输出显示us-east-1,因此更新us-east-1中的函数。 -
在几分钟内多次调用该区域的健康检查端点以触发故障转移:
$ curl -v https://0987654321.execute-api.us-east-1.amazonaws.com/$MY_STAGE/check
- 测试服务的
global端点并注意区域已更改:
$ curl -v https://cncb-regional-failover-service.example.com/hello
{"message":"Your function executed successfully in us-west-2!"
-
在 AWS 控制台中查看 Route53 健康检查的状态。
-
完成后删除堆栈:
$ cd cncb-regional-failover-service
$ npm run rm:lcl:e -- -s $MY_STAGE
$ npm run rm:lcl:w -- -s $MY_STAGE
$ cd ../cncb-regional-failover-check
$ npm run rm:lcl:e -- -s $MY_STAGE
$ npm run rm:lcl:w -- -s $MY_STAGE
它是如何工作的…
我们的区域健康检查服务设计为当所需的一个或多个服务返回错误时返回5xx状态码。我们在健康检查服务中添加了一个名为Api5xxAlarm的 CloudWatch 警报,该警报监控给定区域的 API Gateway 5xxError指标,并在一分钟内至少有一个5xx时发出警报。您可能需要根据具体需求调整警报的灵敏度。接下来,我们向服务中添加了一个名为ApiHealthCheck的 Route53 健康检查,它依赖于Api5xxAlarm并输出ApiHealthCheckId供其他服务使用。最后,我们将healthCheckId与每个区域中每个服务的 Route53 RecordSet 关联起来,例如cncb-regional-failover-service。当警报状态为Unhealthy时,Route53 将停止将流量路由到该区域,直到状态再次变为Healthy。
在这个配方中,我们使用了UNHEALTHY环境变量来模拟区域故障,并手动调用服务以触发警报。正如我们在创建区域健康检查配方中讨论的那样,健康检查通常由另一个服务(如 Pingdom)定期调用,以确保区域健康状态有持续的流量。为了增加覆盖率,我们还可以通过从警报中删除ApiName维度来检查区域中所有服务的5xx指标,但仍依赖于 ping 健康检查服务来断言状态,当没有其他流量时。
在 DynamoDB 中实现区域复制
在区域故障发生时,及时跨区域复制数据对于提供无缝的用户体验非常重要。在正常执行期间,区域复制将在近实时发生。在区域故障期间,应预期数据复制会变慢。我们可以将其视为延期的最终一致性。幸运的是,我们的云原生系统被设计为最终一致性。这意味着它们可以容忍过时的数据,无论数据何时变得一致。本配方展示了如何创建全局表以跨区域复制 DynamoDB 表,并讨论了为什么我们不复制事件流。
准备工作
在开始此配方之前,您需要在us-east-1和us-west-2区域拥有一个 AWS Kinesis Stream,例如在创建事件流配方中创建的那个。
如何操作…
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch10/dynamodb-global-table --path cncb-dynamodb-global-table
-
使用
cd cncb-dynamodb-global-table命令导航到cncb-dynamodb-global-table目录。 -
查看名为
serverless.yml的文件,其内容如下:
service: cncb-dynamodb-global-table
...
plugins:
- serverless-dynamodb-autoscaling-plugin
- serverless-dynamodb-global-table-plugin
custom:
autoscaling:
- table: Table
global: true
read:
...
write:
...
resources:
Resources:
Table:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${opt:stage}-${self:service}-things
...
StreamSpecification:
StreamViewType: NEW_AND_OLD_IMAGES
- 查看名为
handler.js的文件,其内容如下:
...
module.exports.trigger = (event, context, cb) => {
_(event.Records)
.filter(forOrigin)
.map(toEvent)
.flatMap(publish)
.collect()
.toCallback(cb);
};
const forOrigin = e => e.dynamodb.NewImage['aws:rep:updateregion'] &&
e.dynamodb.NewImage['aws:rep:updateregion'].S === process.env.AWS_REGION;
...
-
使用
npm install命令安装依赖项。 -
使用
npm test -- -s $MY_STAGE命令运行测试。 -
查看
.serverless目录中生成的内容。 -
在
us-east-1和us-west-2区域部署堆栈:
$ npm run dp:lcl:e -- -s $MY_STAGE
...
Serverless: Created global table: john-cncb-dynamodb-global-table-things with region: us-east-1
...
$ npm run dp:lcl:w -- -s $MY_STAGE
...
Serverless: Updated global table: john-cncb-dynamodb-global-table-things with region: us-west-2
...
-
在两个区域的 AWS 控制台中查看栈和资源。
-
调用
command函数将数据保存到us-east-1区域:
$ sls invoke -f command -r us-east-1 -s $MY_STAGE -d '{"id":"77777777-4444-1111-1111-111111111111","name":"thing one"}'
- 调用
query函数从us-west-2区域检索数据:
$ sls invoke -f query -r us-west-2 -s $MY_STAGE -d 77777777-4444-1111-1111-111111111111
{
"Item": {
"aws:rep:deleting": false,
"aws:rep:updateregion": "us-east-1",
"aws:rep:updatetime": 1534819304.087001,
"id": "77777777-4444-1111-1111-111111111111",
"name": "thing one",
"latch": "open"
}
}
- 查看两个区域中
trigger和listener函数的日志:
$ sls logs -f trigger -r us-east-1 -s $MY_STAGE
$ sls logs -f listener -r us-east-1 -s $MY_STAGE
$ sls logs -f trigger -r us-west-2 -s $MY_STAGE
$ sls logs -f listener -r us-west-2 -s $MY_STAGE
- 完成后,删除两个栈:
$ npm run rm:lcl:e -- -s $MY_STAGE
$ npm run rm:lcl:w -- -s $MY_STAGE
它是如何工作的…
DynamoDB 全局表负责在所有与全局表关联的区域表中复制数据,并保持数据同步,这一切都在近乎实时完成。serverless-dynamodb-global-table-plugin将创建全局表,并设计用于与serverless-dynamodb-autoscaling-plugin一起工作。对于每个将global标志设置为 true 的表,当服务部署到第一个区域时,插件将创建全局表。对于后续的区域部署,插件将区域表添加到全局表中。每个区域表必须具有相同的名称,启用流,并具有相同的自动扩展策略,这由插件处理。插件不处理的一件事是,当全局表最初部署时,所有表都必须为空。
我们将从快乐的路径场景开始,其中没有区域中断,一切运行顺利,并逐步通过以下图表。当数据写入某个区域的表,例如us-east-1时,数据将被复制到us-west-2区域。us-east-1区域的trigger也会被调用。该触发器有一个forOrigin过滤器,它会忽略所有aws:rep:updateregion字段不等于当前AWS_REGION的事件。否则,触发器将向当前区域的 Kinesis Stream 发布事件,并且所有订阅者将在当前区域执行并复制自己的数据到其他区域。当前服务的listener将忽略它自己产生的任何事件。在us-west-2区域,复制完成后,trigger也会被调用,但forOrigin过滤器将短路逻辑,因此不会向 Kinesis 发布重复事件,也不会在该区域的所有订阅者中重新处理。重复事件处理的不效率和可能的无穷复制循环是最好不复制事件流,而是依赖于叶数据存储复制的原因:
注意,这个配方基于实现双向同步配方中的代码,因此您可以查看该配方以获取有关代码的更多详细信息。
在区域故障转移期间,在最佳情况下,用户的数据已经复制,故障转移过程将完全无缝。用户的下一个命令将在新区域执行,事件链将在新区域处理,最终结果将复制回故障区域。当存在一些复制延迟时,会话一致性有助于使故障转移过程看起来无缝,正如我们在利用会话一致性配方中所讨论的那样。然而,在区域故障转移期间,失败的区域中的一些订阅者可能会落后于处理区域流中剩余的事件。幸运的是,区域中断通常意味着失败区域中的吞吐量较低,而不是没有吞吐量。这意味着将事件处理的结果复制到其他区域会有更高的延迟,但它们最终会变得一致。为最终一致性设计的用户体验,例如移动设备上的电子邮件应用程序,将轻松处理这种拖延的最终一致性。
尝试跟踪哪些事件已处理以及哪些事件卡在失败区域中的复杂性是最好不复制事件流的另一个原因。在无法容忍这种拖延的最终一致性情况下,新区域中的最新事件可以依赖会话一致性以获取更准确的信息,并使用使用逆 oplock 实现幂等性和使用事件溯源实现幂等性配方中讨论的技术来处理从缓慢恢复的区域接收到的顺序错误的旧事件。
实现轮询复制
在我们的多语言持久性架构中,并非所有数据库都支持像 AWS DynamoDB 那样的即插即用区域复制,但我们仍然需要将它们的数据复制到多个区域以提高延迟并支持区域故障转移。本配方演示了如何使用 AWS S3 作为代理为任何数据库添加区域复制。
如何操作…
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch10/round-robin-replication --path cncb-round-robin-replication
-
使用
cd cncb-round-robin-replication导航到cncb-round-robin-replication目录。 -
检查名为
serverless.yml的文件,其内容如下:
service: cncb-round-robin-replication
...
functions:
listener:
...
trigger:
...
search:
...
replicator:
handler: replicator.trigger
events:
- sns:
arn:
Ref: BucketTopic
topicName: ${self:service}-${opt:stage}-trigger
environment:
REPLICATION_BUCKET_NAME: ${self:custom.regions.${opt:region}.replicationBucketName}
custom:
regions:
us-east-1:
replicationBucketName: cncb-round-robin-replication-${opt:stage}-bucket-WWWWWWWWWWWWW
us-west-2:
replicationBucketName: cncb-round-robin-replication-${opt:stage}-bucket-EEEEEEEEEEEEE
...
- 检查名为
replicator.js的文件,其内容如下:
module.exports.trigger = (event, context, cb) => {
_(event.Records)
.flatMap(messagesToTriggers)
.flatMap(get)
.filter(forOrigin)
.flatMap(replicate)
.collect()
.toCallback(cb);
};
...
const forOrigin = uow => uow.object.Metadata.origin !== process.env.REPLICATION_BUCKET_NAME;
...
const replicate = uow => {
const { ContentType, CacheControl, Metadata, Body } = uow.object;
const params = {
Bucket: process.env.REPLICATION_BUCKET_NAME,
Key: uow.trigger.s3.object.key,
Metadata: {
'origin': uow.trigger.s3.bucket.name,
...Metadata,
},
ACL: 'public-read',
ContentType,
CacheControl,
Body,
};
const s3 = new aws.S3(...);
return _(
s3.putObject(params).promise()
...
);
};
-
使用
npm install安装依赖项。 -
使用
npm test -- -s $MY_STAGE运行测试。 -
检查
.serverless目录中生成的内容。 -
在
us-east-1和us-west-2区域部署堆栈:
$ npm run dp:lcl:e -- -s $MY_STAGE
$ npm run dp:lcl:w -- -s $MY_STAGE
-
在
serverless.yml中更新replicationBucketName变量,以便us-east-1复制到us-west-2,反之亦然,然后重新部署堆栈。 -
在 AWS 控制台中检查堆栈和资源。
-
使用以下命令从单独的终端发布事件:
$ cd <path-to-your-workspace>/cncb-event-stream
$ sls invoke -f publish -r us-east-1 -s $MY_STAGE -d '{"type":"thing-created","thing":{"new":{"name":"thing two","id":"77777777-5555-1111-1111-111111111111"}}}'
- 使用以下 curl 命令在
us-west-2区域搜索数据:
$ curl https://<API-ID>.execute-api.us-west-2.amazonaws.com/$MY_STAGE/search?q=two | json_pp
[
{
"id" : "77777777-5555-1111-1111-111111111111",
"url" : "https://s3.amazonaws.com/cncb-round-robin-replication-john-bucket-1cqxst40pvog4/things/77777777-5555-1111-1111-111111111111",
"name" : "thing two"
}
]
- 查看两个区域中
trigger和listener函数的日志:
$ sls logs -f replicator -r us-east-1 -s $MY_STAGE
...
2018-08-19 17:00:05 ... [AWS s3 200 0.04s 0 retries] getObject({ Bucket: 'cncb-round-robin-replication-john-bucket-1a3rh4v9tfedw',
Key: 'things/77777777-5555-1111-1111-111111111111' })
2018-08-19 17:00:06 ... [AWS s3 200 0.33s 0 retries] putObject({ Bucket: 'cncb-round-robin-replication-john-bucket-1cqxst40pvog4',
Key: 'things/77777777-5555-1111-1111-111111111111',
Metadata:
{ origin: 'cncb-round-robin-replication-john-bucket-1a3rh4v9tfedw' },
ACL: 'public-read',
ContentType: 'application/json',
CacheControl: 'max-age=300',
Body: <Buffer ... > })
...
$ sls logs -f replicator -r us-west-2 -s $MY_STAGE
...
2018-08-19 17:00:06 ... [AWS s3 200 0.055s 0 retries] getObject({ Bucket: 'cncb-round-robin-replication-john-bucket-1cqxst40pvog4',
Key: 'things/77777777-5555-1111-1111-111111111111' })
2018-08-19 17:00:06 ... {... "object":{..."Metadata":{"origin":"cncb-round-robin-replication-john-bucket-1a3rh4v9tfedw"}, ...}}
...
-
在移除堆叠之前,请先在每个区域清空水桶
-
完成后,请移除两个堆叠。
$ npm run rm:lcl:e -- -s $MY_STAGE
$ npm run rm:lcl:w -- -s $MY_STAGE
它是如何工作的…
在这个配方中,我们在 Elasticsearch 中创建了一个物化视图,并希望允许用户搜索他们最近区域的数据库。然而,Elasticsearch 不支持区域复制。正如我们在 使用 DynamoDB 实现区域复制 配方中讨论的那样,我们不希望复制事件流,因为该解决方案复杂且难以理解。相反,如图所示,我们将在每个区域将一个 S3 桶放在 Elasticsearch 前面,并利用 S3 触发器来更新 Elasticsearch 以及实现循环复制方案:
注意,这个配方基于 实现搜索 BFF 配方中的代码,因此您可以查看该配方以获取有关代码的更多详细信息。
该服务监听当前区域的 Kinesis 流,并将数据写入当前区域的 S3 桶,这会生成一个路由到 SNS 主题的 S3 触发器。一个函数响应该主题并在当前区域创建 Elasticsearch 中的物化视图。同时,一个 replicator 函数也响应相同的主题。复制器将 S3 桶中对象的全部内容复制到由 REPLICATION_BUCKET_NAME 环境变量指定的下一个区域的匹配桶。这反过来在该区域生成一个触发器。再次,一个函数响应该主题并在该区域创建 Elasticsearch 中的物化视图。该区域的 replicator 也响应并试图将对象复制到下一个区域。这个过程将 trigger 和 replicate 循环进行,直到 forOrigin 过滤器看到原始桶(即 uow.object.Metadata.origin)等于当前复制器的目标(即 process.env.REPLICATION_BUCKET_NAME)。在这个配方中,我们有两个区域——us-east-1 和 us-west-2。数据起源于东部区域,因此东部复制器将数据复制到西部桶(1cqxst40pvog4)。西部复制器不会将数据复制到东部桶(1a3rh4v9tfedw),因为原始桶是东部桶。
这种循环复制技术是一种简单且成本效益高的方法,它建立在现有的事件架构之上。请注意,我们无法利用内置的 S3 复制功能来实现此目的,因为它只复制到单个区域,并且不会产生连锁反应。然而,我们可以像在 为灾难恢复复制数据湖 配方中讨论的那样,将这些桶添加 S3 复制以进行备份和灾难恢复。
第十一章:欢迎多云
在本章中,将介绍以下食谱:
-
在 Google Cloud Functions 中创建服务
-
在 Azure Functions 中创建服务
简介
供应商锁定是服务器无服务器、云原生开发中一个常见的担忧。然而,这种担忧是源于必须从一家供应商整体改变到另一家供应商的单一思维模式的遗物。另一方面,自主服务可以逐个改变。尽管如此,一次编写;到处运行的诱人承诺仍然是多云方法的战斗口号。然而,这种方法忽略了只编写一次的部分只是一个非常大的冰山的一角,而水下部分则蕴含着最大的风险,并且不能直接在云服务提供商之间转换。这不可避免地导致使用一套最少共同分母的工具和技术,这些工具和技术可以更容易地从一家提供商转移到另一家提供商。
我们选择拥抱完全托管、增值云服务的可丢弃架构。这种无服务器优先的方法赋予自给自足的全栈团队以精益和实验新想法的能力,快速失败,学习,并快速调整方向。这自然地导致了一种多语言云或多云的方法,其中团队根据每个服务选择最佳的云服务提供商。最终,公司确实有一个首选的云服务提供商。但是,对于多元化,其中一部分服务是在不同的云服务提供商上实施的,以获得经验和利用,这是一个有争议的观点。目标是在具有类似甚至相同的工具、技术和模式进行开发、测试、部署和监控的服务之间实现一致的管道体验。许多前几章中的食谱专注于 AWS 完全托管、增值云服务。本章中的食谱展示了如何通过添加额外的云服务提供商,如 Google 和 Azure,实现一致的云原生开发管道体验。
使用 Google Cloud Functions 创建服务
无服务器框架在许多不同的云服务提供商之上提供了一个抽象层,这有助于实现一致的开发和部署体验。本食谱演示了如何使用 Google Cloud Functions 创建服务。
准备工作
在开始此食谱之前,您需要一个配置了 Serverless Framework(serverless.com/framework/docs/providers/google/guide/credentials)的 Google Cloud Billing Account、项目和凭证。
如何操作…
- 从以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch11/gcp --path cncb-gcp
-
使用
cd cncb-gcp命令导航到cncb-gcp目录。 -
查看名为
serverless.yml的文件,内容如下:
service: cncb-gcp
provider:
name: google
runtime: nodejs8
project: cncb-project
region: ${opt:region}
credentials: ~/.gcloud/keyfile.json
plugins:
- serverless-google-cloudfunctions
functions:
hello:
handler: hello
events:
- http: path
...
#resources:
# resources:
# - type: storage.v1.bucket
# name: my-serverless-service-bucket
- 查看名为
index.js的文件,内容如下:
exports.hello = (request, response) => {
console.log('env: %j', process.env);
response.status(200).send('... Your function executed successfully!');
};
-
使用
npm install安装依赖项。 -
使用
npm test运行测试。 -
查看在
.serverless目录中生成的内容。 -
部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-gcp@1.0.0 dp:lcl <path-to-your-workspace>/cncb-gcp
> sls deploy -v --r us-east1 "-s" "john"
...
Serverless: Done...
Service Information
service: cncb-gcp
project: cncb-project
stage: john
region: us-east1
Deployed functions
hello
https://us-east1-cncb-project.cloudfunctions.net/hello
-
在 Google Cloud 控制台中查看部署和资源。
-
在以下命令中调用堆栈输出中显示的端点:
$ curl -v https://us-east1-cncb-project.cloudfunctions.net/hello
...
JavaScript Cloud Native Development Cookbook! Your function executed successfully!
- 查看日志:
$ sls logs -f hello -r us-east1 -s $MY_STAGE
...
2018-08-24T05:10:20...: Function execution took 12 ms, finished with status code: 200
...
- 完成后使用
npm run rm:lcl -- -s $MY_STAGE删除堆栈。
工作原理…
首先要注意的是,如何操作… 部分的步骤几乎与所有之前的菜谱相同。这是因为 Serverless Framework 抽象了部署细节,我们进一步将所有命令包装在 NPM 脚本中,以封装依赖项管理。从这里,我们可以使用所有相同的工具和技术进行开发和测试,如第六章构建持续部署管道中概述的。这使得团队成员在跨不同云服务提供商实现的服务工作时可以顺利过渡。
serverless-google-cloudfunctions 插件处理与 Google Cloud APIs(如 Cloud Functions 和 Deployment Manager)交互的细节,以提供服务。serverless.yml 文件看起来应该非常熟悉。我们指定 provider.name 为 google 并设置 plugins,然后我们专注于定义 functions 和 resources。具体细节取决于云服务提供商,但通常我们选择特定提供商的原因是特定服务增值服务的细节。index.js 文件中的 Node.js 代码也很熟悉,尽管函数签名不同。最终,对于实现本烹饪书各菜谱中列举的云原生模式和技术的 Google Cloud 服务有一个清晰的映射。
使用 Azure Functions 创建服务
Serverless Framework 在许多不同的云服务提供商之上提供了一个抽象层,这有助于实现一致的开发和部署体验。本菜谱演示了如何使用 Azure Functions 创建服务。
准备工作
在开始此菜谱之前,您需要一个配置了 Serverless Framework 凭据的 Azure 账户(serverless.com/framework/docs/providers/azure/guide/credentials)。
如何操作…
- 根据以下模板创建项目:
$ sls create --template-url https://github.com/danteinc/js-cloud-native-cookbook/tree/master/ch11/azure --path cncb-azure
-
使用
cd cncb-azure命令进入cncb-azure目录。 -
查看以下内容的
serverless.yml文件:
service: cncb-azure-${opt:stage}
provider:
name: azure
location: ${opt:region}
plugins:
- serverless-azure-functions
functions:
hello:
handler: handler.hello
events:
- http: true
x-azure-settings:
authLevel : anonymous
- http: true
x-azure-settings:
direction: out
name: res
- 查看名为
handler.js的文件,其内容如下:
module.exports.hello = function (context) {
context.log('context: %j', context);
context.log('env: %j', process.env);
context.res = {
status: 200,
body: '... Your function executed successfully!',
};
context.done();
};
-
使用
npm install安装依赖项。 -
使用
npm test运行测试。 -
查看在
.serverless目录中生成的内容。 -
部署堆栈:
$ npm run dp:lcl -- -s $MY_STAGE
> cncb-azure@1.0.0 dp:lcl <path-to-your-workspace>/cncb-azure
> sls deploy -v -r 'East US' "-s" "john"
...
Serverless: Creating resource group: cncb-azure-john-rg
Serverless: Creating function app: cncb-azure-john
...
Serverless: Successfully created Function App
-
在 Azure 控制台中查看部署和资源。
-
在另一个终端中跟踪日志:
$ sls logs -f hello -r 'East US' -s $MY_STAGE
Serverless: Logging in to Azure
Serverless: Pinging host status...
2018-08-25T04:02:34 Welcome, you are now connected to log-streaming service.
2018-08-25T04:05:00.843 [Info] Function started (Id=...)
2018-08-25T04:05:00.856 [Info] context: {...}
2018-08-25T04:05:00.856 [Info] env: {...}
2018-08-25T04:05:00.856 [Info] Function completed (Success, Id=..., Duration=19ms)
- 在以下命令中调用堆栈输出中显示的端点:
$ curl -v https://cncb-azure-$MY_STAGE.azurewebsites.net/api/hello
...
JavaScript Cloud Native Development Cookbook! Your function executed successfully!
- 完成后使用
npm run rm:lcl -- -s $MY_STAGE删除堆栈。
它是如何工作的…
首先要注意的是,“如何做…”(How to do it…)部分的步骤几乎与所有之前的食谱完全相同。这是因为 Serverless Framework 抽象化了部署细节,我们进一步使用NPM scripts将所有命令封装起来以实现依赖管理。从这里开始,我们可以使用与第六章,“构建持续部署管道”中概述的相同工具和技术进行开发和测试。这使得团队成员在跨不同云提供商实现的服务工作时能够顺利过渡。
serverless-azure-cloudfunctions插件处理与 Azure API 交互的细节,例如 Azure Functions 和资源管理器,以提供该服务。serverless.yml文件看起来应该非常熟悉。我们指定provider.name为azure并设置plugins,然后我们专注于定义functions。具体细节是云提供商特定的,但云提供商特定的增值服务细节通常是我们选择特定提供商以提供特定服务的原因。handler.js文件中的 Node.js 代码也很熟悉,尽管函数签名不同。最终,有一个清晰的 Azure 服务映射,用于实现本烹饪书食谱中列举的云原生模式和技术的实现。
642



被折叠的 条评论
为什么被折叠?



