AWS Batch 是如何向 Docker 容器传递参数

AWS Batch 提供了简单有效的方式来运行 Docker 镜像,在单纯执行一个计算任务时比 ECS 使用起来要方便许多。AWS Batch 在提交任务时可以执行 command, environment 和 parameters, 那么它将如何传递那些参数给 Docker 容器呢?

首先看一下提交任务的脚本

注:aws cli 命令参数中有 - 时需要用  --parameters='--name=Spring' 等号的格式

我们将用上面的脚本提交一个任务,然后用 docker inspect <container-id> 观察 Docker 容器收到了什么参数

也没什么意外的,inspect 容器后我们看到 阅读全文 >>

AWS SQS 队列的 DLQ 设置与行为

和 Lambda 类似,AWS 的  SQS 队列也提供了 DLQ(Dead Letter Queue) 来支持重试功能,可以设定某一个消息在接收多次重新变得可见后进入到另一个 SQS 队列中。比如说队列 user-id-queue,设定了它的 Redrive Policy 为三次接收后转入到另一个 SQS 队列 user-id-dlq, 就会显示为

Maximum Receives 3
Dead Letter Queue arn:aws:sqs:us-east-1:<account_id>:user-id-dlq

在运用 SQS 的 DLQ 之前首先要理解 SQS 中消息的几个概念
  1. Message Available:  SQS 客户端可以获取到的消息, 即 Visible Messages
  2. Messages in Flight: 消息被 SQS 收取了之后,由 Available 转为 In Flight, 该状态的消息不能被客户端接收到
  3. Visibility Timeout:  消息停留在 In Flight 状态的时间, 如果在 Timeout 之前未删除这个消息,该消息重新变为 Available 状态

我们可以设置 SQS 队列的默认 Visibility Timeout 大小,也可以在代码中收取消息时指定这个值。

所以我们能够在集群环境中应用 SQS 的这个特性让多个节点同时监听单个 SQS 队列,基本上保证每个节点处理各自不同的消息。有一种例外就是:假设我们设置了 Visibility Timeout 是 30 秒,客户端 1 获取到消息后,消息变为 In Flight 状态,但 30 秒后仍然在处理过程中,此时消息回到Available 状态,客户端 2 也能获取到该消息,这也会造成单条消息的重复处理。解决的办法之一是适当延长 Visibility Timeout 的时间,给予第一个客户端更充分的处理时间。 阅读全文 >>

推送 Docker 镜像到 Amazon ECR 仓库

Docker 镜像在未指定仓库时默认是从  Docker Hub 拉取的。如果需向 Docker Hub 推送镜像的话还可用 docker login 在交互中完成登陆 Docker Hub 的操作。docker login 的命令格式是

docker login [OPTIONS] [SERVER]

所以我们可以连接到任何的 Docker 镜像仓库,也可以是本机,但我们这里所要介绍的是如何推送镜像到 AWS 给我们提供的 Docker 镜像仓库(Amazon ECR - Amazon Elastic Container Registry)。每个帐号下都有自己独立的仓库,镜像推送到了  Amazon ECR 后我们能够很方便的在 ECS, Batch 服务中使用它,也可以从 ECR 拉取镜像到本地来。

首先我们来做一个运行 Spring Boot Web 的简单的 Docker 镜像,假定已用 mvc pacakge 生成了一个可独立运行的 jar 包 java-webapp-0.0.1-SNAPSHOT.jar 。该应用开启一个 Web 服务,访问 http://localhost:8080 显示一行字符串 Hello World!

创建一个目录 aws-docker, 并把 java-webapp-0.0.1-SNAPSHOT.jar 移入该目录,在其下创建 Dockerfile 文件,文件目录结构如下:

aws-docker
  ├── Dockerfile
  └── java-webapp-0.0.1-SNAPSHOT.jar

阅读全文 >>

实现 Amazon S3 数据(文件)分段上传

探索是否能以流式写数据到 S3

通常,在我们项目中用 Java 代码上传数据到 S3 是下面那样的操作

AmazonS3 s3Client = AmazonS3ClientBuilder.defaultClient();
s3Client.putObject("bucket_name", "s3key.txt", new ByteArrayInputStream("hello".getBytes()), new ObjectMetadata()); //ObjectMetadata 没什么特别的话可以为 null

虽然  putObject() 的第三个参数是一个流,但它是输入流, 并非输出流啊。也就是说在执行该方法时必须把所有待上传的上据全部准备在这个输入流中,所以这里就直接用一个 ByteArrayInputStream 来包裹需上入到 S3 的数据内容。

当然 putObject() 也就无法像 FileObjectOutputStream 那样流式写入内容到文件中,因为 putObject() 前后都没有与 bucket 上那个文件上有联系。即使是用 PipedInputStream/PipedOutputStream  也不行, 比如说下面的代码 阅读全文 >>

Terraform 进阶 - 部署 Lambda 并创建相关资源

昨日刚刚体验了 Terraform 是一个什么鬼东西 Terraform 使用 - 从最简单例子开始,今天再进一步。将来尝试的是使用 Terraform 来部署一个 Lambda 应用,并创建相关的资源。

本例中的 Lambda 要由 Kinesis 来触发,并写数据到 S3 Bucket 中去,所以需要做的事情大致如下:

  1. 创建 IAM Role, 该 Role 要能访问 S3, Kinesis 和 CloudWatch
  2. 创建一个 Kinesis Stream (指定 Shard 数目)
  3. 创建一个 S3 Bucket
  4. 部署 Lambda (要指定能访问 S3 Bucket 的 Role, 并其他参数,如环境变量)
  5. 设置 Lambda 的 Kinesis 触发器 (指定源 Kinesis Stream 和  batchSize)

以下是 Lambda 的实现代码,从 Kinesis 读出字符串,逗号分割,第一部分作为 S3 Key, 第二部分作为文件内容写入到 S3 Bucket 中去。S3 Bucket 名称从环境变量中读取。 阅读全文 >>

Terraform 使用 - 从最简单例子开始

Terraform 是一个 IT 基础架构自动化编排工具,它的口号是 "Write, Plan, and create Infrastructure as Code", 基础架构即代码。具体的说就是可以用代码来管理维护 IT 资源,比如针对 AWS,我们可以用它创建,修改,删除 S3 Bucket, Lambda, EC2 实例,Kinesis, VPC 等各种资源。并且在真正运行之前可以看到执行计划(即干运行-dryrun)。由于状态保存到文件中,因此能够离线方式查看资源情况 -- 当然,前提是不要在 Terraform 之外对资源进行修改。

Terraform 配置的状态除了能够保存在本地文件中,也可以保存到 ConsulS3, azure, http, swift 等处。

Terraform 是一个高度可扩展的工具,通过 Provider 来支持新的基础架构,AWS 不过为目前官方内建 68 个 Providers 中的一个。其他能用 Terraform 的地方有 Alicloud(阿里云, 实名制备案才能用), Google Cloud, Heroku, Kubernetes, Microsoft Azure, MySQL, RabbitMQ, Docker 等等。愿意的话可以写自己的 Provider, 如搞个 Kafka 的话,用来管理 Topic 等的创建,维护工作。

Terraform 之前我们对 AWS 的操作用的是 awscli, 或 Serverless。awscli 什么都能做,但它是无状态的,必须明确用不同的命令来创建,修改和删除。Serverless 不是用来管理基础架构的,用它创建  Lambda 时创建资源都是很麻烦的事。AWS 提供的 CloudFormation 才是与 Terraform 较类似的工具,但是看到用法就头疼。 阅读全文 >>

AWS Lambda 按序处理同一个 Kinesis Shard 中的消息

当 AWS Lambda 由 Kinesis 消息来触发时,一个 Kinesis Shard 会相应启动一个 Lambda 实例,比如说 Kinesis Stream 有 5 个 Shards, 那同时只会启动 5 个 Lambda 实例。那么把多条消息发送到同一个 Kinesis Shard 中去,这些消息会被如何消费呢?答案是按顺消息,不管这些消息是否被不同的 Lambda 实例处理。本文就是关于怎么去理解 https://aws.amazon.com/lambda/faqs/ 的下面那段话的内容:

Q: How does AWS Lambda process data from Amazon Kinesis streams and Amazon DynamoDB Streams?
AWS Lambda 如何处理来自于 Amazon Kinesis 和 DynamoDB 的数据

The Amazon Kinesis and DynamoDB Streams records sent to your AWS Lambda function are strictly serialized, per shard. This means that if you put two records in the same shard, Lambda guarantees that your Lambda function will be successfully invoked with the first record before it is invoked with the second record. If the invocation for one record times out, is throttled, or encounters any other error, Lambda will retry until it succeeds (or the record reaches its 24-hour expiration) before moving on to the next record. The ordering of records across different shards is not guaranteed, and processing of each shard happens in parallel.
从 Kinesis 和 DynamoDB 单个 Shard 上的记录会被 Lambda 严格的按序处理。这意味着如果你送两条记录到相同的 Shard, Lambda 将会保证第一条记录成功处理后才会处理第二条记录。假如处理第一条记录时超时,或超过资源使用上限,或碰到任何错误, Lambda 将会不断重试直到成功(或记录在 24 小时后过期), 而后才会去处理下一条记录。跨 Shard 的记录不保证到达顺序,且是并行处理多个 Shard 来的记录。

可以做几个试验,下面的代码可以保证消息总是被发送到同一个 Kinesis Shard,因为 PartitionKey 参数是一个常量 阅读全文 >>

AWS S3 Key 前缀分布优化数据请求的性能

很早就想写下这篇日志的,因为实际使用 AWS S3 来存取文件使用什么样的 Key 对性能的影响是极其大的。当然,如果你对 S3 的并发请求在 50 以内是无所谓的,要是并发要求很高的话,Key 的选择就变得至关重要的,不可不察。S3 Key 从第一个字符算起的任意长度子字符串都被称作前缀(prefix), 而对 S3 文件访问性能影响不在完整的 Key, 恰恰是那个前缀。

背景:我们最初在使用 S3 时,存储的文件的 Key 直接用了数据库的自增 ID,于是保存到 Bucket 中大概下面那样子的

examplebucket/12134850.csv
examplebucket/12134851.csv
examplebucket/12134852.csv
examplebucket/12134853.csv
examplebucket/12134854.csv
examplebucket/12134855.csv
examplebucket/12134856.csv
examplebucket/12134857.csv
examplebucket/12134858.csv

Bucket 中有百万个文件,当初测试时 60 个左右的 Lambda 实例同时访问这个 Bucket 中不同的文件时,加载每个 S3 文件的时间大约在几百毫秒,然后并发上到 70, 80 后加载同样大小的 S3 文件的时间陡然增加到 10 秒以上,并发继续上到 100 以上直接导致众多 S3 的请求超时。后来了解到虽然一个 Bucket 中放多少个文件是没有限制的,而且官方文档说了文件多了并不影响访问的性能,但背后却有一个文件的分区存储机制,这个才是关键。

S3 的分区存储就像是硬盘分区,或文件分布在不同硬盘上的效果。试想一下,如果我们多个线程同时从一块硬盘上读取数据,每个线程需共同一个磁头来读取数据,性能就差; 但如果那些线程同时从不同的硬盘上读取各自的数据,那性能就大大提升了,它们互不干扰。在使用机械硬盘时我有过这样的体验,在同一个磁盘上拷贝文件比从一个磁盘拷贝到另一个磁盘要慢很多。 阅读全文 >>

AWS Java Lambda 与环境变量

一句话概要:对 Lambda 环境变量的任何改动都会引起一次 Lambda 的冷启动,大可放心在 handleRequest(...) 方法外使用环境变量。

AWS 上 Java Lambda 应用记要 中,我学到了 Lambda 的实例是跨请求共享的,所以为使用 Lambda 配置的环境变量时曾写出了下面复杂而多余的  AWS  Lambda 代码:

public class Handler implements RequestHandler<SNSEvent, String> {

    private int threadPoolSize = getThreadPoolSizeFromEnv();
    private ExecutorService threadPool = Executors.newFixedThreadPool(threadPoolSize);

    @Override
    public String handleRequest(SNSEvent snsEvent, Context context) {

        int configuredThreadPoolSize = getThreadPoolSizeFromEnv();
        if(configuredThreadPoolSize != threadPoolSize) {
            threadPoolSize = configuredThreadPoolSize;
            threadPool = Executors.newFixedThreadPool(threadPoolSize);
        }

        return "Hello Lambda";
    }

    private int getThreadPoolSizeFromEnv() {
        return Integer.parseInt(System.getenv().getOrDefault("threadpool_size", "50"));
    }
}

这段代码看起来很在理,既然 Lambda 实例是共享的,那么在必变环境变量之后就可能不会重新初始始化实例,所以在每次的请求方法中对比如果环境变量值改动了就重新用最新的配置值来初始化线程池。然而上面的代码结结实实是多余的,真是把 Lambda 想得太简单了,如果是很多环境变量岂不是逐一判断。 阅读全文 >>

AWS Java Lambda 使用 Logback 记录日志

直接一句话:去掉 Log4J 的依赖,把  Slf4J, Logback, 和 log4j-over-slf4j 依赖加进来就行了,配置文件换成 logback.xml,这就完了,不要往下看了,都是些废话。

当我们用 Serverless 命令 sls create -t aws-java-maven -p hello-lambda 创建的示例项目中直接用的是 Log4J 日志组件,而且也没用像  Slf4j, 或 Apache Common Logging 更上一层的通用日志框架。查看了几个 AWS 本身的组件 S3, SNS, 和 Kinesis 的 SDK, 它们内部是用的 Apache Common Logging 声明的日志变量

import org.apache.commons.logging.LogFactory;
import org.apache.commons.logging.Logger;

private static final Log log = LogFactory.getLog(AmazonKinesis.class)

而我们自己的组件中通用日志组件是 Slf4j, 底层实现为 Logback, 所以我们希望在 Lambda 中使用 Logback 来写日志。

选用一个通用日志框架总是明智之举,因为一个项目经常杂糅了多种日志实现,使用 Slf4J 或 Apache Common Logging 可以把它们(Log4J, Logback, 或更多)输出到共同的目的地,并且有统一的日志输出接口。而我们认为通用日志框架还是 Slf4J 要先进些,所以我们在 Java Lambda 中的日志方案是 Slf4J + Logback,还需要把 Log4J 的日志桥接到 Slf4J 上来,再经由 Logback 输出。

回到前面创建的 hello-lambda 项目,看其中怎么用的 Log4J,先瞧瞧 pom.xml 文件怎么引入的 Log4J, 它是间接通过一个 AWS 定义的 Log4J Appender 引入的 阅读全文 >>