mymon是Open-falcon的用来监控Mysql的组件,今天使用起来遇到了一个问题,数据库明明正确配置,但是启动的时候总是报“NewMySQLConnection Error: Building mysql connection failed!: unexpected EOF”的错误
系统Centos6.10,Mysql5.1.73,中间都是我调试的过程记录,如果想看解决方案直接去第5节
1 粗略分析
定位一下代码,发现Mysql的连接是common/mysql.go这个文件负责的,看一下代码如下
我开始以为是conf.DataBase这几个配置项出了问题,直接替换成了文本,仍然报错
mymon调用的是一个叫mymysql的mysql驱动,无奈,只好开始排查mymysql的问题
决定抓包分析之,看一下mymysql和Mysql之间的TCP连接
2 报文分析
用Linux自带工具tcpdump,在mysql端抓取,-w表示保存为文件,网卡根据自己的情况选择
tcpdump -w tcp.pcap -i [网卡] port 3306
登陆完mysql,把文件弄回windows用wireshark打开
2.1 正常连接的TCP报文
登录Mysql,密码正确登陆成功,mysql的机器ip是56.24,登陆的机子是56.21
sudo tcpdump -w correct.pcap -i eth1 port 3306 #在mysql的机器上
mysql -u root -p #用另一台虚拟机,远程登录mysql
wireshark打开
2.2 mymon的报文
sudo tcpdump -w mymon.pcap -i lo port 3306 #在mysql的机器上
./mymon etc/myMon.cfg #启动mymon
同样wireshark打开
2.3 分析
两个报文一对比很明显了,上面是mymon下面是正常的,可以看到在数据库发送了版本号等信息后,mymon没有给数据库发送登陆请求的报文,然后等待了10秒之后数据库断开了连接
3 mymysql源码分析
分析源代码定位到native/init.go文件夹下的auth()函数,这个函数是负责发送登陆用户名和密码的,代码如下
func (my *Conn) auth() {
if my.Debug {
log.Printf("[%2d <-] Authentication packet", my.seq)
}
flags := uint32(
_CLIENT_PROTOCOL_41 |
_CLIENT_LONG_PASSWORD |
_CLIENT_LONG_FLAG |
_CLIENT_TRANSACTIONS |
_CLIENT_SECURE_CONN |
_CLIENT_LOCAL_FILES |
_CLIENT_MULTI_STATEMENTS |
_CLIENT_MULTI_RESULTS)
// Reset flags not supported by server
flags &= uint32(my.info.caps) | 0xffff0000
if my.plugin != string(my.info.plugin) {
my.plugin = string(my.info.plugin)
}
var scrPasswd []byte
switch my.plugin {
case "caching_sha2_password":
flags |= _CLIENT_PLUGIN_AUTH
scrPasswd = encryptedSHA256Passwd(my.passwd, my.info.scramble[:])
case "mysql_old_password":
my.oldPasswd()
return
default:
// mysql_native_password by default
scrPasswd = encryptedPasswd(my.passwd, my.info.scramble[:])
}
// encode length of the auth plugin data
var authRespLEIBuf [9]byte
authRespLEI := appendLengthEncodedInteger(authRespLEIBuf[:0], uint64(len(scrPasswd)))
if len(authRespLEI) > 1 {
// if the length can not be written in 1 byte, it must be written as a
// length encoded integer
flags |= _CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA
}
pay_len := 4 + 4 + 1 + 23 + len(my.user) + 1 + len(authRespLEI) + len(scrPasswd) + 21 + 1
if len(my.dbname) > 0 {
pay_len += len(my.dbname) + 1
flags |= _CLIENT_CONNECT_WITH_DB
}
pw := my.newPktWriter(pay_len)
pw.writeU32(flags)
pw.writeU32(uint32(my.max_pkt_size))
pw.writeByte(my.info.lang) // Charset number
pw.writeZeros(23) // Filler
pw.writeNTB([]byte(my.user)) // Username
pw.writeBin(scrPasswd) // Encrypted password
// write database name
if len(my.dbname) > 0 {
pw.writeNTB([]byte(my.dbname))
}
// write plugin name
if my.plugin != "" {
pw.writeNTB([]byte(my.plugin))
}
return
}
也就是说经过重重包装,pw就是往TCP连接的缓冲区写数据的一个对象,但是数据为什么没有发送出去呢
我修改了代码,在auth()函数return前强行调用pw.wr.Flush()刷新缓冲区,抓包发现,一个长度128的包发出来了,但是并没有被识别为一个Mysql连接
把这个包的头部和正确的包对比
查阅资料,得知Protocols in frame封装于物理层
又查了一下,说这个Protocols in frame是wireshark自己识别出来的一个东西,不是数据包里面带的,嗯
继续详细对比两个数据包
红框框出来的这一位,可以看到是表示数据的长度的,右边正确数据包的数据长度为3a(十六进制,就是58),左边的长度不对,是80,所以这就是数据包没有发送的原因么,长度设置得过大,数据没有填满所以一直没有发送
看一下这段代码,pay_len经过很长的算式计算出来,在我的账号密码情况下这个pay_len是80
而仔细看正确的数据包可以发现,数据包最后一个部分就是加密后的密码,那么后面的21+1是个啥呢,看代码,我没有填dbname和my.plugin,那么在密码之后写的数据应该就是"mysql_native_password"了吧,
pay_len := 4 + 4 + 1 + 23 + len(my.user) + 1 + len(authRespLEI) + len(scrPasswd) + 21 + 1
if len(my.dbname) > 0 {
pay_len += len(my.dbname) + 1
flags |= _CLIENT_CONNECT_WITH_DB
}
pw := my.newPktWriter(pay_len)
pw.writeU32(flags)
pw.writeU32(uint32(my.max_pkt_size))
pw.writeByte(my.info.lang) // Charset number
pw.writeZeros(23) // Filler
pw.writeNTB([]byte(my.user)) // Username
pw.writeBin(scrPasswd) // Encrypted password
// write database name
if len(my.dbname) > 0 {
pw.writeNTB([]byte(my.dbname))
}
// write plugin name
if my.plugin != "" {
pw.writeNTB([]byte(my.plugin))
}
把my.plugin这个变量相关的代码抽出来,发现了问题,my.info这个变量保存的是握手之后Mysql传回的数据,在我的电脑上,Mysql并没有传回plugin相关的信息,导致my.plugin被抹成了空字符串
if my.plugin != string(my.info.plugin) {
my.plugin = string(my.info.plugin)
}
这时候我发现了一个问题,我一直看的是mymon/vendor文件夹底下的mymysql代码,mymon本身使用的也是这个代码,但是我从github上下载的mymysql代码有所不同,没有使用my.info.plugin,发送plugin的代码改成了如下的一部分
https://github.com/ziutek/mymysql
if my.plugin != "" {
pw.writeNTB([]byte(my.plugin))
} else {
pw.writeNTB([]byte("mysql_native_password"))
}
我把vendor底下的代码改了一下,重新运行,已经可以连接了,但是报了其他的错,也就是说这是mymon/vendor文件夹下的mymysql版本太低,和我的Mysql不适配造成的
我决定用github上的mymysql替换一下,
cd ~
sudo rm -r $GOPATH/src/github.com/open-falcon/mymon/vendor/github.com/ziutek/mymysql/native $GOPATH/src/github.com/open-falcon/mymon/vendor/github.com/ziutek/mymysql/mysql
git clone git clone https://github.com/ziutek/mymysql.git
cp -r mymysql/native $GOPATH/src/github.com/open-falcon/mymon/vendor/github.com/ziutek/mymysql/native
cp -r mymysql/mysql $GOPATH/src/github.com/open-falcon/mymon/vendor/github.com/ziutek/mymysql/mysql
cd $GOPATH/src/github.com/open-falcon/mymon
make && ./mymon etc/myMon.cfg #编译运行
错误依旧,好吧,起码数据库是连上了
4 吾生有崖而bug无崖
这个错误发生在mymon/show.go的182行,newMetaData.SetValue(row.Int(0))这个调用上,ziutek报错,相关代码如下,根据报错,主要是Row.Int(0)这个出了问题,ziutek表示Row这个对象保存Mysql返回的结果,估计是结果返回出错
var row mysql.Row
newMetaData := NewMetric(conf, metric)
row, _, err = db.QueryFirst("SELECT /*!50504 @@GLOBAL.innodb_stats_on_metadata */;")
newMetaData.SetValue(row.Int(0))
这个select语句肥肠奇怪,我从来没见过,进入Mysql执行一下这个select,Mysql直接给我报了个语法错误
我仔细看了看/* */,越看越觉得像一对注释,又看了看其他语句,原来如此啊
给他改掉
var row mysql.Row
newMetaData := NewMetric(conf, metric)
row, _, err = db.QueryFirst("SELECT /*!50504 */@@GLOBAL.innodb_stats_on_metadata;")
newMetaData.SetValue(row.Int(0))
运行,成功了,没有报错
了吗?
看一眼myMon.log
????
这个错误是在show.go的ShowBinaryLogs(()函数执行中产生的,我的Mysql没有启用binlog,所以返回了,这里把直接返回改成一个Warning
binaryLogStatus, err := ShowBinaryLogs(conf, db)
if err != nil {
Log.Warning("Binary Log is not used")
} else {
data = append(data, binaryLogStatus...)
}
好了,去客户端看一眼myMon回报的数据,mysql_alive_local/isSlave=0,port=3306,readOnly=0,type=mysql有了一个数据1,凑合凑合就这样了
5 解决方案总结
5.1 unexpected EOF
碰到了这种错误“NewMySQLConnection Error: Building mysql connection failed!: unexpected EOF”
这个错误我将近研究了一周的时间,终于摸索出了一些,主要是mymon这个版本使用的Mysql驱动和我的Mysql不匹配,替换为最新版本的mymysql可以运行
cd ~
sudo rm -r $GOPATH/src/github.com/open-falcon/mymon/vendor/github.com/ziutek/mymysql/native $GOPATH/src/github.com/open-falcon/mymon/vendor/github.com/ziutek/mymysql/mysql
git clone git clone https://github.com/ziutek/mymysql.git
cp -r mymysql/native $GOPATH/src/github.com/open-falcon/mymon/vendor/github.com/ziutek/mymysql/native
cp -r mymysql/mysql $GOPATH/src/github.com/open-falcon/mymon/vendor/github.com/ziutek/mymysql/mysqlcd $GOPATH/src/github.com/open-falcon/mymon
5.2 index out of range
碰到这个“panic: runtime error: index out of range”
把show.go的181行修改一下
#修改前
row, _, err = db.QueryFirst("SELECT /*!50504 @@GLOBAL.innodb_stats_on_metadata */;")
#修改后
row, _, err = db.QueryFirst("SELECT /*!50504 */ @@GLOBAL.innodb_stats_on_metadata;")
编译运行
make && ./mymon etc/myMon.cfg #编译运行
5.3 You are not using binary logging
看一眼myMon.log,如果出现“You are not using binary logging”,说明你的Mysql没有开binlog
修改一下show.go的82-84行,这里我在日志里打了一个Warning输出,你把整个binlog的代码注释掉也可以
#修改前
if err != nil {
return []*MetaData{binlogFileCounts, binlogFileSize}, err
}
#修改后
if err != nil {
Log.Warning("Binary Log is not used")
} else {
data = append(data, binaryLogStatus...)
}
编译运行
make && ./mymon etc/myMon.cfg #编译运行