RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

無停止リリースを試してみた

f:id:tech-rakus:20210212130204p:plain

こんにちは、株式会社ラクスで楽楽勤怠の開発を行なっている goldminer です。

「楽楽勤怠」は昨年にリリースしたばかりのクラウド型の勤怠管理システムです。 ラクスのサービスは BtoB のサービスが多く、勤怠管理システムも基本的にはバックオフィス業務をサポートするためのシステムです。

バックオフィス業務であれば(サービスの停止を伴う)メンテナンス時間を営業時間外となることが多い深夜時間帯にすれば影響が出にくいのですが、勤怠管理システムには打刻機能があり、これは次のような性質を持っています。

  • 打刻には(いつ打刻したのかという)客観性が必要だが、打刻できずに従業員の自己申告に基づいて後から入力したものは客観性が低い
  • 従業員の勤務時間帯は多様であり、深夜時間帯に行われる打刻も一定数ある

このことから、楽楽勤怠では無停止リリースの重要性が高いです。直近のバージョンアップでたまたま無停止リリースを実施できる条件を満たしているものがありましたので、実際に無停止リリースのための開発を行ってみました。

今回の目的

無停止リリースについては弊社のかみせんプロジェクトでも検証しています。 同じラクス エンジニアブログに 記事 があるので参考になさってください。

上記の記事では次の3つの要件があげられています。

  • セッション管理
  • アプリケーション構成
  • DB運用

この内、「セッション管理」「アプリケーション構成」は前に担当していたサービスでも対応済だったこともありナチュラルに対応しています。 つまり、DB変更がないリリースについてはすでに無停止リリースを達成しています。

今回は「DB運用」の検証で且つ完全無停止を目的としており、リリース中の更新リクエストを制限しません。

「DB変更リリースを完全無停止で行う」ために何が必要であり、開発にどのような影響がでるのかを検証してみました。

無停止リリースの手順

DB変更リリースをサービスを停止して行う場合の手順は次のとおりです。

  1. APサーバー1と2を停止する
  2. APサーバー1と2のアプリケーションを更新する
  3. DB変更パッチを適用する
  4. APサーバー1と2を再開する

無停止リリースの手順は次のとおりです。

  1. DB変更パッチを適用する
  2. APサーバー1への振り分けを停止する
  3. APサーバー1のアプリケーションを更新する
  4. APサーバー1への振り分けを再開する
  5. APサーバー2への振り分けを停止する
  6. APサーバー2のアプリケーションを更新する
  7. APサーバー2への振り分けを再開する

手順の 2 から 7 まではDB変更がない場合の無停止リリースとまったく同じ手順です。

この手順の場合、アプリケーションとDBの組み合わせには次の3つがあります。

  • 古いバージョンのアプリケーションと古いバージョンのDB
  • 古いバージョンのアプリケーションと新しいバージョンのDB
  • 新しいバージョンのアプリケーションと新しいバージョンのDB

1番目と3番目は当たり前のことですので、2番目の「古いバージョンのアプリケーションと新しいバージョンのDBで正常に動作する」ことが特徴的な要件です*1

無停止リリースに向けたアプリケーションの開発

DB変更パッチの作成

先に結論を言ってしまうと、DB変更パッチが以下だけの場合に限り完全無停止で行うことができます。

  • COLUMN の追加( ALTER TABLE ADD COLUMN )
  • TABLE の作成( CREATE TABLE )
    単純にデータ種別が増えて追加する場合に限る、既存の TABLE を分割するような場合は不可

大抵のORマッパーは追加された TABLE と COLUMN は無視しますので、特に何もしなくても 「古いバージョンのアプリケーションと新しいバージョンのDBで正常に動作する」を達成することができます。

それ以外の UPDATE や INSERT ~ SELECT などは、レコード数が多いと負荷が高くなったり容量が急に増えたりする可能性がありますので、運用しながらの実行はするべきではないと考えます。

COLUMN の追加に関する注意点

追加した COLUMN に初期値は設定せず、null のままにします

  • OK 例
ALTER TABLE table_a ADD COLUMN col1 boolean;
  • NG 例
ALTER TABLE table_a ADD COLUMN col1 boolean;
UPDATE table_a SET col1 = true;
ALTER TABLE table_a ALTER COLUMN col1 SET NOT NULL;

NG 例は UPDATE を実行している点がよろしくありません。 UPDATE ではなく DEFAULT を使って初期値を設定するのも同様です。

今回は検証していませんが、全体設定のようなデータを保持する UPDATE ONLY な TABLE であれば レコード数が少なく、影響が把握しやすいので UPDATE も可能と考えています。

追加した COLUMN の INDEX は作成しません

UPDATE と同じく、レコード数が多いと負荷が高くなったり容量が急に増えたりする可能性がありますので、運用しながらの作成はするべきではないと考えます。

やはりレコード数が少なければ作成しても問題ないですが、レコード数が少ないのであればそもそも INDEX を作る必要性が低いので、作成するという選択肢はないと考えています。

TABLE の追加に関する注意点

今回の検証では特に注意する点はありませんでした。

初期レコードを INSERT するかどうかについては次のように考えています。

  • 基本は INSERT しない
  • サンプルレコードや UPDATE ONLY な TABLE のレコードであれば、レコード数が少なく影響が把握しやすいので INSERT も可能

今回の検証では INSERT していません。

INDEX の作成はレコードがない、または数が少ないので問題はないと考えています。 プライマリーキーやユニークキー制約により自動的に作成される INDEX も同様です。

アプリケーションの実装

COLUMN の追加に関する注意点

上述したように追加した直後の COLUMN は null になっています。 これに対応する必要があります。

次のような実装でDBから取得した値が null だった場合に初期値に置換します。

    Entity entity = /* DBから取得する処理 */;
    if (entity.col1 == null) {
        entity.col1 = true;
    }

この置換処理は 、DataAccessObject(DAO) または Repository のどちらかで実行することになるでしょう。 そうすることで、それ以外の箇所では値が null である可能性を除外して実装することができます。

追加した COLUMN を抽出時の条件に使用する場合は 、null である可能性を考慮します。

    Entity findByCol1() {
        // ↓「col1 IS NULL」を条件に付与している.
        String sql = "SELECT * FROM table_a WHERE col1 = true OR col1 IS NULL";
    }

追加した COLUMN でソートする場合、NULLS FIRST / NULLS LAST を指定する必要があります。 今回は検証した中にこのケースがなかったため実際にはやっていません。

TABLE の追加に関する注意点

今回の検証では初期レコードを INSERT しておらず、TABLE の中身が空なのは正常な状態のため特に注意する点はありませんでした。

テスト

無停止リリースに固有のものとして次のテストを行いました。

  1. Apache JMeter を使って常時アクセスがある状況を作り、その中でDB変更パッチを実行するテスト
    以下の観点で確認
    • DB変更パッチが正常終了するか
    • Apache JMeter によるアクセスがすべて正常応答するか(エラー応答がないか、応答時間が正常な範囲内かどうか)
  2. 「古いバージョンのアプリケーションと新しいバージョンのDB」の組み合わせで機能テスト

テスト結果はいずれも問題ありませんでした。

1 のテストはミドルウェアやインフラ構成に大きな変更がない限りは、初回のみの実施でよく、毎バージョンアップで実施する必要はないと考えています。

次回バージョンアップでの課題

ここまでの内容で無停止リリースを実施することができます。 しかし、いくつかの課題を先送りにしており、次回以降のバージョンアップで対応する必要があります。 この対応はサービスを停止して行います。

DB変更パッチの作成

追加した COLUMN は null になっていますので、改めて初期値に更新します*2。 緊急性は高くありませんが、今後の開発でノイズにならないために早めに対応しておくべきだと考えています。

UPDATE table_a SET col1 = true WHERE col1 IS NULL;
ALTER TABLE table_a ALTER COLUMN col1 SET NOT NULL;

INDEX の作成を見送っていた場合は INDEX も作成します。

アプリケーションの実装

null である可能性を考慮した実装がありますが、null がありえなくなりますので 本来あるべき実装にします。

    Entity entity = /* DBから取得する処理 */;
    // 後処理は必要なし.
    Entity findByCol1() {
        String sql = "SELECT * FROM table_a WHERE col1 = true";
    }

まとめ

今回の検証結果を踏まえての結論は次の通りです。

「DB変更リリースを完全無停止で行う」こと自体は条件が整えば可能
しかし、工数心理的負担の増加を考えるとペイしない

次のような作業が必要になり、工数が増加します。

  • ★無停止リリースの手順を作成する
  • 追加した COLUMN が null だった場合を考慮した実装を行う
  • ★常時アクセスがある状況でのリリースのテストを行う
  • 「古いバージョンのアプリケーションと新しいバージョンのDB」の組み合わせで機能テストを行う
  • 追加した COLUMN の null を初期値に更新するDB変更パッチを作成する@次回バージョンアップ
  • 追加した COLUMN が null だった場合を考慮した実装を本来あるべき実装に変更する@次回バージョンアップ

上記で★を付けたものは初回のみの実施でよく、毎バージョンアップで実施する必要はないと考えているものです。 ★が付いていないものが毎バージョンアップで工数が増加する原因になるもので、工数としてはざっくり1、2人日程度です。

工数は飛び抜けて大きなものではありませんが、次回バージョンアップでの対応が発生することによる心理的負担の増加が嫌だなと、感じました。

効果の方はと言いますと、そもそもDB変更パッチが COLUMN と TABLE の追加のみの場合に限られるという問題があります。 さらに今後の予定に書いているように分散システム化することで打刻の無停止が達成できると、効果は「リリース担当エンジニアが深夜作業をしなくてよい」だけになってしまいます。

ビジネスサイドでの無停止リリースの要求が強くなるか開発の負担を減らすことができないと再チャレンジは難しいという所感です。

今後の予定

ビジネスサイドから無停止リリースの要求が出てきた際に応えられるようにと考えて実施した今回の検証ですが、毎バージョンアップで頑張ってDB変更パッチの作成と実装を行わなければならないのは継続性に難があると感じました。 今後の計画に無停止リリースは織り込まず、突発的に無停止リリースの必要が生じた場合に今回の知見を活かせればよいと考えています。

可用性の向上の観点ではシステム全体の完全無停止にはこだわらず、一度仕組みを作ってしまえば以降は再利用するだけでよくなる次のような方針で進めるべきだと考えています。

  • 分散システムにして重要な機能は常時稼働できるようにする
    勤怠管理システムでは打刻機能が該当
  • 更新リクエストのみ制限して参照リクエストは行えるようにする
  • 停止はするが停止時間が極力短くなるようにする

*1:DB変更パッチの適用とアプリケーションの更新の順序を入れ替えれば「新しいバージョンのアプリケーションと古いバージョンのDB」の組み合わせになります。 詳細は割愛しますが、この組み合わせで正常に動作させることは難易度が高く現実的ではありません。 例外的にDB変更が「DROP COULMN」のみの場合は容易に達成できますが、「DROP COULMN」に緊急性がある場合は少なく無停止リリースで行うメリットはないと思われます。

*2:新しいバージョンのアプリケーションで INSERT, UPDATE したレコードでは null ではありません。

Copyright © RAKUS Co., Ltd. All rights reserved.