MySQL InnoDB memcached Pluginを試してみた

## はじめに

MySQL 5.6から [InnoDB memcached Plugin](https://dev.mysql.com/doc/refman/5.6/ja/innodb-memcached.html) というのが導入されました。これは memcached プロトコルおよびクライアントライブラリを利用して、MySQLのInnoDBテーブルにアクセスできるようにするプラグインです。

本記事では、簡単な動作検証を通じて本プラグインの概要を調査します。

## 前提
* Dockerの`mysql:5.7` を利用して検証する
    * インストールされるバージョンは `5.7.20`

## 導入方法
導入方法は[本家マニュアル](https://dev.mysql.com/doc/refman/5.7/en/innodb-memcached-setup.html)に沿って行いますが、至って簡単。

1.  `source /usr/share/mysql/innodb_memcached_config.sql` のSQL文を実行
1. `INSTALL PLUGIN daemon_memcached soname "libmemcached.so";` を実行
    * 一度プラグインをインストールしたら、MySQLサーバーを再起動するたびに自動的にプラグインは有効になります。

```sql
mysql> source /usr/share/mysql/innodb_memcached_config.sql
Query OK, 1 row affected (0.00 sec)

Database changed
Query OK, 0 rows affected (0.03 sec)

Query OK, 0 rows affected (0.01 sec)

Query OK, 0 rows affected (0.02 sec)

Query OK, 1 row affected (0.01 sec)

Query OK, 1 row affected (0.00 sec)

Query OK, 1 row affected (0.01 sec)

Query OK, 1 row affected (0.00 sec)

Query OK, 1 row affected (0.00 sec)

Database changed
Query OK, 0 rows affected (0.02 sec)

Query OK, 1 row affected (0.00 sec)

mysql> INSTALL PLUGIN daemon_memcached soname "libmemcached.so";
Query OK, 0 rows affected (0.01 sec)
```

導入はこれで終わりですが、`innodb_memcached_config.sql` で何が実行されたのか気になるところです。次にこのSQLファイルについて見ていきましょう。

## innodb_memcached_config.sql は何をするのか？

`innodb_memcached_config.sql` の全文は以下です。このSQLファイルでは以下を実施します。

1. `innodb_memcache` データベースの作成
    * `cache_policies` テーブルの作成
    * `containers` テーブルの作成
    * `config_options` テーブルの作成
1. `test.demo_test` テーブルをInnoDB Memcacheのバックエンドとして利用する設定を登録
1. `test` データベースの作成
    * `demo_test` テーブルの作成
    * テスト用レコードを登録

```sql:innodb_memcached_config.sql
create database innodb_memcache;

use innodb_memcache;


-- ------------------------------------------------------------------------
-- Following are set of "configuration tables" that used to configure
-- the InnoDB Memcached.
-- ------------------------------------------------------------------------

-- ------------------------------------------------------------------------
-- Table `cache_policies`
--
-- Each record in this table represents a named caching policy, specifying:
--  * How the memcache GET command is executed, including whether to get
--    records from local cache only, from InnoDB only, from local cache if
--    present (treating InnoDB as a backing store), or not at all.
--  * Similarly, how memcache SET commands are executed.
--  * How memcache DELETE commands are executed.
--  * Whether flushing the cache should cause a mass delete from NDB.
--
-- ------------------------------------------------------------------------
CREATE  TABLE IF NOT EXISTS `cache_policies` (
        `policy_name` VARCHAR(40) PRIMARY KEY,
        `get_policy` ENUM('innodb_only', 'cache_only', 'caching','disabled')
         NOT NULL ,
        `set_policy` ENUM('innodb_only', 'cache_only','caching','disabled')
         NOT NULL ,
        `delete_policy` ENUM('innodb_only', 'cache_only', 'caching','disabled')
         NOT NULL,
        `flush_policy` ENUM('innodb_only', 'cache_only', 'caching','disabled')
         NOT NULL
) ENGINE = innodb;


-- ------------------------------------------------------------------------
-- Table `containers`
--
-- A container record describes an InnoDB table used for data storage by
-- InnoDB Memcache.
-- There must be a unique index on the `key column`, and unique index name
-- is specified in the `unique_idx_name_on_key` column of the table
-- `value_columns` are comma-separated lists of the columns that make up
-- the memcache key and value. Each column width is defined such that they
-- are in consistent with NDB memcached.
-- ------------------------------------------------------------------------

CREATE  TABLE IF NOT EXISTS `containers` (
        `name` varchar(50) not null primary key,
        `db_schema` VARCHAR(250) NOT NULL,
        `db_table` VARCHAR(250) NOT NULL,
        `key_columns` VARCHAR(250) NOT NULL,
        `value_columns` VARCHAR(250),
        `flags` VARCHAR(250) NOT NULL DEFAULT "0",
        `cas_column` VARCHAR(250),
        `expire_time_column` VARCHAR(250),
        `unique_idx_name_on_key` VARCHAR(250) NOT NULL
) ENGINE = InnoDB;

CREATE  TABLE IF NOT EXISTS `config_options` (
        `name` varchar(50) not null primary key,
        `value` varchar(50)) ENGINE = InnoDB;

-- ------------------------------------------------------------------------
-- This is an example
-- We create a InnoDB table `demo_test` is the `test` database
-- and insert an entry into contrainers' table to tell InnoDB Memcache
-- that we has such InnoDB table as back store:
-- c1 -> key
-- c2 -> value
-- c3 -> flags
-- c4 -> cas
-- c5 -> exp time
-- PRIMARY -> use primary key to search
-- ------------------------------------------------------------------------

INSERT INTO containers VALUES ("aaa", "test", "demo_test",
                               "c1", "c2",  "c3", "c4", "c5", "PRIMARY");

INSERT INTO cache_policies VALUES("cache_policy", "innodb_only",
                                  "innodb_only", "innodb_only", "innodb_only");

INSERT INTO config_options VALUES("separator", "|");
INSERT INTO config_options VALUES("table_map_delimiter", ".");

CREATE DATABASE IF NOT EXISTS test;
USE test

-- ------------------------------------------------------------------------
-- Key (c1) must be VARCHAR or CHAR type, memcached supports key up to 255
-- Bytes
-- Value (c2) must be VARCHAR or CHAR type
-- Flag (c3) is a 32 bits integer
-- CAS (c4) is a 64 bits integer, per memcached define
-- Exp (c5) is again a 32 bits integer
-- ------------------------------------------------------------------------
CREATE TABLE demo_test (c1 VARCHAR(32),
                        c2 VARCHAR(1024),
                        c3 INT, c4 BIGINT UNSIGNED, c5 INT, primary key(c1))
ENGINE = INNODB;

INSERT INTO demo_test VALUES ("AA", "HELLO, HELLO", 8, 0, 0);
```

## innodb_memcacheデータベースは何か

詳細については[マニュアル](https://dev.mysql.com/doc/refman/5.7/en/innodb-memcached-internals.html)にまとまっているのでそちらをご確認ください。本項では、そのマニュアルから簡単に各テーブルの用途を説明します。

```sql
mysql> use innodb_memcache;
Database changed
mysql> show tables;
+---------------------------+
| Tables_in_innodb_memcache |
+---------------------------+
| cache_policies            |
| config_options            |
| containers                |
+---------------------------+
3 rows in set (0.00 sec)
```

### cache_policiesテーブル

get, set, delete及びflush操作時の個々のポリシーが設定できると説明されている。つまり、InnoDBテーブルに保存するか、通常のmemcachedとしてかの挙動設定であると思われる。

```sql
mysql> select * from cache_policies;
+--------------+-------------+-------------+---------------+--------------+
| policy_name  | get_policy  | set_policy  | delete_policy | flush_policy |
+--------------+-------------+-------------+---------------+--------------+
| cache_policy | innodb_only | innodb_only | innodb_only   | innodb_only  |
+--------------+-------------+-------------+---------------+--------------+
1 row in set (0.00 sec)
```

### config_optionsテーブル

memcached関連の設定値格納テーブルです。現状の設定値のままで問題ないと思います。

```sql
mysql> select * from config_options;
+---------------------+-------+
| name                | value |
+---------------------+-------+
| separator           | |     |
| table_map_delimiter | .     |
+---------------------+-------+
2 rows in set (0.00 sec)
```

### containersテーブル
このテーブルが一番重要です。このテーブルで、どのInnoDBテーブルとmemcachedをマッピングするかを設定します。

```sql
mysql> select * from containers;
+------+-----------+------------+-------------+---------------+-------+------------+--------------------+------------------------+
| name | db_schema | db_table   | key_columns | value_columns | flags | cas_column | expire_time_column | unique_idx_name_on_key |
+------+-----------+------------+-------------+---------------+-------+------------+--------------------+------------------------+
| aaa  | test      | demo_test  | c1          | c2            | c3    | c4         | c5                 | PRIMARY                |
+------+-----------+------------+-------------+---------------+-------+------------+--------------------+------------------------+
```

## TelnetでMemcachedにアクセスして動作検証

### データ取得

`demo_test` テーブルには `innodb_memcached_config.sql` 実行時に登録されたテスト用レコードが1件登録されています。

```sql
mysql> select * from demo_test;
+----+--------------+------+------+------+
| c1 | c2           | c3   | c4   | c5   |
+----+--------------+------+------+------+
| AA | HELLO, HELLO |    8 |    0 |    0 |
+----+--------------+------+------+------+
1 row in set (0.00 sec)
```

telnetを利用してローカルホストのmemcachedに接続し、GETコマンドで`AA` キーを指定することで、`HELLO, HELLO`の値を取得することができました。

```sh
root@7dbca47d81fc:/# telnet localhost 11211
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
get AA
VALUE AA 8 12
HELLO, HELLO
END
```

### データの格納

SETコマンドを利用してデータの格納もできました。確かにmemcachedプロトコルを利用して、MySQLのInnoDBテーブルにアクセスできています。

```sh
set HOGE 0 0 4
FUGA
STORED
```

```sql
mysql> select * from demo_test;
+------+--------------+------+------+------+
| c1   | c2           | c3   | c4   | c5   |
+------+--------------+------+------+------+
| AA   | HELLO, HELLO |    8 |    0 |    0 |
| HOGE | FUGA         |    0 |    1 |    0 |
+------+--------------+------+------+------+
2 rows in set (0.00 sec)
```

## 疑問
簡単な動作検証を通じて導入方法と使い方は理解できたが、いくつかの疑問がふつふつ湧いてきた。

### memcachedのインストールは不要？

答えは `libmemcached.so` という MySQL に対する memcached デーモンプラグインが同梱されているため、`memcached` のインストールは不要でした。

```sh
root@7dbca47d81fc:/# ls -la /usr/lib/mysql/plugin/ | grep libmemcached
-rw-r--r-- 1 root root  185128 Sep 14 02:10 libmemcached.so
```

### InnoDB memcachedを有効中に、memcachedデーモンを起動したどうなるか

答えは memcachedデーモンを同じポート番号で起動しようとした場合は起動できない。`/var/log/memcached.log`に以下が出力されていた。

```sh
failed to listen on TCP port 11211: Address already in use
```

なので、memcachedデーモンのポート番号を11211以外にすれば起動することはできました。

### 複数のInnoDBテーブルを使い分けることはできるのか

最初に作成した `demo_test` と同じテーブル設計で `demo_test2` テーブルを作成して、このテーブルの情報をmemcached経由で取得するにはどうすればいいのか？

```sql
mysql> select * from test.demo_test2;
+----+--------------+------+------+------+
| c1 | c2           | c3   | c4   | c5   |
+----+--------------+------+------+------+
| BB | WORLD, WORLD |    8 |    0 |    0 |
+----+--------------+------+------+------+
1 row in set (0.01 sec)
```

まず始めに `demo_test2` を InnoDB memcachedで利用する設定を containers テーブルに登録をします。

```sql
INSERT INTO containers VALUES ("bbb", "test", "demo_test2",
                               "c1", "c2",  "c3", "c4", "c5", "PRIMARY");
```

以下のようになります。

```sql
mysql> select * from containers;
+------+-----------+------------+-------------+---------------+-------+------------+--------------------+------------------------+
| name | db_schema | db_table   | key_columns | value_columns | flags | cas_column | expire_time_column | unique_idx_name_on_key |
+------+-----------+------------+-------------+---------------+-------+------------+--------------------+------------------------+
| aaa  | test      | demo_test  | c1          | c2            | c3    | c4         | c5                 | PRIMARY                |
| bbb  | test      | demo_test2 | c1          | c2            | c3    | c4         | c5                 | PRIMARY                |
+------+-----------+------------+-------------+---------------+-------+------------+--------------------+------------------------+
2 rows in set (0.00 sec)
```

その状態で、`demo_test`と`demo_test2` のデータ取得をどのように実現するのか？

```sql
mysql> select * from test.demo_test;
+------+--------------+------+------+------+
| c1   | c2           | c3   | c4   | c5   |
+------+--------------+------+------+------+
| AA   | HELLO, HELLO |    8 |    0 |    0 |
| HOGE | FUGA         |    0 |    1 |    0 |
+------+--------------+------+------+------+
2 rows in set (0.00 sec)

mysql> select * from test.demo_test2;
+----+--------------+------+------+------+
| c1 | c2           | c3   | c4   | c5   |
+----+--------------+------+------+------+
| BB | WORLD, WORLD |    8 |    0 |    0 |
+----+--------------+------+------+------+
1 row in set (0.00 sec)
```


答えは、`@@name` を利用することでアクセスができました。これは[マニュアル](https://dev.mysql.com/doc/refman/5.7/en/innodb-memcached-porting-mysql.html)にも記載があります。

```sh
get @@bbb.BB
VALUE @@bbb.BB 8 12
WORLD, WORLD
END
```

また今まで `get AA`で `demo_test`側の情報が取得できていたのが、`demo_test2`の設定を追加したことで出来なくなりました。今まで出来ていたのは、`demo_test`しか設定がなかったので、デフォルト扱いされていたのだと思われます。

```sh
get AA
END
```

`AA`の情報を取得するにはもちろん `@@aaa.AA` で取得出来ました。

```sh
get @@aaa.AA
VALUE @@aaa.AA 8 12
HELLO, HELLO
END
```

InnoDB memcached pluginを通じて複数テーブルを使う場合は、気をつけないといけないかもしれません。

## サンプルコード
本件で作成したサンプルコードは[こちら](https://github.com/hypermkt/hypermkt/tree/master/php/store-session-in-mysql)にあります。

## おわりに

タイトル通り InnoDB memcached Pluginのお試しとして動作検証を行いました。このプラグインを有効にすることで、InnoDBテーブルにアクセスすることができるmemcachedデーモンを起動することができ、KVSとしてMySQLを利用できることが分かりました。メリットとしては、InnoDB memcached pluginに乗り換える場合にアプリケーションコードの変更は無しに利用できることです。但し、バックエンドはMySQLでありディスクへの書き込みも発生するので、全くmemcachedと同じパフォーマンスが得られるかは不明です。次にInnoDB memcached Pluginの導入を見越した設定項目、パフォーマンスについて調査したいと思います。

## 参考
* [MySQL :: MySQL 5\.7 Reference Manual :: 14\.20 InnoDB memcached Plugin](https://dev.mysql.com/doc/refman/5.7/en/innodb-memcached.html)
* [MySQL :: MySQL 5\.7 Reference Manual :: 14\.20\.3 Setting Up the InnoDB memcached Plugin](https://dev.mysql.com/doc/refman/5.7/en/innodb-memcached-setup.html)
* [MySQL :: MySQL 5\.7 Reference Manual :: 14\.20\.7 InnoDB memcached Plugin Internals](https://dev.mysql.com/doc/refman/5.7/en/innodb-memcached-internals.html)