我厂为了做Greenplum的多租户,将GP master做成了容器,跑在flannel网络里面,而GP segment跑在物理网络上。

topo

之所以master没有跑在宿主机网络(hostnet)上,是因为GP部署会ssh localhost做一些不可描述的事情(比如生成一个id文件),如果容器的网络使用hostnet,在容器中ssh localhost会连到宿主机上的ssh server,那么相应的动作、生成的文件就会在宿主机上,而不是容器里。

改成flannel网络以后,部署没有问题了;我试了下\d看表,nice;试了下create table xxx,nice;试了下select * from xxx,fxxk..挂住了。

抓了个包,看到select时,segment(s1.gp.com)往master容器所在的宿主机(m1.gp.com)上发UDP报文,但被m1无情的以ICMP port unreachable拒绝了。segment发出来的UDP报文目的端口号是54963,目的地址是m1的物理网卡地址;而54963是master容器监听的UDP端口,但是在m1宿主机上,并没有针对该端口号做iptable转发规则,因此被拒绝也就在情理之中了。

看上去问题的原因就在于,segment发UDP的报文时,目的地址不应该填写master容器所在的宿主机的地址,而应该是master容器的地址。由于segment所在的物理机也在kubernetes的flannel网络中,该UDP报文会走overlay转发给master容器,deal done。

问题清楚明白,剩下的就是改代码了,但这个过程,饶实花了一位不愿具名的同事(以及我)很大的力气。

UDP报文是干啥的?

首先来看看这个不受欢迎的UDP是干啥的。为了简化问题,我们在一个正常工作的纯物理机的环境上抓了下这个报文,查询语句是select * from fruit,抓到的报文里我们看到一堆水果(banana/apple/orange),说明这个报文是segment往master上汇报的查询结果。在这个UDP报文之前,所有报文都是TCP报文,那么猜想UDP报文的目的端口号应该是在前面的TCP报文交互时,从master容器带给segment的。

phy-select

但是,我们把前面的报文翻了个底朝天,都没有找到任何关于41590(0xa276)的痕迹。这不是奇了怪了吗。这不是奇了怪了吗。这不是奇了怪了吗。

不怕,磨刀不误砍柴工,我们先来看看这个查询过程。

关于Greenplum查询过程

GP网站上有一篇文章:About Greenplum Query Processing,详细的说明了查询的过程。

client连接到master,下发了select * from fruit指令,之后master解析并优化这个query,并生成query plan。query plan可能是并发,也可能是到某个特定目的segment的。segment负责在其本地数据库上执行query plan。

下图展示了并发query的dispatching过程。

Dispatching the Parallel Query Plan

一些指令(insert/delete/update/select),可能只需要某一特定segment处理,下图展示了这种情况。

 Dispatching a Targeted Query Plan

关于query plan的理解直接参考上面的官网,我们直接来看并行查询。

Greenplum创建了很多database进程来处理query。在master上,这个进程叫做query dispatcher(QD),负责生成和派发query plan,它也负责收集和展示最终结果。在segment上,这个进程叫做query executor(QE),它负责完成该segment部分的工作,并与其他进程交互中间结果。

不同segment上处理同一slice的QE,叫做gangs。各部分工作是按照gang的顺序依次完成,进程内部的交互是通过interconnect完成的。

Query Worker Processes

而interconnect就是UDP连接,我们看到的这个UDP报文也就是interconnect的一个步骤。

了解了这个过程,我们再来看看segment上是如何取得master的监听端口,并发出去这个UDP报文的。

segment如何取得master的监听端口号

Greenplum的master和segment使用的是同一套代码,看起来会有点乱,走读的时候注意区分GpRoleValue,若role为GP_ROLE_DISPATCH,则表示master,若role为GP_ROLE_EXECUTE则表示为segment。代码流程太长,我们只关注UDP端口号是怎么过来的就可以了。

前面我们说过,从报文中根本没有找到UDP端口的影子,那GP是怎么得到的呢?可以肯定的是,端口号在TCP报文里。

回忆下前面我们讲的查询流程,segment在查询结束以后,会再将结果发给下一级gang,对于本次select查询来说,也就是query dispatcher(QD),所以,端口号信息应该会跟QD相关。那么再来看看exec_mpp_query的代码,是不是QueryDispatchDesc相关的处理,看上去很像?

继续走读代码,发现GP在这里做了一个优化,master在发报文之前,会先将数据做zlib压缩,segment收到以后,需要在deserializeNode中调用uncompress_string来解压缩,解压缩之后的数据拿去给readNodeFromBinaryString,之后在readNodeBinary做乾坤大挪移,徒手解析协议报文。

关于报文UDP的处理,在_readCdbProcess中。

static CdbProcess *
_readCdbProcess(void)
{
	READ_LOCALS(CdbProcess);

	READ_STRING_FIELD(listenerAddr);
	READ_INT_FIELD(listenerPort);
	READ_INT_FIELD(pid);
	READ_INT_FIELD(contentid);

	READ_DONE();
}

注意代码里的READ_INT_FIELD,这个宏的定义不是用readfunc.c的,而是readfast.c的,因为readfunc.c是在readfast.c中include进来的,由于COMPILING_BINARY_FUNCS的关系,readfunc.c的宏定义没有生效。在c文件中include另一个c文件,也是活久见了。

#define COMPILING_BINARY_FUNCS
#include "readfuncs.c"

现在简单了,只要将报文中的数据zlib解压缩(我用go写了个小程序,调用zlib.NewReader解压,代码就不献丑了),看下对应listenerAddr和listenerPort的部分是什么情况就行了。这个过程比较枯燥,其实就是根据协议结构来套数据,我贴一下解压、翻译过来的数据。

01 00 00 00 listsize
11 00 T_CdbProcess
00 00 00 00 listenerAddr
76 a2 00 00 listenerPort
1e 70 00 00 pid
ff ff ff ff contentid

listenerAddr这里是个STRING,前4个字节是字符串的长度。显然,字符串长度为0,master没有把自己的IP地址带给segment。

那segment在这种情况下怎么处理呢?其实也没什么办法了,segment只好根据和master的TCP的连接,来推测一下master的地址。具体代码可以看看adjustMasterRouting

void adjustMasterRouting(Slice *recvSlice)
{
	ListCell *lc = NULL;
	foreach(lc, recvSlice->primaryProcesses)
	{
		CdbProcess *cdbProc = (CdbProcess *)lfirst(lc);

		if (cdbProc)
		{
			if (cdbProc->listenerAddr == NULL)
				cdbProc->listenerAddr = pstrdup(MyProcPort->remote_host);
		}
	}
}

我画了一个segment处理的函数调用图,流程很长,如果有问题请告知我。

gp-segment

问题如何解决

解铃还须系铃人,从前面的分析可知,只要master发送’M’消息的时候,带上listenerAddr,就可以了。

gp-master

getCdbProcessesForQD生成了一个CdbProcess用来代表QD,其中监听地址填的是NULL,之后发送interconnection连接信息的时候,填的也是0了。要告知segment监听的地址,只要将这里的listenerAddr改为本地地址就可以了。

List *
getCdbProcessesForQD(int isPrimary)
{
    proc->listenerAddr = NULL;
	proc->listenerPort = Gp_listener_port;

至于怎么找本地地址,google一下,实现方式比较多

看到这里自然就会问一个问题,为什么Greenplum没有这么实现呢?我觉得可能是因为master上可能有多个网卡,在报文中不太容易判断应该带哪个地址(当然要带也是可以的,根据segment的IP地址查路由找最匹配的源地址),所以干脆就不带;因为之前已经有从master到segment的TCP连接,所以segment可以很容易的推断master的地址。但这样处理也就带来一个问题,如果master像我们这样在NAT里面,TCP连接建立以后,segment看到的master地址是做了SNAT以后的地址,拿着这个地址去新建UDP连接,自然也就失败了。

但对于kubernetes环境来说,由于master运行在docker容器里,看到的只有eth0和lo口,所以可以很轻松的决定监听地址,从而解决问题。

master如何决定监听哪个UDP端口呢?

master的端口号是随机决定的,其原理也就是前文说说内核协议栈的端口号所讲的bind 0端口号,让linux帮忙选一个随机端口号,只是这里是udp的。具体实现可以看看setupUDPListeningSocket,代码不贴了。

segment在连接master的UDP server时,报文会从segment上直接走flannel转发给master容器,所以master容器的宿主机是否配置service(即是否配置iptables),也就不重要了。

写在最后

Greenplum的代码写的不是很容易看懂,跟我老东家H3C Comware的代码比起来要差很多。比如PostgresMain长达几千行的函数,比如readNodeBinary几公里的switch case,对看代码的人来说,光鼠标翻页就要手抽筋了。代码写的复杂比较容易,写的简单,老妪能解,更见功力。