如何buildNebulaGraph?如何为NebulaGraph内核做贡献?即便是新手也能快速上手,从本文作为切入点就够了。NebulaGraph的架构简介 为了方便对NebulaGraph尚未了解的读者也能快速直接从贡献代码为起点了解它,我把开发、贡献内核代码入手所需要的基本架构知识在这里以最小信息量的形式总结一下。作为前导知识,请资深的NebulaGraph玩家直接跳过这一章节。服务、进程 NebulaGraph的架构和GoogleSpanner、TiDB很相似,核心部分只有三种服务进程:Graph服务、Meta服务和Storage服务。它们之间彼此通过TCP之上的ThriftRPC协议进行通信。 https:docscdn。nebulagraph。com。cndocs2。01。introduction2。nebulagrapharchitecturenebulagrapharchitecture1。png计算层与存储层 NebulaGraph是存储与计算分离的架构,Meta服务和Storage服务共同组成了存储层,Graph服务是内核提供的计算层。 这样的设计使得NebulaGraph的集群部署可以灵活按需分配计算、存储的资源。比如,在同一个集群中创建不同配置的两组Graph服务实例用来面向不同类型的业务。 同时,计算层解耦于存储层使得在NebulaGraph之上的构建不同的特定计算层成为可能。比如,NebulaGraphAlgorithm、NebulaGraphAnalytics就是在NebulaGraph之上构建了异构的另一个计算层。任何人都可以按需定制专属计算层,从而满足统一图基础存储之上的复合、多样的计算需求。GraphService:nebulagraphd Graph服务是对外接收图库登录、图查询请求、集群管理操作、Schema定义所直接连接的服务,它的进程名字叫graphd,表示nebulagraphdaemon。 Graph服务的每一个进程是无状态的,这使得横向扩缩Graph服务的实例非常灵活、简单。 Graph服务也叫QueryEngine,其内部和传统的数据库系统的设计非常相似,分为:解析、校验、计划、执行几部分。 MetaService:nebulametad Meta服务顾名思义负责元数据管理,进程名字叫metad。这些元数据包括:所有的图空间、Schema定义用户鉴权、授权信息集群服务的发现与服务的分布图空间中的数据分布 Meta服务的进程可以单实例部署。在非单机部署的场景下,为了数据、服务的高SLA,以奇数个实例进行部署。通常来说3个nebulametad就足够了,3个nebulametad通过Raft共识协议构成一个集群提供服务。 StorageService:nebulastoraged Storage服务存储所有的图数据,进程名字叫storaged。storaged分布式地存储图数据,为Graph内部的图查询执行期提供底层的图语义存储接口,方便Storage客户端通过ThriftRPC协议面向涉及的storaged示例进行图语义的读写。 当NebulaGraph中图空间的副本数大于1的时候,每一个分区都会在不同storaged示例上有副本,副本之间则通过Raft协议协调同步与读写。 进程间通信、服务发现机制 在NebulaGraph中graphd、metad、storaged之间通过Thrift协议进行远程调用(RPC),下边给一些例子:graphd会通过metaclient调用metad:将自己报告为一个正在运行的服务,以便被发现;再为用户(使用graphclient)登录进行RPC调用;当它处理nGQL查询时,获取图存储分布情况;graphd会通过storageclient调用storaged:当graphd处理nGQL时,先从metad获得所需的元信息,再进行图数据的读写;storaged会通过metaclient调用metad:将storaged报告为一个正在运行的服务,以便被发现。 当然,有状态的存储引擎内部也有集群同步的流量与通信。比如,storaged与其他storaged有Raft连接;metad与其他metad实例有Raft连接。开发环境搭建 接下来,我们开始NebulaGraph的构建、开发环境的部分。 NebulaGraph只支持在GNULinux分支中构建。目前来说,最方便的方式是在社区预先提供好了依赖的容器镜像的基础上在容器内部构建、调试NebulaGraph代码的更改和Debug。创建一个容器化的NebulaGraph集群 为了更方便地调试代码,我习惯提前创建一个NebulaGraphDocker环境。推荐使用官方的DockerCompose方式部署,也可以使用我在官方DockerCompose基础之上弄的一键部署工具:nebulaup。 下面以nebulaup为例: 在Linux开发服务器中执行curlfsSLnebulaup。siwei。ioinstall。shbash就可以了。代码获取 NebulaGraph的代码仓库托管在GitHub之上,在联网的情况下直接克隆:gitclonegitgithub。com:vesoftincnebula。gitcdnebula创建开发容器 有了NebulaGraph集群,我们可以借助nebuladevdocker提供的开箱即用开发容器镜像,搭建开发环境:exportTAGubuntu2004dockerruntinetworknebulanetsecurityoptseccompunconfinedvPWD:homenebulawhomenebulanamenebuladevvesoftnebuladev:TAGbash 其中,vPWD表示当前的NebulaGraph代码本地的路径会被映射到开发容器内部的homenebula,而启动的容器名字是nebuladev。 待这个容器启动后,会自动进入到这个容器的bashshell之中。如果我们输入exit退出容器,它会被关闭。如果我们想再次启动容器,只需要执行:dockerstartnebuladev 之后的编译、Debug、测试工作都在nebuladev容器内部进行。在容器是运行状态的情况下,可以随时新建一个容器内部的bashshell进程:dockerexectinebuladevbash 为了保持编译环境是最新版,可以定期删除、拉取、重建这个开发容器,以保持环境与代码相匹配。编译环境 在nebuladev这个容器内部,我们可以进行代码编译。进入编译容器:dockerexectinebuladevbash 用CMake准备makefile。第一次构建时,为了节省时间、内存,我关闭了测试DENABLETESTINGOFF:mkdirbuildcdbuildcmakeDCMAKECXXCOMPILERTOOLSETCLANGDIRbingDCMAKECCOMPILERTOOLSETCLANGDIRbingccDENABLEWERROROFFDCMAKEBUILDTYPEDebugDENABLETESTINGOFF。。 开始编译,根据服务器的空闲CPU个数和内存量力而行。比如,我在72核心的服务器上准备允许同时运行64个job,则运行:makej64 第一次构建的时间会慢一些,在make成功之后,我们也可以执行makeinstall把二进制安装到像生产安装时候一样的路径:root1827b82e88bf:homenebulabuildmakeinstallroot1827b82e88bf:homenebulabuildlsusrlocalnebulabindbdumpdbupgradermetadumpnebulagraphdnebulametadnebulastoragedroot1827b82e88bf:homenebulabuildlsusrlocalnebulabinetcpidsscriptsshare调试NebulaGraph 以graphd调试为例。安装依赖 安装一些后边会方便Debug额外用到的依赖:装一个ping,测试一下nebulaup安装的集群可以访问aptupdateaptinstalliputilspingypinggraphd试试看pinggraphdc4安装gdbgdbdashboardaptinstallgdbywgetPhttps:git。io。gdbinitpipinstallpygments准备客户端 准备一个NebulaGraph的命令行客户端:新开一个nebuladev的shelldockerexectinebuladevbash下载nebulaconsole二进制文件,并赋予可执行权限,命名为nebulaconsole并安装到usrbin下wgethttps:github。comvesoftincnebulaconsolereleasesdownloadv3。2。0nebulaconsolelinuxamd64v3。2。0chmodxnebulaconsolemvnebulaconsoleusrbinnebulaconsole 连接到前边我们nebulaup准备的集群之上,加载basketballplayer这个测试数据:nebulaconsoleurootpnebulaaddressgraphdport9669:playbasketballplayer;exitgdb运行graphd 用gdb执行刚刚编译的nebulagraphd二进制,让它成为一个新的graphd服务,名字就叫nebuladev。 首先启动gdb:新开一个nebuladev的shelldockerexectinebuladevbashcdusrlocalnebulamkdirphomenebulabuildloggdbbinnebulagraphd 在gdb内部执行设置必要的参数,跟随fork的子进程:setfollowforkmodechild 设置待调试graphd的启动参数(配置):metaserveraddrs填已经启动的集群的所有metad的地址;localip和wsip填本容器的域名,port是graphd监听端口;logdir是输出日志的目录,v和minloglevel是日志的输出等级;setargsflagfileusrlocalnebulaetcnebulagraphd。conf。defaultmetaserveraddrsmetad0:9559,metad1:9559,metad2:9559port9669localipnebuladevwsipnebuladevwshttpport19669logdirhomenebulabuildlogv4minloglevel0 如果我们想加断点在srccommonfunctionFunctionManager。cpp2783行,可以再执行:bhomenebulasrccommonfunctionFunctionManager。cpp:2783 配置前边安装的gdbdashboard,一个开源的gdb界面插件。设定在gdb界面上展示代码、历史、回调栈、变量、表达几个部分,详细参考https:github。comcyrusandgdbdashboarddashboardlayoutsourcehistorystackvariablesexpressions 最后我们让进程通过gdb跑起来吧:run 之后,我们就可以在这个窗口shell会话下调试graphd程序了。修改NebulaGraph代码 这里,我以issue3513为例子,快速介绍一下代码修改的过程。读代码 这个issue表达的内容是在有一小部分用户决定把JSON以String的形式存储在NebulaGraph中的属性里。因为这种方式比较罕见且不被推崇,NebulaGraph没有直接支持对JSONString解析。 由于不是一个通用型需求,这个功能是希望热心的社区用户自己来实现并应用在他的业务场景中。但在该issue中,刚好有位新手贡献者在里边回复、求助如何开始参与这块的功能实现。借着这个契机,我去参与讨论看了一下这个功能可以实现成什么样子。最终讨论的结果是可以做成和MySQL中的JSONEXTRACT函数那样,改为只接受JSONString、无需处理输出路径参数。 一句话来说就是,为NebulaGraph引入一个解析JSONString为Map的函数。那么,如何实现这个功能呢?在哪里修改 显然,引入新的函数,项目变更肯定有很多。所以,我们只需要找到之前增加新函数的PR就可以快速知道在哪些地方修改了。 一般情况下,可以自底向上地了解NebulaGraph整体的代码结构,再一点点找到函数处理的位置。这时候,除了代码本身,一些面向贡献者的文章可能会帮助大家事半功倍对整体有一个了解。NebulaGraph官方也除了一个系列文章,大家做项目贡献前不妨阅读了解下,参见:延伸阅读5。 具体的实操起来呢?我从pr4526了解到所有函数入口都被统一管理在srccommonfunctionFunctionManager。cpp之中。通过搜索、理解当中某个函数的关键词之后,可以很容易理解一个函数实体的关键词、输入输出数据类型、函数体处理逻辑的代码在哪里实现。 此外,在同一个根目录下,srccommonfunctiontestFunctionManagerTest。cpp之中则是所有这些函数的单元测试代码。用同样的方式也可以知道新加的一个函数需要如何在里边实现基于gtest的单元测试。开始改代码 在修改代码之前,确保在最新的master分支之上创建一个单独的分支。在这里的例子中,我把分支名字叫fnJSONEXTRACT:gitcheckoutmastergitpullgitcheckoutbfnJSONEXTRACT 通过Google了解与交叉验证NebulaGraph内部使用的utils库,知道应该用folly::parseJson把字符串读成folly::dynamic。再cast成NebulaGraph内置的Map()类型。最后,借助于StackOverflowGitHubCopilot,我终于完成了第一个版本的代码修改。调试代码 我兴冲冲地改好了第一版的代码,信心满满地开始编译!实际上,因为我是CPP新手,即使在Copilot加持下,我的代码还是花了好几次修改才通过编译。 编译之后,我用gdb把修改了的graphd启动起来。用console发起JSONEXTRACT的函数调用。先调通了期待中的效果,并试着跑几种异常的输入。在发现新问题、修改、编译、调试的几轮循环下让代码达到了期望的状态。 这时候,就该把代码提交到远端GitHub请项目的资深贡献者帮忙review啦!提交PR PR(PullRequest)是GitHub中方便多人代码协作、代码审查中的一种方式。它通过把一个repo下的分支与这个审查协作的实例(PR)做映射,得到一个项目下唯一的PR号码之后,生成单独的网页。在这个网页下,我们可以做不同贡献者之间的交流和后续的代码更新。这个过程中,代码提交者们可以一直在这个分支上不断提交代码直到代码的状态被各方同意approve,再合并merge到目的分支中。 这个过程可以分为:创建GitHub上远程的个人开发分支;基于分支创建目标项目仓库中的PR;在PR中协作、讨论、不断再次提交到开发分支直到多方达到合并、或者关闭的共识;提交到个人远程分支 在这一步骤里,我们要把当前的本地提交的commit提交到自己的GitHub分叉之中。commit本地修改 首先,确认本地的修改是否都是期待中的:先确定修改的文件gitstatus再看看修改的内容gitdiff 再commit,这时候是在本地仓库提交commit:添加所有当前目录(。这个点表示当前目录)修改过的文件为待commitgitadd。然后我们可以看一下状态,这些修改的文件状态已经不同了gitstatus最后,提交在本地仓库,并用m参数指定单行的commitmessagegitcommitmfeat:introducefunctionJSONEXTRACT提交到自己远程的分支 在提交之前,要确保自己的GitHub账号之下确实存在NebulaGraph代码仓库的分叉fork。比如,我的GitHub账号是weygu,那么我对https:github。comvesoftincnebula的分叉应该就是https:github。comweygunebula。 如果还没有自己的分叉,可以直接在https:github。comvesoftincnebula上点击右上角的Fork,创建自己的分叉仓库。 当远程的个人分叉存在之后,我们可以把代码提交上去:添加一个新的远程仓库叫weygitremoteaddweygitgithub。com:weygunebula。git提交JSONEXTRACT分支到wey这个remote仓库gitpushweyJSONEXTRACT在个人远程分叉分支上创建PR 这时候,我们访问这个远程分支:https:github。comweygunebulatreefnJSONEXTRACT,就能找到OpenPR的入口: 点击Openpullrequest按钮,进入到创建PR的界面了,这和在一般的论坛里提交一个帖子是很类似的: 提交之后,我们可以等待、或者邀请其他人来做代码的审查review。往往,开源项目的贡献者们会从他们的各自角度给出代码修改、优化的建议。经过几轮的代码修改、讨论后,这时候代码会达到最佳的状态。 在这些审查者中,除了社区的贡献者(人类)之外,还有自动化的机器人。它们会在代码库中自动化地通过持续集成CI的方式运行自动化的审查工作,可能包括以下几种:CLA:ContributorLicenseAgreement,贡献者许可协议。PR作者在首次提交代码到项目时,所需签署的协议。因为代码将被提交到公共空间,这份协议的签署意味着作者同意代码被分享、复用、修改;lint:代码风格检查,这也是最常见的CI任务;test:各种层面的测试检查任务。 通常来说,所有自动化审查机器人执行的任务全都通过后,贡献的代码状态才能被认为是可合并的。不出意外,我首次提交的代码果然有测试的失败提示。 调试CI测试代码 NebulaGraph里所有的CI测试代码都能在本地被触发。当然,它们都有被单独触发的方式。我们需要掌握如何单独触发某个测试,而不是在每次修改一个小的测试修复、提交到服务器,就等着CI做全量的运行,这样会浪费掉几十分钟。CTest 本次PR提交中,我修改的函数代码同一层级下的单元测试CTest就有问题。问题发生的原因有多种,可能是测试代码本身、代码变更破坏了原来的测试用例、测试用例发现代码修改本身的问题。 我们要根据CTest失败的报错进行排查和代码修改。再编译代码,在本地运行一下这个失败的用例:我们需要进入到我们的编译容器内部的build目录下dockerexectinebuladevbashcdbuild在DENABLETESTINGON之中编译,如果之前的编译job数下内存已经跑满了的话,这次可以把job数调小一点,因为开启测试会占用更多内存cmakeDCMAKECXXCOMPILERTOOLSETCLANGDIRbingDCMAKECCOMPILERTOOLSETCLANGDIRbingccDENABLEWERROROFFDCMAKEBUILDTYPEDebugDENABLETESTINGON。。makej48可以看到编译成功了CTest的单元测试二进制可执行文件〔100〕LinkingCXXexecutable。。。。。。。。bintestfunctionmanagertest〔100〕Builttargetfunctionmanagertest执行重新修改过的单元测试!bintestfunctionmanagertest〔〕Running11testsfrom1testsuite。〔〕Globaltestenvironmentsetup。〔〕11testsfromFunctionManagerTest〔RUN〕FunctionManagerTest。testNull〔OK〕FunctionManagerTest。testNull(0ms)〔RUN〕FunctionManagerTest。functionCallW2022102023:35:18。57989728679Map。cpp:77〕JSONEXTRACTnestedlayer1:MapcanbepopulatedonlybyBool,Double,Int,Stringvalueandnull,nowtryingtoparsefrom:object〔OK〕FunctionManagerTest。functionCall(2ms)〔RUN〕FunctionManagerTest。time〔OK〕FunctionManagerTest。time(0ms)〔RUN〕FunctionManagerTest。returnType〔OK〕FunctionManagerTest。returnType(0ms)〔RUN〕FunctionManagerTest。SchemaRelated〔OK〕FunctionManagerTest。SchemaRelated(0ms)〔RUN〕FunctionManagerTest。ScalarFunctionTest〔OK〕FunctionManagerTest。ScalarFunctionTest(0ms)〔RUN〕FunctionManagerTest。ListFunctionTest〔OK〕FunctionManagerTest。ListFunctionTest(0ms)〔RUN〕FunctionManagerTest。duplicateEdgesORVerticesInPath〔OK〕FunctionManagerTest。duplicateEdgesORVerticesInPath(0ms)〔RUN〕FunctionManagerTest。ReversePath〔OK〕FunctionManagerTest。ReversePath(0ms)〔RUN〕FunctionManagerTest。DataSetRowCol〔OK〕FunctionManagerTest。DataSetRowCol(0ms)〔RUN〕FunctionManagerTest。PurityTest〔OK〕FunctionManagerTest。PurityTest(0ms)〔〕11testsfromFunctionManagerTest(5mstotal)〔〕Globaltestenvironmentteardown〔〕11testsfrom1testsuiteran。(5mstotal)〔PASSED〕11tests。 成功! 将新的更改提交到远程分支上,在PR的网页中,我们可以看到CI已经在新的提交的触发下重新编译、执行了。过一会儿全部pass,我开始兴高采烈地等待着2位以上的审查者帮忙批准代码,最后合并它! 但是,我收到了新的建议: 另一位贡献者请我添加TCK的测试用例。TCK TCK的全称是TheCypherTechnologyCompatibilityKit,它是NebulaGraph从openCypher社区继承演进而来的一套测试框架,并用Python做测试用例格式兼容的实现。 它的优雅在于,我们可以像写英语一样去描述我们想实现的端到端功能测试用例,像这样!teststckfeaturesfunctionjsonextract。featureFeature:jsonextractFunctionBackground:TestjsonextractfunctionScenario:TestPositiveCasesWhenexecutingquery:YIELDJSONEXTRACT({a:foo,b:0。2,c:true})ASresult;Thentheresultshouldbe,inanyorder:result{a:foo,b:0。2,c:true}Whenexecutingquery:YIELDJSONEXTRACT({a:1,b:{},c:{d:true}})ASresult;Thentheresultshouldbe,inanyorder:result{a:1,b:{},c:{d:true}}Whenexecutingquery:YIELDJSONEXTRACT({})ASresult;Thentheresultshouldbe,inanyorder:result{} 在添加了自己的一个新的tck测试用例文本文件之后,我们只需要在测试文件中临时增加标签,并在执行的时候指定标签,就可以单独执行新增的tck测试用例了:还是在编译容器内部,进入到tests目录下cd。。tests安装tck测试所需依赖python3mpipinstallrrequirements。txtpython3mpipinstallnebula3python3。1。0运行一个单独为tck测试准备的集群makeCONTAINERIZEDtrueENABLESSLtrueCASIGNEDtrueup给teststckfeaturesfunctionjsonextract。feature以开头第一行加上标签,比如weyviteststckfeaturesfunctionjsonextract。feature执行pytest(包含tck用例),因为制定了mwey,只有teststckfeaturesfunctionjsonextract。feature会被执行python3mpytestmwey关闭pytest所依赖的集群makeCONTAINERIZEDtrueENABLESSLtrueCASIGNEDtruedown再次邀请review 待我们把需要的测试调通、再次提交PR并且CI用例全都通过之后,我们可以再次邀请之前帮助审查代码的同学做做最后的查看,如果一切都顺利,代码就会被合并了! 就这样,我的第一个CPPPR终于被合并成功,大家能看到我留在NebulaGraph中的代码了。延伸阅读:基于BDD理论的NebulaGraph集成测试框架重构,https:nebulagraph。com。cnpostsbddtestingpractice如何向NebulaGraph增加一个测试用例,https:nebulagraph。com。cnpostsbddtestingpracticeaddtestcaseNebulaGraph文档之架构介绍,https:docs。nebulagraph。com。cnmaster1。introduction3。nebulagrapharchitecture1。architectureoverviewNebulaGraph源码解读系列,https:www。nebulagraph。com。cnpostsnebulagraphsourcecodereading00 谢谢你读完本文() 如果你想尝鲜图数据库NebulaGraph,记得去GitHub下载、使用、()star它GitHub;和其他的NebulaGraph用户一起交流图数据库技术和应用技能,留下你的名片一起玩耍呀