booklista tech blog

booklista のエンジニアリングに関する情報を公開しています。

新規プロダクト開発でOpenAPIを導入した話

アイキャッチ

簡単な自己紹介

株式会社ブックリスタでスマートフォンアプリエンジニアをしている城と申します。

現在YOMcomaというショートマンガアプリを作っています。 投稿者さん、読者さんに向けたサービスを提供予定ですので、リリースまでどうぞ楽しみにお待ちくださいませ。

OpenAPIとは

REST APIの定義方法をまとめた仕様のことで、かつてはSwaggerという名称でした。 JSONまたはYAMLでREST API定義が可能ですが、公式のサンプルはほぼYAMLで記述されており、開発現場でもYAMLで記述していることが多い印象を受けます。

参画時の状況

YOMcomaは投稿者さん向けのWEBと読者さん向けのスマートフォンアプリ(以下アプリ)があり、それぞれがREST API(以下API)でバックエンドにアクセスする構成となっています。

私は読者さん向けのアプリとバックエンドの開発を担当させていただくことになりました。 そのため本記事ではアプリ⇄バックエンドAPIの話に焦点を合わせて書かせていただきます。

アプリ開発に必要なAPIの基本設計は済んでいて、各APIの概要がドキュメントにまとめてある状態だったのでまずは詳細を定義してしまう事にしました。 OpenAPIで定義したい旨をチームメンバーに相談し「便利そうだから使ってみよう」と快諾いただけました。

YAMLによるAPI定義

サンプル

上に貼ったのは非常に簡単な例ですが、まずはYAMLを書いていきました。 私はVisual Studio Codeで記述していましたが、拡張機能のSwagger Viewerを使えば画像右側のようなプレビューを見ながら記述できるので間違いに気が付きやすかったです。 また今回はOpenAPIが初見のチームメンバーにもレビューしていただきましたが、記法は調べればすぐわかるし直感的に分かりやすいと言っていただけました。

OpenAPI仕様でYAMLを記述する方法は調べるとたくさん情報が出てくるので書き方には触れませんが、必須項目ではないけれど重要だと感じたポイントに絞って書かせていただきます。

example

パラメータや要素に例を記述できます。

後述のモックサーバーを立てる場合に返却値として活用されます。 また各要素のフォーマットを明確にしておく事で齟齬が生まれにくいので記述しておくことをおすすめします。

operationId

各APIに明示的な命名ができます。

後述のソースコード自動生成を使う場合、各APIを実行する関数名に使われるので、プロジェクトの命名規則に準じて記述することをおすすめします。 後述のドキュメント生成を使う場合、operationIdが未指定だとパーマリンクが正しく設定されなくなってしまいます。

実は色々あるYAMLの使い道

APIの詳細をYAMLで記述し、準備はととのいました。 ここからはYOMcomaでどのようにYAMLを活用していたのか紹介させていただきます。

ドキュメント生成

ReDocでYAMLファイルをドキュメントとしてHTMLに変換しました。

具体的には、コミットやプッシュ時に任意のコマンドを自動で実行できるhuskyを使用し、pre-commit時にReDocを実行しています。 これによりYAMLを改変したらドキュメントも自動更新するようにしていました。 HTMLファイルならエディタが入っていない人でも見られますし、YAMLより直感的に理解しやすいです。 API定義が見られれば作業可能なチームメンバーには、git上にあるドキュメントのパスだけ伝えることで常に最新のAPIを参照してもらえます。

huskyでpre-commit時にHTMLを出力するためのコマンド例
npm install -g redoc-cli
npm install -D husky
npx husky-init
npx husky add .husky/pre-commit 'redoc-cli bundle [yaml path] -o [output path]' 

ソースコード自動生成

YOMcomaのバックエンドはTypeScript(NestJS)で、アプリはDart(Flutter)でそれぞれ自動生成しました。

API通信部分を自動生成することで仕様と実装に乖離が生まれず、結合試験が比較的スムーズであった事もよかったです。 またAPIに修正があった時、再度ソースコード自動生成をし直す事で修正の大部分が完了する事も大きなメリットでした。

バックエンド

Himenon/openapi-typescript-code-generatorでレスポンスのスキーマ部分のみ自動生成しました。

自動生成にはほんの少し実装が必要なので、手順はリンクをご確認ください。

アプリ

OpenAPI Generatorでソースコードを自動生成しました。

Modelクラスやシリアライズ処理、APIClientクラスを自動生成し、実装工数がかなり削減できました。 ライブラリへの対応も充実していて、YOMcomaでもFlutterのHTTPクライアントとしてメジャーなライブラリDioを使ったソースコード自動生成をしています。

コマンド例
brew install openapi-generator
openapi-generator generate -i [yaml path] -g dart-dio -o ./openapi

モックサーバーを立てる

Prismでモックサーバーを立てました。

YOMcomaではバックエンド開発よりアプリ開発が先行していました。 そのためバックエンドでスタブを返すことが難しく、モックサーバーの出番となりました。 PrismはYAMLのexampleに記述した値を返してくれるので、ノーコードでモックサーバーを立てることができました。

モックサーバー起動コマンド例
npm install -D prism
npx prism mock [yaml path] -p 8080

テスト実行する

Postmanでテスト実行しました。

バックエンドのAPI開発時はPostmanにYAMLをimportしてテスト実行していました。 エンドポイントやパラメータ名を入力する手間が省けてテスト効率がアップしました。

サンプル

ハマりポイント

今回OpenAPIを使ってハマったポイントです。

allOf

type: arrayの直下にallOfや複数のpropertyを入れているケースは、OpenAPI Generatorでうまく変換されませんでした。

これはソースコードに置き換えて考えると分かりやすいのですが、List型に指定可能なのは単一のクラスです。 少なくとも私の知る言語ではList<{int id, String name}>のようには書けないので、type: arrayの下に複数の値を入れたい場合は別途Schemeを定義する必要がありました。

サンプル

さいごに

OpenAPIで定義する時にはプレビュー機能があったり、プレビューからAPIを実行できたり、他にも便利な機能がたくさんあり、きっとこれからも増えていくのではないでしょうか。 また導入コストが高くないのも大きな魅力の1つと思っています。 現在YOMcoma開発は第2フェーズに入り、チームメンバー全員がYAMLを使ってAPIを改版しており、開発サイクルが定まってきていると感じています。

この記事の内容が、これからアプリ開発をされる方の何かしらのヒントになれば幸いです。

Aurora1からAurora3へアップグレードするときのアプリケーション対応の注意点について

アイキャッチ

はじめまして。プロダクト開発部に所属しているエンジニアの姚です。
今回は「Reader Store(運営:株式会社ソニー・ミュージックエンタテインメント)」のDBをAurora1からAurora3へアップグレードしたときにアプリケーション側が対応したことを話していきます。

Aurora3導入の背景

Amazon Aurora MySQL 1 (MySQL 5.6 互換) は 2023 年 2 月 28 日にサポート終了となります。
サポート期間が終了する前にアップグレードしないといけません。
また、Aurora MySQL 2 (MySQL 5.7 互換) は 2024 年 10 月 31 日にサポート終了となります。
*2022年11月時点、最新の情報は下記公式ページにご参照ください。
Amazon Aurora メジャーバージョンが利用可能な期間

Aurora2にアップグレードしても、二年後にもう一度アップグレードすることになり、二度手間になりますので、今回はAurora1からAurora3へアップグレードすることにしました。

方針

MySQL8.0ではマイナーバージョンアップ時に後方互換を担保しないため、非推奨機能は使えなくなる可能性があります。Aurora3ではまだLTS版が出てないため、AWSのサポート期間が短かいことも予想されます。
マイナーバージョンアップ時に規模の大きな改修を行わないで済むように、アプリケーションへの影響がでる非推奨機能も調査して事前対応することとしました。

対応の流れ

アプリケーション改修

  1. 開発環境の設定変更、DBエンドポイント変更、DBドライバーのバージョンアップ
  2. 事前調査結果に基づき、MySQL8.0で廃止/変更機能、非推奨機能の改修
  3. 動作検証で検知した問題の追加改修

インフラ

  1. MySQL8.0でシステム変数のデフォルト値への対応
  2. アプリケーション改修と並行してAurora3の環境構築とデータ移行

動作検証

  1. 全機能テスト
  2. 負荷試験

リリース準備

  1. リリース手順の整備(リカバリー手順も含む)
  2. リハーサル



MySQLのバージョン変更による影響調査

基本は公式ドキュメントを参照し、MySQL5.7、MySQL8.0の廃止/変更機能+非推奨機能をリストアップして、システムに影響があるかを確認します。


アプリケーションへの影響が大きい変更

  • 廃止/変更機能
    • 予約語
    • GROUP BY暗黙/明示的のソート順変化
    • クエリーキャッシュ廃止
  • 非推奨機能
    • 文字セット(utf8)
    • システム変数explicit_defaults_for_timestamp
  • 課題
    • 一時テーブルメモリ不足のチューニング



予約語

MySQL5.6, 5.7, 8.0のキーワードーと予約語変更の公式ドキュメント: Keywords and Reserved Words

  • 予約語

    下方にキーワード表がありますが、「(R)」が付いているのが予約語です。

  • キーワードだけど非予約語

    非予約語はテーブル名などの識別子として、そのまま使っても大丈夫です。

予約語リスト

参考としてMySQL5.6から8.0の間で変更がある予約語を下記にリストアップします。

追加された予約語(31件)

CUBE
CUME_DIST
DENSE_RANK
EMPTY
EXCEPT
FIRST_VALUE
FUNCTION
GENERATED
GROUPING
GROUPS
JSON_TABLE
LAG
LAST_VALUE
LATERAL
LEAD
NONBLOCKING
NTH_VALUE
NTILE
OF
OPTIMIZER_COSTS
OVER
PERCENT_RANK
RANK
RECURSIVE
ROW
ROWS
ROW_NUMBER
STORED
SYSTEM
VIRTUAL
WINDOW

削除された予約語(1件)

ONE_SHOT

予約語の対応

DB名、テーブル名、カラム名などの識別子としてそのまま使うとダメなので、バッククォートで囲みます。

-- NG
CREATE TABLE rank (id INT PRIMARY KEY, val INT); ❌

-- バッククォートで囲めばOK
CREATE TABLE `rank` (id INT PRIMARY KEY, val INT); ✅



GROUP BY暗黙/明示的なソート順変化

ソート順を指定しないとき、GROUP BYカラムによる暗黙の昇順ソート (ASCが省略されているもの)が8.0以後なくなります。
参考:ORDER BY の最適化

変更点

  1. GROUP BYの暗黙/明示的ソート順変化
-- 発行のSQL
SELECT hoge FROM fuga GROUP BY hoge;

-- 5.7までの動作
SELECT hoge FROM fuga GROUP BY hoge ORDER BY ASC;

-- 8.0での動作
SELECT hoge FROM fuga GROUP BY hoge ORDER BY NULL;

2. GROUP BYのASCとDESCの付け方

-- 5.7まで
SELECT hoge FROM fuga GROUP BY hoge ASC;

-- 8.0
SELECT hoge FROM fuga GROUP BY hoge ORDER BY ASC;

GROUP BY仕様変更対応

  1. GROUP BYでソート順を指定していない箇所に、GROUP BYカラム順にソートするように明記
  2. GROUP BYにASCとDESCが付いているがORDER BYがない箇所にORDER BYを追加



クエリーキャッシュ廃止

クエリーキャッシュは5.7.20で非推奨に、8.0で削除されます。 過去バージョンでクエリーキャッシュが活用されていた場合に性能低下の可能性がありますので、パフォーマンスの変化に気をつける必要があります。

負荷試験を実施し、パフォーマンス劣化がありましたら、下記に限らず適切に対応します。

  • 古いデータ削除やパーティションテーブルを利用して、クエリ対象のデータを減らす
  • インメモリデータベース(Redisなど)を使ったキャッシュを導入
  • ProxySQLを使ったキャッシュの導入※
  • インフラの増強

※参考:MySQL 8.0: Retiring Support for the Query Cache
ProxySQLのキャッシュは旧来のMySQLと異なり、データ更新のタイミングでキャッシュが破棄されないので注意が必要です



文字セット(utf8)

公式ドキュメントに記載がある通りutf8文字セットが非推奨になっています。

MySQLのutf8はutf8mb3のエイリアスなので、utf8mb3 文字セットに合わせて非推奨となります。将来の MySQL リリースで削除される予定のため、かわりに utf8mb4 を使用しようした方が良さそうです。

照合順序

照合順序はデータの文字の大小関係を比較する場合の基準となるものです。

MySQL 5.6は文字セットutf8の照合順序がデフォルトutf8_general_ciとなっています。MySQL 8.0の文字セットutf8mb4のデフォルトの照合はutf8mb4_0900_ai_ciになります。

utf8mb4_0900_ai_ciだと絵文字(🍣、🍺など)が区別できません。 ソート順などを現行のまま保持したい場合は、MySQL 8.0ではutf8mb4_general_ciを明示的に指定する必要があります。

注意点
データベース、テーブル、カラムが変更対象です。文字セットを変更するときCOLLATIONを明示的に指定します。

ALTER TABLE `fuga`.`hoge`
    CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

viewは作成時の照合順序が適用されるので、テーブルの照合順序を変更した後でviewを作り直す必要があります。
なお、viewの照合順序は指定できませんが、view作成時のセッションと同じ照合順序になるためview作成前にセッションの照合順序を明示的に指定します。

-- view作成前にcollationをutf8mb4_general_ciに指定
SET @@session.collation_connection = 'utf8mb4_general_ci';

-- viewを作り直す
CREATE OR REPLACE VIEW `fuga`.`hoge_view` AS
  SELECT concat('ABC',`hoge`.`hoge_column`)
  FROM `hoge`
  WHERE ...;

文字セット移行変更方式の検討

プランA. Aurora3へアップグレード後、ALTERを発行して文字セットを変更する

  • メリット
    • 移行手順が簡単
    • データ整合性は保証できる
  • デメリット
    • 照合順序変更すると、インデックスも作り直されるので時間がかかる
    • 文字セット変換のAlter文実行中は、テーブルがロックされるのでサービス稼働中に実施できない

プランB. DMSを使ってストア稼働中に文字セットを変換する

  • メリット
    • 移行中でもデータベースは利用可能の状態になる
  • デメリット
    • データ整合性のチェックが必要となる

DMSを使った場合の具体的な対応は下記のイメージになります。

  1. 移行先のDBを作成し、utf8mb4に文字セットを変更
  2. AWS DMSでレプリケーション実施
  3. データが一致していることを確認

Amazon Database Migration Serviceを使えば、文字セットが違うDB間でもデータレプリケーションが可能です。
データ量が少ないDBはプランAで対応しやすいですが、 データ量が多いDBやサービスを長時間停止することを避けたい場合は、プランBで対応する方が良さそうです。



システム変数explicit_defaults_for_timestampをOFFにするのは非推奨

explicit_defaults_for_timestampとは

システム変数explicit_defaults_for_timestampはtimestampカラムにnull値セットの処理を有効にするかどうかを決定します。
OFFになっているとき、timestampカラムにnullをセットできますが、ONにするとnullはセットできなくなります。
詳細はMySQL 公式ページをご参照ください。

MySQL 8.0 系においてexplicit_defaults_for_timestampの設定が非推奨であり、デフォルト値 ON の動作は以下となります。

-- NG timestamp null insert
INSERT INTO hoge(..., created_tm, create_user)
values (..., null ,"aaa") ❌
-- OK 
INSERT INTO hoge(..., create_user)
values (..., "aaa") ✅

ORMを利用している場合、Entityのtimestamp項目に値を未設定のまま永続化するとtimestampにnull insertが発生する可能性があります。
なお、Aurora 1において、explicit_defaults_for_timestampを設定せず、無効化になっている事象があります。
アプリケーションがnull insertのSQLを発行しているかを気をつかないといけません。

対策として、explicit_defaults_for_timestampをOFFにすることで、timestampにnull insertのパターンも正常動作できます。
Aurora 3においてパラメータグループで explicit_defaults_for_timestamp の値は変更可能ですが、実際には反映されないので注意が必要です。

AWSサポートに問い合わせをしたところ、下記対応方法を回答いただきました。

explicit_defaults_for_timestamp を無効化する必要がある場合には、アプリケーション等でのセッション開始時に "set explicit_defaults_for_timestamp=0;" を実行いただく必要がございます。
あるいは、ドキュメントの「explicit_defaults_for_timestamp が有効になっている場合...」以下の動作を前提としてアプリケーションを修正いただくこともご検討ください。

Reader Storeにおいては、アプリケーション側でtimestampにnull insertされないように対応しました。



内部一時テーブルメモリ不足のチューニング

MySQL 8.0にアップグレード後、データ量が多いテーブルは、内部一時テーブルメモリが不足する恐れがあるので、チューニングを考慮する必要があります。

内部一時テーブルと使用するストレージエンジンについて

MySQLでは一部のクエリで内部一時テーブルを作成してます。

使用されるストレージエンジンについて、MySQL 5.6ではMemoryしかありません。
MySLQ 8.0からは従来のストレージエンジンであるMemoryに加えて、TempTableが追加され、デフォルトエンジンになります。
ストレージエンジンの仕組みは、AWSのドキュメントをご参照ください。

Aurora 3での注意点

  1. Aurora3 MySQL8.0.23のTempTableに不具合がある

    指定した一時ファイルの割り当てサイズよりデータが多かった場合に、本来は使用すべきInnoDBの内部一時テーブルが使用されず、エラーが発生します。
    参考:https://forums.percona.com/t/mysql-8-0-the-table-tmp-sql1-f519f-7-is-full/10767

  2. Aurora3 で利用可能なストレージエンジン

    パラメーターグループのinternal_tmp_mem_storage_engineでエンジンを指定できますが、リーダーインスタンスはTempTableだけが使えます。

対策

従来のMemoryを使用することにより回避できます。 リーダーインスタンスでTempTableを使用する場合は、現行のテーブルサイズから一時テーブルに使用される容量を見積もり、一時テーブルのパラメーターに適切な値を指定します。
TempTableモードで使用されるストレージ上の内部一時テーブルのサイズが不足するとSQLエラーとなってしまうため、temptable_max_mmapに余裕あるサイズを指定します。



まとめ

今回はアプリケーションエンジニアの視点から、Aurora3へのアップグレードの内容をまとめてみました。
作業するときインフラ側と密にやり取りしたり、周りの協力を得られるとスムーズに進められそうです。

MySQL8.0 へのバーション変更の影響範囲はかなり広いです。ドキュメントやコードを見るだけで全ての影響点を把握しきれてない可能性があります。
全機能テストで問題を検知したこともありますから、しっかりテストを行った方が安心です。

Apache JMeterの負荷シナリオ作りで苦労した話

アイキャッチ

はじめまして。プロダクト開発部アプリケーションエンジニアの中村と申します。
現在、「Reader Store(運営:株式会社ソニー・ミュージックエンタテインメント)」のシステム開発を主な業務として日々取り組んでいます。
今回はReader Storeの開発中に行った負荷試験の話をしていきます。

負荷試験実施の背景

Reader StoreではこれまでAmazon Aurora MySQL 1 (MySQL 5.6 互換)を使用しておりました。
しかし、2023 年 2 月 28 日にサポートが終了するため、Amazon Aurora MySQL 3 (MySQL 8.0 互換)へバージョンアップすることにしました。
バージョンアップ後もこれまでと同等の性能で動作することを確認するために負荷試験を行うことにしました。

Apache JMeterについて

Apache JMeter(以後JMeter)は、Apache Software Foundationが開発した、Webアプリケーションの機能動作及びパフォーマンス計測するためのテストツールです。
テストしたい画面のURLやリクエストパラメータやリクエスト数、スレッド数などを定義することで任意の負荷試験を行うことができます。
私はこれまで負荷試験を行ったことはありませんでしたが、本プロジェクトでは過去にJMeterを使用していたことから知見があったため、今回も使用することにしました。

公式:https://jmeter.apache.org/


また、JMeterではクラスター構成による複数台のサーバーを利用したテストを行うことができます。
1台のサーバーから負荷をかけようとするとスペックやネットワークの問題で期待している負荷をかけられない場合があります。
そのような場合に複数台のサーバーから負荷をかけることで期待通りのテストを行うことができます。

Masterサーバー1台及びSlaveサーバーを任意の台数用意し、MasterのconfigファイルにSlaveサーバーのプライベートIPアドレスを定義することでクラスター構成となります。
今回は、以前の負荷試験で利用していたWindowsのサーバーをMasterサーバーとし、SlaveとなるLinuxサーバを6台用意してクラスター構成でのテストを行いました。

なお、一部クラスター構成では実施できないテストがあるため、そういう場合はローカル環境から実行してます。 シナリオサンプル

テストシナリオの作成

作成の流れ

JMeterでテストシナリオを作成するためにまずはテスト対象の画面を決めます。
テストは全ての画面に対しては行わず、影響の大きさを鑑みて以下を基準に選定しました。

  • リクエスト数が多い画面
  • ユーザーと紐付くレコードの多いテーブルにアクセスがある画面

実際の流れは以下になります。

  1. DBのユーザに紐付く情報を保存しているテーブルのレコード数確認
  2. 各テーブルの1ユーザに紐付くレコード数を確認し、レコード数の多いテーブルを参照しているURLを確認
  3. New Relicからリクエスト数の多いURLを確認
  4. Google Analyticsから該当のURL(レコード数の多いテーブルを参照している or リクエスト数の多い)の分間あたりのリクエスト数を抽出(通常時、セール時(リクエスト増加時)でそれぞれ)
  5. Top(/)のURLを基準に、対象のURLへの導線を辿るように画面遷移のフローを決定
  6. 画面遷移フローと分間リクエスト数をもとにJMeterのシナリオを作成

なお、今回作成したシナリオは調べた分間リクエスト数を1時間実行し続けるようにします。

以下は簡単なシナリオのイメージになります。
これはTop画面を表示するだけのシナリオになります。
Thread Groupの各設定値は以下のようになります。

  • Number of Threads:テストで使用するスレッド数を指定
  • Ramp-up period:スレッド数が生成されるまでの時間(今回は60固定)を指定
  • Loop Count:シナリオの回数を指定でき、無限ループの場合はInfiniteにチェック
  • Specify Thread lifetime:DurationやStartup delayを指定する場合にチェック
  • Duration:シナリオの実行時間(今回は3600秒固定)を指定
  • Startup delay:テストを実行してからこのシナリオが動くまでの間隔を指定

画像の通りに設定した状態でシナリオを実行すると、最初の1分間に10スレッドが生成され、1時間リクエストが行われ続けるようになります。
なお、分間リクエスト数についてはConstant Throughput Timerで制御しますが、こちらについては後述で触れさせていただきます。

シナリオサンプル

シナリオ作成時のポイント(苦労したところ)

今回シナリオを作成した際に気をつけたことや苦労したことを紹介させていただきます。

ログインを一度だけ行いたい

ログインは一度行えば、セッションの有効期限が切れるかログアウトを行うまでは継続されるため、ユーザが何度も行うことはありません。
例えばマイページの画面を表示する負荷試験を行いたい場合、マイページを表示するためにログインは必要になります。
しかし、マイページ表示の前に毎回ログインさせるとログイン処理というテストの目的と沿わない負荷がかかってしまいます。

このような場合にOnce Only Controllerを使用することで実現できました。
Once Only Controller配下に定義したリクエストは、そのシナリオが繰り返し実行された場合でも最初の1度だけ行うようになります。
ログインのリクエストをOnce Only Controller配下で行うことによって、そのシナリオ内でログインを一度だけ行うようにできました。
シナリオサンプル

複数のユーザを使用してテストしたい

テストでログインするユーザを同じではなく複数ユーザで分けて使いたいということがあります。
今回、サービスを多く利用しているユーザ、平均的なユーザ、利用が少ないユーザ(=ユーザに紐づくテーブルのレコード数)を想定したユーザを用意してテストを行おうとしました。

このような場合にUser Defined VariablesUser Parametersを組み合わせることで実現できました。
まずはUser Defined Variablesには変数名(ここではIDとPASSとする)を定義します。
次にUser Parametersにはユーザごとに使用するIDとPASSの値を定義します。
最後に、ログイン処理のリクエストでパラメータの値に${ID}と指定すると、リクエストで変数を参照できるようになります。
そうすると、Thread Groupでスレッド数を2以上にした時、1スレッド目はUser ParametersのUser_1の値、2スレッド目はUser_2の値を使用するようになります。
今回、各条件のユーザのIDとパスワードをそれぞれ定義することでそれぞれのユーザを使用してテストを行うことができました。

ちなみに、User Parametersに定義したユーザ数よりもThread Groupのスレッド数の方が多い場合は、またUser_1の値から使用します。

シナリオサンプル

1ユーザで1度しか行えないテスト(例えば1日1回のログインガチャを引くなど)の負荷試験を行いたい

DBを更新する処理が行われた場合はその後繰り返しテストを行うことはできない場合もあります。
今回あったケースでは、1日1回のログインガチャを引くというテストがあり、同じユーザでは2回目以降はガチャを引けないという問題がありました。

このような場合にPost Processorを使用することで解決できました。

Post Processorをリクエストの配下に定義することで、そのリクエストが完了した後にPost Processorで定義した処理を実行できます。
今回はDBの操作したいため、JDBC PostProcessorを使用しました。
ガチャを引くというリクエストの後にガチャの結果が保存されているテーブルのデータを削除するSQLを実行することで、何度もテストを行うことができました。
実際の定義としては、対象のリクエスト配下にJDBC Connection Configuration(DBの接続情報を定義)とJDBC PostProcessorを定義して使用します。

少々話は逸れますが、クラスター構成で実行する場合は各サーバーで同じテストを行うため、上記のようなケースのテストはうまくテストできない可能性があります。
そのため、今回はそういったケースのテストだけローカルのPC上で実行するという対応をしております。

シナリオサンプル

リクエスト数の制御はConstant Throughput Timerを使用する

時間あたりのリクエスト数を制御するためにはTimerを使用する必要があります。

今回、シナリオを作成し始めた際はプラグインのThroughput Shaping Timerを使用しておりました。
こちらは標準のTimerよりも細かい制御ができる(RPSで指定できる、経過時間によってRPSの値を変更できる等)ため、高性能なTimerとなっております。
しかし、このTimerはスレッド数を増やすとリクエスト数が設定値より少なくなってしまうという問題を発見しました。
試したところ、スレッド数が4以上になると30%ほど低下していました(スレッド数と比例して低下するというわけではなかったです)。

今回は細かいリクエスト数の制御は必要ないため、標準のConstant Throughput Timerを使用することで期待通りのテストを行うことができました。
Throughput Shaping Timerでも適切なチューニングを行うことで期待したリクエスト数を実現できるかは要検証です。

クラスター構成でリクエスト数を制御するには

前述で話題に上がったConstant Throughput Timerですが、こちらはRPMでリクエスト数を指定できます。
例えば100req/分であれば100を指定します。

ただし、クラスター構成で実行する場合はサーバーの台数×RPMとなってしまうため、そのままだとサーバーの数だけリクエスト数が倍増してしまいます。
そのため、クラスター構成で実行する場合はサーバーの台数で割った数を指定する必要があります。

例えば100req/分をサーバー5台で行う場合はConstant Throughput Timerには20を指定します。

負荷試験不可の外部システムがある状態でテストするには

開発しているサービスの中では社内だけではなく社外のサービスと連携している箇所も出てきます。
負荷試験を行う場合には当然外部サービスに対しても負荷がかかってしまうため許可をいただく必要がありますが、場合によっては負荷試験がNGな外部サービスもあります。

このような場合にスタブを用意することで外部への負荷をかけずにテストを行うことができました。
今回はAWSのAPI Gatewayを利用することで簡単にスタブを用意できました。

参考:https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/how-to-mock-integration.html

失敗談

最後に、負荷試験をしていてハマったことがあったので軽くお話しさせていただきます。

今回、シナリオが全て作成できていざテストを実行してみたところ、テストの実行が途中で止まってしまう事態に遭遇しました。
必ず止まるわけではなく正常に終了することもあり、止まってしまった場合のログにエラーが出ているわけでもなかったため原因がすぐにはわかりませんでした。

最初はローカルのPCから実行していたためスペックの問題なども疑い、クラスター構成にして実行するなど試しましたが結果は変わりませんでした。
そこでログを詳しくみたところ、Slaveのサーバーのログでテストが停止する際にどのサーバーも同じシナリオで止まっていたことがわかりました。

そのシナリオの設定を確認したところ、Thread Groupの設定でテストのリクエストでエラーが返った場合の挙動の設定が他のシナリオと異なっていました。
他はContinue(テストを継続する)になっていたのに、そのシナリオだけは謝ってStop Test Now(テストを中断する)にしてしまっていました。

上記の設定の場合にはJMeterとしては正常動作となりエラーログは当然でないため、ログをしっかりと確認して停止の原因となる情報を見つけることが重要でした。
この設定ミスを発見するために数日もハマってしまったのは悔やまれますが、JMeterへの理解が深まったこととログを確認することの重要性の再認識が出来たことはせめてもの救いでした。

シナリオサンプル

まとめ

初めての負荷試験となりましたが、シナリオを作成する上でのセオリーや注意点など色々なことが学べて良い経験となりました。
ただ、JMeterは画面のボタンを押すなどの操作をしているわけではなく、あくまでその操作をした時の擬似的なリクエストを自分で作成して送信しているに過ぎません。
例えばフェデレーションログインのように外部のサービスの画面を操作したいといった場合にはどうすれば良いかなど、まだわかっていないこともあります。
上記のようなテストケースの手法についてや、JMeter以外の負荷試験ツールなどの理解も今後深めていきたいです。

この記事の内容が、これからJMeterを使用する方の何かしらの助けになれば幸いです。

auブックパスのフロントエンド技術スタック

アイキャッチ

株式会社ブックリスタ プロダクト開発部の開発・運用を担当している片山と申します。

今回は、弊社が包括的なパートナーシップを結び開発を行なっている「auブックパス (運営:KDDI株式会社)」のフロントエンドの技術スタックについて記載させていただきます。

auブックパスのフロントエンドは、開発の内製化にともない、2022年11月にjQuery/CakePHPの構成から、React/Next.jsの構成にリプレイスしました。リプレイスしてから1年ほど運用を続けているため、リプレイスしてどのような効果があったかも記載させていただきます。

言語

TypeScript

TypeScriptは豊富な型に関する表現をもつ静的型付け言語です。コンパイルすることでJavaScriptとなります。

アプリケーションの実装だけでなく、AWS CDKを使用したインフラの環境構築も含めてTypeScriptで統一しました。

リプレイス前のJavaScript/PHPのソースコードには以下のような問題がありました。

  • 可読性が低い
    • API・関数・ビューテンプレートのIN/OUTの型が明確になっておらず、中身の実装を読まないと型が分からない
  • 保守性が低い
    • 型の誤りやNull参照エラーなど全てのエラーは実行時にしか分からない

リプレイス後のTypeScriptのソースコードは以下のように改善しました。

  • 可読性が高い
    • any型の使用は原則禁止としたので、API・関数・コンポーネントのIN/OUTは型定義の実装部分のみ読めば分かる
  • 保守性が高い
    • 型の誤りやNull参照エラーなどコンパイル時にある程度エラーが分かる

TypeScriptの豊富な型に関する表現を一度使ってしまうと、もうJavaScriptには戻れません。

ライブラリー/フレームワーク

フロントエンドのライブラリー/フレームワークは、React/Next.jsを使用しています。

React

ReactはUI構築のためのJavaScriptライブラリーです。

Vue.js、Angularではなく、なぜReactなのかというと、Reactの経験者がチーム内に居たからというのが理由です。Vue.js、Angularでもきっと問題はなかったのですが、開発者の経験というのは大事な財産であるので、その財産を活用するために、Reactを採用しました。

リプレイス前のCakePHPのビューテンプレートとjQueryを使用したソースコードには、以下のような問題がありました。

  • ビューテンプレートの粒度が大きく、適切に分割されていない
    • 1つのビューテンレートのソースが長い
    • 重複したソースコードが多く存在する
  • ソースコード間の関連が分かりにくい
    • イベントの発生箇所とイベントリスナーの登録箇所がわかりにくい
    • 変更時の影響範囲がわかりにくい

リプレイス後はReactを使用して、以下のように改善しました。

  • Atomic Designを採用することで、コンポーネントの粒度が小さくなり、適切に分割されている
    • 1つのコンポーネントのソースが短い
    • 重複したソースコードが少ない
  • イベントの発生箇所とイベントリスナーの登録箇所が分かりやすい
    • ReactはonClickなどのイベントをHTML要素に直接定義するので一目瞭然
  • ソースコード間の関連が分かりやすい
    • import/exportを使用したため、モジュール間の関連が明確
    • 影響範囲がわかりやすい

上記は、Reactを使ったから改善したというよりも、リプレイスのために設計し直した効果の方が大きいですね。

Reactのコンポーネントは、原則は関数コンポーネントで作成し、状態管理や副作用の実行などはReact Hooksを使用しています。React Hooksはレンダリングのたびに呼び出されるということに慣れが必要ですが、慣れてしまえば分かりやすいコードを記載できます。

Next.js

Next.jsはReactベースのWebアプリケーションフレームワークです。

ゼロからReactでWebアプリケーションを開発するのはやることが多く結構大変です。Webサーバーを用意したり、webpackなどを使ってトランスパイルの処理を作ったり、ローカルで開発するための仕組みを作ったり、ルーティングの定義を作ったりする必要があります。Next.jsには、Webサーバー・トランスパイル・ローカル開発環境・ファイルベースのルーティングが最初から用意されていて、自分で用意する必要がありません。

また、Next.jsには、SSR・SSGによるプレレンダリング、画像最適化など有益な機能が豊富にあります。

インフラ

auブックパスのインフラはAWSを使用しており、構成としては以下の図のようになります。 ( 今回の図では分かりやすくするために最小限のサービスのみ絞り込んでおり、実際にはもっと多くのAWSサービスを使用しています )

CloudFront
CloudFront
S3
S3
ALB
ALB
Fargate
Fargate
/public配下
/_next/static配下
/public配下...
/api配下
/api配下
複数起動している
コンテナインスタンス
に振り分ける
複数起動しているコンテナインスタンスに振り分ける...
Text is not SVG - cannot display

CloudFrontで最初にリクエストを受け付けて、ALB/FargateS3にリクエストを振り分けます。

ALB/Fargateには動的コンテンツ、S3には静的コンテンツを配置しています。

動的コンテンツ(/api配下、SSR、SSG)のために、Fargateのコンテナインスタンスの中でNode.jsを使ってNext.jsのWebサーバーを起動しています。負荷分散のためにコンテナインスタンスは複数起動しているので、ALBでロードバランシングしています。

リプレイス前はEC2インスタンス上でサーバーを起動していたのですが、リプレイス後はサーバーのコンテナ化を実現できました。

環境構築

AWS CDK

インフラの環境構築は、AWS CDKを使用し、TypeScriptで実装しています。

リプレイス前はインフラのコード化は実現できておらず、同じ環境の構築は不可能で、変更履歴も分からない状態でした。

リプレイス後はAWS CDKでインフラのコード化を実現したので、コードを実行するだけで、新たな環境が作れるようになりました。インフラの変更についても、コードを変更して、そのコードを実行するだけで、インフラが変更できるようになりました。コードなのでGitの履歴を見ればインフラの変更履歴が分かります。

インフラの構築だけでなく後述するAWS CodePipeline/AWS CodeBuildを使用したデプロイ環境の構築もAWS CDKでコード化しています。

CI/CD

GitHub Actions

GitHub ActionsはGitHub上でワークフローを自動化できるGitHubの機能です。

リプレイス前は継続的インテグレーションを実現できていませんでした。

リプレイス後はGitHub Actionsにてプルリク作成時にビルド、Unitテストの実行、lintを実行することで継続的インテグレーションを実現しています。

AWS CodePipeline / AWS CodeBuild

AWS CodePipelineは継続的デリバリーを実現するためのサービスです。

AWS CodeBuildはコンパイル・テスト・デプロイを自動化するサービスです。

AWS CodePipelineにてGitHubの特定のブランチの変更を監視し、AWS CodeBuildを呼び出し、AWS CodeBuildでコンパイル・デプロイを実行しています。

監視

Datadog

Datadogはサーバー監視・分析サービスです。

auブックパスではDatadogにてログ監視、サーバー監視、アプリケーション監視、リソース監視を行なっております。

Amazon Connect

Amazon Connectはコンタクトセンター向けのクラウドプラットフォームです。

auブックパスでは障害発生時の自動オンコールを実現するためにAmazon Connectを使用しています。

詳細はこの記事に記載しています。

まとめ

以上のように、auブックパスのフロントエンドはリプレイスにより比較的新しい技術を使用した構成になっています。auブックパスにはまだ古い技術を使用した部分は残っているため、今後もリプレイスを継続いたします。

今回はauブックパスのフロントエンドにおける代表的な技術スタックだけをピックアップして記載させていただきました。

次回以降にもっと具体的なリプレイス内容・効果や、今回は記載できなかったその他の技術スタックについて記載させていただきます。

Metabaseを使用したデータ民主化に取り組んでいる話

アイキャッチ

自己紹介

はじめまして。デジタルマーケティング部データ分析チームのYZrです。主にデータ分析・活用を業務としています。今回はbooklistaで全社的に実施しているデータ民主化について記していきます。

データ民主化とは

本題に入る前に、データ民主化についてご説明します。 その時々で意味合いが変わることもあるかと思いますが、ここでは「全社員が電子書籍ストアのデータを抽出し、データを基に意思決定する土壌を作って行くこと」を意味します。

まずは、データを見る・使うことが当たり前という状態になることを目指し、更には、業務のために必要なデータの内容や欲しいデータの取得方法まで議論できる組織にしたいと考えています。

取り組みの背景

なぜデータ民主化を実施するかというと、端的に言えばもとのデータ分析フローではできることに限界があったからです。

データ民主化前

まず、データ民主化前のデータ分析フローを簡単に示した図がこちらです。

施策担当者が分析したいと思ってからデータ分析チームに分析依頼を出し、データ分析チームが優先度の高い物から分析に着手し、結果が出たものを施策担当者に受け渡すといった形です。 このデータ分析フローでは以下のような問題が生じてしまいます。

  • 意思決定に時間を要する

    • 施策担当者から別のチームに依頼し、ドメイン知識が必要な分析内容である場合は施策担当者からデータ分析チームに細かな説明をする必要がある
    • 分析チームのマンパワーに依存するため順番待ちの時間が生じる場合がある。または対応不可能な場合がある
  • データ活用、知見を得る機会の損失

    • 軽易なデータ抽出も依頼を出さなければならず、データ活用がしづらい
    • データを閲覧する機会を得づらく、知見を得る機会が減少する

このような環境ではデータが溜まっていても活用される機会が減ってしまいます。

データ民主化後

データ民主化が実現して施策担当者もデータ抽出・集計・分析ができるようになるとどうでしょうか。 データ民主化後のデータ分析フローを簡単に示した図がこちらです。

基本的には施策担当者が自らの手で分析をするようになり、煩雑なデータ分析をデータ分析チームが引き受けます。施策担当者が動かすので「分析したい」という意思ではなく「分析しよう」という意志に変わることや、データを見ることの習慣づけにも繋がると期待できます。他にも以下のようなメリットが考えられます。

  • 意思決定の高速化

    • 施策担当者がユーザ行動・購買履歴などのデータを適切に引き出し、それらを用いて速やかな仮説検証や効果検証を実施できる
    • 分析チームのマンパワーに依存しない
  • 意思決定の高精度化

    • 施策担当者が分析することで、データ分析チームでは思いつかない切り口でも分析ができ意思決定の精度を高めることできる
  • データ活用・知見拡充

    • ストアデータを活用することで顧客が求めていることを知ることができる
    • 顧客体験をより良いものするアイデアを生み出すことができる

データ民主化をする上での課題と解決手段

では、データ民主化をどのように実現していけばよいでしょうか。データ分析チームと同じ手段で施策担当者がデータ分析をするとなると、大きく3つの課題が浮かび上がります。

  1. SQL記法の学習
  2. データベースの構造や処理の理解
  3. ストアデータのテーブル構造やリレーションの理解

1.に関しては、学習コストが高過ぎるので可能であれば避けたいところです。 2.3.に関しては、テーブルやスキーマといった言葉の意味や結合の仕方、良く使うテーブルの構造に内容を絞れば実現できそうです。

SQL記法を学ばない、つまりSQLを書かずにデータ分析をする方法といえば真っ先にBIツールが思い浮かびます。社内で使用しているデータベースの種類や金額面、操作の分かりやすさなどから今回はMetabaseを導入することにしました。

学ばなければならない部分はデータ分析チームが資料を作り、簡単な分析をしながら学べる講座を開くことで対応することにしました。

Metabaseについて

MetabaseはオープンソースのBIツールです。SQLを書かずにWeb画面上でデータを抽出・集計しグラフを作成できます。また、集計結果やグラフをまとめてひとつのダッシュボードとして表示する機能もあります。閲覧権限の設定もできるので、ユーザーによって見せたくないデータがある場合にも対応できます。

Metabase自体はデータを持たず接続したデータベースを参照するつくりになっています。メジャーなデータベースにはだいたい対応しているので、とても使い勝手が良いです。対応しているデータベースの詳細は公式サイトをご確認ください。

ダッシュボードの例がこちらです。

Metabaseでのデータの抽出方法は「簡単な質問」「カスタム質問」「ネイティブクエリ」の3種類があります。画像の左から右に向かって難易度が上がります。

簡単な質問

名前の通り、この機能は最も操作が簡単で表示できるデータもシンプルです。データベースとテーブルを選択するとテーブルの中身を表示します。

カスタム質問

GUIベースでテーブルの結合、グループ化、フィルタ、ソート、行数制限などができる機能です。 データ民主化ではこのカスタム質問を使ったデータ抽出と集計ができるようになることを目指します。 ほとんどの項目はプルダウン表示の選択肢をクリックするだけで指定可能です。カスタム質問で設定した内容をMetabase側が自動的にSQLへ変換し、データの取得と表示が行われます。SQLの変換は接続するデータベースの種類に合わせて行われますので、カスタム質問の設定の仕方をデータベースによって変化させる必要はありません。

ネイティブクエリ

SQLを直接書くことができます。SQLを書ける人はこの機能を使っても良いと思います。

取り組み内容

データ分析チームが行っていることは「Metabase環境の整備」と「Metabaseを使った分析の普及活動」です。

Metabase環境の整備

Metabaseの運用管理はデータ分析チームで行っています。Metabaseを動かすためのサーバーを立てるところから、サーバー自体の管理、Metabaseの設定やユーザー管理などを含みます。システム上の問題が生じたときにもデータ分析チームが対応します。 構成を簡単に示した図がこちらです。

また、MetabaseがフリーズしたときなどMetabase用サーバーを再起動したいときに、施策担当者が簡単に再起動をかけられる仕組みも構築しました。Slackで指定のアカウント(metabase再起動くん)にメンションを飛ばすだけで再起動がかかります。 データ分析チームに連絡をして再起動を依頼していたらデータ分析チームが対応するまで作業が止まってしまいますし、煩わしさで使わなくなってしまう可能性もあります。少しでも使いづらさを感じないような工夫をしています。

Metabaseを使った分析の普及活動

Metabaseを使ったデータ分析を社内に根付かせるための普及活動をデータ分析チームで行っています。

  • テーブル定義表の作成

    データベースのよく使うテーブルについて、物理名・論理名だけではなくデータが作られるタイミングや値の説明を記述したテーブル定義表を作成しました。

  • レシピ集の作成

    データベースのに関する基礎知識やカスタム質問の操作方法(テーブル選択、結合、フィルター etc.)などの基礎的な内容と、実務で使うことを想定したカスタム質問の実用的な内容のレシピ集を作成しました。レシピ集では使うテーブルや設定する項目の手順を記述し、基礎編は操作手順を録画した動画を、実用編ではカスタムクエリを設定した画像を添付し視覚的にも分かりやすい作りにしています。

    目次(基礎編)

    データベースの基礎知識

    基礎操作レシピ

    実用レシピ

  • Metabaseの使い方講座

    レシピ集を作るだけでなく、オンラインでMetabaseの使い方やレシピ集の応用方法を説明する講座を、月に1~2回程のペースで実施しています。講座の内容は録画をして残し、講座の時間に都合がつかなかった人や新入社員にも見られるようにしています。

  • 質問を受けるSlackチャンネルの開設

    ストアデータやMetabaseの使い方に関していつでも質問できるSlackのオープンチャンネルを開設しました。オープンチャンネルなので他の人が過去に質問した内容から学びを得ることもできます。

データ民主化を始めて気づいたこと

  • サーバーを落としても問題がない環境と、再起動の簡単さが大事

    結合やフィルターの設定に慣れていないと膨大な量のデータを取得するクエリを実行してしまい、サーバーが落ちることは良くあります。サーバーが落ちることを恐れてMetabaseの使用に抵抗が出てしまっては意味が無いので、サーバーが落ちても問題がない環境を作り、落ちても問題がないことを伝え、簡単に再起動が出来る手順が確立されていることが大事だと感じます。実際に、上述のmetabase再起動くんはたびたび使われています。

  • レシピ集だけでは実務への応用が難しい

    レシピの手順に少し手を加えた程度では取得できないデータについて質問されることや、「もっと実践的な内容をやってほしい」と言われることが何度もありました。これらに対して、講座の中で説明する応用方法の難易度を上げて複雑なカスタムクエリの使い方を実演する、よく質問を受ける内容をレシピ集に追加する、講座で扱う内容を施策担当者から募集する、などの対応をしています。暫定的な対応となっているので、Metabaseを使ったからこそ出てくる疑問や要望を取りこぼさず講座内容に反映していける仕組みを作ることは今後の課題です。

さいごに

データ民主化を始めるまではレシピ集の作成や講座内容の作成など大変な部分もありましたが、Metabaseを使った分析をしている人が少しずつ増えてきている実感もあります。 データ民主化の活動を継続していき、データを最大限に活かせる組織となることを目指していきます。

このブログが少しでも読んでくださった方の参考になれば幸いです。

Lighthouse CLI を使った Basic 認証ありの環境での性能検証

アイキャッチ

はじめまして。プロダクト開発部に所属しているエンジニアの伊藤です。
弊社で開発しているKDDIのサービス、「auブックパス(運営:KDDI株式会社)」の保守開発を担当しています。
今回はそのauブックパスでLighthouse CLIを使用した性能検証の話をしていきます。

Lighthouse CLI使用の背景

auブックパスでは、昨年Webページの内部コードを一新し、メンテナンスがしやすくなったこともあり、今年度からSEO改善にも力を入れています。
今回はその中でもFIDの改善についての話をしていきます。

FIDとは

FID(First Input Delay)とは、ユーザーが最初にそのページ上で操作したときに、その操作処理が開始するまでの時間を計測したものです。
この時間が短いほど、より使いやすいWebページであるという保証になります。GoogleはFIDを「Core Web Vitals」と1つとしてSEOの指標にしています。
FIDは100ミリ秒程度になることが理想と言われています。

参考:https://web.dev/i18n/ja/fid/


FIDの改善策の1つに、画像の遅延読み込みがあります。

auブックパスは電子書籍のストアなので、トップ画面に書籍表紙の画像が多く表示されます。(ストアのトップはこちら)
何も対策しないとすべての画像を表示するまでユーザー操作処理が遅れ、FIDの測定値が悪くなります。
そのため、ユーザーが表示していない領域の画像を後で表示させるLazy Loading(遅延ローディング)を施す必要があります。

この施策がどれだけ効果があるのかを確認するため、Lighthouse CLIを使いました。

Lighthouse CLI とは

Lighthouseとは、Googleが提供するWebページの品質を計測するためのツールです。
ブラウザのGoogle Chromeに標準搭載されていて、Dev Toolsから使用できます。

ブラウザ版Lighthouse

Dev ToolsのLighthouseタブから計測したいカテゴリ(SEO、パフォーマンスなど)を選んで「Analyze page load」ボタンを押すと開いているWebページを計測してくれます。

Lighthouse CLIは、このLighthouseをNode.jsを使用してコマンドラインから実行できるツールです。
より細かい実行時の設定指定ができたり、コマンドを記述して自動化できるので柔軟な計測や複数回実行が手早くできるなどの利点があります。

Lighthouse CLI の導入・実施方法

実行環境

  • Intel MacBook Pro メモリ32GB
  • Node.js 16.17.1
  • npm 8.15.0
  • lighthouse(Lighthouse CLI) 9.6.7

計測環境について

今回は本番反映前に計測したかったので、ステージング環境を対象にしています。
アクセス制限のため、WebページにBasic認証がかけられているので、リクエストのHTTPヘッダー情報に認証情報を載せる必要があります。

CORS(Cross-Origin Resource Sharing)の対応

auブックパスはJavaScriptで外部オリジンから書籍の画像情報を取得しています。
withCredentialsを設定をしないとプリフライトリクエストでbasic認証のauth情報が渡せず、 CORSポリシーでブロックされてしまいます。
今回は性能の計測が目的なので、同一オリジンポリシーを適用させない方法で上記を解決します。

導入手順

  1. 実行環境にLighthouse CLIをインストール
    npm install -g lighthouse

  2. インストール確認
    これでバージョン番号が出たらインストールが正常に完了しています。 lighthouse --version

  3. Basic認証のID・PASSWORDをbase64エンコード
    echo -n '<ID>:<PASSWORD>' | base64

  4. ligthouseコマンドを記述して実施
    今回は以下の条件で計測を実施します。

    • 計測する値
      • 指定なし(計測できる全項目を計測)
    • 計測結果
      • jsonで出力
      • html形式でも出力可能です。
    • リクエストヘッダ
      • Authoricationの情報を搭載する。
    • Chromeのオプション
      • --disable-web-securityを追加して、同一オリジンポリシーを適用外にする
     lighthouse \
        --chrome-flags="--disable-web-security" \
        --extra-headers='{"Authorization": "Basic <3でエンコードしたIDとPASSWORD>"}' \
        --output=json \
        --output-path="[自分がレポートを置きたいディレクトリ]/[レポート名].json" \
        --quiet "[検証したい画面のURL]"
    
  5. 計測結果のjsonファイルから指定の結果を抽出
    計測結果のjsonは、各項目の計測結果の数値の他、コンソールで出たエラー内容や計測過程のスクリーンショットの情報なども入り数千行に及びます。
    目的の数値のみ取り出したい場合はcatなどを活用します。
    今回はFIDの数値が欲しいので、Lighthouse内で相当するTBT(Total Blocking Time)の値を抽出します。
    ※注意
    TBT(Total Blocking Time)は厳密に言えばFIDとは違う概念で、「Webページがユーザー操作をブロックしていた総時間数」です。
    FIDは本番の環境下で実際のユーザーが使用した時にしか測定できない値なので、Lighthouseなどの自動操作での計測値にはTBTを使うのが一般的です。

cat [自分がレポートを置きたいディレクトリ]/[レポート名].json | jq  '.audits."total-blocking-time"'

上記コマンドを打つと、以下の形式で出力されます。

{
  "id": "total-blocking-time",
  "title": "Total Blocking Time",
  "description": "Sum of all time periods between FCP and Time to Interactive, when task length exceeded 50ms, expressed in milliseconds. [Learn more](https://web.dev/lighthouse-total-blocking-time/).",
  "score": 0.66,
  "scoreDisplayMode": "numeric",
  "numericValue": 412.9999999999991,
  "numericUnit": "millisecond",
  "displayValue": "410 ms"
}

このdisplayValueが計測結果なので、この値を抽出します。
あとは上記手順を複数回行って、結果を各自保存していきます。

計測結果をGUIで見たい

CLIからLighthouseを実行したときに、想定外の画面が表示されて正しく計測されていないことに気づけない場合があります。
実際私が計測した時は、--disable-web-securityのオプションをつけず実行してエラー画面が出ていることに気づけませんでした。
そういう時には、出力された結果のファイルを「Lighthouse Report Viewer」にドラックアンドドロップしてください。
最終的に表示されたページの画像・計測過程のスクリーンショットなどが見えるので想定外の計測がされていないかを確認できます。

Lighthouse Report Viewer

まとめ

今回あくまで最低限のLighthouse CLIの活用方法について触れてきました。
この記事が読んでいる方の手助けになれば幸いです。

iOS16の新機能「ロック画面ウィジェット」を推し活アプリに導入してみた話

アイキャッチ

はじめまして。株式会社ブックリスタ プロダクト開発部の酒井です。
普段はスマホアプリエンジニアとして推し活アプリ「Oshibana」の開発を行なっています。
Oshibanaについては以前の記事で紹介をしていますので、ぜひともご一読ください。

App Storeで3位になったアプリをFlutterでつくっている話
https://techblog.booklista.co.jp/entry/2022/07/11/155531

iOS16について

2022年6月に開催されたWWDC2022にて、iOS16の新機能が発表されました。
今回iOS16ではロック画面のカスタマイズが強化されており、中でも「ロック画面にウィジェットを配置できる」という機能が注目されています。

Oshibanaでは推し活に役立つウィジェットを作成できる機能を提供しています。
弊社はiOS16の新機能であるロック画面ウィジェットに目を付け、Oshibanaで作成したウィジェットをホーム画面だけでなくロック画面にも配置できるようにすることで、より推しへのアプローチが増えると考え、いち早くOshibanaへ導入するため開発に着手していました。

この記事では、iOS16で追加された新機能である「ロック画面ウィジェット」の紹介と、 その機能をOshibanaへ組み込んだ方法について紹介していきます。

ロック画面ウィジェットの紹介

<概要>

iOSではホーム画面にウィジェットを配置できる機能がiOS14の頃より提供されています。
今回、iOS16からはこのウィジェットをロック画面にも配置できるようになりました。
ウィジェットはロック画面に固定で表示されている時計の上下に配置できます。
配置できるウィジェットの種類は以下の通りです。
・インラインウィジェット・・・時計の上に配置できるテキスト1行分のウィジェット
・円形ウィジェット・・・時計の下に配置できる円形のウィジェット
・長方形ウィジェット・・・時計の下に配置できる横長(円形2つ分)のウィジェット

ロック画面

上記画像はロック画面上にiOSの標準アプリである「カレンダー」「バッテリー」「アラーム」「天気」のウィジェットを配置してみたものです。
ホーム画面に配置できるウィジェットと比べ、それぞれサイズが非常にコンパクトなものとなっています。
これは今回のロック画面ウィジェットがApple Watchで配置できるウィジェットのデザインを踏襲しているものであり、腕時計のようにアプリのデータが一目で確認できることを目的としているためです。
例えばカレンダーなら直近の予定が一目でわかり、バッテリーなら電池の残量、アラームならアラームが鳴る時刻、天気なら気温が分かるようになっています。
このように、いかにユーザーへ素早く情報を伝えるかがロック画面に配置されるウィジェットの役割として重要なポイントとなります。

また、制約として、配置されるウィジェットは必ずモノクロのカラーリングとなります。
よって、画像は白黒となり、文字や線などに色をつけることができないので注意してください。

<ウィジェット配置方法>

以下の手順でロック画面にウィジェットを配置できます。
1. ロック画面を表示し、画面を長押し(※1)
2. 画面下に現れた「カスタマイズ」ボタンをタップ
3. 時計の上もしくは下のエリアをタップ
4. 表示されたアプリの一覧から配置したいウィジェットを持つアプリを選択
5. 配置したいウィジェットの種類を選択(※2)
6. アプリ一覧を閉じ、右上の完了ボタンをタップ
7. 「壁紙を両方に設定」ボタンをタップ

(※1) パスコードロックを解除しておかないと長押しを行っても反応しません。ホーム画面を表示した状態で端末の一番上から下に指でスライドさせるとパスコードロック解除済の状態でロック画面を表示させることができます。

(※2) 配置スペースが足りないと配置できません。ウィジェットを追加で配置する場合は既に配置済のウィジェットを削除しておく必要があります。

ロック画面 チュートリアル1ロック画面 チュートリアル2ロック画面 チュートリアル3ロック画面 チュートリアル4ロック画面 チュートリアル5



Oshibanaへの導入

Oshibanaでは様々なウィジェットが用意されていますが、今回は既存のウィジェットからロック画面のデザインに適応できそうな下記4つのウィジェットを選定し、開発しました。
・画像ウィジェット(インラインウィジェットでは表示不可)
・推し始めてウィジェット
・生誕からウィジェット
・デビューからウィジェット

ロック画面 Oshibanaイメージ

将来的には他のウィジェットの実装も検討しています。
しかし、ロック画面ウィジェットは前述の通りサイズが小さく、色がモノクロになるため、コンテンツを表示できる範囲に限界があり、画像の上にテキストを表示させるようなデザインだと見辛くなってしまうなどの問題があります。
なので、ウィジェットのデザインはシンプルなものが望ましく、表示される情報も即時性が高いものを優先させるなど工夫が必要であるため、慎重に検討を進めています。

※iOS16の機能を開発するためには、Xcode14以上が必要です。

Widgetターゲットの新規追加

最初にロック画面ウィジェット用のWidgetターゲットを追加します。
プロジェクトファイルのTARGETSで+ボタンを押下し、ターゲットの選択画面を表示し、「Widget Extension」を選択し、Nextを押下します。
2つ目の画面では、Product Nameを入力し、Include Configuration Intentのチェックを外し、ProjectとEmbed in ApplicationでRunnerを選択し、Finishを押下します。

※RunnerはFlutterでiOSアプリを作った際にデフォルトで作成されるプロジェクト名となります。

スクリーンショット1

スクリーンショット2

Widgetターゲットを追加すると、自動的にProduct Nameに設定した名称でフォルダとswiftファイルが作成されます。
自動追加されたswiftファイル内の各クラスやメソッドについてはホーム画面ウィジェットの作成時と同様のため、詳細な解説は省略し、ロック画面ウィジェットに関係ある部分のみ記載します。

サポートファミリーを定義

下記のように、supportedFamiliesに
・accessoryCircular
・accessoryRectangular
・accessoryInline
の3つを定義します。

struct oshibana_lock_widget: Widget {
    let kind: String = "oshibana_lock_widget"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            oshibana_lock_widgetEntryView(entry: entry)
        }
        .configurationDisplayName("ロック画面ウィジェット")
        .description("設置したいウィジェットを選択しましょう")
        .supportedFamilies([.accessoryCircular,.accessoryRectangular,.accessoryInline])
    }
}

この時点でロック画面ウィジェットは配置可能になっているので、シミュレーターか実機にデプロイし、動作確認を行なってみてください。

Intentsターゲットの新規追加

ロック画面に表示するウィジェットの種類をユーザーが選択できるようにするため、ロック画面ウィジェット用のIntentsターゲットを追加します。

スクリーンショット3

スクリーンショット4

プロジェクトファイルのTARGETSで+ボタンを押下し、ターゲットの選択画面を表示し、「Intents Extension」を選択し、Nextを押下します。
2つ目の画面では、Product Nameを入力し、Include UI Extensionのチェックを外し、ProjectとEmbed in ApplicationでRunnerを選択し、Finishを押下します。

スクリーンショット5

スクリーンショット6

Intentsターゲットを追加すると、Widgetターゲットの時と同様にフォルダとswiftファイル(デフォルト名はIntentHandler.swift)が作成されます。
こちらも作成されたクラスやメソッドの内容はホーム画面ウィジェットの作成時と同じであるため、ロック画面ウィジェットに関係する部分以外の解説は省略します。

Intentsターゲットを追加したら、IntentsターゲットページのSupported Intentsにアイテムを追加します。
既存でホーム画面ウィジェット用のIntentsが存在しているため、別名で登録します。

スクリーンショット7

SiriKit Intent Definition Fileの作成

次に、作成されたロック画面用widgetフォルダの配下にSiriKit Intent Definition Fileを作成します。
ファイル選択後、Save Asにファイル名を入力し、TargetsでRunnerとロック画面のwidgetターゲットとIntentターゲットを選択してCreateボタンを押下します。

スクリーンショット8

スクリーンショット9

作成された.intentdefinitionファイルを開き、下記画像のように設定します。
画像ではParametersで独自のパラメータを定義してますが、作成したいウィジェットの内容に合わせて変更してください。

スクリーンショット10

IntentHandlerを構成

.intentdefinitionファイルを設定した後は、ウィジェット表示内容の選択肢を定義するため、IntentHandler.swiftの修正を行います。

※下記はサンプルのため、一部省略しています。

class IntentHandler: INExtension, LockConfigurationIntentHandling {

    func provideLockwidgettypeOptionsCollection(
        for intent: LockConfigurationIntent,
        with completion: @escaping (INObjectCollection<LockWidgetType>?, Error?) -> Void) 
    {

        var widgetTypes: [LockWidgetType] = []
        widgetTypes.append(LockWidgetType( 省略 ))

        let allCatIdentifiers = INObjectCollection(items: widgetTypes)
        completion(allCatIdentifiers, nil)
    }

    override func handler(for intent: INIntent) -> Any {
        return self
    }
}

provideLockwidgettypeOptionsCollection関数内に.intentdefinitionファイルで定義したパラメータのリストをIntentに対して設定する処理を実装します。
ここでリストを定義することで、ウィジェットをタップした際に動的な選択肢を表示させることができます。
上記例では省略していますが、Oshibanaではアプリで入力したウィジェットの設定値をローカルのDBに保存し、provideLockwidgettypeOptionsCollection関数内で保存したデータを呼び出してリストに設定しています。

IntentTimelineProviderを継承

次に、Widgetターゲット作成時に自動生成されたswiftファイルの中身をIntentsに適用させるよう変更します。

※下記はサンプルのため、一部省略しています。

struct Provider: IntentTimelineProvider {
    typealias Intent = LockConfigurationIntent

    @available(iOSApplicationExtension 16.0, *)
    func recommendations() -> [IntentRecommendation<LockConfigurationIntent>] {
        []
    }

    func placeholder(in context: Context) -> OshibanaEntry {
        let conf = LockConfigurationIntent()
        return OshibanaEntry(date: Date(), configuration: conf)
    }

    func getSnapshot(for configuration: LockConfigurationIntent, in context: Context, completion: @escaping (OshibanaEntry) -> ()) {
        let entry = OshibanaEntry(date: Date(),configuration: configuration)
        completion(entry)
    }

    func getTimeline(for configuration: LockConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        let entry = OshibanaEntry(date: Date(),configuration: configuration)
        let timeline = Timeline(entries: [entry], policy: .atEnd)
        
        省略

        completion(timeline)
    }
}

struct oshibana_lock_widget: Widget {
    let kind: String = "oshibana_lock_widget"

    var body: some WidgetConfiguration {
        return IntentConfiguration(kind: kind, intent: LockConfigurationIntent.self, provider: Provider()) { entry in
            return oshibana_lock_widgetEntryView(entry: entry)
        }
        .configurationDisplayName("ロック画面ウィジェット")
        .description("設置したいウィジェットを選択しましょう")
        .supportedFamilies([.accessoryCircular,.accessoryRectangular,.accessoryInline])
    }
}

デフォルトではTimelineProviderが継承されていましたが、IntentTimelineProviderを継承するよう変更します。
IntentTimelineProviderへの変更に応じて、placeholder、getSnapshot、getTimelineの引数と戻り値の定義を変更し、typealiasを宣言します。
WidgetConfigurationでreturnしているオブジェクトもStaticConfigurationからIntentConfigurationに変更します。

ロック画面ウィジェットにおける基本的な実装は以上です。
後はEntryViewでウィジェットのデザインを整え、Timelineからパラメータを受け渡してウィジェット毎に表示結果を変更するなどの機能追加を行なっていけば良いと思います。

開発中に苦労したこと

1.Xcode14で実機ビルドするとエラーが発生した

Xcode14をインストールし、ソースをビルドすると以下のエラーが発生しました。

error build: '/Users/User/Library/Developer/Xcode/DerivedData/Runner-ftqsuopsbckjgpfaojzqmthsozpo/Build/Products/Debug-iphoneos/Alamofire/Alamofire.framework/Alamofire' does not contain bitcode. You must rebuild it with bitcode enabled (Xcode setting ENABLE_BITCODE), obtain an updated library from the vendor, or disable bitcode for this target. file '/Users/User/Library/Developer/Xcode/DerivedData/Runner-ftqsuopsbckjgpfaojzqmthsozpo/Build/Products/Debug-iphoneos/Alamofire/Alamofire.framework/Alamofire' for architecture arm64

Oshibanaで利用しているライブラリの1つであるAlamofireでエラーが発生していましたが、ここでは直接関係は無く、原因は「Xcode14からはbitcodeが非推奨になったため」でした。

[参考サイト]
Xcode 14 Release Notes

対策として、TARGETSからRunnerを選択し、Build SettingsのEnable BitcodeをYESからNOに変更すれば解消しました。

2.iOS15.5以下の端末でアプリを起動させたら、ホーム画面に設置済だったウィジェットが全て真っ白になった

iOS15.5以下の端末にロック画面ウィジェットの対応が入ったアプリをインストールしたところ、ホーム画面に設置済だったウィジェットが全て真っ白になる、及びウィジェット配置時のアプリ一覧にOshibanaが表示されなくなる事象が発生しました。

iOS16からIntentRecommendationというクラスが追加されており、watchOSにてIntentを設定する際、推奨インテント構成を記述するためのメソッドであるrecommendations関数を実装する必要があリます。
今回はiOSのみの対応であるため本来なら不要な処理ですが、おそらくXcode14では上記クラスをOSの種類やバージョンに関わらず呼ぶようなデプロイが行われており、iOS15では上記クラスが見つからずにエラーが発生し、WidgetKitフレームワークのシンボルが見当たらないとされ、Oshibanaのウィジェット機能そのものが起動しなくなってしまったものと思われます。
(今後のバージョンアップによって修正される可能性はあります)

対策として、以下のコードを既存のホーム画面ウィジェットの処理であるProvider内にも追加しました。

@available(iOSApplicationExtension 16.0, *)
func recommendations() -> [IntentRecommendation<Intent>] {
    return []
}

[参考サイト]
AppleDeveloperフォーラム #709233
Apple公式documentation - IntentRecommendation

3.VSCodeでビルドエラーになり、シミュレーターが起動できず、デバックができなくなった

Oshibanaのアプリ側はFlutterで実装しているため、VSCodeで開発していましたが、ビルド時にエラーが発生するようになりました。
原因はbeta版のXcodeを開発に使っており、VSCodeのビルドで指定するXcodeが正規版(バージョン:13.4)の方になっていたため、iOS16で新規追加されたAPIがビルドできずエラーになっていました。
下記コマンドをターミナルで実行し、Xcode13.4ではなくXcode14betaを指定するよう変更すればビルドできるようになりました。

sudo xcode-select -s /Applications/Xcode-Beta.app

初歩的なミスでしたが、Xcodeのbeta版を使っていたり、複数バージョンを別名にして使い分けていたりすると、よく忘れる内容だと思います。

4.Codemagicで本番用アプリのデプロイを行うとエラーになった

Oshibanaではipaを生成する際、Flutterで作ったアプリのデプロイを自動化できるCIツール「Codemagic」を採用しています。
開発用ipaのデプロイを行なった時は問題なくipaが作成されたのですが、AppleStoreに申請する本番用ipaを作成するためビルドを行なった際、以下のエラーが発生しました。

Error (Xcode): No signing certificate "iOS Development" found: No "iOS Development" signing certificate matching team ID "XXXXXXXX" with a private key was found.

Xcode14からビルドオプションの「CODE_SIGNING_ALLOWED」のデフォルト値が"NO"から"YES"になったらしく、ビルド時にコード署名を行おうとしますが、Codemagicで署名しようとすると署名証明書が見つからずエラーになってしまうようです。

対策として、以下のコードをPodfileに追記しました。

post_install do |installer|
  installer.generated_projects.each do |project|
      project.targets.each do |target|
          target.build_configurations.each do |config|
              config.build_settings["DEVELOPMENT_TEAM"] = "XXXXXXXX"
          end
      end
  end
  installer.pods_project.targets.each do |target|
      if target.respond_to?(:product_type) and target.product_type == "com.apple.product-type.bundle"
        target.build_configurations.each do |config|
            config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'
        end
      end
  end
・・・

「CODE_SIGNING_ALLOWED」を“NO”にする設定を追加することにより、ビルド時にコード署名が行われなくなり、ビルドが通るようになります。
コード署名されてないipaはリリースできませんが、Codemagicの「Build」フェーズの後続に「Distribution」フェーズがあり、そこで自動コード署名を行なっているため、問題ありません。

[参考サイト]
Github CocoaPods issue #11402

5.ロック画面ウィジェットの対応が入ったアプリをAppleStoreに申請してみたが、申請できなかった

iOS16のbeta版が配布されているため、お試しで使える方に使ってもらおうとロック画面ウィジェット機能を導入したバージョンのOshibanaをAppleStoreに申請してみましたが、申請自体ができませんでした。

AppleStore

そもそもbeta版での開発内容(Apple Beta Software Program)は機密情報に該当するため、言われてみればそりゃそうだという感じでした。
この件について認知していない方は意外と少なくはないかもしれないと思ったので、記載させて頂きました。
参考:Apple Beta Software Program よくある質問

やってみた感想

今回の開発はbeta版が提供された時点から着手をしていました。
やはり挙動も少し不安定で、上記で記載した「苦労したこと」以外にも、Oshibana特有の仕様に影響する不具合もいくつか発生し、ブログには記載しづらい部分でも多くの苦労がありました。
何より、新機能ということでネット上に情報が少なく、解消方法を見つけるのも大変で、数時間前にAppleDeveloperフォーラムへ書き込まれた内容を参考に問題が解決したということも多々ありました。

しかし、新しい機能を実際に試して動かせるというところにとても新鮮味があり、なかなか面白い経験をすることができました。
更に、どこよりも早く新機能を実践投入できることへの期待感も高く、苦労したこと以上に楽しさを見いだすことができ、多くの知見を得られました。

2022年夏時点では、iOS16の開発に関する情報も日本だとまだ多くないため、この記事が皆さんのiOSアプリ開発に役立つことを願っています。