Google Cloud Next'17 で GCP の中の人達と話してきた

こんにちは。村田 (yuichi1004) です。スマホアプリが PC でプレイできるプラットフォーム AndApp の開発に携わっています。App Engine + Go が好きです。

この度、3/8-10 にサンフランシスコで行われた Google Cloud Next'17 (Next) に参加してきました。その中で、『Next は GCP の中の人と話をする絶好の機会である』ことが分かったので、その話をしようと思います。

Nextとは

各地で開催される Google Cloud Platform (GCP) の技術発表会です。昨年までは GCP 中心のイベントでしたが、今年からは GSuite を中心としてエンタープライズ系の技術発表も兼ねたイベントとなりました。その関係もあってか、昨年の 2,000 人を大きく上回る 10,000 人規模で開催されました。

IMG_3016.JPG

我々 DeNA は、積極的に GCP を使っています(弊社のGCP導入にあたってはGoogle Cloud Platform Japan Blog の紹介記事をご確認ください)。今年は特に本腰を入れて、総勢 12 名でイベントに押しかけ、情報収集とディスカッションにあたりました。

Next は中の人と話す絶好の機会

Next の情報はすでに各種メディアで公開されていますし、各セッションの動画 は無料で公開されています。ですから、話を聞きに行く・情報を仕入れるだけならばわざわざカリフォルニアまで出向く必要はありません。Next の真髄は GCP の中の人と話せる絶好の機会 であるということです。

GCP の各セッションで登壇する人は各プロダクトを担当するプロダクトマネジャーやエンジニアになります。セッション以外にも、プロダクトの紹介ブースや、Meet The Expert というコーナーもあります。普段自分たちが使っている製品や興味のある製品を開発している人たちと直に話せるということです。

「Cloud Spanner のインスタンスって何なの?」と質問をぶつけてみました

自分は Cloud Spanner に特に興味をもっているのですが、どうしても Cloud Spanner の「インスタンス」や「ノード」が何を指しているのか分かりませんでした。そこで Cloud Spanner のブースに行ってプロダクトマネジャーとセールスエンジニアにこの疑問をぶつけてみました!

cloud-spanner-instance.JPG

担当者がその場で図を描いて事細かに説明してくれました。Google の Spanner の論文 と見比べてみると、ノードとは論文で言うところの Spanserver であり、インスタンスはノードの集合のようです。

ここで大事なことは、Cloud Spanner 上でノード数を 1 と指定すると、3 つのゾーンにそれぞれノードと書かれた箱が描かれる点です。つまり、 特定の物理マシン上で稼働するソフトウェアではなく、3 つのゾーンに分散して動作するソフトウェアスタックをノードと呼ぶ ということのようです。そのため Cloud Spanner は仮にノード数 1 で動作させたとしても 3 つのゾーンに分散して可用性を確保してくれます。

こうしたディープな話をダイレクトに聞くことができることこそが Next 最大の魅力だなと思いました。

英語が苦手、わざわざカリフォルニアまで行けないという方も多いと思います。そういう場合は Google Cloud Platform Community Slack をチェックすることをおすすめします。こちらの Slack にも、プロダクトマネジャーや開発者が目を通しています。質問すると丁寧に返事が返ってきます。

社内報告会も行いました

帰国後、DeNA 社内で Next 参加報告会を開催しました。各自現地で仕入れてきた情報や、ディスカッションを経て得られた情報を共有しました。

internal-report.jpg

GCP の注目度が上がる中、DeNA 社内でも GCP を活用したプロダクトの開発が注目されつつあります。現地のプロダクトマネジャーやエンジニアから得た情報を活かしつつ、今後のより魅力的な製品開発に活かしていきたいです。

ツイート
シェア
あとで読む
ブックマーク
送る
メールで送る

JSONデータ圧縮方式をSnappyからzstdに切り替えた事例紹介

JSONデータ圧縮方式をzstdに切り替えデータ量を38.3%削減した事例、及びマイクロサービスの無停止アップデート事例について紹介したいと思います。

はじめに

JPRゲーム事業本部開発基盤部の池田周平です。先日Rails5対応についてDeNA techブログに投稿した@namusyakaと同じチームで働いています。

JSON文字列をRDBに格納する際の圧縮フォーマットをSnappyからzstdに切り替え、データ量を削減した事例を紹介したいと思います。本対応を実施した目的はDB負荷対策です。DBで扱うデータをより小さくすることで、DBサーバのDiskI/O負荷とMaster-Slave間のレプリケーション遅延対策を目的としています。

「Sakasho」は、DeNAが持つモバイルゲームのためのプラットフォームです。複数タイトルのゲームを取り扱っており、一部データはゲーム毎の仕様差を吸収し柔軟に取り扱うため、あえてスキーマレスなJSONを採用しています。JSON文字列に圧縮を掛けRDBに保存しているのです。RDBにJSONを格納している狙いはデータ不整合を避けるためで、トランザクションを張りデータ操作しています。

Snappy vs zstd

サンプル数はそれぞれ500万件くらいで、平均6Kbyte 最大2MbyteのJSONデータに対する圧縮時のログを集計して計算しました。結果データ圧縮率はzstdが12.4062% Snappyが20.1272%でした。Snappyからzstdに切り替えた結果データ量が38.3%削減されました。またAPPサーバのCPU使用率は反映前後で変化がなくサーバ追加等の対応は発生しませんでした。

あくまでDeNAで運用しているサービスの1事例です。正確な情報はZstandard公式ページをご覧ください。

Snappyとzstdについて

zstd正式名称Zstandardは、Facebookが2016年に公開したBSDライセンスのリアルタイム圧縮アルゴリズムです。リアルタイム圧縮とはデータを高速に圧縮と解凍することに主眼を置いたアルゴリズムであることを意味しています。公式ドキュメントによるとzlibと比較して圧縮率は変わらず、圧縮速度3.9倍、解凍速度2.8倍の性能です。またトレーニング機能を有し、これはデータ毎に固有辞書を生成する機能で、より効率的なデータ圧縮を実現します。

※ 圧縮解凍速度は、Zstandard公式ページに公開されているベンチマークデータから計算しました。またSnappyはGoogleが2011年に公開した圧縮アルゴリズムです。

Sakashoとは

Sakashoは、DeNAが持つモバイルゲームのためのプラットフォームです。 モバイルゲームを開発するために必要な機能を一通り提供し、ゲームの開発の効率化を図るための共通基盤として開発・運用されています。マイクロサービス構成となっており、役割ごとに10の独立したサービスと、管理ツールによって構成されています。

sakasho.jpg

マイクロサービスの無停止アップデート

データ圧縮方式をSnappyからzstdに切り替えるにあたり、無停止でアップデートを実施するためにdeploy方法を工夫しました。

仮にマイクロサービス群に対して順にdeployを行っていくと、後半にdeployするサービスで障害が発生してしまいます。 zstd_deploy_1.png

無停止でアップデートするために、下図のようにdeployを2段階に分け、1段目でSnappyとzstdどちらでもデータをreadできる対応をdeployし、2段目でデータ圧縮方式を切り替える対応をdeployすることで複数のマイクロサービスにまたがる修正を本番反映しました。

zstd_deploy_2.png zstd_deploy_3.png

zstdライブラリの選定

各サービスは主にRubyで開発しています。zstdもRubyから扱う場合がほとんどです。gemに登録されている複数のzstdライブラリのうち、どれを選ぶべきか悩んでいたらruby expertな先輩からnative extentionでビルドしているため、メモリリークの観点で調査するべきだとアドバイスもらいました。

検証結果とコードはこちらです。


# memory_usage_zstd.rb
require 'zstd'

def current_process_memory_usage
  `ps ax -o pid,rss | grep -E "^[[:space:]]*#{Process.pid}"`.strip.split.map(&:to_i)[1]
end

JSONFILE_PATH = 'data_139k.json'
json_data = open(JSONFILE_PATH){ |io| io.to_s }
compressed_data = Zstd.new.compress(json_data)

1_000_000_000.times do |n|
  Zstd.new.decompress(compressed_data)
  puts "loop:#{n} memory usage: #{current_process_memory_usage} Kbytes" if n % 1_000_000 == 0
end

# memory_usage_zstd_ruby.rb
require 'zstd-ruby'
.....
compressed_data = Zstd.compress(json_data)

1_000_000_000.times do |n|
  Zstd.decompress(compressed_data)
  puts "loop:#{n} memory usage: #{current_process_memory_usage} Kbytes" if n % 1_000_000 == 0
end

# gem: zstd (残念ながらメモリリークした)
# https://rubygems.org/gems/zstd/

$ ruby memory_usage_zstd.rb
loop:0 memory usage: 10884 Kbytes
loop:1000000 memory usage: 43200 Kbytes
loop:2000000 memory usage: 74776 Kbytes
loop:3000000 memory usage: 106692 Kbytes
loop:4000000 memory usage: 138616 Kbytes
loop:5000000 memory usage: 169960 Kbytes
loop:6000000 memory usage: 201592 Kbytes
loop:7000000 memory usage: 233584 Kbytes
loop:8000000 memory usage: 265236 Kbytes
loop:9000000 memory usage: 297132 Kbytes
loop:10000000 memory usage: 329136 Kbytes

# gem: zstd-ruby 
# https://rubygems.org/gems/zstd-ruby

$ ruby memory_usage_zstd_ruby.rb
loop:0 memory usage: 10836 bytes
loop:1000000 memory usage: 13060 Kbytes
loop:2000000 memory usage: 13808 Kbytes
loop:3000000 memory usage: 13816 Kbytes
loop:4000000 memory usage: 13944 Kbytes
loop:5000000 memory usage: 13944 Kbytes
loop:6000000 memory usage: 13944 Kbytes
loop:7000000 memory usage: 14176 Kbytes
loop:8000000 memory usage: 14176 Kbytes
loop:9000000 memory usage: 14184 Kbytes
loop:10000000 memory usage: 14184 Kbytes

まとめ

JSONをzstd圧縮してDBに格納したら、Snappyと比較してデータ量が38.3%削減。JSON文字列と比較して87.6%データ削減できました。 Rubyでzstdフォーマットを扱う場合はzstd-rubyがオススメです。

ツイート
シェア
あとで読む
ブックマーク
送る
メールで送る

「Sakasho」のRubyを2.4に、Railsを5にアップグレードしました

はじめに

JPRゲーム事業本部開発基盤部の@namusyakaです。

業務ではDeNAのゲームプラットフォームであるSakashoのバックエンドやインフラ周りの開発・運用をしています。

そして最近アイコンを8~9年ぶりくらいに変えました。よろしくお願いいたします。

さて本題ですが、Sakashoでは今年の2月に管理アプリケーションのRuby・Railsのバージョンの大幅なアップグレードを実施しました。この記事ではそのアップグレード対応について、一つの事例として紹介させていただければと思います。

概略

冒頭でも触れましたが、アップグレードしたのはDeNAのモバイルゲームプラットフォームであるSakashoの機能を制御するための管理アプリケーションになります。

Sakashoとは

Sakashoは、DeNAが持つモバイルゲームのためのプラットフォームです。 モバイルゲームを開発するために必要な機能を一通り提供し、ゲームの開発の効率化を図るための共通基盤として開発・運用されています。

Sakasho全体のアーキテクチャは次のようなイメージです。

sakasho architecture

今回のアップグレードは、この図の右端に位置する管理アプリケーションが対象となります。 ところで、余談ではありますが、SakashoのAPIは原則としてRailsではなくSinatraをベースに実装されています。 機会があれば、そのアプリケーションアーキテクチャについても、今後紹介できればと思います。

管理アプリケーションの特性

主に次のような特性を持ちます。

  • ruby-2.1.4/rails-4.1.1で動作している
  • ゲームプラットフォームが持つ全機能に対する制御系統を持つ。
    • ゆえに、コードベースがかなり大きい。
  • 複数DBを前提とした作りになっている。
    • 一部シャーディングもしている。
  • テストはある程度揃っている

実際の規模感をお伝えするために、rake statsの実行結果を貼っておきます。

+----------------------+--------+--------+---------+---------+-----+-------+
| Name                 |  Lines |    LOC | Classes | Methods | M/C | LOC/M |
+----------------------+--------+--------+---------+---------+-----+-------+
| Controllers          |  19075 |  15518 |     247 |    2224 |   9 |     4 |
| Helpers              |   1879 |   1657 |       0 |     225 |   0 |     5 |
| Models               |  21974 |  16928 |     581 |    1442 |   2 |     9 |
| Mailers              |      0 |      0 |       0 |       0 |   0 |     0 |
| Javascripts          |   3000 |   2763 |       0 |     620 |   0 |     2 |
| Libraries            |  10018 |   7358 |      96 |     395 |   4 |    16 |
| Tasks                |   4416 |   3132 |       2 |      15 |   7 |   206 |
| Config specs         |     23 |     20 |       0 |       0 |   0 |     0 |
| Controller specs     |  45014 |  40239 |      28 |       4 |   0 | 10057 |
| Helper specs         |    850 |    727 |       0 |       1 |   0 |   725 |
| Lib specs            |   9852 |   8384 |       1 |      10 |  10 |   836 |
| Model specs          |  17982 |  15974 |       0 |       3 |   0 |  5322 |
| Support specs        |   1038 |    943 |       2 |      13 |   6 |    70 |
| Validator specs      |    396 |    328 |       2 |       7 |   3 |    44 |
+----------------------+--------+--------+---------+---------+-----+-------+
| Total                | 135517 | 113971 |     959 |    4959 |   5 |    20 |
+----------------------+--------+--------+---------+---------+-----+-------+
  Code LOC: 47356     Test LOC: 66615     Code to Test Ratio: 1:1.4

簡単に見方を説明すると、LOCというのはLines Of Codeの略で、コードの行数を指します。 その他にも、例えばモデルと分類されるクラスが何個あるか、といった情報を読み取ることができるため、参考にしていただければ幸いです。

改修期間

対応のために掛けた期間は着手から本番リリースまで凡そ一ヶ月弱です。 大きく「開発」・「テスト」の2つのフェーズに分けることができ、それぞれにかかった期間としては次のようになります。

  • ruby-2.4.0・rails-5.0.1にアップグレードし、全てのテストケースがpassすること (一週間)
  • QAによるテスト (二週間)

開発期間中のアップグレード作業は私一人で進め、変更点のレビューなどはチームメンバー全体に依頼する形で進めました。 今思うと大分駆け足だった感はありますが、現時点で大きな障害もなく稼働しています。

なお、一応補足しますが、この記事は「テスト」ではなく「開発」のフェーズについての解説となります。

アップグレード戦略

前提となりますが、この管理アプリケーションはruby-2.1.4/rails-4.1.1で動作していました。

それらを目的のruby-2.4.0/rails-5.0.1にアップグレードするために参考にしたのが、Railsが公式に提供しているA Guide for Upgrading Ruby on Railsです。

このアップグレードガイドによると、rails-5にアップグレードするには、対象のアプリケーションはrails-4.2でなければならないようです。 したがって、目標のアップグレードを実施するには段階を踏んでアップグレードする必要があり、まずはrails-4.1.1をrails-4.2.7.1にアップグレードしなければなりません。

実際のアップグレードの流れは次のとおりです。

  • rails-4.1.1をrails-4.2.7.1までアップグレード
  • ruby-2.1.4をruby-2.4.0までアップグレード
    • ruby-2.4にバージョンをあげたらrails-4.2.7.1では動作しなかったので、railsの4-2-stableにこの時点でスイッチしました。
  • rails-5.0.1までアップグレード

一つのフェーズごとにテストが全てpassすることをゴールと設定しています。

そして、各バージョンにアップグレードする前には、可能な限りdeprecation warningsを潰すようにしました。 これは、将来的に削除される機能などを事前に回避しておくことでトラブルを防ぐ意味合いがあります。

また、この戦略を進めるにあたり、リリースブランチとは別にアップグレード専用のブランチを用意するほか、ruby-2.4でテストするための専用のjenkins jobを用意して、通常のリリースフローとは独立する形で作業を進めました。

変更点

この手の作業はbundle updateを通す作業から始まります。 幸いにして5.0.1は去年の12月リリースだったこともあって、ある程度依存しているgemは対応済みで、gemに対してパッチを当てたりforkしたり、といったことは特にする必要はありませんでした。 新しいバージョンが出たばかりのHotな時期にアップグレードするのもいいですが、数年間アップグレードされてこなかった規模の大きいアプリケーションを対象とする場合は、数ヶ月程度の時間を置いてから進めた方がハマりどころが少なくて助かるかもしれません。

次に実際の変更点について紹介します。 しかしながらあまりに数が多すぎるため、特に影響範囲が大きかった変更と、deprecation warningsなどの中から、アップグレードガイドに記載されていないものを中心にいくつか取り上げようと思います。

rails-4.2.7.1対応

元がrails-4.1.1なので、まずはUpgrading from Rails 4.1 to Rails 4.2に従って粛々と対応しました。 Upgrade guideに書いてあることは割愛するとして、主要な変更点を挙げます。

deprecations & minor changes

Primary keyでないidcolumnを持つテーブルにおいて、取得したレコードをinspectした結果、idがnilと表記される

idというカラムは存在するが、primary keyではないケースで発生する不具合です。 不具合といってもinspectの結果がおかしい程度ではあるのですが、次のようなコードで正常にidが出力されない問題がありました。

Book.find_by(id: 1).inspect #=> "#<Book id: nil, ...>"

これについてはRails側を修正することで対応しました。たまたま見つけた不具合という感じです。

Pull Request: Fix inspection behavior when the :id column is not primary key by namusyaka · Pull Request #27935 · rails/rails · GitHub

t.timestampsのデフォルトがNULLを許可からNOT NULLに変更

次のように指定することで解決しました。

create_table :books do |t|
  t.timestamps null: false
end

従来どおりの挙動にしたければnull: trueとします。 動的にtableを生成するケースなどがもしあれば、要注意といえるでしょう。

JSON.loadnilを渡さないように修正

4.2.7.1では例外が出るようになっていました。 nullが返されることを期待しているわけでもなかったので渡さないように変更しました。

関連: Fixed JSON coder when loading NULL from DB by chancancode · Pull Request #16162 · rails/rails · GitHub

MySQL-5.6以上では時刻のミリ秒を保存するようになった

MySQLにミリ秒を持たせるのは別の意味でしんどくなりそうだったので、モンキーパッチで回避しました。

参考

ActiveRecord/ActiveModelが範囲外の値に対して例外を吐くようになった

例えば、次のようなコードについて考えます。

Book.where(id: -1).first

この処理はrails-4.1と4.2で挙動が異なり、4.1ではnilが返り、4.2以降はActiveModel::RangeError: -1 is out of range for ActiveModel::Type::UnsignedInteger with limit Xといった例外が発生するようになっています。

ActiveModelにtype castingの機構が実装されたお陰だと思います。 ロジックに問題のあるコードがこれで顕在化されることになるため、これを機に既存の実装を見直しました。

ところで、この機構は明示的にlimitが指定されていない場合に、signed int(4bytes)を自動的にリミットとして定めてしまうという問題があります。

参考: Avoid RangeError without explicit limit by kamipo · Pull Request #26302 · rails/rails · GitHub

Adequate Recordの影響で動的にテーブル名をセットしているところが壊れた件

Adequate Recordはactive_record/core.rbに定義されている.find.find_by.find_by_xxx系のメソッドに対して、ActiveRecord::StatementCacheを用いてキャッシュを有効化する機能を指します。 Arelを経由したSQLへの変換を行わなくて良くなるので二倍ほど高速化が見込めるという話のようです。

この機能自体は非常に素晴らしいのですが、これを実現するために内部的に使用されているキャッシュキーはprimary keyをベースにしたものとなっており、テーブル名については一切考慮されていませんでした。 したがって、テーブル名が動的に変わった場合もprimary keyが一致すればキャッシュが効いてしまうので、誤ったSQLを発行してしまうリスクが存在しています。

無論そんな使い方をするなという話もあるかもしれませんが、動きそうなところが動かないのは困るということで、Railsを修正しました。 直し方としてはtable_name=が実行されたらキャッシュをリセットするように修正する、というものです。

Pull Request: Make table_name= reset current statement cache by namusyaka · Pull Request #27953 · rails/rails · GitHub

ちなみにこれを回避するには、Adequate Recordが効かないQueryMethodsを使ってやるだけで良いので、次のように書き換えるのが簡単です。

Book.find_by(id: 1) # before

Book.where(id: 1).first # after

ruby-2.4.0対応

rails-4.2.7.1の対応を終えた後に、ruby-2.1.4からruby-2.4.0にアップグレードしたところ全く動きませんでした。 どうやら4.2.7.1はruby-2.4.0に対応していないようで、ruby-2.4にアップグレードする前に一旦4-2-stableを使うように変更しました。

なお、私が対応していた時点では4.2系のバージョンの中では4.2.7.1が最新版でしたが、つい先日4.2.8がリリースされ、公式にruby-2.4がサポートされています。今後アップグレードする際には4.2.8を使用することをオススメします。

さて、この項ではruby-2.1.4からruby-2.4.0にアップグレードしたことで発生した主要な問題点を挙げてみます。といっても、Railsほど苦労はなかった印象です。

FixnumIntegerに統合

軽く書いてはいますが、native extension系のgemは要注意です。 以下は具体例です。

アップグレードする対象のアプリケーションが依存するgemのruby-2.4 対応状況については事前に洗い出しておいた方が良さそうです。 必要であればパッチを送り、新バージョンのリリースをねだりましょう。

サブクラスの抽出に自前のObjectSpace#each_objectではなくActiveSupportのコア拡張であるClass#subclassesを使うように変更

ruby-2.3.0からObjectSpace#each_objectがシングルトンクラスを含むようになりました。 SakashoではClass#subclassesの自前実装をObjectSpace#each_objectをベースに持っていて、そちらを修正しようとも考えたのですが、ActiveSupportのコア拡張でほぼ同じことをやっていたので、そちらを使うようにして修正しました。

参考

openssl系の標準添付ライブラリにて、keyやivの文字数がvalidでないと例外が発生するようになった

もともとの実装では、文字数がオーバーしている場合は丸められていたようです。 したがって、事前に文字列を適切にsliceするように変更して修正しました。

参考: openssl: make Cipher#key= and #iv= reject too long values · ruby/ruby@ce63526 · GitHub

rails-5.0.1対応

rails-5.0.1にアップグレードする際にも、まずはUpgrade guideに沿って進めました。

5へのアップグレードには設定ファイルの追加やinitializerの追加などを含むため、そのあたりを中心に対応を自動化するための機構として、app:updateというRakeタスクが提供されています。 対話的に大きな差分を自プロダクトに反映していくことになりますが、ある程度既存のコードにも影響のある部分なので、しっかり差分を注視しながら進めた方が良さそうです。

このフェーズについても、Upgrade guideの内容は割愛し、主要な変更点を挙げていこうと思います。

deprecations & minor changes

ActionController::ParametersがHashを継承しなくなった

多くの場所で取り上げられている変更点ですが、例に漏れず対応が困難だったもののうちの一つです。 Hashが持つメソッドや振る舞いに依存しているコードの多くが正常に動作しなくなるため、アップグレードする際には事前にその辺を洗い出しておくと楽かもしれません。

参考: Make AC::Parameters not inherited from Hash by sikachu · Pull Request #20868 · rails/rails · GitHub

datetime_selectに入力された値をDateTimeとして取得する際に、受け取った値をそのままhidden_fieldtext_fieldに渡すとRubyのHashをinspectした値がvalueに埋め込まれてしまう件

例えば次のような要素があるとします。

= f.datetime_select :opened_at, use_month_numbers: true, start_year: default_start_year

このヘルパーは、日付にかかる情報を扱うために複数のselect要素をレンダリングします。 そして、opened_at(1i)opened_at(2i)...といった複数のキーからなるopened_atにかかる情報をサーバに送信し、それらのパラメータをActiveRecord::Baseインスタンスに渡してやることで、よしなにopened_atに時刻情報が代入されるという作りになっています。

これはもとからRailsが持つ機能ではありますが、Sakashoでは、更にこのsubmitされた値を確認画面などでhidden_fieldに埋め込むケースが非常に多いです。

サンプルとしては、次のようなコードになります。

= f.hidden_field :opened_at

form_forなどに代表されるこれらのform methodは、第一引数で受け取ったカラム名を用いて、対象となるインスタンスに対して実行し、その結果をvalue属性に代入します。 今回はこの代入される値に問題がありました。次のようになります。

<input type="hidden" value="{1=&gt;2017, 2=&gt;2, 3=&gt;27, 4=&gt;21, 5=&gt;0, 6=&gt;0}" name="archive[opened_at]" id="archive_opened_at" />

これは、内部的に実行されるxxx_before_type_castという、その値をキャストする前の値を返す機構に起因します。 今回のケースでは、opened_atにはもともとdatetime_selectによって生成された複数のselect要素群から作られたHashが代入されています。 当然、validationを通過した結果の確認画面ではそのHashがopened_atの値として埋め込まれており、確認画面を経て、いざ作成しようとした際にエラーが発生して保存できない、といった問題が発生することになります。

この問題についてはPull Requestを投げてはいるものの、まだレビュー待ちという状態です。

反省点

今回は短期間で一気にアップグレードを実施しましたが、本来であれば計画的に実施すべきだと思います。 Railsを使ってアプリケーションを作って終わりではなく、定期的に依存するgemのアップグレードに追従しようとする姿勢こそがRailsと付き合っていくコツといえるでしょう。 そこでSakashoでは、bundle updateを自動で行い、アップグレードされたgemの差分を表示しつつ、そのGemfile.lockの変更差分をコミットするPull Requestを自動で作成する機構を導入しました。

これにより、普段の業務でも自分たちが採用しているossのリリースを考慮して取り組めるようになると考えています。

おわりに

大規模なRailsアプリケーションで、かつ長らくアップグレードされてこなかったプロダクトを最新バージョンにアップグレードするのは非常に骨の折れる作業でしたが、短期間で集中的に取り組めたことは良い経験となりました。

また、管理アプリケーションとしての特性上複数DBを前提としているなど、一般的なRailsアプリケーションのそれとは異なる構成であり、それをきっかけとして発見したRailsのいくつかの不具合を修正することができました。

このような環境下で運用されるRailsアプリケーションはそう多くはない認識ですし、Sakashoのような環境でしか発生しない不具合はまだ存在するはずです。Railsを利用する立場として、そういった不具合に積極的に対処してコミュニティに還元していく姿勢が今後重要であると考えています。

最後になりますが、Railsを採用される際には、用法用量を守って正しくお使いください。

ツイート
シェア
あとで読む
ブックマーク
送る
メールで送る

DeNA TechCon 2017 開催レポート【4】

こんにちは。ゲーム事業本部開発基盤部の池田です。

去る2月10日、DeNAは技術カンファレンス「DeNA TechCon2017」を開催しました。
全4回に渡る振り返り記事も本記事で最後となります。

今回は、筆者が聴講した以下のセッションの内容を紹介します:

  • Bステージ「AndApp開発における全て」KOBAYASHI ATSUSHI
  • Dステージ「DeNA内製ゲームエンジンの現状と目指す未来」ERA KAZUTAKA

また、最後のレポート記事ということで、カンファレンスのクロージングの模様なども合わせてお伝えします。

AndApp開発における全て

オープンプラットフォーム事業本部システム開発部のKOBAYASHI ATSUSHIは、2016年11月にローンチされたAndAppというサービスの舞台裏について発表しました。

056_20170227.JPG

AndAppはスマートフォン向けゲームをPC上でプレイできるプラットフォームで、2017年3月現在、15タイトルが配信されています。
そのサーバサイドのシステムは、従来のオンプレミス環境のサービスとは異なり、Google App Engine Standard Environment(以下、GAE SE)上で動作しています。
また、開発言語も従来のPerlとは異なり、GAE SEで利用可能なGo言語が採用されました。

講演の前半では、AndAppの開発の中で、どのように技術選定を行って上記の構成となったかを述べました。その大きな理由の1つとして、開発・運用する上で絶対に必要な工数以外は、サービスを作る工数に当てたかったから、という点が挙げられました。

後半のトピックは、AndAppのシステムアーキテクチャや開発体制、コストについてでした。
アーキテクチャとしては機能単位でコンポーネント化するMicroservices構成が取られ、それに合わせて、少人数チームを数多く作る開発体制が取られました。
Microservicesとして、本講演時点で26のサービスが稼働しています。

講演のまとめの中で、既存の技術や運用にとらわれずにモノづくりに集中できる環境を作ることの重要性が述べられました。
最後に、「一番大切なことは、めいっぱい楽しむこと。ワクワクしよう!」と締め括られました。

スライド資料

DeNA内製ゲームエンジンの現状と目指す未来

Japanリージョンゲーム事業本部開発基盤部のERA KAZUTAKAは、DeNA内製ゲームエンジン「Lift Engine」の現状と目指す未来について発表しました。

DSC_4350.jpg

Lift Engine®Cocos2d-xをベースとして、ゲーム開発において不足していた機能を実装し、最適化を施した内製ゲームエンジンです。
採用タイトルとしては、「デナレンジャー」や「ガールアックス(*1)」があります。

2DゲームエンジンとしてのLift Engineの紹介の後、Lift Engineを3Dゲームエンジンとして使うための取り組みについて、説明しました。

Lift Engine 3Dで新規に実装した機能として、コマンドバッファを用いてレンダリングを専用スレッドで行うレンダリングパイプライン、コリジョン判定やSIMD対応した行列演算を含む算術関数、独自クラスを用いたシリアライザなどが挙げられました。

Arcana」は、Lift Engine 3Dと共に用いられるゲーム開発のためのアプリケーションフレームワークです。
Lift Engine 3Dでは汎用的・基本的な機能の提供に留め、上位レイヤに位置付けられるArcanaによって、よりゲーム機能の実装に即したモジュール群や、ゲームの作り方を規定する仕組み等を提供します。

講演のまとめの中で、「面白いゲームを作るためには、品質向上に時間を割くことが重要」と述べられました。
今後のLift Engine 3Dでは、まずはゲーム開発のトライ・アンド・エラーの周期を短くすることに注力し、開発・運用効率の最大化を目指していきます。

スライド資料

クロージング

カンファレンスを締め括るクロージングでは、今年も弊社取締役の川崎が登壇し、総括と謝辞を述べました。

DSC_7597.jpg

以前のDeNAでは、レガシーな技術にこだわって使い続ける傾向もありましたが、最近はユーザにより大きな価値を届けるため、新しい技術も積極的に採用しています。
本カンファレンス全体を通して、そのようなDeNAの技術に対する姿勢が伝わったなら幸いです。

昨年に続いて今年も会を開催できただけでなく、昨年を上回る規模のご来場を頂けたことを嬉しく思います。
この場をお借りして、御礼を申し上げます。
特にカンファレンスに足を運んで頂いた方々については、誠にありがとうございました。

本記事執筆時点では、まだ各セッションの動画は公開されておりませんが、追ってTechConのサイトにて公開を予定しています。
Twitterの公式アカウントFacebookページによる案内をお待ち下さい。

それでは、来年もTechConでお会いしましょう!

IMG_0390.jpg

スタッフ集合写真

脚注

(*1) ガールアックス - Google Play の Android アプリ

ツイート
シェア
あとで読む
ブックマーク
送る
メールで送る

DeNA TechCon 2017 開催レポート【3】

こんにちは、 SWET グループの加藤です。

DeNA TechCon 2017 が2017年2月10日に開催されました。

DeNA TechCon レポート第 3 回となる本記事では、 DeNA の基盤技術に迫った

の 3 つの発表をご紹介します。

DeNAの動画配信サービスを支えるインフラの内部

024.JPG

IT基盤部の HATA がライブ配信の SHOWROOM とスマートフォンの生放送・実況配信サービス Mirrativ を題材に、動画配信サービスを安定して運用するためのノウハウについて発表しました。

生放送のための Origin サーバと Edge サーバの構成、負荷分散、障害発生時の自動対応、低遅延を実現するための工夫、インスタンスの状態遷移、監視体制、オートスケールの仕組みについてなど、盛りだくさんの内容でした。

特に、オートスケールに関しては、求められるものに応じて何度もスクラッチから実装し直しているとのこと。

こうして作られたインフラ基盤によって、 SHOWROOM 3周年記念の 24 時間生放送や AKB48 45th シングル選抜総選挙、横浜DeNAベイスターズ主催試合生中継など多くの視聴者のいた放送でも安定して配信できたそうです。

スライド: DeNAの動画配信サービスを支えるインフラの内部 #denatechcon from DeNA

DeNAでのチート診断・脆弱性診断の取り組み

134.JPG

セキュリティ技術グループの SUGIYAMA が、サーバー・クライアントで成り立つスマホ向けゲーム開発におけるチート対策について発表しました。

セキュリティ・キャンプでの講義 にも使われたチート対策の学習用アプリを題材に、典型的なチート手法が紹介されました。

この学習用アプリはブラウザで動くため開発者ツールで簡単に改竄できるため、チートを行う攻撃者の視点から脆弱性対応を学べるとのこと。また、ネイティブアプリであってもチートの手法は基本的に同じであり、クライアントに関しては完全なチート防止は無理なので、サーバ側でのデータ改竄を防止することが重要だそうです。

スライド: DeNA_Techcon2017_DeNAでのチート・脆弱性診断への取り組み from Toshiharu Sugiyama

147.JPG

また、スマートフォン向けゲームのチート対策に関しては、同じセキュリティ部の MURAKAMI の発表「SafetyNetを使ってゲームを守る」もありました。こちらもあわせて見ると、より理解が深まると思います。

DeNAの取り組むテストエンジニアリング

157.JPG

SWET グループの OKITA KUNIO が発表しました。 SWET は SoftWare Engineer in Test を意味する言葉であり、 DeNA ではソフトウェアテストをリードする役割を持つ部署です。

テストエンジニアリングに求められるスキルの紹介や、事業サポートとテスト基盤といった、ロールにあわせた SWET 内でのチーム体制が説明されました。

また、新たな取り組みとして、ツールのバージョンアップを監視し、即座に動作確認を行い、アップデートしても安全化をチェックするマスティフというツールや、安定したパイプラインを実現するための Microservices の構成、クローラの探索による自動チェッキングといった挑戦が語られました。

さらに、 AI による E2Eテストの自動化にも取り組んでいるとのこと。現在はまだ学習データを収集するための基盤づくりの段階だそうですが、将来的にはこれまで踏み込まれなかった領域にまで自動化が進みそうです。

最後に

この他にも、 TechCon では数多くの発表がなされました。公式サイトのスケジュール画面 に、発表の動画とスライドへのリンクがありますので、どうぞごらんください。

次回の記事が DeNA TechCon レポートの最終回となります。お楽しみに!

ツイート
シェア
あとで読む
ブックマーク
送る
メールで送る

DeNA TechCon 2017 開催レポート【2】

こんにちは。ゲーム事業本部開発基盤部の池田です。

去る2月10日、DeNAは技術カンファレンス「DeNA TechCon2017」を開催しました。
公開可能な資料については、公式サイトのスケジュール画面からリンクしておりますので、まだチェックしていないという方は是非ご覧ください。
追って、各セッションの動画もアップ予定です。

本記事は、この「DeNA TechCon2017」振り返り記事の第2弾となります。

今回は特に、DeNAの新たなチャレンジ領域である AI 分野について、Aステージで筆者が聴講した以下の3講演について取り上げます:

  • 基調講演:「実世界の人工知能」株式会社Preferred Networks岡野原大輔様
  • 「強化学習を利用した自律型GameAIの取り組み〜高速自動プレイによるステージ設計支援〜」MASHIKO RYOSUKE, SEKIYA EIJI
  • 「DeNA AIシステム部におけるクラウドを活用した機械学習基盤の構築」SEO NAOTOSHI

基調講演―実世界の人工知能

基調講演では、株式会社Preferred Networks岡野原大輔氏が登壇しました。

DSC_6045.jpg

講演の前半では、畳み込みニューラルネットワークが近年の研究でどのように複雑に進化し、ディープラーニング(深層学習)と呼ばれるようになったかを解説しました。
深層学習で使われる畳み込みニューラルネットワークでは、ネットワークの層数やニューロン数が、それまでのものより桁違いに多くなっています。

この深層ニューラルネットワークが、現在、様々な分野で応用されつつあります。

講演では、応用分野として「自動車」「ロボット」「異常検知」「バイオ・ヘルスケア」「コミュニケーション」「クリエーター」といった分野における取り組みについて取り上げられました。

特に、「クリエーター」分野においては、線画にいい感じに着色する PaintsChainer が最近インターネットなどで話題になったことは、記憶に新しいのではないでしょうか。

結びとしては、深層学習・教科学習の進化は著しく、研究段階から実用化・ビジネス化チームが付き添うことが大事と締め括られました。

強化学習を利用した自律型GameAIの取り組み〜高速自動プレイによるステージ設計支援〜

こちらのセッションは前半・後半の2部構成でした。

前半では、強化学習そのものについてAIシステム部SEKIYA EIJIが発表しました。
まず、強化学習の仕組みについて簡単な解説をした後、2014年に登場した新手法であるDeep Q-Networksの概要を示しました。
次に、強化学習に関する最新の動向として、NIPS 2016で発表されたDeepMind LabやOpenAI Universeなどについて取り上げました。

後半では、強化学習の利用例として、FINAL FANTASY Record Keeperにおける自律型GameAIの活用事例について、同じくAIシステム部のMASHIKO RYOSUKEが発表しました。

DSC_6525.jpg

FINAL FANTASY Record Keeperでは、ボスのパラメータ調整を行うため、バトルを自動プレイするAIに対するニーズがありました。

このバトルAIの行動決定アルゴリズムとして、探索的アプローチであるMonte Carlo Tree Searchと、ニューラルネットを用いたアプローチであるNEAT, Q-learningを適用した結果が比較解説されました。
結果として、ニューラルネットを用いた方法において、学習時間が掛かるなど課題はあるものの、人がプレイする場合と遜色ないレベルでの勝率を達成することができました。

講演の最後では、ゲームへのAI活用のポイントとして、ゲームシステムの設計段階でどこまでAIを利用するか考慮し、シミュレータやデータ形式を用意しておくことの重要性が挙げられました。

DeNA AIシステム部におけるクラウドを活用した機械学習基盤の構築

AIシステム部のSEO NAOTOSHIは、DeNAにおけるクラウドを活用した機械学習システム基盤の構築について発表しました。

DSC_6743.jpg

DeNAの機械学習システムのインフラ面においては、「潤沢なGPU」「隔離された環境」「素早い構築」「運用が楽」「自由度を高く」「ミスが起きにくい」という6つの要素が求められていました。
本発表では、これらの要素一つひとつを達成するために、AWSやGCPを活用したインフラ環境の構築方法について示しました。
例えば、「素早い構築」については、TerraformやItamaeといったツールを活用し、AWS, GCPの両方に対応した環境構築をコード化していることが語られました。

発表の後半には、GPU学習環境をオンデマンドでスケールさせるために整備したツールや、APIサービス環境の構成が取り上げられました。
GPU環境をスケールさせるための内製ツール「ec2-scale-run」の中ではDockerが活用されています。このツールでは、使われなくなったインスタンスを再利用し、また不要になったら確実にシャットダウンする仕組みがあることが説明されました。

結びに

本記事を通して、DeNAがAI分野においてどのようなチャレンジ・取り組みをしているか、少しでも伝われば幸いに思います。

余談ですが、Aステージの講演では社員によるグラフィックレコーディングがリアルタイムに行われ、完成したものは展示スペースに貼り出されました。

下の写真は、基調講演のグラフィックレコードとなります。

CR9A7145.jpg

次回の記事でも引き続き、発表されたセッションの模様を紹介していく予定です。
お楽しみに!

ツイート
シェア
あとで読む
ブックマーク
送る
メールで送る

DeNA TechCon 2017 開催レポート【1】

こんにちは、 SWET グループの加藤です。

DeNA TechCon 2017 が2017年2月10日に開催されました。昨年に続き、第2回目です。

今回から全4回に渡り、 TechCon 2017 の様子をお伝えします。

第1回となる本記事では、 Opening の様子、そして SAKAI KENGO による『DeNA private cloudのその後』のセッションについてご紹介します。

CR9A6825.jpg

CR9A6920.jpg

オープニング

IMG_0018.jpg

オープニングでは木村 (hidek) による Dena TechCon の主旨が述べられました。

TechCon は DeNA の技術チャレンジ総決算です。先人の技術と知恵で DeNA はここまで成長したわけですが、その恩返しの意味も込めて、エンジニアの挑戦で得た知見を社会に還元したいという思いが語られました。

特に、 DeNA は、これから力を入れていく AI についてのアウトプットを増やしてゆくつもりです。 AI は社会を変革する可能性を持ちます。 DeNA の創業技術はインターネット関連技術ですが、それに加えて AI も DeNA の技術の中心に据えて、本気でやっていくという姿勢を表明しました。

展示

CR9A6814.jpg

IMG_9874.jpg

CR9A6801.jpg

トークセッションだけでなく、 DeNA の技術を紹介する会場展示も行われました。たくさんの方が、実機ゲームサンプルやキービジュアル、社内開発のミドルウェア紹介の展示に足を止めていました。

DeNA private cloudのその後

IT 基盤部の SAKAI KENGO が DeNA の社内インフラ基盤について発表しました。

スライド: DeNA private cloudのその後 #denatechcon from DeNA

昨年の Dena TechCon で紹介した、サーバやネットワークの構成がこの一年間でどう変わったかに焦点が当てられました。

過去のエンジニアブログの記事にも書かれたように、 DeNA はオンプレミスのサーバにおいて、 Ceph を SDS(Software Defined Storage) として使い、 OpenStack で構築した Private Cloud 構成を取っています。

2016 年 7 月より開発環境を OPenStack へ移行し、現在ではインスタンス数は 1730, Node 数は 34 まで増えたそうです。また、将来的には LXD の導入などを計画しているとのこと。

また、従来はネットワークとサーバの管理は担当エンジニアが分かれていましたが、それを統合し、 IT 基盤エンジニアがネットワーク、サーバどちらも担当できるようにしました。これによりネットワークグループへの「依頼」削減に成功したそうです。

DeNA のインフラに関する記事は こちらこちら など、 Mobage Developer blog にもあります。読み比べると、インフラ構成について理解が深まると思います。

他記事

DeNA TechCon 2017 を紹介した記事を探してみたところ、いくつかの Web ページが見つかりました。

セッションや会場の雰囲気が伝わってきますね。

最後に

DeNA TechCon 2017 に参加されたみなさま、ご来場ありがとうございました!

次回以降は、各セッションの様子をご紹介します。

ツイート
シェア
あとで読む
ブックマーク
送る
メールで送る

First Class Collection

この記事はDeNA Advent Calendar 2016 8日目の記事です。

はじめまして、オープンプラットフォーム事業本部のpospomeです。
普段は GAE/GO の環境でサーバサイドエンジニアとして働いています。
(´・ω・`)

この記事では「First Class Collection」という実装パターンを紹介します。

First Class Collection とは?

First Class Collection は「ThoughtWorksアンソロジー」という書籍で紹介されている「Array, Map をクラスでラップする」という実装パターンです。

以下のようなユーザーのゲームスコアを扱うArrayがあったとします。


$scores = Array(
    Array(
        'user_id' => 1,
        'team_id' => 1,
        'event_id' => 1,
        'score' => 100,
    ),
    Array(
        'user_id' => 2,
        'team_id' => 2,
        'event_id' => 2,
        'score' => 100,
    ),
);

このArrayをクラスでラップします。


class GameScoreList {
    private $scores;
    function __construct($scores) {
        $this->scores = $scores;
    }
}

これが First Class Collection です。
Array を直接触らず GameScoreList を介してArrayを触ります。

ラップすることによって以下の利点が生まれます。
・ArrayとArrayに対する処理を一緒に管理できる
・Arrayをイミュータブルにできる
・Arrayに名前を付けることができる

1つ1つ説明していきます。

ArrayとArrayに対する処理を一緒に管理できる

Arrayのようなデータの集合体は加工処理やフィルタリング処理をすることが多いと思います。
例えば、「特定の userId の score の合計を取得する」「合計 score が一番高いチームに所属する userId を全て取得する」などです。

このような場合、Arrayをそのまま利用してしまうと、Arrayと加工ロジックが分離してしまいます。
似たようなロジックをあちこちに書いてしまったり、
ちょっとしたロジックであれば、Controllerなどにべた書きしてしまうかもしれません。

First Class Collection を利用すると、Arrayに対する処理を一緒に管理できます。
これによってロジックが重複することもないですし、べた書きする必要もありません。


class GameScoreList {
    private $scores;

    function __construct($scores) {
        $this->scores = $scores;
    }

    //特定の user_id の score の合計を取得する
    function getTotalScoreByUserId($userId) {
        //ロジック省略
    }
}

クラスなので、interface を利用して並び替え処理を抽象化することもできます。
以下はイベントの種類によってスコアの並び替え処理が変わるケースを想定しています。


interface ISort {
    public Sort()
}

class xxxEventScoreList implements ISort{
}

class yyyBossEventScoreList implements ISort{
}

ゲームスコアはDBなどで永続化されている事が多いので、コードを書かずSQLで完結することもありますが、
場合によってはコードでフィルタリングすることもあると思います。
そういった場合にとても便利です。

Arrayをイミュータブルにできる

完全コンストラクタにすることで、Arrayをイミュータブルにすることができます。

要素の追加、削除、更新が必要になった場合は以下のように関数を用意してあげることで、
Arrayに対して副作用を与える操作を制限することができます。
追加対象のArrayに対してバリデーションをかけることも可能です。


class GameScoreList {
    //追加はできるが、削除、更新はできない
    function add($score) {
        $this->scores[] = $score;
    }
}

Arrayに名前を付けることができる

単なる Array では「それがArrayであること」しか表現できないので、具体的に「何のArrayなのか」は変数名でしか表現できません。


//単なるArray
$bigBossEventScores = Array();

First Class Collection ではクラス名で表現することができます。
普段チーム内で利用する言葉で命名すれば「それが何なのか」がすぐに分かります。
Array であることを意識する必要もありません。
チーム内で「ビッグボスイベントのスコア一覧」という言葉を利用するのであれば、
以下の BigBossEventScoreList のように言葉をそのまま表現することができます。


$list = new BigBossEventScoreList($arr);

以下のように BigBossEventScoreクラスのArrayである場合、
「ビッグボスイベントのスコアの集合」であることは分かるのですが、
「ビッグボスイベントのスコア一覧」という言葉を表現することはできません。


$bigBossEventScores = Array(new BigBossEventScore(), BigBossEventScore());

引数や戻り値にもクラス名が利用されるので、
単なる Array よりも「それが何なのか」を明確に表現することができます。
ユビキタス言語を利用するDDDでは必須の実装パターンですね。

以上が First Class Collection の利点です。

その他

クラスにラップすることで Array を foreach のようなループで回すことはできなくなりますが、
ループで回す目的はフィルタリングやデータ加工のはずです。
その処理をクラスに持たせることができるので、
Array をループで回せなくて困るというケースは少ないと思います。
ループは可能な限りクラス内に実装しましょう。

Arrayを直接触りたい場合は getter を用意してあげましょう。
ただし、getter で取得した Array に対する処理を外に置かないように気をつけてください。


class GameScoreList {
    private $scores;

    function get() {
        return $this->scores;
    }
}

終わりに

今回は First Class Collection を紹介しました。

Array, Map は int, string よりも保持している情報量が多いので、
それらに紐づく処理が必要になりがちです。
また、言葉で表現できるデータ集合体であることが多いので、
クラスにラップしてあげた方が分かりやすくなるケースもあると思います。

全ての Array, Map をラップするかは迷うところですね。

人によって好みが別れるところだと思うので、
「データに対する処理が必要になったらラップする」
「レイヤをまたぐ場合はラップする」
「ユビキタス言語で自然に表現できるもののみラップする」
など、色々と試行錯誤してみるのがいいかもしれませんね。

ちなみに、golangには primitive type というプリミティブ型に実装を持たせる機能があるので、
以下のようにサラッと実装することができます。
*Array に直接触れてしまいますが・・・


type UserScoreList []int

func (u UserScoreList) GetTotalScore() {
    //ロジック省略
}

長々と説明しましたが、
First Class Collection は簡単に幅広く活用できる実装パターンだと思うので、
知らなかった方は是非試してみて下さい。

明日は yuichi1004 さんです。(`・ω・´)ゞ

ツイート
シェア
あとで読む
ブックマーク
送る
メールで送る

TLS 1.3 と 0-RTT のこわ〜い話

DeNA Engineer Advent Calendar 2016 7日目の記事です。

こんにちは。奥一穂です。

DeNAのゲームサーバが元になっているオープンソースのHTTPサーバ「H2O」の開発や、IETFでプロトコル関連の標準化活動を行っています。

11月に韓国のソウルで開催されたIETF 97では、私たちが提案中のHTTP拡張(Cache Digests for HTTP/2103 Early Hints)の議論の他、TLSの次期バージョンであるTLS 1.3のプロトコル実装に関する相互運用性の確認を行ってきました。

その結果を先月末に開催されたhttp2勉強会 #10で発表してきましたので、こちらにて資料を公開させていただきます。

TLS 1.3 と 0-RTT のこわ〜い話 from Kazuho Oku

昨年制定されたHTTP/2に続き、TLS 1.3は制定秒読み段階、さらにQUICの標準化作業が開始されるなど、ウェブを支えるプロトコルは変革の時を迎えています。新しい技術の得失を理解し、使いこなす努力が求められているといえるでしょう。

ツイート
シェア
あとで読む
ブックマーク
送る
メールで送る

Atomパッケージを作ってみよう

この記事はDeNA Advent Calendar 2016 6日目の記事です。

こんにちは。エンジニアの加瀬です。 普段はモバイルゲームの開発をしております。

皆さんは開発をするときにどのエディタをメインに使っていますか? Vim、Emacs、それともIDEでしょうか。
昔は自分もVimを使っていたのですが、ちょっとしたプラグインを自分で作ってみようと思ったときにハードルの高さにぶつかってしまい、去年あたりからAtomをメインエディタとして使うようになりました。
AtomはCoffeeScript(後述しますが今はJSでもOKです)でパッケージ(プラグイン)を作ることができます。プラグイン作成のためにニッチな言語を習得する必要がないことからパッケージを作るハードルは比較的低いと言えます。

ですが、自分が実際に作り始めてみるとまだまだドキュメントやサンプルが少なかったことから少し苦労しました。ちょっとでもパッケージ作りに興味を持った人が最初の段階で躓いてしまわないように、基礎のところを解説したいと思います。

パッケージジェネレータ

それでは早速パッケージを作ってみましょう。 Atomが標準でパッケージのテンプレートを作るコマンドを用意してくれていますので、まずはそれを実行します。
コマンドパレット(もしくはメニューのPackagesから)からPackage Generator: Generate Packageを実行します。 保存場所とパッケージ名を尋ねられるので、ここではmy-packageとしておきます。
するといくつかのファイルとディレクトリが作られ、~/.atom/packagesと~/.atom/dev/packagesの下に今作られたmy-packageのディレクトリへのシンボリックリンクが貼られます。

packagesはAtom標準以外のサードパーティのパッケージが保存されているディレクトリで、Atom起動時にここのパッケージが読み込まれます。dev/packagesは開発モード時に読み込まれるディレクトリです。

自分が作ったパッケージを公開したり、あるいは誰かが公開しているパッケージをforkして開発する場合にはdev/packagesにシンボリックリンクを貼ってatom --devから開発モードで起動するといいでしょう。

CoffeeScriptかJSか

そもそもAtomが登場した当初はまだCoffeeScriptが流行っていた頃でしたのでCoffeeScriptで書かれたエディタとして話題を集めていたように記憶しています。なのでそのパッケージを作るにも当然CoffeeScriptと思われますが、2016年現在では素のJSを使うこともできます。それどころかES2015で書くことすらできます。

先ほどのGenerate Packageで作られたテンプレートはCoffeeScriptで書かれていました。もしCoffeeScriptではなくてJSで書きたい場合は、package-generatorというコアパッケージのSettingsでPackage Syntaxをjavascriptに切り替えてから再度Generate Packageを実行してください。

本記事ではJSでテンプレートを生成した前提で解説をしていきます。ですがCoffeeScriptの場合でも.jsを.coffee、.jsonを.csonに置き換えて見てもらえば後は文法が少し異なるだけで問題ありません。

package.jsonの解説

まずはpackage.jsonから見ていきましょう。 package.jsonにはこのパッケージのメタ情報や依存ライブラリの情報を書いていくことになります。


{
  "name": "my-package",
  "main": "./lib/my-package",
  "version": "0.0.0",
  "description": "A short description of your package",
  "keywords": [
  ],
  "activationCommands": {
    "atom-workspace": "my-package:toggle"
  },
  "repository": "https://github.com/atom/my-package",
  "license": "MIT",
  "engines": {
    "atom": ">=1.0.0 <2.0.0"
  },
  "dependencies": {
  }
}

大体のメタ情報は最初から書かれている内容を見れば分かると思うので、見慣れないactivationCommandsだけ解説します。

Atomは最初から全てのパッケージを読み込むわけではなく、遅延読み込みが可能になっています。
実は最初から書かれているこのactivationCommandsだけでそれが実現されており、atom-workspaceという場所でmy-packageのtoggleコマンドを実行しようとしたときに初めてmy-packageが読み込まれる、という意味になっています。

atom-workspaceはAtomのどこでもという意味で、toggleはGenerate Packageでテンプレートを生成したときに初めから用意されているコマンドです。 後ほど解説しますが、toggle以外のコマンドを自分で定義すればもちろんそれをactivationCommandsに割り当てることも可能です。

npmも使えるのか?

packages.jsonはパッケージのメタ情報を書くだけではなく、nodejsでの開発と同様にnpmに必要な情報を書くことも可能です。
依存ライブラリはnpm installdependenciesdevDependenciesに自動登録できますし、必要であればscriptsを作って自分でlintやwatchといったコマンドを登録しても問題ありません。

lib/my-package.js

次にパッケージのコマンドがどこで登録されているか見てみましょう。
lib/my-package.jsはパッケージの起動や終了時の処理、コマンドの登録を行う場所になっています。


// ES2015で書くために必要な記述
'use babel';

import MyPackageView from './my-package-view';
import { CompositeDisposable } from 'atom';

export default {

  myPackageView: null,
  modalPanel: null,
  subscriptions: null,

  // パッケージ起動時に実行される処理
  activate(state) {
    // toggleコマンドで表示されるモーダルのViewオブジェクト
    this.myPackageView = new MyPackageView(state.myPackageViewState);
    this.modalPanel = atom.workspace.addModalPanel({
      item: this.myPackageView.getElement(),
      visible: false
    });

    // イベントを管理するために便利なCompositeDisposable
    // Atomの各所で見かけるが今回は省略
    this.subscriptions = new CompositeDisposable();

    // コマンド登録
    this.subscriptions.add(atom.commands.add('atom-workspace', {
      'my-package:toggle': () => this.toggle()
    }));
  },

  // パッケージ終了時に実行される処理
  deactivate() {
    this.modalPanel.destroy();
    this.subscriptions.dispose();
    this.myPackageView.destroy();
  },

  // パッケージの次回起動時に状態を保存しておく処理
  // 今回は省略
  serialize() {
    return {
      myPackageViewState: this.myPackageView.serialize()
    };
  },

  // toggleコマンドの実際の処理
  toggle() {
    console.log('MyPackage was toggled!');
    // モーダルViewの表示、非表示
    return (
      this.modalPanel.isVisible() ?
      this.modalPanel.hide() :
      this.modalPanel.show()
    );
  }

};

自動的に生成されたMyPackageViewにはtoggleコマンドで表示されるモーダルのHTMLが記述されています。今回は解説しませんが、パッケージで使われるView要素はロジックとは別のモジュールに分けられていることが多いです。

my-packageに新しい機能を追加する

ここまででパッケージのコマンドの登録方法とその実装を見てきました。次はいよいよこのmy-packageに新しい機能を追加してみましょう。

今回はAtomが提供している通知機能を使ってこんな通知を出してみることにします。

atom-my-package-notify.png

Atom APIドキュメント

まずはAtomのAPIドキュメントから通知機能を使うためのAPIを調べましょう https://atom.io/docs/api/

通知機能はNotificationManagerというクラスです。ざっと見てみるとaddSuccessやaddInfo, addErrorというメソッドが並んでおり、いかにもそれっぽいですね。
このNotificationManagerはatom.notificationsからいつでも使えると書いてありますので早速試してみましょう。

コマンドの追加

今回はHello World!と表示させるだけのシンプルな通知を出すコマンドを追加します。
addInfo()を呼び出すnotify()というメソッドを作り、既にコマンド登録されているtoggleの下にnotifyを新しく追加します。


// 変更を加えたメソッドだけ抜粋

  activate(state) {
    this.myPackageView = new MyPackageView(state.myPackageViewState);
    this.modalPanel = atom.workspace.addModalPanel({
      item: this.myPackageView.getElement(),
      visible: false
    });

    this.subscriptions = new CompositeDisposable();

    this.subscriptions.add(atom.commands.add('atom-workspace', {
      'my-package:toggle': () => this.toggle(),
      // notifyを追加
      'my-package:notify': () => this.notify()
    }));
  },

  toggle() {
    console.log('MyPackage was toggled!');
    return (
      this.modalPanel.isVisible() ?
      this.modalPanel.hide() :
      this.modalPanel.show()
    );
  },

  notify() {
    atom.notifications.addInfo('Hello World!');
  }

コードを書き換えて動作確認をする前にAtomをリロードしてパッケージを再読込する必要があります。メニューのView→Developer→Reload Windowでリロードできるのですが、これから何度も使用することになるのでショートカットを覚えてしまいましょう。
Macであればctrl + option + command + Lです。

リロードできたらコマンドパレットを呼び出し、MyPackageを検索します。 toggleしか実行できるコマンドがありませんので、とりあえずtoggleを実行します。package.jsonで解説したようにmy-packageは起動コマンドとしてtoggleが設定されていますので、逆にtoggleを実行するまでは別のコマンドを実行できません。
toggle実行後に再度コマンドパレットで検索をすると今度はnotifyも選べるようになっており、実行するとHello World!という通知が出たはずです。とても簡単な機能ですが、my-packageに新しいコマンドを追加することができました!

activationCommands

新しいコマンドを追加することはできましたが、my-packageの機能を使う前に毎回toggleを実行するのはとても不便ですね。この問題はpackage.jsonのactivationCommandsを削除することで解決できます。
activationCommandsを削除してリロードしてから再度コマンドパレットでMyPackageと検索してみてください。今度は最初からnotifyを実行できるはずです。

activationCommandsが存在しない場合、Atom起動時にパッケージが自動的に読み込まれます。 便利になる反面、CPUやメモリをガンガン使う場合は何もしなくても'重い'パッケージになってしまうので、そのような場合はactivationCommandsを設定して必要になるときまでパッケージの起動を遅らせることができないか検討しましょう。

デバッグ

AtomではChromeと同じ開発者ツールを使ってデバッグができます。
メニューのView→Developer→Toggle Developer Toolsか、Chromeと同じでcommand + options + iのショートカットで起動できます(Macの場合)

JSのデバッグには欠かせないconsole.log()や、debuggerによるブレークポイントの設定ももちろん可能です。

開発者ツールのコンソールではAtomで使われているパッケージが読み込まれている状態なので、ここに直接 atom.notifications.addInfo('Hello World!') と入力しても先ほど実装したnotify()と同じように通知を出すことができます。 APIドキュメントを読みながら色々と実験をするときに重宝するでしょう。

テスト

テストはspecディレクトリに存在します。今回は割愛しますが、もしもパッケージを公開するのであればテストも追加した方がいいでしょう。 ターミナル上でapm testを実行するとspecに存在するテストが全て実行されます。

パッケージ公開

一通りの機能ができたらぜひパッケージを公開してみましょう。 今回は解説しませんが、Atomのドキュメントにパッケージを公開する方法が書いてありますのでぜひ挑戦してみてください。
http://flight-manual.atom.io/hacking-atom/sections/publishing/

終わりに

駆け足になってしまいましたが、パッケージ作成の基本を紹介しました。 Atom独自のお作法を覚えてしまえば、後はJS(CoffeeScript)とCSSで書くことができるので学習コストはそれほど高くないと思います。

ですが、パッケージについてのドキュメントは残念ながらそれほど充実している環境ではないというのが自分の感想です。
Atomのパッケージは公式、サードパーティのどちらもGitHub上で公開されていますので、他の人が作成したパッケージの構成やソースコードを読むことが理解への近道になるはずです。 普段自分が使用しているパッケージや、これから自分が作ろうとしているものに近い機能を提供しているパッケージを参考にするといいでしょう。

プライベートや業務でAtomにこんな機能あったらいいな、と思ったらぜひパッケージ作成に挑戦してみてください。

最後に、拙作ではありますがAtom上でVimのタブ機能を再現するパッケージを公開していますので興味を持った方は使ってみて頂けると嬉しいです。
ソースコードもGitHubに公開してありますので、こちらもぜひ見てみて下さい。
https://atom.io/packages/atom-vim-like-tab
https://github.com/Kesin11/atom-vim-like-tab

ツイート
シェア
あとで読む
ブックマーク
送る
メールで送る