【PostgreSQL内核学习(二十六) —— (共享数据缓冲区)】

共享数据缓冲区

  • 概述
    • 共享数据缓冲区管理
      • 共享缓冲区管理的核心功能包括:
    • 共享数据缓冲区的组织结构
      • 初始化共享缓冲池
      • BufferDesc 结构体
      • InitBufferPool 函数
    • 如何确定请求的数据页面是否在缓冲区中?
      • BufferTag 结构体
      • RelFileNode 结构体
      • ForkNumber 结构体
      • ReadBuffer_common 函数
    • 怎么查看缓冲区中每个缓冲块的状态?
      • BufferDesc 结构体
      • pg_atomic_uint32
    • 怎么快速找到缓冲区的空页?
      • StrategyGetBuffer 函数
      • BufferStrategyControl 结构体
    • 如果没有空闲页了,怎么办?

声明:本文的部分内容参考了他人的文章。在编写过程中,我们尊重他人的知识产权和学术成果,力求遵循合理使用原则,并在适用的情况下注明引用来源。
本文主要参考了 postgresql-10.1 的开源代码和《OpenGauss数据库源码解析》和《PostgresSQL数据库内核分析》一书以及一些相关资料。

概述

  PostgreSQL 的共享数据缓冲区(Shared Buffer Cache)是数据库系统中用于存储从磁盘读取的数据页的内存区域,以便快速重用这些数据,从而减少对磁盘的访问次数和提高查询性能。当数据库需要访问某个数据页时,它首先检查该页是否已在共享数据缓冲区中;如果直接从内存中读取数据,否则从磁盘加载数据页到缓冲区再进行访问
  共享数据缓冲区的大小是可配置的,通过调整 shared_buffers 参数来实现,这个参数定义了缓冲区分配的内存量。理想的缓冲区大小取决于系统的总内存数据库的工作负载以及其他内存需求。正确配置共享数据缓冲区对于优化数据库性能至关重要,因为它直接影响到数据检索的速度。PostgreSQL 还实现了复杂的缓冲区管理策略,比如使用 LRU(最近最少使用)算法来决定哪些数据页应当保留在缓冲区中哪些可以被替换出去,以确保最常访问的数据页尽可能地保留在内存中。

共享数据缓冲区管理

  共享缓冲区管理是数据库管理系统(DBMS)中的一个关键组件,特别是在 PostgreSQL 这样的关系数据库系统中。它涉及到数据库系统如何在内存中有效管理缓存访问数据页的机制。共享缓冲区(Shared Buffers)是内存中的一个区域,用于存储最近访问的数据页(包括索引的数据),以便快速响应后续的数据查询和操作,从而减少直接磁盘 I/O 的需求,提升数据库的整体性能。

共享缓冲区管理的核心功能包括:

  1. 数据页缓存: 当数据库操作需要访问磁盘上的数据页时,这些页首先被加载到共享缓冲区中。如果后续操作再次请求相同的数据页,系统可以直接从内存中获取,避免了磁盘访问的延迟。
  2. 写入策略: 为了保证数据的持久性,对共享缓冲区中的数据页所做的更改最终需要写回磁盘。PostgreSQL 使用写前日志(WAL)来确保即使在系统崩溃的情况下,所有的事务更改也能被恢复。数据页的实际写回可以延迟执行,直到系统认为最合适的时机,比如在缓冲区空间不足或定期检查点(Checkpoint)发生时。
  3. 缓冲区替换策略: 共享缓冲区的大小是有限的,当缓冲区满时,数据库需要决定哪些数据页保留在内存中,哪些被替换出去PostgreSQL 使用近似最近最少使用(LRU)算法进行页替换,以优先保留最频繁访问的数据页。
  4. 配置和调优: 数据库管理员可以通过调整 shared_buffers 参数来配置共享缓冲区的大小,这通常根据系统的总内存和数据库的工作负载进行优化。合理配置共享缓冲区对于提高数据库的性能至关重要。
    在这里插入图片描述
      上图描绘的是数据库中共享数据缓冲区管理的概念模型,其中:
  1. 矩形块: 代表数据库的共享缓冲区中的多个缓冲槽buffer slots),每个槽可以存储一个数据块的内容。这是内存中分配给数据库缓存数据块的区域。其中,一个缓存块的大小由参数 BLOCK_SIZE 定义,在系统初始化时定义。
  2. 椭圆形:缓冲区管理器”(Buffer Manager)的象征,它负责管理共享缓冲区的操作,包括决定何时从磁盘读取数据块到缓冲区,何时将更改后的数据块写回磁盘,以及如何在内存中替换数据块。
  3. 圆柱形:DataBase” 代表数据库的持久存储,如磁盘其他形式持久存储介质,其中实际存储所有数据块。
  4. 读/写操作: 箭头表示数据的读写操作。数据库操作(如查询更新)首先查找共享缓冲区以确定所需的数据块是否已加载到内存中。如果找到,直接进行读写操作,提高性能;如果没有找到,从磁盘读取数据块到共享缓冲区再进行操作。
  5. 备份/恢复: 从共享缓冲区到数据库的双向箭头可能表示数据的备份和恢复流程。在备份时,数据从共享缓冲区写入到备份存储;在恢复时,备份数据可以重新加载到共享缓冲区,然后被恢复到数据库中。

  这张数据库共享缓冲区与管理机制图展示了数据库内存中的共享缓冲区是如何被缓冲区管理器管理的,以及数据块是如何在内存和磁盘之间流动的。这种机制对于数据库的性能和数据的高效处理至关重要,因为它大大减少了对磁盘 I/O 操作的需求,而磁盘 I/O 通常是数据库操作中最耗时的部分。

共享数据缓冲区的组织结构

  PostgreSQL 的共享数据缓冲区是由一组内存页组成的结构,这些页在物理上可以分散存储,但在缓冲区管理器的控制下逻辑上是连续的。每个内存页对应数据库磁盘上的一个数据块,当数据块被读取时,它们被载入这些内存页中。共享数据缓冲区的大小由 shared_buffers 配置参数确定,它定义了内存中用于缓存的页的总数。缓冲区管理器负责维护一个页表,记录哪些数据块当前在缓冲区中,以及它们的使用状态。通过精心设计的替换策略,如 LRU(最近最少使用)算法,管理器优先保留最活跃的数据块,以优化数据的访问速度和减少磁盘 I/O,从而提升数据库操作的效率。这个组织结构是实现 PostgreSQL 高性能数据处理的基础。

初始化共享缓冲池

  共享缓冲池处于共享内存区域,在系统启动的时候需要对其进行初始化操作,负责这一工作的函数为 InitBufferPool。在共享缓冲池管理中,使用了一个全局数组 BufferDescriptor管理池中的缓冲区,其数组元素类型为 BufferDesc数组元素个数为缓冲区的总数,用于管理所有缓冲区的描述符可以认为 BufferDescriptors 就是共享缓冲池。另外还使用了一个全局指针变量 BufferBlocks 来存储缓冲池的起始地址。每个缓冲区对应一个描述符结构 BufferDesc,用于描述相应缓冲区的状态信息。

BufferDesc 结构体

  在 PostgreSQL 的源码中,与共享数据缓冲区管理相关的主要结构体是 BufferDesc,它位于 src/include/storage/buf_internals.h 文件中。BufferDesc 结构体表示一个共享缓冲区内的单个缓冲页,其中包含了页的状态信息锁信息引用计数等。每个 BufferDesc与一个在共享内存中的数据块相对应,对应的结构体源码如下:

typedef struct BufferDesc
{
    BufferTag    tag;            /* 该缓冲页包含的页面的标识符,包含表空间、数据库、关系和块号信息 */
    int          buf_id;         /* 缓冲页的索引编号,从0开始计数 */

    /* 标签的状态,包含标志位、引用计数和使用计数 */
    pg_atomic_uint32 state;      

    int          wait_backend_pid; /* 如果有后台进程在等待该缓冲页的引用计数变为零,则这里记录该后台进程的PID */
    int          freeNext;       /* 在空闲链表中的下一个元素的链接 */

    LWLock       content_lock;   /* 用于锁定对缓冲页内容的访问的轻量级锁 */
} BufferDesc;

在这里插入图片描述

  BufferDesc 结构体描述了 PostgreSQL 中的一个缓冲页数据块)的元数据,这些元数据包括:

  • 缓冲页的内容信息: 通过 BufferTag 结构体标识缓冲页包含的特定页面
  • 缓冲页的位置和状态信息: buf_id 用于标识缓冲页在共享缓冲区中的位置state 用于存储缓冲页的状态,包括是否被修改脏页)、引用计数等。
  • 并发控制信息: wait_backend_pidcontent_lock 用于在并发环境下控制对缓冲页的访问,确保数据的一致性和完整性。
  • 缓冲页的链表信息: freeNext 用于将多个 BufferDesc 串联成一个链表,这通常用于维护空闲的缓冲页。

  此外,管理共享数据缓冲区的核心函数和控制逻辑位于 src/backend/storage/buffer 目录下。特别是,bufmgr.c 文件包含了对缓冲区进行操作的函数,如读取写入分配替换缓冲页的函数。
  PostgreSQL 还使用了缓冲池的概念,通常是一个 BufferPool 结构,它是一个 BufferDesc 数组,表示整个共享缓冲区。缓冲池的管理还涉及锁定机制和引用计数,以确保对缓冲页的并发访问是安全的。

InitBufferPool 函数

  其中,InitBufferPool 函数用于在数据库系统初始化期间设置共享缓冲区。这个函数首先为缓冲区描述符缓冲区块I/O 锁以及检查点用的缓冲区 ID 数组在共享内存中分配空间,并对它们进行初始化。这包括设置每个缓冲区描述符的状态标识符链接到空闲列表的下一个缓冲区、以及初始化相关的轻量级锁LWLock)。如果这些结构在共享内存中已经存在(由 foundDescs 等变量标识),则验证它们全部存在(这是在后台进程模型中期望的情况)。若这些结构尚未存在,则初始化一个新的共享缓冲区,包括设置所有缓冲区描述符并将它们链接成空闲列表。此外,该函数还会初始化用于缓冲区管理策略的其他相关数据结构,以及为后台写回上下文设置文件刷新策略。这个函数是 PostgreSQL 启动时建立核心数据结构和内存管理基础的关键部分,对于数据库能够高效处理缓存和 I/O 操作至关重要。函数源码如下所示:(路径:src\backend\storage\buffer\buf_init.c

/* 初始化共享缓冲池 */
void
InitBufferPool(void)
{
    /* 几个布尔变量,用于标记是否已经找到了共享内存中的相应结构 */
	bool		foundBufs,
				foundDescs,
				foundIOLocks,
				foundBufCkpt;

	/* 将缓冲描述符对齐到缓存线界限 */
	BufferDescriptors = (BufferDescPadded *)
		ShmemInitStruct("Buffer Descriptors",
						NBuffers * sizeof(BufferDescPadded),
						&foundDescs);

	BufferBlocks = (char *)
		ShmemInitStruct("Buffer Blocks",
						NBuffers * (Size) BLCKSZ, &foundBufs);

	/* 将轻量级锁对齐到缓存线界限 */
	BufferIOLWLockArray = (LWLockMinimallyPadded *)
		ShmemInitStruct("Buffer IO Locks",
						NBuffers * (Size) sizeof(LWLockMinimallyPadded),
						&foundIOLocks);

	LWLockRegisterTranche(LWTRANCHE_BUFFER_IO_IN_PROGRESS, "buffer_io");
	LWLockRegisterTranche(LWTRANCHE_BUFFER_CONTENT, "buffer_content");

	/*
	 * 用于排序待检查点缓冲区ID的数组位于共享内存中,
	 * 以避免在运行时分配大量内存。因为这将在检查点过程中进行,
	 * 或者当检查点重启时,内存分配失败将是痛苦的。
	 */
	CkptBufferIds = (CkptSortItem *)
		ShmemInitStruct("Checkpoint BufferIds",
						NBuffers * sizeof(CkptSortItem), &foundBufCkpt);

	if (foundDescs || foundBufs || foundIOLocks || foundBufCkpt)
	{
		/* 应该同时找到所有这些,或者一个都不找到 */
		Assert(foundDescs && foundBufs && foundIOLocks && foundBufCkpt);
		/* 注意:这条路径只在 EXEC_BACKEND 情况下执行 */
	}
	else
	{
		int			i;

		/*
		 * 初始化所有缓冲头。
		 */
		for (i = 0; i < NBuffers; i++)
		{
			BufferDesc *buf = GetBufferDescriptor(i);

			CLEAR_BUFFERTAG(buf->tag);

			pg_atomic_init_u32(&buf->state, 0);
			buf->wait_backend_pid = 0;

			buf->buf_id = i;

			/*
			 * 最初将所有缓冲区作为未使用的链接在一起。
			 * 后续的这个列表的管理由 freelist.c 执行。
			 */
			buf->freeNext = i + 1;

			LWLockInitialize(BufferDescriptorGetContentLock(buf),
							 LWTRANCHE_BUFFER_CONTENT);

			LWLockInitialize(BufferDescriptorGetIOLock(buf),
							 LWTRANCHE_BUFFER_IO_IN_PROGRESS);
		}

		/* 纠正链接列表的最后一个条目 */
		GetBufferDescriptor(NBuffers - 1)->freeNext = FREENEXT_END_OF_LIST;
	}

	/* 初始化其他共享缓冲管理相关的东西 */
	StrategyInitialize(!foundDescs);

	/* 初始化每个后端文件刷新上下文 */
	WritebackContextInit(&BackendWritebackContext,
						 &backend_flush_after);
}

其中,BufferBlocks[] 使用 buffer_id 标识每个缓冲块。
在这里插入图片描述

如何确定请求的数据页面是否在缓冲区中?

  在 PostgreSQL 中,确定请求的数据页面是否在缓冲区中,主要涉及共享缓冲区(Shared Buffer Cache)的查找过程。这个过程大致如下:

  1. 缓冲区标识: 每个数据页面在缓冲区中的存在通过一个缓冲区标识符BufferTag)来标识。这个标识符包括表空间 IDTablespace OID)、数据库 IDDatabase OID)、关系 IDRelation OID)、分叉号Fork Number),以及块号Block Number)。这些信息共同定义了数据页面的唯一性
  2. 缓冲区哈希表: PostgreSQL 使用一个缓冲区哈希表Buffer Mapping Hash Table)来快速查找缓冲区中是否存在指定的数据页面。这个哈希表将缓冲区标识符映射到缓冲区描述符Buffer Descriptor)。
  3. 查找过程:
    • 当数据库需要访问一个数据页面时,它首先构造该页面的缓冲区标识符
    • 然后,数据库通过缓冲区哈希表查找该标识符对应的缓冲区描述符
    • 如果找到了匹配的缓冲区描述符,这意味着请求的数据页面已经在共享缓冲区中
  4. 缓冲区描述符: 缓冲区描述符包含了关于缓冲区状态的信息,如是否被锁定、是否被修改(脏页面),以及关联的数据页面等。如果数据页面在缓冲区中找到,数据库可以直接从内存中访问这个页面,避免了磁盘 I/O 操作。
  5. 未命中缓冲区: 如果请求的数据页面不在缓冲区中(即缓冲区哈希表中没有找到对应的缓冲区描述符),PostgreSQL 需要从磁盘读取该页面。读取后,数据库可能需要在共享缓冲区中为这个页面分配一个新的缓冲槽,并更新缓冲区哈希表,以便未来的访问可以直接从内存中获取。

  通过这种机制,PostgreSQL 可以有效地管理内存中的数据页面,优化数据访问性能。这个过程是数据库缓冲区管理策略的一部分,对于减少数据库的响应时间和提高事务处理的吞吐量至关重要。

BufferTag 结构体

  BufferTag 结构体,它在 PostgreSQL 中用于唯一标识共享缓冲区中的一个数据块BufferTag 是缓冲区管理系统的核心部分,它允许数据库精确地追踪和访问内存中的数据页面。下面是对该结构体中每个字段的详细解释:(路径:src\include\storage\buf_internals.h

typedef struct buftag
{
    RelFileNode rnode;			/* 物理关系标识符,用于唯一标识一个数据库中的表或索引 */
    ForkNumber	forkNum;		/* 分叉号,指明数据块属于关系的哪个物理文件(如主数据文件、TOAST文件等)*/
    BlockNumber blockNum;		/* 块号,相对于关系的开始位置,用于在其所属分叉中唯一标识一个数据块 */
} BufferTag;

  BufferTag 结构体通过组合关系的物理标识符分叉号和块号BufferTag 能够精确地指向数据库中的一个具体数据块,这对于缓冲区管理、数据访问和数据一致性维护至关重要。BufferTag 可以用来查询对应于该文件块的缓冲区,查询流程如下:
在这里插入图片描述

RelFileNode 结构体

  RelFileNode 结构体在 PostgreSQL 中用于表示一个数据库关系(如表或索引)的物理位置。结构体中的每个字段都是使用 Oid对象标识符)类型表示的,用于唯一标识数据库中的对象。下面是对该结构体中每个字段的详细解释:(路径:D:\pg相关src\include\storage\relfilenode.h

typedef struct RelFileNode
{
    Oid         spcNode;        /* 表空间的 OID,用于标识数据块所在的表空间 */
    Oid         dbNode;         /* 数据库的 OID,用于标识数据块所属的数据库 */
    Oid         relNode;        /* 关系的 OID,用于标识具体的表或索引 */
} RelFileNode;

  RelFileNode 结构体通过组合表空间数据库关系的 OID 来完整地描述了一个数据库对象的物理存储位置。这使得 PostgreSQL 能够准确地定位到存储在磁盘上的数据块,是实现数据管理和访问的基础。在数据库系统中,表空间用于管理磁盘上的数据文件数据库 OID 将数据块与特定的数据库实例关联起来而关系 OID 则指定了数据块属于哪个具体的表或索引。这种层次化的标识方法为 PostgreSQL 的数据存储和访问提供了高度的灵活性和效率。

ForkNumber 结构体

  ForkNumber枚举类型,它在 PostgreSQL 中用于标识关系(如表或索引)的不同物理存储分叉。每个分叉代表了关系的不同物理部分,主要用于存储数据自由空间信息可见性信息等。以下是对各个枚举成员的详细中文注释:(路径:src\include\common\relpath.h

typedef enum ForkNumber
{
    InvalidForkNumber = -1,      /* 无效的分叉编号,用于错误检查或初始化 */
    MAIN_FORKNUM = 0,            /* 主分叉,用于存储表的实际数据 */
    FSM_FORKNUM,                 /* 自由空间地图(Free Space Map)分叉,用于跟踪表中未使用的空间 */
    VISIBILITYMAP_FORKNUM,       /* 可见性地图分叉,用于跟踪哪些页面对哪些事务可见 */
    INIT_FORKNUM                 /* 初始分叉,用于存储关系的初始化分叉,仅在关系创建时使用 */

    /*
     * 注意:如果添加了新的分叉,需要修改 MAX_FORKNUM 和可能的 FORKNAMECHARS,
     * 并在 src/common/relpath.c 中更新 forkNames 数组
     */
} ForkNumber;

ReadBuffer_common 函数

  ReadBuffer_common 函数是 PostgreSQL 缓冲区读取操作的核心逻辑。该函数负责从共享缓冲区中读取指定的数据块,如果数据块不在缓冲区中,则从磁盘加载。这个过程涵盖了检查缓冲区是否已存在所需数据块、处理缓冲区分配、读取数据块等多个步骤。以下是对代码的详细解释:(路径:src\backend\storage\buffer\bufmgr.c

/* ReadBuffer_common -- 所有ReadBuffer变种的通用逻辑
 *
 * 如果请求从共享缓冲区缓存中得到满足,则*hit被设置为true。
 */
static Buffer
ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
				  BlockNumber blockNum, ReadBufferMode mode,
				  BufferAccessStrategy strategy, bool *hit)
{
	BufferDesc *bufHdr;
	Block		bufBlock;
	bool		found;
	bool		isExtend;
	bool		isLocalBuf = SmgrIsTemp(smgr);

	*hit = false;

	/* 确保我们有足够的空间来记住缓冲区锁 */
	ResourceOwnerEnlargeBuffers(CurrentResourceOwner);

	isExtend = (blockNum == P_NEW); // 是否扩展操作

	TRACE_POSTGRESQL_BUFFER_READ_START(forkNum, blockNum,
									   smgr->smgr_rnode.node.spcNode,
									   smgr->smgr_rnode.node.dbNode,
									   smgr->smgr_rnode.node.relNode,
									   smgr->smgr_rnode.backend,
									   isExtend);

	/* 如果调用者请求了P_NEW,替换为适当的块号 */
	if (isExtend)
		blockNum = smgrnblocks(smgr, forkNum);

	if (isLocalBuf) // 如果是本地缓冲区
	{
		bufHdr = LocalBufferAlloc(smgr, forkNum, blockNum, &found);
		if (found)
			pgBufferUsage.local_blks_hit++;
		else
			pgBufferUsage.local_blks_read++;
	}
	else // 如果是共享缓冲区
	{
		/*
		 * 查找缓冲区。如果请求的块当前不在内存中,则设置IO_IN_PROGRESS。
		 */
		bufHdr = BufferAlloc(smgr, relpersistence, forkNum, blockNum,
							 strategy, &found);
		if (found)
			pgBufferUsage.shared_blks_hit++;
		else
			pgBufferUsage.shared_blks_read++;
	}

	/* 在这一点上我们不持有任何锁。 */

	/* 如果缓冲池中已经存在所请求的数据块,则完成处理 */
	if (found)
	{
	    if (!isExtend)
	    {
	        /* 如果不是扩展操作,只需更新统计信息即可 */
	        *hit = true; // 标记命中缓冲池
	        VacuumPageHit++; // 更新真空清理命中的页面数
	
	        if (VacuumCostActive) // 如果真空清理成本模型激活
	            VacuumCostBalance += VacuumCostPageHit; // 更新真空清理成本
	
	        TRACE_POSTGRESQL_BUFFER_READ_DONE(forkNum, blockNum,
	                                          smgr->smgr_rnode.node.spcNode,
	                                          smgr->smgr_rnode.node.dbNode,
	                                          smgr->smgr_rnode.node.relNode,
	                                          smgr->smgr_rnode.backend,
	                                          isExtend,
	                                          found);
	
	        /* 如果是 RBM_ZERO_AND_LOCK 模式,调用者期望在返回时页面被锁定 */
	        if (!isLocalBuf) // 如果不是本地缓冲区
	        {
	            if (mode == RBM_ZERO_AND_LOCK) // 如果模式为零并锁定
	                LWLockAcquire(BufferDescriptorGetContentLock(bufHdr),
	                              LW_EXCLUSIVE); // 获取内容锁
	            else if (mode == RBM_ZERO_AND_CLEANUP_LOCK) // 如果模式为零并进行清理锁定
	                LockBufferForCleanup(BufferDescriptorGetBuffer(bufHdr)); // 获取清理锁
	        }
	
	        return BufferDescriptorGetBuffer(bufHdr); // 返回缓冲区描述符对应的缓冲区
	    }
	
	    /* 处理试图扩展关系但发现已存在标记为 BM_VALID 的缓冲区的特殊情况 */
	    bufBlock = isLocalBuf ? LocalBufHdrGetBlock(bufHdr) : BufHdrGetBlock(bufHdr); // 获取缓冲区块
	    if (!PageIsNew((Page) bufBlock)) // 如果页面不是新的
	        ereport(ERROR,
	                (errmsg("unexpected data beyond EOF in block %u of relation %s",
	                        blockNum, relpath(smgr->smgr_rnode, forkNum)),
	                 errhint("This has been seen to occur with buggy kernels; consider updating your system.")));
	
	    /* 在成功之前,我们必须执行 smgrextend,否则内核不会保留该页 */
	    if (isLocalBuf)
	    {
	        /* 只需要调整标志 */
	        uint32      buf_state = pg_atomic_read_u32(&bufHdr->state);
	
	        Assert(buf_state & BM_VALID); // 断言缓冲区状态为有效
	        buf_state &= ~BM_VALID; // 清除有效位
	        pg_atomic_unlocked_write_u32(&bufHdr->state, buf_state); // 更新缓冲区状态
	    }
	    else
	    {
	        /* 处理可能有人在我们清除 BM_VALID 和 StartBufferIO 检查它之间重新设置 BM_VALID 的小概率事件 */
	        do
	        {
	            uint32      buf_state = LockBufHdr(bufHdr); // 锁定缓冲区头
	
	            Assert(buf_state & BM_VALID); // 断言缓冲区状态为有效
	            buf_state &= ~BM_VALID; // 清除有效位
	            UnlockBufHdr(bufHdr, buf_state); // 解锁缓冲区头
	        } while (!StartBufferIO(bufHdr, true)); // 开始缓冲区 IO
	    }
	}
	
	/* 在这一点上,我们已为页面分配了缓冲区,但其内容尚未有效。如果是共享缓冲区,IO_IN_PROGRESS 已设置。*/
	Assert(!(pg_atomic_read_u32(&bufHdr->state) & BM_VALID)); // 断言缓冲区状态不为有效(不需要自旋锁)
	
	bufBlock = isLocalBuf ? LocalBufHdrGetBlock(bufHdr) : BufHdrGetBlock(bufHdr); // 获取缓冲区块
	
	if (isExtend)
	{
	    /* 新缓冲区被零填充 */
	    MemSet((char *) bufBlock, 0, BLCKSZ); // 零填充新缓冲区
	    smgrextend(smgr, forkNum, blockNum, (char *) bufBlock, false); // 扩展存储管理器中的文件
	}
	else
	{
	    /* 读取页面,除非调用者打算覆盖它,只是希望我们分配一个缓冲区。*/
	    // 省略了读取数据块并进行错误处理的代码
	}
	
	/* 如果模式为 RBM_ZERO_AND_LOCK 或 RBM_ZERO_AND_CLEANUP_LOCK,在标记页面有效之前获取缓冲区内容锁 */
	if ((mode == RBM_ZERO_AND_LOCK || mode == RBM_ZERO_AND_CLEANUP_LOCK) && !isLocalBuf)
	{
	    LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_EXCLUSIVE); // 获取独占内容锁
	}
	
	if (isLocalBuf)
	{
	    /* 只需要调整标志 */
	    uint32      buf_state = pg_atomic_read_u32(&bufHdr->state);
	
	    buf_state |= BM_VALID; // 设置缓冲区状态为有效
	    pg_atomic_unlocked_write_u32(&bufHdr->state, buf_state); // 更新缓冲区状态
	}
	else
	{
	    /* 设置 BM_VALID,终止 IO,并唤醒任何等待者 */
	    TerminateBufferIO(bufHdr, false, BM_VALID); // 终止缓冲区 IO
	}
	
	VacuumPageMiss++; // 更新真空清理未命中页面数
	if (VacuumCostActive) // 如果真空清理成本模型激活
	    VacuumCostBalance += VacuumCostPageMiss; // 更新真空清理成本
	
	TRACE_POSTGRESQL_BUFFER_READ_DONE(forkNum, blockNum,
	                                  smgr->smgr_rnode.node.spcNode,
	                                  smgr->smgr_rnode.node.dbNode,
	                                  smgr->smgr_rnode.node.relNode,
	                                  smgr->smgr_rnode.backend,
	                                  isExtend,
	                                  found); // 跟踪日志
	
	return BufferDescriptorGetBuffer(bufHdr); // 返回缓冲区描述符对应的缓冲区
}

  函数首先检查是否为扩展操作(请求新的块 P_NEW),然后根据是本地缓冲区还是共享缓冲区采取不同的处理策略。对于共享缓冲区,BufferAlloc 函数用于尝试找到缓冲区,如果找到,则更新命中或读取的统计信息;如果未找到,则进行进一步的处理,包括可能的磁盘读取
  如果请求的数据块已经存在于缓冲区中found == true),则根据是否是扩展操作读取模式(如 RBM_ZERO_AND_LOCK)进行适当的处理,包括锁定页面以供调用者使用。
  如果数据块不在缓冲区中ReadBuffer_common 会进行磁盘读取为新的数据块分配缓冲区。这涉及到处理缓冲区的有效性标记、执行实际的磁盘读取操作,以及在必要时进行错误处理。

怎么查看缓冲区中每个缓冲块的状态?

  在 PostgreSQL 中,要查看缓冲区中每个缓冲块的状态,你需要访问共享缓冲区中的缓冲区描述符Buffer Descriptor),这是因为缓冲区描述符包含了关于缓冲块状态的详细信息。此外,某些扩展和贡献模块,如 pg_buffercache,提供了查看缓冲区状态的功能。
  其中,BufferDescriptors[] 是一个数组,提供缓冲区页面的描述信息

BufferDescPadded *BufferDescriptors;

typedef union BufferDescPadded
{
	BufferDesc	bufferdesc;
	char		pad[BUFFERDESC_PAD_TO_SIZE];
} BufferDescPadded;

BufferDesc 结构体

  BufferDesc 结构体,它是 PostgreSQL用于描述共享缓冲区中一个缓冲块(buffer block)的状态和属性的数据结构。共享缓冲区是 PostgreSQL 用于缓存磁盘上数据页的内存区域,以减少对磁盘的访问次数和提高数据库性能。以下是对 BufferDesc 结构体中每个字段的详细解释:(路径:src\include\storage\buf_internals.h

typedef struct BufferDesc
{
    BufferTag	tag;			/* 缓冲块中包含的页面的ID,用于唯一标识一个数据库页面 */
    int			buf_id;			/* 缓冲块的索引号,从0开始计数 */

    /* 标签的状态,包含标志位、引用计数和使用计数 */
    pg_atomic_uint32 state;      

    int			wait_backend_pid;	/* 等待该缓冲块引用计数变为0的后台进程PID */
    int			freeNext;		/* 在空闲链表中的下一个缓冲块的链接 */

    LWLock		content_lock;	/* 用于锁定对缓冲块内容的访问的轻量级锁 */
} BufferDesc;

  其中,pg_atomic_uint32 state 是一个原子变量,存储了缓冲块的状态,包括但不限于是否被修改(脏页)是否被固定(引用计数)以及访问频率(使用计数)pg_atomic_uint32 结构体是 PostgreSQL 中用于实现跨平台原子操作的数据结构。原子操作是指在多线程环境下,能够保证被执行的操作在开始到结束期间不会被其他线程中断的操作。在数据库系统中,这种操作对于维护数据的一致性和完整性至关重要。

typedef struct pg_atomic_uint32
{
    /* 根据 pg_atomic_flag 的定义,这里的注释说明了为何需要这样的定义 */
#if defined(__hppa) || defined(__hppa__)	/* 针对 HP PA-RISC 架构,适用于 GCC 和 HP 编译器 */
    int			sema[4];        /* 使用数组提供足够的空间来实现原子操作的信号量 */
#else
    int			sema;           /* 在其他平台上,一个 int 类型足以作为原子操作的信号量 */
#endif
    volatile uint32 value;       /* 实际存储的无符号32位整数值,声明为 volatile 以防止编译器优化导致的问题 */
} pg_atomic_uint32;

pg_atomic_uint32

  BufferDesc 结构体中的 state 字段是一个非常关键的部分,用于存储缓冲块(Buffer Block)的当前状态信息。这个字段是 pg_atomic_uint32 类型,表示它是一个原子的无符号 32 位整数。在 PostgreSQL 的多线程环境中,原子操作确保了在读写这个字段时的线程安全。
  state 字段中包含的信息非常丰富,它通过位掩码的方式存储了多个状态标志(flags),如下表所示:

状态描述
BM_LOCKED表明缓冲区当前被锁定,防止其他进程访问,以确保数据的一致性。
BM_DIRTY表示缓冲区中的数据已被修改(脏数据),需要在某个时刻写回(同步)到磁盘上。
BM_VALID指示缓冲区中的数据是最新的,并且可以被数据库进程安全地读取。
BM_TAG_VALID表示缓冲区的标签(BufferTag,包含表空间、数据库、关系和块号等信息)是有效的,说明缓冲区已经被分配给特定的数据页。
BM_IO_IN_PROGRESS标记当前缓冲区正在进行 I/O 操作,如读取或写入磁盘。
BM_IO_ERROR表示最近一次对该缓冲区的 I/O 操作发生了错误。
BM_JUST_DIRTIED表示缓冲区在最近的写操作开始后被修改过。
BM_PIN_COUNT_WAITER存在进程正在等待缓冲区的固定计数(pin count)变为 0,以便它们可以对缓冲区进行操作。
BM_CHECKPOINT_NEEDED缓冲区需要在下一个检查点(Checkpoint)被写回磁盘,以确保数据的持久性。
BM_PERMANENT表示缓冲区包含的是持久化存储的数据,不是临时或未记录日志的数据。

结论:通过 BufferDesc 结构体中的 state 字段来查看缓冲区中每个缓冲块的状态。

怎么快速找到缓冲区的空页?

  在 PostgreSQL 中,快速找到缓冲区的空页(即未被使用的缓冲槽)依赖于缓冲区管理策略,特别是空闲缓冲槽的管理机制。PostgreSQL 使用几种策略来跟踪和回收空闲的缓冲槽,以便它们可以被重新利用来存储新的数据页。以下是一些关键的机制:

  1. 缓冲区替换策略
      PostgreSQL 使用的一种常见缓冲区替换策略是时钟算法(一种近似的 LRU 算法)。在这种策略中,每个缓冲槽都关联一个使用标记,当缓冲槽被访问时,该标记被设置。时钟算法周期性地扫描缓冲槽,并根据使用标记来选择被替换的缓冲槽。未被使用(即空页)的缓冲槽会优先被选中用于存储新的数据页。
  2. 空闲列表
      PostgreSQL 维护一个空闲列表,用于快速查找可用的缓冲槽。当缓冲槽中的数据被写回磁盘并且不再被任何事务引用时,这个缓冲槽就会被添加到空闲列表中。需要新的缓冲槽来存储数据页时,系统会首先检查空闲列表。
  3. 缓冲区描述符
      每个缓冲槽都有一个对应的 BufferDesc 结构体,其中包含了缓冲槽的状态信息。通过检查这些描述符的状态字段,系统可以确定哪些缓冲槽当前未被使用
  4. 后台写回进程(Checkpointer 和 Background Writer)
      PostgreSQL 的后台写回进程定期将脏页修改过的缓冲槽写回磁盘一旦数据被写回,相关的缓冲槽就可以被标记为可用,从而增加了空闲缓冲槽的数量

StrategyGetBuffer 函数

  StrategyGetBuffer 函数PostgreSQL 的缓冲管理策略中选择一个用于 BufferAlloc() 的候选缓冲区。函数首先尝试使用给定的缓冲访问策略(如果有)来选择缓冲区。如果没有策略策略没有选中任何缓冲区,它会尝试从空闲列表中获取一个缓冲区。如果空闲列表中没有可用的缓冲区,函数将使用时钟扫描算法来找到一个未被锁定的缓冲区。该函数确保返回的缓冲区在返回时保持锁定状态,以防止在缓冲区被锁定之前其他进程锁定它。这个过程是 PostgreSQL 管理其共享缓冲区,特别是在高并发环境下,确保数据的一致性和访问效率的关键部分。函数源码如下所示:(路径:src\backend\storage\buffer\freelist.c

/*
 * StrategyGetBuffer
 *
 *	由缓冲管理器调用,以获取用于BufferAlloc()的下一个候选缓冲区。
 *	BufferAlloc()的唯一硬性要求是所选的缓冲区当前不能被任何人锁定。
 *
 *	strategy 是一个 BufferAccessStrategy 对象,或者为 NULL 表示默认策略。
 *
 *	为了确保在我们锁定缓冲区之前没有其他人能够锁定它,我们必须
 *	在返回缓冲区时保持缓冲区头部的自旋锁(spinlock)。
 */
BufferDesc *
StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state)
{
	BufferDesc *buf;
	int			bgwprocno;
	int			trycounter;
	uint32		local_buf_state;	/* 避免重复引用和解引用 */

	/*
	 * 如果提供了策略对象,看它是否能选择一个缓冲区。我们
	 * 假设策略对象不需要 buffer_strategy_lock。
	 */
	if (strategy != NULL)
	{
		buf = GetBufferFromRing(strategy, buf_state);
		if (buf != NULL)
			return buf;
	}

	/*
	 * 如果需要,我们需要唤醒后台写入器(bgwriter)。因为我们不想依赖
	 * 自旋锁来完成这个工作,所以我们强制先从共享内存中读取一次,然后
	 * 基于该值设置闩锁(latch)。我们需要这样做是因为否则 bgprocno 在我们
	 * 检查时可能会被重置,因为编译器可能会重新从内存中读取。
	 */
	bgwprocno = INT_ACCESS_ONCE(StrategyControl->bgwprocno);
	if (bgwprocno != -1)
	{
		/* 首先重置 bgwprocno,然后再设置闩锁 */
		StrategyControl->bgwprocno = -1;

		/*
		 * 这里没有获取 ProcArrayLock,这稍微有点问题。实际上这是安全的,
		 * 因为 procLatch 永远不会被释放,所以我们只是可能设置了错误进程的
		 * 闩锁(或没有设置任何进程的闩锁)。
		 */
		SetLatch(&ProcGlobal->allProcs[bgwprocno].procLatch);
	}

	/*
	 * 我们计算缓冲区分配请求的次数,以便后台写入器可以估计缓冲区消耗的速率。
	 * 注意,通过策略对象回收的缓冲区在这里故意不被计数。
	 */
	pg_atomic_fetch_add_u32(&StrategyControl->numBufferAllocs, 1);

	/*
	 * 首先在不获取锁的情况下检查是否有缓冲区在空闲列表中。因为我们在大多数
	 * StrategyGetBuffer()调用中不需要自旋锁,所以如果在这里无用地获取它会很遗憾 -
	 * 在大多数情况下是不必要的。这显然留下了一个竞争条件,即一个缓冲区被放入空闲列表
	 * 但我们还没看到存储 - 但这是相当无害的,它将在下一次缓冲区获取时被使用。
	 *
	 * 如果空闲列表中有缓冲区,获取自旋锁以从空闲列表中弹出一个缓冲区。
	 * 然后检查该缓冲区是否可用,如果不可用则重试。
	 *
	 * 注意 freeNext 字段被认为是由 buffer_strategy_lock 而不是
	 * 个别缓冲区自旋锁保护的,所以在不持有自旋锁的情况下操作它们是可以的。
	 */
	if (StrategyControl->firstFreeBuffer >= 0)
	{
		while (true)
		{
			/* 获取自旋锁以从空闲列表中移除元素 */
			SpinLockAcquire(&StrategyControl->buffer_strategy_lock);

			if (StrategyControl->firstFreeBuffer < 0)
			{
				SpinLockRelease(&StrategyControl->buffer_strategy_lock);
				break;
			}

			buf = GetBufferDescriptor(StrategyControl->firstFreeBuffer);
			Assert(buf->freeNext != FREENEXT_NOT_IN_LIST);

			/* 无条件地从空闲列表中移除缓冲区 */
			StrategyControl->firstFreeBuffer = buf->freeNext;
			buf->freeNext = FREENEXT_NOT_IN_LIST;

			/*
			 * 释放锁,以便其他人可以在我们检查这个缓冲区时访问空闲列表。
			 */
			SpinLockRelease(&StrategyControl->buffer_strategy_lock);

			/*
			 * 如果缓冲区被锁定或有非零的使用计数,我们不能使用它;
			 * 丢弃它并重试。(如果VACUUM将一个有效的缓冲区放入空闲列表,
			 * 然后其他人在我们到达之前使用它,这种情况可能发生。自 8.3 版本以来,
			 * 这种情况可能完全不会发生,但我们还是应该检查。)
			 */
			local_buf_state = LockBufHdr(buf);
			if (BUF_STATE_GET_REFCOUNT(local_buf_state) == 0
				&& BUF_STATE_GET_USAGECOUNT(local_buf_state) == 0)
			{
				if (strategy != NULL)
					AddBufferToRing(strategy, buf);
				*buf_state = local_buf_state;
				return buf;
			}
			UnlockBufHdr(buf, local_buf_state);

		}
	}

	/* 如果空闲列表为空,运行“时钟扫描”算法 */
	trycounter = NBuffers;
	for (;;)
	{
	    /* 使用时钟扫描的当前位置获取缓冲区描述符 */
	    buf = GetBufferDescriptor(ClockSweepTick());
	
	    /*
	     * 如果缓冲区被固定(pinned)或有非零的使用计数,我们不能使用它;
	     * 减少使用计数(除非被固定)并继续扫描。
	     */
	    local_buf_state = LockBufHdr(buf);
	
	    if (BUF_STATE_GET_REFCOUNT(local_buf_state) == 0)
	    {
	        if (BUF_STATE_GET_USAGECOUNT(local_buf_state) != 0)
	        {
	            /* 减少缓冲区的使用计数并重置尝试计数器 */
	            local_buf_state -= BUF_USAGECOUNT_ONE;
	
	            trycounter = NBuffers;
	        }
	        else
	        {
	            /* 找到了一个可用的缓冲区 */
	            if (strategy != NULL)
	                AddBufferToRing(strategy, buf);
	            *buf_state = local_buf_state;
	            return buf;
	        }
	    }
	    else if (--trycounter == 0)
	    {
	        /*
	         * 我们已经扫描了所有的缓冲区而没有做任何状态改变,
	         * 所以所有的缓冲区都被固定了(或在我们查看它们时是这样)。
	         * 我们可以希望有人最终会释放一个缓冲区,但失败可能比陷入
	         * 无限循环更好。
	         */
	        UnlockBufHdr(buf, local_buf_state);
	        elog(ERROR, "no unpinned buffers available");
	    }
	    UnlockBufHdr(buf, local_buf_state);
	}
}

  其中,GetBufferDescriptor(StrategyControl->firstFreeBuffer) 根据缓冲区 ID(StrategyControl->firstFreeBuffer)获取对应的缓冲区描述符 (BufferDesc)。这个函数根据缓冲区的索引号(在这个上下文中是 StrategyControl->firstFreeBuffer,表示空闲列表中第一个空闲缓冲区的索引)来检索对应的 BufferDesc 结构体BufferDesc 结构体包含了缓冲区的元数据,如缓冲区的状态锁信息所持数据页的标识等信息。

BufferStrategyControl 结构体

  BufferStrategyControl 结构体是 PostgreSQL 中用于控制共享缓冲区空闲列表的关键数据结构。这个结构体包含了管理缓冲区分配策略所需的控制信息和统计数据。它在 PostgreSQL 的缓冲区管理系统中扮演着至关重要的角色。此结构体封装了控制和监视共享缓冲区空闲列表的各种元素和统计信息,使得 PostgreSQL 能够有效地管理其缓冲区资源优化数据的读写操作,并提高整体数据库性能。函数源码如下所示:(路径:src\backend\storage\buffer\freelist.c

/*
 * 共享的空闲列表控制信息。
 */
typedef struct
{
    /* 自旋锁:保护下面列出的值 */
    slock_t		buffer_strategy_lock;

    /*
     * 时钟扫描的指针:下一个考虑用于替换的缓冲区的索引。注意这不是一个具体的缓冲区 -
     * 我们只是不断增加这个值。因此,要得到一个实际的缓冲区,需要对其使用 NBuffers 取模。
     */
    pg_atomic_uint32 nextVictimBuffer;

    /* 空闲列表的头部索引,指向未使用的缓冲区列表的开始 */
    int			firstFreeBuffer;
    /* 空闲列表的尾部索引,指向未使用的缓冲区列表的结束 */

    /*
     * 注意:当 firstFreeBuffer 为 -1 时(即列表为空),lastFreeBuffer 是未定义的
     */

    /*
     * 统计数据。这些计数器的宽度应该足够宽,以确保它们不会在一个后台写入器周期内溢出。
     */
    uint32		completePasses; /* 时钟扫描的完整循环次数 */
    pg_atomic_uint32 numBufferAllocs;	/* 自上次重置以来分配的缓冲区数量 */

    /*
     * 在发生活动时需要通知的后台工作进程号,或者如果没有则为 -1。参见 StrategyNotifyBgWriter。
     */
    int			bgwprocno;
} BufferStrategyControl;

/* 指向共享状态的指针 */
static BufferStrategyControl *StrategyControl = NULL;

如果没有空闲页了,怎么办?

  1. 使用时钟扫描算法寻找替换候选
      当没有空闲页时,PostgreSQL 会运行时钟扫描(Clock Sweep)算法来寻找一个替换候选。这个算法会遍历缓冲区,寻找引用计数为零(即没有被任何查询或事务锁定)的缓冲页。时钟算法还考虑了缓冲页的使用计数(一个衡量页面活跃程度的指标),尽可能地选择最不活跃的页进行替换。(后面有机会再做详细的学习)
  2. 写回脏页
      如果选中的替换候选是脏页(即自上次写入以来已被修改),PostgreSQL 需要先将这些更改写回磁盘。这确保了数据的持久性,并释放了缓冲页以供重新使用。
  3. 强制检查点
      在极端情况下,如果缓冲区压力过大PostgreSQL 可能会提前触发一个检查点Checkpoint)。检查点的过程会将所有脏页写回磁盘,并更新事务日志,从而减少了在故障恢复时需要重放的日志量。通过这种方式,检查点可以间接释放更多的缓冲页。
  4. 调整缓冲区大小和配置
      如果频繁遇到没有空闲页的情况,可能表明共享缓冲区大小配置(shared_buffers 参数)不足以满足当前的工作负载需求。在这种情况下,数据库管理员可能需要考虑增加共享缓冲区的大小或者优化查询以减少对缓冲区的需求。
  5. 后台写入器和检查点进程
      PostgreSQL后台写入器Background Writer进程检查点(Checkpoint)进程也会定期将脏页写回磁盘。这有助于确保有足够的空闲页可用于即将到来的数据库操作。
  6. 错误处理
      如果经过上述所有步骤后,仍然无法找到可用的缓冲页,PostgreSQL 将无法分配新的缓冲页。在极端情况下,这可能导致数据库操作失败,PostgreSQL 会抛出错误。这种情况下,通常需要数据库管理员介入进行进一步的诊断和配置调整。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/374311.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

高速接口PCB布局指南(二)通用高速信号布线

高速接口PCB布局指南&#xff08;二&#xff09;通用高速信号布线 1.PCB材料编织2.高速信号布线长度3.高速信号布线长度匹配4.高速信号参考平面 tips&#xff1a;资料主要来自网络&#xff0c;仅供学习使用。 1.PCB材料编织 在常见的 PCB 材料上为差分信号布线时&#xff0c;…

【Iceberg学习二】Branch和Tag在Iceberg中的应用

Iceberg 表元数据保持一个快照日志&#xff0c;记录了对表所做的更改。快照在 Iceberg 中至关重要&#xff0c;因为它们是读者隔离和时间旅行查询的基础。为了控制元数据大小和存储成本&#xff0c;Iceberg 提供了快照生命周期管理程序&#xff0c;如 expire_snapshots&#xf…

知识图谱问答:构建人机自然交互的桥梁

目录 前言1 基本概念1.1 图灵测试1.2 特定领域的问答系统1.3 知识图谱问答1.4 典型应用与系统 2 智能问答系统分类2.1 问句类型分类2.2 系统来源分类 3 实现知识图谱问答主要技术方法3.1 基于问句模板的方法3.2 基于语义解析的方法3.3 基于检索排序的方法3.4 基于深度学习的方法…

代码随想录算法训练营第十二天 | 239. 滑动窗口最大值,347.前 K 个高频元素 [栈与队列篇]

代码随想录算法训练营第十二天 LeetCode 239. 滑动窗口最大值题目描述思路参考代码总结 LeetCode 347.前 K 个高频元素题目描述思路参考代码 LeetCode 239. 滑动窗口最大值 题目链接&#xff1a;239. 滑动窗口最大值 文章讲解&#xff1a;代码随想录#239. 滑动窗口最大值 视频讲…

现货黄金突破2050美元 后续还会涨吗?

一两个月以前&#xff0c;受美联储降息预期影响&#xff0c;现货黄金价格一度强势上涨&#xff0c;并且刷新历史新高。随后&#xff0c;市场不断消化降息预期&#xff0c;金价逐步回落&#xff0c;盘中一度下探2000大关。在今年的一季度&#xff0c;行情再度发生变化&#xff0…

华为5G沸沸扬扬!那你知道三防平板网络是什么类型呢!

近日&#xff0c;华为在5G的事件在热搜上可是着实的火了一把啊&#xff01;让小编想起一款来自亿道信息EM-I22K-5G的一款三防平板产品&#xff0c;你知道是什么网络类型的呢&#xff1f; EM-I22K-5G 不知道&#xff1f;没关系呀&#xff01;小编可以为你普及亿道信息EM-I22K-5G…

寒假作业-day5

TCP和UDP区别 TCP ----稳定 1、提供面向连接的&#xff0c;可靠的数据传输服务&#xff1b; 2、传输过程中&#xff0c;数据无误、数据无丢失、数据无失序、数据无重复&#xff1b; 3、数据传输效率低&#xff0c;耗费资源多&#xff1b; 4、数据收发是不同步的&#xff1b; UD…

软件价值8-站点连通性检查

站点连通性检查&#xff0c;即看网站是否能访问得通&#xff0c;实用价值不大&#xff0c;不过用来作软件应用入门还不错。 代码&#xff1a; import urllib.request import tkinter as tkdef test_connectivity():window tk.Tk()window.geometry(600x400)window.resizable(F…

性能实测:分布式存储 ZBS 与集中式存储 HDS 在 Oracle 数据库场景表现如何

作者&#xff1a;深耕行业的 SmartX 金融团队 金鑫 在金融客户的基础架构环境中&#xff0c;HDS 是一种被广泛使用的存储解决方案。作为集中式存储的代表之一&#xff0c;HDS 拥有高性能、高可用性和可扩展性的企业级存储特点&#xff0c;适用于实时数据处理、虚拟化和灾难备份…

阿里云游戏服务器一年费用多少?

阿里云游戏服务器租用价格表&#xff1a;4核16G服务器26元1个月、146元半年&#xff0c;游戏专业服务器8核32G配置90元一个月、271元3个月&#xff0c;阿里云服务器网aliyunfuwuqi.com分享阿里云游戏专用服务器详细配置和精准报价&#xff1a; 阿里云游戏服务器租用价格表 阿…

论文阅读-Transformer-based language models for software vulnerability detection

「分享了一批文献给你&#xff0c;请您通过浏览器打开 https://www.ivysci.com/web/share/biblios/D2xqz52xQJ4RKceFXAFaDU/ 您还可以一键导入到 ivySCI 文献管理软件阅读&#xff0c;并在论文中引用 」 本文主旨&#xff1a;本文提出了一个系统的框架来利用基于Transformer的语…

JavaScript常用技巧专题七

文章目录 一、提炼函数1.1、好处1.2、示例 二、合并重复的条件片段三、把条件分支语句提炼成函数四、合理使用循环五、提前让函数退出代替嵌套条件分支六、传递对象参数代替过长的参数列表七、少用三目运算符八、合理使用链式调用8.1、优点8.2、缺点 九、纯函数9.1、不属于纯函…

2-2 动手学深度学习v2-损失函数-笔记

损失函数&#xff0c;用来衡量预测值和真实值之间的区别。是机器学习里面一个非常重要的概念。 三个常用的损失函数 L2 loss、L1 loss、Huber’s Robust loss 均方损失 L2 Loss l ( y , y ′ ) 1 2 ( y − y ′ ) 2 l(y,y^{\prime})\frac{1}{2}(y-y^{\prime})^{2} l(y,y′)21…

火山引擎ByteHouse:如何为OLAP设计高性能向量检索能力?

更多技术交流、求职机会&#xff0c;欢迎关注字节跳动数据平台微信公众号&#xff0c;回复【1】进入官方交流群 背景 随着 LLM 技术应用及落地&#xff0c;数据库需要提高向量分析以及 AI 支持能力&#xff0c;向量数据库及向量检索等能力“异军突起”&#xff0c;迎来业界持续…

第七届西湖论剑·中国杭州网络安全技能大赛 AI 回声海螺 WP

第七届西湖论剑中国杭州网络安全技能大赛-AI-回声海螺 开题&#xff0c;提示输入密码给FLAG。 这个回声海螺应该是个AI&#xff0c;就是复读机&#xff0c;应该是想办法从中骗出密码。 感觉这题不像是AI&#xff0c;也没用啥模型&#xff0c;应该是WEB。或者是说类似于AI的提示…

第八讲:详解第1套真题

第八讲:详解第1套真题 基本编程题【15 分】简单应用题【25 分】综合应用题【20 分】**问题一**【5分】问题二【5 分】问题二【10 分】小结基本编程题【15 分】 考生文件夹下存在一个文件 PY101.py,请写代码替换横线,不修改其他代码,实现以下功能:【5 分】键盘输入正整数 n…

【从0上手Cornerstone3D】如何使用CornerstoneTools中的工具之同步器

同步器&#xff08;Synchronizers&#xff09;可以使多个视图同步响应同一个工具的操作&#xff0c;例如我们在MPR视图下&#xff0c;同步操作三个视图的缩放程度、windowLevel等等 一个同步器必须需要以下几个部分才可以执行 一个监听事件&#xff08;什么情况下触发同步&…

【日志记录】——单片机可执行文件合并

一&#xff1a;需求场景 现在有一片单片机&#xff0c;执行程序包括自定义boot和应用程序app, 在将打包好的固件给到生产时有以下问题&#xff0c;由于要通过jlink烧录boot&#xff0c;然后上电启动boot&#xff0c;通过boot烧录初始化程序&#xff0c;过程过于复杂&#xff0…

蓝桥杯省赛无忧 课件125 线段树-标记永久化

前置知识 线段树 01 什么是标记永久化 02 如何标记永久化 03 标记永久化的优势

【GAMES101】Lecture 17 材质

目录 材质 漫反射 镜面反射 折射-Snell’s Law Fresnel Reflection / Term&#xff08;菲涅耳项&#xff09; 微表面模型 各向同性与各向异性 BRDF的性质 测量BRDF 材质 渲染方程中的BRDF描述了物体是如何与光线作用的&#xff0c;而物体的材质决定了它看起来是怎么样…