Cloudberry (九) 分布式事务 insert 语句

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值