当创建新的 pod 实例时,.Net Core 连接池在重负载峰值下耗尽(postgres)
.Net Core connection pool exhausted (postgres) under heavy load spike, when new pod instances are created
我有一个 运行 在重负载下稳定的应用程序,但只有在负载增加时才会如此。
我同时 运行 3 或 4 pods,并在必要时扩展到 8 或 10 pods。
标准 requests per minute
约为 4000
(意味着每个节点每秒 66
请求,意味着每个单个 pod 每秒 16
请求)。
有一种情况,当我们收到巨大的负载峰值(从每分钟 4k 到每分钟 20k)。正确创建了新 pods,然后它们开始接收新负载。
问题是,在大约 10-20% 的情况下,新创建的 pod 难以处理初始负载,数据库请求占用超过 5000 毫秒,堆积起来,最终导致连接池耗尽的异常:The connection pool has been exhausted, either raise MaxPoolSize (currently 200) or Timeout (currently 15 seconds)
以下是 NewRelic 的截图:
我可以看到其他 pods 做得很好,而且在最初的挣扎之后,所有 pods 都可以毫无问题地处理负载。
这是我在尝试修复它时所做的:
摆脱非异步调用。 我在 async
方法中有几行阻塞代码。我已将所有内容更改为 async
。我不再有非异步方法。
删除了 运行ning 长交易。 我们有 运行ning 长交易,如下所示:
- beginTransactionAsync
- 选择数据异步
- 保存数据异步
- commitTransactionAsync
我重构为:
- selectDataAsync
- saveDataAsync // under-the-hood EF core short lived transaction
帮助很大,但没有彻底解决问题
- 确保某些连接始终打开并就绪。 我们向连接字符串添加了
Minimum Pool Size=20
,以始终保持至少 20 个连接打开。
这也有帮助,但有时仍然 pods 挣扎。
我们的 pods 在就绪探测 returns 成功后正常启动。就绪探测使用标准 .NET 核心健康检查检查与数据库的连接。
我们的连接字符串有 MaxPoolSize=100;Timeout=15;
设置。
我当然希望最初的新 pod 实例在以较低参数运行时需要一些启动时间,但我不希望 pod 经常会窒息并抛出 90% 的错误。
此处重要提示:
我有 2 个不同的 DbContexts 共享相同的连接字符串(因此相同的连接池)。每个 DbContext 访问数据库中的不同模式。这样做是为了具有模块化架构。 DbContext 从不相互通信,也从不在同一请求中一起使用。
我目前的猜测是,当 pod 刚创建并立即接收到巨大的负载时,pod 会尝试打开所有 100 个连接(在 DB 打开会话图表中可见)这使得它在开始时太多了. 还有什么其他原因?如何确保 pod 从一开始就以最佳性能运行?
最后的笔记:
- 数据库处理器未达到最大值(在最重负载下使用率约为 30%-40%)。
- 大多数 SQL 查询和命令都比较简单 SELECT 和 INSERT
- 在初始部分之后,每个查询花费的时间不超过 10-20 毫秒
- 我不想解决池中连接数增加到 100 多个的问题,因为在最初的挣扎之后 pods 可以正常使用大约 50 个连接
- 我宁愿没有连接泄漏,因为在这种情况下它也会在正常加载后抛出异常,一段时间后
- 我使用作用域 DbContext,它在每个请求结束时被释放(因此连接被释放到池中)
编辑 2020 年 11 月 25 日
我目前的猜测是新创建的 pod 没有收到足够的 BANDWITH 资源或 CPU 资源。这个推理得到了事实的支持,即使那些不包括查询数据库的请求也在苦苦挣扎。
问题:会不会是刚创建的pod一开始被授予的资源(CPU或网络带宽)不足?
编辑 2 2020 年 11 月 26 日(回答 Arca Artem)
App 运行s 在 AWS 上的 k8s 集群上。
应用程序使用标准 ADO.NET 连接池连接到数据库(每个 pod 最多 100 个连接)。
我正在监视数据库连接和数据库 CPU(都在合理的范围内)。主机 CPU 利用率也在 20-25% 左右。
我认为当 pod 启动时,/health
端点成功响应(它检查数据库连接,使用简单的 SELECT 探测)并且 pod 的最大容量是例如200rps - 然后这个 pod 能够处理这个流量,因为 非常第一时刻 在 /health
探测成功之后。但是,从我的日志中我看到,在“/health”探测在 20 毫秒内连续成功 4 次之后,流量开始进入,pod 处理流量的前几秒每个请求花费的时间超过 5 秒(有时每个请求甚至需要 40 秒) .
我没有监控主机网络。
在这一点上,这只是我的猜测,对代码和体系结构了解不多,但值得一提的是我突然想到的一件事。运行状况检查可能没有使用您的其他端点使用的正常代码路径,可能会导致误报。如果您有选择,使用探查器可以帮助您准确指出这种情况发生的时间和方式。如果不是,我们可以有根据地猜测问题可能出在哪里。这里可能有很多东西在起作用,您可能已经熟悉这些,但为了完整起见,我将它们覆盖:
首先,值得记住的是 Postgres 中的连接非常昂贵(简单地说,这是因为它是数据库进程上的 fork
),因此您的 pods当您一次扩展您的应用程序时,可以批量创建它们。设置每一个都需要相对较长的时间,如果您批量进行设置,时间会加起来(多长时间取决于配置、可用资源等)。
假设您正在使用 ASP.NET Core(因为您提到了 DbContext
),初始请求将承担初始化整个堆栈的代价(在池中创建最小所需连接) , 初始化 ASP.NET 堆栈、依赖项...等)。同样,这将完全取决于您如何构建代码以及您的应用程序在初始化期间实际执行的操作。如果您的健康端点直接连接到数据库(不使用连接池),这将意味着跳过昂贵的池初始化,导致您的初始请求承担负担。
当您的负载逐渐增加时,您没有观察到相同的行为,这可能是因为通常这些事情是不同组件之间的相互作用,并且通常是可用资源、代码行为等的非线性函数。具体来说,如果它只是一个新的 pod 旋转起来,它需要的连接数比 5 个新的 pods 旋转起来要少得多,并且 Postgres 能够更快地满足它。 Postgres 是这里的共享资源 - 创建 1 个新连接比创建 100 个新连接(池中 5 pods x 20 分钟连接)要快得多,因为所有 pods 都在等待新连接。
你可以做一些事情来通过配置更改来加速这个过程,使用像 PgBouncer 这样的外部连接池......但是它们不会有效,除非你的健康端点代表你的实际状态pods.
同样,这一切都基于假设,但如果您还没有这样做,请尝试在您的健康端点中使用 DbContext
以确保池已初始化并准备好进行连接。正如评论中提到的那样,值得研究可能更适合实施此模式的其他类型的探测器。
我找到了上述问题的最终原因。 分配给 Pod 的 CPU 资源 不足。
问题很难隔离,因为 NewRelic APM CPU 使用图表的计算方式与预期不同(请参阅 NewRelic 文档)。真正的 pod CPU 使用与 CPU 限制只能在 NewRelic kubernetes 集群查看器中看到(可能它使用不同的算法来绘制那里的 CPU 使用情况)。
另外,pod启动的时候,一开始需要多一点CPU。最重要的是,pods 由于高流量而启动 - 简单地说,没有足够的 CPU 来处理这些请求。
我有一个 运行 在重负载下稳定的应用程序,但只有在负载增加时才会如此。
我同时 运行 3 或 4 pods,并在必要时扩展到 8 或 10 pods。
标准 requests per minute
约为 4000
(意味着每个节点每秒 66
请求,意味着每个单个 pod 每秒 16
请求)。
有一种情况,当我们收到巨大的负载峰值(从每分钟 4k 到每分钟 20k)。正确创建了新 pods,然后它们开始接收新负载。
问题是,在大约 10-20% 的情况下,新创建的 pod 难以处理初始负载,数据库请求占用超过 5000 毫秒,堆积起来,最终导致连接池耗尽的异常:The connection pool has been exhausted, either raise MaxPoolSize (currently 200) or Timeout (currently 15 seconds)
以下是 NewRelic 的截图:
我可以看到其他 pods 做得很好,而且在最初的挣扎之后,所有 pods 都可以毫无问题地处理负载。
这是我在尝试修复它时所做的:
摆脱非异步调用。 我在
async
方法中有几行阻塞代码。我已将所有内容更改为async
。我不再有非异步方法。删除了 运行ning 长交易。 我们有 运行ning 长交易,如下所示:
- beginTransactionAsync
- 选择数据异步
- 保存数据异步
- commitTransactionAsync
我重构为:
- selectDataAsync
- saveDataAsync // under-the-hood EF core short lived transaction
帮助很大,但没有彻底解决问题
- 确保某些连接始终打开并就绪。 我们向连接字符串添加了
Minimum Pool Size=20
,以始终保持至少 20 个连接打开。 这也有帮助,但有时仍然 pods 挣扎。
我们的 pods 在就绪探测 returns 成功后正常启动。就绪探测使用标准 .NET 核心健康检查检查与数据库的连接。
我们的连接字符串有 MaxPoolSize=100;Timeout=15;
设置。
我当然希望最初的新 pod 实例在以较低参数运行时需要一些启动时间,但我不希望 pod 经常会窒息并抛出 90% 的错误。
此处重要提示: 我有 2 个不同的 DbContexts 共享相同的连接字符串(因此相同的连接池)。每个 DbContext 访问数据库中的不同模式。这样做是为了具有模块化架构。 DbContext 从不相互通信,也从不在同一请求中一起使用。
我目前的猜测是,当 pod 刚创建并立即接收到巨大的负载时,pod 会尝试打开所有 100 个连接(在 DB 打开会话图表中可见)这使得它在开始时太多了. 还有什么其他原因?如何确保 pod 从一开始就以最佳性能运行?
最后的笔记:
- 数据库处理器未达到最大值(在最重负载下使用率约为 30%-40%)。
- 大多数 SQL 查询和命令都比较简单 SELECT 和 INSERT
- 在初始部分之后,每个查询花费的时间不超过 10-20 毫秒
- 我不想解决池中连接数增加到 100 多个的问题,因为在最初的挣扎之后 pods 可以正常使用大约 50 个连接
- 我宁愿没有连接泄漏,因为在这种情况下它也会在正常加载后抛出异常,一段时间后
- 我使用作用域 DbContext,它在每个请求结束时被释放(因此连接被释放到池中)
编辑 2020 年 11 月 25 日
我目前的猜测是新创建的 pod 没有收到足够的 BANDWITH 资源或 CPU 资源。这个推理得到了事实的支持,即使那些不包括查询数据库的请求也在苦苦挣扎。
问题:会不会是刚创建的pod一开始被授予的资源(CPU或网络带宽)不足?
编辑 2 2020 年 11 月 26 日(回答 Arca Artem)
App 运行s 在 AWS 上的 k8s 集群上。 应用程序使用标准 ADO.NET 连接池连接到数据库(每个 pod 最多 100 个连接)。
我正在监视数据库连接和数据库 CPU(都在合理的范围内)。主机 CPU 利用率也在 20-25% 左右。
我认为当 pod 启动时,/health
端点成功响应(它检查数据库连接,使用简单的 SELECT 探测)并且 pod 的最大容量是例如200rps - 然后这个 pod 能够处理这个流量,因为 非常第一时刻 在 /health
探测成功之后。但是,从我的日志中我看到,在“/health”探测在 20 毫秒内连续成功 4 次之后,流量开始进入,pod 处理流量的前几秒每个请求花费的时间超过 5 秒(有时每个请求甚至需要 40 秒) .
我没有监控主机网络。
在这一点上,这只是我的猜测,对代码和体系结构了解不多,但值得一提的是我突然想到的一件事。运行状况检查可能没有使用您的其他端点使用的正常代码路径,可能会导致误报。如果您有选择,使用探查器可以帮助您准确指出这种情况发生的时间和方式。如果不是,我们可以有根据地猜测问题可能出在哪里。这里可能有很多东西在起作用,您可能已经熟悉这些,但为了完整起见,我将它们覆盖:
首先,值得记住的是 Postgres 中的连接非常昂贵(简单地说,这是因为它是数据库进程上的 fork
),因此您的 pods当您一次扩展您的应用程序时,可以批量创建它们。设置每一个都需要相对较长的时间,如果您批量进行设置,时间会加起来(多长时间取决于配置、可用资源等)。
假设您正在使用 ASP.NET Core(因为您提到了 DbContext
),初始请求将承担初始化整个堆栈的代价(在池中创建最小所需连接) , 初始化 ASP.NET 堆栈、依赖项...等)。同样,这将完全取决于您如何构建代码以及您的应用程序在初始化期间实际执行的操作。如果您的健康端点直接连接到数据库(不使用连接池),这将意味着跳过昂贵的池初始化,导致您的初始请求承担负担。
当您的负载逐渐增加时,您没有观察到相同的行为,这可能是因为通常这些事情是不同组件之间的相互作用,并且通常是可用资源、代码行为等的非线性函数。具体来说,如果它只是一个新的 pod 旋转起来,它需要的连接数比 5 个新的 pods 旋转起来要少得多,并且 Postgres 能够更快地满足它。 Postgres 是这里的共享资源 - 创建 1 个新连接比创建 100 个新连接(池中 5 pods x 20 分钟连接)要快得多,因为所有 pods 都在等待新连接。
你可以做一些事情来通过配置更改来加速这个过程,使用像 PgBouncer 这样的外部连接池......但是它们不会有效,除非你的健康端点代表你的实际状态pods.
同样,这一切都基于假设,但如果您还没有这样做,请尝试在您的健康端点中使用 DbContext
以确保池已初始化并准备好进行连接。正如评论中提到的那样,值得研究可能更适合实施此模式的其他类型的探测器。
我找到了上述问题的最终原因。 分配给 Pod 的 CPU 资源 不足。
问题很难隔离,因为 NewRelic APM CPU 使用图表的计算方式与预期不同(请参阅 NewRelic 文档)。真正的 pod CPU 使用与 CPU 限制只能在 NewRelic kubernetes 集群查看器中看到(可能它使用不同的算法来绘制那里的 CPU 使用情况)。
另外,pod启动的时候,一开始需要多一点CPU。最重要的是,pods 由于高流量而启动 - 简单地说,没有足够的 CPU 来处理这些请求。