Quantcast
Channel: なからなLife
Viewing all 174 articles
Browse latest View live

Kindle、EPUB、PDF使い勝手

$
0
0

今更ながら、初めてEPUB電子書籍を買ってみたんですよ

直前のエントリで上げた「Amazon Web Services負荷試験入門」を読むにあたって。


過去に「O'Reilly Japan Ebook Store」でPDF電子書籍を買ったことはありましたが、EPUBは買ったことがありませんでした。


個人的に、紙の本からKindle中心にシフトし始めたところで、この本もKindle版が提供されていたのですが、今回は初めて技術評論社の直販電子書籍(Gihyo Degital Publishing)経由で「PDF・EPUBセット」を購入しました。デバイスを縛るようなDRMがないEPUBとPDFがセットになって1冊分のお値段、というところに惹かれました。


そんなわけで、初のEPUB形式、素直に技術評論社さんのサイトのヘルプに従ってChrome+Readiumで読みましたが、リフローもきれいでレイアウトの崩れもなく、なんら支障なく読み進めることができました。
EPUBリフロー読みやすいぃぃ。(まあKindleと同じなんだけど)


iPhoneの場合、EPUBiBookに取り込む形になります。


PDF版は当然固定レイアウトということもあり、数回しか開きませんでした。特にiPhoneで読むときがツラい。。。
たぶん、それなりのサイズのディスプレイとかタブレットなら、気にならないんでしょうけどね。



EPUB、PDFのセット売りされていなかったら、リスク回避でKindle版で買っていただろうなと。
そして、Kindle版を出してないけどEPUBが出ているような他の電子書籍について、この先も見向きもしなかったんだろうなと。



それでもKindleが優れているトコロ

EPUBKindleに劣るのは、デバイス間で読み進めたページ位置を共有できないことですね。
持っているデバイスすべてに複製して読んでいましたが、スマホ+自宅のWindows+会社のWindowsKindle端末持ってない!)で電子書籍を開く私にとって、この辺Kindleは非常に優れているなと。
PDFやEPUBからKindleフォーマットに変換したmobiファイルをKindleに取り込むことは出来ますが、クラウド同期はできないので一緒ですね。


リーダーソフト次第では対応できるのでしょうか?



DRMどうなってんの?

Kindleという専用リーダーの閉じた世界と違って、PDF/EPUB版はオープンな規格のファイルで流通するのですが、それなりに保護ルールがあります。「デジタル著作権管理(Digital Rights Management、DRM)」ってヤツです。


技術評論社さんの電子書籍は、

Q:電子コンテンツにDRMはかかっていますか?
A:Gihyo Digital Publishingで購入された電子コンテンツには,いわゆるDRMと呼ばれている利用や複製を制限するような機構は入っていませんが,購入されたユーザ様を識別できるようなユニークIDとメールアドレス等の個人情報を付加しております。
https://gihyo.jp/dp/help/reading/drm

Q:電子書籍はいくらでもコピーして構いませんか?
A:当サイトで販売されている電子書籍は一部の例外を除き,コピーガードの機能を付加していません。個人としてのご利用の範囲内であれば,コピーしてご利用いただいて結構です。著作権は執筆者にありますので,許可を得ない第三者への配布などはご遠慮ください。不正な利用が見つかった場合は必要な措置を執らせていただく場合があります。
https://gihyo.jp/dp/help/reading/aboutcopy

Q:電子書籍の回し読み,貸し借りはできますか?
A:電子書籍はその性質上,他の人と共有したり,貸し出したりできません。目的によらずコピーして第三者に渡すことは不正利用となりますのでご注意ください。不正な利用が見つかった場合は必要な措置を執らせていただくことがあります。
https://gihyo.jp/dp/help/reading/rental


ということです。


なお、識別情報ですが、目に見えて分かる形では入っていないようです。
昔買ったO'ReillyのPDFは、フッタに購入者のメールアドレスが見える形で埋め込まれていましたが、今はO'Reillyもそうなってなさそうですね。


変なファイル流通の傾向が見えたら、そのファイル拾って解析して流出元を特定する、という感じかなと、勝手に推測しています。


乱立する電子書籍サービス、いろんなプラットフォームに散らしたくはないけれど、選択肢としてEPUBは「大いにアリ」だなと思いました。


Fire HD 8 タブレット (Newモデル) 16GB、ブラック

Fire HD 8 タブレット (Newモデル) 16GB、ブラック


MySQLの「CREATE TABLE .. SELECT/LIKE」の挙動の違い

$
0
0

テーブルの複製のとき、お世話になります。

ちょっとしたバックアップ目的や、検証作業用にテーブルを複製するシーンはよくあります。
MySQLでは、テーブルの複製のためのコマンドが大きく2種類あるので、その違いを確かめた結果のお話です。

  • CREATE TABLE 複製先テーブル SELECT .. FROM 複製元テーブル
  • CREATE TABLE 複製先テーブル LIKE 複製元テーブル

MySQL 5.6.38で動かしていますが、バージョンにはあまり依存しない挙動となっているはずです。

一覧表

MySQL :: MySQL 5.6 リファレンスマニュアル :: 13.1.17 CREATE TABLE 構文

あたりを読めば文章で書いてある話なのですが、ざくっと一覧化します。

コマンド列定義省略列定義変更KEY継承AutoIncrement定義継承ストレージエンジン継承データ継承
CREATE TABLE .. SELECT不可不可不可(指定可)
CREATE TABLE .. LIKE不可不可

列定義関係

SELECT

省略すればSELECTで引っ張ってきた列定義をそのまま使うし、明記することで、複製元とは異なる定義を使うこともできます。
ただし、複製元に定義されていたAutoIncrement定義やKEY(およびIndex)は継承されません。


LIKE

複製元の構造そのまま複製するコマンドなので、異なる定義は使えません。
列そのものの増減や型指定の変更は、複製後にALTERする必要があります。

そのかわり、AutoIncrement定義やKEY(およびIndex)も継承されます。

ストレージエンジン

SELECT

省略すると、システム変数「default_storage_engine」に指定されているストレージエンジンでテーブルを作ります。
複製元とは異なるストレージエンジンを指定することも可能です。
列定義は省略しつつ、ストレージエンジンだけを指定することも可能です。(さっき知った)

LIKE

複製元と同じストレージエンジンでテーブルを作ります。
複製元と異なるストレージエンジンを指定することはできません。必要であれば後でALTERで対応します。

データ

SELECT

テーブル作成と同時にデータも複製されます。
というより、SELECT文で好きなように加工、WHEREによる絞込等、柔軟な制御が可能です。


Oracle脳の人は、テーブル定義だけ複製しようとする時に、「WHERE 1<>1」とか使いますね。

LIKE

データは複製されません。あくまで構造だけを複製します。

実験結果

「default_storage_engine=InnoDB」な環境に、MyISAMな複製元テーブルを作って、

  • hoge_select:CREATE TABLE .. SELECTで複製
  • hoge_like:CREATE TABLE .. LIKEで複製
  • hoge_select_engine:CREATE TABLE .. SELECTかつENGINE変更

の3ターンを実行した時の、定義とデータの出来上がり方が確認できます。

mysql> select@@default_storage_engine;
+--------------------------+
| @@default_storage_engine |
+--------------------------+
| InnoDB                   |
+--------------------------+
1rowin set (0.00 sec)

mysql> CREATETABLE `fuga` (
    ->   `id` INT(11) NOTNULLAUTO_INCREMENT,
    ->   PRIMARYKEY (`id`)
    -> ) ENGINE=MyISAM;
Query OK, 0rows affected (0.07 sec)

mysql> INSERTINTO fuga values ();
Query OK, 1row affected (0.03 sec)

mysql> SELECT * FROM fuga;
+----+
| id |
+----+
|  1 |
+----+
1rowin set (0.00 sec)

mysql> CREATETABLE hoge_select SELECT * FROM fuga;
Query OK, 1row affected (0.12 sec)
Records: 1  Duplicates: 0  Warnings: 0

mysql> SHOWCREATETABLE hoge_select\G
*************************** 1. row ***************************
       Table: hoge_select
CreateTable: CREATETABLE `hoge_select` (
  `id` int(11) NOTNULLDEFAULT'0'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
1rowin set (0.00 sec)

mysql> SELECT * FROM hoge_select;
+----+
| id |
+----+
|  1 |
+----+
1rowin set (0.00 sec)

mysql> 
mysql> CREATETABLE hoge_like LIKE fuga;
Query OK, 0rows affected (0.00 sec)

mysql> SHOWCREATETABLE hoge_like\G
*************************** 1. row ***************************
       Table: hoge_like
CreateTable: CREATETABLE `hoge_like` (
  `id` int(11) NOTNULLAUTO_INCREMENT,
  PRIMARYKEY (`id`)
) ENGINE=MyISAMDEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
1rowin set (0.00 sec)

mysql> SELECT * FROM hoge_like;
Empty set (0.00 sec)

mysql> CREATETABLE hoge_select_engine ENGINE=MyISAMSELECT * FROM fuga;
Query OK, 1row affected (0.05 sec)
Records: 1  Duplicates: 0  Warnings: 0

mysql> SHOWCREATETABLE hoge_select_engine\G
*************************** 1. row ***************************
       Table: hoge_select_engine
CreateTable: CREATETABLE `hoge_select_engine` (
  `id` int(11) NOTNULLDEFAULT'0'
) ENGINE=MyISAMDEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
1rowin set (0.00 sec)

mysql> SELECT * FROM hoge_select_engine;
+----+
| id |
+----+
|  1 |
+----+
1rowin set (0.00 sec)

まとめ

・CREATE TABLEのでのテーブル複製手段には、SELECT(データも複製)とLIKE(構造だけ複製)とがある。
・それぞれ、できること/できないことがある。
・うまいこと使い分けよう。

MariaDB&MySQL全機能バイブル

MariaDB&MySQL全機能バイブル

MySQLのGLOBAL_STATUSとGLOBAL_VARIABLES

$
0
0

今更MySQL5.7を扱うにあたって

MySQL5.6とMySQL5.7のパラメータ差分をあらためて見直してたら、「show_compatibility_56」という、プロダクトのアーキ移行期間にありがちな「いかにも」な名前のパラメータがありまして。


これは何?

「SHOW [GLOBAL] STATUS」や「SHOW [GLOBAL] VARIABLES」という、とてもお世話になるコマンドがあり、その実、テーブルに格納されている値を表示していたわけで、テーブル*1があることから、SELECT文を使うことでSHOWコマンドよりもより柔軟な条件式などを使って参照ができたわけです。

そんなテーブルたち、MySQL5.6まではInformation_Schemaにあったのが、MySQL 5.7からはPerformance_schemaに移動するぞ、ってドキュメントに書いてあります。
その説明のためにまるまる1節とってあります。
https://dev.mysql.com/doc/refman/5.7/en/performance-schema-variable-table-migration.html
MySQLの公式は5.7以降の日本語ドキュメントが存在しないので、ちょっとツライ。Google翻訳で頑張った。)


これがことの始まり。


なんで格納先スキーマが変更になったのか、なんでオプション扱い(とはいえデフォルト有効)な「Performance_schema」に移したのか、理由はよくわかりません。
が、事実として、移動しているわけです。


で、MySQL5.6以前に慣れている人は、身体に染み付いたInformation_schemaへの問い合わせを投げて、MySQL5.7以降だとエラーになるわけです。


そんなとき、このオプション「show_compatibility_56」をONにしてあげると、Information_schemaにあるテーブルをSELECTしてもエラーにならずに値を返してくれます。


あれ、performance_schemaってOffったらどうなるの?performance_schemaのOn/Off関係なく、Information_schemaには従来通りテーブルある?

いろいろ疑問が湧いてくるので、調べてまとめてみました。

パラメータと挙動の対応関係表

以下の表のようになっています。

performance _schemashow _compatibility_56Information _schemaへのSELECTPerformance _schemaへのSELECTSHOWコマンド
ononOKOKOK
onoffNGOKOK
offonOKOKOK
offoffNGOKOK


システム変数「performance_schema」がONであれOFFであれ、「Performance_schemaへのSELECT」と「SHOWコマンド」は利用可能です。

システム変数「show_compatibility_56」がONであれば、Information_schemaへのSELECTは利用可能ですし、逆にOFFなら、一切利用できません。


なお、NGの際のエラーメッセージは、「Unknown table...」ではなく、「...future is disabled; see the documentaion for 'show_compatibility_56'」となります。


show_compatibility_56=OFFの場合、SHOWコマンドでは取得できない値がある

公式ドキュメントの同じページに書いてあります。

The Performance Schema does not collect statistics for Com_xxx status variables in the status variable tables. To obtain global and per-session statement execution counts, use the events_statements_summary_global_by_event_name and events_statements_summary_by_thread_by_event_name tables, respectively.
https://dev.mysql.com/doc/refman/5.7/en/performance-schema-variable-table-migration.html

超訳「Com_xxx statusとれねーよ。events_statements_summary_global_by_event_name とか events_statements_summary_by_thread_by_event_name 使ってね!」


というわけで、どうなっているのか見てみて見たら、まあわけわかんねえ。

mysql> select@@version,@@performance_schema,@@show_compatibility_56;
+-----------+----------------------+-------------------------+
| @@version | @@performance_schema | @@show_compatibility_56 |
+-----------+----------------------+-------------------------+
| 5.7.20    |                    1 |                       1 |
+-----------+----------------------+-------------------------+
1rowin set (0.00 sec)

mysql> select * from information_schema.global_status;
(略)
361rowsin set, 1 warning (0.01 sec)


mysql> select * from performance_schema.global_status;
(略)
206rowsin set (0.00 sec)


mysql> showglobalstatus;
(略)

361rowsin set (0.00 sec)



mysql> select@@version,@@performance_schema,@@show_compatibility_56;
+-----------+----------------------+-------------------------+
| @@version | @@performance_schema | @@show_compatibility_56 |
+-----------+----------------------+-------------------------+
| 5.7.20    |                    1 |                       0 |
+-----------+----------------------+-------------------------+
1rowin set (0.00 sec)

mysql> select * from information_schema.global_status;
ERROR 3167 (HY000): The 'INFORMATION_SCHEMA.GLOBAL_STATUS' feature is disabled; see the documentation for'show_compatibility_56'

mysql> select * from performance_schema.global_status;
(略)
206rowsin set (0.00 sec)

mysql> showglobalstatus;
(略)
353rowsin set (0.00 sec)

件数は、performance_schemaがOnでもOffでも同じでした。


で、Performance_schemaにCom_xxx系の行がないのは、まあ理解した。
しかし、SHOW コマンドの出力結果が8件少ない。。。


比較してみたトコロ、この件数の差分は
Compression
Last_query_cost
Last_query_partial_plans
Slave_heartbeat_period
Slave_last_heartbeat
Slave_received_heartbeats
Slave_retried_transactions
Slave_running
でした。


で、本題の、Com系なんですが。。。大量にでてくるので結果出力は省略しました。カウンタが動いていないからと言って、表示されないわけではなさそうです。
Performance_schemaをSELECTした時は、Com_XXXは1行しか出てきません。これが、Information_schemaやSHOWコマンドに比べて大幅に件数が少ない原因。

で、項目としてはCom_XXXも全部出力されるSHOWコマンドについては、正直良くわからず。少なくともCom_Selectや、Com_show_statusなど、この確認の際に使ったコマンド関連のカウンタは、show_compatibility_56の状態に関係なくカウントアップしてます。
ドキュメント上、明確にどれが機能しなくなるって書いてないので、しばらく回してみないとわからないのかな。


いずれにせよ、show_compatibility_56はそのうち意味がなくなるよ!って宣言されてるパラメータなので、新仕様前提で扱えるようにしたいものです。

まとめ

  • MySQL 5.6とMySQL 5.7で、SHOW STATUS/VARIVLESの取得元となるテーブルの場所がInformation_SchemaからPerformance_Schemaに変わってる。
  • SHOW STATUS/VARIVLESの代わりに、テーブル直接参照のSELECT使えるけど、MySQL 5.7からはInformation_Schemaを見に行くとエラーになるから、Performance_schemaを見よう。
  • MySQL 5.7に関しては、「show_compatibility_56=ON」すると、Information_Schema側をSELECTしてもエラーにならなくなる。
  • SHOW [GLOBAL] {STATUS | VARIABLES}使えば、システム変数performance_schemaやshow_compatibility_56の状態に関係なく通る。
  • Information_Schema.GLOBAL_STATUSでカウント取れた「Com_XXX」の値は、Performance_schemaでは取れなくなる、ってドキュメントに書いてあるけど、詳細わからんw

そして

  • 今更感があろうが、調べたことはアウトプットする!


とりあえず、MySQL 5.7について新機能・変更点をキャッチアップするのに、この本が大活躍してます!マジおすすめ!

詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド (NEXT ONE)

詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド (NEXT ONE)

*1:公式ドキュメントでは「INFORMATION_SCHEMA データベースには複数の読み取り専用テーブルが含まれます。これらには実際にはビューがあるので、関連付けられたファイルはなく、トリガーは設定できません。また、その名前を持つデータベースディレクトリもありません。」と書いてありますが、SHOW CREATE TABLEを見ると「CREATETABLE TEMPORARY TABLE ... ENGINE=MEMORY」と表示されます。

MySQL 8.0をカジュアルに立ちあげる

$
0
0

この記事はMySQL Casual Advent Calendar 2017 - Qiitaの2日目のエントリとなります。

きっかけ、前提

MySQL8.0、まだRCの状態だけど、色々な機能追加の話題が盛り上がっている中でのAdvent Calendarでもあるので、MySQL 8.0新機能検証なネタを投下してくれる人も多いんじゃないかと思います。|д゚)チラッ

そこで、インストール手順自体は、何番煎じだよ、って話でもあるのですが、新機能検証記事を読んで「試してみたい!」ってときに、簡単に試せる環境ほしいよね、ってことで、このタイミングでエントリ投下することにしました。

正直なトコロ、MySQL 5.7を立ち上げる時と、ちょっとした差しかないです。


今時だとコンテナだなんだかんだあるかと思いますが、今回は頼らず、ピュアにOS立ち上げて、って手順にします。
以降、作業はVirtualBoxで、vagrantも使わずやってます。


あと、ゴリゴリやる人はソースも合わせて読むので、ソースから立ち上げるでしょうけど、カジュアルに立てたいので、yum使います。
やっぱyumらくちーん。

OS準備~mysqld起動まで

VituralBoxに(じゃなくてもいいけど)、CentOS 6.xをminimalでインストール。
執筆時点だと、最新かつ6.x系最終の6.9ですね。7.x系でもいいと思います。

AWS/EC2のAmazonLinuxも6.xと同じ操作系なので6.xを選んでいます。


CentOSのiso(CentOS-6.9-x86_64-bin-DVD1.iso)を食わせるとインストーラGUIが起動するので、その途中でネットワークは設定しておきましょう。
カジュアルに立てるには、余計な作業は省きたいです。

それ以外は、ちょっと触りたいだけならば、ほぼデフォでいいと思います。
仮想マシンへ割り当てるCPU数、メモリ、ディスク、および、OS内で設定するswap割り当てあたりはお好みで。

インストーラでの作業が終了し、OSが起動したらログインして、以降rootで作業とします。

yumで初期状態作る
yum -y update
yum -y wget

これだけで十分です。他に必要なライブラリは、MySQLyum installするときに、依存性解決で一緒に持ってきてくれますから。

iptables

ローカルホスト上での操作しかしない場合、この作業は不要です。
でも、リモートでアクセスしたい(外からmysqlコマンドラインjdbcMySQL Workbench等でつなぎたい)場合、TCP:3306ポートが塞がれているので、カジュアルにiptablesをOffります。

chkconfig iptables off
service iptables stop

VirtualBoxじゃなくて、AWS/EC2でやる場合、セキュリティグループのInboundでTCP:3306を許可する設定を忘れないように。


VirtualBoxで作業を続ける場合、テキスト画面にコピペができないので、ここから先はWindowsならTeraterm等、コピペできるsshクライアントで接続して作業することを推奨します。

MySQLyumリポジトリ取得

MySQL 8.0をインストールしようとしているけど、リポジトリrpmファイル名は「mysql57-」です。(執筆時現在)

wget https://dev.mysql.com/get/mysql57-community-release-el6-11.noarch.rpm
yum -y localinstall mysql57-community-release-el6-11.noarch.rpm
yumリポジトリの設定ファイルを修正

デフォのままだとMySQL5.7がインストールされるので、MySQL 8.0だけが有効になるように編集します。

vi /etc/yum.repos.d/mysql-community.repo

[mysql57-community]と[mysql80-community]のブロックで、それぞれ、「enabled」のトコロの「0」「1」を変えてあげます。
(以下は編集後の状態)

[mysql57-community]
name=MySQL 5.7 Community Server
baseurl=http://repo.mysql.com/yum/mysql-5.7-community/el/6/$basearch/
enabled=0
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-mysql


[mysql80-community]
name=MySQL 8.0 Community Server
baseurl=http://repo.mysql.com/yum/mysql-8.0-community/el/6/$basearch/
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-mysql
yumからインストール&初回起動
yum -y install mysql-community-server
service mysqld start

初回起動の時点で、初期化処理が走ります。

なお、MySQL 5.7と同様、自動起動設定(chkconfig)は、ONにされてます。

初回ログイン

MySQL 5.7と同様、rootの初期パスワードがログの中にこっそり吐かれますので、拾ってあげます。
目で全部追いかけるとツライので、grepで拾ってきます。

grep'temporary password' /var/log/mysqld.log
mysql -u root -p
Enter password:<B>ここで、拾ってきた初期パスワードを入力</B>
rootパスワード変更

MySQL 5.7と同様、初期パスワードを変更するまでは、ログインはできても他のコマンドを受け付けてもらえません!


しかも、パスワードポリシーが有効化されているので、大小英文字、数字、記号を含むパスワードしか受け付けてくれません。
めんどくさいのはOracleさんも承知で、公開されているトレーニング資料にも書いてあります。
それに倣って、パスワード文字列は「MySQL_80」を使います。

mysql> SET PASSWORD = 'MySQL_80';
リモートでもrootで入れるように

iptablesと同様、ローカルホストのみでの作業なら不要です。
MySQL Workbench使うよね!今のバージョンのMySQL WorkbenchでもMySQL 8.0を扱えます。

mysql> create user 'root'@'%'identifiedby'任意のパスワード';
mysql> grantallon *.*  to'root'@'%';

一応、ログイン試験してみてね。
パスワード入力要求の返事が来ない場合、ネットワークのドコかで塞がれてる。。。

おまけ-1:my.confの場所

MySQL 8.0に限った話ではないですが、MySQLは色んな場所でパラメータ設定できるけど、yumで構築したときは、ここだけ押さえておけば良いです。

/etc/my.cnf

初期の適用状態は、「SHOW GLOBAL VARIABLES;」投げるとか、もっとカジュアルに確認するならMySQL Workbenchの「Status and System Variables」から見るとよいです。

おまけ-2:パスワードポリシー無効化

これもMySQL 5.7からの話なのですが、パスワードポリシーは以下のコマンドで無効化できます。

mysql> uninstall plugin validate_password;

これで、ゆるゆるな文字列でパスワード設定できます。

おまけ-3:MySQL 5.7までのキャッチアップ

こちらの本が、MySQL 5.7までのキャッチアップには最適かなと思います。
電子書籍版も出てます。

やさしく学べるMySQL運用・管理入門【5.7対応】

やさしく学べるMySQL運用・管理入門【5.7対応】

MySQL 5.7から触り始める人にも優しい本。MySQL 8.0に追加される予定のネタもちょいちょい出てきます。

MySQL 5.6まではわかってる人が、MySQL 5.7で増えた機能をキャッチアップすることを主眼にした本。こちらもMySQL 8.0に入る前に読んでおくとよいです。

さいごに

以上、カジュアルにMySQL 8.0なサーバーが1台立ち上がりました。
カジュアルに立ち上げる方法が分かっていれば、カジュアルに壊すのも怖くない!

MySQLでよくわからないことがあったら、
mysql-casualのSlackに投げると、きっといいことあるよ!


なお、今年のAdvent Calendarのエントリには入れてないですが、
MySQL 5.6をカジュアルに立ちあげる」
MySQL 5.7をカジュアルに立ちあげる」
もありまぁす。

MySQLの「Communications link failure...」の解決方法を調べていて、よく見つけるヤツ

$
0
0

この記事はMySQL Casual Advent Calendar 2017 - Qiitaの5日目のエントリとなります。

みんなだいすきお困りの、アレです。

Oracleだと「ORA-03113:通信チャネルでファイルの終わりが検出されました。」っていう、酷い日本語*1有名なヤツがあるけど、MySQLだとコレが該当するんじゃないでしょうかね。


(1)interactive_timeoutを増やせ

https://ja.confluence.atlassian.com/confkb/attachment-upload-failed-with-communications-link-failure-during-commit-error-250609761.html
とかね。

このパターンに当てはまるのは、
1.トランザクション開始
2.なんかSQL発行して、正常終了
3.しばらく放置
4.Commit発行
で、3と4の間にinteractive_timeoutに設定した秒が経過したとき。


サーバ側:何もしてこないからブチっとしたよー
クライアント側:接続キレてるはずのコネクションオブジェクトを(プログラム内で)つかみっぱなし、そのオブジェクトのCommitメソッド発行

ってなると、接続キレてますよ、って怒られるパターン。


TomcatでConnection Pooling使ってる場合、定期的に検査用SQLを投げる設定とか入れられるので、これをうまく使いましょう。

(2)net_read_timeout/net_write_timeoutを増やせ

サーバーから見て、ネットワーク越しに送られてくるデータを読む(read)、ネットワーク越しにクライアントに返すデータを書く(write)際の時間上限設定です。
リクエスト受け取って処理している時間は含まないはず。


大量データの往来をやろうとすると、コレに引っかかるか、max_allow_packetに引っかかるか。
タイムアウト伸ばしていいなら、増やせばいいし、そうでないなら、メモリの利用状況を鑑みつつmax_allow_packet引き上げで対処。

(3)innodb_log_file_sizeを増やせ

http://fa11enprince.hatenablog.com/entry/2015/03/23/011206
とか。

日本語記事なので、そのまま読めばいいのですが、innodb_log_file_sizeが小さすぎるところに、短い時間で大量更新を行うと、ファイル溢れが起きるわけで、mysqldが死ぬことがあるようです。

そしたら、クライアントセッション全部切れるので、そのセッション持ってるオブジェクトをプログラム内で使い続けたら、、接続キレてますよ、って怒られるパターン。

MySQLのエラーログにかかれているはずのものなので、何かあったら最初にエラーログを確認することがいかに大事であるか伝わってきます。


(4)クライアント側(ex:jdbcドライバ)の「socketTimeout」を増やせ

記事ではあんまり出てこないけど

The last packet sent successfully to the server was XXXX milliseconds ago. The driver has not received any packets from the server.

とか

he last packet successfully received from the server was XXXX milliseconds ago. The last packet sent successfully to the server was XXXX milliseconds ago.

が出る時って、おそらくクライアントサイドのタイムアウト設定によって「諦めた」時。


Connector/Jの場合、この辺読んでおくと良いですね。
https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-reference-configuration-properties.html


jdbcドライバで設定してない場合だと、OSレベルのTCPタイムアウトに引っかかっている可能性あり。


StackTraceを隠蔽(怒)せずにログにちゃんと吐いていれば、
Caused by: java.net.SocketTimeoutException: Read timed out
とか出るよね。


根本原因は、色々あったりします。
NW自体の遅延。元々の距離の問題とか、輻輳とか、中継機器の一時的なトラブルとか。
MySQLサーバ内の処理が「純粋に重い」。MySQLはそもそも複雑なSQL苦手だし、同じソースを通る処理の多重度が高いとセマフォで詰るし。
MySQLサーバで使用しているストレージ側のトラブル。RAIDコントローラとか。
意図せずSWAP使ってたりするときも、この手のエラー出るかも。


その他

まだMySQL 5.7はガチってないんだけど、SELECTを経過時間で強制終了させる
execution_timeout
もハマりそうな予感したので試してみましたが、ここは別のエラーになりますね。

プログラム側でうまいことエラーハンドリングしてあげてください。

execution_timeoutに絞った話は、MySQL Casual Advent Calendar 2017 - Qiitaの9日目の記事として書きました。
http://atsuizo.hatenadiary.jp/entry/2017/12/09/000000




まとめ

  • 接続不良の原因、いろいろあるよね。
  • 対応するパラメータも色々あるよね。
  • いずれにせよ、コネクション切れたのに、コネクションオブジェクトを掴みっぱなしで使い続けようとすると起こるパターンが多いよね。
  • MySQLサーバ側のエラーログは最初に見よう。ここに出力がなければクライアント側メインで調整しつつ、タイムアウト設定確認の中で再びMySQLサーバに戻る感じで。
  • タイムアウト系の設定値、伸ばせば確かに発生頻度減るけど、設計的にそれでいいのかは別問題。業務・アプリ側の要件の次元。
  • アプリ実装に依存して引き起こされてることも多いし、一概にDBのせいにされても困る。


ここまでまとめたはいいけれど、目下、不定期に発生中の障害が解決できてないです。。。つらい。

MySQLトラブルシューティング

MySQLトラブルシューティング

*1:英語の時点でわりと酷い

SELECT文をタイムアウト強制終了させる「MAX_EXECUTION_TIME」使ってる?

$
0
0

この記事はMySQL Casual Advent Calendar 2017 - Qiitaの9日目のエントリとなります。

実行が長引いたSELECT文を強制終了させるヤーツ

MySQL5.6まで、正常に処理が進んでいて遅いSELECTをタイムアウトさせるシステム変数はありませんでした。


正常に処理が進んでいない時のパラメータだと

  • lock_wait_timeout:メタデータロック取得待ち
  • innodb_lock_wait_timeout:レコードロック取得待ち

がありました。


正常に処理が進んでいるけど、厳密には「処理中」ではないときに効くパラメータだと

  • net_read_timeout:クライアントからサーバに送り込んだデータの読み込み時間
  • net_write_timeout:サーバからクライアントへのデータの書き戻し時間

がありました。


他にも、アイドルタイムアウト系で

  • interactive_timeout
  • wait_timeout

などもありました。


しかし、MySQLさん、ちょっと重いSQL投げると延々帰ってこないことがあったりするのに、それをタイムアウトさせるサーバー側の設定がなく、クライアントサイドで対応する必要がありました。

例えば、Javaプログラムの場合、JDBCドライバ「Connector/J」に対して「SocketTimeout」で「クライアントから見て、リクエストがnミリ秒帰ってこなかったら諦めるよ」って形をとる必要がありました。


使用している言語がJavaじゃなかったら、、、わかりません。ドキュメント漁ってください。

ザクッと読んだ限り、connection_timeout、つまり「新規接続時にサーバーが見つからない/返事がない」ヤツっぽいタイムアウトと、コネクションの確立後に投げたリクエストの返答までの時間が間延びしてるタイムアウトが読み分けにくい。。。

Connector/.NETの「MySqlCommand.CommandTimeout Property
なんかはイケそうな感じだし、これ以外は。。。。


シェルスクリプトmysql -e "SQL文"とかやってたら、、、mysqlプログラムのオプションにはSocketTimeOut相当のパラメータはありません。。。


しかし、MySQL 5.7以降(もちろん8.0も)、表題に上げた「MAX_EXECUTION_TIME」によって、実行自体が長引くSELECT文にミリ秒指定でタイムアウト設定をすることが可能になっています。

MAX_EXECUTION_TIMEの使い方

GLOBAL/SESSION VARIABLEで指定する

my.cnfで指定するなり、SET句でしていするなり、今まで通りのサーバーシステム変数の扱いと同じです。

これに限った話ではありませんが、SETでGLOBALだけ設定しても、現在セッションには影響ありません。
GLOBALにSETしたあとにログインするか、SESSIONレベルでSETしましょう。


以下、100万件超、普通に投げると20秒弱かかるSELECT文に対して、「MAX_EXECUTION_TIME」で1000ミリ秒を指定したケースです。

「ERROR 3024 (HY000): Query execution was interrupted, maximum statement execution time exceeded」で強制終了されますが、これが出るまでに何秒かかったかの表示はないです。。。

mysql> selectcount(*)from test_a;
+----------+
| count(*) |
+----------+
|  1048576 |
+----------+
1rowin set (19.07 sec)

mysql> set global max_execution_time=1000;
Query OK, 0rows affected (0.00 sec)

mysql> select@@global.max_execution_time,@@session.max_execution_time;
+-----------------------------+------------------------------+
| @@global.max_execution_time | @@session.max_execution_time |
+-----------------------------+------------------------------+
|                        1000 |                            0 |
+-----------------------------+------------------------------+
1rowin set (0.00 sec)

mysql> selectcount(*)from test_a;
+----------+
| count(*) |
+----------+
|  1048576 |
+----------+
1rowin set (18.97 sec)

mysql> exit
Bye
[root@mysql57 ~]# mysql -u root -p
Enter password:
Welcome to the MySQL monitor.  Commands endwith ; or \g.
Your MySQL connection id is5
Server version: 5.7.20 MySQL Community Server (GPL)

Copyright (c) 2000, 2017, 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> select@@global.max_execution_time,@@session.max_execution_time;
+-----------------------------+------------------------------+
| @@global.max_execution_time | @@session.max_execution_time |
+-----------------------------+------------------------------+
|                        1000 |                         1000 |
+-----------------------------+------------------------------+
1rowin set (0.00 sec)

mysql> selectcount(*)from test_a;
ERROR 1046 (3D000): Nodatabase selected
mysql> use exe_test;
Reading table information for completion of tableandcolumn names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> selectcount(*)from test_a;
ERROR 3024 (HY000): Query execution was interrupted, maximum statement execution time exceeded
オプティマイザ・ヒントで指定する

オプティマイザ・ヒント自体がMySQL 5.7からの新機能で、「/*+ XXXXXXXXX */」の形式で、BKAJやMRRといったアルゴリズム採用をヒント句の形で与えられるものです。
その中で、ちょっと異色なヒントとして、MAX_EXECUTION_TIMEが指定できます。

そのSQLだけに指定できるので、セッションレベルよりも細かい制御ができて便利ですね。

気をつけたいのは、システム変数との違いです。
MAX_EXECUTION_TIMEは「=nミリ秒」ではなく、「(nミリ秒)」で指定します。

mysql> select/*+ MAX_EXECUTION_TIME(1000) */count(*)from test_a;
ERROR 3024 (HY000): Query execution was interrupted, maximum statement execution time exceeded

なお、オプティマイザ・ヒントの文言を間違うと、ふつーにSELECTの処理が実行され、warningが検出されます。

以下、MAX_EXECUTON_TIMEのスペルミス(Iがない)をした例です。

mysql> select/*+ MAX_EXECUTON_TIME(1000) */count(*)from test_a;
+----------+
| count(*) |
+----------+
|  1048576 |
+----------+
1rowin set, 1 warning (18.93 sec)

注意?点

MAX_EXECUTION_TIMEは

  • 読み取り専用SELECTにしか効きません
  • ストアドプログラムには効きません

The execution timeout for SELECT statements, in milliseconds. If the value is 0, timeouts are not enabled.
max_execution_time applies as follows:

  • The global max_execution_time value provides the default for the session value for new connections. The session value applies to SELECT executions executed within the session that include no MAX_EXECUTION_TIME(N) optimizer hint or for which N is 0.
  • max_execution_time applies to read-only SELECT statements. Statements that are not read only are those that invoke a stored function that modifies data as a side effect.
  • max_execution_time is ignored for SELECT statements in stored programs.

https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_max_execution_time

ということで。

トランザクションの扱いはどうなる?

「AutoCommit=OFF」の時、START TRANSACTIONを明示的に発行しなくても、最初のSQLを投げるとトランザクションが開始される話は、以前に触れました。

で、MAX_EXECUTION_TIMEでタイムアウトを引き起こした場合、どうなるか。

トランザクションは継続したままでした。
ま、予想通りでしたが。

MySQLトランザクション、基本的には、エラーでもトランザクションの状態は変わらないですね。
基本的ではないところだと、「デッドロックは、検出したらトランザクションが小さい方(=更新量の少ない方)を強制ロールバックさせる」です。


まとめ

  • MySQL 5.7以降、SELECTの実行時間を「MAX_EXECUTION_TIME」でタイムアウト制御することができる
  • システム変数だけじゃなく、オプティマイザ・ヒント(5.7新機能)により、SELECT文単位でもタイムアウト指定できる。
  • あくまで、「読み取り専用SELECT文だけ」が対象。
  • もともとMySQLの持ってるタイムアウト設定パラメータ、多い&名前が直感的じゃないの、ツラい。


MySQL5.7、GAしてから早2年ですよ。
ネット界隈はMySQL8.0で一喜一憂してますが、レイトマジョリティ以降の現場でも、さすがにMySQL5.7には手を出していいよね。

詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド (NEXT ONE)

詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド (NEXT ONE)

ところで

OracleとかPostgreSQLってどうだっけ?

新年あけまして2018

$
0
0

「あけまして」の挨拶には遅すぎますが、有給つかって仕事始めが1月9日となり、まだだらっと過ごしている真っ最中です。
今年も手短に年頭所感を残しておきたいと思います。


年末から年始にかけて、子供2人が立て続けにインフルエンザにかかり、近年まれに見るぐっだぐだな年末年始になりました。

昨年は

プライベートは、これと言った事件もなく。子どもたちが冬休みに入るまでは。。。


仕事の方は、ほぼまるごと、一昨年と同じことをやっていました。
正直な所、元請け先常駐なのに稼働率が低い(拘束されているけど、それに見合うタスクがない)状態が続いていて、それはそれでお気楽極楽なのですが、その一方で進化、成長を感じられず、やや焦りを感じながら、空き時間を使って業務外の事を吸収することに注力していた感じです。


で、MySQLAWSAWSの中でも使うサービスも変化なし、というなかで、Elastich Searchをオンプレ&AWS ESで試してみたりとか、Hadoop+Hive、Sparkあたりを触ってみたり。


年末に入ると、契約のゴタゴタから常駐が一旦解消になり、再契約があるのか無いのか、みたいな状況のなかでスポット支援案件をこなしていた感じです。



2017年初に書いたエントリを見返してみると、「C++へのチャレンジはできなかったけど、Pythonは年末にかじり始めたなー」とか、概ね書いたことに何かしら取り組んだ感じです。
ノートPCもロースペックなWindowsのやつを自前調達したことで、休日の過ごし方がかなり変わりました。
ていうか、休日にノートPCを持ち歩き、さらにモバイルルーターも入手したことで、ネットにつながっている時間が伸びた一方で、読書の時間が激減しました。


ブログの方は、2016年と2017年で-2件と、あいかわらずマイペースです。内容も、仕事内容に引きずられる形で、MySQLAWSがほとんど。

申し訳程度にやっているアフィリエイトの収入が月数千円程度入ることが時々あったので、それを原資に数ヶ月に1回、技術書をKindle版で買う、というサイクルに変化しています。
今後も、実用エントリのついでに1冊程度、よろしかったらどうぞ的に紹介する感じで、アフィ自体が目的とならない程度にゆるゆるやっていこうと思います。

今年は

無事に本厄を超えたので、後厄ですね。
気を抜かないように用心していきたいと思います。


仕事内容は、「DBいろいろ+AWS」が基本で、その他周辺技術として、言語系だったり、OSSプロダクトだったりを触りたいと思います。
言語だと、引き続きPythonC++ですかね。
プロダクトだと、MySQLを2年やっててOracleをすっかり忘れている間に進化しているので、そこのキャッチアップは必須になりそうです。
それ以外だと、RDBと関連性の高いもの、比較されやすいものがメインになりますね。昨年ちょっと触ったHiveやSpark、こうしたデータストアへデータを送り込んでくるメッセージング、ストリーミング基盤系のKafka、Stormとかでしょうか。AWSならKinesisかな。
分析基盤の構築需要が高いようなので、RDBでもDWH/OLAP用DBの構築ノウハウだったり、それらと比較されるNoSQL等を触る機会が増えそうです。


GPUの並列処理能力を活かした分析基盤、みたいな話がチラチラ聞こえてきているので、避けていたGPUアーキの方にも少し踏み込んでいきそうな感じです。


物欲系という点では、仕事の拠点が常駐先から自社に移り、いろんな客先に出入りすることになることから、「15.6インチでガチモバイル生活ツライ」ってことで、小さくてイケてるノートPCに交換して貰う予定です。
メモリ8GBオーバー乗せられる13インチノートPCが少なくて、つらい。
DB屋なら、ノートPCの上に仮想マシンOracleRACとかMySQLのリードレプリカくらい立てて持ち歩くよねー。


あと、できれば本を1冊書きたいな、というのは、目標として立てておきます。
こっち名義なのか、実名義なのかはわからないけど。
一つのテーマを、本に残せるくらいキッチリ追いかけて、それを整理してまとめるってのは、自分自身に残るものが大きいです。

というわけで

毎年とりとめのない話を年頭所感としてダラっと書いてますが、こんな感じで今年もやっていくかと思います。



今年もよろしくお願いします。

CentOS 6.9の上でapache-loggen使いたくてゴタゴタした。

$
0
0

きっかけ

とある製品の検証をするにあたり、ある程度リアリティのある大量データがほしいなと思ったところから。


apache-loggenは過去にElasticsearchの勉強しているときにちょっと触ったことがあって、ランダムなデータが一定間隔で出力できて、それでいてちゃんとApacheしてるので使い手があるなと。

LogstashならFilter構文簡単だし、正規表現が必要なツールで読むにしても、正規表現サンプル結構転がってるし。
GeoIPとぶつけてあげれば、いい感じに地理情報系のサンプルデータにも転用できるし。

で、久々に環境立て直そうとしたら、apache-loggenを動かす大前提のruby周りでハマったので、いまうまくいくやり方を残しておこう、って話です。

OSインストール

CentOS6.xをMinimal構成でインストール。(VirtualBoxの上にCentOS 6.9)

yum -y update
yum -y groupinstall 'Development tools'
yum -y install wget

rubyインストール

yumrubyを入れると退役宣言されている1.8.7が入ってしまいます。

apache-loggenのReadmeには

履歴
0.0.5 Limitで指定した値よりも1つレコードが多く出力されるのを修正。
0.0.4 Ruby-1.8.7でも動くようにした。
0.0.3 Rate=1くらいの低速度の場合、Flushが走らないので明示的にFlushするようにした。
0.0.2 RubyGemsに登録。コマンドを用意した。クラスの再利用ができるようにした。
0.0.1 はじめてのリリース
https://github.com/tamtam180/apache_log_gen

って書いてあったので、yumならラクチンでいいや、って思って進めたら、途中で詰まりました。



なので、1.8.7でどうにか動かす方法を探るのではなくて、2.0以上を最初から入れておく方法を取りました。

今時Rubyはrbenvでインストールするのが主流のようなので、
CentOSにrbenv, Rubyをインストールする - Qiita
に倣ってCentOS6.9にインストールします。

CentOS7(RHEL7系互換)だと、Yumで入れてもRuby2系が入るらしいです。
未だ、ちょっとした用事だと慣れた6系を使おうとしてしまう人です。

# Gitインストール
sudo yum -y install git

# Gitバージョン確認
git --version# rbenvインストール
git clone https://github.com/sstephenson/rbenv.git ~/.rbenv

# renvディレクトリ確認ls-d ~/.rbenv

# ruby-buildインストール
git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build

# ruby-buildディレクトリ確認ls-d ~/.rbenv/plugins/ruby-build

#  .bash_profile設定echo'# rbenv'>> ~/.bash_profile
echo'export PATH="$HOME/.rbenv/bin:$PATH"'>> ~/.bash_profile
echo'eval "$(rbenv init -)"'>> ~/.bash_profile

# .bash_profile確認
cat ~/.bash_profile

# 反映exec$SHELL--login# rubyのインストールに必要なパッケージをインストール
sudo yum -y install bzip2 gcc openssl-devel readline-devel zlib-devel

# インストールしたパッケージのバージョン確認
bzip2 --version
gcc --version
yum list installed | grep openssl-devel
yum list installed | grep readline-devel
yum list installed | grep zlib-devel

# rbenvバージョン確認
rbenv --version# インストールできるrubyのバージョンを確認
rbenv install --list# rubyインストール
rbenv install 2.4.3

# 切り替え可能なrubyのバージョンを確認
rbenv versions

# rubyのバージョンを切り替え
rbenv global 2.4.3

# rubyのバージョンが切り替わったことを確認
ruby -v
はまったところ

この日の最新の2.5.0を入れたらエラーが出ました。

ちょっとググったところ、
CentOS6.9 に ruby の環境を作って、Ruby on Rails のプロジェクトを作る - Qiita
において

調べてみると、どうやら CentOS6.9 で rbenv の rbenv-build を使って ruby 2.5.0 をビルドしようとすると出るrubyのエラー……なのかな……?rubyにチケット立ってたんだけど。。。
参考:https://bugs.ruby-lang.org/issues/14234

バージョン下げたらうまく行きました。

と同じエラーに引っかかったみたいなので、バージョン1つさげて2.4.3を入れました。




これで、gemもインストールされているので、appache-loggenもインストールできる状態になりました。

appache-loggenのインストール

gem install apache-loggen

これだけ。

ログを出力してみる。

GitHub - tamtam180/apache_log_gen: generate dummy apache log.

にあるものを動かしてみる。

# 毎秒100レコードの速度でファイル「abc.log」に出力
apache-loggen --rate=100 abc.log

適当に強制終了させて、出来上がったファイルサイズと中身を見て確認すればOK。

「--rate」は秒間何行出力するか、ってパラメータのはずなんですが、手元の環境ではまったくその通りにならない&明確に何倍のペースとかいう傾向も掴めなかったので、参考程度に使おうと思います。
今回はそこまで厳密な要件もないし。




オマケ:LogstashでApacheログをCSVに変換する

Apacheのログって、そのままだと空白で区切られたり、区切りたくない所で空白が入ってたり(UserAgentあたり)するので、単純にCSV化するの辛いです。

で、Logstashに「Csv output plugin」ってヤツがいるので、こいつを使って変換してみます。
なお、InputでApacheのログファイルを指定して、読み取り内容のフォーマットを解釈させるところは、LogstashのFilterで
match => { "message" =>"%{COMBINEDAPACHELOG}" }
って書いてあげるだけで解釈してくれるので、設定自体はかなりラクです。


セットアップは過去のブログ
Logstashを使ってみる - Kibanaを立ててみた - なからなLife
に遡って完了しているものとして、使用するconfファイルは、以下のようになります。

input {
    file {
        path =>"入力元パス・ファイル名"
        start_position =>"beginning"type=>"apache-log"}}
filter {if [type]=="apache-log"{
        grok {
            match =>{"message"=>"%{COMBINEDAPACHELOG}"}}
        date {
            match =>["timestamp", "dd/MMM/YYYY:HH:mm:ss Z"]
            locale => en
        }}}output {if [type]=="apache-log"{
        csv {
            fields =>["clientip","ident","auth","timestamp","verb","request","httpversion","response","bytes","referrer","agent"]
            path =>"出力先のパス・ファイル名"}}}

このファイルを作成したら

logstash -f上記の設定ファイルの場所

で動きます。(logstashへのパスは通してあるものとする)

正直、ちょっと重い(遅い)感じがする。いきなり1000万件のデータ読み込ませたからかもしれないけど、何か他の方法の方が速いのかもね。


とはいえ、これでRDBMSへのインポート(あるいは、少量ならexcelへのインポート)も、だいぶラクになったはず。


本当は直接RDBMSのテーブルに投入したいところでもあるのですが、LogstashのOutputプラグインJDBCが存在していないので、
あとで、Embulkでテーブルに投入も試してみたいです。



CSV出力ではなく、Elasticsearch+Kibanaに対してLogstash/Fluentdで流し込む例については、色々な人がブログエントリあげていますが、やはりElasticの大谷さんのやつがわかりやすいですね。
Logstashを利用したApacheアクセスログのインポート - @johtaniの日記 2nd
apache-loggen + fluentd + elasticsearch + kibana = ログ検索デモ - @johtaniの日記 2nd


そんなわけで、とりあえずやりたいこと=apache-loggenでapacheのダミーログデータ大量生成ができる環境になりました。


Python3.6 & Jupyter Notebook環境をCentOS 6.9 on Virtualboxの上に作ってみた。

$
0
0

そろそろかじっておいたほうがいいかなと

関連書籍をあさっていると、お手軽にWindowsの上に直接Python立ててJupyter立てる方法しか書いてない本があったりするんですけど、Windows環境をあんまり汚したくないので、いつでもまっさらにできる仮想環境上に立てたいなと。


機械学習の勉強やるのにAnacondaがいいらしいと聞いて、日曜日の午後にドトールに逃避している時間の中でやってみました。
近所のドトール無線LAN対応(w2、Wi-Fi暗号化なし、ゲストコード認証型、1回3時間まで)に対応したようなので、そのネット環境を存分にお借りして。


例によって全部rootでやります。root以外で構築する場合は、適宜sudoでどうぞ。

いつもの

CentOS 6.9 minimal installします。


なお、ネットワークアダプタは2つ用意して、1つ目はNATでホストマシン経由でインターネットに出られるように、2つ目はホストオンリーネットワークで、ホストからゲストにアクセスし易いようにしておきます。

2つ目のアダプタのIPアドレスは、インストーラDHCP自動起動するようにセットアップしておけば、192.168.56.101(他にVirtualBox上で起動しているゲストがいなければ)になるはずです。

# yum -y update
# yum -y groupinstall 'Development tools'
# yum -y install wget


Juypyter Notebookは8888ポートを使うようなので個別にあけてもよいのですが、めんどくさいので今回はiptablesをまるっと落とします。

# chkconfig iptables off
# chkconfig ip6tables off
# service iptables stop
# service ip6tables stop


省略するけど、selinuxも止めてます。


pyenvで環境を整える

pyenvインストールに必要なものを先に入れておく

これ全部本当に必要なのか確かめてないし、Development toolsのインストールですでに入っているものもありそう。

# yum install gcc zlib-devel bzip2 bzip2-devel readline readline-devel sqlite sqlite-devel openssl openssl-devel git
pyenvを入れる

gitで入れます。

# git clone https://github.com/yyuu/pyenv.git ~/.pyenv
環境設定
# echo 'export PYENV_ROOT="$HOME/.pyenv"' >> .bash_profile
# echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> .bash_profile
# echo 'eval "$(pyenv init -)"' >> .bash_profile
# source .bash_profile
確認
# pyenv version
system (set by /root/.pyenv/version)

Anacondaのインストール

pyenvでanaconda3が使えることを確認した上で、インストールします。

# pyenv install --list | grep anaconda3

実行時の最新は「anaconda3-5.0.1」だったので、これをインストール。

# pyenv install anaconda3-5.0.1
# pyenv global anaconda3-5.0.1
# pyenv rehash

以下のようになることを確認できればOK。

# pyenv versions
  system
* anaconda3-5.0.1 (set by /root/.pyenv/version)
# python --version
Python 3.6.3 :: Anaconda, Inc.

anaconda自身のアップデート

yumっぽいもの。pythonだとpipのイメージが強いけど、anacondaとしてcondaというパッケージマネージャがあるようなので、そちらを使ってみます。

# conda update conda
# conda --version

機械学習の本「Pythonで動かして学ぶ!新しい機械学習の教科書」で前半流し読みしていて出てきたパッケージがいるか、ザクッと確認。

# conda list jupyter*
# conda list numpy*
# conda list matplotlib*
# conda list tensrflow*
# conda list keras*

tensrflowとkerasはいなさそうです。
本の中でもpipインストールするように書いてありました。


あえて本に逆らってcondaで入れてみます。


そもそもcondaで見つかるかyum list available相当の「conda search」で調べて、見つかればinstallする、という流れで。
本にはtensorflowのバージョンが書いてないけど、とりあえず最新(tensorflow 1.4.1とkeras 2.1.3)を入れてみます。

# conda search tensorflow
# conda search keras
# conda install tensorflow
# conda install keras

それぞれが依存しているパッケージも一緒に入れてくれている模様です。

JupyterNotebookにアクセスできる環境を作る

この本にかぎらず、ググってよくヒットする例は、Windowsに直接Anacondaを入れてJupyterを立ち上げ、そのままブラウザも起動してくるパターンですが、今回はそれではないので、追加の設定が必要、とのこと。


ipythonでパスワードハッシュを生成

この後に作るjupyterの設定ファイルに書き込むため、事前にこの作業をしておきます。

# ipython

# 暗号化された文字列を生成するためにライブラリ読み込み
In [1]: from notebook.auth import passwd

# パスワードを入力して暗号化された文字列を生成
In [2]: passwd()
Enter password: 
Verify password: 
Out[2]: 'sha1:(暗号化された文字列)'

# iPythonから抜ける
In [3]:exit                                                                                                                                               
Do you really want to exit ([y]/n)? y
設定ファイルの作成
mkdir ~/.jupyter
touch ~/.jupyter/jupyter_notebook_config.py

vi  ~/.jupyter/jupyter_notebook_config.py


記述する内容は以下のとおりです。

c = get_config()
c.NotebookApp.ip = '*'
c.NotebookApp.open_browser = False
c.NotebookApp.port = 8888
c.NotebookApp.password = u'前述のipythonで生成したパスワードハッシュを転記する'


Jupyter Notebookを起動する。

# jupyter notebook --allow-root

ホストとなるWindowsから
http://ゲストマシンに割り当てられたIPアドレス:8888
でアクセスして、ログイン画面が表示されればOK。


JupyterNotebookの停止

サーバー側で「Ctrl+C」
でいいらしいです。


最後に

本業がDB屋さんってこともあって、PythonもJupyterも、まだ全然わかってないです。ふんいきでやってます。

盛り上がってる技術界隈は、書籍次々に新しいものが出てきていいですね。
翔泳社さんが電子書籍半額セールやってたので、軽く飛びついてみました。

CentOS6 on VirtualBoxでNIC追加・変更する手順

$
0
0

最初に億劫がってブリッジ設定のNICを1つで構築すると、あとが面倒。

Python3.6 & Jupyter Notebook環境をCentOS 6.9 on Virtualboxの上に作ってみた。 - なからなLife
で、
eth0:NAT
eth1:ホストオンリーネットワーク
の2枚構成にしておくとラク、と自分で言っておきながら、すでに「NIC1つ:ブリッジアダプター」でつくってしまった環境をCLIオンリーで直すの、毎回調べながらやっているので、主に自分の備忘用に残しておきます。


普段からオンプレNIC増設構築慣れしている人には何の苦労もない話かもしれませんが、たまにしかやらない人には、ドコをいじればいいのか覚えられないもので。
動いていれば、そうそう触るところじゃないしね。

なお、CentOSGUIもインストールしてある場合、VirtualBoxマネージャー側でネットワークのアダプタ設定変更したら、あとはGUIであっさり設定できるかと思います。
たまにしか変更することがなく、そのわりにシステム全体に影響ありそうな設定・管理については、GUIマンセーな人です。

変更前

VirtualBox

仮想マシン選択->「設定」->「ネットワーク」

のみ有効

CentOS
/etc/sysconfig/network-script/ifcfg-eth0

がある状態。

変更

対比上、VirtualBoxCentOSの順に書いていますが、VirtualBoxのデフォルトコンソールはコピペが効かないので、「旧設定(ブリッジアダプター)で起動、Teraterm等で接続、ifcfg-eth1を作成・保存(コピペ)、ネットワーク再起動、シャットダウン、VirtualBox設定変更」が楽だと思います。

VirtualBox

仮想マシン選択->「設定」->「ネットワーク」

CentOS
  • ifcfg-eth1をviで新規作成。
vi /etc/sysconfig/network-script/ifcfg-eth1
  • 以下を記述して保存。
DEVICE=eth1
TYPE=Ethernet
ONBOOT=yes
NM_CONTROLLED=yes
BOOTPROTO=dhcp
DEFROUTE=yes
PEERDNS=yes
PEERROUTES=yes
IPV4_FAILURE_FATAL=yes
IPV6INIT=no
NAME="System eth1"
  • ネットワークサービスの再起動。
service network restart

なお、IP固定など、細かな設定の方法は、別途ifcfg-ethファイルの編集方法を調べてみてください。

余談

やっぱり、構築の最初の段階で、GUIインストーラ内で設定を済ませてしまうのがラクなんですよね。


なお、CentOS7になると、また話は変わる模様。
そろそろちゃんとやらなきゃなあ。

標準テキスト CentOS 7 構築・運用・管理パーフェクトガイド

標準テキスト CentOS 7 構築・運用・管理パーフェクトガイド

CentOS 6系にyumでmavenをインストール

$
0
0

タイトルどおり、yumなんで

wget http://repos.fedorapeople.org/repos/dchen/apache-maven/epel-apache-maven.repo -O /etc/yum.repos.d/epel-apache-maven.repo
yum install -y apache-maven
mvn --version

これだけ。
今日やった時点で、Maven 3.5.2がインストールされました。


なお、ビルド

cd ビルドしたいJavaソース一式のpom.xmlが置いてあるディレクトリ
mvn install

これだけ。


あとはpomの設定にまかせて、依存関係のあるものは勝手にダウンロードしてきたりして、コンパイルしてjarに固めてくれます。


もちろん、自分で作ったjavaソースのパッケージングは、pomも自分で書くわけで、ちっとも「勝手に」じゃないんだけど。

愚痴?

いやさ、とある有償製品で、マニュアルに「githubからダウンロードしてこい」、って書いてあるから行ってみたら、「mavenのpom.xmlも一緒に渡すから自分でjar作れ」っていうヤツがあったので。

「jarで渡すけど、カスタムしたい人もいるだろうから、ソースとpomも渡すよ」なんじゃないのかね、フツー。


「ただミドルウェアとしてソレを使いたいだけで、Javaで開発するエンジニアじゃない人」にはツライっす。手元のPCにEclipseすら入れてないし。


ネット上に情報があって救われる日々に感謝。


Anaconda3でJupyter Notebookを立てたときに、Python2.7も選択できるようにする

$
0
0

何番煎じだよ

って話なんだろうけど、その先人の煎じたヤツがその通りでは動かなかったので。

結局公式サイト記載のやり方が、一番近かった

Installing the IPython kernel — IPython 6.2.0.dev documentation
近いけど、そのままじゃ動かねえし。



一通りJupyter Notebookを立てた後は、Python3.6系しか使えない状態になっています。


その後にやる作業はこれ、って書いてあることが多い。

conda create -n py27 python=2.7 ipykernel
source activate py27  # または source なしのactivate py27

公式サイトでも同じなのですが、この「activate」コマンドが通らない。。。


かいつまんでしまうと、ここを「pyenv local python2の環境名」で逃げれば行けました。


もう少し丁寧に。

先のエントリの通りに環境が立ち上がったところからスタートです。
atsuizo.hatenadiary.jp

なお、anacondaのバージョンが
前回の
anaconda3-5.0.1
から
anaconda-3-5.1.0
に上がっているのであしからず。


conda create -n py27 python=2.7

で、Python2.7.14をダウンロードしてpy27という名前の仮想環境?を作る
バージョンを細かく指定しないと最新のものがインストールされるようです。

pyenv versions

で、認識されているpythonのバージョンリストを確認すると、
先程のコマンドによって

anaconda-3-5.1.0/envs/py27

ができているのが分かります。

このパス情報を使って、activateコマンドの代わりに

pyenv local anaconda-3-5.1.0/envs/py27

を実行してアクティブにします。


pythonのバージョンを

python --version

で確認してpython2.7系がアクティブであることを確認したら、

python -m ipykernel install --user

で、ipythonのカーネルをインストールします。


すると、userのホームの下に隠しディレクトリ「.local」が作成されて、その下の方に、python2のカーネルが登録されます。

ipykernelというモジュールを探して、userディレクトリにインストールする、という意味のようです。



続けて、jupyterが認識したカーネルのリストから確認します。

jupyter kernelspec list

python2だけが認識されています。

Available kernels:
  python2    /root/.local/share/jupyter/kernels/python2


アクティブを

pyenv local anaconda-3-5.1.0

で元のバージョンに戻してから、

jupyter kernelspec list

を実行すると、python3とpython2が登録されていることが確認できます。

Available kernels:
  python2    /root/.local/share/jupyter/kernels/python2
  python3    /root/.pyenv/versions/anaconda3-5.1.0/share/jupyter/kernels/python3

この状態になったら、Jupyter Notebookを起動します。
rootでやっている場合は、--allow-rootオプションもつけます。

jupyter notebook --allow-root

で、Jupyter Notebookを立てたマシンのホスト名orIPアドレス:8888にアクセスしてログインした後、「New」のドロップダウンに「python2」[python3」があることを確認します。


それぞれのバージョンでノートを立ち上げ、

import sys
sys.version

を実行して、想定しているバージョンが返ってくることを確認すれば完了です。

Jupyter Notebookに絞って勉強しなきゃいけないのかなあ

pythonを書くのが本来のお仕事ではない(Python用にAPIが提供されている製品でそっちのほうが性能出るらしいというので試してみたかっただけという)こともあって、相変わらずふんいきでやっているので、行き詰まったときに、Jupyterのレイヤなのかipyhonのレイヤなのかpython(pyenv、anaconda)のレイヤなのかよくわからなくてしばらく嵌りました。

「道具のための道具」の習得に割く時間は最小限に、効率的に済ませたいところです。

PythonユーザのためのJupyter[実践]入門

PythonユーザのためのJupyter[実践]入門

WindowsUpdate エラー「80072EE2」顛末記

$
0
0

WindowsUpdateが使えなくなった!

メインじゃないのでしばらく使ってなかったWindows7のPC、最後に立ち上げた日もWindowsUpadteがトラブってたの、放置してた。
久々に時間が取れたのでいじってみたけど、何も改善してなかった。

エラーコードは「80072EE2」。原因不明と表示されるけど、ネットワーク周りが原因のときのエラーらしい。

やったこと

ググって拾えることは一通りやった。

・信頼済サイトに登録(「80072EE2」でググると真っ先に出てくるやつ):解決しない!
・WindowsUpdate.diagcab実行:「解決済み」が出るが、解決しない!
・WindowUpdateDiagnostic.diagcab実行:「解決済み」が出るが、解決しない!
・BITDiagnostic.diagcab実行:解決しないが、解決方法が提示されない!
・クリーンブート(msconfigから):クリーンブートはできるけど解決しない!
・WindowsUpdate履歴フォルダの再構築:解決しない!
・Proxyの設定:そもそも使ってないし、netsh winhttp show proxyしても「使ってない!」って出てる。
・問題のトラブルシューティング(コンパネの「ネットワークとインターネット):「インターネット接続」で「www.microsoft.comにつながらない!」と言われる(ブラウザからはつながる)


WindowsOS付属のトラブルシューティングツールで解決した試しがない。。。

基本に立ち返る

上記「やったこと」の最後「www.microsoft.comにつながらない!」について、ブラウザからは接続できるし、なんでやねん?という怒りを押さえて、コマンドプロンプトからpingを打ってみた。

> ping www.microsoft.com
e13678.dspb.akamaiedge.net [2600:140b:2:1b1::356e]に ping を送信しています 32 バイトのデータ:
要求がタイムアウトしました。
要求がタイムアウトしました。
...


orz。。。


結末

ローカルエリア接続のプロパティで「IPv6」のチェック外したら、解決しましたよ。


IPv6おじゃま虫、まだまだ健在。


それにしても、ある日から突然Updateできなくなったんだよなあ。わけわかんねえ。
そして、久々に書いたブログがこんなネタなんて。ひどい。

マスタリングTCP/IP IPv6編 第2版

マスタリングTCP/IP IPv6編 第2版

「アプリケーションエンジニアのためのApache Spark入門」読んでみた

$
0
0

先にまとめておく

  • ApacheSpark2.2.0ベースでの記述で、サンプルソースはSaclaではなくPython(pyspark)。(個人的にはPython歓迎!だが、scalaベースで学びたい人には残念かもね。)
  • Sparkの話だけではなく、fluentd+Kafkaで常時データが生成される環境を作る、具体的なシナリオベースでの解説。これはありがたい!
  • 4章以降、順番に消化していかないとソースとしては動かない構成。
  • 共著で章別に担当したらしく、イマイチ連携取れてないと感じるところがある。共著って、一人あたりの執筆負荷は減るけど、こういうところが難しいよね。
  • サンプルソースがダウンロードできるが、本文にソースのファイル名が記載されていないことがある。
  • サンプルとしてシンプルなものを扱いつつ、実運用に向けて考慮すべき話も各章で言及されているあたり、テクニカルライターというより現場の技術者である、という矜持を感じる。
  • 電子書籍版(Kindleepubかpdf)で欲しかった(発売から3ヶ月過ぎても出る気配なし)
  • 全体としては、人に推薦できる「良本」です。これからSpark触りたい人、ついでにストリーミングデータの扱いについて理解したい人にとって、手を動かしながら学べる本は少ないですからね。貴重です。


以下、章別にメモ垂れ流します。



Chapter1: データ分析プラットフォームの概要

概要の概要なので、言及割愛。


Chapter2:Spark概要

Sparkの概要。ベースとなる環境構築の話。

VagrantVirtualBoxCentOS 7.2+Spark2.2.0」を構築する手順が解説されていますが、「Vagrantなし+VirtualBoxCentOS 7.5+Spark2.3.0」でやりました。

よって、先々でてくるソースを写経するにあたり、Sparkのバージョン指定や、標準不足のライブラリをオプション指定する際に、微妙にバージョンが違うことがあり、時々躓きました。
「Sparkがこのバージョンだから、指定するライブラリはこのバージョンだけど、違う場合は変えてね」とかいう補足がなかったなと。

あと、この環境、IPアドレスが決め撃ちされていて、そのIPアドレスであることが前提として以降の章のソースなども記述されているので、ここも自分の環境に置き換えて行く必要がありますが、スタンドアロンで全部構築しているのに、サンプルソース上も「localhost」ではなくIPアドレスが記述されているので、ソースコピペで動きを見ようとするとコケます。

このとき、エラーメッセージから原因がソコであることを推測しにくいのが難点ですね。

Chapter3:サンプルユースケース概要

仮想の農場に仕込んだセンサーのデータをFluentd+Kafkaでストリーミングデータとして送り込んで、Apache Spark上でデータをアレコレするよ、って話です。

Chapter4:Fluentd、Kafkaによるデータ収集

Spark上でお料理するデータを取り込む仕組みのお話です。Sparkが主体の本ですから、それぞれの説明はサラっとしていますが、最低限の説明はしっかりされています。一応別でFluentdとKafkaの勉強はしたから大丈夫でしたが、ここで初めて触る人にはちょっとツラいかも。

この章は、記載されているソースに番号が振ってなくて、サンプルソースのファイルとの対応関係が取れていません。なので、ファイル開いてみないとわからないです。残念。

たとえば、P62のサンプルソース(こういうところでソースに番号がついてないと説明もツラい)で、ConsoleProducerは9092ポートにつないでいるのに、ConsoleConsumerが2182ポートにつなぐところがあって、見事にメッセージが取れない。直後のP63のConsumerは9092につないでいるので無事メッセージ取れます。


また、P51のセンサーデータ用Fluentd設定で、

/var/log/td-agent/pos/sensor_data.pos

が権限不足。

systemctl start td-agent

が裏でずっとエラーを吐き続け、過去に通ったはずのCURLによる稼働確認すら通らなくなるので、先に「td-agent:td-agent」で
/var/log/td-agent/pos/
を作成するか、むしろ一切記述しない、といった対応が必要になります。

この設定を使う話は、次の5章P99でやっと話が出てくるので、本来この時点では設定を入れる必要がない(そういうシナリオになってない)のですが、本文解説でも出てくるしダウンロードできるソース(04-01.conf)にも書いてあるので、勢いで設定するとハマります。



Chapter5:Spark Streamingによるデータ処理

Spark Streaming、および、Spark2.2系からProduction ReadyになったSpark Structured Streamingについて扱っています。これ両方書いてあるのなにげにありがたい。

ここでの躓きどころ。

P90 の

nc -lk 9999

は、前の章からの流れで、td-agentをport:9999で立ち上げっぱなしにすると衝突します。
よって、

systemctl stop td-agent

してから実行すると、標準のプロンプトに戻らず、ポート9999に文字列を継続送信可能な状態になります。



P96でSpark「Structured」Streamingの「Checkpointの設定設定コード」が紹介されていますが、これをどのソースのどこに埋めるのかに埋めるべきかの指示記述がないです。

ここは、05-02.pyで同じ「query = 」の行を置き換えれば、wordCounts.writeStreamを使わなくなってしまうけど大丈夫かとおもいます。

とはいえ、05-02のソースなのに、出力先が05-01用のディレクトリ向いてるのも気になるところです。


P99から、いよいよ、fluentdとKafkaとの連携が本格化します。

で、そのP99で、fluentdに食わせるセンサーデータログを生成し続けるプログラムをcrontab+シェルスクリプトという形で作ることになりますが、ここでAppendixに飛びます。

ここでしかAppendixに飛ぶタイミングがないので、むしろここで書いても良かったんじゃないか、という。

で、Appendixに書いてあるソースですが、「mdkir」なのに「.log」とファイル名まで書いちゃってる。ダウンロードできるスクリプトの方は正しいですね。



なお、fluentdのエラーにならなければ、仮想マシンに割り当てるCPUは1個でも十分処理できました。
が、pysparkの実行は、CPU使用率の割に時間がかかります。立ち上がりの処理が重いからですかね。
起動のオーバーヘッドが大きいからこそ、ある程度の量のデータを捌くシステムで使わないと、Sparkの速さを享受できない、ってことでしょうか。



Chapter6:外部ストレージへのデータ蓄積

5章はfluentd+kafkaからSparkに取り込んで処理した結果はコンソールで確認するまででしたが、ここでは、ファイルにjsonやparquetで保存したり、Cassandraに保存するケースが取り上げられています。
できれば一般的なRDBMSとしてMySQLPostgreSQLあたりとの連携も取り上げてほしかったなあ。。。


で、Cassandraインストールなのですが、P143にあるyumリポジトリ設定の記述だとうまくいきませんでした。
「repo_gpgcheck=1」が抜けていて「KEYがインストールされてない」といってインストールが止まってしまいますので、追加して切り抜けました。


さらに、インストールの直後、

systemctl start cassandra

しても起動できず、

systemctl daemon-reload

を叩く必要がありました。

Cassandra公式サイトに書いてはあるものの、CentOS7系前提で執筆されている本なので、このへんはフォロー欲しいところですね。



で、その後もP150とかP152の実行の際に、「java.lang.NoClassDefFoundError」で落ちるという現象を回避できず、直近でCassandraを扱う予定もなかったことから、Cassandra連携を試すのは諦めました。



Chapter7:SparkStreamingによるデータ分析

Chapter5ではストリームデータ1行1行を取り込むところを扱いましたが、こちらでは複数行をまとめて集計分析等を行うケースを扱っています。
ストリームデータ分析自体の説明にもページを割いている点は好印象ですが、ちょっとその先が荒っぽい印象を持った章です。


P173で突然「環境前提」があるのですが、具体的に何が動いているべきかのコード番号等がないので、ちょっと面食らいます。

サラッと書かれているけど、以下のことをやる必要があります。
特に、前の章までの勢いを中断して再開したような場合、確実に躓きます。

P56:zookeeper起動(コード番号なし)
P57:kafka起動(コード番号なし)
P99:05-06_command.txt+P100 05-01.conf(/etc/fluentd/fluentd.confへの追加設定)+systemctl start td-agent
P334:appendix1のexecute_create_sensor_data.shの用意+crontab設定
P117:05-13_command.txtの実行(及び呼び出される05-08.pyの用意)

この順で起動した後に、07-01_command.txtを実行することになります。以降、07-06までずっと使用するので起動しっぱなしで良いです。


この「前提」のところの躓きさえクリアすれば、あとは特に躓くこともなく、Window方式を変えたりいろいろなパターンの集計処理が扱われていて実践的な話が豊富です。


Chapter8:Spark SQLによるデータ処理

タイトルはSpark SQLですが、ここから唐突にJupyterを使うことになります。そして、pandasとmatplotlibも。


そして、この章はChapter7以上に前提の切り方が荒いです。サンプルデータの提示がなく、こんな感じのデータがある前提で進めます、という進み方です。
その前提となるデータをどうすればよいのかが、かなり投げやりで、以前のどこの章、すら書いてません。この章用のディレクトリを掘れ、とも書いていません。そうなっていることを前提とします、だけです。


ここは、以下のような感じで切り抜けました。

  • センサーデータのJSON

過去の章でkafkaトピック「sensor-data」として登録していたデータをJSON形式で。
chapter06でsensor_dataをjsonファイルに出力する事例がありますので、コレにならって/opt/spark-book/chapter06/data/jsonの下.jsonファイルを、sample.sensor.*.jsonのファイル名形式にrinameしながら、あるいはfluentdが拾う前の/var/log/sensor_data/sensor_data.logを、そのままsample.sensor.sensor_data.jsonとかいう名前にRenameして/opt/spark-book/chapter08/dataの下にコピーしておけばよいかと。


結果サンプルはともあれ、処理のサンプルコードでは日付を条件指定していないのに、サンプルデータの年月日を固定しつつデータは用意しない、というのは不親切じゃないかなと思いますね。

  • 畑マスタのCSV

P109のものを、1行目のカラム名定義行を_c0,_c1に書き換えて、/opt/spark-book/chapter08/masterの下にsample_sensor_filed.csvという名前で置いておきましょう。




P212の「日付でgroupBy count」をやるときに、date列にdate_format()を行うのですが、ただし、日付のフォーマットが上記2例で用意したものだとyyyy/mm/ddとスラッシュ区切りになっていますが、そのままだとdate_format()に食わせるときに認識してくれません。
jsonファイル内でyyy-mm-ddフォーマットに変換するか、このサンプルコードを実行するときだけ

date_format("date", "yyy/MM")

date_format(replace(date, '/','-'), 'yyy/MM')

と、spark.sqlの中のSQLを書き換えてあげましょう。


pysparkの方だと、

from pyspark.sql.functions import *
base_sensor_rep = base_sensor.withColumn('date', regexp_replace('date', '/', '-'))
base_sensor_rep.groupBy(date_format("date", "yyy/MM").alias("new_date")).count().show()

といった具合に、regexp_replace(他に適した関数ある?)といった変換を1ステップ挟んであげましょう。

とにかく日付形式の文字列を日付として認識させるにはハイフン「-」区切りでないとうまくいかないようです。


欠損値やNull値の処理方法の解説がP213、214とありますが、同様に上記の方法で準備したデータに対象となるようなデータがありません。あしからず。



また、dateでグルーピングする集計例が複数出てきますが、用意できたデータの日付に偏り(1日分しかないなど)がある場合、yyy-MMで集計している例をyyy-MM-ddにするなどしてバラしてあげないと面白くない結果が帰ってきます。




P226冒頭のjsonファイルの出力パスは誤植なのか共著者間の調整不足なのかな?という部分ですね。

/opt/shuwa-book/chapter08/result/field_data/

は、これまでの流れだと

/opt/spark-book/chapter08/result/field_data/

でしょう。

ダウンロードできるサンプルソースも、この章はJupyterNotebookの「.ipynb」ファイル1個だけ、という感じで、全体的に粗さが目立つ章でした。

扱うテーマが割と実用的な話なだけに、ちょっと残念。
あと、Jupiter、Pandas、Matlotlibについては、Spark関係なく別でそこにフォーカスした本があるはずなので、そっちでキャッチアップしたほうが良さそうです。


Chapter9:Spark MLlibによるデータ分析

MLlibの名の通りMachine Learningの話なので、まず大前提となる機械学習概論に冒頭かなりのページ数が割かれています。
喫緊で機械学習をやる予定のない場合、かなりしんどいと思いますし、機械学習を学ぶ必要がある場合は、別途勉強するのがオススメでしょう。


とりあえず、記述通りに動くかどうかだけ試しました。
なお、ここでのサンプル実行処理は重いです。仮想マシンにvCPUを追加で割当しておきました。


Chapter8と同じJupytorを使いますが、こちらの章はテスト用のデータがダウンロードソースの中にcsvで提供されています。
データの配備先がchapter09ディレクトリではないのが微妙ですが。


ユースケースに関する回帰分析の実施サンプルで、2番目で突然セッションとスキーマの定義を省略(前にやったから省略するよ、ってだけで、どこを見ろと書いてない)になるところでびっくりした以外は、わりと順調に進みました。

あと、同じところでcsvファイルからdf6というデータフレームを作るところで何度もエラーで落ちたのですが、原因わからないまま仮想マシンの再起動を実施したら治りました。HiveMetastoreへの書き込みが詰まったらしいのですが、なんでそうなったのかは未だに分からず。




ここ中身理解せず雰囲気で機械学習(写経)しましたが、結構な量があってツラかったです。が、誤植等によるエラーはなかったのが救いですね。




Chapter10:プロダクションに向けたシステムアーキテクチャを考える

この章はアーキを考える章なので、ソースを動かしてどうこう、って話ではありません。
アーキの話を冒頭じゃなくココに持ってくるの珍しい&一通り「雰囲気でやってみた」後に、ちゃんと理解しようって順番もイイ。


Sparkがどう動いている、ってだけじゃなく、AWSGCPでどう構成する、って話まで及んでいるのも嬉しいですね。できればAWS Glue(PySpark)にも触れてほしかったけど、あれは完全マネージドでそれ以上の中身の話は公表されてもいないから難しいか。

最後に

直近Sparkを触らなくてはいけないという強烈な課題意識のもとに読み始めたこともあって、かなりしっかり読み込めました。
そして、期待に沿う内容でした。

共著ゆえの難しさは経験したので理解しているつもりで、小さい所は逐次上記で指摘はしたものの、一貫したコンセプトがブレることなく、しっかりまとめてあったと思います。
私の中では「良本」にランクインしてますね。


また、Sparkに限った話じゃないけど、イノベーター・アーリーアダプター層が早い段階でいじって騒いで、その層にはある程度普及したプロダクト、レイトマジョリティ層が触るころにはバージョンアップにともなってアーキも一部変わってたり、当時ベースのネット記事は陳腐化してるし、新しい情報も出てこないし、当時の人たちは新しいプロダクトに飛びついていたりして、余程定着して「知の高速道路」がメンテ続けられてるものじゃないと、わりとツラいんです。


出版社も、出てきた当初の盛り上がりには敏感だけど、安定期にはいったプロダクトに関する書籍の出版の企画には、あまり積極的になってくれない。つらい。


私がこの本を読む前にSparkについて勉強したときの本は「Apache Spark入門 動かして学ぶ最新並列分散処理フレームワーク」で、2015年初版、Sparkは1.5系で、DataFrameじゃなくRDDベースです。これはこれで良い本でしたが、購入時点でSpark2系が出ていたのに対応している本がなかったです。個人的には当時よりSpark学習欲が高まった(ていうか喫緊の課題としてSparkを使う用事がある)ときに、今回の本が出版されたのはホントにありがたい。


そういう点で、この本はもっと評価されていいんじゃないかと思ってます。
でも、Amazon上、評価の良し悪し以前に件数自体がすごく少ない。。。一応「ベストセラー」は付くようになったから、それなりに売れて入るんでしょうけど。


「だからとにかく、技術書は問答無用で電子も出してくれ。」


いろいろな場所で仕事する隙間に読んで触って書き残して、ってやるのに、紙の本はホント不都合なんですよ。
電子じゃないので買うのためらう+買っても物理的制約で読み進まない=ブログ記事に落とすまでに時間がかかってしまったら、普及に貢献することすらできないです。

良い本は広めたい。


アプリケーションエンジニアのためのApache Spark入門

アプリケーションエンジニアのためのApache Spark入門

AWS Glueを扱ってみてツラかったところ。

$
0
0

冒頭から余談

しばらくブログの更新が空いてました。この期間、かなりしょっぱい仕事してました。まだ終わってないし、落ち着いてもいないけど、なんとなく書き残しておきたいネタになったので。


PySparkとかあんまり理解していない状況から、なんとなく「そのへんのETLツールと同じでしょ」ってノリで提案、受注してきてしまった案件に巻き込まれて触り始めたGlueですが、ツラかったところをツラツラ書きます。
「いや、そういうもんだろ」、って話もちょいちょいありますが。


コンセプトとしては素晴らしいし、すでにうまく活用できている現場もたくさんあるんだとおもいます。
多分、関わった案件の諸条件(要件とか、体制とか)と、マッチしてなかったんだと思います。


ていうか、プロジェクト自体ががうまく行ってたら、自分の中でももう少し良い印象が残り、その余韻で連載的にまとめようと思ってたんですけど、無理っす。
以下の記事に素晴らしいまとめがあるので、そちらに譲ります。
qiita.com


まあ、いわゆる愚痴です。ほどほどにお付き合いください。
「Glue」と検索して「つらい」ってサジェストされるまで連呼するつもりはないので。

RDBMSに対するUpdateがない。

RDBMSに対してInsert(mode=append)かDrop/Create/Insert(mode=overwrite)しかできないです。
ん?overwriteってそういう動きかーい、っていう。

MySQL相手でも、Insert into ... Duplicate key updateが使えないです。


Update相当のことをするには、Out先のデータもGlueの中に読み込んで、新規に取り込むデータとマージして、対象テーブルへのTruncate&Insertで取り込むことになります。
そうでなければ、RDBMS側に毎回新規に取り込むだけのワークテーブルと、本来Updateしたい本体テーブルと2段階で作成しておいて、GlueでワークテーブルにInsertして、RDBMSの中でワークから本体テーブルにSQLでUpdateをかけます。

RDBMSからの取り出しは、常に全件。

RDBMS上で絞り込んだあとのデータを取り出すのではなく、まずテーブルのデータをすべてGlue=Sparkの世界に持ってきてから、Sparkの機能をでフィルタリングを行って対象レコードを絞り込みます。
結果、RDBMS上はフルスキャンかかるし、Glue側はメモリを大量に使うし、その間のトラフィックも大量発生するし。


いや、SELECTに限らず、任意のSQLを飛ばす手段もあるにはあるんですよ。ベタなPythonコードを書いてDBにコネクション張ってSQLを投げるようにコードを書く手段が。ただ、そんなことをするくらいなら、Glue/PySparkじゃなくて普通にPythonで書けばいいじゃない、っていう。

レコードレベルのデータ加工が辛い

これは私のPySparkに対する知識のなさかもしれませんが、やろうとすると、PySparkでのコード作業量が一気に増える。
しかも、Glueのドキュメントでは全然足りなくて、Apache Spark/PySparkのドキュメントを見るしかなくなるんだけど、またこれを読み解くのも結構難解。
ていうかGlueのDynamicFrameではなく、SparkのDataFrameに変換してからの話になるので、DynamicFrameの良さ(速さ)が吹っ飛ぶんだよね。


結局、RDBMSに無加工で取り込んでから、SQLで加工しました。慣れた技術でやるのが一番早い。

取り扱える文字コードがUTF8だけ。

個人的にはUTF以外のファイルを扱いたくもないけど、UTF8以外を扱いたいニーズは山程あるわけで。

CSV読み書きのオプションが弱い。

quoteCharつけても全部の列にquoteされないし。バグ?

Glue Catalog側でヘッダありなしのオプションつけても、ヘッダ無し出力できないし。

Glueが自動生成するPySparkの中で
glueContext.write_dynamic_frame.from_catalogだと、細かいオプション指定できないし。
glueContext.write_dynamic_frame.from_optionsだと、細かいオプション指定できるからヘッダ無し出力はできるかもしれないけど、かわりにData Catalogに定義した内容、意味なくなって、出力先をハードコードしなきゃいけないし。


さすがにエスケープなしの改行混じりデータを読み取れというのは酷か。そんなCSVファイルを送りつけてくるほうが悪い。

ファイル出力時、デフォルトだと大量のファイルに分散出力される。

そもそもが分散処理が前提だから仕方ないけど。


で、GlueのDynamicFrameのままではRepartitionできなくて、一旦SparkのDataFarmeに変換してあげなきゃいけない。
「DynamicFrame.toDF().repartition(1)」して、そのままDataFrameからWriteする(出力先に指定等にDataCatalog使えないからハードコードする)か、fromDF()でDynamicFrameに戻してからWriteするか。


DataFrameのままWriteしようとすると、modeが「error or errorifexists」になっていて、追加も置き換えもできないので、モード変更も追加しなきゃいけないし。

ファイル出力時、任意の名前にできない。

処理結果のファイルを使った後続処理がGlue以外で実装する必要があるときに、ファイル名を固定できないから、取ってくるファイルの判定のためのロジックとか余計に書かなきゃいけないし。




オブジェクトのコピーができない。

似たようなものを作るニーズって絶対あるし、「バックアップをとっておいて、いじってうまくいったら、古いの消そう」とかいうのにも対応してない。
PySparkスクリプトはS3上に保存されるし、CodeEditorで中身をまるごとコピーしてテキストで何処かに保存しておけばなんとかなるけど、Data Catalog側はそれもできない。

オマケに、CLIでDescribe(Glueのコマンドだと「get-ほげほげ」)もCreateも当然サポートしてるんだけど、そのgetで取れるJSONが、そのままCreateに食わせてもエラーになるという残念仕様なので、開発->検証->本番用の移行とか、結局同じ設定作業の繰り返し。最初からGUI使わずにコードで全部組んでおけば良いんだろうけど、Glueって、「ほとんどGUIから」ってのがウリだったはずで。



起動のオーバーヘッドが大きい

「裏でEMRが動いているLambda」とイメージすれば理解はしやすいですが、現実問題としてつらい。
実際、ログを見るとEMR(のコード)が動いてます。


一発目のジョブを起動するのに、10分位かかります。何分放置すると眠ってしまうのかは測っていませんが、連続実行ならすぐ動きます。


「起動オーバーヘッドは大きいが、大量処理は速い」という、このサービスの特徴そのものなのですが、少量データでデバッグしているときがつらい。
走らせて、エラーが出て、考えて、直して、リランする。その間に裏で動くインスタンスはオネム


実行時間1分以内、でも起動に10分、とか、かなり泣けてきます。
Eclipseでローカルビルドに10分かかるような重いマシンで作業しているような感覚。


もしかして、開発エンドポイントを立てて、「Apache Zeppelin」を使えば解決する話?
Zeppelinをイチから覚えている余裕もなかったんで。
流石にGlueの公式資料にZeppelinの話はほとんど載ってないし。




Management Consoleが全体的に辛い。

一覧画面の操作性が不統一。

一覧のソートができたりできなかったり。フィルタがうまく効いたり効かなかったり。
オブジェクト数が増えると軽く死ねる。

最初、日本語表示が機能してなかったんだけど、日本語表示が機能するようになってからは、ソートができる画面でソートさせると描画か固まる(Loadingから進まない)とか。


・・・やっと直したっぽい。
これは英語表示に切り替えればそもそも発生しないので、結局英語で使っているのだけど、他の「普通に日本語で使えているサービスの画面」も英語になってしまうから、クッソ不便でした。


あと、表示列幅も変更できない。割と長いオブジェクト名になりがちなので、ほぼ見切れる。
OnMouseでフルネームがポップアップするんだけど、これがまた誤操作(クリックすると1コ上とか1コ下をクリックした扱いになる)を誘発してウザい。


CrawlerからTablesを作るときの、テーブル名の命名が柔軟性がない自動生成のみ。

必然的にオブジェクト名が長くなる。
カラムタイプをCSVのデータの中身から推定して作ってくれるのはありがたいんだけど。


じゃあ、しかたない、手動でつくるか、、、

Glue Catalogのテーブル定義、手動ではS3対象しか作れない。

RDBMSのTablesも、手動で作りたいやん。
Crawlerから作ると、「任意のPrefix+スキーマ名+テーブル名」なので、かなり長くなる。そして一覧から見切れる。



オブジェクトの情報の更新が制限される

オブジェクト名の修正ができないとか、一部属性が変更できないとか。

さらに、Glue CatalogのTablesで、S3上のファイルを定義するのにS3 Pathを更新できないの。。。




GlueJobの作成ウィザードが弱い

DataのSourceとTargetを指定して、カラムマッピングするGUIが提供されるんだけど、カラムの表示順序がぐっちゃぐちゃで大混乱。
それでも、SourceとTargetで列名を揃えておくと、自動で対応関係をマップしてくれるからそのときはラクだけど、列名バラバラのときは、自動マップもクッソ適当。

PySparkのコードに落としてからだと、このGUIによるマップの再表示もできないから、修正は常にコード上。
マッピング処理するコードが1行で異常に長いので、これを直すのも結構苦痛。


たぶん、向いているユースケースじゃなかったんだ

と思うことにした。

とにかくGlueが自動生成するPySparkのコードに極力手を加えない、という使い方で。
PySparkを手足のように使える人じゃないと、Glueの良さは引き出せないんじゃないか?

逆に、どんなのが向いていそうか、って考えた。

RDBMSCSVから、JSONApache Parquetへの変換に利用するケース(TargetがRDBMSではなくS3)に向いているかなと。いわゆる「S3をデータレイクとして」っていう、最近流行りの文脈。

S3上にファイルを置いて、それを直接アクセスして分析を行う、Athena/S3 Select/EMR(EMRFS)/Redshift Spectrumを利用する際には、parquetとかの方が速いだろうし、適切なサイズで分割されていたほうがよいでしょう。そのための前処理には扱いやすそう。


今の仕事でそのへんを使うケースは、そうそう無さそうなんだけど。



Sparkの勉強をするいい機会になったし、Sparkの環境構築の手間は省けるし、いい本にも巡り会えたけど、Glue自体は、ワリとツラい経験でした。

アプリケーションエンジニアのためのApache Spark入門

アプリケーションエンジニアのためのApache Spark入門


AWSサポートには大変お世話になりました。

一時期、毎回同じ担当の方がずっと対応してくれたのですが、非常に細かく検証して返答をしてくれ、かなり助かりました。


外資系サービス企業のサポート窓口に問い合わせて、こんな真摯な対応をしてもらったのは始めてです。
最近はその時の担当の方に当たららず、回答も微妙なものが増えてきてる感もありますが、AWSサポートに対してはトータルでは好印象です。
今後も頼らせていただきます。


MySQL Workbenchで、Spatial Functionの結果を図で表示する

$
0
0

「できるらしい」という話だけで、誰も触ってなかった?

clubmysql.connpass.com
に参加して、坂井さんからGISにまつわるお話をたっぷり伺ってきました。

で、坂井さんも、「MySQL WorkbenchがGIS対応してるらしいって聞いたことがあるけど、触ったこと無い」とおっしゃってまして。

で、ツイートしたところ、こうなって、

こうなりました。



こんな感じです。

試したのは、MySQL Workbench 8.0.12+MySQL 8.0.12ですが、MySQL Workbenchはもっと前のバージョンから使えるようですね。

f:id:atsuizo:20180901154041p:plain

投げたSQL

SELECT ST_GeomFromText('LINESTRING(1 1, 2 2, 3 1, 4 3, 3 5, 2 4, 1 1)') len;

で、「Y座標 X座標」「Y座標 X座標」*1を「,」で区切った7か所を頂点としてつないだ線を地理情報に変換する関数を実行しているのですが、バイナリで返ってくるので、最初は「BLOB」って表示されます。
厳密にはBLOBじゃない、Spatial専用のデータ型らしいですが。


そこで、結果表示の右下に、表示形式を選べるところがあって、画像右下の赤丸をつけたところの「Spatial view」を選択すると、上記のキャプチャのように表示されます。


なお、「LINESTRING」を使っていますが、出発点に戻ってきているので、

SELECT ST_GeomFromText('POLYGON((1 1, 2 2, 3 1, 4 3, 3 5, 2 4, 1 1))') pol;

にすると、面として認識されて、線で囲まれた内側が色塗られて表示されます。


Spatial Viewは、Spatial Viewで表示できる結果のときでないと、選択肢として表示されないようですね。


なお、
https://mysqlworkbench.org/2014/09/mysql-workbench-6-2-spatial-data/
によると、緯度経度の情報とSRIDを指定すると、さらにかっこよく表示されるようですが、そこは確認できませんでした。
Form Viewの中にも表示されるっぽいけど、これも未確認。。。


とりあえず、頭の中で描いたり、手書きで座標系を確認しながら、じゃなくてもMySQL Workbenchでできるぜ、ってことが確認できたので良かったです。

というわけで


とりあえず1本書いたぜ!



自分、GIS解らなすぎて、どんな本を推薦していいのかわからない模様。

地理空間情報を活かす授業のためのGIS教材

地理空間情報を活かす授業のためのGIS教材

*1:緯度経度のときはY Xだけど、SRID=0の平面座標のときはX Yの順でした

MySQLへのJDBC接続で、とあるバグを踏むまでの話 -(1)「max_allowed_packet」の基本的な働き

$
0
0

3エントリに渡ってお送りいたします

(1)「max_allowed_packet」の基本的な働き <- イマココ
(2)「Connector/J」は結構賢い
(3)古いmariadb-client-javaにて、ヤバイ動き

概要:max_allowed_packetとは

平たく言うと、MySQLのクライアントーサーバー間の通信のパケットサイズの最大値の設定で、1回のリクエストでここの値より長いリクエストを送り込めない、というものです。

MySQL :: MySQL 5.6 リファレンスマニュアル :: 5.1.4 サーバーシステム変数



どんなときに意識する?

MySQL 5.6以降のデフォルトは4MBなので、よほどのことがない限り、デフォルトのままで問題ない、と考えています。


で、「よほどのこと」の例は、

  • BLOBで大きなデータを格納したい。
  • 大量のレコードをバルクインサートでインサートを高速投入したい。

あたりでしょうか。


動的変更が可能(権限さえあれば、サーバー再起動がいらない)ですが、グローバルレベル変数で、セッション変数は読み取り専用、さらに、ログイン時にグローバルの設定値をコピーしてくる性質のものですので、常時実行するようなプログラムの中で必要時だけ引き上げる、というケースは少ないかと思います。

また、各セッション毎に適用される「スレッドバッファ」に該当するものなので、「max_allowed_packet*コネクション数」のメモリをサーバーサイドで消費する可能性があり、闇雲に引き上げるとサーバがあでメモリ不足を起こします。*1

なので、メンテナンスに際して引き上げることによって作業が高速化する、といったケースで一時的に変更する以外は、設計段階である程度計算して見積もりを行ったあとは固定しておく類のもの、と考えています。

max_allowed_packetのエラーを起こしてみる

実験用のテーブルとSQLの準備

実験用のスキーマ(データベース)に、雑にテーブルを作ります。

createdatabase bulk_test;
use bulk_test;
CREATETABLE `loadtest` (
  `id` int(11) NOTNULL,
  `data1` varchar(1000)DEFAULTNULL,
  `data2` varchar(1000)DEFAULTNULL,
  PRIMARYKEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


このテーブルに対して、雑にバルクインサートの量を調整して、いろいろ実験していきます。

サーバーシステム変数「max_allowed_packet」の変更

まずはデフォルトの確認。

mysql> select@@global.max_allowed_packet, @@session.max_allowed_packet;
+-----------------------------+------------------------------+
| @@global.max_allowed_packet | @@session.max_allowed_packet |
+-----------------------------+------------------------------+
|                     4194304 |                      4194304 |
+-----------------------------+------------------------------+
1rowin set (0.00 sec)

エラーを引き起こしやすいように、サイズを激しく小さくしてみます。
max_allowed_packetの最小値は1024バイトです。
my.cnfに書いて再起動でもいいですが、とりあえず動的変更で。

mysql> set @@global.max_allowed_packet = 1024;
Query OK, 0rows affected (0.00 sec)


確認すると、グローバル変数側は更新されますが、セッション変数は変更されません。

mysql> select@@global.max_allowed_packet, @@session.max_allowed_packet;
+-----------------------------+------------------------------+
| @@global.max_allowed_packet | @@session.max_allowed_packet |
+-----------------------------+------------------------------+
|                        1024 |                      4194304 |
+-----------------------------+------------------------------+
1rowin set (0.00 sec)


再接続して、確認します。

mysql> connect;
Connection id:    10
Current database: *** NONE ***

mysql> select@@global.max_allowed_packet, @@session.max_allowed_packet;
+-----------------------------+------------------------------+
| @@global.max_allowed_packet | @@session.max_allowed_packet |
+-----------------------------+------------------------------+
|                        1024 |                         1024 |
+-----------------------------+------------------------------+
1rowin set (0.00 sec)

セッション側にも適用されていることを確認できました。

mysqlクライアントから投入試験

SQL文が1024バイト以上になるように、Insert文を作って投入します。
ポイントは、REPEAT関数ではダメで、あくまでSQL文自体の長さを稼ぐ、というところです。

mysql> insertinto loadtest (id, data1,data2) values (1, 'aaaa・・・記述省略、2列あるvarchar列に1000個の「a」を並べて投入・・・);Query OK, 1 row affected (0.00 sec)

2000 byte強のデータを送り込んでいるのに、通った!?

net_buffer_lengthのいたずら

ドキュメントをしっかり読むと、このように書いてあります。

  • max_allowed_packet

パケットメッセージバッファーは net_buffer_length バイトに初期化されますが、必要に応じて max_allowed_packet バイトまで大きくできます。この値はデフォルトでは小さいため、大きい (正しくない可能性がある) パケットをキャッチできません。
https://dev.mysql.com/doc/refman/5.6/ja/server-system-variables.html#sysvar_max_allowed_packet

  • net_buffer_length

各クライアントスレッドは、接続バッファーおよび結果バッファーに関連付けられています。両者は net_buffer_length で与えられたサイズで開始されますが、必要に応じて、max_allowed_packet バイトまで動的に拡大できます。結果バッファーは、各 SQLステートメントのあとで net_buffer_length に縮小されます。

この変数は通常は変更しませんが、メモリーが非常に少ない場合、クライアントによって送信される予想されるステートメントの長さに設定できます。ステートメントがこの長さを超えた場合、接続バッファーは自動的に拡大されます。net_buffer_length の最大値は 1M バイトに設定できます。
https://dev.mysql.com/doc/refman/5.6/ja/server-system-variables.html#sysvar_net_buffer_length


どうなっているかというと、先程max_allowed_packetだけを変更したので、net_buffer_lengthにはデフォルト値が入っています。

mysql> select@@session.max_allowed_packet, @@session.net_buffer_length;
+------------------------------+-----------------------------+
| @@session.max_allowed_packet | @@session.net_buffer_length |
+------------------------------+-----------------------------+
|                         1024 |                       16384 |
+------------------------------+-----------------------------+
1rowin set (0.00 sec)

16KBですね。


というわけで、バルクインサートで、先程の例の通り、2列のvarchar列にフル桁で文字列を埋める処理を、1行~9行をインサートするファイル「bulk1.sql~bulk9.sql」を用意して、sourceコマンドを実行して試してみます。

ll bulk*
-rw-r--r-- 1 root root  205992422:442018 bulk1.sql
-rw-r--r-- 1 root root  407292422:442018 bulk2.sql
-rw-r--r-- 1 root root  608592422:312018 bulk3.sql
-rw-r--r-- 1 root root  809892422:452018 bulk4.sql
-rw-r--r-- 1 root root 1011192422:452018 bulk5.sql
-rw-r--r-- 1 root root 1212492422:462018 bulk6.sql
-rw-r--r-- 1 root root 1413792422:472018 bulk7.sql
-rw-r--r-- 1 root root 1615092422:472018 bulk8.sql
-rw-r--r-- 1 root root 1816392422:352018 bulk9.sql

mysql -u root -p-D bulk_test
mysql> truncate table loadtest;
source ./bulk1.sql
Query OK, 0rows affected (0.03 sec)

mysql> source ./bulk1.sql
Query OK, 1row affected (0.00 sec)

mysql> truncate table loadtest;
Query OK, 0rows affected (0.01 sec)

mysql> source ./bulk2.sql
Query OK, 2rows affected (0.00 sec)
Records: 2  Duplicates: 0  Warnings: 0

mysql> truncate table loadtest;
Query OK, 0rows affected (0.01 sec)

mysql> source ./bulk3.sql
Query OK, 3rows affected (0.00 sec)
Records: 3  Duplicates: 0  Warnings: 0

mysql> truncate table loadtest;
Query OK, 0rows affected (0.01 sec)

mysql> source ./bulk4.sql
Query OK, 4rows affected (0.01 sec)
Records: 4  Duplicates: 0  Warnings: 0

mysql> truncate table loadtest;
Query OK, 0rows affected (0.01 sec)

mysql> source ./bulk5.sql
Query OK, 5rows affected (0.00 sec)
Records: 5  Duplicates: 0  Warnings: 0

mysql>
mysql> truncate table loadtest;
Query OK, 0rows affected (0.01 sec)

mysql> source ./bulk6.sql
Query OK, 6rows affected (0.00 sec)
Records: 6  Duplicates: 0  Warnings: 0

mysql> truncate table loadtest;
Query OK, 0rows affected (0.01 sec)

mysql> source ./bulk7.sql
Query OK, 7rows affected (0.00 sec)
Records: 7  Duplicates: 0  Warnings: 0

mysql>
mysql> truncate table loadtest;
Query OK, 0rows affected (0.01 sec)

mysql> source ./bulk8.sql
Query OK, 8rows affected (0.00 sec)
Records: 8  Duplicates: 0  Warnings: 0

mysql> truncate table loadtest;
Query OK, 0rows affected (0.01 sec)

mysql> source ./bulk9.sql
ERROR 1153 (08S01): Got a packet bigger than 'max_allowed_packet' bytes

9行分のバルクインサート、「18163 byteのインサート文」を実行したところで、max_allowed_packet起因のエラーが出ました。

max_allowed_packetではなく、net_buffer_lengthの16KBに引っ張られた、と考えられます。


net_buffer_length=max_alloewd_packetでやってみる

では、net_buffer_lengthも小さくしてみましょう。

mysql> set @@global.net_buffer_length=1024;
Query OK, 0rows affected (0.00 sec)

mysql> connect;
Connection id:    51
Current database: bulk_test

mysql> select@@session.max_allowed_packet, @@session.net_buffer_length;
+------------------------------+-----------------------------+
| @@session.max_allowed_packet | @@session.net_buffer_length |
+------------------------------+-----------------------------+
|                         1024 |                        1024 |
+------------------------------+-----------------------------+
1rowin set (0.00 sec)

ここに、先程のバルクインサート用SQLを順に入れていきます。

mysql> truncate table loadtest;
source ./bulk1.sql
Query OK, 0rows affected (0.02 sec)

mysql> source ./bulk1.sql
ERROR 1153 (08S01): Got a packet bigger than 'max_allowed_packet' bytes

1つ目、2000 byte以上のインサートできっちり落ちました。

なお、このbulk1.sqlをベースに、「インサート文全体で1024 byte / 1025byteなファイル」を用意したところ、以下のようになります。

ll bulk_102*
-rw-r--r-- 1 root root 1024  9月 24 23:26 2018 bulk_1024.sql
-rw-r--r-- 1 root root 1025  9月 24 23:27 2018 bulk_1025.sql

mysql -u root -p --max_allowed_packet=1024 --net_buffer_length=1024 -D bulk_test
mysql>  select@@session.max_allowed_packet, @@session.net_buffer_length;
+------------------------------+-----------------------------+
| @@session.max_allowed_packet | @@session.net_buffer_length |
+------------------------------+-----------------------------+
|                         1024 |                        1024 |
+------------------------------+-----------------------------+
1rowin set (0.00 sec)

mysql> source ./bulk_1024.sql
Query OK, 1row affected (0.00 sec)

mysql>
mysql> truncate table loadtest;
Query OK, 0rows affected (0.00 sec)

mysql> source ./bulk_1025.sql
ERROR 1153 (08S01): Got a packet bigger than 'max_allowed_packet' bytes
net_buffer_length < max_allowed_packetも試す
  • net_buffer_length:1024 byte
  • max_allowed_packet:4096 byte

の条件でやってみます。

mysql> set @@global.net_buffer_length=1024;
Query OK, 0rows affected (0.00 sec)

mysql> set @@global.max_allowed_packet=4096;
Query OK, 0rows affected (0.00 sec)

mysql> connect;
Connection id:    59
Current database: bulk_test



mysql> select@@session.max_allowed_packet, @@session.net_buffer_length;
+------------------------------+-----------------------------+
| @@session.max_allowed_packet | @@session.net_buffer_length |
+------------------------------+-----------------------------+
|                         4096 |                        1024 |
+------------------------------+-----------------------------+
1rowin set (0.00 sec)

mysql> truncate table loadtest;
source ./bulk1.sql
Query OK, 0rows affected (0.01 sec)

mysql> source ./bulk1.sql
Query OK, 1row affected (0.00 sec)

mysql>
mysql> truncate table loadtest;
source ./bulk2.sql
Query OK, 0rows affected (0.02 sec)

mysql> source ./bulk2.sql
Query OK, 2rows affected (0.00 sec)
Records: 2  Duplicates: 0  Warnings: 0

mysql>
mysql> truncate table loadtest;
Query OK, 0rows affected (0.01 sec)

mysql> source ./bulk3.sql
ERROR 1153 (08S01): Got a packet bigger than 'max_allowed_packet' bytes

max_allowed_packetの値を超えたところで落ちました。

まとめ

  • 1SQLのサイズ上限は、max_allowed_packetとnet_buffer_lengthで制御される。
  • max_allowed_packetはグローバルにデフォルト値を持ち、コネクション確立時にセッションにコピーされ、セッション変数は読み込み専用である。(セッションに適用するにはconnectなど再接続が必要)
  • net_buffer_lengthはグローバルにデフォルト値を持ち、コネクション確立時にセッションにコピーされ、セッション変数も変更可能である。
  • net_buffer_length => max_allowed_packetの場合、net_buffer_lengthが1SQLの上限値になる。
  • net_buffer_length < max_allowed_packetの場合、max_allowed_packetが1SQLの上限値になる。

本当は、、、

max_allowed_packetとnet_buffer_lengthの影響についてmysqlCLIクライアントで検証する作業に、こんなに時間掛ける予定じゃなかったんだ。。。
本来扱いたかった、JDBC Connector(Connector/J)とBulkInsertの話は次回。



実践ハイパフォーマンスMySQL 第3版

実践ハイパフォーマンスMySQL 第3版

*1:かならず指定値分を確保するわけではなく、必要に応じて確保する最大値。

MySQLへのJDBC接続で、とあるバグを踏むまでの話 -(2)「Connector/J」は結構賢い

$
0
0

3エントリに渡ってお送りしています

(1)「max_allowed_packet」の基本的な働き
(2)「Connector/J」は結構賢い <- イマココ
(3)古いmariadb-client-javaにて、ヤバイ動き

Javaで開発するときにお世話になるヤーツ

MySQLをDBとして採用したとき、アプリケーションをJavaで開発するとなると、JDBCドライバのお世話になります。

MySQL公式としては「Connector/J」と呼ばれるJDBCドライバが提供されているので、通常こちらを使用します。

8.0版も出ていますが、まだまだ5系のこちらを使っていることが多いでしょう。
MySQL :: MySQL Connector/J 5.1 Developer Guide


JDBC経由でバルクインサートする準備

JDBCドライバの準備

MySQL公式のサイトからダウンロードしてきます。
https://dev.mysql.com/downloads/connector/j/

今回は、「5.1.47」のtar.gz版を使います。
解凍して、パスが通るようにしておきます。

めんどくさいので、次に書くソースと同じ場所に落として解凍してます。

ついでに、当然ですがJDKも。
今回は、たまたま検証環境に入っていたOracleのヤツ「java version "1.8.0_152"」を使います。

Javaソースの準備

前回の記事で用意したテーブル&バルクインサートの実行をやるにあたって、下記のようなソースを用意します。本職プログラマじゃないこともあり、汚いですがご容赦を。

やっていることは

  • 接続する。バルクインサートが使えるように「rewriteBatchedStatements=true」にする。
  • 投入先テーブルを初期化(Truncate)する
  • プリペアドステートメントを使いながら、Insert文を組み立てて、バッチの準備をする。(conn.prepareStatement-> setString -> addBatch)
  • 規定数に到達したらバルクインサートを実行する。(executebatch)

といった内容です。

50,000件を、1,000件毎にバルクインサートして、10,000件毎にcommitを入れる、という設定にしてあります。

mport java.sql.*;

class BulkTestMySQL {
    publicstaticvoid main(String[] args) {
        String driver        = "org.gjt.mm.mysql.Driver";
        String JDBC_URL      ="jdbc:mysql://localhost";
        String JDBC_USER     = "testbulk";
        String JDBC_PASS     = "testbulk";
        String JDBC_DBNAME   = "bulk_test";

        String DATA = "";

        int COUNT       = 50000; // total insert recordint BATCH_SIZE  =  1000; // bulk per insertint COMMIT_SIZE = 10000;

        String BULK_MODE = "true";

        int STR_LENGTH = 1000;
        for (int i = 1; i <= STR_LENGTH; i++) {
            DATA = DATA + "a";
        }
        try{
            Class.forName (driver);

            Connection conn = DriverManager.getConnection(JDBC_URL + "/" + JDBC_DBNAME + "?rewriteBatchedStatements=" + BULK_MODE,JDBC_USER, JDBC_PASS);
            conn.setAutoCommit(false);

            // テーブルの初期化
            Statement stmt = conn.createStatement ();
            String sql = "TRUNCATE TABLE loadtest";
            //MySQL ConnectorだとDDLや更新系DMLはexecuteQueryでは実行できない// ResultSet rs = stmt.executeQuery (sql);
            stmt.executeUpdate(sql);
            // BulkInsertTest本体
            PreparedStatement pstmt = conn.prepareStatement("insert into loadtest (id, data1,data2) values (?, ?, ?)");
            for (int i = 1; i <= COUNT; i += BATCH_SIZE) {
                pstmt.clearBatch();
                for (int j = 0; j < BATCH_SIZE; j++) {
                    pstmt.setInt(1, i + j);

                    if (j == 530) {
                        pstmt.setString(2, DATA);
                        pstmt.setString(3, "b");
                    }else{
                        pstmt.setString(2, DATA);
                        pstmt.setString(3, DATA);
                    }
                    pstmt.addBatch();
                }
                pstmt.executeBatch();
                if ((i + BATCH_SIZE - 1) % COMMIT_SIZE == 0) {
                    conn.commit();
                }
            }
            conn.commit();
       } catch (SQLException e) {
            System.err.println("SQL failed.");
            e.printStackTrace ();
        } catch (ClassNotFoundException ex) {
            ex.printStackTrace ();
        }
    }
}
コンパイル&実行

なにぶん、IDEなど用意せず、すべてvi+コマンドで処理しているものでして、以下のようなシェルを用意します。
connecctor/Jのjarは、このJavaファイルと同じ場所にダウンロードしてtar.gzを展開しておいた、という状態です。


  • BulkTestMySQLCompileAndRun.sh
javac BulkTestMySQL.java
export CLASSPATH=./mysql-connector-java-5.1.47/mysql-connector-java-5.1.47.jar:.
java BulkTestMySQL

これで、ソース上に入れた設定を変えて、再テストするときに、このシェル起動で実行できる、と。

実際にJDBC経由でバルクインサートを実行してみる。

まずは、デフォルトの状態から。

mysql -u testbulk -p-e"select @@global.max_allowed_packet, @@global.net_buffer_length"
+-----------------------------+----------------------------+
| @@global.max_allowed_packet | @@global.net_buffer_length |
+-----------------------------+----------------------------+
|                     4194304 |                      16384 |
+-----------------------------+----------------------------+

./BulkTestMySQLCompileAndRun.sh

なんかごっちゃごちゃとSSL絡みのワーニング*1が出ますが、無視するとして、デフォルトだとさらっと終了します。

50000件、途中で落ちたりせずに入っているか念の為確認。

mysql -u testbulk -p-D bulk_test -e"select count(*) from loadtest;select length(data1),length(data2) from loadtest limit 10;"
+----------+
| count(*) |
+----------+
|    50000 |
+----------+
+---------------+---------------+
| length(data1) | length(data2) |
+---------------+---------------+
|          1000 |          1000 |
|          1000 |          1000 |
|          1000 |          1000 |
|          1000 |          1000 |
|          1000 |          1000 |
|          1000 |          1000 |
|          1000 |          1000 |
|          1000 |          1000 |
|          1000 |          1000 |
|          1000 |          1000 |
+---------------+---------------+

max_allowed_packetを小さくしてみる

1MBまで小さくして、同じソースを実行してみます。

mysql -u root -p -e "set @@global.max_allowed_packet = 1048576"

mysql -u testbulk -p -e "select @@global.max_allowed_packet, @@global.net_buffer_length"
+-----------------------------+----------------------------+
| @@global.max_allowed_packet | @@global.net_buffer_length |
+-----------------------------+----------------------------+
|                     1048576 |                      16384 |
+-----------------------------+----------------------------+
./BulkTestMySQLCompileAndRun.sh

見かけ上、何も変化なく、さらっと通りました。
あえて記載もしませんが、データはもれなく格納されています。

よく考えてみましょう。

冒頭で

50,000件を、1,000件毎にバルクインサートして、10,000件毎にcommitを入れる、という設定にしてあります。

と書きました。

1レコードは、連番(最大50,000=5桁)+1,000桁の文字列+1,000桁の文字列で=2,000強です。

前回の検証から、max_allowed_packet > >net_buffer_lengthなので、max_allowed_packetが1SQLでの最大値です。

2,000強byte*1,000行+INSERT文の他の要素 > max_allowed_packet(1MB=1024*1024 byte)ですが、処理が落ちません。


何が起こっているのか。。。


考えられるのは、max_allowed_packetが設定したものと違う値が採用されている」か、「JDBCドライバの中でmax_allowed_packetに即したバルクインサートSQL文を生成している」か。。。


先に正解を書くと、後者「JDBCドライバの中でmax_allowed_packetに即したバルクインサートSQL文を生成している」です。結構賢い。


しかし、Connector/Jの公式ドキュメント、そんな話が一切出てこないし、そもそも「rewriteBatchedStatements」プロパティに言及があるだけで、プリペアドステートメントからのバルクインサート実行のサンプルも何もない。つらい。


何が起こっているかの確認:SHOW STATUS

まず、このサンプルソースで、インサート文が実際に何本実行されているかを確認します。

SHOW STATUSで、mysqld(MySQLサーバー)が起動してから、何がどれくらい動いたか、という累積情報を確認することができます。
今回は「Com_insert」=INSERT文が何回実行されたか(≠挿入行数。=実行数)、を確認します。

service mysqld stop
mysqld を停止中:                                           [  OK  ]
service mysqld start
mysqld を起動中:                                           [  OK  ]

mysql -u testbulk -p-e"select @@global.max_allowed_packet, @@global.net_buffer_length"
+-----------------------------+----------------------------+
| @@global.max_allowed_packet | @@global.net_buffer_length |
+-----------------------------+----------------------------+
|                     4194304 |                      16384 |
+-----------------------------+----------------------------+

mysql -u testbulk -p-e"show global status like 'Com_insert';"
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Com_insert    | 0     |
+---------------+-------+

./BulkTestMySQLCompileAndRun.sh
mysql -u testbulk -p-e"show global status like 'Com_insert';"
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Com_insert    | 50    |
+---------------+-------+


デフォルト状態では、INSERT文が50回実行されています。

50,000件を、1,000件毎にバルクインサートして、10,000件毎にcommitを入れる、という設定にしてあります。

なので、計算通り「50,000/1,000=50」で、50回のバルクインサート文が実行されていることになります。


では、max_allowed_packetを小さくしたケースではどうでしょうか。

service mysqld stop
mysqld を停止中:                                           [  OK  ]
service mysqld start
mysqld を起動中:                                           [  OK  ]

mysql -u root -p-e"set @@global.max_allowed_packet = 1048576"

mysql -u testbulk -p-e"select @@global.max_allowed_packet, @@global.net_buffer_length"
Enter password:
+-----------------------------+----------------------------+
| @@global.max_allowed_packet | @@global.net_buffer_length |
+-----------------------------+----------------------------+
|                     1048576 |                      16384 |
+-----------------------------+----------------------------+

mysql -u testbulk -p-e"show global status like 'Com_insert';"
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Com_insert    | 0     |
+---------------+-------+

./BulkTestMySQLCompileAndRun.sh

mysql -u testbulk -p-e"show global status like 'Com_insert';"

mysql -u testbulk -p-e"show global status like 'Com_insert';"
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Com_insert    | 100   |
+---------------+-------+


先程の2倍、100回カウントされています。

Javaソースコード上、executeBatchによるバルクインサート実行は50回しか実行していないはずですが、MySQLサーバー側のカウンタは100回実行した、と認識しているわけです。
それでいて、実際格納されている件数は50,000件と、想定どおりです。


となると、JDBCレベルで発行するSQLを変えている、と考えるのが筋ですね。公式ドキュメントにはどこにも見当たらないですが。


何が起こっているかの確認:General Log

といっても、ブログに1行2MB*数百行分のバルクインサートのGeneral Logを貼り付けるのは書く方も読む方も苦痛です。主キーもないし。SQLだけで表現するのもつらい。


こういうときは、MySQL Workbenchのお世話になります。



my.cnfを以下のように設定してからMySQLサーバーを再起動し、General Logをテーブルに出力するようにしてから、先程のようにmax_allowed_packet=1MBで実行します。

general_log
log_output=TABLE

実行後、MySQL WorkbenchでGeneral Logテーブルを検索します。
あえてユーザーを分けていたのは、このためだったりします。

select * from mysql.general_log where user_host like 'testbulk%';

すると、結果一覧がグリッド表示されて、実行されたSQLを示すargument列は、「BLOB」と表示されますので、そこをカーソル選択して右クリックから「Open Value in Viewer」しましょう。

f:id:atsuizo:20180925173617p:plain

以下のように、中身が表示されます。

f:id:atsuizo:20180925173630p:plain

そして、このSQLの最後に注目。

f:id:atsuizo:20180925173642p:plain

519番で切れています。さらに次のログを見ると、520番から始まっています。

f:id:atsuizo:20180925173657p:plain


1回のバルクインサートだと溢れてしまうので、max_allowed_packetのサイズに応じて分解したバルクインサート文を生成して発行しています。

こういった調整を、JDBCドライバのレベルでやってくれているので、プログラマがmax_allowed_packetやnet_buffer_lengthの値と挙動を事細かに知らなくてもエラーになりにくくなっています。

表題にあった「Connector/Jは結構賢い」とは、こういうことです。


逆に、エラーにならないから、裏でSQLが何回発行しているとか、気にしないよね。。。(フラグ)



なお、あくまでexecuteBatch()でJDBCドライバ自身がバルクインサート文を組み立てるケースに限ること、合わせて、サーバーサイドプリペアドステートメントでは使用できないことに注意です。
後者については公式ドキュメントに書いてあります。

rewriteBatchedStatements

Should the driver use multiqueries (irregardless of the setting of "allowMultiQueries") as well as rewriting of prepared statements for INSERT into multi-value inserts when executeBatch() is called? Notice that this has the potential for SQL injection if using plain java.sql.Statements and your code doesn't sanitize input correctly. Notice that for prepared statements, server-side prepared statements can not currently take advantage of this rewrite option, and that if you don't specify stream lengths when using PreparedStatement.set*Stream(), the driver won't be able to determine the optimum number of parameters per batch and you might receive an error from the driver that the resultant packet is too large. Statement.getGeneratedKeys() for these rewritten statements only works when the entire batch includes INSERT statements. Please be aware using rewriteBatchedStatements=true with INSERT .. ON DUPLICATE KEY UPDATE that for rewritten statement server returns only one value as sum of all affected (or found) rows in batch and it isn't possible to map it correctly to initial statements; in this case driver returns 0 as a result of each batch statement if total count was 0, and the Statement.SUCCESS_NO_INFO as a result of each batch statement if total count was > 0.

Default: false

Since version: 3.1.13

MySQL :: MySQL Connector/J 5.1 Developer Guide :: 5.3 Configuration Properties for Connector/J

まとめ

  • MySQL Connector/Jは、バルクインサート文を自動で構築してくれる機能がある。
  • MySQL Connector/Jは、バルクインサート文自動生成機能では、max_allowed_packetの設定に合わせて、バルクインサートのSQL文の大きさを調整してくれる。


MySQLをガチで触り始めて3年、MySQLについて「おバカ」「キモい」「カワイイ」という発言はしたことがあるけど、「賢い」と形容するのは初めてかもしれないです。

本丸はこれからだ、、、。

まあ、今回はMySQL自身の問題ではないのだけど。フラグは次回、回収されます。
ていうかタイトル先出ししているのでネタバレしてるわけですが。


実践ハイパフォーマンスMySQL 第3版

実践ハイパフォーマンスMySQL 第3版

に、Connector/Jの話が書いてあったかどうかは忘れた。

*1:WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.

MySQLへのJDBC接続で、とあるバグを踏むまでの話 -(3)古いmariadb-client-javaにて、ヤバイ動き 

$
0
0

3エントリに渡ってお送りしています

(1)「max_allowed_packet」の基本的な働き
(2)「Connector/J」は結構賢い
(3)古いmariadb-client-javaにて、ヤバイ動き <- イマココ

MySQL本体同様、JDBCドライバにもforkプロダクト「MariaDB Connector/J(mariadb-client-java)」があります。

mariadb.com

forkって書いたけど、connector/Jのソースからフォークしたのか、DB本体はforkしたけどJDBCドライバは別で作ったのかは、そのへんの歴史は正直知らないです、ごめんなさい。


とりあえず、同じ用に使えるってことで、MariaDB Connector/Jを使っておきながら、MySQLのDBに接続するのに、JDBC URLを「jdbc:mysql://」で書いても「jdbc:mariadb://」で書いても、ホストやポートが正しければ繋がる子です。


で、AWSさんの資料とかセミナーとか見てると、「Aurora MySQL Compatibility」に接続するJDBCドライバとしてこちらの「MariaDB Connector/J」を推す話がちらほら見かけられます。

EC2 AmazonLinuxyumリポジトリで取得できる「MariaDB Connector/J」のバージョンが古い件

jarだし、yumでいれなくても、開発したアプリで必要な他のjarと一緒にデプロイすればヨクネ?って話はともかく。

cat /etc/system-release
Amazon Linux AMI release 2018.03

yum list available | grep maria
mariadb-connector-java.noarch         1.3.6-1.5.amzn1               amzn-main

「MariaDBconnector/J 1.3.6」って、2016年2月29日リリースですよ。
https://mariadb.com/kb/en/library/mariadb-connector-j-136-release-notes/

まあ、眼の前で必要としているシステムに組み込んで動けば何でもいいwので、動かしてみます。


該当のバージョンのライブラリをmariadbのサイトから入手して、MySQL版のconnector/Jと同じスクリプトを回してみます。

Javaソースの準備

ほぼほぼ前回の流用です。

  • BulkTestMariaDB.java
import java.sql.*;

class BulkTestMariaDB{
    publicstaticvoid main(String[] args) {
        String driver        = "org.mariadb.jdbc.Driver";
        String JDBC_URL      ="jdbc:mariadb://localhost";
        String JDBC_USER     = "testbulk";
        String JDBC_PASS     = "testbulk";
        String JDBC_DBNAME   = "bulk_test";

        String DATA = "";

        int COUNT       = 50000; // total insert recordint BATCH_SIZE  =  1000; // bulk per insertint COMMIT_SIZE = 10000;

        String BULK_MODE = "true";

        int STR_LENGTH = 1000;
        for (int i = 1; i <= STR_LENGTH; i++) {
            DATA = DATA + "a";
        }
        try{
            Class.forName (driver);

            Connection conn = DriverManager.getConnection(JDBC_URL + "/" + JDBC_DBNAME + "?rewriteBatchedStatements=" + BULK_MODE,JDBC_USER, JDBC_PASS);
            conn.setAutoCommit(false);

            // テーブルの初期化
            Statement stmt = conn.createStatement ();
            String sql = "TRUNCATE TABLE loadtest";
            ResultSet rs = stmt.executeQuery (sql);

            // BulkInsertTest本体
            PreparedStatement pstmt = conn.prepareStatement("insert into loadtest (id, data1,data2) values (?, ?, ?)");
            for (int i = 1; i <= COUNT; i += BATCH_SIZE) {
                pstmt.clearBatch();
                for (int j = 0; j < BATCH_SIZE; j++) {
                    pstmt.setInt(1, i + j);

                    if (j == 530) {
                        pstmt.setString(2, DATA);
                        pstmt.setString(3, "b");
                    }else{
                        pstmt.setString(2, DATA);
                        pstmt.setString(3, DATA);
                    }
                    pstmt.addBatch();
                }
                pstmt.executeBatch();
                if ((i + BATCH_SIZE - 1) % COMMIT_SIZE == 0) {
                    conn.commit();
                }
            }
            conn.commit();
       } catch (SQLException e) {
            System.err.println("SQL failed.");
            e.printStackTrace ();
        } catch (ClassNotFoundException ex) {
            ex.printStackTrace ();
        }
    }
}
コンパイル&実行

こちらも前回と同じ条件で、MariaDBのConnector/J版を用意。

  • BulkTestMySQLCompileAndRun.sh
javac BulkTestMariaDB.java
export CLASSPATH=./mariadb-java-client-1.3.6.jar:.
java BulkTestMariaDB

実際にJDBC経由でバルクインサートを実行してみる。

まずは、デフォルトの状態から。

mysql -u testbulk -p-e"select @@global.max_allowed_packet, @@global.net_buffer_length"
+-----------------------------+----------------------------+
| @@global.max_allowed_packet | @@global.net_buffer_length |
+-----------------------------+----------------------------+
|                     4194304 |                      16384 |
+-----------------------------+----------------------------+

./BulkTestMySQLCompileAndRun.sh

mysql -u testbulk -p-D bulk_test -e"select count(*) from loadtest;select length(data1),length(data2) from loadtest limit 10;"
+----------+
| count(*) |
+----------+
|    50000 |
+----------+
+---------------+---------------+
| length(data1) | length(data2) |
+---------------+---------------+
|          1000 |          1000 |
|          1000 |          1000 |
|          1000 |          1000 |
|          1000 |          1000 |
|          1000 |          1000 |
|          1000 |          1000 |
|          1000 |          1000 |
|          1000 |          1000 |
|          1000 |          1000 |
|          1000 |          1000 |
+---------------+---------------+

問題なく、動きました。

max_allowed_packetを小さくしてみる

1MBまで小さくして、同じソースを実行してみます。

service mysqld stop
mysqld を停止中:                                           [  OK  ]
service mysqld start
mysqld を起動中:                                           [  OK  ]

mysql -u root -p -e "set @@global.max_allowed_packet = 1048576"

mysql -u testbulk -p -e "select @@global.max_allowed_packet, @@global.net_buffer_length"
+-----------------------------+----------------------------+
| @@global.max_allowed_packet | @@global.net_buffer_length |
+-----------------------------+----------------------------+
|                     1048576 |                      16384 |
+-----------------------------+----------------------------+

/BulkTestMariaDBCompileAndRun.sh
SQL failed.
java.sql.BatchUpdateException: Duplicate entry '531' for key 'PRIMARY'
        at org.mariadb.jdbc.MariaDbStatement.executeBatch(MariaDbStatement.java:1261)
        at BulkTestMariaDB.main(BulkTestMariaDB.java:50)
Caused by: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '531' for key 'PRIMARY'
        at org.mariadb.jdbc.internal.util.ExceptionMapper.get(ExceptionMapper.java:119)
        at org.mariadb.jdbc.internal.util.ExceptionMapper.throwException(ExceptionMapper.java:69)
        at org.mariadb.jdbc.MariaDbStatement.executeQueryEpilog(MariaDbStatement.java:259)
        at org.mariadb.jdbc.MariaDbStatement.execute(MariaDbStatement.java:323)
        at org.mariadb.jdbc.MariaDbStatement.executeBatch(MariaDbStatement.java:1239)
        ... 1 more
Caused by: org.mariadb.jdbc.internal.util.dao.QueryException: Duplicate entry '531' for key 'PRIMARY'
        at org.mariadb.jdbc.internal.protocol.AbstractQueryProtocol.getResult(AbstractQueryProtocol.java:479)
        at org.mariadb.jdbc.internal.protocol.AbstractQueryProtocol.result(AbstractQueryProtocol.java:400)
        at org.mariadb.jdbc.internal.protocol.AbstractQueryProtocol.executeQuery(AbstractQueryProtocol.java:365)
        at org.mariadb.jdbc.MariaDbStatement.execute(MariaDbStatement.java:316)
        ... 2 more


落ちた!しかも「 Duplicate entry '531' for key 'PRIMARY'」って、主キーの重複違反!

ロジック上、どうやっても主キー重複が発生しないはずなんですけど。。。

何が起こっているかの確認:SHOW STATUS

さっき、再起動してSHOW STATUSのカウンタもリセットしてありますので、さっそくCom_insertの値を見てみます。

mysql -u testbulk -p -e "show global status like 'Com_insert';"
Enter password:
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Com_insert    | 2     |
+---------------+-------+

エラーが起きた番号が531ですし、前回のMySQL Connector/Jと同じ挙動ならば、1,000件ずつのバルクインサートをJDBCドライバが2回に分けて発行した、その2回目にいる番号なので、そんなもんですね。

しかし、なぜ、2回目の531番が主キー違反で落ちるのか。。。


何が起こっているかの確認:General Log

前回同様、MySQL Workbenchで探りを入れてみます。

1回目の最後に、
f:id:atsuizo:20180926134218p:plain


?519のあと、531が入っている!

2回目の最初に、
f:id:atsuizo:20180926134236p:plain


?521から始まってる!520はドコ?!


更に進むと、
f:id:atsuizo:20180926134254p:plain

531がもう1回来てる!


そりゃ、主キー違反になりますがな。


平たく言うと、前回まとめで書いた機能

  • MySQL Connector/Jは、バルクインサート文を自動で構築してくれる機能がある。
  • MySQL Connector/Jは、バルクインサート文自動生成機能では、max_allowed_packetの設定に合わせて、バルクインサートのSQL文の大きさを調整してくれる。

がバグってます。

ここでカラク

前回のJavaソース読んで気づいた人がいるかも知れませんが、531だけ、わざと格納する文字列を短くしてます。通常「a」が1,000個ですが、data2列に入れる文字列を「b」1個だけにしました。

コレ、531番目も同じようにdata2列に「a」を1,000個入れるロジックだと落ちないんです。

いまはテストデータですが、リアルに開発するとき、バルクで取り込むデータの長さってレコードによってまちまちであることのほうが普通ですから、それを模して、長さの違うデータを意図的にインサートさせるためにやってみたものです。

どうやら、mariadb-connector/Jの場合

  • バルクインサート文自動生成機能では、max_allowed_packetの設定に合わせて、バルクインサートのSQL文の大きさを調整してくれる。
  • バルクインサート文の自動生成時、max_allowed_packet値に近づくと、より後方にあるデータで短くて詰め込めそうなものを探して「飛び級?」で入れている。
  • しかし、飛び級?によってスキップしたレコードの一部を失ったり、飛び級したレコードを二重登録してしまう、という不具合が「1.3.6」には潜んでいる。

ってことのようです。


いまはテストデータですが、リアルに開発するとき、バルクで取り込むデータの長さってレコードによってまちまちであることのほうが一般的ですから、このバグを踏む可能性ってフツーにあるわけですね。

最新だとバグってない。

前回エントリで使用した「MySQLのConnector/J」は「5.1.47 (2018-08-17)」と、比較的あたらしいものでした。
古いのと新しいのを比較して、どっちが優れているとか言っても仕方がないですね。MariaDB connector/Jの最新は「2.3.0 (2018-09-07)」で試したところ、この問題は発生しませんでした。


なので、安心して新しいやつを使っていただければと思います。



が、バグレポが見つからない。。。レポが見つからないので、いつFixしたのかもわからない。

とりあえず、力技で、いつ治ったのか探してみました。

1つずつ、バージョンを上げていったところ、トンデモナイやつがいた。

とりあえず、1つ上のもの「1.3.7」を入手して、これを使って実行したら、衝撃の結末が待っていました。

  • BulkTestMySQLCompileAndRun.sh
javac BulkTestMariaDB.java
#export CLASSPATH=./mariadb-java-client-1.3.6.jar:.export CLASSPATH=./mariadb-java-client-1.3.7.jar:.
java BulkTestMariaDB
./BulkTestMariaDBCompileAndRun.sh

mysql -u testbulk -p -D bulk_test -e "select count(*) from loadtest;select length(data1),length(data2) from loadtest limit 10;"
Enter password:
+----------+
| count(*) |
+----------+
|    25910 |
+----------+
+---------------+---------------+
| length(data1) | length(data2) |
+---------------+---------------+
|          1000 |          1000 |
|          1000 |          1000 |
|          1000 |          1000 |
|          1000 |          1000 |
|          1000 |          1000 |
|          1000 |          1000 |
|          1000 |          1000 |
|          1000 |          1000 |
|          1000 |          1000 |
|          1000 |          1000 |
+---------------+---------------+

エラーが出ない!

でも、件数が正しくない!


細かいログは示しませんが、調べた結果、以下のことが起きていました。

  • 1~9999(4桁以下)

各1~519だけ実行、520以上はINSERT文が来ていない。

  • 10000~50000(5桁)

各1~518だけ実行、519以上はINSERT文が来ていない。

  • 結果
番号帯件数/インサートインサート回数小計
1~9999519件10回5190件
10000~50000518件40回20720件
合計50回25910件


ひどい。。。せめてエラーになってくれ。エラーにならずに、データ欠落した状態でインサート成功!(キリッ はヤバいでしょ。


が、列に対する桁溢れを切り捨てて平気な顔をしているMySQLらしさに溢れてる、といえなくもない。(後述するが、本家にはこのバグはない模様)




こんな調査を進めていった結果、以下のようになりました。

MariaDB Connector/J VersionDateバルクインサートの挙動
1.3.52016-02-09分割バグによる主キー違反発生
1.3.62016-02-29分割バグによる主キー違反発生
1.3.72016-03-23分割の後半がINSERTされない
1.4.02016-04-01分割の後半がINSERTされない
1.4.12016-04-11正しく分割&正しく格納


2年以上前のバージョンの挙動差異の確認とか、なにそれ考古学の世界かよ。

追記

1.4.1(ていうか1.4.0~1.4.2)は、別のバグが"Blocker"で報告されています。少なくとも1.4.3までは引き上げる必要があります。
そして、1.4.2~2.2.0RCに影響のあるバグ(古いものには影響がない!)も見つかります。他のバグも含めて、要注意ですね。

じゃあ、同時期のMySQL公式Connector/Jはどうなんだ?

同時期のものを調べてみる
MySQL Connector/J VersionDateバルクインサートの挙動
5.1.372015-10-15正しく分割&正しく格納
5.1.382015-12-07正しく分割&正しく格納
5.1.392016-05-09正しく分割&正しく格納
5.1.402016-10-03正しく分割&正しく格納
5.1.412017-02-28正しく分割&正しく格納

問題なし。

激しく古いヤツを調べてみる。

MySQL本家のConnector/Jが「rewriteBatchedStatements」をサポートしたのは、リファレンスを見ると「Since version: 3.1.13(2006-05-26)」ってことなので、当初どうだったのか調べてみる。

MySQL Connector/J VersionDateバルクインサートの挙動
3.1.132006-05-26バルクインサートしてない!


まさかの、バルクインサートしてない!という。


こうなったら、rewriteBatchedStatementsでバルクインサートをサポートした時期がいつなのか、探してやる。

MySQL Connector/J VersionDateバルクインサートの挙動
3.1.132006-05-26バルクインサートしてない
3.1.142006-10-19バルクインサートしてない
5.0.02005-12-22(入手できず)
5.0.12005-12-22(入手できず)
5.0.22006-07-11(入手できず)
5.0.3(Beta)2006-07-26バルクインサートしてない
5.0.42006-10-20バルクインサートしてない
5.0.52007-03-02正しく分割&正しく格納

見つかりました。


サポートした途端、バグなしで動いています。


で、5.0.5のリリースノートを読んでみたのですが、書いてねえ。。。。
MySQL :: MySQL Connector/J 5.1 Release Notes :: Changes in MySQL Connector/J 5.0.5 (2007-03-02)


ドキュメントの読み込みが甘いかもしれないけど、最新のリファレンスにもそんな機能があるって書いてないし、さもありなん。


まとめ

  • MySQL Connector/Jは、バルクインサート文を自動で構築してくれる機能がある。
  • MySQL互換のMariaDBも、MariaDBconnector/Jを提供していて、同じバルク対応機能もある。
  • MariaDBconnector/J(mariadb-client-java)の古いバージョン(1.4.0以前)には、バグがある。
  • 本家MySQL Connector/JでrewriteBatchedStatementsパラメータが追加されたのは3.1.13(2006-05-26)からだが、このときexecuteBatch()バルクインサート自動生成機能は未対応。
  • 本家MySQL Connector/Jでバルクインサート文を自動で構築してくれる機能は、5.0.5(2007-03-02)から。なお、このバージョンにバグはない。


以上、MySQL&MariaDB Connector/J考古学でした?

最後に

JDBCドライバがバルクインサート文を自動生成してくれる、ってなかなかすごい機能だな、と思いました。
プログラマが意図してexecuteBatchを発行した単位をガン無視して、max_allowed_packetを超えないように丸めて分割してバルクインサート文作ってる、なかなか豪快な機能です。
バグを踏んでない限りエラーにならないから、裏でこんな動きをしているとか、フツーは意識しないよね。


バグは世の常なので、まあ、仕方ないよね。このバグが絡んでるあたりのMariaDB Connector/Jのリリース頻度、10日刻みとかですし、改善されていってるのをひしひしと感じます。


ただし、ドライバは、なるべく新しいものを使おう!


おまけ

Connector/Jでのバルクインサート文の自動生成は、「max_allowed_packet」だけを基準にしています。


なので、

  • net_buffer_length => max_allowed_packetの場合、net_buffer_lengthが1SQLの上限値になる。
  • net_buffer_length < max_allowed_packetの場合、max_allowed_packetが1SQLの上限値になる。

MySQLへのJDBC接続で、とあるバグを踏むまでの話 -(1)「max_allowed_packet」の基本的な働き - なからなLife

は適用されません。ご注意を。



実践ハイパフォーマンスMySQL 第3版

実践ハイパフォーマンスMySQL 第3版

Waiting for table metadata lockの対処の仕方、おさらい。

$
0
0

MySQL Casual Advent Calendar 2018 - Qiita 8日目の記事です。
駆け込み。

やきなおしです。

わりとニーズがあるらしく、このネタで過去エントリにちょいちょいアクセスがくるので、とにかく急いでいる人用に、シンプルにまとめました。
MySQL5.6、5.7で使えます。
たぶん8.0でも使えるんじゃないかな。知らんけど。

MySQL5.7でPerformance_schema.metadata_locksが追加されたけど、これも設定入れないと使えないんだよね。

犯人を特定する。

1. information_schema.innodb_trxを全件参照する。

列は以下の通り。

trx_id	trx_state	trx_started	trx_requested_lock_id	trx_wait_started	trx_weight	trx_mysql_thread_id	trx_query	trx_operation_state	trx_tables_in_use	trx_tables_locked	trx_lock_structs trx_lock_memory_bytes	trx_rows_locked	trx_rows_modified	trx_concurrency_tickets	trx_isolation_level	trx_unique_checks	trx_foreign_key_checks	trx_last_foreign_key_error	trx_adaptive_hash_latched	trx_adaptive_hash_timeout trx_is_read_only	trx_autocommit_non_locking
2. trx_stateが「RUNNING」で、trx_startedが異常に古いもの(表示される中で一番古いもの。*1)を探し、trx_mysql_thread_idを控える。


9割方、こいつがトランザクションを開始してテーブルを触った(metadata lockを取得した)まま、Commit/Rollbackしない(metadata lockを解放しない)まま放置して、DDLが阻害されてる。
この場合、SQL自体はもう発行されてない(終わっている)状態でSLEEPしてるんだよね。

純粋に長いSQLが実行中で「Waiting for table metadata lock」が出た場合は、終わるのを待っていればそのうち解消します。
日単位で返ってこないSQLでなければね。

3. PROCESS権限を持っているMySQLユーザーで、SHOW FULL PROCESSLIST(Information_schema.PROCESSLIST)で、ID=調べたtrx_mysql_thread_idのものを取る。


HOSTやUserから、どこから繋いだ誰なのか、わかりますよね、ちゃんと設計していれば。WebAPとDBを1台のサーバーで、全部rootユーザーで接続してたりしないよね。

実質的な権限が一緒でも、ある程度意味のある塊でMySQLユーザーを分けておくと、トラブルシュートしやすいです。


犯人をやっつける。

1. SUPER権限を持っているMySQLユーザーで接続し、以下のコマンドを実行。
killスレッドID

なお、AWS RDS/MySQLやAurora MySQL Compatibleの場合、SUPER権限は使えませんので、代替提供されている以下のコマンドを実行します。

CALL mysql.rds_kill(スレッドID);

接続元となるプログラム/アプリケーションがわかっていて、そちらからきれいに停止ができるのであればそうしたほうがよいです。

2. 解消したことを確認する

SHOW FULL PROCESSLISTに「Waiting for table metadata lock」が出てこなくなっていればOK。
しばらくしても解消しない場合、まだ犯人がいるので、「犯人の特定」からやりなおし。

合わせて読んでね。

atsuizo.hatenadiary.jp


atsuizo.hatenadiary.jp


atsuizo.hatenadiary.jp


最後に、やっぱりこの本を推しておく!

詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド (NEXT ONE)

詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド (NEXT ONE)

*1:環境によってはUTCで出力されるので注意

Viewing all 174 articles
Browse latest View live