【问题标题】:Lambda SQL Server RDS Connection LeakLambda SQL Server RDS 连接泄漏
【发布时间】:2020-12-11 22:33:18
【问题描述】:

问题

我在频繁调用的 Lambda 中使用 mssql v6.2.0(在标准负载下始终有约 25 次并发调用)。

我似乎在连接池或其他方面遇到问题,因为我一直有大量打开的数据库连接,这些连接使我的数据库(RDS 上的 SQL Server)不堪重负,导致 Lambda 等待查询结果超时。

我已阅读文档、各种类似问题、Github 问题等,但对于这个特定问题没有任何效果。

我已经学到的东西

  • 我确实了解到可以跨调用进行池化,因为处理函数之外的变量在同一容器中的调用之间共享。这让我觉得我应该只看到运行我的 Lambda 的每个容器的几个连接,但我不知道有多少,所以很难验证。最重要的是,池化应该可以防止我打开大量的连接,所以有些东西不能正常工作。
  • 有几种不同的方法可以使用mssql,我已经尝试了其中的几种。值得注意的是,我尝试使用大值和小值指定最大池大小,但得到了相同的结果。
  • AWS 建议您在尝试创建新池之前检查是否已有池。我试过没有用。有点像pool = pool || await createPool()
  • 我知道 RDS Proxy 的存在可以帮助解决此类情况,但似乎(目前)不为 SQL Server 实例提供它。
  • 我确实可以稍微降低数据速度,但这对整个产品的性能有轻微影响,所以我不想这样做只是为了避免解决数据库连接问题。
  • 不加检查,我一次看到多达 700 个与数据库的连接,这让我认为存在某种泄漏,这可能不仅仅是高使用率的合理结果。
  • 我没有找到按照 re:Invent 幻灯片的建议缩短 SQL Server 端连接 TTL 的方法。也许这就是答案的一部分?

代码

'use strict';

/* Dependencies */
const sql = require('mssql');
const fs = require('fs').promises;
const path = require('path');
const AWS = require('aws-sdk');
const GeoJSON = require('geojson');

AWS.config.update({ region: 'us-east-1' });
var iotdata = new AWS.IotData({ endpoint: process.env['IotEndpoint'] });

/* Export */

exports.handler = async function (event) {

    let myVal= event.Records[0].Sns.Message;

    // Gather prerequisites in parallel
    let [
        query1,
        query2,
        pool
    ] = await Promise.all([
        fs.readFile(path.join(__dirname, 'query1.sql'), 'utf8'),
        fs.readFile(path.join(__dirname, 'query2.sql'), 'utf8'),
        sql.connect(process.env['connectionString'])
    ]);

    // Query DB for updated data
    let results = await pool.request()
        .input('MyCol', sql.TYPES.VarChar, myVal)
        .query(query1);

    // Prepare IoT Core message
    let params = {
        topic: `${process.env['MyTopic']}/${results.recordset[0].TopicName}`,
        payload: convertToGeoJsonString(results.recordset),
        qos: 0
    };

    // Publish results to MQTT topic
    try {
        await iotdata.publish(params).promise();
        console.log(`Successfully published update for ${myVal}`);

        //Query 2
        await pool.request()
            .input('MyCol1', sql.TYPES.Float, results.recordset[0]['Foo'])
            .input('MyCol2', sql.TYPES.Float, results.recordset[0]['Bar'])
            .input('MyCol3', sql.TYPES.VarChar, results.recordset[0]['Baz'])
            .query(query2);
        
    } catch (err) {
        console.log(err);
    }
};

/**
 * Convert query results to GeoJSON for API response
 * @param {Array|Object} data - The query results
 */
function convertToGeoJsonString(data) {
    let result = GeoJSON.parse(data, { Point: ['Latitude', 'Longitude']});
    return JSON.stringify(result);
}

问题

请帮助我了解连接失控的原因以及解决方法。加分:在 Lambda 上处理高 DB 并发的理想策略是什么?

最终,该服务需要处理数倍于当前负载的负载——我意识到这将成为一个非常大的负载。我对只读副本或其他读取性能提升措施等选项持开放态度,只要它们与 SQL Server 兼容,而且它们不仅仅是为了编写正确的数据库访问代码。

如果我能改进这个问题,请告诉我。我知道那里有类似的,但我已经阅读/尝试了很多,但没有发现它们可以提供帮助。提前致谢!

相关资料

【问题讨论】:

    标签: concurrency aws-lambda amazon-rds


    【解决方案1】:

    回答

    经过4天的努力,我终于找到了答案。我需要做的就是扩大数据库。代码实际上没有问题。

    我从 db.t2.micro 升级到 db.t3.small(或 1 个 vCPU、1GB RAM 到 2 个 vCPU 和 2GB RAM),净成本约为 15 美元/月。

    理论

    就我而言,数据库可能无法同时处理我所有调用的处理(涉及多个地理计算)。我确实看到 CPU 上升了,但我认为这是高开放连接的结果。当查询速度变慢时,并发调用会随着 Lambda 开始等待结果而堆积起来,最终导致它们超时并且无法正确关闭连接。

    比较:

    db.t2.micro:

    • 200+ 数据库连接(如果你让它继续运行,它会持续增加)
    • 50 多个并发调用
    • 5000+ 毫秒 Lambda 持续时间(当事情变慢时,约 300 毫秒在空载下)

    db.t3.small:

    • 25-35 个数据库连接(不断)
    • ~5 次并发调用
    • ~33 毫秒 Lambda 持续时间

    CloudWatch 仪表板

    总结

    我认为这个问题让我感到困惑,因为它闻起来不像是容量问题。过去我几乎每次处理高数据库连接时,都是代码错误。在那里尝试过选项后,我认为这是我需要理解的“一些无服务器的神奇陷阱”。最后,它就像更改数据库层一样简单。我的看法是,数据库容量问题可能会以 CPU 和内存使用率高以外的其他方式表现出来,而且高连接可能是代码错误以外的其他原因造成的。

    更新(4 个月后)

    这继续运作良好。让我印象深刻的是,将数据库资源翻倍似乎已经提供了 > 2 倍的性能。现在,由于负载(或开发期间的临时错误),数据库连接变得非常高(甚至超过 1k),数据库处理它。我根本没有看到数据库连接超时或数据库因负载而陷入困境的任何问题。自最初撰写本文以来,我添加了几个 CPU 密集型查询来支持报告工作负载,并且它继续同时处理所有这些负载。

    自撰写本文以来,我们还为一位客户将此设置部署到生产环境中,它可以毫无问题地处理该工作负载。

    【讨论】:

    • 错了,如果你这样做,你会陷入一个痛苦的世界,伙计,你会在前进的时候遇到很大的扩展问题。你是对的,虽然实例太小了。如果您的 SNS 消息最终激增怎么办?最好的选择是构建一个批量语句并作为一个连接执行
    • 此外,您还需要了解容器生命周期和大小,例如具有 1024 内存的 Lambda 容器,因为它执行得更快,比小型容器便宜。(基本上是这样说)让我看看我是否可以为您找到一些文章。
    • 感谢您的反馈。你提议的是改变产品行为的架构改变。我希望“实时更新”完全由事件驱动,而不是在队列中等待批量处理。管道的每个部分都遵循这种范式,我宁愿扩大规模来处理负载,也不愿牺牲产品的性能来减少负载。只要连接被正确终止,我不知道为什么会出现扩展问题。也许我错过了什么。
    【解决方案2】:

    所以连接池在 Lambda 上根本没有用,你可以做的就是重用连接。

    问题是每次 Lambda 执行都会打开一个池,它只会像你得到的那样淹没数据库,你希望每个 lambda 容器有 1 个连接,你可以像这样使用一个 db 类(这很粗糙,但如果你知道的话,我会知道有问题)

        export default class MySQL {
    
        constructor() {
    
            this.connection = null
        }
    
        async getConnection() {
    
            if (this.connection === null || this.connection.state === 'disconnected') {
    
                return this.createConnection()
            }
    
            return this.connection
    
    
        }
    
        async createConnection() {
    
            this.connection = await mysql.createConnection({
                host: process.env.dbHost,
                user: process.env.dbUser,
                password: process.env.dbPassword,
                database: process.env.database,
            })
    
    
            return this.connection
        }
    
        async query(sql, params) {
    
            await this.getConnection()
    
            let err
            let rows
            [err, rows] = await to(this.connection.query(sql, params))
    
            if (err) {
    
                console.log(err)
                return false
            }
    
            return rows
        }
    
    }
    
    function to(promise) {
        return promise.then((data) => {
            return [null, data]
        }).catch(err => [err])
    }
    

    您需要了解的是 A lambda 执行是一个小型虚拟机,它执行一项任务然后停止,它确实会在那里停留一段时间,如果其他人需要它,它就会被重用对于单个任务的容器和连接,永远不会有多个连接到单个 lambda。

    希望这有助于让我知道您是否需要更多详细信息!哦,欢迎来到 stackoverflow,这是一个结构合理的问题。

    【讨论】:

    • 感谢您的回答。我还没有机会尝试,但我不知道这与我之前尝试过的pool = pool || await getPool() 有什么不同。看来这本质上就是getConnection() 在此示例中所做的。这是在做任何根本不同的事情吗? @MrkFldig
    • 因为你打开了一个连接池,这个池没有任何作用,它的作用是将 1 个 lambda 函数连接到 1 个 MySQL 连接,一个 MySQL 连接池不能在 Lambda 中使用,因为所有你最终是来自一个函数的大量连接,而不是在 25 次调用之间共享的大量连接,除非我误解了“池” - 请问您的连接字符串是什么没有凭据?
    • 顺便说一句,我可能在这里误解并做出了不正确的假设,但我确实知道以前使用 SNS 之前,我曾因 mysql connctions 而遇到过这种问题。
    • 根据我读过的一些文档,如果池可以在同一个容器上的调用之间共享,则池可以发挥作用(除非我误解了!)。请参阅我刚刚添加的问题中的最后一个链接。我的连接字符串看起来像mssql://user:pass@host.us-east-1.rds.amazonaws.com:1433/dbname。只是为了澄清我使用的是 SQL Server 而不是 MySQL,如果这很重要的话。我知道其中一些主体与 db 无关。
    • 是的,问题是节点是单线程的,所以我认为池不会有任何用途,它一次只有一个连接,但每个单独的容器将有 X 数量的连接来自一个池,所以在你的情况下(并发 SNS lambda 的数量)* 池大小。
    猜你喜欢
    • 2017-12-13
    • 2017-10-12
    • 1970-01-01
    • 2021-12-23
    • 2017-04-16
    • 2020-10-02
    • 1970-01-01
    相关资源
    最近更新 更多