平常運転

アニソンが好き

過去記事とかは記事一覧で見れます

YAPC::Fukuoka 2017 HAKATA で "稼働中の Web サービスの Perl 処理系バージョンアップをしていく話" をしました

f:id:astj:20170706040838j:plain

スライドが200枚を超え20分のトーク枠に収まらない予感がしましたが案の定収まりませんでした。誠に申し訳ございませんでした…… ちなみに社内で再演したところ30分かかりました。はい……

yapcjapan.org

ということで話してきました。前半は割と教科書っぽいお話、後半がはてなの実験的サービスである大チェッカーでの実事例を紹介する、という中身でした。つまり大事な本題を話しそびれたことになりますね……

スライドはこちらです。多分これだけ見ても意味分からない。

speakerdeck.com

後半の事例のうち、大チェッカー自体に関する部分については発表するはずだった内容をエントリ下部に記載しましたのでご確認ください。130枚目くらいからの中身に相当します(が、スライドの順序通りでない部分も多いです)。

処理系のバージョンアップはやったことがないとなにやら末恐ろしいことのように感じるかもしれませんが、(きちんとテストや検証体制がある前提で)バージョン差分が小さければ実際の所そんなに特別なことではないのだ……という認識のもと、サービスの寿命に合わせてヘルシーにバージョンを上げていけるとよいように思います。また、必要とされる変更を小さく切り分けて少しずつリリースしていくことで、バージョンアップそのもののリリースインパクトを小さくすることもヘルシーなバージョンアップ作業の大事な要素ですが、これはよくよく考えると継続的デリバリーにおける割と普遍的な考えではないでしょうか。

今回、B会場では Perl プロダクトの保守に関するいろいろな立場、話題のトークがあって大変興味深かったのですが、このトークもそんなトークの一つとして華を添えられていたなら何よりかなと思います。Hokkaido(Perl6 LT) => Kansai(Perl6 Talk) => Fukuoka(Perl5 Talk) と来たので次の Okinawa でも是非トークしたい。今度は時間に収まるトークをしたい!!!という気持ちであります。

補足

そういえば社内で再演したところ、「じゃあ具体的にはどうバージョンを上げたらいいんや」という話題になりました。個人的な所感としては、

  • 5.x.0 はやや(安定性重視だと)蛮勇なので、 5.x.1 くらいまで待つ
  • 最新の安定版で動作するところまで CPAN モジュールのバージョンは上げる
  • アプリケーションコード自体は、コード内で使っている廃止機能が廃止ではなく警告で収まるギリギリのバージョンまでまず上げる
    • 警告を見ながら廃止予定機能に対応していく
  • 警告を潰せたら最新 or 1つ前の安定版まで上げる

くらいのフローがまあまあヘルシーではないかなと思いました。勿論様々な事情や条件によって変わってきそうですが。


事例

はてなの実験的サービスである"大チェッカー"のフロント部分にあたるアプリケーションのバージョンアップ事例です。
このフロント部分はフレームワーク部分合わせて数千行の Perl アプリケーションで、データストアには MySQL を利用しています。 RSS リーダーとしてのコアロジックは別システムにあり、そちらには今回手を付けていません。

2015年3月にリリースした当初は perl 5.20.1 でしたが、その後2016年6月に perl 5.24.0 、2017年5月に 5.26.0 へと2回バージョンアップを行っています。どちらも該当バージョンのリリースすぐのバージョンアップで、社内最速の実績となります。これはもちろん社内最速でてっぺん狙うというぼく自身の心意気もありますが、小規模なアプリケーションでバージョンアップの事例や、本番稼働している処理系バージョンの事例を重ねて他サービスにフィードバックするという真っ当な目的もあります。

第一部 (5.20.1 => 5.24.0)

1回目のバージョンアップでは 5.20.1 から 5.24.0 へとバージョンアップしました。
とにかく最速に拘った結果、手元では 5.23.9 で練習したり、 CI や検証環境には 5.24.0 RC の時点から投入して検証を進めるということをしました(が、一般的にはそこを頑張る必要は特にないと思います……)。安定性を重視する場合はパッチバージョンが一つ上がって 5.x.1 になるまで待つという選択肢もありそうです。

大チェッカーの CI は自前の Jenkins で行っているのですが、この当時は Jenkins サーバ上に xbuild でセットアップした /opt/perl-5.x.y/bin/perl を用いてテストを実行していました。そのため、バージョンを上げて CI を走らせるためにはまず Jenkins slave それぞれで perl 5.24.0 をインストールして回る必要がありました。また、この当初の Jenkins ビルド workspace(=ファイルシステム上の空間)は各ブランチで同じ場所を使っていたので、特定のブランチでのみ perl のバージョン(や CPAN モジュールの構成)が異なった場合でも ./local が共用されてしまう問題があります。なのでこの時点では、 PERL_CARTON_PATH 環境変数を変えて別のディレクトリに CPAN モジュールをインストールして回避しました。

f:id:astj:20170706030533p:plain

また、 CPAN モジュールについては手元で carton install しながら確認していきましたが、特にデータ永続化関連で互換性を崩さないように、 Storable モジュールを使っている箇所については cpanfile.snapshot で確認した上で、 CPAN モジュールのソースコードを読んで確認する……という作業も念のため行いました。もちろん、検証環境では 5.24.0 にバージョンアップした前後できちんと永続化データを引き継げていることの確認も実際に行っています。

このほか、 cpanfile.snapshot で古いバージョンで固定していたために 5.24 で警告やエラーが出るようになっていたモジュールが一部 (Text::Xslate, DateTime::Format::MySQL)ありましたが、これらは最新版にアップグレードすることで解決しました。いずれも changelog を見ると 5.21~23 で不具合が報告され、すでに修正されていました。

これらの変更により無事 CI でテストが通るようになったことから、検証環境、本番環境の順に反映して挙動の確認を行っていくことになります。大チェッカーでは{検証, 本番}環境の CPAN モジュールはデプロイの世代間で共有しているため、この構成を保ったままオンラインでバージョンアップを反映するのはやや難しいという問題があります。そのため、

  • 検証環境はダウンタイムが許容されるので一度アプリケーションを停止してから CPAN モジュールの再インストー
  • 本番環境はアプリケーションサーバー自体を新しく作り直して入れ替え

という方法で 5.24.0 の動く環境を本番に反映しました。

1回目のバージョンアップの結果得られた教訓は大きく以下の3つでした。

  • やるべき手順をきちんと踏めばきちんとアップデートできる
  • PERL_CARTON_PATH に起因する入れ替えが各サーバで問題になる
  • (成果としては意外と淡々とした物になる)

これまで perl のバージョンアップを実際に行ったことがなかったので大がかりな作業だと思いがちで、実際作業的な要素はそれなりに存在したのですが、バージョンを上げて動くようにする作業そのものは「とてつもなく難しく途方もない何か」ではなく、「勘所を押さえて順番に進めていけば実現可能な変更」である、とように認識を改められたのが心理的には大きかったと思っています。もちろんこれがもっと大きなアプリケーションだとたいへんなことはいろいろあるのでしょうが……

第二部(5.24.0 => 5.26.0)

5.24.0 にバージョンを上げたことで満足していたおよそ1年後、今度は 5.26.0 がリリースされるということで再び社内最速にチャレンジする運びとなりました。必要な変更点自体は1回目と同様なのですが、1回目の教訓(と、他プロジェクトでの事例導入)に基づき予め環境を整えて入れ替え作業の負担を下げるようにしました。

一つ目が Jenkins の Pipeline Multibranch Plugin の導入です。詳細への深入りはしませんが、プロジェクトのブランチごとに Jenkins 上の異なる Workspace を用いるようになるため、他のブランチで perl のバージョンや cpanfile.snapshot を更新した影響を受けずに独立させることができます。半面、各ブランチの初回ビルド時に carton install が0から走ることになる問題があり、結局後述のようにテストに Docker を導入することになって ./local/ 共有問題への対処もそちらに任せる形になってしまいました。CI 環境を入れ替えるついでにエイヤッとやったということもあって Pipeline Multibranch Plugin の導入自体は決して悪いことではなかったと思っているけど、今改めてブログに書きながら振り返るとこれ自体はバージョンアップ負荷低減の本質的な改善ではなかった。

Pipeline Multibranch Plugin - Jenkins - Jenkins Wiki

二つ目が、開発プロセスおよび CI 環境における Docker の導入です。本番環境や検証環境に導入したわけではありませんが、 Jenkins 上でのテスト実行時と、手元から cpanfile.snapshot を更新する際に Docker を利用することにしました。
docker build の過程で cpanfile / cpanfile.snapshot をコピーすることで、 cpanfile に変更がないときは docker build のキャッシュを利用できるようになった他、ホスト側の ./local/ を汚染しなくなったため、同じ workspace 内で cpanfile や perl のバージョンを切り替えた際に ./local/ の内容を退避する必要がなくなりました。

(snip)
WORKDIR /app/
COPY cpanfile /app/
COPY cpanfile.snapshot /app/
RUN carton install

また、 perl をコンテナの中で起動するようにしたことで、 perl のバージョンを変更する際には Dockerfile を更新するだけでよく、 Jenkins 上に新しいバージョンの perl を直接インストールする必要もなくなりました。

これらの準備によって作業的な部分は楽になりましたが、本質的に求められる作業は 5.24 にバージョンアップしたときと変わりません。手元で 5.26.0 に切り替え、入らなくなったモジュールのバージョンを上げ、テストを通して動作確認をする……という流れです。変更点としては、

といったところでしょうか。 perl 5.26 へのアップデートとなると @INC への対処はホットなトピックとなりそうです。

これらの変更を行った上で、検証環境や本番環境への反映は1回目と同様に行ってバージョンアップ完了です。同じ作業を1年越し2回目に行うということで勝手が分かってきたことや、上記のように一部作業面が楽になったこともあり、基本的には1回目よりスムーズにバージョンアップすることができました。

振り返り

このように、大チェッカーでは2回の perl のバージョンアップを行いました。2回目の方が勝手が分かってスムーズに進んだことは前述の通りですが、2回ともバージョンアップ自体に大きく手間取らなかったのはバージョンの差分が小さかったおかげであると考えています(勿論コード量自体が少ないというのもあるのですが…)。
特に本番サーバーの構成変更が伴うとインフラチームとの調整が発生するなど、かかる手間は勿論無視できませんが、長期間保守するプロダクトであれば、バージョンを上げられること / 上げやすいことの価値は大きいと考えています。