进程间通信方式

进程间通信方式,第1张

在操作系统中,一个进程可以理解为是关于计算机资源集合的一次运行活动,其就是一个正在执行的程序的实例。从概念上来说,一个进程拥有它自己的虚拟CPU和虚拟地址空间,任何一个进程对于彼此而言都是相互独立的,这也引入了一个问题 —— 如何让进程之间互相通信?

由于进程之间是互相独立的,没有任何手段直接通信,因此我们需要借助操作系统来辅助它们。举个通俗的例子,假如A与B之间是独立的,不能彼此联系,如果它们想要通信的话可以借助第三方C,比如A将信息交给C,C再将信息转交给B —— 这就是进程间通信的主要思想 —— 共享资源。

这里要解决的一个重要的问题就是如何避免竞争,即避免多个进程同时访问临界区的资源。

共享内存是进程间通信中最简单的方式之一。共享内存允许两个或更多进程访问同一块内存。当一个进程改变了这块地址中的内容的时候,其它进程都会察觉到这个更改。

你可能会想到,我直接创建一个文件,然后进程不就都可以访问了?

是的,但这个方法有几个缺陷:

Linux下采用共享内存的方式来使进程完成对共享资源的访问,它将磁盘文件复制到内存,并创建虚拟地址到该内存的映射,就好像该资源本来就在进程空间之中,此后我们就可以像操作本地变量一样去操作它们了,实际的写入磁盘将由系统选择最佳方式完成,例如操作系统可能会批量处理加排序,从而大大提高IO速度。

如同上图一样,进程将共享内存映射到自己的虚拟地址空间中,进程访问共享进程就好像在访问自己的虚拟内存一样,速度是非常快的。

共享内存的模型应该是比较好理解的:在物理内存中创建一个共享资源文件,进程将该共享内存绑定到自己的虚拟内存之中。

这里要解决的一个问题是如何将同一块共享内存绑定到自己的虚拟内存中,要知道在不同进程中使用 malloc 函数是会顺序分配空闲内存,而不会分配同一块内存,那么要如何去解决这个问题呢?

Linux操作系统已经想办法帮我们解决了这个问题,在 #include <sys/ipc.h>和 #include <sys/shm.h>头文件下,有如下几个shm系列函数:

通过上述几个函数,每个独立的进程只要有统一的共享内存标识符便可以建立起虚拟地址到物理地址的映射,每个虚拟地址将被翻译成指向共享区域的物理地址,这样就实现了对共享内存的访问。

还有一种相像的实现是采用mmap函数,mmap通常是直接对磁盘的映射——因此不算是共享内存,存储量非常大,但访问慢; shmat与此相反,通常将资源保存在内存中创建映射,访问快,但存储量较小。

不过要注意一点,操作系统并不保证任何并发问题,例如两个进程同时更改同一块内存区域,正如你和你的朋友在线编辑同一个文档中的同一个标题,这会导致一些不好的结果,所以我们需要借助信号量或其他方式来完成同步。

信号量是迪杰斯特拉最先提出的一种为解决 同步不同执行线程问题 的一种方法,进程与线程抽象来看大同小异,所以 信号量同样可以用于同步进程间通信

信号量 s 是具有非负整数值的全局变量,由两种特殊的 原子操作 来实现,这两种原子操作称为 P 和 V :

信号量并不用来传送资源,而是用来保护共享资源,理解这一点是很重要的,信号量 s 的表示的含义为 同时允许最大访问资源的进程数量 ,它是一个全局变量。来考虑一个上面简单的例子:两个进程同时修改而造成错误,我们不考虑读者而仅仅考虑写者进程,在这个例子中共享资源最多允许一个进程修改资源,因此我们初始化 s 为1。

开始时,A率先写入资源,此时A调用P(s),将 s 减一,此时 s = 0,A进入共享区工作。

此时,进程B也想进入共享区修改资源,它调用P(s)发现此时s为0,于是挂起进程,加入等待队列。

A工作完毕,调用V(s),它发现s为0并检测到等待队列不为空,于是它随机唤醒一个等待进程,并将s加1,这里唤醒了B。

B被唤醒,继续执行P操作,此时s不为0,B成功执行将s置为0并进入工作区。

此时C想要进入工作区......

可以发现,在无论何时只有一个进程能够访问共享资源,这就是信号量做的事情,他控制进入共享区的最大进程数量,这取决于初始化s的值。此后,在进入共享区之前调用P操作,出共享区后调用V操作,这就是信号量的思想。

在Linux下并没有直接的P&V函数,而是需要我们根据这几个基本的sem函数族进行封装:

正如其名,管道就如同生活中的一根管道,一端输送,而另一端接收,双方不需要知道对方,只需要知道管道就好了。

管道是一种最 基本的进程间通信机制。 管道由pipe函数来创建: 调用pipe函数,会在内核中开辟出一块缓冲区用来进行进程间通信,这块缓冲区称为管道,它有一个读端和一个写端。管道被分为匿名管道和有名管道。

匿名管道通过pipe函数创建,这个函数接收一个长度为2的Int数组,并返回1或0表示成功或者失败:

int pipe(int fd[2])

这个函数打开两个文件描述符,一个读端文件,一个写端,分别存入fd[0]和fd[1]中,然后可以作为参数调用 write 和 read 函数进行写入或读取,注意fd[0]只能读取文件,而fd[1]只能用于写入文件。

你可能有个疑问,这要怎么实现通信?其他进程又不知道这个管道,因为进程是独立的,其他进程看不到某一个进程进行了什么操作。

是的,‘其他’进程确实是不知道,但是它的子进程却可以!这里涉及到fork派生进程的相关知识,一个进程派生一个子进程,那么子进程将会复制父进程的内存空间信息,注意这里是复制而不是共享,这意味着父子进程仍然是独立的,但是在这一时刻,它们所有的信息又是相等的。因此子进程也知道该全局管道,并且也拥有两个文件描述符与管道挂钩,所以 匿名管道只能在具有亲缘关系的进程间通信。

还要注意,匿名管道内部采用环形队列实现,只能由写端到读端,由于设计技术问题,管道被设计为半双工的,一方要写入则必须关闭读描述符,一方要读出则必须关闭写入描述符。因此我们说 管道的消息只能单向传递。

注意管道是堵塞的,如何堵塞将依赖于读写进程是否关闭文件描述符。如果读管道,如果读到空时,假设此时写端口还没有被完全关闭,那么操作系统会假设还有数据要读,此时读进程将会被堵塞,直到有新数据或写端口被关闭;如果管道为空,且写端口也被关闭,此时操作系统会认为已经没有东西可读,会直接退出,并关闭管道。

对于写一个已经满了的管道同理而言。

管道内部由内核管理,在半双工的条件下,保证数据不会出现并发问题。

了解了匿名管道之后,有名管道便很好理解了。在匿名管道的介绍中,我们说其他进程不知道管道和文件描述符的存在,所以匿名管道只适用于具有亲缘关系的进程,而命名管道则很好的解决了这个问题 —— 现在管道有一个唯一的名称了,任何进程都可以访问这个管道。

注意,操作系统将管道看作一个抽象的文件,但管道并不是普通的文件,管道存在于内核空间中而不放置在磁盘(有名管道文件系统上有一个标识符,没有数据块),访问速度更快,但存储量较小,管道是临时的,是随进程的,当进程销毁,所有端口自动关闭,此时管道也是不存在的,操作系统将所有IO抽象的看作文件,例如网络也是一种文件,这意味着我们可以采用任何文件方法操作管道,理解这种抽象是很重要的,命名管道就利用了这种抽象。

Linux下,采用mkfifo函数创建,可以传入要指定的‘文件名’,然后其他进程就可以调用open方法打开这个特殊的文件,并进行write和read操作(那肯定是字节流对吧)。

注意,命名管道适用于任何进程,除了这一点不同外,其余大多数都与匿名管道相同。

消息队列亦称报文队列,也叫做信箱,是Linux的一种通信机制,这种通信机制传递的数据会被拆分为一个一个独立的数据块,也叫做消息体,消息体中可以定义类型与数据,克服了无格式承载字节流的缺陷(现在收到void*后可以知道其原本的格式惹):

同管道类似,它有一个不足就是每个消息的最大长度是有上限的,整个消息队列也是长度限制的。

内核为每个IPC对象维护了一个数据结构struct ipc_perm,该数据结构中有指向链表头与链表尾部的指针,保证每一次插入取出都是O(1)的时间复杂度。

一个进程可以发送信号给另一个进程,一个信号就是一条消息,可以用于通知一个进程组发送了某种类型的事件,该进程组中的进程可以采取处理程序处理事件。

Linux下 unistd.h 头文件下定义了如图中的常量,当你在shell命令行键入 ctrl + c 时,内核就会前台进程组的每一个进程发送 SIGINT 信号,中止进程。

我们可以看到上述只有30个信号,因此操作系统会为每一个进程维护一个int类型变量sig,利用其中30位代表是否有对应信号事件,每一个进程还有一个int类型变量block,与sig对应,其30位表示是否堵塞对应信号(不调用处理程序)。如果存在多个相同的信号同时到来,多余信号会被存储在一个等待队列中等待。

我们要理解进程组是什么,每个进程属于一个进程组,可以有多个进程属于同一个组。每个进程拥有一个进程ID,称为 pid ,而每个进程组拥有一个进程组ID,称为 pgid ,默认情况下,一个进程与其子进程属于同一进程组。

软件方面(诸如检测键盘输入是硬件方面)可以利用kill函数发送信号,kill函数接受两个参数,进程ID和信号类型,它将该信号类型发送到对应进程,如果该pid为0,那么会发送到属于自身进程组的所有进程。

接收方可以采用signal函数给对应事件添加处理程序,一旦事件发生,如果未被堵塞,则调用该处理程序。

Linux下有一套完善的函数用以处理信号机制。

Socket套接字是用与网络中不同主机的通信方式,多用于客户端与服务器之间,在Linux下也有一系列C语言函数,诸如socket、connect、bind、listen与accept,我们无需花太多时间研究这些函数,因为我们可能一辈子都不会与他们打交道,对于原理的学习,后续我会对Java中的套接字socket源码进行剖析。

对于工作而言,我们可能一辈子都用不上这些操作,但作为对于操作系统的学习,认识到进程间是如何通信还是很有必要的。

面试的时候对于这些方法我们不需要掌握到很深的程度,但我们必须要讲的来有什么通信方式,这些方式都有什么特点,适用于什么条件,大致是如何操作的,能说出这些,基本足以让面试官对你十分满意了。

最近对离线数仓体系进行了扩容和架构改造,也算是一波三折,出了很多小插曲,有一些改进点对我们来说也是真空地带,通过对比和模拟压测总算是得到了预期的结果,这方面尤其值得一提的是郭运凯同学的敬业,很多前置的工作,优化和应用压测的工作都是他完成的。 

整体来说,整个事情的背景是因为服务器硬件过保,刚好借着过保服务器替换的机会来做集群架构的优化和改造。 

1.集群架构改造的目标

在之前也总结过目前存在的一些潜在问题,也是本次部署架构改进的目标:

1)之前 的GP segment数量设计过度 ,因为资源限制,过多考虑了功能和性能,对于集群的稳定性和资源平衡性考虑有所欠缺,在每个物理机节点上部署了10个Primary,10个Mirror,一旦1个服务器节点不可用,整个集群几乎无法支撑业务。

2)GP集群 的存储资源和性能的平衡不够 ,GP存储基于RAID-5,如果出现坏盘,磁盘重构的代价比较高,而且重构期间如果再出现坏盘,就会非常被动,而且对于离线数仓的数据质量要求较高,存储容量相对不是很大,所以在存储容量和性能的综合之上,我们选择了RAID-10。

3)集 群的异常场景的恢复需要完善, 集群在异常情况下(如服务器异常宕机,数据节点不可用,服务器后续过保实现节点滚动替换)的故障恢复场景测试不够充分,导致在一些迁移和改造中,相对底气不足,存在一些知识盲区。

4)集群版本过 ,功能和性能上存在改进空间。毕竟这个集群是4年前的版本,底层的PG节点的版本也比较旧了,在功能上和性能上都有一定的期望,至少能够与时俱进。

5)操作系统版本升 ,之前的操作系统是基于CentOS6,至少需要适配CentOS 7 。

6)集群TPCH 压测验收 ,集群在完成部署之后,需要做一次整体的TPCH压测验收,如果存在明显的问题需要不断调整配置和架构,使得达到预期的性能目标。

此外在应用层面也有一些考虑,总而言之,是希望能够解决绝大多数的痛点问题,无论是在系统层面,还是应用层面,都能上一个台阶。

2.集群规划设计的选型和思考

明确了目标,就是拆分任务来规划设计了,在规划设计方面主要有如下的几个问题:

1)Greenplum的版本选择 ,目前有两个主要的版本类别,一个是开源版(Open Source distribution)和Pivotal官方版,它们的其中一个差异就是官方版需要注册,签署协议,在此基础上还有GPCC等工具可以用,而开源版本可以实现源码编译或者rpm安装,无法配置GPCC。综合来看,我们选择了 开源版本的6.16.2 ,这其中也询问了一些行业朋友,特意选择了几个涉及稳定性bug修复的版本。

2)数据集市的技术选型 ,在数据集市的技术选型方面起初我是比较坚持基于PostgreSQL的模式,而业务侧是希望对于一些较为复杂的逻辑能够通过GP去支撑,一来二去之后,加上我咨询了一些行业朋友的意见,是可以选择基于GP的方案,于是我们就抱着试一试的方式做了压测,所以数据仓库和和数据集市会是两个不同规模体量的GP集群来支撑。

3)GP的容量规划 ,因为之前的节点设计有些过度,所以在数量上我们做了缩减,每台服务器部署12个segment节点,比如一共12台服务器,其中有10台服务器是Segment节点,每台上面部署了6个Primary,6个Mirror,另外2台部署了Master和Standby,就是即(6+6)*10+2,整体的配置情况类似下面的模式。

4)部署架构方案选型 ,部署架构想起来比较容易,但是落实起来有很多的考虑细节,起初考虑GP的Master和Standby节点如果混用还是能够节省一些资源,所以设计的数据仓库和数据集市的部署架构是这样考虑的,但是从走入部署阶段之后,很快就发现这种交叉部署的模式是不可行的,或者说有一些复杂度。

除此之外,在单个GP集群的部署架构层面,还有4类方案考虑。

  方案1 :Master,Standby和segment混合部署

  方案2 :Master,Standby和segment独立部署,整个集群的节点数会少一些

  方案3 :Segment独立部署,Master,Standby虚拟机部署

  方案4 :最小化单节点集群部署(这是数据集市最保底的方案)  

这方面存在较大的发挥空间,而且总体来说这种验证磨合的成本也相对比较高,实践给我上了一课, 越是想走捷径,越是会让你走一些弯路 ,而且有些时候的优化其实我也不知道改怎么往下走,感觉已经无路可走,所以上面这4种方案其实我们都做了相关的测试和验证。

3.集群架构的详细设计和实践

1)设计详细的部署架构图

在整体规划之上,我设计了如下的部署架构图,每个服务器节点有6个Primary,6个Mirror,服务器两两映射。

2)内核参数优化

按照官方文档的建议和具体的配置情况,我们对内核参数做了如下的配置:

vm.swappiness=10

vm.zone_reclaim_mode = 0

vm.dirty_expire_centisecs = 500

vm.dirty_writeback_centisecs = 100

vm.dirty_background_ratio = 0 # See System Memory

vm.dirty_ratio = 0

vm.dirty_background_bytes = 1610612736

vm.dirty_bytes = 4294967296

vm.min_free_kbytes = 3943084

vm.overcommit_memory=2

kernel.sem = 500 2048000 200 4096

4.集群部署步骤

1)首先是配置/etc/hosts,需要把所有节点的IP和主机名都整理出来。 

2)配置用户,很常规的步骤

groupadd  gpadmin

useradd gpadmin -g gpadmin

passwd gpadmin

3)配置sysctl.conf和资源配置

4)使用rpm模式安装

# yum install -y apr apr-util bzip2 krb5-devel  zip

# rpm -ivh open-source-greenplum-db-6.16.2-rhel7-x86_64.rpm

5)配置两个host文件,也是为了后面进行统一部署方便,在此建议先开启gpadmin的sudo权限,可以通过gpssh处理一些较为复杂的批量操作

6)通过gpssh-exkeys来打通ssh信任关系,这里需要吐槽这个ssh互信,端口还得是22,否则处理起来很麻烦,需要修改/etc/ssh/sshd_config文件

gpssh-exkeys -f hostlist

7)较为复杂的一步是打包master的Greenplum-db-6.16.2软件,然后分发到各个segment机器中,整个过程涉及文件打包,批量传输和配置,可以借助gpscp和gpssh,比如gpscp传输文件,如下的命令会传输到/tmp目录下

gpscp -f /usr/local/greenplum-db/conf/hostlist /tmp/greenplum-db-6.16.2.tar.gz =:/tmp

或者说在每台服务器上面直接rpm -ivh安装也可以。

8)Master节点需要单独配置相关的目录,而Segment节点的目录可以提前规划好,比如我们把Primary和Mirror放在不同的分区。 

mkdir -p /data1/gpdata/gpdatap1

mkdir -p /data1/gpdata/gpdatap2

mkdir -p /data2/gpdata/gpdatam1

mkdir -p /data2/gpdata/gpdatam2

9)整个过程里最关键的就是gpinitsystem_config配置了,因为Segment节点的ID配置和命名,端口区间都是根据一定的规则来动态生成的,所以对于目录的配置需要额外注意。

10)部署GP集群最关键的命令是

gpinitsystem -c gpinitsystem_config -s 【standby_hostname】

其中文件gpinitsystem_config的主要内容如下:

MASTER_HOSTNAME=xxxx

declare -a DATA_DIRECTORY=(/data1/gpdata/gpdatap1  /data1/gpdata/gpdatap2 /data1/gpdata/gpdatap3 /data1/gpdata/gpdatap4 /data1/gpdata/gpdatap5 /data1/gpdata/gpdatap6)

TRUSTED_SHELL=ssh

declare -a MIRROR_DATA_DIRECTORY=(/data2/gpdata/gpdatam1  /data2/gpdata/gpdatam2 /data2/gpdata/gpdatam3 /data2/gpdata/gpdatam4 /data2/gpdata/gpdatam5 /data2/gpdata/gpdatam6)

MACHINE_LIST_FILE=/usr/local/greenplum-db/conf/seg_hosts

整个过程大约5分钟~10分钟以内会完成,在部署过程中建议要查看后端的日志查看是否有异常,异常情况下的体验不是很好,可能会白等。

5.集群部署问题梳理

集群部署中还是有很多细节的问题,太基础的就不提了,基本上就是配置,目录权限等问题,我提另外几个:

1) 资源配置问题 ,如果/etc/security/limits.conf的资源配置不足会在安装时有如下的警告:

2) 网络问题 ,集群部署完成后可以正常操作,但是在查询数据的时候会抛出错误,比如SQL是这样的,看起来很简单:select count(*) from customer,但是会抛出如下的错误:

这个问题的主要原因还是和防火墙配置相关,其实不光需要配置INPUT的权限,还需要配置OUTPUT的权限。 

对于数据节点可以开放略大的权限,如:

入口的配置:

-A INPUT -p all -s xxxxx    -j ACCEPT

出口的配置:

-A OUTPUT -p all -s xxxxx    -j ACCEPT

3)网络配置问题 ,这个问题比较诡异的是,报错和上面是一样的,但是在排除了防火墙配置后,select count(*) from customer;这样的语句是可以执行的,但是执行的等待时间较长,比如表lineitem这表比较大,过亿的数据量,,在10个物理节点时,查询响应时间是10秒,但是4个物理节点,查询响应时间是在90秒,总体删感觉说不过去。

为了排查网络问题,使用gpcheckperf等工具也做过测试,4节点和10节点的基础配置也是相同的。

gpcheckperf -f /usr/local/greenplum-db/conf/seg_hosts -r N -d /tmp

$ cat /etc/hosts

127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4

::1      localhost localhost.localdomain localhost6 localhost6.localdomain6

#127.0.0.1    test-dbs-gp-128-230

xxxxx.128.238 test-dbs-gp-svr-128-238

xxxxx.128.239 test-dbs-gp-svr-128-239

其中127.0.0.1的这个配置在segment和Master,Standby混部的情况是存在问题的,修正后就没问题了,这个关键的问题也是郭运凯同学发现的。

5.集群故障恢复的测试

集群的故障测试是本次架构设计中的重点内容,所以这一块也是跃跃欲试。

整体上我们包含两个场景,服务器宕机修复后的集群恢复和服务器不可用时的恢复方式。

第一种场景相对比较简单,就是让Segment节点重新加入集群,并且在集群层面将Primary和Mirror的角色互换,而第二种场景相对时间较长一些,主要原因是需要重构数据节点,这个代价基本就就是PG层面的数据恢复了,为了整个测试和恢复能够完整模拟,我们采用了类似的恢复方式,比如宕机修复使用了服务器重启来替代,而服务器不可用则使用了清理数据目录,类似于一台新配置机器的模式。

1)服务器宕机修复后集群恢复

select * from gp_segment_configuration where status!='u'

gprecoverseg  -o ./recov

gprecoverseg -r

select * from gp_segment_configuration where status='u'

2)服务器不可用时集群恢复

重构数据节点的过程中,总体来看网络带宽还是使用很充分的。

select * from gp_segment_configuration where status='u'

select * from gp_segment_configuration where status='u' and role!=preferred_role

gprecoverseg -r

select * from gp_segment_configuration where status='u' and role!=preferred_role

经过测试,重启节点到数据修复,近50G数据耗时3分钟左右

6.集群优化问题梳理

1)部署架构优化和迭代

对于优化问题,是本次测试中尤其关注,而且争议较多的部分。 

首先在做完初步选型后,数仓体系的部署相对是比较顺利的,采用的是第一套方案。

数据集市的集群部分因为节点相对较少,所以就选用了第二套方案

实际测试的过程,因为配置问题导致TPCH的结果没有达到预期。

所以这个阶段也产生了一些疑问和怀疑,一种就是折回第一种方案,但是节点数会少很多,要不就是第三种采用虚拟机的模式部署,最保底的方案则是单节点部署,当然这是最牵强的方案。

这个阶段确实很难,而在上面提到的修复了配置之后,集群好像突然开悟了一般,性能表现不错,很快就完成了100G和1T数据量的TPCH测试。

在后续的改造中,我们也尝试了第三套方案,基于虚拟机的模式,通过测试发现,远没有我们预期的那么理想,在同样的数据节点下,Master和Standby采用物理机和虚拟机,性能差异非常大,这个是出乎我们预料的。比如同样的SQL,方案3执行需要2秒,而方案2则需要80秒,这个差异我们对比了很多指标,最后我个人理解差异还是在网卡部分。

所以经过对比后,还是选择了方案2的混合部署模式。

2)SQL性能优化的分析

此外整个过程的TPCH也为集群的性能表现提供了参考。比如方案2的混合部署模式下,有一条SQL需要18秒,但是相比同类型的集群,可能就只需要2秒钟左右,这块显然是存在问题的。 

在排除了系统配置,硬件配置的差异之后,经典的解决办法还是查看执行计划。

性能较差的SQL执行计划:

# explain analyze select count(*)from customer

QUERY PLAN   

Aggregate  (cost=0.00..431.00 rows=1 width=8) (actual time=24792.916..24792.916 rows=1 loops=1)

   ->  Gather Motion 36:1  (slice1segments: 36)  (cost=0.00..431.00 rows=1 width=1) (actual time=3.255..16489.394 rows=150000000 loops=1)

         ->  Seq Scan on customer  (cost=0.00..431.00 rows=1 width=1) (actual time=0.780..1267.878 rows=4172607 loops=1)

Planning time: 4.466 ms

   (slice0)    Executor memory: 680K bytes.

   (slice1)    Executor memory: 218K bytes avg x 36 workers, 218K bytes max (seg0).

Memory used:  2457600kB

Optimizer: Pivotal Optimizer (GPORCA)

Execution time: 24832.611 ms

(9 rows)

Time: 24892.500 ms

性能较好的SQL执行计划:

# explain analyze select count(*)from customer                            

QUERY PLAN

Aggregate  (cost=0.00..842.08 rows=1 width=8) (actual time=1519.311..1519.311 rows=1 loops=1)

   ->  Gather Motion 36:1  (slice1segments: 36)  (cost=0.00..842.08 rows=1 width=8) (actual time=634.787..1519.214 rows=36 loops=1)

         ->  Aggregate  (cost=0.00..842.08 rows=1 width=8) (actual time=1473.296..1473.296 rows=1 loops=1)

               ->  Seq Scan on customer  (cost=0.00..834.33 rows=4166667 width=1) (actual time=0.758..438.319 rows=4172607 loops=1)

Planning time: 5.033 ms

   (slice0)    Executor memory: 176K bytes.

   (slice1)    Executor memory: 234K bytes avg x 36 workers, 234K bytes max (seg0).

Memory used:  2457600kB

Optimizer: Pivotal Optimizer (GPORCA)

Execution time: 1543.611 ms

(10 rows)

Time: 1549.324 ms

很明显执行计划是被误导了,而误导的因素则是基于统计信息,这个问题的修复很简单:

analyze customer

但是深究原因,则是在压测时,先是使用了100G压测,压测完之后保留了原来的表结构,直接导入了1T的数据量,导致执行计划这块没有更新。

3)集群配置优化

此外也做了一些集群配置层面的优化,比如对缓存做了调整。 

gpconfig -c statement_mem -m 2457600 -v 2457600

gpconfig -c gp_vmem_protect_limit -m 32000 -v 32000

7.集群优化数据

最后来感受下集群的性能:

1)10个物理节点,(6+6)*10+2

tpch_1t=# iming on

Timing is on.

tpch_1t=# select count(*)from customer

   count   

-----------

150000000

(1 row)

Time: 1235.801 ms

tpch_1t=# select count(*)from lineitem

   count    

------------

5999989709

(1 row)

Time: 10661.756 ms

2)6个物理节点,(6+6)*6

# select count(*)from customer

   count   

-----------

 150000000

(1 row)

Time: 1346.833 ms

# select count(*)from lineitem

   count    

------------

 5999989709

(1 row)

Time: 18145.092 ms

3)4个物理节点,(6+6)*4

# select count(*)from customer

   count   

-----------

 150000000

(1 row)

Time: 1531.621 ms

# select count(*)from lineitem

   count    

------------

 5999989709

(1 row)

Time: 25072.501 ms

4)TPCH在不通架构模式下的性能比对 ,有19个查询模型,有个别SQL逻辑过于复杂暂时忽略,也是郭运凯同学整理的列表。

在1T基准下的基准测试表现:

write的奥秘

在Linux下我们在使用设备的时候,都会用到write这个函数,通过这个函数我们可以象使

用文件那样向设备传送数据。可是为什么用户使用write函数就可以把数据写到设备里面

去,这个过程到底是怎么实现的呢?

这个奥秘就在于设备驱动程序的write实现中,这里我结合一些源代码来解释如何使得一

个简简单单的write函数能够完成向设备里面写数据的复杂过程。

这里的源代码主要来自两个地方。第一是oreilly出版的《Linux device driver》中的

实例,第二是Linux Kernel 2.2.14核心源代码。我只列出了其中相关部分的内容,如果

读者有兴趣,也可以查阅其它源代码。不过我不是在讲解如何编写设备驱动程序,所以不

会对每一个细节都进行说明,再说有些地方我觉得自己还没有吃透。

由于《Linux device driver》一书中的例子对于我们还是复杂了一些,我将其中的一个

例程简化了一下。这个驱动程序支持这样一个设备:核心空间中的一个长度为10的数组

kbuf[10]。我们可以通过用户程序open它,read它,write它,close它。这个设备的名

字我称为short_t。

现在言归正传。

对于一个设备,它可以在/dev下面存在一个对应的逻辑设备节点,这个节点以文件的形式

存在,但它不是普通意义上的文件,它是设备文件,更确切的说,它是设备节点。这个节

点是通过mknod命令建立的,其中指定了主设备号和次设备号。主设备号表明了某一类设

备,一般对应着确定的驱动程序;次设备号一般是区分是标明不同属性,例如不同的使用

方法,不同的位置,不同的操作。这个设备号是从/proc/devices文件中获得的,所以一

般是先有驱动程序在内核中,才有设备节点在目录中。这个设备号(特指主设备号)的主

要作用,就是声明设备所使用的驱动程序。驱动程序和设备号是一一对应的,当你打开一

个设备文件时,操作系统就已经知道这个设备所对应的驱动程序是哪一个了。这个"知道"

的过程后面就讲。

我们再说说驱动程序的基本结构吧。这里我只介绍动态模块型驱动程序(就是我们使用

insmod加载到核心中并使用rmmod卸载的那种),因为我只熟悉这种结构。

模块化的驱动程序由两个函数是固定的:int init_module(void) ;void

cleanup_module(void)。前者在insmod的时候执行,后者在rmmod的时候执行。

init_nodule在执行的时候,进行一些驱动程序初始化的工作,其中最主要的工作有三

件:注册设备;申请I/O端口地址范围;申请中断IRQ。这里和我们想知道的事情相关的只

有注册设备。

下面是一个典型的init_module函数:

int init_module(void){

int result = check_region(short_base,1);/* 察看端口地址*/

……

request_region(short_base,1,"short")/* 申请端口地址*/

……

result = register_chrdev(short_major, "short", &short_fops)/* 注册设备

*/

……

result = request_irq(short_irq, short_interrupt, SA_INTERRUPT, "short",

NULL)/* 申请IRQ */

……

return 0

}/* init_module*/

上面这个函数我只保留了最重要的部分,其中最重要的函数是

result = register_chrdev(short_major, "short", &short_fops)

这是一个驱动程序的精髓所在!!当你执行indmod命令时,这个函数可以完成三件大事:

第一,申请主设备号(short_major),或者指定,或者动态分配;第二,在内核中注册设

备的名字("short");第三,指定fops方法(&short_fops)。其中所指定的fops方法就是

我们对设备进行操作的方法(例如read,write,seek,dir,open,release等),如何实现

这些方法,是编写设备驱动程序大部分工作量所在。

现在我们就要接触关键部分了--如何实现fops方法。

我们都知道,每一个文件都有一个file的结构,在这个结构中有一个file_operations的

结构体,这个结构体指明了能够对该文件进行的操作。

下面是一个典型的file_operations结构:

struct file_operations {

loff_t (*llseek) (struct file *, loff_t, int)

ssize_t (*read) (struct file *, char *, size_t, loff_t *)

ssize_t (*write) (struct file *, const char *, size_t, loff_t *)

int (*readdir) (struct file *, void *, filldir_t)

unsigned int (*poll) (struct file *, struct poll_table_struct *)

int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned

long)

int (*mmap) (struct file *, struct vm_area_struct *)

int (*open) (struct inode *, struct file *)

int (*flush) (struct file *)

int (*release) (struct inode *, struct file *)

int (*fsync) (struct file *, struct dentry *)

int (*fasync) (int, struct file *, int)

int (*check_media_change) (kdev_t dev)

int (*revalidate) (kdev_t dev)

int (*lock) (struct file *, int, struct file_lock *)

}

我们可以看到它实际上就是许多文件操作的函数指针,其中就有write,其它的我们就不

去管它了。这个write指针在实际的驱动程序中会以程序员所实现的函数名字出现,它指

向程序员实现的设备write操作函数。下面就是一个实际的例子,这个write函数可以向核

心内存的一个数组里输入一个字符串。

int short_write (struct inode *inode, struct file *filp, const char *buf,

int count){

int retval = count

extern unsigned char kbuf[10]

if(count>10)

count=10

copy_from_user(kbuf, buf, count)

return retval

}/* short_write */

设备short_t对应的fops方法是这样声明的:

struct file_operations short_fops = {

NULL, /* short_lseek */

short_read,

short_write,

NULL, /* short_readdir */

NULL, /* short_poll */

NULL, /* short_ioctl */

NULL, /* short_mmap */

short_open,

short_release,

NULL, /* short_fsync */

NULL, /* short_fasync */

/* nothing more, fill with NULLs */

}

其中NULL的项目就是不提供这个功能。所以我们可以看出short_t设备只提供了

read,write,open,release功能。其中write功能我们在上面已经实现了,具体的实现函

数起名为short_write。这些函数就是真正对设备进行操作的函数,这就是驱动程序的一

大好处:不管你实现的时候是多么的复杂,但对用户来看,就是那些常用的文件操作函数。

但是我们可以看到,驱动程序里的write函数有四个参数,函数格式如下:

short_write (struct inode *inode, struct file *filp, const char *buf, int count)

而用户程序中的write函数只有三个参数,函数格式如下:

write(inf fd, char *buf, int count)

那他们两个是怎么联系在一起的呢?这就要靠操作系统核心中的函数sys_write了,下面

是Linux Kernel 2.2.14中sys_write中的源代码:

asmlinkage ssize_t sys_write(unsigned int fd, const char * buf, size_t count)

{

ssize_t ret

struct file * file

struct inode * inode

ssize_t (*write)(struct file *, const char *, size_t, loff_t *)/* 指向

驱动程序中的wirte函数的指针*/

lock_kernel()

ret = -EBADF

file = fget(fd)/* 通过文件描述符得到文件指针 */

if (!file)

goto bad_file

if (!(file->f_mode &FMODE_WRITE))

goto out

inode = file->f_dentry->d_inode/* 得到inode信息 */

ret = locks_verify_area(FLOCK_VERIFY_WRITE, inode, file, file->f_pos,

count)

if (ret)

goto out

ret = -EINVAL

if (!file->f_op || !(write = file->f_op->write)) /* 将函数开始时声明的

write函数指针指向fops方法中对应的write函数 */

goto out

down(&inode->i_sem)

ret = write(file, buf, count, &file->f_pos)/* 使用驱动程序中的write函数

将数据输入设备,注意看,这里就是四个参数了 */

up(&inode->i_sem)

out:

fput(file)

bad_file:

unlock_kernel()

return ret

}

我写了一个简单的程序来测试这个驱动程序,该程序源代码节选如下(该省的我都省了):

main(){

int fd,count=0

unsigned char buf[10]

fd=open("/dev/short_t",O_RDWR)

printf("input string:")

scanf("%s",buf)

count=strlen(buf)

if(count>10)

count=10

count=write(fd,buf,count)

close(fd)

return 1

}

现在我们就演示一下用户使用write函数将数据写到设备里面这个过程到底是怎么实现的:

1,insmod驱动程序。驱动程序申请设备名和主设备号,这些可以在/proc/devIEces中获得。

2,从/proc/devices中获得主设备号,并使用mknod命令建立设备节点文件。这是通过主

设备号将设备节点文件和设备驱动程序联系在一起。设备节点文件中的file属性中指明了

驱动程序中fops方法实现的函数指针。

3,用户程序使用open打开设备节点文件,这时操作系统内核知道该驱动程序工作了,就

调用fops方法中的open函数进行相应的工作。open方法一般返回的是文件标示符,实际

上并不是直接对它进行操作的,而是有操作系统的系统调用在背后工作。

4,当用户使用write函数操作设备文件时,操作系统调用sys_write函数,该函数首先通

过文件标示符得到设备节点文件对应的inode指针和flip指针。inode指针中有设备号信

息,能够告诉操作系统应该使用哪一个设备驱动程序,flip指针中有fops信息,可以告诉

操作系统相应的fops方法函数在那里可以找到。

5,然后这时sys_write才会调用驱动程序中的write方法来对设备进行写的操作。

其中1-3都是在用户空间进行的,4-5是在核心空间进行的。用户的write函数和操作系统

的write函数通过系统调用sys_write联系在了一起。

注意:

对于块设备来说,还存在写的模式的问题,这应该是由GNU C库来解决的,这里不予讨

论,因为我没有看过GNU C库的源代码。


欢迎分享,转载请注明来源:夏雨云

原文地址:https://www.xiayuyun.com/zonghe/231325.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2023-04-09
下一篇2023-04-09

发表评论

登录后才能评论

评论列表(0条)

    保存