自動テスト関係のメモ
gitの良さは未だに分からないがPullRequestの良さはちょっとだけ分かるぞい
スマートニュース見てたらこんな記事が上がってました。
まあ、gitの良いところはそれなりにありますがローカルコミットを上手に使えるようになるまでの壁はとても高いですよね。
正直言えば私もrebaseとかなるべく打ちたくないし、使いこなせてるとは言い難い。
とはいえPR(本文中はマージリクエスト)がレビューを強制するので良くないとか、trunkに直接コミットすれば良くないとかはどうなん? と思ったり。
そもそも個人的にはSVNとgitで運用方式がさほど変わるとは思えないのですよ。実際変えなかったし。
というわけで、私が使ってるコード管理方式について語りたいと思います。タイトルはちょっと釣り... というかぞいぞい言いたかっただけです!
そもそもSVNでどう運用していたか?
たぶん、多くのサービスがそうであるようにSVNからGitに移行しました。もっと言うとさらに昔はCSVでした。
それも、色々な理由でSVNから短期間でgitの運用らしくないところもありますが、今は当時無理やりgit-flowとか入れなくて良かったなぁとは思ってます。
じゃあ、そもそもSVNでどんな運用してたのよ? という話になりますがおおむね下記のような運用でした。
- trunkは常に本番環境と一致。リリース時にはタグを打つ
- 開発時にはプロジェクト毎にtrunkから開発ブランチを作る。これが同時並行で大量にできる
- 検証環境向けのブランチに開発ブランチをマージしてテストする。検証期間/環境の都合でリリース日の異なる複数のプロジェクトがマージされる事がある
- 3の前後でtrunkと開発ブランチとの差分をとってReviewBoardとかでレビューする
- 3,4の完了したら開発ブランチをtrunkにマージ。タグ打ってリリース
実のところ結果としてGitHub Flowに結構近い運用だと思っています。違うところは3の検証ブランチでしょうけど、検証ブランチ自体はどこにもマージされないので大差は無いんじゃないかと。 まあ、この手のベストプラクティスは似通ってくるものなので不思議ではないですね。
なんで移行したのか?
移行した理由は2つあります。一つはSVNがあまりにとろく、特にマージやDiffが地獄のような重さでした。普通に数分から十数分オーダー。
これはおそらく大量のブランチを運用してたり、用意されていたインフラと管理していたリポジトリ規模がアンマッチだったことがあります。
当時の私はレビューアーとライブラリアンも担当してたので、gitに移行前からsvn-gitとかを使ってローカルリポジトリを運用してました。パフォーマンスマジ大事。
もう1点はPRですね。会社で共通基盤としてStashを導入することは知ってたので、上記のパフォーマンスの理由もあって協力に推進しました。
Gitでどのように運用しているのか?
では、本題のGitでの運用方式です。正直、パフォーマンスがダメダメなだけで運用プロセスには大きな不満があった分けではないので、ほぼそのまま移行しました。
- masterは常に本番環境と一致。リリース時にはタグを打つ
- 開発時にはプロジェクト毎にmasterから開発ブランチを作る。これが同時並行で大量にできる
- 検証環境向けのブランチに開発ブランチをマージしてテストする。検証期間/環境の都合でリリース日の異なる複数のプロジェクトがマージされる事がある
- 3の前後でmasterと開発ブランチとのPRでレビューする
- 3,4の完了したらPRをマージしてmasterにマージ。タグ打ってリリース
外部システムのReviewBoardがPRになり、マージ依頼と統合されました。なにこれ便利すぎる! と狂喜乱舞したのは私です。
それ以外は、まったくと言って良いほど同一プロセスです。gitの操作が独特だったり複雑ってのもTortoiseSVNからTortoiseGitに変わっただけなので、あまり大きな変化はありません。
まあ「Pushを忘れるな!」と徹底するくらいですかね。rebaseでコミットログを綺麗にしようとすると、ドキドキする運用がいっぱい待ってますが「そんなことしなくて良いんです」。
確かに残念なログは入り込みますしが、良くわかってないメンバーが使ってリポジトリがぶっ壊されるよりはマシです。
別に、コミットログが多少汚れてても、リリースのタグだけ適切に管理できてればそんなに困りません。clone/checkout/pull/merge/commit/pushくらい使えれば十分ですね。
なので、これ以外の操作をするときは権限を与えた特定メンバーに依頼しろという運用にしています。ちょっと窮屈だけど、窮屈だと思える程度にスキルがある人には権限与えれば良いですしね。
で、基本的にこれで運用してましたが、いきなり5の手順でmasterに取り込んでリリースをしてましたが、切り戻しが大変というリスクがありました。
特に、一日に何度もリリースする時に朝のリリースがこけると午後のリリースの準備がパーになるのでツラい。
なので、現在はリリースブランチを作って下記のように運用しています。
- masterは常に本番環境と一致。リリース時にはタグを打つ
- 開発時にはプロジェクト毎にmasterから開発ブランチを作る。これが同時並行で大量にできる
- 検証環境向けのブランチに開発ブランチをマージしてテストする。検証期間/環境の都合でリリース日の異なる複数のプロジェクトがマージされる事がある
- 3の前後でmasterと開発ブランチとのPRでレビューする
- 3,4の完了したらmasterからリリースブランチを作成して開発ブランチを取り込む。3のPRは破棄。
- リリースブランチをリリース。無事リリースされたらmasterにマージしてタグを打つ
この手順にすることで、リリース失敗時の切り戻しが簡単になる上に、リリース前にリリースブランチをリグレッションテストできるので品質面でも貢献できました。
ちなみにこの改良版の手順も含めてSVNでできないのはPRくらいでしょう。これもReviewBoardとかの外部ツールで代替はできますが。
PRによるレビューはなぜ重要なのか?
元記事ではさんざん言われてるPRによるコードレビューだけど、これは超重要だと思っています。「あなたがgitに移行するべきたった一つの理由」とか言うそれっぽい記事のタイトルにできそなほどに。
重要な理由は2点あります。
1. 保守性の担保
元記事では「採用ミスの尻ぬぐいだ!」と言わんばかりのコードレビューですが、確かに読んでバグ見つけるよりは動かして見つけた方がずっと早いし合理的です。
が、コードレビューはバグ発見より可読性や設計レベルの保守性を主にチェックする行為だと考えています。コードは読み物なので第三者が読んで読みづらいのはダメです。
これは私やおそらく元記事の方を含むほとんどのエンジニアに必要な行為で、当然ですが自分より優秀な人が書いたコードのレビュー指摘なんていくらでもしたことがあります。
静的解析で基本的な部分はかなり潰れるんですが、設計レベルに由来するところは人が見ないと分からないですがねー。
一応、高いスキルを持ったエンジニアがお互いのコンテキストを良く理解しあった上で開発すればレビューとかいらない気がしますが、 そんな事例1ケースしか私は聞いたことが無いのと、それこそハイスキルな行為ですね。
2. 適切なマージがされるかの確認
これはPR最大の利点ですね。なんとgitはPR出た差分の通りにマージされるんです! ライブラリアン的な作業をしないと気づきづらいかもしれませんが、マージってのは「コンフリクトもせずに開発者の意図とは違うマージになる」可能性があり得るんです 。 gitはsvnよりずいぶんマシとはいえ、しょせんテキストベースのマージですからね。同じコードの一番上と一番したに同じメソッドを追加しても追加したら両方正しく取り込まれますけど、コンパイルエラーになりますよね?
これを防ぐにはSemanticMergeみたいなもう少しインテリジェンスなツールを使う必要があります。
通常は同じ行を修正してれば正しくコンフリクトしますが、コンフリクトの解決を適当にされると稀に「マージの時系列」や「同じ行としての判定」が狂う可能性があります。
この場合、何が怖いってSVNとかの場合は「Diffツールで見た結果と実際のマージ結果が異なる可能性がある」ってことです。せっかくレビューしたのにマージ結果が異なるとかツラい。
そのためリリース時のマージではマージした後に想定したマージ内容になってるかを差分確認という二度手間になっていました。
PRであれば、そもそもマージ申請なので見た目の差分通りに取り込まれるので、初回のレビューしっかりやるだけで良いので簡単ですし、安心感がありますね!
GitではなくSVNに回帰するべきなのか?
正直、管理をしない側がGitとSVNでたいそうな差があるとは思えません。
プロジェクト規模や体制にもよるでしょうが50%から80%くらいの人はrabaseがどうの以前にlogコマンドすら打たないんじゃないかな?
なので、管理者/ヘビーユーザに都合が良いGitから回帰するメリットは特に無いのだけど、全員がrebaseを使うような複雑な運用プロセスはやめたほうが良い気がしますね。
少数精鋭チームなら問題ないかもだけど、大半が初心者なら入れるメリットのがデメリットを上回らない気がします。
あと、これは私が「システムの整合性を保つ唯一の機械的な方法はリポジトリである」という単一リポジトリ教に入ってるせいもありますが、
SVNのようにディレクトリ単位で落とせてもいいんじゃないかなー、とは思ったり。あとshallow cloneにcommitできるとか。
正直、10GB級の巨大リポジトリを運用しようとするとcloneがHTTP Proxyに切断されるとか、サーバの方でハングするとかツラい事象が...
その辺を考慮したツールが欲しいなぁ。
余談
>コードレビュー導入は終局的には「コードはできるだけ書かない」という境地に至る。
本題とは本当に関係ないのだけど、元記事の人と意見が真逆で面白かったのでピックアップ。
「コードはできるだけ書かない」はプログラマーが目指すべき一つの境地だと思ってます。ここだけの秘密ですがコードを書くとバグが発生する可能性が上がるんですよ?
まとめ
元記事で言う「中規模以上のプロジェクトのリリースを本格的に管理する側」を経験した人間なので、ちょっとカウンター記事を書いてみました。
正直、10個も20個もあるそんなに短くないプロジェクトを並行運用するのをみんなどうやってるんだろう? という疑問をずっと持ってたので、 とりあえず自分の現状を公開してあわよくばコメントをもらおうという算段もあったります。
というか、そういうツラい運用したくないからリリーススケジュールをシリアライズして並行稼働開発をしないのが正しい組織運用かもしれないデス。
それでは、Happy Hacking!
技術的負債はリボルビング払いで返そう!
という記事がTwitterに上がってたので見てみましたが、大変共感できるものでした。
システム開発をしていれば技術的負債はつきものです。特にサービス運営をしていれば、「たとえ借金をしてでも今出すべき!」ってタイミングは良くあります。
技術的負債はあくまで価値を生む資産であって、有用なものなのです。返済しないといけないのと利子が付くだけで....
で、元記事の中で、「技術的負債を大きく返すパターン」「技術的負債を小さく返すパターン」が書かれていますが、これって実際の金銭的負債の考え方で言えばリボ払いですよね!
というわけで、今回は技術的負債を実際のクレジットカードの返済方式になぞらえて返し方を考えてみました。
主なクレジットカードの支払い方式
さて、なぞらえるからには前提の説明が必要でしょう。絵心無いですが簡単なイラストにしてみました。
大別すると以下の三つです。
- 一括払い : 利用日の翌月払いです。クレジットカードだと利子かかりません!
- ボーナス払い : ボーナス月にまとめて払う方式です。特定月しか支払わなくて良いのがポイント。
- リボ払い : 毎月決められた金額だけ支払う。ご利用は計画的に。
本当は他にも分割払いとかもありますが、正直技術的負債を分割払いで考えることは無いと思うので割愛。
技術的負債における支払い方式
さて、それではそれぞれの支払い方式で技術的負債を返済する場合を考えてみます。
一括払い
利用日の翌月とありますが、言ってしまえばプロジェクトリリース直後とかですかね。
リリース当日には無理してでも間に合わせる。出してから直せば良い。まあ、良く「考える」ケースですねー。
リリース直後に対応するのであれば、保守性の低下という手数料の支払いもゼロなので、悪く無い方式です。
一方で、それなりの支払い能力、すなわち余剰生産力が必要です。なにしろリファクタリングとか直接的には利益を生みづらい作業をするので、それをしても問題がない組織的な余裕が必要になってきます。
また、プロジェクト内で技術的負債が消化できないレベルのリリースの直後とか、どうせトラブるので人的リソースは枯渇し技術的負債は滞納されます。チーン
ボーナス払い
会社にもよりますが、繁忙期とそうでない時期があります。
そうでない月をボーナス月としましょう。この間に、たまった技術的負債を返すのです!
クレジットカードの支払い方式のイラストを見てわかる通り特定月以外は請求されないので、その分の生産力をトラブル対応や新たな機能の開発に充てることが出来るので、リソースを効果的に使いシステムやサービスの価値を高めることが出来ます。
でも、ちょっと待ってください! 本当にボーナスは支払われるのでしょうか?
年に2回しかないボーナス月です。それまでにたまった負債はとても一人が片手間に返せるものではありません。 私は社会人としてSEになって7年くらいになりますが、プロジェクトの切れ目なんて見たことありません。ましてやチーム単位で。
そう、多くの会社は(技術的負債返却という意味では)ボーナス月の存在しないブラック企業なので、技術的負債は滞納されます。チーン
リボ払い
さて、最後に悪名高きリボ払いです。
これは元金にかかわらず毎月決められた額だけ支払う方式です。これをシステム開発で言うなら一定のコストを常にリファクタリング向けに許容するという事になりますね。
人的リソースを消費しますが、そもそも支払い能力すなわち組織としてプロジェクト影響が許容できる範囲を決めて払いづづければいいのです。
カードでの支払い方式のイラストでも、固定の小さな額だけ払えば良いので負担はちいさそうですよね?
自らが決めてコントロールできるので、技術的負債を返済し続けることが出来ます。そう、つまり滞納しないんです! グレート
ただ、決めた量以上が減らないため、借金する速度の方が返済速度より早ければ元金は減るどころか増え続け、無限に支払いをし続けることになります。
でも、だから何だっていうんでしょうか? サービスが死ぬまで技術的負債を払い続ける覚悟をすれば良いだけじゃないですか! 死んだら払わなくて良いのです!
まあ、あんまり元金増えすぎると利子(保守性の低下)が溜まり過ぎて、死ぬんですけどね!
でも、多少は減り続けるだけ、他の方式よりましだとおもいませんか?
まとめ
さて、あおりっぽくタイトルは「技術的負債はリボルビング払いで返そう!」としましたが、他よりマシってだけで、これだけやればOKという方式でもないかなと思います。
実際には本当の金銭の負債と同様に支払い能力に余裕があるうちはプロジェクト期間内または直後の一括払いを前提として、 手元にキャッシュ(人的リソース)を残してほかの生産的な作業に充てたいときはリボ払い方式を採用しつつ、 ボーナス月が来たらおまとめ払いで元金を可能な限り減らすというのが現実的でしょう。
個人的には技術的負債はずっと付き合っていかなければならないものだと思っているので、計画的に支払い続けれるリボ払い方式を採用できる体制にしていきたいなと思っています。
まあ、カードのリボ払いは手数料高いからしたくないけどね!
それでは、Happy Hacking!
分散バッチフレームワークとしてのApache Spark
ヌラーボさんのGeeks Who Drinkで分散バッチフレームワークとしてのApache Sparkというタイトルで話させていただきました。 nulab.connpass.com
資料はこちらになります。 Apache Spark as Batch
最近、Sparkを触り始めたんですが、世の中の資料は機械学習やログ解析がほとんどで、普段作ってるようなジョブをあんまりどう書くかが議論されていません。 ただ、SparkのAPIはScalaのコレクションAPI。つまり、map/reduce/filter/foreach/groupby等による普通の近代的なコレクション操作APIです。 なので、普通の業務バッチで長時間掛かってるはずやつにも適用できるはず、という観点でこの資料を作ってみました。
個人的にはSparkおよびHadoopシステムはバッチシステムとしての完成度というか作りこみが凄く、管理系の機能が豊富なのでそこまでビックなデータじゃなくても基盤として有用だと考えています。
また、今回はあまり触れてませんがJavaからもふつうに使うことが出来るので、ScalaエンジニアがいなくともJavaエンジニアだけで開発できることも大きなポイントですね。
アクティブアクティブでスケールアウトするバッチサーバを検討中なら多少オーバーヘッドがあることを加味してもありだと思いますね。 DAGの可視化と各処理の実行時間がわかるのはかなり便利だし。
ただ、新しいミドルの運用という別タスクが増えるので、このトレードオフがペイするかは要件等ですね。
ちなみに、他の発表でWebクリエイターボックスの@chibimanaさんがリモートワークについて話されてました。 リモートワークとは少し違いますが、離れた拠点と仕事することは多いので、なるほどなー、と感じることも多くてとても興味深い話しでした。
あと、LTされたKAGURAっていうソフトがかなり面白そうでした。 私も音楽センス0なので、そんな私でもそれっぽい演奏ができるんだろうか? という夢と、MIDIがつながるのでジェスチャーで色んなデバイスを触るためのUIとしても 使えるんじゃないかなー、とおもったり。というわけでbackingはしてみました。ワクワク
Geeks Who Drinkは勉強会というわけではなく、食べ飲みだべりメインでなんかあれば発表もする、というスタイルみたいですね。 これはこれで面白いので、機会があればまた行ってみたいなー、と思います。
それではHappy Hacking!
参考:
書き初めコーディング! Docker + CGI + COBOLな環境を作って温故知新
さて、今年の書き初めコーディングは「温故知新」ということで、古いものと、とても古いものと、最近のものを組み合わせてみました。 というわけで、CGI + COBOL + Dockerという異色組み合わせをしてみました。エンジニアは「最新の技術」ではなく「最適な技術」を押さえる必要があるので、どっちも使ってみないとです。
とりわけ「CGI」は忘れ去られた便利仕様だと思っています。「CGI」と聞くと多くの人は「Perl」「PHPの前の技術」「RailsとかJavaServletのことでしょ?」と色々なイメージがあると思います。 Webアプリケーション黎明期を支えた技術であり、今となってはPHPやRails、JavaEEに取って代わられた技術です。
しかし、「CGI=Perl」というわけではありません。CGIは「Common Gateway Interface」。その名の通り、Apacheと外部システムを連携させる「共通仕様」であり、 標準入力と標準出力を備えた言語...いえ、実行可能モジュールなら、なんでも動きます。Perlはもちろんですし、JavaやRuby, あるいはC言語やBash、そしてCOBOLおばあちゃんだって大丈夫です。
単なる標準入出力のフックなので、JNIとかより圧倒的に手軽で、LinuxコマンドにWeb I/Fを付けるなど、多くの場面で役立ちます。
そして、今回のターゲットは古代言語の代表格「COBOL」! Linux実装にはOpenCOBOL(現GnuCOBOL)を使います。 さらに、これらをDockerに包んでどこでも使えるポータブルなCOBOLのREST環境を作ります。
まずはこちらに出来たものが。
環境としてはDocker Toolboxが入ってることを前提としています。Windowsで利用する場合はVolume周りを修正してください。
まずは単純にCOBOLのHello Woldを実行してみましょう。
000100* HELLO.COB GNU Cobol FAQ example 000200 IDENTIFICATION DIVISION. 000300 PROGRAM-ID. hello. 000310 DATA DIVISION. 000320 WORKING-STORAGE SECTION. 000400 PROCEDURE DIVISION. 000500 DISPLAY "Hello, World!". 000600 STOP RUN.
ことごとく最近はやりの言語と語彙が被ってませんね! とは言え読めばわかる通り「DISPLAY」が表示です。あとは全部オマジナイです(ぉ
実行はdockerでやります。
$ git clone https://github.com/koduki/example-cobol.git $ cd example-cobol $ docker-compose run app cobc -x -o bin/HELLO ./src/cobol/HELLO.COB $ docker-compose run app bin/HELLO Hello, World!
Dockerはデーモン(サービス)のコンテナとして使うのが通常だとは思いますが、こういうインタラクティブなコマンド実行にも使えるので、その用途も結構便利だったりします。
続いて、CGIとして実行します。
$ docker-compose run app cobc -x -o bin/APP ./src/cobol/APP.COB $ docker-compose up
APP.COBはCGI用に書いたCOBOLで下記のような形をしています。見よう見まねで書いてみたので、COBOLっぽくなかったらごめんなさい。
000100 IDENTIFICATION DIVISION. 000200 PROGRAM-ID. APP. 000300 DATA DIVISION. 000400 WORKING-STORAGE SECTION. 000500 01 ARG1 PIC 9(2). 000600 01 ARG2 PIC 9(2). 000700 01 RESULT PIC 9(4). 000800 PROCEDURE DIVISION. 000900 ARGS-INPUT SECTION. 001000 ACCEPT ARG1 FROM CONSOLE. 001100 ACCEPT ARG2 FROM CONSOLE. 001200 MAIN SECTION. 001300 COMPUTE RESULT = ARG1 + ARG2. 001400 JSON-OUTPUT SECTION. 001500 DISPLAY "{arg1:" ARG1 ",arg2:" ARG2 ",result:" RESULT "}" . 001600 JSON-FIN SECTION. 001700 STOP RUN.
ACCEPTが標準入力です。標準出力にはJSONを返しています。
ここで、CGIに詳しい方なら「あれ?」と思ったはずです。 そう、このCOBOLはCGI仕様ではありません! (ぉ
ここで、少しCGIのI/Fを説明します。CGIはかなり単純で、出力が下記フォーマットなら、なんでもHTTPに乗せることが出来ます。
Content-type: text/html # text/planとかもOK # 改行 # 本文
Content-typeを書いて、改行して、本文を入れるだけ。実にシンプル。なのですが、何故かCOBOLで書いたらうまく動きませんでした... 謎ですが、仕方が無いので、以下のようなBashで包んで、Conetent-type部分を補完しています。
#!/bin/bash args=($(echo $QUERY_STRING|sed s/\&/" "/g|sed s/=/" "/g)) ### cgi echo "Content-type:application/json" echo echo -e "${args[1]}\n${args[3]}" |/app/bin/APP
単純に、echoでContent-typeと改行を出力しているだけです。 GETパラメータなどはQUERY_STRINGという環境変数に渡されるので、そちらを分解して標準入力としてCOBOLに渡しています。
これで準備は整ったので、ブラウザで下記URLにアクセスしてみましょう。 ※ docker-machine ip が 192.168.99.100 の場合
http://192.168.99.100/app.cgi?arg1=3&arg2=2
ちゃんと、足し算の結果が返ってきましたね? ちなみに性能としては特にチューニングしなくても1秒あたり100リクエスト程度の性能でした。 コネクション数を増やしても性能が上がらなかったので、Apache側でシリアライズされてる気がしますが、これはちゃんとチューニングすれば問題ないでしょうし、 Linuxコマンドや特殊な言語で書いたプログラムをAPI的に呼びだすだけなら、まあ十分な性能な気もします。
これで、どこでもCOBOLライフを楽しめるようになりましたね! あれ? 全然嬉しくないのはなんでだろう...
ちなみに今回は試しませんでしたがCGIは単純にApacheの1機能として振る舞いますので、HTTP2とかも動かすことができる気がします。 さすがに、Websocketは難しいでしょうけど。こういう既存のノウハウが活かせるところもCGIの良いところですね。
運用系コマンドのWeb I/FにもBashベースで作ればかなり向いてる気がします。
ただし、1点注意があって、社外からアクセスできる場所に置くときには十分に注意してください。Bashがその筆頭ですが、Webで鍛えられてないので脆弱性の塊です。ご利用は計画的に。
というわけで、古い技術の中にも便利なものはあり、新しいものと組み合わせることで更に生きる、ということを胸に今年も頑張りたいと思います!
それでは今年も、Let's Hacking!
さよならスティッキーセッション!PayaraでJavaEEでもセッションをKVSに。
この記事は Java EE Advent Calendar 2015 の 9 日目です。 昨日は btnrouge さんの「Payara Microのからくり」でした。
意図せずしてPayaraネタ連続です。
はじめに
Payara MicroはSpringBootやWildfly Swarmあるいは一日目に紹介されていたKumuluzEEと同様にJavaEE向けマイクロサービス環境です。 javaコマンドで単純に実行できるDockerフレンドリーな奴ですね。
先に上げたMWと一番の違いはPayara MicroはEJBを組み直してfat-jarを作るわけではなく、最小構成のコンテナでwarをデプロイして動かす、ということです。 具体的には下記のようなコマンドで実行します。
$ java -jar payara-micro-4.1.1.154.jar --deploy example-payara_micro.war
payara-micoro.jarがシンプルに起動だけする感じですね。単一jarでは動かないとい点はありますが、ギャップが小さい分、不具合が圧倒的に少ないですね。 Wildfly Swarmなんかは色々アグレッシブな分、まだ安定してない感じがしますが...
通常のwarと開発方法が同じなので、開発時はNetBeans + Payaraで作って実行時はPayara Microみたいな開発が楽な気がします。
さて、そんなPayara Microですがもう1点重要な点として、JavaEEの特徴の一つであるスティッキーセッションを要求されません! GlassFishを始め、JavaEEではインメモリセッションレプリケーションが基本になっており、原則同じサーバへのアクセスが要求されます。 しかし、PayaraではKVSであるHazelcastを組み込んであるため、どこのサーバへのアクセスも透過になるのでスケールアウト構成がシンプルに構築できます。
LL系だとKVSにセッションを格納するのは珍しく無いですが、JavaEEだとOracle Coherenceを担ぎ出す必要があったので大きな一歩ですね。JCacheはHTTP Sessionをカバーする仕様ではなさそうですし。
準備
下記からbootstrapになるpayara-microのjarファイルを落とします。現時点での最新版は下記からダウンロードしてください。
実践
それでは実際に試していきたいと思います。まずは、mavenプロジェクトで今回は作成したpom.xmlは下記の通り。
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>cn.orz.pascal</groupId> <artifactId>example-payara_micro</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <name>example-payara_micro</name> <properties> <endorsed.dir>${project.build.directory}/endorsed</endorsed.dir> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.16.6</version> </dependency> <dependency> <groupId>javax</groupId> <artifactId>javaee-web-api</artifactId> <version>7.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>fish.payara.extras</groupId> <artifactId>payara-micro</artifactId> <version>4.1.152.1</version> <scope>provided</scope> </dependency> </dependencies> <build> <finalName>example-payara_micro</finalName> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.1</version> <configuration> <source>1.8</source> <target>1.8</target> <compilerArguments> <endorseddirs>${endorsed.dir}</endorseddirs> </compilerArguments> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>2.3</version> <configuration> <failOnMissingWebXml>false</failOnMissingWebXml> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <version>2.6</version> <executions> <execution> <phase>validate</phase> <goals> <goal>copy</goal> </goals> <configuration> <outputDirectory>${endorsed.dir}</outputDirectory> <silent>true</silent> <artifactItems> <artifactItem> <groupId>javax</groupId> <artifactId>javaee-endorsed-api</artifactId> <version>7.0</version> <type>jar</type> </artifactItem> </artifactItems> </configuration> </execution> </executions> </plugin> </plugins> </build> </project>
基本、JavaEEベースのものですね? 続いてコードになります。せっかくなので、CDIのSessionScopeを使います。
@Data @Named @SessionScoped public class User implements Serializable{ String name; int age; }
リクエストを受け取り、CDIをバインドするJAX-RSサービスは、StatelessBeansで作ります。
/** * REST Web Service * * @author koduki */ @Path("session") @Stateless public class SessionResource { @Inject private User user; /** * Creates a new instance of SessionResource */ public SessionResource() { } @GET @Produces(MediaType.APPLICATION_JSON) public String get() { String version = "2.0"; String msg = "Application version " + version + System.lineSeparator(); msg += String.format("I am %s. I am %d years old.", user.getName(), user.getAge()); return msg; } @GET @Path("update/{name}") @Produces(MediaType.APPLICATION_JSON) public User update(@PathParam("name") String name, @QueryParam("age") int age, @Context HttpServletRequest request) { user.setName(name); user.setAge(age); return user; } }
では、こちらを動かして見ましょう。
$ mvn package $ java -jar java -jar payara-micro-4.1.1.154.jar --deploy target/example-payara_micro.war [2015-12-09T07:19:25.856+0900] [Payara 4.1] [INFO] [NCLS-CORE-00087] [javax.enterprise.system.core] [tid: _ThreadID=1 _ThreadName=main] [timeMillis: 1449613165856] [levelValue: 800] Grizzly Framework 2.3.23 started in: 33ms - bound to [/0.0.0.0:8080] . . Members [1] { Member [192.168.99.1]:5901 this } ]] . . [2015-12-09T07:19:39.340+0900] [Payara 4.1] [INFO] [] [PayaraMicro] [tid: _ThreadID=1 _ThreadName=main] [timeMillis: 1449613179340] [levelValue: 800] Deployed 1 wars
Deployed 1 warsになれば完了です。
途中、Membersと出ているところがHazelcastのクラスタです。 これは、マルチキャストグループで範囲が指定されていて自動的にひっかけてきます。 登録作業がいらないのでDokcerとかでダイナミックにインスタンスが増減しても、問題にならないのはいいですね。
では、検証コマンドを。ブラウザだと確認が面倒なのでcurlで実施しました。
$ curl -c cookie.txt -b cookie.txt -i http://localhost:8080/example-payara_micro/webresources/session HTTP/1.1 200 OK Server: Payara Micro #badassfish Set-Cookie: JSESSIONID=3ccb47b039bb17f872d2216c9cde; Path=/example-payara_micro; HttpOnly Set-Cookie: JSESSIONIDVERSION=2f6578616d706c652d7061796172615f6d6963726f:0; Path=/example-payara_micro; HttpOnly Content-Type: application/json Date: Tue, 08 Dec 2015 22:53:29 GMT Content-Length: 52 Application version 2.0 I am null. I am 0 years old.
最初は、セッショに何も値が入ってないのでnullが返ります。続いて値の登録して、結果を確認して見ます。
$ curl -c cookie.txt -b cookie.txt -i "http://localhost:8080/example-payara_micro/webresources/session/update/Nanoha?age=9" HTTP/1.1 200 OK Server: Payara Micro #badassfish Set-Cookie: JSESSIONIDVERSION=2f6578616d706c652d7061796172615f6d6963726f:1; Path=/example-payara_micro; HttpOnly Content-Type: application/json Date: Tue, 08 Dec 2015 22:55:24 GMT Content-Length: 25 {"age":9,"name":"Nanoha"}% $ curl -c cookie.txt -b cookie.txt -i http://localhost:8080/example-payara_micro/webresources/session HTTP/1.1 200 OK Server: Payara Micro #badassfish Set-Cookie: JSESSIONIDVERSION=2f6578616d706c652d7061796172615f6d6963726f:2; Path=/example-payara_micro; HttpOnly Content-Type: application/json Date: Tue, 08 Dec 2015 22:55:57 GMT Content-Length: 54 Application version 2.0 I am Nanoha. I am 9 years old.
まずは、セッションが想定どおりに動いているのが確認できました。続いて、もう一つ立ち上げます。
$ java -jar ~/Downloads/payara-micro-4.1.1.154.jar --deploy /tmp/example-payara_micro.war --port 8180 Dec 09, 2015 7:57:46 AM org.glassfish.security.services.impl.authorization.AuthorizationServiceImpl initialize . . Members [2] { Member [192.168.99.1]:5901 Member [192.168.99.1]:5903 this } ]] . . [2015-12-09T07:58:06.839+0900] [Payara 4.1] [INFO] [] [PayaraMicro] [tid: _ThreadID=1 _ThreadName=main] [timeMillis: 1449615486839] [levelValue: 800] Deployed 1 wars
今回は同一サーバに複数個立ち上げるのでポートを変更してあります。 また、Membersに新しく追加されたのが確認できるかと思います。
では、新規に立ち上げた方のサーバにアクセスしてみます。
$ curl -c cookie.txt -b cookie.txt -i http://localhost:8180/example-payara_micro/webresources/session HTTP/1.1 200 OK Server: Payara Micro #badassfish Set-Cookie: JSESSIONIDVERSION=2f6578616d706c652d7061796172615f6d6963726f:3; Path=/example-payara_micro; HttpOnly Content-Type: application/json Date: Tue, 08 Dec 2015 23:01:07 GMT Content-Length: 54 Application version 2.0 I am Nanoha. I am 9 years old.%
nullではなく、適切にレプリケーションされた値が入ってることが確認できました! これでラウンドロビンでアクセスができますね!
Docker対応
やはり、この手のものはDocker経由で使いたいので、Dockerでの動きを検証してみました。
上記のDockerファイルを元に作成して、問題なく動作しました。
少なくとも同一ホストであれば、特になんの設定もしなくても Docker間でのマルチキャストによるディスカバリも含めてセッションレプリケーションも問題なく動作しました。
複数グループ
複数のセッション/クラスタグループを作るには、マルチキャストグループを変更することで可能です。
payara-microの場合は、IPが224.2.2.4がデフォルトのようなので
$ java -jar payara-micro-4.1.1.154.jar --deploy example-payara_micro.war -mcAddress 224.2.2.4
こんな感じで指定すればいいかと思いきや、現状では下記のようなエラーが出ます。
Dec 09, 2015 8:32:16 AM org.hibernate.validator.internal.util.Version <clinit> INFO: HV000001: Hibernate Validator 5.1.2.Final Exception in thread "main" fish.payara.micro.BootstrapException: PlainTextActionReporterFAILURENo configuration found for server.hazelcast-runtime-configuration at fish.payara.micro.PayaraMicro.bootStrap(PayaraMicro.java:714) at fish.payara.micro.PayaraMicro.main(PayaraMicro.java:105) Caused by: org.glassfish.embeddable.GlassFishException: PlainTextActionReporterFAILURENo configuration found for server.hazelcast-runtime-configuration at com.sun.enterprise.glassfish.bootstrap.ConfiguratorImpl.configure(ConfiguratorImpl.java:75) at com.sun.enterprise.glassfish.bootstrap.GlassFishImpl.configure(GlassFishImpl.java:71) at com.sun.enterprise.glassfish.bootstrap.GlassFishImpl.<init>(GlassFishImpl.java:65) at com.sun.enterprise.glassfish.bootstrap.StaticGlassFishRuntime$1.<init>(StaticGlassFishRuntime.java:116) at com.sun.enterprise.glassfish.bootstrap.StaticGlassFishRuntime.newGlassFish(StaticGlassFishRuntime.java:116) at fish.payara.micro.PayaraMicro.bootStrap(PayaraMicro.java:694) ... 1 more
masterでは修正されたようなので、しばらく更新を待つしかないですね。 ちなみに、マイクロじゃない方のPayaraでは問題なく動作しましたので、すぐ使いたいならそちらで。
バージョニング
今回の構成ではラウンドロビンでバランシングができるので、シンプルなリリース要件ならブルーグリーンデプロイメントで問題ないと思います。 ただし、リリース中に一切ログインさせない、不整合も起こさせないといった完全なZero-downtime Deploymentを実施するならバージョン毎に別のセッションが必要です。
しかし、現行のGlassFish及びそれをベースとしたPayaraにはアプリバージョン毎のセッション等は作成できないので、 そちらに関しては別途の仕組みの検討が要りますし、いずれにしてもスティッキー性が必要になってきます。
まとめ
今回はPayara Microでのセッションレプリケーションを確認しました。実にDocker時代と相性の良さそうな機能ですね。 将来的には、mod_mrubyと組み合わせてZero-downtime Deploymentも作ってみたいと思います。
では、明日はsusumuisさんの「Javaプログラマー12年の僕がSpring童貞卒業的なこと書きます!」です!