引言
在 He3DB 这样的数据库系统中,文件操作不仅频繁而且复杂。操作系统提供的文件描述符(FD)数量是有限的,尤其在高并发和大规模数据库操作中,文件描述符资源可能迅速耗尽。为了应对这一挑战,He3DB 引入了 VFD 机制。VFD 通过抽象操作系统的文件描述符,实现了一个虚拟层,使得系统能够有效管理文件资源,同时确保性能的稳定性。
一、VFD (Virtual File Descriptor)的来源
操作系统的文件描述符是用于表示已经打开的文件的句柄。在 He3DB 中,直接使用操作系统提供的文件描述符存在一些限制,如文件描述符数量的限制,文件频繁打开和关闭带来的性能开销等。为了克服这些问题,He3DB 设计了 VFD 这一抽象层,通过在用户空间内管理文件描述符,减少对操作系统直接资源的依赖,并提供了灵活的文件操作接口。VFD 的设计目标包括:
- 减少文件描述符的开销:通过 VFD 管理,避免频繁的文件打开和关闭操作。
- 统一管理文件资源:通过统一的接口和数据结构来管理所有文件资源,方便资源的调度和回收。
- 优化系统性能:通过 LRU 算法管理VFD,确保系统在资源紧张时优先释放最久未使用的资源。
从Linux架构来看,VFD位于应用层的系统调用(即open,read等)函数上方。如下图所示:
1.1 VFD 的数据结构
在 He3DB 中,VFD 主要由一个结构体来表示,该结构体定义在 src/backend/storage/file/fd.c 文件中。这个结构体不仅存储了文件的基本信息,还包括管理文件描述符所需的元数据。
typedef struct vfd
{
int fd; /* current FD, or VFD_CLOSED if none */
unsigned short fdstate; /* bitflags for VFD's state */
ResourceOwner resowner; /* owner, for automatic cleanup */
File nextFree; /* link to next free VFD, if in freelist */
File lruMoreRecently; /* doubly linked recency-of-use list */
File lruLessRecently;
off_t fileSize; /* current size of file (0 if not temporary) */
char *fileName; /* name of file, or NULL for unused VFD */
/* NB: fileName is malloc'd, and must be free'd when closing the VFD */
int fileFlags; /* open(2) flags for (re)opening the file */
mode_t fileMode; /* mode to pass to open(2) */
} Vfd;
- fd
说明: 当前实际的文件描述符。如果没有文件被打开,这个值将被设置为VFD_CLOSED(通常是一个负值,表示文件描述符无效)。
作用: 用于标识和操作实际打开的文件。fd是与操作系统进行文件操作的关键元素。 - fdstate
说明: 用于存储VFD状态的位标志。
作用: 通过位标志,可以表示VFD的不同状态,比如是否被占用、是否在使用中等。位标志提供了对VFD状态的详细控制。 - resowner
说明: 表示VFD的资源拥有者。
作用: 用于自动清理和回收资源。通过资源拥有者,系统可以在特定的资源拥有者(如事务、会话等)结束时自动清理相关的VFD资源。 - nextFree
说明: 链接到下一个空闲的VFD,如果当前VFD在空闲列表中。
作用: 支持VFD池的管理和复用。空闲的VFD通过链表连接,以便在需要时快速分配。 - lruMoreRecently 和 lruLessRecently
说明: 这两个字段用于双向链表,表示VFD在LRU(Least Recently Used)列表中的位置。
作用: 用于维护VFD的使用顺序。lruMoreRecently指向最近使用的VFD,lruLessRecently指向较久未使用的VFD。通过双向链表,系统能够快速访问最近使用和最久未使用的VFD,优化VFD的管理和回收。 - fileSize
说明: 当前文件的大小,如果文件不是临时的,则为0。
作用: 用于存储文件的实际大小。对于临时文件,fileSize为0;对于其他文件,它记录了文件的实际大小,以支持进一步的操作和管理。 - fileName
说明: 文件的名称或路径,如果VFD未使用,则为NULL。
作用: 用于记录文件的名称。这个字段指向一个动态分配的字符串,存储了文件的路径或名称。在VFD关闭时,必须释放这个内存。 - fileFlags
说明: 文件打开时的标志(如open(2)函数的标志)。
作用: 用于控制文件的打开模式,如只读、读写等。通过这些标志,系统可以指定文件的打开方式和权限。 - fileMode
说明: 传递给open(2)函数的模式。
作用: 用于指定文件的创建模式。它定义了文件的权限(如读、写)以及其他文件属性。当创建新文件时,fileMode指定了文件的初始权限设置。
1.2 VFD 的操作流程
VFD 的操作主要涉及文件的打开、关闭、引用计数管理、LRU 链表管理等。在实际运行中,这些操作通过不同的函数实现,且这些函数紧密协作,以确保文件描述符资源的高效管理。
-
文件打开流程:
当需要打开一个文件时,He3DB 首先会检查 VfdCache 中是否已经存在相应的 VFD。如果存在且文件未打开,系统会尝试重新打开该文件。文件打开流程包括以下步骤:
查找 VFD:系统首先在 VfdCache 中查找目标文件的 VFD。
文件描述符的分配:如果 VFD 存在且文件未打开,系统会从操作系统获取一个新的文件描述符,并分配给 VFD 的 fd 字段。
更新 LRU 链表:将该 VFD 移动到 LRU 链表的头部,表示它是最近使用的文件。 -
文件关闭流程:
当一个文件的引用计数变为零时,He3DB 会关闭该文件,并释放对应的操作系统文件描述符。
这一过程包括以下步骤:减少引用计数:调用 FileClose 函数时,首先减少文件的引用计数。判断是否关闭:如果引用计数变为零,系统关闭文件描述符,并从 LRU 链表中移除该 VFD。释放资源:将该 VFD 标记为未使用,并将其添加到空闲 VFD 列表中,以便后续使用。
二、LRU 虚拟文件描述符池
He3DB 通过 LRU(Least Recently Used)算法管理虚拟文件描述符池,确保在系统资源有限的情况下,能够合理地释放和重新分配 VFD。LRU 链表维护了所有的 VFD,按照最近使用的时间顺序排列,最久未使用的 VFD 被优先关闭,以释放系统资源。
LRU 链表通过 lruLink 字段来维护。在 He3DB 中,LRU 链表的管理和操作均在 fd.c 文件中实现,通过特定的函数和宏来完成。
2.1 VfdCache 全局数组
VfdCache 是一个全局数组,用于存储所有的 VFD 实例。这个数组在 He3DB 启动时初始化,并随着系统的运行动态管理其大小。VfdCache 作为 He3DB 管理 VFD 的核心结构,承担了存储、查找、分配和回收 VFD 的功能。
static Vfd *VfdCache;
static Size SizeVfdCache = 0;
/*
* Number of file descriptors known to be in use by VFD entries.
*/
static int nfile = 0;
2.2 VfdCache 数组初始化
在系统启动时,VfdCache 会被初始化,这包括分配数组空间、初始化所有 VFD,并将其标记为未使用状态。该初始化又initFileAccess初始化代码如下:
void
InitFileAccess(void)
{
Assert(SizeVfdCache == 0); /* call me only once */
/* initialize cache header entry */
VfdCache = (Vfd *) malloc(sizeof(Vfd));
if (VfdCache == NULL)
ereport(FATAL,
(errcode(ERRCODE_OUT_OF_MEMORY),
errmsg("out of memory")));
MemSet((char *) &(VfdCache[0]), 0, sizeof(Vfd));
VfdCache->fd = VFD_CLOSED;
SizeVfdCache = 1;
}
注:此时VfdCache[0]不是可以的VFD 是整个LRU池的头节点,指向对空间某个地址。
以下是对每行代码的详细分析:
void InitFileAccess(void)
{
Assert(SizeVfdCache == 0); /* call me only once */
代码使用 Assert 宏检查 SizeVfdCache 的值是否为 0。这是一个断言,用于确保这个初始化函数在缓存大小为 0 时才被调用,保证函数只会被调用一次。如果 SizeVfdCache 的值不是 0,表明缓存已经被初始化过,函数会在调试模式下中断执行。
/* initialize cache header entry */
VfdCache = (Vfd *) malloc(sizeof(Vfd));
if (VfdCache == NULL)
ereport(FATAL,
(errcode(ERRCODE_OUT_OF_MEMORY),
errmsg("out of memory")));
代码分配了一个 Vfd 结构体的内存,并将其指针赋值给 VfdCache。sizeof(Vfd) 计算 Vfd 结构体的大小,以确保分配的内存足够。
如果 malloc 返回 NULL,则表示内存分配失败。此时,ereport 宏用于报告致命错误,提示“out of memory”(内存不足),并使用 ERRCODE_OUT_OF_MEMORY 错误代码。ereport 宏会终止程序的执行,避免进一步的内存错误
MemSet((char *) &(VfdCache[0]), 0, sizeof(Vfd));
VfdCache->fd = VFD_CLOSED;
MemSet 是一个用于将内存区域的每个字节设置为指定值的函数。这里,它将 VfdCache[0] 这个 Vfd 结构体的内存区域中的所有字节设置为 0。这意味着 Vfd 结构体的所有字段都将被初始化为 0。
VfdCache->fd = VFD_CLOSED; 将 Vfd 结构体的 fd 字段设置为 VFD_CLOSED,标志着当前没有打开的文件。VFD_CLOSED 是一个特殊的值,通常是负数,表示文件描述符未被打开。
SizeVfdCache = 1;
SizeVfdCache 被设置为 1,表示 VfdCache 数组中当前有一个 Vfd 实例。这个值用于跟踪缓存的大小,确保系统知道当前有多少个虚拟文件描述符被分配和管理。
2.3 LRU 结构图
LRU池是一个双向链表,开始和结束于元素VfdCache[0], 元素0是特殊节点,它不代表一个文件,其中fd字段总是等于VFD_CLOSED。 元素0是一个头节点,它标明了LRU池的开始/结束。只有当前真正打开(分配了FD)的VFD元素在LRU池中。
虽然LRU池是双向链表,但是Vfd结构中并没有指针,而是通过IruMoreRecently、 IruLessRecently这两个int类型的成员变量实现了双向链表中的next和prev指针的功能.
对于LRU池中的每个VFD,均使用成员IruMoreRecently, IruLessRecently链接两个VFD变量,通过IruMoreRecently成员数组下标链接最近更常使用的VFD;而通过IruLessRecently成员数组下标链接最近不常用的VFD。如下图所示:
其中VfdCache[0]充当该链接池的头节点(特殊VFD);另外该LRU池的尾元素VfdCache[0]通过IruLessRecently成员链接到VfdCache[0]头部,而VfdCache[0]头节点通过IruMoreRecently成员链接到 VfdCache[n]。这样就能够很方便地通过VfdCache[0]头节点找到该池中最近最少使用的VFD。
当然,这个LRU池的大小同样是受到操作系统对进程打开文件描述符数据的限制是一样的。在He3DB中,与max safe fds变量的值极其相关。
2.4 从LRU池获取VFD与释放
当 He3DB 的 postmaster 进程启动时,它会为 VfdCache 指针分配一块内存,用于存储 Vfd 类型的结构体。然而,此时 VfdCache 并不包含任何有效的虚拟文件描述符(VFD)。具体来说,VfdCache 的第一个元素 VfdCache[0] 被用作双向链表的头节点,这个节点不会存储实际的 VFD。
在数据库系统的启动过程中,最初尝试获取 VFD 的时候,进程会调用 AllocateVfd() 函数来分配一个有效的 VFD 变量。如果此时没有可用的 VFD,AllocateVfd() 函数会处理这一情况。
static File
AllocateVfd(void)
{
Index i;
File file;
DO_DB(elog(LOG, "AllocateVfd. Size %zu", SizeVfdCache));
Assert(SizeVfdCache > 0); /* InitFileAccess not called? */
if (VfdCache[0].nextFree == 0)
{
/*
* The free list is empty so it is time to increase the size of the
* array. We choose to double it each time this happens. However,
* there's not much point in starting *real* small.
*/
Size newCacheSize = SizeVfdCache * 2;
Vfd *newVfdCache;
if (newCacheSize < 32)
newCacheSize = 32;
/*
* Be careful not to clobber VfdCache ptr if realloc fails.
*/
newVfdCache = (Vfd *) realloc(VfdCache, sizeof(Vfd) * newCacheSize);
if (newVfdCache == NULL)
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
errmsg("out of memory")));
VfdCache = newVfdCache;
/*
* Initialize the new entries and link them into the free list.
*/
for (i = SizeVfdCache; i < newCacheSize; i++)
{
MemSet((char *) &(VfdCache[i]), 0, sizeof(Vfd));
VfdCache[i].nextFree = i + 1;
VfdCache[i].fd = VFD_CLOSED;
}
VfdCache[newCacheSize - 1].nextFree = 0;
VfdCache[0].nextFree = SizeVfdCache;
/*
* Record the new size
*/
SizeVfdCache = newCacheSize;
}
file = VfdCache[0].nextFree;
VfdCache[0].nextFree = VfdCache[file].nextFree;
return file;
}
下面是对每行代码的详细分析:
static File AllocateVfd(void)
{
Index i;
File file;
定义了局部变量 i 和 file。i 用于循环和索引,file 用于存储分配的 VFD 的索引。
DO_DB(elog(LOG, "AllocateVfd. Size %zu", SizeVfdCache));
这是一个调试输出语句,用于记录当前 VFD 缓存的大小(SizeVfdCache)。DO_DB 是一个宏,可能用于控制调试日志的输出。
Assert(SizeVfdCache > 0); /* InitFileAccess not called? */
使用 Assert 宏检查 SizeVfdCache 是否大于 0。这是一个断言,确保 AllocateVfd 函数在 InitFileAccess 已经被调用且缓存已经初始化之后调用。如果 SizeVfdCache 小于或等于 0,说明缓存还没有初始化,这会导致断言失败。
if (VfdCache[0].nextFree == 0)
{
/*
* The free list is empty so it is time to increase the size of the
* array. We choose to double it each time this happens. However,
* there's not much point in starting *real* small.
*/
Size newCacheSize = SizeVfdCache * 2;
Vfd *newVfdCache;
if (newCacheSize < 32)
newCacheSize = 32;
这段代码检查缓存中的空闲列表是否为空 (VfdCache[0].nextFree == 0)。如果为空,则需要扩展缓存。
计算新的缓存大小 newCacheSize 为当前缓存大小的两倍。这样做可以逐步扩大缓存容量,以减少频繁扩展的开销。确保 newCacheSize 至少为 32,以防止缓存大小过小,影响性能。
/*
* Be careful not to clobber VfdCache ptr if realloc fails.
*/
newVfdCache = (Vfd *) realloc(VfdCache, sizeof(Vfd) * newCacheSize);
if (newVfdCache == NULL)
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
errmsg("out of memory")));
VfdCache = newVfdCache;
使用 realloc 扩展 VfdCache 的内存块。如果内存分配失败,realloc 会返回 NULL,此时报告错误并终止程序。
如果 realloc 成功,将 VfdCache 指向新的内存块。
/*
* Initialize the new entries and link them into the free list.
*/
for (i = SizeVfdCache; i < newCacheSize; i++)
{
MemSet((char *) &(VfdCache[i]), 0, sizeof(Vfd));
VfdCache[i].nextFree = i + 1;
VfdCache[i].fd = VFD_CLOSED;
}
VfdCache[newCacheSize - 1].nextFree = 0;
VfdCache[0].nextFree = SizeVfdCache;
对新分配的 Vfd 实例进行初始化:
- 使用 MemSet 将每个新 Vfd 的内存清零。
- 设置每个新 Vfd 的 nextFree 指向下一个 VFD的索引,使它们形成一个空闲列表链表。
- 设置每个 Vfd 的 fd 字段为 VFD_CLOSED,表示文件描述符未打开。
将 VfdCache 末尾的 nextFree 设置为 0,标记链表的结束。
更新 VfdCache[0].nextFree 以指向扩展前缓存的第一个索引,表示新的空闲列表的开始。
/*
* Record the new size
*/
SizeVfdCache = newCacheSize;
}
更新 SizeVfdCache 为新的缓存大小。记录当前缓存的实际大小,以便后续分配和管理。
file = VfdCache[0].nextFree;
VfdCache[0].nextFree = VfdCache[file].nextFree;
return file;
}
从 VfdCache[0].nextFree 中获取下一个空闲 VFD 的索引,并将其赋值给 file。
更新 VfdCache[0].nextFree 为下一个空闲 VFD 的 nextFree,从而维护空闲列表的链表。
返回分配的 VFD 索引 file。
VfdCache 的分配策略是通过倍增的方法来分配 VFD 内存空间。初始时,VfdCache 中的 VFD 数量设置为一个最小值,比如 32 个。当系统首次初始化 VfdCache 时,如果当前 SizeVfdCache 变量的值为 1(表示当前申请的 VFD 数量为 32),则在内存不足的情况下会申请 32 个新的 VFD 变量内存空间。
这个分配策略的详细步骤如下:
-
初始化 VfdCache:
在数据库进程启动时,VfdCache 的初始分配量是 32 个 VFD 结构体。
SizeVfdCache 变量记录当前分配的 VFD 数量。在第一次初始化时,SizeVfdCache 的值设置为 1,表示当前已分配 32 个 VFD。
首次调用 AllocateVfd(): -
在 AllocateVfd() 函数被调用时,如果发现 SizeVfdCache 为 1(即当前已经分配了 32 个 VFD),但需要更多的 VFD 空间时,系统会进行新的内存申请。
内存申请的数量通常是原有数量的倍数,例如再申请 32 个新的 VFD 空间。
内存分配示意图: -
首次申请时:
已分配:32 个 VFD
SizeVfdCache = 1
如果需要更多的 VFD:
再次申请:32 个新的 VFD
更新 SizeVfdCache 为 2
这样,每次内存不足时,VfdCache 会按照预设的倍增策略继续申请更多的 VFD 结构体空间,确保系统有足够的资源来处理文件操作。
当获取到可用的VFD数组元素之后,接下来就开始调用系统函数来打开所指定的文件,然后将open()系统函数返回的文件描述符fd初始化给VFD中的fd成员变量。同时分别将本次打开文件的模式以及文件权限(若是创建文件的话)初始化给VFD中的成员fileFlags和fileMode。并将其他的成员根据实际情况进行初始化。
File
PathNameOpenFilePerm(const char *fileName, int fileFlags, mode_t fileMode)
{
char *fnamecopy;
File file;
Vfd *vfdP;
DO_DB(elog(LOG, "PathNameOpenFilePerm: %s %x %o",
fileName, fileFlags, fileMode));
/*
* We need a malloc'd copy of the file name; fail cleanly if no room.
*/
fnamecopy = strdup(fileName);
if (fnamecopy == NULL)
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
errmsg("out of memory")));
file = AllocateVfd();
vfdP = &VfdCache[file];
/* Close excess kernel FDs. */
ReleaseLruFiles();
vfdP->fd = BasicOpenFilePerm(fileName, fileFlags, fileMode);
if (vfdP->fd < 0)
{
int save_errno = errno;
FreeVfd(file);
free(fnamecopy);
errno = save_errno;
return -1;
}
++nfile;
DO_DB(elog(LOG, "PathNameOpenFile: success %d",
vfdP->fd));
vfdP->fileName = fnamecopy;
/* Saved flags are adjusted to be OK for re-opening file */
vfdP->fileFlags = fileFlags & ~(O_CREAT | O_TRUNC | O_EXCL);
vfdP->fileMode = fileMode;
vfdP->fileSize = 0;
vfdP->fdstate = 0x0;
vfdP->resowner = NULL;
Insert(file);
return file;
}
PathNameOpenFilePerm 函数的目的是打开一个文件,并为其分配一个虚拟文件描述符(VFD)。下面是对函数每行代码的详细分析:
File PathNameOpenFilePerm(const char *fileName, int fileFlags, mode_t fileMode)
{
char *fnamecopy;
File file;
Vfd *vfdP;
定义了局部变量 fnamecopy(文件名的拷贝)、file(虚拟文件描述符的索引)和 vfdP(指向 VfdCache 的指针)。
DO_DB(elog(LOG, "PathNameOpenFilePerm: %s %x %o",
fileName, fileFlags, fileMode));
调试输出语句,记录函数的输入参数 fileName(文件名)、fileFlags(文件打开标志)和 fileMode(文件权限模式)。DO_DB 是一个宏,用于控制调试日志的输出。
/*
* We need a malloc'd copy of the file name; fail cleanly if no room.
*/
fnamecopy = strdup(fileName);
if (fnamecopy == NULL)
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
errmsg("out of memory")));
使用 strdup 创建 fileName 的一个动态分配的副本。strdup 会分配内存并复制字符串内容。
如果 strdup 失败(返回 NULL),则报告内存不足的错误并终止程序。
file = AllocateVfd();
vfdP = &VfdCache[file];
调用 AllocateVfd 函数分配一个新的虚拟文件描述符(VFD),并将其索引存储在 file 变量中。
设置 vfdP 指向 VfdCache[file],即新分配的 VFD。
/* Close excess kernel FDs. */
ReleaseLruFiles();
调用 ReleaseLruFiles 函数关闭不再使用的内核文件描述符(FD)。这有助于释放系统资源,避免文件描述符耗尽。
vfdP->fd = BasicOpenFilePerm(fileName, fileFlags, fileMode);
调用 BasicOpenFilePerm 函数使用指定的 fileName、fileFlags 和 fileMode 打开文件,并将返回的文件描述符赋值给 vfdP->fd。BasicOpenFilePerm 是实际执行文件打开操作的函数。
if (vfdP->fd < 0)
{
int save_errno = errno;
FreeVfd(file);
free(fnamecopy);
errno = save_errno;
return -1;
}
- 如果 BasicOpenFilePerm 返回负值(表示文件打开失败),则保存当前的 errno 值。
- 调用 FreeVfd 释放先前分配的 VFD。
- 释放 fnamecopy 动态分配的内存。
- 恢复保存的 errno 值。
- 返回 -1 表示失败。
++nfile;
DO_DB(elog(LOG, "PathNameOpenFile: success %d",
vfdP->fd));
- 增加 nfile 计数器,可能用于跟踪当前打开的文件数量。
- 调试输出成功信息,包括新打开文件的描述符 vfdP->fd。
vfdP->fileName = fnamecopy;
/* Saved flags are adjusted to be OK for re-opening file */
vfdP->fileFlags = fileFlags & ~(O_CREAT | O_TRUNC | O_EXCL);
vfdP->fileMode = fileMode;
vfdP->fileSize = 0;
vfdP->fdstate = 0x0;
vfdP->resowner = NULL;
- 将动态分配的 fnamecopy 赋值给 vfdP->fileName,保存文件名。
- 将 fileFlags 保存到 vfdP->fileFlags 中,但去掉 O_CREAT、O_TRUNC 和 O_EXCL 标志。这是为了处理文件的重新打开情况。
- 保存 fileMode 到 vfdP->fileMode。
- 将 fileSize、fdstate 和 resowner 初始化为默认值。这些字段可能用于文件状态管理和资源所有权。
Insert(file);
return file;
}
- 调用 Insert 函数将新分配的 VFD 插入到适当的位置(如文件描述符表或其他管理结构中)。
- 返回分配的虚拟文件描述符 file。
- 到这里时, VFD已经是一个可提供给进程使用的虚拟文件描述符了。给上层的是该VFD位于LRU池中一的数组下标nextFree,而不会对外提供VFD中的成员fd值。接下来的最后一个任务就是初始化VFD中的两个数组下标成员IruMoreRecentl和IruMoreRecently。使它们分别指向VfdCache头节点。以便于快速从VfdCache[0]找到该LRU池中最近常使用、不常使用的VFD。以便于在LRU池超出操作系统文件描述符限制时根据LRU策略删除不常用的VFD。对应代码如下:
vfdP = &VfdCache[file];
vfdP->lruMoreRecently = 0;
vfdP->lruLessRecently = VfdCache[0].lruLessRecently;
VfdCache[0].lruLessRecently = file;
VfdCache[vfdP->lruLessRecently].lruMoreRecently = file;
总结
He3DB 中的 VFD 机制通过对操作系统文件描述符的抽象和扩展,实现了数据库文件操作的高效管理。VFD 的设计包括全局 VfdCache 数组、LRU 链表管理策略、动态分配和回收机制等。通过这些机制,He3DB 能够在高负载环境下有效管理文件资源,确保数据库的稳定性和性能。
VFD 的实现细节充分展示了 He3DB 在资源管理方面的精细设计和优化思路。通过这一机制,系统不仅能够高效地管理文件描述符,还能够在资源紧张时灵活调度,确保数据库系统的平稳运行。。
作者介绍
张杰,移动云数据库工程师,负责云原生数据库He3DB的研发。