使用 row 格式 binlog 撤销操作

MySQL 在使用 row 格式,并使用默认的 binlog_row_image=full 的时候,binlog 中记录了完整的更新前后的数据镜像。因此,根据 row 格式 binlog 进行撤销操作是可行的。我就试着做了这么个工具

[GitHub 页面] [linux 二进制文件]

例如

./binlog_undo -f /data/mysql/log-bin.000004 -p 3958 -o binlog.out

会扫描 /data/mysql/log-bin.000004 从位置 3958 到末尾的所有事务,倒转其中所有事务顺序和每个事务中的语句顺序,并反向所有的操作,把 WRITE 和 DELETE 反转,把 UPDATE 的前后镜像互换,然后将生成后的 binlog 文件写入 binlog.out 中。之后就可以用 mysqlbinlog 工具来回放这个 binlog 来撤销操作了。

这个工具可以用于在主备切换后,撤销掉原主上未同步到备的操作,避免完全重建;也可以作为误操作的后悔药。

记一次 MySQL 循环复制

有朋友的一对 MySQL 出现远大于其他类似实例的大量更新。因为开启了主备双向复制和 log_slave_updates,所以首先猜测是不是有循环复制,主 stop slave 后暂时正常。看了下主上的 relay_log,验证了这一点。幸而来回复制的只是监控用的心跳更新,不会搞坏数据。

但是 MySQL 会比较 server_id ,一般情况下是不会出问题的。于是又仔细看了下主上不正常的 relay_log ,发现其中 event 的 server_id 和当前的都不同。 问下来确实曾经在线改过 server_id。

想了下怎么触发这个问题,在自己的实例上复现了一把

  1. 开启 主->备 的复制
  2. 主上插入若干记录
  3. 更改主的server_id
  4. 开启 备->主 的复制
  5. 轰!

实际操作中,由于存在复制延时,即使没有先停止备->的复制也有可能触发问题。

然后就是怎么拆掉这个雷了。由于来回复制的只是心跳更新,所以只需要跳过就行,其实如果不是的话也已经完蛋没法救了。首先想到的是 binlog_ignore_db 和 replicate_ignore_db 。但是这两个不是动态变量,重启一次服务代价太大。同事 @Tachikoma 提醒我 change master 还有个 ignore_server_ids 选项。于是就只需要 change master to ignore_server_ids (1,2) 忽略之前的 server_id 就行。待复制正常,change master to ignore_server_ids () 就可以解除掉了。

移植 spider 到 MySQL 5.6

MariaDB 中自带了很多 MySQL 中没有的插件。我对其中的 spider 存储引擎很有兴趣。这个引擎可以让 MySQL 作为一个 proxy ,来实现 sharding、高可用等功能。这些功能已经有一些产品实现了,比如 MaxScale、Cobar、OneProxy、Atlas。但是我觉着 spider 把自己作为一个存储引擎来实现这些功能是有其优势的。SQL 解析和查询优化是个非常复杂而且很难做好的工作。其他替代产品都是自己实现,由于复杂性,这些产品都带来了一下限制,没能支持全部常见的 SQL 语句,给使用和实施带来了困难。而作为一个存储引擎,这些工作都由 MySQL 自身完成了,后面的工作就会简单很多,想做点优化的话也会容易些。

由于 MariaDB 从 MySQL 5.5 时代就分道扬镳了,做过很多改动后,和目前版本的 MySQL 已经有了不小差异,所以插件基本上没法直接拿到 MySQL 里编译使用。我就花了点功夫,把 spider 引擎移植到了 MySQL 5.6。

https://github.com/xiezhenye/mysql-plugin-spider-engine

编译使用和一般的插件差不多

cp -r src /path/to/mysql-src/storage/spider
cd /path/to/mysql-src
cmake . -DBUILD_CONFIG=mysql_release -DCMAKE_INSTALL_PREFIX=<mysql install dir>
cd storage/spider
make
make install

之后,执行附带的 install_spider.sql 安装插件,创建所需要的系统表。

mysql ... < scripts/install_spider.sql

具体文档参见 https://mariadb.com/kb/en/mariadb/spider/

在测试过程中,发现安装插件以后,重启 MySQL 后会 crash,然后再也启动不了,移除 ha_spider.so 后才行。具体来说,是在配置为

log_bin=off
spider_support_xa=on
spider_internal_xa=off

的时候。然而在 MariaDB 中却是正常的。开始以为是自己移植过程带来的 bug,或者有什么兼容问题未解决。追踪到后来,发现这居然是 MySQL 自身的 bug。于是去提交了一下。http://bugs.mysql.com/bug.php?id=78050

在使用外部 XA 的时候,如果没启用 binlog,会把 XA 信息通过 TC_LOG_MMAP 来持久化。然后 bug 就出在了那里。

这个 bug 曾经在 2009 年就被发现过,2012 年被 fix 过。但是显然并没有改对。待我自己做了 fix 以后,进一步发现,这个 bug 在 MariaDB 中已经被修复过,然后发现原来在 MySQL 5.7 分支下也是修复过的,但是并没应用到 5.6 分支。

都是几个相当低级的 bug。有成员未初始化,指针计算时搞错了指针类型,未判断空指针…… 。虽然这个地方确实是一般使用很难碰到,但是这代码质量简直无语。

修复 disable myisam 插件的 crash 问题

之前写了一个 禁止创建 MyISAM 表的插件blog),有报 issue,在 MySQL 重启后 uninstall plugin 会引发 crash。

测试了一下果然可以复现。挂上 gdb 后,backstace 如下:

(gdb) bt
#0  0x00000000006f7658 in column_bitmaps_set (write_set_arg=0x189ad98, read_set_arg=0x189ad98, this=0x7fc3a800fa60)
    at /root/mysql-5.6.24-tp/sql/table.h:1228
#1  use_all_columns (this=0x7fc3a800fa60) at /root/mysql-5.6.24-tp/sql/table.h:1238
#2  mysql_uninstall_plugin (thd=thd@entry=0x1d1b030, name=0x1d1d858) at /root/mysql-5.6.24-tp/sql/sql_plugin.cc:2077
#3  0x00000000006e8daf in mysql_execute_command (thd=thd@entry=0x1d1b030) at /root/mysql-5.6.24-tp/sql/sql_parse.cc:4910
#4  0x00000000006ed9d8 in mysql_parse (thd=thd@entry=0x1d1b030, rawbuf=, length=, 
    parser_state=parser_state@entry=0x7fc3c61b02f0) at /root/mysql-5.6.24-tp/sql/sql_parse.cc:6391
#5  0x00000000006ef1cd in dispatch_command (command=COM_QUERY, thd=0x1d1b030, packet=, packet_length=)
    at /root/mysql-5.6.24-tp/sql/sql_parse.cc:1340
#6  0x00000000006f0f24 in do_command (thd=) at /root/mysql-5.6.24-tp/sql/sql_parse.cc:1037
#7  0x00000000006bd662 in do_handle_one_connection (thd_arg=thd_arg@entry=0x1d1b030) at /root/mysql-5.6.24-tp/sql/sql_connect.cc:982
#8  0x00000000006bd710 in handle_one_connection (arg=arg@entry=0x1d1b030) at /root/mysql-5.6.24-tp/sql/sql_connect.cc:898
#9  0x000000000095dea3 in pfs_spawn_thread (arg=0x1dbb410) at /root/mysql-5.6.24-tp/storage/perfschema/pfs.cc:1860
#10 0x00007fc3f7258182 in start_thread (arg=0x7fc3c61b1700) at pthread_create.c:312
#11 0x00007fc3f676530d in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:111

进一步追踪这个 column_bitmaps_set 的调用

  inline void column_bitmaps_set(MY_BITMAP *read_set_arg,
                                 MY_BITMAP *write_set_arg)
  {
    read_set= read_set_arg;
    write_set= write_set_arg;
    if (file && created)
      file->column_bitmaps_signal();
  }

发现是在 file->column_bitmaps_signal 这一步出错,反汇编以后,也确实是在 callq 指令处。注意到反汇编后,gdb 在 callq 指令所调用函数的名字没有能显示出来,说明这个地址很可能已经无效。这里 file 成员变量即是插件生成的替换了 create 操作的 ha_mysiam 的 wrapper。打印其内容发现 _vptr.handler = 0x7fc3d44768d0 ,这里也未能指出指向的类名。

阅读了 sql_handler.cc 中 mysql_uninstall_plugin 的代码后,明白了原因。卸载插件会删除 mysql.plugin 表中的相应记录。在 mysql_uninstall_plugin 中,先打开了 mysql.plugin 表,然后卸载插件,然后删除相应记录。问题就出在 plugin 表也是一个 MyISAM 表。如果打开的时候插件是启用的,那么其对应的 handler 是插件中的 wrapper。而插件卸载以后,对应地址的代码已经不复存在,因此就造成了 crash。那么既然是这个原因,又想到了,如果在插件加载期间打开过表,卸载插件后访问这些表应该也会出错。试了一下果然如此,一阵冷汗。

接着就是考虑如何解决这个问题。既然插件卸载后无法内存,那就需要在卸载时将原有的指向 wrapper 的 handler 都换回原有的 ha_myisam。handler 本身的内存是由 mysql 管理的,并不会因为插件卸载而释放。所以可以修改。wrapper 是 ha_myisam 的派生类,而且没有增加成员,因此内存布局和 ha_myisam 是完全相同的。唯一差异就在虚函数表。C++ 似乎没有这种强制转换到父类的语法。于是就考虑直接替换虚函数表指针。而 C++ 虚函数指针中总是对象最前面。于是直接建一个真的 ha_myisam 对象,然后把它的虚函数指针复制回去。试下来果然可以。接着就是在 wrapper 的构造和析构函数中维持一个当前现有 handler 的集合,用于在卸载时处理即可。

尝试让 cgo 调用绕过线程池

go 语言中,可以通过 cgo 来调用 C 库。但是由于 goroutine 的机制,外部的 C 函数调用可能能够很快返回,也可能执行很长时间。为了 goroutine 调度不被阻塞,就一律对每个 cgo 调用都从线程池中取一个线程来执行,完成后再返回原 goroutine。这样一来,每个 cgo 调用都带来了巨大的额外开销。所以 go 的很多库在实现时,都没有通过包装 C 库,而是选择完全用 go 来实现。这就使得 go 少了大量现有的 C 库可以利用。

读过 go 的代码后发现,要让 cgo 调用不通过线程池调用并不算麻烦,所以就自己修改了一下 cgo 命令。如下面的代码中

package cgo

//
// int add(int a, int b) {
//   int ret = a + b;
//   return ret;
// }
//
import "C"

func CAdd(a, b int) int {
  return int(C.add(C.int(a), C.int(b)))
}

func AsmCAdd(a, b int) int {
  return int(c.add(C.int(a), C.int(b)))
}

C.add 是传统的 cgo 调用方式。c.add 则是修改后不经过线程池的方式。两者可以并存,程序员可以自己判断 C 函数的执行时间,来考虑使用哪种方式。

性能测试代码:

import "testing"

func BenchmarkNormal(b *testing.B) {
  for i:= 0; i < b.N; i++ {
    CAdd(i, i);
  }
}

func BenchmarkDirect(b *testing.B) {
  for i:= 0; i < b.N; i++ {
    AsmCAdd(i, i);
  }
}

测试结果:

testing: warning: no tests to run
PASS
BenchmarkNormal	5000000	      307 ns/op
BenchmarkDirect	50000000	       31.0 ns/op
ok  	cgo	3.437s

可以看出,直接调用的性能大约是传统调用方式的 10 倍。

虽然目前的实现不算很漂亮,但是这玩意给了 go 更强的能力。而且只修改了 cgo 工具,并没有影响 go runtime 和标准库,不破坏兼容性。

代码在此,基于 go 1.4 修改。

https://github.com/xiezhenye/go/tree/directly-cgo/src/cmd/cgo

但是进一步实验发现这样还是有问题。

首先无法通过正常的方式调用 export 出来的 go 函数。这是因为正常的 cgo 调用会先 entersyscall,完成后 exitsyscall。在回调 go 时,会先 exitsyscall,然后 reentersyscall。既然之前跳过了那一步,这里自然就会出错。这点要解决就得改 runtime 了。

然后是个更要命的问题。这种方式下运行的 C 函数是运行在 goroutine 的栈里的。而 goroutine 的栈是由 go runtime 管理的。初始很小,按需扩大。但是 runtime 不知道 C 函数的情况,无法为其扩栈。如果 C 函数使用的栈空间超过了 goroutine 剩余的栈空间可能破坏其他 goroutine 的栈。这一点就基本不可能解决了。因为即使在 cgo 调用前预先扩栈,也不知道究竟需要扩多少才够,也无法对其进行保护,避免越界。

获取 MySQL 崩溃时的 core file

对于一般进程,要让进程崩溃时能生成 core file 用于调试,只需要设置 rlimit 的 core file size > 0 即可。比如,用在 ulimit -c unlimited 时启动程序。

对 MySQL 来说,由于 core file 中会包含表空间的数据,所以默认情况下为了安全,mysqld 捕获了 SEGV 等信号,崩溃时并不会生成 core file,需要在 my.cnf 或启动参数中加上 core-file。

但是即使做到了以上两点,在 mysqld crash 时还是可能无法 core dump。还有一些系统参数会影响 core dump。以下脚本可供参考:

echo 2 >/proc/sys/fs/suid_dumpable
chmod 0777 /var/crash
echo /var/crash/core> /proc/sys/kernel/core_pattern
echo 1 >/proc/sys/kernel/core_uses_pid

由于 mysql 通常会以 suid 方式启动,所以需要打开 suid_dumpable 。对于 core_pattern,最好指定一个保证可写的绝对路径。

之后,就可以用 kill -SEGV 让 mysqld 崩溃,测试一下能不能正常产生 core file 了。

找到 mysql 数据库中的不良索引

为了演示,首先建两个包含不良索引的表,并弄点数据。

mysql> show create table test1\G
*************************** 1. row ***************************
       Table: test1
Create Table: CREATE TABLE `test1` (
  `id` int(11) NOT NULL,
  `f1` int(11) DEFAULT NULL,
  `f2` int(11) DEFAULT NULL,
  `f3` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `k1` (`f1`,`id`),
  KEY `k2` (`id`,`f1`),
  KEY `k3` (`f1`),
  KEY `k4` (`f1`,`f3`),
  KEY `k5` (`f1`,`f3`,`f2`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
1 row in set (0.00 sec)

mysql> show create table test2\G
*************************** 1. row ***************************
       Table: test2
Create Table: CREATE TABLE `test2` (
  `id1` int(11) NOT NULL DEFAULT '0',
  `id2` int(11) NOT NULL DEFAULT '0',
  `b` int(11) DEFAULT NULL,
  PRIMARY KEY (`id1`,`id2`),
  KEY `k1` (`b`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
1 row in set (0.00 sec)

mysql> select count(*) from test2 group by b;                                                                                                        
+----------+
| count(*) |
+----------+
|       32 |
|       17 |
+----------+
2 rows in set (0.00 sec)

1. 包含主键的索引
innodb 本身是聚簇表,每个二级索引本身就包含主键,类似 f1, id 的索引实际虽然没有害处,但反映了使用者对 mysql 索引不了解。而类似 id, f1 的是多余索引,会浪费存储空间,并影响数据更新性能。包含主键的索引用这样一句 sql 就能全部找出来。

mysql> select c.*, pk from 
    ->   (select table_schema, table_name, index_name, concat('|', group_concat(column_name order by seq_in_index separator '|'), '|') cols 
    ->     from INFORMATION_SCHEMA.STATISTICS 
    ->     where index_name != 'PRIMARY' and table_schema != 'mysql'
    -> group by table_schema, table_name, index_name) c,
    ->   (select table_schema, table_name, concat('|', group_concat(column_name order by seq_in_index separator '|'), '|') pk 
    ->     from INFORMATION_SCHEMA.STATISTICS 
    ->     where index_name = 'PRIMARY' and table_schema != 'mysql'
    -> group by table_schema, table_name) p  
    -> where c.table_name = p.table_name and c.table_schema = p.table_schema and c.cols like concat('%', pk, '%');
+--------------+------------+------------+---------+------+
| table_schema | table_name | index_name | cols    | pk   |
+--------------+------------+------------+---------+------+
| test         | test1      | k1         | |f1|id| | |id| |
| test         | test1      | k2         | |id|f1| | |id| |
+--------------+------------+------------+---------+------+
2 rows in set (0.04 sec)

2. 重复索引前缀
包含重复前缀的索引,索引能由另一个包含该前缀的索引完全代替,是多余索引。多余的索引会浪费存储空间,并影响数据更新性能。这样的索引同样用一句 sql 可以找出来。

mysql> select c1.table_schema, c1.table_name, c1.index_name,c1.cols,c2.index_name, c2.cols from
    ->   (select table_schema, table_name, index_name, concat('|', group_concat(column_name order by seq_in_index separator '|'), '|') cols 
    ->     from INFORMATION_SCHEMA.STATISTICS 
    ->     where table_schema != 'mysql' and index_name!='PRIMARY'
    -> group by table_schema,table_name,index_name) c1,   
    ->   (select table_schema, table_name,index_name, concat('|', group_concat(column_name order by seq_in_index separator '|'), '|') cols 
    ->     from INFORMATION_SCHEMA.STATISTICS 
    ->     where table_schema != 'mysql' and index_name != 'PRIMARY'
    -> group by table_schema, table_name, index_name) c2 
    -> where c1.table_name = c2.table_name and c1.table_schema = c2.table_schema and c1.cols like concat(c2.cols, '%') and c1.index_name != c2.index_name;
+--------------+------------+------------+------------+------------+---------+
| table_schema | table_name | index_name | cols       | index_name | cols    |
+--------------+------------+------------+------------+------------+---------+
| test         | test1      | k1         | |f1|id|    | k3         | |f1|    |
| test         | test1      | k4         | |f1|f3|    | k3         | |f1|    |
| test         | test1      | k5         | |f1|f3|f2| | k3         | |f1|    |
| test         | test1      | k5         | |f1|f3|f2| | k4         | |f1|f3| |
+--------------+------------+------------+------------+------------+---------+
4 rows in set (0.02 sec)

3. 低区分度索引
这样的索引由于仍然会扫描大量记录,在实际查询时通常会被忽略。但是在某些情况下仍然是有用的。因此需要根据实际情况进一步分析。这里是区分度小于 10% 的索引,可以根据需要调整参数。

mysql> select p.table_schema, p.table_name, c.index_name, c.car, p.car total from
    ->   (select table_schema, table_name, index_name, max(cardinality) car
    ->     from INFORMATION_SCHEMA.STATISTICS
    -> where index_name != 'PRIMARY'
    -> group by table_schema, table_name,index_name) c,
    ->   (select table_schema, table_name, max(cardinality) car
    ->     from INFORMATION_SCHEMA.STATISTICS
    -> where index_name = 'PRIMARY' and table_schema != 'mysql'
    -> group by table_schema,table_name) p
    -> where c.table_name = p.table_name and c.table_schema = p.table_schema and p.car > 0 and c.car / p.car < 0.1;
+--------------+------------+------------+------+-------+
| table_schema | table_name | index_name | car  | total |
+--------------+------------+------------+------+-------+
| test         | test2      | k1         |    4 |    49 |
+--------------+------------+------------+------+-------+
1 row in set (0.04 sec)

4. 复合主键
由于 innodb 是聚簇表,每个二级索引都会包含主键值。复合主键会造成二级索引庞大,而影响二级索引查询性能,并影响更新性能。同样需要根据实际情况进一步分析。

mysql> select table_schema, table_name, group_concat(column_name order by seq_in_index separator ',') cols, max(seq_in_index) len
    ->    from INFORMATION_SCHEMA.STATISTICS
    ->    where index_name = 'PRIMARY' and table_schema != 'mysql'
    ->    group by table_schema, table_name having len>1;
+--------------+------------+-----------------------------------+------+
| table_schema | table_name | cols                              | len  |
+--------------+------------+-----------------------------------+------+
| test         | test2      | id1,id2                           |    2 |
+--------------+------------+-----------------------------------+------+
1 rows in set (0.01 sec)

自定义 Pacemaker OCF 资源

Pacemaker / Corosync 是 Linux 下一组常用的高可用集群系统。Pacemaker 本身已经自带了很多常用应用的管理功能。但是如果要使用 Pacemaker 来管理自己实现的服务或是一些别的没现成的东西可用的服务时,就需要自己实现一个资源了。

Pacemaker 的资源主要有两类,即 LSB 和 OCF。其中 LSB 即 Linux 标准服务,通常就是 /etc/init.d 目录下那些脚本。Pacemaker 可以用这些脚本来启停服务。在 crm ra list lsb 中可以看到。另一类 OCF 实际上是对 LSB 服务的扩展,增加了一些高可用集群管理的功能如故障监控等和更多的元信息。可以通过 crm ra list ocf 看到当前支持的资源。要让 pacemaker 可以很好的对服务进行高可用保障就得实现一个 OCF 资源。

Pacemaker 自带的资源管理程序都在 /usr/lib/ocf/resource.d 下。其中的 heartbeat 目录中就包含了那些自带的常用服务。那些服务的脚本可以作为我们自己实现时候的参考。

每个 OCF 资源是一个可执行文件。通过命令行参数和环境变量来接受来自 Pacemaker 的输入。下面是一个简单的例子,创建了一个名叫 test 的资源。crm ra meta test的结果如下:

crm(live)# ra meta test
ocf:heartbeat:test

A test resource

Parameters (* denotes required, [] the default):

service_path* (string): the path of service script
    This is a required parameter. The path of service script.

probe_url* (string): the url to detect the status of the service
    This is a required parameter. The url to detect the status of the service.

Operations' defaults (advisory minimum):

    start         timeout=60s
    stop          timeout=30s
    status        timeout=30s
    monitor_0     interval=10s timeout=30s

和其他的资源一样,这个 test 资源也会接受若干的参数,以及对 start、stop 等命令有超时默认值等。下面就是这个资源的 bash 实现代码。实际也可以使用任何语言来实现。

#!/bin/bash
# OCF parameters:

#   OCF_RESKEY_service_path    : service path
#   OCF_RESKEY_probe_url    : probe url
: ${OCF_FUNCTIONS_DIR=${OCF_ROOT}/resource.d/heartbeat}
. ${OCF_FUNCTIONS_DIR}/.ocf-shellfuncs

SERVICE_PATH="${OCF_RESKEY_service_path}"
PROBE_URL="${OCF_RESKEY_probe_url}"
COMMAND=$1
SERVICE_NAME=test

metadata_test() {
    cat <<END
<?xml version="1.0"?>
<!DOCTYPE resource-agent SYSTEM "ra-api-1.dtd">
<resource-agent name="test">
<version>1.0</version>
<longdesc lang="en">
A test resource
</longdesc>
<shortdesc lang="en">test</shortdesc>
<parameters>
    <parameter name="service_path" required="1" unique="1">
        <longdesc lang="en">
        This is a required parameter. The path of service script.
        </longdesc>
        <shortdesc>the path of service script</shortdesc>
        <content type="string" default=""/>
    </parameter>
    <parameter name="probe_url" required="1" unique="1">
        <longdesc lang="en">
        This is a required parameter. The url to detect the status of the service.
        </longdesc>
        <shortdesc>the url to detect the status of the service</shortdesc>
        <content type="string" default=""/>
    </parameter>
</parameters>
<actions>
    <action name="start" timeout="60s" />
    <action name="stop" timeout="30s" />
    <action name="status" timeout="30s" />
    <action name="monitor" depth="0" timeout="30s" interval="10s" />
    <action name="meta-data" timeout="5s" />
    <action name="validate-all"  timeout="5s" />
</actions>
</resource-agent>
END
    return $OCF_SUCCESS
}

monitor_test() {
    return $OCF_SUCCESS
}

start_test() {
    return $OCF_SUCCESS
}

stop_test() {
    return $OCF_SUCCESS
}

status_test() {
    return $OCF_SUCCESS
}

validate_all_test() {
    ocf_log info "validate_all_test"
    if [[ ! -x "$SERVICE_PATH" ]]; then
        ocf_log err "service_path is not found"
        exit $OCF_ERR_CONFIGURED
    fi
    if [[ ! -x "$PROBE_URL" ]]; then
        ocf_log err "probe_url is required"
        exit $OCF_ERR_CONFIGURED
    fi
    return $OCF_SUCCESS
}

run_func() {
    local cmd="$1"
    ocf_log debug "Enter $SERVICE_NAME $cmd"
    $cmd
    local status=$?
    ocf_log debug  "Leave $SERVICE_NAME $cmd $status"
    exit $status
}

case "$COMMAND" in
    meta-data)
        run_func meta-data
        ;;
    start)
        run_func start
        ;;
    stop)
        run_func stop
        ;;
    status)
        run_func status
        ;;
    monitor)
        run_func monitor
        ;;
    validate-all)
        run_func vaidate-all
        ;;
    *)
        exit $OCF_ERR_ARGS
        ;;
esac

首先说说如何接受输入。同标准的 Linux 服务脚本一样,OCF 资源也会以命令名为参数被调用,只是多了几个命令而已。如通过 /usr/lib/ocf/resource.d/heartbeat/test start 来启动服务,/usr/lib/ocf/resource.d/heartbeat/test monitor 来监控服务状态。根据执行的返回码来判断是否成功。具体的返回值含义如下:

OCF_SUCCESS=0
OCF_ERR_GENERIC=1
OCF_ERR_ARGS=2
OCF_ERR_UNIMPLEMENTED=3
OCF_ERR_PERM=4
OCF_ERR_INSTALLED=5
OCF_ERR_CONFIGURED=6
OCF_NOT_RUNNING=7

对于 start、stop 等命令,一定要在服务启动/停止完成后返回。要保证在返回后,通过 monitor 命令检测状态时要能够成功。如果在未完成启动就返回如直接将命令放后台执行,会导致通过 monitor 命令检测状态时,由于此时还未启动完成而失败,导致被认为是故障,从而导致重启或切换。所有命令执行成功时应当返回 OCF_SUCCESS ,即 0。出错时根据具体情况返回对应错误码。如 start 启动失败,monitor 监控到服务故障时应当返回 OCF_NOT_RUNNING 。在检查参数错误时应当返回 OCF_ERR_CONFIGURED。其他错误一般可以返回 OCF_ERR_GENERIC。

参数则是通过环境变量传递。如 test 资源定义的两个参数:service_path 和 probe_url 会分别通过环境变量 $OCF_RESKEY_service_path 和 $OCF_RESKEY_service_path 传递。

meta-data 命令会输出一段 xml。作为资源的元信息,crm ra meta test 的结果也是由此而来的。参见代码中那一大段 xml。每个资源的说明,参数定义,命令定义都由这个 xml 说明。基本上参照例子就能明白。

定义参数的 parameter/content 支持的类型有 string、integer、boolean。default 属性可以定义参数的默认值。需要注意的是,定义资源未指定参数时,指定的默认值并不会出现在 OCF_RESKEY_<参数名> 变量中,而是会放在另外的变量 OCF_RESKEY_<参数名>_default 中。

参考资料:

  • http://www.linux-ha.org/doc/dev-guides/ra-dev-guide.html
  • http://www.linux-ha.org/wiki/LSB_Resource_Agents
  • http://www.linux-ha.org/wiki/OCF_Resource_Agents
  • http://refspecs.linux-foundation.org/LSB_3.2.0/LSB-Core-generic/LSB-Core-generic/iniscrptact.html
  • http://www.opencf.org/

不使用 expect 实现自动化 ssh 密码认证

一般来说,自动化通过 ssh 执行操作或者通过 scp 传文件首先得过 ssh 认证这一关。采用公钥认证是最方便安全的方式。但是有时候不得不使用密码认证。而 ssh 默认是直接读写终端来输出提示信息和读入密码的,所以没法直接用 echo password | ssh ... 的方式来认证。expect 是最常用的用于解决这类问题的工具。但是这玩意实在是很不好用,也不能保证一定安装过。

好在 ssh 还是开了一扇窗,让我们可以实现这点。ssh 有个环境变量,SSH_ASKPASS ,设置了这个环境变量,并且当前会话不是终端时,ssh 在认证时会启动这个程序,从这个程序的标准输出来读取密码。这个功能本来是用于图形终端的,所以还要设置另一个环境变量 DISPLAY=’none:0’,让 ssh 不要试图访问 X11 。至于让进程脱离终端,使用 setsid 就可以了。下面这个例子就展示了自动化实现密码认证并执行命令。

echo  'echo BEGIN!; ls /' | setsid env SSH_ASKPASS='/root/pswd.sh' DISPLAY='none:0' ssh root@127.0.0.1 2>&1
Pseudo-terminal will not be allocated because stdin is not a terminal.
BEGIN!
bin
boot
data
dev
etc
home
lib
lib64
lost+found
media
mnt
opt
proc
root
sbin
selinux
srv
sys
tmp
usr
var

例子里的 /root/pswd.sh 只需要简单输出密码,并确保当前用户可执行就可以了。比如

#!/bin/bash
echo 'PASSWORD'