TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

938

こんにちは、WEARバックエンドエンジニアの 三浦 です。WEARのバックエンドの開発、保守運用に携わっています。個人ではおよそ2年ぶりのテックブログ執筆となります。 さて、今回はWEAR上のコンテンツを運用チームが自由にカスタマイズできるようモジュール化した話をご紹介します。 目次 目次 モジュール概要 導入背景 モジュールの設計 要件と課題 リプレイスとの競合 負荷対策 管理ツール API まとめ さいごに モジュール概要 まず最初にWEARのモジュール化はどのようなものか説明します。 今回導入したモジュール化は、WEARのアプリ上の一部コンテンツをUIモジュールとして扱えるようにすることです。1つ目の画像に表示されている シアーシャツ や 雨の日コーデ といったテーマ別のコンテンツ1つ1つがモジュールとなっており、テーマに合わせて絞られたコーディネート画像が表示されます。モジュール右上の すべてを見る を押下するとそのテーマに沿ったコーディネートの一覧画面へ遷移します。それが2つ目の画像になります。モジュールは運用チームが管理ツールから自由に作成できます。 導入背景 以前ビジネスサイドから、WEARのTOPページのコンテンツを定期的に変更してユーザーに様々な切り口でコーディネートを提案し、効果検証にも活用したいという要望があがりました。しかし、当時は新しいコンテンツを導入する際には、毎回デザイン検討、開発、QAチームによるテストと一連のプロセスを踏む必要があり、かなりの工数がかかりました。そのため、気軽に変更することが難しい状況でした。 そこでTOPページの一部のコンテンツをモジュール化し、大規模な開発をせず自由にコンテンツを変更できる環境を整えようということで今回の施策が動き始めました。 モジュールの設計 要件と課題 ビジネスサイドからの要件は以下のとおりです。この要件をベースに設計を考えていきます。 初回リリース時点ではコーディネートに関するモジュールを作成できること 将来的に動画やフリマ出品アイテムといったコーディネート以外のコンテンツもモジュール化できる汎用的な作りになっていること モジュールは複数表示でき、個別に掲載期間も指定できること モジュールの作成自体は管理ツールから運用チームが自由に作成できること 要件を満たすための設計を検討していく上でいくつかの課題も浮かび上がってきました。 リプレイスとの競合 WEARでは別チームが同時並行でリプレイスを行っており、コーディネートの検索基盤も旧版(Amazon CloudSearch)と新版(Elasticsearch)が同時稼働している状態です。モジュールの すべてを見る のリンクから遷移するコーディネート一覧画面は、開発工数を抑えるため既存の画面をそのまま使用します。こちらは旧版の検索基盤で検索が行われています。一方、TOP画面のモジュールで表示されるコーディネートには、ユーザーが最初に目にする部分であることから、柔軟にロジックを調節できる新版の検索基盤を使用します。 リプレイス前後の検索基盤でインデクシングされている項目に差異があり、リプレイス後の方が検索の絞り込み条件が増えています。ただし、TOP画面と一覧画面で検索結果が変わってしまっては困るので、その差異を考慮しながら検索条件を組み立てる必要があります。 負荷対策 モジュールはTOP画面で表示され、かつモジュールの数に比例してAPIのリクエスト数も増えるので、検索基盤に大きな負荷を掛けてしまう恐れがあります。 これらの要件や課題を踏まえて管理ツール、APIそれぞれの設計をしていきます。 管理ツール 運用チームは、まず管理ツール上のコーディネート検索画面からモジュールに使用したい条件で検索し、検索結果を確認します。ここでの検索には新版の検索基盤を使用しているため、TOP画面のモジュールで表示されるコーディネートになります。検索条件に使用できる条件は、リプレイス前後のパラメータの差分を確認して旧版の検索基盤でも検索可能な条件のみに限定しました。問題なければ同画面に用意しているモジュール作成ボタンからモジュール作成へ進みます。 以下は、上記のフローをシーケンス図にしたものです。 モジュール作成画面では主に以下の値を設定できます。 タイトル 掲載開始日時 掲載終了日時 表示優先度:複数のモジュールを表示した際の優先順位 コンテンツ種別:コーディネート、動画、etc... 検索パラメータ:モジュールに表示するデータの検索条件 ディープリンク:絞り込み条件付きの一覧画面のアプリディープリンク 検索パラメータとディープリンクに関しては、シーケンス図の 1.コーディネート検索画面:検索 で指定した条件を基にバックエンド側で登録します。画面で指定した条件と各検索基盤で使用するキー名のマッピングをモデル内で保持し、検索パラメータとディープリンクを組み立てていきます。ディープリンクのパラメータに関する資料があまり残っておらず、どのようなキー名が使われているかをアプリ上で実際に試しながら実装したため時間がかかりました。マッピングの実装部分は若干複雑になってしまいましたが、運用チームがリプレイスの環境差分やパラメータ名などシステムの仕様を意識せず、直感的にGUI上で登録できるようになりました。 また、同時に掲載できるモジュールの数には制限を設けることにしました。無制限に作成するとシステム負荷が上がるため、運用チームから作成したいモジュール数をヒアリングし、最大4つに制限しました。 API モジュールの一覧APIと詳細APIの2つに分けて作成しました。一覧APIで現在日時が掲載期間内のモジュールのIDを全て取得し、詳細APIではそれぞれのモジュールの内容を1件ずつ取得します。 以下は、上記のフローをシーケンス図にしたものです。 モジュールの内容は全ユーザーに対して同じであるため、負荷対策として詳細APIの内容を一定時間キャッシュする仕組みを導入しました。具体的にはRailsの低レベルキャッシュを使用して、 #{controller_path}/#{モジュールのID} をキーにレスポンスのJSONをキャッシュします。キャッシュ有効期間は、検索基盤のインデクシングが更新されるタイミングに合わせて設定しました。同じモジュールの内容を再利用することで、頻繁な検索基盤へのアクセスを回避し、検索基盤への負荷を軽減しました。 coordinate_module = CoordinateModule .find(params[ :id ]) coordinate_module_json = Rails .cache.fetch( "#{ controller_path } / #{ params[ :id ] }" , expires_in : xx.minutes) do render_to_string json : CoordinateModuleSerializer .serialize( coordinate_module : coordinate_module, coordinates : coordinates_searched_by(coordinate_module)) end render json : coordinate_module_json 詳細APIのレスポンスは、 管理ツールで設定したモジュールの内容 + モジュールと一緒に表示するデータの配列 のJSONになっています。配列部分はコンテンツ種別で設定した内容によってコーディネートの配列や動画の配列といったように変わってきます。クライアント側はコンテンツ種別を見てどんなデータが返ってくるかを判別します。 これによりコンテンツによって大きくレスポンス形式を変えることなく汎用的に利用できるようになりました。 まとめ モジュール化を行なったことで、エンジニアを介さず運用チーム側で定期的にTOP画面のコンテンツを更新できるようになりました。また、システム面で汎用的に作成したので他のページでもこの機能を応用できました。現在はフリマページのおすすめの出品アイテムも一部モジュール化されています。 モジュールを複数個出し始めてから、ユーザーのコーディネートの一覧ページへの回遊率が上昇しました。年齢別やTPOに合わせたテーマが人気のようです。 今後は、検証をした結果を活かして、コンテンツの精度の向上やパーソナライズなどもしていきたいです。 さいごに WEARでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
2022年6月に、Androidテックリードになった いわたん です。最近、某モンスターを育てたり図鑑を埋めたりするゲームで社内大会をやったらフルボッコにされて涙目でした。悔しくて最近は不思議な力でクラフトしたり空飛んだりして王国を救うゲームやってます。 今回はAndroidテックリードとして1年間やってみた施策の紹介と、それぞれの成果や反省点を紹介したいと思います。これからテックリードになろうとしている方やテックリードをしている方の参考になったり、こんな施策もいいよというアドバイスをもらえたら幸いです。 ZOZOのテックリードの役割と責任 実施した施策 テックリード1on1 読書会 歴史的経緯があるアプリのアーキテクチャ整理へのアプローチ ネーミングセンスを鍛える会の取り組み 案件への関わり方 横断的なコードレビュー 横断的に使う機能の実装 まとめ 最後に ZOZOのテックリードの役割と責任 Qiita で弊社VPoEの瀬尾が紹介しているように会社としてのテックリードの役割定義があります。 Qiitaより抜粋したテックリードの役割です。 チームの技術的な方向性、設計や開発手法、実装や品質などプロダクトの技術面での定義に対して責任を持つ 全社横断で、技術施策の策定と普及、スキルアップ支援、採用活動への協力など技術面での貢献を行う 具体的にチームに対してどういった活動をするかはテックリードに任されている状態でした。 そこで、 エンジニアのためのマネジメントキャリアパス という書籍を参考に、活動内容を考えました。 また、ZOZOの組織体制として、チームのマネジメントを行うブロック長がいます。ZOZOTOWN Androidチームでのブロック長の活動は 過去の記事 があるので、そちらも興味があれば読んでいただければ幸いです。 チーム内にテックリードとブロック長と似ているような似ていないような役職が存在することで、役割が重なりチームの二重管理とならないよう、役割の棲み分けを行いました。 メンバーのサポート テックリードは個人、チームの技術面の成長をサポートする ブロック長は、 企業理念 に上げている ZOZOらしさ の成長をサポートする プロダクト開発 テックリードは技術的負債の返還や新たに発生させないよう責務を負う ブロック長はアサイン管理を実施する テックリードが目指すべき具体的なタスクの方向性が見えてきたので、次のような施策を実施しました。 実施した施策 チームとして成長を続けるため、さまざまな施策を試行しました。具体的にはテックリード1on1、読書会、アーキテクチャ座談会、ネーミングセンスを鍛える会など、様々な形の活動を通じて、チーム全体の技術力の向上を目指しました。 これら以外にも試した施策は多数ありますが、本記事では記憶に残っていたり、特に効果的だったものをいくつかピックアップし、それらの施策について詳しく紹介します。それぞれの施策は、テックリードとしてチームの成長を支援するために重要だと考えています。またチームとして、常に新たな取り組みに挑戦することは大切だと考えており、新たな試みから学んだことを積極的にフィードバックに活かすことで、より良いチーム作りを目指しています。 テックリード1on1 前述の『エンジニアのためのマネジメントキャリアパス』にも書かれていたのですが、メンバー個別の成長をサポートするためにテックリード1on1を実施しました。テックリード1on1は、個々のメンバーと直接対話し、具体的な技術的課題や目標に対するサポートを提供するための時間として設定しました。ZOZOでは定期的に上長との1on1を実施していますが、その場とは異なり、より技術的な内容に焦点を当てる時間としました。 テックリード1on1の主な目的は次のとおりです。 技術的な要素が絡む目標で手伝えることは何かを明確にする。 伸ばしたい技術が何か、個々のメンバーと相談する場を提供する。 チーム内で技術知識を横展開し、共有する。 出しているPull Requestのレビューや、一緒にタスク整理をする。 テックリード1on1は最低月1回開催し、半期ごとの目標設定で設定した技術的な目標に対して手伝えることがないかを確認します。また、評価時には、これまでのテックリード1on1で話し合った内容をもとに、個々のメンバーが自己アピールに使えるポイントを明示しました。 ZOZOTOWN Androidのメンバーは10人以上おり、テックリード1on1は全体で毎週半日ほどの時間をかけるようにしました。テックリードになってからの負担としては一番大きい施策ですが、テックリード1on1の取り組みにより、メンバーそれぞれから様々な課題感を聞くことができました。 出てきた課題感として、次のような技術的な課題が出ました。 コードメトリクスの計測 を行いコードの品質を測定したい Android Vitalsを活用 してアプリのパフォーマンスを計測したい 積読を消化したい プログラミングに関する知識をもう一度きちんと勉強したい コードメトリクスの計測やAndroid Vitalsの活用など、メンバーが抱えている課題に対応する時間を、テックリード1on1や個別の対応時間で取り組むことができました。それにより、メンバーが得た知識や解決経験をテックブログへの投稿やDroidKaigiでの登壇に繋げることが出来ました。積読の消化やプログラミング知識に関する相談は次に紹介をする、読書会を開催するようにしていきました。 読書会 私たちのチームは多様なバックグラウンドを持つメンバーで構成されています。未経験からスタートしたエンジニア、他業種から転職してきたエンジニア、そして経験豊富なエンジニアと、そのレベルと経験は様々です。その一方で、リモート環境下では自然と得られるメンバー間での学びの機会が減少し、特に未経験者や他業種から来たエンジニアは基礎的な知識や技術を学ぶのに苦労していました。 ZOZOでは 書籍購入支援 の制度があり電子書籍での購入も認められています。制度を利用し皆で本を購入し読書会を行うことにしました。 読書会で取り上げる書籍は、参加者全員が理解を深められるような基礎的な内容から、より高度で技術的な話題まで幅広く選んでいます。この読書会の目的は、メンバー全員の技術力向上と知識の共有です。未経験者や他業種からの転職者は基礎を固め、経験者は新たな視点を得ることができます。また、それぞれの学びを共有することで、チーム全体としての知識も向上します。 これまでに、『Clean Architecture』や『読みやすいコードのガイドライン』、そしてUML関連の本を取り上げてきました。特に『Clean Architecture』は、アーキテクチャに興味のあるメンバーには好評でしたが、SOLIDの原則以降は難易度が高く、チーム全員が内容を把握するにはハードルが高いと感じました。 一方で、『読みやすいコードのガイドライン』はコスパがよく、コードの品質改善に大きく貢献しました。読書会後にPull Requestを見ていると、メンバーがコードにコメントを書く量が全体的に増えたことからもその影響が見て取れました。 技術的な議論においても明確な根拠を示せるようになりました。具体的には、Pull Requestのレビューの際に、『読みやすいコードのガイドライン』や『Clean Architecture』など、読書会で取り上げた本から出典を示すことが容易になりました。このことにより、指摘の際の説明負荷が軽減し、コミュニケーションがより円滑になりました。 歴史的経緯があるアプリのアーキテクチャ整理へのアプローチ ZOZOTOWNのAndroidアプリは、リリースが2012年5月、現在のコードベースの初回コミットが2015年2月2日と、かなりの歴史があります。長い間に渡って開発が続けられてきた結果、異なるアーキテクチャの思想や実装上の思想が混ざり合い、保守コストが高まり、現在採用しているアーキテクチャや実装方針が見えにくくなるという課題が生じました。 ZOZOTOWN Androidチームはメンバーも多く、同時並行で稼働している案件もある程度の数があります。その為、自分一人でアーキテクチャの選定、メンバーへの普及や教育等を実施するのは難しいと考えました。そこで、将来テックリードになる可能性のある候補者と現テックリードでアーキテクチャについて議論を深めるアーキテクチャ座談会を週に1時間開催しています。 座談会の結果、新しいアーキテクチャとしてGoogleの Modern Android App Architecture (以下、MAD)に従うことを決めました。さらに、その中で、ZOZOTOWNのAndroidアプリがMADに対して持つ独自の部分は何かという問いについて明確化できました。 具体的には、MADではRepository間の依存を認めていますが、ZOZOTOWN Androidではこれを認めずUseCaseを使用すると決めました。他にも決めたことに関してはガイドライン化してドキュメントとしてメンバーが確認出来るようにしました。 また、アーキテクチャ座談会でメンバーを増やしたことで、新しいアーキテクチャの導入に伴う問題点を早期に把握できました。座談会の参加者がそれぞれの担当案件ごとに新しく決めたアーキテクチャを採用することで、その問題点や運用上の問題が明確になりました。 具体的な問題点としては、新旧の実装方針が混在してしまうケースや、同様の責務を持っているが別の名前を付けられているような命名規則の問題が発生しました。原因分析もアーキテクチャ座談会の中で実施できました。結果として原因は、新しい実装方針がチームに十分に浸透しておらず、「なぜ新しい方針に変えるのか」という理由が共有されていなかったと結論づけました。 この記事の執筆時点ではまだ実施が出来ていない状況なのですが、対策として各案件にアーキテクチャの責任者を割り当て、設計やレビューを通じて案件メンバーにアーキテクチャを浸透させる計画をしています。 ネーミングセンスを鍛える会の取り組み ネーミングは必ずしも1つの正解があるわけではなく、各メンバーの感性や背景、経験が反映されます。そのため、同じチーム内でもネーミングやインタフェース定義にはばらつきが生じてしまうことがありました。 この課題を解決するために、「ネーミングセンスを鍛える会」を実施してみました。この会の運営は、AIベースの言語モデルであるChatGPTを用いて行いました。具体的な手順は次の通りです。 ChatGPTを用いて様々なお題を作成します。具体的なプロンプト例は後述します。 そのお題をSlackに投稿し、チームメンバー全員に共有します。 メンバーはそれぞれの解答をSlackに投稿します。 投稿された解答に対して、ChatGPTを用いて添削をします。 実際のSlackでのやり取りはこんな感じでした。 この活動は初めての試みでしたが、参加者からは「面白い」という声が上がり、また「ハードルが低くて参加しやすい」との評価も得ることができました。しかし、この活動はSlack上で自由に参加できる形式を取っていたため、積極的に回答を投稿するメンバーと、そうでないメンバーとの間に差が生じてしまいました。その結果、活動はいつしか自然消滅してしまいました。 実際に使用したプロンプトは下記のとおりです。 制約事項を元に関数の仕様を決め、説明文を生成し出力の書式に沿って生成してください # 制約事項 - 関数は1件とする - 出力は要約と説明文のみにする - 説明文は処理内容の重要な点のみを記述する - 説明文は箇条書きで出力すること - 関数名は出力しない - 説明文はですます調とする - 関数は循環複雑度が3〜5程度の内容とする # 出力 ## 要約 {機能の要約を出力する} ## 説明文 {箇条書きで機能の説明文を出力する} ## 使用例 入力:{ここに関数への入力値が入る} 出力:{ここに関数の出力値が入る} 案件への関わり方 『エンジニアのためのマネジメントキャリアパス』ではテックリードの定義は次のように紹介されていました。 (ソフトウェアの)開発チームに対する責任を担い、最低でも自身の職務時間の3割はチームと共にコードを書く作業に充てているリーダーのこと。 ZOZOTOWN Androidは複数案件が同時並行で走り、かつメンバーも全体で10人以上の規模です。チームとともにコードを書くためには自分は案件に専任で入るのではなくチーム全体に横断的な関わり方をするようにしました。具体的には次のような関わり方をしました。 横断的なコードレビュー 横断的に使う機能の実装 横断的なコードレビュー ZOZOTOWN Androidチームでは、各メンバーがアサインされた案件内でPull Requestを相互でレビューするという体制をとっています。私自身はテックリードになってからは特定の案件にはアサインされない体制にし、時間が許す限りほぼ全てのPull Requestを確認していました。しかし、成長するチームとともにPull Requestの量も増え、全てを見るのが難しくなってきました。 そこで、私の役割を再定義し、重要案件の立ち上がり時のPull Requestや自分の興味があるPull Requestなどで優先順位を付けて確認するように変更しました。これにより、効率的に重要な箇所を見ることができるようになりました。 また、過去のアーキテクチャでの実装箇所やリファクタリング対象で今後は新規実装しない方針のモジュールに変更があった場合、その確認を促すようなBotを作成し導入しました。具体的には、Pull RequestのDiffに該当するファイルがあった際にBotが自分にメンションを投げ、該当のPull Requestを確認するように促します。これにより、重要なモジュールの変更確認が漏れることなく、品質を確保できています。 1年間のPull RequestをGitHubの活動を見える化する Findy Team+ で確認してみたところ、ほぼ全員のPull Requestを確認していました。 横断的に使う機能の実装 横断的な機能の開発と整備を実施しました。具体的には、Loggerの統一、Feature Flagの導入、そしてGitHub Actionsの整備などを行いました。 テックリードが横断的な機能開発に関与する理由は、各案件で横断的に使う機能を実装すると、その実装が特定の用途に過度に特化してしまう可能性があると考えたからです。この特化は、その機能の汎用性が低下する可能性を生み出します。そのため、テックリードがチーム全体を見渡して汎用性の高い実装をすることで、チーム全体の効率と品質を向上させることを目指しました。 今回は具体的な例としてLoggerの統一を紹介します。 ZOZOTOWN Androidではデータを集めたい複数の部署や開発の効率化のためなどの理由で複数のLoggerが使われています。そのため、ログを仕込む箇所での実装も煩雑になっていました。この問題を解決するために、用途ごとに違う色々なLoggerを統一的に使用できる新しいLoggerを設計、実装しました。 結果として次のような実装をしました。 // Logのパラメータ interface LogEvent { // Log送信を行なった画面の情報 val screenName: String ? // interfaceのデフォルト実装を利用することで未実装時はnullを返し、nullを返した場合はZozoLoggerは対応するLoggerに対して送信は行わない val parameterA: ParameterA? // LoggerA用のパラメータ get () = null val parameterB: ParameterB? // LoggerB用のパラメータ get () = null val parameterC: ParameterC? // LoggerC用のパラメータ get () = null } // 統一的に使えるLogger object ZozoLogger { // 各種ロガー private var loggerA: LoggerA? = null private var loggerB: LoggerB? = null private var loggerC: LoggerC? = null // App.kt等でロガーをBindする fun bind( loggerA: LoggerA, loggerB: LoggerB, loggerC: LoggerC, ) { this .loggerA = loggerA this .loggerB = loggerB this .loggerC = loggerC } // ログの送信処理 @JvmStatic fun log(event: LogEvent) { event.parameterA?.let { param -> loggerA.send(param) } event.parameterB?.let { param -> loggerB.push(param) } event.parameterC?.let { param -> loggerC.post(param) } Log.d(event.screenName, event.toString()) } } ZozoLoggerはSingletonにしてHiltでDIする方法も考えました。しかし、ログ送信箇所が古い実装であったりDIが難しい場合に、使えない可能性を考慮しobjectで実装することを選択しました。 次のようにLogEventインタフェースの実装で各Logger用のパラメータを生成することで、各種Logger毎にパラメータを用意して別々にLogを送信する必要性をなくしました。 data class MyLogEvent( override val screenName: String , private val data1: Data, private val data2: Data, ) : LogEvent { // 送信を行いたいLogger用のパラメータだけを実装する override fun parameterA() = ParameterA(data1.value) override fun parameterB() = ParameterB(data1.value, data2.value) // parameterCについては実装していないのでLoggerCには送信されない } 実際にログを送信する際の実装は以下のような形になります。 class MyViewModel() : ViewModel() { fun logic() { // 何か処理をする // ログの送信処理 ZozoLogger.log( MyLogEvent( screenName = "my_screen" , data1 = Data1(...), data2 = Data2(...), ) ) } } このような実装により、新たなLoggerの追加や削除、さらには1つのLoggerで送られていなかったイベントを別のLoggerで送るといった要件変更が容易になります。この変更は送信処理の修正を必要とせず、各LogEventの修正だけで対応可能となり変更コストを下げることが出来ました。 まとめ 私たちのチームでは、チームの成長を実現するために、様々な改善活動を積極的に試行しています。私たちは皆で提案を歓迎し、改善を推進する文化を育ててきました。 その結果、私たちは常にチームの改善に向けて努力を続けることができています。しかし、まだ改善できる点はたくさんあり、私たちは常に更なる成長を目指しています。 また、私たちのチームでは、マネジメントラインとは別にテックリードが存在しています。これにより、テックリードはチームへの技術的なサポートに専念でき、チームの技術力や生産性の向上に集中できています。 一方で、会社としてテックリードに期待されている全社横断の活動については、まだ十分に取り組めていないと感じています。今後は、チーム内で培った知識や経験を、会社全体で共有し、全社の成長に寄与することを目指していきます。 以上の取り組みを通じて、私たちはチームとしての成長を促進し、より良いアプリを提供することを目指しています。これからも私たちは、新たな試みを恐れず、チーム全体での技術力の向上に向けて邁進していきます。 最後に ZOZOではAndroidエンジニアを募集しています。ご興味のある方は下記リンクからぜひご応募ください。 hrmos.co
アバター
はじめに こんにちは、ZOZOTOWN開発本部リユースシステムブロックの西山です。最近の癒やしは飼い猫のお腹に吸いつくことです。普段は買い替え割サービスにおけるバックエンドの開発や運用保守を担当しています。 買い替え割サービスのデータベースはRDS for MySQL 5.7を利用していますが、2023年10月にサポートが終了するため次期バージョンへのアップデートが不可欠となっておりました。 また、サービスの成長に伴い、運用効率、可用性、耐障害性をさらに向上していくために、データベースそのものを見直す必要もありました。 この両面の課題に対応するため、RDS for MySQLからAurora MySQLへの移行をすることになりました。 本記事ではRDS for MySQL 5.7からAurora MySQL v3へ移行時と運用をしてみて気づいたことを紹介していきます。 はじめに 買い替え割サービスとは 抱えている課題 Auroraの選定理由 RDSとAuroraの違い Aurora独自の機能 課題解決 移行方法の検討 移行前後のシステム構成図 移行方法 移行手順 移行した際の気づき 移行による確認と変更 ケース1:Aurora v2を作成時デフォルトパラメータグループが存在しないエラーになる ケース2:Aurora v3の作成で失敗する(その1) ケース3:Aurora v3の作成で失敗する(その2) ケース4:別のAWSアカウントで作業時間が想定よりも長くかかった 運用した際の気づき ケース1:一時領域の不足エラー ケース2:CFnとAWS管理コンソールでAuroraの設定変更による再起動の挙動の違い ケース3:プラグインに関するエラーメッセージ ケース4:断続的に発生するコネクション関連のエラーログ ケース5:ブルーグリーンデプロイ まとめ 最後に 買い替え割サービスとは 買い替え割サービスはZOZOTOWNが提供する「買い替え割」「いつでも買い替え割」サービスの総称です。 「買い替え割」はZOZOTOWNで購入したアイテムを下取りすることで、欲しいアイテムを割引価格で購入できるサービスです。 また、「いつでも買い替え割」はZOZOTOWNで購入したアイテムを好きなタイミングでZOZOポイントに交換できるサービスです。 図1:買い替え割サービス 抱えている課題 冒頭で軽く触れましたが現状抱えている課題を再度整理しました。 RDS for MySQL 5.7.39のサポート期限が2023年10月に迫っている。 サービス成長に伴いユーザーアクセスが増えている中で、メンテナンス作業時間の短縮を求められている。 DBの肥大化により、インデックス設定などのテーブル更新作業で1時間以上かかることもありメンテナンス作業が長時間化している。 Auroraの選定理由 Auroraを選定した理由について説明します。RDSとAuroraの違い、Aurora独自の機能を調査し、課題に対してどのように対応できるかを検討しました。 RDSとAuroraの違い RDS for MySQL Aurora (MySQL) SLA 99.0%以上、99.95%未満 99.0%以上、99.99%未満 処理性能 ※1 100,000 SELECT/秒 500,000 SELECT/秒 Failover完了までの時間 60~120 秒 30 秒以内 Failover時のDB接続先エンドポイント変更 あり なし ストレージ EBS (Elastic Block Store) 仮想クラスターボリューム 最大ストレージ容量 16TiB 128TiB ストレージ領域の自動拡張 なし あり (※1)参考: 「MySQL の 5 倍のパフォーマンス」とはどんな意味ですか? 特にRDSとAuroraではストレージが大きく異なります。RDSではEBSを利用しているのに対し、Auroraでは仮想Clusterボリュームを利用しています。 そのため、RDSでストレージを拡張する場合は手動で行う必要があり、EBSを1台のインスタンス毎にアタッチするためすべてのEBSに対してインスタンスタイプの変更が必要となります。 Auroraでは仮想クラスターボリュームで3つのAZにまたがって6つのストレージノードにレプリケートされ、高可用性を実現しながら自動で拡張するためストレージ領域の管理が不要となります。 図2:RDSとAuroraのストレージの違い 参考: Is Amazon RDS for PostgreSQL or Amazon Aurora PostgreSQL a better choice for me? 参考: Introducing the Aurora Storage Engine Aurora独自の機能 Aurora独自の機能も見ていきます。 ZDP(ゼロダウンタイムパッチ)を利用することで、クライアントの接続を維持しながら5秒程度の遅延でパッチ適用が可能になる。 ストレージサイズを最大128TiBまで自動で拡張するため、ストレージサイズの管理が不要になる。 Auroraのフェイルオーバーではエンドポイントの書き換えが無いため、アプリケーション側のエンドポイントの書き換えが不要となりダウンタイムを短縮できる。 課題解決 現状抱えている課題がどのように解決出来るか見ていきます。 RDS for MySQL 5.7.39のサポート期限が2023年10月に迫っている。 次期8.0系のv3にバージョンアップすることで、サポート期限の延長及びv2からv3への移行の手間がなくなります。 v2は5.7系と互換性がありますが、今後メジャーバージョンのサポート切れになる可能性を考慮し、よりライフサイクルの長いv3が良いと判断しました。 サービス成長に伴いユーザーアクセスが増えている中で、メンテナンス作業時間の短縮を求められている。 Auroraマイナーバージョンはリリースから少なくとも12か月間利用が可能となっており、メンテナンス頻度の削減につながります。 RDSはベンダーによるコミュニティリリースに合わせてマイナーバージョンがリリースされます。2022年のマイナーバージョンアップは4,5回行われていました。 最新バージョンを利用するには、ベンダーのリリースに合わせてマイナーバージョンをアップデートする必要があります。また緊急性の高いセキュリティパッチは期限を過ぎると、強制的に再起動して適用されるため注意が必要です。 参考: Amazon RDS での MySQL のバージョン DBの肥大化により、インデックス設定などのテーブル更新作業で1時間以上かかることもありメンテナンス作業が長時間化している。 ブルーグリーンデプロイを利用することで、運用しながらテーブル更新作業を事前に行えるためメンテナンス時間が短縮可能になります。 前述の内容を踏まえてよりクラウドネイティブに設計されメリットが多く、現状抱えている課題をまとめて解決が出来るAurora v3への移行を決断しました。 移行方法の検討 移行前後のシステム構成図 移行前のRDSと移行後のAuroraの簡略化したシステム構成です。移行前からMultiAZ構成になっていますが、移行後では更に別のAZにリードレプリカを1台増やして可用性を高めています。 図3:移行前後の構成図 移行方法 今回で検討した移行方法は大きく分けて3つありました。RDSからAurora v3へ直接アップデートは出来ないため、RDS→Aurora v2→Aurora v3の順でアップデートしました。 図4:移行方法の比較 移行方法 総評 データの移行速度 作業時間 データの整合性 クラウド環境への適正 1.スナップショットから昇格して移行する ◎ ◎ ◎ ◎ ◎ 2.リードレプリカから昇格して移行する ◯ ◎ △ ◎ ◎ 3.ダンプ+リストアして移行する △ × △ ◎ ◎ 今回はデータの移行速度が早く、作業手順もシンプルでAWSが提供している1のスナップショットから昇格して移行する方針で対応しました。 参考: DB スナップショットを使用した MySQL DB インスタンスから Amazon Aurora MySQL DB クラスターへのデータ移行 移行手順 移行当日に行った移行作業の流れについて説明します。また移行作業の際にハマった点や気づいた点は「 移行した際の気づき 」に後述します。 RDSのスナップショットを取得する 当日作業に問題が発生した場合すぐに切り戻しが出来るようにRDSは残しておきます。 スナップショットからAurora v2のクローンを作成する 作成したAurora v2は作業後不要になります。 Aurora v2のスナップショットを取得する CloudFormation(以後CFnとする)のスタック更新でAurora v2のスナップショットを元にAurora v3のクラスターを作成する チームの運用ルールとしてAWSのリソースをCFnで管理しています。AuroraもCFnで管理するため論理IDを新規に用意しました。 図5:移行作業の流れ 移行した際の気づき 移行による確認と変更 移行時に行った主な確認や変更点をまとめました。 MySQL 5.7からMySQL 8.0で認証プラグインのデフォルト変更による影響の確認 MySQL 5.7では認証プラグインのデフォルトがmysql_native_passwordでしたが、MySQL 8.0ではcaching_sha2_passwordとなります アプリケーション側はPHPを利用しておりDB接続に利用するPDOは現状caching_sha2_passwordに対応してません Aurora v3では認証プラグインのデフォルトがmysql_native_passwordになっているため、移行による影響はありませんでした パラメータグループの設定値の変更と確認 Aurora専用のパラメータ追加、一部パラメータのデフォルト値変更があるためシステムに合わせた設定の調整が必要です 文字コード、タイムゾーンはアプリケーション側と合っているか確認し必要に応じて設定しました ログ周りの設定も必要かどうかを見直しました general_logに関してはAuroraのパフォーマンスに影響をあたえるため本番稼働する環境では無効にすることを推奨していました 一時領域に関するパラメータはシステムに合わせた設定が必要となります 「 ケース1:一時領域の不足エラー 」で後述します 上記以外のパラメータについては、実際のワークロードで性能が発揮できるようにチューニングしているためほとんどのパラメータ設定はデフォルト値で問題ありませんでした 参考: Amazon Aurora MySQL データベース設定のベストプラクティス Auroraではオプショングループのパラメータ設定が変更できなくなる 移行前にパラメータグループの設定を確認し、移行後で設定変更が必要なパラメータはないか確認する必要がありました チームでは監査ログの取得にMariaDB Audit Pluginを利用していました Aurora v3では監査ログを取得する機能が備わっているため必要に応じて設定を変更する必要があります 参考: Amazon Aurora MySQL DB クラスターでのアドバンストな監査の使用 監視メトリクスが追加になる Aurora v3では監視メトリクスが追加になります デッドロックなどのメトリクスの追加があるため、アラート設定を見直しました AuroraはRDSと比較しCPUやメモリ使用率が上昇するため、アラート設定を確認する必要があります ケース1:Aurora v2を作成時デフォルトパラメータグループが存在しないエラーになる 事象 「図5:移行作業の流れ」のステップ2でAurora v2のクラスターを作成する際に以下のようなエラーが発生しました。 DBClusterParameterGroup not found: 'default.aurora-mysql' 原因 Auroraを構築したことがなくデフォルトパラメータグループも存在しないため発生していると考えられます。 原因に関して調査しましたが詳細は不明でした。 対応 対処法についても調査しましたがこれといった解決策は見つかりませんでした。 公式の方法ではないですが、スナップショット復元した後リードレプリカからAurora v3を作成することでデフォルトパラメータグループが作成された状態となります。 以降はスナップショットからAurora v2のクラスターを作成可能となりました。 ケース2:Aurora v3の作成で失敗する(その1) 事象 「図5:移行作業の流れ」のステップ4でAurora v3のクラスターを作成開始後、6時間経過しても作成が完了しない。 原因 テーブルのコメントで utf8mb3 非対応の文字列が含まれているため、Aurora v3のクラスター作成時にエラーが発生していました。 エラーが出なかったため、原因調査ではMySQLバージョンなのかAuroraによる挙動の違いが原因なのかを切り分けていきました。 RDSのみでMySQL 5.7から8.0へのアップデートを行ったところ特に問題は発生せず、Aurora v2からAurora v3へのアップデートを行ったところエラーが発生しました。 Auroraのerrorログには以下のようなエラーが出力されていました。 Comment for table 'データベース名.テーブル名' contains an invalid utf8mb3 character string: '16進数の文字列'. 対応 ALTER文でテーブルのコメントを再度設定することでエラーが発生しなくなり、Aurora v3のクラスター作成が完了しました。 ケース3:Aurora v3の作成で失敗する(その2) 事象 「図5:移行作業の流れ」のステップ4でCFnからAurora v3のクラスターを作成する際に以下のようなエラーが発生しました。 The following resource(s) failed to create: [AuroraCluster]. 原因 RDSとAuroraではCFnのプロパティが異なるため、エラーが出ていました。 対応 Auroraに対応したCFnのプロパティを設定することでAurora v3のクラスター作成が完了しました。 ケース4:別のAWSアカウントで作業時間が想定よりも長くかかった 事象 チームでは検証と本番でAWSのアカウントを分けて運用しています。 本番アカウントにて本番を想定したリハーサルの実施時、予定していた時間よりも長くかかってしまい当日の作業スケジュールに影響する可能性がありました。 移行当日にテーブルの更新作業も合わせて実施予定で、この更新作業が検証よりも1.4倍もの時間がかかっていました。 原因 考えられる原因としては、今回予定していたテーブルの更新作業は型の変換であるため、テーブルのフルスキャンが発生することです。 検証アカウントでは3か月前の本番相当データ量で実施しており、3か月分のデータ差分によって作業時間へ影響した可能性がありました。 対応 時間を短縮するためインスタンスタイプの一時的な変更や、パラメータの調整などを試みましたが、フルスキャンになる型の変更の場合は時間を短縮出来ないことが分かりました。 今回はRDSからAuroraへ移行がメインでテーブルの更新作業は別日に実施する判断となりました。 現状抱えている課題にもなるので、今後はブルーグリーンデプロイを活用することでメンテナンス時間の短縮を目指して行きたいと考えています。 運用した際の気づき ケース1:一時領域の不足エラー 事象 リーダーインスタンスに対してクエリを実行した際に以下のようなエラーが発生しました。 [ERROR] [MY-013132] [Server] The table '/rdsdbdata/tmp/#sql294_800_1' is full! (handler.cc:4388) 原因 リーダーインスタンスとライターインスタンスでメモリ枯渇の挙動が異なり、今回はリーダーインスタンスのメモリ枯渇でエラーとなりました。 下記、参考URLのAppendix(Aurora MySQL 3.0)フロー図を参照するとリーダーインスタンス/ライターインスタンスでエラーになるまでのフローが確認できます。 参考 : Use the TempTable storage engine on Amazon RDS for MySQL and Amazon Aurora MySQL 対応 クエリ実行後にTempTableの合計サイズを確認し、 temptable_max_ram か temptable_max_mmap の値を調整する必要があります。 今回は temptable_max_mmap をTempTableの合計サイズより大きい値に設定することで解決しました。memory-mapped fileは作りすぎたとしても実体はメモリではないのでOOMのリスクはなく、あるとすればストレージ枯渇が考えられます。 temptable_max_ram に関しては値を誤ると メモリを使い切りライターのプロセスが落ちる 可能性もあるため設定を慎重に行う必要があります。 ケース2:CFnとAWS管理コンソールでAuroraの設定変更による再起動の挙動の違い 事象 Auroraのリネームや動的パラメータ変更などAWS管理コンソールから変更した時、Auroraを再起動せずに変更した値の反映が可能です。 しかし、再起動が不要な変更をCFnからスタック更新すると再起動される事象を確認しました。 原因 原因について調査してみたところCFnによる仕様になっておりました。 参考: AWS::RDS::DBClusterParameterGroup 対応 CFnからスタック更新する際は、Auroraが再起動される可能性を想定し事前に確認する。 または、AWS管理コンソールから変更を行い後追いでスタック更新すると再起動なしで更新が可能になります。 パッチ適用で検証した際は再起動されずにスタック更新が終了することを確認しました。この時、スタック更新の挙動はパッチ適用済を確認してスタック更新を終了しました。 ケース3:プラグインに関するエラーメッセージ 事象 移行後にAuroraのerrorログで監査のプラグインに関するエラーログが出力されていました。 [ERROR] [MY-010901] [Server] Can't open shared library '/rdsdbbin/oscar-8.0.mysql_aurora.3.xx.0/lib/plugin/server_audit.so' (errno: 0 /rdsdbbin/oscar-8.0.mysql_aurora.3.xx.0/lib/plugin/server_audit.so: cannot open shared object file: No such file or di). 原因 こちらに関して調査しましたが原因に関する詳細は不明でした。現行の運用に影響が出ていないため、早急な対応は必要ないと判断しました。 対応 エラーとなっているプラグインを削除することで解消できそうだと考えましたがrootでは権限がないため実行できませんでした。 AWSで管理する権限でのみ操作が可能なため、今後のアップデートで解消することを期待しています。 ケース4:断続的に発生するコネクション関連のエラーログ 事象 Auroraのerrorログで以下のようなエラーログが断続的に出力されていました。 Aborted connection 1234 to db: 'unconnected' user: 'rdsadmin' host: 'localhost' (Got an error reading communication packets) 原因 こちらに関して調査しましたが原因に関する詳細は不明でした。 対応 運用に影響はないので、今後のアップデートで解消することを期待しています。 ケース5:ブルーグリーンデプロイ Auroraではブルーグリーンデプロイの機能を提供しています。 ブルーグリーンデプロイは、クラスターを2つ用意し、1つのクラスターで運用しながらもう1つのクラスターで新しいバージョンのAuroraを構築します。テストを行い問題がなければ切り替えるというものです。 オンラインDDLでロックを取得せず運用中にスキーマ変更する機能もありますが、型変換などロックが取得されるものに関しては、一度メンテナンスしてからスキーマ変更する必要があります。 またDBが肥大化しているため実行時間が1時間以上かかるテーブルもありメンテナンス時間が長時間化してしまいます。 そこで、ブルーグリーンデプロイを利用することでメンテナンス時間の短縮が期待できます。まだ検証段階で事例がないので今後ナレッジが蓄積してきたらまた記事にしたいと思います。 まとめ RDS for MySQL 5.7からAuroraへの移行に関する事例について、移行の流れや、移行作業、運用中のケーススタディを紹介しました。 検証やトライアンドエラーを繰り返し、リハーサルテストを重ね当日は無事に本番導入でき、リリース後も特に大きな問題なく稼働しています。今後も買い替え割サービスをはじめとしたリユースシステムのサービスをより良くしていくためにも、リプレイスや機能開発など進めていきたいと考えています。 最後に リユースブロックは「買い替え割」や「いつでも買い替え割」など、買取した中古商品を販売しているZOZOUSED事業に関するサービスの開発チームです。 現在はリプレイスプロジェクトが進んでおり、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
こんにちは。FAANSブロックiOSチームの加藤です。 日本時間の6月6日から10日にかけて WWDC23 が開催されました。 WWDC23では、空間コンピュータ「Apple Vision Pro」を始め、iOS 17、SwiftDataなどワクワクする発表が目白押しでした。 今年は去年と同様に、抽選に当選すれば現地で開催されるApple Parkのパブリックビューイングにも参加できました。ZOZOからは2名が当選して、現地に赴きました! 本記事では、WWDC23におけるZOZOのiOSアプリ開発メンバーの取り組みについてご紹介します。また、オフライン参加メンバーによる現地レポートや、ラボ、セッションに参加して得られた知識も可能な範囲で公開します。ぜひ最後までご覧ください。 WWDCについて 現地で楽しむWWDC23 6月4日 - イベント前日 6月5日 - イベント当日 パブリックビューイング Meet the Teams ZOZO×WWDC23オンライン Activities Labs & Sessions Swift open hours lab & Xcode open hours lab 今年のDemystifyセッション Apple Vision Proの発表に寄せて まとめ さいごに WWDCについて WWDC(Worldwide Developers Conference)は、Appleが年に1度開催している開発者向けのカンファレンスです。OSのアップデートをはじめ、開発環境周りの新機能が発表されます。今年は昨年に続いてハイブリッド開催であり、ZOZOの当選したメンバーは業務の一環として現地参加しました。また、オンラインで参加したメンバーは、セッションやラボ、アクティビティに参加しました。 現地で楽しむWWDC23 こんにちは、計測アプリ部の永井とフロントエンド部でWEARのiOSを担当している山田です。昨年に引き続き、今年もオフライン/オンライン同時開催となり、私たちは現地で参加してきました! 現地参加のスケジュールは以下の通りです。 日付 時間(Pacific Time) コンテンツ 場所 6月4日 3:00 PM Early Check-in Infinite Loop 6月5日 8:00 AM Check-in Apple Park Visitor Center 10:00 AM Keynote Apple Park 1:30 PM Platforms State of the Union Apple Park 3:00 PM Meet the Teams Apple Park - Caffè Macs 5:30 PM Apple Design Awards Apple Park 6月4日 - イベント前日 まず、私たちはEarly Check-inを済ませるため、Infinite Loopへ向かいました。Early Check-inを行うことで、イベント前日にネームカードや参加記念品をもらうことができます。列に並んで待っていると、Appleのスタッフが「ダブダブ!」「ディーシー!」のコールで盛り上げ、イベント前日から既に気合が入っていました。 Early Check-inを済ませた後、参加記念品をいただきました。今年は、トートバック、帽子、水筒、ピンバッジでした。ピンバッジの種類は、Mac OSのレインボーカーソル、アップルのロゴ、絵文字など様々でした。絵文字は人によって異なっていました。 Infinite Loopは多くの人で賑わっており、参加者同士がドリンクや軽食を楽しみながら交流していました。広場の芝生には椅子とパラソルが配置され、くつろぎながらおしゃべりできる雰囲気が漂っていました。 会場では、DJが高いBPMの曲で盛り上げたり、ビッグスケールのジェンガやパズルゲームが行われるなど、非日常的な雰囲気が広がっていました。さらに、そこでAppleの関係者と写真を撮るなど、WWDCのムービーでしか見たことのないような楽しい時間を過ごすことができました。 イベント前日はここで終了です。さあ、明日はどんな日になるのか。 6月5日 - イベント当日 イベント当日。私たちは、チェックインが始まる時間より1時間も前にVisitor Centerへと向かいました。それは、Keynoteを最前列で視聴するため、そして歴史的瞬間を誰よりも前で目撃するためです。しかし、驚くべきことに、そんな私たちよりもさらに早くに来て、チェックインの待機列に並んでいる猛者が20名ほどいました。 待機列に並び、重いまぶたを擦りながら待っているとスタッフがコーヒーと朝食を配ってくれました。前日に引き続き、デベロッパーに対する素晴らしいホスピタリティに感動しました。 待機列に並ぶデベロッパーたちと今年はどんな発表がありそうかと話しているうちに、チェックインの時間となりました。 はやる気持ちを抑えながらチェックインを済ませ、道を進んでいくと、そこには象徴的なドーナツ型のオフィスがありました。近未来的なデザインのオフィスと、それを取り囲む大自然が、不思議と調和していて芸術的でした。 おっと、気を奪われてはいけません。そこからは脇目も振らず、パブリックビューイング会場を目指し、無事に最前列の席を確保できました。早起きしたかいがありました。 今年のパブリックビューイング会場には、巨大な屋根が建て付けられていました。去年の参加メンバーからは日差しが照りつける会場だったと聞いていましたが、今年は去年を踏まえた改善がしっかりとされていました。 パブリックビューイング 席を確保したのち、Caffè Macsで朝食を食べていると、あっという間に時間は過ぎて、Keynoteの時間となりました。 去年と同じく、Keynoteの幕開けとともにティム・クックとクレイグ・フェデリギが壇上に上がりました。最前列だからこそ、ティムとクレイグの存在がすぐそこに感じられました。 Keynote本編では、NameDropやStandByの発表、小島秀夫氏の登場など様々な驚きがあり、その度に会場の聴衆からは歓声が上がりました。その中でも、ひときわ聴衆を熱狂させたのはやはり、「One more thing …」のお馴染みのフレーズとともに発表されたvisionOSの登場でした。 Keynoteに続くPlatforms State of the UnionはCaffè Macsで、WWDCのTシャツを着たAppleのスタッフに交じって視聴しました。Swift macrosやSwiftDataなどデベロッパーとしてワクワクするアップデートが多く、会場からも喜びの声が上がったり、拍手が起きたりしていました。 Meet the Teams パブリックビューイングの時間が終わると、現地ではMeet the Teamsという、Appleのエンジニアやデザイナーと直接話せるイベントが催されました。 会場のスクリーンには、どのエリアに何のチームがいるのかのマップが映し出されており、興味のあるエリアに行って自由に話しかけることができました。 私たちはSpatial Computing、SwiftUI、Design、Developer Toolsなどのエリアに行って、日頃気になっていたことや今年の発表について根掘り葉掘り聞きました。その中でも特に印象的だったのはDesignのエリアです。Appleのデザイナーに自分たちのアプリを見てもらい、目から鱗が落ちるフィードバックを受けられました。そして、そのフィードバックをチームに共有して、実際に改善する動きまで繋げられました。 ZOZO×WWDC23オンライン 現地参加した2名以外のほとんどのメンバーがWWDC23にオンラインで参加しました。ZOZOでは、WWDCの開催期間中、現地に合わせて日本時間2:00〜11:00で勤務するメンバーや、通常の勤務時間で公開されているセッションの映像を視聴するメンバーもいました。 キャッチアップした情報を共有するために、毎日1回、ビデオ通話によるミーティングを行っていました。また、視聴したセッションのサマリや、ラボやアクティビティで得た情報はMiroで管理していました。WWDC終了後には、多くの情報がMiroにまとめられて以下のようになっていました。 Miroを用いたやり方はWWDC21よりZOZOで実施しているもので、下記のWWDC21参加レポートに詳しく公開しているので、よろしければこちらもご覧ください。 techblog.zozo.com Activities WWDC23では、オンラインのActivitiesとして、Q&AやMeet the presenterが実施されました。Q&Aでは、対象のトピックについて気になったことをAppleのエンジニアやデザイナーにSlackを通して質問できます。またMeet the presenterでは、セッション後にセッションの担当者にSlackで質問できます。質問に対して担当者が丁寧に回答してくださるので、セッションについての理解が深まります。 私も「Machine learning open forum」のQ&Aや「Mix Swift and C++」のMeet the presenterで質問をさせていただきました! また、オンラインのActivitiesとして「Trivia Time」が開催されました。Trivia Timeでは、参加者がWWDC23のセッション、開発ツール、Appleの歴史に関するトリビアクイズに挑戦しました。トリビアクイズでは、Xcodeの新機能に関することや、Appleの歴史に関するクイズが出題されました。クイズの参加者はSlackのスレッドで大いに盛り上がっていました! Labs & Sessions 去年に引き続き、ZOZOメンバーがラボでAppleのスタッフにお聞きしたことやフィードバックを一部紹介します。また、セッション動画の中からZOZOメンバーとして気になったものも紹介します。 Swift open hours lab & Xcode open hours lab ZOZOTOWN開発本部iOSブロックの小松( @tosh_3 )です。自分はWWDCではLabに参加して、Appleのエンジニアと話すのが好きで、今年も2つのLabに参加してきました。今年参加したLabはSwift open hours labとXcode open hours labです。 Swift open hours labでは、CombineとAsync & Awaitの連携について相談しました。Combine内でasyncMapのようなその内部でawaitできるような高階関数を作成できないだろうかというのを既存のアイデアともに持っていきました。また、そこに付随しながら、iOS 17で追加されたAPIやそれらのバックポートに対する姿勢などについてAppleのエンジニアと話しました。 Xcode open hours labでは、XcodeのPreviewの機能について質問しました。ZOZOTOWNではメインターゲットとは別に、Preview専用のターゲットを作っています。というのも、メインターゲットでPreviewを行おうとすると必ず失敗するという問題があったためです。 Appleのエンジニアにこの問題について聞いたところ、どうもlinking周りに問題があるらしく、custom linker flagを設定しているか確認されました。これは、CocoaPods側で設定されるもののようで、これが多くなることでPreviewに対して悪影響を与えている可能性があるとのことでした。こういった問題に対して直接Appleのエンジニア回答をもらえるのもLabの魅力です。 今年のDemystifyセッション こんにちは、ZOZOTOWNブロックiOSチームの森口です。 私は毎年密かにDemystifyシリーズのセッションを楽しみにしています。 古くは2015年のMysteries of Auto Layout, Part 1に始まり、2021年に Demystify SwiftUI が発表され、2022年には Demystify parallelization in Xcode builds とDemystifyから始まるセッションが続いています。 これらのセッションはAppleの技術の仕組みをより深く理解するために視聴は欠かせません。 今年のWWDCでは Demystify SwiftUI performance というセッションが発表されました。SwiftUIはシンプルなレイアウトから複雑なレイアウトまで実装できますが、プロダクトに本番導入してみるとパフォーマンスの観点で無視できない問題に遭遇する場合があります。このセッションではSwiftUIにおけるパフォーマンス問題のいくつかの原因と対応について解説しています。 ZOZOTOWNは歴史が長く続く、多くのお客様に利用されているプロダクトです。コードのモダン化を安全に進めていくことが常に課題となる私たちにとって、このセッション内容から得た知見は今後の開発に大いに役立ちそうです。 Apple Vision Proの発表に寄せて ARやVRといったXR領域のリサーチや検証などを担当している創造開発ブロックの @ikkou です。頭はひとつしかないのにVRヘッドマウントディスプレイやARグラスはたくさん持っています。さて、世界で初めて家庭用として販売されたVRヘッドマウントディスプレイの「Oculus Rift DK1」がリリースされたのは10年前の2013年でした。それから10年が経ち、ついにAppleから最初のSpatial computerである「 Apple Vision Pro 」が発表されました。 WWDC20頃から“Apple Glass”なるものが出るぞ出るぞとまことしやかに噂されていましたが、WWDC21、WWDC22と“One more thing”もないまま年月を重ねていました。その一方で他社からは続々とVRヘッドマウントディスプレイやARグラスが登場し、期待ばかりが膨らむ状況が続いていました。 そんな中で満を持して発表されたのがApple Vision Proです。今年こそ発表される確度が高いということを意識してか、競合にあたるとも考えられるMeta社は Meta Quest 3 をWWDC23の直前に急に発表しました。ARグラスのXREAL社(旧Nreal社)も 自社製品との違いを存分にアピール しています。 今回の発表を受けて、iOS開発者界隈に限らず、XR開発者界隈も非常に沸いています。3,499ドルという価格(日本円にして約50万円)はMicrosoft社の複合現実HMDである「 HoloLens 2 」の¥422,180よりも高いです。3,299ドルで販売されている「 Magic Leap 2 」に近い価格帯です。決してお安いお買い物ではありませんが、アーリーアダプター気質のある開発者は間違いなく買うでしょう。もちろん私も買います。 開発者視点では、 Unityの公式対応 が発表されたことも大きな意味を持っています。Unityの公式対応により、生粋のiOSエンジニアだけではなく、Unityを使ったXRエンジニアもこれまでの資産を生かせることになります。早速 ベータプログラム に申し込みました。 ところでAppleはSpatial computer、日本語では「空間コンピュータ」という言葉を用いていて、ARヘッドマウントディスプレイやVRヘッドマウントディスプレイといった言葉を用いていません。系譜としてはSpatial Computingという言葉を用いているMagic Leapに近い印象です。また、日本語では「没入」と訳されることの多いimmersiveというフレーズも用いています。ここには強い意思が感じられます。 そんなApple Vision Proに関連するセッションが複数用意されていたWWDC23でしたが、まず「 Principles of spatial design 」は必見です。あわせてアイトラッキングやハンドトラッキングに触れている「 Design for spatial input 」も欠かせません。空間コンピュータの名の通り、描画するのはiPhoneやiPadといった平面ではなく目の前にある空間そのものです。画面の絵作りや入力方法も大きく変わることを十分に理解する必要があります。 Apple Vision ProはUSでは来年初旬に、その他の国や地域では来年の後半より販売を開始とアナウンスされていますが、その対象に日本が入るかどうかは明示されていません。しかし、開発者向けのテスト施設である「 Apple Vision Pro Developer Labs 」をクパチーノ・ロンドン・ミュンヘン・上海・シンガポール、そして東京に開設することを発表しています。これは間違いなく日本でも発売されると言っても良いのではないでしょうか。 これからApple Vision ProのOSである visionOS の詳細が続々と発表されていくはずです。それらのリソースを頼りに日々“素振り”を続けていきたいところです。現場からは以上です! まとめ 本記事では、WWDC23の参加レポートをお伝えしました。 今年のWWDCも現地・オンラインで楽しむことができ、参加したメンバーとしては充実した5日間だったと思います。また、Apple Vision Pro、iOS 17など数多くの発表があり、アプリ開発者に限らず、たくさんの人が進化や未来を感じたのではないでしょうか。ZOZOは、WWDC23に参加して得られた知見を業務に反映して、サービスの向上に努めていきます! さいごに ZOZOでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
はじめに こんにちは。ZOZO DevRelブロックの @wiroha です。6月5日の深夜から6月6日にかけて Extended Tokyo - WWDC 2023 を開催しました。 Extended Tokyoは、WWDCのメインセッション(Keynote)をさらに楽しむためのイベントです。今年もLINE株式会社、株式会社ZOZO、ヤフー株式会社の3社で主催しました。オフライン会場は2019年以来のヤフー紀尾井町オフィスにあるLODGEです。またオンライン会場は2021年ぶりにclusterのVR LODGEとハイブリッドで開催しました! イベント内容まとめ WWDCのKeynoteは日本時間で深夜2時からです。それに合わせて本イベントも23時30分と遅い時間からはじまりました。クイズ大会、LT大会で気分を高めた後、リアルタイムでKeynoteを視聴しました。 コンテンツ 登壇者 クイズ大会 LT1:Appleの進化を楽しむための歴史の授業 新妻 広康◆ヤフー LT2:あなたの知らないWWDC現地参加の世界 〜Apple Parkへ行った僕が見た、新しいWWDC〜 荻野 隼◆ZOZO LT3:WWDC「間」を復習しよう 平井 亨武◆LINE LT4:メタバースプラットフォーム開発におけるSwiftUIの活用とTips 董 亜飛◆cluster 交流会 Keynote視聴 クイズ大会 WWDCや各社にちなんだクイズ大会でイベントスタートです! 正解した方にはノベルティが贈られました。 じゃんけんのようにクイズに回答 正解者へのプレゼント 現地中継 現地からは歓声も聞こえてきます イベント中、何度か現地参加者とビデオ通話をつないで様子を伝えていただきました。とても明るく良い天気で、日本との気候の違いを感じますね。話しているとちょうど開場がはじまり、人がドッと動き出しました! 臨場感が伝わってきます! Appleの進化を楽しむための歴史の授業 ヤフー株式会社 新妻さま www.docswell.com LT大会へと移り、新妻さまからはXcodeが生まれる前に遡って開発の歴史を紹介いただきました。AutoLayout、Swift、SwiftUIはアプリ開発の問題を解決する大きなソリューションですね。 あなたの知らないWWDC現地参加の世界 〜Apple Parkへ行った僕が見た、新しいWWDC〜 株式会社ZOZO 荻野 speakerdeck.com ZOZOの荻野からは2022年のWWDCに現地参加した体験を時系列で発表しました。会議室やミニコンテンツ、現地で盛り上がった場面やトイレまで知れるのは面白かったです。今年もZOZOから現地に参加しているメンバーがおり、写真とメッセージを共有させていただきました! WWDC「間」を復習しよう LINE株式会社 平井さま speakerdeck.com 平井さまからは1年でWWDCまでの間にあった出来事をご紹介いただきました。間に起きた出来事の中で、App Storeの価格の設定方法のアップデートと UIViewController.ViewLoading について詳細を解説いただきました。価格設定は悩ましいと共感の声が出ていました。 メタバースプラットフォーム開発におけるSwiftUIの活用とTips クラスター株式会社 董さま speakerdeck.com 董さまからはclusterでのSwiftUIの知見を発表いただきました。マルチプラットフォーム対応で毎週リリースしているのはすごいですね。タブインジケータや画像のズーム、Truncated Textの詳細な実装を解説いただきました。ARデバイスの発表に期待する声は他の発表でも出ていました。 交流会 Apple Park内の様子が気になるみなさま Keynoteがはじまるまでは交流会を行いました。登壇者も発表が終わってホッとした様子でみなさんとお話を楽しんでいました。現地とも通話をつないで今年は何があるのか聞いたりしました。 Keynote視聴 ついにスタート!! 交流を楽しんでいるとあっという間にKeynoteの開始時刻となりました! 新しい情報には「おぉー」と声が上がったり笑いやどよめきが起きたり、みなさんと気持ちを共有できる楽しさを感じました。15インチMacBook Air、M2 Ultra、iOS 17など新しい情報が盛りだくさんでしたね。何よりApple Vision Proにはオフライン会場が沸きました! すごい、使ってみたいと早速感想を分かち合いました。 最後に みなさま夜遅い時間にもかかわらずご参加ありがとうございました。WWDCの詳細をもっと知りたいと思った方はぜひ6月27日の「 WWDC23 報告会 at LINE, ZOZO, ヤフー 」にご参加ください。WWDCに参加した各社のエンジニアが、新しく発表された技術や得た知見、情報などを共有します。 line.connpass.com ZOZOでは一緒にサービスを作り上げてくれるiOSエンジニアを募集中です。ご興味のある方は以下のリンクからぜひご応募ください。 hrmos.co
アバター
はじめに こんにちは。DevRelブロックの @wiroha です。6月8日に ZOZO Tech Meetup〜ZOZOTOWNフロントエンドリプレイスの事例紹介〜 を開催しました。ZOZOTOWNを支える開発において「フロントエンドリプレイス」にフォーカスした技術選定や設計手法、設計時の考え方などを具体的な事例を交えながら紹介するオフラインイベントです。 登壇内容まとめ ZOZOのエンジニアと技術顧問の古川さんがZOZOTOWNのフロントエンドリプレイスの事例をLTとパネルディスカッション形式でご紹介しました。 コンテンツ 登壇者 Next.js を選定した ZOZOTOWN のフロントエンドリプレイス、その全体像 ZOZOTOWN開発本部 ZOZOTOWNWEB部 / 武井 勇也 オンプレミスの運用からクラウドの運用へ 〜SREのフロントエンドリプレイス裏側〜 技術本部 SRE部 / 秋田 海人 フロントエンドリアーキテクト2023 技術顧問 / 古川 陽介 パネルディスカッション 懇親会 会場の様子 Next.js を選定した ZOZOTOWN のフロントエンドリプレイス、その全体像 ZOZOTOWN開発本部 武井による発表 speakerdeck.com 武井からはリプレイスの経緯や課題点、その解決法などを発表しました。レンダリングについて詳細を熱く語り、参加者のみなさまも熱心にメモをとって聞いてくださっていました。質問も非常に多くいただきNext.jsの知見への需要を感じました。 オンプレミスの運用からクラウドの運用へ 〜SREのフロントエンドリプレイス裏側〜 技術本部 秋田による発表 speakerdeck.com 秋田からはSRE部のプロジェクト体制、リプレイスプロジェクトの進め方、インフラ構成について発表しました。ZOZOのようにセールなどで急激に負荷が高まるサービスの場合、負荷試験は重要です。自社で開発したGatling OperatorといったOSSツールを利用しているそうです。Gatling Operatorの詳細は次の記事で紹介しています。 techblog.zozo.com フロントエンドリアーキテクト2023 技術顧問 古川さんによる発表 speakerdeck.com 古川さんからはフロントエンドリアーキテクトの知見を発表していただきました。リアーキテクトとはどういったものか、うまくいくかどうかの違いはとても学びになりました。アンチパターンはとてもやりがちなことが紹介されていたので、ぜひ資料をご覧ください! パネルディスカッション みなさんが気になりそうなテーマでディスカッション LTを行った3人で各テーマのディスカッションを行いました。途中「リアーキテクトを完遂するにはエンジニアだけではなく経営判断が関わってくる」といった話になります。そのタイミングでなんと弊社VPoEの瀬尾が飛び入り参加! 経営層との対話などVPoEならではの話をし、来場者からの質問にも回答しました。 今後の予定など気になる質問にも回答 最後に 登壇者の集合写真 みなさまご参加ありがとうございました。今回はオフラインで懇親会の時間を設け、たくさん交流ができたかと思います。今後もさまざまなイベントを開催していきますのでよろしくお願いいたします! ZOZOでは一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
はじめに こんにちは。DevRelブロックの @wiroha です。6月1日に Recap: Google I/O 2023 を開催しました。 Google I/O 2023 で発表されたAndroidのセッションを振り返るイベントです。LINE株式会社、株式会社ZOZO、ヤフー株式会社の3社合同でオフラインとオンラインのハイブリッドで開催しました。 登壇内容まとめ 3社の社員によるLTとパネルディスカッションを行い、その後オフライン会場では交流会を行いました。 コンテンツ 登壇者 Introduction of new in Kotlin for Android 菅野 祐馬 / ヤフー How does the Relay connect Android app development and Design? 堀江 亮介 / ZOZO Android 14’s predictive back gesture 千北 一期 / LINE Jetpack Compose Debugging to fix performance problems 谷川 悠 / ヤフー Panel Discussion 森 洋之 / ヤフー 玉木 英嗣 / LINE 堀江 亮介 / ZOZO 鈴木 航 / ヤフー 当日の発表をYouTubeで公開しましたのでぜひご覧ください。 www.youtube.com Introduction of new in Kotlin for Android ヤフー株式会社 菅野さま speakerdeck.com 菅野さまからはKotlinに関するニュースを紹介いただきました。中でもKotlinがbuild scriptのデフォルト言語になったこと、kaptからKSPへ移行が推奨されていることは重要なトピックだと思いました。Kotlin 2.0 Compilerでのパフォーマンス改善は実際に試してみたいところですね。 How does the Relay connect Android app development and Design? 株式会社ZOZO 堀江 speakerdeck.com 弊社の堀江からはRelayを使ってFigmaからComposableを生成する話が発表されました。とても便利そうです。UIに更新があった場合や、Figmaで複数のVariantを扱う場合、クリックなどのインタラクションも対応されていました。Relayは現在α版だそうで今後に期待が高まります! Android 14’s predictive back gesture LINE株式会社 千北さま speakerdeck.com 千北さまからはpredictive back(予測型「戻る」)の発表がされました。predictive backを使うと画面が少しずつスケールしてアニメーションしながら遷移するため、ユーザはジェスチャー後の移動先を理解しやすくなります。 API level 33で非推奨となった OnBackPressed を新しいAPIに置き換える必要はありますが、Activityごとに部分的な導入もできるそうで試してみてはいかがでしょうか? Jetpack Compose Debugging to fix performance problems ヤフー株式会社 谷川さまの質疑応答タイム speakerdeck.com 谷川さまからはJetpack Composeのデバッグについて発表いただきました。Android Studio Hedgehog以降、Recomposition Stateを確認できるようになりデバッグがしやすくなったそうです。Layout Inspectorを使うとComposableが再描画された回数、スキップされた回数をデバッグできていました。改善の前後のデモが非常にわかりやすかったです。 Panel Discussion パネリストのみなさま 現地の写真 パネルディスカッションは事前にmiroでまとめた資料を基に進行しました。本日司会を務めた鈴木さまは現地参加ができたとのことで、写真や動画を共有いただきました。現地に行きたくなりますね! パネリストの気になる技術やセッション 印象的だった発表や今後期待することはみなさま会話型AIの Studio Bot をあげており大人気でした。まだ日本では使えないため、対応が待ち遠しいです。 面白かったセッションや試したい機能はFlutter、Large Screen、開発ツール、アクセシビリティなどさまざまな話題があがりました。もう一度Google I/Oのページを見て気になるセッションをチェックしてみようと思いました。 io.google 最後に 登壇者の集合写真 登壇者のみなさま、発表ありがとうございました。新しいニュースでワクワクする時間を過ごせました。お越しくださった皆さま、オンラインで視聴してくださった皆さまもありがとうございました! ZOZOでは一緒にサービスを作り上げてくれるAndroidエンジニアを募集中です。ご興味のある方は以下のリンクからぜひご応募ください。 hrmos.co hrmos.co hrmos.co
アバター
はじめに こんにちは、計測プラットフォーム開発本部SREブロックの髙木( @TAKAyuki_atkwsk )です。普段は ZOZOMAT や ZOZOGLASS 、 ZOZOFIT などの計測システムの開発・運用に携わっています。およそ2年ぶりのテックブログ執筆となりました。 さて、今回はCI/CD環境やKubernetesエコシステムのバージョン更新についてRenovateを使って楽しようという話をご紹介します。 CI/CDのワークフローや実行環境、Kubernetesを運用する上で導入するエコシステム 1 の多くはコード管理されています。そして、これらについてどのバージョンを使うかをコード上で指定することが多いです。しかし、コード化はされているもののバージョン更新まではなかなか手が回らなくなっており、どうにか解消したく取り組んだ話になります。 目次 はじめに 目次 背景や課題 バージョン更新のステップ 解決に向けて ツールの導入 Renovateの導入 試験的な運用 Renovateの設定 JSON5 kustomizeのリモートファイル参照 詳細なバージョン更新を行わない 本運用を行ってみて おわりに 背景や課題 計測プラットフォーム開発本部SREブロックでは計測システムにおける実行基盤(AWSやKubernetesを利用することが多い)やCI/CD環境、監視の仕組み、その他運用で必要なツールがあります。これらのほとんどはCloudFormationのテンプレートやKubernetesマニフェスト、ビルドパイプラインの設定ファイルなどコード化して管理されています。コード化を進めていくとパッケージを利用することが少なくないと思います。具体例を挙げると、Dockerイメージ、Helmチャート、CIの実行環境、プログラミング言語のライブラリなどになります。また、パッケージを利用する際にはその名前とバージョンをコード上で指定します。 これらのパッケージは継続的に更新されていて、脆弱性の対応やバグ修正、新機能追加、パフォーマンス向上などの内容が含まれています。したがって、更新された内容を取り込むには指定するバージョンを上げていく必要があります。latestタグなどを使い最新のバージョンを使う方法もありますが、予期しない変更が入ってしまったり、別のバージョンが意図せず同時に稼動したりするため限定的に利用される方法だと思います。 このため、パッケージのバージョン更新に付いていくのが理想ですが、私たちのチームではパッケージのバージョンは一度設定したままになっているか気づいたタイミングで更新するという状態に陥っていました。この状態の問題点は、新しいバージョンを利用したいと思ってもパッケージの変更の差分が大きくなり過ぎるため、更新を適用して問題ないか判断するのに時間が掛かってしまうことです。特にパッケージの特定バージョンのサポートが切れてしまったり脆弱性が見つかったりする場合にこの問題の影響を受けてしまいます。 バージョン更新のステップ あるパッケージのバージョンを更新するには以下のステップが必要になると考えています。 更新されたことに気づく 更新して問題ないか判断する 更新を適用する バージョン更新作業はなんとなく大変だなと思われている方は私以外にもいるかもしれません。この大変さというのを考えてみると、「更新されたことに気づく」「更新して問題ないか判断する」ステップにそれぞれ大変さの要素が存在すると考えが到りました。「更新を適用する」ステップに関しては、私たちは既にデプロイメントパイプラインを整えているため変更のPRをマージさえすれば適用される状態になっています。 更新されたことに気づく そもそも更新されたことに気づかない 全てのパッケージの情報を追っていられない 気づいた人がやることになる 作業の負担が大きく属人化しやすい 更新して問題ないか判断する どう影響するか判断するのが大変 変更差分を探してきて理解する 自動テストが無いものは開発環境に反映して動作確認が必要 これらの要素が組み合わさると以下のような悪循環が発生してしまいます。図の中で示した特に大変さを感じるポイントは差分が溜まった上での更新して問題ないかの判断です。これを解消するには判断する作業自体の負担を減らすこと、更新が溜まらないようにすることが必要だと考えました。 解決に向けて ツールの導入 このような大変さに対してツールを使うことで緩和できるのではと考えました。フロントエンドやバックエンドの開発においては既に浸透しているものだと思います。ツールの具体例としては、 Dependabot や Renovate 、 Scala Steward (Scalaに特化したものですが)があります。これらのツールを用いる場合のツールと人間の作業の棲み分けを以下のようにまとめました。いくつか人間のやることは残っていますが、上記で挙げた大変さの半分以上は解消されそうです。 ステップ ツールのやること 人間のやること 更新されたことに気づく プルリクエスト(以下PRと表記)の作成、レビュアやassigneeの割り当て PRの存在を把握 更新して問題ないか判断する 変更差分のまとめ 変更差分の理解、開発環境での動作確認、更新の適用を見送る場合はPRのクローズ 更新を適用する PRのマージ さて、私たちはこのツールとしてRenovateを使うことにしました。他の選択肢としてはDependabotも候補に挙がりました。Renovateを使う判断の決め手となったのは更新検知される対象パッケージの豊富さでした。特に、CI/CDやKubernetesエコシステムに関してはRenovateに優位性が見られ、私たちの管理するパッケージがその対象範囲に多く含まれると考えました。また、社内で他の部署がRenovateを利用していたので先人からアドバイスをもらえるという点も良かったです。 docs.renovatebot.com ただ、どちらのツールもサポートされる対象が決まっているので、対象外のパッケージについては私たち自身で更新に気づいて作業する必要があります。 Renovateの導入 Renovateは GitHub App が提供されているのでリポジトリに対して設定することで簡単に導入できます。その他、GitHub Actionsを利用する方法などさまざまな導入方法が用意されていますので詳しくは以下のページを参考にしてください。 docs.renovatebot.com GitHub Appを設定すると対象のリポジトリにRenovateの設定ファイルを追加するPRが自動的に作成されます(以下画像を参照)。必要であれば設定ファイルを編集します(設定ファイルの編集はマージ後でも可能です)。このPRをマージすると導入は完了です。 もし、リポジトリ内のパッケージに更新があればRenovateによって以下のようなPRが作成されます。PRの概要には対象のパッケージが何から何に更新されるかの情報とバージョン毎のリリースノートが記載されます。PRに含まれるソースコードの差分としては、以下の例ではArgoCDアプリケーションのソースとして指定するHelmチャートのバージョンを書き換えるものとなります。 リリースノートを探しに行くのは手間になるので、このように概要欄に展開してくれるのは非常に助かります。パッケージによっては記載されないものもあるので、その場合は自分でコミットを追うなりする必要があります。 試験的な運用 1つのリポジトリに導入するのであれば先ほど紹介したような形になりますので比較的容易だと思います。しかし、私たち計測プラットフォームSREが主に管理するリポジトリは現在8つあります。その内訳はインフラ用(CloudFormationテンプレートやTerraformのtfファイルの管理)、共通ツール用、各サービスのKubernetesマニフェスト用(6つある)となっています。Kubernetesマニフェスト用のリポジトリについては以下の記事で説明されていますので良ければ参考にしてみてください。 techblog.zozo.com 最終的にはこれらのリポジトリ全てにRenovateを導入しますが、一度に行うと今まで溜まっていたバージョン更新についてのPRが大量に来てメンバーが捌ききれなくなる懸念を持ちました。ツールの導入が目的ではなく、バージョン更新を楽して継続的にやっていくことが大事と考えたのでまずは試験的に運用することにしました。具体的には、対象のリポジトリを1つとし、対応するメンバーをチームの5人中3人に絞って上手く運用できるか試しました。 また、このように更新作業を進めましょうという簡単なフローを考えて試してみました。 簡単に説明すると、まずRenovateによってパッケージの更新が検知されると新しくPRが作成され自動的にレビュアが割り当てられます。対応メンバー内で最初に確認するメンバーを決めてPRのassigneeに割り当てます。assigneeは更新内容を確認し開発環境に反映して問題ないことを確認します。 確認方法はパッケージによって異なりますが、KubernetesエコシステムであればコントローラーのPodが起動しているか、エラーログが出力されていないかを主に確認します。CIの実行環境であれば、実際にジョブを動かしてみて成功するかを確認します。 確認して問題なければ他のメンバーをレビュアに割り当ててレビューを依頼します。開発環境に反映して問題がある場合、修正等で対応するか対応できないと判断してPRをクローズする、つまりバージョンを更新しないことになります。 このような形で試験運用を行う中で、誰が一番最初に見るかという問題が残っていることに気がつきました。Renovateによって対応するメンバー全員に対してレビュアが割り当てられていることで「誰かが最初の確認をやってくれるだろう」という状態になっていました。チーム内で相談した結果、明示的に担当を決めようということになりました。当初は週に一度PRを眺めて担当を決めていましたが、これも手間だと感じたためRenovateによってランダムアサインする設定を以下のように行いました。 { " assignees ": [ " Alice ", " Bob ", " Carol " ] , " assigneesSampleSize ": 1 } また、開発環境に反映しないと確認できないパッケージについては、確認方法を社内のコンフルエンスページにまとめることでどのメンバーがアサインされても作業が進められるようにしました。試験運用を経てチームメンバーが概ね問題なくバージョン更新の運用をできるようになったので残るリポジトリにもRenovateを導入し本格的に運用を始めました。 以上の改善点を踏まえた上で、以下のフローに沿って運用することにしました。「通常のレビューフロー」について補足すると、PR作成者がレビュアを指定し、レビュア2人からapproveされればマージ可能となっています。RenovateによるPRの場合assigneeがレビュアを指定するという点のみ異なります。また、CIで変更に問題ないことを担保できれば通常のレビューフローとほぼ変わらない形になります。 Renovateの設定 「Renovateの導入」のセクションでGitHub Appを設定後に設定ファイルを追加するPRが作られると紹介しました。初期設定から追加した設定のいくつかをここでは紹介します。 JSON5 初期設定では renovate.json というファイルが作成されますが以下のドキュメントにある通り JSON5 フォーマットにも対応されています。主にコメントを書くことができるという理由でJSON5フォーマットを利用することにしました。 docs.renovatebot.com kustomizeのリモートファイル参照 私たちはKubernetesマニフェストを複数環境に対応させるため kustomize を利用しています。kustomizeではリモートに存在するディレクトリやファイルを参照できます。例えば以下のように書いて kustomize build するとURLを参照しArgoCDインストールに必要なマニフェストを展開してくれます。 apiVersion : kustomize.config.k8s.io/v1beta1 kind : Kustomization resources : - https://github.com/argoproj/argo-rollouts/releases/download/v1.5.0/install.yaml これは remote targets と呼ばれる機能の1つであるremote filesを利用しています。しかしRenovateではこちらの機能はサポートされていません。以下リンク先のremote resourcesがkustomizeのremote directoriesに対応しています。 docs.renovatebot.com 調べたところ RenovateのIssue #18986 に同じようなトピックが存在し、ワークアラウンドが提示されているのを発見しました。これを利用して以下のような設定を追加しています。 { " regexManagers ": [ // GitHub上に存在するマニフェストファイルをkustomizationで利用する部分のバージョン管理 { // kustomization.yml or kustomization.yaml " fileMatch ": [ " kustomization \\ .ya?ml$ " ] , // 例: https://github.com/argoproj/argo-rollouts/releases/download/v1.5.0/install.yaml " matchStrings ": [ " https: \/\/ github\.com \/ (?<depName>.* \/ .*?) \/ releases \/ download \/ (?<currentValue>.*?) \/ " ] , " datasourceTemplate ": " github-releases " , } , { " fileMatch ": [ " kustomization \\ .ya?ml$ " ] , // 例: https://raw.githubusercontent.com/argoproj/argo-cd/v2.7.2/manifests/install.yaml " matchStrings ": [ " https: \/\/ raw.githubusercontent.com \/ (?<depName>[^ \/ ]* \/ [^ \/ ]*) \/ (?<currentValue>.*?) \/ " ] , " datasourceTemplate ": " github-tags " , } ] } この設定によって以下のようなPRが作成されるようになりました。 詳細なバージョン更新を行わない 私たちはCIの基盤としてCircleCIを一部で利用しています。CircleCIのジョブで CircleCI Orb を利用する際に circleci/aws-cli@3 のようにメジャーバージョンのみを指定する箇所がありました。すると以下のようにバージョンを詳細化するPRが作成されました。 この変更は意図しないものだったのでPRをクローズしました。そして新しい設定を追加しました。 rangeStrategy という設定でバージョンの対象範囲を制御しています。 OrbのrangeStrategyはデフォルトでpinになっている ため明示的にreplaceを指定するようにしました。 { " packageRules ": [ { " matchDatasources ": [ " orb " ] , // NOTE: orbの場合はpinになっていて、これだとメジャーバージョンのみ指定していても詳細なバージョンに更新してしまう // これを回避するためにreplaceに設定する " rangeStrategy ": " replace " , }, ] } 本運用を行ってみて 試験運用を含めて約5か月間で77件中55件のPRをマージもしくはクローズしました。PRには例示したように、KubernetesエコシステムやCI/CDの環境に関するものが含まれています。以下のグラフはバージョン更新に関するPRの対応件数を示しています。このように、Renovate導入前はバージョン更新がほぼ手付かずになっていましたが、導入後は継続的に更新できるようになっています。 Renovateによる恩恵によって大変さの悪循環としては以下のように変化しました。まだ循環の要素としては残っていますが、更新差分が溜まりにくくなったことで作業の負担は減っています。 Renovate導入前 Renovate導入後 実際に運用してみて気づいた点を2つ紹介します。1つ目はバージョン更新に必要な情報が均一化されて確認しやすくなったことです。人間によって作成されたPRでは情報の粒度が異なる傾向にあります。RenovateによってPRが作られることで、パッケージへのリンクやリリースノートといった情報が確実に含まれるのでレビューしやすくなると感じました。 気づいた点の2つ目は、長期休暇などで一時的にチームメンバーが減ると、PRがアサインされても確認に取り掛かれないことがありました。この点については、1週間の中でバージョン更新する時間をタイムボックスとして確保することや、パッチバージョンのみの更新であれば自動的にマージすることを対策として考えています。 おわりに 計測プラットフォーム開発本部ではこの記事で紹介したような既存サービスに関わる部分での改善、新規事業の開発など、幅広い業務を担当しています。現在、私たちと共にサービスを支える方を募集しています。少しでもご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com この記事では例えば、 External Secrets Operator や AWS Load Balancer Controller 、 ArgoCD のようなミドルウェアを指します。 ↩
アバター
こんにちは、ZOZO NEXTでウェブエンジニアを担当している 木下 です。先日、弊社が運営するオウンドメディアのFashion Tech Newsにおいて、記事リストのパーソナライズを行いました。本記事ではパーソナライズ導入における、要件定義、レコメンドエンジンの比較、実装での知見や注意点についてまとめます。 fashiontechnews.zozo.com 背景 解決方法の検討 課題の分析 パーソナライズ手法の検討 レコメンド方式について サービスの比較 Amazon Personalizeの実装 実装の流れ アーキテクチャ 実装での工夫点 採用したアルゴリズム アイテムデータの更新頻度 ユーザーの識別 注意点 AWS Personalizeのサンプルリポジトリが古い データの収集には時間がかかる まとまった料金が発生する まとめ 背景 「Fashion Tech News」とは、2018年に運用を開始したZOZO NEXTのオウンドメディアです。ファッションテック領域へ挑戦を続けるZOZO NEXTが、独自の視点でファッション×テクノロジーのニュースを提供しています。 記事本数の増加や継続的なサイトの改善などを進めていましたが、以下の課題がありました。 直帰率が高い 単独の記事を目当てにした新規ユーザーが多い そのためPV数の増加には、回遊率を増やしたり、再訪問率を上げたりすることが必要でした。 解決方法の検討 上記の解決方法として、記事下部にある関連記事のパーソナライズを検討しました。 ©️Fashion Tech News 課題の分析 ユーザーの行動を分析した結果、利用するユーザーの大半は新規ユーザーであり、検索やTwitterから各記事に直接訪問することがわかりました。そこで回遊率向上のために、記事を読み終わった後にある関連記事をパーソナライズすることにしました。ユーザーに合った記事を表示することで、好みの記事が見つかるサイトであるという体験も提供でき、再訪問率の向上も期待できます。 パーソナライズ手法の検討 レコメンド方式について まずはレコメンド方式について調査し、主に以下の方式があることを把握しました。 コンテンツベースフィルタリング 協調フィルタリング それぞれ長所と短所があり、状況に応じて使い分けることが必要です。 コンテンツベースフィルタリングは、アイテムの属性情報をもとに類似度を計算することで、ユーザーの好みに合わせたレコメンドを作成します。一方協調フィルタリングは、ユーザー同士の評価値の相関関係を分析することで、ユーザーの行動履歴から類似ユーザーを見つけ、そのユーザーが好んだアイテムをレコメンドするアルゴリズムです。 今回は新規ユーザーが多いため、コンテンツベースフィルタリングを採用する方向で進めていました。一方でより様々なアイテムのレコメンドを行うために、協調フィルタリングも併せて活用できるのではと考えました。結果的に、協調フィルタリングとコンテンツベースフィルタリングを組み合わせたレコメンドアルゴリズムを採用することとなりました。 サービスの比較 3つのサービスを項目ごとに比較しました。今回比較したサービスはどれも前項で説明したレコメンド方式の両方を提供していました。 比較項目 Amazon Personalize Google Recommendations AI Algolia Recommend レコメンドアルゴリズム - 一緒に購入される - 関連 - 類似 - あなたへのおすす - トレンド - 人気 - 最近見た - ユーザーセグメント - パーソナライズランキング - 新しいアイテムのレコメンド - 一緒に購入される - 関連 - 類似 - あなたへのおすすめ - 最近見た - もう一度購入 - セール中 - 一緒に購入される - 関連 - 類似 - トレンド ハイパーパラメータの調整 ○ ○ × トレーニング頻度の調整 ○ ○ × 月間20万リクエストでの概算料金 300USD 400USD 120USD 特徴 - AWSの他のサービスと連携しやすい - モデルの最初のトレーニングに2-5日必要 - 検索機能も同時に導入可能 - UIライブラリがある - 検索機能も同時に導入可能 比較検討の結果、次の理由でAmazon Personalizeを選択しました。まずは様々な ユースケース に対応していることです。ウェブメディアでも様々なアルゴリズムが試せると考えました。2つ目はMLモデルの調整ができることです。パーソナライズを導入するのが初めてのことであり、効果検証をする中でパーソナライズの精度を調整する可能性も考えたためです。それぞれのサービスに特徴があるので、実装したい機能に応じて使い分けると良いと思います。 Amazon Personalizeの実装 実装の流れ 上記の通りAmazon Personalizeを採用しました。理解しやすいように、大まかな実装の流れを挙げます。 データセットグループ の作成 インタラクションデータ の収集 アイテムデータ のアップロード ソリューションとソリューションバージョン の作成 キャンペーン の作成 レコメンド の取得 ※個人的に理解につまづいた用語の説明を加えます。 用語 説明 ソリューション アルゴリズムやパラメータの管理 ソリューションバージョン トレーニング済みのMLモデル キャンペーン APIでレコメンドを取得できるようにデプロイ 実装の結果、以下のようにレコメンドが取得できました。左が従来通りで、右がレコメンドです。この結果は、「 「ゲーム業界が考えるメタバースとは全く異なる」日本でも話題の「AGLET」が描く戦略とビジョン 」という記事の関連記事であり、記事内容に沿ったレコメンドが確認できます。 ©️Fashion Tech News アーキテクチャ 実装に当たって主に以下のサービスを採用しました。 Amazon API Gateway AWS Lambda Amazon Personalize Amazon PersonalizeのSDKをJavaScriptで直接呼ぶことも可能ですが、AWS LambdaやAPI Gatewayを用いたアーキテクチャを採用した理由は、以下の通りです。 PersonalizeのイベントトラッカーやキャンペーンのARNを隠蔽できる API GatewayのCORSの設定によりブラウザからの不要なリクエストを除ける 実装での工夫点 採用したアルゴリズム Amazon Personalizeでは レシピ と呼ばれる、ユースケースごとのアルゴリズムが用意されています。コールドスタートの新規ユーザーを考慮し、インタラクションデータに加えてアイテムデータを利用したレコメンドを行う、 Similar-Items のレシピを選択しました。これは先述の、協調フィルタリングとコンテンツベースフィルタリングのハイブリッドを意味します。 アイテムデータの更新頻度 新規ユーザーが多いことを踏まえると、アイテムデータの類似度によるレコメンドが役立ちます。そのため新規で公開された記事もレコメンドへ反映されるよう、記事の公開に合わせて毎日再トレーニングが実行されるよう設定しました。処理は定時にGitHub Actionsで実行されます。 ユーザーの識別 ユーザーのインタラクションを記録するには、IDなどで区別する必要があります。しかしサイトにはログイン機能がないため、ユーザーを識別する手段としてGoogle Analytics 4 (GA4)のClient IDを利用しました。一方でGA4が無効になっているユーザーに対しては、従来通りの記事リストが表示されるようにします。 注意点 AWS Personalizeのサンプルリポジトリが古い AWS LambdaやAPI Gatewayの実装には、CFnの拡張機能で利便性が高い AWS SAM を活用しました。AWS SAMはCLIから操作をするのですが、 AWS Quick Start Templates というコマンドがあります。このコマンドにより様々な実装例が確認でき大いに参考になりました。 当初はAmazon Personalizeのサンプルリポジトリにある、 streaming_events というコードを参考にしていました。しかしこのコードはAPI Gatewayの書き方などが古く、大部分を書き直す必要がありました。アーキテクチャを考える上で参考になりますが、お気をつけください。 データの収集には時間がかかる ソリューションを作成するために 必要なデータ量 は、2回以上のインタラクションがあるユニークな25人以上のユーザーによる1,000回のインタラクションで、推奨は50,000回です。そのため、計画を立てる上でデータの収集期間をしっかりと見積もることが大切です。もしくは既にあるアナリティクスのデータなどを用いることができれば、早く済ませることができます。 まとまった料金が発生する Amazon Personalizeを利用する上で主に発生する 料金 は、(1)MLのトレーニングと(2)レコメンドがデプロイされている期間です。 トレーニングにはトレーニング時間(4v CPUと8GiBメモリを使用する1時間のコンピューティング性能)という単位で料金が発生します。トレーニングをする頻度は、料金も踏まえて検討する必要があります。仮に毎日10トレーニング時間を利用した場合、1か月で72USDかかることになります。 レコメンドがデプロイされている期間、つまりキャンペーンが存在する期間は料金が発生します。TPS時間という単位で料金が発生しますが、これが最低でも1時間あたり0.20USD、1か月で144USD程度かかります。 まとめ Amazon Personalizeを採用することで、関連記事リストのパーソナライズを実現しました。Amazon Personalizeは、様々なアルゴリズムを利用でき、一部の設定は微調整もできます。上記注意点に挙げたデータの収集や、サンプルリポジトリが古いことに気づくまでに時間がかかったこともあり、運用開始までに2人で約1.5か月かかりました。ただAWSに日常的に携わって詳しい方なら、1-2週間で運用開始できるのではないでしょうか。 MLの実装知識がなくても利用できるのも魅力の1つです。もちろんどのレシピを選択すべきかなどはMLの仕組みを踏まえて検討するため、その知識は必要になります。現在は結果を踏まえ分析をし、より良いレコメンドが行えるように調整しています。レコメンドの効果までお伝えできなかったのは残念ですが、実装の参考になれば幸いです。 ZOZO NEXTでは、様々な技術を取り入れUXを最大化しながらプロダクト開発に取り組んでいます。絶賛仲間を募集しておりますので、興味を持ってくださった方は以下をご確認ください。 カジュアル面談はこちらからご応募ください。 hrmos.co 募集している職種はこちらからご確認ください。 hrmos.co hrmos.co hrmos.co
アバター
はじめに こんにちは、SRE部カート決済SREブロックの伊藤です。普段はZOZOTOWNのカート決済機能のリプレイス・運用・保守に携わっています。また、チームを跨いだ横断活動としてデータベース(以下DB)周りの運用・保守・構築に関わっています。 ZOZOTOWNではSQL Serverを中心とした各種DBMSが稼働しています。本記事はSQL Serverのパフォーマンスを調査する上で進めた可視化についての取り組みをご紹介します。 はじめに 従来の方法 DMV運用の課題 Splunkによるダッシュボード化 DMVの可視化例 インストールされているServerのログ情報の送信 DatadogのDatabase Monitoringについて Database Monitoringを使用して改善した例 CPU使用率の高いクエリの検出と改善 パフォーマンスが急に悪化した場合の原因調査 今後の展望 最後に 従来の方法 以前下記のテックブログで紹介させていただきましたが、弊社では 動的管理ビュー (Dynamic Management View:以下、DMV)や拡張イベントの情報をロギングしています。 techblog.zozo.com これらの情報を用いることで何かトラブルが起こった際には後追いできる状況を整えています。 例えば特定の時間帯にクエリが滞留した際には次のクエリを実行することで、滞留していたクエリの詳細な情報を調べることができます。 SELECT collect_date, count (*) AS [クエリの滞留数(全体)] FROM [dbo].[dm_exec_requests_dump_per_several_seconds_20230502] WHERE collect_date between @start_date and @end_date GROUP BY collect_date ORDER BY collect_date ; SELECT collect_date, wait_type, count (*) AS [クエリの滞留数(wait毎)] FROM [dbo].[dm_exec_requests_dump_per_several_seconds_20230502] WHERE collect_date between @start_date and @end_date GROUP BY collect_date, wait_type ORDER BY collect_date, wait_type ; SELECT collect_date, current_running_stmt, count (*) AS [クエリの滞留数(statement毎)] FROM [dbo].[dm_exec_requests_dump_per_several_seconds_20230502] WHERE collect_date between @start_date and @end_date GROUP BY collect_date, current_running_stmt ORDER BY collect_date, current_running_stmt ; 他にも、ストアドプロシージャ(以下、ストアド)の実行統計 sys.dm_exec_procedure_stats もDumpしています。 ストアドに修正を行なった際の監視や、特定のストアドが突然パフォーマンス劣化した際などはこちらを確認することで具体的なパフォーマンスを調べることができます。 オリジナル情報は累積値となっているため、LAG関数を使用して1分前の情報と差分を取ることで1分間の実行回数や実行時間を出力しています。 SELECT object_name, collect_date, execution_count AS ' 実行回数(累積値) ' , -- リセットされていたらキャッシュアウトされた可能性あり CASE WHEN cached_time = LAG(cached_time, 1 , 0 ) OVER ( ORDER BY object_name, collect_date) THEN CONVERT (nvarchar, execution_count - LAG(execution_count, 1 , 0 ) OVER ( ORDER BY object_name, collect_date)) ELSE ' - ' END AS ' 実行回数(1分間の合計) ' , CASE WHEN cached_time = LAG(cached_time, 1 , 0 ) OVER ( ORDER BY object_name, collect_date) THEN CONVERT (nvarchar, total_worker_time - LAG(total_worker_time, 1 , 0 ) OVER ( ORDER BY object_name, collect_date)) ELSE ' - ' END AS ' CPU時間(1分間の合計) ' , CASE WHEN cached_time = LAG(cached_time, 1 , 0 ) OVER ( ORDER BY object_name, collect_date) THEN CONVERT (nvarchar, total_elapsed_time - LAG(total_elapsed_time, 1 , 0 ) OVER ( ORDER BY object_name, collect_date)) ELSE ' - ' END AS ' 実行時間(1分間の合計) ' , CASE WHEN cached_time = LAG(cached_time, 1 , 0 ) OVER ( ORDER BY object_name, collect_date) THEN CONVERT (nvarchar, total_logical_reads - LAG(total_logical_reads, 1 , 0 ) OVER ( ORDER BY object_name, collect_date)) ELSE ' - ' END AS ' 論理読み込み量(1分間の合計) ' , CASE WHEN cached_time = LAG(cached_time, 1 , 0 ) OVER ( ORDER BY object_name, collect_date) THEN CONVERT (nvarchar, total_logical_writes - LAG(total_logical_writes, 1 , 0 ) OVER ( ORDER BY object_name, collect_date)) ELSE ' - ' END AS ' 論理書き込み量(1分間の合計) ' FROM dbo.dm_exec_procedure_stats_dump WITH (NOLOCK) WHERE collect_date BETWEEN @start_date AND @end_date AND object_name = ' <絞り込みたいストアドの名前> ' ORDER BY object_name, collect_date DMV運用の課題 ロギングしたDMVは上記のようにトラブルシューティング時に役立てることができますが、周りのメンバーに対応してもらうにあたってハードルの高さを課題として感じていました。 まず、上記のSQLを作るにはDMVについて理解を深める必要があります。SREの全員がDBに精通しているわけではなく、普段SQLを書かないメンバーもいるので学習コストが必要となります。 また、DBに接続するためにはプライベートなネットワークを経由する必要があるなど一手間かかります。本番環境で稼働しているDBサーバーに対してSQLを実行する必要があるので、日常的な運用には向いていません。 SQL Server 2016からはクエリの実行履歴を保存して可視化できる クエリストア の機能も追加されました。トラブルシュートする上で有用ではありますが、上記と同様の課題や表示速度なども含めて、運用の利便性が高いものとは言えませんでした。 そのためパフォーマンス悪化の徴候があったとしても後手に周り、問題が発生してからでないと気付きにくい側面がありました。 Splunkによるダッシュボード化 DMVの課題を解決するために取り組んだのがまずSplunkによるダッシュボード化です。 弊社ではさまざまな場所でSplunkを活用しています。過去のテックブログにもSplunkに関する記事がありますので、興味のある方は是非ご覧ください。 techblog.zozo.com 今回使用したのは Splunk DB Connect というアドオンです。Splunk DB Connectではデータベースの情報を直接インポートできる他に、カスタムクエリを定期的に実行して結果をSplunkに送信できます。 Splunk DB Connect自体のインストール方法やDBとの接続方法に関しましては 公式ドキュメント をご参照ください。 DMVの可視化例 冒頭でDMVの活用例としてストアドのパフォーマンス調査を挙げましたが、まずはこちらをSplunk DB Connectで毎分実行し、Splunk側へ蓄積されるようにしました。作成したのが下記クエリです。 ストアド毎に、3分前から1分前の間にdumpとして保存された2つのレコードを取得し、差分を出力しています。SQL内のコメント文はSplunk DB Connectで設定する際に動作影響が出るため削除しています。 DECLARE @start_date DATETIME2 = dateadd(mi, -3 , GETDATE()); DECLARE @end_date DATETIME2 = dateadd(mi, -1 , GETDATE()); SELECT object_name AS ' stored_procedure ' , collect_date, exec_count_sum_1m, cpu_time_sum_1m, exec_time_sum_1m, logical_read_sum_1m, logical_write_sum_1m FROM ( SELECT object_name, collect_date, CASE WHEN (object_id = LAG(object_id, 1 , 0 ) OVER ( ORDER BY object_name, collect_date)) and (cached_time = LAG(cached_time, 1 , 0 ) OVER ( ORDER BY object_name, collect_date)) THEN CONVERT (nvarchar, execution_count - LAG(execution_count, 1 , 0 ) OVER ( ORDER BY object_name, collect_date)) ELSE ' - ' END AS ' exec_count_sum_1m ' , CASE WHEN (object_id = LAG(object_id, 1 , 0 ) OVER ( ORDER BY object_name, collect_date)) and (cached_time = LAG(cached_time, 1 , 0 ) OVER ( ORDER BY object_name, collect_date)) THEN CONVERT (nvarchar, total_worker_time - LAG(total_worker_time, 1 , 0 ) OVER ( ORDER BY object_name, collect_date)) ELSE ' - ' END AS ' cpu_time_sum_1m ' , CASE WHEN (object_id = LAG(object_id, 1 , 0 ) OVER ( ORDER BY object_name, collect_date)) and (cached_time = LAG(cached_time, 1 , 0 ) OVER ( ORDER BY object_name, collect_date)) THEN CONVERT (nvarchar, total_elapsed_time - LAG(total_elapsed_time, 1 , 0 ) OVER ( ORDER BY object_name, collect_date)) ELSE ' - ' END AS ' exec_time_sum_1m ' , CASE WHEN (object_id = LAG(object_id, 1 , 0 ) OVER ( ORDER BY object_name, collect_date)) and (cached_time = LAG(cached_time, 1 , 0 ) OVER ( ORDER BY object_name, collect_date)) THEN CONVERT (nvarchar, total_logical_reads - LAG(total_logical_reads, 1 , 0 ) OVER ( ORDER BY object_name, collect_date)) ELSE ' - ' END AS ' logical_read_sum_1m ' , CASE WHEN (object_id = LAG(object_id, 1 , 0 ) OVER ( ORDER BY object_name, collect_date)) and (cached_time = LAG(cached_time, 1 , 0 ) OVER ( ORDER BY object_name, collect_date)) THEN CONVERT (nvarchar, total_logical_writes - LAG(total_logical_writes, 1 , 0 ) OVER ( ORDER BY object_name, collect_date)) ELSE ' - ' END AS ' logical_write_sum_1m ' FROM dbo.dm_exec_procedure_stats_dump WITH (NOLOCK) WHERE collect_date BETWEEN @start_date AND @end_date AND object_name not like ' sp[_]% ' AND exists ( select * from sys.objects ob with (nolock) where ob.object_id = object_id(object_name) and is_ms_shipped = 0 ) ) AS sample WHERE NOT exec_count_sum_1m= ' - ' ORDER by collect_date, object_name 上記のクエリで収集した情報を次のようなサーチ文で可視化できます。 index = " heavy_forwarder_db " sourcetype= " dbconnect " host=XXX source= " XXX-procedure-stats " | timechart span=1m useother= false limit= 20 sum (exec_count_sum_1m) by stored_procedure 収集に使用した時間とSplunkが受信する時間に差があるので表示上少しのずれは発生してしまいますが、許容範囲としています。 また、上記のサーチ文では上位20件の情報を表示させていますが、別途テキスト入力欄を設けて特定のストアドを追えるダッシュボードも提供しています。 インストールされているServerのログ情報の送信 Splunk DB Connectとは別に、サーバーにSplunk Universal Forwarderをインストールすることでパフォーマンスモニタやイベントログを送信できます。 同一ダッシュボード内でイベントログとクエリのパフォーマンス情報を表示することで両者の相関関係を結びつけることができ、以下の切り分けが容易になります。 サーバー自体の問題なのか サーバー上で動いているSQL Serverの問題なのか SQL Server上で実行された特定のクエリの問題なのか Splunkによるダッシュボード化を行うことで、DMVやSQLに精通していないメンバーがトラブルシュートのために必要な情報を容易に取得できるようになりました。またプライベートなネットワークを経由して本番環境のDBサーバーにSQLを実行する必要がなくなり、運用の利便性と安全性の向上を実現しました。 DatadogのDatabase Monitoringについて 弊社ではDatadogも活用しています。 techblog.zozo.com 2022年8月、DatadogのDatabase Monitoring機能がSQL Serverに対してもサポートされるようになったため、オンプレミス環境の主要DBへの導入を進めました。 Datadog Agentのインストール方法については公式サイトをご参照ください。 docs.datadoghq.com データ収集は以下の理由からKubernetesクラスタ上にDatadog Agentのpodを立ててDBにアクセスする方法を採用しました。 元々RDSに接続してメトリクスを取得するための雛形を用意していたこと 何か問題があった場合にAgentのアンインストールが不要であること オンプレの場合は直接Agentをインストールした方がOSのメトリクスなど取得できる情報は増えるが別手段で収集済みであり、そこまで重要視しないこと Database Monitoringを使用して改善した例 CPU使用率の高いクエリの検出と改善 Database Monitoringを有効化することで使用できるようになる クエリメトリクスビュー ではクエリ毎のリソース使用率を見ることができます。 次の画像はWORKER TIME(CPU)順で並び替えた画像であり、特定のクエリでCPUを多く使っていることがわかります。 クエリの詳細を確認した結果が次の画像となりますが、グラフから実行時間が安定していないことがわかりました。それぞれの時間帯で記録されていた実行計画を見たところ、遅い時間帯のみ特定のテーブルでスキャンが発生していました。 問題のクエリの平均レイテンシ パフォーマンスが悪い時の実行計画 パフォーマンスが良い時の実行計画 スキャンするプランは望んでいないためFORCESEEKヒントを追加したところクエリパフォーマンスが改善し、DB全体のCPU使用率の改善も確認できました。 Database Monitoringの導入により、視野が広がったことで今まで問題視していなかった部分に対しても先回りして修正できるようになりました。手動で実施していたパフォーマンス情報の収集や キャッシュからの実行計画の収集 もクエリメトリクスビューから閲覧可能なので不要となり、作業の効率化へと繋げられました。 パフォーマンスが急に悪化した場合の原因調査 実行計画の変化や特定の負荷がかかった場合などパフォーマンスが急激に悪化することが度々ありました。その場合はDMVを利用して深掘りを行なっていましたが、最初のアクションとしてDatabase Monitoringを確認するという手段が取れるようになりました。 画像はハードウェア起因のトラブルが発生し、エラーが多発してしまった際のものです。 WriteLogのWaitが大量に発生してしまっており、何らかの要因でトランザクション書き込みが待たされていることがわかります。 緊急時には一刻も早い原因特定が求められるため、簡単に確認ができ、また対応できるメンバーを増やせることは非常に嬉しいポイントです。 今までだと問題に応じて様々なDMVを使い分ける必要があり、対応メンバーにはDMVに対する知見が必要でした。Database Monitoringを活用することで様々な角度から初期調査ができ、属人化の削減に繋げられました。 今後の展望 以上のように、SplunkとDatadogを用いてDBのパフォーマンスを可視化する取り組みを進めました。 現状の両者の使い分けとしては、Datadogによって自動でパフォーマンスを取得し、カバーしきれていない範囲をSplunkのダッシュボードにまとめています。 ただしその理由は時系列的な側面が強く、例えばDatadogでもカスタムクエリを使用したメトリクス化は実現可能であるためそれらをDatadog側に寄せていくことも可能です。 一方でSplunkは自社ではDBに関連する様々なリソースのログも蓄積されてきているため、相関的に情報を得やすいというメリットが存在します。 両者のメリットを活かしつつ、さらに最適なDBパフォーマンスの可視化戦略を今後考えていきたいと思います。 また、現状一部のDBにしか対応できていないため、他のDBに対しても同様の可視化を進めていきたいです。 最後に ZOZOでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com
アバター
こんにちは、バックエンドエンジニアの 近 です! 2023/5/11〜13に長野県にて開催されたRubyKaigi 2023でプラチナスポンサーとして協賛し、スポンサーブースを出展しました。 また、今年は我々が運営しているファッションコーディネートアプリ「WEAR」のサービス紹介CMを作成し、RubyKaigiの会場にて放映させていただきました。 technote.zozo.com technote.zozo.com 実際に放映されたCMは以下になります! www.youtube.com 我々が運営・開発しているファッションコーディネートアプリ「 WEAR 」のバックエンドはRuby on Railsで開発しています。2013年にVBScriptで作られたシステムですが、2020年頃からVBScriptのシステムをコードフリーズし、リプレイスをはじめました。現在もリプレイスを進めながら、新規の機能もRubyで開発しています。 次に、セッションの紹介とブースでの取り組み、その他RubyKaigiの様子をお届けします。 今年はバックエンドエンジニア9人の参加となったため、盛り沢山の内容となっております! エンジニアによるセッション紹介 Multiverse Ruby @tsuwatch です!  Multiverse Ruby を紹介しようと思います。このタイトルだけ見るとなにかよくわからず、もしかしたら候補から外れていた方もいるかもしれません。 内容としては、グローバルなネームスペースを利用せずにコードを共有できる Im (イム)というGemの紹介です。Isolated Module Loaderと紹介されています。Rubyはグローバルな名前空間を利用するしかなく、名前の衝突が問題になります。 解決策として、Rubyの匿名モジュールという特徴を利用して名前空間を作り出し、利用したいモジュールをロードできるというものです。 mod = Module .new mod.name #=> nil mod:: Foo = Module .new mod:: Foo .name #=> "#<Module:0x0…>::Foo" MyFoo = mod:: Foo mod:: Foo .name #=> "MyFoo" 実装としては Kernel#load の wrap オプションにモジュールを指定することで、そのモジュール配下に load できる機能の活用をしていました。また、 Module#const_added を prepend することで、匿名モジュールを命名したタイミングで処理を挟むことなどをしていました。 :: Module .prepend( Im :: ModuleConstAdded ) こういう感じで使えます。 require ‘im’ loader = Im :: Loader .for_gem loader.setup loader:: MyGem # my_gem.rbが自動的に読み込まれる こういったRubyの特徴を活かしてハックできるのがRubyの楽しいところで、こういうGemが大好きなので最高でした。 Power up your REPL life with types 天春です。去年はオンライン参加でしたが、今年は初めての会場参加でした。思った以上に楽しかったです。 Power up your REPL life with types を紹介しようと思います。 内容としては、irb 1 ではできない型分析に基づいたオートコンプリートを提供する katakata_irb というGemの紹介でした。 katakata_irb をインストールしてrequireを書くだけですぐ使えます。 gem install katakata_irb % irb irb(main): 001 : 0 > require ' katakata_irb ' => true .irbrc に以下の内容を追加しておくと毎回requireしなくても利用できます。 require ' katakata_irb ' rescue nil 今までのirbではメソッドチェーンやブロックパラメーターなどのオートコンプリートは表示されていませんでした。 katakata_irb を使うことで型に合わせてオートコンプリートが表示されるので便利ですね。 メソッドチェーンやブロックパラメーターにオートコンプリートを実現するため、何をしたかの説明もありましたが、難しくて理解はできませんでした。 簡単に説明すると Ripper を使って以下の3段階で実装したとのことでした。 1 . 不完全なコードの構文ツリーを取得する 2 . RBSを使用してメソッドチェーンを評価する 3 . IRBの完了ロジックをオーバーライドする 今回をきっかけにパーサーについて興味が湧いたのでもっと理解できるように勉強したいと思います! Developing Chrome Extension with ruby.wasm 近 です! 自分からはYuma Sawai氏の「Developing Chrome Extension with ruby.wasm」というセッションの紹介をしたいと思います。 このセッションではYuma Sawai氏が作成した、ruby.wasmを使って簡単にChrome拡張機能の開発ができるunloosenというフレームワークの紹介をしていました。 このフレームワークを作成した背景として、以下のように語っていました。 昨年発表されたruby.wasmだが、それを使って作られたアプリケーションが少ない ruby.wasmにgemsを使った記事はない ruby.wasmを使った開発環境がまだ万全ではない 開発のしやすさは、開発者の増加に繋がるのではないか 次に、unloosenのメリットとして、以下が紹介されていました。 Simple Syntax フレームワークのTopLevelにdocumentやalertなどのエイリアスを読み込んでいるため、JavaScriptの機能が簡単に使用できる Live Reload コードの変更があった際に拡張機能の再読み込みをしなくてよい Chrome拡張機能の実装に必要なファイルの管理が少なくなる Simple Syntaxについてですが、通常ruby.wasmを使ってJSライブラリを呼び出す場合、以下のように記述します。 JS .global[ :document ][ :body ][ :style ][ :backgroundoColor ] = JS .try_convert( ' red ' ) unloosenではRuby上でJavaScriptのようにコードを書くことが可能となっています。 document.body.style.backgroundColor = ' red ' また、unloosenの使い方も簡単で、以下の手順にてインストール・実装が可能となっています。 unloosen-ruby-loaderという起動用スクリプトをnpm installする インストールした起動用スクリプトをChrome拡張機能用の設定ファイルであるmanifest.jsonにて指定 これにより、ruby.wasmとunloosen本体が読み込まれる メインの処理を記述するapp.rbファイルを作成し、実装する あとは、app.rbをunloosenが読み込み、ruby.wasmにて実装される 自分も実際にunloosenにてChrome拡張機能を実装してみましたが、ruby.wasmを使った実装周りで躓いた箇所は少しあったものの、比較的簡単に作成できました。 皆さんも是非試してみてください! 今回紹介したスライドは以下になります。 speakerdeck.com Learn Ractor 笹沢( @sasamuku )です。趣味はポケモンカードです。私からはMasatoshi Seki氏による Learn Ractor をご紹介します 2 。恐らくはRubyKaigiでポケモンカードに触れていた唯一の発表でした。 発表は「Ractorの紹介」と「ケーススタディ」の2部構成でした。前半ではサンプルコードとともにRactorの次のような特徴が取り上げられました。 Ractor.new に渡すブロック内では外部の変数(グローバル変数含む)にアクセスできない Ractor.new の引数経由であれば外部の変数を渡せるがディープコピーになる Ractor.make_shareable で外部の変数を同一オブジェクトとして Ractor.new の引数に渡せる 3 つまりRactor間でのオブジェクトの共有は基本的にできません。これはRactorがスレッドセーフな並列処理を簡単に書くことを志向しているためです。 手元でも動作を確認してみました。 # `Ractor.new`に渡すブロック内では外部の変数(グローバル変数含む)にアクセスできない a = " hoge " #=> "hoge" Ractor .new { puts a } #=> <internal:ractor>:267:in `new': can not isolate a Proc because it accesses outer variables (a). (ArgumentError) ... # `Ractor.new`の引数経由であれば外部の変数を渡せるがディープコピーになる Ractor .new(a) { |x| puts x } #=> hoge a.object_id #=> 1587220 Ractor .new(a) {|x| puts x.object_id } #=> 1668420 # `Ractor.make_shareable`で外部の変数を同一オブジェクトとして`Ractor.new`の引数に渡せる Ractor .make_shareable(a) #=> "hoge" a.object_id #=> 1587220 Ractor .new(a) {|x| puts x.object_id } #=> 1587220 # 当然ではありますがSymbolなどのイミュータブルなオブジェクトは例外でした b = :hoge #=> :hoge b.object_id #=> 3036508 Ractor .new(b) {|x| puts x.object_id } #=> 3036508 後半ではRactorを活用した高速化事例としてSeki氏が運営されるポケモンカードのデッキ解析サイトが紹介されました。サイトの機能の1つに、デッキの類似度を計算してクラスタリングすることで、ある週のデッキの分布、つまり流行っているデッキを可視化できるというものがありました。デッキの類似度を週ごとに計算する処理をRactor化することで40%ほど処理速度を改善していました。 笹田氏による "Ractor" reconsidered では、Ractorの普及状況が嘆かれていましたが、こうした実例が増えていくことで利用者が増え性能向上のサイクルが回り始めるのだと理解しました。私もこれからはRactorを使った高速化ができないか常に目を光らせていこうと思います。 発表の最後にSeki氏が「今日はデッキを持ってきています」と話されていたのですが、生憎私は持ってきておらず後悔しました。来年は持っていこうと思います! Revisiting TypeProf - IDE support as a primary feature 小島です。私からは 「Revisiting TypeProf - IDE support as a primary feature」 の発表を紹介します。 TypeProfは型注釈のないRubyのコードを型解析してくれます。今回の発表ではこのTypeProfのv2の紹介でした。 発表では、初めに現在のTypeProf v1の課題として型推論だけでは開発者体験の向上に不十分であったとし、TypeProf v2ではIDEサポートをゴールとして開発していると述べていました。TypeProf v1はIDEサポートを考えて作られていなかったこともあり、型解析の速度が遅く、TypeProf v1でそのままIDEサポートを実現することが難しかったようです。そこで、大幅なパフォーマンス改善をすることで、IDEサポートを目標としてTypeProf v2を開発しているとのことでした。 パフォーマンスの改善度合いは数値でも示されていました。TypeProf v1では解析に約3sec掛かっていたところを、v2では初回の解析で約1.003sec、コード編集ごとの追加解析では約0.029secで解析が完了するようでした。 発表ではデモがあり、メソッドに入れる引数の値によって即時に型が推論されVSCode上に表示されるところや、型が間違っている値を代入しようとした場合に警告が出るところなどをデモで見ることができました。 デモを見た感想としては、タイムラグなく型が推論されて表示されておりとてもストレスなく開発できそうでした。 最後に、今回紹介したTypeProf v2はRuby 3.3までに利用可能にすることを目指しているようです。楽しみですね! 今回紹介した発表資料のリンクは以下になります。 speakerdeck.com Ruby + ADBC - A single API between Ruby and DBs 伊藤です。私は今年初めてRubyKaigiに参加しましたが、内容が幅広く、興味深いセッションばかりでした! 私からはSutou Kouhei氏による Ruby + ADBC - A single API between Ruby and DBs を紹介させていただきます! このセッションでは、 A rrow D ata b ase C onnectivity (ADBC) を用いてRubyでも大量のデータを読み書きしようという試みを紹介されていました。 既にEmbulkがあるのではと考えた方もいらっしゃるかと思いますが、Embulkは(J)Rubyのサポートを徐々に縮小していく計画だと発表しています。そこで、Embulkとは異なるアプローチとして、ADBCを用いてみようとのことです。 ADBCは、以下の特徴を持っています。 各種DBにアクセスするための共通API ActiveRecordやSequelも同様 多言語対応 ActiveRecordではRubyでAdapterを実装する必要があるが、ADBCでは他の言語で実装されたAdapterも使える 大きな列指向データに最適化 高速で大量のデータを処理できる Apache Arrow データフォーマットに特化 並列処理が可能 ADBCは大量のデータの読み書きが得意とのことですが、実際どのくらい早いのか気になりますよね? セッション内で紹介されていました! Sutou氏の実測によると、整数値カラム1つだけのテーブルからレコードをSELECTする場合、1000万レコードを参照する際にlibpqの2倍の速度が出るようです。 ただし、libpqの2倍の速度が出るのは Apache Arrow Flight SQL というプロトコルを用いた場合で、libpqをドライバーとして用いた場合はADBCの方が現時点では遅くなるようです。 Apache Arrow Flight SQLとは、Apache Arrow Flight上でSQLを使えるようにしたもので、以下の特徴を持っています。 Arrowフォーマットを使った高速RPCフレームワーク データ交換コストが低い 並列転送 ストリーム処理 Apache Arrow Flight SQLを用いれば、ADBCが高速になるとのことでした。 Apache Arrow Flight SQLを用いると高速になることはわかりましたが、PostgeSQLはApache Arrow Flight SQLを使えるのでしょうか? なんと、Sutou氏は Apache Arrow Flight SQL adapter for PostgreSQL を開発されていました! PostgreSQLでApache Arrow Flight SQLを使用するためのAdapterです。このプロダクトが実用的になると、ADBCを使ってPostgreSQLから高速に大量データを取り込んだり取り出したりできるようになるとのことです。 また、RubyからADBCにアクセスするためのAPIはありますが、ActiveRecord用のAdapter( Active Record ADBC adapter )の開発も始められたとのことです! Ruby on Railsを使用している身としては非常にありがたいです。 まとめますと、以下のような内容でした。 ADBCを使うとRubyで高速に大量データを読み書きできる PostgreSQLでApache Arrow Flight SQLを使えるようにする Apache Arrow Flight SQL adapter for PostgreSQL を開発中 ActiveRecord経由でADBCを使えるようにする Active Record ADBC adapter を開発中 私達が開発しているWEARは今年で10周年を迎え、大量のデータが蓄積されています。それらのデータをRuby on Rails上で高速処理できるようになるかもしれないとのことで、非常に夢の広がるお話だと思いました。 Sutou氏は開発メンバーを募集されていたので、興味のある方は是非参加してみてはいかがでしょうか? 私もこれを機にADBCやApache Arrow Flight周りについてもっと勉強してみようと思います! Gradual typing for Ruby: comparing RBS and RBI/Sorbet 小山です。私からは Gradual typing for Ruby: comparing RBS and RBI/Sorbet のセッションを紹介します。 このセッションではまずはじめに型定義のエコシステムの誕生を時系列で振り返りました。その後、型定義に使われる言語(RBS, RBI)、Type Checker(Steep, Sorbet)といった複数の手段で型定義にアプローチができるものに対する各特徴が解説されました。 個人的に、Rubyの型定義は言語やツールが複数存在していてそれぞれの役割を把握できていなかったのですが、このセッションのおかげで整理されてとても感謝しています。 話者がSorbetを開発しているShopifyで働いていることもあって、セッション中Shopify社内におけるSorbetや型定義に関するサマリーとアンケートが発表され、その内容も興味深かったです。 Shopifyのモノリスのうち98%のファイルに対して型付けがされており61%のメソッドに対してsigが付与されている Shopifyの400を超えるプロジェクトがSorbetを採用している より多くのコードに型付けされていることを望むかという質問に対して、Shopifyのエンジニアが2019年7月時点では57%がyesだったが、2022年9月時点では79%がyesと回答している Sorbetを他のShopifyのプロジェクトに導入することを望むかという質問に対して、2019年7月時点では39%がyesだったが、2022年9月時点では70%がyesと回答している これらからRubyの型定義を積極的に現場に導入していて、その結果ポジティブな反応が得られていることがわかりました。 また、発表の中で一番印象的だったのが、SteepとSorbetでType Checkingの速度を比較してみた結果でした。大規模なShopifyの本体のアプリケーションに対してそれぞれでType CheckをしたところSteepは完了に45分要したのに対し、Sorbetは10分で完了したとのことでした。 実際に運用しているアプリケーションでType Checkをしてみたベンチマーク結果が聞けたのは貴重でした。 Steepは型定義にRBSを使い、Sorbetは型定義にRBIを使うのですが、RBS, RBIそれぞれで、現状どのRubyの文法に対応できているかの対応表もとてもわかりやすかったです。 このセッションはRubyの型定義をキャッチアップできていなかった自分にとってとても良い学びになりました。今回の学びを足がかりにして、プロダクトに導入できるように調査を進めていきたいと思います! Implementing "++" operator, stepping into parse.y 三浦 です。 今年のRubyKaigiはパーサーに関するセッションがたくさんありました。 その中でも印象に残ったShioiさんのセッション「Implementing "++" operator, stepping into parse.y」についてご紹介します。 speakerdeck.com Rubyで実装するとき「なぜインクリメント演算子が使えないのか?」という疑問を持ったことがあるのではないでしょうか。 このセッションではMRIの字句解析器(スキャナ)と構文解析器(パーサー)で i++ はどのように解釈されているのかを探り、試行錯誤しながらインクリメントの実装をしていました。 ruby コマンドでは -y のオプションをつけることで構文解析のログを出力してくれます。 $ruby -ye 'i=0;i++' ... Next token is token '+' (1.5-1.6: ) Shifting token '+' (1.5-1.6: ) // 1つ目の'+'を解析 ... Next token is token "unary+" (1.6-1.7: ) Shifting token "unary+" (1.6-1.7: ) // 2つ目の'+'を解析 Entering state 48 Stack now 0 2 71 313 88 367 48 Reading a token parser_dispatch_scan_event:9857 (1: 7|1|0) // 2つ目の'+'の後に文字がないかを解析 Now at end of input. -e:1: syntax error, unexpected end-of-input i=0;i++ (※こちらはRuby 3.2.2で実行しました) このログを見ると、 i++ の2つめの + は単行演算子として判断されます。 MRIでは + の後には数字が来ることを期待していますが、実際はここでコードは終了しているためシンタックスエラーが発生してしまいます。 インクリメントを実現するために4つの方法を試していました。 - ++の挙動をInteger#succに置き換える - ++ 専用の構文ルールを追加し、この構文ルールに一致した場合 Integer#succ を呼ぶようアクションを追加 - ++ を Integer#succ のエイリアスのような感じで扱えるようになる - しかし Integer#succ はレシーバーの値を+1した結果を返しますがレシーバーに結果の代入はしてくれないので、 i++ としても変数iの値自体は更新されない ++の挙動を自前のメソッドで置き換える Integer#succ に変わる自前メソッド Integer#__plusplus__ を作成し、同じ方法で呼び出す レシーバーの変数名を取得し、その変数名に対して値を代入して返す しかし 1++ といったレシーバーにリテラルが来るとシンタックスエラーとなってしまう ++をスキャナで+=1に置き換える スキャナを改造して、 ++ が来た時に += と同じ構文木になるよう記号を返す しかし i++ * 2 といったインクリメントの後に他の演算子が来た時に演算子の優先度が変わってしまい、iに想定外の値が代入されてしまう ++をパーサで+=1に置き換える i++ 専用の構文ルールを追加し、この構文ルールに一致した場合 i+=1 と同じ挙動になるようアクションを追加 しかし既存の構文が1つ壊れてしまい、 i++ 1 といった予期しない値が来た場合に本来発生しないシンタックスエラーが発生するように パーサーの仕組みから試行錯誤しながら実装した流れまで丁寧に説明されており非常に分かりやすかったです。 動いた、しかしこんな問題が〜という流れの繰り返しは笑いを誘い面白かったです。 Shioiさんは鹿児島Ruby会議02の際に構文解析についての詳しい解説をされておりこちらも非常に勉強になりましたので、興味ある方は是非読んでみてください! たのしいRubyの構文解析ツアー The Adventure of RedAmber - A data frame library in Ruby 高久です。私からはHirokazu SUZUKIさんの 「The Adventure of RedAmber - A data frame library in Ruby」 についてご紹介します。 このセッションでは、Rubyでデータフレームを扱うためのライブラリであるRedAmberの機能紹介やどのように開発したかをデモを交えてお話しされていました。 データフレームとは行と列からなる表形式のデータ構造のことで、スプレッドシートやRDBのテーブルの構造に似ています。RedAmberを使うことで、Rubyらしい書き方で様々なデータ処理を行うことができます。 デモでは、RubyKaigiの過去の開催地リストとGeoloniaの住所データをデータソースとして、最終的には日本地図にRubyKaigiの過去の開催地をマッピングするまでの過程を紹介していました。内容としては両データを結合するためにKeyとなるデータを文字列加工したり、両データをleft_joinをして結合させていたり、高校生ぶりに見たtanを使った簡単な計算をしていました。 自分は業務で大規模なデータ処理を行うことが少ないこともあり、こういったデータ処理を行う時は今まではGoogleのスプレッドシート一択でした。ただオンラインでの処理になるため、データ量が多いとデータの受け渡しや描画処理に時間がかかってしまうこともありました。RedAmberを使うことで書き慣れているRubyで、わかりやすくデータ処理の記述ができるので、今後データ処理を行うことがあれば使ってみようと思いました。 以下発表スライドです。 speakerdeck.com スポンサーブース 今年も去年に続き、スポンサーブースを出展しました。 今年は、去年のTシャツに加えてWEARのロゴやQRコードがプリントされているクッキーや、「一合一会」という洒落の効いたお米、加えてZOZOMATやZOZOGLASSなどを配布しました。 中でもTシャツとお米は好評で、「ZOZOさんのTシャツお洒落ですよね!」や「ブースでお米配ってましたよね!」など、色々なところで感想を言っていただきました。 また、今年はブースにて『エンジニアのファッション事情を大調査!』というアンケートを実施しました。 リモートワーク時の服装は? 全身部屋着 97票 トップスだけ着替える 33票 全身着替える 47票 リモートワークをしたことがない 3票 個人的には「トップスだけ着替える」が一番多くなると予想していましたが、「全身部屋着」派が一番多く、驚きました。また、意外にもちゃんと「全身着替える」派がけっこうな割合いますね。 春に着たいアウターは? コート 29票 ジャケット 126票 パーカー 180票 カーディガン 84票 こちらは予想通り、プログラマーの制服とも言われている(※諸説あり)パーカーが一番多いですね! 僕もパーカー派です。 ノベルティで欲しいファッションアイテムは? Tシャツ 22票 パーカー 51票 靴下 18票 その他 トートバッグ キャップ ハンカチ サコッシュ ウィンドブレーカー 傘 ビーチサンダル マイクロファイバークリーナー 皆さんに「その他」の項目で様々な回答をいただきました。ありがとうございます! 自分達では出ないようなアイテムもあって面白いですね。次回の参考とさせていただきます。 結果としては、ここでもパーカーがかなりの人気となりました。確かにWEARロゴ入りパーカー欲しいです! ブース企画も大勢に参加して頂き、ノベルティも全て配布できました。ありがとうございました! 最後に ZOZOではセミナー・カンファレンスへの参加を支援する福利厚生があり、カンファレンス参加に関わる渡航費・宿泊費などは全て会社に補助してもらっています。ZOZOでは引き続きRubyエンジニアを募集しています。 以下のリンクからぜひご応募ください。 https://hrmos.co/pages/zozo/jobs/0000026 hrmos.co おまけ ブースを設置し、ラーメン屋のようなポーズで記念撮影している様子。 文化祭みたいで楽しいですね。 WEARポーズで集合写真を撮りました。 OfficialPartyの様子。 大勢が参加していました。いろんな人と交流できて楽しかったです! 今年はコロナも落ち着いて、セッションだけでなくOfficialPartyなど色々な人と交流できる場が多くなっていてとても楽しかったです。 Matzさんとも記念撮影できました! 来年はなんと沖縄開催で、会場は大盛り上がりでした。 自分も既にテンションが上がっています。待ち遠しいですね! 今年はセッションだけでなく、交流会も多くあって色々な人と関わることができたのでとても楽しかったです。また来年沖縄でお会いしましょう! irb はInteractive Rubyの略です。対話的に実行 (REPL) するためのシェルです ↩ 発表資料は こちら で公開されています。 ↩ make_shareable できるオブジェクトには制限があります。また make_shareable されたオブジェクトは freeze されます。 ↩
アバター
はじめに こんにちは。ZOZO DevRelブロックの @wiroha です。5/25にオンラインイベント「 ZOZO物流システム今昔物語〜モノリスからマイクロサービスへ〜 」を開催しました。ZOZOの開発において「物流システムリプレイス」にフォーカスした技術選定や設計手法、設計時の考え方などを紹介するイベントです。 登壇内容まとめ 弊社から次の3名が登壇しました。 ZOZOTOWN物流システム20年史 (基幹システム本部 物流開発部 / 矢野 敏明) 現在のZOZOTOWN物流システムの概要紹介 (基幹システム本部 物流開発部 / 武信 一平) モノリスからの脱却に向けた物流システムリプレイスの概要紹介 (基幹システム本部 物流開発部 / 矢部 佑磨) 当日の発表はYouTubeのアーカイブで視聴可能です。 www.youtube.com ZOZOTOWN物流システム20年史 矢野より物流拠点・サービス・システムの歴史を紹介 speakerdeck.com 矢野からはZOZOTOWNの物流システムの歴史について発表しました。2004年のサービス開始時からと長い間継続しているシステムです。VBScriptを使用しており当時は適していた技術であるものの、現在では技術者が不足しているといった課題が出てきています。物流拠点「ZOZOBASE」と開発側双方の課題を解決するため、発送業務からリプレイスを開始することになりました。「大事なのは温故知新」ということで「今のシステムへのリスペクト」「新しいことに取り組む姿勢」は大事なメッセージだと感じました。 社内公募制度についてもご紹介し、質問では興味を持っていただいていました。 質疑応答の補足 時間内に回答しきれなかったご質問について、こちらで回答いたします。 質問1. 発送業務がデータの分離をしやすいと分かった経緯をもう少し教えて頂けないでしょうか?既存システムを知っている有識者の知識のみで実施できたのでしょうか?それともデータモデリングなどを実施したのでしょうか? 質問2. 切り出しやすい。ってどう導きだしたのでしょうか? 回答: 2件まとめて回答します。今回はほぼ既存システムを知っている有識者の意見を参考にしました。また、データに付いて分離という言葉を使っていますが意味合いとしてはシステム間が疎結合になっているという意味で分離という言葉を使っています。発送作業の元データは基幹サービスから持ってくるので基幹と発送サービスで同じデータを持っていることになりますがそこにひも付きはありませんので「分離」と表現しています。 分離の観点としまして、発送作業(ピッキング、梱包)において次の観点で話を進めました。 在庫管理が必要であれば分離は困難 在庫管理が必要なければ分離できる可能性あり 今回は発送サービスをあくまで発送作業を行うツールというような形で捉えました。例えば、発送サービス側で商品バーコードと格納ロケーションのみ知っていればピッキング作業は可能です。在庫管理は発送作業完了データを基幹システムに流し、基幹側で非同期に行う仕組みとしました。 上記の理由から次の判断をしました。 発送作業では在庫管理の必要はない 複雑なひも付きのない単体データで発送作業が可能 これらの判断から発送に必要なデータのみを分離しました。 現在のZOZOTOWN物流システムの概要紹介 武信より発送システムの概要を紹介 speakerdeck.com 武信からは発送システムの概要を紹介しました。図解によりさまざまな手順を経ていることがわかります。システム障害リスクの増大、機能追加の労力の増大という課題を解決するためリプレイスをすることになりました。開発案件の起案フローや開発フロー、リリースフローもご紹介しました。非常に多くのご質問をいただき、物流へ興味を持つ方がこんなにもいるのかと嬉しく思います。 質疑応答の補足 質問: アジャイル開発を実施していない理由はありますか? 回答: ZOZOTOWNでは当初からウォーターフォール開発の手法を採用していたのでそれを継続しているというのが一番大きいです。基幹システムという特性上、速く開発する事よりも正確な処理をする事を重視しているという側面もあります。軽微な修正等はアジャイル開発に近い手法で進める事もあります。 モノリスからの脱却に向けた物流システムリプレイスの概要紹介 矢部よりリプレイスの概要を紹介 speakerdeck.com 矢部からはリプレイスの概要・工程について発表しました。超えてきた障壁には技術習得、人員確保、現行システム開発案件との並列化などがあげられました。リプレイスにあたり、独自の物流システムを持っており膨大なビジネスドメインがあるため、ドメイン駆動設計を導入しました。導入により既存よりも大幅に読みやすいコードにできメリットを感じているそうです。リプレイス後のインフラ構成はさまざまな組み合わせで要件やコストを満たすか比較されていました。現段階では正解かわからない点もあるとのことで、今後また聞ける機会を設けられればと思います。 質疑応答の補足 質問: 分析で出てきたメタデータと実装のメタデータはどのように管理されていますか? 回答: 分析で出てきたメタデータはホワイトボードツールを使っていましたので付箋などで表していました。実装のメタデータは基本的にコードで表現しますが、できないものはコメントまたはGitHubリポジトリのWikiに書くなどして使い分けています。 最後に 今回は実際の物がかかわる物流という特殊なドメインにフォーカスしたイベントを開催しました。非常に多くのご質問・ご参加をいただきありがとうございました。質疑応答も含んでおりますので、ぜひ YouTubeのアーカイブ をご覧ください! ZOZOでは一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
はじめに こんにちは。FAANSバックエンドエンジニアの浜口( @xlgorbylx )です。普段はFAANSのバックエンドシステムの開発をしています。 FAANSとは、弊社が2022年8月に正式ローンチした、アパレル店舗のショップスタッフの販売サポートツールです。例えば、ZOZOTOWN上で実店舗の在庫取り置きができる機能や、コーディネート投稿の機能などを備えています。投稿されたコーディネートはZOZOTOWNやWEAR、Yahoo!ショッピング、ブランド様のECサイト等に連携が可能です。これによりお客様のコーディネート選びをサポートし、購買体験をより充実したものにします。機能の詳細に関しましては、下記プレスリリースをご覧ください。 corp.zozo.com 本稿では、Go言語で実装されたFAANSのバックエンドシステムについて、SonarSource社の提供するSaaSである「 SonarCloud 」を用いてテストカバレッジ推移を可視化できるようにした経緯と方法、また導入に際して顕在化した課題とその解決方法についてご紹介します。 静的コード解析やテストカバレッジの可視化、ソフトウェア品質の向上に興味をお持ちの方のご参考になれば幸いです。 なお、FAANSの利用技術に関連する記事として「 Cloud FirestoreからPostgreSQLへ移行したお話 」も合わせてご覧いただくとより深くご理解いただけます。 techblog.zozo.com 目次 はじめに 目次 テストカバレッジ推移を可視化した背景 コード解析ツール選定の過程 SonarCloudとは 解析対象となるコード行数について SonarCloud導入までの手順 CIの実行時間について テストカバレッジ計測対象となるテスト CI/CDワークフロー上のジョブ設定 ジョブの分割 SonarCloudにUnit Testの成果物を共有する ジョブの並列実行 SonarCloudの設定ファイルについて SonarCloud設定ファイルの自動生成 まとめ さいごに テストカバレッジ推移を可視化した背景 FAANSのバックエンドシステムでは、Unit Testや各APIエンドポイント単位のIntegration Testが既に実装されています。しかし、テスト対象のソースコードのうち、どの程度の割合のコードがテストされたかについては可視化されておらず、本来テストするべきコードが網羅されていない可能性が否定できない状況でした。 この問題を解決するため、「テストの網羅率(カバレッジ)」を可視化可能なコード解析ツールの導入を検討していました。また、FAANS開発チームは「ソフトウェア品質の更なる向上」を大きな目標としています。コード解析ツールの導入によって「テストカバレッジがどの程度改善されたのか」について定量的に評価できるようになることも期待されていました。 コード解析ツール選定の過程 まず、テストカバレッジを確認したいタイミングについて開発チーム内で認識を合わせました。その結果、「A. テスト対象となるソースコードを新規実装した時」と「B. 既存実装のテストカバレッジを確認したい時」の2パターン存在することが分かりました。 このAのパターンについては「Pull Requestが作成された時」と換言可能であり、CI/CDに用いているGitHub Actions上で実行可能なコード解析ツールであることが求められます。また、Bのパターンについては、コード解析対象とするGitHubリポジトリの任意のブランチのコード解析結果を、任意のタイミングで閲覧可能であれば解決しそうです。 この要件を念頭に複数のコード解析ツールを選定した結果、SonarSource社の提供するSaaSである「SonarCloud」を導入することに決めました。弊社が既に法人契約を結んでおり、社内のいくつかのチームで参考となる導入実績が存在していたため、導入のハードルが相対的に低かったことが決め手となりました。 SonarCloudとは SonarCloud は、 CI/CDワークフロー上で動作するクラウドベースの静的コード解析ツール です。CI/CDワークフローに組み込むことで、GitHub上でのPull Request作成時やブランチへのPush時などに解析対象となるソースコードを下記の観点で解析可能です。 Reliability(コードの信頼性) Maintainability(コードの保守性) Security, Security Review(コードのセキュリティ) Coverage(コードのテストカバレッジ) Duplications(コードの重複) なお、解析済みのPull Requestやブランチについては、SonarCloudの管理画面上にて、Pull Request単位やブランチ単位で解析結果を閲覧可能です。そのため、SonarCloudであれば上記のA, Bどちらのパターンの場合にも適した利用が可能であると判断し採用に踏み切りました。 また、契約プランについては、SonarCloudへ登録しているGitHubリポジトリ内の解析対象となるコード行数に応じて料金が変動する仕組みとなっています。該当のコード行数はSonarCloud側で自動的に計測されますが 1 、利用料金の見積もりのため事前に解析対象となるコード行数を計測することにしました。 解析対象となるコード行数について GitHubリポジトリ内の解析対象となるコード行数について、今回はコード行数計測ツールである「 cloc 」を利用して計測しました。clocは Count Lines of Code の略称であり、対象リポジトリ内で下記のように実行すると言語別にファイル数やコード行数を出力してくれます。 SonarCloudは対象リポジトリ内のソースコードのみを解析対象とするため、ここでは --vcs=git オプションを付与することでGit管理下のソースコードのみをカウントするように指定しています。 cloc --vcs=git 3157 text files. 3141 unique files. 15 files ignored. github.com/AlDanial/cloc v 1.96 T=3.45 s (910.2 files/s, 187794.9 lines/s) ------------------------------------------------------------------------------- Language files blank comment code ------------------------------------------------------------------------------- Go 2458 60059 38064 312271 YAML 531 168 392 120504 JSON 54 0 0 110973 SQL 43 108 8 1747 Markdown 16 400 0 1075 TOML 6 100 0 482 JavaScript 3 54 15 446 Bourne Shell 5 56 32 361 Smarty 3 29 0 201 make 1 47 13 171 Dockerfile 5 17 1 55 Text 7 30 0 53 HTML 6 32 0 45 Properties 2 11 2 21 CSV 1 0 0 4 ------------------------------------------------------------------------------- SUM: 3141 61111 38527 548409 ------------------------------------------------------------------------------- 上記の通り、解析対象となるコード行数が判明したら適切な契約プランを指定して利用を開始します。 SonarCloud導入までの手順 まず、SonarCloud上にOrganization情報を登録します。弊社の場合は、GitHub OrganizationをSonarCloudのOrganizationとして既に連携済みの状態でした。そのため、公式ドキュメントの Getting Started With GitHub を参考にしながら手順を進めました。 次に、Organization配下に対象リポジトリをProjectとして追加し、SonarCloud利用ユーザのGitHubアカウントをOrganizationに紐付けることで利用権限を付与します。 ここまでの手順により、各ユーザのSonarCloudへのログイン及びProjectの閲覧が可能となります。その後、GitHub ActionsによるCI-basedなソースコード解析が実行可能となるように 公式ドキュメント の手順通りに設定します。 以上により、CI/CDワークフロー上でのPull Request単位およびブランチ単位の静的コード解析が実現できました。しかし、私たちFAANS開発チームの場合は下記のような課題が発生しました。 SonarCloudの静的コード解析に要する時間分、CIの実行時間が長くなってしまう SonarCloudの設定ファイル( sonar-project.properties )のメンテナンス性が低い ここからは上記2点の課題について、その詳細とどのように解決したかをご紹介します。 CIの実行時間について まず、テストカバレッジの計測対象となるテストを整理し、そのテストをGitHub Actions上でどのように実行しているかを紹介します。その後、CIの実行時間をどのように抑制・削減したかについて説明します。 テストカバレッジ計測対象となるテスト FAANSバックエンドシステムにはUnit TestとIntegration Testが既に実装されていますが、SonarCloudによるカバレッジ計測が可能な対象はUnit Testのみとなります。 Integration Testが計測対象に含まれない理由は、Go言語で起動したAPIサーバにリクエストを投げ、期待したレスポンスが返却されるか否かという「結果」のみに注目したテストであるためです。テスト自身は「どのように実装されているか」という詳細については把握していないため、カバレッジ計測の対象外となります。 一方で、Unit Testの場合はテスト実行時に解析対象となるソースコードのどの程度の割合がテストされたかが把握可能なため、SonarCloudによるカバレッジ計測が可能となります。 CI/CDワークフロー上のジョブ設定 これまでの既存実装においては、Pull Request作成時およびメインブランチへのpush時に、GitHub ActionsによるUnit TestおよびIntegration Testを 1つのジョブ内で かつ 順次的に 実行していました。 そのため、実装量の増加に比例してCIのテスト実行時間が増長しており、開発効率が次第に低下している状態でした。 この状況を改善することなくSonarCloudの静的コード解析をCIに追加する場合、計測対象となるUnit Testの完了を待つということは即ち、待つ必要のないIntegration Testの実行完了も無駄に待機し、さらに静的コード解析に要する時間もCIの実行完了までに上乗せさせることとなります。 この状況を改善させるため、Unit TestとIntegration Testで ジョブを分割すること と 並列でテストを実行すること を決めました。 ジョブの分割 Unit TestとIntegration Testのジョブを分割するにあたり、Go言語や依存パッケージのインストール、DBのセットアップなどの両者で同一のステップについては、記述内容の重複により保守性が低下してしまうことを予防するため、YAMLで定義された1つの設定内容を共通の定義として利用するようにしました。 ジョブ間で同じステップの設定内容を共通化させるためには、作成したステップに対して「 composite action 」を定義する必要があります。 composite actionを利用すると、任意のステップの処理を別のYAMLファイルに切り出すことが可能です。この別のYAMLファイルに切り出した処理を呼び出す形式で、異なるジョブ間でも記述内容の重複を発生させずにステップの実行内容を設定可能となります。 一方で、ジョブを分割した影響により1点追加の対応が必要になります。SonarCloudによる解析を実行するジョブに対して、Unit Testによって出力されるテストレポート等の成果物を共有する必要があります。 Unit Testを実行するジョブとSonarCloudによる解析を実行するジョブが異なるジョブである場合、どのようにすれば成果物を共有できるでしょうか? SonarCloudにUnit Testの成果物を共有する 同一ワークフロー内の異なるジョブに成果物を共有したい場合、GitHub Actionsの upload-artifact と download-artifact の利用が考えられます。 これらを利用すると、GitHub上のストレージ領域に任意の成果物を保存することが可能となり、同一ワークフロー内の任意のジョブからダウンロードして利用できます。 具体的には、Unit Test実行ジョブの完了後にupload-artifactを用いてテストレポートをGitHub上のストレージ領域に保存します。その後、SonarCloudによる解析を実行するジョブではdownload-artifactを用いてこのUnit Testの成果物を読み込ませます。 このように対応することで、ジョブを分割した場合にも異なるジョブの成果物を利用して次のジョブの処理を実行可能となります 2 。 ジョブの並列実行 GitHub Actionsの公式ドキュメント に記載の通り、異なるジョブはデフォルトでは相互に並列で実行されます。これまでUnit TestとIntegration Testが順次実行されるようになっていた理由は、同一のジョブ内で異なるステップとして定義していたためでした。そのため、上述の通り両者のジョブを分割しただけでUnit TestとIntegration Testの並列実行が実現されます。 以上により、「ジョブの分割と並列実行」が実現できました。Integration Testの実行完了を待たずにUnit Testが実行され、完了次第SonarCloudのコード解析が始まるように改善されたため、CI実行時間の肥大化という課題は解消されました。 次は「SonarCloudの設定ファイル( sonar-project.properties )のメンテナンス性が低い」という課題について見ていきましょう。 SonarCloudの設定ファイルについて SonarCloudは、デフォルトではリポジトリのルート直下にある sonar-project.properties を設定ファイルとして読み込みます 3 。 この設定ファイルにはOrganization情報やプロジェクトキー、解析対象としたいソースコード等を記述します。なお、静的コード解析およびテストカバレッジを適切に取得するためには、解析対象に含めたくないファイルをこの設定ファイル上で個別に除外指定する必要があります。最終的に記述したい内容は下記のようなイメージです。 # sonar-project.properties sonar.organization=ORGANIZATION_NAME sonar.projectKey=PROJECT_KEY sonar.sources=. sonar.exclusions=**/*_test.go,**/hoge/**,**/huga/**,**/openapi/**,**/mock/**,**/auto_generated_model/**,**/vendor/**,**/*.js sonar.tests=. sonar.test.inclusions=**/*_test.go sonar.test.exclusions=**/vendor/** sonar.coverage.exclusions=**/hoge/**,**/huga/**,**/openapi/**,**/mock/**,**/auto_generated_model/**,**/vendor/**,**/*.js,**/integration/**,**/testutil/** sonar.go.tests.reportPaths=./test-results/report.json sonar.go.coverage.reportPaths=./test-results/coverage.out 上記をご覧の通り、除外したいファイルが増えた場合に適宜カンマ区切りで対象ファイルを追記する必要があります。しかし、多くのファイルを指定していくと、次第に見通しが悪くなりメンテナンス性を著しく低下させてしまいます。 この課題を解決すべく、Go言語の標準パッケージである text/template を利用して、この設定ファイルをmakeコマンド1つで自動生成できるように工夫しました。 SonarCloud設定ファイルの自動生成 関連するディレクトリ・ファイル構成は下記のようなイメージです。 . ├── Makefile ├── (sonar-project.properties) #makeコマンドで自動生成されるSonarCloud設定ファイル └── sonarcloud ├── README.md └── properties_generator ├── config │ ├── config.go │ └── file_list.go ├── main.go └── template └── sonar-project.properties まず、makeコマンドを下記の通り定義します。 sonar-project.properties ファイルを自動生成するためのmain関数を実行させるだけのシンプルなコマンドです。 # Makefile .PHONY: generate-sonar-project-properties generate-sonar-project-properties: go run ./sonarcloud/properties_generator/main.go 次に、自動生成コマンド実行時のテンプレートとなるファイルを作成します。内容は下記のイメージで、SonarCloudの設定項目に対して値をプレースホルダーで定義します。 # sonarcloud/properties_generator/template/sonar-project.properties sonar.organization={{ .Organization }} sonar.projectKey={{ .ProjectKey }} sonar.sources={{ .Sources }} sonar.exclusions={{ .Exclusions }} sonar.tests={{ .Tests }} sonar.test.inclusions={{ .TestInclusions }} sonar.test.exclusions={{ .TestExclusions }} sonar.coverage.exclusions={{ .CoverageExclusions }} sonar.go.tests.reportPaths={{ .GoTestsReportPaths }} sonar.go.coverage.reportPaths={{ .GoCoverageReportPaths }} makeコマンド内で実行されるmain関数の中身は、一部省略・簡素化してますが下記のようになります。 sonar-project.properties ファイルを生成し、テンプレートファイルのプレースホルダーに対して値を書き出す処理となっています。 // sonarcloud/properties_generator/main.go package main import ( "fmt" "os" "text/template" "github.com/*****/sonarcloud/properties_generator/config" ) func main() { t, err := template.ParseFiles( "./sonarcloud/properties_generator/template/sonar-project.properties" ) if err != nil { panic (err) } // 生成したpropertiesファイルはルート直下に配置する targetFile, err := os.Create( "./sonar-project.properties" ) defer func (f *os.File) { if err := f.Close(); err != nil { fmt.Println(err) } }(targetFile) if err != nil { panic (err) } c := config.NewConfig() if err = t.Execute(targetFile, c); err != nil { panic (err) } } 上記の config.NewConfig() の部分を詳細に見ていきましょう。下記の通り NewConfig() はテンプレートファイルに定義したプレースホルダーと一致するFieldを持つ構造体を返却します。そのため、 t.Execute(targetFile, c) が実行されると、プレースホルダー部分に実際の値が書き出されます。 // sonarcloud/properties_generator/config/config.go package config import "strings" const ( organization = "ORGANIZATION" projectKey = "PROJECT_KEY" sources = "." tests = "." goTestsReportPaths = "./test-results/report.json" goCoverageReportPaths = "./test-results/coverage.out" ) type Config struct { Organization string ProjectKey string Sources string Exclusions string Tests string TestInclusions string TestExclusions string CoverageExclusions string GoTestsReportPaths string GoCoverageReportPaths string } func NewConfig() *Config { return &Config{ Organization: organization, ProjectKey: projectKey, Sources: sources, Exclusions: convertToCommaSeparatedList(exclusions()), Tests: tests, TestInclusions: convertToCommaSeparatedList(testInclusions()), TestExclusions: convertToCommaSeparatedList(testExclusions()), CoverageExclusions: convertToCommaSeparatedList(coverageExclusions()), GoTestsReportPaths: goTestsReportPaths, GoCoverageReportPaths: goCoverageReportPaths, } } func convertToCommaSeparatedList(list [] string ) string { return strings.Join(list, "," ) } // exclusions関数には、コード解析の対象(sonar.sources)から除外したい要素を指定します。 func exclusions() [] string { list := make ([] string , 0 ) list = append (list, TestFiles()...) // テストコード list = append (list, AutoGeneratedFiles()...) // 自動生成コード list = append (list, VendorFiles()...) // 外部ライブラリ list = append (list, NonGoFiles()...) // Go言語以外のコード return list } // testInclusions関数には、テストコード解析の対象を指定します。 func testInclusions() [] string { list := make ([] string , 0 ) list = append (list, TestFiles()...) return list } // testExclusions関数には、テストコード解析の対象から除外したい要素を指定します。 func testExclusions() [] string { list := make ([] string , 0 ) list = append (list, VendorFiles()...) return list } // coverageExclusions関数には、テストカバレッジ解析の対象から除外したい要素を指定します。 // 指定しない場合、全てのコードがテストカバレッジの解析対象となり正確なカバレッジが取得できません。 func coverageExclusions() [] string { list := make ([] string , 0 ) list = append (list, AutoGeneratedFiles()...) // 自動生成コード list = append (list, VendorFiles()...) // 外部ライブラリ list = append (list, NonGoFiles()...) // Go言語以外のコード list = append (list, TestUtilFiles()...) // テストにのみ使用するhelperやfactoryのコード list = append (list, IntegrationTestFiles()...) // 結合テストのコード return list } ここまででmakeコマンド、makeコマンドで実行されるmain関数、main関数実行時に実際に値を書き出すための構造体を返却する関数が登場しました。 最後に、実際に人間がメンテナンスする必要のあるファイルは下記になります。ファイルパスの文字列を要素とする配列のsliceを返却する関数をファイルの種類ごとに定義し、該当するファイルパスをその配列の要素として指定するだけです。 ファイルパスを追加・削除・修正したい場合は下記のファイルを更新し、 make generate-sonar-project-properties を実行する運用となります。 // sonarcloud/properties_generator/config/file_list.go package config // TestFiles関数には、テストコードのファイルパスを指定します。 func TestFiles() [] string { return [] string { "**/*_test.go" , } } // VendorFiles関数には、外部ライブラリのファイルパスを指定します。 func VendorFiles() [] string { return [] string { "**/vendor/**" , } } // AutoGeneratedFiles関数には、自動生成されるファイルパスを指定します。 func AutoGeneratedFiles() [] string { return [] string { "**/openapi/**" , "**/mock/**" , } } // NonGoFiles関数には、Go言語以外のファイルパスを指定します。 func NonGoFiles() [] string { return [] string { "**/*.js" , } } // IntegrationTestFiles関数には、結合テストのファイルパスを指定します。 func IntegrationTestFiles() [] string { return [] string { "**/integration/**" , } } // TestUtilFiles関数には、テスト利用目的のヘルパー関数系のファイルパスを指定します。 func TestUtilFiles() [] string { return [] string { "**/testutil/**" , } } いかがでしょうか。「SonarCloudの設定ファイル( sonar-project.properties )のメンテナンス性が低い」という課題は、ある程度見通しのよい状態で管理できるように改善されました。 要点を端的に伝えるため実際の設定内容をかなり簡素化し記載していますが、実際にはその他の自動生成コード等の影響で除外指定ファイルがとても多い状況でした。カンマ区切りの文字列をベタ書きしていた当初は、コード解析対象を適切に指定できているか極めて不透明な状態でした。 今回の改善により、意図した通りの適切なテストカバレッジが取得されていることがある程度担保されるようになりました。 まとめ 上記一連の対応により、FAANSではSonarCloudを用いた静的コード解析によるソフトウェア品質向上のための第一歩を踏み出すことができました。SonarCloudの導入に際して、CI実行時間の肥大化防止やメンテナンスコストの抑制、適切な解析対象の設定が課題となりましたが、上述の通り1つ1つ丁寧に問題解決しながら課題を解消できました。 なお、SonarCloudの導入はあくまでソフトウェア品質向上のための第一歩であり、導入後もコード解析結果を継続的に意識する必要があります。 私たちFAANS開発チームでは、SonarCloudがPull Requestに対して発行するコード解析後のコメントを確認する運用としており、日頃から静的コード解析の恩恵を享受するようにしています。 例えば下記画像の例では、解析対象のソースコード内にTODOコメントが1点残存しているため「1 Code Smell」と表示されています。これは極めて簡単な一例ですが、静的コード解析が無ければ見逃してしまっていたようなバグやテストの実装漏れ、コードの重複、脆弱性等に対して常日頃からアンテナを張れるようになりました。 FAANSはまだ歴史の浅い新進気鋭のサービスですが、新規機能の開発・リリースのみならず、ソフトウェア品質の維持・向上についても強い関心を寄せながらチーム一丸となって日々取り組んでいます。 さいごに ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com https://hrmos.co/pages/zozo/jobs/0000175 hrmos.co SonarCloud公式サイト のFAQにて、コード行数に関する詳細な説明を確認できます。 ↩ より詳細な説明は GitHub公式ドキュメント をご参考ください。 ↩ sonar-project.properties ファイルに設定可能な項目は SonarCloud公式ドキュメント に記述されています。 ↩
アバター
こんにちは。ZOZO DevRelブロックの @wiroha です。RubyKaigiではじめて協賛ブースに立ち、知り合いも増えて嬉しく感じている今日この頃です。 はじめに 5/18に After RubyKaigi 2023〜メドピア、ZOZO、Findy〜 をオフライン・オンラインのハイブリッドで開催しました。RubyKaigi 2023のスポンサー企業であるメドピア株式会社、株式会社ZOZO、ファインディ株式会社の3社合同でのRubyKaigi Afterイベントです。 イベント概要 3社の社員によるLT、RubyKaigi SpeakerによるLT、パネルディスカッションを行い、その後は懇親会で盛り上がりました! LT REPLとデバッガを取り巻く環境の変化 -Pry, IRB, そしてdebug.gem- / メドピア株式会社 古川健二 @frkawa_ ruby.wasm + unloosenでChrome拡張機能を作ってみた / 株式会社ZOZO 近海斗 @Ver3Alt そうだ RubyKaigi、行こう。 〜初めてのRubyKaigiの歩き方〜 / ファインディ株式会社 遠藤薫 @aiandrox Speakers LT Road to RubyKaigi Speaker (case sue445) / Go Sueyoshi @sue445 After RubyKaigi 2023〜メドピア、ZOZO、Findy〜 / unasuke(Yusuke Nakamura) @yu_suke1994 パネルディスカッション登壇者 Go Sueyoshi @sue445 unasuke(Yusuke Nakamura) @yu_suke1994 メドピア株式会社 平川弘通 @arihh 株式会社ZOZO 諏訪智大 @tsuwatch ファインディ株式会社 神谷健 @k_m_y_ 当日の発表はYouTubeのアーカイブでご覧下さい。 www.youtube.com 発表詳細 REPLとデバッガを取り巻く環境の変化 -Pry, IRB, そしてdebug.gem メドピア株式会社 古川健二さま speakerdeck.com メドピア株式会社の古川さまより、デバッガについての発表が行われました。だんだんとデバッガの機能が拡充され、Rubyのバージョンが上がると選択肢も増えていることがわかります。irbを使ったデモも行われました。 ruby.wasm + unloosenでChrome拡張機能を作ってみた 株式会社ZOZO 近海斗 speakerdeck.com 弊社ZOZOの近からはunloosenを使ったChrome拡張開発の発表が行われました。実際に開発してみた中でのハマりポイントが共有されるのはありがたいですね。デモ動画に対してはあたたかい拍手が送られていました。 そうだ RubyKaigi、行こう。 〜初めてのRubyKaigiの歩き方〜 ファインディ株式会社 遠藤薫さま speakerdeck.com ファインディ株式会社の遠藤さまからは、はじめてRubyKaigiに行ってみた経験談が発表されました。実際に参加してみてのアドバイスとして「予習をする」「目標を立てる」「公式イベントに参加する」「写真を撮る」といった点をあげていました。「次回はコントリビュートしたいという熱が高まった」というお話には、皆さん共感したのではないでしょうか。 Road to RubyKaigi Speaker (case sue445) Go Sueyoshiさま speakerdeck.com Go SueyoshiさまはこれまでRubyKaigiに5本応募して3本採択されたという高い採択率を誇っていました。プロポーザルを書くときのコツは「イベントの趣旨に合ったことを出す」「1年考え続ける」「自分が第一人者であることを出す」「早めに出してレビューをもらう」など、説得力のあるアドバイスだと感じました。「それってsueさんがすごいだけでは?」と疑問があるかもしれませんが、「すごいと言ったらその人とに壁を作って成長しなくなる」という言葉が印象的でした。 After RubyKaigi 2023〜メドピア、ZOZO、Findy〜 unasukeさま slide.rabbit-shocker.org unasukeさまの発表は事前に受け付けていた質問への回答からはじまりました。RubyアソシエーションGrantという開発助成金の制度に応募した体験談はなかなか聞けない話だと思いました。新卒の頃RubyKaigiに初参加し、その後proposalを出したりhelperとして参加するようになったりしたそうです。採択されるには自分でコードをめっちゃ書く、採択されなくても楽しんだもの勝ち、という明るくなるメッセージをいただきました。 パネルディスカッション 楽しかった話に花が咲きました まずは各社のブースが紹介され、数年越しの思いが込められていたり工夫が感じられました。来年は那覇ということで行きたい方がたくさんいるのは各社同じのようでした。楽しみですね! RubyKaigiの感想としては、人が想像以上にたくさん来ており情熱を感じたという話がありました。登壇者視点では、オフラインだとトークの後にそのまま登壇の感想をもらえるのが嬉しかったそうです。 懇親会 乾杯! Rubyメソッドかるたで盛り上がるみなさま 懇親会ではドリンク片手にみなさん盛り上がっていました。登壇者に質問をする方々、参加者持参のRubyメソッドかるたで遊ぶ方々など、楽しんでいただけてよかったです! 最後に 登壇者全員でRubyのポーズ 登壇者のみなさまありがとうございました。今回の発表を聞いて来年は参加しようと思った方、登壇したいと思った方が増えたはずです。引き続きRubyコミュニティを盛り上げていければ幸いです。 ZOZOでは一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
はじめに こんにちは。ZOZOTOWN開発本部アプリ部バックエンドの髙井です。普段は筋肉のビルドが趣味のエンジニアをやっています。私のチームではZOZOTOWNアプリのバックエンド全般の開発から運用までを行っています。 突然ですが、皆さんご存知でしょうか? ZOZOTOWNはカスタマーサポートセンターの運営管理や従業員のマネジメント等を総合的に評価する「HDI五つ星認証プログラム」にて、五つ星認証を4回連続で取得しています。これは、CS(カスタマーサポート)対応をする弊社社員の皆さんの愛あるサポートの賜物で、同じサービスに携わる身としてもとても誇らしい気持ちです。 そんなCS対応ですが、問い合わせによっては原因調査をエンジニアが行っています。本記事では、CSからエンジニアに来たお問い合わせ(以後、CS問い合わせと呼ぶ)をまとめたレポート作成の自動化についての事例を紹介します。運用コストを抑えながら様々なデータを見やすくまとめる参考になれば幸いです。 目次 はじめに 目次 CS問い合わせとは CSレポートとは 課題点 Looker Studio お金がかからない 簡単操作でリッチなUIが作れる 連携可能なデータが豊富 データの自動反映 レポート作成の手順 CSレポート自動化で工夫したポイント データの集計とソート 過去データとの比較ができる 過去の問い合わせを検索できる 未回答の問い合わせを検索できる 自動化後の効果 まとめ CS問い合わせとは CS問い合わせについてもう少し詳しく説明します。「はじめに」でも触れましたが、CS問い合わせとはCSからエンジニアへのお問い合わせのことです。お客様からのお問い合わせの中でも、CSが調べられないデータの調査が必要なお問い合わせやZOZOTOWNの仕様に関するお問い合わせなどがCS問い合わせとして寄せられます。 問い合わせをすることはお客様にとっても手間のかかることです。手間をかけてでもお問い合わせをしているということや、お客様〜CS〜エンジニアとやり取りが多いので、CS問い合わせの回答スピードもなるべく早くする必要があります。 また、CS問い合わせは新たなバグの発見にも繋がります。お問い合わせを調査していく中で、機能改善にも役立てています。 CSレポートとは CS問い合わせに対してCSレポートとは、CS問い合わせの各種データを集計した月次レポートのことです。具体的には以下のような情報をまとめています。 問い合わせ傾向の総評 MAUと問い合わせ数の比較 注文者数と問い合わせ数の比較 各チームへのエスカレーション数 エスカレーション割合 各チームのリードタイム カテゴリ別問い合わせ数 これらは全て、Slackのワークフローから取得した回答データを基に生成していました。そして、これらのデータを手動でGoogleスライドにまとめることでCSレポートを作成しています。 また、作成したCSレポートは、CS、開発者間で共有することで、問い合わせの傾向の把握やリードタイム(エンジニアが問い合わせを受けてから回答を作成するまでの時間)の改善などに役立てています。 一方で、運用を続けるうちに課題も見えてきていました。 課題点 出てきた課題は以下のような点です。 毎月のレポート作成に工数がかかる グラフや表を毎回作成する必要がある 過去のレポートと比較しにくい これらの課題を解決するために、Looker Studioを導入しました。 Looker Studio Looker Studio とは、さまざまなデータソースから自動でグラフや表を作成できるツールです。Looker Studioのいくつかのメリットを紹介します。 お金がかからない Looker StudioはGoogleが提供する無料のツールなので、基本的にお金はかかりません。Googleアカウントさえあれば簡単にレポート作成を始めることができます。 簡単操作でリッチなUIが作れる LoockerStudioはとても簡単な操作で見やすいグラフや表を作成できます。SQLの知識やプログラミングスキルは不要なので、個人的にはGoogleスライドと同じくらい簡単で使いやすいツールだと思っています。 画像の例は、Looker Studioが提供する サンプルレポート です。 これだけクオリティの高いレポートでも簡単な操作で作成できます。 連携可能なデータが豊富 Looker Studioでは800以上のデータソースに簡単に接続してデータを結合できます。CS問い合わせのデータをまとめているGoogleスプレッドシートにも接続できることが、CSレポートにLooker Studioを使用する決め手の1つとなりました。 データの自動反映 Looker Studioでは、基となるデータソースから取得したデータを見やすい形式に書き換えて表示します。Looker Studioへのデータ連携は自動で行われるので、基のデータソースを更新するとLooker Studioにも自動で変更内容が反映されます。そのため、一度Looker Studioでレポートを完成させてしまえば、それ以降作成したレポートに手を加える必要がなくなります。 結果として、課題であった「グラフや表を毎回作成する必要」がなくなり、その分の「工数削減」ができます。 レポート作成の手順 Looker Studioを使ったレポート作成の手順は以下のようになります。 Looker Studioにログインします。 レポートの追加 空のレポートを選択し、レポートを作成します。 データソースの選択 レポートのデータソースを選択します。利用可能なデータソースの一覧が表示されるので、適切なデータソースを選択します。 ビジュアライゼーションの作成 レポートの編集画面が開きます。ここで、ビジュアライゼーション(グラフやチャートなど)を作成します。 データソースから必要なフィールドを選択し、グラフのタイプや設定を選択します。 必要に応じてフィルターや集計の設定を追加し、データの表示をカスタマイズします。 フィルターの追加 レポートにフィルターを追加して、データを絞り込むことができます。例えば、日付範囲や地域などの条件を設定します。 レポートの設定 レポートのタイトルや説明を追加します。 レポートの表示形式やサイズを調整します。 レポートの保存 レポートが完成したら、保存ボタンをクリックして変更を保存します。 CSレポートを作成する際も同様の流れで作成しました。 CSレポート自動化で工夫したポイント Looker Studioでは、様々なカスタマイズができます。本記事ではCSレポートを作成するにあたって活用したLooker Studioの機能と、工夫したポイントを紹介します。 データの集計とソート Looker Studioでは、データを集計でき、集計方法を選択するだけで意図した値を簡単に集計できます。 CSレポートでは各チームのリードタイムを集計するために、中央値と平均値を表に記載しました。 また、チームごとの問い合わせ数も集計し、グラフに表示させています。 問い合わせ総数の推移と問い合わせに対する対応チーム比率がわかりやすく表示されています。 過去データとの比較ができる これまでのCSレポートでは毎月新しくスライドを作成してレポートをまとめていました。そのため、「去年の同じ月はどんな傾向があったんだっけ?」などと思ったときには、その都度対象のレポートを探し出して見比べる必要がありました。 一方で、自動化後のレポートでは複数レポートを見比べる必要はありません。年月での絞り込み機能を追加したので、1つのレポート内で確認したい年月を絞り込むことができます。 この機能によって、課題として挙げていた「過去レポートとの比較」を可能にし、レポートを見る側にとっての利便性を高めることができました。 過去の問い合わせを検索できる これまで過去の問い合わせを調べるときには、スプレッドシートで調べるかSlack上で調べるしか方法がなく、特定の条件で問い合わせを絞り込む際には工夫が必要でした。 この問題を自動化後のCSレポートで解決しました。同じレポート内で過去問い合わせ検索用のページを作成し、問い合わせを検索できる機能を追加しました。各種絞り込みだけでなく、フリーワード検索もできるところがGoodなポイントです。 実際に使ってみて、Slack上での検索やスプレッドシートでの検索よりもとても便利な機能だと感じています。検索機能の作成もLockerStudioでは容易に実現できました。 未回答の問い合わせを検索できる お客様への返信漏れがないよう、未回答の問い合わせを検索できる機能を追加しました。既存運用ではCSがアラートを上げて初めて気づくパターンが多く、ここをエンジニア内で検知できるようになりました。 具体的には、スプレッドシートに記録される問い合わせ回答一覧の中で、”回答内容”という欄が空欄(null)となっているデータのみを抽出するフィルタを作成し、過去問い合わせをフィルタリングしています。 Looker Studioでは新規フィルターを画像のように簡単な設定だけで作成できます。”nullである”以外にも以下の画像のようにフィルターの種類を選択できます。 この機能によって、未回答の問い合わせのみを表示するグラフを作成でき、未回答の問い合わせを調べる時間を削減できました。 自動化後の効果 CSレポートを自動化した結果、以下のような効果が得られました。 レポート作成に掛ける工数削減 レポートの見やすさの向上 データのリアルタイム性向上 データ調査の自由度向上 元々は工数削減が大きなメリットだと考えていましたが、レポートを作成してみて、「見やすさ」という点もLooker Studio導入の大きなメリットだと感じました。 まとめ 本記事では、Looker Studioを活用したCSレポート自動化の事例を取り上げました。社内での活用事例がほとんどないツールでしたが、実際に使ってみるとたくさんのメリットがあり、導入による効果を感じています。本記事で紹介したCSレポート自動化で、Looker Studioのナレッジを社内で蓄えることができました。今後も新たなツールのナレッジを蓄えていくことで、業務効率化とより高い品質のサービス提供に役立てていきたいと思っています。 ZOZOでは、そんなサービスを一緒に作り上げてくれる方を募集中です。ご興味のある方は、下記のリンクからぜひご応募ください。 hrmos.co hrmos.co hrmos.co
アバター
こんにちは。カート決済部カート決済基盤ブロックの高橋です。 カート決済部では、現在Spring BootのJavaプロジェクトを運用しています。今回Spring Bootのバージョンアップを実施した際に発生した問題点と対応内容、注意点をご紹介します。加えて、使用しているライブラリなどのバージョンも上げているのでご紹介します。 アップデート前後のバージョン 種類 前バージョン 後バージョン Java 17 17 Spring Boot 2.7 3.0 Gradle 7.x 8.x SpringFox 3.0.0 - Springdoc-openapi - 2.1 openapi-generator 5.1 6.5 Spock Framework 2.1-groovy-3.0 2.4-M1-groovy-4.0 JavaはSpring Bootのバージョンアップ前からJava 17を使用しており、今回は変更していません。 Spring Bootのバージョンアップ 今回はSpring Bootの2.7から3.0にバージョンアップしています。 以下の表は 公式発表されているSpring BootのバージョンごとのOSSサポート期間 です。 バージョン2.6以前のサポート期間は終了しています。また、2.7や今回バージョンアップした3.0もそれぞれ2023年11月にOSSサポート期間が終了してしまいます。 現在のタイミングですと、3.1のリリースを待って対応でも良かったのですが、以下の理由からこのタイミングで3.0へのバージョンアップを実施しました。 新メンバーが既存プロジェクトに触れる良い機会であったため 3.1への対応をスムーズにするため javaxからjakartaパッケージに変更 Spring Boot 2.7までは、Spring Framework 5.3がベースでしたが、Spring Boot 3.0では、Spring Framework 6.0をベースとしています。 これにより、Java EEからJakarta EE9へ変更になっているため、パッケージの変更が必要になります。これは、パッケージ名を javax から jakarta に変更することで対応しました。 これと同時にJavaのベースバージョンも17となっているため、これより前のバージョンを使用している場合は、Javaのバージョンアップも同時に行う必要があります。 Spring MVCのURLマッチングの変更 Spring Framework 6.0では、URLの末尾のスラッシュにデフォルトで一致しなくなりました。 GET /hoge/ と GET /hoge は一致しなくなり、以下のようなコードでは、 GET /hoge/ はHTTP 404エラーを返すようになります。 @RestController public class HogeController { @GetMapping ( "/hoge" ) public String hoge() { return "Hoge" ; } } これらを一致させるためには以下の2つの方法があります。 1つ目は、以下の通り明示的に宣言することです。 @GetMapping("/hoge", "/hoge/") 2つ目は、Spring MVCのWebMvcConfigurerのconfigurePathMatchメソッドをオーバーライドすることで対応します。 @Configuration public class WebConfiguration implements WebMvcConfigurer { @Override public void configurePathMatch(PathMatchConfigurer configurer) { configurer.setUseTrailingSlashMatch( true ); } } setUseTrailingSlashMatchメソッドは非推奨になっているのでご注意ください。そのため、修正可能である場合は、呼び出し元のパスを統一する形に修正する方が良いと思います。 アクセスログの対応 Spring Boot 3.0以降では、 logback-access-spring-boot-starter が未対応のため、以下のように対応しています。 ライブラリの変更 - implementation "net.logstash.logback:logstash-logback-encoder:6.6" - implementation "net.rakugakibox.spring.boot:logback-access-spring-boot-starter:2.7.1" + implementation "net.logstash.logback:logstash-logback-encoder:7.3" + implementation "ch.qos.logback:logback-access:1.4.6" + implementation "org.codehaus.janino:janino:3.1.9" アプリケーションクラスに以下のBeanを追加 @Bean public WebServerFactoryCustomizer<ConfigurableTomcatWebServerFactory> webServerFactoryCustomizer() { var logbackValve = new LogbackValve(); logbackValve.setFilename( "sample-logback-access.xml" ); return (factory) -> factory.addEngineValves(logbackValve); } ライブラリの変更・バージョンアップ SpringFoxからSpringdoc-openapiへ変更 Spring Bootのバージョンアップと同時にSpringFoxからSpringdoc-openapiへの移行も行いました。SpringFoxがSpring Boot 3.0では動作しなくなってしまったため、これを機にSpringdoc-openapiへ移行しました。 対応内容は、依存ライブラリの変更です。 - implementation "io.springfox:springfox-boot-starter:3.0.0" + implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0" これに伴い、Controllerクラスで使用するアノテーションも変更になっています。 io.swagger.annotations 配下を使用していたものを io.swagger.v3.oas.annotations に変更しています。主なアノテーションの変更点をまとめたのが次の表です。 修正前アノテーション 修正後アノテーション @Api @Tag @ApiOperation @Operation @ApiResponse @ApiResponse @PostMapping @RequestMapping @ApiParam @Parameter 実際のコードは以下の通りです。 // 修正前 @Api (value = "Sample" , description = "the Sample API" ) public class SampleApiController { @ApiOperation ( value = "サンプル取得処理 " , nickname = "sampleRequests" , notes = "サンプル取得処理 " , response = SampleResult. class , tags={ "sampleRequests" , }) @ApiResponses (value = { @ApiResponse (code = 200 , message = "200 (OK)" , response = SampleResult. class ), @ApiResponse (code = 400 , message = "400 (Bad Request)" , response = BadRequest. class ) @ApiResponse (code = 500 , message = "500 (Internal Server Error)" , response = InternalServerError. class )}) @PostMapping ( value = "/sample" , produces = { "application/json" }, consumes = { "application/json" } ) public ResponseEntity<SampleResult> getSample( @ApiParam (value = "" ,required= true ) @Valid @RequestBody GetSampleRequests getSampleRequests, @ApiParam (value = "id" ,required= true ) @PathVariable ( "id" ) String id, @ApiParam (value = "header-id" ) @RequestHeader (value= "header-id" , required= false ) String headerId) { } // 修正後 @Tag (name = "Sample" , description = "the Sample API" ) public class SampleApiController { @Operation ( operationId = "sampleRequests" , summary = "サンプル取得処理 " , description = "サンプル取得処理 " , tags = { "sampleRequests" }, responses = { @ApiResponse (responseCode = "200" , description = "200 (OK)" , content = { @Content (mediaType = "application/json" , schema = @Schema (implementation = SampleResult. class )) }), @ApiResponse (responseCode = "400" , description = "400 (Bad Request)" , content = { @Content (mediaType = "application/json" , schema = @Schema (implementation = BadRequest. class )) }), @ApiResponse (responseCode = "500" , description = "500 (Internal Server Error)" , content = { @Content (mediaType = "application/json" , schema = @Schema (implementation = InternalServerError. class ))} } ) default ResponseEntity<SampleResult> getSample( @Parameter (name = "GetSampleRequests" , description = "" , required = true ) @Valid @RequestBody GetSampleRequests getSampleRequests, @Parameter (name = "id" , description = "id" , required = true , in = ParameterIn.PATH) @PathVariable ( "id" ) String id @Parameter (name = "header-id" , description = "Header Id" , in = ParameterIn.HEADER) @RequestHeader (value = "header-id" , required = false ) String headerId ) { } 次に、ライブラリの変更に伴いアプリケーションクラスの@EnableSwagger2のアノテーションを削除しています。 @SpringBootApplication - @EnableSwagger2 public class SampleApplication { public static void main(String[] args) { SpringApplication.run(SampleApplication.class, args); } } openapi-generatorのアップデート openapi-generatorもこの機会にアップデートをしています。今回のアップデートでは、 openapi.config に以下のような設定を追加・削除しています。 - "java8": true, + "useSpringBoot3": true, + "generatedConstructorWithRequiredArgs": false, generatedConstructorWithRequiredArgs を追加したのは、Lombokのアノテーションと競合してしまうのを防ぐためです。というのは、以下のように必須項目であるrequiredの定義をして自動生成したときにコンストラクタが自動生成されてしまうためです。 type : object properties : item_id : $ref : ./item_id.yaml required : - item_id Spock Frameworkのアップデート テストフレームワークのSpock Frameworkを使用しています。これは以下の通り、groovyのバージョンと共にSpock Frameworkのバージョンアップをすることで対応できました。 - testImplementation "org.codehaus.groovy:groovy-all:3.0.8" - testImplementation "org.spockframework:spock-core:2.0-groovy-3.0" - testImplementation "org.spockframework:spock-spring:2.0-groovy-3.0" - testImplementation "org.spockframework:spock-guice:2.0-groovy-3.0" + testImplementation "org.apache.groovy:groovy-all:4.0.11" + testImplementation "org.spockframework:spock-core:2.4-M1-groovy-4.0" + testImplementation "org.spockframework:spock-spring:2.4-M1-groovy-4.0" + testImplementation "org.spockframework:spock-guice:2.4-M1-groovy-4.0" まとめ Spring Bootの2.7から3.0へのバージョンアップに伴う変更内容としては、以下の通りです。 ベースとなるSpring Frameworkのバージョン変更によるパッケージの変更 Spring MVCのURLマッチングの変更 アクセスログのライブラリ変更 Spring Bootのバージョンアップをしたことで、SpringFoxが使えなくなってしまうということもありました。最初にも書きましたが、Spring Boot 2.7と3.0のOSSサポート期間が2023年11月です。そのため、Spring Boot 2.7以下を使用している場合は、早めに3.0以上にバージョンアップしておくのが良いと思いました。 最後に カート決済部では、仲間を募集しています。ご興味のある方は、こちらからご応募ください。 hrmos.co
アバター
こんにちは、バックエンドエンジニアの近です! 4/24〜4/26にかけてアトランタで開催されたRailsConf 2023にWEARバックエンドブロックから近・小山・高久の3人が参加しました。 去年はコロナの影響もあってオンラインの開催だったのですが、今年はオフラインでの開催となり、大勢が参加していて大盛況でした。 我々が開発・運営しているファッションコーディネートアプリ「 WEAR 」のバックエンドはRuby on Railsで開発しています。現在では、新機能の開発やリプレイスなど、チームメンバーの全員がRuby on Railsに関わっているため、今回RailsConfにて様々なセッションを聞けたことはとても有意義な経験でした。 RailsConfとは 1年に1回開催されるRuby on Railsに関する世界最大のカンファレンスとなります。( 公式サイト ) 2020〜2022年はコロナの影響でオンライン開催でしたが、2023年は4年ぶりのオフライン開催となりました。 また毎年開催地が変わり、今年はアメリカのアトランタで行われました。 カンファレンスの様子 以下がRailsConfのメイン会場でした。かなり広かったです。スピーカーの文字起こしもあったので、とてもありがたかったです。 会場はメイン会場に加えて、サブ会場が5箇所もあり、全体的にとても賑わっていました。 また、アメリカの様々なテック企業が参加していて、自分達も知っている所だとGitHub, Shopifyから、アメリカのベンチャー企業まで様々な人たちが参加していました。 発表の種類としてはRailsCoreの話や新しいgemの紹介、マイクロサービスについてなどの技術的な話からエンジニアチームの組織作りやメンターとメンティーの関係構築、ペアプロについての話がありました。更にはワークショップ形式でRailsのバージョンアップを皆で一緒にしたりなど、幅広く様々なセッションやワークショップが行われていました。 また、最終日には1人5分でLTをする時間があり、そこでは自分のエンジニア人生の話やRubyで作った便利ツールの紹介、OSSにコミットした話など、皆気軽にトークしていてとても面白かったです。 この記事では、その中から私たちが興味を持ったセッションをいくつか紹介したいと思います! セッション紹介 Exploring the Power of Turbo Streams & Action Cable バックエンドエンジニアの高久です。Kevin Liebholzさんの「Exploring the Power of Turbo Streams & Action Cable」についてご紹介します。 Rails 7ではよりリッチなフロントエンドを実現するためのHotwire、Turboといったライブラリがデフォルトでインストールされるようになりました。 そして、その機能の1つ「Turbo Streams」と既存の「Action Cable」を使ってリアルタイム通信を使ったWebページが簡単に実現できるようになりました。 Action CableとはRailsでWebSocketを利用したリアルタイム通信を実現するフレームワークです。Rails 5から実装されています。 また、Turbo Streamsとは <turbo-stream></turbo-stream> で囲まれたHTML要素をさまざまなソースをトリガーにして更新できる機能です。 Turbo StreamsとAction Cableを組み合わせることによって、WebSockets通信によるサーバ/クライアント間のリアルタイムでの通信&画面描画を簡単に実現できます。 このセッションでは「まるばつゲーム」のWebページ実装をデモとして、それらのツールを使ってどのように実装するかを紹介していました。 詳しいコードは以下のセッション資料に記載されていますので、気になる方は見てみてください。 speakerdeck.com An imposter's guide to growth in engineering 次も高久よりEbun Segunさんの「An imposter's guide to growth in engineering」のセッションを紹介します。 RailsConfでは技術系の話だけではなく、キャリア・成長に関するセッションが複数ありました。このセッションではインポスター症候群について概要や、成長との関わり、エンジニアに関連した症状、対処法について話されていました。 インポスター症候群とは「明らかな成功にもかかわらず、圧倒的に不十分だと自己を過小評価してしまう傾向」のことを指します。明らかな成功とは「客観的にみて評価されるべき成果を上げた」ということで、ジュニアレベルを卒業したエンジニアに多くみられるそうです。 「インポスター症候群」という名前を自分は聞いたことがなかったのですが、セッション中の挙手によるアンケートでは会場にいた人のほとんどが知っており、また多くの人が経験していると答えていました。世界的には一般的なようです。 症状として「自分は詐欺師のようだ」「自分が思うよりも周りから賢いと思われている」「みんなを失望させるのが怖い」「自分にはこんなことはできないと思う」というような発言をすることが挙げられます。またエンジニア特有の行動として、以下が挙げられます。 話さない、質問しない 話せば話すほど、自分がインポスターだと思われてしまうような気がするため 会議の場で話さなかったり、とりあえず知的な人の意見に同意してしまう 自分の仕事について話さなければいけない時、いつも緊張してしまう 成功するために他のことを探してしまう 自分が生産的だと思われたいため 目の前の難しいプロジェクトより、自分ができるバグ潰しに専念してしまう コードの完璧主義 じっくりと時間をかけてコードを完璧にする プルリクエストを出す前に、自分のコードにシミや傷がないことを確認する これらの行動は個人の成長の妨げになり得ます。しかし決して悪いものではなく、まだ成長の余白があることを示しています。 これらを解決するためにEbun Segunさんは「チームやプロジェクトにおける自分の存在意義を把握する」「自分がすでに知っていることの価値に気づく」が大事であると述べていました。セッションではさらに詳細な話をされていました。 ここで紹介されていた症状が自分にも当てはまることが多く、胸が痛いと共に良い気づきを得ることができ、自身の成長に繋げられそうなセッションだったと感じました。また国や文化に関係なく、みんなが起きていること、悩んでいることを知れることが海外カンファレンスのいいところであると実感しました。 Upgrading Rails: The Dual-Boot Way バックエンドエンジニアの小山です。 RailsConfはセッションだけではなくワークショップも充実していました。今回私はRailsアップグレードのプロセスを学ぶワークショップに参加してきました。 こちらのワークショップは参加者30名程で、講師が用意してくれたスライドの解説を聴きながら、クローンしたサンプルアプリケーションをRails 6.1から7.0にアップグレードするという内容でした。 next_railsというgemを導入し、Railsを複数バージョンで起動するDual Bootという手法を使い、Railsをアップグレードしました。アップグレードの過程で非推奨の警告を確認しRailsとgem依存関係を確認しながらパッチを当てていきました。 next_railsについて補足すると、このgemをインストールすることで、 next --init コマンドが使えるようになります。 このコマンドによりDual Boot用の Gemfile.next と Gemfile.next.lock が生成されます。 next bundle install を実行することで、新しいRailsバージョンに互換性のあるgemをインストールできます。 また、 next rails s でサーバーを起動すると、 Gemfile.next を使用してサーバーが起動されます。 next_railsの詳細は以下のリポジトリを参照してください。 https://github.com/fastruby/next_rails ワークショップの内容をかいつまんでご紹介します。 最初にクローンしたアプリケーションに対してgem next_railsをインストールしてDual Boot用の Gemfile を生成します。 # Add Gemfile group :development , :test do gem ' next_rails ' end $ bundle install $ next --init 次にnext_rails用の設定を Gemfile に追記します。 # Add Gemfile def next? File .basename( __FILE__ ) == " Gemfile.next " end if next? gem ' rails ' , ' ~> 7.0.4.3 ' else gem ' rails ' , ' 6.1.7 ' end railsバージョンをアップデートします。 $ next bundle update rails 今回のRails 6.1から7.0へのアップグレードではbundlerがgem "railties" で互換性のあるバージョンを見つけられずエラーを吐きました。 Bundler could not find compatible versions for gem "railties": In Gemfile.next: activeadmin (~> 2.10.1) was resolved to 2.10.1, which depends on railties (>= 6.0, < 6.2) rails (= 7.0.4.3) was resolved to 7.0.4.3, which depends on railties (= 7.0.4.3) activeadminのバージョンを指定することで解消されるため Gemfile に以下の修正を加えてアップデートしました。 # Fix Gemfile if next? gem ‘activeadmin‘ else gem ‘activeadmin ' , ‘~> 2.10.1’ end $ next bundle update rails activeadmin また、ZeitwerkはRails 6で導入され、Rails 7では必須になっています。Zeitwerkの互換性を確認するタスクを実行するとAPIという定数が存在しないとエラーが出力されます。 $ next bin/rails zeitwerk:check rails aborted! NameError: uninitialized constant API mount API::Base => '/api' ^^^ Did you mean? Api こちらは ActiveSupport::Inflector の語尾の活用機能を用いて略語を指定することでZeitwerkオートローダーの入力時のエラーを修正できます。 # Add to config/initializers/inflections.rb ActiveSupport :: Inflector .inflections( :en ) do |inflect| inflect.acronym " API " end 最後にデフォルトの各バージョンのRailsの設定を読み込むためにload_defaultsを修正します。この更新を忘れると非推奨の警告がでます。 # Fix Gemfile def next? $next_rails = File .basename( __FILE__ ) == " Gemfile.next " end # Fix config/application.rb if $next_rails config.load_defaults 7.0 else config.load_defaults 6.1 # or stay with 5.0 end これらの修正を加えることで非推奨の警告を解消しgemの依存関係を解消してDual BootでRailsを起動できるようになりました。 弊社のサービスであるWEARでは現在Rails 6.0を使用しており7.0へのアップグレードを予定しています。そのため、今回、ワークショップ形式で手を動かしながら7.0へのアップグレードを実践できたことはとても良い経験になりました。 ワークショップで使用した資料が共有されているので、もし興味がある方は手を動かして試してみてください。 docs.google.com Migrating Shopify's Core Rails Monolith to Trilogy バックエンドエンジニアの近です! 自分からはAdrianna Chang氏の「Migrating Shopify's Core Rails Monolith to Trilogy」というセッションの紹介をしたいと思います。 このセッションでは、TrilogyというMySQL用のクライアントライブラリをShopifyのRailsコアに適用するにあたっての追加機能や、デプロイ時に当たった問題とその解決法の紹介をしていました。 RailsとMySQLを繋げるクライアントライブラリとして有名なのはmysql2がありますが、新しいアダプタとしてTrilogyというものがあるのは今回初めて知りました。 Trilogyの特徴として、以下があるそうです。 2022年にGitHubによってOSS化されたMySQLクライアント 独自の低レベルネットワークプロトコル実装 テキスト プロトコルの最も頻繁に使用される以下をサポート ハンドシェイク パスワード認証 クエリ、ping、および終了コマンド 最小限の動的メモリ割り当て、メモリ効率の最適化 柔軟性、性能、組み込みやすさを追求したデザイン また、他のアダプタと比較したときのTrilogyに乗り換えるメリットとして、以下を挙げていました。 コンパイルに必要な依存関係が少ない libmysqlclient / libmariadbへの依存がなく、インストールがシンプル クライアントとサーバーのバージョンの不一致問題を解消 パケットを扱う際のデータのコピー回数を最小限に抑えられる Ruby VMのコンテキストで効率的に動作するよう設計 動的メモリ割り当ての意識的な使用 可能な限りノンブロッキング操作とI/Oコールバックを使用するようAPIが設計されている 確かに、インストール時にlibmysqlclient等の依存関係でエラーにハマったことのある人は多くいると思うので、この辺りが解消され、更にパフォーマンスの向上が期待できるのは嬉しいですね。 上に書いたメリットの中で、さらにShopifyがTrilogyに乗り換えたい強い理由として、以下が挙げられていました。 より良い開発体験 クエリパフォーマンスの高速化 強い保守性 これに加えて、Trilogyをコミュニティのスタンダードにしたいという思いもあるようです。 次に、MySQLがサポートしているマルチステートメント機能の話題になりました。 MySQLでは、セミコロンで区切られた複数のステートメントを含む文字列の実行をサポートしています。 これを、Trilogyを用いたRubyにて記述しようとすると、以下のようになります。 require ' trilogy ' client = Trilogy .new( host : ' 127.0.0.1 ' , port : 3306 , username : ' root ' , multi_statement : true , ) sql = <<- SQL DROP TABLE IF EXISTS users; CREATE TABLE users (name VARCHAR(255)); INSERT INTO users VALUES ('John'); SELECT * FROM users; SQL client.query(sql) マルチステートメントクエリは通常のクエリより高いパフォーマンスを発揮するため、本来上記のように書きたいところなのですが、当時、Trilogyはマルチステートメントの対応はされていませんでした。 Shopifyでは1000を超えるサンプルデータを持っていて、それらが更に100件のデータを持っていたりしているので、これらを効率的にDBへインサートするため、この機能を活用したいと考えていました。 mysql2アダプタでは既にこのマルチステートメントがサポートされているので、Trilogyでもサポートしたいとのことでした。 そして、 multi_statement: trueという構文をサポートするために、C拡張を用いて実装し、その紹介も行っていました。 static VALUE rb_trilogy_initialize (VALUE self, VALUE opts) // ← optsを追加 { struct trilogy_ctx *ctx = get_ctx (self); trilogy_sockopt_t connopt = { 0 }; trilogy_handshake_t handshake; VALUE val; Check_Type (opts, T_HASH); ... if ( RTEST ( rb_hash_aref (opts, ID2SYM (id_multi_statement)))) { // ← connopt.flags |= TRILOGY_CAPABILITIES_MULTI_STATEMENTS; // ← } ... } static VALUE rb_trilogy_more_results_exist (VALUE self) // ← { struct trilogy_ctx *ctx = get_open_ctx (self); if (ctx->conn.server_status & TRILOGY_SERVER_STATUS_MORE_RESULTS_EXISTS) { return Qtrue; } else { return Qfalse; } } static VALUE rb_trilogy_next_result (VALUE self) // ← { struct trilogy_ctx *ctx = get_open_ctx (self); if (!(ctx->conn.server_status & TRILOGY_SERVER_STATUS_MORE_RESULTS_EXISTS)) { return Qnil; 実際のPRはこちらです。 中身はC言語ですが、セッションで紹介している内容が実際に見られて面白いです。 https://github.com/github/trilogy/pull/57 https://github.com/github/trilogy/pull/35 他にも、当日のセッションではマルチステートメントの詳細な説明やTrilogy C APIとRubyの連携・実装の話など詳細に語っていて面白かったのですが、長くなるので触りだけの紹介でした。 次に、Trilogyを実際にShopifyの本番環境にデプロイした時の話になりました。 最初にCIをオールグリーンにするため、以下を修正していました。 まず、MySQLクライアントをMysql2からTrilogyに書き換え Trilogy用に、クライアントの小さなAPI変更に対応 Trilogy用のエラーハンドリング コードは全てMysql2::Errorになっていたため、それらをTrilogy::Errorに変更 もちろん、エラーメッセージも変わるのでテストなどの修正 思いの外、簡単に修正が済んだようです。 この辺りでShopifyのインフラ構成の紹介になりました。 Railsのコアアプリケーションはモジュラーモノリス型 ピーク時は1秒間に1400万クエリが実行される(!) DB周りのインフラ 水平パーティショニング MySQLインスタンスはGoogle Cloud上で動作し、Chefで管理されている ProxySQL クライアント接続数:10万 バックエンド接続数:2万 1秒間に1400万クエリは全く想像がつきません。 Trilogyのデプロイにあたり、まずは本番環境の1%で動作させたところ、スムーズに動作していたのですが、次に50%でデプロイしてみたところ、エラーレートが上昇しリバートすることになったそうです。 なにがあったのか? ProxySQLはバックエンド接続のプールを保持する クライアント機能が異なる場合、ProxySQLはCOM_CHANGE_USERを実行する必要があった mysql2とTrilogyの両方を用いる場合、ProxySQLではバックエンドを切り替える際に新しい接続オプションを設定するCOM_CHANGE_USERコマンドを実行する必要があった というような問題がおき、エラーレートが上昇したとのことでした。 加えて、ProxySQLのpod接続状況を確認したところ、mysql2ではCLIENT_MULTI_RESULTSが設定されているのに、Trilogyでは設定されていなかったのも原因だったそうです。 これらを修正し…再度、本番環境に100%デプロイしたところ、無事動作し、なんとMysql2に比べて22%もパフォーマンスが向上していました。 Request Time: Mysql2: Avg 3.46ms Trilogy: Avg 2.70ms -22% faster また、クエリタイムも17%ほど速くなっていたとのこと。 MySQL query time: Mysql2: Avg 1.49ms Trilogy: Avg 1.24 ms -17% faster DBのクライアントを変更しただけでリクエストタイム、クエリタイム共に高速化されているのは非常に凄いですね。特に、Shopifyのような大量のユーザーを抱えているサービスでの恩恵は大きそうです。 今まではGitHubのみが本番運用していたTrilogyですが、今回OSS化されたことによってShopifyのように今後導入するサービスが増えていきそうなので是非チェックしてみてください! 今回紹介したスライドは以下になります。 speakerdeck.com おまけ 今回参加した3人は海外カンファレンス初参加だったので、慣れない環境での生活や英語でのコミュニケーションに四苦八苦しながらの参加だったのですが、現地の人たちは皆優しく接してくれて、とても助かりました。 日本と違うなーと感じた部分はセッション中に拍手が起きたり、(面白いシーンで)笑いが多かったり、隣の席の人とフランクに喋っていたりと、アメリカンな一面が見えました。また全体を通してセッション終了後のQ&Aにて積極的に質問する姿も見られました。 セッション後にお菓子を食べながら参加者同士でコミュニケーションを取る時間がありました。英語に自信がなかったのでドキドキしていたのですが、最初に雑談した相手がなんと日本語を喋れて盛り上がったりなど、とても面白かったです(LinkedInを交換することに成功しました) ソーシャルコミュニケーションを取る様子 また、会場ではお昼ご飯が提供されていたのですが、日本に比べて全体的に味が濃かったです。開催がアトランタということもあり、南部の郷土料理である「グリッツ」を食べることもできました。 ランチはビュッフェ形式。とても美味しかったですが、味は濃いめでした。 ランチの様子 アメリカ南部の郷土料理「グリッツ」。ベーコンが入っている。おかゆのような食感で、味はコーンスープみたいな独特な感じでした。見た目では伝わりにくいですが美味しかったです! RailsConf公式でホテルが準備されており、格安で泊まることができました。 ホテルからカンファレンス会場へは専用通路で外に出ず行くことができます。出不精にとっては最高の環境です。 最終日のKeynoteを担当していたAaron Patterson氏と記念撮影もしていただきました! 来年のRailsConfの開催地の発表もありました。次回はデトロイトでの開催を予定しているそうです。 最後に ZOZOではセミナー・カンファレンスへの参加を支援する福利厚生があり、カンファレンス参加に関わる渡航費・宿泊費などは全て会社に負担してもらっています。ZOZOでは引き続きRubyエンジニアを募集しています。以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
こんにちは、MA部の谷口( case-k )と @gachi-muchi-engineer です。 私達のチームではマーケティングオートメーションシステムの開発や運用をしています。ZOZOTOWNではマーケティングオートメーションによって、メールやPush、LINEなど各チャンネルに対して日々配信しています。配信方法は大きく2種類に分けられ、特定のユーザーセグメント向けの「マス配信」と、個別のユーザーに最適化された「パーソナライズ配信」があります。パーソナライズ配信基盤を社内ではリアルタイムマーケティングシステム「RTM」と呼んでいます。リアルタイムマーケティングシステムは随分と前に作られたこともあり、現在リアルタイムマーケティングシステム全体のリプレイスを進めています。本記事ではリアルタイムマーケティングシステムで用いられている、リアルタイムデータ連携基盤をリプレイスした事例をご紹介します。 既存のリアルタイムデータ連携システムの紹介 既存のリアルタイムデータ連携の仕組み なぜリプレイスをしたのか Windows Serverの運用負荷が高い リアルタイムマーケティングシステム全体のリプレイスに必要なデータ連携基盤が必要 リプレイス後の配信用リアルタイムデータ基盤 安全にリプレイスするための方針 変更前後のデータを取得する方法を検討 変更前後のデータを取得するクエリ 変更ログの取得 データ連携実績ログの取得 変更ログの集計(変更データの取得) データ連携実績ログの集計(変更前データの取得) データ連携実績ログに含まれていない変更前データの取得 変更前後のデータをマージ アーキテクチャ概要と処理の流れ 変更前後のデータ取得 データ連携実績テーブル メッセージブローカーへ連携 データ連携API(Analyzer) Appendix:データ連携API(Push/LINE配信基盤) 最終同期メッセージを書き込む 初回の全量データ連携 移行前後の評価 データの整合性を評価 データの欠損を評価 データの遅延時間を評価 監視設計 プロデューサの監視 コンシューマの監視 リプレイスによる改善点 リアルタイムマーケティングシステム全体のリプレイスに必要な基盤を構築 運用負荷の軽減 今後の課題 パフォーマンスの改善 初回全量データ連携処理の完全移行 まとめ 最後に 既存のリアルタイムデータ連携システムの紹介 既存のリアルタイムデータ連携システムについて紹介します。既存のリアルタイムデータ連携システムでは配信処理に必要なデータをリアルタイムにSQL Serverから取得しています。SQL Serverの変更データを検知して、必要な加工処理を施し、リアルタイムマーケティングシステムへ連携しています。連携されたデータはリアルタイムマーケティングシステムにキャッシュされ配信処理で使われます。ZOZO固有のユーザIDを配信用のトークンへ変換したり、配信のトリガーとしても用いられています。例えば在庫切れを起こしていた商品が入荷されたのをトリガーに配信する仕組みがあります。 以降、リアルタイムデータ連携基盤を「Tracker」、連携されたデータを用いて配信処理をしているアプリケーションを「Analyzer」と呼びます。TrackerとAnalyzerを含む基盤がリアルタイムマーケティングシステム「RTM」です。本記事ではTrackerをリプレイスした事例をご紹介します。 RTMについては以下の記事をご確認ください。 techblog.zozo.com 既存のリアルタイムデータ連携の仕組み リプレイス前のTrackerのデータ連携の仕組みについてご紹介します。 TrackerはJavaで書かれており、Windows Server上のクラスタにデプロイされていました。TrackerはSQL Serverで変更のあったデータを取得し、加工処理を施した上でデータを連携しています。 SQL Serverの変更データの取得にはChange Trackingと呼ばれるSQL Serverの機能を用いています。変更追跡を用いて60秒に1回SQL Serverへクエリを投げ、Analyzerで必要となる加工処理を施しています。加工されたデータは各テーブルごとに定義されたAnalyzerのエンドポイントへリクエストされます。リクエストされたデータはAnalyzerでキャッシュされています。 Trackerで定期的に投げている変更追跡クエリは以下のようになっています。 SELECT a.SYS_CHANGE_OPERATION as changetrack_type, a.SYS_CHANGE_VERSION as changetrack_ver, #{columns} FROM CHANGETABLE(CHANGES #{@tablename}, @前回更新したバージョン) AS a LEFT OUTER JOIN #{@tablename} ON a.#{@primary_key} = b.#{@primary_key} 変更追跡のバージョンは「SYS_CHANGE_VERSION」で取得でき、変更があるとインクリメントされます。最終同期した変更追跡のバージョン「@前回更新したバージョン」を渡すことで、渡したバージョン以降に変更のあったプライマリーキーを取得できます。取得したプライマリーキーを変更のあったテーブルと「LEFT JOIN」することで、変更後のデータを取得できます。変更追跡で取得できるのは変更後の最新のデータのみです。変更履歴の取得はできません。変更タイプには「SYS_CHANGE_OPERATION」には以下の3つの種類が存在します。どのような変更がSQL Serverで実施されたか確認できます。 変更タイプ 説明 I 新規登録 U 更新 D 削除 SQL Serverの変更追跡については以下の記事をご確認ください。 learn.microsoft.com また、Trackerでは変更前のデータも連携していました。Analyzerのキャッシュにはインメモリなデータストアを採用しており、KeyValue形式でデータを保存します。一部のキャッシュはKEYとしてSQL Serverのプライマリーキーではない、メンバーIDやEmailIDを用いています。データの削除や更新があった際これらのキャッシュに対して処理が必要でした。 変更追跡の仕組み上、取得できるのは変更のあったプライマリーキーと変更後のデータのみです。そこで、変更前のデータを取得するために、SQL Serverのトリガー機能を用いていました。トリガーとは、ストアドプロシージャに分類され、SQL Serverでイベントが発生したときに自動的に実行されます。今回は変更前のデータが必要であるため、トリガー機能の1つであるDMLトリガーを利用します。DMLトリガーはDMLイベントを介してデータを変更したときに実行されるトリガーです。DMLトリガーではdeletedとinsertedテーブルという2つの特別なテーブルが使用されます。この2つのテーブルはSQL Serverが自動で作成し、管理しています。 このテーブルの役割は以下の通りです。この2つのトリガーテーブルを用いて、Trackerでは変更前のデータを取得しています。 テーブル名 説明 deleted 「DELETE」または「UPDATE」で変更される前に、影響を受ける行のコピー inserted 「INSERT」または「UPDATE」の後に、新しいまたは変更された行のコピー 変更前のデータを取得する際は以下のようなトリガーを用いていました。物理削除された変更前のデータを取得する場合は直接deletedテーブルから取得するのではなく、データの整合性を担保するため別テーブルへ書き出したデータを利用していました。このテーブルからデータを取得することで、変更前のデータをAnalyzerに連携していました。 CREATE TRIGGER [Database].[SaveDeletedTable] ON [Database].[ Table ] AFTER INSERT , DELETE AS BEGIN -- SET NOCOUNT ON added to prevent extra result sets from -- interfering with SELECT statements. SET NOCOUNT ON ; -- delete unused primary key from DeletedTable DELETE FROM Database.DeletedTable WHERE primary_key IN ( SELECT primary_key FROM deleted UNION ALL SELECT primary_key FROM inserted); INSERT INTO dbo.DeletedTable SELECT * FROM deleted; END SQL Serverのトリガー機能の詳細は以下の記事をご確認ください。 learn.microsoft.com なぜリプレイスをしたのか なぜTrackerをリプレイスをしたのかご紹介します。 Windows Serverの運用負荷が高い TrackerはWindows Server上で運用されていました。元々社外で作られた基盤であったため、チーム内にWindows Serverの知見も少なく、インフラのコード化やデプロイの自動化が難しいところもありました。また、Windows Serverのライセンス費用も高額でした。運用負荷の高いWindows Serverから脱却し、SQL Serverも廃止したいと考えていました。 リアルタイムマーケティングシステム全体のリプレイスに必要なデータ連携基盤が必要 リアルタイムマーケティングシステムの全体のリプレイスを進める上で、Analyzer以外の各マイクロサービスへもリアルタイムにデータ連携できる仕組みが必要でした。汎用的な要件に対応できる、同じような仕組みを作りたいと考えていました。今回のタイミングで新しく汎用的な基盤を構築し、Trackerをリプレイスすることにしました。 リプレイス後の配信用リアルタイムデータ基盤 先に述べたような課題があるため、既存の配信用リアルタイムデータ基盤の課題をリプレイスしました。以降リプレイス後の配信用リアルタイムデータ基盤を「新Tracker」、リプレイス前の基盤を「旧Tracker」と呼びます。 安全にリプレイスするための方針 既に述べたように旧Trackerでは変更前のデータをトリガー機能を用いて取得していました。リプレイスを進めるにあたり、変更前のデータを取得する方法を検討する必要がありました。本来は変更前のデータをリアルタイムマーケティングシステム側のキャッシュからとる方が望ましいです。しかし、連携先のAnalyzerに大きな手を加えることは困難でした。Analyzerのデプロイには数時間かかります。冒頭で紹介したとおり、インメモリなデータストアなので、障害等があった場合メモリ上のデータが吹き飛ぶ懸念もあります。データのリカバリにも8〜9時間ほどがかかり、ロールバックさせるのは難しい状態でした。また、移行対象となるクエリも22テーブルほどあり、クエリにて複雑な加工処理を施していました。 そこで、Analyzer側には手を加えない方針でリプレイスを進めることにしました。もしリプレイス後に問題が発生しても、Analyzer側に手を加えていなければ旧Trackerに切り戻しが可能です。また、リプレイスに伴うデータ評価の点でも、旧Trackerと新Trackerで出力されるデータを揃えることでデータの評価が可能になります。 変更前のデータをリアルタイムマーケティングシステムのキャッシュから取るようにすることは、新Trackerへリプレイス後でも可能で容易になります。今回のリプレイスが完了した後、対応していくことにしました。 変更前後のデータを取得する方法を検討 新Trackerでは、BigQuery上に構築された全社共通のデータ基盤であるリアルタイムデータ基盤から変更データを取得しています。リアルタイムデータ基盤にすることで、SQL Serverから脱却し、ライセンス費用等のコストや運用負荷の軽減、パフォーマンス面での改善が期待できます。 リアルタイムデータ基盤は数年前に作られ、同じようにSQL Serverの変更データを変更追跡の機能を用いて、リアルタイムでBigQueryへデータ連携しています。旧Trackerは全社共通のリアルタイムデータ基盤ができる前からあったシステムのため、独自でSQL Serverの変更データを集めていました。今回のリプレイスのタイミングでリアルタイムデータ基盤からデータを取得することにしました。 リアルタイムデータ基盤の詳細は以下の記事をご確認ください。 techblog.zozo.com データソースをSQL Serverからリアルタイムデータ基盤にしたことで以下を考慮する必要がありました。 データの重複 データの順序 変更前データの取得方法 旧TrackerではSQL Serverの変更追跡を用いて、変更のあったデータを取得しているため、取得したデータの順序は保証されており、データの重複もありませんでした。 しかし、リアルタイムデータ基盤では運用しやすいよう「at-least-once」な設計になっています。データは重複し、遅延データも入るため順序は保証されていません。 また、旧TrackerではSQL Serverのトリガー機能を用いて変更前のデータを取得していました。リアルタイムデータ基盤へ移行したことで、データの整合性を担保しつつ変更前のデータを取得する方法の検討が必要になります。 変更前後のデータを取得するクエリ これらの要件を満たすために、リアルタイムデータ基盤からデータを取得する際にデータの重複排除と順序保証をしています。また、変更前のデータは配信基盤へ連携済みの実績テーブルから取得するようにしました。具体的にどのようなクエリを実行しているかご紹介します。 変更前後のデータを取得するクエリはテーブル関数として用意しています。次のようにタイムスタンプを渡すことで、渡したタイムスタンプ以降に変更のあった変更前後のデータを取得できるようにしています。 テーブル関数の使い方は次の通りです。 SELECT * FROM `< table ID>`( ' 2023-05-01 ' ) テーブル関数には以下の2種類用意しています。 変更後のデータのみ取得する関数 変更前後のデータを取得する関数 テーブル関数内で具体的にどのような処理をしているかご紹介します。 変更ログの取得 リアルタイムデータ基盤より、変更のあったデータを取得しています。取得したデータは順序保証されておらず、データの重複もあります。詳細は後述の「変更ログの集計(変更データの取得)」でご紹介しますが、順序を保証し、データの重複を排除するためにプライマリーキーが必要になります。対象テーブルのプライマリーキーをカラムとして作ります。クエリ内の「last_sync_time」はTIMESTAMP型で、テーブル関数から渡されるパラメータです。最終同期したデータの時刻を渡すことで、該当時刻より後に変更のあったデータを取得できます。 取得期間を3時間にしているのは、リアルタイムデータ基盤の遅延データに対応するためです。前述したとおり、リアルタイムデータ基盤では順序保証されていないため遅延データを考慮する必要があります。実際3時間も遅れることはないのですが、最終同期された時刻である「last_sync_time」に遅延時間を考慮して変更データを取得しています。遅延データを考慮しないと、変更ログの取得の際にパーティション外となりデータが欠損してしまいます。変更ログの取得に旧Trackerのように変更追跡のバージョンではなく、タイムスタンプを用いているのも順序保証されず遅延した際のデータ欠損を防ぐためです。 streaming AS ( SELECT *, CONCAT (${ join ( " , " ,primary_key)}) AS primary_key FROM `${project_changetracking}.${dataset_changetracking}.${table_changetracking}` WHERE bigquery_insert_time >= TIMESTAMP_SUB( CAST ( FORMAT_TIMESTAMP( " %Y-%m-%d " , TIMESTAMP_SUB(last_sync_time , INTERVAL 3 Hour) , " Asia/Tokyo " ) AS timestamp ) , INTERVAL 9 HOUR ) ) データ連携実績ログの取得 クエリで取得した変更前後のデータは実績テーブルに書き込まれます。以降「データ連携実績テーブル」と呼びます。データ連携実績テーブルの用途は後述しますが、データ連携実績テーブルに書き込まれたデータは書き込まれた順に各サービスへデータ連携されます。こうすることで、データ連携実績テーブルから変更前のデータを取得することで、Analyzerにキャッシュされているデータとの整合性をとることができます。 また、リアルタイムデータ基盤で取得したログから連携済みの実績を排除するためにも利用しています。この後の「変更前後のデータをマージ」にて説明します。 event_sync_logs AS ( SELECT realtime_message_unique_id, realtime_changetrack_ver, CONCAT (${ join ( " , " ,primary_key)}) AS primary_key,tracking_type FROM `${project}.${tracking_event_log_dataset}.${table_base}` WHEREå tracking_start_time >= TIMESTAMP_SUB( CAST ( FORMAT_TIMESTAMP( " %Y-%m-%d " , TIMESTAMP_SUB(last_sync_time , INTERVAL 36 Hour) , " Asia/Tokyo " ) AS timestamp ) , INTERVAL 9 HOUR ) ) 取得期間を36時間にしているのは、変更前のデータの取得に全社共通データ基盤の全量データを用いるためです。データ連携実績ログで取得できるデータの範囲に変更前のログが含まれているとは限りません。例えば最後に更新されたログが5日前の場合パーティションの範囲外となります。 全社共通データ基盤では日次のバッチ処理で、SQL Serverにあるテーブルを全件BigQueryへ連携しています。もし、データ連携実績ログの取得の際、取得期間を数時間にしてしまうと、全社共通データ基盤では日次のバッチ処理連携後に変更のあった一部のデータが欠損してしまいます。日次連携された時刻よりも前から取得することでデータの欠損を防ぐことができます。データ連携側の遅延も考慮して、36時間としています。 全社共通データ基盤については以下の記事をご確認ください。 techblog.zozo.com 変更ログの集計(変更データの取得) SQL Serverから共通基盤であるリアルタイムデータ連携基盤へのデータ連携には冒頭でご紹介したSQL Serverの変更追跡を用いています。テーブルのプライマリーキーと最新の変更追跡バージョンを集計し、変更履歴とJOINすることで最新の変更データを取得できます。この集計により、リアルタイムデータ基盤内のデータ重複を排除し、順序の保証もしています。変更後のデータのみ必要な場合は後述している変更前のデータを取得する処理は不要です。 streaming_latest_version AS ( SELECT primary_key, MAX (changetrack_ver) AS changetrack_ver_max, FROM streaming GROUP BY primary_key ), streaming_latest AS ( SELECT streaming.* FROM streaming INNER join streaming_latest_version ON streaming.primary_key = streaming_latest_version.primary_key AND streaming.changetrack_ver = streaming_latest_version.changetrack_ver_max ), データ連携実績ログの集計(変更前データの取得) データ連携実績ログより変更前のログを取得します。データ連携実績ログ内のデータを変更前のデータとして利用します。 streaming_before_latest_version AS ( SELECT primary_key, MAX (realtime_changetrack_ver) AS realtime_changetrack_ver_max FROM event_sync_logs WHERE primary_key IN ( SELECT primary_key FROM streaming_latest ) AND tracking_type = 0 GROUP BY primary_key ), streaming_before_latest AS ( SELECT a.* FROM streaming AS a INNER join streaming_before_latest_version AS b ON a.primary_key = b.primary_key AND a.changetrack_ver = b.realtime_changetrack_ver_max ), データ連携実績ログに含まれていない変更前データの取得 前述の「データ連携実績ログの取得」で述べたとおり、変更前のログがデータ連携実績ログに含まれていない場合があります。データ連携実績ログに変更のあったプライマリーキーの変更前データがない場合は日次の全量データから変更前のデータを取得しています。データの整合性の観点でも、全量データから取得した変更前のデータはAnalyzerにキャッシュされているデータとも一致するため問題ありません。 daily_before_latest AS ( SELECT streaming_latest_id.massage_unique_id, " ${dataset} " AS database_name, " ${table_base} " AS table_name, CAST ( NULL AS string) AS changetrack_type, CAST ( NULL AS int64) AS changetrack_ver, CAST ( NULL AS int64) AS changetrack_last_sync_ver, CAST ( NULL AS timestamp ) AS changetrack_start_time, CAST ( NULL AS timestamp ) AS bigquery_insert_time, streaming_latest_id.primary_key, ${ join ( " ,\n " ,columns)} FROM ( SELECT *, CONCAT (${ join ( " , " ,primary_key)}) AS primary_key FROM `${project_snapshot}.${dataset_snapshot}.${table_base}_20*` AS snapshot_table WHERE _TABLE_SUFFIX IN ( SUBSTR ( FORMAT_TIMESTAMP( " %Y%m%d " , TIMESTAMP_SUB(last_sync_time, INTERVAL 1 day), " Asia/Tokyo " ), 3 ) ) ) AS snapshot_table INNER join ( SELECT massage_unique_id, primary_key FROM streaming_latest ) AS streaming_latest_id ON snapshot_table.primary_key = streaming_latest_id.primary_key WHERE snapshot_table.primary_key NOT IN ( SELECT primary_key FROM streaming_before_latest_version ) ) 変更前後のデータをマージ 変更後と変更前のデータをマージして、変更前後のデータを取得しています。変更前後のデータを識別できるよう「tracking_type」を付与しています。変更後は「0」変更前は「1」としてます。また、実績ログを用いて連携済みのデータは排除しています。重複排除にはメッセージ単位でユニークとなるメッセージID「realtime_message_unique_id」を利用しています。 SELECT massage_unique_id AS realtime_message_unique_id, 0 AS tracking_type, * FROM streaming_latest UNION ALL SELECT CONCAT (massage_unique_id, " 1 " ) AS realtime_message_unique_id, 1 AS tracking_type, * FROM streaming_before_latest UNION ALL SELECT CONCAT (massage_unique_id, " 2 " ) AS realtime_message_unique_id, 1 AS tracking_type, * FROM daily_before_latest) WHERE realtime_message_unique_id NOT IN ( SELECT realtime_message_unique_id FROM event_sync_logs ) このようなクエリを用いて、変更前後のデータを取得しています。 アーキテクチャ概要と処理の流れ 新Trackerのアーキテクチャ概要と処理の流れについてご紹介します。 アーキテクチャの全体は次の通りです。新Trackerではリアルタイムデータ連携基盤から変更前後のデータを取得して、メッセージブローカーにパブリッシュしています。メッセージブローカーへパブリッシュされたデータはAnalyzerを含む各サービス毎に作られたデータ連携用のAPIを用いて連携されます。以降各処理の流れの詳細をご紹介します。 変更前後のデータ取得 新Trackerではリアルタイムデータ基盤から変更後のデータを取得しています。変更前のデータは後述するデータ連携実績テーブルから取得しています。 新TrackerはGKE上にネームスペースを分けてデプロイしています。各サービスごとに同じテーブルでも必要となるETL処理が異なります。また、同じテーブル名でもDB単位でデータは異なります。そのため、各リソースはサービス単位でテーブルの識別ができるように分けています。 「サービス名 × データベース名 × テーブル名」 GKEのデプロイメントは以下のようになっています。サービスとしては「analyzer」と「zozo-notification-delivery」があり、同じテーブルでも別のリソースとしてデプロイされています。 kubectl get pod -n realtime-datapump app-analyzer-table1-db1 1 / 1 Running 9 ( 46h ago ) 25d app-analyzer-table2-db2 1 / 1 Running 8 ( 27h ago ) 25då app-analyzer-table3-db3 1 / 1 Running 9 ( 16h ago ) 25d ..... app-zozo-notification-delivery-table1-db1 1 / 1 Running 9 ( 2d4h ago ) 25d app-zozo-notification-delivery-table2-db2 1 / 1 Running 9 ( 13h ago ) 25d app-zozo-notification-delivery-table3-db3 1 / 1 Running 6 ( 46h ago ) 25dåå リソースごとに設定ファイルも分けています。デプロイする際に「サービス名 × データベース名 × テーブル名」を環境変数として渡し、環境変数に基づいて設定情報を取得しています。BigQueryやCloud Pub/Subのリソース情報を制御できるようにしてます。リカバリ等も考慮し、マイクロサービス単位でテーブル等リソースは分けて管理しています。 # analyzer [services.analyzer-db1-table1] gcp_project = "gcp_project" pubsub_topic_project = "pubsub_topic_project" message_reflesh_count = 50000 pubsub_topic = "<table1>" dataset_event_send_ids = "db1_tracking_event_send_ids" dataset_event_logs = "db1_tracking_event_logs" ... # zozo-notification-delivery [services.zozo-notification-delivery-db1-table1] gcp_project = "gcp_project" pubsub_topic_project = "pubsub_topic_project" message_reflesh_count = 50000 pubsub_topic = "<table1>" dataset_event_send_ids = "zozo_notification_delivery_<db1>_tracking_event_send_ids" dataset_event_logs = "zozo_notification_delivery_<db1>_tracking_event_logs" デプロイされた新Trackerはステートレスになっており、まず最終同期したメッセージに紐づく時刻を取得します。後述の「最終同期メッセージを書き込む」でご紹介しますが、サービスにパブリッシュされた最後のメッセージは別テーブルで管理されています。新TrackerのPod起動時に最終同期したメッセージの時刻を取得します。 最終同期の時刻を、先ほどご紹介したテーブル関数に渡し、変更前後のデータを取得します。各マイクロサービスで必要となるETL処理をしています。 データ連携実績テーブル 加工されたデータは別テーブルへ書き込まれます。書き込まれたデータは古い順から全て配信基盤側へ連携されます。加工されたデータを一度書き出す理由としては、冪等性の担保と重複排除によりパフォーマンスをあげるためです。 変更前のデータをデータ連携実績テーブルから取らないと、リトライされた場合、変更前のデータがキャッシュされているデータと一致しなくなります。同じプライマリーキーに対して、複数回の更新処理が走った場合を考慮すると、リアルタイムデータ基盤にある変更前のデータとAnalyzerでキャッシュされている変更前のデータが一致しなくなるためです。リアルタイムデータ基盤にある変更データを別テーブルへ書き出すことで、変更前データの整合性を担保しています。また、前述した「変更前後のデータを取得するクエリ」で述べたとおり、変更前後のデータを取得するにはタイムスタンプを用いています。リアルタイムデータ基盤からデータ欠損がないよう遅延データも考慮して、変更データを取得するため、すでに連携済みのデータも取得されてしまいます。データ連携実績テーブルを用いることで、連携済みのデータを除外し、パフォーマンスを向上させるためにも利用します。データ連携実績テーブルに連携されたデータはサービスへ連携されるため、障害時の原因調査にも役立ちます。 メッセージブローカーへ連携 データ連携実績テーブルへ書き込まれたデータは古いデータから順に取り出され、メッセージブローカーへパブリッシュされます。メッセージブローカーを挟むことで、非同期処理が可能となり耐障害性が向上します。もし、メッセージブローカー内のデータを処理しているコンシューマが障害を起こした場合でも、メッセージブローカーへデータをパブリッシュするプロデューサは影響されずに処理を継続できます。また、メッセージブローカーを挟むことで、各マイクロサービスへのデータ連携で必要なインタフェースを揃えることができるため、汎用性の高いシステムを構築できます。 データ連携実績テーブルからデータを取り出すクエリは以下のようになっています。「LastSyncTime」メッセージブローカーへの配信が成功した最後のメッセージに紐づく時刻が入ります。データ連携実績テーブルに連携済みで、まだメッセージブローカーへ配信できていないメッセージのみ抽出します。 SELECT * FROM `zozo-ma-realtime-datapump-{{.Env}}.{{.EventLogsDataset}}.{{.EventName}}` WHERE tracking_start_time > " {{.LastSyncTime}} " ORDER BY tracking_start_time ASC メッセージブローカーにはCloud Pub/Subを採用しています。Cloud Pub/Subで順序保証するには順序保証キー「OrderingKey」を用いる必要があります。SQL Serverのプライマリーキーを順序保証キーとして、パブリッシュしています。順序を保証するため、メッセージブローカーへのパブリッシュが1件でも失敗した場合、実績テーブル内の未連携データは全て再連携されます。 cloud.google.com Cloud Pub/Subへのパブリッシュ時に以下のように属性情報「Attributes」も渡しています。 publishResult := t.Publish(ctx, &pubsub.Message{ Data: [] byte (msg), Attributes: map [ string ] string { "event" : event.EventSourceName() + "-" + event.EventDatabaseName() + "-" + event.EventName(), "message_id" : event.MessageId(), "key" : event.RealtimeMessageKey(), "action" : event.Action(), }, OrderingKey: event.OrderId(), }) 属性情報の役割は以下の通りです。 属性 説明 event 各イベントを識別するために利用 message_id メッセージのユニーク値を識別するために利用 key SQL Serverのプライマリーキー action 変更のあったイベントタイプ「upsert」と「delete」 これらの属性情報に基づき、後述する配信系サービスへデータ連携を担うAPIで処理されています。 データ連携API(Analyzer) Cloud Pub/SubへパブリッシュされたデータはAnalyzerへデータ連携されます。Analyzerへのデータ連携APIにはCloud Dataflowを用いています。Cloud Pub/Subへパブリッシュ時に属性情報として渡した「event」を用いて、イベント名に基づきAnalyzerへのエンドポイントに対してリクエストします。AnalyzerはAWS環境にあるため、GCPからAWS環境へリクエストするために、ZOZO内の共通基盤であるShard VPCを用いています。 ShardVPCについては以下の記事をご確認ください。 techblog.zozo.com 当時Cloud RunやCloud Functionsを採用しなかったのは従量課金によるコストを抑えたかったためです。Cloud RunやCloud Functionsの比較的新しい料金体系である「Always on CPU」だと、リクエスト課金が発生しません。大量のデータを扱うログ収集基盤などではスケーリングが速く、コスト面の費用対効果も高いです。 cloud.google.com ただし、今回はShard VPCを利用するためCloud RunやCloud Functionsを使う場合はサーバレスVPCを利用する必要があります。サーバレスVPCは従量課金となってしまうため、運用実績もあり、費用帯効果の高いCloud Dataflowを採用しました。 cloud.google.com しかし、実際にCloud Runも運用してみてパフォーマンス面や最小ワーカー数の制御等、Cloud Dataflowよりも優れている点が多いように感じました。要件次第ではありますが、今後新規でストリーミング系のデータ連携をする場合は積極的にCloud Runを使いたいと思いました。 Appendix:データ連携API(Push/LINE配信基盤) 前述の「なぜリプレイスをしたのか」でご紹介したとおり、新TrackerはAnalyzer以外の各マイクロサービスへもリアルタイムにデータ連携できます。Appendixとして配信基盤へのデータ連携についても簡単にご紹介します。 リアルタイムマーケティングシステム全体のリプレイスに伴い、Analyzerの各チャンネルへの配信機能を配信基盤として切り出し、モジュール化しています。配信基盤は全社の共通基盤としてZOZO内部の他のシステムからも配信処理が実施できるように作られています。配信基盤の機能として、配信基盤へのリクエストに含まれているメンバーIDを用いて、通知設定の確認やPushやLINE配信に必要なトークンへの変換をしています。配信基盤で必要なデータを新Trackerを用いて連携しています。 配信基盤用のデータ連携APIにはCloud Runを採用しています。Cloud Runを採用したのは、Cloud Dataflowよりもスケーリングが速く、パフォーマンス面で優れていたためです。また、「Always on CPU」を使えば今後データ量が増えてもリクエスト課金による懸念はなくなります。配信基盤用のデータ連携APIはAWSと疎通することもないため、サーバレスVPCの費用を気にする必要もありません。 配信基盤のストレージにはGCPのサーバレスでNoSQLデータベースであるCloud FirestoreをDatastoreモードで採用してます。配信基盤に必要なデータをデータ連携APIを用いて、Cloud Firestoreにキャッシュしています。属性情報の「action」に基づいてデータの更新と削除をしています。配信基盤でもAnalyzer同様に十分なパフォーマンスがでるよう、Cloud FirestoreのKEYにメンバーIDを用いています。MA部以外のシステムからも配信できるようメンバーIDを用いて配信に必要なパーミッションなどの情報を取得するためです。Cloud FirestoreにキャッシュされたデータもAnalyzer同様、必要に応じ削除しています。例えばLINEの連携を解除した際にキャッシュされたデータを消す必要があります。 ただし、AnalyzerのようにメンバーIDなどの変更前のデータは必要ありません。以下のように属性情報の「key」で渡されたSQL Serverのプライマリーキーを用いて、対象のデータを抽出して削除をしています。 func (mdsrepository *EventCacheRepository) Delete(ctx context.Context, cacheLog entity.TableCatchLog) error { // delete datastore key from sql server primary key _, err := mdsrepository.client.RunInTransaction(ctx, func (tx *gcpdatastore.Transaction) error { query := gcpdatastore.NewQuery(mdsrepository.entityKind).Transaction(tx).FilterField(cacheLog.ToPrimaryKeyName(), "=" , cacheLog.ToPrimaryKey()) it := mdsrepository.client.Run(ctx, query) catchIterator := CatchIterator{ CatchName: cacheLog.ToCatchName(), Iterator: it, } for { catch, err := catchIterator.NextEvent() if err == iterator.Done { break } if err != nil { return err } key := gcpdatastore.NameKey(mdsrepository.entityKind, catch.ToKey(), nil ) if err := tx.Delete(key); err != nil { return err } } return nil }) if err != nil { mdsrepository.logger.Error( "Transaction Faile To Delete Entity" ) return err } return nil } 配信基盤のストレージの選定時にKEYではなく、クエリで十分なパフォーマンスが出るかも必要な要件でした。SQL Serverのプライマリーキーを用いてキャッシュの操作をするためです。結果整合性によりパフォーマンスで優れているCloud Firestoreは負荷検証の結果1億件を超えるデータでも高速に処理できることが確認できました。 cloud.google.com なお、バッチの洗い替えなど大量にデータを削除するには不向きなので注意が必要です。夜間であれば問題ありませんが、配信中などに実施するとクエリのレイテンシが悪化します。ドキュメントでも負荷検証等で十分なパフォーマンスがでるか検証することを勧めています。 cloud.google.com 配信基盤用のデータ連携クエリは、Analyzerとは異なり変更データ取得の際にBigQueryのリソースを多く消費することもありません。Analyzerのインメモリなデータストアで実現できるのかパフォーマンスの確認は必要となりますが、今後Analyzerにも同様の改修を入れたいと考えています。 最終同期メッセージを書き込む メッセージブローカーへのパブリッシュが全て成功した場合は最後にパブリッシュした、メッセージのメッセージIDとデータの取得開始時刻をBigQueryへ同期的に書き込みます。最終同期時刻を変更データ取得用に作られたテーブル関数へ渡し、変更のあったデータを取得しています。また、このメッセージIDを用いて、実績テーブルからデータ連携するデータを絞っています。冒頭で説明したSQL Serverの変更追跡バージョンと同じ役割を果たしています。障害発生時のリカバリもこの最終同期メッセージの時刻を巻き戻すことで、最終同期した時刻以降のデータを再連携可能です。 初回の全量データ連携 新Trackerで取得できるデータは変更データのみです。初回時のデータ連携やリカバリ時には全量データの連携が必要になります。以降「ローダーバッチ」と呼びます。ローダバッチではDigdagを用いて、BigQueryのクエリ実行結果をCloud Storageへdumpし、並列にCloud Pub/Subへパブリッシュしています。Cloud Pub/Subへパブリッシュされたデータは前述した各サービスごとに作られたデータ連携APIを用いてキャッシュされます。ストリーミング処理である新Trackerの差分連携、バッチ処理であるDigdagを用いた全量連携で使うデータ連携APIの共通化が可能です。バッチ処理とストリーミング処理の両方で同じロジックのメンテナンスをする必要がないため、運用負荷を軽減できます。また、新Trackerを用いて新しくデータ連携する場合の導入工数も削減できます。 なお、Cloud Pub/Subへのパブリッシュで十分なパフォーマンスがでない場合は、Cloud Pub/Subクライアントのバッチメッセージングの設定値を調整する必要があります。今回はmax_messagesをデフォルトの100から1000に変更し、max_latencyをデフォルトの10msから1sに変更しました。これにより、約1.7億件のデータをCloud Pub/Subへパブリッシュするのに10時間かかっても終わらなかったのが、約90分ほどで完了するようになりました。 設定値の詳しい説明は、以下の公式のドキュメントをご確認ください。 cloud.google.com Digdagについては以下の記事をご確認ください。 techblog.zozo.com 移行前後の評価 リプレイスにあたり以下の観点でデータの評価をしました。Analyzerに連携しているテーブルは約22テーブルほどあり、マスタテーブル等のJOIN等複雑な加工処理を実施しています。移行時の評価方法についてご紹介します。 データの整合性を評価 データの整合性を担保するため、旧Trackerと新Trackerのログを比較できるようにしました。旧TrackerのログはWindows Server内から取得し、新Trackerの方はCloud Loggingにログを書き出しました。両方の結果をハッシュ値で比較して、データの値が一致しているか調べました。言語仕様等でずれがあった場合は問題ないか確認していきました。評価の過程で旧Tracker側の問題、新Tracker側の問題両方見つかりました。修正が必要なテーブルはクエリを修正し、対応しました。 データの欠損を評価 次にデータの欠損を調べました。データ欠損の観点では旧Trackerで変更のあったプライマリーキーが新Trackerに含まれているか調べました。遅延を考慮し、ウィンドウ幅を1時間程度に調整して調べました。プライマリーキーの有無で調べたのは、データ連携の性質上、短い期間に複数回の更新が走った場合はプライマリーキーに紐づくデータが新旧で一致しなくなるからです。プライマリーキーであれば、旧Trackerにあるキーは新Trackerにないといけないため、データの欠損を調べることができます。データの欠損がないか確認し、問題がないことを確認しました。 データの遅延時間を評価 旧Trackerをベンチマークにデータの遅延時間を調べました。旧Trackerの遅延の調査は旧Trackerとリアルタイムデータ基盤のログをBigQuery上で突合して調べました。 新旧Trackerのデータ遅延だけではなく、リアルタイムデータ基盤側のデータ遅延も調べました。調べたところ旧Trackerでは最大で20分程度の遅延が発生していることが確認できました。一方で、新TrackerではBigQueryのコンピューティングリソースであるスロットを十分確保すると、遅くても数十秒ほどでクエリの完了が確認できました。しかし、数十テーブルの連携で十分なスロットを確保する場合は600スロットほど必要なことがわかりました。調査したところ主に変更前のデータ取得で多くのスロットを消費していることが分かりました。 運用ではコストを抑えるため100スロットに固定しています。100スロット固定だとパフォーマンスは遅くなりますが、旧Trackerのパフォーマンスは超えることができました。後述しますが、Analyzer側に修正を加えることで100スロット以内に、必要なパフォーマンスをだせる予定です。 監視設計 新Trackerの監視設計について紹介します。リアルタイムに差分データを取得しているアプリケーションを「プロデューサ」、メッセージブローカーのデータを処理するデータ連携APIを「コンシューマ」と呼びます。 プロデューサの監視 プロデューサの監視ではCloud Monitoringを活用して、データの遅延と正常に稼働しているか監視しています。変更前後のデータを取得する一連の処理が完了した際に、Cloud Loggingを用いて監視で用いるイベント情報を書き出しています。一定時間たってもイベントが書き込まれない場合はアラートを飛ばすようにしています。 コンシューマの監視 コンシューマの監視にはCloud Pub/Sub内のACKされていないデータを監視しています。コンシューマで障害が発生し、処理が完了しなかった場合はExponential Backoffでリトライするように作られています。リトライしても成功しない場合はCloud Pub/Sub内でACKされていないデータが増え続けます。Cloud Pub/Sub内のメトリクスである「oldest_unacked_message_age」を監視して、コンシューマの障害を検知できるようにしています。 リプレイスによる改善点 リプレイスしたことによる改善点をご紹介します。 リアルタイムマーケティングシステム全体のリプレイスに必要な基盤を構築 リアルタイムマーケティングシステム全体のリプレイスを進めていく上で、必要な基盤を構築できました。各マイクロサービスで必要なデータをリアルタイムに連携が可能となりました。また、初回の全量データ連携の仕組みも共通化できました。新規でサービスを追加する場合は、新しくクエリやトピック等追加することで容易にリアルタイムデータ連携が可能です。 運用負荷の軽減 旧Trackerからリプレイスできたことで、運用負荷の高かったWindows Serverから脱却できました。Windows Serverにデプロイされたクラスタの運用やSQL Server起因の障害がなくなりました。リプレイスに伴いデプロイも自動化でき安心して実施できるようになりました。また、半年間大きな障害なく運用もできています。 今後の課題 最後に今後の課題について紹介します。リプレイスは完了しましたが、まだいくつか改善の余地があります。 パフォーマンスの改善 今回Analyzerに手を加えない形で修正しました。しかし、変更前データを実績テーブルから取得することで多くのBigQueryのコンピューティングリソース(スロット)を消費しています。Analyzerはプライマリーキーではないものをキーにしてるキャッシュが多いためです。十分なスロットが確保できれば、遅くても数十秒以内でクエリは完了します。配信基盤のようにAnalyzerも変更前のデータをSQL Serverのプライマリーキーから取得するよう改修することで、コストやパフォーマンス面で改善が見込まれます。今後この基盤を用いてさらにデータ連携するサービスが増えていく予定なので、対応していきたいです。 初回全量データ連携処理の完全移行 前述した初回全量データ連携する仕組み(ローダバッチ)ですが、Analyzerの全量データ連携ではまだ利用できていません。ローダーバッチの仕組みを利用できているのは配信基盤(Push/LINE)用のデータ連携のみです。Analyzerでも全量データをロードするための仕組みがあり、インメモリ上のキャッシュが吹き飛んだ時などに用いています。 しかし、SQL Serverから全量データを取得するには時間もかかり、ロードには8〜9時間程度かかります。今回紹介したローダーバッチへ移行できると、BigQueryからデータを取得できるので、パフォーマンス面での改善が期待できます。DigdagにAnalyzer用のクエリを追加すればいいため、Analyzerやデータ連携APIには手を加えずにリプレイスできます。より短い時間でリカバリできれば、Analyzerを運用していく上で一番大きな不安も解消されるので、今後対応していきたいです。 まとめ 本記事ではリアルタイムマーケティングシステム全体のリプレイスに向け、配信用リアルタイムデータ連携基盤をリプレイスした事例をご紹介しました。 リプレイスに伴い、運用負荷の高いWindows Serverから脱却できました。今回のリプレイスで変更データの取得元をSQL Serverから全社共通の基盤であるリアルタイムデータ基盤に変更しました。データソースの変更に伴い、「データの重複」「データの順序」「変更前データの取得方法」を考慮した設計が必要でした。さらに、Analyzerの制約も考慮し、切り戻しや評価できるよう安全にリプレイスを進める必要がありました。 構築した新Trackerで連携できるデータは差分データのみなので、初回の全量データを連携するための仕組みが必要でした。運用負荷や導入工数を考慮し、ストリーミング処理とバッチ処理で同じデータ連携用のAPIを用いています。 今回のリプレイスに伴い、旧Trackerの抱えていた課題を解決できましたが、まだ課題は残っているので今後対応していきたいです。 最後に この記事を読んで、もしご興味をもたれた方は是非採用ページからお申し込みください。 https://hrmos.co/pages/zozo/jobs/0000196 hrmos.co
アバター
はじめに こんにちは、技術本部・MA部・MA開発1ブロックでマーケティングオートメーションのシステムを開発している長澤( @snagasawa_ )です。この記事ではパーソナライズ配信におけるルールベースの最適化を改善した事例を紹介します。 ZOZOTOWNでは、マーケティングオートメーションによってキャンペーンやセール情報などの配信を日々行なっています。配信はその対象によって2種類に大別でき、特定のユーザーセグメント向けの「マス配信」と、個別のユーザーに最適化された「パーソナライズ配信」があります。 この後者のパーソナライズ配信において、既存の最適化処理である課題を抱えていました。それは、特定の条件下でユーザーへ配信が行われずに機会損失が発生するというものでした。今回はこの課題の原因となっていた実装の依存関係を見直し、配信のKPIを改善した事例について紹介します。 ルールベースの最適化の課題 はじめに、パーソナライズ配信の最適化フローと今回改善した課題を紹介します。 最適化フローの概要は過去のテックブログ記事でも紹介していますので、配信システムのアーキテクチャや構成要素も合わせてこちらの記事でご確認ください(過去の記事で「リアルタイムマーケティングシステム」と呼んでいるものを、この記事では最適化の側面から「パーソナライズ配信」と呼んでいます)。 techblog.zozo.com この最適化フローでは、「チャネル」「時間」「通数」の3つの最適化を行なっています。 処理名 処理内容 チャネル最適化 メール・LINE・アプリPushの中で、ユーザーが最も反応しやすいチャネルを配信先に選択します。 時間最適化 ユーザーが反応しやすい時間帯に配信時間を調整します。 通数最適化 過剰な配信によってオプトアウトされないように、ユーザーごとに一定期間内の配信通数の上限を設け、達していた場合は配信しないように制御します。 課題が存在したのは、ひとつ目のチャネル最適化です。 処理の順序として、チャネル最適化後に通数最適化を行なっていたため、ユーザーの反応しやすいチャネルが選択されたものの、通数最適化で通数上限に達していた場合に配信されないという事象が発生していました。 具体例で言うと、あるユーザーの最適なチャネルとしてメールが選択されたものの、そのメールの通数上限に達していたために配信されなくなるという流れです。 これは通数最適化の目的からすれば意図した挙動だと思われるかもしれません。しかし、これには改善の余地があります。それは、最適化されたチャネルの「次の優先順位のチャネルでの配信」です。 先ほどの例であれば、メールの次に反応しやすいチャネルがLINEだった場合、そのチャネルで配信し直すという処理です。言われてみれば実装されて然るべきだと思われるような機能ですが、修正前までは未実装でした。 また、加えてもうひとつの課題がありました。最適化フローは過去の配信実績に基づいて行われるため、すでに配信実績のあるチャネルに偏りやすいという課題です。例えばあるキャンペーンが新しい配信チャネルに対応したり、ユーザーのチャネルの利用動向が変化したりしても、過去に最も利用されていたチャネルで配信されやすい傾向にありました。設定によりチャネルの優先度に補正をかけることも可能ですが、その都度補正を調整する手間がかかります。 もしも「次の優先順位のチャネルの配信」が実装されていれば、配信実績のあるチャネルで通数上限に達した場合でも、配信実績の少ないチャネルでの配信が期待できます。 最適化のフロー 先に改善の結論を言うと、通数上限チェックをチャネル最適化の処理内へ移行し、通数の上限到達済みチャネルを選択肢から除外するように修正しました。改善は至ってシンプルです。続いて、最適化フローを詳しく説明します。 最適化の前処理 最適化の前処理として、「イベント検知・キャンペーン判定・ユーザー抽出」の3つがあります。 処理名 処理内容 イベント検知 キャンペーンの条件となるイベントを検知します。 キャンペーン判定 検知されたイベントからキャンペーンを判定します。 ユーザー抽出 SQLでデータベースからキャンペーンの対象のユーザーIDを取得します。 イメージしやすい「お気に入り商品の値下げ通知」キャンペーンを例にします。 ある商品が値下げされると、データベースの商品テーブルの価格カラムが更新され、それをイベントとして配信システムにリクエストを送信します(イベント検知)。 配信システムはそのイベントの内容が「お気に入り商品の値下げ通知」キャンペーンの配信条件であることを判定し、キャンペーンの情報を生成します(キャンペーン判定)。 そのキャンペーン情報に含まれるセグメントのSQLを実行し、ユーザーIDを抽出します(ユーザー抽出)。 一連の流れで取得されたユーザーごとの情報は、JSONで最適化情報(Optimization Context)として後続の最適化処理に渡されて処理が移ります。 チャネル最適化 この処理でははじめに、キャンペーンごとに設定される「優先チャネル」でそのユーザーへの配信の可否をチェックし、可能であればその時点でチャネルが確定します。しかし、優先チャネルが未設定や配信不可の場合、配信候補のチャネルで配信実績のクリックログからユーザーの反応しやすさを判定してチャネルの優先度付けを行います。 上記の最適化を経て、優先度は以下の4パターンのいずれかの値を元に算出します。 番号 処理内容 ① 当該ユーザーの「キャンペーン×チャネル」のクリック率 ② 当該ユーザーのキャンペーンのクリック率と、「キャンペーン×チャネル」の全ユーザーのクリック率 ③ 当該ユーザーの他キャンペーンでのチャネルのクリック率と、キャンペーン全体のクリック率 ④ チャネルの全キャンペーンのクリック率で優先度計算 このように、基本的には配信対象ユーザーの配信実績やクリック率をもとに優先度を計算します。しかし、配信可否や配信実績の有無によっては、他のキャンペーン・チャネル・ユーザーのクリック率を利用します。 時間最適化 キャンペーンごとに設定される配信タイミングやチャネルの配信可能な時間帯などにもとづき、即時での配信、または指定時間での配信を予約します。配信予約の場合は、JBoss Data Grid(JDG)というインメモリの分散キャッシュデータストアに配信情報を保存し、指定の時間にそれを取り出して配信します。 www.redhat.com また、時間以外の判定材料として「おまとめ配信」という機能があります。キャンペーンの配信条件によっては都度配信が過剰な配信数になりかねないものがあり、そうしたキャンペーンは一定時間内の配信内容を一通にまとめて配信します。先ほどの例の「お気に入り商品の値下げ通知」であれば、仮にわずかな時間差でお気に入り商品の値下げが連続してもまとめて配信されます。 通数最適化 ここでは配信の重複と配信通数の上限をチェックをします。過去に同様の配信が済んでいたり、配信数が上限に達していたりした場合、配信をキャンセルすることにより過剰な配信を防ぎます。 チェック名 内容 チャネルの重複 同一キャンペーン・同一チャネルでの配信 コンテンツの重複 同一キャンペーン・同一コンテンツ(例えば「同じ商品の値引き通知」などの配信内容)」での配信 チャネルの通数上限 チャネル単位での通数上限 ここまでの最適化を経て配信処理に移ります。 最適化の改善 改めて今回の最適化の改善について説明します。 課題は、通数最適化の通数上限チェックが最適化全体のフローの最後に行われていたため、最適化チャネルが上限到達済みの場合に他のチャネルで配信されないことでした。そのため、この通数上限チェックを前倒ししてチャネル最適化の処理内で行うことにより、上限到達済みチャネルを最適化の対象から除外するように修正しました。 具体的には、元々チャネル最適化で「配信対象ユーザーにとって利用可能」でなおかつ「優先度が高い」チャネルから優先度付けを行っていたところに、上限到達チャネルの除外処理を移行しました。 KPIの改善 改善の結果はKPIに現れました。 こちらはある特定のキャンペーンにおける配信チャネルの比率のグラフです。6/19のリリースを境にPushチャネル(緑色)の比率が増加しています。これはメールやLINEで上限到達済みの場合に、チャネル最適化でPushチャネルが選択されるようになったためです。 続いて、配信数の積み上げグラフではいずれのチャネルも配信数が増加しています。チャネル最適化の時点で上限到達済みチャネルが選択されず、次の優先チャネルでの配信が試行されるようになったためと見られます。 こちらは全キャンペーンにおける配信除外(最適化による配信のキャンセル)の除外理由ごとの積み上げグラフです。「通数上限(日)」(青色)と「通数上限(期間)」(赤色)が6/20以降減少しており、「チャネル利用不可(チャネル選択時)」(緑色)が増加しています。こちらも、それまでの通数上限による配信除外がチャネル最適化内の処理に吸収され、その次の優先チャネルでの配信が試行されるようになったためです。 このように、これまで最適化の機会を損なわれていたチャネルで配信数の増加する改善結果が見られました。売上損失のみならず、ユーザーの購入機会の損失を防いで利便性向上に繋がりました。また、この期間内では現れていませんが、ユーザーの利用チャネルの傾向の変化に合わせて優先順位が計算されるようになりました。結果、パーソナライズの精度が高まりました。 まとめ 本記事ではZOZOTOWNのパーソナライズ配信におけるルールベースの最適化改善の事例を紹介しました。 今回の改善は現状の課題におけるごく一部に過ぎません。他の課題では以下のような例があり、真の目的である「ユーザーが本当にほしい通知だけの配信」の実現までには改善の余地が多く残されています。 配信トリガーの判定がシンプルすぎるために確度の低い配信が発生している 週間の通数上限はユーザーごとに可変である一方、日間の通数上限はチャネルごとに固定のため機会損失が生じている 配信システムの改修コストの高さが、この課題改善の障害のひとつとして存在しています。現在はこの問題に対処すべく、配信システムのリプレイスを予定しています。 techblog.zozo.com リプレイス完了の暁には、ルールベースから機械学習による最適化への移行を計画しています。 さいごに ZOZOでは一緒に楽しく働くエンジニアを絶賛募集中ですので、興味のある方は以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
はじめに こんにちは、CISO部の兵藤です。日々ZOZOの安全のためにSOC対応を行なっています。 本記事では、世間で横行しているフィッシング詐欺に関する情報を収集し、ZOZOを騙ったフィッシングを検知する取り組みについて紹介します。 目次 はじめに 目次 背景と概要 フィッシングハント - ドメイン編 ドッペルゲンガードメイン openSquat 構築 概要 特徴 運用 フィッシングハント - メール編 フィッシングメール収集源 フィッシングメール収集方法 Botによる監視 まとめ おわりに 背景と概要 フィッシング詐欺というと、特殊詐欺にあたるものの1つです。「メール」「SMS」などの媒体を介してユーザを本物とよく似せたフィッシングサイトに誘導し、個人情報やクレジットカード情報、IDパスワード情報を搾取する目的で行われることが多いです。 ZOZOではSNSやお客様からの情報を元に、フィッシング詐欺(フィッシングサイトやフィッシングメール)の対応を行なっていました。ですが、本対応だけではフィッシングの対応が後手となってしまい、被害拡大の可能性があります。 そこでCISO部ではフィッシングメール、フィッシングサイトになりうるドッペルゲンガードメインの収集(フィッシングハント)を行い、ZOZOの脅威になりうる情報を検知する基盤を構築しました。 フィッシング詐欺は事業を行なっている企業全てに関係する脅威だと思います。同じような取り組みを実施したいと考える皆様の参考になれば幸いです。 フィッシングハント - ドメイン編 ドッペルゲンガードメイン 攻撃者がフィッシングサイトを建てる際、似たようなサイトになるように努力をすることでしょう。その内の1つにはドメインも含まれており、コンテンツ改竄やホスティングサービスを使用しない場合は新規でドメインを登録する必要があります。 ドメインを取得する際には本物のドメインと類似したドッペルゲンガードメインを利用する場合があります。 本物のドメイン ドッペルゲンガードメイン zozo.jp zoz0(ゼロ).jp 上記のようなドッペルゲンガードメインが新規で作られていれば、その情報を収集するツールは多くあります。ZOZOではopenSquatというツールを使用し、ドッペルゲンガードメインを収集しています。 openSquat openSquatはドッペルゲンガードメインを収集するオープンソースのセキュリティツールです。公式サイトは こちら のリンクをご参照ください。 このopenSquatは1日1回新規ドメインリストを更新してくれます。そのドメインリストの中から、 keywords.txt で設定した本物のドメインに対するドッペルゲンガードメインを収集します。また、オプション( --phishing )によっては既知のフィッシングドメインからドッペルゲンガードメインを収集できます。 構築 概要 ZOZOでは1日1回、上記openSquatを実行する基盤をAWS上に構築しました。以下が概要図になります。 1日1回、EventBridgeを用いて起動命令を飛ばす。 起動命令をLambdaで処理し、NAT、ECSコンテナを作成。 コンテナでopenSquatを実行。 取得したドッペルゲンガードメインからurlscanを用いてレピュテーションとスクリーンショットを取得。 悪性スコアとスクリーンショットをSlackに通知。 上記全て完了すればNAT、ECSコンテナの削除を実施。 特徴 この基盤の特徴としてはLambdaではなく、コンテナ上でopenSquatを実行しているところです。 というのもopenSquatは起動するときにファイルを諸々作成することになるので、インメモリで実行されるLambdaでは相性が悪かったという経緯があります。openSquatの構造を変更せずに実装する場合では、コンテナでパッケージ化することが実装の近道でした。 また、 urlscan のAPI 1 を用いることで、ドッペルゲンガードメインのレピュテーションやスクリーンショットを自動取得することも特徴でしょう。この機能によりSlackを確認するだけでフィッシングサイトなのか、ある程度の判断が可能です。 urlscanを利用する上で注意すべき項目としては、スキャンにある程度待ち時間が存在することです。NWの状況によってはスキャンに時間がかかったり、できなかったりします。スキャンが終了するまでの間はレスポンスが404で返されます。そのような状況を踏まえて以下のようにスキャンの合間に time.sleep 関数を挟んでいます。 try : uuid = do_scan(domain) time.sleep( 40 ) #urlscan完了までの待ち時間 image = get_image(uuid) #自作関数 score = get_score(uuid) #自作関数 domain = domain.replace( "." , "[.]" ) #Defang処理 運用 現在、毎日この可愛いワンちゃんがお知らせをしてくれます。フィッシングサイトであれば一目で確認できます。 実際にフィッシングサイトを検知した様子 フィッシングハント - メール編 フィッシングメール収集源 突然ですが、ブログサイトにはメールを使った投稿機能があるのを皆さんご存知でしょうか? ブログ投稿用のメールアドレスを用意して、そのメールアドレスに届いたメールの内容がブログに投稿されるといった流れです。 このメールアドレスが何らかの理由で流出し、フィッシングメールが届くようになればそのフィッシングメールの内容がブログへ投稿されるようになります。このフィッシングメールが投稿されているブログを監視することでフィッシングメールの収集が可能です。 上記の仕組みについてはフィッシング詐欺ハンターの「にゃんたく」さんの記事 2 が参考になります。 フィッシングメール収集方法 ブログの情報はRSSを用いて収集が可能です。このRSSの情報を収集すればフィッシングメールを自動的に収集できるというわけです。 RSSのURLは各ブログページのHTMLを表示すれば確認できます。以下が記載例になります。※URLはZOZOのドメインを使用しています。 # feed階層配下 < link rel = "alternate" type = "application/rss+xml" title = "ZOZO - RSS" href = "https[:]//zozo.com/feeds/posts/default?alt=rss" /> # index.rdf形式 < link rel = "alternate" href = "http[:]//zozo.jp/index.rdf" type = "application/rss+xml" title = "RSS" /> # rss階層 < link rel = "alternate" type = "application/rss+xml" title = "RSS2.0" href = "https[:]//zozo.com/rss" /> 上記のRSSを用いて、SlackのChannelに投稿させることで、フィッシングメールを収集するChannelが出来上がります。 ZOZOではフィッシングメール情報をRSSを用いてSlackの1Channelに集約しています。以下がその模様です。 Botによる監視 上記のChannelには大量のフィッシングメールが届きます。このフィッシングメール全ての人力監視はリソースを考えると不可能です。 ZOZOではこのフィッシングメールを監視してくれるSlackBotを作成し、何かあればChannelの参加者にメンションを行う仕組みを導入しています。 Botは slack_bolt を使用し、Azure Web Appsで起動させています。簡易的なアプリの起動であれば即座に構築できるのでとても便利です。 slack_boltで監視するものは基本的にRSSで投稿される message イベントになります。これでフィッシングメールの内容がZOZOに関するものか判断します。 @ app.event ( "message" ) def event_message (client, event, say): content = event[ "text" ] 実際にZOZOを標的にしたフィッシングメールを検知した際には以下のようにメンションとスタンプでお知らせしてくれます。 まとめ フィッシングメール、フィッシングサイトになりうるドッペルゲンガードメインの収集(フィッシングハント)を行い、ZOZOの脅威となる情報を検知する基盤構築の取り組みを紹介しました。 意外と簡単に基盤が構築できると感じたのではないでしょうか? ZOZOではこれからもフィッシングメールやフィッシングドメインを能動的に収集し、検知することで少しでもフィッシングの被害に合う方達を無くすことを目的に活動していこうと考えています。 近年では、ホスティングサービスを利用したフィッシングサイトやSNSを利用したフィッシング、またWeb3技術のIPFSを利用したフィッシング 3 も観測されています。ドッペルゲンガードメインだけでは検知できないフィッシングサイトも上記の通り出現している傾向があるため、フィッシング詐欺への対策は更なる工夫と検知精度が必要です。そのためにも地道にフィッシング詐欺への対策を1つずつ実施し、脅威情報を少しでも多く収集し活用していくことが大切です。 本記事がフィッシング詐欺に対しこれから対策していく足掛かりになれば幸いです。 おわりに ZOZOでは、一緒に安全なサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクから是非ご応募ください! corp.zozo.com urlscanの APIドキュメント ↩ 不審なメールを収集できる(かもしれない)ポストブログについて書いてみた。 ↩ 注目の脅威:サイバー犯罪者がフィッシング攻撃やマルウェア攻撃にIPFSを採用 ↩
アバター