本文介绍了利用开源API网关APISIX加速NebulaGraph多个场景的落地最佳实践:负载均衡、暴露接口结构与TLSTermination。API网关介绍什么是API网关 API网关是位于客户端和服务器之间的中间人,用于管理、监控和保护API。它可以在API之前执行一些操作,例如:身份验证、授权、缓存、日志记录、审计、流量控制、安全、防火墙、压缩、解压缩、加密、解密等。 API网关可以工作在TCPIP4层和OSI7层。跑在7层的API网关可以使用多种协议,例如:HTTP、HTTPS、WebSocket、gRPC、MQTT等。在这些应用层协议中做一些操作,比如,请求的重写、转发、合并、重试、缓存、限流、熔断、降级、鉴权、监控、日志、审计等等。 这里举例一下借助API网关可以做的具体的事:在网关层增加认证层,比如:JWT认证、OAuth2认证、OpenID认证等等,这样不需要在每个服务中都做具体的认证集成工作,进而节省许多开发成本。借助网关给跳板机SSH流量增加无需客户端修改的复杂认证,比如:跳转任何客户端的SSH登录,给出一个网址或者输入框,引导登陆者通过网页的SSO认证(包含多因素认证),再通过网关转发到SSH服务。甚至在网关层做Serverless数据库!TiDB社区的同学们就在做这个事儿,他们从普通的MySQL客户端的登录请求中解析能推断出转到需要的TiDB示例的信息,并且在需要coldstart唤醒实例的时候把连接保持住,可以参考这篇文章:TiDBGateway。如果你特别惨在维护屎山项目,不得不针对旧版本的应用程序对新版本的服务端进行兼容,这时候API网关也可以通过一些请求重写,把旧版本的请求转换成新版本的请求。 只要脑洞大,理论上API网关可以做很多事。但显然不是所有的事情都是适合在这一层去做的,通常那些比较通用的事情才适合在这一层去做,上面我只是给出一些典型和极端的具体例子。ApacheAPISIX API网关是从LB、ReverseProxy项目演进过来的。随着云原生的兴起,API网关也逐渐成为了云原生的一部分,流行的开源网关有:NginxApacheAPISIXKongLuraOpenRestyTykTraefikIstioEnvoy 而且其中很多都是基于NginxOpenResty的下游项目。这里就以ApacheAPISIX为例,介绍一下NebulaGraph借助API网关的几个实践。NebulaGraph介绍 NebulaGraph是一个开源的分布式图数据库,它的特点是:高性能:可达到每秒百万级的读写,具有极高的扩展性,在千亿点、万亿边的数据规模下支持毫秒级的查询。易扩展:分布式的架构可在多台机器上扩展。每台机器上可以运行多个服务进程,它的查询层是无状态的计算存储分离架构,可以容易地引入不同配置、不同类型的计算层,实现同一集群上TP、AP、图计算等不同负载的混合查询。易使用:类SQL的原生查询语言,易于学习和使用,同时支持openCypher。丰富生态:NebulaGraph的生态系统正在不断壮大,目前已经有了多个客户端,包括Java、Python、Go、C、JavaScript、Spark、Flink等,同时也有了多个可视化工具,包括NebulaGraphStudio、NebulaGraphDashboard、NebulaGraphExplorer等。本文讨论的问题 本文给出了基于NebulaGraph集群应用中涉及到API网关的几个场景。查询接口的负载均衡底层存储接口的暴露传输层的加密查询接口负载均衡 首先是图数据库查询接口graphd的负载均衡与高可用的问题。 NebulaGraph内核由三种服务组成:graphd、metad和storaged: 所以,在默认情况下,集群只会暴露graphd的接口,提供给客户端连接,执行nGQL的查询。其中,graphd是无状态的,这意味着可以在多个graphd之间做负载均衡。这里,我们有两种方法:基于客户端的(ClientSideLB)与基于代理的。客户端的负载均衡 客户端的负载均衡,就是在客户端,也就是应用程序中,实现负载均衡的逻辑。NebulaGraph的各个语言的客户端里边已经内置了轮询(RoundRobin)负载均衡,我们只需要在客户端配置多个graphd的地址就可以了。比如,我们在创建连接池的时候,指定了两个不同的graphd的地址(对应不同进程实例),下面以Python代码为例:fromnebula3。gclient。netimportConnectionPoolfromnebula3。ConfigimportConfigconfigConfig()config。maxconnectionpoolsize10connectionpoolConnectionPool()connectionpool。init(〔(127。0。0。1,9669),(127。0。0。1,49433)〕,config) 在取得连接的时候,就会从连接池中随机取得一个连接:In〔10〕:connection0connectionpool。getconnection()In〔11〕:connection1connectionpool。getconnection()这两个连接的graphd地址是不同的In〔12〕:connection0。port,connection1。portOut〔12〕:(9669,49433) 这种客户端负载均衡的问题在于配置、实现细节与应用代码耦合在一起,如果需要修改负载均衡的策略,就要修改应用代码,这样就会增加应用的复杂度。代理的负载均衡 基于代理的负载均衡,就是在应用程序之前,增加一个代理层,来实现负载均衡的逻辑。这样,应用程序就不需要关心负载均衡的问题了。在K8s里的话,我们可以使用K8s的Service来实现这个代理层。 这是一个在Minikube中为NebulaGraph集群中graphd创建的Service:catEOFkubectlcreatefapiVersion:v1kind:Servicemetadata:labels:app。kubernetes。iocluster:nebulaapp。kubernetes。iocomponent:graphdapp。kubernetes。iomanagedby:nebulaoperatorapp。kubernetes。ioname:nebulagraphname:nebulagraphdsvcnodeportnamespace:defaultspec:externalTrafficPolicy:Localports:name:thriftport:9669protocol:TCPtargetPort:9669nodePort:30000name:httpport:19669protocol:TCPtargetPort:19669nodePort:30001selector:app。kubernetes。iocluster:nebulaapp。kubernetes。iocomponent:graphdapp。kubernetes。iomanagedby:nebulaoperatorapp。kubernetes。ioname:nebulagraphtype:NodePortEOF 创建后,我们就可以通过它暴露的单独端口来访问NebulaGraph集群中的graphd了:In〔13〕:connectionpoolConnectionPool()。。。:connectionpool。init(〔(192。168。49。2,9669)〕,config)Out〔13〕:TrueIn〔14〕:connection0connectionpool。getconnection()In〔15〕:connection1connectionpool。getconnection()In〔16〕:connection0。ip,connection1。ipOut〔16〕:(192。168。49。2,192。168。49。2) 可以看到,在连接层面上来看,客户端只知道代理的地址,而不知道NebulaGraph集群中的graphd的地址,这样就实现了客户端与NebulaGraph集群中的graphd的解耦。 然而,当我们在Connection之上创建Session的时候,就能看到实际上客户端的不同请求是落在了不同的graphd上的:In〔17〕:sessionconnectionpool。getsession(root,nebula)In〔18〕:session。sessionidOut〔18〕:1668670607568178In〔19〕:session1connectionpool。getsession(root,nebula)In〔20〕:session1。sessionidOut〔20〕:1668670625563307得到每一个session的IDIn〔21〕:session。execute(SHOWSESSIONS)它们分别对应了两个不同的graphd实例Out〔21〕:ResultSet(keys:〔SessionId,UserName,SpaceName,CreateTime,UpdateTime,GraphAddr,Timezone,ClientIp〕,values:〔1668670607568178,root,,utcdatetime:20221117T07:36:47。568178,timezoneoffset:0,utcdatetime:20221117T07:36:47。575303,timezoneoffset:0,nebulagraphd0。nebulagraphdsvc。default。svc。cluster。local:9669,0,172。17。0。1〕,〔1668670625563307,root,,utcdatetime:20221117T07:37:05。563307,timezoneoffset:0,utcdatetime:20221117T07:37:03。638910,timezoneoffset:0,nebulagraphd1。nebulagraphdsvc。default。svc。cluster。local:9669,0,172。17。0。1〕)底层存储接口的暴露 在NebulaGraph中,可以通过StorageClient来访问底层的存储接口,这个接口可以用来做一些分析型、数据全扫描计算的工作。 然而,存储层的分布式服务实例不像graphd那样,它们是有状态的。这其实与K8s或者DockerCompose的部署模型是相违背的。如果访问的应用storaged客户端在集群外部,我们需要在NebulaGraph集群中的每一个存储实例上都部署一个代理Service。这非常不方便,有时候还是一种浪费。 此外,由于NebulaGraph内部服务发现机制和storaged客户端的实现机制决定,每一个storaged服务实体都是由其内部的host:port唯一确定和寻址的,这给我们中间的代理工作也带来了一些麻烦。 总结来看,我们的需求是:能够从集群外部访问NebulaGraph的存储层每一个实例每一个实例的访问地址(host:port)和内部的地址是完全一致的 为了实现这个需求,我之前的做法是为每一个实例单独部署一个graphd代理(消耗一个地址,保证端口不变),再在外部手动搭一个Nginx作为代理,配合DNS把内部的地址解析Nginx上,然后通过域名找到上游(每一个单独的graphd代理)。本文的延伸阅读1、2中给出了相关的实验步骤。 最近,我找到了一个相对优雅的可维护的方式:在NebulaGraph集群同一个命名空间下引入一个APISIX网关;利用APISIX中的NginxTCP代理的封装streamproxy来暴露storaged的接口;为了最终只利用一个集群的出口(Service,我们利用其支持的TLSv1。3中的extendhostname字段:SNI来路由上游),做到用不同域名的TCPoverTLS指向后端的不同只需要Storage客户端能支持TLSv1。3(发送SNI),并且能解析所有storaged的地址到APISIX的Service上即可; 示例图:K8sClusterNebulaGraphClusterAPISIXAPIGATEWAYstoraged0streamproxy。。addr:9559storaged1DNS(Service)tls:true。,SNIstoraged2storaged3 这样做的好处是:在APISIX中比较优雅地维护代理的配置,并且可以用到APISIX现代化的流量管理能力;不需要为每一个storaged单独创建Service,只需要一个Service、集群地址就可以了;为流量增加了TLSv1。3的加密,提高了安全性。同时,没有给NebulaGraph集群内部的南北流量带来的性能损耗; 在本文的结尾,给出了实验过程,包含了本文提到的所有要点和细节。传输层的加密 我们在前一个问题中提及到了,在APISIX网关中terminateTLSv1。3的连接,借助SNI信息路由storaged的方法。其实,单独将graphd接口的TLS交给网关来做,好处也是非常明显的:证书管理在统一的网关控制面做,更加方便;证书运维无NebulaGraph集群配置侵入(NebulaGraph原生支持TLS加密,但是加密之后带来了集群内部通信的开销,而且配置和集群其他层面配置在一起,证书更新涉及进程重启,不够灵活); 具体的方法在后边实操中也是有体现的。实操:利用APISIX的streamproxy暴露storaged的接口实验环境:Minikube 本实验在本地的Minikube上做。首先,启动一个Minikube。因为APISIX内部的etcd需要用到storageclass,我们带上穷人版的storageclass插件。同时,为了在K8s外部访问storaged的时候用和内部相同的域名和端口,将把nodeport允许的端口扩充到小于9779的范围。addonsdefaultstorageclassextraconfigapiserver。servicenodeportrange165535实验环境:NebulaGraphonK8s 这里,我们使用NebulaOperator来部署NebulaGraph集群,具体的部署方法可以参考NebulaOperator文档:https:docs。nebulagraph。com。cn3。3。0nebulaoperator1。introductiontonebulaoperator。 咱们做实验,就偷个懒,用我写的NebulaOperatorKinD来一键部署:curlsLnebulakind。siwei。ioinstallonK8s。shbash实验环境:APISIXonK8s 首先,是安装。在Helm参数中指定打开streamproxy的开关:helmrepoaddapisixhttps:charts。apiseven。comhelmrepoaddbitnamihttps:charts。bitnami。combitnamihelmrepoupdatehelminstallapisixapisixapisixsetgateway。typeNodePortsetgateway。stream。enabledtruesetingresscontroller。enabledtruedashboard也装上,方便我们绕过adminAPIcall做一些方便的操作。helminstallapisixdashboardapisixapisixdashboard 因为截止到现在,APISIX的HelmChart之中并没有提供streamproxyTCP的监听端口的TLS支持的配置格式,见:https:github。comapacheapisixhelmchartissues348。我们需要手动更改APISIX的ConfigMap,把streamproxy的TLS配置加上:kubectleditConfigMapapisix 我们编辑把streamproxy。tcp改写成这样:streamproxy:TCPUDPproxyonly:falsetcp:TCPproxyportlistaddr:9779tls:trueaddr:9559tls:true 这里我们需要重建APISIXPod,因为APISIX的streamproxy的TLS配置是在启动的时候加载的,所以我们需要重建APISIXPod:kubectldelete(kubectlgetpolapp。kubernetes。ionameapisixoname)开始实验 这个实验的目标是把NebulaGraph的storaged的接口暴露出来,让外部的客户端可以访问到,而暴露的方式如图:K8sClusterNebulaGraphClusterAPISIXAPIGATEWAYstoraged0streamproxy。。addr:9559storaged1DNS(Service)tls:true。,SNIstoraged2storaged3 我们已经有了所有的框架,我们要往里填箭头和圆圈就行。kubectlgetpoNAMEREADYSTATUSRESTARTSAGEapisix6d89854bc55m78811Running1(31hago)2d4hapisixdashboardb544bd766nh79j11Running8(31hago)2d10hapisixetcd011Running2(31hago)2d10hapisixetcd111Running2(31hago)2d10hapisixetcd211Running2(31hago)2d10hnebulagraphd011Running2(31hago)3d4hnebulametad011Running2(31hago)3d4hnebulastoraged011Running2(31hago)3d4hnebulastoraged111Running2(31hago)3d4hnebulastoraged211Running2(31hago)3d4h配置APISIX的streamproxy 参考APISIX文档:https:apisix。apache。orgdocsapisixstreamproxyaccepttlsovertcpconnection。 我们用APISIX的API来配置streamproxy:apisixapikeyedd1c9f034335f136f87ad84b625c8f1apisixpod(kubectlgetpolapp。kubernetes。ionameapisixoname)kubectlexecitapisixpodcurlhttp:127。0。0。1:9180apisixadminstreamroutes1HXAPIKEY:apisixapikeyXPUTd{sni:nebulastoraged0。nebulastoragedheadless。default。svc。cluster。local,upstream:{nodes:{172。17。0。13:9779:1},type:roundrobin}}kubectlexecitapisixpodcurlhttp:127。0。0。1:9180apisixadminstreamroutes2HXAPIKEY:apisixapikeyXPUTd{sni:nebulastoraged1。nebulastoragedheadless。default。svc。cluster。local,upstream:{nodes:{172。17。0。18:9779:1},type:roundrobin}}kubectlexecitapisixpodcurlhttp:127。0。0。1:9180apisixadminstreamroutes3HXAPIKEY:apisixapikeyXPUTd{sni:nebulastoraged2。nebulastoragedheadless。default。svc。cluster。local,upstream:{nodes:{172。17。0。5:9779:1},type:roundrobin}} 这里需要注意,目前,APISIX的streamproxy上游节点不支持域名解析是受限于上游的lua库,详见issue:https:github。comapacheapisixissues8334。理想情况下,这里应该给出每一个storaged的SNI相同的地址作为upstream。nodes。像这样:kubectlexecitapisixpodcurlhttp:127。0。0。1:9180apisixadminstreamroutes1HXAPIKEY:apisixapikeyXPUTd{sni:nebulastoraged0。nebulastoragedheadless。default。svc。cluster。local,upstream:{nodes:{nebulastoraged0。nebulastoragedheadless。default。svc。cluster。local:1},type:roundrobin}}配置APISIX中storaged地址的TLS证书 在生产环境下,我们应该以云原生的方式去管理自签或者公共信任的证书。这里,我们就手动利用MKCert工具来做这件事儿。 安装MKCert:首次运行,需要安装mkcert,并且生成根证书macOS的话brewinstallmkcertubuntu的话aptgetinstallwgetlibnss3tools然后再去https:github。comFiloSottilemkcertreleases下载mkcert 签发证书:mkcert。nebulastoragedheadless。default。svc。cluster。local 利用APISIXDashboard将证书导入到APISIX之中。单独开一个终端,运行:exportPODNAME(kubectlgetpodslapp。kubernetes。ionameapisixdashboard,app。kubernetes。ioinstanceapisixdashboardojsonpath{。items〔0〕。metadata。name})exportCONTAINERPORT(kubectlgetpodPODNAMEojsonpath{。spec。containers〔0〕。ports〔0〕。containerPort})kubectlportforwardPODNAME8080:CONTAINERPORTaddress0。0。0。0 浏览器访问:http:10。1。1。168:8080ssllist,账号密码都是admin。点击Create按钮,将刚刚生成的证书导入到APISIX之中。 增加APISIX的NodePortService 创建一个NodePortService,用于暴露APISIX的9779端口。这样,我们就可以通过外部的IP地址访问到APISIX了。catEOFkubectlapplyfspec:selector:app。kubernetes。ioinstance:apisixapp。kubernetes。ioname:apisixports:protocol:TCPport:9779targetPort:9779name:thriftnodePort:9779type:NodePortEOF 因为前边Minikube中我们配置了端口的范围覆盖到了9779,所以我们可以看到,这个NodePortService的端口在宿主机上也可以从Minikubeip的同一个端口访问到:minikubeserviceapisixsvcminikubeservicelistNAMESPACENAMETARGETPORTURL。。。defaultapisixsvcthrift9779http:192。168。49。2:9779。。。 当然,Minikube假设我们的服务都是HTTP的,给出的URL是HTTP:的。不用理会它,我们心里知道它是TCPoverTLS就好了。配置K8s外部DNS 这里需要配置一个DNS服务,让我们可以通过nebulastoraged0。nebulastoragedheadless。default。svc。cluster。local等三个域名通过Minikube的NodePortService访问到NebulaGraph的storaged服务。 获得Minikube的IP地址:minikubeip192。168。49。2 配置etchosts192。168。49。2nebulastoraged0。nebulastoragedheadless。default。svc。cluster。local192。168。49。2nebulastoraged1。nebulastoragedheadless。default。svc。cluster。local192。168。49。2nebulastoraged2。nebulastoragedheadless。default。svc。cluster。local192。168。49。2nebulametad0。nebulametadheadless。default。svc。cluster。local验证NebulaGraphStorageClient可以从所有的节点中获取到数据 这里,为了方便,我们用到Python的客户端。 由于在写本文的时候,NebulaGraphPython客户端的StorageClient尚未支持TLS,对它支持的PR刚好是我为了本实验写的:https:github。comvesoftincnebulapythonpull239。 所以,这里从个人分支安装这个客户端:gitclonehttps:github。comweygunebulapython。gitcdnebulapythonpython3mpipinstall。python3mpipinstallipython进入ipythonipython 我们在iPython中交互式验证:fromnebula3。mclientimportMetaCache,HostAddrfromnebula3。sclient。GraphStorageClientimportGraphStorageClientfromnebula3。ConfigimportSSLconfigimportsslimportosmetacacheMetaCache(〔(nebulametad0。nebulametadheadless。default。svc。cluster。local,9559)〕,50000)storageaddrs〔HostAddr(hostnebulastoraged0。nebulastoragedheadless。default。svc。cluster。local,port9779),HostAddr(hostnebulastoraged1。nebulastoragedheadless。default。svc。cluster。local,port9779),HostAddr(hostnebulastoraged2。nebulastoragedheadless。default。svc。cluster。local,port9779)〕自签证书配置currentdiros。path。abspath(。)sslconfigSSLconfig()sslconfig。certreqsssl。CERTOPTIONALsslconfig。certreqsssl。CERTOPTIONALsslconfig。cacertsos。path。join(os。path。expanduser(。localsharemkcert),rootCA。pem)sslconfig。keyfileos。path。join(currentdir,nebulastoragedheadless。default。svc。cluster。local1key。pem)sslconfig。certfileos。path。join(currentdir,nebulastoragedheadless。default。svc。cluster。local1。pem)实例化StorageClientgraphstorageclientGraphStorageClient(metacache,storageaddrs,5000,sslconfig)验证可以从所有的节点中获取到数据respgraphstorageclient。scanvertex(spacenamebasketballplayer,tagnameplayer)whileresp。hasnext():resultresp。next()forvertexdatainresult:print(vertexdata) 结果:(player112:player{name:JonathonSimmons,age:29})(player117:player{name:StephenCurry,age:31})(player119:player{name:KevinDurant,age:30})(player134:player{name:BlakeGriffin,age:30})(player141:player{name:RayAllen,age:43})(player144:player{name:ShaquilleONeal,age:47})(player149:player{name:BenSimmons,age:22})(player100:player{name:TimDuncan,age:42})(player101:player{name:TonyParker,age:36})(player110:player{name:CoryJoseph,age:27})(player126:player{name:KyrieIrving,age:26})(player131:player{name:PaulGeorge,age:28})(player133:player{name:YaoMing,age:38})(player140:player{name:GrantHill,age:46})(player105:player{name:DannyGreen,age:31})(player109:player{name:TiagoSplitter,age:34})(player111:player{name:DavidWest,age:38})。。。总结NebulaGraph查询接口的负载均衡可以借助K8sService来做;NebulaGraph底层存储接口的暴露在K8s中可以利用APISIXStreamProxy和SNI来优雅实现;利用API网关对出口传输层的加密是一个很好的选择,相较于用NebulaGraph原生的TLS的方式。一些坑 fbthriftPython并不支持发送extendhostname(SNI):https:github。comvesoftincnebulapythonpull238,写了PR去做支持。这时候APISIX中的报错是failedtofindSNI:2022111510:18:26〔error〕7878:1744270stream〔lua〕init。lua:842:streamsslphase():failedtofetchsslconfig:failedtofindSNI:pleasecheckiftheclientrequestsviaIPorusesanoutdatedprotocol。Ifyouneedtoreportanissue,provideapacketcapturefileoftheTLShandshake。,context:sslcertificatebylua,client:172。17。0。1,server:0。0。0。0:9779 参考延伸阅读的36。 此外,我还发现APISIXstream里边不解析上游node域名,我查了所有DNS都没有问题,去提了issue才知道是已知问题:https:github。comapacheapisixissues8334,只好先手配IP:Port作罢。2022111512:26:59〔error〕4444:9538531stream〔lua〕resolver。lua:47:parsedomain():failedtoparsedomain:nebulastoraged0。nebulastoragedheadless。default。svc。cluster。local,error:failedtoquerytheDNSserver:dnsclienterror:101emptyrecordreceivedwhileprereadingclientdata,client:172。17。0。1,server:0。0。0。0:97792022111512:26:59〔error〕4444:9538531stream〔lua〕upstream。lua:79:parsedomainfornodes():dnsresolverdomain:nebulastoraged0。nebulastoragedheadless。default。svc。cluster。localerror:failedtoquerytheDNSserver:dnsclienterror:101emptyrecordreceivedwhileprereadingclientdata,client:172。17。0。1,server:0。0。0。0:97792022111512:26:59〔error〕4444:9538531stream〔lua〕init。lua:965:streamprereadphase():failedtosetupstream:novalidupstreamnodewhileprereadingclientdata,client:172。17。0。1,server:0。0。0。0:9779延伸阅读https:gist。github。comweygu950e4f4c673badae375e59007d80d372https:gist。github。comweygu699b9a2ef5dff5f0fb5f288d692ddfd5https:docs。python。org3libraryssl。htmlssl。SSLContext。sslsocketclasshttps:github。comapachethriftcommit937228e030569bf25ceb379c9491426709792701https:github。comapachethriftpull894https:github。comapachethriftblobe8353cb46e9f5e71f9b76f55d6bf59530b7f98eflibpysrctransportTSSLSocket。pyL184 谢谢你读完本文() 要来近距离体验一把图数据库吗?现在可以用用NebulaGraphCloud来搭建自己的图数据系统哟,快来节省大量的部署安装时间来搞定业务吧NebulaGraph阿里云计算巢现30天免费使用中 想看源码的小伙伴可以前往GitHub阅读、使用、()star它GitHub:https:github。comvesoftincnebula