まっしろけっけ

めもてきなやーつ

MySQL の binlog について調べたメモ

MySQL の binlog について

実際に実行された更新系クエリの情報が記述されていてなんらかの理由によりデータが壊れた際の
データ復旧とかにも役にたつ。

binlog の format には以下の 3 種類ある

フォーマットの種類 設定値(文字列) 設定値(数字) 備考
ステートメントベース STATEMENT 1 実際に実行された SQL を記録
行ベース ROW 2 実際に変更された行のデータの情報を記録
ミックス MIXED 0 基本的にはステートメントベースと同じで非決定性のクエリの際は行ベースと同じ形式のログを出力する

ここら辺は DB server を構築する際にレプリケーションとかを考えると思うので
基本的には理解している内容だと思われます。

今回は binlog の中身を除いて実際にどうなってるの?というところを調べた。

実際にログを出力させて比べる

環境
my.cnf

my.cnf を変更して binlog を出力するようにする

[mysqld]
log_bin
binlog_format = 1 # ステートメントベースのログを出力する
table

今回は下記の table を使用する

CREATE TABLE `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;
使用する SQL
INSERT INTO users SET name = UUID(), created_at = NOW(), updated_at = NOW(); # 非決定性のクエリ
INSERT INTO users SET name = "test", created_at = NOW(), updated_at = NOW();

ステートメントベース

$ mysqlbin mysql-bin.000001
# 一部抜粋
BEGIN
/*!*/;
# at 247
# at 279
#160612 14:56:46 server id 1  end_log_pos 279 CRC32 0xc624c39f 	Intvar
SET INSERT_ID=20/*!*/;
#160612 14:56:46 server id 1  end_log_pos 476 CRC32 0x6a52b700 	Query	thread_id=1	exec_time=0	error_code=0
use `db`/*!*/;
SET TIMESTAMP=1465711006/*!*/;
INSERT INTO users SET name = UUID(), created_at = NOW(), updated_at = NOW()
/*!*/;
# at 476
#160612 14:56:46 server id 1  end_log_pos 507 CRC32 0xf803a15e 	Xid = 21
COMMIT/*!*/;
# at 507
#160612 14:56:53 server id 1  end_log_pos 634 CRC32 0xd617f39d 	Query	thread_id=1	exec_time=0	error_code=0
SET TIMESTAMP=1465711013/*!*/;
BEGIN
/*!*/;
# at 634
# at 666
#160612 14:56:53 server id 1  end_log_pos 666 CRC32 0xe6f7f9d2 	Intvar
SET INSERT_ID=21/*!*/;
#160612 14:56:53 server id 1  end_log_pos 863 CRC32 0x765b9f4d 	Query	thread_id=1	exec_time=0	error_code=0
SET TIMESTAMP=1465711013/*!*/;
INSERT INTO users SET name = "test", created_at = NOW(), updated_at = NOW()
/*!*/;
# at 863
#160612 14:56:53 server id 1  end_log_pos 894 CRC32 0x828b94cf 	Xid = 22
COMMIT/*!*/;

確かに発行された SQL がそのまま出力されている

行ベース

$ mysqlbin mysql-bin.000002
# 一部抜粋
BEGIN
/*!*/;
# at 220
#160612 14:58:55 server id 1  end_log_pos 295 CRC32 0xa71dbfff 	Table_map: `db`.`users` mapped to number 83
# at 295
#160612 14:58:55 server id 1  end_log_pos 383 CRC32 0xb2425e19 	Write_rows: table id 83 flags: STMT_END_F

BINLOG '
H/pcVxMBAAAASwAAACcBAAAAAFMAAAAAAAEAGGJsb2dfc2FtcGxlc19kZXZlbG9wbWVudAAFdXNl
cnMABAMPEhIE/QIAAAL/vx2n
H/pcVx4BAAAAWAAAAH8BAAAAAFMAAAAAAAEAAgAE//AWAAAAJABiZjYwNTc0MC0zMDYyLTExZTYt
OGQ2Zi0yZjg2MTFjNWE2ZjGZmZjut5mZmO63GV5Csg==
'/*!*/;
# at 383
#160612 14:58:55 server id 1  end_log_pos 414 CRC32 0xf290e6c8 	Xid = 21
COMMIT/*!*/;
# at 414
#160612 14:58:57 server id 1  end_log_pos 514 CRC32 0xc1908135 	Query	thread_id=1	exec_time=0	error_code=0
SET TIMESTAMP=1465711137/*!*/;
BEGIN
/*!*/;
# at 514
#160612 14:58:57 server id 1  end_log_pos 589 CRC32 0x96559f32 	Table_map: `db`.`users` mapped to number 83
# at 589
#160612 14:58:57 server id 1  end_log_pos 645 CRC32 0x56be64d2 	Write_rows: table id 83 flags: STMT_END_F

BINLOG '
IfpcVxMBAAAASwAAAE0CAAAAAFMAAAAAAAEAGGJsb2dfc2FtcGxlc19kZXZlbG9wbWVudAAFdXNl
cnMABAMPEhIE/QIAAAIyn1WW
IfpcVx4BAAAAOAAAAIUCAAAAAFMAAAAAAAEAAgAE//AXAAAABAB0ZXN0mZmY7rmZmZjuudJkvlY=
'/*!*/;
# at 645
#160612 14:58:57 server id 1  end_log_pos 676 CRC32 0x24726578 	Xid = 22
COMMIT/*!*/;

な、なるほどわからん
もっと詳しく見れるようにオプションを追加する

$ mysqlbinlog -vvv --base64-output=DECODE-ROWS mysql-bin.000002
# 一部抜粋
BEGIN
/*!*/;
# at 220
#160612 14:58:55 server id 1  end_log_pos 295 CRC32 0xa71dbfff 	Table_map: `db`.`users` mapped to number 83
# at 295
#160612 14:58:55 server id 1  end_log_pos 383 CRC32 0xb2425e19 	Write_rows: table id 83 flags: STMT_END_F
### INSERT INTO `db`.`users`
### SET
###   @1=22 /* INT meta=0 nullable=0 is_null=0 */
###   @2='bf605740-3062-11e6-8d6f-2f8611c5a6f1' /* VARSTRING(765) meta=765 nullable=1 is_null=0 */
###   @3='2016-06-12 14:58:55' /* DATETIME(0) meta=0 nullable=0 is_null=0 */
###   @4='2016-06-12 14:58:55' /* DATETIME(0) meta=0 nullable=0 is_null=0 */
# at 383
#160612 14:58:55 server id 1  end_log_pos 414 CRC32 0xf290e6c8 	Xid = 21
COMMIT/*!*/;
# at 414
#160612 14:58:57 server id 1  end_log_pos 514 CRC32 0xc1908135 	Query	thread_id=1	exec_time=0	error_code=0
SET TIMESTAMP=1465711137/*!*/;
BEGIN
/*!*/;
# at 514
#160612 14:58:57 server id 1  end_log_pos 589 CRC32 0x96559f32 	Table_map: `db`.`users` mapped to number 83
# at 589
#160612 14:58:57 server id 1  end_log_pos 645 CRC32 0x56be64d2 	Write_rows: table id 83 flags: STMT_END_F
### INSERT INTO `db`.`users`
### SET
###   @1=23 /* INT meta=0 nullable=0 is_null=0 */
###   @2='test' /* VARSTRING(765) meta=765 nullable=1 is_null=0 */
###   @3='2016-06-12 14:58:57' /* DATETIME(0) meta=0 nullable=0 is_null=0 */
###   @4='2016-06-12 14:58:57' /* DATETIME(0) meta=0 nullable=0 is_null=0 */
# at 645
#160612 14:58:57 server id 1  end_log_pos 676 CRC32 0x24726578 	Xid = 22
COMMIT/*!*/;

column 名が書かれているわけではなく @1 とか書かれてあるのだなぁ。
定義した table の column の順番通り @1 = id, @2 = name, @3 = created_at, @4 = updated_at という順番っぽい
UUID() も展開後の文字列が入っているのがわかる。

ミックス

$ mysqlbin mysql-bin.000003
# 一部抜粋
BEGIN
/*!*/;
# at 220
#160612 15:05:14 server id 1  end_log_pos 295 CRC32 0x3b4cb14b 	Table_map: `db`.`users` mapped to number 83
# at 295
#160612 15:05:14 server id 1  end_log_pos 383 CRC32 0x9905bb6a 	Write_rows: table id 83 flags: STMT_END_F

BINLOG '
mvtcVxMBAAAASwAAACcBAAAAAFMAAAAAAAEAGGJsb2dfc2FtcGxlc19kZXZlbG9wbWVudAAFdXNl
cnMABAMPEhIE/QIAAAJLsUw7
mvtcVx4BAAAAWAAAAH8BAAAAAFMAAAAAAAEAAgAE//AYAAAAJABhMTY1OWFmNi0zMDYzLTExZTYt
OTc1Yi1hZjExNDczMDFkMDGZmZjxTpmZmPFOarsFmQ==
'/*!*/;
# at 383
#160612 15:05:14 server id 1  end_log_pos 414 CRC32 0x84766030 	Xid = 21
COMMIT/*!*/;
# at 414
#160612 15:05:16 server id 1  end_log_pos 541 CRC32 0xe4c9b7a8 	Query	thread_id=1	exec_time=0	error_code=0
SET TIMESTAMP=1465711516/*!*/;
BEGIN
/*!*/;
# at 541
# at 573
#160612 15:05:16 server id 1  end_log_pos 573 CRC32 0x10f97ca5 	Intvar
SET INSERT_ID=25/*!*/;
#160612 15:05:16 server id 1  end_log_pos 770 CRC32 0xd9adc7e3 	Query	thread_id=1	exec_time=0	error_code=0
use `db`/*!*/;
SET TIMESTAMP=1465711516/*!*/;
INSERT INTO users SET name = "test", created_at = NOW(), updated_at = NOW()
/*!*/;
# at 770
#160612 15:05:16 server id 1  end_log_pos 801 CRC32 0xa8cd90ff 	Xid = 22
COMMIT/*!*/;

本当にミックスされている
もっと詳しく見てみる

$ mysqlbinlog -vvv --base64-output=DECODE-ROWS mac-no-MacBook-Pro-bin.000003
# 一部抜粋
BEGIN
/*!*/;
# at 220
#160612 15:05:14 server id 1  end_log_pos 295 CRC32 0x3b4cb14b 	Table_map: `db`.`users` mapped to number 83
# at 295
#160612 15:05:14 server id 1  end_log_pos 383 CRC32 0x9905bb6a 	Write_rows: table id 83 flags: STMT_END_F
### INSERT INTO `db`.`users`
### SET
###   @1=24 /* INT meta=0 nullable=0 is_null=0 */
###   @2='a1659af6-3063-11e6-975b-af1147301d01' /* VARSTRING(765) meta=765 nullable=1 is_null=0 */
###   @3='2016-06-12 15:05:14' /* DATETIME(0) meta=0 nullable=0 is_null=0 */
###   @4='2016-06-12 15:05:14' /* DATETIME(0) meta=0 nullable=0 is_null=0 */
# at 383
#160612 15:05:14 server id 1  end_log_pos 414 CRC32 0x84766030 	Xid = 21
COMMIT/*!*/;
# at 414
#160612 15:05:16 server id 1  end_log_pos 541 CRC32 0xe4c9b7a8 	Query	thread_id=1	exec_time=0	error_code=0
SET TIMESTAMP=1465711516/*!*/;
BEGIN
/*!*/;
# at 541
# at 573
#160612 15:05:16 server id 1  end_log_pos 573 CRC32 0x10f97ca5 	Intvar
SET INSERT_ID=25/*!*/;
#160612 15:05:16 server id 1  end_log_pos 770 CRC32 0xd9adc7e3 	Query	thread_id=1	exec_time=0	error_code=0
use `db`/*!*/;
SET TIMESTAMP=1465711516/*!*/;
INSERT INTO users SET name = "test", created_at = NOW(), updated_at = NOW()
/*!*/;
# at 770
#160612 15:05:16 server id 1  end_log_pos 801 CRC32 0xa8cd90ff 	Xid = 22
COMMIT/*!*/;

非決定性のクエリの際は行ベースと同じ形式のログを出力することがわかった。

mysqlbinlog コマンドのオプション + SQL

よく使いそうなオプションをまとめる

$ mysqlbinlog -h host --read-from-remote-server mysql-bin.000001 # リモートサーバの binlog を見ることができる
$ mysqlbinlog --start-datetime="2016-01-01 00:00:00" --stop-datetime="2016-01-01 12:00:00" mysql-bin.000001 # 日時を指定して binlog を見ることができる

binlog 情報を出力してくれる SQL をまとめる

mysql> SHOW GLOBAL VARIABLES LIKE 'binlog_format'; # binlog format を表示
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| binlog_format | MIXED |
+---------------+-------+

mysql> SHOW BINARY LOGS; # server の binlog 一覧を表示
+------------------+-----------+
| Log_name         | File_size |
+------------------+-----------+
| mysql-bin.000001 |       531 |
| mysql-bin.000002 |       437 |
| mysql-bin.000003 |       933 |
+------------------+-----------+

mysql> SHOW BINLOG EVENTS; # binlog 内のイベントを表示
+------------------+-----+-------------+-----------+-------------+----------------------------------------------------------------------------------------+
| Log_name         | Pos | Event_type  | Server_id | End_log_pos | Info                                                                                   |
+------------------+-----+-------------+-----------+-------------+----------------------------------------------------------------------------------------+
| mysql-bin.000001 |   4 | Format_desc |         1 |         120 | Server ver: 5.6.27-log, Binlog ver: 4                                                  |
| mysql-bin.000001 | 120 | Query       |         1 |         247 | BEGIN                                                                                  |
| mysql-bin.000001 | 247 | Intvar      |         1 |         279 | INSERT_ID=16                                                                           |
| mysql-bin.000001 | 279 | Query       |         1 |         477 | use `db`; insert into users  set name = UUID(), created_at = NOW(), updated_at = NOW() |
| mysql-bin.000001 | 477 | Xid         |         1 |         508 | COMMIT /* xid=24 */                                                                    |
| mysql-bin.000001 | 508 | Stop        |         1 |         531 |                                                                                        |
+------------------+-----+-------------+-----------+-------------+----------------------------------------------------------------------------------------+

最後に

ということで今回は MySQL の binlog についての format による違いを確認してみました。
binlog にどのような情報が記述されているか最低限覚えておくと
ここぞという時に役に立つかと思います

Ruby と Google Cloud Platform の Cloud Vision API で画像を解析して貰う

Vision API is 何?

画像のさまざまな情報を解析してくれる API です。

物体検知、有害コンテンツ検知、ロゴ検知、ランドマーク検知、OCR、顔検知、色検知等を行ってくれる。

料金は下記に詳細が書いてありますが 1000 ユニット / 月 は無料で利用出来るので、
どんなものか遊んでみたいとか Google すげぇって思ったりするのは問題ないかと思います。
cloud.google.com

実際に使って見る

事前準備

事前準備としては Google Cloud Platform のアカウントを作成して、
API KEY を取得しておく必要があります。(クレジットカードの登録が必要)

google-api-client を使う

Google の様々な API の client がまとまっている gem の google-api-client を使って Vision API に画像を投げてみます。
物体検知と色検知を指定しています。

$ gem install google-api-client
require 'google/apis/vision_v1'
Vision = Google::Apis::VisionV1
vision = Vision::VisionService.new
vision.key = "API KEY"

request = Google::Apis::VisionV1::BatchAnnotateImagesRequest.new(
  requests: [
    {
      image:{
        content: File.read("1.png")
      },
      features: [
        {
          type: "LABEL_DETECTION",
          maxResults: 10
        },
        {
          type: "IMAGE_PROPERTIES",
          maxResults: 10
        }
      ]
    }
  ]
)

vision.annotate_image(request)

gcp-vision を使う

google-api-client は require した際に一瞬固まる(load に時間がかかる)のが気になったので Vision API 専用の軽量な client を自作してみた。

$ gem install gcp-vision
require 'gcp/vision'

Gcp::Vision.configure do |config|
  config.api_key = "API KEY"
end

request = {
  requests: [
    {
      image:{
        content: Base64.encode64(File.read("1.png"))
      },
      features: [
        {
          type: "LABEL_DETECTION",
          maxResults: 10
        },
        {
          type: "IMAGE_PROPERTIES",
          maxResults: 10
        }
      ]
    }
  ]
}

Gcp::Vision.annotate_image(request)

レスポンスとしてどんな情報が返ってくるか?

実際にどんな情報が Vision API から返って来るのか?
ということで自分が普段 SNS 等のアイコンに使用している下記の愛犬の画像を Vision API に投げてみました。
f:id:shiro-16:20160508183558j:plain:w100

{
  "responses": [
    {
      "labelAnnotations": [
        {
          "mid": "/m/068hy",
          "description": "pet",
          "score": 0.98951507
        },
        {
          "mid": "/m/0bt9lr",
          "description": "dog",
          "score": 0.98687589
        },
        {
          "mid": "/m/04rky",
          "description": "mammal",
          "score": 0.9618566
        },
        {
          "mid": "/m/0jbk",
          "description": "animal",
          "score": 0.95587397
        },
        {
          "mid": "/m/02cyl6",
          "description": "maltese",
          "score": 0.89896905
        },
        {
          "mid": "/m/09686",
          "description": "vertebrate",
          "score": 0.89741188
        },
        {
          "mid": "/m/01lrl",
          "description": "carnivoran",
          "score": 0.85160905
        },
        {
          "mid": "/m/0kpmf",
          "description": "dog breed",
          "score": 0.77777779
        },
        {
          "mid": "/m/036hyn",
          "description": "toy dog",
          "score": 0.7
        },
        {
          "mid": "/m/0b26w3",
          "description": "schnoodle",
          "score": 0.68533921
        }
      ],
      "imagePropertiesAnnotation": {
        "dominantColors": {
          "colors": [
            {
              "color": {
                "red": 197,
                "green": 199,
                "blue": 195
              },
              "score": 0.53806108,
              "pixelFraction": 0.33916494
            },
            {
              "color": {
                "red": 86,
                "green": 84,
                "blue": 82
              },
              "score": 0.030347142,
              "pixelFraction": 0.046761185
            },
            {
              "color": {
                "red": 220,
                "green": 230,
                "blue": 229
              },
              "score": 0.31318155,
              "pixelFraction": 0.19654006
            },
            {
              "color": {
                "red": 165,
                "green": 162,
                "blue": 158
              },
              "score": 0.047778718,
              "pixelFraction": 0.19640999
            },
            {
              "color": {
                "red": 53,
                "green": 55,
                "blue": 55
              },
              "score": 0.021588614,
              "pixelFraction": 0.016324142
            },
            {
              "color": {
                "red": 123,
                "green": 116,
                "blue": 115
              },
              "score": 0.017005477,
              "pixelFraction": 0.10022113
            },
            {
              "color": {
                "red": 63,
                "green": 92,
                "blue": 77
              },
              "score": 0.009515699,
              "pixelFraction": 0.012291883
            },
            {
              "color": {
                "red": 25,
                "green": 28,
                "blue": 29
              },
              "score": 0.0089049507,
              "pixelFraction": 0.0029916754
            },
            {
              "color": {
                "red": 35,
                "green": 56,
                "blue": 49
              },
              "score": 0.0033917534,
              "pixelFraction": 0.0018210198
            },
            {
              "color": {
                "red": 89,
                "green": 121,
                "blue": 105
              },
              "score": 0.0033548647,
              "pixelFraction": 0.017364724
            }
          ]
        }
      }
    }
  ]
}

物体検知の結果は pet, dog, mammal, animal, maltese... 等が返ってきた。
dog だけではなく maltese といった感じで正確な犬種まで返って来るのには驚くばかりだ。

次に色検知だが RGB だとわかりづらいので下記のようにしてみた。
⬛︎⬛︎⬛︎⬛︎⬛︎
全体的に白いのでまぁこんな感じかぁ

最後に

その他の検知方法を使用したい場合などは下記のマニュアルに載っているので興味があれば調べてみるのが良いかと。
cloud.google.com

Elasticsearch の勉強会を社内で行なった話

最近圧倒的インプットによって、圧倒的にアウトプットが減っている僕です。
ということでリハビリがてら 4 月の初めに Elasticsearch の勉強会を社内でやったのでそのことについて書いてみます。
そこまで技術的に深い話はないので期待しないでください。

勉強会の開催経緯

今のプロジェクトで検索に関してあれこれ頑張っていく中で Elasticsearch を使用することになり、
index の設計とか Rails から Elasticsearch 使うとことか作ったわけですが
チームメンバーから知見を共有してくれ!という要望があったのと自分自信も自分だけわかっても後々辛い感じになるだろうという感じだった + 弊社はいるだけで成長出来るらしいのでみんなを成長させようではないかという思いから、
やるぞ!ってなったのが 2 月末です。

で開催したのが 4 月初め...だいぶ遅い...(反省はしている)

実際の内容

Elasticsearch とは?みたいなところをざっくり説明した後に、
tokenizer, filter, analyzer の説明を行なった後に実際に設定している settings 周りの解説を行なった。

その後、実際に設定している mappings 周りの説明をしたり、ドキュメントが登録される際の流れや、クエリを投げた際の流れを説明

最後に、ハンズオンとして index を定義した後に基本的な CRUD + 検索クエリを投げる、現在プロジェクトで実際に投げられているクエリの解説という流れで行なった。

若干資料作成をミスって手順がグダッてしまった感はあったが、所々で @monochromegane さんがナイスな質問を投げてくれたので参加者のみんなの理解が深まったのでは?と勝手に思っている。

同時間内で @yano3クラスタについてより詳しく話してもらう予定だったのだが、
時間が押してしまい別日で説明をしてもらった。その内容が下記の記事になります。

yano3.hatenablog.jp

最後に

Elasticsearch をリアルタイムログ解析以外で触ったことがなかったので色々大変だったのですがそれはまた別のお話。
やるぞ!って言ってから 1 ヶ月経過したのは本当に反省している。
Elasticsearch を ES と略すのに若干の違和感を覚えている僕なのでした

圧倒的インプットによりネタはいっぱいあるので、またちょいちょいブログ書いていこうと思っています。。。

WEB+DB PRESS Vol.92 「Web開発新人研修」の一部を書きました。

久しぶりのブログです。
4 月なのに 2016 年初ブログです。

本題

4 月 23 日発売の WEB+DB PRESS Vol.92 内の特集の一つである「Web開発新人研修」をペパボのエンジニアで寄稿させていただきました。
献本を頂いたのでざっくりと説明をしていこうと思います。
Web 開発とはどんなものなのかを浅く広く把握できる内容になっています。

自分は 3 章の「Web アプリケーションを作ろう」を担当しました。
Web アプリケーションの概要、フレームワークについて、Rails を使用して MVC を解説しながら、
ToDo アプリケーションを開発するという内容になっています。

コード自体は scaffold で作成されるファイルの中でも必要最低限なコードの作成と解説をしています。
scaffold は凄いのですが、凄過ぎてよくわからないけど動くという状況になることもあるかと思うので
このような形式で解説をさせて頂きました。


説明を行おうと思えば記事では触れていない部分もいくらでも説明出来るのですが、
今回は新人研修というのが目的の記事になるので、詳細な説明を省いている為説明不足な点も多いかと思います。
そのような点はイケメン先輩エンジニアが新人さんに教えてあげたり、逆に新人さんは先輩エンジニアにがんがん聞いていくのが良いのではないでしょうか

記事のまとめにも書いたのですが、Web アプリケーションを作るのは本当に楽しいので
今回の記事を読んでそのようなエンジニアが増えてくれたら嬉しく思います。

最後に、執筆の機会を下さった @june29 さん、執筆でお世話になりました株式会社技術評論社の池田さんありがとうございました。

gihyo.jp

STF を使ってブラウザ上から Android をいじってみた

STF is 何 ?

STF | Smartphone Test Farm

CyberAgent 社がオープンソースとして公開しているブラウザ上から Android を操作できるようになるやつ。
ブラウザ上から apk を install させたりもたしかできたはず
Android は様々な端末があり、
端末ごとに動作が違ったりで検証が大変 + 多数のプロダクトで Android アプリを開発していると端末の貸し借りが発生したりするので
端末の管理が大変という問題とか開発拠点が離れている場合などもあるのでそこら辺を解消できる。

なぜか使った記憶がありとても便利だった。(元社員だっただけです)

なぜやってみたか

@misyobun さんがやってたのを見て
そういえば弊社CTOのあんちぽさんに「導入しといて」って雑に振られたので
面白そうだし「わかりました」って言ったのを思い出したから
(2週間前くらいだっただろうか…)

さっそく導入

homebrew があると便利なので入れとくといいかと

  • rethinkdb
  • graphicsmagick
  • zeromq
  • protobuf
  • yasm
  • pkg-config

上記をまとめて install

brew install rethinkdb graphicsmagick zeromq protobuf yasm pkg-config

node.js を install

$ brew install node
$ node -v
v5.0.0

stf を install

$ npm install -g stf
...
# node-gyp の install で error が発生した…

調べると node-gyp は最新の node に対応してないとかなんとか
なので node v0.12.7 を入れた

node の uninstall は下記で行う

$ brew uninstall node

もう一度 stf を install

$ npm install -g stf
...
# pkg-config が見つけられないてきな error が発生した…

# PKG_CONFIG_PATH を指定してやる (path は各々の環境に合わせてください)
$ export PKG_CONFIG_PATH=/usr/local/Cellar/zeromq/4.1.3/lib/pkgconfig/
$ npm install -g stf

install 関連は終わり

stf を立ち上げる

まずは rethinkdb から

$ rethinkdb
Recursively removing directory /hoge/rethinkdb_data/tmp
Initializing directory /hoge/rethinkdb_data
Running rethinkdb 2.1.5-2 (CLANG 7.0.0 (clang-700.0.72))...
Running on Darwin 14.5.0 x86_64
Loading data from directory /Users/mac/rethinkdb_data
...
...

stf を立ち上げる
端末はUSBでつないでおく

$ stf local
# ログがいっぱいでる

http://localhost:7100/ にアクセスすると下記のような画面が表示される
f:id:shiro-16:20151103180449p:plain

画像で「停止する」の部分が「準備中」になっていていくら待っても準備が完了しない…

log を見てみるとなにか失敗している

INF/device:resources:service 44364 [hoge] Installing STFService
FTL/device 44364 [hoge] Setup had an error TimeoutError: operation timed out
    at afterTimeout (/usr/local/lib/node_modules/stf/node_modules/adbkit/node_modules/bluebird/js/main/timers.js:11:15)
    at timeoutTimeout (/usr/local/lib/node_modules/stf/node_modules/adbkit/node_modules/bluebird/js/main/timers.js:53:9)
    at Timer.listOnTimeout (timers.js:119:15)

STFService.apk の install が失敗してしまっている様子?
とりあえず手動で STFService.apk を install する
(adb コマンド is 何 って人はググってくれ!)

apk のソースは ここ にあるのでこれを build してもいいと思う

$ adb install /usr/local/lib/node_modules/stf/vendor/STFService/STFService.apk

これで stf を再起動してみたが同じエラーが出てたので USB を一度抜き挿ししたら端末を操作できるようになった。
結果は下記をみてください


最後に

結構簡単にできた。
RethinkDB触ったことないのでちょっといじってみるのは良さそう
実際にこれを会社で導入する場合 server 用意してそれに USB ハブを挿しまくるのだろう。
そこら辺はなんかいい感じに相談してみよう。

やはり便利だ。

Sidekiq の queue を眺める際に使用するコマンドを雑にまとめる

はじめに

前回書いたこの記事を書く際に実際に積まれている queue をあれこれ見ていたのですが、
その際に使ったコマンドを忘れそうだったのでまとめておく。
めんどくさくてまとめなかったわけでは…

shiro-16.hatenablog.com

redis-cli

まずは基本的なこと redis-cli を使って redis のデータを見ることが出来る

Redis server に接続して対話的に実行
$ redis-cli
redis 127.0.0.1:6379> KEYS *
引数を指定して実行
$ redis-cli KEYS '*'

perform_async

※ perform_async で user 3 つ default 2 つ queue を積んだ状態

どんな key が生成されるか
$ $ redis-cli KEYS '*'
1) "queue:default"
2) "queues"
3) "queue:user"
key の member を全て取得
# SMEMBERS key
$ redis-cli SMEMBERS 'queues'
1) "user"
2) "default"
queue の件数を取得
$ redis-cli LLEN "queue:user"
(integer) 3
queue を 1 件取得
# LINDEX index
$ redis-cli LINDEX "queue:user" 0
"{\"class\":\"UserWorker\",\"args\":[1],\"retry\":true,\"queue\":\"user\",\"jid\":\"adddb1f368e7a622225ef315\",\"created_at\":1445260869.8274186,\"enqueued_at\":1445260869.8274736}"
queue を複数件取得
# LRANGE start end
$ redis-cli LRANGE "queue:user" 1 2
1) "{\"class\":\"UserWorker\",\"args\":[1],\"retry\":true,\"queue\":\"user\",\"jid\":\"b009bbcaa2b0dca70201b448\",\"created_at\":1445260841.7604742,\"enqueued_at\":1445260841.760538}"
2) "{\"class\":\"UserWorker\",\"args\":[1],\"retry\":true,\"queue\":\"user\",\"jid\":\"c9d40578849234b9f8b8d429\",\"created_at\":1445260828.8738196,\"enqueued_at\":1445260828.8746564}"

perform_in

※ perform_in で 3 つ queue を積んだ状態

どんな key が生成されるか
$ redis-cli KEYS '*'
1) "schedule"
schedule の member を見る

start と end を指定する

# ZRANGE key start end
$ redis-cli ZRANGE schedule 0 0
1) "{\"class\":\"UserWorker\",\"args\":[1],\"retry\":true,\"queue\":\"default\",\"jid\":\"2b6f8042a496b71128881c12\",\"created_at\":1445259670.319731}"

全て取得する

$ redis-cli ZRANGEBYSCORE schedule -inf +inf
1) "{\"class\":\"UserWorker\",\"args\":[1],\"retry\":true,\"queue\":\"default\",\"jid\":\"2b6f8042a496b71128881c12\",\"created_at\":1445259670.319731}"
2) "{\"class\":\"UserWorker\",\"args\":[1],\"retry\":true,\"queue\":\"default\",\"jid\":\"edc66e38f51e4b405ef2b984\",\"created_at\":1445260137.1156876}"
3) "{\"class\":\"UserWorker\",\"args\":[1],\"retry\":true,\"queue\":\"default\",\"jid\":\"8eb4b0bf44dee20a4c797424\",\"created_at\":1445260138.2186956}"

削除する

# ZREM key member
$ redis-cli ZREM schedule "{\"class\":\"UserWorker\",\"args\":[1],\"retry\":true,\"queue\":\"default\",\"jid\":\"2b6f8042a496b71128881c12\",\"created_at\":1445259670.319731}"

全部消したい!

普通はやらないけど全部のデータを消したい時に使う

$ redis-cli FLUSHALL
OK

最後に

ここら辺が使えれば Sidekiq の Redis 周りのことを調べるには困らないはず!

Sidekiq について基本と1年半運用してのあれこれ

はじめに

実際に運用していた時に非同期にしていた主な処理は下記のようなものがあります。

  • iOS Android の push 通知の送信処理
  • ログの作成
  • 様々な外部 API の呼び出し
  • 非同期で更新しても問題ないデータの更新

Sidekiq is なに

sidekiqは非同期処理を実現する gem
他にも Ruby で非同期処理を実現できる有名な gem には
resque や delayed_job 等がある。

sidekiq.org

Enterprise版等もありますが、
今回はOSS版を使用している前提でのお話しです。

他の非同期処理が可能な gem との簡単な比較

FAQ · mperham/sidekiq Wiki · GitHub
この内容は結構真実を語っていることを最近知った

Sidekiq
  • Redis
  • マルチスレッド
  • リトライ処理あり
  • おしゃれなダッシュボード
Resque
  • Redis
  • ジョブごとにフォーク
  • リトライ処理なし
  • Sidekiqに比べると簡素なダッシュボード
Delayed Job
  • DB(専用テーブル作成)
  • リトライ処理あり
  • delay method が便利
  • 基本的にDBを使うので導入簡単

実装例

Redis は既にセットアップ済みとする

sidekiq を追加

$ vi Gemfile
+ gem 'sidekiq'

$ bundle install

config を追加

$ vi config/sidekiq.yml
:verbose: true
:pidfile: ./tmp/pids/sidekiq.pid
:logfile: ./log/sidekiq.log
:concurrency: 10 # worker process 数
:queues: # 処理するキュー名
  - default
  - user

env による設定の変更方法

production:
  :concurrency: 25
staging:
  :concurrency: 15

下記のように記述することで priority が設定できる

:queues:
  - [user, 2]
  - [default, 1]

worker file を作成

# UserWorker を作成
$ bundle exec rails g sidekiq:worker User
      create  app/workers/user_worker.rb
      create  test/workers/user_worker_test.rb

user_worker を編集
※今回は例としてわかりやすい処理にしています。

$ vim app/workers/user_worker.rb
- def perform(*args)
- # Do something
+ sidekiq_options queue: :user # キュー名指定がない場合は default になる
+ def perform(id, name)
+   user = User.find(id)
+   user.update(name: name)

キューを積む

$ bundle exec rails c
irb> UserWorker.perform_async(1, "test")
=> "f212bd8bae56c79467494ec8"
irb> User.find(1) # この状態ではまだ record が更新されていない
  User Load (0.3ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
=> #<User id: 1, name: "1", created_at: "2015-10-11 13:13:39", updated_at: "2015-10-11 13:13:50">

Sidekiq を起動する

$ bundle exec sidekiq -C config/sidekiq.yml -d
# ちょっと時間をおいて
$ bundle exec rails c
irb> User.find(1) # name が更新されていることが確認できる
  User Load (0.3ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
=> #<User id: 1, name: "test", created_at: "2015-10-11 13:13:39", updated_at: "2015-10-11 13:17:14">

これで簡単な実装例はおしまいです。

Sidekiq の enqueue の解説

Sidekiq が Redis にどのように queue を追加するかを解説する。
Sidekiq は使う method によって使う Redis のデータ型が違う

perform_asyncの場合

「セット型」と「リスト型」を使う。

1.セット型で queues という key で各ワーカーのキュー名を member として登録する
(実装例の場合は member に user を指定する)

2.リストの先頭に queue:キュー名 をキーにして string を追加する。
(実装例の場合は queue:user というキー名になる)

3.string は { class: worker class 名, arg: perform_asyncの引数配列, retry: retry機能が有効かのフラグ, queue: キュー名, jid: SecureRandom.hex(12) で生成された job id, created_at: 作成日, enqueued_at: キューに追加した時間 } この Hash を json にしたもの

以下が実装例での string の例

# Hash
{ class: "UserWorker", args: [1, "test"], retry: true, queue: "user", jid: 6284ea2d3637756fc121b8f7, created_at: 1444571406.7752862, enqueued_at: 1444571406.7754705 }

# Json化
"{\"class\":\"UserWorker\",\"args\":[1,\"test\"],\"retry\":true,\"queue\":\"user\",\"jid\":\"6284ea2d3637756fc121b8f7\",\"created_at\":1444571406.7752862,\"enqueued_at\":1444571406.7754705}"

perform_in の場合

「ソート済みセット型」を使う

1. ソート済みセット型で schedule という key で score に 実行時間を指定して member を登録する

2. member は { class: worker class 名, arg: perform_asyncの引数配列, retry: retry機能が有効かのフラグ, queue: キュー名, jid: SecureRandom.hex(12) で生成された job id, created_at: 作成日 } この Hash を json にしたもの

以下が実装例での member の例

# Hash
{ class: "UserWorker", args: [1, "test"], retry: true, queue: "user", jid: 6284ea2d3637756fc121b8f7, created_at: 1444571406.7752862 }

# Json化
"{\"class\":\"UserWorker\",\"args\":[1,\"test\"],\"retry\":true,\"queue\":\"user\",\"jid\":\"6284ea2d3637756fc121b8f7\",\"created_at\":1444571406.7752862}"

Redis について

Sidekiq を使う上で欠かせない Redis ですが、今回の記事で Redis のことまで深く説明すると
だいぶ長い記事になるので別記事として後日公開します。

工夫した点

ここからが本題の知見と言ってもいいかなという部分になります。

1. Redis の構成

当時開発していた Rails のアプリケーションでは、
session, sidekiq, cache に Redis を使用していました。
それぞれ別々の Redis にデータを保存していました。

別々にした理由は下記

  • Redis はオンメモリ型のKVSなので容量的な制約が大きい(実メモリの約半分程度)
  • 複数の用途で使い容量が設定によっては消えて欲しくないものが消える危険
  • 1つの Redis で運用をし後々分割しようと思った際にデータを分けるのが面倒
構成

レプリケーションを組み,Redis Sentinelで自動フェイルオーバーするように設定を行う。
アプリケーション側は下記の記事で書きましたが, gem の redis-sentinel を導入

shiro-16.hatenablog.com

redis-sentinel を導入すると Rails は Redis Sentinel から現在の master 情報を取得し接続、
障害により接続に失敗した場合に 「Redis Sentinel から master 情報を取得し接続」を繰り返し
master が切り替わったタイミングで正常に接続ができるようになるという感じ

2. 引数は出来るだけ少なく

perform_async 等の引数は出来るだけ少なくしましょうという話

例として Twitter でファボられた時に push を送る処理を実装
(実際には1ユーザに複数のtokenが紐づく可能性がある)

# ダメな例
# token: device_token or registration_id
# message: push 内容
def perform(token, message)
   FavoritePush.send(token, message)
end

# 良い例
# id: favorite情報を保存している record の id
def perform(id)
  favorite = Favorite.find(id)
  message = FavoritePush.message(favorite)
  FavoritePush.send(favorite.tweet.user.token, message)
end
何が良いのか?

・Redis の容量の圧迫を軽減出来る
・キューを積んでから実行されるまでに record が更新された場合にも対処出来る(今回の場合 token が更新されて push が送信出来ない問題を回避出来る)
・console 等からキューを積む場合に id を渡すだけなのでシンプル
SQLの発行回数が増えるのですが primary key や index を使用した検索になることが多いと思われるので問題ないかと思われる。
DBが重くなるのであれば Slave を増やせばいいので Redis の容量問題に比べれば微々たるものだし、primary key や index を使用しても重いならそれは設計が(ry
・自分が運用していた時は引数は1〜3つでした。(例外が一部ありましたが)

※ ただし引数が多くなる場合も、勿論あります。

3. 判定処理等を worker 側に持たせる

今回は2の例に push を送信するかどうかを判定する処理を追加する

# ダメな例
def perform(token, message)
  FavoritePush.send(token, message)
end
# キューを積む際に
# PushWorker.perform_async(favorite.tweet.user.token, FavoritePush.message(favorite)) if favorite.tweet.user.send_push?


# 良い例
def perform(id)
  favorite = Favorite.find(id)
  return unless favorite.tweet.user.send_push?
  message = FavoritePush.message(favorite)
  FavoritePush.send(favorite.tweet.user.token, message)
end
何が良いのか?

今回の場合はシンプルな判定だが、もっと複雑な判定を行う場面は多い
そうなった場合に特に有効

・キューを積む際に判定するより同期処理側の処理速度は速くなる
・今回の場合は message を生成する処理も worker 側で行うので同期処理側の処理速度は速くなる
・console 等からキューを積む場合に判定内容を気にしなくて良くなる

※ Redis に積むキューの数自体は増えてしまうので Redis の使用量は増えてしまいます

4. retry 機能に注意する

Sidekiq には retry 機能があるのでそのことを念頭に置いて実装を行う必要がある。
push 送信の処理に送信済みかフラグを持たせる

def perform(id)
  favorite = Favorite.find(id)
  return unless favorite.tweet.user.send_push?
  message = FavoritePush.message(favorite)
  FavoritePush.send(favorite.tweet.user.token, message)
  favorite.update(send_push: true)
end

retry 機能があるのでもし favorite.update でエラーが起こった場合
retry されて再度実行されます。
なので favorite.update が正常に終了するまで同じ内容の push を送信し続けることになってしまいます。

例外をうまく扱うか update を別 worker として実装し push 送信後にキューを積む等の工夫が必要

※ retry を個別に off にすることも可能

5. record が削除されている場合も考慮する

Sidekiq だけの話ではないですが、
非同期処理なので処理が実行される際には既に対象の record が削除されている可能性も考えなければなりません。
4.の retry の話を念頭に置いて ActiveRecord::RecordNotFound 等の例外を上手く処理する必要があります。

6. batch 処理

「3.判定処理等を worker 側に持たせる」と同じような内容になるのですが、
batch 等であるテーブルの各 record に対して複雑な処理をする場合、
処理は全て worker 側で行ってしまおうということです。

例)全ユーザを対象に何かしらの計算を処理を行う batch があるとする

# ダメな例
User.find_each do |user|
  # 複雑な処理

  UserWorker.perform_async(user.id, result)
end
class UserWorker
  def perform(id, result)
    user = User.find(id)
    user.update(result: result)
  end
end

# いい例
User.select(:id).find_each do |user|
  UserWorker.perform_async(user.id)
end

class UserWorker
  def perform(id)
    user = User.find(id)
    # 複雑な処理

    user.update(result: result)
  end
end
良い点

・時間がかかる複雑な処理をマルチスレッドで行うことで処理速度が上がる

deploy

deployに関しては下記のページを参考にするのが良さそう
Deployment · mperham/sidekiq Wiki · GitHub

その他

Best Practices · mperham/sidekiq Wiki · GitHub
ここら辺も合わせて読むと良いかと

最後に

なぜこのような記事にまとめようと思ったのかというと
前々からまとめておこうとは思ったのですが、
最近 Sidekiq を使用していないので忘れてしまいそうだったことと、
最近 Sidekiq 感覚で Delayed Job を使用したら少々痛い目を見たという
辛い経験があったので非同期処理についてまとめておこうといった感じです。

また、ここに書いてあることが全て正しいわけではなく
自分の開発していたアプリケーションにはこのパターンが良かったというだけで、
これに沿ったからといって問題が全て解決するというわけではないと思います。

Sidekiq と Redis の特徴を理解して上手く付き合っていくのが良いかと思います。

自分は Sidekiq 好きなので贔屓目の記事になっているかもしれません。