この記事は、CyberAgent エンジニア Advent Calendar 2014 の10日目の記事です。
昨日の記事はhorimislimeさんの
まだObjective-Cで消耗してるの? 既存サービスをSwift移行する様々なめりっとでした。
こんにちは、インフラ&コアテク本部セキュリティGのsakatakeです。
私達セキュリティGは、社内においても社外においても
表に出ることが滅多にありません。
そのため、何をしているチームなのかよくわからない、
と思われる方もいらっしゃるかと思います。
今回はその方達(社内外問わず)向けに、
この場を借りてセキュリティGについてご紹介いたします。
■活動内容
ITセキュリティに関することであれば何でも対応しています。
具体的には
・サービスに対する脆弱性診断
・ソフトウェア等に脆弱性が発覚した場合の情報展開・ハンドリング
・インシデント対応を中心としたCSIRT活動
(日本シーサート協議会に加盟しています)
・セキュリティレビュー
・セキュリティスキル向上のトレーニング
・フォレンジック
・各種運用に関するセキュリティ向上支援
…等。
これら以外にも各種セミナーや同業者様との勉強会にも参加し、
課題共有などもしています。
■活動範囲
・社内、グループ会社の方へ
メディア事業を中心にサイバーエージェント本体だけでなく
グループ会社にも活動範囲を広げています。
ITセキュリティに関してお困りの方は、
セキュリティGまでご連絡ください。
・ご利用者様へ
脆弱性報告窓口がございます。
弊社サービスにおけるITセキュリティについてお気づきの点がございましたら、
日本シーサート協議会に記載されている チームの Email アドレスよりお気軽にご連絡ください。
もちろん、IPA脆弱性関連情報の届け出よりのご連絡でも対応可能です。
■メンバー、仕事環境について
メンバーの2/3がセキュリティベンダ出身、
1/3は開発エンジニア出身で全員中途入社です。
仕事環境として特筆すべきは
・自社のことだからこその当事者意識を持ち、
責任持って決められることがある
・セキュリティ向上のためならば
自由度が高くやりたいことが出来る
という点にあり、非常にやりがいがあります。
メンバー全員が30代男性ということもあり
キラキラなイメージはご期待されぬようお願いいたします。
■終わりに
冒頭で「表に出ることが滅多にない」と記載しましたが、
セキュリティがもて囃される、ありがたがられる状況は、
ご利用者様や組織が危機に瀕している時です。
セキュリティの心配が無い状態で、
・ご利用者様には弊社サービスを楽しんでもらう
・社員にはより価値のあるサービスを産み出してもらう
という当たり前のことを目標に、日々業務に取り組んで参ります。
明日はyoheiMuneさんです。
セキュリティグループの活動内容について
PR: その悩み人権侵害かも 法務局へご相談を!-政府ITV
Spark StreamingでHyperLogLogを実装してみた
この記事は、CyberAgent エンジニア Advent Calendar 2014 の18日目の記事です。
本日12/18は@sitotkfmが担当いたします。
昨日は@yuichiro.nakazawa1さんの「ログ解析にNorikraを使ってみた」でした。
明日は@strskさんです。
さて、唐突な上に私事で大変恐縮ですが最近秋葉原オフィスの近くに引っ越しました。
場所は秋葉原の少し西なので通勤途中に両国を通過します。
相撲といえば今年大記録が生まれました。
「巨人、大鵬、卵焼き」の中央に座する歴史的横綱大鵬と並ぶ32度目の優勝!
そんな大横綱白鵬のブログが読めるのは
アメブロだけ!
(PCでみると、、、すごく、、、黒いです、、、)
さてCyberAgentのAdvent Calendarとしての義理を果たしたところで本題です。
私は秋葉原ラボに勤務しており、主にログの集積・解析システムの開発・運用をしています。
その中では弊社内製の分散ストリーミング解析基盤Onixを使ってストリーミング処理を実装したりもしているのですが、その際に調査した「HyperLogLog」についてお話させて頂きます。
- ストリーム処理について
あらかじめ決められた間隔で処理を行うバッチ処理とは違い、ストリーム処理というのは時々刻々と流れてくるデータに対し処理を行います。
利点としてはその都度解析を行うのでデータの反映が早い点で、低レイテンシが求められる場合は向いています。昨日の記事とネタ被りしてるような
しかし、長所があれば短所もあるのが世の常、ストリーミング処理には苦手な処理があります。
それはカーディナリティ(基数)を求める処理です。つまり集合の異なる要素の数を求める計算ですね。Web系でよくある指標だとユニークユーザがその一つです。
何が難しいのかというとカーディナリティというのは算出の度に集合をなめる必要がありストリーミングで次々データが流れてくるストリーミング処理とは相性が悪いのです。
都合が悪いのは計算量だけではなく、集合を持ち続ける必要があるためデータの範囲が大きくなるとメモリを逼迫してしまいます。
例えばユニークユーザを求めることを考えたとき、ユーザの集合をビット(bit)にマッピングするとします。ユーザAを「0001」(集合A),、ユーザBを「0010」(集合B)、ユーザCを「0100」(集合C)とマッピングし、カーディナリティを求めるときは全ての和集合をとりビットの数を数えます。(A∪B∪C=「0111」なのでユニークユーザは3です。)
しかし、このビットマップ方式だと日本の人口約1億2千万をカバーするのに2の27乗のビットが必要になります。
これはビットだけ考えた場合16MBですね。
「16MBって大した事無くない?」
そう思った方はすぐにキン肉マンを読んで頂きたいです。
16MBなのはあくまでユーザが来たかどうかだけしかみていないので、例えばサイト内のページ毎のユニークな来訪者を求めたい場合はそのページの数だけビットマップが必要になります。
このようなかけ算にかけ算を掛け合わせて数値が膨大になる俗に言うウォーズマン的計算法でメモリはすぐ枯渇します。
(そもそも日本人だけがサイトをみるとは限らないですし、一人が複数のidを持つ可能性もあるので16MBというのも怪しいもんですが、まああくまで仮定という事でお願いします。)
そしてメモリサイズが大きくなると集合が大きくなるので計算量はえてして増加しがちです。ストリーム処理ならこっちの方が問題になったりします。
あらどうしましょうということで、HyperLogLogの登場です。
- HyperLogLog
「HyperLogLog」というのはアルゴリズムの名前です。
どういうアルゴリズムか、かいつまんで言えば「多少精度に目をつぶって省メモリでカーディナリティを推定する」アルゴリズムです。推定値であることに気をつけて下さい。
アルゴリズムの流れは次のようになります。元論文はここですね。
- m=2^bとなmを決めてm個のカウンターを用意
- データをバイナリにハッシュ化
- バイナリを二つに分割し、一方でカウンタのindexを、他方でカウンタに追加する値をもとめる。このときの追加する値はバイナリで1が最初に出た位置とする。
- Indexに対応するカウンタの値と比較し、現在の値が大きければカウンタを更新
- 1~4を繰り返す
- カウンタの調和平均をカーディナリティとする
カウントの保持する値から統計的な手法を用いてカーディナリティを推定するのがHyperLogLogです。カウンタの値がバイナリの1の位置にすることでカウンタの上限値を押さえる事が出来ます。
相対誤差は1.04√mで、メモリサイズを減らすと誤差が大きくなる関係にあります。
また、HyperLogLogで求められる値は推定値なのでカウンタの数よりも数が小さくても誤差が生じ得ます。
なのでカーディナリティが大きい場合(大きくなると予想出来そうな場合)に使う事をおすすめしたいです。
ちなみにこのHyperLogLogはRedisでも用意されています。
prefixがPFのコマンドですね。(PFはPhilippe FlajoletにちなんでつけられたとRedis開発者@antirezのブログに書いてあります。)
既にHyperLogLogのライブラリは色々あります。
Javaだとjava-hllがあります。
なんでせっかくストリーミングアルゴリズムとしての紹介なのでストリーミング処理としてのHyperLogLogを実装をしてみようと思います。
業務ではストリーム処理を書くのに前述のOnixを使っているのですがせっかく外に公開するものなので前から使ってみたかった「Spark Streaming」で実装してみようと思います。
実装の前にSparkとSpark Streamingについて軽く触ります。
- Sparkとは
Sparkとは最近注目の分散処理システムです。
分散処理システムといえばHadoopが有名ですが,SparkはHadoopと異なり中間データを一々ディスクに書き込むということをしないのでHadoopと比べて高速に処理が行える上に
Spark SQL(SQL), MLLib(数値計算), GraphX(グラフ演算)など解析別にAPIが用意されていて様々な分野で利用です。
Sparkの特徴としてRDDというデータ構造があり、RDDというデータ構造にデータをつめこみRDDに変換(transformation)をかけることで処理を行います。
また分散したRDDの一部が欠落しても変換の手順が分かれば欠落した部分を再計算(再変換?)して復旧することができます。
本当はもっと書くべきなんでしょうが主題がぶれるのでSparkについてはこのぐらいにさせてください。
- Spark Streamingとは
Spark Streamingはストリーミングデータをウィンドウで切ってRDDに詰めて処理するAPIです。
この連続したRDDのフローをDStreamと呼びます。
なのでSpark Streamingの処理は準リアルタイム(near realtime)なのですがウィンドウサイズを小さくするとレイテンシは低下します。その粒度は要件と相談ですね。
Spark StreamingはDStreamに流れてくるRDDに変換をかけることで処理を行います。
WordCountならlines Dstreamに対して単語にパースするflatMapの変換をかける事でwords Dstreamに変換するといった具合ですね。
DStreamから流れてきたデータをSparkでバッチで処理することもできるみたいです。
(図はSpark Streamingの公式ドキュメントから引用しました。)
- Spark StreamingでHyperLogLogを実装
では早速実装に入ります。
Spark StreamingはScalaとJavaで書く事が出来るのですがJavaだとコードが若干煩雑になりそうで普段あまり使わないScalaで書いてみました。
クソコードかと思いますがご了承ください。
package jp.co.cyberagent.spark
import scala.util.hashing.MurmurHash3
import org.apache.spark.SparkConf
import org.apache.spark.SparkContext
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.Seconds
import org.apache.spark.streaming.StreamingContext
import org.apache.spark.streaming.StreamingContext.toPairDStreamFunctions
import org.apache.spark.streaming.Time
import org.apache.spark.streaming.flume.FlumeUtils
import org.apache.spark.streaming.flume.SparkFlumeEvent
object HllStreaming {
var b = 14
var window = 10;
var slide = 10;
def main(args: Array[String]) {
if (args.length >= 1) {
b = args(0).toInt
}
if (args.length >= 2) {
window = args(1).toInt
}
if (args.length >= 3) {
slide = args(2).toInt
}
var m = math.pow(2, b).intValue()
var alpha = getAlpha(m)
println("m = " + m);
println("window = " + window);
println("estimate error rate = " + 1.04 / math.sqrt(m));
val conf = new SparkConf().setMaster("local[2]").setAppName("HyperLogLogCount")
val sc = new SparkContext
val ssc = new StreamingContext(conf, Seconds(10))
val dStream = FlumeUtils.createStream(ssc, "localhost", 41414)
import StreamingContext._
val rootStream = dStream.map(hashMapping) // indexとvalueを求める
.reduceByKeyAndWindow(
(
(a:Int, b:Int) => if(a > b) a else b),
Seconds(window),
Seconds(slide)
)
.map(c => math.pow(2, -c._2)) // 2^-M[j]
rootStream
.reduce(_ + _)
.union(rootStream.count.map(c => c.toDouble)) // indexの総数をもつRDDをjoin
.foreachRDD((rdd, t) =>calcAndShow(rdd, t)(m, alpha))
ssc.start()
ssc.awaitTermination()
}
def hashMapping(event: SparkFlumeEvent) = {
val body = new String(event.event.getBody.array)
val hash = MurmurHash3.stringHash(body)
val index = hash & ((1 << b) - 1)
val value = getValue(hash)
(index, value)
}
def getValue(hash: Int): Int = {
val upperValue = hash >> b;
var value = 1
var mask = 1
for(i <- 0 to 32 ) {
if((upperValue & mask) != 0) return value;
value += 1
mask <<= 1;
}
return value;
}
def getAlpha(m: Int) = {
if(
m == 1 ||
m == 2 ||
m == 4 ||
m == 8
) {
1
} else if(m == 16) {
0.673
} else if (m == 32) {
0.697
} else if (m == 64) {
0.709
} else {
0.7213/(1+ 1.079/m)
}
}
def calcAndShow(rdd: RDD[Double], t: Time)(m: Int, alpha: Double) ={
if(rdd.collect().length != 2)
println("Data Not Found") // データが流れてこないとき
else {
val d = rdd.collect()(0) + (m - rdd.collect()(1)); // 2^-(M[j])と2^0の総数
val hll = alpha * m * (m / d); // HyperLogLogの値
printf("hyperLogLog value = %f at %s\n", hll, t)
}
}
}
まず真っ先に頭の悪い引数の取り方をしていますが、第一引数がb、第二引数はウィンドウの時間(秒)、第三引数はスライドの時間(秒)です。
例えばウィンドウの時間を30秒としてスライドの時間を10秒とするとSpark Streamingでは
12:00:00~12:00:30→12:00:10~12:00:40→12:00:20~12:00:50
とい30秒間のデータの保持をします。
データはFlumeで送られるようにしています。Spark StreamingはFlume以外にもkafkaやZeroMQ, MQTTなんかでもデータを送る事が出来ます。
一般的にHyperLogLogの実装はカウンタを用意してその値を更新して行くんですが、上記の実装では流れてきたデータに対してindexを割り当てて、その中で最大値を求めてます。
しかし、データによってはindexが割り当てられない数値が出てしまいますが、配列に入れている訳ではないので値の無いindex(M[j] = 0となるj)を判別する事ができません。
なのでrootStreamを分割してindexの総数を求め、m - (indexの総数)より割り当てられていないindexの総数を求めています。
(なんか我ながらスケールしなさそうな実装ですが、、、)
あとScalaって標準でmurmurHashがあるんですね。
- 実験(というかテスト...)
では早速動かしてみました。
環境はローカルのMac Book Pro(厚)で動かしました。spark-1.1.1をダウンロードしてsbtでコンパイルしてlibディレクトリにjarファイルを置いてspark-submitしているだけです。なので分散等は行っていないのでご了承ください。
まずデータについてですがよくSpark Streamingではtwitterのデータを流したりするんですが、今回はカーディナリティの誤差を見たいのであらかじめテストデータを作っておいてflumeを使ってワンショットで送っています。
テストデータはdata1とdata2を用意しており、ランダムに繋がれた4文字の文字列が10万行書かれたファイルとなっております。
このテストデータのカーディナリティは次のようになります。
実験なんですがbを14, ウィンドウを30, スライドを10にしてdata1→data2の順番で流しました。data2はdata1のデータを流してスライドしたのを確認して流しています。
ではその時の標準出力になります。
data2はdata1の値が出てから流したのでdata1のカーディナリティは90649.265869になりますね。
その後、164417.376140が二行連続で出ますがこの値はdata1∪data2の値となります。
そして次の行ではdata1のデータがウィンドウがら落ちているので、90901.397719はdata2のカーディナリティになります。
それぞれの誤差なんですが次のようになります。
データ | 実際のサイズ | HLLの値 | 誤差率(%) |
---|---|---|---|
data1 | 89763 | 90649.265869 | 0.987 |
data2 | 90017 | 90901.397719 | 0.982 |
data1∪data2 | 161985 | 164417.376140 | 1.501 |
b=14の時の誤差は約0.8%なのでちょっと上回ってますがまあ大体こんなもんだろという数値ですね。murmurHashのseed値を変えたりデータが変わるとまた違った結果になるかと思います。
まとめ
ここまででHyperLogLogをストリーミング用にSpark Streamingで実装して試してみました。
まあぶっちゃけそんなストリーミングでHyperLogLogを使わざる得ないってかなりのデータが流れてこないと効果が。。。
それでもHyperLogLogは膨大なデータのカーディナリティの算出には有用ですし、Spark StreamingはSparkそのものの勢いも相まって期待のプロダクトであることには間違いないです。
まあ二つを混ぜる必要は無かったですね。。。
あと上記のコードではハッシュのseed値が起動後変わらないので同じデータで同じ結果が出てしまいます。まあそれで問題はないんでしょうが、バッチだと実行時のパラメータはその都度変更出来るけどSpark Streamingのようなウィンドウがスライドするような場合は処理中にパラメータを変更させなければならないので、ここがストリーム処理の難しさだと思います。ウィンドウサイズとスライドの時間を一緒にすれば出来なくはないとは思いますが。
アドベントカレンダーって氷水被ってもらう人を指名するんでしたっけ?違うか。
それでは
ごっつぁんでした!
NewSQLのCockroachDBについて調べてみた
CyberAgent エンジニア Advent Calendar 2014の23日目の記事です。
秋葉原ラボの、鈴木(@brfrn169)、Shtykh Roman、柿島です。
普段は、分散DB(主にHBase)やストリーミング処理基盤の開発・運用などをやっています。
今回は、NewSQLの1つであるCockroachDBについて紹介します。
NewSQLとは
CockroachDBについて紹介する前に、NewSQLについて簡単に説明します。
NewSQLとは、一言で言うとNoSQL+SQL機能(トランザクション)です。RDBMSとNoSQLの良いとこどりをしているともいえるでしょう。
従来、RDBはスケールアウトのしづらいモノリシックな作りになっていました。しかし、RDBでは、昨今のビックデータの潮流に対応できず、NoSQLの技術が登場します。NoSQLは、スケールアウトが容易で、高い読み書き性能を持っているものが多いです。ただし、トランザクション機能などの一貫性を求められる処理については不向きでした。そのため、近年、RDBとNoSQLの融合に関心が強まってききました。その結果として、HAやスケラビリティ、SQL機能(トランザクション)を持ったNewSQLが現れました。
NewSQLの代表的なプロダクトとしては、今回紹介するCockroachDBの他に、VoltDB、Clustrix、MemSQL、FoundationDBなどがあります。GoogleのSpannerやF1などもNewSQLに分類されます。
CockroachDB
CockroachDBは、GoogleのSpannerのオープンソースクローンで、Go言語で実装されています。元Google社のエンジニアである、Spencer Kimball氏などによって開発されています。現在は、まだ、Alphaバージョンであり、実用段階ではありませんが、現在とても活発に開発されています。
CockroachDBは、ACIDトランザクションを備えた分散キーバリューストアであり、名前通りのゴキブリのようなresilienceを目指して開発されています。ラック間やデータセンター間でクラスタを組むことを想定しており、ディスクやマシン、ラック、更にはデータセンターの障害が起きたとしても耐えられるようにデザインされています。また、クラスタ内の各ノードには差がなく、CassandraのようにいわゆるP2P型のアーキテクチャとなっています。NoSQLのように、ノード追加により、リニアに性能上がっていくのも特徴の1つとなります。
デザイン
ここからは、CockroachDBのデザインについて説明します。
データモデル
CockroachDBのデータモデルは、Keyでソートされたソートマップになっています。KeyとValueは任意のバイト配列となっていて、Valueはバージョンを持ちます。
レンジ
データは、レンジ(デフォルトで64MB)と呼ばれる単位に分けられ、各ノードのRocksDBに保存されます。レンジは、開始Keyと終了Keyで定義され、設定されたサイズを維持するためにマージされたり分割されたりします。
レプリケーション
レンジは、トータルで3つ以上のノードにレプリケーションされ、ノードがダウンした場合にも、データロストしてしまうことはありません。また、意図的に別のデータセンターにもレプリケーションされるようになっていて、データセンター障害にも耐えることができます。レンジの一貫性のために、Raft合意アルゴリズムが利用される予定となっています。
ゴシッププロトコル
先ほど説明したように、CockroachDBはP2P型のアーキテクチャを採用しています。そのため、集中管理を行わないゴシッププロトコルを用いています。ゴシッププロトコルでは、各ノードは近傍Nノードと連絡を取り合い、次の情報の交換を行っています。その際のノード間のコミュニケーションはProtocol buffersによりエンコードされています。
- ロード情報(CPU, ディスク容量,ノード状態等)
- レンジ情報(利用できないレプリカ,R/Wロード等)
- ネットワーク構成(ラック内/DC間の帯域幅/通信レイテンシ等)
トランザクション
CockroachDBのトランザクションは、Snapshot Isolation(SI)とSerializable Snapshot Isolation(SSI)により実現されています。基本的にどちらも一貫性を保つロックフリーのREAD/WRITEメカニズムですが、write skew現象が起こり得るSIより、パフォーマンスコストが高くても安全なSSIをデフォルトにしてある。
※ CockroachDBは、Spannerと違って原子時計やGPS受信機を想定していないようです。
アーキテクチャ
CockroachDBのアーキテクチャは、複数のレイヤーで構成されています。一番上位のレイヤーはSQLを扱うレイヤーとなっています。次のレイヤーとして、Structured Data APIがあります。Structured Data APIは、スキーマやテーブル、カラム、インデックスなどのリレーショナルモデルに似たコンセプトを提供します。最後のレイヤーは、Distributed Key Value Storeです。Distributed Key Value Storeは、モノリシックなKey Value Storeを提供していて、各ノードのレンジへのアクセスはこのレイヤーで行っています。各ノードは複数のStoreを持っています。Storeは物理デバイスを抽象化する概念です。
各ストアは、複数のレンジを含んでいます。レンジは、Raft合意アルゴリズムによってレプリケーションされます。下の図では、それぞれのStoreを拡大したもので、1つのレンジに付き3つのレプリカが存在しています。
※ 図は、https://github.com/cockroachdb/cockroach/blob/master/README.md から引用
API
現在は、REST API(CRUDのみ)と、DB API(protobuf/json over HTTP)の2種類のAPIが用意されているようです。開発段階
ブログ執筆時点では、MLで以下のような発言がされており、未実装な部分が多数あると思われます。
- None of us is running a cluster in anything other than unittests
- At present, Cockroach would only bother to use a single machine with no replication, no matter how many nodes you have participating in the gossip network.
CockroachDBのドキュメント上は以下のように、書いてあります。
ALPHA
- Gossip network
- Distributed transactions
- Cluster initialization and joining
- Basic Key-Value REST API
- Range splitting
Next Step
- Raft consensus
- Rebalancing
その他のNewSQL
CockroachDB以外のNewSQLは、VoltDB、Clustrix、MemSQL、FoundationDBが上げられます。以下、簡単に特徴を書きます。
VoltDB
- オープンソースのin-memoryデータベース
- 低レイテンシなトランザクション
- コミュニティ版は無償
Clustrix
- リアルタイム解析や大規模なトランザクションのワークロードをサポート
- オープンソースではないが、無償コミュニティ版や個人ユーザ版がある
MemSQL
- in-memoryデータベース
- 低レイテンシなトランザクション
- MySQLとwire-compatible
- ライセンス情報なし(無償トライアル?)
FoundationDB
- CockroachDBのように、トランザクションをサポートしたソートマップ
- 複数のプログラミング言語サポート
- トランザクションの制限あり(時間とサイズ)
- 6プロセスまでの無償版あり
まとめ
今回は、NewSQLの1つであるCockroachDBの概要について紹介しました。
弊社でも、NoSQLを使っている事例が多々ありますが、トランザクションが無いため苦労しているところもあり、NewSQLを早い段階からキャッチアップしていこうと考えていて、今回、CockroachDBを調査することにしました。まだ、未実装な部分が多いため、今回は検証するところまではいきませんでしたが、CockroachDBは現在活発に開発されており、今後に期待できるプロダクトだと思います。これからも、CockroachDBの動向を追っていく予定なので、今後、機会がありましたら、このブログで報告できたらなと考えています。
参考文献
- Spanner: Google's Globally-Distributed Database, http://research.google.com/archive/spanner.html
- CockroachDB, https://github.com/cockroachdb/cockroach, http://cockroachdb.org
- Raft合意アルゴリズム, https://raftconsensus.github.io/
- Snapshot Isolation, http://en.wikipedia.org/wiki/Snapshot_isolation
- Serializable Snapshot Isolation(SSI), M. J. Cahill, U. Rohm, and A. D. Fekete. Serializable isolation for snapshot databases. In SIGMOD, pages 729–738, 2008.
- RocksDB, http://rocksdb.org/
- Protocol Buffers, https://code.google.com/p/protobuf/
- VoltDB, http://voltdb.com/
- Clustrix, http://www.clustrix.com/
- MemSQL, http://www.memsql.com/
- FoundationDB, https://foundationdb.com/
Rails4へのアップグレードを行ったお話
この記事は、CyberAgent エンジニア Advent Calendar 2014 の24日目の記事です。
コミュニティ事業本部の後藤(@shiro166)です。
パシャっとmyペット(以下パシャペ)というサービスのシステム責任者をやっています。
パシャペでは今年の2月にPHP(CodeIgniter)からRuby(Ruby on Rails)へのリプレースを行いました。
リプレースを行った当初はRuby2.0系最新とRails3.2系最新を使用していたのですが、
6月にRubyを2.1へ10月にRailsを4.1へのアップグレードを行いました。
今回はリプレースの際のお話ではなく、
Railsを3.2から4.1へアップグレードした際に行った作業の一部の話になります。
構成
サイバーエージェントでのRailsアプリケーションの基本的な構成は大崎さんが以前このブログに書いたこちらの記事を参照してください。
パシャペはこの基本的な構成と比べ下記のような若干の違いがあります。
・Queueingに関してresqueではなくsidekiqを採用している
・shardingしたDBが存在している
規模的にはmodel数で50~100くらいです。
またPHP時代の名残りから複合主キーを使用したtableが存在したりとDB周りがARには若干つらめな構成になっています。
strong_parametersの導入
Rails4からattr_accesibleが外部のgemとして切り出されRailsに取り込まれたのがstrong_parametersです。Rails3.2でも下記のようにGemfileに記述すれば問題なく使用出来ます。
gem 'strong_parameters'controller側も下記の例のように修正しました。
UsersController < ActionController::Base
def create
User.create(user_params)
end
private
def user_params
params.require(:user).permit(:name, :sex, :age)
end
end
Rails4用のコードにconvert
Rails3 から4に対応したコードへの変換はsynvertというgemを使用しました。defaultのsnippetsを使用して一部で問題が発生したので、
変更を加えて使用しました。
synvert の基本的な使い方は下記です。
$ gem install synvert$ synvert -l # snippetの一覧が表示される$ synvert -r rails/upgrade_3_2_to_4_0 # rails 3.2から4.0へのコードの変換$ synvert -r rails/upgrade_4_0_to_4_1 # rails 4.0から4.1へのコードの変換
ActiveRecord周りの修正
一部定義されていないmethodがあるRails4.1では下記の場合にエラーが発生します。
users = User.limit(10) users.pop # NoMethodError users.shift # NoMethodError# to_aを使用することで回避可能 users = User.limit(10).to_a users.pop users.shiftconditions がdeprecatedになった
下記のような構文でエラーになるようになりました。
has_many :posts, conditions: '`id` < 100'こちらはブロックを渡すことで元の動作と同じになります。
has_many :posts, -> { where('`id` < 100') }model内で#connection使用の場合の修正
数カ所で生のqueryを使用している部分があるのですが
下記のような記述の際にエラーが発生するようになりました。
query = "SELECT ..."connection.select(query)下記のような記述でエラーが発生しないようにできます。
query = "SELECT ..."ActiveRecord::Base.connection.select(query)slow query
ARで生成されるSQLのqueryでいくつかRails3の時と違うqueryが生成されていた為に
slow queryが発生していたのでRails3と同じqueryが生成されるように修正を行いました。
composite_primary_keysでのAR4.1.6対応
先で述べた通り複合主キーのtableが存在しているので、その対応の為にcomposite_primary_keysを使用しているのですが、
当時の最新版がRails4.1.6に対応しておらず一部でエラーが発生していました。
commit履歴を見ると4.1.6対応はmergeされていたのでとりあえずbranch ar_4.1.xを指定して動作確認しようとしたらエラーが発生したので
fork > 該当箇所を修正 > pull request
という流れを行いAR4.1.6対応版がリリースされるまではforkしたbranchを指定していました。
その後対応版がreleaseされたのでそちらのversionを指定して使用しています。
RussianDollCachingへの対応
Rails3ではsweeperを使用して対象のrecordに変更があった場合はキャッシュを削除するようにしていたのですが、
sweeperがrails-observersという別gemに切り出されたのでキャッシュのキーにrecordの更新日時を使用することでrecordが更新された場合に自動的に新しいキャッシュを使用するように変更しました。最初から更新日使えばよかった
実際のリリース
実際のリリース時はメンテナンスを挟み行いました。Rails4への移行時は最小限の変更で済むように
今回説明した修正の中でもRails3でも正常に動作する部分に関してはRails3の状態で事前にリリースをしておきリリース時に出来る限り最小限の変更で確認が少なくてすむように心がけました。
まとめ
今回紹介した内容は実際に行った修正の一部になります。実際にアップグレードを行ってみての感想はtestがしっかりしていると実際に動作検証した際の不具合もほぼ無くて済んだので安心感がありました。
パフォーマンスに関しては若干の低下がありましたが、想定の範囲内でした。
Ruby界隈では新しいversionが公開されるサイクルが早く追うのが大変ですが
個人的にはそれが楽しかったりしています。
Rails4.2がリリースされ、Ruby2.2のリリースも間もなくのようでRubyistの皆さんは楽しみですね。パシャペでも順次対応していこうと考えています。
おまけ
Rails4.2がリリースしたのでパシャペの開発環境をRails4.2にして遊んでみました。おまけなので説明は省きます。(軽く動作確認を行った程度です)まずはGemfile修正
gem 'rails', '4.2.0'gem 'composite_primary_keys', git: 'https://github.com/composite-primary-keys/composite_primary_keys', branch: 'ar_4.2.x'gem 'responders', '~> 2.0'gemを更新する
$ bundle updaterails serverを起動(vm上で動作してます)
$ rails s -b 0.0.0.0上記の作業で移行が完了してとりあえず閲覧出来る程度にはなりました。testを実行するとDEPRECATION WARNINGが大量に出力されていたのといくつかのtestが失敗していたので実際に移行する際はそこら辺の修正やその他の検証が必要になるかと思います。
今年も1年ありがとうございました(プレゼント企画)※ 12/25 15:00応募締切
この記事は CyberAgent エンジニア Advent Calendar 2014 25日目の投稿です。
昨日は@shiro166さんの「Rails4へのアップグレードを行ったお話」でした。
エンジニアブログ運営チームです。
もとい、サンタです!
1年間のエンジニアブログ、1ヶ月間のAdvent Calendarいかがでしたでしょうか。
楽しんでいただけたなら幸いです。
では、1日目の記事 でも予告していたプレゼント企画のお知らせです!
★プレゼント企画★
まずはプレゼント内容の発表です。クリスマスにちなんだプレゼントを用意しました。
さて、皆さん。
クリスマスといえば ・・・
1. チキン
これがないとはじまらない。モンゴが無いくらいに始まらないですね。
からあげクンレッドうまあああああああああああああい
2. ケーキ
次はこれですよね、やっぱり。クリスマス=ケーキ。
糖分がなければ頭も回らない、モンゴみたいにね(筆者注:意味不明)
プレミアムロールケーキうまあああああああああああああい
3.コーヒー
最後にケーキで口の中が甘くなったんでコーヒー飲みたいっす。
やはりエンジニアといえばコーヒー。このデータ入れすぎたモンゴみたいに苦いコーヒーがいいんだ。
コーヒーがうまあああああああああああああい
これらの3つをセットにして
抽選で10名の方にプレゼントいたします。
このセットでこのクリスマス乗り越えてくださいね!
★応募方法★
CyberAgent エンジニア Advent Calendar 2014 の初日(12/1)と最終日(12/25)を除いた記事のどこかに応募に必要なキーワードが3つに分割されて隠れています。キーワードを探しつつ、ぜひぜひ気になる記事を読んでいただけたらと思います。
ヒント: キーワードはすべて画像の中にあります
しつこいようですが、キーワードの例はこんな感じです。しつこいようですが。
1. 当ブログのTwitterアカウント @principia_ca をフォローしてください
2. @principia_ca に リプライで応募のキーワードをつぶやいてください(コチラからつぶやけます)
応募の締め切りは、12/25 15:00です。
当選者の発表は @principia_ca で、12/25 16~17時頃に行う予定です。
プレゼントの受け渡しにはローソンe-Giftを利用します。住所等の連絡は必要ありません。
http://www.lawson.co.jp/service/static/e-gift/
今年も1年ありがとうございました!
来年もよろしくお願いいたします
2015年始の挨拶と2014年人気記事の発表!
あけましておめでとうございます!!!
エンジニアブログ運営チームです!
もう1/16だぞ、新年のあいさつ今更じゃない?
と私も思っておりますが、もう一度!あけましておめでとうございます!!
今年をきれいなクラウチングスタートで突っ走るためには、やはり去年の振り返りは欠かせませんね!もう今年始まってから2週間経ってますが!
ということで、
タイトル通り2014年の記事の振り返りをしていきたいと思います。
気になるタイトルの記事があれば読んで頂けると幸いです。
!2014年公式エンジニアブログランキング!
10位: TOTEC2014 インフラチューニング(チューニンガソン)で優勝したはなし
そもそもTOTECってなんやねんって方が大半だと思われますが、社内チューニンガソンの大会の事で、 その大会で優勝された方の試行錯誤が時系列に読めてとっても面白い内容となっております。
9位:たのしい Scala
とっても優しくScalaの導入を教えてくださっています。
文章から滲み出る人柄にも注目です!
8位:エンジニアの僕が写真を存分に使って社内の紹介
弊社のアドベントカレンダーに便乗して、弊社自慢のIT芸人が体を張った記事を書いてくれました。
とっても心温まるエントリーとなっております。
7位:#e100q 新人エンジニアにお勧めする一冊
サイボウズさんの「エンジニア100人に聞きました」の企画に乗らせていただいた記事です。
エンジニアの生の声が聞けて新卒エンジニアでなくても気になるエントリーになっているのではないでしょうか!
6位:WebPの画質とファイルサイズを評価する
画像配信/変換野郎さんがWebPについての知見を書かれていますね。
WebP!WebP!
5位:Redisとハサミは使いよう
Redisのロックと設定の同期の機能に焦点を当てて、Redisの紹介をしてくださっています。
Redisかわいいよ、Redis
4位:Amebaの開発環境について
Amebaのエンジニアに如何に快適に開発をしてもらうかという取り組みの内容で書かれています。
山あり谷あり、色々な試行錯誤が読み取れるエントリーになっております。
3位:アメーバピグにおけるDB構成&対応記
アメーバピグのDB構成、またその構成と日々どう向き合っているかの奮闘記です。
運用大事!
2位:おすすめオブジェクト指向練習方法
OOPを理解するためのオブジェクト指向エクササイズの紹介!
練習練習!
1位:MySQL初心者に贈るインデックスチューニングのポイントまとめ2014
MySQL初心者のためにどういう観点でMySQLのチューニングをしていけばいいかをわかりやすく纏めてくれています。
勉強になります。
!おまけ!
上記記事の紹介は2014年に公開された記事のランキングですが、
せっかくなので今まで公開されている全ての記事を含めた2014年にアクセスされた記事ランキングを振り返ってみましょう。
10位:2013年サイバーエージェント エンジニア プレゼンデータまとめ
弊社エンジニアのプレゼンデータのまとめ。
やはり人気・・・!2014年度もやらなきゃ・・・!
9位:【研究課題レポート抜粋】Jenkins+Unityで構築するスマフォアプリビルドサーバー
弊社研究レポートからの抜粋記事ですが、なんとこちら2011年の記事・・・!
とっても息が長い記事となっております。
8位:burp suiteによる初歩のWeb監査
Webアプリケーションの監査の一例の紹介です。
本エントリで紹介している手法は絶対に自分の管理しているサイト以外に適用しないでください!!
7位:AmebaアプリのiOS7対応時に行ったUI実装
実際に行われたiOS7へのUI実装における対応記。
実運用から出る知見、助かります。
6位:おすすめオブジェクト指向練習方法
タブルランクイン!
OOP!OOP!
5位:MySQL初心者に贈るインデックスチューニングのポイントまとめ2014
またまたタブルランクイン!
こちらもあわせてどうぞ
4位:はじめての RabbitMQ
RabbitMQのinstallから簡単な導入まで。
3位:大量のサーバを管理するために、IPMIのお話
ipmiの紹介記事。
流行りに左右されないHW系のツールの知見は、やはり需要があるのですね!
2位:サイバーエージェントのスタンディングデスク事情
弊社におけるスタンディングデスク事情の紹介。
今でも根強く残っているスタイルです。
1位:redis、それは危険なほどのスピード
堂々一位はredisの紹介!
タイトルがとてもキャッチーなのも人気の一つなのかなと思っていたら、
公式ドキュメントにも、危険なほどのスピードで動作すると書いてあるのですね。
Redisかわいいよ、Redis
!最後に!
2015年も継続的に弊社文化やエンジニアの活動を発信していければと思います。
本年もサイバーエージェント公式エンジニアブログをよろしくお願いいたします。
ソシャゲからアドテクになって技術的に変わった7つのこと
どうも、アドテクスタジオ所属RightSegmentチームの安田です。
元ソシャゲエンジニアで今はアドテクエンジニアしてます。
サーバーサイドJavaエンジニアです。
なので、昨今は特別派手なネタがないので
ソーシャルゲームからアドテクに業種変更して技術的な違いとか書きたいと思います。
ちなみにRightSegmentはDMPという位置づけなのですが
DMPとはData Management Platformの略で
広告主がもつデータ、第三者のデータなどを一元管理・分析し、
最適な広告配信をするために活用されるプラットフォームになります。
主にタグと呼ばれるJavaScriptを広告主のサイトに貼ってもらい、
サイトに訪れたユーザーのアクセス履歴からユーザ情報を作成してカテゴリ(セグメント)管理しています。
例えば、
某不動産サイトが有名サイトに広告配信したいとした場合、
ランダムに全国の物件情報の広告を出すのは広告枠の無駄遣いなので、
「東京で物件を探してる人」には「東京」の物件情報、
「大阪で物件を探してる人」には「大阪」の物件情報の広告を配信したいはずです。
そういった場合に
「東京で物件を探してる人」は東京で物件検索したことのある人をDMP側で抽出してリスト化しておき、
そのリストに該当するユーザーのアクセスがあったら、
連携している広告配信のシステム側に東京の物件情報を配信してもらうといった仕組みになってます。
さて、本題の技術的な違い7つです。
その1 ユーザーIDが固定じゃない
ソシャゲだとユーザーIDはユーザー登録時に採番されて、以後同じIDで管理されますが
アドテクはその仕組み上、ユーザーIDを端末クッキーに保存するケースが多いです。
そのため、端末ごとやクッキーを削除するたびに新しいIDを採番するので
実際のユーザー数の数十倍ものIDを管理する必要があります。
なので余裕でレコード数が億超えてくるのでMySQLだと厳しいです。
その2 NoSQLが主流
その1が主な理由なのですが、ソシャゲの場合はユーザーに紐付いたデータを別テーブルで管理していって
一人辺りのデータ量が多くなっていく傾向があります。
アドテクの場合はユーザーに紐づくデータはそれほど多くなく、増えるのはキーのほうなので
RDBではなくてKVSで管理するのに非常に向いています。
Cassandraとか一部MySQLを使うところもあります。
Redisで管理してるところもありましたが
相当数のサーバーが必要でレプリ遅延とか永続化に苦労してるようでした。
ちなみにうちのシステムはHDSF上にログファイルを永続化して、
MapReduce処理でCassandraに必要分を持たせて配信サーバーからユーザーIDをキーに取得し、
ブラウザCookieを更新するという感じです。
最近はAerospike(エアロスパイク)が注目されていて
絶賛検証中なので、検証後に誰かが紹介してくれると思います(笑)
その3 アクセスがかなり多い
様々なサイトにタグが貼られているのでアクセス数が桁違いに多いです。
秒間で8000アクセスとかくるのでサーバーも40台以上合ったりします。
ただアクセスは多いのですが、ソシャゲと違って処理することが少なく、
ログ出して終わりくらいなレベルなのでミドルウェアのチューニングが結構重要だったりします。
その4 画面がない
まぁこれは当たり前なのですが、scriptタグやimgタグでサイトに貼ってもらうので
ソシャゲのようにFreemarkerで画面実装とかはないです。
ただ、タグ実装で必須なのでJavaScriptは使います。
自分はJavaScript書けないので詳しいことはわかりませんがw
その5 バッチ処理が多い
アクセス数が多いのでリアルタイム性が必要でない情報は
ログファイルをHDFS上に蓄えて深夜にMapReduceで集計処理とかします。
あとクライアント向けにレポート作成とかあって
デイリーのUU数とかピクセリング数とかバッチ処理で集計されてます。
ソシャゲでもバッチ処理することもありますが
報酬付与とかランキング集計とかぐらいで基本はリアルタイム処理だったと思います。
まぁデータ件数的に分散処理でないと処理できないってのが大きいですね。
その6 複数のドメインをまたいだ設計
ソシャゲは基本的に1ドメインで作られます。
共通的なシステムと連携することもありますが
ドメイン内で完結することが多いと思います。
アドテクの場合は他ドメインのシステムと連携するのが基本なので
連携先のシステムからコールバックされて、またリダイレクトしてと
複数のドメインを行ったり来たりします。
なので最初の頃は慣れなくて
ユーザーのリクエストをこっちに飛ばして、その際にコールバックがあるから戻ってきてまた飛ばしてとか。。
毎日のように知恵熱出してました。。
その7 障害検知が難しい
ソシャゲの場合はテストサイトで実際に動かしてデバッグすることができます。
またデータもDBを見れば判断することができます。
アドテクの場合はテストサイトもありますが
システムの最後の結果を容易に確認することができません。
テストサイトにタグを貼ってアクセスしても
タグの検証にしかならないので
その後、バッチを回してログをDBに取り込み、
再度アクセスして状態を確認するという流れになります。
それでもきちんと広告が配信されるかは連携先のシステムを叩かないとわからず
テストなので本番システムを叩くわけにもいかず、
リリースされるまでドキドキだったりします。
そしてリリースしてもすぐには結果がわからないことが多いです。
なので、
テストフレームワークを使ったテストコード実装が割りと必須だったりします。
SpringTest、Mockito、DBUnit、Hamcrest辺りを使ってます。
ちなむと管理画面以外はJava製です。
最後にソシャゲでもアドテクでも共通することですが
パフォーマンスと運用のバランスを意識したアーキテクチャ設計やロジックの実装、
テーブル設計、ミドルウェアのチューニング等々
ソシャゲ開発で培った技術が今でも役に立っています。
golangのある生活
こんにちは
技術本部でエンジニアというかプログラマをしております、okzkと申します。
最近ようやくとっかかり始めたgo言語についてグダグダ書いてみます。とはいえgo歴1ヵ月程度のgo弱ですので、生暖かい目で読んでみてください。
go言語について
Google謹製の比較的新しめのプログラミング言語です。
詳細は「golang」でググってみてください。
最近ではDockerに代表されるようにgoで作られたメジャーなプロダクトも出てきてますし、そろそろこのビッグウェーブに(ryと思って一か月くらい試行錯誤してみた上での個人的印象は次のようなカンジです。
- 言語設計における機能の取捨選択が非常に特徴的。
- CSPをベースにしているだけに、並列プログラムのサポートがイケてる。
gopher君はまあともかくとして、擬人化マダー?
なお言語設計については、go言語FAQをみると「言語として何を取捨選択しているか」を伺い知ることができます。必読です!
go言語のgeneric型
言語仕様として切り捨てられた例外や型継承とは対照的に、FAQの中で珍しく「いつかは実装するんじゃねーの?」というカンジで言及されてる機能にgenerics型があります。
まあ、とはいえ現時点では実装されてないことには変わりないのですが、以下のような気になる記述もあります。
Meanwhile, Go's built-in maps and slices, plus the ability to use the empty interface to construct containers (with explicit unboxing) mean in many cases it is possible to write code that does what generics would enable, if less smoothly.
……ふむふむ、interface{}で頑張ればなんとかなるって?
おーけい、んじゃ試しにそれで頑張って何か実装してみようじゃないかい!
# さて、ここらへんからgo言語書いたことない人を完全に置いてけぼりにします。すみません。
んじゃ、何を書く?
そういやgo書いててsliceに対する操作のサポートが十分じゃないことにイラっとくることないですか?
ほら、map処理やselect処理とか、そういうのさらっと書きたくないですか?そんなことないですか?私は書きたいです。
ということで、goでmap処理を書いてみることにしましょう。
(golangのsliceでmap処理実装というのはこちらに先行ポストがありますが、本記事とは実装上のアプローチが全然違うのでパクリって言わないでください)
さてさて、なにも考えずに普通に型制約のあるカタチでmap処理を書くと以下のようになるかと思います。
func twice(i int) int { return i * 2}func Map(in []int, f func(i int) int) []int { len := len(in) out := make([]int, len, len) for i, v := range in { out[i] = f(v) } return out}func main() { src := []int{1, 2, 3} Map(src, twice) // => []int{2, 4, 6}}
……なんとなく気持ちよくないですね。メソッドチェイン形式で記述できないからですかね?
やっぱりsrc.Map(twice).Map(twice)みたいに書きたいですよね!
型制約は後程ゴニョゴニョするとして、まずはメソッドチェインできるようにしてみましょう。
オペレータの導入
レシーバとしてテンポラリのオペレータを導入してみます。
先のMapを書き換えてみます。
type Op struct { Slice []int}func NewOp(slice []int) *Op { return &Op{slice}}func (op *Op) Map(f func(i int) int) *Op { len := len(op.Slice) out := make([]int, len, len) for i, v := range op.Slice { out[i] = f(v) } return &Op{out}}func main() { src := []int{1,2,3} NewOp(src).Map(twice).Map(twice).Slice // => []int{4, 8, 12}}
メソッドチェインで書けるようになって、ちょっと気持ちよくなりました。
型制約からの解放
では、型制約を外してみましょう。
とりあえず、型はすべてinterface{}にします。
type Op struct { Slice interface{}}func (op *Op) Map(f interface{}) *Op { // 実装は後程}func main() { src := []int{1, 2, 3} NewOp(src).Map(twice).Map(twice).Slice.([]int) // => []int{4, 8, 12}}
最後に型アサーションでの取り出しが必要になちゃいましたけど、まあ許容範囲ですね。
では肝心のMapの実装ですが、型情報が失われているためリフレクションで実行時に型をハンドリングする必要があります。
func (op *Op) Map(f interface{}) *Op { // Value型に変換 vs := reflect.ValueOf(op.Slice) vf := reflect.ValueOf(f) // sliceの長さ取得 len := vs.Len() // fの返り値の型でsliceを作成。appendで追加するので初期長はゼロ vos := reflect.MakeSlice(reflect.SliceOf(vf.Type().Out(0)), 0, len) // fを実行して値をつめていく。 for i := 0; i < len; i++ { vos = reflect.Append(vos, vf.Call([]reflect.Value{vs.Index(i)})[0]) } return &Op{vos.Interface()}}
引数で渡すfunctionの返り値の型でsliceを作るようにしているので、型変換を伴うマップ処理もできるようになってます。
func toString(i int) string { return fmt.Sprintf("%d", i)}func main() { src := []int{1, 2, 3} NewOp(src).Map(twice).Map(twice).Map(toString).Slice.([]string) // => []string{"4", "8", "12"}}
引数に渡すfunctionの返り値の型に合わせて、[]stringが返ってきます。
ちょっとだけイイカンジですね!
まとめ
FAQの通り、generic型がなくても、interface{}型とリフレクションでなんとかなることを示しました。
完全なソースコードはgithubで公開しています(ドキュメント等は全然ないですけど)。
やっつけですがinject/select/all/any/sort/shuffle等の処理も実装してますし、記事中では省略した実行時の型整合性チェックもしているのでよかったら見てください。
でもここまで紹介しといてアレですが、今回実装したやつ、benchmarkすると相当遅いです。
リフレクション使ってるせいでしょうけど、数十倍~数百倍というオーダです。
おまけにコンパイル時の型チェックもきかないし、ミスると実行時にpanicするしで、正直常用するにはちょっとツライですね orz...
そんなわけで早いトコ、golangにもgeneric型が導入されて静的にコンパイルできるようになってほしいと思います。
それではみなさん、ステキなgolang生活を。
ワッフルワッフル!
flynnを使ったオートスケーリングシステム
はじめまして。
そもそもオートスケーリングとはなんでしょうか
flynnとは?
flynnとはなんなのでしょうか
$ curl -fsSL -o /tmp/install-flynn https://dl.flynn.io/install-flynn... take a look at the contents of /tmp/install-flynn ...$ sudo bash /tmp/install-flynn});
$ sudo flynn init
$ sudo flynn daemon
かなりログが出てきてなにやら始まります。
$ sudo CLUSTER_DOMAIN=test.localflynn.com flynn-host bootstrap /etc/flynn/bootstrap-manifest.json
$ sudo flynn cluster add -g test.localflynn.com:2222 -p GUhOzIhavtZoqbJr0zykUvJKyaOYLPvT6wrABF29GwY= default https://controller.test.localflynn.com dd3e7f0a438a69f715a0a660f5d1ebc1
flynn clusterコマンドによって現在登録されているクラスタ一覧が出力されます。
$ sudo flynn
cluster NAME URL
default https://controller.test.localflynn.com (default)
次git pushするための鍵を登録します。
そのときに使う鍵です。
$ sudo flynn key addKey ab:fd:f5:2a:13:a1:66:2d:ho:ge:b3:2d:8e:29:9c:63 added.
$ git clone https://github.com/flynn-examples/nodejs-flynn-example
$ cd nodejs-flynn-example$ sudo flynn create example //flynnにリポジトリを登録
There is already a git remote called flynn //既にリポジトリが登録されている場合はメッセージが出る。
Are you sure you want to replace it? (yes/no): yesCreated example$ git push flynn master
ここまでやればアプリケーションのデプロイが始まるはずです。
本題です。
■ サービスディスカバリ / flynn discoverd
■ Load Balancer / flynn router
■ リソース管理 / なし
今回は単一ホスト上で動かしているので使用していません。
■ 仮想マシン / libvirt
■ 仮想マシンの生成/廃棄のイベント検知 / Flynn cli, 自作スクリプト
仮想マシンの生成/廃棄時にflynnが特定の文字列を標準出力に出力するため、それを自作スクリプトで検知します。
逆に仮想マシンが廃棄されたときにZabbixから登録抹消しています。
■ リソース監視 / Zabbix
⑥ また、flynn discoverdがweb appを発見して自動的にルーティングの対象に加えてくれます。新たにweb appが加わることで全体としての負荷は落ち着く(はずです。)
⑥ そしてflynn discoverdが自動的に廃棄したweb appをルーティング対象から除外してくれます。
flynnの機能的には複数台のマシンでクラスタを組み、マシンをまたがってflynnを動作させることも可能なようです。
web appに対してhttpリクエストを投げまくって負荷をコントロールして、負荷に応じてweb appの数が増減するかを確認します。
実験結果
結果は以下のグラフになります。
グラフをよく見てみると、負荷追加したときにオートスケーリングシステムが反応してapp追加が行われています。app追加によってCPUの平均使用率が減少しています。負荷削減したときには、app廃棄が行われ、CPU平均使用率が上昇しています。
おわりに
そのときに比べると必要なツールが少なくて楽でした
参考:http://hogepiyo.hatenablog.jp/entry/2014/07/06/172217
flynnはたまに起動しなくてもう一回手順を最初からやり直すとうまくいくとか不安定なところがあってこれからに期待です。
最初は複数マシン上でflynnを動かそうとしてましたが、途中で単体マシン上でしか動かなくなり諦めました・・。
それにしてもrouterとdiscoverdはよかったです。
自分ですると結構めんどくさい部分だったので自動的によきに計らってくれるというのはかなり楽でした。
アプリケーションの個数をコマンド一発で変えられるのもよい感じでした。なんといっても楽。
欲を言えば負荷計測機能が欲しいところです。今後に期待。
ということでflynnを使ってオートスケーリングシステムをつくってみたお話でした。
楽しんでいただけたら幸いです。
付録
オートスケーリングシステムをつくるにあたっての自作スクリプトのソースコードはこちら
https://github.com/marshi/autoscaling
(以前別の機会で作ったapache Mesos + docker用のスクリプトも入っています)
新米Androiderが開発する上できっと役立つであろう10のサイト
昨年の6月にサーバサイドJavaエンジニアからAndroiderへ暗黙な型変換でジョブチェンジしました。会社ではAmeba事業本部でAndroidアプリの開発を担当しています。
いや~、もうすっかり春ですねー
春といえば・さく・・・DroidKaigiーーー!!
Androiderとしてはぜひ参加したいイベントですねー
これからAndroid開発するにあたって見ておいてほしいサイトを2つご紹介
1. Android Development Training Course Repository
株式会社mixiが公開しているAndroidアプリ開発のトレーニングサイト
https://github.com/mixi-inc/AndroidTraining
2. Best practices in Android development
Android開発に関するベストプラクティスが文章で公開されてます。
https://github.com/futurice/android-best-practices
(日本語訳はこちら)
https://github.com/futurice/android-best-practices/blob/master/translations/Japanese/README.ja.md
とても参考になったAndroidアプリプロジェクト3選
新米Androiderがいざアプリ開発を始めると、色々な疑問が次々に湧いてきます。
・どういう構成で作ったらいいんだろうか…?? (´・ω・`)
・一般的に使われているネットワークやDBまわりのライブラリは?? (´・ω・`)
・リソースはどんな感じ定義すると管理しやすいのかな?? (´・ω・`)
・Gradleでビルド?どんな感じで書けばイケてる?? (´・ω・`)
Google Samples
1つ目はGoogle社が公開する実装Sampleアプリです。
https://github.com/googlesamples
Google I/O Android App
2つ目はGoogle I/O 公式アプリです。
https://github.com/google/iosched
やはりGoogle純正アプリということで、お手本的な存在ですね。
Rebuild.fm for Android
3つ目は有名なPodcastの非公式Androidクライアントです。
https://github.com/rejasupotaro/Rebuild
U+2020
4つ目はAndroid界隈の巨匠JakeWharton氏がベストプラクティスを詰め込んだショーケースアプリです
https://github.com/JakeWharton/u2020
イケてるAndroidライブラリが見つかるサイト2選
Awesome Android
1つ目は弊社のAndroidリードエンジニア @wasabeef さん が運営するUI/UX & Coreライブラリまとめサイト
・https://github.com/wasabeef/awesome-android-ui
私も最近メンテナーとしてお誘いいただいたので、いいもの見つけたら更新していきます^^
Android Arsenal
2つ目は世界的に一番見ている人が多そうなAndroidライブラリ&ツール全般を扱うサイト
https://android-arsenal.com/
Material Designで使えるアイコンサイト2選
Material Design Icons
1つ目はGoogle社公開するのアイコン集サイト
https://github.com/google/material-design-icons
Material Design Community Icons
2つ目はコミュニティーが提供するアイコン集サイト
http://materialdesignicons.com/
おわりに
アキバ系社会人ドクターのすすめ
こんにちは.技術本部・秋葉原ラボのエンジニア,2年目(そろそろ3年目)のKazumiです.
弊社のエンジニアブログでは,技術の話が多いですが,今回は,社会人ドクターをしている私の社会人ドクタースタートの経緯や,エンジニアと社会人ドクターの二足のわらじ生活,社会人ドクターを始めて気づいたことなどをご紹介したいと思います.
博士課程に興味のある社会人の方や,就職あるいは進学で悩んでいる学生の方の参考になれば幸いです.
自己紹介
私は,現在入社2年目でD1になります(このエントリーが公開される頃には入社3年目でD2??).技術本部・秋葉原ラボでデータ分析・機械学習システムの開発・運用を担当しながら,とある国立大学の博士後期課程で金融工学の研究をしています.
もう少し具体的に説明します.
業務でやっていることは,Java・R・Pythonを使ったデータ分析や機械学習システムの設計・開発・運用です.現在は,トレンド検知システムやスパムフィルタリングシステムに携わっています.
一方,大学院での研究テーマは,新聞記事などのテキストから人々の心理や世の中の雰囲気を表すインデックスを作成し,株価との関係を明らかにすることです.研究の詳細については,論文や共同研究者の記事などをご覧ください.
入学までの経緯
きっかけ
博士後期課程へ進学したいと強い気持ちがあったわけではありません.
準備
まず,志望する大学・研究室を決めなければいけません.仕事をしながら研究を行なうので,平日に大学に通うことは難しいです.そこで,共同研究者の方に紹介頂いた,社会人ドクター受け入れ実績のある教授にメールを送りました.メールでは,具体的な修了要件や研究指導についての確認をして,取り組みたい研究を伝えました.
志望する研究室が決まったので,いよいよ入試勉強を始めます.試験科目は,英語と面接でした.英語に関して,私は経済学専攻でしたので,Pop Internationalismや,表紙がイケてるhappiness & economicsを読みました.
面接対策では,認められる業績を作ることを意識していました.入学前に論文を執筆していると,面接で有利になりますし,また論文執筆自体も博士後期課程修了条件になっているからです.
面接
修士時代の成績について話がありました.博士後期課程に進学できる成績だったので,成績については,特に問題にはならなかったと考えています.
それよりも,博士後期課程で取り組む研究の計画や目標が話の中心になります.例えば.研究が既存の研究と比べてどのような新規性があるのか,また,先行研究よりも「良い」結果を出すためのアイディアについてなどでした.
業務と研究
無事に入学することができ,社会人ドクター生活がスタートしました.
平日の深夜や週末に指導教員の研究室などで研究・作業をするというスタイルです.ここからは,二足のわらじ生活を始めて,良かったことと悪かったことについて書きたいと思います.
PROS
社会人ドクターを始めて少し経った頃からいいことがいくつかありました.
第一に,業務と研究が有機的なつながりを持ち始めたことです.
たしかに,私の専攻している金融工学は,業務と直接的な関係はありません.業務で,株式市場の予測を行なったり,特定企業の業績の分析をしているわけではないからです.しかし,関連している箇所もあります.研究で行っているシステム設計・データ収集・リアルタイム性・時系列分析・モデル解釈に関する知識と経験は,業務にも役立ちます.また,その逆もあります.
第二に,プログラミングをする時間が圧倒的に増えました.
平日はもちろん,休日もプログラミングをしたり,コードリーディングをしています.周りの達人の方々のレベルと同じ!とまではいきませんが,少しは成長できたかなと思います.
第三に,大学の先生方・学生さんとのパイプを広げることができたことです.
多くの学会・研究会に参加することになるので,自然と名刺交換ができます.いつか大学の研究者の方々と協力をして,いいサービス・仕組みを作ることができればいいなと妄想しています.
第四に,フィードバックの豊富さです.
会社でやっていることは守秘義務があるので,問題解決のためのアドバイスが社内のみで限定的になりがちです.しかし,研究報告は,会社と比べて制限はほとんどないので,多くのフィードバックを期待できます.ある問題に対する見方・解決策が,会社員と研究者の間で同じであったり異なっていたりすることが,面白く気付きが多いです.
第五に,学割です.
アカデミックオプションがある場合は,積極的に活用するようにしています.同僚から「学割せこい!」と言われたときには,学割を利用するために支払った金額を説明しましょう.
CONS
もちろんいいことだらけではありません.
第一に,休む暇がありません.
業務後の深夜と休日に研究をして疲れが溜まって,業務に支障を来すことはあってはならないことです.自分なりのストレス発散方法が必要です.私の場合,もうくじけそうなときは,水炊きと焼肉です.
第二に,お金の問題です.
入学金・授業料は決して安くはありません.約3年通うとすると,入学金と授業料で約200~250万円必要です.いつお金が必要になるかわかりません.もしものために医療保険には若いうちから加入しておく必要があるでしょう.
第三に,休暇申請に関することです.
学会・研究会など,業務と関係のないイベントは有給を取って参加しなければなりません.「もう有給がない!」という状況に陥る可能性が有ります.社会人ドクターをするには,徹底した健康管理が必要かもしれません.弊社シェアハウスに設置されているトレーニング器具で体を動かしたりしています.
まとめ
Be the Worstという言葉があります.これは,チームの中で最低であることを自覚して,地道に努力する必要があるという意味です.私のような若手エンジニア・若手研究者にとって,良き指導者・同僚のいる環境に身を置くことは,会社でも大学でも重要だと感じた社会人ドクター1年目でした.
今後は,博士論文の執筆と研究成果を業務に取り入れることを考えながら,残りの社会人ドクター生活を(周囲に感謝の気持ちを忘れず)駆け抜ける予定です.
参考
社会人ドクターやっておいた方がいいことリスト
謝辞
社会人ドクターをご理解頂いている職場と進学を勧めて頂いた先輩社員に,この場を借りて感謝致します.
Tellme for Androidで使ったライブラリやツールを紹介するよ
こんにちは。エンジニアの清水です。
昨年の7月まではフロントエンドエンジニアとして主にJavaScriptを書いていたのですが、2014年8月からネイティブエンジニアとしてAndroidアプリを作っていました。
4月からはまたフロントエンドの仕事もしています。
今回は、私が開発に携わったTellmeというQ&AサービスのAndroidアプリで利用したライブラリを紹介してみようと思います。サンプルコードも書く意欲が湧いたものは書いていきます。
※ちなみにこんなアプリです
Libraries
ButterKnife
ButterKnifeはアノテーションを用いてView Injectionを行うライブラリです。
これを使うとonCreate/onCreateView
の中でfindViewById
を書き連ねる必要がなくなります。
Activityで使ってみると下記のような感じでいけます。
class SampleActivity extends Activity { @InjectView(R.id.user_name) TextView mUserName; @InjectView(R.id.user_avatar) ImageView mUserAvatar; @OnClick(R.id.btn_follow) public void follow() { // call api } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_sample); ButterKnife.inject(this); }}
現状の最新版はv6.1.0ですが、次回メジャーバージョンアップ(v7.0.0)から@InjectView
が@FindView
に変わるらしいので、現在利用している方は注意しましょう。
Otto
Ottoは、いわゆるPub/SubをJavaで実現するためのライブラリです。
フロントエンド開発では、Backbone.EventsのようなEventBusやDOM Eventをカスタマイズしたものを使っていたので、同じような機能を使いたいと思い行き着いたのがOttoでした。
TellmeではAPIの戻り値を受け取ったり、Fragment - Fragment間、Fragment - Activity間のデータのやりとり等に使っています。
// apimApi.loadNewArrival(new Callback<List<QuestionResponse>>() { @Override public void success(List<QuestionResponse> questionList) { // BusProviderは、Busインスタンスを保持するシングルトン // NewArrivalLoadedはResponse/Errorを格納し受信先へ渡すためのオブジェクト BusProvider.get().post(new NewArrivalLoaded(questionList)); } public void failure(Error error) { BusProvider.get().post(new NewArrivalLoaded(error)); }});// Fragmentpublic void onResume() { // イベントを受信できるように登録 BusProvider.get().register(this);}public void onPause() { // イベントの受信を解除 BusProvider.get().unregister(this);}@Subscribepublic void onNewArrivalLoaded(NewArrivalLoaded event) { if (event.isError()) { return; } // some code}
同じ機能を提供するライブラリとして、greenrobotの提供するEventBusがあります。こちらのほうがOttoよりも高機能でパフォーマンスがいいらしいのですが、結局アノテーションでイベント受信先を定義できるOttoの方を選びました。
ただ、EventBusもv3.0でアノテーションをサポートするようなので、近いうちに”書きやすさ”という意味での差はなくなるかもしれません。
Retrofit
Retrofitを使うと、通信周りの処理が非常に簡素に記述できます。
基本的にinterfaceの定義をするだけですみます。
// API定義public interface QuestionApi { @GET("/api/questions/newarrival.json") void loadNewArrival( @Query("since_id") Integer sinceId, @Query("max_id") Integer maxId, @Query("per") Integer per, Callback<List<QuestionResponse>> cb);}// 使い方RestAdapter restAdapter = new RestAdapter.Builder() .setEndpoint("http://tell-me.jp") .setConverter(new GsonConverter(new Gson())) .build();QuestionApi api = restAdapter.create(QuestionApi.class);api.loadNewArrival(null, null, 20, new Callback<List<QuestionResponse>>() { @Override public void success(List<QuestionResponse> questionList) { // QuestionResponseはPOJO } @Override public void failure(RetrofitError error) { }});
サンプルはコールバックを利用していますが、RxAndroidを併用すると結果をObservableで受け取ることもできます。
マルチパートリクエストにも対応しているので、画像アップロードだってお手のものです。
OkHttp
Retrofitと同じくsquare製のHttpクライアントです。SPDYにも対応していますが、Retrofitのついでに入れただけなので特に有効活用してないです^^;
Glide
画像の読み込みライブラリです。bumptechというのは、あれです。Googleに買収されたBumpです。
Glideは画像読み込み時にActivityやFragmentを指定することができます。これにより、対象のActivity/Fragmentのライフサイクルに沿った読み込み・開放処理をGlide側がいい感じに行ってくれるようです。
インターフェースはPicassoとほとんど変わらないので、Picasso使いなAndroidエンジニアなら特に違和感なく使えると思います。
アニメーションGIFが素のママで使えたり、細かくキャッシュ設定できる点がPicassoよりよいです。
Timber
ログをいい感じに出力してくれるライブラリです。
Android標準のログでデバッグ時に表示してリリース時には表示しない、みたいなことを実現しようと思うと、以下のようなコードになると思います。
if (BuildConfig.DEBUG) { Log.d(TAG, "foo");}
これだと、毎回if文書いたりしてダルいですし、if文忘れてデバッグログが本番アプリで出力される、とかあるあるです。
そこでこのTImber。Treeインターフェースを利用して、ログ出力の可否を自由に設定できます。
Applicationクラスで一度設定したら、後は特に条件分岐を加える必要もなくライブラリがよきに取り計らってくれます。
本番アプリでは基本ログ出力せず、エラーログをCrashlyticsに送りたい、というようなニーズにも簡単に対応できます。
IcePick
savedInstanceState関連の処理をアノテーションを利用して簡素にできるライブラリです。
Parcelable自動生成する系のライブラリと組み合わせる時は、同じ作者のAutoParcelがいいようです(AutoValueのAndroid移植版なので、またちょっと別の知識も必要になってきますが…)
Android-ObservableScrollView
ListView/ScrollView/WebViewに統一されたスクロールイベントのインターフェースを提供してくれるライブラリです。これを利用すると、Toolbarをスクロールで隠す/Floating Action Buttonをスクロールで隠すといったような処理を、一つにまとめることができます。もう、ListView/ScrollView/WebViewの三通りの実装をしなくてもよいのです。
サンプルも充実しており、非常に参考になります。
Retrolambda
Java8のラムダ式をAndroidでも使えるようにしてくれます。それ以上でもそれ以下でもありませんが、ラムダが使えるようになるだけでだいぶコードの見通しがよくなります。
// beforenew Handler().post(new Runnable() { @Override public void run() { Timber.d("some code"); }});// afternew Handler().post(() -> Timber.d("some code"));
Tools
Android Studioプラグイン
Android Parcelable code generator
Parcelableを使うのに必要なあの膨大なコードを自動生成してくれます。
List用のコードがうまく生成できなかったり不具合はいくつかありますが、ないよりはだいぶマシです。
Otto IntelliJ Plugin
上で紹介したOttoのイベント発信元と受信先を簡単に行き来できるようにするAndroid Studio/IntelliJ用のプラグインです。
Ottoを利用するとコールバックが減ってコードがスッキリする反面、どのイベントをどこでハンドリングしているかが分かりづらくなりがちなので、とても重宝しています。
Grunt
node.js製のタスクランナーです。
デザイナーさんに用意してもらった画像のdpi別バリエーションを作成・減色するために使用しています。
いきなりJavaScriptが出てきておや?と思うかもしれませんが、使えるものは言語を問わず使うと良いです。
grunt-resource-resizer
まだ発展途上ですが拙作の画像リサイズプラグインです。
一つ画像用意しておけば任意のサイズに一括で書き出してくれます。よさ気なものが見つからなかったので作りましたが、いいのがあったら教えてください。
grunt-image
弊社1000chさん謹製の画像最適化プラグインです。
メタ情報の削除だけでなく、減色もしてくれる優れものです。
詳しくはこのあたりをどうぞ
まとめ
いかがでしょうか。少しでも参考になったら幸いです。
こうして見てみると、Square/Jake神製のライブラリが結構多いですね。もうサンフランシスコに足を向けて眠れません。
同じ作者のライブラリを使いすぎるとなにかあった時に怖い気もしますが、まあOSSですしどうにかなるでしょう。
p.s. そろそろアメブロもMarkdownとシンタックスハイライト対応してほしいです
Written with StackEdit.
PiggPARTYでのリアルタイム通信の仕組み
ピグ事業部でサーバーサイドエンジニアをしている有馬です。
先日、弊社よりスマートフォン向けネイティブアプリとして、 「PiggPARTY」がリリースされました。
PiggPARTY Androidアプリ
iOSアプリは近日公開予定です
PiggPARTYは、スマートフォンのアプリ上で、 顔や洋服などの様々パーツを組み合わせて、自分好みのピグ(アバター)を作成することができ、
渋谷エリアや、原宿エリアといった現実を模したエリアや、 好みの家具で模様替えした自分のお部屋でパーティ(イベント)を開催したり、 他のユーザーと、テキストチャットやスタンプなどで、 リアルタイムにコミュニケーションを楽しむことができるサービスです。
PCアメーバピグをご存じの方には、そのスマートフォン版というと伝わりやすいかもしれません。
PiggPARTYでは、同期的なリアルタイムコミュニケーションを実現するために、 新たにリアルタイム通信サーバーライブラリをスクラッチで開発しており、 今回はその仕組みについて紹介いたします。
なお、弊社内ではそのライブラリをtoychatという名称で開発しており、 以降文中でもその名称を使用させていただきます。
(PiggPARTYでは、他にも、clay(クレイアニメーション)や、origami(折紙)といった、 ユニークな名称でライブラリ開発が行われています。)
toychatの特徴
toychatは、Node.jsで書かれたサーバーライブラリで、 リアルタイムのチャットアプリケーションや、 オンラインゲームのゲームサーバーなどでの利用を想定して作成されています。
特徴としては下記になります。
- TCPベースの分散リアルタイムメッセージングサーバー
- MQTTやWebSocketなどマルチプロトコルを選択可能
- フェイルオーバー
toychatの仕組み
分散リアルタイムメッセージング
TCPベースで、チャットのようなリアルタイムメッセージングサーバーを考えた場合、 最小構成は下記のようなものが考えられます。
1台のサーバー、ユーザー間を、WebSocketなど、サーバーからのPush通信が可能なプロトコルで接続し、 サーバーは、あるエリアに送信されたメッセージを、そのエリアに接続中のユーザーに配信します。
しかし、当たり前ですが、1台で大量ユーザーの負荷を捌くのは限界があります。 そこでサーバーを分散するのですが、サーバー間で同一エリアへのメッセージをどう同期するかが問題になります。
社内外事例から、いくつかのパターンを参考に検討しました。
RabbitMQ、Redisなどメッセージキューを利用したパターン
この構成では、エリアに問わず、ユーザの接続はロードバランサーにより均等に振り分けられます。
バックエンドにメッセージキューを配置し、 キューを介してサーバー間でメッセージをやりとりすることによって、 同一エリアへのメッセージを同期します。
分散チャットシステムでは定番的な構成なように思います。
非常にシンプルでステートレスなため、スケールもしやすいですが、 PiggPARTYのようにメッセージ送信だけでなく、エリアに関する情報を多数取り扱うようなサービスでは、 同一エリアの情報を同期するためのデータストアが必要になり、アプリケーションコードが複雑化しやすい印象です。
例えば、PiggPARTYではエリア内におけるユーザーの座標情報を、ユーザーが移動するたびに更新しますが、 これらを複数のサーバーで共有更新するとなると、 更新負荷や並列更新の同期化など一筋縄ではいきません。
ログインサーバー、 チャットサーバーのパターン
この構成では、 ユーザーはエリアへの入室に際し、どのチャットサーバーに接続したら良いのかをログインサーバーに問い合わせ、 指示された接続先に接続します。
同一エリアへの接続は必ず同じサーバーに振り分けられるため、 エリアに関する情報をサーバーのオンメモリで取り扱うことができ、 高速かつ比較的シンプルにアプリケーションを構築することができるように思います。
PCアメーバピグはこれに近い構成になっています。
チャットサーバーを並列に並べることでスケールしますが、 チャットサーバー分グローバルIPが必要になるなど、運用面で若干複雑になる傾向がありました。
toychatの構成
これらを参考に、toychatでは下記のような構成で分散リアルタイムメッセージングを行っています。
ピグライフなどPCピグゲームの仕組みにかなり近い構成になっており、 より役割を明確にシンプルにした構成になっています。
それぞれの役割を簡単に説明します。
ProxyServer
ユーザーの接続を受け付け、 エリアへのメッセージや、接続断などのイベントを、エリアに対応するChatServerプロセスへルーティングします。 また、ChatServerから送信されたメッセージをユーザーへ送信します。
ChatServer
ProxyServerを介して、ユーザーとのメッセージングを行います。
各ChatServerプロセスは、それぞれ異なる一つ以上のエリアを担当し、同じエリアにいるユーザーへメッセージを送信したり、 エリア内へのメッセージのブロードキャスト等を行います。 またインターナル通信により、異なるエリアを担当するChatServerプロセスにメッセージを送信することもできます。
アプリケーションでの利用
ProxyServerやChatServerはNode.jsのEventEmitterを継承しており、 ユーザーの接続、接続断、エリアへの入退室、メッセージ送信など各アクションに応じてイベントを生成します。
ライブラリを利用するアプリケーションはリスナーを登録することで、 各イベントのタイミングで任意の処理を実行することが可能です。
下記は、ライブラリを利用するコードのイメージです。
分散環境でも、単一のサーバーで処理を行っているかのような感覚で開発できる、というイメージが伝わればと思います。
/** ChatServerに送信されたメッセージを、* エリアに入室中の他ユーザーにブロードキャストするコード例** chatUser メッセージ送信者を表すオブジェクト* room ルーム(エリア)を示す文字列* body メッセージ*/chatServer"message" on// エリアに入室中のユーザーを取得var joinedUsers = chatRooms room;if joinedUsers// メッセージの送信者以外を取得var others = _ values joinedUsers filterreturn joined userId !== chatUser userId;;// 送信者以外にメッセージをブロードキャスト(実際にはProxyServerを介して送信される)chatServersend others room body;;
通信プロトコル
toychatではユーザー端末との通信プロトコルは差し替え可能な作りになっています。
例えば、PiggPARTYではMQTT(※)プロトコルを使用していますが、 WebSocketなど異なるプロトコルを使用することも可能です。
ProxyServerとChatServer間はJSON over TCPでsokcet通信を行っています。
(※) Facebook Messangerなどで利用されているPub/Subモデルの軽量メッセージングプロトコル
https://www.facebook.com/notes/facebook-engineering/building-facebook-messenger/10150259350998920
スケールアウト
サーバーの負荷が増えてきた場合、下記のようなイメージで スケールアウトしていくことが可能です。
このように、サーバーを横に並べていくことで、 ProxyServer、ChatServer間の接続数がTCPポート数の上限を超えないレベルまでは、 この構成で負荷を分散することができます。
ルーティングとフェイルオーバー
各ChatServerプロセスがどのエリアを担当するかは、 Consistent hashingを使用して決定しています。
ChatServerプロセスに障害が発生した場合は、 ハッシュリングの再計算を行い、エリアの割り当てを更新します。
各サーバー間で正しくエリアの割り当てを行うには、 各ChatServerプロセスのリストを同期し、 各サーバー間で同様のハッシュリングが計算されている必要があります。
プロセスリストの同期にはいくつかの方法をとることができますが、 PiggPARTYでは、Serf (※)を利用して同期を行っています。
プロセスを監視し、異常があった場合には、 Serfを通して情報を伝播し、プロセスリストの更新を行っています。
アプリケーション側でプロセスリストの更新イベントを拾い、 ハッシュリングの再計算を行っています。
まとめ
以上、簡単ではありましたが、 PiggPARTYでのリアルタイム通信の仕組みについて紹介いたしました。
toychatの開発にあたり、アメーバピグやピグライフなど、 既存の大規模リアルタイムサービスの仕組みが大変参考になりました。
これらを作り上げた先人のエンジニア達に感謝するとともに、 今回の記事が少しでも皆様の参考になれば幸いです。
最後までおつきあいいただき、ありがとうございました。
最速を究める! 2つのサーバ間で特盛りデータを30倍速で転送する方法
こんにちは. エンジニアの平野です. ふだんはプライベートクラウドのサーバハードウェアとストレージを担当しています.
サーバのリプレイスや増設, 仮想サーバの移植などでテラバイトクラスのデータを2つのサーバ間で転送することがよくあります.
こんなとき, 転送終了を待ちながら「あと何時間掛かるのかなー」とか「もっと速く転送終わらないかなー」なんて考えたことはありませんか?
今回は下記のようなシーンで活躍する, 特盛りデータを30倍高速に転送する方法をご紹介します.
- サーバの交換でデータを移設したい
- MySQLスレーブサーバの増設したい
- 仮想サーバを別のホストに移植したい
- 大量のファイルを別のサーバに移設したい
- 大容量データをバックアップしたい
■ 環境を用意
ServerAとServerBの2つの物理サーバを用意し, 1Gbpsのスイッチ経由で接続します.
2つのサーバスペックは下記の通りです.
ServerA(送信側):
DELL PowerEdge R630
E5-2650L v3 x2 (合計47Core)
DDR4 256GB
1.2TB SAS HDD x2 RAID1
ServerB(受信側):
DELL PowerEdge FC630
E5-2650L v3 x2 (合計47Core)
DDR4 256GB
1.2TB SAS HDD x2 RAID1
下記のコマンドで, 転送するデータを2つ作っておきます.
ServerA# dd if=/dev/urandom of=rand.dat bs=1M count=1024
ServerA# dd if=/dev/zero of=zero.dat bs=1M count=1024
今回は, 圧縮率の良いファイルとそうではないものを試しますので, ファイルを2つ用意します.
■ テスト条件
各テストは3回実施し, 平均値を結果として取ります.
テスト開始前は下記コマンドでcacheを空っぽにしておきます
ServerA# sync; echo 3 > /proc/sys/vm/drop_caches
■ 転送してみる
準備ができたところで, 先ずは何も考えずにscpで転送してみます.
ServerA# scp zero.dat root@ServerB:./
100% 1024MB 102.4MB/s 00:10
結果: 102.4MB/s ≒ 819.2Mbps
普通ですね. The普通. 普通ofふつう.
この結果を基準に, 高速化をはかっていきます.
scpでの転送は, 暗号化・復号化の処理がボトルネックになっているみたいですので, 暗号化せずに転送してみましょう.
同じフロアであったり, 経路が暗号化されているなど, セキュアな環境同士であれば暗号化は必要ありません.
今回は, nc(netcat, nmap-ncat)を使ってみます.
ncは, たいていのLinuxディストリの標準リポジトリから導入できます.
ncを使ったファイルのコピーでは, 受け側でTCPポートをListenし, 送り側からそのポート宛にデータを流し込みますので, ServerBでncコマンドを叩いたあとで, ServerAからncコマンドを叩きます.
ServerB# nc -l 9999 > zero.dat
ServerA# dd if=zero.dat bs=1M | nc ServerB 9999
9.19443 s, 117 MB/s
結果: 117MB/s ≒ 936Mbps
ほぼワイヤスピードですね.
scpと比較して114%速くなりました.
サーバリソースは余裕ですが, ネットワークがボトルネックになっていて, これ以上多くは転送できません.
これくらい速ければ良さそうな気もしますが, ファイルの中身はゼロです. 効率よくネットワークを使うために, データを圧縮してみましょう.
てっとり早くgzipを使ってみます.
ServerB# nc -l 9999 | gzip -d > zero.dat
ServerA# dd if=zero.dat bs=1M | gzip -c |nc ServerB 9999
10.5008 s, 102 MB/s
結果: 102MB/s ≒ 816Mbps
非圧縮とくらべて遅くなってしまいました.
gzipはCPUリソースを食い過ぎるみたいです.
もっとCPUにやさしい圧縮アルゴリズムを使わないと, 速さは稼げないみたいなので, こんどはlzoを使ってみましょう.
lzo(lzop)は, OpenVPNにも使われている, ネットワークストリームの圧縮・解凍が得意なアルゴリズムです.
これもCentOSであればepelのリポジトリから導入できます.
ServerB# nc -l 9999 | lzop -d > zero.dat
ServerA# dd if=zero.dat bs=1M | lzop -c |nc ServerB 9999
3.84884 s, 279 MB/s
結果: 279MB/s ≒ 2,232Mbps
1Gbpsの壁を越えました!
scpと比較して, 272%の高速化です.
でも, まだ満足できません!
今回使っているサーバはCPUコアが48コアありますが,
使われているのはncとlzopの2プロセス(2コア)だけです.
他の46個のプロセッサも無駄なく使ってあげれば, より高速化できるはずです.
分散圧縮ができる pigz を使ってみましょう.
pigzコマンドをオプション無しで叩くと, サーバで利用可能なプロセッサ数を自動判別して, すべてのプロセッサを使って並列処理してくれます.
pigzはgzipと同じアルゴリズムですが, 並列化すれば速度は他と比べものにならないくらい速くなるはずです.
pigzもepelのリポジトリから導入できます.
ServerB# nc -l 9999 | pigz -d > zero.dat
ServerA# dd if=zero.dat bs=1M | pigz -c | nc ServerB 9999
3.29683 s, 326 MB/s
結果: 326MB/s ≒ 2,608Mbps
記録更新! scpと比較して, 318%の高速化です!
サービスが動いていないサーバでは有効ですが, CPUを限界まで使っているので, KVMのホストマシンやサービスが動いている環境など, 共有リソース下でこれを使ってしまうと他のサービスの動作に影響が出てしまいます.
共有リソースの場合はlzopを,
専用リソースの場合はpigzを,
使う場面にあわせて使い分けるようにしましょう.
zero.datはとてもよく圧縮が掛かるデータですが, 圧縮が掛かりにくいrand.datはどうなるんでしょうか.
こっちのファイルでも試してみましょう.
ServerB# nc -l 9999 | lzop -d > rand.dat
ServerA# dd if=rand.dat bs=1M | lzop -c | nc ServerB 9999
26.5191 s, 40.5 MB/s
ServerB# nc -l 9999 | pigz -d > rand.dat
ServerA# dd if=rand.dat bs=1M | pigz -c |nc ServerB 9999
9.06602 s, 118 MB/s
結果:
lzop: 40.5MB/s ≒ 326.4Mbps
pigz: 118MB/s ≒ 994Mbps
pigzはほぼワイヤスピードが出ましたが, lzopは圧縮・解凍がボトルネックになってしまい, 速度が落ちてしまいました.
圧縮が効きにくいデータはlzopで転送しない方が良さそうですね.
■ 遅いネットワーク環境でのテスト
ここまで, 1Gbpsのストレスフリーなネットワーク環境でテストを行ってきましたが, 拠点を跨ぐ場合やクライアントVPNで接続している環境では, 転送速度はこんなに出ません.
ネットワーク速度がプアな環境も想定して, 試しに, ServerAとServerBの接続を100Mbpsに落としてテストをしてみましょう.
ServerB# ethtool -s p4p1 speed 100 autoneg off duplex full
ServerB# ethtool p4p1 | grep Speed
Speed: 100Mb/s
これでネットワーク接続が100Mbpsになりましたので, 各テストを実行してみましょう.
ServerB# scp zero.dat root@ServerB:./
100% 1024MB 11.1MB/s 01:32
結果: 11.1MB/s ≒ 88.8Mbps, 100%
ServerB# dd if=zero.dat bs=1M | nc ServerB 9999
91.1991 s, 11.8 MB/s
結果: 11.8MB/s ≒ 94.4Mbps, 106%
ServerB# dd if=zero.dat bs=1M | lzop -c | nc ServerB 9999
3.73494 s, 287 MB/s
結果: 287MB/s ≒ 2,296Mbps, 2,580%
ServerB# dd if=zero.dat bs=1M | pigz -c | nc ServerB 9999
3.19667 s, 336 MB/s
結果: 336MB/s ≒ 2,688Mbps, 3,027%
ServerB# dd if=rand.dat bs=1M | lzop -c | nc ServerB 9999
91.1698 s, 11.8 MB/s
結果: 11.8MB/s ≒ 94.4Mbps, 106%
ServerB# dd if=rand.dat bs=1M | pigz -c | nc ServerB 9999
90.9118 s, 11.8 MB/s
結果: 11.8MB/s ≒ 94.4Mbps, 106%
100Mbpsでは, 全体的にscpより高速となりました.
zero.dataをpigz圧縮で流した結果で, なんと30倍の高速化! 最高速度をたたき出しました. ネットワーク転送に割かれるCPUリソースが少なくなったためだと思います.
1Gbpsでrand.datをlzop経由で転送した時はlzopプロセスがボトルネックでしたが, 今回は100Mbpsなのでネットワークがボトルネックになっています. 実効速度300Mbpsを切るネットワークで, 圧縮率が悪いデータはは圧縮せずに転送した方が良さそうです.
■ 10Gbpsネットワークでのテスト
1Gbpsの通常環境と, 遅い100Mbpsとテストしてみましたが, 逆に速いネットワーク環境ではどうなるのでしょうか. 来たるべく未来の10Gbpsでも試してみましょう.
あらたにServerCを用意し10Gbps接続でServerAに直結します.
あまりにも速く転送が終わってしまうので, データも10GBに増やしてテストしてみます.
ServerB# scp zero.10GB.dat root@ServerC:./
123.4MB/s
結果: 123.4MB/s ≒ 987.2Mbps, 100%
ServerB# dd if=zero.10GB.dat bs=1M | nc ServerC 9999
51.7511 s, 207 MB/s
結果: 207MB/s ≒ 1,656Mbps, 134%
ServerB# dd if=zero.10GB.dat bs=1M | lzop -c | nc ServerC 9999
36.4958 s, 294 MB/s
結果: 294MB/s ≒ 2,352Mbps, 190%
ServerB# dd if=zero.10GB.dat bs=1M | pigz -c | nc ServerC 9999
35.0364 s, 306 MB/s
結果: 306MB/s ≒ 2,448Mbps, 198%
ServerB# dd if=rand.10GB.dat bs=1M | lzop -c | nc ServerC 9999
221.463 s, 48.5 MB/s
結果: 48.5MB/s ≒ 388Mbps, 34%
ServerB# dd if=rand.10GB.dat bs=1M | pigz -c | nc ServerC 9999
41.6055 s, 258 MB/s
結果: 258MB/s ≒ 2,064Mbps, 167%
想定通り, 圧縮しやすいデータは速いのですが, 圧縮の効きにくいデータはlzopでは高速化できませんでした.
pigzは両方のデータとも, 高速化できました.
あらたに用意したServerCは, 型落ちの12コアのサーバ機で, DISK性能もあまり良くないので, 48コアのServerBほど速度は出ませんでした.
送り側と受け側のCPU性能差がありすぎると, 高速化にも限界があるみたいです.
■ まとめ
scpは論外ですが, 状況によってベターな転送方法で行わなければ, 最高速度は出せないという結果となりました.
- 送信側/受信側で利用可能なCPUリソース
- 受信側/受信側のDISK IO性能
- ネットワーク帯域
- ネットワーク経路の圧縮率(VPNトなどで, 経路が圧縮されてる場合, 2重圧縮すると遅くなる)
- ネットワークに流す前のデータ圧縮率
それぞれを考慮してベストな選択をしましょう.
正しい選択をすれば, 30倍速でお仕事が終わりますので, 余った時間はゆっくりお昼寝他のお仕事に割くことができます.
回線やリソースの状況がよくわからないときは, 下記のコマンドで少量のデータを"味見"してから, 全部のデータを転送開始すると良いと思います.
ServerA# dd if=unknoun.dat bs=1M | head -c 100000000 | nc ServerB 9999
ServerA# dd if=unknoun.dat bs=1M | head -c 100000000 | lzop -c | nc ServerB 9999
ServerA# dd if=unknoun.dat bs=1M | head -c 100000000 | pigz -c | nc ServerB 9999
ファイルではなくディレクトリツリーを転送する場合は, tarで丸めると転送できます.
ServerB# cd /var/lib/
ServerB# nc -l 9999 | pigz -d | tar xv
ServerA# cd /var/lib/
ServerA# tar cf - mysql | pigz -c | nc ServerB 9999
■ さいごに
この方法を使うようになったのは何年か前からですが, 劇的にお仕事が捗るので, 100MB程度の転送でも, 日常的にnc+圧縮の方法で転送しています.
ncだけだとネットワークエラーの検知ができず, ファイルサイズだけで成功/失敗の判断をするしかありませんが, 何らかの圧縮アルゴリズムを挟めば, 途中でエラーを吐いてくれるので, できるだけ圧縮を挟んだ転送をおすすめします. (エラー判定は, tee経由でmd5sumに渡せばhash値が取れますが, CPUコストが掛かってしまいますし, 何より面倒です)
今回は導入が楽ですぐに使える lzop と pigz を使って高速化を試しましたが, snappyなど高速な圧縮アルゴリズムを使ったり, 圧縮レベルを変えてみたりしても良いかもしれません.
また, ncはTCPセッションが1本だけですが, 複数のセッションを使って分散化できれば, ネットワークリソースをより効率的に使えるかもしれません.
効率化って, やり始めるとキリがないですけど, いろいろな視点でベストを考えるのも楽しいものですね.
「ビックトラフィックCAMP」にメンターとして参加してみた
はじめまして、15新卒エンジニアの菊地です。
本ブログでは、5月15、16日に新卒エンジニア採用イベントの一環として開催された「ビッグトラフィックCAMP」へメンターとして参加した事についてレポートします。
ビッグトラフィックCAMPとは?
自社が運営するサービスをテーマに、大規模トラフィックにおけるサービス開発を体験する、学生の方向けのイベントです。
今回はテレビCMでもおなじみの「755」をテーマに、100万ユーザーの同時アクセスにも耐えられるサーバサイドアプリケーション開発を目指します。1日目はサーバサイドDay、2日目はチューニングDayと計2日間行われました。
1日目のサーバサイドDayは、参加者がそれぞれお好みの言語を選び、開発環境構築やAPIの開発スピードや品質を競います。2日目のチューニングDayは、1日目のサーバサイドDayで開発したAPIに対し、大規模なトラフィックに耐えうるチューニングを施し実行速度を競います。参加者には今回のイベント用に、AWSのインスタンスが各々に1つ提供されました。
このイベントにおけるメンターの役割は、参加者同士の意見のやりとりを支援することが主な役割となっています。
それではここから先は具体的なレポートに入っていきます。
1日目サーバサイドDay
1日目は環境構築、時間が余れば仕様書にのっとったAPI作成に取り掛かるという流れでした。私の使用言語はJavaということで、まずApache+Tomcatを使用して実装することに決めました。AWS環境にSSHでログインすると、以下のファイルが設置されていました。
user.tsv ユーザ基礎データ 100,000件
userId:ユーザーID
userCreateDataTime:ユーザアカウントの生成日時
box.tsv 箱基礎データ 100,000件
boxId:箱のID
boxCategory:箱のカテゴリ名
boxPriority:箱の優先順位
card.tsv カード基礎データ 1,000,000件
cardId:カードのID
cardMessage:カードに書かれたメッセージ
cardType:カードの種別
cardTags:カードに付与されるタグ
cardMetrics:カードに与えられた数値
user2card.tsv ユーザ対カード対応データ 200,000件
userId:ユーザのID
cardId:カードID
box2card.tsv 箱対カード対応データ 800,000件
boxId:箱のID
cardId:カードのID
次にAPI仕様書について説明します。以下の説明は、カードの条件、検索のルール、戻り値、検索シナリオという順になっています。
カードのメトリクスはint型では入りきらないものもでてくるので、long int型でテーブルを作成することにしました。
検索のルール
【プロトコル】
HTTP
【メソッド】
GET
【検索対象】
listCardsInBoxページ(path:/listCardsInBox)
【検索条件】
クエリストリングスにより指定する。key1=value1&key2=value2形式。パラメータ内容は下記「検索シナリオ」を参照。また、検索上限数は常に指定されるものとする。
【評価方法】
試験プログラムが下記「検索シナリオ」に沿ったリクエストをランダムに生成し、正しい結果を返した場合のみ評価(加点)する。各条件には既定の点数があり、それに応答時間が加味される。
戻り値
【型】
JSON形式で返す。
【項目】
・result
→結果に1枚以上のカードがある場合には「true」、結果が0枚の場合には
「false」を返す。
・data(配列)
→カードID、メッセージ、カードタイプ、タグ(配列)、メトリクス、オー
ナーを格納して返す。
【ステータスコード】
resultが「true」の場合は「200 OK」を、resultが「false」の場合は「404 Not Found」を返す。
検索シナリオ(検索のみ)
・箱で検索(1点)
→指定のIDの箱をオーナーにもつすべてのカードを検索。
・カテゴリで検索(1点)
→指定のカテゴリの箱をオーナーにもつすべてのカードを検索。
・タグ以外全部で検索する(5点)
→1.指定の箱、指定の箱カテゴリ、指定域内の優先度の箱をオーナーにもち、
2.指定のカードタイプ、指定域内のメトリクスをもつすべてのカードを検索。
・全部で検索(8点)
→タグも含むすべての条件に合致するすべてのカードを検索。
検索シナリオ(検索+ソート)
・タグ以外全部で検索+ソート(10点)
→「タグ以外全部で検索」の結果を1個以上のソート条件を適用。
・全部で検索+ソート(20点)
→すべての条件で検索し、すべてのソート条件を適用。
以上がAPI仕様書の説明になります。
サンプルリクエストとレスポンスはこの様になります。
【リクエスト】
http://IPアドレス/listCardsInBox?findByBoxCategoryEqual=長月&findByBoxPriorityLTE=90125&findByCardTypeEqual=北海道&sortByCardMetrics=descend&limit=10
【レスポンス】
{"result":true,"data":[
{"cardId":"Cd3y0e8i","message":"馬は速い。","type":"北海道","tags":["社外秘","CAPEX","業務委託","GCE"],"metrics":2171841991,"owner":"Bxspq03w"},
{"cardId":"Cd0yjqh9","message":"猿はかわいい。","type":"北海道","tags":["社外秘","OPEX","製造請負"],"metrics":2131759284,"owner":"Bxivh03x"},
{"cardId":"Cd9fasf6","message":"大きい羊。","type":"北海道","tags":["社外秘","関係者のみ","SAKURA"],"metrics":2126427314,"owner":"Bxvf03tk"},
{"cardId":"Cd3eb7d1","message":"遅い牛。","type":"北海道","tags":["2Q案件","Cloudn"],"metrics":2126158748,"owner":"Bx0xmee0"},
{"cardId":"Cdcpttjj","message":"兎は遅い。","type":"北海道","tags":["オンプレ","AWS","NiftyC"],"metrics":2111111115,"owner":"Bx74aawt"},
{"cardId":"Cd979aec","message":"かわいい猪。","type":"北海道","tags":["製造請負"],"metrics":2105876373,"owner":"Bxgsmbef"},
{"cardId":"Cdee0y8m","message":"大きい馬。","type":"北海道","tags":["4Q案件","AWS","GCE","MSAzure"],"metrics":2073393330,"owner":"Bxofa943"},
{"cardId":"Cd6v9wbv","message":"かわいい兎。","type":"北海道","tags":["4Q案件"],"metrics":2028819480,"owner":"Bxrzi2fu"},
{"cardId":"Cdcgrkjh","message":"猿は遅い。","type":"北海道","tags":["CAPEX","主任決裁済み","1Q案件","4Q案件","IDCFC"],"metrics":1974042405,"owner":"Bx6esf54"},
{"cardId":"Cdfj9f7k","message":"蛇は大きい。","type":"北海道","tags":["OPEX","社長決裁済み","GCE","IDCFC"],"metrics":1963100827,"owner":"Bx3soecb"}
]
}
ではここから、私が実際に行った環境構築について話します。
MySQLでcardテーブルと、boxテーブルをそれぞれ作成しました。テーブルの詳細は以下の通りです。
まず、データベース(以下、DB)を作成し、card.tsvとbox.tsvの中に含まれるデータをMySQLのテーブルに全て格納していきます。mysqlを選んだ理由は提供された環境に元々mysqlが入っていたので、それを使用することにしました。ファイルの中のデータはタブ区切りで保存されていましたので、格納は下記のmysqlコマンドで行いました。
LOAD DATA LOCAL INFILE 'インポートしたいファイル' INTO TABLE テーブル名 FIELDS TERMINATED BY '\t';
card.tsvはcardテーブルに、box.tsvはboxテーブルに、もそれぞれデータを格納しました。
次に、cardテーブルのowner列にデータを格納します。APIの仕様書を見た時、cardIdに紐づいたboxIdをowner列に格納しておけば解けることが分かったので、box2card.tsvを読み込み、boxIdをowner列に格納するコードを書きました。(ちなみにここには載せていませんが、チュートリアルを解く場合は、user2card.tsvからcardIdに紐づいたuserIdもowner列に格納しなければなりません。)
あと今回、Apache+Tomcatを使用するということで、Tomcatをまずインストールしました。インストールしたTomcatはTomcat7で、ApacheとTomcatを紐づける設定を行いました。
環境構築以外は、板敷さんによる「755の年末年始CM対応」に関するプレゼンが行われました。
「755」とは、藤田社長と堀江貴文さんが立ち上げたサービスで、著名人のトークをのぞいたり、直接やりとりすることを特徴とした、トークライブアプリのことです。開発、運営は株式会社7gogoで行われています。
板敷さんの発表は755のCMを行うことによりユーザが急増して、システムの負荷が上がるという問題に対し、どのように対処していったのかについて焦点が当てられたものでした。
CMにより発生する負荷対策に対して、藤田社長はこのように言いました。
「CMやるんだし、100万同時接続ぐらいはさばけないとね」
板敷さんはCM特別対策チームを発足し、5つの手順を明確にして対策を進めていきました。
対応方針検討
実装、アーキテクチャ変更
負荷試験実施
チューニング
本番環境に適用
1.対応方針検討
以下の観点で対応方針を検討しました。
キャパシティが常に検証可能であること
100万同時接続以降もスケールアウトできること
キャパシティがあふれた場合のセーフティネットがあること
最小工数で最大の効果が期待できること
2.実装、アーキテクチャ変更
対応前アーキテクチャはAPIが一か所にまとまっていますが、対応後のアーキテクチャはAPIを機能ごとに分散させています。これにより、API/DBのスケールやチューニングが最適化しやすいというメリットがあります。マイクロサービス化したのはコメント機能のみですが、これはコメント機能が一番リクエスト数が多くて負荷対応が必要だったからだそうです。
3.負荷試験実施
通常時のアクセスパターンとアクセスが多い時のアクセスパターンを組み合わせて実施していました。そしてチューニングの繰り返しです。
これらの取り組みにより、負荷が上昇する正月のあけおめイベントを乗り切ったそうです。
2日目チューニングDay
2日目は、運営エンジニアの方からチューニングポイントについての発表が行われました。が、その前にまず1日目に作成したAPIの作成の続きを行いました。
・API作成
私のTomcatの直下に存在するROOTディレクトリ以下のディレクリ構成はこのようになっています。今回は検索シナリオの中の箱で検索の機能をもったAPIの実装に取り組み、チューニング前後でどれくらいパフォーマンスが変化するか試してみることにしました。
servlet-apiはversion3.0のものを配置。作成したAPIは、SELECT * FROM card WHERE owner = (URLクエリパラメータから受け取ったboxId);というSQL文をたたき、cardテーブルの中身を表示させるコードを書きました。また、limitにも対応しなければならないということで、limitにも対応できるようにしました。
2日目はAPI作成の途中、運営エンジニアによるチューニングポイントのプレゼンが行われました。チューニングポイントは、カーネル編、WebServer編、DB編、キャッシュ編の4つに関してそれぞれ説明がありました。
・カーネル編
カーネルのパラメータの設定をいじります。カーネルのパラメータを調節するファイルは、sysctl.confです。
このファイルにTIME_WAITの発生を抑えるために以下の設定を試みます。
サーバーリソースを使いきれていない場合に、Defalut値を見直します。
※カーネルパラメータの設定変更はサーバ全体に影響を及ぼすので、十分な検討と検証を勧めます
sysctl.confに記載したものを反映する方法と反映されているか確かめる方法を以下の通りです。
反映:sysctl -p
確認:sysctl -a
・WebServer編
Apacheの設定(httpd.conf)をいじります。同時接続数が増え、error_logに同時接続数上限までアクセスが来ており、サーバのリソースを使いきっていない場合はMaxClientsを増やします。逆にサーバのリソースがいっぱいの場合、その数を減らします。
もう一つ紹介されていたものが、PHPと組み合わせたチューニング方法です。
ApacheとPHPのメモリの割り当てです。Apacheにおいて、MaxClientsなどで子プロセスの数を調節します。PHPにおいてはmemory_limitを調節し、1プロセスあたりのメモリ使用量を調節します。
MaxClients = 使用可能なメモリ量/Apacheの1プロセスが使用するメモリ量となります。
※memory_limitやMaxClients変更後はApacheを再起動する必要があります。
・DB編
MySQLのパラメータチューニングです。チューニングでポイントとなるのは、同時接続数の増減と割り当てメモリの増減です。
同時接続数:同時に100万というサービスに対し、DBが同時に100しかアクセスを受け付けない設定だった場合、サービスがダウンします。サーバの能力に応じて適切な値を設定する必要があります。
割り当てメモリ:MySQLやORACLE、PostgresSQLなどの多くのRDBMSはクエリーの結果をメモリ内にキャッシュします。このサイズが多ければ多いほど、多くのクエリをキャッシュして、INDEXが効かないクエリにも高速で応答できるようになります。ただし、マシンスペックより多くのメモリを割り当てると、DISK Readがかかって、パフォーマンスが悪化します。
※同時接続数などの変更はmysqlコマンドで行うか、設定ファイル(通常はmy.cnf)に記載後、mysqldを再起動する必要があります。
INDEXについて(ポイント!)
・カーディナリティの高いものだけにINDEXをはる
・極力INDEXが使われるSQLを書く(EXPLAINで確認する)
・無理にSQLに負荷をかけさせないで済ませる(大規模アクセスを前提としたシステムの考え方の場合)
・遅いSQLの調査の仕方は、MySQLではslowlogを設定しておけば抽出可能。
※ただし、設定すると若干遅くなるため、調査終了後はslowlog書き出しをOFFに
私はINDEXをどう貼ればパフォーマンスが上昇するのかわからなかったため、参考になるサイトを紹介してもらいました。こちらにも載せておきます。とりあえずチューニングの講義の際は、INDEXは中身にバラつきの多い列に貼るのがよいと聞いたので、ばらつきの多かったownerに貼ることにしました。
MySQLインデックスの基礎 : ひとつのテーブルに対するクエリの最適化法
http://yakst.com/ja/posts/2462
MySQLインデックスの基礎 その2 : 2つのクエリの違いとオプティマイザの判断
http://yakst.com/ja/posts/2385
・キャッシュ編
プロキシサーバを導入し、レスポンスをキャッシュする手法です。
昨年のTOTEC2014インフラチューニングにおいて、上位は皆Varnish Cacheを導入して、その上で他のチューニング観点で勝負するという取り組みが行われていたようです。
ちなみにチューニング前後でスコアに大きな変化が現れたので、載せておきます。inCorreountはチューニング後の方が多いですが、明らかにスコア自体は高いことが分かります。
最後に
今回のハッカソンにメンターとして参加し、渡されたAPI使用仕様書を見てどのようにDBを設計し、どういう手順でAPIを実装していくのかといった方針を明確にしておく大切さを実感しました。今後DBと連携して何かを実装していく機会があれば、パフォーマンス向上を意識してINDEXの貼り方にも気を配っていきたいと思いました。
ちなみに参加している学生の使用言語は全体的にPHPが多く、優勝者はGoLangを使用していました。上位の学生はほとんどINDEXを上手く駆使することで高得点を獲得していたようです。
サイバーエージェントでは、毎年このようなイベントが採用の一貫として定期的に開催されているので、興味のある学生のみなさん、ぜひ参加してみてはいかがでしょうか?
Ameba OwndのSEOを支える技術 for AngularJS
こんにちは、サーバーサイドのエンジニアをやっているoinumeです。今回は昨年8月ぐらいから作っていたAmeba Owndというサービスで行ったSEO対策について紹介します。
AmebaOwndって?
ブログ機能を備えたスタイリッシュなデザインのWebサイトを簡単に作成できるサービスです。
などのサイトがAmeba Owndを利用して作られています。
アーキテクチャ
ユーザーさんがWebブラウザでアクセスするページについてはAngularJS + REST API(Nginx + Go)で作られています。一方でGooglebotなどのクローラーからのアクセスの場合は、受けたリクエストをNginxがPrerender CacheというシステムにProxyして、このPrerender CacheからHTMLを返すようにしています。
AngularJSのSEO対策(なぜPrerender Cacheを使ったか)
現在のGooglebotは公式に発表されている通り、JavaScriptを実行することができます。ただ、
- どこまでちゃんとJavaScriptが実行できるのかわからない(Angular本当にちゃんと動くの?)
- レンダリングが途中でストップされ、コンテンツが中途半端にインデックスされないか
- Googlebot以外はJavaScriptが実行できるかよくわからない(中国語圏のBaidu, 韓国語圏のNaverなど)
- 開発スケジュール的に1.や2.をちゃんと検証している時間がなかった
が不安要素としてあったため、あえてVarnish + PhantomJS によるキャッシュシステムを用意しました。それがPrerender Cacheと呼んでいるものになります。
Prerender Cacheのアーキテクチャ
Varnish+Prerender(Node.JS + PhantomJS) という組み合わせになっています。Prerenderの前段にVarnishがいるのは、PhantomJSがHTML + JavaScriptをレンダリングするのに5秒以上かかるケースがあったため、レンダリング済みのページはキャッシュし、Googlebotに対して高速にレスポンスを返せるようにするためです。実際にはVarnishとPrerenderの前にELBとNginxがいるため、下の図のようにもう少し複雑なアーキテクチャになっています。
処理の流れは以下のようになります。
- https://starbucks.amebaownd.com/にGooglebotからのアクセスが来る
- NginxがPrerender CacheにProxyする
- Varnishに該当URLのキャッシュがある場合はそれを返す
- Varnishにキャッシュがない場合はlocalhost:8081で稼働しているNginxを通してPrerenderにProxyする
- PrerenderのPhantomJSがhttps://starbucks.amebaownd.com/にアクセスして、レンダリング結果を返す
- Varnishがキャッシュしてレスポンスを返す
Nginx
具体的な設定ファイルを見て行きましょう。まず、一番最初にリクエストを受けるNginxの設定です。$prerender = 1の場合はバックエンドのPrerender CacheのELBにProxyしています。# Prerender.ioset $prerender 0;if ($http_user_agent ~* "applebot|baiduspider|bingbot|bingpreview|developers\.google\.com|embedly|googlebot|gigabot|hatena::useragent|ia_archiver|linkedinbot|madridbot|msnbot|rogerbot|outbrain|slackbot|showyoubot|yahoo! slurp|Y!J-|yandex|yeti|yodaobot") { set $prerender 1;}if ($args ~ "_escaped_fragment_") { set $prerender 1;}if ($args ~ "prerender=true") { set $prerender 1;}if ($http_user_agent ~ "Prerender") { set $prerender 0;}if ($uri ~ "\.(js|css|xml|less|png|jpg|jpeg|gif|pdf|doc|txt|ico|rss|zip|mp3|rar|exe|wmv|doc|avi|ppt|mpg|mpeg|tif|wav|mov|psd|ai|xls|mp4|m4a|swf|dat|dmg|iso|flv|m4v|torrent)") { set $prerender 0;}# Resolve every 10 seconds# http://d.hatena.ne.jp/hirose31/20131112/1384251646resolver <resolver ip="None"> valid=10s;if ($prerender = 1) { #setting prerender as a variable forces DNS resolution since nginx caches IPs and doesnt play well with load balancing set $backend_prerender_varnish "<elb fqdn="None">"; rewrite .* /$scheme://$host$request_uri? break; proxy_pass http://$backend_prerender_varnish; break;}proxy_pass http://backend:3000/;
Varnish
/etc/varnish/default.vcl
VarnishのVCLファイルは以下のようになっています。
vcl 4.0;# Default backend definition. Set this to point to your content server.backend default { .host = "127.0.0.1"; .port = "8081"; .connect_timeout = 3s; .first_byte_timeout = 20s; .between_bytes_timeout = 10s;}sub vcl_recv { if (req.http.User-Agent ~ "(?i)i(Phone|Pad|Pod)") { set req.http.X-UA-Device = "sp"; } else if (req.http.User-Agent ~ "(?i)Android") { set req.http.X-UA-Device = "sp"; } else if (req.http.User-Agent ~ "(?i)Googlebot-Mobile") { set req.http.X-UA-Device = "sp"; } else { set req.http.X-UA-Device = "pc"; } if (req.method != "GET" && req.method != "HEAD" && req.method != "PUT" && req.method != "POST" && req.method != "TRACE" && req.method != "OPTIONS" && req.method != "DELETE") { /* Non-RFC2616 or CONNECT which is weird. */ return (pipe); } if (req.method != "GET" && req.method != "HEAD") { /* We only deal with GET and HEAD by default */ return (pass); } if (req.http.Authorization) { /* Not cacheable by default */ return (pass); } return (hash);}sub vcl_hash { if (req.http.X-UA-Device) { hash_data(req.http.X-UA-Device); }}sub vcl_backend_response { if (beresp.status == 401 || beresp.status == 402 || beresp.status == 403 || beresp.status == 404 || beresp.status == 500 || beresp.status == 501 || beresp.status == 502 || beresp.status == 503 || beresp.status == 504) { set beresp.ttl = 0d; } else { set beresp.ttl = 24h; } return (deliver);}sub vcl_backend_error { return (retry);}sub vcl_deliver { if (obj.hits > 0) { set resp.http.X-Cache = "HIT"; } else { set resp.http.X-Cache = "MISS"; }}
/etc/nginx/conf.d/localhost.conf
Varnishのサーバに同居しているNginxの設定は以下のようになっています。VCLファイルに名前解決が必要なELBのドメインを指定するとエラーになるため、このように間にNginxを挟んでいます。
server { listen 8081; server_name 127.0.0.1 localhost; root /usr/share/nginx/html; index index.html index.htm; access_log /var/log/nginx/prerender/access.log combined; error_log /var/log/nginx/prerender/error.log; location / { resolver {{ nginx.resolver }} valid=10s; set $backend "<elb fqdn="None">"; proxy_pass http://$backend; } error_page 404 500 502 503 504 /50x.html; # redirect server error pages to the static page /50x.html # location = /50x.html { root /usr/share/nginx/html; } location ~ /\.ht { deny all; } location /nginx_status { stub_status on; access_log off; allow 127.0.0.1; deny all; }}
PC版とスマホ版のキャッシュ分かれてない問題
徐々にコンテンツが集まってきて順調にGoogleにインデックスされるようになってきたように見えたのですが、一つ落とし穴がありました。というのは、GoogleはPCサイトのインデックスとスマホ向けのインデックスが分かれているため、PC/スマホで返すHTMLを分けなくてはいけません。最初のバージョンのPrerender Cacheではこのことが考慮されておらず、スマホ版のGooglebotにもPC向けのHTMLを返すようになってしまっていて、危うくスマホの検索結果からインデックスが消えてしまいそうになりました。
よって以下の対応を施しました。
- VarnishでURL + PC/SP のキーでキャッシュするようにUserAgentによるPC/スマホの判定処理を追加(デフォルトではURLがキー)
- PrerenderのPhantomJSがアクセスする際に、スマホ版のGooglebotの場合はviewportの設定を変え、iPhoneをUserAgentに追加
- https://github.com/prerender/prerender/blob/master/lib/server.js#L163のviewportをSP向けに設定する
- https://github.com/prerender/prerender/blob/master/lib/server.js#L171のuserAgentの文字列にiPhoneを入れる(iPhoneはたいていSPブラウザーかどうかの判定に使われている)
これにより無事PC/スマホで別々のHTMLをキャッシュして返せるようになりました。
まとめ
AngularJSのSEO対策は大変です。SEOが必要なサイトをAngularJSで作る場合は、きちんと検証するかPrerender Cacheのようなシステムを作る必要があることを事前に認識しておきましょう:-) もしくはReactを使ってServer Side Renderingがナウいやり方かもしれません。
Cyberagentでの「うるう秒」対策
皆様うるう秒対応お疲れ様でした。
こんにちはこんにちは!!
インフラ基盤サービスグループという名前のアレに所属させてもらって、画像配信基盤とかネイティブアプリ基盤などのインフラの面倒を見させてもらってる、@kakerukaeruと申します。
副業でジャック・ニコル◯ンの偽物をしたり社内chatにAAや画像を貼って場を荒らす和ませる仕事をしております。
はい
というわけで、
ここ最近うるう秒対策チームを私と@nekoruriさんの二人でやっていたのですが、
せっかくだからウチはこういう対策やったよ、ってのを
「you エンジニアブログに書いちゃいなよ」って雑談から始まり、
「me エンジニアブログに書いちゃいなよ(?)」となり、
公式エンジニアブログに初参戦させていただく運びと相成りました。もううるう秒終わってんじゃねーか、うるう秒の前に公開しろや。とは僕も思いますが、突っ込まないで下さい
うるう秒ってなんだっけ
うるう秒に関する情報は有識者たちが既にたくさん書いてくれているので、ここでは割愛します。
うるう秒チームの@nekoruriさんもLinuxのうるう秒おさらい
を書いてくれてるので、ここをみればだいたい分かります(宣伝)
もっともっと詳しく知りたいという人はNTPメモとかを参照して下さい。
メモどころではない知識が詰まっています。
で、どういう対応をしたの?
弊社では大きく分けて、AWSとオンプレミスの2つのインフラ環境が存在します。
うるう秒を乗り切るに当たり、こちらの2環境での対応を進める事になりました。
基本方針
うるう秒は受け入れない。
うるう秒発生時に日本標準時より全てのサーバが1秒未来を生きる形に統一。
その後、ゆっくりと時間を修正させる。
AWS編
AWSを使用しているサービスの方には、ntpdを最新verにupdateし、slew modeで起動させてうるう秒を乗り切っていただくようにしました。
So Simple :)
オンプレ編
オンプレ編で基本方針を実現するために必要なのはイカの2つ
結論
構成的にはこんな感じ。
実際の対策の手順はこんな感じ。
この構成でうるう秒を乗り切る大前提として、各DCに設置されてるサーバが各々の内部NTPサーバをちゃんと参照してる事が必須となる。
NTPサーバの基本的な設定は、OS install時にKickstartで内部のNTPサーバに向くように設定されているが、取りこぼしがないようにScriptを組んで各DCのセグメントに対してブロードキャスト的にntpの参照先を確認して回った。
だいたい16000台ぐらい調べたのだが、なんと全部内部のNTPサーバを参照していた、ほんまかいな。まぁ、数十台ログイン出来ない謎サーバがいたんですが(その後ちゃんと調べました)
もうひとつ
を実現させなければイケないのだが、実現方法はイカ。
基本的にオンプレのサーバたちは数が多すぎるので、設定変更をさせない。
ので、各clientを強制的にslew_modeにさせて放置、はナシ
なので、NTPサーバ側でScriptを仕込んで、ゆっくりと時間を戻す方針を取る。
実際に動かした、Scriptはイカ。
#!/bin/bash
#!/bin/bash
UPSTREAMORIG_NTP_CONF=/etc/ntp.conf.upstream
NO_UPSTREAMORIG_NTP_CONF=/etc/ntp.conf.no_upstream
NTP_CONF=/etc/ntp.conf
# check
if [ ! -e $UPSTREAMORIG_NTP_CONF ]; then
echo "Not found $UPSTREAMORIG_NTP_CONF"
exit 1
fi
if [ ! -e $NO_UPSTREAMORIG_NTP_CONF ]; then
echo "Not found $NO_UPSTREAMORIG_NTP_CONF"
exit 1
fi
export PATH=/bin:/usr/bin:/sbin:/usr/sbin
print_date() {
N=$1
echo $1 "$(date +%Y-%m-%dT%H:%M:%S) "
}
syslog() {
logger -t slew_batch -p user.info "$1"
}
syslog_and_echo() {
logger -t slew_batch -p user.info "$1"
print_date -n
echo "$1"
}
log_ntpdate() {
msg=$(ntpdate -q 210.173.160.27 | head -1)
syslog_and_echo "$msg"
}
# 1ループで15msぐらいずつ合わせる
# 無限ループで手で止める。
n=0
while true; do
n=$(($n + 1))
print_date -n
syslog_and_echo "[loop:$n]"
# 上位NTPサーバに向ける
syslog_and_echo "Connecting to MFEED"
log_ntpdate
sudo cp "$UPSTREAMORIG_NTP_CONF" "$NTP_CONF"
sudo /sbin/service ntpd restart
# 40秒まつ(0.5ms * 40s = 最大20msだけずらす: 実際はiburst同期で実績15msくらい)
wait=40
wait_notify_interval=10
wait_start=$(date +%s)
wait_notify=$wait_start
syslog_and_echo "Waiting $wait secs"
while [ $(($(date +%s) - $wait_start)) -le $wait ]; do
if [ $(($(date +%s) - $wait_notify)) -ge $wait_notify_interval ]; then
# $wait_notify_interval ごとにntpdate表示
# 時間かかるので先にwait_notify更新
wait_notify=$(date +%s)
log_ntpdate
fi
sleep 1
done
# sysclock => hwclock
syslog_and_echo "Sync to hwclock"
log_ntpdate
sudo /sbin/hwclock --systohc
# 上位NTPサーバを外しhwclockのみ見るようにする
syslog_and_echo "Disconnecting from MFEED. Only hwclock"
sudo cp "$NO_UPSTREAMORIG_NTP_CONF" "$NTP_CONF"
sudo /sbin/service ntpd restart
# 1024s(default max poll) + 60s(予備) = 1084s まつ
wait=1084
wait_notify_interval=100
wait_start=$(date +%s)
wait_notify=$wait_start
syslog_and_echo "Waiting $wait secs"
while [ $(($(date +%s) - $wait_start)) -le $wait ]; do
# こちらは待つだけなのでピリオド表示
if [ $(($(date +%s) - $wait_notify)) -ge $wait_notify_interval ]; then
# $wait_notify_interval ごとに経過秒数とntpdateを表示
echo "[$(($(date +%s) - $wait_start))]"
log_ntpdate
wait_notify=$(date +%s)
fi
# 長く待っても大丈夫なので10秒でsleep
sleep 10
echo -n "."
done
echo
done
考え方はイカ
1.2015/07/01 09:00:00 以降に上位外部NTPサーバに戻す。
2.NTPクライアントがSTEPモードにならないよう、閾値の128msより低い40msだけ時間を修正させる。
3.修正されたシステム時刻をハードウェアクロックに保存する。
4.NTPサーバの参照先を上位外部NTPサーバからハードウェアクロックに向ける。
5.1024秒(NTPクライアントの最大ポーリング間隔) + 60秒 待つ。
6.NTPサーバの参照先を上位外部NTPサーバに戻す。
7.2~6を時間が調整されるまで繰り返す
最初にちょっと1loopで戻す時間を頑張りすぎたせいでoffset監視のalertが飛びまくったがごめんなさい、変更時間を修正してからは特に何事も無くゆっくり修正されました。
ばっちり✌(´◉౪◉)✌ィェーィ
まとめ
冒頭にリンクを記載した、@nekoruriさんのブログでも記載されてましたが、
非うるう秒三原則の厳守
- 持たない
- 作らない
- 持ち込ませない
これを徹底した感じです。
全サーバ調べたんだからその時にntpの設定変更しても良かったんじゃない?とか
逆に一気に時間戻っても良くない?とか
うるう秒とかどうでもよくない?とか
色々考えましたが大きな事故が起こらず過ごせて一旦安心です。
うるう秒的小ネタ話
NTP終端サーバでhwclockの提供を開始したらalert大量発生事件
Leap Indicator配布の前日に、上位NTPサーバへの経路を切ってhwclockの提供を開始した途端にntpのalertが大量に飛んできた。
offsetを確認してみてもどうやら遅れてる気配はない。。
調べてみるとどうやらmonからの監視だけがコケているようだ。。。
使われていた監視スクリプトを読んでみると、イカのような記述が。
28 =item B<--maxstratum> Maximum stratum number, default is 10. Stratum
29 16 indicates that ntp is running on a system, but the clock is not
30 synchronized. An alarm will be triggered if this value is exceeded.
どうやら、defaultでStratum 10イカであることの監視が入っている模様。。
hwclockの提供を開始した際に、そのサーバでStratum 10を設定してしまったために、
下位のclientの監視が全てコケてしまったというオチ。
設定変更前のStratumを再設定して、収束。
ちゃんと確認しましょう(土下座
上位NTPサーバの時間がずれるとLOCAL(0)を見て帰ってこない問題
ntp.confとかに、
server 127.127.1.0
を記述している場合、
うるう秒によるSLEWモード調整などで上位NTPサーバの時計がズレる場合、
LOCAL(0)を信用してしまい上位NTPサーバが候補に乗らなくなってしまう問題がある。
ので、一気に時間を調整して時間を元に戻す未来は厳しかったのかもしれない。
http://support.ntp.org/bin/view/Support/UndisciplinedLocalClock
The Undisciplined Local Clock is not a back-up for leaf-node (i.e. client only) ntpd instance.
DCのNTPサーバにCentOS4.6がいた事件
今回を機にリプレイスをして引退してもらいました。合掌。
[kakerukaeru@ntp02 ~]$ cat /etc/redhat-release
CentOS release 4.6 (Final)
[kakerukaeru@ntp02 ~]$ uptime
14:50:38 up 2542 days, 22:02, 1 user, load average: 0.00, 0.00, 0.00
最後に
なんでこんな謎時間の更新なの?と思われるかもしれませんが、
ついさっきまでゆっくりと時間調整が行われるのを優しい目で見守っていたからなのでした。
以上になります。
みなさま、次回もいいうるう秒に巡りあえますように。
参考にさせて頂いたURL
NTPメモ (Internet Archive)
NTP設定 - とあるSIerの憂鬱
The Network Time Protocol (NTP) Distribution
うるう秒挿入後に Leap Second Insertion フラグを削除する - Red Hat Customer Portal
データ解析のシステムからCQRSについて学ぼう
どすこい!
昨年ぶりです!最近よくお相撲さんを見る@sitotkfmです!
またエンジニアブログを書かせて頂くことになりました!
今回のトピックですが、最初は前回同様ストリーミングアルゴリズムでも発表しようかと思ったんですが、まああんまり受けが良くなかったので今回はパスで。。。(同期に「お前Advent Calender書いてたのかよ」と言われました。。。)
それとあの後業務で本当にSparkをつかうことになりまして、まあ、えっと、本当に大変ですね。。。
さて今回のエンジニアブログではCQRSについて説明したいと思います。
CQRSが何故必要なのか
まずドメイン駆動設計(Domain-Driven Degien)という、ビジネスの概念の抽象化であるドメインの要求を最優先として設計を行うソフトウェアの設計思想があります。
ここではドメイン駆動設計について説明しだすと主題から外れかねないのと私がブログの締め切りまで間に合わないのでの細かい説明は省きます。ドメイン駆動設計には様々な概念がありますが、その中にレイヤ化アーキテクチャという概念があります。レイヤ化アーキテクチャとはシステムをユーザインターフェイス層、アプリケーション層、ドメイン層、インフラストラクチャ層に分けてドメインを表すオブジェクトとその他の役割を行うオブジェクトを明確に分けることを目的としています。このレイヤ化システムアーキテクチャというのは様々なシステムで取り入れられているかと思いますが、ここで問題になるのはそのレイヤをどう重ねているかいうことです。
一番シンプルな方法はユーザインターフェイス層、アプリケーション層、ドメイン層、インフラストラクチャ層を一層ずつそのまま重ね合わせる方法だと思いますが、CQRSの文献にこの重ね合わせたアーキテクチャ(文献中ではstereotypical architectureと書かれてますので以後ステレオタイプアーキテクチャとします)の長所と短所を指摘しています。長所はシンプルで直感的な事、短所はドメインを正確に表すのが難しいあるいはドメインの表現力が乏しいということです。これはどういうことでしょうか?見て行きましょう。
ドメインで何らかの変更が起きたとき、ドメインイベントが発生します。
このステレオタイプアーキテクチャではデータストアのアクセスはドメインイベントをBeanのようなgetter/setter付きのオブジェクトに詰め込んでCRUDで行われると考えられます。
例えばブログの記事だと「記事名」、「ライター名」、「文章」等ブログ記事を表す情報を対応するフィールドを持つ「記事オブジェクト」があるとします。記事を作成するときは記事オブジェクトに記事の情報を詰め込んでインフラストラクチャ層に渡し、記事を取得するときはインフラストラクチャ層から取得した記事オブジェクトから必要な情報を取り出せば良いわけです。
このぐらい単純ならさほど問題なくむしろシンプルに見えます。
ではドメインが複雑化した場合どうでしょうか。
多くのブログサービスには下書きという機能があります。書いている途中の記事を公開することなく保存する機能ですね。
これをステレオタイプアーキテクチャで表す場合どうなるでしょうか。
おそらく記事オブジェクトの中にin_draftの様なBool型のフィールドを新たに設けて、下書きかそうじゃないかをこのフィールドで見極めるようになるかと思います。
ドメインイベントは動詞で終わる一文で表す事が出来ます。この下書きの例では「ブログ記事を下書きにする」ですね。対して記事オブジェクトでは「下書き状態の記事」のように名詞で表すことができます。つまりステレオタイプアーキテクチャではドメインイベントを「ブログ記事を下書きにする」を「下書き状態の記事に更新する」と置換する必要があります。
これが先の文献で指摘しているドメインの表現力の乏しさとなります。
もっと詳らかにすると「ブログの記事を下書き状態とするフィールドを真にして記事に更新する」と表すことができます。ドメインがもっと複雑になり状態を表すフィールドが増えると、「Anemic Domain Model」というドメインに関係ないオブジェクトすら扱う必要が生じるアンチパターンに陥ってしまうと先の文献では指摘しています。
CQRSについて
ここでCQRSの登場です。
CQRSは「Command Query Responsibility Segregation」の略です。
ここでいうCommandは「状態を変更するメソッド」でQueryは「状態を変更するメソッド」を表します。つまりWritetとReadですね。
では「Responsibility」は何でしょうか。日本語だと責務と訳される事が多いようです。
実は「Command Query Segregation」という手法も存在します。
CQSはCommandとQueryをメソッドで切り分けるのに対し、CQRSはCQSの拡張でCommandとQueryをメソッドではなくオブジェクトで切り分け、そしてQueryをドメイン層の外に出します。
CQRSではもう一つ大きな特徴があります。それは一貫性を結果整合性(Eventual Consistency)として許容する点です。
これによってドメインイベントを即時にデータストアに反映させるのではなく、非同期でQueryが取得し易いように非正規化したデータ構造に変換することが可能になります。こうしてCommandはQueryのデータ構造を気にする必要がなくなり、先述の記事オブジェクトのようなオブジェクトに変換してデータストアに渡す必要がなくなります。またQueryの参照性能向上も期待できます。
CQRSと共に語られることが多い概念としてEvent Sourcingというものがあります。参考URL
これは端的に言えばアプリケーションの状態をイベントの列を持つ事で管理する手法です。例えば「ブログを公開したが公開した写真が盗用ということでネット上で炎上したので急いでブログを削除した」という香ばしい事案が生じたとします。このときのドメインイベントは「ブログ記事を公開する」「ブログ記事を削除する」の二つです。一般的なデータストアだとブログ記事にあたるエントリを作成し、そのエントリを削除するという処理が行われます(もしくは削除フラグを立てる)。Event Sourcingではブログを作成したエントリの後にブログ記事を削除したというエントリが追加されます。つまりEvent Sourcingではエントリの削除を実際にするわけではなく、削除というイベントを追記する事で削除と見なします。
データの復元するはイベントの列を遡ることで可能になります。(これをローリングスナップショットと呼びます。)
Event Sourcingの利点としてドメインイベントをデータストアに(ほぼ)そのまま渡す事ができるということです。
CQRSとEvent Sourcingを踏まえた上でどのようなアーキテクチャになるか見てみましょう。
Command HandlerはCommandを渡すだけのレイヤーです。
Domain Layerはドメインの処理、ドメインイベントの生成を行うレイヤです。前述の通りここにQueryは含まれません。
Event StorageはEventを保持するデータストアでEvent Handlerは非同期でEvent Storeのデータを非正規化してData Strorageに保存します。データを読み込む場合はQuery HandlerのからQueryつかってData Sotrageのデータを取得します。
ここまで読んでいて私は何か似ているなと思いました。
(゚o゚;) ハッ
データ解析システムってCQRSに似てるじゃん!
…...はい、皆さんが思っても思わなくても話を続けます。
ではデータ解析のシステムをCQRSに無理矢理当てはめてみましょう!
データ解析システムをCQRSに当てはめてみよう
User Interface → 各サービス
ユーザが触るのはサービスですね。
Domain Event → 各サービスのログ
これはまさにそのまんまですね。ログはユーザの振る舞いを表します。
Event Storage → Flume&HDFS
例えばFlumeでは一つのチャネルから複数のSinkを指定する事が可能です。このSinkでデータを流す先を決める事が出来ますが、そのうち一つにHDFS Sinkを使えば分散ファイルシステムであるHDFSにTimestampやHeaderの値に基づいてパスを変えてファイルに追記することができます。ログはドメインの振る舞いを表しそれをファイルに追記して行くHDFS Sinkは乱暴にいうとこれも一種のEvent Sourcingと言えるんじゃないかと。
実際にシステムに落とし込むときにこのEvent Storageをどうするかっていうのが問題になりそうな気がします。
データストアですとその名の通り、Event Storeというデータベースもあります。またDatomicも良さそうかなとは思いますが、これを使えば良いっていうものは出てないように思えます。
それとイベントハンドラに渡す事を考えて、データ解析の例で言うFlumeに相当する部分をPub/Subの様なメッセージングシステムを置いておくことも有り得そうですね。Apache KafkaやRabbitMQ等が思いつきますが、Redisの作者の@antirezさんの開発しているdisqueも気になりますね。(productionですぐ使える事はないでしょうが)
Event Handler →各種解析(Hadoop, Hive, Spark, Onix, etc.)
ここはHadoopやHiveでもBigQueryでもRedShiftでも良いんですが、前回紹介したSpark Streamingとしましょう。
Spark StreamingはFlumeから直接イベントを受け取って解析を行う事が可能で解析結果を非正規化してData Storageに保存します。
ここで何らかの理由でSpark Streamingが止まったりして再解析が必要になったとき、HDFSに保存しているログからSparkを使って復元することも可能です。解析にもよりますがSparkのデータ構造であるRDDのスキーマをストリーム処理とバッチ処理で共通にしておけばこのようにローリングスナップショット的なことも出来るっちゃできます。
Data Storage → HBase
解析データは弊社の場合はHBaseに入れる事が多いです。
ではHBaseで非正規化されたデータ構造をもたせるにはどうすればいいか。
そこらへんについて詳しく書いたHBaseの入門書が弊社のエンジニアが出していた気がしますが、広告のレギュレーションが問題になっている昨今わざわざ渦中につっこんでいく必要も無いという事でここでリンクを貼るのはやめておきます。そもそも宣伝した所で私の懐には一銭も入りません!
あとはData Storageからデータを取得するQuery Handlerのレイヤーを設ければCQRSとほぼ見なせますね。
まとめ
みなさん、いかがでしょうか。
我ながら無理矢理な感じは否めませんが、CQRSの理解の手助けになっていただければ幸いです。
もちろんCQRSは結果整合性を容認する必要があったりコンポーネントが多くシンプルとは言い辛いのでアプリケーションを選ぶアーキテクチャではあるかと思いますが、ドメインが複雑になり得るアプリケーションには有効かなとは思います。あとはEvent Storageの部分をどうソフトウェアに落とし込むのかでブレイクスルーがあれば普及するんじゃないかと。
データを一方通行に流すアーキテクチャという点でFacebookのFluxにも類似しているのかなと思います。
ではみなさん、ごっつぁんでした!
参考文献・URL
CQRS和訳
Event Store
Datomic
Apache Kafka
RabbitMQ
Disque
Amebaのログ解析基盤のワークフロースケジューラー