力扣日均百万代码提交仅需三台主机 沙箱隔离取代算力成核心挑战

[Chinese] Case Study: Design Leetcode — Scott Shi

Scott Shi

LeetCode 评测系统属纯 CPU 密集型架构,短路判定与容量测算表明极小集群即可覆盖全天负载,架构设计的真正瓶颈在于防范解压炸弹与越权访问的沙箱隔离而非服务扩容。

AI-assisted summary of [Chinese] Case Study: Design Leetcode — original source. Check the original for full context.

Source published:

Key ideas

  • 与多数 I/O 密集型互联网系统不同,LeetCode 属于典型的 CPU 密集型(CPU-bound)架构,核心负载集中在沙箱代码执行与编译阶段。 (5:00)
  • 基于日活 10 万开发者、每日 100 万次提交(单次限时 200ms)测算,全天纯 CPU 计算耗时仅约 20 万秒,平摊后仅需 2 到 3 台物理宿主机即可覆盖常规评测负载。 (5:00)
  • 评测结果查询坚决不应使用缓存,推荐通过异步 Worker Pool 模式生成 Submission 收据,由客户端节流轮询或服务端 WebSocket / 长轮询主动推送处理。 (20:00)
  • 针对测试运行(Run Code),前瞻架构可引入 WebAssembly(Wasm)将代码直接在客户端浏览器编译并执行对比,彻底消除服务端运算与网络往返开销。 (14:48)

Detailed notes

## Background 该内容由 Scott Shi 于 2020 年 11 月 24 日发布在 YouTube 频道,视频围绕 LeetCode 及类似在线判题系统(Online Judge)的系统架构设计展开,并附带了详细的参考设计文档。

## TL;DR 本期系统设计案例深入剖析了 LeetCode 这一典型 CPU 密集型(CPU-bound)系统的核心架构。主讲人通过容量估算证明其核心算力仅需两三台物理宿主机即可承载,并给出了基于 API Gateway、Kafka 削峰队列与无状态 Docker 沙箱的解耦方案,甚至前瞻性探讨了使用 WebAssembly 将计算下放前端的可能性。此外,讨论重点聚焦于沙箱安全防御(防 Zip 炸弹、禁止网络与文件访问)、评测短路优化(Short Circuit),以及关系型数据库存储状态机元数据与对象存储托管长源码的动静分离数据模型。

## Atmosphere & style 分享整体呈现出务实深入、循序渐进的工程技术讨论氛围。主讲人结合自身刷通全题库的经验与一线工业界实践,穿插着诸如拿星巴克取餐小票类比异步 Worker Pool、调侃 Chrome 吞噬内存以及守法公民防攻击等风趣比喻,使硬核的架构拆解既专业又毫不枯燥。整场互动充满实战面试复盘的临场感,通过对踩坑警示与螺旋式思考的强调,给听众带来扎实而清晰的技术启发。

## Bulletin - [05:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=300s) 与多数 I/O 密集型互联网系统不同,LeetCode 属于典型的 CPU 密集型(CPU-bound)架构,核心负载集中在沙箱代码执行与编译阶段。 - [05:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=300s) 基于日活 10 万开发者、每日 100 万次提交(单次限时 200ms)测算,全天纯 CPU 计算耗时仅约 20 万秒,平摊后仅需 2 到 3 台物理宿主机即可覆盖常规评测负载。 - [20:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=1200s) 评测结果查询坚决不应使用缓存,推荐通过异步 Worker Pool 模式生成 Submission 收据,由客户端节流轮询或服务端 WebSocket / 长轮询主动推送处理。 - [14:48](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=888s) 针对测试运行(Run Code),前瞻架构可引入 WebAssembly(Wasm)将代码直接在客户端浏览器编译并执行对比,彻底消除服务端运算与网络往返开销。

## Key takeaways - [00:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=0s) 分布式数据库的多副本主要通过多数派仲裁(Quorum Voting)保障高可用与线性一致性,而文件系统多副本则核心依赖 Checksum 防御物理介质位衰减与静默损坏。 - [00:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=0s) 在缺乏拜占庭将军问题的前提下,计算节点的返回默认可信,但物理时钟永远无法绝对信任,即便是 Google Spanner 的 TrueTime 也只能给出带误差的区间范围。 - [05:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=300s) LeetCode 的容量瓶颈在于 CPU 算力而非数据 I/O,经容量估算,全网常规评测只需 2-3 个并发容器,峰值下 6-9 个容器或数台物理宿主机即可胜任,多余节点主要用于故障冗余。 - [10:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=600s) 系统通过 API Gateway 配合 Redis 校验用户 Token,核心评测任务打入 Kafka 消息队列进行削峰解耦,实现无状态 Compute Worker 弹性扩缩容并消除单点故障。 - [14:48](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=888s) 在前端利用 WebAssembly 进行代码编译与用例运行是极具前瞻性的 In-Browser IDE 方案,能将沙箱运行成本完全下放给现代浏览器,服务端实现零算力开销。 - [20:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=1200s) 代码提交评测采用类星巴克点单的异步 Receipt 模式(返回 submission_id),解决同步阻塞导致的系统可用性骤降问题。 - [20:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=1200s) 沙箱评测的核心壁垒在于安全性,必须严格限制输入大小(10~20KB)、截断标准输出防止解压炸弹(Zip Bomb),并物理切断宿主文件系统、数据库及网络套接字访问。 - [14:48](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=888s) 业务元数据量较小且调用模式固定时,无需盲目推进复杂的微服务架构,简明清晰的单体应用(Monolith)配合 Monorepo 往往兼顾效率与可维护性。 - [30:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=1800s) 评测结果具有即时状态流转特征,引入缓存会导致脏读或高延迟;工程上应依赖配置化轮询(Configurable Polling)或 WebSocket 服务端主动推送。 - [30:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=1800s) 引入短路执行(Short Circuit)机制:测试用例一旦失败立即中断并返回错误,按约 40% 的错误提交率推算,可直接节省评测集群一半以上的算力消耗。 - [30:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=1800s) 评测沙箱容器遵循“发射后不管”(Fire-and-Forget)哲学,单节点被恶意代码击穿后直接销毁并由 K8s 重建,任务向用户抛出 Internal Error 即可。 - [40:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=2400s) 提交数据建模应做动静分离:高频流转的状态机与指标字段存入 SQL 关系型数据库,庞大的源码只读大对象存放至 Amazon S3 或 MongoDB,切忌存入裸本地文件系统。 - [45:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=2700s) Submission 记录具备不可变性(Immutable),重复提交会生成全新 ID 而非原地修改;若无绝对证据支撑 NoSQL 优势,面试与实际工程应默认首选 SQL 关系型数据库。 - [40:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=2400s) 题目耗时百分比分布(Runtime Distribution)无需强实时计算,由后台离线定时批量统计即可;仅在竞赛等强实时场景下才需引入 Kafka + Apache Flink 流计算。 - [48:17](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=2897s) 用户连续点击提交应在客户端做防抖(Debounce)处理以减少网络浪费;未提交草稿则可通过 Session/Local Storage 或轻量级定期轮询持久化至后端。 - [50:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=3000s) 代码文本网络传输无需引入高强度压缩算法,以避免在 CPU 紧张的评测集群中造成昂贵的解压缩时间换空间劣势。

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=0s) 数据冗余的目的:数据库高可用与文件系统校验的本质差异

讨论以分布式系统底层的数据冗余机制开篇。主讲人指出,在诸如 Cassandra 和 DynamoDB 这样的分布式数据库中,副本(Replica)与数据冗余的核心目的是为了在并发读写时通过多数派仲裁投票(Quorum Voting)来保障系统的线性一致性(Linearizability)与高可用性(High Availability)。以经典的 Dynamo 论文为例,其底层使用的存储引擎是 Berkeley DB Transaction Data Store(后来亚马逊虽然更换了底层引擎,但核心原理未变);数据库引擎自身已经能确保落盘数据不会发生静默损坏(Data Corruption)。因此,数据库层的多副本设计绝非用于修复损坏文件,而是为了容忍节点下线或网络分区故障——即使某些节点失去响应,系统仍能保证读取到最新版本的数据。

```mermaid flowchart TD accTitle: 数据库多副本与文件系统校验对比 accDescr: 展示数据库通过多数派副本保障线性一致性,而文件系统通过哈希与校验和防止数据损坏的差异。 subgraph DB["分布式数据库 (如 Dynamo / Cassandra)"] D1["底层存储引擎 (如 Berkeley DB)<br/>自带防损坏与事务保证"] D2["多节点仲裁写入 / 读取 (Quorum Voting)"] D3["核心目的: 提高可用性与线性一致性"] D1 --> D2 --> D3 end subgraph FS["底层文件系统与 P2P 存储"] F1["物理存储 (SSD / HDD / 固态硬盘)"] F2["位衰减 / 文件易损坏 (Bit Rot)"] F3["依赖 Checksum / Hash 进行完整性校验"] F1 --> F2 --> F3 end ```

相比之下,裸文件系统(File System)缺乏数据库层的高级抽象。无论数据写入 HDD 还是固态硬盘(SSD),文件本身都可能因为硬件原因产生位翻转或数据损坏,且底层文件系统无法像数据库那样通过内置的事务日志或纠错算法直接恢复单条记录。因此,文件系统必须借助于校验和(Checksum)或哈希值(Hash)来比对数据完整性。在 P2P 对象存储系统中,由于节点频繁加入和断开(Churn),文件多副本除了防止单点故障(SPOF)外,每次读取都必须与 Checksum 及其它分片比对,以防读取到损坏的数据。

在系统设计中建立正确的思维模型至关重要:在假定没有拜占庭将军问题的前提下,计算节点本身返回的结果默认是可信且正确的,唯一的例外是**时钟(Clock)**。由于物理时钟在分布式网络中极难统一定义真理来源(Source of Truth),时钟读数永远无法绝对信任,即便在 Google Spanner 中依托 GPS 和原子钟构建的 TrueTime API,其返回的也仅是一个包含误差区间的边界范围($[\text{lower bound}, \text{upper bound}]$)。主讲人感慨,系统设计非常讲究这种“螺旋式思考”,很多问题初看可能考虑得不够全面,但随着不同观点的碰撞和对各类数据库底层逻辑的推演,理解才会逐步深化。

[05:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=300s) 剖析 LeetCode 核心功能与 CPU-Bound 系统容量估算

本期讨论的核心课题是设计 LeetCode(或类似的 Online Judge 在线判题系统)。主讲人展示了自己的 LeetCode 账号,提到自己几年前刷完了当时的全部题目(大约六百多道),并以自身经验推荐大家深入做题以理解系统设计与数据库选型。从产品功能出发,LeetCode 需要实现的核心 Feature 包括: 1. **题目列表与筛选**:按 Easy、Medium、Hard 分级展示题目; 2. **自定义测试用例**:支持在 Console 中自定义 Expected Input 和 Output; 3. **提交记录与反馈**:提交代码失败时返回明确的 Error Code,成功后可查看历史提交(Submissions); 4. **指标分布统计**:展示该次提交的 Runtime Distribution 与 Memory Distribution 百分比; 5. **在线调试(Debug Code)**:在控制台内实时单步调试或运行代码。虽然该功能在前端 UI 交互上看起来像在浏览器本地跑,但实际上底层依旧调用了后台的 REST API。

在系统约束与容量估算方面,LeetCode 与以往讨论过的 12306 订票系统、YouTube 或网络爬虫有着根本性的不同。以往设计的系统绝大多数是数据密集型(Data-intensive / Data-bound / Database-bound),瓶颈在 I/O 和数据库;而 LeetCode 是典型的 **CPU 密集型(CPU-bound)** 系统,其最重的计算负载完全落在 CPU 运算与代码沙箱执行上。

基于 LeetCode 的沙箱限制与用户规模,主讲人推导了系统的计算资源配额: - **单次运行开销**:单次代码执行的时间上限(Runtime Cap)按 200ms 计算,内存上限(Memory Cap)按 200MB 预估(实际部分题目内存上限更小,仅约 42MB 左右)。 - **用户基数与提交频次**:参考全美约 400 万程序员、热门求职招聘平台(如 Triplebyte)约 20 万工程师的基准,假设 LeetCode 拥有 10 万日活跃开发者(DAU)。假定每个活跃用户每天平均提交 10 次代码,系统每日承载的提交总量即为 1,000,000 次(1M Submissions/day)。 - **计算时间换算**:每天 100 万次提交,每次 200ms,对应的全天纯 CPU 高负载计算时间仅为 $1,000,000 \times 0.2\text{s} = 200,000\text{ 秒}$。 - **机器资源估算**:一天总共有 86,400 秒,平均下来系统全天并发需要的计算节点仅为 $\frac{200,000}{86,400} \approx 2.3$ 个 Docker 容器或虚拟机;考虑到白天与夜晚的高峰系数(Peak to Normal Ratio 约 3 倍),高峰期也仅需大约 6 到 9 个 Docker 容器。这意味着该系统的实际计算负载规模并不夸张,通常两到三台物理宿主机、每台运行若干容器就足以轻松应对。

[10:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=600s) 架构非功能需求与 API Gateway / Kafka 核心解耦设计

在系统非功能需求方面,首先是**弹性伸缩(Elastic Scalability)**。代码评测存在极强的昼夜周期性——夜晚几乎无人做题,机器空转会造成严重的云资源浪费;而白天及特定活动期间流量陡增。以周赛(Contest)为例,在线人数峰值可达 15,000 人,必须依赖弹性扩缩容机制:既可以基于定时计划(Scheduled Scaling),也可以依据监控指标(如宿主机 CPU 利用率超过 50% 时自动拉起新的计算 Worker)。其次是**高可用性(High Availability)**,确保用户随时能提交评测,解决早期 LeetCode 频繁出现的“Unresponsive”服务假死痛点。主讲人同时明确划定了讨论边界:题目下方的评论区(Comments)、服务端细粒度的权限认证体系(OAuth)以及限流熔断算法(Rate Limiter / Throttling)均属独立通用议题,本场不再赘述。

```mermaid flowchart TD accTitle: LeetCode 在线评测系统总体架构 accDescr: 请求通过 API Gateway 鉴权与分发,读写元数据访问 User Service,判题任务由 Kafka 削峰后分发至无状态 Compute Service。 Client["客户端 (Web / Console)"]

subgraph Gateway["网关层与缓存"] APIGW["API Gateway 集群<br/>(Rate Limit / Load Balancing)"] Cache[("In-Memory 缓存<br/>Redis / Memcached<br/>(User Token)")] APIGW <--> Cache end

subgraph CoreServices["业务服务层"] UserSvc["User Service (单体 / 无状态)<br/>元数据管理 / 题目列表 / 提交历史"] DB[(业务主数据库)] UserSvc <--> DB end

subgraph AsyncPipeline["异步判题计算流"] KafkaMQ{{"Kafka Work Queue<br/>(多 Broker 分区解耦)"}} Worker1["Compute Worker 1<br/>(Docker / 沙箱)"] Worker2["Compute Worker 2<br/>(Docker / 沙箱)"] WorkerN["Compute Worker N<br/>(Docker / 沙箱)"] end

Client -->|"REST API / 页面请求"| APIGW APIGW -->|"题目浏览 / 历史查询"| UserSvc APIGW -->|"提交代码 (Submit Code)"| KafkaMQ KafkaMQ --> Worker1 KafkaMQ --> Worker2 KafkaMQ --> WorkerN ```

系统架构设计的首要原则是消除单点故障(No Single Point of Failure, SPOF)。任何看似单一的组件(如 API Gateway、User Service、Work Queue)在物理拓扑上都是多实例部署。User Service 与 Compute Service 均被设计为**无状态服务(Stateless)**,节点宕机时不会丢失上下文,可随时按需销毁与水平扩容。在数据流向与容灾设计上: - **网关鉴权流程**:API Gateway 集群采用 Look-Aside 模式访问分布式内存数据库(如 Redis 或 Memcached)校验活跃用户 Token。若 Token 校验不合法,网关直接拦截并返回 `401 Unauthorized`;若 API Gateway 主节点宕机,备用节点上线接管时若发生缓存未命中,最坏情况仅需触发用户重新登录,不会波及核心计算。 - **任务排队削峰**:提交评测任务通过分布式消息队列 Kafka 作为 Work Queue 进行缓冲解耦。依托 Kafka 自带的 Partition 与多 Broker 容灾机制,即便单个 Broker 挂掉也不会丢失评测任务,后端的无状态 Compute Service 消费者集群则根据队列深度动态拉取代码进行沙箱编译与运行。

[14:48](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=888s) 代码执行机制演进:从 Serverless Lambda 到前端 WebAssembly

针对判题与调试操作(如点击“Run Code”),主讲人深入剖析了执行模式的演变路径。传统的做法是客户端将用户解答与测试用例驱动的包装类代码(Main Class)整体封装,通过 REST API 发送至后端;网关完成鉴权与防刷校验(避免恶意滥用或 DDoS 攻击)后,交由 Serverless 服务(如 AWS Lambda)或后端的沙箱容器启动运行并返回 Output 结果。由于代码执行生命周期短且无需保留运行状态,无状态的 Serverless Node 能够天然支撑这种突发流量。

然而,主讲人指出这并非架构的最优解,更具前瞻性的方案是拥抱 **WebAssembly(Wasm)**。以往 Web 浏览器只能原生执行 HTML、CSS 和 JavaScript 及其转译脚本;而 WebAssembly 作为全新的 W3C 规范,支持将 C++、Rust、Go 等多种语言直接编译为能在浏览器安全沙箱内执行的字节码。当时 Y Combinator 已涌现出多个主打 In-Browser IDE 的初创公司,其核心架构正是将用户的代码直接在前端浏览器编译为 WebAssembly 运行,并直接在本地对比预期输出。

采用 WebAssembly 方案能带来颠覆性的架构收益: - **零服务端计算成本**:用户的测试运行完全消耗其本地设备资源,系统不仅省去了往返网络延迟(RTT)与 REST API 开销,还彻底削减了后端判题集群的高额算力支出; - **浏览器宿主开销权衡**:针对“在浏览器跑编译运行是否会拖慢系统内存”的疑虑,主讲人调侃现代浏览器(特别是 Google Chrome)本身就是众所周知的“内存吞噬怪兽”。他当场切到系统的 Activity Monitor(活动监视器)展示 Chrome 动辄占用数百兆内存(如当前面板占用 353MB),因此微小的 Wasm 沙箱运行开销在现代客户端设备上完全可以忽略。

主讲人总结道,LeetCode 的业务元数据量相比其他互联网产品小得多,属于纯粹的 CPU-bound 业务,因此除核心编译运行沙箱(Compute Service)需要极其慎重地设计资源隔离、安全性与弹性伸缩外,周边的业务实体、题目列表以及用户元数据管理完全不需要强推微服务架构,采用一个简明清晰的单体应用(Monolith)配合 Monorepo 仓库就足以高效支撑。

[20:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=1200s) 异步 Worker Pool 模型与安全防御机制

在处理大量代码评测任务时,系统需要不断横向扩容计算机器来应对压力,这种场景非常适合采用 Worker Pool 模型进行异步处理(Asynchronous Processing)。当用户提交代码评测请求时,系统并不会阻塞等待执行完成,而是先生成一个收据(Receipt),即 `submission_id` 返回给客户端,随后客户端通过轮询(Polling)该 ID 来获取最终评测结果。Scott 举了一个形象的星巴克买咖啡例子:如果你去柜台点咖啡,服务员必须花 5 分钟做好咖啡亲手递给你才能接待下一个人,系统可用性(Availability)就会极低,排 20 个人就得等上很久;但如果服务员先给你一张小票(Receipt),做好了你再去取,系统的吞吐量和可用性就会显著提升。

针对 LeetCode 类型的在线评测系统,数据量与 CPU 吞吐并非最大难点,真正的设计壁垒在于**安全性(Security)**。Scott 开玩笑称,如果真有恶意攻击者想要搞垮 LeetCode,其实“并不是很复杂的一件事”,但自己作为一个守法公民(Good Citizen)自然不会去干。要保障沙箱执行环境的绝对安全,必须设下多道防线: - **限制代码输入大小(Code Size Limit)**:代码文件绝不能无上限接收,通常需严格限制在 10KB 到 20KB 以内,防止大文件耗尽内存或网络带宽。 - **资源访问与隔离禁令**:执行用户提交的代码时,必须严禁访问任何宿主文件系统(File)、数据库(Database)以及网络和套接字(Network / Socket)。一旦开放这些权限,系统将面临灾难级的攻击风险。 - **防御解压炸弹(Zip Bomb)**:攻击者可能将 1KB 极高压缩比的恶意数据和解压算法直接打包进代码,在内存中几何级膨胀解压出 2TB 的无用数据。此外,由于评测系统需要将标准输出(stdout)落库并回显给用户,如果未对评测输出大小(Output Size)进行阶段性截断(Truncate)和限制,评测机与存储端都会被轻易撑爆。

```mermaid flowchart TD accTitle: LeetCode 异步提交流程与安全防护 accDescr: 描述用户提交代码后的异步处理链路及沙箱执行中的各项安全限制 subgraph ClientLayer["客户端交互"] User["用户提交代码"] Poll["轮询 (Polling) / Push 机制"] end subgraph GatewayLayer["接入与安全校验"] Limit["代码与输出大小限制<br/>(10~20KB, Truncate Output)"] TaskQueue["异步任务队列<br/>(Worker Pool)"] end subgraph ExecutionLayer["沙箱执行环境"] Worker["Docker 评测节点"] ResourceBlock["资源访问禁用<br/>(禁 File / DB / Socket)"] end

User --> Limit Limit -->|"返回 Submission ID"| Poll Limit --> TaskQueue TaskQueue --> Worker Worker --> ResourceBlock ```

---

[25:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=1500s) 单体架构、SOA 与微服务的演进及生产运维重点

针对单体架构(Monolith)拆分微服务(Microservices)以及它与面向服务架构(SOA, Service-Oriented Architecture)的区别,Scott 进行了深入辨析。SOA 诞生较早,是与 Java 及传统企业级软件(Enterprise Software)同一时代的概念,它更多关注的是服务间协作的协议契约(Agreement / Protocol),例如通过统一定义的 REST API 规约 Resource 与固定操作(GET, POST, PUT),并给出调用 Guideline。而微服务与传统单体或早期的 SOA 在工程落地形态上有着本质区别。

以 Salesforce 为例,早期整个庞大的单体系统甚至需要一整台高配工作站(Workstation)才能本地跑起来,部署生产环境时也必须全量部署。但在真实业务中,各模块的流量分布极不均衡:获取/修改用户信息的请求可能只有 1k RPS,而核心交易业务(Transaction)可能高达 1M RPS。如果强行绑在单体中全量发布,两者的迭代发布频率完全脱节。拆分为微服务后,独立出的 Transaction Service 不仅代码体积骤降、便于专属团队维护,更重要的是可以按需针对性扩容(Scale),再也不用给每台支撑用户请求的节点都背负沉重的交易处理开销。不过,系统设计面试中通常不会把“单体 vs 微服务”作为绝对考点,因为两者各有适用场景与数据访问模式(Data Access Pattern),并无绝对优劣。

此外,生产落地的可观测性(Observability)和发布运维是不可忽视的环节: - **日志(Logging)**:可集成第三方云服务,如 LogDNA 等。 - **监控指标(Metrics)**:推荐业界标准的 Prometheus 搭配 Grafana 可视化面板。 - **异常追踪(Error Tracking)**:如 Uber 内部使用的 HealthLine,或者各类捕获 Error Stack 的应用工具。 - **运维比重**:实际软件研发中,单写原始代码可能只要一个月,但后续针对生产环境的 Rollout、Service Maintenance 及 Bug Fixing 往往要花上三个月,这才是消耗工程师精力最多的核心阶段。

---

[30:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=1800s) 结果获取机制、沙箱生命周期与短路执行(Short Circuit)

对于评测结果的获取机制,现场讨论了用户高频轮询给服务器带来压力时是否应该使用缓存。Scott 指出,**此处绝对没有必要使用 Cache**,因为评测结果具备很强的即时状态流转性,引入缓存不仅会导致结果脏读或延迟(Latency),更增加了不必要的系统复杂度。工程上推荐的解决策略有两种: 1. **客户端节流配置(Configurable Polling)**:前端提交后默认间隔 15 秒或 30 秒轮询一次,除非用户手动主动刷新(Manual Poll)才立即穿透拉取最新结果。 2. **服务端主动推送(Push Model)**:采用 WebSocket 或长轮询(Long Polling)保持连接,评测节点执行完毕后主动通知前端,彻底省去无谓的 HTTP 请求。

在评测的核心执行链路上,用户提交的代码(C++、Java、Python 等)首先作为纯文本传入。编译型语言先编译,解释型语言直接运行,两者均需要针对 $N$ 个测试用例(Test Cases)重复执行。这里存在一个核心优化技巧——**短路判定(Short Circuit)**:一旦代码在第一个测试用例产生不匹配的输出,执行节点立刻判定为 Run / Wrong Answer 并直接中断返回,不再继续跑后续用例。Scott 提到以自身约 60% 的提交通过率推算,全网至少有 40% 的错误提交可以通过短路机制跳过后续评测,这能够直接节省评测集群一半以上的无效计算开销。

评测资源与 Docker 容器调度细节: - **资源硬上限**:单次运行需严格限制在固定阈值,如 CPU 耗时上限设为 200ms,内存限制参考 LeetCode 现行策略设为 50MB(或根据语言基准调整)。 - **容器设计原则(Fire-and-Forget)**:评测代码必须完全运行在 Docker 隔离环境中。为了防止恶意代码打死宿主机,计算节点采用“发射后不管”(Fire-and-Forget)哲学,底层由 Kubernetes 等容器编排系统进行管理。若容器因恶意攻击挂掉(Crash),直接丢弃该 Pod 并由 K8s 拉起新节点;对于挂掉的任务则直接向用户返回内部错误(Internal Error),早期的 LeetCode 经常出现此类由于底层计算节点挂起导致的 Internal Error。 - **多租户平衡(Multi-Tenant Trade-off)**:每个请求单独新建 Docker 的开销过大,因此评测节点必然采用多租户共享模式。但单个 Docker 内部的 CPU 和内存资源配额必须精确把控,避免某一任务的资源毛刺影响共存 Session 的执行性能。实际上经过容量预估,LeetCode 评测核心负载只需 3 台物理机即可覆盖,多余的机器主要是为了提供错误冗余(Redundancy)。

---

[35:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=2100s) 核心 API 定义与关系型数据库建模(User & Submission)

该系统的数据模型与 API 较为聚焦,主要分为四组核心接口: 1. `GET /problems`:传入 `user_token` 与 `session_id`,返回题目列表、状态(已通过/未通过)与基础元数据。 2. `POST /submit`:接收 `user_token`、`problem_id`、`code`、`language_id` 及 `session_id`;时间戳一律在服务端生成,提交成功后返回 `submission_id`。 3. `GET /submissions`:支持单条 ID 查询或根据题目 ID / 用户历史拉取所有提交记录。 4. `GET /runtime_distribution`:获取当前代码运行时间(Runtime)在全网用户中的百分比分布(直方图打败率统计)。

在底层数据存储设计上,用户体系数据必须使用关系型数据库(SQL)。

| 字段名 | 约束 / 类型要求 | 业务与安全说明 | |:--- |:--- |:--- | | `user_id` (UUID) | Primary Key | 用户唯一标识符 | | `username` | Varchar / Unique | 用户名 | | `created_at` | Timestamp | 账号创建时间 | | `password_hash` | Hash Text (加盐哈希) | **严禁存储明文密码**,直接存明文属于系统设计致命硬伤(Red Flag) | | `membership_expires_at`| Timestamp | 会员到期时间 | | `payment_id` / Table | 外键关联独立表 | 支付信息独立拆表,以支持单用户绑定多张信用卡(Credit Card) |

---

[40:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=2400s) 代码存储权衡、提交状态机与实时统计引擎

系统数据层设计最核心的技术难点在于**代码文件与评测记录的存储形式**。标准关系型数据库(如 MySQL)单行行高上限通常为 8KB,直观上难以安全承载几十 KB 的长源码。对此存在几种架构选择:

```mermaid flowchart LR accTitle: 提交记录与源代码存储架构分离 accDescr: 状态与指标写入关系型数据库,代码写入对象存储或文档数据库 subgraph Client["用户端"] SubmitReq["提交代码请求"] end subgraph DataStore["分层数据存储"] direction TB subgraph SQL_DB["SQL 关系型数据库 (状态高频更新)"] SubMeta["Submission 状态流转<br/>(Submitted -> Running -> Success/Error)<br/>Runtime & Memory 消耗统计"] end subgraph Object_DB["对象存储 / DocumentDB (只读大文本)"] CodeBody["Source Code 源码内容<br/>(S3 / MongoDB)"] end end

SubmitReq -->|"写入元数据"| SubMeta SubmitReq -->|"写入代码大对象"| CodeBody SubMeta -.->|"code_path / ID 索引映射"| CodeBody ```

有些工程师会主张“干脆把整个 `submission` 表全部放进 MongoDB 等文档型数据库,因为其单个 Document 上限有 16MB”。Scott 指出**这是一个明显的糟糕解法(Yellow Flag)**。原因在于 `submission` 的生命周期中,`status`(`submitted` $\to$ `running` $\to$ `success` / `error` / `aborted`)、`runtime` 及 `memory_consumption` 字段需要被评测节点高频反复修改多次。文档数据库的更新粒度通常基于整行 Document,每次仅更新几个字节的状态,底层却需要将几 MB 大小的整份代码 JSON 读出修改再完整写回,造成极大的 I/O 放大。因此最佳实践是**动静分离**:状态机落 SQL,源码内容存入 MongoDB 或 Amazon S3 等对象存储(Object Storage)。同时注意:**绝不可将源码直接存放在裸本地文件系统(Filesystem)中**,因为缺乏完整性保障容易发生数据损坏(Corrupted),而 S3 等专业存储自带高持久性保障。

提交状态流转与直方图统计链路: - **状态机生命周期**:代码提交排队时为 `submitted`;Worker 取出任务开始评测变为 `running`;最终判定后写入 `success`、`error`;支持用户手动放弃操作(User Aborted),若工作节点物理 Crash,后台扫描进程也会将其置为 `aborted`。 - **直方图统计计算(Runtime Distribution)**:统计某道题目在特定编程语言下的耗时分布并渲染直方图,这类聚合分析**完全不需要强实时(Real-time)计算**,由离线后台进程定时扫描该题目的历史评测数据生成即可。假使业务端(如周赛 LeetCode Contest)强行要求实时展示 Trending Runtime 或滑动窗口统计(Rolling Window),则需引入事件流架构——将所有评测结果打入 Apache Kafka 主题,并由 Apache Flink 流处理引擎实时聚合算出。 - **关于 SQL 与 NoSQL 的选型原则**:Scott 强调业界共识是只要业务数据模型规整且单条尺寸可控,**应毫无疑问首选 SQL(No-brainer)**。选用 NoSQL 必须给出极强的前提证明(Justify)。不过针对特定巨头企业也有例外,例如在 Uber 内部,MySQL 在进行主从切换或 Leader Election 期间存在约 200ms 的不可用时间窗口(Unresponsive),为了追求极致的 4 个 9 甚至 5 个 9 可用性(High Availability),工程师才会有理有据地放弃关系型数据库。

[45:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=2700s) Submission 的不可变性、SQL/NoSQL 的选型权衡与分布式状态

对于在线测评系统(Online Judge)中的提交记录设计,当用户在前端点击 "Submit" 提交代码时,服务端会为其即时生成一个唯一的 Submission ID。这个记录从落库起就是不可变(Immutable)的;即使用户修改了代码再次提交,或者手抖连点了两次,系统都会生成两个独立的 Submission ID 与记录,而绝不会在原有记录上直接覆盖修改。针对观众提出“用户与其他表没有强事务关联,是否能直接用 NoSQL,甚至用 Payment Array 来记录付费”的疑问,其实现代关系型数据库(SQL)同样对 Array 和 JSON 字段有良好的支持。在系统设计面试中选用 NoSQL 时,候选人必须主动给出充分的论据来 "justify your choice"——无论是因为数据规模过大、可用性权衡、还是读取延迟要求;若没有极具说服力的硬性理由,最稳妥的选择依然是优先使用 SQL,因为在面试官看来 SQL 的事务与稳定性往往更经得起推敲。

关于代码编辑状态的归属,用户在 LeetCode 网页端编辑代码时,所有的按键与改动本质上全部发生在客户端的浏览器内存中,直到点击提交时才会向后端发起真正的 API 请求。针对分布式数据库中 SQL 主节点选举(Leader Election)耗时较长的问题,这是由于节点协商必须依赖分布式共识算法(Consensus Protocols,如 Raft 或 Paxos),整体协调与通信的系统复杂度极高,必然无法像常规单节点数据库写操作那样在数毫秒内完成。而在高可用与主从复制方面,系统的只读从库可以直接采用异步复制(Asynchronous Replication),因为现代高可靠基础设施中节点宕机的概率极低,异步复制足以应对绝大多数读取扩容场景。

```mermaid flowchart TD accTitle: Leetcode Submission and Storage Architecture accDescr: High level flow of code submission, metadata separation, and consensus-backed storage.

subgraph Client ["Client Tier (Browser)"] UI["Code Editor (In-Memory Draft)"] SubmitBtn["Submit Action (Debounce Check)"] UI --> SubmitBtn end

subgraph Gateway ["Stateless Service Tier"] API["User & Compute Services"] SubmitBtn -->|"POST /submit"| API end

subgraph Storage ["Storage Tier"] direction TB SQLMaster["SQL Master (Metadata & Transactions)"] SQLSlave["SQL Slave (Async Read Replica)"] ObjectStore["Object Storage / S3 (Code, I/O, Images)"]

API -->|"Generate Immutable Submission ID"| SQLMaster SQLMaster -.->|"Async Replication"| SQLSlave API -->|"Store Raw Code & Payloads"| ObjectStore end ```

- 任何关键服务都不能只部署单节点(Single Node),否则该节点一旦宕掉就会导致服务不可用(Unavailable)。 - 在整个系统中,绝大部分服务如 User Service、Compute Service 都是完全无状态的(Stateless),遇到高并发或算力不足时可以随意水平加机扩容。 - 唯一必须严格维护状态的是底层的关系型数据库;即使用户的 API Token 存在服务器内存中,由于节点故障是低概率事件(例如上百天才宕机一次),即便偶发宕机也只需让用户重新登录(Force Re-login)刷新 Token,不会造成灾难性后果。

---

[48:17](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=2897s) 客户端交互细节:防重提交、草稿保存与有状态服务容灾

针对用户手抖快速重复点击提交按钮的边界情况,这类频率控制最自然、最轻量的解法是在客户端直接做防抖(Debounce)处理:前端在每次提交后记录当前时间戳,如果下一次点击的时间间隔小于预设的安全阈值(如数秒以内),则直接在客户端拦截并提示提交过于频繁,避免无谓的网络开销与后端算力浪费。

对于用户登出后重新登录仍能恢复未提交代码的现象,属于典型的用户体验优化(类似于 Gmail 的草稿自动保存)。实现方式有两种可能:一种是前端做定时器(Periodic Save),通过轻量级 API 轮询将编辑中的草稿同步推送到后端持久化;另一种更加轻量的方式则是直接缓存在当前浏览器的 Session Storage 或 Local Storage 中。如果是后者,一旦用户更换浏览器或清除本地缓存,就无法再恢复未提交的代码。

---

[50:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=3000s) 代码压缩权衡、Cassandra 历史表与题目系统 (Problem Table) 设计

在网络传输中,代码从客户端发往服务端必然要经过序列化与反序列化(Serialization/Deserialization),但对于代码内容本身是否需要做压缩(Compression),则是一个典型的时间换空间权衡。压缩算法高度依赖 CPU 计算(CPU-bound),如果评测系统的后端 CPU 资源本来就非常紧张,为了节省微小的文本存储空间而引入额外的压缩与解压缩计算往往得不偿失。对于历史提交记录(Historical Submissions)的持久化,使用 Cassandra 同样是一个非常合理的方案:可以将 Submission ID 作为 Partition Key 或 Column Key,更新评测状态时只需修改特定的列,而无需重写庞大的代码字符串,避免了不必要的写放大。

对于有观众提出的“增量编译”(Incremental Compilation),在在线评测系统的多租户隔离与沙箱评测场景下并不适用。针对题目表(Problem Table)的数据建模,核心字段应包括 Problem ID、创建者(Creator)、创建时间(Creation Time)、标题(Title)、描述(Description)、输入输出测试用例 ID(I/O ID)以及元数据(提交总数、通过总数等)。对于描述中包含的插图等多媒体静态资源,应直接剥离并存入文件系统或 Amazon S3 等对象存储中;题目描述与输入输出文件本身由于体积较大且读写模式极其简单(只按 ID 单点读取),应与核心关系型数据解耦,存放在专门的文档数据库(Document Store)或对象存储中。

- 讨论区与评论系统(Comments and Discussions)非常适合使用 NoSQL(例如在 Slack 架构设计中采用的 DynamoDB)来支撑高频追加与海量并发读取。 - 题目通过率、提交数等高频统计数据若需要强实时性展示,可以通过 Kafka 配合 Apache Flink 等流处理计算引擎,将实时窗口数据与过去的基线数据聚合后再行呈现。

---

[54:55](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=3295s) 会员过期机制答疑与后续 Mock Interview、底层存储预告

针对会员(VIP)权益过期的逻辑设计,本质上并不复杂:当支付网关完成扣费事务后,服务端直接在用户账户上更新会员过期时间(Expiration Date)。系统支持设置自动续费(Auto-renewal / Auto-charge),定时任务在到期前触发扣费,扣费成功则顺延过期时间;若未开通自动续费或扣费失败,一旦系统当前时间越过过期时间戳,用户的会员权限即告终止。

在完成 LeetCode 整体架构的串讲后,整个系统设计的答疑告一段落。在系统设计学习中,理论与实战必须结合,“就跟打球一样,一边要学理论一边要搞实战,实战还是很重要的”。后续将组织志愿者参与全真模拟面试(Mock Interview),采用单人半小时实战问答的形式进行录制复盘。同时,后续课程将深入到数据库底层存储引擎的核心机理,重点对比分析 B-Tree 与 LSM-Tree(Log-Structured Merge-tree)的数据读写机制,并拆解分析型数据库(OLAP)与事务型数据库(OLTP)在存储模型与查询优化上的本质区别。

## Notable quotes > [00:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=0s) 在系统设计中建立正确的思维模型至关重要:在假定没有拜占庭将军问题的前提下,计算节点本身返回的结果默认是可信且正确的,唯一的例外是时钟(Clock)。 — Scott Shi

> [00:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=0s) 系统设计非常讲究这种“螺旋式思考”,很多问题初看可能考虑得不够全面,但随着不同观点的碰撞和对各类数据库底层逻辑的推演,理解才会逐步深化。 — Scott Shi

> [05:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=300s) 以往设计的系统绝大多数是数据密集型(Data-intensive / Data-bound / Database-bound),瓶颈在 I/O 和数据库;而 LeetCode 是典型的 CPU 密集型(CPU-bound)系统,其最重的计算负载完全落在 CPU 运算与代码沙箱执行上。 — Scott Shi

> [20:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=1200s) 如果你去柜台点咖啡,服务员必须花 5 分钟做好咖啡亲手递给你才能接待下一个人,系统可用性(Availability)就会极低,排 20 个人就得等上很久;但如果服务员先给你一张小票(Receipt),做好了你再去取,系统的吞吐量和可用性就会显著提升。 — Scott Shi

> [20:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=1200s) 如果真有恶意攻击者想要搞垮 LeetCode,其实“并不是很复杂的一件事”,但自己作为一个守法公民(Good Citizen)自然不会去干。 — Scott Shi

> [25:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=1500s) 实际软件研发中,单写原始代码可能只要一个月,但后续针对生产环境的 Rollout、Service Maintenance 及 Bug Fixing 往往要花上三个月,这才是消耗工程师精力最多的核心阶段。 — Scott Shi

> [30:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=1800s) 此处绝对没有必要使用 Cache,因为评测结果具备很强的即时状态流转性,引入缓存不仅会导致结果脏读或延迟(Latency),更增加了不必要的系统复杂度。 — Scott Shi

> [40:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=2400s) 干脆把整个 submission 表全部放进 MongoDB 等文档型数据库,因为其单个 Document 上限有 16MB。这是一个明显的糟糕解法(Yellow Flag)。 — Scott Shi

> [40:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=2400s) 绝不可将源码直接存放在裸本地文件系统(Filesystem)中,因为缺乏完整性保障容易发生数据损坏(Corrupted),而 S3 等专业存储自带高持久性保障。 — Scott Shi

> [40:00](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=2400s) 业界共识是只要业务数据模型规整且单条尺寸可控,应毫无疑问首选 SQL(No-brainer)。选用 NoSQL 必须给出极强的前提证明(Justify)。 — Scott Shi

> [54:55](https://www.youtube.com/watch?v=_eRTl_YxgUw&t=3295s) 就跟打球一样,一边要学理论一边要搞实战,实战还是很重要的 — Scott Shi

## Mentioned LeetCode, Scott Shi, Cassandra, DynamoDB, Berkeley DB, Google Spanner, Triplebyte, Docker, Kafka, Redis, Memcached, AWS Lambda, WebAssembly, Google Chrome, Salesforce, LogDNA, Prometheus, Grafana, Uber, HealthLine, Kubernetes, MySQL, MongoDB, Amazon S3, Apache Flink, Slack, Raft, Paxos

## Video description Please subscribe to my channel,

in this video, we talked with design of the leetcode or similar online judge system

doc: https://docs.google.com/document/d/1MgoMz8McpZAmAq_hJfn1topocHqfmAhcbqq42zJEMsY/edit

[Source](https://www.youtube.com/watch?v=_eRTl_YxgUw)

中文简报

力扣日均百万代码提交仅需三台主机 沙箱隔离取代算力成核心挑战 LeetCode 评测系统属纯 CPU 密集型架构,短路判定与容量测算表明极小集群即可覆盖全天负载,架构设计的真正瓶颈在于防范解压炸弹与越权访问的沙箱隔离而非服务扩容。 本期系统设计案例深入剖析了 LeetCode 这一典型 CPU 密集型(CPU-bound)系统的核心架构。主讲人通过容量估算证明其核心算力仅需两三台物理宿主机即可承载,并给出了基于 API Gateway、Kafka 削峰队列与无状态 Docker 沙箱的解耦方案,甚至前瞻性探讨了使用 WebAssembly 将计算下放前端的可能性。此外,讨论重点聚焦于沙箱安全防御(防 Zip 炸弹、禁止网络与文件访问)、评测短路优化(Short Circuit),以及关系型数据库存储状态机元数据与对象存储托管长源码的动静分离数据模型。 与多数 I/O 密集型互联网系统不同,LeetCode 属于典型的 CPU 密集型(CPU-bound)架构,核心负载集中在沙箱代码执行与编译阶段。 (5:00) 基于日活 10 万开发者、每日 100 万次提交(单次限时 200ms)测算,全天纯 CPU 计算耗时仅约 20 万秒,平摊后仅需 2 到 3 台物理宿主机即可覆盖常规评测负载。 (5:00) 评测结果查询坚决不应使用缓存,推荐通过异步 Worker Pool 模式生成 Submission 收据,由客户端节流轮询或服务端 WebSocket / 长轮询主动推送处理。 (20:00) 针对测试运行(Run Code),前瞻架构可引入 WebAssembly(Wasm)将代码直接在客户端浏览器编译并执行对比,彻底消除服务端运算与网络往返开销。 (14:48)

Reading summary

主讲人 Scott Shi 指出,LeetCode 属于典型的 CPU 密集型(CPU-bound)系统,而非传统数据密集型业务。其架构核心在于通过异步工作流与安全沙箱执行解耦计算,利用短路机制削减无效开销,并在数据层实行提交状态机与源码大对象严格分离的存储策略。 CPU容量估算与客户端执行演进 (5:00) 根据容量估算,系统计算规模远低于直觉预期。假设平台拥有 10 万日活跃用户(DAU),日均产生 100 万次提交且单次运行上限设为 200 毫秒,全天核心计算负载仅需 20 万秒 CPU 时间。平均折算全天只需约 2.3 个容器节点,即使面对 3 倍日间高峰,6 至 9 个容器或两三台物理宿主机即可支撑常规负载。 在代码执行演进上,除传统将代码打包通过网关交由后端 Serverless 或 Docker 沙箱运行外,主讲人提出更具前瞻性的方案是拥抱 WebAssembly(Wasm)。利用 W3C 标准将多语言编译为字节码直接在浏览器沙箱运行,可将“Run Code”调试阶段的算力完全转移至客户端,消除网络往返延迟与服务端账单,且现代浏览器内存足以承载微小的 Wasm 开销。 异步工作池、短路优化与沙箱安全防御 (20:00) 为避免评测阻塞,系统采用 Worker Pool 异步模型:提交代码后服务端生成不可变的 submission_id 凭证,客户端通过间隔轮询(15–30 秒)或 WebSocket/长轮询获取结果;结果因状态流转快严禁引入缓存。在执行链路中引入短路判定(Short Circuit),一旦首个测试用例未通过即刻中断返回。按主讲人 60% 的提交通过率推算,全网约 40% 的失败提交可借此节省评测集群超半数的无效计算。 针对沙箱环境,首要挑战是安全性防御而非吞吐量。必须设下严密防线:限制源码大小在 10KB 至 20KB;彻底封禁宿主文件系统、数据库与网络套接字访问;对 stdout 标准输出实施截断以防御解压炸弹。Docker 容器由 Kubernetes 管理并贯彻“发射后不管”(Fire-and-Forget)哲学,节点受攻击崩溃后直接丢弃并向客户端返回内部错误。 动静分离存储与提交状态机设计 (40:00) 系统数据存储的核心在于提交记录的“动静分离”。submission 记录的生命周期包含 submitted、running、success 等高频状态变更。若将整表存入 MongoDB 等文档数据库,每次仅更新几字节状态都会导致数 MB 的源码 JSON 全量写回,带来严重的 I/O 放大。因此推荐将状态机与耗时指标记录于 SQL(或 Cassandra 的特定列),源码和用例则解耦落入 Amazon S3 等对象存储,严禁直接写入缺乏完整性校验的裸文件系统。 打败率直方图(Runtime Distribution)无需强实时计算,由后台离线批处理定时聚合即可;若面临周赛等实时窗口需求,可接入 Kafka 与 Apache Flink。主讲人强调,只要业务数据模型规整,应首选 SQL 作为稳健基线,选用 NoSQL 必须给出极强的前提证明。 主讲人总结认为,LeetCode 业务元数据规整,外围功能采用单体配合 Monorepo 即可稳定支撑,无需过早推行微服务。在线评测系统的架构核心不在于盲目堆砌分布式组件,而在于以审慎的容量推演、严格的沙箱多租户隔离与动静存储分离,在控制算力成本的同时保障系统安全与高可用。

Source quotes

[0:00] Scott Shi: 在系统设计中建立正确的思维模型至关重要:在假定没有拜占庭将军问题的前提下,计算节点本身返回的结果默认是可信且正确的,唯一的例外是时钟(Clock)。 [0:00] Scott Shi: 系统设计非常讲究这种“螺旋式思考”,很多问题初看可能考虑得不够全面,但随着不同观点的碰撞和对各类数据库底层逻辑的推演,理解才会逐步深化。 [5:00] Scott Shi: 以往设计的系统绝大多数是数据密集型(Data-intensive / Data-bound / Database-bound),瓶颈在 I/O 和数据库;而 LeetCode 是典型的 CPU 密集型(CPU-bound)系统,其最重的计算负载完全落在 CPU 运算与代码沙箱执行上。 [20:00] Scott Shi: 如果你去柜台点咖啡,服务员必须花 5 分钟做好咖啡亲手递给你才能接待下一个人,系统可用性(Availability)就会极低,排 20 个人就得等上很久;但如果服务员先给你一张小票(Receipt),做好了你再去取,系统的吞吐量和可用性就会显著提升。 [20:00] Scott Shi: 如果真有恶意攻击者想要搞垮 LeetCode,其实“并不是很复杂的一件事”,但自己作为一个守法公民(Good Citizen)自然不会去干。 [25:00] Scott Shi: 实际软件研发中,单写原始代码可能只要一个月,但后续针对生产环境的 Rollout、Service Maintenance 及 Bug Fixing 往往要花上三个月,这才是消耗工程师精力最多的核心阶段。 [30:00] Scott Shi: 此处绝对没有必要使用 Cache,因为评测结果具备很强的即时状态流转性,引入缓存不仅会导致结果脏读或延迟(Latency),更增加了不必要的系统复杂度。 [40:00] Scott Shi: 干脆把整个 submission 表全部放进 MongoDB 等文档型数据库,因为其单个 Document 上限有 16MB。这是一个明显的糟糕解法(Yellow Flag)。 [40:00] Scott Shi: 绝不可将源码直接存放在裸本地文件系统(Filesystem)中,因为缺乏完整性保障容易发生数据损坏(Corrupted),而 S3 等专业存储自带高持久性保障。 [40:00] Scott Shi: 业界共识是只要业务数据模型规整且单条尺寸可控,应毫无疑问首选 SQL(No-brainer)。选用 NoSQL 必须给出极强的前提证明(Justify)。 [54:55] Scott Shi: 就跟打球一样,一边要学理论一边要搞实战,实战还是很重要的

Original transcript

[0:00] to record. Oh. Oh Record, okay. Just to hold the phone, it can be done with you data. But for the存在, it has a different purpose. In Cassandra and DynamoDB, the duplicate is a red column column. It is for you to be able to do and write when you read and write. The core is the vote. This is the vote. This is a simple way. For example, we use Denimal paper. Denimal paper, the database is the Berkeley database transaction data store. It is a Berkeley database. It is a data store. It is a data store. It is a data store. It was a data store. I heard it on Amazon said that he changed a data store. But you can see it in the bottom of the database. It has a database store. It has a database store. It has a guarantee that the data is not been corrupted. So the duplicates actually is not used to handle corruption. It's not to handle corruption, it's to duplicate. For handling availability, for increasing availability, for example, duplicate the sample. I can write two of them every time. I read the two of them. If I read and write two of them, I can guarantee the latest data. I can guarantee the latest data. I can guarantee the latest data. I can guarantee the latest data. So the duplicate, it's used to guarantee linear ability. And the duplicate case design is also increased availability. If you have a node, if you have a node unresponsible, it's not responsible. Unresponsible is node or network is not responsible. So this duplicate shows but in file system you can use file system. You will not have abstraction. You file system, any file can be crafted. For example, you file system is written in SSD or HDD. It is written in disk or in the固体硬碟. You can be crafted in this file. When you craft it, Actually, it's not a database that says you have this record crafted, and you can crack it, and you can use some error correction algorithms or something. It's not a database to guarantee this. So when you read this file, you can use incorrect data or crafted data. So the usual way to use check sum. or hash This is a guarantee, if you have a bit of a file, If you change the hash code or check sum, it's not the same. to test the file. So in this case, the duplicate case is often One is also to ensure that you have a copy of a single point failure. But in the other hand, it is not a database as a guarantee. So you have a duplicate, every copy is data. You read it, you must compare with other duplicate. You must compare with the check sum. You have a different section with check sum. You can compare with other parts. And then the last is P2P system. This P2P system is an object store. It's an object store. It's an object store. The node is a disconnect at field. Any time there will be a new node. It will be disconnected. So the file, and the底层, is a file system. So you need to check the correctness. You need to check the correctness. So the duplicate case is used in different places. There are different tools. In file system, you must run check sum, and see if the file is not a problem. But if you build the service on the database, the database is guaranteed to guarantee you the data is not corrupted. Because in design, you just think about this. In the first time, when design, computer runs every single node is guaranteed. One is you return the correct result, and you will not return the wrong result. You won't return the wrong result. The exception is clock. Clock will return the wrong result. Because clock is hard to define the source of truth. So clock's answer, clock's result is You can't rely on clock results, even if clock is spanner, you can know the arrow bound. You can see the arrow bound, upper bound and lower bound. Yeah. [5:00] 就是有一些discussion,我觉得还是比较有趣的,我觉得这个system design里面就讲究这种螺旋式思考,有时候开始的时候可能稍微弱一点,就是你开始的时候可能就是思考的时候有一个地方没有思考全面,然后你在不停的思考的时候,就是有有心的问题思考的时候,然后包括有人不同观点的时候,就是这个有一定的就是你自己的理解会不断的深入啊,不同的database的理解会不断的深入。好我们这周的话就讨论leetcode我可以看一下lead code in case大家没有用过lead code嗯List code的话,这边的话我们主要就是implement几个feature,首先你能看到List of problem,比如说你点开List of problem,你能看到这个list of problem,就是有一些哪些问题啊,然后有一些hard,medium,easy啊什么的。这个Lead Call Account也是我几年前做的一个,我当时是把收货题都做完了,可能做了600多题。但是反正就是连续的把所有题都做完了还是有一些还是挺有趣的然后我觉得这个database其实有的也挺有趣的,像我们system design的话,这个database可能需要理解比较深入一点。我推荐大家没有做过的话也可以做一下然后如果你设计Code的话,如果你有Fill的话,你需要return一个properErrorCode。然后其次的话,你可以自己define你的input/output,比如说我们随便找一个我们随便找一个随便找一个铁 to sum 好了在这面的话你应该有一个应该有一个有段时间没用了,有点不习惯,好,我看一下,debug code in background, run default ratio last code, edit shortcut,这块你应该是可以加一个input的就是expected input和expected output的就是下面的console对好的好的好的我有段时间没用了就是你可以 define 这个 input 和 output 然后其次的话 你可以看一下过去的 submission 比如说你可以看一下过去什么时候 submitted 过是吧 这种然后呃你可以看 runtime distribution 比如说对你其中一个 submitted code你可以看一下你的Runtime Distribution在什么在什么水平之间是吧,然后你也可以看到memory的这个distribution在什么水平之间好,然后其次的话,这个feature是比较新的,我昨天才留意到,就是你可以在这个console里面debug code在 console 里面 debacle 这块的话看起来好像在你的 browser 里面 run 的 但实际上它还是有一个 rest api endpoint好,我们说这个就是System Design里面就是你需要花很多时间去understand business,use case,common sense,你可能需要不停的问面试官,像这种情况下我觉得lead code相对我们大家都比较理解一点,我可能就会跳过了。constrents的话我简单估算了一下这个Litcode,就是我看了一下它这个cap的话,Runtime大概是200毫秒,然后memory是200MB。其实它更小我看它这个CAP的memory好像就是个42MB是比较小的,我这块是假设200MB。它其实是和我们之前设计的系统都不一样,跟我们之前设计的,不管是12306订票系统,还是YouTube,还是Crawler,那些都是data-bonded。说白了都是data-intensive application,都是database-bonded。这个里面的话它其实是CPU-bounded的,所以它的所有的heavy part都在CPU上面。所以说这个系统和我们之前设计是不太一样的。然后这块的话我假设有10万个Data User,作为一个Reference的话,好像美国是有400万个Programmer。很像比较热门的一些从事员找工作的网站的话,比如说TripleByte这种是有20万个Engineers,是吧?我计划就假设10万个Developer,Shrang user的话,然后每个User,Active user的话,每天是有10个Submission那这样算下来的话大概有一个million的submission那一个Million Submission的话,如果换算成每个都是跑到200毫秒的话,你实际上这个系统的计算时间就是它这个highway loadHeavy-Live compute 的时间的话是一天是28秒,那算下来的话,如果平均下来的话,average 的话可能是需要三个 docker,或者是Virtual Machine就是20万除以每天是86400秒如果consider peak to normal rate 可能是三个的话,我们可能需要6-9 docker during peak time,那这个并且这个可能需要,你可能需要一些elastic这种 scalability 是吧,就是你是比较 [10:00] big time的话你可能是多一点这个machine然后你不是peak time,可以少一点machine,但你这个compute的这个machine可能就是需要6到9个docker就可以了然后其实这个量不是很大,这个量真的不是很大,可能你需要三台模式,差不多一个模式,两个docker,或者几个docker,其实就可以handle了,这个量不是很大。好,然后我们再看这个requirements,这个go的话应该是,首先是high scalable的,因为这个白天和晚上这个是明显有作业周期性的嘛。就是晚上肯定没有人去设计 code然后白天的时候设计的 code 可能比较多然后context的时候参加的人的时候是比较多的像我看这位Edy同学说context的时候15000人peak time 所以这个10万的这个user还是远远大于这个context的人说的然后我们这块不讨论的话,首先是high scalable,其次是scalability需要非常Flexible 是吧,人少的时候,有很多经济在空转的话会浪费很多钱,所以人少的时候你可以把机器再减少一点,最好能elastically scale 是吧,但是不行的话,比如说你就固定的schedule一下也可以,或者是你可以detect,比如说你这个机器的话,CPU utilization rate 大于50%,你就新建一个新起来一个新的worker也可以,然后其次的话,how available,当你想去submit的时候,你总有机会去submit它,你不必要是有机会的,除了这个Service unresponsive我不知道现在LeadCore怎么样但是我几年前用的时候,四五年前用的时候,它总是非常容易unresponsive,我经常就不response了所以它的availability还是多少有点问题的然后我们不讨论了,这里面不讨论,一个是comments,就是这个comments我们在YouTube,然后在那个slack里面已经讨论过多次了,我觉得这个有很多duplicates,所以就不讨论了。然后其次的话,Server side authentication and authorization 是不讨论的这个其实是一个单独的议题,就是你Outh,Outh是一个单独的一个问题。我们就不讨论了,然后这个server side的sortle也是不讨论的,这个rate limiter和sortle也是个单独的问题。如果有计划我们可以讨论一下,但在这个问题里面就不讨论了。我这块设计architecture比较简单,其实它这个系统确实比较简单,这个里面的话,所有的API requests都要去API Gateway这个API-GitFace要做多件事情,它比较做rate limiting,要做OAWS check,表示你是一个valid user,你可能同时,这个API Gateway有多少有点load balancing的功能,就是它把这个request balance,比如说你这个user service有两个node,它这个user service是stiteless,所以你就可以把request balance distributed到这两个node里面,然后这是这个load balancing,就是API Gateway它主要做rate limiting OOS load balancing这都有通常解决,我们就不在这讨论了。然后这个设计里面有几个重要点,一个是首先是No Single Point Failure,任何一个东西,就是在你设计任何东西说,你可能只画一个service,比如说只画一个API Gateway Service,但是你一定要临界,它其实不是一个node,可能是多个node在后面。它不是简单的一个node,比如说这个work queue的话,这个work queue的话,这个就是当你summit code的时候,你就把这个codesummit到work queue里面,这个code里面呢,其实应该是,应该是这个,因为它是个kafka嘛,所以这个kafka本来就有scalable这个功能,你可能是有多个node去hold Kafka的保证你如果一个single note down的话,这个Cafrica topic还是要available的有人说让我放大一点谢谢大家好 这样应该好一点好首先要注意的是一定要有No Single Pied Failure, 这里面User Service和Compure Service是Stateless, 意思是如果Service down的话它是没有任何State的然后就没有任何 state,所以你就是需要就是就是你意思是你可以随便的 bring up worker,就是可以增加这个 worker 数量,特别这个 compute service 是不应该有任何 state 的这个情况下如果你load比较高的话你可以多加几个node去计算这个submission,如果load比较低的话你可以减少几个service然后这个API Gateway的话,API Gateway是有Active User Token的它可以成ActiveUserToken,或者是你可以把ActiveUserToken放到一个InMemoryDatabase,比如说Redis或MemCache,然后你这个APSGateway呢是比如说你如果要查这个User是不是ValidToken的话,你可能先去Redis和MemCache里面,lookup这个Token,然后说这个Token是Valid以后,然后你再继续设众其他Request。 [15:00] 这个token不是vallet的话,你这块就应该终止掉了,就应该return emellied access,或者是request unauthorized access,401这种error然后这个API gateway也是多个server的,只不过这几个多个server呢,如果不是stateful的话,如果你不是 stateful的话你是你需要look aside,就是look aside去那个in-memory database 去查你的token这块的话,你防止single-point failure其实非常简单,你只要有一个backup API gateway service就可以当你major service go down的时候,你就backup起来就可以了。如果你这个API Gateway service down的话,user可能要去backup那个API Gateway service,这时候你的token是可能被lost的,如果你存在in-memory的话存在in-memory token的话,这个token可能会loss,那你这样的话可能需要false re-login,这个不是一个主要的意想,因为毕竟这个service down的概率,就是这个机器down的概率是非常低的。然后这个Online Compound就是我们看起来像是在browser里面Submit code 你可以在这里面Submit code 比如说你直接点个run code 在这里面run实际上它还是一个REST API endpoint这个情况的话,它其实就可以用一些serverless node,比如说AWS的lambda这种serverless node,你实际上submitted的时候,你是submitted的时候的complete code的,你就是说不只是你自己的solution code,你把mate class的code一起submit过去了。所以这种情况下,这个server是完全不需要transtate的。这种情况下的话,你只要用这个API-GitP去verify你这个token,你不是想DDOS,你不是想abuse它的resource那你就可以只要验证了你确实是个Register user你就可以靠它的那个 serverlessserverless lambda去run这个code,然后verify input output,我想说的是这个其实不是最优解这个不是最右解,最右解的话应该是用web assembly,web assembly是一个最新的一个technology,它是一个standard,它是一个最新的standard,它是browser是支持WebAssembly,是直接在这个browser里面 run的,我们知道现在这个browser里面能 run的那个code只有HTTP 是吧 他可以 render HTML 可以browser 可以 render HTML CSS和JavaScript还有一些比如说 JavaScript 上面衍生的一些一些 codeWebAssembly是去年12月刚通过的,就是说它可以把你的其他codecompile 从 Web Assembly Code 然后直接 run 这个 Web Assembly Code实际上 Y Combinator 有个最新的 startup 最新的有一个 startup 就是专门做这个 in browser 的这个您可以再用的Browser作为IDE就是你所有的code呢就是写在browser里面写code的然后你brother里面写code呢,然后你可以用brother里面把它你的codecompile 成 web assembly,然后你的 browser 就 run web assembly,这样是最好的这样的话你就减少了,你就没有这个 additional cost,你就不需要去靠这个你减少一个REST API call 还减少你的server load 实际上你的browser就开始run了嗯对吧这个其实是比较新的一个technology,非常新的一个technology,就是去年年底才通过的,现在主流的brother可能最新的更新可能都支持webassembly的run了。这样的browser是不是很费内存,你如果看一下这个内存的话,你看一下这个Activity Monitor的话,你会你会留意到这个Chrome永远是非常在内存的。这个Chrome是非常非常在内存的,就是你可以看一下自己的这个Chrome是非常非常在内存的我们可以看一眼我这块chrome是占了353兆的内存chrome是非常占内存的on on on对好,我这块是作为一个衍生阅读吧,如果大家感兴趣的话可以读一下WebAssembly,这是非常新的一个protocol。好,然后当你的这个data much smaller compared to other services,这个data总体来说是个CPU-bounded,这个service总体来是CPU-bounded的。所以说我们这块其他的这个所有的relationship,所有的这个metadata management,就用一个user service就可以handle了。没有必要做一些非常细的micro service我个人觉得这个特别是你做这种非常小的service 蛮简单怎么样来你就只有个monorepo和monolis service 就足够了然后这个Compute Service的话是Heavy Lifting part,就是你Submit Code, Run Code的计划是比较重的部分 [20:00] 就是就是计划是你的不同的那个需要被不停的增加机器去optimize的地方这种情况下我们可以用一个WorkerPoolModel,这个也是之前讲过很多次,就是你过来的时候就是Asynchronize处理,你比如说你要run这个code,我先给你一个Recepe,SubmissionID,然后你可以不停的Poll submission ID,然后我什么时候执行完了这个code的话,我告诉你说这个执行完了。然后这样的话这个好处是什么呢当你是Asynchronized Processing的话你的Availability会提高这个我举过很多次例子就比如说你去新巴克点咖啡你可以说这个点咖啡的时候然后那个Serve咖啡的人直接把咖啡做好然后再还给你这是一种是吧或者是他给你个receive然后你过段时间去问一下他咖啡做好了没有然后再给你,如果他每次过来一个人他花五分钟时间去做好咖啡还给你的话,那它其实整个availability会很低,你有20个人就得等一个多20了,但如果它asynchronized处理的话,它这个availability会显著的提高。所以这样Asynchronize处理的话也是增加这个Litcode的这个网站的availability然后其次的话,我们就是这个网站呢,我个人觉得Litcode这块难点呢,是,呃,难点主要是被这个,这个就是,它整个data是没有难点的,CPU 帮的这块是没有难点的,但是它 security 是很大的一个难点,如果你真的是想去把 lit code 公当的话,我觉得不是很复杂的一件事,但是我自己没有事,我还是做一个好的good citizen,我是没有事的。但是我觉得如果你真是malicious,想把lead code供当的话,我觉得可能性不是太复杂。比如说他这个里面,首先为了security,首先你一定要put a limit on code size,你这个code不能超过,比如说不能超过10kb,10KB其实也很大了,或者计划的话我就简单定义不能超过20KB的,message size不能太大。然后其次的话其次的话,比如说你这个code run的时候,你不应该访问任何file,你不应该访问任何 file 你不应该访问任何 database 你不应该访问任何 network 和 socket当你访问这个的时候你就本身就有很大的security risk这是我觉得leadcode相对来说,如果我runleadcode的话,我觉得是比较难点的地方。但是你这种情况下你还是有一种方法可以去攻击这个Litcode网站,比如说你可以用一个zipbomb,就是你有一个压缩好的文件,当你解压缩的时候它size会几何倍的那个,比如说你压缩好只有一个k,但你解压缩的时候可能就两个t的size,两个terabyte size,这个是这个zipbomb的一个什么,如果你把这个zipbomb扣的,你可能不用它library,你就直接把这个code和解压缩的算法超过去然后你就zibomb一下然后反正Lead Code本身也需要把它的output显示给你嘛,所以如果它不限制它的output size的话,那你这个ZBomb它,其实我觉得效率也很高,是吧?毕竟它要把它output存下来,然后再返回给你。它可能tronk它的opputsize,但总体来说,我觉得Litcode的security check是比较复杂的一部分。是比较复杂的一部分然后这块的话,user service也可以broke down to micro service,但是现在的话就monolith service就比较好。然后有人问说micro service SOA有什么区别?SOA是一种agreement, service-oriented agreement更多的是一种谢谢大家更多的是一种就是你不同之间合作的时候哦它是一个protocol,是个agreement你知道吗,就是说你比如说你communicate的时候,你有一种agreement,像API一样,就是你定义好你的API,然后你们之间交流的时候,比如说你这个service之间交流的时候,你通过这个agreement来交流。对, service-oriented architectureService-oriented architecture. SOA是比较早的概念了,它很多时候绑定的是,比如说跟Java,Java 同一年代出现的,Java 和 Enterprise Software同一个年代出现的他很多时候,他很多时候就是,更多的时候是说,你怎么样有一个非常好的比如说你这个API design的时候你就是你是个REST API你REST API的话所有的REST API你都是就是一个resource或一个object是吧你定一个resource object然后你的REST API的话你的那个所有的action也是固定的比如说get post put 是吧,然后你当你进行这个,当你给这个第三方service或者internal一个啊Teams提供这个Service的时候,你就说你这个Resource应该怎么使用,你是不是有一个guide line,然后有个best example这种这个 [25:00] Purice 的话Micro Service的话跟这个不一样,Micro Service的话是跟SOA不一样,SOA的话很多时候是指的,因为它那个出现年代的话很多时候指的是monolith service,就是你deploy的,比如说像Southforce的话,有个Southforce是run到一个desk上的就说我在Southforce工作的时候,我们所有的Southforce,整个Southforce软件呢,你需要一个workstation来run的。它一起run的这种情况下,当你deployed production的时候,你就整个把Salesforce的软件deployed production上了。但是比如说你这个service使用的情况不一样,是吧?比如说你在某一个地方,比如说你get user或者user modify,你的request per second可能只有1k。但是你比如说你做一个transaction的request per second是一个million那你run的时候,deploy code的时候,你这两个codes同时deploy过去,但是它unplied code frequency是极其不一样的如果你做Micro Service的话,你可以就是把Transaction作为一个单独的Service,把它剥离出来。然后你这样的话,你这个Transaction Service,首先你这个Code Size会减少很多是吧,因为只有Transaction Service。co-size会减少很多 其次的话你这个你作为专门的一个team作为一个专门team的话,你可以专门maintain这个transaction。其次的话,你skill的时候,你这个transaction service也可以单独的skill。你不需要每台机器都有一个monolist的一个server,不需要每台机器都有个monolist server,然后起来说不仅支持你的transaction的request,你也支持user request,你可以专门做一个就是专门针对的一个transaction的这个这个transaction的service 然后只针对transaction request好所以如果你感兴趣更多的话,你可以搜更多的信息。这个Micro Service和Monolith Service有很多更详细的对比,你可以读几篇文章看到其中更多的区别。一般这个是不会作为一个不会作为一个就是专门考的考点来讲的,就是不会System Design的时候不会考这个,因为这个Monolysis和Micro Service呢,其实大家还是有不同的Preference,就是没有说是一个一定比另一个好的还是depends on the use case, depends on the data access pattern, depends on the data flow没有说有绝对的好,但是您可以看一下这两个明显的不同,Micro Service不同和Monolith Service不同,然后你需要有一些简单的understanding,好好 最后的话需要加一些Service Login Matrix Error Stack嗯login的话比如说你可以用一些promiseus然后加那个graphina是吧Login的话,比如说有很多第三方Service,比如说那个LogDNA啊什么,有好多第三方Service支持这个Login是吧。然后Metrics的话就Promisius加Graphana是吧,Aerostack的话,Uber是HealthLine嘛,就是都有很多,有很多那个Application你可以用的。这个Rollout的话,如果你的Design问题比较简单的话,你可以简单提一下,因为毕竟在日常生活中研发问题中,就是production的时候,rollout,service,maintenance是最复杂的一部分。其实比原始的code development更复杂。我个人觉得就是code development的话,比如说code development是花的时间,假设一个月的话,你这个mainten,rollout,还有backfixing,可能花三个月时间,这可能是占据正常developer时间最多的地方。如果有时间的话,你可以扫一下这个方面。好,然后这个API的话就相对比较简单了,我先停一下,就是这个architecture带有什么疑问吗?会有什么观点,不同的观点,或者觉得这个哪一块儿,不明解或者是觉得哪块有问题可以直接说就是直接说如果有问题的话Hello Scott, 您说, 还有一个问题, 就是你在讲, 就等user他submit了一个, 呃,就是query之后呢然后相当于会它会constant pull然后去看这个submit的结果所以说它在短时期内会产生很多的API call就这块一般是怎么handle的是用cache吗还是有什么其他的办法不用cache啊这块没有必要用cache的因为你用cache的话你的result肯定是就是有learnetency的或者那个什么的,并且用开始增加了unnecessary复杂度。这块用什么呢?就是您可以做几种方式,一个是你可以,像这个Litcode的话,你实际上这个client呢,你可以就是你这个Pol的frequency可以自己configure嘛,比如说你submit了以后你就15秒或者是30秒去Pull一次这个其实就无所谓了是吧除非你user自己手动的去Pull这个result那这种情况下user [30:00] 手动Pow的话你应该return最新的结果是吧如果不是的话你就15秒30秒Pow一次然后其次的话你可以做那种就是那种啊就是push model,push model有很多比如说用web socket啊这种然后有一些老的比如说Long Polling这种是吧,Long Polling或WebSocket就可以,就是Client端不去push,不去不去泡 server 端而 server 端好的时候 notify 去看的 比如说 web socket 和 long polling 这几个 mechanism好的 感谢好的 大家有什么其他问题吗好如果没有的话我们接着看啊这边的话就是问就是这块API的话相对比较简单,我觉得这个Leadcode总体来说跟YouTube比就是简单很多,YouTube前天我花了好几个小时,Leadcode这个我可能写了一个小时多就写完了。YouTube那个可能前天后来大概写了四五个小时我觉得它中间还有很多复杂的地方,如果大家感兴趣的话可以再回看一下YouTube那个视频,我觉得还是很有趣的地方。YouTube还有很多很有趣的地方。好,这个的话就比如说你可以getLitcode problems,然后你给一个user session,一个user token和session ID。不同 session 的话这个 problem 可能 mark 的不一样是吧然后你就显示 problem ID 然后 problem title 和这个 marker 是到底之前做过还是没有做过然后另一个的话就是 submit endpoint嗯上面的end point的话就比如说user token,problem id,code,language id和session id,这里面的话你这个timestamp是在server generator,所以你肯定不会从这个API这块submit。然后当你的Code submitted以后,你会return一个submissionID,然后你就可以hold这个result,然后看一下你最新submission的result。然后在internally,在这个serverinternally,你可能会要把这个saucecode的,就是当你把这个saucecode的,不管是比如说C++还是Java,当你把它交过去的时候,它首先是个文本。然后你需要compile这些 code如果是compile的language的话你可能compile code如果是interpret language的话interpretation language比如说python的话你就直接run就可以了不管你是Compile language 还是Interpret language 你就需要不停地执行N次 是吧?这个N是和你的这个test case相关的然后你把这个Result output呢和你的Crack output进行对比因为我记得这个LitCode是short circuit的,就是只要第一个不一样的时候,它就return,这个result是run,这个code是run,这样可以节省很多时间,因为大部分我不知道平均通过率多少,但是我自己的话是可能有60%的通过率。就是40%的code 是上交的时候是run的,就是,就是,所以这样的话可以至少能节省一半的这个一半以上的这个 unnecessary result trap就可以short circuit一下然后这个run的话再次说一定要cap到200毫秒和200MB实际上的话我看它Litcode是50MB,我们就说50MB,然后你需要额外的Security check,这个Security check我总觉得还是比较复杂的一部分,我觉得如果SUM的话就是你不能访问任何Resource你可以暂用CPU,你可以暂用memory,但你CPU和memory就是时间是固定死的,但你不能访问任何resource。然后其次的话这个code是一定要run在docker里面的因为这个docker里面如果你这个docker有毒是吧你这个crush掉了然后这个你可以有一些Docker Orchestration Service比如Kubernetes可以 bring up一个新的Docker node,所以你这个Docker挂掉挂掉了,所有这个Hairly lifting所有这个Compute node, Heavy Lifting,都是Sign and Forget,Sign and Forget的意思就是说它如果挂掉就挂掉了,如果有需要的话,我们可以兴起一个,就是兴起一个Compute,或者兴起一个新的Worker,就是Compute Service。那之前挂掉的话,因为我们现在早不见这个Submission,这Submission就死掉了,那我们就说可以,不管是Return Internal Error,还是Return什么Error,反正Return一个Error就可以了。我记得Litcode早些年的时候,我上次用Litcode三年前,还是有很多这种internal error,经常就是submit以后就遇见一个internal error,要求我重新submit,这种可能就属于server挂掉了。谢谢大家这个也是就是加一些better isolation,就是当你比如说当你这个Docker Node挂掉了以后,你不应该影响其他的是吧,比如说我觉得这个Docker Node的话,这个Docker CPU的Size和这个MemorySize还是需要非常carefully orchestrate it, [35:00] 你肯定是一个 multi-tenant的一个 docker,不可能一个 docker只 serve 一个 request你这个overhead太大,就是你新建一个docker的overhead太大,你肯定是monit tenant。但是你这个monit tenant如果太大的话,你docker cpu size和memory size太大的话,你一个program,一个session run的时候会领想另一个session的performance。所以这样也不太好,所以这个可能他需要不停地做然后决定一下具体是什么样的Docker的这个CPU数量啊MemorySets是比较符合它这个Performance就是CodeEfficient是最高的我们看到过其实这个按这个之前这个数量估计的话,它其实只要三台机器就可以了,并且这三台机器不是说为了handle这个heavy load,更多是如果其中有一台机器到的话,就是提供一些Aero的荣誉是吧?提供一些荣誉就够了所以整个这个cost其实整个这个least code cost其实很低的它更多是一种content cost就是比如说初心提的时候怎么样提更有质量这种content cost好其次的话,您可以有guess submission,是吧?这里面problem就是您可以根据一个IDguess submission,也可以根据所有的这个,就是所有的 submission 来 get submission嗯根据你自己过去所有的submission来getsubmission然后最后的话是getRuntimeDistribution比如说这个就是我们这个Runtime花了多久然后打败了多少的用户这种谢谢大家啊好,这个 API嗯就这样子了然后大家有什么问题吗这块可能还有一个就是更简单就是get problem get single problem就是我这块没有写然后这个就是detail的这个data model我也没有写很细啊,因为这个我觉得跟我风格有关,就是这个我觉得是比较tds的work,其实没有什么难点,就当然比如说你要get problem,给一个user token,给一个problem id,get problem的话,自己也可以handle掉,就是他怎么样的data结构里面要说fill的,这些都可以自己handle掉,我这块没有练很细啊。好,这个API这款大家有什么疑问吗?而Edith说,如果按memory limit应该是 language based, Python 可以很大的 memory, Java 的话,250 megabytes 基本就很大。就是吧, memory limit exceeded.然后是否考虑需要discussion部分啊,我这块是没有考虑discussion和comments的啊,为什么呢?因为在那个YouTube那个视频里面有专门一个session怎么样讲community,然后discussion,就是上周,上周三啊上面有讲那个 discussion 和 community 我觉得是非常重复的内容 我觉得没有必要去讲重复内容好 如果没有问题的话我们看最后的database这就是最后一部分了好Database 首先有个 user table,然后这个 user table 肯定用 SQL 我觉得说 user table 用 SQL 应该是没有什么问题,除非这个User Table设计的有问题啊,我觉得用Useql的话肯定是比较来说比较约定输送的东西了有个user uoid 然后有个username 然后有creation date,password是用hash的,password是一定要用hash text的,就是你不能存密码的,否则的话你security会有问题。这也是design中的一个red flag就是你password如果用明码存的话是一个非常大的red flag然后有个Membership Expiration Date 这个是如果你是有Member的话是这样子然后你有Credit Card,Payment,是吧,Payment也可以单独做出来,做一个单独的Table因为你一个人可能有多个payment,可能做一个单独table,然后还有一些additional metadata,比如说location,github,link,link,link,这种我就不列那么细了。然后其次的话,这块需要有个session table,我这块好像是可以自己定义的,比如说我这块就create了一个,Create 了一个 Java Session 是吧?然后还有一个 Session 是有个 Go Session,就是你可以create 一个 Session Table然后最后的话是有个submission table嗯Submission Table是这个里面我觉得相对来说从Data角度比较复杂的一部分,我们注意到一点就是SQL的Rho Size是8个KB的,就是我查了一下就是SQL的Rho Size是8KB的,所以这个Code呢,直观感觉这个Code是不能直接存在这个SQL里面。然后这个code可以存在两个地方,一个是存在Document Database,因为MongoDB的Document Size 16MB所以这个code是可以存在mongo里面,或者是存在一个,这个code也可以存在哪里,也可以存在一个object storage,比如说amlon,s3这种地方也存在去也可以。但是这个code,就是可能会有一个solution,说是你把这个code, [40:00] 整个这个table,整个这三mission table 存在mongo里面然后把这个code也存到Mongo里面,因为毕竟Mongo的这个Document size比较大我觉得这个不是一个好的solution为什么呢?因为这个里面这个submission这个state是要被反复更改过几次的包括你这个runtime和memoryconsumption是反复更改过几次的如果你把code和这两个放在一起的话,特别这两个需要更改过一两次的放在一起的话你每次更改的时候你实际上是读取整个大的几个MB的Json然后modify一些,然后再把它写回去,实际上每次modify的时候,每次modify是document,就是granularity在document上的。所以你这个code其实反复被写了两遍就虽然你只改了比如说你这块只把state从submitted变成running但你这个code是反复读了一遍又写了一遍的总体来说,如果你想优化更多的话,这个不是一个很好的选择嗯但是如果以这个lead code的这三密审量的话,final9也不是一个很大的red flag,是一个yellow flag,不是一个很大的red flag然后最后的话这个State的话和Runtime Matrix, Runtime就是表示你这个Code run了多久,然后你consume了多少memory,然后这个state的话可以是submitted当你 submit code 的时候,然后 queue 到那里就 submit it然后当这个有一个worker pick up 你的 code 的时候就 running然后当成功是success或者是error,然后user应该可以about it,有个user about it operation。或者是你这个worker node直接就down掉了是吧,你找不见这个submission了,你也可以把它变成有个background process,把它变成about it然后最后的话,这个RuntimeStates,RuntimeStatuses,Statistics就是你看到这个Data呢,Runtime Statistic Data 应该有另一个Process generated, 它可以读下所有的Submission对于同一个Problem对同一个problem同一个language所有submission然后把它简单统计下,然后做成一个非常简单的一个,一个一个就是那个直方图,然后,然后跟你现在的三 mission在一个脂肪处的位置然后把它显示出来应该有另一个就是这个process处理,这个是不需要real-time 的。不需要real time如果是需要real time的话有一些case比如说这个trending什么real time啊比如说假设leetcode有个这个trending runtime比如说Lead Code Context有个Trending Runtime的话,你可以把这个说的这个submission放在一个Kafka Topic里面,然后你可以用Flinkflink 处理一下这个 runtime,叫你有个 trending runtime,或者是 rolling window runtime但是实际上大部分的时候都不是不需要那个有 runtime 的好,我们计划有几个问题啊第一个问题说IDA说为什么user table不用non-sql呢?这个可能是和之前讨论有关,就是就是这个我觉得虽然是说我个人的preference 但我觉得也是业界的共识就是 Seql 优化了很多东西如果你能用 Seql 的时候任何情况下都应该优先用 Seql 用 non-Seql 会给你造成很多不必要的麻烦除非你有很strong的 reason,比如说这种情况下,这个code可能是个几十k,20k,30k,是吧?就举个例子,20k,30k那你放到这个SQL里面的话,它有Row Size Limit,你可能就不适合放SQL,并且这个Code的读取都是K-Value的读取,是吧,就不适合放SQL,除非你有Strong Racer,否则的话,尽量能用SQL的时候尽量用SQL,特别是这种这种所有的data都不是很大,并且这个well understood question,用SQL可以解决很多很多麻烦。好你可以认为 SQL 是 no-brainer,大部分是 no-brainer,当你用 no-sql 的时候,你需要 justify 你的 choice一般来说但是在这个也是另说的就是在不同公司也不一样,比如说在Uber里面,如果你有MySQL的话,MySQL他那块很明确的说,MySQL当他做这个leader election的时候,他是有availability会有影响,他会有200毫秒的。200毫秒的这个就是on response,所以他availability在那个leader election的时候会有on response time,所以它availability会比其他的service其他database choice的availability的低谢谢大家所以那种情况下的话,你可能说我为了保证我这availability是4个9还是5个9的话,我可能要不用这一口,这也是一个valid point。然后code ID是否应该是code pass,code file pass,这个的话我不建议直接把它存到filesystem里面,就是code不建议直接存到filesystem里面。我们之前就是讨论过就是filesystem说你这个file可能是coopted的,你还是把它存到一个signal里面你或者是用amlone s3 guarantee 你这个data object是不会crafted或者是用DocumentDB,也是guarantee你这个data是boycrafted是不建议把这个就是直接存到 [45:00] 还是用一个ID或者Document Reference,Mongodb叫Document Reference好然后 Mike 说每个 submission 是不是就一个 record 有一个 ID改了重新summit的应该成一个新的吧?对,这个summission是当你summit的时候,当你clicksummit的时候就给你generate一个summissionID这个就变成immutable了,就这个submission是不变了你不小心多click两个,你是两个submission,那你应该有两个submission ID。谢谢大家谢谢大家谢谢大家还有一个说是感觉这个user和其他表没有什么transaction,可以像之前一样做成noSQL。这样 payment 可以做成 fieldType 是 payment array 叫Payment Array,就是用SQL也是可以支持Array的,现在这个SQL也是支持进行Array的,就是不是说SQL不支持Array的,就是SQL的Field也是支持Array的,您可以支持JSON,放个JSON也可以。SQL和NoSQL还是这样,当你用NoSQL的时候,你需要justify your choice,就是说你不管是它 data size 不行啊,还是它有各种各样的原因啊,或者说它这个比如说它 response availability 比较低啊,或者 response 比较慢啊,你需要 justify your choice,但是一般来说,当你没有一个 strong reason use no single 的话,我个人觉得这是我个人的choice,同时我觉得也是基本上我见过所有的就是system design,就是interviewer的一个choice,就是尽量用SQL,因为SQL毕竟还是稳定一些。然后静于说,lead code正在edit code是放在哪里?那里的code被改的频率应该很高你edit code就在browser里面比如说你在这改code的时候你的code是这样子在browser里面你brother就是一个client嘛,当你在这idea的时候,你就是在brother里面。当你只有当你在submit的时候,你会做一个API call,比如说你在这ID的时候,不管是加等什么的,就在brother里面,就是承认的code。谢谢大家然后请问一下Seql为什么在Leader election的时间比较长因为Leader election是要做consensus当你做leader election的时候,我们会在周四的时候讲很多基础的东西。leader election是你要做consensus的时候。然后consensus的话,常有的比如说就是,不管是raft,最近流行的raft,还是之前的paxels,它整个系统复杂度是非常高的。你需要几个coordinate,你需要communicate,所以这个leader election时间是肯定要比较长的。这所以这个Leader election时间是肯定比较长的,它不像一个简单的Database write,它时间可能就几毫秒或者是几毫秒之下。所以我想问一下那user表就没有做replication是吗我们这块说过,你其实可以做Copy吗?就是SlaveCopy,做一份Copy就可以了。所以他们是一步步伏制一步复制线,Asynchronize,因为这个database的盗鸟的概率太低,你Asynchronize复制就可以。谢谢大家谢谢大家基本上所有的 service 都不能只有一个 single node 因为只有一个 single node 当你一个 service 当掉的时候就 unavailable 了这里面的话我们看Stateful ServiceStateForServiceGl SQL StateFolderwork queue 理论上是state fault 但是你实际上submit一个work 你当地也无所谓 你就直接说servererror就行了这里面其实只有secret state fault 然后你的api user token是state fault但你这个token也是可以丢的,当你这个node当鸟,这node的当鸟概率非常非常低,就比如说100天当鸟一次,你当鸟的话你就让user,fals user,relogin就可以了。其实这里面只有SQL是Stateful,你说这个User Service都是Stateless,你就可以十遍加你随便加,比如说你处理不过来了,或者Compute Service处理不过来,可以随便加。好多问题啊今天我们看一下有时候,user多次click的时候,submit的时候会submit太快了,这个fature怎么实践呢?这肯定是在client短时间,你比如说submit了以后加一个那个加一个那个wait time,就是你看一下当前时间,记录一下当前时间,然后你下一次click的时候对比一下上一次click的时间如果这个时间超过几秒钟,你就说click太快了,就在看了一段就可以实践。好然后还有说重新login的时候,正在edit code好像在一周内会restort重新login的时候做的end code,那这种情况下,如果你是这种情况下的话,你的这个,它可能做了一些optimization,就是你的code它可能会Pure Articles save下你的code所以它会有一个API-Code到backend把这个code save下来在看上去做了的就跟静怡说的一样,就是你设计的Code logout 回来以后,还可以看到Edit一半的Code, [50:00] 比如说Gmail传一遍,Gmail传一遍,这是一个User Experience Optimization然后对3MediaCode需要做Serialization吗?还是直接String存进Table?你肯定要做,Serialization是肯定要做的,你从你这块,就是当你transfer你的code从client端到server端的时候,你肯定要做Serialization/Desserialization我觉得你这块应该说是compress吧,你是不是要这个compression可做可不做,如果您可以做一下实验,就是bitcode有data可以做一下实验,说compress的话,它能节省很多space的话,是可以做compression的。但是我觉得它是个CPU-bonded 所以没必要做就是说你加compress和decompress 你是用那个时间换空间嘛,你说白了是减少空间吗?但是它是CPU bonded的话,本来这个CPU就够忙的话,我觉得可能没必要好宣溟 存历史 存 historical submission 的话我觉得用Cassandra做submissionID作为columnKid,然后在后台batch 合并为一起,用 Cassandra 也可以,这个 field 这个 table 用 Cassandra Submission 也可以,我觉得 Cassandra 也是一个 valid choice嗯并且用Cassandra的话,你这个code update的时候,第二次update的时候是不需要modify它的,就是Cassandra是个不错的 choice,确实。你需要想一下你的partition key和sort key,但是当你进行Katandra,就是你第一次存储,你update的时候,你可以只update后面几个column。没必要update这个code就是没必要updatecode不一起update 不帮帮到update是可以的谢谢大家谢谢大家增量编译有没有办法实践?增量编译Liu,你能解释下什么是增量变异吗?谢谢大家谢谢大家中量变异incremental compile我没有听说过Increant Contal Compile的问题,我还是学过一点Compiler的课,也做过一些Compiler的优化,但我没听过Increant Contal Compile。incrementalcompal我没有听说过啊你如果在的话能解释一下啊好,没有submit code在logout,还能看到之前edded一半的code,有可能是在session里面存了,你换个browser就看不到,这也是有可能的。就是它可能是存在后段,有可能是在browser里面就是进了一些响亮的优化,有可能。之前讨论过的comments和discussion是否可以考虑noSQL?在后端的话,请问如何和 SQL 表提供数据给前台,如何考虑 single page load?comments和 discussion 可以看一下之前的 code,就是之前的讨论。在YouTube里面讨论过一个comments和discussion,然后在Design Slack里面也讨论过一个comments和discussion,这里面其实是这个data是有不同的优化过的。我记得在slack里面,我当时propose的是用danymode的,就是这个slack conversation, discussion show DynamoDemYouTube我忘了YouTube是什么,可能是用MySQL吧,但是你用Denymal也可以的,是可以考虑用NoSQL的确实。好,最后的话就是Problem Table了,在Problem Table相对比较简单,你需要一个Problem ID,然后需要一个Creator,Creation time, 有个title, 有个description这个Description 因为它量还是比较大的,其实这个其实挺大的。然后你可能需要另外有需要一个IOID和一个additional data是吧,metadata,比如说有多少个submitted,有多少个accepted,然后你这个submitted,accepted,如果是需要实施的话,你可能还是需要一个kafka和一个和一个flink 啊这样的话你比如说有实时性那种你可能需要这种realtime specular state然后结合一下过去的这个这个可能需要这两个然后这里面Description ID的话,你可能需要支持一些Multimedia,比如说有个Image什么的,你可以把Image存在一些存在我还真不知道他image是哪儿,我直观想想的话可能能存在一个resource里面,比如说AMLON S3这种,或者是存在一个filesystem里面。把 image 成为一个 file system 成为一个 image file谢谢大家但是不管怎么说这个DescriptionID这个descripient是比较大的 和这个code处理方式是一样的input和output file,input和output file,code和problem description在YAM2NetDocumentDatabase,YAM2NetObjectDatabase里面这个应该是就是不要存在这个relational database 里面 因为它毕竟size比较大一点然后它读的那个pattern也比较简单 就是一个read 一个write一个read,一个read,就是根据K来读 [55:00] 有的这个fetch 也比较简单LeadCode就相对来说就设计完了,不是太复杂,大家可以思考一下最后看一下这个问题,然后有什么额外的问题,再给几分钟时间内有什么额外的问题。谢谢大家有个人说Submission是NoSQL表吗?Submission可以用SQL,也可以用NoSQL,NoSQL的话,我觉得刚才有同学说DenimalDB是一个不错的选择,Denimal是一个不错的选择,SQL也可以,SQL的话你只要把code reference 到另一个data storage就可以谢谢大家请问会员过期类似部分需要考虑吗?会员过期的类似部分谢谢大家这个没有什么复杂的吧,就是你比如说你你当你呃Charge 的时候就是当你当你就是你有 payment 的时候你就把这个 expiration date 换成一个时间反正这个表示你自己定的,你随便定一个时间,你可以定的永远不expire。或者定了多久以后X bar然后它这个transaction,payment transaction通过了以后,就set expiration date当你这个XPAR 可能有个 auto charge 是吧, auto refresh 是吧或者是Authersubscription 是吧然后如果是yes的话到时候就再查一遍然后再把你这个ExperationDate然后再refresh一下如果是 no 的话,你到时候可能就是过了这个Membership expiration date的话,你就不是Member了,这个处理相对比较简单。好好好好,今天就讲到这里,然后我会,就是周四的时候吧,周四的时候我会就是征求大家看有谁想感兴趣,可能找两位同学啊,找两位同学,然后下周面试一个比较简单的一个System Design,然后我们就是,尝试一些面试的过程,找两个volunteer然后我会另找时间我看已经有一个volunteer了,我会另早时间,然后就是单独的,我们先就是花半个小时,然后问一个问题,然后讨论一下,然后录两次,然后我们明天的时候就讲这个讲这个就是被interview的这个,看一下实战的,interview的实战啊,就跟打敲拍一样,一边要学理论一边要搞实战嘛,就是这个实战还是很重要的。System Design 实在是很重要的。到时候就是练习一下,我到时候在群里面再问一下,就是优先报名两个人,我看Warrant已经报名了,再找一位同学。好然后周四的话,我想周四讲什么,我给你看一下周四讲什么。中四的帐篷谢谢观看谢谢大家哦,周四不在这里面,周四讲database的存储。我们会讲一下,一个讲一下Btree,讲一个Btree,然后讲一个LogStructureTree。然后再讲一下那个Analytic Database和Transactional Database的不同,还是有一些很有趣的地方的,然后大家感兴趣还过来参加一下。好,今天就到这里了,谢谢大家。谢谢大家谢谢大家。