【问题标题】:Azure Function creating too many connections to PostgreSQLAzure 函数创建太多与 PostgreSQL 的连接
【发布时间】:2019-11-20 11:09:34
【问题描述】:

我有一个 Azure 持久函数,它与同样托管在 Azure 中的 PostgreSQL 数据库进行交互。

PostgreSQL 数据库的连接限制为 50,此外,我的连接字符串将连接池大小限制为 40,为超级用户/管理员连接留出了空间。

尽管如此,在某些负载下我得到了错误

53300:剩余的连接槽保留给非复制超级用户连接

This documentation from Microsoft 似乎相关,但我似乎无法制作静态客户端,并且正如它所提到的,

因为您仍然会用完连接,所以您应该优化与数据库的连接。

我有这个方法

private IDbConnection GetConnection()
{
    return new NpgsqlConnection(Environment.GetEnvironmentVariable("PostgresConnectionString"));
}

当我想与 PostgreSQL 交互时,我确实喜欢这样

using (var connection = GetConnection())
{
    connection.Open();
    return await connection.QuerySingleAsync<int>(settings.Query().Insert, settings);
}

所以我正在创建(和处理)大量NpgsqlConnection 对象,但根据this,这应该没问题,因为连接池是在幕后处理的。但 Azure Functions 中的某些内容可能会使这种想法无效。

我注意到我最终得到了很多空闲连接(来自 pgAdmin): 基于此,我尝试摆弄Npgsql connection parameters,例如Connection Idle LifetimeTimeoutPooling,但连接过多的问题似乎在某种程度上仍然存在。此外,我尝试限制并发编排器和活动函数的数量(请参阅this doc),但这似乎部分违背了 Azure Functions 可扩展的目的。它确实有帮助 - 我得到的太多连接错误更少)。据推测,如果我继续用较低的数字测试它,我什至可能会消除它,但同样,这似乎与这一点无关,可能还有另一种解决方案。

如何在不最大化连接的情况下将 PostgreSQL 与 Azure Functions 结合使用?

【问题讨论】:

  • 您绝对应该创建一个不需要每次运行都重新初始化的公共静态客户端。请向我们展示您的代码
  • public static 客户端会是什么样子?如果我创建一个public static NpgsqlConnection,我会收到错误 > A command is already in progress
  • @ScottH 我的回答没有帮助吗?
  • @HariHaran 不幸的是没有。
  • 目前最好的解决方案似乎是将连接池限制为 5 个连接并扩展 Timeout 值。

标签: postgresql azure azure-functions npgsql azure-durable-functions


【解决方案1】:

我没有好的解决方案,但我想我已经解释了为什么会发生这种情况。

为什么 Azure Function App 会达到最大连接数?

即使您为池大小指定了 40 个限制,它仅在函数应用的一个实例上得到遵守。请注意,函数应用可以根据负载横向扩展。它可以在同一个函数应用实例中同时处理多个请求,此外它还可以创建应用的新实例。同一实例中的并发请求将遵循池大小设置。但在多个实例的情况下,每个实例最终使用的池大小为 40。

即使是持久函数中的并发限制也不能解决这个问题,因为它们只限制单个实例,而不是跨实例。

如何将 PostgreSQL 与 Azure Functions 结合使用而不使连接达到最大值?

很遗憾,函数式应用不提供执行此操作的本机方式。请注意,连接池大小不是由函数运行时管理,而是由 npgsql 的库代码管理。运行在不同实例上的这个库代码不能相互通信。

请注意,这是使用共享资源的经典问题。在这种情况下,您有 50 个这些资源。支持更多消费者的最有效方法是减少每个消费者使用资源的时间。 大幅减少Connection Idle Lifetime 可能是最有效的方法。增加Timeout 确实有助于减少错误(并且是一个不错的选择),但不会增加吞吐量。它只是平滑了负载。减少Maximum Pool size也不错。

将其视为共享资源上的锁。您会希望在最短的时间内使用锁。当一个连接打开时,它会锁定 50 个连接中的一个。通常,SQL 库会进行池化,并保持连接打开以节省每个新连接所涉及的初始设置时间。但是,如果这限制了并发性,那么最好尽快终止空闲连接。在应用程序的单个实例中,库会在达到最大池大小时自动执行此操作。但在多个实例中,它无法终止另一个实例的连接。

需要注意的一点是,减少Maximum Pool Size 并不一定会限制您的应用程序的并发性。在大多数情况下,它只是减少了 idle 连接的数量 - 代价是 - 在以后需要建立新连接时支付初始设置时间。

更新

WEBSITE_MAX_DYNAMIC_APPLICATION_SCALE_OUT 可能有用。您可以将其设置为 5,将池大小设置为 8 或类似值。如果减少 Maximum Pool SizeConnection Idle Lifetime 没有帮助,我会这样做。

【讨论】:

    【解决方案2】:

    这就是Dependency Injection 真正有用的地方。您可以创建一个singleton 客户端,它会完美地完成这项工作。如果您想了解有关服务生命周期的更多信息,可以在docs 阅读它

    1. 先添加这个nugetMicrosoft.Azure.Functions.Extensions.DependencyInjection

    2. 现在添加一个如下所示的新类并解析您的客户端。

    [assembly: FunctionsStartup(typeof(Kovai.Serverless360.Functions.Startup))]

    namespace MyFunction
    {
        class Startup : FunctionsStartup
        {
            public override void Configure(IFunctionsHostBuilder builder)
            {
                ResolveDependencies(builder);
            }
        }
        public void ResolveDependencies(IFunctionsHostBuilder builder)
        {
            var conStr = Environment.GetEnvironmentVariable("PostgresConnectionString");
            builder.Services.AddSingleton((s) =>
            {
                return new NpgsqlConnection(conStr);
            }
        }
    }
    

    现在您可以轻松地从您的任何函数中使用它

    public FunctionA
        {
            private readonly NpgsqlConnection _connection;
            public FunctionA(NpgsqlConnection conn)
            {
                _connection = conn;
            }
    
            public async Task<HttpResponseMessage> Run()
            {
                //do something with your _connection
            }
        }
    

    【讨论】:

    • 依赖注入是一个不错的选择,但在这种情况下,如果您需要声明一个要在所有实例之间共享的静态客户端,则不需要这样做
    【解决方案3】:

    这是一个使用静态 HttpClient 的示例,您应该考虑这一点,这样您就不需要显式管理连接,而是允许您的客户端这样做:

    public static class PeriodicHealthCheckFunction
    {
        private static HttpClient _httpClient = new HttpClient();
    
        [FunctionName("PeriodicHealthCheckFunction")]
        public static async Task Run(
            [TimerTrigger("0 */5 * * * *")]TimerInfo healthCheckTimer,
            ILogger log)
        {
            string status = await _httpClient.GetStringAsync("https://localhost:5001/healthcheck");
    
            log.LogInformation($"Health check performed at: {DateTime.UtcNow} | Status: {status}");
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2015-06-13
      • 2016-02-19
      • 2014-07-25
      • 2014-05-23
      • 2021-10-03
      • 2019-11-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多