关于资源所有者的说明

概述

资源所有者对象是为了简化查询相关资源(如缓冲区引用和表锁)管理而发明的一个概念。这些资源需要以可靠的方式进行跟踪,以确保它们在查询结束时被释放,即使查询因错误而失败也是如此。我们不期望整个执行器都具备完美无缺的数据结构,而是将这些资源的跟踪工作集中到一个模块中。

资源所有者 API 的设计以我们的内存上下文 API 为蓝本,后者在防止内存泄漏方面已被证明非常灵活且成功。特别地,我们允许资源所有者拥有子资源所有者对象,从而可以形成资源所有者森林;释放一个父资源所有者时,也会作用于其所有直接和间接的子资源所有者。

(有人可能会考虑将资源所有者和内存上下文统一为单一对象类型,但它们的使用模式差异较大,这样做可能并没有实际帮助。)

资源所有者的创建与层次

我们为每个事务或子事务创建一个资源所有者,同时也为每个 Portal 创建一个。在 Portal 执行期间,全局变量 CurrentResourceOwner 指向 Portal 的资源所有者。这使得诸如 ReadBufferLockAcquire 等操作将所获取资源的所有权记录在该资源所有者对象中。

当 Portal 关闭时,任何剩余的资源(通常只有锁)将成为当前事务的责任。这是通过使 Portal 的资源所有者成为当前事务资源所有者的子对象来实现的。resowner.c 在释放子对象时会自动将资源转移给父对象。类似地,子事务的资源所有者是其直接父事务资源所有者的子对象。

我们既需要事务相关的资源所有者,也需要 Portal 相关的资源所有者,因为事务可能会在没有关联 Portal 存在时就发起需要资源的操作(如查询解析)。


API 概述

对资源所有者的基本操作包括:

  • 创建一个资源所有者
  • 将某个资源与资源所有者关联或取消关联
  • 释放资源所有者的资产(释放所有拥有的资源,但不释放所有者对象本身)
  • 删除一个资源所有者(包括子所有者对象);所有资源必须事先已释放

此 API 直接支持 src/backend/utils/resowner/resowner.cResourceOwnerData 结构体定义中列出的资源类型。其他对象可以通过在其结构中记录所属资源所有者的地址来与资源所有者关联。我们为其他模块提供了 API,使其能在资源所有者释放期间获得控制权,以便扫描自己的数据结构来找到需要删除的对象。

锁的特殊处理

锁的处理比较特殊,因为在非错误情况下,锁应该保持到事务结束,即使它最初是由子事务或 Portal 获取的。因此,当对子资源所有者执行"释放"操作时,如果 isCommit 为真,锁的所有权会转移给父资源所有者,而不是真正释放锁。

CurrentResourceOwner 全局变量

每当我们处于事务内部时,全局变量 CurrentResourceOwner 指示应该将获取资源的所有权分配给哪个资源所有者。但请注意,当不在任何事务内部(或处于失败的事务中)时,CurrentResourceOwner 为 NULL。在这种情况下,获取查询生命周期资源是无效的。

当取消缓冲区引用、释放锁或缓存引用时,CurrentResourceOwner 必须指向获取该缓冲区、锁或缓存引用时所处的资源所有者。如果增加额外的簿记工作,放宽这一限制是可能的,但目前看来没有这个必要。


  • 以上为resowner中readme的翻译
typedef struct ResourceOwnerData
{
	ResourceOwner parent;		// toplevel owner的话为NULL
	ResourceOwner firstchild;	/* head of linked list of children */
	ResourceOwner nextchild;	/* next child of same parent */
	const char *name;			/* name (just for debugging) */

	/* We have built-in support for remembering: */
    // RESOURCE_RELEASE_BEFORE_LOCKS 释放对其他后端可见的资源 
    // 从其他后端的视角,锁释放的那一刻 = 事务完全完成 所有对其他后端有影响的操作必须在释放锁之前完成  
	ResourceArray bufferarr;	// 持有的buffer
	ResourceArray bufferioarr;	// 进行中的bufferio
	ResourceArray relrefarr;	// relcache 引用
    ResourceArray dsmarr;		// dynamic shmem segments */
	ResourceArray jitarr;		// JIT contexts */
	ResourceArray cryptohasharr;	/* cryptohash contexts */
	ResourceArray hmacarr;		/* HMAC contexts */

    // RESOURCE_RELEASE_AFTER_LOCKS 释放仅自己可见的资源 对其他backend无影响
	ResourceArray catlistrefarr;	// catcache-list pins
   	ResourceArray catrefarr;	// catcache 引用
	ResourceArray planrefarr;	// plancache 引用
	ResourceArray tupdescarr;	// tupdesc 引用, 在使用type cache中的tupdesc时 需要增加和减少引用
	ResourceArray snapshotarr;	// snapshot 引用
	ResourceArray filearr;		// open temporary files

	/* We can remember up to MAX_RESOWNER_LOCKS references to local locks. */
    // RESOURCE_RELEASE_LOCKS阶段释放锁,或者将锁传递给父resowner
	int			nlocks;			/* number of owned locks */
	LOCALLOCK  *locks[MAX_RESOWNER_LOCKS];	/* list of owned locks */
}			ResourceOwnerData;

// 在引用对应的缓存资源时,需要将对应资源加入数组,释放时从数组中去除
typedef struct ResourceArray
{
	Datum	   *itemsarr;		// 数组指针,存放缓存资源的指针或者描述符,是一个动态数组,根据capacity进行扩容
	Datum		invalidval;		// 数组中值==invalidval时无效,不同的资源无效值可能不同,比如filearr的无效值为-1,planrefarr等指针无效值为PointerGetDatum(NULL)
	uint32		capacity;		// allocated length of itemsarr[] */
	uint32		nitems;			// how many items are stored in items array */
	uint32		maxitems;		// current limit on nitems before enlarging */
	uint32		lastidx;		// getany 返回的下标
} ResourceArray;

创建resourceowner

什么时候创建resourceowner?一般在事务开启,以及portal执行时创建resourceowner,用来监控和管理资源,目前会创建以下resourceowner:TopTransaction(事务开启时创建),SubTransaction(子事务开启时创建),Portal(sql执行时创建,portal结束时释放)

  • resourceowner的设计类似内存上下文,也是以树形式管理
    在这里插入图片描述

其他场景可能需要单独管理某些资源时也可以创建resowner,防止资源泄漏,base backup(backup时创建,保存BufFile的引用),AuxiliaryProcess(辅助进程创建),PL/pgSQL simple expressions,PL/pgSQL procedure resources,PL/pgSQL DO block simple expressions(存储过程执行前创建,用来保存执行计划)

新增引用的资源

// 数组扩容
static void
ResourceArrayEnlarge(ResourceArray *resarr)

// 数组中添加新的引用(不会去重,如果是同一个引用,也会直接添加到数组中,没有及时释放可能会有内存堆积,清理引用时会根据数组中的个数减少refcount)

static void
ResourceArrayAdd(ResourceArray *resarr, Datum value)

    
// 每个资源对象 会调用上述两个函数封装对应的函数
void
ResourceOwnerEnlargeBuffers(ResourceOwner owner)
{
	/* We used to allow pinning buffers without a resowner, but no more */
	Assert(owner != NULL);
	ResourceArrayEnlarge(&(owner->bufferarr));
}

// 调用ResourceOwnerRemember前 最好应该调用ResourceOwnerEnlarge扩容数组,Remember不会动态扩容
void
ResourceOwnerRememberBuffer(ResourceOwner owner, Buffer buffer)
{
	ResourceArrayAdd(&(owner->bufferarr), BufferGetDatum(buffer));
}


// ResourceArrayAdd在容量小于64时,会直接放到数组里,容量大于64时,根据hash存放,加速查找的速度
┌─────────────────────────────────────────────────────────────────┐
│              ResourceArray 自适应存储策略                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   capacity <= 64 (RESARRAY_MAX_ARRAY)                          │
│   ┌─────────────────────────────────────────┐                  │
│   │         线性数组模式           │                  │
│   │  ┌───┬───┬───┬───┬───┬───┬───┬───┐     │                  │
│   │  │ 01234...│ n │   │     │                  │
│   │  └───┴───┴───┴───┴───┴───┴───┴───┘     │                  │
│   │         简单追加,O(1)                  │                  │
│   │         maxitems = capacity             │                  │
│   └─────────────────────────────────────────┘                  │
│                                                                 │
│   capacity > 64                                                 │
│   ┌─────────────────────────────────────────┐                  │
│   │       开放寻址哈希表模式   │                  │
│   │  ┌───┬───┬───┬───┬───┬───┬───┬───┐     │                  │
│   │  │   │ 5 │   │ 2 │   │ 8 │   │   │     │                  │
│   │  └───┴───┴───┴───┴───┴───┴───┴───┘     │                  │
│   │  哈希定位 + 线性探测,平均 O(1)         │                  │
│   │  maxitems = capacity * 3/4 (负载因子75%)│                  │
│   └─────────────────────────────────────────┘                  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

去除引用的资源

除了锁资源外,其他引用的资源都应在对应的生命周期内被释放

// 成功return true,失败返回false,resowner并不实际释放对象的资源,只作为跟踪对象资源使用,remove只是将对象从数组中去除
static bool
ResourceArrayRemove(ResourceArray *resarr, Datum value)

// 每个资源封装对应的forget函数
void
ResourceOwnerForgetBuffer(ResourceOwner owner, Buffer buffer)
{
	if (!ResourceArrayRemove(&(owner->bufferarr), BufferGetDatum(buffer)))
		elog(ERROR, "buffer %d is not owned by resource owner %s",
			 buffer, owner->name);
}
  • 使用场景如下: syscache的引用和释放
static pg_noinline HeapTuple
SearchCatCacheMiss(CatCache *cache,
				   int nkeys,
				   uint32 hashValue,
				   Index hashIndex,
				   Datum v1,
				   Datum v2,
				   Datum v3,
				   Datum v4)
{
//....
		while (HeapTupleIsValid(ntp = systable_getnext(scandesc)))
		{
			ct = CatalogCacheCreateEntry(cache, ntp, scandesc, NULL,
										 hashValue, hashIndex);
			/* upon failure, we must start the scan over */
			if (ct == NULL)
			{
				stale = true;
				break;
			}
			// 扩容对应的catrefarr数组
			ResourceOwnerEnlargeCatCacheRefs(CurrentResourceOwner);
            // 增加refcount的引用计数
			ct->refcount++;
            // tuple指针作为对象存入数组
			ResourceOwnerRememberCatCacheRef(CurrentResourceOwner, &ct->tuple);
			break;				/* assume only one match */
		}

		systable_endscan(scandesc);
	} while (stale);
//...
}


void
ReleaseCatCache(HeapTuple tuple)
{
    // 根据偏移找到CatCTup头
	CatCTup    *ct = (CatCTup *) (((char *) tuple) -
								  offsetof(CatCTup, tuple));

	/* Safety checks to ensure we were handed a cache entry */
	Assert(ct->ct_magic == CT_MAGIC);
	Assert(ct->refcount > 0);
	// 减少引用计数, refcount=0时可以根据淘汰策略淘汰
	ct->refcount--;
    // 数组中去除引用对象
	ResourceOwnerForgetCatCacheRef(CurrentResourceOwner, &ct->tuple);

	if (
#ifndef CATCACHE_FORCE_RELEASE
		ct->dead &&
#endif
		ct->refcount == 0 &&
		(ct->c_list == NULL || ct->c_list->refcount == 0))
		CatCacheRemoveCTup(ct->my_cache, ct);
}

//SearchSysCache1 ReleaseSysCache为SearchCatCacheMiss ReleaseCatCache的宏
indexTuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(indexoid));
if (!HeapTupleIsValid(indexTuple))	/* should not happen */
	elog(ERROR, "cache lookup failed for index %u", indexoid);

ReleaseSysCache(indexTuple);

在这里插入图片描述

删除和清理resourceowner

// 遍历删除resowner和他的子resowner,如果有父reswoner,将其从父节点的指针置为NULL,调用delete前最好确保上面的引用资源都被release掉,否则会有泄漏
void
ResourceOwnerDelete(ResourceOwner owner)
    
// 清理当前resowner上的资源
// 注册回调函数,在ResourceOwnerReleaseInternal最后调用,ResourceOwnerReleaseInternal有3个阶段,每个阶段都会调用回调函数
void
RegisterResourceReleaseCallback(ResourceReleaseCallback callback, void *arg)
// 取消注册的回调函数
void
UnregisterResourceReleaseCallback(ResourceReleaseCallback callback, void *arg)

void
ResourceOwnerRelease(ResourceOwner owner,
					 ResourceReleasePhase phase,
					 bool isCommit,
					 bool isTopLevel)
{
	/* There's not currently any setup needed before recursing */
	ResourceOwnerReleaseInternal(owner, phase, isCommit, isTopLevel);
}

// 清理当前resowner上的资源,commit时,除了锁资源外(锁资源跟随事务的生命周期,因为在非错误情况下,应该随顶层事务释放),其他资源都应该在执行阶段被清理,否则会报warning(bufferio会报panic)
// ResourceReleasePhase有3个阶段
// RESOURCE_RELEASE_BEFORE_LOCKS 释放lock前,释放其他backend可见的资源
// RESOURCE_RELEASE_AFTER_LOCKS 释放lock后,释放只有当前backend可见的资源,对其他backend无影响
// RESOURCE_RELEASE_LOCKS 释放lock,只有为toplevel并且为TopTransactionResourceOwner才释放锁,非toplevel commit时将锁资源转移给parent,非toplevel rollback时可以释放lock
static void
ResourceOwnerReleaseInternal(ResourceOwner owner,
							 ResourceReleasePhase phase,
							 bool isCommit,
							 bool isTopLevel)
{
	//...
	// 遍历清理child resowner
	for (child = owner->firstchild; child != NULL; child = child->nextchild)
		ResourceOwnerReleaseInternal(child, phase, isCommit, isTopLevel);

	//切换CurrentResourceOwner为指定owner
	save = CurrentResourceOwner;
	CurrentResourceOwner = owner;
	// RESOURCE_RELEASE_BEFORE_LOCKS 释放lock前,释放其他backend可见的资源
	if (phase == RESOURCE_RELEASE_BEFORE_LOCKS)
	{
		// 遍历array,如果有未释放的bufferio,commit时报错,非commit释放资源
		while (ResourceArrayGetAny(&(owner->bufferioarr), &foundres))
		{
			Buffer		res = DatumGetBuffer(foundres);

			if (isCommit)
				elog(PANIC, "lost track of buffer IO on buffer %d", res);
			AbortBufferIO(res);
		}

		// ...释放资源,如果为commit会报warning
	}
    // RESOURCE_RELEASE_LOCKS 释放lock
	else if (phase == RESOURCE_RELEASE_LOCKS)
	{
		if (isTopLevel)
		{
			// 只有为toplevel并且为TopTransactionResourceOwner才释放锁
			if (owner == TopTransactionResourceOwner)
			{
				ProcReleaseLocks(isCommit);
				ReleasePredicateLocks(isCommit, false);
			}
		}
		else
		{
			// 非toplevel commit时将锁资源转移给parent,rollback时可以释放lock
			LOCALLOCK **locks;
			int			nlocks;

			Assert(owner->parent != NULL);

			/*
			 * Pass the list of locks owned by this resource owner to the lock
			 * manager, unless it has overflowed.
			 */
			if (owner->nlocks > MAX_RESOWNER_LOCKS)
			{
				locks = NULL;
				nlocks = 0;
			}
			else
			{
				locks = owner->locks;
				nlocks = owner->nlocks;
			}

			if (isCommit)
				LockReassignCurrentOwner(locks, nlocks);
			else
				LockReleaseCurrentOwner(locks, nlocks);
		}
	}
	// RESOURCE_RELEASE_AFTER_LOCKS 释放lock后,释放只有当前backend可见的资源,对其他backend无影响
	else if (phase == RESOURCE_RELEASE_AFTER_LOCKS)
	{
		//... 释放资源,如果为commit会报warning
	}

	// 调用注册的回调函数
	for (item = ResourceRelease_callbacks; item; item = next)
	{
		/* allow callbacks to unregister themselves when called */
		next = item->next;
		item->callback(phase, isCommit, isTopLevel, item->arg);
	}

	CurrentResourceOwner = save;
}

具体调用如下

// 存储过程
Datum
plpgsql_call_handler(PG_FUNCTION_ARGS)
{
    //...
    procedure_resowner =
		(nonatomic && func->requires_procedure_resowner) ?
		ResourceOwnerCreate(NULL, "PL/pgSQL procedure resources") : NULL;

//...
	PG_FINALLY();
	{
		/* Decrement use-count, restore cur_estate */
		func->use_count--;
		func->cur_estate = save_cur_estate;

		// release存储过程保存的plancache后,删除procedure_resowner,因为procedure_resowner上只保存plancache 不需要清理其他资源
		if (procedure_resowner)
		{
			ResourceOwnerReleaseAllPlanCacheRefs(procedure_resowner);
			ResourceOwnerDelete(procedure_resowner);
		}
	}
//...
}


// 流复制中 WAL 发送进程的错误Resource清理函数
void
WalSndResourceCleanup(bool isCommit)
{
	ResourceOwner resowner;

	if (CurrentResourceOwner == NULL)
		return;

	/*
	 * Deleting CurrentResourceOwner is not allowed, so we must save a pointer
	 * in a local variable and clear it first.
	 */
	resowner = CurrentResourceOwner;
	CurrentResourceOwner = NULL;

	// ResourceOwnerRelease调用时需要区分3个阶段
	ResourceOwnerRelease(resowner,
						 RESOURCE_RELEASE_BEFORE_LOCKS, isCommit, true);
	ResourceOwnerRelease(resowner,
						 RESOURCE_RELEASE_LOCKS, isCommit, true);
	ResourceOwnerRelease(resowner,
						 RESOURCE_RELEASE_AFTER_LOCKS, isCommit, true);
	ResourceOwnerDelete(resowner);
}

// 事务提交
static void
CommitTransaction(void)
{
	// 。。。 

	ResourceOwnerRelease(TopTransactionResourceOwner,
						 RESOURCE_RELEASE_BEFORE_LOCKS,
						 true, true);

	/* Check we've released all buffer pins */
	AtEOXact_Buffers(true);

	/* Clean up the relation cache */
	AtEOXact_RelationCache(true);

	/*
	 * Make catalog changes visible to all backends.  This has to happen after
	 * relcache references are dropped (see comments for
	 * AtEOXact_RelationCache), but before locks are released (if anyone is
	 * waiting for lock on a relation we've modified, we want them to know
	 * about the catalog change before they start using the relation).
	 */
	AtEOXact_Inval(true);

	AtEOXact_MultiXact();

	ResourceOwnerRelease(TopTransactionResourceOwner,
						 RESOURCE_RELEASE_LOCKS,
						 true, true);
	ResourceOwnerRelease(TopTransactionResourceOwner,
						 RESOURCE_RELEASE_AFTER_LOCKS,
						 true, true);

	// ...
	ResourceOwnerDelete(TopTransactionResourceOwner);
	//...
}

关键术语对照表

英文术语 中文翻译 说明
ResourceOwner 资源所有者 管理查询相关资源的对象
buffer pins 缓冲区引用 对数据页的引用计数锁定
MemoryContext 内存上下文 PostgreSQL 的内存管理机制
Portal Portal/游标门户 查询执行的抽象容器
subtransaction 子事务 嵌套事务
isCommit 是否提交 标识操作是提交还是回滚
Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐