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

セキュリティグループの活動内容について

$
0
0

この記事は、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

$
0
0
ひとりで悩まず、ご相談ください あなたの人権を守る法務局の取組について紹介します

Spark StreamingでHyperLogLogを実装してみた

$
0
0

この記事は、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」というのはアルゴリズムの名前です。
どういうアルゴリズムか、かいつまんで言えば「多少精度に目をつぶって省メモリでカーディナリティを推定する」アルゴリズムです。推定値であることに気をつけて下さい。

アルゴリズムの流れは次のようになります。元論文はここですね。

  1. m=2^bとなmを決めてm個のカウンターを用意
  2. データをバイナリにハッシュ化
  3. バイナリを二つに分割し、一方でカウンタのindexを、他方でカウンタに追加する値をもとめる。このときの追加する値はバイナリで1が最初に出た位置とする。
  4. Indexに対応するカウンタの値と比較し、現在の値が大きければカウンタを更新
  5. 1~4を繰り返す
  6. カウンタの調和平均をカーディナリティとする


カウントの保持する値から統計的な手法を用いてカーディナリティを推定するのが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

Spark StreamingはDStreamに流れてくるRDDに変換をかけることで処理を行います。
WordCountならlines Dstreamに対して単語にパースするflatMapの変換をかける事でwords Dstreamに変換するといった具合ですね。
Spark Streaming

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のデータを流してスライドしたのを確認して流しています。
ではその時の標準出力になります。

Spark Streamingの標準出力

data2はdata1の値が出てから流したのでdata1のカーディナリティは90649.265869になりますね。
その後、164417.376140が二行連続で出ますがこの値はdata1∪data2の値となります。
そして次の行ではdata1のデータがウィンドウがら落ちているので、90901.397719はdata2のカーディナリティになります。

それぞれの誤差なんですが次のようになります。
データ実際のサイズHLLの値誤差率(%)
data18976390649.2658690.987
data29001790901.3977190.982
data1∪data2161985164417.3761401.501


b=14の時の誤差は約0.8%なのでちょっと上回ってますがまあ大体こんなもんだろという数値ですね。murmurHashのseed値を変えたりデータが変わるとまた違った結果になるかと思います。

まとめ
ここまででHyperLogLogをストリーミング用にSpark Streamingで実装して試してみました。
まあぶっちゃけそんなストリーミングでHyperLogLogを使わざる得ないってかなりのデータが流れてこないと効果が。。。
それでもHyperLogLogは膨大なデータのカーディナリティの算出には有用ですし、Spark StreamingはSparkそのものの勢いも相まって期待のプロダクトであることには間違いないです。
まあ二つを混ぜる必要は無かったですね。。。
あと上記のコードではハッシュのseed値が起動後変わらないので同じデータで同じ結果が出てしまいます。まあそれで問題はないんでしょうが、バッチだと実行時のパラメータはその都度変更出来るけどSpark Streamingのようなウィンドウがスライドするような場合は処理中にパラメータを変更させなければならないので、ここがストリーム処理の難しさだと思います。ウィンドウサイズとスライドの時間を一緒にすれば出来なくはないとは思いますが。

アドベントカレンダーって氷水被ってもらう人を指名するんでしたっけ?違うか。

それでは
ごっつぁんでした!力士7。

NewSQLのCockroachDBについて調べてみた

$
0
0

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は物理デバイスを抽象化する概念です。

Cockroachアーキテクチャ1


各ストアは、複数のレンジを含んでいます。レンジは、Raft合意アルゴリズムによってレプリケーションされます。下の図では、それぞれのStoreを拡大したもので、1つのレンジに付き3つのレプリカが存在しています。

Cockroachアーキテクチャ2

※ 図は、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の動向を追っていく予定なので、今後、機会がありましたら、このブログで報告できたらなと考えています。

参考文献

  1. Spanner: Google's Globally-Distributed Database, http://research.google.com/archive/spanner.html
  2. CockroachDB, https://github.com/cockroachdb/cockroach, http://cockroachdb.org
  3. Raft合意アルゴリズム, https://raftconsensus.github.io/
  4. Snapshot Isolation, http://en.wikipedia.org/wiki/Snapshot_isolation
  5. Serializable Snapshot Isolation(SSI), M. J. Cahill, U. Rohm, and A. D. Fekete. Serializable isolation for snapshot databases. In SIGMOD, pages 729–738, 2008.
  6. RocksDB, http://rocksdb.org/
  7. Protocol Buffers, https://code.google.com/p/protobuf/
  8. VoltDB, http://voltdb.com/
  9. Clustrix, http://www.clustrix.com/
  10. MemSQL, http://www.memsql.com/
  11. FoundationDB, https://foundationdb.com/

桑野さんキーワード3



Rails4へのアップグレードを行ったお話

$
0
0

この記事は、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.shift
conditions が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 update
rails serverを起動(vm上で動作してます)
$ rails s -b 0.0.0.0
上記の作業で移行が完了してとりあえず閲覧出来る程度にはなりました。testを実行するとDEPRECATION WARNINGが大量に出力されていたのといくつかのtestが失敗していたので実際に移行する際はそこら辺の修正やその他の検証が必要になるかと思います。

今年も1年ありがとうございました(プレゼント企画)※ 12/25 15:00応募締切

$
0
0

この記事は 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つに分割されて隠れています。キーワードを探しつつ、ぜひぜひ気になる記事を読んでいただけたらと思います。

ヒント: キーワードはすべて画像の中にあります

しつこいようですが、キーワードの例はこんな感じです。
しつこいようですが。

例


応募は以下の2つの条件を満たすことで完了します。

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年人気記事の発表!

$
0
0

あけましておめでとうございます!!!
エンジニアブログ運営チームです!


もう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つのこと

$
0
0

どうも、アドテクスタジオ所属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のある生活

$
0
0

こんにちは

技術本部でエンジニアというかプログラマをしております、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を使ったオートスケーリングシステム

$
0
0

はじめに

はじめまして。
新卒2年目(そろそろ3年目)エンジニアの向井といいます。
業務ではアメブロのサーバサイドを担当しています。

さて、今回の記事のネタは「flynnというオープンソースを使ってオートスケーリングシステムを構築してみた」というお話です。

flynnの導入から実際にオートスケーリングさせてみた結果までを綴っていきます。

オートスケーリングとは?

そもそもオートスケーリングとはなんでしょうか
オートスケーリングとは、サーバの負荷に応じて自動的にサーバの台数を増減させることです。
AWSではAuto Scalingとして提供されている機能です。

flynnとは?

flynnとはなんなのでしょうか
どうやらアプリケーションのデプロイ、そしてアプリケーションとデータベースのスケーリングを取り扱ってくれるPaaSのようです。
Herokuのオープンソース版のようなものだと思えばいいのかな。
アプリケーションのスケーリングというところに惹かれます。
オートスケーリングに使えそうな気配がしてきました。

flynnを紹介してらっしゃる方や使ってみたという記事を書いてらっしゃる方もいます。
flynnのインストール

基本的には公式サイトを参考にしました。

OSはUbuntu 14.04です。
公式サイトのInstallationに書いてあるスクリプトを実行すると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});

社内環境の問題で外に出られるポートが制限されていて、上記install-flynnを実行した時にkeyserver.ubuntu.comとの通信ができませんでした。
どうやらURLをちょっと変えれば80ポートで通信できるようでした。
インストールが完了するとflynnやflynn-hostといったflynn関連のコマンドが使えるようになります。
インストールが済んだら以下のようにしてflynnを起動させます。
公式のやり方だとうまく動かなかったのでそちらとは若干異なります。

$ sudo flynn init
$ sudo flynn daemon

かなりログが出てきてなにやら始まります。
ここでこのコンソールは放っておいて別のコンソールで作業をつづけます。

$ sudo CLUSTER_DOMAIN=test.localflynn.com flynn-host bootstrap /etc/flynn/bootstrap-manifest.json

上記コマンドでflynnの各サブシステムが稼働し始めます。
コマンドが完了するとflynnのクラスタを登録するためのコマンドが出力されますので、そのままコピペして実行します。

$ sudo flynn cluster add -g test.localflynn.com:2222 -p GUhOzIhavtZoqbJr0zykUvJKyaOYLPvT6wrABF29GwY= default https://controller.test.localflynn.com dd3e7f0a438a69f715a0a660f5d1ebc1

flynn clusterコマンドによって現在登録されているクラスタ一覧が出力されます。
さきほどのコマンドでクラスタをdefaultという名前で登録したのでそのように登録されているはずです。

$ sudo flynn 
cluster NAME URL
default https://controller.test.localflynn.com (default)

次git pushするための鍵を登録します。
flynnではgitreceivedというサブシステムに対してgit pushすることでアプリケーションがデプロイされます。
そのときに使う鍵です。
ちなみに、~/.ssh/id_rsa.pubの内容が登録されますので、事前にssh-keygenで生成しておきましょう。

$ sudo flynn key add
Key ab:fd:f5:2a:13:a1:66:2d:ho:ge:b3:2d:8e:29:9c:63 added.

次にいよいよアプリケーションをデプロイしてみましょう。
アプリケーションはflynnの公式ページで用意されているものを使います。

$ 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

ここまでやればアプリケーションのデプロイが始まるはずです。
さて、稼働したアプリケーションには特定のURLでアクセスすることができます。
今回でいうとexampleという名前のアプリケーションが動いており、
http://example.test.localflynn.com
でアクセスできます。ただしhostsファイルの設定が必要になります。
デプロイしたアプリケーションにアクセスすると、下記のようにコンテナのIDが出力されるページが表示されます。


オートスケーリングシステムの構築

本題です。

オートスケーリングシステムの構成要素

オートスケーリングシステムを構築するにあたって必要なものがいくつかあります。
それらを列挙していきつつ具体的に使ったツールについて説明します。
以下、”役割 / 使ったツール”の形式で書いていきます。

■ サービスディスカバリ / flynn discoverd
起動したアプリケーションの発見

■ Load Balancer / flynn router
アプリケーションが複数稼働している場合、それらにリクエストを分散させるLoad Balancer

■ リソース管理 / なし
flynnを複数台のサーバで動かしてクラスタを組んでいてflynn上のアプリケーションの数を増やしたとき、どのサーバ上でアプリケーションを起動させるかを決定します。
今回は単一ホスト上で動かしているので使用していません。

■ 仮想マシン / libvirt
flynnがアプリケーションやDBを起動するとデフォルトでlibvirtを使って起動します。
サブシステムやアプリケーションと仮想マシンは1対1の関係です。

■ 仮想マシンの生成/廃棄のイベント検知 / Flynn cli, 自作スクリプト
仮想マシンの生成/廃棄時にflynnが特定の文字列を標準出力に出力するため、それを自作スクリプトで検知します。
それからZabbixで負荷監視をしている都合上、仮想マシンが起動したときにZabbixへ登録しています。
逆に仮想マシンが廃棄されたときにZabbixから登録抹消しています。

■ リソース監視 / Zabbix
flynn上で稼働するアプリケーションにかかる負荷を監視し、負荷に応じてスケールアウト/インを行うスクリプトを実行します。

オートスケーリングシステム動作説明

オートスケーリングシステムの全体図です。

図中の番号と対応させながら説明していきます。
① クライアントからのアクセスはflynn routerを通じて各web appにルーティングされます。
②, ③ Zabbixで監視されている仮想マシンの負荷の平均が上昇してくるとZabbixからflynn_scaleout.shが実行され、追加で仮想マシン(web app)が起動します。
④ 具体的には flynn scale というコマンドを使っていて、web appの個数を自由に指定できます。今回はflynn_scaleout.shを起動すると1つweb appを増やします。
⑤ 新たにweb appが起動するとZabbixにもホストの追加します。
⑥ また、flynn discoverdがweb appを発見して自動的にルーティングの対象に加えてくれます。新たにweb appが加わることで全体としての負荷は落ち着く(はずです。)

②, ③, ④ 負荷が下がってくると flynn_scalein.shが実行され web appが1台廃棄されます。
⑤ Zabbixからホスト情報の削除を行います。
⑥ そしてflynn discoverdが自動的に廃棄したweb appをルーティング対象から除外してくれます。

実験

では、実際にオートスケーリングを動かしてみましょう。
実験では1台のマシンのうえに複数台の仮想マシンを起動させました。
flynnの機能的には複数台のマシンでクラスタを組み、マシンをまたがってflynnを動作させることも可能なようです。

実験内容

web appに対してhttpリクエストを投げまくって負荷をコントロールして、負荷に応じてweb appの数が増減するかを確認します。
まず、web app用の仮想マシンを1台立ち上げた状態でZabbixに監視登録したところからスタートです。
Zabbixには, 各仮想マシンのCPU負荷の平均が30%以上になったらflynn_scaleout.shを実行させ、10%以下になったらflynn_scalein.shを実行させる設定をしています。

実験結果

結果は以下のグラフになります。
上のグラフが各仮想マシンのCPUの使用率の平均で、下のグラフが仮想マシン(web app)の台数を表しています。横軸は時間で、上下ともに同じ時間のグラフです。
何度か故意に負荷を上げたり下げたりしています。
手動で負荷を操作した箇所は「負荷追加」と「負荷削減」で、オレンジ色でラベリングしています。
下のグラフの仮想マシンの台数に関してはすべて自動的に増減しています。

オートスケーリングシステムグラフ
グラフをよく見てみると、負荷追加したときにオートスケーリングシステムが反応してapp追加が行われています。app追加によってCPUの平均使用率が減少しています。負荷削減したときには、app廃棄が行われ、CPU平均使用率が上昇しています。

いずれにしても自動的にCPU平均使用率が10%~30%に収まるようにappの追加/廃棄が行われていることが確認できました。

おわりに

過去にApache Mesosというオープンソースを使ってオートスケーリングシステムを作ってみたことがあり、そのときはLoad BalancerにHAProxyを使ったりdocker eventというdockerの機能を使って仮想マシンの生成/廃棄のイベント検知を行ったりといろいろなツールを組み合わせていました。
そのときに比べると必要なツールが少なくて楽でした

参考:http://hogepiyo.hatenablog.jp/entry/2014/07/06/172217

flynnはたまに起動しなくてもう一回手順を最初からやり直すとうまくいくとか不安定なところがあってこれからに期待です。
最初は複数マシン上でflynnを動かそうとしてましたが、途中で単体マシン上でしか動かなくなり諦めました・・。
それにしてもrouterとdiscoverdはよかったです。
自分ですると結構めんどくさい部分だったので自動的によきに計らってくれるというのはかなり楽でした。
アプリケーションの個数をコマンド一発で変えられるのもよい感じでした。なんといっても楽。
欲を言えば負荷計測機能が欲しいところです。今後に期待。

ということでflynnを使ってオートスケーリングシステムをつくってみたお話でした。
楽しんでいただけたら幸いです。


付録

オートスケーリングシステムをつくるにあたっての自作スクリプトのソースコードはこちら
https://github.com/marshi/autoscaling
(以前別の機会で作ったapache Mesos + docker用のスクリプトも入っています)

新米Androiderが開発する上できっと役立つであろう10のサイト

$
0
0

はじめまして、 ogaclejapan です。
昨年の6月にサーバサイドJavaエンジニアからAndroiderへ暗黙な型変換でジョブチェンジしました。会社ではAmeba事業本部でAndroidアプリの開発を担当しています。


いや~、もうすっかり春ですねー
春といえば・さく・・・DroidKaigiーーー!!
Androiderとしてはぜひ参加したいイベントですねー

昨年6月Androiderへ転身してもうすぐ一年になりますがこの一年間を振り返ると、
自作アプリ1つとライブラリを3つ公開とAndroid漬けな1年でした。
春ということで、これから入社する新入社員や新しいことにチャレンジする人も多い時期だと思います。今回は新米Androiderが開発する上できっと役立つであろう10のサイトを紹介します。


最初に目を通しておくとよいサイト2選

これからAndroid開発するにあたって見ておいてほしいサイトを2つご紹介

1. Android Development Training Course Repository

株式会社mixiが公開しているAndroidアプリ開発のトレーニングサイト
https://github.com/mixi-inc/AndroidTraining

Androiderの登竜門的なサイトです。私も昨年お世話になりました。
とりあえずこれ全部目を通しておくだけでもかなり役立ちます。


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を始めたばかりの開発者には若干敷居高いですが、
かなり実戦的なベストプラクティスなので必ず読んでほしいサイトです。


とても参考になったAndroidアプリプロジェクト3選

新米Androiderがいざアプリ開発を始めると、色々な疑問が次々に湧いてきます。

・どういう構成で作ったらいいんだろうか…?? (´・ω・`)
・一般的に使われているネットワークやDBまわりのライブラリは?? (´・ω・`)
・リソースはどんな感じ定義すると管理しやすいのかな?? (´・ω・`)
・Gradleでビルド?どんな感じで書けばイケてる?? (´・ω・`)

ということで、
自分が去年Androidを勉強する上でとても参考なった4つのアプリをご紹介

Google Samples

1つ目はGoogle社が公開する実装Sampleアプリです。
https://github.com/googlesamples

Android Developersでも以前からサンプルはありましたが、昨年GitHubで公開されました。公開されているサンプル数がすごいので何かしら実装の参考になると思います。

Google I/O Android App

2つ目はGoogle I/O 公式アプリです。
https://github.com/google/iosched

やはりGoogle純正アプリということで、お手本的な存在ですね。

・リソース命名規則
・APIバージョンの処理切り分け
・パッケージ構成

などなど困ったときはとりあえず参考に真似してみるのがいいと思います。
毎年Google I/O開催後にコードが公開されるようなので、
今年も使われているライブラリなどは要チェックです。


Rebuild.fm for Android

3つ目は有名なPodcastの非公式Androidクライアントです。
https://github.com/rejasupotaro/Rebuild

こちらは自分がAndroidを始めた当初よく参考にしたアプリです。
コード量がそんなに多くないので読みやすく、画像の取得からネットワーク通信やデータベース参照など、
一般的なアプリでよく使われそうな一通りの機能が実装されているので実用的なコードがたくさんあります。


U+2020

4つ目はAndroid界隈の巨匠JakeWharton氏がベストプラクティスを詰め込んだショーケースアプリです
https://github.com/JakeWharton/u2020

Dependency Injection(DI)といえばサーバーサイドだとSpringが有名ですが、
Androidでは巨匠が作るDaggerが一番有名です。

このアプリはDIを使う構成を前提にした場合のお手本的な存在です。
弊社でも最近DIを採用したプロジェクト構成で作るプロジェクトもちらほら見かけるようになりました。

DIだけじゃなく使われているライブラリも大変参考になるので、
先進的な構成でアプリを作るなら絶対ここは参考にしておくべきです。


イケてるAndroidライブラリが見つかるサイト2選

はい、次はライブラリです。

Gradleが昨年正式にAndroidのビルドツールとして採用されてから外部ライブラリが非常に使いやすくなりました。
使うことが許されない特殊な環境下でない限り、積極的に公開されているOSSライブラリを活用したほうがいいと思います。

ということで、
イケてるライブラリを見つけることができるサイトを2つご紹介。


Awesome Android

1つ目は弊社のAndroidリードエンジニア @wasabeef さん が運営するUI/UX & Coreライブラリまとめサイト

https://github.com/wasabeef/awesome-android-ui
https://github.com/wasabeef/awesome-android-libraries

昨年末すさまじいGitHubスターを叩きだした注目のライブラリサイトです。
UI/UXライブラリに関してはGIFや画像があるのでやりたいことから探す際にはベストだと思います。

私も最近メンテナーとしてお誘いいただいたので、いいもの見つけたら更新していきます^^


Android Arsenal

2つ目は世界的に一番見ている人が多そうなAndroidライブラリ&ツール全般を扱うサイト
https://android-arsenal.com/

ここの新着に掲載されるとGitHubスターがつきやすいので、
新着をチェックしている人はかなり多いのではないかと思います。

自作ライブラリを作った際には、
世界中の人に知ってもらうためにもここには必ず載せてもらいましょう。


Material Designで使えるアイコンサイト2選

はい、次はアイコンです。
そろそろ読み飽きてくるころだと思いますので、これ最後にします^^;

Material Design Icons

1つ目はGoogle社公開するのアイコン集サイト
https://github.com/google/material-design-icons

いやー鉄板ですね。個人で開発するときは重宝しまくりです。
アイコン作る敷居が高くて自作アプリを断念していた方も少なからずいると思いますので、
個人的には日本語に対応したNoto Fontと同じくらい公開でテンション上がりましたw

あとiconファイルの名前とか命名規則はとても参考になりますね。
デザイナーが作ったアイコン素材に名前つけるときにも参考として使えます。


Material Design Community Icons

2つ目はコミュニティーが提供するアイコン集サイト
http://materialdesignicons.com/

公式のアイコンに追加や削除を示すマークがついたものなど、こちらも色々あります。
Googleさんのアイコン集を探してみてお目当のものがなかった場合にこちらも見てみるといいでしょう。


おわりに

簡単なサイト紹介になってしまいましたが、
何かしらこれからAndroid開発を始める人の参考になってくれれば幸いです。

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

それでは春のDroidKaigiでお会いしましょう(…抽選枠組ですが:P)

アキバ系社会人ドクターのすすめ

$
0
0

こんにちは.技術本部・秋葉原ラボのエンジニア,2年目(そろそろ3年目)のKazumiです.

弊社のエンジニアブログでは,技術の話が多いですが,
今回は,社会人ドクターをしている私の社会人ドクタースタートの経緯や,エンジニアと社会人ドクターの二足のわらじ生活,社会人ドクターを始めて気づいたことなどをご紹介したいと思います.


博士課程に興味のある社会人の方や,就職あるいは進学で悩んでいる学生の方の参考になれば幸いです.

自己紹介

私は,現在入社2年目でD1になります(このエントリーが公開される頃には入社3年目でD2??).技術本部・秋葉原ラボでデータ分析・機械学習システムの開発・運用を担当しながら,とある国立大学の博士後期課程で金融工学の研究をしています.

もう少し具体的に説明します.
業務でやっていることは,Java・R・Pythonを使ったデータ分析や機械学習システムの設計・開発・運用です.現在は,トレンド検知システムやスパムフィルタリングシステムに携わっています.

一方,大学院での研究テーマは,新聞記事などのテキストから人々の心理や世の中の雰囲気を表すインデックスを作成し,株価との関係を明らかにすることです.研究の詳細については,論文共同研究者の記事などをご覧ください.  


【研究室風景】


入学までの経緯

きっかけ

博士後期課程へ進学したいと強い気持ちがあったわけではありません.

しかし,入社後も,修士時代に行なっていた研究をほそぼそと続けていました.
そんなとき,共同研究者の方に「博士後期課程に進学してはどうか?」と勧められたのが
博士後期課程進学へのきっかけとなりました.

秋葉原ラボには,Ph.D.の方が数名在籍しており,アカデミアとの連携や知識は必要だと考えていたので,トレーナーとラボ室長(現メディア事業CTO)に,この話を相談しました.幸いにも了承を得ることができたので,博士後期課程入学を決意しました.

準備

まず,志望する大学・研究室を決めなければいけません.仕事をしながら研究を行なうので,平日に大学に通うことは難しいです.そこで,共同研究者の方に紹介頂いた,社会人ドクター受け入れ実績のある教授にメールを送りました.メールでは,具体的な修了要件や研究指導についての確認をして,取り組みたい研究を伝えました.

志望する研究室が決まったので,いよいよ入試勉強を始めます.
試験科目は,英語と面接でした.英語に関して,私は経済学専攻でしたので,Pop Internationalismや,表紙がイケてるhappiness & economicsを読みました.


面接対策では,認められる業績を作ることを意識していました.入学前に論文を執筆していると,面接で有利になりますし,また論文執筆自体も博士後期課程修了条件になっているからです.

面接

修士時代の成績について話がありました.博士後期課程に進学できる成績だったので,成績については,特に問題にはならなかったと考えています.

それよりも,博士後期課程で取り組む研究の計画や目標が話の中心になります.
例えば.研究が既存の研究と比べてどのような新規性があるのか,また,先行研究よりも「良い」結果を出すためのアイディアについてなどでした.

業務と研究

無事に入学することができ,社会人ドクター生活がスタートしました.

平日の深夜や週末に指導教員の研究室などで研究・作業をするというスタイルです.
ここからは,二足のわらじ生活を始めて,良かったことと悪かったことについて書きたいと思います.

PROS

社会人ドクターを始めて少し経った頃からいいことがいくつかありました.  

第一に,業務と研究が有機的なつながりを持ち始めたことです.
たしかに,私の専攻している金融工学は,業務と直接的な関係はありません.業務で,株式市場の予測を行なったり,特定企業の業績の分析をしているわけではないからです.しかし,関連している箇所もあります.研究で行っているシステム設計・データ収集・リアルタイム性・時系列分析・モデル解釈に関する知識と経験は,業務にも役立ちます.また,その逆もあります. 

第二に,プログラミングをする時間が圧倒的に増えました.
平日はもちろん,休日もプログラミングをしたり,コードリーディングをしています.周りの達人の方々のレベルと同じ!とまではいきませんが,少しは成長できたかなと思います.

第三に,大学の先生方・学生さんとのパイプを広げることができたことです.
多くの学会・研究会に参加することになるので,自然と名刺交換ができます.いつか大学の研究者の方々と協力をして,いいサービス・仕組みを作ることができればいいなと妄想しています.

第四に,フィードバックの豊富さです.
会社でやっていることは守秘義務があるので,問題解決のためのアドバイスが社内のみで限定的になりがちです.しかし,研究報告は,会社と比べて制限はほとんどないので,多くのフィードバックを期待できます.ある問題に対する見方・解決策が,会社員と研究者の間で同じであったり異なっていたりすることが,面白く気付きが多いです.

第五に,学割です.
アカデミックオプションがある場合は,積極的に活用するようにしています.同僚から「学割せこい!」と言われたときには,学割を利用するために支払った金額を説明しましょう.

CONS

もちろんいいことだらけではありません.

第一に,休む暇がありません.
業務後の深夜と休日に研究をして疲れが溜まって,業務に支障を来すことはあってはならないことです.自分なりのストレス発散方法が必要です.私の場合,もうくじけそうなときは,水炊きと焼肉です.

 


第二に,お金の問題です.
入学金・授業料は決して安くはありません.約3年通うとすると,入学金と授業料で約200~250万円必要です.いつお金が必要になるかわかりません.もしものために医療保険には若いうちから加入しておく必要があるでしょう.

第三に,休暇申請に関することです.
学会・研究会など,業務と関係のないイベントは有給を取って参加しなければなりません.「もう有給がない!」という状況に陥る可能性が有ります.社会人ドクターをするには,徹底した健康管理が必要かもしれません.弊社シェアハウスに設置されているトレーニング器具で体を動かしたりしています.

まとめ

Be the Worstという言葉があります.これは,チームの中で最低であることを自覚して,地道に努力する必要があるという意味です.私のような若手エンジニア・若手研究者にとって,良き指導者・同僚のいる環境に身を置くことは,会社でも大学でも重要だと感じた社会人ドクター1年目でした.

今後は,博士論文の執筆と研究成果を業務に取り入れることを考えながら,残りの社会人ドクター生活を(周囲に感謝の気持ちを忘れず)駆け抜ける予定です.

参考

社会人ドクターやっておいた方がいいことリスト

  • お金の準備
  • TOEIC / TOEFLの勉強
  • 入学前に論文執筆
  • 共同研究者との連携
  • 謝辞

    社会人ドクターをご理解頂いている職場と進学を勧めて頂いた先輩社員に,この場を借りて感謝致します.

    Tellme for Androidで使ったライブラリやツールを紹介するよ

    $
    0
    0

    こんにちは。エンジニアの清水です。
    昨年の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でのリアルタイム通信の仕組み

    $
    0
    0

    ピグ事業部でサーバーサイドエンジニアをしている有馬です。

    先日、弊社よりスマートフォン向けネイティブアプリとして、 「PiggPARTY」がリリースされました。
    PiggPARTY Androidアプリ
    iOSアプリは近日公開予定です

    pigg-PARTY

    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.on("message", function(chatUserroombody) {
      // エリアに入室中のユーザーを取得 
      var joinedUsers = chatRooms[room];
     
      if (joinedUsers) {
        // メッセージの送信者以外を取得 
        var others = _.values(joinedUsers).filter(function(joined) {
          return joined.userId !== chatUser.userId;
        });
     
        // 送信者以外にメッセージをブロードキャスト(実際にはProxyServerを介して送信される) 
        chatServer.send(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を通して情報を伝播し、プロセスリストの更新を行っています。

    アプリケーション側でプロセスリストの更新イベントを拾い、 ハッシュリングの再計算を行っています。

    (※) https://www.serfdom.io/

    まとめ

    以上、簡単ではありましたが、 PiggPARTYでのリアルタイム通信の仕組みについて紹介いたしました。

    toychatの開発にあたり、アメーバピグやピグライフなど、 既存の大規模リアルタイムサービスの仕組みが大変参考になりました。

    これらを作り上げた先人のエンジニア達に感謝するとともに、 今回の記事が少しでも皆様の参考になれば幸いです。

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

    最速を究める! 2つのサーバ間で特盛りデータを30倍速で転送する方法

    $
    0
    0

    こんにちは. エンジニアの平野です. ふだんはプライベートクラウドのサーバハードウェアとストレージを担当しています.

    サーバのリプレイスや増設, 仮想サーバの移植などでテラバイトクラスのデータを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」にメンターとして参加してみた

    $
    0
    0

     はじめまして、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の実装に取り組み、チューニング前後でどれくらいパフォーマンスが変化するか試してみることにしました。

    tomcat直下


     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を再起動する必要があります。

    httpdconf設定


    ・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

    $
    0
    0

    こんにちは、サーバーサイドのエンジニアをやっている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を実行することができます。ただ、

    1. どこまでちゃんとJavaScriptが実行できるのかわからない(Angular本当にちゃんと動くの?)
    2. レンダリングが途中でストップされ、コンテンツが中途半端にインデックスされないか
    3. Googlebot以外はJavaScriptが実行できるかよくわからない(中国語圏のBaidu, 韓国語圏のNaverなど)
    4. 開発スケジュール的に1.や2.をちゃんと検証している時間がなかった

    が不安要素としてあったため、あえてVarnish + PhantomJS によるキャッシュシステムを用意しました。それがPrerender Cacheと呼んでいるものになります。

    Prerender Cacheのアーキテクチャ

    Varnish+Prerender(Node.JS + PhantomJS) という組み合わせになっています。Prerenderの前段にVarnishがいるのは、PhantomJSがHTML + JavaScriptをレンダリングするのに5秒以上かかるケースがあったため、レンダリング済みのページはキャッシュし、Googlebotに対して高速にレスポンスを返せるようにするためです。実際にはVarnishとPrerenderの前にELBとNginxがいるため、下の図のようにもう少し複雑なアーキテクチャになっています。

    処理の流れは以下のようになります。

    1. https://starbucks.amebaownd.com/にGooglebotからのアクセスが来る
    2. NginxがPrerender CacheにProxyする
    3. Varnishに該当URLのキャッシュがある場合はそれを返す
    4. Varnishにキャッシュがない場合はlocalhost:8081で稼働しているNginxを通してPrerenderにProxyする
    5. PrerenderのPhantomJSがhttps://starbucks.amebaownd.com/にアクセスして、レンダリング結果を返す
    6. 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を返すようになってしまっていて、危うくスマホの検索結果からインデックスが消えてしまいそうになりました。

    よって以下の対応を施しました。

    これにより無事PC/スマホで別々のHTMLをキャッシュして返せるようになりました。

    まとめ

    AngularJSのSEO対策は大変です。SEOが必要なサイトをAngularJSで作る場合は、きちんと検証するかPrerender Cacheのようなシステムを作る必要があることを事前に認識しておきましょう:-) もしくはReactを使ってServer Side Renderingがナウいやり方かもしれません。

    Cyberagentでの「うるう秒」対策

    $
    0
    0

    皆様うるう秒対応お疲れ様でした。

    こんにちはこんにちは!!

    インフラ基盤サービスグループという名前のアレに所属させてもらって、画像配信基盤とかネイティブアプリ基盤などのインフラの面倒を見させてもらってる、@kakerukaeruと申します。
    副業でジャック・ニコル◯ンの偽物をしたり社内chatにAAや画像を貼って場を荒らす和ませる仕事をしております。

    はい

    というわけで、
    ここ最近うるう秒対策チームを私と@nekoruriさんの二人でやっていたのですが、
    せっかくだからウチはこういう対策やったよ、ってのを
    「you エンジニアブログに書いちゃいなよ」って雑談から始まり、
    「me エンジニアブログに書いちゃいなよ(?)」となり、
    公式エンジニアブログに初参戦させていただく運びと相成りました。
    もううるう秒終わってんじゃねーか、うるう秒の前に公開しろや。とは僕も思いますが、突っ込まないで下さい

    うるう秒ってなんだっけ

    うるう秒に関する情報は有識者たちが既にたくさん書いてくれているので、ここでは割愛します。
    うるう秒チームの@nekoruriさんもLinuxのうるう秒おさらい
    を書いてくれてるので、ここをみればだいたい分かります(宣伝)

    もっともっと詳しく知りたいという人はNTPメモとかを参照して下さい。
    メモどころではない知識が詰まっています。

    で、どういう対応をしたの?

    弊社では大きく分けて、AWSとオンプレミスの2つのインフラ環境が存在します。
    うるう秒を乗り切るに当たり、こちらの2環境での対応を進める事になりました。

    基本方針

    うるう秒は受け入れない。
    うるう秒発生時に日本標準時より全てのサーバが1秒未来を生きる形に統一。
    その後、ゆっくりと時間を修正させる。

    AWS編

    AWSを使用しているサービスの方には、ntpdを最新verにupdateし、slew modeで起動させてうるう秒を乗り切っていただくようにしました。


    So Simple :)

    オンプレ編

    オンプレ編で基本方針を実現するために必要なのはイカの2つ

  • Leap Indicatorを受け取らず、うるう秒を挿入させない。

  • うるう秒経過後に大幅な時間の修正がかからないようにゆっくりと時間を巻き戻す。

  • 結論

    構成的にはこんな感じ。



    実際の対策の手順はこんな感じ。


  • 各DCの内部NTPサーバの経路を変更して、一組のNTPサーバへ終端させる
  • 終端先のNTPサーバをうるう秒フラグ挿入1日前からhwclock提供に切り替える
  • うるう秒経過後、1秒をゆっくりもどす(後ほど詳細を説明)
  • 1秒戻った事を確認し各DCの内部NTPサーバの経路を元の上位NTPサーバへ戻す

  • この構成でうるう秒を乗り切る大前提として、各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&lt;--maxstratum&gt; 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について学ぼう

    $
    0
    0

    どすこい!力士7。
    昨年ぶりです!最近よくお相撲さんを見る@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を踏まえた上でどのようなアーキテクチャになるか見てみましょう。

    CQRS概略図

    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のログ解析基盤のワークフロースケジューラー

    $
    0
    0

    技術本部でログ解析基盤を担当している善明です。

    Amebaのログ解析基盤Patriot向けに開発したワークフロースケジューラーをオープンソースとして公開したので、その紹介をさせて頂きたいと思います。

    開発の背景

    Patriotのようなログ解析基盤では、データの取り込み、変換、集計という一連の処理の流れ(ワークフロー)を管理する必要があります。 これがないと、例えば、必要なデータがそろってないのに集計処理が実行され誤った数字がレポーティングされる、といった事態を引き起こしてしまいます。

    PatriotではAmebaの様々なサービスのデータを扱っており、それを処理するためのワークフローは大規模で複雑なものになります。 また、例えば、ブログとピグの両方を使っているユーザ数などの複数のサービスをまたぐ集計も行っているのでサービス毎にワークフローを分割して管理するのは難しく、継続率や累計利用日数など処理によって必要となるデータの範囲も異なるためワークフローを期間(月毎など)で分割するのも適切ではありません。 このためには、一つの巨大で複雑なワークフローを効率的に管理する必要があります。

    現在、様々なワークフロースケジューラが開発されておりオープンソースとして公開されているものも多くあります。 Patriotも初期は他のオープンソースのジョブ管理システムを使っていたのですが、上記の要件が強くなってくるとともに、既存のものでは対応が厳しくなり独自で開発しました。

    アーキテクチャ

    下の図はこのスケジューラのアーキテクチャです。


     ジョブストアと読んでいるジョブ管理データベース(実際はRDBMSを使います)でジョブとその依存関係を管理し、各ワーカープロセスがジョブストアから実行可能なクエリを検索して実行します。

    ジョブと依存関係は以下のようなモデルを想定しています。


    各ジョブ(角丸四角形)をプロダクト(四角形)を参照して生成するものとします。 また、プロダクトは自身を生成するジョブが全て正常終了している場合に利用可能になるものとします。 そして、ジョブは自身が参照するプロダクトが全て利用可能である場合に実行可能になります。

    例えば、上図のprocess1は参照しているプロダクトがdata1のみでそれを生成するジョブがimport1だけなので、import1が終了すれば実行可能になります。 それに対してprocessAllは参照しているプロダクトがall data のみですが、それを生成するジョブが二つあるので両方とも終了しないと実行可能になりません。

    ジョブと依存関係は以下のようなDSLで設定します。 このスケジューラはRubyで実装されており、DSLもRubyの内部DSLになります。

    sh{  require ['product1'] # run after 'product1' is created  name "consumer"  commands "echo 'this is a consumer'"}sh{  produce ['product1'] # this job creates 'product1'  name "producer"  commands "echo 'this is a producer'"}

    上記では二つのechoコマンドを実行するジョブ(consumerとproducer)が定義されています。 参照、生成するプロダクトはそれぞれ、require、produceを用いて指定します。

    インストールと設定

    まず、スケジューラ本体のgemをインストールして、スケジューラのベースディレクトリを初期化します。

    % sudo gem install patriot-workflow-scheduler% patriot-init ${BASE_DIR}

    この時点でサンプルのジョブが動作できる状態になります。 ジョブの実行は以下のようにpatriotコマンドを用いて行います。

    % cat test.pbcsh{  name "test"  commands "echo '#{_date_}' > /tmp/test.out"}% ./bin/patriot execute 2015-04-01 test.pbc% cat /tmp/test.out2015-04-01

    patriotコマンドの引数は、サブコマンド、ジョブの対象日、バッチ設定ファイルになります。 バッチ設定ファイル中の_date_は引数に指定したジョブの対象日で置き換えられます。

    次にジョブをRDBMS(MySQL)で管理するための準備をします。 まず、データベースを作成します。データベース名やユーザ名等はなんでもいいです。 なお、最後でつかっているDDLはここにあります。

    % mysql -u root -p> create database ${PATRIOT_DB};> grant all on ${PATRIOT_DB}.* to ${PATRIOT_USER}@'%' identified by '${PATRIOT_PASSWORD}';( > grant all on ${PATRIOT_DB}.* to ${PATRIOT_USER}@'localhost' identified by '${PATRIOT_PASSWORD}'; # if case of localhost)> exit;% mysql -u ${PATRIOT_USER} -h ${PATRIOT_DBHOST} --password=${PATRIOT_PASSWORD} ${PATRIOT_DB} <  misc/mysql.sql

    つぎににMySQLに接続するためのプラグインをインストールします。

    % cd ${BASE_DIR}% sudo ./bin/patriot plugin install patriot-mysql2-client

    最後に上でつくったDBを参照するための設定をします。${BASE_DIR}/config/patriot.ini を以下のように編集してください。 PATRIOT_DBなどの変数には上記で作成したデータベース名等を設定してください。

    % cat ${BASE_DIR}/config/patriot.ini[common]plugins=patriot-mysql2-clientjobstore.root.class=Patriot::JobStore::RDBJobStorejobstore.root.adapter=mysql2jobstore.root.database=${PATRIOT_DB}jobstore.root.host=${PATRIOT_DBHOST}jobstore.root.username=${PATRIOT_USER}jobstore.root.password=${PATRIOT_PASSWORD}log_factory = Patriot::Util::Logger::Log4rFactorylog_level   = INFOlog_format  = "[%l] %d %C (%h) : %m"log_outputters = stdoutlog_outputter.stdout.class = Log4r::StdoutOutputter[worker]nodes=testnode.test.type=anynode.test.threads=1log_outputters = filelog_outputter.file.class = Log4r::DateFileOutputterlog_outputter.file.dir =  /tmp/log_outputter.file.file = patriot-worker.log

    これでジョブとその依存関係をMySQLで管理できるようになります。 MySQLへのジョブ登録は以下のようにregisterコマンドをつかって行います。

    % cat workflow_test.pbcsh{  require ['product_#{_date_}'] # run after 'product1' is created  name "consumer_#{_date_}"  commands "echo 'this is a consumer'"}sh{  produce ['product_#{_date_}'] # this job creates 'product1'  name "producer_#{_date_}"  commands "echo 'this is a producer'"}% ./bin/patriot register 2015-07-01 workflow_test.pbc

    また、ワーカプロセスの起動は、workerコマンドをつかって行います。

    % sudo ./bin/patriot worker start

    これで、起動したワーカが登録したジョブを実行します。

     % tail -f /tmp/patriot-worker_2015-07-03.log[INFO] 2015-07-03 12:19:23 Patriot::Worker::MultiNodeWorker (main) : get 1 jobs[INFO] 2015-07-03 12:19:23 Patriot::Worker::MultiNodeWorker (worker_test_1) :  executing job: sh_producer_2015-07-01_2015-07-01[INFO] 2015-07-03 12:19:23 Patriot::Command::ShCommand (worker_test_1) : start shell command[INFO] 2015-07-03 12:19:24 Patriot::Command::ShCommand (worker_test_1) : executing echo 'this is a producer': results stored in /tmp/patriot-workflow-scheduler/2015-07-03/jsh_producer_2015-07-01_2015-07-01_20150703_121923[INFO] 2015-07-03 12:19:24 Patriot::Command::ShCommand (worker_test_1) : echo 'this is a producer' is finished[INFO] 2015-07-03 12:19:24 Patriot::Command::ShCommand (worker_test_1) : end shell command[INFO] 2015-07-03 12:24:23 Patriot::Worker::MultiNodeWorker (main) : get 1 jobs[INFO] 2015-07-03 12:24:23 Patriot::Worker::MultiNodeWorker (worker_test_1) :  executing job: sh_consumer_2015-07-01_2015-07-01[INFO] 2015-07-03 12:24:23 Patriot::Command::ShCommand (worker_test_1) : start shell command[INFO] 2015-07-03 12:24:24 Patriot::Command::ShCommand (worker_test_1) : executing echo 'this is a consumer': results stored in /tmp/patriot-workflow-scheduler/2015-07-03/jsh_consumer_2015-07-01_2015-07-01_20150703_122423[INFO] 2015-07-03 12:24:24 Patriot::Command::ShCommand (worker_test_1) : echo 'this is a consumer' is finished[INFO] 2015-07-03 12:24:24 Patriot::Command::ShCommand (worker_test_1) : end shell command

    ワーカプロセスはジョブストアから定期的(デフォルトは5分毎)に実行可能なジョブを取得し実行します。 上のログをみると、最初のサイクルでproducerのジョブを事項し、次のサイクルでproducerジョブの完了により新たに実行可能になったconsumerジョブを実行していることが分かります。

    まとめ

    以上、簡単にAmebaのログ解析基盤Patriotで使っているワークフロースケジューラについて紹介させて頂きました。 Patriotでは、2012年くらいからこのスケジューラでワークフローを管理しており、一日あたり最大で13000超のジョブを管理してきました。(現在は整理、最適化をすすめ一日あたりのジョブ数は7000くらいになっています。)

    また、今回は単純なコマンドを実行するジョブのみ紹介しましたが、ジョブのグループ化やテンプレート化、カスタマイズしたジョブタイプをプラグインとして追加可能など、複雑なジョブを効率的に実装、実行するための機能を組み込んでいます。(http://cyberagent.github.io/patriot-workflow-scheduler/pbc.html) 興味を持って頂けた方は試してみて頂ければと思います。


    Viewing all 161 articles
    Browse latest View live