Xcode Cloud 触ってみた

本記事はTVer Advent Calendar 2023の19日目の記事です。

はじめに

こんにちは、TVeriOSアプリ開発を担当しています小森です。
Xcode Cloudの発表からしばらく経ちましたが、 CI/CDサービスを検討するに当たってXcode Cloudを初めて触ってみましたので、
本記事でXcode Cloudについてのセットアップ方法と、触ってみた感想をまとめたいと思います。
Xcode Cloudを検討されている方の参考になれば幸いです。

Xcode Cloudとは

Xcode CloueはAppleが提供するAppleプラットフォームのためのCI/CDサービスです。

developer.apple.com

利用手順について

XcodeのReport NavigatorのCloudタブからWorkflowの作成をして実行します。

Workflowについて

下記に簡易的なWorkflowの設定についてまとめます。 詳細情報は公式ドキュメントを参照してください。

developer.apple.com

Workflow 編集画面

General

ここではWorkflow名、ワークフロー説明文、リポジトリ、対象のProject or Workspaceの設定を行います。

Environment

ここでは実行するXcode VersionとmacOS Versionの選択と環境変数の設定を行います。 事前定義されている環境変数はこちらを参照してください。

developer.apple.com

Start Conditions

Workflowの起動トリガーの設定を行います。 他のCI/CDサービスと同じようにブランチの変更、プルリクエストの変更、タグの変更、スケジュール実行などに対応しています。

Actions

ActionはBuild, Test, Analyze, Archiveの4つから追加できます。 各Actionは並列実行することが可能です。

Post-Actions

Post-ActionsはTestFlight External Testing, TestFlight Internal Testing, Notarize, Notifyの4つから追加できます。 NotifyはSlackとEmailに対応しています。

触ったみた個人的感想

  • とにかくシンプルで楽

Xcodeに組み込まれている機能ということもあり、セットアップはとても楽で簡単でした。
また、証明書やProvisioningProfileなどの管理が自動で対応されるのでfastlane matchを導入してごにょごにょなどする必要がありません。
そして、Test FlightやAppStoreConnectの連携も簡単にできます。
AppStoreConnectからWorkflowの実行も可能なので開発者以外も容易にWorkflow実行が可能なのは魅力的です。

  • メンテナンスしにくいかも

ほかのCI/CDサービスだとWorkflowの設定をyamlファイルなどで記述し管理することができますが、 Xcode Cloudの場合は、XcodeもしくはAppStoreConnectからのGUIでの編集かApp Store Connect API経由での編集のみになります。
チームでメンテナンスしていくためにApp Store Connect APIも用意していると公式ドキュメントには記載されていますが ソースコードでWorkflowの設定を管理することが可能な他のCI/CDサービスと比較するとちょっとメンテナンスしにくそうだなという印象を持ちました。

developer.apple.com

  • キャッシュ機能がイマイチ

Deriverd Data配下のみがキャッシュ対象でSwift Package Managerを使用してインストールしたライブラリのみキャッシュされるみたいです。
サードパーティ製のツールやライブラリを導入する場合は、Xcode Cloudが提供しているHomebrewを使用し、 任意のタイミングで実行されるcustom build scriptsを使用してインストールする必要があります。
そのインストールしたツールやライブラリをキャッシュすることはできないので毎回インストールしなければならずイマイチだなという印象を持ちました。
ただ、Git LFSをサポートしてるので、CocoaPodsなどに関してはPodsディレクトリをGit管理下に置くアプローチを採用していればキャッシュ意識する必要がないので問題なく運用可能だと思います。

まとめ

今回はXcode Cloudの簡易的なセットアップ方法と触ってみた個人的感想を述べた記事でした。 個人開発をする上ではXcode Cloud一択で十分ではないかと思いますが、他のCI/CDサービスと比較するとメンテナンス性やキャッシュ機能などが少しネックかなと考えております。
ただ、料金に関しては比較的安く機能に関してもシンプルで使いやすい印象を受けたのでデメリットを補う案を模索しながら引き続き採用検討を進めたいと思いました。

SnapHelperがどうやってSnappingを実現しているのか

本記事は TVer Advent Calendar 202320日目の記事です。

はじめに

こんにちは、TVerAndroidアプリ開発をしています石井です。

AndroidViewでコンテンツの一覧などを表示する際にRecyclerViewがよく使われると思いますが、カルーセルのようなUIにするためにはどうすれば良いでしょうか。

一般的にはRecyclerViewにLinearSnapHelperをアタッチすることで、カルーセルのようにコンテンツを中央寄せさせるUIを作ることが可能です。
ただし、LinearSnapHelperはあくまでも中央へのSnappingしか提供していないため、左寄せや右寄せなど中央以外にしたい場合は独自にSnapHelperを継承した実装をしなければなりません。独自に実装するとして、各関数がSnappingに対してどのような役割を担っているか理解が必要です。

では、どのようにしてこのSnappingは実現しているのでしょうか。

今回はLinearSnapHelperを例にしつつ、SnapHelperがどのようにしてSnappingを実現しているのかを深ぼっていこうと思います。

LinearSnapHelperを用いた中央寄せ

前提にはなりますが、そもそもSnappingがどういった挙動なのか、どう実装すればSnappingができるのかを簡単に見てみましょう。

今回はよくある横スクロールで挙動を確認していきたいので、横スクロールのRecyclerViewを作成します。

val layoutManager = LinearLayoutManager(
    requireContext(),
    LinearLayoutManager.HORIZONTAL,
    false,
)
recyclerView.layoutManager = layoutManager

それではLinearSnapHelperでSnappingを実装していきます。

val snapHelper = LinearSnapHelper()
snapHelper.attatchToRecyclerView(recyclerView)

これで簡単に中央にSnappingするRecyclerViewが完成しました。

Snappingの仕組み

では、どのようにSnappingを実現しているのでしょうか。

SnapHelperには3つの抽象関数があります。

  • calculateDistanceToFinalSnap
  • findSnapView
  • findTargetSnapPosition

特に関連しそうな calculateDistanceToFinalSnapfindTargetSnapPosition にフォーカスして、まずはSnapHelperの実装を見ていきます。

SnapHelper側ではFlingが実行されると snapFromFling が実行されます。

private boolean snapFromFling(
    @NonNull RecyclerView.LayoutManager layoutManager,
    int velocityX,
    int velocityY
) {
    // 省略
    int targetPosition = findTargetSnapPosition(
        layoutManager,
        velocityX,
        velocityY
    );
    if (targetPosition == RecyclerView.NO_POSITION) {
        return false;
    }

    smoothScroller.setTargetPosition(targetPosition);
    layoutManager.startSmoothScroll(smoothScroller);
    return true;
}

findTargetSnapPosition で計算されたpositionの位置まで startSmoothScroll でスクロールが実行されていくのが分かります。また smoothScroller に対象の位置をsetしているのもわかります。

次に calculateDistanceToFinalSnap の方を見ていきましょう。

@Nullable
@Deprecated
protected LinearSmoothScroller createSnapScroller(
    @NonNull RecyclerView.LayoutManager layoutManager
) {
    if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
        return null;
    }
    return new LinearSmoothScroller(mRecyclerView.getContext()) {
        @Override
        protected void onTargetFound(
            View targetView,
            RecyclerView.State state,
            Action action
        ) {
            if (mRecyclerView == null) {
                // The associated RecyclerView has been removed so there is no action to take.
                return;
            }
            int[] snapDistances = calculateDistanceToFinalSnap(
                mRecyclerView.getLayoutManager(),
                targetView
            );
            final int dx = snapDistances[0];
            final int dy = snapDistances[1];
            final int time = calculateTimeForDeceleration(
                Math.max(Math.abs(dx), Math.abs(dy))
            );
            if (time > 0) {
                action.update(dx, dy, time, mDecelerateInterpolator);
            }
        }

        @Override
        protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
            return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
        }
    };
}

先ほどsetした対象の位置に対して onTargetFound が実行され、 calculateDistanceToFinalSnap で計算された距離分だけ動いていくことがわかります。

まとめると以下のことがわかります。

  • findTargetSnapPosition で対象となるViewの位置を計算
  • 計算した位置までスクロール
  • 対象となるViewが画面上に出てきたら onTargetFound が実行される
  • 微調整を行なってSnappingを実現させる

LinearSnapHelperの中央寄せ

では最後にLinearSnapHelperの実装を見て、どのように中央へSnappingをしているかを見てみます。

@Override
public int[] calculateDistanceToFinalSnap(
    @NonNull RecyclerView.LayoutManager layoutManager,
    @NonNull View targetView
) {
    int[] out = new int[2];
    if (layoutManager.canScrollHorizontally()) {
        out[0] = distanceToCenter(
            targetView,
            getHorizontalHelper(layoutManager)
        );
    } else {
        out[0] = 0;
    }

    if (layoutManager.canScrollVertically()) {
        out[1] = distanceToCenter(
            targetView,
            getVerticalHelper(layoutManager)
        );
    } else {
        out[1] = 0;
    }
    return out;
}

private int distanceToCenter(@NonNull View targetView, OrientationHelper helper) {
    final int childCenter = helper.getDecoratedStart(targetView)
            + (helper.getDecoratedMeasurement(targetView) / 2);
    final int containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
    return childCenter - containerCenter;
}

distanceToCenter で対象となるViewの中央とコンテナの中央間の距離が計算されます。

findTargetSnapPosition の方はコード量が多いので割愛しますが、スクロールした時のスクロール量とViewの大きさをもとに最も中心に表示できるViewの位置を返すロジックになっていました。

改めてSnapHelper含め、LinearSnapHelperを振り返ると以下のようになります。

  • findTargetSnapPosition でスクロール量から対象となるViewを事前計算
  • 対象のViewまで startSmoothScroll を使ってスクロール
  • 対象となるViewが画面上に出てきたら onTargetFound が実行される
  • calculateDistanceToFinalSnap で対象のViewの中心とコンテナの中心間の距離を計算
  • その分移動させることで、Viewの中心とコンテナの中心が重なり中央へのSnappingが実現できる

まとめ

今回はSnappingの挙動からSnapHelperのSnappingに関わる部分を掻い摘んでざっくりと解説させていただきました。Snapping自体は珍しい挙動ではなく様々なサービスで利用されています。 SnapHelperの挙動について理解を深めておくことで多様なSnappingの実装が簡単にできるようになりますし、SnapHelperに依存せず同様な処理を実装できるのではないかと思います。

ISUCON初挑戦記

こんにちは、TVerでバックエンドエンジニアをやっている水野です。

こちらは TVer Advent Calendar 2023 の18日目の記事です。

初めてISUCONに挑戦しました。結果は最終スコア0で、悔いが残りますが、次回ISUCON14(開催未定)に向けての備忘録として振り返ります。

参加までの流れ

私がISUCON13に参加したきっかけは、ISUCON夏祭りへの参加でした。

techblog.tver.co.jp

ISUCON夏祭りのハンズオンでprivate-isuを解いたり、トークセッションで先人たちの戦略を聞いたりしているうちに、自分も参加者としてスキルを試してみたいという強い思いが芽生えました。

チームメンバーは社内で募ったSREエンジニア(tomoner_94444)、データチームエンジニア(sy6124)、そして私の3人で構成されました。

ISUCON13 オンラインライブ中継 - YouTube

当日やったこと

各自が当日に行った作業は以下の通りです。詳細は割愛し、サマリのみを記載します。

  • スロークエリ、N+1調査
  • Index貼る
  • tagsテーブルのオンメモリキャッシュ化

tomoner_94444

  • DB分割(アプリ側、DNS)
  • MySQL replication構成
  • HA proxy導入クエリ分散
  • カーネル設定変更 TCP 周り
  • Nginx cache周りの設定追加

sy6124

  • pprofを入れようとして失敗
  • アプリの改善(forでクエリを投げまくっているところのforをなくす

会社からのサポート

株式会社TVerは、ISUCONへの挑戦を後押しする文化があり、以下のサポートを提供していただきました。

また、ISUCON13の企業スポンサーとして、大会への直接的なサポートも行っています。

techblog.tver.co.jp

  1. ISUCON素振り、ISUCON当日を勤務扱い
  2. ISUCON企業スポンサー枠として参加
  3. ISUCON素振り時、会社のAWS SandBox環境を提供
  4. ISUCON当日に、昼食とコーヒーの差し入れ

ISUCON素振り、ISUCON当日を勤務扱い

休日に行ったISUCON素振りとISUCON当日を勤務扱いとして認めて頂きました。勤務扱いにより、土日をフルに使った素振りを行うことができました。ISUCONと通常業務を両立させやすい環境で助かりました。

ISUCON企業スポンサー枠として参加

ISUCON企業スポンサー枠を得て、ISUCON13への参加が可能となりました。これにより、激しいISUCONエントリーの競争をスポンサー枠で回避し、無事に参加できました。

ISUCON素振り時、会社のAWS SandBox環境を提供

ISUCON素振り時、会社のAWS SandBox環境を利用させて頂きました。AWSのコストを気にすることなく、本番同様の素振りができるのは非常にありがたかったです。

ISUCON当日に、昼食とコーヒーの差し入れ

ISUCON当日に、CTOからカツサンド、マネージャー陣からコーヒーとスイーツを差し入れして頂きました。人情厚い差し入れに感謝です!

来年への抱負

今年のISUCONは最終スコア0で終わり、悔しさが残ります。来年が開催されるならば、再び参加し、納得のいくスコアを収めたいと考えています。

最終スコアが0になった原因は恥ずかしいので触れませんが、十分な素振りの時間を確保していれば防げたでしょう。。。来年はさらに準備をして臨みます。

「百聞は一見に如かず」のように、100回のISUCON素振りをしても一回のISUCON本戦参加によって得られる課題と楽しさがあると思います。今回それを知ることができ、ISUCON迷い中の方にはぜひとも参加をおすすめします。

今年のISUCON参加を通じて、自身のバックエンドエンジニアとしての通用する技術や不足している技術を明確にできました。これを日々の業務や自己研鑽に生かしていきます!

Media-JAWS にて登壇しました #jawsug #mediajaws

本記事はTVer Advent Calendar 2023の15日目の記事です。

はじめに

こんにちは。去年も15日目の記事を書いていたバックエンドエンジニアの伊藤です。

11/15にInterBEEに合わせて海浜幕張で開催されたMedia-JAWSにて初の登壇をしてきました。

ということで今年は登壇ブログを書いていきたいと思います。

media-jaws.doorkeeper.jp

Media-JAWSとは

以下、公式サイトからの引用です。

Media-JAWSは、例えば急激なトラフィック処理や映像や画像のワークロード処理、セキュリティ対策など、放送・ラジオ・新聞・雑誌・Web・SNSなどのメディア特有の性質が求められるサービスを、AWSを活用してどのように構築・運用しているか、といった情報交換や、交流の場として活用される勉強会です。

開催頻度は、3ヶ月に一度を目標にしています。 関東以外の方のために、基本的にライブ配信を実施します。 いずれは関東以外での開催や、関東以外の方によるリモートでの登壇なども行いたいと考えています。

ハッシュタグは #mediajaws

登壇の経緯

SREチームの加我さんから登壇しない?とお話があり、やりますということで登壇させていただきました。

やりますと言って締切が決まれば大体なんとかなるの精神です。

元々登壇はいつかしたいと思っていたのですが、今年の4月に行われたAWS Summit Tokyo 2023で色々な方々と交流する機会がありそこでモチベーションを得たことも大きなきっかけの一つです。とても良い体験でした。

登壇内容について

資料はこちらです。 speakerdeck.com 発表内容についてですが、2022/4のリニューアルを機に大幅に下がってしまったSEOのスコアを取り戻すためにバックエンドエンジニアがやったこととその延長線上で行ったOGP対応についてのお話ししました。

4行にまとめれば、以下のようなことが書いてあります。

  • 2022/04にリニューアルしたけどSEOのスコア下がっちゃった
  • 検索結果にTVer出てこないからバックエンドで対策して欲しい
  • 頑張って対策したらリニューアル前より良くなったよ
  • ついでにOGP対応もやったよ

大枠の実装は今も変わらずTVerで使われているものです。ただ実はまだ何個か対応しなければいけないこともあり現在も改修は続けられています。

特にSEO対策に使っている簡易クローラーはECSタスクで定期的に実行しているのですが、chromedpがハングしてタスクが終わらないという事象*1に立ち向かっていたり、OGP対応ではLPから切り替えきれていないページの切り替えを引き続き対応しています。アラートに一区切りつけて無事落ち着いて年を越せると良いなと思っております。

余談ですが、去年のアドベントカレンダー で寄稿したHTMLの編集の話はこのSEO対策のためにやっていたことだったので、ちょうど一年越しに取り組みについて深掘りする形の資料を書くことになりました。

また上の資料にあるTVerについての情報は以下の資料の引用になります。 TVerについて全ての資料を確認したい場合は是非こちらをご覧ください。

speakerdeck.com

おわりに

初めての登壇ということで緊張しましたが、なんとか無事終えることができました。資料作成や人前で話すこと自体卒研ぶりで、ほぼ未知の世界でしたが良い経験でした。 今年できたら良いなーくらいのゆるい目標でしたが、年内に登壇することができ満足しています。

資料のレビューをしてくれた社内の方々や、当日サポートしてくれたスタッフの方々ありがとうございました。 また機会があればどこかで登壇したいなと思いました。

SREチームの加我さん *2 によるInterBEEやMedia-JAWSの参加レポートもありますので是非。 techblog.tver.co.jp

*1:無限にアラート飛んできてとてもつらい

*2:最初から最後までとてもお世話になった

私とAWSと2023年

こんにちは。
アドテク領域のエンジニアをしています安部です。
こちらは TVer Advent Calendar 2023 の13日目の記事です。

個人的に今年はAWSに縁があった年でしたのでAWSにからめて1年を振り返ります。

1月〜3月

フロントエンド開発をがんばっていました。(いきなりAWSじゃない!)
OCJP Silver取ったりしてました。(バックエンド!)
開発業務専門でAWSをどう使っているか、どうデプロイされているのかはあまり気にしていませんでした。
転機になったのは3月にログのアラートをslackに連携したいという話が出てきて、CloudWatch LogsとLambdaを使用してslack通知をするツールを作成したことでした。
AWSのサービス駆使して色々できそう!これはサービス知っておいたほうが得だな!と思い始めた時期です。

4月〜6月

AWS Summit Tokyo参加

AWS Summit Tokyoに行きました。

個人的テーマは「AWSを知ること」でした。
その時の記事はこちら

techblog.tver.co.jp

ここでAWSのサービスを色々知りました。
認定資格を取ってみようかなとも考え始めた時期です。
余談ですが、このときにもらったクッションは今でも使ってます。

ツール作成

4月には広告入稿システムのリプレイスがありました。
このシステムと対向システムでデータの同期しているのですが、データに差分が出ていないか確認する必要があります。
そのため対向システムから不整合が発生している可能性のあるデータを連携していただき、確認するという定常業務がありました。
元々の確認方法は連携データを1データずつSQLを実行して確認する方式だったのですが、Lambdaを使い自動化しました。
ツールについては詳しい内容を別ブログにしたいなと思っています。

7月〜12月

資格取得

7月にAWS Certified Cloud Practitioner を取得しました。

なんとか合格

 

2週間Udemyの問題集をひたすら解いて挑みました。
次はASSOCIATE資格を狙っています。

CI/CD構築

Bitbucket Pipelinesを使用したCI/CD構築を担当しました。
そこでAWSのどのサービスをどのように使っているのか、どのような設定をすればいいのかがちょっとわかりました。
CLF資格取得と被っている時期にやっていたので「ここ見たぞ!」「これかやったわ!」と心のなかで呟きながら作業したり資格勉強したりしていました。
資格のための勉強が業務に繋がったので資格を取ったかいがありました。

 

JAWS Festa 参加

10月は福岡で行われたJAWS Festaに参加しました。

その時のブログはこちら

techblog.tver.co.jp福岡ならではの事例を聞くことができたり、懇親会で多くの参加者の方とお話できたりして楽しかったです。
個人的目標の「地元福岡であるテック系イベントに参加する」が達成できて嬉しくもありました。

天神のエンジニアカフェ、年末に行ってみたいなと思っています。

 

2023年まとめ

AWSに興味を持って、学び、業務に反映できた

 AWSを知ると面白そうだぞと興味を持ち、イベントに行ったり資格の取得を通して知見を広め、それを業務に還元することができました。

 おそらく年初に興味を持って学んでいなかったらツールを開発しようとも思わなかったし、CI/CDの構築も時間がかかったのではないかと思います。

 興味と業務をリンクさせることができたのは収穫でした。

・テックブログの執筆は難しい

 前職も含めイベントには何度か参加しましたが、フィードバックは社内クローズのちょっとした報告書を作成する程度でした。

 テックブログでイベントのレポートを書くということは新鮮で、どう書けばちゃんと伝わるか、何を学んだか理解してもらえるかを考え書くことに苦労しました。

 もっと文章力を上げたいと思う1年でした。

・イベント参加はおもしろい+難しい

 イベントで事例を聞くこと、ハンズオンセミナーを受講して実際に体験すること、どのように業務に活かすかを考えることはとても面白いことだと感じています。

 その一方、イベント参加者とたくさんお話したいと思いつつも「何を喋ろう…」や「今話しかけていいタイミングなのかしら…」と若干の人見知りを発動してなかなか交流を図ることができないことがありました。

 コミュニケーションの取り方難しいと改めて実感する1年でした。

来年の抱負

せっかくなので来年の抱負も書きます。
・テックブログ書く
 私が書いているブログ、今のところイベントに参加しました!系ブログしかありません。
 来年は「こんなことやったよ!」というブログも書きたいと思います。
・Lambdaともっと仲良くなる
 ツールを作ることがきっかけで使うようになったLambdaですが、まだLambdaのことを半分も活用できていない気がしています。
 もっとLamdbaを知って業務に取り入れていければと思っています。

・資格取得

 ASSOCIATE資格のどれか1つは取りたいです。

 弊社では資格取得支援制度があり、事前に申請し合格した場合は受験料の補助があります。

 すでにSAAの事前申請はしているので来年の早い時期に取得できるように勉強しています!
・イベント参加
 来年はもっといろんなイベントに行きたいと思います。
 東京・福岡はもちろん、日本全国イベント巡り(と温泉行って観光)したい!
 

New Relicをフルに活用するためにデータ量とコストに気を配る

こんにちは、TVerの加我です。
こちらは TVer Advent Calendar 2023New Relic 使ってみた情報をシェアしよう! by New Relic Advent Calendar 2023 の8日目の記事です。

みなさまNew Relicを活用していますか?サービスの信頼性を担保していますか?オブザーバビリティの導入・実現に向けてNew Relicを使い倒していますか?

New Relicは非常に高機能なオブザーバビリティプラットフォームです。TVerではフロントエンドからバックエンドまでNew Relicを活用した横断的な観測を行っています。しかしNew Relicを導入し活用していくにつれて気になってくるのがコストです。

ということで、New Relicを活用しつつコストを最適化するためのポイントについて考えていきます。

New Relicのコスト計算についておさらい

まずはNew Relicの価格設定について理解していきましょう。
New Relicでは「有償のユーザー数」と「取り込むデータ量」の2点が請求対象となります。

ユーザー数による請求については後述する有償のユーザー追加がなければ変動がない部分なのでほぼ固定費、取り込むデータ量についてはアクセス数やイベント数により大きく変動することがあるため変動費と考えることができます。つまり取り込むデータ量を最適化することが重要です。

docs.newrelic.com

こちらについてはAdministrationのPlan & usageにて利用状況を確認することができます。

Plan & usage

Plan & usageのUsage breakdownからView detailsを選択することでデータソース毎の取り込むデータ量を確認することができます。

Data ingested (GB) per day by data source

1. 有償のユーザー数

New Relicには「Full Platform User」「Core User」「Basic User」という3つのユーザー種別があります。このうち有償のユーザーとして請求対象になるのは「Full Platform User」と「Core User」です。TVerではFull Platform UserとBasic Userを利用した権限管理を行っています。Core Userはユースケースがマッチしなかったので利用していません。

ユーザー区分 利用者 利用目的
Full Platform User エンジニア
カスタマーサポート
バックエンドAPIのTransaction調査
モバイルアプリのクラッシュ調査
クラウドインフラのリソース調査
Core User - -
Basic User ディレクター
エンジニア
ダッシュボードを通じたデータ確認

ユーザー毎の権限については下記ドキュメントを参照ください。

docs.newrelic.com

2. 取り込むデータ量

イベントやメトリクスやログといったテレメトリーデータをNew Relic (NRDB) に取り込む際に請求対象となります。TVerではフロントエンドからバックエンドまでNew Relicを利用しているため、各所で発生したテレメトリーデータがNew Relic (NRDB) に取り込まれ請求対象となります。

主なデータの発生元 New Relicへの取り込み方法 主なデータソース (NRDB)
Webブラウザ New Relic Browser Browser events
Mobileアプリ New Relic Mobile Mobile events
バックエンドAPI New Relic APM APM events
クラウドインフラ
(AWSGoogle Cloud)
New Relic Infrastructure Metrics
Infrastructure integrations
各種ログ New Relic Logs Logging

docs.newrelic.com

今回はこちらの「取り込むデータ量」についてのお話です。

何が起こったのか

2022年4月のサービスリニューアル以降、定期的なNew Relic Agentのアップデートを行えていませんでした。New Relic Agentには定期的なバグフィックスやセキュリティアップデートや機能追加が行われているため、2023年3月12日頃に諸々のAgentのアップデートを行いました。しかしアップデートを実施した結果、New Relic Browser Agent (Web) の更新によりBrowser eventsで取り込むデータ量が10倍程度に膨れ上がってしまいました。

Browser eventsのデータ量が急増

New Relicのサポートにもアップデート内容の確認や原因の調査についてご相談したのですが、残念ながらこれという決定的な証拠を見つけることができませんでした。可能性としては「当初の設定の不備により必要なデータを取得できていなかった」もしくは「分散トレーシング周りのアップデートがあり取得できるイベントが増えた」かと考えています。

フロントエンド系のデータを扱うNew Relicアカウントで急激なデータ増加

BrowserEventsBytesで急激なデータ増加

当時はNew Relicを駆使してサービスの信頼性を可視化・担保するというミッションを推進していたため「今はどんどん可視化を進めていきたい」「何はともあれオブザーバビリティ」といった流れで進めていった結果、データ量やコストの意識が二の次になってしまいました。New Relicにかかるコストのオブザーバビリティは後回しになってしまい恥ずかしい限りです。

特定のURLが高頻度で閲覧されていたりスクレイピングされている可能性も疑いましたが、全体的なイベント数が増加していることを確認して改善案を考えました。

改善

取り込むデータ量の可視化と通知、そしてデータ量の削減という二軸で対応しました。

1. 取り込むデータ量の可視化 & 定期的な通知

まずは「現状どのような取り込みデータ量になっているのか」「いつからデータ量が増加したのか」「どのデータソースで増加があったのか」を判断できるようなデータおよびダッシュボードを整備しました。そのためにNew Relicアカウント別のグラフとデータソース別のグラフを用意しました。

NRQLだとこんな感じです。24時間のグラフだと増減がわかりにくかったので3日間のデータを毎日見るようにしています。

# [前日比] アカウント別のデータ量
SELECT rate(sum(GigabytesIngested), 1 day) AS avgGbIngestTimeseriesByAccount FROM NrConsumption WHERE productLine = 'DataPlatform' FACET consumingAccountName TIMESERIES AUTO SINCE 3 days AGO COMPARE WITH 1 day ago
# [前日比] データソース別のデータ量
SELECT rate(sum(GigabytesIngested), 1 day) AS avgGbIngestTimeseries FROM NrConsumption WHERE productLine = 'DataPlatform' FACET usageMetric LIMIT MAX TIMESERIES AUTO SINCE 3 days AGO COMPARE WITH 1 day ago

上記のNRQLをグラフにしたのが下記のスクリーンショットです。

New Relic全体のデータ量推移 (アカウント別とデータソース別)

また、バックエンド (New Relic APM, New Relic Infrastructure) に比べるとフロントエンド (New Relic Browser, New Relic Mobile) の方がデータ量にばらつきが大きいことがわかっているため、主要なイベント数の推移を把握できるグラフを用意しました。こちらも24時間のグラフだと増減がわかりにくかったので3日間のデータを毎日見るようにしています。

# とあるBrowserアプリケーションのグラフをイベントごとに作成
SELECT count(*) FROM AjaxRequest WHERE appName = '<ブラウザアプリ>' TIMESERIES SINCE 3 days ago
SELECT count(*) FROM BrowserInteraction WHERE appName = '<ブラウザアプリ>' TIMESERIES SINCE 3 days ago
SELECT count(*) FROM JavaScriptError WHERE appName = '<ブラウザアプリ>' TIMESERIES SINCE 3 days ago
SELECT count(*) FROM PageView WHERE appName = '<ブラウザアプリ>' TIMESERIES SINCE 3 days ago
SELECT count(*) FROM PageViewTiming WHERE appName = '<ブラウザアプリ>' TIMESERIES SINCE 3 days ago
# とあるMobileアプリケーションのグラフをイベントごとに作成
SELECT count(*) FROM Mobile WHERE appName = '<モバイルアプリ>' TIMESERIES SINCE 3 days ago
SELECT count(*) FROM MobileCrash WHERE appName = '<モバイルアプリ>' TIMESERIES SINCE 3 days ago
SELECT count(*) FROM MobileRequest WHERE appName = '<モバイルアプリ>' TIMESERIES SINCE 3 days ago
SELECT count(*) FROM MobileRequestError WHERE appName = '<モバイルアプリ>' TIMESERIES SINCE 3 days ago
SELECT count(*) FROM MobileSession WHERE appName = '<モバイルアプリ>' TIMESERIES SINCE 3 days ago
SELECT count(*) FROM MobileVideo WHERE appName = '<モバイルアプリ>' TIMESERIES SINCE 3 days ago
SELECT count(*) FROM Span WHERE appName = '<モバイルアプリ>' TIMESERIES

上記のNRQLをグラフにしたのが下記のスクリーンショットです。各グラフをStacked Bar形式にして積み上げて確認しています。

Browser系のイベント数推移

Mobile系のイベント数推移

推移を見るグラフが整備できたのであればあとは通知です。クラスメソッド株式会社の新井成一さんがNew RelicのダッシュボードをSlackへ定期的に投稿するためのツールを公開してくれていたため、こちらを利用させていただきました。

github.com

dev.classmethod.jp

実際にSlackに送られるとこのような感じになります。

Slack通知

これにより、データ量の推移と異常を見逃すことがなくなりました。Anomalyでデータ容量に対してアラートを設定するのもアリかもしれません。

2. 取り込みデータ量の削減

New Relicで取り込みデータ量を削減するには「取り込みデータのサンプリングを実施する」か「Data Drop Ruleを設定してデータの除外設定をする」という2つの方法があります。当時はデータのサンプリングについての検証が行えていなかったため、素直にData Drop Ruleを設定して不要なデータを除外する方向で対応しました。

Data Drop RuleについてはNerdGraphというGraphQLのAPIを利用してデータを除外するためのルールを作成・削除することが可能です。

docs.newrelic.com

NerdGraph API Explorer にアクセスすると下記のような画面が表示されます。

NerdGraph API Explorer

例えばDrop Ruleの一覧を取得したい場合には下記のようなクエリを入力して実行することでDrop Ruleの一覧を取得することが可能です。

{
  actor {
    account(id: <対象となるNew RelicのアカウントID>) {
      nrqlDropRules {
        list {
          rules {
            id
            nrql
            accountId
            action
            createdBy
            createdAt
            description
          }
          error {
            reason
            description
          }
        }
      }
    }
  }
}

また、Drop Ruleを追加したい場合には下記のようなクエリを入力して実行することでDrop Ruleを追加することが可能です。
DROP_DATAは指定したNRQLに該当するデータを除外するもの、DROP_ATTRIBUTESはNRQLに該当する属性・カラムのデータのみを除外することが可能です。

mutation {
  nrqlDropRulesCreate(
    accountId: <対象となるNew RelicのアカウントID>
    rules: {action: <DROP_DATA|DROP_ATTRIBUTES>, description: "<Dropルールの説明>", nrql: "<Dropしたいデータを抽出するためのNRQL>"}
  ) {
    failures {
      error {
        description
        reason
      }
    }
    successes {
      account {
        id
        name
      }
      accountId
      action
      createdAt
      createdBy
      description
      creator {
        id
        name
      }
      id
      source
      nrql
    }
  }
}

しかしNerdGraphはあまり利便性が高いとは言えないため、ここに関してはTerraformで管理することをおすすめします。

# drop_dataの例
resource "newrelic_nrql_drop_rule" "foo" {
  account_id  = 12345
  description = "Drops all data for MyCustomEvent that comes from the LoadGeneratingApp in the dev environment, because there is too much and we don’t look at it."
  action      = "drop_data"
  nrql        = "SELECT * FROM MyCustomEvent WHERE appName='LoadGeneratingApp' AND environment='development'"
}

# drop_attributeの例
resource "newrelic_nrql_drop_rule" "bar" {
  account_id  = 12345
  description = "Removes the user name and email fields from MyCustomEvent"
  action      = "drop_attributes"
  nrql        = "SELECT userEmail, userName FROM MyCustomEvent"
}

Resource: newrelic_nrql_drop_rule registry.terraform.io

Data Drop Ruleの整備により不要なデータを除外することに成功しました。オレンジ色のBrowser eventsの領域が小さくなっていることがわかります。この後に青のMobile eventsを最適化するためにDrop Ruleの調整を行っています。

Plan & usageにてBrowser eventsの減少を確認

フロントエンド系のデータを扱うNew Relicアカウントでデータの減少を確認

BrowserEventsBytesのデータ減少を確認

今後の展望

現在モバイルアプリ開発にて取り込みデータのサンプリングの検証を進めており、Data Drop Ruleとサンプリングの併用でデータ量の最適化を行う予定です。

私達が不要と考えて除外している特定のデータが実はユーザーの体験に関わる問題に関連しており、それを見逃してしまうという可能性を回避したいというのが背景です。本来であれば全てのデータを取得したうえで一律に削減したほうが良いと考えていますが、まだまだ検証中の段階でありリリースは2024年前半の見込みです。

まとめ

TVerにおけるオブザーバビリティの導入・推進の裏側で発生していたデータ量とコストの問題にフォーカスした記事でした。パブリッククラウドSaaSも便利ですが請求に影響するデータのチェックを怠ってはいけません。1つの設定変更で大きな請求が発生してしまう可能性があります。まずはしっかりと状況を可視化したうえで対処していきましょう。

余談ですが、株式会社ヘンリー様のエンジニアブログにある「オブザーバビリティにはお金がかかる」という記事を拝見しまして「私も会社にはNew Relicとオブザーバビリティの重要さについて説明してきたし理解して貰ってると思うけど、とはいえ安くはないし悩ましいよなぁ・・・」という思いから今回の記事の執筆のモチベーションとさせていただきました。

dev.henry.jp

BigQueryのNULLの扱いまとめ

こんにちは、TVerでデータ分析をしている高橋です。
こちらは TVer Advent Calendar 2023 の12日目の記事です
弊社の分析業務は、主にBigQueryに蓄積されたデータを対象としています。データ処理の効率を向上させるため、データの前処理から集計までを一貫してSQLクエリで実施しています。この過程でNULL値の取り扱いは避けて通れない重要なテーマとなっています。
この記事では、(直近タスクでNULL含む処理の検証に多くの時間を溶かした筆者が)弊社で頻繁に使用されるSQLクエリの処理においてNULLがどのように扱われるかをまとめたのでご紹介します。

チートシート

今回調べた内容を整理すると以下の3パターンになりました。

種類 演算子、構文、関数など
NULLとして扱われる 四則演算子, ビット演算子, 比較演算子, 論理演算子,
ARRAY_AGG, FIRST_VALUE, LAST_VALUE, ORDER BY
無視される AVG, MAX, MIN, SUM, STRING_AGG, LOGICAL_AND, LOGICAL_OR, COUNT
FALSE として扱われる CASE, IF

以下、サンプルクエリとともに個別に見ていきます。

演算子

公式ドキュメントには、特に指定がない場合、すべての演算子において被演算子のいずれかが NULL の場合はNULL を返す、と記載されています。

例えば以下のクエリは全てNULL を返します。

SELECT
    -- NULL = 1 <-- これはエラーが発生する
    CAST(NULL AS INT64) = 1,  --> NULL
    CAST(NULL AS INT64) + 1,  --> NULL
    CAST(NULL AS INT64) < 1 , --> NULL

論理演算子の場合は3値論理に基づいて判定が行われます。
演算子NULLを含んでいながらNULLではない値が返ってくるケースがあるようです。

公式ドキュメントから引用

条件式

CASE文FALSENULLは区別されないようです。
以下のクエリを実行すると、すべてのWHEN句でNULLとなるのでunknownが返ってきます。

SELECT
    CASE 
        WHEN NULL AND TRUE THEN "a"
        WHEN CAST(NULL AS INT64) = 1 THEN "b"
        ELSE "unknown"
    END --> "unknown"

IF文も同様です。以下クエリを実行すると2が返ってきます。

SELECT
    IF(NULL, 1, 2) AS col1 --> 2

集計関数

集計関数のうち、AVG, MAX, MIN, SUM, STRING_AGGnon-NULL値を対象として処理が行われるようです。

SELECT
    AVG(val) AS avg_val,    --> 3.0
    MAX(val) AS max_val,    --> 4
    MIN(val) AS min_val,    --> 2
    SUM(val) AS sum_val,    --> 6
    STRING_AGG(CAST(val AS STRING)) AS str_vals --> "2,4"
FROM (
    SELECT
        val
    FROM
        UNNEST(ARRAY[2, 4, NULL]) AS val
)

LOGICAL_AND, LOGICAL_OR も同様にnon-NULL値を対象として処理が行われます。

SELECT
    LOGICAL_AND(flag), --> [TRUE, FALSE] の AND なので FALSE
    LOGICAL_OR(flag)   --> [TRUE, FALSE] の OR  なので TRUE
FROM
    UNNEST(ARRAY[TRUE, FALSE, NULL]) AS flag

COUNT は入力の行数または式がNULL以外の値に評価された行数を取得します。

SELECT
    COUNT(1),    --> 3
    COUNT(flag), --> 2
    COUNT(NULL)  --> 0
FROM
    UNNEST(ARRAY[TRUE, FALSE, NULL]) AS flag

ARRAY_AGG は配列にNULL要素が含まれているとエラーが発生します。
IGNORE NULLSで除外してあげましょう。

SELECT
    -- ARRAY_AGG(val) AS arr           --> Error
    ARRAY_AGG(val IGNORE NULLS) AS arr --> [2,4]
FROM (
    SELECT
        val
    FROM
        UNNEST(ARRAY[2, 4, NULL]) AS val
)

ナビゲーション関数

FIRST_VALUE, LAST_VALUENULL値を含んで計算されます。
例えば、ユーザーが最後に視聴した日付を日次で取得する処理を考えます。
この場合IGNORE NULLSオプションでNULLを無視することで所望の結果を得ることができます。

WITH
play_logs AS (
    SELECT
        "xxx" AS user_id,    
        day,
        logs.begin_d IS NOT NULL AS has_played,
    FROM
        UNNEST(GENERATE_TIMESTAMP_ARRAY(
            TIMESTAMP("2023-12-01 00:00:00"),
            TIMESTAMP("2023-12-05 00:00:00"),
            INTERVAL 1 DAY
        )) AS day
    LEFT OUTER JOIN (
        SELECT
            begin_d
        FROM
            UNNEST(ARRAY[
                TIMESTAMP("2023-12-01 00:00:00"), 
                TIMESTAMP("2023-12-02 00:00:00"), 
                TIMESTAMP("2023-12-05 00:00:00")
            ]) AS begin_d
    ) AS logs
    ON
        day = logs.begin_d
)

SELECT
    *,
    LAST_VALUE(IF(has_played, day, NULL)) OVER(PARTITION BY user_id ORDER BY day ASC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS day_last_played,
    LAST_VALUE(IF(has_played, day, NULL) IGNORE NULLS) OVER(PARTITION BY user_id ORDER BY day ASC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS day_last_played_ignore_nulls
FROM
    play_logs

実行結果

ORDER BY句

昇順に並べるとNULLは一番先頭に来るようです。
順序は以下の通り(公式doc)。

はいはいなるほどNULLが最初で...NaN?

NaN (Not a Number)は不正な演算を行った場合に返ってくる値のようです(SELECT IEEE_DIVIDE(0, 0)NaNを返す)。
NULLS LASTオプションでNULLを末尾に追いやることが可能です。

SELECT
    val
FROM
    UNNEST(ARRAY[1, 3, 2, NULL]) AS val
ORDER BY 
    val NULLS LAST

1が先頭になりました

まとめ

  • NULL含む論理演算と条件式は難しいのでNULL埋めしてあげたい
  • ORDER BY句のNULLS LASTは便利そう

最後に

ニッチ感の否めない記事となりましたが、このようにTVerではデータ分析を通じてより確からしい示唆を得ようと日々精進を重ねています。
もしご興味があれば以下よりご連絡ください。お待ちしております!

recruit.tver.co.jp