系统设计

2020/12/28 SystemDesign 共 11079 字,约 32 分钟

系统设计- 参考

几本书:

设计数据密集型应用-Data-Intensive Application

https://github.com/donnemartin/interactive-coding-challenges

两周系统设计面试速成大纲

常见分布式系统设计图解

应用部分:

基础设施部分:

主从挂了切主中间数据恢复怎么办?

  • 多主架构,写入时自动找可写入的主写入。多对一。要是冲突就就用冲突检测合并,类似在线文档。
  • 多主架构,写修复,先写到一个备用的主,然后故障的主恢复时,就从备用的主上拉数据进行恢复。

img

读加速

img

答题流程

看看这个先 link

高可用,主要以下几个方面:

  • 数据持久化
  • 主从复制
  • 自动故障恢复
  • 集群化

问面试官问题,即便很清晰也要问,将问题重新定义。

  1. Users: who will use, how the system will be used?
  2. Scale: qps, tps等
  3. Performance: the delay of write-to-read
  4. Cost

需求重新定义:这里不需要问面试官

  • 对于功能模块: 定义api: name以及req, resp

  • 对于非功能需求: 规模、性能、可用性。

数据Model

scalability, performance, availability(灾备、无单点)

consistency, cost

一个大模块就组织出来了。

数据模型,来存储。

各有什么优缺点。

SQL是否可以满足,如何满足的,这块Nosql满足的话成本是不是更少。

处理服务 processing service

画图 有数据流 和 控制流。

时间分配

以40分钟的面试时间来算(掐头去尾除去自我介绍问问题),我面试的大概流程如下。

  • 【3分钟】理解需求 (询问系统的商业目的 + 询问系统的功能和技术需求 + 定义成功)
    • Functional Requirements
      • 用户是否需要登录
      • 仅仅上传图片,不需要上传视频
      • 图片保存有效期
    • Non-functional Requirements
      • 基本可用性
      • 交互流畅、低延迟
      • 是否支持某个动作反馈有一定延迟
  • 【0-5分钟】资源估算(optional)(计算需要多少台机器,需要多少内存硬盘和CPU的能力)
    • Capacity 可能非核心内容加上时间有限,做个大致计算估计的方法。
      • Traffic
        • qps xx times per day, xx times per second
        • tps, 按读写比10:1
        • peak qps: qps * 5
      • Storage
        • add data xx per day, xx per year
        • cache xx..
  • 【5分钟】接口设计

  • 【5分钟】数据结构与存储

  • 【5分钟】High-level diagram

    • 划分模块
  • 【10分钟】核心子服务设计

  • 【5分钟】扩展性,容错性,延迟要求

    • 架构扩展性:

      • 关注: 存储数据量, 存储吞吐量; 解决方案:存储拆分、单体吞吐低可以batch,多级缓存。
      • 服务: 吞吐压力: 先尝试扩容,无状态服务。否则降级: 保障核心服务,关闭弱依赖。
      • 伸缩性: 无状态,可横向扩容
      • 高峰期流量过高: mq削峰
    • 业务可扩展性

      • 搞多app矩阵的支持,增加namespace, appId

      • 类型增加,增加ItemType

    • 容错性

      • 数据库挂了怎么办
        • 主从灾备
      • 接口挂了怎么办
        • 失败重试,缓存队列
        • 防止雪崩,要熔断
      • 降级、熔断
      • 异步
        • 消息队列 削锋
  • 【2-7分钟】专题 deep dive
    • 用户scope变大
      • 多机房同步
      • 多语言
    • 某个时间点流量特别大了怎么办
      • 降级、熔断
      • 异步
    • 缓存
      • 大V设计
  1. User case. Func, Non-Func
  2. Scale: QPS, 多少条数据每天
  3. High level
  4. 可用性、扩展性

系统设计时间和性能估算

参考

1天86400s

每个字符占用 2 个字节

bit 是位,1 byte == 8 bits

B=byte, 1M = 1024byte

读写比默认10:1

数据类型的空间占用: 在很多系统中,Boolean 占用 1 个字节,字符占用 2 个字节,Integer/Float 是 4 个字节,Long/Double 则是 8 个字节。

时间数量级

Jeff Dean 十年前有一个著名的分享,介绍了他认为重要的系统的数值。我觉在讨论多数系统来说,有这样几个关于时间的数值(参考数量级)比较常见(注意时间单位的关系:1 秒 = 1000 毫秒 (millisecond) = 1,000,000 微秒 (microsecond) = 1,000,000,000(nanosecond)):

  • CPU 访问(包括 CPU 缓存):10 纳秒
  • 内存访问:100 纳秒
  • HDD 磁盘访问:10 毫秒,如果是 SSD 大约快 100 倍
  • HDD 磁盘吞吐量:100 MB/s,如果是 SSD 则高几倍
  • 同机房网络时延:1 毫秒
  • 异地网络时延:10 毫秒
  • 国际网络时延:100 毫秒

单机吞吐量上限

  • Web 服务器的 QPS:1000
  • RDB 单机 QPS:1000
  • NoSQL DB 磁盘单机 QPS:10K
  • 内存访问单机 QPS:1M

并发估算

一般应用来说,并发估算公式如下:

qps = 5 * 日pv / 86400

5是通用峰值倍数,如果你有高峰值特性自行调整,比如秒杀功能。

pv是访问次数,比如你每天数据库访问次数是100万,可估算峰值qps为60左右,比如数据库qps可达3000,那么可估算你的服务器还能撑同等场景50倍增长。

本质上就是访问次数除以时间,这就是qps每条访问次数的定义了。

1、抢购和活动场景

按照80%的访问量集中在20%的时间内

qps=pv*80%/86400*20%

2、白天提供业务的场景(例如政务查询系统等)

按照一半时间去计算4w秒

qps=pv/40000

3、全天后无明显波峰场景

qps=pv/86400

存储延迟和性能

以下可用性都在99.95%左右

redis 2ms 百万qps

elastic search 简单查询20-30ms, 复杂几t的可能秒级

rocksDB 10ms

mysql 20ms 千万以上基本就要分库分表了。读支持50w qps, 写大概在几千

mongo 20ms

MQ 性能

rocketmq 几十万tps

kafka 百万 tps

存储考虑点, 对数据库的需求有:

需求是否必须
低延迟必须
支持CP模型必须
支持非结构化数据存储必须
有亿级数据的存储方案必须
有成熟的扩容方案必须
冷热数据非必须

系统设计常用算法

参考

  • Bloom filter
  • count-min sketch bloom filter基础上增加计数,以最小那个hash计数为值
  • Frugal Streaming
    • Frugal Streaming uses only one unit of memory per group to compute a quantile for each group
  • Geohash / S2 Geometry 地理位置
  • Leaky bucket / Token bucket 限流器
  • Lossy Counting ,top k频繁 hashmap不停轮换清counter为0的
  • Operational transformation 操作转换,类似于google doc多人编辑时
  • Quadtree / Rtree ☑️
    • 四叉树是一种数据结构,每一个节点有四个孩子。一般需要用到四叉树的情况往往是二位平面,通过把区域分成四个区块来定义。
    • 应用:比如确定并显示一条曲线的具体位置时
    • R树 vs Geohash
  • Ray casting ☑️
  • Reverse index ✅
  • Rsync algorithm ✅
  • Trie algorithm ✅

二维地址查询

  • 一种是使用 QuadTree,就是说把地图上任意一个区域都划分成四个子区域,每个区域如果节点超过一个阈值,就继续划分。
  • 第二种是使用 Geohash,本质上就是降维。降维的原因是,一维的数据管理和查找起来要容易得多,二维的数据要做到高效查找比较困难。我们的查找条件是基于经纬度的,而不是一个单值;我们存储的数据也都是一个个经纬度形成的点,因此,Geohash 的办法就是把查找条件和存储的数据全部都变成一个个单值,这样就可以利用我们熟悉的一维数组区域查找的技术来高效实现(比如把它索引化,而索引化其实是可以通过 B+树来实现的,因此 Geohash 的查询时间复杂度和 QuadTree 是可以在同一个数量级的,都是 log n)。

Geohash 是一种空间数据编码方法,将地理坐标(经度、纬度)转换为一个短字符串,通常是字母和数字的组合。它的基本原理是将地球表面分割成网格,并将每个网格用一个唯一的标识符表示。随着精度的提高,Geohash 编码的长度会增加。

R 树 是一种自适应的多维空间索引树结构,特别适用于二维或多维数据的快速查询。每个节点在 R 树中都包含了一个最小的边界矩形(Bounding Box,简称 MBR),这些矩形覆盖了树中的对象。R 树的每个分支表示一组空间数据的最小边界,可以通过逐级遍历来进行范围查询或邻近查询。

  • Geohash 适合用于地理位置的简单编码和索引,主要通过编码简化了地理数据的存储和查询。
  • R 树 是更为复杂的空间索引结构,适用于需要高效处理多维空间数据和复杂空间查询的场景。它能更好地处理空间对象的重叠和查询效率。

两者可以结合使用,比如将 Geohash 用于初步的地理区域划分,再用 R 树处理更细粒度的空间数据。

R树需要数据库支持,如果没有相应存储就还是Geohash吧。

Bloom filter

Bloom Filter 是由一个长度为 m 的比特位数组(bit array)与 k 个哈希函数(hash function)组成的数据结构。位数组均初始化为 0,所有哈希函数都可以分别把输入数据尽量均匀地散列。

当要插入一个元素时,将其数据分别输入 k 个哈希函数,产生 k 个哈希值。以哈希值作为位数组中的下标,将所有 k 个对应的比特置为 1。

当要查询(即判断是否存在)一个元素时,同样将其数据输入哈希函数,然后检查对应的 k 个比特。如果有任意一个比特为 0,表明该元素一定不在集合中。如果所有比特均为 1,表明该元素有(较大的)可能性在集合中。为什么不是一定在集合中呢?因为一个比特被置为 1 有可能会受到其他元素的影响,这就是所谓“假阳性”(false positive)。相对地,“假阴性”(false negative)在 Bloom Filter 中是绝不会出现的。

优点:

  • 不需要存储数据本身,只用比特表示,因此空间占用相对于传统方式有巨大的优势,并且能够保密数据;
  • 时间效率也较高,插入和查询的时间复杂度均为O(k);
  • 哈希函数之间相互独立,可以在硬件指令层面并行计算。

缺点:

  • 存在假阳性的概率,不适用于任何要求 100% 准确率的场景;
  • 只能插入和查询元素,不能删除元素,这与产生假阳性的原因是相同的。我们可以简单地想到通过计数(即将一个比特扩展为计数值)来记录元素数,但仍然无法保证删除的元素一定在集合中。

所以,Bloom Filter 在对查准度要求没有那么苛刻,而对时间、空间效率要求较高的场合非常合适,本文第一句话提到的用途即属于此类。另外,由于它不存在 假阴性 问题,所以用作“不存在”逻辑的处理时有奇效,比如可以用来作为 缓存系统(如Redis)的缓冲,防止缓存穿透。

Lossy Counting Method

算法本身的作用是找出长度为 N 的数据流中出现频率超过 s % 的元素,保证误差小于 a %。其中 s 与 a 是传入的参数,a 一般设定为 s 的十分之一。此算法从数学上保证:

  1. 在数据流中,出现频率高于 s * N 的元素最后都会输出。
  2. 在数据流中,如果出现频率低于 ( s - a ) * N 的元素不会被输出。
  3. 估算的出现次数与实际次数的差距不会高于 a * N。

算法实现起来也相对简单,只有三步:

  1. 把现有的数据分成 N 个窗口,每个窗口的大小为 1 / a。
  2. 按顺序统计每一个窗口里面元素出现的个数并减去 1。
  3. 把剩下的值加起来并且返回所有大于 (s-a) * N 的值。

优点:

  • 容易实现以及修改,能够控制元素频率以及误差。
  • 快速高效,建立哈希表 V 只需要遍历一遍数据即可。
  • 这个算法也适用于“如何设计一个显示最热门的 100个 标签的系统“这类场景。

缺点:

  • 这个算法适用于玩家分布比较集中的情况,也就是说有很多玩家的分数是相同的。如果玩家的分数都不同,或者分布零散的话,哈希表 V 需要内存维护大量元素,会退化成桶排序。
  • FindRank需要在哈希表 V 中遍历所有比该分数大的桶。

Frugal Streaming

Geohash / S2 Geometry 地理位置

Leaky bucket / Token bucket 限流器

Lossy Counting ,tok频繁 hashmap不停轮换请counter为0的

Operational transformation 操作转换,类似于google doc多人编辑时

Quadtree / Rtree ☑️

  • 四叉树是一种数据结构,每一个节点有四个孩子。一般需要用到四叉树的情况往往是二位平面,通过把区域分成四个区块来定义。
  • 应用:比如确定并显示一条曲线的具体位置时

  • Ray casting ☑️

  • Reverse index ✅

  • Rsync algorithm ✅

  • Trie algorithm ✅

常见题目

系统设计参考1 Done

系统设计参考2 差不多,图片都失效了。

分布式ID生成器

发号器产生的 ID 一般都是 64 位整数,这样对数据库比较友好,容量也能满足业务需求

核心需求

  • 全局唯一(unique)
  • 按照时间粗略有序(sortable by time)
  • 尽可能短

雪花算法:

  • 4个字节表示的Unix timestamp,
  • 3个字节表示的机器的ID
  • 2个字节表示的进程ID
  • 3个字节表示的计数器

时间在前面可保证有序

使用mysql自增id, 比如8台机器,每个机器每次+8。

不用进程ID就可以保证单调递增,但依赖时间。 雪花算法几乎可以是非常完美了,但它有一个致命的缺点 —— 强依赖机器时间。 如果机器上的系统时间回拨,即时间较正常的时间慢,那么就可能会出现发号重复的情况。对于这种情况,我们可以在本地维护一个文件,写入上次的时间戳,随后与当前时间戳比较。如果当前时间戳小于上次时间戳,说明系统时间出了问题,应该及时处理。

  • 比如 让程序等待一段时间(如等待到 last_timestamp 对应的时间后),保证生成的 ID 是单调递增的。
  • 维护一个逻辑时间(logical_timestamp),如果检测到时间回拨,则使用 last_timestamp + 1 作为时间戳,以避免回拨带来的影响。

前面加namespace, appId这样区分,分业务。

  • 时间戳可以变成HLC时间戳,它是物理时钟 + 逻辑时钟,同时定时ping pong取最大值,每个物理时钟的逻辑时钟重新加一。
  • 提前缓存生成的id到本地内存。

其它实现: mysql自增主键, redis自增。

短网址系统

需求

  • 足够短

网上链接大概45亿,长度7的字符串足够了,每个元素是大小写字母+数字

用分布式id生成器生成短链,用Nosql kv来存, 短网址-> 长网址

使用302临时重定向。

id生成器是数字,再变成62进制这样变成字符串。

信息流

专门blog

定时任务调度器

参考个人blog

API限速

参考个人blog: 限流器

设计线程安全的hashmap

hashmap可以拉链法,也可以java8一样,不是拉链,而是个红黑树,解决了hash冲突后的问题。

可以像concurrentHashMap一样采用分段锁保护。

实时输出最近一个小时内访问频率最高的10个IP

  1. 3600s,3600个hashmap,存储每一秒的每个ip的访问次数。

  2. 同时还要新建一个固定大小为10的小根堆,用于存放当前出现次数最大的10个IP。堆顶是10个IP里频率最小的IP。

  3. 每次来一个请求,就把该秒对应的HashMap里对应的IP计数器增1,并查询该IP是否已经在堆中存在,

  • 如果不存在,则把该IP在3600个HashMap的计数器加起来,与堆顶IP的出现次数进行比较,如果大于堆顶元素,则替换掉堆顶元素,如果小于,则什么也不做
  • 如果已经存在,则把堆中该IP的计数器也增1,并调整堆
  1. 需要有一个后台常驻线程,每过一秒,把最旧的那个HashMap销毁,并为当前这一秒新建一个HashMap,这样维持一个一小时的窗口。

  2. 每次查询top 10的IP地址时,把堆里10个IP地址返回来即可。

如果内部存不下,可以使用redis等,同时时间跨度长的话还可以桶塌缩,近似统计。

设计一个负载均衡

负载均衡策略需要将请求均匀分配到各个服务节点,避免出现请求集中在某一点上的情况。有时会考虑节点权重,会话粘连等需求

参考

服务端节点列表存储下来,定时探活。

  • 随机:数组存储列表,随机数字。

  • 无权重轮询:移动cursor,数组

  • 有权重轮询:移动cursor,数组。权重分别为1,2,3的3该节点A,B,C,会先请求A一次,再请求B两次,再请求C三次

  • 有权重的平滑权重轮询

    • 所谓平滑, 即在一定的时间内, 不仅服务器被选择的次数分布和权重一致,满足权重要求,且调度算法还能比较均匀的选择节点分配请求

    • totalWeight:保存所有节点的权重和,该值在后续流程中保持不变

      nodeOriginWeight:保持每个节点的原始权重,在后续流程中也保持不变

      nodeCurWeights:保存每个节点的当前权重,该数组在后续每次计算请求应该分配到哪个节点时都会发生变化,初始化为每个节点的权重

      每次选择节点,都会执行以下3步

      • 选出当前权重中,值最大节点a
      • 将a的当前权重值减去totalWeight
      • 将每个当前权重加上每个节点的原始权重
    • 原理:若某个节点增长越快,则越有概率被选中,而增长的速度和权重大小成正比,因此节点权重越大,越有概率被选中。相反或节点权重越小,增长成为最大当前权重节点的速度越慢,被选中的概率较低,从而达到按权重分配请求的效果

      当每个节点被选中后,减去的值都相等,由于减去了一个较大的值(所有节点原始权重总和),使得该节点在下几次请求中,被选中的概率较低,因为恢复成为最大值需要时间。从而达到平滑的效果

  • 最小活跃数:按活跃数排序,可以是avl树、跳表等。每次请求+1

  • 一致性hash:参考个人blog,如数组二分查找、红黑树等,虚拟节点,hash个数字。

配置中心

配置中心的核心作用是 存储和管理系统的配置信息,并 确保所有服务获取到的配置数据是正确的。如果配置数据不一致,可能会导致 系统故障、服务异常、甚至宕机,因此 一致性比可用性更重要。 如果选择 AP,可能在分区期间读取到旧数据,导致配置回滚或者错误行为。

配置信息存储之后,需要考虑如何将配置的变更推送给服务端,这样就可以实现配置的动态变更,也就是说不需要重启服务器就能让配置生效了。

一般会有两种思路来实现变更推送:一种是轮询查询的方式;一种是长连推送的方式。

比较是否变化,可以比较MD5值,来降低全部拉取的带宽压力。 由于配置中心里存储的配置项变化的几率不大,所以使用这种方式后,每次轮询请求就只是返回一个MD5值,可以大大地减少配置中心服务器的带宽。

另一种长连的方式,它的逻辑是在配置中心服务端保存每个连接关注的配置项列表。这样,当配置中心感知到配置变化后,就可以通过这个连接,把变更的配置推送给客户端。这种方式需要保持长连,也需要保存连接和配置的对应关系,实现上要比轮询的方式复杂一些,但是相比轮询方式来说,能够更加实时地获取配置变更的消息。

配置服务中存储的配置变更频率不高,所以对于实时性要求不高,但是期望实现上能够足够简单,所以如果选择自研配置中心的话,可以考虑使用轮询的方式。

可用性:

  • 配置存储是分级的,有公共配置,有个性的配置,一般个性配置会覆盖公共配置,这样可以减少存储配置项的数量;
  • 配置中心可以提供配置变更通知的功能,可以实现配置的热更新;
  • 配置中心关注的性能指标中,可用性的优先级是高于性能的,一般我们会要求配置中心的可用性达到99.999%,甚至会是99.9999%。
  • 在配置中心的客户端上,增加两级缓存:第一级缓存是内存的缓存;另外一级缓存是文件的缓存。

Metrics 监控系统

使用时序数据库存储数据。

img

  • Metrics Source 指标来源,应用服务,数据库,消息队列等。
  • Metrics Collector 指标收集器。
  • Time series DB 时序数据库,存储指标数据。
  • Query Service 查询服务,向外提供指标查询接口。
  • Alerting System 告警系统,检测到异常时,发送告警通知。
  • Visualization System 可视化,以图表的形式展示指标。

Flink 适用于任何需要处理实时、大规模数据流的应用,特别是在对低延迟和高吞吐量要求较高的场景下。常见的应用领域包括金融、互联网、物联网、智能城市、企业监控等。

下采样是把高分辨率的数据转换为低分辨率的过程,这样可以减少磁盘使用。由于我们的数据保留期是1年,我们可以对旧数据进行下采样,这是一个例子:

  • 7天数据,不进行采样。
  • 30天数据,下采样到1分钟的分辨率
  • 1年数据,下采样到1小时的分辨率。

img

image-20250222223351280

ELK海量日志收集

参考

ELK 是三个开源软件的缩写,分别表示:Elasticsearch、Logstash、Kibana,其中还没有提到轻量级的日志收集工具 Filebeat。以下是这几个组件的简介:

  • Filebeat,轻量级的日志收集处理工具
  • Logstash,主要是用来日志的搜集、分析、过滤日志的工具,支持大量的数据获取方式。一般工作方式为 c/s 架构,client 端安装在需要收集日志的主机上(这里用 Filebeat 代替),server 端负责将收到的各节点日志进行过滤、修改等操作存储到 ES 或者其他存储系统上。
  • Elasticsearch,是个开源分布式搜索引擎,提供搜集、分析、存储数据三大功能。
  • Kibana,提供的日志分析友好的 Web 界面,可以帮助汇总、分析和搜索重要数据日志。

权限系统设计

  • 认证 (Authentication): 你是谁。
  • 授权 (Authorization): 你有权限干什么。

系统权限控制最常采用的访问控制模型就是 RBAC 模型 。

什么是 RBAC 呢? RBAC 即基于角色的权限访问控制(Role-Based Access Control)。这是一种通过角色关联权限,角色同时又关联用户的授权的方式。

简单地说:一个用户可以拥有若干角色,每一个角色又可以被分配若干权限,这样就构造成“用户-角色-权限” 的授权模型。在这种模型中,用户与角色、角色与权限之间构成了多对多的关系。

img

  • 用户表
  • 角色表
  • 菜单表

设计文件系统

系统核心功能:上传文件和下载文件

背景知识:

  1. 一个文件的数据主要由两部分组成:元数据和具体的文件数据
    • 元数据描述了文件的基本信息,如文件名称,大小,格式.
    • 元数据的访问频率远高于具体的文件数据,我们会查看文件夹里的内容,但不会每个文件都打开,因此元数据和文件数据一般是分开存储的
  2. one master + chunk servers
  3. master 存储文件元信息和文件chunk所在服务器信息
  4. chunkserver存储具体的文件chunk

image.png

image.png

上传: 客户端需要先将大文件分片

  1. 客户端发送文件名(文件ID)和chunk编码
  2. 服务端返回该chunk应该写在哪个哪台服务器上
  3. 客户端传上传具体的分片数据
  4. 返回写入成功

订单撮合系统

分为交易控制(包含订单校验),撮合和清算,分别做成三个微服务。

撮合不涉及账户余额与持仓, 那是交易系统或者清算该考虑的事。 清算偏离线计算

首先放到内存里,崩的场景非常小,其次如果崩了,不可能没有主从、双主、也可以保证无缝切换,这样也能保证事务,重启的时候,就像redis的aof,肯定有操作日志的保存的,再执行一遍就行了

撮合模块,我们的撮合和回报流程选用了netty websocket进行推送,分布式异步无阻塞处理,内存撮合,写操作采用交易日志同步写,数据库持久化采用异步线程操作的方式,总体性能还可以,吞吐量可以达到每秒2W笔左右,单笔延迟100ms左右

对于撮合交易系统,交易吞吐量和延时永远都是最核心的指标

img

评论系统设计

评论可能会盖楼,分为一层、两层

评论上记录item_id 和parent_id。parent_id 不为空就是二层的。

按照时间排序。每个评论读时过滤可见性。

设计Key-Value存储引擎

参考个人blog: leveldb设计及实现

大数据

参考个人blog算法系列大数据问题

对账

参考

对账-对比逻辑

对账的核心即为两边数据的对比,因此如上所示会存在4种比对情况。

我方有银行无、银行有我方无、双方有但金额不一致、双方有且金额一致。

火车票订票系统

在这里插入图片描述

Search

    Table of Contents