本文分享的内容是如何参与贡献PolarDB for PostgreSQL开源。
说起开源,近年来的开源项目越来越多,开源也越来越热,那是什么原因呢?每个人都有自己的想法,我个人认为随着软件行业的发展,无论是软件的消费端还是生产端,都有对软件产品的诉求。
软件的消费端也就是我们的用户,他希望的是产品是一个长久的产品,不会因为某些原因停止服务,没有人维护了,也希望是软件产品的后续功能能得到继续支持。
作为软件的开发人员,也就是我们的生产端,他更希望的是我们能开发出更多、更新、更炫的一些功能,并能快速地分享给大家。
而开源正是给他们提供了一个非常好的温床,一个好的开源项目离不开大家的支持和贡献,更离不开好的开源管理。
而作为开源的参与者,也就很想知道如何能快速高效地参与进来,并开发出符合开源标准的高质量代码。本文将简单讲一下PolarDB for PostgreSQL开源项目开发环境的快速搭建以及一些开发的规范,最后用一个案例来演示一下如何快速参与到我们的贡献中。
一、开发环境搭建
作为开发人员,最想有的就是一个比较适合自己的开发环境,就像一个砍树的人需要一个锋利的斧头一样,使得自己能够快速地投入到工作中,而不是为其他繁杂的问题所影响,正所谓欲善其事必先利其器。
(一)开发环境搭建—系统环境搭建与依赖安装
开源环境搭建分成两部分,一部分是讲一下系统环境搭建,虽然我们在开源项目里已经提供了Docker版本的快速部署,但是还是需要给大家介绍里边具体做了什么,因为作为开发人员,如果要对这里面的内容进行开发和维护,肯定要知道里边具体都做了什么。
由于我们项目是基于PostgreSQL(后面简称为PG)开发,如果以前参与过PG的开发,基于对PG的了解,大家都知道PG是一个基于C语言开发的。我们这里首先需要安装一些C语言的编译环境以及一些依赖库,同时为了方便我们后期调试使用以及代码编辑,也需要安装一些其他的工具,比如git。这个git实际上是我们的一个代码管理工具,可以快速获取到最新代码,GDB用来调试debug我们的代码,vim可以进行一个快速的编辑,还有SSH服务等。SSH服务在集群部署中会使用到,然后安装一些PolarDB的依赖,这些依赖是后续一些编译工作能够顺利展开的重要保障。
比如说,我们这里提到的bison和flex,如果我们后期开发过程中需要添加新的SQL语法或者是对现有语法进行修改,肯定就需要安装这些依赖,为什么呢?因为我们的语法解析器正是基于两个工具进行开发的。
(二)开发环境搭建—源码获取与编译安装
下面介绍一下代码。
我们的代码是托管在GitHub上,可以通过前面安装的git管理工具git clone,快速获取最新的代码,同时也欢迎大家去浏览GitHub项目地址,时刻可以看到开源的最新动态以及最新讨论。
通过git clone拿到最新代码之后,我们首先要考虑的工作就是进行编译。
前面讲到装了很多依赖,这里我们的编译工作会顺利很多。我们提供了一个build.sh的快速编译脚本,通过这个脚本可以快速进行编译安装,然后它的默认安装位置就会安装到HOME目录下的polardb子目录以及polardbhome的子目录下。
这个脚本都做了什么?实际上做了三件事,就是我们的configure,用来检测依赖环境,然后生成make file,然后是我们的一些 make,进行编译,然后就是make install进行安装。
对于我们这种开发人员来讲,更关心的肯定是一些我们的自己手动编译安装的步骤,因为对于这种编译的环境,我们肯定有根据自己的这开发要求进行修改,这里提供了一些简单的手动编译过程,通过configure命令,指定几个参数来进行configure。
Configure里比如“-O0”,可以通过这种“-O0”让编译器不要去优化一些我们的代码,这样在debug过程中可以得到更多有效的信息,enable-debug实际上是在我们编译的命令上加了一些参数,比如加了个-g的参数,这样也可以在debug过程中提供代码详细的文字信息。
enable-tap-tests实际上是一个基于TAP测试框架,因为我们的产品的测试分成两部分,一部分可以认为是内核功能的测试,还有一部分功能可以认为是一些周边的工具,包括一些插件的测试,有一些周边的工具和插件,它就用了基于perl这种tap的测试框架,我们在这里面支持它,方便后期的测试。
对于Python需要支持,因为我们有些代码是基于Python开发,prefix参数指定了默认的安装目录,刚才上面提到,如果通过build.sh去进行编译安装,它指定的一个目录,我们根据自己的需要,根据自己开发的目录管理可以自己指定一个目录,然后编译DMA高可用的一个功能,我们就照着对应的目录进行编译即可。然后回到code主目录进行编译,进行安装。
最后是contrib目录,我们所有的插件都放在这个目录下面进行编译和安装。到此为止,开发环境搭建已经初步完成,可以进行编译代码的debug调试,包括编译等工作都可以在这上面运行了。
有些同学有自己的一些习惯提升开发环境,大家可以根据自己的喜好安装,比如Eclipse、Source Inside、VScode等,在这里就不做具体介绍。
(三)开发环境搭建—regression 测试
我们拿到了一个环境编译,安装完之后,想快速验证一下这个产品是否好用,或者怎么快速知道产品的一些特性功能。
我们提供了一个测试框架,这种测试框架实通过make check、make check-world,然后make checkdma、make check-world-dma这几个命令可以快速对产品内核的功能进行一些测试验证,包括一些周边工具的测试验证。比如make check-world,check-world,可以看到它是在5个目录下面进行分别的进行make check,如果看到它的make file是怎么写,在这么一个make check的过程中,实际上不仅它包含了上面提到的make check,make check只是测试我们内核功能。
刚才前面也提到了,测试功能主要分成内核功能还有一些设施周边的工具和插件,这种make check-world就可以测试到所有的这些模块上,它通过这种方式也包含了核心功能的测试,执行一个make check-world,它时间会稍微长一点,但是它可以做一个全量测试。
我们这个开源版本里面提供了make checkdma,这个checkdma是什么?实际上前面提到这是支持DMA高可用的一个版本,这种高可用的是一个集群版,它是三节点集群。前面像make check,make check-world,它是一个单节点模式下的运行的测试,我们想测试一下在集群下的所有测试是否通过,我们提供了一个快速的命令 make checkdma,那也可以通过make checkdma,类似于make check-world功能一样,它区别在于启动每个测试的时候都是启动一个集群的测试,而不是一个单节点测试。
二、开发规范
下面介绍一下开发规范。
大家都知道开发规范对于平时开发工作有重要的作用,但是开发人员平时对开发规范可能关注得不够,但是作为开源项目,开发规范是显得尤为重要。
大家都知道这些年PG社区发展非常快,每年有大版本的发布,他们实际上就是通过一些标准的开发规范,包括一些制度,然后能联合全世界各个国家无数优秀的开发人员,使其通过协同开发来开发出高质量的代码。
所以开发规范在开源项目中就好比一套标准,只要大家做出来的东西符合这套标准,大家就有了共同沟通的语言,它是一个开源项目能否良好持续发展的关键之一,我们的PolarDB-for-PostgreSQL依然坚持这套标准。
下面将简单介绍一下经常使用的一些规范,希望这些规范能帮助大家在开发过程中更得心应手,提升开发的代码质量,更加符合一些国际规范,为自己以后的发展提供一个好的支撑。
(一)编码习惯—C语言格式规定
由于我们的代码大部分都是用C语言开发,因此这里主要介绍一些C语言的开发规范。
首先是缩进,可能大家在过去的开发过程中被告知不要使用这种制表服务,就是我们Tab键。实际上PG要求使用tab键,而且是用4列制表符,大家使用时要遵循这个标准,因为所有的代码都是基于这个标准做的。
布局规则,比如大括号的位置信息怎么放置,它要遵循BSD习惯,特别是一些if、while、switch等受控块的花括号,受控块的花括号要独自占一行。
还有是限制行长度,比如每行的长度建议在80列宽度上,这个是为了代码的可读性,如果代码写得太长,可能不利于代码的阅读。那是否就不能超过80列?也不是,比如说我们的一些 errormsg可能会写得很长,如果为了80列的宽度限制而在任意位置进行切断,就会影响可读性,所以目的还是为了可读性。在保证最大可读性的情况下,我们要尽量保证80列的宽度限制。
除此之外,不要使用C++的一些注释风格,比如说双斜杠,还有C编译器,实际上是不接受这种注释的。出于相同的原因,我们也不推荐使用C++的扩展,比如在一个控制块中的一些变量的声明,比如说for语句,for语句如果在控制块里边,声明一个变量I,在这里边是不允许的,需要把这个声明变量拿到for控制框外面的。然后要follow一下注释的模式,通过这种多行注释块去填注释。
(二)编码习惯—如何报错
下面主要讲一下我们的报错信息。
作为开发人员,知道输出日志信息是我们和软件用户之间一个重要的交流手段,输出的正确日志信息尤其重要,为什么呢?
因为它告诉客户我们是否专业,如果写的信息很随意的,客户看到这个软件注册信息的时候,不论你是否写了一个非常优秀的算法或功能,如果你的信息写的不够专业,从用户的角度来看,他会认为这个软件产品不够专业。因此,往往我们忽略的东西可能正是一个非常关键的东西。
产品里报错信息主要通过两个函数来实现的,分别是ereport和elog。
首先讲一下ereport,可以看到ereport这个语句主要分三部分, error就是我们的严重级别,errcode就是一些错误代码,最后是errmsg。
严重级别根据PG里边的一些规范,我们这里规定是用DEBUG5到DEBUG1,包括我们的NOTICED、WARNING、ERROR、FATAL、PANIC,随着级别越来越高,对于客户的信息产生了不同的重要程度,也可以通过我们的配置文件设置给客户输出什么级别的信息。
如果是error或者更高,需要注意的是如果严重级别没有设置,或者说严重级别设置了error或者更高ereport,会终止用户定义函数的执行,并且不会返回到调用者。如果是严重级别低于error,那ereport会正常返回。
errcode指定对于该情况的一些错误标识代码,这里的错误代码是一般是由5个字符组成,这5个字符包含了一些数字和大写字母,它表示多种错误和警告情况的代码,所以它也是分层次的。
前两位一个是错误的总分类,后三个字符表示一个情况的子类,代码5个0,“00000”代表着一个成功的状态,这可以帮助我们快速定位问题的类型,也大概知道debug的方向在哪里。如果没有指定errcode错误严重性,特别是error或者更高的错误标识,会默认使用ERRCODE_INTERNAL_ERROR 。如果错误级别是warning标识符的这种, errcode会默认使用ERRCODE_WARNING 。
如果错误严重级别是在notice以及以下,就会使用ERRCODE_SUCCESSFUL_COMPLETION 这么一个errcode,这是一个默认的情况,如果没有设置errcode的话。
对于我们来讲,有的时候不用errcode是一个很方便的情况,但是如果考虑到不用errcode的时候,要很清楚地知道自己不用的原因是什么,考虑一下它们是否合适。
剩下就是errmsg 。errmsg主要是提供消息主体,就是要报错的信息。然后就是ereport,ereport这里边实际上还有很多辅助函数,就里提到了我们的errcode(),errmsg()这里边都是辅助函数,还有很多其他的辅助函数,这些辅助函数大家可以在PG的官网上进行查看。
下面讲第二种错误代码,就是说我们elog。
elog的作用是什么?它是一个旧有的模式,它可以等效于 ereport这种模式,可以看到它提供了level,错误级别是一样的,但它没有提供errcode,就跟刚才讲到的,根据严重级别提供默认的errcode。然后消息通过一个辅助函数errmsg_internal() 去show出来,过程和刚才讲到的ereport里面的errmsg是不同的,errmsg()根据地域设置设定,比如它可以翻译成对应国家的语言,比如说翻译成汉语,实际上errmsg_internal()这个语言就不会受翻译的限制,它可以自动把原的语言打印出来。
为什么还要保留这种旧有的模式呢?因为它足够简洁,在有一些内部的错误,比如说PG内核一些内部错误的时候,这种错误实际上它并不是给用户show出来的,不是用户感兴趣的这些错误,这些错误可以用这种简洁的模式进行打印,这种方式是非常方便的,因此也得到了保留。
(三)编码习惯—错误消息如何撰写
下面讲讲我们消息的怎么输出,这也是很关键的一个步骤。
错误消息通常分成三部分,主要的错误消息是通过简单概要的一个介绍是什么样的一个错误,如果简单概要的这个信息不够完整,或者不能够足够表达出具体的错误,也可以通过添加Detail信息,把具体的一些细节打印出来。
同时,如果你提供了比如对错误的一些修复建议,可以通过这种Hint的方式进行加入,比如可以提供一下这个东西是不是文件不见了,可以去做一些文件添加等提示信息。
然后就是引号,因为在引用时,英文文本应该都使用双引号,我们通常总是用这个引号界定文件名,包括用户提供的一些标识服务,以及其他可能包含词的一些变量,不要用他们标记那些不包含词的那种变量。为什么要这么做?实际上因为我们有一些对象在插入到信息中,它是会产生歧义的,如果不用双引号,那就产生了歧义,把它插到这个消息中,通过双引号可以避免歧义。
还有就是语法和标点,主要错误信息,例如通常第一个字母不要大写,不要用一个句点结束一个消息,甚至不要考虑用一个感叹号结束一个消息。
关于详细和提示信息,尽量要使用完整的句子,并且每一句都要用句点结束,对句子的第一个词进行首字母大写,如果后面跟着另一个句子,在句号后面放两个空格。
为什么有这些具体的建议或规范,比如主要消息为什么不要大写?第一个字母怎么不要大写?因为这些消息对于用户来讲,他会把你的信息很方便插入到各种各样语法的上下文中,如果用完整句子可能就不方便。因为通常来讲,主要消息通常不是一个语法上完整的句子,如果它是一个很长的句子,尽量把它拆分成一个主要部分和一个详细部分。不过详细和提示消息会很长,通常需要包括多个句子,为了一致,即使它们只是一个句子,也应该遵循完整句子的风格,我们要求它的首字母大写,每个句都句点结束。
关于大写和小写方面,对消息使用小写的这种形式,主要错误消息里面第一个字母也要小写,如果是SQL命令,一些关键词出现在消息中,请为它们使用大写,因为这也是为了明确告诉大家这些信息。然后避免要被动态,尽量使用主动语态,在有主语时使用完整的句子,比如说类似于“A could not do B”,如果主语是程序本身,请使用没有主语的电报风格,但不要为程序加上“I”。因为程序并不是人,所以不需要去假装。
现在时和过去时,如果一次尝试做某事失败,但是可能下一次就成功了,就使用过去式,如果失败必定是持久的,那就使用现在时。比如下面这两个句“could not open”和“cannot open”有什么区别?第一个表示打开文件尝试失败,该消息应该给出一个具体的原因,比如说磁盘满了或者是文件不存在,过去时更合适,因为下一次磁盘可能就不满了,或者是文件就存在了。第二种形式表示打开所提及的文件的功能在程序中根本就不存在,或者概念上是不可能的,现在时更合适,因为在该种情况下将无限保持,就是这种错误的无限保持,不可能因为这个文件存在。因为你的功能没有,即使文件存在,也没有这个功能,为什么要这样呈现?普通用户没有能力从消息的时态上得出重要的结论的,但是由于语言给我们提供了语法,就应该正确地使用它。
关于对象类型,在引用一个对象名称时,说明该对象的类型,因为如果不说明它就没有人会了解这个对象是什么类型,比如 “foo.bar.baz”具体是什么东西,我们需要给它一个具体说明。
然后是组装错误消息。比如有一些消息是在别处生成的,这种产生的文本怎么嵌入到我们消息里面。消息总是要说明为什么错误会发生,如果没有已知的原因,最好去修复这个代码。比如一个错误的实例 “could not open file”,这种没有给的原因,没有给原因就不确定错误的信息是怎么产生的。最好的是说比如 “could not open file”,然后改给它一个I/O failure,是因为I/O错误导致的。
不要在消息中使用函数名,可能大家开发过程中经常会在日志里用到这个函数名,但是日志写函数名实际上对用户来讲是没有任何作用的,因为他不清楚你在里边实现什么样的,请大家把函数名保留在debug阶段,最后提交的时候还是要把函数名去掉,写成一个合规的错误消息。
避免缩略语,比如尽量不要使用“can't”,应该使用“cannot”。避免使用“Unable、Bad、Illegal、Unknown”这种单词,为什么?比如说unable,它更接近于被动态,使用“cannot”等会更好。比如bad result这种错误信息,实际上很难从错误信息中知道到底是什么东西,最好是写出一个结果不好原因,比如Invalid format,是一个无效的格式。Illegal表示违反法律,最好是写成Invalid,如果说明为什么无效就更好了。尝试避免Unknown,为什么?比如说Unknown responds,如果你不知道响应是什么,怎么知道它是错误的?应该使用Unrecognized,相当于没有识别或这是一个更好的选择。
Find and Exists,找到还是存在的问题。如果是使用了某个算法去查找,而算法执行失败了,就用无法找到更好,如果是已知的资源位置,但是程序无法去找到它,那不存在会更合适一点。
(四)编码习惯—其他编码习惯
下面介绍一下其他的一些编码习惯。
比如我们在C语言里一些宏和内联函数,因为带有参数的宏和内联函数在代码中经常会被使用,当使用宏时有多次计算风险的时候,就推荐使用后者,如果是其他情况,只能尽量去使用宏,因为宏使用起来会更容易一点。
信号处理函数是一个很重要的部分,因为除了一些特殊的代码,信号处理函数处理不好会导致程序混乱,在信号处理器中,只应该调用那些对异步信息安全的函数,比如在处理信号的时候去操作一个锁,这个锁是在外部调用的,已经锁上了,此时信号处理函数中要去加这个锁可能会发生什么,大家清楚。在这里推荐只是提示一下这个信号已经到达,并用一个 latch 去唤醒信号处理器之外的一些代码,比如记录一下这是一个sigh up信号,同时去唤醒一些其他代码去执行。
还有就是调用函数指针,如果函数指针是个简单的变量,调用的时候推荐显式调用,虽然不加也是会有效的。
(五)编码习惯—其他语言编码规范
如上所示,还有一些其他的开发规范。因为我们不仅有C语言,还有其他的语言,大家自行参考上方地址的信息,参考对应的代码规范。
三、开发示例
下面将简单用一个实例验证,告诉大家如何快速加入到我们的开源项目中。
我们通过git clone那个项目代码,这里已经gitclone好了,可以看一眼gitlog,看到已经更新到最新的位置信息了。
下面是我们刚才讲到的提供了一个docker的快速启动方式。
比如build出一个docker版本,我们可以通过这个命令,比如这个命令可以用社区提供的dockerfile去创建。
创建以后我们可以看到这个Images。
可以看到生成好的一个Image,通过Image就可以直接启动集群。
我们需要把它停下来,去启动我们的集群。通过我exec命令登记到集群里边,进来以后可以看到代码已经给它git clone下来了。
我们可以尝试看一下build.sh。
可以看到是下图这样,但是我们看到没有语法高亮,尝试一下用vim去看一下语法高亮。
没有提供我们的vim,就需要去装一个。
如果发现没有去装,我们是否可以让社区版自己提供vim,这样下次使用的时候就可以不用自己安装了,这就是我们修改贡献开源的一个好处。
装好以后,我们就可以看到高亮了。
这个改进是否可以直接把它作为代码提交到开源社区,答案是当然可以,这里边已验证了,通过安装vim就可以了。
我们转到docker目录。
Docker目录里,我们打开了dockerfile,可以在这里进行安装,比如加一个vim
我们去验证一下这个改进是否可以。
如上所示,这样就可以了。
我们重新拉起了一个,登进来以后看一下是否直接就可以进行一个vim。
出现上图则说明我们这个功能是ok的,可以把这个代码进行提交。
进行代码提交的时候,首先给它一个新的branch,比如叫fix_dockerfile,然后给它checkout fix_dockerfile,可以看一下我们是在哪个分支上。
这个时候我们可以看到更改这个文件已经可以了,这里还有更改历史。
可以看到我们的提交已经完成了。
那怎么把我们的提交推到社区呢?提交前需要git check到master。
接着需要更新一下代码,更新了代码以后要checkout fix_dockerfile,我然后再rebase master就可以提交了。
这样我们就把代码提交到社区了,之后可以看一眼我们的代码。
我们可以通过建一个pull request,然后Comepare pull request。
然后可以看到docker file,描述一下这是一个什么提交
然后通过create poll request就可以把我们的代码快速提到master分支里,到这整个代码提交流程包括修改流程就基本完成了。
这里我把命令列出来,大家可以参考一下。
下面是一些git的命令。
最后,欢迎大家一起加入到我们的开源项目中,贡献一份力量,提出一些想法,让我们一起努力,把这个项目做大做强。