ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Apache Doris + Iceberg 湖仓一体实战:从迁移到调优全解析

Apache Doris + Iceberg 湖仓一体实战:从迁移到调优全解析 最近好几个团队都在问我同一个问题Apache Doris和Apache Iceberg到底怎么配合用问的人多了我意识到这不是个别团队的困惑而是很多人对“湖仓一体”这个方向有期待但又不知道从哪里下手。这篇文章我不打算讲太多虚的就围绕 Doris 作为查询分析引擎、Iceberg 作为数据湖表格式这条主线把我实际搭建、迁移、调优过程中的选型逻辑、操作步骤和踩坑记录整理出来。无论你是刚接触数据湖还是已经在生产环境里跑 Hive Spark 想找替代方案这篇应该都能给你一些可直接落地的参考。需要先说清楚这不是一篇产品对比稿不是要论证 Doris 和 Iceberg 谁取代谁。Doris 擅长的是高性能极速分析Iceberg 擅长的是在数据湖上提供统一、可靠、可演化的表格式管理。它们的组合本质上是把“存储”和“计算”解耦让一份数据能被 Doris、Spark、Flink、Trino 多个引擎共享同时 Doris 端又能拿到接近本地表的查询性能。这个架构走到今天已经相当成熟但中间的细节——Catalog 配置、分区演化、谓词下推、元数据刷新——才是真正决定你能不能把架构落地的关键。1. 为什么偏偏是 Doris Iceberg 这对组合1.1 传统 Hive 表格式的瓶颈在哪里先回到问题的原点。过去几年数据湖这个词被喊得震天响但大部分团队实际跑在生产线上的还是 Hive 分区表 Parquet/ORC 文件这套组合。这套组合在小数据量、低并发场景下没什么大问题可一旦表的分区数量涨到几千上万小文件开始爆炸痛点就全出来了。最直观的问题是元数据性能。Hive 的 Metastore 在分区非常多的时候一次 list partition 操作可能就要等上几十秒查询引擎拿分区列表的时间比真正扫描数据的时间还长。而且 Hive 表的 schema 演化和分区演化的能力非常弱想改一个字段类型通常得 rewrite 整张表或者用各种 hack 手段稍有不慎就是线上事故。另一个问题是并发写入的约束。同一个 Hive 分区如果同时被两个任务写入很容易产生数据互相覆盖的问题只能靠外部调度系统强加锁来规避。这种“锁”的粒度很粗运维成本高还经常漏掉边界情况。这些瓶颈不是某一个查询引擎能解决的它属于表格式Table Format层面的缺陷。Hive 表本质上只是“目录 文件”的约定它没有事务的概念没有快照隔离没有可靠的文件清单管理。这也是 Iceberg 这类表格式诞生的核心原因。1.2 Iceberg 解决了什么问题Iceberg 作为数据湖表格式核心思路是把表的元数据组织成三层结构Catalog 指向 Metadata FileMetadata File 指向 Manifest 列表Manifest 列表里的每个 Manifest 再指向具体的数据文件。每一层都是不可变的任何写入操作都会生成一个新的 Metadata 版本也就是一个新的快照Snapshot。这套机制带来几个非常重要的能力ACID 事务与快照隔离多个任务可以并发写同一张表通过乐观锁机制解决冲突读任务看到的是写入前的完整快照不会被半成品数据污染。时间旅行只要你保留了历史快照就可以随时查询某个时间点或某个快照 ID 对应的数据状态。Schema 演化加列、删列、改列顺序、升级列类型都不需要重写数据文件元数据层面直接完成。分区演化Iceberg 的表分区规则是可以改的旧数据继续按旧规则管理新数据自动套用新规则这解决了 Hive 表在分区方案设计错了以后只能重建表的尴尬。但 Iceberg 只是一个表格式规范它本身不提供查询能力。你还需要一个高性能的查询引擎来“消费”这些表。这就是 Doris 登场的位置。1.3 Doris 在这个组合里的定位Apache Doris 是 MPP 架构的分析型数据库它的优势集中在这几个方面向量化执行引擎、列式存储、智能物化视图、高并发点查能力以及非常低的运维门槛。Doris 的查询速度在 ClickBench 这类榜单上常年靠前但它本质上更适合作为“分析引擎”而非“通用存储底座”。如果只有 Doris 没有 Iceberg那架构就是经典的数据仓库模式所有数据都要灌进 Doris 内部存储存储成本高、数据共享能力弱Spark 和 Flink 想直接读 Doris 内部表非常不灵活。如果只有 Iceberg 没有 Doris那你还是得回到 Spark/Trino 那一套——不是不行但如果你需要的是更快的查询响应、更低的查询延迟、更高的并发Spark 和 Trino 都很难达到 Doris 的效果。两者组合的意义在于Iceberg 负责构建一个可共享、可演化、多引擎通用的数据底座Doris 负责在这份数据上提供极速分析服务。这也是当前讨论度很高的“湖仓一体”Lakehouse架构的典型落地形态。Doris 通过 Catalog 机制对接 Iceberg把 Iceberg 表映射成外部表查询时利用 Doris 的分布式计算能力直接扫描对象存储或 HDFS 上的数据文件。2. 环境准备与 Catalog 配置先把路打通2.1 版本选型与兼容性判断在动手之前版本匹配是最容易踩的坑。Doris 对 Iceberg 的集成是逐步完善的不同版本能力差异很大。我建议优先关注几个版本节点Doris 1.2.x开始支持 Iceberg Catalog但功能相对基础不支持某些复杂类型。Doris 2.0.x查询性能显著提升对 Iceberg v2 表格式的支持开始完善。Doris 2.1.x生产环境建议的最低版本Iceberg 相关功能到了可以放心用的程度。Doris 3.x新架构、新优化持续推进对 Iceberg 的谓词下推、元数据缓存有更多增强。Iceberg 本身的话0.14 之后 API 变化开始收敛1.x 是目前的主流版本2.x 也已经在社区里铺开。这里有一个非常容易踩的坑你的 Iceberg 运行时版本和 Doris 内置的 Iceberg 解析器版本可能存在差异。我自己遇到过一种情况Spark 用的是 Iceberg 1.3Doris 侧的解析逻辑版本偏旧导致某些新版本生成的元数据文件字段读不出来报错信息还特别不直观最后是通过统一 Iceberg 运行时版本到同一大版本解决的。生产环境我建议直接选Doris 2.1.x 及以上 Iceberg 1.4/1.5 这套组合并且尽量让所有写引擎Spark、Flink与 Doris 读取的 Iceberg 版本保持一致。这个“一致”不一定要精确到小版本但大版本必须对齐。2.2 部署 Doris 时的依赖准备Doris 本身是秒级部署的架构FE 负责元数据管理和查询规划BE 负责数据存储和计算执行。对接 Iceberg 不需要在 Doris 节点上部署额外的客户端服务因为 Doris 是通过 Thrift 协议直接连接 Hive Metastore作为 Iceberg Catalog 的载体来获取元数据。但你需要注意网络层面的连通性FE 节点必须能访问 Hive Metastore 的 Thrift 端口默认 9083。BE 节点必须能访问 HDFS NameNode/DataNode 或对象存储S3、OSS、GCS的相应端点。如果开启了 Kerberos 认证FE 和 BE 都需要配置对应的 keytab 文件和 krb5.conf。我见过不少团队在本地验证时一切正常一上生产就卡在“FE 能访问 HMS但 BE 访问不了 HDFS”这种网络隔离问题上。部署前的网络连通性检查一定要做全别默认“能通”。2.3 Catalog 创建的完整配置Doris 对接 Iceberg 的操作核心是创建一个 Catalog。以 Hive Metastore 作为 Iceberg Catalog 载体是最常见的用法SQL 如下CREATE CATALOG iceberg_hms PROPERTIES ( type iceberg, iceberg.catalog.type hms, hive.metastore.uris thrift://hive-metastore:9083 );执行成功后你可以用SHOW DATABASES FROM iceberg_hms;查看这个 Catalog 下已经存在的 Iceberg 库再用USE iceberg_hms.xxx;切换库。如果你的 Iceberg 表放在文件系统或对象存储上并且不是通过 Hive Metastore 管理的那就要用REST Catalog或Glue Catalog。举个例子用 REST Catalog 的配置CREATE CATALOG iceberg_rest PROPERTIES ( type iceberg, iceberg.catalog.type rest, uri http://iceberg-rest-service:8181, warehouse s3://my-bucket/warehouse, s3.endpoint https://s3.xxx.amazonaws.com, s3.access_key your-ak, s3.secret_key your-sk );这种 REST Catalog 的方式在云原生环境里越来越流行它的好处是整个 Iceberg 的元数据服务统一由一个服务端管理不需要单独维护 HMS权限控制也更集中。2.4 验证 Catalog 与表是否打通Catalog 创建完成后先跑一个简单的查询确认链路正常SELECT COUNT(*) FROM iceberg_hms.db_name.table_name LIMIT 1;这个 SQL 能跑通说明从 Doris 到 HMS 到存储文件的链路是通的。但如果数据量大这条 SQL 本身可能会比较慢因为它要扫描全部数据文件。更快的验证方式是先看元数据SHOW TABLES FROM iceberg_hms.db_name; DESC iceberg_hms.db_name.table_name;每次执行查询前Doris 会缓存 Iceberg 的元数据。当你通过 Spark 或 Flink 往 Iceberg 表里写了新数据后Doris 侧默认不会实时感知到新快照需要手动刷新。刷新有两层操作-- 刷新整个 Catalog 的元数据缓存 REFRESH CATALOG iceberg_hms; -- 只刷新某张表的元数据缓存 REFRESH TABLE iceberg_hms.db_name.table_name;这个刷新的频率要根据业务实时性要求来定。如果你的链路是分钟级调度可以在每批次调度前先执行刷新如果是实时写入场景可以设置更短周期的定时刷新任务。Doris 后续版本也在不断优化自动元数据感知的能力但生产环境里至少要在代码层预留刷新机制。3. 从 Hive 分区表到 Iceberg 的完整迁移路径3.1 迁移前的表结构与数据摸底别急着写 Spark 脚本先花时间把每一张表的现状摸清楚。我列一个经验性的检查清单表的总行数、总文件数、平均文件大小。文件平均大小如果低于 64MB说明小文件问题已经很严重迁移到 Iceberg 之后应该先做一次文件合并。分区字段的类型与基数。Hive 表常见pt_date2025-01-01这种字符串分区Iceberg 虽然也支持但更推荐用 date/timestamp 类型做分区列这样在 Doris 端的谓词下推效果更好。是否有复杂嵌套类型。比如 mapstring, array 这类Doris 对 Iceberg 复杂类型的支持是逐渐增强的最好提前确认版本兼容。数据更新模式。如果源 Hive 表是每天全量覆盖迁移到 Iceberg 后就应该设计成 insert-overwrite如果是增量追加那就走 append-only 模式如果有 update/delete 需求必须使用 Iceberg v2 表格式。这些信息可以通过一段 Hive SQL 快速盘点也可以用 Spark 读 meta 信息。盘点清楚后你再决定每张表的迁移策略避免一张表一种情况临时救火。3.2 离线全量迁移Spark 写 Iceberg 表离线全量迁移最稳的方案是写一个 Spark 作业把 Hive 表的数据读出来写入 Iceberg 表。Spark 3.x Iceberg 1.4 的 Spark SQL 扩展是当前最成熟的组合。核心逻辑如下val df spark.read.table(old_hive_db.orders) df.writeTo(lake_db.orders) .using(iceberg) .partitionedBy( org.apache.iceberg.spark.Spark3Util.toTransforms( days(order_time) ) ) .createOrReplace()这里有一个非常值得留意的点Hive 表原来的物理分区和 Iceberg 表的分区规范不是一回事。Hive 表的分区是物理目录结构Iceberg 的分区是隐藏分区Hidden Partitioning它在元数据里维护分区值到数据文件的映射不依赖物理目录。所以迁移时你可以根据 Iceberg 表最终查询模式重新设计分区粒度而不必沿用 Hive 的老分区规则。比如 Hive 表原来的分区是pt_dateyyyyMMdd到 Iceberg 里可以用days(order_time)做时间粒度分区或者干脆用量级更合适的分区策略。写入时 Iceberg 会自动按新分区规则路由数据文件你不用关心物理目录结构。这种灵活性是 Hive 表完全没有的。3.3 增量同步Flink CDC 到 Iceberg全量迁移只能解决存量。如果业务表有持续的新增和更新增量链路就得设计好。目前业界最常用的链路是业务数据库MySQL/PostgreSQL- Flink CDC - Kafka - Flink - IcebergFlink 侧写入 Iceberg 的代码大致如下DataStreamRowData stream ...; // 来自 Kafka 或 CDC 的数据流 TableLoader tableLoader TableLoader.fromHadoopTable(hdfs://namenode:8020/warehouse/lake_db/orders); IcebergStreamWriterRowData writer FlinkSink.forRowData(stream) .tableLoader(tableLoader) .equalityFieldColumns(order_id) .upsert(true) .build();有几个经验教训如果表需要做 row-level update不要用默认的 append-only 模式要开启 upsert 并配置 equality field否则更新的数据会变成新增行产生重复。Flink 写入 Iceberg 时Flink 的 checkpoint interval 会直接影响 Iceberg 快照生成的频率。checkpoint 越频繁Iceberg 可视化出来的数据时效性越好但产生的小文件也越多。建议结合下游 Doris 查询的实时性要求来平衡。如果你的 UPSERT 场景非常多注意 Iceberg v2 的 MORMerge-on-Read机制导致读取时可能需要合并删除文件Doris 对这类文件的读取效率取决于版本支持情况。生产环境里要测试一下。3.4 首次刷新与数据一致性校验增量链路或全量迁移跑完后千万别急着在 Doris 里跑业务报表。先到 Doris 侧执行REFRESH TABLE iceberg_hms.lake_db.orders;然后做两层校验。第一层是行数级别SELECT COUNT(*) FROM iceberg_hms.lake_db.orders; SELECT COUNT(*) FROM internal_db.orders_old; -- 或者直接对比源 Hive 表第二层是对账字段级别挑几个关键维度字段和指标字段分别做 sum 和 group by 对比例如SELECT order_status, COUNT(*), SUM(order_amount) FROM iceberg_hms.lake_db.orders GROUP BY order_status;和源表结果对比到一致后再放量切流。不要省这一步数据湖迁移最容易出问题的地方就是写入链路的不同导致细微的对账偏差例如时区转换问题、Decimal 精度截断问题、字符串字段 trim 问题这些都需要通过字段级对账才能发现。3.5 小文件治理一个必须提前规划的环节Iceberg 还有一个 Hive 比不了的特性通过 Rewrite 相关 Action 来小文件合并。但 Doris 不负责这个事它只负责读。所以生产环境里一定要规划一个定期执行的小文件合并任务否则时间一长Iceberg 的 manifest 文件数量会急剧膨胀Doris 读取元数据的成本也会上升。常见的做法是写一个 Spark 作业调用 Iceberg 的 RewriteDataFiles Actionimport org.apache.iceberg.spark.source.SparkTable import org.apache.iceberg.actions.RewriteDataFiles val table spark.table(lake_db.orders).asInstanceOf[SparkTable].table() val result SparkActions.get() .rewriteDataFiles(table) .option(target-file-size-bytes, 536870912) // 512MB .execute()我一般建议的触发策略是每天统计一次表的文件数量和平均文件大小当平均文件大小低于 128MB 时自动触发合并。不要设置得太激进因为文件合并本身会消耗计算资源而且会生成新的快照。4. 查询性能调优与常见问题排查4.1 谓词下推是否真正生效Doris 查询 Iceberg 表时能不能把过滤条件下推到 Iceberg 的文件扫描层直接决定了查询快慢。理论上Doris 会把WHERE条件里的等值、范围、IN 等条件下推到数据源扫描阶段Iceberg 再通过 manifest 文件和分区元数据做文件裁剪File Pruning。如果裁剪做得好一个几千分区的表可能最终只扫描几个文件。但实际生产里经常会遇到下推失效的情况。我遇到最多的一个原因是分区字段的类型不匹配。比如 Iceberg 表的分区字段是order_datedate 类型你在 Doris 里查询时写了where order_date 2025-01-01这里的字面量是字符串类型如果 Doris 不能把这个字符串自动转换并下推为 date 类型就可能导致裁剪失效整个表的分区全扫一遍。解决方法是查询时显式转换类型SELECT * FROM iceberg_hms.lake_db.orders WHERE order_date DATE 2025-01-01;怎么判断下推是否生效看查询计划。在 Doris 里执行EXPLAIN SELECT * FROM iceberg_hms.lake_db.orders WHERE order_date DATE 2025-01-01;explain 的结果里会展示 Doris 生成的 ScanNode 信息。如果谓词下推正常你会看到类似iceberg scan with predicates的提示如果没看到就说明过滤条件被拿到上层执行了底层还是全量扫描。这个排查步骤非常关键。我建议每次新建的离线或实时表接入 Doris 时都跑一次EXPLAIN确认谓词下推做到位了而不是等业务方反馈“表很慢”再回头查。4.2 元数据刷新慢怎么办Doris 读取 Iceberg 表之前需要获取表的元数据包括所有 manifest 文件和文件统计信息。如果表的数据文件非常多、manifest 文件也很多这个元数据拉取过程会占到查询总耗时的很大比例。以下是几个从实践中总结的处理方法调整 Doris 侧的元数据缓存参数。Doris 2.1 开始支持针对 Iceberg 表做 catalog 级别的缓存配置你可以设置ALTER CATALOG iceberg_hms SET PROPERTIES ( iceberg.table-list-cache-enabled true, iceberg.table-list-cache-expire-time-seconds 300 );控制 manifest 文件数量。定期做 RewriteManifest 操作把大量小 manifest 合并成大 manifest。Iceberg 的元数据是树形结构manifest 文件越多每次查询要读取的元数据层级就越深。注意第一查询的冷启动问题。集群刚重启或 Catalog 刚创建后第一次查询往往特别慢因为元数据还没缓存。建议对核心大表做一个预热任务每天凌晨定时执行一次SELECT COUNT(*)或更轻量的元数据访问。4.3 查询慢但数据量不大优先看文件扫描范围慢查询有一个通用排查思路先确认这个查询到底扫描了多少数据量、多少个文件。Doris 在查询完成后会记录每个 ScanNode 的统计信息。在 MySQL 协议客户端里执行SET is_report_success true; SELECT * FROM iceberg_hms.lake_db.orders WHERE order_date DATE 2025-01-01;然后去 Doris 的 WebUI 或fe.log里查看 Query Profile。重点看两个指标FileScanRange的数量如果过滤条件很强但扫描的文件数仍然接近总数那说明文件裁剪没有生效。RowsRead和ReadBytes这两个数值能被压到很小查询速度自然就快起来了。这类问题通常不是 Doris 的 bug而是表设计或查询写法层面的问题。根据我遇到的真实情况文件裁剪失效的常见原因有三种分区字段类型不匹配、时间函数嵌套导致无法识别为分区裁剪条件、过滤条件里的字段不是 Iceberg 表的分区字段Iceberg 也支持通过 manifest 里存的 min/max 统计信息做文件裁剪但这依赖于文件层面的统计信息是否足够准确定期做 Spark 侧的rewrite_data_files能提升统计信息准确性。4.4 常见报错与处理方案报错现象根因处理方案Failed to connect to Hive MetastoreFE 到 HMS 的网络不通或 HMS 服务异常telnet检查 9083 端口查看 HMS 日志确认 thrift uri 没有配错Gone to com.google.common.base.Preconditions checkStateDoris 内置的 Iceberg 解析器和表元数据版本不兼容统一升级 Doris 到 2.1确保写入时使用的 Iceberg 运行时版本与 Doris 解析版本大版本一致查询结果和源表对不上分区字段类型不一致导致文件裁剪过多或.gz压缩格式某些格式的数据文件解析异常逐步排查 scan range打印 profile 看扫描了哪些文件检查写入时是否空值分区被丢弃创建 Catalog 成功但SHOW DATABASES为空HMS 中库没有被标记为 Iceberg 库或库不存在确认 HMS 中已存在该库确认 Iceberg 表注册在正确的 namespace 下查询报File not found数据文件被移动/删除但元数据没有同步更新检查对象存储生命周期规则是否误删了文件检查 Iceberg 表的快照过期策略是否过于激进这些报错背后没有太多玄学大部分就是版本、网络、类型这老三样。遇到问题先看日志Doris 的日志在 FE 节点的fe.log和fe.warn.log报错信息里通常都有足够的上下文。5. 一套可落地的湖仓一体架构与业务效果5.1 典型部署形态一份数据多引擎读写把上面这些环节串起来最后形成一套可以照搬的架构。我们用文字描述这套部署形态底层存储HDFS 或 S3/OSS 对象存储统一存放 Iceberg 数据文件。元数据服务Hive Metastore 或独立的 Iceberg REST Catalog Service管理所有 Iceberg 表的元数据。写入侧Flink 负责实时链路把业务库 CDC、Kafka 消息流写入 IcebergSpark 负责离线链路做全量迁移、批量回刷、小文件合并。查询侧Apache Doris 通过 Catalog 对接 Iceberg对外提供 JDBC/MySQL 协议接口支撑报表、大屏、即席查询、高并发点查等多种场景。这套架构的关键收益有两点。第一数据只存一份不会因为 Doris、Spark、Flink 各自需要独立的数据拷贝而产生存储浪费和数据不一致问题。第二计算完全解耦Doris 只负责它擅长的查询分析不参与数据湖的数据写入和管理职责边界清晰。5.2 冷热分层的一种实践方式这套架构还能灵活演变。一个非常实用的改进是在 Doris 内部存热数据把冷数据定期归档到 Iceberg业务数据先实时写入 Doris 内部表用于最近 7 天或 30 天的秒级查询。超过保留周期的历史分区通过 Doris 的导出能力定期写入 Iceberg 表然后在 Doris 内部删除对应分区。查询端通过视图对外统一暴露查最近数据走 Doris 内部表查历史数据走 Iceberg 外部表业务方无感知。这种方式的好处是既保留 Doris 对热数据的高性能查询又不用无限扩容 Doris 的存储来容纳全量历史数据存储成本大幅降低。我在实际项目里看到存储成本能降一半以上同时热数据查询性能完全不受影响。5.3 适合你的场景吗不是所有团队都需要这套组合我的判断标准很简单如果你们的数据量只有 TB 级查询场景也不复杂Doris 单库就能搞定完全没有必要引入 Iceberg 增加运维复杂度。如果你们有多个引擎需要共享同一份数据或者数据量已经到了几十 TB 甚至 PB 级且存储成本压力明显那么 Doris Iceberg 的组合就值得认真考虑。如果你们已经在跑 Hive Spark 架构正头疼元数据性能和事务问题那向 Iceberg 迁移是一个顺势而为的方向Doris 则可以作为查询端替代 Spark SQL 的补充选项。另外一个容易忽略的问题团队的能力储备。Iceberg 的写入链路由 Flink/Spark 负责这意味着运维团队要有一定的大数据组件维护能力。如果团队里的 Spark/Flink 经验薄弱建议先把写入链路练熟再谈大规模迁移。5.4 从实践中总结的几条建议我经历了几次从 Hive 迁移到 Iceberg、再对接 Doris 的项目沉淀下来几条个人经验放在最后供参考第一先做小表再动大表。选一张数据量在千万级别的表做全链路验证把 Catalog 配置、查询验证、对账逻辑全部跑通再逐步扩展到核心大表。第二统一版本统一权限。Iceberg 的写入端版本和 Doris 解析版本大版本必须对齐这是避免大量疑难杂症的前提。权限侧如果能统一接到 Ranger在 Doris 和 Iceberg 之间做统一鉴权会省去很多安全审计的麻烦。第三别忽视对象存储的限额问题。S3/OSS 都有请求数 QPS 上限当 Doris 并行扫描大量 Iceberg 数据文件时会触发对象存储的限流表现就是查询偶发超时。这时候可以考虑调整 Doris 的并行度参数或者对 Iceberg 表做文件合并减少单次查询触发的文件请求数量。第四监控一定要提前做。至少监控 Doris FE 的元数据缓存命中率、BE 节点扫描 Iceberg 文件的数据量、HMS 的请求耗时这几个指标这些指标能帮你提前发现数据湖查询性能劣化的趋势而不是等问题爆发了再去捞日志。回到开头那个问题Apache Doris 和 Apache Iceberg 到底怎么配合我现在可以给出更完整的答案用 Iceberg 把数据湖的表格式规范管起来解决多引擎共享、事务、演化、存储成本这些问题用 Doris 把查询性能提上去解决分析师和业务方对响应速度的诉求。这套组合不是万能的它有学习成本有运维复杂度也有版本兼容上的各种小心思。但对于数据规模上来了、又希望保留灵活性和查询性能的团队来说Doris Iceberg 确实是我目前看到的最务实的答案之一。希望这篇内容能帮你少走一些弯路有架构选型问题也欢迎在评论区继续聊。
返回列表