PostgreSQL中的锁:4.内存中的锁

让我提醒您,我们已经讨论过关系 锁,行级锁其他对象 (包括谓词)锁以及不同类型的锁之间的关系。

今天,我以关于内存锁的文章结束本系列。 我们将讨论自旋锁,轻型锁和缓冲区锁,以及期望监视和采样工具。



自旋锁


与普通的“重型”锁不同,更轻巧(在开销方面)的锁用于保护共享RAM中的结构。

其中最简单的是自旋锁自旋锁 。 它们旨在在很短的时间内捕获(几个处理器指令),并保护内存的各个部分免受同时更改的影响。

自旋锁是基于来自处理器的原子指令(例如比较和交换)实现的。 它们支持单个排他模式。 如果锁繁忙,则等待过程将执行主动等待-重复执行命令(循环中“旋转”,因此命名),直到命令成功执行为止。 这是有道理的,因为在估计冲突的可能性非常低时使用了自旋锁。

自旋锁不提供对死锁的检测(PostgreSQL开发人员正在监视它),并且不提供任何监视工具。 总的来说,我们对自旋锁唯一能做的就是知道它们的存在。

轻锁


接下来是所谓的轻型锁 (轻型锁,lwlocks)。

它们会在处理数据结构(例如,哈希表或指针列表)所需的短时间内被捕获。 通常,轻锁不会保持很长时间,但是在某些情况下,轻锁会保护I / O操作,因此原则上时间会变得很长。

支持两种模式:独占(用于更改数据)和共享(只读)。 因此,没有等待队列:如果有多个进程正在等待释放锁,则其中一个进程或多或少会随机获得访问权限。 在具有高度并行性和高负载的系统中,这可能导致令人不快的影响(例如,请参见讨论 )。

没有提供检查死锁的机制,这仍然取决于内核开发人员。 但是,电锁具有监视工具,因此,与自旋锁不同,可以“看到”它们(稍后再说明)。

剪辑缓冲区


我们已经在文章中讨论了有关缓冲区高速缓存的另一种类型的锁是缓冲区固定

使用固定的缓冲区,您可以执行各种操作,包括更改数据,但条件是由于多版本,这些更改对于其他进程将是不可见的。 也就是说,您可以在页面上添加新行,但是不能用另一行替换缓冲区中的页面。

如果绑定阻碍了该过程,则通常只跳过这样的缓冲区并选择另一个。 但是在某些情况下,当需要此特定缓冲区时,过程会排队并入睡-取消固定后,系统将唤醒它。

合并期望可用于监视。

示例:缓冲区缓存




现在,要了解一些(不完整!)如何使用锁以及在何处使用锁,请考虑一个缓冲区高速缓存的示例。

若要访问包含对缓冲区的引用的哈希表,该过程必须在共享模式下捕获轻型缓冲区映射锁,如果需要更改表,则必须在异常模式下进行。 为了降低粒度,此锁安排为一部分,由128个单独的锁组成,每个锁都保护自己的哈希表部分。

该过程使用自旋锁访问缓冲区的头。 使用来自处理器的原子指令,也可以在没有显式锁的情况下执行单独的操作(例如递增计数器)。

要读取缓冲区的内容,需要缓冲区内容锁定。 通常,仅在读取指向行版本的指针所需的时间内捕获它,然后缓冲区剪辑提供的保护就足够了。 若要修改缓冲区的内容,必须以特殊模式捕获此锁。

从磁盘读取缓冲区(或写入磁盘)时,也会捕获进行中的IO,这表明其他进程正在读取(或写入)该页面-如果他们还需要对该页面执行某些操作,它们可以排队。

指向空闲缓冲区和下一个受害者的指针受单个缓冲区策略锁旋转锁的保护。

示例:日志缓冲区



另一个示例:日志缓冲区。

对于日记缓存,还使用哈希表,该哈希表包含页面到缓冲区的映射。 与缓冲区高速缓存不同,此哈希表受WALBufMappingLock唯一的轻型锁保护,因为日志高速缓存的大小较小(通常为缓冲区高速缓存的1/32),并且对缓冲区的访问更加简化。

将页面写入磁盘受到轻量级WALWriteLock锁的保护,因此一次仅一个进程可以执行此操作。

要创建日记帐分录,该过程必须首先在WAL页面上保留一个空间。 为此,它捕获了自旋锁插入位置锁。 保留位置后,该过程会将其记录的内容复制到指定位置。 复制可以同时由多个过程执行,为此,记录由8个易锁插入锁(每个过程必须捕获其中的一个)来保护。

该图未显示与预记录日志有关的所有锁,但是此示例和上一个示例应提供一些有关在RAM中使用锁的想法。

期望监控


从PostgreSQL 9.6开始,等待监视工具内置在pg_stat_activity视图中。 当某个进程(系统或维护)无法完成其工作并正在等待某事时,可以在视图中看到这种期望:wait_event_type列指示期望的类型,而wait_event列指示特定期望的名称。

请记住,视图仅显示在源代码中适当处理的那些期望。 如果视图未显示期望,则通常并不意味着该过程确实没有任何期望的可能性为100%。

不幸的是,关于期望的唯一可用信息是当前信息。 不保留任何统计信息。 随时间推移获得期望的图片的唯一方法是通过以特定间隔对视图状态进行采样 。 没有内置的方法,但是您可以使用扩展名,例如pg_wait_sampling

有必要考虑抽样的概率性质。 为了获得或多或少可靠的图像,测量次数必须足够大。 低频采样可能无法提供可靠的图像,而增加频率将导致开销增加。 出于同样的原因,采样对于分析短期会话毫无用处。

所有期望可以分为几种类型。

对所考虑的锁的期望占很大的类别:

  • 等待对象锁定(wait_event_type列中的锁定值);
  • 等待电灯锁(LWLock);
  • 等待固定的缓冲区(BufferPin)。

但是流程可以预期其他事件:

  • 当进程需要写入或读取数据时,会发生I / O期望(IO)。
  • 流程可以等待来自客户端(客户端)或另一个流程(IPC)的工作所需的数据;
  • 扩展可以注册其特定期望(扩展)。

在某些情况下,流程根本无法完成有用的工作。 此类别包括:

  • 在其主循环(活动)中等待后台进程;
  • 等待计时器(超时)。

通常,这种期望是“正常的”,不会带来任何问题。

期望类型后跟特定期望的名称。 完整表可在文档中找到。

如果未指定等待名称,则进程不处于等待状态。 这样的时间应该被认为是无法解释的 ,因为实际上目前尚不清楚确切发生了什么。

但是,该看一下了。

=> SELECT pid, backend_type, wait_event_type, wait_event FROM pg_stat_activity; 
  pid | backend_type | wait_event_type | wait_event -------+------------------------------+-----------------+--------------------- 28739 | logical replication launcher | Activity | LogicalLauncherMain 28736 | autovacuum launcher | Activity | AutoVacuumMain 28963 | client backend | | 28734 | background writer | Activity | BgWriterMain 28733 | checkpointer | Activity | CheckpointerMain 28735 | walwriter | Activity | WalWriterMain (6 rows) 

可以看出,所有后台服务进程都在“混乱”。wait_event_type和wait_event中的空值表示该进程不期望任何内容-在我们的示例中,服务进程正在忙于执行请求。

取样方式


为了使用采样大致了解期望,我们使用pg_wait_sampling扩展。 它必须从源代码编译; 我将省略这部分。 然后,我们在shared_preload_libraries参数中注册该库,然后重新启动服务器。

 => ALTER SYSTEM SET shared_preload_libraries = 'pg_wait_sampling'; 

 student$ sudo pg_ctlcluster 11 main restart 

现在在数据库中安装扩展。

 => CREATE EXTENSION pg_wait_sampling; 

该扩展允许您查看期望历史记录,该历史记录存储在循环缓冲区中。 但是最有趣的是查看期望的概况-整个工作时间的累积统计数据。

这是我们将在几秒钟后看到的内容:

 => SELECT * FROM pg_wait_sampling_profile; 
  pid | event_type | event | queryid | count -------+------------+---------------------+---------+------- 29074 | Activity | LogicalLauncherMain | 0 | 220 29070 | Activity | WalWriterMain | 0 | 220 29071 | Activity | AutoVacuumMain | 0 | 219 29069 | Activity | BgWriterMain | 0 | 220 29111 | Client | ClientRead | 0 | 3 29068 | Activity | CheckpointerMain | 0 | 220 (6 rows) 

由于自服务器启动以来未发生任何事情,因此主要的期望类型是活动(服务进程正在等待工作出现)和客户端(psql正在等待用户发送请求)。

使用默认设置( pg_wait_sampling.profile_period参数),采样周期为10毫秒,即每秒保存100次。 因此,要估计等待时间(以秒为单位),必须将count的值除以100。

为了理解期望的过程,我们将pg_stat_activity视图添加到请求中:

 => SELECT p.pid, a.backend_type, a.application_name AS app, p.event_type, p.event, p.count FROM pg_wait_sampling_profile p LEFT JOIN pg_stat_activity a ON p.pid = a.pid ORDER BY p.pid, p.count DESC; 
  pid | backend_type | app | event_type | event | count -------+------------------------------+------+------------+----------------------+------- 29068 | checkpointer | | Activity | CheckpointerMain | 222 29069 | background writer | | Activity | BgWriterMain | 222 29070 | walwriter | | Activity | WalWriterMain | 222 29071 | autovacuum launcher | | Activity | AutoVacuumMain | 221 29074 | logical replication launcher | | Activity | LogicalLauncherMain | 222 29111 | client backend | psql | Client | ClientRead | 4 29111 | client backend | psql | IPC | MessageQueueInternal | 1 (7 rows) 

让我们加载pgbench,看看图片是如何变化的。

 student$ pgbench -i test 

我们将收集的配置文件重置为零,并在一个单独的过程中运行测试30秒。

 => SELECT pg_wait_sampling_reset_profile(); 

 student$ pgbench -T 30 test 

该请求必须在pgbench进程完成之前完成:

 => SELECT p.pid, a.backend_type, a.application_name AS app, p.event_type, p.event, p.count FROM pg_wait_sampling_profile p LEFT JOIN pg_stat_activity a ON p.pid = a.pid WHERE a.application_name = 'pgbench' ORDER BY p.pid, p.count DESC; 
  pid | backend_type | app | event_type | event | count -------+----------------+---------+------------+------------+------- 29148 | client backend | pgbench | IO | WALWrite | 8 29148 | client backend | pgbench | Client | ClientRead | 1 (2 rows) 

当然,根据特定系统的不同,对pgbench的期望也会有所不同。 在我们的案例中,很有可能会显示等待日志条目(IO / WALWrite),但大多数情况下,该过程并非空闲,而是参与了一些有用的事情。

轻锁


您应该永远记住,采样时没有任何期望并不意味着没有期望。 如果它短于采样周期(在我们的示例中为百分之一秒),则它根本就不会落入样本中。

因此,配置文件中没有出现光锁-但是,如果您长时间收集数据,则会显示光锁。 为了保证对它们的了解,您可以人为地降低文件系统的速度,例如,使用基于FUSE文件系统构建的slowfs项目。

这是我们在同一测试中可以看到,如果任何I / O操作花费1/10秒的时间。

 => SELECT pg_wait_sampling_reset_profile(); 

 student$ pgbench -T 30 test 

 => SELECT p.pid, a.backend_type, a.application_name AS app, p.event_type, p.event, p.count FROM pg_wait_sampling_profile p LEFT JOIN pg_stat_activity a ON p.pid = a.pid WHERE a.application_name = 'pgbench' ORDER BY p.pid, p.count DESC; 
  pid | backend_type | app | event_type | event | count -------+----------------+---------+------------+----------------+------- 29240 | client backend | pgbench | IO | WALWrite | 1445 29240 | client backend | pgbench | LWLock | WALWriteLock | 803 29240 | client backend | pgbench | IO | DataFileExtend | 20 (3 rows) 

现在,pgbench进程的主要期望与I / O有关,或者更确切地说,与每次提交以同步模式执行的日志条目有关。 由于(如上面的示例所示)将日志写入磁盘是受轻量级的WALWriteLock锁保护的,因此该锁也出现在配置文件中-这正是我们要查看的内容。

剪辑缓冲区


要查看缓冲区的固定,我们利用了以下事实:打开的游标会固定该引脚,以便更快地读取下一行。

我们开始交易,打开光标并选择一行。

 => BEGIN; => DECLARE c CURSOR FOR SELECT * FROM pgbench_history; => FETCH c; 
  tid | bid | aid | delta | mtime | filler -----+-----+-------+-------+----------------------------+-------- 9 | 1 | 35092 | 477 | 2019-09-04 16:16:18.596564 | (1 row) 

检查缓冲区是否固定(pinning_backends):

 => SELECT * FROM pg_buffercache WHERE relfilenode = pg_relation_filenode('pgbench_history') AND relforknumber = 0 \gx 
 -[ RECORD 1 ]----+------ bufferid | 190 relfilenode | 47050 reltablespace | 1663 reldatabase | 16386 relforknumber | 0 relblocknumber | 0 isdirty | t usagecount | 1 pinning_backends | 1 <--   1  

现在我们将清除表:

 | => SELECT pg_backend_pid(); 
 | pg_backend_pid | ---------------- | 29367 | (1 row) 

 | => VACUUM VERBOSE pgbench_history; 
 | INFO: vacuuming "public.pgbench_history" | INFO: "pgbench_history": found 0 removable, 0 nonremovable row versions in 1 out of 1 pages | DETAIL: 0 dead row versions cannot be removed yet, oldest xmin: 732651 | There were 0 unused item pointers. 
 | Skipped 1 page due to buffer pins, 0 frozen pages. 
 | 0 pages are entirely empty. | CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s. | VACUUM 

如我们所见,页面被跳过(由于缓冲针而跳过了1页)。 实际上,清理无法处理它,因为禁止从固定缓冲区中的页面物理删除行版本。 但是清洁不会等待-该页面将在下次处理。

现在我们将冻结进行清洁

 | => VACUUM FREEZE VERBOSE pgbench_history; 

使用明确请求的冻结,您将无法跳过冻结图中未标记的单个页面-否则,无法减少pg_class.relfrozenxid中未冻结事务的最大期限。 因此,清除挂起,直到光标关闭。

 => SELECT age(relfrozenxid) FROM pg_class WHERE oid = 'pgbench_history'::regclass; 
  age ----- 27 (1 row) 
 => COMMIT; --    

 | INFO: aggressively vacuuming "public.pgbench_history" | INFO: "pgbench_history": found 0 removable, 26 nonremovable row versions in 1 out of 1 pages | DETAIL: 0 dead row versions cannot be removed yet, oldest xmin: 732651 | There were 0 unused item pointers. 
 | Skipped 0 pages due to buffer pins, 0 frozen pages. 
 | 0 pages are entirely empty. | CPU: user: 0.00 s, system: 0.00 s, elapsed: 3.01 s. | VACUUM 

 => SELECT age(relfrozenxid) FROM pg_class WHERE oid = 'pgbench_history'::regclass; 
  age ----- 0 (1 row) 

好吧,让我们看一下执行VACUUM命令的第二个psql会话的期望配置文件:

 => SELECT p.pid, a.backend_type, a.application_name AS app, p.event_type, p.event, p.count FROM pg_wait_sampling_profile p LEFT JOIN pg_stat_activity a ON p.pid = a.pid WHERE p.pid = 29367 ORDER BY p.pid, p.count DESC; 
  pid | backend_type | app | event_type | event | count -------+----------------+------+------------+------------+------- 29367 | client backend | psql | BufferPin | BufferPin | 294 29367 | client backend | psql | Client | ClientRead | 10 (2 rows) 

等待类型BufferPin指示刷新正在等待释放缓冲区。

基于此,我们将假定我们已完成锁定。 谢谢大家的关注和评论!

Source: https://habr.com/ru/post/zh-CN466199/


All Articles