记一次MySQL大表空间收缩

最近收到 MySQL 数据库磁盘空间使用率过高的报警,发现其中的一个表空间占用竟然高达三百多 G。删除了一半多的数据后,可是表文件大小还是没变,当时就懵了。
请教 DBA 才知道删除大量数据后存在数据空洞,MySQL 不会自动收缩表空间,需要手工操作。

产生原因

  1. InnoDB 里的数据都是用 B+树的结构组织的,每当删除了一条记录后,InnoDB 引擎只会把这个记录标记为删除,而在一段时间内的大量删除操作,会使这种留空的空间变得比存储列表内容所使用的空间更大。

  2. 记录的复用只限定符合范围条件的数据。比如说删除一条 ID 为 400 的数据,当执行插入操作 ID 为 400 前后的数据时,MySQL 可能会复用这个位置。但如果某个空白空间一直没有被大小合适的数据占用,仍然无法将其彻底占用,就形成了碎片。

  3. 如果是随机插入数据也会造成数据空洞。

解决方案

  1. 对数据导出后进行收缩,然后导入数据。
  2. 重建表

目前没有导出数据的条件,采用重建表的方法,可以采用alter table A engine=InnoDB 命令。
MySQL 5.6 版本开始引入了 Online DDL,允许在表上执行 DDL 的操作(比如创建索引)的同时不阻塞并发的 DML 操作 和 查询(select)操作。

  • Algorithm=Inplace :为了避免表拷贝导致的实例性能问题(空间、I/O 问题),建议在 DDL 中包含该选项。如果 DDL 操作不支持 Algorithm=Inplace 方式,DDL 操作会立刻返回错误。

  • Lock=None :为了在 DDL 操作过程中不影响业务的 DML 操作,建议在 DDL 中包含该选项。如果 DDL 操作不支持 Lock=None (允许并行 DML 操作)选项,DDL 操作会立刻返回错误。

所有的 DDL 操作均建议在业务低峰期进行,避免对业务产生影响。经过测试,删除表的数据后,对应的磁盘空间也正常释放了。

React中props和state的区别

对于 React 组件而言,数据分为两种:

  • props
  • state

React 的数据是自顶向下单向流动的,这两者有什么区别呢?

Props

React 的核心思想就是组件化思想,组件可以将 UI 切分成一些独立的、可复用的部件,这样你就只需专注于构建每一个单独的部件。

组件从概念上看就是一个函数,它可以接收任意的输入值(称之为”props”),并返回一个需要在页面上展示的 React 元素。所以可以把 props 理解为从外部传入组件内部的数据。由于 React 是单向数据流,所以 props 基本上也就是从服父级组件向子组件传递的数据。

只读性

无论是使用函数或是类来声明一个组件,它决不能修改它自己的 props。如果 props 在渲染过程中可以被改变,会导致这个组件显示的形态变得不可预测。只有通过父组件重新渲染的方式才可以把新的 props 传入组件中。所有的 React 组件必须像纯函数那样使用它们的 props。

默认参数

defaultProps 可以被定义为在组件类本身上的一个属性,为该类设置默认属性。这对于未定义(undefined)的属性来说有用,而对于设为空(null)的属性并没用。

state

state 是什么呢?

State is similar to props, but it is private and fully controlled by the component.

一个组件的显示形态可以由内部状态和外部参数所决定,props 是组件外传递进来的数据,state 代表的就是 React 组件的内部状态。
组件或子组件都不能知道某个组件是有状态还是无状态,并且它们不应该关心某组件是被定义为一个函数还是一个类,组件可以选择将其状态作为属性传递给其子组件。

setState

state 不同于 props 的一点是,state 是可以被改变的,setState 函数来修改组件 state,而且可以引发组件重新渲染。

  • 不要直接更新状态,不可以直接通过 this.state=XX的方式来修改,应当使用 setState()。
  • 状态更新可能是异步的,你不应该依靠它们的值来计算下一个状态。
  • 状态更新合并,React 可以将多个 setState() 调用合并成一个调用来提高性能。

总结

props 是外部传给组件的数据,而 state 是组件内部自己维护的数据,对外部是不可见的。

所以,判断一个数据应该放在哪里,用下面的原则:

  • 如果数据由外部传入,放在 props 中。
  • 如果是组件内部状态,是否这个状态更改应该立刻引发一次组件重新渲染?如果是,放在 state 中。不是,放在成员变量中。
  • 没有 state 的叫做无状态组件,有 state 的叫做有状态组件。
  • 多用 props,少用 state。也就是多写无状态组件

网络性能指标

从网上收集的一些评估网络性能的指标。

带宽

带宽,表示链路的最大传输速率,单位是 b/s(比特 / 秒)。在你为服务器选购网卡时,带宽就是最核心的参考指标。常用的带宽有 1000M、10G、40G、100G 等。

吞吐量

吞吐量,表示没有丢包时的最大数据传输速率,单位通常为 b/s (比特 / 秒)或者 B/s(字节 / 秒)。吞吐量受带宽的限制,吞吐量 / 带宽也就是该网络链路的使用率。

延时

延时,表示从网络请求发出后,一直到收到远端响应,所需要的时间延迟。这个指标在不同场景中可能会有不同的含义。它可以表示建立连接需要的时间(比如 TCP 握手延时),或者一个数据包往返所需时间(比如 RTT)。

PPS

PPS,是 Packet Per Second(包 / 秒)的缩写,表示以网络包为单位的传输速率。PPS 通常用来评估网络的转发能力,而基于 Linux 服务器的转发,很容易受到网络包大小的影响(交换机通常不会受到太大影响,即交换机可以线性转发)。

网络可用性

网络可供用户使用的时间百分比。即网络稳定不出故障的时间 / 用户总的使用时间

并发连接数

是客户端向服务器发起请求,并建立了 TCP 连接,每秒钟服务器链接的总 TCP 数量

丢包率

测试中所丢失数据包数量占所发送数据组的比率。

重传率

重新发送信息的与全部的调用信息之间的比值。

响应时间(RT)

响应时间是指系统对请求作出响应的时间。

吞吐量(Throughput)

吞吐量是指系统在单位时间内处理请求的数量。

JavaScript数组随机取一部分不重复元素

从一个 JavaScript 数组当中,随机抽取部分元素,构成新数组,要求这些元素不能重复,即随机获取不重复的数组元素。
这个问题很简单,相信很多人都会在几分钟内给出以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function randomMembers1(arr, limit) {
const result = [];

for (let i = 0; i < limit; i++) {
result[i] = arr[Math.floor(Math.random() * arr.length)];
for (let j = 0; j < i; j++) {
if (result[j] === result[i]) {
i--;
break;
}
}
}
return result;
}

randomMembers1([11, 12, 13, 14, 15, 16, 17, 18], 5); //[ 18, 16, 12, 17, 14 ]

解决思路就是从第二次随机抽取的元素开始,将抽取的元素与已抽取元素相比较,如果相同,则重新抽取,并再次执行比较的操作。
但是这种写法存在循环语句和条件语句多层嵌套,复杂度较,执行效率很。随着元素的抽取越多,要比较的次数越来越多,“失败的抽取”概率越来越大。

我们可以优化一下比较的逻辑,比如以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function randomMembers2(arr, limit) {
const hash = {};
const result = [];

while (limit > 0) {
const index = Math.floor(Math.random() * arr.length);
if (!hash[index]) {
hash[index] = true;
result.push(arr[index]);
limit--;
}
}

return result;
}

randomMembers2([11, 12, 13, 14, 15, 16, 17, 18], 5); //[ 16, 17, 13, 11, 18 ]

和第一种方法相比,节省了第一种方法中依次比较的步骤,但依旧存在“失败抽取”的现象,而且失败抽取的概率没有发生任何变化。

是否可以把抽取到的元素从数组中删除,从而避免重复抽取呢?可以利用 splice 方法,将抽取到的元素从数组当中删除掉,并把返回值存储(push)到结果数组当中。

1
2
3
4
5
6
7
8
9
10
11
12
13
function randomMembers3(arr, limit) {
const result = [];
let num = arr.length > limit ? limit : arr.length;

while (num > 0) {
const index = Math.floor(Math.random() * arr.length);
result.push(arr.splice(index, 1)[0]);

num--;
}

return result;
}

问题就是这种方法会修改源数组,产生副作用,抽取的元素会从数组中删除。我们可以新建一个数组保存原来数组的下标,从下标数组中进行抽取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function randomMembers4(arr, limit) {
const result = [];
const keyList = [...arr.keys()];
let num = arr.length > limit ? limit : arr.length;

while (num > 0) {
const index = Math.floor(Math.random() * keyList.length);
const key = keyList.splice(index, 1)[0];

result.push(arr[key]);

num--;
}

return result;
}

完整代码

安装指定版本node

mac 环境下,使用 homebrew 安装的 node, 默认是最高版本,如何安装指定版本的 node 呢?

  1. 如果之前使用 brew install node 安装过 node,需要先执行brew unlink node解绑node。

  2. 查找可用的 node 版本 brew search node

  3. 安装你需要的版本, 比如 brew install node@10

  4. 然后 brew link node@10, 这一步可能会报错, 按照提示执行命令就 ok 了, 比如我最后执行的是 brew link --overwrite --force node@10

node -v 不出意外, 就安装好了你想要的 node 版本。

参考文档:

https://www.jianshu.com/p/c5c298486dbd

JavaScript实现四则混合运算

背景

最近在项目中需要自己解析四则混合运算,如果只是简单的加减乘除运算,我相信对大家来说没有任何困难,但是实现带括号的四则运算,还是有一定的难度的。

  • 操作数:小数、整数
  • 运算符:加、减、乘、除
  • 分界符:圆括号 ( ) , 用于指示运算的先后顺序

这里使用逆波兰表达式解决数值运算以及括号带来的优先级提升问题。

逆波兰表达式

  • 中缀表达式(Infix Notation)
    是一个通用的算术或逻辑公式表示方法, 操作符是以中缀形式处于操作数的中间。比如1 + 2 + 3

  • 前缀表达式(Prefix Notation)
    是指将运算符写在前面、操作数写在后面、不包含括号的表达式,而且为了纪念其发明者波兰数学家 Jan Lukasiewicz 所以前缀表达式也叫做波兰表达式。比如- 1 + 2 3

  • 后缀表达式(Postfix Notation)
    与之相反,是指运算符写在操作数后面的不含括号的算术表达式,也叫做逆波兰表达式。比如1 2 3 + -

前后缀表达式的出现是为了方便计算机处理,它的运算符是按照一定的顺序出现,所以求值过程中并不需要使用括号来指定运算顺序,也不需要考虑运算符号(比如加减乘除)的优先级。逆波兰表达式在编译技术中有着普遍的应用。

中缀表达式转换成后缀表达式算法:

  1. 从左至右扫描一中缀表达式。
  2. 若读取的是操作数,则判断该操作数的类型,并将该操作数存入操作数堆栈
  3. 若读取的是运算符:
    1. 该运算符为左括号”(“,则直接存入运算符堆栈。
    2. 该运算符为右括号”)”,则输出运算符堆栈中的运算符到操作数堆栈,直到遇到左括号为止。
    3. 该运算符为非括号运算符:
      1. 若运算符堆栈栈顶的运算符为括号,则直接存入运算符堆栈。
      2. 若比运算符堆栈栈顶的运算符优先级高或相等,则直接存入运算符堆栈。
      3. 若比运算符堆栈栈顶的运算符优先级低或者优先级相等,则输出栈顶运算符到操作数堆栈,直到比运算符堆栈栈顶的运算符优先级低或者为空时才将当前运算符压入运算符堆栈。
    4. 当表达式读取完成后运算符堆栈中尚有运算符时,则依序取出运算符到操作数堆栈,直到运算符堆栈为空。

流程图如下所示:

逆波兰表达式求值算法:

  1. 循环扫描语法单元的项目。
  2. 如果扫描的项目是操作数,则将其压入操作数堆栈,并扫描下一个项目。
  3. 如果扫描的项目是一个二元运算符,则对栈的顶上两个操作数执行该运算。
  4. 如果扫描的项目是一个一元运算符,则对栈的最顶上操作数执行该运算。
  5. 将运算结果重新压入堆栈。
  6. 重复步骤 2-5,堆栈中即为结果值。

算法实现

  • 中缀表达式转换成逆波兰表达式
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
const operatorRand = {
'+': 1,
'-': 1,
'*': 2,
'/': 2,
};

/**
* 中缀表达式转换成逆波兰表达式
* @param {string[]} str 中缀表达式
*/
function convert(inputArr) {
if (!Array.isArray(inputArr) || inputArr.length === 0) return [];

const operatorArr = [];
const outputArr = [];

inputArr.forEach(input => {
if (!Number.isNaN(Number(input))) {
// 如果是数字,只接输出
outputArr.push(input);
} else if (input === '(') {
// 如果是左括号,入操作符栈
operatorArr.push(input);
} else if (input === ')') {
// 如果是右括号,循环输出,知道匹配到左括号为止
while (operatorArr.length > 0) {
const operator = operatorArr.pop();
if (operator === '(') break;
outputArr.push(operator);
}
} else {
// 如果是运算符
while (operatorArr.length >= 0) {
const topOperator = operatorArr[operatorArr.length - 1];

// 如果运算符栈为空,或者栈顶运算符是(,或者当前运算符优先级比栈顶运算符优先级高
if (
operatorArr.length === 0 ||
topOperator === '(' ||
operatorRand[input] > operatorRand[topOperator]
) {
operatorArr.push(input);
break;
} else {
outputArr.push(operatorArr.pop());
}
}
}
});

// 输入循环结束,如果运算符栈不为空,循环输出
while (operatorArr.length > 0) {
outputArr.push(operatorArr.pop());
}

return outputArr;
}
  • 逆波兰算法求值
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function compute(leftNum, rightNum, operator) {
switch (operator) {
case '+':
return leftNum + rightNum;
case '-':
return leftNum - rightNum;
case '*':
return leftNum * rightNum;
default:
// 除法
return leftNum / rightNum;
}
}

/**
* 计算逆波兰表达式
* @param {string} str 逆波兰表达式
*/
export function count(reversePolishArr) {
if (!Array.isArray(reversePolishArr) || reversePolishArr.length === 0) return 0;

const tmpArr = [];

reversePolishArr.forEach(input => {
if (!Number.isNaN(Number(input))) {
// 数字接直接push
tmpArr.push(Number(input));
} else {
// 运算符
const num1 = tmpArr.pop();
const num2 = tmpArr.pop();

if (isNaN(num1) || isNaN(num2)) {
throw new Error(`无效的表达式:${reversePolishArr.join(',')}`);
}

tmpArr.push(compute(num2, num1, input));
}
});

return Number(tmpArr[0].toFixed(3));
}

完整代码请参考:https://github.com/zubincheung/js-rpn

日志框架winston的使用

日志对于问题定位、调试,系统性能调优至关重要,尤其是系统复杂以及在线运行的情况下。之前的项目日志输出一直用 log4js,输出到一个文件,最近对那一块进行重构。

分别考虑了两款 Node.js 框架,分别是BunyanWinston

  • Winston 是 Node.js 最流行的日志框架之一,设计为一个简单通用的日志库,支持多传输
  • Bunyan 以略微不同的方式处理结构化,机器可读性被重点对待。实际上就是 JSON.stringify 的一个输出。

预期效果

  • 日志分级
  • 根据不同的代码分层来产生不同的 log 输出到不同的文件。
  • 输出到 log 文件同时还可以选择输出到标准输出。
  • 自定义格式化日志输出,输出到标准输出的格式便于读取,输出到 log 文件的格式便于分析。
  • 按天自动切割日志文件。

为何选择 winston

  • github start 数多。
  • 更加灵活,可以灵活的组织 transport,来完成比较复杂的日志输出任务。
  • 日志格式为 json 字符串,方便后期分析,当然可以自定义 format.
  • 支持简单的 log 分析,Profiling。
  • 支持 stream Api。
  • 简单 Log Query Api,当然无法和专业的日志分析工具比。

使用方法

定义标准输出 Transport

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ConsoleTransport extends winston.transports.Console {
constructor(options) {
super(options);

this.format = winston.format.combine(
winston.format(info => {
info.hostname = hostname();
info.pid = process.pid;
info.level = info.level.toUpperCase();
return info;
})(),
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss,SSSS' }),
winston.format.ms(),
winston.format.colorize(),
winston.format.printf(options.formatter),
);
}
}

定义文件输出

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
27
28
29
30
31
32
33
34
35
36
37
const winston = require('winston');
require('winston-daily-rotate-file');

class FileTransport extends winston.transports.DailyRotateFile {
constructor(options) {
super(options);

this.datePattern = 'YYYY-MM-DD';
this.zippedArchive = true;
this.maxSize = '100m';
this.maxFiles = '14d';

const defaultFormatter = winston.format.combine(
winston.format(info => {
info.hostname = hostname();
info.pid = process.pid;
info.level = info.level.toUpperCase();
return info;
})(),
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss,SSSS' }),
winston.format.ms(),
);

if (options.json) {
// 输出json格式
this.format = winston.format.combine(
defaultFormatter,
winston.format.json(options.formatter),
);
} else {
this.format = winston.format.combine(
defaultFormatter,
winston.format.printf(options.formatter),
);
}
}
}

调用方法

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const options = {
name: 'app',
module: 'app',
filePath: './logs',
formatter: meta => {
return `${meta.timestamp} ${meta.level} ${meta.hostname} ${meta.pid} (${meta.ms}) [${
meta.module
}] ${meta.message}`;
},
consoleLevel: 'error',
json: false,
};

const consoleTransport = new ConsoleTransport({
level: options.consoleLevel,
name: options.name,
formatter: options.consoleFormatter,
});

const infoTransport = new FileTransport({
level: 'info',
name: options.name,
filename: join(options.filePath, `info/${options.name}-info-%DATE%.log`),
formatter: options.formatter,
json: options.json,
});

const errorTransport = new FileTransport({
level: 'error',
name: options.name,
filename: join(options.filePath, 'error/error-%DATE%.log'),
formatter: options.formatter,
json: options.json,
});

const transports = [consoleTransport, infoTransport, errorTransport];
const logger = winston.createLogger({ transports });

logger.info('log1');
logger.error('error 1');

代码地址为:https://github.com/zubincheung/cw-logger-winston

输出效果

1
2
3
4
5
6
7
8
9
10
11
2018-12-30 10:07:28,4605 INFO zubin-pc.local 59468 (+0ms) [app] app log 1
2018-12-30 10:07:28,4695 ERROR zubin-pc.local 59468 (+1ms) [app] Error: app error 1
Error: app error 1
at Object.it (/Users/zubincheung/ciwong/cw-logger-winston/test/cw-logger.test.js:61:35)
at Object.asyncJestTest (/Users/zubincheung/ciwong/cw-logger-winston/node_modules/jest-jasmine2/build/jasmine_async.js:108:37)
at resolve (/Users/zubincheung/ciwong/cw-logger-winston/node_modules/jest-jasmine2/build/queue_runner.js:56:12)
at new Promise (<anonymous>)
at mapper (/Users/zubincheung/ciwong/cw-logger-winston/node_modules/jest-jasmine2/build/queue_runner.js:43:19)
at promise.then (/Users/zubincheung/ciwong/cw-logger-winston/node_modules/jest-jasmine2/build/queue_runner.js:87:41)
at process.internalTickCallback (internal/process/next_tick.js:77:7)

CPU使用率

什么是 CPU 使用率

CPU 使用率就是除了空闲时间外的其他时间占总 CPU 时间的百分比

事实上,为了计算 CPU 使用率,性能工具一般都会取间隔一段时间(比如 3 秒)的两次值,作差后,再计算出这段时间内的平均 CPU 使用率,即

需要注意的是,**>性能分析工具给出的都是间隔一段时间的平均 CPU 使用率,所以要注意间隔时间的设置**,特别是用多个工具对比分析时,你一定要保证它们用的是相同的间隔时间。

怎么查看 CPU 使用率

  • top 显示了系统总体的 CPU 和内存使用情况,以及各个进程的资源使用情况。
    • 系统的 CPU 使用率(%Cpu)
  • pidstat 专门分析每个进程 CPU 使用情况的工具
    • 用户态 CPU 使用率 (%usr);
    • 内核态 CPU 使用率(%system);
    • 运行虚拟机 CPU 使用率(%guest);
    • 等待 CPU 使用率(%wait);
    • 以及总的 CPU 使用率(%CPU)。
  • perf 分析 CPU 性能问题
  • pstree 用树状形式显示所有进程之间的关系,可以用来查找一个进程的父进程
  • execsnoop 专为短时进程设计的工具

CPU 使用率过高怎么办?

  • CPU 使用率是最直观和最常用的系统性能指标,更是我们在排查性能问题时,通常会关注的第一个指标。所以我们更要熟悉它的含义,尤其要弄清楚用户(%user)、Nice(%nice)、系统(%system) 、等待 I/O(%iowait) 、中断(%irq)以及软中断(%softirq)这几种不同 CPU 的使用率。比如说:

    • 用户 CPU 和 Nice CPU 高,说明用户态进程占用了较多的 CPU,所以应该着重排查进程的性能问题。
    • 系统 CPU 高,说明内核态占用了较多的 CPU,所以应该着重排查内核线程或者系统调用的性能问题。
    • I/O 等待 CPU 高,说明等待 I/O 的时间比较长,所以应该着重排查系统存储是不是出现了 I/O 问题。
    • 软中断和硬中断高,说明软中断或硬中断的处理程序占用了较多的 CPU,所以应该着重排查内核中的中断服务程序。
  • 碰到 CPU 使用率升高的问题,你可以借助 top、pidstat 等工具,确认引发 CPU 性能问题的来源;再使用 perf 等工具,排查出引起性能问题的具体函数。

  • 碰到常规问题无法解释的 CPU 使用率情况时,首先要想到有可能是短时应用导致的问题,比如有可能是下面这两种情况。

    • 应用里直接调用了其他二进制程序,这些程序通常运行时间比较短,通过 top 等工具也不容易发现
    • 应用本身在不停地崩溃重启,而启动过程的资源初始化,很可能会占用相当多的 CPU

CPU上下文切换

什么是 CPU 上下文切换

CPU 寄存器,是 CPU 内置的容量小、但速度极快的内存。程序计数器,则是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。它们都是 CPU 在运行任何任务前,必须的依赖环境,因此也被叫做CPU 上下文

CPU 上下文切换,就是先把前一个任务的 CPU 上下文(也就是 CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。

进程上下文切换

Linux 按照特权等级,把进程的运行空间分为内核空间用户空间

  • 内核空间(Ring 0)具有最高权限,可以直接访问所有资源。
  • 用户空间(Ring 3)只能访问受限资源,不能直接访问内存等硬件设备,必须通过系统调用陷入到内核中,才能访问这些特权资源。
  • 系统调用(特权模式切换):一个进程用户态与内核态的互相转变
  • 上下文切换:从一个进程切换到另一个进程运行
    • 虚拟内存、栈、全局变量等用户空间的资源
    • 内核堆栈、寄存器等内核空间的状态

一次系统调用的过程,发生了次 CPU 上下文切换。

什么时候会发生?

  • 进程 CPU 时间片耗尽,被系统挂起,切换到其他正在等待 CPU 的进程
  • 系统资源不足时进程被系统挂起,系统调度其他进程运行
  • 进程通过睡眠函数 sleep 这样的方法将自己主动挂起
  • 有优先级更高的进程运行,当前程序会被挂起
  • 发生硬件中断,转而执行内核中的终端服务程序

线程上下文切换

线程与进程的区别

  • 线程是调度的基本单位,而进程是资源拥有的基本单位
  • 当进程只有一个线程时,可以认为进程就等于线程
  • 当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源,在上下文切换时,这些资源不需要修改
  • 线程有自己的私有数据,例如栈和寄存器等,在上下文切换时需要保存

什么时候会发生

  • 前后两个线程属于不同进程。此时因为资源不共享,因此等同于进程上下文切换
  • 前后两个线程属于同一个进程,因为虚拟内存共享,所以只需要切换私有数据、寄存器等不共享的数据

虽然同为上下文切换,但同进程内的线程切换,要比多进程间的切换消耗更少的资源,而这,也正是多线程代替多进程的一个优势。

中断上下文切换

  • 中断处理会打断进程的正常调度和执行
  • 对同一个 CPU 来说,中断处理比进程拥有更高的优先级

怎么查看系统的上下文切换情况

vmstat

vmstat 是一个常用的系统性能分析工具,主要用来分析系统的内存使用情况,也常用来分析 CPU 上下文切换和中断的次数。

  • 需要特别关注的四列内容:
    • cs(context switch) 表示每秒上下文切换的次数
    • in(interrupt)表示每秒中断次数
    • r(Running or Runnable)表示就绪队列的长度,也就是正在运行和等待 CPU 的进程数
    • b(Blocked)表示处于不可中断睡眠状态的进程数 #每隔 5 秒输出一组数据

pidstat

vmstat 只给出了系统总体的上下文切换情况,要想查看每个进程的详细情况,就需要使用 pidstat 了。给它加上 -w 选项,你就可以查看每个进程上下文切换的情况了。

  • 需要特别关注的两列内容
    • cswch 表示每秒自愿上下文切换的次数
    • nvcswch 表示每秒非自愿上下文切换的次数
  • 自愿上下文切换:进程无法获取所需资源
  • 非自愿上下文切换:进程由于时间片已到等原因,被系统强制调度
  • 自愿上下文切换变多了,说明进程都在等待资源,有可能发生了 IO 等其他问题
  • 非自愿上下文切换变多了,说明进程都在被强制调度,即在争抢 CPU,说明 CPU 成为瓶颈
  • 中断次数变多了,说明 CPU 被中断处理程序占用,还需要通过查看/proc/interrupts 文件来分析具体的中断类型

小结:

不管是哪种场景导致的上下文切换,我们应该知道:

  • CPU 上下文切换,是保证 Linux 系统正常工作的核心功能之一,一般情况下不需要我们特别关注。
  • 但过多的上下文切换,会把 CPU 时间消耗在寄存器、内核栈以及虚拟内存等数据的保存和恢复上,从而缩短进程真正运行的时间,导致系统的整体性能大幅下降。

碰到上下文切换次数过多的问题时,我们可以借助 vmstat 、 pidstat 和 /proc/interrupts 等工具,来辅助排查性能问题的根源。

时区问题小结

修改基于 Alpine 的 Docker 容器的时区

在容器中修改

进入容器

1
# docker exec -it container_name /bin/sh

安装 timezone,列出安装的时区文件,验证是否下载成功。

1
2
# apk add -U tzdata
# ls /usr/share/zoneinfo

拷贝需要的时区文件到 localtime,国内需要的是 Asia/Shanghai:

1
# cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

验证时区

1
2
date
Wed Dec 12 19:09:09 CST 2018

CST 即为 中国标准时间。

移除时区文件:

1
# apk del tzdata

在 Dockerfile 指定时区

1
2
3
4
5
6
7
# Install base packages, set timezone
RUN apk update && apk add curl bash tree tzdata

# cp -r -f /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

ENV TZ Asia/Shanghai

Node.Js 中 sequelize 时区的配置方法

sequelize 默认情况下,保存日期时会转换成 +00:00 时区

解决方式:

sequelize 时配置时区
timezone: ‘+08:00’

1
2
3
4
5
6
7
8
9
10
11
const sequelize = new Sequelize(config.database, config.username, config.password, {
host: config.host,
port: config.port,
dialect: 'mysql',
pool: {
max: 5,
min: 0,
idle: 10000,
},
timezone: '+08:00',
});

参考文档

Setting the timezone
Sequelize.html#instance-constructor-constructor