Skip to main content
Version: v6 - stable

在 AWS Lambda 中使用 sequelize

AWS Lambda 是一种无服务器计算服务,允许客户运行代码而无需担心底层服务器。如果未正确理解某些概念并且未使用适当的配置,则在 AWS Lambda 中使用 sequelize 可能会很棘手。本指南旨在阐明其中一些概念,以便库的用户可以为 AWS Lambda 正确配置 sequelize 并解决问题。

¥AWS Lambda is a serverless computing service that allows customers to run code without having to worry about the underlying servers. Using sequelize in AWS Lambda can be tricky if certain concepts are not properly understood and an appropriate configuration is not used. This guide seeks to clarify some of these concepts so users of the library can properly configure sequelize for AWS Lambda and troubleshoot issues.

TL;DR

如果你只是想了解如何为 AWS Lambda 正确配置 sequelize 连接池,你需要知道的是 sequelize 连接池与 AWS Lambda 的 Node.js 运行时不能很好地配合,最终导致的问题比它解决的问题还要多。因此,最合适的配置是在同一调用中使用池化,并避免跨调用进行池化(即最后关闭所有连接):

¥If you just want to learn how to properly configure sequelize connection pooling for AWS Lambda, all you need to know is that sequelize connection pooling does not get along well with AWS Lambda's Node.js runtime and it ends up causing more problems than it solves. Therefore, the most appropriate configuration is to use pooling within the same invocation and avoid pooling across invocations (i.e. close all connections at the end):

const { Sequelize } = require("sequelize");

let sequelize = null;

async function loadSequelize() {
const sequelize = new Sequelize(/* (...) */, {
// (...)
pool: {
/*

* Lambda functions process one request at a time but your code may issue multiple queries

* concurrently. Be wary that `sequelize` has methods that issue 2 queries concurrently

* (e.g. `Model.findAndCountAll()`). Using a value higher than 1 allows concurrent queries to

* be executed in parallel rather than serialized. Careful with executing too many queries in

* parallel per Lambda function execution since that can bring down your database with an

* excessive number of connections.

* * Ideally you want to choose a `max` number where this holds true:

* max * EXPECTED_MAX_CONCURRENT_LAMBDA_INVOCATIONS < MAX_ALLOWED_DATABASE_CONNECTIONS * 0.8
*/
max: 2,
/*

* Set this value to 0 so connection pool eviction logic eventually cleans up all connections

* in the event of a Lambda function timeout.
*/
min: 0,
/*

* Set this value to 0 so connections are eligible for cleanup immediately after they're

* returned to the pool.
*/
idle: 0,
// Choose a small enough value that fails fast if a connection takes too long to be established.
acquire: 3000,
/*

* Ensures the connection pool attempts to be cleaned up automatically on the next Lambda

* function invocation, if the previous invocation timed out.
*/
evict: CURRENT_LAMBDA_FUNCTION_TIMEOUT
}
});

// or `sequelize.sync()`
await sequelize.authenticate();

return sequelize;
}

module.exports.handler = async function (event, callback) {
// re-use the sequelize instance across invocations to improve performance
if (!sequelize) {
sequelize = await loadSequelize();
} else {
// restart connection pool to ensure connections are not re-used across invocations
sequelize.connectionManager.initPools();

// restore `getConnection()` if it has been overwritten by `close()`
if (sequelize.connectionManager.hasOwnProperty("getConnection")) {
delete sequelize.connectionManager.getConnection;
}
}

try {
return await doSomethingWithSequelize(sequelize);
} finally {
// close any opened connections during the invocation
// this will wait for any in-progress queries to finish before closing the connections
await sequelize.connectionManager.close();
}
};

使用 AWS RDS 代理

¥Using AWS RDS Proxy

如果你使用的是 AWS RDS,并且使用的是 极光支持的数据库引擎,则使用 AWS RDS 代理 连接到你的数据库。这将确保每次调用时打开/关闭连接对于底层数据库服务器来说不是一个昂贵的操作。

¥If your are using AWS RDS and you are using Aurora or a supported database engine, then connect to your database using AWS RDS Proxy. This will make sure that opening/closing connections on each invocation is not an expensive operation for your underlying database server.


如果你想了解为什么必须在 AWS Lambda 中以这种方式使用 sequelize,请继续阅读本文档的其余部分:

¥If you want to understand why you must use sequelize this way in AWS Lambda, continue reading the rest of this document:

Node.js 事件循环

¥The Node.js event loop

Node.js 事件循环 是:

¥The Node.js event loop is:

是什么让 Node.js 能够执行非阻塞 I/O 操作 - 尽管 JavaScript 是单线程的——

¥what allows Node.js to perform non-blocking I/O operations — despite the fact that JavaScript is single-threaded —

虽然事件循环实现是用 C++ 实现的,但这里有一个简化的 JavaScript 伪实现,它说明了 Node.js 如何执行名为 index.js 的脚本:

¥While the event loop implementation is in C++, here's a simplified JavaScript pseudo-implementation that illustrates how Node.js would execute a script named index.js:

// see: https://nodejs.cn/docs/guides/event-loop-timers-and-nexttick/
// see: https://www.youtube.com/watch?v=P9csgxBgaZ8
// see: https://www.youtube.com/watch?v=PNa9OMajw9w
const process = require('process');

/*

* counter of pending events

* * reference counter is increased for every:

* * 1. scheduled timer: `setTimeout()`, `setInterval()`, etc.

* 2. scheduled immediate: `setImmediate()`.

* 3. syscall of non-blocking IO: `require('net').Server.listen()`, etc.

* 4. scheduled task to the thread pool: `require('fs').WriteStream.write()`, etc.

* * reference counter is decreased for every:

* * 1. elapsed timer

* 2. executed immediate

* 3. completed non-blocking IO

* 4. completed thread pool task

* * references can be explicitly decreased by invoking `.unref()` on some

* objects like: `require('net').Socket.unref()`
*/
let refs = 0;

/*

* a heap of timers, sorted by next ocurrence

* * whenever `setTimeout()` or `setInterval()` is invoked, a timer gets added here
*/
const timersHeap = /* (...) */;

/*

* a FIFO queue of immediates

* * whenever `setImmediate()` is invoked, it gets added here
*/
const immediates = /* (...) */;

/*

* a FIFO queue of next tick callbacks

* * whenever `require('process').nextTick()` is invoked, the callback gets added here
*/
const nextTickCallbacks = [];

/*

* a heap of Promise-related callbacks, sorted by promise constructors callbacks first,

* and then resolved/rejected callbacks

* * whenever a new Promise instance is created via `new Promise` or a promise resolves/rejects

* the appropriate callback (if any) gets added here
*/
const promiseCallbacksHeap = /* ... */;

function execTicksAndPromises() {
while (nextTickCallbacks.length || promiseCallbacksHeap.size()) {
// execute all callbacks scheduled with `process.nextTick()`
while (nextTickCallbacks.length) {
const callback = nextTickCallbacks.shift();
callback();
}

// execute all promise-related callbacks
while (promiseCallbacksHeap.size()) {
const callback = promiseCallbacksHeap.pop();
callback();
}
}
}

try {
// execute index.js
require('./index');
execTicksAndPromises();

do {
// timers phase: executes all elapsed timers
getElapsedTimerCallbacks(timersHeap).forEach(callback => {
callback();
execTicksAndPromises();
});

// pending callbacks phase: executes some system operations (like `TCP errors`) that are not
// executed in the poll phase
getPendingCallbacks().forEach(callback => {
callback();
execTicksAndPromises();
})

// poll phase: gets completed non-blocking I/O events or thread pool tasks and invokes the
// corresponding callbacks; if there are none and there's no pending immediates,
// it blocks waiting for events/completed tasks for a maximum of `maxWait`
const maxWait = computeWhenNextTimerElapses(timersHeap);
pollForEventsFromKernelOrThreadPool(maxWait, immediates).forEach(callback => {
callback();
execTicksAndPromises();
});

// check phase: execute available immediates; if an immediate callback invokes `setImmediate()`
// it will be invoked on the next event loop iteration
getImmediateCallbacks(immediates).forEach(callback => {
callback();
execTicksAndPromises();
});

// close callbacks phase: execute special `.on('close')` callbacks
getCloseCallbacks().forEach(callback => {
callback();
execTicksAndPromises();
});

if (refs === 0) {
// listeners of this event may execute code that increments `refs`
process.emit('beforeExit');
}
} while (refs > 0);
} catch (err) {
if (!process.listenerCount('uncaughtException')) {
// default behavior: print stack and exit with status code 1
console.error(err.stack);
process.exit(1);
} else {
// there are listeners: emit the event and exit using `process.exitCode || 0`
process.emit('uncaughtException');
process.exit();
}
}

Node.js 中的 AWS Lambda 函数处理程序类型

¥AWS Lambda function handler types in Node.js

Node.js 中的 AWS Lambda 处理程序有两种类型:

¥AWS Lambda handlers come in two flavors in Node.js:

非异步处理程序(即 callback):

¥Non-async handlers (i.e. callback):

module.exports.handler = function (event, context, callback) {
try {
doSomething();
callback(null, "Hello World!"); // Lambda returns "Hello World!"
} catch (err) {
// try/catch is not required, uncaught exceptions invoke `callback(err)` implicitly
callback(err); // Lambda fails with `err`
}
};

异步处理程序(即使用 async/awaitPromise):

¥Async handlers (i.e. use async/await or Promises):

// async/await
module.exports.handler = async function (event, context) {
try {
await doSomethingAsync();
return "Hello World!"; // equivalent of: callback(null, "Hello World!");
} catch (err) {
// try/cath is not required, async functions always return a Promise
throw err; // equivalent of: callback(err);
}
};

// Promise
module.exports.handler = function (event, context) {
/*

* must return a `Promise` to be considered an async handler

* * an uncaught exception that prevents a `Promise` to be returned

* by the handler will "downgrade" the handler to non-async
*/
return Promise.resolve()
.then(() => doSomethingAsync())
.then(() => "Hello World!");
};

虽然乍一看似乎异步与非异步处理程序只是代码样式的选择,但两者之间存在根本区别:

¥While at first glance it seems like async VS non-async handlers are simply a code styling choice, there is a fundamental difference between the two:

  • 在异步处理程序中,当处理程序返回的 Promise 解析或拒绝时,Lambda 函数执行完成,无论事件循环是否为空。

    ¥In async handlers, a Lambda function execution finishes when the Promise returned by the handler resolves or rejects, regardless of whether the event loop is empty or not.

  • 在非异步处理程序中,当发生以下条件之一时,Lambda 函数执行结束:

    ¥In non-async handlers, a Lambda function execution finishes when one of the following conditions occur:

为了合理化 sequelize 如何受其影响,理解这一根本差异非常重要。下面举几个例子来说明差异:

¥This fundamental difference is very important to understand in order to rationalize how sequelize may be affected by it. Here are a few examples to illustrate the difference:

// no callback invoked
module.exports.handler = function () {
// Lambda finishes AFTER `doSomething()` is invoked
setTimeout(() => doSomething(), 1000);
};

// callback invoked
module.exports.handler = function (event, context, callback) {
// Lambda finishes AFTER `doSomething()` is invoked
setTimeout(() => doSomething(), 1000);
callback(null, "Hello World!");
};

// callback invoked, context.callbackWaitsForEmptyEventLoop = false
module.exports.handler = function (event, context, callback) {
// Lambda finishes BEFORE `doSomething()` is invoked
context.callbackWaitsForEmptyEventLoop = false;
setTimeout(() => doSomething(), 2000);
setTimeout(() => callback(null, "Hello World!"), 1000);
};

// async/await
module.exports.handler = async function () {
// Lambda finishes BEFORE `doSomething()` is invoked
setTimeout(() => doSomething(), 1000);
return "Hello World!";
};

// Promise
module.exports.handler = function () {
// Lambda finishes BEFORE `doSomething()` is invoked
setTimeout(() => doSomething(), 1000);
return Promise.resolve("Hello World!");
};

AWS Lambda 执行环境(即容器)

¥AWS Lambda execution environments (i.e. containers)

AWS Lambda 函数处理程序由内置或自定义 runtimes 调用,该 runtimes 在跨调用的执行环境(即容器)中运行。容器只能处理 一次一个请求。Lambda 函数的并发调用意味着将为每个并发请求创建一个容器实例。

¥AWS Lambda function handlers are invoked by built-in or custom runtimes which run in execution environments (i.e. containers) that may or may not be re-used across invocations. Containers can only process one request at a time. Concurrent invocations of a Lambda function means that a container instance will be created for each concurrent request.

实际上,这意味着 Lambda 函数应设计为无状态,但开发者可以使用状态进行缓存:

¥In practice, this means that Lambda functions should be designed to be stateless but developers can use state for caching purposes:

let sequelize = null;

module.exports.handler = async function () {
/*

* sequelize will already be loaded if the container is re-used

* * containers are never re-used when a Lambda function's code change

* * while the time elapsed between Lambda invocations is used as a factor to determine whether

* a container is re-used, no assumptions should be made of when a container is actually re-used

* * AWS does not publicly document the rules of container re-use "by design" since containers

* can be recycled in response to internal AWS Lambda events (e.g. a Lambda function container

* may be recycled even if the function is constanly invoked)
*/
if (!sequelize) {
sequelize = await loadSequelize();
}

return await doSomethingWithSequelize(sequelize);
};

当 Lambda 函数不等待事件循环为空并且重新使用容器时,事件循环将为 "paused",直到下一次调用发生。例如:

¥When a Lambda function doesn't wait for the event loop to be empty and a container is re-used, the event loop will be "paused" until the next invocation occurs. For example:

let counter = 0;

module.exports.handler = function (event, context, callback) {
/*

* The first invocation (i.e. container initialized) will:

* - log:

* - Fast timeout invoked. Request id: 00000000-0000-0000-0000-000000000000 | Elapsed ms: 5XX

* - return: 1

* * Wait 3 seconds and invoke the Lambda again. The invocation (i.e. container re-used) will:

* - log:

* - Slow timeout invoked. Request id: 00000000-0000-0000-0000-000000000000 | Elapsed ms: 3XXX

* - Fast timeout invoked. Request id: 11111111-1111-1111-1111-111111111111 | Elapsed ms: 5XX

* - return: 3
*/
const now = Date.now();

context.callbackWaitsForEmptyEventLoop = false;

setTimeout(() => {
console.log(
"Slow timeout invoked. Request id:",
context.awsRequestId,
"| Elapsed ms:",
Date.now() - now
);
counter++;
}, 1000);

setTimeout(() => {
console.log(
"Fast timeout invoked. Request id:",
context.awsRequestId,
"| Elapsed ms:",
Date.now() - now
);
counter++;
callback(null, counter);
}, 500);
};

AWS Lambda 中的 Sequelize 连接池

¥Sequelize connection pooling in AWS Lambda

sequelize 使用连接池来优化数据库连接的使用。sequelize 使用的连接池是使用 setTimeout() 回调(由 Node.js 事件循环处理)实现的。

¥sequelize uses connection pooling for optimizing usage of database connections. The connection pool used by sequelize is implemented using setTimeout() callbacks (which are processed by the Node.js event loop).

鉴于 AWS Lambda 容器一次处理一个请求,人们可能会想按如下方式配置 sequelize

¥Given the fact that AWS Lambda containers process one request at a time, one would be tempted to configure sequelize as follows:

const { Sequelize } = require('sequelize');

const sequelize = new Sequelize(/* (...) */, {
// (...)
pool: { min: 1, max: 1 }
});

此配置可防止 Lambda 容器因连接数量过多而压垮数据库服务器(因为每个容器最多占用 1 个连接)。它还确保容器的连接在空闲时不会被垃圾收集,因此在重新使用 Lambda 容器时不需要重新建立连接。不幸的是,这种配置存在一系列问题:

¥This configuration prevents Lambda containers from overwhelming the database server with an excessive number of connections (since each container takes at most 1 connection). It also makes sure that the container's connection is not garbage collected when idle so the connection does not need to be re-established when the Lambda container is re-used. Unfortunately, this configuration presents a set of issues:

  1. 等待事件循环为空的 Lambda 总是会超时。sequelize 连接池每 options.pool.evict 毫秒调度一次 setTimeout,直到所有空闲连接都被关闭。然而,由于 min 设置为 1,池中总会有至少一个空闲连接,从而导致无限事件循环。

    ¥Lambdas that wait for the event loop to be empty will always time out. sequelize connection pools schedule a setTimeout every options.pool.evict ms until all idle connections have been closed. However, since min is set to 1, there will always be at least one idle connection in the pool, resulting in an infinite event loop.

  2. 某些操作(例如 Model.findAndCountAll())异步执行多个查询(例如 Model.count()Model.findAll())。最多使用一个连接会强制查询串行执行(而不是使用两个连接并行执行)。虽然为了维持可管理数量的数据库连接,这可能是可接受的性能折衷,但如果查询需要超过默认或配置的 options.pool.acquire 超时才能完成,长时间运行的查询可能会导致 ConnectionAcquireTimeoutError。这是因为序列化查询将在池中等待,直到释放其他查询使用的连接。

    ¥Some operations like Model.findAndCountAll() execute multiple queries asynchronously (e.g. Model.count() and Model.findAll()). Using a maximum of one connection forces the queries to be executed serially (rather than in parallel using two connections). While this may be an acceptable performance compromise in order to maintain a manageable number of database connections, long running queries may result in ConnectionAcquireTimeoutError if a query takes more than the default or configured options.pool.acquire timeout to complete. This is because the serialized query will be stuck waiting on the pool until the connection used by the other query is released.

  3. 如果 AWS Lambda 函数超时(即超出配置的 AWS Lambda 超时),则 Node.js 事件循环将为 "paused",无论其状态如何。这可能会导致竞争条件,从而导致连接错误。例如,你可能会遇到这样的情况:非常昂贵的查询导致 Lambda 函数超时,在昂贵的查询完成并将连接释放回池之前,事件循环为 "paused",并且如果以下情况,后续 Lambda 调用将失败并显示 ConnectionAcquireTimeoutError: 容器被重新使用,并且在 options.pool.acquire 毫秒后连接尚未返回。

    ¥If the AWS Lambda function times out (i.e. the configured AWS Lambda timeout is exceeded), the Node.js event loop will be "paused" regardless of its state. This can cause race conditions that result in connection errors. For example, you may encounter situations where a very expensive query causes a Lambda function to time out, the event loop is "paused" before the expensive query finishes and the connection is released back to the pool, and subsequent Lambda invocations fail with a ConnectionAcquireTimeoutError if the container is re-used and the connection has not been returned after options.pool.acquire ms.

你可以尝试使用 { min: 1, max: 2 } 来缓解问题 #2。然而,这仍然会遇到问题#1 和#3,同时引入其他问题:

¥You can attempt to mitigate issue #2 by using { min: 1, max: 2 }. However, this will still suffer from issues #1 and #3 whilst introducing additional ones:

  1. 如果连接池逐出回调执行之前的偶数循环 "pauses" 或 Lambda 调用之间的时间间隔超过 options.pool.evict,则可能会出现竞争条件。这可能会导致超时错误、握手错误和其他与连接相关的错误。

    ¥Race conditions may occur where the even loop "pauses" before a connection pool eviction callback executes or more than options.pool.evict time elapses between Lambda invocations. This can result in timeout errors, handshake errors, and other connection-related errors.

  2. 如果你使用像 Model.findAndCountAll() 这样的操作,并且底层 Model.count()Model.findAll() 查询失败,你将无法确保在 Lambda 函数执行完成之前另一个查询已完成执行(并且连接放回到池中),并且 事件循环是 "paused"。这可能会使连接处于旧状态,从而导致 TCP 连接过早关闭和其他与连接相关的错误。

    ¥If you use an operation like Model.findAndCountAll() and either the underlying Model.count() or Model.findAll() queries fail, you won't be able to ensure that the other query has finished executing (and the connection is put back into the pool) before the Lambda function execution finishes and the event loop is "paused". This can leave connections in a stale state which can result in prematurely closed TCP connections and other connection-related errors.

使用 { min: 2, max: 2 } 可以缓解额外的问题#1。然而,该配置仍然存在所有其他问题(原始#1、#3 和附加#2)。

¥Using { min: 2, max: 2 } mitigates additional issue #1. However, the configuration still suffers from all the other issues (original #1, #3, and additional #2).

详细的竞争条件示例

¥Detailed race condition example

为了理解该示例,你需要更多有关 Lambda 和 sequelize 的某些部分是如何实现的背景信息。

¥In order to make sense of the example, you'll need a bit more context of how certain parts of Lambda and sequelize are implemented.

nodejs.12x 的内置 AWS Lambda 运行时是在 Node.js 中实现的。你可以通过读取 Node.js Lambda 函数内 /var/runtime/ 的内容来访问运行时的整个源代码。相关子集的代码如下:

¥The built-in AWS Lambda runtime for nodejs.12x is implemented in Node.js. You can access the entire source code of the runtime by reading the contents of /var/runtime/ inside a Node.js Lambda function. The relevant subset of the code is as follows:

运行时/Runtime.js

¥runtime/Runtime.js

class Runtime {
// (...)

// each iteration is executed in the event loop `check` phase
scheduleIteration() {
setImmediate(() => this.handleOnce().then(/* (...) */));
}

async handleOnce() {
// get next invocation. see: https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html#runtimes-api-next
let { bodyJson, headers } = await this.client.nextInvocation();

// prepare `context` handler parameter
let invokeContext = new InvokeContext(headers);
invokeContext.updateLoggingContext();

// prepare `callback` handler parameter
let [callback, callbackContext] = CallbackContext.build(
this.client,
invokeContext.invokeId,
this.scheduleIteration.bind(this)
);

try {
// this listener is subscribed to process.on('beforeExit')
// so that when when `context.callbackWaitsForEmptyEventLoop === true`
// the Lambda execution finishes after the event loop is empty
this._setDefaultExitListener(invokeContext.invokeId);

// execute handler
const result = this.handler(
JSON.parse(bodyJson),
invokeContext.attachEnvironmentData(callbackContext),
callback
);

// finish the execution if the handler is async
if (_isPromise(result)) {
result
.then(callbackContext.succeed, callbackContext.fail)
.catch(callbackContext.fail);
}
} catch (err) {
callback(err);
}
}
}

运行时在初始化代码末尾安排一次迭代:

¥The runtime schedules an iteration at the end of the initialization code:

运行时/index.js

¥runtime/index.js

// (...)

new Runtime(client, handler, errorCallbacks).scheduleIteration();

Lambda 处理程序使用 sequelize 调用的所有 SQL 查询最终都使用 Sequelize.prototype.query() 执行。该方法负责从池中获取连接,执行查询,并在查询完成时将连接释放回池中。以下代码片段显示了无事务查询的方法逻辑的简化:

¥All SQL queries invoked by a Lambda handler using sequelize are ultimately executed using Sequelize.prototype.query(). This method is responsible for obtaining a connection from the pool, executing the query, and releasing the connection back to the pool when the query completes. The following snippet shows a simplification of the method's logic for queries without transactions:

sequelize.js

class Sequelize {
// (...)

query(sql, options) {
// (...)

const connection = await this.connectionManager.getConnection(options);
const query = new this.dialect.Query(connection, this, options);

try {
return await query.run(sql, bindParameters);
} finally {
await this.connectionManager.releaseConnection(connection);
}
}
}

字段 this.connectionManager 是特定于方言的 ConnectionManager 类的实例。所有特定于方言的管理器都继承自抽象 ConnectionManager 类,该类初始化连接池并将其配置为在每次需要创建新连接时调用特定于方言的类的 connect() 方法。以下代码片段显示了 mysql 方言 connect() 方法的简化:

¥The field this.connectionManager is an instance of a dialect-specific ConnectionManager class. All dialect-specific managers inherit from an abstract ConnectionManager class which initializes the connection pool and configures it to invoke the dialect-specific class' connect() method everytime a new connection needs to be created. The following snippet shows a simplification of the mysql dialect connect() method:

mysql/连接管理器.js

¥mysql/connection-manager.js

class ConnectionManager {
// (...)

async connect(config) {
// (...)
return await new Promise((resolve, reject) => {
// uses mysql2's `new Connection()`
const connection = this.lib.createConnection(connectionConfig);

const errorHandler = (e) => {
connection.removeListener("connect", connectHandler);
connection.removeListener("error", connectHandler);
reject(e);
};

const connectHandler = () => {
connection.removeListener("error", errorHandler);
resolve(connection);
};

connection.on("error", errorHandler);
connection.once("connect", connectHandler);
});
}
}

字段 this.lib 引用 mysql2,函数 createConnection() 通过创建 Connection 类的实例来创建连接。该类的相关子集如下:

¥The field this.lib refers to mysql2 and the function createConnection() creates a connection by creating an instance of a Connection class. The relevant subset of this class is as follows:

mysql2/connection.js

class Connection extends EventEmitter {
constructor(opts) {
// (...)

// create Socket
this.stream = /* (...) */;

// when data is received, clear timeout
this.stream.on('data', data => {
if (this.connectTimeout) {
Timers.clearTimeout(this.connectTimeout);
this.connectTimeout = null;
}
this.packetParser.execute(data);
});

// (...)

// when handshake is completed, emit the 'connect' event
handshakeCommand.on('end', () => {
this.emit('connect', handshakeCommand.handshake);
});

// set a timeout to trigger if no data is received on the socket
if (this.config.connectTimeout) {
const timeoutHandler = this._handleTimeoutError.bind(this);
this.connectTimeout = Timers.setTimeout(
timeoutHandler,
this.config.connectTimeout
);
}
}

// (...)

_handleTimeoutError() {
if (this.connectTimeout) {
Timers.clearTimeout(this.connectTimeout);
this.connectTimeout = null;
}
this.stream.destroy && this.stream.destroy();
const err = new Error('connect ETIMEDOUT');
err.errorno = 'ETIMEDOUT';
err.code = 'ETIMEDOUT';
err.syscall = 'connect';

// this will emit the 'error' event
this._handleNetworkError(err);
}
}

根据前面的代码,以下事件序列显示了 { min: 1, max: 1 } 的连接池竞争条件如何导致 ETIMEDOUT 错误:

¥Based on the previous code, the following sequence of events shows how a connection pooling race condition with { min: 1, max: 1 } can result with in a ETIMEDOUT error:

  1. 收到 Lambda 调用(新容器):

    ¥A Lambda invocation is received (new container):

    1. 事件循环进入 check 阶段,调用 runtime/Runtime.jshandleOnce() 方法。

      ¥The event loop enters the check phase and runtime/Runtime.js's handleOnce() method is invoked.

      1. handleOnce() 方法调用 await this.client.nextInvocation() 并等待。

        ¥The handleOnce() method invokes await this.client.nextInvocation() and waits.

    2. 由于没有待处理的计时器,事件循环会跳过 timers 阶段。

      ¥The event loop skips the timers phase since there no pending timers.

    3. 事件循环进入 poll 阶段,handleOnce() 方法继续。

      ¥The event loop enters the poll phase and the handleOnce() method continues.

    4. 调用 Lambda 处理程序。

      ¥The Lambda handler is invoked.

    5. Lambda 处理程序调用 Model.count()Model.count() 又调用 sequelize.jsquery()query() 又调用 connectionManager.getConnection()

      ¥The Lambda handler invokes Model.count() which invokes sequelize.js's query() which invokes connectionManager.getConnection().

    6. 连接池为 Model.count() 初始化一个 setTimeout(..., config.pool.acquire),并调用 mysql/connection-manager.jsconnect() 创建新连接。

      ¥The connection pool initializes a setTimeout(..., config.pool.acquire) for Model.count() and invokes mysql/connection-manager.js's connect() to create a new connection.

    7. mysql2/connection.js 创建 TCP 套接字并初始化 setTimeout(),以防止与 ETIMEDOUT 的连接失败。

      ¥mysql2/connection.js creates the TCP socket and initializes a setTimeout() for failing the connection with ETIMEDOUT.

    8. 处理程序返回的 Promise 被拒绝(此处未详细说明原因),因此 Lambda 函数执行完成,Node.js 事件循环为 "paused"。

      ¥The promise returned by the handler rejects (for reasons not detailed here) so the Lambda function execution finishes and the Node.js event loop is "paused".

  2. 调用之间有足够的时间间隔,以便:

    ¥Enough time elapses between invocations so that:

    1. config.pool.acquire 计时器到时。

      ¥config.pool.acquire timer elapses.

    2. mysql2 连接计时器尚未到期,但已几乎到期(即竞争条件)。

      ¥mysql2 connection timer has not elapsed yet but has almost elapsed (i.e. race condition).

  3. 收到第二个 Lambda 调用(容器重新使用):

    ¥A second Lambda invocation is received (container re-used):

    1. 事件循环是 "resumed"。

      ¥The event loop is "resumed".

    2. 事件循环进入 check 阶段,调用 runtime/Runtime.jshandleOnce() 方法。

      ¥The event loop enters the check phase and runtime/Runtime.js's handleOnce() method is invoked.

    3. 事件循环进入 timers 阶段,config.pool.acquire 计时器到期,导致先前调用的 Model.count() promise 被 ConnectionAcquireTimeoutError 拒绝。

      ¥The event loop enters the timers phase and the config.pool.acquire timer elapses, causing the previous invocation's Model.count() promise to reject with ConnectionAcquireTimeoutError.

    4. 事件循环进入 poll 阶段,handleOnce() 方法继续。

      ¥The event loop enters the poll phase and the handleOnce() method continues.

    5. 调用 Lambda 处理程序。

      ¥The Lambda handler is invoked.

    6. Lambda 处理程序调用 Model.count()Model.count() 又调用 sequelize.jsquery()query() 又调用 connectionManager.getConnection()

      ¥The Lambda handler invokes Model.count() which invokes sequelize.js's query() which invokes connectionManager.getConnection().

    7. 连接池为 Model.count() 初始化 setTimeout(..., config.pool.acquire),从 { max : 1 } 开始,它等待待处理的 connect() promise 完成。

      ¥The connection pool initializes a setTimeout(..., config.pool.acquire) for Model.count() and since { max : 1 } it waits for the pending connect() promise to complete.

    8. 由于没有待处理的立即数,事件循环会跳过 check 阶段。

      ¥The event loop skips the check phase since there are no pending immediates.

    9. 竞态条件:事件循环进入 timers 阶段并且 mysql2 连接超时已过,导致使用 connection.emit('error') 触发 ETIMEDOUT 错误。

      ¥Race condition: The event loop enters the timers phase and the mysql2 connection timeout elapses, resulting in a ETIMEDOUT error that is emitted using connection.emit('error').

    10. 触发的事件拒绝 mysql/connection-manager.jsconnect() 中的 promise,而 connect() 又将被拒绝的 promise 转发到 Model.count() 查询的 promise。

      ¥The emitted event rejects the promise in mysql/connection-manager.js's connect() which in turn forwards the rejected promise to the Model.count() query's promise.

    11. lambda 函数失败并出现 ETIMEDOUT 错误。

      ¥The lambda function fails with an ETIMEDOUT error.