0%

执行流程

图片

连接器

  1. 建立连接,完成 tcp 握手
  2. 完成权限验证,验证之后以后的所有的会话都基于建立会话时的权限,如果此时管理员修改了权限,已经建立的连接的权限不会修改
  3. 关闭连接,如果客户端太长时间没有交互,连接器就会关闭连接,默认为 8 小时。(由 wait_timeout 参数控制)
  4. MySQL 临时使用的内存是管理在连接对象里边的,在关闭连接时自动释放。如果长时间使用一个连接不进行释放可能会引起内存持续增加。

查询缓存

查询缓存会将查询结果缓存下来,如果有相同的查询语句会直接返回已经缓存的结果。一旦表有更新就会删除查询缓存。

建议不要用,MySQL 8 开始已经完全移除。

分析器

分析器会进行词法分析、语法分析,判断是否符合 MySQL 语法。如果发生错误,会返回“You have an error in your SQL sytanx”的错误提醒。

优化器

  1. 决定是否使用索引。
  2. 决定关联表时的执行顺序。

执行器

调用具体的存储引擎获取执行结果。

年少的时候的那股昂扬向上的生命力,都随着高考的结束而埋葬。随之而来的,便是无止境的自由与恣意。与之相伴的,便是慵懒与散漫。我曾经积蓄了全部力量来获取自由,终于目的达到了,而生命也开始褪色。

我曾经在自由的日子里走遍开封城的大街小巷,万岁山的吊桥、御河的流觞曲水、相国寺的晚钟、翰园的碑林字帖、曹门的市井小摊、梁园的吹台三友。站在繁塔的顶层,看着塔下倾颓的开封城,我享受着这自由,欣赏着和我一样没落了的开封城。却从来没想到过诗意中隐隐的侵蚀,让我更加没落。

我常常思考,人生就算卑微如尘埃,如果身处其中的人能够乐在其中,也未免不是一件幸事。然而,尘世中的我们常常身不由己,多数的潇洒都要有足够的资本。那时自由的我如同风儿一样无拘无束,也如风儿一样了无痕迹。如此这般,大概之后也只能风餐露宿了。

道家圣地延庆观的一缕香,唤醒了恍如隔世的我。我开始思考以后的人生轨迹。大概自由的日子要结束了,我要告别诗意,拥抱生活了。

之后的日子虽然不是顺风顺水,却也称不得波澜壮阔。日子对我始终是宽容的,虽然我荒废了一段大好时光,仍然能够勉强学到一点糊口的本领。我在计算机大楼的实验室中一点点地学习编程,闲暇时欣赏雪垠湖碧绿的湖光、婀娜的垂柳。原来即使牺牲掉了自由,也可以悠然自得的生活,还能为明天的生活埋下希望。感谢我生命中的旅人,给了我莫大的帮助,失去的时光随着日子的继续流逝一点点追了回来。

时光荏苒,黄山的旅途为大学生活划上了句号。

上海,这座魔幻的城市倒也对我还算不错。很容易找到一份还可以的工作,同那些满怀激情的小伙伴们一起启程,将知识化为代码,将代码化为软件。一天天的工作正是我当下的价值。我曾经的那些诗词歌赋能让我悠然自得抑或心潮澎湃,却不能让我在这城市的屋檐下有容身之所。枕上书越来越多,却都是技术书籍。诸如数据库、算法之类的书让我工作中能够得心应手,至于《诗经》、《乐府诗》之类的,却是已然蒙灰了。

于是在这城市的夜里,我时常感到孤独。父亲曾告诉我,不要像蚂蚁一样生活。而如今的我,不正是如此。茫茫人海中,我们本难寻找生命的意义;而如今,我竟连思考生命意义的时间都没有了,只是一日日的工作,一日日的变老。

于是生命又要再次改变,诗意又要稍许回头。这喧嚣的尘世中我们必须立足,却似乎也应该有时间去思考生命的真谛,去品味人生的酸甜苦辣。尽力做好工作是自己的价值,诗意地生活是自己的追求。我曾经告别诗意,如今又想重新拥抱诗意;曾经努力去谋生,如今又厌恶这样生活中只有工作。

于是,我选择与自己和解。

花也一半,柴也一半。
酒也一半,盐也一半。
诗也一半,杂也一半。

这便是以后的箴言了。

问题

线上已经运行很久的一个 Redis (阿里云64节点集群版)经常会报 CPU 偏高,经确认后发现只有 13 节点 CPU 偏高,且是其他节点的 3 倍以上,即大部分节点的 CPU 占用率在 20% 到 30%,QPS 大约是 7000,问题节点的 CPU 占用率超过 80%,QPS 是 27000。

排查问题

初次定位

我们首先怀疑是大 key 问题引起的,于是使用 monitor 命令在业务低峰时进行了 monitor,发现某个 类型为 hash 的 key 拆分了100片,每个 hash 的长度大约是 30 万。我们观察了这 100 个 key,在每个节点分布的比较均匀,不应该引起如此严重的偏移问题。

所以结论是该节点的 CPU 偏高不是由于大 key 问题引起的。

再次排查

通过阿里云的 imonitor 命令,我对指定的节点进行了监听,并将结果保存为文本文件进行分析。

用 telnet 命令连接到阿里云的 Redis Server Cluster,在 telnet 中执行

1
imonitor 13

将结果保存为 redis-13.txt

对结果进行分析

1
cat redis-13.txt | grep -o -E "1561806\d+" | sort | uniq -c

结果为

image.png

可以看出该节点的 QPS 大概为 7000 多,远不及监控上的 27000。

因此,我们确定是阿里云的监控上对应的节点和 imonitor 的节点不对应。

发现这个问题后,我们尝试用 iinfo 命令找出来 CPU 比较高的节点。

1
2
3
4
5
#遍历每个节点
for i in {0..63}
do
redis-cli -h $host -a iinfo $i CPU
done

通过该方法找到 5 节点的 CPU 显著高于其他节点。

再次使用 imonitor 命令获取该节点的 QPS:

1
cat redis-5.txt | grep -o -E "1561807\d+" | sort | uniq -c

image.png

QPS 为 28000 左右,符合监控曲线。

然后我们通过使用 redis-faina 分析 Monitor 的结果。
首先要对结果进行处理,将 imonitor 接收到的每行开头的 “+” 号去掉,执行命令

1
sed -i "" "s/+//g" redis-5.txt 

然后使用 redis-faina 分析结果,执行命令

1
python redis-faina.py redis-5.txt 

得出来访问量较高的命令,发现有一个 hget 命令的 key 和 field 明显写反了,且 QPS 很高,正是这个 key 导致了 CPU 和 miss 曲线的升高。

我们在代码库中查找出问题的 key,逐行排查,发现在一个项目中确实存在 key field 写反的情况,于是通知了开发者进行修改。

第二天开发者修改了代码并进行了发布之后,曾经偏高的 CPU 节点稳定如他,问题终于算得到了解决。

结论

  1. imonitor 命令和阿里云后台的监控的节点不一致,导致问题迟迟未被发现(阿里云反馈仅在早期的 Redis 集群中有该现象,新版的 Redis 集群都是对应的)。

  2. 使用 imonitor 和 redis-faina 一起,可以方便地排查 redis 大 key 和热 key。

  3. 具体问题要具体分析,不能解决问题的时候要变通思路,通过各种方法去尝试解决问题。

昨天凌晨,因为占用容量峰值为 218 G,DBA 将 Redis 的容量由 500G 调低到了 320G。在调低容量后,Redis 开始频繁报 HighMemoryUsage 的警告。而查看 Redis 集群的监控后发现,总容量用掉的比例不足 60%,不应该频繁报警,因此开始了排查之路。

排查原因

首先,我们已经排除了是总容量大小的问题。

由于我们的 Redis 是阿里云的集群版,有多个节点,因此我查看了各个节点的大小。果然,最大的节点已经超过 90%,而小的节点的占用则在 30% 左右。这说明 Redis 发生了非常严重的倾斜。

我们现有的程序已经充分考虑到了 Redis 集群的特性,key 设计策略没有问题,不应该出现如此大的倾斜,所以问题在其他方面。

我们用阿里云的 python 脚本来查询 Redis 大 key,果真发现有一些很老的废弃的大 key,key 为 hash 类型,每个的长度在 500 万以上,最长的接近 1000 万。因此,倾斜的原因是这些大 key 的存在,且这些大 key 在不同节点分布不均匀。

找出大 key

由于阿里云的 python 脚本非常慢,因此我写了一个 golang 的脚本来查找大 key。

因为阿里云集群版 Redis 做了一些改造,因此需要使用 iscan 命令(阿里云Redis命令列表)。

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
59
60
61
62
63
for i := 0; i < 32; i++ {
fmt.Printf("node %d start\n", i)
var cursor int
for {
values, err := redis.Values(r.Do("ISCAN", i, cursor, "COUNT", 500, "MATCH", "rc:2019*"))
if err != nil {
fmt.Println(err)
return
}

cursor, err = redis.Int(values[0], nil)
//fmt.Println(cursor)
if err != nil {
fmt.Println(err)
return
}

items, err := redis.Strings(values[1], nil)
if err != nil {
fmt.Println(err)
return
}

for i := range items {
t, err := redis.String(r.Do("TYPE", items[i]))
if err != nil {
fmt.Println(t)
}

var l int

switch t {
case "string":
l, _ = redis.Int(r.Do("STRLEN", items[i]))
case "hash":
l, _ = redis.Int(r.Do("HLEN", items[i]))
case "set":
l, _ = redis.Int(r.Do("SCARD", items[i]))
case "zset":
l, _ = redis.Int(r.Do("ZCARD", items[i]))
case "list":
l, _ = redis.Int(r.Do("LLEN", items[i]))
}
fmt.Printf("key: %s, length: %d\n", items[i], l)
if l > bigKeyLen {
fmt.Printf("find big key: %s, len: %d\n", items[i], l)
} else {
err := r.Send("DEL", items[i])
if err != nil {
fmt.Println(err)
}
}
}
err = r.Flush()
if err != nil {
fmt.Println(err)
}
if cursor == 0 {
fmt.Printf("node %d end\n", i)
break
}
}
}

在这里,如果发现了应该删除的小 key ,我们直接删掉,并将大 key 找了出来。

删除大 key

因为 Redis 的特性,删除大 key 可能会导致线程阻塞,其他请求无法接收造成应用闪崩。因此我需要将大 key 的元素逐步清空,再移除这个 key(在 Redis 4.0 以上版本中,可以直接使用 unlink 命令另外启动一个线程来删除大 key)。

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
for {
values, err := redis.Values(r.Do("HSCAN", bigKey, cursor, "COUNT", 100))
if err != nil {
fmt.Println(err)
return
}

cursor, err = redis.Int64(values[0], nil)
if err != nil {
fmt.Println(err)
return
}

items, err = redis.Int64Map(values[1], nil)
if err != nil {
fmt.Println(err)
return
}

for key := range items {
err := r.Send("HDEL", bigKey, key)
if err != nil {
fmt.Println(err)
}
}

err = r.Flush()
if err != nil {
fmt.Println(err)
}

if cursor == 0 {
break
}
}
_, err = redis.Do("DEL", bigKey)

if err != nil {
fmt.Println(err)
}

这样便完成了大 key 的删除。

总结

在确定 Redis 分布不均匀后,我们要首先检查是否是有大 key,如果有的话要逐步清除,不能影响业务。之后需要选择合适的方案设计 Redis key,使其均匀的分布在各个节点。

我们经常需要在 linux 和 unix 上使用 du 命令来查看文件和文件夹的大小,在这里,我列举一下常用的一些用法。

基本命令参数

-h 以人类可读的方式显示(比如 K, M 和 G)

-g 以 Gb 为单位显示

-m 以 Mb 为单位显示

-k 以 Kb 为单位显示

-d depth 显示指定层级数

-s 只显示一条(相当于 -d 0)

常用的命令

查看文件夹的总大小

1
du -sh

image.png

查看当前目录下所有文件(夹)的大小

1
du -sh *

image.png

或者

1
du -hd 1

image.png

这两个命令有些小小的区别,大家可以自己体会。

显示当前目录文件的大小并排序

1
2
3
du -sk * | sort -rn  #以 Kb 为单位
du -sm * | sort -rn #以 Mb 为单位
du -sg * | sort -rn #以 Gb 为单位

image.png

显示多层目录的文件(夹)的大小

1
du -hd 3 //数字表示层级

image.png

线上一组 kafka 消费者在运行了很多天之后突然积压,日志显示该 kafka 消费者频繁 rebalance 并且大概率返回失败。

错误消息如下

1
2
3
kafka server: The provided member is not known in the current generation

Request was for a topic or partition that does not exist on this broker

有时候日志里还会伴随着

1
i/o timeout

我们添加了 errors 和 notifications 日志,发现每次错误都伴随着 rebalance。

我们首先认为是超时时间过短导致的,于是我们调大了连接超时和读写超时,但是问题没有得到解决。

我们又认为是我们处理信息的时间过长,导致 kafka server 认为 client 死掉了,然后进行 rebalance 导致的。于是我们将每条获取到的 message 放到 channel 中,由多个消费者去消费 channel 来解决问题,但是问题仍然没有解决。我们阅读了 sarama 的 heartbeat 机制,发现每个 consumer 都有单独的 goroutine 每 3 秒发送一次心跳。因此这个处理时间应该只会导致消费速度下降,不会导致 rebalance。

我们于是只好另外启动了一个消费者,指定了另外一个 group id,在消费过程中,我们看到并未发生 rebalance。这时我们更加一头雾水了。

直到我们看到了这篇文章 kafka consumer 频繁 reblance,这篇文章提到:

kafka 不同 topic 的 consumer 如果用的 group id 名字一样的情况下,其中任意一个 topic 的 consumer 重新上下线都会造成剩余所有的 consumer 产生 reblance 行为。

而我们正是不同的 topic 下有名字相同的 group id 的多个消费者。为了验证确实是由这个问题导致的,我们暂停了该 group id 下其他消费者的消费,之前频繁 rebalance 的消费者果真再也没有发生过 rebalance。

于是我们更改了这些消费者的 group id,以不同后缀进行区分,问题便解决了。

结论

由于 zookeeper 的特性,相同 group id 的不同 topic,只要有一个发生了 rebalance,都会广播给所有 topic,导致其他 topic 也发生 rebalance。在最坏情况下可能导致每个消费者都夯住。因此我们要注意用不同的 group id 来消费 kafka。

从 go1.11 开始,go 引入了 go mod 模块。在 $GOPATH 目录下,默认 go mod 是不启用的,需要手动执行 GO111MODULE=on来进行开启。

新项目中 go mod 的使用

1
2
3
4
5
6
7
8
9
10
11
12
//初始化 go mod
go mod init $package_name
//获取包
go get $package_name@$version
//下载依赖,会将依赖下载到 $GOPATH/pkg/mod
go mod download ./...
//将依赖下载到 vendor 包
go mod vendor
//添加引用但是还没下载的 mod
go mod tidy
//更新包版本
go get -u

旧项目中 go mod 的使用

1
2
3
4
export GO111MODULE=on 
//初始化 go mod
go mod init $package_name
go mod tidy

在 go1.5 之前,go 使用第三方包时需要通过 go get 命令。使用 go get 命令获取的包都会存在于 GOSRC 中,这样的话团队之间控制第三方包会产生很多问题。在 go1.5 之后,go 会优先检测 vendor 目录下的包,之后会检测 GOSRC,这样总是会产生一些问题,比如包名不同,或者是依赖补全。合理使用 govendor 可以解决这个问题。

获取指定的包

1
govendor fetch package

列出项目所有的包

1
2
3
4
5
6
govendor list
m missing 本地缺失
l local 在当前 vendor 中
p program
u unused
e external

获取包

1
2
govendor fetch +a #所有
govendor fetch +m//缺失

删除未使用的包

1
govendor remove +u

使用 xargs -I {} 来用 {} 替代输入字符

1
2
3
4
5
6
7
8
9
10
$ ll 1.* | awk '{print $9}'
1.1.c
1.2.c
1.3.c
1.4.c
1.4.h
1.5.c
1.6.c
1.7.c
1.8.c
1
$ ll 1.* | awk '{print $9}' | cut -c 3- | xargs -I {} mv 1.{} 2.{}

使用该命令来将所有1.开头的文件重命名为2.开头。

运行结果

1
2
3
4
5
6
7
8
9
10
$ ll 1.* | awk '{print $9}'
2.1.c
2.2.c
2.3.c
2.4.c
2.4.h
2.5.c
2.6.c
2.7.c
2.8.c

MySQL 直接通过命令即可开启慢查询日志。

1
2
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 2;

通过上面两句命令便可以开启慢查询了。

通过 slow_log 表可以获取慢查询的具体情况:

1
select * from mysql.slow_log order by start_time desc;

slow_log 的表结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CREATE TABLE `slow_log` (
`start_time` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
`user_host` mediumtext NOT NULL,
`query_time` time(6) NOT NULL,
`lock_time` time(6) NOT NULL,
`rows_sent` int(11) NOT NULL,
`rows_examined` int(11) NOT NULL,
`db` varchar(512) NOT NULL,
`last_insert_id` int(11) NOT NULL,
`insert_id` int(11) NOT NULL,
`server_id` int(10) unsigned NOT NULL,
`sql_text` mediumblob NOT NULL,
`thread_id` bigint(21) unsigned NOT NULL
) ENGINE=CSV DEFAULT CHARSET=utf8 COMMENT='Slow log'

查看 slow_log 相关配置

1
show variables like 'slow_query_log%';

结果是

1
2
3
4
5
6
+---------------------+--------------------------------------+
| Variable_name | Value |
+---------------------+--------------------------------------+
| slow_query_log | ON |
| slow_query_log_file | /var/lib/mysql/hostname-slow.log |
+---------------------+--------------------------------------+

然后我们也可以从 slow_query_log_file 读取和分析慢查询的具体情况