Andrey Borodin 在他的报告中将告诉您他们在设计连接池时如何考虑扩展 PgBouncer 的体验
视频:
大家好! 我叫安德鲁。
在 Yandex,我开发开源数据库。 今天我们有一个关于连接池连接的主题。
如果您知道如何用俄语调用连接池,请告诉我。 我真的很想找到一个应该在技术文献中建立的好的技术术语。
这个主题非常复杂,因为在许多数据库中连接池是内置的,您甚至不需要了解它。 当然,到处都有一些设置,但在 Postgres 中却不是这样工作的。 与此同时(在 HighLoad++ 2019),Nikolai Samokhvalov 发布了一份关于在 Postgres 中设置查询的报告。 据我了解,来到这里的人们已经完美地配置了他们的查询,而这些人面临着与网络和资源利用相关的更罕见的系统问题。 在某些地方,由于问题并不明显,这可能相当困难。
Yandex 有 Postgres。 许多 Yandex 服务都位于 Yandex.Cloud 中。 我们有几个 PB 的数据,在 Postgres 中每秒至少生成一百万个请求。
而且我们为所有服务提供了一个相当标准的集群——这是该节点的主要主节点、通常的两个副本(同步和异步)、备份、在副本上读取请求的扩展。
每个集群节点都是Postgres,节点上除了安装Postgres和监控系统外,还安装了一个连接池。 连接池用于防护及其主要目的。
连接池的主要用途是什么?
Postgres 在使用数据库时采用进程模型。 这意味着一个连接就是一个进程,一个 Postgres 后端。 在这个后端有很多不同的缓存,为不同的连接制作不同的缓存是相当昂贵的。
此外,Postgres 代码有一个名为 procArray 的数组。 它包含有关网络连接的基本数据。 几乎所有 procArray 处理算法都具有线性复杂度;它们在整个网络连接数组上运行。 这是一个相当快的周期,但随着传入网络连接的增加,成本会变得更高一些。 当价格变得有点贵时,您最终可能会为大量网络连接支付非常高的价格。
有 3 种可能的方法:
- 在应用方面。
- 在数据库方面。
- 而之间,也就是各种组合。
不幸的是,内置池化器目前正在开发中。 我们 PostgreSQL Professional 的朋友主要做这件事。 它何时出现很难预测。 事实上,我们有两种解决方案供架构师选择。 它们是应用程序端池和代理池。
应用程序端池是最简单的方法。 几乎所有客户端驱动程序都为您提供了一种方法:将代码中的数百万个连接呈现为与数据库的数十个连接。
出现的问题是,在某个时刻您想要扩展后端,想要将其部署到许多虚拟机。
然后您意识到您还有多个可用区、多个数据中心。 客户端池化方法会产生大量数据。 大的大约有 10 个连接。 这是可以正常工作的边缘。
如果我们谈论代理池化器,那么有两个池化器可以做很多事情。 他们不仅仅是泳池玩家。 它们是池化器+更酷的功能。 这
但不幸的是,并不是每个人都需要这个附加功能。 这导致池化器仅支持会话池,即一个传入客户端,一个传出客户端到数据库。
这不太适合我们的目的,因此我们使用 PgBouncer,它实现事务池,即服务器连接仅在事务持续时间内与客户端连接匹配。
在我们的工作量中,确实如此。 但有几个问题.
当您想要诊断会话时,问题就开始了,因为所有传入连接都是本地的。 每个人都带着环回,不知怎的,跟踪会话变得很困难。
当然你可以使用application_name_add_host。 这是 Bouncer 端将 IP 地址添加到 application_name 的一种方法。 但 application_name 是由附加连接设置的。
在此图中,黄线是真实请求,蓝线是飞入数据库的请求。 而这个区别恰恰就是application_name的安装,它只是为了追踪才需要,但它根本不是免费的。
此外,在 Bouncer 中,您无法限制一个池,即每个特定用户、每个特定数据库的数据库连接数。
这会导致什么? 你有一个用 C++ 编写的加载服务,并且节点附近有一个小服务,该服务不会对数据库做任何可怕的事情,但它的驱动程序会发疯。 它打开 20 个连接,其他一切都会等待。 甚至你的代码也是正常的。
当然,我们为 Bouncer 编写了一个小补丁,添加了此设置,即限制客户端进入池。
可以在 Postgres 端执行此操作,即通过连接数限制数据库中的角色。
但随后您就无法理解为什么没有与服务器的连接。 PgBouncer 不会抛出连接错误,它总是返回相同的信息。 你无法理解:也许你的密码已经改变,也许数据库刚刚丢失,也许出了什么问题。 但没有诊断。 如果会话无法建立,您将不知道为什么无法建立。
在某个时刻,您查看应用程序图表,发现该应用程序无法运行。
查看顶部,发现 Bouncer 是单线程的。 这是服务生命周期的转折点。 您意识到您准备在一年半内扩展数据库,并且需要扩展池化器。
我们得出的结论是我们需要更多的 PgBouncer。
保镖已经修补了一些。
他们做到了,可以通过重用 TCP 端口来引发多个 Bouncer。 并且操作系统使用循环自动在它们之间传输传入的 TCP 连接。
这对客户端来说是透明的,这意味着看起来您有一个 Bouncer,但正在运行的 Bouncer 之间存在空闲连接碎片。
在某个时刻,您可能会注意到这 3 个保镖都 100% 消耗了他们的核心。 你需要相当多的保镖。 为什么?
因为你有 TLS。 您有一个加密连接。 如果你对启用和不启用 TLS 的 Postgres 进行基准测试,你会发现启用加密后建立的连接数量几乎下降了两个数量级,因为 TLS 握手会消耗 CPU 资源。
在顶部,您可以看到相当多的加密函数,这些函数在出现一波传入连接时执行。 由于我们的主节点可以在可用区域之间切换,因此一波传入连接是相当典型的情况。 也就是说,由于某种原因,旧的主数据中心不可用,整个负载被发送到另一个数据中心。 他们都会同时过来向 TLS 打招呼。
而大量的TLS握手可能不再向Bouncer打招呼,而是会掐断他的喉咙。 由于超时,传入连接的浪潮可能会变得不受抑制。 如果您在没有指数退避的情况下重试基点,它们将不会以连贯的波一次又一次地出现。
以下是 16 个 PgBouncer 的示例,16 个内核的负载为 100%。
我们来到了级联PgBouncer。 这是使用 Bouncer 在我们的负载上可以实现的最佳配置。 我们的外部Bouncer用于TCP握手,内部Bouncer用于真正的池化,以免外部连接碎片过多。
在此配置中,可以顺利重启。 您可以一一重新启动所有这 18 个 Bouncer。 但维持这样的配置是相当困难的。 系统管理员、DevOps 和实际负责该服务器的人员不会对这种安排感到非常满意。
看起来我们所有的改进都可以推广到开源,但是Bouncer并没有得到很好的支持。 例如,一个月前就承诺在一个端口上运行多个 PgBouncer 的能力。 几年前有一个带有此功能的拉取请求。
或者再举一个例子。 在 Postgres 中,您可以通过将密钥发送到不同的连接来取消正在进行的请求,而无需进行不必要的身份验证。 但有些客户端只是发送 TCP 重置,即中断网络连接。 保镖会做什么? 他什么也不会做。 它将继续执行请求。 如果您收到大量连接,这些连接创建了带有小请求的数据库,那么简单地断开与 Bouncer 的连接是不够的;您还需要完成数据库中正在运行的那些请求。
该问题已被修补,并且该问题尚未合并到 Bouncer 的上游。
所以我们得出的结论是,我们需要自己的连接池,它将被开发、修补,其中的问题可以被快速纠正,当然,它必须是多线程的。
我们将多线程设置为主要任务。 我们需要能够很好地处理传入的 TLS 连接浪潮。
为此,我们必须开发一个名为 Machinarium 的单独库,该库旨在将网络连接的机器状态描述为顺序代码。 如果您查看 libpq 源代码,您会看到一些非常复杂的调用,它们可以返回结果并说:“稍后给我打电话。 现在我暂时有 IO,但是当 IO 消失时,处理器上就会有负载。” 这是一个多层次的计划。 网络通信通常由状态机来描述。 很多规则,例如“如果我之前收到了大小为 N 的数据包标头,现在我正在等待 N 个字节”、“如果我发送了 SYNC 数据包,现在我正在等待包含结果元数据的数据包”。 结果是一个相当困难、违反直觉的代码,就好像迷宫被转换为行扫描一样。 我们这样做是为了让程序员以普通命令式代码的形式描述交互的主要路径,而不是状态机。 只是在这段命令式代码中,您需要插入需要通过等待网络数据来中断执行序列的位置,并将执行上下文传递给另一个协程(绿色线程)。 这种做法类似于我们连续写下迷宫中最期望的路径,然后向其添加分支。
因此,我们有一个线程执行 TCP 接受并循环将 TPC 连接传递给许多工作线程。
在这种情况下,每个客户端连接始终在一个处理器上运行。 这允许您使其对缓存友好。
此外,我们还稍微改进了将小数据包收集为一个大数据包的方式,以减轻系统 TCP 堆栈的负担。
此外,我们还改进了事务池,Odyssey 在配置后可以在网络连接失败时发送 CANCEL 和 ROLLBACK,即如果没有人在等待请求,Odyssey 将告诉数据库不要尝试满足可能会浪费宝贵资源的请求。
只要有可能,我们都会与同一客户端保持连接。 这避免了必须重新安装 application_name_add_host。 如果这是可能的,那么我们就不必额外重置诊断所需的参数。
我们为 Yandex.Cloud 的利益而工作。 如果您使用托管 PostgreSQL 并安装了连接池,您可以向外创建逻辑复制,也就是说,如果您愿意,可以使用逻辑复制。 Bouncer不会向外部释放逻辑复制流。
这是设置逻辑复制的示例。
此外,我们还支持物理向外复制。 当然,在云中,这是不可能的,因为集群会给你太多关于它自己的信息。 但在您的安装中,如果您需要通过 Odyssey 中的连接池进行物理复制,这是可能的。
Odyssey 具有与 PgBouncer 完全兼容的监控功能。 我们有相同的控制台,可以运行几乎所有相同的命令。 如果缺少某些内容,请发送拉取请求,或者至少在 GitHub 上发送问题,我们将完成必要的命令。 但我们已经拥有了 PgBouncer 控制台的主要功能。
当然,我们还有错误转发。 我们将返回数据库报告的错误。 您将收到有关您未包含在数据库中的原因的信息,而不仅仅是您未包含在数据库中的信息。
如果您需要与 PgBouncer 100% 兼容,则禁用此功能。 为了安全起见,我们可以像保镖一样行事。
进入菜单
关于奥德赛源代码的几句话。
例如,有“暂停/恢复”命令。 它们通常用于更新数据库。 如果您需要更新 Postgres,那么您可以在连接池中暂停它,执行 pg_upgrade,然后执行恢复。 从客户端来看,数据库似乎只是变慢了。 此功能是社区人员为我们带来的。 她还没有被冻结,但很快一切都会冻结。 (已经冻结了)
此外,PgBouncer 的新功能之一是支持 SCRAM 身份验证,这也是由不在 Yandex.Cloud 工作的人给我们带来的。 两者的功能都很复杂而且很重要。
因此,我想告诉您 Odyssey 是由什么组成的,以防您现在也想编写一些代码。
您有 Odyssey 源代码库,它依赖于两个主要库。 Kiwi 库是 Postgres 消息协议的实现。 也就是说,Postgres的原生proto 3是前端和后端可以交换的标准消息。 它们在 Kiwi 库中实现。
Machinarium 库是一个线程实现库。 这个机械迷城的一小部分是用汇编语言编写的。 但不要惊慌,只有 15 行。
奥德赛建筑。 有一台正在运行协程的主机。 该机器实现接受传入的 TCP 连接并将它们分配给工作人员。
多个客户的处理程序可以在一个工作人员中工作。 主线程还运行控制台和 crone 任务的处理,以删除池中不再需要的连接。
Odyssey 使用标准 Postgres 测试套件进行测试。 我们只需通过 Bouncer 和 Odyssey 运行安装检查,我们就会得到一个 null div。 有几项与日期格式相关的测试在 Bouncer 和 Odyssey 中通过的测试并不完全相同。
此外,还有许多驱动程序有自己的测试。 我们用他们的测试来测试奥德赛。
此外,由于我们的级联配置,我们必须测试各种捆绑包:Postgres + Odyssey、PgBouncer + Odyssey、Odyssey + Odyssey,以确保如果 Odyssey 最终出现在级联的任何部分中,它仍然可以工作正如我们所期望的。
耙
我们在生产中使用奥德赛。 如果我说一切正常,那就不公平了。 不,也就是说,是的,但并非总是如此。 例如,在生产中一切正常,然后我们 PostgreSQL Professional 的朋友过来告诉我们发生了内存泄漏。 他们确实是,我们纠正了他们。 但这很简单。
然后我们发现连接池有传入的 TLS 连接和传出的 TLS 连接。 连接需要客户端证书和服务器证书。
Bouncer 和 Odyssey 服务器证书由它们的 pcache 重新读取,但客户端证书不需要从 pcache 重新读取,因为我们的可扩展 Odyssey 最终会遇到读取此证书的系统性能问题。 这让我们感到惊讶,因为他没过多久就反抗了。 起初它是线性扩展的,但在 20 个传入同时连接之后,这个问题就显现出来了。
可插拔身份验证方法是使用内置 Lunux 工具进行身份验证的能力。 在 PgBouncer 中,它的实现方式是有一个单独的线程等待 PAM 的响应,并且有一个主 PgBouncer 线程为当前连接提供服务并可以要求它们驻留在 PAM 线程中。
我们没有实施这一举措的原因很简单。 我们有很多线程。 我们为什么需要这个?
这最终会产生问题,因为如果您有 PAM 身份验证和非 PAM 身份验证,那么大量 PAM 身份验证会显着延迟非 PAM 身份验证。 这是我们尚未解决的问题之一。 但如果你想修复它,你可以这样做。
另一个好处是我们有一个线程接受所有传入连接。 然后它们会被转移到工作池,并在那里进行 TLS 握手。
最重要的是,如果您有 20 个网络连接的连贯浪潮,它们都会被接受。 在客户端,libpq 将开始报告超时。 默认情况下似乎是 000 秒。
如果他们都不能同时进入数据库,那么他们就不能进入数据库,因为这一切都可以通过非指数重试来覆盖。
我们得出的结论是,我们从 PgBouncer 复制了这里的方案,事实上我们限制了我们接受的 TCP 连接的数量。
如果我们发现我们正在接受连接,但它们最终没有时间握手,我们会将它们放入队列中,这样它们就不会浪费 CPU 资源。 这导致可能无法对所有已到达的连接执行同时握手。 但至少有人会进入数据库,即使负载相当重。
路线图
您希望在未来的《奥德赛》中看到什么? 我们准备好发展什么?我们对社区有什么期望?
截至 2019 年 XNUMX 月。
这是八月份奥德赛路线图的样子:
- 我们需要 SCRAM 和 PAM 身份验证。
- 我们想将阅读请求转发到待机状态。
- 我想要在线重启。
- 以及在服务器上暂停的能力。
该路线图的一半已经完成,但不是由我们完成的。 这很好。 因此,让我们讨论剩下的内容并添加更多内容。
原则上,在Postgres中,从10开始,连接时可以指定session_attrs。 您可以列出连接中的所有数据库主机,并说明为什么要访问该数据库:写入或只读。 并且驱动程序自己会在列表中选择他最喜欢的第一个主机,该主机满足session_attrs的要求。
但这种方法的问题是它无法控制复制滞后。 您可能有一些副本滞后于您的服务,时间长得不可接受。 为了在副本上启用读取查询的全功能执行,我们本质上需要支持 Odyssey 在无法读取时不运行的功能。
Odyssey 必须不时地访问数据库并询问与主数据库的复制距离。 如果达到了限制值,则不允许新的请求进入数据库,告诉客户端需要重新发起连接,并且可能选择另一台主机来执行请求。 这将允许数据库快速恢复复制延迟并再次返回以响应请求。
很难给出实施的时间框架,因为它是开源的。 但是,我希望不会像 PgBouncer 的同事那样需要 2,5 年。 这是我希望在《奥德赛》中看到的功能。
但是proto3上有消息协议级别的prepared语句。 这是创建准备好的语句的信息以结构化形式出现的地方。 我们可以支持这样的理解:在某些服务器连接上,客户端要求创建准备好的语句。 而且即使事务关闭,我们仍然需要保持服务器和客户端之间的连接。
但这里对话出现了矛盾,因为有人说你需要了解客户端创建了什么样的准备语句,并在创建此服务器连接的所有客户端之间共享服务器连接,即谁创建了这样的准备语句。
Andres Freund 说,如果一个客户端已经在另一个服务器连接中创建了这样的准备好的语句,那么就为他创建它。 但是在数据库而不是客户端中执行查询似乎有点错误,但是从编写与数据库交互的协议的开发人员的角度来看,如果简单地给他一个网络连接,那么会很方便有这样一个准备好的查询。
我们还需要实现一项功能。 我们现在有与 PgBouncer 兼容的监控。 我们可以返回平均查询执行时间。 但平均时间就是医院里的平均温度:有的冷,有的热——平均而言,每个人都很健康。 这不是真的。
我们需要实现对百分位数的支持,这将表明存在浪费资源的缓慢查询,并使监控更容易接受。
最重要的是我想要1.0版本(1.1版本已经发布了)。 事实上,Odyssey 现在处于 1.0rc 版本,即候选版本。 除了内存泄漏之外,我列出的所有问题都已使用完全相同的版本修复。
1.0版本对我们意味着什么? 我们正在将奥德赛推广到我们的基地。 它已经在我们的数据库上运行了,但是当它达到每秒 1 个请求时,我们就可以说这是发布版本,这是一个可以称为 000 的版本。
社区中的一些人要求 1.0 版本包含暂停和 SCRAM。 但这意味着我们需要将下一个版本推出到生产环境,因为 SCRAM 和暂停都还没有被终止。 但是,这个问题很可能很快就会得到解决。
我正在等待您的拉取请求。 我还想听听您在使用 Bouncer 时遇到了什么问题。 让我们来讨论一下它们。 也许我们可以实现您需要的一些功能。
我的部分到此结束,我想听听大家的意见。 谢谢你!
问题
如果我设置自己的 application_name,它会被正确转发吗?包括在 Odyssey 的事务池中吗?
奥德赛还是保镖?
在奥德赛中。 在 Bouncer 中它被抛出。
我们来做一组吧
而如果我的真实连接跳转到其他连接上,会被传输吗?
我们将创建列表中列出的所有参数的集合。 我无法判断 application_name 是否在此列表中。 我想我在那里见过他。 我们将设置所有相同的参数。 通过一个请求,该集将执行客户端在启动过程中安装的所有操作。
谢谢安德烈的报告! 好报告! 我很高兴奥德赛每分钟都在发展得越来越快。 我想继续这样下去。 我们已经要求您拥有多数据源连接,以便 Odyssey 可以同时连接到不同的数据库,即主从,然后在故障转移后自动连接到新的主数据库。
是的,我似乎记得这个讨论。 现在有几个仓库。 但它们之间没有切换。 在我们这边,我们必须轮询服务器是否还活着,并了解发生了故障转移,谁将调用 pg_recovery。 我有一个标准的理解方式,我们没有来找大师。 我们应该从错误中以某种方式理解还是什么? 也就是说,这个想法很有趣,正在讨论中。 写更多评论。 如果你有懂得 C 语言的员工,那就太好了。
我们也对跨副本扩展的问题感兴趣,因为我们希望应用程序开发人员尽可能简单地采用复制集群。 但在这里我想更多的评论,即到底如何去做,如何做好。
问题还与复制品有关。 事实证明,您有一个主副本和多个副本。 很明显,它们到副本进行连接的频率低于到主服务器进行连接的频率,因为它们可能存在差异。 你说数据的差异可能是这样的,不能满足你的业务,你不会去那里,直到它被复制。 同时,如果你很长一段时间没有去那里,然后才开始去,那么需要的数据也不会立即可用。 也就是说,如果我们不断地访问主服务器,那么那里的缓存就会预热,但在副本中缓存会稍微滞后。
对,是真的。 pcache不会有你想要的数据块,真正的缓存不会有你想要的表的信息,计划不会有解析的查询,什么都不会。
当你有某种集群,并在那里添加一个新的副本时,当它启动时,其中的一切都是坏的,即它增加了缓存。
我明白了。 正确的方法是首先在副本上运行一小部分查询,这将预热缓存。 粗略来说,我们有一个条件,就是落后master不能超过10秒。 而且这种情况并不是一波包含的,但对于某些客户来说是顺利的。
是的,增加体重。
这是一个好主意。 但首先我们需要实施此关闭。 首先我们需要关闭,然后我们再考虑如何打开。 这是一个可以顺利启用的很棒的功能。
Nginx有这个选项 slowly start
在服务器集群中。 他逐渐增加负荷。
是的,好主意,当我们有时间的时候我们会尝试一下。
来源: habr.com