← 2026-06-11 📂 All Days 2026-06-13 →

Day 64 — System Design: Design a CDN from Scratch

🏗️ 系统设计 / System Design — Design a CDN from Scratch


真实场景 / Real-World Scenario

想象你在构建一个视频平台,用户遍布全球。一个视频文件 5GB 存储在你的纽约数据中心——东京的用户每次播放都要跨越 14,000 公里的海底光纤,延迟高达 200ms+,缓冲不断。

Imagine you're building a video platform with global users. A 5GB video stored in your NYC datacenter means Tokyo users cross 14,000km of undersea fiber cables — 200ms+ latency, constant buffering.

解决方案:内容分发网络 (CDN)。把内容推近用户,而不是把用户拉向内容。

Solution: Content Delivery Network (CDN). Push content close to users, not the other way around.


架构图 / Architecture Diagram

┌─────────────────────────────────┐ │ Origin Server │ │ (NYC — Source of Truth) │ └──────────────┬──────────────────┘ │ Cache Miss → Pull ┌──────────────▼──────────────────┐ │ CDN Control Plane │ │ (DNS routing, cache policies, │ │ purge API, analytics) │ └───┬─────────────┬───────────────┘ │ │ ┌─────────────▼──┐ ┌─────▼──────────────┐ │ Edge PoP: LAX │ │ Edge PoP: NRT(Tokyo)│ │ ┌──────────┐ │ │ ┌──────────────┐ │ │ │L1 Cache │ │ │ │ L1 Cache │ │ │ │(SSD,hot) │ │ │ │(SSD, hot) │ │ │ └──────────┘ │ │ └──────────────┘ │ │ ┌──────────┐ │ │ ┌──────────────┐ │ │ │L2 Cache │ │ │ │ L2 Cache │ │ │ │(HDD,warm)│ │ │ │(HDD, warm) │ │ │ └──────────┘ │ │ └──────────────┘ │ └────────────────┘ └────────────────────┘ ▲ ▲ US West users Asia-Pacific users ~5ms latency ~8ms latency

核心组件 / Core Components

1. DNS 路由 / DNS Routing

当用户访问 cdn.example.com/video.mp4:

  1. DNS 返回距离用户最近的 PoP (Point of Presence) IP
  2. 基于 Anycast 或 GeoDNS 实现就近路由
  3. TTL 设置短 (30-60s) 以支持快速故障转移

When a user requests cdn.example.com/video.mp4:

  1. DNS returns the IP of the nearest PoP
  2. Routing via Anycast or GeoDNS
  3. Short TTL (30-60s) enables fast failover

2. 缓存策略 / Cache Strategy

Cache-Control: public, max-age=86400, stale-while-revalidate=3600
  • 静态资源 (JS/CSS/images): 长 TTL + 内容哈希命名 (app.a3f2bc.js)
  • 视频内容: 按块缓存 (HLS segments, 2-10s each)
  • 动态内容 (API 响应): 短 TTL 或 bypass CDN
  • Cache Key: URL + 重要请求头 (Accept-Encoding, Accept-Language)

3. 缓存填充 / Cache Population

两种模式 / Two modes: Pull Model (Lazy): User Request → Cache Miss → Fetch from Origin → Cache → Serve Push Model (Eager): Upload → CDN API → Pre-populate all edges → Serve instantly
  • Pull: 适合长尾内容,按需缓存 / Good for long-tail content
  • Push: 适合热门发布 (新专辑、大型活动) / Good for hot launches

4. 缓存失效 / Cache Invalidation

# Purge by URL
curl -X DELETE https://api.cdn.com/cache?url=https://cdn.example.com/image.png

# Purge by tag (bulk)
curl -X DELETE https://api.cdn.com/cache?tag=product-123

挑战:全球 200+ PoP,purge 传播延迟 5-30s。

Challenge: 200+ PoPs globally, purge propagation takes 5-30s.


关键权衡 / Key Tradeoffs

决策选项A选项B如何选
缓存 TTL长 (高命中率)短 (内容新鲜)静态用长+内容哈希;动态用短
Pull vs Push按需填充预热所有边缘热门内容 Push;长尾 Pull
边缘计算在 PoP 运行逻辑回源运行逻辑认证/个性化放 Edge
HTTPS 终止在 Edge 终止透传到 Origin在 Edge 终止:降延迟

别踩这个坑 / Common Mistakes

❌ 忘记 Vary 头

# 没有 Vary: Accept-Encoding
# 压缩版本和非压缩版本共用同一缓存 key → 部分用户收到乱码
Vary: Accept-Encoding

❌ 缓存含用户数据的 API 响应

# 错误:用户特定数据被公开缓存 Cache-Control: public, max-age=300 ← 危险!包含用户 ID # 正确: Cache-Control: private, max-age=60 ← 只在用户浏览器缓存

❌ 单点 Origin 导致 CDN 全量回源时雪崩

解法:Origin 也要部署在多个区域,CDN 回源时就近选择。


📚 References

  • https://www.cloudflare.com/learning/cdn/what-is-a-cdn/
  • https://aws.amazon.com/cloudfront/features/
  • https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control

🧒 ELI5

CDN 就像麦当劳的供应链:总部 (Origin) 做好配方,全球门店 (Edge PoP) 就近备好食材。北京的顾客不用飞到美国总部点汉堡——最近的门店直接出餐。

CDN is like McDonald's supply chain: HQ (Origin) creates the recipe, local franchises (Edge PoPs) stock the ingredients nearby. Beijing customers don't fly to US HQ for a burger — the nearest store serves them instantly.

Day 64 — Algorithms: #199 Binary Tree Right Side View (Medium)

💻 算法 / Algorithms — #199 Binary Tree Right Side View (Medium)

🧩 树遍历模式 (9/15) — building on the DFS/BFS template from Day 56


今日问题与模式 / Today's Problem & Pattern

前面 8 道树题 (Invert → LCA) 主要用 DFS 递归。今天的「右视图」有个关键转折——我们需要 层级信息 (每层最后一个节点),这让 BFS (层序遍历) 成为更自然的选择。这正是对模式的变奏:同一棵树,换个遍历视角。

The previous 8 tree problems primarily used DFS recursion. Today's "right side view" has a key twist — we need level information (last node per level), making BFS (level-order traversal) the more natural fit. This is the pattern variation: same tree, different traversal perspective.

🔗 LeetCode #199 🟡 Medium

📹 NeetCode Solution


真实类比 / Real-World Analogy

想象你站在一棵树的 正右侧,从上往下看。每层你只能看到最右边的那个节点——左边的节点被遮住了。这就是「右视图」:每层最右边的节点的值。

Imagine standing to the right side of a tree, looking in. For each level, you can only see the rightmost node — the left ones are blocked. That's the right side view: the value of the rightmost node at each level.

1 ← 看到 1 (只有一个节点) / \ 2 3 ← 看到 3 (最右) \ \ 5 4 ← 看到 4 (最右) 输出: [1, 3, 4]

方法对比 / Approach Comparison

方法 1:BFS (推荐)

每层处理完取最后一个节点 → 自然对应「右视图」语义

方法 2:DFS (更优雅,但反直觉)

先遍历右子树,用 depth 追踪当前层,首次访问某 depth 时即为右视图节点。


BFS 解法 / BFS Solution

from collections import deque
from typing import Optional, List

class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

def rightSideView(root: Optional[TreeNode]) -> List[int]:
    if not root:
        return []
    
    result = []
    queue = deque([root])
    
    while queue:
        level_size = len(queue)  # snapshot current level size
        
        for i in range(level_size):
            node = queue.popleft()
            
            # Only record the LAST node in this level
            if i == level_size - 1:
                result.append(node.val)
            
            # Add children for next level
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)
    
    return result

执行追踪 / Execution Trace

Tree: 1 / \ 2 3 \ \ 5 4 Initial queue: [1] Level 0: size=1, process node 1 → i=0 == size-1 → append 1. Add children: 2, 3 Level 1: size=2, process node 2 (i=0, skip), process node 3 (i=1 == size-1 → append 3). Add 5, 4 Level 2: size=2, process node 5 (i=0, skip), process node 4 (i=1 == size-1 → append 4) Result: [1, 3, 4] ✓

DFS 解法 / DFS Solution (Bonus — More Elegant)

def rightSideView_dfs(root: Optional[TreeNode]) -> List[int]:
    result = []
    
    def dfs(node, depth):
        if not node:
            return
        
        # If visiting this depth for the FIRST time (right-first DFS)
        # then this is the rightmost node at this depth
        if depth == len(result):
            result.append(node.val)
        
        # Visit RIGHT first so right nodes are recorded first
        dfs(node.right, depth + 1)
        dfs(node.left, depth + 1)
    
    dfs(root, 0)
    return result

核心洞察:先遍历右子树 + 用 depth == len(result) 判断是否是该层首次访问 = 自动只记录最右节点。

Key Insight: Visit right first + depth == len(result) as "first visit" check = automatically only records rightmost nodes.


复杂度 / Complexity

解法TimeSpace
BFSO(n)O(w) w=最宽层节点数
DFSO(n)O(h) h=树高
  • 平衡树:BFS 空间 O(n/2),DFS 空间 O(log n)
  • 链状树:BFS 空间 O(1),DFS 空间 O(n)

举一反三 / Pattern Connections

这道题是 #102 Binary Tree Level Order Traversal (Day 63) 的直接变体:

  • #102 记录每层所有节点 → #199 只记录每层最后一个节点
  • 同样的 BFS 骨架,只改了 result.append 的条件

连接整个树遍历 Block:

  • #226 Invert → 修改树结构
  • #102 Level Order → 层级处理 (BFS)
  • #199 Right Side View → 层级处理变体 ← 今天
  • #104/#543 → 递归收集信息 (DFS)
  • #235 LCA → 路径问题 (DFS)

📚 References

  • https://leetcode.com/problems/binary-tree-right-side-view/
  • https://neetcode.io/problems/right-side-view-of-a-binary-tree
  • https://www.geeksforgeeks.org/print-right-view-binary-tree-2/

🧒 ELI5

假设你站在一排树的右边,每层你只能看到最右边的那棵。BFS 就是从上往下,一层一层看,每层最后一棵就是你看到的那棵。

Imagine standing to the right of a row of trees. For each row, you only see the rightmost one. BFS is going row by row from top to bottom, and the last tree in each row is what you see.

Day 64 — Soft Skills: How do you keep a long-running project on track when momentum stalls?

🗣️ 软技能 / Soft Skills — Project Management: Keeping Momentum


为什么这很重要 / Why This Matters

长期项目 (3个月+) 几乎必然遭遇动力衰退——范围蔓延、依赖方失联、团队疲惫、优先级漂移。Senior/Staff 工程师被考察的不只是「能不能交付」,而是在逆境中维持团队信心和执行力的能力。

Long-running projects (3+ months) almost always hit momentum stalls — scope creep, disengaged dependencies, team fatigue, priority drift. Senior/Staff engineers are evaluated not just on "can you deliver" but the ability to maintain team confidence and execution under adversity.


STAR 框架 / STAR Breakdown

Situation (情境):

项目推进到第 3 个月,遇到一个外部依赖团队连续 4 周未交付关键 API 接口,导致我们的进度完全卡住,团队士气低落,发布日期岌岌可危。

We were 3 months into a project. A critical external team missed their API delivery for 4 consecutive weeks. Our progress was completely blocked, team morale dropped, and the release date was in jeopardy.

Task (任务):

作为项目技术负责人,需要在不失去发布时间窗口的情况下,重建团队执行力。

As tech lead, I needed to rebuild execution momentum without losing the release window.

Action (行动):

  1. 诊断真实阻塞 — 开 1:1 了解阻塞真相:外部团队 API 文档不全 → 帮他们写好 spec,降低他们的摩擦
  2. 解耦依赖 — 用 mock server + contract testing 让我们的开发不再等待真实 API
  3. 重新切分里程碑 — 把一个大 milestone 拆成 2 周一个的「进度锚点」,每个都有可演示的产出
  4. 对上管理预期 — 主动向 PM 和管理层提前报告风险,附带缓解方案 (不是光报告问题)
  5. 庆祝小胜利 — 每完成一个进度锚点,在 Slack 公开认可团队贡献

Result (结果):

延期 2 周但完整交付,团队没有人离开项目。事后复盘时,这个「解耦+小里程碑」模式被推广到部门其他团队。

Shipped 2 weeks late but with full scope. Zero team attrition during the crisis. The "decouple + small milestones" pattern was adopted across the org after the retrospective.


❌ Bad vs ✅ Good

❌ 糟糕回答 / Bad Answer:

"我们加班把落后的进度赶回来了,团队非常努力。"

→ 只展示了执行力,没展示系统思维和主动风险管理。

✅ 优秀回答 / Good Answer:

展示三层能力:

  1. 诊断根本原因 (不只是症状)
  2. 创造可执行的替代路径
  3. 管理上下左右的预期

Senior/Staff 加分项 / Senior-Level Tips

  • 提前报警,不要晚节不保 — 每 2 周做一次 risk review,发现趋势立即升级,而不是等到危机爆发
  • 让「不确定性」可视化 — 用置信区间代替单点估算:"我们 80% 概率在 Q3 结束前交付,有 20% 风险延到 Q4 第一周,原因是 X"
  • 投资团队能量 — 「战时」也要保持 1:1,了解谁在 burn out,提前换人比临阵崩溃好
  • 构建交付肌肉记忆 — 坏消息要比好消息早到达管理层,这样你的信用额度才足够扛过危机

Key Takeaways / 关键要点

  1. 解耦优先 — 外部依赖阻塞时,先想「我怎么在没有它的情况下继续前进?」
  2. 大里程碑 → 小锚点 — 可见进展本身就能维持士气
  3. 主动管理 vs 被动汇报 — 带方案报告问题,不带方案只是转移焦虑
  4. 动力 = 进展感 — 团队需要感受到「我们在往前走」

📚 References

  • https://www.manager-tools.com/2005/07/the-feedback-model
  • https://www.amazon.com/Making-Work-Visible-Exposing-Optimize/dp/1942788150
  • https://paulgraham.com/mean.html

🧒 ELI5

项目卡住就像自行车链条脱落。不是踩得更用力 (加班),而是先下车把链条挂回去 (解除阻塞),然后把长途旅程分成几个小段 (里程碑),每段终点都有可以休息和庆祝的地方。

A stalled project is like a bicycle chain falling off. Don't pedal harder (overtime) — get off and reattach the chain (unblock), then split the long journey into short segments (milestones), each with a rest stop where you can celebrate progress.

Day 64 — AI: Structured Output & JSON Mode

🤖 AI — Structured Output & JSON Mode


问题背景 / The Problem

你让 LLM 返回 JSON,但得到这样的结果:

You ask an LLM to return JSON, and you get:

Sure! Here's the JSON you asked for:

{"name": "Alice", "age": 30}

Let me know if you need any modifications!

然后你的 json.loads() 直接抛异常。这种情况在生产系统里非常危险。

Then your json.loads() throws an exception. This is dangerously unreliable in production.


结构化输出是什么 / What Is Structured Output?

结构化输出 = API 层面强制保证模型输出严格符合 JSON Schema,而不只是「提示模型生成 JSON」。

Structured output = API-level guarantee that model output strictly conforms to a JSON Schema, not just "asking the model nicely to produce JSON."

实现原理:约束解码 (Constrained Decoding)

模型在生成每个 token 时,只能从符合当前 Schema 状态的 token 集合中选择。用上下文无关文法 (CFG) 将 JSON Schema 编译成状态机,逐 token 过滤。

Implementation: Constrained Decoding

At each token generation step, the model can only sample from tokens valid for the current schema state. A JSON Schema is compiled into a state machine (Context-Free Grammar), filtering token candidates at each step.


两种模式对比 / Two Modes Compared

JSON Mode (旧方式 / Legacy)

# OpenAI — JSON Mode
response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Return user info as JSON"}],
    response_format={"type": "json_object"}  # Only guarantees valid JSON syntax
)
# 保证: 是合法 JSON。不保证: 字段名、类型、结构
# Guarantees: valid JSON syntax. Does NOT guarantee: field names, types, structure

Structured Outputs (新方式 / Current Standard)

# OpenAI — Structured Outputs with Pydantic
from pydantic import BaseModel

class UserInfo(BaseModel):
    name: str
    age: int
    email: str

response = client.beta.chat.completions.parse(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Extract user info from: Alice, 30, alice@example.com"}],
    response_format=UserInfo
)
user = response.choices[0].message.parsed  # Already a UserInfo object!
print(user.name)  # "Alice"

Anthropic 原生结构化输出 / Anthropic Native Structured Outputs

据报道,Anthropic 于 2026 年初正式 GA 结构化输出功能,支持两种模式:

Anthropic reportedly launched structured outputs GA in early 2026 with two modes:

import anthropic

client = anthropic.Anthropic()

# Mode 1: JSON Outputs Mode (data extraction)
response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=1024,
    # Note: Exact parameter name may vary — check docs.anthropic.com for current API
    output_format={
        "type": "json_schema",
        "json_schema": {
            "name": "UserInfo",
            "schema": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "age": {"type": "integer"},
                    "email": {"type": "string"}
                },
                "required": ["name", "age", "email"]
            }
        }
    },
    messages=[{"role": "user", "content": "Extract: Alice, 30, alice@example.com"}]
)
import json
user = json.loads(response.content[0].text)  # Guaranteed valid + schema-conformant

# Mode 2: Strict Tool Use (agentic workflows)
tools = [{
    "name": "create_user",
    "input_schema": {
        "type": "object",
        "properties": {"name": {"type": "string"}, "age": {"type": "integer"}},
        "required": ["name", "age"]
    },
    "strict": True  # Parameters will EXACTLY match the schema
}]

最佳实践 / Best Practices

1. Schema-First 开发

先定义 Pydantic 模型 / Zod schema,再写 prompt,不要用字符串拼 JSON。

# ❌ 旧方式
prompt = 'Return JSON with fields: name (string), age (int), email (string)'

# ✅ 新方式  
class UserInfo(BaseModel):
    name: str
    age: int
    email: str
# 直接传 UserInfo 给 response_format

2. 处理拒绝 / Handle Refusals

if response.choices[0].message.refusal:
    # Model refused due to safety — handle gracefully
    print("Model refused:", response.choices[0].message.refusal)
else:
    user = response.choices[0].message.parsed

3. 两阶段生成 / Two-Stage for Complex Reasoning

对于需要推理的任务,先让模型 think,再格式化输出:

# Stage 1: Let the model reason freely
reasoning = client.chat("Think through this problem...")

# Stage 2: Format the conclusion
result = client.chat(
    f"Given this reasoning: {reasoning}\n\nNow output as JSON:",
    response_format=MySchema
)

什么时候用 / When to Use

场景推荐方式
数据提取 (实体抽取、分类)Structured Outputs
Agentic 工具调用Strict Tool Use
简单 JSON 转换JSON Mode (够用)
需要推理 + 结构化输出Two-stage generation
创意写作不用结构化输出

📚 References

  • https://platform.openai.com/docs/guides/structured-outputs
  • https://docs.anthropic.com/en/docs/test-and-evaluate/strengthen-guardrails/increase-consistency
  • https://docs.anthropic.com/en/docs/build-with-claude/tool-use

🧒 ELI5

让 LLM 返回 JSON 就像让朋友帮你填一张表格。普通方式:你口头说字段名,朋友可能填错地方。结构化输出:你给朋友一张预印的表格,每个格子里只能填指定类型的内容,填完自动校验——出不了错。

Asking an LLM to return JSON is like asking a friend to fill out a form. Normal way: you describe the fields verbally — your friend might put things in the wrong box. Structured output: you hand them a pre-printed form where each box only accepts certain content and auto-validates — errors are impossible.