forked from lostz/MySQLBinlogProtocol
-
Notifications
You must be signed in to change notification settings - Fork 0
/
protocol.tex
1762 lines (1450 loc) · 62.6 KB
/
protocol.tex
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
%!TEX TS-program = xelatex
%!TEX encoding = UTF-8 Unicode
\documentclass[a4paper, titlepage, 10pt, bookmark]{article}
\usepackage{geometry}
\usepackage{graphicx}
\usepackage{listings}
\usepackage{xcolor}
\usepackage{setspace}
\usepackage{fontspec,xltxtra,xunicode}
\usepackage{titlesec}
%\usepackage[dvipdfm]{hyperref}
%\geometry{letterpaper}
\defaultfontfeatures{Mapping=tex-text}
\setromanfont{SimSun} %设置中文字体
\usepackage{fontspec}
\setmainfont{微软雅黑}
\linespread{1.2}
\XeTeXlinebreaklocale “zh”
\XeTeXlinebreakskip = 0pt plus 1pt minus 0.1pt %文章内中文自动换行
\definecolor{sectionblue}{rgb}{0.37, 0.72, 0.99}
\titleformat{\section}
{\color{sectionblue}\normalfont\Large\bfseries}
{\color{sectionblue}\thesection}{1em}{}
\titleformat{\subsection}
{\color{sectionblue}\normalfont\large\bfseries}
{\color{sectionblue}\thesubsection}{1em}{}
\titleformat{\subsubsection}
{\color{sectionblue}\normalfont\large\bfseries}
{\color{sectionblue}\thesubsubsection}{1em}{}
%%%% --begin: 设置lstlisting
\definecolor{dkgreen}{rgb}{0, 0.6, 0}
\definecolor{gray}{rgb}{0.5, 0.5, 0.5}
\definecolor{darkgray}{rgb}{0.4, 0.4, 0.4}
\definecolor{lightgray}{rgb}{0.99, 0.99, 0.99}
\definecolor{mauve}{rgb}{0.58, 0, 0.82}
\lstset{
backgroundcolor=\color{lightgray}, % choose the background color
basicstyle=\scriptsize, % the size of the fonts for the code
%breakatwhitespace=false, % breaks should only happen at whitespace
breaklines=true, % sets automatic line breaking
captionpos=b, % sets the caption-position to bottom
commentstyle=\color{dkgreen}, % comment style
%deletekeywords={...}, % delete keywords from the given language
%escapeinside={\%*}{*)}, % if you want to add LaTeX within your code
%extendedchar=true, % lets you use non-ASCII characters; for 8-bits encodings only, does not work with UTF-8
frame=single, % adds a frame around the code
keywordstyle=\color{blue}, % keyword style
language=Java, % the language of the code
morekeywords={val,def,object}, % add more keywords to the set
%numbers=left, % where to put the line-numbers; possible values are (none, left, right)
%numbersep=5pt, % how far the line-numbers are from the code
%numberstyle=\tiny\color{gray}, % the style that is used for the line-numbers
rulecolor=\color{black}, % if not set, the frame-color may be changed on line-breaks within not-black text (e.g. comments (green here))
showspaces=false, % show spaces everywhere adding particular underscores; it overrides 'showstringspaces'
showstringspaces=false, % underline spaces within strings only
showtabs=false, % show tabs within strings adding particular underscores
%stepnumber=2, % the step between two line-numbers. If it's 1, each line will be numbered
stringstyle=\color{mauve}, % string literal style
tabsize=4, % sets default tabsize to 2 spaces
% title=\lstname % show the filename of files included with \lstinputlisting; also try caption instead of title
}
%%%% --end
\usepackage[unicode]{hyperref}
\setlength{\parindent}{0pt}
\graphicspath{{images/}}
\newfontfamily{\H}{SimHei}
\newfontfamily{\E}{Arial}
\title{\H \color{gray}MySQL协议文档}
\author{\color{gray}Alex Wang}
\date{\E\color{gray}\today}
\begin{document}
\setcounter{secnumdepth}{6}
\setcounter{tocdepth}{5}
\maketitle
\color{darkgray}
\tableofcontents
\newpage
\section{概述}
MySQL协议用于MySQL客户端与服务器端的交互。MySQL Connectors库(包括Connector/C、Connector/J等等)、MySQL Proxy、以及MySQL复制方案中Master和Slave之间都使用MySQL协议。\\
MySQL协议支持以下特性:
\begin{itemize}
\item 使用SSL透明加密
\item MySQL Server能力和认证数据交换
\item 支持SQL语句的预编译以及存储过程的调用
\end{itemize}
本文档基于以下的源代码:
\begin{itemize}
\item sql/sql\_parse.cc文件中的基础协议函数:dispatch\_command()
\item sql/sql\_prepare.cc文件中的SQL预编译相关协议处理函数:mysqld
\_stmt\_prepare()、mysqld\_stmt\_execute()、mysqld\_stmt\_close()、mysqld\_stmt\_reset()、mysqld\_stmt\_fetch()、mysql\_stmt\_get\_longdata()
\item sql/sql\_repl.cc文件中的binlog处理函数:mysql\_binlog\_send()
\item sql/protocol.cc文件中关于类型和值编码的部分
\end{itemize}
\subsection{基础类型}
\subsubsection{整数类型}
MySQL协议包含了两种整数类型编码方式:
\begin{itemize}
\item 定长整数
\item 变长(length-encoded)整数
\end{itemize}
\paragraph{定长整数}
定长整数类型(Protocol::FixedLengthInteger)包括了type 1、type 2、type 3、type 4、type 6、type 8共六种长度的整数。在存储的时候,低位在前,比如,type 3类型的1存储为:
\begin{spacing}{1.0}
\begin{lstlisting}
01 00 00
\end{lstlisting}
\end{spacing}
\paragraph{变长整数}
变长整数类型(Protocol::LengthEncodedInteger)根据值的大小长度可能是1、3、4或者9个byte。变长整数的编码方式如下:
\begin{itemize}
\item 如果value < 251,使用1 byte
\item 如果value >= 251 同时 value < 2 ** 16,使用fc + 2 byte
\item 如果value >= 2 ** 16 同时 value < 2 ** 24,使用fd + 3 byte
\item 如果value >= 2 ** 24 同时 value < 2 ** 64,使用fe + 8 byte
\end{itemize}
当需要获取变长整数的值时只需要先检查第一个byte,然后判断长度:
\begin{itemize}
\item 如果第一个byte位 < 251,转换为1byte的整数
\item 如果第一个byte位 >= 251 同时 < 2 ** 16,则将后两位转换为2 byte的整数
\item 如果第一个byte位 >= 2 ** 16 同时 < 2 ** 24,则将后三位转换为3 byte的整数
\item 如果第一个byte位 >= 2 ** 24 同时 < 2 ** 64,则将后八位转换为8 byte的整数
\end{itemize}
下面章节使用lenenc\_int表示变长整数。
\subsubsection{String类型}
String类型是一个byte数组,在协议中可以有以下的编码方式:
\begin{itemize}
\item Protocol::FixedLengthString:长度已知的字符串,比如ERR\_Packet包中的sql-state字段的长度总是5,以下简称string.fix\_len
\item Protocol::NulTerminatedString:以[00]结尾的字符串,以下简称string.NUL
\item Protocol::VariableLengthString:字符串的长度由其它字段表示、或者在运行时计算得出,以下简称string.var\_len
\item Protocol::LengthEncodedString:字符串有一个变长整型的前缀表示其长度,是Protocol::VariableLengthString的一个特例,以下简称lenenc\_str
\item Protocol::RestOfPacketString:字符串位于一个包的尾部,它的长度可由包的长度减去当前位置获取,以下简称string.EOF
\end{itemize}
\subsection{MySQL包}
当MySQL客户端或者服务器端发送数据时,必须:
\begin{itemize}
\item 将数据切分成长度不超过$2^{24}$-1,那么持续发送长度为(0xffffff)的包,直到包的长度小于$2^{24}$-1。的数据包
\item 为每个数据包添加一个包头
\end{itemize}
下图表示一个超长包的打包示意图:
\begin{center}
\includegraphics[width=4in]{027.png}
\end{center}
MySQL数据包长度不超过16MB,每个包包含以下内容:
\begin{itemize}
\item 3 byte: 包体长度,不包含4个byte的头
\item 1 byte: 序列号
\item string[len]: 包体内容
\end{itemize}
比如COM\_QUIT的编码如下:
\begin{spacing}{1.0}
\begin{lstlisting}
01 00 00 00 01 /* length=0x01, sequence id=0x01, command=0x01*/
\end{lstlisting}
\end{spacing}
\subsubsection{大于16MB的包}
如果发送的数据长度大于$2^{24}$-1,那么持续发送长度为(0xffffff)的包,直到包的长度小于$2^{24}$-1。
\subsubsection{序列号}
在一个命令交互过程中,序列号从0开始,每个包递增1;当处理下一个命令序列时,清零。
\subsection{通用响应包}
对于大多数客户端发出的命令,服务器端响应OR\_Packet、 ERR\_Packet或EOF\_Packet。
\subsubsection{OR\_Packet}
MySQL服务器发送OK包表示命令处理成功。如果设置了CLIENT\_PROTOCOL\_41,OK包中包含一个warning数量的字段。OK包的格式描述如下:
\begin{spacing}{1.0}
\begin{lstlisting}
1 [00] the OK header
lenenc-int affected rows
lenenc-int last-insert-id
//if capabilities & CLIENT_PROTOCOL_41 {
2 status_flags
2 warnings
//} elseif capabilities & CLIENT_TRANSACTIONS {
2 status_flags
//}
// if capabilities & CLIENT_SESSION_TRACK {
lenenc-str info
// if n > 0 {
lenenc-int total length (n) of session state-change
information to follow
n session state-change information
//}
//}
// else {
string[EOF] info
// }
\end{lstlisting}
\end{spacing}
[TODO] 字段解释
\subsubsection{ERR\_Packet}
表示MySQL Server处理命令时发生错误,格式如下:
\begin{spacing}{1.0}
\begin{lstlisting}
1 [ff] the ERR header
2 error code
// if capabilities & CLIENT_PROTOCOL_41 {
string[1] '#' the sql-state marker
string[5] sql-state
// }
string[EOF] error-message
\end{lstlisting}
\end{spacing}
[TODO] 字段解释
\subsubsection{EOF\_Packet}
如果设置了CLIENT\_PROTOCOL\_41,EOF包会包含warning count和status flags字段:
\begin{spacing}{1.0}
\begin{lstlisting}
1 [fe] the EOF header
// if capabilities & CLIENT_PROTOCOL_41 {
2 warning count
2 status flags
// }
\end{lstlisting}
\end{spacing}
注意:EOF包可能会和Protocol::LengthEncodedInteger发生混淆,因此,必须通过检查包的长度是否小于9来确认是EOF包。
[TODO] 字段解释
\subsubsection{状态信息}
Protocol::StatusFlags取值如下:
\begin{center}
\includegraphics[width=7in]{001.jpg}
\end{center}
\subsection{连接的生命周期}
本节描述MySQL客户端和服务器连接的生命周期包括了连接建立阶段和命令处理阶段两个部分。下一章我们结合这两个阶段对MySQL协议处理过程进行详细描述。
\newpage
\section{连接建立}
连接建立阶段包括了:
\begin{itemize}
\item 交换客户端和服务器端的能力
\item 如果需要,建立SSL通道
\item 对客户端进行认证
\end{itemize}
连接建立过程由客户端发起。当客户端同MySQL Server建立连接网络连接后,MySQL Server可以发送一个ERR\_Packet作为响应结束握手、或者发送一个Protocol::Handshake(握手包)要求。当客户端接收到Protocol::Handshake后,需要发送一个Protocol::HandshakeResponse(握手响应包)。完成握手之后,客户端可以在发送认证包之前要求MySQL Server建立一个SSL通道。\\
完成握手之后,MySQL Server向客户端表明认证的方式(除非在握手阶段已经表明了认证方式)并持续交换认证数据,直到MySQL Server返回OK\_Packet或ERR\_Packet。\\
认证的过程如下图所示:
\begin{center}
\includegraphics[width=5in]{002.png}
\end{center}
\subsection{握手}
握手开始于MySQL Server发送Protocol::Handshake,之后,客户端可以要求建立一个SSL通道(客户端发送Protocol::SSLRequest包),或直接发送Protocol::HandshakeResponse完成握手。
\subsubsection{普通握手方式}
MySQL Server发送Protocol::Handshake包,客户端响应Protocol::HandshakeResponse完成握手:
\begin{center}
\includegraphics[width=6in]{003.png}
\end{center}
\subsubsection{SSL握手方式}
MySQL Server发送Protocol::Handshake、客户端响应Protocol::SSLRequest要求建立SSL通道、之后客户端和MySQL Server进行一系列标准的SSL交互建立SSL通道;通道建立之后,客户端发送Protocol::HandshakeResponse包完成握手:
\begin{center}
\includegraphics[width=6in]{004.png}
\end{center}
\subsubsection{能力交换}
为了兼容低版本的MySQL客户端,MySQL Server发送的Protocol::Handshake包中包含了:
\begin{itemize}
\item MySQL Server的版本
\item MySQL Server的能力,掩码表示。关于服务器的能力参考:\url{http://dev.mysql.com/doc/internals/en/capability-flags.html#packet-Protocol::CapabilityFlags}
\end{itemize}
客户端在Protocol::HandshakeResponse包中声明自己的能力。
\subsubsection{选择认证方法}
认证方法与用户的账号关联,存储在mysql.user表的plugin字段中。客户端发送的Protocol::HandshakeResponse包中包含了账号信息,MySQL Server根据账号信息查询mysql.user表获取认证方法。\\
实际使用中,为了减少一些网络交互,MySQL Server和客户端在握手阶段会采用一种乐观的方法选择认证方式:MySQL Server会在Protocol::Handshake带上缺省的认证方法、以及相关的数据;客户端在Protocol::HandshakeResponse包中会对是否选择默认的认证方法进行应答。\\
客户端可以不使用MySQL Server在Protocol::Handshake包中提议的认证方法,客户端将自己使用的认证方法加载到Protocol::HandshakeResponse包中发送给MySQL Server。当MySQL Server建议的认证方法和客户端使用的认证方法不同时,MySQL Server发送Protocol::AuthSwitchRequest包要求客户端使用包中表明的认证方法。\\
[TODO] MySQL旧有的密码认证方法、认证方法插件
\subsection{快速认证}
假设客户端以用户名U登录、使用的认证方法是M,当客户端和MySQL Server都使用方法M并在握手交互包中携带认证相关的数据时,就可以快速认证,认证的第一个阶段在握手就完成了。之后,客户端和MySQL Server根据认证方法的不同继续交换认证数据,直到认证成功或者失败。\\
一次快速认证成功的路径如下图所示:
\begin{center}
\includegraphics[width=6in]{005.png}
\end{center}
\begin{itemize}
\item 客户端连接到MySQL Server
\item MySQL发送Protocol::Handshake包,包中建议使用认证方法M
\item 客户端发送Protocol::HandshakeResponse包,包中表明客户端也将使用方法M
\item 客户端和MySQL Server根据M的要求持续发送认证数据,直到MySQL Server返回OK\_Packet
\end{itemize}
MySQL Server在阶段4发送的是Protocol::AuthMoreData包,包的前缀为0x01。\\
认证失败时,MySQL Server最后响应ERR\_Packet。
\subsection{认证方法不一致}
假设客户端以用户名U登录、使用的认证方法是M,当:
\begin{itemize}
\item MySQL Server默认的认证方法不同于M
\item 或客户端在Protocol::HandshakeResponse包中包含的认证方法不同于Protocol::Handshake包中建议的认证方法
\end{itemize}
客户端和MySQL Server需要重新协商选择正确的认证方法。MySQL Server发送Protocol::AuthMoreData包,包中包含了将要使用的认证方法、以及新方法产生的初始化认证数据。客户端应该使用新的认证方法并按照新方法的提示继续认证的过程。如果客户端不支持新的认证方法,则主动断开连接。
\subsubsection{更换认证方法}
步骤如下图所示:
\begin{center}
\includegraphics[width=6in]{006.png}
\end{center}
\subsubsection{客户端能力缺失}
当MySQL Server发现客户端的能力不足以完成认证过程时,发送ERR\_Packet并拒绝连接。以下情形会导致MySQL Server拒绝连接:
\begin{itemize}
\item 一个不支持插件式认证(CLIENT\_PLUGIN\_AUTH没有设置)的客户端发起连接,提供的账号需要使用Secure Password Authentication或Old Password Authentication之外的认证方式
\item 一个不支持安全认证(CLIENT\_SECURE\_CONNECTION没有设置)的客户端发起连接,提供的账号需要使用Old Password Authentication之外的认证方式
\item MySQL Server缺省的认证方法不是Secure Password Authentication,并且客户端不支持插件式认证(CLIENT\_PLUGIN\_AUTH没有设置)
\end{itemize}
此时客户端和MySQL Server的交互过程如下:
\begin{center}
\includegraphics[width=6in]{007.png}
\end{center}
\subsubsection{客户端不支持新认证方法}
即使客户端支持外部的认证方法(CLIENT\_PLUGIN\_AUTH设置了),客户端也可能不支持新的认证方法,在这种情况下,客户端直接断开连接。\\
此时客户端和MySQL Server的交互过程如下:
\begin{center}
\includegraphics[width=6in]{008.png}
\end{center}
\subsubsection{不支持插件式认证方法的客户端}
[TODO]
\subsection{连接阶段消息包结构}
\subsubsection{Protocol::Handshake}
当客户端连接到MySQL Server时,MySQL Server向客户端发送Protocol::Handshake包,不同版本的MySQL Server、以及不同的配置导致Protocol::Handshake包的内容不同。\\
Protocol::Handshake包的格式如下:
\begin{center}
\includegraphics[width=5in]{009.jpg}
\end{center}
从MySQL 3.21.0开始使用v10协议。Handshake包中的capability类型为Protocol::Capability,具体含义参考\url{http://dev.mysql.com/doc/internals/en/capability-flags.html#packet-Protocol::CapabilityFlags}。\\
character set字段类型为Protocol::CharacterSet,具体含义参考\url{http://dev.mysql.com/doc/internals/en/character-set.html#packet-Protocol::CharacterSet}。\\
status flags字段类型为Protocol::StatusFlags,具体含义参考\url{http://dev.mysql.com/doc/internals/en/status-flags.html#packet-Protocol::StatusFlags}。\\
客户端接收到MySQL Server的Handshake包之后,发送HandshakeResponse包作为响应。
\subsubsection{Protocol::HandshakeResponse}
从MySQL 4.0开始使用HandshakeResponse41格式的协议包:
\begin{center}
\includegraphics[width=5in]{010.jpg}
\end{center}
\newpage
\section{文本协议}
所谓文本协议,是指SQL语句中只包含文本,没有参数。当然了,这个查询SQL可以是select,也可以是update、delete、insert一类的。文本协议有很多,本文重点关注COM\_QUERY和相关协议。
\subsection{COM\_QUERY}
COM\_QUERY包的处理流程如下:
\begin{center}
\includegraphics[width=3in]{011.png}
\end{center}
COM\_QUERY包格式如下:
\begin{center}
\includegraphics[width=5in]{012.jpg}
\end{center}
MySQL接收到COM\_QUERY之后进行处理,并把处理结果加载在COM\_QUERY\_RESPONSE类型的包中返回给客户端。
\subsection{COM\_QUERY\_RESPONSE}
COM\_QUERY\_RESPONSE实际是一个元数据包(meta packet),它可以是以下四种类型:
\begin{itemize}
\item ERR\_Packet
\item OK\_Packet
\item Protocol::LOCAL\_INFILE\_Request
\item ProtocolText::Resultset
\end{itemize}
COM\_QUERY\_RESPONSE的基本结构如下图所示:
\begin{center}
\includegraphics[width=5in]{013.png}
\end{center}
当COM\_QUERY\_RESPONSE的第一个byte是0x00,表示OK\_Packet;0xff表示一个ERR\_Packet;0xfb表示LOCAL\_INFILE\_Request包;否则,就是一个结果集(ResultSet)包。我们重点关注结果集包。\\
结果集由两个部分构成:
\begin{itemize}
\item 列定义
\item 行数据
\end{itemize}
一个语句的结果集可能由多个包构成。列定义部分由一个标示了列的个数的包开始、后续是包含列定义的内容的数据包,最后以一个OF\_Packet包结尾;数据部分由一系列包含行数据的包构成、以OF\_Packet包结尾。需要注意的是,如果MySQL Server能生成列定义部分、但是在生成行数据包的时候出错了,MySQL Server就会发送一个ERR\_Packet代替EOF\_Packet。\\
TextResult的基本结构如下:
\begin{center}
\includegraphics[width=5in]{014.png}
\end{center}
\subsubsection{列定义}
列定义包的类型为Protocol::ColumnDefinition。如果设置了CLIENT\_PROTOCOL\_41,则使用Protocol::ColumnDefinition41版本的格式;否则,使用Protocol::ColumnDefinition320。本文只关注41版本的格式。\\
Protocol::ColumnDefinition格式如下:
\begin{center}
\includegraphics[width=5in]{015.jpg}
\end{center}
其中,列类型字段参考\url{http://dev.mysql.com/doc/internals/en/binary-protocol-value.html#packet-ProtocolBinary::MYSQL_TYPE_DECIMAL}。
\subsubsection{行数据}
行数据包类型为ProtocolText::ResultsetRow,除了NULL值由0xfb表示,其它的值都转换成Protocol::LengthEncodedString类型。
\newpage
\section{Binlog}
本章讨论MySQL Binlog的格式与应用。Binlog包含了在MySQL Server上执行的所有导致数据变化的操作。本章讨论的内容同样适用与MySQL Slave的Relay Log,Binlog和Relay Log的格式是完全相同的。
\subsection{什么是Binlog}
Binlog是MySQL Server实例在运行时产生的一系列文件(我们称之为Binary Log File),其中包含了所有对MySQL Server数据更改的信息。我们可以通过设置--log-bin选项启用Binlog。\\
Binlog最早出现在MySQL 3.23.14版本。最初的Binlog是基于语句的,也就是说Binlog记录了所有可能改变数据的SQL语句。之后,MySQL支持基于行的Binlog。基于行的Binlog不再记录SQL语句了,而是把SQL语句以“事件”的形式记录下来:事件以数据行为单位,描述了SQL语句执行前后数据的变化。\\
Binlog中还包含了一些元数据信息:
\begin{itemize}
\item 所有确保Binlog记录的数据变化事件能够重现的MySQL Server状态信息
\item 错误码
\item 维护Binlog自身需要的元数据(比如rotate事件)
\end{itemize}
Binlog会跟踪记录MySQL Server运行时数据的变化,这些变化信息以“事件”的形式记录下来。换句话说,Binlog的“事件”可以重现MySQL Server数据的变化。\\
Binlog有两个重要的作用:
\begin{itemize}
\item MySQL主备复制场景:首先在Replication Master Server上开启Binlog记录Master所有的数据变化;之后Master将Binlog发送到Replication Slave Server;Replication Slave Server先将Binlog转存为Relay Log(Relay Log与Binlog格式完全相同),之后将Relay Log中的事件重放,从而保证Replication Master和Slave的事件一致
\item 一致的时间点恢复(PITR):通过mysqldump来做全备,然后通过Binlog记录之后所有的数据变更事件
\end{itemize}
通过下图,我们可以了解MySQL复制的基本原理、以及binlog在复制过程中所起的作用:
\begin{center}
\includegraphics[width=5in]{019.png}
\end{center}
\subsubsection{Binlog格式}
MySQL Binlog日志有三种格式,分别为Statement、MiXED、以及ROW格式。传统意义上说,MySQL复制记录了产生变化的语句,称为基于语句的复制(Statement-based replication、SBR),此时Binlog记载了所有产生变化的SQL语句。基于语句的复制的缺点是无法保证所有的语句都正确复制,所以在MySQL 5.1版本中,MySQL提供了基于行的复制(Row-based replication、RBR),此时binlog以行为单位记录了数据库数据的变化。\\
行复制在某些情况下会产生大量的数据,比如当我们更新10000条记录的时候,基于行复制的binlog文件中就会记录这10000条记录变更的情况。在这种情况下,我们宁愿记录这条update语句,而不是10000条记录数据变化的情况。因此,MySQL又提供了一种Mixed格式的binlog文件。
\paragraph{基于语句的Binlog}
SBR每一条会修改数据的sql都会记录在Binlog中。\\
优点:不需要记录每一行的变化,减少了Binlog日志量,节约了IO,提高性能。(相比row能节约多少性能与日志量,这个取决于应用的SQL情况,正常同一条记录修改或者插入row格式所产生的日志量还小于Statement产生的日志量,但是考虑到如果带条件的update操作,以及整表删除,alter表等操作,ROW格式会产生大量日志,因此在考虑是否使用ROW格式日志时应该跟据应用的实际情况,其所产生的日志量会增加多少,以及带来的IO性能问题。)\\
缺点:由于记录的只是执行语句,为了这些语句能在slave上正确运行,因此还必须记录每条语句在执行的时候的一些相关信息,以保证所有语句能在slave得到和在master端执行时候相同的结果。另外mysql的复制,像一些特定函数功能,slave可与master上要保持一致会有很多相关问题(如sleep()函数、last\_insert\_id()、以及user defined functions(udf)会出现问题)。
使用以下函数的语句也无法被复制:
\begin{itemize}
\item LOAD\_FILE()
\item UUID()
\item USER()
\item FOUND\_ROWS()
\item SYSDATE() (除非启动时启用了 --sysdate-is-now 选项)
\item 在INSERT ...SELECT 会产生比RBR更多的行级锁
\end{itemize}
\paragraph{基于行的Binlog}
RBR不记录sql语句上下文相关信息,仅保存哪条记录被修改。\\
优点: Binlog中可以不记录执行的sql语句的上下文相关的信息,仅需要记录那一条记录被修改成什么了。所以rowlevel的日志内容会非常清楚的记录下每一行数据修改的细节。而且不会出现某些特定情况下的存储过程,或function,以及trigger的调用和触发无法被正确复制的问题。\\
缺点:所有的执行的语句当记录到日志中的时候,都将以每行记录的修改来记录,这样可能会产生大量的日志内容。比如一条update语句修改多条记录,则Binlog中每一条修改都会有记录,这样造成Binlog日志量会很大,特别是当执行alter table之类的语句的时候,由于表结构修改,每条记录都发生改变,那么该表每一条记录都会记录到日志中。
\paragraph{混合模式的binlog}
是以上两种level的混合使用,一般的语句修改使用statment格式保存Binlog,如一些函数;statement无法完成主从复制的操作,则采用row格式保存Binlog,MySQL会根据执行的每一条具体的sql语句来区分对待记录的日志形式,也就是在Statement和Row之间选择一种.新版本的MySQL中队row level模式也被做了优化,并不是所有的修改都会以row level来记录,像遇到表结构变更的时候就会以statement模式来记录。至于update或者delete等修改数据的语句,还是会记录所有行的变更。
\subsubsection{启用Binlog}
Mysql Binlog日志格式可以通过mysql的my.cnf文件的属性binlog\_format指定。比如:
\begin{spacing}{1.0}
\begin{lstlisting}
binlog_format = MIXED //binlog日志格式
log_bin = ./mysql-bin.log //binlog日志名
expire_logs_days = 7 //binlog过期清理时间
max_binlog_size = 100m //binlog每个日志文件大小
\end{lstlisting}
\end{spacing}
按照上面的方法启用Binlog后,我们对数据库进行一系列操作:
\begin{spacing}{1.0}
\begin{lstlisting}
mysql> use test
Database changed
mysql> create table tbl(a int, b int);
iQuery OK, 0 rows affected (1.44 sec)
mysql> insert into tbl values(1,2);
Query OK, 1 row affected (0.13 sec)
mysql> flush logs;
Query OK, 0 rows affected (0.34 sec)
\end{lstlisting}
\end{spacing}
上述语句在test数据库实例中创建了表tbl、插入一条记录,然后让Binlog文件轮转。接下来可以观察到以下的Binlog事件:
\begin{spacing}{1.0}
\begin{lstlisting}
mysql> show binlog events in 'mysql-bin.000003'\G
*************************** 1. row ***************************
Log_name: mysql-bin.000003
Pos: 4
Event_type: Format_desc
Server_id: 1
End_log_pos: 120
Info: Server ver: 5.6.17-65.0-log, Binlog ver: 4
*************************** 2. row ***************************
Log_name: mysql-bin.000003
Pos: 120
Event_type: Query
Server_id: 1
End_log_pos: 224
Info: use `test`; create table tbl(a int, b int)
*************************** 3. row ***************************
Log_name: mysql-bin.000003
Pos: 224
Event_type: Query
Server_id: 1
End_log_pos: 303
Info: BEGIN
*************************** 4. row ***************************
Log_name: mysql-bin.000003
Pos: 303
Event_type: Query
Server_id: 1
End_log_pos: 404
Info: use `test`; insert into tbl values(1,2)
*************************** 5. row ***************************
Log_name: mysql-bin.000003
Pos: 404
Event_type: Xid
Server_id: 1
End_log_pos: 435
Info: COMMIT /* xid=46 */
*************************** 6. row ***************************
Log_name: mysql-bin.000003
Pos: 435
Event_type: Rotate
Server_id: 1
End_log_pos: 482
Info: mysql-bin.000004;pos=4
6 rows in set (0.00 sec)
\end{lstlisting}
\end{spacing}
这个Binlog文件中包含了一个格式描述事件、三个查询事件、一个Xid事件和一个日志轮转事件。后续章节会对每个事件进行详细描述,这里只简单看一下每个事件包含的字段:
\begin{itemize}
\item Log\_name:这个事件所在的Binary Log File名称.这里是在mysql-bin.000005
\item Pos:这个事件在当前Binary Log File中的位置
\item Event\_type:这个事件的类型,类型是有很多,用来记录数据库的操作
\item Server\_id:这个事件是在哪个server上发生的。注意在replication中,这个server id记录的是master端的server id
\item End\_log\_pos:下一个事件的位置。因此当前这个event的长度是End\_log\_pos-Pos
\item Info:直观可读的关于事件的信息
\end{itemize}
\subsubsection{Binlog结构与内容小结}
通过上面的描述,我们对Binlog的结构与内容有了大致的认识,这里对上面的内容做一个简单的总结:
\begin{itemize}
\item Binlog是由一组Binary Log File和Index File构成
\item 每个Binary Log File由4 byte的magic number开头,后续是一系列事件,描述了数据的变更状况:
\begin{itemize}
\item magic number值为0xfe 0x62 0x69 0x6e(即0xfe 'b''i''n')
\item 每个事件都包括了事件头和数据部分:
\begin{itemize}
\item 事件头包含了事件类型、产生时间、服务器标识等等
\item 数据部分与事件类型有关
\end{itemize}
\end{itemize}
\item Binary Log File的第一个事件是描述事件,表明了这个Binary Log File文件的版本(即描述事件使用的格式信息)
\item Binary Log File的最后一个事件是日志轮转事件,包含了下一个Binary Log File的文件名
\end{itemize}
Binary Log File的默认命名规则为"HOSTNAME-bin.NNNNNN",Index文件的默认命名规则为“HOSTNAME-bin.index”,其中NNNNNN以1为单位递增。\\
Relay Log File的命名规则类似,只是在将前缀中的bin改为relay:"HOSTNAME-relay.NNNNNN"、"HOSTNAME-relay.index"。
\subsection{Binlog事件}
\subsubsection{事件定义}
Binlog事件在log\_event.h文件中定义,代码如下:
\begin{spacing}{1.0}
\begin{lstlisting}
enum Log_event_type {
UNKNOWN_EVENT= 0,
START_EVENT_V3= 1,
QUERY_EVENT= 2,
STOP_EVENT= 3,
ROTATE_EVENT= 4,
INTVAR_EVENT= 5,
LOAD_EVENT= 6,
SLAVE_EVENT= 7,
CREATE_FILE_EVENT= 8,
APPEND_BLOCK_EVENT= 9,
EXEC_LOAD_EVENT= 10,
DELETE_FILE_EVENT= 11,
NEW_LOAD_EVENT= 12,
RAND_EVENT= 13,
USER_VAR_EVENT= 14,
FORMAT_DESCRIPTION_EVENT= 15,
XID_EVENT= 16,
BEGIN_LOAD_QUERY_EVENT= 17,
EXECUTE_LOAD_QUERY_EVENT= 18,
TABLE_MAP_EVENT = 19,
PRE_GA_WRITE_ROWS_EVENT = 20,
PRE_GA_UPDATE_ROWS_EVENT = 21,
PRE_GA_DELETE_ROWS_EVENT = 22,
WRITE_ROWS_EVENT = 23,
UPDATE_ROWS_EVENT = 24,
DELETE_ROWS_EVENT = 25,
INCIDENT_EVENT= 26,
HEARTBEAT_LOG_EVENT= 27,
ENUM_END_EVENT
/* end marker */
};
\end{lstlisting}
\end{spacing}
事件按用途分类如下:
\begin{itemize}
\item Binlog管理:
\begin{itemize}
\item START\_EVENT\_V3
\item FORMAT\_DESCRIPTION\_EVENT
\item STOP\_EVENT
\item ROTATE\_EVENT
\item SLAVE\_EVENT
\item INCIDENT\_EVENT
\item HEARTBEAT\_EVENT
\end{itemize}
\item SBR相关事件:
\begin{itemize}
\item QUERY\_EVENT
\item INTVAR\_EVENT
\item RAND\_EVENT
\item USER\_VAR\_EVENT
\item XID\_EVENT
\end{itemize}
\item RBR相关事件:
\begin{itemize}
\item TABLE\_MAP\_EVENT
\item DELETE\_ROWS\_EVENTv0
\item UPDATE\_ROWS\_EVENTv0
\item WRITE\_ROWS\_EVENTv0
\item DELETE\_ROWS\_EVENTv1
\item UPDATE\_ROWS\_EVENTv1
\item WRITE\_ROWS\_EVENTv1
\item DELETE\_ROWS\_EVENTv2
\item UPDATE\_ROWS\_EVENTv2
\item WRITE\_ROWS\_EVENTv2
\end{itemize}
\item Load INFILE相关事件:
\begin{itemize}
\item LOAD\_EVENT
\item CREATE\_FILE\_EVENT
\item APPEND\_BLOCK\_EVENT
\item EXEC\_LOAD\_EVENT
\item DELETE\_FILE\_EVENT
\item NEW\_LOAD\_EVENT
\item BEGIN\_LOAD\_QUERY\_EVENT
\item EXECUTE\_LOAD\_QUERY\_EVENT
\end{itemize}
\end{itemize}
\subsubsection{事件描述}
事件描述(这里只描述了MySQL 5.0之后还在使用的事件、以及RBL格式的事件):
\begin{itemize}
\item UNKNOWN\_EVENT:如果从Binary Log File中读出的事件类型不在类型定义范围内,那么这个事件就被认为是一个UNKNOWN\_EVENT
\item QUERY\_EVENT:执行一个引起数据变化的DML语句
\item STOP\_EVENT:mysqld进程停止
\item ROTATE\_EVENT:mysqld切换一个新的Binary Log File。这个事件发生有两种情况,一是用户执行了Flush Logs命令,二是一个Binary Log File大小超过了max\_binlog\_size
\item FORMAT\_DESCRIPTION\_EVENT:每个Binary Log File第一个事件,包含了服务器ID、Binlog版本号等
\item BEGIN\_LOAD\_QUERY\_EVENT:用于LOAD DATA INFILE语句
\item EXECUTE\_LOAD\_QUERY\_EVENT:用于LOAD DATA INFILE语句
\item TABLE\_MAP\_EVENT:用于RBL格式的日志,记录了下一条事件所对应的表信息,在其中存储了数据库名和表名。使用这个事件的目的是为了在Replication Master和Slave表的定义不同时复制数据
\item WRITE\_ROWS\_EVENT:记录了对单表的insert操作
\item UPDATE\_ROWS\_EVENT:记录了对单表的update操作
\item DELETE\_ROWS\_EVENT:记录了对单表的delete操作
\item INCIDENT\_EVENT:Used to log an out of the ordinary event that occurred on the master. It notifies the slave that something happened on the master that might cause data to be in an inconsistent state.
\item HEARTBEAT\_LOG\_EVENT:Master和Slave之间的心跳事件,注意:这个事件不会记录在Binary Log File中
\end{itemize}
\subsubsection{事件结构}
所有的Binlog事件都遵循相同的格式:固定的事件头和变长的事件数据 ,如下图所示:
\begin{center}
\includegraphics[width=4in]{020.jpg}
\end{center}
其中,flags字段含义参考\url{http://dev.mysql.com/doc/internals/en/event-flags.html}。
\subsubsection{FORMAT\_DESCRIPTION\_EVENT}
FORMAT\_DESCRIPTION\_EVENT事件是Binary Log File中的第一个事件,记录了Binlog的版本、MySQL Server的版本号、其它事件头的长度(总是19)、事件的附加头长度。FORMAT\_DESCRIPTION\_EVENT的格式如下图所示:
\begin{center}
\includegraphics[width=5in]{021.jpg}
\end{center}
[TODO]:附加头长度的计算方法
\subsubsection{ROTATE\_EVENT}
mysqld切换一个新的Binary Log File时会产生ROTATE\_EVENT,换句话说,ROTATE\_EVENT总在Binary Log File的尾部。ROTATE\_EVENT事件发生有两种情况,一是用户执行了Flush Logs命令,二是一个Binary Log File大小超过了max\_binlog\_size。\\
ROTATE\_EVENT事件的格式如下:
\begin{center}
\includegraphics[width=5in]{022.jpg}
\end{center}
\subsubsection{TABLE\_MAP\_EVENT}
TABLE\_MAP\_EVENT记录了下一条事件所对应的表信息,格式如下所示:
\begin{center}
\includegraphics[width=5in]{023.jpg}
\end{center}
TABLE\_MAP\_EVENT以事务为单位组织,每个事务的第一个事件必须是TABLE\_MAP\_EVENT,之后是一系列的WRITE\_ROWS\_EVENT、UPDATE\_ROWS\_EVENT或DELETE\_ROWS\_EVENT,如下图所示:
\begin{center}
\includegraphics[width=5in]{024.jpg}
\end{center}
\subsubsection{ROWS\_EVENT}
ROWS\_EVENT是WRITE\_ROWS\_EVENT、UPDATE\_ROWS\_EVENT和DELETE\_ROWS\_EVENT的统称,分别记录了insert、update和delete语句对应的Binlog事件。ROWS\_EVENT经历了v0、v1和v2共三个版本,其中v0版在MySQL 5.1.15之后就不在使用了,v1版在5.6之后不再使用,我们这里介绍v2版本的ROWS\_EVENT事件。\\
WRITE\_ROWS\_EVENT、UPDATE\_ROWS\_EVENT和DELETE\_ROWS\_EVENT的格式类似,如下图所示:
\begin{center}
\includegraphics[width=5in]{025.jpg}
\end{center}
需要注意两个字段的用途:
\begin{itemize}
\item columns-present-bitmap:每个bit表示一个列,如果bit置位了,表示该列的数据包含在后续的“行数据”部分。[TODO:我还没想到那种场景下会有没置位的情况]
\item nul-bitmap:每个bit表示列的值是否为Null
\end{itemize}
\subsection{监听流程}
客户端向MySQL Server发送COM\_BINLOG\_DUMP后等待MySQL将Binlog事件回送。COM\_BINLOG\_DUMP的格式如下:
\begin{center}
\includegraphics[width=5in]{026.jpg}
\end{center}
\newpage
\section{代码实现(基于Golang)}
基于Golang的MySQL Driver实现(GoMySQL),简单起见,并没有遵守database/sql/driver接口协议。
\subsection{模块布局}
GoMySQL包含了以下主要模块:
\begin{center}
\includegraphics[width=5in]{016.png}
\end{center}
其中:
\begin{itemize}
\item mysql模块:对外提供接口,包括数据库连接、关闭、SQL执行、结果集遍历以及binlog事件监听等等
\item init模块:处理客户端与MySQL Server的握手和认证信息
\item command模块:处理文本模式的协议
\item result模块:结果集
\item row模块:行数据
\item field模块:列定义
\item packet模块:网络层包读写相关操作
\end{itemize}
GoMySQL的功能主要包括:
\begin{itemize}
\item 上下文管理
\item Packet读写
\item 编解码
\item 数据库连接
\item 查询语句处理
\item 结果集处理
\item binlog时间监听
\end{itemize}
后续我们将以功能为单元依次展开讨论。
\subsection{上下文管理}
上下文管理功能包含在mysql模块的两个结构体中,定义如下:
\begin{spacing}{1.0}
\begin{lstlisting}
// infomation from handshake packet
type serverInfo struct {
protocol_version byte
server_version []byte
connection_id uint32
scramble [20]byte
capability uint16
lang byte
}
// MySQL connection handler
type MySQL struct {
protocol string // Network protocol
address string // Server address
user string // MySQL username
passwd string // MySQL password
dbname string // Database name
socket net.Conn // MySQL connection
rd *bufio.Reader
wr *bufio.Writer
info serverInfo // MySQL server information
seq byte // MySQL sequence number
status uint16 // Current status of MySQL server connection
max_pkt_size int // Maximum packet size that client can accept from server
timeout time.Duration // Timeout for connect
}
\end{lstlisting}
\end{spacing}
serverInfo结构体用于保存MySQL Server发送的Handshake消息包的内容,包括协议版本号、Server版本信息、连接ID(即执行show processlist语句显示的ID)、Server缺省认证方法产生的认证数据、Server支持的特性、以及Server使用的字符集信息。\\
MySQL结构体保存了一个客户端和MySQL Server连接的上下文信息,包括了如下信息:
\begin{itemize}
\item 连接使用的协议族
\item Server地址
\item 连接使用的用户名和密码
\item 连接的数据库实例名
\item 连接底层的socket句柄以及读写缓冲区
\item Handshake携带的Server信息
\item 当前包的序列号
\item 连接状态
\item 包允许的最大尺寸
\item 连接超时时间
\end{itemize}
\subsection{Packet读写}
客户端和MySQL Server通过数据包(packet)进行交互,packet的基本结构如下:
\begin{center}
\includegraphics[width=2in]{017.png}
\end{center}
Packet读写功能由packet模块处理。packet模块包含了pktReader和pktWriter两个结构体分别用于实现packet的读和写。
\subsubsection{pktReader}
pktReader的结构如下:
\begin{spacing}{1.0}
\begin{lstlisting}
type pktReader struct {
rd *bufio.Reader
seq *byte
remain int
last bool
buf [8]byte
ibuf [3]byte
}
\end{lstlisting}
\end{spacing}
其中包含了:
\begin{itemize}
\item rd:底层socket读缓冲
\item seq:序列号,从0开始
\item remain:数据包的长度(不包括包头4个byte)
\item last:当包大于16M时会分包,last用于表示是否是最后一个包
\item buf:
\item ibuf:3个byte的数组,用于读取包头的size字段
\end{itemize}
pktReader.newPktReader()构造一个pktReader对象:
\begin{spacing}{1.0}
\begin{lstlisting}
func (my *MySQL) newPktReader() *pktReader {
return &pktReader{rd: my.rd, seq: &my.seq}
}
\end{lstlisting}
\end{spacing}
pktReader.readHeader()读取包头的内容(首先读取3 byte的包长度,然后读取seq,最后判断是否是最后一个包):
\begin{spacing}{1.0}
\begin{lstlisting}
func (pr *pktReader) readHeader() {
buf := pr.ibuf[:]
for {
n, err := pr.rd.Read(buf)
if err != nil { panic(err) }
buf = buf[n:]
if len(buf) == 0 { break }
}
pr.remain = int(DecodeU24(pr.ibuf[:]))
seq, err := pr.rd.ReadByte()
if err != nil { panic(err) }
// Chceck sequence number
if *pr.seq != seq { panic(ErrSeq) }
*pr.seq++
// Last packet?
pr.last = (pr.remain != 0xffffff)
}
\end{lstlisting}
\end{spacing}
pktReader.readFull(buf []byte)方法读取buf长度的内容:
\begin{spacing}{1.0}