写点什么

Apache Kylin 在百度地图的实践

  • 2016-01-05
  • 本文字数:7980 字

    阅读完需:约 26 分钟

1. 前言

百度地图开放平台业务部数据智能组主要负责百度地图内部相关业务的大数据计算分析,处理日常百亿级规模数据,为不同业务提供单条 SQL 毫秒级响应的 OLAP 多维分析查询服务。

对于 Apache Kylin 在实际生产环境中的应用,在国内,百度地图数据智能组是最早的一批实践者之一。Apache Kylin 在 2014 年 11 月开源,当时,我们团队正需要搭建一套完整的大数据 OLAP 分析计算平台,用来提供百亿行级数据单条 SQL 毫秒到秒级的多维分析查询服务,在技术选型过程中,我们参考了 Apache Drill、Presto、Impala、Spark SQL、Apache Kylin 等。对于 Apache Drill 和 Presto 因生产环境案例较少,考虑到后期遇到问题难以交互讨论,且 Apache Drill 整体发展不够成熟。对于 Impala 和 Spark SQL,主要基于内存计算,对机器资源要求较高,单条 SQL 能够满足秒级动态查询响应,但交互页面通常含有多条 SQL 查询请求,在超大规模数据规模下,动态计算亦难以满足要求。后来,我们关注到了基于 MapReduce 预计算生成 Cube 并提供低延迟查询的 Apache Kylin 解决方案,并于 2015 年 2 月左右在生产环境完成了 Apache Kylin 的首次完整部署。

Apache Kylin 是一个开源的分布式分析引擎,提供 Hadoop 之上的 SQL 查询接口及多维分析(OLAP)能力以支持超大规模数据,最初由 eBay Inc. 开发并贡献至开源社区,并于 2015 年 11 月正式毕业成为 Apache 顶级项目。

2. 大数据多维分析的挑战

我们在 Apache Kylin 集群上跑了多个 Cube 测试,结果表明它能够有效解决大数据计算分析的 3 大痛点问题。

痛点一:百亿级海量数据多维指标动态计算耗时问题,Apache Kylin 通过预计算生成 Cube 结果数据集并存储到 HBase 的方式解决。

痛点二:复杂条件筛选问题,用户查询时,Apache Kylin 利用 router 查找算法及优化的 HBase Coprocessor 解决;

痛点三:跨月、季度、年等大时间区间查询问题,对于预计算结果的存储,Apache Kylin 利用 Cube 的 Data Segment 分区存储管理解决。

这 3 个痛点的解决,使我们能够在百亿级大数据规模下,且数据模型确定的具体多维分析产品中,达到单条 SQL 毫秒级响应。因此,我们对 Apache Kylin 产生了较高的兴趣,大数据计算查询分析的应用中,一个页面通常需要多条 SQL 查询,假设单条 SQL 查询需要 2 秒响应,页面共有 5 个 SQL 请求,总共就需要 10 秒左右,这是不可接受的。而此时,Apache Kylin 对于一个页面多条 SQL 查询响应的优势就尤为突出。

在实践过程中,根据公司不同业务的需求,我们数据智能团队的大数据 OLAP 平台后台存储与查询引擎采用了由 Apache Kylin、Impala 及 Spark SQL 组成,在中小数据规模且分析维度指标较为随机的情况下,平台可提供 Impala 或 Spark SQL 服务;在超大规模百亿级行数据的具体产品案例上,因查询性能需求较高,同时具体产品对其需要分析的维度和指标较为明确,我们使用 Apache Kylin 解决方案。下文将主要介绍 Apache Kylin 在百度地图内部的实践使用。

3. 大数据 OLAP 平台系统架构

(点击放大图像)

主要模块

数据接入:主要负责从数据仓库端获取业务所需的最细粒度的事实表数据。

任务管理:主要负责Cube 的相关任务的执行、管理等。

任务监控:主要负责Cube 任务在执行过程中的状态及相应的操作管理。

集群监控:主要包括Hadoop 生态进程的监控及Kylin 进程的监控。

集群环境

因业务特殊性,我们并未采用公司内部的Hadoop 集群进行计算、存储和查询,而是独立部署一台完整的集群,并独立维护。

集群机器:共4 台,1 台master(100G 内存) + 3 台slaves(30G 内存)。

软件环境:CDH + Hive + HBase + Kylin 0.71

4. 基于 Apache Kylin 的二次开发

4.1 数据接入模块二次开发

对于任何一个数据计算处理平台,数据的接入十分关键,就像熟知的 Spark,对数据接入也是十分重视。目前,我们的大数据 OLAP 平台可以支持 2 种数据源的引入: MySQL 数据源及 HDFS 数据源。在实践中,我们遇到一个问题,假设 MySQL 及 HDFS 数据源没有标识表示 T-1 天的数据已经计算完成的情况下,如何确定 T-1 天的数据已经准备就绪。对于 Hive 数据源,查询数据所在 Hive Meta 的 partition 是否就绪;对于 MySQL,我们目前想到的办法是间隔一定时间循环探测当天数据行数是否变化,如果没有变化,我们基本能够简单认为第 T-1 天的数据已经由数据仓库计算完毕,接下来就可以触发数据拉取模块逻辑将数据拉取到 Master 节点的本地文件系统中,根据业务判断是否需要对这些数据细加工,然后,导入到 Master 的 Hive 中,触发事实表对应任务涉及到的所有 cube,启动 MapReduce 计算,计算结束后,前端可以刷新访问最新数据。另外,如果到了指定时间,发现数据仓库端的数据仍旧没有准备好,数据接入模块会短信报警给仓库端,并继续循环检测直至指定时刻退出。

(点击放大图像)

数据引入模块

4.2 任务管理模块二次开发

任务管理对于计算型平台服务十分重要,也是我们大数据 OLAP 多维分析平台的核心扩展工作之一。对于用户而言,Apache Kylin 对于 Cube 的最小存储单位为 data segment,类似于 Hive 的 partition,data segment 采用左闭右开区间表示,如 [2015-11-01,2015-11-02) 表示含有 2015-11-01 这一天的数据。对于 Cube 数据的管理主要基于 data segment 粒度,大致分为 3 种操作: 计算 (build)、更新 (refresh)、合并 (merge)。对于一个具体产品来说,它的数据是需要每天例行计算到 cube 中,正常例行下,每天会生成 1 个 data segment,但可能会因为数据仓库的任务延迟,2 天或多天生成 1 个 segment。随着时间推移,一方面,大量的 data segment 严重影响了性能,另一方面,这也给管理带来了困难和麻烦。因此,对于 1 个 cube,我们按照 1 个自然月为 1 个 data segment,清晰且易管理。

假设我们有 1 个月 30 天的数据,共 23 个 data segment 数据片段,如:[2015-11-01,2015-11-02), [2015-11-02,2015-11-04), [2015-11-04,2015-11-11), [2015-11-11,2015-11-12), [2015-11-12,2015-11-13), 。。。[2015-11-30,2015-12-01)

问题 1: 假设因为数据有问题,需要回溯 2015-11-01 的数据,因为我们能够在 cube 中找到 [2015-11-01,2015-11-02) 这样一个 data segment,满足这个时间区间,于是,我们可以直接界面操作或者 Rest API 启动这个 data segment 的 refresh 更新操作。

问题 2: 假设我们需要回溯 2015-11-02 到 2015-11-03 的数据,同理,可以找到一个符合条件的 data segment [2015-11-02,2015-11-04),然后 refresh 更新这个 data segment。

问题 3: 假设我们需要回溯 2015-11-01 到 2015-11-02 的数据,我们找不到直接满足时间区间的 data segment。于是我们有 2 种解决方案,第 1 种方案是分别依次 refresh 更新 [2015-11-01,2015-11-02), [2015-11-02,2015-11-04) 这 2 个 data segment 实现;第 2 种方案是先合并 (merge)[2015-11-01,2015-11-02), (2015-11-02,2015-11-04) 这两个 data segment,合并后得到 [2015-11-01,2015-11-04) 这样 1 个 data segment,然后我们再拉取新数据后执行更新操作,即可满足需求。

问题 4: 假设我们需要刷新 2015-11-01~2015-11-30 这 1 个月的数据,我们在另 1 套集群上基于 Kylin 1.1.1 对同一个 cube 进行测试,如果采用问题 3 中的第 1 种方案,我们需要逐步刷新 cube 的 23 个 data segment,大约耗时 17.93min X 30=537 分钟; 如果我们采用问题 3 中的第 2 种方案, 那么我们只需要将 23 个 data segment 合并成 [2015-11-01,2015-12-01) 这 1 个 data segment,计 1 次操作。然后再执行 1 次更新操作,共 2 次操作即可完成需求,总体上,耗时约 83.78 分钟,较第 1 种方法性能上提高很多。

基于上面的问题,目前我们平台对 Apache Kylin 进行了二次开发,扩展出了任务管理模块。

对于 cube 的计算 (build) 操作,假设数据仓库 2015-11-29~2015-12-02 的数据因故延迟,在 2015 年 12-03 天产出了 (T-1 天的数据),如果不判断处理,就会例行计算生成一个时间区间为 [2015-11-29,2015-12-03) 的 data segment。所以,在每个 cube 计算前,我们的逻辑会自动检测跨自然月问题,并生成 [2015-11-29,2015-12-01) 和 [2015-12-01,2015-12-03) 两个 data segment.

对于 cube 的更新 (refresh) 操作,我们会采用问题 3、问题 4 中提到的第 2 种方案,自动合并 (merge)data segment 后再执行更新 refresh 操作。因为上面已经保证了不会有跨月 data segment 的生成,这里的自动合并也不会遇到生成跨自然月的情况。

对于 cube 的合并 (merge) 操作,如果每天都自动合并该自然月内前面日期已有的所有 data segment,假设我们想回溯更新 2015-11-11 这一天的数据,那么就需要回溯 (2015-11-01,2015-11-12)(因为这个时间区间的 data segment 每天都被自动合并了),其实,我们没有必要回溯 2015-11-01~2015-11-10 这 10 天的数据。所以,对于 1 个自然月内的 cube 的数据,在当月,我们先保留了 1 天 1 个 data segment 的碎片状态,因为在当月发现前面某几天数据有问题的概率大,回溯某个 data segment 小碎片就更加合理及性能更优。对于上个月整个月的数据,在下个月的中上旬时数据已经比较稳定,回溯的概率较小,通常要回溯也是上个月整月的数据。因此,在中上旬整体合并上 1 个月的数据而不是每天合并更合理。

(点击放大图像)

任务管理模块

4.3 平台监控模块二次开发

4.3.1 任务监控

通常,1 个产品对应多个页面,1 页面对应 1 个事实表,1 个事实表对应多个 cube,那么一个产品通常会包含多个 cube,上面提到的 cube 基于 data segment 的 3 种任务状态,很难人为去核查,所以对于任务执行的监控是非常必要的,当任务提交后,每隔一段时间检测一次任务的状态,任务状态中间失败或者最后成功后,则会发送邮件或者短信报警通知用户。

4.3.2 集群监控

由于我们的服务器是团队内部独自部署维护,为了高效监控整套 Hadoop 集群、Hive,HBase、Kylin 的进程状态,以及处理海量临时文件的问题,我们单独开发了监控逻辑模块。一旦集群出现问题,能够第一时间收到报警短信或者邮件。

(点击放大图像)

平台监控模块

4.4 资源隔离二次开发

由于我们以平台方式提供给各个业务线使用,当某个业务线的业务数据计算规模较大,会造成平台现有资源紧张时,我们会根据实际情况,要求业务方提供机器资源,随之而来的就是如何根据业务方提供的机器资源分配对应的计算队列的资源隔离问题。目前,官方的 Apache Kylin 版本对于整个集群只能使用 1 个 kylin_job_conf.xml, 平台上所有项目的所有 Cube 的 3 种操作只能使用同一个队列。于是,我们基于 kylin-1.1.1-incubating 这个 tag 的源码做了相关修改,支持了以项目为粒度的资源隔离功能,并提交 issue 到 https://issues.apache.org/jira/browse/KYLIN-1241 ,方案对于我们平台管理员自身也参与项目开发的应用场景下非常适用。对于某个项目,如果不需要指定特定计算队列,无需在 $KYLIN_HOME 下指定该项目的 kylin_job_conf.xml 文件,系统会自动调用官方原有的逻辑,使用默认的 Hadoop 队列计算。

(点击放大图像)

资源隔离

4.5 Hadoop 及 HBase 优化

因独立部署的 Hadoop 集群硬件配置不高,内存十分有限,所以,在项目实践过程中也遇到不少问题。

4.5.1 Hadoop 任务内存资源不够,cube 计算失败

调整 MapReduce 分配资源参数:在 cube 计算过程中,会出现 mr 任务失败,根据日志排查,主要因 mr 的内存分配不足导致,于是,我们根据任务实际情况整体调整了 yarn.nodemanager.resource.memory-mb,mapreduce.map.memory.mb, mapreduce.map.java.opts, mapreduce.reduce.memory.mb 及 mapreduce.reduce.java.opts 等参数。

4.5.2 HBase RegionServer 在不同节点随机 down 掉

由于机器整体资源限制,我们给 HBase 配置的 HBASE_HEAPSIZE 值较小,随着时间推移,平台承载的项目越来越多,对内存及计算资源要求也逐步提高。后来平台在运行过程中,HBase 的 RegionServer 在不同节点上出现随机 down 掉的现象,导致 HBase 不可用,影响了 Kylin 的查询服务,这个问题困扰了团队较长时间,通过网上资料及自身的一些经验,我们对 HBase 和 Hadoop 相关参数做了较多优化。

A. HBase 的 JVM GC 相关参数调优,开启了 HBase 的 mslab 参数:可以通过 GC 调优获得更好的 GC 性能,减少单次 GC 的时间和 FULL GC 频率;

B. HBase 的 ZK 连接超时相关参数调优:默认的 ZK 超时设置太短,一旦发生 FULL GC,极其容易导致 ZK 连接超时;

C. ZK Server 调优,提高 maxSessionTimeout:ZK 客户端(比如 Hbase 的客户端)的 ZK 超时参数必须在服务端超时参数的范围内,否则 ZK 客户端设置的超时参数起不到效果;

D. HBASE_OPTS 参数调优:开启 CMS 垃圾回收期,增大了 PermSize 和 MaxPermSize 的值;
Hadoop 及 HBase 优化

(点击放大图像)

Hadoop 及 HBase 优化

5. Apache Kylin 项目实践

5.1 基于仓库端 join 好的 fact 事实表建 Cube,减少对小规模集群带来的 hive join 压力

对于 Cube 的设计,官方有专门的相关文档说明,里面有较多的指导经验,比如: cube 的维度最好不要超过 15 个, 对于 cardinality 较大的维度放在前面,维度的值不要过大,维度 Hierarchy 的设置等等。

实践中,我们会将某个产品需求分为多个页面进行开发,每个页面查询主要基于事实表建的 cube,每个页面对应多张维度表和 1 张事实表,维度表放在 MySQL 端,由数据仓库端统一管理,事实表计算后存放在 HDFS 中,事实表中不存储维度的名称,仅存储维度的 id,主要基于 3 方面考虑,第一:减少事实表体积;第二:由于我们的 Hadoop 集群是自己单独部署的小集群,MapReduce 计算能力有限,join 操作希望在仓库端完成,避免给 Kylin 集群带来的 Hive join 等计算压力;第三:减少回溯代价。 假设我们把维度名称也存在 Cube 中,如果维度名称变化必然导致整个 cube 的回溯,代价很大。这里可能有人会问,事实表中只有维度 id 没有维度 name,假设我们需要 join 得到查询结果中含有维度 name 的记录,怎么办呢?对于某个产品的 1 个页面,我们查询时传到后台的是维度 id,维度 id 对应的维度 name 来自 MySQL 中的维度表,可以将维度 name 查询出来并和维度 id 保存为 1 个维度 map 待后续使用。同时,一个页面的可视范围有限,查询结果虽然总量很多,但是每一页返回的满足条件的事实表记录结果有限,那么,我们可以通过之前保存的维度 map 来映射每列 id 对应的名称,相当于在前端逻辑中完成了传统的 id 和 name 的 join 操作。

5.2 Aggregation cube 辅助中高维度指标计算,解决向上汇总计算数据膨胀问题

比如我们的事实表有个 detail 分区数据,detail 分区包含最细粒度 os 和 appversion 两个维度的数据 (注意: cuid 维度的计算在仓库端处理),我们的 cube 设计也选择 os 和 appversion,hierarchy 层次结构上,os 是 appversion 的父亲节点,从 os+appversion(group by os, appversion) 组合维度来看,统计的用户量没有问题,但是按照 os(group by os) 单维度统计用户量时,会从基于这个 detail 分区建立的 cube 向上汇总计算,设上午用户使用的是 android 8.0 版本,下午大量用户升级到 android 8.1 版本,android 8.0 组合维度 + android 8.1 组合维度向上计算汇总得到 os=android(group by os, where os=android) 单维度用户,数据会膨胀且数据不准确。因此我们为事实表增加一个 agg 分区,agg 分区包含已经从 cuid 粒度 group by 去重后计算好的 os 单维度结果。这样,当用户请求 os 维度汇总的情况下,Apache Kylin 会根据 router 算法,计算出符合条件的候选 cube 集合,并按照权重进行优选级排序 (熟悉 MicroStrategy 等 BI 产品的同学应该知道这类案例),选择器会选中基于 agg 分区建立的 os 单维度 agg cube,而不从 detail 这个分区建立的 cube 来自底向上从最细粒度往高汇总,从而保证了数据的正确性。

5.3 新增留存类分析,如何更高效更新历史记录?

对应小规模集群,计算资源是非常宝贵的,假设我们对于某个项目的留存分析到了日对 1 日到日对 30 日,日对 1 周到日对 4 周,日对 1 月到日对 4 月,周对 1 周到周对 4 周,月对 1 月到月对 4 月。那么对于传统的存储方案,我们将遇到问题。

5.3.1 传统方案

假如今天是 2015-12-02,我们计算实际得到的是 2015-12-01 的数据

(点击放大图像)

上面数据存储方案的思路是,当今天是2015-12-02,那么2015-12-01 可以计算活跃用户了,于是,我们会将2015-11-30 的日对第1 日留存, 2015-11-29 的日对第2 日, 2015-11-28 的日对第3 日等的这些列指标数据进行更新(如上红色对角线部分),这是因为每天数据的每1 列都是以当天为基准,等今后第n 天到了,再回填这1 天的这些第x 日留存,如此,对于1 个任务会级联更新之前的多天历史数据,如上红色对角线的数据。

此方案的优势:

a, 如果要查看某个时间范围内的某一个或者多个指标,可以直接根据时间区间,选择需要的列指标即可。

b, 如果要查看某 1 天的多个指标,也可以直接选择那 1 天的多个指标即可

此方案的缺点:

a, 每天都需要更新历史数据,如上红色对角线的数据,造成大量 MapReduce 任务预计算 cube,需要较多的机器计算资源支持。

b, 如果今后增加新的留存,比如半年留存,年留存,那么对角线长度就更长,每天就需要回溯更新更多天数的历史数据,需要更多时间跑任务。

c, 对于级联更新的大量的历史数据任务,其实依赖性很强,如何保证留存项目多个 cube 每一天的多个 data segment 级联更新正确,非常复杂,难以维护和监控,对于数据仓库端也易遇到如此问题。

d, 对于需要批量回溯一个较大时间区间的历史数据时,问题 3 中涉及的任务计算难点和困难尤为突出。

5.3.2 变通方案

假如今天是 2015-12-02,我们计算实际得到的是 2015-12-01 的数据(可和上面的结构对比)

(点击放大图像)

此方案的思路是,当今天是2015-12-02,实际是2015-12-01 的数据,如上示例存储,但日对第n 日的留存表示的是n 日前对应的那个日期的留存量,相当于旋转了红色对角线。

此方案的优势:

a, 如果要查看某个时间范围内的某 1 个指标,直接选择该范围的该列指标即可

b, 如果今后增加新的留存,比如半年留存,年留存等指标,不需要级联更新历史天数的数据,只需要更新 2015-12-01 这 1 天的数据,时间复杂度 O(1) 不变,对物理机器资源要求不高。

此方案的缺点:

a, 如果涉及到某 1 天或者某个时间范围的多列指标查询,需要前端开发留存分析特殊处理逻辑,根据相应的时间窗口滑动,从不同的行,选择不同的列,然后渲染到前端页面。

目前,我们在项目中采用变通的存储方案。

6. 总结

目前,我们大数据 OLAP 多维分析平台承载百度地图内部多个基于 Apache Kylin 引擎的亿级多维分析查询项目,共计约 80 个 cube,平均半年时间的历史数据,共计约 50 亿行的源数据规模,单表最大数据量为 20 亿 + 条源数据,满足大时间区间、复杂条件过滤、多维汇总聚合的单条 SQL 查询毫秒级响应,较为高效地解决了亿级大数据交互查询的性能需求,非常感谢由 eBay 贡献的 Apache Kylin,从预计算和索引的思路为大数据 OLAP 开源领域提供了一种朴素实用的解决方案,也非常感谢 Apache Kylin 社区提供的支持和帮助。

作者简介

王冬,百度地图数据智能组成员,北京理工大学计算机本硕毕业,2012 加入 Microstrategy,负责 BI Server 核心组件 SQL Engine 相关开发。并于 2014 年加入百度地图数据智能组,主要负责大数据 OLAP 多维分析计算方向研究,热爱大数据离线、实时平台建设应用、Spark 生态应用等。


感谢郭蕾对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入 InfoQ 读者交流群(已满),InfoQ 读者交流群(#2))。

2016-01-05 16:1919033

评论

发布
暂无评论
发现更多内容

安卓内存监控悬浮窗,2021Android面试心得,全套教学资料

欢喜学安卓

android 程序员 面试 移动开发

2021互联网大厂高频面试专题500道:并发编程/Spring/MyBatis(附答案解析)

比伯

Java 编程 架构 程序人生 计算机

MySQL 索引概要

小方

MySQL 索引

python 函数详解

若尘

函数编程 函数

Go Functions

escray

学习 极客时间 Go 语言 4月日更

因为这几个TypeScript代码的坏习惯,同事被罚了500块

华为云开发者联盟

typescript 运算符 代码 null strict

external-provisioner源码分析(3)-组件启动参数分析

良凯尔

Kubernetes 源码分析 Ceph CSI

边缘计算是流行词还是风口?开发者怎样选开源项目?

华为云开发者联盟

开源 开发者 5G 边缘计算 EdgeGallery 社区

Spring Bean创建过程的Hook

邱学喆

BeanPostProcessor @Autowired注入原理 @Resource注入原理 @Value注入原理

还有人搞不懂数据仓库与数据库的区别?

大数据技术指南

数据仓库 4月日更

架構實戰營 - 模塊 2 作業

Frank Yang

架构实战营

这才是大数据的正确打开方式

华为云开发者联盟

大数据 数据仓库 云原生 数据治理 灾备

架构实战营 模块二作业

fazinter

架构实战营

基于crudapi增删改查接口后端Java SDK二次开发之环境搭建(一)

crudapi

Java API sdk crud crudapi

external-provisioner源码分析(2)-main方法与Leader选举分析

良凯尔

Kubernetes 源码分析 Ceph CSI

kubernetes ceph-csi分析-目录导航

良凯尔

Kubernetes 源码分析 Ceph CSI Kubernetes Plugin

技术实践丨列存表并发更新时的锁等待问题原理

华为云开发者联盟

事务 update 元组 列存表

Play with Go

Rayjun

教程 Go 语言

k8s通过ceph-csi接入存储的概要分析

良凯尔

Kubernetes 源码分析 Ceph CSI

第十一周总结

MySQL存储过程的异常处理

Sakura

4月日更

安卓rxjava合并多个请求,我的阿里手淘面试经历分享,面试必会

欢喜学安卓

android 程序员 面试 移动开发

Golang Map 和字符串

escray

学习 极客时间 Go 语言 4月日更

数据脱敏:数仓安全隐私保护见真招儿

华为云开发者联盟

数据仓库 加密 隐私保护 GaussDB(DWS) 数据脱敏

external-provisioner源码分析(1)-主体处理逻辑分析

良凯尔

Kubernetes 源码分析 Ceph CSI

第 0 期架构训练营模块 2 作业

架构实战营

架构师实战营 模块二作业(微信朋友圈高性能复杂度架构分析)

代廉洁

架构实战营

模块二作业

求索

架构实战营

LitmusChaos: K8s上的混沌工程框架

混沌工程实践

k8s 混沌工程 litmuschaos 实践框架 故障实验库

架构实战营模块二作业

日照时间长

架构实战营

阿里P8重磅总结:看完别说不会了哦,SpringBoot「完结篇」

比伯

Java 编程 程序人生 计算机 架构】

Apache Kylin在百度地图的实践_最佳实践_王冬_InfoQ精选文章