HandlerSocket是通过socket和innodb通信的,但是网络通信如果双方连接没有按照规则正常关闭,可能导致wati_close状态的TCP连接大量存在,占用大量端口资源,需要慎重处理。

最近碰到这样的一个案例,检查一个服务器上的php fastcgi的连接情况,发现一些异常:

[root@mail ~]# netstat -anpo | grep php-fpm|wc -l
157

[root@banping~]# netstat -anpo | grep php-fpm|wc -l

517

而实际是没有这么多连接的,进一步检查详细信息,发现很多wati_close状态的TCP连接,占用了大量端口,而服务器端对应的是HandlerSocket插件的9998端口,初步判断是socket连接没有正常关闭导致的问题:

[root@banping ~]# netstat -anpo | grep php-fpm

tcp        1      0 10.0.0.1:43635            10.0.0.2:9998               CLOSE_WAIT  3592/php-fpm: pool  off (0.00/0/0)

tcp        1      0 10.0.0.1:43699            10.0.0.2:9998               CLOSE_WAIT  3592/php-fpm: pool  off (0.00/0/0)

tcp        1      0 10.0.0.1:43335            10.0.0.2:9998               CLOSE_WAIT  3592/php-fpm: pool  off (0.00/0/0)

tcp        1      0 10.0.0.1:43306            10.0.0.2:9998               CLOSE_WAIT  2894/php-fpm: pool  off (0.00/0/0)

......

但是很难定位在客户端的哪个具体操作会导致这个问题,或许是在有一定负载压力的情况下才会出来这种死连接。

估计的原因有几种可能,一是HandlerSocket自身代码的问题,二是PHP客户端的实现问题,这个客户端并没有显示的断开socket连接的接口提供,三是自身代码问题或压力负载导致的。

不好定位问题,可以先通过Linux的keepalive参数来控制TCP的超时来释放CLOSE_WAIT的连接:

[root@banping ~]# vi /etc/sysctl.conf

net.ipv4.tcp_keepalive_time = 300

net.ipv4.tcp_keepalive_probes = 2

net.ipv4.tcp_keepalive_intvl = 2

[root@banping ~]# sysctl -p

把超时时间设为300秒,这个默认是7200秒,把tcp_keepalive_intvl(keepalive探测包的发送间隔)和tcp_keepalive_probes (如果对方不予应答,探测包的发送次数)都设为2秒,这样就能很快的释放连接。

默认参数可以这样查看:

[root@banping ~]# cat /proc/sys/net/ipv4/tcp_keepalive_time

7200

[root@banping ~]# cat /proc/sys/net/ipv4/tcp_keepalive_probes

9

[root@banping ~]# cat /proc/sys/net/ipv4/tcp_keepalive_intvl

75

然后过一段时间我们再观察CLOSE_WAIT连接数:

[root@banping ~]# netstat -anpo | grep php-fpm|grep CLOSE_WAIT|grep 9998|wc -l

203

这没有解决程序上的本质问题,需要继续观察评估效果。

 
本文开头提到的问题就是在这个锁表的过程中不成功,造成等待而最终超时退出。
今天凌晨2点50分,一台MySQL服务器的备份遭遇了一个错误,信息如下:
innobackupex: Error: Connection to mysql child process (pid=20080) timedout. (Time limit of 900 seconds exceeded. You may adjust time limit by editing the value of parameter "$mysql_response_timeout" in this script.) while waiting for reply to MySQL request: 'FLUSH TABLES WITH READ LOCK;' at /home/mysql/backup/xtrabackup-1.6/innobackupex line 336.
这是使用XtraBackup容易引起的一个经典错误,引起这个错误的原因也很简单,就是XtraBackup在备完innodb表的时候,要获得对所有表的锁定来备份MyISAM等其他表,来保证数据的一致性,如果数据库不断的有DML操作,XtraBackup就可能迟迟无法获得对所有表的锁定,最后超时。
让我们先了解一下XtraBackup备份MySQL的原理和过程。

前几天一台MySQL服务器的备份遭遇了一个错误,信息如下:

innobackupex: Error: Connection to mysql child process (pid=20080) timedout. (Time limit of 900 seconds exceeded. You may adjust time limit by editing the value of parameter "$mysql_response_timeout" in this script.) while waiting for reply to MySQL request: 'FLUSH TABLES WITH READ LOCK;' at /home/mysql/backup/xtrabackup-1.6/innobackupex line 336.

这是使用XtraBackup容易引起的一个经典错误,引起这个错误的原因也很简单,就是XtraBackup在备完innodb表的时候,要获得对所有表的锁定来备份MyISAM表和其他文件,来保证数据的一致性,如果在此过程中数据库不断的有DML操作,XtraBackup就可能迟迟无法获得对所有表的锁定,最后超时。

要避免这个问题,一个是加大超时的时间限制,但这显然不是一个好的解决办法,另外就是在备份的时候加上--no-lock参数,就是在复制MyISAM表和.frm等文件的时候不锁表,但要保证这时没有对MyISAM表的DML操作和Innodb表的DDL操作,这一般通过应用端权衡时间是容易办到的。

但是要注意的是,如果备份出来的文件要用于主从复制,那么不锁表会导致没有输出 slave_info文件和binlog_info文件,这对于一些人的应用场景可能会有影响。这时可以加一个--safe-slave-backup参数,使得在从库上备份的时候停止SQL THRED,这样即使从库能从主库接收binlog文件,但是不会应用,relay log position就不会移动了。但是单纯的停掉slave SQL Thread是不会影响binlog position的。

因为临时表的原因,--safe-slave-backup需要在SHOW STATUS的输出中slave_open_temp_tables为0的时候才停止slave SQL Thread,否则会导致复制出问题。因为临时表是基于用户session的,因此如果正在操作临时表的数据的时候停止slave SQL Thread,会导致可能后续的数据不一致,在其他停用slave SQL Thread的场景也要注意这个问题。

 

XtraBackup是percona公司提供的开源工具,以热备Innodb表著称而被广泛采用。

XtraBackup对Innodb的备份之所以是热备,无需锁表,是基于Innodb自身的崩溃恢复机制,它首先复制所有的Innodb数据文件,这样复制出来的文件肯定是不一致的,然后对每个文件进行崩溃恢复处理,最终达到一致。就和MySQL在启动Innodb的时候一样,会通过比较数据文件头和redo log文件头信息来检查数据是否是一致的,如果不一致就尝试通过前滚(把redo log中所有提交的事务写入数据文件)和回滚(从数据文件中撤销所有redo log中未提交的事务引起的修改)来使数据达到最终一致。

This works because InnoDB maintains a redo log, also called the transaction log. This contains a record of every change to InnoDB's data. When InnoDB starts, it inspects the data files and the transaction log, and performs two steps. It applies committed transaction log entries to the data files, and it performs an undo operation on any transactions that modified data but did not commit.
XtraBackup works by remembering the log sequence number (LSN) when it starts, and then copying away the data files. It takes some time to do this, so if the files are changing, then they reflect the state of the database at different points in time. At the same time, XtraBackup runs a background process that watches the transaction log files, and copies changes from it. XtraBackup needs to do this continually because the transaction logs are written in a round-robin fashion, and can be reused after a while. XtraBackup needs the transaction log records for every change to the data files since it began execution.
The above is the backup process. Next is the prepare process. During this step, XtraBackup performs crash recovery against the copied data files, using the copied transaction log file. After this is done, the database is ready to restore and use.
The above process is implemented in the xtrabackup compiled binary program. The innobackupex program adds more convenience and functionality by also permitting you to back up MyISAM tables and .frm files. It starts xtrabackup, waits until it finishes copying files, and then issues FLUSH TABLES WITH READ LOCK to prevent further changes to MySQL's data and flush all MyISAM tables to disk. It holds this lock, copies the MyISAM files, and then releases the lock.
The backed-up MyISAM and InnoDB tables will eventually be consistent with each other, because after the prepare (recovery) process, InnoDB's data is rolled forward to the point at which the backup completed, not rolled back to the point at which it started. This point in time matches where the FLUSH TABLES WITH READ LOCK was taken, so the MyISAM data and the prepared InnoDB data are in sync.
The xtrabackup and innobackupex tools both offer many features not mentioned in the preceding explanation. Each tool's functionality is explained in more detail on its manual page. In brief, though, the tools permit you to do operations such as streaming and incremental backups with various combinations of copying the data files, copying the log files, and applying the logs to the data.

XtraBackup在启动的时候会记录一个LSN(log sequence number),然后就把所有的Innodb数据文件复制出来,这样复制出来的数据文件是不一致的,但是XtraBackup会在后台运行一个进程把所有对redo log file的修改记录下来,只要有了这个数据,就能进行崩溃恢复。只所以要额外记录下来,是因为MySQL自身的redo log file是可重用的。

以上的操作是由xtrabackup二进制程序(比如xtrabackup_55)完成的,如果使用innobackupex 脚本,刚才的步骤完成以后,innobackupex就会去备份MyISAM表和.frm文件,这时要保证数据的一致性就会先锁表了,通过FLUSH TABLES WITH READ LOCK命令锁表然后把文件复制出来,再释放掉这个锁。

在恢复数据的时候,要经过prepare(recovery)和restore两个步骤。在prepare结束以后,Innodb的表恢复到了复制Innodb文件结束的时间点,这个时间点也就是锁表复制MyISAM表的起点,所以最终数据是一致的。一般我们在恢复的时候执行两次prepare,是因为第二次prepare会帮助我们生成redo log文件,从而加快MySQL数据库启动的速度。

我们再来看一下实际备份的日志来理解这个过程:

......

110701 03:29:13  innobackupex: Starting ibbackup with command: xtrabackup_55  --defaults-file="/home/mysql/3306/my.cnf" --backup --suspend-at-end --log-stream --target-dir=./

innobackupex: Waiting for ibbackup (pid=22334) to suspend

innobackupex: Suspend file '/home/mysql/3306/data/xtrabackup_suspended'

xtrabackup: suspend-at-end is enabled.

xtrabackup: uses posix_fadvise().

xtrabackup: cd to /home/mysql/3306/data

xtrabackup: Target instance is assumed as followings.

xtrabackup:   innodb_data_home_dir = /home/mysql/3306/data

xtrabackup:   innodb_data_file_path = ibdata1:512M:autoextend

xtrabackup:   innodb_log_group_home_dir = /home/mysql/3306/redolog

xtrabackup:   innodb_log_files_in_group = 3

xtrabackup:   innodb_log_file_size = 134217728

110701  3:29:13 InnoDB: Using Linux native AIO

110701  3:29:13  InnoDB: Warning: allocated tablespace 268, old maximum was 0

xtrabackup: Stream mode.

>> log scanned up to (2371741708)

110701 03:29:15  innobackupex: Continuing after ibbackup has suspended

innobackupex: Starting to backup InnoDB tables and indexes

innobackupex: from original InnoDB data directory '/home/mysql/3306/data'

innobackupex: Backing up as tar stream 'ibdata1'

>> log scanned up to (2371741708)

>> log scanned up to (2371742105)

>> log scanned up to (2371742105)

innobackupex: Backing up file '/home/mysql/3306/data/test/t.ibd'

>> log scanned up to (2371742115)

innobackupex: Backing up files '/home/mysql/3306/data/banping/*.ibd' (16 files)

......

110701 03:29:35  innobackupex: Connected to database with mysql child process (pid=22630)

>> log scanned up to (2371742526)

110701 03:29:39  innobackupex: Starting to lock all tables...

>> log scanned up to (2371742526)

>> log scanned up to (2371742526)

110701 03:29:51  innobackupex: All tables locked and flushed to disk

110701 03:29:51  innobackupex: Starting to backup .frm, .MRG, .MYD, .MYI,

innobackupex: .TRG, .TRN, .ARM, .ARZ, .CSM, .CSV and .opt files in

innobackupex: subdirectories of '/home/mysql/3306/data'

innobackupex: Backing up files '/home/mysql/3306/data/banping/*.{frm,MYD,MYI,MRG,TRG,TRN,ARM,ARZ,CSM,CSV,opt,par}' (17 files)

innobackupex: Backing up file '/home/mysql/3306/data/test/t.frm'

......

110701 03:29:53  innobackupex: Finished backing up .frm, .MRG, .MYD, .MYI, .TRG, .TRN, .ARM, .ARZ, .CSV, .CSM and .opt files

innobackupex: Resuming ibbackup

xtrabackup: The latest check point (for incremental): '2371742526'

>> log scanned up to (2371742526)

xtrabackup: Transaction log of lsn (2371741708) to (2371742526) was copied.

110701 03:29:55  innobackupex: All tables unlocked

110701 03:29:55  innobackupex: Connection to database server closed

innobackupex: Backup created in directory '/home/mysql/backup/data/3306'

innobackupex: MySQL binlog position: filename 'bin.000014', position 309836330 mysql,information_schema,performance_schema

innobackupex: MySQL slave binlog position: master host '', filename '', position

innobackupex: You must use -i (--ignore-zeros) option for extraction of the tar stream.

110701 03:29:55  innobackupex: completed OK!

 

前几天网上一个朋友反映碰到了网站性能的问题:

怎么看LINUX服务器是不是假死啊 我的网站老是出现可以PING 也可以登录SSH 就是网站不能访问啊 重启了 又可以了

free

从这个free的输出看内存有很多空闲,我请他输出vmstat 5的结果如下所示:

vmstat

从这个输出可见内存使用较少,CPU有一定的使用,但并不高,IO有等待,初步判断瓶颈在IO上,后来登录到服务器查看,主要发现了三个问题,一是MySQL都用默认的参数在跑,内存给的是8M,有30多个schema。二是apache连接数配置的太少,很容易就溢出,三是文件系统有问题,需要用fsck来修复,不排除是磁盘故障。

于是我做了以下动作:

一是调整MySQL的配置,由于他的大部分表是MyISAM引擎,部分memory引擎,加大key_buffer_size到1G,毫无疑问这是MyISAM引擎最重要的参数,如果只能调整一个参数,那么就要增大这个值来给MySQL使用更多的内存而不是空闲在那里。另外我调整了两个参数如下:

#add by banping

max_connections = 1000

key_buffer_size = 1G

myisam_sort_buffer_size = 128M

实际上这个myisam_sort_buffer_size参数意义不大,这是个字面上蒙人的参数,它用于ALTER TABLE, OPTIMIZE TABLE, REPAIR TABLE 等命令时需要的内存。如果真的要提高排序用的内存,是要调整sort_buffer_size参数,但这个参数是为每个session分配的,要慎用。

二是增加了apache连接数的大小,通过ps -ef|grep httpd|wc -l命令查看,挂死的时候连接数达到了上限,它是使用prefork模式的,我修改了配置文件:

[root@centos conf]# vi extra/httpd-mpm.conf

<IfModule mpm_prefork_module>

StartServers          5

MinSpareServers       5

MaxSpareServers      10

ServerLimit         1000

MaxClients          350

MaxRequestsPerChild   0

</IfModule>

通过top命令可见一些httpd进程占用17M左右的内存,按照平均10M计算,那么350*10就是3.4G的内存,而服务器的内存配置是4G。可以估算他这个服务器目前最多也就能撑500个以下的连接,这时还要减少分配给MySQL的内存。

三是文件系统的问题,发现有些文件虽然是rw的属性,但是操作的时候是read only的,通过以下命令发现了一堆错误:

[root@centos ~]# dmesg |grep error

EXT3-fs error (device sdb1): ext3_lookup: unlinked inode 110350817 in dir #84690004

EXT3-fs error (device sdb1): ext3_journal_start_sb: Detected aborted journal

EXT3-fs error (device sdb1): ext3_lookup: unlinked inode 97047037 in dir #84689192

EXT3-fs error (device sdb1): ext3_lookup: unlinked inode 95801904 in dir #84689116

EXT3-fs error (device sdb1): ext3_lookup: unlinked inode 101257606 in dir #84689449

EXT3-fs error (device sdb1): ext3_lookup: unlinked inode 110563813 in dir #84690017

检查fstab设置是没问题的:
[root@centos ~]# cat /etc/fstab
LABEL=/                 /                       ext3    defaults        1 1
LABEL=/home             /home                   ext3    defaults        1 2
LABEL=/var              /var                    ext3    defaults        1 2
LABEL=/boot             /boot                   ext3    defaults        1 2
/dev/sdb1               /web                    ext3    defaults        1 2
tmpfs                   /dev/shm                tmpfs   defaults        0 0
devpts                  /dev/pts                devpts  gid=5,mode=620  0 0
sysfs                   /sys                    sysfs   defaults        0 0
proc                    /proc                   proc    defaults        0 0
LABEL=SWAP-sda5         swap                    swap    defaults        0 0
你这台服务器不只是web server吧 是不是数据库也在,有没有很多写入?
新叶子-陈志鑫(77008008) 10:12:19
有啊 好像有很多写入 我就是搞不懂啊
半瓶(9155091) 10:12:48
估计你的瓶颈在

这时可做的操作是先umount文件系统后执行fsck检查和修复再mount,再有问题可能就是磁盘的硬件故障了,但是考虑到风险我并没有做这个操作,而且要做必须先要停掉应用,否则是无法umount的。

之后的观察了几天发现在apache连接数超过256的时候仍然会挂掉,这时我怀疑是ServerLimit参数没生效,这个ServerLimit参数对应的是src/include/httpd.h文件中的#define HARD_SERVER_LIMIT 256这个变量值,这个值会限制连接数的大小,而不管你的MaxClients参数大小是多少。我修改了apache的配置后是restart而不是stop再start的,估计这样的操作由问题。

后来caoz知道了这个问题,他说apache连接溢出有多种可能,上去看了这台服务器,找到了连接数增长的原因是因为默认的KeepAlive On和Timeout 300导致连接没有及时的释放。改变这两个参数的值,连接数的压力就下来了。高人啊,一下就找到了本质的原因。然后他通过fsck修复了文件系统的问题。

通过这个事例我们可以发现很多事情的产生必然有背后的原因,做决定要有充足的依据做支撑,解决是表象背后的本质问题才是真正的解决问题。

 

HandlerSocket Plugin for MySQL的协议提供了比较丰富的功能,但是第三方开发者提供的客户端参差不齐,文档也大多不完善,所以应用起来还是有些累。

简单分析下HandlerSocket的协议:

1、OpenIndex

<indexid> <dbname> <tablename> <indexname> <columns> [<fcolumns>]

<indexid>编号(后续操作都会用到)

<dbname> 数据库名

<tablename> 表名

<indexname>索引名

<columns>需要返回结果的列名列表

[<fcolumns>]可选,用于过滤条件的列名列表,这个f是filter的意思

2、get data

<indexid> <op> <vlen> <v1> ... <vn> [LIM] [IN] [FILTER ...]

<indexid> opened index id

<op> 操作符 '=', '>', '>=', '<', and '<='

<vlen> 参数中提供的索引包含的列的个数

<v1> ... <vn>索引中包含的列,这个参数可以比实际索引中的列少

[LIM] <limit> <offset>

[IN] <icol> <ivlen> <iv1> ... <ivn> 列名,长度,值

[FILTER ...] <ftyp> <fop> <fcol> <fval> 过滤类型F或W(条件或循环),操作符,列名,值。

3、Updating/Deleting data

<indexid> <op> <vlen> <v1> ... <vn> [LIM] [IN] [FILTER ...] MOD

MOD <mop> <m1> ... <mk>

<mop> is 'U' (update), '+' (increment), '-' (decrement), 'D' (delete),

4、Inserting data

<indexid> + <vlen> <v1> ... <vn>

5、Authentication  这个是后来加上的,以前的旧版本没有权限控制

A <atyp> <akey>

详情参见官方文档:https://github.com/ahiguti/HandlerSocket-Plugin-for-MySQL/blob/master/docs-en/protocol.en.txt

对于PHP客户端,有个开源的项目在以下地址:http://code.google.com/p/php-handlersocket/

$hs = new HandlerSocket($hs_host, $hs_port);
$dbname = 'rtindex';
$table = 'friendfeed';
if (!($hs->openIndex(23, $dbname, $table, 'user_id', 'post_id','post_id')))
{
echo $hs->getError(), PHP_EOL;
die();
}
if ($maxid==0){
$value = $hs->executeSingle(23, '=', array($user_id), 9999999, 0);
}else{
//int $limit, int $skip, strint $modop, array $values, array $filters, int $invalues_key, array $invalues ] )
$value = $hs->executeSingle(23, '=', array($user_id), 9999999, 0,null,null,array(array('F','<',0,$maxid)));

常见的调用方法如下:

1、新建一个对象

$hs = new HandlerSocket($hs_host, $hs_port);

2、打开索引

$hs->openIndex(23, $dbname, $table, 'index_id', 'return_column','filter_column');

3、简单查询

$value = $hs->executeSingle(23, '=', array($user_id), 9999999, 0);

4、复杂查询,含过滤条件

$value = $hs->executeSingle(23, '=', array($user_id), 9999999, 0,null,null,array(array('F','<',0,$maxid)));

以上是php-handlersocket 0.1.0版本实现的接口,0.2.0增加了IN过滤方式。

HandlerSocket查询速度还是很快的,而且比传统的NoSQL通过key get data的方式有更丰富的过滤条件。唯一的遗憾是没有提供排序功能。排序就要占用CPU资源,这对于HandlerSocket的设计初衷是违背的,所以也无可厚非。