Hatena::ブログ(Diary)

めもおきば このページをアンテナに追加 RSSフィード

Contact: akitan@gmail.com
noteにて投げ銭受付中 ⇒ 【投げ銭】こたろー写真

2014-12-05

Ameba等で利用しているOpenStack Swiftを利用したオブジェクトストレージ

CyberAgent エンジニア Advent Calendar 2014 5日目です。

5日目は、インフラ&コアテク本部の@nekoruriが担当します。

私たちが所属するインフラ&コアテク本部は、「(^q^)くおえうえーーーるえうおおおwwwwwwwwwwwwwww」でお馴染みアニメ放映中のガールフレンド(仮)アメブロを初めとするAmeba、755などグループ会社に対して、最適化されたサービスインフラとその運用ノウハウを提供する組織です。自社開発のプライベートクラウドの開発・運用だけでなく、パブリッククラウドやCDNなどの外部サービスの活用支援や、サービスに関するセキュリティプロセスなど、「サービスを動かす基盤」全体の向上に責任を負っています。

今日は、インフラ&コアテク本部が提供しているサービスの一つである、AOS(Airforce Object Storage)と呼ばれるオブジェクトストレージについて紹介します。

AOS: Airforce Object Storage

以前エンジニアブログで紹介した画像配信基盤とは別に、変換など画像に特化した機能がない代わりに汎用に使えるAOSという汎用データストアを運用しています。

スマホ専用電子コミックサイト「読書のお時間です」や「アメーバピグ」などで使われているほか、自社のネットワーク内にあり、通信費を掛けずに大量に高速の転送が可能という利点を活かし、各サービスのバックアップデータなども保管しています。ざっくり比較すれば、某氷河系ストレージほど安くは無いが柔軟に利用でき、某低冗長化ストレージよりは安価で気楽に利用できる、という感じの位置付けを目指しています。

こんな感じの目的を実現するため、クラウド基盤OSSのOpenStackに含まれるSwiftをベースに、キャッシュや管理機能を追加したものがAOSです。

AOSの全体像がこちらです。

f:id:nekoruri:20141205201037p:image

OpenStack Swiftを中心に、キャッシュとパス変換のためのVarnishを前段に置き、それらを管理する管理画面を自作しています。また、独自の認証基盤へ繋ぎ込むため、Keystone等は利用せず、管理画面からswauthへの同期を行っています。

VarnishマジVarnish

静的なファイルの配信と言えばキャッシュです。あらゆる段階でキャッシュをどれだけ活かすことができるかに、サービスの品質やコストが直結します。AOSではVarnishによるキャッシュを標準で提供することで、そういった要求に対応しています。

データストアの人柱として有名な弊社ですので、もちろんVarnishのバックエンドストレージとしてもpersistentを利用しています。他のmallocやfileではvarnishdを再起動するとキャッシュデータが消えてしまうのですが、persistentであれば永続化されているためカジュアルにrestartできます。とはいいつつ、persistentはやむを得ない理由が無ければ使うなという状況になっているため、今後はどうしようかなというところです。

特徴的な使い方としては、AOSへはいくつかのホスト名を付与していて、ホスト名ごとにVarnishのキャッシュポリシーを変更しています。例えば、さらに上位にAkamai等のCDNを挟む場合は、中間層でのキャッシュがトラブルの元となる可能性があるため、一切キャッシュをしないホスト名を用意しています。簡単なVCLでこのような柔軟な対応ができるのもVarnishならではですね!

認証にswauthを利用しているため、Swiftそのままだと、URLに

http://example.jp/AUTH_12345678-1234-1234-1234-1234567890ab/file.jpg

のような「AUTH_」で始まるアカウントの識別子が含まれてしまい、ちょっと格好悪いです。そこで、Varnishに変換ルールを仕込み、管理画面からその変換ルールを設定できるようにしています。また、キャッシュのパージも管理画面から実施できます。このように、開発者がインフラエンジニアの手に頼らず自力で設定できる環境を整備していくのも、インフラ&コアテク本部のミッションの一つです。

ちなみに、varnishには「うわべだけ取り繕う」「ごまかす」という意味があるそうです。今知りました。

細かすぎて伝わらないSwiftのTIPS

まさかのGREEさんとのネタ被りですが、オブジェクトストレージの根幹となっているのがSwiftです。

Swiftは、とにかくHTTPちっくなAPIでファイルを置けてパーミッションとかも良い感じに掛けられてサーバ増やせばいくらでも冗長性・拡張性が確保できる夢みたいなストレージで、ぶっちゃけるとAmazon S3みたいな奴です。国内だとGMOインターネットこのはちゃん採用しているのでご存知の方が多いと思います。

今更Swiftの紹介とか書いても、SwiftStackのドキュメントより分かりやすいものができるわけもないので、AOSの担当になってから今までに得た細かいTIPSを書いていきます。

ゾーン追加とレプリカ数

Swiftはデータの冗長性を確保するため、「極力遠くに同じデータ(標準では3つのレプリカ)を保存する」という仕組みになっています。この「遠さ」を表現するために、以下のような階層が用意されています。

  1. Region (最上位)
  2. Zone
  3. Node (1IPアドレスで特定される1サーバ)
  4. Disk (サーバ上のマウントポイント)
  5. Partition (データの保存単位)

全てのデータは、Partitionという単位でまとめられて、それがどこかのRegion、Zone、Node、Diskに分散してばらまかれる事になります。この分散先を決めるときに先ほどの「極力遠くに」というポリシーが前提となるわけです。例えば1サーバ1ディスクだけのテストサーバであればその1ディスクの那珂に全部入りますし、たくさんのリージョンに別れた広域分散環境であれば、良い感じに分散してくれるというわけです。

──さて。

AOSでは当初の設計として物理サーバを2つのラックに設置しており、ラック単位でのネットワークスイッチや電源の故障に備えるため、1ラック=1 Zoneとして構築しました。その後、順調に利用量が増えて1ラック分サーバを追加することになりました。サーバ追加するだけで簡単にキャパシティも増える夢のストレージですよさすがですね!ただ、いくら追加すれば良いとはいえ、初の大規模拡張なので、何かあってもすぐ切り戻せるように少しずつ追加していきたいところです。

とりあえず検証してみました。そりゃもうloopback mount生やしまくりで数だけ合わせて1GBのディスクをなんと合計200本以上も作りました。追加しました。


f:id:nekoruri:20141205201038p:image



f:id:nekoruri:20141205201039p:image



そう、Zoneをまたいで分散しろというプレッシャーが強すぎて、追加した1台のサーバに3つめのレプリカが集中してしまうのです。Swiftには、どのサーバにどれだけのデータを割り振るかを「Weight」として指定することができるのですが、Zoneを跨げという力の前には無力で、たとえweightを他のサーバの1/100にしていても同じ結果でした。

というわけで、最初のキャパシティプランニング段階でZone数はレプリカ数に合わせて設計しておき、Zone内でのサーバ追加で対応できるようにするのが良いです。

サーバ追加後のデータ同期

swift-recon というコマンドを使うと、全てのサーバ・全てのディスクごとに、利用率を比較することができます。

===============================================================================
--> Starting reconnaissance on 17 hosts
===============================================================================
[2014-12-05 19:59:46] Checking disk usage now
Distribution Graph:
 54%    1 *
 56%    2 ***
 57%    7 ***********
 58%   13 *********************
 59%   34 *******************************************************
 60%   42 *********************************************************************
 61%   26 ******************************************
 62%    8 *************
 63%   11 ******************
 68%    2 ***
 69%    3 ****
 70%    9 **************
 71%   10 ****************
 72%   17 ***************************
 73%   10 ****************
 74%    5 ********
 75%    3 ****
 76%    1 *
Disk usage: space used: 390692674875392 of 611703698153472
Disk usage: space free: 221011023278080 of 611703698153472
Disk usage: lowest: 54.01%, highest: 76.43%, avg: 63.8695950433%
===============================================================================

サーバを追加すると、0%のところに「*」が増えて、データ同期が進むにつれてじわじわ他の利用率に近寄っていきます。そこで、この出力をみんなだいすきGrowthForecastに流し込んであげればさらっと可視化できます。recon_growthforecast.shとして置いておきますので、興味がある方はお試しください。

あと、当然ながら、でっかいストレージだとデータ同期もでっかい時間が掛かります。

100TB/1Gbps=約9.3日、という計算式を置いておきます。

swauthのパスワードを調べる

swauthは、OpenStack標準の認証コンポーネントであるKeystoneを使わないときに用いるSwift独自の認証機構です。swauthはSwift内部のミドルウェアとして組み込まれ、認証情報をSwift自身に保存します。しかも驚きのplaintextで。そのため、swauthのパスワードを後から取得することができます。

swauthが認証に関する情報を保存するため、super_admin_keyというマスターパスワードを使います。このsuper_admin_keyというさも強そうなパスワードは、プロキシサーバの設定ファイルに書かれています。そのsuper_admin_keyを見つけられれば、あとは swauth-list コマンドが使えます。

# grep super_admin_key /etc/swift/proxy-server.conf
super_admin_key = PASSWORD
# swauth-list -K PASSWORD ca.amb.username ca.amb.username
{"groups": [{"name": "ca.amb.username:ca.amb.username"}, {"name": "ca.amb.username"}, {"name": ".admin"}], "auth": "plaintext:USERPASSWD"}

これでパスワードを忘れても大丈夫ですね!

コンテナ内のファイル数

という話題で書こうとしていたら、GREEさんとネタが被ったので省略します。

物理配置を調べる

どのデータをどのノードのどのディスクに保存するかは、リングと呼ばれるファイルで定義されています。swift-get-nodesというコマンドにこのリングファイルを食わせてあげることで、どこに物理的に配置できるかを知ることができます。

手順的には二段階で、まず対象のアカウントを調べます。

# swift -A http://127.0.0.1:8080/auth/v1.0 -U username:username -K USERPASSWD stat | grep Account
   Account: AUTH_12345678-1234-1234-1234-1234567890ab

続いて、コンテナ名を指定して、swift-get-nodes を実行します。

# swift-get-nodes /etc/swift/container.ring.gz AUTH_12345678-1234-1234-1234-1234567890ab containername
Account         AUTH_12345678-1234-1234-1234-1234567890ab
Container       containername
Object          None
 
Partition       219040
Hash            deadbeefdeadbeefdeadbeefdeadbeef
Server:Port Device      2001:db8:dead:beef::ffff:feff:1:6001 sde
Server:Port Device      2001:db8:dead:beef::ffff:feff:2:6001 sdj
Server:Port Device      2001:db8:dead:beef::ffff:feff:3:6001 sdi
Server:Port Device      2001:db8:dead:beef::ffff:feff:4:6001 sdb      [Handoff]
Server:Port Device      2001:db8:dead:beef::ffff:feff:5:6001 sdl      [Handoff]
Server:Port Device      2001:db8:dead:beef::ffff:feff:6:6001 sdm      [Handoff]
 
curl -I -XHEAD "http://2001:db8:dead:beef::ffff:feff:1:6001/sde/219040/AUTH_12345678-1234-1234-1234-1234567890ab/containername"
curl -I -XHEAD "http://2001:db8:dead:beef::ffff:feff:2:6001/sdj/219040/AUTH_12345678-1234-1234-1234-1234567890ab/containername"
curl -I -XHEAD "http://2001:db8:dead:beef::ffff:feff:3:6001/sdi/219040/AUTH_12345678-1234-1234-1234-1234567890ab/containername"
curl -I -XHEAD "http://2001:db8:dead:beef::ffff:feff:4:6001/sdb/219040/AUTH_12345678-1234-1234-1234-1234567890ab/containername" # [Handoff]
curl -I -XHEAD "http://2001:db8:dead:beef::ffff:feff:5:6001/sdl/219040/AUTH_12345678-1234-1234-1234-1234567890ab/containername" # [Handoff]
curl -I -XHEAD "http://2001:db8:dead:beef::ffff:feff:6:6001/sdm/219040/AUTH_12345678-1234-1234-1234-1234567890ab/containername" # [Handoff]
 
Use your own device location of servers:
such as "export DEVICE=/srv/node"
ssh 2001:db8:dead:beef::ffff:feff:1 "ls -lah ${DEVICE:-/srv/node}/sde/containers/219040/eef/deadbeefdeadbeefdeadbeefdeadbeef/"
ssh 2001:db8:dead:beef::ffff:feff:2 "ls -lah ${DEVICE:-/srv/node}/sdj/containers/219040/eef/deadbeefdeadbeefdeadbeefdeadbeef/"
ssh 2001:db8:dead:beef::ffff:feff:3 "ls -lah ${DEVICE:-/srv/node}/sdi/containers/219040/eef/deadbeefdeadbeefdeadbeefdeadbeef/"
ssh 2001:db8:dead:beef::ffff:feff:4 "ls -lah ${DEVICE:-/srv/node}/sdb/containers/219040/eef/deadbeefdeadbeefdeadbeefdeadbeef/" # [Handoff]
ssh 2001:db8:dead:beef::ffff:feff:5 "ls -lah ${DEVICE:-/srv/node}/sdl/containers/219040/eef/deadbeefdeadbeefdeadbeefdeadbeef/" # [Handoff]
ssh 2001:db8:dead:beef::ffff:feff:6 "ls -lah ${DEVICE:-/srv/node}/sdm/containers/219040/eef/deadbeefdeadbeefdeadbeefdeadbeef/" # [Handoff]

よく分かっているなと思うのが、Swift API(curl)でコンテナの情報を取ったり、対象サーバに接続してディレクトリをlsするワンライナーがどかどか表示されるところです。

例えば、もし仮に万が一1コンテナ内に1億ファイルくらい突っ込んじゃってコンテナのSQLite DBがどれぐらいのサイズに太っちゃったか知りたくなったときも、表示されれたssh行を実行すれば、すぐに該当ディレクトリをlsしてDBサイズを診ることができるわけです。素晴らしいですね。

ソースコード

Swiftの一番良いところは、Pythonで書かれていて、ソースコードにもすぐにアクセスができるところです。また、ソースコードも良い意味でそれほど大きくないため、LL言語に馴染みのある人であれば、ちょっとした動きに違和感があり調査が必要になっても追いかけることがで来ます。

Grizzlyのswift-ring-builderコマンドにはIPv6アドレスを受け取れない不具合があるのですが、ちょっと検索すれば正規表現を直すパッチが転がっていたりしますし、作業用サーバだけ手パッチ当てて動作確認みたいなこともすぐに可能です。ここまでカジュアルに深いところまで触りやすいデータストアはSwiftぐらいでは無いでしょうか。

「Swift」

どこかのアップルとか言うスマホの会社がiOS(IOS)に続いてど派手な名前被りをしてくれたせいで、「Swift」という語句のググラビビリティが大変下がってしまいました。諦めて「OpenStack Swift」で検索しましょう。

まとめ

Swiftは、OpenStackのコンポーネント群のなかでも、Rackspaceが商用サービスで使っていたものをベースとしているだけあって非常に枯れています。また、HTTP(REST)ベースということで内部のトラブルシュートもしやすいです。

うまく他のネットワーク環境との兼ね合いや機器構成を練らないと、パブリッククラウドオブジェクトストレージとコストだけで闘うのは厳しいですが、うまくユースケースにはまればとても便利な基盤になるので、どうぞご利用ください。

2014-12-03

Dockerfile for Tiarra

今日はTiarra

Tiarraのconfはちょっといろいろ分量おおいのでどうしようかと思ったけど、結局VOLUMEから注入する形であきらめた。まあ動的にいじってRELOADすることもあるだろうしこれはこれで良いのかも。

/data/conf 以下にconf置いて、docker run時にファイル名を指定です。

mkdir -p $HOME/tiarra/conf
vi $HOME/tiarra/conf/your.conf
docker run -d --name tiarra -v $HOME/tiarra:/data:rw -p 6667:6667 nekoruri/tiarra:0.1 your.conf

だんだんなれてきた。

2014-12-02

Dockerfile for RawGit

RawGit落ちてて自前でも欲しいって話が出たので早速作ってみる。

docker run -d -e FQDN=rawgit.example.jp --name rawgit -p 80:80 -p 443:443 nekoruri/rawgit:0.1

Vagrant用にbootstrap.shがあったのでそのままそれを利用してるので、そのままだとオレオレ証明書が勝手に作られてTLSでもサービス上がります。このあたりの整備は後ほど。

あと、12-factor appに従って、docker run -e FQDN=rawgit.example.jp でFQDNを注入できるようにしてみた。

2014-12-01

ミニマムなDockerfile for GrowthForecastつくってみた

自分が使いたくてミニマムなGrowthForecastのDockerfileが欲しかったので作ってみた。

とりあえずDocker hubに投げてみたけど、Buildingから進まないので細かいことはあとで書く

あと、いくつか考えたポイントをGistにぺらぺら書き残してる。

これだけでいける。

mkdir -p $HOME/vol/gf
docker run -d --name growthforecast -v $HOME/vol/gf:/data:rw -p 5125:5125 nekoruri/growthforecast:0.1

残り課題

  • VOLUME書き忘れた
  • [ok] Trusted buildからpullして動かすテスト
  • timezone

2014-09-26

ウェブアプリにおけるBash脆弱性の即死条件 #ShellShock

条件1. /bin/shの実体がbashディストリビューション

条件2. 動作環境

条件3. プログラム内容

  • Passengerは全死亡 *1
  • systemや `command`、 '| /usr/lib/sendmail' などで外部コマンド実行 *2
  • PHPのmailやmb_send_mail、その他フレームワーク等を介したメール送信 *3
  • 以下は条件1が不要
    • 明示的にbashを呼ぶ
    • 先頭で #!/bin/bash や #!/usr/bin/env bash しているプログラムを実行 (rbenv等)

補足: Passengerについて

Passengerは、デフォルトでRAILS_ROOT/config/environment.rbのファイル所有者の権限で実行されるため、一般的なインストール状況であればpublic以下にファイルを置いたり等なんでもできます。特に、パスワード無しでのsudoが可能なユーザでファイルを置いていた場合には、なんでも、できることを、手元では検証しました。

ワーカプロセスは使い回されるため、百発百中ではありませんが、一定のリクエスト毎にワーカプロセスを捨てて作り直すPassengerMaxRequestsが設定されていたり、アクセス量に応じてワーカプロセス数が変動するデフォルト設定であれば、繰り返しの試行により再現が可能と思われます。

*1:ワーカプロセス生成時に/bin/shを経由するため。参考: Security advisory

*2PerlRubyでメタ文字を含まないsystemなど直接execvp(3)するものはセーフ

*3:QdmailやSwift mailer等でSMTPサーバを指定して接続している場合はセーフ