一.引言
Oracle 数据库是许多应用程序的核心,适用于各个行业,覆盖了广泛的复杂性和部署规模。这些应用程序的范围从小型自主数据库到多个 Exadata 服务器组成的大型配置,可以部署在云端或本地的各种硬件环境中。这些应用可以支持从少量到数百万的并发用户。每个用户可以执行多种事务,例如拨打电话、在线购物或在超市付款。这些系统通常被称为**在线事务处理(OLTP)**系统,尽管它们通常也包括批处理任务。
数据库的工作负载会以多种方式变化:用户数量可能改变,活动的类型和组合可能变化,每个用户的负载也可能有所不同。应用程序编程的一个重要目标是无论工作负载如何变化,都能保证数据库安全且持续正常运行。每当用户执行某个操作时,都会有一个"句柄"将用户与数据库关联起来。为了让数据库能够并发处理大量句柄,必须实现共享 和池化。
本文讨论了共享和池化的方法。
二.背景
一个现代化的网站可以同时支持数万到数百万用户。在用户的浏览器或设备与数据库服务器之间,通常存在一个复杂的技术栈,包括网络、防火墙和应用服务器。在用户端,例如,浏览网站10分钟或拨打电话1小时,可能只会触发数据库执行10条 SQL 语句,每条语句平均使用10毫秒的数据库时间。以具体数字说明:假设一个用户在线1000秒(约15分钟),仅使用100毫秒的数据库时间。如果数据库服务器拥有多个 CPU 和良好的 I/O 子系统,可以在每秒内提供10秒的数据库时间,那么它就可以在一秒钟内支持 100 个用户(10秒/100毫秒)。这意味着,数据库能够支持平均 100,000 个并发用户。
为了实现这一点,数据库服务器上所需的各种资源(如 CPU、内存和 I/O)必须能够在这些用户之间高效共享。因此,类似 OLTP 的数据库应用程序必须始终包括连接池。为了保护数据库,仅允许合理数量的连接至关重要。以广义的方式来看,如果 CPU 是主要资源需求,那么数据库的连接数量应为可用 CPU 核心数量的一个较低倍数。连接池的必要性在于使宝贵的数据库资源能够在用户之间共享和复用。
在这个层面上,复杂性开始显现------不仅因为"连接池"一词含义模糊,拥有许多不同的解释,还因为技术栈中的不同部分以不同方式支持共享或池化。池化可以通过数据库支持的机制实现,例如数据库驻留连接池(DRCP)和共享服务器(Shared Servers) ,通过 Universal Connection Pool (UCP) 或 JDBC 中的池化数据源实现,或者通过 OCI 的连接池实现。
连接池可以是无状态的,也可以是有状态的。从表面上看,这些方法似乎都提供了实现相同目标的机制,但实际上,它们无论是有意还是无意,都隐藏了其中的细节。
术语"连接"并没有一个精确的定义,通常被用作一个笼统的概念。在本文中,"TCP/IP 连接"一词将被用来明确标识具有两个 TCP/IP 套接字端点的物理连接。
数据库概念定义
让我们从一些定义开始,这些定义有助于解释池化过程中发生的事情。首先,我们讨论数据库层中进程和内存的使用。然后,我们解释会话的概念,这是本文的核心内容。接着,我们探讨应用程序端如何连接到数据库。
数据库进程和内存
Oracle 数据库服务器采用多进程架构,并使用共享内存。每个进程执行特定的任务,大致可分为以下两类:
- 后台进程:代表整个数据库及其所有连接用户执行任务。
- 服务器进程:为特定的应用程序或用户执行任务。
所有这些进程都可以访问一个名为系统全局区(SGA, System Global Area)的大型共享内存区域。SGA 包括缓冲区缓存 、共享池 和其他内存结构。此外,与特定用户相关的工作内存称为用户全局区(UGA, User Global Area)。
在执行数据库工作时(即执行一组 SQL 语句以实现应用程序功能),需要一个服务器进程。该进程负责:
- 解析 SQL 语句;
- 将 SQL 语句中的名称与数据库对象(如列和表)关联;
- 在应用程序和数据库之间传输数据和结果。
执行这些任务时,服务器进程可以访问:
- SGA:包含所有用户共享的数据;
- UGA:与特定用户相关的工作数据。
后台进程负责执行多种多样的任务,例如:
- 将缓冲区缓存中的修改块写入数据库文件;
- 总体监控数据库实例的状态;
- 归档数据以支持在数据库文件丢失时进行恢复。
三.数据库会话
任何数据库工作都需要一个数据库会话。例如,考虑以下简单的 SQL 语句:
SELECT * FROM emp;
数据库会话 包含关于用户名和密码的信息。如果没有用户名,emp
是没有意义的------它可能是:
- 一个模式中的表;
- 另一个模式中的一个同义词,指向第一个模式中的表;
- 一个模式中的视图。
没有经过身份验证的会话(即使用有效密码登录的用户名),emp
就无法被解析。因此,身份验证是数据库会话的一个重要属性。
会话中的其他内容:
- 游标(Cursor) :与单个 SQL 语句关联的句柄。应用程序通常同时使用多个游标,每个游标与不同的 SQL 语句或 PL/SQL 块关联。例如:
-
如果应用程序需要执行:
SELECT * FROM dept;
和:
SELECT * FROM emp WHERE deptno = :1;
则必须打开两个游标。
-
应用程序逻辑决定了游标的执行顺序。例如,一个订单录入系统可能有不同的游标用于:
- 选择客户;
- 插入订单行;
- 选择商品;
- 更新库存。
-
如果订单包含多条订单行,其中一些游标可能会被反复执行。
-
会话中的状态信息:
- 插入或更新行的信息会保留,直到会话执行提交(
commit
)操作。 - 在客户提交多个订单的情况下,客户信息可能会保留,直到会话终止。
会话是保存在内存中的状态,包含以下信息:
- 身份验证信息;
- NLS(国家语言支持)设置;
- 事务信息;
- 游标;
- PL/SQL 包变量的内容;
- 其他类似信息。
会话与连接或进程的关联性
由于会话状态仅存在于内存中,因此会话不一定与连接或进程直接关联。例如,通过 DBMS_SCHEDULER
机制创建的会话:
- 完全独立于用户、客户端和应用服务器;
- 不需要数据库连接。
四.数据库连接
在大多数现代实现中,应用程序代码通常在通过 TCP/IP 网络与数据库隔离的系统上执行。这些系统通常分为应用层(Application Tier)和数据库层(Database Tier)。
数据库连接是通过 Oracle 数据库的 TNS 监听进程创建的 TCP/IP 套接字的两个端点。要通过 TCP/IP 从一台机器连接到数据库服务器,您需要指定以下信息:
- 主机名(hostname);
- TCP/IP 端口号;
- 服务名(service name)。
在 Oracle 的 Easy Connect 语法中,这些信息表示为:
//host:port/service
4.1组合:会话、进程和连接
在传统的专用客户端/服务器连接中,会话、进程和 TCP/IP 连接这三个概念是结合在一起的,如下图所示:
- 应用进程:用红色圆圈表示。
- 数据库进程:用黄色圆圈表示。
- TCP/IP 连接:用实心黑线表示。
- 箭头代表应用进程和数据库进程之间的 TCP/IP 连接端点。
4.2 SQL*Plus 的连接过程
考虑以下 SQL*Plus 示例:
sqlplus username/{password}@//host/service
执行上述命令时,发生以下四件事:
-
启动应用进程 :
应用端启动一个进程运行 SQL*Plus 应用程序代码,用红色圆圈表示。
-
建立 TCP/IP 连接 :
使用数据库服务器上的 TNS 监听进程,建立一个 TCP/IP 连接,用实心黑线表示。
-
启动数据库端的服务器进程 :
TNS 监听进程在数据库端启动一个服务器进程(前台进程),用黄色圆圈表示。TCP/IP 连接的两个端点分别是应用进程和服务器进程。
-
创建会话 :
在服务器进程中,用您的用户名和密码进行身份验证后,会创建一个会话,用绿色矩形表示。
图中垂直的红线表示应用层(左侧)和数据库层(右侧)的分隔。
4.3 SQL*Plus 的执行与退出
- 在 SQL*Plus 中,您可以执行 SQL 语句,这些语句会使用分配在 UGA(User Global Area,用户全局区)的游标。
- UGA 的分配位置 :
UGA 是在服务器进程直接分配的 PGA(Program Global Area,程序全局区)内存中。 - 当您完成操作并退出 SQL*Plus 时:
- 会话会终止;
- 释放 TCP/IP 连接;
- 相关进程也会被释放。
4.4 无状态会话与有状态会话
会话与其状态相关联。一个直接的后果是,两个不同的用户不能使用同一个会话。传统的应用程序编程通常将每个用户与整个技术栈关联起来:一个连接、一个进程和一个用户的一个会话。因此,会话状态可能会长时间保留,并且------就本文讨论的内容而言------即使用户没有主动使用数据库,会话也可能保持打开状态并保留其状态。这意味着数据库处于空闲状态,没有任何处理发生,只是在等待用户采取行动。
在现代应用程序编程中,这一范式已经发生了变化。应用程序要么与用户进行交互(包括等待),要么执行数据库工作,例如执行 SQL 语句或事务。在执行数据库工作的间隙,当应用程序与用户交互时,数据库中不会保留任何状态。这意味着,如果用户没有主动使用数据库,他们就没有打开的会话,数据库内存中也不会保留任何状态。正因如此,这种类型的应用程序模型通常被称为无状态。
无状态功能是指应用程序主动封装数据库调用,通过从池中获取一个会话,在数据库调用完成后立即将会话返回池中。在本文中,这种机制被称为会话池。
使用无状态应用程序编程的好处
使用无状态应用程序编程的好处显而易见:只有在真正需要时,数据库中的会话才会存在或处于活动状态。一旦用户完成工作,该会话就会被归还到池中。随后,该会话所占用的资源就可以被另一个用户使用。池的最大大小可以被限制,这样当池中的所有会话都处于活动状态时,数据库的工作负载可以保持在安全操作范围内。当达到限制时,用户会被放入队列,等待其他用户将会话归还到池中。
为什么不直接允许更多会话?
如果普通用户在数据库中只有非常短暂的活跃时间,这会发生什么?问题在于,数据库过载的风险可能会导致系统完全故障。
几乎每个人都经历过系统不可用的情况。在大多数情况下,这是由于过载引起的:过多的会话同时尝试执行数据库工作,导致系统几乎完全停止运行。在这种停止状态下,许多数据库管理员会发现他们甚至无法通过 SQL*Plus 简单登录到数据库。
五.常见四种数据库连接方式
5.1 传统的专用连接
传统(遗留)编程模型类似于以下代码:
database_logon();
loop ...
user_interface();
sql1;
user_interface();
sql2;
commit;
end loop;
database_logoff();
这种代码可能运行在个人电脑上,或者终端会话中。每个用户的代码都是单独执行的。尽管大部分时间都花在了 user_interface()
等待用户交互上,但应用程序通常在整个过程(从登录数据库到注销数据库)中是有状态的。这种情况在图 2 中以可视化方式表示:
图 2:多个传统应用
- 红色圆圈表示每个用户的应用程序进程。
- 黄色圆圈表示数据库进程。
- 绿色矩形表示数据库会话。
- 应用进程和数据库进程之间的箭头表示它们通过 TCP/IP 建立的连接。
从编程的角度来看,这种模型非常简单,通常使用单线程编程实现。然而,如果有许多用户几乎同时活跃,这种模型会有很高的风险导致数据库过载。
5.2 使用会话池
现代应用程序编程采用了一种完全不同的范式,其中包括会话池。其代码结构类似如下:
handle=identify();
loop ...
user_interface();
user_interface();
get_session(handle);
sql1;
sql2;
commit;
release_session(handle);
end loop;
相比传统的编程风格,这里有几个重要的不同点:
- 当现代应用程序启动时(通常是用户登录系统时),通常不会直接登录到数据库。取而代之的是验证用户的权限,以便后续的数据库工作可以被授权执行。这是通过一个 句柄(handle) 来管理的。
- 当用户界面交互完成后(通常通过点击"提交"或类似按钮),会话池会通过句柄分配一个数据库会话。
- 数据库调用完成后,会话立即归还到池中,不会在会话中保留任何状态。
示例接口
Java 的 Universal Connection Pool (UCP) 是支持这种机制的一种流行的应用程序接口。UCP 是一个多线程接口,其中每个 Java 进程包含多个工作线程运行代码,此外还有 UCP 管理的其他线程用于维护会话池。
图 3:客户端会话池
- 应用程序端现在是多线程的。除了工作线程(红色)外,还存在用于会话池的某些线程(灰色)。
- 会话池由连接到数据库的 TCP/IP 连接组成,每个连接都有一个服务器进程和一个会话。
- 会话池类似一个漏斗,允许大量的应用程序工作线程共享一个较小数量的数据库连接、进程和会话。
应用程序端的多线程模式
应用程序端现在采用了多线程模式。除了工作线程(红色)外,还包含组成会话池的某些线程(灰色)。这些线程维护与数据库的 TCP/IP 连接,每个连接都对应一个服务器进程和一个会话。
会话池的设计类似于一个漏斗,允许大量的应用程序工作线程共享少量的数据库连接、进程和会话。与每个客户端拥有专用的 TCP/IP 连接相比,会话池有以下巨大优势:
- 池的大小可以配置,从而降低甚至完全消除数据库过载的风险。
- 如果突然有大量工作线程需要执行数据库操作,这些线程会排队等待可用的会话。这种排队虽然会增加用户感知的执行时间,但有效保护了数据库免于过载。
数据库工作的登录
会话池的使用可以避免数据库中存在大量潜在的空闲进程和会话,降低过载的风险。然而,这种方法需要多线程编程,以便大量的工作线程能够共享少量的数据库会话。
如果应用程序代码在用户界面和数据库操作之间有明确的分离,可以通过在执行数据库操作时登录数据库的方式达到类似的效果。这种方式不需要多线程编程,其编程模式如下:
示例代码
loop ...
user_interface();
user_interface();
database_logon();
sql1;
sql2;
commit;
database_logoff();
end loop;
从原理上讲,这种方法实现了数据库中不留空闲进程的目标,但它带来了非常高的成本:数据库登录。
数据库登录的高成本
数据库登录的代价很高,包括以下步骤:
- 使用 TNS 监听器建立 TCP/IP 连接;
- 创建操作系统进程;
- 初始化数据库环境;
- 验证用户名和密码等。
数据库登录的成本可能是执行几个 SQL 语句所需数据库时间的 10 倍 甚至 100 倍。如果即将执行的 SQL 语句很简单,数据库登录的成本就会显得过于高昂。
批处理中的登录成本
在批处理场景中(如执行许多长时间运行的 SQL 语句时),数据库登录的成本相对较小,因为实际数据库操作的成本占主导地位。例如,在执行提取、转换和加载(ETL)任务时,这些任务往往运行时间较长且对数据库依赖性强。在这种情况下,登录成本并不显著。
5.3 数据库驻留连接池 (Database Resident Connection Pooling, DRCP)
为了避免数据库登录的开销,同时让单线程应用程序也能从会话池中受益,Oracle 提供了一种将池放置在数据库服务器上的机制:数据库驻留连接池 (DRCP)。这类似于客户端会话池的使用,它允许大量工作进程共享较少数量的数据库会话。这在图 4 中有说明------注意,目前只有一个应用进程拥有一个会话:
图 4:使用 DRCP 的会话池
在 DRCP 模式中,应用端的数据库会话池由数据库服务器上的一个类似池取代。一个代理进程(Broker Process)接受请求,为数据库会话分配池中可用的会话。如果当前没有可用会话,则应用程序会在队列中等待,除非可以启动一个新的服务器。当应用端的进程完成数据库操作后,会话会被归还给代理进程。
DRCP 的实际意义
在实际应用中,DRCP 允许单线程应用程序替换传统的高开销的登录和注销操作,改为通过更快速的调用从数据库端的池中获取会话。
DRCP 的句柄(Handle)的使用与多线程应用程序在客户端使用会话池类似,即使句柄实际关联的是数据库端的池。
使用 DRCP 的注意事项
与客户端会话池类似,使用 DRCP 的应用程序必须将用户界面与数据库操作分离,如示例代码 2 所示。如果尝试在以下情况下使用 DRCP:
- 应用程序在拥有会话期间(即
get_session()
和release_session()
之间)等待用户操作或新的请求到达, 则 DRCP 无法提供任何池化的优势。
此外,DRCP 会断开长时间不活动的会话,这个时间通过参数 max_think_time
配置。
5.4 使用共享服务器(Shared Servers)
Oracle 提供了另一种机制,称为 共享服务器(Shared Servers),通过减少数据库服务器中的进程数量来降低过载风险。尽管共享服务器与 DRCP 看起来相似,但它们适用于不同的场景,并具有各自的优势。
在共享服务器模式中,每个用户在数据库中都有一个会话,并带有所有会话的相关属性和后果。即使应用程序在处理用户界面时会话状态长时间保留,服务器进程会被放入一个池中,从而实现共享。
这一模型在图 5 中进行了展示:
与传统模式的比较
在与传统模型的比较中,每个客户端进程(红色)仍然拥有一个独立的数据库会话(绿色),但它们之间的连接方式有所不同。客户端进程不再通过单个 TCP/IP 连接绑定到一个专用的数据库进程,而是通过较少数量的服务器进程共享连接。在这种模式下,TCP/IP 连接的端点是一个分发器(dispatcher)进程。
当客户端进程(如图 5 中的进程 5)没有执行任何数据库操作时,其 TCP/IP 连接处于空闲状态。一旦客户端进程开始执行数据库工作(如从游标获取数据或提交事务),数据库操作会通过分发器路由到一个可用的服务器进程。该服务器进程随后附加到包含完整会话状态(UGA,用户全局区)的内存结构,执行实际的数据库操作,并将结果返回给分发器。这一过程在图 5 中通过从客户端到其数据库状态的黑色箭头表示。服务器进程与会话内存的关联随后被释放,分发器通过 TCP/IP 连接将结果发送回客户端。
队列处理
如果在某一时刻,客户端应用程序尝试执行的数据库工作超出了共享服务器进程的可用数量,分发器会将这些应用程序放入队列中等待。
优缺点分析
共享服务器方法看起来似乎优于前面描述的其他方法:
-
优点:
- 允许单线程编程。
- 可以长时间保留会话状态。
- 通过限制服务器进程数量降低了过载风险。
-
缺点:
- 额外开销 :
- 每次客户端与服务器之间的数据库操作(每次 TCP/IP 往返)都会引入额外的进程切换,增加了总的路径长度。
- 每次往返都需要将 UGA 的内存映射到实际进程中。
- 配置复杂性 :
- UGA 无法再成为每个进程内存(PGA,程序全局区)的一部分。
- 由于会话的实际工作由随机进程处理,UGA 必须对所有进程可用,因此被分配到 SGA(系统全局区),这需要更复杂的数据库管理。
- 功能限制 :
- 使用共享服务器时,一些功能(如 Exadata 的智能扫描)不可用。
- 额外开销 :
设计建议
基于上述缺点,应用程序不应专门为共享服务器而设计 。
共享服务器模式应仅适用于以下场景:现有应用程序无法修改以实现用户界面和数据库工作的明确分离,以适应会话池模式时,可以考虑使用共享服务器。
六. 总结
四个模型对比
|----------|--------------------------------------|-------------------------------------------------|--------------------------|---------------------------------------------|
| 应用模式 | 专用连接 | 客户端会话池 | DRCP | 共享服务器 |
| 应用风格 | 任何应用,包括传统模型,长时间保留会话状态。 | 需要将用户界面与数据库工作分离,需显式调用获取和释放会话;释放会话后状态(少数例外)无法保留。 | 任何应用 | 任何应用 |
| 编程模型 | 任何 | 需要多线程编程 | 任何 | 任何 |
| 优点 | 支持传统应用 | 数据库资源仅在实际使用时保留 | 数据库资源仅在实际使用时保留 | 支持传统应用 |
| 缺点 | 数据库资源未使用时仍被占用;高数据库竞争风险 | 需要多线程编程 | 数据库资源消耗在池化上 | 数据库资源消耗在池化上;每次往返需进行进程切换;数据库配置复杂;部分数据库功能不可用。 |
| 设计考量 | 适合执行批处理操作,例如在登录和注销之间执行许多或重型数据库调用的情况。 | 为所有OLTP风格应用的首选设计 | 可用于OLTP风格应用,但不支持多线程编程的情况 | 仅适用于无法采用其他模型的现有应用。 |
参考短连接造成系统负载过高的案例"一个很小的系统为什么负载那么高?"
一个很小的系统为什么负载那么高?_esxi cpu占用高-CSDN博客
整理翻译至oracle官方文件,原文的如下链接
https://download.oracle.com/ocomdocs/global/Application_Programming_Using_Pooling.pdf