DynamoDB 单表设计的优势与考量

大多数开发都有关系数据库设计经验,在初次使用 DynamoDB 设计数据模型的时候,很容易陷入关系数据库的思维陷阱, 不自觉的遵守关系数据库设计的范式, 尝试将数据模型规范化,每个实体或实体关系都有对应的单独的表,通常称之为多表设计。
与之对应的是,将所有实体和实体关系都存储在同一张表中,毕竟 DynamoDB 是 Schemaless 的数据库,称之为单表设计。
这儿要强调的是,这两种设计只是极端的两点。可能也不是一个合适的命名,因为在实际应用中,单表设计并不意味着只能有一张表。
在两个极端之间,单表设计更倾向于将相关实体存入在同一张表中,多表设计则倾向将不同实体类型存入不同的表中。

官方文档中,单表和多表设计比较时也较为推荐单表设计。本文就来根据实际经验,讨论下实际实践中单表设计的优势。
我们自己的项目采用的是单表设计,很大程度上受 《The DynamoDB Book》影响,作者 Alex DeBrie 是单表设计的推崇者。当然,我们项目中已经有十几张表,尽管我们已经尽量将相关实体存入同一张表中。

阅读更多

AWS Connect 转接最近通话的客服

需求

最近接到一个需求,需要将客户来电转接到最近与客户通话的客服。这个需求很容易理解,
客户可能因为各种各样的原因中断通话,再次来电很可能是因为同一个诉求,比如保险索赔,可能需要多次来回沟通。
将通话转给同一个客服,客服可以接着继续处理而不用熟悉客户场景,这样做能够提高处理效率。
尽管这个需求看起来很基础,但是并没有一个开箱可用的方案。我们的呼叫中心是 Amazon Connect,不过并没有启用 Profile,一些方案也不能采用。

阅读更多

AWS client getaddrinfo EMFILE issue

最近,在我们系统中引入了 AWS Cloud Map 作为我们的服务发现系统。部署几周后没有问题,今天突然抛出错误,日志显示错误 getaddrinfo EMFILE events.ap-southeast-2.amazonaws.com
当然,并非所有请求都触发了此错误,只是在高流量时段才出现了这个错误。

阅读更多

如何防止重复处理 SQS 消息

问题

一般来说在我们的系统中,消息处理必须保证幂等性,以防止消息重复处理。在我们的系统中,下面两种情况可能导致相同消息被重复处理:

  1. 调度器和消息生产者:调度器或消息生产者可能会被多次触发,比如时不时有些任务因为超时而被多次触发。
  2. 队列管理:如果一个 Lambda 实例处理消息超时,另一个实例可能会在 visibility timeout 设置不合适的情况下得到重新处理相同消息的机会。

如果消息被多次处理,我们可能会向客户发送重复的电子邮件和短信,甚至礼品卡都可能重复发送。所以,我们需要一个通用的机制来确保相同消息不会被多次处理。

阅读更多

Athena in IntelliJ IDE

目前所在公司使用的是 Serviceless 架构,数据库使用 DynamoDB,每天定时任务会导入数据湖,所以平时会经常使用 Athena 查询来排查问题,尤其是最近在调查数据一致性的问题。Athena 本身可以满足日常需求,只是使用多的时候觉得不如 IDE 方便。

阅读更多

使用 React 开发邮件模板

刚来土澳的时候很不习惯每天检查邮件,大小事都是邮件,在国内很多时候都是手机短信通知或者应用内通知。在澳洲待了几年,也习惯了每天查看下邮箱。现在这家公司绝大多数于用户沟通都是通过邮件,包括关键的用户通知,还有营销推送。我们每天发送大量的营销邮件,公司使用 Orrto 这个平台,这个工具可能很少人听说,举个场景:用户在我们网站上进行了一些操作,填了邮箱信息,就成了一个潜在用户。我们会在 Ortto 中创建一个 Journey,这个潜在用户进入这个 Journey 之后,我们会开始会发送一些邮件,过几天会再次发送一封类似的邮件提醒,当然在此过程中他已经完成了相应订单,我们就要将其从这个 Journey 中移除。总的来说,发送邮件的场景很多,我们的邮件模板梳理也非常多。

阅读更多

Distribute tracing - newrelic

newrelic 提供的 Distribute tracing 功能非常实用。阿里内部使用的是鹰眼系统,因为一直做前端开发所以我并不是很清楚到底怎么工作的。虽说之前也多少翻过一些文章介绍大概的系统的架构,但是一直以为在调用下游服务时是显式传入当前 TraceId 的。直到最近使用 newrelicDistribute tracing 追踪才了解到还有一种无侵入的方案。

比较流行的全链路监控方案 Zipkin Pinpoint Skywalking 也有完全无侵入的方案,但都是 JAVA 编写的。而且在搜寻对应的 Node.js 实现时,找到的仓库代码往往并不齐全。无奈只能在 newrelic 源码中寻找线索,果然发现大量的 instrumentation,其中就有对原生 http模块上方法的拦截。这样就可以解释为什么我们不需要显式传给下游 TraceId

或许不难猜到,在发起新请求时,只需要把当前的 traceId 附带到请求头上即可。在接收请求时解析请求头上信息,并将其传给当前新建的 transaction 作为 parentId。
但是还有一个问题:如果有多个请求同时处理,如何保证传给下游正确的 TraceId。假设服务端的伪代码是这样的:

1
2
3
4
5
6
app.get('/frontend', (req, res) => {
asyncTask((is) => {
const result = await http.get('/downstream');
res.json(result);
});
})

如果同时有两个请求 /frontend(1) /frontend(2) 怎么保证调用链不串呢?因为我们并不能保证异步任务的时间,完全有可能第二个请求先调用了下游服务。 Skywalking Node.js 就有这样的问题

继续查看代码又有新的发现,newrelic 同时也对很多基础模块进行了拦截,包括我能想到的 Timer, Promise Async, FS 等等, 包装回调主要目的在执行回调之前找到当时的 segment。

这么多的拦截代码想当然的会需要一定的开销。暂时未见到 newrelic 上关于这方面的文档。

更多链接
https://juejin.im/post/5a7a9e0af265da4e914b46f1

一个内存泄漏问题分析

一般来说,借助于强大的 GC 和 lint 工具,前端还是很少会碰到内存泄漏问题的。这篇文章说下我最近遇到的例子以及排查的过程。

内存泄漏的检测还是非常容易的:打开 Chrome DevTools 选择 Memory 选项,点击 Take heap snapshot 等待查看内存大小。重复这个步骤,如果你发现内存大小定期增长,或者增长的很有规律,那么八成出现内存泄漏了。这个是 Google 的 文档

问题检测

我们的应用是这样组织的,采用微前端架构,涉及到几个项目,一个 Shell 负责管理具体渲染哪个页面,应用默认是 Documents 页面,还有一个 Teams 页面分别属于不同的项目。 在 Teams 页面采集内存信息,回到 Documents 页面等待页面加载完成再回到 Teams 页面再次采集内存信息。重复这个过程几次,这是结果截图。为了确保数据准确,在隐私窗口测试以免受插件影响,在每次收集之前都点击 Collect Garbage。每次都多次采集直到得到 4 个相同的值表示内存大小稳定。这个截图上,5,6,7与8相同就移除了,不过足以说明应用确实存在内存泄漏了。

memory snapshot

问题诊断

现在我们知道有内存泄漏,先比较下 Snapshot 19 和 Snapshot 15 的内存信息. 😱 好吧, 太多对象了,几乎是组件树上的所有示例都有在列,毫无头绪。因为涉及到三个项目,完全不知道如何下手。不过不管怎样,先从可以做的做起,先把开发环境准备好。当然开发环境本身就有更多干扰因素,不过好歹还是有了第一条线索。当我尝试复现问题时,根本不等 Documents 页面完成渲染就切换页面,控制台有一个警告信息:

Warning: Can’t perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.

因为有错误堆栈信息,所以很快发现,这个错误时因为没有清理定时器导致的。虽说本书是有逻辑来清理定时器的,但是没有考虑到这些逻辑因为用户页面跳转中断。所以说最好还是在 componentWillUnmount 完成所有的清理工作。

修复这个问题并且排查了所有的定时器之后,发现内存泄漏还在,看起来没那么容易解决。

为了方便问题排查,先修改 Teams 为仅渲染普通文本,依然稳定复现内存泄漏。不过其次最为可疑的就是全局的事件监听,排查一遍发现有些监听未被移除。幸运的是,发现一个低级错误,本来 componentWillUnmount 应该移除监听结果又添加了一遍:.

1
2
3
4
5
6
componentDidMount() {
window.matchMedia('print').addListener(this.printHandler);
}
componentWillUnmount() {
window.matchMedia('print').addListener(this.printHandler);
}

把 addListener 改为 removeListener 之后重新检查一遍,发现还是存在内存泄漏。

检查了所有的事件监听之后,确信没有遗漏,在查看内存信息的时候看到 onLoad 事件回调,对了,on 事件给漏了。在 shell 里一些可疑代码:

1
2
3
4
5
6
7
const tag = document.createElement('script');
// ... some code
tag.onload = () => {
resolve();
};
// ...
document.body.appendChild(tag);

不管怎样,我们应该清理到这些事件:

1
2
3
4
5
6
tag.onerror = () {
tag.onload = tag.onerror = null;
};
tag.onload = () {
tag.onload = tag.onerror = null;
};

重新检查内存泄漏,发现还是存在。上面的代码因为只允许了一次所以不会导致内存大小变化。这个时候都怀疑是不是第三方库的原因了。不过还是当把 Documents 页面换成普通文本时发现,没有问题了。所以问题肯定在 Documents 组件上。 为了进一步缩小范围,试着把 render 方法移除,发现问题这样都有问题。 所以说,问题还是在 componentDidMountcomponentDidMount 上的事件监听上. 但是看起来一切正常。 因为对 matchMedia 这个实验特性不熟,又再次查看了下文档. 这次注意到:它说每次都会返回一个新的对象 …

意思就是 matchMedia('print') !== matchMedia('print') 这也就是为什么 matchMedia('print').removeListener(this.printHandler); 压根没有的原因。修复这个问题之后,再次检查就没有内存泄漏的问题了。

总结

内存泄漏很少碰到,当然也很难调试犹如大海捞针。 除了排查定位:定时器,全局的事件监听,以及全局对象是优先排查的对象。

烦人的 Flow.js

不管怎么说,Javascript 构建的项目越来越大且越来越复杂,弱类型这个短板显得越来越为致命。而 Flow.js 提供了一个向强类型过渡的方案。

  1. 挫挫的枚举类型
1
2
3
4
5
6
7
8
type Color = 'red' | 'blue' | 'green';

const Colors = Object.freeze({
Red: 'Red',
Blue: 'Blue',
Green: 'Green',
})
type Color = $Values<typeof Colors>;
  1. 莫名其妙的严格
1
2
3
4
5
6
7
8
type Vehicle = { name?: string }
const car: Vehicle = {}; // It works

type Vehicle = {| name?: string, color?: string |}
const car: Vehicle = { color: '' }; // It works

type Vehicle = {| name?: string |}
const car: Vehicle = {}; // Error
  1. 不严格的类型和里式替换

我们知道,在 flow.js 中,当我们定义一个类型时,type Vehicle = { name: string } 只是意味着这个对象至少有 name: string 这个属性,你可以这样 const bike: Vehicle = { name: 'bike', brand: 'phoenix' }。就是因为太不严格,所以我们会选择严格类型 type Vehicle = {| name: string |} 不过接下来就遇到麻烦了。

1
2
3
4
5
6
7
8
9
type Vehicle = {|
name: string,
|}
type Car = {
...Vehicle,
wheel: number
};
const car: Car = { name: 'Toyota', wheel: 4 };
const vehicle: Vehicle = (car: Vehicle); // Cannot cast `car` to `Vehicle`
  1. 三方库依赖问题

有次突然发现我们漏掉了 import * as React from "react"; 但是可以直接使用 React.Node,flow.js 竟然没有报错。当然,它只可能是默认的 Any 类型。很快就定位到在 flow-typed/npm/enzyme_v3.x.x.js 包含有:

1
2
3
4
import * as React from "react";
declare module "react-redux" {
// Here will use React type
}

看样子是,只要在声明文件中导入之后,类型就被污染了。被污染的还有:Dispatch, Store, React, ComponentType and ElementConfig 不难猜到越是流行的库越可能被污染。而这一切发生的时候,flow.js 没有任何警告信息或错误提示。
Known issue: https://github.com/flow-typed/flow-typed/issues/1857

  1. 第三方库

时不时的第三方库导致 flow 错误,又不能 ignore 所有的 node_modules 文件,每次遇到新错误,都只能加载后面。甚至 node_modules/**/test/*.json 都有可能导致 flow 错误。 目前没有什么优雅的办法:https://github.com/facebook/flow/issues/869

1
2
[ignore]
.*\/node_modules\/draft-js\/lib\/.*.js.flow.*
  1. 关于 $FlowFixMe

有时 flow 不够聪明,即使我们知道没有那个逻辑,当然有时我们可以绕过这些报错。个人认为用额外的逻辑来弥补 flow 的错误更不合适。$FlowFixMe 是很烦,但是至少不会引起困惑。

HMR 踩坑记

昨天遇到一个问题:使用 next/tree 时报错,即使最简单的 Demo 也会报错,而官网提供的则不会,最终问题定位到 react-hot-loader 上。我使用的是 react-hot-loader@next版本,需要在 babel 配置中引入 react-hot-loader/babel plugin。禁用这个 plugin 正常渲染,启用就报错,这个问题太诡异了,所以有必要搞清楚 HMR。

阅读更多