Dunwu Blog

大道至简,知易行难

ZAB 协议

ZooKeeper 并没有直接采用 Paxos 算法,而是采用了名为 ZAB 的一致性协议。**ZAB 协议不是 Paxos 算法**,只是比较类似,二者在操作上并不相同。Multi-Paxos 实现的是一系列值的共识,不关心最终达成共识的值是什么,不关心各值的顺序。而 ZooKeeper 需要确保操作的顺序性。

ZAB 协议是 Zookeeper 专门设计的一种支持故障恢复的原子广播协议

ZAB 协议是 ZooKeeper 的数据一致性和高可用解决方案。

ZAB 协议定义了两个可以无限循环的流程:

  • 选举 Leader - 用于故障恢复,从而保证高可用。
  • 原子广播 - 用于主从同步,从而保证数据一致性。

选举 Leader

故障恢复

ZooKeeper 集群采用一主(称为 Leader)多从(称为 Follower)模式,主从节点通过副本机制保证数据一致。

  • 如果 Follower 节点挂了 - ZooKeeper 集群中的每个节点都会单独在内存中维护自身的状态,并且各节点之间都保持着通讯,只要集群中有半数机器能够正常工作,那么整个集群就可以正常提供服务
  • 如果 Leader 节点挂了 - 如果 Leader 节点挂了,系统就不能正常工作了。此时,需要通过 ZAB 协议的选举 Leader 机制来进行故障恢复。

ZAB 协议的选举 Leader 机制简单来说,就是:基于过半选举机制产生新的 Leader,之后其他机器将从新的 Leader 上同步状态,当有过半机器完成状态同步后,就退出选举 Leader 模式,进入原子广播模式。

术语

  • myid - 每个 Zookeeper 服务器,都需要在数据文件夹下创建一个名为 myid 的文件,该文件包含整个 Zookeeper 集群唯一的 ID(整数)。
  • zxid - 类似于 RDBMS 中的事务 ID,用于标识一次更新操作的 Proposal ID。为了保证顺序性,该 zxid 必须单调递增。因此 Zookeeper 使用一个 64 位的数来表示,高 32 位是 Leader 的 epoch,从 1 开始,每次选出新的 Leader,epoch 加一。低 32 位为该 epoch 内的序号,每次 epoch 变化,都将低 32 位的序号重置。这样保证了 zxid 的全局递增性。

服务器状态

  • LOOKING - 不确定 Leader 状态。该状态下的服务器认为当前集群中没有 Leader,会发起 Leader 选举
  • FOLLOWING - 跟随者状态。表明当前服务器角色是 Follower,并且它知道 Leader 是谁
  • LEADING - 领导者状态。表明当前服务器角色是 Leader,它会维护与 Follower 间的心跳
  • OBSERVING - 观察者状态。表明当前服务器角色是 Observer,与 Folower 唯一的不同在于不参与选举,也不参与集群写操作时的投票

选票数据结构

每个服务器在进行领导选举时,会发送如下关键信息

  • logicClock - 每个服务器会维护一个自增的整数,名为 logicClock,它表示这是该服务器发起的第多少轮投票
  • state - 当前服务器的状态
  • self_id - 当前服务器的 myid
  • self_zxid - 当前服务器上所保存的数据的最大 zxid
  • vote_id - 被推举的服务器的 myid
  • vote_zxid - 被推举的服务器上所保存的数据的最大 zxid

投票流程

(1)自增选举轮次 - Zookeeper 规定所有有效的投票都必须在同一轮次中。每个服务器在开始新一轮投票时,会先对自己维护的 logicClock 进行自增操作。

(2)初始化选票 - 每个服务器在广播自己的选票前,会将自己的投票箱清空。该投票箱记录了所收到的选票。例:服务器 2 投票给服务器 3,服务器 3 投票给服务器 1,则服务器 1 的投票箱为(2, 3), (3, 1), (1, 1)。票箱中只会记录每一投票者的最后一票,如投票者更新自己的选票,则其它服务器收到该新选票后会在自己票箱中更新该服务器的选票。

(3)发送初始化选票 - 每个服务器最开始都是通过广播把票投给自己。

(4)接收外部投票 - 服务器会尝试从其它服务器获取投票,并记入自己的投票箱内。如果无法获取任何外部投票,则会确认自己是否与集群中其它服务器保持着有效连接。如果是,则再次发送自己的投票;如果否,则马上与之建立连接。

(5)判断选举轮次 - 收到外部投票后,首先会根据投票信息中所包含的 logicClock 来进行不同处理

  • 外部投票的 logicClock 大于自己的 logicClock。说明该服务器的选举轮次落后于其它服务器的选举轮次,立即清空自己的投票箱并将自己的 logicClock 更新为收到的 logicClock,然后再对比自己之前的投票与收到的投票以确定是否需要变更自己的投票,最终再次将自己的投票广播出去。
  • 外部投票的 logicClock 小于自己的 logicClock。当前服务器直接忽略该投票,继续处理下一个投票。
  • 外部投票的 logickClock 与自己的相等。当时进行选票 PK。

(6)选票 PK - 选票 PK 是基于(self_id, self_zxid)(vote_id, vote_zxid) 的对比

  • 外部投票的 logicClock 大于自己的 logicClock,则将自己的 logicClock 及自己的选票的 logicClock 变更为收到的 logicClock
  • 若 logicClock 一致,则对比二者的 vote_zxid,若外部投票的 vote_zxid 比较大,则将自己的票中的 vote_zxid 与 vote_myid 更新为收到的票中的 vote_zxid 与 vote_myid 并广播出去,另外将收到的票及自己更新后的票放入自己的票箱。如果票箱内已存在(self_myid, self_zxid)相同的选票,则直接覆盖
  • 若二者 vote_zxid 一致,则比较二者的 vote_myid,若外部投票的 vote_myid 比较大,则将自己的票中的 vote_myid 更新为收到的票中的 vote_myid 并广播出去,另外将收到的票及自己更新后的票放入自己的票箱

(7)统计选票 - 如果已经确定有过半服务器认可了自己的投票(可能是更新后的投票),则终止投票。否则继续接收其它服务器的投票。

(8)更新服务器状态 - 投票终止后,服务器开始更新自身状态。若过半的票投给了自己,则将自己的服务器状态更新为 LEADING,否则将自己的状态更新为 FOLLOWING

通过以上流程分析,我们不难看出:要使 Leader 获得多数 Server 的支持,则 ZooKeeper 集群节点数必须是奇数。且存活的节点数目不得少于 N + 1

每个 Server 启动后都会重复以上流程。在恢复模式下,如果是刚从崩溃状态恢复的或者刚启动的 server 还会从磁盘快照中恢复数据和会话信息,zk 会记录事务日志并定期进行快照,方便在恢复时进行状态恢复。

原子广播(Atomic Broadcast)

ZooKeeper 通过副本机制来实现高可用

那么,ZooKeeper 是如何实现副本机制的呢?答案是:ZAB 协议的原子广播。

ZAB 协议的原子广播要求:

**所有的写请求都会被转发给 Leader,Leader 会以原子广播的方式通知 Follow。当半数以上的 Follow 已经更新状态持久化后,Leader 才会提交这个更新,然后客户端才会收到一个更新成功的响应**。这有些类似数据库中的两阶段提交协议。

在整个消息的广播过程中,Leader 服务器会每个事务请求生成对应的 Proposal,并为其分配一个全局唯一的递增的事务 ID(ZXID),之后再对其进行广播。

ZAB 是通过“一切以领导者为准”的强领导者模型和严格按照顺序提交日志,来实现操作的顺序性的,这一点和 Raft 是一样的。

参考资料

初识 Python

Python 简介

Python 是一种广泛使用的解释型、高级和通用的编程语言。Python 支持多种编程范型,包括结构化、过程式、反射式、面向对象和函数式编程。它拥有动态类型系统和垃圾回收功能,能够自动管理内存使用,并且其本身拥有一个巨大而广泛的标准库。

Python 历史

1991 年,Python 的第一个解释器诞生。

1994 年,Python 1.0 版本发布。它包含了异常处理、函数和模块等基本特性。

2000 年,Python 2.0 版本发布。它引入了新的特性,如列表推导式、垃圾回收机制等。

2008 年,Python 3.0 版本发布。它进行了重大修订而不能完全后向兼容。

2020 年,Python 2.0 停止更新。

Python 应用

Python 在以下领域都有用武之地。

  • 后端开发 - Python / Java / Go / PHP
  • DevOps - Python / Shell / Ruby
  • 数据采集 - Python / C++ / Java
  • 量化交易 - Python / C++ / R
  • 数据科学 - Python / R / Julia / Matlab
  • 机器学习 - Python / R / C++ / Julia
  • 自动化测试 - Python / Shell

Python 开发环境

目前,Python 有两个版本,一个是 2.x 版,一个是 3.x 版,这两个版本是不兼容的。由于 3.x 版本越来越普及,所以推荐安装 3.x 版本。

安装 Python

Linux

Linux 环境自带了 Python 2.x 版本,但是如果要更新到 3.x 的版本,可以在 Python 官网 下载 Python 的源代码并通过源代码构建安装的方式进行安装,具体的步骤如下所示(以 CentOS 为例):

(1)安装依赖库(因为没有这些依赖库可能在源代码构件安装时因为缺失底层依赖库而失败)。

1
yum -y install wget gcc zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gdbm-devel db4-devel libpcap-devel xz-devel libffi-devel

(2)下载 Python 源代码并解压缩到指定目录。

1
2
3
wget https://www.python.org/ftp/python/3.7.6/Python-3.7.6.tar.xz
xz -d Python-3.7.6.tar.xz
tar -xvf Python-3.7.6.tar

(3)切换至 Python 源代码目录并执行下面的命令进行配置和安装。

1
2
3
cd Python-3.7.6
./configure --prefix=/usr/local/python37 --enable-optimizations
make && make install

(4)修改 .bash_profile 文件

1
2
cd ~
vim .bash_profile

配置 PATH 环境变量并使其生效

1
2
3
4
5
# ... 此处省略上面的代码 ...

export PATH=$PATH:/usr/local/python37/bin

# ... 此处省略下面的代码 ...

(5)激活环境变量

1
source .bash_profile

Mac

Mac 系统自带的 Python 版本是 2.7。要安装 Python 3.x,有两个方法:

方法一、从 Python 官网 下载 Python 的安装程序,下载后双击运行并安装。

方法二、如果安装了 Homebrew,直接通过命令 brew install python3 安装即可。

Windows

Python 官网 下载合适的 Windows 安装版本(64 位还是 32 位),下载后双击运行并安装。

注:要勾选 Add Python 3.x to PATH 选项,将安装路径自动添加到环境变量,否则需要自行配置。

运行 Python

执行以下命令可以检查 python 版本:

1
python --version

直接执行 python 命令可以进入交互式环境。

第一个程序

新建一个 hello.py 文件,内容如下:

1
print('hello world')

在终端执行如下命令:

1
python hello.py

打印如下内容

1
hello world

Python 开发工具

PyCharm

PyCharm 是由 JetBrains 打造的一款 Python IDE,支持 macOS、 Windows、 Linux 系统。

我认为,PyCharm 是最好用的 Python IDE,功能丰富,UI 很酷,缺点是正版比较贵。

VSCode

VSCode(全称:Visual Studio Code)是一款由微软开发且跨平台的免费源代码编辑器,VSCode 开发环境非常简单易用。

pip

pip 是 Python 包管理工具,该工具提供了对 Python 包的查找、下载、安装、卸载的功能。

目前最新的 Python 版本已经预装了 pip。

查看是否已经安装 pip,可以使用以下命令:

1
pip --version

下载安装包,可以使用以下命令:

1
pip install some-package-name

卸载安装包,可以使用以下命令:

1
pip uninstall some-package-name

查看已安装的包,可以使用以下命令:

1
pip list

IPython

IPython 是一种基于 Python 的交互式解释器。相较于原生的 Python 交互式环境,IPython 提供了更为强大的编辑和交互功能。可以通过 Python 的包管理工具 pip 安装 IPython,具体的操作如下所示。

1
pip install ipython

1
pip3 install ipython

Anaconda

Anaconda 是一个集成的数据科学和机器学习环境,其中包括了 Python 解释器以及大量常用的数据科学库和工具。Anaconda 发行版包含了 Python。

Anaconda 包及其依赖项和环境的管理工具为 conda 命令,与传统的 Python pip 工具相比 Anaconda 的conda 可以更方便地在不同环境之间进行切换,环境管理较为简单。

Anaconda详细安装与介绍参考:Anaconda 教程。

参考资料

Python 基础语法

编码

默认情况下,Python 3 源码文件以 UTF-8 编码,所有字符串都是 unicode 字符串。 当然你也可以为源码文件指定不同的编码:

1
# -*- coding: cp-1252 -*-

注释

Python 中的注释有三种形式:

  • 单行注释# 开头
  • 多行注释可以用 '''""" 标记开始和结尾
1
2
3
4
5
6
7
8
9
10
11
12
13
# 单行注释

'''
这是多行注释,用三个单引号
这是多行注释,用三个单引号
这是多行注释,用三个单引号
'''

"""
这是多行注释,用三个双引号
这是多行注释,用三个双引号
这是多行注释,用三个双引号
"""

保留字

Python 保留字意味着,不能将这些关键字用作任何标识符名称。

Python 的标准库提供了一个 keyword 模块,可以输出当前版本的所有关键字:

1
2
3
>>> import keyword
>>> keyword.kwlist
['False', 'None', 'True', 'and', 'as', 'assert', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try', 'while', 'with', 'yield']

变量

Python 中的变量不需要声明。每个变量在使用前都必须赋值,变量赋值以后该变量才会被创建。

Python 基本赋值

1
2
3
4
5
6
7
8
9
10
a = 1
b = 2.0
c = "test"
print(f'a={a}')
print(f'b={b}')
print(f'c={c}')
# 输出
# a=1
# b=2.0
# c=test

数据类型

Python3 中有六个标准的数据类型:

  • 不可变数据(3 个):Number(数字)、String(字符串)、Tuple(元组);
  • 可变数据(3 个):List(列表)、Dictionary(字典)、Set(集合)。

操作符

Python 语言支持以下类型的运算符:

  • 算术运算符
  • 比较(关系)运算符
  • 赋值运算符
  • 逻辑运算符
  • 位运算符
  • 成员运算符
  • 身份运算符
  • 运算符优先级

语句

python 最具特色的就是使用缩进来表示代码块,不需要使用大括号 {}

缩进的空格数是可变的,但是同一个代码块的语句必须包含相同的缩进空格数。

1
2
3
4
if True:
print ("True")
else:
print ("False")

以下代码最后一行语句缩进数的空格数不一致,会导致运行错误:

1
2
3
4
5
6
if True:
print ("Answer")
print ("True")
else:
print ("Answer")
print ("False") # 缩进不一致,会导致运行错误

Python 通常是一行写完一条语句,但如果语句很长,我们可以使用反斜杠 \ 来实现多行语句,例如:

1
2
3
total = item_one + \
item_two + \
item_three

[], {}, 或 () 中的多行语句,不需要使用反斜杠 \ ,例如:

1
2
total = ['item_one', 'item_two', 'item_three',
'item_four', 'item_five']

ifwhiledefclass 这样的复合语句,首行以关键字开始,以冒号( : )结束,该行之后的一行或多行代码构成代码组。

我们将首行及后面的代码组称为一个子句(clause)。

同一行显示多条语句

Python 可以在同一行中使用多条语句,语句之间使用分号 ; 分割,以下是一个简单的实例:

1
2
3
import sys; x = 'test'; sys.stdout.write(x + '\n')
# 输出
# test

使用交互式命令行执行,输出结果为:

1
2
3
>>> import sys; x = 'test'; sys.stdout.write(x + '\n')
test
5

此处的 5 表示字符数,test 有 4 个字符,\n 表示一个字符,加起来 5 个字符。

1
2
3
>>> import sys
>>> sys.stdout.write(" hi ") # hi 前后各有 1 个空格
hi 4

多个语句构成代码组

缩进相同的一组语句构成一个代码块,我们称之代码组。

像 if、while、def 和 class 这样的复合语句,首行以关键字开始,以冒号( : )结束,该行之后的一行或多行代码构成代码组。

我们将首行及后面的代码组称为一个子句(clause)。

如下实例:

1
2
3
4
5
6
if expression :
suite
elif expression :
suite
else :
suite

控制语句

Python 支持三类控制语句:

  • 选择语句 - if...elif...elsematch...case(Python 3.10 新增)
  • 循环语句 - whilefor
  • 中断语句 - breakcontinuepass

函数

Python 定义函数使用 def 关键字。

语法格式如下:

1
2
def 函数名(参数列表):
# do something

函数示例:

1
2
3
4
5
6
# 函数定义
def hello():
print("hello world!")

# 函数调用
hello()

输入和输出

print 默认输出是换行的,如果要实现不换行需要在变量末尾加上 end=""

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
a = "a"
b = "b"
# 换行输出
print(a)
print(b)

print('---------')

# 不换行输出
print(a, end="")
print(b, end="")
print()

# 输出
# a
# b
# ---------
# ab

input 输入

执行下面的程序在按回车键后就会等待用户输入:

1
input("\n按下 enter 键后退出。\n")

以上代码中,一旦用户按下 enter 键时,程序将退出。

模块

在 python 用 import 或者 from...import 来导入相应的模块。

将整个模块(somemodule)导入,格式为: import somemodule

从某个模块中导入某个函数,格式为: from somemodule import somefunction

从某个模块中导入多个函数,格式为: from somemodule import firstfunc, secondfunc, thirdfunc

将某个模块中的全部函数导入,格式为: from somemodule import *

【示例】导入 sys 模块

1
2
3
4
5
6
7
import sys

print('================Python import mode==========================')
print('命令行参数为:')
for i in sys.argv:
print(i)
print('\n python 路径为', sys.path)

【示例】导入 sys 模块的 argv,path 成员

1
2
3
4
from sys import argv,path  #  导入特定的成员

print('================python from import===================================')
print('path:',path) # 因为已经导入path成员,所以此处引用时不需要加sys.path

命令行参数

很多程序可以执行一些操作来查看一些基本信息,Python 可以使用 -h 参数查看各参数帮助信息:

1
2
3
4
5
6
7
8
9
$ python -h
usage: python [option] ... [-c cmd | -m mod | file | -] [arg] ...
Options and arguments (and corresponding environment variables):
-c cmd : program passed in as string (terminates option list)
-d : debug output from parser (also PYTHONDEBUG=x)
-E : ignore environment variables (such as PYTHONPATH)
-h : print this help message and exit

[ etc. ]

我们在使用脚本形式执行 Python 时,可以接收命令行输入的参数,具体使用可以参照 Python 3 命令行参数

参考资料

Python 操作符

Python 语言支持以下类型的运算符:

  • 算术运算符
  • 比较(关系)运算符
  • 赋值运算符
  • 逻辑运算符
  • 位运算符
  • 成员运算符
  • 身份运算符
  • 运算符优先级

算术运算符

假设变量: a=10,b=20

运算符 描述 实例
+ 加 - 两个对象相加 a + b 输出结果 30
- 减 - 得到负数或是一个数减去另一个数 a - b 输出结果 -10
* 乘 - 两个数相乘或是返回一个被重复若干次的字符串 a * b 输出结果 200
/ 除 - x 除以 y b / a 输出结果 2
% 取模 - 返回除法的余数 b % a 输出结果 0
** 幂 - 返回 x 的 y 次幂 a**b 为 10 的 20 次方, 输出结果 100000000000000000000
// 取整除 - 返回商的整数部分 9//2 输出结果 4 , 9.0//2.0 输出结果 4.0

比较运算符

假设变量: a=10,b=20

运算符 描述 实例
== 等于 - 比较对象是否相等 (a == b) 返回 False。
!= 不等于 - 比较两个对象是否不相等 (a != b) 返回 True。
<> 不等于 - 比较两个对象是否不相等。python3 已废弃。 (a <> b) 返回 True。这个运算符类似 != 。
> 大于 - 返回 x 是否大于 y (a > b) 返回 False。
< 小于 - 返回 x 是否小于 y。所有比较运算符返回 1 表示真,返回 0 表示假。这分别与特殊的变量 True 和 False 等价。 (a < b) 返回 True。
>= 大于等于 - 返回 x 是否大于等于 y。 (a >= b) 返回 False。
<= 小于等于 - 返回 x 是否小于等于 y。 (a <= b) 返回 True。

赋值运算符

假设变量: a=10,b=20

运算符 描述 实例
= 简单的赋值运算符 c = a + b 将 a + b 的运算结果赋值为 c
+= 加法赋值运算符 c += a 等效于 c = c + a
-= 减法赋值运算符 c -= a 等效于 c = c - a
*= 乘法赋值运算符 c *= a 等效于 c = c * a
/= 除法赋值运算符 c /= a 等效于 c = c / a
%= 取模赋值运算符 c %= a 等效于 c = c % a
**= 幂赋值运算符 c **= a 等效于 c = c ** a
//= 取整除赋值运算符 c //= a 等效于 c = c // a

位运算符

运算符 描述 实例
& 按位与运算符:参与运算的两个值,如果两个相应位都为 1,则该位的结果为 1,否则为 0 (a & b) 输出结果 12 ,二进制解释: 0000 1100
| 按位或运算符:只要对应的二个二进位有一个为 1 时,结果位就为 1。 (a | b) 输出结果 61 ,二进制解释: 0011 1101
^ 按位异或运算符:当两对应的二进位相异时,结果为 1 (a ^ b) 输出结果 49 ,二进制解释: 0011 0001
~ 按位取反运算符:对数据的每个二进制位取反,即把 1 变为 0,把 0 变为 1 (~a ) 输出结果 -61 ,二进制解释: 1100 0011, 在一个有符号二进制数的补码形式。
<< 左移动运算符:运算数的各二进位全部左移若干位,由”<<”右边的数指定移动的位数,高位丢弃,低位补 0。 a << 2 输出结果 240 ,二进制解释: 1111 0000
>> 右移动运算符:把”>>”左边的运算数的各二进位全部右移若干位,”>>”右边的数指定移动的位数 a >> 2 输出结果 15 ,二进制解释: 0000 1111

逻辑运算符

运算符 逻辑表达式 描述 实例
and x and y 布尔”与” - 如果 x 为 False,x and y 返回 False,否则它返回 y 的计算值。 (a and b) 返回 20。
or x or y 布尔”或” - 如果 x 是 True,它返回 x 的值,否则它返回 y 的计算值。 (a or b) 返回 10。
not not x 布尔”非” - 如果 x 为 True,返回 False 。如果 x 为 False,它返回 True。 not(a and b) 返回 False

成员运算符

运算符 描述 实例
in 如果在指定的序列中找到值返回 True,否则返回 False。 x 在 y 序列中 , 如果 x 在 y 序列中返回 True。
not in 如果在指定的序列中没有找到值返回 True,否则返回 False。 x 不在 y 序列中 , 如果 x 不在 y 序列中返回 True。

身份运算符

运算符 描述 实例
is is 是判断两个标识符是不是引用自一个对象 x is y, 如果 id(x) 等于 id(y) , is 返回结果 1
is not is not 是判断两个标识符是不是引用自不同对象 x is not y, 如果 id(x) 不等于 id(y). is not 返回结果 1

运算符优先级

运算符 描述
** 指数 (最高优先级)
~ + - 按位翻转, 一元加号和减号 (最后两个的方法名为 +@ 和 -@)
* / % // 乘,除,取模和取整除
+ - 加法减法
>> << 右移,左移运算符
& 位 ‘AND’
^ | 位运算符
<= < > >= 比较运算符
<> == != 等于运算符
= %= /= //= -= += *= **= 赋值运算符
is is not 身份运算符
in not in 成员运算符
not or and 逻辑运算符

参考资料

Python 变量和数据类型

变量

变量简介

Python 中的变量不需要声明。每个变量在使用前都必须赋值,变量赋值以后该变量才会被创建。

Python 基本赋值

1
2
3
4
5
6
7
8
9
10
a = 1
b = 2.0
c = "test"
print(f'a={a}')
print(f'b={b}')
print(f'c={c}')
# 输出
# a=1
# b=2.0
# c=test

Python 允许多个变量同时赋值

1
2
3
4
5
6
7
8
a = b = c = 1
print(f'a={a}')
print(f'b={b}')
print(f'c={c}')
# 输出
# a=1
# b=1
# c=1

Python 允许为多个变量同时赋不同的值

1
2
3
4
5
6
7
8
a, b, c = 1, 2.0, "test"
print(f'a={a}')
print(f'b={b}')
print(f'c={c}')
# 输出
# a=1
# b=2.0
# c=test

变量命名规则

  • 第一个字符必须是字母表中字母或下划线 _
  • 标识符的其他的部分由字母、数字和下划线组成。
  • 标识符对大小写敏感。

在 Python 3 中,可以用中文作为变量名,非 ASCII 标识符也是允许的了。

数据类型

在 Python 中,变量就是变量,它没有类型,我们所说的”类型”是变量所指的内存中对象的类型。

Python3 中有六个标准的数据类型:

  • 不可变数据(3 个):Number(数字)、String(字符串)、Tuple(元组);
  • 可变数据(3 个):List(列表)、Dictionary(字典)、Set(集合)。

Python 内置的 type() 函数可以用来查询变量所指的对象类型

1
2
3
4
5
6
7
8
9
10
a, b, c, d = 1, 2.0, True, 3.14j
print(f'a={type(a)}')
print(f'b={type(b)}')
print(f'c={type(c)}')
print(f'd={type(d)}')
# 输出
# a=<class 'int'>
# b=<class 'float'>
# c=<class 'bool'>
# d=<class 'complex'>

Number

在 Python 中,Number 数据类型用于存储数值。

数据类型是不允许改变的,这就意味着如果改变 Number 数据类型的值,将重新分配内存空间。

Python 中数学运算常用的函数基本都在 math 模块、cmath 模块中。

Python math 模块提供了许多对浮点数的数学运算函数。

Python cmath 模块包含了一些用于复数运算的函数。

cmath 模块的函数跟 math 模块函数基本一致,区别是 cmath 模块运算的是复数,math 模块运算的是数学运算。

字符串

字符串是 Python 中最常用的数据类型。可以使用引号 ( ) 来创建字符串。

Python 中单引号 ' 和双引号 " 使用完全相同。

使用三引号('''""")可以指定一个多行字符串。

转义符 \

反斜杠可以用来转义,使用 r 可以让反斜杠不发生转义。 如 r”this is a line with \n”\n 会显示,并不是换行。

按字面意义级联字符串,如 “this “ “is “ “string” 会被自动转换为 this is string

字符串可以用 + 运算符连接在一起,用 ***** 运算符重复。

Python 中的字符串有两种索引方式,从左往右以 0 开始,从右往左以 -1 开始。

Python 中的字符串不能改变。

Python 没有单独的字符类型,一个字符就是长度为 1 的字符串。

字符串切片 str[start:end],其中 start 是切片开始的索引,end 是切片结束的索引(但不包括该索引指向的字符)。

字符串的切片可以加上步长参数 step,语法格式如下:str[start:end:step]

字符串的截取的语法格式如下:变量[头下标:尾下标:步长]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
str='123456789'

print(str) # 输出字符串
print(str[0:-1]) # 输出第一个到倒数第二个的所有字符
print(str[0]) # 输出字符串第一个字符
print(str[2:5]) # 输出从第三个开始到第六个的字符(不包含)
print(str[2:]) # 输出从第三个开始后的所有字符
print(str[1:5:2]) # 输出从第二个开始到第五个且每隔一个的字符(步长为2)
print(str * 2) # 输出字符串两次
print(str + '你好') # 连接字符串

print('------------------------------')

print('hello\nrunoob') # 使用反斜杠(\)+n转义特殊字符
print(r'hello\nrunoob') # 在字符串前面添加一个 r,表示原始字符串,不会发生转义

列表

序列是 Python 中最基本的数据结构。序列中的每个元素都分配一个数字 - 它的位置,或索引,第一个索引是 0,第二个索引是 1,依此类推。

Python 有 6 个序列的内置类型,但最常见的是列表和元组。

序列都可以进行的操作包括索引,切片,加,乘,检查成员。

列表的数据项不需要具有相同的类型。创建一个列表,只要把逗号分隔的不同的数据项使用方括号括起来即可。

1
2
3
list1 = ['physics', 'chemistry', 1997, 2000]
list2 = [1, 2, 3, 4, 5 ]
list3 = ["a", "b", "c", "d"]

元祖

Python 的元组与列表类似,不同之处在于元组的元素不能修改。

元组使用小括号,列表使用方括号。

元组创建很简单,只需要在括号中添加元素,并使用逗号隔开即可。

1
2
3
tup1 = ('physics', 'chemistry', 1997, 2000)
tup2 = (1, 2, 3, 4, 5 )
tup3 = "a", "b", "c", "d"

字典

字典是另一种可变容器模型,且可存储任意类型对象。

字典的每个键值 key:value 对用冒号 : 分割,每个键值对之间用逗号 , 分割,整个字典包括在花括号 {} 中。

1
2
3
tinydict = {'Alice': '2341', 'Beth': '9102', 'Cecil': '3258'}
tinydict1 = { 'abc': 456 }
tinydict2 = { 'abc': 123, 98.6: 37 }

数据类型转换

Python 数据类型转换可以分为两种:

  • 隐式类型转换
  • 显式类型转换

隐式类型转换示例

1
2
3
4
5
6
7
8
9
10
11
12
num_int = 1
num_float = 2.0
num_new = num_int + num_float
print("num_int 数据类型为:", type(num_int))
print("num_float 数据类型为:", type(num_float))
print("num_new 值为:", num_new)
print("num_new 数据类型为:", type(num_new))
# 输出
# num_int 数据类型为: <class 'int'>
# num_float 数据类型为: <class 'float'>
# num_new 值为: 3.0
# num_new 数据类型为: <class 'float'>

显示类型转换方法:

  • int() - 将指定的数值或字符串转换成整数,可以指定进制。
  • float() - 将指定的字符串转换成浮点数。
  • str() - 将指定的对象转换成字符串,可以指定编码。
  • chr() - 将指定的整数转换成该编码对应的字符。
  • ord() - 将指定的字符转换成对应的编码(整数)。
1
2
3
4
5
6
7
8
9
10
a = int("100")
b = float(2)
c = str(3.0)
print(f'a={a}, type={type(a)}')
print(f'b={b}, type={type(b)}')
print(f'c={c}, type={type(c)}')
# 输出
# a=100, type=<class 'int'>
# b=2.0, type=<class 'float'>
# c=3.0, type=<class 'str'>

参考资料

Python 控制语句

选择语句

Python 的选择语句的语法格式为:if...elif...else 语句。

  • if 语句至多有 1 个 else 语句,else 语句在所有的 elif 语句之后。
  • if 语句可以有若干个 elif 语句,它们必须在 else 语句之前。
  • 一旦其中一个 elif 语句检测为 true,其他的 elif 以及 else 语句都将跳过执行。
1
2
3
4
5
6
7
8
9
code = 3
if code == 0:
print("code == 0")
elif code == 1:
print("code == 1")
else:
print("code != 0 && code != 1")
# 输出
# code != 0 && code != 1

循环语句

while 循环

只要布尔表达式为 truewhile 循环体会一直执行下去。

1
2
3
4
5
6
7
8
9
10
count = 1
while (count <= 5):
print('count = ', count)
count = count + 1
# 输出
# count = 1
# count = 2
# count = 3
# count = 4
# count = 5

for 循环

for 循环可以遍历任何的序列对象或可迭代对象。

【示例】遍历字符串字符

1
2
3
4
5
6
7
8
9
for letter in 'python':
print("char: %s" % letter)
# 输出
# char: p
# char: y
# char: t
# char: h
# char: o
# char: n

【示例】遍历数组

1
2
3
4
5
6
7
colors = ['red', 'yellow', 'blue']
for color in colors:
print('color: %s' % color)
# 输出
# color: red
# color: yellow
# color: blue

【示例】遍历指定整数范围

1
2
3
4
5
6
7
8
for num in range(1, 10):
if num % 2 == 0:
print('num = ', num)
# 输出
# num = 2
# num = 4
# num = 6
# num = 8

中断语句

break 语句

break 语句用来终止循环语句,即循环条件没有 False 条件或者序列还没被完全递归完,也会停止执行循环语句。

break 语句用在 whilefor 循环中。

【示例】遍历字符串,找到指定字母的位置后退出

1
2
3
4
5
6
7
8
9
pos = 0
for letter in 'python':
if letter == 'h':
print('h pos: ', pos)
break
else:
pos += 1
# 输出
# h pos: 3

continue 语句

使用 continue 语句意味着跳过当前循环的剩余语句,然后继续进行下一轮循环。

continue 语句用在 whilefor 循环中。

1
2
3
4
5
6
7
8
9
10
11
12
num = 1
for num in range(1, 10):
if num % 2 == 0:
continue
else:
print(f'num = {num}')
# 输出
# num = 1
# num = 3
# num = 5
# num = 7
# num = 9

pass 语句

Python pass 是空语句,是为了保持程序结构的完整性。

pass 不做任何事情,一般用做占位语句。

1
2
3
4
5
6
7
8
9
10
# pass 语句
age = 65
if age < 18:
print("未成年")
elif age >= 18 and age < 30:
print("成年人")
elif age >= 30 and age < 65:
pass
else:
print("老年人")

参考资料

Sentinel 快速入门

Sentinel 简介

Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件,主要以流量为切入点,从流量控制、流量路由、熔断降级、系统自适应保护等多个维度来帮助用户保障微服务的稳定性。

img

Sentinel 中有两个基本概念:

资源:资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。只要通过 Sentinel API 定义的代码(即资源),就可以被 Sentinel 保护起来。大部分情况下,可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。

规则:围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则。所有规则可以动态实时调整。

Sentinel 的主要工作机制如下:

  • 对主流框架提供适配或者显示的 API,来定义需要保护的资源,并提供设施对资源进行实时统计和调用链路分析。
  • 根据预设的规则,结合对资源的实时统计信息,对流量进行控制。同时,Sentinel 提供开放的接口,方便您定义及改变规则。
  • Sentinel 提供实时的监控系统,方便您快速了解目前系统的状态。

Sentinel 基本原理

在 Sentinel 中,任意资源都对应一个资源名称以及一个 Entry 对象。Entry 可以通过对主流框架的适配自动创建,也可以通过注解的方式或调用 API 显式创建;每一个 Entry 创建的时候,同时也会创建一条检查链(slot chain)。

Sentinel 会采用职责链模式,依次处理链上的各个插槽。这个链路大致会执行下列操作:

  1. 采用滑动时间窗口算法统计当前流量;
  2. 依次使用多种限流熔断规则进行流量判断,一旦判定当前流量超出规则中设置的阈值时,即触发限流或熔断,抛出 BlockException 异常;
  3. 业务侧根据 BlockException 进行回调处理。

总体的框架如下:

arch overview

大致介绍一下 Sentinel 的核心插槽:

  • NodeSelectorSlot 负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;
  • ClusterBuilderSlot 则用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据;
  • StatisticSlot 则用于记录、统计不同纬度的 runtime 指标监控信息;
  • FlowSlot 则用于根据预设的限流规则以及前面 slot 统计的状态,来进行流量控制;
  • AuthoritySlot 则根据配置的黑白名单和调用来源信息,来做黑白名单控制;
  • DegradeSlot 则通过统计信息以及预设的规则,来做熔断降级;
  • SystemSlot 则通过系统的状态,例如 load1 等,来控制总的入口流量;

除此以外,Sentinel 也支持通过 SPI 技术来自定义规则,用户可以自行加入自定义的 slot 并编排 slot 间的顺序。需要注意的是:

  • 1.7.2 版本以前用 SlotChainBuilder 作为 SPI
  • 1.7.2 版本以后用 ProcessorSlot 作为 SPI

Slot Chain SPI

下面将逐一详细讲解各插槽的用法。

NodeSelectorSlot

这个 slot 主要负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级。

1
2
3
4
5
6
ContextUtil.enter("entrance1", "appA");
Entry nodeA = SphU.entry("nodeA");
if (nodeA != null) {
nodeA.exit();
}
ContextUtil.exit();

上述代码通过 ContextUtil.enter() 创建了一个名为 entrance1 的上下文,同时指定调用发起者为 appA;接着通过 SphU.entry()请求一个 token,如果该方法顺利执行没有抛 BlockException,表明 token 请求成功。

以上代码将在内存中生成以下结构:

1
2
3
4
5
6
7
 machine-root
/
/
EntranceNode1
/
/
DefaultNode(nodeA)

注意:每个 DefaultNode 由资源 ID 和输入名称来标识。换句话说,一个资源 ID 可以有多个不同入口的 DefaultNode。

1
2
3
4
5
6
7
8
9
10
11
12
13
ContextUtil.enter("entrance1", "appA");
Entry nodeA = SphU.entry("nodeA");
if (nodeA != null) {
nodeA.exit();
}
ContextUtil.exit();

ContextUtil.enter("entrance2", "appA");
nodeA = SphU.entry("nodeA");
if (nodeA != null) {
nodeA.exit();
}
ContextUtil.exit();

以上代码将在内存中生成以下结构:

1
2
3
4
5
6
7
            machine-root
/ \
/ \
EntranceNode1 EntranceNode2
/ \
/ \
DefaultNode(nodeA) DefaultNode(nodeA)

上面的结构可以通过调用 curl http://localhost:8719/tree?type=root 来显示:

1
2
3
4
5
6
7
EntranceNode: machine-root(t:0 pq:1 bq:0 tq:1 rt:0 prq:1 1mp:0 1mb:0 1mt:0)
-EntranceNode1: Entrance1(t:0 pq:1 bq:0 tq:1 rt:0 prq:1 1mp:0 1mb:0 1mt:0)
--nodeA(t:0 pq:1 bq:0 tq:1 rt:0 prq:1 1mp:0 1mb:0 1mt:0)
-EntranceNode2: Entrance1(t:0 pq:1 bq:0 tq:1 rt:0 prq:1 1mp:0 1mb:0 1mt:0)
--nodeA(t:0 pq:1 bq:0 tq:1 rt:0 prq:1 1mp:0 1mb:0 1mt:0)

t:threadNum pq:passQps bq:blockedQps tq:totalQps rt:averageRt prq: passRequestQps 1mp:1m-passed 1mb:1m-blocked 1mt:1m-total

ClusterBuilderSlot

此插槽用于构建资源的 ClusterNode 以及调用来源节点。ClusterNode 保持资源运行统计信息(响应时间、QPS、block 数目、线程数、异常数等)以及原始调用者统计信息列表。来源调用者的名字由 ContextUtil.enter(contextName,origin) 中的 origin 标记。可通过如下命令查看某个资源不同调用者的访问情况:curl http://localhost:8719/origin?id=caller

1
2
3
4
id: nodeA
idx origin threadNum passedQps blockedQps totalQps aRt 1m-passed 1m-blocked 1m-total
1 caller1 0 0 0 0 0 0 0 0
2 caller2 0 0 0 0 0 0 0 0

StatisticSlot

StatisticSlot 是 Sentinel 的核心功能插槽之一,用于统计实时的调用数据。

  • clusterNode:资源唯一标识的 ClusterNode 的 runtime 统计
  • origin:根据来自不同调用者的统计信息
  • defaultnode: 根据上下文条目名称和资源 ID 的 runtime 统计
  • 入口的统计

Sentinel 底层采用高性能的滑动窗口数据结构 LeapArray 来统计实时的秒级指标数据,可以很好地支撑写多于读的高并发场景。

sliding-window-leap-array

AuthoritySlot

SystemSlot

这个 slot 会根据当前系统的整体情况,对入口资源的调用进行动态调配。其原理是让入口的流量和当前系统的预计容量达到一个动态平衡。

注意系统规则只对入口流量起作用(调用类型为 EntryType.IN),对出口流量无效。可通过 SphU.entry(res, entryType) 指定调用类型,如果不指定,默认是EntryType.OUT

ParamFlowSlot

FlowSlot

这个 slot 主要根据预设的资源的统计信息,按照固定的次序,依次生效。如果一个资源对应两条或者多条流控规则,则会根据如下次序依次检验,直到全部通过或者有一个规则生效为止:

  • 指定应用生效的规则,即针对调用方限流的;
  • 调用方为 other 的规则;
  • 调用方为 default 的规则。

DegradeSlot

这个 slot 主要针对资源的平均响应时间(RT)以及异常比率,来决定资源是否在接下来的时间被自动熔断掉。

Sentinel 基本使用

参考:Sentinel 官方文档之快速开始

使用 Sentinel 来进行资源保护,主要分为几个步骤:

  1. 定义资源
  2. 定义规则
  3. 检验规则是否生效

Sentinel 支持 5 种定义资源方式:

  1. 主流框架的默认适配 - Sentinel 支持大部分主流框架,例如 Web Servlet、Dubbo、Spring Cloud、gRPC、Spring WebFlux、Reactor 等,只需要引入对应的依赖即可方便地整合 Sentinel。
  2. 抛出异常的方式定义资源 - SphU 包含了 try-catch 风格的 API。
  3. 返回布尔值方式定义资源 - SphO 提供 if-else 风格的 API。
  4. 注解方式定义资源 - Sentinel 支持通过 @SentinelResource 注解定义资源并配置 blockHandlerfallback 函数来进行限流之后的处理。
  5. 异步调用支持 - Sentinel 支持通过 SphU.asyncEntry(xxx) 方法定义资源,并通常需要在异步的回调函数中调用 exit 方法。

Sentinel 支持以下几种规则:

  1. 流量控制规则 (FlowRule) - 流量控制主要有两种统计类型,一种是统计线程数,另外一种则是统计 QPS。
  2. 熔断降级规则 (DegradeRule)
  3. 系统保护规则 (SystemRule) - Sentinel 系统自适应限流从整体维度对应用入口流量进行控制,结合应用的 Load、CPU 使用率、总体平均 RT、入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
  4. 来源访问控制规则 (AuthorityRule) - 黑白名单根据资源的请求来源(origin)限制资源是否通过,若配置白名单则只有请求来源位于白名单内时才可通过;若配置黑名单则请求来源位于黑名单时不通过,其余的请求通过。
  5. 热点参数规则 (ParamFlowRule)

Sentinel 流量控制

FlowSlot 会根据预设的规则,结合前面 NodeSelectorSlotClusterNodeBuilderSlotStatistcSlot 统计出来的实时信息进行流量控制。

限流的直接表现是在执行 Entry nodeA = SphU.entry(资源名字) 的时候抛出 FlowException 异常。FlowExceptionBlockException 的子类,您可以捕捉 BlockException 来自定义被限流之后的处理逻辑。

同一个资源可以对应多条限流规则。FlowSlot 会对该资源的所有限流规则依次遍历,直到有规则触发限流或者所有规则遍历完毕。

一条限流规则主要由下面几个因素组成,我们可以组合这些元素来实现不同的限流效果:

  • resource - 资源名,即限流规则的作用对象
  • count - 限流阈值
  • grade - 限流阈值类型,QPS 或线程数
  • strategy - 根据调用关系选择策略

基于 QPS/并发数的流量控制

流量控制主要有两种统计类型,一种是统计线程数,另外一种则是统计 QPS。

线程数限流用于保护业务线程数不被耗尽。例如,当应用所依赖的下游应用由于某种原因导致服务不稳定、响应延迟增加,对于调用者来说,意味着吞吐量下降和更多的线程数占用,极端情况下甚至导致线程池耗尽。为应对高线程占用的情况,业内有使用隔离的方案,比如通过不同业务逻辑使用不同线程池来隔离业务自身之间的资源争抢(线程池隔离),或者使用信号量来控制同时请求的个数(信号量隔离)。这种隔离方案虽然能够控制线程数量,但无法控制请求排队时间。当请求过多时排队也是无益的,直接拒绝能够迅速降低系统压力。Sentinel 线程数限流不负责创建和管理线程池,而是简单统计当前请求上下文的线程个数,如果超出阈值,新的请求会被立即拒绝。

当 QPS 超过某个阈值的时候,则采取措施进行流量控制。流量控制的手段包括下面 3 种,对应 FlowRule 中的 controlBehavior 字段:

  1. 直接拒绝(RuleConstant.CONTROL_BEHAVIOR_DEFAULT)方式。该方式是默认的流量控制方式,当 QPS 超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException。这种方式适用于对系统处理能力确切已知的情况下,比如通过压测确定了系统的准确水位时。具体的例子参见 FlowqpsDemo
  2. 冷启动(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式。该方式主要用于系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过”冷启动”,让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮的情况。具体的例子参见 WarmUpFlowDemo
  3. 匀速器(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式。这种方式严格控制了请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。具体的例子参见 PaceFlowDemo

基于调用关系的流量控制

调用关系包括调用方、被调用方;方法又可能会调用其它方法,形成一个调用链路的层次关系。Sentinel 通过 NodeSelectorSlot 建立不同资源间的调用的关系,并且通过 ClusterNodeBuilderSlot 记录每个资源的实时统计信息。

有了调用链路的统计信息,我们可以衍生出多种流量控制手段。

根据调用方限流

ContextUtil.enter(resourceName, origin) 方法中的 origin 参数标明了调用方身份。这些信息会在 ClusterBuilderSlot 中被统计。

限流规则中的 limitApp 字段用于根据调用方进行流量控制。该字段的值有以下三种选项,分别对应不同的场景:

  • default:表示不区分调用者,来自任何调用者的请求都将进行限流统计。如果这个资源名的调用总和超过了这条规则定义的阈值,则触发限流。
  • {some_origin_name}:表示针对特定的调用者,只有来自这个调用者的请求才会进行流量控制。例如 NodeA 配置了一条针对调用者caller1的规则,那么当且仅当来自 caller1NodeA 的请求才会触发流量控制。
  • other:表示针对除 {some_origin_name} 以外的其余调用方的流量进行流量控制。例如,资源NodeA配置了一条针对调用者 caller1 的限流规则,同时又配置了一条调用者为 other 的规则,那么任意来自非 caller1NodeA 的调用,都不能超过 other 这条规则定义的阈值。

同一个资源名可以配置多条规则,规则的生效顺序为:**{some_origin_name} > other > default**

根据调用链路入口限流:链路限流

NodeSelectorSlot 中记录了资源之间的调用链路,这些资源通过调用关系,相互之间构成一棵调用树。这棵树的根节点是一个名字为 machine-root 的虚拟节点,调用链的入口都是这个虚节点的子节点。

一棵典型的调用树如下图所示:

1
2
3
4
5
6
7
          machine-root
/ \
/ \
Entrance1 Entrance2
/ \
/ \
DefaultNode(nodeA) DefaultNode(nodeA)

上图中来自入口 Entrance1Entrance2 的请求都调用到了资源 NodeA,Sentinel 允许只根据某个入口的统计信息对资源限流。比如我们可以设置 FlowRule.strategyRuleConstant.CHAIN,同时设置 FlowRule.ref_identityEntrance1 来表示只有从入口 Entrance1 的调用才会记录到 NodeA 的限流统计当中,而对来自 Entrance2 的调用漠不关心。

调用链的入口是通过 API 方法 ContextUtil.enter(name) 定义的。

具有关系的资源流量控制:关联流量控制

当两个资源之间具有资源争抢或者依赖关系的时候,这两个资源便具有了关联。比如对数据库同一个字段的读操作和写操作存在争抢,读的速度过高会影响写得速度,写的速度过高会影响读的速度。如果放任读写操作争抢资源,则争抢本身带来的开销会降低整体的吞吐量。可使用关联限流来避免具有关联关系的资源之间过度的争抢,举例来说,read_dbwrite_db 这两个资源分别代表数据库读写,我们可以给 read_db 设置限流规则来达到写优先的目的:设置 FlowRule.strategyRuleConstant.RELATE 同时设置 FlowRule.ref_identitywrite_db。这样当写库操作过于频繁时,读数据的请求会被限流。

Sentinel 熔断降级

除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。

chain

Sentinel 提供以下几种熔断策略:

  • 慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
  • 异常比例 (ERROR_RATIO):当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
  • 异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。

注意异常降级仅针对业务异常,对 Sentinel 限流降级本身的异常(BlockException)不生效。为了统计异常比例或异常数,需要通过 Tracer.trace(ex) 记录业务异常。示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Entry entry = null;
try {
entry = SphU.entry(resource);

// Write your biz code here.
// <<BIZ CODE>>
} catch (Throwable t) {
if (!BlockException.isBlockException(t)) {
Tracer.trace(t);
}
} finally {
if (entry != null) {
entry.exit();
}
}

开源整合模块,如 Sentinel Dubbo Adapter, Sentinel Web Servlet Filter 或 @SentinelResource 注解会自动统计业务异常,无需手动调用。

Sentinel 支持注册自定义的事件监听器监听熔断器状态变换事件(state change event)。示例:

1
2
3
4
5
6
7
8
9
10
11
EventObserverRegistry.getInstance().addStateChangeObserver("logging",
(prevState, newState, rule, snapshotValue) -> {
if (newState == State.OPEN) {
// 变换至 OPEN state 时会携带触发时的值
System.err.println(String.format("%s -> OPEN at %d, snapshotValue=%.2f", prevState.name(),
TimeUtil.currentTimeMillis(), snapshotValue));
} else {
System.err.println(String.format("%s -> %s at %d", prevState.name(), newState.name(),
TimeUtil.currentTimeMillis()));
}
});

Sentinel 系统自适应保护

Sentinel 系统自适应保护从整体维度对应用入口流量进行控制,结合应用的 Load、总体平均 RT、入口 QPS 和线程数等几个维度的监控指标,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。

Sentinel 做系统自适应保护的目的:

  • 保证系统不被拖垮
  • 在系统稳定的前提下,保持系统的吞吐量

Sentinel 的设计理念是,根据系统能够处理的请求,和允许进来的请求,来做平衡,而不是根据一个间接的指标(系统 load)来做限流。Sentinel 在系统自适应保护的实际做法是,用系统负载作为启动控制流量的值,而允许通过的流量由处理请求的能力,即请求的响应时间以及当前系统正在处理的请求速率来决定。

系统保护规则是从应用级别的入口流量进行控制,从单台机器的总体 Load、RT、入口 QPS 和线程数四个维度监控应用数据,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。

系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量(EntryType.IN),比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。

系统规则支持以下的阈值类型:

  • Load(仅对 Linux/Unix-like 机器生效):当系统 load1 超过阈值,且系统当前的并发线程数超过系统容量时才会触发系统保护。系统容量由系统的 maxQps * minRt 计算得出。设定参考值一般是 CPU cores * 2.5
  • CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0)。
  • RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
  • 线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
  • 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。

注:这种系统自适应算法对于低 load 的请求,它的效果是一个“兜底”的角色。对于不是应用本身造成的 load 高的情况(如其它进程导致的不稳定的情况),效果不明显。

Sentinel 集群流量控制

集群流量控制简介

集群流控可以精确地控制整个集群的调用总量,结合单机限流兜底,可以更好地发挥流量控制的效果。

集群流控中共有两种身份:

  • Token Client:集群流控客户端,用于向所属 Token Server 通信请求 token。集群限流服务端会返回给客户端结果,决定是否限流。
  • Token Server:即集群流控服务端,处理来自 Token Client 的请求,根据配置的集群规则判断是否应该发放 token(是否允许通过)。

Sentinel 1.4.0 开始引入了集群流控模块,主要包含以下几部分:

  • sentinel-cluster-common-default: 公共模块,包含公共接口和实体
  • sentinel-cluster-client-default: 默认集群流控 client 模块,使用 Netty 进行通信,提供接口方便序列化协议扩展
  • sentinel-cluster-server-default: 默认集群流控 server 模块,使用 Netty 进行通信,提供接口方便序列化协议扩展;同时提供扩展接口对接规则判断的具体实现(TokenService),默认实现是复用 sentinel-core 的相关逻辑

集群流量控制规则

FlowRule 添加了两个字段用于集群限流相关配置:

1
2
private boolean clusterMode; // 标识是否为集群限流配置
private ClusterFlowConfig clusterConfig; // 集群限流相关配置项

其中 用一个专门的 ClusterFlowConfig 代表集群限流相关配置项,以与现有规则配置项分开:

1
2
3
4
5
6
7
8
9
10
// 全局唯一的规则 ID,由集群限流管控端分配.
private Long flowId;

// 阈值模式,默认(0)为单机均摊,1 为全局阈值.
private int thresholdType = ClusterRuleConstant.FLOW_THRESHOLD_AVG_LOCAL;

private int strategy = ClusterRuleConstant.FLOW_CLUSTER_STRATEGY_NORMAL;

// 在 client 连接失败或通信失败时,是否退化到本地的限流模式
private boolean fallbackToLocalWhenFail = true;
  • flowId 代表全局唯一的规则 ID,Sentinel 集群限流服务端通过此 ID 来区分各个规则,因此务必保持全局唯一。一般 flowId 由统一的管控端进行分配,或写入至 DB 时生成。
  • thresholdType 代表集群限流阈值模式。其中单机均摊模式下配置的阈值等同于单机能够承受的限额,token server 会根据客户端对应的 namespace(默认为 project.name 定义的应用名)下的连接数来计算总的阈值(比如独立模式下有 3 个 client 连接到了 token server,然后配的单机均摊阈值为 10,则计算出的集群总量就为 30);而全局模式下配置的阈值等同于整个集群的总阈值

ParamFlowRule 热点参数限流相关的集群配置与 FlowRule 相似。

Sentinel 热点参数限流

热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:

  • 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
  • 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制

热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。

Sentinel Parameter Flow Control

Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。

要使用热点参数限流功能,需要引入以下依赖:

1
2
3
4
5
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-parameter-flow-control</artifactId>
<version>x.y.z</version>
</dependency>

然后为对应的资源配置热点参数限流规则,并在 entry 的时候传入相应的参数,即可使热点参数限流生效。

注:若自行扩展并注册了自己实现的 SlotChainBuilder,并希望使用热点参数限流功能,则可以在 chain 里面合适的地方插入 ParamFlowSlot

那么如何传入对应的参数以便 Sentinel 统计呢?我们可以通过 SphU 类里面几个 entry 重载方法来传入:

1
2
3
public static Entry entry(String name, EntryType type, int count, Object... args) throws BlockException

public static Entry entry(Method method, EntryType type, int count, Object... args) throws BlockException

其中最后的一串 args 就是要传入的参数,有多个就按照次序依次传入。比如要传入两个参数 paramAparamB,则可以:

1
2
3
// paramA in index 0, paramB in index 1.
// 若需要配置例外项或者使用集群维度流控,则传入的参数只支持基本类型。
SphU.entry(resourceName, EntryType.IN, 1, paramA, paramB);

注意:若 entry 的时候传入了热点参数,那么 exit 的时候也一定要带上对应的参数(exit(count, args)),否则可能会有统计错误。正确的示例:

1
2
3
4
5
6
7
8
9
10
11
Entry entry = null;
try {
entry = SphU.entry(resourceName, EntryType.IN, 1, paramA, paramB);
// Your logic here.
} catch (BlockException ex) {
// Handle request rejection.
} finally {
if (entry != null) {
entry.exit(1, paramA, paramB);
}
}

对于 @SentinelResource 注解方式定义的资源,若注解作用的方法上有参数,Sentinel 会将它们作为参数传入 SphU.entry(res, args)。比如以下的方法里面 uidtype 会分别作为第一个和第二个参数传入 Sentinel API,从而可以用于热点规则判断:

1
2
3
4
@SentinelResource("myMethod")
public Result doSomething(String uid, int type) {
// some logic here...
}

来源访问控制(黑白名单)

很多时候,我们需要根据调用方来限制资源是否通过,这时候可以使用 Sentinel 的黑白名单控制的功能。黑白名单根据资源的请求来源(origin)限制资源是否通过,若配置白名单则只有请求来源位于白名单内时才可通过;若配置黑名单则请求来源位于黑名单时不通过,其余的请求通过。

调用方信息通过 ContextUtil.enter(resourceName, origin) 方法中的 origin 参数传入。

黑白名单规则(AuthorityRule)非常简单,主要有以下配置项:

  • resource:资源名,即限流规则的作用对象
  • limitApp:对应的黑名单/白名单,不同 origin 用 , 分隔,如 appA,appB
  • strategy:限制模式,AUTHORITY_WHITE 为白名单模式,AUTHORITY_BLACK 为黑名单模式,默认为白名单模式

注解埋点支持

Sentinel 提供了 @SentinelResource 注解用于定义资源,并提供了 AspectJ 的扩展用于自动定义资源、处理 BlockException 等。使用 Sentinel Annotation AspectJ Extension 的时候需要引入以下依赖:

1
2
3
4
5
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-annotation-aspectj</artifactId>
<version>x.y.z</version>
</dependency>

@SentinelResource 用于定义资源,并提供可选的异常处理和 fallback 配置项。 特别地,若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出 BlockException 时只会进入 blockHandler 处理逻辑。若未配置 blockHandlerfallbackdefaultFallback,则被限流降级时会将 BlockException 直接抛出

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class TestService {

// 对应的 `handleException` 函数需要位于 `ExceptionUtil` 类中,并且必须为 static 函数.
@SentinelResource(value = "test", blockHandler = "handleException", blockHandlerClass = {ExceptionUtil.class})
public void test() {
System.out.println("Test");
}

// 原函数
@SentinelResource(value = "hello", blockHandler = "exceptionHandler", fallback = "helloFallback")
public String hello(long s) {
return String.format("Hello at %d", s);
}

// Fallback 函数,函数签名与原函数一致或加一个 Throwable 类型的参数.
public String helloFallback(long s) {
return String.format("Halooooo %d", s);
}

// Block 异常处理函数,参数最后多一个 BlockException,其余与原函数一致.
public String exceptionHandler(long s, BlockException ex) {
// Do some log here.
ex.printStackTrace();
return "Oops, error occurred at " + s;
}
}

动态规则扩展

Sentinel 提供两种方式修改规则:

  • 通过 API 直接修改 (loadRules)
  • 通过 DataSource 适配不同数据源修改

通过 API 修改比较直观,可以通过以下几个 API 修改不同的规则:

1
2
FlowRuleManager.loadRules(List<FlowRule> rules); // 修改流控规则
DegradeRuleManager.loadRules(List<DegradeRule> rules); // 修改降级规则

手动修改规则(硬编码方式)一般仅用于测试和演示,生产上一般通过动态规则源的方式来动态管理规则。

上述 loadRules() 方法只接受内存态的规则对象,但更多时候规则存储在文件、数据库或者配置中心当中。DataSource 接口给我们提供了对接任意配置源的能力。相比直接通过 API 修改规则,实现 DataSource 接口是更加可靠的做法。

我们推荐通过控制台设置规则后将规则推送到统一的规则中心,客户端实现 ReadableDataSource 接口端监听规则中心实时获取变更,流程如下:

push-rules-from-dashboard-to-config-center

DataSource 扩展常见的实现方式有:

  • 拉模式:客户端主动向某个规则管理中心定期轮询拉取规则,这个规则中心可以是 RDBMS、文件,甚至是 VCS 等。这样做的方式是简单,缺点是无法及时获取变更;
  • 推模式:规则中心统一推送,客户端通过注册监听器的方式时刻监听变化,比如使用 Nacos、Zookeeper 等配置中心。这种方式有更好的实时性和一致性保证。

Sentinel 目前支持以下数据源扩展:

流量治理标准数据源:OpenSergo

拉模式扩展

实现拉模式的数据源最简单的方式是继承 AutoRefreshDataSource 抽象类,然后实现 readSource() 方法,在该方法里从指定数据源读取字符串格式的配置数据。比如 基于文件的数据源

推模式扩展

实现推模式的数据源最简单的方式是继承 AbstractDataSource 抽象类,在其构造方法中添加监听器,并实现 readSource() 从指定数据源读取字符串格式的配置数据。比如 基于 Nacos 的数据源

注册数据源

通常需要调用以下方法将数据源注册至指定的规则管理器中:

1
2
ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new NacosDataSource<>(remoteAddress, groupId, dataId, parser);
FlowRuleManager.register2Property(flowRuleDataSource.getProperty());

若不希望手动注册数据源,可以借助 Sentinel 的 InitFunc SPI 扩展接口。只需要实现自己的 InitFunc 接口,在 init 方法中编写注册数据源的逻辑。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.test.init;

public class DataSourceInitFunc implements InitFunc {

@Override
public void init() throws Exception {
final String remoteAddress = "localhost";
final String groupId = "Sentinel:Demo";
final String dataId = "com.alibaba.csp.sentinel.demo.flow.rule";

ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new NacosDataSource<>(remoteAddress, groupId, dataId,
source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {}));
FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
}
}

接着将对应的类名添加到位于资源目录(通常是 resource 目录)下的 META-INF/services 目录下的 com.alibaba.csp.sentinel.init.InitFunc 文件中,比如:

1
com.test.init.DataSourceInitFunc

这样,当初次访问任意资源的时候,Sentinel 就可以自动去注册对应的数据源了。

常见问题

为什么有时候限流不是完全精准

滑动时间窗是在固定时间窗算法基础上增加了样本数配置,支持按需配置时间片。sentinel 默认 1 秒两个样本,即 500ms 为一个时间窗口。样本数越多,计算越精确,但性能损耗更多。选择 500ms 是一个性能与精确统计的折中值。对于限流场景来说,性能可能比统计准确更重要,所以 sentinel 不是完全精准按照配置的阈值来限流,当然也不会相差很多。这是为了保证业务服务的高性能,减少限流组件对业务服务的性能影响。
另外,sentinel 监控数据最小精度是秒级的,但是实际统计窗口是 500ms,所以对某些极端场景的突刺流量在监控上不能很好的展示,这个是监控数据聚合的问题,不是限流不准的问题。

参考资料

Linux 命令 Cheat Sheet

常见命令分类

基础

  • 学习 Bash 的基础知识。具体地,在命令行中输入 man bash 并至少全文浏览一遍; 它理解起来很简单并且不冗长。其他的 shell 可能很好用,但 Bash 的功能已经足够强大并且到几乎总是可用的( 如果你学习 zsh,fish 或其他的 shell 的话,在你自己的设备上会显得很方便,但过度依赖这些功能会给您带来不便,例如当你需要在服务器上工作时)。

  • 熟悉至少一个基于文本的编辑器。通常而言 Vim (vi) 会是你最好的选择,毕竟在终端中编辑文本时 Vim 是最好用的工具(甚至大部分情况下 Vim 要比 Emacs、大型 IDE 或是炫酷的编辑器更好用)。

  • 学会如何使用 man 命令去阅读文档。学会使用 apropos 去查找文档。知道有些命令并不对应可执行文件,而是在 Bash 内置好的,此时可以使用 helphelp -d 命令获取帮助信息。你可以用 type 命令 来判断这个命令到底是可执行文件、shell 内置命令还是别名。

  • 学会使用 >< 来重定向输出和输入,学会使用 | 来重定向管道。明白 > 会覆盖了输出文件而 >> 是在文件末添加。了解标准输出 stdout 和标准错误 stderr。

  • 学会使用通配符 * (或许再算上 ?[]) 和引用以及引用中 '" 的区别(后文中有一些具体的例子)。

  • 熟悉 Bash 中的任务管理工具:&ctrl-zctrl-cjobsfgbgkill 等。

  • 学会使用 ssh 进行远程命令行登录,最好知道如何使用 ssh-agentssh-add 等命令来实现基础的无密码认证登录。

  • 学会基本的文件管理工具:lsls -l (了解 ls -l 中每一列代表的意义),lessheadtailtail -f (甚至 less +F),lnln -s (了解硬链接与软链接的区别),chownchmoddu (硬盘使用情况概述:du -hs *)。 关于文件系统的管理,学习 dfmountfdiskmkfslsblk。知道 inode 是什么(与 ls -idf -i 等命令相关)。

  • 学习基本的网络管理工具:ipifconfigdig

  • 学习并使用一种版本控制管理系统,例如 git

  • 熟悉正则表达式,学会使用 grepegrep,它们的参数中 -i-o-v-A-B-C 这些是很常用并值得认真学习的。

  • 学会使用 apt-getyumdnfpacman (具体使用哪个取决于你使用的 Linux 发行版)来查找和安装软件包。并确保你的环境中有 pip 来安装基于 Python 的命令行工具 (接下来提到的部分程序使用 pip 来安装会很方便)。

日常使用

  • 在 Bash 中,可以通过按 Tab 键实现自动补全参数,使用 ctrl-r 搜索命令行历史记录(按下按键之后,输入关键字便可以搜索,重复按下 ctrl-r 会向后查找匹配项,按下 Enter 键会执行当前匹配的命令,而按下右方向键会将匹配项放入当前行中,不会直接执行,以便做出修改)。

  • 在 Bash 中,可以按下 ctrl-w 删除你键入的最后一个单词,ctrl-u 可以删除行内光标所在位置之前的内容,alt-balt-f 可以以单词为单位移动光标,ctrl-a 可以将光标移至行首,ctrl-e 可以将光标移至行尾,ctrl-k 可以删除光标至行尾的所有内容,ctrl-l 可以清屏。键入 man readline 可以查看 Bash 中的默认快捷键。内容有很多,例如 alt-. 循环地移向前一个参数,而 alt-* 可以展开通配符。

  • 你喜欢的话,可以执行 set -o vi 来使用 vi 风格的快捷键,而执行 set -o emacs 可以把它改回来。

  • 为了便于编辑长命令,在设置你的默认编辑器后(例如 export EDITOR=vim),ctrl-x ctrl-e 会打开一个编辑器来编辑当前输入的命令。在 vi 风格下快捷键则是 escape-v

  • 键入 history 查看命令行历史记录,再用 !nn 是命令编号)就可以再次执行。其中有许多缩写,最有用的大概就是 !$, 它用于指代上次键入的参数,而 !! 可以指代上次键入的命令了(参考 man 页面中的“HISTORY EXPANSION”)。不过这些功能,你也可以通过快捷键 ctrl-ralt-. 来实现。

  • cd 命令可以切换工作路径,输入 cd \~ 可以进入 home 目录。要访问你的 home 目录中的文件,可以使用前缀 \~(例如 \~/.bashrc)。在 sh 脚本里则用环境变量 $HOME 指代 home 目录的路径。

  • 回到前一个工作路径:cd -

  • 如果你输入命令的时候中途改了主意,按下 alt-# 在行首添加 # 把它当做注释再按下回车执行(或者依次按下 ctrl-a, **#**, enter)。这样做的话,之后借助命令行历史记录,你可以很方便恢复你刚才输入到一半的命令。

  • 使用 xargs ( 或 parallel)。他们非常给力。注意到你可以控制每行参数个数(-L)和最大并行数(-P)。如果你不确定它们是否会按你想的那样工作,先使用 xargs echo 查看一下。此外,使用 -I{} 会很方便。例如:

1
2
find . -name '*.py' | xargs grep some_function
cat hosts | xargs -I{} ssh root@{} hostname
  • pstree -p 以一种优雅的方式展示进程树。

  • 使用 pgreppkill 根据名字查找进程或发送信号(-f 参数通常有用)。

  • 了解你可以发往进程的信号的种类。比如,使用 kill -STOP [pid] 停止一个进程。使用 man 7 signal 查看详细列表。

  • 使用 nohupdisown 使一个后台进程持续运行。

  • 使用 netstat -lntpss -plat 检查哪些进程在监听端口(默认是检查 TCP 端口; 添加参数 -u 则检查 UDP 端口)或者 lsof -iTCP -sTCP:LISTEN -P -n (这也可以在 OS X 上运行)。

  • lsof 来查看开启的套接字和文件。

  • 使用 uptimew 来查看系统已经运行多长时间。

  • 使用 alias 来创建常用命令的快捷形式。例如:alias ll='ls -latr' 创建了一个新的命令别名 ll

  • 可以把别名、shell 选项和常用函数保存在 \~/.bashrc,具体看下这篇文章。这样做的话你就可以在所有 shell 会话中使用你的设定。

  • 把环境变量的设定以及登陆时要执行的命令保存在 \~/.bash_profile。而对于从图形界面启动的 shell 和 cron 启动的 shell,则需要单独配置文件。

  • 要想在几台电脑中同步你的配置文件(例如 .bashrc.bash_profile),可以借助 Git。

  • 当变量和文件名中包含空格的时候要格外小心。Bash 变量要用引号括起来,比如 "$FOO"。尽量使用 -0-print0 选项以便用 NULL 来分隔文件名,例如 locate -0 pattern | xargs -0 ls -alfind / -print0 -type d | xargs -0 ls -al。如果 for 循环中循环访问的文件名含有空字符(空格、tab 等字符),只需用 IFS=$'\n' 把内部字段分隔符设为换行符。

  • 在 Bash 脚本中,使用 set -x 去调试输出(或者使用它的变体 set -v,它会记录原始输入,包括多余的参数和注释)。尽可能地使用严格模式:使用 set -e 令脚本在发生错误时退出而不是继续运行;使用 set -u 来检查是否使用了未赋值的变量;试试 set -o pipefail,它可以监测管道中的错误。当牵扯到很多脚本时,使用 trap 来检测 ERR 和 EXIT。一个好的习惯是在脚本文件开头这样写,这会使它能够检测一些错误,并在错误发生时中断程序并输出信息:

1
2
set -euo pipefail
trap "echo 'error: Script failed: see failed command above'" ERR
  • 在 Bash 脚本中,子 shell(使用括号 (...))是一种组织参数的便捷方式。一个常见的例子是临时地移动工作路径,代码如下:
1
2
3
# do something in current dir
(cd /some/other/dir && other-command)
# continue in original dir
  • 在 Bash 中,变量有许多的扩展方式。${name:?error message} 用于检查变量是否存在。此外,当 Bash 脚本只需要一个参数时,可以使用这样的代码 input_file=${1:?usage: $0 input_file}。在变量为空时使用默认值:${name:-default}。如果你要在之前的例子中再加一个(可选的)参数,可以使用类似这样的代码 output_file=${2:-logfile},如果省略了 $2,它的值就为空,于是 output_file 就会被设为 logfile。数学表达式:i=$(( (i + 1) % 5 ))。序列:{1..10}。截断字符串:${var%suffix}${var#prefix}。例如,假设 var=foo.pdf,那么 echo ${var%.pdf}.txt 将输出 foo.txt

  • 使用括号扩展({})来减少输入相似文本,并自动化文本组合。这在某些情况下会很有用,例如 mv foo.{txt,pdf} some-dir(同时移动两个文件),cp somefile{,.bak}(会被扩展成 cp somefile somefile.bak)或者 mkdir -p test-{a,b,c}/subtest-{1,2,3}(会被扩展成所有可能的组合,并创建一个目录树)。

  • 通过使用 <(some command) 可以将输出视为文件。例如,对比本地文件 /etc/hosts 和一个远程文件:

1
diff /etc/hosts <(ssh somehost cat /etc/hosts)
  • 编写脚本时,你可能会想要把代码都放在大括号里。缺少右括号的话,代码就会因为语法错误而无法执行。如果你的脚本是要放在网上分享供他人使用的,这样的写法就体现出它的好处了,因为这样可以防止下载不完全代码被执行。
1
2
3
{
# 在这里写代码
}
  • 了解 Bash 中的“here documents”,例如 cat <<EOF ...

  • 在 Bash 中,同时重定向标准输出和标准错误:some-command >logfile 2>&1 或者 some-command &>logfile。通常,为了保证命令不会在标准输入里残留一个未关闭的文件句柄捆绑在你当前所在的终端上,在命令后添加 </dev/null 是一个好习惯。

  • 使用 man ascii 查看具有十六进制和十进制值的 ASCII 表。man unicodeman utf-8,以及 man latin1 有助于你去了解通用的编码信息。

  • 使用 screentmux 来使用多份屏幕,当你在使用 ssh 时(保存 session 信息)将尤为有用。而 byobu 可以为它们提供更多的信息和易用的管理工具。另一个轻量级的 session 持久化解决方案是 dtach

  • ssh 中,了解如何使用 -L-D(偶尔需要用 -R)开启隧道是非常有用的,比如当你需要从一台远程服务器上访问 web 页面。

  • 对 ssh 设置做一些小优化可能是很有用的,例如这个 \~/.ssh/config 文件包含了防止特定网络环境下连接断开、压缩数据、多通道等选项:

1
2
3
4
5
6
7
TCPKeepAlive=yes
ServerAliveInterval=15
ServerAliveCountMax=6
Compression=yes
ControlMaster auto
ControlPath /tmp/%r@%h:%p
ControlPersist yes
  • 一些其他的关于 ssh 的选项是与安全相关的,应当小心翼翼的使用。例如你应当只能在可信任的网络中启用 StrictHostKeyChecking=noForwardAgent=yes

  • 考虑使用 mosh 作为 ssh 的替代品,它使用 UDP 协议。它可以避免连接被中断并且对带宽需求更小,但它需要在服务端做相应的配置。

  • 获取八进制形式的文件访问权限(修改系统设置时通常需要,但 ls 的功能不那么好用并且通常会搞砸),可以使用类似如下的代码:

1
stat -c '%A %a %n' /etc/timezone
  • 使用 percol 或者 fzf 可以交互式地从另一个命令输出中选取值。

  • 使用 fppPathPicker)可以与基于另一个命令(例如 git)输出的文件交互。

  • 将 web 服务器上当前目录下所有的文件(以及子目录)暴露给你所处网络的所有用户,使用:
    python -m SimpleHTTPServer 7777 (使用端口 7777 和 Python 2)或python -m http.server 7777 (使用端口 7777 和 Python 3)。

  • 以其他用户的身份执行命令,使用 sudo。默认以 root 用户的身份执行;使用 -u 来指定其他用户。使用 -i 来以该用户登录(需要输入你自己的密码)。

  • 将 shell 切换为其他用户,使用 su username 或者 sudo - username。加入 - 会使得切换后的环境与使用该用户登录后的环境相同。省略用户名则默认为 root。切换到哪个用户,就需要输入哪个用户的密码。

  • 了解命令行的 128K 限制。使用通配符匹配大量文件名时,常会遇到“Argument list too long”的错误信息。(这种情况下换用 findxargs 通常可以解决。)

  • 当你需要一个基本的计算器时,可以使用 python 解释器(当然你要用 python 的时候也是这样)。例如:

1
2
>>> 2+3
5

文件及数据处理

  • 在当前目录下通过文件名查找一个文件,使用类似于这样的命令:find . -iname '*something*'。在所有路径下通过文件名查找文件,使用 locate something (但注意到 updatedb 可能没有对最近新建的文件建立索引,所以你可能无法定位到这些未被索引的文件)。

  • 使用 ag 在源代码或数据文件里检索(grep -r 同样可以做到,但相比之下 ag 更加先进)。

  • 将 HTML 转为文本:lynx -dump -stdin

  • Markdown,HTML,以及所有文档格式之间的转换,试试 pandoc

  • 当你要处理棘手的 XML 时候,xmlstarlet 算是上古时代流传下来的神器。

  • 使用 jq 处理 JSON。

  • 使用 shyaml 处理 YAML。

  • 要处理 Excel 或 CSV 文件的话,csvkit 提供了 in2csvcsvcutcsvjoincsvgrep 等方便易用的工具。

  • 当你要处理 Amazon S3 相关的工作的时候,s3cmd 是一个很方便的工具而 s4cmd 的效率更高。Amazon 官方提供的 aws 以及 saws 是其他 AWS 相关工作的基础,值得学习。

  • 了解如何使用 sortuniq,包括 uniq 的 -u 参数和 -d 参数,具体内容在后文单行脚本节中。另外可以了解一下 comm

  • 了解如何使用 cutpastejoin 来更改文件。很多人都会使用 cut,但遗忘了 join

  • 了解如何运用 wc 去计算新行数(-l),字符数(-m),单词数(-w)以及字节数(-c)。

  • 了解如何使用 tee 将标准输入复制到文件甚至标准输出,例如 ls -al | tee file.txt

  • 要进行一些复杂的计算,比如分组、逆序和一些其他的统计分析,可以考虑使用 datamash

  • 注意到语言设置(中文或英文等)对许多命令行工具有一些微妙的影响,比如排序的顺序和性能。大多数 Linux 的安装过程会将 LANG 或其他有关的变量设置为符合本地的设置。要意识到当你改变语言设置时,排序的结果可能会改变。明白国际化可能会使 sort 或其他命令运行效率下降许多倍。某些情况下(例如集合运算)你可以放心的使用 export LC_ALL=C 来忽略掉国际化并按照字节来判断顺序。

  • 你可以单独指定某一条命令的环境,只需在调用时把环境变量设定放在命令的前面,例如 TZ=Pacific/Fiji date 可以获取斐济的时间。

  • 了解如何使用 awksed 来进行简单的数据处理。 参阅 One-liners 获取示例。

  • 替换一个或多个文件中出现的字符串:

1
perl -pi.bak -e 's/old-string/new-string/g' my-files-*.txt
  • 使用 repren 来批量重命名文件,或是在多个文件中搜索替换内容。(有些时候 rename 命令也可以批量重命名,但要注意,它在不同 Linux 发行版中的功能并不完全一样。)
1
2
3
4
5
6
# 将文件、目录和内容全部重命名 foo -> bar:
repren --full --preserve-case --from foo --to bar .
# 还原所有备份文件 whatever.bak -> whatever:
repren --renames --from '(.*)\.bak' --to '\1' *.bak
# 用 rename 实现上述功能(若可用):
rename 's/\.bak$//' *.bak
  • 根据 man 页面的描述,rsync 是一个快速且非常灵活的文件复制工具。它闻名于设备之间的文件同步,但其实它在本地情况下也同样有用。在安全设置允许下,用 rsync 代替 scp 可以实现文件续传,而不用重新从头开始。它同时也是删除大量文件的最快方法之一:
1
mkdir empty && rsync -r --delete empty/ some-dir && rmdir some-dir
  • 若要在复制文件时获取当前进度,可使用 pvpycpprogressrsync --progress。若所执行的复制为 block 块拷贝,可以使用 dd status=progress

  • 使用 shuf 可以以行为单位来打乱文件的内容或从一个文件中随机选取多行。

  • 了解 sort 的参数。显示数字时,使用 -n 或者 -h 来显示更易读的数(例如 du -h 的输出)。明白排序时关键字的工作原理(-t-k)。例如,注意到你需要 -k1,1 来仅按第一个域来排序,而 -k1 意味着按整行排序。稳定排序(sort -s)在某些情况下很有用。例如,以第二个域为主关键字,第一个域为次关键字进行排序,你可以使用 sort -k1,1 | sort -s -k2,2

  • 如果你想在 Bash 命令行中写 tab 制表符,按下 ctrl-v [Tab] 或键入 $'\t' (后者可能更好,因为你可以复制粘贴它)。

  • 标准的源代码对比及合并工具是 diffpatch。使用 diffstat 查看变更总览数据。注意到 diff -r 对整个文件夹有效。使用 diff -r tree1 tree2 | diffstat 查看变更的统计数据。vimdiff 用于比对并编辑文件。

  • 对于二进制文件,使用 hdhexdump 或者 xxd 使其以十六进制显示,使用 bvihexedit 或者 biew 来进行二进制编辑。

  • 同样对于二进制文件,strings(包括 grep 等工具)可以帮助在二进制文件中查找特定比特。

  • 制作二进制差分文件(Delta 压缩),使用 xdelta3

  • 使用 iconv 更改文本编码。需要更高级的功能,可以使用 uconv,它支持一些高级的 Unicode 功能。例如,这条命令移除了所有重音符号:

1
uconv -f utf-8 -t utf-8 -x '::Any-Lower; ::Any-NFD; [:Nonspacing Mark:] >; ::Any-NFC; ' < input.txt > output.txt
  • 拆分文件可以使用 split(按大小拆分)和 csplit(按模式拆分)。

  • 操作日期和时间表达式,可以用 dateutils 中的 dateadddatediffstrptime 等工具。

  • 使用 zlesszmorezcatzgrep 对压缩过的文件进行操作。

  • 文件属性可以通过 chattr 进行设置,它比文件权限更加底层。例如,为了保护文件不被意外删除,可以使用不可修改标记:sudo chattr +i /critical/directory/or/file

  • 使用 getfaclsetfacl 以保存和恢复文件权限。例如:

1
2
getfacl -R /some/path > permissions.txt
setfacl --restore=permissions.txt
  • 为了高效地创建空文件,请使用 truncate(创建稀疏文件),fallocate(用于 ext4,xfs,btrf 和 ocfs2 文件系统),xfs_mkfile(适用于几乎所有的文件系统,包含在 xfsprogs 包中),mkfile(用于类 Unix 操作系统,比如 Solaris 和 Mac OS)。

系统调试

  • curlcurl -I 可以被轻松地应用于 web 调试中,它们的好兄弟 wget 也是如此,或者也可以试试更潮的 httpie

  • 获取 CPU 和硬盘的使用状态,通常使用使用 tophtop 更佳),iostatiotop。而 iostat -mxz 15 可以让你获悉 CPU 和每个硬盘分区的基本信息和性能表现。

  • 使用 netstatss 查看网络连接的细节。

  • dstat 在你想要对系统的现状有一个粗略的认识时是非常有用的。然而若要对系统有一个深度的总体认识,使用 glances,它会在一个终端窗口中向你提供一些系统级的数据。

  • 若要了解内存状态,运行并理解 freevmstat 的输出。值得留意的是“cached”的值,它指的是 Linux 内核用来作为文件缓存的内存大小,而与空闲内存无关。

  • Java 系统调试则是一件截然不同的事,一个可以用于 Oracle 的 JVM 或其他 JVM 上的调试的技巧是你可以运行 kill -3 <pid> 同时一个完整的栈轨迹和堆概述(包括 GC 的细节)会被保存到标准错误或是日志文件。JDK 中的 jpsjstatjstackjmap 很有用。SJK tools 更高级。

  • 使用 mtr 去跟踪路由,用于确定网络问题。

  • ncdu 来查看磁盘使用情况,它比寻常的命令,如 du -sh *,更节省时间。

  • 查找正在使用带宽的套接字连接或进程,使用 iftopnethogs

  • ab 工具(Apache 中自带)可以简单粗暴地检查 web 服务器的性能。对于更复杂的负载测试,使用 siege

  • wiresharktsharkngrep 可用于复杂的网络调试。

  • 了解 straceltrace。这俩工具在你的程序运行失败、挂起甚至崩溃,而你却不知道为什么或你想对性能有个总体的认识的时候是非常有用的。注意 profile 参数(-c)和附加到一个运行的进程参数 (-p)。

  • 了解使用 ldd 来检查共享库。但是永远不要在不信任的文件上运行

  • 了解如何运用 gdb 连接到一个运行着的进程并获取它的堆栈轨迹。

  • 学会使用 /proc。它在调试正在出现的问题的时候有时会效果惊人。比如:/proc/cpuinfo/proc/meminfo/proc/cmdline/proc/xxx/cwd/proc/xxx/exe/proc/xxx/fd//proc/xxx/smaps(这里的 xxx 表示进程的 id 或 pid)。

  • 当调试一些之前出现的问题的时候,sar 非常有用。它展示了 cpu、内存以及网络等的历史数据。

  • 关于更深层次的系统分析以及性能分析,看看 stapSystemTap),perf,以及sysdig

  • 查看你当前使用的系统,使用 unameuname -a(Unix/kernel 信息)或者 lsb_release -a(Linux 发行版信息)。

  • 无论什么东西工作得很欢乐(可能是硬件或驱动问题)时可以试试 dmesg

  • 如果你删除了一个文件,但通过 du 发现没有释放预期的磁盘空间,请检查文件是否被进程占用:
    lsof | grep deleted | grep "filename-of-my-big-file"

单行脚本

一些命令组合的例子:

  • 当你需要对文本文件做集合交、并、差运算时,sortuniq 会是你的好帮手。具体例子请参照代码后面的,此处假设 ab 是两内容不同的文件。这种方式效率很高,并且在小文件和上 G 的文件上都能运用(注意尽管在 /tmp 在一个小的根分区上时你可能需要 -T 参数,但是实际上 sort 并不被内存大小约束),参阅前文中关于 LC_ALLsort-u 参数的部分。
1
2
3
sort a b | uniq > c   # c 是 a 并 b
sort a b | uniq -d > c # c 是 a 交 b
sort a b b | uniq -u > c # c 是 a - b
  • 使用 grep . *(每行都会附上文件名)或者 head -100 *(每个文件有一个标题)来阅读检查目录下所有文件的内容。这在检查一个充满配置文件的目录(如 /sys/proc/etc)时特别好用。
  • 计算文本文件第三列中所有数的和(可能比同等作用的 Python 代码快三倍且代码量少三倍):
1
awk '{ x += $3 } END { print x }' myfile
  • 如果你想在文件树上查看大小/日期,这可能看起来像递归版的 ls -l 但比 ls -lR 更易于理解:
1
find . -type f -ls
  • 假设你有一个类似于 web 服务器日志文件的文本文件,并且一个确定的值只会出现在某些行上,假设一个 acct_id 参数在 URI 中。如果你想计算出每个 acct_id 值有多少次请求,使用如下代码:
1
egrep -o 'acct_id=[0-9]+' access.log | cut -d= -f2 | sort | uniq -c | sort -rn
  • 要持续监测文件改动,可以使用 watch,例如检查某个文件夹中文件的改变,可以用 watch -d -n 2 'ls -rtlh | tail';或者在排查 WiFi 设置故障时要监测网络设置的更改,可以用 watch -d -n 2 ifconfig

  • 运行这个函数从这篇文档中随机获取一条技巧(解析 Markdown 文件并抽取项目):

1
2
3
4
5
6
7
8
function taocl() {
curl -s https://raw.githubusercontent.com/jlevy/the-art-of-command-line/master/README-zh.md|
pandoc -f markdown -t html |
iconv -f 'utf-8' -t 'unicode' |
xmlstarlet fo --html --dropdtd |
xmlstarlet sel -t -v "(html/body/ul/li[count(p)>0])[$RANDOM mod last()+1]" |
xmlstarlet unesc | fmt -80
}

冷门但有用

  • expr:计算表达式或正则匹配

  • m4:简单的宏处理器

  • yes:多次打印字符串

  • cal:漂亮的日历

  • env:执行一个命令(脚本文件中很有用)

  • printenv:打印环境变量(调试时或在写脚本文件时很有用)

  • look:查找以特定字符串开头的单词或行

  • cutpastejoin:数据修改

  • fmt:格式化文本段落

  • pr:将文本格式化成页/列形式

  • fold:包裹文本中的几行

  • column:将文本格式化成多个对齐、定宽的列或表格

  • expandunexpand:制表符与空格之间转换

  • nl:添加行号

  • seq:打印数字

  • bc:计算器

  • factor:分解因数

  • gpg:加密并签名文件

  • toe:terminfo 入口列表

  • nc:网络调试及数据传输

  • socat:套接字代理,与 netcat 类似

  • slurm:网络流量可视化

  • dd:文件或设备间传输数据

  • file:确定文件类型

  • tree:以树的形式显示路径和文件,类似于递归的 ls

  • stat:文件信息

  • time:执行命令,并计算执行时间

  • timeout:在指定时长范围内执行命令,并在规定时间结束后停止进程

  • lockfile:使文件只能通过 rm -f 移除

  • logrotate: 切换、压缩以及发送日志文件

  • watch:重复运行同一个命令,展示结果并/或高亮有更改的部分

  • when-changed:当检测到文件更改时执行指定命令。参阅 inotifywaitentr

  • tac:反向输出文件

  • shuf:文件中随机选取几行

  • comm:一行一行的比较排序过的文件

  • strings:从二进制文件中抽取文本

  • tr:转换字母

  • iconvuconv:文本编码转换

  • splitcsplit:分割文件

  • sponge:在写入前读取所有输入,在读取文件后再向同一文件写入时比较有用,例如 grep -v something some-file | sponge some-file

  • units:将一种计量单位转换为另一种等效的计量单位(参阅 /usr/share/units/definitions.units

  • apg:随机生成密码

  • xz:高比例的文件压缩

  • ldd:动态库信息

  • nm:提取 obj 文件中的符号

  • abwrk:web 服务器性能分析

  • strace:调试系统调用

  • mtr:更好的网络调试跟踪工具

  • cssh:可视化的并发 shell

  • rsync:通过 ssh 或本地文件系统同步文件和文件夹

  • wiresharktshark:抓包和网络调试工具

  • ngrep:网络层的 grep

  • hostdig:DNS 查找

  • lsof:列出当前系统打开文件的工具以及查看端口信息

  • dstat:系统状态查看

  • glances:高层次的多子系统总览

  • iostat:硬盘使用状态

  • mpstat: CPU 使用状态

  • vmstat: 内存使用状态

  • htop:top 的加强版

  • last:登入记录

  • w:查看处于登录状态的用户

  • id:用户/组 ID 信息

  • sar:系统历史数据

  • iftopnethogs:套接字及进程的网络利用情况

  • ss:套接字数据

  • dmesg:引导及系统错误信息

  • sysctl: 在内核运行时动态地查看和修改内核的运行参数

  • hdparm:SATA/ATA 磁盘更改及性能分析

  • lsblk:列出块设备信息:以树形展示你的磁盘以及磁盘分区信息

  • lshwlscpulspcilsusbdmidecode:查看硬件信息,包括 CPU、BIOS、RAID、显卡、USB 设备等

  • lsmodmodinfo:列出内核模块,并显示其细节

  • fortuneddatesl:额,这主要取决于你是否认为蒸汽火车和莫名其妙的名人名言是否“有用”

仅限 OS X 系统

以下是仅限于 OS X 系统的技巧。

  • brew (Homebrew)或者 port (MacPorts)进行包管理。这些可以用来在 OS X 系统上安装以上的大多数命令。

  • pbcopy 复制任何命令的输出到桌面应用,用 pbpaste 粘贴输入。

  • 若要在 OS X 终端中将 Option 键视为 alt 键(例如在上面介绍的 alt-balt-f 等命令中用到),打开 偏好设置 -> 描述文件 -> 键盘 并勾选“使用 Option 键作为 Meta 键”。

  • open 或者 open -a /Applications/Whatever.app 使用桌面应用打开文件。

  • Spotlight:用 mdfind 搜索文件,用 mdls 列出元数据(例如照片的 EXIF 信息)。

  • 注意 OS X 系统是基于 BSD UNIX 的,许多命令(例如 pslstailawksed)都和 Linux 中有微妙的不同( Linux 很大程度上受到了 System V-style Unix 和 GNU 工具影响)。你可以通过标题为 “BSD General Commands Manual” 的 man 页面发现这些不同。在有些情况下 GNU 版本的命令也可能被安装(例如 gawkgsed 对应 GNU 中的 awk 和 sed )。如果要写跨平台的 Bash 脚本,避免使用这些命令(例如,考虑 Python 或者 perl )或者经过仔细的测试。

  • sw_vers 获取 OS X 的版本信息。

仅限 Windows 系统

以下是仅限于 Windows 系统的技巧。

在 Winodws 下获取 Unix 工具

  • 可以安装 Cygwin 允许你在 Microsoft Windows 中体验 Unix shell 的威力。这样的话,本文中介绍的大多数内容都将适用。

  • 在 Windows 10 上,你可以使用 Bash on Ubuntu on Windows,它提供了一个熟悉的 Bash 环境,包含了不少 Unix 命令行工具。好处是它允许 Linux 上编写的程序在 Windows 上运行,而另一方面,Windows 上编写的程序却无法在 Bash 命令行中运行。

  • 如果你在 Windows 上主要想用 GNU 开发者工具(例如 GCC),可以考虑 MinGW 以及它的 MSYS 包,这个包提供了例如 bash,gawk,make 和 grep 的工具。MSYS 并不包含所有可以与 Cygwin 媲美的特性。当制作 Unix 工具的原生 Windows 端口时 MinGW 将特别地有用。

  • 另一个在 Windows 下实现接近 Unix 环境外观效果的选项是 Cash。注意在此环境下只有很少的 Unix 命令和命令行可用。

实用 Windows 命令行工具

  • 可以使用 wmic 在命令行环境下给大部分 Windows 系统管理任务编写脚本以及执行这些任务。

  • Windows 实用的原生命令行网络工具包括 pingipconfigtracert,和 netstat

  • 可以使用 Rundll32 命令来实现许多有用的 Windows 任务

Cygwin 技巧

  • 通过 Cygwin 的包管理器来安装额外的 Unix 程序。

  • 使用 mintty 作为你的命令行窗口。

  • 要访问 Windows 剪贴板,可以通过 /dev/clipboard

  • 运行 cygstart 以通过默认程序打开一个文件。

  • 要访问 Windows 注册表,可以使用 regtool

  • 注意 Windows 驱动器路径 C:\ 在 Cygwin 中用 /cygdrive/c 代表,而 Cygwin 的 / 代表 Windows 中的 C:\cygwin。要转换 Cygwin 和 Windows 风格的路径可以用 cygpath。这在需要调用 Windows 程序的脚本里很有用。

  • 学会使用 wmic,你就可以从命令行执行大多数 Windows 系统管理任务,并编成脚本。

  • 要在 Windows 下获得 Unix 的界面和体验,另一个办法是使用 Cash。需要注意的是,这个环境支持的 Unix 命令和命令行参数非常少。

  • 要在 Windows 上获取 GNU 开发者工具(比如 GCC)的另一个办法是使用 MinGW 以及它的 MSYS 软件包,该软件包提供了 bash、gawk、make、grep 等工具。然而 MSYS 提供的功能没有 Cygwin 完善。MinGW 在创建 Unix 工具的 Windows 原生移植方面非常有用。

更多资源

免责声明

除去特别小的工作,你编写的代码应当方便他人阅读。能力往往伴随着责任,你 有能力 在 Bash 中玩一些奇技淫巧并不意味着你应该去做!;)

授权条款

img

本文使用授权协议 Creative Commons Attribution-ShareAlike 4.0 International License

关系数据库简介

什么是关系型数据库

关系型数据库是指采用了关系模型来组织数据的数据库。关系模型是一种数据模型,它表示数据之间的联系,包括一对一、一对多和多对多的关系。在关系型数据库中,数据以表格的形式存储,每个表格称为一个“关系”,每个关系由行(记录或元组)和列(字段或属性)组成。

常见的关系型数据库有:MySQL、Oracle、PostgreSQL、MariaDB、SQL Server、SQLite 等。

什么是 SQL

SQL 是 Structured Query Language(结构 化查询语言)的缩写。SQL 是一种专门用来与数据库沟通的语言。

SQL 有两个主要的标准,分别是 SQL92 和 SQL99。92 和 99 代表了标准提出的时间,SQL92 就是 92 年提出的标准规范。除了 SQL92 和 SQL99 以外,还存在 SQL-86、SQL-89、SQL:2003、SQL:2008、SQL:2011 和 SQL:2016 等其他的标准。主流 RDBMS,比如 MySQL、Oracle、SQL Sever、DB2、PostgreSQL 等都支持 SQL 语言,也就是说它们的使用符合大部分 SQL 标准,但很难完全符合。

SQL 语言按照功能可以划分成以下的 4 个部分:

  • DDL 是 Data Definition Language 的缩写,即数据定义语言,它用来定义我们的数据库对象,包括数据库、数据表和列。通过使用 DDL,我们可以创建,删除和修改数据库和表结构。
  • DML 是 Data Manipulation Language 的缩写,即数据操作语言,我们用它操作和数据库相关的记录,比如增加、删除、修改数据表中的记录。
  • DCL 是 Data Control Language 的缩写,即数据控制语言,我们用它来定义访问权限和安全级别。
  • DQL 是 Data Query Language 的缩写,即数据查询语言,我们用它查询想要的记录,它是 SQL 语言的重中之重。在实际的业务中,我们绝大多数情况下都是在和查询打交道,因此学会编写正确且高效的查询语句,是学习的重点。

数据库核心术语

  • 数据库 - 数据库 (DataBase 简称 DB) 就是信息的集合或者说数据库是由数据库管理系统管理的数据的集合。
  • 数据库管理系统 - 数据库管理系统 (Database Management System 简称 DBMS) 是一种操纵和管理数据库的大型软件,通常用于建立、使用和维护数据库。
  • 数据库系统 - 数据库系统 (Data Base System,简称 DBS) 通常由软件、数据库和数据管理员 (DBA) 组成。
  • 数据库管理员 - 数据库管理员 (Database Administrator, 简称 DBA) 负责全面管理和控制数据库系统。
  • OLTP - 联机事务处理 (OLTP) 系统的主要用途是处理数据库事务。
  • OLAP - 联机分析处理 (OLAP) 系统的主要用途是分析聚合数据。
  • 元组 - 元组(Tuple)是关系数据库中的基本概念,关系是一张表,表中的每行(即数据库中的每条记录)就是一个元组,每列就是一个属性。 在二维表里,元组也称为行。
  • - 码就是能唯一标识实体的属性,对应表中的列。
  • 候选码 - 若关系中的某一属性或属性组的值能唯一的标识一个元组,而其任何、子集都不能再标识,则称该属性组为候选码。例如:在学生实体中,“学号”是能唯一的区分学生实体的,同时又假设“姓名”、“班级”的属性组合足以区分学生实体,那么{学号}和{姓名,班级}都是候选码。
  • 主码 - 主码也叫主键。主码是从候选码中选出来的。一个实体集中只能有一个主码,但可以有多个候选码。
  • 外码 - 外码也叫外键。如果一个关系中的一个属性是另外一个关系中的主码则这个属性为外码。
  • 主属性 - 候选码中出现过的属性称为主属性。比如关系 工人(工号,身份证号,姓名,性别,部门). 显然工号和身份证号都能够唯一标示这个关系,所以都是候选码。工号、身份证号这两个属性就是主属性。如果主码是一个属性组,那么属性组中的属性都是主属性。
  • 非主属性 - 不包含在任何一个候选码中的属性称为非主属性。比如在关系——学生(学号,姓名,年龄,性别,班级)中,主码是“学号”,那么其他的“姓名”、“年龄”、“性别”、“班级”就都可以称为非主属性。

数据模型

数据模型是对现实世界数据特征的抽象,也就是说数据模型是用来描述数据,组织数据和对数据进行操作的

现有的数据库都是基于某种数据模型的,数据模型是数据库系统的核心和基础

数据模型可以分为两类:第一类是概念模型,第二类是逻辑模型和物理模型。

概念模型

概念模型也称为信息模型,它是按照用户的观点来对数据和信息建模,主要用于数据库设计。

概念模型主要涉及以下一些概念:

  • 实体 - 客观存在并可相互区别的事务称为实体。
  • 属性 - 实体具有的某一特性称为属性。
  • 码 - 唯一标识实体的属性集称为码。
  • 实体模型 - 用实体名及其属性名集合来抽象和刻画同类实体,称为实体模型。
  • 实体集 - 同一类型实体的集合就是实体集。
  • 联系
    • 实体之间的联系通常指的是不同实体集之间的联系。
    • 实体之间的联系有一对一,一对多和多对多等多种类型。

概念模型的一种表示方法是:实体-联系方法,该方法使用 ER 图来描述现实世界的概念模型,ER 方法也成为 ER 模型。

逻辑模型

逻辑模型是计算机系统的观点对数据建模,主要用于数据库管理系统的实现。

逻辑模型通常由三部分组成:

  • 数据结构 - 数据结构描述数据库的组成对象以及对象之间的联系,也就是说,数据结构描述的内容有两类,一类是对象的类型、内容、性质有关的,一类是与数据之间联系有关的对象。
  • 数据操作 - 数据操作指的是对数据库中各种对象的实例允许执行的操作的集合,包括操作及有关的操作规则。主要有查询和更新两大操作。
  • 数据的完整性约束 - 数据的完整性约束条件是一组完整性规则。

物理模型是对数据最底层的抽象,他描述数据在系统内部的表示和存储方式,是面向计算机系统的。

函数依赖

定义:设 R(U)属性集合 U={ A1, A2, … , An } 上的一个关系模式,X, Y 是 U 上的两个子集,若对 R(U) 的任意一个可能的关系 r ,r 中不可能有两个元组满足在 X 中的属性值相等而在 Y 中的属性值不等,则称 “ X 函数决定 Y ” 或 “ Y 函数依赖于 X ” ,记作 X->Y 。

白话:在一个关系 R 中,属性(组) Y 的值是由属性(组) X 的值所决定的 。又可以说,在关系 R 中,若两个元组的 X 属性值相同,那么这两个元组的 Y 属性值也相同

为什么叫做函数依赖? 函数的定义:对于定义域中任意 x ,有且只有一个 y 与之对应。 属性之间的依赖:对于相同的 X 属性值,有且只有一个 Y 属性值与之对应。

本质:函数依赖的本质就是反应了 一个关系中属性之间约束关系,或者依赖关系。函数依赖是一种数据依赖。

举例

1
2
U = { 学号,姓名,年龄,专业 }
{学号}{ 姓名,年龄,专业 }

扩展阅读:白话详解数据库函数依赖和 Armstrong 公理及其引理

范式

数据库规范化,又称“范式”,是数据库设计的指导理论。范式的目标是:使数据库结构更合理,消除存储异常,使数据冗余尽量小,增进数据的一致性

根据约束程度从低到高有:第一范式(1NF)、第二范式(2NF)、第三范式(3NF)、巴斯-科德范式(BCNF)等等。

第一范式 (1NF)

1NF 要求所有属性都不可再分解

第二范式 (2NF)

2NF 要求记录有唯一标识,即实体的唯一性,即不存在部分依赖

假设有一张 student 表,结构如下:

1
2
-- 学生表
student(学号、课程号、姓名、学分、成绩)

举例来说,现有一张 student 表,具有学号、课程号、姓名、学分等字段。从中可以看出,表中包含了学生信息和课程信息。由于非主键字段必须依赖主键,这里学分依赖课程号,姓名依赖学号,所以不符合 2NF。

不符合 2NF 可能会存在的问题:

  • 数据冗余 - 每条记录都含有相同信息。
  • 删除异常 - 删除所有学生成绩,就把课程信息全删除了。
  • 插入异常 - 学生未选课,无法记录进数据库。
  • 更新异常 - 调整课程学分,所有行都调整。

根据 2NF 可以拆分如下:

1
2
3
4
5
6
-- 学生表
student(学号、姓名)
-- 课程表
course(课程号、学分)
-- 学生课程关系表
student_course(学号、课程号、成绩)

第三范式 (3NF)

如果一个关系属于第二范式,并且在两个(或多个)非主键属性之间不存在函数依赖(非主键属性之间的函数依赖也称为传递依赖),那么这个关系属于第三范式。

3NF 是对字段的冗余性,要求任何字段不能由其他字段派生出来,它要求字段没有冗余,即不存在传递依赖

假设有一张 student 表,结构如下:

1
2
-- 学生表
student(学号、姓名、年龄、班级号、班主任)

上表属于第二范式,因为主键由单个属性组成(学号)。

因为存在依赖传递:(学号) → (学生)→(所在班级) → (班主任) 。

可能会存在问题:

  • 数据冗余 - 有重复值;
  • 更新异常 - 有重复的冗余信息,修改时需要同时修改多条记录,否则会出现数据不一致的情况

可以基于 3NF 拆解:

1
2
student(学号、姓名、年龄、所在班级号)
class(班级号、班主任)

反范式

反范式,顾名思义,与范式的目标正好相反。范式的目标是消除冗余反范式的目标是冗余以提高查询效率

范式并非越严格越好,现代数据库设计,一般最多满足 3NF。范式越高意味着表的划分更细,一个数据库中需要的表也就越多,用户不得不将原本相关联的数据分摊到多个表中。当用户同时需要这些数据时只能通过关联表的形式将数据重新合并在一起。同时把多个表联接在一起的花费是巨大的,尤其是当需要连接的两张或者多张表数据非常庞大的时候,表连接操作几乎是一个噩梦,这严重地降低了系统运行性能。因此,有时为了提高查询效率,有必要适当的冗余数据,以达到空间换时间的目的——这就是“反范式”

ER 图

E-R 图又称实体关系图,是一种提供了实体,属性和联系的方法,用来描述现实世界的概念模型。通俗点讲就是,当我们理解了实际问题的需求之后,需要用一种方法来表示这种需求,概念模型就是用来描述这种需求。

ER 图中的要素:

  • 实体 - 用矩形表示。实际问题中客观存在的并且可以相互区别的事物称为实体。实体是现实世界中的对象,可以具体到人,事,物。可以是学生,教师,图书馆的书籍。
  • 属性 - 用椭圆形表示。实体所具有的某一个特性称为属性,在 E-R 图中属性用来描述实体。比如:可以用“姓名”“姓名”“出生日期”来描述人。
  • 主键 - 在属性下方标记下划线。在描述实体集的所有属性中,可以唯一标识每个实体的属性称为键。键也是属于实体的属性,作为键的属性取值必须唯一且不能“空置”。
  • 联系 - 用菱形表示。世界上任何事物都不是孤立存在的,事物内部和事物之间都有联系的,实体之间的联系通常有 3 种类型:一对一联系,一对多联系,多对多联系。

绘制 ER 图常用软件:

  • drawio 官网 - 开源的绘图工具,特点是简洁、清晰,并且同时支持线上线下绘图。
  • Visio - Office 的绘图工具,特点是简单、清晰。
  • 亿图 - 国内开发的、收费的绘图工具。图形模板、素材非常全面,样式也很精美,可以导出为 word、pdf、图片。
  • StarUML - 样式精美,功能全面的 UML 工具。
  • Astah 官网 - 样式不错,功能全面的绘图工具。
  • ArgoUML 官网
  • ProcessOn 官网 - 在线绘图工具,特点是简洁、清晰。

扩展阅读:

ER 图(实体关系图)怎么画?

参考资料

MySQL 复制

复制

复制是解决系统高可用的常见手段。其思路就是:不要把鸡蛋都放在一个篮子里。

复制解决的基本问题是让一台服务器的数据与其他服务器保持同步。一台主库的数据可以同步到多台从库上,从库本身也可以被配置成另外一台服务器的主库。主库和从库之 间可以有多种不同的组合方式。

MySQL 支持两种复制方式:基于行的复制和基于语句的复制。这两种方式都是通过在主库上记录 bin log、在从库重放日志的方式来实现异步的数据复制。这意味着:复制过程存在时延,这段时间内,主从数据可能不一致。

复制如何工作

在 MySQL 中,复制分为三个步骤,分别由三个线程完成:

  • binlog dump 线程 - 主库上有一个特殊的 binlog dump 线程,负责将主服务器上的数据更改写入 binlog 中。
  • I/O 线程 - 从库上有一个 I/O 线程,负责从主库上读取 binlog,并写入从库的中继日志(relay log)中。
  • SQL 线程 - 从库上有一个 SQL 线程,负责读取中继日志(relay log)并重放其中的 SQL 语句。

这种架构实现了数据备份和数据同步的异步解耦。但这种架构也限制了复制的过程,其中最重要的一点是在主库上并发运行的查询在从库只能串行化执行,因为只有一个 SQL 线程来重放 中继日志中的事件。

主备配置

假设需要配置一对 MySQL 主备节点,环境如下:

  • 主库节点:192.168.8.10
  • 从库节点:192.168.8.11

主库上的操作

(1)修改配置并重启

执行 vi /etc/my.cnf ,添加如下配置:

1
2
3
[mysqld]
server-id=1
log_bin=/var/lib/mysql/binlog
  • server-id - 服务器 ID 号。在主从架构中,每台机器的 ID 必须唯一。
  • log_bin - 同步的日志路径及文件名,一定注意这个目录要是 mysql 有权限写入的;

修改后,重启 mysql 使配置生效:

1
systemctl restart mysql

(2)创建用于同步的用户

进入 mysql 命令控制台:

1
2
$ mysql -u root -p
Password:

执行以下 SQL:

1
2
3
4
5
6
7
8
9
10
11
12
-- a. 创建 slave 用户
CREATE USER 'slave'@'%' IDENTIFIED WITH mysql_native_password BY '密码';
-- 为 slave 赋予 REPLICATION SLAVE 权限
GRANT REPLICATION SLAVE ON *.* TO 'slave'@'%';

-- b. 或者,创建 slave 用户,并指定该用户能在任意主机上登录
-- 如果有多个从库,又想让所有从库都使用统一的用户名、密码认证,可以考虑这种方式
CREATE USER 'slave'@'%' IDENTIFIED WITH mysql_native_password BY '密码';
GRANT REPLICATION SLAVE ON *.* TO 'slave'@'%';

-- 刷新授权表信息
FLUSH PRIVILEGES;

注意:在 MySQL 8 中,默认密码验证不再是 password。所以在创建用户时,create user 'username'@'%' identified by 'password'; 客户端是无法连接服务的。所以,需要加上 IDENTIFIED WITH mysql_native_password BY 'password'

补充用户管理 SQL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 查看所有用户
SELECT DISTINCT CONCAT('User: ''', user, '''@''', host, ''';') AS query
FROM mysql.user;

-- 查看用户权限
SHOW GRANTS FOR 'root'@'%';

-- 创建用户
-- a. 创建 slave 用户,并指定该用户只能在主机 192.168.8.11 上登录
CREATE USER 'slave'@'192.168.8.11' IDENTIFIED WITH mysql_native_password BY '密码';
-- 为 slave 赋予 REPLICATION SLAVE 权限
GRANT REPLICATION SLAVE ON *.* TO 'slave'@'192.168.8.11';

-- 删除用户
DROP USER 'slave'@'192.168.8.11';

(3)加读锁

为了主库与从库的数据保持一致,我们先为 mysql 加入读锁,使其变为只读。

1
mysql> FLUSH TABLES WITH READ LOCK;

(4)查看主库状态

1
2
3
4
5
6
7
mysql> show master status;
+------------------+----------+--------------+---------------------------------------------+-------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+------------------+----------+--------------+---------------------------------------------+-------------------+
| mysql-bin.000001 | 4202 | | mysql,information_schema,performance_schema | |
+------------------+----------+--------------+---------------------------------------------+-------------------+
1 row in set (0.00 sec)

注意:需要记录下 FilePosition,后面会用到。

(5)导出 sql

1
mysqldump -u root -p --all-databases --master-data > dbdump.sql

(6)解除读锁

1
mysql> UNLOCK TABLES;

(7)将 sql 远程传送到从库上

1
scp dbdump.sql root@192.168.8.11:/home

从库上的操作

(1)修改配置并重启

执行 vi /etc/my.cnf ,添加如下配置:

1
2
3
[mysqld]
server-id=2
log_bin=/var/lib/mysql/binlog
  • server-id - 服务器 ID 号。在主从架构中,每台机器的 ID 必须唯一。
  • log_bin - 同步的日志路径及文件名,一定注意这个目录要是 mysql 有权限写入的;

修改后,重启 mysql 使配置生效:

1
systemctl restart mysql

(2)导入 sql

1
mysql -u root -p < /home/dbdump.sql

(3)在从库上建立与主库的连接

进入 mysql 命令控制台:

1
2
$ mysql -u root -p
Password:

执行以下 SQL:

1
2
3
4
5
6
7
8
9
10
-- 停止从库服务
STOP SLAVE;

-- 注意:MASTER_USER 和
CHANGE MASTER TO
MASTER_HOST='192.168.8.10',
MASTER_USER='slave',
MASTER_PASSWORD='密码',
MASTER_LOG_FILE='binlog.000001',
MASTER_LOG_POS=4202;
  • MASTER_LOG_FILEMASTER_LOG_POS 参数要分别与 show master status 指令获得的 FilePosition 属性值对应。
  • MASTER_HOST 是主库的 HOST。
  • MASTER_USERMASTER_PASSWORD 是在主节点上注册的用户及密码。

(4)启动 slave 进程

1
mysql> start slave;

(5)查看主从同步状态

1
mysql> show slave status\G;

说明:如果以下两项参数均为 YES,说明配置正确。

  • Slave_IO_Running
  • Slave_SQL_Running

(6)将从库设为只读

1
2
3
4
5
6
7
8
9
10
11
mysql> set global read_only=1;
mysql> set global super_read_only=1;
mysql> show global variables like "%read_only%";
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| innodb_read_only | OFF |
| read_only | ON |
| super_read_only | ON |
| transaction_read_only | OFF |
+-----------------------+-------+

注:设置 slave 服务器为只读,并不影响主从同步。

读写分离

主服务器用来处理写操作以及实时性要求比较高的读操作,而从服务器用来处理读操作。

读写分离常用代理方式来实现,代理服务器接收应用层传来的读写请求,然后决定转发到哪个服务器。

MySQL 读写分离能提高性能的原因在于:

  • 主从服务器负责各自的读和写,极大程度缓解了锁的争用;
  • 从服务器可以配置 MyISAM 引擎,提升查询性能以及节约系统开销;
  • 增加冗余,提高可用性。

参考资料