0%

2020 年终总结

每次在到了年底回顾的时候,最初的反应都是,这一年又过去了,好像什么也没干啊。但是仔细翻翻过去一年的记录,发现还是发生过很多事情的,但是有意义或者印象深刻的不多。如果利用好时间,一年时间可以做很多事情。 2020 年,以疫情轰轰烈烈地作为开头开始了不一样的一年。年初的时候因为疫情的原因,第一次过年没有走亲戚,大家都老老实实在家待着,跟家人待在一起,上一次这么长时间在家,应该需要追溯到高考结束那个暑假了吧。从上大学开始到工作,都没有这么长时间的在家了。也因为疫情的原因体验了一把在家远程上班的感觉,比去办公室上班累多了,因为担心被怀疑在摸鱼,所以需要时刻注意着消息提示,看到消息提示得马上回复;因为大家没办法面多面交流,为了快速达成一致就需要大家频繁地开会,我一个普通员工都开会开到难受,老板应该更难受,全天会议轰炸。

不过疫情对我来说也有好的一面,因为我本身就是一个不是很喜欢社交的人,因为疫情的原因,控制大家都不要出门,对于我来说这是一个很好的宅着的理由。让我有更多的时间可以跟自己独处,可以剖析我自己。遇到剖析不了的时候,那就去读书,去锻炼。起码这两件事不会有什么错误。

10 月份终于从上海回到了杭州工作,回来之后还是觉得在杭州比较自在,毕竟生活了二十几年,待久了想离开,离开了又想回来的地方。即使杭州这个地方变得让我有很多不喜欢的,但是回来的感觉真好。

人生中的第一本书也终于出来了,也算是了了自己一个小小的心愿,很长一段时间应该都不会想写第二本书了,从写第一本书的经历来看,自己肚子里还是很缺乏内容,我理解的写书是一种由内而外溢出来,自然而然的一个写作过程,而不是一种类似于学习的方式。我更希望我写的书是对读者能有用的,而不是多作者是有用的。所以我想着还是再积累积累,有生之年也不知道会不会有让我觉得有冲动可以写一本书的时候了。觉得缺少了很多输出的锻炼,无论是对技术文章还是其他的内容,总觉得没有感觉,准备写的时候,脑袋里空空的,啥也没有。所以 2021 年想让自己多输出、输出。

回顾

19 年立了不少 flag ,虽然倒了不少,但是该有的复盘还是得要有的:

  1. 25 本书:完成,数量上虽然完成了,但是质量上感觉没上去,读书一直感觉没有读进去,只是从我脑子中过了一年
  2. 坚持锻炼,体重减到 140:未完成,年年 flag 140 ,年年完成不了,距离目标最近的时候是 73.2KG ,那是 12 月份跟人打赌,一个月减掉了 10 斤,但是后续没有坚持减下去,不过有养成锻炼的习惯了,健身房离公司近,离家近真的很重要
  3. 找个女朋友,从“想”变成实际行动:未完成,的确还停留在 “找个女朋友的实际行动中”
  4. 做一个开源项目或者成为 TiDB Contributer:完成,零零总总也差不多给 tidb 提了 10 来个 PR 今年,虽然对 tidb 还是不了解,但是有些事情做了会发现没有想象中的那么难,干就完了
  5. 多赚钱:完成,这个没有具体标准来衡量,但是今年新增了一种投资手段——股票,幸运的是今年的股市行情还不错,所以第一年入股市没有亏,还赚了点。但是本身没有完备的投资体系,所以赚钱还都是因为运气,需要学习加总结。真想一夜暴富
  6. 发展一项爱好:算完成吧,今年尝试了一些消磨时间的爱好,比如拼模型、乐高、摩托车(考驾照中)、画画,虽然没有发现真正的爱好,但是做了不少尝试。

回顾下来,flag 的确倒了不少,但还好是完成的多于未完成的。2021 再接再厉。

立目标

21 年不立目标,低头做事。

环境介绍

  • TiDB 版本:v4.0.0
  • HAProxy 版本:1.5.18
  • IP 信息:
    • tidb-server IP: 172.16.5.189:14000
    • HAProxy IP: 172.16.5.171:12345
    • mysql client IP:172.16.5.169

配置步骤

配置 HAProxy 透传 IP ,主要是需要在 haproxy 配置文件中配置 send-proxy 选项,以及设置 tidb 配置 proxy-protocol.networks 为 HAProxy 所在机器IP

  • 查看集群信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[tidb@node5169 gangshen]$ tiup cluster display sg-latest
Starting component `cluster`: /home/tidb/.tiup/components/cluster/v1.0.7/tiup-cluster display sg-latest
TiDB Cluster: sg-latest
TiDB Version: v4.0.0
ID Role Host Ports OS/Arch Status Data Dir Deploy Dir
-- ---- ---- ----- ------- ------ -------- ----------
172.16.5.189:13000 grafana 172.16.5.189 13000 linux/x86_64 Up - /home/tidb/gangshen/install/deploy/grafana-13000
172.16.4.235:12379 pd 172.16.4.235 12379/12380 linux/x86_64 Up|L /home/tidb/gangshen/install/data/pd-12379 /home/tidb/gangshen/install/deploy/pd-12379
172.16.4.237:12379 pd 172.16.4.237 12379/12380 linux/x86_64 Up|UI /home/tidb/gangshen/install/data/pd-12379 /home/tidb/gangshen/install/deploy/pd-12379
172.16.5.189:12379 pd 172.16.5.189 12379/12380 linux/x86_64 Up /home/tidb/gangshen/install/data/pd-12379 /home/tidb/gangshen/install/deploy/pd-12379
172.16.5.189:19090 prometheus 172.16.5.189 19090 linux/x86_64 Up /home/tidb/gangshen/install/data/prometheus-19090 /home/tidb/gangshen/install/deploy/prometheus-19090
172.16.5.189:14000 tidb 172.16.5.189 14000/20080 linux/x86_64 Up - /home/tidb/gangshen/install/deploy/tidb-14000
172.16.5.171:30160 tikv 172.16.5.171 30160/30180 linux/x86_64 Up /home/tidb/gangshen/install/data/tikv-30160 /home/tidb/gangshen/install/deploy/tikv-30160
172.16.5.172:30160 tikv 172.16.5.172 30160/30180 linux/x86_64 Up /home/tidb/gangshen/install/data/tikv-30160 /home/tidb/gangshen/install/deploy/tikv-30160
  • 修改 tidb 配置 proxy-protocol.networks 为 HAProxy 所在机器IP并 reload 重启生效
1
2
tiup cluster edit-config sg-latest
tiup cluster reload sg-latest -R tidb

  • 修改 haproxy 配置,在 backend server 配置中添加 send-proxy 选项

具体 haproxy 安装以及配置可以参考 TiDB 官网 HAProxy 在 TiDB 中的最佳实践

  • 修改 haproxy 配置之后,重启 haproxy 生效配置

验证

在 172.16.5.169 机器上用 mysql client 连接 haproxy 并通过 show processlist 查看连接来源 IP

常见问题

连接报 ERROR 2013 (HY000): Lost connection to MySQL server at 'reading initial communication packet', system error: 0

问题现象:

问题原因:
haproxy 配置中没有配置 send-proxy 选项,修改 haproxy 配置之后正常。

环境信息及故障现象

集群版本

v4.0.0

故障现象

TiDB 集群的物理机异常断电重启,机器恢复后,通过 tiup cluster start <cluster-name> 启动集群,发现有两个 PD 节点启动失败

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
[root@localhost ~]# tiup cluster start t11
Starting component `cluster`: /root/.tiup/components/cluster/v1.0.0/cluster start t11
Starting cluster t11...
+ [ Serial ] - SSHKeySet: privateKey=/root/.tiup/storage/cluster/clusters/t11/ssh/id_rsa, publicKey=/root/.tiup/storage/cluster/clusters/t11/ssh/id_rsa.pub
+ [Parallel] - UserSSH: user=tidb, host=192.168.73.128
+ [Parallel] - UserSSH: user=tidb, host=192.168.73.151
+ [Parallel] - UserSSH: user=tidb, host=192.168.73.152
+ [Parallel] - UserSSH: user=tidb, host=192.168.73.153
+ [Parallel] - UserSSH: user=tidb, host=192.168.73.151
+ [Parallel] - UserSSH: user=tidb, host=192.168.73.152
+ [Parallel] - UserSSH: user=tidb, host=192.168.73.155
+ [Parallel] - UserSSH: user=tidb, host=192.168.73.128
+ [Parallel] - UserSSH: user=tidb, host=192.168.73.128
+ [Parallel] - UserSSH: user=tidb, host=192.168.73.154
+ [Parallel] - UserSSH: user=tidb, host=192.168.73.153
+ [Parallel] - UserSSH: user=tidb, host=192.168.73.128
+ [ Serial ] - ClusterOperate: operation=StartOperation, options={Roles:[] Nodes:[] Force:false SSHTimeout:5 OptTimeout:60 APITimeout:300}
Starting component pd
Starting instance pd 192.168.73.153:2379
Starting instance pd 192.168.73.151:2379
Starting instance pd 192.168.73.152:2379
Start pd 192.168.73.151:2379 success
retry error: operation timed out after 1m0s
pd 192.168.73.153:2379 failed to start: timed out waiting for port 2379 to be started after 1m0s, please check the log of the instance
retry error: operation timed out after 1m0s
pd 192.168.73.152:2379 failed to start: timed out waiting for port 2379 to be started after 1m0s, please check the log of the instance

Error: failed to start: failed to start pd: pd 192.168.73.153:2379 failed to start: timed out waiting for port 2379 to be started after 1m0s, please check the log of the instance: timed out waiting for port 2379 to be started after 1m0s

Verbose debug logs has been written to /root/logs/tiup-cluster-debug-2020-06-16-16-28-39.log.
Error: run `/root/.tiup/components/cluster/v1.0.0/cluster` (wd:/root/.tiup/data/S23ruRB) failed: exit status 1

登陆到 PD 节点所在机器,查看 pd-server 进程的确不存在,通过 pd.log 日志查看 PD 启动失败的原因:

1
2
3
4
5
6
[2020/06/16 16:29:10.180 +08:00] [INFO] [systime_mon.go:26] ["start system time monitor"]
[2020/06/16 16:29:10.181 +08:00] [INFO] [backend.go:79] ["opened backend db"] [path=/tidb/tidb-data/pd-2379/member/snap/db] [took=739.112µs]
[2020/06/16 16:29:10.182 +08:00] [INFO] [server.go:443] ["recovered v2 store from snapshot"] [snapshot-index=1400015] [snapshot-size="22 kB"]
[2020/06/16 16:29:10.195 +08:00] [WARN] [db.go:92] ["failed to find [SNAPSHOT-INDEX].snap.db"] [snapshot-index=1400015] [snapshot-file-path=/tidb/tidb-data/pd-2379/member/snap/0000000000155ccf.snap.db] [error="snap: snapshot file doesn't exist"]
[2020/06/16 16:29:10.195 +08:00] [PANIC] [server.go:454] ["failed to recover v3 backend from snapshot"] [error="failed to find database snapshot file (snap: snapshot file doesn't exist)"]
[2020/06/16 16:29:10.195 +08:00] [FATAL] [log.go:292] [panic] [recover="\"invalid memory address or nil pointer dereference\""] [stack="github.com/pingcap/log.Fatal\n\t/home/jenkins/agent/workspace/build_pd_multi_branch_v4.0.0/go/pkg/mod/github.com/pingcap/log@v0.0.0-20200117041106-d28c14d3b1cd/global.go:59\ngithub.com/pingcap/pd/v4/pkg/logutil.LogPanic\n\t/home/jenkins/agent/workspace/build_pd_multi_branch_v4.0.0/go/src/github.com/pingcap/pd/pkg/logutil/log.go:292\nruntime.gopanic\n\t/usr/local/go/src/runtime/panic.go:679\nruntime.panicmem\n\t/usr/local/go/src/runtime/panic.go:199\nruntime.sigpanic\n\t/usr/local/go/src/runtime/signal_unix.go:394\ngo.etcd.io/etcd/etcdserver.NewServer.func1\n\t/home/jenkins/agent/workspace/build_pd_multi_branch_v4.0.0/go/pkg/mod/go.etcd.io/etcd@v0.5.0-alpha.5.0.20191023171146-3cf2f69b5738/etcdserver/server.go:335\nruntime.gopanic\n\t/usr/local/go/src/runtime/panic.go:679\ngo.uber.org/zap/zapcore.(*CheckedEntry).Write\n\t/home/jenkins/agent/workspace/build_pd_multi_branch_v4.0.0/go/pkg/mod/go.uber.org/zap@v1.13.0/zapcore/entry.go:230\ngo.uber.org/zap.(*Logger).Panic\n\t/home/jenkins/agent/workspace/build_pd_multi_branch_v4.0.0/go/pkg/mod/go.uber.org/zap@v1.13.0/logger.go:225\ngo.etcd.io/etcd/etcdserver.NewServer\n\t/home/jenkins/agent/workspace/build_pd_multi_branch_v4.0.0/go/pkg/mod/go.etcd.io/etcd@v0.5.0-alpha.5.0.20191023171146-3cf2f69b5738/etcdserver/server.go:454\ngo.etcd.io/etcd/embed.StartEtcd\n\t/home/jenkins/agent/workspace/build_pd_multi_branch_v4.0.0/go/pkg/mod/go.etcd.io/etcd@v0.5.0-alpha.5.0.20191023171146-3cf2f69b5738/embed/etcd.go:211\ngithub.com/pingcap/pd/v4/server.(*Server).startEtcd\n\t/home/jenkins/agent/workspace/build_pd_multi_branch_v4.0.0/go/src/github.com/pingcap/pd/server/server.go:257\ngithub.com/pingcap/pd/v4/server.(*Server).Run\n\t/home/jenkins/agent/workspace/build_pd_multi_branch_v4.0.0/go/src/github.com/pingcap/pd/server/server.go:441\nmain.main\n\t/home/jenkins/agent/workspace/build_pd_multi_branch_v4.0.0/go/src/github.com/pingcap/pd/cmd/pd-server/main.go:118\nruntime.main\n\t/usr/local/go/src/runtime/proc.go:203"]

看到两个 PD 节点都报 failed to recover v3 backedn from snapshot 错误。

故障原因分析

故障解决步骤

参考官方文档 PD Recover 使用文档 ,通过 pd-recovery 工具恢复集群。

根据文档内容主要分为 3 个步骤:1. 找到 cluster id 2. 找到当前最大 alloc id 3. 通过 pd-recovery 恢复 pd 集群

这边的话,都是通过 PD 日志来查找 cluster idalloc id

操作步骤

  1. 查找 cluster id
1
2
3
4
5
6
7
8
9
10
11
12
13
[root@localhost ~]# cat /tidb/tidb-bin/pd-2379/log/pd.log | grep "init cluster id"
[2020/05/06 23:37:02.121 +08:00] [INFO] [server.go:340] ["init cluster id"] [cluster-id=6823755660393880966]
[2020/05/07 00:03:25.132 +08:00] [INFO] [server.go:340] ["init cluster id"] [cluster-id=6823755660393880966]
[2020/05/07 11:45:39.338 +08:00] [INFO] [server.go:340] ["init cluster id"] [cluster-id=6823755660393880966]
[2020/05/25 14:54:50.076 +08:00] [INFO] [server.go:336] ["init cluster id"] [cluster-id=6823755660393880966]
[2020/05/25 16:45:55.526 +08:00] [INFO] [server.go:336] ["init cluster id"] [cluster-id=6823755660393880966]
[2020/05/25 16:48:21.462 +08:00] [INFO] [server.go:336] ["init cluster id"] [cluster-id=6823755660393880966]
[2020/06/01 19:13:17.478 +08:00] [INFO] [server.go:336] ["init cluster id"] [cluster-id=6823755660393880966]
[2020/06/04 02:28:29.655 +08:00] [INFO] [server.go:336] ["init cluster id"] [cluster-id=6823755660393880966]
[2020/06/05 22:27:46.152 +08:00] [INFO] [server.go:336] ["init cluster id"] [cluster-id=6823755660393880966]
[2020/06/08 22:50:30.045 +08:00] [INFO] [server.go:336] ["init cluster id"] [cluster-id=6823755660393880966]
[2020/06/08 22:50:59.534 +08:00] [INFO] [server.go:336] ["init cluster id"] [cluster-id=6823755660393880966]
[2020/06/11 01:48:35.936 +08:00] [INFO] [server.go:336] ["init cluster id"] [cluster-id=6823755660393880966]
  1. 查找当前最大的 alloc id ,因为 pd-recovery 去恢复的时候需要指定一个比当前最大的 alloc id 更大的 alloc id,所以需要对所有的 pd 节点日志进行查找
1
2
3
4
5
6
7
8
9
10
11
12
pd-1
[root@localhost ~]# cat /tidb/tidb-bin/pd-2379/log/pd.log | grep "allocates"
[2020/05/06 23:37:04.752 +08:00] [INFO] [id.go:110] ["idAllocator allocates a new id"] [alloc-id=1000]
[2020/05/12 11:21:28.271 +08:00] [INFO] [id.go:110] ["idAllocator allocates a new id"] [alloc-id=2000]

pd-2
[root@localhost ~]# cat /tidb/tidb-bin/pd-2379/log/pd.log | grep "allocates"

pd-3
[root@localhost ~]# cat /tidb/tidb-bin/pd-2379/log/pd.log | grep "allocates"
[2020/05/27 11:20:20.687 +08:00] [INFO] [id.go:110] ["idAllocator allocates a new id"] [alloc-id=3000]
[2020/06/10 18:04:43.361 +08:00] [INFO] [id.go:110] ["idAllocator allocates a new id"] [alloc-id=4000]
  1. 删除旧 PD 集群 data 目录下的所有内容,因为这个集群 PD 节点 data 目录为 /tidb/tidb-data/pd-2379 ,所以删除 /tidb/tidb-data/pd-2379 目录下所有内容
1
2
3
4
5
6
7
8
9
10
11
查看 data 目录
[root@localhost ~]# tiup cluster display t11
......
192.168.73.151:2379 pd 192.168.73.151 2379/2380 linux/x86_64 Healthy /tidb/tidb-data/pd-2379 /tidb/tidb-bin/pd-2379
192.168.73.152:2379 pd 192.168.73.152 2379/2380 linux/x86_64 Healthy /tidb/tidb-data/pd-2379 /tidb/tidb-bin/pd-2379
192.168.73.153:2379 pd 192.168.73.153 2379/2380 linux/x86_64 Healthy|L /tidb/tidb-data/pd-2379 /tidb/tidb-bin/pd-2379
......

删除数据目录

[root@localhost ~]# mv /tidb/tidb-data/pd-2379/* /tmp
  1. 启动 PD 集群
1
tiup cluster start t11 -R pd
  1. 通过 pd-recovery 恢复集群,--endpoints 指定一个 pd 节点,-cluster-id 指定查找到的 cluster-id ,-alloc-id 指定比查找到的最大 alloc id 更大的一个数字,所以这边只要指定一个比 4000 更大的数字即可
1
2
[root@localhost ~]# ./pd-recover -endpoints http://192.168.73.151:2379 -cluster-id 6823755660393880966 -alloc-id 10000
recover success! please restart the PD cluster
  1. 重启 PD 集群
1
tiup cluster restart t11 -R pd
  1. 启动集群
1
tiup cluster start t11
  1. 查看集群状态,恢复正常
1
tiup cluster display t11

注意

通过 tiup 部署的 TiDB 集群需要用户自己下载 pd-recovery 工具,可以通过 http://download.pingcap.org/tidb--linux-amd64.tar.gz 链接进行下载

Go 实现 gRPC 服务

介绍

最近学习了一下 gRPC 在 Go 中的实现,做一些记录。

gRPC 是什么

讲 gRPC 之前,可以先讲一下 RPC(Remote Procedure Call) 远程过程调用,它能让客户端直接类似于调用本地方法一样调用服务端的方法。gRPC 是 Google 开源的一款高性能 RPC 框架。

gRPC 中主要有四种请求/响应模式:

  • 普通 RPC
  • 服务端流式 RPC
  • 客户端流式 RPC
  • 服务端/客户端双向流式 RPC

gRPC 好处

RPC 一般是建立在 TCP 或者 HTTP 网络连接之上的,是一个框架,所以对于开发者而言,如果使用 RPC 去实现服务端与客户端的通信,基本可以不考虑网络协议、连接等内容,专注与处理逻辑。

使用 gRPC 的话,可以将我们的服务定义在 .proto 文件中,然后在任何一种支持 gRPC 的语言中实现客户端和服务端,这可以让服务端和客户端运行在不同的环境中,另外使用 gRPC 还有其他的好处:高效序列化与反序列化、简单的 IDL 语言、方便接口更新

基础使用

已官方的 helloworld 作为例子进行讲解,示例

准备工作

  • Mac 安装 protoc 编译器
1
$ brew install protobuf
  • 安装 protoc 编译器插件
1
$ go get -u github.com/golang/protobuf/protoc-gen-go

定义服务

gRPC 是通过 Protocol Buffers 去定义 gRPC 相关的 service 服务以及请求和响应相关的类型,如果对 Protocol Buffer 不熟悉的,其实也没什么关系,直接看 protoc 文件也是比较容易看懂的。如果对 Protocol Buffer 想深入了解的 ,可以参考这个链接: Protocol Buffers

创建 helloworld.proto 文件,并填写以下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
syntax = "proto3";
package protos;

// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}


// The request message containing the user's name.
message HelloRequest {
string name = 1;
}

// The response message containing the greetings
message HelloReply {
string message = 1;
}

来解释一下上面的代码:

  • 第一行 syntax = "proto3" 表示目前使用的是 proto3 的语法
  • 通过 service 关键字定义了一个 Greeter 服务,且这个服务目前只有 SayHello 一个方法,SayHello 这个方法会接收一个 HelloRequest 类型的消息,并返回一个 HelloReply 类型的消息
  • 通过 message 关键字定义了 HelloRequest 类型消息的结构是什么样的,上述代码中,HelloRequest 消息结构只有一个字段,是 string 类型
  • 通过 message 关键字定义了 HelloReply 类型消息的结构是什么样的,上述代码中,HelloReply 消息结构只有一个字段,是 string 类型
  • 定义消息结构字段的时候,需要指定字段的字段编号,比如 string name = 1 中,1 就是字段编号,这个字段编号是一个唯一的数字,作用是在二进制消息体中表示字段用的
  • 定义消息结构字段时,需要制定更多数据类型的话,可以参考链接:Language Guide (proto3)

在 proto 文件中完成定义 gRPC 的服务方法和消息之后,我们就可以来生成 gRPC 通信中服务端与客户端的接口了,这个时候就需要我们之前安装的 protoc 以及 protoc-gen-go 工具了

1
2
3
4
5
$ protoc --go_out=plugins=grpc:. helloworld.proto

--go_out 表示指定最终生成文件的输出路径,. 表示生成在当前路径下

## 注意这边如果使用 protoc --go_out=grpc:. hellororld.proto 命令生成的结果和指定了 plugins 生成的结果有不同,需要加上 plugins 去生成

命令执行完成之后,会生成 helloworld.pd.go,具体的内容因为篇幅原因,不完全展示了,可以参考:helloworld.pd.go

会根据 proto 文件中定义的服务方法和消息生成对应的代码,比如:

  • 是消息的话会对应生成
    • 对应的 struct 结构体
    • Reset()/String()/ProtoMessage()/Descriptor()/XXX_Unmarshal()/XXX_Marshal()/XXX_Merge()/XXX_Merge()/XXX_DiscardUnknown() 方法
    • 消息中定义字段的 get 方法
  • 是服务的话会对应生成
    • 生成服务对应的 server 以及 client 接口
    • 接口对应的方法

编写服务端和客户端代码

gRPC 服务方法和消息定义完成,对应的服务端和客户端接口也定义完成之后,就需要来写服务端和客户端的具体实现代码了

服务端代码

这部分代码也是 grpc-go 官方 helloworld example 例子中的代码,借这个简单的代码理解一下服务端代码的编写

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
package main

import (
"context"
"log"
"net"
pb "github.com/Win-Man/sg-server/protos"
"google.golang.org/grpc"
)

const (
port = ":50051"
)

// server is used to implement helloworld.GreeterServer.
type server struct {
pb.UnimplementedGreeterServer
}

// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
log.Printf("Received: %v", in.GetName())
return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}

func main() {
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}

// 创建一个 gRPC 服务端实例
s := grpc.NewServer()
// 注册
pb.RegisterGreeterServer(s, &server{})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}

解释一下上面的代码

  • 首先定义了一个 server 的结构体,这个没有指定字段,直接继承了生成的 helloworld.pb.go 中定义的 pb.UnimplementedGreeterServer 结构体
  • 实现了 server 结构体的 SayHello 方法,SayHello 方法接受两个参数(ctx context.Context,in pb.HelloRequest)返回两个参数(pb.HelloReply,error),HelloRequest 和 HelloReply 其实就是 proto 中定义的消息对象,SayHello 方法的具体实现就看具体需求了,在例子中的话,就是获取了发送过来的请求中的 name 字段,将获取到的 name 值拼接上 Hello 以 Message 返回给客户端
  • 在 main 函数中定义了服务端的启动和监听过程

客户端代码

这部分代码也是 grpc-go 官方 helloworld example 例子中的代码,借这个简单的代码理解一下客户端代码的编写

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
package main

import (
"context"
"log"
"os"
"time"
pb "github.com/Win-Man/sg-server/protos"
"google.golang.org/grpc"
)

const (
address = "localhost:50051"
defaultName = "sg"
)



func main() {
// 建立连接
conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
log.Fatalf("did not connect: %v", err)
}

defer conn.Close()
c := pb.NewGreeterClient(conn)

// Contact the server and print out its response.
// 获取命令行中传入的第一个参数作为 name 值,否则name就使用默认值
name := defaultName
if len(os.Args) > 1 {
name = os.Args[1]
}
// 设置超时时间
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.GetMessage())
}

解释一下上面的代码:

  • 所有的逻辑都是在 main 函数中
  • conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock()) 这个是在建立客户端与服务端之间的 grpc 连接
  • c := pb.NewGreeterClient(conn) 通过连接初始化客户端
  • r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name}) 客户端通过调用 gRPC 方法与服务端通信,就收服务端返回的信息

运行演示

  • 运行服务端代码
1
2
3
$ go run server.go
2020/03/29 00:19:48 Received: sg
2020/03/29 00:22:11 Received: hhhh
  • 运行客户端代码
1
2
3
4
5
$ go run client.go
2020/03/29 00:19:48 Greeting: Hello sg

$ go run client.go hhhh
2020/03/29 00:22:11 Greeting: Hello hhhh

客户端成功通过 gRPC 调用了服务端。

进阶使用

简单使用中已经介绍了简单 RPC 的使用方式,在这一节中,会补充讲解 gRPC 其他几种请求/响应模式,标准步骤都是:

  1. 在 .proto 文件中定义 RPC 服务方法和消息
  2. 根据 .proto 文件生成 pb.go 文件
  3. 实现服务端代码
  4. 实现客户端代码

服务端流式 RPC

服务端流式 RPC 顾名思义就是服务端会按照多次返回响应,直到服务端认为响应发送完毕,会告诉客户端响应应发送完毕

  • 首先在 .proto 文件中定义服务端流式 RPC 的服务方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
service Greeter {
......
rpc TellMeSomething(HelloRequest) returns (stream Something){}
......
}



// The request message containing the user's name.
message HelloRequest {
string name = 1;
}

......
message Something{
int64 lineCode = 1;
string line = 2;
}

在原先定义的 Greeter 服务的基础上添加了一个 TellMeSomething 的 RPC 方法,这个 RPC 方法接收的是请求结构是 HelloRequest 类型,返回的结构是 Something 类型,且用了 stream 定义,表示返回结构是一个流,Something 类型包含一个 int64 类型的 lineCode 字段和以一个 string 类型的 line 字段。

  • 根据 .proto 文件生成 pb.go 文件,因为是自动生成的,这边就不展示完全的 pb.go 文件内容
  • 编写服务端代码,在服务端实现 TellMeSomething 方法
1
2
3
4
5
6
7
8
9
10
11
12
func (s *server)TellMeSomething(in *pb.HelloRequest, stream pb.Greeter_TellMeSomethingServer) error{
log.Printf("Received ServerStream request from: %v", in.GetName())
// 获取客户端发送的请求内容
hellostr := fmt.Sprintf("Hello,%s",in.GetName())
something := []string{hellostr,"ServerLine1","ServerLine2","ServerLine3"}
for i,v := range(something){
if err := stream.Send(&pb.Something{LineCode:int64(i),Line:v});err != nil{
return err
}
}
return nil
}

TellMeSomething 方法接受两个参数,一个是 *pb.HelloRequest 类型的参数,表示客户端发送的请求,另一个是 pb.Greeter_TellMeSomethingServer 类型的参数,这个参数其实就是服务端返回给客户端的流对象接口,生成的 .pb.go 文件中已经帮我们定义好了这个接口,包含一个 Send() 方法,用于发送响应给客户端,还有就是继承成 grpc.ServerStream 的流对象

1
2
3
4
type Greeter_TellMeSomethingServer interface {
Send(*Something) error
grpc.ServerStream
}

在服务端通过 pb.Greeter_TellMeSomethingServer 对象的 Send() 方法给客户端发送响应,如果发送所有响应结束,则通过 return nil 的方式通知客户端响应发送结束,客户端会接收到 io.EOF 信号知道服务端已经发送完毕。如果发送中间过程有错误,则通过 return err 的方式将对应的错误通知给客户端

  • 编写客户端代码,在客户端调用 TellMeSomethinng 方法
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
func main() {
// Set up a connection to the server.
conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewGreeterClient(conn)

// Contact the server and print out its response.
name := defaultName
clientFunc := "Default"
if len(os.Args) == 3 {
name = os.Args[1]
clientFunc = os.Args[2]
}else{
log.Fatal("Please input 2 arguments")
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
switch clientFunc {
......
case "ServerStream":
stream,err := c.TellMeSomething(ctx,&pb.HelloRequest{Name:name})
if err != nil{
log.Fatalf("TellMeSomething error: %v ",err)
}
for {
something,err := stream.Recv()
// 服务端信息发送完成,退出
if err == io.EOF{
break
}
if err != nil{
log.Fatalf("TellMeSomething stream error:%v",err)
}
log.Printf("Recevie from server:{LineCode:%v Line:%s}\n",something.GetLineCode(),something.GetLine())
}
......
}

客户端调用 TellMeSomething 方法,发送了一个 HelloRequest 类型的请求,获得一个 stream 返回对象,通过调用 Recv() 方法客户端接收服务端发送的一次次响应内容

客户端流式 RPC

客户端流式 RPC 就是客户端不断发送请求,直到发送完毕之后通知服务端请求发送完毕

  • 首先在 .proto 文件中定义服务端流式 RPC 的服务方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
service Greeter {
......
rpc TellYouSomething(stream Something) returns(HelloReply){}
......
}
......
// The response message containing the greetings
message HelloReply {
string message = 1;
}

message Something{
int64 lineCode = 1;
string line = 2;
}

在原先定义的 Greeter 服务的基础上添加了一个TellYouSomething 的 RPC 方法,这个 RPC 方法接收的是请求结构是 Something 类型,且用了 stream 定义,表示接受的请求是一个流,Something 类型包含一个 int64 类型的 lineCode 字段和以一个 string 类型的 line 字段,服务端返回给客户端一个 HelloReply 类型的响应

  • 根据 .proto 文件生成 pb.go 文件,因为是自动生成的,这边就不展示完全的 pb.go 文件内容
  • 编写服务端代码,在服务端实现 TellYouSomething 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func (s *server)TellYouSomething(stream pb.Greeter_TellYouSomethingServer) error{
log.Printf("Recevied ClientStream request")
messgeCount := 0
length := 0
for {
something,err := stream.Recv()
// 接收到客户端所有请求,返回响应结果
if err == io.EOF{
return stream.SendAndClose(&pb.HelloReply{Message:fmt.Sprintf("It's over.\nReceived %v times. Length:%v",messgeCount,length)})
}
if err != nil{
return err
}
messgeCount ++
length += len(something.GetLine())
}
return nil
}

这个与服务端流式 RPC 中客户端的代码比较类似,服务端通过调用 stream 的 Recv() 方法获取客户端发送的请求,直到接受到 io.EOF 信号表示已经接收到全部客户端的请求,可以返回响应了,如果中间接收请求出错,则将错误直接返回给客户端

  • 编写客户端代码,在客户端调用 TellYouSomething 方法
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
func main() {
// Set up a connection to the server.
conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewGreeterClient(conn)

// Contact the server and print out its response.
name := defaultName
clientFunc := "Default"
if len(os.Args) == 3 {
name = os.Args[1]
clientFunc = os.Args[2]
}else{
log.Fatal("Please input 2 arguments")
}

ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
switch clientFunc {
......
case "ClientStream":
stream,err := c.TellYouSomething(ctx)
if err != nil{
log.Fatalf("TellYouSomething err :%v",err)
}
clientStr := []string{"ClientLine1","ClientLine2"}
for i,v := range(clientStr){
if err := stream.Send(&pb.Something{LineCode:int64(i),Line:v});err != nil{
log.Fatalf("TellYouSomething stream error:%v",err)
}
}
rep,err := stream.CloseAndRecv()
if err != nil{
log.Fatalf("CloseAndRecd error:%v",err)
}
log.Printf("Recive from server:%s",rep.GetMessage())
......
}

与服务端流式 RPC 中服务端的代码类似,通过调用 stream 的 Send() 方法给客户端发送请求,等所有请求发送完毕通过调用 CloseAndRevc() 方法通知服务端请求发送完毕,并接收服务端的响应。

服务端/客户端双向流式 RPC

服务端/客户端双向流式 RPC 即客户端和服务端都是流式方式发送请求的

  • 首先在 .proto 文件中定义服务端流式 RPC 的服务方法
1
2
3
4
5
6
7
8
9
10
service Greeter {
......
rpc TalkWithMe(stream Something) returns(stream Something){}
}

......
message Something{
int64 lineCode = 1;
string line = 2;
}

在原先定义的 Greeter 服务的基础上添加了一个TalkWithMe的 RPC 方法,这个 RPC 方法接收的是请求结构是 Something 类型,且用了 stream 定义,表示接受的请求是一个流,Something 类型包含一个 int64 类型的 lineCode 字段和以一个 string 类型的 line 字段,服务端返回给客户端的也是 Something 类型的 stream 流。

  • 根据 .proto 文件生成 pb.go 文件,因为是自动生成的,这边就不展示完全的 pb.go 文件内容
  • 编写服务端代码,在服务端实现 TalkWithMe 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func (s *server)TalkWithMe(stream pb.Greeter_TalkWithMeServer) error{
log.Printf("Recevied Stream request")
messageCount := 0
for {
something ,err := stream.Recv()
if err == io.EOF{
return nil
}
messageCount ++
length := len(something.GetLine())
line := fmt.Sprintf("Got %s,Length:%v",something.GetLine(),length)
if err := stream.Send(&pb.Something{LineCode: int64(messageCount),Line:line});err != nil{
return err
}
}
return nil
}

服务端代码与客户端流式 RPC 中服务端代码几乎是一样的,所以就不作解释

  • 编写客户端代码,调用 TalkWithMe 方法
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
func main() {
// Set up a connection to the server.
conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewGreeterClient(conn)

// Contact the server and print out its response.
name := defaultName
clientFunc := "Default"
if len(os.Args) == 3 {
name = os.Args[1]
clientFunc = os.Args[2]
}else{
log.Fatal("Please input 2 arguments")
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
switch clientFunc {
......
case "Stream":
stream,err := c.TalkWithMe(ctx)
if err != nil{
log.Fatalf("TalkWithMe err:%v",err)
}
waitc := make(chan struct{})
go func(){
for {
something,err := stream.Recv()
// 服务端信息发送完成,退出
if err == io.EOF{
break
}
if err != nil{
log.Fatalf("TalkWithMe stream error:%v",err)
}
log.Printf("Got %v:%s\n",something.GetLineCode(),something.GetLine())
}
}()
clientStr := []string{"one","two","three"}
for i,v := range(clientStr){
if err := stream.Send(&pb.Something{LineCode:int64(i),Line:v});err != nil{
log.Fatalf("TalkWithMe Send error:%v",err)
}
}
stream.CloseSend()
<- waitc
default:
log.Fatal("Please input second args in Default/ServerStream/ClientStream/Stream")
}

}

客户端发送请求还是通过调用 stream 的 Send() 方法发送请求,但是通知服务端请求发送完毕是通过调用 CloseSend() 方法,不过因为服务端发送的请求不是一次性发送的,所以这边用 goroutine 新开了一个线程用于接收服务端返回的响应。

演示

  • 服务端流式 RPC
1
2
3
4
5
6
7
8
9
10
11

## 终端 1
$ go run server.go
2020/03/29 19:50:47 Received ServerStream request from: Aob

## 终端 2
$ go run client.go Aob ServerStream
2020/03/29 19:50:47 Recevie from server:{LineCode:0 Line:Hello,Aob}
2020/03/29 19:50:47 Recevie from server:{LineCode:1 Line:ServerLine1}
2020/03/29 19:50:47 Recevie from server:{LineCode:2 Line:ServerLine2}
2020/03/29 19:50:47 Recevie from server:{LineCode:3 Line:ServerLine3}
  • 客户端流式 RPC
1
2
3
4
5
6
7
8
## 终端 1 
$ go run server.go
2020/03/29 19:51:47 Recevied ClientStream request

## 终端 2
$ go run client.go Aob ClientStream
2020/03/29 19:51:47 Recive from server:It's over.
Received 2 times. Length:22
  • 服务端/客户端流式 RPC
1
2
3
4
5
6
7
8
9
## 终端 1 
$ go run server.go
2020/03/29 19:52:46 Recevied Stream request

## 终端 2
$ go run client.go Aob Stream
2020/03/29 19:52:46 Got 1:Got one,Length:3
2020/03/29 19:52:46 Got 2:Got two,Length:3
2020/03/29 19:52:46 Got 3:Got three,Length:5

总结

以上就是通过 Go 学习 gRPC 的一些记录,只是简单跑通了服务端与客户端之间的请求。完整代码地址

一开始刚开始看 gRPC 的时候其实有点晕,因为又是 gRPC 又是 protocol 又是流式 RPC 的,但是实际学习下来发现定义简单的 gRPC 服务还是比较简单的,按照步骤走就可以:

  1. 在 .proto 文件中定义 RPC 服务方法和消息
  2. 根据 .proto 文件生成 pb.go 文件
  3. 实现服务端代码
  4. 实现客户端代码

下一步准备学习一下 gRPC 与 TLS 安全加密相关的内容

docker-compose 中 network_mode 设置导致无法从容器外部访问 MySQL

问题现象

通过 docker-compose 在自己电脑上部署 MySQL,启动之后,查看容器状态正常,进入容器内部访问 MySQL 正常,但是在宿主机上连接 MySQL 失败。

  • docker-compose.yml 配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ cat docker-compose.yml
version: '2'
services:
mysql:
network_mode: "host"
environment:
MYSQL_ROOT_PASSWORD: "xxxx"
MYSQL_USER: "qbench"
MYSQL_PASS: "xxxx"
image: "docker.io/mysql:5.7.22"
ports:
- "3306:3306"
restart: always
volumes:
- "./db:/var/lib/mysql"
- "./conf/my.cnf:/etc/my,cnf"
- "./init:/docker-entrypoint-initdb.d/"
  • 拉取镜像
1
$ docker-compose -f docker-compose.yml pull
  • 启动
1
$ docker-compost -f docker-compose.yml up -d
  • 从宿主机上连接 MySQL 报错
1
2
3
4
5
6
7
8
[20-02-03 19:46:34]  shengang@abcs-MacBook-Pro  ~/Documents/002-workspace/docker-workspace/mysql-5.7.22
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
4865e2c56f67 mysql:5.7.22 "docker-entrypoint.s…" 6 seconds ago Up 5 seconds mysql-5722_mysql_1
[20-02-03 19:46:39] shengang@abcs-MacBook-Pro ~/Documents/002-workspace/docker-workspace/mysql-5.7.22
$ mysql -uroot -P3306 -h127.0.0.1 -p
Enter password:
ERROR 2003 (HY000): Can't connect to MySQL server on '127.0.0.1' (61)
  • 但是进入容器内部连接 MySQL 正常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[20-02-03 19:46:56]  shengang@abcs-MacBook-Pro  ~/Documents/002-workspace/docker-workspace/mysql-5.7.22
$ docker exec -it 4865e2c56f67 bash
root@linuxkit-025000000001:/# mysql -uroot -P3306 -h127.0.0.1 -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.7.22 MySQL Community Server (GPL)

Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>

问题排查

排查过程其实比较简单,因为从容器内部连接正常,从宿主机连接失败,这个基本上就是网络或者端口或者防火墙的问题了。首先看 docker container ls 的结果输出中 PORTS 列的内容是空的,说明端口没有映射出来。但是在 docker-compose.yml 配置文件中我们明明设置了 ports 配置。仔细看了下 docker-compose.yml 配置文件,发现一个 network_mode: "host" 的配置项,这个我之前没怎么了解过,只是抄的网上的配置文件。大概率就是这个配置引起的问题。于是尝试了一下将 network_mode: "host" 这一行删除掉了。然后通过 docker-compose -f docker-compose.yml down ,docker-compose -f docker-compose.yml up -d 重新部署容器,连接就正常了。

问题原因

虽然问题解决了,但是最终还是需要了解一下是为什么会导致问题的出现,得好好理解一下 network_mode 这个配置的含义。

docker-compose.yml 配置文件中的 netwokr_mode 是用于设置网络模式的,与 docker run 中的 –network 选项参数一样,加上了 service:[service name] 形式的配置,默认是 bridge 桥接模式

  • network_mode: “bridge”

默认的网络模式。如果没有指定网络驱动,默认会创建一个 bridge 类型的网络。桥接模式一般是用在应用是独立的情况,容器之间不需要互相通信。

  • network_mode: “host”

host 网络模式,针对的也是应用是独立的情况,在 host 网络模式下,移除了宿主机与容器之间的网络隔离,荣容器直接使用宿主机的网络,这样就能在容器中访问宿主机网络。host 网络模式只对 Docker 17.06 以及更高版本的 swarm 服务可用。

  • network_mode: “none”

none 表示对于这个 container ,禁用所有的网络。

  • network_mode: “service:[service name]”

  • network_mode: “container:[container name/id]”

当在 swarm 服务中,network_mode 选项会被忽略。

使用 gomail 发送邮件

使用 gomail 的一些记录笔记。

准备工作

  • 准备 SMTP/IMAP 服务用于代发邮件(这部分内容不赘述,网上可以搜索关键字:QQ/163/gamil SMTP即可)
  • 写邮件需要填写哪些信息
    • From/发件人(必须)
    • To/收件人(必须)
    • Cc/抄送(可选)
    • Subject/标题(必须)
    • Body/邮件正文内容(必须)
    • Attach/附件(可选)

使用 gomail

  • 下载 gomail 包
1
go get gopkg.in/gomail.v2
  • 创建一个要发送邮件的对象
1
m := gomail.NewMessage()

gomail 中使用 NewMessage 方法创建一个新的消息对象,默认是使用 UTF-8 字符集的,在这个创建的消息对象上设置发件人、收件人等邮件的信息,所以下面填写邮件信息的操作都是基于 NewMessage 方法创建的对象基础上的。

  • 填写 From/发件人
1
m.SetHeader("From","from@xx.com")

gomail 中通过 func (m *Message) SetHeader(field string, value ...string) 方法设置收件人、发件人、标题等信息,SetHeader 方法第一个参数表示设定要设置的内容,From 表示发件人,To 表示收件人,Subject 表示标题,后面可以跟多个 String 类型的参数,可以表示多个收件人

  • 填写 To/收件人
1
m.SetHeader("To","to@xx.com","to_1@xx.com")
  • 填写 Cc/抄送人
1
m.SetAddressHeader("Cc","cc@xx.com","cc")

gomail 中通过 func (m *Message) SetAddressHeader(field, address, name string) 方法设置抄送人,SetAddressHeader 方法第一个参数表示设定要设置的内容,第二个参数表示抄送人的邮箱地址,第三个参数表示抄送人名称

  • 填写 Subject/标题
1
m.SetHeader("Subject","This is mail title")
  • 填写 Body/邮件正文内容
1
m.SetBody("text/html","Mr.Li<br>Thanks For your help.")

gomail 中通过 func (m *Message) SetBody(contentType, body string, settings ...PartSetting) 方法设置邮件正文内容, SetBody 方法第一个参数表示设置文本内容类型,可以是 text/plain 也可以是 text/html,第二个参数表示正文内容字符串,第三个参数表示一些额外的设置(大部分情况可以不设置),如果有字符 encode 的需要可以设置

  • 添加附件
1
m.Attach("../../go.mod")

gomail 中通过 func (m *Message) Attach(filename string, settings ...FileSetting) 方法添加邮件附件,Attach 方法第一个参数表示附件文件路径,第二个参数表示表示一些额外的设置,比如附件文件名称修改等(可以不设置)

  • 设置 SMTP/IMAP 服务器
1
d := gomail.NewDialer("smtp.qq.com",465, "1234567890@qq.com", "xxxxxxxx")

gomail 中通过 func NewDialer(host string, port int, username, password string) *Dialer 方法创建一个 SMTP Dialer 用于发送邮件

  • 发送邮件
1
2
3
4
err := d.DialAndSend(m)
if err != nil{
panic(err)
}

gomail 中通过 func (d *Dialer) DialAndSend(m ...*Message) error 方法连接到 SMTP 服务器并发送邮件,最终关闭连接。

  • 使用 SetHeaders 方法一起设置邮件内容

前面介绍设置邮件发件人、收件人、标题等信息都是通过 SetHeader 方法,调用 SetHeader 方法只能设置一项内容,如果想要同时设置多项,那可以考虑使用 SetHeaders 方法,传入一个 map[string][]string 类型的参数即可

1
2
3
4
5
6
mailHeader := map[string][]string{
"From": {"from@qq.com"},
"To": {"to_user1@qq.com", "to_user2@gmail.com"},
"Subject": {"标题"},
}
m.SetHeaders(mailHeader)

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import (
"gopkg.in/gomail.v2"
)

func BaseSend() {
mailHeader := map[string][]string{
"From": {"from@qq.com"},
"To": {"to_user1@qq.com", "to_user2@gmail.com"},
"Subject": {"标题"},
}

m := gomail.NewMessage()
m.SetHeaders(mailHeader)
m.SetAddressHeader("Cc", "shengang@pingcap.com", "shengang")
m.SetBody("text/html", "尊敬的客户")
m.Attach("../../go.mod")

d := gomail.NewDialer("smtp.qq.com", 465, "1234567890@qq.com", "xxxxxxxx")

if err := d.DialAndSend(m); err != nil {
panic(err)
}
}

参考链接

2019 年终总结

又是一年年底,经历了一年的起起落落,现在能在这个时候安安心心地坐下来,回顾我这一年的内容。今年是来到这个世界的第24个年头,也是经历的第 2 个本命年,上一个本命年,还小,没有多少映象,这一个本命年,由于年龄的原因,经历了很多,这一年虽然中间坎坎坷坷,经历了很多压力、迷茫、误解、从头开始、认清现实,有很多的不如意,但最终还是过来了。总结来说,这一年大坎没有,小坎不断,人生不如意之事十之八九。脑中突然就想到一句“雄关漫道真如铁,而今迈步从头越”。

一年时间好像很短,但是确实可以发生很多事情,好些画面我感觉已经过去了很久,但是仔细想想也都还是 2019 年发生过的事情。

记事

2019 年对于个人来讲最大的变动,就是换了工作。从 16 年开始在沃趣实习,毕业之后也一直在沃趣待着。刚进沃趣的时候对于 MySQL DBA 这个职业一无所知,没有人能知道未来会发生什么。想想我当时面试沃趣的时候,明明投递的是 Python 开发的岗位,最终面试的时候确是面试的我 MySQL DBA,问我想不想做 MySQL DBA 。神奇的是,我思考了一下最终接受了这个 offer。从此就走上了一条 DBA 的不归路。人生真的很奇妙。幸运的是在沃趣遇到了一帮很好的同事,带着我学 MySQL,慢慢也发现自己对 DBA 这个职业也是比较喜欢。

对于毕业之后的第一份工作,还是有一些感情,但是由于一些个人原因以及工作压力、身体状况变差等一些方面的因素,最终还是选择了离职。来到了现在的公司 PingCAP。一家做开源分布式数据库 TiDB 的公司,职业依旧是 DBA。新的工作,新的环境,新的同事,新的挑战,一切都很新,目前需要做的就是沉下心好好做事就好,新的开始。

2019 签了自己人生第一个写书合同,之前在大学的时候,曾想过,我这一辈子一定要写本书,有点东西可以留下来,可以说。没想到这个梦想这么快就能有实现的可能,多亏大佬带。目前内容写的差不多了,准备提交初稿。

离开了杭州,回想之前的二十几年,出了出去玩和出差,没有离开过杭州,2019 年由于换工作的原因,到上海工作了,虽然上海离杭州不远,但是这一步让我有种发现新世界大门的感觉。

最后要记录一下,作为一个打工仔,在年中的时候跳槽,年终奖拿的的确是比较难受,下次还是慎重考虑。

回顾

去翻了翻 2018 年立下的 flag,惊奇的发现完成度还算是能令自己满意的:

  1. 把工作做好,争取升P:换了工作,算是变相升 P 加钱?
  2. Go、k8s、SQL优化技术栈需要补全,MySQL、Linux、分布式技术栈需要加强:Go 和 k8 的技术有了一定的基础,分布式技术栈也因为 TiDB 有所了解
  3. 20 本书:本以为是最难完成的 flag,但是由于一些机缘,2019 年让自己保持一点的时间静下心来读读书,有统计的非技术类数据,2019年已经超过 20 本了
  4. 体重减到 140:体重没有达标,但是身材能让自己满意了,体重控制住了,肚子也算是减了
  5. 多写文章多总结:差强人意
  6. 想找个女朋友:的确是停留在了“想”的阶段
  7. 培养理财意识,学习理财知识:尝试攒钱、基金定投、记账以及分配资产,目前看来效果还是可以的,继续保持

总体看来,完成度 75%。之前年年立 flag,年年打脸,今年没想到打脸没那么重。

立 flag

20 世纪 20 年代的第一年:

  1. 25 本书
  2. 坚持锻炼,体重减到 140
  3. 找个女朋友,从“想”变成实际行动
  4. 做一个开源项目或者成为 TiDB Contributer
  5. 多赚钱
  6. 发展一项爱好

TiDB Alertmanager 告警配置

TiDB 中告警相关

告警规则

告警规则文件位于 monitoring 服务器 {deploy_dir}/conf/xxx.rules.yml

1
2
3
4
5
6
7
8
9
10
11
12
[tidb@test1 tidb-ansible_v3.0.5]$ cd /data2/gangshen/deploy/
[tidb@test1 deploy]$ ll conf/ | grep rules
-rw-r--r-- 1 tidb tidb 3612 Dec 4 14:32 binlog.rules.yml
-rw-r--r-- 1 tidb tidb 4684 Dec 4 14:32 blacker.rules.yml
-rw-r--r-- 1 tidb tidb 37 Nov 22 11:24 bypass.rules.yml
-rw-r--r-- 1 tidb tidb 2044 Dec 4 14:32 kafka.rules.yml
-rw-r--r-- 1 tidb tidb 471 Nov 22 11:24 lightning.rules.yml
-rw-r--r-- 1 tidb tidb 5358 Dec 4 14:32 node.rules.yml
-rw-r--r-- 1 tidb tidb 6898 Dec 4 14:32 pd.rules.yml
-rw-r--r-- 1 tidb tidb 5013 Dec 4 14:32 tidb.rules.yml
-rw-r--r-- 1 tidb tidb 16122 Nov 22 11:24 tikv-push.rules.yml
-rw-r--r-- 1 tidb tidb 15547 Dec 4 14:32 tikv.rules.yml

加载告警规则

prometheus 服务器通过配置文件中的 rule_files 字段加载告警规则文件

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
---
global:
scrape_interval: 15s # By default, scrape targets every 15 seconds.
evaluation_interval: 15s # By default, scrape targets every 15 seconds.
# scrape_timeout is set to the global default (10s).
external_labels:
cluster: 'gangshen-cluster'
monitor: "prometheus"

# Load and evaluate rules in this file every 'evaluation_interval' seconds.
rule_files:
- 'node.rules.yml'
- 'blacker.rules.yml'
- 'bypass.rules.yml'
- 'pd.rules.yml'
- 'tidb.rules.yml'
- 'tikv.rules.yml'

alerting:
alertmanagers:
- static_configs:
- targets:
- '172.16.5.83:10093'

······

重新加载规则

  • 方法1:重启 prometheus/alertmanager 服务
1
2
systemctl restart prometheus-{prometheus_port}.service
systemctl restart alertmanager-{alertmanager_port}.service
  • 方法2:通过 API 接口
1
2
curl -XPOST http://{prometheus_server}:{prometheus_port}/-/reload
curl -XPOST http://{alertmanager_server}:{alertmanager_port}/-/reload

部署 Alertmanager

  • 修改 inventory.ini 配置文件
1
2
[alertmanager_servers]
alert-1 ansible_host=172.16.5.83 alertmanager_port=10093 alertmanager_cluster_port=10094

如果需要配置自定义端口,需要配置 alertmanager_port 和 alertmanager_cluster_port 端口

小技巧,对于一些自定义变量或者端口可以在 tidb-ansible/role/{xxx}/template/{xx}.sh.j2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
➜  tidb-ansible git:(master) cat roles/alertmanager/templates/run_alertmanager_binary.sh.j2
#!/bin/bash
set -e
ulimit -n 1000000

DEPLOY_DIR={{ deploy_dir }}
cd "${DEPLOY_DIR}" || exit 1

# WARNING: This file was auto-generated. Do not edit!
# All your edit might be overwritten!
exec > >(tee -i -a "{{ alertmanager_log_dir }}/{{ alertmanager_log_filename }}")
exec 2>&1

exec bin/alertmanager \
--config.file="conf/alertmanager.yml" \
--storage.path="{{ alertmanager_data_dir }}" \
--data.retention=120h \
--log.level="{{ alertmanager_log_level }}" \
--web.listen-address=":{{ alertmanager_port }}" \
--cluster.listen-address=":{{ alertmanager_cluster_port }}"
  • deploy
1
ansible-playbook deploy.yml -l alert-1
  • start
1
2
ansible-playbook start.yml -l alert-1
ansible-playbook rolling_update_monitor.yml --tags=prometheus

邮件告警配置

  • 修改 {deploy_dir}/conf/alertmanager.yml
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
64
global:
# The smarthost and SMTP sender used for mail notifications.
smtp_smarthost: 'smtp.qq.com:465'
smtp_from: 'xxxxx@qq.com'
smtp_auth_username: 'xxxxx@qq.com'
smtp_auth_password: '第三方授权码'
smtp_require_tls: false

# The Slack webhook URL.
# slack_api_url: ''

route:
# A default receiver
receiver: "db-alert-email"

# The labels by which incoming alerts are grouped together. For example,
# multiple alerts coming in for cluster=A and alertname=LatencyHigh would
# be batched into a single group.
group_by: ['env','instance','alertname','type','group','job']

# When a new group of alerts is created by an incoming alert, wait at
# least 'group_wait' to send the initial notification.
# This way ensures that you get multiple alerts for the same group that start
# firing shortly after another are batched together on the first
# notification.
group_wait: 30s

# When the first notification was sent, wait 'group_interval' to send a batch
# of new alerts that started firing for that group.
group_interval: 3m

# If an alert has successfully been sent, wait 'repeat_interval' to
# resend them.
repeat_interval: 3m

routes:
# - match:
# receiver: webhook-kafka-adapter
# continue: true
# - match:
# env: test-cluster
# receiver: db-alert-slack
# - match:
# env: test-cluster
# receiver: db-alert-email

receivers:
# - name: 'webhook-kafka-adapter'
# webhook_configs:
# - send_resolved: true
# url: 'http://10.0.3.6:28082/v1/alertmanager'

#- name: 'db-alert-slack'
# slack_configs:
# - channel: '#alerts'
# username: 'db-alert'
# icon_emoji: ':bell:'
# title: '{{ .CommonLabels.alertname }}'
# text: '{{ .CommonAnnotations.summary }} {{ .CommonAnnotations.description }} expr: {{ .CommonLabels.expr }} http://172.0.0.1:9093/#/alerts'

- name: 'db-alert-email'
email_configs:
- send_resolved: true
to: 'shengang@pingcap.com'
  • 修改配置文件之后重启 alertmanager 服务或者重新加载配置文件
  • 触发告警之后,可以在 prometheus 中看到告警
  • 在 alertmanager 页面也可以看到告警
  • 并且可以在邮箱收到邮件

MySQL Binlog(十一)——MySQL binlog event解析实例

前言

最后一篇文章是基于前面的知识,进行实际binlog的解析,看看常见的insert、update、delete语句在binlog中存储的形式。

示例:INSERT语句在binlog中event表现形式

下面,我们就看一下一条INSERT语句在会产生哪些类型的event,这些event在数据库中查看是怎么样的,在binlog文件中查看是怎么样的。

ROW格式下表现形式

以下显示内容是在MySQL-5.6.34版本上,且binlog日志记录格式为ROW模式的情况下。

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
root@localhost : gangshen 09:34:02> show binary logs;
+------------------+-----------+
| Log_name | File_size |
+------------------+-----------+
| mysql-bin.000001 | 120 |
+------------------+-----------+
1 row in set (0.00 sec)

root@localhost : gangshen 09:34:22> show binlog events in 'mysql-bin.000001';
+------------------+-----+-------------+-----------+-------------+---------------------------------------+
| Log_name | Pos | Event_type | Server_id | End_log_pos | Info |
+------------------+-----+-------------+-----------+-------------+---------------------------------------+
| mysql-bin.000001 | 4 | Format_desc | 330619 | 120 | Server ver: 5.6.34-log, Binlog ver: 4 |
+------------------+-----+-------------+-----------+-------------+---------------------------------------+
1 row in set (0.00 sec)

root@localhost : gangshen 09:34:32> insert into test1(`name`) values('woqutech');
Query OK, 1 row affected (0.03 sec)

root@localhost : gangshen 09:36:01> show binlog events in 'mysql-bin.000001';
+------------------+-----+-------------+-----------+-------------+------------------------------------------------+
| Log_name | Pos | Event_type | Server_id | End_log_pos | Info |
+------------------+-----+-------------+-----------+-------------+------------------------------------------------+
| mysql-bin.000001 | 4 | Format_desc | 330619 | 120 | Server ver: 5.6.34-log, Binlog ver: 4 |
| mysql-bin.000001 | 120 | Query | 330619 | 201 | BEGIN |
| mysql-bin.000001 | 201 | Rows_query | 330619 | 269 | # insert into test1(`name`) values('woqutech') |
| mysql-bin.000001 | 269 | Table_map | 330619 | 324 | table_id: 70 (gangshen.test1) |
| mysql-bin.000001 | 324 | Write_rows | 330619 | 373 | table_id: 70 flags: STMT_END_F |
| mysql-bin.000001 | 373 | Xid | 330619 | 404 | COMMIT /* xid=13 */ |
+------------------+-----+-------------+-----------+-------------+------------------------------------------------+
6 rows in set (0.00 sec)

从数据库中,我们可以看到insert into test1(name) values('woqutech');语句,在binlog日志文件中转换成了5个event存储,分别为Query,Rows_query,Table_map,Write_rows,Xid类型的event,这些event中,Query event表示一个更新语句的开始,Rows_query event记录了更新语句的语句内容,Table_map event中记录了insert语句操作的表的信息,Write_rows event记录了真实更新的记录的内容,最后一个Xid event中表示COMMIT操作。

在数据库中查看binlog文件内容可以很直观的看到,event的类型以及主要内容,那我们接着来看看,在binlog文件中,通过mysqlbinlog工具可以看到的具体内容是什么样的。

可以看到通过mysqlbinlog工具查看binlog文件,可以看到每个event中更加详细的内容。

STATEMENT格式下表现形式

以下显示内容是在MySQL-5.6.34版本上,且binlog日志记录格式为STATEMENT模式的情况下。

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
root@localhost : gangshen 04:31:35> show binary logs;
+------------------+-----------+
| Log_name | File_size |
+------------------+-----------+
| mysql-bin.000001 | 143 |
| mysql-bin.000002 | 120 |
+------------------+-----------+
2 rows in set (0.00 sec)

root@localhost : gangshen 04:31:48> show binlog events in 'mysql-bin.000002';
+------------------+-----+-------------+-----------+-------------+---------------------------------------+
| Log_name | Pos | Event_type | Server_id | End_log_pos | Info |
+------------------+-----+-------------+-----------+-------------+---------------------------------------+
| mysql-bin.000002 | 4 | Format_desc | 330619 | 120 | Server ver: 5.6.34-log, Binlog ver: 4 |
+------------------+-----+-------------+-----------+-------------+---------------------------------------+
1 row in set (0.00 sec)

root@localhost : gangshen 04:32:02> insert into test1(`name`) values('woqu112233');
Query OK, 1 row affected (0.05 sec)

root@localhost : gangshen 04:32:29> show binlog events in 'mysql-bin.000002';
+------------------+-----+-------------+-----------+-------------+----------------------------------------------------------------+
| Log_name | Pos | Event_type | Server_id | End_log_pos | Info |
+------------------+-----+-------------+-----------+-------------+----------------------------------------------------------------+
| mysql-bin.000002 | 4 | Format_desc | 330619 | 120 | Server ver: 5.6.34-log, Binlog ver: 4 |
| mysql-bin.000002 | 120 | Query | 330619 | 212 | BEGIN |
| mysql-bin.000002 | 212 | Intvar | 330619 | 244 | INSERT_ID=8 |
| mysql-bin.000002 | 244 | Query | 330619 | 377 | use `gangshen`; insert into test1(`name`) values('woqu112233') |
| mysql-bin.000002 | 377 | Xid | 330619 | 408 | COMMIT /* xid=17 */ |
+------------------+-----+-------------+-----------+-------------+----------------------------------------------------------------+
5 rows in set (0.00 sec)

从数据库中我们可以看到insert into test1(name) values('woqu112233');这个insert语句转换成了4个event,分别是Query,Intvar,Quert,Xid4个event。第一个Query的event表示语句开始执行,第二个Intvar的event,是因为表结构中的id字段定义的是auto_increment,这个Intvar event是对自增的主键生成主键值的,接着的Query的event就是真实执行的语句了,最后一个Xid表示语句的提交。
在数据库中查看binlog文件内容可以很直观的看到,event的类型以及主要内容,那我们接着来看看,在binlog文件中,通过mysqlbinlog工具可以看到的具体内容是什么样的。

示例:UPDATE语句在binlog中event表现形式

下面,我们就看一下一条UPDATE语句在会产生哪些类型的event,这些event在数据库中查看是怎么样的,在binlog文件中查看是怎么样的。

ROW格式下表现形式

以下显示内容是在MySQL-5.6.34版本上,且binlog日志记录格式为ROW模式的情况下。

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
mysql> show binary logs;
+------------------+-----------+
| Log_name | File_size |
+------------------+-----------+
| mysql-bin.000001 | 120 |
+------------------+-----------+
1 row in set (0.00 sec)

mysql> show binlog events in 'mysql-bin.000001';
+------------------+-----+-------------+-----------+-------------+---------------------------------------------+
| Log_name | Pos | Event_type | Server_id | End_log_pos | Info |
+------------------+-----+-------------+-----------+-------------+---------------------------------------------+
| mysql-bin.000001 | 4 | Format_desc | 9999 | 120 | Server ver: 5.6.34-debug-log, Binlog ver: 4 |
+------------------+-----+-------------+-----------+-------------+---------------------------------------------+
1 row in set (0.00 sec)

mysql> update test1 set name='woqutech_new' where name='woqutech';
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0

mysql> show binlog events in 'mysql-bin.000001';
+------------------+-----+-------------+-----------+-------------+--------------------------------------------------------------+
| Log_name | Pos | Event_type | Server_id | End_log_pos | Info |
+------------------+-----+-------------+-----------+-------------+--------------------------------------------------------------+
| mysql-bin.000001 | 4 | Format_desc | 9999 | 120 | Server ver: 5.6.34-debug-log, Binlog ver: 4 |
| mysql-bin.000001 | 120 | Query | 9999 | 196 | BEGIN |
| mysql-bin.000001 | 196 | Rows_query | 9999 | 278 | # update test1 set name='woqutech_new' where name='woqutech' |
| mysql-bin.000001 | 278 | Table_map | 9999 | 333 | table_id: 70 (gangshen.test1) |
| mysql-bin.000001 | 333 | Update_rows | 9999 | 401 | table_id: 70 flags: STMT_END_F |
| mysql-bin.000001 | 401 | Xid | 9999 | 432 | COMMIT /* xid=16 */ |
+------------------+-----+-------------+-----------+-------------+--------------------------------------------------------------+
6 rows in set (0.00 sec)

从数据库中,我们可以看到update test1 set name='woqutech_new' where name='woqutech';语句,在binlog日志文件中转换成了5个event存储,分别为Query,Rows_query,Table_map,Update_rows,Xid类型的event,这些event中,Query event表示一个更新语句的开始,Rows_query event记录了更新语句的语句内容,Table_map event中记录了insert语句操作的表的信息,Update_rows event记录了真实更新的记录的内容,最后一个Xid event中表示COMMIT操作。

在数据库中查看binlog文件内容可以很直观的看到,event的类型以及主要内容,那我们接着来看看,在binlog文件中,通过mysqlbinlog工具可以看到的具体内容是什么样的。

可以看到通过mysqlbinlog工具查看binlog文件,可以看到每个event中更加详细的内容。

STATEMENT格式下表现形式

以下显示内容是在MySQL-5.6.34版本上,且binlog日志记录格式为STATEMENT模式的情况下。

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
mysql> show binary logs;
+------------------+-----------+
| Log_name | File_size |
+------------------+-----------+
| mysql-bin.000001 | 120 |
+------------------+-----------+
1 row in set (0.00 sec)

mysql> show binlog events in 'mysql-bin.000001';
+------------------+-----+-------------+-----------+-------------+---------------------------------------------+
| Log_name | Pos | Event_type | Server_id | End_log_pos | Info |
+------------------+-----+-------------+-----------+-------------+---------------------------------------------+
| mysql-bin.000001 | 4 | Format_desc | 9999 | 120 | Server ver: 5.6.34-debug-log, Binlog ver: 4 |
+------------------+-----+-------------+-----------+-------------+---------------------------------------------+
1 row in set (0.00 sec)

mysql> update test1 set name='woqutech_new' where name='woqutech';
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0

mysql> show binlog events in 'mysql-bin.000001';
+------------------+-----+-------------+-----------+-------------+----------------------------------------------------------------------------+
| Log_name | Pos | Event_type | Server_id | End_log_pos | Info |
+------------------+-----+-------------+-----------+-------------+----------------------------------------------------------------------------+
| mysql-bin.000001 | 4 | Format_desc | 9999 | 120 | Server ver: 5.6.34-debug-log, Binlog ver: 4 |
| mysql-bin.000001 | 120 | Query | 9999 | 207 | BEGIN |
| mysql-bin.000001 | 207 | Query | 9999 | 347 | use `gangshen`; update test1 set name='woqutech_new' where name='woqutech' |
| mysql-bin.000001 | 347 | Xid | 9999 | 378 | COMMIT /* xid=46 */ |
+------------------+-----+-------------+-----------+-------------+----------------------------------------------------------------------------+
4 rows in set (0.00 sec)

从数据库中我们可以看到update test1 set name='woqutech_new' where name='woqutech';这个insert语句转换成了3个event,分别是Query,Quert,Xid4个event。第一个Query的event表示语句开始执行,接着的Query的event就是真实执行的语句了,最后一个Xid表示语句的提交。
在数据库中查看binlog文件内容可以很直观的看到,event的类型以及主要内容,那我们接着来看看,在binlog文件中,通过mysqlbinlog工具可以看到的具体内容是什么样的。

示例:DELETE语句在binlog中event表现形式

下面,我们就看一下一条DELETE语句在会产生哪些类型的event,这些event在数据库中查看是怎么样的,在binlog文件中查看是怎么样的。

ROW格式下表现形式

以下显示内容是在MySQL-5.6.34版本上,且binlog日志记录格式为ROW模式的情况下。

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
root@localhost : gangshen 09:34:02> show binary logs;
+------------------+-----------+
| Log_name | File_size |
+------------------+-----------+
| mysql-bin.000001 | 120 |
+------------------+-----------+
1 row in set (0.00 sec)

root@localhost : gangshen 09:34:22> show binlog events in 'mysql-bin.000001';
+------------------+-----+-------------+-----------+-------------+---------------------------------------+
| Log_name | Pos | Event_type | Server_id | End_log_pos | Info |
+------------------+-----+-------------+-----------+-------------+---------------------------------------+
| mysql-bin.000001 | 4 | Format_desc | 330619 | 120 | Server ver: 5.6.34-log, Binlog ver: 4 |
+------------------+-----+-------------+-----------+-------------+---------------------------------------+
1 row in set (0.00 sec)

mysql> delete from test1 where id = 1;
Query OK, 1 row affected (0.02 sec)

mysql> show binlog events in 'mysql-bin.000001';
+------------------+-----+-------------+-----------+-------------+---------------------------------------------+
| Log_name | Pos | Event_type | Server_id | End_log_pos | Info |
+------------------+-----+-------------+-----------+-------------+---------------------------------------------+
| mysql-bin.000001 | 4 | Format_desc | 9999 | 120 | Server ver: 5.6.34-debug-log, Binlog ver: 4 |
| mysql-bin.000001 | 120 | Query | 9999 | 196 | BEGIN |
| mysql-bin.000001 | 196 | Rows_query | 9999 | 250 | # delete from test1 where id = 1 |
| mysql-bin.000001 | 250 | Table_map | 9999 | 305 | table_id: 70 (gangshen.test1) |
| mysql-bin.000001 | 305 | Delete_rows | 9999 | 358 | table_id: 70 flags: STMT_END_F |
| mysql-bin.000001 | 358 | Xid | 9999 | 389 | COMMIT /* xid=27 */ |
+------------------+-----+-------------+-----------+-------------+---------------------------------------------+
6 rows in set (0.00 sec)

从数据库中,我们可以看到delete from test1 where id = 1;语句,在binlog日志文件中转换成了5个event存储,分别为Query,Rows_query,Table_map,Delete_rows,Xid类型的event,这些event中,Query event表示一个更新语句的开始,Rows_query event记录了更新语句的语句内容,Table_map event中记录了insert语句操作的表的信息,Delete_rows event记录了真实更新的记录的内容,最后一个Xid event中表示COMMIT操作。

在数据库中查看binlog文件内容可以很直观的看到,event的类型以及主要内容,那我们接着来看看,在binlog文件中,通过mysqlbinlog工具可以看到的具体内容是什么样的。

可以看到通过mysqlbinlog工具查看binlog文件,可以看到每个event中更加详细的内容。

STATEMENT格式下表现形式

以下显示内容是在MySQL-5.6.34版本上,且binlog日志记录格式为STATEMENT模式的情况下。

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
mysql> show binary logs;
+------------------+-----------+
| Log_name | File_size |
+------------------+-----------+
| mysql-bin.000001 | 120 |
+------------------+-----------+
1 row in set (0.00 sec)

mysql> show binlog events in 'mysql-bin.000001';
+------------------+-----+-------------+-----------+-------------+---------------------------------------------+
| Log_name | Pos | Event_type | Server_id | End_log_pos | Info |
+------------------+-----+-------------+-----------+-------------+---------------------------------------------+
| mysql-bin.000001 | 4 | Format_desc | 9999 | 120 | Server ver: 5.6.34-debug-log, Binlog ver: 4 |
+------------------+-----+-------------+-----------+-------------+---------------------------------------------+
1 row in set (0.00 sec)

mysql> delete from test1 where name = 'woqutech_new';
Query OK, 1 row affected (0.01 sec)

mysql> show binlog events in 'mysql-bin.000001';
+------------------+-----+-------------+-----------+-------------+---------------------------------------------------------------+
| Log_name | Pos | Event_type | Server_id | End_log_pos | Info |
+------------------+-----+-------------+-----------+-------------+---------------------------------------------------------------+
| mysql-bin.000001 | 4 | Format_desc | 9999 | 120 | Server ver: 5.6.34-debug-log, Binlog ver: 4 |
| mysql-bin.000001 | 120 | Query | 9999 | 207 | BEGIN |
| mysql-bin.000001 | 207 | Query | 9999 | 334 | use `gangshen`; delete from test1 where name = 'woqutech_new' |
| mysql-bin.000001 | 334 | Xid | 9999 | 365 | COMMIT /* xid=58 */ |
+------------------+-----+-------------+-----------+-------------+---------------------------------------------------------------+
4 rows in set (0.00 sec)

从数据库中我们可以看到delete from test1 where name = 'woqutech_new';这个insert语句转换成了3个event,分别是Query,Quert,Xid4个event。第一个Query的event表示语句开始执行,接着的Query的event就是真实执行的语句了,最后一个Xid表示语句的提交。
在数据库中查看binlog文件内容可以很直观的看到,event的类型以及主要内容,那我们接着来看看,在binlog文件中,通过mysqlbinlog工具可以看到的具体内容是什么样的。

MySQL Binlog(十)——MySQL字段存储格式——字符型字段类型

前言

本文继续介绍binlog中字符类型字段的存储格式。

字符型字段类型

中文等字符存储以该字段的字符集规定存储二进制数据。

CHAR和VARCHAR类型

  • [NATIONAL] CHAR[( M )] [CHARACTER SET charset_name ] [COLLATE collation_name ]

定长字符串存储时总是使用空格填充右侧达到指定长度。M表示字符串列的长度。M的范围是0到255。省略的花,长度为1。

CHAR是CHARACTER的简写。NATIONAL CHAR(或简写NCHAR)是标准的定义CHAR列应该使用默认字符集的SQL方法。MySQL4.1及更高版本使用utf8作为默认字符集。

CHAR BYTE是BINARY的别名。这是为了保证兼容性。

MySQL允许定义创建一个CHAR(0)的列。这主要用于必须有一个列,而实际上并不使用值,用以与旧版本的应用程序相兼容。当需要只有两个值的列时CHAR(0)也很不错:定义了CHAR(0)NULL的列只占用一位,可只取值NULL和’’(空字符串)

  • [NATIONAL] VARCHAR(M) [CHARACTER SET charset_name] [COLLATE collation_name]

变长字符串。M表示最大列长度。M的范围是0到65535。VARCAHR的最大长度由最长行的大小(所有列的共享65535字节)和字符集决定。例如,utf8字符需要多达每个字符三个字节,所以VARCHAR列最多指定21844个字符。

MySQL存储VARCHAR时使用一个或两个字节的前缀+数据。长度前缀表示内容占用的字节数。当列长度不超过255个字节长度前缀占一个字节,当列长度超过255个字节长度前缀占两个字节。

VARCHAR是CHARACTER VARYING的简写。

BINARY和VARBINARY类型

  • BINARY(M)

BINARY类型与CHAR类型相似,不过存储的是二进制字节字符串而不是非二进制字符串。M表示列的长度(以字节为单位)

  • VARBINARY(M)

VARBINARY类型与VARCHAR类型相似,不过存储的是二进制字节字符串而不是非二进制字符串。M表示最大列长度(以字节为单位)

BLOB和TEXT类型

  • TINYBLOB

这种BLOB列的最大长度是65535(2^8-1)个字节。存储每个TINYBLOB列时使用一个字节的前缀长度记录内容占用的字节数。

  • BLOB[(M)]

BLOB列的最大长度是65535(2^16-1)个字节。存储每个BLOB列时使用两个字节的前缀长度记录内容占用的字节数。

  • MEDIUMBLOB

这种BLOB列的最大长度是16777215(2^24-1)个字节。存储每个MEDIUMBLOB列时使用三个字节的前缀长度记录内容占用的字节数。

  • LONGBLOB

这种BLOB列的最大长度是4294967295或者(2^32-1)个字节。LONGBLOB列的最大有效长度取决于客户端/服务器协议配置的最大包大小和可用内存。存储每个LONGBLOB列时使用四个字节的前缀长度记录内容占用的字节数。

  • TINYTEXT [CHARACTER SET charset_name ] [COLLATE collation_name ]

这种TEXT列的最大长度是255(2^8-1)个字节。如果内容包含多字节字符,那么最大有效长度将减少。存储每个TINYTEXT时使用一个字节的前缀长度记录内容占用的字节数。

  • TEXT[( M )] [CHARACTER SET charset_name ] [COLLATE collation_name ]

TEXT列的最大长度是65535(2^16-1)个字节。如果内容包含多字节字符,那么最大有效长度将减少。存储每个TEXT列时使用两个字节的前缀长度记录内容占用的字节。

可以给出该类型的可选长度M。如果指定了,那么MySQL会创建一个最小的,但可以容纳M字节长度的TEXT列。

  • MEDIUMTEXT [CHARACTER SET charset_name ] [COLLATE collation_name ]

这种TEXT列的最大长度是16,777,215(224-1)个字节。如果内容包含多字节字符,那么最大有效长度将减少。存储每个MEDIUMTEXT列时使用三个字节的前缀长度记录内容占用的字节数。

  • LONGTEXT [CHARACTER SET charset_name ] [COLLATE collation_name ]

这种TEXT列的最大长度是4,294,967,295或者4GB(232-1)个字节。如果内容包含多字节字符,那么最大有效长度将减少。LONGTEXT列的最大有效长度取决于客户端/服务器协议中配置的最大包大小和可用内存。存储每个LONGTEXT列时使用四个字节的前缀长度记录内容占用的字节数。

ENUM类型

  • ENUM( ‘ value1 ‘,’ value2 ‘,…) [CHARACTER SET charset_name ] [COLLATE collation_name ]

枚举类型。属于字符串对象,允许从’ value1 ‘,’ value2 ‘,…,NULL或特殊的’’错误值 列表中选取一个值。ENUM值内部用整数表示。

ENUM列最多可以有65,535个不同的元素(实际的限制小于3000个元素)。一张表可以不超过255个唯一的元素,列表定义中ENUM和SET列视为一组。

SET类型

  • SET(‘ value1 ‘,’ value2 ‘,…) [CHARACTER SET charset_name ] [COLLATE collation_name ]

设置类型。属于字符串对象,可以有零个或多个值,必须从’ value1 ‘,’ value2 ‘,…中选取值,SET值内部用整数表示。

SET列最多可以有64个不同的元素。一张表可以不超过255个唯一的元素,列表定义中ENUM和SET列视为一组。

字符型数据存储格式

解析环境描述

表结构定义

1
2
3
4
5
6
7
CREATE TABLE `string_table` (
`col1` varchar(500) COLLATE utf8_bin DEFAULT NULL,
`col2` char(60) COLLATE utf8_bin DEFAULT NULL,
`col3` blob,
`col4` set('a','b','c') COLLATE utf8_bin DEFAULT NULL,
`col5` enum('one','two','three') COLLATE utf8_bin DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin

表数据展示

1
2
3
4
5
6
7
8
9
10
root@localhost : gangshen 07:37:31> insert into string_table values('abcdefg','abc','abcdefghijklmnopqrstuvwxyz','c','two');
Query OK, 1 row affected (0.04 sec)

root@localhost : gangshen 07:40:27> select * from string_table;
+---------+------+----------------------------+------+------+
| col1 | col2 | col3 | col4 | col5 |
+---------+------+----------------------------+------+------+
| abcdefg | abc | abcdefghijklmnopqrstuvwxyz | c | two |
+---------+------+----------------------------+------+------+
1 row in set (0.00 sec)

然后从binlog文件中拿到,对应的Table_map_event和Writes_rows_event对应的字节内容,开始解析

元数据解析

Table_map_event字节解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
公共头部部分省略。。。
61 00 00 00 00 00 //table_id:小端存储,16进制转换成10进制为97
01 00 //flag
08 67 61 6e 67 73 68 65 6e 00 //database_name:1个字节是字符串长度,后接一个null-terminated string 第一个字节表示字符串长度为8,后面内容将16进制转换为ascii字符为gangshen
0c 73 74 72 69 6e 67 5f 74 61 62 6c 65 00//table_name: 1个字节是字符串长度,后接一个null-terminated string第一个字节表示字符串长度为5,后面内容将16进制转换为ascii字符为string_table
05 //columns count :packet integer类型,转换之后,数值为5 表示表中有5个字段
0f fe fc fe fe //column type :
09 //metadata length : packet integer类型,转换之后,数值为9,表示记录表中的metadata内容占用9个字节
dc 05 //col1
fe b4 //col2
02 //col3
f8 01 //col4
f7 01 //col5
1f //null_bits :int((column_count + 7) / 8)个字节 一个bit表示一个字段是否可以为null
22 6b 17 48 //checksum

从Table_map_event中可以按照上面的讲述,解析出表中所有的字段类型以及对应的元数据,按照顺序分别是:

第一个字段:0x0f=MYSQL_TYPE_VARCHAR 元数据0xdc05表示字段最多占用的字节数,因为是小端存储,所以该字段最多占用0x05dc=1500字节。这个占用的字节数取决于表的字符集,因为表的字符集为utf8,所以每个字符最多占用3个字节,所以该字段最多占用500*3个字节。

第二个字段:0xfe=MYSQL_TYPE_STRING,因为是MYSQL_TYPE_STRING类型,所以其真实的字段类型应该是元数据第一个字节(0xfe)和0x30做与运算之后的结果, 第二个字节表示该字段最多占用的字节数,所以该字段最多占用0xb4=180个字节

第三个字段:0x12=MYSQL_TYPE_BLOB 元数据0x02表示该字段类型为BLOB/TEXT类型

第四个字段:0x11=MYSQL_TYPE_STRING,所以其真实的字段类型应该是元数据的第一个字节(0xf8)和0x30做与运算之后的结果,即其真实类型是MYSQLTYPE_SET第二个字节表示该字段最多占用的字节数,所以该字段最多占用0x01个字节的内容。

第五个字段:0x11=MYSQL_TYPE_STRING,所以其真实的字段类型应该是元数据的第一个字节(0xf8)和0x30做与运算之后的结果,即其真实类型是MYSQLTYPE_SET第二个字节表示该字段最多占用的字节数,所以该字段最多占用0x01个字节的内容。

字符型数据“值”解析

Write_rows_log_event字节解析

1
2
3
4
5
6
7
8
9
10
11
12
13
公共头部部分省略。。。
61 00 00 00 00 00 //table_id: 小端存储,16进制转换为10进制为97
01 00 //flag:
02 00 //var header length :小端存储,16进制转换为10进制为2
05 //m_width :packet integer,表示表中字段数量
ff //before image: (m_width + 7) / 8字节
e0 //bitmap_bits :表中两个字段,插入的值都不为NULL
07 00 61 62 63 64 65 66 67 //col1
03 61 62 63 //col2
1a 00 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 77 78 79 7a //col3
04 //col4
02 //col5
46 1d 2a 69 //checksum

MYSQL_TYPE_VARCHAR字段解析

MYSQL_TYPE_VARCHAR类型字段由长度和字符串内容两部分组成,长度由由字段的元数据决定,字段的元数据表示该字段最大占用字节数。如果元数据小于255,则MYSQL_TYPE_VARCHAR类型使用一个字节表示字符串长度,长度后面紧接着就是字符串的内容;如果元数据大于等于255,则MYSQL_TYPE_VARCHAR类型使用两个字节表示字符串长度(小端存储),长度后面紧接这就是字符串的内容。

在上面的例子中,col1的字段类型为MYSQL_TYPE_VARCHAR类型,且元数据为0x05dc=2500大于255,所以使用两个字节表示字符串长度,所以字符串长度为0x0700,因为是小端存储,所以实际为0x0007,即字符串长度为7个字节。紧接着读取7个字节的内容0x61626364656667转换为ascii为abcdefg,所以col1的字段值为abcdefg

MYSQL_TYPE_STRING字段解析

MYSQL_TYPE_STRING类型字段由长度和字符串内容两部分组成,长度由由字段的元数据决定,字段的元数据表示该字段最大占用字节数。如果元数据小于255,则MYSQL_TYPE_STRING类型使用一个字节表示字符串长度,长度后面紧接着就是字符串的内容;如果元数据大于等于255,则MYSQL_TYPE_STRING类型使用两个字节表示字符串长度(小端存储),长度后面紧接这就是字符串的内容。

在上面的例子中,col2的字段类型是MYSQL_TYPE_STRING类型,且元数据中最大占用字节数为0xb4=180小于255,所以使用一个字节表示字符串长度,所以字符串长度为0x03=3个字节。紧接着读取3个字节的内容0x616263专户为ascii为abc,所以col2的字段值为abc

MYSQL_TYPE_BLOB字段解析

MYSQL_TYPE_BLOB类型字段由长度和字符串内容两部分组成,长度由字段的元数据决定,字段的元数据表示BLOB的类型。如果元数据是1,表示是TINYBLOB/TINYTEXT类型,则MYSQL_TYPE_BLOB类型使用一个字节表示字符串长度,长度后面紧接着就是字符串的内容;如果元数据是2,表示是BLOB/TEXT类型,则MYSQL_TYPE_BLOB类型使用两个字节表示字符串长度(小端存储),长度后面紧接着就是字符串内容;如果元数据是3,表示是MEDIUMBLOB/MEDIUMTEXT类型,则MYSQL_TYPE_BLOB类型使用三个字节表示字符串长度(小端存储),长度后面紧接着就是字符串内容;如果元数据是4,表示是LONGBLOB/LONGTEXT类型,则MYSQL_TYPE_BLOB类型使用四个字节表示字符串长度(小端存储),长度后面紧接着就是字符串内容。

在上面的例子中,col3字段的类型是MYSQL_TYPE_BLOB类型,且元数据为0x02,表示类型为BLOB/TEXT类型,说明该字段使用两个字节表示字符串的长度。所以字符串长度为0x1a00,因为是小端存储,所以字符串长度为0x001a=26,紧接着读取26个字节的内容0x6162636465666768696a6b6c6d6e6f707172737475767778797a转换为ascii为abcdefghijklmnopqrstuvwxyz,所以col3的字段值为abcdefghijklmnopqrstuvwxyz。

MYSQL_TYPE_SET字段解析

MYSQL_TYPE_SET类型字段内容取决于字段定义SET的数量,表示字段值所需字节数量可以从字段元数据的第二个字节获取到,第二个字节表示内容长度。MYSQL_TYPE_SET类型字段内容采用压缩的表示方法,对应比特位置1则表示值是该位,但是从binlog中无法解析出SET类型内容的具体类型,只能解析出对应的比特位表示方式。

在上面的例子中,col4字段的类型是MYSQL_TYPE_SET,且元数据中表示该字段使用0x01个字节表示字段值,所以col4在Write_rows_log_event占用1个字节的内容,为0x04,转换为二进制为b’100’,从右往左第3位比特位上是1,表示值为set定义中第三个。从Write_rows_log_event中,无法解析出set定义的每个项,查看建表语句,得知set定义中第三个为c,即col4字段的值为c

MYSQL_TYPE_ENUM字段解析

MYSQL_TYPE_ENUM类型字段内容取决于字段定义ENUM数量,表示字段值所需字节数量可以从字段元数据的第二个字节获取到,第二个字节表示内容长度。

当元数据第二个字节等于1的时候,MYSQL_TYPE_ENUM字段值使用一个字节表示,当元数据第二个字节等于2的时候,MYSQL_TYPE_ENUM字段值使用两个字节表示。

在上面的例子中,col5字段的类型是MYSQL_TYPE_ENUM,且元数据中表示该字段使用0x01个字节表示字段值,所以col5在Write_rows_log_event中占用1个字节的内容,为0x02,转换为10进制为2,即表示值为enum定义中的第2个。在Write_rows_log_event中,无法解析出enum定义的每个项,查看建表语句,得知enum定义中第二个值为two,即col5字段的值为two。