本文由 简悦 SimpRead 转码, 原文地址 juejin.cn
第五届字节跳动青训营讲师非常用心给大家整理了课前、中、后的学习内容,同学们自我评估,选择性查漏补缺,便于大家更好的跟上讲师们的节奏,祝大家学习愉快,多多提问交流~
规则引擎是一种嵌入在应用服务中的组件,可以将灵活多变的业务决策从服务代码中分离出来。通过使用预定义的语义模块来编写业务逻辑规则。在执行时接受数据输入、解释业务规则,并做出决策。规则引擎能大大提高系统的灵活性和扩展性。
在字节跳动,规则引擎已经在风控识别、活动运营、配置下发等场景得到了广泛的应用。开发人员可以将业务逻辑与服务代码解耦,实现灵活、高效的业务策略发布。目前公司内部基于规则引擎的动态决策系统已经承接了千万级别 QPS 的决策请求。
规则引擎的实现需要在满足大容量、高请求、低延迟的基础上尽可能做到简单易上手。本次课程将会带领大家实现一个简单版的规则引擎。
- 了解规则引擎的组成部分和应用场景。
- 学习并掌握规则引擎的设计与实现原理。
- 明确一个规则引擎的设计目标,并完成各部分的设计与实现步骤拆解。
- 动手实现规则引擎项目,完成预定目标。
- [课外扩展] 结合其他课程,完成一个在线 规则引擎 服务。
重点
- 规则引擎的设计 。明确设计目标、完成步骤拆解、完成各部分状态机的详细设计
- 规则引擎的实现。基于项目工程完成词法分析、语法分析、抽象语法树的执行功能
难点
- 规则引擎的核心原理(理论)。词法分析、语法分析、类型检查、语法树执行
主要涉及到编译原理的部分
课前必看!!!
本部分是需要大家在上课之前了解的内容,主要是一些基本的概念和原理。
在这门课程之前你可能根本没有听说过规则引擎这个东西,当然也可能是浅浅的大概知道这是个什么东西,或者是个规则引擎方面的资深专家(还没毕业,五年工作经验那种🐶,如果是这样请赶紧找我内推)。都没有关系,这门课包教包会!!!(学不会的下课后可以找我们运营人员联系我一对一教学)
当然,这门课程还是有一定的门槛的,这也就是我为什么要说这么多一定要让你仔细看看这部分的原因。经过实验,课程的内容如果只依赖于课上老师的讲解,只能做到:能听懂,能跟上,来不及思考。要想能够理解掌握这部分内容,能跟别人 battle 下,再向自己的知识山峰上加那么一块小石头,得好好预习。
开始之前先百度或者 Google 一下 “规则引擎” 简单浏览下哈,📪📪📪另外掘金 app 上面也有许多不错的文章。可以先浏览看看。
数据结构得学过吧,考多少分?😁
这块的预习目标呢,包括以下几个部分
- 精通常用数据结构:数组、结构体、指针、队列、二叉树 等等等,课本上有的都得看看
- 熟练掌握二叉树的各种遍历方式:前中后序遍历,层序遍历,打印二叉树,有时间可以自己写几个小 demo,当然最基础的是需要知道各种遍历方式的区别
- 掌握 Go 语言的基础语法,能读懂项目代码
是的,就这一个要求,其实学完青训营的前几节课就可以达到了
编译原理被誉为 "程序员的三大浪漫" 之一,足以可见这块知识的深度与广度,我们这次课程也是简单的介绍一下与规则引擎相关的概念。
那么可能会有疑问了,不是讲规则引擎么?为啥还得学编译原理?
规则引擎的本质呢就是我们自己定义一套语法,然后去解析用这套语法写的表达式,然后根据解析的内容执行表达式。这个过程其实就是编译和执行的过程。
因此呢需要自行了解以下的内容
-
编译的概念:
- 编译的过程发生了什么?
- 一般分为哪几个步骤,每个步骤的中间结果是什么?
-
词法分析:
- 词法如何表示?| 正则文法
- 词法分析阶段的输出是什么
- 词法分析阶段是怎么做的?
- 词法分析可能会产生什么问题?
- 如何解决词法分析过程中产生的问题?| 左递归问题怎么解决
-
语法分析
- 语法如何表示?上下文无关语法、巴克斯范式怎么理解
- 语法分析阶段的输出是什么? 一般怎么表示
- 语法分析有哪些方式?什么是递归下降算法?
-
抽象语法树
- 抽象语法树是什么?
- 抽象语法树如何执行?
-
类型检查
- 类型检查怎么做?有哪些方式?
- 类型检查什么时候做?有什么区别?
课程之前,大家需要根据项目工程,来完成环境的搭建和 Demo 的运行
项目地址:
相信大家已经完成了 Go 环境的搭建,项目工程依赖了 hertz 框架,如果在之前的课程中完成了项目环境搭建可以直接复用。
项目环境:
- go 语言环境搭建
- 需要安装 docker 环境
- 安装 docker-compose 工具
项目 clone 到本地后,可以执行测试脚本来测试环境的可用性。如果有错误欢迎百度和 Google 解决
git clone https://github.com/qimengxingyuan/young_engine.git
chmod a+x ./setup.sh
./setup.sh
复制代码
脚本执行成功,则环境可以支持项目的执行
项目说明:
本项目是一个简单的规则引擎的实现,详细目录可以参考 README.md
项目实现时间有限,没有做比较完备的测试,如果在 demo 执行的过程中出现某些 bug 或者执行异常可以直接在 github 提交 issue 或者修复后提起 PR
编译的过程就是 把某种语言的源程序,在不改变语义的条件下,转换成另一种语言程序 (目标语言程序)
- 如果源代码编译后要在操作系统上运行,那目标代码就是汇编 / 机器代码。
- 如果编译后是在虚拟机里执行,那目标代码就可以不是汇编代码,而是一种解释器可以理解的中间形式的代码即可。
解释型语言和编译型语言
-
有的语言提前把代码一次性转换完毕,这种就是编译型语言,用的转换工具就叫编译器,比如 C、C++、Go。一次编译可重复执行
- 编译后产物不能跨平台,不同系统对可执行文件的要求不同。.exe
- 特殊的,c、c++、汇编、源代码也不能跨平台
-
有的语言则可以一边执行一边转化,用到哪里了就转哪里,这种就是解释性语言,用的转化工具叫虚拟机或者解释器,比如 java python、javascript
关于 Java 和 Python .
- Java 既有编译又有解释。但是编译并没有直接编译成机器码,而是编译成字节码,然后再放到虚拟机中执行。
- Python 执行过程也是经过两个阶段,先编译成字节码 .pyc 再放到虚拟机中去执行
JVM 和 Python 解释器 | 为什么一个叫虚拟机一个叫解释器
- “虚拟机”对二进制字节码进行解释,而 “解释器” 是对程序文本进行解释。
- 从历史上看,Java 是为解释二进制字节码而设计和实现的,而 Python 最初是为解释程序文本而设计和实现的。因此,“Java 虚拟机” 这个术语在 Java 社区中具有历史意义并且非常成熟,“Python 解释器” 这个术语在 Python 社区中具有历史意义并且非常成熟。人们倾向于延长传统并使用很久以前使用的相同术语。
- 对于 Java,二进制字节码解释是程序执行的主要方式,而 JIT 编译只是一种可选的和透明的优化。而对于 Python,目前,程序文本解释是 Python 程序执行的主要方式,而编译成 Python VM 字节码只是一种可选的透明优化。
把源代码字符串转换为词法单元 (Token) 的这个过程。
确定的有限自动机 DFA | Deterministic Finite Automaton
确定的有限自动机就是一个状态机,它的状态数量是有限的。该状态机在任何一个状态,基于输入的字符,都能做一个确定的状态转换。
词法分析是识别一个个的单词,而语法分析就是在词法分析的基础上识别出程序的语法结构。这个结构是一个树状结构。这棵树叫做抽象语法树(Abstract Syntax Tree,AST)。树的每个节点(子树)是一个语法单元,这个单元的构成规则就叫 “语法”。每个节点还可以有下级节点。
Token -> AST
上下文无关语法 Context-Free Grammar
语言句子无需考虑上下文,就可以判断正确性
... a = 0; ... 这是一个赋值语句,无论此语句的前后是什么代码,此语句所代表的操作是确定的。即给变量a赋值等于0 复制代码
编程语言为什么不用人类的语言(自然语言),而是用上下文无关的文法呢? 因为
- 便于设计编译器。 客观上技术目前无法实现,如果使用了上下文相关文法,那就是真正实现了人工智能,NLP 领域将会有重大突破。
- 便于代码开发维护。 如果开发出来的代码像高考的语文阅读理解一样,每个人都有不同的理解,那么,到底哪个才是作者真正想要表达的?如果人类都确定不了含义,那计算机同样也确定不了,最终结果就是错误执行或无法执行。
- 汇编语言 / 机器语言是上下文无关的。CPU 执行指令时,读到哪条执行哪条。如果 CPU 需要考虑上下文,来决定一个语句到底要做什么,那么 CPU 执行一条语句会比现在慢千倍万倍。考虑上下文的事情,完全可以用户在编程的时候用算法实现。既然机器语言是上下文无关的,那高级语言也基本上是上下文无关的,可能有某些个别语法为了方便使用,设计成了上下文相关的,比如脚本语言的弱类型。在便于使用的同时,增加了解析器的复杂度。
上下文无关语法 G:终结符集合 T + 非终结符集合 N + 产生式集合 P + 起始符号 S
G 由 T、N、S 和 P 组成,由语法 G 推导出来的所有句子的集合称为 G 语言!
终结符: 组成串的基本符号。可以理解为词法分析器产生的 token 集合。比如 +
Id
(
)
等
非终结符: 表示 token 的的集合的语法变量。比如 stmt
varDecl
等等
start:blockStmts ; //起始
block : '{' blockStmts '}' ; //语句块
blockStmts : stmt* ; //语句块中的语句
stmt = varDecl | expStmt | returnStmt | block; //语句
varDecl : type Id varInitializer? ';' ; //变量声明
type : Int | Long ; //类型
varInitializer : '=' exp ; //变量初始化
expStmt : exp ';' ; //表达式语句
returnStmt : Return exp ';' ; //return语句
exp : add ; //表达式
add : add '+' mul | mul; //加法表达式
mul : mul '*' pri | pri; //乘法表达式
pri : IntLiteral | Id | '(' exp ')' ; //基础表达式
复制代码
产生式:表示形式,S : AB ,就是说 S 的含义可以用语法 AB 进行表达
S : AB
A : aA | ε
B : b | bB
复制代码
展开
(expand):将 P(A->u ) 应用到符号串 vAw 中,得到新串 v_u_ **w
折叠
(reduce):将 P(A->uu ) 应用到符号串 v_uu_ w 中,得到新串 vAw
推导
(derivate):符号串 u 应用一系列产生式,变成符号串 v ,则 u =>v:S => ab | b | bb
巴科斯范式
BNF 是描述上下文无关理论的一种具体方法,通过 BNF 可以实现上下文无关文法的具体化、公式化、科学化,是实现代码解析的必要条件。
<expr> ::= <expr> + <term>
| <expr> - <term>
| <term>
<term> ::= <term> * <factor>
| <term> / <factor>
| <factor>
<factor> ::= ( <expr> )
| Num
复制代码
BNF 本质上就是树形分解,分解成一棵抽象语法树
- 每个产生式就是一个子树,在写编译器时,每个子树对应一个解析函数。
- 叶子节点叫做 终结符,非叶子节点叫做 非终结符。
递归下降算法 Recursive Descent Parsing
基本思路就是按照语法规则去匹配 Token 串。比如说,变量声明语句的规则如下:
varDecl : types Id varInitializer? ';' ; //变量声明
varInitializer : '=' exp ; //变量初始化
exp : add ; //表达式
add : add '+' mul | mul; //加法表达式
mul : mul '*' pri | pri; //乘法表达式
pri : IntLiteral | Id | '(' exp ')' ; //基础表达式
复制代码
如果写成产生式格式,是下面这样:
varDecl -> types Id varInitializer ';'
varInitializer -> '=' exp
varInitializer -> ε
exp -> add
add -> add + mul
add -> mul
mul -> mul * pri
mul -> pri
pri -> IntLiteral
pri -> Id
pri -> ( exp )
复制代码
而基于这个规则做解析的算法如下:
匹配一个数据类型(types)
匹配一个标识符(Id),作为变量名称
匹配初始化部分(varInitializer),而这会导致下降一层,使用一个新的语法规则:
匹配一个等号
匹配一个表达式(在这个步骤会导致多层下降:exp->add->mul->pri->IntLiteral)
创建一个varInitializer对应的AST节点并返回
如果没有成功地匹配初始化部分,则回溯,匹配ε,也就是没有初始化部分。
匹配一个分号
创建一个varDecl对应的AST节点并返回
复制代码
int a = 2
-
对于一个非终结符,要从左到右依次匹配其产生式中的每个项,包括非终结符和终结符。
-
在匹配产生式右边的非终结符时,要下降一层,继续匹配该非终结符的产生式。
-
如果一个语法规则有多个可选的产生式,那么只要有一个产生式匹配成功就行。如果一个产生式匹配不成功,那就回退回来,尝试另一个产生式。这种回退过程,叫做回溯(Backtracking)。
课上我们重点讲了规则引擎的设计和实现,结合前面课程的内容课后实现一个在线版本的规则引擎
使用 Hertz 框架开发一个 HTTP 服务,服务使用 mysql,支持表达式的增删查改和编译执行。
并实现以下接口
请求参数为待执行的表达式和表达式中参数的值,并输出编译结果
实时编译并执行结果,不需要写入 DB 中
POST api/engine/run
- Request
{
"exp": "uid == 12345 && did > 0",
"params": {
"uid": 123456,
"did": 0
}
}
复制代码
- Response
{
"code": 0,
"message": "success",
"data": { // 执行结果
"result": true
}
}
复制代码
新增一条表达式到 DB 中,并返回表达式在 DB 中的 ID
需要检测表达式是否已经存在,如果已经存在,直接返回表达式的 ID
需要检测表达式是否合法 (编译是否通过) ,如果编译失败,返回错误码 20001
和编译错误
POST api/engine/exp/new
- Request
{
"exp": "uid == 12345 && did > 0",
}
复制代码
- Response
{
"code": 0,
"message": "success",
"data": { // 表达式ID
"id": 1
}
}
// 编译失败时
{
"code": -1,
"message": "compile error: xxxxx", // 编译失败的信息
"data": { // 表达式ID
"id": 0
}
}
复制代码
查询数据库中所有的表达式
GET api/engine/exp/list
- Response
{
"code": 0,
"message": "success",
"data": [
{
"id": 1,
"exp": "uid > 0"
}
]
}
复制代码
根据 ID 删除表达式,表达式不存在时返回错误码20002
, 和错误信息
删除成功返回被删除的表达式信息
DELETE api/engine/exp/:id
- Response
// 删除成功时
{
"code": 0,
"message": "success",
"data": { // 表达式ID
"id": 1,
"exp": "uid > 0"
}
}
// 删除失败时
{
"code": -1,
"message": "exp id 1 not exist", //查询失败的信息
"data": {}
}
复制代码
根据表达式的 ID,查询出表达式内容,并编译执行。表达式不存在时返回错误码20002
, 和错误信息
POST api/engine/exp/run
- Request
{
"exp_id": 1,
"parmas": {
"uid": 123456,
"did": 0
}
}
复制代码
- Response
{
"code": 0,
"message": "success",
"data": { // 执行结果
"result": true
}
}
// 表达式不存在时
{
"code": -1,
"message": "exp id 1 not exist", //查询失败的信息
"data": {}
}
复制代码
为了帮助同学们更好地理解本课程,我为大家准备了本学员手册。它包含以下几大模块内容:
- 课程目标,本课程主要框架的简单介绍,便于同学们抓住课程的框架结构,把握听课节奏;
- 课前,本课程的重要前置知识点,便于同学们在听课过程中快速理解、跟紧思路;
- 课中,本课程各章节涉及的关键概念和知识点,帮助同学们加深核心内容的理解和认识;
- 课后,本课程的内容提炼,便于同学们总结课程要点,争取达到举一反三的效果。
本课程的包含以下四个方面:
-
什么是架构
- 围绕架构的定义和演进两部分内容展开
-
企业级后端架构剖析
- 详细介绍企业级后端架构的形态
-
企业级后端架构的挑战
- 企业级架构都面临着哪些挑战,如何解决
-
后端架构实战
- 结合前三部分的知识点,以第三部分中的一个挑战为例,讲解如何做架构设计
常见软件架构:
- 单机
- 单体
- 垂直应用
- SOA (Service Oriented Architecture)
- 微服务 (Microservice)
一些小问题:
- 如何给架构下定义?
- 架构的重要性?
- 架构演进的初衷?
- 架构演进的思路?
-
云计算
-
基础
- 虚拟化
- 编排
-
架构
- IaaS
- SaaS
- PaaS
- FaaS
-
-
云原生
-
弹性资源
- 计算资源
- 存储资源
-
微服务架构
- 通信协议
- 中间件
-
DevOps
- 软件生命周期
-
服务网格
-
- 离线任务
- 在线任务
- IO 密集型
- CPU 密集型
- 服务治理
- IPC (Inter-Process Communication)
- RPC (Remote Procedure Call)
- 负载均衡 Load Balancing
- 服务发现 Service Discovery
- 服务注册 Service Registry
- 宿主机 Host
- 容器 Container
- 时序数据 Time Series
- 一致性哈希 Consistent Hash
-
软件架构演进至今都有哪些形态?它们分别解决了什么问题?仍然存在什么问题?
-
云计算有哪些基础技术?云计算服务的形态又有哪些?
-
云原生是什么?它跟云计算的关系是?
-
云原生的代表技术有哪些?
-
企业级后端架构面临的挑战有哪些?
Q:如何给架构下定义?
A:架构,又称软件架构:
-
是有关软件整体结构与组件的抽象描述
-
用于指导软件系统各个方面的设计
Q:架构的重要性?
A:那盖房子来做举例子。
我们都知道,地基对于一栋楼房的主要性,架构对于一个软件的重要性也是类似的:
- 架构没设计好,软件容易崩,用户体验上不去。最终要么重构,要么放弃
- 架构设计好了,软件的稳定性上去了,用户体验高了,口碑一点点就打造出来了
- 良好的架构基础,也为软件的未来发展提供了更多的可能。为用户赋能,实现自身价值
All in one,所有的东西都在一个进程里,部署在一个机器上。
优点:
- 简单
缺点:
- 运维需要停服,用户体验较差
- 承载能力有限。了解下 c10k 问题
在单机架构的基础上,将进程部署到多个机器上。
优点:
- 具备水平扩容能力
- 运维不需要停服
缺点:
- 后端进程职责太多,越来越臃肿
- 爆炸半径较大,进程中一个很小的模块出现问题,都可能导致整个进程崩溃
在单机架构基础上,将进程按照某种依据切分开。比如,A 软件和 B 软件的后端原先采用单机架构部署,那就是一个进程部署在多个机器上;如果用垂直应用架构,可以将 A 和 B 的后端拆分为 A、B 两个进程,然后再按照单体模式的思路,部署在多个机器上。
优点:
- �一定程度上减少了后端进程职责
- 一定程度上缩小爆炸半径
缺点:
- 没有根本解决单体架构的问题
SOA 架构中,服务为一等公民,将进程按照不同的功能单元进行抽象,拆分为『服务』。有了服务之后,SOA 还为服务之间的通信定义了标准,保证各个服务之间通讯体验的一致性。
优点:
- 各服务的职责更清晰
- 运维粒度减小到服务,爆炸半径可控
缺点:
- ESB (企业服务总线) 往往需要一整套解决方案
在 SOA 架构中,ESB 起到了至关重要的作用。但从架构拓扑来看,它更像是一个集中式的模块。有一个 SOA 分布式演进的分支,最终的形态便是微服务。
优点:
- 兼具 SOA 解决的问题
- 服务间的通信更敏捷、灵活
缺点:
- 运维成本
- 架构演进的初衷:满足软件迭代诉求,提高迭代效率
- 架构演进的思路:垂直切分——分布式,水平切分——分层 / 模块化
云计算基础:
-
虚拟化技术
- 硬件层面(VM 虚拟机)- KVM/Xen/VMware
- 操作系统层面(Container 容器)- LCX/Docker/Kata Container
- 网络层面 - Linux Bridge/Open v Switch
-
编排方案
- VM - OpenStack/VMWare Workstation
- Container - Kubernetes/Docker Swarm
云计算架构:
-
云服务
- IaaS - 云基础设施,对底层硬件资源池的抽象
- PaaS - 基于资源池抽象,对上层提供的弹性资源平台
- SaaS - 基于弹性资源平台构建的云服务
- FaaS - 更轻量级的函数服务。好比 LeetCode 等 OJ,刷题时只需要实现函数,不需要关注输入输出流
-
云部署模式(拓展)
- 私有云 - 企业自用
- 公有云 - AWS/Azure/Google Cloud/Huawei
- 混合云
云原生,实际是云原生(计算)的简称,它是云计算发展到现在的一种形态。
云原生技术为组织(公司)在公有云、自由云、混合云等新型的动态环境中,构建和运行可弹性拓展的应用提供了可能。 它的代表技术:
- 弹性资源
- 微服务架构
- DevOps
- 服务网格
基于虚拟化技术,提供的可以快速扩缩容的能力。可以分为弹性计算资源和弹性存储资源两个方面。
弹性计算资源:
-
计算资源调度
- 在线计算 - 互联网后端服务
- 离线计算 - 大数据分析。Map-Reduce/Spark/Flinnk
-
消息队列
-
在线队列 - 削峰、解耦
-
离线队列 - 结合数据分析的一整套方案,如 ELK
-
弹性存储资源:
-
经典存储
- 对象存储 - 视频、图片等。结合 CDN 等技术,可以为应用提供丰富的多媒体能力
- 大数据存储 - 应用日志、用户数据等。结合数据挖掘、机器学习等技术,提高应用的体验
-
关系型数据库
-
元数据
- 服务发现
-
NoSQL
-
KV 存储 - Redis
-
文档存储 - Mongo
-
在云原生的大背景下,不论是计算资源还是存储资源,他们都像是服务一样供用户使用。
微服务架构下,服务之间的通讯标准是基于协议而不是 ESB 的。
-
HTTP - H1/H2
-
RPC - Apache Thrift/gRPC
如何在 HTTP 和 RPC 之间选择?
- 性能 - RPC 协议往往具备较好的压缩率,性能较高。如 Thrift, Protocol Buffers
- 服务治理 - RPC 中间件往往集成了丰富的服务治理能力。如 熔断、降级、超时等
- 可解释性 - HTTP 通信的协议往往首选 JSON,可解释性、可调试性更好
什么是服务网格?
-
微服务之间通讯的中间层
-
一个高性能的 4 层网络代理
-
将流量层面的逻辑与业务进程解耦
没有什么是加一层代理解决不了的问题,服务网格相比较于 RPC/HTTP 框架:
- 实现了异构系统治理体验的统一化
- 服务网格的数据平面代理与业务进程采取进程间通信的模式,使得流量相关的逻辑(包含治理)与业务进程解耦,生命周期也更容易管理
基础设施层面:
Q:我们总说,云是弹性的,也就是说,在用户的角度,云提供的资源是无限的。然而,云背后的物理资源是有限的。在企业级后端架构里,云如何解决近乎无限的弹性资源和有限的物理资源之间的矛盾?
Q:闲事的资源就这么空着呢?如何提高资源利用率,提高物理资源的价值转换率?
用户层面:
Q:上了云原生微服务后,服务之间的通信开销较大,应该如何做成本优化?
Q:微服务看起来没有那么美好,抖动导致的运维成本较高,如何解决?
Q:异构的物理环境应该对用户是透明的,如何屏蔽这些细节?
考虑到在线业务的潮汐性,物理资源的用量不是一成不变的。离在线资源并池,可以:
- 提高物理资源利用率
- 提供更多的弹性资源
微服务之间的通信成本较高,是否可以:
-
形态上是微服务架构
-
通信上是单体架构
亲合性部署,通过将微服务调用形态与资源调度系统结合,将一些调用关系紧密、通信量大的服务部署在同一个机器上,并且使用 IPC 代替 RPC 的方式,降低网络通信带来的开销
Q:微服务之间的通信流量为什么需要治理?
Q:都有哪些常用的治理手段?
Q:微服务中心件和服务网格在其中扮演着怎样的角色?
Q:基础设施层往往是个复杂的异构环境,比如,有些机器的 CPU 是英特尔的,而有些是 AMD 的。就算是同一个品牌,也可能是不同代际。如何将这些差异屏蔽掉,使用户尽可能不感知呢?
Q:什么情况下,我们觉得,服务需要扩容了?异构环境会对这个评判标准产生怎样的影响?
如何设计一个根据主机层面的资源信息,实时进行流量调度的系统,打平不同宿主机异构环境的算力差异。
关键点:
- 紧急回滚能力
- 大规模
- 极端场景
设计需求:
-
多端支持
- 微信 / 支付宝小程序
- App
- 网页
-
使用云原生基础设施
-
用户画像很重要
-
积极参加妇女节 / 光棍节等活动
-
没有最好的架构,只有最合适的架构
-
做架构设计
- 先从需求出发。要满足什么样的需求?预期规模有多大?
- 做足够的业界调研。业界对于类似的需求是怎么做的?有无成熟的方案可以借鉴?直接拿来用有什么问题?
- 技术选型。涉及的技术组件是自研,还是使用开源的?
- 异常情况。任何时候,都不能做『输入合法』的假设。容灾能力一定要有
-
学好架构,是工程师成长的一个重要标志
本节课程主要分为 6 个方面:
- 概述
- 系统模型
- 理论基础
- 分布式事务
- 共识协议
- 分布式实践
课前部分主要罗列课程中涉及到的概念。对于不熟悉的概念,同学们可以提前查询预习;课中部分主要罗列每一部分的关键思路,帮助同学们跟上课程的进度;课后部分是一些问题,帮助同学们在课后梳理本课程的重点。
- 什么是分布式?
- Why-How-What
- 常见的分布式系统
- 故障模型
- 拜占庭将军问题
- 共识和一致性
- 时间和事件顺序
- CAP 理论
- ACID 理论
- BASE 理论
- 两阶段提交
- 三阶段提交
- MVCC
- Quorum NWR 模型
- RAFT 协议
- Paxos 协议
- MapReduce
- 分布式 KV
-
什么是分布式?
- 分布式系统定义:跨多个节点的计算机程序的集合
- 使用分布式系统的五大优势:去中心化、低成本、弹性、资源共享、可靠性高
- 分布式系统的挑战:故障、网络、环境、安全
-
Why-How-What
- 使用者视角:大规模计算存储的述求
- 学习者视角:后端开发必备技能
-
常见的分布式系统
- 分布式存储:GFS、Ceph、HDFS、Zookeeper
- 分布式数据库:Spanner、TiDB、HBase、MangoDB
- 分布式计算:Hadoop、YARN、Spark
-
六种故障模型,从处理的难易程度分类
- Byzantine failure:节点可以任意篡改发送给其他节点的数据,是最难处理的故障
- Authentication detectable byzantine failure (ADB):节点可以篡改数据,但不能伪造其他节点的数据
- Performance failure:节点未在特定时间段内收到数据,即时间太早或太晚
- Omission failure:节点收到数据的时间无限晚,即收不到数据
- Crash failure:节点停止响应,持续性的故障
- Fail-stop failure:错误可检测,是最容易处理的故障
-
故障模型举例,按照模型分类
- 磁盘、主板、交换机、网络分区、cpu、内存、线缆、电源等故障详细说明
-
两将军问题
-
定义:
- 两支军队的将军只能派信使穿越敌方领土互相通信,以此约定进攻时间。该问题希望求解如何在两名将军派出的任何信使都可能被俘虏的情况下,就进攻时间达成共识
-
结论:
- 两将军问题是被证实无解的电脑通信问题,两支军队理论上永远无法达成共识
-
TCP 是两将军问题的一个工程解
-
-
三将军问题:
- 两个 “忠将”A 和 B,一个 “叛徒”C,互相传递消息,消息可能丢失,也可能被篡改,当有一个将军是 “叛徒”(即出现拜占庭故障)时,整个系统无法达成一致。
- 由于 “叛徒”C 的存在,将军 A 和将军 B 获得不同的信息。这样将军 A 获得 2 票进攻 1 票撤退的信息,将军 B 获得 1 票进攻 2 票撤退的信息,产生了不一致
-
四将军问题:
-
将军 D 作为消息分发中枢,约定如果没收到消息则执行撤退
-
步骤:
- 如果 D 为 “叛徒”,ABC 无论收到任何消息,总能达成一致
- D 为 “忠将”,ABC 有 2 人将 D 的消息进行正确的传递,同样能保证最终决策符合大多数。
-
进而能够证明,当有 3m+1 个将军,m 个 “叛徒” 时,可以进行 m 轮协商,最终达成一致
-
- 不同客户端 A 和 B 看到客户端 C 写入,因为时机的不同,产生数据读取的偏差。引导出最终一致性的详细说明
- 要保证所有客户端看到相同的值,需要多节点进行 “协商”,达成共识,来保证线性一致性
- 一致性和可用性是对矛盾
-
1978 年 Leslie Lamport 发表《Time, Clocks, and the Ordering of Events in a Distributed System》
- 定义了计算机系统中的时间和事件顺序,引入 happened before 和并发的定义,可以以此对分布式系统中的事件进行推导
- 根据上述推导,创造了 Lamport 逻辑时钟的概念,这个概念在分布式理论中具有革命性的意义,帮助我们在一系列分布式事件当中梳理出逻辑的先后关系。利用逻辑时钟,我们可以对整个系统中的事件进行全序排序
-
CAP 的定义,分别代表一致性、可用性、分区容错性。三者无法同时达到
-
CAP 诞生了三类系统:
- CA 系统:传统数据库的代表
- AP 系统:放弃强一致性,保证高可用,不少 nosql 存储系统采用
- CP 系统:放弃可用性,保证数据一致性
-
举例说明两个分布式进程之间同步数据,当出现故障的时候,如何选择不同的 CAP 系统,以及带来的影响
- CP 系统:故障发生时,为了避免读到不一致的数据,可能拒绝访问
- AP 系统:故障发生时,为了保证可用性,允许不同进程读到不同的数据
-
针对故障场景,可以通过故障转移的方式,做一个相对较优的解决方式:
- 允许一个进程作为 Master,其他进程作为 Backup,当故障时将请求转移给 Backup 进行处理
- ACID 理论是针对 CA 系统而言的,通常在数据库中具有广泛意义
- 事务是数据库系统中非常重要的概念,它是数据库管理系统执行过程中的一个逻辑单元,它能够保证一个事务中的所有操作要么全部执行,要么全都不执行
- 数据库事务拥有四个特性 ACID:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)
-
BASE 理论是针对 AP 系统而言的,其来源于对大型互联网分布式实践的总结
- Basically Available(基本可用):假设系统,出现了不可预知的故障,但还是能用
- Soft state(软状态):允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性
- Eventually consistent(最终一致性):数据最终一定能够达到一致的状态
-
定义:
- 二阶段提交(Two-phase Commit):为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种演算法。
-
三个假设:
- 协调者和参与者进行通信
- 预写式日志被保持在可靠的存储设备上
- 所有节点不会永久性损坏,即使损坏后仍然可以恢复
-
正常流程:Prepare 阶段和 Commit 阶段
-
异常流程:Prepare 阶段失败 -> 回滚;协调者宕机 -> 重新启用新的协调者;双故障重启 -> 数据库管理员介入
-
两阶段提交需解决的问题:
- 性能问题:需要多次网络通信,资源需要等待并锁定
- 新协调者:如何确定状态选出新协调者
- Commit 阶段网络分区带来的数据不一致:非所有节点都收到 Commit 请求
-
两个思考:
- 日志被保存在「可靠」的存储设备上。如何保证这一点?
- 参与者 Commit 了,但 Ack 信息协调者没收到。怎么办?
- 针对两阶段提交的补充,将两阶段提交中的 Prepare 阶段,拆成两部分:CanCommit 和 PreCommit 机制
- CanCommit 阶段:询问是否可以执行;PreCommit 阶段:重新确认是否可以执行
- DoCommit 阶段:向所有人提交事务
-
MVCC:多版本并发控制的方法。维持一个数据的多个版本使读写操作没有冲突。所以既不会阻塞写,也不阻塞读。提高并发性能的同时也解决了脏读的问题。
-
悲观锁和乐观锁
- 悲观锁:操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据
- 乐观锁:不会上锁,只是在执行更新时判断别人是否修改数据,只有冲突时才放弃操作
-
版本的选取:使用物理时钟或逻辑时钟
- 物理时钟:提供 TrueTime API,有 Master 节点维持一个绝对时间,保证各个服务器之间时钟误差控制在ϵ内,通常ϵ<7ms。
- 逻辑时钟:中心化授时的方式 -- 时间戳预言机(TSO),好处是无需硬件的支持
-
三要素:
- N:在分布式存储系统中,有多少份备份数据
- W:代表一次成功的更新操作要求至少有 w 份数据写入成功
- R: 代表一次成功的读数据操作要求至少有 R 份数据成功读取
- 为了保证强一致性,需要保证 W+R>N
-
Quorum NWR 模型将 CAP 的选择交给用户,是一种简化版的一致性模型
-
引起的并发更新问题
- 如果允许数据被覆盖,则并发更新容易引起一致性问题
-
概述
- Raft 协议是一种分布式一致性算法(共识算法),即使出现部分节点故障,网络延时等情况,也不影响各节点,进而提高系统的整体可用性。Raft 是使用较为广泛的分布式协议。
-
三种角色
- Leader - 领导者:Leader 负责处理所有的客户端请求,并向 Follower 同步请求日志,当日志同步到大多数节点上后,通知 Follower 提交日志
- Follower - 跟随者:接受并持久化 Leader 同步的日志,在 Leader 告知日志可以提交后,提交日志
- Candidate - 备选者:Leader 选举过程中的临时角色。向其他节点发送请求投票信息
-
四种定义:
- Log(日志):节点之间同步的信息,以只追加写的方式进行同步,解决了数据被覆盖的问题
- Term(任期号):单调递增,每个 Term 内最多只有一个 Leader
- Committed:日志被复制到多数派节点,即可认为已经被提交
- Applied:日志被应用到本地状态机:执行了 log 中命令,修改了内存状态
-
状态转移:
-
Leader 选举过程:
-
初始全部为 Follower
-
Current Term + 1
-
选举自己
-
向其它参与者发起 RequestVote 请求,retry 直到
- 收到多数派请求,成为 Leader,并发送心跳
- 收到其它 Leader 的请求,转为 Follower,更新自己的 Term
- 收到部分,但未达到多数派,选举超时,随机 timeout 开始下一轮
-
-
Log Replication 过程:
- 新 Leader 产生,Leader 和 Follower 不同步,Leader 强制覆盖 Followers 的不同步的日志
-
切主:当 Leader 出现问题时,就需要进行重新选举
- Leader 发现失去 Follower 的响应,失去 Leader 身份
- 两个 Follower 之间一段时间未收到心跳,重新进行选举,选出新的 Leader,此时发生了切主
- Leader 自杀重启,以 Follower 的身份加入进来
-
Stale 读:
- 发生 Leader 切换,old leader 收到了读请求。如果直接响应,可能会有 Stale Read
-
Paxos 算法与 RAFT 算法区别:
- Multi-Paxos 可以并发修改日志,而 Raft 写日志操作必须是连续的
- Multi-Paxos 可以随机选主,不必最新最全的节点当选 Leader
-
优劣势
- 优势:写入并发性能高,所有节点都能写
- 劣势:没有一个节点有完整的最新的数据,恢复流程复杂,需要同步历史记录
- 设计一个简易的 MapReduce 系统,思考如何应对故障?
- 设计一个简易的分布式键值系统,要求具备弹性的能力和达成线性一致
- 分布式系统有哪些优势和挑战?
- 两将军问题为什么理论上永远达不成共识?
- 为什么 TCP 采用三次握手?而不是两次和四次?
- 为什么在 4 将军问题中,增加 1 轮协商就可以对抗拜占庭故障?
- 什么是最终一致性?什么是线性一致性?
- CAP 理论中,请举例说明可用性和一致性的矛盾?
- 数据库里的一致性和分布式系统中的一致性有什么区别?
- 两阶段提交中,什么场景需要数据库管理员介入?
- 三阶段提交缓和两阶段提交的哪两个问题?
- 什么场景适合乐观锁?什么场景适合悲观锁?
- 在共识协议中,为什么说允许数据被覆盖会带来数据一致性问题?
- RAFT 协议中,Leader 写成功日志 Log20 但未同步给 Followers 后宕机,Follower 重新选举后产生一条新日志 Log20,这时 Leader 重启,整个系统发现两种不一样的 Log20 的记录,请问如何区分并拒掉前面的 Log20?
- RAFT 协议中,Stale 读是如何产生的?该如何解决 Stale 读的问题?