system-design-interview笔记
限流器
-
放在哪里?
- 客户端, 容易被伪造绕过
- 服务端应用代码, 灵活性高, 但占据工程资源
- 云上微服务, api网关等, 灵活性低
-
经典算法:
-
令牌桶: 固定速率产生令牌, 每个请求消耗令牌
- pros: 容易实现, 内存占用少, 允许突发流量
- cons: 调参困难, 可能需要不断填充桶
-
漏桶: 请求进入一个定长FIFO队列, server给定速度从队列取数据处理
- pros: 容易实现, 内存占用少, 请求以固定速率处理
- cons: 突发请求占据队列使得新请求得不到处理, 调参困难,可能需要不断填充桶
-
固定窗口计数器: 每一个给定时间窗口进行计数,例如每分钟100个请求
- pros: 容易实现, 内存占用少
- cons: 突发请求可以达到两倍限制的QPS(在窗口边缘有突刺)
-
滑动窗口日志: 记录请求时间戳, 数据保存在redis等缓存,每当新请求进来的时候把过时时间戳删除, 如果请求时间 戳比时间窗口的最低值还低, 拒绝请求
- pros: 速率限制准确
- cons: 内存开销大, 需要存储多个时间戳
-
滑动窗口计数器: 某时刻限制请求数 = 窗口上限 - 上一个窗口请求数 * 这一个窗口的和上一个窗口的重叠百分比。例如, 窗口为每分钟1000, 上一分钟800, 在这一分钟的30秒时, 限制这一分钟窗口的请求数最多为 1000 - 30 / 60 * 800 = 600
-
相当于假设上一个窗口的平均速率
-
pros: 平滑了流量峰值, 内存高效(只需要计数)
-
cons: 不太严格, 然而其实可以
然而,这个问题可能并不像它看起来那么糟糕。 根据Cloudflare[10]所做的实验,在4亿个请求中,只有0.003%的请求被错误地允许或限制速率
-
-
超过速率限制
如果一个请求被限制了速率,API会向客户端返回一个HTTP响应代码429(请求太多)。根据不同的使用情况,我们可能会将速率受限的请求排队等候以后处理。 例如,如果一些订单由于系统过载而受到速率限制,我们可以保留这些订单以便以后处理。
限流器请求头
一个客户如何知道它是否被节流?客户端如何知道在被节流之前允许的剩余请求的数量?答案就在HTTP响应头中。限流器向客户端返回以下 HTTP 标头:
- X-Ratelimit-Remaining:窗口内允许请求的剩余数量
- X-Ratelimit-limit:它表示客户端在每个时间窗口可以进行多少次调用
- X-Ratelimit-Retry-After:等待的秒数,直到你可以再次提出请求而不被节流。
当用户发送过多请求时,将向客户端返回 429 too many requests 错误和 X-Ratelimit-Retry-After 标头。
- 规则被存储在磁盘上。工作者经常从磁盘 中提取规则,并将其存储在高速缓存中。
- 当客户端向服务器发送请求时,该请求首先被发送到限流中间件。
- 限流中间件从缓存中加载规则。它从Redis缓存中获取计数器和最后一次请求的时间戳。根据响应,限流器决定:
- 如果请求没有速率限制,它将被转发到API服务器。
- 如果请求受到速率限制,限流器会向客户端返回 429 too many requests 错误。 同时,请求被丢弃或转发到队列。
分布式限流
redis:
- lua脚本
- sorted set每个用户有一个自己的set,试图执行动作时,使用MULTI原子地执行下列操作:
ZREMRANGEBYSCORE
删除给定时间间隔前的元素, ZADD 添加当前时间戳, ttl设置为rate limit,计算sorted set的数量,如果超过限额,就失败 (滑动窗口日志)(有一说一我觉得这个如果没有精确的需求不如滑动窗口计数器, 开销感觉有点大)
多限流器同步: 用户hash redis集群分配之类, 尽量别做这种事情
其他层级的限流:
- iptables
- ...
一致性hash
基本步骤如下:
- 使用均匀分布的哈希函数将服务器和键映射到环上。
- 要想知道一个键被映射到哪个服务器,从键的位置顺时针查找,直到找到环上的第一个服务器。
步骤是很简单的, 麻烦的是问题
环上服务器分区大小难以保证相同(添加删除服务器)
解决方法: 虚拟节点(分成更小的块)
每个服务器动态地分配小块(虚拟节点), 这样还可以考虑服务器容量自动缩放问题
一致性模型
N = 副本数
W = 大小为 W 的规定写入。要将写入操作视为成功,必须从 W 个副本确认写入操作。
R = 大小为 R 的读取规定人数。为了使读取操作被认为是成功的,读取操作必须等待至少R个副本的响应。
如何配置N、W和R以适应我们的使用情况?
下面是一些可能的设置:
- 如果
R=1,W=N
,系统被优化为快速读取- 如果
W=1,R=N
,系统被优化为快速写入- 如果
W+R>N
,就可以保证强一致性(通常N=3,W=R=2
)。- 如果
W+R<=N
,则不能保证强一致性
复制->
高可用->
不一致
发现并解决冲突: 常用是向量时钟 vector clock, 但有几个问题
- 处理冲突逻辑复杂
- 动态增加删除服务器逻辑复杂
参考:
实际业界的用例参考Dynamo
算法简介:在每个服务器内部维护一个所有服务器的vector
- 当自己发生事件时,增加
vector[self]
, 并告诉需要告诉的服务器(核心就在于不广播, 没有全局时间!当然也因此不保证解决冲突) - 当自己收到A server的事件时,
vector[A] += d
最后想要觉得一个事件的全局顺序的时候, 查看所有的server的vec
如果有一个server的vec是最大的,那么严格有因果关系,取它的值就行
但如果没有最大的vec, 就需要解决冲突的策略
策略
- 交给客户端, 如dynamo
- 加上时间戳,
vec' = [...servers, timestamp]
冲突取ts最大的 - 随机取一个
所以向量时钟算法的实质是:
(1)将逻辑上可以合并的冲突成功合并;
(2)逻辑上无法合并的冲突依旧冲突;
拓展: 向量时钟的剪枝, riak, 牺牲绝对的正确性(false merge)来换取对太长的vec clock的剪枝
gossip 协议 quorom
唯一ID生成器
-
多主复制: 下一个id += k, k是服务器数量,拓展性差
-
uuid
-
引自维基百科,"在每秒产生10亿个UUIDs,大约100年后,创造一个重复的概率会达到50%"。
-
优点:
-
生成 UUID 很简单。服务器之间不需要协调,因此不会有任何同步问题。
-
该系统易于扩展,因为每个 Web 服务器负责生成它们使用的 ID。 ID 生成器可以轻松地与 Web 服务器一起扩展。
-
-
缺点:
- ID 是 128 位长, 空间开销。
- ID 不会随时间上升
- ID 可以是非数字的
-
-
ticket服务器, 在分布式系统之中维持一个唯一的ticket server用于签发id
-
**优点:**数字 ID,易于实施,适用于中小型应用程序
-
**缺点:**单点故障
-
-
twitter雪花算法
- 符号位:1 位,它将始终为 0。这是为将来使用 保留的。它可以潜在地用于区分有符号数和无符号数。
- 时间戳:41 位。自纪元或自定义纪元以来的毫秒数。我们使用 Twitter 雪花默认纪元 1288834974657,相当于 2010 年 11 月 4 日 01:42:54 UTC。
- 数据中心 ID:5 位,这给了我们 25=3225=32 个数据中心。
- 机器 ID:5 位,每个数据中心有 25=3225=32 台机器
- 序列号:12 位。对于在该机器/进程上生成的每个 ID,序列号都会递增 1。该数字每毫秒重置为 0。
也就是说雪花在一台机器上1毫秒可以支持4096个新id
额外要点:
- 时钟同步。在我们的设计中,我们假设 ID 生成服务器具有相同的时钟。当服务器在多个内核上运行时,此假设可能不成立。同样的挑战存在于多机场景中。时钟同步的解决方案超出了本书的范围;但是,了解问题的存在很重要。网络时间协议是这个问题最流行的解决方案。
- 节段长度调整。例如,较少的序列号但较多的时间戳位对低并发性和长期应用是有效的。
- 高可用性。由于 ID 生成器是关键任务系统,因此它必须具有高可用性
拓展阅读: 网络时间协议
短URL
点击较短的别名重定向到原始url
-
URL缩短:给定一个长的URL => 返回一个短得多的URL
-
URL重定向:给定一个短的URL => 重定向到原来的URL
-
高可用性、可扩展性和容错考虑
值得在这里讨论的一件事是 301 重定向与 302 重定向。
301重定向