Mysql监控组件mymon报错“NewMySQLConnection Error: Building mysql connection failed!: unexpected EOF”问题

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  #编译运行
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值