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

#e100q エンジニア100人に聞きました~ながら聴き~

$
0
0

皆様こんにちは。サイバーエージェントエンジニアブログ運営委員です。
このたび、サイボウズさんの企画「エンジニア100人に聞きました」に参加させていただきました。
(第一回目の時にサイボウズさんはじめ、一斉に記事を上げていたのが羨ましかったのです…)

「エンジニア100人に聞きました」とは、サイボウズさんのブログから引用させていただくと
>これは、毎回、同じアンケートをそれぞれの企業内で行い、結果を「せーの」で同時公開する、というものです。
>あくまでも「お楽しみ企画」なので、統計学的に有意な結果を得ようというわけではなく、ただ、それぞれの企業カラーを反映した「エンジニアの気風」が見えてきたら楽しかろう、というぐらいのつもり。何より、テクノロジーを愛するエンジニア同士、一緒に面白いことをやって盛り上がれれば、それが一番、というスタンスです。
とのことです!

この企画を通して弊社のエンジニアの雰囲気が伝わればと思います。
それではよろしくお願いします。



今回のテーマは「ながら聴き」です。アメーバ事業本部のエンジニア78名に聞きました。
図中の[]は人数です。

では基礎項目からです。
基礎項目は、年齢、性別、初めて使ったコンピュータについて聞いています。

【基礎項目】年齢は?

20、30台が半分くらいずつ在籍中です。

【基礎項目】性別は?

1割が女性エンジニアと言ったところでしょうか
その他がとても気になりますね。

【基礎項目】初めて使ったコンピュータは?

PC互換機が多いですね。
20~30台が中心となるとこんな感じの分布になるのではないでしょうか。
PC9800シリーズ、MSXで入った方も結構多いですね。このあたりの方はおそらく20台ではないと実行委員会は睨んでいます、と思ったら20台の方もいてびっくりしました。
弊社エンジニアの層の厚さというか、濃さというか、何かを感じさせます。

その他のでは様々なマシンをかいていただきました。初めてのマシンはやっぱり印象に残りますよね。
  • X1-turboII
  • Z80マイコン。16進キーボードですが何か
  • PC-8801
  • FM-TOWNS
  • ポケコン
ちなみに運営委員Aの初めてのマシンはPC-9801DAです。
80386の20MHzという、当時ではパワーの有る、、、え、なに?やめて、ちょっと(連れて行かれる)


はい、すいません。
それでは、本題の「ながら聴き」についてです。

Q1. 仕事のとき、ヘッドフォン等で何かを聴きながら作業することがありますか?

ほとんどの方が聴いていますね。

(Q1であると答えた方)Q2:聴く理由は?(最も大きな理由を1つ)

音を遮断するため、気分を高揚させるため、が半分ずつでした。
その他には
  • 気分転換
  • 眠気防止
  • No Music No Life
という意見もありました。
最後の方はミュージシャンって事でいいですね?

(Q1であると答えた方) Q3: どんなものをよく聴いてますか?(ここ最近で、よく聴いてるものを1つ)

やはりほとんどの方は歌や、インストゥルメンタルな音楽を聞いてる人でした。

が、一部実況動画やラジオなど音楽ではない意見や、無音(ノイズキャンセル機能だけオン)という人もいました。
実況動画は頭が色々こんがらがりそうです。

(Q3で 音楽を選んだ方)Q3-1:もしよろしければ、聴いているものを具体的に教えてください。

  • Mr. Children
  • きゃりーぱみゅぱみゅ
  • perfume
  • ボカロ
  • ドラクエBGM
  • ロマンシングサガ・バトルメドレー
  • ロック
  • ジブリオーケストラ
  • HipHop
  • R&B
など音楽ジャンルほぼ全部出てきました!

また、インターネットサービスを利用しているという人も少なくなく、
  • ニコニコ動画
  • ネットラジオ(radiko.jpなど)
という意見もありました。

(Q1であると答えた方)Q4:これを聴くと「はかどる!」、あるいは、ここぞというときに聴くものがあれば、教えてください。

  • マクロスFの劇場版アルバム
  • RPGのボス戦メドレー。しょぼい作業でも世界を救っている気分になれます。
  • クラシック
  • メロコア・スラッシュメタル
  • 美少女ゲームソング
  • モーニング娘。
  • マジLOVE2000%
テンポの良い曲、アニメ、ゲームの曲が目立ちました。

(Q1であると答えた方)Q5:何で聴いていますか?

量販店の売り場を見てると最近はカナル型ヘッドフォンが主流になってきたと思っていましたが、インナーイヤー型を使用する人が多いのですね。

(Q5を答えられた方)Q5-1: 使用機器のこだわりポイントがあれば、ぜひ

メーカー指定系のこだわり
  • オーディオテクニカ "CK70Pro"
  • SONYのスタジオ用ヘッドホン
  • iPhone付属のイヤホン
  • BOSE
  • ビルの1Fのコンビニで売っているPanasonicのもの
機能のこだわり
  • bluetooth 
  • ノイズキャンセル機能
  • 音漏れしないこと
かと思えば
  • 周りの音を遮断しすぎない
という意見も…。

あとは、
  • 話しかけられても聞こえないふりができる感じの見た目
確信犯がここにいました!いましたよ!


(Q1であると答えた方)Q6: 「ながら聴き」によるエピソードがあれば教えてください。

ノっちゃった系
  • 「頭が揺れてる」とよく言われる
  • 気付いたら歌っていた
  • 気付いたら踊っていた
  • 気付いたら終電を逃していた

聞こえなかった系
  • 先輩からの声掛けを無視する
  • ふと横に部長がいて何かしゃべってたけど聞き取れず
失敗系
  • PCにヘッドホンがちゃんと刺さっていなくて、実はフロアに音が響いてた
つっこもうにも、ずうずうしい人なのか天然なのか判断しずらいですね。。

その他
  • プレーヤーを止めるまでヘッドホンを外せない
  • 歌ってしまってるんじゃないのかという不安
エンジニアは繊細なのです…。


(Q1で "ない" と答えた方)Q7:「聴かない」あるいは「聴かなくなった」理由は?

逆にノリすぎて集中できなくなりそうな時は聴かないという選択肢もありですね。

(Q1で "ない" と答えた方)Q8:仕事場で「ながら聴き」をしている人を見て、どう思いますか?

急ぎでない話はスカイプやIRCを使う方がコストが低いので、あまり直接話しかけなくなりました。




今回たくさんのエンジニアから回答が集まり、様々な意見が出ました。
仕事の仕方も人それぞれですね。

いかがでしたでしょうか?
本アンケートは一斉公開なため、まだ他社さんの結果を全く知りません。
他社さんの結果も気になりますね!

AmebaアプリのiOS7対応時に行ったUI実装

$
0
0

AmebaアプリのiOS版を担当している田坂(@tasanobu)です。
先日、iOS7のデザインに最適化したバージョンをストアにリリースしました。
今回はiOS7対応時に行ったUI実装をご紹介させて頂きます。

1. NavigationBarやToolbarがコンテンツに被ってしまう問題

iOS7から、UIViewControllerの全画面レイアウトが採用されました。
また、デフォルトではStatusBarやNavigationBar、Toolbarなどは透過になり、バーの下部の領域までアプリのコンテンツが表示される変更が加えられました。
そのため、UIViewControllerのviewのframeは上下左右に拡大されてしまい、NavigationBarやToolbarがviewの上に被ってしまいます。
   
BeforeAfter


UIViewController.edgesForExtendedLayoutというviewのレイアウト調整用プロパティの値をviewDidLoadで変更することにより解決しています。

// iOS7の場合

if( [UIDevice currentDevice].systemVersion.floatValue >= 7.0f )
{

    self.edgesForExtendedLayout = UIRectEdgeNone;

}


また、以下のようにNavigationBarとToolbarのtranslucentを変更しても同様の動作をするようです。(正しい実装かは怪しいですが。。。)

self.navigationController.toolbar.translucent       = NO;

self.navigationController.navigationBar.translucent = NO;


2. StatusBarがコンテンツに被ってしまう問題

NavigationBarを表示しない場合、"1"の対応を入れてもStatusBarがviewに被ってしまいます。

BeforeAfter

この場合、UIViewController.topLayoutGuideの値を参照し、viewDidLayoutSubviewsでveiwのframeを調整することにより解決しています。


// StatusBarBottomの値をviewOffsetとして使う

CGFloat topOffset = self.topLayoutGuide.length;

// Offset値からViewframeを調整

CGRect rect      = self.view.frame;

rect.size.height = rect.size.height - topOffset;

rect.origin.y    = rect.origin.y    + topOffset;

self.view.frame  = rect;

3. UINavigationBarのtintColor設定

iOS7からUINavigationBarやUIToolbarなどのtintColorの仕様が以下のように変わっています。

iOS7iOS6
tintColorはbarButtonItemの色を指定。
barTintColorでbarの背景色を指定。
tintColorでbarの背景色を指定。

AppDelegate の application:didFinishLaunchingWithOptions: でNavigationBarの背景色設定などをiOS7とそれ以前に分けて行っています。
(UIAppearanceを利用してアプリケーション全体に設定を反映させています。)

// NavigationBarの背景色

UIColor* barBaseColor = [UIColor whiteColor];

    

if( [UIDevice currentDevice].systemVersion.floatValue >= 7.0f )

{

    // NavigationBarの背景色指定

    [[UINavigationBar appearance] setBarTintColor:barBaseColor];

    // BarButtonItemのデフォルト色指定

    [[UINavigationBar appearance] setTintColor:[UIColor greenColor]];

}

else

{

    [[UINavigationBar appearance] setTintColor:barBaseColor];

}


4. UITableViewのSeparator設定

iOS7からのデフォルトでは、Separatorは左端より若干内側から引かれるようになりました。

BeforeAfter

AmebaアプリではSeparatorを左端まで引くデザインを採用しています。
AppDelegate の application:didFinishLaunchingWithOptions: でUITableView.separatorInsetの値を設定しています。
("3"と同様、UIAppearanceを利用してアプリケーション全体に設定を反映させています。)

// iOS7の場合

if( [UIDevice currentDevice].systemVersion.floatValue >= 7.0f )
{

      [UITableView appearance].separatorInset  = UIEdgeInsetsZero;

}


なお、UITableView.separatorInsetの調整はInterface Builderでも可能です。
Interface Builderを利用する場合、以下のように"Separator Insets"のリストボックスを"Default"から"Custom"に切り替え、Leftの値を"0"に変更します。

BeforeAfter


5. UIImagePickerControllerのカメラに電池残量が表示されてしまう問題

UIImagePickerControllerのカメラをモーダル表示すると、StatusBarの表示が残ってしまいます。 Stack Overflowでも同じ問題に苦しんでいる人がいるようでした。 



Info.plistにUIViewControllerBasedStatusBarAppearanceキーを追加し、値にNOを設定することで対応しています。

まとめ

AmebaアプリでiOS7対応時に行ったUI実装をご紹介させて頂きました。
開発環境をXcode5に移行した際、多くのアプリで上記のような対応が必要になってくると思います。ベストプラクティスではないかもしれませんが、これからiOS7対応をされる方の参考になれば幸いです。

参考情報

対応期間中はAppleから公開されている iOS7 UI Transition Guide を何度も読み返しました。
UIKitの各種クラスにおけるiOS7とiOS6の違いなどが網羅的にまとめられており、新UIの考え方や実装方法の理解を深めるのにとても役立ちました。
iOS7対応を始める前に、ご一読することをお勧めします。

PR: 【三井の賃貸】礼金0・フリーレント等、お得物件を特集

$
0
0
“お得な住み替えキャンペーン”を実施している都心の人気賃貸マンションをご紹介!

Amebaのログ解析基盤にCloudera ImpalaとPrestoを導入しました

$
0
0

(この記事は、Hadoop Advent Calender 2013 の12日目の記事です)

こんにちは、Amebaのログ解析基盤Patriotの運用をしている、鈴木(@brfrn169)と柿島です。

Patriotについては以下をご覧ください。
http://ameblo.jp/principia-ca/entry-10635727790.html
http://www.slideshare.net/cyberagent/cloudera-world-tokyo-2013

今回、Amebaのログ解析基盤PatriotにCloudera ImpalaとPrestoを導入しました。

Cloudera ImpalaとPrestoのインストール方法や詳細ついては、下記URLをご覧ください。

Cloudera Impala
http://www.cloudera.com/content/cloudera-content/cloudera-docs/Impala/latest/

Presto
http://prestodb.io/

インストールは、基本的にはドキュメントに従って実施しました。
一箇所だけハマったのが、PrestoでのHDFSのHAの設定方法です。

etc/catalog/hive.propertiesに以下を追加する必要があります。

hive.config.resources=/etc/hadoop/conf/core-site.xml,/etc/hadoop/conf/hdfs-site.xml

上記対応は、以下のURLを参考にしました。
https://github.com/facebook/presto/issues/845

なぜ、似たような用途、アーキテクチャのCloudera ImpalaとPrestoの両方を本番に導入したのかと思われるかもしれないのですが、いくつか理由があります。

(1)両者ともに成長中のミドルウェアであるため、私たちの利用する観点で機能を比較したとき、それぞれにメリット、デメリットがありました。
(2)検証環境でも試しているのですが、低スペックなマシン、少ないデータでの検証結果だけで、どちらか一方の本番導入を決めるよりも、本番環境のスペック、大量データで実際に両者を使用して比較をしたほうがよいと考えました。
(3)アナリストの方に触ってもらい意見を聞きながらよりよいほうを選択していきたいと思っています。

(1)機能について

基礎的な項目+α(私たちが重要と考えた項目)について比較していきます。
(ソースはまだ読めてないので、基本ドキュメントベースとドキュメントで見つからないものは実際に動かしての比較になります)

共通項目
・SQLに似た言語を使用可能
・MapReduceを用いないので、低レイテンシで対話的にデータの探索的な分析ができる
・両方インメモリで処理を行うので、割当てメモリの制限を受ける

Cloudera Impala Presto
開発言語 C++ Java
ライセンス Apache  License Apache License v2
接続インターフェース
  • impala-shell
  • Hue
  • JDBC
  • ODBC
  • Presto  CLI
  • jdbc(ドキュメントには見当たりませんでしたが、gitにはありそうです)
  • 対応データ型
  • boolean
  • tinyint
  • smallint
  • int
  • bigint
  • float
  • double
  • timestamp
  • string

    ※maps, arrays, structsなどは対応していない
    (SELECTもエラーになる)

  • データ型の詳細について、表の下で別途まとめる
    対応しているステートメント
  • SELECT
  • INSERT
  • CREATE
  • ALTER
  • DROP
  • EXPLAIN
  • など
  • SELECT
  • EXPLAIN
  • など

    ※ CREATEやINSERTなどは未対応
    ビルトイン関数 こちら こちら ※Cloudera Impalaの対応していないWindow FunctionやJSON Functionに対応している
    UDFサポート サポート(1.2.0以上) 未サポート
    Hiveでデータ更新後
    のメタ情報の更新
    メタ情報のキャッシュをするため、メタ情報を更新するためには、REFRESH(既存テーブル)、
    INVALIDATE METADATA(新規テーブル)の実行が必要

    ※1.2.1のCatalogdの導入からCloudera Impala経由での更新については自動更新
    ドキュメントには書いてないが、少しの時間差を伴って自動更新されるように見える
    耐障害性 SPOFなし
    ※impala-serverがダウンした場合はクエリが失敗する、statestoreが死んだ場合はホスト情報の更新ができなくなる
    ドキュメントにはのってないように見えるので、coordinator兼discovery-serviceを落としてみたところ、クエリの処理が停止、新しく接続するcoordinator兼discovery-serviceはいないので、SPOFに見える。
    ※ それぞれが複数立てれるかは未検証

    Prestoのデータ型変換について

    Hive側で以下のテーブルを作成しました。

    create table test (
    a TINYINT,
    b SMALLINT,
    c INT,
    d BIGINT,
    e FLOAT,
    f DOUBLE,
    g DECIMAL,
    h TIMESTAMP,
    i STRING,
    j BOOLEAN,
    k ARRAY<INT>,
    l MAP<STRING, INT>,
    m STRUCT<col1 : INT, col2: STRING>
    );

    Presto側では、このテーブルの各カラムの型は以下のように見えます。

    presto:default> desc test;
     Column |  Type   | Null | Partition Key
    --------+---------+------+---------------
     a      | bigint  | true | false
     b      | bigint  | true | false
     c      | bigint  | true | false
     d      | bigint  | true | false
     e      | double  | true | false
     f      | double  | true | false
     h      | bigint  | true | false
     i      | varchar | true | false
     j      | boolean | true | false
     k      | varchar | true | false
     l      | varchar | true | false
     m      | varchar | true | false


    今回試した数種類のカラムは、Prestoではboolean, bigint, double, varchar の4つのいずれかの型として扱われました。

    両者のARRAY型、MAP型、STRUCT型の扱いについて

    Hive上でデータをINSERTした後、Cloudera Impala, PrestoそれぞれでこのテーブルをSELECTすると以下のようになります。

    Cloudera Impalaの場合(エラーになる)

    > select * from test;
    Query: select * from test
    ERROR: AnalysisException: Failed to load metadata for table: default.test
    CAUSED BY: TableLoadingException: Failed to load metadata for table 'test' due to unsupported column type 'array<int>' in column 'k'

    ARRAY型, MAP型, STRUCT型を含んでいるとSELECTもできません。


    Prestoの場合
    (SELECTできる)

    presto:default> select * from test;
     a | b | c | d |  e  |  f  | h |  i   |  j   |    k    |          l          |           m
    ---+---+---+---+-----+-----+---+------+------+---------+---------------------+-----------------------
     1 | 1 | 1 | 1 | 1.0 | 1.0 | 0 | true | true | [1,2,3] | {"b":2,"a":1,"c":3} | {"col1":1,"col2":"a"}
    (1 row)

    Prestoでは、ARRAY, MAP, STRUCT型の3つもvarchar型として扱われ、Impalaと違いエラーにはなりません。

    (補足)varcharは中の値に対しても関数を使うことで、個々の値にアクセスが可能となっています。
    presto:default> select json_extract_scalar(k,'$[0]'), json_extract_scalar(l,'$.b'), json_extract_scalar(m,'$.col1') from test;
     _col0 | _col1 | _col2
    -------+-------+-------
     1     | 2     | 1
     

    私たちの環境にはARRAY型, MAP型のカラムを持つテーブルが多く存在するので、型の情報は変わってしまってもSELECTできるPrestoは魅力的です。
    ※Cloudera Impalaでもバージョン2.0では、ARRAY型,MAP型のサポート予定があるそうです

    (2)環境について

    導入した環境ではCDH、Cloudera Impala, Prestoの各プロセスは下図のようにマスター系3台とワーカー系28台にわかれて起動しています。

    インストールは、chefでできるようにしています。今回は、Cloudera ImpalaのインストールにはCloudera Managerを使いませんでしたが、Cloudera Managerを使ったほうが簡単だと思います。

    バージョンは以下の通りです。

    ・CDH 4.3.0
    ・Cloudera Impala 1.1.1
    ・Presto 0.54

    検証環境では、CDH4.5.0やCloudera Impala1.2.1も検証中ですが、Hiveのバグを踏んだり、impala-catalogdの起動後、hive-metastoreが死んだりと不安定になる現象が発生したので、本番は上のバージョンで動かしています。

    DataNodeを起動しているサーバのスペックは
    ・CPU Intel(R) Xeon(R) CPU E5-2650L 0 @ 1.80GHz * 2
    ・ディスク SATA 3TB × 12本(JBOD), SAS300GB * 2 (RAID1)
    ・メモリ 96GB( メモリの内訳は DataNode 4GB, TaskTracker 1.5GB, RegionServer 8GB, Cloudera Impala 5GB, Presto 5GB, その他 MapReduceのスロット用、OS用となっています)

    また、Cloudera Impala, Prestoを両方稼働するためにそれぞれに割り当て可能なメモリ量が5GBと少なくなっています。

    実際にクエリを流してみた結果

    上記環境で、いくつかのパータンの結果を一例として紹介させていただきます。
    ※ 本番クラスタなので他(Flumeや他のMapReduce)からの影響を受けている可能性があるので、それぞれ3回実施した平均値を記載しています。

    処理内容 Cloudera Impala Presto Hive
    約1GB 約40,000,000行 SELECT COUNT 5 95
    約1GB 約40,000,000行 GROUP BY 13 7 101
    約1GB 約40,000,000行 ORDER BY LIMIT 100 16 114 213
    約1GB 約40,000,000行 JOIN 約800MB 約40,000,000行 GROUP BY 44 166 385
    結果の単位はsec

    雑感
    ・ふれこみ通り、Hiveに比べれば両者とも数倍から10数倍程度の実行時間の差がありました。
    ・私たちが試したケースでは、複雑なクエリではCloudera Impalaのほうが速く処理を終えることが多かったです。
    ・両者を比較するために試すクエリが弊社でよく使っているARRAY型, MAP型を含まないものに限られてしまったことは残念に感じました。

    (3) アナリストに実際に使ってもらった感想
    Presto CLI は結果画面がlessに渡してくれるのが便利
    ・Presto CLIの実行画面はCloudera Impalaに比べて進捗状況がわかりやすい
    ・Prestoは単純なクエリの場合だと爆速感がありますが、group by とか関数使うとそんなに速くない印象を受けました。
    ・(Hiveに比べて)両者とも、headっぽい(SELECT LIMIT 10のような)使い方が爆速にできるのは非常にありがたくて、分析の思考を止めること無く素早くクエリを作り上げることができそうです。

    まとめ
    今回、ログ解析基盤 PatriotにCloudera ImpalaとPrestoを導入しました。
    現在のところ、PrestoもCloudera Impalaも一長一短なので、しばらくは併用しながら両者の動向を追っていきたいと思っています。
    弊社ではARRAY型やMAP型を多用しているので Impalaの対応が待ち遠しいです。
    実際に使っていく中でまた何か知見が得られましたら、また報告させていただきたいと思います。

    Node.js Cluster+Socket.IO+Redisによるリアルタイム通知システム

    $
    0
    0

    アメーバピグの開発エンジニアをしている古谷です。

    アメーバピグはもうじき5周年を迎えます。
    ユーザ数はリリース当初よりはるかに多くなり、それに伴いシステム規模も大きくなってきているピグですが、
    Java, MySQL, Node.js, MongoDB, Redis, ランキングシステム, 外部連携API、認証... リアルタイムなサービスからスマホ向けアプリまで...
    と、開発に携わってみるとWeb関連の様々な事が学べます。

    最近、アメーバピグ関連サービス(アメーバピグ、ピグライフ ...etc)にて、サービス間でユーザの行動をリアルタイム通知するためのシステムを開発する機会がありました。
    今回は、そのシステムの一部である、Node.js Cluster + Socket.IO + Redisを利用したフロントサーバについてかきます。

    現在、アメーバピグ関連サービス全体だと約200,000人のユーザが同時にプレイしてくれています。
    これらのユーザに対してリアルタイム通知を送るため、全ユーザとのコネクションを維持し、通知データをユーザに届けるというのがフロントサーバの役割です。
    ちなみに、アメーバピグ関連サービスはそれぞれクライアント⇔サーバ間の通信プロトコルや、クライアントのデバイスが異なるため、各サービスのシステムを利用して相互にリアルタイム通知を送るのは困難でした。
    そこで、みなさんもご存知のSocket.IOを利用したリアルタイム通知専用のシステムを作ることになったわけです。



    サーバについて
    フロントサーバは、Node.jsで実装し、Node.jsのCluster機能を利用しました。
    サーバ上にはマスタープロセスとワーカープロセスが起動し、ワーカープロセスではSocket.IOが稼動します。
    マルチプロセス上でSocket.IOを利用するために、Socket.IOのRedisStoreという機能を利用しています。

    現在、この通知システムはピグの裏側で稼動していますが、リリースに至るまでにはいくつかの壁がありました。その中から、参考になりそうなものをピックアップして紹介したいと思います。

    Node.js Cluster
     【クライアント接続が各プロセスに均等に分散されない。】
    開発で利用したバージョン(0.10.21)のNode.jsの場合、Cluster構成はリスニングソケット共有型であるため、各ワーカプロセスへの分散はカーネルによって行われます。そのため、プロセスごとに接続が偏る傾向があります。
    コネクションを維持するようなシステムの場合、特定のプロセスに接続が偏るため負荷によるプロセスダウンの可能性があります。


    回避策として、Cluster構成を接続済みソケット共有型し、ラウンドロビン方式で分散するよう修正して利用しています。
    修正自体は、次期バージョンにて組み込まれる予定の内容をcherry-pickしています。

    Socket.IO
    開発で利用したバージョン(0.9.16)のSocket.IOでRedisStoreの機能を利用する場合に、以下の問題点があります。※MemoryStore利用時は問題ないです。
      
    【ハンドシェイクデータのpub/subが遅延した場合、接続率が低下する。】
    RedisStore利用時は、クライアントの接続情報はRedisのpub/subによって他プロセスに共有されます。
    Socket.IOの場合、クライアントはハンドシェイク → 接続という2段階の接続方式をとっており、以下のようなフローで接続確立されます。
      1.ハンドシェイク
      2.pub/subにより、ハンドシェイクデータが各プロセスに共有される
      3.接続
      4.接続確立


    ただし、高負荷によりRedisのpub/subが遅延している場合は以下のような状態になり、未ハンドシェイクと判断されクライアントが切断されるケースがあります。
    このように、pub/subの遅延が接続率の低下に繋がるわけです。
      1.ハンドシェイク
      2.接続
      3.未ハンドシェイクとして切断(クライアントに「client not handshaken error」エラーが返されます。)
      4.pub/subにより、ハンドシェイクデータが各プロセスに共有される


    また、仮にエラー時にクライアントを再接続するようにしていた場合、クライアントの再接続によりハンドシェイクの数が増えるためpub/subの遅延を悪化させます。
    さらに、ハンドシェイクデータのpub/subは、Socket.IOを起動している全プロセスへ通信が発生するため、接続率の低下だけではなく各プロセスの負荷高騰の原因にもなる可能性があります。

    回避策として、接続時にハンドシェイクデータが共有されていない場合、一定期間ハンドシェイクの共有を待つよう修正して利用しています。

    【クライアントの接続/切断が大量に行われるとメモリリークする。】
    リリース後、数日間で各プロセスのメモリ使用量が上昇していく現象がありました。
    原因は、Socket.IOモジュール内で、クライアント切断時に行われるべきunsubscribe処理の漏れでした。
    修正は、本家のSocket.IOにコミットしてあります。 この修正前のバージョンをご利用の方は、以下のコミットが反映されているバージョンのご利用をお勧めします。
    まとめ
    今回Node.js、Socket.IO、Redisを利用したシステムは、通知データをユーザへ届けるためのハブのようなシステムであったためIOバウンドな処理が多く、組み合わせとしては相性が良かったように思います。
    ただし、利用するためには今回書かせていただいた内容以外にもいくつか工夫が必要でした。今後、機会があればブログに書きたいと思います。

    PR: NTT Comの月々980円のスマホパケットコース

    2014年始の挨拶と2013年人気記事の発表!

    $
    0
    0

    あけましておめでとうございます。

    あけましておめでとうございます。
    仕事柄まだ去年からの障害を引きずっていて年明けどころじゃないよ!と言う方もいらっしゃるかもしれませんが・・・。
    新年ということで心機一転システム改善/新規開発、頑張って行きましょう!

    昨年は滞っていた更新を定期更新ペースに戻すことが出来たのではないかと思っているサイバーエージェント公式エンジニアブログです。


    2013年エンジニアブログランキング

    それでは、早速ですが2013年の総決算として2013年に公開された記事のアクセス数ランキングを公開いたします!
    この年始のお時間あるときに読んでいただけましたらと思います。

    10位:ピグ麻雀のアルゴリズム
    ピグのミニゲームとして提供している麻雀ゲームのアルゴリズムについての記事です。
    麻雀やったこと無いエンジニアの人たちが麻雀ゲームを作るための苦悩も綴られていますw

    9位:redisをsentinelとAliasIPを利用して冗長化
    優しいマサカリを受けた記事ですね。
    ありがたいことですm(_ _;)m

    8位:弊社の最近のDevOpsへの取り組み
    監視の生成等を自動で行うためのアプリケーションの作成などについて語っています。
    弊社でのAWS事例としても。

    7位:第6回テックヒルズでアメーバピグにおけるJenkinsの活用例を発表しました。
    六本木ヒルズで行われた、CROOZさん主催のテックヒルズにおけるJenkinsの発表について。みんなでHadoopTシャツ着ていった所視線が熱かったですw

    6位:社内勉強会制度「Skill U Friday」
    社内勉強会をまとめている織田さんのエントリです。
    織田さんはSUFだけではなく他の社内外の勉強会にも精力的に進めています。

    5位:cassandra運用監視小ネタ集
    Cassandra芸人と言われるOranieさんの運用監視小ネタ集です。(弊社NoSQL芸人を多数抱えております)
    ただのTIPS集ではない血反吐にまみれた記事をお読みください。

    4位:出、出、出~ameba画像配信奴~!!
    アメブロの画像配信の現在とこれからについての話です。
    タイトルとは違って内容はきちんと書かれていますw

    3位:lombokで快適Java生活
    getter/setterメソッドなどのジェネレイトツールであるlombokの紹介です。
    Scalaを使おう!

    2位:WebSocketで監視もリアルタイムに
    WebSocketを使った監視のリアルタイム更新についてまとめたエントリになります。
    ゲーム系などのリアルタイムが重要に成るサービスなどにはこういう監視方法が主流に成るかもしれません。

    1位:キラキラ女子を支える技術
    弊社故にといいますか、この記事が1位になってしまいました・・・ですが、中身は真面目な社内のエンジニア環境の紹介エントリになっていますw


    2014年もよろしくおねがいします

    今年も、技術レポート、企画などを通じた会社の文化やエンジニア環境の紹介、イベントレポート、頂いたフィードバックの反映等々をみなさまにお届けしたいと思っております。
    本年もサイバーエージェント公式エンジニアブログをよろしくお願いいたします。

    WebPの画質とファイルサイズを評価する

    $
    0
    0
    こんにちは。AA職人のsaeoshiらとともに画像配信/変換システムの運用を行っております、古田と申します。
    二度目の執筆になりますがよろしくお願いします。

    amebaは大量の画像をそのまま配信するだけでなく、ダイナミックに加工を行ったり画質を変えたりといった機能を備えたプロキシサーバも保有しており、そのアプリケーションの開発および運用を現在担っているのが私の属するチームになります。

    「どうすれば綺麗な画像を高速に作れるか」
    「どうすれば綺麗なまま画像を圧縮できるか」
    「どうすれば画像の補完を高速に行えるか」
    「最適な変換パラメータは何か」
    「今度の食事支援は何か」

    を考えながら日々コードを書く簡単なお仕事をしています。

    本トピックではそのような画像変換システムと切っても切り離せない関係にある、画質に関する話題を書こうと思います。


    ■ はじめに

    私は、次の3つのKPIすべてで高得点をマークするのが良い画像変換システムであると考えています。

    1. 変換速度
    2. 圧縮率
    3. 画質

    このうち1と2は時間やファイルサイズなど比較的簡単に計測可能な情報から導くことが可能ですが、3を簡単に導き出せる評価方法は意外と知られていないようで、「一番良いので頼む」と思考停止に陥っている場合があったりなかったりするようです。

    画質を適切に最適化しないということは、いくら速度と圧縮率を頑張ってもトータルで良い画像変換システムにはなりません。そのような、画質を簡単に導き出せる評価方法について今回は紹介したいと思います。


    ■ 主観評価 MOS

    たくさんの人たちにアンケートを取ることをかっこ良くいったもので、mean opinion score(平均オピニオン評点)の略です。この評価手法は最も正確かつある意味簡単です。

    なぜこれが最も正確なのかと申しますと、、、この手法は、実装であるとともに定義そのものでもあるからです。画質って「ある画像が何らかの基準画像と比較して、劣化しているという印象を受ける人の多さ」のことですから、それを直接測っているMOSは最も正確な画質指標であるといえます。

    この手法、メリットは正確さですがデメリットはコストの高さです。僕のようなボッチのエンジニアの場合は普段あまり声をかけたことがない人に協力を依頼しなければならなかったり、そもそも偏りのない集団を用意するのが難しいなど、精度の高い測定をするには結構コストがかかります。こういう時、一声かけるだけで労せず何十人も人を集めることができる、弊社のキラキラエンジニア達が少しうらやましく思いますw

    ■ 客観評価 MSE/PSNR/SSIM

    客観的な情報から演算的に画質を推定しようという試みがあります。これはMOSのような主観評価に対し、客観評価と呼ばれます。

    その一つがMSE(mean squared error, 平均二乗誤差)です。これは次の式によって定義される、2つの画像I,Kのピクセル間の差の二乗の平均をとったものです。

    MSE


    これをもう少し人間の感覚に近い評価量として扱うためにデジベルに変換したのがPSNR(Peak signal-to-noise ratio, ピーク信号対雑音比)で、次の式で定義されます。

    PSNR


    ただ、これらの評価値は2つのフレームバッファの局所的かつ単純なピクセル差分だけで定義されているので、一部の大きな差分と大域的な小差分を区別できません。人間の視覚は局所的な信号の変化に敏感に反応する性質を持っているため、これらは人間の主観的との相関はあまり良くないことが知られています。

    そこで、局所的なピクセル差分だけではなく、輝度平均の差、画素値の標準偏差の差、画素間の共分散という3つの評価軸の積によって画質を評価する手法があります。それはSSIM(Structural similarity, 構造的類似性)という名前の手法で、次の式で定義されています。

    SSIM


    これは開発者のZhou WangらによるMOSとの比較で、主観評価と良い相関があることが示されています[1]。最大値は1.0, 最低値は0.0 と、使いやすい値域となっているのもSSIMの良い所です。一般的に、SSIM の値が0.95 以上あればオリジナルと同等の品質を備えていると言われています。

    MSE, PSNR, SSIMとも、フレームバッファを比較するだけで良いのでプログラム化するのが容易です。

    上記の論文などを参考に、みなさんも実装してみると良いと思います。




    ■ WebPの画質とファイルサイズを測ってみよう!

    前置きが長くなりましたがここからが本題です。

    WebPという画像形式があります。Googleが開発した画像形式で、非可逆圧縮モードなら(同一画像、同等画質の)JPEGと比較して25-34%ほど小さいとGoogleはアピールしています。

    画像配信担当者として、これが本当なのか前からちょっと気になっていたので、SSIMで画質を比較しながら調べてみようと思います。



    0. 元画像を用意

    本トピックでは元画像として次の弊社サービス画像を使うこととします。ファイル名をgf.pngとしてローカルに保存しておきます。



    1. WebPに変換

    PNG -> WebPへの変換にはGoogle謹製の cwebp [2]を使います。PNGを入力画像とする際はlibpngの開発環境が必要なので、libpng-devel をインストールする必要があります。cweb のビルド方法はお馴染みの confugure & make ですので省略します。


    $ sudo yum install gcc-c++
    $ sudo yum install libpng-devel
    $ ./configure
    $ make
    $ sudo make install

    cwebでは次のようなパラメータで変換を実施します。これは品質 90 のWebPファイルを生成するという意味になります。q の取る値は 0以上100位下です。90という指定に大きな意味はなく、ここでは品質はどんな値でも結構です。他にも多数のオプションがあありますので、[3] をご確認ください。

    $ cwebp -q 90 gf.png -o gf/90.webp


    2. 変換後のWebPの画質をSSIMで計測

    ここではOpenCVを併用してSSIMの算出を行ってみようと思います。まず、素のOpenCVはWebPの読み込みに対応してませんので、[4]で入手できるWebPパッチを当てることとします。また、OpenCVのビルドにはcmakeが必要なので予めインストールしておきます。

    $ sudo yum install cmake
    $ wget http://sourceforge.net/projects/opencvlibrary/files/opencv-unix/2.4.2/OpenCV-2.4.2.tar.bz2
    $ tar xvzf OpenCV-2.4.2.tar.bz2
    $ http://www.atinfinity.info/opencv/extension/opencv2.4.2_webp_enable_patch_20120809.zip
    $ unzip opencv2.4.2_webp_enable_patch_20120809.zip
    $ cp -av opencv2.4.2_webp_enable_patch_20120809/* OpenCV-2.4.2/
    $ cd OpenCV-2.4.2
    $ cmake .
    $ make
    $ sudo make install

    実はOpenCVのGPGPUのサンプルコード中にはSSIM算出ロジックを含むコードがすでに存在している[5]ので、
    これを手直しして使ってみます。簡略化のため(笑)、手直しした後のソースコードをGitHubにアップロードしておきます[6]。

    このソースは以下のようにビルド・実行ができます。

    $ g++ -o ssim ssim.cpp -I/usr/local/include/opencv/ -lopencv_legacy
    $ ./ssim gf.png gf/90.webp
    SSIM R:0.958753 G:0.982877 B:0.940175 AVG:0.960602

    引数に2つの画像を指定すると、RGBそれぞれのSSIMの値と、その平均値が出力されます。この場合、q=90 のWebP画像の場合、平均値は約0.961であり、また、ファイルサイズは116KBでした

    もしlibopencv_legacy.so が見つからない、というエラーが出て実行できない場合は、次のように環境変数を設定してみてください。

    $ export LD_LIBRARY_PATH=/usr/local/lib


    3. 様々な圧縮率のJPEGを作成して比較

    ImageMagickを使い、以下のようにPNG -> JPEG画像を作ります。

    $ convert gf.png -type Optimize -quality 90 gf/90.jpg

    めんどくさいのでここでは以下のワンライナーを使い、quality の値を80から100まで1刻みで変更しながらJPEGの生成を行ってしまいます。

    $ perl -e 'foreach $i(80..100){system("convert gf.png -type Optimize -quality $i gf/$i.jpg"); $ssim=`./ssimx gf.png gf/$i.jpg`;$s=`du -h gf/$i.jpg`;chomp ($ssim,$s); print "q=$i\t$s\t$ssim\n";}'
    q=80    100K    gf/80.jpg   SSIM    R:0.922086  G:0.955917  B:0.881467  AVG:0.919823
    q=81    104K    gf/81.jpg   SSIM    R:0.924317  G:0.957926  B:0.884391  AVG:0.922212
    q=82    108K    gf/82.jpg   SSIM    R:0.926317  G:0.95937   B:0.887052  AVG:0.924246
    q=83    112K    gf/83.jpg   SSIM    R:0.928507  G:0.96135   B:0.890086  AVG:0.926648
    q=84    112K    gf/84.jpg   SSIM    R:0.930452  G:0.962964  B:0.893071  AVG:0.928829
    q=85    116K    gf/85.jpg   SSIM    R:0.932549  G:0.964608  B:0.895942  AVG:0.931033
    q=86    120K    gf/86.jpg   SSIM    R:0.935057  G:0.96683   B:0.899367  AVG:0.933751
    q=87    124K    gf/87.jpg   SSIM    R:0.937562  G:0.968753  B:0.903127  AVG:0.93648
    q=88    128K    gf/88.jpg   SSIM    R:0.939877  G:0.970771  B:0.90722   AVG:0.939289
    q=89    132K    gf/89.jpg   SSIM    R:0.94186   G:0.97247   B:0.910751  AVG:0.941693
    q=90    180K    gf/90.jpg   SSIM    R:0.968204  G:0.978728  B:0.951346  AVG:0.966093
    q=91    192K    gf/91.jpg   SSIM    R:0.970959  G:0.980744  B:0.955161  AVG:0.968955
    q=92    200K    gf/92.jpg   SSIM    R:0.97361   G:0.9826    B:0.9591    AVG:0.97177
    q=93    212K    gf/93.jpg   SSIM    R:0.976485  G:0.984692  B:0.963619  AVG:0.974932
    q=94    232K    gf/94.jpg   SSIM    R:0.97977   G:0.98691   B:0.968263  AVG:0.978314
    q=95    252K    gf/95.jpg   SSIM    R:0.982816  G:0.988914  B:0.972891  AVG:0.98154
    q=96    280K    gf/96.jpg   SSIM    R:0.986281  G:0.991212  B:0.977948  AVG:0.985147
    q=97    312K    gf/97.jpg   SSIM    R:0.989467  G:0.993357  B:0.9826    AVG:0.988475
    q=98    356K    gf/98.jpg   SSIM    R:0.992588  G:0.99551   B:0.987442  AVG:0.991847
    q=99    444K    gf/99.jpg   SSIM    R:0.995432  G:0.997468  B:0.992342  AVG:0.99508
    q=100   520K    gf/100.jpg  SSIM    R:0.997507  G:0.998577  B:0.99612   AVG:0.997402


    4. 結果と考察

    今回の場合は、quality=90 のときのJPEGのSSIM値が、q=90を指定したWebPとほぼ同等の 0.966093 という値でした。また、この時のJPEGのファイルサイズは184KBであることも、上記のコンソール出力より同時にわかります。WebPのサイズは116KBであったことから、63% のファイルサイズで同等画質を実現していることが分かりました。

    すなわち、
    > 非可逆圧縮モードなら(同一画像、同等画質の)JPEGと比較して25-34%ほど小さいとのことです。
    というGoogleの主張は、少なくともこの検証の範囲内では合っていることが確かめられました!!

    なお、ここではさらに検証を進め、WebP, JPEGの quality を断続的に変化させながらファイルを多数生成する実験も行ったので、その結果も記述しておきます。

    このときのSSIMとファイルサイズの関係をプロットすると、グラフ1のようになりました。


    このグラフから、

    ・全体的にWebPの方がサイズが小さい
    ・ただしq=100付近のWebPは同品質のJPEGよりもサイズが大きい
    ・WebPのloss-less 圧縮は、同程度の画質を備えたJPEGよりもサイズが小さい
    ・WebPは低q値域において、JPEGと比較し、画質を保ったままファイルサイズを削減できている
    ・逆に高q値域においては、画質とファイルサイズの面でJPEGと大きな差は出ない

    などといったWebPの特徴が分かります。

    よって、

    ・高画質で配信したいものはJPEGでもWebPでもファイルサイズに大差ないため、互換性を重視しJPEGで配信する
    ・そんなに高品質でなくても良い場合は、圧縮率の高いWebPを積極的に使う

    といった使い分けが考えられますね。

    これはあくまで上記のパラメータをつかって変換を行った際の結果なのでcwebpの他のパラメータを変更することで別の傾向を示す可能性が大いにありますが、何はともあれこのように主観的な項目をも客観的に数値化することで、定量的な比較の取っ掛かりができるようになると思います。

    今後は他の変換オプションの検証や変換速度の違いなどを比較検討しながら、様々な用途に応えられる画像変換・配信システムの構築を行っていく予定です。

    ではでは。




    [1] https://ece.uwaterloo.ca/~z70wang/research/ssim/ 
    [2] Downloading and Installing WebP - WebP — Google Developers https://developers.google.com/speed/webp/download
    [3] cwebp - WebP — Google Developers https://developers.google.com/speed/webp/docs/cwebp
    [4] OpenCV/Patch to support WebP format on OpenCV 2.4.2 - Point at infinity http://www.atinfinity.info/wiki/index.php?OpenCV%2FPatch%20to%20support%20WebP%20format%20on%20OpenCV%202.4.2
    [5] OpenCV-2.4.2/samples/cpp/tutorial_code/gpu/gpu-basics-similarity/gpu-basics-similarity.cpp
    [6] https://github.com/yohsuke/ssim_opencv


    PR: 1年間、毎日1万円もらえる毎日がBIGキャンペーン!

    $
    0
    0
    コンビニで「BIG」系商品を1500円分以上購入すると、豪華賞品も当たります!

    任意のデータをピクセルとして画像に埋め込んでみた

    $
    0
    0

    こんにちは、Morino(@kohei_april20)と申します。(だいぶ前にFlashの代替としてのHTML5というエントリを書いて今回2回目です


    ちょっと前にスマホブラウザ向けのサービスでアバターを動かすアニメーションの仕組みが必要になった時がありました。それでボーンアニメーションやパラパラ漫画形式のアニメーションのスプライトシートによるアニメーションの仕組みを、Flashのアニメーション素材を元に生成して再生する仕組みを作ったりしていたのですが、その時に画像データにピクセルの色データとしてメタデータを埋め込む仕組みを作ったのでそれの紹介をしたいと思います。(ここ扱う画像はアルファ有りのPNG画像です)

    特徴

    • スプライトシートアニメーションは通常スプライトシート画像とメタデータの少なくとも2つのデータを配信する必要があるのですが、このメタデータを画像データに突っ込んでしまえば一気に配信することが可能になり、必要なリクエスト数を減らすことができます。またこれはスプライトシート用途に限らず、汎用性があります。
    • PNG画像の仕様から言えば実はチャンクと呼ばれるデータの塊を独自に付加できるので、データをここに付加することが可能です。しかし、ここにデータを入れてもブラウザからJavaScriptを通じてのアクセスが非常に悪く実用的でなさそうです。(仮にこの方法で出来ても画像とメタデータを両方得るには無駄に2度ロードが必要かも?)そのため、ピクセルの色データを画像として付加することでJavaScriptからのアクセス可能なデータとしてデータ付与ができるようになっています。
    下の画像がデータを埋め込んだ例です。画像の下部の帯状になっているのがデータ付与のために追加された部分です。ちなみにこの画像はボーンアニメーション用の画像でキャラクターの骨格のパーツをばらして一枚の画像に収めてあります。付与したデータには各パーツの切り出し位置や骨格を組み上げる初期配置情報が入っています。

    データを埋め込んだ画像例



    PNG画像へのデータ埋め込みと復元

    方法
    画像はアニメーションスプライトとしての素材であり背景などとの重ねあわせが想定されアルファ情報が必須であるので、対象のPNGはアルファを持つ1ピクセルあたり4バイトの形式のものとします。 素材画像部分は劣化させる事ができないためそのまま残し、下図のように画像の高さをオリジナルの画像よりも増やし末尾に必要な分だけ追加で領域を確保、付加したいデータをその領域の各ピクセルの色情報に変換して情報を付加します。追加するピクセルはオリジナル画像の幅の倍数になるので、付加部分の端にあまった部分であるアライメントが発生しますが、ここは利用しないのでゼロフィルします。付加情報はどのような形式でも可能ですが汎用性とデータの利用しやすさを考慮してJSONデータを文字列として付与するようにしました。 画像の末尾にデータを付加する関係で、データ復元のしやすさを考えデータを後ろから読む方式を採っています。

    データを付与された画像

    データ構造
    埋め込むデータの構造は下図のようになっています。

    データ構造

    埋め込み
    1. 埋め込むデータ量を算出(情報が付加されているかを判別するためのシグネチャ、付加情報文字列のデータ長、付加情報本体の総和)
    2. 埋め込むデータ量から、1ピクセル3バイト(本来は4バイトあるが後述の理由により3バイトのみ利用)として必要ピクセル数を算出
    3. オリジナル画像の幅から必要行数を算出
    4. 「幅」×「必要行数」で確保するピクセル数の内の余るデータ長(アラインメント)を算出
    5. アラインメント部分(ゼロフィル)、付加情報、付加情報データ長、シグネチャ(文字列'EMB'の3バイト)を合わせて埋め込みデータを整形
    6. 埋め込みデータを各ピクセルに順にセット(アルファ部分は0を埋めてスキップ)

    データ埋め込み部分の抜粋
    ※Flash素材からデータを生成するツールでAIRアプリケーションによる実装なのでActionScript3です。
    public function embedToBitmap(bitmap:BitmapData, stringBytes:ByteArray):BitmapData {
     
        var lengthWithoutAlpha:int = stringBytes.length + STR_LENGTH_SIZE + SIGNATURE_SIZE;
        var numLines:int = Math.ceil(lengthWithoutAlpha / (bitmap.width * RGB_SIZE));
        var fraction:int = lengthWithoutAlpha % (bitmap.width * RGB_SIZE);
        var alignment:int = fraction == 0 ? 0 : bitmap.width * RGB_SIZE - fraction;
     
        var appendDataBytes:ByteArray = new ByteArray();
        var i:int, j:int;
        for (i = 0; i < alignment; i++) {
            appendDataBytes.writeByte(0);
        }
        appendDataBytes.writeBytes(stringBytes, 0, stringBytes.length);
        appendDataBytes.writeShort(stringBytes.length);
        // signature 'EMB'
        appendDataBytes.writeByte(0x45);
        appendDataBytes.writeByte(0x4D);
        appendDataBytes.writeByte(0x42);
     
        var newBitmapData:BitmapData = new BitmapData(bitmap.width, bitmap.height + numLines, true, 0x00FFFFFF);
        newBitmapData.draw(bitmap);
     
        var y:int = bitmap.height;
        var x:int = 0;
        // add data to original bitmap
        for (i = 0; i < appendDataBytes.length; i += 3) {
            var b1:uint = appendDataBytes[i] << 16;
            var b2:uint = appendDataBytes[i+1] << 8;
            var b3:uint = appendDataBytes[i+2];
            var color:uint = 0xff000000 | b1 | b2 | b3;
            // alpha must be 0xFF to avoid rounding RGB values by browser
            newBitmapData.setPixel32(x, y, color);
            // proceed pixel
            ++x;
            if (x >= bitmap.width) {
                x = 0;
                y++;
            }
        }
        return newBitmapData;
    }
    

    復元
    1. 末尾より4バイト(アルファ情報は捨てて3バイト)を読み、シグネチャを確認
    2. 更に3バイト(アルファ情報は捨てて2バイト)を読み、データ長を取得
    3. 付加情報領域の行数を算出
    4. データ長分をアルファ情報をスキップして読み進みバイナリ情報を構築して文字列を得る

    データ復元部分抜粋
    ....
    var data = context.getImageData(0,0,width,height).data;
     
    // data length including image part and extra data part
    var length = height * width * RGBA_SIZE;
    var currentPosition = length - 4;
    var sign = fromCharCode(
        data[currentPosition],
        data[currentPosition + 1],
        data[currentPosition + 2]
    );
    if (sign !== SIGNATURE) {
        // error handling
        ...
        return;
    }
    currentPosition = currentPosition - 3;
    var strLength = data[currentPosition] << BITS_PER_BYTE | (data[currentPosition + 1]);
    var numExtraLines = ceil((strLength + BYTES_STR_LENGTH + BYTES_SIGNATURE_LENGTH) / (width * RGB_SIZE));
    var imageHeight = height - numExtraLines;
    // data length of extra data part excluding alpha data (1 byte for each pixel 4 bytes)
    var extraLength = width * RGB_SIZE * numExtraLines;
    // data length of alignment excluding alpha data (1 byte for each pixel 4 bytes)
    var alignLength = extraLength - (strLength + BYTES_STR_LENGTH + BYTES_SIGNATURE_LENGTH);
    var extraStartPosition = imageHeight * width * RGBA_SIZE;
    var strStartPosition = extraStartPosition + alignLength +
        // add alpha data length
        floor(alignLength / RGB_SIZE);
     
    var text = '';
    currentPosition = strStartPosition;
    for (var i = 0; i < strLength; i++) {
        text += fromCharCode(data[currentPosition++]);
        if ((currentPosition - extraStartPosition + 1) % RGBA_SIZE === 0) {
            currentPosition++;
        }
    }
    ...
    

    色データへ変換したデータの復元性とその検証

    アルファの値とブラウザの挙動
    アルファ情報を持つPNGの各ピクセルの色データはR(赤)・G(緑)・B(青)・A(アルファ)を1バイトずつの計4バイトあり、始めは4バイトすべてを利用してデータを格納しようと試みました。しかし、ブラウザで画像データに含まれるピクセルのデータをピックアップして復元する際に、アルファの値によってRGBの値がわずかに元のデータから変化してしまい元のデータを復元することができないケースが多発しました。これはアルファによる各色への影響を考慮した上でブラウザでの見た目上問題の無い範囲で丸め処理がブラウザによって行われているように思われます。そこでアルファの値を他の色に影響のない0xFFに固定してみたところ完全に元データを復元することができました。この理由から、4バイトのうちの1バイトは捨ててRGBの3バイトのみを利用してデータ埋め込みを行いました。

    検証と実用性
    検証はRGBの3バイトのとりうるすべてのパターンについて色データから復元したものと元データを照らしあわせ差異の無いことをGoogle Chrome、iOS4・5・6・7のSafari、Android2系・4系のデフォルトブラウザで確認しすることで行いました。また、万一復元のうまくいかないブラウザがあった場合に備えて、サーバ側でスプライトシートだけでなく付加情報だけを取得できるように配信をしておき、クライアント側で復元にエラーが発生した場合にのみ付加情報を別途サーバにリクエストするようにしておけば安全を期すことができます。実用性という点に関しては現在「ペコロッジ」というサービスで実運用しているので一応運用実績もあります。※ちなみにペコロッジはまもなくクローズしてしまいます(´;ω;`)(執筆時点での情報です)

    データの正当性
    復元の際のデータの正当性に関してはシグネチャの有無、復元処理でのエラーの有無、復元後のテキストデータのパースでのエラーの有無によって判定することができます。これだけでも確率的には運用上問題ないと判断して上記のようなデータ構造を採っていますが、CRC検査などを入れればより堅実なチェックが出来るようになるのではないかと思います。



    以上、スプライトシートに限らずメタデータとPNG画像がセットで必要になる場合にはこんな配信の仕方もあるよ、という紹介でした。
    ちなみに画像などのコンテンツにデータを埋め込むことを電子透かしと言いますが、これも一応電子透かしと呼んで良いのでしょうかね・・?ただデータを末尾に追加してるだけですが。

    2013年サイバーエージェント エンジニア プレゼンデータまとめ

    $
    0
    0
    皆様こんにちは

    以前、社内勉強会制度 Skill U Friday のご紹介をさせていただいた織田と申します。
    昨年は多くのセミナーを通じて、当社エンジニアをお引き立てくださりありがとうございました。

    さて今回は、昨年サイバーエージェントのエンジニア職が登壇したセミナーのプレゼン資料をまとめてご紹介差し上げます。
    今年も、多くの外部セミナーや当社発信のセミナーを通じて皆様と技術交流が出来ることを楽しみにしております。
    宜しくお願い申し上げます。

    ■秋葉原ラボ
    seminar02  seminar03
    seminar08  seminar07
    seminar39  seminar01


    ■プラットフォーム

    seminar05  seminar04

    ■フロントエンド

    seminar48  seminar49

    seminar50  seminar51

    seminar52  seminar53

    seminar54  seminar47

    seminar06  seminar10

    seminar09  seminar11

    seminar12  seminar14

    seminar13  seminar15

    seminar16  seminar17

    seminar19  seminar18

    seminar21  seminar22

    seminar23  seminar20

    seminar25  seminar26

    seminar24  seminar27

    seminar29  seminar28

    seminar44


    ■サーバサイド

    seminar31  seminar32

    seminar33  

    ■インフラ

    seminar37  seminar35

    seminar38  seminar36

    seminar40  seminar42

    seminar43  seminar41

    seminar45  seminar46

    seminar34

    たのしい Scala

    $
    0
    0

    はじめに

    初めまして。
    2011年度入社のつちはしと申します。
    アメーバのゲーム部門でエンジニアをしています。
    今回はエンジニアブログを書く機会を頂きましたので、大好きな Scala について書かせていただきました。

    「たのしさ」というとらえどころのない話しゆえ、すこしゆるーくなっておりますが、ご了承くださいませ。

    というわけで、よく「Ruby は使っていて楽しいお 気持ちいいお」と聞くけど、
    Scala も楽しいし気持ちいいんだよー!

    とゆうのを伝えたいです。。 伝わるといいです。。

    (この記事は私が Scala の楽しいと感じる部分に的を絞って書いています。
    Scala には楽しくない部分もいろいろありますが、それに関してはここでは触れません。
    Scala たんは俺の嫁)

    さくっと書いてためしてみることが出来るの

    本題に入る前に。。
    Ruby には irb という、その場でプログラムを書いてすぐに試せるツールがありますが、 Scala にも似たようなものがあるんです。
    Scala REPL といいます

    Scala が入っている環境なら、コマンドラインから

    $ scala
    

    と入力すれば起動しますお。早速何か入力して、エンター押してみましょう~
    println("hello world.")
    

    mac で brew だったら
    brew install scala
    

    で Scala が入るので、ここから一緒に試してみましょうお

    なにが楽しいのかな

    というわけで、本題です~

    Scala は何が楽しいのかな?
    うーん、うーん、と考えてみました
    きっと楽しさは人それぞれなので、私の主観をたくさん含みつつ、以下の5つに絞ってみました
    • マスコットがかわいい
    • やりたいことが素直に書ける
    • 言語仕様にわくわくできる
    • 言語を拡張できる
    • すてきな環境

    いろいろありそうですが、私はこの5つかなーと

    順番に見ていきますお~

    ■「マスコットがかわいい」

    画像のライセンスが不明なので、リンクを。。
    http://subtech.g.hatena.ne.jp/secondlife/20090701/1246418689
    あらかわいい

    真夜中3時に障害対応していたって、この子の可愛さでがんばれますね(*ノ∀ノ)

    ■「やりたいことが素直に書ける」


    プログラミングは本来楽しいものだと思うのです
    けど、書きたいものと実際に書くものの間に乖離があるほど、楽しくなくなっていくと思うのですお
    やりたいことをやろうとしたら、ちょっと邪魔が入った。みたいな感じでしょうか
    考えるままに書けると、とても気持ちがいいものですよね

    • 今までの考え方がそのまま使えること
    • それがよりシンプルに実現できること

    この2つかなぁと思うのです

    Scala はこれらを満たしていると思うんです~
    例を見ていきますね

    コレクション操作の考え方

    たとえば Ruby のコレクション操作はとても気持ちいいです
    それは、 Ruby のコレクション操作の考え方に慣れ親しんでいるからで、
    それがそのまま Scala でも使えたら、気持ちいいと思うんです

    Scala でのコレクション操作は、こんな感じです

    List(1, 2, 3).map(i => i * 2).foreach(i => println(i))
    

    > 2
    > 4
    > 6

    (*´Д`) あら気持ちいい・・

    Scala の場合、もうちょっと自明のものを削ることも出来ますお
    List(1, 2, 3).map(_ * 2).foreach(println)
    


    文字列操作の考え方


    Ruby は文字列操作も気持ちよくできます

    Scala も気持ちがいいんですお
    "hello_scala_world".split("_").map(s => s.capitalize).mkString("")
    
    > HelloScalaWorld

    (*´Д`)

    値としての関数という考え方

    (ここは関数型言語の考え方を知っている人じゃないと、少し難しいかもしれません。
    関数型言語の考え方も Scala ではそのまま使える例として載せています。)

    関数型言語では関数を値として扱うことができます
    上のコレクション操作でも、関数を値として関数に渡していました

    Scala の場合はこんなかんじですお~

    // func という関数を受け取り呼び出す関数
    def callCallback(func: String => Any) = {
      func("callback!!")
    }
    
    
    // print するだけの関数
    def callback(s: String) = println(s)
    
    
    // callCallback に callback を引数として渡す
    callCallback(callback)
    
    > callback!!

    代数的データ型とパターンマッチという考え方

    (ここは関数型言語の考え方を知っている人じゃないと、少し難しいかもしれません。
    関数型言語の考え方も Scala ではそのまま使える例として載せています。)

    関数型言語では代数的データ型というものを使うらしいですお

    Wikipedia  の例を Scala で書くと、こんな感じになりますお


      sealed trait Node
    
    
      final case class Leaf(l: Int) extends Node
    
    
      final case class Branch(a: Node, b: Node) extends Node
    
    
      def depth(tree: Node): Int = tree match {
        case Leaf(_) => 1
        case Branch(a, b) => 1 + depth(a).max(depth(b))
      }
    
    
      val tree = Branch(Branch(Leaf(1), Leaf(2)), Branch(Leaf(3), Leaf(4)))
      depth(tree)
    
    
    
    > 3

    (´;ω;`) うーん、オブジェクト指向に慣れ親しんだ身としてはわかりやすいですが、 Haskell と比べると冗長で見劣りしてしまう。。

    こういうのをみると、 Scala はオブジェクト指向言語に関数型の考え方を取り入れたものなんだなぁ。。
    という気がしてきます。 あくまでオブジェクト指向が主で、関数型の考え方でも組めますよ。というスタンスを感じます

    余談ですが、この書き方は Visitor パターンの代わりとしても使えますね

    よりシンプルな DTO

    Java で DTO を作るとき、 getter や setter を定義した100行くらいのコードを書くことがよくあります

    Scala はこれをより容易にしてくれます

    case class User(
      name: String,
      age: Int)
    

    こう書くことで getter, setter, コンストラクタ, clone みたいなもの等が定義されます~
    Lombok  みたいですね

    モナ・・・

    あいつはボクには難しすぎるお。。。

    ■ 「言語仕様にわくわくできる」

    次に、「言語仕様にわくわくできる」と楽しいなーと私は感じます
    言葉足らずでごめんなさいなのですが、
    私は、統一感があって、しっくりくると「すごいなー たのしいなー」
    と感じるのだと思います

    幾つか例をみていきます~

    データの生成の一般化

    多くの言語では配列や連想配列等、組み込みのデータ構造が特別扱いされていたりします
    新しいデータ構造を作った場合、組み込みのデータ構造とは別の書き方をしないといけなかったりもします

    Scala ではデータの作り方が統一されています。
      val list = List(1, 2, 3)
      
      val array = Array(1, 2, 3)
    
    
      val map = Map(
        "モナド" -> "わかんない",
      )
    

    自分でデータ構造を作るときも、同じように作ることができるので、
    統一感があるなーとかんじますお

    null の扱いの一般化

    NULL に初めてであったのは C/C++ 言語でした
    あの時から、私の悩みの種は尽きません
    null ってなに? 0 なの? ポインタなの? なんなの!? またぬるぽ起きたし!

    考えてみると、この定義は謎です。
      val foo: Foo = null
    
    Foo 型の foo に null が代入できるのであれば、 null の型は Foo 型のサブタイプでなければなりません。
    じゃあ、 null の型はなあに?

    Scala はこの点、少しだけ頑張っています
    null の型は Null であり、 Null はあらゆる参照型のサブタイプと定義されています


    Null


    Null は親クラスのメソッドをオーバーライドしており、
    実行するとほとんどのメソッドが NullPointerException を投げるようになっています

    null.toString
    
    > java.lang.NullPointerException

    「すべてはオブジェクトである」という考えを頑張って実現しようとしている努力が可愛い Scala たん。。(*´Д`)ハァハァ


    ■「言語を拡張できる」

    それと「言語を拡張できる」こと
    これがとてもたのしいです!

    • プロジェクト固有の構文を定義できたり
    • 組み込み型にプロジェクト固有の処理を追加できたり

    気軽に DSL みたいなものが作れそうですねー!
    (やり過ぎるとマサカリが飛んでくるのできおつけるのお。。)

    演算子の定義

    Scala の演算子は、実際はメソッド呼び出しです

    例えばこんなのがあったとすると。。
    1 + 2
    
    これはこんなメソッド呼び出しと同じです
    1.+(2)
    

    つまり、演算子を自分で作ることができます

    case class Vector2(x: Double, y: Double) {
      def +(v: Vector2) =
        Vector2(this.x + v.x, this.y + v.y)
    }
    
    
    Vector2(1, 2) + Vector2(3, 4)
    
    > Vector2(4.0,6.0)

    制御構造の定義

    Ruby で見かける 3.times do xxx end
    こういうのを自分で作ることができます。

    例えば、例外を無視する構文
    import scala.util.control.NonFatal
    
    
    def ignoreException(f: => Any) {
      try {
          f
      } catch {
        case NonFatal(e) =>
      }
    }
    
    
    ignoreException {
      println("hi!")
      throw new Exception
    }
    
    > hi!

    標準ライブラリに面白い例がいろいろ入っているので、みてみるとたのしいですおー!
    - Actor
    - Try

    組み込みクラスにメソッドを追加

    Java 以外の様々な言語で、組み込みクラスやライブラリのクラスにメソッドを追加することができます

    Scala の場合はこんな感じですお

    implicit class MyString(val s: String) extends AnyVal {
      def hello = s"Hello! $s こんにちは!"
    }
    
    
    "よっしー".hello
    
    > Hello! よっしー こんにちは!

    Java の String には、似た方法で Scala 独自の便利なメソッドがたくさん追加されていますお

    for 式

    Scala の for 式はとてもとてもおもしろいのです!

    Ruby 等と同様、 for 式はシンタックスシュガーで、
    対象のインスタンスの map, flatMap, filter, foreach 等のメソッド呼び出しに置換されます~
    つまり、 for の実際の意味はこれらのメソッドによって定義できるということ!

    そして、 Scala の for はループのためだけのシンタックスシュガーではなく、さらに広い利用範囲をもっているのです。

    これを使ったライブラリはこんなものがあります。
    - Future と Promise
      非同期処理の連鎖を for 式でシンプルに記述できます

    マクロ

    これを使用すると、コンパイル時にプログラムを生成することができます
    Scala のプログラムをパースした結果をプログラムで処理し、新しいプログラムを作ることができます
    マジック!

    これを使ったライブラリにはこんなものがあります

    - SLICK の direct embedding
      Scala のプログラムから SQL を生成できます
    - ScalaLogging
      ログを出さない時はログ生成コードを一切実行しないようにできる、ログライブラリです

    すてきな環境

    最後に、言語のたのしさは環境によるところも大きいです
    すてきな Vim 拡張があったりすると、テンション上がりますよねー!

    Scala の場合は IntelliJ IDEA  に Scala プラグインを入れて準備は完了!
    (Eclipse や Vim もあるけど、 私は IDEA が好き!)

    考えるままにキーを叩くと、それを正確な補完でサポートしてくれる IDE がとてもいいやつにおもえます。

    ライブラリの海に潜っていこう

    困ったときは IDE で「定義に飛んで」みましょう!
    Scala の標準ライブラリだって、誰かの作ったライブラリだって、すぐにソースとドキュメントが読めますお。
    (Scala に限らないけど) 使っているライブラリのソースをシームレスに参照できる環境ってすてきです~

    最後に


    長々と書いてきましたが、 Scala の楽しさがすこしでも伝わったら嬉しいです(*´∀`)

    ぜひ一度 Scala を触ってみて、それがきっかけで、日本に、社内に Scala 好きな人が増えると嬉しいです~
    (言語の選定をできる立場の方は、まずは自分で触ってみて欲しいのです。 Scala は Java の資産も使えますよ~)

    ありがとうございました!


    サイバーエージェントのスタンディングデスク事情

    $
    0
    0

    こんにちは。最近専らjavascriptを書いています、maginemuです。今回はjavascriptとは全く関係ないエントリーです。

    はじめに

    巷でスタンディングデスクとか言われてどれくらい経ったのでしょうか。

    サイバーエージェントでもスタンディングデスクをしている人は少しだけ居ます(僕のチームくらいしか見たことない)。

    そういう試みをしてる人も居るよということで紹介してみたりします。

    スタンディングデスクとは?

    にわかに話題になった「スタンディングデスク」というキーワード。一言でいえば「立って仕事をする」ということなのですが、思いの外メリットもあるようです。

    ざっと「スタンディングデスク」で検索すると沢山エントリーが出てきます。
    いくつか挙げてみましょう。

    などなど…内容としては主に

    • 健康によい
    • 集中できる
    • 自作してみた
    • 製品紹介

    といったところでしょうか。

    我々の現状

    さて、それでは我々の職場を紹介していきたいと思います。

    2013年5月頃

    当時が、現在所属しているプロジェクトにジョインした時期でした。

    スタンディングデスクではないのですが、当時はバランスボールの導入を始めた時期でした。
    なぜ始めたのか今は全然覚えてないのですが、多分運動不足とものぐさの結果ではないかと思います。

    2013年6月末

    オフィスの階を引っ越し、あるときトレーニーのチャーリー(日本人です)が段ボール箱を机の上に載せ始めたのが始まりでした。

    「スタンディング憧れるわー、やりたいけど良いところないかな」

    「これで良くね?」

    というノリで始まりました。w

    2013年10月末

    スタイルが定着。バランスボールも併用しています。

    2013年12月

    キーボードをPCのものから外付けを導入した結果、マウスも使いたくなったりしてダンボールを2つ併用したりしています。

    引っ越しの時にしぼませたバランスボールがしぼんだまま…最近はスタンディングと椅子で過ごしていました。

    スタンディングデスク開始から半年

    月並みですが所感をば。

    インターネット上に載っているエントリーで自分の感覚に一番しっくりきたのは

    こちらのエントリーでしょうか。この感覚に近い状態に、今なっていると思います。

    どちらが良いというものではない

    暫く立って仕事をしていると、立っている状態も普通になってきます。

    立つことによるメリット/デメリットの話は散々語られていると思うのですが、
    個人的には立つのが良い悪いというよりは、座るのと同じくらい立ってもいいと思う、ということ。

    幸いなことにプログラミングという仕事は立っていてもできるので、そのスタイルは座ったままに留めておく必要はないと思います。

    ではどうすればいいの?

    立つことによって

    • 集中できる(ノれる)
    • 疲れる(のが良い。頭だけでなく身体も疲れたい)
    • 思考や作業内容の切り替えがしやすくなる

    というのは実際に感じることができました。さらに

    • 特に辛くはない
    • (辛くなるほど頑張らない)

    とも思います。疲れたら座れば良くて、立っている時間は10分とかでもいいのではないかと思います。

     重要なのは、その時々に合わせて「簡単に立ったり座ったりを切り替えられる状態を作る」ことかなと思います。
     ここ数週間はあまり立ってなかったんですが、それでもいいかなと。ふと思い立った時に、立てばいいだけです。

    たとえば

    • 朝来てメールチェック, 事務処理
    • ふとトイレに立つ
    • 戻ってきたらそのまま立って仕事したくなって、足元からダンボールを机に上げる
    • 立ってプログラミング! (どうでもいいんですがクラブミュージック流してると個人的にはイイカンジデス)
    • 疲れてきたなと思ったのですぐ座る
    • 暫くするとそのうちまた立ちたくなってくる

    慣れてくると、作業によって立ちと座りが決まってきたりしますね。 レビューやリーディングしてるときは座ってて、 書く時は立つとか。

    さいごに

    そんな感じでスタンディングを取り入れて半年続けてきたわけですが、特に身体に不調などは感じられていません。

     個人的には座っていると姿勢が悪くなりがちなのですが、立っているとそこまで変な姿勢にはなりづらいですし、ネットに載ってる通りの効果も実感できたし、今後も続けていくのではないかなーと思っています。

    ただ、最近は慣れたのですが序盤は

    • 罰ゲームに見える

    というデメリットがありました。これはきちんとした環境ではなくダンボールを使っているというのと、まだあまりスタンディングデスクという文化が定着していないためだと思っています。

    このエントリーで「立つのも普通」という文化がもっと定着していけば良いなと思います!

    ではでは、Happy Standing!!

    Redisとハサミは使いよう

    $
    0
    0

    こんにちは。
    Amebaの基幹システムを担当している松本と申します。

    待望のCluster機能が実装された3.0が2014/2/11にBetaになり、
    正式リリースされる日が近づいてきているRedis

    今回はアプリ開発でRedisを使って実現できる機能の一部をご紹介させていただきます。


    Redisとは

    本ブログにも何度か出てきているためご存知の方も多いと思いますが、
    簡単に紹介しますと、Redisは高速なデータ操作が可能なインメモリKVSです。
    シングルスレッドで動作しているためアトミックな操作が可能になっています。

    また、Redisは様々なデータ型に対応していて、文字列型の他にリスト型、ハッシュ型、
    セット型、ソート済みセット型、さらにはPub/Subまで扱えることが最大の特徴だと思います。

    それぞれのデータ型に対するコマンドも多数用意されており、
    それを組み合わせることで様々なケースで利用することができます。

    普通にデータ操作を行うだけでも十分魅力的なRedisですが、
    複数のコマンドの組み合わせるとどのような機能が実現できるのでしょうか。

    カウンターやランキング、タイムライン等が有名ですが、他にもあるのです。
    簡単ではありますが、2つほどJavaを用いてご紹介していきます。


    ロック

    1つめは、RedisのDocumentでも少しだけふれているロック機能を実装してみます。

    ロック機能はRedisがアトミックな操作を保証しているから実現できる機能で、
    実装に用いるのは以下の4つのコマンドです。

    SETNX キーに値をセットする。
    キーが既に存在する場合は何もしない。
    EXPIRE キーに有効期限を設定する。
    GET キーの値を取得する。
    DEL キーを削除する。

    ロック機能のポイントはSETNXです。

    指定したキーがなかった場合は値をセットして1を返し、
    既に存在する場合は何もせず0が返ってきます。
    つまり、1はロック成功、0は他からロック済みと判断することができます。

    それでは実装に進みましょう。
    まずロックのインタフェースを用意します。

    public interface Lock {
    
        public void lock() throws TimeoutException;
    
        public void unlock();
    }
    

    最低限のロックとアンロックを用意しました。

    続いて中身を実装します。

    public class RedisLock implements Lock {
    
        private static final String LOCK_KEY_PREFIX = "lock:";
        private static final int LOCK_EXPIRE = 30;
        private static final long LOCK_SLEEP = 10L;
    
        private final Redis redis;
        private final String lockKey;
        private final long lockMillis;
    
        private volatile boolean isLocked = false;
        private volatile String lockedValue = "";
    
        public RedisLock(Redis redis, String lockKey, long lockMillis) {
            this.redis = redis;
            this.lockKey = LOCK_KEY_PREFIX + lockKey;
            this.lockMillis = lockMillis;
        }
    
        @Override
        public void lock() throws TimeoutException {
            if (isLocked) return;
           
            // ロック待機時間
            long max = System.currentTimeMillis() + lockMillis;
    
            while (true) {
                long now = System.currentTimeMillis();
                String value = String.valueOf(now);
                // ロック試行
                Long result = redis.setnx(lockKey, value);
    
                // ロックが成功したらexpireを設定してデッドロック防止
                if (result != null && result.longValue() == 1L) {
                    redis.expire(lockKey, LOCK_EXPIRE);
                    isLocked = true;
                    lockedValue = value;
                    break;
                }
               
                // ロック待機時間を過ぎたら終了
                if (max <= now) {
                    String locked = redis.get(lockKey);
                    if (locked != null) {
                        long elapsedTime = now - Long.parseLong(locked);
                        // expireが効いていなかったら削除してデッドロック防止
                        if (elapsedTime >= LOCK_EXPIRE * 1000) {
                            redis.del(lockKey);
                        }
                    }
                    throw new TimeoutException("lock timeout.");
                }
    
                try {
                    // ロックできなかったらsleepして再試行
                    Thread.sleep(LOCK_SLEEP);
                } catch (InterruptedException ignore) { }
            }
        }
    
        @Override
        public void unlock() {
            if (!isLocked) return;
            // ロックの値を取得
            String value = redis.get(lockKey);
            // このインスタンスでロックされたものなら削除
            if (lockedValue.equals(value)) {
                redis.del(lockKey);
            }
    
            isLocked = false;
            lockedValue = "";
        }
    }
    

    大分できてきました。

    SETNXでロック可否を判断し、ロックできたらEXPIREで有効期限を設定します。
    他からロックされていた場合は再試行し、待機時間が過ぎたらロック失敗です。
    ロック失敗後はロック情報を調査し、EXPIREが効いていなかったら削除します。

    アンロック時はインスタンスでロックしたものかを確認後に削除します。

    最後に、この実装を返すFactoryを用意します。

    public class RedisLockFactory {
    
        private final long lockTime;
        private final Redis redis;
    
        public RedisLockFactory(Redis redis, long lockTime) {
            this.redis = redis;
            this.lockTime = lockTime;
        }
    
        public Lock getLock(String lockKey) {
            return new RedisLock(redis, lockKey, lockTime);
        }
    }
    

    これでロック機能の完成です。

    以下のようにロックを取得して利用します。

    public class RedisLockExample {
    
        public static void main(String[] args) throws Exception {
            Redis redis = new RedisPool("localhost", 6379);
            RedisLockFactory lockFactory =
                new RedisLockFactory(redis, 10000L);
    
            for (int i = 1; i <= 10; i++) {
                // ロック取得
                Lock lock = lockFactory.getLock("hoge");
                // ロック実行
                lock.lock();
                try {
                    // 排他制御を行いたい更新処理
                    update();
                    Thread.sleep(3000L);
                } finally {
                    // アンロック
                    lock.unlock();
                }
                Thread.sleep(1000L);
            }
        }
    
        private static void update() {
            System.out.println("hoge");
        }
    }
    

    ローカルでRedisを起動し、このクラスを複数実行してからMONITORコマンドで状況を
    見てみると、ロックされている様子がよくわかります。

    1392260782.952660 [0 127.0.0.1:56465] "SETNX" "lock:hoge" "1392260782949"
    1392260782.955800 [0 127.0.0.1:56465] "EXPIRE" "lock:hoge" "30"
    1392260783.955176 [0 127.0.0.1:56464] "SETNX" "lock:hoge" "1392260783951"
    1392260783.968747 [0 127.0.0.1:56464] "SETNX" "lock:hoge" "1392260783965"
    
    ・・・略・・・
    
    1392260785.940036 [0 127.0.0.1:56464] "SETNX" "lock:hoge" "1392260785936"
    1392260785.953764 [0 127.0.0.1:56464] "SETNX" "lock:hoge" "1392260785949"
    1392260785.960863 [0 127.0.0.1:56465] "GET" "lock:hoge"
    1392260785.963952 [0 127.0.0.1:56465] "DEL" "lock:hoge"
    1392260785.967388 [0 127.0.0.1:56464] "SETNX" "lock:hoge" "1392260785963"
    1392260785.970122 [0 127.0.0.1:56464] "EXPIRE" "lock:hoge" "30"
    1392260786.968606 [0 127.0.0.1:56465] "SETNX" "lock:hoge" "1392260786964"
    1392260786.983315 [0 127.0.0.1:56465] "SETNX" "lock:hoge" "1392260786979"
    

    このRedisを用いたロック機能はAmebaのサービスで実際に使用しているものもあります!


    設定の同期

    2つめは、設定変更時に通知して複数サーバ間で設定を同期する機能を実装してみます。

    実装に用いるのは以下の3つのコマンドです。

    PUBLISH チャンネルにメッセージを送信する。
    SUBSCRIBE チャンネルに接続し、送信されたメッセージを受信する。
    UNSUBSCRIBE チャンネルの接続を終了する。

    RedisのPub/Subは非常に面白い機能で、簡単にObserverパターンの実装ができます。
    SentinelもPub/Subを使用して情報のやりとりをしています。

    今回はこのPub/Subを使って設定の同期を実装します。

    public class SyncProperties {
    
        private static final String SYNC_PROP_CHANNEL = "sync.prop";
    
        private final Redis redis;
        private final Properties properties;
        private final SyncPropPubSub syncPropPubSub;
        private final ExecutorService threadPool;
    
        private boolean isStart = false;
    
        public SyncProperties(Redis redis, Properties properties) {
            this.redis = redis;
            this.properties = properties;
            this.syncPropPubSub = new SyncPropPubSub();
            this.threadPool = Executors.newSingleThreadExecutor();
        }
    
        public void syncStart() {
            if (isStart) return;
            isStart = true;
            // subscribeはスレッドを専有するので別スレッドでチャンネル接続
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    redis.subscribe(syncPropPubSub, SYNC_PROP_CHANNEL);
                }
            });
        }
    
        public void syncEnd() {
            if (!isStart) return;
            // チャンネル接続を閉じる
            syncPropPubSub.unsubscribe();
            isStart = false;
        }
    
        public void shutdown() {
            try {
                threadPool.shutdown();
                if (!threadPool.awaitTermination(3, TimeUnit.SECONDS)) {
                    try {
                        threadPool.shutdownNow();
                    } catch (Exception ignore) { }
                }
            } catch (Exception e) {
                try {
                    threadPool.shutdownNow();
                } catch (Exception ignore) { }
            }
        }
    
        public String get(String key) {
            return properties.getProperty(key);
        }
    
        public String get(String key, String defaultValue) {
            return properties.getProperty(key, defaultValue);
        }
    
        public void put(String key, String value) {
            properties.put(key, value);
            String message = "put:" + key + ":" + value;
            // put情報をチャンネルに送信
            redis.publish(SYNC_PROP_CHANNEL, message);
        }
    
        public void remove(String key) {
            properties.remove(key);
            String message = "remove:" + key;
            // remove情報をチャンネルに送信
            redis.publish(SYNC_PROP_CHANNEL, message);
        }
    
        @Override
        public String toString() {
            return properties.toString();
        }
    
        private class SyncPropPubSub extends RedisPubSub {
            @Override
            public void onMessage(String channel, String message) {
                // 受信したメッセージを分解
                String[] m = message.split(":");
                String type = m[0];
                switch (type) {
                case "put":
                    if (m.length < 3) break;
                    // put情報を反映
                    properties.put(m[1], m[2]);
                    break;
                case "remove":
                    if (m.length < 2) break;
                    // remove情報を反映
                    properties.remove(m[1]);
                    break;
                default:
                    break;
                }
            }
        }
    }
    

    Propertiesをラップし、putとremoveが実行された時にメッセージを送信します。
    受信したメッセージは分解して解析し、Propertiesに反映します。

    送信側は以下のように使用します。

    public class PublisherExample {
    
        public static void main(String[] args) throws Exception {
            Redis redis = new RedisPool("localhost", 6379);
    
            Properties properties = new Properties();
            properties.put("hoge", "0");
    
            SyncProperties syncProperties =
                new SyncProperties(redis, properties);
            // 設定追加
            syncProperties.put("fuga", "true");
            for (int i = 1; i <= 10; i++) {
                // 設定更新
                syncProperties.put("hoge", String.valueOf(i));
                Thread.sleep(2000L);
            }
            // 設定削除
            syncProperties.remove("fuga");
    
            syncProperties.shutdown();
        }
    }
    

    受信側は以下のように使用します。

    public class SubscriberExample {
    
        public static void main(String[] args) throws Exception {
            Redis redis = new RedisPool("localhost", 6379);
    
            Properties properties = new Properties();
            properties.put("hoge", "0");
    
            SyncProperties syncProperties =
                new SyncProperties(redis, properties);
            // 同期開始
            syncProperties.syncStart();
            for (int i = 1; i <= 10; i++) {
                System.out.println(syncProperties.toString());
                Thread.sleep(3000L);
            }
            // 同期終了
            syncProperties.syncEnd();
            syncProperties.shutdown();
        }
    }
    

    受信側を起動してから送信側を起動すると、受信側のコンソールで同期の様子がわかります。

    {hoge=0}
    {hoge=0}
    {fuga=true, hoge=1}
    {fuga=true, hoge=3}
    {fuga=true, hoge=4}
    {fuga=true, hoge=6}
    {fuga=true, hoge=7}
    {fuga=true, hoge=9}
    {fuga=true, hoge=10}
    {hoge=10}
    

    設定のマスタもRedisに持つようにして、起動時にそこから持ってくるようにすれば、
    常に設定を最新の状態にすることができますね!


    まとめ

    Redisは使い方次第で様々な機能を実装することができます。
    特にPub/Subは設定をはじめ、チャットやゲームの同期やMQの実装まで、
    とても応用できる幅が広く面白い機能だと思います。

    高速なデータ操作だけではない、使い方次第で色々な可能性を秘めているRedis。
    ぜひみなさんも使ってみてください。

    なお、今回作成したソースコードはこちらで公開しております。
    ご興味がありましたらご覧ください。

    https://github.com/yosuque/redis-lock
    https://github.com/yosuque/redis-message

    ぼくがかんがえたさいきょうの LDAP テスト法

    $
    0
    0

    こんにちは。
    全社システムの吉田です。

    私の所属する全社システムでは、主に社内向けシステムの構築や運用を行っています。先日、社内の認証に用いている LDAP サーバーのバッチを作成しました。

    全社システムでは、コーディングをする際には主に Ruby を用いています。
    Ruby から LDAP とお話するには、ActiveLdap というモジュールを使用しました。ActiveLdap の利用法についてはるびまに記事が載っています。Rails を使用した事のある人ならば、きっと直感的に使用出来ると思います。

    ところで、このバッチ処理のテストはどうすれば良いでしょう??
    出来れば LDAP のモックを使った Unit Test を書きたい所です。でも、適当なモックを探して見たのですが、なかなか見つかりませんでした。Schema 登録無しで OpenLDAP と ActiveDirectory のデータが登録できて、Ruby で簡単に動かせるモックが欲しかったのですが。

    結局、今回は  Unit Test は書かずにリリースしましたが、何となく気になっていたので、引きこもりの時間を利用してモックを自作してみる事にしました。バレンタイン前後に外出すると精神衛生上よろしく無いですし、2014 年は記録的大雪が降りました。今月はいつもより余計に引きこもっております。(編集部注: 執筆は2月)

    その時に LDAP のプロトコルも調べてみたので、今回はその内容を簡単にご紹介します。
    RFC4511 によると、LDAP 通信の流れは以下の様になっています。

    BER とは Basic Encoding Rules の略で、簡単に言うと構造化されたデータをシリアライズする方法です。構造化されたデータのシリアライズという意味で JSON と似た使い方をしますが、人間にとっての可読性よりプログラムにとっての可読性を重視している点で JSON と大きく異なっています。

    LDAPMessage とは、各 Request や Response 等に固有の MessageID をつけた物です。
    LDAP では認証等の一部の通信を除き、一つの TCP セッションで複数の Request を同時に投げる事が可能です。例えば、SearchRequest を投げて、その結果が返ってくる前に AddRequest を投げる事が出来ます。サーバー側も、SearchRequest の結果を返しながら途中で AddRequest の結果を返す事も有ります。なので、Response がどの Request に対応する物なのか判別する為に MessageID を使用するんですね。

    では、もう少し詳しく見てみましょう。
    少し長くなりますが、おつきあい下さい。

    初めにBER のエンコード方法について簡単に調べてみます。

    BER は ITU-T Recommendations で定められている ASN.1 (Abstract Syntax Notation One)  のエンコード方法の事ですが、生憎このドキュメントは有料となっています。そこで、ちょっとズルをして Wikipedia (英語) を参考にしました。

    BER は以下の様に成っています。  
    (Wikipedia の図では、この後に "End-of-contents octets" が来ると書いてありますが、
    これは滅多に使わないので今回の説明では省きます。)

    Identifier octets TypeLength octets LengthContent octets Value
    データの型Value のバイト数Value

    例えば、整数の 1 を BER でエンコードする事を考えてみましょう。


    まず、Value はデータの中身です。今回は 1 です。

    次に、Length は Value の長さを表します。今回、Value の長さは 1byte なので、Length は 1 となります。
    (Value の長さが 127 byte 以下の場合、Length はそのバイト数です。)

    最後に、Type はデータの型を表します。(Json で整数の 1 と文字列の "1" が異なるように、BER ではここで ASCII コード、整数などの型を指定します。)
    Type は 最上位 2 bit, 次の 1 bit, 最下位の 5 bit に分かれ、次のような構造に成っています。

    87654321
    ClassP/Ctag

    Class は ASN.1 で定められた共通の Type の場合 0 に、独自拡張した物の場合はその方法によって0以外の値に成ります。(ASN.1 では、独自拡張がプロトコルとして許されているんですね。)
    また、P/C は Primitive (単体の値) か Constructed (配列、集合のような値) かを、
    最後の 5 byte は tag (実際に何の型なのか) を表します。

    ちなみに、この ASN.1 で定められた Type を表す Class を Universal と言い、独自拡張した Class には Application, Context-specific, Private の 3種類が有ります。

    また、独自拡張において 31 以上の tag を使う場合は longform と言う、上記とは少し違うフォームになる様ですのでご注意を。

    今回、Universal Class (ASN.1 で定められた場合) の INTEGER 型でエンコードする事にすると、Class は Universal Class を表す 0、P/C は Primitive を表す 0, tag は INTEGER を表す物で、先ほどの wikipedia によると 2に成るそうです。
    つまり、Type は 2 になるわけですね。

    結果として、整数の 1 を BER でエンコードすると、次の 3 byte になります。

    TypeLengthValue
    211

    では、Ruby で [1, 2] と表されるデータを BER で送信するにはどうしたら良いでしょう?
    今回は Array を Universal SEQUENCE 型でエンコードしてみます。

    1 を BER でエンコードすると、2 1 1 の 3 byte でした。同様に 2 を BER でエンコードすると、2 1 2 の 3 byte に成ります。
    なので、[1, 2] を Universal SEQUCNCE でエンコードする場合、そのValue は上記の 3 byte を 2 個合わせた 2 1 1 2 1 2 の 6 byte に成ります。
    当然、Length は6、Type は P/C が 1 (Constructed) に成る事に注意すると 48 です。

    なので、Ruby の [1, 2] を BER でエンコードすると次の 8 byte になります。

    SEQUENCE [1, 2]
    TypeLengthValue
    486INTEGER 1INTEGER 2
    TypeLengthValueTypeLengthValue
    211212

    色々と端折ってしまいましたが、BER について、何となくご理解いただけたでしょうか?
    ソースは Wikipedia (キリッ

    次は LDAP のプロトコルを見てみましょう。
    今回は uid=user,dc=cyberagent,dc=co,dc=jp というユーザーが openSesami というパスワードを用いて LDAP v3 のシンプルバインド (認証) する事を例に考えてみます。

    まず、認証を行う為の BindRequest について調べてみましょう。
    BindRequest のデータ型については、Section 4.2 を見てください。次の様に書いてあります。
    BindRequest ::= [APPLICATION 0] SEQUENCE {
                 version INTEGER (1 .. 127),
                 name LDAPDN, 
                 authentication AuthenticationChoice }


    BindRequest は、「INTEGER 型の version, LDAPDN 型の name, AuthenticstionChoice 型の authentication を [APPLICATION 0] SEQUENCE でエンコードしたもの」だそうです。

    分からない事だらけなので、上から順に調べていきます。

     [APPLICATION 0] SEQUENCE とは何でしょう?
    これは、「SEQUENCE 型の BER、ただし、Identifier octets Type は Class を Application の独自拡張に, tag が 0  にしなさい」という意味です。
    つまり、「Class が Application class を表す 1、P/C が Constructed 型の 1、最後の 5 bit が 0、合計  96 を Type として、{} の中にくくられた値の SEQUENCE を作れ」という事ですね。

    次の「version INTEGER」は、「LDAP Protocol の version を Universal INTEGER 型でエンコードしたもの」という意味です。そして、1 から 127 という制限も有るようです。
    今回は version 3 を使うので、3 をエンコードした次の 3 byte に成ります。

    TypeLengthValue
    213

    では、その次の「LDAPDN 型の name」とは?
    これは、「name を LDAPDN 型でエンコードしたもの」という意味です。
    じゃあ、「LDAPDN 型って何よ?」と思ってもう一度 RFC を読み返すと、section 4.1.3 に「LDAPString 型、ただし RFC  4514 の制限を満たすもの」と書いてあります。

    LDAPDN ::= LDAPString
               -- Constrained to <distinguishedName> [RFC4514]
     


    で、LDAPString について探すと、ちょっと上の section 4.1.2 に「ISO 10646 に規定された文字を UTF-8 でエンコードしたものを OCTET STRING 型でエンコードした物」と記載されています。

    LDAPString ::= OCTET STRING -- UTF-8 encoded,
                                -- [ISO10646] characters


    なんか、たらい回しにされている感がありますが、要約すると LDAPDN 型とは
    「OCTET STRING 型でエンコードした文字列。ただし、DN として有効な文字列であり、マルチバイトの際は UTF-8 でエンコードしたもの」という事ですね。
    UTF-8 なのに UTF8String では無く OCTET STRING を使うのは少し気持ちが悪いですが、互換の問題でしょうか?(よく分からない)

    ただ、LDAPDN 型については分かりましたが、私には name が何の事か、RFC から判断する事はできませんでした。仕方ないので既存の LDAP Clinet をハッキングしてみた所、どうやら LDAP の bind dn の事の様です。
    なので、今回の name は uid=user,dc=cyberagent,dc=co,dc=jp を OCTET STRING 型でエンコードすれば良いでしょう。

    uid=user,dc=cyberagent,dc=co,dc=jp の ASCII Code は次の 34 byte です。
    117 105 100 61 117 115 101 114 44 100 99 61 99 121 98 101 114 97 103 101 110 116 44 100 99 61 99 111 44 100 99 61 106 112
    結果として、name は以下の36 byte になります。

    TypeLengthValue
    434117 105 100 ...... 112


    BindRequest 最後の AuthenticationChoice 型 の authentication とは何でしょう?
    これは、BindRequest のすぐ下に書いてあります。
    「[0] OCTET STRING 型の simple か  [3] SaslCredentials 型の sasl を選べ」だそうです。今回は simple auth を使用するので、simple を使いましょう。

    AuthenticationChoice ::= CHOICE {
         simple                  [0] OCTET STRING,
                                 -- 1 and 2 reserved
         sasl                    [3] SaslCredentials,
         ... }


    [0] OCTET STRING 型とは、「OCTET STRING 型、ただし、Type は Class を Context-specific の独自拡張に、tag  は 0しろ」という意味です。すると、Type は上位 2 bit が Context-specific を表す 1 0 に、次の 1 bit は Primary を表す 0 に、最後の 5 bit は 0 になるので 128 に成ります。

    しかし、やはり私には simple で何を送信したら良いのか、RFC からだけでは判断できませんでした。これも先ほどと同様に既存の LDAP Client ツールをハッキングした限りではパスワードをそのまま記載すれば良いようです。
    今回、パスワードは openSesami なので、最終的に 
    authentication は次の様に成ります。

    TypeLengthValue
    12810111 112 101 ...... 105
    (openSesami の ASCII Code は 111 112 101 110 83 101 115 97 109 105)

    これで、version, name, authentication のそれぞれが分かりました。
    後は、
     [APPLICATION 0] SEQUENCE 型でまとめれば BindRequest の完成です。
    今回の BindRequest は以下のようになるはずです。

    BindRequest
    TypeLengthValue
    9651versionnameauthentication
    TypeLengthValueTypeLengthValueTypeLengthValue
    213434117 ...... 11212810111 ...... 105

    さて、BindRequest が出来たので、今度はこれを LDAPMessage でラップします。
    LDAPMessage については RFC4511 section 4.1.1 をご覧下さい。

    LDAPMessage ::= SEQUENCE {
         messageID       MessageID,
         protocolOp      CHOICE {
              bindRequest           BindRequest,
              bindResponse          BindResponse,
              unbindRequest         UnbindRequest,
              searchRequest         SearchRequest,
              searchResEntry        SearchResultEntry,
              searchResDone         SearchResultDone,
              searchResRef          SearchResultReference,
              modifyRequest         ModifyRequest,
              modifyResponse        ModifyResponse,
              addRequest            AddRequest,
              addResponse           AddResponse,
              delRequest            DelRequest,
              delResponse           DelResponse,
              modDNRequest          ModifyDNRequest,
              modDNResponse         ModifyDNResponse,
              compareRequest        CompareRequest,
              compareResponse       CompareResponse,
              abandonRequest        AbandonRequest,
              extendedReq           ExtendedRequest,
              extendedResp          ExtendedResponse,
              ...,
              intermediateResponse  IntermediateResponse },
         controls       [0] Controls OPTIONAL }

    MessageID ::= INTEGER (0 ..  maxInt)

    maxInt INTEGER ::= 2147483647 -- (2^^31 - 1) --

    一見長く見えますが、早い話が「INTEGER 型の MessageID と ProtocolOp (今回は BindRequest) を SEQUENCE にしろ。オプションで controls をつける事もある」との事です。
    また、MessageID については「0 でない値にしろ。同一セッション中で、サーバーが処理を終了するまでは同じ値を再利用するな。クライアントは毎リクエスト毎にインクリメントするのが良いだろう」とすぐ下の section 4.1.1.1 に書いてあります。

    今回は controls は使いません。
    また、大抵の場合、BindRequest は各セッションの最初に行うと思われるのでここでは 1 を使う事にします。
    つまり、LDAP Client が Server に投げる LDAPMessage は MessageID の 1 と先ほどの作成した BindRequest を SEQUENCE にした物なんですね。ここは普通に実装すれば大丈夫の様です。

    この LDAPMessage を受け取ったサーバーはどうするのでしょうか?
    ざっくり言うと、Client と逆の事をすれば良いんです。

    まず、LDAPMessage をほどいて Request を取り出します。 Request の Type を見ると、それが Application Class, tag が 0 の BER で有る事が分かるでしょう。
    このような BER は LDAP プロトコルにおいては BindRequest しかありません。(LDAP において Application Class で Type が同じ BER は同じ型と思って大丈夫です。)
    なので、中身の 1個目が version, 2個目が name, 3個目が authentication という事が分かります。
    また、authentication の Type は Context-specific Class で tag が 0 です。Context-specific Class については前後関係を確認しないと何の型なのか分からないのですが、少なくとも BindRequest 中の authentication では simple を表し、その Value はパスワードのはずです。

    後は、実際に認証を行い、その結果から BindResponse を作成し、Request と同じ MessageID (今回は 1) を使って LDAPMessage を作成し、Client に返せば終了です。

    以上、駆け足でしたが LDAP の通信についてイメージできましたでしょうか?
    今回は BindRequest を例にご説明しましたが、同様に他の Request も調べれば
    LDAP Server のモックは実装できると思います。

    今回、私は、普段とは違うレイヤーの技術を調べてみて「少し面白いな」と思いました。
    この気持ちを他の人と共有したいと思って記事にしてみたのですが、皆さんに少しでも伝われば幸いです。

    最後に、私が作成した LDAP のモックを github で公開しました。
    まだα版ですしドキュメントも揃っていませんが、興味のある方は使ってみてください。
    Ruby 2.0 と Mac OSX で動作確認をしました。

    残念な事に Ruby の ActiveLdap はまだ動きません。でもnet-ldap という別の Ruby の gem や ldapadd, ldapsearch, ldapmodify, ldapdelete という Unix 系の各種コマンドの動作確認は取れました。
    詳細は README.md をご覧下さい。

    少し長かったですが、おつきあい頂きありがとうございました。

    Fusion-io ioDriveを導入するときに注意すること

    $
    0
    0

    こんばんは、佐野です。前回はこんな記事を書きました。最近は窓際で仕事するふりをしています。
    2011年3月入社なのでどうやら入社して3年も経ってしまったようです。通算して4回目の登場です...。このエンジニアブログは年イチくらいのペースで書いてることになるのかな...。入社したときは20代だったんですが今はもう立派な三十路戦士です。酒飲むとなかなか酒が抜けません。平日に深酒するといつも後悔します。でも反省はしません。

    昨今のウェブサービス開発はスタートアップを中心にクラウドを使うのが主流で、あまり需要がないかもしれませんが...今回は「甘え」と言われることもあるioDriveを導入する際に注意しておくべきポイントについて書かせていただきます。
    ioDriveをはじめとするフラッシュストレージは、強力なIO性能をもつこともあってiopsなどの性能指標値ばかりに検証の焦点がいってしまいがちなのですが、プロダクションで運用するには性能以外にも見ておくべきポイントがあります。今回言及するのは熱落ち、電力不足、運用が始まってから気をつけるべきこと、の三点です。前者二点はOEMで提供されているものであっても直面することがあるのでちゃんと検証した方が良いです。

    アメーバのFlash事情

    まずはコラム的に...弊社ではアメーバピグで導入(2010年~2011年頃)して以来、要所要所でフラッシュを使っていて、導入実績、コストともに、インフラの選択肢の一つとなっています。フラッシュの内訳はFusion-io社のioDrive(ioDrive2)とVirident社のFlashMAXの二本立てで、ioDrive(ioDrive2):FlashMAX=9:1くらいです。他のベンダのものも検証はしていますが、今プロダクションにあるのはこの二つ。用途としては大抵はMySQLのデータ領域として使っています。ちなみにほとんど壊れたことがありません。2年くらい前に買ったやつが2~3個壊れたくらいです。

    気をつけることその1: 温度

    マシン筐体内部の排熱が滞って熱落ちすることがあります。落ちると言ってもマシンそのものが落ちるわけではなく、ioDriveがマシンから自動的に切り離されます。これは安全のためにこうなる仕様の模様。ただ、サービスが止まることにかわりはありません。

    調査方法

    ddコマンド、fio、sysbenchなどのベンチマークツールを長時間かけるのが良いでしょう。ioDriveのユーティリティに含まれているfio-statusコマンドで温度が確認できるので、閾値に達していないかを定期的に確認してみます。まぁ、熱落ちするとマウントしたファイルシステムにアクセスできなくなるのでそれで気がついたりもしますが...。

    対処方法

    ioDriveを刺すPCIeスロットの位置を変えてみます。場所を変えることによって筐体内のエアフローが変化して、この問題が解消することがあります。これでも無理だったらマシンを別の機種や別のベンダのものに変えて同様の検証をしてみましょう。

    気をつけることその2: 電力

    ラックの電力にも注意ですが、PCIeスロットの供給電力が追いつかなくなる警告が出ることもあります。こんなの↓ね。
    !! ---> There are active errors or warnings on this device!  Read below for details.
            ACTIVE WARNINGS:
                Over PCIe power budget alarm triggered.
    Fusion-io社のドキュメントでは、"1つのioDrive内に複数のデバイスを搭載したもの(つまりioDrive DuoやioDrive2 Duo)では注意しろ"、という文言がありますが、1マシンにioDriveを二枚刺したときもこの現象に直面したことがあります(まあ、Duoを使ったときと同じ状況になるのかな)。

    調査方法

    熱落ち問題と同様、ioDriveに十分な負荷を与えて、定期的にfio-statusコマンドを叩いて、上記のような警告が出ていないか確認します。

    対処方法

    外部電源を使う。もしくはドライバの設定(external_power_overrideオプション)でPCIeスロットへの供給電力のリミットを解除します。ただしこの設定を行う際は、マシンのPCIeスロットが55W以上の電力に対応しているかを確認した方が良いです。

    気をつけることその3: 運用

    主にMySQLの土台としての用途が多いのでその場合について。サーバ台数が減る、リカバリが高速になる、調査が楽、などいいことがたくさんあります。が、基本的にはフラッシュ導入後もDBAがやることは変わりません。

    * 適切なインデックスを貼る
    * 適切なクエリを書く
    * テーブルの分散を考慮する

    今までと同じようにケアしましょう。これらをおろそかにすると普通にスロークエリが発生してサービス影響が出ます。今まで以上に大量のクエリを捌けるようにはなりますが、やはり無敵ではないです。また、想像はつくと思いますがボトルネックはIOからCPUに移ります。

    あんま気合い入ってないゆるい記事で申し訳ございません。以上です。

    運用エンジニア募集中!!!!
    俺を助けて!!!!!!!!!!!!!!!!!!!!

    #e100q 新人エンジニアにお勧めする一冊

    $
    0
    0

    皆様こんにちは。サイバーエージェントエンジニアブログ運営委員です。
    今回の記事は、サイボウズさんの企画「エンジニア100人に聞きました」の企画です。

    「エンジニア100人に聞きました」とは、サイボウズさんのブログから引用させていただくと
    >これは、毎回、同じアンケートをそれぞれの企業内で行い、結果を「せーの」で同時公開する、というものです。
    >あくまでも「お楽しみ企画」なので、統計学的に有意な結果を得ようというわけではなく、ただ、それぞれの企業カラーを反映した「エンジニアの気風」が見えてきたら楽しかろう、というぐらいのつもり。何より、テクノロジーを愛するエンジニア同士、一緒に面白いことをやって盛り上がれれば、それが一番、というスタンスです。
    (サイボウズ式 「エンジニア100人に聞きました」始めます。より)

    弊社が参加するのは今回で2回目です。(前回はながら聞きについてアンケートをとりました。)

    今回のアンケートのテーマは「新人にお勧めする一冊」。
    新人エンジニアに読んでほしい一冊を25名の先輩エンジニアから集めました。


    Q1.新人エンジニアにお勧めする一冊を教えてください。

    Q2.お勧めする動機を教えてください。(選択式)

    どの本もエンジニアなら一度は目にしたことのあるいわゆる"エンジニア入門書"が揃いましたね。
    新人エンジニアの皆様はまずはこの辺りから読んでみるとよいと思います。

    インフラエンジニアの教科書
    >今にして思えば、自分が新人のときにこれを読んでおけば良かったと考えているから

    マスタリングTCP/IP 入門編 第5版
    >自分が新人のとき、先輩から勧められたから。

    UNIXという考え方―その設計思想と哲学
    >今にして思えば、自分が新人のときにこれを読んでおけば良かったと考えているから。

    エキスパートのためのMySQL[運用+管理]トラブルシューティングガイド
    >今にして思えば、自分が新人のときにこれを読んでおけば良かったと考えているから。

    システムはなぜダウンするのか
    >自分が新人のとき、実際に読んでみてためになったから。

    開発効率をUPする Git逆引き入門
    >今にして思えば、自分が新人のときにこれを読んでおけば良かったと考えているから。

    Introduction to Information Retrieval
    >今にして思えば、自分が新人のときにこれを読んでおけば良かったと考えているから。

    Webを支える技術 -HTTP、URI、HTML、そしてREST
    >自分が新人のとき、先輩から勧められたから。

    世界で闘うプログラミング力を鍛える150問 ~トップIT企業のプログラマになるための本~
    >自分が新人のとき、実際に読んでみてためになったから。

    プログラミングの基礎
    >今にして思えば、自分が新人のときにこれを読んでおけば良かったと考えているから。

    リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック
    >今にして思えば、自分が新人のときにこれを読んでおけば良かったと考えているから。
    >自分が新人のとき、実際に読んでみてためになったから。

    EFFECTIVE JAVA 第2版
    >自分が新人のとき、実際に読んでみてためになったから。

    Scalaスケーラブルプログラミング第2版
    >自分が新人のとき、実際に読んでみてためになったから。

    ノンデザイナーズ・デザインブック
    >デザインの理由が言語化されているので、プログラマには楽しい読み物

    情熱プログラマー ソフトウェア開発者の幸せな生き方
    >自分が新人のとき、先輩から勧められたから。

    闘うプログラマー
    >タメにはなってないけど、超一流のプログラマーの生態として面白い

    ハッカーと画家 コンピュータ時代の創造者たち
    >今にして思えば、自分が新人のときにこれを読んでおけば良かったと考えているから。

    主体的に動く アカウンタビリティ・マネジメント
    >社会人になって、一番影響を受けた書籍だから。「主体的に動く」大切さを学んだ。

    孫子
    >今にして思えば、自分が新人のときにこれを読んでおけば良かったと考えているから。

    武士道
    >自分が新人のとき、実際に読んでみてためになったから。

    Q3.Q1で挙げた一冊以外に、お勧めの本があれば教えてください。

    複数回答でQ1で挙げられた本以外をまとめました。
    Q1で挙げられていたEFFECTIVE JAVAやハッカーと画家はここでも多く挙げられていました。

    ウェブオペレーション ―サイト運用管理の実践テクニック
    現場で使える MySQL
    iPhone/iPad/iPod touchプログラミングバ―iOS7/Xcode5対応
    Unityで作るスマートフォン3Dゲーム開発講座 Unity4対応
    見てわかるUnity4 C#超入門 (GAME DEVELOPER BOOKS)
    現場で通用する力を身につける Node.jsの教科書
    はじめてのNode.js -サーバーサイドJavaScriptでWebアプリを開発する-
    HerokuではじめるRailsプログラミング入門
    Java魂―プログラミングを極める匠の技
    CODE COMPLETE
    7つの言語 7つの世界
    人月の神話【新装版】
    アジャイルサムライ-達人開発者への道-
    小さなチーム、大きな仕事〔完全版〕: 37シグナルズ成功の法則
    Team Geek ―Googleのギークたちはいかにしてチームを作るのか
    プロデュース能力 ビジョンを形にする問題解決の思考と行動
    失敗の本質―日本軍の組織論的研究
    root(ルート)から/(ルート)へのメッセージ―スーパーユーザーが見たひととコンピュータ
    暗号解読
    ウォール街のランダム・ウォーカー
    だまされない保険
    世界で一番美しい元素図鑑
    五輪書
    Software Design
    WEB+DB PRESS
    ベルセルク
    新宿スワン
    リアル
    喧嘩商売
    ハチワンダイバー
    日々ロック
    BLAME!
    賭博黙示録カイジ

    Q4.先輩エンジニアとして、新人エンジニアに贈る言葉をお願いします。

    一人一言、なかなか深い言葉が集まりました。

    ・事実確認をしっかりする
    ・先輩から教わるだけじゃなく、自分でも考えて学ぶ姿勢が大事!
    ・焦らず、貪欲に。
    ・実る努力は実る
    ・自分のコード1行、1ワードにも常に根拠(そう書く理由)を考える癖をつけて下さい
    ・とりあえずチャレンジしてみる
    ・悩んだら帰って風呂
    ・無所属新人
    ・単体テストは書きましょう
    ・モヒカンになれ
    ・流行よりも好みで技術を勉強しよう
    ・組織貢献には色々な方法があると思いますが、ゼークト組織論における「無能な働き者」にだけはならないように気をつけてください。
    ・人生万事塞翁が馬
    ・自分の頭で考えて問題を解決する癖を身につけましょう!
    ・普通のやつらの上を行け
    ・プログラミングは楽しいものです。成長を目一杯楽しんで下さい。
    ・Done is better than perfect. by ザッカーバーグ
    ・一見エンジニアリングと関係ないジャンルの本にもいろんなヒントが隠れていたりするので、技術書以外も読むことをお勧めします
    ・漫画でも読んで肩の力を抜きましょう。



    いかがでしたでしょうか。
    誰もが一度は読んだことがあるような入門書や古くからの良書、息抜きまで揃っていましたね。
    業務が忙しくなるとアウトプットばかりでインプットするための本を読まなくなりがちですが、
    新人エンジニアの皆様にはぜひ本をたくさん読んで力をめりめりとつけていってほしいです。

    結果をまとめていたら読み返したい本や知らなかった本を読みたくなったのでこの辺で!


    本企画の他社さんのアンケート結果はこちら。

    ドリコムのエンジニア100人に聞きました(株式会社ドリコム)
    「エンジニア100人に聞きました」~新人エンジニアにお勧めする一冊編~(グリー株式会社)
    新人エンジニアに勧める一冊──エンジニア100人に聞きました(第3回)(サイボウズ株式会社)
    アクシス の エンジニア100人に聞きましたプロジェクト (株式会社アクシス)

    Javaプロジェクトでテストをたのしく書くための試み

    $
    0
    0
    こんにちは、Ameba事業本部ゲームプラットフォーム室の山田(@stormcat24)です。
    自分のミッションは主にゲーム部門の開発の改善で、最近はScalaでモナ・・・しながらツールを書いてたりClojureに手を出したりしています。

    はじめに

    ところでみなさんJava書いてますか?サイバーエージェントでは最近node熱が高いのですが、Javaプロジェクトもまだまだ根強く存在します。僕も隙あらばScalaをぶっこもうとしてますが、大人の事情でまだまだJavaを書くシーンも多いのです。
    で、そんなテンションが上がりにくいJavaプロジェクトをやっていく上で、せめてテストくらいはなるべくたのしく書きたい!ということで、今のプロジェクトで取り入れた施策を簡単にですが紹介します。
    めちゃくちゃ尖った技術を使ってるわけではないですが、これらをやっておけばそれなりに楽しく書けるかなと思ってますので、ゆる~い感じで呼んで頂ければ幸いです。

    Javaであるが故の"テストの表現力"の限界

    JavaでユニットテストといえばJUnitですよね。まだまだ健在なJUnitですが、なかなかレガシーなAPIですし、最近主流となっているBDDスタイルなテストを書くには貧弱さが否めません。

    assertThat(JUnit4から登場)やhamcrestのMatcherが登場してタイプセーフに「それっぽい英文」みたいにテストを書けるようにはなりましたが、パッと見そのテストコードが何を意味しているかということは書いた人じゃないとわからないでしょう。RubyのRSpec、JavaScriptのJasmine、ScalaのSpecs2等でBDDを経験している方にとっては貧弱な仕組みに映るのではないでしょうか。

    このような仕組みでテストをちゃんと書きましょう!なんて言っても、
    メンバーにただ苦痛なテストコードを書くことを強いるだけだと考えていました。テストが無いプロジェクトは何かと叩かれる事も多くてそれは仕方ないとは思っていますけど、Javaプロジェクトに関しては「他の言語に比べて可読性と生産性に優れた仕組みが確立されてない」という面がテストの積み上げが促進されない要因の一つではないかと思います。

    今回のプロジェクトではテストをとても大事にしたかったので、このような仕組みだとメンバーにただ苦痛なテストコードを書くことを強いるだけだと考え、色々と施策を考えることにしました。

    Spock

    そこで今回白羽の矢が立ったのがBDDテスティングフレームワークの「Spock」です。SpockはJavaで利用できますが、威力を発揮するのはGroovyでテストコードを書くときです。SpockはGroovyのDSL(ドメイン固有言語)の仕組みを利用していて、Javaと比べて簡潔に、高い表現力でテストを書くことができます。簡単に1つ例を紹介しましょう。

    package example
    
    import spock.lang.Specification
    import spock.lang.Unroll
    
    @Unroll
    class SpockExample extends Specification {
    
        def "minimum of two numbers must be #expect" () {
            expect:
            Math.min(left, right) == expect
            where:
            left  | right  || expect
            1     | 3      || 1
            0     | -1     || -1
            -1    | -2     || -2
        }
    }
    
    java.lang.Math#minを検証する単純なコードです。まず目につくのはwhere部分じゃないでしょうか。これはData Tablesという仕組みで、パイプ区切りで入力値や期待値を設定しておいて、ヘッダ部分で定義してある変数にセットし、expect部分で記述している検証コードをレコード数分行ってくれるというものです。

    また、この検証メソッドには文字列でSpec(仕様)を記述することができます。この中ではData Tablesで利用している変数を#つきで記述すると、Specに変数を当てて結果を出力してくれるので便利です。

    他にもSpockの機能はありますが、Javaで書くテストに比べて生産性はもちろん表現力も優れていることがわかってもらえるかと思います。BDDはもちろん、TDD開発も促進されますね!

    Groovyとのうまい付き合い方

    テストをGroovyで書くということですが、Groovyを経験したことの無いメンバーの方が多いので、うまい付き合い方を決めておいた方が良いです。

    Spockの基本的な書き方(DSL部分)さえ覚えてしまえば、DSL以外は普通にJavaスタイルで書いてもらってOKで、無理にGroovyスタイルで書く必要は無いと思います(言わずとも皆Javaスタイルで書いていて、Groovyスタイルで書いていたのは自分だけであった)。

    アプリ本体のコードをGroovyでとなれば話は別ですが、テストに限定した利用だったのでそう抵抗無く受け入れられた感じです。

    Spring Frameworkとの統合

    今のプロジェクトでは
    Spring Frameworkを利用しているので、Spockとうまく連携できる仕組みが必要でした。幸いにもSpockにはSpring連携ライブラリがあります。pom.xmlに以下の依存を追加してあげれば導入できます。
    <dependencies>
        <dependency>
            <groupId>org.spockframework</groupId>
            <artifactId>spock-spring</artifactId>
            <version>0.7-groovy-2.0</version>
            <optional>true</optional>
        </dependency>
    </dependencies>
    
    spock-springにあるorg.spockframework.spring.SpringInterceptorを使えば、Spockのテスト起動時にDIコンテナを初期化したり、@Transactionalでのトランザクション制御ができるようになります。今回のプロジェクトではもっとカスタマイズしたかったので独自にInterceptorを実装しています。

    コントローラー部分にはSpringMVCを利用していますが、SpringTestとの親和性も良いです。

    DBUnit

    これも古くからあるテスティングフレームワークですが、DBが関わるテストでテストデータを投入するために利用しています。

    Spring+Spockと連携するために拡張を施していて、以下のようにDbUnitというアノテーションを付与するとデータを自動で投入してくれます。この処理は前述のSpockの独自Interceptor内で実装しています。
    class TestServiceSpec {
        @Autowired
        TestService testService
        @DbUnit(path = ["master/m_hoge.xml", "test/user.xml"],
            datasource = "masterDataSource")
        @Transactional
        def "testspec" () {
            // test code
            expect:
                true
        }
    }
    

    H2 Database

    主たるデータストアはMySQLなのですが、ユニットテストに関してはJava製のインメモリDBである
    H2 DatabaseをMySQLモードで利用しています。

    GHE(Github Enterprise)にpushされたコードはすぐさまJenkinsでテストが実行されるようにしてCIをワークさせてますが、テストコードも多いのでインメモリであるH2を使ったほうが圧倒的にテスト時間は短くなります。

    H2利用時の注意点は厳密なSQLを用意しなくてはならないということでしょうか。mysqldumpを使ってダンプしたようなDDLはたいてい通らないです。あらかじめスキーマをSQLで用意するような運用が必要になります。
    また、このプロジェクトではFlywayVagrantを使って各自のDBのマイグレーションを運用しているので、この仕組みとH2でのテストがシームレスに連携できるような仕組みを構築中です。

    導入してみて

    おかげさまで順調にテストは積み上がりました。これらの施策を実施してみて高い生産性と表現力を得ることができたと思います。表現力の高いテストコードが書けるということは、他の人が書いたテストコードを理解しやすいというメリットがありますね。

    また、大胆にリファクタリングがしやすくなりました。これが一番大きいですね。恐る恐るリファクタリングしなくちゃいけないような開発は精神を摩耗するだけです。

    今後はこの仕組みをもっとブラッシュアップしてフレームワーク化して、「たのしくテストを書く」という文化とともに浸透させていきたいと考えています。

    終わりに
    というわけでみなさん、Groovyに飽きたらScalaをやりましょうね!


    PR: PANDA ファーストアニバーサリーフェア開催

    アメーバピグにおけるDB構成&対応記

    $
    0
    0

    2ヶ月前にインフルエンザとウィルス性胃腸炎でひどくダメージを受けた増田(@masudaK)です。アメーバピグは2009年2月に始まったサービスで、FLASH・Javaで作られています。そして、データストアにMySQLを用いてます。本記事では、わたくしが2年ほど見続けているアメーバピグのDB環境について構成や、日々どのようにして問題と向き合っているかを紹介したいと思います。インフラ寄りの内容が多いため、アプリ寄りの話は弊社生沼の資料を御覧ください。

    1. 構成と規模

    1.1. 構成

    まず構成ですが、読み書きはすべてマスターへ行うようにしています。そのため、スレーブには参照を向けず、ホットスタンバイとして使っています。バージョンに関しては2012年中旬までは5.0を使ってましたが、DC移転にあわせて5.5にあげました。ロック機能を用いたシャード構成をしてまして、2014年3月現在6シャードになっています。したがって、サービスで用いている台数は12台になります。加えてバックアップ用DBサーバが一台、準本番環境用に一台あります。

    この他にも下記第1図にあるように、認証用のDB、ランキング用のDB、スマホアプリ向けDBなど色々ありますが、本稿ではサービスを動かしている12台のMySQLサーバに焦点を当てます。また、現在のMySQLを使った構成に至るまでは、様々な工夫・苦労があったのですが、それは弊社桑野の資料デブサミのレポートを見て頂ければと思います。




    第一図: ピグDB環境の全体図

    1.2. 規模。そしてトラフィック

    総トラフィック・総参照・更新量としては約700Mbps、参照3万/秒、更新7千/秒になります。この量をマスター6台で捌いています。また、待機系も作るということになり、動作確認のために待機系を稼働させると、第二図にあるようにエリアのオンライン情報の共有をしているテーブルなどは倍のトラフィックが流れるときもあります。

    第二図: 待機系稼働時のトラフィック量

    アメーバピグのDBはこれぐらいの規模で運用されているんだなという印象を持って頂ければと思います。

    2. どう戦ってきたか

    上述したような構成・規模で運用されているMySQLですが、運用してから色々と問題がありましたので、ここからはそれぞれの問題について、どう対応していったかを述べていきます。

    2.1. クエリ調査

    サービス開発をしていると、リソースが枯渇してしまうような問題が生じます。ピグはioDrive Duoを使っており、IOへの負荷よりも基本CPUへの負荷のほうが高騰しやすく、mysqldがCPUを酷使し、ロードアベレージ300超えというときもありました。

    負荷が高騰して、処理が終わらないクエリが出てきているので、調査しなければなりません。そのような負荷高騰時は基本的にはshow processlistで現在実行されているSQLを見たり、スロークエリログを見て時間のかかっているSQLを特定したりするなど、まず一般的なアプローチでトラブルシューティングを行います。またそれに加え、tcpdumpでパケットキャプチャを行い、そのデータをpt-query-digestコマンドに読み込ませ、大量に発行されたSQLなどを特定したりするなどして、それらを改修してもらうということを行っています。show proceslistに関しては割愛します。pt-query-digestに関しては、出力の読み方も具体的に説明します。まず、DBサーバでパケットキャプチャをします。以下のようなコマンドを発行します。

    # /usr/sbin/tcpdump -s 65535 -x -nn -q -tttt -i [INTERFACE_NAME] -c 10000 port [PORT] > /tmp/`hostname`-`date +%Y%m%d_%H%M%S`.pcap

    インターフェース名やポートは各環境に合わせて変更してください。-cオプションで取得するバイト数を指定していますので、トラフィックが比較的あるサーバであれば、一瞬で終わるかと思います。そして、このpcapファイルをpt-query-digestが使える環境にSCPします。

    次に、pcapファイルを解析する環境を整えます。まずそのサーバにpercona-toolkitパッケージをインストールしましょう。パッケージはPERCONAのサイトに置いてありますので、そこからダウンロードして使ってください。そして、以下のコマンドで解析を行います。

    $ pt-query-digest --type tcpdump db04-20140319_203516.pcap > db04-20140319_203516.digest
    $ cat db04-20140319_203516.digest

    そうすると以下のような形式から始まるファイルが出力されます。簡単ではありますが説明していきます。

    # 4.8s user time, 70ms system time, 32.33M rss, 164.88M vsz
    # Current date: Wed May 22 00:25:07 2013
    # Hostname: test01
    # Files: db04-20140319_203516.pcap
    # Overall: 1.96k total, 49 unique, 917.04 QPS, 33.47x concurrency ________
    # Time range: 2014-03-19 00:03:25.966159 to 00:03:28.107843
    # Attribute          total     min     max     avg     95%  stddev  median
    # ============     ======= ======= ======= ======= ======= ======= =======
    # Exec time            72s       0      2s    36ms   241ms   141ms   152us
    # Rows affecte         941       0     220    0.48    1.96    4.96       0
    # Query size       238.84k       8   1.15k  124.53  329.68  154.31   76.28 # Warning coun     160.14k       0  48.75k   83.49       0   1.59k       0 # Boolean: # No index use   0% yes,  99% no

    見方は難しくなく、いつからいつまでにキャプチャされたデータで、実行時間やクエリサイズの平均値・最小値・最大値という単純なものから、標準分布、中央値などの統計的な値も算出されていて、全クエリの解析結果がまず出ています。実行時間が長いものを見つけたり、クエリサイズの大きいものを見つけられるかもしません。たとえば、2秒かかってるクエリがあったというのは気に留めておいて調査をしてもよいでしょう。

    次は、キャプチャ時に流れていたクエリの内訳になります。それぞれのクエリのコール数や占有している時間などが分かります。降順で並んでますので、上位5位ぐらいまで見ていくと大体怪しそうなものは見つかるでしょう。また、平常時のデータと比較することで、高負荷時にはコール数が増えていたり、時間がかかっているようであれば、目星もつけられるでしょう。

    # Profile
    # Rank Query ID           Response time Calls R/Call V/M   Item
    # ==== ================== ============= ===== ====== ===== ===============
    #    1 0x19E9A02CB74CDC83 11.2006 15.6%   137 0.0818  0.68 INSERT UPDATE table1
    #    2 0x51AF3D00FC8FC8BA  9.5568 13.3%   181 0.0528  0.42 SELECT table1
    #    3 0x097D1EE66A7D9EEB  9.3640 13.1%   238 0.0393  0.59 SELECT table2
    #    4 0xF9F96162CEE7817F  8.7675 12.2%   542 0.0162  0.61 SET
    #    5 0x85B0DB48178B131C  5.2483  7.3%    81 0.0648  0.51 INSERT UPDATE table3
    #    6 0x6CFA293E5A09DFD5  4.2260  5.9%    40 0.1056  0.40 INSERT UPDATE table2
    #    7 0xFC70D8E1CBAE0B6D  3.3443  4.7%   224 0.0149  0.33 SELECT table5
    #    8 0x2D30BB0A76BC19BE  3.1292  4.4%   147 0.0213  0.22 SELECT table6
    #    9 0x840399521E7FA550  3.0779  4.3%    20 0.1539  0.62 INSERT UPDATE table6
    #   10 0x9F81E35658AD5D28  2.4473  3.4%    24 0.1020  0.52 SELECT table7
    #   11 0x3E68A4EEC870A8E9  2.2641  3.2%    43 0.0527  0.63 SELECT table8
    #   12 0x7FADBD30E1E24E40  1.7834  2.5%    54 0.0330  0.49 INSERT UPDATE table10
    #   13 0x6B96B871E111AD7B  1.4353  2.0%    10 0.1435  1.10 SELECT table11
    #   14 0xFBD8C9F5097ADB41  1.0718  1.5%     4 0.2679  0.52 SELECT table12
    #   15 0x397C925C3827CB0F  0.9169  1.3%     7 0.1310  0.22 SELECT table13
    #   16 0x6D2D77FAC9FACD66  0.6539  0.9%    37 0.0177  0.37 SELECT table14
    # MISC 0xMISC              3.1911  4.5%   175 0.0182   0.0 <33 ITEMS>

    次は、クエリそのものの解析です。

    # Query 1: 64.81 QPS, 5.30x concurrency, ID 0x19E9A02CB74CDC83 at byte 8436064
    # This item is included in the report because it matches --limit.
    # Scores: V/M = 0.68
    # Time range: 2014-03-19 00:03:25.966932 to 00:03:28.080809
    # Attribute    pct   total     min     max     avg     95%  stddev  median
    # ============ === ======= ======= ======= ======= ======= ======= =======
    # Count          6     137
    # Exec time     15     11s   161us      1s    82ms   455ms   235ms   224us
    # Rows affecte  24     235       0       2    1.72    1.96    0.68    1.96
    # Query size    17  42.22k     286     337  315.57  329.68   11.45  313.99
    # Warning coun   0       0       0       0       0       0       0       0
    # String:
    # Databases    db04
    # Error msg    ******************************
    # Errors       65535
    # Hosts        10.200.110.89 (14/10%), 10.200.110.81 (11/8%)... 36 more
    # Query_time distribution
    #   1us
    #  10us
    # 100us  ################################################################
    #   1ms  #
    #  10ms
    # 100ms  ###########
    #    1s  #
    #  10s+
    # Tables
    #    SHOW TABLE STATUS FROM `db1` LIKE 'table1'\G
    #    SHOW CREATE TABLE `db1`.`table1`\G
    insert into table1 values ( _binary'********************' , _binary'\*********' ) on duplicate key update data = _binary
    '\0\0\0\0\0\0\0\n\0\0\0\0\0\;\G

    このクエリはINSERTしているものですが、64.81qps出ています。また、同時並列数5になっていて、更新頻度も高くなっています。このようなクエリが大量に走り、IOに限界が来ていたり、行ロック待ち多発等つまりが見られるようであれば、まとめてINSERTできないかどうか、非同期でINSERTできないかどうかなどの検討もできますし、テーブルを他のシャードに移動させるということも検討できます。
    このような対応は一例ではありますが、これぐらい詳細なデータがツールから得られるので、対応がしやすくなるというのが雰囲気からでも伝わって頂ければ幸いです。
    このように、高負荷時のデータを平常時と比較することで、負荷の全体像を把握することができます。もちろん、binlogを集計することで更新クエリの比率などを見ることはできますが、実行時間の分布や並列処理数含めた詳細なデータが簡単に手に入るので、個人的にはこの方法を好んで使っています。

    また上記の方法のみならず、ピグではDB前段にあるキャッシュにヒットしたかどうかもトレースできるように、ログからの解析も行っています。どのテーブルに対して挿入・更新したか、読み込みしたかをピグではログに残しており、ひまたぎなど一時的に負荷が高騰し、その後沈静化してしまうようなものに関しても、どのテーブルへの参照・更新が多いかを分単位でログから集計し、それをもとに対策をおこなえるようにしています。

    たとえば、以下はある特定の日のDBオペレーションのログとなります。第3フィールドが「テーブル名」で、第4フィールドが「キャッシュにヒットしているかどうか」になります。これらの項目を使うことで、前日や前週と比べて、クエリが急増してないかどうか、キャッシュヒット率は下がってないかどうか等を見ることが可能となります。

    14:54:00.000    1       table1      Y       01dc5468        [ServerWorker-49]
    14:54:00.000    1       table2      Y       62617369635f657874725f617369616e627265657a5f686f7573655f6f725f31333033 [ServerWorker-6]
    14:54:00.004    1       table3      Y       786d61735f635f636f736d657365745f626b5f31333132  [ServerWorker-35]
    14:54:00.004    1       table4      N       00737b0041      [ServerWorker-49]
    14:54:00.004    1       table5      Y       62617369635f657874725f6e6f6d616c5f636f636f6e7574735f747265655f31333032 [ServerWorker-6]
    14:54:00.004    1       table6      Y       00af4bf6        [ServerWorker-11]
    14:54:00.004    1       table7      Y       786d61735f635f636f736d656261675f626b5f31333132  [ServerWorker-35]
    14:54:00.004    1       table8      Y       00af4bf6        [ServerWorker-11]
    14:54:00.004    1       table9      Y       617369616e5f7461626c655f726f756e645f31303132    [ServerWorker-6]
    14:54:00.004    1       table10     Y       01dc5468        [ServerWorker-49]

    フォーマットも決まってますので、テーブルごとのキャッシュヒット率を出すワンライナーを一つ作っておけば、平常時と比べてどのような変化があったのかが見られます。このようにして、誰でも調査しやすい環境も整えています。

    2.2. DB冗長化

    DBマスターサーバは、Webサーバやアプリケーションサーバに比べると、障害時の影響が大きく、冗長化をしておかなければなりません。ピグではリリース時にホットスタンバイのサーバは用意していましたが、ホットスタンバイ構成の場合切り替えまでのタイムラグが発生するためどうしてもサービスのダウンタイムが発生してしまいます。そのためデータベースの冗長化を行うためにMHAの導入を進めました。その際、以下の様な項目を事前に検討・検証し、本番環境導入を進めました。

    • ヘルスチェックの方法と間隔の検討。具体的にはSELECT 1を3秒に一回発行して確認する方法でいいのかどうか
    • 様々な状況でのMHAによるフェールオーバーテスト(デーモンstop, kill -9, OSシャットダウン, FIO強制デタッチ)
    • 切り替えまでの間隔の確認

    ヘルスチェックの方法と間隔の検討ですが、SELECT 1を3秒に一回投げ続けるという点、そしてヘルスチェックを複数環境から行う必要があるかということを主に検討しました。SELECT 1であることはチーム内で異論は特になく、このクエリが失敗するのであれば、明らかにDBサーバとの通信で影響が出ており、アプリにも支障が出ているという点でこのヘルスチェックをデフォルトのまま採用しています。加えて、複数環境からヘルスチェックを行うかに関しては、MHAマネージャーからのヘルスチェックでエラーを返しているようであれば、アプリからの参照もうまくいっていない可能性があり、複数環境からはヘルスチェックせず、MHAマネージャーからの監視のみでよいという判断をしました。現状、1年半ほどこの方針で運用していますが、現状何も問題は起きていません。

    また、ヘルスチェックが失敗する状況を再現するため、デーモンを停止させたり、kill -9コマンドでプロセスを殺したり、想定しうる様々な方法を試しました。いずれの方法を試しても、フェールオーバーが成功したので、有事の際も検知は問題なくできるだろうと判断しました。
    これらをチーム内で検討し、その上で本番に準じた環境を作成し、実際にDBを落としてアプリケーションの実際の挙動の確認を行いました。その際DBCPの設定が正しくできておらず、プール内のコネクションの死活監視はしていても、すぐ切り替えられるような設定にはなっていなかった等の問題が判明したためその部分を修正し、本番に導入を行うことが出来ました。

    具体的には、DBCPの「timeBetweenEvictionRunsMillis」というパラメータを用いることで定期的にプール内コネクションの状態を監視することができます。この監視を行う専用のスレッドがプール内のコネクションの有効性を確認し続けていますので、アプリは正常にコネクションを使用することができます。
    また、有効性を確認できない場合はプールからそのコネクションを破棄し、正常にコネクションのみプール内に存在するように処理します。しかし、本番に準じた環境でテストした際に、その監視間隔が10分になっていたため、フェールオーバーに伴うコネクションの異常を検知できず、問題を抱えたコネクションを破棄せずに使いまわそうとしていました。
    そのため、最大10分にわたって異常なコネクション(フェールオーバー前のDBに接続しようとするコネクション)を使い続けた結果、アプリ側で接続異状によるエラーログが出力され続けていました。その対応として、監視間隔を10秒にし、問題のある状態をすぐ検知し、フェールオーバー時も正常なコネクションを使用できるようにしました。

    MHA導入後、本番環境でマスターがリブートし、全く応答を返さなくなったことがありましたがその際も10秒ほどでスレーブに切り替わり、ユーザの体感的には遅延程度の影響で済むレベルになっています。

    2.3 データ量肥大化対応

    加えて、ピグでは一日に2~3GBデータサイズが増えており、容量圧迫の対策として、pt-online-schema-changeを使い、テーブルのデフラグをしています。

    以前はそこまで容量増加に関して意識を向けていなかったのですが、第3図で示したように、容量が徐々に増え、容量を圧迫し始め対策をしなければならないということになり、pt-online-schama-changeの導入を進めました。pt-online-schama-changeは弊社のサービスでも既に導入実績があったため、テスト環境での検証、途中で止めた際の挙動確認・対応などを調べ、導入をすることにしました。
    以下の図は特定のDBのデータ量の増加をグラフにしたものですが、昨年の4月には使用量40-45%ぐらいだったものが、7ヶ月後の11月頃には60%近くなっています。つまり、このペースで更に1年ほど経つと、容量はほぼ使い果たしてしまい、更新することができなくなってしまいます。

    第三図: DBのデータ量増加の推移

    そのため、テーブルのデフラグによりひとまず容量を確保することとなりました。もちろん、そのあいだに消せないデータがないかもチームで検討を進め、デフラグと不要データ削除の方針で対応を進めていきました。

    pt-online-schama-changeですが、他のチームでの導入実績が合ったとはいえ、いきなり本番マスターで実行するわけにはいきません。そのため、以下の様な項目を検証し、本番への導入を進めました。

    • テスト環境での挙動確認
    • 本番に準じた環境での動作確認
    • 本番スレーブでの動作確認
    • 本番マスターでの動作確認

    テスト環境では、実際にpt-online-schama-change経由でデフラグができることの確認のみならず、途中でキャンセルした場合の挙動確認、負荷の確認を行っています。その後、本番に準じた環境で確認したのち、本番スレーブでも確認を行い、マスターと同スペックでの負荷の確認を行いました。スレーブではレプリケーションにより更新クエリも流れているため、書込み量なども参考にできます。そのような情報をもとに本番マスターでも稼働させられると判断し、実際に実行しました。また、その際もデータ量の少ないテーブルから処理をするようにし、データ量が多いテーブルへの処理はイベントなどが少ない時間に行い、ユーザへの影響を配慮した上で本番導入をしました。

    以下の図が対応を入れた際のディスク容量の図です。65%ぐらいまで達していた容量が55%ぐらいに達し、その後再度実行し50%近くまで削減できています。

    第四図: pt-online-schama-change実行前後のデータ量の変化

    以下がデフラグ前後の容量(単位はキロバイト)の変化です。参考までにデフラグの処理にかかった時間も記しておきました。

    デフラグ前容量 デフラグ後容量 差分 処理にかかった時間
    25845881830920-7536683m10.157s
    3338240333824445m53.351s
    1648645613889604-25968524m38.501s
    109863914109342980-520934159m31.411s
    12575216476177972-49574192199m27.108s
    1011724880644-1310801m18.705s
    13025361175560-1269761m27.022s
    19947761884172-1106042m18.840s
    25968801597448-9994323m34.345s
    53617162834460-25272569m5.613s
    54354084456476-9789329m54.702s
    58286403842048-19865929m7.552s
    59638442785280-31785646m17.178s
    67339005615648-11182525m30.092s
    73154926193192-112230010m4.802s
    103998606672412-37274489m19.385s
    106660287725112-29409166m40.966s
    120055049138308-286719629m7.796s
    249653686656080-1830928813m29.364s
    2648891226452168-3674435m16.178s
    4429865235119232-917942038m52.427s
    7664875277013576364824110m15.918s
    10764737272106488-35540884104m58.513s

    第一表: pt-online-schama-change実行前後の各テーブルのデータ量の差異

    まれにデフラグが効果的ではなく、容量が減らないテーブルもありますが、一般的には効果があるようです。30GB以上減らせるものもありましたので、ピグの場合は非常に助かりました。処理時間に関しては、容量がかなり多くても、2時間あればピグでは問題なく処理が終わってました。

    また負荷に関しても、200MB/secぐらいの書き込みが発生するときもあり、通常時より数倍書き込み増えてはいましたが、ioDrive使ってることもあり、安心してコマンド発行しています。現在のところ全く問題なく使えており、ユーザ影響もないため、非常に重宝したツールとなっています。もちろん、不要なデータを削除するなどの抜本的な対策は必要ですが、長年運用しているとデータサイズは大きくなってしまうので、pt-online-schema-changeはピグには欠かせないものとなっています

    終わりに

    この記事ではアメーバピグにおけるDB構成と、どのように日々抱える問題と向き合ってるかについて、簡単ではありますが、述べてみました。

    クエリ調査やDB冗長化などはどのサービスにでも当てはまる内容でしょうし、データ量肥大化もサービスによっては同じような状況になっているものもあるかもしれません。

    いずれにしても、ピグでは様々な意見をもとに、上述したようなアプローチで本番導入を進めることができています。まだまだ不十分な点はあるかもしれませんが、少しでも参考にして頂き、問題に取り組むための判断材料にして頂ければ幸いです。不明な点等あればお問い合わせ頂ければと思います。

    Viewing all 161 articles
    Browse latest View live