我们知道ProxySQL对每条SQL的执行都有统计功能,其统计信息位于stats库表stats_mysql_query_digest中。针对每条SQL,ProxySQL会计算其digest, 以及digest_text,并在该表记录它的执行信息,如最大执行时间,最小执行执行时间,执行频次等。但这些功能ProxySQL是如何实现的呢? 代码在哪儿呢?

经过一番研究发现,在文件
Query_Processor.cpp (L#1265)中有一个方法query_parser_init ,SQL在执行前都要经过这个方法的解析。

1
void Query_Processor::query_parser_init(SQP_par_t *qp, char *query, int query_length, int flags)

这个方法中又调用了如下方法:

1
2
3
qp->digest_text=mysql_query_digest_and_first_comment(query, query_length, &qp->first_comment, ((query_length < QUERY_DIGEST_BUF) ? qp->buf : NULL));
int digest_text_length=strnlen(qp->digest_text, mysql_thread___query_digests_max_digest_length);
qp->digest=SpookyHash::Hash64(qp->digest_text, digest_text_length, 0);

通过函数mysql_query_digest_and_first_comment得到digest_text,然后调用SpookyHash::Hash64来计算digest。我们再看下mysql_query_digest_and_first_comment这个方法,它被定义在文件c_tockenizer.c文件中。其实现都是处理字符串。

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
char *mysql_query_digest_and_first_comment(char *s, int _len, char **first_comment, char *buf){
int i = 0;

char cur_comment[FIRST_COMMENT_MAX_LENGTH];
cur_comment[0]=0;
int ccl=0;
int cmd=0;

int len = _len;
if (_len > mysql_thread___query_digests_max_query_length) {
len = mysql_thread___query_digests_max_query_length;
}
char *r = buf;
if (r==NULL) {
r = (char *) malloc(len + SIZECHAR);
}
char *p_r = r;
char *p_r_t = r;

char prev_char = 0;
char qutr_char = 0;

char flag = 0;
char fc=0;
int fc_len=0;

char fns=0;

bool lowercase=0;
lowercase=mysql_thread___query_digests_lowercase;

while(i < len)
{
// =================================================
// START - read token char and set flag what's going on.
// =================================================
if(flag == 0)
{
// store current position
p_r_t = p_r;

// comment type 1 - start with '/*'
if(prev_char == '/' && *s == '*')
{
ccl=0;
flag = 1;
if (*(s+1)=='!')
cmd=1;
}

// comment type 2 - start with '#'
else if(*s == '#')
{
flag = 2;
}

// comment type 3 - start with '--'
else if(prev_char == '-' && *s == '-' && ((*(s+1)==' ') || (*(s+1)=='\n') || (*(s+1)=='\r') || (*(s+1)=='\t') ))
{
flag = 3;
}

// string - start with '
else if(*s == '\'' || *s == '"')
{
flag = 4;
qutr_char = *s;
}

// may be digit - start with digit
else if(is_token_char(prev_char) && is_digit_char(*s))
{
flag = 5;
if(len == i+1)
continue;
}

// not above case - remove duplicated space char
else
{
flag = 0;
if (fns==0 && is_space_char(*s)) {
s++;
i++;
continue;
}
if (fns==0) fns=1;
if(is_space_char(prev_char) && is_space_char(*s)){
prev_char = ' ';
*p_r = ' ';
s++;
i++;
continue;
}
}
}

// =================================================
// PROCESS and FINISH - do something on each case
// =================================================
else
{
// --------
// comment
// --------
if (flag == 1) {
if (cmd) {
if (ccl<FIRST_COMMENT_MAX_LENGTH-1) {
cur_comment[ccl]=*s;
ccl++;
}
}
if (fc==0) {
fc=1;
}
if (fc==1) {
if (fc_len<FIRST_COMMENT_MAX_LENGTH-1) {
if (*first_comment==NULL) {
*first_comment=(char *)malloc(FIRST_COMMENT_MAX_LENGTH);
}
char *c=*first_comment+fc_len;
*c = !is_space_char(*s) ? *s : ' ';
fc_len++;
}
if (prev_char == '*' && *s == '/') {
if (fc_len>=2) fc_len-=2;
char *c=*first_comment+fc_len;
*c=0;
//*first_comment[fc_len]=0;
fc=2;
}
}
}
if(
// comment type 1 - /* .. */
(flag == 1 && prev_char == '*' && *s == '/') ||

// comment type 2 - # ... \n
(flag == 2 && (*s == '\n' || *s == '\r' || (i == len - 1) ))
||
// comment type 3 - -- ... \n
(flag == 3 && (*s == '\n' || *s == '\r' || (i == len -1) ))
)
{
p_r = p_r_t;
if (flag == 1 || (i == len -1)) {
p_r -= SIZECHAR;
}
if (cmd) {
cur_comment[ccl]=0;
if (ccl>=2) {
ccl-=2;
cur_comment[ccl]=0;
char el=0;
int fcc=0;
while (el==0 && fcc<ccl ) {
switch (cur_comment[fcc]) {
case '/':
case '*':
case '!':
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
case ' ':
fcc++;
break;
default:
el=1;
break;
}
}
if (el) {
memcpy(p_r,cur_comment+fcc,ccl-fcc);
p_r+=(ccl-fcc);
*p_r++=' ';
}
}
cmd=0;
}
prev_char = ' ';
flag = 0;
s++;
i++;
continue;
}

// --------
// string
// --------
else if(flag == 4)
{
// Last char process
if(len == i + 1)
{
p_r = p_r_t;
*p_r++ = '?';
flag = 0;
break;
}

// need to be ignored case
if(p_r > p_r_t + SIZECHAR)
{
if(
(prev_char == '\\' && *s == '\\') || // to process '\\\\', '\\'
(prev_char == '\\' && *s == qutr_char) || // to process '\''
(prev_char == qutr_char && *s == qutr_char) // to process ''''
)
{
prev_char = 'X';
s++;
i++;
continue;
}
}

// satisfied closing string - swap string to ?
if(*s == qutr_char && (len == i+1 || *(s + SIZECHAR) != qutr_char))
{
p_r = p_r_t;
*p_r++ = '?';
flag = 0;
if(i < len)
s++;
i++;
continue;
}
}

// --------
// digit
// --------
else if(flag == 5)
{
// last single char
if(p_r_t == p_r)
{
*p_r++ = '?';
i++;
continue;
}

// token char or last char
if(is_token_char(*s) || len == i+1)
{
if(is_digit_string(p_r_t, p_r))
{
p_r = p_r_t;
*p_r++ = '?';
if(len == i+1)
{
if(is_token_char(*s))
*p_r++ = *s;
i++;
continue;
}


}
flag = 0;
}
}
}

// =================================================
// COPY CHAR
// =================================================
// convert every space char to ' '
if (lowercase==0) {
*p_r++ = !is_space_char(*s) ? *s : ' ';
} else {
*p_r++ = !is_space_char(*s) ? (tolower(*s)) : ' ';
}
prev_char = *s++;

i++;
}

// remove a trailing space
if (p_r>r) {
char *e=p_r;
e--;
if (*e==' ') {
*e=0;
}
}

*p_r = 0;

// process query stats
return r;
}

这个方法将一条SQL中的变量部分替换成问号,生成digest_text.但这个方法依赖了这个文件中的其它函数,不能单独使用,若想按照它的方式来解析SQL的digest需要拷贝整个文件使用。

SpookyHash类定义在SpookyV2.cpp文件中,它是由一个叫Bob Jenkins的人写的一个计算128位hash的工具。ProxySQL的作者也是直接拿来用的。最终的sql digest就是由这个SpookyHash类来计算的。

知道这个过程对我们很有帮助,因为有时候我们发现来一条问题SQL,想要把它屏蔽掉,或者发送到Slave执行,根据proxy SQL的原则,我们必须知道它的SQLdigest才行,但由于SQL带有许多参数,而且有时候应用端访问的SQL许多都很类似,SQL间就多一个where条件,在这种情况下,我们往往很费力才能从sstats_mysql_query_digest表中确定该SQL的digest,然后为其定义路由规则。

如果我们知道了ProxySQL生成digest的实现方式,我们可以自己开发应用调用它的实现,传入问题SQL,直接输出SQL的digest。这大大简化来运维的复杂度。甚至我们还可以将这一功能做到web界面上,点点鼠标就能轻松处理有问题的SQL。