インメモリデータベース　VoltDBの最新バージョン（v8.3.1）をクラスタ構成でインストールし、簡単なプロシージャを実行する

# はじめに

VoltDBは2018年10月時点で最新バージョンがv8.3.1です。
※2018年11月時点でv8.3.3がリリースされていました。
※2019年4月にv9.0がリリースされていました。

私が初めてVoltDBを使ったときはv4.9でしたので、だいぶバージョンが上がっています。
今回、最新バージョンを触ってみたかったので、まずはインストールし、クライアントプログラムを実行するところまでやってみました。

[VoltDB 最新版のリリースノート](https://docs.voltdb.com/ReleaseNotes/)

v9.0もリリースされていますが、インストール手順はv8.3.xと同じです。

少し古いですが、VoltDB 8.2のリリース内容は以下を参照。
[VoltDB 8.2リリース。古いデータを自動で削除するTTL機能を試してみました](https://qiita.com/mkyz08/items/8200b5456d9f9b96746b)

こちらはクラスタ構成の構築手順の記事で、シングル構成での構築手順は以下の記事になります。

[インメモリデータベース　VoltDBの最新バージョン（v8.3.1）をインストールする（シングル構成）](https://qiita.com/mkyz08/items/47fa33cd715178a50df3)


# VoltDBとは

VoltDBはインメモリRDBで、PostgreSQLに携わったマイケル・ストーンブレーカーにより設定されています。
NoSQLより高速でありながら、SQLを利用できトランザクションもサポートされています。

最近はあまり話題にならない（？）ですが、「2011年の国民的アイドルグループ選抜総選挙」で利用されたときに話題になりました。
私も購入したのですが、舞台裏は以下の本に書いています（技術的に深い内容ではありませんが）。

[過負荷に耐えるWebの作り方 ~国民的アイドルグループ選抜総選挙の舞台裏 (Software Design plus)](http://www.amazon.co.jp/exec/obidos/ASIN/4774162051/hatena-blog-22/)

興味がある方は以下も参考になるかと思います。

VoltDBを使ってみる
https://qiita.com/ytake/items/4da625997079716404ad
VoltDB(Wikipedia)
https://ja.wikipedia.org/wiki/VoltDB

VoltDB社がCassandraとの性能比較を2017年に実施しており、Cassandraより性能・コスト面で優れていると書かれています。

Comparing Fast Data Performance: A comparison of VoltDB and Cassandra Benchmarks
https://www.voltdb.com/blog/2017/10/02/comparing-fast-data-performance-a-comparison-of-voltdb-and-cassandra-benchmarks/

[DB-ENGINES](https://db-engines.com/en/ranking)では2018年11月時点で、全体109位、RDBMSで56位でした。


# 環境と構築手順の概要

VirtualBox上にCentOS 7.4を3台起動し、VoltDBをクラスタ構成で構築します。
ローカル環境なので、firewalldとSELinuxは無効化しています。
[2018/10/8追記]firewalldを有効のままポート解放し設定する手順を追記しました。

サーバは以下の3台です。
・192.168.10.121
・192.168.10.122
・192.168.10.123


構築では以下の順番で実施します。

1. OpenJDKのインストール
1. NTPの設定
1. VoltDBのインストール
1. VoltDBの設定
1. スキーマ作成
1. プロシージャの作成とコンパイル
1. VoltDB起動


# 構築手順

## OpenJDKのインストール

OpenJDK 8かOracleJDK 8をインストールします。
今回はOpenJDKをインストールしましたが、OracleJDKが推奨されています。
[2018/10/4追記]
OracleJDKが推奨されていますが、OpenJDKもサポートされています。

```
# yum install java-1.8.0-openjdk-devel

# java -version
openjdk version "1.8.0_191"
OpenJDK Runtime Environment (build 1.8.0_191-b12)
OpenJDK 64-Bit Server VM (build 25.191-b12, mixed mode)
```

JAVA_HOMEを設定し、PATHにJavaのパスを追加しています。

```terminal
# echo "export JAVA_HOME=$(readlink -e $(which java)|sed 's:/bin/java::')" > /etc/profile.d/java.sh
# echo "PATH=\$PATH:\$JAVA_HOME/bin" >> /etc/profile.d/java.sh
# source /etc/profile
```

## NTPの設定

クラスタ構成の場合はVoltDBの各サーバ間での時刻のずれが100ms未満である必要があるため、NTPで同期させます。

私の環境では「volt1：192.168.10.122」をNTPサーバ、他の2台をNTPクライアントとして設定しました。

以下はテスト環境なのでざっくりと設定。

①　1台目のVoltDBサーバ（NTPサーバ）

CentOS 7なのでchronyがインストールされています。

```
vi /etc/chrony.conf
```
以下を追記。

```
# 以下をコメントアウトする
#server 0.centos.pool.ntp.org iburst
#server 1.centos.pool.ntp.org iburst
#server 2.centos.pool.ntp.org iburst
#server 3.centos.pool.ntp.org iburst

# 以下のコメントを外す
allow 192.168.0.0/16
local stratum 10

```

chronydを再起動。

```
systemctl restart chronyd
```

②　2台目以降のVoltDBサーバ（NTPクライアント）

NTPクライアントはchronyをそのまま利用します。

```
vi /etc/chrony.conf
```

以下を追記します。

```
#server 0.centos.pool.ntp.org iburst
#server 1.centos.pool.ntp.org iburst
#server 2.centos.pool.ntp.org iburst
#server 3.centos.pool.ntp.org iburst
#server 192.168.10.121 iburst
server volt1 iburst minpoll 4 maxpoll 4

peer volt2 minpoll 4 maxpoll 4
peer volt3 minpoll 4 maxpoll 4
```

chronydを再起動。

```
systemctl restart chronyd
```

2, 3台目のサーバ（volt1, volt2）で「chronyc sources」で時刻同期されていることを確認します。

```
# chronyc sources
210 Number of sources = 3
MS Name/IP address         Stratum Poll Reach LastRx Last sample               
===============================================================================
^* volt1                        10   4    37     5    -42us[-1569us] +/-  387us
=? volt2                         0   4     0     -     +0ns[   +0ns] +/-    0ns
=? volt3                        11   4     1    10  -7673us[-9201us] +/-   12ms


```


## firewalldの設定

[2018/10/8追記]

VoltDBがデフォルトで使用するポートの一覧は以下のとおりです。

Client Port: 21212
Admin Port: 21211
Web Interface Port (httpd): 8080
Web Interface Port (with TSL/SSL enabled): 8443
Internal Server Port: 3021
Replication Port: 5555
Zookeeper port: 7181

この内、「Web Interface Port (with TSL/SSL enabled)」と「Replication Port」は使用する場合だけポートを解放すればよいです。

```
firewall-cmd --add-port=21212/tcp --permanent
firewall-cmd --add-port=21211/tcp --permanent
firewall-cmd --add-port=8080/tcp --permanent
firewall-cmd --add-port=3021/tcp --permanent
firewall-cmd --add-port=5555/tcp --permanent
firewall-cmd --add-port=7181/tcp --permanent
firewall-cmd --add-port=123/udp --permanent
firewall-cmd --add-port=22/tcp --permanent
firewall-cmd --reload
```
※123はNTP用

以下のコマンドで設定を確認します。

```
firewall-cmd --list-ports --zone=public
21212/tcp 21211/tcp 8080/tcp 3021/tcp 5555/tcp 7181/tcp 22/tcp
```

## SSHの設定

互いのサーバはSSHでパスフレーズなしでログインできる必要があります。
※後で手順を書く。


## VoltDBのインストール

VoltDBのモジュールは以下の公式サイトから「COMMUNITY EDITION」をダウンロードします。
一部使用できない機能もありますが、プロダクション環境でも十分な機能が網羅されている（と思います）。

https://www.voltdb.com/try-voltdb/

上のURLだと必ず最新版がダウンロードされるため、古いバージョンを使用する場合は、以下のようにファイル指定でダウンロードします。

http://voltdb.com/downloads/technologies/server/voltdb-community-8.3.1.tar.gz

![image.png](https://qiita-image-store.s3.amazonaws.com/0/179607/53c9ab85-ed7f-006e-ff84-42fc43ccbb98.png)

まず、voltdb-community-8.3.1.tar.gzを/opt以下に展開し、シンボリックリンクを作成します。

```
# tar xvzf voltdb-community-8.3.1.tar.gz -C /opt/
# ln -s /opt/voltdb-community-8.3.1/ /opt/voltdb
```

VoltDBでは透過的なHugePages（Transparent HugePage）を無効化する必要があります。これを実行しないとVoltDBが起動しません。
これだとOS起動後に毎回実行しないといけない。

```
# echo never > /sys/kernel/mm/transparent_hugepage/enabled
# echo never > /sys/kernel/mm/transparent_hugepage/defrag
```

OS起動時に自動実行するにはrc.localを編集します。[2018/10/7追記]
/etc/rc.localに以下を追記します。（CentOS 7.4の場合）

```
if test -f /sys/kernel/mm/transparent_hugepage/enabled; then
   echo never > /sys/kernel/mm/transparent_hugepage/enabled
fi
if test -f /sys/kernel/mm/transparent_hugepage/defrag; then
   echo never > /sys/kernel/mm/transparent_hugepage/defrag
fi
```

また、rc.localに実行権限も付与してください。

```
# chmod u+x /etc/rc.d/rc.local
```

最後にOSを再起動し、以下のコマンドを実行して両方ともneverになっていることを確認します。

```
# cat /sys/kernel/mm/transparent_hugepage/enabled
always madvise [never]
# cat /sys/kernel/mm/transparent_hugepage/defrag
always madvise [never]
```

今回使用するサーバ3台をhostsに入れます。

```
vi /etc/hosts
```

「/etc/hosts」に追加する内容は以下のとおりです。

```
192.168.10.121	volt1
192.168.10.122	volt2
192.168.10.123	volt3

```

次に「/opt/voltdb/bin」をパスに追加します。

```
vi /etc/profile.d/voltdb.sh
```

追加する内容は以下のとおりです。

```
export PATH="$PATH:/opt/voltdb/bin"
```

すぐに反映させるため、以下のコマンドを入力します。

```
source /etc/profile
```

## メモリ管理関係の設定


- [2.3. Configure Memory Management](https://docs.voltdb.com/AdminGuide/adminmemmgt.php)

```
# vi /etc/sysctl.conf

vm.swappiness=0
vm.overcommit_memory=1
vm.max_map_count=1048576

# sysctl -p
vm.swappiness = 0
vm.overcommit_memory = 1
vm.max_map_count = 1048576
```

## TCP Segmentationオフ

```
$ vi /etc/rc.d/rc.local

ethtool -K enp0s8 tso off
ethtool -K enp0s8 gro off

確認
$ ethtool --show-features eth0
```
※enp0s8は環境によって異なります。

## JVMの統計収集無効化

JVMの統計収集を無効化します。

```
# vi /etc/profile.d/voltdb.sh
export VOLTDB_OPTS='-XX:+PerfDisableSharedMem'
```


## VoltDBの設定

VoltDBのクラスタ構成などの設定を記載した設定ファイルを新規に作成します。
以下のような「/opt/voltdb/deployment.xml」のファイルを作成します。

「kfactor="1"」がバックアップの数です。"1"に設定しておくと各データのバックアップが1つずつ作成されるので、1台ダウンしてもクラスタを維持することができます。2台落ちた場合はクラスタがダウンします。
クラスタがダウンした場合は、ファイル出力しておいたスナップショットから復元することになります。
今回は「\<snapshot enabled="false"/>」としてスナップショットをオフにしています。

```xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<deployment>
    <cluster sitesperhost="8" kfactor="1" schema="ddl"/>
    <paths>
        <!-- <voltdbroot path="/opt/voltdb/voltdbroot"/> -->
        <snapshots path="snapshots"/>
        <exportoverflow path="export_overflow"/>
        <droverflow path="dr_overflow"/>
        <commandlog path="command_log"/>
        <commandlogsnapshot path="command_log_snapshot"/>
        <largequeryswap path="large_query_swap"/>
    </paths>
    <partition-detection/>
    <heartbeat/>
    <ssl/>
    <httpd enabled="true">
        <jsonapi enabled="true"/>
    </httpd>
    <snapshot enabled="false"/>
    <commandlog enabled="false">
        <frequency/>
    </commandlog>
    <systemsettings>
        <temptables/>
        <snapshot/>
        <elastic/>
        <query/>
        <procedure/>
        <resourcemonitor>
            <memorylimit/>
        </resourcemonitor>
    </systemsettings>
    <security/>
</deployment>
```


## スキーマ作成

「/opt/voltdb」以下にschema.sqlを作成します。
VoltDB4.9とはDDLの構文が若干変わっていました。
テーブルやプロシージャは、インストール後に作成することもできるので、必ずしもインストール前に準備する必要はありません。

TEST_DATAテーブルを作成し、GetData、InsertDataという2つのプロシージャを作成しています。

```sql
CREATE TABLE TEST_DATA (
   NAME varchar(50) NOT NULL,
   DATA smallint,
   UNIQUE (NAME),
   PRIMARY KEY (NAME)
);
PARTITION TABLE TEST_DATA ON COLUMN NAME;

CREATE PROCEDURE
   PARTITION ON TABLE TEST_DATA COLUMN NAME
   FROM CLASS test.GetData;

CREATE PROCEDURE
   PARTITION ON TABLE TEST_DATA COLUMN NAME
   FROM CLASS test.InsertData;
```

## プロシージャの作成とコンパイル

TEST_DATAを検索するGetDataとインサートするInsertDataのサンプルプログラム（プロシージャ）は以下のとおりです。

今回はプロシージャを作成していますが、実はVoltDBではデフォルトのプロシージャが作成されており、insert/update/delete/selectはできます。デフォルトで実現できないプロシージャを実現するときだけ、カスタムのプロシージャを作成する必要があります。

```java
package test;

import org.voltdb.SQLStmt;
import org.voltdb.VoltProcedure;
import org.voltdb.VoltTable;

public class GetData extends VoltProcedure {

	public final SQLStmt selectData = new SQLStmt("select data from test_data where name = ?");

	public VoltTable[] run(String name) {

		voltQueueSQL(selectData, name);
		VoltTable[] results = voltExecuteSQL();

		return results;
	}
}
```

```java
package test;

import org.voltdb.SQLStmt;
import org.voltdb.VoltProcedure;
import org.voltdb.VoltTable;

public class InsertData extends VoltProcedure {

	public final SQLStmt insertData = new SQLStmt("insert into test_data values(?, ?)");

	public VoltTable[] run(String name, int data) {

		voltQueueSQL(insertData, name, data);
		VoltTable[] results = voltExecuteSQL();

		return results;
	}
}



```

コンパイルするため、/opt/voltdb/voltdb以下にあるvoltdb-8.3.1.jarをビルドパスに追加しておきます。
そして、Jarファイルに固めて（catalog.jar）、各サーバの/opt/voltdb以下に格納します。


## VoltDBの初期化と起動

プロシージャ（catalog.jar）、DDL（schema.sql）、設定ファイル（deployment.xml）を使用して、VoltDBを初期化します。3台全てで実行します。
※-fをつけると強制的に初期化します。今回は初回なので必要ないですが既に初期化済みの場合は-fをつけます。

```
# cd /opt/voltdb
# voltdb init -D /opt/voltdb -C /opt/voltdb/deployment.xml -j /opt/voltdb/catalog.jar -s /opt/voltdb/schema.sql -f
```

VoltDBを起動します。

```
# cd /opt/voltdb
# voltdb start -D /opt/voltdb -H volt1 -c 3 -B
```

-D：　VoltDBのルートディレクトリ（init時と同じディレクトリを指定）
-H：　接続先のクラスタのサーバ
　今回はvolt1に接続していますが、通常はvolt1,volt2,volt3としておけばよいです。
-c：　クラスタ中のサーバ台数
-B：　バックグラウンドで実行

# VoltDBのWeb画面を開く

VoltDBでは標準でSQLを実行したり、パフォーマンスを見ることができるWebアプリケーションが起動します。
各サーバで実行されており、8080ポートでアクセスできます。

http://192.168.10.121:8080/

画面は以下のようになります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/179607/ee581715-618b-0a85-df2b-5cf8932ef8fd.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/179607/8234f03f-02c6-6d21-5428-479c774ff815.png)


# クライアントプログラムを実行する

先ほど作成したGetData、InsertDataのプロシージャへアクセスするクライアントプログラムを作成します。

```java
import java.io.IOException;

import org.voltdb.VoltTable;
import org.voltdb.VoltType;
import org.voltdb.client.Client;
import org.voltdb.client.ClientFactory;
import org.voltdb.client.ClientResponse;
import org.voltdb.client.NoConnectionsException;
import org.voltdb.client.ProcCallException;

public class SimpleMain {

	public static void main(String[] args) throws NoConnectionsException, IOException, ProcCallException {
		Client client = ClientFactory.createClient();
		client.createConnection("192.168.10.121");
		client.createConnection("192.168.10.122");
		client.createConnection("192.168.10.123");

		String msgId = "test1";
    	VoltTable[] results;
    	ClientResponse res;

    	res = client.callProcedure("InsertData", msgId, 1);

    	if (res.getStatus() == ClientResponse.SUCCESS) {
    		System.out.println("Insertに成功");
    	}

    	res = client.callProcedure("GetData", msgId);
    	results = res.getResults();

    	while (results[0].advanceRow()) {
    		Short value = (Short) results[0].get(0, VoltType.SMALLINT);
    		System.out.println("value = " + value);
    	}
	}
}
```

以下でクラスタを構成するすべてのサーバへ接続しています。

```java
		Client client = ClientFactory.createClient();
		client.createConnection("192.168.10.121");
		client.createConnection("192.168.10.122");
		client.createConnection("192.168.10.123");
```

# 性能を計測してみる

50万レコードを500スレッドでInsertするテストプログラムを作成し、性能を計測してみました。
VoltDBでは1サーバ8コア程度を割り当てるのが良いのですが、今回はローカルPCの仮想環境で動かしているので1コア3サーバ構成になっています。
測定結果はVoltDBのWeb画面で見ることができます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/179607/45825a15-bb97-dbc7-edee-ee7d853805b5.png)

まず、トランザクションは1秒間に最大で約18,000レコードと、貧弱な環境としてはまずまずでしょうか？

![image.png](https://qiita-image-store.s3.amazonaws.com/0/179607/3e45c4ef-3b72-0bf3-c1b6-2841fb262ac1.png)

特に平均レイテンシ（ms）は0.02msとなっておりかなり速いです。インメモリRDBの真価が出ているかと思います。