作者|林武,携程数据开发工程师,专注于图数据库等领域。背景携程金融成立于2017年9月,在金融和风控业务中,有很多场景需要对图关系网络进行分析和实时查询。传统关系型数据库难以保证此类场景下的关联性能,实现复杂度高,离线关联耗时过长,因此对图数据库的需求越来越大。携程财经从2020年开始引入大规模图存储和图计算技术,基于星云构建了千亿节点的图存储和分析平台,并取得了一些实际应用成果。本文主要分享星云在携程财经的实践,希望能给大家带来一些实战启发。本文主要分析以下几个部分:图基础介绍图平台搭建内部应用案例分析痛点与优化总结规划由一系列边配对(连接)的顶点集合。比如我们用图来表示社交网络,每个人都是一个顶点,认识的人通过边相连。在图数据库中,我们使用(start,edgetype,rank,end)来表示一条边。起点和终点比较好理解,表示一条边的两个顶点的进出方向。边类型用于区分异构图的不同边。比如我关注你,我给你转账。跟随和转移是两种不同类型的边缘。秩用来区分起点和终点相同的不同边。例如,A到B的多条转账记录中,起点、终点、边型完全相同。因此,需要时间戳作为等级来区分不同的方面。同时,顶点和边可以有属性,比如:A的手机号、银行卡、身份证号、家乡等信息可以作为A的点属性存在,A给B转账的边也可以有属性,例如转账金额和转账地点等属性。1.2什么时候用图片(信息采集自开源社区、公共技术博客、文章、视频)1)金融风控:欺诈电话的特征提取,比如不在三步社交邻居圈,被拒绝大量特征等实时识别拦截。(银行/网警等)转账实时拦截(银行/支付宝等)实时欺诈检测,羊毛党识别(电商)非法团伙识别,用户关联良好贷款记录,为用户提供贷款增加,收入增加2)通过影子集团进行股权渗透,集团客户多层交叉持股,识别层层股权嵌套的复杂关系(天眼查/企业查查)3)数据仓库开发过程中,数据血缘关系会由于数据的跨表关联而产生大量中间表,根据关系模型用图直接表达数据处理过程和数据流向,依赖时快速定位上下游关于任务问题。4)知识图谱构建行业知识图谱5)泛安全ip关系、计算机进程线程安全管理等黑客攻击场景关注、一起看文章等,产品相似度,实现好友精准推荐'产品或咨询。通过用户画像、好友关系等,进行用户分组,实现用户群体的精准管理。7)代码依赖分析8)供应链上下游分析如汽车供应链的上下游可能涉及数以万计的零部件和供应商。分析某些零件的成本上升/单一供应商/低库存等多维影响(捷豹)1.3谁在开发地图,谁在使用地图(信息收集于开源社区,公共技术博客,文章,视频)目前,国内几家大公司都开发了自己的图数据库,以满足内部应用的需要。大部分都是闭源的,只有百度的hugegraph是开源的。其他优秀的开源产品还有GoogleDgraph、vesoft的nebula等,其中nebula被国内互联网公司广泛使用。结合我们的应用场景,以及外部公测和内部压测,我们最终选择了星云来搭建金融图平台。2.图平台建设2.1.图平台搭建我们的图平台早期只有一个3节点的星云集群。随着图应用场景的不断扩展,需要满足实时检索、离线分析、数据同步验证等功能。演变成上面的架构图。1)离线图:主要用于图构建阶段(建模、图算法分析),通过spark-connector和集团大数据平台,另外我们还封装了Nebula提供的几十种常用的图算法作为工具,方便图分析师在spark集群中提交图算法作业。2)在线图:离线图分析确定最终建模后,通过spark-connector将数据导入在线图。通过对接qmq消息(群内消息框架)的实时更新,对外提供实时检索服务。同时每天通过spark-connector写入T+1个hive增量数据。3)全量验证:NebulaGraph虽然通过TOSS保证了正负边的插入一致性,但仍然不支持事务。由于数据不断更新,实时图和离线(Hive数据)可能存在不一致的情况,所以我们需要定期校验全量数据(将图读入Hive,与图数据进行对比存入Hive表,找出差异,修复),保证数据的最终一致性。4)集群规模:为满足千亿节点图业务需求,实时集群采用三台独立部署的高性能机器,每台64core/320GB/12TBSSD,版本nebulav2.5,跨机房部署.离线集群64core/320GB/3.6TBSSD*12,测试集群48core/188GB/5THDD*4.2.2。遇到的问题在nebula的应用过程中,也发现了一些问题,期待逐步完善:1)资源隔离,目前nebula没有资源组隔离功能,不同的服务会相互影响;如果服务地图A正在导入数据,服务地图B的在线延迟很高。2)版本升级问题:Nebula在版本升级过程中需要停止服务,无法实现热更新;对于实时风控等可靠性要求非常高的场景非常不友好。在这种情况下,如果需要保证在线升级,就需要配置主备集群,每个集群在size之后一个一个升级,这样会增加业务的复杂度和运维成本。客户端不兼容,需要客户端随服务器升级版本。对于一个已经被多个应用使用的星云集群,很难协调所有应用方同时升级客户端。3.内部应用案例分析3.1数据血缘图数据治理是近几年的热门话题。是解决数据仓库无序扩展的有效手段。其中,数据沿袭是有效数据治理的重要基础。金融使用星云构建数据血脉图,支撑数据治理的体系建设。数据沿袭是数据产生的环节,记录??了数据处理的流程,经过了哪些流程和阶段;主要解决ETL过程中几十个甚至上百个中间表造成的复杂表关系。借用数据沿袭,可以清晰记录从数据源头到最终数据的生成过程。图a是数据亲缘关系图。数据库名+表名作为图的顶点,保证点的唯一性。点属性是单独的库名和表名,可以通过库名或表名进行属性查询。两个表之间将建立一条边。边的属性主要存储任务的产生和运行,例如:任务开始时间、结束时间、用户ID,以及其他与任务相关的信息。图b是实际查询中的关系图。箭头方向表示表的处理方向。我们可以通过上游或者下游表快速的找到它的依赖关系,清晰的展示出从上游到下游的每一个环节。如果要表达复杂的血缘关系图,需要通过传统关系型数据库实现复杂的SQL(循环嵌套),性能比较差。但是,如果实现图数据库,就可以直接按照数据的依赖关系存储起来,读起来很方便。比传统DB更快,非常简洁。目前,数据沿袭也是金融BU在图数据库上的经典应用。3.2风控关系人图关系人图常用于欺诈识别等场景。是一种关系网络,通过ID、设备、手机标识等媒体信息将不同的用户关联起来。比如用户A和用户B共用一个WiFi,则他们是局域网下的关联方;用户C与用户D相互下单,为下单关联方。简而言之,系统通过多个维度的数据将不同的用户关联起来,这就是关系人图。在构建模型时,通常需要查询某个时间点(如欺诈事件发生前)的关系图,并对该时间点的图进行模型提取和特征构建。我们称此过程图回溯。返回的图数据也会根据回溯时间点动态变化;比如一个人上午和下午分别打了一个电话,需要在中午时间追溯这个人的图关系,只会出现上午的通话记录。对于图,每一种边都有这样的时间特性,每次查询都需要限制时间。对于图回溯场景,我们最初尝试通过HIVESQL来实现,发现对于二阶及以上的图回溯,SQL表达式会非常复杂,性能难以接受(例如二阶回溯Hive需要运行数小时,三阶回溯Hive难以实现);因此,尝试借助图数据库来实现,将时间建模为边秩,然后根据边关系进行过滤,实现回溯。这种回溯方式更加直观简洁,使用简单的API即可完成。与Hive相比,性能也提升了一个数量级以上(二阶回溯,图节点:100亿级,待回溯节点:10万级)。举个例子来说明:如图(a)所示,A点分别在时间t0、t1、t2建立边。B点和C点对应t0和t1,因为回到tx时间点时,t2还没有发生;当最终返回的图关系为t0和t1时,VertexA->VertexB,VertexA->VertexC(见图(c))。在这个例子中,一种边缘用于回溯。实际查询可能涉及2~3跳,并且存在异构边(打电话是一种边,订外卖是另一种边,订酒店机票是一种边。边都是不同类型的边),而这种异构图的数据具有回溯的特性,所以关系图的实际回溯查询也会变得复杂。3.3实时反欺诈图当用户下单后,将进入快速风控阶段:通过基于关系数据库和图数据库规则的模型特征计算,判断用户是否是否为风险用户,是否对该用户下单拦截(实时反欺诈)。我们可以根据图关系匹配模型规则来挖掘诈骗团伙。比如已知某个uid是诈骗团伙的成员,根据图关联判断与他关系密切的用户之间是否存在诈骗行为。为了避免影响正常用户的下单流程,风控阶段需要快速响应,因此对图查询的性能要求非常高(P95<15ms)。我们基于星云构建了百亿级的反欺诈图,并在优化查询性能方面做了很多思考。这个图模式是脱敏后的图模型的一部分,隐藏了很多建模信息。这里将在下一部分对查询过程和相关信息进行简单说明。上图展示了一个图查询过程。每个图查询同时从用户uid、用户手机和其他用户信息等多个起点开始。每条线都是一个关联查询,因此一个图查询由几十个点边查询组成。经过从起点开始的一跳查询和两跳查询,最终将结果集返回给风控引擎。系统会将用户的信息转化为用户的标签。查询图时,根据这些标签进行独立查询,如uid、mobile。例如,根据某个uid进行一跳查询,找出与其关联的5个手机号。然后根据这5个手机号进行独立的2-hop查询,可能会出来25个uid,查询会有数据扩容。因此,系统会做一个查询限制。查看这5个手机号关联的uid是否超过了系统设置的热点值。如果手机查询关联的手机号和uid过多,系统会判断为热点数据,不会返回副结果。(二阶/三阶回溯,图点边:百亿)。4.痛点及优化上述应用场景中,对于风控关系图和反欺诈图,由于图规模大(百亿级顶点),查询量大,时延要求高高,一些典型的问题,接下来简单介绍一下。4.1查询性能问题为了满足实时场景下p9515ms2-hop查询的要求,我们对graphschema、连接池、查询端做了一些优化:4.1.1牺牲写性能换取读性能首先,我们来看这样一个需求:查询id关联的手机号,需要满足这个手机号的关联边不超过3条。这里解释一下为什么要限制关联边的数量,因为对于我们正常人来说关联边的数量是有限的,对于大多数人来说都会有一个边的数量阈值比如p95,超过这个阈值就是脏数据.对于这个阈值验证,需要多跳一跳查询每个查询的结果。如图(a)所示,我们需要进行两次查询。第一跳查询是查询用户id关联的手机号,第二跳查询是保证我们的结果值合法(在阈值内),这样每一跳查询最终都需要2跳查询来满足。图中为图查询的gsql2步伪代码,无法满足我们本例的高时效性。如何优化呢?看下图(b):我们可以将热点查询固定在点属性上,这样一跳查询就可以知道该点有多少条关联边,避免图a中(2)的语句验证.还是以图(a)为例,从一个用户ID开始查询,查询他的手机号关联。此时因为手机号关联的边变成了一个点属性(修改了schema),所以图(a)中有2条query语句实现的功能可以变成querygofrom$idover$edgeName其中$手机号码。用户id边缘数据<5|limit5.这种设计的好处是可以加快读取时的验证过程,节省查询中的一跳。代价是:每写入一条边,需要同时更新两个点属性,记录该点关联的边情况,需要保证幂等性(保证重复提交不会叠加属性+1).插入一条边时,首先去图中检查这条边是否存在,不存在才进行写入边和点属性+1的操作。即我们以牺牲写性能换取读性能,通过定期校验保证数据的一致性。4.1.2池化连接减少延迟第二种优化方法是通过池化连接来减少延迟。Nebula官方的连接池需要建立一个初始连接-执行查询任务-每执行一次查询就关闭连接。但是在高频(QPS可达几千)查询场景下,频繁的创建和关闭连接会极大地影响系统的性能和稳定性。另外,连接建立过程平均耗时6ms,比实际查询时间约1.5ms高出好几倍,让人难以接受。因此,我们对官方客户端进行了重新封装,实现连接复用和共享。最终将查询p95从20毫秒减少到4毫秒。通过合理控制并发,我们最终将2跳查询的性能控制在p9515ms。把代码贴在这里供参考:publicclassSessionPool{/***创建连接池**@parammaxCountSession默认创建连接数*@paramminCountSession最大创建连接数*@paramhostAndPort机器端口列表*@paramuserName用户名*@parampassWord密码*@throwsUnknownHostException*@throwsNotValidConnectionException*@throwsIOErrorException*@throwsAuthFailedException*/publicSessionPool(intmaxCountSession,intminCountSession,StringhostAndPort,StringuserName,StringpassWord)throwsUnknownHostException,NotValidConnectionException,IOErrorException,AuthFailedException{this.minCountSession=minCountSession;this.maxCountSession=maxCountSession;this.userName=用户名;this.passWord=passWord;this.queue=newLinkedBlockingQueue<>(minCountSession);();}公开赛sionborrow(){Sessionse=queue.poll();如果(se!=null){返回se;}try{returnthis.pool.getSession(userName,passWord,true);}catch(Exceptione){log.error("执行借用会话失败,详情:",e);抛出新的RuntimeException(e);}}publicvoidrelease(Sessionse){if(se!=null){booleansuccess=queue.offer(se);如果(!成功){se.release();}}}publicvoidclose(){this.pool.close();}privatevoidinitSession()throwsNotValidConnectionException,IOErrorException,AuthFailedException{for(inti=0;i
