0%

Spark

Spark 简介

Spark 概念

  • 大规模分布式通用计算引擎
    • Spark Core:核心计算框架
    • Spark SQL:结构化数据查询
    • Spark Streaming:实时流处理
    • Spark MLib:机器学习
    • Spark GraphX:图计算
  • 具有高吞吐、低延时、通用易扩展、高容错等特点
  • 采用 Scala 语言开发
  • 提供多种运行模式

Spark 特点

  • 计算高效
    • 利用内存计算、Cache 缓存机制,支持迭代计算和数据共享,减少数据读取的 IO 开销
    • 利用 DAG 引擎,减少中间计算结果写入 HDFS 的开销
    • 利用多线程池模型,减少任务启动开销,避免 Shuffle 中不必要的排序和磁盘 IO 操作
  • 通用易用
    • 适用于批处理、流处理、交互式计算、机器学习算法等场景
    • 提供了丰富的开发 API,支持 Scala、Java、Python、R 等
  • 运行模式多样
    • Local 模式
    • Standalone 模式
    • YARN/Mesos 模式
  • 计算高效
    • 利用内存计算、Cache 缓存机制,支持迭代计算和数据共享,减少数据读取的 IO 开销
    • 利用 DAG 引擎,减少中间计算结果写入 HDFS 的开销
    • 利用多线程池模型,减少任务启动开销,避免 Shuffle 中不必要的排序和磁盘 IO 操作
  • 通用易用
    • 适用于批处理、流处理、交互式计算、机器学习等场景
    • 提供了丰富的开发 API,支持 Scala、Java、Python、R 等

Spark 原理

编程模型

RDD

  • 弹性分布式数据集(Resilient Distributed Datesets)
    • 分布在集群中的只读对象集合
    • 由多个 Partition 组成
    • 通过转换操作构造
    • 失效后自动重构(弹性)
    • 存储在内存或磁盘中
  • Spark 基于 RDD 进行计算

RDD 操作(Operator)

  • Transformation(转换)
    • 将 Scala 集合或 Hadoop 输入数据构造成一个新 RDD
    • 通过已有的 RDD 产生新 RDD
    • 惰性执行:只记录转换关系,不触发计算
    • 例如:map、filter、flatmap、union、distinct、sortbykey
  • Action(动作)
    • 通过 RDD 计算得到一个值或一组值
    • 真正触发计算
    • 例如:first、count、collect、foreach、saveAsTextFile

RDD 依赖(Dependency)

  • 窄依赖(Narrow Dependency)
    • 父 RDD 中的分区最多只能被一个子 RDD 的一个分区使用
    • 子 RDD 如果有部分分区数据丢失或损坏,只需从对应的父 RDD 重新计算恢复
    • 例如:map、filter、union
  • 宽依赖(Shuffle/Wide Dependency )
    • 子 RDD 分区依赖父 RDD 的所有分区
    • 子 RDD 如果部分或全部分区数据丢失或损坏,必须从所有父 RDD 分区重新计算
    • 相对于窄依赖,宽依赖付出的代价要高很多,尽量避免使用
    • 例如:groupByKey、reduceByKey、sortByKey

YARN

YARN 的目标是解决 MapReduce 的缺陷。

MapReduce 的缺陷(Hadoop 1.x)

  • 身兼两职:计算框架 + 资源管理框架
  • JobTracker
    • 既做资源管理,又做任务调度
    • 任务太重,开销过大
    • 存在单点故障
  • 资源描述模型过于简单,资源利用率较低
    • 仅把 Task 数量看作资源,没有考虑 CPU 和内存
    • 强制把资源分成 Map Task Slot 和 Reduce Task Slot
  • 扩展性较差,集群规模上限 4K
  • 源码难于理解,升级维护困难

YARN 简介

YARN(Yet Another Resource Negotiator,另一种资源管理器)是一个分布式通用资源管理系统

设计目标:聚焦资源管理、通用(适用各种计算框架)、高可用、高扩展。

YARN 系统架构

  • 主从结构(master/slave)
  • 将 JobTracker 的资源管理、任务调度功能分离
  • 三种角色:
    • ResourceManager(Master) - 集群资源的统一管理和分配
    • NodeManager(Slave) - 管理节点资源,以及容器的生命周期
    • ApplicationMaster(新角色) - 管理应用程序实例,包括任务调度和资源申请

ResourceManager(RM)

主要功能

  • 统一管理集群的所有资源
  • 将资源按照一定策略分配给各个应用(ApplicationMaster)
  • 接收 NodeManager 的资源上报信息

核心组件

  • 用户交互服务(User Service)
  • NodeManager 管理
  • ApplicationMaster 管理
  • Application 管理
  • 安全管理
  • 资源管理

NodeManager(NM)

主要功能

  • 管理单个节点的资源
  • 向 ResourceManager 汇报节点资源使用情况
  • 管理 Container 的生命周期

核心组件

  • NodeStatusUpdater
  • ContainerManager
  • ContainerExecutor
  • NodeHealthCheckerService
  • Security
  • WebServer

ApplicationMaster(AM)

主要功能

  • 管理应用程序实例
  • 向 ResourceManager 申请任务执行所需的资源
  • 任务调度和监管

实现方式

  • 需要为每个应用开发一个 AM 组件
  • YARN 提供 MapReduce 的 ApplicationMaster 实现
  • 采用基于事件驱动的异步编程模型,由中央事件调度器统一管理所有事件
  • 每种组件都是一种事件处理器,在中央事件调度器中注册

Container

  • 概念:Container 封装了节点上进程的相关资源,是 YARN 中资源的抽象
  • 分类:运行 ApplicationMaster 的 Container 、运行应用任务的 Container

YARN 高可用

ResourceManager 高可用

  • 1 个 Active RM、多个 Standby RM
  • 宕机后自动实现主备切换
  • ZooKeeper 的核心作用
    • Active 节点选举
    • 恢复 Active RM 的原有状态信息
  • 重启 AM,杀死所有运行中的 Container
  • 切换方式:手动、自动

YARN 资源调度策略

FIFO Scheduler(先进先出调度器)

调度策略

将所有任务放入一个队列,先进队列的先获得资源,排在后面的任务只有等待

缺点

  • 资源利用率低,无法交叉运行任务
  • 灵活性差,如:紧急任务无法插队,耗时长的任务拖慢耗时短的任务

Capacity Scheduler(容量调度器)

核心思想 - 提前做预算,在预算指导下分享集群资源。

调度策略

  • 集群资源由多个队列分享
  • 每个队列都要预设资源分配的比例(提前做预算)
  • 空闲资源优先分配给“实际资源/预算资源”比值最低的队列
  • 队列内部采用 FIFO 调度策略

特点

  • 层次化的队列设计:子队列可使用父队列资源
  • 容量保证:每个队列都要预设资源占比,防止资源独占
  • 弹性分配:空闲资源可以分配给任何队列,当多个队列争用时,会按比例进行平衡
  • 支持动态管理:可以动态调整队列的容量、权限等参数,也可动态增加、暂停队列
  • 访问控制:用户只能向自己的队列中提交任务,不能访问其他队列
  • 多租户:多用户共享集群资源

Fair Scheduler(公平调度器)

调度策略

  • 多队列公平共享集群资源
  • 通过平分的方式,动态分配资源,无需预先设定资源分配比例
  • 队列内部可配置调度策略:FIFO、Fair(默认)

资源抢占

  • 终止其他队列的任务,使其让出所占资源,然后将资源分配给占用资源量少于最小资源量限制的队列

队列权重

  • 当队列中有任务等待,并且集群中有空闲资源时,每个队列可以根据权重获得不同比例的空闲资源

资源

大数据简介

简介

什么是大数据

大数据是指超出传统数据库工具收集、存储、管理和分析能力的数据集。与此同时,及时采集、存储、聚合、管理数据,以及对数据深度分析的新技术和新能力,正在快速增长,就像预测计算芯片增长速度的摩尔定律一样。

  • Volume - 数据规模巨大
  • Velocity - 生成和处理速度极快
  • Variety - 数据规模巨大
  • Value - 生成和处理速度极快

应用场景

基于大数据的数据仓库

基于大数据的实时流处理

Hadoop 编年史

时间 事件
2003.01 Google发表了Google File System论文
2004.01 Google发表了MapReduce论文
2006.02 Apache Hadoop项目正式启动,并支持MapReduce和HDFS独立发展
2006.11 Google发表了Bigtable论文
2008.01 Hadoop成为Apache顶级项目
2009.03 Cloudera推出世界上首个Hadoop发行版——CDH,并完全开放源码
2012.03 HDFS NameNode HA加入Hadoop主版本
2014.02 Spark代替MapReduce成为Hadoop的缺省计算引擎,并成为Apache顶级项目

技术体系

HDFS

概念

  • Hadoop 分布式文件系统(Hadoop Distributed File System)
  • 在开源大数据技术体系中,地位无可替代

特点

  • 高容错:数据多副本,副本丢失后自动恢复
  • 高可用:NameNode HA,安全模式
  • 高扩展:10K 节点规模
  • 简单一致性模型:一次写入多次读取,支持追加,不允许修改
  • 流式数据访问:批量读而非随机读,关注吞吐量而非时间
  • 大规模数据集:典型文件大小 GB~TB 级,百万以上文件数量, PB 以上数据规模
  • 构建成本低且安全可靠:运行在大量的廉价商用机器上,硬件错误是常态,提供容错机制

MapReduce

概念

  • 面向批处理的分布式计算框架
  • 编程模型:将 MapReduce 程序分为 Map、Reduce 两个阶段

核心思想

  • 分而治之,分布式计算
  • 移动计算,而非移动数据

特点

  • 高容错:任务失败,自动调度到其他节点重新执行
  • 高扩展:计算能力随着节点数增加,近似线性递增
  • 适用于海量数据的离线批处理
  • 降低了分布式编程的门槛

Spark

高性能分布式通用计算引擎

  • Spark Core - 基础计算框架(批处理、交互式分析)
  • Spark SQL - SQL 引擎(海量结构化数据的高性能查询)
  • Spark Streaming - 实时流处理(微批)
  • Spark MLlib - 机器学习
  • Spark GraphX - 图计算

采用 Scala 语言开发

特点

  • 计算高效 - 内存计算、Cache 缓存机制、DAG 引擎、多线程池模型
  • 通用易用 - 适用于批处理、交互式计算、流处理、机器学习、图计算等多种场景
  • 运行模式多样 - Local、Standalone、YARN/Mesos

YARN

概念

  • Yet Another Resource Negotiator,另一种资源管理器
  • 为了解决 Hadoop 1.x 中 MapReduce 的先天缺陷
  • 分布式通用资源管理系统
  • 负责集群资源的统一管理
  • 从 Hadoop 2.x 开始,YARN 成为 Hadoop 的核心组件

特点

  • 专注于资源管理和作业调度
  • 通用 - 适用各种计算框架,如 - MapReduce、Spark
  • 高可用 - ResourceManager 高可用、HDFS 高可用
  • 高扩展

Hive

概念

  • Hadoop 数据仓库 - 企业决策支持
  • SQL 引擎 - 对海量结构化数据进行高性能的 SQL 查询
  • 采用 HDFS 或 HBase 为数据存储
  • 采用 MapReduce 或 Spark 为计算框架

特点

  • 提供类 SQL 查询语言
  • 支持命令行或 JDBC/ODBC
  • 提供灵活的扩展性
  • 提供复杂数据类型、扩展函数、脚本等

HBase

概念

  • Hadoop Database
  • Google BigTable 的开源实现
  • 分布式 NoSQL 数据库
  • 列式存储 - 主要用于半结构化、非结构化数据
  • 采用 HDFS 为文件存储系统

特点

  • 高性能 - 支持高并发写入和查询
  • 高可用 - HDFS 高可用、Region 高可用
  • 高扩展 - 数据自动切分和分布,可动态扩容,无需停机
  • 海量存储 - 单表可容纳数十亿行,上百万列

ElasticSearch

  • 开源的分布式全文检索引擎
  • 基于 Lucene 实现全文数据的快速存储、搜索和分析
  • 处理大规模数据 - PB 级以上
  • 具有较强的扩展性,集群规模可达上百台
  • 首选的分布式搜索引擎

术语

数据仓库(Data Warehouse) - 数据仓库,是为企业所有级别的决策制定过程,提供所有类型数据支持的战略集合。它是单个数据存储,出于分析性报告和决策支持目的而创建。 为需要业务智能的企业,提供指导业务流程改进、监视时间、成本、质量以及控制。

资源

系统原理面试题

📦 本文已归档到:「blog

1. 分布式缓存

1.1. Redis 有什么数据类型?分别用于什么场景

数据类型 可以存储的值 操作
STRING 字符串、整数或者浮点数 对整个字符串或者字符串的其中一部分执行操作
对整数和浮点数执行自增或者自减操作
LIST 列表 从两端压入或者弹出元素
读取单个或者多个元素
进行修剪,只保留一个范围内的元素
SET 无序集合 添加、获取、移除单个元素
检查一个元素是否存在于集合中
计算交集、并集、差集
从集合里面随机获取元素
HASH 包含键值对的无序散列表 添加、获取、移除单个键值对
获取所有键值对
检查某个键是否存在
ZSET 有序集合 添加、获取、删除元素
根据分值范围或者成员来获取元素
计算一个键的排名

What Redis data structures look like

1.2. Redis 的主从复制是如何实现的

  1. 从服务器连接主服务器,发送 SYNC 命令;
  2. 主服务器接收到 SYNC 命名后,开始执行 BGSAVE 命令生成 RDB 文件并使用缓冲区记录此后执行的所有写命令;
  3. 主服务器 BGSAVE 执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
  4. 从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
  5. 主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
  6. 从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;

1.3. Redis 的 key 是如何寻址的

1.3.1. 背景

(1)redis 中的每一个数据库,都由一个 redisDb 的结构存储。其中:

  • redisDb.id 存储着 redis 数据库以整数表示的号码。
  • redisDb.dict 存储着该库所有的键值对数据。
  • redisDb.expires 保存着每一个键的过期时间。

(2)当 redis 服务器初始化时,会预先分配 16 个数据库(该数量可以通过配置文件配置),所有数据库保存到结构 redisServer 的一个成员 redisServer.db 数组中。当我们选择数据库 select number 时,程序直接通过 redisServer.db[number] 来切换数据库。有时候当程序需要知道自己是在哪个数据库时,直接读取 redisDb.id 即可。

(3)redis 的字典使用哈希表作为其底层实现。dict 类型使用的两个指向哈希表的指针,其中 0 号哈希表(ht[0])主要用于存储数据库的所有键值,而 1 号哈希表主要用于程序对 0 号哈希表进行 rehash 时使用,rehash 一般是在添加新值时会触发,这里不做过多的赘述。所以 redis 中查找一个 key,其实就是对进行该 dict 结构中的 ht[0] 进行查找操作。

(4)既然是哈希,那么我们知道就会有哈希碰撞,那么当多个键哈希之后为同一个值怎么办呢?redis 采取链表的方式来存储多个哈希碰撞的键。也就是说,当根据 key 的哈希值找到该列表后,如果列表的长度大于 1,那么我们需要遍历该链表来找到我们所查找的 key。当然,一般情况下链表长度都为是 1,所以时间复杂度可看作 o(1)。

1.3.2. 寻址 key 的步骤

  1. 当拿到一个 key 后,redis 先判断当前库的 0 号哈希表是否为空,即:if (dict->ht[0].size == 0)。如果为 true 直接返回 NULL。
  2. 判断该 0 号哈希表是否需要 rehash,因为如果在进行 rehash,那么两个表中者有可能存储该 key。如果正在进行 rehash,将调用一次_dictRehashStep 方法,_dictRehashStep 用于对数据库字典、以及哈希键的字典进行被动 rehash,这里不作赘述。
  3. 计算哈希表,根据当前字典与 key 进行哈希值的计算。
  4. 根据哈希值与当前字典计算哈希表的索引值。
  5. 根据索引值在哈希表中取出链表,遍历该链表找到 key 的位置。一般情况,该链表长度为 1。
  6. 当 ht[0] 查找完了之后,再进行了次 rehash 判断,如果未在 rehashing,则直接结束,否则对 ht[1]重复 345 步骤。

1.4. Redis 的集群模式是如何实现的?

Redis Cluster 是 Redis 的分布式解决方案,在 Redis 3.0 版本正式推出的。

Redis Cluster 去中心化,每个节点保存数据和整个集群状态,每个节点都和其他所有节点连接。

1.4.1. Redis Cluster 节点分配

Redis Cluster 特点:

  1. 所有的 redis 节点彼此互联(PING-PONG 机制),内部使用二进制协议优化传输速度和带宽。
  2. 节点的 fail 是通过集群中超过半数的节点检测失效时才生效。
  3. 客户端与 redis 节点直连,不需要中间 proxy 层。客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。
  4. redis-cluster 把所有的物理节点映射到[0-16383] 哈希槽 (hash slot)上(不一定是平均分配),cluster 负责维护 node<->slot<->value。
  5. Redis 集群预分好 16384 个桶,当需要在 Redis 集群中放置一个 key-value 时,根据 CRC16(key) mod 16384 的值,决定将一个 key 放到哪个桶中。

1.4.2. Redis Cluster 主从模式

Redis Cluster 为了保证数据的高可用性,加入了主从模式。

一个主节点对应一个或多个从节点,主节点提供数据存取,从节点则是从主节点拉取数据备份。当这个主节点挂掉后,就会有这个从节点选取一个来充当主节点,从而保证集群不会挂掉。所以,在集群建立的时候,一定要为每个主节点都添加了从节点。

1.4.3. Redis Sentinel

Redis Sentinel 用于管理多个 Redis 服务器,它有三个功能:

  • 监控(Monitoring) - Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。
  • 提醒(Notification) - 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。
  • 自动故障迁移(Automatic failover) - 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作, 它会将失效主服务器的其中一个从服务器升级为新的主服务器, 并让失效主服务器的其他从服务器改为复制新的主服务器; 当客户端试图连接失效的主服务器时, 集群也会向客户端返回新主服务器的地址, 使得集群可以使用新主服务器代替失效服务器。

Redis 集群中应该有奇数个节点,所以至少有三个节点。

哨兵监控集群中的主服务器出现故障时,需要根据 quorum 选举出一个哨兵来执行故障转移。选举需要 majority,即大多数哨兵是运行的(2 个哨兵的 majority=2,3 个哨兵的 majority=2,5 个哨兵的 majority=3,4 个哨兵的 majority=2)。

假设集群仅仅部署 2 个节点

1
2
3
4
+----+         +----+
| M1 |---------| R1 |
| S1 | | S2 |
+----+ +----+

如果 M1 和 S1 所在服务器宕机,则哨兵只有 1 个,无法满足 majority 来进行选举,就不能执行故障转移。

1.5. Redis 如何实现分布式锁?ZooKeeper 如何实现分布式锁?比较二者优劣?

分布式锁的三种实现:

  • 基于数据库实现分布式锁;
  • 基于缓存(Redis 等)实现分布式锁;
  • 基于 Zookeeper 实现分布式锁;

1.5.1. 数据库实现

1.5.2. Redis 实现

  1. 获取锁的时候,使用 setnx 加锁,并使用 expire 命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的 value 值为一个随机生成的 UUID,通过此在释放锁的时候进行判断。
  2. 获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
  3. 释放锁的时候,通过 UUID 判断是不是该锁,若是该锁,则执行 delete 进行锁释放。

1.5.3. ZooKeeper 实现

  1. 创建一个目录 mylock;
  2. 线程 A 想获取锁就在 mylock 目录下创建临时顺序节点;
  3. 获取 mylock 目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
  4. 线程 B 获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
  5. 线程 A 处理完,删除自己的节点,线程 B 监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。

1.5.4. 实现对比

ZooKeeper 具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。
但 ZooKeeper 因为需要频繁的创建和删除节点,性能上不如 Redis 方式。

1.6. Redis 的持久化方式?有什么优缺点?持久化实现原理?

1.6.1. RDB 快照(snapshot)

将存在于某一时刻的所有数据都写入到硬盘中。

快照的原理

在默认情况下,Redis 将数据库快照保存在名字为 dump.rdb 的二进制文件中。你可以对 Redis 进行设置, 让它在“N 秒内数据集至少有 M 个改动”这一条件被满足时, 自动保存一次数据集。你也可以通过调用 SAVE 或者 BGSAVE,手动让 Redis 进行数据集保存操作。这种持久化方式被称为快照。

当 Redis 需要保存 dump.rdb 文件时, 服务器执行以下操作:

  • Redis 创建一个子进程。
  • 子进程将数据集写入到一个临时快照文件中。
  • 当子进程完成对新快照文件的写入时,Redis 用新快照文件替换原来的快照文件,并删除旧的快照文件。

这种工作方式使得 Redis 可以从写时复制(copy-on-write)机制中获益。

快照的优点
  • 它保存了某个时间点的数据集,非常适用于数据集的备份。
  • 很方便传送到另一个远端数据中心或者亚马逊的 S3(可能加密),非常适用于灾难恢复。
  • 快照在保存 RDB 文件时父进程唯一需要做的就是 fork 出一个子进程,接下来的工作全部由子进程来做,父进程不需要再做其他 IO 操作,所以快照持久化方式可以最大化 redis 的性能。
  • 与 AOF 相比,在恢复大的数据集的时候,DB 方式会更快一些。
快照的缺点
  • 如果你希望在 redis 意外停止工作(例如电源中断)的情况下丢失的数据最少的话,那么快照不适合你。
  • 快照需要经常 fork 子进程来保存数据集到硬盘上。当数据集比较大的时候,fork 的过程是非常耗时的,可能会导致 Redis 在一些毫秒级内不能响应客户端的请求。

1.6.2. AOF

AOF 持久化方式记录每次对服务器执行的写操作。当服务器重启的时候会重新执行这些命令来恢复原始的数据。

1.6.3. AOF 的原理

  • Redis 创建一个子进程。
  • 子进程开始将新 AOF 文件的内容写入到临时文件。
  • 对于所有新执行的写入命令,父进程一边将它们累积到一个内存缓存中,一边将这些改动追加到现有 AOF 文件的末尾,这样样即使在重写的中途发生停机,现有的 AOF 文件也还是安全的。
  • 当子进程完成重写工作时,它给父进程发送一个信号,父进程在接收到信号之后,将内存缓存中的所有数据追加到新 AOF 文件的末尾。
  • 搞定!现在 Redis 原子地用新文件替换旧文件,之后所有命令都会直接追加到新 AOF 文件的末尾。

1.6.4. AOF 的优点

  • 使用默认的每秒 fsync 策略,Redis 的性能依然很好(fsync 是由后台线程进行处理的,主线程会尽力处理客户端请求),一旦出现故障,使用 AOF ,你最多丢失 1 秒的数据。
  • AOF 文件是一个只进行追加的日志文件,所以不需要写入 seek,即使由于某些原因(磁盘空间已满,写的过程中宕机等等)未执行完整的写入命令,你也也可使用 redis-check-aof 工具修复这些问题。
  • Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写:重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。整个重写操作是绝对安全的。
  • AOF 文件有序地保存了对数据库执行的所有写入操作,这些写入操作以 Redis 协议的格式保存。因此 AOF 文件的内容非常容易被人读懂,对文件进行分析(parse)也很轻松。

1.6.5. AOF 的缺点

  • 对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。
  • 根据所使用的 fsync 策略,AOF 的速度可能会慢于快照。在一般情况下,每秒 fsync 的性能依然非常高,而关闭 fsync 可以让 AOF 的速度和快照一样快,即使在高负荷之下也是如此。不过在处理巨大的写入载入时,快照可以提供更有保证的最大延迟时间(latency)。

1.7. Redis 过期策略有哪些?

  • noeviction - 当内存使用达到阈值的时候,所有引起申请内存的命令会报错。
  • allkeys-lru - 在主键空间中,优先移除最近未使用的 key。
  • allkeys-random - 在主键空间中,随机移除某个 key。
  • volatile-lru - 在设置了过期时间的键空间中,优先移除最近未使用的 key。
  • volatile-random - 在设置了过期时间的键空间中,随机移除某个 key。
  • volatile-ttl - 在设置了过期时间的键空间中,具有更早过期时间的 key 优先移除。

1.8. Redis 和 Memcached 有什么区别?

两者都是非关系型内存键值数据库。有以下主要不同:

数据类型

  • Memcached 仅支持字符串类型;
  • 而 Redis 支持五种不同种类的数据类型,使得它可以更灵活地解决问题。

数据持久化

  • Memcached 不支持持久化;
  • Redis 支持两种持久化策略:RDB 快照和 AOF 日志。

分布式

  • Memcached 不支持分布式,只能通过在客户端使用像一致性哈希这样的分布式算法来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。
  • Redis Cluster 实现了分布式的支持。

内存管理机制

  • Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题,但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。
  • 在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘。而 Memcached 的数据则会一直在内存中。

1.9. 为什么单线程的 Redis 性能反而优于多线程的 Memcached?

Redis 快速的原因:

  1. 绝大部分请求是纯粹的内存操作(非常快速)
  2. 采用单线程,避免了不必要的上下文切换和竞争条件
  3. 非阻塞 IO

内部实现采用 epoll,采用了 epoll+自己实现的简单的事件框架。epoll 中的读、写、关闭、连接都转化成了事件,然后利用 epoll 的多路复用特性,绝不在 io 上浪费一点时间。

2. 分布式消息队列(MQ)

2.1. 为什么使用 MQ?

  • 异步处理 - 相比于传统的串行、并行方式,提高了系统吞吐量。
  • 应用解耦 - 系统间通过消息通信,不用关心其他系统的处理。
  • 流量削锋 - 可以通过消息队列长度控制请求量;可以缓解短时间内的高并发请求。
  • 日志处理 - 解决大量日志传输。
  • 消息通讯 - 消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯。比如实现点对点消息队列,或者聊天室等。

2.2. 如何保证 MQ 的高可用?

2.2.1. 数据复制

  1. 将所有 Broker 和待分配的 Partition 排序
  2. 将第 i 个 Partition 分配到第(i mod n)个 Broker 上
  3. 将第 i 个 Partition 的第 j 个 Replica 分配到第((i + j) mode n)个 Broker 上

2.2.2. 选举主服务器

2.3. MQ 有哪些常见问题?如何解决这些问题?

MQ 的常见问题有:

  1. 消息的顺序问题
  2. 消息的重复问题

2.3.1. 消息的顺序问题

消息有序指的是可以按照消息的发送顺序来消费。

假如生产者产生了 2 条消息:M1、M2,假定 M1 发送到 S1,M2 发送到 S2,如果要保证 M1 先于 M2 被消费,怎么做?

解决方案:

(1)保证生产者 - MQServer - 消费者是一对一对一的关系

缺陷:
  • 并行度就会成为消息系统的瓶颈(吞吐量不够)
  • 更多的异常处理,比如:只要消费端出现问题,就会导致整个处理流程阻塞,我们不得不花费更多的精力来解决阻塞的问题。

(2)通过合理的设计或者将问题分解来规避。

  • 不关注乱序的应用实际大量存在
  • 队列无序并不意味着消息无序

所以从业务层面来保证消息的顺序而不仅仅是依赖于消息系统,是一种更合理的方式。

2.3.2. 消息的重复问题

造成消息重复的根本原因是:网络不可达。

所以解决这个问题的办法就是绕过这个问题。那么问题就变成了:如果消费端收到两条一样的消息,应该怎样处理?

消费端处理消息的业务逻辑保持幂等性。只要保持幂等性,不管来多少条重复消息,最后处理的结果都一样。
保证每条消息都有唯一编号且保证消息处理成功与去重表的日志同时出现。利用一张日志表来记录已经处理成功的消息的 ID,如果新到的消息 ID 已经在日志表中,那么就不再处理这条消息。

2.4. Kafka, ActiveMQ, RabbitMQ, RocketMQ 各有什么优缺点?

## 3. 分布式服务(RPC)

3.1. Dubbo 的实现过程?

节点角色:

节点 角色说明
Provider 暴露服务的服务提供方
Consumer 调用远程服务的服务消费方
Registry 服务注册与发现的注册中心
Monitor 统计服务的调用次数和调用时间的监控中心
Container 服务运行容器

调用关系:

  1. 务容器负责启动,加载,运行服务提供者。
  2. 服务提供者在启动时,向注册中心注册自己提供的服务。
  3. 服务消费者在启动时,向注册中心订阅自己所需的服务。
  4. 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
  5. 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
  6. 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

3.2. Dubbo 负载均衡策略有哪些?

Random
  • 随机,按权重设置随机概率。
  • 在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。
RoundRobin
  • 轮循,按公约后的权重设置轮循比率。
  • 存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。
LeastActive
  • 最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差。
  • 使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。
ConsistentHash
  • 一致性 Hash,相同参数的请求总是发到同一提供者。
  • 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。
  • 算法参见:http://en.wikipedia.org/wiki/Consistent_hashing
  • 缺省只对第一个参数 Hash,如果要修改,请配置 <dubbo:parameter key="hash.arguments" value="0,1" />
  • 缺省用 160 份虚拟节点,如果要修改,请配置 <dubbo:parameter key="hash.nodes" value="320" />

3.3. Dubbo 集群容错策略 ?

- **Failover** - 失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 retries="2" 来设置重试次数(不含第一次)。 - **Failfast** - 快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。 - **Failsafe** - 失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。 - **Failback** - 失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。 - **Forking** - 并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks="2" 来设置最大并行数。 - **Broadcast** - 播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。

3.4. 动态代理策略?

Dubbo 作为 RPC 框架,首先要完成的就是跨系统,跨网络的服务调用。消费方与提供方遵循统一的接口定义,消费方调用接口时,Dubbo 将其转换成统一格式的数据结构,通过网络传输,提供方根据规则找到接口实现,通过反射完成调用。也就是说,消费方获取的是对远程服务的一个代理(Proxy),而提供方因为要支持不同的接口实现,需要一个包装层(Wrapper)。调用的过程大概是这样:

消费方的 Proxy 和提供方的 Wrapper 得以让 Dubbo 构建出复杂、统一的体系。而这种动态代理与包装也是通过基于 SPI 的插件方式实现的,它的接口就是**ProxyFactory**。
1
2
3
4
5
6
7
8
9
10
@SPI("javassist")
public interface ProxyFactory {

@Adaptive({Constants.PROXY_KEY})
<T> T getProxy(Invoker<T> invoker) throws RpcException;

@Adaptive({Constants.PROXY_KEY})
<T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) throws RpcException;

}

ProxyFactory 有两种实现方式,一种是基于 JDK 的代理实现,一种是基于 javassist 的实现。ProxyFactory 接口上定义了@SPI(“javassist”),默认为 javassist 的实现。

3.5. Dubbo 支持哪些序列化协议?Hessian?Hessian 的数据结构?

  1. dubbo 序列化,阿里尚不成熟的 java 序列化实现。
  2. hessian2 序列化:hessian 是一种跨语言的高效二进制的序列化方式,但这里实际不是原生的 hessian2 序列化,而是阿里修改过的 hessian lite,它是 dubbo RPC 默认启用的序列化方式。
  3. json 序列化:目前有两种实现,一种是采用的阿里的 fastjson 库,另一种是采用 dubbo 中自已实现的简单 json 库,一般情况下,json 这种文本序列化性能不如二进制序列化。
  4. java 序列化:主要是采用 JDK 自带的 java 序列化实现,性能很不理想。
  5. Kryo 和 FST:Kryo 和 FST 的性能依然普遍优于 hessian 和 dubbo 序列化。

Hessian 序列化与 Java 默认的序列化区别?

Hessian 是一个轻量级的 remoting on http 工具,采用的是 Binary RPC 协议,所以它很适合于发送二进制数据,同时又具有防火墙穿透能力。

  1. Hessian 支持跨语言串行
  2. 比 java 序列化具有更好的性能和易用性
  3. 支持的语言比较多

3.6. Protoco Buffer 是什么?

Protocol Buffer 是 Google 出品的一种轻量 & 高效的结构化数据存储格式,性能比 Json、XML 真的强!太!多!

Protocol Buffer 的序列化 & 反序列化简单 & 速度快的原因是:

  1. 编码 / 解码 方式简单(只需要简单的数学运算 = 位移等等)
  2. 采用 Protocol Buffer 自身的框架代码 和 编译器 共同完成

Protocol Buffer 的数据压缩效果好(即序列化后的数据量体积小)的原因是:

  1. 采用了独特的编码方式,如 Varint、Zigzag 编码方式等等
  2. 采用 T - L - V 的数据存储方式:减少了分隔符的使用 & 数据存储得紧凑

3.7. 注册中心挂了可以继续通信吗?

可以。Dubbo 消费者在应用启动时会从注册中心拉取已注册的生产者的地址接口,并缓存在本地。每次调用时,按照本地存储的地址进行调用。

3.8. ZooKeeper 原理是什么?ZooKeeper 有什么用?

ZooKeeper 是一个分布式应用协调系统,已经用到了许多分布式项目中,用来完成统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等工作。

  1. 每个 Server 在内存中存储了一份数据;
  2. Zookeeper 启动时,将从实例中选举一个 leader(Paxos 协议);
  3. Leader 负责处理数据更新等操作(Zab 协议);
  4. 一个更新操作成功,当且仅当大多数 Server 在内存中成功修改数据。

3.9. Netty 有什么用?NIO/BIO/AIO 有什么用?有什么区别?

Netty 是一个“网络通讯框架”。

Netty 进行事件处理的流程。Channel是连接的通道,是 ChannelEvent 的产生者,而ChannelPipeline可以理解为 ChannelHandler 的集合。

> 参考:https://github.com/code4craft/netty-learning/blob/master/posts/ch1-overview.md

IO 的方式通常分为几种:

  • 同步阻塞的 BIO
  • 同步非阻塞的 NIO
  • 异步非阻塞的 AIO

在使用同步 I/O 的网络应用中,如果要同时处理多个客户端请求,或是在客户端要同时和多个服务器进行通讯,就必须使用多线程来处理。

NIO 基于 Reactor,当 socket 有流可读或可写入 socket 时,操作系统会相应的通知引用程序进行处理,应用再将流读取到缓冲区或写入操作系统。也就是说,这个时候,已经不是一个连接就要对应一个处理线程了,而是有效的请求,对应一个线程,当连接没有数据时,是没有工作线程来处理的。

与 NIO 不同,当进行读写操作时,只须直接调用 API 的 read 或 write 方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入 read 方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将 write 方法传递的流写入完毕时,操作系统主动通知应用程序。 即可以理解为,read/write 方法都是异步的,完成后会主动调用回调函数。

参考:https://blog.csdn.net/skiof007/article/details/52873421

3.10. 为什么要进行系统拆分?拆分不用 Dubbo 可以吗?

系统拆分从资源角度分为:应用拆分和数据库拆分。

从采用的先后顺序可分为:水平扩展、垂直拆分、业务拆分、水平拆分。

是否使用服务依据实际业务场景来决定。

当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式服务框架(RPC)是关键。

当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)是关键。

3.11. Dubbo 和 Thrift 有什么区别?

  • Thrift 是跨语言的 RPC 框架。
  • Dubbo 支持服务治理,而 Thrift 不支持。

分布式系统基本原理

📦 本文已归档到:「blog

1. 分布式术语

1.1. 异常

  • 服务器宕机 - 内存错误、服务器停电等都会导致服务器宕机,此时节点无法正常工作,称为不可用。服务器宕机会导致节点失去所有内存信息,因此需要将内存信息保存到持久化介质上。
  • 网络异常 - 有一种特殊的网络异常称为——网络分区 ,即集群的所有节点被划分为多个区域,每个区域内部可以通信,但是区域之间无法通信。
  • 磁盘故障 - 磁盘故障是一种发生概率很高的异常。使用冗余机制,将数据存储到多台服务器。

1.2. 超时

在分布式系统中,一个请求除了成功和失败两种状态,还存在着超时状态。

可以将服务器的操作设计为具有 幂等性 ,即执行多次的结果与执行一次的结果相同。如果使用这种方式,当出现超时的时候,可以不断地重新请求直到成功。

1.3. 衡量指标

性能

常见的性能指标有:吞吐量、响应时间。

其中,吞吐量指系统在某一段时间可以处理的请求总数,通常为每秒的读操作数或者写操作数;响应时间指从某个请求发出到接收到返回结果消耗的时间。

这两个指标往往是矛盾的,追求高吞吐的系统,往往很难做到低响应时间,解释如下:

  • 在无并发的系统中,吞吐量为响应时间的倒数,例如响应时间为 10 ms,那么吞吐量为 100 req/s,因此高吞吐也就意味着低响应时间。

  • 但是在并发的系统中,由于一个请求在调用 I/O 资源的时候,需要进行等待。服务器端一般使用的是异步等待方式,即等待的请求被阻塞之后不需要一直占用 CPU 资源。这种方式能大大提高 CPU 资源的利用率,例如上面的例子中,单个请求在无并发的系统中响应时间为 10 ms,如果在并发的系统中,那么吞吐量将大于 100 req/s。因此为了追求高吞吐量,通常会提高并发程度。但是并发程度的增加,会导致请求的平均响应时间也增加,因为请求不能马上被处理,需要和其它请求一起进行并发处理,响应时间自然就会增高。

可用性

可用性指系统在面对各种异常时可以提供正常服务的能力。可以用系统可用时间占总时间的比值来衡量,4 个 9 的可用性表示系统 99.99% 的时间是可用的。

一致性

可以从两个角度理解一致性:从客户端的角度,读写操作是否满足某种特性;从服务器的角度,多个数据副本之间是否一致。

可扩展性

指系统通过扩展集群服务器规模来提高性能的能力。理想的分布式系统需要实现“线性可扩展”,即随着集群规模的增加,系统的整体性能也会线性增加。

2. 分布式基础理论

img

:bulb: CAP 和 BASE 详尽剖析,请参考:CAP 定理和 BASE 理论

3. 分布式事务

3.1. 两阶段提交(2PC)

两阶段提交(Two-phase Commit,2PC)

主要用于实现分布式事务,分布式事务指的是事务操作跨越多个节点,并且要求满足事务的 ACID 特性。

通过引入协调者(Coordinator)来调度参与者的行为,并最终决定这些参与者是否要真正执行事务。

运行过程

准备阶段

协调者询问参与者事务是否执行成功,参与者发回事务执行结果。

提交阶段

如果事务在每个参与者上都执行成功,事务协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。

需要注意的是,在准备阶段,参与者执行了事务,但是还未提交。只有在提交阶段接收到协调者发来的通知后,才进行提交或者回滚。

问题

同步阻塞

所有事务参与者在等待其它参与者响应的时候都处于同步阻塞状态,无法进行其它操作。

单点问题

协调者在 2PC 中起到非常大的作用,发生故障将会造成很大影响,特别是在阶段二发生故障,所有参与者会一直等待状态,无法完成其它操作。

数据不一致

在阶段二,如果协调者只发送了部分 Commit 消息,此时网络发生异常,那么只有部分参与者接收到 Commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。

太过保守

任意一个节点失败就会导致整个事务失败,没有完善的容错机制。

PC 优缺点

优点:尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能 100%保证强一致)
缺点:实现复杂,牺牲了可用性,对性能影响较大,不适合高并发高性能场景。

3.2. 补偿事务(TCC)

补偿事务(TCC)其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段:

  1. Try 阶段主要是对业务系统做检测及资源预留。
  2. Confirm 阶段主要是对业务系统做确认提交,Try 阶段执行成功并开始执行 Confirm 阶段时,默认 Confirm 阶段是不会出错的。即:只要 Try 成功,Confirm 一定成功。
  3. Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。

举个例子,假设 Bob 要向 Smith 转账,思路大概是:

  1. 首先在 Try 阶段,要先调用远程接口把 Smith 和 Bob 的钱给冻结起来。
  2. 在 Confirm 阶段,执行远程调用的转账的操作,转账成功进行解冻。
  3. 如果第 2 步执行成功,那么转账成功,如果第二步执行失败,则调用远程冻结接口对应的解冻方法 (Cancel)。

TCC 优缺点

  • 优点:跟 2PC 比起来,实现以及流程相对简单了一些,但数据的一致性比 2PC 也要差一些。
  • 缺点:缺点还是比较明显的,在 2,3 步中都有可能失败。TCC 属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用 TCC 不太好定义及处理。

3.3. 本地消息表(异步确保)

本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性。

  1. 在分布式事务操作的一方完成写业务数据的操作之后向本地消息表发送一个消息,本地事务能保证这个消息一定会被写入本地消息表中。
  2. 之后将本地消息表中的消息转发到 Kafka 等消息队列(MQ)中,如果转发成功则将消息从本地消息表中删除,否则继续重新转发。
  3. 在分布式事务操作的另一方从消息队列中读取一个消息,并执行消息中的操作。

这种方案遵循 BASE 理论,采用的是最终一致性。

本地消息表利用了本地事务来实现分布式事务,并且使用了消息队列来保证最终一致性。

本地消息表优缺点

  • 优点:一种非常经典的实现,避免了分布式事务,实现了最终一致性。
  • 缺点:消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。

3.4. MQ 事务消息

有一些第三方的 MQ 是支持事务消息的,比如 RocketMQ,他们支持事务消息的方式也是类似于采用的二阶段提交。但是市面上一些主流的 MQ 都是不支持事务消息的,比如 RabbitMQ 和 Kafka 都不支持。

以阿里的 RocketMQ 中间件为例,其思路大致为:

  1. Prepared 消息,会拿到消息的地址。
  2. 执行本地事务。
  3. 通过第一阶段拿到的地址去访问消息,并修改状态。

也就是说在业务方法内要想消息队列提交两次请求,一次发送消息和一次确认消息。如果确认消息发送失败了 RocketMQ 会定期扫描消息集群中的事务消息,这时候发现了 Prepared 消息,它会向消息发送者确认,所以生产方需要实现一个 check 接口,RocketMQ 会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。

MQ 事务消息优缺点

  • 优点:实现了最终一致性,不需要依赖本地数据库事务。
  • 缺点:实现难度大,主流 MQ 不支持。

4. 共识性算法

4.1. Paxos

img

:bulb: Paxos 详尽剖析,请参考:深入剖析共识性算法 Paxos

4.2. Raft

img

:bulb: Raft 详尽剖析,请参考:深入剖析共识性算法 Raft

5. 参考资料

大型系统核心技术

大型系统的设计目标就是为了快速、高效、稳定的处理海量的数据以及高并发的请求。

单机服务受限于硬件,客观存在着资源瓶颈,难以应对不断增长的数据量和请求量,为了打破瓶颈,大型系统基本上都被设计为分布式系统。

分布式系统由于其面临的共性问题,在很多场景下的解决方案往往也存在着共性。因此,我们会发现,很多优秀的大型系统在设计方案上存在着很多的共同点。

本文主要讨论应对分布式系统共性问题的解决方案,这既可以加深对分布式系统运作原理的理解,也可以作为设计大型分布式系统时的借鉴。

1. 分布式事务

参考:分布式原理#4-分布式事务问题

2. 分布式锁

Java 原生 API 虽然有并发锁,但并没有提供分布式锁的能力,所以针对分布式场景中的锁需要解决的方案。

分布式锁的解决方案大致有以下几种:

  • 基于数据库实现
  • 基于缓存(redis,memcached 等)实现
  • 基于 Zookeeper 实现

2.1. 基于数据库实现分布式锁

实现

1. 创建表
1
2
3
4
5
6
7
8
CREATE TABLE `methodLock` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁定的方法名',
`desc` varchar(1024) NOT NULL DEFAULT '备注信息',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成',
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';
2. 获取锁

想要锁住某个方法时,执行以下 SQL:

1
insert into methodLock(method_name,desc) values (‘method_name’,‘desc’)

因为我们对 method_name 做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。

成功插入则获取锁。

3. 释放锁

当方法执行完毕之后,想要释放锁的话,需要执行以下 Sql:

1
delete from methodLock where method_name ='method_name'

问题

  1. 这把锁强依赖数据库的可用性。如果数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
  2. 这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
  3. 这把锁只能是非阻塞的,因为数据的 insert 操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
  4. 这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。

解决办法

  1. 单点问题可以用多数据库实例,同时塞 N 个表,N/2+1 个成功就任务锁定成功
  2. 写一个定时任务,隔一段时间清除一次过期的数据。
  3. 写一个 while 循环,不断的重试插入,直到成功。
  4. 在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。

小结

  • 优点: 直接借助数据库,容易理解。
  • 缺点: 会有各种各样的问题,在解决问题的过程中会使整个方案变得越来越复杂。操作数据库需要一定的开销,性能问题需要考虑。

2.2. 基于 Redis 实现分布式锁

相比于用数据库来实现分布式锁,基于缓存实现的分布式锁的性能会更好一些。目前有很多成熟的分布式产品,包括 Redis、memcache、Tair 等。这里以 Redis 举例。

Redis 命令

  • setnx - setnx key val:当且仅当 key 不存在时,set 一个 key 为 val 的字符串,返回 1;若 key 存在,则什么都不做,返回 0。
  • expire - expire key timeout:为 key 设置一个超时时间,单位为 second,超过这个时间锁会自动释放,避免死锁。
  • delete - delete key:删除 key

实现

单点实现步骤:

  1. 获取锁的使用,使用 setnx 加锁,锁的 value 值为一个随机生成的 UUID,再使用 expire 设置一个过期值。
  2. 获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
  3. 释放锁的时候,通过 UUID 判断是不是该锁,若是该锁,则执行 delete 进行锁释放。

问题

  • 单点问题。如果单机 redis 挂掉了,那么程序会跟着出错。
  • 如果转移使用 slave 节点,复制不是同步复制,会出现多个程序获取锁的情况

小结

可以考虑使用 redisson 的解决方案

2.3. 基于 ZooKeeper 实现分布式锁

实现

这也是 ZooKeeper 客户端 curator 的分布式锁实现。

  1. 创建一个目录 mylock;
  2. 线程 A 想获取锁就在 mylock 目录下创建临时顺序节点;
  3. 获取 mylock 目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
  4. 线程 B 获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
  5. 线程 A 处理完,删除自己的节点,线程 B 监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。

小结

ZooKeeper 版本的分布式锁问题相对比较来说少。

  • 锁的占用时间限制:redis 就有占用时间限制,而 ZooKeeper 则没有,最主要的原因是 redis 目前没有办法知道已经获取锁的客户端的状态,是已经挂了呢还是正在执行耗时较长的业务逻辑。而 ZooKeeper 通过临时节点就能清晰知道,如果临时节点存在说明还在执行业务逻辑,如果临时节点不存在说明已经执行完毕释放锁或者是挂了。由此看来 redis 如果能像 ZooKeeper 一样添加一些与客户端绑定的临时键,也是一大好事。
  • 是否单点故障:redis 本身有很多中玩法,如客户端一致性 hash,服务器端 sentinel 方案或者 cluster 方案,很难做到一种分布式锁方式能应对所有这些方案。而 ZooKeeper 只有一种玩法,多台机器的节点数据是一致的,没有 redis 的那么多的麻烦因素要考虑。

总体上来说 ZooKeeper 实现分布式锁更加的简单,可靠性更高。但 ZooKeeper 因为需要频繁的创建和删除节点,性能上不如 Redis 方式。

3. 分布式 Session

在分布式场景下,一个用户的 Session 如果只存储在一个服务器上,那么当负载均衡器把用户的下一个请求转发到另一个服务器上,该服务器没有用户的 Session,就可能导致用户需要重新进行登录等操作。

分布式 Session 的几种实现策略:

  1. 粘性 session
  2. 应用服务器间的 session 复制共享
  3. 基于 cache DB 缓存的 session 共享

3.1. Sticky Sessions

需要配置负载均衡器,使得一个用户的所有请求都路由到一个服务器节点上,这样就可以把用户的 Session 存放在该服务器节点中。

缺点:当服务器节点宕机时,将丢失该服务器节点上的所有 Session。

3.2. Session Replication

在服务器节点之间进行 Session 同步操作,这样的话用户可以访问任何一个服务器节点。

缺点:占用过多内存;同步过程占用网络带宽以及服务器处理器时间。

3.3. Session Server

使用一个单独的服务器存储 Session 数据,可以存在 MySQL 数据库上,也可以存在 Redis 或者 Memcached 这种内存型数据库。

缺点:需要去实现存取 Session 的代码。

4. 分布式存储

通常有两种解决方案:

  1. 数据分布:就是把数据分块存在不同的服务器上(分库分表)。
  2. 数据复制:让所有的服务器都有相同的数据,提供相当的服务。

参考:分布式原理.md#2-数据分布

5. 分布式缓存

使用缓存的好处:

  • 提升数据读取速度
  • 提升系统扩展能力,通过扩展缓存,提升系统承载能力
  • 降低存储成本,Cache+DB 的方式可以承担原有需要多台 DB 才能承担的请求量,节省机器成本

根据业务场景,通常缓存有以下几种使用方式

  • 懒汉式(读时触发):写入 DB 后, 然后把相关的数据也写入 Cache
  • 饥饿式(写时触发):先查询 DB 里的数据, 然后把相关的数据写入 Cache
  • 定期刷新:适合周期性的跑数据的任务,或者列表型的数据,而且不要求绝对实时性

缓存分类:

  • 应用内缓存:如:EHCache
  • 分布式缓存:如:Memached、Redis

参考:分布式原理.md#6-分布式缓存问题

6. 分布式计算

7. 负载均衡

7.1. 算法

轮询(Round Robin)

轮询算法把每个请求轮流发送到每个服务器上。下图中,一共有 6 个客户端产生了 6 个请求,这 6 个请求按 (1, 2, 3, 4, 5, 6) 的顺序发送。最后,(1, 3, 5) 的请求会被发送到服务器 1,(2, 4, 6) 的请求会被发送到服务器 2。

该算法比较适合每个服务器的性能差不多的场景,如果有性能存在差异的情况下,那么性能较差的服务器可能无法承担过大的负载(下图的 Server 2)。

加权轮询(Weighted Round Robbin)

加权轮询是在轮询的基础上,根据服务器的性能差异,为服务器赋予一定的权值。例如下图中,服务器 1 被赋予的权值为 5,服务器 2 被赋予的权值为 1,那么 (1, 2, 3, 4, 5) 请求会被发送到服务器 1,(6) 请求会被发送到服务器 2。

最少连接(least Connections)

由于每个请求的连接时间不一样,使用轮询或者加权轮询算法的话,可能会让一台服务器当前连接数过大,而另一台服务器的连接过小,造成负载不均衡。例如下图中,(1, 3, 5) 请求会被发送到服务器 1,但是 (1, 3) 很快就断开连接,此时只有 (5) 请求连接服务器 1;(2, 4, 6) 请求被发送到服务器 2,只有 (2) 的连接断开。该系统继续运行时,服务器 2 会承担过大的负载。

最少连接算法就是将请求发送给当前最少连接数的服务器上。例如下图中,服务器 1 当前连接数最小,那么新到来的请求 6 就会被发送到服务器 1 上。

加权最少连接(Weighted Least Connection)

在最少连接的基础上,根据服务器的性能为每台服务器分配权重,再根据权重计算出每台服务器能处理的连接数。

随机算法(Random)

把请求随机发送到服务器上。和轮询算法类似,该算法比较适合服务器性能差不多的场景。

源地址哈希法 (IP Hash)

源地址哈希通过对客户端 IP 哈希计算得到的一个数值,用该数值对服务器数量进行取模运算,取模结果便是目标服务器的序号。

  • 优点:保证同一 IP 的客户端都会被 hash 到同一台服务器上。
  • 缺点:不利于集群扩展,后台服务器数量变更都会影响 hash 结果。可以采用一致性 Hash 改进。

7.2. 实现

HTTP 重定向

HTTP 重定向负载均衡服务器收到 HTTP 请求之后会返回服务器的地址,并将该地址写入 HTTP 重定向响应中返回给浏览器,浏览器收到后需要再次发送请求。

缺点:

  • 用户访问的延迟会增加;
  • 如果负载均衡器宕机,就无法访问该站点。

DNS 重定向

使用 DNS 作为负载均衡器,根据负载情况返回不同服务器的 IP 地址。大型网站基本使用了这种方式做为第一级负载均衡手段,然后在内部使用其它方式做第二级负载均衡。

缺点:

  • DNS 查找表可能会被客户端缓存起来,那么之后的所有请求都会被重定向到同一个服务器。

修改 MAC 地址

使用 LVS(Linux Virtual Server)这种链路层负载均衡器,根据负载情况修改请求的 MAC 地址。

修改 IP 地址

在网络层修改请求的目的 IP 地址。

代理自动配置

正向代理与反向代理的区别:

  • 正向代理:发生在客户端,是由用户主动发起的。比如翻墙,客户端通过主动访问代理服务器,让代理服务器获得需要的外网数据,然后转发回客户端。
  • 反向代理:发生在服务器端,用户不知道代理的存在。

PAC 服务器是用来判断一个请求是否要经过代理。

8. 资料

负载均衡基本原理

📦 本文已归档到:「blog

1. 负载均衡简介

1.1. 负载均衡的作用

负载均衡(Load Balance,简称 LB)是高可用基础架构的关键组件,通常 用于将网络流量分发到多个服务器上,以提高应用的响应速度和可用性

系统的扩展可分为垂直扩展和水平扩展。

  • 垂直扩展,是从单机的角度通过增加硬件处理能力,比如 CPU 处理能力,内存容量,磁盘等方面,实现服务器处理能力的提升。这种方式不能满足大型分布式系统(网站),大流量,高并发,海量数据的问题。
  • 水平扩展,是通过添加机器来满足大型网站服务的处理能力。比如:一台机器不能满足,则增加两台或者多台机器,共同承担访问压力。
    • 应用集群:将同一应用部署到多台机器上,组成处理集群,接收负载均衡设备分发的请求,进行处理,并返回相应数据。
    • 负载均衡设备:将用户访问的请求,根据负载均衡算法,分发到集群中的一台处理服务器。(一种把网络请求分散到一个服务器集群中的可用服务器上去的设备)

负载均衡的作用:

  • 高并发 - 负载均衡通过调整负载,尽力让应用集群中的节点工作量达到均匀,以此提高应用集群的并发处理能力(吞吐量)。
  • 伸缩性 - 添加或减少服务器数量,然后由负载均衡进行分发控制。这使得应用集群具备伸缩性。
  • 故障转移 - 负载均衡器可以监控候选服务器,当服务器不可用时,自动跳过,将请求分发给可用的服务器。这使得应用集群具备高可用的特性。
  • 安全防护 - 有些负载均衡软件或硬件提供了安全性功能,如:黑白名单处理、防火墙,防 DDos 攻击等。

1.2. 负载均衡的分类

负载均衡大致可以分为两大类:

  • 软件负载均衡
  • 硬件负载均衡

软件负载均衡

软件负载均衡,应用最为广泛,无论大公司还是小公司都会使用。

软件负载均衡从软件层面实现负载均衡,一般可以在任何标准物理设备上运行。

软件负载均衡的 主流产品 有:NginxHAProxyLVS

  • LVS 可以作为四层负载均衡器。其负载均衡的性能要优于 Nginx。
  • HAProxy 可以作为 HTTP 和 TCP 负载均衡器。
  • NginxHAProxy 可以作为四层或七层负载均衡器。

软件负载均衡的 优点

  • 扩展性好 - 适应动态变化,可以通过添加软件负载均衡实例,动态扩展到超出初始容量的能力。
  • 成本低廉 - 软件负载均衡可以在任何标准物理设备上运行,降低了购买和运维的成本。

软件负载均衡的 缺点

  • 性能略差 - 相比于硬件负载均衡,软件负载均衡的性能要略低一些。

软件负载负载均衡从通信层面来看,又可以分为四层和七层负载均衡。

  • 四层负载均衡 - 基于 IP 地址和端口进行请求的转发。
  • 七层负载均衡 - 就是可以根据访问用户的 HTTP 请求头、URL 信息将请求转发到特定的主机。

硬件负载均衡

硬件负载均衡,一般是在定制处理器上运行的独立负载均衡服务器,价格昂贵,土豪专属。

硬件负载均衡的 主流产品 有:F5A10

硬件负载均衡的 优点

  • 功能强大 - 支持全局负载均衡并提供较全面的、复杂的负载均衡算法。
  • 性能强悍 - 硬件负载均衡由于是在专用处理器上运行,因此吞吐量大,可支持单机百万以上的并发。
  • 安全性高 - 往往具备防火墙,防 DDos 攻击等安全功能。

硬件负载均衡的 缺点

  • 成本昂贵 - 购买和维护硬件负载均衡的成本都很高。
  • 扩展性差 - 当访问量突增时,超过限度不能动态扩容。

2. 负载均衡算法

负载均衡产品多种多样,但是各种负载均衡算法原理是共性的。

负载均衡算法有很多种,分别适用于不同的应用场景,本文仅介绍最为常见的负载均衡算法的特性及原理:轮询、随机、最少连接、一致性 Hash。

2.1. 轮询

轮询(Round Robin) 算法 将请求依次分发到候选服务器

  • 特点:请求完全均匀分发,即服务器请求被完全均匀的分发到集群的各节点上。
  • 场景:适合服务器硬件相同的场景

下图中,一共有 6 个客户端产生了 6 个请求,这 6 个请求按 (1, 2, 3, 4, 5, 6) 的顺序发送。最后,(1, 3, 5) 的请求会被发送到服务器 1,(2, 4, 6) 的请求会被发送到服务器 2。

img

该算法比较适合每个服务器的性能差不多的场景,如果有性能存在差异的情况下,那么性能较差的服务器可能无法承担过大的负载(下图的 Server 2)。

img

节点存储结构:

1
private List<V> nodeList = Collections.emptyList();

算法实现示例:

1
2
3
4
5
6
7
8
9
private V select() {
if (CollectionUtil.isEmpty(this.nodeList)) {
return null;
}

int size = this.nodeList.size();
offset.compareAndSet(size, 0);
return nodeList.get(offset.getAndIncrement());
}

加权轮询

加权轮询(Weighted Round Robbin) 算法在轮询算法的基础上,增加了权重属性。性能高、处理速度快的机器应该设置更高的权重,使得分发时优先将请求分发到权重较高的服务器上。

  • 优点:根据权重,调节转发服务器的请求数目。
  • 缺点:比轮询算法复杂。

加权轮询是在轮询的基础上,根据服务器的性能差异,为服务器赋予一定的权值。例如下图中,服务器 1 被赋予的权值为 5,服务器 2 被赋予的权值为 1,那么 (1, 2, 3, 4, 5) 请求会被发送到服务器 1,(6) 请求会被发送到服务器 2。

img

节点存储结构:

1
2
// key 存储实际节点内容,value 存储节点的权重
private Map<V, Integer> nodeMap = new LinkedHashMap<>();

算法实现示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public V select() {
if (MapUtil.isEmpty(nodeMap)) {
return null;
}

int totalWeight = nodeMap.values().stream().mapToInt(a -> a).sum();
int number = offset.getAndIncrement() % totalWeight;

for (Map.Entry<V, Integer> item : nodeMap.entrySet()) {
if (item.getValue() > number) {
return item.getKey();
}
number -= item.getValue();
}
return null;
}

2.2. 随机

随机(Random) 算法 将请求随机分发到候选服务器

  • 特点:调用量较小的时候,可能负载并不均匀,调用量越大,负载越均衡
  • 场景:适合服务器硬件相同的场景

img

节点存储结构:

1
List<V> nodeList = Collections.emptyList();

算法实现示例:

1
2
3
4
5
6
7
8
public V select() {
if (CollectionUtil.isEmpty(this.nodeList)) {
return null;
}

int offset = random.nextInt(nodeList.size());
return nodeList.get(offset);
}

加权随机

加权随机(Weighted Random) 算法在随机算法的基础上,按照概率调整权重,进行负载分配。

节点存储结构:

1
2
// key 存储实际节点内容,value 存储节点的权重
Map<V, Integer> nodeMap = new LinkedHashMap<>();

算法实现示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public V select() {
if (MapUtil.isEmpty(keyWeightMap)) {
return null;
}

List<V> list = new ArrayList<>();
for (Map.Entry<V, Integer> item : keyWeightMap.entrySet()) {
for (int i = 0; i < item.getValue(); i++) {
list.add(item.getKey());
}
}

int totalWeight = keyWeightMap.values().stream().mapToInt(a -> a).sum();
int number = random.nextInt(totalWeight);
return list.get(number);
}

2.3. 最少连接

最少连接(Least Connection) 算法 将请求分发到连接数最少的候选服务器(目前处理请求最少的服务器)。

  • 特点:根据候选服务器当前的请求连接数,动态分配。
  • 场景:适用于对系统负载较为敏感或请求连接时长相差较大的场景

由于每个请求的连接时长不一样,如果采用简单的轮循或随机算法,都可能出现某些服务器当前连接数过大,而另一些服务器的连接过小的情况,这就造成了负载并非真正均衡。虽然,轮询或算法都可以通过加权重属性的方式进行负载调整,但加权方式难以应对动态变化。

例如下图中,(1, 3, 5) 请求会被发送到服务器 1,但是 (1, 3) 很快就断开连接,此时只有 (5) 请求连接服务器 1;(2, 4, 6) 请求被发送到服务器 2,只有 (2) 的连接断开。该系统继续运行时,服务器 2 会承担过大的负载。

img

最少连接算法会记录当前时刻,每个候选节点正在处理的连接数,然后选择连接数最小的节点。该策略能够动态、实时地反应服务器的当前状况,较为合理地将负责分配均匀,适用于对当前系统负载较为敏感的场景。

例如下图中,服务器 1 当前连接数最小,那么新到来的请求 6 就会被发送到服务器 1 上。

img

加权最少连接

加权最少连接(Weighted Least Connection)在最少连接的基础上,根据服务器的性能为每台服务器分配权重,再根据权重计算出每台服务器能处理的连接数。

img

2.4. 源地址哈希

**源地址哈希(IP Hash)**算法 根据请求源 IP,通过哈希计算得到一个数值,用该数值在候选服务器列表的进行取模运算,得到的结果便是选中的服务器

  • 特点:保证特定用户总是请求到相同的服务器,若服务器宕机,会话会丢失。
  • 场景:会话粘滞。

img

算法示例:

1
2
3
4
5
6
public V select(final String key) {
List<V> list = new ArrayList<>(nodes);
int hashCode = key.hashCode();
int idx = hashCode % list.size();
return list.get(Math.abs(idx));
}

2.5. 一致性哈希

一致性哈希(Consistent Hash)算法的目标是:相同的请求尽可能落到同一个服务器上

  • 相同的请求是指:一般在使用一致性哈希时,需要指定一个 key 用于 hash 计算,可能是:
    • 用户 ID
    • 请求方 IP
    • 请求服务名称,参数列表构成的串
  • 尽可能是指:服务器可能发生上下线,少数服务器的变化不应该影响大多数的请求。

当某台候选服务器宕机时,原本发往该服务器的请求,会基于虚拟节点,平摊到其它候选服务器,不会引起剧烈变动。

一致性哈希算法示例:

  • 构建虚拟 Hash 环
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private final static int VIRTUAL_NODE_SIZE = 1000;
private final static String VIRTUAL_NODE_SUFFIX = "&&";
private Set<V> nodes = new LinkedHashSet<>(collection);
private TreeMap<Integer, V> hashRing = buildConsistentHashRing(this.nodes);

TreeMap<Integer, V> buildConsistentHashRing(Set<V> nodes) {
TreeMap<Integer, V> hashRing = new TreeMap<>();
for (V node : nodes) {
for (int i = 0; i < VIRTUAL_NODE_SIZE; i++) {
// 新增虚拟节点的方式如果有影响,也可以抽象出一个由物理节点扩展虚拟节点的类
hashRing.put(hashStrategy.hashCode(node + VIRTUAL_NODE_SUFFIX + i), node);
}
}
return hashRing;
}

核心算法

1
2
3
4
5
6
7
8
9
10
public V select(String key) {
int hashCode = hashStrategy.hashCode(key);
// 向右找到第一个 key
Map.Entry<Integer, V> entry = hashRing.ceilingEntry(hashCode);
if (entry == null) {
// 想象成一个环,超过尾部则取第一个 key
entry = hashRing.firstEntry();
}
return entry.getValue();
}

3. 负载均衡技术

根据实现技术不同,可分划分如下:

  • 七层负载均衡
    • DNS 重定向
    • HTTP 重定向
    • 反向代理
  • 四层负载均衡
    • 修改 IP 地址
    • 修改 MAC 地址

3.1. DNS 负载均衡

DNS 负载均衡一般用于互联网公司,复杂的业务系统不适合使用。大型网站一般使用 DNS 负载均衡作为 第一级负载均衡手段,然后在内部使用其它方式做第二级负载均衡。DNS 负载均衡属于七层负载均衡。

DNS 即 域名解析服务,是 OSI 第七层网络协议。DNS 被设计为一个树形结构的分布式应用,自上而下依次为:根域名服务器,一级域名服务器,二级域名服务器,… ,本地域名服务器。显然,如果所有数据都存储在根域名服务器,那么 DNS 查询的负载和开销会非常庞大。

img

因此,DNS 查询相对于 DNS 层级结构,是一个逆向的递归流程,DNS 客户端依次请求本地 DNS 服务器,上一级 DNS 服务器,上上一级 DNS 服务器,… ,根 DNS 服务器(又叫权威 DNS 服务器),一旦命中,立即返回。为了减少查询次数,每一级 DNS 服务器都会设置 DNS 查询缓存。

DNS 负载均衡的工作原理就是:基于 DNS 查询缓存,按照负载情况返回不同服务器的 IP 地址

img

DNS 重定向的 优点

  • 使用简单 - 负载均衡工作,交给 DNS 服务器处理,省掉了负载均衡服务器维护的麻烦
  • 提高性能 - 可以支持基于地址的域名解析,解析成距离用户最近的服务器地址(类似 CDN 的原理),可以加快访问速度,改善性能;

DNS 重定向的 缺点

  • 可用性差 - DNS 解析是多级解析,新增/修改 DNS 后,解析时间较长;解析过程中,用户访问网站将失败;
  • 扩展性低 - DNS 负载均衡的控制权在域名商那里,无法对其做更多的改善和扩展;
  • 维护性差 - 也不能反映服务器的当前运行状态;支持的算法少;不能区分服务器的差异(不能根据系统与服务的状态来判断负载)

3.2. HTTP 负载均衡

HTTP 负载均衡是基于 HTTP 重定向实现的。HTTP 负载均衡属于七层负载均衡。

HTTP 重定向原理是:根据用户的 HTTP 请求计算出一个真实的服务器地址,将该服务器地址写入 HTTP 重定向响应中,返回给浏览器,由浏览器重新进行访问

img

HTTP 重定向的 优点方案简单

HTTP 重定向的 缺点

  • 性能较差 - 每次访问需要两次请求服务器,增加了访问的延迟。
  • 降低搜索排名 - 使用重定向后,搜索引擎会视为 SEO 作弊。
  • 如果负载均衡器宕机,就无法访问该站点。

由于其缺点比较明显,所以这种负载均衡策略实际应用较少。

3.3. 反向代理

反向代理(Reverse Proxy)方式是指以 代理服务器 来接受网络请求,然后 将请求转发给内网中的服务器,并将从内网中的服务器上得到的结果返回给网络请求的客户端。反向代理负载均衡属于七层负载均衡。

img

反向代理服务的主流产品:Nginx、Apache。

正向代理与反向代理有什么区别?

  • 正向代理:发生在 客户端,是由用户主动发起的。翻墙软件就是典型的正向代理,客户端通过主动访问代理服务器,让代理服务器获得需要的外网数据,然后转发回客户端。
  • 反向代理:发生在 服务端,用户不知道代理的存在。

img

反向代理的 优点

  • 多种负载均衡算法 - 支持多种负载均衡算法,以应对不同的场景需求。
  • 可以监控服务器 - 基于 HTTP 协议,可以监控转发服务器的状态,如:系统负载、响应时间、是否可用、连接数、流量等,从而根据这些数据调整负载均衡的策略。

反向代理的 缺点

  • 额外的转发开销 - 反向代理的转发操作本身是有性能开销的,可能会包括,创建连接,等待连接响应,分析响应结果等操作。

  • 增加系统复杂度 - 反向代理常用于做分布式应用的水平扩展,但反向代理服务存在以下问题,为了解决以下问题会给系统整体增加额外的复杂度和运维成本:

    • 反向代理服务如果宕机,就无法访问站点,所以需要有 高可用 方案,常见的方案有:主备模式(一主一备)、双主模式(互为主备)。
    • 反向代理服务自身也存在性能瓶颈,随着需要转发的请求量不断攀升,需要有 可扩展 方案。

3.4. 网络地址转发

网络地址转发(Network Address Translation,简称 NAT) 是指 通过修改目的 IP 地址实现负载均衡。NAT 属于四层负载均衡。

NAT 工作原理:

  • 用户请求数据包到达 LB 后,LB 在操作系统内核进程获取网络数据包。
  • LB 器根据负载均衡算法得到 RIP,并将请求目的地址修改为 RIP,不需要经过用户进程处理。然后,将请求发送给 RS。
  • RS 处理完成请求后,将响应数据包返回到 LB。
  • LB 再将数据包源地址修改为自身的 IP 地址,发送给用户浏览器。

img

NAT 的 优点:在内核进程完成数据分发,比在应用层分发 性能更好

NAT 的 缺点:所有请求响应都需要经过负载均衡服务器,集群 最大吞吐量受限于负载均衡服务器网卡带宽

网络层负载均衡主流的产品是:

  • LVS 的 NAT 模式。
  • HAProxy 的 TCP 四层负载均衡。

在 NAT 模式下,HAProxy 使用比 LVS 更方便。

3.5. 隧道技术

隧道技术( IP Tunneling,简称 TUN) 是指:采用 NAT 模式时,由于请求和响应的报文必须通过负载均衡器重写地址,当客户请求越来越多时,负载均衡器处理能力将成为瓶颈。为了解决这个问题,负载均衡器把请求的报文通过 IP 隧道转发到真实的服务器。真实的服务器将响应处理后的数据直接返回给客户端。这样负载均衡器就只需处理请求报文。

TUN 工作原理:

  • 客户请求数据包,根据目标地址 VIP 发送到 LB 上。
  • LB 接收到客户请求包,根据负载均衡算法计算出分发的 RS。
  • 然后,进行 IP Tunnel 封装。即在原有的请求包上封装 IP Tunnel 的包头。然后发送出去。
  • RS 根据 IP Tunnel 包头信息(此时就又一种逻辑上的隐形隧道,只有 LB 和 RS 之间懂)收到请求包,然后解开 IP Tunnel 包头信息,得到客户的请求包并进行响应处理。
  • 响应处理完毕之后,RS 使用自己的出公网的线路,将这个响应数据包发送给客户端。源 IP 地址还是 VIP 地址。

3.6. 直接路由

直接路由(Direct Routing,简称 DR) 是指 通过修改目的 MAC 地址实现负载均衡。DR 属于四层负载均衡。

DR 工作原理:

  • 客户请求数据包到达 LB 后,LB 根据调度算法选出一台 RS,将数据帧的 MAC 地址改写为 RS 的 MAC 地址,然后发送出去。
  • 交换机会根据 MAC 地址将数据封包发送给 RS,RS 将处理完的结果直接返回给客户端。

img

数据链路层负载均衡主流的产品是:LVS 的 DR 模式。

DR 的 优点

  • 不需要负载均衡服务器进行地址的转换。
  • 数据响应时不需要经过负载均衡服务器,性能高于 NAT。

DR 的 缺点

  • 对网卡带宽要求较高。
  • 提供服务的端口必须一致。VIP 的端口对外端口为 80,但后端服务的真实端口为 8080,通过 LVS 的 DR 模式无法实现。
  • LVS 和真实服务器必须在同一网络。

3.7. 混合负载均衡

混合负载均衡就是集各家之所长,让不同的负载均衡技术在合适的场景发挥作用。

由于多个服务器群内硬件设备、各自的规模、提供的服务等的差异,可以考虑给每个服务器群采用最合适的负载均衡方式,然后又在这多个服务器群间再一次负载均衡或群集起来以一个整体向外界提供服务(即把这多个服务器群当做一个新的服务器群),从而达到最佳的性能。将这种方式称之为混合型负载均衡。

此种方式有时也用于单台均衡设备的性能不能满足大量连接请求的情况下。是目前大型互联网公司,普遍使用的方式。

方式一,如下图:

img

以上模式适合有动静分离的场景,反向代理服务器(集群)可以起到缓存和动态请求分发的作用,当时静态资源缓存在代理服务器时,则直接返回到浏览器。如果动态页面则请求后面的应用负载均衡(应用集群)。

方式二,如下图:

img

以上模式,适合动态请求场景。

因混合模式,可以根据具体场景,灵活搭配各种方式,以上两种方式仅供参考。

4. 术语

  • LB - Load Balancer,即负载均衡器。
  • RS - Real Server,即真实服务器。
  • RIP - Real Server IP,即真实服务器 IP。
  • VIP - Virtual IP,即虚拟 IP,是外部直接面向用户请求,作为用户请求的目标 IP 地址。
  • DIP - Director Server IP,即直连服务器 IP,主要用于和内部主机通信的 IP 地址。
  • CIP - Client IP,即客户端 IP。
  • NAT - Network Address Translation,即网络地址转发。
  • TUN - IP Tunneling,即 IP 隧道技术。
  • DR - Direct Routing,即直接路由。

5. 参考资料

扩展性架构

扩展性和伸缩性是不同的概念:

  • 扩展性(Extensibility) - 指对现有系统影响最小的情况下,系统功能可持续扩展或提升的能力。表现在系统基础设施稳定不需要经常变更,应用之间较少依赖和耦合,对需求变更可以敏捷响应。它是系统架构设计层面的开闭原则(对扩展开放、对修改关闭),架构设计考虑未来功能扩展,当系统增加新功能时,不需要对现有系统的结构和代码进行修改。
  • 伸缩性(Scalability) - 指系统能够通过增加减少自身资源规模的方式增减自己计算处理事务的能力。如果这种增减是成比例的,就被称作线性伸缩性。在网站架构中 ,通常指利用集群的方式增加服务器数量、提高系统的整体事务吞吐能力。

1. 易扩展的系统架构

低耦合的系统更容易扩展、复用

可扩展架构的核心思想是模块化,并在此基础上,降低模块间的耦合性,提高模块的复用性

分层和分割不仅可以进行架构伸缩,也是模块化设计的重要手段,利用分层和分割的方式将软件分割为若干个低耦合的独立的组件模块,这些组件模块以消息传递及依赖调用的方式聚合成一个完整的系统。

在大型网站中,这些模块通过分布式部署的方式,独立的模块部署在独立的服务器上,从物理上分离模块间的耦合关系,进一步降低耦合性提高复用性。

2. 利用分布式消息队列降低系统耦合性

2.1. 事件驱动架构

事件驱动架构通过在低耦合的模块间传输事件消息,以保持模块的松散耦合,并借助事件消息的通信完成模块间合作。典型的事件驱动架构就是操作系统中常见的生产者消费者模式。在大型网站中,最常见的实现手段就是分布式消息队列。

2.2. 分布式消息队列

消息生产者应用程序通过远程访问接口将消息推送给消息队列服务器,消息队列服务器将消息写入本地内存队列后立即返回成功响应给消息生产者。消息队列服务器根据消息订阅列表查找订阅该消息的消息消费者应用程序,将消息队列中的消息按照先进先出(FIFO)的原则将消息通过远程通信接口发送给消息消费者程序。

在伸缩性方面,由于消息队列服务器上的数据可以看作是即时处理的,因此类似于无状态的服务器,伸缩性设计比较简单。将新服务器加入分布式消息队列集群中,通知生产者服务器更改消息队列服务器列表即可。

在可用性方面,为了避免消费者进程处理缓慢,分布式消息队列服务器内存空间不足造成的问题,如果内存队列已满,会将消息写入磁盘,消息推送模块在将内存队列消息处理完成以后,将磁盘内容加载到内存队列继续处理。

3. 利用分布式服务打造可复用的业务平台

巨无霸系统的问题:

  • 构建、部署困难
  • 代码分支管理困难
  • 数据库连接耗尽
  • 扩展业务困难

而解决巨无霸系统问题的方案就是拆分:

  • 通过纵向拆分将业务拆分多个应用或模块;
  • 通过横向拆分将可复用业务作为独立应用。

然后,需要通过一个分布式服务管理框架将这些应用或服务组织管理起来:通过接口分解系统耦合性,不同子系统通过相同的接口描述进行服务调用。常见的分布式服务管理框架如:Spring Cloud、Dubbo 等。

大型网站分布式服务的需求与特点:

  • 负载均衡
  • 失效转移
  • 高效的远程通信
  • 整合异构系统
  • 对应用最少侵入
  • 版本管理
  • 实时监控

4. 可扩展的数据结构

传统的关系型数据库为了保证关系运算的正确性,在设计数据库表结构的时候,就需要指定表的 schema ——字段名称,数据类型等,并要遵循特定的设计范式。这些规范带来一个问题:难以面对需求变更带来的挑战,所以有人通过预先设计一些冗余字段来应对。

许多 NoSql 数据库使用 ColumnFamily 设计来设计可扩展的数据结构。

5. 开放平台

很多大公司会利用开放平台提供大量开放性 API 使得企业和个人可以方便的接入业务。通过开放平台,可以构建生态圈,提升品牌价值以及竞争力。

开放平台不是一朝一夕完成的,这需要大量 OPEN API 的沉淀。系统架构在设计之初,应该有意识的将未来可能被复用的接口好好设计,以便于需要开放 OPEN API 时,可以便捷的暴露服务接口。

6. 参考资料

高可用架构

1. 网站可用性的度量

网站不可用也被称作网站故障,业界通常用多个 9 来衡量网站的可用性。如 QQ 的可用性为 4 个 9,即 99.99% 可用。

1
2
网站不可用时间 = 故障修复时间点 - 故障发现时间点
网站年度可用性指标 = (1 - 网站不可用时间/年度总时间) * 100%

一般来说:

  • 2 个 9 是基本可用,一年不可用时间小于 88 小时;
  • 3 个 9 是较高可用,一年不可用时间小于 9 小时;
  • 4 个 9 是具有自动恢复能力的高可用,一年不可用时间小于 55 分钟;
  • 5 个 9 是极高可用,一年不可用时间小于 5 分钟。

2. 高可用的系统架构

通常,企业级应用系统为提高系统可用性,会采用较昂贵的软硬件设备,当然这样的设备也比较稳定。

互联网公司或一些初创型公司基于成本考虑,更多采用 PC 级软硬件设备,节约成本所付出的代价就是设备较为不稳定。服务器一年中出现几次宕机,高强度读写磁盘导致磁盘损坏等事件实属正常。

综上,硬件出现故障应视为必然的,而高可用的系统架构设计目标就是要保证当出现硬件故障时,服务依然可用,数据依然能够保存并被访问

实现高可用的系统架构的主要手段是数据和服务的冗余备份及失效转移,一旦某些服务器宕机,就将服务切换到其他可用的服务器上;如果磁盘损坏,则从备份的磁盘读取数据。

3. 高可用的应用

应用层主要处理网站应用的业务逻辑,一个显著的特点是应用的 无状态 性。

所谓的 无状态 的应用是指应用服务器不保存业务的上下文信息,而仅根据每次请求提交的数据进行相应的业务逻辑处理,多个服务实例之间完全对等,请求提交到任意服务器,处理结果都是完全一样的。

3.1. 通过负载均衡进行无状态服务的失效转移

无状态的应用实现高可用架构十分简单,由于服务器不保存请求状态,那么所有服务器完全对等,在任意节点执行同样的请求,结果总是一致的。这种情况下,最简单的高可用方案就是使用负载均衡。

负载均衡,顾名思义,主要使用在业务量和数据量较高的情况下,当单台服务器不足以承担所有的负载压力时,通过负载均衡手段,将流量和数据分摊到一个集群组成的多台服务器上,以提高整体的负载处理能力。

3.2. 应用服务器集群的 Session 管理

应用服务器的高可用架构设计主要基于服务无状态这一特性。事实上,业务总是有状态的,如购物车记录用户的购买信息;用户的登录状态;最新发布的消息等等。

Web 应用中将这些多次请求修改使用的上下文对象称作会话。单机情况下,Session 可由部署在服务器上的 Web 容器管理。

而在集群环境下,Session 管理有以下手段:

3.2.1. Session 复制

Session 复制是指应用服务器开启 Web 容器的 Session 复制功能,在集群中的几台服务器之间同步 Session 对象,使得每台服务器上都保存所有用户的 Session 信息。

这种方案很简单但不可取。因为当集群规模较大时,集群服务间需要大量的通信来进行 Session 复制,占用服务器和网络的大量资源。

3.2.2. Session 绑定

Session 绑定可以利用负载均衡的源地址 Hash 算法实现,负载均衡服务器总是将来源于同一 IP 的请求分发到同一台服务器上。这样在整个会话期间,用户所有的请求都在同一台服务器上处理,即 Session 绑定到某台特定服务器上。这种方法又被称作会话黏滞。

但是这种策略不符合系统高可用的需求,因为一旦某台服务器宕机,那么该机器上的 Session 也就不复存在了。

可以将 Session 记录在客户端(浏览器 Cookie),每次请求服务器时,将 Session 放在请求中发送给服务器,服务器处理完请求后再将修改过的 Session 响应给客户端。

这种策略的缺点是:

  • Cookie 有大小限制,能记录的信息有限;
  • 每次请求响应都需要传输 Cookie,影响性能;
  • 如果用户关闭 Cookie,访问就不能工作。

3.2.4. Session 服务器

利用独立部署的 Session 服务器(集群)统一管理 Session,应用服务器每次读写 Session 时,都访问 Session 服务器。

实现 Session 服务器的一种简单方法时:利用分布式缓存、数据库等,在此基础上进行包装,使其符合 Session 的存储和访问要求。如果业务对 Session 管理有较高要求,如利用 Session 服务集成单点登录(SSO)、用户服务等功能,则需要开发独立的 Session 服务管理平台。

4. 高可用的服务

高可用的服务策略:

  • 分级管理
    • 将服务根据业务重要性进行分级管理,核心应用和服务优先使用更好的硬件,在运维响应速度上也格外迅速。
    • 在服务部署上进行必要的隔离,避免故障的连锁反应。低优先级的服务通过启动不同的线程或部署在不同的虚拟机上进行隔离,而高优先级的服务则需要部署在不同的物理机上,核心服务和数据甚至要部署在不同地域的数据中心。
  • 超时设置 - 由于服务器宕机、线程死锁等原因,可能导致应用程序对服务端的调用失去响应。所以有必要引入超时机制,一旦调用超时,服务化框架抛出异常,应用程序根据服务调度策略,选择重试或请求转移到其他机器上。
  • 异步调用 - 对于需要即时响应的业务,应用在调用服务时可以通过消息队列等异步方式完成,避免一个服务失败导致整个应用请求失败的情况。当然不是所有服务调用都可以异步调用,对于获取用户信息这类调用,采用异步方式会延长响应时间,得不偿失;此外,对于那些必须确认服务调用才能继续下一步操作的应用也不适宜食用异步调用。
  • 服务降级 - 网站访问高峰期,服务可能因为大量并发调用而性能下降,严重时可能会导致宕机。为了保证核心功能的正常运行,需要对服务进行降级。降级有两种手段:
    • 拒绝服务 - 拒绝低优先级应用的调用,减少服务调用并发数,确保核心应用正常使用。或者随机拒绝部分调用,节约资源,避免要死大家一起死的惨剧。
    • 关闭服务 - 关闭部分不重要的服务,或者服务内部关闭部分不重要的功能,以节约资源。
  • 幂等性设计 - 为了避免服务重复调用,可以通过设置编号的方式进行服务调用有效性校验,有效的操作才能继续执行。

5. 高可用的数据

5.1. CAP 原理

分布式系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容忍性(P:Partition Tolerance),最多只能同时满足其中两项。

5.1.1. 可用性

可用性指分布式系统在面对各种异常时可以提供正常服务的能力。可以用系统可用时间占总时间的比值来衡量,4 个 9 的可用性表示系统 99.99% 的时间是可用的。

在可用性条件下,系统提供的服务一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。

5.1.2. 分区容忍性

网络分区指分布式系统中的节点被划分为多个区域,每个区域内部可以通信,但是区域之间无法通信。

在分区容忍性条件下,分布式系统在遇到任何网络分区故障的时候,仍然需要能对外提供一致性和可用性的服务,除非是整个网络环境都发生了故障

5.1.3. 一致性

一致性指的是多个数据副本是否能保持一致的特性

在一致性的条件下,系统在执行数据更新操作之后能够从一致性状态转移到另一个一致性状态。

数据一致性又可以分为以下几点:

  • 强一致性 - 数据更新操作结果和操作响应总是一致的,即操作响应通知更新失败,那么数据一定没有被更新,而不是处于不确定状态。
  • 最终一致性 - 即物理存储的数据可能是不一致的,终端用户访问到的数据可能也是不一致的,但系统经过一段时间的自我修复和修正,数据最终会达到一致。

5.1.4. 权衡

在分布式系统中,分区容忍性必不可少,因为需要总是假设网络是不可靠的。因此,CAP 理论实际在是要在可用性和一致性之间做权衡。

可用性和一致性往往是冲突的,很难都使它们同时满足。在多个节点之间进行数据同步时,

  • 为了保证一致性(CP),就需要让所有节点下线成为不可用的状态,等待同步完成;
  • 为了保证可用性(AP),在同步过程中允许读取所有节点的数据,但是数据可能不一致。

5.2. 数据备份

  • 冷备份 - 定期将数据复制到某种存储介质。
  • 热备份
    • 异步热备方式 - 异步热备方式是指多份数据副本的写入操作异步完成,应用程序收到数据服务系统的写操作成功响应时,只写成功了一份,存储系统将会异步地写其他副本。
    • 同步热备方式 - 同步热备方式是指多份数据副本的写入操作同步完成,即应用程序收到数据服务系统的写成功响应时,多份数据都已经写操作成功。但是当应用程序收到数据写操作失败的响应式,可能有部分副本或者全部副本都已经写入成功了(因为网络或者系统故障,无法返回操作成功的响应)。

5.3. 失效转移

5.3.1. 失效确认

判断服务器宕机的手段有两种:心跳检测访问失败报告

对于应用程序的访问失败报告,控制中心还需要再一次发送心跳检测进行确认,以免错误判断服务器宕机。因为一旦进行数据访问的失效转移,意味着数据存储多份副本不一致,需要进行后续一系列的复杂动作。

5.3.2. 访问转移

确认某台数据服务器宕机后,就需要将数据读写访问重新路由到其他服务器上。对于完全对等存储的服务器,当其中一台宕机后,应用程序根据配置直接切换到对等服务器上。如果存储不对等,就需要重新计算路由,选择存储服务器。

5.3.3. 数据恢复

因为某台服务器宕机,所以数据存储的副本数目会减少,必须将副本的数目恢复到系统设定的值,否则,再有服务器宕机时,就可能出现无法访问转移,数据永久丢失的情况。因此系统需要从健康的服务器复制数据,将数据副本数目恢复到设定值。

6. 高可用的软件质量保证

高可用的软件质量保证的手段:

  • 自动化发布
  • 自动化测试
  • 预发布验证
  • 代码控制
  • 灰度发布

7. 系统监控

不允许没有监控的系统上线。

  • 监控数据采集
    • 用户行为日志收集
      • 服务端日志收集 - Apache、Nginx 等几乎所有 Web 服务器都具备日志记录功能,只要开启日志记录即可。如果是服务器比较多,需要集中采集日志,通常会使用 Elastic 来进行收集。
      • 客户端日志收集 - 利用页面嵌入专门的 JavaScript 脚本可以收集用户真实的操作行为。
      • 日志分析 - 可以利用 ElasticSearch 做语义分析及搜索;利用实时计算框架 Storm、Flink 等开发日志统计与分析工具。
    • 服务器性能监控 - 收集服务器性能指标,如系统负载、内存占用、CPU 占用、磁盘 IO、网络 IO 等。常用的监控工具有:Apache SkyWalking 等。
    • 运行数据报告 - 应该监控一些与具体业务场景相关的技术和业务指标,如:缓存命中率、平均响应时延、TPS、QPS 等。
  • 监控管理
    • 系统报警 - 设置阈值。当达到阈值,及时触发告警(短信、邮件、通信工具均可),通过及时判断状况,防患于未然。
    • 失效转移 - 监控系统可以在发现故障的情况下主动通知应用进行失效转移。
    • 自动优雅降级
      • 优雅降级是为了应付突然爆发的访问高峰,主动关闭部分功能,释放部分资源,以保证核心功能的优先访问。
      • 系统在监控管理基础之上实现自动优雅降级,是柔性架构的理想状态。

8. 资料