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