A/B(シームレス)システム アップデート

シームレス アップデートとも呼ばれる A/B システム アップデートにより、無線(OTA)アップデート中に実行可能な起動システムがディスク上に残ります。この方法では、アップデート後のデバイスが動作しなくなる可能性が低くなり、修理保証センターでのデバイスの交換や修理回数を低減できます。ChromeOS などのその他の商用グレードのオペレーティング システムも A/B アップデートを問題なく使用しています。

A/B システム アップデートとその動作の詳細については、パーティションの選択(スロット)をご覧ください。

A/B システム アップデートには次のようなメリットがあります。

  • OTA アップデートは、システムの実行中にユーザーの妨げにならずに実行できます。ユーザーは OTA アップデート中も継続してデバイスを使用でき、アップデート中のダウンタイムは、デバイスが更新されたディスク パーティションで再起動したときだけです。
  • アップデート後の再起動にかかる時間は通常の再起動と変わりません。
  • フラッシュがうまくできなかったなど、OTA を適用できない場合も、ユーザーに影響はありません。ユーザーは引き続きアップデート前の OS を実行し、クライアントはアップデートを再試行できます。
  • OTA アップデートが適用されたものの起動できない場合、デバイスは古いパーティションで再起動し、使用できる状態を保ちます。クライアントは、いつでもアップデートを再試行できます。
  • I/O エラーなど、すべてのエラーは未使用のパーティション セットだけに影響し、再試行できます。I/O 負荷を意図的に低くしているため、このようなエラーは起こりにくくなっており、ユーザー エクスペリエンスの低下を回避できます。
  • アップデートは A/B デバイスにストリーミングされ、パッケージをダウンロードする必要はありません。ストリーミングにより、ユーザーはアップデート パッケージを /data/cache に保存するのに必要な空き領域を確保する必要がありません。
  • キャッシュ パーティションは、OTA アップデート パッケージの保存には使用しなくなるため、将来の更新に備えて大きなキャッシュ パーティションを用意する必要はありません。
  • dm-verity により、デバイスは破損していないイメージを起動できます。デバイスが OTA や dm-verity の問題により起動しない場合、デバイスは古いイメージで再起動できます(Android Verified Boot では A/B アップデートが必要ありません)。

A/B システム アップデートについて

A/B アップデートには、クライアントとシステムの両方を変更する必要があります。ただし、OTA パッケージ サーバーでは変更の必要はなく、HTTPS を介してアップデート パッケージが配信されます。Google の OTA インフラストラクチャを使用しているデバイスでは、システムの変更はすべて AOSP にあり、クライアント コードは Google Play 開発者サービスで提供されます。Google の OTA インフラストラクチャを使用していない OEM の場合、AOSP システムコードを再利用できますが、独自のクライアントを用意する必要があります。

クライアントが独自のクライアントを用意するには、次のことを行う必要があります。

  • アップデートを行うタイミングを決めます。A/B アップデートはバックグラウンドで行われるため、ユーザーが開始するものではありません。ユーザーの邪魔にならないよう、夜間や Wi-Fi 接続中などデバイスがアイドル状態のメンテナンス モードのときにアップデートをスケジュールすることをおすすめします。 ただし、クライアントは任意のヒューリスティックを使用できます。
  • OTA パッケージ サーバーにチェックインし、アップデートが利用可能かどうかを判断します。これは、デバイスが A/B に対応していることを伝えたい場合を除き、既存のクライアント コードとほとんど同じです(Google のクライアントには、ユーザーが最新のアップデートを確認するための今すぐ確認ボタンもあります)。
  • アップデート パッケージが利用可能な場合はその HTTPS URL を使用して update_engine を呼び出します。update_engine はアップデート パッケージをストリーミングする際に、その時点で未使用のパーティションの元ブロックを更新します。
  • update_engine 結果コードに基づいてサーバーにインストールの成功または失敗をレポートします。アップデートが正常に適用された場合、次の再起動時に update_engine はブートローダーに新しい OS をブートするよう指示します。新しい OS がブートに失敗した場合、ブートローダーは古い OS にフォールバックするので、クライアントからの作業は不要です。アップデートが失敗した場合、クライアントは詳細なエラーコードに基づいて再試行するかどうか、いつ再試行するかを判断する必要があります。たとえば、正常なクライアントは部分的な(差分)OTA パッケージが失敗したことを認識し、代わりにフル OTA パッケージを試すことができます。

必要に応じて、クライアントは次のことを行えます。

  • ユーザーに再起動を促す通知を表示します。ユーザーに定期的にアップデートを促すポリシーを実装する場合は、この通知をクライアントに追加できます。クライアントがユーザーに再起動を要求しない場合は、次回再起動すると更新されます(Google クライアントでは更新ごとに遅延を構成可能です)。
  • 新しい OS バージョンで起動したかどうか、または起動を試みたものの以前の OS バージョンにフォールバックしたかどうかを示す通知を表示します(通常、Google クライアントはどちらも表示しません)。

システム側では、A/B システム アップデートによって以下の影響があります。

  • パーティションの選択(スロット)、update_engine デーモン、ブートローダーとの交信(詳細は下記をご覧ください)
  • ビルドプロセスと OTA アップデート パッケージの生成(詳細はA/B アップデートの実装をご覧ください)

パーティションの選択(スロット)

A/B システム アップデートではスロットと呼ばれる 2 つのパーティション セットが使用されます。通常はスロット A とスロット B と呼ばれます。システムは現在のスロットから実行され、未使用のスロットのパーティションは通常の運用中に実行中のシステムからはアクセスされません。 この方法では、未使用スロットをフォールバックとして確保しておくことでフォールト トレラントにできます。更新中または更新直後にエラーが発生した場合、システムは古いスロットにロールバックし、動作可能な状態を継続します。この目的を達成するために、OTA アップデートの一環として現在のスロットで使用されるパーティションは更新されません。パーティションが 1 つしかない場合でも同様です。

各スロットには起動可能属性があり、デバイスが起動できる適切なシステムがスロットにあるかどうかを示します。現在のスロットは、システムの実行中に起動可能ですが、もう一方のスロットにシステムの(現時点では問題のない)古いバージョン、新しいバージョン、無効なデータが含まれている可能性があります。現在のスロットの状態にかかわらず、アクティブなスロット(ブートローダーが次回起動時に起動するスロット)または優先スロットは 1 つです。

各スロットには、ユーザー空間で設定された正常属性もあります。スロットが起動可能である場合にのみ関係します。正常とされたスロットでは起動、実行、更新ができます。何度か起動を試みたものの正常とされていない起動可能スロットは、ブートローダーによって起動不能としてマークされ、アクティブ スロットは別の起動可能スロットに変更されます。通常は新しい、新規のアクティブ スロットで起動する直前に実行されていたスロットに変更されます。インターフェースの具体的な詳細は、 boot_control.h をご覧ください。

アップデート エンジンのデーモン

A/B システム アップデートでは、update_engine と呼ばれるバックグラウンド デーモンを使用してシステムを更新して新しいバージョンに起動する準備をします。このデーモンは次のアクションを実行できます。

  • OTA パッケージの指示どおりに、現在のスロット A/B パーティションから読み取り、未使用のスロット A/B パーティションにデータを書き込みます。
  • 定義済みのワークフローで boot_control インターフェースを呼び出します。
  • OTA パッケージの指示どおりに、未使用スロットのパーティションすべてに書き込んだ後、新しいパーティションからインストール後プログラムを実行します。詳細については、インストール後の手順をご覧ください。

update_engine デーモンは起動プロセス自体には関係しないため、更新中の機能は現在のスロット内の SELinux ポリシーと機能に限定されています。システムが新しいバージョンで起動するまで、このポリシーや機能は更新できません。堅牢なシステムを維持するために、アップデート プロセスとしてパーティション テーブル、現スロットのパーティションの内容、または出荷時設定へのリセットでも消去されない非 A/B パーティションの内容は修正しないでください

アップデート エンジンのソース

update_engine のソースは system/update_engine にあります。 A/B OTA dexopt ファイルは、installd とパッケージ マネージャーに次のとおり分割されています。

実際の例については、/device/google/marlin/device-common.mk をご覧ください。

アップデート エンジンのログ

Android 8.x 以前のリリースでは、update_engine ログは logcat およびバグレポートにあります。ファイル システム内で update_engine ログを利用可能にするには、ビルドに次の変更パッチを適用します。

これらの変更により、最新の update_engine ログのコピーが /data/misc/update_engine_log/update_engine.YEAR-TIME に保存されます。 現在のログに加えて、直近の 5 つのログが /data/misc/update_engine_log/ に保存されます。ログのグループ ID を持つユーザーは、ファイル システムのログにアクセスできます。

ブートローダーの操作

update_engine(場合によっては他のデーモンも)は boot_control HAL を使用して、ブートローダーに何を起動するかを指示します。一般的なシナリオ例と関連するステータスは次のとおりです。

  • 通常のケース: スロット A または B どちらか現在のスロットからシステムを実行しています。現在までに適用されたアップデートはありません。システムの現在のスロットは起動可能、正常、アクティブなスロットです。
  • 更新中の場合: システムはスロット B で実行されているため、スロット B は起動可能、正常、アクティブなスロットです。 スロット A の内容は更新中ですが、まだ完了していないため、スロット A は起動不能でした。この状態で再起動すると、スロット B からの起動が続行されます。
  • 更新は適用済み、再起動の保留中の場合: システムはスロット B から起動しています。スロット B は起動可能で正常ですが、スロット A がアクティブでした(したがって起動可能です)。スロット A はまだ正常とされておらず、ブートローダーによってスロット A からの起動が何度か試行されます。
  • システムが新しいアップデート版で再起動した場合: システムはスロット A から初めて起動していて、スロット B は起動可能かつ正常な状態、スロット A は起動可能でアクティブな状態ですが、正常ではありません。ユーザー空間デーモンの update_verifier は、いくつかのチェックを行った後にスロット A を正常としてマークします。

ストリーミング アップデートのサポート

ユーザー デバイスの /data にアップデート パッケージをダウンロードする十分な空き容量がない場合があります。OEM やユーザーは /cache パーティションのスペースを無駄にしたくないため、一部のユーザーはデバイスにアップデート パッケージを保存する空き容量がないため、アップデートを行わないまま利用していました。 この問題に対処するため、Android 8.0 では、ブロックを /data に保存せず、ダウンロード時に B パーティションに直接書き込むストリーミング A/B アップデートをサポートしました。 ストリーミング A/B アップデートには一時ストレージはほとんど必要なく、100 KiB 程度のメタデータに必要な保存容量だけで済みます。

Android 7.1 でストリーミング アップデートを有効にするには、次のパッチから選択して適用します。

Android 7.1 以降でストリーミング A/B アップデートをサポートするには、上記のパッチが必要です。Google モバイル サービス(GMS)やその他のアップデート クライアントでも同様です。

A/B アップデートのライフサイクル

更新プロセスは、OTA パッケージ(コード内ではペイロードとして参照)がダウンロード可能な状態になると開始します。デバイスのポリシーは、電池残量、ユーザーの操作、充電状況などに基づいて、ペイロードのダウンロードや適用を遅延させることがあります。また、更新はバックグラウンドで実行されるため、ユーザーには更新が進行中であることはわかりません。つまり、ポリシー、予期しない再起動、ユーザーの操作などにより、いつでも更新プロセスが中断される可能性があります。

必要に応じて、OTA パッケージのメタデータ自体が、ストリーミングにより更新できることを示します。同じパッケージを非ストリーミング インストールに使用することもできます。サーバーはメタデータを使用してクライアントにストリーミングを伝えることができるため、クライアントは OTA を update_engine に正しく引き渡せます。独自のサーバーとクライアントを持つデバイス メーカーは、アップデートがストリーミングされていることをサーバーに識別させることで、ストリーミング アップデートを有効にできます。これにより、クライアントはストリーミングのため適切に update_engine を呼び出せます。メーカーは、パッケージがストリーミング バリアントであることを利用して、フレームワーク側への引き渡しをストリーミングにより行うフラグをクライアントに送信できます。

ペイロードが使用可能になると、更新プロセスは次のようになります。

手順 アクティビティ
1 現在のスロット(またはソーススロット)は、まだマークされていない場合、markBootSuccessful() により正常とマークされます。
2 未使用のスロット(またはターゲット スロット)は、setSlotAsUnbootable() 関数を呼び出して起動不可としてマークされます。現在のスロットは更新の開始時には常に正常とマークされ、無効なデータの入る未使用のスロットにブートローダーがフォールバックしないようにします。システムがアップデートの適用を開始できる状態になると、クラッシュ ループの UI など、他の主要コンポーネントが破損している場合でも、現在のスロットは正常とマークされます。新しいソフトウェアがこれらの問題を修復する可能性があるからです。

更新ペイロードは opaque blob で、新しいバージョンに更新する手順を示します。更新ペイロードは、以下で構成されます。
  • メタデータ。更新ペイロードの比較的小さい部分を占めるメタデータには、ターゲット スロットに新しいバージョンを生成して検証する操作のリストが含まれます。たとえば、特定の blob を解凍し、ターゲット パーティション内の特定のブロックに書き込む、ソース パーティションから読み取ったデータにバイナリパッチを適用して、ターゲット パーティション内の特定のブロックに書き込むなどのオペレーションです。
  • その他のデータ。更新ペイロードの大部分を占めるオペレーションに関連付けられたその他のデータは、この例にある圧縮された blob やバイナリパッチから構成されます。
3 ペイロードのメタデータがダウンロードされます。
4 メタデータで定義された各オペレーションは順番に、関連データがある場合はメモリにダウンロードされ、オペレーションが適用された後、関連付けられたメモリが破棄されます。
5 パーティション全体が再読み込みされ、予想されるハッシュに対して検証されます。
6 インストール後の手順が(もしあれば)実行されます。いずれかのステップの実行中にエラーが発生した場合、更新は失敗し、別のペイロードがあった際に再試行されます。上記のすべてのステップが成功した場合、更新は成功し、最後のステップが実行されます。
7 未使用のスロットは setActiveBootSlot() を呼び出すことによってアクティブとしてマークされます。未使用のスロットをアクティブとマークしても、起動が完了するわけではありません。ブートローダー(またはシステム自体)が正常な状態を読み取れない場合、アクティブなスロットを元に戻すことができます。
8 インストール後(後述)には、古いバージョンを稼働させながら「新しいアップデート」バージョンからプログラムを実行します。OTA パッケージで定義されている場合、このステップは必須であり、プログラムは exit コード 0 を返します。それ以外の場合、更新は失敗です。
9 システムが新しいスロットで正常に起動し、再起動後のチェックが完了すると、新しい現在のスロット(それまでの「ターゲット スロット」)は、markBootSuccessful() を呼び出して正常とマークされます。

インストール後の手順

インストール後の手順が定義されているすべてのパーティションで、update_engine は新しいパーティションを特定の場所にマウントし、マウントされたパーティションに関して OTA で指定されたプログラムを実行します。たとえば、インストール後のプログラムがシステム パーティションで usr/bin/postinstall と定義される場合、未使用のスロットにあるこのパーティションは、/postinstall_mount などの固定の場所にマウントされ、/postinstall_mount/usr/bin/postinstall コマンドが実行されます。

インストール後の手順を正常に実行するには、古いカーネルで次のことができる必要があります。

  • 新しいファイル システム フォーマットをマウントする。圧縮されたファイル システム(SquashFS など)を使用する場合の圧縮アルゴリズムなど、古いカーネルでサポートされていない限り、ファイル システムのタイプは変更できません。
  • 新しいパーティションのインストール後のプログラム形式を理解する。 ELF(Executable and Linkable Format)バイナリを使用している場合、古いカーネルと互換性があるはずです。たとえば、32 ビットから 64 ビットのアーキテクチャに切り替えた場合の古い 32 ビットのカーネル上で実行されている 64 ビットの新しいプログラムなどです。ローダー(ld)が他のパスを使用するか、静的バイナリを作成するように指示された場合、ライブラリは新しいシステム イメージではなく古いシステム イメージから読み込まれます。

たとえば、シェル スクリプトを、先頭に #! マーカーの付いた旧システムのシェルバイナリで解釈されたインストール後のプログラムとして使用し、新しい環境からライブラリパスを設定して、より複雑なバイナリでインストール後プログラムを実行します。また、インストール後の手順を専用の小さなパーティションから実行すると、メインシステム パーティションのファイル システム形式を更新する際に、下位互換性の問題や飛び石の更新を避けられます。こうすることでファクトリ イメージから最新バージョンに直接更新できます。

新しいインストール後のプログラムは、旧システムで定義された SELinux ポリシーによって制限されます。そのため、インストール後の手順は、A/B 対応ファームウェアやブートローダーの更新、新しいバージョンのデータベースのコピー準備など、特定のデバイスや他のベストエフォート型タスクでデザイン上必要なタスクを実行するのに適しています。必要な権限が予想できない再起動前の 1 回限りのバグ修正には、インストール後の手順は適しません

選択したインストール後のプログラムは、postinstall SELinux コンテキストで実行されます。新しくマウントされたパーティション内のすべてのファイルは postinstall_file でタグ付けされ、新しいシステムに再起動した後の属性には関係ありません。新しいシステムで SELinux 属性を変更しても、インストール後の手順には影響しません。インストール後のプログラムに追加の権限が必要な場合は、インストール後のコンテキストに追加する必要があります。

再起動後の手順

再起動後、update_verifier は dm-verity を使用して整合性チェックをトリガーします。このチェックは、安全なロールバックを妨げるような不可逆的な変更を Java サービスが行わないよう、zygote の前に開始されます。このプロセス中、ブートローダーとカーネルは、確認済みブートや dm-verity が破損を検出した場合にも再起動を行います。チェックが完了したら update_verifier は起動が正常に成功したことをマークします。

update_verifier は、AOSP コードの使用時に A/B OTA パッケージに含まれる /data/ota_package/care_map.txt にリストされているブロックのみを読み取ります。GmsCore などの Java システム アップデート クライアントは、care_map.txt を抽出し、デバイスを再起動する前にアクセス権を設定すると、システムが新しいバージョンで正常に起動した後でその抽出したファイルを削除します。