PostgreSQL 的代码里面有一个README文件,很详细的描述了事务的各种操作关系和函数调用关系 。
For example, consider the following sequence of user commands:
1) BEGIN
2) SELECT * FROM foo
3) INSERT INTO foo VALUES (...)
4) COMMIT
In the main processing loop, this results in the following function call
sequence:
/ StartTransactionCommand;
/ StartTransaction;
1) < ProcessUtility; << BEGIN
\ BeginTransactionBlock;
\ CommitTransactionCommand;
/ StartTransactionCommand;
2) / PortalRunSelect; << SELECT ...
\ CommitTransactionCommand;
\ CommandCounterIncrement;
/ StartTransactionCommand;
3) / ProcessQuery; << INSERT ...
\ CommitTransactionCommand;
\ CommandCounterIncrement;
/ StartTransactionCommand;
/ ProcessUtility; << COMMIT
4) < EndTransactionBlock;
\ CommitTransactionCommand;
\ CommitTransaction;
begin/commit 操作,每一条语句都会被 StartTransactionCommand 和 CommitTransactionCommand (AbortCurrentTransaction) 包裹起来。
因为是 Begin 命令,所以有 BeginTransactionBlock,然后还会调用 StartTransaction 表示事务开始。
BeginTransactionBlock 是 Begin 命令专有的函数, 表示后续的 SQL 语句是一个完整的事务,所以要做一些状态处理。
StartTransaction 属于底层的事务调用,无论有没有 Begin,都会调用到。
Begin 模块里面,ProcessUtility 是具体执行逻辑的地方,包含了 BeginTransactionBlock,在 Greenplum 代码里,会在 master 和 segment 上面都执行 Begin。
Insert 模块里面,ProcessQuery 是具体执行逻辑的地方,在 Cloudberry / Greenplum 代码里,会在这一步把 insert 相关的 SQL 发到对应的 segment 上面。
Commit 模块里面,CommitTransaction 是具体执行逻辑的地方。在 Cloudberry / Greenplum 代码里,有两个步骤,第一步发送 DTX_PROTOCOL_COMMAND_PREPARE 到每个 segment,第二步发送 DTX_PROTOCOL_COMMAND_COMMIT_PREPARE 到每个 segment,这就是传说中的两阶段提交。

简单的begin 命令, 只用了 libpq 来做通信。如果 有复杂的 Query,会使用 Greenplum 自研的 Interconnect 机制做数据交互。

用"psql -d gpadmin" 连接 master node,这个命令会先连接 postgres 主进程,然后会 fork 出一个子进程出来,这个也是 PostgreSQL 单机版的常规动作。然后从客户端执行 "begin;" 命令。这个命令是通过 libpq 的协议发送到 master node 的。
case 'Q': /* simple query */
在 master node 上,被新 fork 出来的进程里面,会一直在"ReadCommand" 这个函数这里等待命令。
/* ----------------
* ReadCommand reads a command from either the frontend or
* standard input, places it in inBuf, and returns the
* message type code (first byte of the message).
* EOF is returned if end of file.
* ----------------
*/
static int
ReadCommand(StringInfo inBuf)
{
int result;
SIMPLE_FAULT_INJECTOR(BeforeReadCommand);
if (whereToSendOutput == DestRemote)
result = SocketBackend(inBuf);
else
result = InteractiveBackend(inBuf);
return result;
}
ReadCommand 是从 SocketBackend 读入数据的。读到以后,发现"firstchar == 'Q'",所以这是一个 libpq 的 simple query,经过 query statement 的 parsing 工作,发现是一个单独的 begin 命令,就在本地执行了 BeginTransactionBlock。
这之后,开始调用 sendDtxExplicitBegin,开始做分布式的工作。
dtmPreCommand("sendDtxExplicitBegin", "(none)", NULL,
/* is two-phase */ true, /* withSnapshot */ true, /* inCursor */ false );
dtmPreCommand 这个函数mark 目前的分布式事务是否要使用两阶段提交协议的,
最后到 AllocateWriterGang, 检测到没有 Gang ,然后开始创建 gang,
writerGang = createGang(GANGTYPE_PRIMARY_WRITER, PRIMARY_WRITER_GANG_ID, nsegdb, -1);
Gang 是 Greenplum 里面工作在不同 segment 上面,但是为了同一个 Slice 而生成的一组内存资源。 master node 上面的 Gang。
if (writerGang == NULL)
{
int nsegdb = getgpsegmentCount();
insist_log(IsTransactionOrTransactionBlock(),
"cannot allocate segworker group outside of transaction");
if (GangContext == NULL)
{
GangContext = AllocSetContextCreate(TopMemoryContext,
"Gang Context",
ALLOCSET_DEFAULT_MINSIZE,
ALLOCSET_DEFAULT_INITSIZE,
ALLOCSET_DEFAULT_MAXSIZE);
}
Assert(GangContext != NULL);
oldContext = MemoryContextSwitchTo(GangContext);
writerGang = createGang(GANGTYPE_PRIMARY_WRITER,
PRIMARY_WRITER_GANG_ID, nsegdb, -1);
writerGang->allocated = true;
/*
* set "whoami" for utility statement.
* non-utility statement will overwrite it in function getCdbProcessList.
*/
for(i = 0; i < writerGang->size; i++)
setQEIdentifier(&writerGang->db_descriptors[i], -1, writerGang->perGangContext);
MemoryContextSwitchTo(oldContext);
}
GUC 叫做 gp_connections_per_thread,这个 GUC 能决定是使用多线程的方式去建立 master 到 segment 的数据库连接,还是用异步的方式来建立连接。default 的数值是 0,如果是 0 就是用异步方式,大概就是用 connect 做连接,然后 poll 做 socket 的异步监控,直到最后把所有连接都建立好,把 fd 存好。如果这个 GUC 的数值不是 0,那么就会用多线程的方式来连接 segment,起多少个线程需要根据 GUC 的值和 segment 的数量来计算。这是同一的目标的两种不同实现,code 也比较清楚,可以自己翻看源码。
因为我们用的是 default 的值,所以用的是异步的方式,通过 createGang_async 最后调用了 PQconnectStartParams,这个函数就相当于 psql 客户端执行(psql -d gpadmin)会去连接每个 segment 数据库的 postgres 进程。
这些进程也会 fork 子进程出来,然后开始准备环境,执行后续 sql 命令。代码到了这里,master 连接 segments 的工作就完成了。
后面的函数就是在发送具体的命令,就是 Begin 命令。cdbdisp_dispatchToGang 发送,因为是异步,所以 cdbdisp_waitDispatchFinish 等待发送完成,然后 cdbdisp_getDispatchResults 等待 segments 回复结果。
dispatcher分配QE资源
AllocatwGang()
GANG 大小分配灵活
最小一个
一般为segment的个数
甚至可以大于segment的个数(一个segment为一个gang分配多于一个的QE资源)
QE资源闲置以后可以被后续查询使用(或者闲置一段时间后被清除)
dispatcher功能分发任务
CdbDispatchPlan Plan + SliceTable
CdbDispatchCommand
CdbDispatchDtxProtocolCommand
CdbDispatchUtilityStatement
dispatcher功能协调控制
cdbdisp_checkDispatchResult(等待模式)
等待模式
1、blocking 阻塞等待所有QEs完成执行或者出现异常
2、Non -blocking 检查所有QEs的状态,若QEs有异常则报错,否则立即返回
3、Finish 给所有活动的QEs发送QueryFinish消息提前结束任务,QE不报错
4、Cancel 给所有活动的QEs发送QueryCancel消息,终止任务。
典型的dispatcher程序
ds = cdbdisp_makeDispatcherState
primaryGang = AllocateGang(ds,GANGTYPE_PRIMARY_WRITER,segment) cdbdisp_dispatchToGang(ds,primaryGang,-1)
cdbdisp_waitDispatchFinish(ds)
cdbdisp_checkDispatchResult(ds,DISPATCH_WAIT_NODE) cdbdisp_getDispatchResults(ds,&qeError)
cdbdisp_destroyDispatcherstate(ds)
Motion的内部实现是Interconnect
sender和receiver之间通过网络在QE之间移动数据,在GPDB中,该网络模块叫做Interconnect

UDPIFC线程模型
为什么使用线程模型?
1、UDPIFC在应用层保证传输的可靠性,需要单独的线程来保证传输可靠协议
2、QE在fork的时候呼启动一个udpifc线程,该线程服务该session所有将要可能的查询
3、udpifc线程接受所有发送给该QE的数据包并通过共享内存移交给主进程。
4、设计的函数
线程细节可参考rxThreadFunc函数
接受端逻辑可参考RecvTupleChunkFrom*函数
发送端逻辑可参考SendChunkUDPIFC
 分布式事务 insert 语句&spm=1001.2101.3001.5002&articleId=159280066&d=1&t=3&u=bfdc06ac02b14f449ec2ad1cc0b7cd29)
702

被折叠的 条评论
为什么被折叠?



