分散システムの「キホン」の「キ」 - あるいは普通のWebアプリの作り方

みなさん、分散システムというと何を思い浮かべますか? Hadoopですか? Sparkですか?

現代だと多くの人がそういったイメージを持つと思いますが、調べてみるとそれらは厳密には分散「コンピューティング」に属する技術のようです。

分散システムとはメインフレームの対義語つまり普通に我々が今使ってるほとんどのコンピュータですね。

当たり前すぎてふだんは「分散」って部分を忘れがちなんですが、これを意識してシステム開発をしないと思わぬところで落とし穴にはまります。

という分けで、今回はシステム開発をするにあたって、分散システム—つまり普通のWebアプリのようなシステムを組む際に、どういう所が問題になりやすいかを考えてみました。

分散システムの特徴

たくさんの処理を同時に実行するのに向いてる

分散システム、すなわち現代的なコンピューティングは「同時」に「複数の事」を処理するのにとても向いています。

たとえばRailsJavaEEのようなWebアプリケーション。秒間100件のスループットを考えたときに「0.01秒間の処理を100回実行するシステム」と「1秒の処理を10個同時に実行するシステム」なら断然後者の方が簡単に作れます。

特に最近は「スケールアップ」と言っても実際はCPUのコア数が増えるだけのこともありますし、台数を増やすことでスケールアウトするのも一般的です。例外はいっぱいありますが。

なので、基本的には「超速い処理」を作るよりは「まあまあの速度を大量に実行する」方向にミドルウェア等も進化してるので、それを意識した設計をすることが大事です。

早く到着したからと言って、最初に発生した順とは限らない

では、同時に処理を実行したさいの弊害はなんでしょうか? その一つは順番が狂う事。つまりイン・オーダー実行が保証しきれないことです。

ネットワークの遅延だったり、負荷分散システムのバランシングの都合だったり、様々な理由で順番がずれます。

これを考慮せずにシステムを組むと… チケットの予約よりキャンセル処理が先に来たりなかなか楽しいシステムができます。

ほとんど同じ時間に処理されることもある

上記の話と少し似てますが、ほとんど同じ時間に処理されることもあります。ミリ秒以下の単位で。

これで何が困るかというと雑な乱数やタイムスタンプをキー情報にするとキーが重複するってことです。

乱数のデフォルトシードがタイムスタンプな言語は多いと思うので、乱数を使ってるつもりで重複への品質がタイムスタンプと同程度だったというのは良くある笑えない話。

排他制御はあるけれど…

他にもデータの更新処理は要注意です。二重更新問題によるデータ破壊が起こります。

そういったことを防ぐお馴染みの方法の一つが上記のページに記載さているような「ロックによる排他制御」です。

排他制御は言ってしまえば並列処理をシリアライズしてシーケンシャルに処理するので、並列処理にまつわることが大体解決しますが当然性能が落ちます。

特に、カウンターとか合計値の計算とかはロック待ちになり、台数を増やしてもマシンリソースが余っていても全然性能が伸びないってことになりがちです。

基本的な対処方法

では、上記の特徴を踏まえて基本的な対策というかシステムの作り方を紹介していきます。

下記のような構成のシステムをベースに考えていきます。

|Front App| -> (バランサ) -> |Back-end API| -> |RDB|

到着順の問題はどう解決すればいい?

到着順とイベントの発生順がずれる問題はいくつか対処方法がありますが、ひとつはイベントが発生した時間(Event Time)を値として持って置き、少し間をおいてソートしなおして実行することです。

この「少し」って範囲はシステムごとに異なります。1秒程度なのか1分なのか1時間なのか1日なのか、業務的に許容できることろは異なってくるでしょう。メモリでもRDBでも一時的に格納してしまい、ウインドウ集計をすることで問題を回避できます。

たとえば、「登録」と「キャンセル」が非常に頻繁に行われる場合はBack-end APIでは到着順に「予約管理一時テーブル」にデータを突っ込むと実際のタイムスタンプと異なるイベントタイムで格納します。

この際重要なのはポイントは何らかの方法で「イベントタイム」をパラメータに含めておくこと。

予約管理一時テーブル:

予約IDユーザID操作TimestampEvent Time
1Shiroキャンセル00:0100:02
1Shiro登録00:0200:01
2Emiya登録00:0300:02
3Emiyaキャンセル00:0400:03
4Bob登録00:0500:05

この状態で予約管理一時テーブルから、「例えば5秒毎に予約IDで抽出してEventTimeでSortして上から順に結果を適用した結果を予約管理テーブルに格納する」みたいな処理を作れば、到着順がばらけてしまうようなケースでも不整合なく処理が出来ます。

予約管理テーブル:

ユーザIDステータスTimestampEvent Time
Shiroキャンセル00:0600:02
Emiya登録00:0700:03
Bobキャンセル00:1000:05

また、ユーザや別のシステムは「予約管理テーブル」を見る事で5秒遅延した状態ながらも正確な状態を見ることが出来ます。

閲覧者にとって厳密なリアルタイムのステータス更新が必須でなければ十分にとれるアプローチですね。

また、難易度は上がりますが、5分の枠を1分ずつずらすとかでウインドウを重ねて集計すれば精度はかなり高くできます。

それでも業務上許容できないときは、例えばFront App側がBack-end APIに投げる時に同期処理として順番に投げるとかクライアント側での工夫が不可欠になります。

ただし、性能を犠牲にせざる得ないので、ウインドウ集計で作って万一問題が出る場合は、別途整合性チェックの処理を走らせて業務的に回避するとかバッチで再計算するとかが出来れば検討したい。

被らないキーはどう作る?

さて、分散システムで以外にめんどくさいのがユニークキーの生成です。

一見するとタイムスタンプで良さそうですが、単純なタイムスタンプでは十分に並列度があるとわりと被ります。特に精度がミリ秒だともう普通に被る。

なので、別な方法を考える必要があります。一つはRDBのシーケンスなどを使ってユニークキーを作る方法。これはRDBを利用したシステムだと非常に手軽なのでIDとかに使いやすいですね。

ただし、シーケンスの発行がRDB側でボトルネックになる可能性が大いにあります。Oracle RACであればCache + NOORDERである程度改善出来ますし、他のDBでも似たような改善方法はあると思われます。

それでも、一カ所でコントロールというのは限度がありますし、シャーディングしたDBに一意に書きたいこともあるので、そういったときはアプリ側で実装します。

  1. 独自で組む(ナノ秒 + メモリサイズをシードとした乱数 + スレッドID + MACアドレス の組み合わせとか)
  2. UUIDやSnowflakeなどの既存の仕組みを使う

1のように独自で組むというのも一つの手です。環境や状況が搾れるので比較的軽量なロジックを組むことが出来る可能性があります。また、2で記載してるように分散環境でもユニークキーが求めれる仕組みを使うのも手です。

特にUUIDは色々な言語での実装がすでにあると思うので、導入が手軽なのは魅力的です。

RDBでのUPDATE文でのロック待ち対策

RDBでUPDATE文で更新が重なるとロック待ちになるのは仕方ないです。これを改善することは出来ません。 なので、UPDATE自体を極力減らしロック対象も小さくする事で「ロック待ちが重なる」という事そのものを減らすのが基本戦略です。

正規化しよう

まずは、ロック対象を減らす話。

カラムを大量にもったテーブルの場合、ロックが必要なカラムがアプリによって違う場合もあります。これは単純にテーブルの設計が悪いですね?

なので正規化を検討しましょう。正規化をしてロック対象を小さくすることで、ロック待ちが競合する可能性を減らすことが出来ます。

たとえば、以下のようなテーブルを考えます。

ユーザ:

名前フレンド件数サーバント数
koduki3090

上記のような作りにしてしまうと、フレンド申請とガチャが被ると同じ行に更新ロックが掛かってしまいます。

なので、下記のようなテーブルとします。

ユーザ:

ID名前
1koduki

フレンド数:

ユーザIDフレンド数
130

サーバント数:

ユーザIDサーバント数
190

こんな感じで、3つのテーブルに分けることでフレンド申請とガチャで更新ロックが被ることは無くなります。

UPDATEからINSERTに切り替える

次に検討するべきはUPDATE文からINSERT文に切り替えることです。

そもそも「UPDATE文でロック待ちになる」ということは何等かの共通データであり、多くの場合それは「合計値」や「件数」といったサマリテーブルです。

このようなテーブルは下記のようにINSERTベースのテーブルにして、SELECT時に集計するという方法が考えられます。

たとえば、以下のようなテーブルを考えます。

フレンドに呼ばれた回数一覧:

名前回数
ステンノ3
エウリュアレ5
メデューサ1

この場合、参照時のSQLはシンプルに

select * from フレンドに呼ばれた回数一覧;

ですね。ただ、これだと同時にフレンドから呼ばれた場合はロック待ちが発生していします。人気サーヴァントだと相当時間がかかってしまいますね。

なので下記のようにします。

フレンドに呼ばれた履歴一覧:

名前
ステンノ
ステンノ
ステンノ
エウリュアレ
エウリュアレ
エウリュアレ
エウリュアレ
エウリュアレ
エウリュアレ
メデューサ

この場合、参照時のSQLは下記のようになります。

select 名前, count(名前) as 回数 from フレンドに呼ばれた履歴一覧 group by 名前;

いずれも、得られる結果は

名前回数
ステンノ3
エウリュアレ5
メデューサ1

なので、業務的には等価ですね? INSERT方式の場合はロックが発生しません。*1

このように実際に参照するモデルとは違う格納モデルにして、性能を稼ぐことが考えられます。

ただ、この作りは性能が出ないケースがあります。最近のDBは十分に速いので意外とそのまま行けるケースも多そうですが、性能が出ない場合は下記を検討します。

  1. Materialized View
  2. サマリテーブル + (Trigger or バッチ)
  3. (1 or 2) + 直近データだけ集計して合計する

1, 2の方法はgroup_byの計算結果をデータとして保持しておくパターンです。書き込みと参照で若干の不整合が許されるなら、これだけで十分です。

厳密な整合性が必要な場合は、3の直近の1分間とか1時間とか性能にインパクトを与えない範囲のデータだけトランザクションテーブルから集計して、サマリテーブルの値と合計する方式になると思います。

システムとしてはちょっと複雑になっちゃいますが、この方法であれば厳密性と並列性(性能)を両立できる可能性があります。

まとめ

分散システムという事に注目して、ふつうにWebアプリ等を書くときにスケールしなくなるポイントを整理してみました。

スケールさせるにあたって非同期キュー入れたりするケースとかも多々あるでしょうが、そもそも根底としては今回書いたようなケースを意識する必要があるんじゃないかと。

まあ、あんまり課題と例が適切じゃない気もするけど、そこはイメージで(ぉ

他にも色々あるでしょうし、「それは違うよ!」ってのがあればご指摘/コメントいただければと。

それでは、Happy Hacking!

*1:索引のロックが掛かるケースはあるのでRDBに応じてパーティション化等が必要になる事も

Hadoop の時代は終わってないけど、使いどころは限定されてきたかもしれない

id:shiumachi さんが書かれてる下記の記事がとても良かったです。 shiumachi.hatenablog.com

私自身もSparkを触る前は「Hadoop == MapReduce」と思ってましたが、どちらかというとYARNやHDFSHadoopファミリの核だと最近は思いますし その意味でのHadoopは全然終わってないと思います。記事の中で書かれてる通り、ある意味ではさらなる進化を遂げて花開いてる状態かな、と。

ただ、Twitterにも少し書いたんですが一方で「猫も杓子もHadoop!」の時代が終わりつつあるのも事実かな、と思います。

もうちょっというとHadoopに限らず巨大スケールの分散システムの用途が収斂してきたのかな、と。

HadoopGoogle MapReduce登場時のと違い、ストレージI/Oは133MB/sとかの単位で争ってたHDDからストレージに代わって、今や3GB/sを超えます。しかもランダムアクセスもHDDよりずっと速くなっている。

ちょうど最近発表されたZenベースのサーバだと64コア/128スレッドで、メモリも4TBです。普通に当時の10倍以上のマシンスペックがある訳です。単純に。

pc.watch.impress.co.jp

また、そういったハードウェアスペックに合わせてインメモリDBやクラスストレージメモリ、あるいはGPUを活用する形にRDBなどのミドルウェアも進化しています。

それこそ2010年からしばらくは「デカいデータ処理(あえてビックデータとは呼ばない)は分散システムでやるべし、すなわちHadoop!」って感じだったと思っています。

これは本来Hadoopがターゲットにしているビックデータ(TB級やPB級)ではなく数百GB級でも普通に組んだら性能足らなくて仕方なく使ってたケースがあるかと。

そういった用途で使われていたHadoopファミリは徐々に利用されない形になっていくんじゃないかと。

例えば、3GB/sのI/O性能が出るなら、1TBのデータ読み込みですら5分と少しです。

数TBのサイズであればインメモリDBにデータがすべて乗ります。

数十から百程度の並列処理なら分散しなくても単体マシンで処理可能です。

こうなってくると機械学習や特大のDWH以外はTB,PBクラスの処理をすることは少ないと思います。特に歴史がある会社だと昔から運用してるので業務的に圧縮してるはずだから、通常業務のデータはそんなにでかくないはずなんですね。日次とか月次でサマったり。じゃないと元々運用できないですし。

なので、そういった業務に対して単純に利用者が増えるとかの線形なデータ増加に対する対応だとHadoopのような巨大な分散システムを運用する必要性は小さくなってきます。

もちろん、冗長性やローリングアップデートのためにもシステムが多重化されてる事は大事なんですが、数百台のクラスタで動かすことを想定した分散システムではなく、 それなりに信頼できる数台のマシンで構成した小さなクラスタを運用する方向性が強まってくるんじゃないかな、と。

ただ、強まってくるんじゃないかと思いつつもRDBに分投げる方はともかく、分散処理エンジンであるSparkに代替するような並列処理エンジン/管理システムがあんまり無いのも困りもの。

自分が知る限りだとそのコンセプトを強く打ち出してるのはAsakusa-on-M3BPくらいですね。

RDBをインメモリで運用できてそっちに処理をオフロード出来るケースなら、あまり問題ないのですがそうじゃない場合は「複数スレッドをマネージするバッチの処理基盤」自体はやはり必要です。

何も考えずに各々のアプリが好きなサイズのスレッドを取得して使うと、ジョブスケジューリングが適切にできなくなってしまいます。

なので、Asakusaのような思想でのミドルがもう少し増えて、運用ノウハウが溜まっていけばな、と思ってます。

あるいはMesosやk8sでリソースを別途管理する基盤を作るかですね。JavaEEのjBatchもそういう方向で活用が出来そうだけど、ぶっちゃけ登場が遅すぎて機能とエコシステムが弱いのがネックですねぇ。

この辺は決定版が無さそうだから管理ツールとか含めて自作する必要があるかも。

まとめ

なんか途中から大分脱線した気がしなくもないけど、Hadoopの時代は終わってなものの「本当はHadoop使うには微妙だけど他に選択しないからHadoop」って用途がHWやMWの進化で、 適切なシステムに移行していくので、結果としてHaoopのような大規模分散クラスタは特定の用途に寄るだろうなってのが言いたい事でした。

それではHappy Hacking!

参考:

Javaの分散トレーシングとかAPMとか実行時ログとか

メモ的まとめ

APM

blog.takipi.com

www.itcentralstation.com

http://www.hawkular.org/hawkular-apm/www.hawkular.org

github.com

分散トレーシング

http://zipkin.io/zipkin.io

http://opentracing.io/opentracing.io

ログ系

builder.japan.zdnet.com

docs.oracle.com

http://icedtea.classpath.org/wiki/HeapStats/jpicedtea.classpath.org

www.slideshare.net

Weblogic(ECID)

www.slideshare.net

blogs.oracle.com

形式手法関連のメモ

ソフトウェア工学の道具としての形式手法

形式手法に関して歴史的かつ網羅的にまとめてあった

OpenJML

上記でも触れられてたJava向けの拡張静的チェッカ? ECS/Java2の後継? Java8対応らしいので試してみる

机上の Kubernetes - 形式手法で見るコンテナオーケストレーション

形式手法はソフトウェア設計に適用するものって凝り固まってたけど、 考えてみれば状態遷移やルール記述の塊であるインフラ構成やフローにも適用できるよね。目から鱗

ccvanishing.hateblo.jp

www.slideshare.net

EJB コンポーネントアーキテクチャの SPIN による振舞い解析

Spinを使ったEJBコンポーネントの記述事例。分散システムの記述にSPINは良いらしい

ゼロから始めるdocker生活 ~ VT-Xなんてない

さて、タイトルで誤解されそうですがこの記事はDockerの入門記事ではありません。

現代的なツールチェインが使えない異世界な気分に浸れる開発環境を与えられたとあるプログラマーの異世界訪問記みたいなものです。

現代的な開発環境を作る上で欠かせない存在。その一つはDockerです。

サーバ環境と同等の開発環境をサクッと作れるのは開発者はもちろん開発環境や運用環境を構築する担当としても有用なものです。

でも、そんなDockerが使えなかったら? 今回はそんな開発環境に迷い込んだ時のお話。

※ この物語はフィクションです。登場する人物・団体・名称、エピソード等は架空であり、実在のものとは少ししか関係ありません。

開発環境? もちろんWindowsだよ!

今日から新しい職場です。プロジェクトの初期メンバとして開発環境も作らないとなのでがんばるぞー。

え、開発環境はWindows? MacLinuxではない。。。? 動作環境はLinuxなのに? まあ、そうですよね。

でも、そんなのは慣れっこです。Vagrantを使う開発手順にしましょう(※1)。

サーバに上げないと開発できないとかツラいし、Cygwinとかで作ると細かい差で本番で死ぬからね!

という分けで基本的にはIDEで開発/テストをし、Bashを含んだような全体の動作確認はVagarantで。

共有フォルダを設定しとけばデータ連携もスムーズだし、chefやserverspecでSTG/PRD環境との互換性も保ちやすくて良いね!

※1 当時はDockerは出たばかりで仕事で使うにはちょっと。。。 な時代

ごめんね? 今日からあなたの開発環境はシンクライアントなの。

「みなさんに大事なお話があります」

偉い人は言いました。

「今日からみんなシンクライアント(VDI)! いいね?」

そして開発環境もVDIになりました。

「マウスを高速に動かすと画面がちらつく!」「キー入力にラグがある気がする!」

そんな声もありましたが、私はあまり気になりませんでした。

むしろ、元のマシンがHDDな残念な子だったので、SSDになったし画面も3日もすれば慣れたから問題は無いかな? という気分でした。

そう。Vagarantが動けばね!

夢の32bit OS

32bit OSをご存知でしょうか?

今までのシステムより圧倒的な大容量なメモリ。素晴らしいグラフィック。そしてセキュアで堅牢な作り。

時代の要請に答えて圧倒的に進化したOS、それが32bit OSです!

. . .

およそ25年ほど前の話なら、ね。

私は選択しました。Vagrant上で動かすシステムを32bitにすることを。

だって、VDIは仮想環境だからVT-Xが使えないんです。つまりVirtualBoxとか普通に使ってる仮想環境だとVT-Xの利用を前提にしてる64bitOSは動かない。

Nested Virtualization はなかなか難しいし。。。

幸いにして32bitでも必要なライブラリはそろいました。JavaがベースなのであまりOSレベルの差は気になりません。

というわけでゲストOSを64bitから32bitにダウングレードすることで妥協ラインに。

そう。。。いつ32bit版のライブラリが無くなるかという爆弾を抱えたまま、私たちは走り始めたのです。

マスター、一番いいのを頼む

困った。メモリが足りない。

JavaIDE立ち上げて、アプリケーションサーバ立ち上げて、DB立ち上げて、Excel立ち上げて、Chrome立ち上げてたら

4GBぽっちのメモリはすぐに枯渇してしまいます。

あと、CPUもたりない。

マルチスレッドでの性能向上の検証とか2コアでどうしろと。。。 サーバと開発機の性能が違いすぎるの困りもの。。。

困ったときは相談です。とりあずVDIの担当者に話に行きます。

「メモリ、増やせるよ?」

!? あなたは神か。。。 工夫とか特にしなくても問題解決☆ 富豪プログラマー万歳です。

というわけでCPUを8コアにメモリを16GBにとりあえず増やしてもらいました。

よし、普通に動く。自分だけだとあまり意味がないので、担当者と話して申請手順も整備、これでOK.

再起動しただけでメモリ増えるなんて、なんだVDI以外と出来る子じゃない。

先生…!! Dockerがでドヤ顔したいです……

やがて時は過ぎ環境構築の主流はVagarantからDockerに移っていきました。

自宅ではDockerで開発環境をいろいろ作り、便利さはある程度分かってきたつもりです。

そうなると、やはり仕事でも使いたいのが人情。でもVT-Xが使えない。。。

Dockerは基本的に64bit Linuxが、前提。無理すれば32bitでも作れそうな記事も見かけたけど、DockerHubとかのエコシステムにまったく乗れない。

ぜんぜん。。うれしく、ない。。。

virtualboxvmwareもVT-Xを前提に64bit化を実現します。32bitなら遅くてもVT-Xで動くのにこの格差は何なんだ。。。

そう思いながらもんもんとしていたある日、ふと閃きました。

Bochsqemuがあるじゃない!」

天啓でした。Android端末の上でWindowsを動かすことにしか最近使われてない気がするBochsqemuですが、KVMとか使わなければ「普通のアプリケーションとして」フルエミュレーションをするはずです。

さっそくダウンロードです。boot2dockerのISOも落としておきましょう。

$ 'C:\Program Files\qemu\qemu-img.exe' create -f qcow2 harddisk.qcow2 32G
$ "C:\Program Files\qemu\qemu-system-x86_64w.exe" -m 4G -cdrom boot2docker.iso -name boot2docker -drive file=harddisk.qcow2,media=disk,cache=writeback  -smp 2 -netdev user,id=user.0 -device e1000,netdev=user.0

動いた! 無事クジラさんが見えました! docker psやdocker runも打てるよ!

そして…

さて、無事にVT-Xが使えないシンクライアント上でもDockerが動くようになりました。圧倒的 進歩!

ただ、まだこれで終わりではありません。qemuって重いんです。知ってました?

これでどこまで現実的に開発に利用できるかは試してみるしかありません。

docker-machineとの連携も調べないといけません。genericsとかでやれば動くでしょうか? そもそもボリュームどうしましょう?

DockerRegistoryどうしようとか考えることは山盛りあります。

残念ながら、異世界開発環境で「おれつぇー」が出来るのはもう少し先のことになりそうです。

あとがき

さて、たんにdocker on qemu without VT-xが出来たので記事にしようと思ったら、何故かこんなものを書いてました。

夜勤明けにビール飲みながら見てるインターネッツは怖いですねー。

ゼロから始める魔法の書』を見ながら、ふとタイトルを思いたのが運の尽きですが、結果として出来たタイトルはどちらかというとリゼロなのが不思議です。

そのうち「docker on qemu without VT-x」関連を抜粋してQiitaに投稿しておきます。

それではHappy Hacking!

Oracle Exadataは分散システム?

はじめに

最近、Oracle Exadata/RACを触る機会がありました。

個人的にはOracle RAC自体まともに触るのが初めてだったので、お約束のキャッシュフィージョンを起こして性能が出なかったりとか、RAC初心者あるあるを起こしたりもしました。

その時に、ExadataやRACの公開情報をあらためて見てたのですが、Exadataは分散システムとして理解したほうがしっくりくるなぁ、という事に気付きチューニングもその観点を持ったほうが良い気がしたので、少しまとめてみました。

Oracle Exadataとは?

Exadataは簡単に言えばOracleが考えた「僕が考えた最強のRDB」です。

RDB製品としてトップクラスの実績を誇るOracle Databaseとそのクラスタ構成であるRAC(Real Application Clusters)をボトルネックなく最速で動かすための当代最速のマシンで組み上げたアプライアンスです。

各ノードはInfinibandという40Gbpsを誇る高速な回線で、TPC/UDPよりもオーバーヘッドが小さいRDMAを採用。ストレージもSSDマシマシでメモリもたくさん載せる。CPUは当然大量に。というRACボトルネックになりそうなところをキチンとつぶした構成になっています。

加えて、ソフトレベルで色々チューニングが施してあって、単に同じハードでOracle Database Enterprise Editionを実行するよりも数倍速いってのが特徴です。

購入はこちらから! って言ってこのURLがアフィリエイトだったら私がウハウハできる程度にはお高いのがネックでしょうか。

Exadata Database Machine | Oracle

まあ、Oracle Enterprise入れることを検討する程お金があるなら一考の余地はあるでしょうけど。あと、クラウドもあるみたい。

Oracle RAC(Real Application Clusters)とは?

Oracle RACOracle Databaseをクラスタリングし、性能と可用性を高めるための製品です。その最大の特徴はShared Everything.

MySQLPostgreSQLの良くあるクラスタと違ってストレージも共有してますし、キャッシュも共有しています。つまり計算するDBエンジンだけを分散化してるのが特徴です。

正直、2から数台程度のスケールだとこれが一番面倒は無さそうというのは納得です。ただ、後述する通り「厳密にはExadataの構成とは違う」ってのが注意点。

f:id:pascal256:20170326182338p:plain

ここでRACのポイントとなるのがASMとキャッシュ。ASMはAutomatic Storage Managementの略で、その名の通りストレージの自動管理をしてくれる仕組みみたい。大分誤解がありそうだけど自分の理解としてはDB特化型のソフトウェアストレージですね。

DBで直接ブロックデバイスを扱わずにソフト的な抽象レイヤーを作る事で、耐障害性をあげたりと色々な事をする機能。これがExadataでは大活躍します。

つづいてキャッシュだけど、これの注意点はすべてのDBサーバで共有されてるということ。

つまり良くあるmemcache的な分散型キャッシュシステムとして動いてるのではなく、Netflix Hollowみたいな散在型キャッシュシステムです。

この仕組みであれば参照時のローカリティが常に保たれるため効率的ですね。半面、書き込み時にキャッシュを共有する必要が出てきます。これがキャッシュフィージョンです。

通常は、キャッシュフィージョンのコストが参照時にキャッシュヒットせずにストレージアクセスするコストより小さいという分析から、こういう仕組みになってるのでしょう。

f:id:pascal256:20170326185057p:plain

当然、これが頻発すると性能が落ちます。特に同じキャッシュの場所を更新するとコンフリクトの解決が必要になるので目に見えて遅くなります。

私のようにRAC構成のOracleが分散システムであるという事を忘れてInsertをしまくると痛い目を見ます。ただ、解決方法もあってハッシュパーティションとかでDB上の更新箇所を散らしてやるのが効果的です。このあたりのチューニング方法は下記の記事が分かりやすかったです。

実はシンプル!RACチューニングの考え方(PDF)

ExadataとSmart Scan

Exadataの最大の特徴といえばSmart Scanでしょう。下記の記事が詳しいです。

Smart Scanを簡単に言ってしまえば「インデックスを張らずにフルスキャンをしても高速にレスポンスを返す」ための仕組みです。

どやってるかというと発想は単純で、ストレージサーバ側で簡易な検索を行いデータを小さくした後にDBサーバに返してDBサーバ側でJOINなど複雑な処理をする、というものです。

これによってInsert時のIndexにまつわるコストを大幅に下げつつ、性能を上げることが出来ます。

f:id:pascal256:20170326191604p:plain 引用: Oracle Exadataはなぜ速い?圧倒的なパフォーマンスの秘密を徹底解剖

データが大きくなればなるほどストレージからDBサーバにデータを持ってくる通信コストは無視できなくなりますし、分散してりるストレージサーバ上で処理すればビックデータに対しての性能が稼ぎやすくなります。

このあたりはHadoopGoogleのBigQueryと近しい思想だと思います。

さすがにPKとかでKey Valueで採ってこれるような処理よりは遅いみたいですが、バッチシステムとかだったら十分効果ありってのがOracle側の見解です。

さて、この辺のExadataの記事見てたら気づくと思うのですがRACの説明と大きく違うところがあります。そう、ストレージが1個じゃねーじゃん、と。

ExadataとRACと分散ストレージ

RACの特徴はShared Everythingでストレージが1つしかないことです。しかし、RACを採用してるはずのExadataの特徴は複数のストレージサーバに処理をオフロード出来ることです。この差は何でしょうか?

色々あるのですが「Exadataで動いてるシステムは厳密にはRACじゃない」と理解しておくのが一番楽そうです。RACをベースとした「クラスタシステム:Exadata」と思っとく方が無難ですね。

たしかに、RACはソフトウェアにすぎませんし、そこから見る単一のストレージシステムってのは「ASMで抽象化されたストレージ」くらいの意味しかないと思います。

その先がNASやSANであれば「個人向けのNASのようにハードディスクを直結した単一にぶら下がるストレージ」であろうが、「GlusterFSのような分散ストレージ」であろうがどちらであっても「単一のストレージ」です。OracleやEMCのNASやSANも中身が複数台で構成されてるってのは普通にあるでしょうし。

DBエンジン毎に違うストレージが刺さってないってのがポイントでしょう。Exadataはその構成上、分散ストレージになってると思いますがそれだけであればASMが頑張れば良いだけです。

ただ、SmartScanを考慮すると話が違います。どんなにASMが頑張ってSQL処理だけ特別な処理をするように透過的に要求を処理しても、selectの結果として返ってくるレコードの量が減ってるとかDBエンジン側でも考慮しないとストレージのバグとして処理される気がするんですよね。

このあたりは同じEngineered製品であるBig Data Applicanceと組み合わせた時の振る舞いが顕著です。

BDAにはOralce Big data SQLという面白い機能があって、これはExadataのSmartScanからExadataのストレージサーバのようにHadoop(Hive)を取り扱うものです。 f:id:pascal256:20170326194836p:plain

引用: Why Oracle Big Data SQL Potentially Solves a Big Issue with Hadoop Security

この際、OracleのDatabaseサーバ側からみると、ExadataのストレージサーバもBDAのHadoopノードのも等価として扱われるとのことです。

その意味でもSmartScanとそれにまつわる仕組みは単一ストレージとして考えず、分散システムとして考えた方がしっくりきます。

まとめ

Exadataはアプライアンスなので単一システムとして売られていますが、本質的には分散システムでありHDFSのような分散ストレージを使った製品であると理解したほうが良さそうです。

Netfix hollowHDFSに似てるところがあるからこんな感じの理解ですかね?

f:id:pascal256:20170326200907p:plain

こうしてみると商用RDBの最先端であるOracle DatabaseもAWS Auroraを始めとしたNewSQLと同じような方向性の進化を進めてることが良くわかります。

アプライアンスはハードウェアも込みでアーキテクチャが練られてるところが良いですね。

この辺はOracleがSunを買ったメリットの一つかなぁ。HPには災難だろうけど自社で完結するほうが良いよね。

それではHappy Hacking!

参考

いつのまにかGitのshallow cloneが”Push”も"Pull"もできるように超進化していたよ! すごーい!

流行りに乗ってフレンズ用語を活用しようと思って、タイトルで力尽きたブログはこちらです。

はじめに

不向きと言われながらもgitでGBを超える巨大リポジトリを取り扱うことはあります。 そんなときの強い味方が Shallow Clone

これは任意の世代だけを持ってくるので、どうせ滅多に参照しない1年以上前の履歴とか持ってくる必要がない便利な機能です。

なんですが、最近まで私の中では「shallow clone(笑)」な印象でした。

というのも、Gitのshallow cloneには制約が多く、fetchが出来ないのでputもpushも使えずせいぜいがCIサーバでのビルド時くらいだろうと思ってました。

実際、昔は出来なかったし今でもブログとかでも何だかんだで古いものがヒットするから、同じ印象だった人もそれなりにいるんじゃないかな? と思います。

Facebookが改造したというMercurialのshallow cloneはfetchが出来るので、うらやましく思ったものです。

www.infoq.com

でも、実は、Git 1.9から変わってた!。下記のAtlassianの記事でもさりげなく言及されてました。さりげなさすぎるよ!

japan.blogs.atlassian.com

blogs.atlassian.com

1.9が出た当時だから2014年以降のgitはshallow clone後もfetchが出来ますが、その結果どうなるかはあまり明言された記事も見つからなかったので確認してみました。

基本的なpushとpull

元になるリポジトリの作成

まずは検証用のリポジトリを作ります。

$ echo "# sandbox-git_shallow-clone" >> README.md
$ git init
$ git add README.md
$ git commit -m "first commit"
$ git remote add origin https://github.com/koduki/sandbox-git_shallow-clone.git
$ git push -u origin master

以降の作業が分かりやすいように全履歴をもつ通常のリポジトリfull-repoとしてcloneします。

$ git clone git@github.com:koduki/sandbox-git_shallow-clone.git full-repo
$ cd full-repo/

full-repoにいくつかcommitをしてみます。

$ touch history.txt
$ git add history.txt
$ echo "history1" >> history.txt
$ git commit -m "history commit 1 from full-repo" -a
$ echo "history2" >> history.txt
$ git commit -m "history commit 2 from full-repo" -a
$ echo "history3" >> history.txt
$ git commit -m "history commit 3 from full-repo" -a
$ git push

当たり前ですが下記のように履歴が出ています。

$ git log --pretty=oneline
7539bfe549690c037c172aec11c1b764424ee101 history commit 3 from full-repo
886bbd29a664c388473494af38689b362a7adf34 history commit 2 from full-repo
f8024e169867ca6c2ce3731ede8c2f87de8841da history commit 1 from full-repo
17404e2207096399f2e15a7176665dd62f5f4821 first commit

shallow cloneリポジトリの作成とPush

続いて検証のためにshallow cloneをdepth=2くらいで、shallo-repoとして作成します。

$ git clone --depth 2 git@github.com:koduki/sandbox-git_shallow-clone.git shallow-repo
$ cd shallow-repo
$ git log --pretty=oneline
7539bfe549690c037c172aec11c1b764424ee101 history commit 3 from full-repo
886bbd29a664c388473494af38689b362a7adf34 history commit 2 from full-repo

ちゃんと2世代分だけがとれたことが確認できました。ではshallow cloneした側でも同様に適当なcommitをしてみます。

$ echo "history4" >> history.txt
$ git commit -m "history commit 4 from shallow-repo" -a
$ echo "history5" >> history.txt
$ git commit -m "history commit 5 from shallow-repo" -a
$ git log --pretty=oneline
3f1dcc459ab3e105d0e10fba007e1beb693ab277 history commit 5 from shallow-repo
4a034d35a5893150308a9bd73d07370cd5dc971b history commit 4 from shallow-repo
7539bfe549690c037c172aec11c1b764424ee101 history commit 3 from full-repo
886bbd29a664c388473494af38689b362a7adf34 history commit 2 from full-repo
$ git push

とりあえず、エラーもなくPushもできました。

full-repoとshallow-repoでの相互のpull & push

では元の全履歴を持ったfull-repoでpullできるかを確認します。

$ cd ../full-repo/
$ git log --pretty=oneline -n1
7539bfe549690c037c172aec11c1b764424ee101 history commit 3 from full-repo
$ git pull
$ git log --pretty=oneline
3f1dcc459ab3e105d0e10fba007e1beb693ab277 history commit 5 from shallow-repo
4a034d35a5893150308a9bd73d07370cd5dc971b history commit 4 from shallow-repo
7539bfe549690c037c172aec11c1b764424ee101 history commit 3 from full-repo
886bbd29a664c388473494af38689b362a7adf34 history commit 2 from full-repo
f8024e169867ca6c2ce3731ede8c2f87de8841da history commit 1 from full-repo
17404e2207096399f2e15a7176665dd62f5f4821 first commit

pullする事で、ちゃんとshallow cloneした側のコミット内容が取り込まれましたね?

当たり前ですがshallow cloneした側の履歴で上書きされたりせず、元の履歴もきちんと保持しています。

念のため、全履歴を持つfull-repoからshallow-repoへの反映も確認します。

$ echo "history6" >> history.txt
$ git commit -m "history commit 6 from full-repo" -a
$ git push
$ cd ../shallow-repo
$ git pull
$ git log --pretty=oneline
e94867ec121d779cc234441cd9045fa03494d2e1 history commit 6 from full-repo
3f1dcc459ab3e105d0e10fba007e1beb693ab277 history commit 5 from shallow-repo
4a034d35a5893150308a9bd73d07370cd5dc971b history commit 4 from shallow-repo
7539bfe549690c037c172aec11c1b764424ee101 history commit 3 from full-repo
886bbd29a664c388473494af38689b362a7adf34 history commit 2 from full-repo

問題なくcommitが反映されてますね。相互にcommitしあっても何の問題も無さそうですね。これなら他のブランチの修正もmasterマージ後に取り込める

まとめ

今回はgitのshallow cloneについて検証をしました。

正直「これが出来たらなぁ」と思ってた機能が3年近く前から実装されてたのは衝撃ですが、これは中々に便利がよさそうなので活用していきたいです。

ちなみに私の手元の環境では数十分cloneにかかってたのがshallowだと数分になりました。たのしー

しかし「git 巨大リポジトリ 対処」とかで調べても昔の情報ばかり出てくるのは残念。

それではHappy Hacking!