データベース自作PCサーバー作成 6

先日、自宅から出ずにずっと作業してて、ソファで寝落ちしてたのですが、起きて5秒後に何を思ったのか、突然まくらを購入した小堤です。

良い睡眠は〜とか、そんなんじゃないんです。まぁ肩痛いのくらい治るかなとは思いましたけど、そもそもがオマエ、ソファーで寝てる時、枕さんは別な部屋にいるし。枕って高いよなぁ・・・毎年買うわけでもないんですけど、いざ買うと。Tシャツにも同じこと言えますが、Twitterでたまに見かける

「だって布だぜ?」

ですよ、ホントと。同じ値段のPCなら即買うのにねぇ。ま、買ったんですけど。

さて、やっとマシンとOSが整った!って感じまできました。どこがデータベースサーバーやねん!っていうのは、ここから払拭していきたいですね。さぁ、マリアちゃんのアザラシしばいていくぜ。

MariaDBのMariaは娘さんの名前らしいですね。そして、ロゴがアザラシなんです、別にマリアちゃんのアザラシではないようですけど。

ともあれ、MariaDBをとりあえず普通にぶち込みます。Bashコンソールの左側(PS1)今回から$にしますが、セットアップ時は基本的に、rootユーザーで作業します。sudo 使って一般ユーザーから操作するのは、セットアップ完了後の運用時からかな?普段は。

MariaDBインストール

# リポジトリ追加
$ curl -sS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | sudo bash

# MariaDB インストール
$ dnf -y remove MariaDB-*
$ dnf clean packages;
$ dnf -y install \
  MariaDB-server \
  MariaDB-client

# MariaDB 自動起動設定と起動
$ systemctl enable --now mariadb

# MariaDBへ接続確認
$ mariadb
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 3
Server version: 10.5.11-MariaDB MariaDB Server

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

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

MariaDB [(none)]> exit # ← 終了
Bye

CentOS8、今回は、AlmaLinux 8.4 を利用していますが、バージョンが10.3になります。用途によっては十分っていうのもあるかもしれませんが、MariaDBのバージョンはできるだけ最新版を入れて使っていきたいと思いますので、リポジトリの追加を行っています。

MariaDB 公式ページからリポジトリ設定ファイルを生成して自分で配置することもできますが、毎度のことながらダルいので、ワンライナーでぶち込みましょう、いい感じにスクリプトがリポジトリ設定をやってくれます。とりあえず、MariaDB-serverMariaDB-client をインストールします。他にもバックアップやら、この後説明するストレージエンジンなどのパッケージがありますが、今日に限っては使わないので、これだけです。

ストレージエンジン

MariaDB、およびMySQLもですが、プラガブル(抜き差し可能なプラグイン構造)なストレージエンジン機能があります。Pluggable Storage Engine Architecture​ っていうやつですね。実際にデータを保存するとき、どのような形式で保存するかはプラグイン形式で、設定することが可能になっています。

現在のMariaDBでの標準ストレージエンジンは、InnoDBです。ちょっと前(10年前後?)までは、MySQLの標準ストレージエンジンは、MyISAMだったんですが、こちらも現在InnoDBです。

利用可能なストレージエンジンは、以下のように確認することができます。

MariaDB [(none)]> show engines;

先程のインストールで、利用可能なストレージエンジンは、次のとおりです。

ストレージエンジン名説明
CSVCSVファイルにデータを保存
MRG_MyISAMMERGEストレージエンジン、複数のテーブルをマージして利用
MEMORYメモリ上にデータを保存
AriaMyISAMの代替、クラッシュセーフなMyISAM
MyISAMパフォーマンスが良い、フットプリントの小さなストレージエンジン
SEQUENCE自動採番専用テーブル用
InnoDBトランザクションが利用可能なデフォルトストレージエンジン
PERFORMANCE_SCHEMAサーバーパフォーマンス監視

これらが利用可能です。プラガブルなので、コレ以外のストレージエンジンを追加することができます。

  • OQGRAPH
  • S3
  • RocksDB
  • Connect
  • Spider
  • ColumnStore

パッケージインストールで追加するだけで、上記のストレージエンジンも利用可能です。また自分でストレージエンジンを開発して追加もできますし、野良ストレージエンジンも追加できます。

サーバー設定

MariaDBサーバーの設定は、/etc/my.cnf.d/server.cnf で行います。今回は下記のように設定しました。物理メモリ量などによってチューニングしていくんですけど、まぁ、今回はチューニング回じゃないので。

#
# These groups are read by MariaDB server.
# Use it for options that only the server (but not clients) should see
#
# See the examples of server my.cnf files in /usr/share/mysql/
#

# this is read by the standalone daemon and embedded servers
[server]

# this is only for the mysqld standalone daemon
[mysqld]

# ---------------------------------------------------
#  アクセス許可設定
# ---------------------------------------------------
bind-address = 0.0.0.0
# ---------------------------------------------------

# ---------------------------------------------------
#  文字セット・コード設定
# ---------------------------------------------------
character-set-server=utf8mb4
collation-server=utf8mb4_bin
# ---------------------------------------------------

# ---------------------------------------------------
#  SQLモード設定
# ---------------------------------------------------
sql_mode = 'ANSI_QUOTES'
# ---------------------------------------------------

# ---------------------------------------------------
#  パフォーマンススキーマ有効化
# ---------------------------------------------------
performance_schema = ON
# ---------------------------------------------------

# ---------------------------------------------------
#  イベントスケジューラー設定
# ---------------------------------------------------
event-scheduler = ON
# ---------------------------------------------------

# ---------------------------------------------------
#  テーブル定義キャッシュサイズ
# ---------------------------------------------------
table_definition_cache = 1024
# ---------------------------------------------------

# ---------------------------------------------------
#  InnoDB バッファプールサイズ設定
#  [物理メモリ] x 0.64
# ---------------------------------------------------
innodb_buffer_pool_size=40G
# ---------------------------------------------------

# ---------------------------------------------------
#  InnoDB ログサイズ
# ---------------------------------------------------
innodb_log_file_size = 2G
# ---------------------------------------------------

# ---------------------------------------------------
# InnoDB データファイルサイズ設定
innodb_data_file_path = ib_data:1G:autoextend
# ---------------------------------------------------

# ---------------------------------------------------
#  InnoDB データインデックス分割設定
# ---------------------------------------------------
innodb_file_per_table = 1
# ---------------------------------------------------

# ---------------------------------------------------
#  パケット最大サイズ設定
# ---------------------------------------------------
max_allowed_packet = 32MB
# ---------------------------------------------------

# ---------------------------------------------------
#  最大コネクション数設定
# ---------------------------------------------------
max_connections = 5000
# ---------------------------------------------------

# ---------------------------------------------------
#  ヒープ領域サイズ設定
# ---------------------------------------------------
max_heap_table_size = 2GB
tmp_table_size = 2GB
# ---------------------------------------------------

# ---------------------------------------------------
#  接続タイムアウト設定
# ---------------------------------------------------
connect_timeout = 300
# ---------------------------------------------------

# ---------------------------------------------------
#  インデックス未使用でのJOIN時に使用するバッファ
# ---------------------------------------------------
join_buffer_size = 4G
# ---------------------------------------------------

# ---------------------------------------------------
#  スレッドキャッシュ保持最大数
# ---------------------------------------------------
thread_cache_size = 5000
# ---------------------------------------------------

# ---------------------------------------------------
#  名前解決無効
# ---------------------------------------------------
skip-name-resolve = 1
# ---------------------------------------------------

# ---------------------------------------------------
#  スレッドプール設定
# ---------------------------------------------------
thread_handling = pool-of-threads
thread_pool_size = 32
# ---------------------------------------------------

#
# * Galera-related settings
#
[galera]
# Mandatory settings
#wsrep_on=ON
#wsrep_provider=
#wsrep_cluster_address=
#binlog_format=row
#default_storage_engine=InnoDB
#innodb_autoinc_lock_mode=2
#
# Allow server to accept connections on all interfaces.
#
bind-address=0.0.0.0
#
# Optional setting
#wsrep_slave_threads=1
#innodb_flush_log_at_trx_commit=0

# this is only for embedded server
[embedded]

# This group is only read by MariaDB servers, not by MySQL.
# If you use the same .cnf file for MySQL and MariaDB,
# you can put MariaDB-only options here
[mariadb]

# This group is only read by MariaDB-10.5 servers.
# If you use the same .cnf file for MariaDB of different versions,
# use this group for options that older servers don't understand
[mariadb-10.5]

保存して、MariaDBを再起動します。

$ systemctl restart mariadb

テストデータ作成

実際にMariaDBにデータを入れたり検索したりして、サーバーのパフォーマンスを計測したいのですが、何はともあれ入れるデータを作らなくてはなりません。sysbenchでパフォーマンスを図るのも良いのですが、できうる限り利用するテーブルに近い構造で確認を行いたいですね。

今回は、機械学習などで有名な、KagleOutbrain Click Prediction を利用してみたいと思います。

page_views.csv.zip をダウンロードしてきて展開します。なんと、そのサイズZIPの状態で37.3 GB。展開すると、94.9 GBです。構造を確認するためのpage_views_sample.csv.zip(148.51 MB) もありますので、先にこのpage_views_sample.csvの方を確認してみます。

vi などのテキストエディタで開いたら、でかすぎてマシン固まりますからね!!

$ head ~/Downloads/page_views.csv
uuid,document_id,timestamp,platform,geo_location,traffic_source
1fd5f051fba643,120,31905835,1,RS,2
8557aa9004be3b,120,32053104,1,VN>44,2
c351b277a358f0,120,54013023,1,KR>12,1
8205775c5387f9,120,44196592,1,IN>16,2
9cb0ccd8458371,120,65817371,1,US>CA>807,2
2aa611f32875c7,120,71495491,1,CA>ON,2
f55a6eaf2b34ab,120,73309199,1,BR>27,2
cc01b582c8cbff,120,50033577,1,CA>BC,2
6c802978b8dd4d,120,66590306,1,CA>ON,2

表にすると、こんな感じですかね。

uuiddocument_idtimestampplatformgeo_locationtraffic_source
1fd5f051fba643120319058351RS2
8557aa9004be3b120320531041VN>442

このデータを INSERT するプログラムを作り、ついでに INSERT性能を計測してやろうという魂胆です。プログラムはなんでもいいんですけど、PythonかPHPでいいかな〜ってことで、どっちも入れてないので、標準のPHPを入れちゃって MySQLi 経由でぶちこんじゃいますかね。

# PHPインストール
$ dnf -y install php php-mysqli

# PHP バージョン確認
$ php -v
PHP 7.2.24 (cli) (built: Oct 22 2019 08:28:36) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies

# MySQLi利用可能確認
$ php -i|grep mysqli
/etc/php.d/30-mysqli.ini,
mysqli
mysqli.allow_local_infile => Off => Off
mysqli.allow_persistent => On => On
mysqli.default_host => no value => no value
mysqli.default_port => 3306 => 3306
mysqli.default_pw => no value => no value
mysqli.default_socket => /var/lib/mysql/mysql.sock => /var/lib/mysql/mysql.sock
mysqli.default_user => no value => no value
mysqli.max_links => Unlimited => Unlimited
mysqli.max_persistent => Unlimited => Unlimited
mysqli.reconnect => Off => Off
mysqli.rollback_on_cached_plink => Off => Off
API Extensions => mysqli,pdo_mysql

なんか、使えそうですね。コマンドラインからの利用なので、とりあえずコレで。

次にテーブル設計を行います。標準のストレージエンジンであるInnoDBでテーブルを作成しますが、下記のようにしようと思います。created_at, updated_at は重複しますが、作成日時と、更新日時もたせるでしょ、どうせ。idをオートインクリメントにして、id、uuid、document_idをプライマリキーになるとしましょう。

CSVは、操作しているデスクトップマシンにダウンロードしたので、こいつをサーバーへ転送しておく必要があります。

scp ~/Downloads/page_views.csv 接続ユーザー名@mariadb1.vlue.io:/data

/dataは、いままでの設定だとrootユーザーしか書き込めません。なので必要に応じて書き込み権限を設定してください。別に/dataじゃなくてもいんですけど。

データベース・テーブル作成

何はともあれ、データベースを作成します、名前は mytest

CREATE DATABASE `mytest` CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_bin';

この mytest データベースに、テーブルを作成します、テーブル名は、page_viewsとしましょうか。

CREATE TABLE "page_views" (
  "id" int(11) NOT NULL AUTO_INCREMENT,
  "uuid" char(14) COLLATE utf8mb4_bin NOT NULL,
  "document_id" int(11) NOT NULL,
  "platform" tinyint(4) DEFAULT NULL,
  "geo_location" varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
  "traffic_source" tinyint(4) DEFAULT NULL,
  "created_at" timestamp NULL DEFAULT NULL,
  "updated_at" timestamp NULL DEFAULT NULL ON UPDATE current_timestamp(),
  PRIMARY KEY ("id","uuid","document_id") USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2000250001 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

とりあえず、これで入れる場所ができたので、ここにCSVを入れていくPHPを作成しましょう。

データ挿入プログラム作成

PHPの説明ではないので、プログラムコードだけ記載します。INSERT文は、そのままだとかなり遅いのでバルクインサートで行います。created_at = timestamp にする以外は、カラム名通りにデータ入れます。updated_atは、ON UPDATE current_timestamp()で、更新時のタイムスタンプで更新されるようにしました。まぁ、INSERT時は結局CURRENT_TIMESTAMP()を指定するんですけど。

んで、まずこの大量のデータをバルクインサートするため、そのSQL文を生成するPHPを作成しました。35万件、約32MBずつのINSERT文が書かれているSQLファイルが生成されます。その数、5812個

<?php

# ----------------------------------------------------------------------------
#  設定
# ----------------------------------------------------------------------------

# バルクインサートサイズ
$BULK_SIZE = 350000;

# CSVファイルパス
$CSV_FILE_PATH = '/data/page_views.csv';

# SQLファイル出力先パス
$SQL_OUTPUT_PATH = '/root/page_views';


# ----------------------------------------------------------------------------
#  メイン処理
# ----------------------------------------------------------------------------

# ファイルオープン
$fp = @fopen($CSV_FILE_PATH, "r");

if ($fp) {

    # カウンタ初期化
    $i = 0;
    $cnt = 0;

    # 挿入データバッファ初期化
    $buff = [];

    # 計測開始
    $start = microtime(true);

    # 1行ずつCSV解析をして読み込み
    while ($line = fgetcsv($fp)) {

        $i++;

        # 1行目はヘッダーなのでスキップ
        if ($i === 1) {
            continue;
        }

        # バッファへデータ追加
        $buff[] = $line;

        if (sizeof($buff) === $BULK_SIZE) {

            $values = [];

            foreach ($buff as $item) {
                $values[] = '  (' . implode(', ', [
                        "'" . $item[0] . "'"
                        , $item[1]
                        , $item[3]
                        , "'" . $item[4] . "'"
                        , $item[5]
                        , "'" . date("Y-m-d H:i:s", $item[2] + 1465876799998 / 1000) . "'"
                        , "CURRENT_TIMESTAMP()"
                    ]) . ')';
            }

            $query = implode(PHP_EOL, [
                PHP_EOL
                , " INSERT IGNORE INTO `page_views` ("
                , "     `uuid`"
                , "   , `document_id`"
                , "   , `platform`"
                , "   , `geo_location`"
                , "   , `traffic_source`"
                , "   , `created_at`"
                , "   , `updated_at`"
                , " )"
                , " VALUES"
                , implode(',' . PHP_EOL, $values)
            ]);

            file_put_contents($SQL_OUTPUT_PATH . '/insert_' . str_pad($cnt, 6, 0, STR_PAD_LEFT) . '.sql', $query);

            # カウンターインクリメント
            $cnt++;

            # 挿入データバッファ初期化
            $buff = [];
        }
    }

    if (sizeof($buff) > 0) {

        $values = [];

        foreach ($buff as $item) {
            $values[] = '  (' . implode(', ', [
                    "'" . $item[0] . "'"
                    , $item[1]
                    , $item[3]
                    , "'" . $item[4] . "'"
                    , $item[5]
                    , "'" . date("Y-m-d H:i:s", $item[2] + 1465876799998 / 1000) . "'"
                    , "CURRENT_TIMESTAMP()"
                ]) . ')';
        }

        $query = implode(PHP_EOL, [
            PHP_EOL
            , " INSERT IGNORE INTO `page_views` ("
            , "     `uuid`"
            , "   , `document_id`"
            , "   , `platform`"
            , "   , `geo_location`"
            , "   , `traffic_source`"
            , "   , `created_at`"
            , "   , `updated_at`"
            , " )"
            , " VALUES"
            , implode(',' . PHP_EOL, $values)
        ]);

        file_put_contents($SQL_OUTPUT_PATH . '/insert_' . str_pad($cnt, 6, 0, STR_PAD_LEFT) . '.sql', $query);


    }


}

# ファイルクローズ
fclose($fp);

echo "completed generate insert sql process." . PHP_EOL;

# EOF

先に、このrun_insert.php を作りました。phpをCLIで動作させ、1プロセスで実行してもいいんですが、8コア16スレッドあって暇してんやろ・・・・ってことで、子プロセス10個まで同時に実行するためのスクリプトです。

<?php

# ----------------------------------------------------------------------------
#  メイン処理
# ----------------------------------------------------------------------------

# プロセス数上限
$PROC_LIMIT = 10;

# カウンタ初期化
$cnt = 0;

# 実行プロセスID配列初期化
$pids = [];

# 計測開始
$start = microtime(true);

while ($cnt <= 5812) {

    # 実行プロセスリスト初期化
    $update_pids = [];

    # 実行中プロセス更新
    foreach ($pids as $pid) {

        # 実行結果初期化
        $output = [];

        # 実行コマンド作成
        $cmd = "ps h " . $pid;

        # 実行
        exec($cmd, $output);

        # 実行プロセスリストへ追加
        if (sizeof($output) === 1) {
            $update_pids[] = $pid;
        } else {
            echo sprintf('%0.10f', microtime(true) - $start) . PHP_EOL;
        }

    }

    $pids = $update_pids;

    # 実行中のプロセスが上限に達している場合スキップ
    if (sizeof($pids) >= $PROC_LIMIT) {
        usleep(1);
        continue;
    }

    # 連番作成
    $num = str_pad($cnt, 6, 0, STR_PAD_LEFT);

    # 実行コマンド作成
    $cmd = "php insert.php $num > /dev/null & echo $!";

    # 実行結果初期化
    $output = [];

    # インサートプロセス実行
    exec($cmd, $output);

    # PID追加
    $pids[] = $output[0];

    # インクリメント
    $cnt++;

}

echo "completed insert process." . PHP_EOL;

# EOF

次に、生成したSQLファイルを利用してINSERT処理をする、insert.phpを作成していきます。このPHPは単純です、引数に与えられた番号を利用して、SQLファイル名を特定して、読み込み実行しているだけです。

<?php

# ----------------------------------------------------------------------------
#  設定
# ----------------------------------------------------------------------------

# 接続先ホスト
$HOSTNAME = 'localhost';

# 接続ユーザー名
$USERNAME = 'root';

# 接続パスワード
$PASSWORD = 'password';

# 接続データベース
$DBNAME = 'mytest';

# SQLファイルパス
$SQL_PATH = '/root/page_views';

# 連番取得
$num = $argv[1];

# データベース接続
$mysqli = new mysqli($HOSTNAME, $USERNAME, $PASSWORD, $DBNAME);

# データベース接続エラー処理
if ($mysqli->connect_error) {
    echo $mysqli->connect_error;
    exit();
}

# キャラセット設定
$mysqli->set_charset("utf8mb4");

# SQL取得
$query = file_get_contents($SQL_PATH . '/insert_' . $num . '.sql');

# SQL実行
$mysqli->query($query);

// DB接続を閉じる
$mysqli->close();

# EOF

では、早速実行。バックグラウンド処理で実行し、run_insert.phpで経過時間を出力しているので、その経過時間をinsert.logに吐き出します。

$ php run_insert.php > insert.log &
$ tail -f insert.log

tailコマンドなどでログ出力をみれば、進んでるかどうか確認できますし、

$ ps -aux | grep php

として、処理中のプロセス、引数の番号で進み具合も確認できます。

完了したら、登録件数を確認してみましょう。

MariaDB [(none)]> use mytest;
MariaDB [mytest]> SELECT COUNT(*) FROM page_views;

どうでしょうか、ちゃんと入りましたかね。しっかし、COUNT取るだけで12分ですって、コレくらいの件数になったときに、いかにInnoDBが集計系の処理が苦手か、わかりますねぇ。

再び実行するとキャッシュが効くので高速化されますが、それでも5分かかってます。ま、あきらメロンって感じですね。この件数とCSVの件数を照らし合わせたいので、CSVファイルの行数を取得します。

$ wc -l /data/page_views.csv
2034275449 page_views.csv

1行目がヘッダーなので、件数ピッタリOKですね。

約20億レコード!!

いやぁ〜時間かかるわ、ぶっこむの。ログを確認すると、僕のマシンで3時間(10922.3823790550秒)でした。まぁこれでも爆速だと正直思います、バルクINSERTじゃなかったり・・・とか遅くなる要素結構減らしたので。初期設定のままだと、計算上3日以上かかる速度で挿入はじめましたからね。20億レコードが3時間、このスペックでっていう一つの指針にはできてサイジング計算できるから、単純計算ではないせよ。100万件だと15時間ってことですよね、InnoDBのディスク書き込みによる一時的なパフォーマンス低下とかも、この3時間の中に含まれてますし、そもそもMariaDBのテーブルに20億件いれねーよっていわれたらそれまでなんですが、仕事だと普通に有りえたりしますんで・・・

INSERT性能計測

先のINSERTプログラムで、ログを出力していました、その結果を確認してみましょう。合わせて、sarコマンドでロードアベレージの変化も確認します。上記にも記載下通り、3時間程度で、秒間 186,248 件平均で挿入できてるってことですね。今回のINSERTはまとめて入れるときの話なんで、実運用のときはチマチマいれていくんですけど、秒間18万件ってスゴイ。でも、その速度じゃないと20億終わらんよね、3時間で笑

10890.9318001270
10892.8104751110
10893.5190300941
10895.9931759834
10898.3707320690
10901.6219120026
10905.0063550472
10906.6536550522
10907.3238840103
10910.6990981102
10910.9942960739
10913.0309159756
10913.8176531792
10916.4012670517
10918.7953150272
10922.3823790550
completed insert process.

余裕ある感じで推移していたみたいですね、概ね満足かな。もーちょい行けそうな気がしないでもないけど、特にI/O。

SELECT性能計測

COUNT取るだけで、12分もかかってたので、正直使い物にならないんですけど、さらにランキング表示を行ってみたいと思います(無謀)

SELECT
( SELECT count( DISTINCT document_id ) FROM page_views b WHERE a.document_id < b.document_id ) + 1 rank 
FROM
page_views a 
ORDER BY
rank ASC

・・・・・。

・・・・・・・・・。

全然結果でないwwww 1時間は待ったぞ?まぁ、当然遅いですよね(あきらめきれてないメロン)

ヤメだ、ヤメ!!!笑

おわりに

なんとか、20億件という大量のレコードを挿入することに成功しました。昔MySQLで大量に入れてみることをやったときは、1000万件でも無理〜みたいな記憶がありまして、正直感動レベル高いです。性能上がってんだろうなぁ、ハードウェアの。まだ1台構成なんで、最終目標の3台構成まで完成したときには、まだまだ増やしてもいけるのではないのか?目指せ100億件!とかできんのかなぁ〜・・・その前にデータ生成ツール作らないと無理や。

前述どおり、こんなに入れることはあまりないかもしれませんが、大は小を兼ねるので、この件数でしっかり使える構造になっていれば、少なければ早くなりますよね。当然。

んで、使えるレベルっていう意味では、集計処理に関しては、全く使いのにならないレベルです。いや、コレでも使うっていう手もなくはないですけど笑

集計処理が弱いのは、予想通りなので、コレを次回 ColumnStore を使ってみて解決できるかどうかにチャレンジしてみたいと思います。ちなみに、多分できるんだろうなって思い込んでるだけで、検証終わってるわけでもなく、着手すらしてません笑

ほんと地道だなぁ・・・書き出して検証結果まとめたりするのって。

ってことで、まだ続く?

最新の更新を
プッシュ通知で購読しよう