Quantcast
Channel: サイバーエージェント 公式エンジニアブログ
Viewing all 161 articles
Browse latest View live

Apache Igniteとのインメモリーコンピューティング

$
0
0

シュティフ ロマン@秋葉原ラボ
@ukrinja

初めに

全世界に無数に散らばったサービスやデバイスから生み出されるデータ量の急激な増大に伴って、ストリーム処理とインメモリーコンピューティングは避けられない近未来であり、重要なデータ処理パラダイムでもあると言える。ストリーム処理とインメモリーコンピューティングの融合により、多様なデータから新しい価値を発見し、新たなビジネスチャンスを引き出した利益拡大が可能である。なぜなら、データの価値はデータの新しさ(freshness)と強い相関関係を持つ。

インメモリーコンピューティングは、Hadoopのようにスポットライトを浴びるテクノロジーではないが、以前数回も期待の技術としてハイライトがされることがあり、メモリーの大容量化と低コスト化が進められる今こそ、その期待度が高まっている。
低価格DRAMは、ディスクより速い。さらに、DRAMのI/Oは一般的にフラッシュメモリーのI/Oより1000x倍速い。

データストレージ観点から、分散キャッシュ中心の以下のような素晴らしいインメモリー技術がある
─ 
Apache Ignite [1]
─ 
Hazelcast [2]
─ Apache Geode [3]
─ Infinispan [4]
上記のソリューションは、データストレージのみならず、メモリー上に格納されるデータの超高速な処理機能も提供する。


Apache Ignite

Apache Ignite In-Memory Data Fabricはリアルタイムで(トランザクションを含め)膨大なデータの処理を行うための高性能分散インメモリープラットフォームである[5]。インメモリーストレージのみならず、Igniteクラスター上でMapReduceのような様々な分散処理を行ったり、Apache Spark[6]との統合ができたりするプラットフォームでもある。

つまり、データグリッド+Key-Valueインターフェース、インデックス検索、自動データ再分配、トランザクションサポート等を備えた計算グリッド。

ApacheIgnite

(画像引用: http://ignite.apache.org/images/apache-ignite.png)

また、Apache Ignite(以下、Ignite)は高いスケーラビリティや耐障害性を実現している。計算ロジックのソースコードをJAR化し各ノードに転送したり、ノードを再起動したりすることを要しないZero DeploymentというP2Pクラスロード技術もサポートしている。

Igniteクラスタ

Igniteクラスタをセットアップするには、Igniteインスタンスを幾つか立ち上げることになる。デフォルト設定のマルチキャストでノード探索が行われる。マルチキャストを利用できない場合、IPアドレスやポート番号リストを用いたTCPディスカバリによりクラスタのネットワークトポロジー構成が可能である。しかし、TCPディスカバリはハードウェア交換、ノード追加などが行われるデータセンターネットワーク内での利用の場合柔軟性に欠けるので、Zookeeperに基づくディスカバリが有効である。

クラスタ内のデータ配置には、partitioningというテクニックが用いられる。また、Igniteは分散コンピューティングでよく使われるConsistent Hashingではなく、Rendezvousとも呼ばれるHighest Random Weight(HRW)ハッシュ化がパティショニング戦略アルゴリズムを使用している。HRWは、人気のConsistent Hashingに比べてよりuniformなデータdistributionを実現できるアルゴリズムとして知られる。
(Consistent Hashingとの比較[7])

クラスタへのリクェストが行われると、ハッシュコードが計算され、そのコードで必要なクラスタノードが発見されることになる。そしてリクェストはそのノードに転送されノード内でデータ処理が行われるのが一般的な流れである。

データ保存手法として、以下の3つがある
- スケーラビリティの高いpartitioned mode
  -- データはアサインされたクラスタのパティションに入る(バックアップ数も指定可能)

- replicated mode
  -- データはクラスタの各ノードにレプリケートされる

- local mode
  -- データはネットワーク上に配信されない。一般的にローカルハッシュに利用される。

データグリッドの用途

サービス開発に様々なIgniteの用途は考えられる。ここではすべてを述べきれないため、ウェブサービス開発に特に有用な機能をピックアップした。

HTTP sessionのキャッシュ

HTTPセッションのキャッシュやクラスタリングはデータグリッドの人気のユースケースである。それによって、サーブレットコンテナやJava EEアプリケーションサーバは
- HTTPセッションへの超高速なアクセスを得る
- アプリケーションノード間やサーブレットコンテナ間のフェイルオーバー、高可用性が可能となる

Spring Caching

Springアプリケーションコンテキスト内でSpringCacheManagerをキャッシュマネージャとして設定してから、@Cacheableアノテーションを利用してソースコード内の引数をキャッシュするのが可能になる。それによってデータグリッドのフェイルオーバー機構を通じてキャッシュされたデータ損失を防げる。

@Cacheable("products")
public Product findProduct(ProductId pid) {...}


たとえば、この例ではfindProductメソッドはproductsというキャッシュと関連付けられる。findProductメソッドは呼び出される度に、このメソッドは以前呼び出されたことがあるかチェックが入る。

データ分析のためのクエリー

IgniteはテキストクエリーやANSI-99規格に準拠するSQLをサポートしているため、人気のある上位nアイテムの発見をはじめ[8]幅広いデータ解析に用いられる。また、Apache Zeppelin[9]と統合されインタラクティブ解析やデータ可視化が可能である。

ベンチマーク

Igniteの一番大きな競合はHazelcast[2]であると思われる。2社共のIgnite対Hazelcastのベンチマークが存在しているが、どれにもグリッドのバージョンが異なるため結論が出ない。

Ignite   Hazelcast  Benchmark
1.0.0    3.5            https://ignite.apache.org/benchmarks/ignite-vs-hazelcast.html
1.3.0    3.6            https://hazelcast.com/resources/benchmark-gridgain/

Apache Igniteを利用した開発

Java、C++ や.NETがサポートされている。また、RESTやmemcachedプロトコルもサポートされているため、他プログラミング言語・環境からの簡単なデータ操作も可能である。

Apache Flumeとの統合


弊社ではBigData処理にログ収集基盤としてApache Flume[10]がよく使われるため、FlumeとIgniteの統合に目を付けた。一般的に、Igniteのストリーム処理には、内部でキーをバッチし適切なノードに配置してくれるIgniteDataStreamerが使われる。一方、Source-Channel-Sinkの概念に従ってFlumeとの統合を実現するのは一般的なので、ここで紹介する統合はIgniteに標準的ではない。

IgniteSink


本実装では、FlumeのSinkを実装しIgniteデータグリッドへのデータ投入を可能にした。最新版Apache Ignite 1.5に含まれる。

https://github.com/apache/ignite/tree/ignite-1.5/modules/flume


セットアップと利用事例


1. 先ず、Flume Channelから受け取ったデータをデータグリッドに格納したい形に変換する処理が必要となる。そのため、以下のようにEventTransformerインターフェースを実装する。

2. ${FLUME_HOME}にあるplugins.d ディレクトリ内で'ignite'ディレクトリ作成する。
3. LogEventTransformerをビルドし${FLUME_HOME}/plugins.d/ignite-sink/libにコピーする。
4. IgniteSinkのJARをはじめ他の関連モジュールを${FLUME_HOME}/plugins.d/ignite-sink/libextにコピーする。


Agent設定に於いて、Sinkの設定は以下のように記述する。

a1.sinks.k1.type = org.apache.ignite.stream.flume.IgniteSink
a1.sinks.k1.igniteCfg = /some-path/ignite.xml
a1.sinks.k1.cacheName = testCache
a1.sinks.k1.eventTransformer = my.company.MyEventTransformer
a1.sinks.k1.batchSize = 100

ソースとチャンネルを記述すれば(Flumeマニュアル参照)Flumeagentをスタートし、データはtestCacheに転送されることになる。そのデータを利用して様々な分析を行うことができるが、極めて簡単な例として、以下のようにContinuousQueryを設けてログにあったエラー通知を行うことができる。

まとめ


この記事では、データグリッドに焦点を当てインメモリーコンピューティングを紹介し、Apache Igniteに特化した実用的な使い方を幾つか述べた。
そのなか、弊社でBigData処理にログ収集基盤として使われるApache FlumeとIgniteの統合の実装や活用例を紹介した。

今後もデータグリッドの動向を見守って他のプロジェクトや新規開発事例を紹介していきたい。


[1] Apache Ignite, https://ignite.apache.org/

[2] Hazelcast, https://hazelcast.com/

[3] Apache Geode, http://geode.incubator.apache.org/

[4] Infinispan, http://infinispan.org/

[5] What is Ignite, http://apacheignite.gridgain.org/docs/what-is-ignite

[6] Apache Spark, http://spark.apache.org/

[7] Rendezvous Hashing, https://github.com/clohfink/RendezvousHash

[8] Ignite Streaming Example, http://apacheignite.gridgain.org/docs/streaming-example

[9] Apache Zeppelin, http://zeppelin.incubator.apache.org/

[10] Apache Flume, https://flume.apache.org/


AWAにおけるDevOps

$
0
0

はじめに

AWAでサーバサイドエンジニアをやっている山下といいます。初めてエンジニアブログを書かせていただきます。よろしくお願いします。

簡単に自己紹介をさせていただくと、2011年にSIerからサイバーエージェントに中途入社し、いくつかのコミュニティ系サービスの立ち上げ・運用、インフィード広告配信APIの作成などを経て、2015年4月からAWAチームに参加しました。

今回は、(今さら?な) DevOpsについてです。といってもスムーズな連携をするには といった話ではなく、現在のチームに参加してからというもの、「エンジニア間の役割が変わってきていて、DevOpsって現状にあってないんじゃないか」と感じており、それについてご紹介させていただきます。

AWAとは


定額制音楽配信サービスです。
スマートフォン、PCでアプリを提供しており、他のデバイスにも順次対応をすすめています。2015年のベストアプリにAndroid,iOS両方で選ばれました。
詳しい紹介はこちらをご覧ください。
同様のサービスとして、AppleMusic、GooglePlayMusic、KKBox、LINE Musicなどがあります。

DevOpsとは

私自身この概念に詳しい訳ではありませんが、調べると、
「開発(Dev)と運用(Ops)が協力し、システム開発をより柔軟、スピーディにし、ユーザー価値を最大化する」
ということのようです。

より具体的な私の解釈としては、
「アプリケーションエンジニア(Dev)とインフラエンジニア(Ops)が密に連携し、爆速開発してユーザー価値を高めよう」
です。


※便宜上エンジニアを以下のように区別します。  

  • アプリケーションエンジニア: サービス仕様を実装してアプリケーションを作る  
  • インフラエンジニア: サーバ作成やドメイン設定などを行う  

私はアプリケーションエンジニアに属します。

これまでの開発の流れと問題点

AWAに来る前までは、ざっくり以下の様な流れで開発を行ってきました。

[アプリ]=アプリケーションエンジニアの作業
[インフラ]=インフラエンジニアの作業
とします。

リリースまで

  1. 担当してくれるインフラエンジニアが決定
  2. そのエンジニアと相談し、システム構成について決定
    (DBはMySQLで何台、アプリはnodejsで何台など)
  3. [インフラ]ドメイン、DNS、証明書の取得・設定
  4. [インフラ]インスタンス、LBの作成・設定
  5. [アプリ]仕様の実装
  6. [アプリ・インフラ]負荷試験
  7. リリース

リリース後:サーバを追加したい場合  

  1. [アプリ]サーバのミドルウェア構成やスペック、台数、期限などを伝える
  2. [インフラ]構築
  3. [インフラ]完了連絡
  4. [アプリ]確認
  5. [アプリ]確認完了




このように、アプリケーションエンジニアとインフラエンジニアの担当領域が明確に分かれていました。私が担当していたのはアプリの部分だけで、インフラ側については依頼するだけでした。
サーバが欲しければ、「nginx,javaが入ったスペックこれくらいのサーバを来週までに2台ください><」と依頼していました。

このやり方では、安心感はあるものの、「依頼→(待ち)→できたよ→確認します→(待ち)→OKです」 といったやり取りが毎回発生し、お互い「連絡待ち」の状態のまま数日たったということもよくありました。特にインフラエンジニアは複数のサービスの担当をしているのが普通なので、今すぐにお願いします!というのも気が引けます。。
DevOpsが目指す「スピードと柔軟性」があまり実現できていない状態です。  

AWAの開発体制

エンジニア以外にも多くの人が関わっていますが、ここではエンジニアだけの紹介にします。  

  • サーバ: 4人
  • スマホアプリ: 各5人
  • Web、PCアプリ: 5人
  • インフラ: 0人

担当インフラエンジニアがいません。
(あくまで担当がいないだけで、色々相談を受けてくれる人はいます。いつもありがとうございます。)

AWAに来て変わったこと

これまでインフラエンジニアに依頼してやってもらっていた部分を、アプリケーションエンジニアが担当しています。
具体的には、

  • DNS、ネットワークの設定
  • インスタンス、LBの作成・設定
  • CDNの設定
  • リソース監視の設定
  • SSL証明書の更新

などです。
まさか自分が証明書の更新を行うことになるとは思ってもみませんでした。

元々、ネットワーク設定周りなどインフラ関連に苦手意識を持っていたので、チームに参加した時そこも担当範囲だと聞き、衝撃を受けました。焦りまくって色々勉強しなおしました。。

自信のないところは、
「ああ、あれね (どれ?)。Aさんどう思う?(ごめんなさい)」
でかわしつつ、その間にこっそり聞いたり調べたりしてます。
今でも苦手意識を克服できたとは言いがたいですが、たいぶマシになったはず、、です。  

使っているツール類

一部ですが、実際にAWAで使用しているツール類をご紹介します。
バックエンドはAWS(一部GCP)を使っています。
サーバ群の管理にはWebコンソールは使用せず、下記のツールを使っています。  

これらのツールの設定ファイルは、Githubに専用リポジトリを作り、バージョン管理をしています。新しく参加したメンバーも、このリポジトリを見れば現在のシステム構成を把握できます。
ただし、terraformはCloudFrontに対応していないので、そこはWebから設定しています。
(2016/02/15現在 プルリクエストは出されている)

例として、新規サーバの作成は以下の手順で行います。

  1. ansibleとpackerでAMIを作成する
  2. terraformの設定ファイルに、インスタンスの種類やサイズ、LB、AutoScalingのしきい値、セキュリティグループ、DNSなどの指定を書き、プルリクエスト&レビューをしてもらう
  3. レビューOK後、terraformを実行してインスタンス作成を行う

上記作業は全てアプリケーションエンジニアが行います。
初めて本番環境に terraform apply した時のドキドキ感といったら。。もし設定をミスっていたら、障害を起こしてしまうわけで、今でもあまりやりたい作業ではありません。。

精神的な健康を保つため、チーム内のローカルルールとして、

  • 実行前に必ず terraform plan の結果をレビューしてもらう
  • 実行時に必ず -target オプションを付与して変更対象以外のサーバに影響がないようにする

などを決めています。
そのおかげか、これまで本番環境で特に大きな障害は発生していません。  

DevOpsがどうなったか

これまでの手順からどのように変わったのか、サーバを追加したい場合を比較します。

  1. terraform設定を書く
  2. プルリクエスト&レビュー
  3. 実行




上述のこれまでの手順とくらべて、「待ち」が減り、実際の作業はアプリケーションエンジニアが行うようになりました。

アプリケーションエンジニア(Dev)がインフラエンジニア(Ops)領域も担当することにより、開発スピードは大きく向上しました。
「依頼→できたよ→確認します→OKです」 のやり取りがなくなり、インフラ関連で何か欲しければ、自分で設定を書いて実行すればよくなったからです。

その分、責任を負う範囲も増えましたが、インフラまで含めたシステム全体を把握しなければならないという意識が強まりました(それができているかは置いといて)。
また、問題が発生した時に、アプリケーションからネットワークまで全体を見て、問題を特定し、解決法を模索、実施できるようになり、「スピードと柔軟性」というDevOpsが目指すものに少し近づけた気がします。

インフラエンジニアとの関わり方の変化

これまでは、インフラエンジニアもサービスのエンジニアチームの一員として、がっつり議論の場に参加してもらったりしていましたが、AWAではそれが変化しており、  

  • 原因が突き止められないトラブルの相談
  • インフラ構成変更のプルリクエストのレビュー
  • DBの新バージョンの検証結果フィードバック

など、少し下がった位置からのアドバイザー的になっています。
障害発生時や負荷試験時になると颯爽とあらわれて、助言・解決したら去っていくようになりました。

「このDBのこのバージョンはこういうバグがあるから1個前のバージョンのほうがいいよ」
「terraformのこのバージョンはこの操作をするとここがおかしくなるよ」
といった、ググっても見つからないようなノウハウを共有してもらったりしています。  

DevOpsの意味と今後

アプリケーションエンジニアがインフラ側に進出するようになったことで、インフラエンジニアはサーバ構築・運用の作業時間が減り、その分、

  • terraformって運用に耐えれるのか?その管理方法は?
  • 属人化しないリリースフローとは?
  • つまり、インフラ構築をどうやってよりスムーズにかつ安全に実現するか?

といった、より本質的な課題の検討・検証にあてる時間が増えてきたと聞きました。

従来のDevOpsが指すDevとOpsはすでに区別がなくなっていて、それを踏まえた上でさらにどうするのかを考えなくてはいけないと感じています。
AWSやGCPなどの、いわゆるクラウドサービスが当たり前になる中で、今後この流れは強まっていくのではと思います。
さらに、AWS Lambda のようなサーバレスの仕組みが当たり前になると、そもそもサーバサイドエンジニアって何?ということになるかもしれません。

まとめ

AWAに参加して感じた、エンジニアの役割の変化についてご紹介しました。

現時点では差別化になっている技術や知識であっても、どんどんコモディティ化していく中で、エンジニアとして今後どうしていくべきなのか?という課題について、引き続き考えていきたいと思います。

最後まで読んでいただきありがとうございました。

iOS用音楽アプリのGoogle Cast対応

$
0
0

はじめに

こんにちは。
AWAのiOS版を開発している岐部と申します。

まず、この記事を書こうと考えた背景から説明します。
AWAには2014年にAWA開発チームが結成されたタイミングでジョインし、それ以来ずっとiOSアプリを開発しています。
Androidアプリを業務で開発した経験はありません。

そんな中、iOS版AWAをGoogle Castに対応させる事になりました。
早速対応方法を調査していたのですが、AndroidアプリのGoogle Cast対応の方法が記載された記事等はそれなりに見つかるものの、iOSエンジニア向けの資料はあまり多くなく、感覚を掴むまでに少々苦労してしまいました。

そこで、シンプルなGoogle Cast対応アプリが作れるようになるところまで、iOSエンジニアの視点で解説する記事を書くことにしました。

Google Castとは

スマホアプリで再生する音楽や動画を他のデバイスに配信する機能の名称です。
主に、家庭にある大きなテレビやスピーカーでコンテンツを楽しむ用途で使用されています。

「Google Cast」という名称をあまり聞いたことが無い方でも、「Chromecast」はご存知かもしれませんね。
ChromecastはGoogle Castに対応している代表的な製品の一つです。

Google Castのシステム構成

Google Castを理解するためには、まず「Sender」と「Receiver」という2つの登場人物を把握しておく必要があります。

Sender = iPhone

Senderには、iPhone等が相当します。
Senderは、Chromecast等のGoogle Cast対応デバイスに、再生すべきコンテンツを指示する役割を持っています。
あくまでSenderは「指示する」のみで、実際に動画や音楽を再生する処理はReceiverに任せます。

Receiver = Chromecast

Receiverには、Chromecast等のGoogle Cast対応デバイスが相当します。
Receiverは、Senderから渡されたコンテンツ情報を基に、動画や音楽を再生する処理を担います。

AirPlayとの違い

Appleにも「AirPlay」という似た仕様があるのをご存知でしょうか。
特にAppleTVをお持ちの方なら、AirPlayを利用した事がある方も多いと思います。

AirPlayはスマートフォン上の動画/音楽をテレビで楽しむための、Appleの規格です。
多少のトラブルはあれど AVPlayer 等のiOSの標準的なAPIを使っていればほぼ自動的に対応が完了するため、対応アプリもたくさん存在します。

Google CastとAirPlayで明確に違いが出るのは、以下の様なシーンです。

機能 Google Cast AirPlay
アプリが終了された後も、テレビ側の再生を継続できるか ×
複数のスマートフォンが同時に接続できるか ×
iTunesライブラリに同期してある楽曲等、ローカル環境にしか無いファイルの再生ができるか ×
アプリのバックグラウンド実行時、iPhoneアプリが再生状態を監視/制御できるか ×


AirPlayはスマートフォンが本体であり、AppleTVを外部ディスプレイとして利用するイメージです。
再生・一時停止等のコントロールは全てスマートフォン側で行います。

一方、Google Castでは再生制御を行うのはあくまでReceiver側であり、SenderはReceiverに対して命令を行うコントローラーのような立場に過ぎません。
更に、実装方法によっては複数のSenderから一つのReceiverに接続するような1:nの接続(!)も可能になります。


この辺りをはじめに理解しておくと、この先も躓きにくいと思います。

注意点

実際に作るときはガイドラインを意識しましょう

User Experience with Google Castのページに、Google Castに対応させる際のガイドラインが記載されています。
この記事ではコード量を必要最低限に抑えるためにガイドラインに沿っていない箇所にも目を瞑って実装していますが、実際にGoogle Castに対応する際はこのガイドラインに準拠するように意識してください。

iOSとAndroidでガイドラインの内容が異なる箇所がある点にも注意が必要です。

Receiverの準備

では早速、実際に制作してみましょう。
まずはReceiver側の準備から行います。

Google Cast SDK Developer Consoleに登録

Google Cast SDK Developer Console に初めてアクセスすると、Welcomeページが表示されます。
この画面で$5(USD)を支払う必要があるので、ご注意ください。

Google Cast SDK Developer ConsoleにReceiverアプリを登録

Google CastはReceiverアプリをカスタマイズすることも可能です。
しかし、今回は話をシンプルにするためにデフォルト状態のReceiverを使用することにします。

Applicationの登録

  1. 「ADD NEW APPLICATION」を選択します。

  2. 「Styled Media Receiver」を選択します。

  3. Receiverアプリに好きな名称をつけて「Save」ボタンを押すと保存されます。

ここまでで、Applicationの登録は完了です。

しかし、まだSenderから接続できる状態にはなっていません。
Senderから接続させるためには、「Publish(※1)」操作を行うか、「デバイスの登録(※2)」操作を行う必要があります。

※1…このReceiverアプリが公開されます。ApplicationIDを知っているSenderであれば誰でも接続できる状態になります。
※2…ApplicationIDを知っているSenderであっても、登録した特定のReceiver端末にしか接続できません。


Publish操作を行うためには、iOSアプリの以下の情報の登録が必要になります。

  • iTunes ID
  • Bundle ID
  • App Launch URI

iTunes IDはアプリをiTunes Connectに登録しないと発行されませんが、今回のアプリはAppStoreに公開するつもりも無いので、Publish操作ではなくデバイスの登録操作を行う事にします。

Receiverのデバイス登録

  1. Developer Console上の「ADD NEW DEVICE」を選択します。

  2. シリアル番号と説明文を入力して、「OK」を選択します。※シリアル番号はChromecast本体の裏面にプリントされています。


以上でReceiverの準備は完了です。

デバイスの登録完了までは、15分程度かかります。
私が試した時は、Statusが「Ready for Testing」になった後、Chromecastを再起動するとSenderから認識されるようになりました。

Senderの実装

次に、いよいよSenderの実装をしていきます。

今回はAppleのSearch APIを使用して、iTunes Storeのランキング情報から再生する楽曲の情報・音源を取得しています。
サンプルコードはこちらです。

sender

※権利関係に配慮してタイトルは「Sound**」としていますが、実際のアプリではトラック名が表示されます。

必要なフレームワークを登録

公式ドキュメント(iOS App Development | Cast | Google Developers)に必要なフレームワークの一覧があるので、それに従って登録していきます。
※GoogleCast.frameworkだけは手動ではなくCocoaPodsでインストールしました。

XcodeのLinked Frameworks and Librariesセクション

Podfile

platform :ios, '8.0'pod 'google-cast-sdk'

Receiverに接続する

接続では、以下の3ステップを踏んでいきます。

  1. 接続可能なReceiverデバイスを検索
  2. 接続するReceiverデバイスを選択
  3. 接続先のReceiverデバイスにアプリケーションを起動させる

1. 接続可能なReceiverデバイスを検索

// Receiverデバイスを検索(Receiver App Idによるフィルター有)self.deviceScanner = GCKDeviceScanner(filterCriteria: GCKFilterCriteria(forAvailableApplicationWithID: YOUR_RECEIVER_APP_ID))// 検索開始if let deviceScanner = self.deviceScanner {    deviceScanner.addListener(self)    deviceScanner.startScan()    deviceScanner.passiveScan = true}

アプリ起動と同時に、Receiverデバイスの検索を開始します。
なお、GCKDeviceScannerの初期化には引数の無い GCKDeviceScanner() の方法も存在しますが、こちらは deprecated 扱いなので使用しないようにしてください。

GCKFilterCriteria の初期化に使用している YOUR_RECEIVER_APP_ID は、Google Cast SDK Developer Consoleで先ほど登録したApplicationの「Application ID」の箇所に記載されているIDです。

2. 接続するReceiverデバイスを選択

画面下部の「Switch Casting」ボタンが押されたら、デバイスを選択するアクションシートを表示します。

let alertController = UIAlertController(title: nil, message: "Select device to cast", preferredStyle: .ActionSheet)deviceScanner.passiveScan = falsefor device in devices {    alertController.addAction(        UIAlertAction(            title: device.friendlyName,            style: .Default,            handler: { (action: UIAlertAction) -> Void in                                // (選択したデバイスへの接続操作/後述)                                deviceScanner.passiveScan = true                self.castButton.hidden = false        }))}alertController.addAction(    UIAlertAction(        title: "キャンセル",        style: .Cancel,        handler: { (action: UIAlertAction) -> Void in            deviceScanner.passiveScan = true            self.castButton.hidden = false    }))presentViewController(alertController, animated: true, completion: nil)

ユーザーがデバイスを選択したら、そのデバイスに接続します。

let deviceManager = GCKDeviceManager(device: device, clientPackageName: NSBundle.mainBundle().bundleIdentifier)self.deviceManager = deviceManagerdeviceManager.delegate = selfdeviceManager.connect()

これでアプリがReceiverへの接続を試みてくれます。
deviceManager.delegate = self を実行しておくことで、接続の状況に応じてdelegateメソッドが呼ばれるようになります。

接続は、以下の2ステップに分けて行います。

  1. Receiverデバイスへの接続
  2. Receiverデバイス上で動作するアプリケーションへの接続
func deviceManagerDidConnect(deviceManager: GCKDeviceManager!) {    self.deviceManager?.launchApplication(YOUR_RECEIVER_APP_ID)}

deviceManagerDidConnectGCKDeviceManagerDelegate のdelegateメソッドで、上記「1. Receiverデバイスへの接続」が正常に完了した時に実行されます。
ここで self.deviceManager?.launchApplication(YOUR_RECEIVER_APP_ID) を実行することで、上記「2. Receiverデバイス上で動作するアプリケーションへの接続」のステップに移行します。

func deviceManager(deviceManager: GCKDeviceManager!, didConnectToCastApplication applicationMetadata: GCKApplicationMetadata!, sessionID: String!, launchedApplication: Bool) {    // (楽曲のロード操作/後述)}

deviceManager:didConnectToCastApplication:sessionID:launchedApplicationGCKDeviceManagerDelegate のdelegateメソッドで、上記「2. Receiverデバイス上で動作するアプリケーションへの接続」が正常に完了した時に実行されます。
SenderとReceiverの接続が完了したので、この段階から楽曲情報をReceiverに渡せるようになります。

APIから楽曲情報を取得する箇所を実装する

ここはGoogle Castに直接関係のある実装ではないので、詳細は省きます。
サンプルコードでは、 DataSourceService 内でSearch APIのJSONから楽曲情報を取り出し、それらを扱いやすいように EntityTrack という独自のEntityの配列に変換しています。

再生したい楽曲をロードさせる

楽曲のメディア情報をReceiverに渡すため、楽曲情報を GCKMediaInformation に変換します。

func generateMediaInformation(track: EntityTrack) -> GCKMediaInformation {    let metadata = GCKMediaMetadata()    metadata.setString(track.trackName, forKey: kGCKMetadataKeyTitle)    metadata.setString(track.artistName, forKey: kGCKMetadataKeyArtist)    metadata.setString(track.albumName, forKey: kGCKMetadataKeyAlbumTitle)    if let url = NSURL(string: track.imageURLString) {        metadata.addImage(GCKImage(URL: url, width: track.imageHeight, height: track.imageHeight))    }        return GCKMediaInformation(        contentID: track.previewURLString,        streamType: GCKMediaStreamType.Buffered,        contentType: track.type,        metadata: metadata,        streamDuration: track.durationInSeconds,        customData: nil)}

変換し終えた GCKMediaInformation のインスタンスを、 GCKMediaControlChannel のインスタンス経由でReceiverに渡します。

self.deviceManager?.addChannel(self.mediaControlChannel)self.mediaControlChannel.loadMedia(mediaInfo) // mediaInfo = GCKMediaInformationのインスタンス

これで無事に楽曲のアートワークがテレビに表示され、音源も再生されるはずです!
※権利関係に配慮してアートワーク画像とタイトルは差し替えてありますが、実際のアプリではトラック名とそのアートワークが表示されます。


ただ、この方法で対応出来るのは単一のメディア情報のみで、音楽アプリのような連続再生には対応できません。
連続再生をさせるためには、 GCKMediaQueueItem を使う必要があります。

再生したい楽曲情報のキューを渡す

複数楽曲の情報をSenderからReceiverに渡すときは、 GCKMediaQueueItem の配列を作ります。

var mediaQueueItems: [GCKMediaQueueItem] = []for track in tracks {    let queueItemBuilder = GCKMediaQueueItemBuilder()    queueItemBuilder.startTime = 0    queueItemBuilder.autoplay = true    queueItemBuilder.preloadTime = 20    queueItemBuilder.mediaInformation = self.generateMediaInformation(track)    mediaQueueItems.append(queueItemBuilder.build())}

変換し終えた GCKMediaQueueItem の配列を、 GCKMediaControlChannel のインスタンス経由でReceiverに渡します。

self.deviceManager?.addChannel(self.mediaControlChannel)self.mediaControlChannel.queueLoadItems(    mediaQueueItems,    startIndex: startIndex,    playPosition: 0,    repeatMode: .Off,    customData: nil)

これで、連続再生にも対応したSenderアプリが完成しました!

おわりに

私が実装し始めた頃の感想は「AirPlay対応と比べると、やることが多くて大変」でした。。
iOS版のGoogle Cast SDKはAndroid版と比較してもカバーしてくれる範囲が狭く、なかなかハードな開発でした。
(例)Castアイコンの表示切替は自分で実装しないといけない、標準のCast dialogが提供されていない など。

しかし、いざAWAのGoogle Cast対応版がリリースされると、ユーザーの皆様から好評な声が結構な数で挙がっていて、とても嬉しかったのを覚えています。
ChromecastはAppleTVと比べると安価なので、その分普及し易いのかもしれません。
自分で使っていても、大きなテレビ画面や良質なスピーカーで自分のアプリが動作している姿は壮観なので、一度試してみる事をオススメします!

最後まで読んでいただき、ありがとうございました。

インタラクションを支える技術

$
0
0

こんにちは。
AWAでAndroidエンジニアをしている新家(ニイノミ)です。

最近インタラクションって言葉、よく聞きますよね。
インタラクションの役割や意味合い等はここでは割愛しますが、「インタラクションをつくる」といった場合、コードを書くエンジニアとしては「動きをつくる」という意味合いが大きいかと思います。

この「動き」について、今まではゲームやFlashを用いたスペシャルサイトなど、世界観の構築やブランディングが必要なサービスで用いられることは多かったものの、SNSなどのスマホのコミュニティ系サービスで取り入れられることはあまり多くありませんでした。
しかし、ユーザーの操作感や体験を重視するようになってきた昨今、コミュニティ系サービスの開発においても「動きをつくる」要件は増えてきました。
実際、僕が開発に携わっているAWAもユーザー体験を重視しており、その一つとしてインタラクションにはこだわって開発をしてきました。

プログラミング言語に備わっているAPIを用いれば簡単に動きを作れることもありますが、技術の移り変わりが激しい昨今、枯れない技術として本質的な部分を理解しておくことは非常に重要です。

そこで今回は、簡単な知識を用いて動きをつくるための方法をいくつか紹介します。
ソースコードは、動きの基本的な部分の紹介にはProcessingを、実際のサービスでの使い方などの紹介にはAndroidを用います。

正規化

正規化とは、値を一定のルールに基づいて変形し、利用しやすくすることです。
対象となる物体を移動させたり、大きさを変える際にこの正規化が結構役立ちます。
ここでは、よく使う3つの正規化を紹介します。

normalize

ある範囲の値を0.0 ~ 1.0の間の値に変換します。
計算式は単純で、(初期値 - 最小値) / (最大値 - 最小値)となります。
processingではnorm()という関数で用意されてます。

lerp

線形補間とも言います。
normalizeとは逆に0.0 ~ 1.0の範囲の値を別の範囲の値に変換します。
計算式は、最小値 + (最大値 - 最小値) * 初期値(0.0 ~ 1.0)
processingではlerp()

map

ある範囲の値を別の範囲の値に変換します。
normalizeとlerpを組み合わせてできています。
lerp(最小値B, 最大値B, norm(初期値, 最小値A, 最大値B))
processingではmap()

この3つの中でもmapは特に便利で、実際にAWAのAndroidアプリでもスクロールに応じたアイテムの位置を自身のサイズに変換するなどの処理でこの計算式を用いています。

Android Java

1.for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
2. View targetView = mRecyclerView.getChildAt(i);
3. float scaleRate, centerY = mWinHeight / 2;
4.
5. if (targetView.getTop() > distance) {
6. // 拡大
7. scaleRate = map(targetView.getTop(), mWinHeight, centerY, 0.9f, 1f);
8. } else {
9. // 縮小
10. scaleRate = map(targetView.getTop(), centerY, 0, 1f, 0.9f);
11. }
12.
13. targetView.setScaleX(scaleRate);
14. targetView.setScaleY(scaleRate);
15.}



画面の中のアイテムについて、

位置 -> サイズ

に変換しています。

このように、あるプロパティの値を別のプロパティに適用するなどの場合に正規化は役立ちます。

速度と加速度

毎フレームごとに位置を更新することで物体を移動させるアニメーションを表現できます。
この移動について、速度と加速度の観点から考えてみましょう。


上は一定の速度で移動しているのに対して、下は徐々に速くなっているのがわかるかと思います。
これは、上は毎フレームごとに速度を位置に加算して等速運動を表現しており、下は位置に加算する速度に加速度を加えているからです。

つまり、加速度は速度に作用し、速度は位置に作用するという関係が言えます。

1.// 等速運動
2.位置 += 速度
3.
4.// 加速運動
5.速度 += 加速度
6.位置 += 速度

これは動きをシミュレートする際の基本的な考え方になります。

イージング

イージングは加速や減速など、動きに緩急をつけるための仕組みです。
イージングを変えることで、加速や減速などのアニメーション表現が可能になり、より現実世界の動きに近づけることができます。


UIを構築するための多くのプログラミング言語では様々なイージングのクラスが用意されており、普段はそれらを利用すれば良いのですが、ここではそのイージングがどのように動きに影響しているかを見ていきたいと思います。

イージングの値は数式によって求められ、この数式によってできるグラフをイージングカーブと呼びます。

下のグラフはy = x^4のイージングカーブです。


x 軸を時間、y軸をイージング値と考えると、最初はほとんど値が変わらず、0.6秒を超えたあたりから急激に割合が大きくなっていることがわかります。
つまり、これは加速のイージングカーブです。

移動する物体にこのイージングカーブを適用すると以下のようになります。


徐々に加速していますね。

イージング値を使って現在位置を求める式は次のようになります。

現在位置 = 初期位置 + (目標位置 - 初期位置) * イージング値(0.0 ~ 1.0)

これを使って、ボールが下から上に500msかけて移動するプログラムは以下のようになります。

Processing

1.static final float FPS = 60;
2.float elapsedTime = 0;
3.float totalDuration = 500;
4.
5.void draw() {
6. if (elapsedTime < totalDuration) {
7.   // 1フレームあたりのミリ秒を加算
8. elapsedTime += (1000 / FPS);
9. } else {
10. elapsedTime = totalDuration;
11. }
12. // y = x^4
13. float rate = pow(getNormalizedElapsedTime(), 4);
14. x = width;
15. // Proccesingでは左上が原点になるので、これを加味して位置を計算
16. y = height - height * rate;
17.
18. // 描画
19. ellipse(x, y, 10, 10);
20.}
21.
22.// 経過時間を0.0 ~ 1.0の範囲に変換
23.float getNormalizedElapsedTime() {
24. return norm(elapsedTime, 0, totalDuration);
25.}

※細かい部分は省略してあります。

このように、式によって得られたイージング値(0.0 ~ 1.0)を物体のy座標に適用することで加速を表現することができます。

他にも式を変えることで動きの緩急を変化させられます。

y = 1 - (1 - x)^4



y = x^0.5

ここで用いたのはシンプルな指数関数だけですが、もっと複雑なイージングの計算式はネット上で色々見つけられます。
例えば、有名なRobert Pennerのイージンング関数なんかはプログラムとしてもすぐに使えそうです。

ベジェ曲線からイージングカーブをデザインする

先ほどは計算式からイージングカーブを作りましたが、複雑な動きを簡単に表現するにはベジェ曲線を用いるのも有効です。
ベジェ曲線とは、N 個の制御点から得られる N - 1 次曲線のことです。

多くの言語ではベジェ曲線からイージングカーブをつくるAPIが用意されているので、これらを用いれば簡単に動きをアレンジすることができるでしょう。

AndroidならPathInterpolatorを用いることができます。
※API level 21

Android Java

1.ObjectAnimator animator = ObjectAnimator.ofFloat(targetView, "translationX", 0, winWidth);
2.animator.setDuration(500);
3.animator.setInterpolator(new PathInterpolator(0.71f, -0.88f, 0.32f, 1.68f));
4.animator.start();

ベジェ曲線の制御点の計算はわかりにくいですが、ベジェ曲線を作成できるサイトがあるので、制御点を移動させながら自分が目指す動きを作っていくと良いと思います。

より詳しいイージングカーブのデザイン方法についてはこちらの記事がわかりやすいです。

グラフソフトを使ってイージングカーブをデザインする

複雑な動きを作るには、複数の数式を組み合わせてイージングカーブを作り、経過時間ごとに適用する数式を変えるような処理にしたほうが良い場合もあります。
Macに標準搭載されているGrapherなどのグラフ描画ソフトを用いれば簡単に数式からグラフを作成することができます。


このグラフをプログラムでイージング値に変える場合、AndroidではInterpolatorを実装した独自クラスを作ることで実現できます。

Android Java

1.public class EaseInOutInterpolator implements Interpolator {
2. @Override
3. public float getInterpolation(float t) {
4. if (t < 0.4f) {
5. return (float)(8 * Math.pow(t, 2));
6.
7. } else if (t < 0.68f) {
8. return (float)(Math.pow(5 * t - 2.8f, 2) + 0.6f);
9.
10. } else if (t < 0.87f) {
11. return (float)(Math.pow(5 * t - 3.9f, 2) + 0.8f);
12.
13. } else {
14. return (float)(Math.pow(5 * t - 4.69f, 2) + 0.9f);
15. }
16. }
17.}

GrapherとInterpolatorを使ったAndroidの実装についてはこちらの記事が参考になるかと思います。

まとめ

駆け足になりましたが、インタラクションを支える技術の基本的な部分からいくつが抜粋して紹介しました。
今回紹介した以外にもベクトルやバネなどの重要な概念が存在するので、調べてみると良いかもしれません。

ProcessingとAndroidごちゃまぜで紹介したのでよくわからなかった人もいるかもしれませんが、基本的な動きを学習するにはProcessingはとても良い言語です。
そして、その基礎を理解した上でフレームワークのAPIを用いることで、エンジニアの書くコードと出来上がるインタラクションが共に良いものになるかと思います。

こういった本質的な動きの作り方に関する情報はまだあまり多くない印象なので、もっとたくさん出てくるといいですね。

最後までお付き合いいただきありがとうございました。

画像システムの車窓から

$
0
0

2年半くらい画像システムを担当していたのですが、3月イッパイで異動することになりましたokzkです。

 

異動記念ということで、とりとめもなくエンジニアブログを書いてみます。長いです。よろしくお願いいたします。

 

画像システムのこれまでのストレージ事情

最初にアメブロ(以下、単にブログ)のユーザ投稿画像関連でのストレージの歴史をアレコレをまとめてみようと思います。なお、以下swiftと書いたらOpenStack Swiftのことです。流行のプログラミング言語のことではありません。


はるか昔の状況

昔は単純にWebDAVを複数台並べ、イッパイになったら更に次の世代のWebDAVを追加する、というような構成で、参照時に画像URLパスに含まれる年月ベースで適切な世代のWebDAVにルーティングしていました。

 

(参考:画像URLのパスの例) /user_images/20160401/00/shibuya/...

 

新規投稿は特定のWebDAV群だけに行って、過去分のWebDAVは参照と削除だけ担当するようなイメージです。

 


swiftツライよ事案

過去の記事ではswiftにリプレースしたなんて、謳ってますねー。

ステキですねー。カッコイーですねー。

 

えー、結論からいうと、導入失敗してます。1度目は派手に。2度目は静かに。


1度目の失敗

swiftって乱暴に簡素化して説明すると、objectサーバ、containerサーバ、accountサーバという構成で、以下のような役割分担があります。

  • accountサーバ: containerリストを管理
  • containerサーバ: objectリストを管理
  • objectサーバ: object実体を管理

実装レベルでみると、account/containerサーバの永続層はsqliteで、objectサーバの永続層は普通にobjectとファイルシステム上のファイルがマッピングされています。

 

でもって最初の失敗ですが、もう単純にswiftの使い方を理解してなくて、ブログの画像を全部一つのコンテナにぶち込もうとしてました。 \(^o^)/ 
account/containerサーバのIO分散なんか一切できていませんし、結果、画像のPUT/DELETEでレイテンシが連日ハネまくるとかの大障害を引きおこしておりました。

 

そんな中、まだ担当ではない私は「画像チーム、マジ大変そーやなー」とか傍目に生暖かく見守っていたのですが、2013年9月頃に一人の担当者の退職に伴い、私も画像チームを担当するハメになりました。 完全にバチがあたってますね。

 

そうやってできあがった200GB弱のsqliteのファイルに衝撃を受けたりしながらも、いろんな事情が重なって結局どうにもならなくなったので、WebDAVを更に追加した構成に切り戻したのが同年10月くらいです。



2度目の失敗

swiftを完全にサービスから切り離したあとは、もう一度swiftの検証のやり直しです。

 

  • IOが分散するようaccount/containerをパスのハッシュ値で分割するようにしたり
  • (分割してるとはいえ)1container当たりのファイル数が多くなり過ぎないように、さらに年月で分割したり
  • アレコレ分割したせいでファイルアクセス方法が複雑化してしまったのを、swift外からは既存の画像パスでファイルにアクセスできるようにしたり
  • 完全にインターナルなので認証周りの処理を外したり

などなどをswiftプラグインを書くことで対応することにしまして、まあそれなりになんとかなりそうな感触を掴むことができました。

 

いよいよ本番導入です。

 

とはいえ、一度完全にやらかしてますので、いきなり新規投稿を受け付けるのではなく、古い世代のWebDAV上の画像のファイルを移行するところから始めました。

 

…  
……  
………  
…………

 

いや、あの、最初は良かったんですよ。最初は。。。。
ファイル移行が進むにつれて、ドンドンPUTのレイテンシがあがるようになっていって(原因は後述)、ちょーっとツライかな。。。なーんて。

 

そんなカンジでファイル移行に手間取っている最中に、一緒に検証したメンバーが異動でぶっこ抜かれて行ったりしてしまって、そもそものswiftの運用もままならない状況に追い込まれていきます。

 

とはいえ過去の分のファイルは移行してしまっていたので、そっとそのまま運用し続けることにしました。2014年の夏頃です。


 

WebDAVツライよ事案

WebDAVとswiftで誤魔化しながら運用している中、画像チームの当時の担当3人のうち2人が偶然同タイミングで退職するという椿事が発生しました。  
そしてその直後に新規投稿を受け付けているWebDAVのIOがハネてしまって、まともに新規投稿を受け付けることができないようになる障害が続きます マジ呪われてる。

 

 とはいえサービス側からするとそんな事情は関係ないわけで「月末までになんとかせい」という指令がその月の最終週にでる中、ジェバンニ新たにチームにジョインした二人と一緒に新規投稿分をAWS S3に逃がすという対応をやりきりました。2014年11月です。

 


ナニがダメだったの?

さてそろそろ画像ストレージの運用で大ハマリしたポイントについての説明なんですが、結論「ファイルシステム上のファイル数が多すぎる」でした。

 

通常のシステムでそんなトコでハマることはないんですけど、画像みたいにソコソコの大きさの画像が大量に保存しているようなシステムの場合、inodeがキャッシュに乗らなくなったくらいから、ものすごくIO負荷(特に更新系)が大きくなります。

 

これが最終的にswiftもWebDAVもどうにもならなくなった理由です。

 

末期では「キャッシュに載ってない状況ではlsコマンドが10秒以上かかる」という状況にまでなってました。

 

えー、そんなわけでー、皆様におかれましてはー、単なるファイル数だけでもサービスを殺せるという事実を覚えていただけるとウレシイですー。  どこで役にたつかは知りませんが。


WebDAVもswiftもツライよ事案

そんなこんなで新規投稿分は札束で解決できる状況にしたのですが、古くからあるWebDAVは保守部材が調達困難なレベルで老朽化してきたり、swiftもswiftで検証したメンバーがチーム内に私しかいなくなり(前述の二人同時退職事件)、完全にオペレーションもままならない状況になってる中、私の方にも「そろそろ異動してほしいんだけどー?」という別方面からの空気を読まないプレッシャーがかかるという状況がやってきてしまいました。

 

というわけで、本格的にどうにもならなくなる前に抜本的な解決をはかることにしました。


ファイル数という暴力に抵抗

ブログでは従来、画像投稿時にサムネイル画像も生成してストレージに保存していたのですが、サムネイル画像は参照時にオリジナル画像から動的にリサイズして生成する方式に切り換えました。

 

すでに参照時の画像リサイズ機能は存在していましたので対応自体はごく簡単だったのですが、コレでなんと管理対象のファイルが半分になりました。(∩´∀`)∩ワーイ

副次効果として保存するファイルが1つだけになったことで、投稿時のレイテンシも改善しました。

 

なお参照は元々CDNに加えて内部キャッシュ(varnish)もある構成だったこともあり、とくに目立った影響ありませんでした。

 

内製オブジェクトストレージの開発

WebDAVのHW老朽化はまったなしです。

 

swiftの運用可能人員を増やしつつ、WebDAVからswiftへの移行する、という選択肢も検討したのですが、移行に1年以上かかるという事実が判明した時点で諦めました。  
すべてS3というのも考えたのですが 「そんなんちーっとも面白くないよな」という個人的ワガママでコスト面を考慮して、内製で新規にオブジェクトストレージを作ることにしました。

 

作ったオブジェクトストレージがどんなモノかは結構長くなるので、次節で紹介します。

 

最終的にはWebDAVとswiftの画像はすべて、4ヶ月ほどで内製オブジェクトストレージに移すことができました。



まとめ

ブログのユーザ画像のストレージの歴史をかいつまんで、説明しました。  

現在は、昨年分のS3上の画像を内製オブジェクトストレージへ移行することで、さらなるコスト削減を目指しています。

 

なお、途中微妙に期間が空いているのは、ブログ以外の画像システムも担当していたり、画像システム以外のシステムを担当していて手が回らなかったからです。  

# 名前を呼んではいけないKVSを使った内製オブジェクトストレージを運用者がいない状態で押し付けられた件でも、日本酒が美味しくいただけます。

(参考)他社事例

先方の内部事情は一切全く存じ上げないのですが、swiftを導入して一年後にヤメたというGREE様のブログ記事は涙なしでは読めませんでした。  

# 美味しいビールを飲みに行きたいです。

 

内製オブジェクトストレージの紹介

前節でちらっと紹介した内製オブジェクトストレージをもうちょっと掘り下げて紹介します。  
とはいっても、別にOSSとして公開しているわけではないですので、設計とか開発の経緯を中心にまとめます。

 

反響があれば社外公開もあるかもしれませんが、私が4月で異動なのでどうなるかはわかりません。

 

要件について。

これまでの経緯を踏まえつつ、作ろうとしている画像ストレージの要件をまとめると、以下の様なモノでした。

  • 新規投稿は無い。
  • 既存ファイルの上書きも無い。
  • 削除はあるが多くはない。
  • 参照は比較的あるが、オンメモリでなければさばけないほどではない。
    • ただし、1オブジェクト1ファイルの設計だと死ぬ。
  • オンプレでやる以上は高集約できるようにしたい。
  • HW老朽化に伴うデータ移行や、DC移設に伴うデータ移行の工数も削減したい。
  • 退職・異動に伴う引き継ぎ工数も削減したい。
    • 複雑な実装・運用はNG

これだけ見ると、正直、全部S3にもっていきたくなりますね。


分散システム?

通常オブジェクトストレージというと何かしらの分散システムを考えると思うのですが、分散システムは分散システム特有のしんどさ(整合性とか、一貫性とか、アンチエントロピーとか)があるため、実装したとしても、シンプルにはならず、運用工数や引き継ぎ工数が跳ね上がることが目に見えてます。

 

そこで「分散システムとしての設計を放棄してひたすらシンプルに作る」よう発想を転換させました。

 

さて、要件をよくよく考えてみると、新規投稿も上書きもないんですから、オンラインの更新トランザクション的なモノは「削除」だけです。  
つまり、以下のように問題を分解することが可能です。

  • 「ファイルに対して削除フラグを立てる」というトランザクションを担当するDB
  • readonlyなちょっとした(!?)静的ファイル置き場

「ファイルの削除フラグ」なんて、DELETEリクエストの分だけレコードinsertすればいいだけなので余裕でメモリにのるサイズです。ということで、なにも考えずにMySQLにぶっ込むことして、残りの「静的ファイル置き場」をどうするかにだけに注力しました。


静的ファイル置き場

前述のようにファイルシステム上で1オブジェクトが1ファイルにマッピングされていると試合終了となります。ということは単純に複数のオブジェクトを一つのファイルにまとめればいいだけですので、以下のようなファイルを作成することにしました。

  • 複数のオブジェクトを単純に直列化してまとめたアーカイブファイル
  • 参照用に画像パスとアーカイブファイル上の位置をマッピングしたインデックスファイル

要は「参照時にパスを元にインデックスからアーカイブファイル上の位置を引いてきて、preadでオブジェクトをガツンとまとめて読み込んでレスポンスを返す」という方針です。

 

当然、参照用のインデックスは完全にメモリ上に展開したいですよね?  

 

ここでもアーカイブファイルがreadonlyであるという利点が生きてきて、あらかじめtrieで辞書をつくるようにしたりとイロイロ工夫した結果、1オブジェクトあたりのインデックスのメモリ使用量は20byte程度に抑えて、全部メモリにのせることができるようになりました。

 

ファイルシステムのinodeのサイズを考えると、1オブジェクト1ファイル構成と比較して大幅な節約ですし、パス長の平均は80byteくらいなので単純なハッシュやtreeと比較しても省メモリです。


こんなウレシイことも!!

アーカイブファイルは一つで数TBなのですが、更新はないのでコピー時に整合性の問題は発生しません。データ移行が「単なるファイルのコピー」だけで済むようになりました。scpやncで雑にコピー可能です。

 

おまけに1オブジェクト1ファイルだとどうしてもランダムIOが発生してしまいますが、でっかいファイルだとシーケンシャルリード/ライトだけで済むので相当なスピードアップも見込めます。

 

また最終的なファイル数も少ないので、データ移行時に地味にメンドクサイ「移行モレのチェック」もとても簡単です。もしモレてしまった場合でも、数千万オブジェクト単位でアクセス不能になるのですぐに判明します。

 

加えて参照時のIOに関しても、個別ファイルのinodeのルックアップ等が不要になったおかげで、WebDAV時よりはるかに高集約化しているにも関わらず、iostatのavgrq-szが一桁増えてくれて、結果、格段に負荷は下がってくれました。

 

以上を踏まえて最終的な構成

最終的な構成は以下のようなカタチです。

 

 

ここまでに至る経緯とかは横において、出来上がったモノだけみてみると、

  • プロキシサーバ: DBを引いてルールベースで適切なデータサーバにルーティングするリバースプロキシ
    • ルーティングに必要なメタデータもDBに突っ込んでます。
  • データサーバ: ローカルのアーカイブファイルからオブジェクトを返すだけのhttpサーバ

本当に面白みのない、たいしたコトない構成ですね。実際、ソースコードもコアだけなら数時間で全部読み込めるレベルです。

 

これは、内製のアプリケーションは「書いた瞬間から負債になる」というやるせない事実を踏まえて、そもそも極限までシンプルにすることでその負債の量を減らす、という設計上の狙いの一つでもあります。

 

ということで「退職・異動に伴う引き継ぎ工数の削減」もなし崩し的に実現出来てる……と評価してもらえるといいなぁ。。。

 

一応耐障害性についても

MySQLはレプリを組んでゴニョゴニョゴニョというよくある構成ということで、ググっていただければいいので割愛します。

 

アーカイブファイルを置いてあるサーバに関しては、更新もないので同じアーカイブファイルを複数台に配置しておくだけです。
サーバ吹っ飛んだら、生きているサーバから手動でファイル転送するだけ、という手動アンチエントロピー対応を想定しています。

 

実にシンプルですね。

 

サーバは死に方によってはパーツ交換の保守だけで復活できますし、高集約している場合に数日単位でかかるファイルコピーが自動で動くよりもヒトが確認しながら対応したほうが最終的には運用負荷が低いんじゃねーの? というのを言い訳に手を抜いたわけです。


なお、ディスク容量節約のためHDFSのイレイジャーコーディングみたいな実装もちょっとは検討はしましたが、実装と運用が複雑になりそうなので結局採用しませんでした。

 

まとめ

画像チームで作ったオブジェクトストレージの設計の紹介をしました。

 

かなりユースケースが限られるモノですが、冗談みたいに大量の小さいファイルを扱う場合などには参考にしていただけると幸いです。


余談

設計固めて、WebDAVのファイルのアーカイブを始めたあとくらいに、@brfrn169にfacebookのhaystackf4というオブジェクトストレージの論文を教えてもらいました。設計方向とかに共通部分が多いので、よければソチラも参考にしていただければと思います。つーか、もっと早く教えてくれよ!!!


画像を扱うサービスのベストプラクティス

今度は視点をガラッとかえて、ユーザ投稿の画像を受け付けるサービス開発において、あらかじめ考慮しておくと良いと思われるポイントを列挙していきます。
最初に謝っておきますが、ベストプラクティスなんてタイトル詐欺感満載です。ゴメンナサイ。

# ところどころ社内向け情報が混ざってますが、反響があれば社外公開されるかもしれません。


同一URLの画像の更新を不可にしよう。

画像を扱うオペレーションには、乱暴には以下の4種類があります。

  • 新規追加
  • 参照
  • 更新
  • 削除

このうち「更新」については設計上できないようにしましょう。

もし更新が必要なら、アプリケーションを工夫して「別パスでアップロードしなおしてリンクを差し替え、古い方を削除する」としてください。

 

こうすることで、CDNやブラウザのキャッシュに悩まされずに済みますし、更新がない分だけ積極的にブラウザキャッシュを有効活用できるので、ユーザ的にも表示が早くなってウレシイですし、コストに直結する通信量も削減できます。

 

逆に更新を許容してしまうと、キャッシュパージの問題などに、ずーーーっと悩まされることになります。ツライです。

 

また、結果整合性のオブジェクトストレージも採用しやすくなりますし、前述の内製オブジェクトストレージみたいに「更新がないからこそ採用できるストレージ」というのもあります。将来的な選択肢を残す意味でも画像の上書き更新は設計時点で排除しておきましょう。


画像URLパスにオブジェクトストレージの仕様や制限を反映させない

オブジェクトストレージにはそれぞれ特有の制限やベストプラクティスがあります。

その制限やベストプラクティスには従うべきなのですが、画像URLのパスはそれ自体名前空間であり、サービスの仕様です。
特定のオブジェクトストレージ特有の事情を反映させてしまうと、他のオブジェクトストレージに乗り換える際に大きな負担となります。

 

ロックインに覚悟完了していない場合は、オブジェクトストレージ特有の事情とサービスの仕様とを吸収するプロキシ的なモノを挟むとよいでしょう。

# (社内限り)S3用のプロキシ、あるよ。


画像のパスから世代を判別できるようにしよう

ユーザ投稿の画像は一般的に「直近に投稿された画像ほどよく参照される」という傾向があります。これは言い換えれば「昔に投稿された画像はほとんど参照されない」ということです。

 

このため、直近のモノをHotなオブジェクトストレージに、そうでないモノをWarmなオブジェクトストレージに、というような世代別な管理がコスト・性能的に効果的であるケースが(超大規模になれば)あります。

 

そのため、画像URLに投稿年月を含めておくなどしてルールベースで世代判別できるようにしておきましょう。

 

サービス開発の最初期に複数のオブジェクトストレージを使うことなんてアリエナイと思いますが、画像URLパスをその場その場の行き当たりばったりで決定してしまうと、ルールベースで世代判別不能になってしまい、将来的な選択肢を失うことになります。

 

関連画像を事前生成する場合は、そのパスからオリジナル画像のパスを生成可能にしておこう

サムネイル画像のように関連画像を投稿時に一緒に生成してストレージに保存する、という設計は比較的よくあると思います。その場合のサムネイル画像のURLをからオリジナル画像のURLをルールベースで変換できるように、サムネイル画像のURLを決定しましょう。

 

時間がたち参照が減るにつれて、オリジナル以外の画像をストレージに保持するコストの方が高くなるタイミングがいずれはやってきます。  そうです、ファイル数は暴力です。

 

そんな時はサムネイル画像のストレージ保存をやめて、リクエストに対してオリジナル画像をオンザフライで変換したものをレスポンスで返すとよいのですが、そのためには「オリジナル画像のパス」がワカラナイと詰みます。ツライです。

 

なお、最初から関連画像はすべてオンザフライで変換するようにするというのもアリです。  


small_light系のモジュールを利用してもよいですし、自分たちで画像変換サービスを運用するのがメンドウならakamaiなどのCDNのサービスを利用するのも手です。

# (社内限り)画像チームで作成した画像変換プロキシモジュール、あるよ。


オンザフライの画像変換をするときはメモリ使用量を制限しておこう

画像変換の内容によっては、1リクエストでものすごく大量のメモリが必要になってしまうケースが出てきます。ある程度で上限を設けておきましょう。

 

さもないとOSのOOM killerでサーバプロセスごとお亡くなりになってしまいます。

 

オンザフライの画像変換をするときはthundering herdに気をつけよう

CDNを用意していても、投稿直後などにキャッシュヒットしなかった画像変換クエリが大量に流れてくるケースがあります。
通常は問題なくても、アニメーションGIFの変換のようなとてもとても時間のかかる変換アクセスが大量に流れてくると、その勢いで画像変換サーバが全滅したりします。

 

構成複雑になるので初期で対応する必要はないと思いますが、問題になった場合は同一の画像変換リクエストを束ねるような構成を検討しましょう。
VarnishやApache Traffic Serverなどが候補になると思います。


exif情報の取り扱いを考慮しよう

ユーザ投稿の画像ではexif情報がそのまま入っているケースが非常に多いです。
GPSの情報のようにそれなりにセンシティブなデータもあるので、ユーザのプライバシー保護のために(少なくともデフォルトでは)削除するようにしましょう。

# (社内限り)golangのexif削除ライブラリ、あるよ。

 

exif情報をクレジット的な用途で使っている方もいらっしゃるので、その場合はexif情報を残すオプションを用意するのはアリです。


初期から自前でストレージを運用するのはやめよう

初期は黙ってS3とかのマネージドサービスを利用しましょう。
自前運用を検討するのは、スケールメリットがでる規模になってからでいいです。


初期からCDNを利用を検討しよう

最近はオンラインでポチポチするだけで使えるCDNがいくつもあります。
コスト的にも有利なケースが多いですし、運用で手がかかるモノではないので積極的に利用を検討しましょう。

 

なお、CDNによってはポリシー的にNGなコンテンツがあるケースがあります。アダルト系のコンテンツを扱う場合は特に注意しましょう。


最後に身も蓋もないことを

手に負えないほど大規模にならなければ、ぶっちゃけ後からでもどうにかなります。

サービス作るときは、イーカンジに手を抜きましょう。

 


それでは皆様、ステキなエンジニアライフを。

Go言語でTestableなWebアプリケーションを目指して

$
0
0

はじめまして@shohhei1126です! 

 

2016年1月にリリースされたAmebaFRESH!のサーバサイドを担当しております。

ここ1、2年社内でもGo言語を使うプロジェクトが増えてきているのですが(Go Lang in Cyberagent こちらもどうぞ)AmebaFRESH!でもGo言語をメインに使っています。

 

今回はGo言語のテストへのアプローチについて考えたいと思います。

テストの書きやすさ

Goでは言語レベルでテストをサポートしている(https://golang.org/pkg/testing/#pkg-overview)のでテストを書くという敷居は他の言語より低く感じます。実際これまでのプロジェクトの中で一番テストを書いていますし、やっぱりテストがあると安心感ありますね(・∀・)

Mock化の難しさ

テストの取っ掛かりとしての敷居は低いですがデータアクセスまわりのテストを書く場合は事前に考えておかないと後々問題が起きてきます。

データアクセス部分でよく見る構成はパッケージ内に変数を持つパターンです。

package   model

var db *sql.DB // ココ

 

func InitDB(dataSourceName string) {

  var err error

  db, err = sql.Open( "mysql" , dataSourceName)

  // ...

}

 

type Channel struct {

  Title string

   // ...

}

 

func (c Channel) findById() error {

   rows, err := db.Query( "SELECT * FROM channels" )

   // ...

}

この構成はmodelパッケージ自体のテストはやりやすいですがmock化できない*1のでHandlerやSerivceのテストを書く場合にもデータベースが必要になりテストコードが煩雑になっていきます。また複数パッケージのテストを並行で実行できないためテスト時間が長くなってしまうというデメリットもあります*2。

 

逆にmodelとhandlerパッケージだけの構成でhandlerのテストを書く必要がないような小さなアプリケーションはこのような構成でいいかもしれません。

 

*1 ドライバ自体をモック化する方法はあります(https://github.com/erikstmartin/go-testdb)がテストでも実際にデータベースへ接続したほうが余計な罠を踏まずに済みそうなので今回見送りました。

*2  パッケージごとにデータベースを用意するという選択肢はありそうな気がしますがなんかね。。。

interface 使ってモック化する

前述の問題を解決するためにinterfaceを使ってDIするような構成を考えます。

ソースは shohhei1126/bbs-go にあるので細かいところはこちらを御覧ください。

パッケージ構成

ある程度の規模を想定してhandler, service, daoの三層構成にします。modelパッケージはテーブルに対応するstructを定義したものです。

$ tree

├── dao

├── handler

├── main.go

├── model

└── service

     ...

DAO

interfaceとその実装です。ORMにgorpを使っているので*gorp.DbMapをフィールドに持たせます。またSQLビルダーにsquirrelを使用しています。

package   dao

import   ...

 

type Thread  interface   {

   FindList(paging Paging) (model.ThreadSlice, error)

}

 

type ThreadImpl struct {

   db *gorp.DbMap

}

func (t ThreadImpl) FindList(paging Paging) (model.ThreadSlice, error) {sql, args, err := squirrel.Select("*").From("threads").    OrderBy(paging.OrderBy).Limit(paging.Limit).Offset(paging.Offset).    ToSql()if err != nil {return nil, err}var threads model.ThreadSliceif _, err := t.db.Select(&threads, sql, args...); err != nil {return nil, err}return threads, nil}

テストは実際にデータベースに接続して行こないます。アサートにstretchr/testifyを使っています。

package daoimport ...// func TestMain(m *testing.M)で予め初期化しておきますvar (  dbMap      *gorp.DbMapthreadDao  Thread)

func TestThreadFindList(t *testing.T) {

   dbMap.TruncateTables()

   createdAt := time.Unix(time.Now().Unix(), 0)

  updatedAt := createdAt

   threads := make(model.ThreadSlice,  10 )

   for   i := range threads {

     createdAt = createdAt.Add(time.Hour)

     updatedAt = updatedAt.Add(-time.Hour)

    threads[i].CreatedAt = createdAt

     threads[i].UpdatedAt = updatedAt

     if   err := dbMap.Insert(&threads[i]); err != nil {

       t.Fatal(err)

     }

  }

 

   tests := []struct {

     paging   Paging

     expected model.ThreadSlice

   }{

     {

       paging: Paging{OrderBy:  "created_at desc" , Limit:  3 , Offset:  0 },

       expected: threads[ 7 :].SortBy(func(t1, t2 model.Thread) bool {

         return   t1.CreatedAt.After(t2.CreatedAt)

       })},

     {

       paging: Paging{OrderBy:  "updated_at desc" , Limit:  3 , Offset:  0 },

       expected: threads[ 0 : 3 ],

     },

  }

 

   for   _, test := range tests {

     threads, err := threadDao.FindList(test.paging)

       if   err != nil {

       t.Fatal(err)

     }

     assert .Equal(t, test.expected, threads,  "" )

   }

}

mockgenを使ってモック作成

mockgen を使ってモックを作成します。生成したモックはServiceのテストで使用します。

$ cd dao

$ mockgen - package   dao -destination thread_mock.go -source thread.go

$ cat thread_mock.go

// Automatically generated by MockGen. DO NOT EDIT!

// Source: thread.go

 

package   dao

 

import   (

  gomock  "github.com/golang/mock/gomock"

  model  "github.com/shohhei1126/bbs-go/model"

)

 

// Mock of Thread interface

type MockThread struct {

  ctrl     *gomock.Controller

  recorder *_MockThreadRecorder

}

Service

ServiceもDAOと同じような作りになります。内部で使うDAOをプロパティとして持たせています。

package serviceimport ...type Thread interface { FindThreads(paging dao.Paging) (model.ThreadSlice, error)}type ThreadImpl struct {  userDao    dao.User  threadDao  dao.Thread}

func (t ThreadImpl) FindThreads(paging dao.Paging) (model.ThreadSlice, error) {

   threads, err := t.threadDao.FindList(paging)

   if   err != nil {

     return   nil, err

   }

   // ...

}

テストでは 先ほど作ったDAOをモックを使っています。

func TestThreadFindThreads(t *testing.T) {

  ctl := gomock.NewController(t)

  defer ctl.Finish()

 

  paging := dao.Paging{OrderBy: "updated_at" , Limit:  3 , Offset:  3 }

   threads := model.ThreadSlice{

     {Id:  2 , UserId:  12 },

     {Id:  3 , UserId:  13 },

     {Id:  4 , UserId:  14 },

  }

   threadDaoMoc := dao.NewMockThread(ctl)

   threadDaoMoc.EXPECT().FindList(paging).Return(threads, nil)  //ここで挙動を指定

 

   users := model.UserSlice{

    {Id:  12 , Username:  "username 12" },

    {Id:  13 , Username:  "username 13" },

    {Id:  14 , Username:  "username 14" },

   }

   userDaoMoc := dao.NewMockUser(ctl)

   userDaoMoc.EXPECT().FindByIds([]uint32{ 12 13 14 }).Return(users, nil)  //ここで挙動を指定

  threadService := NewThread(userDaoMoc, threadDaoMoc)

  actualThreads, err := threadService.FindThreads(paging)

   if   err != nil {

     t.Fatal(err)

  }

   assert .Equal(t,  int (paging.Limit), len(actualThreads),  "" )

   for   _, thread := range actualThreads {

    assert .Equal(t, thread.UserId, thread.User.Id)

   }

}

Handlerのテストのため先ほどのDAOと同じようにmockgenでservice.Threadのモックを作っておきます。​

Handler

Handlerはモック化する必要が無いのでstrcutにしています。

package handlerimport ...

type Thread struct {

   threadService service.Thread

}

 

func (t Thread) List(ctx context.Context, r *http.Request) response.Response {

   limit, err := strconv.ParseInt(r.URL.Query().Get( "limit" ),  10 64 )

   if   err != nil {

   // ...

}

service.Threadのモックを作成しテストします。

func TestThreadList(t *testing.T) {

   ctl := gomock.NewController(t)

   defer ctl.Finish()

 

   threadServiceMock := service.NewMockThread(ctl)

   threadServiceMock.

     EXPECT().

     FindThreads(dao.Paging{Limit:  5 , Offset:  0 , OrderBy:  "updated_at desc" }).

     Return(model.ThreadSlice{}, nil).

     Times( 1 // 一度だけ呼ばれることを確認

   threadHandler := NewThread(threadServiceMock)

 

  r := http.Request{}

  url, err := url.Parse( "http://localhost?limit=5&offset=0" )

  if   err != nil {

     t.Fatal(err)

   }

   r.URL = url

   threadHandler.List(ctx, &r)

  }

}

テスト

データベースへの接続がdaoパッケージのみになるのでまとめてテストを実行することが出来ます。

$ GO15VENDOREXPERIMENT= 1

$ cd $GOPATH/src/github.com/shohhei1126/bbs-go

$ go test $(go list ./... | grep -v vendor)

ok      github.com/shohhei1126/bbs-go/dao    0 .106s

ok      github.com/shohhei1126/bbs-go/handler    0.012s

ok      github.com/shohhei1126/bbs-go/model  0.011s

ok      github.com/shohhei1126/bbs-go/service    0 .013s

...

main.go

それぞれの実装クラスのインスタンスを作ってHandlerをGojiのHTTP multiplexerに登録します。

dbm := parseDb(conf.DbMaster)

dbs := parseDb(conf.DbSlave)

dbMMap := model.Init(dbm, log.Logger)

dbSMap := model.Init(dbs, log.Logger)

 

mux := goji.NewMux()

userDao := dao.NewUser(dbMMap, dbSMap)

threadDao := dao.NewThread(dbMMap, dbSMap)

threadService := service.NewThread(userDao, threadDao)

threadHandler := handler.NewThread(threadService)

mux.HandleFuncC(pat.Get( "/v1/threads"), wrap(threadHandler.List))

サーバ起動とAPI実行

$ go run main.go &

INFO[ 0000 ] starting server...

 

$ curl -XGET  "http://localhost:8080/v1/threads?limit=5&offset=0"

[{ "id" : 9 , "title" : "i" , "body" : "i" , "createdAt" :" 20 ...

まとめ

ある程度の規模のプロジェクトでもinterfaceとmockgenを使うことでテストが書きやすくテストの実行時間も短くすることが出来ます。もし同じような問題に直面しているのであれば参考にしていただけると幸いです。

 

またAmebaFRESH!ではマイクロサービスアーキテクチャをとっているので大小様々なマイクロサービスが存在しています。それぞれのマイクロサービスの規模や重要度などによって構成やテストの方針は変わってくるので個別最適した形で開発しています。

 

最後にデータベースアクセスに関して Practical Persistence in Go: Organising Database Access で他のパターンも含めて丁寧にまとめられていますのでこちらも合わせて読んでいただければと思います。

 

長くなりましたが最後まで読んでいただきありがとうございました!

GCPのCloud IAMを試してみた

$
0
0

メディア事業(アメーバ)を中心にAWS/GCPを担当している柿島です。前回は、Google Cloud Platform(GCP)の各プロジェクトでコストを追える環境を作る を書きました。前回の続報としては、一部には使ってもらいつつも、可視化の部分で cloudyn を検証中です。

 

今回は、GCPのリソースの認可の話になります。GCPの利用がメディア事業でも増えてきています。パブリッククラウドを組織としてそれなりの人数で利用していると、やはり気になってくるのが権限の管理です。セキュリティや作業事故防止を考えると、メンバー全員に強い権限を渡すというわけにもいかず、メンバーの役割ごとに権限を与えるということが必要になります。

Cloud IAM の登場

先日の GCP NEXT で、Google Cloud Identity and Access Management (Cloud IAM) が発表されました。AWSの AWS Identity and Access Management (IAM) に比べて、GCPはこの部分が弱いと思っていたので、首を長くして待っていた機能になります。Youtubeにある紹介動画 Introduction to Cloud IAM が4分くらいで概要をつかめるのでオススメです。

 

ベストプラクティスの資料もすでに用意されています。

 

IAM best practice guides available now

以上です!

 

.....とはいかないので、

  • Cloud IAM の紹介
  • 実際に触ってみる
  • その他、試した所感

の流れで、自分なりに理解した説明と試した記録を残します。もし間違い等がありましたらご指摘ください。

Cloud IAM の紹介

Cloud IAM の話に登場する大きな要素は、「Identity」、「Role(とPermission)」、「Resource」の3つです。IdentityはGoogle AccountやService Accountといった誰に対応する部分です。所属するメンバーを一括管理するために Google GroupGoogle Apps Domain をIdentityとして指定することもできます。Roleは、何ができるかという役割(Permissionの集まり)です。ResourceはProject 、Project内のGCEといった特定のサービス、GCE内のインスタンスといったような GCPの各種リソースです。

 

(引用: https://cloud.google.com/iam/docs/ )

After Google authenticates the identity making a request, Cloud IAM makes an authorization decision whether that identity is allowed to perform an operation on a resource.

https://cloud.google.com/iam/docs/overview#concepts_related_to_access_management

 

このIdentityとRoleとResource(後述するRerouce Hierarchyのレベル)の組み合わせで誰が何にどのようにアクセスできるかを定義すると、そのIdentityがResouceへアクセスをする際に権限があるかどうかをCloud IAMがチェックしてくれます。

 

また、Cloud IAM には重要な概念として、Resource Hierarchy というものがあります。冒頭に紹介したベストプラクティスの Using Resource Hierarchy for Access Control で詳しく説明がされています。HierarchyのrootとしてOrganizationが存在し、その子としてProject、Projectの子として、GCEなどのResourceが存在します。Organizationは、Google Apps for Work の顧客向けに提供されています(現在Alpha)。


(引用: https://cloud.google.com/iam/docs/resource-hierarchy-access-control )

 

Hierarchyの上位で許可した権限は下位に引き継がれます。下に引用した図はOrganizationに対して、compute.networkAdmin のRoleが付与されているbob@example.comさんが各プロジェクトのGCEでnetworkリソースの作業追加ができることを表した図です。

 

(引用: https://cloud.google.com/iam/docs/resource-hierarchy-access-control#example_compute_engine )

 

複数の箇所で有効なPolicyが指定されている場合は、和集合となるようです。各RoleがどのHierarchyのレベルに対応しているかは、こちらのページの表のResource Typeに書かれています。例えば、Projectにだけ対応しているRoleもあれば、OrganizationとProjectの両方に対応しているRoleもあります。Organization専用のRoleである roles/
resourcemanager.organizationAdmin
というRoleを持ったIdentityはそのOrganizationに属するどのProjectのどのResourceに対しても管理者権限を持つことができるようです。

Role/Resourceに関して

これまでものGCPでも、前回書いた記事 でも使ったように、BigqueryではProjectやDatasetごとにACLを設定できたり、ほかにもGCSでBucketsやObjectsに対して ACLでのアクセスコントロールができたりと、一部のサービスでは細かな権限にも対応していました。しかし、GCP全体となるとチームメンバーのGoogle Accountに設定できるのは、Owner(is Owner)、Editor(can Edit)、Viewer(can View)という3種類だけでした。

Team members may be authorized to have one of three levels of access:

  • “can View” (called Viewer in App Engine Console) allows read-only access.
  • “can Edit” (called Developer in App Engine Console) allows modify and delete access.
    This allows a developer to deploy the application and modify or configure its resources.
  • “is Owner” (called Owner in App Engine Console) allows full administrative access.
    This includes the ability to add members and set the authorization level of team members.

https://cloud.google.com/docs/permissions-overview#h.bgs0oxofvnoo

 

これらのRoleしかない場合、特定のリソースにアクセスをさせたいだけの場合でも、強めの権限を付与する必要がありました。今後は、必要最低限の権限しか渡したくない(貰いたくない)場合には、今回登場した新しいRoleの Curated roles を使うことができます。これまでの Owner、Editor、Viewer というRoleは、Primitive roles と呼ばれるようになります。


Curated rolesには例えば、VMインスタンス(やディスク)の作成、編集、削除が許されたRoleとして compute.instanceAdmin が用意されています。他にどのようなCurated rolesがあるかは https://cloud.google.com/iam/docs/understanding-roles#curated_roles をご覧下さい。

 

AWSで例えると、これまでは、AdministratorAccess、PowerUserAccess、ReadOnlyAccessのポリシーしかアタッチできなかったのが、AmazonEC2FullAccessなども選べるようになったというイメージでしょうか。

 

各サービスのIAMへの対応状況は、以下のようにまだまだBetaが多いです(2016/03/31現在)。Betaのものは、後方互換のない変更が起こる可能性があるため、プロダクション環境への利用は推奨されていません。また、すでに別のアクセス制限方法があるBigqueryは未対応だったりと、すべてのサービスがIAMに対応をしているわけではないようです。(今後どうなるかはわかりません)。また、Beta期間ではGCEのrolling updatesなど一部のオペレーションも未対応のようです。

 

(引用: https://cloud.google.com/iam/docs/ 2016/03/30時点)

 

Curated RoleはPermissionの集まりとなっていて、Permissionは以下のようなフォーマットで表現されます。

Service.Resource.Verb

例えば roles/compute.securityAdminという Curated Role は、以下のPermissionの集合になっています。しています。

compute.firewalls.*
compute.networks.{get|list}
compute.operations.get
compute.projects.get
compute.regions.*
compute.routes.{get|list}
compute.sslCertificates.*
compute.zones.*

 

動画に出てきた例は、pubsub.topics.list と storage.bucket.create でした。AWSのaction風に書くと pubsub:listTopics, storage:createBucketでしょうか(実際に存在するものではありません)。

 

Permissionは、通常REST APIのMethodと1対1で対応しているようです。

Permissions usually, but not always, correspond 1:1 with REST methods. 

https://cloud.google.com/iam/docs/overview#permissions

 

例えば、以下に対応するPermissionはcompute.networks.insertだと思われます。

insert POST  /project/global/networks

https://cloud.google.com/compute/docs/reference/latest/#Networks

実際に触ってみる

では実際に触っていきます。Cloud IAMは、GCP Console、REST API、gcloudコマンドで設定ができます。注意点としては別のユーザのPermissionの設定には、ProjectのOwner権限が必要になります。

 

登場するProject:

example-project(仮名)

 

登場するIdentity:

(1) 今回の権限のテスト対象となるGoogle Account (マスクまたはtarget@example.com 仮名)

(2) 権限付与時に使用するOwner Roleを持った Google Account (owner@example.com 仮名)

 

登場するRoleとそのPermission

(1) Compute Security Admin(roles/compute.securityAdmin)

  • compute.firewalls.*
  • compute.networks.{get|list}
  • compute.operations.get
  • compute.projects.get
  • compute.regions.*
  • compute.routes.{get|list}
  • compute.sslCertificates.*
  • compute.zones.*

 (2) Compute Instance Admin(roles/compute.instanceAdmin)

  • compute.{global}addresses.{get|list|aggregatedList}
  • compute.autoscalers.*
  • compute.disks.{insert|delete|get|list|aggregatedList}
  • compute.disksTypes.{get|list|aggregatedList}
  • compute.globalOperations.{get|list}
  • compute.licenses.{list}
  • compute.machineTypes.{get|list|aggregatedList}
  • compute.networks.{get|list}
  • compute.images.{get|list}
  • compute.instances.*
  • compute.instanceGroups.*
  • compute.instanceGroupManagers.*
  • compute.instanceTemplates.*
  • compute.projects.get
  • compute.regions.{get|list}
  • compute.regionOperations.{get|list}
  • compute.subnetworks.{get|list|aggregatedList}
  • compute.zones.{get|list}
  • compute.zoneOperations.{get|list}

Also permits users to connect to an instance using SSH.

(3) Storage Object Viewer ※名前のみ

 

試す操作の流れ:

(1) owner@gmail.comで GCP Console から target@example.com に Compute Security Admin のRoleを設定

(2) target@example.com でGCP Console から ネットワークの一覧(許可されている操作)

(3) target@example.com でGCP Console から VMインスタンスの一覧(許可されていない操作)

(4) target@example.com で認証した gcloudコマンドから プロジェクト内のサーバ(test)にSSH(許可されていない操作)

(5) owner@example.com で GCP Console から target@example.com に Compute Instance Admin のRoleを追加

(6) target@example.com で GCP Console から インスタンスの一覧(新たに許可した操作)

(7) target@example.com で認証した gcloudコマンドから プロジェクト内のサーバ(test)にSSH(新たに許可した操作)

(8) owner@example.com で認証した gcloudコマンドから target@example.com に Storage Object Viewer のRoleを追加

(1) owner@example.comで GCP Console から target@example.com に Compute Security Admin のRoleを設定

GCPのConsoleの左上メニューからPermissionsを選びます。

 

 

Permisionsタブ(デフォルトで選択されている)の中から Add members ボタン をクリックします。

 

 

Roleがこれまでと違いOwner、Editor、Viewer以外も選べるようになっています。Compute Security Adminを選択して Add します。

 

 

これで、target@example.comにCompute Security Admin のRoleが付与されました。これまでのOwnerなどと並んで、Curated Roleごとに表示されるようです。

 

 

(2) target@example.com でGCP Console から ネットワークの一覧(許可されている操作)

この権限の状態で、実際に操作をしてみます。まずは、許可されている操作の確認としてネットワークの一覧が見れるか確認します。Compute Security Adminは、compute.networks.{get|list} のPermissionを持っているので、ネットワークの一覧が閲覧できます。(ただ、 compute.networks.insert権限はないため"ネットワークを作成"ボタンはグレーアウトされて押せなくなっています。)

 

 

(3) target@example.com でGCP Console から VMインスタンスの一覧(許可されていない操作)

許可されていない操作の確認として、 Compute EngineのVMインスタンスの一覧を見てみます。 Compute Security Adminには、 compute.instances.{get|list} のPermissionは含まれない You don't have permission to view the instances in this project とエラーが表示されます。

 

 

(4) target@example.com で認証した gcloudコマンドから プロジェクト内のサーバ(test)にSSH(許可されていない操作)

gcloudコマンドやGCP ConsoleからのSSHは、Compute Instance Admin(roles/compute.instanceAdmin) Roleが必要です。Compute Security Admin のRoleしか付与していないtarget@example.comからは接続できないはずです。

 

$ gcloud compute --project "example-project" ssh --zone "asia-east1-a" "test"ERROR: (gcloud.compute.ssh) Could not fetch instance:- The resource 'projects/example-project' was not found

 

エラーメッセージは若干わかりづらいものの予定通り失敗しました。(他の権限がなくてその手前でこけているのかもしれません)

(5) owner@example.com で GCP Console から target@example.com に Compute Instance Admin のRoleを追加

owner権限のGoogle Accountに戻って、target@example.comにCompute Instance AdminのRoleを追加してみます。PermissionのページからRoleを追加したいMemberのRoleの部分をクリックするとRoleが選べる状態になります(複数選択可)。今回は Compute Instance Admin にチェックを付けて Save します。 

 

 

Compute Security Adminと並んで、Compute Instance AdminのRoleが画面に現れ、追加したtarget@example.comがmemberとして表示されました。

 

(6) target@example.com で GCP Console から インスタンスの一覧(新たに許可した操作)

target@example.comに戻って(再ログインが必要な可能性あり)、先ほどはPermissionがなくて閲覧できなかったVMインスタンスの一覧 を見てみます。新しく付与したRoleのCompute Instance Admin は  compute.instances.* のPermissionを持っているため無事閲覧ができました。

 

(7) target@example.com で認証した gcloudコマンドから プロジェクト内のサーバにSSH(新たに許可した操作)

Compute Instance AdminにはSSHログインの権限がありますので、こちらもログインできるようになりました。

 

$ gcloud compute --project "example-project" ssh --zone "asia-east1-a" "test"Warning: Permanently added 'x.x.x.x' (RSA) to the list of known hosts.[hoge@test ~]$ 

(8) owner@example.com で認証した gcloudコマンドから target@example.com に Storage Object Viewer のRoleを追加

最後に、gcloudコマンドからCloud IAMの設定や確認をしてみます。 まだ、Cloud IAMはbetaとなるので、gcloudコマンドのアップデートとbetaコマンドのインストールをします。

 

$ gcloud components update

 

$ gcloud components update beta

 

まずはgcloud beta projects get-iam-policyコマンドで、現在のProjectに対する設定の確認をします。

 

$ gcloud beta projects get-iam-policy example-projectbindings:- members:  - serviceAccount:hogehoge@appspot.gserviceaccount.com  role: roles/editor- members:  - user:owner@example.com  role: roles/owner- members:  - serviceAccount:fugafuga@developer.gserviceaccount.com  role: roles/viewer- members:  - user:target@example.com  role: roles/compute.securityAdmin- members:  - user:target@example.com  role: roles/compute.instanceAdminetag: HogeFugaversion: 1

 

次に、gcloud beta projects add-iam-policy-binding コマンドで設定を追加します。

 

$ gcloud beta projects add-iam-policy-binding example-project --member user:target@example.com --role roles/storage.objectViewer(略)- members:  - user:target@example.com  role: roles/compute.securityAdmin- members:  - user:target@example.com  role: roles/compute.instanceAdmin- members:  - user:target@example.com  role: roles/storage.objectViewer

 

コマンドの実行結果や、先ほども叩いた gcloud beta projects get-iam-policy で確認すると、role: roles/storage.objectViewer の記述が増えていることがわかります。他にもjsonを使った設定方法もあるようです。詳しくは、Access control via the gcloud tool をご覧ください。

    Service Account とCloud IAM

    Service Accountは人が介在しない状況で、アプリやGCEなどからサービスを使いたい場合に使います。記事が長くなってしまったので、Service AccountでのCloud IAMの利用は注意事項の引用で終わります。

    Keep in mind that:

    • IAM roles are currently in Beta and not all services support IAM. For example, there are no IAM roles to grant access to BigQuery. We recommend using IAM only if there are IAM roles to support all the access the default service account needs. For example, if a default service account requires access to Google Cloud Storage and Google BigQuery, we recommend you use access scopes for now.

    • You must still set access scopes to authorize access from the instance. IAM roles do not replace access scopes. The level of access a service account has is determined by a combination of access scopes and IAM roles. Learn more about access scopes and IAM roles.

    (引用: https://cloud.google.com/compute/docs/authentication?hl=ja#usingroles )

    その他、試した所感

    • 嬉しいこと
      • Google AppsのGoogle AccountやGoogle Groupを利用可能
        • Google Appsを利用しているので相性がいい
        • 例えば、projectname_engineer@example.com グループや network_team@example.com グループとか security_team@example.com グループなどなどを作ってGroup単位での権限管理は楽にできそう
      • Resource Hierarchyの仕組みは、うまく設計ができれば便利そう
        • 例えば、network_team@example.comやsecurity_team@example.comをOrginizationレベルで管理に必要なRoleを付与することでプロジェクトを横断した管理が楽になりそう
    • 悩んでいること
    • 他、試していないが便利そうなのでこれから試したいもの

    AWSと比べてしまうと、もう少し細かく制限ができるといいなと思う部分もありますが、Cloud IAMが出てきただけで非常に嬉しいです。Organizationなど管理が楽になる部分もありそうなので、ベストプラクティスの資料を読みつつ検証を進めGCPでのIAM戦略を練って、各Resourceへの対応がBetaではなくなってきたら順次各Projectの権限の見直しをしていく予定です。

     

    ではでは!

    運用型広告【A.J.A.】の紹介と高速化の工夫

    $
    0
    0

    技術本部メディア広告開発室のこまはらです。

    A.J.A.という運用型広告システムの開発・運用を担当しています。

     

    A.J.A.では、現在、アメーバブログSpotlightby.Sといった自社メディアに、インフィード広告を中心とした広告を配信しており、配信先を順次拡大しています。

    様々な種類の広告を配信していますが、すべてひっくるめると1日に10億impression前後の配信規模になっています。

     

    今回は、A.J.A.の概要編ということで、前半で基本的なシステムの構成についてご紹介し、後半に高速なレスポンスを実現するための工夫について書かせていただきます。

     

    基本構成

    A.J.A.の広告システムは、

    • ユーザの広告リクエストを受け、広告を返す配信サーバ
    • 数ある広告の候補から最適なものを選択するレコメンデーションAPIサーバ
    • 配信設定や実績のレポーティングを行うための運用管理画面
    • 配信実績のリアルタイムカウンターや集計を行うための集計サーバ
    • 配信実績やメディアの情報を元に、ユーザセグメント等を作り出すプライベートDMP

    といったサブシステムに分けられ、そのほぼ全てをAmazon Web Services(AWS)上で構築しています。

     

     

     

    Front-end VPCとBack-end VPCの2つに分かれているのですが、ユーザのリクエストを直接受ける配信サーバや、配信設定を行う運用管理画面など、Webアプリケーションとしての機能をFront-end VPCのシステム群が担当し、実績の集計や分析、配信すべき広告のレコメンデーションAPIなどをBack-end VPCのシステム群が担当しています。

     

    実際の体制上も、Back-end VPCは弊社のR&D部門である秋葉原ラボが開発・運用しており、2つの部署とシステムが連携して、1つのプロダクトを構成しています。

     

    主に、サーバサイドはScala/Java/Node.jsが、フロントエンドではTypeScript/AngularJS/PostCSSなどが使われています。

     

    高速化のための工夫

    Webにおける広告配信に求められる、重要な指標の一つとして、広告リクエストに対する高速なレスポンスが挙げられます。

     

    レイテンシは、そのままユーザ端末上での表示速度に直結し、表示速度は広告効果に直結し、ひいては広告収益全体を左右する重要な一要素となりうるからです。

    昨今のトレンドでもあり、A.J.A.でも主力商品であるインフィード広告においては、広告はコンテンツの一部でもあるため、広告の表示速度の低下は、メディアそのもののUXの悪化につながりかねません。

     

    一方で、ある広告枠から広告リクエストを受けた際に、その広告枠に対して配信されうる広告の候補は、

    • 管理画面から行われる新規の入稿や配信先設定
    • 配信実績に応じた予算の消化状況
    • 広告枠側の設定や、NG業種などの拒否設定

    など多くの要素で決まり、しかも設定の変更や時間の経過によって時々刻々と変化していきます。

     

    秒間数千~数万にも及ぶ広告リクエストのたびに、配信候補をリアルタイムにデータベースに問い合わせていては、とても求められる応答速度を実現できません。

     

    配信候補のプリフェッチ

    そのため、A.J.A.ではAd loaderと呼ばれるサーバが広告枠に対する配信候補のマッピングを予めすべて作成し、Redisに保存しています。

    このデータは世代管理がされており、約3分間隔で新しい世代のマッピングが作成されていきます。

    さらに、各配信サーバや広告のレコメンドAPIサーバは、Redis上のデータの世代の更新を検知すると、このキャッシュデータを、配信処理とは非同期でサーバ上のメモリに吸い上げ、ローカルキャッシュとして保持します。

    こうすることで、配信処理中には配信対象のデータ取得のためにサーバの外に通信が出ることがないようにして、処理の高速化を図っています。

     

    (実際には広告が直接広告枠を指定できるわけではありませんが説明のため簡略化しています)

    逆に言えば、管理画面から行われる配信設定の変更や新規の入稿、停止、再開などは、最大3分程度は実際の配信に反映されるまでに時間差が生じることになります。

    即時性や正確性を一部犠牲にしてでも高速性を優先した設計となっており、この選択肢が取れるのも広告ならではかも知れません。

     

    また、機能追加などによって発生する配信設定側のデータ構造の変更が配信サーバやレコメンドAPIサーバで行われる配信処理への影響を、キャッシュレイヤーで吸収することができます。

     

    広告が選ばれて配信されるまで

    さて、このようにして得られた広告候補の中から、レコメンドAPIサーバの内部ロジックが、

    • オーディエンスターゲティングの設定条件に合致していない候補の除外
    • リターゲティングの設定条件に合致していない候補の除外
    • フリークエンシーキャップにより除外条件に合致した候補の除外

    を行い、残った候補について、過去の配信実績やユーザの属性、入札CPCなどから計算された品質スコアが算出され、バンディットアルゴリズムにしたがってランキングが付けられて、最終的に配信する広告を決定し、配信サーバにレスポンスします。

    (このあたりの話もとてもおもしろいので、そのうちチームで一緒に働いているメンバーがこのテーマで書いてくれると思います)

     

    配信サーバはレコメンドAPIから返された広告のIDから、配信面用のレスポンス形式に整形して、広告の素材データやimpression計測用やclick時の遷移先などの各種URLを含んだJson形式で配信面に対してレスポンスを行います。

     

    高速化の実際

    実際のところ、1回の配信処理において最も時間を要するのは、やはり上記のレコメンドAPIの内部ロジック部分です。

    また、当然ながら、広告配信において最も重要な部分でもあります。

    だからこそ、許容されるごく短い時間の大部分をこのロジック部分で使えるよう、マッピングデータやマスタデータの参照は最短で行えるよう工夫しているわけです。

     

    それでも、配信サーバはレコメンドAPIをコールしてから800ミリ秒以内にレスポンスがなければタイムアウトとみなし、配信面に対して広告無しのレスポンスを返却します。

    そのため、配信サーバが広告リクエストを受けてからレスポンスを行うまでの時間は、遅くとも800ミリ秒強になります。

     

    本来であれば広告無しでレスポンスすることは機会損失になるため、収益面では多少レスポンスが遅くとも広告を返却できるほうがよいのですが、上述のとおり、レスポンスタイムの悪化がメディアサイドのクオリティにまで大きく影響してしまうのを避けるため、このような内部的な制限を入れています。

     

    もちろん、広告の表示速度を決定づける要素は、ほかにも

    • サイトの描画が始まってから広告が呼び出されるまでの時間差
    • 回線の通信品質、速度
    • ユーザ端末の性能、メモリ状況

    など挙げれば様々ありますが、少なくとも配信サーバがボトルネックになるようなことがないように気をつけています。

     

    ちなみに、ここ最近の状況では、平均50ミリ秒前後でレスポンスしています。

     

    最後に

    今回は概要編ということで基本的な構成と高速化の工夫の一つを紹介させていただきました。

     

    A.J.A.は2015年3月のローンチ以来、配信規模を急速に拡大しながらプロダクトとしてもシステムとしても急成長を続けています。

    そんななかで、システム各所でこういった工夫がいたるところで行われており、配信規模の拡大や機能拡張に対しても、高速性を維持できるよう常に改善を続けています。

     

    また、本記事では紹介できませんでしたが、広告の配信だけでなく、メディアの内部回遊を最適化する回遊エンジンとしての機能もあり、そちらも順次導入先を拡大しています。

     

    アドテク領域は、外から見るととてもシンプルな機能に見えて、その裏では信じられないほど奥深い世界が広がっています。

    また、新しいチャレンジに比較的取り組みやすい領域でもあり、その取り組みの結果がすぐに数字となって現れることも醍醐味の一つです。

     

    まだまだ紹介したいことはたくさんあるのですが、今回はここまでにして、続きは他のチームメンバーに譲ることにします。

    最後までご覧いただき、ありがとうございました。

     


    try! Swift: Type Erasureのユースケースを考えてみた話

    $
    0
    0

    こんにちは
    Ameba事業本部でiOSエンジニアをしている @tasanobu です。

     

    先日(3/2~4)、弊社にて try! Swift を開催しました。

    try! Swift
    世界中のSwiftデベロッパーが一堂に会し、知識や技術を互いに共有し高め合うことを目的としたカンファレンスです。

     

    各セッションは国内外の著名なエンジニアがスピーカーを務めたこともあり、深い話が多く、満足度が高かったです。

    反面、一度聞いただけでは使いどころがイメージできないものがありました。

     

    そこで、このエントリではGwendolyn Westonさんによる Keep Calm and Type Erase On にて解説されていた Type Erasure を題材とし、具体的なユースケースに当てはめて理解を深めていきたいと思います。

    Type Erasureとは?

    Swiftでは通常のプロトコルは変数の型として使用できます。

    /// 通常のプロトコルprotocol Animal {    func eat()}let animal: Animal // 変数の型として指定可能

     

    一方、 associated type を持つプロトコルは変数の型として指定することができません。

    /// associated type を持つプロトコルprotocol Animal {    associatedtype Food    func eat(food: Food)}let animal: Animal/// error: protocol 'Animal' can only be used as a generic constraint /// because it has Self or associated type requirements

     

    下記の言語仕様の通り、プロコトルが適用されるまで、 associated type  として使う実際の型が決定しないためです。

     

    The actual type to use for that associated type is not specified until the protocol is adopted. 

     

    The Swift Programming Language (Swift 2.2) - Associated Types

     

    Type Erasureとは、プロトコルを実際の型に適用し、この言語仕様を回避する手法です。

    (Swiftの標準ライブラリでは、 AnySequence などでこの手法が使われています。)

    struct AnyAnimal<A: Animal>: Animal {    typealias Food = A.Food    private let _eat: (Food) -> ()    init<Inner: Animal where Food == Inner.Food>(_ inner: Inner) {        _eat = inner.eat    }    func eat(food: Food) {       _eat(food)    }}/// Foodstruct Grass {}/// Concrete Animalstruct Cow: Animal {    typealias Food = Grass    func eat(food: Food) {        /// do something...    }}let animal: AnyAnimal<Cow>! // 変数の型として指定可能

     

    ここまでが端的なType Erasureの説明です。

    抽象的すぎて使いどころが今ひとつイメージしにくいのではないでしょうか???

    ユースケース: NSUserDefaults

    写真撮影機能を持つアプリには、大抵 "カメラロールに保存するか?" "保存時のサイズ" といった設定があると思います。

    今回、NSUserDefaults (以降、UD) で " カメラロール保存フラグ"  " 保存サイズ"  をプロパティとして持つ構造体を管理するユースケースを考えてみます。

    struct CameraDefault {    enum Size: Int {        case Large, Medium, Small    }    let saveToCameraRoll: Bool    let size: Size}

    要件

    • UDに保存する設定
      • CameraConfig  以外に複数ある
      • 個々の設定値(" カメラロール保存フラグ" や" 保存サイズ" )に対してUDのKey/Valueを設定するのではなく、設定を構造化して管理したい
      • 設定はType Safeに扱いたい(AnyObjectは極力使いたくない)
    • その他
      • テスト時はUDではなくモックを使いたい

    設定用プロトコル

    CameraConfig  を例にしますが、実際のプロダクトでは別の設定もUDに保存する必要があります。

    そのため、データ型を抽象化する目的でプロトコルを作ります。

    protocol DefaultConvertible {    /// UDへの保存時に使うキー文字列    static var key: String { get }    /// UDから取得した値をデータ型に変換    init?(_ object: AnyObject)    /// UDへはAnyObjectで保存するのでAnyObjectに変換    func serialize() -> AnyObject}

    CameraConfig  にプロコトルを適用します。

    struct CameraConfig: DefaultConvertible {    enum Size: Int {        case Large, Medium, Small    }    let saveToCameraRoll: Bool    let size: Size    static let key = "CameraConfig"    init?(_ object: AnyObject) {        guard let dict = object as? [String: AnyObject] else { return nil }        self.saveToCameraRoll = dict["cameraRoll"] as? Bool ?? true                if let rawSize = dict["size"] as? Int, let size = Size(rawValue: rawSize) {            self.size = size        } else {            self.size = .Medium        }    }    func serialize() -> AnyObject {        return ["cameraRoll": saveToCameraRoll, "size": size.rawValue]    }}

    データストア用プロトコル

    テスト時はUDを使いたくありません。

    抽象化のためにデータストア用のプロトコルを用意します。

    protocol DefaultStoreType {    associatedtype Default: DefaultConvertible    func set(value: Default)    func get() -> Default?    func remove()}

    このプロトコルを用いてUDをラップした型を作ります。

    final class PersistentStore<Default: DefaultConvertible>: DefaultStoreType {    private let defaults = NSUserDefaults.standardUserDefaults()    init() {}    func set(value: Default) {        let obj = value.serialize()        defaults.setObject(obj, forKey: Default.key)    }    func get() -> Default? {        guard let obj = defaults.objectForKey(Default.key) else { return nil }        return Default(obj)    }    func remove() {        defaults.removeObjectForKey(Default.key)    }}

    また、UDの代わりにテスト用途で使う型も作ります。
    設定値はメモリ上のDictionaryに保持するだけなので、永続化されることはなくテスト毎に破棄され、便利です。

    final class InMemoryStore<Default: DefaultConvertible>: DefaultStoreType {    private var dictionary: [String: AnyObject] = [:]    init() {}    func set(value: Default) {        let obj = value.serialize()        dictionary[Default.key] = obj    }    func get() -> Default? {        guard let obj = dictionary[Default.key] else { return nil }        return Default(obj)    }    func remove() {        dictionary[Default.key] = nil    }}

    ここでDefaultStoreTypeの変数を作ってみます。

    しかし、前述の言語仕様により、コンパイルエラーになります。

    let defaults: DefaultStoreType<CameraConfig>! = PersistentStore<CameraConfig>()/// error: protocol 'DefaultStoreType' can only be used as a generic constraint /// because it has Self or associated type requirements

    Type Erasureを用いたデータストア型

    PersistentStore  を変数に保存できるようにするため、Type Erasureを用いて新たな型を作ります。

    final class AnyStore<D: DefaultConvertible>: DefaultStoreType {    typealias Default = D    private let _set: Default -> ()    private let _get: () -> Default?    private let _remove: () -> ()    init<Inner: DefaultStoreType where Inner.Default == D>(_ inner: Inner) {        _set = inner.set        _get = inner.get        _remove = inner.remove    }    func set(value: Default) {        _set(value)    }    func get() -> Default? {        return _get()    }    func remove() {        _remove()    }}

    AnyStore 型ならば変数に指定可能です。

    let ds = PersistentStore<CameraConfig>()let defaults: AnyStore<CameraConfig>! = AnyStore(ds)

     

    プロダクトにおいてViewControllerなどで使う場合は次のように書けます。

    /// アプリのコードclass CameraViewController: UIViewController {    lazy var config: AnyStore<CameraConfig> = {        let ds = PersistentStore<CameraConfig>()        return AnyStore(ds)    }()    ...}

    また、テストの場合は  PersistentStore  ではなく  InMemoryStore  に差し替えることが可能です。

    /// テストコードclass CameraViewControllerTests: XCTestCase {    var viewController: CameraViewController!    override func setUp() {        viewController = CameraViewController()        let defaultConfig = CameraConfig([:])!        let ds = InMemoryStore<CameraConfig>()        ds.set(defaultConfig) //        viewController.config = AnyStore(ds)    }}

    まとめ

    NSUserDefaultsをユースケースとして、Type Erasureを復習してみました。 Type Erasure自体はとても抽象的な手法だと思うのですが、意外にアプリ開発の現場で結構使いどころがありそうですね。

    今回の記事で使ったコードは、下記のライブラリとしてGithubに公開しました。 もしよかったら、使ってみて下さい。

     

    GitHub

    tasanobu/TypedDefaults

    Contribute to TypedDefaults development by creating an account on GitHub.

     

    改善の余地は色々あると思うので、PRやIssueでフィードバック頂けると幸いです。

    【イベント登壇情報】AWS Summit Tokyo 2016 に弊社エンジニアが登壇します

    $
    0
    0

    エンジニアブログ運営チームです。

     

    今回は弊社エンジニアのイベント登壇情報をお伝えします。

    現在、株式会社AbemaTVに出向し、「AbemaTV FRESH!」 の開発をしている @stormcat24 が アマゾン ウェブ サービス ジャパン株式会社の主催する AWS Summit Tokyo 2016 の Developers Conference(DevCon) に登壇いたします。

     

    講演タイトル 「DockerだらけのFRESHな動画配信プラットフォーム」

    日時: 2016/06/03 13:20 ~ 14:00

     

    講演概要:

    新たに開始した動画配信プラットフォームはMicroservices志向の元、全てのサービスをDockerを用いてAmazon ECSで運用しています。フルDockerのサービスが、サービス開発にどのような影響を及ぼし、新たな価値を生み出したのかについてご紹介します。

    イベントの来場申込みは こちら から可能です。@stormcat24 のセッションにご登録していただき、足を運んでいただければ幸いです。

     

    参考リンク:


    Flexible Blue Green Deploymentのススメ
    http://ameblo.jp/principia-ca/entry-12071871177.html

     

    Next FRESH! Applications with Amazon ECS
    https://speakerdeck.com/stormcat24/next-fresh-applications-with-amazon-ecs

     

    お前のDockerイメージはまだ重い
    https://speakerdeck.com/stormcat24/oqian-falsedockerimezihamadazhong-i

     

    オンラインで安全にスキーマ変更可能なpt-online-schema-change

    $
    0
    0

    こんにちは、技術本部の鬼海雄太(@fat47)です。

    Amebaのソーシャルゲーム全般のインフラを担当しつつ賃貸マンションの間取りを眺めたり、
    戸境壁の工法による防音性の違いについて日夜研究しています。
    いつかD値が60以上のマンションに住みたいです。

    さて、今回はWebサービスを運用しているとたびたび発生する
    「新機能開発の為にINDEX追加やカラム追加を行いたい」

    という課題を解決する方法をご紹介します。
    数年前まではそういった変更の際は、ゲームをメンテナンス状態にしてから
    スキーマ変更を行っていました。
    しかしゲームをメンテナンス状態にする事は、各方面への調整や利用者への影響が大きいため
    気軽にできることではありませんでした。

    オンラインでスキーマ変更

    そこで私達のチームではオンラインでスキーマ変更が可能なpt-online-schema-change(pt-osc)を導入することにしました。
    pt-online-schema-changeとは「Percona-Toolkit」に同梱されているツールで、

    通常はロックが掛かってしまうALTER TABLEクエリをロックなしで実行することが可能になります。
    MySQL5.6からは無停止でINDEX追加やカラム追加ができるオンラインDDLが実装されましたが、
    運用しているゲームのデータベースはMySQL5.5の為、このツールによりスキーマ変更を実現させています。


    pt-online-schema-changeの仕組み

    下記のような順番でスキーマ変更が行われています。

    1.対象テーブルAと同じスキーマ構造をした作業用テーブルBを作成

    2.作業用テーブルBに変更するALTER文を適用


    3.トリガーを3つ作成し、対象テーブルAへの挿入・削除・更新が作業用テーブルBに反映されるようにする


    4.対象テーブルAから作業用テーブルBへレコードをコピー


    5.RENAMEして対象テーブルAと作業用テーブルBを入れ替える


    6.入れ替え後の古いテーブルAとトリガー3つを削除する



    実際の利用例

    インストールは簡単です(例:CentOSに導入する場合)
    # yum install http://www.percona.com/downloads/percona-release/redhat/0.1-3/percona-release-0.1-3.noarch.rpm# yum install percona-toolkit

    あとは実行するだけです
    # pt-online-schema-change --execute --alter="ADD INDEX idx_hogehoge(hoge_id)" h=localhost,D=DB_NAME,t=TABLE_NAME,u=root
    オプション名動作内容
    --alterスキーマ変更を行うクエリを指定
    h接続先
    DDBスキーマ名を指定
    tテーブル名を指定
    u実行MySQLユーザを指定

    pt-online-schema-changeを慎重につかうオプション

    動作の仕組み上、テーブルのコピーやトリガーの処理が走るため、実行中は普段より高負荷になりやすいです。
    そのためpt-online-schema-changeには本番環境へ悪影響が出ないように、リソース状況をチェックしながら動作を制御できる便利なオプションが存在しています

    オプション名デフォルト設定動作内容
    --max-load25スレッド指定スレッド数を超えた場合、処理を一時停止
    --critical-load50スレッド指定スレッド数を超えた場合、処理を終了する
    --check-slave-lagなし全スレーブのレプリ遅延チェック(実行ユーザはMaster,Slaveで同じ権限が必要)
    --max-lag1秒指定秒数のレプリ遅延が発生すると処理を一時停止する
    --check-interval1秒--max-lagをチェックする間隔
    --dry-runなし実際には処理が行われないドライラン確認用オプション
    --executeなし処理の実行を行うオプション
    --nodrop-old-tableyesスキーマ変更前の旧テーブルのDROPを行わないオプション。確認で残しておきたい時に
    --set-varsなしMySQLの変数を渡すオプション。レプリケーションさせない時にset-vars="sql_log_bin=0"等

    過去の失敗事例

    このように色々な機能がある便利なツールですが、
    運用で利用できなかったパターンや事故が発生したこともあります。
    あまりこのような事例がネットの記事になかったのでいくつかご紹介したいと思います。

    事例1:外部キー制約で実行失敗
    発生環境
    pt-online-schema-change 2.2.14
    OS CentOS6.2
    MySQL 5.5.34

    検証環境でpt-online-schema-changeを実行
    # pt-online-schema-change --execute --alter="ADD INDEX idx_hoge(id_hoge)" h=localhost,D=d1,t=t_1,u=root 

    実行時のエラー文
    You did not specify --alter-foreign-keys-method, but there are foreign keys that reference the table. Please read the tool's documentation carefully.

    --alter-foreign-keys-methodというオプションで外部キー制約の動作を制御できるようです。
    https://www.percona.com/doc/percona-toolkit/2.1/pt-online-schema-change.html#cmdoption-pt-online-schema-change--alter-foreign-keys-method

    auto 

    自動判別するモード。rebuild_constraintsが使えれば使い、使えなかったらdrop_swapを使う

    rebuild_constraints 

    新しいテーブルを参照する外部キー制約を削除してから再追加する。高速

    drop_swap

    外部キーチェックを無効にする(FOREIGN_KEY_CHECKS = 0)、新しいテーブルを作成して元のテーブルを削除して入れ替える。

    この方法は高速でブロックしないが、元のテーブルを削除し一時テーブルの名前を変更する間の時間はテーブルは存在せず、それに対するクエリはエラーになる。

    このオプションでテストしてみます

    pt-online-schema-change --execute --alter-foreign-keys-method auto --alter="ADD INDEX idx_hoge(id_hoge)" h=localhost,D=d1,t=t1,u=root
    実行結果

    実行したマスターDBサーバで良さそうな感じの実行ログが出てご満悦。
    でしたが、スレーブのステータスを確認してみるとレプリケーションが停止していました。
    Last_SQL_Error: Error 'Cannot add or update a child row: a foreign key constraint fails *****

    事例にもある通り、サービス稼働時のdrop_swapは難しいかもしれません。
    事例2:複合主キーを持つ巨大テーブルで実行失敗
    発生環境
    pt-online-schema-change 2.2.13
    OS CentOS6.5
    MySQL 5.5.34

    実行コマンド
    # pt-online-schema-change --execute --alter="ADD INDEX idx_hoge_event(hoge_id, hoge_type, fuga_id)" h=localhost,D=d1,t=t1,u=root

    実行エラー文
    2015-03-28T00:54:36 Error copying rows from `d1`.`t1` to `d1`.`_t1_new`:2015-03-28T00:54:36 Error copying rows at chunk 1 of d1.t1 because MySQL used only 12 bytes of the PRIMARY index instead of 20.  See the --[no]check-plan documentation for more information.
    解決策はテーブル設計を変更して2列までの複合主キーにするしかなさそうです。
    PT3.0では解決してくれるのでしょうか?
    Chunking handles multi-column indexes poorly


    事例3:--set-vars="sql_log_bin=0"が無視されてエラー
    発生環境
    pt-online-schema-change 2.2.7-1
    OS CentOS6.2
    MySQL 5.5.24

    pt-online-schema-changeでは普通に実行してもレプリ遅延は発生しづらい作りになっているのですが、
    より遅延の発生をなくすために、sql_log_bin=0(binlogを出さないオプション)をつけて実行する運用をしている事がありました。
    マスター、スレーブ構成のDBで、先にスレーブ全台でonline-schema-changeでカラム追加してから、
    最後にマスターでsql_log_bin=0で実行してカラム追加を行う方法です。
    ※Rolling Schema Upgrade(RSU)というかっこいい呼び方もあるようです。
    この方法でカラム追加を行ったところ、マスターでpt-osc実行後にスレーブ全台のレプリケーションがエラーで停止しました。
    Last_SQL_Error: Error executing row event: 'Table 'hoge._fuga_table_new' doesn't exist'
    先程説明したpt-oscの仕組みの中で
    1.
    対象テーブルAと同じスキーマ構造をした作業用テーブルBを作成
    2.作業用テーブルBに変更するALTER文を適用
    3.
    トリガーを3つ作成し、対象テーブルAへの挿入・削除・更新が作業用テーブルBに反映されるようにする
    という動きがあります。

    本来はpt-oscのオプションでsql_log_bin=0をつけているのでこれらのクエリはスレーブ側には飛ばないはずです。
    しかし、仕組みの項目
    (3)のトリガー作成だけがスレーブ側で実行され、
    スレーブ側には既に存在しない「作業用テーブルB」への更新がトリガーで実行されてしまい
    エラーになってレプリケーションが停止しました。
    このエラーが発生する状況を調査したのですが、最終的にこれだという結論に辿り着くことはできませんでした。
    検証内容結果
    OSやMySQL、pt-oscのバージョン変更して検証エラー再現
    同じテーブル構造でレコード数を少ない状態で検証エラーなし
    別のテーブルでレコード数を数億件追加して検証エラーなし

    もし今後このRolling Schema Upgradeをする運用を続けるのであれば、
    仕組みの項目
    (1)の作業用テーブルBを事前にスレーブ側に作成しておくことでエラーの回避をすることが可能です。
    カラム追加するテーブル名が「user_account」の場合、「_user_account_new」という作業用テーブルを作成しておくことで、
    トリガーがもし作成されてレプリケーションの停止は発生しなくなります。


    事例4:テーブルのCOMMENTが日本語の場合すべて文字化けする
    発生環境
    pt-online-schema-change 2.2.14
    OS CentOS6.2
    MySQL 5.5.34

    このようなテーブル構造があるとします。

    pt-oscを実行します
    # pt-online-schema-change --execute --alter="ADD COLUMN hoge_test int NULL DEFAULT NULL COMMENT 'テストだよ' AFTER from_days" h=localhost,D=fuga,t=m_hoge,u=root
    日本語がすべて文字化けしてしまいました

    --charsetオプションでutf8を指定してみます。MySQLのSET NAMESとperlの標準出力を制御してくれます。
    pt-online-schema-change --execute --charset=utf8 --alter="ADD COLUMN hoge_test int NULL DEFAULT NULL COMMENT 'テストだよ' AFTER from_days" h=localhost,D=fuga,t=m_hoge,u=root
    無事文字化けしなくなりました


    事例5:Replicate_Ignore_Tableを使っているスレーブでレプリケーション遅延が発生する
    発生環境
    pt-online-schema-change 2.2.5
    OS CentOS6.2
    MySQL 5.5.24

    pt-oscをマスターで実行した際に、スレーブの設定でReplicate_Ignore_Tableで指定されているテーブルに
    スキーマ変更をかけた場合、本来ならばスレーブでは無視されるはずのクエリが実行されて、
    作業用テーブルが作成されてしまいデータのコピーが始まります。
    その間レプリケーションが遅延し続けてしまいます。
    Replicate_Do_Tableを利用しているサーバでは発生しませんでした。

    設定例
    スレーブ名レプリケーション設定
    スレーブAReplicate_Ignore_Table: Table_A
    スレーブBReplicate_Do_Table: Table_B
    スレーブCレプリケーションするテーブル指定なし

    実行例
    # pt-online-schema-change --execute --alter="ADD INDEX idx_hogehoge(hoge_id)" h=localhost,D=hoge,t=Table_A,u=root

    スレーブA
    [root@スレーブA ~]# ls -lh /var/lib/mysql/hoge/*.ibd-rw-rw---- 1 mysql mysql 21G 2月 18 11:02 2016 /var/lib/mysql/hoge/_Table_A_new.ibd-rw-rw---- 1 mysql mysql 42G 2月 18 11:05 2016 /var/lib/mysql/hoge/Hoge_Table.ibd-rw-rw---- 1 mysql mysql 11G 2月 17 17:30 2016 /var/lib/mysql/hoge/Table_B.ibd

    スレーブB
    [root@スレーブB ~]# ls -lh /var/lib/mysql/hoge/*.ibd-rw-rw---- 1 mysql mysql 11G 2月 17 17:30 2016 /var/lib/mysql/hoge/Table_B.ibd

    スレーブC
    [root@スレーブB ~]# ls -lh /var/lib/mysql/hoge/*.ibd-rw-rw---- 1 mysql mysql 21G 2月 18 11:02 2016 /var/lib/mysql/hoge/_Table_A_new.ibd-rw-rw---- 1 mysql mysql 59G 2月 18 11:02 2016 /var/lib/mysql/hoge/Table_A.ibd-rw-rw---- 1 mysql mysql 42G 2月 18 11:05 2016 /var/lib/mysql/hoge/Hoge_Table.ibd-rw-rw---- 1 mysql mysql 11G 2月 17 17:30 2016 /var/lib/mysql/hoge/Table_B.ibd
    スレーブAではTable_A以外のテーブルをレプリケーションするように設定されていますが、
    _Table_A_newというTable_Aの作業用テーブルが作成されてしまい全力でデータコピーが走っています。
    この間スレーブAではずっとレプリケーション遅延が発生してしまいました。
    これ以降の運用では、テーブル指定のレプリケーションをしている環境においては
    Rolling Schema Upgrade方式でスキーマ変更をかけることにしました。

    まとめ

    pt-online-schema-changeはかゆいところに手が届く便利ツール!
    銀の弾丸ではないので入念な事前検証と動作確認を!

    Slackを導入してみました~管理者編~

    $
    0
    0

     

    こんにちわニコニコ

    Make Your Goals Come True. Easier + Faster.

    をコンセプトに開発環境の改善や統一化をおこなっているanzaiです。

     

    2013年10月よりチャットツールとして某Hを導入しておりましたが、2015年12月にSlackへ乗り換え(移行し)ました。

    今回は、管理者視点で以下項目について書かせていただきました!

    • 数あるチャットツールの中から、なぜ、Slackを選んだのか?
    • 移行時に意識したところ
    • はまったところ
    • 大規模組織で運用してみて
    • 今後の課題

    ※以下記載した情報は移行時点のものです。Slackは日々アップデートが行われていて、新しい機能やAPIが実装され続けています。あくまでもご参考までとして、移行を計画する際には、最新情報をご確認ください!

     

    数あるチャットツールの中から、なぜ、Slackを選んだのか?

    • Integrationが豊富である

    昨今、IT界隈で"CI"が注目のキーワードであるように、弊社でもCI環境については常に見直しが検討されています。
    そういった流れの中で、Integrationが豊富で、様々なCIツール群と連携させることが可能なSlackへの移行は必然的という印象です。
    弊社では以下の図のように、CI環境を構築し、GHEやJenkins等のツールからの通知をSlackに出すことで、業務の効率化や品質向上を目指しています。

     

    • 通知(メンション)が気付きやすい

    Slack上でメンション(@ユーザ名)を付けられて呼ばれた場合、通知や会話があったチャットルームの更新情報が、オンラインになった時に通知されます。

     

    某Hはオフラインからの復帰時に通知されない仕様だったので、これは気付きやすくて便利です。

    また、チャットルーム毎に通知の仕方を変更できるので、チャットルームの目的・用途に応じて通知レベルの変更も可能です。

    また、デスクトップのアプリと、モバイルアプリで違いを付けることもできます。 


    より良いCI環境を整備していく上では、小さなメリットと思われがちの機能ですが大きなメリットと考えています。

     

    • UIの良さ

    SlackのUIは直感的でわかりやすいUIです。弊社でも多くの方がこのポイントをメリットとして挙げ、Slackに移行したいという要望が多くありました。
    また、サイドバーの色もデフォルトで8パターンあったり、自分で色を変更できたりと、UIの色のカスタマイズが出来るところも愛着が湧きます。

    他にも、コメントの修正が簡単、絵文字が豊富などの良さもあります。

     

    移行時に意識したところ

    弊社はチャットツールへの依存度がかなり高いです。

    コミュニケーションだけでなく、サービス状況のレポーティング等も行われているなど、某H上の会話履歴がなくなってしまったり、移行時に短時間でも利用できなくなることは業務に与える影響が大きいと考え、

    業務に一切影響を与えずに、スムーズにユーザがSlackへ移行できること

    を重要な要件とし、全てのチャットルーム、会話履歴、添付ファイルを営業時間外で移行することにしました。

    また、1日も早くSlackを利用したいという声が大きかったので、2015年12月にまずは、移行の必要のないチャットルームやダイレクトメッセージを利用したい方向けに、Slackをリリースしました。

    はまったところ

    • 某Hのエクスポート機能

    某Hからエクスポートしたデータをインポートしてくれる機能がSlackにはデフォルトで備わっていますので、当初は某Hのエクスポート機能を利用しデータをSlackへ簡単にインポート出来るであろうと考えていました。

    しかし、その目論見は外れ…某Hのエクスポート機能は利用できなかったのです。 。

    何千個もルームがあることや、ルーム名に日本語や記号を多様していたためかもしれません。

    とにかく、エクスポートが途中で止まってしまい、某Hへ問い合わせをしても解決することはできませんでした。

    • 不要なデータ(チャットルーム)の洗い出し

    某H上にあるデータ量が膨大であることがは、移行時の足かせになることはエクスポート問題が発生した時に学んだので、まずは、不要なデータ(チャットルーム)を削除することにしました。

    某HのAPIは、非公開チャットルームであっても最終更新やオーナー情報を取得することが可能でしたので、それを利用し、すべてのチャットルームの情報を取得しました。ここまではさくさくっと進んだものの・・・。

     

    チャットルームの要/不要はオーナーにしか判断できないため、約7000件を超えるチャットルームのオーナーへひたすらヒアリングする作業が必要になりました。

    もちろんできる限りヒアリングを自動化しましたけど・・・詰めの部分は人の手がどうしても必要になり、これが、なんと、大変だったことか。。

    さらには、ヒアリングをしている間にも新しいチャットルームは次々と作成され・・・ヒアリングの繰り返し…途方も無い闘いでしたが、6000件近くの不要なチャットルームを削除し、最終1150個のチャットルームに絞りました。

    • 某HとSlackの仕様把握

    某HのAPIを活用して、会話履歴等を取得して、Slackの仕様に合わせてCSV出力させるスクリプト作成する上ではまったところです。※SlackではCSVファイルから会話履歴と添付ファイルをインポートすることが可能(参照:csv import

    • Slackのインポート機能を使うと、エクスポートした会話履歴にいるメンバーがチャットルームに参加した状態になってしまう。※つまり、会話をしたことがある人は、退席していても参加している状態になり、逆に一度も会話してな人は参加してない状態になってしまうのでメンバーの精査と追加が必要になります。
    • 某HのAPIでは、公開チャットルームのメンバーは移行開始時にログインしているメンバーしか取得できない。
    • Slackではチャットルーム名に英数字しか使用できない
    • ダブルクォーテーションにはバックスラッシュを付けないとインポートに失敗する
    • HTMLタグは削除しないとインポート時にタグがそのまま表示される
    • 某Hは最新順でデータはき出すので、古い順に時系列をソートしないといけなかった。
    • jqコマンドによるSlack仕様へのデータ変換

    某HとSlack間の仕様を合わせる箇所が多くて、jqコマンドで変換したんですが、

    会話履歴が10MBytes程度のチャットルームを変換するのに6時間もかかりました。当初は移行にそれほど時間がかからないと思っていましたが、2015年12月のリリースから移行完了まで2~3ヶ月の時間を要してしまいました。。

      大規模組織で運用してみて

      • UIはすべて英語

      Slackは日本語未対応です。そのためUIはすべて英語です。※2016年度内に対応するという噂ありw
      英語というだけで拒否反応を起こしてしまう方も中にはいるので、全ての職種の方へ浸透させるには大きなハードルになりますので、弊社ではwikiにマニュアルを作成しました。

      • Usernameとかチャットルーム名とか

      英数字であれば21文字制限付きですが、自由に設定できます。

       

      管理者からすると、これが地味に困ってしまう問題です。

      弊社ではUniqueなPersonalIDは、メールアドレスの左側か社員番号なんですが、
      メールアドレスをUsernameにしようとすると21文字を超えてしまう方がいたり・・・

      自由に設定できてしまうことで、あだ名を設定されてしまう方がいて、誰なのかわからない状況になってしまったり・・・

      大規模でSlackを利用する場合は、検索及び表示における利便性を考えて最低限のルールを策定したほうがベターです。

      • 非公開チャットルームへ管理者権限でもアクセスできない

      これはユーザーからするとメリットですが、管理側からすると若干デメリットでもあります。

      例えば、非公開チャットルームを操作するには、管理者自身もそのチャットルームのメンバーになる必要があったり、非公開チャットルームのデータは管理者権限でもエクスポートできないので、つまり、他チャットツールへの移行時には非公開チャットルームの移行が大変になります。

      今後の課題

      • Integrationとセキュリティ

      新しいIntegrationが次々と増え、利便性が向上するに比例して、誤って重要な情報を外部に漏えいする危険性も高くなりますので、どのIntegrationをどの様に利用しているのか、情報漏えいしてしまうような使い方をしていないか、定期的にチェックをしていく予定です。

      新卒研修環境の構築をTerraformで自動化してみた

      $
      0
      0

      アドテクスタジオのDynalystというチームで働いている黒崎 (@kuro_m88) です。
      たまに社内で自作ドローンを飛ばしたりしています。

      早いもので入社2年目になりました。つい先月まで新卒だったはずなのですが…(・・;)

      今年も新卒の技術者が約60名入社し、新入社員全体の研修が終わり現在はエンジニアの技術研修が行われています。 今回はその環境構築で行ったことについて紹介しようと思います。

      エンジニアの新卒研修の概要

      今年の研修のゴールは「アーキテクチャをゼロから考え、実装できるようになる」というもので、研修課題は2つあります。

      1つ目は現在まさに取り組んでもらっているのですが、2週間でミニブログシステムを3~4名のチームで制作してもらいます。 チームによってスキルセットが違うので、ネイティブアプリに特化するチームもいれば、バックエンドやインフラでいかにスケールしやすい構成にするか、みたいな所を工夫するチームもいるんじゃないでしょうか。

      今回の研修の特徴として、各チームにAWSアカウントと検証端末(Android, iOS)を渡し、あとは自由に技術選定/計画/開発を進めてもらう方式をとっています。

      2つ目はまだ秘密です。これは社外秘というより新卒にもまだ発表されていないので、書けません(笑) チームメンバーの入れ替えがあり、開発も発生すると予想されるので各AWSのアカウントのIAMユーザを再発行する必要があります。

      検証端末は、社内にあるものを各チームに配布するとして、AWSのアカウントをどうやって配布しようかという事を考えはじめたのが今回のきっかけです。

      要件を考える

      AWSアカウントを渡して自由に使用してもらえるばいいだけなので、チームごとにAWSアカウントを発行して、クレジットカード番号を入力したあとrootアカウントのメールアドレスとパスワードの組み合わせをそのまま配布するのが一番簡単そうです。 もちろんこれはかなり危険というかリスクが大きいのでやりたくありません。

      研修ですし、クラウドをがっつり触るのは初めてだという新卒も沢山いますので、できるだけミスが許容できる環境が必要です。 それらの要件を考えると、

      • 運営用のAWSアカウント (一括請求で紐付けるための親アカウントになる)
        • 運営用のIAM User (管理者権限)
        • 監視用のIAM User (読み取り権限のみ)
        • CloudTrail (運営アカウントの操作ログを保存)
        • CloudTrailのログ保存用のS3バケット
      • チーム用のAWSアカウント
        • メンター用IAM User (メンター社員が直接ログインしてサポートする必要が出た時に使う, 管理者権限)
        • チーム用のIAM User(チームのメンバーが作業する用, 管理者権限 & コスト確認のために請求情報を閲覧する権限)
        • 監視用のIAM User (読み取り権限のみ)
        • CloudTrail (チームアカウントの操作ログを保存)
        • CloudTrailのログ保存用のS3バケット

      チームは12チームありますので、チーム用のAWSアカウントは12個作成します。

      監視用のIAM Userを作った理由ですが、DataDogと連携させて各チームのリソースの使用状況やコストを一覧できるようにするためです。 DataDogで監視して明らかに異常な変化があればそのAWSアカウントにログインして詳細を確認した後に聞き取りをし、場合によっては直接対応することでクラウド破産その他のリスクを抑えます。

      運用方法

      各チームには設定済みのチーム用のIAM Userのみを渡し、AWS Console上から自由に操作をしてもらいます。 請求情報も閲覧を許可しているのでコストも各自把握して決められた予算内に収まるように調整してもらいます。

      もし誤ってチーム用のIAM UserのパスワードやAPIキーを外部に漏らしてしまった場合はメンター用のIAM Userでログインし、チーム用のIAM Userを再作成します。 メンター用のIAM Userでもログインできないような事になった場合はrootアカウントでログインしてCloudTrailのログ以外の全てのリソースの削除ができるようにしておきます。

      今回は初期構築を自動化するためにrootアカウントのAPIキーを発行して使っています。 AWSのベストプラクティスからするとrootアカウントのAPIキーを発行することは推奨されないのですが、少しでも手作業を減らすためにこの方法を取りました。 rootアカウントのAPIキーは社内のサーバに保存し、そのサーバからのみ作業を行うようにしました。

      Terraformとの出会い

      AWSで構成を自動化するサービスといえばCloudFormationですがJSONのみで設定を記述するので、構成を定義したJSONを書いたり、それを各チーム分生成するのが大変そうという印象がありました。 その次に思い浮かんだのがTerraformというツールで、名前を聞いたことがある程度だったのですが、ドキュメントのAWSの項目を読んでみて、これならある程度自動化しやすそう!と思ったのでさっそく検証に入りました。 https://www.terraform.io/docs/providers/aws/index.html

      余談ですが、ProviderとしてGithubもあったのが驚きでした。GithubのOrganizationのアカウントやチームの管理もTerraformで記述できたら面白そうです。

      Terraformで設定を記述

      最終的に出来上がったものが以下のリポジトリにあります。

      https://github.com/CyberAgent/ca-16th-training-terraform

      誤って認証情報をcommitしてしまうのが怖かったので、開発を行うサーバと実際に適用をするサーバを分けて作業しました。 アドテクスタジオには個人の開発用にOpenStack環境が用意されているので、こういう時に便利です。 このリポジトリの中を役割ごとに紹介していこうと思います。

      aws-admin

      ca-16th-training-terraform/aws-admin

      運営用AWSアカウントの設定です。

      先ほど決めた要件を設定に落とし込んでいます。

      IAMユーザのログインパスワードの設定ですがTerraformで設定をする方法がなかったため、provisionerとしてlocal-execを使い、ローカルでawsコマンドを叩いてパスワードを設定するようにしました。

       

      123456
      resource "aws_iam_access_key" "admin_user" {user = "${aws_iam_user.admin_user.name}"provisioner "local-exec" {command = "AWS_DEFAULT_REGION=${var.region} AWS_ACCESS_KEY=${var.access_key} AWS_SECRET_ACCESS_KEY_ID=${var.secret_key} aws iam create-login-profile --user-name ${aws_iam_user.admin_user.name} --password ${var.admin_user_password} || AWS_DEFAULT_REGION=${var.region} AWS_ACCESS_KEY=${var.access_key} AWS_SECRET_ACCESS_KEY_ID=${var.secret_key} aws iam update-login-profile --user-name ${aws_iam_user.admin_user.name} --password ${var.admin_user_password}"}}

       

      aws-team

      ca-16th-training-terraform/aws-team

      こちらも先ほど決めた要件を設定に落とし込んでいます。 基本的には aws-admin と似た構成になっていますが、チームのパスワードは初回ログイン時に変更を強制するようにコマンドオプションがついています。 (--password-reset-required)

      bin

      ca-16th-training-terraform/bin

      実際にTerraformの適用を行うためのスクリプトをここに記述しました。

      ここが一番面倒でした。 普段Terraformを使うとなると1アカウントに対して適用することが多い思うのですが、今回は順番に12アカウントに対して適用していきます。

      そうなると困るのが tfstate ファイルの扱いです。 tfstate ファイルはTerraformが扱うリソースの状態が記述されるファイルで、適用するときにはこのファイルを元にどのようなオペレーションを行うかがスケジューリングされます。

      アカウントごとに読み込む tfstate ファイルを分けて扱わないと、 tfstate に記述されているリソースの状態と実際のリソースの状態に矛盾が生じて意図したオペレーションが正しく行われなく鳴ってしまいます。

      そこで、

      • ローカルにある tfstate ファイルを削除する
      • tfstate ファイルをS3から読み込む
      • 適用する
      • tfstate ファイルをS3に書き出す
      • ローカルにある tfstate ファイルを削除する

      このような手順で操作を行い、S3から読み書きする tfstate ファイルを操作しているチームに合せて変えるシェルスクリプトを書きました。

      たとえば、1チーム分のterraform apply をするスクリプトはこのようになっています。

       

      123456789101112131415161718
      #!/bin/bashexport AWS_DEFAULT_REGION="ap-northeast-1"TERRAFORM=~/terraform/terraformbucket=$1team=$2pushd . > /dev/nullcd `dirname $0`/../aws-teamrm -rf .terraformrm -rf terraform.tfstaterm -rf terraform.tfstate.backup${TERRAFORM} remote config -backend=S3 -backend-config="bucket=${bucket}" -backend-config="key=aws/${team}"${TERRAFORM} plan --var-file=../conf/account_${team}.tfvars${TERRAFORM} apply --var-file=../conf/account_${team}.tfvars${TERRAFORM} remote pushrm -rf .terraformrm -rf terraform.tfstaterm -rf terraform.tfstate.backuppopd > /dev/null

       

       

      このスクリプトをさらに別のスクリプトから全チーム分について呼び出すことで一度にセットアップができるようにしました。

       

      conf

      ca-16th-training-terraform/conf

      アカウントごとのAPIキーやユーザ名等必要な変数をここに記述しておきます。

      ここで記述される設定は秘密にしておかなければならないものなので、誤ってpushされたりしないように気をつけなければいけません。

      実際に流してみた

      実際に12チーム分の設定を流してみた動画です。 terraform plan してこれから実行される内容を表示したあとに terraform apply して実際に適用しています。 AWSのaccount idやユーザ名など、見えるのがあまりよくなさそうなものはシェルスクリプトでマスクして見えないようにしています。

      3分も掛からずに適用が終ったのが分かります!!

      https://www.youtube.com/watch?v=ikiG5zOsKXU

      Terraformでできなかったこと

      今回、全部の操作がTerraformで自動化できたわけではありません。

      • AWSアカウント自体の作成
      • クレジットカード番号の入力
      • rootアカウントのAPIキーの発行
      • IAM Userが請求情報を閲覧できるようにする設定
        • IAMの権限とは別にrootアカウントから許可する設定をする必要があります

      これらの操作はrootアカウントでAWSコンソールを操作して設定をする必要があったので、人事の方に設定をお願いしました…ありがとうございます m(_ _)m

      さいごに

      AWSのアカウントを大量に初期設定する方法について紹介しました。 本当はAWSだけじゃなくGCP, Azureなど各種クラウドを自由に選定して使ってもらう環境を用意したかったのですが、技術研修の期間(約1ヶ月半)のわりに予算や請求の管理が大変そうだったので今回はAWSのみ、そのかわりAWSのサービス内であれば自由に選定して触ってもらえるようにしました。

      弊社では技術選定が自由に任されているため、実務でも本当に色んな場面でオペレーションの自動化をしたい場面に遭遇します。

      今回のようなちょっとしたアカウント管理を自動化したいという要望からオンプレやクラウド関係なくガッツリインフラの構築を自動化する仕事まで、どんどん自動化を推進する仕事に興味ある学生の方は夏前にインターンの募集が始まると思いますので、ご応募お待ちしております! https://www.cyberagent.co.jp/recruit/fresh/internship/ (2016/04/26時点ではまだ募集を開始しておりません、もうしばらくお待ち下さい)

      おまけ

      1チーム分を plan => apply => destroyの操作をやってみたバージョンの動画です。

      https://www.youtube.com/watch?v=KvYv8NzBAtQ

      OpenSMILEで音楽分析してみる

      $
      0
      0

      Goðan daginn!(こんにちは!)

      秋葉原ラボのY.F.です。

       

      ゴールデンウィーク、みなさんいかがお過ごしでしたか?

      ブルーラグーン

      アイスランドまで足を伸ばしてみました。

      さて、連休明けにブログを書けと言われたもので、帰りの機内でぽつぽつ執筆しているのですが…

      今回は、音楽から様々な特徴や情報を取り出すツール兼ライブラリ"OpenSMILE"で色々と遊んでみることにします。

      ※OpenSMILEは個人・研究(企業での基礎研究も含む)用と商用ではライセンスが異なります。今回はあくまで基礎研究として取り扱っています。

      まずはOpenSMILEのダウンロードはこちら。[1]

      おおむね、音楽分析の流れとしては

      • 音楽(=サンプリングされた数値の列)を入力
        • →これを基に一部情報を取得
      • フーリエ変換でもって周波数単位のデータに変換
      • 各種分析を行って結果を出力
      • 様々な曲の結果データから音楽の特性を判定したり、類似する音楽を探しだしたりする

      となります。


      今回はOpenSMILEの中の手法を使って、音楽の主旋律を取り出す処理を試してみることにします。とはいえ実際の曲を使うと色々と大変なのでテストデータ[2]で。
      以下の図のように進めていきます。

      (0) 元データの波形 元データ

      (1) 時間単位に分割→窓関数をかける

      音楽データを一定の秒数ごとに分割します。分析したい周波数領域によって長さを調整します。音楽であれば50ms-200ms程度。今回は50msごとに、前後50ms(合計100ms)ずつ分割しておきます。

      さらに前後のデータの干渉をある程度取り除くために関数を掛け合わせます。

      (2) フーリエ変換→周波数スペクトルを算出

      大学で理工系を履修された方には聞き覚えがあるんじゃないかと思いますが、一応(離散フーリエ変換について)ざっくり書くと、ある数値ベクトルx_i(i=0, 1, ..., n-1)に対するフーリエ変換は

      となります。これをうまいことバタフライ演算などでコンピュータ上で計算するもとを高速フーリエ変換(FFT)というわけですね。

      フーリエ変換によって、元の波形データからは1波長~(データ数/2)波長の周波数ごとの強度が理屈の上では取得できます。

      計算上、(1)で分割したデータからは、1波長=1/0.1(/s)=10Hz以上のデータが取得できることになるはずです。

      今回のテスト用の音楽は22kHzがサンプリング周波数なので、上限は自ずと半分の11kHzになります(ナイキスト周波数)。

      (3) 音程ごとに集約

      現代音楽で一般的に使われている(と思われる)平均律は、A(ラの音)=27.5Hz の2の累乗をベースに、1音ごとに"2の1/12乗"ずつ積み上がっていくようになっています。

      これを踏まえて、(2)で算出した周波数スペクトルを音階ごとに振り分けていきます。

      Subtone結果

      (4) CHROMA

      (3)の結果をオクターブを外した音階(ドレミ…)ごとにまとめます。

      CHROMA結果

      (5) 主旋律を選択

      (4)のうち、分割した単位ごとに最大の音階を計算すれば完成です。

      CHROMA最大値(全域)

      拡大してみると

      CHROMA最大値(先頭25秒)

      (3)の段階でも最大値を取ってみるとMIDIのようにデータが取れている(気がします)。

      Subtone最大値(全域) Subtone最大値(先頭25秒)

      いかがでしょうか? 簡単でしたが、皆さんもお試しあれ。

      ただし著作権のある市販の音楽などで試す場合はあくまでも個人で楽しむ範囲にしましょう。

      今回はゴールデンウィーク明けということで、こんな感じで失礼しますm(_ _)m

       

      参考

      VRで美少女を踊らせてみた(仮)

      $
      0
      0

       

      ゲーム技術戦略室のミカエルキュン佐藤です。

       

      この度、サイバーエージェントグループで、ゲーム関連の最新技術ばかりを扱う新しい組織「ゲーム技術戦略室」を立ち上げました。

      宣伝も兼ねてブログを書かせていただきたいと思います。

       

      ゲーム技術戦略室では、UnrealEngine・Amazon Lumberyard・CryEngineといったゲームエンジンだったり、Oculus Rift・HTC Vive・Play Station VRといったVRの話だったり、さらにはAppleTV・AppleWatch・ChromeCast・Leap Motion・Theta Sといったガジェット系などなど、色んな最新技術に対してガツガツ攻めていきます。

       

      技術戦略室で取り組んだ調査や制作物に関しては、下記の公式ブログにて展開していきますので、ブックマークをお願いいたします!

       

      TechnoloGEEK ~ゲーム技術戦略室 公式ブログ~

       

      さて、前置きはこのくらいにして、実際に技術に触れていきたいと思います。

      今回は”UnrealEngine+MMD+VR”を題材にします。

       

      Unreal Engineに触れてみる

       

       Unreal Engineという統合開発環境をご存知ですか?昨年7月に発表された、「FINAL FANTASY VII REMAKE」がUnreal Engine4を採用したというニュースを耳にした方も多いのではないでしょうか?このニュースがUnreal Engineのランチャーメニューに表示されていたのを見て私たちも心が踊りました。また、Unreal Engineは昨年3月より誰でも無料で利用することが可能となりました。その影響により、企業だけでなく多くの一般ユーザによるUnreal Engineを使った開発が活発になったように思えます。最近では「UNREAL FEST」や「Unreal Engine MeetUp」などUnreal Engineに詳しい方たちによる講演が各地で行われています。

       

      Unreal Engineでここみんを踊らせてみる

       「ここみん」とは、Amebaによって運営されているスマートフォン向けソーシャルゲーム「ガールフレンド(仮)」に登場する「椎名心実」というキャラクターです。

       

       

       このように、Unreal Engineは簡単にVRプレビューができますし、MMDのモデルを使うことによって好きなキャラクターを動かすことも容易です。マーケットプレイスで販売されているアセットの数も徐々に増えてきているので、プロダクトの開発速度も加速していくことでしょう。

       

      Unreal Engineでここみんを操作するためにやったこと

       

       今回、Unreal EngineにMMDモデルを読み込むために利用したものとしては、IM4UプラグインによるMMDモデルインポート、MMDとMMDBridgeを使ったモーションデータの出力となります。ここからは実際に行った手順をスクリーンショットと併せて説明していきます。

       

      開発環境

      Intel(R) Core(TM) i7-4790 CPU @ 3.60GHz
      実装RAM 16.0GB
      64ビット オペレーティングシステム
      Windows 8.1 Pro
      Unreal Engine 4.10.4
      Oculus Rift DK2
      Unreal Engineの新規プロジェクトにて利用したテンプレート:Third Person

       

       

      使用したモデルは下記書籍に収録されています。

       

       

       

      使用したモーションは下記の通りです。ありがとうございました。

       

      モーション名作者
      H57_SO_女の子走り速め_s2500_p20.vmdsusuki様
      歩行モーション_おんな_ゆっくり.vmdノラ様
      11.やったぁ.vmdおかっち様
      rea_stand.vmdあー、とり様
      海自っぽい敬礼(海自).vmdMcDuck<めぐみ>様
      ようかい体操第一_配布用.vmdjmccmy様

       

      Unreal EngineでOculus Riftを使う前に

       Unreal Engine4.10.4では、Oculus Runtimeのバージョンが0.8でなければVRプレビューを利用できませんでした。Oculus Runtime のインストール後はコンピュータを再起動しましょう。(※Unreal Engine4.11以降の場合は Oculus Runtimeのバージョンが0.8で動作しません。公式サイトより最新のRift Runtimeをインストールして下さい。)

       

       

      Unreal EngineでMMDモデルを読み込む

       モデルインポートには、Unreal EngineにMMDモデルをインポートする時に便利なIM4Uプラグインを利用しました。こちらは、GitHubからアクセスすることができます。ご利用のUnreal Engineのバージョンに合ったプラグインを取得して下さい。


      作者:bm9様
      MIT License

      https://github.com/bm9/IM4U/tree/4.10.X


       ダウンロードしたIM4Uプラグインは、作成したUnreal Engineプロジェクトのフォルダ内に「Plugins」フォルダを作って中に入れます。

       

       

       

       KokominTest.uprojectを起動し、上部メニューの「編集>Plugins」からIM4Uプラグインが有効になっているか確認します。チェック操作をした場合は、一度エディタを再起動して反映させます。

       

       

       

       

       コンテンツブラウザ(ウィンドウ>コンテンツブラウザ)を開き、お手持ちのMMDモデルデータをドラッグアンドドロップしてインポートします。MMDインポートオプションでは、「Import Morph Targets」、「Import Materials」、「Import Textures」にチェックを入れて「Import All」ボタンをクリックして下さい。

       

       

       

       スケルタルメッシュを開くと「メッシュに有効なスケルトンがありません。新しいスケルトンを作成しますか?」と表示されますが「はい」をクリックして問題ありません。シェーダーのコンパイルが終わるとモデルが表示されるはずです。

       

       

       

      ここみんにモーションを付ける

       ここまでの操作でモデルデータを読み込めましたが、まだ動作モーションが付いていないので歩いている動きなどを表現することができません。そこで、Unreal Engineでインポートしたモデルに適用可能なモーションデータを作っていきます。モーションデータの作成方法については「【MMDBridge】 VMD出力機能」という作者による投稿動画が参考になります。「元々人柱向けのプラグインのため、運が悪いと動かないこともあります」とも書かれているので、残念ながら確実に動作するという保証は無さそうです。また、「Unreal Engine4でMMDモデルに表情を付ける方法」にて操作手順を記しているので必要であればご覧になられて下さい。

       


       無事にインポートできると、スケルタルメッシュのアニメーションタブを開いたときに、アセットブラウザウィンドウ内でアニメーションシーケンスがある事を確認できます(スクリーンショットでは4つのモーションデータをインポートしています)。これを使ってキャラクターの動きを制御していきます。

       

       

      ここみんの動作を制御する

      歩行モーションを設定する

       今回は歩行モーションの作成に、単一の入力値に基いてブレンドされる「ブレンドスペース1D」と呼ばれるアセットを使います。ブレンドスペース1Dを作成したいフォルダで下図のように選択して下さい。

       

       

       

       スケルトンを選択するように促されるので対象のスケルトンを選択します。

       

       

       

       ブレンドスペース1Dを開くと、中央下部にアニメーションシーケンスを配置できる一本の線が確認できます。赤い枠内にアニメーションシーケンスをドラッグアンドドロップして下さい。

       

       

       

       「椎名心実_待機_椎名心実_Skeleton2」を配置したのが下図になります。プレビュー中のここみんが動き始めました。

       

       

       

       次に、待機モーションをドラッグアンドドロップした場所から少し離した場所に、歩きモーションをドラッグアンドドロップします。すると、X軸を移動させることで自然に待機モーションから歩きモーションに切り替わっていくことが確認できると思います。

       

       

       

       最後に、歩きモーションをドラッグアンドドロップした場所から少し離した場所に、走りモーションをドラッグアンドドロップします。

       

       

       

       以上で歩行モーションの作成は完了です。

       

      ジャンプモーションを設定する

       ジャンプモーションは待機モーションと異なり、3つのアニメーションコンポジットに分割する必要があるので少し手間がかかります。まず、アニメーションコンポジットを作成したいコンテンツブラウザ上で右クリックして下図のように選択します。こちらもブレンドスペース1D同様に、スケルトンを選択するよう促されるので対象のスケルトンを選択しましょう。

       

       

       

        ここでは、JumpStartAnimComposite、JumpLoopAnimComposite、JumpEndAnimCompositとして3つ作成しておきました。まず、JumpStartAnimCompositeを開いてみると下図のような画面が表示されるので、赤い枠内に「椎名心実_ジャンプ_椎名心実_Skeleton2」をドラッグアンドドロップします。

       

       

       
       すると、ここみんがジャンプを開始します。しかし、JumpStartAnimCompositeにはジャンプを開始してから一番高い地点になるまでのモーションを設定したいので、開始時間と終了時間を設定してアニメーションシーケンスから切り取りましょう。上記スクリーンショットの赤い枠内に表示されているコンポジットトラックの上部分をクリックすると詳細ウィンドウが表示されます。この詳細ウィンドウで開始時間と終了時間を指定できるので、それぞれ「0.0」、「0.536」に設定しました(0.536秒の地点がジャンプしてから一番高い位置だったためです)。

       

       

       

        JumpLoopAnimCompositeにはジャンプ中の状態を設定したいので、開始時間と終了時間を共に「0.537」に設定しておきます。

       

       

       

        JumpEndAnimCompositにはジャンプ中からジャンプ終了までのモーションを設定したいので、開始時間と終了時間をそれぞれ「0.538」、「1.35」に設定しました。

       

       

       

       以上でアニメーションコンポジットの設定は完了です。次に、アニメーションブループリントを使ってここまで作成してきたアニメーションアセットを状態によって切り替えていきます。これにより、ここみんが動いていない時は待機モーションが実行されたり、空中にいる時はジャンプモーションが実行されたりといったことが可能となります。

       

      アニメーションブループリントを設定する

       アニメーションBP(ブループリント)はドキュメントによると、スケルタルメッシュのアニメーションを制御するグラフとのこと。アニメーションBPには「EventGraph」と「AnimGraph」という2つの主要コンポーネントがあり、この2つが連動して各フレームの最終アニメーションが作成されます。アニメーションBPを作成したいコンテンツブラウザ上で右クリックして下図のように選択します。

       

       

       

       対象のスケルトンを選択して生成されたアニメーションBPを開きましょう。

       

       

       

       下図の赤の枠で囲まれたノード(最終アニメーションポーズ)に流し込まれたアニメーションポーズをここみんがやってくれることになります。ですので、流し込むアニメーションポーズを条件によって切り替える必要があります。

       

       

       

       今回はステートマシーンを使ってキャラクターのアニメーションを切り替えていきます。アニムグラフタブ内のどこかで右クリックして「新規のステートマシーンを追加...」を選択します。

       

       

       

       生成されたステートマシーンノードを最終アニメーションポーズに接続し、ダブルクリックして「Kokomin State Machine」(名前は変更しました)を開きます。

       

       

       

       「Kokomin State Machine」内では、既に作成している「歩行モーション」、「ジャンプスタートモーション」、「ジャンプループモーション」、「ジャンプエンドモーション」の状態を作ります。これらの状態を条件によって切り替えることで、最終アニメーションポーズに最適なアニメーションポーズを流し込みます。「Kokomin State Machine」内のどこかで右クリックして「ステートを追加...」を選択します(名前には分かりやすいものを付けます)。

       

       

       

       生成された状態をダブルクリックで開くと、最終アニメーションポーズノードがあることを確認できます。最終アニメーションポーズに流し込まれたアニメーションポーズが、この状態から出力されるアニメーションポーズとなります。ステートの名前に「Wait/Walk/Run」と付けたので、歩行モーションアセットをアセットブラウザからドラッグアンドドロップして最終アニメーションポーズに接続します。

       

       

       

       「KokominBlendSpace1D」ノードには入力ピンがありますが、これが「KokominBlendSpace1D」を開いたときに表示されていた、アニメーションを滑らかに切り替える線のX軸の値です。入力する値をここみんの移動速度によって変化させたいので「Speed」という変数を作ります。変数はマイブループリントタブの変数項目で追加できます。

       

       

       

       Speedには値を代入していく必要があるのですが、今回はThirdPersonテンプレートを使っているので、「Mannequin>Animations>ThirdPerson_AnimBP」を参考にします。

       

       

       

       グレーマンアニメーションBPのイベントグラフタブを見ると、ここみんに必要な実装が載っているので全てコピーします(実際ステートマシーンもグレーマンアニメーションBPと同じように実装すれば問題ないです)。この時、「IsInAir?」という変数が無いので作っておきます。

       

       

       

       この状態で左上の保存とコンパイルを実行すると、プレビュー中のここみんが動き出します。まだジャンプの状態は作っていませんが、動作確認をしたいのでデフォルトのキャラクターとここみんを入れ替えてみます。コンテンツブラウザの「ThirdPersonBP>Blueprints>ThirdPersonCharacter」を開いて下さい。

       

       

       

       ビューポートタブを選択してグレーマンをクリックすると、自動的に詳細タブが開かれます。詳細タブ内の「Mesh」項目でスケルタルメッシュを設定できるので、作成したここみんのスケルタルメッシュを選択すると...

       

       

       

       表示されるキャラクターがここみんになりました!アニメーションが再生されていないようなので「Animation」項目の「Anim BluePrint Generated Class」で「KokominAnimBlueprint」を選択します。これでここみんが動き始めるはずです。

       

       

       

       ゲームをプレビューしてみます。うむ...良い。

       

       

       

       ただ、待機モーションから走りモーションまでの遷移が早過ぎるので、「KokominBlendSpace1D」のX軸範囲を修正します。これでモーション遷移の違和感が抑えられました。

       

       

       

       ...と、完成した感が半端ないのですがジャンプの状態を作りましょう。作成したジャンプモーションアセットに対応した状態を作りたいので、「JumpStart」、「JumpLoop」、「JumpEnd」という3つの状態を作りました。

       

       

       

       まず「JumpStart」の設定です。アセットブラウザから「JumpStartAnimComposit」をドラッグアンドドロップして最終アニメーションポーズに繋ぎます。この時、デフォルトでループアニメーションのチェックが入っているのでチェックを取り除いておきます。これで設定は終わりです。同様の設定を「JumpLoop」、「JumpEnd」にも適用しておきます。

       

       

       

       次に作成したノードを線で繋がなければなりませんが、ジャンプという動作は歩行モーション中であればいつでも実行させたいので、「Wait/Walk/Run」ノードから「JumpStart」に繋ぎましょう。

       

       

       

       線を繋ぐとトランジションルール(遷移させるための条件)を付けることが出来ます。繋いだ線をダブルクリックして編集します。JumpStart状態には、ここみんが空中にいる時に遷移してほしいので、先ほど追加した「IsInAir?」を繋ぎましょう。

       

       

       

       次に、「JumpStart」と「JumpLoop」を繋ぎます。

       

       

       

       ここでは特に必要な条件がないので「Time Remaining」ノードを利用します。このノードを利用し、「JumpStartAnimComposit」の残り再生時間が0.1以下を切ったら「JumpLoopAnimComposit」に遷移するようにしました。「<」ノードは右クリックメニューで「<」と検索すると見つかります。また、ノードを整列する機能がありますので積極的に使っていきます!

       

       

       

       次に、「JumpLoop」と「JumpEnd」を繋ぎます。

       

       

       

       JumpEnd状態には、ここみんが空中にいない時に遷移してほしいので、「IsInAir?」の逆を条件としておきます。「Not」ノードは右クリックして表示されるメニューで検索すると見つかります。

       

       

       

       最後に「JumpEnd」と「Wait/Walk/Run」を繋ぎます。

       

       

       

       この遷移も特に必要な条件がないので「Time Remaining」ノードを利用します。条件については違和感のない見た目になるように秒数を調整しました。

       

       

       

       保存してコンパイルすればゲームプレビューにてここみんを自由に操作できるようになっています。状態を1つ追加すればここみんを踊らせることも容易です。

       

      じっくり見る

      敬礼!

       

       

       

       


      リアルタイム通信ゲーム勉強会で発表してきた

      $
      0
      0

      SGE(Smartphone Games & Entertainment)のグレンジ所属の塚原と袴田です。

       

      2016/04/20に開催されたリアルタイム通信ゲーム勉強会で発表してきたので報告をします。

       

      グレンジについて

      CAのゲーム事業部(SGE)の中の1つです。

      ポコロンダンジョンズ

      イグドラシル戦記

      の2つを開発・運営しています。

      勉強会ではポコロンダンジョンズの共闘(多人数の協力プレイ)の仕組みについて発表しました。

       

      ポコロンダンジョンズについて

      2014年夏にリリースされた「なぞるパズルRPG」です。

      当初は1人プレイのみでしたが、2015年春に最大4人の協力プレイ「共闘」が実装されました。

      共闘はSocket.IOを使ったリアルタイム通信システムによって動作していて、勉強会ではサーバサイド/クライアントサイドの仕組みを「そこまで見せるのか」というほど公開しています。

       

      サーバサイド編(発表者:塚原)

      主に、利用技術、サーバ構成、やりとりしているデータを例にどのようにマルチプレイを実現させているのかを説明しています。

      サーバサイドは非リアルタイム通信部分をPHP、リアルタイム通信部分をNode.js(socket.io)で構成されているので、その棲み分けの話もしています。

      ちなみに、発表の数日後に仕組みを改修し、課題としていたNodeプロセスのCluster化を行いました。(なので、発表資料の説明の一部は古いです)
      1コア1ワーカプロセスでサーバを並べていたのを4コア4ワーカプロセスにしてサーバー台数を削減しています。

      発表資料

       

      クライアントサイド編(発表者:袴田)

      こちらは利用技術、構成、発生した問題と解決方法の話を織り交ぜながら、どのように端末間での同期を実現しているのか処理の流れを追いながら説明しています。

      クライアントサイドはcocos-2dxとsocket.ioでリアルタイム通信をしているのですが、ソケットが切断されてしまった際の復旧や、端末間で盤面にズレが生じた際の仕組み、通信データ量を抑えるためにどうしているのか、乱数を各端末で同期する仕組みの話とポコロンダンジョンズのリアルタイム通信のキモになっている部分の多くを公開しています。

      発表資料

       

      さいごに

      今回はポコロンダンジョンズで行っているリアルタイム通信について公開しました。

      自分達なりに試行錯誤してきた結果ですが、別の方法はあると考えています。

      何かご意見等あれば是非お願いします。

       

       

      サイバーエージェント新卒エンジニア技術研修2016

      $
      0
      0

       

      みなさんこんにちは。

       

      技術組織戦略G & 技術内閣の板敷です。

      普段はAmebaのシステム障害の火消し → 障害対策ならびにエンジニアの育成評価等をしています。

       

      今回は、先日終了したサイバーエージェント新卒エンジニア技術研修について紹介したいと思います。

       

      研修概要

      従来の研修内容を一新、内製での技術研修に挑戦

       

      これまでの新卒技術研修は委託先講師の話を新卒全員が聞くという、いわゆる「座学中心」の研修でしたが、ここ数年の技術的進歩、組織内でのエンジニアに対する期待の変化を反映するため、内製での研修へとゼロから再設計しました。

       

       

      研修のコンセプトと概要

      ベースとなる研修のコンセプトは以下2点からなります。

      1. 全体のアーキテクチャを俯瞰して設計、技術選定をする
      2. 自分の頭で考え、動く

      これらのコンセプトを実現するために今回の研修では、2つの某有名サービスを

      自分たちで設計、実装するという形をとりました。

       

      研修詳細

      次に研修全体の流れに沿って、詳細内容を紹介します。

       

      以下1~6の流れをお題を変えて2回繰り返すことで、1回目の反省を2回目に活かせる設計としました。(新卒だけではなく、運営側もw)

       

      1.お題、レギュレーション、チーム分け発表

       

      ■お題

      1回目は某有名SNSサービス、2回目は某有名検索エンジンの開発としました。

       

      ■開発レギュレーション

      レギュレーションは機能面、性能面、コスト面の3軸からなります。

       

      機能面

       ・投稿ができる、フィードが表示される、お気に入りができるなど(SNSサービス)

       ・and, or , not検索ができる、検索結果の関連度/適合率/網羅性など(検索エンジン)  

       

      性能面

       ・40万フォロワーのいるユーザが投稿してからN秒以内にフォロワーのフィードに反映される(SNSサービス)

       ・継続的に検索結果がN秒以内に返ってくる(検索エンジン)

       

      コスト面

       ・開発中のインスタンスコストを$300程度に抑える(今回はAWSを利用)

       

      ■チーム分け

      ・各チーム4名(一部3名)で構成。

      ・基本的に得意技術がバランスよくなるようなチーム分けを目指しました。

       

       

      2.設計コンセプト、技術選定

      次にお題、レギュレーションを踏まえ設計コンセプト、技術選定を行います。

       

       

      ※以下実際の例

       

      設計コンセプト

      ・「疎結合で可搬性の高い、スケーラブルなアーキテクチャ」

      技術選定

      ・Go, Python, AngularJS2, Elasticsearch, Docker, Kubernetes, GRPC, Terraformなど

       

      設計コンセプト

      ・「完全に真似るのではなく遊び心を取り入れ楽しいアプリケーションを作る」

      技術選定

      ・Unity, Scala, PlayFrameworkなど

       

      設計コンセプト

      ・「自分達が慣れていなくても良いものを選ぶ」

      技術選定

      ・Go, React.js, Webpack, ECS, Elastisearchなど

       

      設計コンセプト

      ・「堅実な言語で実際の開発を意識し、コードやデザインパターンはモダンな仕様で設計」

      技術選定

      ・Java, SpringBoot, Apache, Tomcat, MySQL, Swift, Realmなど

       

       

      などなど。

      チャレンジのいい機会としてモダンな技術を選択したチーム、完成度重視で扱い慣れた技術を選択するチームなど個性がはっきりと現れました。

       

       

      3.役割決め、スケジュール作成

       

      実際の開発に入る前に、役割を決めスケジュールを引きます。

       

      1回目のお題時は見積もり精度が低かったり、得意なものからとりあえず実装、 というチームも見られましたが、2回目は開発する機能、メンバーのスキルセット踏まえた進め方へと大きく改善していました。

       

      また2回目の開発では、従来クライアントしかやったことのないメンバーもサーバサイド担当になるなど、今回の研修ならではのチャレンジも見られました。

       

       

      4.開発スタート

       

      スムーズに開発がスタートできたチームもあれば、サンプルデータの投入でハマったチーム、技術選定の失敗に気づき1からやり直すチームなど、機能開発以前でのつまづきも見られましたが、毎日1時間メンタータイムとして先輩エンジニアがフォローすることでこれらを乗り越えました。(メンターのみなさんありがとう!)

       

       

       

      5.中間発表     

       

      各チームの進捗や設計/実装内容を発表、メンターからのフィードバックを受け軌道修正を行います。

       

      1回目のお題では2度の中間発表を設けましたが、2回目は難易度を上げるため、中間発表を1度のみにするなど難易度調整にも役立ちました。

       

       

      6.最終プレゼン

       

      各チーム最終成果物を発表。 設計コンセプトやアピールポイントなど発表しつつデモンストレーションをし、審査員からの質問に答えます。

       

      実装した機能の完成度はもちろん、スライドのクオリティも予想よりはるかに高く、

      「最近の若手はなんでこんなに優秀なんだ」と一回り以上離れた新卒を見ながら改めて感じさせられるイベントでした・・・。

       

      また最終プレゼンを受け、いくつかの賞の発表も行いました。

      ベストクライアント賞、ベストアーキテクチャ賞、ベストサーチエンジン賞など。

       

      研修振り返り&まとめ

      今回は内容面でも運営面でも従来の方針を一新し、ゼロからの再設計となりましたが、全体としてはおおむね上手く行き、またいいチャレンジができたと感じています。

       

      新卒の技術研修というのはエンジニアとしてのスタートでもあるので、最初だからこそ意識してほしいことや、チャレンジしてほしいことを実現するための研修を心がけました。

       

      一方、研修後の振り返りでは課題も多く見つかったので、来年に向けてはこれらを解決する方法を取り込みながら、事業・技術・組織の変化に合わせて研修もアップデートしていければと思っています。

       

       

      以上が今回の新卒エンジニア技術研修の紹介となります。

      サイバーエージェントの技術研修に興味を持ってくれた学生のみなさん、ぜひコチラからエントリーしてみてくださいね!

       

      お会いできるのを楽しみにしています。

      以上です。

       

      ElasitcSearch(0.19)で無理やりデータ復旧させた話

      $
      0
      0

      こんにちは。Amebaの基幹系インフラ担当している鳥垣です。

       

      ユーザーのサービス用途でElasticSearch(0.19.10)を使用しているのですが、先日ElasticSearchの障害で一部のShardが読めなくなってしまいまして、それを力技で無理やり読めるように復旧させたのでその時の奮闘記を記載したいと思います。

       

      運用情報

      • 台数:30台
      • CPU:24コア
      • Heap:8GB
      • インデックス数:3
      • 総データ容量:約300GB
      • Shard数:128
      • レプリカ数:2
      • バージョン:0.19.10

      ※OpenStackの仮想サーバ

       

      ホスト障害発生

      • OpenStackのホストサーバがダウンし、ElasticSearchのノードが1台ダウン。
      • Shardの再配置処理が走り、ダウンしたノードが持っていたShardは他ノードに分散される。
      • この時点ではElasticSearchのクラスタステータスはグリーンだった(Headプラグインで確認)。
      • ElasticSearchと連携しているAPIサーバ側からのElasticSearchヘルスチェックをしているのだが、そのチェックがfalseを返していた。

       

      ホスト復旧

      • ホストサーバが復旧し、ダウンしていたElasticSearchノードサーバも起動したことを確認。
      • 他のホストサーバにマイグレーションする(またいつホストダウンするかわからないため)。

       

      ノード復旧

      • マイグレーション完了後、ElasticSearchのプロセスを起動しクラスタに入れる。
      • Shardの再配置処理が走り、クラスタに無事Joinできたことを確認。
      • クラスタステータスもグリーンになっていることを確認。
      • しかし、APIサーバ側からのElasticSearchヘルスチェックがfalseを返していた。。
      • 以前別環境で運用しているElasticSearchでもAPIサーバ側のヘルスチェックでfalseを返していたときに全ノードを再起動して復旧させたことがあったため、今回も全ノードの再起動を実施する。

       

      ※これが悲劇の始まりでした。。

       

      全台再起動

      • 1号機から順番に再起動を実施。ログファイルにてステータス確認しながら順次再起動していく。
      • 20台目までを再起動したあたりからAPIサーバ側にて遅延発生。
      • 再起動オペレーションを一旦中断。
      • HeadプラグインでElasticSeachのステータスが見れなくなる。。

      ※Shardの再配置処理が走りまくったせいでクラスタが不安定になりました。。。

      • 再び1号機から順番に全台再起動を実施。
      • Headプラグインでステータスが見れるようになる。
      • APIサーバ側の遅延解消し、ヘルスチェックもtrueを返すようになる。

       

      しかし、ここで12個のShardがクラスタに戻らない事象が発生

      その時のShard状態のキャプチャがこちらです。

       

       

      ※プライマリShardまでクラスタから外れてしまい、クラスタに戻ってくれない状態になってしまいました。。

      ※APIサーバ自体は遅延もおさまり運用できるようになりましたが、データが一部ロストしている状態になってしまいました。。

       

      復旧検証

      allocateコマンドを使ってunassignedになっているShardの移動を検討。

      以下のようなコマンドで移動をやってみました。

      curl -XPOST 'http://nodeIP:9200/_cluster/reroute' -d '{  "commands": [{  "allocate": {  "index": "index name",  "shard": 53,  "node": "node host name",  "allow_primary": false  }  }]}'

      結果:失敗 

      エラーになり移動できませんでした。

       

      Shardファイル確認

      unassignedになってしまっているShardのファイルがどのように保存されているか確認してみました。

      ※ここでは3つのインデックスを仮にA,B,Cと呼称。

      Aインデックス

      Shard番号:53

      状態:

      • 3,10,22,29号機にディレクトリを確認
      • ただしファイルがあるのは22号機のみ
      • 3,10,29号機のディレクトリは空状態

      ステータスファイル:「A/53/_state/state-93」ファイルの中身

      {"version" : 93,"primary" : false}

      ※レプリカとして認識されていると予測。

       

      Bインデックス

      Shard番号:51

      状態:

      • 27,28号機にディレクトリを確認
      • 両方にデータがあるがファイル内容が異なっている

      ステータスファイル:「B/51/_state/state-108」ファイルの中身

      {"version" : 108,"primary" : false}

      ※レプリカとして認識されていると予測 。

      ※27号機にはステータスファイルはありませんでした。

       

      Cインデックス

      Shard番号:2,4

      状態:

      • 27号機にのみ2番のディレクトリを確認
      • 25号機にのみ4番のディレクトリを確認

      ステータスファイル:

      「C/2/_state/state-427」ファイルの中身

      {"version" : 427,"primary" : true}

      「C/4/_state/state-417」ファイルの中身

      {"version" : 417,"primary" : true}

      ※両方ともプライマリとして認識されていると予測

       

      無理やり復旧させてみる

      上記の状態を踏まえて、それぞれのShardを以下方法で無理やり復旧させてみました。

      AインデックスのShard復旧手順

      ファイルコピー

      ※対象ノードのElasticSerchプロセスを最初に停止しておく。

      • 22号機にのみファイルが残っているので、そのファイルをディレクトリのみ残っている3,10号機にコピーする。
      • 29号機のディレクトリは放置しておく(放置しておいても自然に削除されることを検証済み)。

      ステータスファイルの書き換え

      • 22号機のみ「   "primary"   : false 」の箇所を「true」に変更する。
      • 3,10号機は「false」のままにする。

      ※上記作業を完了後、ElasticSeachプロセスを起動する。

      ※HeadプラグインでShardがクラスタに戻っていることを確認する。

       

      BインデックスのShard復旧手順

      このサイトを参考にlucene-coreを使ったインデックスチェックを実施してみる。

      27号機のほうは壊れており、28号機のほうは正常であることを確認。

      ファイルコピー

      ※対象ノードのElasticSerchプロセスを最初に停止しておく。

      • 27号機のファイルは壊れているので削除する。
      • 28号機のファイルを27号機と他ノードにコピーする。

      ステータスファイルの書き換え

      • 28号機のみ「   "primary"   : false 」の箇所を「true」に変更する。
      • 他ノードは「false」のままにする。

      ※上記作業を完了後、ElasticSeachプロセスを起動する。

      ※HeadプラグインでShardがクラスタに戻っていることを確認する。

       

      CインデックスのShard復旧手順

      ファイルコピー

      ※対象ノードのElasticSerchプロセスを最初に停止しておく。

      • 2番Shardは27号機にのみファイルが残っているので、そのファイルを他の2ノードにコピーする。
      • 4番Shardは25号機にのみファイルが残っているので、そのファイルを他の2ノードにコピーする。

      ステータスファイルの書き換え

      • 27,25号機のみ「   "primary"   : true 」のままにする。
      • コピーした他ノードは「false」に変更する。

      ※上記作業を完了後、ElasticSeachプロセスを起動する。

      ※HeadプラグインでShardがクラスタに戻っていることを確認する。

       

      今回の反省

      • 再起動するときは自動再配置をOFFにしてから作業するべきでした。
      • 1ノードの再起動後はログファイルの確認だけでなく、再配置処理が完全に終了するのを見届けてから次のノードの再起動をするべきでした。

      今後について

      バージョンアップします!

      Cassandraの面倒を見るのが大変だったので今まで大きな障害もなく放置していましたが、もうこんな古いバージョン使いつづけるのはやめます。。

       

      今回の記事が、Shardが戻らなくなって困ったという悩みを抱えてらっしゃる方(あまりいらっしゃらないと思いますが。。。)のお役に立てれば幸いです。

       

      サイバーエージェントグループ Go勉強会

      $
      0
      0

      みなさんこんにちは! エンジニアブログ運営チームの板敷です。

       

      今回は、先日社内で行われたGo勉強会について紹介したいと思います。

      今回の勉強会では、サイバーエージェントグループの各社から発表がありました。

       

      勉強会ポスター。事前予約不要!

       

      Golangの注目度は高く、開始即満員御礼でした。(若手中心に立ち見もw

       

       

      それでは発表内容を紹介したいと思います。

       

      ※勉強会資料は社内情報が含まれているため全公開はできませんが、

      勉強会の雰囲気だけでも感じ取っていただけると幸いです。

       

      「Goトラップ ~中級者向けGo言語でよく引っかかる同期処理など周りの問題、分析と解決方法~」

      技術本部 基盤システムG マリオさんの発表です。

       

      ※発表資料

      https://github.com/imkira/gostudy

       

      後述しますとおり、この他の発表内容は「サービスの中でGoをどのように使っているか」か話題の中心でしたが、

      マリオさんの内容はGoを使う上で遭遇する言語仕様、または罠に関するもので、実際にコードを実行し修正しながらの発表という大変ユニークなものでした。

       

      内容的には"Data Races”や”Deadlock”など、意識して書かないと当然Goでも発生しうるトラップについてでした。

       

      新しく習得する言語だとついこの辺の考慮が漏れがちですからね。ありがたい共有となりました。

       

       

      「GoでつくるAbemaTV」

      続いてAbemaTVの西尾さんです。

       

      注目度の高いサービスということもあり、開始直後にも関わらず会場の集中力は最高レベルでした。(当社比)

       

      内容的には、golangのみならずマイクロサービスをベースとしたAbemaTVのシステム設計の話も多く、

      これからgo + dockerなどを用いて設計する際の参考になるお話でした。

       

       

      またKubernetesの読み方に関してはざわめきが起こることも予想されましたが、

      「日本人なんで”クバネティス”と読む!」と言い切ってくれたので胸のモヤモヤが消えました。

       

      「GAE/Goのススメ」

      続いてアドテクスタジオ LODEOの池田さんです。

       

      しょっぱな「Goの話し期待してると思うが、GAEが9割」の宣言から始まり

      GAE/Go最強の理由を様々な観点から解説してくれました。

       

      この発表でもgoの並行処理についての話があり、改めてそのメリットは大きいと実感させられました。

      「みんなGAE興味ないかも知れないけど…」との言葉がありましたが、興味ある人が見に来ていますのでとてもよかったです!

       

      「Go言語とDockerでAIバトル基盤を書いたら便利だった話」

      次はアプリボット15新卒向井くん。

       

      勉強会前に少し話したところ、本人的には実プロダクトでgoの経験がなく他の発表者と比べて内容が薄いかも知れないが、

      せっかくの機会なんで思い切って話してみることにしました とのことでした。

       

      この姿勢がすばらしいですね!

       

       

      内容としては、アプリボット総会で使うAIバトル用に開発した「ボ◯バーマン風ゲーム」についてでした。アプリボットなんかすごいw

      golangの特徴の一つである並行処理の書きやすさにも言及していました。

       

      「10年物のレガシーPHPをGoにリプレイスしている話」

       

      次はアドテクスタジオ CAリワード 平田さんです。

       

      これまで支えてくれたレガシーシステムに圧倒的感謝をしつつ、Goに順次リプレイスするための設計の話が中心でした。

       

      Goでは複雑なOOPは難しいのでシンプルな設計にする、テスタビリティを上げるためにinterfaceを積極利用、

      ランタイムエラーを避けコンパイル時に気づくようにする など、今後Goでリプレイスする際には参考になるものばかりでした。

       

      レガシーには悩まされたものの、「PHPには圧倒的感謝!(本人談)」の気持ちを忘れず今後もリプレイスを続けてくれることでしょう。

       

       

      「クリラボにGo導入している話」

       

      続いてアドテクスタジオ iXam Creative Lab. 水沼さんの発表です。

       

      インフィード広告用のシステムにGoを導入したお話でした。

       

      チームとしてGoを習得するために、毎週1~2時間Goの勉強会を実施したり、静的コード解析ツールをいくつかまとめ「Go養成ギプス」として導入するなど、チーム全体でGoに移行する際のヒントの多い発表となりました。

       

      改めて、もくもく会は同じテーマだと捗りそうですねぇ。

       

      「優秀な若手に狂奔するオッサン劇場 ~生き残りをかけて臨んだGoの道~」

      まだまだ「Goへの移行ネタ」続きます! 続いてはアドテクスタジオ CAリワード 小高さんです。

       

      ここまでの発表を聞いて、アドテクはほんとPHP多かったんだなという印象。同じ会社なのに意外と知らないことは多いものです。

       

      レガシー化したPHPに悩んでいる中、優秀な若手とともにGoへの移行のお話でした。

      にしても、最近の若手の優秀さは異常ですね・・・!

       

       

      まとめ

      以上がGo勉強会の紹介となります。

       

      サイバーエージェントでは、社内外問わず日々勉強会等が行われており、

      グループ各社の勉強会はじめ技術ネタは以下のブログでも発信されています。

      よろしければご覧ください。

       

      CAリワード TechBlo

      アプリボット エンジニアブログ「てっくぼっと」

      アドテクスタジオ TECH BLOG

       

       

      そして最後に、サイバーエージェント&AbemaTVではGoエンジニアを募集しています。

      興味のある方のご応募、下記よりお待ちしております!

       

      サイバーエージェントキャリア採用

      AbemaTVをネット発のマスメディアへ!GoエンジニアWANTED!

       

       

      以上になります。

      PR: ジカウイルス感染症!何が危ない?どう防ぐ?-政府広報

      $
      0
      0
      中南米中心に流行!渡航する方の注意点は? 国内でも蚊の発生を抑える対策を
      Viewing all 161 articles
      Browse latest View live