Uber 如何构建实时基础设施来处理每天数拍字节的数据?
2024/9/25 21:03:05
本文主要是介绍Uber 如何构建实时基础设施来处理每天数拍字节的数据?,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
作者创建的图像。
这篇文章最初发布于 https://vutr.substack.com。
-
上下文
-
需求
-
逻辑构建块
-
深入探讨Uber的开源解决方案:Apache Kafka, Apache Flink, Apache Pinot, HDFS, Presto
-
用例
- Uber的经验教训.
Uber 是一家科技公司,它在2010年代初通过推出一款应用程序,使得司机和乘客之间能够轻松连接,从而彻底改变了出租车市场。截至2023年,有1.37亿人在每月使用Uber或Uber Eats一次。此外,在2023年,Uber司机完成了94.4亿次行程。为了支持业务,Uber积极利用数据分析和机器学习模型进行运营。从动态定价的Uber乘车服务到UberEats餐厅管理仪表板,所有这些都需要实时数据高效运行。在这篇博客文章中,让我们一起看看Uber如何管理其支持众多实时应用程序的幕后基础设施。
注意:这篇博客是我阅读论文《Uber的实时数据基础设施》(Real-time Data Infrastructure at Uber)后的笔记。
优步基础设施中的高级数据流。由作者创建,参考文献为 参考。
优步的业务具有高度的实时性。数据不断从多个来源收集:司机、乘客、餐厅、食客或后端服务。优步处理这些数据以提取有价值的信息,为诸如客户激励、欺诈检测和机器学习模型预测等许多用例做出实时决策。实时数据处理在优步的业务中起着至关重要的作用。该公司依赖开源解决方案并进行内部改进来构建实时基础设施。
从高层次来看,Uber中的实时数据处理主要包括三个大领域:
- 消息平台 允许生产者和订阅者之间的通信。
- 流处理 允许对消息流应用处理逻辑。
- 在线分析处理(OLAP) 允许对所有数据进行近乎实时的分析查询。
每个区域都有三个基本的扩展挑战:
- 数据扩展:总的数据流入量呈指数级增长。此外,Uber 的基础设施分布在多个地理区域以实现高可用性,这意味着系统必须在保持数据新鲜度、端到端延迟和可用性 SLA 的同时处理数据量的增加。
- 用例扩展:随着 Uber 业务的增长,新的用例不断涌现,不同组织部门之间有不同的需求。
- 用户扩展:与实时数据系统交互的用户具有不同的技术水平,从没有工程背景的业务用户到需要开发复杂实时数据管道的高级用户。
Uber 的实时基础设施需要以下几点:
- 一致性:关键应用需要在所有区域间保持数据一致性。
- 可用性:基础设施必须高度可用,并保证99.99百分位的可用性。
- 时效性:大多数用例需要秒级时效性。这确保了能够响应特定事件,例如安全事件。
- 延迟:某些用例需要在原始数据上执行查询,并要求p99查询延迟低于1秒。
- 可扩展性:系统可以随着不断增长的数据量进行扩展。
- 成本:Uber需要低数据处理和提供成本以确保高运营效率。
- 灵活性:Uber必须提供一种编程和声明式的接口来表达计算逻辑,以服务于不同的用户类别。
在本节中,我们将看一下Uber基础设施的主要逻辑构建模块:
作者创建的图像。
- 存储:这一层为其他层提供具有写后读一致性保证的对象或块存储。它用于长期存储,并应优化为高写入速率。Uber也使用这一层将数据回填或初始化到流或OLAP表中。
- 流:它作为发布-订阅接口,应优化为低延迟的读写操作。它需要对数据进行分区并保证至少一次语义。
- 计算:这一层为流和存储层提供计算功能。这一层也需要在源和目标之间保证至少一次语义。
- OLAP:这一层提供对来自流或存储的数据的有限SQL能力。它应优化以服务分析查询。在从不同来源摄入数据时,它需要至少一次语义。某些用例需要基于主键的数据恰好一次摄入。
- SQL 是计算和OLAP层之上的查询层。它将SQL语句编译为计算函数,这些函数可以应用于流或存储。当与OLAP层一起使用时,它将增强OLAP层的SQL限制能力。
- API:高级应用程序访问流或计算功能的编程方式。
- 元数据:管理所有层的所有元数据的简单接口。这一层需要元数据版本控制和版本间的向后兼容性。
下面的部分将介绍优步采用的开源系统,对应于相应的构建模块。
流存储
作者创建的图像。
Apache Kafka 是一个流行的开源事件流处理系统,在业界被广泛采用。它最初由 LinkedIn 开发,并于 2011 年初开源。除了性能之外,Kafka 被采用的其他因素还包括简单性、生态系统成熟度和开源社区。
在 Uber,他们拥有最大的 Apache Kafka 部署之一:每天处理数万亿条消息和数 PB 的数据。Kafka 在 Uber 中应用于多种工作流程:从乘客和司机应用程序传播事件数据,支持流处理分析平台,或将数据库变更日志传递给下游订阅者。由于 Uber 的独特规模特性,他们对 Kafka 进行了以下增强:
逻辑集群
作者创建的图片。
Uber 开发了一个联邦 Kafka 集群设置,该设置隐藏了集群细节,使生产者和消费者无需了解这些细节。
- 它们向用户暴露“逻辑 Kafka 集群”。用户不需要知道某个主题位于哪个集群中。
- 一个专用服务器集中了所有集群和主题的元数据,以便将客户端请求路由到物理集群。
- 此外,集群联邦有助于提高可扩展性。当一个集群被完全利用时,Kafka 服务可以通过添加更多集群来水平扩展。新主题可以在新集群上无缝创建。
- 集群联邦还简化了主题管理。由于许多应用程序和客户端,将一个正在运行的主题在 Kafka 集群之间迁移需要大量的工作。在大多数情况下,该过程需要手动配置以将流量路由到新集群,这会导致消费者重启。集群联邦有助于将流量重定向到另一个物理集群,而无需重启应用程序。
失败消息的队列
作者创建的图像。
在某些情况下,下游系统无法处理消息(例如,消息损坏)。最初,有以下两种处理这种情况的选项:
- Kafka 丢弃这些消息。
- 系统无限重试,这会阻塞后续消息的处理。
然而,Uber 有许多场景需要既不能丢失数据也不能阻塞处理。为了解决这类用例,Uber 在 Kafka 之上构建了 Dead Letter Queues (DLQ) 策略:如果消费者在重试后仍无法处理某条消息,它将把该消息发布到 DLQ。这样,未处理的消息将被单独处理,不会影响其他消息。
中间层
作者创建的图像。
在拥有数以万计运行 Kafka 的应用程序的情况下,Uber 面临着调试这些应用程序和升级客户端库的挑战。用户还在组织内部使用多种编程语言与 Kafka 进行交互,这使得提供多语言支持变得困难。
Uber 构建了一个消费者代理层来解决这些问题;代理从 Kafka 读取消息并将其路由到 gRPC 服务端点。它处理了消费者库的复杂性,应用程序只需采用一个薄的 gRPC 客户端。当下游服务无法接收或处理某些消息时,代理可以重试路由,并在多次重试失败后将消息发送到死信队列(DLQ)。代理还将 Kafka 中的消息分发机制从消息轮询更改为基于推送的消息分发。这提高了消费吞吐量,并允许更多的并发应用程序处理机会。
高效地在集群之间复制主题
由于业务规模庞大,Uber 在不同的数据中心使用多个 Kafka 集群。采用这种部署方式,Uber 需要 Kafka 的跨集群数据复制,原因有二:
- 用户需要一个全局的数据视图来满足各种使用场景。例如,他们必须整合并分析来自所有数据中心的数据以计算行程指标。
- Uber 通过复制 Kafka 部署来实现故障情况下的冗余。
Uber 开发并开源了一个名为 uReplicator 的可靠解决方案,用于 Kafka 的复制目的。该复制器具有一个重新平衡算法,在重新平衡期间将受影响的主题分区数量保持在尽可能低的水平。此外,在流量激增的情况下,它可以将负载重新分配给备用工作进程。我稍微研究了一下 uReplicator 的高级架构,以下是我的发现:
作者创建的图片,参考:链接。
- Uber 使用 Apache Helix 进行 uReplicator 集群管理。
- Helix 控制器负责将主题分区分配给工作节点,处理主题/分区的添加/删除,检测节点故障,并重新分配特定的主题分区。
- 在收到主题/分区复制请求后,Helix 控制器会将主题/分区与活跃工作节点之间的映射更新到 Zookeeper 服务,该服务充当中央状态管理服务。
- 当映射发生变化时,工作节点中的 Helix 代理会收到通知。
- 工作节点中的 DynamicKafkaConsumer 实例将执行复制任务。
Uber 还开发并开源了一个名为 Chaperone 的服务,以确保跨集群复制过程中不会丢失数据。它收集关键统计信息,例如每个复制阶段的独特消息数量。然后,Chaperone 对比这些统计信息,并在出现不匹配时生成警报。
流处理
作者创建的图像。
Uber 使用 Apache Flink 构建了处理来自 Kafka 的所有实时数据的流处理平台。Flink 提供了一个具有高吞吐量和低延迟的分布式流处理框架。Uber 选择 Apache Flink 的原因包括:
- 它的健壮性支持多种工作负载,并具有原生的状态管理和检查点功能,以实现故障恢复。
- 它易于扩展,并能高效地处理反压。
- Flink 拥有一个庞大且活跃的开源社区。
Uber 对 Apache Flink 做了以下贡献和改进:
使用SQL构建流式分析应用程序。
作者创建的图像。
Uber 开发了一个名为 Flink SQL 的层,它可以在 Flink 上运行。它可以将 Apache Calcite 的 SQL 输入 转换为 Flink 作业。处理器将查询编译成一个分布式 Flink 应用程序,并管理其整个生命周期,让用户专注于处理逻辑。在后台,系统将 SQL 输入转换为逻辑计划,然后经过优化器形成物理计划。最后,该计划通过 Flink API 转换为 Flink 作业。
然而,隐藏复杂性给用户带来了对基础设施团队管理生产任务的操作负担。Uber 需要应对这些挑战:
- 资源估算和自动扩缩容:Uber 使用分析来找出常见作业类型与资源需求之间的关联。他们还持续监控工作负载,以实现更好的集群利用率并按需进行自动扩缩容。
- 作业监控和自动故障恢复:由于用户不知道后台发生了什么,平台必须自动处理 Flink 作业失败的问题。Uber 为此构建了一个基于规则的引擎。该组件比较作业的指标,然后采取相应的行动,例如重启作业。
注意:Flink SQL 是一个具有无界输入和输出的流处理引擎。其语义与批处理 SQL 系统(如 Presto)不同,将在后面进行讨论。
作者创建的图片。
Uber 的 Flink 统一平台导致了分层架构的出现,从而提高了可扩展性和可扩展性。
- 平台层 组织业务逻辑并与诸如机器学习或工作流管理等其他平台进行集成。该层将业务逻辑转换为标准的Flink作业定义并传递给下一层。
- 作业管理层 处理Flink作业的生命周期:验证、部署、监控和故障恢复。它存储作业信息:状态检查点和元数据。该层还充当代理,根据作业信息将作业路由到物理集群。该层还包含一个持续监控作业健康状况并自动恢复失败作业的共享组件。它为平台层提供了一组API抽象。
- 底层 由计算集群和存储后端组成。它提供了一种抽象,无论资源是本地还是云基础设施,都提供了物理资源的抽象。例如,存储后端可以使用HDFS,Amazon S3,或Google Cloud Storage (GCS)用于Flink作业的检查点。
感谢这些改进,Flink 已经成为 Uber 的中央处理平台,负责数千个作业。现在,让我们继续介绍用于 OLAP 构建块的下一个开源系统:Apache Pinot。
OLAP系统
作者创建的图像。
Apache Pinot 是一个开源的分布式 OLAP 系统,用于执行低延迟的分析查询。它是在 LinkedIn 创建的,“在工程团队确定没有现成的解决方案能满足社交网络站点的需求后。” Pinot 采用 Lambda 架构,在在线(实时)和离线(历史)数据之间提供统一视图。
自从 Uber 引入 Pinot 以来的两年里,其数据量从几 GB 增长到了几百 TB。随着时间的推移,查询负载也从每秒几百次查询(Queries Per Second)增加到了每秒数万次查询。
Pinot 支持多种索引技术来回答低延迟的 OLAP 查询,例如 倒排索引,范围索引,或 星树索引。Pinot 采用 分而治之 的方式来分布式查询大型表。它按时间边界划分数据,并将其分组成段,同时查询计划并行执行它们。以下是优步决定使用 Pinot 作为其 OLAP 解决方案的原因:
- 在2018年,可用的选项是 Elasticsearch 和 Apache Druid ,但后续评估显示,Pinot 的内存和磁盘占用更小,并且支持显著更低的查询延迟 SLA。
- 对于 Elasticsearch :在相同的数据量下,Elasticsearch 的内存使用量是 Pinot 的 4 倍,磁盘使用量是 Pinot 的 8 倍。此外,Elasticsearch 的查询延迟是 Pinot 的 2-4 倍,通过结合过滤、聚合和分组/排序查询进行基准测试。
- 对于 Apache Druid :Pinot 的架构与 Apache Druid 类似,但集成了优化的数据结构,如位压缩前向索引,以降低数据占用。它还使用了专门的索引以加快查询执行速度,例如星树索引、排序和范围索引,这可能会导致查询延迟的量级差异。
在 Uber,用户利用 Pinot 处理许多实时分析用例。这些用例的主要需求是数据新鲜度和查询延迟。工程师们为 Apache Pinot 贡献了以下功能以满足 Uber 的独特需求:
upsert 操作结合了插入和更新操作。它允许用户更新现有记录,并在记录不存在时插入一条新记录。upsert 在许多用例中是一个常见需求,例如纠正乘车费用或更新配送状态。
作者创建的图像。
Upsert 操作的主要挑战是找到所需的记录位置。为了解决这个问题,Uber 将输入流按主键拆分成多个分区,并将每个分区分发到一个节点进行处理。这意味着同一个节点将处理具有相同键的所有记录。Uber 还开发了一种路由策略,将同一分区的子查询路由到同一个节点。
Pinot 最初缺少一些重要的 SQL 功能,如子查询和连接。Uber 将 Pinot 与 Presto 集成,以支持在 Pinot 上执行标准的 PrestoSQL 查询。
Uber 在将 Pinot 与其他数据生态系统集成方面投入了大量精力,以确保良好的用户体验。
例如,Pinot 与 Uber 的模式服务集成,从输入的 Kafka 主题推断模式并估计数据的基数。Pinot 还与 Flink SQL 集成作为数据目标,因此客户可以构建 SQL 转换查询并将输出消息推送到 Pinot。
归档存储
作者创建的图像。
Uber 使用 HDFS 存储长期数据。来自 Kafka 的大多数 Avro 格式数据以原始日志的形式存储在 HDFS 中。压缩过程将日志合并为 Parquet 格式,然后可以通过处理引擎如 Hive、Presto 或 Spark 访问。这个数据集作为所有分析目的的唯一来源。Uber 还使用此存储进行 Kafka 和 Pinot 的数据回填。此外,其他平台也使用 HDFS 以满足其特定需求。例如:
- Apache Flink 使用 HDFS 进行作业检查点的存储。
- Apache Pinot 使用 HDFS 进行长期段归档。
交互查询层
作者创建的图像。
Uber 采用了 Presto 作为其交互式查询引擎解决方案。Presto 是一个开源的分布式查询引擎,由 Facebook 开发。它被设计用于对大规模数据集进行快速分析查询,通过采用 大规模并行处理(MPP) 引擎,并将所有计算在内存中执行,从而避免将中间结果写入磁盘。
Presto 提供了一个高性能的 I/O 接口的 Connector API,允许连接到多个数据源:Hadoop 数据仓库、RDBMS 或 NoSQL 系统。Uber 为 Presto 构建了一个 Pinot 连接器,以满足实时探索的需求。这样,用户可以在 Apache Pinot 之上执行标准的 PrestoSQL。
Pinot 连接器需要决定哪些物理计划的部分可以推送到 Pinot 层。由于 API 的限制,该连接器的第一个版本仅包括了 谓词下推。Uber 改进了 Presto 的查询计划器,并扩展了 Connector API,以便尽可能多地将操作推送到 Pinot 层。这有助于降低查询延迟并利用 Pinot 的索引功能。
在了解了Uber如何使用开源系统构建实时基础设施之后,我们将讨论Uber生产环境中的一些用例,以及他们如何使用这些系统来实现他们的目标。
优步的动态定价用例是一种动态定价机制,它平衡了可用司机的供应与乘车需求。该用例的整体设计为:
- 流数据从Kafka摄入。
- 该管道在Flink中运行一个基于复杂机器学习的算法,并将结果存储在键值存储中,以便快速查找。
- 价格调整应用程序优先考虑数据的新鲜度和可用性,而不是数据的一致性,以满足延迟SLA要求,因为迟到的消息不会对计算产生贡献。
- 这种权衡导致Kafka集群的配置为更高的吞吐量,而不是无损保证。
Uber Eats餐厅经理仪表板允许餐厅老板运行slice-and-dice查询,以查看Uber Eats订单中的洞察,例如客户满意度、热门菜单项和服务质量分析。该用例的整体设计:
- 该用例要求使用最新数据并降低查询延迟,但并不需要太多的灵活性,因为查询模式是固定的。
- Uber 使用 Pinot 和 start-tree 索引来减少服务时间。
- 他们利用 Flink 执行过滤、聚合和汇总等任务,帮助 Pinot 减少处理时间。
- Uber 还观察到 Flink 转换时间和 Pinot 查询时间之间的权衡。转换过程会生成优化后的索引(在 Pinot 中),并减少服务的数据量。作为回报,它降低了服务层的查询灵活性,因为系统已经将数据转换为“固定形状”。
机器学习在 Uber 中扮演着至关重要的角色,为了确保模型的质量,监测模型预测输出的准确性至关重要。整个用例的设计如下:
- 由于数据量大且特征种类多:成千上万的部署模型,每个模型都有数百个特征,因此该解决方案需要具备可扩展性。
- 它利用了 Flink 的水平扩展能力。Uber 部署了一个大型流处理作业来聚合指标并检测预测异常。
- Flink 作业创建预聚合的 Pinot 表以提高查询性能。
UberEats团队需要对来自骑手、餐厅和食客的实时数据执行临时分析查询。这些洞察将用于基于规则的自动化框架中。该框架尤其在疫情期间帮助运营团队根据规定和安全规则运营业务。整个用例的设计如下:
- 底层系统必须高度可靠和可扩展,因为这个决策过程对业务至关重要。
- 用户使用Presto在Pinot管理的实时数据之上检索相关指标,然后将这些指标输入自动化框架。
- 框架使用Pinot聚合给定位置在过去几分钟内的所需统计数据,然后根据情况为快递员和餐厅生成警报和通知。
- Pinot、Presto和Flink在数据增长时快速扩展,并在高峰期表现可靠。
在文章结束前,我将介绍Uber的全活跃策略,如何管理数据回填,以及从Uber中学到的经验。
本节将展示Uber是如何提供业务弹性和连续性的。
Uber 依赖于多区域策略,确保服务在地理位置分布的数据中心中有备份运行,这样如果一个区域中的某个服务不可用,其他区域的服务仍然可以正常运行。这一策略的基础是一个多区域的 Kafka 设置,提供数据冗余和流量延续。
作者创建的图像。
这里是动态定价应用的主备架构示例:
- 所有的行程事件都会发送到Kafka区域集群,然后路由到聚合集群以获取全局视图。
- Flink作业会计算每个区域的不同区域的定价。
- 每个区域都有一个更新服务实例,并且一个全活协调服务会将其中一个标记为主实例。
- 主区域的更新服务将定价结果存储在一个全活数据库中,以便快速查询。
- 当主区域出现故障时,全活服务会将另一个区域指定为主区域,并将计算任务切换到另一个区域。
- Flink作业的计算状态太大,无法在区域之间同步复制,因此必须独立计算。
→ 这种方法计算密集度高,因为 Uber 需要在每个区域管理冗余的管道。
Uber 需要回溯时间重新处理数据流,原因有几方面:
- 新的数据管道通常需要与现有数据进行测试。
- 机器学习模型必须使用几个月的数据进行训练。
- 流处理管道中的更改或错误需要重新处理旧数据。
Uber 使用 Flink 构建了一个流处理回填解决方案,该解决方案有两种运行模式:
- SQL-based: 此模式允许用户在实时(Kafka)和离线数据集(Hive)上执行相同的SQL查询。
- API-based: Kappa+ 架构允许流处理逻辑直接重用于批处理数据。
优步(Uber)构建了大部分实时分析栈,依赖于开源组件。依赖这些组件为优步提供了坚实的基础。然而,这仍然会遇到一些挑战:
- 在他们的经验中,大多数开源技术都是为特定目的而构建的。
- Uber 需要做很多工作来使开源解决方案能够适用于广泛的使用场景和编程语言。
对于像 Uber 这样的大公司来说,架构演进中常常会看到多个驱动因素,比如新的业务需求或工业趋势。因此,Uber 意识到需要支持快速软件开发,以便每个系统能够迅速演进。
- 接口标准化对于清晰的服务边界至关重要。Uber 使用 Monorepo 来管理所有项目,将它们放在一个代码仓库中。
- Uber 始终倾向于使用 瘦客户端 来减少客户端升级的频率。在引入瘦 Kafka 客户端之前,升级一个 Kafka 客户端需要几个月的时间。
- 他们采用语言整合策略来减少与系统通信的方式。Uber 只支持 Java 和 Golang 作为编程语言,以及 PrestoSQL 作为声明式 SQL 语言。
- 平台团队将所有基础设施组件与 Uber 的专有 CI/CD 框架集成,以在预发布环境中持续测试和部署开源软件更新或功能开发。这也有助于减少生产环境中的问题和错误。
- 操作 : Uber 投资于声明式框架来管理系统部署。用户定义了集群上下线、资源重新分配或流量均衡等操作的高层次意图后,框架将处理这些指令而无需工程师干预。
- 监控 : Uber 使用 Kafka、Flink 或 Pinot 构建了每个特定用例的实时自动化仪表板和警报。
Uber 在以下几个方面努力解决用户扩展的挑战:
- 数据发现:Uber的集中式元数据仓库,作为Kafka、Pinot和Hive等系统中模式的唯一来源,使得用户查找所需的数集变得非常方便。系统还记录了这些组件间数据流的数据血缘。
- 数据审计:应用程序的事件从端到端进行审计。Kafka客户端为每个事件添加额外的元数据,例如唯一标识符、应用程序时间戳、服务名称和层级。系统使用这些元数据来跟踪数据生态系统每个阶段的数据丢失和重复,帮助用户高效地检测数据问题。
- 无缝集成:系统自动为生产环境中部署的服务提供应用程序日志的Kafka主题。用户还可以使用拖放界面创建Flink和Pinot管道,这隐藏了基础设施配置的复杂性。
Uber 的论文包含了关于实时基础设施、系统设计以及公司如何改进和调整如 Kafka、Pinot 或 Presto 这样的开源解决方案以满足其独特的扩展需求的宝贵经验。
我计划将我的写作主题扩展到系统设计和数据架构等领域,特别是大型科技公司如何管理和开发他们的大数据技术栈,所以敬请期待我未来的文章 ;)
现在是说再见的时候了,下周见。
参考 :
[1] 尤鹏傅和钦梅索曼,优步的实时数据基础设施(2021)。
[2] Mansoor Iqbal, Uber 收入和使用统计数据 (2024).
[3] Arpit Bhayani, 理解读写一致性及其重要性。
[4] Alex Xu, 最多一次、至少一次、恰好一次 (2022).
[5] 徐洪亮,uReplicator:Uber 工程的可扩展且健壮的 Kafka 复制器(2018)。
[6] CelerData,计算架构的优缺点 — 分散/聚合、MapReduce 和 MPP(大规模并行处理) (2023)
[7] Aditi Prakash, 揭秘谓词下推:优化数据库查询指南 (2023).
[8] Dremio,切片和切块分析。
我的通讯是一种每周的博客风格的电子邮件,在其中我会记录从比我聪明的人那里学到的东西。
所以,如果你想和我一起学习和成长,可以在这里订阅:https://vutr.substack.com .
这篇关于Uber 如何构建实时基础设施来处理每天数拍字节的数据?的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-12-23Fluss 写入数据湖实战
- 2024-12-22揭秘 Fluss:下一代流存储,带你走在实时分析的前沿(一)
- 2024-12-20DevOps与平台工程的区别和联系
- 2024-12-20从信息孤岛到数字孪生:一本面向企业的数字化转型实用指南
- 2024-12-20手把手教你轻松部署网站
- 2024-12-20服务器购买课程:新手入门全攻略
- 2024-12-20动态路由表学习:新手必读指南
- 2024-12-20服务器购买学习:新手指南与实操教程
- 2024-12-20动态路由表教程:新手入门指南
- 2024-12-20服务器购买教程:新手必读指南