迁移mysql字符集不一致_MySQL字符集不一致导致性能下降25%,你敢信?

故事是这样的:

我在对MySQL进行性能测试时,发现CPU使用率接近100%,其中80%us, 16%sys,3%wa,iostat发现磁盘iops2000以下,avgqu-sz不超过3,%util最高70%,看来瓶颈不在磁盘IO上面,而在CPU上。sys部分使用率有点高。

于是我果断使用perf top查看,赫然排在前面的2个,是my_ismbchar_utf8mb4和my_charpos_mb。

my_ismbchar_utf8mb4顾名思义,很明显是与字符集相关的;my_charpos_mb暂时不清楚。

dd6f8cf7b53cb99050a6645ed46b9169.png

经验告诉我,这很不正常!通常来说,消耗CPU最多的应该是数据页相关的操作才对啊。

我快速打开MySQL internal文档搜索,没找到有价值的信息。

哦,你想要知道这个故事的前情提要?抱歉,我刚刚只说了压测,按照国际惯例,我这就贴出环境和版本信息:

硬件:8核16GB,200GB SSD,腾讯云虚拟机

操作系统版本:CentOS release6.9(Final)

MySQL版本:5.7.28-log MySQL Community Server (GPL),二进制方式安装

MySQL参数:innodb_buffer_pool_size=10752M

innodb_flush_log_at_trx_commit= 1sync_binlog= 1character-set-server =utf8mb4

sysbench版本:1.0.19sysbench参数:sysbench/usr/share/sysbench/oltp_read_write.lua --tables=3 --table-size=1000000 --mysql-password=*** --mysql-user=root --mysql-socket=/usr/local/mysql5.7.28/mysql.sock --threads=128 --time=1800 run

server的字符集是utf8mb4,接下来检查一下db和表的字符集吧:

ba50ae24038982de05c8bb510865e223.png

49cb53bf8c66f0386127b2c18dd6dd7c.png

7b9d0d1b35a8e1ca178c9b296308f310.png

嗯嗯,看起来一切都是那么的正常……

server, DB, table的字符集都一致,现在只剩下sysbench的嫌疑最大!

可是,要怎么检查sysbench已经连接到MySQL的那些会话的字符集设置呢?

我的sysbench命令没有显式地指定字符集;show processlist没有character_set_client信息,information_schema库和mysql库里面也没有与character_set_client信息。

看来,sysbench连接MySQL的字符集设置,应该默认是latin1,应该是这里的字符集设置不一致导致的。

BUT,对于技术问题,我不能光靠猜测啊!我一定要刨根问底,查它个水落石出……

源码:

吃CPU最多的是my_ismbchar_utf8mb4函数对吧?那就先到源码中搜它:

在strings/ctype-utf8.c 中定义的:

static uintmy_ismbchar_utf8mb4(const CHARSET_INFO *cs, const char *b, const char *e)

{int res= my_valid_mbcharlen_utf8mb4(cs, (const uchar*)b, (const uchar*)e);return (res > 1) ? res : 0;

}

它本身没有复杂的逻辑,只是调用了my_valid_mbcharlen_utf8mb4,然后对返回值res 进行判断,如果>1,就返回res,否则返回0。

行,那我再看看my_valid_mbcharlen_utf8mb4吧,

static intmy_valid_mbcharlen_utf8mb4(const CHARSET_INFO *cs __attribute__((unused)),const uchar *s, const uchar *e)

{

uchar c;if (s >=e)returnMY_CS_TOOSMALL;

c= s[0];if (c < 0xf0)returnmy_valid_mbcharlen_utf8mb3(s, e);if (c < 0xf5)

{if (s + 4 > e) /*We need 4 characters*/

returnMY_CS_TOOSMALL4;/*省略若干行……*/

if (!(IS_CONTINUATION_BYTE(s[1]) &&IS_CONTINUATION_BYTE(s[2]) &&IS_CONTINUATION_BYTE(s[3]) &&(c>= 0xf1 || s[1] >= 0x90) &&(c<= 0xf3 || s[1] <= 0x8F)))returnMY_CS_ILSEQ;return 4;

}returnMY_CS_ILSEQ;

}

这个函数对输入的字符进行比对,判断是utf8mb3还是utf8mb4。utf8mb3?以前没听说过啊!上知乎一搜,原来还有这么一段有趣的历史 ☜

不过,仅仅看这个函数的代码,是不会相信它居然会吃掉7%以上的CPU的。我也不信!

好吧,先做个perf record看看:

#第1步,查看mysqld进程的pidps -ef | grepmysqld

#第2步,将mysqld进程相关的cpu-clock事件及调用堆栈记录起来,默认保存在perf.data文件中

perf record-e cpu-clock -g -p 14345#第3步,用perf script工具对perf.data进行解析

perf script-i perf.data &>perf.unfold

#第4步,下载一个集漂亮、强大于一身的工具:

git clone https://github.com/brendangregg/FlameGraph.git

#第5步:将perf.unfold中的符号进行折叠

./FlameGraph/stackcollapse-perf.pl perf.unfold &>perf.folded

#第6步,生成火焰图

./FlameGraph/flamegraph.pl perf.folded > perf.svg

效果就是这样的↓  可以看出,my_ismbchar_utf8mb4占比确实最高,达到了7.47%

3a28f0b622e1d04bfe34647dd2366901.png

去跟踪调用堆栈,可以发现是在sql\sql_lex.cc中的get_text()函数中,调用了宏use_mb和my_ismbchar来检查字符集。

这2个宏同样都是调用ismbchar() - detects whether the given string is a multi-byte sequence。utf8mb4中的mb,全称就是multi-byte

static char *get_text(Lex_input_stream *lip, int pre_skip, intpost_skip)

{

uchar c,sep;uint found_escape=0;const CHARSET_INFO *cs= lip->m_thd->charset();

lip->tok_bitmap= 0;

sep= lip->yyGetLast(); //String should end with this

while (! lip->eof())

{

c= lip->yyGet();

lip->tok_bitmap|=c;

{intl;if (use_mb(cs) &&(l=my_ismbchar(cs,

lip->get_ptr() -1,

lip->get_end_of_query()))) {

lip->skip_binary(l-1);continue;

}

}if (c == '\\' &&

!(lip->m_thd->variables.sql_mode &MODE_NO_BACKSLASH_ESCAPES))

{//Escaped character

found_escape=1;if (lip->eof())return 0;

lip->yySkip();

}// 省略若干行……

}return 0; //unexpected end of query

}

解决方法:

上面说了一大通,可能有点云里雾里,抱歉哈,我能力有限,不能把它解释得更通俗一些。

简而言之,就是证明了确实是字符集不一致,导致MySQL在语法解析的时候,对每一个用户输入的字符(MySQL关键字除外),都要进行若干次字符集检查,所以才会发生my_ismbchar_utf8mb4吃掉很多CPU资源这样一个故事 。

要解决就很简单啦:保持character_set_server  &&  database characterset  &&  table characterset  &&  Client characterset一致!

我就是因为忽略了sysbench的字符集设置,所以才把自己给坑了。

既然sysbench没有提供字符集相关的选项和参数,那我就把MySQL的字符集统一成latin1来测吧(也可以去修改sysbench的mysql driver源码,让它支持设置字符集,但是我不擅长C……)

最后总结:

调整字符集之前,QPS最高只能压到73797,统一字符集之后,QPS达到了98272。73797/98272*100%=75.09%

7a52a93b0fd7d8731ebf636ee875f326.png

再来看看TPS,调整字符集之前,TPS最高只能压到3689,统一字符集之后,QPS达到了3689。  73797/4913*100%=75.08%

8fe07a5f7d78d78ca09b455abafc7f02.png

多么痛的领悟……

static char *get_text(Lex_input_stream *lip, int pre_skip, int post_skip){  uchar c,sep;  uint found_escape=0;  const CHARSET_INFO *cs= lip->m_thd->charset();

lip->tok_bitmap= 0;  sep= lip->yyGetLast();                        // String should end with this  while (! lip->eof())  {    c= lip->yyGet();    lip->tok_bitmap|= c;    {      int l;      if (use_mb(cs) &&          (l = my_ismbchar(cs,                           lip->get_ptr() -1,                           lip->get_end_of_query()))) {        lip->skip_binary(l-1);        continue;      }    }    if (c == '\\' &&        !(lip->m_thd->variables.sql_mode & MODE_NO_BACKSLASH_ESCAPES))    {// Escaped character      found_escape=1;      if (lip->eof())return 0;      lip->yySkip();    }    else if (c == sep)    {      if (c == lip->yyGet())            // Check if two separators in a row      {        found_escape=1;                 // duplicate. Remember for deletecontinue;      }      else        lip->yyUnget();

/* Found end. Unescape and return string */      const char *str, *end;      char *start;

str= lip->get_tok_start();      end= lip->get_ptr();      /* Extract the text from the token */      str += pre_skip;      end -= post_skip;      DBUG_ASSERT(end >= str);

if (!(start= static_cast(lip->m_thd->alloc((uint) (end-str)+1))))return (char*) "";// Sql_alloc has set error flag

lip->m_cpp_text_start= lip->get_cpp_tok_start() + pre_skip;      lip->m_cpp_text_end= lip->get_cpp_ptr() - post_skip;

if (!found_escape)      {lip->yytoklen=(uint) (end-str);memcpy(start,str,lip->yytoklen);start[lip->yytoklen]=0;      }      else      {        char *to;

for (to=start ; str != end ; str++){  int l;  if (use_mb(cs) &&              (l = my_ismbchar(cs, str, end))) {      while (l--)  *to++ = *str++;      str--;      continue;  }  if (!(lip->m_thd->variables.sql_mode & MODE_NO_BACKSLASH_ESCAPES) &&              *str == '\\' && str+1 != end)  {    switch(*++str) {    case 'n':      *to++='\n';      break;    case 't':      *to++= '\t';      break;    case 'r':      *to++ = '\r';      break;    case 'b':      *to++ = '\b';      break;    case '0':      *to++= 0;// Ascii null      break;    case 'Z':// ^Z must be escaped on Win32      *to++='\032';      break;    case '_':    case '%':      *to++= '\\';// remember prefix for wildcard      /* Fall through */    default:              *to++= *str;      break;    }  }  else if (*str == sep)    *to++= *str++;// Two ' or "  else    *to++ = *str;}*to=0;lip->yytoklen=(uint) (to-start);      }      return start;    }  }  return 0;// unexpected end of query}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值