系统设计 System Design -4-5-系统设计问题-Dropbox设计(云存储系统设计、Google Drive)CRDT、S3对象存储

69 阅读22分钟

Dropbox设计(云存储系统设计)

1. 为什么需要云存储?

云文件存储服务通过简化跨设备数字资源的管理与共享,已成为现代计算的基础设施。从单一PC到多平台(智能手机、平板、桌面端)无缝切换的需求,推动了云存储的普及。

核心优势:

  • 可用性 (Availability): 数据随时随地可访问。
  • 可靠性与持久性 (Reliability and Durability): 通过多副本、异地容灾等机制,现代云服务(如 Amazon S3)可提供高达 99.999999999% (11个9) 的数据持久性保证,远超传统方案。
  • 可扩展性 (Scalability): 用户存储空间可按需动态扩展,无需关心底层硬件。

2. 系统需求与目标

核心需求:

  • 用户能通过任意设备上传、下载文件/照片。
  • 用户能与他人共享文件或文件夹。
  • 系统支持跨设备自动同步。
  • 系统应支持GB级别的单个大文件存储。
  • 保证文件操作的 ACID 特性(Atomicity, Consistency, Isolation, Durability)。
  • 支持离线编辑,用户上线后自动同步变更。

扩展需求 (Modern Requirements):

  • 版本控制 (Versioning): 用户可以回溯并恢复文件的任何历史版本。
  • 端到端加密 (End-to-End Encryption - E2EE): 作为一项可选或默认的安全特性,确保只有用户本人(而非服务提供商)能访问其文件内容,这已成为衡量隐私保护水平的关键指标。

3. 核心设计考量

  • 读写均衡: 预计读写比接近 1:1,系统需同时为两种操作优化。
  • 文件分块(Chunking): 文件在客户端被切分为固定大小的块(如 4MB),这是增量同步和数据去重的基础。
  • 增量同步: 系统只传输被修改的文件块,而非整个文件,极大节省带宽和时间。
  • 内容寻址与数据去重: 通过对文件块内容进行哈希计算(如 SHA-256),将哈希值作为该块的唯一标识符。这天然地实现了存储层面的数据去重。
  • 客户端元数据缓存: 客户端本地维护一份文件元数据的轻量级副本,减少网络请求,并实现离线操作。
  • 冲突的优雅解决: 离线或并发编辑必然导致冲突。系统必须有一套健壮的无冲突合并机制,例如 CRDTs (Conflict-Free Replicated Data Types)
  • 可观测性 (Observability): 在微服务架构下,必须具备全面的日志、指标(Metrics)和分布式追踪(Tracing)能力,以便于监控系统健康状况和排查问题。

4. 容量估算与约束

  • 总用户: 5亿
  • 日活用户 (DAU): 1亿
  • 平均文件数/用户: 200
  • 总文件数: 5亿 * 200 = 1000亿
  • 平均文件大小: 100KB
  • 总存储: 1000亿 * 100KB = 10 PB (Petabytes)
  • 每分钟活跃连接数: 100万

5. 高层架构设计

采用云原生微服务架构。客户端通过一个API网关与后端服务集群通信。后端服务各司其职,通过 gRPC 进行内部高效通信,并通过消息队列进行异步解耦。

  • 客户端 (Client): 运行于用户设备上,负责文件监控、分块、加解密、与后端通信。

  • API 网关 (API Gateway): 系统的统一入口,负责认证、鉴权、路由、限流。

  • 后端微服务 (Backend Microservices):

    • 认证服务 (Auth Service): 处理用户注册、登录,生成和验证 JWT (JSON Web Tokens)。
    • 元数据服务 (Metadata Service): 核心服务,管理文件/文件夹的结构、版本、权限等信息。
    • 文件网关服务 (File Gateway Service): 负责处理文件上传/下载的业务逻辑,生成访问对象存储的预签名URL (Pre-signed URLs)
    • 通知服务 (Notification Service): 通过 WebSocket 实现与客户端的长连接,实时推送变更通知。
  • 分布式数据库 (Distributed Database): 存储所有元数据,选用兼具SQL事务和水平扩展能力的数据库。

  • 对象存储 (Object Storage): 实际存储文件块数据的地方,如 Amazon S3 或 Google Cloud Storage。

  • 消息队列 (Message Queue): 用于服务间的异步通信,如文件处理完成后的通知。

  • 分布式缓存 (Cache): 缓存热点元数据,降低数据库压力。

6. 组件详细设计 (Component Design) - Go 实践

a. 客户端 (Client)

使用 Go 语言的跨平台能力,可以编写一套核心逻辑,编译到 Windows, macOS, Linux, Android, 和 iOS (通过 Gomobile)。

  • I. 文件监控器 (Watcher):

    • 技术栈: 使用 github.com/fsnotify/fsnotify 库。
    • 工作流: 在一个独立的 Goroutine 中运行,实时监控用户工作区目录的文件创建、修改、删除事件,并将事件发送到一个内部 channel。
  • II. 分块与哈希 (Chunker):

    • 技术栈: Go 标准库 crypto/sha256。
    • 工作流: 当检测到文件变更时,以流式方式读取文件,按 4MB 大小切块,并对每个块计算 SHA-256 哈希值。哈希值即为块的ID。
  • III. 索引与冲突处理 (Indexer & CRDT):

    • 技术栈: 本地使用 SQLite (通过 cgo) 或 BadgerDB (纯 Go KV 存储) 存储元数据。集成一个 CRDT 库,如 github.com/y-crdt/y-go 的思想来构建文件目录树。
    • 工作流: Indexer 处理来自 Watcher 的事件。当文件修改时,它会更新本地的 CRDT 数据结构。这种结构天然支持合并来自服务端的或其他客户端的并发修改,能以可预测的方式解决冲突,而无需复杂的锁定机制。
  • IV. 通信层 (Communicator):

    • 技术栈:

      • 通知服务 建立持久的 WebSocket 连接 (github.com/gorilla/websocket),用于接收实时更新。
      • 与其他服务通过 gRPC 进行通信 (google.golang.org/grpc),定义严格的 API 契约 (Protobuf),享受高性能和双向流能力。
    • 工作流: 启动时,建立 WebSocket 连接。当需要同步时,通过 gRPC 调用元数据服务,提交变更(CRDT state diff),并获取其他客户端的变更。

b. 元数据数据库 (Metadata Database)
  • 选型: TiDBCockroachDB

  • 理由: 它们是分布式SQL数据库,对外表现为 MySQL/PostgreSQL,因此可以使用成熟稳定的 Go 驱动(go-sql-driver/mysql 或 pgx)。它们在底层自动处理数据的分片和复制,完美解决了原文中提到的“元数据分区”难题,同时提供了强一致性的ACID事务保证。

  • 数据模型 (简化):

    • users: (user_id, name, public_key, ...)
    • files: (file_id, parent_id, name, type, owner_id, crdt_state, ...)
    • file_chunks: (file_id, chunk_hash, chunk_index)
    • chunks: (chunk_hash, ref_count, location, ...)
    • shares: (file_id, user_id, permission_level)
c. 同步服务 (Synchronization Service - 以微服务形式实现)

所有服务都用 Go 编写,编译成轻量级的静态二进制文件,易于容器化部署。

  • 元数据服务 (Metadata Service):

    • 核心职责: 提供对文件元数据的 CRUD gRPC 接口。所有数据库操作都在此服务内,确保事务的完整性。
    • 工作流: 接收客户端的 gRPC 请求(如“提交文件变更”),解析 CRDT 数据,启动一个数据库事务,更新 files 和 file_chunks 表,然后将变更事件发布到消息队列。
  • 通知服务 (Notification Service):

    • 核心职责: 维护与在线客户端的 WebSocket 连接池。
    • 工作流: 订阅消息队列中的“元数据更新”主题。当收到消息时,查询哪些用户订阅了该文件,并向这些用户的 WebSocket 连接推送一个轻量级的“有更新”通知。客户端收到通知后,再主动向元数据服务拉取具体变更内容。
d. 消息队列服务 (Message Queuing Service)
  • 选型: NATS 或 Kafka。

  • 理由: NATS 本身由 Go 编写,轻量、高性能,非常适合作为微服务间的事件总线。

  • 工作流:

    1. 元数据服务在完成数据库事务后,向 metadata.updated 主题发布一条消息,内容包含 file_id 和 user_id。
    2. 通知服务是该主题的消费者,接收消息并实时推送给客户端。
    3. 未来可以扩展其他消费者,如分析服务、索引服务等。
e. 云/对象存储 (Cloud/Object Storage)
  • 选型: Amazon S3Google Cloud Storage (GCS)

  • 理由: 它们是成熟、可靠、可无限扩展且成本优化的标准解决方案。

  • Go 实践: 使用官方 Go SDK (github.com/aws/aws-sdk-go-v2 或 cloud.google.com/go/storage)。

  • 关键模式 - 预签名URL (Pre-signed URL): 客户端从不直接将文件块上传到我们的服务器。

    1. 客户端通过 gRPC 调用 文件网关服务,请求上传/下载一个或多个块(提供哈希值)。

    2. 文件网关服务进行权限校验后,为每个块生成一个有时间限制的、专用的上传/下载 URL。

    3. 客户端使用这个 URL,通过标准的 HTTP PUT/GET 请求,直接与 S3/GCS 进行数据传输。

    • 优势: 极大地降低了我们后端服务的带宽压力和计算负载,实现了存储流量的无限水平扩展。

7. 文件处理工作流 (File Processing Workflow - Modern)

  1. 客户端A 修改文件,Watcher 检测到变更。
  2. Chunker 将文件切块并计算哈希值 H1, H2, H3。
  3. Indexer 更新本地元数据,并将变更(CRDT diff)暂存。
  4. Communicator 通过 gRPC 向 文件网关服务 请求上传 H1, H2, H3 的预签名 URL。
  5. 客户端A 使用返回的 URL,将三个块直接上传到 S3
  6. 上传成功后,客户端A 通过 gRPC 向 元数据服务 发起“提交变更”请求,包含文件ID和新的块列表 (H1, H2, H3)。
  7. 元数据服务 在一个数据库事务中更新文件版本信息,然后向 NATS 的 metadata.updated 主题发布通知。
  8. 通知服务 收到 NATS 消息,找到订阅该文件的在线客户端B和C,通过各自的 WebSocket 连接发送通知。
  9. 客户端B和C 收到通知,向 元数据服务 拉取最新的文件元数据。
  10. 客户端B和C发现它们缺少 H1, H2, H3 这三个块,于是向 文件网关服务 请求下载这三个块的预签名URL。
  11. 客户端B和C 使用返回的 URL,直接从 S3 下载 缺失的块,并在本地重构文件。
  • fsnotify 就是 Watcher: 完全正确。fsnotify 是 Watcher 角色的具体 Go 语言实现。

  • Watcher 和 Chunker 的关系: 你描述的非常准确!这正是 Go 并发设计的经典模式。

    • Watcher 是一个独立的 Goroutine,它像一个哨兵,在监控文件系统。
    • 当它发现一个事件(比如文件 "a.txt" 被修改),它不会自己去处理,而是把这个任务(包含文件路径 "a.txt" 的一个结构体)扔进一个 Go channel 里。
    • 另一边,有一个或多个 "工人" Goroutine(代表 Chunker 和后续流程),它们一直在等待从这个 channel 里取任务。一旦取到,就开始对 "a.txt" 进行分块、哈希等一系列操作。
  • Indexer 的本地元数据:

    • 它的作用是客户端的“大脑”和“记忆” 。它在本地(比如一个 SQLite 文件)维护了一个完整的、关于同步文件夹所有文件/目录的状态镜像。

    • 它记录了:

      1. 文件目录树结构。
      2. 每个文件的当前版本号。
      3. 每个文件版本对应的哈希值列表(这就是拼装文件的“菜谱”)。
    • 有了它,客户端才能在离线时知道哪些文件被修改了,才能在在线时高效地计算出需要同步哪些差异。

  • CRDT 和 Indexer 的关系:

    • CRDT 不是消息,它是一种数据结构。Indexer 使用 CRDT 这种数据结构来管理文件目录树这部分的本地元数据。
    • 当 Indexer 收到一个“消息”(比如 Watcher 说:“文件 a.txt 被创建”),Indexer 会执行一个 CRDT 操作:crdt_tree.add_file('a.txt')。
    • 离线同步: 你的理解完全正确!所有在离线期间对本地文件的操作,都会被 Indexer 记录为对本地 CRDT 的一系列变更。当网络恢复时,Communicator 会把这些累积的变更(CRDT diff)一次性推送给服务器。
  • 预签名 URL

    • 权限极小:一个用于上传 chunk-ABC 的 URL,绝对不能用来下载 chunk-DEF,也不能用来删除任何东西,更不能用来列出所有的文件
    • 时间极短
    • 无密钥泄露:访问整个 S3 权限的主密钥从未离开过我们的服务器

8. 数据去重

通过内容寻址存储 (Content-Addressable Storage) 自然实现。

  • 每个块的存储键 (Key) 就是其内容的 SHA-256 哈希值。
  • 上传前,客户端先向元数据服务查询一批哈希值是否存在。服务端只需在 chunks 表中进行一次批量查询。
  • 对于已存在的块,客户端无需上传,服务端只需在 chunks 表中将其引用计数(ref_count)加一。
  • 这是一种在线去重 (in-line deduplication) ,提供了最优的网络和存储效率。

9. 元数据分区

  • 解决方案: 采用 TiDBCockroachDB 后,这个问题由数据库底层自动解决。开发者无需关心手动分区的复杂性。数据库会根据数据大小和访问热点自动分裂和移动数据分片(Region/Range),对应用层完全透明。

10. 缓存

  • 选型: Redis

  • Go 实践: 使用 github.com/go-redis/redis/v8 库。

  • 缓存内容:

    • 热点元数据: 频繁访问的文件或目录的元数据。
    • 用户会话信息: 用户登录状态和权限。
    • 预签名URL: 短时间内可以缓存,避免重复生成。
  • 策略: 采用 Cache-Aside 模式。服务先查询 Redis,如果未命中,则查询数据库,然后将结果写回 Redis。

11. 负载均衡 (Load Balancer)

  • 实现: 在云原生环境中,这通常由 Kubernetes Ingress Controller (如 NGINX, Traefik) 或云服务商提供的负载均衡器 (如 AWS ALB/NLB) 处理。它们负责将外部流量分发到 API 网关或 gRPC 服务。

12. 安全、权限与文件共享

  • 认证 (Authentication):

    • 技术: 使用 JWT。用户登录后,认证服务 颁发一个包含 user_id 和过期时间的 JWT。后续所有请求都需在 Header 中携带此 Token。
    • Go 实践: github.com/golang-jwt/jwt/v4 库用于生成和解析 Token。API 网关会作为中间件统一校验。
  • 授权 (Authorization):

    • 在元数据服务中实现基于角色的访问控制(RBAC)。shares 表记录了文件与用户之间的共享关系和权限(如 viewer, editor)。
  • 端到端加密 (E2EE) 工作流:

    1. 每个用户在注册时生成一对公私钥。公钥上传到服务器。
    2. 当客户端A 创建一个新文件时,它会生成一个随机的对称密钥(文件密钥),并用此密钥加密文件的所有块(使用 AES-256-GCM,由 Go crypto/aes 和 crypto/cipher 提供)。
    3. 然后,客户端A 用自己的公钥加密这个文件密钥,并将加密后的文件密钥与元数据一起上传。
    4. 当客户端A 想与用户B 共享文件时,它会下载加密的文件密钥,用自己的私钥解密,然后再用用户B的公钥加密,并将这个新的加密副本发送给元数据服务。
    5. 这样,只有持有相应私钥的用户才能解密文件密钥,进而解密文件内容。服务端全程无法看到明文数据。

13. 知识点

CRDTs (无冲突合并机制)
  • CRDTs 是什么? CRDTs 是一种特殊的数据结构,无论以何种顺序、在多少个不同的地方对它进行修改,当这些修改最终汇集到一起时,它总能合并成一个确定的、一致的结果,并且这个合并过程永远不会产生冲突。常用于协同编辑应用(如 Google Docs)、分布式计数器和云同步

  • 基本原理: 想象一下你和朋友在两个离线的设备上编辑一个共享的购物清单(这是一个 Set 集合):

    • 初始清单:{牛奶, 面包}
    • 你离线时添加了鸡蛋,删除了面包。你的清单现在是 {牛奶, 鸡蛋}。
    • 你朋友离线时添加了果汁,删除了面包。他的清单现在是 {牛奶, 果汁}。

    当你们都上线后,系统如何合并?一个简单的系统会很困惑:面包被删了两次,这没问题。但你们都添加了东西。最终结果应该是 {牛奶, 鸡蛋, 果汁}。CRDTs 通过一种巧妙的设计保证了这一点。例如,对于一个 Set,它的添加和删除操作被设计成可交换的 (Commutative) ,即 操作A + 操作B 的结果和 操作B + 操作A 的结果完全一样。这样系统就不需要知道谁先谁后,只要把所有的操作都应用一遍,就能得到最终一致的状态。

  • 和分布式存储的关系? CRDTs 是关于应用层的数据模型的,而不是底层存储块。它确保的是你应用中的数据(比如文件目录树的结构)在多个客户端之间能无冲突地同步,最终达到一致。所以它是一种上层的分布式算法。

  • 和 Raft 的天壤之别:

    • Raft 是一种共识算法 (Consensus Algorithm)。 它的目标是让一组服务器就一个单一的、严格有序的操作日志达成绝对一致。它追求的是强一致性 (Strong Consistency) 。数据库 TiDB/CockroachDB 的底层就用了 Raft (或其变种) 来保证数据的正确性。
    • CRDTs 追求的是最终一致性 (Eventual Consistency)。 它允许每个客户端独立工作(离线),不要求实时达成共识。它通过数据结构本身的设计,来保证这些独立的操作最终能够“和平地”合并在一起。

    总结:Raft 是让服务器集群像一个“单体”一样工作,保证不出错。CRDTs 是让多个独立的客户端(或数据中心)像一群“合作者”一样工作,保证不冲突。

S3/对象存储的 Key 和文件修改流程
  • Key 是我们自己生成的: Key 就是文件块内容的 SHA-256 哈希值。这是在客户端对每个 4MB 的块计算出来的。S3 完全不知道也不关心这个 Key 是怎么来的,它只认这个 Key 是这个对象的唯一标识符。

  • Bucket 就是一个巨大的 KV 存储: S3 (以及所有对象存储) 的核心模型就是一个 Key-Value 存储。Key 是对象名(在我们的例子里是哈希值),Value 就是对象本身的数据(4MB 的文件块)。它没有传统文件系统的“文件夹”概念。

  • 如何拼装和修改 (关键点):

    1. 用户在本地修改文件(比如一个 100MB 的文档,只改了中间一句话)。

    2. 我们的客户端程序不会去下载任何东西。它直接在本地对这个修改后的、完整的 100MB 文件重新执行一遍“分块和哈希”流程。

    3. 现在,客户端得到了一个全新的哈希值列表,比如 [H1', H2', H3', ..., H25']。

    4. 客户端会查询它本地元数据中记录的、这个文件修改前的旧哈希值列表 [H1, H2, H3, ..., H25]。

    5. 关键一步:比较新旧两个列表。

      • 大部分块的内容没有变,所以 H1' == H1, H3' == H3 等等。对于这些哈希值相同的块,客户端什么都不用做
      • 只有被修改的那一句话所在的那个 4MB 块,它的内容变了,所以它的新哈希值 H2' 会和旧的 H2 不同
    6. 结论: 客户端只需要上传那个哈希值为 H2' 的新块

    7. 最后,客户端告诉元数据服务器:“请把文件 XYZ 的版本更新一下,它现在对应的块列表是 [H1, H2', H3, ..., H25。”

14. 云存储示例

1. “偷摸的”/被动同步模式 (The "Magic Folder" Model - 同步盘)

这就是我们一直在设计的 Dropbox,以及 iCloud Drive、Google Drive 桌面版、OneDrive 桌面版的核心模式。

  • 核心哲学: “云端只是你电脑上一个文件夹的实时镜像。”
  • 用户体验: 你几乎感觉不到它的存在,所以你说“偷摸的”非常形象。你不需要学习任何新操作。你就像平常一样,把文件保存到电脑上的那个“Dropbox”或“iCloud Drive”文件夹里,然后就完事了。你甚至可以忘了它的存在。当你拿出手机或换一台电脑时,会惊喜地发现:“哇,文件已经在这里了!”
  • 如何实现“偷摸”: 这正是我们设计的核心——Watcher (fsnotify) 在后台默默工作。它像一个 7x24 小时的不知疲倦的秘书,盯着你那个文件夹的一举一动。你一有风吹草动(保存、修改、删除),它立刻就开始了我们之前讨论的完整工作流:分块、哈希、上传、通知... 整个过程对用户是透明自动的。
  • iCloud Drive 就是这个模式: 当你在 Mac 的“系统设置”里打开“iCloud Drive”并勾选了“桌面与文稿文件夹”后,你的这两个文件夹就被置于 fsnotify 这样的监控之下了。你在桌面上保存一个文件,实际上就是在触发一次后台的自动同步。

所以,这种模式的关键词是:无感、自动、镜像、同步。


2. “主动的”上传下载模式 (The "Digital Warehouse" Model - 网盘)

这主要是指 百度网盘 的核心体验,以及早期的很多网盘产品。

  • 核心哲学: “云端是一个独立的、巨大的网络硬盘。”

  • 用户体验: 你需要明确地执行“上传”和“下载”操作。它更像一个仓库。你需要把东西“搬进去”(上传),需要用的时候再“取出来”(下载)。你对每一次数据传输都有明确的感知。

  • 为什么感觉不一样:

    1. 它不以本地文件夹同步为默认: 百度网盘的核心界面是一个类似文件管理器的网页或App。它的首要任务是存储和分享,特别是大文件分享。虽然它现在也提供了“同步空间”功能(这其实就是在学习 Dropbox 模式),但这并不是它的立身之本。
    2. 内容审核 (你提到了!): 这是一个本质区别。因为百度网盘的一个核心场景是用户之间的资源分享(电影、软件、学习资料等),这使得它变成了一个半公开的平台。为了符合法规,它必须对上传的内容进行扫描和审核,防止盗版和非法内容的传播。它通常通过文件哈希值(所谓的“秒传”就是这个原理)来快速识别已知的文件。
    3. 分享功能极其强大: 创建带密码和有效期的分享链接,是它最重要的功能之一。

所以,这种模式的关键词是:主动、存储、仓库、分享。


3. “内嵌的”/生态融合模式 (The "Integrated Ecosystem" Model)

这个模式是前两种的结合与升华,iCloud (非 Drive 部分)WPS 云文档 是最好的例子。

  • 核心哲学: “云存储不是一个功能,而是我们应用/生态的血液。”

  • 用户体验: 感觉比“偷摸的”模式更加无缝和神奇。你甚至都不知道有“文件”这个东西,你只知道你的“照片”、“联系人”或者“文档”在所有设备上都是最新的。

  • 如何实现的:

    • iCloud (Photos, Contacts): 当你在 iPhone 上拍一张照片时,触发同步的不是文件系统监控器,而是 “相机”和“照片”这两个 App 本身。iOS 系统和 App 深度集成,拍照动作完成后,App 内部的同步模块立刻被唤醒,开始上传。它同步的不是一个 .jpg 文件,而是包含照片、编辑历史、地理位置等信息的结构化数据。你感觉不到,是因为同步的发起者是 App,而不是你“保存文件到某个文件夹”这个动作。
    • WPS 云文档: 这也是一个完美的例子。当你在 WPS 里编辑一个文档并按下 Ctrl+S 保存时,WPS 程序不仅仅是把文件写入你电脑的硬盘。它内置的同步客户端会同时启动我们设计的那一套流程。它把文件系统监控 (fsnotify) 的功能直接集成到了软件内部。所以对用户来说,我只是保存了一个文档,但 WPS 默默地帮你完成了云同步。

所以,这种模式的关键词是:无缝、生态、应用驱动、数据同步。


总结与对比
模式类型核心哲学用户动作感觉代表产品
同步盘 (Sync)云端是本地文件夹的镜像被动/隐式 (在特定文件夹工作)“偷摸的” / 自动Dropbox, iCloud Drive, Google Drive (桌面版)
网盘 (Warehouse)云端是独立的网络硬盘主动/显式 (上传/下载)“主动的” / 手动百度网盘, Mega, 阿里云盘
生态 (Integrated)云是应用和生态的血液自动/应用驱动 (使用App)“神奇的” / 无感iCloud Photos, WPS 云文档, Google Pho

异同点:

  • 相同点: 它们的底层技术栈非常相似。无论哪种模式,到了最底层,都离不开我们设计的那些东西:文件分块、哈希去重、元数据管理、对象存储、消息通知等等。
  • 不同点: 它们把这些技术包装成了完全不同的产品形态,去服务于不同的核心需求:Dropbox 解决的是“跨设备文件协同”,百度网盘解决的是“大文件存储与分享”,iCloud/WPS 解决的是“生态内数据无缝流转”。