百亿级图谱的毫秒级查询:贝壳分布式图数据库选型与实践

对着背影说爱祢 提交于 2020-08-11 04:26:09
你想知道百亿级图谱如何实现毫秒级查询吗?社区众多的图数据库中如何才能挑选到一款适合实际应用场景的图数据库呢?贝壳找房的行业图谱480亿量级的三元组究竟是如何存储的呢?

 

本文分享的主题《分布式图数据库在贝壳找房的应用实践》将带你探索上述问题并从中得到解答,共分为以下五大块内容:

 

  • 图数据库简介

  • 图数据库技术选型

  • 图数据库平台建设

  • 原理&优化&不足

  • 未来规划

 

一、图数据库简介

 

 

先来看一个问题:贝壳找房最大的图谱——行业图谱,目前量级已经达了480亿三元组,如此海量的图谱数据究竟应该如何存储,如何查询,才能满足高并发场景下的毫秒级响应,从而支持贝壳业务的快速发展呢?我们带着这个问题开始本次的分享。

 

1、为什么需要图数据库?  

 

 

贝壳的行业图谱中包含了很多信息,比如房源、客户、经纪人、开发商、小区、地铁、医院、学校、超市、电影院等等。

 

我们假设这样一种特殊的查询场景:找出开发商是XXX,小区绿化率大于30%,周边200米有大型超市,500米有地铁,1000米有三甲医院,2000米有升学率超过60%的高中,房价在800W以内,最近被经纪人带看次数最多的房子。

 

这可能是一个客户想要的房子,但是各位觉得有哪个产品可以支持么?

 

如果说我们用传统的关系型数据库,MySQL或者Oracle可以吗?那是不是我们要关联房源表、客户表、经纪人表、开发商表等等,一次关联几十张表才可能得到想要的结果?但明显这是不现实的。

 

那ES ( Elasticsearch ) 可以吗?ES在搜索领域非常火,它可以解决吗?其实ES也是解决不了的,ES要搜这样的房源,肯定是需要有一张很宽的房源表,那怎么搜索这套房源周边200米有大型超市?难道要建距离周边超市的距离这样一个字段吗?显然也是不现实的。

 

HBase更不用说了。

 

所以显而易见这种行业图谱的数据只能使用图数据库,比如Neo4j这样的存储引擎才可以支持。

 

2、图数据库简介  

 

简单介绍一下什么是图数据库?

 

  • 不是存储图片的数据库。

  • 存储节点和关系,以图结构存储和查询。

 

应用场景非常广泛,远不止我们聊到的行业图谱、知识图谱这些,它包含:

 

  • 社交网络、计算机网络、道路网络、电信网络。

  • 关联查询,搜索推荐。

  • 风险预测,风控管理。

  • 业务流程,生物流程。

  • 公司或市场结构。

  • 事件及其之间的因果关系或其他联系。

 

还有很多其他领域都可以使用图数据库来解决。

 

这是DB-Engines上的各种类型数据库的流行度趋势图,可以明显的看出图数据库在近两年来越来越流行,越来越被大众所关注。

 

 

这是图数据库领域的各类产品,排名第一的就是大家最熟悉的Neo4j,下面还有很多开源的、闭源的、单机的、分布式的等等各种图数据库,产品非常繁多。

 

 

二、图数据库技术选型

 

刚才提到图数据库的应用场景非常广:搜索、推荐、关系图谱、知识图谱等等。目前贝壳也有各种场景需要使用到图数据库,但选型各不相同,有的部门使用JanusGraph,有的使用Neo4j,每个有需要的部门都得从头搭建一套。

 

所以我们想是不是应该有一个通用的图数据库平台,可以支撑所有需要使用图数据库的场景,然后让做关系图谱、行业图谱的同学可以更关注于上层的算法和策略,而无需关注底层的存储、分布式、高性能、高可用等等?

 

答案显然是确定的:我们需要这样一个统一的图数据库平台。那么目前图数据库领域已经有这么多产品,要做图数据库平台的话,到底应该选用哪一个呢?所以我们进入第二个主题,图数据库的技术选型。

 

1、图数据库技术选型  

 

当我们进行图数据库技术选型的时候,我们具体需要关注哪些指标?哪些因素会影响我们的决策呢?我们主要关注以下几个方面:开源、成熟度、可扩展性、文档丰富度、性能、稳定性、运维成本、易用性。其中运维成本是比较容易忽视的,但我们做技术选型时必须考虑清楚,每种选型的运维成本是否是可接受的,投入产出比是否值得。

 

 

前面看到图数据库虽然有很多种,但实际上开源的、流行的图数据库就只有以下几种:Neo4j、OrientDB、ArangoDB、JanusGraph、Dgraph,Neo4j实际上是用来做对比的,OrientDB和ArangoDB都是老牌的图数据库了,发展比较早,从2012、2013年就开始做了,JanusGraph和Dgraph是比较新的,从2016、2017年才开始做。

 

2、主流图数据库对比  

 

那它们的主要区别是什么呢?我们将上述主流的图数据库做一下调研,先简单粗略的对比分析一下:

 

主流图数据库对比

 

Neo4j历史悠久,且长期处于图数据库领域的龙头地位,那为什么不考虑它呢?原因很简单,因为它开源的社区版本只支持单机,不支持分布式。

 

OrientDB和ArangoDB它们起步比较早,最初的时候都是一个单机的图数据库,然后随着用户数据量的不断增加,后期增加了分布式模式,支持集群和副本,但是经过调研发现,可能是由于后加的功能,他们的分布式支持的不是很好。

 

所以主要注意力放到了JanusGraph和Dgraph上,他们发展的比较晚,从设计之初就考虑了分布式和扩展性,所以对分布式支持的非常好,也都是完全的开源免费,存储数据模型也都是专为图数据而设计。

 

他们有一个比较大的区别就是,JanusGraph的存储需要依赖于其他存储系统,而Dgraph使用自身的存储系统,这就造成了前面提到的运维成本的问题。

 

例如JanusGraph多数使用HBase作为底层存储系统,而HBase又依赖于Zookeeper和HDFS,另外JanusGraph的索引又依赖于ES,所以想要搭建一套完整的JanusGraph,需要同时搭建维护好几套系统,维护成本非常大;而Dgraph这些都是原生支持的,所以相对来说,Dgraph维护成本低很多。

 

下面我们具体对比一下二者的架构。

 

3、JanusGraph架构  

 

JanusGraph架构

 

前文提到,JanusGraph的存储系统依赖于像Cassandra、HBase、BerkelyDB等等这样的存储系统,索引系统依赖于Elasticsearch、Solr、Lucene等等;也基于这些原因,它和大数据生态结合的非常好,可以很好地和Spark结合做一些大型的图计算。

 

但缺点就是它的维护成本会非常高,依赖于这么多的外部系统,搭建一套JanusGraph系统的同时需要搭建好几套依赖系统;另一方面就是稳定性,根据经验来看,系统越复杂,依赖系统越多,整体可控性就越差,稳定性风险就越大。

 

4、Dgraph架构  

 

这是Dgraph的架构,它的架构其实非常简单,所有功能都是原生支持的,不依赖于任何第三方系统,下面这张图从下往上看:

 

Dgraph架构

 

  • zero:集群大脑,用于控制集群,将服务器分配到一个组,并均衡数据。通过raft选主;相当于hadoop的namenode或者Elasticsearch的master。

  • alpha:存储数据并处理查询,托管谓词和索引,即datanode。

  • group:多个alpha组成一个group,数据分片存储到不同group,每个group内数据通过raft保证强一致性。

  • ratel:可视化界面,用户可通过界面来执行查询,更新或修改schema。

  • 同时Dgraph还支持gRPC或者HTTP来连接alpha进行写入或查询。

 

Dgraph只有一个可执行文件,通过指定不同的参数在不同的机器上启动,就能自动组成集群,无需搭建维护其他任何第三方系统,这是它的优势。那是不是就能通过这样的架构对比,因Dgraph运维简单就直接选择它呢?

 

肯定不行,我们还需要做一些性能压测来对比,如果说JanusGraph的性能是Dgraph的好几倍,那维护成本高些也是可以接受的。所以基于这个目的,我们对这两个图数据库做了详细的性能对比测试。

 

5、性能对比  

 

JanusGraph和Dgraph性能对比:

 

 

在48核、128G内存、SATA硬盘的三台物理机环境,4800万个点、6300万条边、4.5亿三元组、总计30G的数据集下进行性能对比测试:

 

1)写入性能维度来看,分为实时写入和初始化写入三元组两种,在实时写入对比中,点的写入性能:JanusGraph达到15000/s,Dgraph达到35000/s;边的写入性能:JanusGraph达到9000/s,Dgraph达到10000/s。

 

2)查询性能维度来看,相差较大,主要测试图数据库典型的几种场景,比如点的属性,点的一度、二度、三度关系,包括最短路径等等。大家可以从表中看到,在简单查询的场景下,比如查询点的属性、点的一度关系时,二者都是毫秒级别的,没有太大的性能差别;但是随着查询越来越复杂,JanusGraph的查询越来越慢,最后查到三度的顶点和属性要消耗700多毫秒,但Dgraph一直保持在几毫秒之内。

 

所以可以看出Dgraph相对JanusGraph的查询性能的优势是非常大的。

 

6、JanusGraph VS Dgraph  

 

总结一下两种图数据库特性的对比:

 

 

  • 架构方面:Dgraph是分布式的,而JanusGraph构建于其他分布式数据库之上。

  • 副本方面:Dgraph是强一致性的,JanusGraph需要依赖底层的存储DB。

  • 数据均衡方面:Dgraph支持自动均衡,JanusGraph也是依赖底层的存储DB。

  • 语言方面:JanusGraph使用了比较常用的Gremlin,而Dgraph使用基于GraphQL改进的GraphQL+-。

  • 全文检索、正则表达式、地理位置检索方面:Dgraph是原生支持的,JanusGraph依赖外部检索系统。

  • 可视化方面:Dgraph有自己的可视化系统,JanusGraph依赖外部系统。

  • 维护成本方面:由于不依赖其他系统,Dgraph远低于JanusGraph。 

  • 写入性能方面:Dgraph稍高一些。

  • 查询性能方面:深度查询时,Dgraph性能远高于JanusGraph。

 

所以基于以上对比,我们最终选择了使用Dgraph来构建我们的图数据库平台。

 

三、图数据库平台建设

 

在图数据库选型确定后,就需要真正地把图数据库平台搭建起来。

 

1、集群的建设  

 

 

上文提到,搭建Dgraph集群其实非常简单,我们使用docker+k8s技术对Dgraph进行统一的容器化部署和管理。

 

如图使用三台服务器,每台服务器上启动四个节点,其中三个是Alpha节点,就是存储数据、索引、执行查询的节点,一个zero节点,是Dgraph的控制节点。

 

需要注意到的一点是,每个Group的3个Alpha用于存储同一份数据的三个副本,一个Group的不同的Alpha肯定是不能在同一台机器的,而哪几个Alpha组成一个Group是zero根据副本数来确定的,比如dgraph zero -- replicas 3这样启动时候就指定三个副本数,并且根据Alpha的启动顺序来确定。

 

所以有个启动技巧就是,先在第一台服务器上启动Alpha1,然后切到第二胎服务器上启动Alpha2,再到第三台服务器上启动Alpha3,这三个先启动的Alpha组成第一个Group;然后轮流顺序地启动其他的Alpha 4、5、6组成第二个Group,7、8、9组成第三个Group,不能直接在第一台服务器上直接启动123,这样组成一个Group是无法保证高可用的,因为当这一台机器挂掉之后三个副本就全部丢失了。

 

左侧是Dgraph的启动命令,启动zero时候指定一下副本数量;启动alpha的时候,指定一下zero的地址就可以了;这样就启动了一个Dgraph集群,由此可见,它的搭建和维护成本确定很低。

 

2、数据写入  

 

集群搭建好了以后,就要考虑数据的写入了,因为是要做一个通用的图数据库平台,所以要考虑多种的数据写入模式,比如实时数据流、批量数据流和初始化数据流。

 

 

1)实时数据流模式:有一个Data-Accepter模块,用户可以将实时变更的数据用过这个模块推过来,然后通过Kafka做异步消峰,写到Kafka队列里面,后面有Graph-Import模块从Kafka将数据取出写到Dgraph集群。

 

2)批量数据流模式:比如说要做全量的数据更新,目前贝壳大部分的行业图谱数据都是存在Hive或者是HDFS中的,这时候会有一个Hive2Kafka的spark任务,从用户的Hive表或者HDFS拿到全部的图谱数据,同样写入Kafka,最后通过Graph-Import模块从Kafka取出数据写入Dgraph集群。

 

3)初始化数据流模式:Dgraph还有一个不同点就是支持数据初始化导入,这个导入是非常快的,比如像行业图谱这样的480亿数据,第一次全部导入,按照这种数据流的模式一条条去写一定是要花很多时间的。

 

所以采用Dgraph提供的初始化导入,使用Dgraph的Bulk Loader接口,通过MapReduce的方式预先生成它的数据文件和索引文件,然后再启动Dgraph的Alpha节点去加载这些文件,这样可以实现非常快的初始化导入,后面会详细说这块内容。

 

我们也支持这种初始化数据流,通过脚本可以一键式的完成初始化数据生成,然后调用k8s接口启动一个Dgraph集群,再加载生成好的数据,最后返回一个查询API接口给Dgraph的使用方。

 

3、数据查询   

 

 

完成数据导入之后,接下来就是数据查询了,上图为Dgraph的可视化界面Ratel,左边输入一个查询语句之后,右边就会出现相应图的展示;这个例子是要查出名字包含“秀园”,绿化率大于百分之三十的小区的附近一千米内的所有幼儿园。右边展示图的下面可以看到Dgraph的服务端查询只花了24毫秒,加上来回网络开销总计也就91毫秒,非常的快。

 

4、Graph SQL  

 

 

最上面就是Dgraph的查询语句,大家可以感受一下,并不是那么简单,是需要一定的学习成本的。而我们前面说到建设图数据库平台需要考虑易用性,需要尽量简化使用方的学习成本,所以需要考虑是否有更简单的查询语法。

 

这种查询在Gremlin是怎么写的呢?如图,使用多个has然后select就可以筛选出来,但同样不够简单明了。

 

考虑到大部分程序员最熟悉的查询语言就是SQL,甚至不是程序员,一些数据分析师也会SQL,那是否可以使用SQL对图数据库查询?

 

于是我们设计了一套使用SQL查询图数据库的语言,称之为Graph SQL。比如上图最下面的语句:select小区名字、小区绿化率、幼儿园名字;from小区到幼儿园的这么一个关系,此处和SQL有所不同,不是from一个表,而是from一个小区到幼儿园的关系子图;然后where小区名字包含"秀园",绿化率大于30%,距离幼儿园的距离小于1000米。

 

 

上图为目前我们提供的一个完整的基于SQL查图的语法,增加了一些特定的图的关键字,比如:shortestpath:查询图的最短路径;degree:查询一度关系、二度关系、三度关系等等。

 

然后也支持查点、查边、查节点的属性等等,后面还有GROUP BY、HAVING、ORDER BY、LIMIT等等,LIMIT支持对点、也支持对边进行LIMIT。

 

当然目前只支持了一些简单的语法,后面复杂的查询还在继续完善中。通过支持这种类SQL的查询语法,就可以极大的降低图数据库平台的接入成本和学习成本。 

 

 

这是最终的一个实现效果,可以看到通过发送一个简单的HTTP请求,里面包含一个SQL查询语句,可以很快的返回图数据库的查询结果。

 

5、整体架构  

 

汇总前面的集群搭建、数据写入、数据查询,再复用统一的服务治理框架,整合起来就是我们当前图数据库平台的整体架构了:

 

 

统一的网关,用来做鉴权、分发、限流、熔断和降级。

 

在网关之下有统一的数据流模块和查询层模块:

 

  • 数据流模块包含数据源、数据接收、增量、全量、Kafka、数据导入。

  • 查询层模块支持Graph-SQL,如果有Graph-SQL无法支持的查询,也可以使用原生的GraphQL+-来进行复杂的查询,然后通过Graph-Client连接到底层的Dgraph集群执行并返回结果。

 

其中:

 

  • Dgraph集群整体是通过Docker+K8s虚拟化技术部署到物理机上的

  • 右边复用了贝壳搜索平台的整体服务治理能力,所有微服务都通过注册中心、配置中心、负载均衡、消息总线、熔断降级、链路追踪、监控告警等技术进行统一调度、管理、监控等。

 

四、原理&优化&不足

 

完成了图数据库平台的搭建之后,相当于只是完成了从0到1的工作,只是有了这样一个图数据库平台可用;下一步就是要完成从1到N 的过程,需要保证平台的稳定性、提升平台的性能和体验,这是第二部分的工作。

 

为了完成第二部分的工作,就需要对Dgraph做一些深入的学习、深入的理解、深入的优化,需要知道它的优势和不足,知道他的底层的原理实现。

 

1、Dgraph原理  

 

简单介绍一下Dgraph的原理:

 

 

① 存储引擎

 

Dgraph的存储引擎是自研的Badger,完全由Go语言开发。最初Dgraph存储也是使用RocksDB,但后来上层通过go调用,出现一些内存溢出的问题,于是Dgraph团队干脆自己用Go实现了一个高效的、持久化的,基于LSM的键值数据库,并且号称随机读比RocksDB快3.5倍。

 

② 存储结构 ( 因为存储引擎是KV的,所存储结构也是KV的 )

 

(Predicate, Subject) --> [sorted list of ValueId],Key是由谓词和主语组成的,Value是一个有序的数组。

 

举个例子:

 

(friend, me)-->[person1, person2, person3, person4, person5],Key是friend和me,friend是关系,me是主语,这样组成的一个Key;Value是有序的,me的所有friend,从person1、person2到person5这些ID组成的一个有序的数组。

 

基于这样的底层存储结构设计,Dgraph同一个谓词下的所有数据都存储在同一个数据节点甚至同一个数据块中,所以这样查询一个谓词数据时候,只需要一次RPC调用就可以拿到这个谓词下面全部需要的数据,对于后面的一度、二度、多度的关联查询有非常大的性能提升,这是它核心的优势。

 

③ 数据分片 ( 作为一个分布式系统,要想平滑的扩展,必须要支持数据分片 )

 

根据谓词分片,相同谓词的数据按序存储在同一个节点,减少RPC,提升查询性能,不同谓词可能是在不同的节点。

 

定期数据均衡 ( rebalance_interval ),zero节点会定期的检测各个节点的数据是否均衡,如果某个节点数据过大或者过小,会导致查询的性能下降,因此zero节点会尽量的保证每个节点的数据均衡。

 

group根据replicas和alpha启动顺序确定,因为Dgraph的副本一致性是依赖Raft协议的,所以要保证至少有三个节点,才能保证数据的强一致性。

 

④ 高可用

 

每个group至少3个alpha,互为副本,raft协议保证强一致性;每个group中的alpha的数据保持一致,这样某个alpha节点挂了,可以通过其他的alpha进行数据恢复。

 

write-ahead logs,预写日志;分布式很常见的WAL机制,为了提升写入性能,一定是先写缓存后刷磁盘的,不会直接写磁盘的,那样性能会非常低,但先写内存后写磁盘会带来一个问题,一旦机器挂掉了,内存数据没有刷到磁盘中,那这部分数据就会丢失。因此大部分分布式系统,比如:HBase、Elasticsearch、Dgraph等都是数据写内存之前,先预写日志,日志会实时刷到磁盘上,然后再将数据写内存,一旦内存中数据丢失了,可以通过磁盘上的日志回放这些数据,从而保证高可用性。

 

2、Bulk Loader优化  

 

 

其实我们对于Dgraph的研究也仅仅只有几个月而已,所以目前只是做了一些小的优化:480亿的行业图谱如何快速的导入到集群中?

 

最开始使用Java客户端写入,发现这种方法性能非常低,完全写完可能需要整整一周的时间。

 

然后使用Dgraph的Bulk Loader写入,先生成索引数据,再通过alpha节点加载,最后启动集群来提供服务,这种方式需要48小时才能全部写入完成,时间也有点长,是否还能进一步优化提升速度呢?

 

于是我们研究了一下Bulk Loader的源码,发现只是一个简单的Map Reduce过程,但他是在单机上执行的,使用单机执行是因为它要分配一个全局唯一的UID,为了保证UID的唯一性和顺序性而选择单机执行,使用单机多线程,启动多个Map和Reduce线程,然后每个线程生成Shard文件,最后通过Dgraph的alpha加载数据。

 

于是基于对源码的理解,我们发现是可以优化的,Dgraph原本作为分布式系统,各种查询写入都是可以做线性扩展的,不能说最初的批量导入只能是一个单机的模块。

 

所以我们对源码进行了一定的优化,将原来的单机多线程改为了多机多线程模式,首先通过Partition模块,为原来的每条数据分配一个UID,这块还是单机执行的,把相同group的数据分到一个数据块中。

 

然后把这些数据块分发到不同机器上,每台机器上都可以启动原来的Map进程和Reduce进程,每台机器都可以生成Dgraph需要的数据文件,再在每台机器上启动alpha进程加载这些数据文件,直至整个集群启动成功为止。这样把480亿三元组的数据初始化导入从48小时提升到了15小时,提升了三倍性能。

 

3、性能压测  

 

把这480亿数据导入后,就可以回答我们最初的问题了,它真的可以支持百亿级图谱数据毫秒级查询吗?

 

 

于是我们专门对其进行了性能压测,如上图,可以看出Dgraph的性能确实很好,底下横坐标是我们的并发线程,左边纵坐标是响应时间 ( 单位毫秒 ),右边纵坐标是QPS吞吐率;当我们用1000并发压测时,仍然可以保持在50ms的响应延迟,并且QPS可以达到15000/s,性能非常好。

 

4、Dgraph不足  

 

那Dgraph性能这么好,运维又简单,是不是就可以说Dgraph是一个完美的图数据库呢?是不是所有的场景都可以用Dgraph来支持呢?显然不是的,没有最完美的系统,只有最适合你业务的系统;就像没有最完美的人,只有最适合你的人一样。Dgraph也是有它的缺陷和不足的:

 

 

① 不支持多重边

 

就是说任意一对顶点,相同标签类型的边只允许存在一条;在JanusGraph中,两个顶点确定之后,是允许存在多重边的。比如:Dgraph中,我和你是同学关系,那只能有一条叫同学关系的边;但在JanusGraph中,我和你可以同时是小学同学、中学同学、大学同学,有三条同学关系的边。

 

② 一个集群只支持一个图

 

目前Dgraph一个集群只支持一个图,支持多图这个功能官方正在开发中,后期会支持;目前对贝壳的影响还不大,贝壳的图谱都是比较大的、隔离的,比如行业图谱480亿本身就是需要一个单独的集群的,不会和其他图谱共用,目前还够不成太大的问题。后期自然是希望官方可以尽快的支持了。

 

③ 大数据生态兼容不够

 

不像JanusGraph和大数据生态兼容的那么好,因为JanusGraph本身就是基于HBase存储的;Dgraph本身使用Go开发,使用Spark对它进行大并发写的时候,会出现overload的状态。

 

④ 不是很成熟

 

Dgraph从2016年开始做,总结下来并不是很成熟,有很多小问题,但是更新也比较快,很多问题很快就修复了。

 

总结一下,就是没有最完美的系统,只有最合适的系统;我们做技术选型,主要就是看它的优势是不是我们需要的、缺陷是不是我们可以接受的,所以最后我们选择了Dgraph作为我们的图数据库选型,然后基于它搭建我们的图数据库平台,后续开放给需要的各个业务方去使用。

 

五、未来规划

 

最后简单说一下未来的规划,我这边主要是负责贝壳整体的搜索平台建设,Dgraph建设只是其中的一部分,在整个搜索的架构之下。目前我们已经有基于Elasticsearch的文本检索引擎,以及基于Dgraph的图数据检索引擎,后续还会有基于Faiss的向量检索引擎。

 

搜索云平台是一个业务接入平台,将与下层的效果平台、算法平台、三大引擎、容器平台全部打通,同时集成统一的服务治理能力,整体构成一个搜索中台。以后业务方不用再关心底层的数据存储、写入和查询,由搜索中台来统一整合相关能力,然后提供统一的入口和出口,同时保障整体性能和稳定性,从而快速对业务赋能,业务方只需要关注上层的业务逻辑和策略。

 

 

所以对图数据库的整体规划是:

 

  • 深入学习,性能、稳定性优化,源码改进。

  • 作为搜索中台基础引擎,支持各种图数据库检索需求。

  • 接入搜索云平台,界面化操作快速配置接入,简化运维。

  • 增强搜索功能,提升搜索效果。

  • 支持行业图谱、关系图谱、知识图谱、风险管理……

 

今天的分享就到这里,谢谢大家。

 

作者丨高攀 来源丨DataFunTalk(ID:datafuntalk) dbaplus社群欢迎广大技术人员投稿,投稿邮箱: editor@dbaplus.cn
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!