Quantcast
Channel: 無印吉澤
Viewing all 107 articles
Browse latest View live

ansible コマンドでのログ検索結果を TSV や Markdown 形式に変換する方法

$
0
0
f:id:muziyoshiz:20171211235628p:plain

これは Ansible Advent Calendar 2017の13日目の記事です。

ansible コマンドでログ検索

ansible コマンドって使ってますか?

Ansible と言えば普段は ansible-playbook コマンドを使うと思うので一応解説しておくと、ansible コマンドは Ansible モジュールを1個だけ実行するコマンドです。-m でモジュール、-a でそのモジュールに渡す引数を指定します。Running Ad Hoc Commands - Ansible Tips and Tricksあたりが詳しいです。

この ansible コマンドで shell モジュールを使うと、複数サーバ上のログを簡単に検索できます。

例えば以下のように、すべてのアプリケーションサーバに対して、一括で ERROR ログ数を検索できます。

$ ansible -i hosts app -m shell -a "cat /var/log/foobar | grep "ERROR" | wc -l"
app1 | SUCCESS | rc=0 >>
177

app2 | SUCCESS | rc=0 >>
84

app3 | SUCCESS | rc=0 >>
37

僕の場合は wc の代わりに head を使って ERROR ログを何件か表示し、サーバごとの傾向の違いを調べることもあります。

$ ansible -i hosts app -m shell -a "cat /var/log/foobar | grep "ERROR" | head -5"

もちろん、普段の運用に必要なログは、特定のサーバやデータベースに集めておくべきです。わかってますよ、わかってるんですが、集めてないログを調べたくなるときはどうしてもあって。そういうときに ansible コマンドで手軽に検索できるのは便利です。

ansible コマンドの結果を TSV/JSON/Markdown/Backlog 形式に変換

ログを調べたら、(それが仕事なら)たいてい他の人に共有したくなるわけですが、この出力は若干使い勝手がよくありません。

Excel や Google Spreadsheet で処理するにも手作業でのコピペが必要になりますし、Wiki にそのまま貼るにも不便です。以前からこの出力がちょっと気になっていました。

そこで Go の勉強がてら、ansible コマンドの結果を他の形式に変換する ansible2tab コマンドを作ってみました。

github.com

先ほど例に挙げた出力をこの ansible2tab に通すと、以下のように変換されます。

$ ansible -i hosts app -m shell -a "cat /var/log/foobar | wc -l" | ansible2tab
app1    177
app2    84
app3    37

デフォルトが TSV なので ansible2tab という名前にしましたが、--formatを指定すると JSON などにも変換できます。ちなみに、僕が仕事で使うので Backlog 記法にも対応しました。

$ ansible -i hosts app -m shell -a "cat /var/log/foobar | wc -l" | ansible2tab --format json
{"app1":"177","app2":"84","app3":"37"}
$ ansible -i hosts app -m shell -a "cat /var/log/foobar | wc -l" | ansible2tab --format markdown
|Host|Value|
|---|---|
|app1|177|
|app2|84|
|app3|37|
$ ansible -i hosts app -m shell -a "cat /var/log/foobar | wc -l" | ansible2tab --format backlog
|Host|Value|h
|app1|177|
|app2|84|
|app3|37|

出力が複数行の場合はうまく表になりません。その場合は --format markdown-code--format backlog-codeと指定すると、以下のようなコードブロックに変換します。

gist.github.com

できることはこれだけの、地味なツールです。でも、個人的には日常業務で結構こういうことしてるんですよね……。

この ansible2tab コマンドですが、Mac なら brew 経由でインストールできます。

$ brew tap muziyoshiz/ansible2tab
$ brew install ansible2tab

それ以外の環境では リリースページからダウンロードしてパスを通すか、あるいは Go の開発環境があれば以下のコマンドでインストールできます。

$ go get github.com/muziyoshiz/ansible2tab

まとめ

ansible コマンドは、複数サーバ上のログを手軽に検索するには便利です。今回は、このログ検索した結果を TSV/JSON/Markdown/Backlog 出力する ansible2tab を作りました。Go で実装したおかげで、さくっとクロスプラットフォーム対応できました。

世間に似たようなニーズがどれくらいあるかわからないですが、よかったら使ってみてください。別の形式にも対応して欲しい、JSON はもっと違う形式がいい、などの要望は GitHub の Issue@muziyoshizまでお願いします。

あと、Go 初心者なのでコードがあまりイケてないと思います。そのあたりのツッコミも歓迎です。

今回の参考情報

Go 言語全般

みんなのGo言語【現場で使える実践テクニック】

みんなのGo言語【現場で使える実践テクニック】

Go のコードに詰まってから読み返すと、ヒントになることが書いてある(と気づく)本でした。良い本です。

以下、今回参考にさせて頂いた Web 上の情報です。

バイナリの配布方法


Rails 5 で艦これアーケードのプレイデータ管理ツール "Admiral Stats"を開発中

$
0
0
f:id:muziyoshiz:20160828002458p:plain:w600

開発のきっかけ

このブログでゲームのことは書いたことなかったと思いますが、個人的には、長い期間かけてチマチマやるゲームが好きで、ここ数年は Ingress と艦これをやってます。

今年の4月には、艦これのアーケード版(艦これアーケード)もリリースされて、これも週1くらいのペースでゲーセンに通ってプレイしてたりします。

この艦これアーケードは筐体がネットワーク接続されており、自分のプレイデータをあとから SEGA の公式サイトで閲覧できるようになってます。このサイトで結構細かいデータまで見られるのですが、見られるデータはアクセス時の最新状態のみです。

このプレイデータを過去の分まで記録して時系列データとして可視化したら面白そう

と思いついたのと、

どうせ作るなら6月末にリリースされた Ruby on Rails 5 でも使ってみようか

ということで、この夏休みを使ってプレイデータ管理ツールを作ってみました。今回の記事は、このツール "Admiral Stats"の開発中間報告です。

2016-09-03追記

この記事の公開後、9/3にサービスリリースしました。Twitter アカウントでログインして使えます。ぜひお試しください。

www.admiral-stats.com

艦これアーケードとは?

艦これを全く知らない人向けに説明すると、艦これアーケードとは、艦娘と呼ばれるキャラのカードを集めて、選りすぐりのデッキを作成し、ステージを攻略していくアクションゲームです。ステージをクリアするたびに、ランダムで新たなカードが排出されます。

どのステージでどのカードが出やすいか、などのカード排出に関する法則性は全く公開されていないため、「自分(たち)が試したらこうだった」という情報が Wiki などで頻繁にやりとりされています。この法則性をつかむために(あるいは自分の不運をなぐさめるために)プレイデータを記録している人も多いと思います。

ちなみに、サービス開始直後は何時間も待たないとプレイできないほど人気でしたが、最近は少し待てばプレイできる程度に空いてきています。秋にゲーム内イベントがあるらしいので、それまでは空いてるんじゃないでしょうか。

Admiral Stats とは?

今回開発した Admiral Stats は、この艦これアーケードのプレイデータを可視化するサイトです。SEGA 公式のプレイヤーズサイトが対応していない、時系列での可視化に対応しています。

Ingress を知っている人なら Agent Statsの艦これアーケード版」という説明が一番分かりやすいと思います。実際、Agent Stats からの連想で Admiral Stats を作ることを思いつきましたし、名前も Agent Stats からの連想で付けました*1

f:id:muziyoshiz:20160828004335p:plain
Agent Stats の画面例

Admiral Stats の画面サンプル

サンプル 1:カードの入手履歴

Admiral Stats にまずログインすると、最近のプレイで入手したカードの一覧が表示されます。ずっとプレイしていてカードが増えてくると、「あれ、このカードって前にゲットしたっけ? 今日が初めてだっけ?」とわからなくなってくるのですが(自分はそうでした)、そういう場合を想定した機能です。

f:id:muziyoshiz:20160828003035p:plain

サンプル 2:カードの入手数・入手率のグラフ

カードの種類(ノーマル、レアなど)ごとの入手数、入手率のグラフです。Admiral Stats の内部に各カードのリリース時期のデータを登録してあるため、入手率は減少することもあります。

f:id:muziyoshiz:20160828003052p:plain

サンプル 3:レベル・経験値のグラフ

艦娘のレベル・経験値だけでなく、艦種(駆逐艦とか)や艦隊全体の累計レベル・経験値も表示できます。

f:id:muziyoshiz:20160828003101p:plain

サンプル 4:カード入手状況の一覧表示

公式サイトでも見られる情報なのですが、Admiral Stats では情報量を絞る代わりに、1ページにまとめて表示します。

f:id:muziyoshiz:20160828003114p:plain

データのアップロード方法

艦これアーケードの公式サイトは、残念ながら、プレイデータのダウンロード機能を提供していません。ただ、このサイトはとても綺麗に作られていて、プレイデータはすべて API 用の URL から JSON で取得し、Web ブラウザ側で画面を描画しています。

そのため、今回はこの JSON をそのままファイルに出力する admiral_stats_exporterというエクスポートツールを作りました。このツールが出力した JSON ファイルを Admiral Stats にアップロードすると、上記のサンプルのような画面が表示されます。

f:id:muziyoshiz:20160828002458p:plain:w600

Admiral Stats へのログイン方法

メールアドレスの管理をしたくなかったので、Twitter アカウントでログインする方法を採用しました。 Admiral Stats から SEGA のサイトに直接アクセスすることはないので、SEGA ID などの登録は必要ありません。

Admiral Stats の公開予定

実装は一通り終わりました。ローカルの仮想マシンで動かせば、自分1人で使う分には実用的に使えています。

ただ、どうせなら元ネタの Agent Stats の 「全ユーザとの比較」ページのように統計情報を表示できると、もっと面白くなるんじゃないかと思ってます。Agent Stats ではレベルや経験値の分布、プレイ傾向がわかる指標(攻撃重視か構築重視か、など)の分布が公開されています。艦これの場合、レアカードの所有率の分布とかでしょうか。

そこで他のユーザのデータもアップロードしてもらえるように、Admiral Stats を設置したサイトを公開するための準備中です。ただ、以下のような作業がまだ残っていて、公開できるのは1〜2週間先になる見込みです。

  • サーバのレンタル
  • SSL 証明書導入(Let's Encrypt)と HTTPS 対応
  • デプロイ自動化スクリプトの作成(場当たり的に開発環境を作ったので、必要な手順や設定を整理できてなくて……)
  • production 設定での動作確認
  • 最低限のテスト

もし、Admiral Stats を使ってみたい方は、admiral_stats_exporterで事前にプレイデータをエクスポートしておいてください。ただ、こちらはあくまで非公式のツールなので、リンク先の説明を理解したうえで、利用は自己責任でお願いします。

あと、このエクスポータは突貫で実装したツールなので、使いづらいのはご容赦ください……。本当は Agent Stats のように、スマホだけでエクスポートからインポートまで完結できると良いと思うんですけどね。そこまで手が回りませんでした。

Admiral Stats についての紹介はここまでで、これ以降は Rails 5 での実装に関する細かい話です。

実装の詳細

最近は PHP や Java で Web アプリを作っていたので、rails でまともにアプリを作るのは、Ruby on Rails 2 以来だったりします。そのため、Rails を使い慣れている人には当たり前の話が多いかもしれません。

開発環境

コーディングはホストOS(Mac OS X Yosemite)、実行はゲストOS(Vagrant + VirtualBox + CentOS 7.2)で行いました。IDE は、最近 IntelliJ に慣れてきたので RubyMineにしました。

  • IDE: RubyMine 2016.2.1
  • Ruby: ruby 2.3.1p112
  • Ruby on Rails: Rails 5.0.0.1

プラグイン

画面は Bootstrap のデフォルトのデザインをほぼそのまま採用し、グラフは Highcharts、表は Datatables で作りました。いずれも gem でインストールできました。便利ですね。

自分で明示的に導入したプラグインと、導入方法、参考にしたページなどは以下の通りです。

bootstrap-sass (3.3.7)

  • twbs/bootstrap-sass: Official Sass port of Bootstrap 2 and 3.
  • rails newを実行した時点で、Gemfiles に gem 'sass-rails', '~> 5.0'が入っていた。そのため、追加したのは gem 'bootstrap-sass', '~> 3.3.6'のみ。
  • application.css のファイル名を application.scss に変更し、以下の行を追加。
@import "bootstrap-sprockets";
@import "bootstrap";
  • application.scss にした時点で、元の CSS ファイルにあった *= require_tree .の文法は使えなくなる。そのため、rails generate controller <controller_name>で自動生成される <controller_name>.scss は、自動的には読み込まれない。もし読み込みたければ、各ファイルを明示的に @import で指定するか、css - Proper SCSS Asset Structure in Rails - Stack Overflowの回答(日本語訳)にあるような手段を使う必要がある。
  • Sass 自体については、後述する書籍と、Sass + Railsの基礎 - Qiitaを主に参考にした。
  • Bootstrap の使い方については、公式サイトの Getting Startedの Examples と、Componentsおよび CSSを参考にした。

jquery-datatables-rails (3.4.0)

  • jquery-datatables-railsの "Twitter Bootstrap 3 Installation"の手順に従ってインストール。ただし、Sass 版の Bootstrap をインストールしたので、application.scss には以下のように記載する。
@import "dataTables/bootstrap/3/jquery.dataTables.bootstrap";

highcharts-rails (4.2.5)

//= require highcharts
//= require highcharts/highcharts-more

// チャート画像のダウンロード機能
//= require highcharts/modules/exporting
//= require highcharts/modules/offline-exporting

omniauth (1.3.1), omniauth-twitter (1.2.1)

google-analytics-rails (1.1.0)

Ruby on Rails 5 を使ってみた感想

Rails 2 時代の知識のアップデートするために、まずは本屋で Rails 4 の本をいくつか流し読みしてから、そのうちの1冊を買ってきて読みました。これは内容が網羅的で、かつ読みやすい良書でした。

Ruby on Rails 4 アプリケーションプログラミング

Ruby on Rails 4 アプリケーションプログラミング

また、Rails 5 に関するページをいくつか流し読みしました。主に参考にしたページはこのあたりです。

今回の開発の範囲では、基本的な機能しか使わなかったせいか、Rails 5 だからという理由でつまづくことは特にありませんでした。本当に何もなくて、拍子抜けしたくらいです。

Rails 5 からデフォルトの開発用 Web サーバが Webrick から Puma に変わったとのことですが、特に意識せずに使えました。また、プラグインも、Rails 5 だから動かない、というものはありませんでした。

強いて言えば、いままでは rake db:migrateのように rake で実行していたコマンドが、rails db:migrateで実行できるようになったので、新しいやり方に慣れるためになるべく rails の方を使っていました。まあ、rake の方も使えるので、無理に rails を使う必要はなさそうですけど。

今後、Admiral Stats に機能を追加する機会があれば、API mode など、Rails 5 の新機能をうまく入れ込んでみたいと思います。

*1:Ingress ではプレイヤーのことを Agent と呼び、艦これでは提督(Admiral)と呼ぶため。

Ansible 2.4 で import_tasks/include_tasks に tags を付けるときの注意点

$
0
0
f:id:muziyoshiz:20160331232512p:plain:w300

前提:Ansible 2.4 から include は非推奨になった

Ansible 2.4 を使っていたら既に嫌というほど見てると思いますが、include を使うと以下のような警告が出るようになりました。

[DEPRECATION WARNING]: The use of 'include' for tasks has been deprecated. Use 'import_tasks' for static inclusions or 'include_tasks' for dynamic inclusions. This feature will be removed in a future release. Deprecation warnings can be disabled by setting deprecation_warnings=False in ansible.cfg.

Ansible 2.4 から、いままで includeが持っていた機能は以下の4つのアクションに分けられました。

Action Operation Reusable content
import_tasks static task
include_tasks dynamic task
import_playbook static playbook
include_playbook dynamic playbook
include (DEPRECATED) both static and dynamic task or playbook

この変更は、静的(static)な読み込みと動的(dynamic)な読み込み、そして読み込む対象(task ファイルか playbook ファイルか)を明確に区別することを目的に行われたようです。

static と dynamic の違い

ここで言う static と dynamic の意味は、公式サイトの Creating Reusable Playbooksによると次の通りです。

  • 静的な読み込み(static import)は、Playbook をパースする段階で、事前に実行される
  • 動的な読み込み(dynamic include)は、タスクを順に処理して、その行に来た段階で実行される

includeは基本的には static import を行いますが、状況によって dynamic include を行うなどして動作がわかりづらいため、Ansible 2.8 で廃止予定とのことです。

じゃあ今までに書いた includeはどうすればいいのか?というと、個人的な意見ですが、特に理由がなければ import_tasksimport_playbookに書き換えておけばいいと思います。dynamic include にすると文法ミスの事前検知が行われなくなるので、デバッグが面倒になります。

もっと詳しく知りたい方は、以下のページをどうぞ。@heriet さんによる Qiita の記事は、Ansible 2.2 の時点で書かれたものですが、具体例が多く、基本的な考え方の勉強になりました。

本題:import_tasks/include_tasks に tags を付けた場合の動作

ここまでは前提の話で、ここからやっと本題です。

以下のように import_tasks/include_tasks に tags を付けると、その中に含まれるすべてのタスクに同じタグを付けることができます。これは公式の Tags — Ansible Documentationにも書かれた方法です。

- import_tasks: main.yml
  tags: [ tag1 ]
- include_tasks: main.yml
  tags: [ tag1 ]

例えば、main.yml の中身が、

- debug: msg="Task 1"

- debug: msg="Task 2"

の場合、ansible-playbook --tags=tag1で両方のタスクが実行されます。

しかし、何かの理由で Task 2 だけ実行したくなったとします(Ansible 使ってるとよくありますよね?)。そこで、

- debug: msg="Task 1"

- debug: msg="Task 2"
  tags: [ tag2 ]

と直して、ansible-playbook --tags=tag2を実行すると、include_tasks を使った場合は Task 2 が実行されません。どうやら、以下のような動作の違いがあるようです。

  • import_tasksに tags を付けた場合は、「main.yml 内のすべてのタスクに追加するタグ」として扱われる
  • include_tasksに tags を付けた場合は、「include_tasksアクション自体に追加するタグ」のように扱われる

ローカルホストで実験

ローカルホストで簡単に実験できるのでやってみましょう。Gist にファイルを用意しました。

Example of import_tasks/include_tasks with tags

これらのファイルを同じディレクトリに置いて、以下のように実行してください。

$ ansible-playbook playbook1.yml --tags=tag1

Ansible 2.4.2.0 で実行した結果は、以下の通りです。include_tasksの場合だけ、tag2 を付けたタスク(Task 2)が無視されているのがわかります。

No. Playbook How to import --tags option Task 0 Task 1 Task 2
1 playbook1.yml include None ok ok ok
2 --tags=tag1 ok ok ok
3 --tags=tag2 - - ok
4 playbook2.yml import_tasks None ok ok ok
5 --tags=tag1 ok ok ok
6 --tags=tag2 - - ok
7 playbook3.yml include_tasks None ok ok ok
8 --tags=tag1 ok ok ok
9 --tags=tag2 - - -

まとめ:結局どうしたらいいのか?

import_tasksinclude_tasksを使った場合で、タグの扱いに微妙な違いがあることがわかりました。

上記の結果を見ると、「include_tasks使うとタグが無視されることがあるのか。使うのやめよう」と思うかもしれません。しかし、以下のように書かれているのに、main.yml 内に tag1 以外のタグがついているというのは、それはそれで可読性に問題があります。

- import_tasks: main.yml
  tags: [ tag1 ]

そう考えると、結論としては「import_tasks/include_tasks で読み込むタスクにはタグを付けない」というルールを設けるのが良いのではないでしょうか。

OS X + Docker Machine + Cloudera QuickStart Docker Image で Spark MLlib のお試し環境を構築する

$
0
0
f:id:muziyoshiz:20160529223041p:plain

はじめに

Cloudera は以前から、Hadoop の機能を簡単に試すための VM イメージを配布しています(Cloudera QuickStart VM のダウンロードページ)。配布されているイメージは KVM 版、Virtual Box 版、VMWare 版の3種類です。

しかし、Vagrant で使える box ファイル版は提供されておらず、コマンド一発での環境構築はできませんでした。もちろん、Virtual Box 版のイメージから box ファイルを作ることは可能ですが、Cloudera のバージョンアップに追従する作業は面倒でした。

しかし、去年の12月から、Cloudera 社が QuickStart VM の Docker イメージ版を公式に配布するようになりました。以下は、公式配布に関する Cloudera 公式のブログ記事です。

blog.cloudera.com

最近、Spark MLlib を勉強するための環境を作る機会があったので、せっかくなので Cloudera QuickStart Docker Image で環境構築してみました。その際に、普通に進めるとうまくいかないポイントがいくつかあったので、そのときの構築手順をまとめておきます。

動作環境

  • MacBook Pro (Retina, 15-inch, Mid 2014)
    • 16GB onboard memory
  • OS X Yosemite version 10.10.5

構築される環境

今回の手順を実行すると、最終的に以下のような環境が構築されます。

  • VirtualBox 上で、Docker 用の Linux VM(名前:default)が動作する。
    • この Linux VM は CPU 4コア、メモリ 9GB を使用(デフォルトは CPU 1コア、メモリ 2GB)
  • 上記の Linux VM 上で、Cloudera Quickstart Docker Image から作られたコンテナが動作する。
    • このコンテナはメモリ 8GB を使用
  • Docker コンテナ内のシェルから、spark-shell を実行できる。

Cloudera Express は 8GB 以上のメモリを必要とします(参考:Cloudera QuickStart VM)。そのため、構築手順のなかで、Linux VM のメモリをデフォルトより増やす作業を行います。

構築手順

Docker Toolboxのインストール

OS X への Docker のインストール方法は、Installation on Mac OS Xが詳しいです。OS X 上で Linux の Docker イメージを直接動かすことはできないため、VirtualBox で Linux VM(名前は default)を動作させ、この Linux VM 上で Docker daemon を動作させます。

f:id:muziyoshiz:20160529182710p:plain
(※ Installation on Mac OS Xより抜粋)

まず、Docker Toolboxからインストーラをダウンロードして、Docker Toolbox をインストールします。現時点の最新版は DockerToolbox-1.11.1b.pkg です。

インストール後に以下のコマンドを実行すると、Docker のバージョンを確認できます。この時点では、まだ default VM が動いていないので、Docker daemon に接続できないというメッセージが出ます。

% docker version
Client:
 Version:      1.11.1
 API version:  1.23
 Go version:   go1.5.4
 Git commit:   5604cbe
 Built:        Tue Apr 26 23:44:17 2016
 OS/Arch:      darwin/amd64
Cannot connect to the Docker daemon. Is the docker daemon running on this host?

default VM の初期設定

OS X の Launchpad から Docker Quickstart Terminal を実行します。しばらく待って、Docker のアイコンが出てくれば設定完了です。この例では、Docker ホストの IP アドレスは 192.168.99.100 になりました。

Last login: Sat May 21 00:24:50 on ttys004
bash --login '/Applications/Docker/Docker Quickstart Terminal.app/Contents/Resources/Scripts/start.sh'
% bash --login '/Applications/Docker/Docker Quickstart Terminal.app/Contents/Resources/Scripts/start.sh'
Running pre-create checks...
Creating machine...
(default) Copying /Users/myoshiz/.docker/machine/cache/boot2docker.iso to /Users/myoshiz/.docker/machine/machines/default/boot2docker.iso...
(default) Creating VirtualBox VM...
(default) Creating SSH key...
(default) Starting the VM...
(default) Check network to re-create if needed...
(default) Waiting for an IP...
Waiting for machine to be running, this may take a few minutes...
Detecting operating system of created instance...
Waiting for SSH to be available...
Detecting the provisioner...
Provisioning with boot2docker...
Copying certs to the local machine directory...
Copying certs to the remote machine...
Setting Docker configuration on the remote daemon...
Checking connection to Docker...
Docker is up and running!
To see how to connect your Docker Client to the Docker Engine running on this virtual machine, run: /usr/local/bin/docker-machine env default


                        ##         .
                  ## ## ##        ==
               ## ## ## ## ##    ===
           /"""""""""""""""""\___/ ===
      ~~~ {~~ ~~~~ ~~~ ~~~~ ~~~ ~ /  ===- ~~~
           \______ o           __/
             \    \         __/
              \____\_______/


docker is configured to use the default machine with IP 192.168.99.100
For help getting started, check out the docs at https://docs.docker.com

ちなみに、この表示が出た後で VirtualBox Manager を開くと、default という名前の VM が「実行中」になっていることが確認できます。

default VM のCPUコア、メモリ使用量の変更

Docker コンテナが 8GB のメモリを使えるようにするには、Docker が動作する default VM のメモリ使用量を 8GB 以上にする必要があります。メモリを増やす方法は、以下のページを参考にしました。

具体的な手順は以下の通りです。

  • docker-machine stop defaultを実行し、default VM を停止
  • VirtualBox Manager を起動し、"default"という名前の VM の「設定」欄を開く
  • 設定の「システム」タブで、「マザーボード」を選択し、メインメモリーを 2048MB から 9216MB(9GB)に変更
  • 同じく、設定の「システム」タブで、「プロセッサー」を選択し、プロセッサー数を 1 から 4 に変更
  • 「OK」ボタンを押して、設定を閉じる
  • docker-machine start defaultを実行し、default VM を再起動

docker infoコマンドを実行すると、メモリとCPUコア数が増えていることが確認できます。

% docker info
Containers: 0
 Running: 0
 Paused: 0
 Stopped: 0
Images: 0
Server Version: 1.11.1
Storage Driver: aufs
 Root Dir: /mnt/sda1/var/lib/docker/aufs
 Backing Filesystem: extfs
 Dirs: 0
 Dirperm1 Supported: true
Logging Driver: json-file
Cgroup Driver: cgroupfs
Plugins:
 Volume: local
 Network: bridge null host
Kernel Version: 4.4.8-boot2docker
Operating System: Boot2Docker 1.11.1 (TCL 7.0); HEAD : 7954f54 - Wed Apr 27 16:36:45 UTC 2016
OSType: linux
Architecture: x86_64
CPUs: 4
Total Memory: 8.762 GiB
Name: default
ID: 5L66:CKKI:BIK7:BRUT:H6NI:XMFL:AR36:R6UQ:YOFG:OQPM:M5L6:PU6L
Docker Root Dir: /mnt/sda1/var/lib/docker
Debug mode (client): false
Debug mode (server): true
 File Descriptors: 13
 Goroutines: 32
 System Time: 2016-05-24T13:18:09.388371356Z
 EventsListeners: 0
Registry: https://index.docker.io/v1/
Labels:
 provider=virtualbox

Cloudera QuickStart Docker Image のダウンロード

Cloudera QuickStart Docker Image は Docker Hub で公開されています(Docker Hub の cloudera/quickstart ページ)。そのため docker pullコマンドでイメージをダウンロードできます。ダウンロードサイズが 4.4GB あるので、結構時間がかかりました。

% docker pull cloudera/quickstart:latest
latest: Pulling from cloudera/quickstart
1d00652ce734: Pull complete
Digest: sha256:f91bee4cdfa2c92ea3652929a22f729d4d13fc838b00f120e630f91c941acb63
Status: Downloaded newer image for cloudera/quickstart:latest
% docker images
REPOSITORY            TAG                 IMAGE ID            CREATED             SIZE
cloudera/quickstart   latest              4239cd2958c6        6 weeks ago         6.336 GB

コンテナの起動

以下のコマンドを実行して、コンテナを起動します。

docker run --hostname=quickstart.cloudera \
--privileged=true -t -i -d -p 18888:8888 -p 17180:7180 -p 10080:80 \
-m 8192m cloudera/quickstart:latest /usr/bin/docker-quickstart

このコマンドは Docker Hub の cloudera/quickstart ページにあるものをベースに、以下の修正を加えたものです。

  • コンテナをデーモンとして起動(あとから docker exec で接続)
  • -m 8192mを指定して、メモリ使用量を 8GB に指定
  • マッピングされるポート番号が起動のたびに変わるのを避けるために、マッピング先を、元のポート番号に10000足した値に固定

コンテナの起動後は、docker exec コマンドで、コンテナ上のシェルに接続します。以下の例では、コンテナ ID の一部を指定して接続しています。

% docker ps
CONTAINER ID        IMAGE                        COMMAND                  CREATED             STATUS              PORTS                                                                     NAMES
048c433261a7        cloudera/quickstart:latest   "/usr/bin/docker-quic"   23 seconds ago      Up 23 seconds       0.0.0.0:10080->80/tcp, 0.0.0.0:17180->7180/tcp, 0.0.0.0:18888->8888/tcp   gigantic_poitras
% docker exec -it 048c bash

接続後に free コマンドでメモリ使用量を見ると、8GB 以上のメモリを使っているように見えます。

[root@quickstart /]# free
             total       used       free     shared    buffers     cached
Mem:       9187728    5623316    3564412     169020      34224    1127784
-/+ buffers/cache:    4461308    4726420
Swap:      3227556          0    3227556

ただ、cgroup の設定を確認すると、1024*1024*1024*8 = 8589934592で 8GB になっていたので、制限は効いているのではないかと思います。このあたりはちょっと自信がないです。

[root@quickstart /]# cat /sys/fs/cgroup/memory/memory.limit_in_bytes
8589934592

Cloudera Express の起動

コンテナ内で以下のコマンドを実行し、Cloudera Express を起動します。このコマンドは、コンテナの再起動後にも実行する必要があります。

[root@quickstart /]# /home/cloudera/cloudera-manager --express --force
[QuickStart] Shutting down CDH services via init scripts...
kafka-server: unrecognized service
JMX enabled by default
Using config: /etc/zookeeper/conf/zoo.cfg
[QuickStart] Disabling CDH services on boot...
error reading information on service kafka-server: No such file or directory
[QuickStart] Starting Cloudera Manager server...
[QuickStart] Waiting for Cloudera Manager API...
[QuickStart] Starting Cloudera Manager agent...
[QuickStart] Configuring deployment...
Submitted jobs: 15
[QuickStart] Deploying client configuration...
Submitted jobs: 16
[QuickStart] Starting Cloudera Management Service...
Submitted jobs: 24
[QuickStart] Enabling Cloudera Manager daemons on boot...
________________________________________________________________________________

Success! You can now log into Cloudera Manager from the QuickStart VM's browser:

    http://quickstart.cloudera:7180

    Username: cloudera
    Password: cloudera

出てくるメッセージは QuickStart VM の場合と全く一緒ですね……。

Web UI の接続確認

ここまで来れば、ホスト OS(OS X)の Web ブラウザから、Cloudera の Web UI の動作を確認できます。

また、この時点ではアクセスできませんが、Cloudera Manager から Hue を起動した後に、以下のアドレスで Hue にアクセスできます。

Cloudera Manager からサービス起動

http://192.168.99.100:17180/にアクセスし、以下の手順で、Spark Shell の動作に必要なサービスを起動します。

  • Cloudera Manager Service の起動を待つ
  • クロックオフセットの設定を修正する(後述)
  • 他のサービスを、以下の順に起動する
    • HDFS
    • Hive
    • YARN
    • Spark

サービスの起動までに時間が掛かります。起動した直後は Bad Health のことがありますが、その場合はしばらく待てば起動します。

また、他の人の環境でも起こる問題かはわからないのですが、私の環境では Host のステータスが以下のようになる問題が発生しました。

致命的なヘルスの問題:クロックのオフセット
(英語では Bad health: Clock Offset)

この問題は、同じ OS X マシンで Cloudera QuickStart VM を使ったときにも発生したのですが、そのときと同じ対処方法が有効でした。以下の手順に従って、オフセットの設定を無効にすれば、ステータスが Good に戻って、問題なく動作するようになります。

  • Cloudera Managerのメニューから、 Host(ホスト) → Configuration(設定) と遷移する
  • 検索キーワード欄を使って、Host Clock Offset Thresholds(ホストクロックオフセットのしきい値) を表示する
  • この設定の「Warning(警告)」と「Critical(致命的)」を、両方とも「Never(行わない)」に変更する
  • 「Save Changes(変更の保存)」ボタンを押す

Spark Shell の起動

Spark Shell を初めて起動する前に、以下のコマンドを実行してください。

[root@quickstart /]# sudo -u hdfs hadoop fs -chmod 777 /user/spark
[root@quickstart /]# sudo -u spark hadoop fs -chmod 777 /user/spark/applicationHistory

上記のコマンドを実行しなかった場合、Spark Shell の起動中に以下のエラーが表示されてしまいます。どうも、Cloudera QuickStart の不備のようです(参考:Solved: [CDH 5.5 VirtualBox] unable to connect to Spark Ma... - Cloudera Community)。

16/05/29 06:37:15 ERROR spark.SparkContext: Error initializing SparkContext.
org.apache.hadoop.security.AccessControlException: Permission denied: user=root, access=WRITE, inode="/user/spark/applicationHistory":spark:supergroup:drwxr-xr-x

そして、以下のコマンドを実行すると Spark Shell が起動します。Cloudera Quickstart VM は擬似分散モードで動作しているので、引数には --master yarn-clientを指定する必要があります。

[root@quickstart /]# spark-shell --master yarn-client
Setting default log level to "WARN".
To adjust logging level use sc.setLogLevel(newLevel).
Welcome to
      ____              __
     / __/__  ___ _____/ /__
    _\ \/ _ \/ _ `/ __/  '_/
   /___/ .__/\_,_/_/ /_/\_\   version 1.6.0
      /_/

Using Scala version 2.10.5 (Java HotSpot(TM) 64-Bit Server VM, Java 1.7.0_67)
Type in expressions to have them evaluated.
Type :help for more information.
Spark context available as sc (master = yarn-client, app id = application_1464516865260_0001).
SQL context available as sqlContext.

scala>

以上で、Spark MLlib を試すための環境構築は完了です。

Spark MLlib を試す

Apache Spark入門 動かして学ぶ最新並列分散処理フレームワーク (NEXT ONE)

Apache Spark入門 動かして学ぶ最新並列分散処理フレームワーク (NEXT ONE)

  • 作者:株式会社NTTデータ,猿田浩輔,土橋昌,吉田耕陽,佐々木徹,都築正宜,下垣徹
  • 出版社/メーカー:翔泳社
  • 発売日: 2015/10/29
  • メディア:大型本
  • この商品を含むブログを見る

構築に成功したか確認するために、Apache Spark 入門の8章に掲載されていたコードを Spark Shell で実行してみました。内容は、K-means を用いたクラスタリングです。

HDFS へのサンプルデータのアップロード

Cloudera QuickStart には Spark MLlib のサンプルデータが含まれていませんでした。そのため、まずはテストに使うサンプルデータをダウンロードして、HDFS にアップロードします。

[root@quickstart /]# curl https://raw.githubusercontent.com/apache/spark/master/data/mllib/kmeans_data.txt > kmeans_data.txt
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0    72    0    72    0     0     40      0 --:--:--  0:00:01 --:--:--    47
[root@quickstart /]# cat kmeans_data.txt
0.0 0.0 0.0
0.1 0.1 0.1
0.2 0.2 0.2
9.0 9.0 9.0
9.1 9.1 9.1
9.2 9.2 9.2
[root@quickstart /]# hadoop fs -put kmeans_data.txt /tmp
[root@quickstart /]# hadoop fs -ls /tmp
Found 5 items
drwxrwxrwx   - hdfs   supergroup          0 2016-05-29 13:02 /tmp/.cloudera_health_monitoring_canary_files
drwxrwxrwt   - mapred mapred              0 2016-04-06 02:26 /tmp/hadoop-yarn
drwx-wx-wx   - hive   supergroup          0 2016-05-29 09:57 /tmp/hive
-rw-r--r--   1 root   supergroup         72 2016-05-29 13:02 /tmp/kmeans_data.txt
drwxrwxrwt   - mapred hadoop              0 2016-05-29 10:19 /tmp/logs

サンプルデータの RDD への変換

Spark Shell を起動して、HDFS からサンプルデータを読み込んで、RDD に変換します。

[root@quickstart /]# spark-shell --master yarn-client
(中略)

scala> import org.apache.spark.mllib.clustering.KMeans
import org.apache.spark.mllib.clustering.KMeans

scala> import org.apache.spark.mllib.linalg.Vectors
import org.apache.spark.mllib.linalg.Vectors

scala> val data = sc.textFile("hdfs://localhost/tmp/kmeans_data.txt")
data: org.apache.spark.rdd.RDD[String] = hdfs://localhost/tmp/kmeans_data.txt MapPartitionsRDD[1] at textFile at <console>:29

scala> val parsedData = data.map{ s =>
     |   Vectors.dense(
     |     s.split(' ').map(_.toDouble))
     | }.cache()
parsedData: org.apache.spark.rdd.RDD[org.apache.spark.mllib.linalg.Vector] = MapPartitionsRDD[2] at map at <console>:31

KMeans のモデルの作成

これ以降は、本に載っているコードをそのまま実行できました。KMeans.train メソッドでモデルを生成します。

scala> val numClusters = 2
numClusters: Int = 2

scala> val numIterations = 20
numIterations: Int = 20

scala> val clusters = KMeans.train(parsedData, numClusters, numIterations)
16/05/29 13:07:00 WARN netlib.BLAS: Failed to load implementation from: com.github.fommil.netlib.NativeSystemBLAS
16/05/29 13:07:00 WARN netlib.BLAS: Failed to load implementation from: com.github.fommil.netlib.NativeRefBLAS
clusters: org.apache.spark.mllib.clustering.KMeansModel = org.apache.spark.mllib.clustering.KMeansModel@5d27cdd2

上記の WARN は、native library のロードに失敗したことを表しているようです(参考:Using Spark MLlib)。Hardware acceleration が効かないだけなので、この WARN は無視します。

生成されたモデルの確認

生成されたクラスタの数、および各クラスタの中心の座標を確認します。

scala> clusters.k
res0: Int = 2

scala> clusters.clusterCenters
res1: Array[org.apache.spark.mllib.linalg.Vector] = Array([0.1,0.1,0.1], [9.099999999999998,9.099999999999998,9.099999999999998])

新たなベクトルを与えて、いずれのクラスタに分類されるか確認します。

scala> val vec1 = Vectors.dense(0.3, 0.3, 0.3)
vec1: org.apache.spark.mllib.linalg.Vector = [0.3,0.3,0.3]

scala> clusters.predict(vec1)
res2: Int = 0

scala> val vec2 = Vectors.dense(8.0, 8.0, 8.0)
vec2: org.apache.spark.mllib.linalg.Vector = [8.0,8.0,8.0]

scala> clusters.predict(vec2)
res3: Int = 1

元のファイルに含まれていた座標が、いずれのクラスタに分類されるかを判定します。判定結果は HDFS に出力します。

scala> val predictedLabels = parsedData.map(vec => clusters.predict(vec))
predictedLabels: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[32] at map at <console>:39

scala> predictedLabels.saveAsTextFile("/tmp/output")

scala> exit
[root@quickstart mllib]# hadoop fs -ls /tmp/output
Found 3 items
-rw-r--r--   1 root supergroup          0 2016-05-29 07:12 /tmp/output/_SUCCESS
-rw-r--r--   1 root supergroup          8 2016-05-29 07:12 /tmp/output/part-00000
-rw-r--r--   1 root supergroup          4 2016-05-29 07:12 /tmp/output/part-00001
[root@quickstart mllib]# hadoop fs -cat /tmp/output/part-*
1
1
1
0
0
0

まとめ

一通りの作業を試してみて、Cloudera QuickStart Docker Image で Spark MLlib のお試し環境を構築できることがわかりました。

使ってみた感想としては、コマンドラインから QuickStart のコンテナを起動・停止できるのは便利でした。その一方で、この QuickStart Docker Image は、Cloudera QuickStart VM を単純に Docker image にしただけ、という感じで、Web ブラウザから行わなければいけない設定やサービス起動は相変わらず多かったです。そのため、この Docker image をベースに環境構築を自動化するのは大変そうです。

Spark を CDH と一緒に使いたいという特別な理由がなければ、もっと軽量な Docker イメージを探したほうがよいかもしれませんね……。

Fluentd Meetup 2016 Summer レポート 〜 v0.14 の新機能からプラグイン開発者向け API まで

$
0
0
f:id:muziyoshiz:20160602000413p:plain
  • イベント名:Fluentd Meetup 2016 Summer
  • 開催日時:2016-06-01(月)
  • 会場:イベント&コミュニティスペース dots.

約1年ぶりに開催された Fluentd Meetup に参加してきました。今回は、5月31日にリリースされたメジャーバージョンアップの v0.14 について、ユーザ向けの機能紹介から、プラグイン開発者向けの深い話まで、盛りだくさんの内容でした。自分でプラグインを書くくらい、Fluentd をヘビーに使う人向けのイベントという感じで、どの話も面白かったです。

最近、私は Fluentd を使う機会が全然なかったこともあって、「Fluentd も機能的には枯れてきて、そろそろ新機能もあまりないだろう」と思っていたのですが、まだこんなに改善の余地があったのか……とちょっと驚きました。個人的には、古橋さんの講演で将来の構想として出てきた、Kafka っぽい PubSub は便利そうなので是非実装してほしいです。

以下、講演内容のメモです。プレゼンに書かれてなくて口頭で説明されていたことや、個人的に気になったことを中心にメモしています。

講演内容

Past & Future of Fluentd (古橋 貞之, @frsyuki)

  • Fluentdの歴史

    • 2011年6月に最初のバージョンをリリースした。今年の6月で5年。ここまで続くと、かなり成功したOSSと言っていいのではないか。
    • 新メジャーバージョンの 0.14 が昨日リリースされた。
  • 2年前にリリースされたv0.12の機能の振り返り

    • タグの書き換えを不要にする label と filter
  • 将来の構想

    • Advanced back pressure - 転送先が詰まるのを防ぐ
    • PubSub & pull forward - Kafkaに近い発想。ログが必要なアプリが取りに行く
    • Compressed buffer
    • Distributed buffer - twitterがリリースした distributedlogや、Apache BookKeeperのような機能を Fluentd の buffer のところに入れる
    • Service plugins - パッケージ化されたソフト(例えば Norikra)をFluentdで動作させるようにする。service label を書くだけで起動。インストールが簡単になる
    • Schema-validated logging - バリデートされたログだけを転送することで、無駄な転送や、データベース書き込み時のエラーを回避する
    • Centralized config manager - Chef や Ansible を使うことも考えられるが、Dockerで起動したインスタンスのなかの設定に Chef などを使いたくない。これに fluentd-ui が組み合わさると嬉しい
  • Fluentdのロゴが変わる予定

v0.14 Overview (中川 真宏, @repeatedly)

  • New Plugin APIs

    • v0.12向けに作ったプラグインは、互換性レイヤがあるので、ほぼ動くはず
    • インスタンス変数に直接アクセスしているようなプラグインは、動かない可能性がある
    • 次の v1 でも互換性レイヤは残す予定
  • buffer の設計の大幅な見直し

    • v0.12 buffer design: chunkの保存方法。ElasticSearch に書き込むときに、インデックスの値を見て書き込み先を変える、といった場合に問題になった。リトライがうまくいかない。
    • v0.14で、chunkの管理が柔軟になり、このような問題は解決できる
  • Plugin Storage & Helpers

    • プラグイン内で使えるストレージレイヤ(KVS)の導入
    • プラグインの作成時によく使う雛形コードをhelperで提供
  • Time with nanosecond

    • ミリ秒とナノ秒のどちらにするか、という議論があったが、ナノ秒でログを出すシステムが増えてきたのでナノ秒
    • 内部的には Fluent::EventTime という形で持つ。既存のプラグインに対しては Integer っぽく振る舞うクラス。
  • ServerEngine based Supervisor

    • プロセスモデルを ServerEngine に置き換える
  • Windows support

    • Linux 向けに特化したコードを、Windows で動くように直したりした
    • HTTP RPC でバッファのフラッシュなどを行う
  • v0.14.x - v1

    • https://github.com/fluent/fluentd/issues/1000に並んでいる
    • v1 を出す。v1 で新機能を入れることはない。v1 は今まで入れたAPIをFixする、と宣言するためのバージョン。フィードバックは v0.14 のうちにしてほしい
    • v1 は 2016 4Q か 2017 1Q に出る
    • いくつか v1 で消したい機能はある
  • v0.14 系でこれから入れる予定の目玉機能

    • Symmetric multi-core processing: パフォーマンス向上。TCPポートをWorker間で共用し、Workerで処理を分ける。ServerEngine を使って実現する
    • Counter API: Workerが分かれるので、カウンタを作りづらくなる点をカバー
    • secure-forward をコアに入れる。forward と secure-forward の細かい違いをなくす
  • ベンチマーク結果

    • CPU使用率が、v0.14 で若干上がっている
    • EC2で、100,000msgs/sec での測定結果によると、CPU使用率が4〜5%上がった
    • v0.14はリリースしたばかりで、性能改善できる余地が残っている。これから改善する予定
  • td-agent 3

    • fluentd v0.14 は td-agent 3 から採用(現行の td-agent 2 系には入らない)
    • Ruby 2.3 に上げる
    • Windows 向けパッケージも提供予定
    • リリース日は未定(v0.14 がまだ stable と言えないので)
  • Q) chunk から取得したログの一部で処理が失敗した場合、リトライしたいと思うが、その場合 chunk の詰め直しはできるか?

    • A) できない。その場合は、エラーストリームという機能を使ってほしい

Fluentd ServerEngine Integration & Windows Support (成田 律太, @naritta)

  • 自己紹介

    • 2015年の Treasure Data のインターンシップに参加し、今年入社した
  • ServerEngine

    • Unicorn と大体同じようなもの
    • Supervisor, server, worker の構成を簡単に作れるようにするためのフレームワーク
    • Worker Module, Server Module を Ruby で書く
    • Worker type: thread, process, spawn
    • v0.14 では spawn worker type を利用している。Windows に fork の機能がないため
  • Fluentd で ServerEngine を採用したことによるメリット

    • auto restart
      • Linux でも Windows でも、fluentd のプロセス(worker)をKILLすると、自動的にリスタートするのを確認できる
    • live restart
      • 設定変更したあとでシグナル(または HTTP RPC)を送ると、worker だけをリスタートできる
      • server が TCP 接続を持っているので、ログは欠損しない
    • socket manager
      • server 側で listen して、socket を worker に共有する
      • 将来的にはマルチコアでソケットを共有するために ServerEngine を使う(いまはマルチコアで動かすには in_multiprocess plugin を使う必要がある)
    • signal handler
      • signal handler と log rotation は、Fluentd ユーザにとってのメリットというより、内部実装に関するメリット
      • シグナルをキューに溜めて、シグナル同士の競合を防ぐ
    • log rotation
      • ruby core にすでにポーティングされている機能
      • マルチプロセスでのログローテーションなど
  • Q) ServerEngine が入ることで、設定ファイルが増える?

    • A) Fluentd が内部的に ServerEngine の設定ファイルを作って、ServerEngine に渡す。ユーザ側は Fluentd/td-agent の設定ファイルだけを編集すれば良い

Fluentd v0.14 Plugin API Updates (田籠 聡, @tagomoris)

  • 何故新しい API セットを作ったか?

    • いままでのプラグインには、開発者向けドキュメントがなかった。みんな他の人の書いたプラグインの真似をして書いている
    • プラグイン内で独自にスレッドを起動するようなことが必要で、そのせいでテストしづらい
    • あるプラグインで使えるパラメータが、他のプラグインでも使えるのかどうかがわかりづらい
    • 割りとコアな処理をプラグイン側で上書きしてしまっていることがよくある
    • などなど
    • 上記のような問題を解決したい。また、便利な機能も一緒に提供することで、移行を進めたい
  • Compatibility of plugins

    • v0.12のプラグインは Fluent::Input などのクラスのサブクラス
    • compatibility layer: Fluent::Compat::Klass で、v0.12 にしかないメソッドを提供するなどして、互換性を維持
  • Compatibility of configurations

    • v0.12 形式のパラメータを v0.14 形式に自動変換する helper を提供する。plugin 開発者にこれを使ってもらう必要がある
    • v0.14 API を使ったプラグインを、v0.12 で動かすことはできない(ようにした)。gemspec に依存関係を書いてもらう必要がある
  • v0.14 プラグインクラスの書き方

    • すべてのクラスを Fluent::Plugin モジュールの下に置く(いままでは Fluent 直下だった)
    • 親クラスのメソッド(#configure とか)をオーバライドするときは必ず super を呼ぶ
    • super を呼んでない場合は、互換性レイヤが代わりに super を呼んで、WARNING を出す
    • 親クラス(Fluent::Plugin::Input とか)の階層を整理
  • Fluent::Plugin::Output

    • Output Plugin は物凄くいろいろ変わった。v0.12 まではやりたいことによって異なるクラスを使い分ける必要があったが、1個の Output クラスに統合された
    • chunk を分ける条件を細かく指定できるようになった。いままではサイズのみの chunking だったが、例えば時間でも chunking できるし、時間とサイズの組合せでもできる
    • Non-buffered, buffered synchronous, buffered asynchronous のいずれか1個を実装してあれば動く
  • Delayed commit

    • 書き込みの ACK を待ちたい場合、いままでは write メソッドの中で待つ必要があった。write メソッドが終わると、送ったデータが chunk から消されてしまうため
    • v0.14 では try_write メソッドで commit を待たずに return する。この時点では chunk が消されない。非同期のチェックスレッドが ACK を受け取って #commit_write が呼ばれた時点で、chunk が消される
    • ユースケース:分散ファイルシステムに書き込んでから、正しく読めることを確認してから chunk を消したい場合
  • configurations: flushing buffers

    • chunk を flush するタイミングを、細かく制御できるようになった
    • flush_mode: immediate → すぐに書き込んで欲しいが、失敗した時はリトライして欲しい場合に使うモード
  • Retries

    • いままではリトライ回数(retry_count)しか指定できなかったが、タイムアウト(retry_timeout)を指定できるようになった(どれだけの期間、リトライし続けるか)
    • いままでは72時間リトライし続けていた。この仕様は自明ではなかった。いつまで待てば諦めが付くのか、誰にもわからない状態だった
  • その他のプラグイン: Buffer, Parser, Formatter, Storage, ...

    • これらを v0.14 では "Owned" plugin と呼ぶ → 他のプラグイン経由で実体化されるプラグイン
      • 対義語は primary plugins → Input, Output, Filter plugin
    • Storage plugin:プラグイン内でデータを永続化したい場合に使うプラグイン
      • 例:file plugin の pos file で管理している情報を、storage plugin で永続化
  • Plugin Helpers

    • ヘルパーを使いたいときは、明示的に "helpers :name"と指定してもらうことで、どのヘルパーを使っているのか明示されるようにした
  • New Test Drivers

    • いままでは実行タイミングに依存していたようなコードを、きちんとテストできるようにした
    • 例えば、flushのタイミングを制御するとか
  • Plans for v0.14.x

    • 古橋さんの話+α
    • plugin generator を入れようと思っている。いまは、他の人の書いたプラグインの真似をしてプラグインを書く、という状態で、これは良くない
    • 新しいAPI:Fluentd 全体で使えるバッファサイズの上限を指定、Counter API
  • 開発者向けのドキュメントはこれから書く(書かなければいけないと思ってはいる)

Habitat を触っていて気になった、細かいことあれこれ

$
0
0
f:id:muziyoshiz:20160617201522p:plain

Habitat について知りたい方は、まずは私がエンジニアブログに書いた Habitat の概要説明をご覧ください。自作のイメージ図を使って、Habitat のわかりにくい独自用語を解説しています。

recruit.gmo.jp

で、上記の記事を書いた時に、あまりにも細かすぎるので省いた話題がいくつかありました。放っておくと忘れそうなので、今回はその細かいことあれこれをご紹介します。

"Habitat"の意味

  • Habitat とは、居住環境、居住地、生息地、などの意味を持つ英単語です。
  • これを書いている時点で "Habitat"でググったところ 約 146,000,000 件ヒットしました。Chef といい、この会社はどうしてこう、検索しにくい名前をツールに付けてしまうのか……。

Habitat のバージョン番号

  • これを書いている時点で hab -Vを実行したら hab 0.7.0/20160614231131と出てきました。
  • 0.1 でも 1.0 でもない、これまた微妙なところを……。開発陣としては、現時点の実装をどれくらいの完成度だと思っているんでしょう? もう実サービスで使えるレベル? 少なくとも、ドキュメントのなかに「まだ production に使うな」というありがちな文章は見当たりませんでした。

Habitat の推奨環境

  • Habitat が推奨する、あるいは Habitat 開発者が最初にテストしている Linux ディストリビューションって何なんでしょう?
  • Habitat ファーストインプレッションにも書いた通り、現時点では Linux でしか Supervisor は動作しません。とはいえ、私が VirtualBox & CentOS 7 で試したところ、それでもチュートリアル通りには動きませんでした。
  • habitat-sh/habitat: Modern applications with built-in automation のトップディレクトリにある Dockerfile が FROM ubuntu:xenialで始まってるので、Ubuntu の可能性大。次に試すときは Ubuntu でやります。

Package と Artifact

  • ドキュメントを読んでいると、Package と同じ概念を Artifact と呼んでいる箇所がいくつかありました。Artifact という用語も使われているのか、この用語はもう廃止されたけどドキュメントの一部に残っているだけなのか?
  • そういえば、Package ファイルの拡張子の hart って、Habitat Artifact の略称なんですかね。

Depot への Package のアップロード

  • Habitat CLI referenceの目次には "hab pkg upload"のリンクが載っているんですが、このリンクをクリックした先の説明はありませんでした。
  • hap pkg upload --helpを実行すると、hab pkg upload [FLAGS] [OPTIONS] <HART_FILE>...と出てきます。なんだあるんじゃーんと思ってコマンドを叩いたら、チュートリアルで作った muziyoshiz/mytutorialapp を見事アップロードできました(アップロード先)。
  • で、アップロードはできたんですが、Habitat Web って、パッケージの削除機能がまだ無いみたいです。もしかして、これが CLI reference からコマンドが消されている理由では。削除機能が追加されたら消しますごめんなさい……。

Habitat の P2P ネットワークと Topology

  • Habitat は Supervisor 同士で P2P ネットワークを構築します。このネットワークのことを Ring あるいは Supervisor Ring と呼ぶようです。しかし、Supervisor Internalsによると、このネットワークは SWIM (Scalable Weakly-consistent Infection-style process group Membership protocol) で構成されるとのこと。リングネットワークを組んでいるわけでもないのに Ring というのはモヤモヤします。
  • 一方、Habitat には Topology という用語があり、Supervisor 間の論理的な関係を定義できます。Running packages in topologiesによると、現時点では standalone, leader-follower, initializer の3種類から選べるようです。Topology という単語が Network Topology を指しているわけではない、というのもなんだかモヤモヤします。

設定ファイルの一部で TOML 形式を採用

  • Habitat は設定ファイルを Handlebars形式で書くことができて、この Handlebars に渡す変数を TOML 形式で定義できます。
  • TOML 形式って初耳だったので調べてみたところ、toml/toml-v0.4.0.mdに仕様がありました。Tom's Obvious, Minimal Languageの略で TOML なんですね。僕も便乗して YOML とか作ってやろうか(紛らわしすぎる)。
  • ちなみにこの仕様、有志による日本語訳が toml/toml-v0.4.0.mdにて公開されていました。
  • HashiCorp にも HCLとかありますし、運用管理ツールを開発していると Yet Another な YAML が欲しくなるものなんでしょうか。

Habitat と Google Analytics

  • hab setup を実行してセットアップすると、その最後に、利用データを Habitat の Google Analytics アカウントにアップロードしてよいかと尋ねられます。No を選択すると ~/.hab/cache/analytics/OPTED_OUTに空のファイルが作られて、それきり何も質問されません。
  • ツールの利用状況を収集したい気持ちはよくわかります。でもまあ、No を選択しますよね。そういえば、最近 Google Analytics を後から導入して揉めたソフトがなにかあった気がしますけど、何でしたっけ?

とりとめもなくなってきたので、今日はこのへんで。

手を動かす Spark MLlib & Word2Vec Part 1 (spark-ec2 でクラスタを構築するまで)

$
0
0
f:id:muziyoshiz:20160626223709p:plain

このシリーズについて

機械学習系のツールを全然使ったことがなかったので、勉強のためになにか1つ選んで、実際に手を動かしてみることにしました。マシンを並べて負荷分散することを想定して、まずは Spark MLlib を選びました。

このシリーズでは、Amazon EC2 上に構築した Spark Cluster (Standalone Mode) で、Wikipedia のデータから Word2Vec のモデルを作るところまでの方法を解説していきます。ただ、実際やってみてわかったのですが、Spark 自体、Spark MLlib の Word2Vec クラス、およびクラスタ構築に使った spark-ec2 に設定項目が多いせいで、細かいところで何度も何度もつまづきました……。

そのため、このシリーズでは各ステップについて、「最終的にやったこと」と、その最終的なやり方にたどり着くまでに「つまづいたこと」を分けました。やり方を知りたいだけの場合は「最終的にやったこと」の方だけ読んでください。「つまづいたこと」は、うまく行かなかった場合のための参考情報です。

Part 1 の範囲

Amazon EC2 に master 1台、slave 3台構成の Spark Cluster (Standalone mode) を構築し、spark-shell から Word2Vec を実行するところまで。

Spark をローカル環境(Mac)にインストールする

最終的にやったこと

まず、ローカル環境で Spark MLlib が動くかどうかを試してみました。環境は以下の通りです。

  • MacBook Pro (Retina, 15-inch, Mid 2014)
  • OS: OS X Yosemite 10.10.5
  • CPU: 2.2 GHz Intel Core i7
  • メモリ: 16GB 1600 MHz DDR3

OS X に Spark をインストールする場合、以下のコマンドだけでインストールできます(参考:ApacheSpark — BrewFormulas)。

% brew update
% brew install apache-spark

私が試した時点では Spark 1.6.1 でした。Java は Java 8 です。

% spark-shell --version
Welcome to
      ____              __
     / __/__  ___ _____/ /__
    _\ \/ _ \/ _ `/ __/  '_/
   /___/ .__/\_,_/_/ /_/\_\   version 1.6.1
      /_/

Type --help for more information.

つまづいたこと

spark-shell ローカルモードで(--master localを指定して)実行すると、spark>というプロンプトが表示されるまでに、色々と WARN が出ます。

16/06/07 00:17:36 WARN Connection: BoneCP specified but not present in CLASSPATH (or one of dependencies)

BoneCPは JDBC Connection Pool ライブラリの名前です。scala - What do WARN messages mean when starting spark-shell? - Stack Overflowによると、ローカルモードで実行しているときは問題ないとのこと。

16/06/07 00:17:38 WARN ObjectStore: Version information not found in metastore. hive.metastore.schema.verification is not enabled so recording the schema version 1.2.0
16/06/07 00:17:38 WARN ObjectStore: Failed to get database default, returning NoSuchObjectException

こちらも、Hive metastore に接続できないことを表す WARN なので、ローカルモードでは関係ないと判断しました。

ローカル環境での Word2Vec の実行

最終的にやったこと

Spark MLlib のページ(Feature Extraction and Transformation)に、Spark MLlib に含まれる Word2Vec クラスを使ったサンプルコードがあります。これをローカルモードで実行してみます。

まず、サンプルコードで使っている text8.zip をダウンロードして、解凍します。これは、スペースで区切られた英単語が羅列された(意味のある文章ではない)100 MB のテキストファイルです。

% wget http://mattmahoney.net/dc/text8.zip
% unzip text8.zip
% ls -la text8
-rw-r--r--@ 1 myoshiz  staff  100000000  6  9  2006 text8

この text8 を置いたディレクトリで、以下のコマンドを実行します。spark.driver.memoryはドライバのメモリ使用量を表すオプションで、デフォルトは 1g(1GB)です。

% spark-shell --master local \
--conf spark.driver.memory=5g \
--conf spark.serializer=org.apache.spark.serializer.KryoSerializer

spark-shell のプロンプトで、Word2Vec のサンプルコードを入力すれば、myModelPath ディレクトリ以下に、Word2Vec のモデルデータが生成されます。

なお、spark-shell の起動中は http://localhost:4040/にアクセスすることで、ジョブの状態を確認できます。

つまづいたこと

最初は --conf spark.driver.memory=5g"を指定せずに spark-shell を起動していました。その状態でword2vec.fit(input)` を実行すると、OutOfMemoryError で spark-shell が落ちます。私の環境では、ファイルが 100MB だと落ちて、80MB まで減らすと落ちない、という状態でした。

scala> val model = word2vec.fit(input)
[Stage 0:>                                                          (0 + 1) / 3]
Exception in thread "refresh progress" java.lang.OutOfMemoryError: GC overhead limit exceeded
    at scala.StringContext.s(StringContext.scala:90)
(スタックトレース、および後続のエラーは省略)

エラーメッセージをもとに調べたところ、JVM の設定が悪いような情報をいくつか見かけました。

しかし、これを指定してもエラーメッセージは変わりませんでした。というか、私は Java 8 で実行していたので、そもそもこの設定には意味がありませんでした。

この Java の仕様変更を踏まえて、以下のように spark-shell を実行したところ、落ちなくなりました。ただし、この方法だと、OutOfMemoryError が出ないだけで、いつまでも処理が終わらないという状態になってしまいました……。

% SPARK_REPL_OPTS="-XX:MaxMetaspaceSize=1024m" spark-shell --master local

結局、Configurationに載っているメモリ関係のパラメータを一通り確認して、前述の spark.driver.memoryを増やしたところ、うまく動いたようで、処理が完了しました。JVM のパラメータを変更する必要はなかったようです。

Amazon EC2 への Spark クラスタの構築(spark-ec2 を使った方法)

最終的にやったこと

Slave の台数を増やすことで、Spark MLlib の実行時間が短くなることを確認するために、Spark クラスタを構築しました。今回は Spark に同梱されている spark-ec2 というスクリプトを使って構築しました。このスクリプトの説明は Running Spark on EC2 - Spark 1.6.1 Documentationにあります。

Amazon Elastic MapReduce (EMR) で Spark を使えることは知っていますが、いずれオンプレに Spark クラスタを構築したかったのと、かといってマシンスペックを何パターンか試すときに手作業での構築は大変すぎたので spark-ec2 を使いました。

まず、AWS のマネジメントコンソールを使って、以下の設定を行います。今回は Spark の話がメインなので、AWS の設定の詳細は省略します。

  • IAM ユーザ "word2vec-user"の作成
  • IAM ユーザ "word2vec-user"に対する "AdministratorAccess"ポリシーのアタッチ(EC2 と S3 に絞っても良い)
  • EC2 でのキーペア "word2vec-key-pair"の作成
  • ローカルマシンに対する環境変数 AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY の設定
  • ローカルマシンに対するキーペアの配置(以下では /Users/myoshiz/.ssh/word2vec-key-pair.pemに置いたと仮定)

brew で Spark をインストールすると、spark-ec2 は入っていません。そのため、spark-ec2 を使うために、Apache Spark のダウンロードページから zip ファイルをダウンロードします。今回は、以下のファイルを選択しました。

  • Spark release: 1.6.1
  • Package type: Pre-built for Hadoop 2.6 and later

ダウンロードした zip ファイルを解凍すると、ec2 ディレクトリに spark-ec2 というスクリプトが入っています。このディレクトリに移動し、まずは master 1台、slave 3台のクラスタを構築してみます。そのためには、以下のコマンドを実行します。

% ./spark-ec2 \
--key-pair=word2vec-key-pair \
--identity-file=/Users/myoshiz/.ssh/word2vec-key-pair.pem \
--region=us-west-1 \
--zone=us-west-1a \
--instance-type=m4.large \
--copy-aws-credentials \
--hadoop-major-version=yarn \
--slaves 3 \
launch spark-cluster

各オプションの意味と、上記の値を指定した理由は以下の通りです。

  • --regionは、デフォルトはバージニア北部(us-east-1)が使われる。国内だとインスタンス利用費が若干高く、東海岸は遠いので、北カリフォルニア(us-west-1)を指定した。
  • --instance-typeは、デフォルトでは m1.large が使われる。m1.large は古いインスタンスタイプのため、スペックに比して割高のため、同じく 2 vCPU、メモリ8GBの m4.large を指定。調べた時点では m1.large が $0.19/hour、m4.large が $0.14/hour だった。
  • --copy-aws-credentialsを指定すると、環境変数に設定された AWS のアクセスキーが、master の hadoop にも設定される。ただし、後述の通り Spark に対しては設定されないので、hadoop コマンドを使わないなら、指定しなくても良い。
  • --hadoop-major-version=yarnは、使用する Hadoop のバージョンを指定する。今回は Pre-built for Hadoop 2.6 and later をダウンロードしているので、yarn を指定する必要がある。デフォルトは 1(Hadoop 1.0.4)。
  • --slavesは slave の台数を指定する。

10〜20分待つとクラスタの構築が完了し、以下のようなメッセージが表示されます。Mac から以下の URL にアクセスすると、Spark UI や、Ganglia の画面を確認できます。

Spark standalone cluster started at http://ec2-xxx-xxx-xxx-xxx.us-west-1.compute.amazonaws.com:8080
Ganglia started at http://ec2-xxx-xxx-xxx-xxx.us-west-1.compute.amazonaws.com:5080/ganglia
Done!

上記のホスト名は、以降の作業でも使うので、以下のように環境変数に設定しておきます。シェルの設定ファイル(.bash_profile とか)で指定してもいいですが、クラスタを作るたびにホスト名が変わる点だけは注意が必要です。

% export EC2_SPARK_MASTER=ec2-xxx-xxx-xxx-xxx.us-west-1.compute.amazonaws.com

つまづいたこと(1):GitHub の spark-ec2

brew で spark をインストールすると、そのなかには spark-ec2 が入っていません。そのため、このスクリプトだけ別に入手できないかと思い、GitHub で公開されている spark-ec2 を clone して実行してみました。

github.com

この spark-ec2 を実行すると、エラーも出ずに最後まで処理が進むのですが、Spark クラスタが起動しないようです。http://ec2-xxx-xxx-xxx-xxx.us-west-1.compute.amazonaws.com:8080にアクセスしても応答がなく、spark-shell で --master spark://ec2-xxx-xxx-xxx-xxx.us-west-1.compute.amazonaws.com:7077を指定しても接続できない、という状態になりました。

色々悩んだ結果、大人しく Apache Spark のダウンロードページから zip ファイルをダウンロードして、そのなかの spark-ec2 を使ったところ、実行したコマンドの引数は同じにも関わらず、クラスタが起動しました。

GitHub 版も見た目はきちんと動いているように見えたために、他に原因があると思い込んでしまい、この問題で数日詰まってしまいました……。

つまづいたこと(2):--hadoop-major-version=yarnの指定

このオプションは、公式サイトの Running Spark on EC2には書かれていません。しかし spark-ec2 --helpを実行すると、以下のオプションが表示されます。

  --hadoop-major-version=HADOOP_MAJOR_VERSION
                        Major version of Hadoop. Valid options are 1 (Hadoop
                        1.0.4), 2 (CDH 4.2.0), yarn (Hadoop 2.4.0) (default:
                        1)

上記のオプションを指定しないと、クラスタの構築後に spark-shell を実行した時に、以下のようなエラーが出て sqlContext の初期化に失敗しました。

16/06/13 14:08:02 INFO DataNucleus.Datastore: The class "org.apache.hadoop.hive.metastore.model.MResourceUri" is tagged as "embedded-only" so does not have its own datastore table.
java.lang.RuntimeException: java.io.IOException: Filesystem closed
    at org.apache.hadoop.hive.ql.session.SessionState.start(SessionState.java:522)
(中略)
<console>:16: error: not found: value sqlContext
         import sqlContext.implicits._
                ^
<console>:16: error: not found: value sqlContext
         import sqlContext.sql
                ^

Spark クラスタでの Word2Vec の実行

最終的にやったこと

spark-ec2 の login コマンドを使用すると、master にログインできます。もちろん ssh でもログインできますが、master のホスト名を書かなくてよいのがメリットだと思います。ちなみに、オプションの指定が面倒ですが、以下の3つは必須のようです。

% ./spark-ec2 \
--key-pair=word2vec-key-pair \
--identity-file=/Users/myoshiz/.ssh/word2vec-key-pair.pem \
--region=us-west-1 \
login spark-cluster

次に、先ほどと同じサンプルコードを実行するために、text8.zip をダウンロードします。また、このファイルを、クラスタ上で動作する HDFS にアップロードします。これは、ファイルを slave からアクセス可能にするための作業です。後ほど、HDFS の代わりに S3 を使う方法も紹介します。

$ wget http://mattmahoney.net/dc/text8.zip
$ unzip text8.zip
$ ./ephemeral-hdfs/bin/hadoop fs -put text8 /

ここまでの準備が終わったら、master 上で spark-shell を実行します。指定するオプションは以下の通りです。ローカルモードの場合とは、--masterの指定が変わっています。

$ ./spark/bin/spark-shell \
--master spark://${EC2_SPARK_MASTER}:7077 \
--conf spark.driver.memory=5g \
--conf spark.serializer=org.apache.spark.serializer.KryoSerializer

ホスト名の指定が面倒ですが、--master spark://localhost:7077という指定では接続できませんでした。

あとは、spark-shell で以下のように入力すると、Word2Vec が実行されます。ローカルモードとの違いは、textFile() や save() に渡されたファイルパスが、HDFS のファイルパスとして扱われることです。今回はルート直下に text8 を置いたため、/text8のように指定しています。

import org.apache.spark._
import org.apache.spark.rdd._
import org.apache.spark.SparkContext._
import org.apache.spark.mllib.feature.{Word2Vec, Word2VecModel}

val input = sc.textFile("/text8").map(line => line.split(" ").toSeq)

val word2vec = new Word2Vec()

val model = word2vec.fit(input)

val synonyms = model.findSynonyms("china", 40)

for((synonym, cosineSimilarity) <- synonyms) {
  println(s"$synonym $cosineSimilarity")
}

// Save and load model
model.save(sc, "/model_text8")
val sameModel = Word2VecModel.load(sc, "/model_text8")

以上により、Word2Vec のジョブが slave 上で実行されます。ただ、http://ec2-xxx-xxx-xxx-xxx.us-west-1.compute.amazonaws.com:8080にアクセスするとわかるのですが、このままだと3台ある slave のうち、1台しか使われません。次は、ジョブを分散するために、Word2Vec のパラメータを変更します。

つまづいたこと

最初、ローカルディスクのファイルにアクセスできないことに気づきませんでした。file://を付けても駄目でした。

次に、HDFS 上にファイルをアップロードする方法で悩んだのですが、これは ./ephemeral-hdfs/bin以下のコマンドが使えることに気付いたあとは簡単でした。hadoop コマンドに馴染みのない人は、Apache Hadoop 2.7.1 –などが参考になると思います。

すべての slave に処理が分散されることの確認(Word2Vec のパラメータ変更)

最終的にやったこと

Word2Vec のパラメータは、Word2Vec クラスの setter で指定できます。用意された setter とそのデフォルト値は Word2Vec の API リファレンスに記載されています。

これらの setter のうち、setNumPartitions() でパーティション数を1よりも大きくすると、複数の slave 間で処理が分散されます。この値のデフォルトが1なので、そのままでは slave が1台しか使われません。

val word2vec = new Word2Vec()

// Set this
word2vec.setNumPartitions(4)

val model = word2vec.fit(input)

slave 3台で試したところ、パーティション数を4まで増やした段階で、すべてのslaveに処理が分散されました。ただ、5台で試したときには、パーティション数を6にしても、slave 4台しか使われませんでした。単純に slave の台数 + 1 にすればよいというわけではなさそうで、詳細はまだわかりませんが、少なくとも slave の台数よりも大きい数を指定する必要がありそうです。

ただ、このパーティション数を増やすと、増やした分だけ負荷分散されて処理時間が短くなっていく一方で、計算結果の正確さも落ちていくとのことです。Word2Vec の処理が分散しない理由を調べている際に、以下の情報を見かけました。

stackoverflow.com

  • イテレーションの数は、パーティション数と同じか、それ以下にすべき
  • 正確さのために、パーティション数は小さい値を使うべき
  • 結果(モデル)を正確にするためには、複数のイテレーションが必要

どれくらい結果が変わっていくのか、text8 を3台の slave 上で処理して調べてみました。以下は、numPartition = 1, 3, 6 での、"china"に類似した単語の上位10件です。パーティションが1個の場合の上位3件を太字にしています。text8 は意味のない文字列ですが、結果が変わっていく様子は参考になると思います。

numPartitions 1 3 6
1位 taiwantaiwanindonesia
2位 koreakoreataiwan
3位 japanjapanafghanistan
4位 mongolia mainland kazakhstan
5位 shanghai indonesiapakistan
6位 tibet india japan
7位 republic pakistan ireland
8位 india mongolia india
9位 manchuria thailand uzbekistan
10位 thailand africa iran

ちなみに、使われた slave の台数と、処理時間の関係は以下のようになりました。text8 くらいのデータ量(100 MB)だと、おおよそ、使われる slave の台数に応じて大きく処理時間が減るようです。

numPartitions 1 3 6
slave の台数 1 2 3
処理時間 6.3 min 3.6 min 1.9 min

つまづいたこと

負荷分散しない理由が Word2Vec のパラメータの方にある、ということが最初なかなか分からずに苦労しました。

普通に考えると「Spark MLlib から Word2Vec を使いたい人=負荷分散を期待している人」だから、Word2Vec のパラメータの初期値は負荷分散するようになっているはずだ(だから Spark のパラメータの方に問題があるはずだ)と思い込んでいました……。

Part 1 のまとめ

ここまでの手順で、Amazon EC2 上に Spark クラスタを構築する方法を確認できました。

次の Part 2 では、この Spark クラスタの slave の台数およびマシンスペックを強化し、処理するデータ量も Wikipedia 英語版(Gzip 圧縮した状態で 12 GB)まで増やしてみます。

Part 1 の主な参考文献

手を動かす Spark MLlib & Word2Vec Part 2 (Wikipedia 英語版から Word2Vec モデルを作るまで)

$
0
0
f:id:muziyoshiz:20160626223709p:plain

このシリーズについて

実際に手を動かして Spark MLlib に慣れていこう、というシリーズです。

Spark を使うならそれなりに大きなデータを分散処理しないと面白くないと思い、Wikipedia のデータから Word2Vec のモデルを作るところまでやってみました。環境構築については Part 1 をご参照ください。

muziyoshiz.hatenablog.com

Part 2 の範囲

Wikipedia 英語版のデータから作成したコーパスを Amazon EC2 上の Spark Cluster で処理して、Word2Vec のモデルを作成するところまで。

Wikipedia 英語版のデータからコーパス作成

最終的にやったこと

Wikipedia:Database downloadから辿って、https://dumps.wikimedia.org/enwiki/ (HTTP) または Data dump torrents (BitTorrent) からダウンロードできます。

Wikipedia のデータは XML および SQL で公開されており、それぞれ色々ファイルがあります。今回は Wikipedia の本文からコーパスを作りたいので enwiki-latest-pages-articles.xml.bz2 をダウンロードしました。私がダウンロードした時の最新版は 2016-06-03 作成の 12.1 GB のファイルでした。

このファイルを解凍すると以下のような XML が入っています。<text> タブの中身が本文です。

<page>
    <title>Anarchism</title>
    <ns>0</ns>
    <id>12</id>
    <revision>
      <id>721573764</id>
      <parentid>719202660</parentid>
      <timestamp>2016-05-22T19:25:12Z</timestamp>
      <contributor>
        <username>PBS-AWB</username>
        <id>11989454</id>
      </contributor>
      <comment>modification to template Cite SEP and possibly some gen fixes using [[Project:AWB|AWB]]</comment>
      <model>wikitext</model>
      <format>text/x-wiki</format>
      <text xml:space="preserve">{{Redirect2|Anarchist|Anarchists|the fictional character|Anarchist (comics)|other uses|Anarchists (disambiguation)}}
{{pp-move-indef}}
{{Use British English|date=January 2014}}
{{Anarchism sidebar}}
'''Anarchism''' is a [[political philosophy]] that advocates [[self-governance|self-governed]] societies based on voluntary institutions.
(中略)
[[Category:Far-left politics]]</text>
      <sha1>sfoc30irh5k1bj62ubt29wp1ygxark0</sha1>
    </revision>
  </page>

この XML から本文だけ取り出す方法を色々探してみたところ、wp2txtというツールがあったので、今回はこちらを使わせてもらいました。

github.com

wp2txt は Ruby で書かれており、関連する gem のインストールに苦労したという記事も見かけたのですが、以下の手順で問題なく動作しました。

% mkdir wp2txt
% cd wp2txt
% bundle init
% echo 'gem "wp2txt"' >> Gemfile
% bundle install
% mkdir output_dir
% bundle exec wp2txt -i enwiki-latest-pages-articles.xml.bz2 -o enwiki --no-heading --no-title --no-marker

今回は本文だけが必要なので、余分な出力をなるべく減らすためのオプション(--no-heading, --no-title, --no-marker)を付けました。それでも、MediaWiki のマークアップなどは残ってしまうのですが、それは Spark で除去することにします。

wp2txt の処理が終わると、enwiki ディレクトリに enwiki-latest-pages-articles.xml-<連番>.txtというファイルが作成されます。今回は合計 1,754 ファイル、18 GB でした。ちなみに、私の環境(MacBook Pro 2.2 GHz, 16GB DDR3)では、実行に19時間くらいかかりました。

つまづいたこと

Wikipedia のデータからコーパスを作る方法を色々探したのですが、英語圏も含めて、簡単にできる方法がなかなか見つかりませんでした。

wp2txt 以外で良さそうなものとしては wiki2vecというツールが、コーパスを作る機能を持っているようでした。ただ、時間の都合で今回は試せませんでした。ちなみに、この記事を書くために読み返していて気付きましたが、wiki2vec のパラメータとして minCount = 50, vectorSize = 500, windowSize = 10 という例が載っていました。次はこれで試してみるのもよいかもしれません。

github.com

その後、Word2Vec 関係の記事を探しまわるなかで、以下の記事から wp2txt の存在に気づき、今回は wp2txt を使うことにしました。

techblog.gmo-ap.jp

コーパスの、S3 へのアップロード

最終的にやったこと

Apache EC2 上に構築した Spark Cluster に Wikipedia のファイルを渡さなければいけないのですが、データサイズが大きいので、今回は S3 経由で渡しました。

Spark の textFile メソッドは gzip 圧縮されたファイルも読み込めるので、まずは先程のファイルを圧縮します。圧縮後のファイルは 6 GB になりました。

% gzip enwiki/*.txt

そして、これを S3 にアップロードします。aws s3 cp コマンドはワイルドカードが使えないので、一括アップロード時には、ディレクトリ名を指定して --recursiveを指定する必要があります。

% aws s3 cp enwiki s3://my-bucket-name/ --recursive

aws コマンドがない場合は、Installing the AWS Command Line Interface - AWS Command Line Interfaceに従ってインストールしてください。Mac の場合は pip でインストールします。

つまづいたこと

最初は、以下のように1ファイルにまとめてアップロードしたのですが、これだとファイル読み込みが全く分散されませんでした。

% cat enwiki/*.txt > enwiki.txt
% gzip enwiki.txt
% aws s3 cp enwiki.txt.gz s3://my-bucket-name/

今回は wp2txt がファイルを複数に分割してくれていましたが、他の方法でコーパスを作った場合も、ファイルを分けてアップロードしたほうがいいですね。

spark-submit で使う jar の作成

最終的にやったこと

いままでは spark-shell で Word2Vec を実行していましたが、データ量が増えると実行時間が長くなって、EC2 インスタンスへの接続が切れてしまう可能性が出てきます。そのため、Word2Vec を実行する簡単な jar を作って、spark-submit で実行することにします。

以下がそのコードです。

Simple Word2Vec application

Feature Extraction and Transformationにあるサンプルに、以下の修正を加えています。

  • コマンドライン引数から、読み込むファイル、モデルの出力先、Word2Vecのパラメータを設定
  • repartition メソッドを使って、RDD を分割(これをしないと読み込み後の処理が分散されない)
  • split する際の区切り文字に、スペース以外の文字も含めることで、MediaWiki 記法の文字を除去
  • filter で長さが 1 の文字を除去
  • 処理の最後で、実行時間を出力

jar をビルドしたい場合は、以下のリポジトリを使ってください。

github.com

sbt assemblyを実行すると target/scala-2.11ディレクトリに word2vec-model-generator-assembly-0.0.1.jarができます。この jar ファイルだけ master ノードに持っていけば実行できます。

つまづいたこと

最初は repartition メソッドを使わなかったのですが、その場合、ファイルの読み込みは分散しても、その後の処理が途中まで全く分散されませんでした。repartition メソッドに渡す引数は scala - Spark: Repartition strategy after reading text file - Stack Overflowを参考にしました。

MediaWiki記法を除去するルールは、ローカルマシンで enwiki-latest-pages-articles.xml-0001.txtを処理して手探りで決めたのですが、もっと良い方法がありそうです。例えば、先ほど紹介した wiki2vecは、うまく処理していそうです("Word2Vec Corpus"の節を参照)。

Amazon EC2 への Spark クラスタの構築(5台構成)

最終的にやったこと

Part 1 でもクラスタを構築しましたが、それは1回削除して、Slave の台数とスペックを増やしたクラスタを作り直しました。ちなみに、spark-ec2 destory spark-clusterでクラスタを削除できます。

大規模データで Spark MLlib を試すのは初めてなので、手間取っている間にマシンが無駄に動いている……という可能性があったので(というか実際そうなったので)、少しケチって以下のスペックで構築しました。

  • master: r3.large ($0.185/hour, 2 vCPU, 15 GB memory, 1 x 32 SSD) 1台
  • slave: m3.2xlarge ($0.616/hour, 8 vCPU, 30 GB memory, 2 x 80 SSD) 5台

構築時のコマンドは以下の通りです。

./spark-ec2 \
--key-pair=word2vec-key-pair \
--identity-file=/Users/myoshiz/.ssh/word2vec-key-pair.pem \
--region=us-west-1 \
--zone=us-west-1a \
--master-instance-type=r3.large \
--instance-type=m3.2xlarge \
--copy-aws-credentials \
--hadoop-major-version=yarn \
--slaves 5 \
launch spark-cluster > spark-submit.log 2>&1 &

構築が完了したら、前回同様に .bash_profile に環境変数 EC2_SPARK_MASTER を設定します。また、今回は環境変数 AWS_ACCESS_KEY_ID と AWS_SECRET_ACCESS_KEY も設定します。これは、Spark アプリケーションから S3 にアクセスするために必要な設定です。

export EC2_SPARK_MASTER=ec2-xxx-xxx-xxx-xxx.us-west-1.compute.amazonaws.com
export AWS_ACCESS_KEY_ID={{ IAM ユーザ "word2vec-user" のアクセスキーID }}
export AWS_SECRET_ACCESS_KEY={{ IAM ユーザ "word2vec-user" のシークレットアクセスキー}}

つまづいたこと

Part 1 でも書きましたが、--copy-aws-credential を指定しても、Spark に対してはアクセスキーID、シークレットアクセスキーが設定されません。環境変数を設定したところ、Spark(spark-shell, spark-submit)から S3 にアクセスできるようになったので、今回はこの方法で済ませました。

なお、AWS のインスタンスプロファイルを使ってアクセス権限を与えることもできると思いますが、今回は試していません。クラスタを作ったり壊したりを繰り返していたため、そのたびに AWS マネジメントコンソールをいじるのは面倒だったので……。

spark-submit の実行

最終的にやったこと

先ほど作った jar を、master にコピーします。

% scp -i ~/.ssh/word2vec-key-pair.pem word2vec-model-generator-assembly-0.0.1.jar root@${EC2_SPARK_MASTER}:/root/

そして、以下のようにコマンドを実行すると、Word2Vec アプリケーションが実行されます。ssh 接続が切れた場合のために、バックグラウンドで実行し、標準出力はファイルに出力させておきます。

$ ./spark/bin/spark-submit \
--master spark://${EC2_SPARK_MASTER}:7077 \
--conf spark.serializer=org.apache.spark.serializer.KryoSerializer \
--conf spark.driver.memory=11g \
--conf spark.akka.frameSize=1024 \
--class jp.muziyoshiz.word2vec.Word2VecModelGenerator \
word2vec-model-generator-assembly-0.0.1.jar \
s3n://my-bucket-name/enwiki/*.txt.gz \
s3n://my-bucket-name/enwiki_model \
10 20 50 > spark-submit-wp8.log 2>&1 &

Spark から S3 にアクセスする際は、aws コマンドで指定するときの s3://ではなくて、s3n://を指定する必要がある点に注意です。

Word2Vec のパラメータを変えて、何度か実行してみたところ、実行時間は以下のようになりました。

Pattern No. numPartition numIteration minCount vectorSize 実行時間
1 10 1 5 10 75.0 min
2 10 1 20 20 82.6 min
3 10 1 20 50 130.6 min

最初のパターン1は、vectorSize をかなり減らしたにも関わらず時間がかかり、後述するようにモデルの精度もよくありませんでした。

そのため、パターン2では minCount(単語の最小出現回数)を大きくして、vocabSize(モデルに含まれる単語数)を減らしました。その結果、vectorSize をパターン1の2倍にしたにも関わらず、実行時間は1.1倍程度に収まり、モデルの精度も若干上がりました。

最後に、他のパラメータは同じままで vectorSize のみ50に増やしたところ、vectorSizeはパターン2の2.5倍で、実行時間は1.5倍になりました。

実行時間の内訳を Spark UI で確認したところ、処理の最後に slave 1台で実行するタスク(Locality Level NODE_LOCAL のタスク)があり、これが1〜2時間かかっていました。このタスクがボトルネックになっているということは、少なくとも Word2Vec については、Spark MLlib による分散処理のメリットって、もしかしてあまり無いとか……?

ただ、今回は numIteration を 1 で固定にしましたが、精度を上げるためには numPartition と同じ 10 まで上げたほうがよいはずです。numIteration を増やせば、分散処理されるタスクが占める割合も増えるので、Spark の恩恵が得られるのではないかと思います。それはまた今度試してみます。

つまづいたこと(1):ドライバのメモリ使用量を増やさないと落ちる

最初に実行したところ、以下のエラーが発生してジョブが止まりました。

16/06/25 09:39:46 ERROR TransportRequestHandler: Error while invoking RpcHandler#receive() for one-way message.
java.lang.IllegalStateException: unread block data

Spark UI の Environment タブで、Spark Properties を確認したところ、spark.executor.memory は m3.2xlarge(メモリ 30GB)に合わせて 26837m に指定されていたのですが、spark.driver.memory のほうは何も指定されていませんでした。spark.driver.memory のデフォルトは 1g です。

これを r3.large のメモリ 15GB から 4GB を引いた 11GB(11g)に指定したところ、このエラーは出なくなりました。また、Environment タブで、spark.driver.memory が指定されていることを確認できました。

つまづいたこと(2):vocabSize*vectorSize が大きすぎると落ちる

numPartition numIteration minCount vectorSize
10 1 5 100

Spark MLlib の vectorSize のデフォルト値は 100 なので、最初はこの値を使っていました。しかし、上記のパラメータの組み合わせで実行したところ、Stage 1 の処理の途中で以下のエラーが出て、タスクが止まってしまいました。

Exception in thread "main" java.lang.RuntimeException: Please increase minCount or decrease vectorSize in Word2Vec to avoid an OOM. You are highly recommended to make your vocabSize*vectorSize, which is 3856720*100 for now, less than `Int.MaxValue/8`.
        at org.apache.spark.mllib.feature.Word2Vec.fit(Word2Vec.scala:319)
(スタックトレースは省略)

Int.MaxValue/8 = 268435455です。つまり、vocabSize(単語数)*vectorSize がこの上限を大幅に超えていることが原因のようです。Int の最大値の8分の1ってなんでまた……。

とにかく、単語数を減らすか、vectorSize を減らす必要があることがわかりました。そのため、これ以降のテストでは vectorSize を減らすパターン(パターン1)と、minCount を増やして単語数を減らすパターン(パターン2〜3)を試しました。

つまづいたこと(3):モデルのサイズが大きすぎると akka のフレームサイズ上限を超えて落ちる

Word2Vec.fit() の最後、生成された Word2Vec モデルを parquet 形式で出力するところで、以下のエラーが出て落ちました。

16/06/25 11:28:06 ERROR InsertIntoHadoopFsRelation: Aborting job.
org.apache.spark.SparkException: Job aborted due to stage failure: Serialized task 1793:0 was 213710572 bytes, which exceeds max allowed: spark.akka.frameSize (134217728 bytes) - reserved (204800 bytes). Consider increasing spark.akka.frameSize or using broadcast variables for large values.

spark.akka.frameSize のデフォルトは 128(単位は MB)で、タスクのサイズがこれを超えると落ちるようです。小さいデータセットでは出なかったエラーなのですが、Wikipedia 規模になると出るようです。1〜2時間待ったあとで、最後の最後にこのエラーで落ちると、非常に(精神的にも金銭的にも)痛いです……。

設定可能な上限値は調べても分かりませんでしたが、ひとまず --conf spark.akka.frameSize=1024を指定して 1GB にしたところ、Word2Vec モデルの出力まで成功しました。

ローカルマシン上での Word2Vec モデルの利用

最終的にやったこと

先ほどの spark-submit の実行により、Word2Vec モデルが S3 にアップロードされました。このモデルをローカルマシンにダウンロードして使ってみます。

% aws s3 cp s3://my-bucket-name/enwiki_model ./enwiki_model --recursive

以下のコマンドで spark-shell を起動します。Part 1 で使ったオプションに加えて、--conf spark.kryoserializer.buffer.max=1gを指定しています。

% spark-shell --master local \
--conf spark.driver.memory=5g \
--conf spark.serializer=org.apache.spark.serializer.KryoSerializer \
--conf spark.kryoserializer.buffer.max=1g

そして、spark-shell のプロンプトで以下を実行し、Word2Vec モデルをロードします。

import org.apache.spark._
import org.apache.spark.rdd._
import org.apache.spark.SparkContext._
import org.apache.spark.mllib.feature.{Word2Vec, Word2VecModel}

val model = Word2VecModel.load(sc, "enwiki_model")

また、今回は Apache Spark 入門に書かれたメソッドを使って、関係性を加味した推測を行ってみます。有名な例で言うと、「"king"に対する "kings"は、"queen"に対する何か?」という関係を、Word2Vecモデルから推測することができます。

以下は、Apache Spark 入門の p.224 から抜粋したコードです。これを spark-shell に貼り付けるか、ファイルに書いておいて :load <file name>でロードして使います。

import org.apache.spark.mllib.linalg.Vectors

def relationWords(w1: String, w2: String, target: String, model: Word2VecModel) :Array[(String, Double)] = {
    val b = breeze.linalg.Vector(model.getVectors(w1))
    val a = breeze.linalg.Vector(model.getVectors(w2))
    val c = breeze.linalg.Vector(model.getVectors(target))
    val x = c + (a - b)
    model.findSynonyms(Vectors.dense(x.toArray.map(_.toDouble)), 10)
}

パターン1:minCount = 5, vectorSize = 10

ベクトル数が小さいためか、精度はかなり悪いです。Tokyoという都市名、Japanという国名に対する類似語を求めても、上位10件にそれらしいものが現れません。

scala> model.findSynonyms("Tokyo", 10).foreach(println)
(Rebounder,5.026514312082014)
(Fivepenny,5.006271473525809)
(Riviera,4.9806280562664655)
(Pirmahal,4.977896311409738)
(A2217,4.973896329049228)
(Pestújhely,4.967955406306887)
(Tri,4.966647609406325)
(Cigarros,4.966214313196464)
(Seahorses,4.9657892250050715)
(Club,4.965424934604451)

scala> model.findSynonyms("Japan", 10).foreach(println)
(Prabda,3.8591253451462766)
(Skateabout,3.789246081518729)
(detailslink,3.756286768742609)
(Oceania,3.7439580152901946)
(Daeges,3.743037606956309)
(Equestrianism,3.73990681262581)
(Miegs,3.7392088293670396)
(Fleuth,3.735308547592705)
(KBID-LP,3.730579527776324)
(Powerlifting,3.717090309581691)

関係性を加味した推測も、以下のようにうまく行きませんでした。

scala> relationWords("king", "kings", "queen", model).foreach(println)
(satsuma-biwa,4.95347264322314)
(shoguns,4.93869343414127)
(mystics,4.931215483461304)
(Zelimxan,4.925167012454619)
(Christianized,4.922235458369835)
(veneration,4.921893688910249)
(Shi’i,4.921205040607001)
(Russified,4.917586471812209)
(pagan,4.912822109308089)
(revered,4.911351827558269)

scala> relationWords("prince", "king", "princess", model).foreach(println)
(Pandava,4.2101984410814834)
(Aegisthus,4.207452272387961)
(bandit,4.202362575975742)
(amanuensis,4.194580140364399)
(Aerope,4.188601884423512)
(tradesman,4.178661804898081)
(Candaules,4.177194064593601)
(princess,4.173209621638307)
(Shoulang,4.165125455530385)
(Seibei,4.163678291883964)

パターン2:minCount = 20, vectorSize = 20

単語数を減らし、ベクトル数を上げた結果、精度が若干向上しました。Tokyo に対する類義語として、日本の都市の Osaka、Sapporo が出てくるようになりました。一方で、Japan に対する類義語のほうは、あまり改善が見られません。

scala> model.findSynonyms("Tokyo", 10).foreach(println)
(Wrestle,7.689458080058069)
(Split,7.626499879518354)
(Osaka,7.620597049534027)
(Sapporo,7.556529623946273)
(Setagaya,7.513748270603075)
(Hiroshima,7.490792005499523)
(Shinjuku,7.45951304352636)
(Kanazawa,7.459122453399323)
(Expo,7.453010168798164)
(ESCOM,7.447874763780933)

scala> model.findSynonyms("Japan", 10).foreach(println)
(Tokyo,5.679376270328159)
(AXN,5.640570343734289)
(Wrestle,5.60396135079362)
(Expo,5.590382781259281)
(TV2,5.522196857434101)
(Hanoi,5.495135749493573)
(TV6,5.490184062697079)
(Kyoto,5.486577183328772)
(Skate,5.4760554670281065)
(Benelux,5.430530293625971)

関係性を加味した推測は、まだあまりうまくいきません。ただ、後者のほうは5位に正解の "queen"が出ているので、若干精度が向上しています。

scala> relationWords("king", "kings", "queen", model).foreach(println)
(pagan,6.667731329068959)
(garb,6.659426546093454)
(gods,6.648366573398432)
(symbolised,6.648168276539841)
(sacred,6.6085783714277975)
(personages,6.598811565877372)
(veneration,6.597536687593547)
(puranas,6.590383098194837)
(deities,6.588936982768422)
(beauties,6.588806331810932)

scala> relationWords("prince", "king", "princess", model).foreach(println)
(lord,6.574825899196509)
(princess,6.522661208674787)
(bride,6.521167177599623)
(lady,6.492377997870626)
(queen,6.479450084505509)
(first-born,6.466189456944019)
(king,6.441766970616445)
(blessed,6.441764119985444)
(beloved,6.4396910737789606)
(bridegroom,6.423838321417851)

パターン3:minCount = 20, vectorSize = 50

ベクトル数を大幅に増やした結果、かなり精度が向上しました。Tokyo に対して日本の都市、Japan に対してアジアの国名や首都が表示されるようになりました。

scala> model.findSynonyms("Tokyo", 10).foreach(println)
(Osaka,6.442472711716078)
(Fukuoka,6.3918200759436)
(Saitama,6.343209033208874)
(Setagaya,6.237343626467007)
(Japan,6.063812875793321)
(Sapporo,6.027676167552773)
(Nagano,5.955215285602899)
(Kobe,5.891646194480255)
(Yamagata,5.86912171881318)
(Shibuya,5.835765966270005)

scala> model.findSynonyms("Japan", 10).foreach(println)
(Tokyo,5.510337298616405)
(Korea,5.509610108188756)
(China,5.486622516556292)
(Fukuoka,5.378651363703807)
(Taiwan,5.377869828524535)
(Seoul,5.321357314331263)
(Shizuoka,5.31678565272272)
(Prefecture,5.297746506109964)
(Hamamatsu,5.159312705112953)
(Kanagawa,5.157422752148916)

関係性を加味した推測も、かなり精度が向上しました。前者は正解の "queens"が2位、後者も正解の "queen"が2位に表示されています。イテレーション数(numIteration)を増やすなど、更にパラメータを調整すれば、これらの単語が1位に上がることが期待できます。

scala> relationWords("king", "kings", "queen", model).foreach(println)
(realms,6.380416904839411)
(queens,6.292521776188793)
(knightly,6.2558567330155626)
(consorts,6.241017073100756)
(kings,6.200374546691251)
(kindreds,6.17249501613232)
(lamas,6.1721177720161915)
(monuments,6.147651372785442)
(patrilineal,6.1288029631730545)
(depictions,6.121416883901753)

scala> relationWords("prince", "king", "princess", model).foreach(println)
(princess,5.956775488416378)
(queen,5.9055082324742685)
(slaying,5.793197818446893)
(king’s,5.696965618712307)
(betrothal,5.59067630474941)
(goddess,5.58159904439838)
(apparition,5.554027664552106)
(martyrdom,5.534826668619817)
(Pelops,5.503355785910461)
(ancestress,5.4953139994512545)

つまづいたこと

spark-shell の起動時に --conf spark.kryoserializer.buffer.max=1gを指定しないと、Word2VecModel.load() の呼び出しで落ちました。

16/06/25 22:00:26 WARN TaskSetManager: Lost task 1.0 in stage 2.0 (TID 3, localhost): TaskKilled (killed intentionally)
org.apache.spark.SparkException: Job aborted due to stage failure: Task 0 in stage 2.0 failed 1 times, most recent failure: Lost task 0.0 in stage 2.0 (TID 2, localhost): org.apache.spark.SparkException: Kryo serialization failed: Buffer overflow. Available: 1, required: 4. To avoid this, increase spark.kryoserializer.buffer.max value.
    at org.apache.spark.serializer.KryoSerializerInstance.serialize(KryoSerializer.scala:299)
(スタックトレースは省略)

あと、余談ですが、zsh を使っていると --master local[*]の指定ができないようです。以下のようなエラーが出ます。bash なら指定できるので、spark-shell の実行時だけ bash に切り替えました。

% spark-shell --master local[*] \
--conf spark.driver.memory=5g \
--conf spark.serializer=org.apache.spark.serializer.KryoSerializer \
--conf spark.kryoserializer.buffer.max=1g
zsh: no matches found: local[*]

Part 2 のまとめ

Wikipedia の大規模データを、Amazon EC2 上に構築した Spark Cluster で処理できました。また、実際にいろいろなパラメータを試してみて、大規模データを処理する際に注意すべき Spark のプロパティや、Word2Vec のパラメータを把握することができました。

今回生成した Word2Vec のモデルはまだあまり精度が良くありませんが、以下の方針で精度を上げることができそうです。

  • minCount を大きくして、単語数(vocabSize)を小さくする
  • vectorSize を増やす
  • numIteration を numPartition に近づける
  • 元データからノイズ(MediaWiki記法など)を除去する方法を改善する

実際に試してみて、Word2Vec の場合は slave 1台で実行される処理がボトルネックになっていることがわかりました。Amazon EC2 の高いマシンを借りているのに、slave 1台だけが頑張っていて、残りの4台は遊んでいる、というのはなかなか焦ります。

Slave 1台しか動かない時間を短くする方法としては、vocabSize*vectorSize を小さくするしかないのですかね? 実際のアプリで Word2Vec を使う際には、Spark MLlib での処理の前に、そのアプリで使わない単語を除去してしまって単語数を大幅に減らしておく、などの対策が必要かもしれません。

Part 2 の主な参考文献


Treasure Data Tech Talk 201607 レポート(古橋さんと成瀬さんの講演メモのみ)

$
0
0
f:id:muziyoshiz:20160724153127p:plain:w320

先週末に、Treasure Data Tech Talk に参加してきました。このイベントは毎回濃い話を聞けるので、行けるときはなるべく参加するようにしています。

今回は、古橋さんによる Digdag での YAML 利用の話と、成瀬さんによる PerfectQueue の話が特に面白かったです。以下、講演内容のメモと、公開済みのスライドです。

講演内容

DigdagはなぜYAMLなのか? (Sadayuki Furuhashi, @frsyuki)

  • Digdag とは何か?

    • Workflow automation system
    • Digdag で一番やりたいのはバッチデータ解析の自動化
  • Digdag の競合

    • OSS, Proprietary それぞれに競合がある
    • Workflow automation system は、ワークフローの定義方法によって3つに分類できる
      • プログラミング言語型:Luigi など
      • GUI型:Rundeck など
      • 定義ファイル+スクリプト型:Azkaban など
    • ワークフローの作りやすさと、カスタマイズの柔軟性のトレードオフ
  • Digdag

    • Digdag は定義ファイル+スクリプト型
    • 定義ファイル+スクリプト+俺たちのYAML
    • YAMLは便利だが、include できない、変数の埋め込みができない、(言語内DSLのように)プログラムが書けない、という欠点がある
    • Digdag では、YAML の仕様に従ったうえで、これらの欠点を克服した
  • include できる

    • YAML の仕様では、値(scalar)の前に "!"から始まる文字(タグと呼ばれる)を付与できる
    • 通常、YAMLパーサは、正規表現によるマッチでタグを決定して、自動的にタグを付与している
    • Digdag では "!include : filename"という表記を、ファイルインクルードの文法として使っている
    • !include の後ろに、""を書く必要がある。このスペースが大事。このスペースのおかげで、通常の YAML パーサでも、キーが ""、値が filename のハッシュとして読み込める
    • ただし、この !include を複数書くと、キーが ""のハッシュが重複してしまう。Digdag の YAML パーサでは、複数の !include を書けるように、内部的にキーを UUID に書き換えている
  • 変数の埋め込みができる、プログラムが書ける

    • Java 8 は、Nashorn(ナスホーン)という JavaScript Engine を同梱している。これを使って ${} 内を評価している
    • だから Digdag は Java8 必須
  • Q) 何故YAMLをベースにした?

    • A) 比較的書きやすい、読みやすい。YAMLとして既存のプログラムから扱える。
  • Q) YAMLはグラフ構造を表現するのに適さないのでは?

    • A) YAMLはDAG、グラフを扱うわけではない。ツリーを扱っている。
  • Q) YAMLにコードを書けるとのことだが、悪さはできないのか?

    • A) 悪さできないように対策している。JavaScript はサンドボックス内でしか動作しない。そのためにJavaScriptを採用した。

PerfectQueueはいかにパーフェクトか、あるいはRubyとMySQLでジョブキューを作る試みについて (Yui Naruse, @nalsh)

  • Who is naruse

    • nkfメンテナ
    • Rubyコミッタ
    • Treasure DataではバックエンドのRubyを担当
  • そもそもジョブキューとは

    • FIFO
    • フロントエンドとバックエンドを疎結合化
  • PerfectQueue の特徴

    • MySQL で実装
    • At-least-once を優先(at-least-once と at-most-once はトレードオフの関係)
  • キューのデータ構造

CREATE TABLE `queue` (
    /* unique key (-> at most once) */
    id VARCHAR(255) NOT NULL,
    /* for FIFO's timeline */
    timeout INT NOT NULL,
    /* opaque data */
    data LONGBLOB NOT NULL,
    /* alive or finished */
    created_at INT,
    PRIMARY KEY (id)
)
  • タスクのライフサイクル

    • タスクの投入時に、timeout, created_at を現在時刻にする
    • タスクの取得時は、timeout が小さいものから優先的に取得し、timeout を 300 秒後に更新
    • タスクの実行中は、ハートビートとして、timeout を定期的に 300 秒後に更新
    • タスクの完了後は、created_at を NULL にして、一定時間保存するために timeout を 720 秒後に更新(タスクの重複を検出するため)
    • 前述の retention time を過ぎたら物理削除
  • タスクの取得、削除時の排他制御が大変

    • 排他処理のために、最初期は FOR UPDATE を使っていた
      • しかし、頻繁にデッドロックする
      • デッドロックを避けるには MySQL の気持ちになってクエリを書く必要がある
    • LOCK TABLES を使うと、テーブル全体をロックしてしまうので、SELECT にも影響
    • GET_LOCK が一番安全、でもクエリの書き方によってはデッドロックが発生した
      • ロックの delete クエリと acquire のクエリがバッティングしないように、タイミングの調整が必要(間隔を乱数で変える)
      • 調整しないと性能問題が起きた
      • さらに、ネットワーク遅延が発生すると影響大
        • GET_LOCK から RELEASE LOCK まで 3 RTT かかるので、影響大
    • queue テーブルに owner カラムを追加したうえで、FOR UPDATE を使うクエリに変更し、テーブルロックを不要にした
      • MySQL の気持ちになって書いたので安全
  • 結論

    • PerfectQueue はより完璧になった
  • Q) 何故 MySQL をキューに選んだのか?

    • A) 当時、Amazon RDS で PostgreSQL が使えなかった。RDBMS を選んだ理由は、Amazonで提供されている、フェイルオーバーがある、など。新しいジョブキューを開発したのは、ジョブキューにフェアスケジューリングの機能(Treasure Dataのサービスで必要)を付けたかったから。(古橋)

感想

Digdag での YAML の拡張(いや、標準の仕様に従っているので拡張と言うのは不適切か?)については、話としては面白かったんですが、そもそも YAML でプログラミングするのは辛そう、というのが第一の感想でした。Ansible もそんな感じのつらみがありますしね。AnsibleSpec みたいな、Digdag で書いたワークフローをテストするツールとかが、いずれ出てきたりするんでしょうか。

また、マークアップ言語として YAML にこだわる必要があるんだろうか、とも思ったのですが、じゃあ代替手段として何があるのか、と考えてみると、なかなか難しそうです。XML よりもマシな選択肢となると、いまは YAML なのかな……。HashiCorp の HCLのように、独自方式を作る方向もあったと思いますが、Digdag に独自方式を作るほどの要件はなかったんですかね。

Rails 5 で艦これアーケードのプレイデータ管理ツール "Admiral Stats"を開発中

$
0
0
f:id:muziyoshiz:20160828002458p:plain:w600

開発のきっかけ

このブログでゲームのことは書いたことなかったと思いますが、個人的には、長い期間かけてチマチマやるゲームが好きで、ここ数年は Ingress と艦これをやってます。

今年の4月には、艦これのアーケード版(艦これアーケード)もリリースされて、これも週1くらいのペースでゲーセンに通ってプレイしてたりします。

この艦これアーケードは筐体がネットワーク接続されており、自分のプレイデータをあとから SEGA の公式サイトで閲覧できるようになってます。このサイトで結構細かいデータまで見られるのですが、見られるデータはアクセス時の最新状態のみです。

このプレイデータを過去の分まで記録して時系列データとして可視化したら面白そう

と思いついたのと、

どうせ作るなら6月末にリリースされた Ruby on Rails 5 でも使ってみようか

ということで、この夏休みを使ってプレイデータ管理ツールを作ってみました。今回の記事は、このツール "Admiral Stats"の開発中間報告です。

2016-09-03追記

この記事の公開後、9/3にサービスリリースしました。Twitter アカウントでログインして使えます。ぜひお試しください。

www.admiral-stats.com

艦これアーケードとは?

艦これを全く知らない人向けに説明すると、艦これアーケードとは、艦娘と呼ばれるキャラのカードを集めて、選りすぐりのデッキを作成し、ステージを攻略していくアクションゲームです。ステージをクリアするたびに、ランダムで新たなカードが排出されます。

どのステージでどのカードが出やすいか、などのカード排出に関する法則性は全く公開されていないため、「自分(たち)が試したらこうだった」という情報が Wiki などで頻繁にやりとりされています。この法則性をつかむために(あるいは自分の不運をなぐさめるために)プレイデータを記録している人も多いと思います。

ちなみに、サービス開始直後は何時間も待たないとプレイできないほど人気でしたが、最近は少し待てばプレイできる程度に空いてきています。秋にゲーム内イベントがあるらしいので、それまでは空いてるんじゃないでしょうか。

Admiral Stats とは?

今回開発した Admiral Stats は、この艦これアーケードのプレイデータを可視化するサイトです。SEGA 公式のプレイヤーズサイトが対応していない、時系列での可視化に対応しています。

Ingress を知っている人なら Agent Statsの艦これアーケード版」という説明が一番分かりやすいと思います。実際、Agent Stats からの連想で Admiral Stats を作ることを思いつきましたし、名前も Agent Stats からの連想で付けました*1

f:id:muziyoshiz:20160828004335p:plain
Agent Stats の画面例

Admiral Stats の画面サンプル

サンプル 1:カードの入手履歴

Admiral Stats にまずログインすると、最近のプレイで入手したカードの一覧が表示されます。ずっとプレイしていてカードが増えてくると、「あれ、このカードって前にゲットしたっけ? 今日が初めてだっけ?」とわからなくなってくるのですが(自分はそうでした)、そういう場合を想定した機能です。

f:id:muziyoshiz:20160828003035p:plain

サンプル 2:カードの入手数・入手率のグラフ

カードの種類(ノーマル、レアなど)ごとの入手数、入手率のグラフです。Admiral Stats の内部に各カードのリリース時期のデータを登録してあるため、入手率は減少することもあります。

f:id:muziyoshiz:20160828003052p:plain

サンプル 3:レベル・経験値のグラフ

艦娘のレベル・経験値だけでなく、艦種(駆逐艦とか)や艦隊全体の累計レベル・経験値も表示できます。

f:id:muziyoshiz:20160828003101p:plain

サンプル 4:カード入手状況の一覧表示

公式サイトでも見られる情報なのですが、Admiral Stats では情報量を絞る代わりに、1ページにまとめて表示します。

f:id:muziyoshiz:20160828003114p:plain

データのアップロード方法

艦これアーケードの公式サイトは、残念ながら、プレイデータのダウンロード機能を提供していません。ただ、このサイトはとても綺麗に作られていて、プレイデータはすべて API 用の URL から JSON で取得し、Web ブラウザ側で画面を描画しています。

そのため、今回はこの JSON をそのままファイルに出力する admiral_stats_exporterというエクスポートツールを作りました。このツールが出力した JSON ファイルを Admiral Stats にアップロードすると、上記のサンプルのような画面が表示されます。

f:id:muziyoshiz:20160828002458p:plain:w600

Admiral Stats へのログイン方法

メールアドレスの管理をしたくなかったので、Twitter アカウントでログインする方法を採用しました。 Admiral Stats から SEGA のサイトに直接アクセスすることはないので、SEGA ID などの登録は必要ありません。

Admiral Stats の公開予定

実装は一通り終わりました。ローカルの仮想マシンで動かせば、自分1人で使う分には実用的に使えています。

ただ、どうせなら元ネタの Agent Stats の 「全ユーザとの比較」ページのように統計情報を表示できると、もっと面白くなるんじゃないかと思ってます。Agent Stats ではレベルや経験値の分布、プレイ傾向がわかる指標(攻撃重視か構築重視か、など)の分布が公開されています。艦これの場合、レアカードの所有率の分布とかでしょうか。

そこで他のユーザのデータもアップロードしてもらえるように、Admiral Stats を設置したサイトを公開するための準備中です。ただ、以下のような作業がまだ残っていて、公開できるのは1〜2週間先になる見込みです。

  • サーバのレンタル
  • SSL 証明書導入(Let's Encrypt)と HTTPS 対応
  • デプロイ自動化スクリプトの作成(場当たり的に開発環境を作ったので、必要な手順や設定を整理できてなくて……)
  • production 設定での動作確認
  • 最低限のテスト

もし、Admiral Stats を使ってみたい方は、admiral_stats_exporterで事前にプレイデータをエクスポートしておいてください。ただ、こちらはあくまで非公式のツールなので、リンク先の説明を理解したうえで、利用は自己責任でお願いします。

あと、このエクスポータは突貫で実装したツールなので、使いづらいのはご容赦ください……。本当は Agent Stats のように、スマホだけでエクスポートからインポートまで完結できると良いと思うんですけどね。そこまで手が回りませんでした。

Admiral Stats についての紹介はここまでで、これ以降は Rails 5 での実装に関する細かい話です。

実装の詳細

最近は PHP や Java で Web アプリを作っていたので、rails でまともにアプリを作るのは、Ruby on Rails 2 以来だったりします。そのため、Rails を使い慣れている人には当たり前の話が多いかもしれません。

開発環境

コーディングはホストOS(Mac OS X Yosemite)、実行はゲストOS(Vagrant + VirtualBox + CentOS 7.2)で行いました。IDE は、最近 IntelliJ に慣れてきたので RubyMineにしました。

  • IDE: RubyMine 2016.2.1
  • Ruby: ruby 2.3.1p112
  • Ruby on Rails: Rails 5.0.0.1

プラグイン

画面は Bootstrap のデフォルトのデザインをほぼそのまま採用し、グラフは Highcharts、表は Datatables で作りました。いずれも gem でインストールできました。便利ですね。

自分で明示的に導入したプラグインと、導入方法、参考にしたページなどは以下の通りです。

bootstrap-sass (3.3.7)

  • twbs/bootstrap-sass: Official Sass port of Bootstrap 2 and 3.
  • rails newを実行した時点で、Gemfiles に gem 'sass-rails', '~> 5.0'が入っていた。そのため、追加したのは gem 'bootstrap-sass', '~> 3.3.6'のみ。
  • application.css のファイル名を application.scss に変更し、以下の行を追加。
@import "bootstrap-sprockets";
@import "bootstrap";
  • application.scss にした時点で、元の CSS ファイルにあった *= require_tree .の文法は使えなくなる。そのため、rails generate controller <controller_name>で自動生成される <controller_name>.scss は、自動的には読み込まれない。もし読み込みたければ、各ファイルを明示的に @import で指定するか、css - Proper SCSS Asset Structure in Rails - Stack Overflowの回答(日本語訳)にあるような手段を使う必要がある。
  • Sass 自体については、後述する書籍と、Sass + Railsの基礎 - Qiitaを主に参考にした。
  • Bootstrap の使い方については、公式サイトの Getting Startedの Examples と、Componentsおよび CSSを参考にした。

jquery-datatables-rails (3.4.0)

  • jquery-datatables-railsの "Twitter Bootstrap 3 Installation"の手順に従ってインストール。ただし、Sass 版の Bootstrap をインストールしたので、application.scss には以下のように記載する。
@import "dataTables/bootstrap/3/jquery.dataTables.bootstrap";

highcharts-rails (4.2.5)

//= require highcharts
//= require highcharts/highcharts-more

// チャート画像のダウンロード機能
//= require highcharts/modules/exporting
//= require highcharts/modules/offline-exporting

omniauth (1.3.1), omniauth-twitter (1.2.1)

google-analytics-rails (1.1.0)

Ruby on Rails 5 を使ってみた感想

Rails 2 時代の知識のアップデートするために、まずは本屋で Rails 4 の本をいくつか流し読みしてから、そのうちの1冊を買ってきて読みました。これは内容が網羅的で、かつ読みやすい良書でした。

Ruby on Rails 4 アプリケーションプログラミング

Ruby on Rails 4 アプリケーションプログラミング

また、Rails 5 に関するページをいくつか流し読みしました。主に参考にしたページはこのあたりです。

今回の開発の範囲では、基本的な機能しか使わなかったせいか、Rails 5 だからという理由でつまづくことは特にありませんでした。本当に何もなくて、拍子抜けしたくらいです。

Rails 5 からデフォルトの開発用 Web サーバが Webrick から Puma に変わったとのことですが、特に意識せずに使えました。また、プラグインも、Rails 5 だから動かない、というものはありませんでした。

強いて言えば、いままでは rake db:migrateのように rake で実行していたコマンドが、rails db:migrateで実行できるようになったので、新しいやり方に慣れるためになるべく rails の方を使っていました。まあ、rake の方も使えるので、無理に rails を使う必要はなさそうですけど。

今後、Admiral Stats に機能を追加する機会があれば、API mode など、Rails 5 の新機能をうまく入れ込んでみたいと思います。

*1:Ingress ではプレイヤーのことを Agent と呼び、艦これでは提督(Admiral)と呼ぶため。

艦これアーケード第3回イベント「索敵機、発艦始め!」プレイデータ解析

$
0
0
f:id:muziyoshiz:20170128164643p:plain

はじめに

1年半くらい前から、艦これアーケードのプレイデータ管理ツール "Admiral Stats"を開発・運用しています。自分のプレイデータを記録するためにツールが欲しかったのと、他の人のプレイデータを見てみたかったというのが理由です。

そんなわけで今回の記事は、2017年11月30日から2018年1月9日まで開催されていた第3回イベント「索敵機、発艦始め!」のプレイデータ解析です。

最初に断っておくと、このプレイデータは、Admiral Stats を使ってくれているプレイヤーのものに限られています。そのため、プレイヤーの母集団と比べると、ヘビープレイヤーに寄った結果になっている可能性が高いです。それを念頭に置いて、大まかな傾向だけ参考にしてください。

第3回イベント「索敵機、発艦始め!」について

解析結果について話す前に、今回のイベントの簡単な紹介から。

今回は、第2回イベントと同様に「前段作戦」「後段作戦」に分かれたイベントでした。以下のように、前段作戦でクリアした難易度のみ、後段作戦でも出撃できます。詳しくは 第2回イベントのプレイデータ解析に書きました。

f:id:muziyoshiz:20180223215553p:plain

今回のイベント期間は1ヶ月以上あり、しかも正月を挟んでいたので、ネット上でも期間の長さに関する不満は少なかったようです。第2回イベントは前段作戦36日、後段作戦21日で、期間が短いとの不満があったので、その点はかなり改善されていました。

  • 前段作戦:2017年11月30日 7:00 〜 2018年1月9日 23:59 (41日)
  • 後段作戦:2017年12月14日 7:00 〜 2018年1月9日 23:59 (27日)

また、今回はクリスマス前後に1週間、期間限定クリスマスフレーム艦娘カードが排出されるキャンペーン期間があり、イベントを早めに終わらせてしまった人もうまく集客していました。SEGA 恐るべし……。

  • クリスマスフレーム排出:2017年12月20日 7:00 〜 12月27日 6:59 (7日)

難易度については、レア艦娘がいなくてもできる攻略法(時間ギリギリまで空母で削ってから戦闘開始)が発見されたおかげで、時間さえかければ甲E-6クリアも簡単でした(※個人の感想です)。それ以外の正攻法でのクリアは、練度とプレイヤースキルがないと難しそうというか……僕はチャレンジしましたが無理でした。

また、今回は前段の報酬(天津風、Prinz Eugen)と後段の報酬(大鳳、Bismarck)がはっきり分かれていました。前回はこれが分かれてなくて、大和がいつまでも出ずにハマる人が多かったので、おそらくはその救済策ですね。

全体的に、第2回よりも改善されたイベントだったと思います。個人的には楽しかったですよ。

集計対象のプレイデータ

今回は前段作戦145名、後段作戦136名分のプレイデータが集まりました。第2回イベントより約50名プレイヤーが増えています。いつも Admiral Stats のご利用ありがとうございます。

集計対象としたプレイデータの詳しい情報は、以下の通りです。

  • 集計対象とした提督
    • 第3回イベントに参加して、Admiral Stats に1回以上プレイデータをアップロードした提督
  • 集計対象としたプレイデータ(イベント進捗情報、艦娘一覧など)
    • イベント攻略率と艦娘カード入手率は、2018年1月16日までにエクスポートされたプレイデータを集計
    • レベルや経験値については、各プレイヤーの、イベント終了時刻に最もプレイデータを集計

集計対象を1月16日までとしたのは、SEGA の公式サイト(提督情報ページ)でプレイデータを閲覧できたのがこの日までだったからです。イベント終了1週間後に消える、というのは第2回イベント時と一緒でした。

以下、プレイデータの解析結果です。

プレイデータの解析結果

イベント攻略率

第2回イベントと比べると、甲難易度の攻略率は大幅に上がりました。これは、開催期間が長かったことと、手軽な攻略法が存在したことが理由と思われます。

イベント 「甲」攻略率(前段) 「甲」攻略率(後段)
第2回 57.9 % 57.5 %
第3回 72.4 % 72.1 %

イベント攻略率:前段作戦の詳細

前段作戦の甲難易度をクリアしたのは、前段作戦に出撃した提督の 72.4 % でした。

また、甲難易度をクリアした提督のうち、63.8 % 以上がレアカード(甲種勲章がプリントされた艦娘カード)を求めて2周以上クリアしていました。100周超えている提督が複数居るのは、もう凄いとしか言いようがないですね……。

f:id:muziyoshiz:20180223211848p:plain
f:id:muziyoshiz:20180223213957p:plain:w675

詳細:イベント攻略率(索敵機、発艦始め! 前段作戦) - Admiral Stats

イベント攻略率:後段作戦の詳細

後段作戦の甲難易度をクリアしたのは、後段作戦に出撃した提督の 72.1 % でした。

また、甲難易度をクリアした提督の 63.3 % 以上が2周以上クリアしていました。難易度が高い分、前段作戦よりも周回数は減っていますが、それでも50周近く回ってる提督がいるのはすごいです。

f:id:muziyoshiz:20180223211947p:plain
f:id:muziyoshiz:20180223214024p:plain:w675

詳細:イベント攻略率(索敵機、発艦始め! 後段作戦) - Admiral Stats

艦娘カード入手率

以下は、このイベントでの新艦娘を入手できた提督の割合です。ちなみに、自分でドロップしたカードだけでなく、買ったり借りたりして読み込んだカードも「入手」に含みます。これは、公式プレイヤーズサイトの仕様による制限です。

作戦 図鑑 No. 艦名 N Nホロ N中破
前段 176 Prinz Eugen 84.1 % 20.3 % 7.2 %
前段 181 天津風 87.0 % 22.5 % 10.1 %
後段 153 大鳳 73.9 % 10.9 % 2.9 %
後段 171 Bismarck 73.9 % 10.9 % 2.9 %

この結果を見る限り、艦娘ごとのドロップ率に大きな偏りはなさそうです。後段作戦のほうが大鳳・Bismarck ともに入手率が低いのは、前段よりも周回するのが難しくて、片方の艦娘しか手に入らなかった提督が多かったということかもしれません。

それと、今回のイベントでは、前段・後段ともに夕立改二の限定カードがドロップしました。入手率は意外と高く、半分以上の提督の手に渡ったようです(でもうちにはいません……)。

作戦 図鑑 No. 艦名 入手率
前段/後段 144 夕立改二 50.7 %

さらに、今回のイベントでは、第2回イベントでドロップした以下の限定カードも再度ドロップしました。11/30 3:00時点のアクティブ提督(過去60日)の入手率と比較すると、イベント後には入手率が15〜20%程度上がったようです。

作戦 図鑑 No. 艦名 11/30時点の入手率 イベント終了時の入手率
前段 103 日向改 41.2 % 61.6 %
後段 102 伊勢改 42.0 % 57.2 %

艦隊司令部レベル、および経験値

ここから先は、Admiral Stats 上で表示していない(自動集計機能がまだない)集計結果の紹介です。

まず、前回と同様に、ステージのクリア回数とステージの難易度によって増える「艦隊司令部レベル」の分布を出してみました。

艦隊司令部レベル 提督数 割合 (参考)第2回終了時の割合
0 以上 10 未満 0 0.00% 3.16%
10 以上 20 未満 2 1.38% 1.05%
20 以上 30 未満 1 0.69% 2.11%
30 以上 40 未満 3 2.07% 5.26%
40 以上 50 未満 5 3.45% 5.26%
50 以上 60 未満 3 2.07% 10.53%
60 以上 70 未満 12 8.28% 7.37%
70 以上 80 未満 9 6.21% 7.37%
80 以上 90 未満 7 4.83% 8.42%
90 以上 100 未満 38 26.21% 29.47%
100 以上 110 未満 56 38.62% 18.95%
110 (上限) 9 6.21% 1.05%
総計 145 100.00% 100.00%

艦隊司令部レベルの平均値と中央値は以下の通りです。

艦隊司令部レベル 第3回終了時 第2回終了時
平均 90.1 77.0
中央値 99.0 87.0

艦隊司令部レベルを経験値に換算すると、次のようになりました。レベル99に必要な経験値は 1,000,000 なので、イベント参加提督の 51.72 % がレベル99以上ということになります。このプレイデータは、間違いなくヘビープレイヤーに寄ってますね……。

経験値 提督数 割合 (参考) 第2回終了時
0 以上 100000 未満 9 6.21% 14.74%
100000 以上 200000 未満 8 5.52% 14.74%
200000 以上 300000 未満 15 10.34% 8.42%
300000 以上 400000 未満 6 4.14% 6.32%
400000 以上 500000 未満 3 2.07% 6.32%
500000 以上 600000 未満 4 2.76% 3.16%
600000 以上 700000 未満 5 3.45% 6.32%
700000 以上 800000 未満 9 6.21% 3.16%
800000 以上 900000 未満 11 7.59% 3.16%
900000 以上 1000000 未満 0 0.00% 0.00%
1000000 以上 2000000 未満 30 20.69% 24.21%
2000000 以上 3000000 未満 12 8.28% 5.26%
3000000 以上 4000000 未満 11 7.59% 2.11%
4000000 以上 5000000 未満 8 5.52% 0.00%
5000000 以上 6000000 未満 14 9.66% 2.11%
総計 145 100.00% 100.00%
経験値 第3回終了時 第2回終了時
平均 1712572.4 808893.7
中央値 1000000.0 491500.0

イベントの間に6ヶ月空いたので、全体的にかなり底上げされてます。新規プレイヤーがあまり入ってこなかったとも解釈できますが、ただ、それは Admiral Stats のユーザに限った話かもしれません。

攻略度と艦隊司令部レベルの比較

甲難易度をクリアするのに必要なレベルを知るために、攻略度と、艦隊司令部レベルの関係を調べた結果が以下の表です。

前段 - 後段 攻略度 提督数 提督数 (割合) 最大 最小 平均 中央値 標準偏差
甲 - 甲 98 67.59% 110 62.0 99.1 101.0 10.7
甲 - 乙 2 1.38% 99 64.0 81.5 81.5 17.5
甲 - 丙 1 0.69% 72 72.0 72.0 72.0 -
甲 - 未出撃 4 2.76% 107 95.0 101.0 101.0 4.3
乙 - 乙 8 5.52% 100 41.0 82.1 95.5 20.8
乙 - 丙 3 2.07% 87 34.0 57.3 51.0 22.1
乙 - 未攻略 2 1.38% 97 63.0 80.0 80.0 17.0
乙 - 未出撃 1 0.69% 80 80.0 80.0 80.0 -
丙 - 丙 12 8.28% 99 37.0 63.7 64.5 18.7
丙 - 未攻略 4 2.76% 74 64.0 68.8 68.5 4.0
丙 - 未出撃 3 2.07% 102 54.0 85.3 100.0 22.2
未攻略 - 未攻略 6 4.14% 99 17.0 40.8 32.0 28.2
未攻略 - 未出撃 1 0.69% 98 98.0 98.0 98.0 -
全体 145 100.00% 110 17.0 90.1 99.0 21.3

「未出撃」とあるのは、後段作戦に出撃しなかったか、後段作戦の期間中に Admiral Stats にプレイデータをアップロードしなかったユーザです。

後段作戦の甲E-6をクリアした提督と、それ以外の提督で、レベル差があることがわかります。しかし、乙E-6までしかクリアしていない提督(表の「乙 - 乙」の行)も中央値はそれほど低くありません。このあたりの提督が、どういう理由で甲をクリアしなかったのかは気になるところです。

攻略度と艦娘レベルの比較

攻略に艦娘のレベルの高さは必須だったのかどうかを調べるために、各提督の、レベルが一定値(50, 70, 90, 99)以上の艦娘数を調べてみました。 すると、レベル90あたりが、関係がありそうな結果になりました。

以下の表は、各提督の攻略度と、Lv90以上の艦娘数の関係です。

前段 - 後段 攻略度 提督数 提督数 (割合) 最大 最小 平均 中央値 標準偏差
甲 - 甲 98 67.59% 119 0 17.5 9 21.0
甲 - 乙 2 1.38% 5 0 2.5 2.5 2.5
甲 - 丙 1 0.69% 0 0 0.0 0 -
甲 - 未出撃 4 2.76% 35 0 13.3 9 14.0
乙 - 乙 8 5.52% 8 0 1.6 1 2.5
乙 - 丙 3 2.07% 0 0 0.0 0 0.0
乙 - 未攻略 2 1.38% 3 0 1.5 1.5 1.5
乙 - 未出撃 1 0.69% 1 1 1.0 1 -
丙 - 丙 12 8.28% 5 0 0.4 0 1.4
丙 - 未攻略 4 2.76% 1 0 0.3 0 0.4
丙 - 未出撃 3 2.07% 13 0 8.0 11 5.7
未攻略 - 未攻略 6 4.14% 0 0 0.0 0 0.0
未攻略 - 未出撃 1 0.69% 0 0 0.0 0 -
全体 145 100.00% 119 0 12.5 3 19.0

甲難易度をクリアした艦隊とそうでない艦隊では、Lv90以上の艦娘数に明らかな違いがあることがわかります。

攻略度と空母の練度の比較

今回のイベントは、空母を育てていれば使える攻略法がありました。そこで、攻略度と、各提督が所持する空母のレベルに関係があったかを調べてみました。

以下は、攻略度と、各提督が所持する空母(正規空母、装甲空母、軽空母)の上位5隻の平均レベルを比較した結果です。

前段 - 後段 攻略度 提督数 提督数 (割合) 最大 最小 平均 中央値 標準偏差
甲 - 甲 98 67.59% 99 18.8 74.1 75.4 20.2
甲 - 乙 2 1.38% 40.4 29.2 34.8 34.8 5.6
甲 - 丙 1 0.69% 47.6 47.6 47.6 47.6 -
甲 - 未出撃 4 2.76% 93.2 39.4 74.2 82.1 20.9
乙 - 乙 8 5.52% 70.6 26.2 52.2 56.4 15.4
乙 - 丙 3 2.07% 37.6 19.6 29.7 31.8 7.5
乙 - 未攻略 2 1.38% 60.2 44.8 52.5 52.5 7.7
乙 - 未出撃 1 0.69% 63.6 63.6 63.6 63.6 -
丙 - 丙 12 8.28% 77.6 16 40.0 37.5 17.2
丙 - 未攻略 4 2.76% 49.8 35.4 43.0 43.3 5.1
丙 - 未出撃 3 2.07% 90.2 28.6 62.9 70 25.6
未攻略 - 未攻略 6 4.14% 64 10.8 25.0 16.9 18.4
未攻略 - 未出撃 1 0.69% 72.4 72.4 72.4 72.4 -
全体 145 100.00% 99 10.8 64.9 66.4 24.5

これを見ると、空母の平均レベルが高いほど、甲難易度を攻略できた可能性は高そうですね。ただ、平均レベルが18.8でも甲E-6をクリアできている提督がいるのはちょっと驚きです。空母のレベルが低くても行けたのか、他の方法で攻略したのか……?

まとめ

いくつかの方法で、甲種勲章を入手した提督と、そうでない提督のプレイデータを比較してみました。

今回は甲難易度をクリアできた提督が全体の70%以上と多かったため、「甲提督 vs それ以外」という比較はあまりうまく行きませんでした。「レベルがある程度高ければクリアできた」という感じでしたね。第2回イベントのように「大井改二・北上改二がいれば」「甲標的があれば」といったレベル以外の条件による影響は見つけられませんでした。

もし、「こういう観点で調べてほしい」といったアイディアがありましたら Admiral Stats のお知らせ用アカウント @admiral_statsまでお寄せください。今後のネタにさせていただきます。

最後に、艦これアーケードをプレイしていて、こういうプレイデータに興味があったら是非 Admiral Statsを使ってみてください。よろしくお願いします。

おまけ:2/16 のケッコンカッコカリ実装時に、レベル99到達済みの艦娘数を集計した結果

あわせて読みたい

muziyoshiz.hatenablog.com

muziyoshiz.hatenablog.com

muziyoshiz.hatenablog.com

muziyoshiz.hatenablog.com

Playbook 側から Ansible のバージョンを指定する方法(あるいは Ansible 2.x.0 を絶対使わせない方法)

$
0
0
f:id:muziyoshiz:20160331232512p:plain:w300

もうすぐ Ansible 2.5 がリリースされますね。僕もそろそろ Ansible 2.5 Porting Guideとか読み始めました。

ところで、僕は Ansible のバージョンが上がった最初のリリースでつまづくことが多くて、どうしてもしばらく様子見してしまいます。例えば、Ansible 2.4.0 では aws_s3 モジュールに不具合があって、既存の playbook が動かなくなったりしました(下記)。

github.com

しかし、いくら自分が注意していても、そういう経験の無い人が気軽に Ansible のバージョンを上げてしまい、あとから「動かなくなったんだけど」と言われることもあります。そんなわけで、playbook 側から実行環境の Ansible のバージョンを指定できないか?と考えてみたら、うまく動いたので紹介します。

対象読者

  • Ansible 2.x.0 を基本的に信用していない
  • 同じ Ansible playbook を操作・編集する人が自分以外にもいる
  • ある程度動作確認が終わってから、Ansible のバージョンを上げたい

実現方法

Ansible では、alwaysという特別なタグを付けたタスクは「毎回必ず呼ばれるタスク」として扱われます。そこで、Ansible のバージョンをチェックするタスクを、この「毎回必ず呼ばれるタスク」として playbook に登録すれば、今回やりたいことを実現できます。

There is a special always tag that will always run a task, unless specifically skipped (--skip-tags always) Tags — Ansible Documentation

実行環境の Ansible のバージョン番号は、ansible_versionという変数から参照できます。例えば、debug モジュールでこの変数を参照すると、以下のように出力されます。

- debug: msg="{{ ansible_version }}"
TASK [common : debug] ************************************************************************************************
ok: [localhost] => {
    "msg": {
        "full": "2.4.3.0",
        "major": 2,
        "minor": 4,
        "revision": 3,
        "string": "2.4.3.0"
    }
}

バージョン番号の4番目は個別に取得できませんが、そこまで確認する機会は考えにくいので大丈夫でしょう。

具体例

どの playbook からも常に呼ばれるロールを作ります。この例では "common"という名前にします。

まず、固定したいバージョンを表す変数 expected_ansible_version を作ります。今回は、Ansible 2.4.0 だけは使われたくないので、2.4.1 以降なら許すということにします。

roles/common/vars/main.yml

---expected_ansible_version:major:2minor:4revision:1

そして、この変数と ansible_version を比較するタスクを作ります。

roles/common/tasks/main.yml

---- name: Ansible major & minor version check
  fail:msg:- Expected Ansible version is
        {{ expected_ansible_version.major }}.{{ expected_ansible_version.minor }},
        but actual version is {{ ansible_version.major }}.{{ ansible_version.minor }}
  when: not (expected_ansible_version.major == ansible_version.major
             and expected_ansible_version.minor == ansible_version.minor)
  run_once:Truetags: always

- name: Ansible revision check
  fail:msg:- Expected Ansible version is
        {{ expected_ansible_version.major }}.{{ expected_ansible_version.minor }}.{{ expected_ansible_version.revision }}+,
        but actual version is {{ ansible_version.full }}when: not (expected_ansible_version.major == ansible_version.major
             and expected_ansible_version.minor == ansible_version.minor
             and expected_ansible_version.revision <= ansible_version.revision)
  run_once:Truetags: always

この例では、revision(バージョン番号の3番目)が想定より大きい場合は許しています。また、エラーメッセージをわかりやすくするために、タスクを2個に分けていますが、Ansible revision checkの方だけでも十分です。

あとは、その環境のすべての playbook で、ロールの先頭にこの common を追加すれば OK です。

playbook1.yml

---- hosts: all
  roles:- common

こうすると、ansible-playbook コマンドでタグを指定してもしなくても、必ずバージョンチェックが実行されます。when でチェックを実行しているので、チェックに成功すると "skipping"と表示されます(この表示は若干わかりにくいので、他にいい方法があったら教えてください)。

$ ansible-playbook -i inventory -clocal playbook1.yml

PLAY [all] ***********************************************************************************************************

TASK [Gathering Facts] ***********************************************************************************************
ok: [localhost]

TASK [common : Ansible major & minor version check] ******************************************************************
skipping: [localhost]

TASK [common : Ansible revision check] *******************************************************************************
skipping: [localhost]

TASK [common : Example task 1] ***************************************************************************************
ok: [localhost]=>{"msg": "Example task 1 is executed."}

TASK [common : Example task 2] ***************************************************************************************
ok: [localhost]=>{"msg": "Example task 2 is executed."}

PLAY RECAP ***********************************************************************************************************
localhost                  : ok=3changed=0unreachable=0failed=0

$ ansible-playbook -i inventory -clocal playbook1.yml --tags=tag1

PLAY [all] ***********************************************************************************************************

TASK [Gathering Facts] ***********************************************************************************************
ok: [localhost]

TASK [common : Ansible major & minor version check] ******************************************************************
skipping: [localhost]

TASK [common : Ansible revision check] *******************************************************************************
skipping: [localhost]

TASK [common : Example task 1] ***************************************************************************************
ok: [localhost]=>{"msg": "Example task 1 is executed."}

PLAY RECAP ***********************************************************************************************************
localhost                  : ok=2changed=0unreachable=0failed=0

メジャーバージョンかマイナーバージョンが合わないと処理が止まります。例えば、Ansible 2.4.3 以上を指定したのに Ansible 2.5 で実行されると、以下のように表示されます。

$ ansible-playbook -i inventory -clocal playbook1.yml --tags=tag1

PLAY [all] ***********************************************************************************************************

TASK [Gathering Facts] ***********************************************************************************************
ok: [localhost]

TASK [common : Ansible major & minor version check] ******************************************************************
fatal: [localhost]: FAILED! =>{"changed": false, "msg": ["Expected Ansible version is 2.4, but actual version is 2.5"]}
    to retry, use: --limit @/Users/myoshiz/devel/ansible_version/playbook1.retry

PLAY RECAP ***********************************************************************************************************
localhost                  : ok=1changed=0unreachable=0failed=1

同じく、リビジョンが小さすぎても止まります。例えば、Ansible 2.4.3 以上を指定したのに Ansible 2.4.0 で実行されると、以下のように表示されます。

$ ansible-playbook -i inventory -clocal playbook1.yml --tags=tag1

PLAY [all] ***********************************************************************************************************

TASK [Gathering Facts] ***********************************************************************************************
ok: [localhost]

TASK [common : Ansible major & minor version check] ******************************************************************
skipping: [localhost]

TASK [common : Ansible revision check] *******************************************************************************
fatal: [localhost]: FAILED! =>{"changed": false, "msg": ["Expected Ansible version is 2.4.3+, but actual version is 2.4.0.0"]}
    to retry, use: --limit @/Users/myoshiz/devel/ansible_version/playbook1.retry

まとめ

Ansible は頻繁にバージョンアップされるので、特に問題がないなら、最新版に追従したほうがいいのは確かです。Ansible 2.5.0 を安心して使う気になれるまでの一時しのぎとして、よかったら試してみてください。

余談:Ansible 2.5 での仕様変更

Ansible 2.5 Porting Guideの冒頭に書いてありますが、include_tasks に付けられた属性(タグなど)の扱いが変わるようです。以下の記事で紹介した事象は、Ansible 2.4 でのみ発生する一時的な問題だったみたいですね。

muziyoshiz.hatenablog.com

2018-03-13追記

Ansible のバージョンチェックは1回で十分なので、run_once: Trueを付けました(参考)。

Blob をファイルとしてダウンロードさせるブックマークレットが「何もしてないのに壊れた」話

$
0
0
f:id:muziyoshiz:20180430135846p:plain

背景

私は、趣味で 艦これアーケードのプレイデータ管理ツール Admiral Statsというサービスを開発・運用しています。これは、

  • SEGA の艦これアーケード公式ページから自分のプレイデータを JSON 形式でダウンロードするツール(通称エクスポータ)
  • プレイデータをアップロードしてもらい、それを可視化する Web アプリケーション

を組み合わせて実現しているサービスで、既に1年半以上運用しています。

このエクスポータは Ruby 版、PowerShell 版、ブックマークレット版を提供していて、一番使われているのはユーザにとって手軽なブックマークレット版です。去年9月の調査では、88%のユーザがブックマークレット版を使っていました。

ブックマークレット版のエクスポータにはさらに以下の2種類があります。いずれも、PCとスマホの両方で動く状態でした。

  • 自動アップロード用
    • エクスポートした JSON を、Admiral Stats に自動的にアップロードする
    • ダウンロード用よりもこちらの方が便利で、普通はこちらが使われている(はず)
  • ダウンロード用
    • エクスポートした JSON を、ローカルに .json ファイルとしてダウンロードする
    • Admiral Stats が終了した場合などのために、手元にファイルとして保存しておきたい人向け

今回はこのなかの、ブックマークレット版の「ダウンロード用」が何もしてないのに壊れたという話です。

何が起こったか

4/24 にユーザーの方から、「ダウンロード用のブックマークレットを実行しても、ファイルがダウンロードされない」という不具合報告を頂きました。詳しく情報を伺ってみると、

  • .json ファイルのダウンロードが始まる代わりに、本来ダウンロードしたい JSON そのものがページ内に表示されてしまう
  • ブラウザは Firefox 59.0.2
  • Admiral Stats 上のデータを見る限り、2018年3月29日まではダウンロードできていた

とのことで、僕の手元でも最新の Firefox で再現しました。また、Chrome でも同じく再現しました。

原因を調べたところ、a タグの download 属性が全く効いていないことがわかりました。

SEGA 公式サイトにログインしたあとに、このブックマークレットを実行すると、XMLHttpRequest を実行し、その結果を blob として取得します。そして、この blob の URL を参照する a タグを作り、自動的にクリックさせることでファイルのダウンロードを実現しています。コード中の以下の箇所です。

var blob = new Blob([req.response]);
          if (window.navigator.msSaveBlob) {window.navigator.msSaveBlob(blob, fname);
          }else{var url = window.URL || window.webkitURL;
            var blobUrl = url.createObjectURL(blob);
            var a = document.createElement('a');
            document.body.appendChild(a);
            a.download = fileType + '_' + ymdhms + '.json';
            a.href = blobUrl;
            a.click();
            document.body.removeChild(a);
          }

https://github.com/muziyoshiz/admiral_stats_exporter_js/blob/v1.10.1/admiral_stats_exporter.js#L82

この download 属性が効いていないために、blob の表示ページに遷移してしまい、ブラウザの画面に JSON そのものが表示されてしまうようです。

しかし、このコードは元々普通に動いていました。それが突然なぜ……?

調査

最近のブラウザは自動アップグレードされてしまうので、なるべく古いバージョンのブラウザが残っていないかと手元の PC を漁って調べてみました。各バージョンでの動作結果はこちら。

ブラウザ リリース日 ブックマークレット(ダウンロード用)の動作
IE 11.371.16299.0 2013-11-08 成功
Microsoft Edge 41.16299.371.0 2017-11-05 成功
Firefox 47.0.1 2016-06-28 失敗
Firefox 59.0.2 2018-03-27 失敗
Chrome 65.0.3325.181 2018-03-06 失敗
Chrome 66.0.3359.117 2018-04-17 失敗
Safari 11.1 2018-02-22 失敗(ファイル名が unknown になる)

ダウンロード用エクスポータの公開日は 2016-10-19 でした。この時点では Chrome, Firefox, Edge, IE 11 で動いていて、特に動かないという報告もありませんでした。

ダウンロード用エクスポータに最後に機能追加したのは 2017-09-22 でした。普段の開発は Chrome で行っているので、この時点で、少なくとも Chrome では動いていた……はずです。

IE と Edge は window.navigator.msSaveBlobを使っているからか問題なし。Safari は当時からダウンロードが動かなかったので、状況変わってません。

Firefox は、かなり古いバージョンがインストールされたものが見つかったので動作確認したところ、エクスポータ公開日の 2016-10-19 より過去のバージョンなのに動きませんでした。ということは、SEGA 公式サイト側の動作が何か変わっている……?

Chrome は、2ヶ月以上古いバージョンが手元になかったので状況不明。しかし、Chrome 65 からクロスオリジンに対する a タグの download 要素はブロックされるようになったことがわかりました。

developers.google.com

Block cross-origin <a download>
To avoid what is essentially a user-mediated cross-origin information leakage, Blink will now ignore the presence of the download attribute on anchor elements with cross origin attributes. Note that this applies to HTMLAnchorElement.download as well as to the element itself.

今回のブックマークレットの場合、公式サイトと JSON の取得元は同一ドメインなので、このクロスオリジンに対する変更は関係ないのでは?と思ったのですが、以下のページによると手動設定された HTTP ヘッダーの種類によっては同一ドメインでもクロスオリジンの扱いになるようです。

developer.mozilla.org

ユーザーエージェントによって自動的に設定されたヘッダー (たとえば Connection、 User-Agent、 または Fetch 仕様書で "forbidden header name"として定義されている名前のヘッダー) を除いて、手動で設定できるヘッダーは、 Fetch 仕様書で "CORS-safelisted request-header"として定義されている以下のヘッダーだけです。
- Accept
- Accept-Language
- Content-Language
- Content-Type (但し、下記の要件を満たすもの)
- Last-Event-ID
- DPR
- Save-Data
- Viewport-Width
- Width

問題のブックマークレットは、JSON を取得する際に、CSRF チェックに引っかからないように X-Requested-With ヘッダを手動で設定しています。これが原因??

    req.setRequestHeader('X-Requested-With', 'XMLHttpRequest');

https://github.com/muziyoshiz/admiral_stats_exporter_js/blob/v1.10.1/admiral_stats_exporter.js#L53

Chrome 64 以前で動くならコレが原因と断言できるのですが、手元の Chrome はすべて Chrome 65 以降にアップグレード済みだったので検証できず。

それとは別に、Google Chrome ver60からa要素のdownload属性が同一オリジンポリシーに厳密になった(Qiita)には、Chrome 60(2017-07-25 公開)からこの制限が入ったとの情報もあります。しかし、2017-09-22 に新機能の動作確認をしているので、そのときには動いていたはずなんですよね……。Deprecations and Removals in Chrome 60にはそういう記載もなく、動作が変わった時期はよくわかりません。うーん。

対応

結局のところ、この問題が具体的にいつから発生したのかはわかりませんでした。しかし、モダンブラウザでは crossorigin 属性の付いたコンテンツの <a download>は許可しない方向に動いていることはわかりました。

そのため、この制限を何らかの方向で回避することは不可能と判断して、報告してくれたユーザーには代替案の利用をオススメしました。

こういうことが時々あるのが、Web アプリ開発の嫌なところですね……。

Admiral Stats のユーザ向けのまとめ

  • ブックマークレットでの SEGA 公式からの JSON ダウンロードは不可能になりつつある
  • ブラウザ側の機能制限によるものなので対策不可
  • 自動アップロード用のブックマークレットのほうが便利なので、そちらに乗り換えて欲しい(使い方
  • .json ファイルをダウンロードしたい場合は PowerShell 版などをどうぞ(使い方

このブログ(無印吉澤)を HTTPS 対応させました

$
0
0

このブログ(無印吉澤)を HTTPS 対応させました。今後は HTTP の方にアクセスしても、HTTPS の方にリダイレクトされます。

今回はそのために必要だった作業と、結局どうしようもなかった部分のまとめです。

はてなブログの HTTPS 対応状況

はてなブログは2018年2月22日から HTTPS での配信に対応し、その後も改良が続いているようです。

staff.hatenablog.com

staff.hatenablog.com

staff.hatenablog.com

手順については、以下のヘルプを読めば大体のことはわかりました。

help.hatenablog.com

help.hatenablog.com

必要だった作業

1. 「HTTPS配信の状況」の「変更する」ボタンを押す

まず、はてなブログの「設定」→「詳細設定」→「HTTPS配信」から、URL 設定のページを表示。

「HTTPS配信の状況」の「変更する」ボタンを押すと一瞬で切り替わります。

はてな内のサービスを見てみましたが、はてなブックマークの URL は https にすぐに切り替わったように見えました。

http://b.hatena.ne.jp/entrylist?url=muziyoshiz.hatenablog.com

2. サイト内に埋め込まれている自サイトの URL を https://に書き換える

http://にアクセスされてもリダイレクトされるので、基本的には書き換え不要なんですが、Feedly の Follow ボタンだけは https://muziyoshiz.hatenablog.com/feedで作り直しました。

Follower 0件になって寂しいんですが、まあ、今後 HTTPS が常識になるなら早いうちに合わせたほうがマシかなと。

3. Mixed Content の確認

ここからは、ひたすら Mixed Content の確認です。Chrome の開発者ツールを開いた状態で、ブログ一覧から「次のページ」を押し続けるという地味な作業をやりました。

うちはまだ記事数少ないので良かったですが、毎日ブログ投稿してるような人は死にそうですね……。

embed:cite 記法で書かれたリンクの修正

普通の [title](url)記法で書かれたリンクは http でも問題ないんですが、はてなブログ独自の記法は、サムネイルが埋め込まれる関係か Mixed Content のエラーが出たので修正しました。

  • Before: [http://muziyoshiz.hatenablog.com/entry/2016/04/25/015644:embed:cite]
  • After: [http://muziyoshiz.hatenablog.com/entry/2016/04/25/015644:embed:cite]

はてな以外のサービスの URL も、http で書いていたものは https に修正しました。

  • Before: [http://twitter.com/okapies/status/856528271306928128:embed:cite]
  • After: [https://twitter.com/okapies/status/856528271306928128:embed:cite]

うちのサイトは、embed:cite 記法の URL をたまたま全部 https に直せましたが、リンク先が HTTPS 対応してない場合は [title](url)記法に直したほうがいいかもしれません。

はてなフォトライフ(Fotolife)の画像の再読み込み

はてなブログに画像を貼るときは、Fotolife にアップロードして、以下の記法で埋め込むのが楽です。このブログでは主にアイキャッチ画像を貼るのに使っています。

<div align="center">[f:id:muziyoshiz:20160425013924j:plain]</div>

最近のブログに埋め込んだ画像は、何もしなくても最初から https になっていたんですが、2016年6月より前に埋め込んだアイキャッチ画像の URL は http になってしまっていました(Fotolife 自体がこのくらいの時期に HTTPS 化した?)。

対策としては、「ブログを開く」→「何かしら編集する」→「ブログを保存する」と、URL が再生成されて https に変わりました。これを、Fotolife の画像を埋め込んだ記事すべてでやらないといけないので、これはめんどくさい……。

どうしようもなかった部分

Amazon の商品画像埋め込み

はてなブログは、以下のような記法で Amazon の商品画像を埋め込めます。

[asin:B01FRIOYEC:detail]

この商品画像が、どうも HTTPS 配信のものと HTTP 配信のものが混在しているらしく、特に古い本は HTTP 配信のようでした。

例えば、以下の記事の最後にある「参考文献」で、「プログラミング Elixir」の画像は HTTPS、「Programming Phoenix」の画像は HTTP です。

muziyoshiz.hatenablog.com

ブログ記事を編集→保存しても直らなかったので、はてなではなくて Amazon 側の問題なんだろうと思って諦めました。

SlideShare のスライド埋め込みでエラー

HTTPS 化とは関係ないんですが、過去の記事を見返してみたら、SlideShare のスライドを埋め込んでいる箇所で大量のエラーが出てました。

f50c7d109361420bbc0ee31b2c44d54e:5 Uncaught SyntaxError: Unexpected token o in JSON at position 1
    at JSON.parse (<anonymous>)
    at n.receive (player-67e207486452a6edc528fc9bbb3a0e02.js:26)
    at player-67e207486452a6edc528fc9bbb3a0e02.js:26
    at nrWrapper (f50c7d109361420bbc0ee31b2c44d54e:5)

とか、

combined_base.js?f14b451c58:55 [Deprecation] chrome.loadTimes() is deprecated, instead use standardized API: Paint Timing. https://www.chromestatus.com/features/5637885046816768.

とか。

SlideShare が生成する iframe タグが古いからかな?と思って、SlideShare のサイトで iframe タグを生成し直してみました。でも、リンクの URL が http から https に変わっただけで、上記のエラーは消えませんでした。

あと、

Access to Font at 'https://public.slidesharecdn.com/fonts/fontawesome-webfont.woff2?v=4.3.0?cb=1526503678' from origin 'https://www.slideshare.net' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'https://www.slideshare.net' is therefore not allowed access.

という CORS のエラーが出ていたので、こちらも少し調べました。2017年1月に SlideShare に報告してくれた人がいる問題のようですが、いまも解決してませんでした。

github.com

このあたりのエラーが放置されてると、今後は SlideShare はなるべく使わないようにしようかなあと思っちゃいますね。

まとめ

結果的に大した手間もなく移行できて、移行から1週間くらい様子を見た感じでは、問題なく動いていそうです。

ただ、はてなフォトライフの URL を作り直すのだけは本当に面倒だったので、ブログ記事が多い場合は、なにか自動化手段を先に用意したほうがいいと思います。がんばってください。

Webhook の受信を契機として複数の API を叩く Lambda 関数を Node.js で書きたいときのためのメモ

$
0
0
f:id:muziyoshiz:20180630010936p:plain

ときどき AWS Lambda の Lambda 関数を Python で書いてます。AWS Lambda 便利ですよね。

しかし最近、他の人に Lambda 関数をいじってほしいけど Python での開発環境を作ってもらうのが面倒なので、一度書いた Lambda 関数を Node.js で書き換える、という機会がありました。そのときに Node.js の流儀がわからず困ってしまったので、あとで自分が読み返すためのメモを書いておきます。

このメモの対象読者は次のような人です。

  • Webhook の受信を契機として、複数の API を叩くような処理が書きたい
  • その処理を AWS Lambda に乗せて、楽に運用したい
  • AWS Lambda と API Gateway へのデプロイを楽にしたいので Serverless Framework を使いたい
  • 開発環境構築を楽にするために Node.js で書きたい(Serverless Framework を使うなら Node.js もあるはず)

※注意事項:私は Node.js 初心者で、Node.js の書き方はよくわかってません。なので、もっと良い書き方がわかったら、このメモをちょっとずつ直していきます。

目次

Serverless Framework のインストール

  • Node.js のインストール
    • brew install npm
  • Serverless Framework のインストール
    • npm install -g serverless

参考:Serverless Framework - AWS Lambda Guide - Installing The Serverless Framework

AWS クレデンシャルの設定

AWS クレデンシャルを設定していない場合、sls コマンドで作れます。~/.aws/credentialsに設定済みであれば、この手順は不要です。デフォルトプロファイル以外でも大丈夫です。

  • Administrator Access 権限のある IAM ユーザーの作成
  • AWS クレデンシャルの設定
    • sls config credentials --provider aws --key アクセスキーID --secret シークレットアクセスキー

参考:Serverless Framework - AWS Lambda Guide - Credentials

新しい Serverless service (Node.js 8.10) の作成

Lambda 関数のランタイムには、async/await が使える Node.js 8.10 を選択します。現時点で、AWS Lambda で使える最新の Node.js はコレです。

ただ、6/30現在では sls create -t aws-nodejsでひな型(boilerplate)を作ると、Node.js 6.10 が選択されてしまいます。

$ sls create -t aws-nodejs -p example
Serverless: Generating boilerplate...
Serverless: Generating boilerplate in"/Users/myoshiz/example"
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.27.3
 -------'Serverless: Successfully generated boilerplate for template: "aws-nodejs"

上記のコマンドを実行すると、以下のファイルが作られます。

  • .gitignore
  • handler.js
  • serverless.yml

この serverless.yml の

provider:name: aws
  runtime: nodejs6.10

となっている部分を

provider:name: aws
  runtime: nodejs8.10

に書き換えると、Node.js 8.10 が使えます。

参考:Node.js 8.10 runtime now available in AWS Lambda | AWS Compute Blog

デフォルトステージの設定

Serverless Framework には「ステージ」という概念があって、1個のコードを、ステージごとに Lambda 関数名や設定を変えてデプロイできます。ステージの主な用途は、開発環境と本番環境の切り替えです。

このステージを引数から指定できるようにするための設定を、serverless.yml に追加します。

provider:name: aws
  runtime: nodejs8.10
  stage: ${opt:stage, self:custom.defaultStage}
custom:defaultStage: dev

Serverless Framework のデフォルトのステージは dev なので、そのままで使うならこの設定は不要です。しかし dev が不要な場合は、デフォルトを production にしておくといいでしょう。

参考:Serverless Framework - AWS Lambda Guide - Deploying

デフォルトで使うAWSプロファイルおよびリージョンの設定

AWS プロファイルとリージョンも、引数から指定可能にしておきます。以下の設定を serverless.yml に追加します。

profile: ${opt:profile, self:custom.defaultProfile}
  region: ${opt:region, self:custom.defaultRegion}
defaultProfile: default
  defaultRegion: ap-northeast-1

ステージに応じて AWS プロファイルを切り替えたい場合は、以下の方法で実現できるようです。

参考:Serverless Frameworkで環境変数を外部ファイルから読み込み、環境毎に自動で切り替えてみる | Developers.IO

環境変数の定義

AWS Lambda で使いたい機密情報(パスワードや API キーなど)は、「環境変数」として AWS Lambda に登録できます。しかし、この環境変数は、git リポジトリには登録したくありません。

Serverless Framework では、

  • 環境変数を git リポジトリ管理外の YAML ファイルに書く
  • デプロイ時のみこの YAML ファイルを用意する

ということが可能です。

まず、以下の YAML ファイルを用意します。

conf/dev.yml - dev ステージで使う環境変数
conf/production.yml - production ステージで使う環境変数

そして、.gitignore に以下のように書いて、git リポジトリへの登録を禁止します。

# Secrets
conf/*.yml

最後に、serverless.yml に以下の設定(otherfile:以下)を追加します。

custom:defaultStage: dev
  defaultProfile: default
  defaultRegion: ap-northeast-1
  otherfile:environment:dev: ${file(./conf/dev.yml)}
      production: ${file(./conf/production.yml)}

YAML ファイル(dev.yml, production.yml)には、以下のように記載します。環境変数に数値を代入したい場合は、ダブルクォートで囲んで、文字列にする必要があります。これは Serverless Framework の問題ではなく、AWS Lambda の制約です。

---MESSAGE: Hello
YEAR:"2018"

参考:Serverless Frameworkで環境変数を外部ファイルから読み込み、環境毎に自動で切り替えてみる | Developers.IO

request-promise モジュールのインストール

API の呼び出しには request-promise モジュールを使います。このモジュールを使うためには request モジュールも必要です。

$ npm init
$ npm install --save request
$ npm install --save request-promise

package.json と package-lock.json が作成されます。このファイルは、ソースコードと一緒に git リポジトリにコミットします。

参考:request/request: Simplified HTTP request client., request/request-promise: The simplified HTTP request client 'request' with Promise support. Powered by Bluebird.

開発用プラグインの追加

Webhook を受け付けるためには、Amazon API Gateway と AWS Lambda を組み合わせて使うのが一般的です。

しかし、動作確認のためにいちいち API Gateway にデプロイするのは面倒なので、serverless-offline プラグインを使って、ローカルで動作確認できるようにします。

以下のコマンドでインストールできます。

$ npm install --save-dev serverless-offline

serverless.yml に以下の設定を追加。

plugins:- serverless-offline

sls offline startを実行すると、ローカルで HTTP サーバが動作します。

参考:dherault/serverless-offline: Emulate AWS λ and API Gateway locally when developing your Serverless project

handler の定義(Amazon API Gateway の設定)

sls createコマンドで作られた Lambda 関数 "hello"に handler の設定(events:以下)を追加してみます。

functions:hello:handler: handler.hello
    events:- http:path: hello
          method: post
    environment:MESSAGE: ${self:custom.otherfile.environment.${self:provider.stage}.MESSAGE}
      YEAR: ${self:custom.otherfile.environment.${self:provider.stage}.YEAR}

先ほど説明した環境変数を Lamnbda 関数内で使うには、environment:以下で明示的に指定する必要があります。

そして、handler.js 内の hello 関数を以下のように修正してみましょう。リモートにデプロイすると、この関数は API Gateway から起動されます。

module.exports.hello = (event, context, callback) => {
    console.log(`MESSAGE: ${process.env.MESSAGE}`)
    console.log(`YEAR: ${process.env.YEAR}`)

    console.log(event);

    const body = event['body'];

    try{const json = JSON.parse(body);

        if (json && json.message) {
            callback(null, {
                statusCode: 200,
                body: JSON.stringify({message: `${process.env.MESSAGE} ${json.message} ${process.env.YEAR}`})
            })
        }else{
            callback(null, {statusCode: 400, body: JSON.stringify({error: 'No message'})});
        }}catch (e) {
        console.log(e);
        callback(null, {statusCode: 400, body: JSON.stringify({error: 'Invalid JSON'})});
    }};

ローカルでの動作確認

この hello 関数をローカルで動作確認します。まず、以下のコマンドで HTTP サーバを起動します。

$ sls offline start
Serverless: Starting Offline: dev/ap-northeast-1.

Serverless: Routes for hello:
Serverless: POST /hello

Serverless: Offline listening on http://localhost:3000

他のターミナルから curl で JSON を POST し、応答が返されたら成功です。

$ curl -H'Content-Type:application/json'-d'{"message":"world"}' http://localhost:3000/hello
{"message":"Hello world 2018"}

sls offline startを実行したターミナルには、console.log()に渡した event オブジェクトが以下のように出力されます。

Serverless: POST /hello (λ: hello)
MESSAGE: Hello
YEAR: 2018{ headers:
   { Host: 'localhost:3000',
     'User-Agent': 'curl/7.54.0',
     Accept: '*/*',
     'Content-Type': 'application/json',
     'Content-Length': '19'},
  path: '/hello',
  pathParameters: null,
  requestContext:
   { accountId: 'offlineContext_accountId',
     resourceId: 'offlineContext_resourceId',
     apiId: 'offlineContext_apiId',
     stage: 'dev',
     requestId: 'offlineContext_requestId_27367216451093634',
     identity:
      { cognitoIdentityPoolId: 'offlineContext_cognitoIdentityPoolId',
        accountId: 'offlineContext_accountId',
        cognitoIdentityId: 'offlineContext_cognitoIdentityId',
        caller: 'offlineContext_caller',
        apiKey: 'offlineContext_apiKey',
        sourceIp: '127.0.0.1',
        cognitoAuthenticationType: 'offlineContext_cognitoAuthenticationType',
        cognitoAuthenticationProvider: 'offlineContext_cognitoAuthenticationProvider',
        userArn: 'offlineContext_userArn',
        userAgent: 'curl/7.54.0',
        user: 'offlineContext_user'},
     authorizer:
      { principalId: 'offlineContext_authorizer_principalId',
        claims: undefined },
     protocol: 'HTTP/1.1',
     resourcePath: '/hello',
     httpMethod: 'POST'},
  resource: '/hello',
  httpMethod: 'POST',
  queryStringParameters: null,
  stageVariables: null,
  body: '{"message":"world"}',
  isOffline: true}
Serverless: [200]{"statusCode":200,"body":"{\"message\":\"Hello world 2018\"}"}

デプロイ

ローカルで動作確認できたら、リモートの AWS Lambda と Amazon API Gateway にデプロイします。デプロイは sls deployコマンドで行います。

$ sls deploy
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
.....
Serverless: Stack create finished...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service .zip file to S3 (2.28 MB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
..............................
Serverless: Stack update finished...
Service Information
service: example
stage: dev
region: ap-northeast-1
stack: example-dev
api keys:
  None
endpoints:
  POST - https://aaaaaaaa.execute-api.ap-northeast-1.amazonaws.com/dev/hello
functions:
  hello: example-dev-hello

サービス名-ステージ名-関数名という名前の Lambda 関数が作られました(この例では example-dev-hello)。本番環境のステージを指定するときは sls コマンドに --stage productionを付けてください。

デプロイに成功すると、ローカルの場合と同じように動作確認できます。

$ curl -H'Content-Type:application/json'-d'{"message":"world"}' https://aaaaaaaa.execute-api.ap-northeast-1.amazonaws.com/dev/hello

1回デプロイしたあとなら、sls deploy -f helloのようにして、特定の Lambda 関数だけ更新することもできます。

$ sls deploy -f hello

ちなみに、Lambda 関数がもう要らなくなったという場合は sls removeで削除できます。

リモートでの動作確認

Lambda 関数がうまく動かないときにはログを確認したくなると思いますが、その場合は console.log()でログ出力すると、CloudWatch Logs から確認できます。

ログは /aws/lambda/example-dev-helloのような名前のロググループに登録されます。

Webhook を受信したら複数の API を叩く

ここまでで、Webhook を受信したらなにかする、という Lambda 関数は書けました。次は、複数の API を叩くコードの例を紹介します。

API を1個叩くだけなら、request モジュールのみで実現できます。

しかし、ある API を叩き、その返り値を元に別の API を叩き、さらにその返り値を元に……と処理が続く場合は、コールバック地獄を避けるために request-promise を使ったほうが楽です。また、Promise を使うだけでなく、async/await も使ったほうがコードが読みやすくなります。

ここでは、試しに、先ほどの hello を3回呼び出すエンドポイント hello_hello_hello を作ってみます。

まず、serverless.yml に以下を追加します。これはローカルでの動作確認(後述)を簡単にするために method: getにしていますが、実際に Webhook から起動するときは method: postにしてください。

hello_hello_hello:handler: handler.helloHelloHello
    events:- http:path: hello_hello_hello
          method: get

そして、handler.js に以下のコードを追加します。awaitで1個目の API からの応答が来るのを待ってから、次の API 呼び出しをしているのがわかるでしょうか。

module.exports.helloHelloHello = (event, context, callback) => {const callHello = (message) => {const request = require('request-promise');

        const params = {
            message: message
        };

        const options = {
            method: 'POST',
            uri: `http://localhost:3000/hello`,
            body: params,
            json: true};

        return request(options)
            .then(function (parsedBody) {return parsedBody;
            })
            .catch(function (err) {
                console.log(err);
                returnnull;
            });
    };

    const callHello3 = async (message) => {const response1 = await callHello(message);
        console.log(response1);

        const response2 = await callHello(response1.message);
        console.log(response2);

        const response3 = await callHello(response2.message);
        console.log(response3);

        callback(null, {statusCode: 200, body: response3.message});
    };

    callHello3('world');
};

この関数は、以下のように動作します。

sls offline startで HTTP サーバを起動して、curl で以下のコマンドを実行してみてください。

$ curl http://localhost:3000/hello_hello_hello
Hello Hello Hello world 2018 2018 2018

これと同じ要領で、hello 以外の API も呼び出すことができます。

基本は以上です。あとは頑張ってください。

付録1. Windows の場合

Windows の場合は、Serverless Framework のインストール方法が違います。また、普通は curl コマンドが使えないので、別の方法を使う必要があります。

Serverless Framework のインストール

まず node.jsから Windows 用のインストーラをダウンロードして、Node.js をインストールします。

次に、Windows のメニューから "Node.js command prompt"を選択して、プロンプトを開きます。このプロンプトで npm install -g serverlessを実行すれば、Serverless Framework をインストールできます。

これ以降は、"Node.js command prompt"から、上記の sls コマンドを実行できます。

curl コマンドの代わり

curlコマンドの代わりに、Node.js の REPL からテスト用のリクエストを送信できる。

JSON を POST して、HTTP リクエストのボディを標準出力に表示。

C:\> node
> const request = require('request');
> const options = {url: 'http://localhost:3000/hello', headers: {'Content-type': 'application/json'}, json: {message: 'world'} };
> request.post(options, function(error, response, body){ console.log(body); });

GET して、HTTP リクエストのボディを標準出力に表示。

C:\> node
> const request = require('request');
> request.get('http://localhost:3000/hello_hello_hello', function(error, response, body){ console.log(body); });

付録2. IntelliJ IDEA Ultimate で Node.js 8.10 を使う

僕は IndelliJ IDEA Ultimate でコードを書いているのですが、Node.js 8.10 でエラーが出ない状態にするには以下の設定が必要でした。

  • Preferences > Plugins > Install JetBrains Plugin... を選択し、その画面から NodeJS プラグインをインストールする
  • Preferences > Languages & Frameworks > Node.js and NPM を選択し、その画面から Node.js Core library を有効にする(Enable ボタンを押す)
  • Preferences > Languages & Frameworks > JavaScript を選択し、その画面上で JavaScript language version を "ECMAScript 6"に変更する

参考:javascript - Arrow function "expression expected" syntax error - Stack Overflow

その他の参考ページ


Amazon API Gateway で API (Webhook) の呼び出し元 IP アドレスを制限する

$
0
0
f:id:muziyoshiz:20180630010936p:plain

前回の記事では、Webhook の受け口を Amazon API Gateway で作る方法をまとめました。

muziyoshiz.hatenablog.com

この方法で作った API は、URLを知っている人なら誰でも叩けてしまいます。Amazon API Gateway は IAM や API キーでの認証をサポートしていますが、一般的な Webhook は指定された URL に POST を送るだけなので使いづらいです。

しかし、Amazon API Gateway のリソースポリシーを使うと、API の呼び出しを許可する IP アドレス(つまり Webhook の送信元 IP アドレス)を限定することができます。何も無いよりはずっと良いので、今回はこの設定方法をまとめておきおます。

Amazon API Gateway のリソースポリシー対応

昔の Amazon API Gateway には送信元 IP アドレスの制限機能はなく、Amazon API Gateway Lambda オーソライザー(カスタムオーソライザー)で自前で実装する必要があったそうです(※僕は使ったことないのですが、ググると出てきます)。

しかし、2018年4月2日に Amazon API Gateway が API リソースポリシーをサポート開始し、これで IP アドレス制限ができるようになりました。

https://aws.amazon.com/jp/about-aws/whats-new/2018/04/amazon-api-gateway-supports-resource-policies/aws.amazon.com

以下の AWS のドキュメントに、リソースポリシーの例が掲載されています。

docs.aws.amazon.com

Webhook の場合は「送信元 IP アドレスを特定のサービスに限定する」ことができれば十分でしょう。例えば、Backlog というサービスでは以下のように Webhook を実行するサーバーの IP アドレス範囲を公開しています。

backlog.com

この Backlog の Webhook のみを受け取りたい場合、以下のようにリソースポリシーを書けば OK です(リージョンなどは伏せてます)。

{"Version": "2012-10-17",
    "Statement": [{"Effect": "Deny",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": ["arn:aws:execute-api:region:account-id:api-id/*"
            ],        },
        {"Effect": "Allow",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": ["arn:aws:execute-api:region:account-id:api-id/*"
            ],
            "Condition": {"NotIpAddress": {"aws:SourceIp": ["52.199.190.133/32",
                        "54.238.59.48/32",
                        "54.65.8.249/32",
                        "52.192.161.184/32",
                        "52.68.222.36/32"
                    ]}}}
    ]
}

Serverless Framework のリソースポリシー対応

Serverless Framework も、2018年4月7日リリースのバージョン1.28でリソースポリシーに対応しました。今では、serverless.yml 内でリソースポリシーを記述できます。

github.com

簡単な例が公式ドキュメントに載っています。

serverless.com

serverless.yml に以下のように書くと、先ほどの Backlog の例に挙げたリソースポリシーが自動設定されます。"103.79.14.0/24"のような範囲指定も可能です。

provider:name: aws
  runtime: nodejs8.10

  resourcePolicy:- Effect: Allow
      Principal:"*"Action: execute-api:Invoke
      Resource:- execute-api:/*/*/*
      Condition:IpAddress:aws:SourceIp:- "52.199.190.133"- "54.238.59.48"- "54.65.8.249"- "52.192.161.184"- "52.68.222.36"

設定結果は、以下のように AWS コンソールで確認できます。

f:id:muziyoshiz:20180729112632p:plain:w800

範囲外の IP アドレスからのアクセスはどのように拒否されるか

前回の記事で作った hello 関数で試してみます。

hello 関数は、以下のように JSON でメッセージを受け取って、文字列を付け足して返す関数でした。

$ curl -H'Content-Type:application/json'-d'{"message":"world"}' http://localhost:3000/hello
{"message":"Hello world 2018"}

許可されていない IP アドレスから、この curl コマンドを実行すると、403 とエラーメッセージ(JSON)が返されます。

$ curl -v-H'Content-Type:application/json'-d'{"message":"world"}' https://**********.execute-api.ap-northeast-1.amazonaws.com/dev/hello
*   Trying ***.***.***.***...
* TCP_NODELAY set
* Connected to **********.execute-api.ap-northeast-1.amazonaws.com (***.***.***.***) port 443(#0)
* TLS 1.2 connection using TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
* Server certificate: *.execute-api.ap-northeast-1.amazonaws.com
* Server certificate: Amazon
* Server certificate: Amazon Root CA 1
* Server certificate: Starfield Services Root Certificate Authority - G2
> POST /dev/hello HTTP/1.1
> Host: **********.execute-api.ap-northeast-1.amazonaws.com
> User-Agent: curl/7.54.0
> Accept: */*
> Content-Type:application/json
> Content-Length: 19>
* upload completely sent off: 19 out of 19 bytes
< HTTP/1.1 403 Forbidden
< Content-Type: application/json
< Content-Length: 165< Connection: keep-alive
< Date: Sun, 29 Jul 201802:08:33 GMT
< x-amzn-RequestId: 4ba819ee-92d4-11e8-a7e3-7feb0ae86886
< x-amzn-ErrorType: AccessDeniedException
< x-amz-apigw-id: KxIxOGMatjMFhWg=
< X-Cache: Error from cloudfront
< Via: 1.1 7b63dc372a4330b28d4dd1f11ec139a7.cloudfront.net (CloudFront)< X-Amz-Cf-Id: PPGEdw9vYkOwQejOGAztIO63Atb73po3rzaszbcRpJpXZEQbjVzPIg==<
* Connection #0 to host **********.execute-api.ap-northeast-1.amazonaws.com left intact{"Message":"User: anonymous is not authorized to perform: execute-api:Invoke on resource: arn:aws:execute-api:ap-northeast-1:********3987:**********/dev/POST/hello"}

一度設定したリソースポリシーの削除方法

ここはまだ自分でもよくわかっていない部分です。

一度設定したリソースポリシーを消すために、以下のように resourcePolicy以下をコメントアウトして sls deployを実行しても、AWS コンソール上のリソースポリシーは削除されませんでした。

provider:name: aws
  runtime: nodejs8.10

#  resourcePolicy:#    - Effect: Allow#      Principal: "*"#      Action: execute-api:Invoke#      Resource:#        - execute-api:/*/*/*#      Condition:#        IpAddress:#          aws:SourceIp:#            - "52.199.190.133"#            - "54.238.59.48"#            - "54.65.8.249"#            - "52.192.161.184"#            - "52.68.222.36"

また、AWS コンソール上で直接、以下のようにリソースポリシーを削除しても、IP アドレス制限は有効なままでした。Amazon API Gateway の設定を変更するだけでは駄目? CloudFormation や CloudFront の設定も絡んでるんでしょうか?

f:id:muziyoshiz:20180729112647p:plain:w800

結局、以下のように、すべての IP アドレスを許可するリソースポリシーを設定したあとで、sls deployを実行すると、IP アドレス制限を解除することができました。

resourcePolicy:- Effect: Allow
      Principal:"*"Action: execute-api:Invoke
      Resource:- execute-api:/*/*/*

kintone devcamp 2018 にて AWS Lambda を使ったサービス間連携についてのハンズオンセッションを行いました

$
0
0
f:id:muziyoshiz:20180630010936p:plain

8/2(木) に開催された kintone devCamp 2018にて、kintone と Backlog を API 連携させる方法についてのハンズオンセッションを実施しました。サイボウズの方から企画の提案があって、(資料を作りながら自分でも)手を動かしてみる良い機会と思い、引き受けさせていただきました。

kintone & Backlogハンズオン 〜利用シーンに応じたタスク管理ツールの使い分けをAPIで実現〜

目次はこんな感じ。90分のハンズオンセッションでした。

  • Backlogとは?
  • Backlogとkintoneの連携
  • Backlog APIによるサービス間連携
  • ハンズオン1. ノンプログラミングでの連携
  • AWS LambdaとFunction as a Service
  • ハンズオン2. AWSのサービスを用いた高度な連携

このハンズオンセッションは2部構成で、前半は kintone プラグインを使ったノンプログラミングでの連携。後半は AWS Lambda と Amazon API Gateway を使った連携でした。後半はスライドの p.94 からです。AWS Lambda だけ興味のある方は、そこから見てもらえれば。

スライドにはかなり細かく手順を書きましたし、ソースコードも GitHub で公開しているので、AWS Lambda を一度使ってみたい!という方は是非お試しください。

github.com

ちなみに、ハンズオン当日は、前半のハンズオンは7〜8割の参加者が設定完了して動かすことができたのですが、前半で時間を使いすぎて、後半はかなり駆け足で解説だけして終わってしまいました。それでも、後ほど頂いたアンケート結果で、8割近くが満足度「普通」以上の回答で一安心しました。

資料を作り始める前は、90分あれば余裕と思ってたんですが、実際に手を動かしてもらうハンズオンは時間調整が難しいですね……。次の機会があったらもう少し工夫します。

あわせて読みたい

最近書いた以下の記事は、このハンズオンの準備中に調べたことのまとめです。ハンズオンを触ってみる際は、こちらもあわせてご覧ください。

muziyoshiz.hatenablog.com

muziyoshiz.hatenablog.com

SRE Lounge #5 にて Backlog における SRE の事例について講演しました

$
0
0
f:id:muziyoshiz:20180927232609p:plain

僕は去年の8月にヌーラボに入社して、そこから Backlog の SRE として働いています。

SRE としての経験は約1年なのですが、ちょうどサービスが成長し、会社もエンジニアを積極的に採用して拡大している時期だったこともあり、色々な経験ができました。そのなかで、SRE の難しさ、SRE の組織の問題にも直面してきました。

このあたりの経緯を整理して話すだけでも SRE にとって面白い話になるのではないか、と思い、今回の SRE Lounge #5 では「Backlog における SRE の事例 〜プロダクトの成長のために SRE はなにをすべきか〜」というタイトルで発表させていただきました。

sre-lounge.connpass.com

発表スライドはこちらです。

発表のときは冒頭で説明したのですが、これがベストプラクティスと言うつもりは全然ありません。僕らもまだ悩んでいる最中の問題を、その背景も含めて(話せる範囲で)丁寧にまとめて説明することで、他社の SRE の方々からご意見をいただきたい……という趣旨の発表でした。

その後の質疑応答や、Twitter での反響を見た限り、とても好評だったようでホッとしています。「SREの業務範囲は放っておくと再現なく広がっていく」、「SRE を組織の中でどう配置すべきかはすごく難しい」といった問題意識は多くの方に同意してもらえたようです。

詳しくはスライドの方を見てもらうとして、以下に当日の質疑応答や、懇親会で話したこと、今回のプレゼンの関連資料などをまとめておきます。

質疑応答

覚えている範囲でまとめておきます。発表直後でかなりテンパっていたので、質問者の質問や、僕の回答の表現は、実際に話されたものとは多少ズレているかもしれません。

  • Q) 開発チームはスクラムなどに従って開発していると思うが、SRE の仕事はその開発スパンには合わないのではないか。そこはどうしているか
    • A) 開発者と SRE で同じカンバンは共有しているが、SRE は独立して動いている。障害対応があるときはそれを優先し、それ以外のときは改善のタスクを取るとか
  • Q) SRE が開発チームに入ることで良くなる面があったのはわかった。逆に、SRE が入ることで悪くなった面はあるか?
    • A) 開発者の側としては割り込みが増えたと感じているかもしれない。エスカレーションが早くなる分、「調べたけど問題なかった」というハズレも増える
  • Q) チームは拠点ごとに分かれているか? このチームは福岡に全員いる、など
    • A) ヌーラボはリモートでのコラボレーションを促進するサービスを提供していることもあり、複数拠点に分かれたチームはたくさんある。自分もそう。ただ、1拠点にまとまったチームもある
  • Q) Backlog の開発者の規模はどれくらいで、そのうち SRE は何名いるか
    • A) 開発者は30名前後と考えてもらえれば。SRE は講演でも話したとおり、現時点では4名
  • Q) SRE を採用するときに重視している点はあるか
    • A) 個人的には、技術力が高いこと、自分から問題を定義して仕事を作れること、チームをまたいで仕事をする必要があるのでそういうコミュニケーションを嫌がらないこと、の3点を重視している

懇親会で話したこと、質問を受けたこと

お話できたのは一部の方だけでしたが、SRE やインフラエンジニアが多い印象を受けました。ターゲットに沿った人がうまく集まっていて、SRE Lounge は SRE にとって良い議論・発表の場だと思いました。

オプトの渋谷さんの発表に対する質疑応答でも出ていた質問ですが、SRE は何人いたらいいのか? 開発者に対する割合としてどれくらい必要なのか? という質問は何度か受けました。北野さんがツイートしてましたけど、答えはまさにこれ(↓)ですね。何人必要かは、自分たちもまだ模索中です。

あとは、これから新しく SRE チームを作るとしたら何に注意すべきか、という相談も受けました。SRE という用語を生み出した Google 自身もそうですが、自社でサービスを開発・運用している組織でないと、SRE に適切なインセンティブをもたせることは難しいのではないか……というくらいしかわからないですね。僕も答えは持ってませんが、SRE チームをうまく構築するスキルをもったコンサルのニーズは高そうだなあと思いました。スクラムマスターとか技術顧問みたいな感じで。

関連資料

スライドの中で触れた、Backlog SRE による過去の発表資料、ブログ記事はこちらです。

speakerdeck.com

nulab-inc.com

この発表内容がまとまるまでの経緯

スタディスト(ヌーラボ東京事務所の1つ上のフロア)の北野さんから講演のお誘いを受けて、SRE について考え直すいい機会かな、と思ってとりあえずOKしたところから始まりました。

そこから、まずは入社時に聞いた SRE チームの歴史のメモからたたき台を作り、週一の SRE チーム定例で毎回レビューをお願いして、発表内容をブラッシュアップしていきました。そのおかげで発表内容がよくなっただけでなく、SRE チーム内で過去の経緯を共有したり、今後のあるべき姿を考えるいい機会にもなりました。

他の会社の SRE チームでも、同じように過去の経緯をまとめてみる(そして SRE Lounge で発表する)のはいいんじゃないかなーと思います。オススメです。

最後に、SRE Lounge 運営の皆さん、今回の会場をご提供くださったウォンテッドリー株式会社の皆さんに感謝します。僕も今後できるだけ SRE Lounge をお手伝いしたいと思います。また会場でお会いしましょう。

ボルダリングジム Rocky がリリースした新アプリ「サテライトロッキー」がすごい!

$
0
0
f:id:muziyoshiz:20181027214926p:plain

去年の8月に同僚から誘われてボルダリングを始めて、いまは週2でジムに行くくらいハマっています。具体的に言うと、無意識にこんなこと言い出してしまうくらいハマってます。

一応IT業界の人間なので、ボルダリングをもっと楽しむのに使えるサービスやアプリって無いのかな……と色々探してるんですが、意外といいのがありません。

そんな中、今月の10日に「Rocky」というボルダリングジムが、「サテライトロッキー」というアプリをリリースしました。このアプリはとても良くできていて、今後ボルダリングジムがこぞって提供し始めるような、デファクトになる可能性を秘めていると思います。

そこで今回は、ボルダリングにハマるとどういうサービスやアプリが欲しくなるかと、その観点から見て「サテライトロッキー」がどれくらいイケてるアプリなのか、をご紹介します。

ボルダリングとは?

ボルダリングを知らない人向けに、話の前提として簡単に説明します。

ボルダリングは、高さ5m前後の壁(屋内)や岩(屋外)を、予め決められたスタート地点からゴール地点まで登って、達成感を味わうスポーツです。細かいルールは色々ありますが、基本はそれだけのシンプルなスポーツです。

実際に体験してもらわないと面白さを伝えるのは難しいので、とりあえず1回、経験者と一緒にジムに行ってやってみてください。僕の知り合いの人なら、呼んでもらえればホイホイついていくのでぜひ行きましょう。

ボルダリングにハマると欲しくなるもの

ボルダリングにハマると、「もっと難易度の高い課題(※ボルダリング用語でコースのこと)を登りたい」と思う一方で、「最近あまりうまくなってない気がする」という壁にもぶつかるものです。

そうなると、だいたい以下の2つのニーズが出てきます。

  • うまい人がどうやってるのか知りたい
  • 自分の成長を記録したい

「サテライトロッキー」が出る以前に、いままで僕がどういうことを試してきたかを、一例として書いていきます。

うまい人がどうやってるのか知りたい

まずはちゃんと知識を得たい、ということで本を読み漁りました。自分で買ったり、会社の同僚(ボル部の部長)から借りたりして読んだなかで、特に参考になったのはこのあたり。

インドア・ボルダリング練習帖 (RS Books)

インドア・ボルダリング練習帖 (RS Books)

完全図解 スポーツクライミング教本 すべてのクライマー必読の教科書決定版

完全図解 スポーツクライミング教本 すべてのクライマー必読の教科書決定版

パフォーマンス・ロック・クライミング

パフォーマンス・ロック・クライミング

  • 作者:デイルゴダード,ウドノイマン,Dale Goddard,Udo Neumann,森尾直康
  • 出版社/メーカー:山と溪谷社
  • 発売日: 1999/04/01
  • メディア:単行本
  • 購入: 2人 クリック: 8回
  • この商品を含むブログ (4件) を見る

Jack中根のクライミング道場 目からウロコが50枚 もっとうまくなるための50のTIPS!

Jack中根のクライミング道場 目からウロコが50枚 もっとうまくなるための50のTIPS!

  • 作者:中根穂高(ジャック中根),江崎善晴
  • 出版社/メーカー:山と渓谷社
  • 発売日: 2018/01/11
  • メディア:単行本(ソフトカバー)
  • この商品を含むブログを見る

しかし、関連書籍はあまり多くないですし、一通り読み終わると「用語は覚えたけど、どういうときにどのムーブを使えばいいのかよくわからない」という状態になりました。

ブログやウェブサイトも探したのですが、初心者の役に立つようなものは山と溪谷社の CLIMBING-netくらい。じゃあ動画を、と思って YouTube で「ボルダリング 解説」などで検索しても、意外とあまり動画がありません。

ボルダリング関係のちょっと変わった動画サイトとして、OnlineObservationというサイトがあります。このサイトは、課題を3Dモデルで確認した上で、クリア動画も見られるのですが、作るのが手間だからか、動画数が少ないのが難点です。

最終的に「これは良い」と思ったのは、自分が行ったジムの名前で YouTube や Instagram を検索して、自分が登れなかった課題を登ってる動画を探すという方法でした。これなら、自分が一度トライしているのでどういう課題かわかるし、自分が登れなかった部分に絞って何回も再生したりできて、勉強になります。

自分がよく行くジムに行っているユーザーの YouTube チャンネルをチャンネル登録したり、Instagram でフォローするのも良いと思います。おかげで、最近は YouTube と Instagram がすっかりクライミング動画アプリになってしまいました……。

自分の成長を記録したい

うちの会社と1つ上の階の会社のメンバで「ボル部」を作って、毎週水曜に3〜5名でボルダリングジムに行ってます。お互いに動画を撮って Google Photo のアルバムで共有しているので、それが成長記録みたいな感じになっています。

ただ、そんなに常にお互いの動画を撮ってられないし(そんなことより登りたいし)、細かい成長度合いはよくわかりません。

そこで、最近になって自分のトレーニング内容を日誌に記録し始めました。これは書籍(「パフォーマンスロッククライミング」や「Jack中根のクライミング道場」)で推奨されていた方法です。主に以下のようなことを書いています。

  • 挑戦した課題の難易度や、その数
  • 完登した課題数
  • フラッシュした(1回で完登した)課題数
  • その日の体の調子

こういうのを記録する良いアプリがあればよかったんですが、結局普通の手帳に書いてます。

アプリ「サテライトロッキー」の機能とイケてるところ

そこで、今回の「サテライトロッキー」です。

「構想から開発、完成まで2年」と宣言するだけのことはある、よくできたアプリになっています。正直に言うと、アプリとしての使い勝手にはまだ改善の余地が多いのですが、Rocky というボルダリングジムのシステムと非常によく噛み合ってます。

公式サイトにも説明はあるのですが、僕の注目するポイントからも機能紹介します。

https://www.rockyclimbing.com/2018/09/20/satelliterocky-news/www.rockyclimbing.com

機能1. 完登した課題を記録して、あとから写真込みで確認できる

ロッキーの店舗でサテライトロッキーを起動すると、自分が完登した課題を登録できる「チャレンジモード」に入れます。店舗にいるかは GPS で判定されます。このチャレンジモードで登録した結果は、あとからいつでもアプリで確認できます。

会員証代わりにアプリを提供しているボルダリングジムはたまにありますが、まずこんな記録機能自体がいままでに無いものです。ロッキーは、サテライトロッキーの提供以前から、すべての課題に対して、難易度順に一意な番号を振っています。そのシステムが元からあるからこそできる。これは結構すごいことです。

f:id:muziyoshiz:20181027215553j:plain:w640

それに加えて、完登した際に、それがフラッシュかどうかも記録できます(フラッシュの場合はカミナリのアイコンが表示されます)。フラッシュかどうかをクライマーは重視するので、そのあたりわかってる人がアプリ開発してるの良いですね。

f:id:muziyoshiz:20181027220030j:plain:w320

さらに、ここから課題番号を選ぶと、なんとその課題の写真も見られます。いままでは気になる課題を自分で撮影したりしてましたけど、その手間もなくなるなんて……。この画像の用意はさぞかし大変だろうと思いますが、ここまで徹底すると他のジムには簡単に真似できない価値になってます。

f:id:muziyoshiz:20181027220059j:plain:w320

機能2. 全来店者でのランキングを確認できる

先ほど書いた通り、ロッキーではすべての課題に対して、難易度順に一意な番号を振っています。自分が完登した課題の上位10個の合計値でランキングを競う「集大成」というシステムがあり、ランキングの集計結果が約2ヶ月に1回公開されています。

www.rockyclimbing.com

これと同じシステムがサテライトロッキーにも実装されています。違うのは、ランキングの集計が1日1回に変わったこと。

いままでは2ヶ月に1回の更新だったので、他のクライマーとのポイント差を意識するようなことはあまりありませんでした。しかし、頻度が上がったことで、自分と近いレベルの人(例えばよく一緒にジムに行ってる友達)との接戦を楽しんだりできそうです。

これは、アプリで新たに実装した機能というよりは、もともとロッキーというジムに存在していたシステムをアプリに導入したものです。この機能も、他のジムが真似しようとしたら、ジムのシステムから見直す必要があり、容易ではなさそうです。

f:id:muziyoshiz:20181027220748j:plain:w320

機能3. 他のクライマーをフレンド登録して、フレンドのチャレンジ結果も確認できる

最後に、このサテライトロッキーには SNS のようなフレンド機能があります。現状では、フレンド(フォローした相手)の完登記録を確認したり、次の来店予定日を共有できたりするようです。

サテライトロッキーの開発側は、グレードの高いクライマーをみんながフォローするようになり、グレードが上がるほどフォロワーが増えてモチベーションが上がる、といったことを考えているのかもしれません。

僕のような普通のクライマーにとっては現状ではフォロワーとフォロイーを一覧できる程度ですが、この機能も、発展させていけばかなり面白くなるんじゃないかと思っています。

f:id:muziyoshiz:20181027221254j:plain:w320f:id:muziyoshiz:20181027221305j:plain:w320

サテライトロッキーへの要望いろいろ

そんな感じで非常に良いサテライトロッキーなんですが、もっとこうだったら、という点もいくつかあります。

要望1. 挑戦したけど完登できなかった、という記録も残せるようにしてほしい

サテライトロッキーは「自分の成長を記録したい」というニーズをかなり満たしてくれているのですが、現時点では「挑戦したけど駄目だった」記録は残すことはできません。

ロッキーには半年以上通っていますが、3回目かの来店でやっと完登できた課題や、2ヶ月以内に完登できずに壁を張り替えられてしまった課題なんかもありました。そのときの課題も、記録に残しておいてあとから見返せるとちょっと嬉しいです。

要望2. 課題から Youtube, Instagram 動画へのリンクを張れるようにしてほしい

上記のように記録機能は充実している一方で、「うまい人がどうやってるのか知りたい」というニーズはまだあまり満たしてくれません。

せっかく他のクライマーの完登記録を見る機能があるので、そこから一歩進んで「この課題の完登動画」へのリンクを登録できるようにしてほしいです。例えば、自分が完登した課題を選択したら、そこから Instagram に完登動画を登録できる、とか。

課題と動画を紐付けられれば、課題のページにその課題の完登動画をリストアップできるようになります。そうしたら、クリアできない課題があったらサテライトロッキーを開いて完登動画を見て再チャレンジ……ってことがその場ですぐできるはず。

あと、現在でもプロフィールに Instagram アカウントへのリンクを設定できるようになってますが、YouTube チャンネルへのリンクも設定できるといいんじゃないでしょうか。

要望3. フォロー関係を使って各種 SNS に投稿された動画を通知してほしい

要望2の延長で、もしフレンドが完登動画を登録したら、それを通知してもらえると嬉しいです。

今でも Instagram アカウントでフレンドを直接フォローしてしまえば、もちろん動画は見られます。ただ、Instagram にはクライミング関係以外にもいろいろ投稿するのが普通なので、クライミング動画を見たいときはサテライトロッキーを見る、という感じで使い分けられるといいんじゃないでしょうか。

まとめ

サテライトロッキーは、「自分の成長を記録したい」というクライマーのニーズを満たしてくれるアプリです。また、フレンド機能が今後発展していくと、「うまい人がどうやってるのか知りたい」というニーズまで満たしてくれる可能性も持っています。

この記事を読んでちょっとでも興味をもってくれたら、ぜひ以下の公式サイトからサテライトロッキーをインストールしてみてください。そして一緒にボルダリングしましょう!(ダイレクトな勧誘)

艦これアーケードイベント全5回分の攻略率・周回数の時系列データ(CSV)を公開しました

$
0
0
f:id:muziyoshiz:20170128164643p:plain

Admiral Stats とは?

僕は2016年9月から2年以上、艦これアーケードのプレイデータ管理ツール Admiral Stats を開発・運用しています。Twitter アカウントと、艦これアーケードをプレイ済みの SEGA ID を持っている人なら、誰でも使えるサービスです。

www.admiral-stats.com

このサービスを作った経緯などに興味のある方は、Admiral Stats カテゴリーから過去のブログ記事をどうぞ。

艦これアーケードの期間限定海域とその攻略率

この2年以上の間に、期間限定海域(約1〜2ヶ月だけプレイできる特別なステージ)が5回リリースされており、その5回すべてのプレイデータも溜まっています。各難易度の最終的な攻略率は、Admiral Stats 上で誰でも見られるようにしています。

例えば、これは最新の第5回イベントの攻略率です。最高難易度(甲)をクリアしたプレイヤー(提督)は、全体のうち49.6%いるというのがわかります。

イベント攻略率(AL作戦/MI作戦 Extra Operation)

過去5回のイベントすべての、「甲」難易度の攻略率だけ並べるとこんな感じ。

イベント名 前段作戦 後段作戦 Extra Operation 備考
第1回「敵艦隊前線泊地殴り込み」 56.3 % - - これは乙の攻略率(第1回は丙・乙のみ、作戦の区分なし)
第2回「南方海域強襲偵察!」 57.9 % 57.5 % - 第2回は前段・後段のみ
第3回「索敵機、発艦始め!」 72.4 % 72.1 % - 第3回は前段・後段のみ
第4回「決戦!鉄底海峡を抜けて!」 71.8 % 62.0 % 56.1 %
第5回「AL作戦/MI作戦」 66.9 % 65.7 % 49.6 %

ここから難易度の傾向がある程度わかります。例えば、第4回イベントは後半になればなるほど難しくなっている、第2〜3回と第5回イベントは前段と後段の難易度がほぼ同じらしい、等々。

SEGA 公式はこういうデータを公開していないので、(Admiral Stats という特殊なツールを使ってくれるヘビーユーザーに偏っている可能性が高いとはいえ)貴重なデータです。

イベント期間中の攻略率・周回数の時系列データを公開しました

こんな感じで、イベント終了時の最終的な攻略率は、Admiral Stats のイベント攻略率ページ上で誰でも見られる状態にしていました。

ただ、この攻略率が、時間経過とともにどういう感じで上がってるかは公開してませんでした。例えば、最終的な攻略率が同じ60%でも、

  • 最初の1週間で攻略率が60%になって、イベント終了までそのまま横ばい
  • 最初の1週間で攻略率40%、そこからじわじわと攻略率が上がって終了時に60%

では、後者のほうが難易度が高かったと言えそうですよね。

Admiral Stats 上でそういうグラフを見られるようにしようかと思ったんですが、そこまで実装する時間がなかなか取れなかったのと、どういう切り口でグラフを書いたら面白いか思いつきませんでした。

なので、今回、イベント攻略率・周回数に関する統計情報を CSV ファイルとして公開しました。ぜひ自由にいろいろ集計して、面白い結果が出たら Twitter やブログで紹介してください!

gist.github.com

なお、イベント開始から、Admiral Stats での対応までに時間がかかったために、最初の数日間のデータが欠けているイベントもあります。そのへんは非公式ツールの限界ということでご容赦ください。

この CSV ファイル内の統計値の詳しい計算方法が気になる方は、こちらのコード(↓)をどうぞ。

CSV ファイル作成バッチのソースコード

具体例:この CSV ファイルからこういうグラフを描けます

Admiral Stats にプレイデータを登録した人の数は、イベント開始時から終了時に向けて徐々に増えていきます。このプレイデータ登録者数と、攻略率の推移を比べてみましょう。

第1回イベントで、プレイデータ登録者数と、乙難易度(第1回での最高難易度)の攻略率を比べるとこうなります。

f:id:muziyoshiz:20181129211937p:plain

一方、最新の第5回イベントで、プレイデータ登録者数と、甲難易度の攻略率を比べたものはこちら。

f:id:muziyoshiz:20181129211959p:plain

この棒グラフの傾きが緩やかだと、クリアまでに時間がかかった提督が多い、つまり難易度が高いと言えそうです。そう考えると、

  • 第1回イベントは開始1週間程度で最終的な攻略率まで上がったので、難易度が低かったかも?
  • 第5回イベントはEOが一番簡単で前段が一番難しかったのかも?

といったことがわかります。そこから掘り下げて、でも第5回イベントでEOの最終的な攻略率が49.6 %と低かったのはなぜ…?とか考察していくと面白そうです。

この CSV ファイルには、1回クリアした提督が、同じ難易度を周回した回数のデータも入っています。攻略率に加えて、周回数でもグラフを描いてみると面白い結果が出ると思います。ぜひお試しください!

Viewing all 107 articles
Browse latest View live