GA

2014/01/24

groonga-index-dumpで遊んでみる

これがやりたくてtweets.csvをMroongaに食わせていた

$ time /usr/ruby/2.0.0/bin/groonga-index-dump /usr/mysql/5.6.15/data/mroonga.mrn

real    0m33.430s
user    0m29.649s
sys     0m2.924s

$ ll
合計 16
drwxr-xr-x 2 root     root     4096  7月  3 19:14 2013 hsperfdata_root
drwxr-xr-x 2 yoku0825 yoku0825 4096 11月  6 11:49 2013 hsperfdata_ttanaka
drwxrwxr-x 3 yoku0825 yoku0825 4096  1月 24 12:04 2014 index-dump
drwx------ 2 yoku0825 tmux    4096  1月 10 15:19 2014 tmux-3012

--output-directoryで指定しない場合はカレントディレクトリの下にindex-dumpディレクトリを掘る。

$ cd index-dump/

$ ll
合計 2012
drwxrwxr-x 2 yoku0825 yoku0825 2056192  1月 24 12:04 2014 tweets-text.index

$ cd tweets-text.index/

$ ll | wc
  47997  479962 3723665

index-dumpの下に<テーブル名>-<インデックス名>.indexディレクトリが作られるぽい。
ls -l叩くと泣きたくなるくらいにファイルの数が多い。

$ ll | head
合計 201992
-rw-rw-r-- 1 yoku0825 yoku0825    222  1月 24 12:04 2014 %21%21%21%21%21%21%21%21%21%21%21%21%21%21.dump
-rw-rw-r-- 1 yoku0825 yoku0825    218  1月 24 12:04 2014 %21%21%21%21%21%21%21%E3%80%8D.dump
-rw-rw-r-- 1 yoku0825 yoku0825    256  1月 24 12:04 2014 %21%21%21%21%21%21%21.dump
-rw-rw-r-- 1 yoku0825 yoku0825    337  1月 24 12:04 2014 %21%21%21%21%21%21.dump
-rw-rw-r-- 1 yoku0825 yoku0825    216  1月 24 12:04 2014 %21%21%21%21%21%E3%80%8D.dump
-rw-rw-r-- 1 yoku0825 yoku0825    417  1月 24 12:04 2014 %21%21%21%21%21.dump
-rw-rw-r-- 1 yoku0825 yoku0825    218  1月 24 12:04 2014 %21%21%21%21%E3%80%8D%E3%80%8C.dump
-rw-rw-r-- 1 yoku0825 yoku0825    417  1月 24 12:04 2014 %21%21%21%21.dump
-rw-rw-r-- 1 yoku0825 yoku0825    212  1月 24 12:04 2014 %21%21%21%28.dump

なんかざっと見ただけで、1つのトークンに対して1つの<トークン>.dumpファイルが出来上がっている予感。

$ cat %21%21%21%21%21%21%21%21%21%21%21%21%21%21.dump
index: tweets-text.index        term: <!!!!!!!!!!!!!!>  domain: tweets-text     range: tweets   have_section: false     have_weight: false    have_position: true
  weight        position        term_frequency  record
  0     74      1       tweets[398641294714499072].text

$ cat %E3%81%84%E3%81%BC.dump
index: tweets-text.index        term: <いぼ>    domain: tweets-text     range: tweets   have_section: false     have_weight: false    have_position: true
  weight        position        term_frequency  record
  0     40      1       tweets[198244626002481152].text

やりたいことは myisam_ftdump 的なことなので、この.dumpファイルをテキトーにパースしてやらないといけないっぽい。

取りあえず、投げやりなPerlワンライナーで。

$ cat `ls | head -1000` | perl -nle 'if (/\sterm:\s<(.+)>\s/) {printf("%d\t%s\n", $count, $term); $term= $1; $count= 0;} elsif (/\s([0-9]+)\s+tweets\[[0-9]+\]\.text$/) {$count+= $1;}' | sort -nr | head
12305   、
11013   。
9295    @
7042    /
4599    :
3583    。。
3479    #
2577    ://
2561    (
2243    「

cat *でやろうとしたらなんか怒られた(´・ω・`)

$ cat * | perl -nle 'if (/\sterm:\s<(.+)>\s/) {printf("%d\t%s\n", $count, $term); $
term= $1; $count= 0;} elsif (/\s([0-9]+)\s+tweets\[[0-9]+\]\.text$/) {$count+= $1;}' | sort -nr | head
cat: オプションが違います -- '%'
詳しくは `cat --help' を実行して下さい.

これで索引の中の無駄なトークンをあぶりだしてメンテしてやるのがゴールのつもり。


【2014/01/24 13:46】
catがダメなのは"-"で始まるファイルっぽい。

$ for f in *; do cat $f > /dev/null || (echo $f; break); done
cat: オプションが違います -- '%'
詳しくは `cat --help' を実行して下さい.
-%23.dump
cat: オプションが違います -- '%'
詳しくは `cat --help' を実行して下さい.
-%25%27.dump
cat: オプションが違います -- '%'
詳しくは `cat --help' を実行して下さい.
-%26.dump
..

cat ./*にしたら食えた。

$ cat ./* | perl -nle 'if (/\sterm:\s<(.+)>\s/) {printf("%d\t%s\n", $count, $term); $term= $1; $count= 0;} elsif (/\s([0-9]+)\s+tweets\[[0-9]+\]\.text$/) {$count+= $1;}' | sort -nr | head
12305   、
11013   。
10217   _
9295    @
7042    /
6456    って
4599    :
4514    mysql
4147    ない
3801    5

うん、mysql多いなオイ。

全文検索のテスト用にtweets.csvを食わせるSQL

毎回手打ちするのが面倒なのでメモ。

mysql> CREATE TABLE tweets (tweet_id bigint unsigned primary key, timestamp timestamp NOT NULL, text text NOT NULL, FULLTEXT KEY(text)) Engine= mroonga;
Query OK, 0 rows affected (0.05 sec)

mysql> LOAD DATA INFILE '/tmp/tweets.csv' INTO TABLE tweets FIELDS TERMINATED BY ',' ENCLOSED BY '"' IGNORE 1 ROWS (tweet_id, @dummy, @dummy, @timestamp, @dummy, text, @dummy) SET timestamp= DATE_ADD(@timestamp, INTERVAL 9 HOUR);
Query OK, 14410 rows affected, 28821 warnings (0.35 sec)
Records: 14410  Deleted: 0  Skipped: 0  Warnings: 28821

CSVのtext以降の部分をTRUNCATEしたってワーニングがごちゃっと出る。
timestampは"2013-12-17 03:06:59 +0000"(=UTC)の形で入っていて、SET TIME_ZONEで頑張ってみたけど上手く行かなかったのでSETで無理矢理9時間足してみた。

2014/01/23

Galera Arbitratorというもの(garbd)

Galera Cluster(俺が実際に色々やってるのはPercona XtraDB Cluster)はスプリットブレイン対策としてQuorum方式を採用している。クラスターを構成するノードの *過半数* とコミュニケーションが取れていない場合、自身がネットワークから切り離されているとみなして *全ての操作を* 拒否するようになる。Galera Clusterはそもそも全てのノードのデータが「仮想完全同期」されているという前提でマルチマスターのトポロジーを提供しているため、コミュニケーションパスが途切れて同期できていないかもしれない状態は、マルチマスターを提供するための下地が崩れるからこうなっている。

mysql> use test
ERROR 1047 (08S01): Unknown command

mysql> SELECT * FROM d1.t1;
ERROR 1047 (08S01): Unknown command

出力はこんな感じで、何をしてもError: 1047のUnknown commandが返ってくるようになる。流石にmysqlコマンドラインクライアントだけで完結するコマンド(helpとかcharsetとかteeとか)は実行できるけど、サーバー側から情報を引っ張って何かするコマンド(statusとuseくらい?)は実行できなくなる。

で、コミュニケーションパスが生きている必要があるノードはクラスターを構成するノードの *過半数* なので、2台構成にしてしまうと1台お亡くなりになった時に 残った1台> 2* 50% が成り立たないため、生き残った1台はこの状態に突入する。これを避けるために、Galera Clusterは最低3台での構成を推奨 している。

SET GLOBAL wsrep_provider_options= 'pc.ignore_sb= ON'; という手もあるけれど(名前の通り、スプリットブレイン状態になっててもコマンドを拒否しなくなる)、本当にネットワークがおかしくなった時に何が起こるかわからない(2つのmysqldそれぞれに矛盾する更新がかかったら、とか、更新が伝達されずにレコードを読んじゃったら、とか)ので、あまりやりたくない。かといって物理サーバー3台…うーん、微妙。

そんなわたしに贈る、Galera Arbitrator。プロセス名はgarbd。

$ ll /usr/local/mysql5534_pxc/bin/garbd
-rwxr-xr-x 1 root root 18015250 12月  2 21:20 /usr/local/mysql5534_pxc/bin/garbd

Percona XtraDB Cluster 5.5.34の.tar.gzのバイナリーを解凍した版にはbinの下に入ってた。ソースからコンパイルした人にはいない。どこから手に入れるのかもよく判らないけど、ソースからコンパイルした人、libの下にlibgalera_smm.soもなくて別途codershipから取ってきて入れないといけないから、そこから入れるのかな。PXCなら.tar.gz版がオールインワンで扱いやすいと思う余談。MariaDB Galera Clusterは(入ってるのか入ってないのか)知らない。

$ /usr/local/mysql5534_pxc/bin/garbd --help

Usage: /usr/local/mysql5534_pxc/bin/garbd [options] [group address]

Configuration:
  -d [ --daemon ]       Become daemon
  -a [ --address ] arg  Group address
  -g [ --group ] arg    Group name
  --sst arg             SST request string
  --donor arg           SST donor name
  -o [ --options ] arg  GCS/GCOMM option list
  -l [ --log ] arg      Log file
  -c [ --cfg ] arg      Configuration file


Other options:
  -v [ --version ]      Print version
  -h [ --help ]         Show help message

FATAL: Exit

ざっくりと、必要最低限っぽいオプションは-a(PXCのwsrep_cluster_addressに相当)と-g(PXCのwsrep_cluster_nameに相当)かしら。

$ /usr/local/mysql5534_pxc/bin/garbd -a "gcomm://xxx.xxx.xxx.xxx:13333" -g "cluster_name" -o "gmcast.listen_addr= tcp://0.0.0.0:13334"
2014-01-23 12:13:20.944  INFO: Read config:
        daemon:  0
        address: gcomm://xxx.xxx.xxx.xxx:13333
        group:   cluster_name
        sst:     trivial
        donor:
        options: gmcast.listen_addr= tcp://0.0.0.0:13334; gcs.fc_limit=9999999; gcs.fc_factor=1.0; gcs.fc_master_slave=yes
        cfg:
        log:

2014-01-23 12:13:20.952  INFO: protonet asio version 0
2014-01-23 12:13:20.953  INFO: backend: asio
2014-01-23 12:13:20.967  INFO: GMCast version 0
2014-01-23 12:13:20.972  INFO: (50019916-83dc-11e3-a310-bfb42459a728, 'tcp://0.0.0.0:13334') listening at tcp://0.0.0.0:13334
2014-01-23 12:13:20.972  INFO: (50019916-83dc-11e3-a310-bfb42459a728, 'tcp://0.0.0.0:13334') multicast: , ttl: 1
2014-01-23 12:13:20.992  INFO: EVS version 0
2014-01-23 12:13:21.000  INFO: PC version 0
2014-01-23 12:13:21.000  INFO: gcomm: connecting to group 'cluster_name', peer 'xxx.xxx.xxx.xxx:13333'
2014-01-23 12:13:21.007  INFO: (50019916-83dc-11e3-a310-bfb42459a728, 'tcp://0.0.0.0:13334') turning message relay requesting on, nonlive peers: tcp://xxx.xxx.xxx.xxx:13332
2014-01-23 12:13:21.489  INFO: (50019916-83dc-11e3-a310-bfb42459a728, 'tcp://0.0.0.0:13334') turning message relay requesting off
2014-01-23 12:13:21.505  INFO: declaring 1663d468-79c2-11e3-a5e5-2f016f2db075 stable
2014-01-23 12:13:21.505  INFO: declaring bc9b8196-83d6-11e3-b4d0-1a4766eec11e stable
2014-01-23 12:13:21.506  INFO: Node 1663d468-79c2-11e3-a5e5-2f016f2db075 state prim
2014-01-23 12:13:21.506  INFO: view(view_id(PRIM,1663d468-79c2-11e3-a5e5-2f016f2db075,43) memb {
        1663d468-79c2-11e3-a5e5-2f016f2db075,
        50019916-83dc-11e3-a310-bfb42459a728,
        bc9b8196-83d6-11e3-b4d0-1a4766eec11e,
} joined {
} left {
} partitioned {
})
2014-01-23 12:13:22.005  INFO: gcomm: connected
2014-01-23 12:13:22.005  INFO: Changing maximum packet size to 64500, resulting msg size: 32636
2014-01-23 12:13:22.005  INFO: Shifting CLOSED -> OPEN (TO: 0)
2014-01-23 12:13:22.005  INFO: Opened channel 'cluster_name'
2014-01-23 12:13:22.006  INFO: New COMPONENT: primary = yes, bootstrap = no, my_idx = 1, memb_num = 3
2014-01-23 12:13:22.006  INFO: STATE EXCHANGE: Waiting for state UUID.
2014-01-23 12:13:22.006  INFO: STATE EXCHANGE: sent state msg: 5055781a-83dc-11e3-b9e1-e2687a7bb5a6
2014-01-23 12:13:22.006  INFO: STATE EXCHANGE: got state msg: 5055781a-83dc-11e3-b9e1-e2687a7bb5a6 from 0 (xxxx_3333)
2014-01-23 12:13:22.006  INFO: STATE EXCHANGE: got state msg: 5055781a-83dc-11e3-b9e1-e2687a7bb5a6 from 2 (xxxx_3332)
2014-01-23 12:13:22.007  INFO: STATE EXCHANGE: got state msg: 5055781a-83dc-11e3-b9e1-e2687a7bb5a6 from 1 (garb)
2014-01-23 12:13:22.007  INFO: Quorum results:
        version    = 2,
        component  = PRIMARY,
        conf_id    = 33,
        members    = 2/3 (joined/total),
        act_id     = 29266,
        last_appl. = -1,
        protocols  = 0/4/2 (gcs/repl/appl),
        group UUID = 64863b36-62f7-11e3-bef2-a746941be182
2014-01-23 12:13:22.007  INFO: Flow-control interval: [9999999, 9999999]
2014-01-23 12:13:22.007  INFO: Shifting OPEN -> PRIMARY (TO: 29266)
2014-01-23 12:13:22.007  INFO: Sending state transfer request: 'trivial', size: 7
2014-01-23 12:13:22.007  INFO: Node 1 (garb) requested state transfer from '*any*'. Selected 0 (xxxx_3333)(SYNCED) as donor.
2014-01-23 12:13:22.007  INFO: Shifting PRIMARY -> JOINER (TO: 29266)
2014-01-23 12:13:22.008  INFO: 0 (xxxx_3333): State transfer to 1 (garb) complete.
2014-01-23 12:13:22.008  INFO: 1 (garb): State transfer from 0 (xxxx_3333) complete.
2014-01-23 12:13:22.008  INFO: Shifting JOINER -> JOINED (TO: 29266)
2014-01-23 12:13:22.008  INFO: Member 0 (xxxx_3333) synced with group.
2014-01-23 12:13:22.008  INFO: Member 1 (garb) synced with group.
2014-01-23 12:13:22.008  INFO: Shifting JOINED -> SYNCED (TO: 29266)

"gmcast.listen_addr= tcp://0.0.0.0:13334"は、このノード既にPXCが3インスタンスいるのでポートがカブらないように調整しているだけで、フツーにPXCのmysqldを起動させた時と同じように、コミュニケーションパスを使って自分のステータスをSYNCEDまで持って行ってる。SST(=フル同期)はしない。

他のノードからSHOW GLOBAL STATUS LIKE 'wsrep_cluster_size'で見ると、確かに増えている。これでmysqldが3つなくても、1台落ちただけなら過半数を満たせる数が確保できるという算段。APサーバーの先頭か何かにgarbdだけ入れておけばね。


……MHA-Managerみたい。。

2014/01/22

Percona XtraDB Cluster設定中に気付いたwsrep_certify_nonPK

Primary Keyが無いテーブル + Galera Clusterで悲惨な目に なんていうことが昔ありました。

PXC(wsrepの要求なので、Gelara Cluster for MySQL, MariaDB Galera Cluster も同じ)はbinlog_format= ROW が必須パラメーターになっているのでPrimary Keyが無いと悲惨な目に遭うんですが、これを強制的に禁止するオプション(wsrep_certify_nonPK)を発見。

mysql> SELECT @@wsrep_certify_nonPK;
+-----------------------+
| @@wsrep_certify_nonPK |
+-----------------------+
|                     1 |
+-----------------------+
1 row in set (0.00 sec)

デフォルトはON。

mysql> CREATE TABLE d1.t1 ( num INT, val varchar(32) ) Engine= InnoDB;
Query OK, 0 rows affected (0.02 sec)

mysql> INSERT INTO d1.t1 VALUES (1, 'one');
Query OK, 1 row affected (0.00 sec)

mysql> UPDATE d1.t1 SET val= 'eins' WHERE num= 1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> SELECT * FROM d1.t1;
+------+------+
| num  | val  |
+------+------+
|    1 | eins |
+------+------+
1 row in set (0.00 sec)

mysql> DELETE FROM d1.t1 WHERE num= 1;
Query OK, 1 row affected (0.00 sec)

Primary Keyが無くてもINSERTもUPDATEもDELETEもできる。フツーの動作。

mysql> SET GLOBAL wsrep_certify_nonPK= OFF;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT @@wsrep_certify_nonPK;
+-----------------------+
| @@wsrep_certify_nonPK |
+-----------------------+
|                     0 |
+-----------------------+
1 row in set (0.00 sec)

これをOFFる。と、想像通り、

mysql> INSERT INTO d1.t1 VALUES (1, 'one');
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

容赦なく蹴られるように。
Error 1213のDeadlock found.. が表示されるのは、全ノード間でデータの同期を保証する為の2相コミットっぽい仕組み(実際には2相コミットじゃない)で失敗したから(これをwsrepではCertificationと呼んでいる)で、Local Certification Failure(XA Prepare的なのに失敗)はError 1213 Deadlock found..で返される仕様になっているから。

なお飽くまで"Local" Certification Failureなので、他の(wsrep_certify_nonPK= ONな)ノードでINSERTするとINSERTできる。よって、設定する時は全てに設定しておくこと。


ところでこの動作、MariaDBには標準で無かったっけ? とか思ったけど違った。RBRの時にPkeyが無いテーブルへの更新は拒否する、じゃなくて、RBRの時でPkeyが無いテーブルへの更新でも最適化してスレーブに適用してくれる、という風情らしい。

Row-based replication with no primary key

Perconaがオリジナルだっていうのも知らなかった。

2014/01/20

mysqlコマンドラインクライアントでuseの代わりにcdを使う

何を言っているのか略という感じな気もするタイトルですね。。
話の流れとしては、


とか馬鹿なこと言ってたら
とツッコミが入ったので、やってみました。
最初はuseはステートメントだと思っていたので、SQLパーサーをいじって USEステートメント をCDステートメントに変えてやろうと思っていたんですが、"SQLデリミターを省略しても実行される", "mysqlコマンドラインクライアント上でhelp叩くと出てくる"からmysqlコマンドラインクライアント上のコマンドだよ、とツッコミをもらい、そっちをいじくってみることに。

mysql-5.6.15/client/mysql.ccのこのあたりですね。

 322 static COMMANDS commands[] = {
..
 357   { "use",    'u', com_use,    1,
 358     "Use another database. Takes database name as argument." },
..

mysqlコマンドラインクライアント上のuseコマンドを叩くとcom_use関数が呼ばれて、com_use関数の中でmysql_select_db(&mysql, ..)を叩いてcurrent_dbを変更しています。
com_use関数以降は同じで構わないので、ここだけちゃちゃっと変えてコンパイル。


$ client/mysql -uroot -S /usr/mysql/5.6.15/data/mysql.sock
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 5.6.15-log Source distribution

Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> help

For information about MySQL products and services, visit:
   http://www.mysql.com/
For developer information, including the MySQL Reference Manual, visit:
   http://dev.mysql.com/
To buy MySQL Enterprise support, training, or other products, visit:
   https://shop.mysql.com/

List of all MySQL commands:
Note that all text commands must be first on line and end with ';'
?         (\?) Synonym for `help'.
clear     (\c) Clear the current input statement.
connect   (\r) Reconnect to the server. Optional arguments are db and host.
delimiter (\d) Set statement delimiter.
edit      (\e) Edit command with $EDITOR.
ego       (\G) Send command to mysql server, display result vertically.
exit      (\q) Exit mysql. Same as quit.
go        (\g) Send command to mysql server.
help      (\h) Display this help.
nopager   (\n) Disable pager, print to stdout.
notee     (\t) Don't write into outfile.
pager     (\P) Set PAGER [to_pager]. Print the query results via PAGER.
print     (\p) Print current command.
prompt    (\R) Change your mysql prompt.
quit      (\q) Quit mysql.
rehash    (\#) Rebuild completion hash.
source    (\.) Execute an SQL script file. Takes a file name as an argument.
status    (\s) Get status information from the server.
system    (\!) Execute a system shell command.
tee       (\T) Set outfile [to_outfile]. Append everything into given outfile.
cd        (\u) Use another database. Takes database name as argument.
charset   (\C) Switch to another charset. Might be needed for processing binlog with multi-byte charsets.
warnings  (\W) Show warnings after every statement.
nowarning (\w) Don't show warnings after every statement.

For server side help, type 'help contents'

mysql> SHOW DATABASES;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| d1                 |
| mysql              |
| performance_schema |
| test               |
+--------------------+
5 rows in set (0.00 sec)

mysql> SELECT database();
+------------+
| database() |
+------------+
| NULL       |
+------------+
1 row in set (0.02 sec)

mysql> cd d1
Database changed

mysql> SELECT database();
+------------+
| database() |
+------------+
| d1         |
+------------+
1 row in set (0.00 sec)

mysql> use mysql
    -> ;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT database();
+------------+
| database() |
+------------+
| mysql      |
+------------+
1 row in set (0.00 sec)

helpの結果がuseコマンドからcdコマンドになり、useを叩くと末尾にセミコロンを求められる。で、セミコロンつけて送信するとMySQLのパーサーがUSEステートメントとして解釈するから"Query OK, .."の応答メッセージ。

ううむ、意外と面白いぞこれ。

2014/01/15

Percona XtraDB Cluster設計の迷いどころ(現在進行形)=> 解決

今、とあるサービスに Percona XtraDB Cluster を突っ込むことを画策しているんですが、今まさに迷っている真っ最中なのでメモしておきます。誰か何か思いついたら教えてください。

Percona XtraDB Cluster(以下PXC)というか Galera Replication の仕組みというかコンセプトというか自体が、「マルチマスターなMySQL *の* クラスター」なわけで、PXCを使えばこれは簡単に実現できます。

が。

PXCでマルチマスターすると、バイナリーログの扱いがちょっと問題になってきます。
wsrep providerが提供するPXC内の同期はGalera Cacheと呼ばれる専用のバイナリーログ(からヘッダーを取り除いたもの)に格納され、同期されます。Galera Cacheのファイルは(どういう仕組みなのか調べてないけれど)原則として一定サイズで(必要に応じて拡張され、必要なくなったら元に戻る)、これだけ見てるとInnoDBログファイルみたいにシーケンシャルに書いて、ファイルの末端に言ったら先頭に戻っている気がしなくもないです(調べてないです)

これは別にどうでも良いんですが、Galera Cacheを使って同期されたデータは log-slave-updates で拾えず、つまりバイナリーログは *更新SQLを受け取ったmysqldのみ* が保存します。
この前提かつマルチマスターにすると、

バ イ ナ リ ー ロ グ が 複 数 台 に 分 散 さ れ 、 ポ イ ン ト イ ン タ イ ム リ カ バ リ ー が 出 来 な い

ことになるんじゃないでしょうか。ってか、今のところなってます。
分散するところまでは良いです。バックアップ用と割り切れば、 MariaDB 10.0のマルチソースレプリケーション とか、 MySQL 5.7のLabs版 でlog-slave-updatesもアリな気はします。MySQLには 由緒正しいN:1レプリケーション もありますが、今回はデータをごにょごにょしたいのではなくバイナリーログの保全が目的になるのでちょっとこの方式は取れなそうです。

いや、MariaDB 10.0もMySQL 5.7も *シリアライズを保証できない* 時点で完全にアウトなんですけどね :(

バイナリーログを一箇所に集めたところで、PITRの為に流しなおす範囲のバイナリーログを全てデコードして、時間順に並べ変えて…としても、バイナリーログは秒までしか時間を保存しないので、その間に同じレコードが2度更新されたりすると、どっちが最終状態として正しいのか判らなくなってPITRの一貫性が崩れる。

かといってGalera Cacheは上手く吸い上げてやるにはファイルの機構がバイナリーログとは違いそうだし、なんともかんともはや。。

codershipのフォーラムでも未対応ってことになってた(URLわかんなくなった。。)んですが、一応 *将来ね!* って雰囲気ではありました。

どうしようかなー。。


【2014/01/15 14:59】
今現在はこんなことを考えてます。。





【2014/07/09 17:36】
バージョンアップで出来るようになってた! これで(この迷いどころは)なくなったぞ!
Percona XtraDB Clusterでいつの間にかPITRできるようになってた 

2013/12/20

Mroongaのロックが突き刺さるとどうなるか

むるーん、ガッ(挨拶
このエントリーは 全文検索エンジンGroonga Advent Calendar 2013 の20日目です。

みなさん、最近、ロック刺さりましたか?
クラッシュ以外でそうそう刺さることはないと思うんですが、いざ刺さるとどうなるのか…というのは予め考えておいた方が良いと思います(こんなエントリーを書いている時点でお察しください)

復旧作業については @yoshi_kenさん の Y-Ken Studio: groonga/mroongaの.mrnファイルがlock failedとなった場合の復旧方法 が詳しいですのでそちらで。

クラッシュして刺さるなら、クラッシュした時点で諸々通知されたりサービスから切り離されているので、まあそんなに困りはしないんですが、*何故かクラッシュせずに刺さってしまった場合* の動きは(当然)かなりシビアなものになります。

mysql> SHOW CREATE TABLE t1\G
*************************** 1. row ***************************
       Table: t1
Create Table: CREATE TABLE `t1` (
  `num` int(10) unsigned NOT NULL,
  `val` varchar(32) NOT NULL,
  PRIMARY KEY (`num`),
  FULLTEXT KEY `val` (`val`)
) ENGINE=mroonga DEFAULT CHARSET=utf8
1 row in set (0.02 sec)

mysql> SELECT * FROM t1;
+-----+-----+
| num | val |
+-----+-----+
|   1 | one |
|   2 | two |
+-----+-----+
2 rows in set (0.02 sec)

まあ、こんなテーブルを作るわけです。

ロックの刺し方(どのプロセスも触っていないのに、mrnファイルがロックされたまま解放されない状態の作り方)は すとーさん にTwitterで教えてもらいました。



$ /usr/ruby/2.0.0/bin/ruby -e 'require "groonga"; Groonga::Database.open("/usr/local/mysql/data/d1.mrn"); Groonga["t1"].lock; exit'

と、こう刺してやると、

mysql> SELECT * FROM t1;
+-----+-----+
| num | val |
+-----+-----+
|   1 | one |
|   2 | two |
+-----+-----+
2 rows in set (0.00 sec)

mysql>
mysql> DELETE FROM t1 WHERE num= 1;

SELECTはできるけど、DELETEは返ってこなくなります。
これは良い。別に良い。想定通りの動作です。
困る(?)のはここから後。

mysql> SELECT @@global.lock_wait_timeout;
+----------------------------+
| @@global.lock_wait_timeout |
+----------------------------+
|                          1 |
+----------------------------+
1 row in set (0.00 sec)
mysql> SHOW PROCESSLIST;
+----+------+-----------+------+---------+------+----------+-----------------------------+
| Id | User | Host      | db   | Command | Time | State    | Info                        |
+----+------+-----------+------+---------+------+----------+-----------------------------+
|  3 | root | localhost | d1   | Query   |   63 | updating | DELETE FROM t1 WHERE num= 1 |
|  4 | root | localhost | d1   | Query   |    0 | init     | SHOW PROCESSLIST            |
+----+------+-----------+------+---------+------+----------+-----------------------------+
2 rows in set (0.00 sec)

実際ロックが競合してはいるんですが、lock_wait_timeoutは効きません。SHOW PROCESSLISTを見てやると、Stateはupdatingで、Lock待ち扱いではないですね。ちょっとgdbで覗いてみます。

$ gdb -p `pidof mysqld`
..
(gdb) thread apply all bt
..
Thread 3 (Thread 0x7fb2000be700 (LWP 8370)):
#0  0x0000003eb480ed2d in nanosleep () at ../sysdeps/unix/syscall-template.S:82
#1  0x00007fb200645f79 in grn_nanosleep (nanoseconds=) at ctx.c:81
#2  0x00007fb20074bc72 in grn_io_lock (ctx=0x7fb17400fb38, io=0x7fb17402b0c0, timeout=10000000) at io.c:1320
#3  0x00007fb20066df80 in grn_table_delete_by_id (ctx=0x7fb17400fb38, table=0x7fb17402adf0, id=1) at db.c:1784
#4  0x00007fb1e38ea422 in ha_mroonga::storage_delete_row (this=0x7fb17400e9e0, buf=0x7fb1740379f0 "\001")
    at ha_mroonga.cpp:6241
#5  0x0000000000590460 in handler::ha_delete_row (this=0x7fb17400e9e0, buf=0x7fb1740379f0 "\001")
    at /home/yoku0825/mysql-5.6.15/sql/handler.cc:7281
#6  0x0000000000823ebb in mysql_delete (thd=0x46a4ec0, table_list=0x7fb174005038, conds=0x7fb174005b58,
    order_list=, limit=18446744073709551615, options=0)
    at /home/yoku0825/mysql-5.6.15/sql/sql_delete.cc:373
#7  0x00000000006d5c4a in mysql_execute_command (thd=0x46a4ec0) at /home/yoku0825/mysql-5.6.15/sql/sql_parse.cc:3571
#8  0x00000000006d8607 in mysql_parse (thd=0x46a4ec0, rawbuf=, length=,
    parser_state=) at /home/yoku0825/mysql-5.6.15/sql/sql_parse.cc:6247
#9  0x00000000006d9db3 in dispatch_command (command=COM_QUERY, thd=0x46a4ec0, packet=0x7fb2000bdaf0 "\300Nj\004",
    packet_length=27) at /home/yoku0825/mysql-5.6.15/sql/sql_parse.cc:1334
#10 0x00000000006a23ed in do_handle_one_connection (thd_arg=)
    at /home/yoku0825/mysql-5.6.15/sql/sql_connect.cc:982
#11 0x00000000006a2522 in handle_one_connection (arg=)
    at /home/yoku0825/mysql-5.6.15/sql/sql_connect.cc:898
#12 0x0000000000972a37 in pfs_spawn_thread (arg=0x4766040) at /home/yoku0825/mysql-5.6.15/storage/perfschema/pfs.cc:1858
#13 0x0000003eb4807851 in start_thread (arg=0x7fb2000be700) at pthread_create.c:301
#14 0x0000003eb40e894d in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:115
..

grn_io_lockがgrn_nanosleepを呼んでいるので、まあこの辺りですよね。
grn_io_lockはGroonga(not Mroonga)のlib/io.c に実装されています。

1290 grn_io_lock(grn_ctx *ctx, grn_io *io, int timeout)
1291 {
..
1296   for (count = 0;; count++) {
..
1306       if (!timeout || (timeout > 0 && timeout == count)) {
1307         GRN_LOG(ctx, GRN_LOG_WARNING,
1308                 "[DB Locked] time out(%d): io(%s) collisions(%d/%d)",
1309                 timeout, io->path, _ncolls, _ncalls);
1310         break;
1311       }
..
1320       grn_nanosleep(1000000);
1321       continue;
1322     }
..
1327 }

タイムアウトするまで自前でループしています。mysqld的にはMroongaストレージエンジンが勝手にループしているだけでロックを検知することができませんので、StateはLockedにはならず、Lock wait timeout exceededも起こりません(´・ω・`)

で、コイツがタイムアウトしてくれるまでが長い。

grn_nanosleep(1000000)なので、1回のスリープは1ms。timeoutはgrn_io_lockの呼び出し元すべてで10000000がハードコードされているので、10,000s。

…いちまんびょう? 3時間?;

GroongaのRedmineにも上がってる けど、長いし変更できないのはつらい。
取り敢えずゴニョゴニョしてtimeoutを1000msに変更してみる。

mysql> INSERT INTO t1 VALUES (1, 'one');
ERROR 1026 (HY000): grn_io_lock failed

$ perror 1026
MySQL error code 1026 (ER_ERROR_ON_WRITE): Error writing file '%-.200s' (errno: %d - %s)

$ less groonga.log
2013-12-19 18:04:52.165728|n|120c6700|io(d1.mrn.0000100) collisions(1000/117): lock failed 1000 times
2013-12-19 18:04:52.165901|w|120c6700|[DB Locked] time out(1000): io(d1.mrn.0000100) collisions(1000/117)
2013-12-19 18:04:52.165917|e|120c6700|grn_io_lock failed
..

うん、やっぱりこれ自分で設定したいなぁ。。(チラッチラッ
欲を言えばmy.cnfで設定したいし、更に言うならMySQLのlog-errorにも吐いてほしい。


同じくラッパーモード。

mysql> SHOW CREATE TABLE t1\G
*************************** 1. row ***************************
       Table: t1
Create Table: CREATE TABLE `t1` (
  `num` int(10) unsigned NOT NULL,
  `val` varchar(32) NOT NULL,
  PRIMARY KEY (`num`),
  FULLTEXT KEY `val` (`val`)
) ENGINE=mroonga DEFAULT CHARSET=utf8 COMMENT='engine "innodb"'
1 row in set (0.00 sec)

$ /usr/ruby/2.0.0/bin/ruby -e 'require "groonga"; Groonga::Database.open("/usr/local/mysql/data/d2.mrn"); Groonga["t1"].lock; exit'

mysql> SELECT * FROM t1;
+-----+-----+
| num | val |
+-----+-----+
|   1 | one |
|   2 | two |
+-----+-----+
2 rows in set (0.00 sec)

mysql> DELETE FROM t1 WHERE num= 1;

$ gdb -p `pidof mysqld`
..
(gdb) thread apply all bt
..
Thread 2 (Thread 0x7fd1a009d700 (LWP 16797)):
#0  0x0000003eb480ed2d in nanosleep () at ../sysdeps/unix/syscall-template.S:82
#1  0x00007fd20c481f79 in grn_nanosleep (nanoseconds=) at ctx.c:81
#2  0x00007fd20c587c72 in grn_io_lock (ctx=0x7fd17c012a18, io=0x7fd17c310200, timeout=10000000) at io.c:1320
#3  0x00007fd20c4a9f80 in grn_table_delete_by_id (ctx=0x7fd17c012a18, table=0x7fd17c330ae0, id=1) at db.c:1784
#4  0x00007fd1ed583422 in ha_mroonga::storage_delete_row (this=0x7fd17c0118c0, buf=0x7fd17c038a80 "\001")
    at ha_mroonga.cpp:6241
#5  0x0000000000590460 in handler::ha_delete_row (this=0x7fd17c0118c0, buf=0x7fd17c038a80 "\001")
    at /home/yoku0825/mysql-5.6.15/sql/handler.cc:7281
#6  0x0000000000823ebb in mysql_delete (thd=0x44a0910, table_list=0x7fd17c005038, conds=0x7fd17c005b58, 
    order_list=, limit=18446744073709551615, options=0)
    at /home/yoku0825/mysql-5.6.15/sql/sql_delete.cc:373
#7  0x00000000006d5c4a in mysql_execute_command (thd=0x44a0910) at /home/yoku0825/mysql-5.6.15/sql/sql_parse.cc:3571
#8  0x00000000006d8607 in mysql_parse (thd=0x44a0910, rawbuf=, length=, 
    parser_state=) at /home/yoku0825/mysql-5.6.15/sql/sql_parse.cc:6247
#9  0x00000000006d9db3 in dispatch_command (command=COM_QUERY, thd=0x44a0910, packet=0x7fd1a009caf0 "\020\tJ\004", 
    packet_length=27) at /home/yoku0825/mysql-5.6.15/sql/sql_parse.cc:1334
#10 0x00000000006a23ed in do_handle_one_connection (thd_arg=)
    at /home/yoku0825/mysql-5.6.15/sql/sql_connect.cc:982
#11 0x00000000006a2522 in handle_one_connection (arg=)
    at /home/yoku0825/mysql-5.6.15/sql/sql_connect.cc:898
#12 0x0000000000972a37 in pfs_spawn_thread (arg=0x45619d0) at /home/yoku0825/mysql-5.6.15/storage/perfschema/pfs.cc:1858
#13 0x0000003eb4807851 in start_thread (arg=0x7fd1a009d700) at pthread_create.c:301
#14 0x0000003eb40e894d in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:115

全く同じところで突き刺さる。
これ、ラッパーモードだろうと更新であればmrnファイルは常に並列性が無くて、ただ mrnファイルはトランザクションのコミットに関わらず先行して更新が終わってしまう からこのジャイアントロックが問題になることは無くて…というシナリオなんだろう、きっと、たぶん(あるいは、ほんのり問題にはなっているけどそれに気付かないか)

で、mrnファイルのロックが突き刺さると更新系のクエリーが全てupdatingとかexecuteで自前ループを回し、connectionがどんどん溜まりそいつらも自前でループし、最終的にCPUを振り切って略…となったることが予想されるので、タイムアウト値は短めに設定して、groonga.logかアプリのログを監視しておいた方が良いかも。

別にしょっちゅうロックが刺さっててGroongaなんとかしる、という訳ではなく、たまたま刺さった時に大変だったそうなので備えておきましょうね、というお話でした :)


【2014/01/30 12:57】
2014/01/29リリースのMroonga 3.12で、MySQLのSET GLOBALで動的にタイムアウト閾値を変えられるようになりました!(need Groonga >= 3.1.2)

ありがとうございます!