TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

939

こんにちは。zozoバックエンド部の廣瀬です。 弊社のサービスではDBMSとしてMicrosoft社のSQL Serverを使用している箇所があります。 本記事では、過去に経験したSQL Server関連のトラブル及びその調査内容をご紹介し、最後にトラブルシューティングを通して策定した統計情報の更新に関する方針をまとめます。 トラブル発生 ある日突然、SQL Serverが稼働しているサーバーのCPU使用率が高騰し、100%に張り付く状態が一定期間続きました。 CPU使用率のグラフを見ただけでは、なぜ突然高騰したのか原因は分かりません。 そのため、原因を特定するための調査を実施しました。 トラブルシューティング 同一ホスト上で稼働している主要なプロセスはSQL Serverしか無かったため、SQL Server上でCPUリソースを多く消費するようなクエリが実行されていることを疑いました。 現在実行中のクエリのステータスを確認するため、動的管理ビュー(Dynamic Management Viewの略。以下DMVと呼ぶ)を使用したクエリを実行します。 ※実行には VIEW SERVER STATE権限 が必要です。 SELECT TOP 500 dest.TEXT AS [Command text] ,( SELECT TOP ( 1 ) lastwaittype FROM master.dbo.sysprocesses WHERE spid = der.session_id ) AS lastwaittype ,datediff(s, der.start_time, GETDATE()) AS time_sec ,datediff(s, der.start_time, GETDATE()) / 60.0 AS time_min ,DB_NAME(der.database_id) AS DatabaseName ,der.command ,des.login_time ,des.[host_name] ,des.[program_name] ,der.session_id ,SUBSTRING(dest.TEXT, der.statement_start_offset / 2 , ( CASE WHEN der.statement_end_offset = - 1 THEN LEN(CONVERT(NVARCHAR(MAX), dest.TEXT)) * 2 ELSE der.statement_end_offset END - der.statement_start_offset ) / 2 ) AS current_running_stmt ,qp.* -- 実行プラン FROM sys.dm_exec_requests der INNER JOIN sys.dm_exec_connections DEC ON der.connection_id = DEC.connection_id INNER JOIN sys.dm_exec_sessions des ON des.session_id = der.session_id OUTER APPLY sys.dm_exec_sql_text(sql_handle) AS dest OUTER APPLY sys.dm_exec_query_plan(plan_handle) AS qp WHERE des.is_user_process = 1 AND datediff(s, der.start_time, GETDATE()) >= 1 ORDER BY datediff(s, der.start_time, GETDATE()) DESC 実行結果の一部を抜粋します。クエリの内容は伏せさせていただきますが、同一のクエリが多数実行中で、かつ最長で20秒間も実行中の状態でした。 また、lastwaittypeカラムの多くがCPU高負荷の際に発生することが多い SOS_SCHEDULER_YIELD となっており、突然のCPU使用率高騰との関連性が考えられます。 このクエリの平均のCPU使用時間を確認するため、さらに別のDMVを使ったクエリを実行します。 SELECT TOP 1000 [Average CPU used(ms)] = total_worker_time / qs.execution_count / 1000 ,[Total CPU used(ms)] = total_worker_time / 1000 ,[Execution count] = qs.execution_count ,[Individual Query] = SUBSTRING(qt.TEXT, qs.statement_start_offset / 2 , ( CASE WHEN qs.statement_end_offset = - 1 THEN LEN(CONVERT(NVARCHAR(MAX), qt.TEXT)) * 2 ELSE qs.statement_end_offset END - qs.statement_start_offset ) / 2 ) ,[Parent Query] = qt.TEXT ,DatabaseName = DB_NAME(qt.dbid) ,last_execution_time ,creation_time ,[Total Duration(ms)] = total_elapsed_time / 1000 ,[Average Duration(ms)] = total_elapsed_time / qs.execution_count / 1000 ,qp.* FROM sys.dm_exec_query_stats qs OUTER APPLY sys.dm_exec_sql_text(qs.sql_handle) AS qt OUTER APPLY sys.dm_exec_query_plan(plan_handle) AS qp WHERE qt.TEXT LIKE '%ここにストアド名やクエリの一部を指定することで絞り込む%' ORDER BY total_worker_time / qs.execution_count DESC 平均のCPU使用時間が約5秒と非常に長いです。 このクエリの1分当たりのクエリ実行回数を計算してみたところ、他のクエリと比べて実行頻度が高いようです。 したがってこのクエリによってCPU高負荷となった可能性が非常に高いと判断しました。 該当のクエリは、かなり前から実行されているクエリだったため、突然実行プランが狂った可能性を疑います。 DMVを使ったクエリでは推定実行プランは取得できますが、実際の実行プランもみれると嬉しいです。 そのためSSMS (SQL Server Management Studio)上で[実際の実行プランを含める]にチェックをつけた状態で該当クエリを実行しました。 ※実際の実行プランを取得する方法として、他には拡張イベントを使用する方法があります。 取得した実行プランの中で、キー参照を約50万回おこなっている箇所があり、ここがボトルネックのようです。 予測実行回数(6301.45)の約80倍も実行されていることが見て取れます。 ボトルネックが判明しましたが、今回はもともと十分な速さで実行されていたクエリが、突然遅くなったという事象です。 そのため、ボトルネックを改善するためのチューニング方法ではなく、なぜ突然CPU高負荷な実行プランが生成されてしまったかを考える必要があります。 原因として、統計情報が古くなっていたことを疑い、該当クエリで使用しているテーブルの 統計情報を更新 してみました。 --統計情報を更新するためのクエリ update statistics <テーブル名> その結果、下図のように問題ない速度で実行されるようになりました。 ※今回は統計情報の更新後にクエリがリコンパイルされることを期待し、期待通りリコンパイルされました。ただし、統計情報の更新=必ずリコンパイル、というわけでもないようです。 https://www.brentozar.com/archive/2015/01/updating-statistics-cause-recompile-no-data-changed/ SQL Serverにおいて、統計情報は自動的に更新される仕組みも用意されています。ただしレコード数の20%が更新されたとき等、自動更新のためには条件を満たす必要があります。 今回のトラブルでは、自動更新が走る前に統計情報が古くなってしまったことで実行プランが狂ったと判断しました。そのため恒久的な対応策として1日1回、定期的に全テーブルの統計情報を更新するジョブを作成しました。 これで今回のトラブルシューティングは完了したはずでした。 トラブル再発 後日、またCPU高負荷な状態に陥ってしまいました。しかも犯人は同じクエリです。 何故だろうとクエリと再度にらめっこしていたところ、気づいたことがありました。それはレコード更新の性質がテーブルによって異なるということです。 クエリに登場するテーブルは開発時に使用した経験があり、各テーブルのレコード更新の性質についてたまたま熟知していました。 テーブルA : ユーザーの行動によって少しずつ自然に変化していく テーブルB : バッチ処理で定期的に一定のレコード数が変化する 対応策として実施した1日1回の定期的な統計情報の更新は、テーブルAのような性質を持ったテーブルには効果的でしたが、テーブルBに対しては効果が薄いようです。 よく「統計情報が古い」から実行プランがおかしくなったという表現を使いますが、これは決して統計情報の最終更新日から時間が経過している、という意味では無いと思い知らされました。 正確には「統計情報が古い」とは、「統計が実際のデータ分布と大きく乖離している」状態を指します。 そのためたとえ1時間前に統計情報を更新したばかりでも、バッチ処理等で大量にデータ更新された後では、現在の統計情報は「古い」といえます。 上記考察を踏まえて、定期的な統計情報の更新に加えて、バッチ処理等でレコード数に大きな変化がある場合は処理完了直後に統計情報の更新を実施するようにしました。 その結果、CPU高負荷になることはなくなりました。 統計情報の更新に関する方針 今回のトラブルシューティングを通して学んだ、統計情報の更新に関する方針をまとめます。 下記2点の方針で統計情報を更新することで、「統計情報が古い」ことに起因する実行プランの変化とそれに伴う実行速度の低下および、CPU使用率の上昇を避けることが期待できます。 ユーザーの行動等によって自然に少しずつレコード数が変化していく性質をもったテーブルは、1日1回等、定期的に統計情報を更新 バッチ処理等で、大量にレコード削除/挿入/更新を行う場合は、処理完了直後に統計情報を更新 統計情報更新のためのクエリ 全DBの全テーブルの統計情報を更新するための具体的なクエリも紹介します。こちらをSQL Serverのジョブとして定期的に実行する、などの方法が考えられます。 尚、SQL Serverの メンテナンスプラン にも統計情報更新の仕組みは用意されています。 ただしサンプル数を指定する際、全テーブル一律でフルスキャンまたは一定のサンプリングレートしか指定できないようです。 update statisticsを実行する際、サンプリングレート未指定の場合はテーブルのレコード数によって動的にサンプリングレートが決定されます。 この挙動を全テーブルに対する統計情報の更新処理で実現するために、自分でクエリを作成しました。 DECLARE @Database sysname ,@sql nvarchar( 2000 ) DECLARE CUpdateStatsDatabases CURSOR FAST_FORWARD FOR SELECT name FROM master.sys.databases WITH (NOLOCK) WHERE -- システムDBおよびディストリビューションDB除外、かつオンラインのDBに限定 Cast(CASE WHEN name IN ( 'master' , 'model' , 'msdb' , 'tempdb' ) THEN 1 ELSE is_distributor END As bit) = 0 AND state = 0 OPEN CUpdateStatsDatabases FETCH NEXT FROM CUpdatestatsDatabases INTO @Database -- DB単位のループ WHILE @@fetch_status = 0 BEGIN set @sql = '' -- useを使う必要があるが、use単体でexecuteすると実行後にコンテキストが現在のDBに戻ってしまう。そのため丸ごと動的SQLで実行する set @sql = @sql + ' USE ' + CAST(@Database AS NVARCHAR( 100 )) + ';' set @sql = @sql + ' DECLARE @sql_update nvarchar(500) ' set @sql = @sql + ' ,@Table sysname ' set @sql = @sql + ' ,@Schema sysname ' set @sql = @sql + ' DECLARE ' set @sql = @sql + ' CUpdateStatsTables_' + CAST(@Database AS NVARCHAR( 100 )) + ' CURSOR FAST_FORWARD FOR ' set @sql = @sql + ' SELECT ' set @sql = @sql + ' SCHEMA_NAME(schema_id) ' set @sql = @sql + ' ,name ' set @sql = @sql + ' FROM ' set @sql = @sql + ' sys.tables WITH(NOLOCK) ' set @sql = @sql + ' ' set @sql = @sql + ' OPEN CUpdateStatsTables_' + CAST(@Database AS NVARCHAR( 100 )) + ' ' set @sql = @sql + ' FETCH NEXT FROM CUpdateStatsTables_' + CAST(@Database AS NVARCHAR( 100 )) + ' INTO ' set @sql = @sql + ' @Schema, @Table ' set @sql = @sql + ' ' -- テーブル単位のループ ' set @sql = @sql + ' WHILE @@fetch_status = 0 ' set @sql = @sql + ' BEGIN ' set @sql = @sql + ' ' set @sql = @sql + ' set @sql_update = ''update statistics '' + CAST(@Schema AS VARCHAR(100)) + ''.'' + CAST(@Table AS VARCHAR(500)) ' set @sql = @sql + ' execute sp_executesql @sql_update ' set @sql = @sql + ' select @sql_update ' set @sql = @sql + ' ' set @sql = @sql + ' FETCH NEXT FROM CUpdateStatsTables_' + CAST(@Database AS NVARCHAR( 100 )) + ' INTO ' set @sql = @sql + ' @Schema, @Table ' set @sql = @sql + ' END ' set @sql = @sql + ' ' set @sql = @sql + ' CLOSE CUpdateStatsTables_' + CAST(@Database AS NVARCHAR( 100 )) + ' ' set @sql = @sql + ' DEALLOCATE CUpdateStatsTables_' + CAST(@Database AS NVARCHAR( 100 )) + ' ' execute sp_executesql @sql FETCH NEXT FROM CUpdatestatsDatabases INTO @Database END CLOSE CUpdateStatsDatabases DEALLOCATE CUpdateStatsDatabases それでも残る再発の可能性 仮に統計情報をサンプリングレート100%でフルスキャンし、かつ定期的な更新で最新の状態に保ち続けたとしても、クエリが突然遅くなる可能性はまだあります。 ストアドプロシージャやパラメータ化クエリの場合、SQL Serverはコンパイル時に渡されたパラメータを考慮して、最適な実行プランを生成します(パラメータスニッフィングと呼ばれています)。 この時に渡されたパラメータがたまたま非典型パラメータの場合、それ以外の典型的なパラメータにとっては遅い実行プランになってしまう恐れがあります。 Microsoftのブログ の中で、ストアドプロシージャであればwith recompileを指定するなど、実行時に強制的に毎回コンパイルさせることで非典型パラメータに関する問題を回避させる案が紹介されています。 こちらは効果的ですが、毎回コンパイルの分だけ実行時間とCPU使用時間が増大してしまい、ユーザーおよびサーバーにとってマイナスの側面もあります。そのためできる限り使用は避けるべきと考えています。 その他の対策としてはOPTIMIZE FOR UNKNOWNという クエリヒント を使用する方法もあります。 こちらはクエリのコンパイルおよび最適化の際に、その時渡されたパラメータ値ではなく、統計データを使用するよう指定するヒントです。 まとめ 本記事では、実際に経験したSQL Serverに関するトラブルから学んだ統計情報の更新に関する方針について紹介しました。 本記事に出てくる技術的な内容や調査用のクエリはほとんどWeb上で既出の内容かと思います。 しかしながら個々の知識を組み合わせて実際に起きたトラブルを調査し、解決まで至ったというプロセスを紹介する記事はあまり無いように思います。 本記事がトラブルシューティングの実例として参考になれば幸いです。 スタートトゥディテクノロジーズでは、一緒にサービスを作り上げてくれるエンジニアを大募集中しています。 ご興味のある方は、こちらからご応募ください! https://www.starttoday-tech.com/recruit/
アバター
こんにちは! 最近暑いのでビール最高と感じている新事業創造部バックエンドエンジニアのりほやんです。 今回はAppleが提供しているお試し価格の機能のひとつである無料トライアル機能を紹介します。 お試し価格とは お試し価格とは自動更新の定期購読に対して割引価格を設定したり、定期購読の開始時に一定の無料トライアル期間を設けることができる機能です。 お試し価格には都度払い、前払い、無料トライアルの3種類があります。 お試し価格はユーザーにとって、有料会員の体験ができるとても便利な機能です。 しかし公式ドキュメント以外の資料が少なく実装に何点か困ったことがありました。 そこで今回はお試し価格の無料トライアル機能を導入する方法についてご紹介します。 この記事が無料トライアルを導入しようか悩んでいる方、実際に無料トライアルを導入する方のお役に立てば幸いです。 本記事では、課金機能自体の実装の説明は行いません。 自動更新の定期購読のサーバーサイド実装については、こちらの記事を参考になります。 tech.starttoday-tech.com tech.starttoday-tech.com 無料トライアルとは 無料トライアル機能について 公式サイト には下記のように説明されています。 一定期間無料で定期購読を利用できるようにする方法です。定期購読はすぐに開始されますが、無料トライアル期間が終了するまで請求が行われることはありません。定期購読を試してもらいながら、請求が発生する前にキャンセルすることもできるようにする場合に効果的です。 この機能は通常と同じフローで課金処理が行われますが、実際にユーザーにはお金が請求されません。 無料トライアル指定できる日数は『3日・1週間・2週間・1か月・2か月・3か月・6か月・1年』の8種類です。 注意する点 お試し価格の対象ユーザーは一度もお試し価格を利用したことが無いユーザーです。 そのため現在課金中のユーザーも、無料トライアルを利用したことが無いユーザーは再課金時に無料トライアルを利用できます。 適用の条件を図にするとこのようになります。 https://developer.apple.com/documentation/storekit/in_app_purchase/offering_introductory_prices_in_your_app またフローチャートで表すと下記のようになります。 既に課金機能をリリース済みのサービスに対して無料トライアル機能を追加する場合は、再課金時に無料トライアル適用されるユーザーと適用されないユーザーがいることに注意してください。 実際に適用してみる 無料トライアルを実際に適用する場合は以下の2つの作業を行う必要があります。 iTunes Connect上で無料トライアルを適用する アプリケーション側で、購入レシートから無料トライアルかどうかを判断する 『アプリケーション側で、購入レシートから無料トライアルかどうかを判断する』処理については、再課金時に無料トライアルの適用対象かを判断する場合に用います。 無料トライアルを利用したユーザーが課金を終了し再課金した場合はお試し価格が適用されないため、無料トライアル機能へ誘導することはできません。 このような表示の出し分けに使用することもできるため、ユーザーが無料トライアルを利用したことがあるかどうかの情報はDB上に保存することをおすすめします。 iTunes Connect上で無料トライアルを適用する iTunes Connect上で無料トライアル機能を適用します。 お試し機能は課金アイテムごとに適用する必要があります。 1. iTunes Connectにログイン iTunes Connect にアクセスし、 マイAppから無料トライアルを適用したいアプリのページに移動します。 2. 課金アイテムに移動 iTunes Connect上で無料トライアルを適用したい課金アイテムのページに移動します。 3. お試し価格を適用する +ボタンをクリック お試し価格を適用する地域を選びます。 特に指定がない場合は、『すべてのテリトリ』を選択します。 お試し価格の開始日と終了日を選択します。 開始日はお試し価格を開始させたい日です。すぐに始めたい場合はその当日を選択します。 終了日はお試し価格を終了させたい日です。特に決まっていない場合は『終了日なし』を選択します。 お試し価格のタイプを選択します。 今回は無料トライアルを行うため、無料トライアルを選択します。 また無料トライアルが行える期間を選択し、終了をクリックします。 保存ボタンを押します。 これで無料トライアルの設定ができました。 課金アイテム画面の登録価格から無料トライアルが適用されていることを確認してください。 注意点 iTunes Connect上で無料トライアルの設定を行う際に、注意すべきポイントがいくつかあったのでご紹介します。 適用されるまでのタイムラグ サイト上で適用する際の文言は『いますぐ適用』と書いてありますが、実際に使ったところ設定反映までに30~40分のタイムラグがありました。 終了する際も同様に、30〜40分タイムラグがありました。 タイムラグがあることについてはどの資料にも記載されておらず、実装時にとても困りました。 実際に無料トライアル機能が適用されているか確認するには、課金画面を確認します。 下記の画面になっていれば無料トライアルが適用されています。 課金は1度しかされない ユーザーは1つのアプリに対して、無料トライアルは1度しか適用されません。 課金アイテムが複数ある場合も、1度でも無料トライアルを利用したユーザーは違う課金アイテムを購入しても無料トライアルは適用されないため注意が必要です。 レシートから無料トライアルかどうかを判別する方法 次にAppleから取得するレシートから無料トライアルかどうかを判別する方法についてご紹介します。 Appleから取得する定期購読型のレシートは下記のようになっています。 { "quantity": "1", "product_id": "monthly_paid_1", "transaction_id": "12345678901234", "original_transaction_id": "12345678901234", "purchase_date": "2000-01-01 00:00:00 Etc/GMT", "purchase_date_ms": "12345678901234", "purchase_date_pst": "2000-01-01 00:00:00 America/Los_Angeles", "original_purchase_date": "2001-01-01 00:00:00 Etc/GMT", "original_purchase_date_ms": "1234567890123415", "original_purchase_date_pst": "2001-01-01 00:00:00 America/Los_Angeles", "expires_date": "2001-01-01 00:00:00 Etc/GMT", "expires_date_ms": "12345678901234", "expires_date_pst": "2001-01-01 00:00:00 America/Los_Angeles", "web_order_line_item_id": "1234567890123456", "is_trial_period": "true" } 取得したレシートが、無料トライアル中かどうかを判定するためには、レシート内の is_trial_period を確認します。 is_trial_period が true だった場合は、無料トライアルのレシートです。 また通常 expires_date は購入したプランの終了日になりますが、無料トライアルのレシートの場合 expires_date が無料トライアル終了日になっています。 お試し価格が終わった際のレシート お試し価格期間後は下記の2パターンが存在します。 定期購読を継続する 無料トライアル中に定期購読をキャンセルする 定期購読を継続している場合 無料トライアル期間終了後に、 is_trial_period が false のレシートが最新レシートとして取得できます。 { "quantity": "1", "product_id": "monthly_paid_1", "transaction_id": "12345678901234", "original_transaction_id": "12345678901234", "purchase_date": "2000-01-01 00:00:00 Etc/GMT", "purchase_date_ms": "12345678901234", "purchase_date_pst": "2000-01-01 00:00:00 America/Los_Angeles", "original_purchase_date": "2001-01-01 00:00:00 Etc/GMT", "original_purchase_date_ms": "1234567890123415", "original_purchase_date_pst": "2001-01-01 00:00:00 America/Los_Angeles", "expires_date": "2001-01-01 00:00:00 Etc/GMT", "expires_date_ms": "12345678901234", "expires_date_pst": "2001-01-01 00:00:00 America/Los_Angeles", "web_order_line_item_id": "1234567890123456", "is_trial_period": "false" } 定期購読を継続している場合に取得するレシートは、下記のようになっています。 is_trial_period が false expires_date が購入したプランの終了日 このような状態の場合は、ユーザーが継続課金をしているとみなします。 無料トライアル中に定期購読をキャンセル 無料トライアル中に定期購読をキャンセルした場合は、期間終了後にAppleからレシートが送られてこないことに注意してください。 こちら側でAppleにレシートを問い合わせる必要があります。 無料トライアル中に定期購読をキャンセルした場合、レシートは下記のような状態になります。 is_trial_period が true expires_date が現在時刻より過去 このような状態の場合は、ユーザーが継続課金をキャンセルしたとみなします。 無料トライアル中 無料トライアル中に定期購読をキャンセルした場合 無料トライアル後に課金を継続した場合 それぞれの場合 is_trial_period と expires_date がどのような状態になるかをまとめると以下のようになります。 状態 is_trial_period expires_date 無料トライアル中 true 現在時刻より未来 無料トライアル中に定期購読をキャンセル true 現在時刻より過去 無料トライアル後に課金を継続 false 現在時刻より未来 注意する点 Sandbox環境で無料トライアル中のキャンセルができない Sandbox環境で無料トライアルを適用した場合、必ず無料トライアル後課金が継続されます。 継続課金が何回続くかはランダムですが、無料トライアルだけで課金が終了することはありません。 そのため無料トライアル中にキャンセルした場合の検証が行えません。 まとめ 本記事ではiOSアプリのお試し価格の1つである無料トライアル機能についてご紹介しました。 無料トライアル機能はユーザーにとって、有料会員の体験ができるとても有用な機能です。 しかし実際に無料トライアル機能を適用する上で、公式ドキュメントに記載されていないことや注意すべきポイントがいくつかありました。 公式ドキュメント以外にほとんど資料がなかったため、本記事では無料トライアル機能についてまとめてみました。 無料トライアルの導入を検討している方、実際に導入する方のお役に立てば幸いです。 スタートトゥディテクノロジーズでは、一緒にサービスを作り上げてくれるエンジニアを大募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! www.starttoday-tech.com starttoday-tech.connpass.com 参考リンク Offering Introductory Prices in Your App 定期購読を提供する プロモーション用の App 内課金の追加(iOS) 価格および配信状況
アバター
こんにちは。zozoフロントエンド部の大平です。さだまさし好きが昂じて社内では「さださん」と呼ばれています。 計測してますか? 皆さん計測していますか? 何かを改善しようとした場合、パフォーマンスを数値化し、その内容をもとに改善案を考えて行動することが、基本的な取り組み方になります。 そして、いかに現状を測定可能な状態にし数値化可能な指標を設定するか、という事が取り組みの第一歩になります。ダイエットや体質改善でもそうですし、英語など語学学習でもそうだと思います。 ということで、計測はとても大事です。 私事ですが、私は最近さだまさしさんのファンが集うコミュニティに参加しています。古くからのファンの方々はさだまさしさんのデビュー時からずっと追っかけている猛者も多く、私みたいな若輩者の青二才は圧倒的な「さだ力」の不足を感じる毎日です。 最近、コミュニティの中で「 楽曲検定 」というサイトが流行しました。このサイトではiTunesの試聴データを用いてアーティストの楽曲のイントロクイズを提供しています。 さだまさしさんは今年デビュー45週年を迎え、作った楽曲は550曲を超えると言われています。多すぎて覚えられないよとの一部ファン(私)のため息が漏れる中、2018年もまだ新曲を出す予定のようです。 そんな「ビッグデータ」を相手にしても、先輩さだファンは 「配信曲全て出題、間違ったら即終了 全曲編」 という鬼畜にも程がある問題を選択し、皆軽く全問正解していきます。無論私も全問正解するのですが、さだコミュニティの中では全楽曲を知っているのがベースラインとなり、「さだ力」を表現するKPIとして機能しません。 なので、最近はストップウォッチを片手にタイムトライアルを個人的に実施するようにし、日々の日課にしています。「さだ力」のKPIを楽曲数やクイズの正答数ではなく、 「MSPS (Masashi Sada Per Sec)」 においたわけです。 最初は普通のノートPCを使っていたのですが、時間を計測することによりトラックパッドによるマウスカーソルの移動に予想以上に時間を要している事に気づきました。 抜本的な改善のためには道具が必要ですので、Chrome OS/Android OSのハイブリッド利用が可能な Chromebook を導入し、画面のタッチパネルを使用する事により大幅に時間短縮を果たしました。 今では調子のよい時は「曲視聴~回答~次の問題選択」の一連の流れを1曲あたり2秒台(0.3~0.5 MSPS)で実現できるようになりました。しかしあまりに動きが速すぎるためかサイトがフリーズしてしまうことが頻発するようになってしまったのが悩みです。 Datadogについて よくわからない事を書いてしまいましたが、ここから本論に入ります。 弊社内ではいくつかのサービスを開発運営しておりますが、サービスの安定稼働を実現するためにMonitoringツールを用いた監視を行っています。 各サービス利用しているツールは様々なのですが、私が現在担当しているシステムでは Datadog をメインの監視ツールとして使用しています。 ご存知の方も多いと思いますが、DatadogはNewYorkを拠点にサービスを展開しているいわゆるMonitoring SaaSです。日本においてはMackerelがMonitoring SaaSとしては有名ですが、Datadogはグローバルで使用ユーザーが多いサービスの1つとして知られています。 以下のグラフは、 SRECon というイベントでのアンケート結果です。 NewRelic などと拮抗して来場者の多くが使用しているツールであることが見て取れます。 Datadogの利点は数多いですが、ここでは私の主観でいくつかピックアップします。 豊富なグラフ表現とカスタマイズ性 Datadogの他のMonitoring Toolと比べたアドバンテージは、非常にリッチなグラフ表現をドラッグ&ドロップで簡単に実現できるところにあるかなと思います。 たとえば以下のようなDashboardを手軽に作成できます。 Slackとの連携が容易 現代的な開発エンジニアの間ではSlackなどのチャットツールがコミュニケーションのハブになっており、そこに情報を集約することが開発効率の向上や情報共有の円滑化につながります。 Mackerelなどでも同様の機能がありますが、DatadogではSlackなどに対してグラフ付きでメッセージやアラートを通知する機能をもっており、チーム内でのコラボレーションに非常に有益です。 豊富なAPI DashboardやAlertingの設定は画面からでも行なえますが、Datadogは豊富なAPIを提供しているためそれらの操作をAPIを介して半自動化することも可能です。 よく使われている事例としては、 Barkdog などのツールを用いたAlertingの設定のDSL化かなと思います。 Barkdogを用いると設定をGitHubなどで管理でき、差分管理やCIツールと連携した適用の自動化などを実現することが可能です。 Service Integration 豊富なService Integrationの機能もDatadogの魅力です。 AWS CloudWatchの情報や、NewRelicなどの他のMonitoring Toolの情報などを簡単な設定でDatadogに取り込む事ができます。この機能を用いることでDashboardの情報をDatadogに一元化することが可能になります。 また、Datadogの情報を入力ソースとして他のシステムと連携することもできます。代表的な事例は Pagerduty などのOn-Callツールとの連携です。Datadogのメトリクス内容を使用して特定の閾値に達したらPagerdutyのEscalation Flowに基いて障害通知を行う、といった事が実現できます。 Docker Integration そして、最近は Kubernetes の流行などもあり、Docker/Docker Orchestration環境とMonitoring Toolの連携も必要になってきています。この辺については後段で詳しく触れます。 Kubernetes環境との連携 ということで、DatadogとKubernetesの連携について、AutoDiscovery機能を軸に少しだけ深掘りします。 たとえばKubernetes上にPodを展開すると、設定によってはAuto Scaling/Self HealingにてPod数がダイナミックに増減します。どのHost Computer上にどのPodが置かれるかは基本的にはKubernetesが管理することになり、人間が介在し事前に把握することが難しいです。 またDocker imageの中に必ず監視ツールを同梱する、もしくはSide-Car的に1対1で設置するというのも効率が悪いです。 ですのでMonitoring Toolは、何かしらの方法で現在動作しているPodの存在を自動的に把握し、監視対象とする仕組みが必要になります。 その仕組をサポートするものとして、PrometheusではService Discovery機能があります。これによりKubernetesクラスタ上に存在するServiceやPod、Ingressといったものの追跡が可能になっています。 https://prometheus.io/docs/prometheus/latest/configuration/configuration/#%3CKubernetes_sd_config%3E 同様に、Datadogには AutoDiscovery 機能があり、これによりPrometheusのService Discoveryと同様の機能を実現できます。 設定内容など詳しくは以下のQiitaに記事を書きましたので、こちらも参照ください。 Datadog の AutoDiscovery 機能を用いて自動的に Kubernetes pod の監視をする 現場では、以下のようなメトリクスについてAutoDiscoveryの機能を用いて取得しています。 nginxのメトリクス (/nginx_status) Spring Bootのメトリクス Prometheus Exporter経由で取得できるようにしています Datadog を用いて Prometheus Exporter の監視をする Docker時代/Kubernetes時代のMonitoring Toolには、このようなDiscovery機能が必須になってきていると思います。そして、Prometheus同様、Datadogはそういう時代の変化に追随しているサービスだなと感じており、組織の中でも重用しています。 STTクラウド技術共有会 少し話題が変わりますが、先日4月1日に会社が改組されたこともあり、これをひとつのきっかけに社内の技術者が組織を横断して集う情報の共有会を行いました。 技術分野は多岐に渡るため分科会的にいくつかの会を設けており、私はクラウド関連の技術についてディスカッションする会に参加しています。 先日開催された初回ではMonitoring Toolについてをテーマに開催されました。 どうしても監視周りはシステムの導入時期や担当エンジニアの嗜好なども影響します。そのため採用しているツールは各チームまちまちでしたが、その状況が共有できたこと、それぞれのPros/Consや導入の背景が理解できたことは良かったです。 とはいえ各チームとも以下については共通しており、同じ時代を同じ組織の中でともに歩むエンジニアとして、同じ方向性を見れていることを知れた事も有益でした。 クラウドネイティブなツールを使って管理コストを減らしたい、というモチベーションが強い 設定はDSL化してGitHub管理している Slackへの通知など、コミュニケーションツールとの連携を重視している このような会を定期的に開催し、各チームや各エンジニアの考えていることや取り組みを可視化しMonitoringすることも、とても大事なことだなと感じています。 まとめにかえて Datadogについて、そしてMonitoringについてとりとめなく記事を書かせていただきました。 社内でよく話すのですが、Monitoringはひとつの手段でしかなく、目的ではありません。たとえば今回ご紹介したDatadogは本当によく出来たツールで、こちらにメトリクスを流し込んでグラフを作成しただけで何かしら言い知れぬ達成感・全能感があったりします。 しかし、技術者に求められているのは測定することだけではなく、 「システムをいかに安定稼働させるか」「そのためにいかにシステムの状況を正確に知るか」 にあります。 そして、Monitoringした内容等を基に 「仕事やサービスに対していかに効果的なアクションを取るか」 が、本当に求められていることです。 ついつい技術者は目の前の手段にとらわれがちですが、こういう意識を忘れずに、日々サービスを改善をしていければと思います。 私が「さだ力」を向上したいと思っているのも 「えー、さだまさしの事そんなに詳しいんだ、すごーい」 と女の子にキャッキャウフフともてはやされることが目的ですので、本来のゴールを見失わないよう日々慎ましく生きていきたいと思います。 [AD] 弊社では、さだまさしに対する愛情は必ずしも必要ないですが、情熱をもってサービスを支えるエンジニアを募集しております。 ブログでの情報発信の他に、弊社では月一回のペースでMeetupイベントなども開催しております。皆様との交流を通じて、我々の行っている取り組みや、我々のサービスにかける思いを知っていただく場をもっと増やして行こうと考えておりますので、ご興味を持たれた方はぜひぜひご参加いただければと存じます。 starttoday-tech.connpass.com www.starttoday-tech.com
アバター
iOSチームの @hiragram です。 所属するプロジェクトでは依存管理にCarthageを使っていますが、Carthageの成果物である Carthage/ 以下をコミットするかどうかはよく議論になる話題かと思います。 私はコミットしない派なので、そのメリットを残しつつデメリットをなくすためにやってみたことを紹介します。 メリットとデメリット コミットしない派のメリット リポジトリが肥大化しない 以前のプロジェクトでは Carthage/ 以下をコミットしていて、リポジトリがめちゃでかくなってcloneにめちゃ時間がかかる感じになっていました。 diffがうるさくならない 言わずもがな。 Xcodeのバージョンを積極的に上げられる SwiftのABI安定化はまだ先なので、コミットされたバイナリはそれをビルドしたのと同じバージョンのSwiftからしか扱えない。 Swift.org - ABI Dashboard Swift ABI Stability Manifesto 複数人で開発していると、新しいバージョンのSwiftでコンパイルされたバイナリをmasterへ取り込む時に、全員せーのでXcodeのバージョンを上げなきゃいけなくなる。 XcodeやSwiftのアップデートに追随していく精神的な妨げになるのでよくない。 コミットしない派のデメリット cloneしたあとすぐにアプリが使えない これそんなにデメリットに思わない。 make install とか昔やってましたよね…? CIコンテナの上で依存パッケージもビルドするとえらい時間がかかる 「時間がかかる」を解消するために筋悪選択をしてしまうのはちょっとなーという気がする Bitriseにはソリューションがありそう  https://discuss.bitrise.io/t/how-to-cache-carthage-dependencies/197 異なる依存パッケージをもつブランチ間を移動するたびに再ビルドしないといけない 確かに。 同じ依存パッケージをもつブランチ間での移動では再ビルドは必要ないが、それが必要なのかぱっと分からない 確かに。 異なる依存パッケージをもつブランチ間を移動するたびに再ビルドしないといけない 同じ依存パッケージをもつブランチ間での移動では再ビルドは必要ないが、そもそも必要なのかどうかぱっと分からない 今回はこれを解決する方法を考えてみました。 どのように解決するか 課題解決の要件はこのようにします。 ブランチ切り替えによって Cartfile.resolved が変わった時に毎回再ビルドするのは時間がもったいないので、過去にその依存パッケージ郡をビルドしたことがあったら再ビルドはしなくていいようにする 必要な場合は再ビルドしないとアプリのビルドが通らないようにする = 不要な場合はそのままアプリがビルドできる Cartfile.resolved と Carthage/ 以下のファイルは1対1で対応するはずです。即ち、 Cartfile.resolved の中身ごとに Carthage/ 以下をキャッシュしておけばブランチ切替時の再ビルドが不要になるはずです。 (厳密にはそうではない。後述します) シンボリックリンクを活用し、以下のようなディレクトリ構成にしました。 $ tree -L 5 . ├── Cartfile ├── Cartfile.resolved ├── Carthage -> CarthageCache/`なんらかの識別子` ├── CarthageCache │ └── `なんらかの識別子` │ └── Build │ └── iOS │ ├── RxBlocking.framework │ ├── RxCocoa.framework │ ├── RxSwift.framework │ └── RxTest.framework . . . $ carthage update (or bootstrap )で出来た Carthage/ ディレクトリを CarthageCache/なんらかの識別子/ に移動し、 Carthage ディレクトリの代わりにそこへのシンボリックリンクを置きます。 先程「 Cartfile.resolved と Carthage/ 以下は1対1に対応する」と書きましたが、実は正確ではありません。Carthageは --configuration Debug のようにビルド設定を指定することが出来ます。 Cartfile.resolved の中身だけで識別子を作ってしまうと、その中身がデバッグビルドなのかリリースビルドなのかわからなくなってしまいます。 そこで「なんらかの識別子」は、 Cartfile.resolved のチェックサムにビルド設定( Release or Debug )を付加したものにします。 Xcodeのプロジェクトにフレームワークを追加する時は、このシンボリックリンク経由のパスで追加します(コツがいります。後述します)。 そしてXcodeのBuild Phaseの先頭で、適宜シンボリックリンクの向き先を変えるようにします。 こんな感じでしょうか。 実装と導入 carthage_hack.sh こんなシェルスクリプトを書きました。 まず、普段使っている $ carthage update や $ carthage bootstrap は今後このコマンドで置き換えます。 $ ./carthage_hack.sh [update|bootstrap] [Release|Debug] このコマンドは、 Carthageでビルド できた Carthage/ ディレクトリからフレームワークを CarthageCache/識別子/Build/iOS/ 以下に移動 Carthage/ ディレクトリを削除してそこにシンボリックリンクを設置 を行います。 また、 $ ./carthage_hack.sh symlink [Release|Debug] このコマンドは現在の Cartfile.resolved と引数に渡したビルド設定に該当するディレクトリを向いたシンボリックリンクを張ります。これはXcodeのBuild PhaseのCompile Sourcesの手前にRun script phaseを追加し ./carthage_hack.sh symlink ${CONFIGURATION} こう呼び出しています。 ${CONFIGURATION} はXcodeの環境変数でアプリの現在のビルド設定が入っているので、「アプリはリリースビルドなのに依存フレームワークがデバッグビルドのままだった」という事故を防ぎます。 問題 あとはシンボリックリンク経由のパスでフレームワークをプロジェクトに追加するだけですが、ここで困ってしまいました。 FinderからXcodeにファイルをドラッグ&ドロップで追加する時、シンボリックリンク経由のパスではなく実体のパスで追加されてしまいます(Add filesからやっても同じ)。 これではシンボリックリンクの切り替えによる恩恵が受けられません。 苦肉の策感が否めないですが、以下のような方法で回避出来ました。 シンボリックリンク Carthage を Folder referenceとして プロジェクトに追加する Folder referenceの中のファイルを直接プロジェクト内で使えないようなので、さらにもうひと手間 New group without folder で作ったプロジェクト内の別のGroupに Xcodeのファイルツリーから ドラッグ&ドロップで追加 Finderを経由しなければシンボリックリンク経由のパスとして追加できる ちなみに、画像でいうDependenciesグループが空の状態だと正しく追加できますが、既に何か入ってるところにさらにFolder reference越しにファイルを追加しようとするとおかしなファイル移動が起きたりそれを戻そうとするとXcodeが落ちたりしました。 回避するためには毎回Dependenciesを空にする必要がありそうです。今はプロジェクト初期で依存パッケージが少ないのであまり問題になりませんが、多くなってくると大変かもしれません。 結果 要件を満たせたか確認してみます。 まず、デバッグビルドで依存パッケージをビルドします。 $ ./carthage_hack.sh bootstrap Debug $ tree -L 5 . ├── Cartfile ├── Cartfile.resolved ├── Carthage -> CarthageCache/c0480ba77cbcf45447d7e0ee1e2f25cb92d83e33cf4a38635015c676f91837af-Debug ├── CarthageCache │ └── c0480ba77cbcf45447d7e0ee1e2f25cb92d83e33cf4a38635015c676f91837af-Debug │ └── Build │ └── iOS │ ├── RxBlocking.framework │ ├── RxCocoa.framework │ ├── RxSwift.framework │ └── RxTest.framework . . . Xcodeにフレームワークを追加して、アプリをデバッグビルドしてみます。 通りました。 では、次にアプリをリリースビルドしてみます。 Failed to read file or folder at /Users/hiragram/Development/teikibin-ios/Carthage/Build/iOS/RxSwift.framework Command /bin/sh failed with exit code 1 RxSwift.framework が見つからないと言われてビルドが失敗しました。 リリースビルドのフレームワークはまだ無いのでこれも正しいです。 リリースビルドで依存パッケージをビルドします。 $ ./carthage_hack bootstrap Release $ tree -L 5 . ├── Cartfile ├── Cartfile.resolved ├── Carthage -> CarthageCache/c0480ba77cbcf45447d7e0ee1e2f25cb92d83e33cf4a38635015c676f91837af-Release # 向き先がReleaseに変わった ├── CarthageCache │ ├── c0480ba77cbcf45447d7e0ee1e2f25cb92d83e33cf4a38635015c676f91837af-Debug │ │ └── Build │ │ └── iOS │ │ ├── RxBlocking.framework │ │ ├── RxCocoa.framework │ │ ├── RxSwift.framework │ │ └── RxTest.framework │ └── c0480ba77cbcf45447d7e0ee1e2f25cb92d83e33cf4a38635015c676f91837af-Release # リリースビルドのフレームワークが置かれた │ └── Build │ └── iOS │ ├── RxBlocking.framework │ ├── RxCocoa.framework │ ├── RxSwift.framework │ └── RxTest.framework . . . アプリをもう一度リリースビルドしてみます。 今度は通りました。 次に、 Cartfile で指定しているフレームワークのバージョンを変えて再ビルドしてみます。 すると、 Cartfile.resolved の中身も変わるので別の識別子でシンボリックリンクが作られました。 $ tree -L 5 . ├── Cartfile ├── Cartfile.resolved ├── Carthage -> CarthageCache/5a4a9fb30dcc42dc98d9982e141446e2544b89b1ecac779d665ff76af7466228-Debug # 新しいほうに切り替わった ├── CarthageCache │ ├── 5a4a9fb30dcc42dc98d9982e141446e2544b89b1ecac779d665ff76af7466228-Debug # 増えた │ │ └── Build │ │ └── iOS │ │ ├── RxBlocking.framework │ │ ├── RxCocoa.framework │ │ ├── RxSwift.framework │ │ └── RxTest.framework │ ├── c0480ba77cbcf45447d7e0ee1e2f25cb92d83e33cf4a38635015c676f91837af-Debug │ │ └── Build │ │ └── iOS │ │ ├── RxBlocking.framework │ │ ├── RxCocoa.framework │ │ ├── RxSwift.framework │ │ └── RxTest.framework │ └── c0480ba77cbcf45447d7e0ee1e2f25cb92d83e33cf4a38635015c676f91837af-Release │ └── Build │ └── iOS │ ├── RxBlocking.framework │ ├── RxCocoa.framework │ ├── RxSwift.framework │ └── RxTest.framework . . . これで、例えばフレームワークのバージョンを上げる作業をしているブランチとmasterブランチの間を移動しても、Build Phaseの先頭でシンボリックリンクが切り替わる仕組みによって 一度すればそれ以降再ビルドが不要になりました。 逆に一度もビルドしていないときは、リリースビルドのフレームワークが見つからなかったときと同様にアプリのビルドが失敗するので 間違ったバージョンを使ってしまう心配もありません。 いい感じじゃないでしょうか。 まとめ この仕組みはまだ導入したばかりなので、まだ遭遇していないシーンで困ることがあるかもしれません。その時はまた何か考えます。 この仕組みによって、コンパイル済フレームワークをコミットすることで得られるメリットの1つ「間違ったバイナリを使ってしまうことがない」が得られたのではないでしょうか。更にこのやり方であれば、リリースビルドとデバッグビルドを間違えることもなくなります。 今後はこういった開発環境を健全に保つための仕組みづくりをどんどんやっていこうと思っています。こういう取り組みに興味を持っていただけた方、是非ご連絡下さい。お待ちしています。
アバター
VASILY CTOの今村です。 今日はこのブログを普段からご覧頂いている皆様にお知らせがあります。 本日発表させていただきました通り、スタートトゥデイ工務店、VASILY、カラクルの3社が統合され 新会社「スタートトゥデイテクノロジーズ」が誕生いたしました。 http://press.starttoday-tech.com/entry/20180402_news press.starttoday-tech.com それに伴い、VASILYテックブログは本日をもって、 「スタートトゥデイテクノロジーズ テックブログ」として新しく生まれ変わります。 このテックブログでは約6年半に渡り、VASILYが運営するサービスに纏わる様々な技術的内容をお届けしてきました。 実際にこのブログから発信される記事を通して、VASILYを知ってくれたエンジニアの読者の方も多いと思います。 今後はスタートトゥデイテクノロジーズとして、 ZOZOTOWN、WEAR、おまかせ定期便やスタートトゥデイ研究所など 様々な内容をベースにその裏側を支える技術的な記事を投稿していきたいと思います。 まさに今日から始まったばかりのスタートトゥデイテクノロジーズですが、 既存事業のさらなる拡大、テクノロジードリブンな新規事業を創造すべく、 この会社の黎明期にリーダーシップを発揮することができる博士やエンジニア、アナリストやデザイナー、プロデューサーなど 様々な領域の「天才」「逸材」たちを募集します! 少しでも興味のある方はぜひ、スタートトゥデイテクノロジーズホームページ内の特設ページをご覧ください。 ご応募お待ちしております! https://www.starttoday-tech.com/recruit/genius/ www.starttoday-tech.com
アバター
こんにちは、フロントエンド開発部マネージャーの荒井です。今回はVASILYフロントエンドチームの体制、開発手法、マネージャーの役割について紹介したいと思います。 はじめに 私はエンジニアの採用担当もしているのですが、会う方に「どのように開発をしているのですか?」「リファクタリングの工数は取れていますか?」といった質問をよく頂きます。働く上で事業ドメインや採用技術も重要ですが、同様に日々のタスクの進め方、チーム体制を重要視している方も多いようです。今回はVASILYのフロントエンドチームの体制、開発手法について紹介します。 フロントエンドチームの役割 まず初めにフロントエンドチームの業務範囲を説明します。 VASILYフロントエンドチームは大きく3つのプラットフォームを担っています。 Webアプリケーション開発 iOSアプリケーション開発 Androidアプリケーション開発 ネイティブアプリケーションを「アプリ開発部」などとして分けている企業も多いかと思いますが、VASILYではすべてまとめて「フロントエンド開発事業部」としています。 Kotlin、Swift、 Ruby、JavaScriptといった言語を使用しており、HTMLやCSSをコーディングするのもフロントエンドチームの役割です。 チーム体制 現在は6名の体制で開発を進めています。 多くの企業では明確に役割分担されており、専任が多いと思います。VASILYのフロントエンドチームの変わったところは、プラットフォームの垣根を取り払って開発をすることもある点です。下記図のようにすべてのプラットフォームを担当してるメンバーも2名います。 プロダクション環境に反映されるmasterブランチへのマージはコードレビューが必至となります。コードレビューの体制を整える都合上、各プラットフォームは最小で2名としています。案件に応じて2〜4名で開発しています。 専任と兼任 元々複数のプラットフォーム経験のあるメンバーが揃っていたわけではなく、兼任メンバーは働き初めてから学習期間を設けてコンバートしました。現在Android開発が出来るメンバーは4名となっていますが、元々Android開発を行っていたのは1名で、iOSから1名、Webから2名増えています。 複数のプラットフォームに精通するメンバーが増えることで、案件に応じて柔軟なリソース配分が可能となりました。また、特定のメンバーに負荷がかかることを避けるメリットもあります。 兼任 個人のキャリアパスを無視して業務命令で兼任とさせることはしていません。 以下のことは大事にしています。 他のプラットフォーム開発に興味があるメンバー 学習期間を設ける 有識者のもとで開発をする Android開発は Udacity にて基礎を学び、はじめはベテランのAndroidエンジニアのもとで小さいタスクから行なっていきました。学習方法については随時振り返りを行い手法を変えていっています。 専任 iOSのみ開発するメンバーも2名います。Swiftという言語に興味があり、キャリアパスとしても現状iOSの開発に力を入れたいというメンバーは専任としています。個々がモチベーションを保てる業務内容であることが大前提で、それを踏まえた上でチームとしてリソース配分をしていくことが重要です。 技術の選定 異なるプラットフォームの実装をするには負荷がかかります。その負荷を軽減するためには技術選定も重要です。直近ですとiOS、AndroidではReactiveXを取り入れて開発を進めるといった取り組みをしています。同じパラダイムを採用することでプラットフォームを跨いだ時の負荷を軽減しています。 開発手法 プラットフォームやプロジェクトにもよりますが、多くのプロジェクトでは1〜2週間のスプリントで開発を行っています。現在アクティブなプロジェクトでは以下のような役割分担で行っています。 プロダクトオーナー 開発チーム マネージャー 前述した6名は開発チームにあたります。 役割 プロダクトオーナー プロダクトの方向性を決定します。プロダクト開発における優先順がついたタスクリストであるプロダクトバックログを作成します。殆どの場合、開発チームに直接の指示は出していません。 開発チーム プロダクトバックログに記載されているタスクの工数を見積もります。見積もった結果、マネージャーと共に、どの機能まで次のスプリントに入れるかを決定します。ここで決定したスプリントバックログの進捗をマネージャーが管理していきます。 マネージャー 作成されたスプリントバックログが問題なく完了するように調整していきます。フロントエンド事業部として持つタスクの優先度をつけ、開発チームに「差し込み案件」などが来ないようにします。案件は様々発生してしまうため、その場合、開発メンバーに流れないように開発したりします。 スプリント 2週間の間には以下のことが行われます。 スプリントバックログの作成 開発 QA リリース プロダクトオーナー、デザイナーも含めたQAが完了し、機能がリリースされるとそのスプリントは完了とみなされます。プロジェクトによっては Measure というステータスを設け、実施した施策に対する測定を行なってから DONE へ移行するようにしています。 マネージャーの役割 マネージャーの業務は多岐に渡ります。また、私自身がエンジニアのマネージャーとして大切にしたいことは多々あるのですが、今回は質問が多い「リファクタリング」「新しい技術の導入」の観点に絞って紹介します。 アカウンタビリティ 「リファクタリング」も「新しい技術の導入」も率先して行なっています。リファクタリング用に工数を取りますし、タスクの調整も行なっています。その時に必要なのが「アカウンタビリティ(説明責任)」です。「何故リファクタリングが必要なのか」を明確にする必要があります。幸い会社が開発に対して理解があるので揉めたことは一度もありませんが、説明は意識するようにしています。 新しい技術導入も同様です。VASILYには「 エンジニアマニフェスト 」が存在しており、新しい技術の導入にはポジティブです。しかし、手当たり次第新しいことを試すのではなく、「何の課題を解決するのか」は全員が考えて選定しています。これが担保されるのならば会社として実績がないことでも挑戦する機会を設ける方針です。 開発チームのタスク 新しいことを始めるには学習コストがかかりますし、日々の業務でも集中して作業する時間が必要です。「集計依頼が頻繁にきて開発時間が確保出来ない」等の問題が出来るだけ起きないように意識しています。VASILYにおいてスクラムマスターは存在していませんが、チームとしてスピードが出るように障害を取り除いていくポジションという意味では近しい役割を担っています。 まとめ 今回はフロントエンド事業部の紹介をしました。チームによって様々ですが、VASILYの開発スタイルが少しでも伝われば幸いです。現在エンジニアのマネージャーは私しかいません。一緒にマネージャーとしてチームを作って行ける方、是非ご連絡ください。また、こんなチームで働いてみたいなと思った方は是非一度オフィスへ遊びに来てください。お待ちしております。
アバター
データサイエンティストの中村です。 webで発生するトランザクション(購買など)の中には、確率分布を仮定することで抽象化できる物があります。 今回は、トランザクションが発生する現象をモデリングする手法のひとつであるBG/NBDモデルと、この手法にもとづいて将来発生するトランザクションの回数を予測するためのライブラリである lifetimes を紹介します。 トランザクションのモデリングについて 1987年にSchmittlein等によってPareto/NBDというモデルが提案されました。これは顧客の継続的に発生する購買行動に確率分布を当てはめ抽象化する手法で、結果として将来発生する購買を予測することに成功しました。顧客が離脱したか否かの判断や顧客生涯価値の見積もりが可能になるという点で、Pareto/NBDモデルは顧客分析における非常に強力なツールのひとつです。 Pareto/NBDをベースとして改良されたモデルはいくつか存在し、Fader等によって提案されたBG/NBDモデルもそのひとつです。BG/NBDモデルはPareto/NBDに用いられたデータ表現にアレンジを加え、精度を損なわずシンプルに改良したモデルです。 本来これらのモデルは購買行動を分析対象として設計されたものですが、後述する仮定に嵌まるものであれば、購買行動以外の現象にも適用できます。以降はwebのデータを想定して、用語をユーザーとトランザクションに統一します。 BG/NBDモデル データ 観測期間中に発生した、あるユーザーに由来するトランザクションの回数を とします(最初のトランザクションを0回目とカウントする)。 回目のトランザクションが発生した時刻を、0回目のトランザクションを起点として で表します。すなわち、観測期間中の最後のトランザクションは となります。また、0回目のトランザクションから観測終了までの時間を とおきます。 データに対する仮定 ユーザー個人に由来するトランザクションに対して、以下の仮定を敷きます。 i. ユーザーは生存/離脱を示す2値の状態を持ち、生存のときのみトランザクションが発生する。状態は観測されない。 最初のトランザクションが観測された時点で生存となり、この状態である限りはトランザクションがランダムに発生します。ユーザーがサービスを離脱した時、以降トランザクションは発生せず、このユーザーは再び生存状態になることはありません。 生存/離脱を正確に見積もることはターゲティングの観点から非常に重要です。 ii. ユーザーが生存中は、観測期間中に発生するトランザクションの回数は単位時間を としてパラメータ のPoisson分布に従う。 同時に、Poisson分布と指数分布の関係から、トランザクションの時間間隔は指数分布に従うことが言えます。 はモデルのパラメータで、トランザクションの発生率のような量を表します。 iii. パラメータ はGamma分布に従う。 iv. トランザクションの発生後にユーザーが離脱する確率は一定で 、 とする。 すなわち、 回目のトランザクション後にユーザーが離脱する確率は幾何分布で表現できます。 正直、確率変数に を使いたくないですが、表記は元の論文に合わせます。 v. パラメータ はBeta分布に従う。 そして、ユーザー全体に対する仮定として、ユーザーの特性を示すパラメータ は独立とします。 をユーザーのインデックス、 をユーザー数として、 モデルの入力は 、ユーザー個人の行動を制御するパラメータは 、モデルのパラメータは となります。 尤度関数の導出 本記事では導出過程を一部省略します。全体が気になる方はFader05をご覧ください。 観測期間中に 回のトランザクションが観測され、 ]にトランザクションが発生していない時の尤度は、以下のようになります。 の時、 ]にトランザクションが発生しないので、 の時、 で離脱した場合と まで生存しながら ]にトランザクションが発生しない場合を考慮して、 上記2式をクロネッカーのデルタを使って書き直します。 次に、パラメータ の周辺化を試みます。事前分布を用いて となります。式の見た目は屈強ですが計算は意外と簡単です。 に関する積分と に関する積分は別々に計算できますし、確率密度関数と正規化項の関係を利用して楽ができます。 よって目的関数は以下のようになります。 推論時に重要な指標 Fader05ではトランザクションの回数に関する確率である やその期待値 ]の導出が述べられています。式を書き写すだけになるので本記事では扱いません。 の条件付き事後分布 それぞれ混合分布の形になると思います。私の手計算につき間違えている可能性もあるので、おかしな部分を発見した場合はご指摘ください。 のとき、 のとき、 lifetimesについて lifetimes はトランザクションデータからユーザーの生存/離脱を推定し、未来に発生するトランザクションやユーザーの生涯価値を予測するためのライブラリです。生データをほぼそのまま食わせることができるため非常に簡単に使えます。 github: https://github.com/CamDavidsonPilon/lifetimes docs: http://lifetimes.readthedocs.io/en/latest/ 公式のdocumentationにて使い方が丁寧に紹介されています。詳細はそちらを参照いただくとして、本記事ではかいつまんで紹介します。 ちなみに、本記事では購買データの代わりにIQONのコーデ投稿のログを用いています。 データの加工 ほとんど必要ありません。生ログからユーザーIDとdateのみ抽出して pandas.DataFrame にロードします。 In [ 4 ]: transactions.head() Out[ 4 ]: transaction_id user_id date 0 100001 2533078 2017 -08- 18 1 100002 2489799 2017 -08- 18 2 100003 2595561 2017 -08- 18 3 100004 2391692 2017 -08- 18 4 100005 2515961 2017 -08- 18 トランザクションの集計は lifetimes.utils.summary_data_from_transaction_data や lifetimes.utils.calibration_and_holdout_data にDataFrameを食わせるだけです。 In [ 5 ]: summary_calib_holdout = calibration_and_holdout_data(transactions, 'user_id' , 'date' , ...: calibration_period_end= '2017-06-30' , ...: observation_period_end= '2017-12-31' , ...: freq= 'D' ) ...: summary_calib_holdout.head() ...: Out[ 5 ]: frequency_cal recency_cal T_cal frequency_holdout \ user_id 7 0.0 0.0 93.0 0.0 77 0.0 0.0 156.0 1.0 260 0.0 0.0 93.0 0.0 1468 11.0 132.0 140.0 0.0 1938 40.0 134.0 175.0 12.0 duration_holdout user_id 7 184 77 184 260 184 1468 184 1938 184 学習 scikit-learn と同じように fit を実行します。 bgf = BetaGeoFitter(penalizer_coef= 0.0 ) bgf.fit(summary_calib_holdout.frequency_cal, summary_calib_holdout.recency_cal, summary_calib_holdout.T_cal) サポートしているアルゴリズムは github をご確認ください。 可視化 可視化の為の関数は充実しています。 例えば、キャリブレーションとホールドアウトそれぞれの期間で発生したトランザクションのトラジェクトリーを描画するには、以下の関数に学習済みモデルを渡すだけです。 plot_calibration_purchases_vs_holdout_purchases(bgf, summary_cal_holdout, n= 20 ) オレンジがモデルの予測で青が実測です。モデルの予測と実際のトランザクション数が概ね一致している事が確認できます。 サンプルコード というほどのものでもないですが、実験に使ったJupyter Notebookを載せておきます。 後半では、周辺化した をMCMCで復元してみました。モデルパラメータ はユーザー全体の特徴を表現する値でしたが、 を復元することでユーザー個人を分析することができます。 MCMCのスクリプトはNotebookと同じgistに公開しました。 モデルの制約 Pareto/NBDやBG/NBDモデルでは、 定常的に発生するトランザクションをモデル化したもので、周期性のある購買行動や広告などの影響は説明できない 一度離脱したユーザーは二度と復帰しない というかなり強い制約があります。したがってこの手法を適用する前はデータの性質をよく観察しておく必要があります。 モデルの制約を緩める方向に発展させた手法は存在し、例えばRemoro13はユーザーの状態が変化することを許容し、HMMで状態遷移をモデル化しています。 まとめ VASILYでは、最新の研究にアンテナを張りながら、同時にユーザーの課題解決を積極的に行うメンバーを募集しています。 興味のある方はこちらからご応募ください。 https://www.wantedly.com/projects/109351 www.wantedly.com 参考 David C. Schmittlein, Donald G. Morrison, Richard Colombo, Counting Your Customers: Who-Are They and What Will They Do Next?, 1987. Peter S. Fader, Bruce G. S. Hardie, Ka Lok Lee, “Counting Your Customers” the Easy Way: An Alternative to the Pareto/NBD Model, 2005. Jaime Romero, Ralf van der Lans, Berend Wierenga, A Partially Hidden Markov Model of Customer Dynamics for CLV Measurement, 2013.
アバター
こんにちは。フロントエンドエンジニアの遠藤です。 皆さん、ConstraintLayoutを使用していますか? 弊社では最近、ほとんどのレイアウトをConstraintLayoutを使用して実装しています。 今回はConstraintLayoutを使用してレイアウトを組んだ際に便利だなと思ったポイントや難しくてはまったことについて紹介したいと思います。 今回はConstraintLayoutを使用したレイアウトの組み方について注目するので、基本的な使い方については説明しません。 ConstraintLayoutの基本的な使い方は下記の記事が分かりやすくておすすめです。 Google Developer Yukiの枝折: ConstraintLayout 実用例 今回はConstraintLayout1.1.0-beta5を使用しています。 サンプルコードはGitHubにあげてあります。 github.com 1. Chainを利用した複数要素の中央寄せ 例えば次のようなデザインを実装することを考えます。 このデザインは、コンテンツと閉じるボタンを合わせて中央寄せする必要があります。 今まではrootにFrameLayout、中央のコンテンツにRelativeLayoutを使用してレイアウトをネストすることで実装していました。 <FrameLayout xmlns : android = "http://schemas.android.com/apk/res/android" android : layout_width = "match_parent" android : layout_height = "match_parent" android : background = "@color/background_color_dialog" > <RelativeLayout android : layout_width = "wrap_content" android : layout_height = "wrap_content" android : layout_gravity = "center" > <View android : id = "@+id/background" android : layout_width = "240dp" android : layout_height = "250dp" android : background = "@drawable/background_white_corner_radius" /> <TextView android : id = "@+id/title" android : layout_width = "wrap_content" android : layout_height = "wrap_content" android : layout_alignTop = "@id/background" android : layout_centerHorizontal = "true" android : layout_marginTop = "20dp" android : text = "@string/sample_1_title" android : textColor = "@color/colorAccent" android : textSize = "15sp" android : textStyle = "bold" /> ・・・ <TextView android : id = "@+id/dismissLink" android : layout_width = "wrap_content" android : layout_height = "wrap_content" android : layout_below = "@id/background" android : layout_centerHorizontal = "true" android : layout_marginTop = "20dp" android : text = "@string/sample_1_dismiss" android : textColor = "@android:color/white" /> </RelativeLayout> </FrameLayout> しかし、ConstraintLayoutのchainという仕組みを使うことでレイアウトをネストすることなく2つの要素を中央に配置することが出来ます。 <android . support . constraint . ConstraintLayout xmlns : android = "http://schemas.android.com/apk/res/android" xmlns : app = "http://schemas.android.com/apk/res-auto" android : layout_width = "match_parent" android : layout_height = "match_parent" android : background = "@color/background_color_dialog" > <View android : id = "@+id/background" android : layout_width = "240dp" android : layout_height = "250dp" android : background = "@drawable/background_white_corner_radius" app : layout_constraintBottom_toBottomOf = "parent" app : layout_constraintLeft_toLeftOf = "parent" app : layout_constraintRight_toRightOf = "parent" app : layout_constraintTop_toTopOf = "parent" app : layout_constraintVertical_chainStyle = "packed" /> <TextView android : id = "@+id/title" android : layout_width = "wrap_content" android : layout_height = "wrap_content" android : layout_marginTop = "20dp" android : text = "@string/sample_1_title" android : textColor = "@color/colorAccent" android : textSize = "15sp" android : textStyle = "bold" app : layout_constraintLeft_toLeftOf = "@id/background" app : layout_constraintRight_toRightOf = "@id/background" app : layout_constraintTop_toTopOf = "@id/background" /> ・・・ <TextView android : id = "@+id/dismissLink" android : layout_width = "wrap_content" android : layout_height = "wrap_content" android : layout_marginTop = "20dp" android : text = "@string/sample_1_dismiss" android : textColor = "@android:color/white" app : layout_constraintBottom_toBottomOf = "parent" app : layout_constraintLeft_toLeftOf = "parent" app : layout_constraintRight_toRightOf = "parent" app : layout_constraintTop_toBottomOf = "@id/background" /> </android . support . constraint . ConstraintLayout> ConstraintLayoutのChainは制約を要素どうし双方向に設定することでグループのように扱えます。 Chainにはいくつか種類があり、今回はグループ化した要素を詰めて表示したいため packed を使用しています。詳しくは こちら をご参照下さい。 2. ネガティブマージンの設定方法 このような重なっているデザインを実装する際に、四角枠の左上にバツボタンを配置してネガティブマージンを使用してデザインを実装する方法を思い浮かべるのではないでしょうか。 しかし、ConstraintLayoutで layout_marginTop に-20dpを設定しても反映されません。 ConstraintLayoutでネガティブマージンを表現するには少し工夫が必要になります。 こちらの記事 でGoogleのデベロッパーが Space を使用する方法を紹介しています。 今回の例を実装するには、四角枠の左上側にSpaceの右下側を配置するようにします。 そのSpaceの左上側にバツボタンの左上側を配置すると表現できます。 <android . support . constraint . ConstraintLayout xmlns : android = "http://schemas.android.com/apk/res/android" xmlns : app = "http://schemas.android.com/apk/res-auto" android : layout_width = "match_parent" android : layout_height = "match_parent" > <View android : id = "@+id/contentsBackground" android : layout_width = "100dp" android : layout_height = "100dp" android : background = "@drawable/background_gray_corner_radius" app : layout_constraintBottom_toBottomOf = "parent" app : layout_constraintLeft_toLeftOf = "parent" app : layout_constraintRight_toRightOf = "parent" app : layout_constraintTop_toTopOf = "parent" /> <android . support . v7 . widget . AppCompatImageView android : id = "@+id/android" android : layout_width = "80dp" android : layout_height = "80dp" app : layout_constraintBottom_toBottomOf = "@id/contentsBackground" app : layout_constraintLeft_toLeftOf = "@id/contentsBackground" app : layout_constraintRight_toRightOf = "@id/contentsBackground" app : layout_constraintTop_toTopOf = "@id/contentsBackground" app : srcCompat = "@drawable/ic_android" /> <Space android : id = "@+id/negativeMargin" android : layout_width = "15dp" android : layout_height = "15dp" app : layout_constraintBottom_toTopOf = "@id/contentsBackground" app : layout_constraintRight_toLeftOf = "@id/contentsBackground" /> <android . support . v7 . widget . AppCompatImageView android : layout_width = "40dp" android : layout_height = "40dp" app : layout_constraintLeft_toLeftOf = "@id/negativeMargin" app : layout_constraintTop_toTopOf = "@id/negativeMargin" app : srcCompat = "@drawable/ic_close_vector" /> </android . support . constraint . ConstraintLayout> 3. 文字の省略とレイアウト寄せ リストで文章を表示するときに全てを表示せずに省略して表示するデザインは多いです。 今回、以下のような文章の横にテキストを配置しているレイアウトを組むのが難しくとてもはまってしまいました。 文章を省略する 文章を省略するのに ellipsize を設定しますが、ConstraintLayoutを使用している場合それだけでは省略されません。 layout_constrainedWidth にtrueを設定する必要があります。 これを設定することで、きちんと文章が省略されるようになります。 文章と「既読」ラベルを左に寄せて表示させる 順を追って説明します。 文章と、「既読」ラベル、日付の要素をchainしてpackedを設定してしまうと以下の図のように表示されます。 文章と「既読」ラベルを左寄せで表示させたいので、日付との双方向の制約を外してChianしないようにします。 しかし、文章が長いときに日付とかぶらないように「既読」ラベルを日付の左側に配置するという制約は残しておきます。 これで、文章と「既読」ラベルだけがグループになります。 これだけは左寄せにならないのでbiasを設定します。 これで文章と「既読」ラベルが左に寄って表示されるようになります。 以下、上記のデザインのxmlです。 <android . support . constraint . ConstraintLayout xmlns : android = "http://schemas.android.com/apk/res/android" xmlns : app = "http://schemas.android.com/apk/res-auto" android : layout_width = "match_parent" android : layout_height = "100dp" > <android . support . v7 . widget . AppCompatImageView android : id = "@+id/imageView" android : layout_width = "80dp" android : layout_height = "80dp" android : layout_marginStart = "8dp" app : layout_constraintBottom_toBottomOf = "parent" app : layout_constraintStart_toStartOf = "parent" app : layout_constraintTop_toTopOf = "parent" app : srcCompat = "@drawable/ic_android" /> <TextView android : id = "@+id/nameLabel" android : layout_width = "wrap_content" android : layout_height = "wrap_content" android : layout_marginStart = "16dp" android : ellipsize = "end" android : lines = "1" android : scrollHorizontally = "true" android : textColor = "@color/text_color_black" android : textSize = "18sp" android : textStyle = "bold" app : layout_constrainedWidth = "true" app : layout_constraintEnd_toStartOf = "@id/dateLabel" app : layout_constraintHorizontal_bias = "0" app : layout_constraintStart_toEndOf = "@id/imageView" app : layout_constraintTop_toTopOf = "@id/imageView" /> <TextView android : id = "@+id/dateLabel" android : layout_width = "wrap_content" android : layout_height = "wrap_content" android : layout_marginEnd = "16dp" android : textColor = "@color/text_color_gray" android : textSize = "15sp" app : layout_constraintBottom_toBottomOf = "parent" app : layout_constraintEnd_toEndOf = "parent" app : layout_constraintTop_toTopOf = "parent" /> <TextView android : id = "@+id/messageLabel" android : layout_width = "wrap_content" android : layout_height = "wrap_content" android : layout_marginEnd = "8dp" android : layout_marginStart = "16dp" android : layout_marginTop = "8dp" android : ellipsize = "end" android : lines = "1" android : textColor = "@color/text_color_black" android : textSize = "15sp" app : layout_constrainedWidth = "true" app : layout_constraintEnd_toStartOf = "@id/readLabel" app : layout_constraintHorizontal_bias = "0" app : layout_constraintHorizontal_chainStyle = "packed" app : layout_constraintStart_toEndOf = "@id/imageView" app : layout_constraintTop_toBottomOf = "@id/nameLabel" /> <TextView android : id = "@+id/readLabel" android : layout_width = "wrap_content" android : layout_height = "wrap_content" android : layout_marginEnd = "8dp" android : layout_marginStart = "2dp" android : ellipsize = "end" android : lines = "1" android : scrollHorizontally = "true" android : text = "@string/sample_2_read_label" android : textColor = "@color/text_color_gray" android : textSize = "12sp" app : layout_constraintBaseline_toBaselineOf = "@id/messageLabel" app : layout_constraintEnd_toStartOf = "@id/dateLabel" app : layout_constraintStart_toEndOf = "@id/messageLabel" /> </android . support . constraint . ConstraintLayout> まとめ ConstraintLayoutを使用したレイアウトの組み方の実用例でした。 ConstraintLayoutは表現力が高く、柔軟に要素を配置することができるのですごく便利です。 レイアウトを組む際に参考として見ていただけると幸いです。 最後に VASILYではデザイン実装にこだわりを持っているエンジニアを募集しています。 少しでも興味がある方は以下のリンクからお申し込みください。
アバター
こんにちは、VASILYで主にAndroid開発を担当している @Horie1024 です。先日 potatotips #48 で「Alexa、APKを配布して」というタイトルでLTさせて頂きました。 資料は以下になりますが、本投稿では、Alexaスキルの仕組みから最終的にAPKが配布されるまでを出来る限り詳細に解説していきます。 目次 目次 AlexaでAPKを配布する流れ Alexaスキル開発事始め Alexaスキルの仕組み 対話モデル インテントとスロット サンプル発話 インテントスキーマ APKを配布するスキルの作成 Amazon developerへの登録 Alexaメニューの表示 スキルの作成 対話モデルの作成 設定(AWS Lambdaエンドポイントの指定) Serverlessを利用したAWS Lambdaエンドポイントの開発 Serverlessのインストール テンプレートから雛形プロジェクト作成 必要なライブラリの追加 serverless.ymlの更新 Alexaに対応したLambda functionの実装方法 Bitriseの設定 セットアップ Workflows DeployGateへのAPKのアップロード AWS LambdaからのBitriseのビルドのトリガー AWS Lambdaへのデプロイ IAMの作成 認証情報の登録 デプロイ シミュレータでのテスト 実機でのテスト 公開情報の入力 プライバシーコンプライアンスの入力 テスターへの追加とベータテストの開始 まとめ 最後に AlexaでAPKを配布する流れ 最初にAPKを配布するまでの全体の流れについて概要を示します。 ユーザーがAlexa対応デバイス(Amazon echoなど)に対して音声でAPKの配布指示を出し実際に配布されるまでの流れは以下のようになります。 ユーザーが「Alexa、BitriseでAPKを配布して」と発話 Alexaサービスでの発話の処理とAWS Lambdaエンドポイントへのリクエスト Lambda FunctionでBitriseのビルドをトリガー APKのビルドとDeployGateへのアップロード DeployGateを経由したAPKの配布 ユーザーの音声をAlexaが理解し、AWS Lambdaエンドポイントを経由してBitriseでのビルドを動かす事でAPKを配布します。 Alexaスキル開発事始め Alexaに開発者によって追加された機能を スキル と呼びます。Alexaスキルには以下のような種類があります。 カスタムスキル : 汎用のスキル スマートホームスキル : 家電製品などを制御するスキル フラッシュブリーフィングスキル : ニュースなどを読み上げるスキル APKを配布するスキルはカスタムスキルにあたり、スマートホームスキルやフラッシュブリーフィングスキルは使用しません。 Alexaスキルの仕組み Alexaがユーザーの発話を受けてレスポンスを返すまでの流れは以下のようになります。 引用: Alexaスキル開発トレーニングシリーズ 第1回 初めてのスキル開発 ユーザーが発話し、Alexa対応デバイスからレスポンスを受け取るまでを順番に見ていきましょう。 ユーザーがAlexa対応デバイスに発話 デバイスはマイクで集音した音声データをAlexaサービスに送信 Alexaサービスは音声データを解析し、AWS Lambdaを呼び出して解析結果を伝達 Lambdaはスキルの処理を実行し、結果をAlexaサービスにレスポンスとして返却 Alexaサービスはレスポンスに応じた音声データを生成、デバイスに返信 デバイスは音声データを再生し、ユーザーに結果を伝達 この流れを実現するために開発者が行うことは以下の2点です。 対話モデル と呼ばれるユーザーの音声をAlexaサービスに理解させるためのルールの設定 対話モデルによる解析結果に応じて実行する AWS Lambdaエンドポイント の開発 対話モデル 以下の画像は対話モデルとAWS Lambdaエンドポイントの関係を表す図です。 引用: Alexaスキル開発トレーニングシリーズ 第2回 対話モデルとAlexa SDK Alexaサービスは、対話モデルによってユーザーの発する音声データを理解しAWS Lambdaエンドポイントを呼び出します。 対話モデルは、 インテントとスロット および サンプル発話 から構成されます。 インテントとスロット AlexaサービスがAWS Lambdaエンドポイントを呼び出す際のリクエストは、 インテント と スロット からなります。スロットはユーザーの音声データのうち特定の型(日付や都市名)に当てはまるものを値として受け取れる変数のようなものです。APKを配布するスキルでは使用しません。 インテント(Androidエンジニアには親しみやすい名前ですね!)はユーザーの要望や意図を表し、スキルはインテントに応じた振る舞いを行うようにします。インテントは1つのスキルで複数定義することが可能です。 引用: Alexaスキル開発トレーニングシリーズ 第2回 対話モデルとAlexa SDK また、インテントには標準の組み込みインテントがあり、それらも利用できます。以下は主な組み込みインテントです。 インテント名 概要 AMAZON.HelpIntent スキルの使い方を尋ねるインテント AMAZON.StopIntent 処理やスキルを終了させるインテント その他の組み込みインテントについては Built-in Intent Library を参照してください。 さて、ではAPKを配布するスキルはどのようなインテント名が良いでしょうか。APKを配布するという要望を表した DistributeApk とします。 サンプル発話 サンプル発話は、ユーザーの発話を適切なインテントとスロットに紐付けるための例文です。APKを配布する例文として以下を想定します。例文を増やすほど様々な言い回しに対応できますが、この3通りで恐らく問題ないでしょう。 apk を 配布して apk を 配布 apk を 配って 実際にユーザーがスキルを使用する場合、アレクサ + 呼び出し名 + サンプル発話 の形で発話することになります。呼び出し名はスキルを識別するためのprefixで必ず必要になります。 インテントスキーマ インテントとスロット、サンプル発話はインテントスキーマというJSON形式でスキルに設定されます。これによって、ユーザーの発話がサンプル発話の例文に当てはまる場合、インテント名がDistributeApkとしてリクエストがAWS Lambdaエンドポイントに送られます。 { " intents ": [ { " name ": " DistributeApk ", " slots ": [] , " samples ": [ " apk を 配布して ", " apk を 配布 ", " apk を 配って " ] } ] , " types ": [] } APKを配布するスキルの作成 ここからは、Amazon developerのAlexaコンソールにアクセスし、実際にスキルを作成する流れを紹介します。 Amazon developerへの登録 Amazon developerポータル へアクセスし、アカウントを作成します。この時ハマりやすいポイントがあるので注意してください。 Alexa 開発者アカウント作成時のハマりどころ を読んでからアカウントを作成することをオススメします。 Alexaメニューの表示 ログイン後ALEXAタブをクリックし以下のような画面を表示し、「Alexa Skills Kit」の始めるをクリックします。 スキルの作成 「新しいスキルを追加する」をクリックするとスキルの情報を入力する画面が表示されるので埋めていきます。 以下のように入力しています。呼び出し名がカタカナの方が認識されやすいです。 項目 値 スキルの種類 カスタム対話モデル 言語 Japanese スキル名 Distribute Apk 呼び出し名 ビットライズ 呼び出し名にビットライズを指定することで、ユーザーは「アレクサ、ビットライズで○○」という発話でスキルを使用します。呼び出し名をビットライズにしているのは、APKのビルドと配布に Bitrise を利用するためです。これについては後ほど解説します。また、 呼び出し名を省略することはできません。 対話モデルの作成 対話モデルの作成は、2018年3月現在でベータ版ですがSkill Builderを使用した方が簡単です。「スキルビルダーを起動する」をクリックすると起動します。 左メニューのIntentsのADDをクリックすることでインテントを追加できます。 Create a new custom intentの入力フォームに「DistributeApk」を入力しCreate Intentをクリックします。 「Sample Utterances」のフォームにサンプル発話を入力します。サンプル発話の節で想定したサンプル発話を入力します。 apk を 配布して apk を 配布 apk を 配って 入力が完了したら「SaveModel」をクリックし対話モデルを保存します。保存が完了したら「Build Model」をクリックし対話モデルをビルドします。 これで対話モデルの作成は完了です。ここまでGUIで対話モデルを作成しましたが、Code Editorを選択するとJSONで表現された対話モデルを確認でき編集も可能です。 設定(AWS Lambdaエンドポイントの指定) サービスエンドポイントのタイプにAWS LambdaのARN(Amazonリソースネーム)を選択し、デフォルトにデプロイ済みのLambda FunctionのARNを指定します。Lambda Functionの実装はまだ行っておらず、ARNはわからないので一旦入力せずに保存しておきます。 Serverlessを利用したAWS Lambdaエンドポイントの開発 AlexaスキルはAlexaサービスのリクエスト先としてエンドポイントを用意する必要があり、AWS Lambdaで実装することが推奨されています。 今回、Lambda functionの実装には言語としてTypeScript、AWSへのデプロイにはServerlessを使用しています。 Serverlessは、サーバーレスアーキテクチャへのデプロイや管理を行うためのツールです。AWS Lambda、Google Cloud Functions、Azure Functions、IBM OpenWhiskといったFaaS(Function as a Service)に対応しています。 serverless.com Serverlessを使用するとLambda functionのデプロイおよび管理を簡単に行えます。 Serverlessのインストール Node.jsのインストールが必要になります。 nvm や nodebrew を使うと簡単です。本記事ではNode.js v8.9.4 を使用しています。 Serverlessは、npmを使用する場合は以下のようにインストールします。 $ npm install -g serverless また、 yarn を使用する場合以下のようになります。 $ yarn global add serverless 本記事で使用するserverlessのバージョンは1.26.1になります。 $ serverless -v 1.26.1 テンプレートから雛形プロジェクト作成 aws-nodejs-typescriptをtemplateに指定してserverless createを実行します。 $ serverless create --template aws-nodejs-typescript Serverless: Generating boilerplate... _______ __ | _ .-----.----.--.--.-----.----| .-----.-----.-----. | |___| -__ | _| | | -__ | _| | -__ |__ -- |__ -- | |____ |_____|__| \___/|_____|__| |__|_____|_____|_____| | | | The Serverless Application Framework | | serverless.com, v1. 26 . 1 ------- ' Serverless: Successfully generated boilerplate for template: "aws-nodejs-typescript" Serverless: NOTE: Please update the "service" property in serverless.yml with your service name 以下のように生成されます。 $ tree . ├── handler.ts ├── package.json ├── serverless.yml ├── tsconfig.json └── webpack.config.js 必要なライブラリの追加 開発に必要なライブラリや型定義の依存を追加します。 $ npm install alexa-sdk @types/alexa-sdk @types/request --save-dev requestモジュール については通常のdependenciesで追加します。型定義のみdevdependenciesとしました。 $ npm install request--save yarnを使用する場合以下の通りです。 $ yarn add alexa-sdk @types/alexa-sdk @types/request --dev $ yarn add request serverless.ymlの更新 serverless.ymlをAlexaサービスからのリクエストを受けられるよう変更します。具体的には events に alexaSkill を指定します。serverlessのドキュメントは こちら です。 これでAlexaサービスからのリクエストを handler.bitriseSkill で受けられるようになりました。 service : name : alexa-bitrise-distribute-apk # Add the serverless-webpack plugin plugins : - serverless-webpack provider : name : aws runtime : nodejs6.10 functions : bitriseSkill : handler : handler.bitriseSkill events : - alexaSkill Alexaに対応したLambda functionの実装方法 Alexaスキルのエンドポイントに対応したLambda functionの書き方はシンプルで、 alexa.registerHandlers によって対話モデルで定義したインテント名に対応した処理を登録し、 alexa.execute() を実行するのみです。 AlexaサービスからDistributeApkインテントが送られてくると、対応するプロパティ DistributeApk が実行されます。 Unhandled 、 LaunchRequest 、 AMAZON.StopIntent はそれぞれ標準で用意されている組み込みインテントに対応しています。 import * as Alexa from 'alexa-sdk' import * as request from 'request' export const bitriseSkill = ( event : Alexa.RequestBody < any > , context : Alexa.Context ) => { const appSlug = process .env.APP_SLUG const apiToken = process .env.API_TOKEN const appId = process .env.APP_ID const alexa = Alexa.handler(event, context) alexa.appId = appId alexa.registerHandlers( { 'Unhandled' : function () { this .emit( ':tell' , 'よく分かりません。' ) } , 'LaunchRequest' : function () { this .emit( ':tell' , 'こんにちは。このスキルはビットライズのビルドをスタートさせエーピーケーを配布します。' ) } , 'AMAZON.StopIntent' : function () { this .emit( ':tell' , 'またご利用ください。' ) } , 'DistributeApk' : function () { // TODO ここにDistributeApkインテントがリクエストされた際の処理を書く } } ) alexa.execute() } DistributeApkインテントがリクエストされた場合に行う処理は、Bitriseの Build Trigger API を使用したビルドのトリガーです。具体的な実装を行う前にBitriseの設定を行いましょう。 Bitriseの設定 Bitrise はモバイルアプリにフォーカスしたCI/CDのPlatform as a Service(PaaS)です。セットアップが非常に簡単で数クリックするだけでAndroidアプリのCI/CD環境を構築することができます。また、Worlflowの概念を取り入れていてビルドステップのカスタマイズも容易です。 今回BitriseをAPKのビルドとDeployGateへのアップロードを行うCIサービスに選んだ理由は以下の3点です。 Androidプロジェクトのセットアップが容易である Build Trigger APIが用意されている Workflowを利用できる 以下のサンプルアプリプロジェクトについてセットアップします。 github.com セットアップ Bitriseにログインし、「Add first app」をクリックします。 セットアップ画面が表示されるので画面の指示に従って設定していきます。「Connect your repository」で接続するリポジトリを選択します。 「Setup repository access」では特に必要がなければ「AUTOMATIC」を選択し、「No, auto-add SSH key」をクリックします。 「VALIDATING REPOSITORY」と表示されるのでしばらく待ちます。Bitriseが対象のリポジトリに含まれるプロジェクトがどのプラットフォームに属するかを判別します。 Validationが完了するとプロジェクトの判別結果に基づいてビルドの設定を自動的に行なってくれます。問題があれば「MANUAL」をクリックし手動で設定できます。 特に問題が無ければ「Confirm」をクリックします。 「Register a Webhook for me!」をクリックするとGitHubやBitbucketからのWebhookを自動的にセットアップし、コードをリポジトリにpushした際、Bitriseが自動的にビルドを開始してくれるようになります。 これでセットアップは完了です。 Workflows BitriseのビルドフローはStepと呼ばれる単一のタスクを実行する部品のコレクションとして表され、 Workflows と呼ばれています。アプリケーションのトップページで「Workflow」タブをクリックすることでWorkflow Editorが起動し、Workflowsを編集可能です。 Workflowsはデフォルトで「primary」と「deploy」が用意されています。新たなWorkflowを作る事も出来ますし、複数のWorkflowを繋げる事も可能です。 DeployGateへのAPKのアップロード DeployGateへのAPKのアップロードは、StepをWorkflowに追加する事で実現します。今回、deploy workflowが実行された場合のみAPKがDeployGateへアップロードされるようにします。「deploy workflow」を選択し、Gradle Runner stepの直後に新たにScript stepを追加します。 DeployGateへのAPKのアップロードはcurlコマンドで実行可能です。詳しくは DeployGate APIリファレンス を参照してください。 以下のコマンドを追加したScript stepで実行するよう設定します。 curl \ -F " token= ${DEPLOY_GATE_API_TOKEN} " \ -F " file=@ ${BITRISE_APK_PATH} " \ -F " message=distribute from alexa " \ https://deploygate.com/api/users/horie1024/apps DEPLOY_GATE_API_TOKEN は環境変数としてBitriseに登録します。Workflow Editorからdeploy workflowでのみ有効な環境変数を登録可能です。 BITRISE_APK_PATH はデフォルトでBitriseが用意する環境変数になります。 最終的なScript stepは以下のようになります。これでdeploy workflowが実行された場合にAPKがDeployGateへアップロードされるようになりました。 AWS LambdaからのBitriseのビルドのトリガー 先程deploy workflowが実行された場合にAPKがDeployGateへアップロードされるようBitriseを設定しました。次にBitriseの Build Trigger API を利用してAWS Lambdaからdeploy workflowを実行します。 Alexaに対応したLambda functionの実装方法 で紹介したコードに処理を追加しましょう。 Build Trigger APIでは、branchやworkflowを指定する事が可能で、deploy workflowでmasterブランチについてビルドし「triggered from alexa」というメッセージをビルドに付加するリクエストBodyは以下のようになります。このリクエストBodyをBuild Trigger APIのエントリーポイントへPOSTリクエストで送信します。 { " hook_info ": { " type ": " bitrise ", " api_token ": " ... " } , " build_params ": { " branch ": " master ", " workflow_id ": " deploy ", " commit_message ": " triggered from alexa " } } requestモジュール を利用したPOSTリクエストは以下のように書く事ができます。コード中のappSlugとapiTokenはAWS Lambdaの環境変数に予め登録しておき実行時に取得します。appSlugおよびapiTokenは、BitriseのアプリケーショントップのCodeタブから確認可能です。 Build Trigger APIへのリクエストが完了後、 this.emit(':tell', '') を使用してユーザーへレスポンスを返します。レスポンスからworkflow idを取得してユーザーへ返すメッセージを動的に変更しています。 request( { method : 'post' , url : `https://www.bitrise.io/app/ ${ appSlug } /build/start.json` , headers : { "Content-Type" : "application/json; charset=utf-8" } , json : true , body : { 'hook_info' : { 'type' : 'bitrise' , 'api_token' : apiToken } , 'build_params' : { 'branch' : 'master' , 'workflow_id' : 'deploy' , 'commit_message' : 'triggered from alexa' } } } , ( error , response , body ) => { let workflowId = body.triggered_workflow this .emit( ':tell' , `ワークフローアイディー ${ workflowId } で、ビットライズでのビルドを開始しました。ビルドが完了後、エーピーケーを配布します。` ) } ) 最終的なコードは以下のようになります。 import * as Alexa from 'alexa-sdk' import * as request from 'request' export const bitriseSkill = ( event : Alexa.RequestBody < any > , context : Alexa.Context ) => { const appSlug = process .env.APP_SLUG const apiToken = process .env.API_TOKEN const appId = process .env.APP_ID const alexa = Alexa.handler(event, context) alexa.appId = appId alexa.registerHandlers( { 'Unhandled' : function () { this .emit( ':tell' , 'よく分かりません。' ) } , 'LaunchRequest' : function () { this .emit( ':tell' , 'こんにちは。このスキルはビットライズのビルドをスタートさせエーピーケーを配布します。' ) } , 'AMAZON.StopIntent' : function () { this .emit( ':tell' , 'またご利用ください。' ) } , 'DistributeApk' : function () { request( { method : 'post' , url : `https://www.bitrise.io/app/ ${ appSlug } /build/start.json` , headers : { "Content-Type" : "application/json; charset=utf-8" } , json : true , body : { 'hook_info' : { 'type' : 'bitrise' , 'api_token' : apiToken } , 'build_params' : { 'branch' : 'master' , 'workflow_id' : 'deploy' , 'commit_message' : 'triggered from alexa' } } } , ( error , response , body ) => { let workflowId = body.triggered_workflow this .emit( ':tell' , `ワークフローアイディー ${ workflowId } で、ビットライズでのビルドを開始しました。ビルドが完了後、エーピーケーを配布します。` ) } ) } } ) alexa.execute() } サンプルコードはこちらです。 github.com AWS Lambdaへのデプロイ IAMの作成 Serverlessが使用するIAMを作成します。Serverlessの AWS - Credentials を参考に作成しましょう。 AWSにログイン後サービスからIAMを選択しユーザーメニューから「ユーザーを追加」をクリックしてください。または こちら をクリックするとユーザーを追加画面が表示されます。 ユーザ名に「serverless-admin」アクセスの種類は「プログラムによるアクセス」にチェックを付けます。 次のステップでは、アクセス権限を設定を設定します。「既存のポリシーを直接アタッチ」からAdministratorAccessにチェックを付けます。 最後に選択内容を確認し「ユーザーの作成」をクリックします。ユーザーの追加が成功すると「アクセスキーID」と「シークレットアクセスキー」が表示されるのでこちらをメモしておきます。 アクセス情報の取扱いには十分注意してください 。 認証情報の登録 作成後 serverless config でAWSの認証情報を登録します。 $ serverless config credentials --provider aws --key YOUR_KEY --secret YOUR_SECRET デプロイ 登録が完了後、 serverless deploy を実行するとTypeScriptのコンパイルからAWS Lambdaへのデプロイまで自動的に行われます。 $ serverless deploy Serverless: Bundling with Webpack... ts-loader: Using typescript@ 2 . 7 . 2 and tsconfig.json Time: 3875ms Asset Size Chunks Chunk Names handler.js 4 . 46 MB 0 [ emitted ] [ big ] handler handler.js.map 5 . 99 MB 0 [ emitted ] handler [ 30 ] ./node_modules/alexa-sdk/lib/utils/textUtils.js 1 . 48 kB { 0 } [ built ] [ 82 ] ./node_modules/extend/index.js 2 . 27 kB { 0 } [ built ] [ 144 ] ./node_modules/request/lib/cookies.js 974 bytes { 0 } [ built ] [ 182 ] ./handler.ts 1 . 96 kB { 0 } [ built ] [ 183 ] ./node_modules/alexa-sdk/index.js 1 . 62 kB { 0 } [ built ] [ 184 ] ./node_modules/alexa-sdk/lib/alexa.js 8 . 83 kB { 0 } [ built ] [ 807 ] ./node_modules/alexa-sdk/lib/templateBuilders/bodyTemplate1Builder.js 923 bytes { 0 } [ built ] [ 808 ] ./node_modules/alexa-sdk/lib/templateBuilders/bodyTemplate2Builder.js 1 . 16 kB { 0 } [ built ] [ 809 ] ./node_modules/alexa-sdk/lib/templateBuilders/bodyTemplate3Builder.js 1 . 16 kB { 0 } [ built ] [ 810 ] ./node_modules/alexa-sdk/lib/templateBuilders/bodyTemplate6Builder.js 1 . 16 kB { 0 } [ built ] [ 817 ] ./node_modules/alexa-sdk/lib/services/directiveService.js 2 . 27 kB { 0 } [ built ] [ 818 ] ./node_modules/alexa-sdk/lib/directives/voicePlayerSpeakDirective.js 604 bytes { 0 } [ built ] [ 819 ] ./node_modules/alexa-sdk/lib/utils/imageUtils.js 3 . 33 kB { 0 } [ built ] [ 820 ] ./node_modules/request/index.js 3 . 97 kB { 0 } [ built ] [ 825 ] ./node_modules/request/request.js 44 . 7 kB { 0 } [ built ] + 920 hidden modules Serverless: Packaging service... Serverless: Uploading CloudFormation file to S3... Serverless: Uploading artifacts... Serverless: Validating template... Serverless: Updating Stack... Serverless: Checking Stack update progress... ......... Serverless: Stack update finished... Service Information service: alexa-bitrise-distribute-apk stage: dev region: us-east-1 stack: alexa-bitrise-distribute-apk-dev api keys: None endpoints: None functions: bitriseSkill: alexa-bitrise-distribute-apk-dev-bitriseSkill Serverless: Removing old service versions... AWSコンソールを開きサービスからLambdaを選択すると「alexa-bitrise-distribute-apk-dev-bitriseSkill」という関数が作成されています。関数名をクリックし関数の詳細画面に遷移後 ARN を確認してください。 Alexaスキルメニューの設定でARNを入力し保存しましょう。 シミュレータでのテスト スキルのテストは、Alexaスキルメニューの「テスト」から簡単に行うことができます。2018年3月現在でベータ版ですが、実際に発話して動作を確認できるためテストシミュレータを使用することをオススメします。 「テストシミュレータに進む」をクリックする事でテストシミュレータに切り替える事が可能です。 また、「このスキルをテストするには対話モデルのタブを完成させてください。」を有効に切り替えてください。 Alexa Simulatorタブの入力フォーム横マイクアイコンをクリックしホールドしている間、テストシミュレータは音声入力を受け付けます。シミュレータでテストする場合最初に「Alexa」と話しかける必要はありません。 実機でのテスト Amazon echoなどのAlexa対応デバイス実機でスキルを動かすには、スキルのベータテストを開始する必要があります。 ベータテストを開始するには、「スキル」「対話モデル」「テスト」「公開情報」「プライバシーコンプライアンス」の全ての項目が完了している必要があります。 ここまでで未入力な項目は「公開情報」「プライバシーコンプライアンス」の2項目のはずなので完了させましょう。 公開情報の入力 公開情報の入力を完了するためには全ての項目を埋める必要があります。スキルのアイコン画像2種類(108×108、512×512)を登録する必要があるので用意してください。 今回はスキルを公開しないので仮で入力してしまって問題ありません。公開する場合全ての情報を入力後に審査を受ける必要があります。 以下のように設定しました。 項目 値 カテゴリー Productivity サブカテゴリー Organizers & Assistants テストの手順 test 国と地域 Japan スキルの簡単な説明 AlexaにAPKの配布を依頼します。 スキルの詳細な説明 Alexaに「ビットライズでAPKを配布して」と話しかけることでAPKをDeployGate経由で配布します。 サンプルフレーズ ビットライズでAPKを配布して キーワード 未入力 プライバシーコンプライアンスの入力 プライバシーコンプライアンスは以下のように設定しました。 項目 値 このスキルを使って何かを購入をしたり、実際にお金を支払うことができますか? いいえ このスキルはユーザーの個人情報を収集しますか? いいえ このスキルは13歳未満の子供をターゲットにしていますか? いいえ 輸出コンプライアンス チェック このスキルは広告を含みますか? いいえ テスターへの追加とベータテストの開始 入力が完了し「申請する」ボタンを押せるようになりますが押さず、Skills Beta Testingの「☆ スキルのベータテスト」をクリックします。 次に、Amazon echoのセットアップに利用したメールアドレスをテスターに追加し「テストの開始」をクリックします。 これでベータテストが開始され、テスターに追加したメールアドレスに招待メールが送信されます。 JP customersのリンクをクリックしてください 。 https://alexa.amazon.co.jp/spa/index.html#skills/your-skills に遷移しますのでログイン後、表示されるダイアログで「スキルテスト」をクリックしてください。 そして「有効にする」をクリックする事でAmazon echo実機でスキルをテストすることができます。 まとめ Alexa対応デバイスに話しかける事でAPKを配布することができました。実際に実機で試してみると声だけで配布を実行できるのが新鮮でとても楽しかったですし、思っていた以上に便利です。今後Slack botで行っていた作業をAlexaスキルで実装してみるなどして、日々の業務効率の改善に生かせるか実験してみようと思います。 最後に VASILYでは、Androidエンジニアを募集しています。少しでも興味がある方のご応募をお待ちしています。
アバター
こんにちは。フロントエンドエンジニアの茨木( @niba1122 )です。 弊社のAndroidアプリ開発ではMVVMアーキテクチャを用いています。日々肥大化・複雑化していくViewModelが保守性や品質を担保する上で課題になっていましたが、Fluxアーキテクチャの導入により改善することができました。 本記事では、実際どのようにFluxアーキテクチャを導入したのかを、設計やコード例を交えながらご紹介します。 今までのMVP・MVVMの限界 アプリ開発ではMVP・MVVMといったアーキテクチャがよく用いられます。弊社のAndroidアプリ開発でもMVVMを用いています。これらのアーキテクチャはビューとドメインロジックを分割するのに役立っています。しかし、昨今のUIには多くのイベントや状態があり、更にそこにAPIリクエストなどの非同期処理が絡んできます。これらが関わるプレゼンテーション層のロジックをドメイン層に持ち込むことは難しく、結果的にPresenterやViewModelが肥大化・複雑化しがちです。弊社でも複雑な画面でよくViewModelが肥大化・複雑化し、実装・メンテナンス上の課題になっていました。それらを解決するための手法として、アプリ開発でも徐々に普及しつつあるFluxに着目しました。 Fluxについて ここで、一旦Fluxについておさらいしておきましょう。FluxはFacebookが提唱する、イベントや状態を扱うアーキテクチャです。次の図に示すように、Fluxではデータフローを単一方向に扱います。 出典: https://github.com/facebook/flux FluxではイベントやAPIリクエストの完了後、それを直接ビューで購読することなくDispatcherにActionオブジェクトで通知します。StoreはDispatcherを購読し、Actionオブジェクトに応じてState(状態)を更新します。ViewはStoreのStateをウォッチして、変更があった場合に自身を更新します。このようなデータフローにすることで、APIリクエストやリクエスト完了後の状態更新を切り分けることが出来ます。それにより、コードの見通しが良くなるだけでなくテストの書きやすさも向上します。 Fluxを導入してみる 基本設計 特定の画面の状態やイベントをFluxで管理したいので、FluxをMVVMのViewModelに適用します。AndroidにはメジャーなFluxのライブラリがないので、抽象クラスを自作することにしました。Fluxの実装例はReduxやVuexなどいくつかあります。しかし、これらのインタフェースは弊社で積極的に使っているRxJava2との相性があまり良くないです。そのため、抽象クラスのインタフェースも独自で設計しました。次の図は、Flux導入後のアプリのアーキテクチャです。 図中のStoreの範囲がFluxに相当します。Fluxの各構成要素は他のライブラリを参考に定義しています。構成要素の定義は各ライブラリのドキュメントで示されていますが、抽象的なものが多いです。そこで、本記事における構成要素の定義を明確にしておきます。 Store Fluxの構成要素を保持し、依存関係の解決やActionの伝達を行います。 Event ボタンのクリックやタイマーなどのイベントに直接対応します。実体は後述するActionCreatorのメソッドです。 ActionCreator イベントに応じてAPIリクエストなどの処理を実行します。非同期処理は全てここで行います。状態更新が必要なタイミングでActionのインスタンスを通知します。 Action Actionはイベントの発生やAPIのリクエスト開始/完了/失敗といった状態変更の基点を表し、payloadと呼ばれるデータを併せ持ちます。APIリクエスト成功のActionの場合、payloadとしてレスポンスの値を渡したりします。 Reducer&State Stateはビューの表示や更新に必要な状態変数で、Reducerのメンバーとして定義します。ReducerはActionを受け取り、それに応じてStateを更新します。単一方向のデータフローを守るため、Stateは外部からはimmutableにします。また、Stateには動的に取得するトークンなど、表示に使わない状態変数も含まれます。その為、ReducerはFluxの外に公開しません。更に、ReducerはActionのみに基づいてStateを更新するため、クラス外部へのアクセスも行いません。 Getter&View Property View Propertyはビューの状態に直接対応する要素で、View PropertyはGetterのメンバーとして定義します。GetterはReducerのメンバーであるStateを購読し、View Propertyにマッピングして通知します。Stateと同様に外部からはimmutableにします。Getterはビューに通知する必要があるので、Fluxの外部に公開します。 抽象クラス 前述の設計に従って実装を行うための抽象クラスを定義しました。抽象クラスではActionCreator、Reducer、Getterクラスに依存関係を注入します。StateやViewPropertyの通知に関しては敢えて抽象化していません。実装者が柔軟にRxJava2のストリームを定義できるようにしています。StateやViewPropertyのimmutable性は、Subjectをクラス外に公開しない、Observableをvalで公開するというコーディング規約で担保しています。 package com.example.niba1122.flux.util.flux import io.reactivex.Observable import io.reactivex.disposables.CompositeDisposable import io.reactivex.subjects.PublishSubject abstract class Store<AT, out AC, R, out G> where AC : DisposableMapper, R : DisposableMapper, G : DisposableMapper { private val dispatcher: PublishSubject<AT> = PublishSubject.create() private val reducer: R by lazy { createReducer(dispatcher.observeOn(Schedulers.computation())) } val actionCreator: AC by lazy { createActionCreator({ dispatcher.onNext(it) }, reducer) } val getter: G by lazy { createGetter(reducer) } protected abstract fun createActionCreator(dispatch: (AT) -> Unit , reducer: R): AC protected abstract fun createReducer(action: Observable<AT>): R protected abstract fun createGetter(reducer: R): G fun clearDisposables() { actionCreator.disposables.clear() reducer.disposables.clear() getter.disposables.clear() } } open class DisposableMapper { val disposables = CompositeDisposable() } Storeを実装する場合は上の抽象クラスを継承した具象クラスを定義します。この際に、ActionCreator、Reducer、Getterを生成するメソッドの定義を抽象クラスから要求されます。各構成要素でRepositoryなどを用いる場合は、具象クラスのコンストラクタからインスタンスを与えます。 ユーティリティ RxJava2は高い表現力を持っていますが、より開発しやすくするために幾つかクラスを定義して使っています。 Variable BehaviorSubjectに似たようなものです。 .value で値の取得だけでなく通知も可能な点が異なります。値を購読する場合には .observable で取得したObservableを使います。更に、immutableなインタフェースを持っているので、クラス外部へimmutableに公開することができます。 import io.reactivex.Observable import io.reactivex.subjects.BehaviorSubject interface ImmutableVariable<T> { val value: T val observable: Observable<T> } class Variable<T>(initialValue: T) : ImmutableVariable<T> { private val subject = BehaviorSubject.createDefault(initialValue) override var value: T get () = subject.value set (value) { subject.toSerialized().onNext(value) } override val observable: Observable<T> get () = subject.distinctUntilChanged() } NullableVariable 先程のVariableはRxJava2がnullを許容しないためにnullの値を扱うことが出来ません。 そこで、SwiftやRustのOptional型のように値をラップすることでnullも扱えるようにしたのがNullableVariableです。 import io.reactivex.Observable import io.reactivex.subjects.BehaviorSubject interface ImmutableNullableVariable<T> { val value: T? val observable: Observable<Wrapper<T>> } @Suppress ( "unused" ) sealed class Wrapper<T> { class Some<T>( val value: T) : Wrapper<T>() class None<T> : Wrapper<T>() fun unwrap(): T? = when ( this ) { is Some -> this .value is None -> null } } class NullableVariable<T>(initialValue: T?) : ImmutableNullableVariable<T> { private val subject = BehaviorSubject.createDefault<Wrapper<T>>( if (initialValue != null ) { Wrapper.Some(initialValue) } else { Wrapper.None() }) override var value: T? get () = subject.value.let { when (it) { is Wrapper.Some -> it.value is Wrapper.None -> null } } set (value) { if (value != null ) { subject.toSerialized().onNext(Wrapper.Some(value)) } else { subject.toSerialized().onNext(Wrapper.None()) } } override val observable: Observable<Wrapper<T>> get () = subject.toSerialized().distinctUntilChanged() } 実装例 ここまでで説明してきたFluxの実装例として、スクロールによりページングする一覧画面の例をご紹介します。 サンプルコードは GitHub にもアップロードしてあります。 ViewModel 先に述べたようにViewModelではEventをstoreに通知し、View Propertyを受け取ります。ListViewModelでは、ビューの初期化とスクロール完了のEventをStoreに通知しています。そして、View PropertyとしてRecyclerViewのアイテム、初期ローディング、エラー時のスナックバーに関する値を受け取ります。弊社で最近開発しているサービスに合わせ、RxBindingやDataBindingを使用せずにLiveDataをObserveしてViewを更新しています。 import android.arch.lifecycle.MutableLiveData import android.arch.lifecycle.ViewModel import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable class ListViewModel(repository: Repository) : ViewModel() { private val disposables: CompositeDisposable = CompositeDisposable() private val store = ListStore(repository) val listItems: MutableLiveData<List<ListItemType>> = MutableLiveData() val isShownPageLoading: MutableLiveData< Boolean > = MutableLiveData() init { store.getter.listItems .observeOn(AndroidSchedulers.mainThread()) .subscribe({ listItems.value = it }, {}).let { disposables.add(it) } store.getter.isShownPageLoading.observable .observeOn(AndroidSchedulers.mainThread()) .subscribe({ isShownPageLoading.value = it }, {}).let { disposables.add(it) } store.getter.errorSnackbar .observeOn(AndroidSchedulers.mainThread()) .subscribe({ errorSnackbar.value = it }, {}).let { disposables.add(it) } } override fun onCleared() { disposables.clear() store.clearDisposables() } fun onInitialize() { store.actionCreator.onInitialize() } fun onScrollToLast() { store.actionCreator.onScrollToLast() } } Store Storeでは画面で用いるActionCreator、Reducer、Getterの注入を行います。Repositoryの注入も併せてここで行います。 import io.reactivex.Observable import io.reactivex.Single class ListStore( private val repository: Repository) : Store<ListActionType, ListActionCreator, ListReducer, ListGetter>() { override fun createActionCreator(dispatch: (ListActionType) -> Unit , reducer: ListReducer): ListActionCreator = ListActionCreator(dispatch, reducer, repository) override fun createReducer(action: Observable<ListActionType>): ListReducer = ListReducer(action) override fun createGetter(reducer: ListReducer): ListGetter = ListGetter(reducer) } Action 状態更新の基点になるActionを定義しています。Action毎に付随するデータが異なるので、enumではなくsealed classを用いています。 sealed class ListActionType { class StartInitialLoad : ListActionType() class SuccessInitialLoad( val elements: List<Element>) : ListActionType() class StartNextLoad : ListActionType() class SuccessNextLoad( val elements: List<Element>) : ListActionType() class Error( val error: Throwable) : ListActionType() } ActionCreator Eventに応じてAPIリクエストなどの処理を行い、状態更新の基点でActionを通知します。 class ListActionCreator( private val dispatch: (ListActionType) -> Unit , private val reducer: ListReducer, private val repository: Repository) : DisposableMapper() { fun onInitialize() { dispatch(ListActionType.StartInitialLoad()) repository.list( 1 ) .subscribe({ dispatch(ListActionType.SuccessInitialLoad(it)) }, { dispatch(ListActionType.Error(it)) }).let { disposables.add(it) } } fun onScrollToLast() { if (reducer.isNextLoading.value) return dispatch(ListActionType.StartNextLoad()) repository.list(reducer.page + 1 ) .subscribe({ dispatch(ListActionType.SuccessNextLoad(it)) }, { dispatch(ListActionType.Error(it)) }).let { disposables.add(it) } } } Reducer Actionを購読し、Actionの種類に応じてStateを更新しています。ここではStateの型を、値の保持や通知が必要かどうかに応じて使い分けています。また、値を実際に保持・通知する変数と外部に公開する変数を分けることで、外部からのimmutable性を実現しています。 import io.reactivex.Observable import io.reactivex.subjects.PublishSubject class ListReducer(action: Observable<ListActionType>) : DisposableMapper() { init { action.ofType(ListActionType.StartInitialLoad:: class .java) .subscribe { mIsInitialLoading.value = true }.let { disposables.add(it) } action.ofType(ListActionType.SuccessInitialLoad:: class .java) .subscribe { mIsInitialLoading.value = false mElements.value = it.elements }.let { disposables.add(it) } action.ofType(ListActionType.StartNextLoad:: class .java) .subscribe { mIsNextLoading.value = true }.let { disposables.add(it) } action.ofType(ListActionType.SuccessNextLoad:: class .java) .subscribe { mIsNextLoading.value = false mElements.value += it.elements page++ }.let { disposables.add(it) } action.ofType(ListActionType.Error:: class .java) .subscribe { mError.onNext(it.error) }.let { disposables.add(it) } } var page: Int = 1 private set private val mElements: Variable<List<Element>> = Variable(listOf()) val elements: ImmutableVariable<List<Element>> get () = mElements private val mIsInitialLoading: Variable< Boolean > = Variable( false ) val isInitialLoading: ImmutableVariable< Boolean > get () = mIsInitialLoading private val mIsNextLoading: Variable< Boolean > = Variable( false ) val isNextLoading: ImmutableVariable< Boolean > get () = mIsNextLoading private val mError: PublishSubject<Throwable> = PublishSubject.create() val error: Observable<Throwable> get () = mError } Getter Reducerから受け取ったStateをView Propertyにマッピングしています。ここでのlistItemsはこのままRecyclerViewに渡され、DiffUtilにより更新される想定です。 import io.reactivex.Observable import io.reactivex.functions.BiFunction class ListGetter(reducer: ListReducer) : DisposableMapper() { val listItems: Observable<List<ListItemType>> = Observable.combineLatest( reducer.elements.observable, reducer.isNextLoading.observable, BiFunction { elements, isNextLoading -> var listItems: List<ListItemType> = elements.map { ListItemType.Data(it) } if (isNextLoading) { listItems += ListItemType.Loading() } listItems }) val isShownPageLoading = reducer.isInitialLoading val errorSnackbar = reducer.error } まとめ Fluxを導入してViewModelの処理を分割したメリットは想像以上でした。どこにどのような処理が書かれているかを把握しやすくなったので、コードの保守性がかなり向上しました。更に、複雑化したViewModelでは難しかったUI改善がFlux導入で可能になり、ユーザー体験も向上させることができました。 最後に VASILYではこだわりをもってプロダクトを開発したいエンジニアを募集しています! 興味のある方は次のリンクよりお申し込み下さい。
アバター
インフラエンジニアの光野(@kotatsu360)です。 今週のテックブログは豪華二本立てでお送りいたします。 一本目はバックエンドエンジニアの木曽による「 福利厚生を使ってAWSソリューションアーキテクト アソシエイトを取得しました 」でした。 二本目は UserData、OpsWorks、Lambdaを組み合わせ、常に新鮮なSpotFleetインスタンスでサービスを運用する という取り組みについて紹介させていただきます。 なお、本記事は先日開催された X-Tech JAWS 【第2回】~9割のX-Techと1割の優しさで切り拓く未来~ での登壇資料を補足するものです。適宜スライドを取り上げて説明をいたします。 キーワード:EC2 SpotFleet、AWS Lambda、UserData、Apache Mesos ネットワーク概要 弊社が運営する IQON は、 独自のクローラー を用いて契約済みECサイトから情報を収集、整理、掲載しています。 そのためIQONにおいてクローラーは非常に重要な存在です。 このクローラー、何度かの刷新を経て現在は次の構成を取っています。 アプリケーションはコンテナ化 計算資源は、Amazon EC2 SpotFleetによるクラスタをApache Mesosで抽象化 クロール状況に応じてコンテナ数が増減 全体図は以下のとおりです 1 。 これらは、大きく3つのフェーズで構築されました。 本記事で触れる範囲は Phase.3 です 2 。 Phase.3以前の悩み SpotFleetによる柔軟な構成を取ることでインスタンスを立てる手間は無くなりました。 しかし、1つだけ手元に残った作業があります。 custom AMIの更新作業 です。 Packer by Hashicorp でAMIの作成自体は自動化していましたが、AMI作成用のクレデンシャルや実行方法は属人化しています。これを解決することを目的としたのがPhase.3です。 UserDataとOpsWorksとLambda 2017年の中頃、構成管理にOpsWorksを導入していたということもあり 3 、SpotFleetインスタンスたちも同様に管理することにしました。 順を追って説明します。 1. インスタンスの起動とUserDataによるOpsWorksの呼び出し まず、SpotFleetインスタンスが起動します。SpotFleetの起動設定として次のUserDataが登録されています。 # SpotFleet インスタンスについての記述 # 記述... UserData : Fn::Base64 : !Sub - | #cloud-config repo_update : true repo_upgrade : security # RedHat系ならこっち apt_upgrade : true # Debian系ならこっち packages : - python-pip runcmd : - LC_ALL=C sudo pip install awscli - /usr/local/bin/aws opsworks --region us-east-1 register --infrastructure-class ec2 --stack-id ${OpsWorksStackId} --local --use-instance-profile 2>&1 | tee /tmp/register.log - export INSTANCE_ID=$(grep 'Instance ID' /tmp/register.log | grep -oE '[a-z0-9\-]+$' ) - while ! /usr/local/bin/aws opsworks --region us-east-1 assign-instance --instance-id $INSTANCE_ID --layer-ids ${OpsWorksLayerId1} ${OpsWorksLayerId2} ${OpsWorksLayerId3}; do echo 'wait...' ; sleep 20; done - OpsWorksStackId : !Ref OpsWorksStack OpsWorksLayerId1 : !Ref OpsWorksLayerCommon OpsWorksLayerId2 : !Ref OpsWorksLayerMesosBase OpsWorksLayerId3 : !Ref OpsWorksLayerMesosSlave # SpotFleet インスタンスについての記述 # 記述... これはCloudFormationテンプレートによるUserDataの設定です。依存するOpsWorksのID類が解決され、最終的にはBase64エンコードされた文字列が登録されます。 runcmd では、3つのタスクを行っています。 awscliのインストール OpsWorksへの登録(register) OpsWorksレイヤへの追加(assign) whileループはregisterの終了を待ってassignするための処理です 4 。 OpsWorksの呼び出しがUserDataの役割 で、これ以降は全てOpsWorks側で行います。 2. OpsWorksによる構成管理とMesosクラスタへの参加 OpsWorks側には Setup と Deploy の2つのタイミングでレシピが実行 されるように設定されています。 OpsWorksLayerMesosSlave : # レイヤについての記述 # 記述... CustomRecipes : Setup : - 'mesos::slave' Deploy : - 'mesos::attach' # レイヤについての記述 # 記述... mesos::slave はMesosエージェントのインストールなどの前準備、 mesos::attach がエージェントの再起動や最終的な設定変更を担当しています。 最終的な設定変更が終了するとMesosクラスタの一員として新しいタスクを処理できるようになります。 Chefはレシピの実行順を制御することができません 5 。その為、OpsWorksのライフサイクルを利用してSetup完了後(=環境構築後)のDeployイベントに最終的な設定を任せることにしています 6 。 余談ですが、昔、手でcustom AMIを作成していた時代には、 mesos::attach 相当の内容がUserDataとして書かれていました。UserDataからOpsWorksを呼び出すなら一緒にChefのレシピにした方が楽だという事でお引っ越しです。 閑話休題。 3. Lambdaによる状態監視とリトライ 2. OpsWorksによる構成管理とMesosクラスタへの参加 までが終わると、新しいSpotFleetインスタンスはMesosクラスタの一員としてタスクを処理します。 ここからは運用面のフォローについて補足します。 OpsWorksで構築を行っていると、時たまネットワーク的な不調で構成管理に失敗することがあります。リトライすると直ります。一時は手でフォローしていたのですが辛くなったので今は Lambdaに状態監視とリトライ を任せています。 CloudWatch Eventsで定期的にLambdaを発火、LambdaはOpsWorksに登録されているインスタンスを監視し、必要に応じて再setupのリクエストを投げます。 4. OpsWorksから削除 最後はOpsWorksからインスタンスを削除する部分です。OpsWorksは明示的にderegisterのリクエストを打たない限り、インスタンスを管理し続けようとします。 その為、SpotFleetインスタンスを落とす際には インスタンス自身がderegisterをリクエストする ようcronスクリプトで準備しています。 #!/bin/bash # terminate notificationを受け取ったときのスクリプト RES = $( curl -w ' %{http_code} ' -o /dev/null -s -- ' http://169.254.169.254/latest/meta-data/spot/termination-time ' ) if [ " ${RES} " = "404" ] ; then : else # SpotFleetインスタンスなので再起動することが無い。困ったら破棄。tmp以下のファイルで連携しても大して問題にならない export INSTANCE_ID= $( grep ' Instance ID ' /tmp/register.log | grep -oE ' [a-z0-9\-]+$ ' ) /usr/ local /bin/aws opsworks --region us-east -1 deregister-instance --instance-id $INSTANCE_ID 2 > & 1 | tee -a /tmp/register.log # mesosのエージェントを止めたり、監視とめたり、Dockerコンテナ止めたり、、、 # 後始末 fi 毎分メタデータから終了予告の有無をチェックし、必要であればderegisterします 7 。 1点注意があります。ユーザ操作でSpotFleetクラスタのサイズを変える場合は termination-time が設定されません。何らか手動で操作する場合は、このスクリプト相当の作業を手で行う必要があります。 日常的なSpotFleetクラスタのサイズ変更は、これまた別のLambdaがコントロールしています。そちらはLambda(lambda.amazonaws.com)による実行となるためか termination-time が設定されます。特に手を出すことはありません。 メリット・デメリット custom AMIを作成して利用するというやり方から、OpsWorksを利用するやり方への変遷についてご紹介しました。 改めてメリット・デメリットを整理します。 メリットは次の3つです。 事前にcustom AMIを作成する準備が不要 クレデンシャルや手順書を用意する必要がない 常にパッケージ類が最新 固定したいものがあれば、それもできる 同じ枠組みでサービスの全インスタンスが管理される EC2インスタンスとSpotFleetインスタンスを共通のレシピから構築 一方、毎回OpsWorksで管理させることのデメリットです。 Mesosクラスタに参加するまでのリードタイムが長い 10〜15分、custom AMI時代は3〜5分 たまに構築が失敗する Lambdaでフォロー インターネットに優しくない 毎回パッケージ類を取得するため 新鮮さと速度のトレードオフ 。 custom AMI作成の自動化を検討したこともありますが、作成自動化+SpotFleetクラスタの更新まで考えると最終的に今の形に落ち着きました。 まとめ 本記事では、UserDataとOpsWorks、Lambdaを使ったSpotFleetインスタンスの構成管理についてご紹介しました。新鮮なインスタンスが使えること、custom AMIの更新がなくなったこと、より健全な運用体制ができたと感じています。 さて、弊社のMesosクラスタに関して、これまでの取り組みを以てネットワーク構成面での不満は概ね解消されました。そのため次はMesos自体も含めた各種ソフトウェアのバージョンアップに取り組む予定です。いずれまたご紹介できればと考えています。 VASILYでは現状に満足せず、より健全な姿を目指して挑戦できる、挑戦が好きなエンジニアを募集しています。興味がある方は以下のリンクからお申し込み下さい。 この構成は、2018年3月で本番投入からちょうど1年になりました🎉 ↩ Phase.2については Docker / Apache Mesos / Marathon による3倍速いIQONクローラーの構築 にて紹介しています ↩ CloudFormationとOpsWorksでインフラを育てる ↩ なお、OpsWorksはwaitのAPIがあるため、 /usr/local/bin/aws opsworks --region us-east-1 wait instance-registered --instance-ids $INSTANCE_ID と書けば、whileで待つ必要はありません。最近、知りました。。 ↩ 大抵は書いた順ですが、include_resipeやnotifiesによる呼び出しを全て把握して書くのは煩雑です ↩ AWS OpsWorks Stacks のライフサイクルイベント ↩ temination-timeはdeprecatedらしく、instance-actionを使うように勧められています。 ↩
アバター
つい最近、中途入社しましたバックエンドチームの id:takanamito です。 今回は入社してすぐに会社の福利厚生をつかってAWSソリューションアーキテクト アソシエイトレベルを取得した話をご紹介します。 Amazon - Badge Verification - CertMetrics きっかけ 作戦 後に引けない状況をつくる どんな試験なのか知る どう勉強するかを考える 模試を受ける 試験当日 福利厚生 まとめ きっかけ 転職に伴いまとまった時間が確保できたことや、前職の同僚が同資格のプロフェッショナルを持っていて仕事でけっこう役立ってそうな感じでうらやましかったので取ろうと思いました。 過去に何度か時間ができたタイミングで趣味webサービスを作っていたんですが、途中で飽きたり運営がうまくいかなかったりといい結果が出ていませんでした。 そこで今回は路線変更して、資格という成果がわかりやすいものに挑戦してみることにしました。 作戦 せっかくなので、実際に資格を取るにあたってどのような方針で進めていったかを残します。 後に引けない状況をつくる まず試験に申し込むことにしました(16,200円) この時点で試験日が決まり、クレカ決済が発生します。 締め切りとお金の負担を発生させることで自分を追い込みます。 どんな試験なのか知る 次にどんな試験なのかを調べました。サンプル問題があるので見てみることに。 AWS 認定ソリューションアーキテクト – アソシエイト 選択問題で正しいものを選ばせる形式の問題ということがわかりました。 また詳細な試験要項を読むと出題分野と傾向を把握することもできます。 http://media.amazonwebservices.com/jp/certification/AWS_certified_SolutionArchitect_associate_blueprint_JP.pdf 高可用性、コスト効率、対障害性、スケーラブルなシステムの設計: 60% 実装/デプロイ: 10% データセキュリティ: 20% トラブルシューティング: 10% これらを調べた時点で、試験で問われる知識は クラウドデザインパターン(CDP)に出てきそうな構成 各種サービスの用途や仕組み 高可用性の実現 低コスト化 スケーラビリティ 各種サービスの細かな仕様 であることがわかりました。 どう勉強するかを考える どんな試験で何を問われるかだいたいわかったので、次はどう勉強していくかを考えます。 AWSが公式に紹介しているおすすめフローは以下 AWS 認定 – 認定の準備 トレーニングクラスの受講 試験学習ガイドを読む ホワイトペーパーを読む 試験準備ワークショップの受講 模試を受ける また過去の多くの受験者がブログなどで紹介していたのは以下の3つ 合格対策 AWS認定ソリューションアーキテクト - アソシエイト を読む WEB問題集 に課金 Black Beltの資料を読む AWS公式ドキュメントを読む これらの勉強法から自分にあったやり方や教材を選んでいくことに。 サンプル問題や出題傾向から以下の様なことを感じていました。 使ったことのないサービスについて問われるとキツイ 細かい知識について問われると覚えてない 逆にCDPで紹介されているような高可用性/スケーラビリティを実現する構成は知ってることが多かった 基本的には知識を貯め込んでいく勉強になるのですが、片っ端からドキュメントや資料を読んでいくには量が多く勉強自体を退屈に感じそうな予感がしました。 そこで公式資料をそのまま読み進めるのは避けて、問題を解きつつ 間違えたところについて公式ドキュメントやblack beltの資料を読んで知識を埋めていく やり方を採用し、以下の4つに教材を絞り込みました。 Web問題集に課金 Black Beltの資料を読む AWS公式ドキュメントを読む 模試を受ける ワークショップは、たまたま寒くてあまり出かけたくない日が続いてたのでやめました。 模試を受ける 1日1時間ほどの時間を取りながら2週間ほど問題集を解きました。 700問程度ある問題のうち2〜300問くらい解いたと思います。 知らなかった知識については、個人用の Kibela のブログに問題とその解説をしているドキュメントや資料のURLをセットでメモしました。 試験前に確認する自分専用の参考書にする狙いです。 また、このタイミングでどの程度学習が進んでいるかの確認も兼ねて模試を受けることにしました。 無事に75%の正答率で合格判定が出ました。 総合評点: 75% トピックレベルスコアリング: 1.0  Designing highly available, cost-efficient, fault-tolerant, scalable systems: 66% 2.0  Implementation/Deployment: 100% 3.0  Data Security: 75% 4.0  Troubleshooting: 100% しっかり勉強が成果に結びついていることを確認できたのでうれしかったのと、家にいながらブラウザで実際の試験と同じ形式の画面で問題を解くので本番当日に「どのような画面が出てくるのか」を知れたのが大きかったです。 この模試は問題をスクショで保存しておけるので、後日ゆっくり解き直すことができます。 試験当日 模試の翌週に本試験を受ける日程だったので、残った日は引き続き問題集を解いて知識の穴埋めを進め、使ったことのないサービスについてBlack Beltの資料を読む作業をしていました。 この辺の内容も全てKibelaにメモしています。 試験当日は出発前にKibelaのメモを確認してから出発しました。 試験結果は以下のとおりです。 総合評点: 85% トピックレベルスコアリング: 1.0  Designing highly available, cost-efficient, fault-tolerant, scalable systems: 84% 2.0  Implementation/Deployment: 66% 3.0  Data Security: 90% 4.0  Troubleshooting: 100% 模試のときより正答率を10%上げて合格することができました🎉 うれしい。 福利厚生 VASILYには福利厚生で資格試験の補助制度があり、今回の受験料も会社から全額補助を受けることができました💪 これがなければモンハンワールドを買うための家庭内稟議が通らない可能性もあったので激アツ補助でした。 ソリューションアーキテクトのプロフェッショナルは受験料が32,400円とアソシエイトに比べて高額なので、プロフェッショナル受験も目指す私としては非常にありがたい制度です。 まとめ 実際に稼働していた期間は2〜3週間ほどなんですが、まず最初に申し込んでしまい自分を追い込んでから勉強にとりかかるやり方は、怠惰な自分にとっては非常によい方法でした。 また会社の補助があることで、資格取得のハードルが下がり勉強に取り組みやすかったです。 今後はプロフェッショナルレベルの取得に向けて勉強していきますが、次回もこの制度を利用して合格を目指そうと思います。 VASILYではソリューションアーキテクトを募集しています。 興味がある方は以下のリンクからお申し込みください。
アバター
こんにちは、フロントエンドエンジニアの権守です。 プログラミングをしていると頭を悩まされるものの1つにエラーハンドリングがあると思います。シンプルにできた実装に手を入れる必要が生じるなど、正直目を逸らしたくなることもあります。ですが、Androidアプリ開発を通して改めて考える機会があったので、そこで得られた知見を紹介しようと思います。 本記事では特に、エラー時にユーザーへどのようなフィードバックを返すかという点に注目して、それについての考えと取り入れている仕組みを紹介します。 Androidにおけるフィードバックのデザイン まず、エラー時のフィードバックにおいて、どのようなコンポーネントを使い分けているかについて説明します。 VASILYではAndroidアプリを開発する際には Material Design Guidelines にできるだけ準拠するように心がけています。エラー時のフィードバックについても例外ではありません。ガイドラインでは、簡潔なフィードバックを表示する際にはSnackbarを使うように書かれています。そのため、エラーメッセージは基本的にSnackbarを用いて表示しています。一方で、システムからのメッセージはToastを使うように書かれているので、カメラやストレージへのアクセス権限がないときのエラーはToastを用いて表示するようにしています。 また、Activityの初期化時にエラーが起こった場合には、エラー表示用のレイアウトに切り替えて再読込を促すようにしています。 APIリクエスト時におけるエラーのフィードバック 表示するデータの多くをWeb APIを利用して取得するアプリでは、APIリクエスト時のエラーをどのように扱うかが特に重要です。そのため、ここではAPIリクエスト時のエラーに着目して、どのような問題があるかとそれを解決するために導入した仕組みについて説明します。 エラーメッセージの管理 同じサービスをiOSとAndroidなど複数のプラットフォームで開発する際に、エラーメッセージをどのように管理するかは悩ましい問題の1つです。よくある解決法としてはAPI側とアプリ側で共通のエラーコードを持ち、それを元にアプリ側でエラーメッセージの出し分けをするものがあります。 一方、VASILYではAPIに起因するエラーメッセージはAPI側で管理するようにしています。具体的には、更新リクエストやエラー時のレスポンスのJSONには、フィードバックに利用するメッセージを載せるようにしています。 { " message ":" プロフィール画像を保存しました ", " code ": 200 } { " message ":" プロフィール画像の保存に失敗しました ", " code ": 400 } これによりAPI側に起因するエラー時にはプラットフォームをまたいで同じエラーメッセージを表示することができます。 アプリ実装時の問題点 しかし、このアプローチが有効なのは、あくまでAPIのアプリケーションサーバー(Rails等)でのエラー発生時に限ります。なぜなら、アプリからAPIへリクエストを行った場合、アプリケーションサーバーへ到達する間に様々なエラーが起こり得るからです。そして、それらはもちろん上記のJSONフォーマットに沿ったものではありません。 例えば、Webサーバーの不調により、アプリケーションサーバーまで到達せずにエラーステータスのみが返ってくることが考えられます。他にも、端末がそもそもネットワークに接続されていない場合には、OS側からのExceptionとして返ってきます。 これらの様なケースを考慮せずに実装してしまうと、ライブラリやミドルウェアなどが返したエラーメッセージをそのままユーザーに表示してしまうといった事態に陥ります。その結果、ユーザに不信感を与えるだけでなくセキュリティ的に問題を起こす可能性もあります。 ErrorFeedbackクラスの導入 そこで、ErrorFeedbackという以下に示すようなクラスを実装し、エラーメッセージを扱いやすくしました。 *サンプルプロジェクト全体は こちら から見れます。 実装はシンプルで、受け取ったThrowableを元に対応するErrorFeedbackを継承したクラスのオブジェクトを返すというものです。 サンプルでは対応するExceptionを少ししか書いていませんが、必要に応じて追加していただければ様々なライブラリに対応することもそれほど難しくないと思います。また、パースするJSONのフォーマットを別のクラスに変えることで、他のAPIに対応させることもできます。 Snackbarを表示するためのExtensionの導入 引数として直接文字列やリソースIDを渡すのではなく、ErrorFeedbackオブジェクトを渡すことでSnackbarを表示できるExtensionを実装しました。これにより、アプリケーション全体で一貫してユーザーフレンドリーなエラーメッセージを表示できるようになりました。 fun Activity.snackbar(errorFeedback: ErrorFeedback, duration: Int = Snackbar.LENGTH_LONG): Snackbar = Snackbar.make(findViewById(android.R.id.content), errorFeedback.getMessage( this ), duration) まとめ Androidアプリ開発におけるエラー時のフィードバックをどのように扱っているかを紹介しました。普段、エラー時の表示まであまり気を配れていない方もこれを機に改めて考えてみてください。 最後に VASILYでは、細部までこだわりを持って実装できるエンジニアを募集しています。少しでも興味がある方は以下のリンクからお申し込みください。
アバター
こんにちは、インターンの@takewakaです。 もう春休みなのでほぼ毎日出勤してカスタマーサポートのAPIとUIを作る毎日です。 私は現在大学4年で4月からVASILYに就職する予定です。VASILYでのインターンは来月で丁度1年となります。 私は就職活動を始める前からwebエンジニアとして働く希望があったので、一般的な就職活動はあまりせず実務型インターンを通じてwebエンジニアとしてのスキルと経験を培うことに専念しました。 (ここで言う一般的な就職活動とは、『自己分析&企業研究→志望動機&自己PR作成→エントリー→面接→内定』の一連の流れを指します) 今振り返るとこのような形で就職活動をしたことは、自分にとって素晴らしい選択であったと自信を持って言えます。 本記事では、私がこのような就職活動を経た理由について書きます。 実務型インターンを主軸として就職活動した理由 得られるものの違い エントリーシートを無数に提出し、面接を複数受け精神的にも肉体的にも消耗してしまっている人を見かけたことが何回かあります。仮に落ちたとして得られるものは、面接に受かるコツくらいのものな気がして、あまり価値を見い出せませんでした。 インターンとして働けば、実務で必要とされる能力が得られます。 ここでいう実務で必要とされる能力とは、大規模なアプリケーションが持つ問題を解決する能力、複数人で効率よく開発するためのツールを使う能力、仕事を効率よく完了させるためのコミュニケーション能力などです。 また実力がつけば、効率よく人脈やお金も得ることができます。私はインターンのおかげでお金には困らなくなりましたし、これまでの人生では決して出会うことのなかった人達と出会うことができています。 本当にその職種に就きたいか判断する大きな手がかり 実際に就業するのだから、自分の職業適性を判断する上でこれ以上の判断材料はないだろうという考え方です。 理論と実践の間には少なからずギャップが存在していて、実際に就業してからでないとわからないことがたくさんあります。私にも実際に仕事をしてから気づいたことがたくさんありました。 仕事をする前は、エンジニアの仕事に関してものを作るということ以上のことはわかりませんでした。実際に仕事をしてみて、エンジニアはものを作るだけでなくユーザーに提供できる価値を認識し、提供する価値を高めるための改修を捻出し、それらを定期的にできるように常に将来を見据えた設計&実装をすることを知りました。私が思っていたよりずっと頭を使う、創造性が試される仕事だと気づきました。このことに気づくことができたのは、実際に運用されているアプリケーションの開発を任せてくれるVASILYのインターンのおかげです。 このように自分の職業適性を判断する上で、インターンで実務をこなすことで得た発見を考慮に入れることができるのは、判断の正確性を大きく高めることに繋がります。 また一旦就職をしてしまうと、仕事に向いていないことがわかってもなかなか辞めづらいと思います。インターンの時点でそこの判断ができてしまえば、軌道修正が比較的しやすいはずです。それどころかインターンで得た経験がなければ考えることもできなかった判断が下せるかもしれません。 そこで働く人を知れる 『仕事というものは何をやるかよりも誰とやるかだ』と考える人もいるように、一緒に仕事をする仲間がとても重要なのは言うまでもないと思います。 実務型インターンの利点として、就職した際仕事を共にする人々をよく知れる、ということがあります。 就職を決める前にそこで働く人々のことをよく知ることで、1つの不安要素を消し去ることができるのは大きなメリットだと思います。 インターンを通じて社内の色々な人と関わったおかげで、私にはそこに対する不安はありません。 また仕事上での触れ合いを通じて、就職した後自分がどうチームに貢献できるかを明確に描くこともできます。 真の意味での安定性 就職にあたって誰もが気にすることの1つに、収入の安定性があると思います。私も就職活動をする前は職を得られない心配をしました。 私はインターンとして一線で活躍するエンジニア達の横で仕事をしてきた上で、一つ確信したことがあります。 それは『安定することとは技術を持つこと』だということです。 滑り止めの内定を確保するために使う時間を、実務に当ててスキルと経験を得る方がセーフティネットとしての効果が高いと思います。 また社会で生き抜くために必要なのは実力、エンジニアの場合技術だと思うので、先回りするように実務経験を得ることは長期的に見てもメリットだと思います。 まとめ ここまで私が実務型インターンを中心に就職活動をした理由、及びその利点について説明してきました。 私はインターンのおかげでエンジニアとしての実務経験を得ながら、就職したい会社に就職することができました。 その過程で一人の力では得られなかったであろうスキルや人脈を得ることもできました。 今振り返っても、重要なタスクを振り当て本番デプロイまで任せてくれるVASILYのインターンは、エンジニアとして成長するのに最適なインターンだと思います。 本記事がこれから就職活動を控えている学生の皆さんのお役に立てば幸いです。 現在VASILYではインターンの募集を行っています。 この記事を読んで興味を持った方は以下のリンクからご応募ください。
アバター
こんにちは。 データチームの後藤です。 A/Bテストはサービス改善のための施策の効果測定に欠かせないツールですが、最近のVASILYでは、運用するサービスが増えてきたことに伴いA/Bテストの内容も多様化してきました。今回はそのA/Bテストにベイズ推論を用いた具体的な例を紹介します。 問題設定 あるサービスのコンバージョン率を上げるため、コンバージョンの前提となる行動Xを増やすための修正を実施しました。ここで、「コンバージョン」は商品の購入などの成約を、「コンバージョン率」は利用数に対して成約に結びついた割合を、「行動X」は買い物カゴに商品を入れるなどコンバージョンの前提となる行動のことを指すことにします。修正がコンバージョン率の上昇に寄与したのかをデータから判断する必要があります。 修正後のページ(パターンA)を表示したグループと、修正前のページ(パターンB)を表示したグループの行動ログを集計し、以下の3つの仮説を検証します。各パターンでは、表示したページ以外の条件は同じであると仮定します。 仮説 パターンAはパターンBより行動Xを取りやすい パターンAはパターンBよりコンバージョン率が高い パターンAはパターンBよりコンバージョンに至るまでの時間が短い これらの仮説を検証する際、通常は平均値の差の検定や母比率の差の検定などの仮説検定が用いられます。今回は、ベイズ推論のフレームワークを利用しビジネスサイドが状況を判断しやすい結論を出していきます。 ベイズ推論を使うメリット ベイズ推論を利用した場合、従来の仮説検定と比べて以下のメリットが考えられます。 前提知識や常識を事前分布として反映できる 推定値の分布に正規性を仮定していないので、より柔軟な区間推定ができる データの生成過程をモデリングすることにより、得られた結果からストーリーを語ることができる PyMC3とは PyMC3はPythonでベイズ推論を実行できるフレームワークです。 http://docs.pymc.io/index.html 公式ドキュメントのExampleをみるとわかるように、様々な機能が実装されています。今回のようなA/Bテストに用いるだけでなく、GLMやMixture Modelの推論など様々な用途に利用できます。今回はPyMC3の生成モデルの定義とMCMCによるサンプリングの機能を利用します。 仮説1: パターンAはパターンBより行動Xを取りやすい 現実的には、ユーザーはサービスの利用開始から様々な過程を通じて行動Xに至る(あるいは、至らない)と考えられます。問題を単純にするために、それらを一旦無視して、利用開始から24時間以内に行動Xを1回でも取ったかどうかを焦点とします。 行動Xを取ったユーザーを1、行動Xを取らなかったユーザーを0と数値化することにより、これらのサンプルが確率 のベルヌーイ分布から生成されると考えます。また に関しての情報はないので、事前分布として一様分布を採用します。 データの集計結果は以下のようになりました。 - グループA - サンプル数: 1602 - 行動Xを取った数: 740 - 平均: 0.462 - グループB - サンプル数: 2121 - 行動Xを取った数: 933 - 平均: 0.440 グループAのほうが行動Xを取った割合が高いので、修正の効果が出ているかもしれません。 PyMC3のフレームワークを使えば、今回のモデルを以下のように記述できます。行動Xを取る確率が増えたかどうかも知りたいので、 という と の差を新たな生成量として定義します。 モデル import pymc3 as pm with pm.Model() as model: # 事前分布として一様分布を採用 p_A = pm.Uniform( 'p_A' , 0 , 1.0 ) p_B= pm.Uniform( 'p_B' , 0 , 1.0 ) # 各行動はベルヌーイ分布から生成される obs_A = pm.Bernoulli( "obs_A" , p_A, observed=sample_A) obs_B = pm.Bernoulli( "obs_B" , p_B, observed=sample_B) # 新たな生成量として行動を起こす確率の差を定義 delta_prob = pm.Deterministic( 'delta_prob' , p_B - p_A) 推論 with model: start = pm.find_MAP(fmin=optimize.fmin_powell) step = pm.Slice() trace = pm.sample( 20000 , step=step, start=start) _ = pm.traceplot(trace) _ = pm.plot_posterior(trace) 、 とその差 の標本は均衡分布に収束していることが確認できます。各変数の分布も左右対称のベル型をとっていることもわかります。 pm.summary(trace) mean sd mc_error hpd_2.5 hpd_97.5 n_eff Rhat p_A 0.461861 0.012475 0.000059 0.437458 0.485777 39737.0 0.999980 p_B 0.439937 0.010784 0.000055 0.418653 0.460909 37254.0 1.000117 delta_prob 0.021924 0.016421 0.000081 -0.009180 0.055029 38586.0 1.000014 解釈 実際に となるサンプルの数を数えて、Aのほうが行動Xを取りやすいと言える確率を算出します。サンプリングした 、 を用いて、 が成り立つ場合に1を、成り立たない場合に0をとる変数 を定義し平均を取ります。 (trace[ 'p_A' ] - trace[ 'p_B' ] > 0 ).mean() 0.90927500000000006 グループAのほうがグループBに比べて行動Xを取りやすいと言える確率は約91%で、高い確率で修正の効果があったと言えそうです。 と の平均値は約2%ほど差があるので、修正の結果行動Xをとるユーザーを2%ほど増やせた可能性が高いです。 仮説2: パターンAはパターンBよりコンバージョン率が高い 仮説1ではコンバージョンの前提となる行動Xをとるユーザーの割合について扱いましたが、コンバージョン率についても同様の問題設定で対応することができます。 モデル with pm.Model() as model: # 事前分布として一様分布を採用 p_A = pm.Uniform( 'p_A' , 0 , 1.0 ) p_B= pm.Uniform( 'p_B' , 0 , 1.0 ) # 各行動はベルヌーイ分布から生成される obs_A = pm.Bernoulli( "obs_A" , p_A, observed=sample_A) obs_B = pm.Bernoulli( "obs_B" , p_B, observed=sample_B) # 新たな生成量として行動を起こす確率の差を定義 delta_prob = pm.Deterministic( 'delta_prob' , p_A-p_B) 推論 with model: start = pm.find_MAP(fmin=optimize.fmin_powell) step = pm.Slice() trace = pm.sample( 20000 , step=step, start=start) _ = pm.traceplot(trace) _ = pm.plot_posterior(trace) pm.summary(trace) mean sd mc_error hpd_2.5 hpd_97.5 n_eff Rhat p_A 0.114902 0.004827 0.000025 0.105367 0.124348 39673.0 0.999975 p_B 0.106540 0.007789 0.000036 0.091163 0.121687 38234.0 0.999989 delta_prob 0.008362 0.009185 0.000043 -0.009795 0.026113 38091.0 0.999987 (trace[ 'p_A' ] - trace[ 'p_B' ] > 0 ).mean() 0.82094999999999996 解釈 パターンAがパターンBより優れている確率の推定値は約82%となりました。パターンAが優れていると完全に言い切るのは難しい結果になりました。一方で、パターンAはパターンBと比べて極端に劣る可能性は低いと考えられます。仮説1での主張と合わせることで、今後はパターンAを採用するという判断ができるかもしれません。 別解 サンプル数を 、コンバージョン数を とすると、Xは二項分布に従います。事前分布に二項分布の共役事前分布であるベータ分布を採用すると、事後分布が解析的に求まるため、MCMCを使うことなくサンプルすることができます。事前分布に 、 のベータ分布(一様分布)を用いると、事後分布は以下の式に従います。 from scipy.stats import beta visitors_to_A = 4297 conversion_from_A = 397 visitors_to_B = 1584 conversion_from_B = 156 alpha_prior = 1 beta_prior = 1 posterior_A = beta(alpha_prior + conversion_from_A, beta_prior + visitors_to_A - conversion_from_A) posterior_B = beta(alpha_prior + conversion_from_B, beta_prior + visitors_to_B - conversion_from_B) samples = 20000 samples_posterior_A = posterior_A.rvs(samples) samples_posterior_B = posterior_B.rvs(samples) prob = (samples_posterior_A < samples_posterior_B).mean() 仮説3: パターンAはパターンBよりコンバージョンに至るまでの時間が短い 長期間コンバージョンに至らないユーザーをコンバージョンするまで追い続けるのは現実的でないです。そのため、データを用意する際に何らかの工夫が必要になります。ここでは、M日以内にコンバージョンしない場合はデータセットから除外するという処理を行いました。この制限を設けたことにより、データの分布はM日目より先が打ち切られた若干歪な分布を持つことになります。 今回はM=9としてデータセットを作成しました。頻度の低い現象が発生するまでにかかる時間間隔の分布を取り扱っているため、指数分布に従っていると考えられます。まずは分布を確認します。 コンバージョンに至るまでの時間の分布(グループA) コンバージョンに至るまでの時間の分布(グループB) sample_A_size: 146 sample_B_size: 391 mean_A: 1.87 mean_B: 1.78 まずはこのデータが指数分布から生成されたと仮定して、ベイズ推論を行います。指数分布のパラメータ の事前分布は一様分布を採用しています。 モデル with pm.Model() as model: # Priors for unknown model parameters lambda_A = pm.Uniform( 'lambda_A' , 0 , 5.0 ) lambda_B= pm.Uniform( 'lambda_B' , 0 , 5.0 ) obs_A = pm.Exponential( "obs_A" , lambda_A, observed=sample_A) obs_B = pm.Exponential( "obs_B" , lambda_B, observed=sample_B) ave_A = pm.Deterministic( 'ave_A' , 1 /lambda_A) ave_B = pm.Deterministic( 'ave_B' , 1 /lambda_B) delta = pm.Deterministic( 'delta' , ave_A - ave_B) 推論 with model: start = pm.find_MAP(fmin=optimize.fmin_powell) step = pm.Slice() trace = pm.sample( 20000 , step=step, start=start) _ = pm.traceplot(trace) 結果 mean sd mc_error hpd_2.5 hpd_97.5 n_eff Rhat lambda_A 0.536024 0.043165 0.000222 0.450473 0.619218 40000.0 0.999990 lambda_B 0.583912 0.041367 0.000202 0.504196 0.666190 38614.0 0.999978 ave_A 1.877783 0.152392 0.000807 1.589442 2.181460 39591.0 0.999995 ave_B 1.721235 0.122699 0.000598 1.483570 1.961930 38377.0 0.999986 delta 0.156548 0.196036 0.001019 -0.213939 0.555821 39552.0 1.000006 ave_Aとave_Bのmeanと実際のデータの平均が一致していており一見良い推定結果が得られたように見えますが、モデルにデータの打ち切りの過程を含められていないため、推定されるパラメータにバイアスがかかっていると考えられます。 そこで推定する分布関数そのものをデータに合わせて定義し直してみます。具体的には指数分布の9日以降の部分を切ったTruncatedExponentialを定義します。 TruncatedExponential class from pymc3.distributions.dist_math import bound from pymc3.distributions import draw_values, generate_samples import theano.tensor as tt import scipy.stats.distributions class TruncatedExponential (pm.Continuous): def __init__ (self, lambda_, *args, **kwargs): super ().__init__(*args, **kwargs) self.mode = tt.minimum(tt.floor(lambda_).astype( 'float32' ), 1 ) self.lambda_ = lambda_ = tt.as_tensor_variable(lambda_) def te_cdf (self, lambda_, size= None ): lambda_ = np.asarray(lambda_) dist = scipy.stats.distributions.expon() lower_cdf = dist.cdf( 0 ) upper_cdf = 1 nrm = upper_cdf - lower_cdf sample = np.random.random(size=size) * nrm + lower_cdf return dist.ppf(sample) def random (self, point= None , size= None ): lambda_ = draw_values([self.lambda_], point=point) return generate_samples(self.te_cdf, lambda_, dist_shape=self.shape, size=size) def logp (self, value): lambda_ = self.lambda_ p = pm.math.log(lambda_) - lambda_ * value - pm.math.log( 1 - pm.math.exp(- 9 *lambda_)) log_prob = bound( p, lambda_ >= 0 , value >= 0 ) # Return zero when mu and value are both zero return tt.switch( 1 * tt.eq(lambda_, 0 ) * tt.eq(value, 0 ), 0 , log_prob) 推論 with pm.Model() as model: # Priors for unknown model parameters lambda_A = pm.Uniform( 'lambda_A' , 0.0 , 5.0 ) lambda_B= pm.Uniform( 'lambda_B' , 0.0 , 5.0 ) TE( 'obs_A' , lambda_=lambda_A, observed=sample_A) TE( 'obs_B' , lambda_=lambda_B, observed=sample_B) ave_A = pm.Deterministic( 'ave_A' , 1 /lambda_A) ave_B = pm.Deterministic( 'ave_B' , 1 /lambda_B) delta = pm.Deterministic( 'delta' , ava_A - ave_B) start = pm.find_MAP(fmin=optimize.fmin_powell) step = pm.Slice() trace = pm.sample( 20000 , step=step, start=start) 結果 mean sd mc_error hpd_2.5 hpd_97.5 n_eff Rhat lambda_A 0.314618 0.060387 0.000309 0.194953 0.431916 37317.0 1.000036 lambda_B 0.364973 0.038134 0.000196 0.288430 0.438147 40000.0 1.000004 ave_A 3.310105 0.724105 0.003919 2.142754 4.715704 30976.0 1.000038 ave_B 2.770713 0.298075 0.001547 2.207913 3.351493 37732.0 1.000012 delta 0.539392 0.782097 0.004153 -0.799306 2.125230 31161.0 1.000069 指数分布を仮定した場合に比べて、推定された平均値が高くなっていることがわかります。データ取得の事情を反映したTruncatedExponentialを使って指数分布のパラメータ を推定したことによる効果だと考えられます。 と の分布は、右側に伸びた左右非対称なものとなっています。9日以降のデータが無いために平均値の高い側が定まりにくいという事情を反映していると考えられます。 コンバージョンまでにかかる平均時間のグループ間の差は0を中心に分布していることがわかります。この結果から、コンバージョンに至るまでの時間短縮への寄与があるとは言えないと判断できます。 まとめ PyMC3を使って、ベイズ推論によるA/Bテストを行いました。今回のデータセットを分析した結果、修正により行動Xは増加しており、コンバージョン率は上がる可能性が高いが、コンバージョンに至るまでの時間の短縮には繋がっているとは言えない、と判断できました。 データの生成過程をより単純なものに置き換えたり、分布関数を定義し直すなどの工夫を取り入れ、各命題に直感的な解釈を与えることができたかと思います。 最後に 弊社では、画像データや行動ログなど多様なデータを扱い、ビジネスサイドの意思決定を支える分析を行います。 多様なデータの分布を捉え、収益へ貢献できるデータサイエンティストを募集しています。 https://www.wantedly.com/projects/109351 www.wantedly.com
アバター
こんにちは、VASILYでエンジニアインターンをしている劉です。 僕は昨年の四月、大学一年生になりました。その後、自分でプログラミングを学ぼうとしましたが行き詰まり、六月に思い切ってVASILYのインターンシップに応募しました。それから半年以上働いてみて、あの時思い切って応募してよかったと思うことが多くありました。今日はそのいくつかを紹介したいと思います。 エンジニアインターンシップのいいところ 1. コードが綺麗になる 一人でプログラミングの勉強をしていると、とりあえずプログラムが動けばいい、という考えに陥りがちです。その結果、そのコードをどのように書くか、という部分まで気が回らないことも多々あります。 しかしプログラムというのは面白いもので、同じ動作をするものでもコードの書き方は一通りになりません。人の数だけコードがあると言っていいでしょう。そしてコードの書き方をちょっと変えるだけで、ちょっと見たときに意味がわかりやすくなったり、後から変更を加えやすくなったりします。こういう書き方を積み重ねていくと、プログラムを作るスピードはどんどん上がっていき、作る作業もどんどん楽しくなっていきます。 エンジニアインターンでは、コードを書くたびにメンターとなった社員の方がレビューをしてくれます。ちゃんと動くかだけでなく、この書き方で本当によいのかまでしっかり見て指導を行ってくれます。インターンを続けていくことで、自分でもわかるほどはっきりとコードが綺麗になっていくのを感じるはずです。 VASILYに来る前の僕は、 典型的な「動けばいいや」の考え方の人でした。口で説明するよりも、その時期に僕が書いたコードの一部を見ていただいた方がよくわかると思います。 class WifiSpot < ApplicationRecord reverse_geocoded_by :latitude , :longitude def self . return_jsons (validator) if validator.range.present? @range = validator.range.to_f/ 1000 else @range = 0.5 end if validator.refining.present? @refining = validator.refining.to_i else @refining = 5 end if validator.address.present? @origin = validator.address else @origin = [validator.latitude,validator.longitude] end @language = validator.language @wifi_spots = self .near( @origin , @range ).limit( @refining ) jarray = [] for i in 0 .. @wifi_spots .length- 1 do jarray.push( @wifi_spots [i].response_format( @origin , @language )) end return jarray end def response_format (origin,language) @distance = ( self .distance_from(origin)* 1000 ).to_i if language.eql?( " english " ) @address = self .english_address @name = self .english_name else @address = self .japanese_address @name = self .japanese_name end wifi_spot = { " name " : @name , " distance " : @distance , " address " : @address } return wifi_spot end end 謎の空行がたくさん入っていたり、コードを書き始める位置がグラグラしていたりしてしまっています。あまりプログラミングをよく知らない人でも、なんとなくこのコードが「汚い」ことはおわかりいただけると思います。ちなみに、このコードが入ったファイルはこんな調子で595行も続いていました。今考えると恐ろしいです。 そんな僕が最近個人的に書いたコードがこちらです。 class OrderedItem < ApplicationRecord belongs_to :item belongs_to :order validates :number , numericality : { greater_than : 0 } def self . make_models_from_hashes (hashes) return [] if hashes.blank? hashes.map do | hash | new_hash = {} item = Item .find(hash[ ' item_id ' ]) new_hash[ :item ] = item new_hash[ :shop ] = item.shop new_hash[ :number ] = hash[ ' number ' ] new_hash end end def self . calculate_sum (model_hashes) model_hashes.sum {| hash | hash[ :item ].price * hash[ :number ]} end def session_hash { ' number ' => number, ' item_id ' => item_id} end def valid_cart? temp_order = Order .new self .order = temp_order valid? end def cancel update( canceled : true ) if order.ordered_items.all?{| item | item.canceled} order.update( deliveryman_done : true , user_done : true ) end end def cancellable? (user) return false unless order.deliveryman == user return false if canceled return false if order.he_done?(user) true end end 行なっている処理の違いもありますが、前のコードよりもなんとなくスッキリした印象が感じられるかと思います。自分で言うのも何ですが、改善した点をいくつかあげておきます。 ブロックを作るときにしっかりインデントをするようになった インデントの幅が統一された メソッドごとに空行を入れたことでコードがみやすくなった 手続き的にだらだら続く処理を別メソッドに切り出した 意味もなく他のコードに書いてある記法を続けなくなった(ここではインスタンス変数を作る"@") Rubyの組み込み関数を使って処理を行うようになった これらは、多くの人にとっては当たり前のことかもしれません。しかし、その当たり前のことを当たり前にできるようになったのは、インターンのおかげに他なりません。少しでも自分のコードが「綺麗なコード」に近づけたことを、とても嬉しく思います。 2. 何を勉強すればいいかがわかる 現在の世の中には情報が溢れています。それはプログラミングの世界でも同様で、初心者向けか上級者向けかに関わらず、世界中の人が新しい技術を学ぶための情報を日々発信し続けています。 そんな世界で勉強を進めようとすると、自分の現在のレベルに適した情報を見つけられないことが多々あります。新しいことを学ぼうとPCを開いたのに、自分には全く理解できない情報かわかりきった情報しか見つからずそっとPCを閉じる、そんな経験をしたことがある人も多いはずです。 エンジニアインターンをしていればそんなことはありません。一緒に働く社員の皆さんは確かなスキルを持った一流のエンジニアですし、一緒に働く中でインターン生のスキルも正確に把握してくれます。今足りないスキルから、それを得るための良い本やWebページまで、自分で学んでいくための十分なアドバイスがもらえるはずです。 冒頭にお話しした通り、インターンに来る以前の僕は完全にプログラミングの学習に行き詰まっていました。当時利用していたWebサービスで一通りプログラミングの入門コースを終わらせて、いざ自分で新しいことを学ぼうとすると、見つかる情報は初心者の僕には訳のわからないものばかりでした。何かを学びたいのに、適切な難易度のものが見つからなくてできない、それを歯がゆく思う日々が続きました。 そこでインターンシップに応募、合格すると、即座に綺麗なコードを書くための指針となる本を紹介されました。その後も、プログラムの設計についての本や絶対に知っておきたい基礎技術を解説した本やWebページなど、様々なものを紹介していただきました。それらを読み吸収した結果、最近はインターンに来る前とは比べ物にならないほど自分に知識がついたのを実感できます。それを実感するたびに、インターンに来てよかったと僕は思うのです。 3. 個人では触れることのない技術に触れられる より良いプログラムを作るために、今でも世界中で様々な新技術が生み出されています。中には仕事でプログラムを作るのにはほとんど必須と言っていいものも多々あります。しかしそのほとんどは、個人で勉強していると触れることのないものです。 例えば有償のサービスには手が出しづらいでしょうし、複数人で開発をしないと恩恵を受けにくい技術もあります。使うための初期設定を面倒に感じ使うのをやめてしまうこともあるでしょう。しかし、そういった理由で世の中で広く使われている技術に一切触れないのは非常にもったいないことです。 エンジニアインターンで働く場所は、実際に世の中でサービスを動かしている企業です。当然様々な技術が現場で使われていて、インターン生もそれに触れることができます。もちろんそれらにお金を払うのは企業なので、金銭的な心配はいりません。複数人開発のための技術の恩恵も最大限に享受することができますし、面倒な初期設定を自分でする必要もありません。様々な技術に触れるのに理想的な環境だと言えるでしょう。 僕は初めてインターンにやって来たとき、いまや全エンジニアに必須とも言えるバージョン管理システムであるGitすら満足に扱うことができませんでした。しかしインターン生として働く中で、Gitはもちろん、その他にもAWSやGCPの各サービスや種々のCIツールなど様々な技術に触れて来ました。おそらくあのまま個人で勉強していたら、これらの技術には触れることがなかったでしょう。貴重な経験ができていることに感謝しています。 4. 目標を高く保てる 一人でプログラムを書いていると、現在自分にどの程度の能力があるかを感じることはほとんどありません。特に、周囲の友人に同じようなことをしている人がいないとき、その傾向は顕著になります。こうした状況にあると、ちょっと動くプログラムを作れただけで、そんな自分に満足してしまいます。しかし、現状に満足してしまえば、その人はほとんど成長しなくなります。 エンジニアインターンでは、日々世の中に送り出すサービスを作り続けるプロの社員の方々と働くことになります。綺麗なコードを非常に素早く書ける上、様々な分野に深い知識を持つ彼らに、毎日驚かされることになると思います。 また、インターンを募集している会社には何人ものインターン生がやって来ます。自分から進んでインターンに応募する彼らはやはり意欲、能力共に高いことが多く、同世代であっても見習うべきところが多くあります。現状に満足しきっていた人でも、こうした目標とすべき人々に囲まれた環境では、気を引き締め直すことができるでしょう。 僕の周囲には、プログラムを書くような友人はほとんどいませんでした。そのせいか、「プログラミングできるんだ。すごいね」と言われることも多々あり、自分は本当にすごいんじゃないかと思ってしまうこともありました。 そんな中インターンにやって来て、僕は社員の方々と自分のレベルの差に愕然としました。緩みかけていた気持ちを引き締め直して、頑張って勉強していこうという気持ちになりました。 また、半年以上の間に多くのインターン生たちと知り合いました。彼らは例外なく皆優秀で、話すたびに自分の至らなさを痛感させられます。現在一緒に働いている人の中にも、自分よりもはるかにできる高校生がいたりして、刺激を受ける日々が続いています。 自分はまだまだ半人前ですが、少しでもレベルアップできるようにこれからも頑張りたい、そう思えるのはこの環境に身を置いたからだと思います。 5. お金がもらえる 友達とご飯に行ったり、服を買ったり、旅行に行ったりと、学生生活には何かとお金がいります。なんとかして自力でお金を工面しなければなりません。 しかし、お金を得るために働くと、それだけ技術的な勉強ができる時間も減ってしまいます。勉強はしたいけどお金も欲しい、どちらも叶える道はないものかと考えている人もいるでしょう。 エンジニアインターンではそれが叶います。インターンでは給料がもらえるので、インターン生として様々なことを学んでいると、いつのまにか口座に十分なお金が溜まっているのです。こんなに嬉しいことはありません。 僕は大学に入学した頃、ほとんど働いていませんでした。そうすると、新歓期のドタバタで入学祝いに貰ったお小遣いもすぐに消えて行き、昼食に五百円払うのすら躊躇する状態が続きました。仕方なくPCに触れる時間を減らして始めたアルバイトは単純作業の繰り返しで、そのつまらなさが苦痛で全く続きませんでした。 インターンでできる仕事は楽しく、前のアルバイトで感じたような苦痛は一切ありません。その調子で続けているとお金はどんどん溜まっていき、今ではサッと昼食で五百円以上を出せるようになりました。これは充実した生活を送るには非常に重要なことだと思います。 最後に ここまで、僕が思うエンジニアインターンのいいところを解説して来ました。繰り返しになりますが、僕はあの時インターンに応募してよかったと今でも思っています。エンジニアインターンに興味はあるけど躊躇して踏みとどまっている人も、ぜひ思い切って門を叩いてみてください。きっとかけがえのない経験が得られると思います。 VASILYでは、現在もエンジニアインターンを募集中です。少しでも気になった人は、是非下のリンクから気軽に応募してください。お待ちしてます!
アバター
こんにちは! 最近は熱燗にはまってます。 バックエンドエンジニアのりほやんです。 本記事では、最近2段階認証などにもよく使われているSMS認証のサーバーサイド実装についてご紹介します。 FacebookAccountKitを用いて実装しました。 SMS認証とは SMS認証とは、SMS(ショートメッセージ)を利用した認証方法です。 ユーザーが入力した電話番号に対してショートメッセージを送り、ショートメッセージに記載された認証番号を入力することでユーザーを認証する方法です。 FacebookAccountKitとは FacebookAccountKitはFacebookがSMS認証用に提供しているサービスです。 電話番号やメールアドレスを用いて、ユーザー認証を行うことができます。 Web, iOS, Androidの3種類のSDKが提供されています。 利用価格についてですが、2018年8月までは無料で使えるそうです。 2018年8月までSMSの課金は行いません。2018年9月以降、1か月あたり10万件を超えるSMSの確認メッセージには通常のSMS料金が課金される可能性があります。 https://developers.facebook.com/docs/accountkit/faq 認証の流れ 大まかな流れは下記のようになります。 参考: https://developers.facebook.com/docs/accountkit/overview アプリ内でユーザーが電話番号を入力 Account Kitから、入力された電話番号にSMSが届く SMSに記載された確認番号をアプリに入力し認証 認証が完了すると、Account Kitからaccess_codeが発行される 発行されたaccess_codeをサーバーに送信 アプリから送られたaccess_codeを元にaccess_tokenを発行 access_tokenを用い、AccountKit Graph APIからユーザー情報を取得 この記事では、サーバーサイドの認証である、6と7の処理について説明します。 サーバーサイドの実装方法 以下の手順でSMS認証を行います。 FacebookAccountKitの設定 access_codeからaccess_tokenを取得 access_tokenを用いてAccountKit Graph APIからユーザーの情報を取得 1. FacebookAccountKitの設定 初めにFacebookAccountKitの設定をします。 AccountKitを導入するためにはFacebookアプリケーションを作成している必要があります。 Facebookアプリケーション一覧 こちらからAccountKitを使いたいFacebookアプリケーションの画面に行き、 プロダクトの『製品を追加』をクリックし、AccountKitの設定をクリックします。 AccountKitの設定から、『サーバー認証をオンにしますか』を有効にし、スタートボタンを押します。 サーバー認証をオンにすると、設定画面において『App Secretをオンにする』が有効になります。 以上で、FacebookAccountKit側の設定は完了です。 2. access_codeからaccess_tokenを取得 サーバーでは、アプリからaccess_codeを受け取りますが、access_codeではユーザーの情報を取得することはできません。 access_codeを用いて、ユーザーの情報を取得できるaccess_tokenを取得する必要があります。 Facebook AccountKit Graph APIは2018年2月8日現在、バージョン1.3で下記のURLを用います。 https://graph.accountkit.com/v1.3/ accesss_tokenを取得するために、 /access_token というエントリポイントに対して下記パラメーターで、GETリクエストを送りaccess_tokenを取得します。 bodyパラメーター { grant_type: 'authorization_code', code: アプリから送られてきたaccess_code, access_token: アプリアクセストークン } access_tokenについて access_tokenは2種類存在します。 ユーザーアクセストークンとアプリアクセストークンです。 詳しくは、 ドキュメント を参考にしてください。 ユーザーの情報を取得するために必要なのは、ユーザーアクセストークンです。 ユーザーアクセストークンを取得するために、ここではアプリアクセストークンをパラメーターに指定する必要があります。 アプリアクセストークンは、アプリIDとAccountKitAppSecretを連結したもので、下記のように記述します。 AA|アプリID|AccountKitAppSecret アプリIDとAccountKitAppSecretは、FacebookアプリケーションのAccountKitのダッシュボードで確認できます。 レスポンス リクエストが成功すると、以下のようなJSONが返ってきます。 JSON内のaccess_tokenを、ユーザーの情報を取得する際に使用します。 { "id" : account_kitのユーザーID, "access_token" : ユーザーアクセストークン, "token_refresh_interval_sec" : トークンの利用期限 } 3. graph apiからユーザー情報を検証 取得したaccess_tokenを用いて、graph apiを叩きます。 /me というエントリポイントに対して下記のパラメーターでGETリクエストを送り、ユーザーの情報を取得します。 bodyパラメーター { access_token: アプリアクセストークン, appsecret_proof: appsecret_token, } appsecret_proofについて AccounKitのダッシュボードにて『App Secretをオンにする』設定が有効にした場合、access_tokenを使用するリクエストにはappsecret_proofが必須になります。 appsecret_prooftとは、リクエストが自身のサーバーから行われたものであることを証明するためのパラメーターです。 appsecret_proofは、access_tokenのSHA-256ハッシュで、キーにAccountKitAppSecretを用います。 AccountKitの公式ドキュメント にはPHPでの記述例が下記のように紹介されています。 $appsecret_proof = hash_hmac('sha256', $access_token, $app_secret); Ruby on Railsではappsecret_proofを求める場合はopensslを用い、以下のように書くことができます。 OpenSSL::HMAC.hexdigest('sha256', AccountKitAppSecret, ユーザーアクセストークン) 算出したappsecret_proofを、パラメーターとして使用します。 レスポンス リクエストが成功すると、以下のようなJSONが返ってきます。 { id: account_kitのユーザーID, phone: { number: 電話番号 country_prefix: 国番号, national_number: 国番号を除いた電話番号 }, application: { id: アプリID } レスポンスに含まれるアプリIDを用い、正しいアプリIDと比較して不正なレスポンスではないことを確認します。 得られたユーザーの情報から、DBと照合しユーザーログインを行います。 注意点 access_codeの使用期限 access_codeは、1度しか使えません。 一度使用したaccess_codeを再度使用した場合400エラーが発生します。 Account_kitアカウントIDについて Account_kitアカウントIDについて公式ドキュメントには、下記のように記述されています。 これらのアカウントIDは、アプリ固有のものです。アプリにFacebookログインも使用する場合、Facebookのapp-scoped IDと競合することはありません。 https://developers.facebook.com/docs/accountkit/overview?locale=ja_JP しかし実際どのような場合にAccount_kitアカウントIDが変わるのかが詳しく記述されていないため使用する際は注意が必要です。 電話番号に対して固有の値なのか、デバイスなどが変われば変わる値なのかは実際に検証する必要があります。 まとめ 本記事では、FacebookAccountKitを用いたSMS認証についてご紹介しました。 FacebookAccountKitはとてもシンプルなため、簡単にSMS認証が導入できます。 特にFacebookログインを実装した経験があると、GraphAPIの叩き方などが共通しているためスムーズに取り入れることができると思います。 ぜひこの記事がSMS認証導入のきっかけになれば幸いです。 VASILYでは、新しいことに挑戦できるエンジニアを募集しています。 興味ある方は以下のリンクからご応募ください。 参考 Facebook Account Kit 公式ドキュメント
アバター
iOSアプリチームの @hiragram です。 最近、ファーストリリース時からあった画面の大規模なリファクタリングを担当しました。 コードは遅かれ早かれ賞味期限が切れて少しずつ腐っていくものですが、その賞味期限を少しでも伸ばすために、普段コードを書く時にSwiftのOptionalについて意識していることを記事にします。 「とりあえずOptional」をやめる SwiftのOptionalは便利ですが、「Optionalを使えば、nilを安全に扱えて良い」と捉えてしまうと、気づくとモデルのプロパティがOptionalだらけになっていて使う側で毎回アンラップをしなくてはいけないような状況に必ずなります。 そうではなく、「Optionalの存在のおかげで、非Optionalなところにnilが絶対入ってこないことが保証されて良い」と捉えるべきだと思っています。 nilに口なしといいますが、Optionalなプロパティにnilが入っている時、そのnilは初期値がセットされる前の未初期化値なのか、データを提供しないことをユーザーが明示的に選択した空値なのか、はたまたエラーによって本来の値が得られなかったnilなのかを判断することはできません。 データ構造の中にnilが生まれそうになった時、それは何か設計が間違っているかもしれません。 nilは無くせるなら無い方がいいのです。 例えば、ViewControllerのあるプロパティは viewDidLoad で値がセットされるとすると、ViewControllerのイニシャライズ時点では値が無いため、安易にOptionalにすることができます。 class ViewController : UIViewController { var hogehoge : String ? override func viewDidLoad () { super .viewDidLoad() hogehoge = createHogehoge() } override func viewDidAppear (animated : Bool ) { super .viewDidAppear(animated : animated ) if let hogehoge = self .hogehoge { // アンラップしないといけない } else { // 値が取れなかったらどうする?????????? } } } しかし、「初期値を与えるのを遅延させたい」というのを実現するために、それ以降の読み出しでいちいち guard や if let を使ってアンラップする必要がでてきます。 「初期値が与えられる前にその値を参照することはそもそも間違っていて、かつ初期値が与えられて以降はnilに戻ることはない」というデータ構造なのであれば、以下のようなテクニックが使えます。 // 初期値が与えられる前に参照されたらアプリをクラッシュさせる func uninitialized <T> () -> T { fatalError() } class ViewController : UIViewController { lazy var hogehoge : String = uninitialized() override func viewDidLoad () { super .viewDidLoad() hogehoge = createHogehoge() } override func viewDidAppear (animated : Bool ) { super .viewDidAppear(animated : animated ) hogehoge // ここでアンラップする必要がない } } こうすることにより、プロパティがOptionalだった時に考えなければいけなかった「値が取れなかった時にどうするか」を考える必要がなくなります。 エラーやデータ不整合を握りつぶさない、回復不能ならアプリを落とすことも検討 上述のように、Optionalを多用してしまうと「型はOptionalだけど、実装上ここがnilになることは無いはずだから、アンラップできなかった時はreturnでいいや」みたいな判断をしがちです。 しかし、その後の改修や仕様変更でnilになるシーンが出てきた時に、以下のような問題を生む可能性があります。 アンラップで失敗を握りつぶされてしまった結果、データの不整合に気がつかない 動いてほしいメソッドがguardで落ちて実際の処理をしてくれない 私は「実装上ここがnilになることは無いはずなら、もし将来nilが入ってきた時に必ず対応する必要がある」と考え、必ずアンラップできるはずのOptional変数がnilだった場合、 fatalError() や preconditionFailure() で明示的にアプリをクラッシュさせることを検討します( ! でアンラップするのと変わらなくなってしまいますが、それは治安が悪いのでやりたくありませんよね)。 「アプリがクラッシュしない」というのは大事なことですが、「アプリ内のデータや状態が不整合を起こしていてもなんとなく動き続けてしまう」よりは潔くクラッシュしたほうが健全だと思います。 クラッシュすればクラッシュログが溜まって気づけますが、本番環境で壊れながらのらりくらり動き続けるアプリはそもそも問題として気づかれないことが多く、ついに崩壊してユーザーの目に明らかな壊れ方をした時には、すでにその状態から遡ってバグの原因を突き止めるのは非常に困難なはずです。 まとめ 以上2点は、Swiftを書く上で意識したいOptionalの本質であると私は考えます。 「クラッシュしないこと」がいちばん大事なのではなく、「正しく動き続けること」が大事なのです。 Optionalにすることでクラッシュしないアプリを手軽に作ることができるようになりましたが、それは実は本質的でなく、将来の耐改修性を損ねている可能性があるということを常に頭にいれて、上手にOptionalと付き合っていきたいものです。 VASILYではiOSエンジニアを募集しています。 この記事に共感してもらえるような、Swiftの機能の本質を考えて最良の設計ができるエンジニアと一緒に働きたいです。 ご応募お待ちしております。 https://www.wantedly.com/projects/88978 www.wantedly.com
アバター
こんにちは。インフラエンジニアの内山(@k4ri474)です。 弊社ではCloudFormationとOpsWorksを活用してインフラ構築をしています。 この両サービスではハマりどころが多く、中でもOpsWorksでインスタンスが複数のレイヤに所属する構成を構築した際にとても苦戦しました。 そこで今回は、私が上記構成でインフラ構築をした際に悩んだ点の解決法をTipsとして公開します。 目次 目次 サービスの概要 CloudFormation OpsWorks [Theme 1]インスタンス名の決定 前提条件 命名法則 順序が明確でない場合の挙動 複数レイヤを同じ変更セットで作成した場合 複数インスタンスを同じ変更セットで作成した場合 順序のコントロール [Theme 2]ダミーセキュリティグループの利用 導入ケース 実装方法 [Theme 3]レシピのマージ 注意点 問題発見のアプローチ まとめ サービスの概要 CloudFormation CloudFormationは、AWSリソースをJSON/YAMLで表現したテンプレートを元に、その構成を自動で構築してくれるサービスです。 本エントリでは、後述するOpsWorksのリソース宣言や設定項目のセットをCloudFormationのテンプレートを用いて紹介しています。 OpsWorks OpsWorksは管理下のインスタンスに対してChefを実行することで構成管理を行うサービスです。 スタック、レイヤ、インスタンスという3要素でインスタンスを管理しており、以下のような条件で構成されます。 インスタンスは1つ以上のレイヤに所属する レイヤはスタックに割り当てる このため、上記3要素は Stack > Layer > Instance という階層のように表現されます。 なお、1つのスタックに複数のレイヤを割り当てることができ、インスタンスは複数のレイヤに所属することができます。 CloudFormationとOpsWorksについて詳しく知りたい方はこちらのエントリをご覧ください。 tech.vasily.jp [Theme 1]インスタンス名の決定 前提条件 スタックのAdvanced optionsにある Hostname theme が、デフォルトの Layer Dependent であることを前提条件とします。 命名法則 OpsWorksで作成したインスタンスの名前(Nameキーの値)は、以下3つの要素から構成されます。 所属するスタックのNameプロパティの値 インスタンスが所属するレイヤの中で最も先に作成されたレイヤのShortNameプロパティの値 2の値が同じインスタンスの中で、何番目に作成されたインスタンスなのかを表す数値 これら3要素を使い <stack_name> - <layer_shortname><number> と命名されます。 ここで命名に利用されるレイヤは1つだけという制約があるのですが、1つのインスタンスは複数のレイヤに所属することが可能です。 この場合どのレイヤが優先されるかは、リソースとしてどのレイヤが先に作成されたかで決定されます。 例えば、以下のようなリソースがCloudFormationのテンプレートに記述されていたとします。 TestStack : Type : "AWS::OpsWorks::Stack" Properties : Name : "TestStack" CommonLayer : Type : "AWS::OpsWorks::Layer" Properties : Shortname : "common" StackId : !Ref TestStack WebLayer : Type : "AWS::OpsWorks::Layer" Properties : Shortname : "web" StackId : !Ref TestStack WebFirst : Type : "AWS::OpsWorks::Instance" Properties : LayerIds : - !Ref CommonLayer - !Ref WebLayer StackId : !Ref TestStack (※説明簡略化のため必須プロパティを省略しています) 上記4つのコードブロックでは、以下の状態を表しています。 TestStackというスタックにCommonLayer・WebLayerという2つのレイヤがある WebFirstというインスタンスが両レイヤに所属している 上の例の場合、WebLayerがCommonLayerより先に作成されていた際のWebFirstの名前は、 TestStack - web1 となります。 逆にCommonLayerの方が先に作成されていた場合は、WebFirstの名前は TestStack - common1 となります。 この時、CloudFormationのテンプレートで指定した WebFirst は論理IDというリソースとして扱われ、各サービスでのインスタンス名表記には利用されないことに注意します。 順序が明確でない場合の挙動 複数レイヤを同じ変更セットで作成した場合 先述の通り、レイヤの作成順序によってインスタンス名が左右されます。 作成順序は適用順で決まるので、より先の変更セットに含まれているリソースが"先に作成された"と見なされます。 また、同じ変更セットで作成された複数のレイヤにインスタンスを所属させた場合、どちらのレイヤのShortNameが使われるかは未定です。 上の例の場合、同じ変更セットでCommonLayerとWebLayerを作成すると TestStack - web1 になるのか TestStack - common1 になるのかが把握できないということになります。 複数インスタンスを同じ変更セットで作成した場合 CloudFormationは可能な限り並列にAWSリソースを作成・更新しようとします。 そのため、同じ変更セットで同レイヤに所属するインスタンスを複数作成しようとする場合は、"何番目のインスタンスか"を表す数値が同一になります。 上の例の場合、インスタンスを2つ作成すると2つとも TestStack - web1 となってしまいます。 論理IDをWebFirst・WebSecondのように設定していても上図のようになるので、戸惑いがちなポイントかと思います。 このように、レイヤ・インスタンスの作成順序が明確でないと運用上困ることがあるので、後述するDependsOn属性をリソースに付与してコントロールします。 順序のコントロール DependsOn属性 を付与することで、テンプレート上で明確に作成順序を分けることができます。この場合、変更セットを分ける必要はありません。 レイヤの作成順序を制御したコードは以下のようになります。 CommonLayer : Type : "AWS::OpsWorks::Layer" DependsOn : - WebLayer Properties : Shortname : common StackId : !Ref TestStack WebLayer : Type : "AWS::OpsWorks::Layer" Properties : Shortname : web StackId : !Ref TestStack WebFirst : Type : "AWS::OpsWorks::Instance" Properties : LayerIds : - !Ref CommonLayer - !Ref WebLayer StackId : !Ref TestStack WebSecond : Type : "AWS::OpsWorks::Instance" DependsOn : - WebFirst Properties : LayerIds : - !Ref CommonLayer - !Ref WebLayer StackId : !Ref TestStack CommonLayerにDependsOn属性を記述してWebLayerが先に作成されるように明示することで、この両レイヤの作成順序は常にWebLayerが先になります。 そのため、WebFirst・WebSecondインスタンスのNameキーの値には web が利用されるようになります。 インスタンスにおいてもルールは同様です。 WebSecondにてDependsOn属性を記述してWebFirstに依存するように設定すると、同じ変更セットでインスタンスを作成しても以下のように命名されます。 WebFirst : TestStack - web1 WebSecond : TestStack - web2 このようにDependsOn属性を活用することで、コード上で順序関係を把握することができ、1つの変更セットで意図した通りに複数リソースを作成できます。 [Theme 2]ダミーセキュリティグループの利用 導入ケース OpsWorksでは、全てのレイヤにセキュリティグループを割り当てることが必須となっています。 この制約がネックとなる場面を例示するため、弊社のテンプレートの設計を紹介します。 弊社では以下3種類のレイヤを作成しており、インスタンスは最大で3レイヤに所属しています。 サービスにおける役割(ロールと呼ぶ)ごとの設定を記述したレイヤ 複数のロールにおける共通の設定を記述したレイヤ 全インスタンスにおける共通の設定を記述したレイヤ この時、2と3のレイヤにおいては複数のロールに跨っていることを考慮してセキュリティグループのルールを考えねばなりません。 2のレイヤでアプリケーションサーバ用のレイヤとwebサーバ用のレイヤをくくり出していた場合、共通のインバウンド・アウトバウンドルールとして挙げられる選択肢はそう多くないと思います。 こういった場合に、ルールを持たないセキュリティグループが役に立ちます。 実装方法 セキュリティグループの作成時にインバウンド・アウトバウンドのルールは必須項目ではないため、ルールを持たないセキュリティグループを作成する事ができます。 CloudFormationのテンプレートでは以下のように記述します。 DummySecurityGroup : Type : "AWS::EC2::SecurityGroup" Properties : GroupDescription : "dummy-SG" Tags : - Key : "Name" Value : "dummy-SG" VpcId : !Ref MyVPC このようなルールを持たないセキュリティグループをレイヤに割り当てることで、CloudFormationの制約を守りながらサービスに影響を与えることなく構築ができます。 [Theme 3]レシピのマージ 注意点 マルチレイヤ構成をとる場合、CustomJsonに記述したレシピについて以下の注意点があります。 インスタンスが複数レイヤに所属する場合、レイヤ間のCustomJsonはマージされる 同名の要素があった場合はどちらの要素が使われるか不定 そのため、以下のコードブロックの例だとCommonLayer・WebLayerの両レイヤに所属するインスタンスでは、Rubyのversionが2.5.0か2.2.5のどちらかとなります。 CommonLayer : Type : "AWS::OpsWorks::Layer" DependsOn : - WebLayer Properties : CustomRecipes : Setup : - 'ruby' CustomJson : | { "ruby" : { "version" : "2.5.0" } } WebLayer : Type : "AWS::OpsWorks::Layer" Properties : CustomRecipes : Setup : - 'ruby' CustomJson : | { "ruby" : { "version" : "2.2.5" } } スタック全体のレイヤ数が増え、インスタンスが3レイヤ以上に所属するような状態だと、意図せず発生してしまう問題です。 問題発見のアプローチ OpsWorksによって作成された全てのインスタンスには、 AWS OpsWorks スタック エージェント がインストールされます。 OpsWorksスタックエージェントのCLIで用意されたコマンドの1つである get_json を用いると、上のコードブロックのようにChefで実行されたJSONを確認することができます。 $ sudo opsworks-agent-cli get_json { " ruby " : { " version " : " 2.2.5 " } , " run_list " : [ ] } $ CloudFormationを利用するとテンプレート上で全てのリソースを把握できますが、実際のインスタンスでの適用状況を確認することも重要となります。 まとめ CloudFormationとOpsWorksを使って複数レイヤ構成を構築する際のTipsを紹介しました。実現したいインフラの構成が違えば直面する問題も異なってくるかと思いますが、この記事が参考になりましたら幸いです。 VASILYではモダンな手法を取り入れながら柔軟にインフラ構築をしていきたいエンジニアを募集しています。興味のある方は以下のリンクからご応募ください。 https://www.wantedly.com/projects/108005 www.wantedly.com
アバター
こんにちは、バックエンドエンジニアの塩崎です。 最近のTECH BLOGではMatzさんのインタビュー記事を書いたり、RubyKaigiの発表まとめを書いたりして、他人の褌で相撲を取っていました。 今回は心を入れ替えて(?)、自分自身が取り組んだ内容について書きます。 VASILYでは検索用のミドルウェアとしてApache Solr(以下、Solr)を使用しています。 全文検索や、ファセット機能などはMySQLだけでは不十分なために、Solrを併用しています。 Solrのサーバー構成例にはいくつかのパターンがありますが、今回はその中でも最も可用性の高いSolrCloudをサービスインしたので、それについて紹介を行います。 Solrの構成例を幾つか紹介 Solrの構成例は大きく以下の3つに分けられます。 まずは、それぞれについて詳しく説明していきます。 スタンドアローン構成 master slave構成 SolrCloud構成 スタンドアローン構成 スタンドアローン構成は3つの構成例のなかで最もシンプルな構成です。 Solrサーバーは1台のみで、その1台がclientからのすべてのread/writeリクエストを受け付けます。 この構成ではSolrサーバーが死んだ場合にはすべてのread/writeリクエストができない状態になってしまいます。 master slave構成 更新系のリクエストを担当するmaster nodeと参照系のリクエストを担当するslave nodeからなる構成です。 検索インデックスの更新はmaster nodeのみで行い、構築済みのインデックスをslave nodeにコピーします。 インデックスのコピーにはSolrのreplication機能を使います。 参考: Index Replication この構成ではslave nodeの台数を1台以上にすることで参照系の可用性を上げることができます。 すべてのslave nodeが死なない限り、参照系のリクエストを処理することができます。 他方で書き込み系は冗長構成をとっていないため、唯一のmaster nodeが死んだ場合にはすべての書き込みリクエストが失敗します。 なお、場合によってはSolrクライアントとSolr slaveの間にあるロードバランサーを省略することもできます。 Solrクライアント自身が適当なslave nodeへの通信の振り分けと、各slave nodeの死活監視をすれば、ロードバランサーは不要になります。 SolrCloud構成 SolrCloud構成は基本的にはmaster slave構成と同じですが、より可用性が上がった構成です。 更新系のリクエストを処理するleader nodeと参照系のリクエストを処理するfollower nodeがあります。 そして、それらの間のインデックスの同期はreplication機能によってなされます。 master slave構成との大きな違いはleader node(master node)に障害が発生したときの挙動です。 SolrCloud構成ではleader nodeに障害が発生した場合には、適当なfollower nodeが新しいleader nodeになります。 そのため、クラスタ内のどの1台が死んでも、クラスタとしては正常に動作することができます。 これらの機能を実現するためには、現在のクラスタ情報を保持したり、新たなleader nodeを選出するための仕組みが必要です。 SolrCloudではその機能をzookeeperに担わせています。 そのため、Solrクライアントはzookeeperに対して、現在のクラスタ情報を問い合せて、適切なノードに対して通信を振り分ける必要があります。 また、nodeの追加や削除でクラスタ情報が更新された時にはクライアントもそれに追従する必要があります。 さらに、SolrCloud構成ではシャーディングによる水平分割もサポートしているため、leader nodeの負荷分散を行うこともできます。 それぞれの特徴まとめ 各構成の特徴を「必要な台数」と「可用性」という観点からまとめてみました。 スタンドアローン master slave SolrCloud 必要な台数 1 1 + slave台数 可用性の要求次第で何台でも + zookeeper 参照系が死ぬ条件 1台しかないnodeが死んだとき 全slave nodeが死んだとき 全nodeが死んだとき 更新系が死ぬ条件 1台しかないnodeが死んだとき 1台しかないmaster nodeが死んだとき 全nodeが死んだとき 注意が必要な点としては、SolrCloud構成ではzookeeperの構築も必要な点が挙げられます。 zookeeperはSolrCloudの中で非常に重要な役割を果たすため、zookeeper自体の冗長構成も必要です。 zookeeperを冗長構成にするためには最低3台のnodeが必要なので、その考慮が必要です。 上の表を見ると、SolrCloud構成のみが参照系・更新系ともに単一障害点(SPOF)をなくすことのできる構成なことがわかります。 そのかわりに、SolrCloud構成では必要とされるサーバーが多くなりがちです。 SPOFをゼロにしようとした場合は、Solrサーバー 2台とzookeeperサーバー3台の合計5台が最低限必要です。 そのため、個人サービスやまだ小規模なサービスであれば、スタンドアローン構成やmaster slave構成を採用するのが良いのではないかと思います。 SolrCloudの検証 構築してみる さて、ここからはSolrCloudの検証について話していきます。 対象読者はSolrCloudの構築をしたことはないけど、スタンドアローン構成やmaster slave構成のSolrの構築をしたことがある人です。 Solrの構築を一度もしたことがない人は以下の書籍やオンライン資料などを参考にシンプルな構成を試してみてから読んで見てください。 改訂第3版 Apache Solr入門 Apache Solr Reference Guide 環境 今回の検証は以下の環境で行いました。 $ java -version java version "1.8.0_73" Java(TM) SE Runtime Environment (build 1.8.0_73-b02) Java HotSpot(TM) 64-Bit Server VM (build 25.73-b02, mixed mode) $ system_profiler SPHardwareDataType Hardware: Hardware Overview: Model Name: MacBook Pro Model Identifier: MacBookPro11,3 Processor Name: Intel Core i7 Processor Speed: 2.5 GHz Number of Processors: 1 Total Number of Cores: 4 L2 Cache (per Core): 256 KB L3 Cache: 6 MB Memory: 16 GB Boot ROM Version: MBP112.0142.B00 SMC Version (system): 2.19f12 Serial Number (system): ************ Hardware UUID: ********-****-****-****-************ すべてのSolrプロセスは同一のMacの中で起動させています。 検証に使用したSolrのバージョンは2018/01/19時点で最新バージョンである7.2.1です。 構築手順 Solrにはサンプルの環境を簡単に立ち上げることのできるモードがあります。 このモードでは対話的にreplica数やshard数を打ち込むと、それに応じてイイカンジにSolrCloudが立ち上がります。 ですが、あまりにも簡単に立ち上がってしまうので、今回はstep by stepで進めて行きたいと思います。 まずはzookeeperを立ち上げます。 本番環境で運用するときには冗長構成が必要ですが、とりあえずは1台のみの構成にします。 $ wget http://ftp.jaist.ac.jp/pub/apache/zookeeper/zookeeper-3.4.11/zookeeper-3.4.11.tar.gz $ tar xvf zookeeper-3.4.11.tar.gz $ cd zookeeper-3.4.11 conf/zoo.cfgに以下の設定ファイルを配置します。 tickTime=2000 dataDir=/tmp/zookeeper clientPort=2181 以下のコマンドでzookeeperを起動します。 bin/zkServer.sh start 次にSolrCloudのnodeを立ち上げます。 今回はreplica数とshard数の両方を2で構築してみます。 node数は合計で2 * 2 = 4必要です。 SolrCloudでは大抵の設定ファイルはzookeeperによって管理されます。 ですが、zookeeperと接続するための設定だけは各nodeに配置する必要があります。 この部分は共通なので、以下のsolr.xmlを用意します。 <? xml version = "1.0" encoding = "UTF-8" ?> <solr> <solrcloud> <str name = "host" > ${host:} </str> <int name = "hostPort" > ${jetty.port:8983} </int> <str name = "hostContext" > ${hostContext:solr} </str> <bool name = "genericCoreNodeNames" > ${genericCoreNodeNames:true} </bool> <int name = "zkClientTimeout" > ${zkClientTimeout:30000} </int> <int name = "distribUpdateSoTimeout" > ${distribUpdateSoTimeout:600000} </int> <int name = "distribUpdateConnTimeout" > ${distribUpdateConnTimeout:60000} </int> <str name = "zkCredentialsProvider" > ${zkCredentialsProvider:org.apache.solr.common.cloud.DefaultZkCredentialsProvider} </str> <str name = "zkACLProvider" > ${zkACLProvider:org.apache.solr.common.cloud.DefaultZkACLProvider} </str> </solrcloud> <shardHandlerFactory name = "shardHandlerFactory" class = "HttpShardHandlerFactory" > <int name = "socketTimeout" > ${socketTimeout:600000} </int> <int name = "connTimeout" > ${connTimeout:60000} </int> </shardHandlerFactory> </solr> そして、4node分のsolr_homeディレクトリを作り、solr.xmlをコピーします。 mkdir -p solr_home/node1/solr mkdir -p solr_home/node2/solr mkdir -p solr_home/node3/solr mkdir -p solr_home/node4/solr cp solr.xml solr_home/node1/solr cp solr.xml solr_home/node2/solr cp solr.xml solr_home/node3/solr cp solr.xml solr_home/node4/solr 最後に、以下のコマンドを実行することで各nodeが立ち上がります。 bin/solr start -cloud -s solr_home/node1/solr -p 8001 -z 127.0.0.1:2181 -h 127.0.0.1 bin/solr start -cloud -s solr_home/node2/solr -p 8002 -z 127.0.0.1:2181 -h 127.0.0.1 bin/solr start -cloud -s solr_home/node3/solr -p 8003 -z 127.0.0.1:2181 -h 127.0.0.1 bin/solr start -cloud -s solr_home/node4/solr -p 8004 -z 127.0.0.1:2181 -h 127.0.0.1 この状態で、 127.0.0.1:8001 にアクセスするとSolrのwebコンソールが見えます。 左側のメニューのCloudという項目がSolrCloud特有の項目で、ここからzookeeper内のデータを見たり、クラスタの状態を確認したりすることができます。 まだこの時点ではcollectionを作っていないため、Cloud画面にはほとんど情報がありません。 次にcollectionを作成します。 ここではポート8001で起動しているnodeに対してcreate_collectionを行っていますが、他のnodeに対してcreate_collectionをしても結果は変わりません。 bin/solr create_collection -c collection1 -d server/solr/configsets/_default -p 8001 -shards 2 -replicationFactor 2 collectionの作成に成功すると、自動的に各nodeからleader選出が行われ、それぞれのnodeがどれかのshardに属します。 どのnodeがleaderになるか、どのnodeがどのshardに属するのかということはその時々で変わります。 次にcollectionに対してfieldを追加していきます。 fieldの追加はweb uiから行う方法とSchema APIから行う方法の2つがあります。 今回はSchema APIを経由して行います。 Schema API 以下のコマンドでpint型のfield1を追加します。 curl -X POST -H 'Content-type:application/json' --data-binary '{ "add-field":{ "name":"field1", "type":"pint", "stored":true, "indexed": true } }' http://localhost:8001/solr/collection1/schema 同様にして、field2、field3も追加します。 Schema APIによるfieldの追加によってzookeeperで管理されているmanaged-schemaファイルの更新が行われます。 そして、その変更を各nodeが取り込むことによって、全nodeでfield追加が反映されます。 ドキュメントの投入を行います。 サンプルとして各fieldに対して0〜100までの乱数を設定したドキュメントを10000件投入します。 require ' rsolr ' solr_host = ' 127.0.0.1 ' solr_port = 8001 solr_collection = ' collection1 ' solr_url = " http:// #{ solr_host } : #{ solr_port } /solr/ #{ solr_collection }" rsolr = :: RSolr .connect( url : solr_url, retry_503 : 5 , retry_after_limit : 1 ) ( 1 .. 10000 ).each do | i | doc = {} doc[ :id ] = i ( 1 .. 3 ).each do | j | doc[ " field #{ j }" .to_sym] = :: Random .rand( 100 ) end rsolr.add(doc) rsolr.commit end ドキュメントの検索は通常のsolrと同じです。 以下のコマンドで1つめのnodeに対して検索を行います。 curl http://localhost:8001/solr/collection1/select?indent=on&q=*:*&wt=json port番号の部分を変更することで、他のnodeに対して検索リクエストを投げることもできますが、返される結果は同じです。 これは検索リクエストを受け取ったnodeが他のshardのデータを取得し、その結果をマージしているためです。 クライアントから使ってみる RubyからSolrCloudを使う方法を説明します。 RubyからSolrを使うためにはrsolrというgemを使用することが多いです。 SolrCloudを使うためにはzookeeperへの問い合わせなどが必要なため、rsolrの機能だけでは不十分です。 そのため、enigmoさんが公開しているgemであるrsolr-cloudを併用します。 rsolr-cloudはrsolrのアダプターとして機能し、zookeeperへのクラスタ情報の問い合わせや、リクエストの種類に応じて適切なnodeに通信を振り分ける機能などを提供します。 rsolr-cloud 注意点はrsolr-cloudを使うときにはrsolrのメジャーバージョンを1にする必要があることです。 そのため、以下にGemfileでバージョンを指定する必要があります。 gem 'rsolr', '~>1.1.2' # rsolrのv2以上はrsolr-cloud未対応のため gem 'rsolr-cloud' あとは、以下のようにすることで、SolrCloudに対するクエリを投げることができるようになります。 require ' zk ' require ' rsolr/cloud ' zoopeepers = [ ' localhost:2181 ' , ' localhost:2182 ' , ' localhost:2183 ' , ] zk = ZK .new(zookeepers.join( ' , ' )) cloud_connection = RSolr :: Cloud :: Connection .new(zk) solr_client = RSolr :: Client .new(cloud_connection) # 必ずcollectionを指定する必要あり response = solr_client.get( ' select ' , collection : ' collection1 ' , params : { q : ' *:* ' }) 可用性の検証 SolrCloudの可用性を検証してみます。 上の環境で作ったSolrCloudの環境で何台かのnodeを落としてみて、その時の挙動を確認してみます。 サーバーの突然死を再現するためには、以下のkillコマンドを使いました。 kill -9 <PID> 注)この検証では、zoopeekerが死ぬことについては想定していません。 無事なケース leaderが突然死ぬ時 masterが突然死する場合は、書き込みできない期間が数秒間発生してしまいます。 これはSolrCloudクラスターが次のleaderを選出するための期間です。 一方でmaster slave構成の場合には人間が手動でmasterへの昇格を行う必要があり、数分〜数十分かかってしまいがちなことを考えると、SolrCloud構成の可用性の高さがうかがい知れます。 なお、この挙動が嫌な場合には、書き込み処理をsidekiqなどの非同期処理を用いて実装するのが良いと思います。 followerが突然死ぬ時 followerが突然死する場合は一切のリクエストが失敗することなく、SolrCloudは正常に動作を続けます。 これはzookeeperが各nodeの死活監視を行い、Solrクライアントはzookeeper内のドキュメントの変更を検知したときに接続先情報の変更をしているためです。 ダメなケース 上でSolrCloudの可用性が高いことを紹介しましたが、流石のSolrCloudでも復帰できないケースもあります。 シャード内の全nodeが死ぬ時 ある特定のシャードに属するすべてのnodeが死んだ場合、SolrCloudクラスタは正常に機能を提供できなくなります。 そのため、シャード内のレプリカの数は単にreadの負荷分散という観点だけからではなく、可用性という観点からも決める必要があります。 まとめ SolrCloud構成の利点や構成方法について説明をしました。 いくつかあるSolrの代表的な構成例の中でも一番サーバー台数が必要な構成ですが、可用性も一番高い構成です。 検索サーバーが落ちたことによる緊急メンテナンスが辛かった経験のある方は是非検討してみてください。 VASILYでは現状のサーバー構成に満足せず、ユーザーにとってより良い体験を提供するために常に新しい仕組みを導入していきたい人を募集しています。 興味のある方は以下のリンクからご応募ください。
アバター