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 参数是一个常量 阅读全文 >>

《Practical Vim》阅读笔记 (4)

一路阅读完第五章直接跳跃到第十章有关宏的内容,宏的录制和回放功能是多数编辑器都会提供,但我却基本不用这一功能。在 Vim 中宏比点号(.) 更有效的处理重复性操作,既然作为一个 Vimer, 以重复操作为耻,因而十分有必要对它加以了解和掌握的,又是一个一分钟上手需一辈子来掌握的东西。

1. 宏是录制在某个寄存器中,用 q{register} 启动录制,例如 qa, 然后执行一系操作,再次按 q 结束录制,第一次回放用 @{register}, 如 @a,以后多次执行上一次宏回放可用 @@.也可以支持数字,如 10@a, 或 10@@:reg a 可查看寄存器 a 中宏的内容

2. 录制宏的一条金律是:确保每一条命令是可重复的。h, j, k, l, 没什么意义,而 0e, A, I 就是可重复的。当回放宏时遇到光标移动失败时, 如光标已在行首,宏中有 h 操作,那么后续的操作被放弃,根据 visualbell 配置可能有响声提示。宏的 motion 失败即放弃的特性让我们在重复执行宏时可以用一个足够大的数字,如 200@@, 反正无副作用

3. 宏的序列执行与并发执行,当用 5@a 执行宏 "a   0f.r)w~j, 其实是把宏放在一个队列里,往下跳行执行,其他任何一行执行失败,后续行将得不到更新。如果针对宏 "a   0f.r)w~, 对选择的多行执行 :'<,'>normal @a 时效果就不一样了,它的意思是针对选择的多行单独执行宏,任何行的失败互不影响,所以它是并发执行的 阅读全文 >>

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

《Practical Vim》阅读笔记 (3)

1. 学习完前面的三大模式:正常模式,插入模式和可视模式后,现在步入了命令模式。:, / 或 ?, 和 <C-r>= 分别进入到 Ex 命令,搜索和表达式,它们都被称作命令模式, 以前还常常把正常模式说成是命令模式。: 后的是 Ex 命令,是因为我们仍然沿袭了 Vim 的前身 Ex。我在 Mac 下输入命令 ex 就会看到这个

进入到 Vim 的 Ex mode, 输入 visual 命令就是正常的 Vim 界面。Vim 之于 Ex 就像 Gui 之于 Shell, Vim 的 Ex 命令可以做任何操作,见 :h ex-cmd-index

2. 命令模式在 : 提示符下输入时可以使用插入模式下的很多命令,如 <C-w>, <C-u>, <C-k>, <C-v>, <C-r>{reg}, 甚至是 <C-r>=,就是不能用 motion 命令,光标移得靠方向键

3. 在阅读本章之前对 Vi 的命令模式只能用不觉明厉来形容,它对我的贡献仅仅是 :wq 之类的,模式查找,简单替换,再就是执行一些未设置快捷键插件功能。也一直未理解替换怎么是 %s/abc/dev, 只是依葫芦画瓢,现在终于可以在本章中理解那些命令了。从中真的能体会到 Ex 命令模式的乐趣,Ex 是不会把整个文件内容显示在屏幕上的, Ex 命令通常由 范围 + 命令 组成。

:2           -- 光标跳到第一行,正常模式下可以 2G, 或 2gg
:print    -- 打印当前行的内容

范围和命令一般会写在一块,范围可借用同样意义的特殊字符,默认操作当前行,因此下面一系列的命令就好理解了 阅读全文 >>

Spring 项目中把属性或 SQL 语句写在 .xml 文件中

Spring 项目中把大量的 SQL 分散在 Java 代码中,无 Here Doc 的情况下用加号来连接写着实在是不爽,于是之前思考这个 Spring 项目中把 SQL 语句写在 .sql 文件中 --  把它们写在 *.sql 文件中,但是这个 *.sql 需要特定的格式来标识属性 Key

--!select.user
select id, firstname, lastname, address
--!update.user
update ........

而且还需要一个额外的类 SqlPropertySourceFactory 来解析上面的 *.sql 文件, 识别出 select.user 是 Key, 紧接着后面的块是相应的属性值,用注解引用它时还有点额外的 factory 属性来配置,如

@PropertySource(value = "classpath:sql/queries.sql", factory = SqlPropertySourceFactory.class)

所以一直在思考是否能够再简单些,是否能用一个自定义的注解,如

@SqlPropertySource("classpath:sql/queries.sql")

捉摸了很久,似乎有点难度,不过再不断发掘的过程中找到了这个类 org.springframework.core.io.support.PropertiesLoaderUtils, 有下面的代码片断 阅读全文 >>

代码整洁之道(Clean Code) 笔记(三)

第八章:边界

本章关于如何学习使用第三方组件

  1. 第三组件或框架追求普适性,而使用者则想要集中满足特定的需求
  2. 学习第三方组件首当其冲当然还是文档,其次重要的是它的单元测试,我甚至是把单元测试当作文档的一部分来看待。其实更重要的方法是学习性测试(learning tests), 通过测试来学习才是切实的体验,是一种精确的试验,我们甚至可以用自己的测试来验证第三方组件的新版本
  3. 边界上会发生有趣的事,这就要求我们对它清晰的分割和定义,以避免我们的代码过多地了解第三方组件中的特定信息,依靠你能控制的东西好过依靠你控制不了的东西,免得日后受它控制。简而言之就是尽可能隔离边界,在边界改动时只需要修改一下适配器,对的适配

第九章:单元测试

这一章我自认为是很重要的,特别是正在采用测试驱动开发/设计(TDD) 的程序员来讲。充分,良好的单元测试是保证我对生产代码大刀阔斧的关键,这也就是 TDD 的最后一个 D 又能理解了 Design 的原由

阅读全文 >>

JMockit 中捕获 mock 对象方法的调用参数

三个月前写过一篇 Mockito 中捕获 mock 对象方法的调用参数,一般项目中 Mockito 不决求助于 JMockit, 同样的在 JMockit 也需对捕获被 Mock 的方法调用参数。当我们用 new Expectations(){{}} 打桩并在后面断言了返回值,那就无需捕获参数来断言,匹配到了方法调用即证明传入的参数也是对的,如下面的代码所示

public class UserServiceTest {

    @Mocked
    private UserDao userDao;

    @Test
    public void couldCallUserDaoToAddUser() {
        new Expectations(){{
           userDao.findById(123);
           result = "Yanbin";
        }};

        UserService userService = new UserService(userDao);
        String user = userService.findBy(123);

        assertThat(user).isEqualTo("Yanbin");  //这里断言成功也就证明了 userDao.findById(123) 方法被调用,参数必须是 123
    }
}

但如果是未打桩的方法,或打桩是用的模糊参数(withInstanceOf(String.class)), 或是无返回值的方法就要事后对是否调用了某个方法以及传入什么参数的情况进行断言。 阅读全文 >>

使用 Awaitility 测试异步代码

对于同步方法的测试很简单,调用完后可立马检查执行状态; 而异步方法,由于我们无法确切的知道何时结束,因此以往的办法是用 Thread.sleep(500) 来预估一个执行时间。然后通常我们估计的要长于实际的时间,这就很浪费,况且偶然的超过预估的等待时间也并不意味着代码有问题。还有 sleep 方法还抛出一个检测异常 InterruptedException, 一般会要对 Thread.sleep(500) 作下简单包装。

于是今天要介绍的 Awaitility 就应运而生了,专门针对异步方法的测试。它的官方文档在 https://github.com/awaitility/awaitility/wiki/Usage。本文主要关注在 Java 8 环境下用 Lambda 的代码书写方式。Awaitlity 实际运行是以某种轮询的方式来检查是否达到某个运行状态,可设定最多,最少等待时间,或永久等待,或自定义轮询策略,之后就开始进行需要的断言,所以它可以尽可能的节省测试异步方法所需的时长。而不像 Thread.sleep(500) 一路等到黑,并且没有回头路。

通常我会在项目中给 JUnit 配上三个最佳伴侣,它们是(按 mvn dependency:tree 中的显示方式):

  1. org.awaitility:awaitility:2.0.0:test
  2. org.assertj:assertj-core: version: 3.8.0:test
  3. org.mockito:mockito-core:2.7.22:test

当然如果项目中没有异步调用自然是不需要 Awaitility, 在我的项目中是基本不可能的。以上三种都追求 DSL,以流畅的方式进行愉快的测试。

现在来尝试下 Awaitility 的几种基本的用法,先假定有下面的代码 UserService 阅读全文 >>

代码整洁之道(Clean Code) 笔记(二)

第四章:注释

  1. 别给糟糕的代码加注释 -- 重新写吧
  2. 什么也不会比乱七八糟的注释更有本事搞乱一个模块。什么也不会比陈旧,提供错误信息的注释更有破坏性
  3. 若编程语方足够有表达力,或者我们长于用这些语方来表达意图,就不那么需要注释 -- 也许根本不需要
  4. 注释的恰当用法是弥补我们在用代码表达意图时遭遇的失败。注释总是一种失败
  5. 不准确的注释要比没注释坏得多。它们满口胡言。代码,只有代码能忠实的告诉你它做的事
  6. 好注释不多,如法律信息,警示等。我都不觉得 TODO 是多好的东西
  7. 大多数注释都是坏注释,甚至一些 IDE 自动生成的代码也加一堆的废话注释,分散阅读注意力。我也写过那种日志式注释,这本是版本控制系统干的事。
  8. 很奇怪 IntelliJ IDEA 还在自动为新建的类加下创建者,代码是团队共有,时间长了根本就与作者无半毛关系。我经常碰到这种写有作者的代码哭笑不得,直接把原作者改了或删了也不太好,还不如把文件删了,新建一个没有作者注释的类
  9. 直接把代码注释掉是种讨厌的做法,自己可能不会再启用,其他人以为很重要更不敢删。不用了最好果断删掉,版本控制系统会帮你记住历史。
  10. 个人认为,注释会胡说八道,产品代码和测试代码不会撒谎。有功夫写注释还不如写好测试用例,测试需要维护,它本身就是最真实的文档。

阅读全文 >>

使用 Mockito 修改私有属性

修改私有属性来 Mock 可能不是一种很好的测试方式, 因为属性名是动态的,但有时不得已而为了,例如下面的代码:

public class UserService {
    private ExternalApi external = ExternalApi.default();
    private UserDao userDao;

    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }

    public User findUserById(int id) {
        return userDao.findById(external.convertId(id));
}

测试时欲隔离对 ExternalApi 的外部依赖, 当然可以把它也作为构造函数的一个参数,这样创建 UserService 实例时就可以 Mock external 属性。不过 external 经常是不变的,所以作为方法参数的必要性也不大。这就希望能在构造出 UserService 之后对 external 私有属性进行 Mock 处理。

在 Mockito 1.x 和 2.x 下要使用不同的方式,分别使用到 Whitebox 和 FieldSetter 类,它们都来自于  mockito.internal.util.reflection 包,可见 Mockito 打心底不推荐直接使用它们,但谁叫它们是 public 的呢。还有一种方式是使用 PowerMock + Mockito, 这是后话。 阅读全文 >>