Google Cloud Run で社内フリードリンク在庫判定ボットを作ってみた。

はじめまして、データプロダクト開発チームの@yuu_itoです。

一緒に仕事をしている@toohskとフリードリンクの在庫判定をするSlackボットを作成しましたので紹介します。

フリードリンクとフリードリンク在庫判定ボットとは

Quipperでは福利厚生の一環として、社内でフリードリンクが提供されています。

フリードリンクの冷蔵庫はオフィスエリアの一角にあり定期的に充填されています。

既にWebカメラを搭載したRaspberry Piで定期的に様子を撮影しており、 わざわざ席を立って在庫の状況を見にいかなくても、 Slackの専用チャンネルより確認できます。(thanks to @yoshimaru46)

Raspberry Piから撮影される冷蔵庫の画像例

しかし、空っぽの写真だけが延々とチャンネルに流れていても意味がない (フリードリンクは大変人気で、入荷日以外は在庫が空状態で続くことも多いのです...)ので 在庫が空になり、その後飲み物が入荷したときだけ通知すればみんなが嬉しいのでは? と考えました。

システム構成

今回用意したシステムは以下の通りです。

コンポーネント図 処理の流れは以下の通りです。

  1. 冷蔵庫を撮影する。
  2. Slackのタイムラインに撮影した画像を投稿する。
  3. Slack Real Time Messaging APIからGoogle Apps Script(GAS) にSlackのチャンネルに流れているメッセージを送信する。
  4. 送られてくるメッセージから対象の画像のみを画像分類ロジック(Google Cloud Runで実装) に送信する。
  5. 事前に学習した画像分類モデルで推論し結果を返す。
  6. GASのキャッシュ機能(CacheService)を用いて直前の画像分類結果と比較し、 『在庫なし』から『在庫あり』へ状態が変化した場合のみ、Slackチャンネルに投稿(@here)する。
  7. Quipperメンバーに通知が飛ぶ。

Google Cloud Run について

実はボット開発開始時はGoogle Cloud Functionsというサーバーレスアーキテクチャのサービスを用いて、画像分類モデルの推論を動かしていました。 しかし、より新しいサービスとしてGoogle Cloud Runが発表されており業務利用時の検証を兼ねて移行してみました。

移行した主な理由はCloud Runがコンテナベースのアーキテクチャであることでした。それというのもCloud Functionsはランタイム(Python, Node.js, Go)からしか選べず、他の言語はもちろんOSなどのミドルウェアを選ぶことはできません。

一方、Cloud RunはDockerコンテナとして用意できればなんでも選べるため、 例えば

  • 最新バージョンのPythonをビルドしたコンテナイメージ
  • OS依存のライブラリを利用するコンテナイメージ

などを実行環境として採用することができます。 今後、ボットに複雑な学習や推論をさせてみたり、業務で新規開発を検討した場合に新しいミドルウェアやライブラリを候補にできるのはメリットだと思い、 このボットを人柱に 移行しました。

執筆時点ではベータ版ではありますが、すでにTokyoリージョンでの利用もできたのも理由のひとつです。

Google Cloud Functions から Cloud Run に移行するためにしたこと

1. Dockerfileの追加

基本的には公式ドキュメントが丁寧に用意されています。 Cloud Functionと違い、Cloud Runはコンテナ上で動作するため、Dockerfileを用意します。

Cloud Functions 利用時には意識しなかったアプリケーションサーバの起動コマンドをDockerfileに記載します。

さらに今回のボットでは、コンテナをビルドする際に必要な Slack、GCPなどのAPIを利用するためのキーファイルや Pypiからパッケージをインストールするための requirements.txt を コピーする処理をDockerfileに追記します。

## Dockerfile

FROM python:3.7
...
COPY requirements.txt .  # 利用するパッケージ
COPY models models  # 分類モデルを含んだディレクトリ
COPY google-serviceaccount.json .  # キーファイル
...
RUN pip install -r requirements.txt  # 利用するパッケージのインストール

CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 main:app

2. Pythonコードの修正

Cloud Runでは単一のアプリケーションとして動作するようにエンドポイントを追加する必要があります。 しかし、追加するコードはわずかでエンドポイントとなるメソッドを用意し、もともとCloud Functionsで使っていたメソッドを呼び出すだけでした(簡単!)

## main.py

import flask
from flask import Flask, request
...

app = Flask(__name__)  # Cloud Run 用に追加

# Cloud Functions の時点で用意していたメソッド
def predict_from_http(request):
    """ requestから画像データを取得、分類結果を返す
    Args:
        request (flask.Request): HTTP request object.
    Returns:
    """
    ...

    return flask.make_response(
                flask.jsonify(predict_info),
                200)

# 以下 Cloud Run 用に追加したメソッド
@app.route('/', methods=['POST'])
def endpoint():
    return predict_from_http(request)

画像分類モデルについて

今回使ったモデルはシンプルなCNNモデルで構築しました。 学習データはこれまでWebカメラで撮影した冷蔵庫のデータ(600枚程度)を手動でラベリングしました。 (在庫が空の場合はempty,1つでもボトルが残っている場合はnot_empty) 学習の処理はGoogle Colaboratory上で行っています。

モデル構築、検証時の内容も面白い話があるのですが、 書き出すとボリュームが出てくるので今回は割愛します。 (反響あれば、次回以降で記事にするかも?!)

動作結果

入荷された場合の通知

ハマったところ

学習した分類モデルのファイルやPythonパッケージのインストール、キーファイルの配置が漏れていたために ビルド、コンテナの登録まで問題に気づけず、実行時にエラーとなっていました。 思い返すと、Cloud Functions利用時は配下のファイルを全部まとめてzipしアップロードしてくれていたので 意識していませんでした。

問題に気づいた後は、開発時にローカルのDocker環境でビルド、 実行して試すことでCloud Runへ登録する前に気付けくことができるようになりました。

そして実はCloud RunよりもGASのほうが悩まされ、時間がかかっていました。 主にキャッシュの動作確認や、変数のtypoによるundefined errorが多発してしまいました。

キャッシュについてはCloudRunとの連携部のエンドポイントとは別に キャッシュされている情報を確認する口を用意しました。(動作結果の画像を参照)

TypoについてはJSで書かず、TypeScriptで書けばtslintなどで事前に確認できたなと反省しています...

まとめ

  • Cloud RunとCloud Functions
    • Cloud Functions
      • さくっと始めやすい。
      • テストは難しい。
      • ランタイムに依存することで自由度は低め。
    • Cloud Run
      • Dockerの理解があると始めやすい。
      • ローカルで動作確認ができるため、検証が容易。
      • コンテナにビルドできればなんでも使える!

在庫判定ボットで快適なオフィス生活に!(していきたい)

負荷試験との向き合い方

こんにちは。SRE の近藤(@chaspy)です。

先日、より高い信頼性でサービスを提供するために、スタディサプリ小中高大のサービスの最後の砦であるデータベース、MongoDB のインスタンスクラスのスケールアップを行いました。また、スケールアップをするにあたり、負荷試験を行いました。

本記事では、データベースインスタンスのスケールアップの際に行なった負荷試験に対する考え方と、得た学びを紹介します。

なぜスケールアップするのか

サービスの急成長に伴い、アクセス数もデータ量も増加しています。数年前に「しばらくは大丈夫」と判断できるインスタンスクラスにスケールアップをしたデータベースも、高負荷時には性能が劣化してしまう問題に遭遇しました。

私たちの MongoDB は AWS 上に EC2 インスタンスとしてセルフホストしており、MongoDB Cloud Manager を使って運用の一部を自動化しています。可用性のために3台でクラスタを組んでおり、同じスペックの Primary / Secondary インスタンスと、読み書きされない待機用の Hidden Secondary で構成されています。

この構成では、一部の Read Query を Secondary に逃したとしても、(実際に、一部の Query は secondaryPrefered モードで動作しています)書き込みは Primary で行う必要があり、いつか I/O がボトルネックになってしまうことは明白です。

長期的には Data Restructuring Project による学習データの切り離し*1 による効果を期待していますが、それ以外に I/O の問題を解決するためには、シャーディングか、スケールアップしかありません。

水平方向のスケールであるシャーディングはシステムが今以上に複雑になってしまうことによるリスクがあること、また、負荷の軽減が急務であることから、今回は実績のあるスケールアップを行うことにしました。スケールアップする前は i3 という I/O に特化したインスタンスタイプでしたが、最近リリースされた、さらに I/O 性能が高い i3en タイプに変更することにしました。

しかし、本当にスケールアップで問題は解決するのでしょうか? Fact Based で考えるためには、負荷試験が必要です。

*1:学習データがデータ容量としても、書き込みの頻度としても高い

続きを読む

Monitor and fix app's performance with Android vitals

Overview

Hi! I am @padobariya again! working as a mobile engineer with Quipper (Japan office).

In this post, I will talk about Android vitals and how to monitor your app's technical performance and some of the possible fixes. It is a tool by Google to improve the stability and performance of Android apps. This article is useful for those who already published an Android app to Google Play or planning to release in the near future.

When an opted-in user runs your app, their device logs various metrics, including data about app stability, render time, and battery usage. The Play Console aggregates this data and displays it in the Android Vitals dashboard.

The dashboard highlights crash rate, ANR rate, excessive wakeups, and stuck wake locks, these are the core vitals developers should give attention to. This information helps you understand the performance of your app and alerts you when your app is exhibiting bad behaviors.

Why Android vital is so vital

It's no secret, we as users nobody wants to deal with an app that drains power, crashes randomly or freezes. As the following google stats suggests all these parameters have a direct impact on user reviews which is a leading indicator of customer satisfaction.

f:id:quipper-ja:20190902184211p:plain

Google analysis of play store review

  • Higher performance leads to higher app ratings, which in turn leads to a higher number of installs and user engagement.
  • Most of the users abandon an app after using them only once due to poor performance.
  • With 2.7 million apps available as of June 2019, users can easily find a replacement if yours disappoints. When consumers have options, the pressure is on you to deliver a quality product, and a substantial part of that promise is stability.
  • When ANR or crash rate increases the average time spent in-app decrease significantly.
  • App with lower crash rate appeared above in search than the app with higher crash rate, and also to be considered in Featured apps and Editors Choice.

As of now, Android vitals collects data and reports issues on the following broad categories, We will discuss the core android vitals category in detail and possible fix for them.

Stability

Stability refers to how often an app is crashing or displaying Application Not Responding (ANR) error messages.

ANR

An ANR occurs when an app’s UI thread is blocked for a long period. Users are given the option to quit or wait when an ANR occurs.

Android will display the ANR dialog for a particular application when it detects one of the following conditions:

  • No response to an input event (such as key press or screen touch events) within 5 seconds.
  • A BroadcastReceiver hasn't finished executing within 10 seconds.

How to avoid:

  • Avoid network or disc operation, long calculations on the main thread. To detect and ensure not doing this use StrictMode in debug build.
  • Any method that runs in the UI thread should do as little work as possible on that thread. In particular, activities should do as little as possible to set up in key life-cycle methods such as onCreate() and onResume(). Potentially long-running operations such as network or database operations, or computationally expensive calculations such as resizing bitmaps should be done in a worker thread (or in the case of database operations, via an asynchronous request).
  • As a general rule, broadcast receivers are allowed to run for up to 10 seconds before the system will consider them non-responsive and ANR the app. The specific constraint on BroadcastReceiver execution time emphasizes what broadcast receivers are meant to do: small, discrete amounts of work in the background such as saving a setting or registering a Notification. So as with other methods called in the UI thread, applications should avoid potentially long-running operations or calculations in a broadcast receiver. But instead of doing intensive tasks via worker threads, your application should start an IntentService if a potentially long-running action needs to be taken in response to an intent broadcast.
  • Another common issue with BroadcastReceiver objects occurs when they execute too frequently. Frequent background execution can reduce the amount of memory available to other apps. For more information about how to enable and disable BroadcastReceiver objects efficiently, see Manipulating Broadcast Receivers on Demand.
  • If your application has a time-consuming initial setup phase, consider showing a splash screen or rendering the main view as quickly as possible, indicate that loading is in progress and fill the information asynchronously.
  • Use performance tools such as Systrace and Traceview to determine bottlenecks in your app's responsiveness.

Crashes

Crashes, on the other hand, occur as a result of unhandled exceptions.

Startup Time

Android Vitals takes into consideration that it can take different time spans for an app to start up, depending on how long it has been since it was last opened. For instance, a cold start is when an app hasn’t been used in a while, where the activity goes from launched to running state. A slow cold start would be anything taking 5+ seconds.

A slow warm start (when the app has been used recently and is cached in memory) counts as 2+ seconds, and a slow hot start (when both app and activity are in memory) counts as 1.5 seconds.

How to reduce:

  • The chief reason for a poor cold-startup time is doing too much work in the onCreate() of your launcher activity. Check the onCreate of your class and move all the independent and unblocking but time-consuming methods to a separate thread.
  • Make sure you use lazy initializations wherever you can so that the onCreate() is not bombarded by unnecessary method or class initializations.
  • Use Local variables wherever possible.
  • Use Dagger2 for dependency injection: Dagger is a fully static, compile-time dependency injection framework for both Java and Android. It implements the dependency injection design pattern without the burden of writing the boilerplate and aims to address many of the development and performance issues that have plagued reflection-based solutions.
  • Using Handler with delay: Now this one might sound like a hack but it does wonders to your app startup performance in cases where there is some piece of lethargic code which you cannot afford to shift from the main thread, you can put in a Handler and use the postDelayed method, adding an appropriate delay (100 to 200ms should do just fine). This takes the weight off the onCreate and your app launches much quicker than before, with unnoticeable delay in the execution of the delayed method.
  • Restructure the splash screen: Image rendering is a time-consuming process and can mess up with your startup time. If your app uses a splash screen that displays your logo in an ImageView, one optimization you can do is to remove the ImageView from the xml and instead add the logo to the theme of your activity as window background.

Render Time

Ideally, an app’s speed of rendering should be under 16ms to hit 60 frames per second. Anything that takes longer than 16ms is considered slow.

Slow UI rendering results in frames being skipped, making for a clunky experience. If frames take longer than 700ms to render, they are considered to be frozen.

Here are some of the methods to identify slow rendering:

How to reduce:

  • Avoid Layout overdrawing: Overdraw describes how a pixel on the screen drawn multiple times during the same frame. There are still performance issues in rendering such as the most prone: over-rendering/over-drawing. In a multi-level attempted structure, if the invisible UI is also doing the drawing operation, some pixel area will be drawn multiple times, which will waste a lot of CPU and GPU resources.
  • Refresh rate and Frame loss: We all know Android system redraws the activity every 16ms, which means that your app must complete all the logical operations of the screen refresh within 16ms, So, that it can reach 16 frames/s. However, sometimes your program will have such a situation where your logical operation exceeds 16ms. At this time the frame loss will occur and a user sees the updated picture within 32ms. The more time it takes to execute the code in the main thread, the more the frame loss is. It can cause the UI to freeze or sometimes can also cause glitches.
  • Flatten the view hierarchy to reduce nesting: Along similar lines, the layout should not be deeply nested, in terms of parent and child layouts. If possible use ConstraintLayout to avoid duplicate nested layouts.
  • RecyclerView: notifyDataSetChanged(): Every item in your RecyclerView being rebound (and thus re-laid out and re-drawn) in one frame, make sure that you're not calling notifyDataSetChanged(), setAdapter(Adapter), or swapAdapter(Adapter, boolean) for small updates. Those methods signal that the entire list content has changed, and will show up in Systrace as RecyclerView FullInvalidate. Instead, use SortedList or DiffUtil to generate minimal updates when content changes or is added.
  • RecyclerView: Bind taking too long: Bind (that is, onBindViewHolder(VH, int)) should be very simple, and take much less than one millisecond for all but the most complex items. It simply should take POJO items from your adapter's internal item data, and call setters on views in the ViewHolder. If RV OnBindView is taking a long time, verify that you're doing minimal work in your bind code.

Battery Usage

Mainly there are following issues which can drain the battery by keeping the device from going into a low-power state.

  • Stuck wake-locks
  • Excessive wakeups
  • Excessive Wi-Fi scans (background)
  • Excessive network usage (background)

How to avoid:

  • To avoid wake-locks issues Android provides alternative APIs for almost every use-case that previously required a partial wake lock. One remaining use-case for partial wake locks is to ensure that a music app continues to play when the screen is off. If you are using wake locks to run tasks, consider the following alternatives:
  • To avoid excessive wakeups use wakeup alarms only if your app needs to perform a user-facing operation (such as posting a notification or alerting the user). For a list of AlarmManager best practices, see Scheduling Repeating Alarms.
  • Don’t use AlarmManager to schedule background tasks, especially repeating or network background tasks. Use JobScheduler or Firebase JobDispatcher to schedule background tasks
  • Avoid scanning Wi-Fi in the background, when an app performs Wi-Fi scans in the background, it wakes up the CPU, causing the rate of battery drain.
  • Avoid network usage in the background, when an app connects to the mobile network in the background, the app wakes up the CPU and turns on the radio. Doing so repeatedly can run down a device’s battery.
  • Use JobScheduler or WorkManager API when a job needs to run in the background.

Permission

Most apps require that users grant them certain app permissions in order to function properly. However, in some cases, users might not grant permissions.

Android vitals can help you gauge your users’ privacy preferences and engagement by informing you about the percentage of permission denials your app is receiving.

Thank you so much for reading my post. Please refer to the following resources if you want to know further.

References

here are some helpful resources