Studyplus Engineering Blog

スタディプラスの開発者が発信するブログ

Kotlin Flow+Roomで作るTimer&Stopwatch

こんにちは、モバイルクライアントグループの若宮(id:D_R_1009)です。 2月以降リモートワークで開発を続けております。会社近くのラーメン屋さんが恋しくなってきました。

今回は5月中旬にリリースした、AndroidアプリのTimer&Stopwatch改修について書きます。 Kotlin FlowとRoomを組み合わせ、複数のUIで同時にある値を表示する実装となったので、参考になれば幸いです。

Timer&Stopwatchの改修経緯

Studyplusスアプリには、学習している時間を計測するためのTimerとStopwatch機能があります。 Timerは予め時間を指定して経過時間を記録する機能、Stopwatchは計測開始から終了までの経過時間を記録する機能です。

今年の4月以降、このStopwatch機能について不具合報告が多くなっていました。 不具合を精査したところ、一部「ユーザーの利用環境変化」が理由と考えられるものが発生していることがわかりました。

利用状況の変化の詳細については、弊社の発表や過去のブログ記事をご参照ください。

info.studyplus.co.jp

tech.studyplus.co.jp

今回影響がありそうな点は、スタディプラスのユーザーがより自宅で学習する状況となったことです。 自宅で「動画教材を視聴しその時間を計測する」ユーザーが増加し、「教材の動画を視聴していると計測がストップする」という不具合が起こりやすくなったと推測できます。

開発を検討していた4月半ばごろは緊急事態宣言の話が取り沙汰されていました。 そのためこの問題が起こりやすい状況が続くことも予想され、優先度を高めて対応することとしました。

既存実装について

まず、不具合が報告された既存実装から簡単に説明します。

これまでの実装では、ForegroundService 上で時間計測用のTimerやCountDownTimerを行い、その計測結果をRxBusやIntentを用いてUIに送信する実装となっていました。 これはサービスとスレッドのどちらを選択するかを参考に、バックグラウンドでAndroid OSの許す限り計測を行うという実装方針のもとに選択したものです。 既存実装の設計時には「(物理媒体の)教材を学習中、スマートフォンで時間を計測する」ようなケースを想定していたため、この実装でも問題ないだろうと考えていました。

ただこの実装ではユーザーが例えば動画のストリーミングアプリを立ち上げた時、端末のスペックによってはStudyplusアプリがメモリから破棄されてしまいます。 この破棄時に状態を保持するためにはには SavedInstance への保存処理、そして SavedInstance からの復元処理を記述する必要があります。 しかしながら、既存実装ではこの対応が行えていませんでした。 またServiceの SavedInstance 対応は調査から始める必要があり、導入コストが高いと見積もられ、すぐに着手できる状況ではありませんでした。

不具合の原因まとめ

簡単にまとめると、下記のようになります。

  1. 既存実装は ForegroundService 上で計測処理を実施していた
  2. Activity/Fragmentは Service から送られてくる計測状態を表示していた
  3. メモリ破棄時の SavedInstance が未実装であった
  4. 外出自粛や各種休校に伴い動画視聴と並行して利用されるケースが増加した(と考えられる)

Kotlin Flow+Roomによる計測機能

新たにTimer&Stopwatchの設計をするにあたり2つの方針を立てました。

  1. 不揮発領域に計測状態を保存し、アプリの生存期間に関わりなく時間の計測ができるようにする
    1. アプリ破棄時への対応
    2. アプリ再起動時への対応
  2. Activity/FragmentとServiceを同列に扱い、どちらも"UI"としての役割とする
    1. 計測をUI層ではなくModel層で実施する
    2. DIが可能にすることでテスタビリティを向上する

なお、簡単な実装を下記のリポジトリで公開しています。 動作を見つつコードを確認したい場合は、ご活用ください。

github.com

Roomの設計

以前のエントリで紹介しているように、スタディプラスのAndroidアプリではRoomを採用しています。

tech.studyplus.co.jp

このため、今回の機能開発においてもRoomを利用することとしました。 都合の良いことにRoom 2.2よりKotlin Flowをサポートしているため、今回の用件にマッチした次第です。

medium.com

Roomに保存するテーブルは、下記のようにします。 なお説明を簡単にするため、Stopwatch機能のみの実装としています。 実際にはTimerや計測中の教材に関連する情報などのデータも含む実装です。

@Entity(tableName = "measurement")
data class MeasurementEntity(
    @PrimaryKey
    @ColumnInfo(name = "entity_id")
    val entityId: Int = 0,
    val state: MeasurementState,
    @ColumnInfo(name = "start_date_time")
    val startDateTime: String, // 2020-04-20'T'08:20:00+09:00
    @ColumnInfo(name = "elapsed_sec")
    val elapsedSec: Long = 0L
) {
    companion object {
        val INIT_OBJECT = MeasurementEntity(
            state = MeasurementState.INIT,
            startDateTime = ""
        )
    }
}

MeasurementState は次の通りです。 Stopwatchはユーザーがボタンを操作して状態が変化する仕組みとなるため、シンプルにRUNとSTOPの2つの状態だけを想定します。

enum class MeasurementState {
    INIT,
    STOPWATCH_RUN,
    STOPWATCH_STOP
}

これでStopwatchの状態、計測開始時刻、前回までの計測で加算された時間を保存することができるようになります。 続いて、経過した時間について開発を進めていきます。

Kotlin Flowによる計測

ここからはDaoの定義を行い、ブログタイトルの通りKotlin Flowでデータを取得します。 Flowを使うことで「rowを更新する」処理と「rowが更新された」処理を分離し、特に「rowが更新された」処理をリアクティブな実装とすることができます。

RoomはSELECTメソッドの返り値を Flow<MeasurementEntity?> とすることで、rowが存在しない時に null となるFlowを作ることができます。 後述のRepositoryでnon-nullに変換していますが、 Flow<List<MeasurementEntity>> とすることでnon-nullにすることもできるので、お好みで選択してください。

@Dao
interface MeasurementDao {

    @Query("SELECT * FROM measurement WHERE entity_id=0")
    fun findFlow(): Flow<MeasurementEntity?>

    @Query("SELECT * FROM measurement WHERE entity_id=0")
    suspend fun find(): MeasurementEntity?

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(entity: MeasurementEntity)
}

最後にFlowで受け取った MeasurementEntity に合わせて、描画更新用のイベント clock を作成します。 サンプルでは1つのActivityでしか描画を受け取っていませんが、同期イベントをRepositoryから作成することで、複数のUIで同時に描画を更新することができます。

transformLatest を利用することで MeasurementEntity が変更された時に、rowの更新があったとき clock が更新されるようになります。 このため clock 内で while(true) のループを回す実装を採用しています。

@Singleton
class StopwatchRepository @Inject constructor(private val dao: MeasurementDao) {

    val entity = dao.findFlow().map {
        it ?: MeasurementEntity.INIT_OBJECT
    }

    private val clock = entity.transformLatest {
        emit(OffsetDateTime.now())
        when (it.state) {
            MeasurementState.STOPWATCH_RUN -> {
                while (true) {
                    delay(INTERVAL_MILLI)
                    emit(OffsetDateTime.now())
                }
            }
            else -> {
                // nop
            }
        }
    }

    companion object {
        private const val INTERVAL_MILLI = 500L
    }
}

計測時間の表示

ここまで計測のベースを作成したので、リアルタイムで更新される描画を考えていきます。 経過時間は現在時刻と計測開始時刻の差と前回までの計測で加算された時間の合計と考えることができます。

@Singleton
class StopwatchRepository @Inject constructor(private val dao: MeasurementDao) {

    val elapsedTime = combine(entity, clock) { entity, event ->
        when (entity.state) {
            MeasurementState.INIT -> {
                0L
            }
            MeasurementState.STOPWATCH_RUN -> {
                entity.secUntil(event) + entity.elapsedSec
            }
            MeasurementState.STOPWATCH_STOP -> {
                entity.elapsedSec
            }
        }
    }
}

ViewModelではこの値を表示させるため、LongからStringへの変換を行います。 どのようなStringへ変換するかはUI依存のため、今回はViewModelで変換するのが良さそうと判断しています。

class MainViewModel @Inject constructor(private val repository: StopwatchRepository) : ViewModel() {

    val elapsedTime = repository.elapsedTime.map { formatMeasureTime(it) }.asLiveData()
}

fun formatMeasureTime(duration: Long): String {
    val hour = duration / 3600L
    val minute = duration % 3600L / 60L
    val second = duration % 60L

    return String.format(Locale.US, "%02d:%02d:%02d", hour, minute, second)
}

あとは各ボタンの有効状態を repository.entityMeasurementState を基に調整することで、Stopwatch機能が完成です。 現実装では StopwatchRepository に作成した操作用メソッドをViewModelから操作することで、Roomの状態を更新しています。

不具合の改修状況

新たな実装ではRoomに保存されている状態を見ればいつでも計算を行うことができるようになりました。 このため、既存の Service のライフサイクルに依存することにより発生した問題を解消しています。

また、「Stopwatchを止めずに端末の電源をOFFにした」ケースにも対応することができました。 サンプルのコードを動かしている場合は、ぜひ1度お試しください。*1

時間計測は考慮することが多く大変なのですが、基本的なユースケースにシンプルなコードで対応できたように思います。 なお端末の時間を変更した場合などのエッジケースが存在するのですが、そちらの考慮は割愛させていただきます。

終わりに

5月半ばよりリリースした、RoomとKotlin Flowを組み合わせて時間計測機能を追加する実装を紹介しました。 現時点まで大きな不具合報告もなく、市場で動作しているようで安心しています。

Roomから簡単にKotlin Flowを取得することができるので、Roomを中心とした設計にするとFlowが扱いやすいなと感じました。 ちょうどRxJavaをアプリから削除し終わったので、今後もFlowを活用していきます。

最後までお読みいただきありがとうございました!

*1:もしもビルドがめんどくさければ、是非Studyplusをダウンロードしてみてください!

Studyplus iOS版におけるアプリ内課金時のUI制御

こんにちは、モバイルクライアントグループの明渡です。

先日のGoogle Play定期購入でプランを切り替えるモードの話でも記載の通り、スタディプラスでは今年3月に有料会員サービス Studyplus Pro をiOS/Android両OSでリリースしました。

info.studyplus.co.jp

iOS版でのアプリ内課金の実装自体は、用意されているフレームワークにてアプリ開発者が考慮しなければいけない事項がAndroidより少ないです。そして、Appleの豊富なドキュメント*1や、先人たちが多々残してくれている豊富な知見*2に助けられスムーズに実装を進められました。

そんな中で今回は、アプリ内課金実装そのものと比べれば些事ながらも結果的にリリース直前まで苦しんだ、アプリ内課金操作を行なった際のUI制御についてお話しします。

iOSのアプリ内課金実装の中でもアプリ開発者の裁量に委ねられていてアプリごとに最適解が異なりそうなテーマになりますので、ほんの一例としてご覧いただければ幸いです。

やりたかったこと

  • アプリ内課金を試行中、アプリ画面の操作はできないようにする

    • 購入操作を行うためのApp Storeアラートが表示されるまで数秒かかることがあり、何も制御しないとアプリ画面の操作ができてしまう
  • アプリ内課金に関する操作が終了後、アプリ画面を操作できるようになる

  • アプリ内課金の購入・復元完了後に完了した旨を伝える画面を表示する

実装

最終的に落ち着いた実装が以下の通りになります。

前提

  • Appleのドキュメントからダウンロードできるサンプルコード*3を基に実装したので、フレームワークはStoreKitをそのまま利用

    • App Storeのオブザーバにてストアからのコールバック処理時、画面側へデリゲートによりアプリ内課金のトランザクション処理ステータスを伝えて制御
  • UI制御が絡まない実装の記述は割愛

実行環境

  • Xcode 11.5
  • Swift 5.2.4

App Storeのオブザーバ

protocol AppStoreObserverDelegate: class {
    func storeObserverPurchaseSucceed()
    func storeObserverDidReceiveMessage(message: String)
    func storeObserverDidCancelled()
}
final class AppStoreObserver: NSObject {
    static let shared = AppStoreObserver()
    weak var delegate: AppStoreObserverDelegate?
}

extension AppStoreObserver: SKPaymentTransactionObserver {
    
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        for transaction in transactions {
            switch transaction.transactionState {
            case .purchasing:
                break
            case .deferred:
                DispatchQueue.main.async { [weak self] in
                    self?.delegate?.storeObserverDidCancelled()
                }
            case .purchased:
                handlePurchased(transaction, success: {
                    // サーバーでレシート検証して会員ステータス更新が成功した際、購入完了時の処理を行う
                    DispatchQueue.main.async { [weak self] in
                        self?.delegate?.storeObserverPurchaseSucceed()
                    }
                }, failure: { [weak self] message in
                    DispatchQueue.main.async { [weak self] in
                        self?.delegate?.storeObserverDidReceiveMessage(message: message)
                    }
                })
            case .failed:
                if let error = transaction.error as? SKError, error.code != .paymentCancelled {
                    DispatchQueue.main.async { [weak self] in
                        self?.delegate?.storeObserverDidReceiveMessage(message: error.localizedDescription)
                    }
                } else {
                    DispatchQueue.main.async { [weak self] in
                        self?.delegate?.storeObserverDidCancelled()
                    }
                }
                handleFailed(transaction)

            case .restored:
                handleRestored(transaction, success: {
                    // サーバーでレシート検証して会員ステータス更新が成功した際、購入完了時の処理を行う
                    DispatchQueue.main.async { [weak self] in
                        self?.delegate?.storeObserverPurchaseSucceed()
                    }
                }, failure: { [weak self] message in
                    DispatchQueue.main.async { [weak self] message in
                        self?.delegate?.storeObserverDidReceiveMessage(message: error.localizedDescription)
                    }
                })
            @unknown default:
                let message = "Unknown payment transaction case"
                #if DEBUG
                fatalError(message)
                #endif
            }
        }
    }
}

画面

final class ViewController: UIViewController {
    // UIActivityIndicatorViewを含む画面全体を覆うカスタムビュー
    // 購入手続き・復元処理開始時に表示しておく
    private lazy var loadingView: LoadingMaskView = {
        return LoadingMaskView(frame: view.frame)
    }()
}

extension ViewController: AppStoreObserverDelegate {
    func storeObserverPurchaseSucceed() {
        DispatchQueue.main.async { [weak self] in
            self?.loadingView.dismiss()
            // Studyplus Pro 登録完了画面を表示
        }
    }
    
    func storeObserverDidReceiveMessage(message: String) {
        DispatchQueue.main.async { [weak self] in
            self?.loadingView.dismiss()
            // messageのアラート表示
        }
    }
    
    func storeObserverDidCancelled() {
        DispatchQueue.main.async { [weak self] in
            self?.loadingView.dismiss()
        }
    }
}

苦労したこと

ユーザーへアプリから伝えるべき情報とそうでないものの切り分け

購入・復元処理が完了したあと、App Storeのオブザーバ内で弊社サーバーへレシート検証リクエストも立て続けに行なっております。 SKPaymentTransactionによって購入〜課金ステータス更新までを1つトランザクションとして扱える恩恵にあやかるためですね。

苦しんだ要因としては、その後続処理側のエラー時の挙動も並行してきちんと整理せず実装を進めているうちに、無駄にこんがらがってしまっただけという側面が強いです...

App Storeからのレスポンスについては、以下のような対応に落ち着きました。

  • SKPaymentTransactionState
    • .purchased または .restored
      • レシート検証を進める
    • .deferred
      • Ask to Buyによる購入承認リクエストが送信されたがまだ購入が確定していないので、レシート検証は行わずアプリの利用を再開できるようにする
    • .failed のうち、ユーザーが自らキャンセル(SKError.paymentCancelled)
      • ユーザーの意志で購入が行われなかったので、レシート検証は行わずアプリの利用を再開できるようにする
    • .failed のうち、上記以外
      • ユーザーの意志に反して購入・復元完了しなかった恐れがあるため、エラーの内容を表示しつつアプリの利用を再開できるようにする

App Storeでの操作完了後、アプリ側のローディング表示をきちんと終了する

上記のソースコードなのですが、DispatchQueue.main.async のクロージャがくどく感じません?

オブザーバ・画面共にどちらかのクロージャを欠くと、ローディング表示を終了できず永遠にローディング表示のままとなる事象が一定確率で発生するため必須だったのです...

一定確率で発生するのがミソで、リリース版がFIXする直前まで見落として開発を進めてしまい、発覚した際には少々青ざめながら検証して現在の実装に落ち着きました。

自アプリ以外の要素が絡む処理はとりわけ、メインスレッドで実行したい処理を DispatchQueue.main.async のクロージャへ明記しないといけないのだなと理解しました。

さいごに

今回はテーマに関わるソースコードを抜粋しながら記事を書き起こしたのですが、元のソースコードをもう少し整理してリファクタしたい欲に駆られました...

近いうちにアプリ内課金前提の動作確認の伴う機能開発と併せて、今回の記事を見直してリファクタリングに臨もうかなと思います。

少人数で複数のマイクロサービスの開発を行うための弊社の開発事情

こんにちは、サーバーサイドエンジニアの山田です。

昨年の11月に入社して以降、Studyplusアプリのバックエンドとそれに付随するいくつかのマイクロサービスの開発に関わってきました。今回はその中で感じたサーバーサイドチームの開発環境のいいところや改善したい点をいくつか紹介したいと思います。

背景

わたしが所属するStudyplus事業部のサーバーサイドチームでは、10ほどのマイクロサービスの開発を4人のサーバーサイドエンジニアが担当しています。その中で一番改修が多いのはStudyplusアプリのバックエンドとなるRailsアプリケーションになりますが、それ以外のサービスもたまに改修を入れていくことになります。わたしの場合は入社して約半年で7個ほどのサービスの改修に関わりました。

それらのサービス開発に関わった結果、新しく入ったメンバーが早く立ち上がりやすい環境だと感じたので、そのあたりの観点を中心にチームの開発環境について紹介していきます。

ローカル環境編

docker-composeによる環境構築

どのサービスもdocker-composeでローカル環境を構築できるようになっており、環境構築に時間を取られることが少ないです。これによって、あるサービスにちょっとした改修を入れたいだけなのに環境構築に時間が取られるといったことを避けることができています。

開発環境のDBに接続して開発

ローカルでアプリケーションを動かす時に時間を取られやすい作業にデータの準備があると思います。 弊チームでは以下のように開発者個人のローカルのアプリケーションから開発環境のDBに接続して開発を行う方法をとっています。

f:id:yshunske:20200608113144p:plain

例えばRailsの場合はseeds.rbで初期データを準備する方法がありますが、seedのメンテナンスが手間で中途半端になってしまうことも多いと思います。

この方法のメリットとして以下のような点が挙げられます。

  • seedのメンテが不要
  • seedでカバーできない(or 準備コストが高い)ような特定の条件を確認するためのデータのバリエーションについても初めてローカルの環境を起動してすぐに使える
  • ローカル環境での確認後に開発環境で確認するといった場合に2度同じデータを作成する手間が省ける

一方で以下のようなデメリットもありますが、現状は問題にはなっておらずデメリットよりメリットの方が大きいと感じています。

  • DBのスキーマ変更を行う場合に別の開発者と競合してしまう可能性がある
  • みなが使うDBなので試しにスキーマを変更したいという場合にやり辛い

開発環境DBでやりづらい開発の場合は以下のような方法で回避することが多いです。

  • ローカル環境ではテストコード上での確認のみを行い、残りは開発環境へのdeploy後に確認する
  • 開発環境のDBをコピーしてそこに接続して開発

CI/CD編

テストコードとCircleCI

サーバーサイドチームが担当している全てのリポジトリにテストコードがあり、プルリクエストはCircleCI上のテストと二人以上のレビューが通らないとmasterブランチへマージできないようにしています。 これにより初めて修正を行うリポジトリでもテストコードで動作を確認しつつ自信を持って改修をしていくことができています。

デプロイ方法の統一

デプロイは以下のようにslackからデプロイする方法で統一されています。そのため個別の手順を覚えたりする手間が省けています。

f:id:yshunske:20200608113447p:plain

アプリケーション編

開発言語とフレームワーク

バックエンドは基本的にRubyとRailsが使われています(一部はGoなど別の言語を使用)。言語とフレームワークが揃っていることで学習コストを抑えて開発を行えています。 詳しくは後述しますが、フロントエンドはこのあたりに課題を抱えています。

Railsアプリケーションのレイヤー

Railsアプリケーション上で他のマイクロサービスと連携する処理は gateways というレイヤーを設けてそこに集約しています。

下のようなイメージです。

# Railsの app以下のディレクトリ
.
├── assets
├── controllers
├── gateways <- ここに他のマイクロサービスと連携する処理を集約している
├── helpers
・
・
・

他サービスとの連携は処理を追いづらいことが多いですが、異なるRailsアプリでも同じ構成をとることで処理が追いやすくなっています。

インフラ編

AnsibleとTerraformによるIaC

弊社のインフラの多くは、AnsibleとTerraformでコード化して管理しています。新しい仕組みの導入や大きな変更はSREチームが担当しますが、アプリケーションの変更に伴うちょっとした修正はサーバーサイドチームのメンバーも修正を行います。その際、AnsibleやTerraformのコードを修正してSREチームのレビューを受けてから反映しています。

これにより以下のようなメリットがあると感じています。

  • アプリケーションを開発するサーバーサイドのメンバーもインフラ構成や設定を把握しやすくなる
  • 作業がSREチームのみに集中することを避けられている
  • SREチームのコードレビューを受けることで一定の質を保って修正が行える

改善したいところ

ここまでは良いと感じている点を挙げてきましたが、改善したい点もいくつか紹介します。

フロントエンドの技術スタックが統一されていない

弊社の体制上、Webのフロントエンドが得意なエンジニアは、スマホアプリではなくWebのサービスであるStudyplus for Schoolを開発している部署に集まっています。そのためStudyplus事業部が担当するWeb画面を持ったサービスはサーバーサイドチームのメンバーがメンテナンスしています。

そのような体制ではありますが、使っているサービスによってJavaScriptのフレームワークは以下のように各種フレームワークが使われています。

  • Vue.js
  • React
  • AngularJS(Angularではない)
  • jQuery

これらをサーバーサイドエンジニアがメンテナンスしているためコストが上がってしまっており、ある程度は技術スタックを揃えていく必要があると感じています。

Ruby、Railsのバージョン

現状は各サービスで使っているRuby、Railsのバージョンがそれぞれ以下となっており、最新のバージョンに追従できていない状態です。

  • Ruby: 2.5系 or 2.6系
  • Rails: 5.2系

これがすぐに問題となるわけではありませんが、最新のバージョンからの差が大きくなるほどバージョンを上げるコストが上がっていきます。また、最新の機能や改善された機能の恩恵を受けられないというデメリットもあり、近いうちにバージョンを上げていきたいと考えています。

また、バージョンを上げていく作業は今後も発生し続けるため、なるべくコストを抑えつつ新バージョンに追従していく仕組みや体制を考えていきたいです。

まとめ

サーバーサイドチームが担当するサービスで共通することを中心に開発事情を紹介しました。個人的には複数のサービスで技術スタックや開発のフローが統一されている点が現状のチーム体制を考えると効率的だと感じて気に入っている点です。

新しいサービスにJOINして開発を開始するまでに必要な要素は、ドメイン知識など多岐にわたると思います。その中でもサービスを横断して統一されていることが多いとその一部を把握する時間をショートカットできるので立ち上がりが早くなると思います。

一方、そのマイクロサービスに適した個別のやり方を選択することも必要なので、開発の体制とバランスを取りながらより価値を発揮しやすい環境にしていきたいところです。

スタディプラスメンバーのリモートワーク作業環境事情

スタディプラスメンバーの作業環境事情

こんにちは。ForSchool事業部の田口です。
COVID-19の影響で在宅勤務になってしばらく経ちましたが、みなさんいかがおすごしでしょうか。
スタディプラスでは以前からリモートワークが可能でしたが、今回の騒動で原則在宅勤務となりました。
そして、会社の好意により5月の給与振込日に在宅勤務手当が振り込まれました。在宅勤務が続く中でこのような手当がいただけるのはありがたいですね。
元々リモートワークを行っていたメンバーならまだしも、今回の騒動でほとんど経験のない在宅勤務を行う必要が出てきたという方も多いと思うので、今回は在宅勤務に馴染みのある職種以外の方も含めてスタディプラスのいろんな職種の方の在宅作業環境を紹介しようと思います。
この記事では、作業環境を紹介してくれるみなさんに作業環境の写真を提供していただき、各メンバーに

  • (もしあれば)こだわっているところや商品情報など
  • 今の在宅勤務環境の良い点・悪い点
  • 在宅勤務手当で買いたいもの

の各項目に答えていただいた内容を紹介します。

目次

  • ForSchool事業部エンジニア: 田口
  • ForSchool事業部エンジニア: 石上さん
  • ForSchool事業部エンジニア: 冨山さん
  • ForSchool事業部デザイナー: 秋間さん
  • ForSchool事業部長: 宮坂さん
  • Studyplus事業部iOSエンジニア: 明渡さん
  • Studyplus事業部デザイナー: 小松さん
  • 総合広告事業部アカウントプランナー: 伊藤さん
  • ポルト事業部デザイナー: 竹内さん

ForSchool事業部エンジニア: 田口

というわけでまずこの記事を書いている自分の作業環境から紹介させていただきます。

写真

f:id:tagucch:20200601164736j:plain f:id:tagucch:20200601164801j:plain f:id:tagucch:20200601164814j:plain

こだわっているところや商品情報など

  • L字のデスクを使っていて、正面(Lの長い辺)にメインの作業環境、右側の手前(Lの短い辺の部分)にWindowsマシン用のモニターやキーボード、Switchなどを置いています(PC本体は足元にあります)。モニターの左に立ててあるMacBook Proは私物です。
  • 4Kディスプレイが少なくとも1枚はほしいので、LGの27インチ4Kモニターをメインディスプレイにしていますが、もう1枚欲しくなってきました。ゲーム用のモニターは壊してしまったのでこのモニターでゲームもやるのですが、HDMIケーブルをつけ直すのが面倒なのでHDMIセレクターの導入を検討しています。
  • キーボードはPFUのHHKB Professional2 墨/無刻印をかれこれ3年くらい利用しています。気分転換で他のキーボードを使うこともあって、MiSTEL BAROCCOのMD650Lを使ったりErgoDoxを使ったりもします。ただこれらのセパレート型のキーボードは設置にちょっと手間がかかるのでなんだかんだHHKBを使っていることが多いです。やはり静電容量無接点方式は最高ですね。
  • 第二世代のApple Pencilが使えるiPad Pro11インチ(第二世代)を持っていて、macOS CatalinaにアップデートしてSidecarが利用できるようになったのでもう1枚のディスプレイとして使おうと思っていました。が、なんだかんだでYouTubeを垂れ流したり音楽をかけたりするのに便利なためわりと単独で使ってます。
  • マイクは前に誕生日に贈ってもらったSonyのECM-PCV80Uをマイクアームにつけて利用しています。外付けのマイクでUSB接続もでき、値段もお手頃なためおすすめです。(ポップガードはマイクアームについていたものです)

今の在宅勤務環境の良い点・悪い点

良い点

  • デスク自体は大きく奥行きもあるので、わりと広々としていてNintendo TOKYOで買ったマグカップやはてなボックスなどが置けます。
  • 飲み物やお菓子をすぐに取りにいけるのでとにかく楽です。面倒くさがりなので楽な方向に倒れ続けるのはとても素晴らしいです。
  • ANKERのスピーカーとiPad Proがあるので好きな音を流せるのもいいところです。

悪い点

  • 今の椅子も悪くはないんですが、オフィスで使っていたオカムラのBaronが恋しいです。
  • とにかく運動不足になりがちです。やはりスタンディングデスクやステッパーが必要なのかもしれません。
  • 大きめの通りに面しているので車の音がちょびっと気になったりします。

在宅勤務手当で買いたいもの

  • この前発売されたREALFORCE for Mac PFU Limited Editionがとてもほしいです。
  • 持っているイヤホンは前に買った5000円程度のもので特に不自由はしてないのですが、せっかくなのでいいイヤホンを買いたいです。友人に教えてもらったSHUREのSE215が気になっています。

ForSchool事業部エンジニア: 石上さん

続いて、ForSchool事業部エンジニアの石上さんの作業環境を紹介します。

写真

f:id:tagucch:20200601172314j:plain

焦ったときはI'm very busy 猫を見て落ち着くようにしています。

とのことです。

今の在宅勤務環境の良い点・悪い点

良い点

  • コンパクト
  • 集中できる
  • 好きな姿勢で仕事ができる
  • 行き詰まったときに変な声を出せる

悪い点

  • ゆくゆくはもうちょっと机を広げたい

在宅勤務手当で買いたいもの

  • 美味しいせんべいをたくさん買いたいと思います。

ひとこと

石上さんの作業環境はシンプルでコンパクトにまとまっておりきれいですね。I'm very busy猫がかわいい。
画面はMacBook Proの1枚のみですが目線の高さまでスタンドを使って上げているのがいいですね。WebカメラもMacBook Pro内蔵のものが使えてよさそうです。

ForSchool事業部エンジニア: 冨山さん

次はForSchool事業部エンジニアチームのリーダー冨山さんです。

写真

f:id:tagucch:20200601165211j:plain f:id:tagucch:20200601165242j:plain f:id:tagucch:20200601165301j:plain

(もしあれば)こだわっているところや商品情報など

  • 電動昇降デスク/flexispot E3
    • 仕事中にボタン一つで高さを調整できるので立ったり座ったりできて腰痛に悩まされないため重宝しています!立っているときは足元のメディスンボールの上に立って体幹を鍛え上げてます!
  • キーボード(左)/planck rev6
    • ortholinear配列のキーボードで打鍵のときに手をあまり動かさなくて良いのでコーディングするときに重宝しています!
  • キーボード(右)/MISTEL BAROCCO MD650L
    • こちらはロープロファイルのメカニカルキーボードでwindowsマシンに繋いであります.クライアントアプリケーションをwindows環境でデバッグ作業する時くらいにしか使いませんが色合いが好きで気に入ってます!!

今の在宅勤務環境の良い点・悪い点

良い点

  • 空調を自分で好きなように調整できること
  • 電動昇降デスクで自分が一番しっくりくる体勢で仕事ができるので集中力が上がる
  • 音楽をスピーカーでかけても怒られないこと

悪い点

  • MBPをクラムシェルモードで運用したいんですが,ウェブカメラがないので会議の時MBPを机の上に広げておく必要があって机の上が密
  • 話し相手がいないので雑談やちょっとした会話ができずに精神的負荷がかかる
  • 椅子がAmazonで買った安いプラスチック椅子なので座り心地が悪い

在宅勤務手当で買いたいもの

  • 椅子(GTRacingがほしいです!!!)
    • 座り心地が....
  • ウェブカメラ
    • クラムシェルモード...
  • カーテン
    • 背中に窓があるため,会議のときに逆光になってラスボスっぽくなってしまうので
    • 男の夢

ひとこと

冨山さんはなんといってもスタンディングデスクがあるのがポイントですね。電動昇降なので気分や体調によって高さを自由に決められるのは運動不足になりがちなリモートワークでは重要ですよね。
キーボードもこだわりが見られますね。自分もMD650Lを持っているのですが、左右を分離させることもできますし冨山さんの写真のようにくっつけることもできます。

ForSchool事業部デザイナー: 秋間さん

次はForSchool事業部デザイナーの秋間さんです。

写真

f:id:tagucch:20200601165402j:plain f:id:tagucch:20200601165421j:plain

(もしあれば)こだわっているところや商品情報など

  • デスクの横に花を必ず飾っています。FLOWERというお花便のサービスを使って月二回季節の花が届くので、デスク周りが華やかになって気持ちも落ち着きます。
  • リモート勤務になってすぐに、椅子をバランスボールに切り替えました。VLUVのバランスボールは、転がりにくくて、安定感もあってとても気に入っています。姿勢も気にすることができるので、とてもありがたい存在です。
  • iPad proで音楽を流したり、オンライン会の視聴をしたり、ほどよく手軽に使えて、自宅勤務になってからよく使うようになりました。
  • デスクとデスク横のチェストは無印良品です。
  • スピーカーは、BOSEのSoundLink Miniを愛用しています。
  • MacBook Proを置くためのスタンドは、Rain DesignのmStandを使っています。
  • イヤホンは、AirPods Pro。今年買ってよかったランキングの1位かもしれません。

今の在宅勤務環境の良い点・悪い点

良い点

  • 好きな音楽を聴きながら作業できたりリラックスしながら作業ができるところ
  • 家の環境をきれいに保てるので、精神衛生上良いです。(仕事してるときは土日くらいしか片せなかった)

悪い点

  • 部屋の壁紙がちょっとダサいので賃貸でも使える壁紙を貼りたい・・・
  • ライトが暖色系しかなく、夜になると集中しにくくなってしまう
  • 部屋の冷暖房環境があまり整っていないので、夏は暑さに耐えられるのか不安。

在宅勤務手当で買いたいもの

  • philips hueの電球を買ったものの、デスクライトの電球ソケットと合わず(事前に調べてなかった)使っていないので、新しく照明器具を買おうと思っています。
  • キーボードが有線しか自宅にないので、Bluetoothのものを買いたいです。
  • 欲をいえば、電動のスタンディングデスクが欲しいです。

ひとこと

秋間さんの作業環境はバランスボールが目を引きます。そして花が机にあると見た目が華やかになりますね。
全体的にコンパクトにまとまっている印象ですが、物が少ないので窮屈な印象はなく、花があって素敵な環境だと思います。

ForSchool事業部長: 宮坂さん

次はForSchool事業部の部長、宮坂さんです。

写真

f:id:tagucch:20200601165501j:plain

(もしあれば)こだわっているところや商品情報など

  • キーボード / REALFORCE for Mac
    • 出張や会議も多いのでラップトップのキーボードを使っていたのですが、REALFORCEを買いました。新卒で就職した会社(リブセンス)では、社員全員がREALFORCEを会社に用意してもらっていたので久しぶりのREALFORCE(当時はfor Macがなかったのでこれも感動)
  • 椅子 / イトーキ オフィスチェア サリダ YL6
    • アーロンチェアは流石に高いので(当時のリブセンスは社員全員がアーロンチェアでした汗)、ロッキング機構がある椅子でリーズナブルなものということでこれを選びました。 購入してからは疲労感も消えたのでとても良かったです。
  • Google Nest Wifi
    • 以前から自宅でリモートワークしていた際にzoomの接続が不安定なときがあったので、回線の改善のために購入しました。購入してからは一度もzoomが不安定になることはありませんでした。

今の在宅勤務環境の良い点・悪い点

良い点

  • 通勤時間がないのでそれだけ長く働ける点。コロナ禍でオンラインで指導せざるをえない学習塾のご支援が増えたことで、仕事量はどうしても増えてしまうのでこれは良かったです。

悪い点

  • もともと運動不足でしたから、通勤さえもなくなってしまうと、本当に動かなくなってしまったので困りました。しかも椅子に座りっぱなしなので、上記の椅子が到着するまでは、疲労も酷いものでした。

在宅勤務手当で買いたいもの

  • グリーンバック
    • 私のMacBook12inchはグリーンバックがないとバーチャル背景を利用できないためこれは欲しいです。
  • SwitchBot(スマートカーテン)
    • カーテンを閉めないと逆光になってしまうので、オンライン会議のときはいつもカーテンを締めているのですが、せっかくGoogle Homeがあるので、カーテンを音声で締めたいです。

ひとこと

宮坂さんの作業環境もシンプルにまとまっていますね。壁に飾られた植物や照明がおしゃれです。
そしてREALFORCE for Macが目立ちますね。机上が黒でまとまっておりかっこいい。

Studyplus事業部iOSエンジニア: 明渡さん

次は事業部が移り、Studyplus事業部iOSエンジニアの明渡さんの作業環境です。

写真

f:id:tagucch:20200601165757j:plain

(もしあれば)こだわっているところや商品情報など

  • モニタはDMM.makeの55インチのもの
    • 購入当初に電源の入らない初期不良品を引き当て、交換のために再梱包する手間がつらかったので無闇に人におすすめできない気持ち。メーカーに対応はちゃんとしてもらえました
    • 自分はモニタのサイズこの半分くらいで良い派で、家族が大きいモニター使ってみたいということで昨年購入してみたもの
  • 作業部屋が2階、リビングが1階なので手軽に水分補給できるよう電気ポット+急須を常備
    • オフィスにも急須を持ち込んでいるので平常運転

今の在宅勤務環境の良い点・悪い点

良い点

  • 今年2月にお迎えしたおねこさまが床へおもむろに転がっていることがあり、たいへん可愛い。癒し
  • 夕飯を摂る時間が不規則だったのだが、3食とも概ね決まった時間に食べるようになった

悪い点

  • 通勤が多少の運動になっていたようで、意識して体を動かさないとぶくぶく太る
  • 通勤時間を考慮しなくて良いこともあり、うっかり所定時間オーバーして働く頻度が高くなった

在宅勤務手当で買いたいもの

  • 4月時点で手当について発表があり、入ってくるのを当てにして+αでPanasonicの目もとエステなる充電式ホットアイマスク的なものを買いました
    • 昼休憩や寝る直前に使うとすっきりしますが、使い捨てが気にならなければ蒸気でホットアイマスクで十分かもという使用感です

ひとこと

まず大きなモニターが目を引きますね。55インチとなると同時に色々な画面を表示できそうです。そして水分の摂取を怠らないように電気ポットと急須があるのもいいですね。これからの季節は水分不足に注意しなければいけないです。
リモートワークの大きなメリットの一つに、飼っているペットと常に一緒にいられるというのがありますね。飼い猫と一緒に過ごせるのは大変素晴らしいです。

Studyplus事業部デザイナー: 小松さん

次はStudyplus事業部デザイナーの小松さんです。後述されていますが、元々リモートワークの環境作りにはかなり力を入れていたようでとても素敵な作業環境です。

写真

f:id:tagucch:20200601165842j:plain f:id:tagucch:20200601165904j:plain

(もしあれば)こだわっているところや商品情報など

  • フリーランスだった期間が8年くらいあり、たまに副業もするので、もともと家でも作業できるようにはしてました。
  • 昨年夏に引っ越してから、片付けつつじわじわと部屋づくりをしてた。そんなに予算は無いのでコスパにこだわりがち。
  • モニターは LG UltraFine 5K Display 、きれいな中古があったので買った。
  • デュアルのディスプレイアームマウントトレー で2画面をできるだけシームレスに使えるようにしている。
  • デスクは、昔あった無印のユニットシェルフのデスク。フリーランス時代にでっかいデスクを置きたくて2個並べていたのの生き残り。
  • 椅子は WilkhahnSolis 、コレもフリーランスを始める際に勢いで大枚を叩いて購入し、文字通り今まで身体を支えてきてくれたのだけど、10年以上使ってファブリックとか流石にいろいろくたびれてきたので、感謝しつつ買い替えたい気分。
  • ヘッドホンはSONYの WH-1000XM3 。集中するのにとてもいいです。UI検証用に用意したPixel3aとの接続だとより高音質で音楽聞けるコトを発見し、ちょっとうれしい。
  • AmazonのEcho(Plus , Show5 , Dot)とIKEAのTRÅDFRIなどで、だいたいの照明と家電は声で制御できるようにしている、スマート!
  • 目の前のリビングのペンダントライトは IKEA の GRIMSÅS、ちょっとファンシー過ぎるのではないかとも思ったけど、そのシルエットと落とす影とクラフト感で、さっぱりはしてて味気ない無い部屋の中でアクセントになっている。
  • 運動不足解消・健康維持の為に リングフィットアドベンチャー をやる頻度が上がり、自粛前より身体ができあがってきたw

今の在宅勤務環境の良い点・悪い点

良い点

  • 照明・空調を自分の好きなように調整できる、窓全開にしとくのは気持ちいい。
  • スピーカーで音楽が聴ける、歌える。
  • 隙あらばラジオも聴ける。
  • 洗濯物をいつでも取り込める
  • 移動しないでいい

悪い点

  • 線路が近いので電車がうるさい
    • ノイズキャンセリングヘッドホンで緩和できるが、そろそろ暑い。
  • 休憩中も、なんか家事とかやってしまって、厳密には休んでないコトに気づいた。
  • 目を使ってばかりなので、目の疲れはひどい。
    • 目が痛くなったら、ホットアイマスクをして10分横になるといい。
  • リモートの会議やユーザーインタビュー中にAlexaが反応する。
  • 会社で買ってもらった本が無い。
  • お菓子を自前で用意せねばならぬ。

在宅勤務手当で買いたいもの

  • 眼精疲労の緩和の為にブルーライトカットな眼鏡をかけるのだけど、ヘッドホンと干渉するので Short Temple というカタチの眼鏡を購入したい。
  • 朝になるとブラインドが自動的に上がるといいなって思うので、ブラインドを巻き上げる機械が欲しい(ちょっと高い、2個必要)。
  • コーヒーをドリップするようになったので、グラインダーが欲しい。
  • 除湿機も考えている。
  • もしくは植物を増やす。
  • あとアートを飾りたい。
  • 物欲は無限大。

ひとこと

小松さんの作業環境は僕が憧れてしまいました。今揃っているものもそうですし、今後買い足したいものもおしゃれだったりこだわりが強かったりしてかっこいいです。物欲は無限大、本当にそのとおりだと思いますね。
そしてスマートホーム化が進んでいるのもリモートワークだと便利でいいですよね。

総合広告事業部アカウントプランナー: 伊藤さん

続いてアカウントプランナー(営業)の伊藤さんです。リモートワークというとエンジニアやデザイナーの環境を思い浮かべがちなので、営業の方の環境を見せてもらえるのは貴重です。

写真

f:id:tagucch:20200601170020j:plain 退勤後は一式をトレイにまとめています(再現) f:id:tagucch:20200601170057j:plain

(もしあれば)こだわっているところや商品情報など

  • 部屋にデスクを置くスペースがないため、以前はちゃぶ台で頑張っていました。
  • 現在はドレッサー(化粧台)をデスクとして使用しています。
  • デスクにそのままPCを置くと猫背になるため高さや傾斜が変えられるスタンドを使っています。使わないときは折りたためて便利です!
  • 我が家にはディスプレイがないので、欲しい時にはiPadでDuetDisplayを起動してディスプレイにしています。
    • ディスプレイを使わないときはスタンドにスマホを立てています。

今の在宅勤務環境の良い点・悪い点

リモートについて

良い点

  • 独り言を言っても特に問題ない
  • 昼食代が安く済む
  • 休憩時間に洗濯等家事ができる
  • 体調がよくない日は服装や体勢を工夫して仕事ができる
  • MTGや打合せの平均時間が短い

悪い点

  • 画面越しにしか人と話していないため、通常の会話がどのようなものだったかわからなくなってきている。コミュニケーション能力が下がっている気がする
  • 仕事が終わっても気持ちの切り替えがしづらい
  • 運動不足
  • ラーメンが食べられない
  • ラーメンが食べられない

この環境について

良い点

  • 目の高さに画面があるので、背中や腰の痛みが以前より軽減されている
  • 仕事が終わったらコンパクトに片づけられる
  • すぐ左に冷蔵庫があり、いつでも飲み物を飲める

悪い点

  • シンプルに狭い
  • 隣の住人がいる方の壁に向いているため電話やWeb会議の声が聞こえていないかやや心配。
  • ライトがまぶしい
  • ドレッサーの椅子は8時間座るものではない。痛い。
  • ドレッサーを使用しているので、途中で化粧を直したくなった時に少し大変

在宅勤務手当で買いたいもの

  • やはりお尻が痛くなるので、いい感じのクッションが欲しいです。もしくはバランスボールに座りたいです。

番外編(ベランダ勤務)

f:id:tagucch:20200601170137j:plain

暖かい晴れた日にやりました。 大変気分転換になりましたが、電源がない、なんとなく通話がしづらい、 花粉症の症状が強まるなど課題は多そうでした。

ひとこと

伊藤さんはドレッサーを作業環境にして、退勤時に片付けるという形をとっているそうです。ディスプレイをiPad含め2枚にしていますが、視線が下を向かないように高さを合わせていていいですね。
ベランダでの勤務はいい気分転換になりそうですが、花粉なども考えるとたまーにやるくらいがちょうどよさそうですね。
冷蔵庫がすぐ隣にあり、明渡さんと同じく水分補給が手間なくできるのもよさそうです。

ポルト事業部デザイナー: 竹内さん

最後に、昨年9月にリリースされたPortoの開発・運営を行うポルト事業部のデザイナー、竹内さんの環境を紹介します。

写真

f:id:tagucch:20200601170227j:plain f:id:tagucch:20200601170249j:plain

こだわっているところや商品情報など

  • デスクはずっと使っているもので、とても気に入ってます
    • たまにオイル塗るメンテナンスが必要だけど、ピカピカになるので良い
    • が、奥行きが狭い(500mm)ので、ディスプレイアームでディスプレイ・PCを浮かせて使えるエリアを広げています
  • オンラインMTG用にマイクはRODE NT-USB Mini、イヤホンはAirPods Proを使用
  • スピーカーはBose SoundLink Mini Bluetooth speaker II で、音楽聴くのに使っています
    • キャンプとかアウトドアにも持っていけるのでオススメ
  • ワイヤレス充電スタンドは、スマホが対応していれば、立てておくだけで充電できて便利なのでオススメです。AirPods Proも充電できます

今の在宅勤務環境の良い点・悪い点

良い点

  • 家族の理解があり、作業用に一部屋(の一角)使えているので、子どもがMTGに映り込むトラブルはまだ無い
  • 家族と3食いっしょに食事できるので良い

悪い点

  • 北側の部屋なので、あまり日差しを感じられない & 寒い
  • 宝くじが当たったらもっと良い椅子が欲しい...

在宅勤務手当で買いたいもの

  • Wi-Fiルータを新調したい(調子が悪くてインターネットがたまに途切れるので)

ひとこと

竹内さんは元々フルリモートで勤務しているので、作業環境がとても整っていますね。特にデスクがおしゃれで、全体的に白色の部屋に対していいコントラストなように思います。
特にお子さんがいる家庭ではリモートワークでみなさん苦労している方が多いように思いますが、作業部屋があり家族の理解があるというのは素晴らしいですね。

さいごに

今回は弊社スタディプラスのメンバーのリモートワークでの作業環境の紹介でした。
リモートワークではずっと家にいるので、長時間いることを前提に自分の好みの環境を作りたいですよね。みなさんの個性が出ていて、この記事を書いていてとても楽しかったです。
弊社ではコロナウィルスの影響によりリモートワークが続いており、今後も適宜リモートワークを選択していける環境が整っています。ご興味のある方はぜひこちらをご覧の上ぜひご応募ください。

新型コロナウイルスとStudyplus for School

新型コロナウイルスとStudyplus for School

こんにちは、Studyplus for School事業部エンジニアの島田です。

はじめに

新型コロナウイルス感染拡大により多くの学習塾が休業などの対応を求められる事態になりました。 これを期にオンライン授業を導入する学習塾が増え、「Studyplus for School」も期間限定で無償提供を開始し、それに伴い様々な対応をする事となりました。

info.studyplus.co.jp

今回は、無償提供を開始した2月末からの「Studyplus for School」のシステム改善について紹介します。

数値でみる変化

具体的な数値は非公開情報のため、各数値の増減率を見ていただきます。

生徒数

まずは生徒数の増加率から見ていきます。 2020年2月から2020年5月末の時点で380%以上の増加率となっています。

f:id:yo-shimada:20200601085634p:plain

各機能の利用数

各機能の概要は以下になります。

  • 生徒メッセージ:Studyplus for SchoolとStudyplusのアプリを利用した、先生と生徒のメッセージ機能
  • 保護者メッセージ:Studyplus for SchoolとLINEを利用した、先生と保護者のメッセージ機能
  • 学習記録:Studyplusのアプリを利用した学習を記録する機能
  • 学習計画:学習計画を登録し、オンラインでいつでも学習計画と進捗を確認する機能
  • 面談記録(カルテ):個別指導の指導記録や定期面談の内容を記録する機能

新年度から学校の休業要請まで(2019年4月~2020年2月)と、それ以後(2020年3~5月)の期間でどれくらい各機能の件数が増加したかを見ます。

f:id:yo-shimada:20200601085752p:plain

  • 生徒メッセージ:490%
  • 保護者メッセージ:187%
  • 学習記録:135%
  • 学習計画:66%
  • 面談記録(カルテ):61%

オンライン授業することになり、メッセージやファイルのやりとりをする事が急増しました。

リクエスト数

Application Load Balancerの日別リクエスト合計数

f:id:yo-shimada:20200601090228p:plain

こちらも3月から徐々に上昇していき4月末から急激な上昇をしています。

エラーの発生数

Studyplus for Schoolでは、エラーの追跡・監視にSentryを利用しています。

2019年9月からは一定のエラー発生でしたが、ユーザーが増加し始めた2月から増加し始めてきました。 リクエストの増加と共に、エラーの対応を徐々に始めることで3月、4月に爆発的にエラーが発生することを抑えました。 サービスの特性上、そこまで急激にユーザー数が伸びることを想定していなかったので、いくつかの機能でパフォーマンスの課題が出てきました。 また、それを起因として様々なエラーが多発するようになりました。 後述する対応と個別のエラーをつぶしてくことでエラー数も徐々に低減させていきました。

f:id:yo-shimada:20200601091404p:plain

DBのCPU利用率

改善施策の実施前後のRDSのCPU利用率の比較です。

非同期処理のジョブの中でSlowQueryが実行されることで、ユーザー数の増加に伴い当該ジョブの処理頻度が高まり、DBのCPU利用率が100%近くまで上昇するという事態になっていました。 後述するSlowQueryの改善等で利用率減らすことが出来ました。

f:id:yo-shimada:20200601092202p:plain

機能開発の優先度を検討する

2月時点で考えていた2020年の開発ロードマップを白紙にして、臨機応変に対応する事としました。 方針としては、オンライン授業により利用が高まっている機能のパフォーマンスや機能追加・改善を中心に対策していく事にしました。

機能要件

  • メッセージに機能追加:画像ファイルの送信はできていましたが、利用数の増加と共に要望が多く上がっていたPDF等の画像以外のファイル送信を出来るようにしました。
  • 保護者一斉送信:先生と保護者の1対1のメッセージのみでしたが、より保護者との連絡が多くなり、先生から同じ内容を一斉に送信したいという事で、保護者の方へ一斉送信が出来るようにしました。

tech.studyplus.co.jp

非機能要件:負荷対策

  • APIサーバーのEC2インスタンスを追加
  • 非同期処理のパフォーマンス向上:Studyplus for Schoolでは非同期処理にSidekiqを利用しています。EC2のインスタンス追加とDBのコネクション数を調整して、Sidekiqの並行処理数(Concurrency)をあげることで、ユーザー増加に伴う非同期処理の大幅な遅延を起こさないようにしました。
  • DBの冗長化、CPU利用率の低減:
    • Masterの一台構成からMaster・Slaveの冗長化構成へ変更
    • MySQL -> Auroraに変更。パフォーマンスインサイトを導入
    • パフォーマンスインサイトにより、DBのCPU利用率の上昇原因となっていたSlowQueryを最適化

さいごに

コロナショックにより学習塾のオンライン指導に注目が高まり、Studyplus for Schoolでも短期間のうちに利用者が急増しました。 こうした機能追加やパフォーマンスの改善はユーザーである先生方にも大変喜ばれました。

短期間で状況が目まぐるしく変化する中で、概ね首尾よく対応できたのではないかと思っています。 要因としては全ての要望に完璧に応えようとするのではなく、本質的な課題に対して実現可能なアイデアを出しそれを一つづつ実行していった積み重ねだと考えられます。

これを機会に、今後もオンライン授業というスタイルや、学習塾のICTはの活用が浸透し広がっていくと感じます。 テクノロジーによって教育がより良いものへなるように、その一助としてStudyplus for Schoolを通じて生徒・先生がストレスのなく学習出来る事を心掛けていきたいと思います。

Amazon AuroraにAuto Scalingを導入してCPU高負荷を乗り切った話

こんにちは、SREチームの菅原(id:ksugahara08)と栗山(id:shepherdMaster)です。

新型コロナウイルスが流行して世の中では外出自粛になる中、Studyplusは例年以上に多くのユーザー様に利用していただく機会を得ました。それに伴って4月からサーバーへのアクセスが増えていき、5月にはコロナ前と比べて約2倍のリクエスト数がAWS ALBに来るようになりました。

リクエスト数が増えた結果何が起きたかというと、タイトルからお察しの通りAmazon AuroraのCPU負荷が上がり、頻繁にアラートが発砲されるようになりました。

EC2に関してはAutoScalingを設定していたため負荷に応じてスケールアウトしていたのですが、Auroraに関してはインスタンス数固定で運用してきたため、このようなことが起きるようになりました。

そこでサーバーサイド+SREチームでは他の作業を全てSTOPして、Auroraの負荷対策を行うことにしました。

ちなみにこれが当時(ピークタイムだけじゃなく日中にもCPUアラートがなりだした時)の様子です。 f:id:ksugahara08:20200525161342p:plain

ネタバレになりますが、各エンジニアの尽力によりラグナロクは回避されました。

そもそもなぜAuto Scalingさせる必要があったのか

AuroraのCPU使用率が高騰するようになったと言っても、すぐにスケールアウトを行ったわけではありません。DBのインスタンス数を増やす前にスロークエリ等の改善を行いました。というのも負荷が上がるのはピークタイムの数時間のみで、Auroraが高負荷状態になってもユーザー問合せが来るほどレスポンスタイムも大きく劣化していなかったためです。

スロークエリの改善やN+1問題の解消等の改善を行ってみたのですが、リクエスト数の増加による負荷高騰の要素が大きく、結果としてAutoScalingを入れて負荷分散をさせることにしました。

AutoScalingを選択したのはAuroraのインスタンスは高価で常に起動させているとAWS利用料金が大きく上がってしまうことを避けたかったからです。

弊社のAmazon Aurora構成

話の前提として弊社のAamazon Aurora構成について話しておきます。 特殊な構成では無いのですが、以前は以下のような構成になっていました。

Aurora-cluster
├── db-1 (Master)
├── db-2 (Read Replica)
├── db-3 (Read Replica)
└── analysis-db (分析用)

BIツールのために分析用のanalysis-dbを入れています。APサーバーからはカスタムエンドポイントを使って読み込み先DBインスタンスを分散させています。常時Master 1インスタンスとRead Replica 2インスタンスが起動している状態でした。

カスタムエンドポイント(読み込み先)の対象は分析用以外全て含めています。これはMasterにも読み込み処理を行ってもらい、Read Replicaを増やさなくて済むようにしているためです。弊社のアプリは読み込み系の負荷が大きくなることも背景にあります。

こちらの構成だったclusterを今回以下に変更しました。

Aurora-cluster
├── db-1 (Master)
├── db-2 (Read Replica)
├── db-3 (Read Replica managed by AutoScaling)
├── db-4 (Read Replica managed by AutoScaling)
・・・
├── db-n (Read Replica managed by AutoScaling)
└── analysis-db (分析用)

不意なフェイルオーバーに対応できるように常時MasterとRead Replicaをそれぞれ1インスタンス起動し、CPU負荷をトリガーにしてRead ReplicaをAuto Scaleさせるように設定します。弊社のリクエストの傾向として夜間はリクエスト数が減るため、台数を2インスタンスに減らして夕方のピークタイムに増やすようにしたいという意図がありました。

また、弊社のリスエストピークは毎日同じ時間に起き、ゲームアプリのような急なリクエスト増加も滅多に起こらないため、Auto Scalingの設定で用件が満たせるだろうと予想しました。

Aurora Auto Scalingの設定

この記事ではAurora Auto Scalingで出てくる用語については説明を省略して弊社での設定を中心に話します。もし用語がわからなければ公式ドキュメントを参照してください。

結論から書いてしまうと、弊社では以下のような設定値で落ち着きました。 f:id:ksugahara08:20200525162948p:plain

ターゲットメトリクスを44%にしているのは、Read Replica全てのインスタンスにおけるCPU使用率の平均値で判定されるため、分析用DBが入っている分ターゲット閾値を(しかたなく)低く設定しました。この値は何度も試行を繰り返しながらこの値に落ち着いた形です。

スケールインクールダウン期間は夜間にスケールインを想定しており、負荷の下がり方が急なので5分に設定しました。 スケールアウトクールダウン期間は30分に設定しました。DBがclusterに追加されてから、利用可能になるまで5分半かかり(何度も作り直し平均値を取りました)、追加されたRead Replicaがエンドポイントに追加されて、負荷が分散されるまでに時間がかかるためこれくらい長くすることにしました。

カスタムエンドポイントの変更

公式のAuto Scalingでは読み込みエンドポイントを推奨しているのですが、分析用DBインスタンスをがいることと、料金的事情でMasterにも読み込みリクエストを処理して欲しいという理由でカスタムエンドポイントを継続して使うことにしました。

カスタムエンドポイントには追加設定で今後追加されるインスタンスをこのクラスターにアタッチするという項目があるのでこれを有効にしました。これでAuto Scalingによって追加されたRead Replicaをカスタムエンドポイントで接続させることができます。

f:id:ksugahara08:20200525163021p:plain

サービス停止を伴うメンテナンスをしたくなかったので、10分程度の接続断が発生するカスタムエンドポイントの変更は行わず、カスタムエンドポイントを2つ作って付け替えることにしました。

アプリケーション側の対策

弊社ではRuby on Railsを使っており、コネクションプールを有効にしています。 そのため、DBがオートスケールすると以下の問題が発生します。

  • DBがスケールアウトしたあと、追加されたDBをRailsが認識できない(=追加されたDBにSQLが実行されない)
  • DBがスケールインしたあと、削除されたDBへ接続しようとしてエラーになる

なかなか悩ましい問題なのですが、なんとかこれらの対応をしました。

DBがスケールアウトしたとき

DBを定期的に自動的にデプロイされるように、Jenkinsの設定をしました。 (最初はCPU使用率トリガーでオートスケールさせていたので、いつオートスケールされるかわからないためオートスケールされそうな時間帯に30分に1回デプロイしてましたが、決まった時間でオートスケールさせるようにしてからは、その時間よりちょっとあとにデプロイされるようにしました。) デプロイしなおせばコネクションプールが作り直されるので、スケールアウトして追加された新しいDBに接続がされるようになります。 かなりアドホックなやり方なのですが、他にいい方法が思い浮かばなかったのでこうしています。何か他にいい方法があれば教えて下さい…。

その他には、コネクションプールを無効にしようかと思い、 activerecord-refresh_connection gemの導入を考えましたが、こちらのgemはpumaでは使えないため諦めました。 またpuma worker killerの導入もしましたが、puma worker killerの実行のタイミングでたまにNo connection poolエラーが発生するという未解決の問題があり、最終的に定期デプロイに落ち着きました。

DBがスケールインしたとき

RailsではDBに接続できなくなった場合、エラーを投げ、コネクションを削除するようです。そのため次に接続するときは生きているDBに接続がされます。 なのでスケールイン後ずっとエラーが出続けるわけではないですが、エラーが出ないようにしたいです。 色々探した結果、 activerecord-mysql-reconnect gemを導入しました。 こちらを入れるとDB接続エラーが発生した場合、再接続をしてくれます。ありがたいですね。

ScheduledActionの設定

Auto Scalingを設定してみたものの、ターゲットメトリクスの閾値設定に苦慮するようになりました。当初CPU使用率が55%でスケールアウト・インするように設定をしていたのですが、CPU使用率の値は小刻みに変動するため、不要なスケールアウト・インが繰り返されるという悩みです。これが起きると上記で上げたアプリケーション側での対応も必要になり設定がより複雑化します。

そこでScheduledActionを設定することにしました。ScheduledActionは時刻指定で最大、最小のインスタンス数を変えることができます。

今回はインスタンス数の最小値を定時に変えることで、閾値付近をCPU使用率の値が超えたり下がったりしないようにしました。 具体的には以下のような設定をTerraformで入れています。

# JSTの20:20にスケールアウト
resource "aws_appautoscaling_scheduled_action" "scaleout-nighttime" {
  name               = "scaleout-nighttime"
  service_namespace  = "rds"
  resource_id        = "cluster:my-aurora-cluster"
  scalable_dimension = "rds:cluster:ReadReplicaCount"
  schedule           = "cron(20 11 * * ? *)"

  scalable_target_action {
    min_capacity = 3
    max_capacity = 5
  }
}

# JSTの0:30以降にスケールイン
resource "aws_appautoscaling_scheduled_action" "scalein-nighttime" {
  name               = "scalein-nighttime"
  service_namespace  = "rds"
  resource_id        = "cluster:my-aurora-cluster"
  scalable_dimension = "rds:cluster:ReadReplicaCount"
  schedule           = "cron(30 15 * * ? *)"

  scalable_target_action {
    min_capacity = 2
    max_capacity = 5
  }
}

※こちらの設定ファイルはあくまで例です。

AWS CLIで設定する場合は以下になります。

# JSTの20:20にスケールアウト
aws application-autoscaling put-scheduled-action \
  --service-namespace rds \
  --schedule "cron(20 11 * * ? *)" \
  --scheduled-action-name 'scaleout-nighttime' \
  --resource-id 'cluster:my-aurora-cluster' \
  --scalable-dimension rds:cluster:ReadReplicaCount \
  --scalable-target-action 'MinCapacity=3,MaxCapacity=5'

# JSTの0:30以降にスケールイン
aws application-autoscaling put-scheduled-action \
  --service-namespace rds \
  --schedule "cron(30 15 * * ? *)" \
  --scheduled-action-name 'scalein-nighttime' \
  --resource-id 'cluster:my-aurora-cluster' \
  --scalable-dimension rds:cluster:ReadReplicaCount \
  --scalable-target-action 'MinCapacity=2,MaxCapacity=5'

# 結果確認
aws application-autoscaling describe-scheduled-actions \
  --service-namespace rds \
  --resource-id 'cluster:my-aurora-cluster'

AuroraのScheduledActionはAWSマネジメントコンソール上からは設定できないためAWS CLIかTerraformでの設定で変更をかけることができます。そのためこの設定項目があることに最初は気がつくことができませんでした。ScheduledActionを入れてからはスケールがかなり安定したように思います。

クエリ改善

DBに負荷をかけているクエリ(より正しくはDBを長く使用したクエリ)を探すには、パフォーマンスインサイトが有用でした f:id:ksugahara08:20200525163309p:plain このように具体的なSQLがわかるので上位のSQLを改善(クエリチューニングしたりインデックスを作成したり)することでCPU使用率がかなり改善しました。

特に、弊社の中では一番大きなテーブルである勉強記録テーブルにインデックスを作成したことで、CPU使用率が大幅に下がりました。

改善後はピークタイムでもスケールアウトする必要がなくなりました 🎉

逆にサービス利用率が低い夜中はスケールインさせてDBの台数を減らすことでコストカットを実現することができました。

スマートフォンアプリの改善

スマートフォンアプリチームにも今回の負荷高騰対策に協力してもらいました。 サーバーへのリクエスト数を減らす改善を入れたことで、負荷が上がる以前のリクエスト数と同じ水準まで下がりました。圧倒的感謝っ・・・・!

みんなで総力戦をした結果

SREとサーバーサイドチームのAurora Auto Scaling対応、SQLチューニング、N+1解消、DBのインデックス作成。クライアントアプリチームのリクエスト数削減対応。 これらの対応を総力戦で行った結果、ラグナロクは回避され平和が訪れました。 それどころか負荷が大幅に下がりAuroraのインスタンス数を以前より減らすことさえできるようになりました 🎉

今後の課題

今回改善が進みましたが、新しい課題にも直面しました。例えば以下のようなものです。

  • Auroraがスケールアウトした際にはコネクション再生成のためにデプロイが必要になっています。RDS Proxyが正式リリースされればそれで解決されないだろうか 🤔
  • 分析用DBインスタンスはcluster内にいるとAuto Scalingのターゲット値が決めづらいのでclusterから外す形にするか検討したいです。
  • スケールインしたAuroraをMackerelから退役させるには手動で行わないといけませんでした。AWSインテグレーションを使って入れているので、メトリクスを取得してそのメトリクスがなければpower offにして退役というシェルも使えなさそうでした。

もし良い方法をご存知の方が入ればコメント等で教えて頂ければと思います。

まとめ

4月から負荷が高騰して緊急対応が必要になりましたが、大きな障害にならず、エンジニアみんなで乗り越えられたことは事業部内での成果にもなりました。また、このような事態に直面したことで、今まで優先的に対応できてなかったパフォーマンス改善タスクが一気に進んだように思います。

Aurora Auto Scalingを導入している事例をあまり見かけませんが、非常に便利なものでしたので皆さんも導入検討してみてはいかがでしょうか?

Elmの何が良いのか?何ができるのか?

こんにちは。ForSchool事業部の石上です。お菓子はばかうけが好きです。今日はElmの話です。

背景

Studyplus for Schoolには、Elmで実装された画面アプリケーションがあります。こういうやつです。

仕様はとても小さく、QRコード読み取る -> APIへ投げるという機能のみだったため、Elmでの実装が許されました。今回は、Elmを普段触っていないチームメンバーに「なんでElmなんて使ってるんだ...?」と思われないように、その良さを伝えておきたいと思います。

Elmの特徴

まずElmについて簡単に書いておきます。Elmの特徴は主に3つでしょう。

The Elm Architecture

The Elm Architectureを構成する要素は、ModelとViewとUpdateです。Modelはアプリケーションの状態を表すデータ構造、ViewはDOMを出力する関数、Updateはイベント1に対して状態を変更する関数です。

Redux経験があれば、あれとほぼ同じものと考えて良いと思います。個人的にReduxとTypeScriptを使う場合と比較して好きなのは、ReduxとTypeScriptだとアクションの型定義が面倒だったり工夫が必要だったりするところ、Elmでは type Msg = MyMsg Payloadのように書けて、記述しやすいのが好きです。

Runtime Errorが起きない

Elmは静的型付け言語です。基本的には2コンパイル時に不正な関数呼び出しを検出することができます。

コンパイルエラーが親切

コンパイルエラーが親切なのもElmの良いところです。たとえば、あるモジュール間で循環参照がある場合に、以下のようにわかりやすいエラーになります。

./src/Main.elm
Error: Compiler process exited with error Compilation failed
Compiling ...-- IMPORT CYCLE ----------------------------------------------------------------

Your module imports form a cycle:

    ┌─────┐
    │    Note
    │     ↓
    │    Main
    └─────┘

Learn more about why this is disallowed and how to break cycles
here:<https://elm-lang.org/0.19.1/import-cycles>

Your module imports form a cycle と言われて意味がわからなくても、この図を見れば、「ああ、MainがNoteをimportして、NoteがMainをimportしているからぐるぐるしちゃうんだな」というのがわかります。そして、必ずと言っていいほど最後に参考リンクが載っています。

SPA化

Elmに限らず、ウェブアプリケーションをSPA(シングルページアプリケーション)にするときには、リンクを少し拡張するような仕組みが必要になります。通常のHTMLのaタグであれば、リンクされたURLへ遷移する際、ドキュメント(HTMLなど)をすべてダウンロードしてブラウザに表示します。しかしSPAでは、それを行わずに、必要なリソースを必要なときに取得しつつ、画面の必要な部分を更新するというようなことをします。雑に書くと以下のようなイメージです。

<script>
const handleSPALink = (e) => {
  e.preventDefault(); // 通常の遷移をしない
  updateState(); // 状態を更新
  render(); // 表示
}
</script>
<a onclick="handleSPALink" href="/hoge">SPA Link</a>

ReactであればReact Routerが使われることが多いです。

Elmの場合、Browser.application という関数を使います。

  1. Browser.applicationの引数に以下のようにメッセージを設定
    1. haskell Broser.application { ... , onUrlRequest = UrlRequest , onUrlChange = UrlChange }
  2. a要素の関数でa要素を表示
  3. a要素をクリックすると、 UrlRequestが発行される
  4. update関数でUrlRequestをつかまえる
    1. 内部リンク(Browser.Internal)の場合:Navigation.pushUrlでURLを変更
      1. UrlChangeが発行される
      2. URLに応じたページの初期化を行う
    2. 外部リンク(Browser.External)の場合:Navigation.loadで外部URLへ遷移

なんだか面倒そうですね。しかしこれには良いところもあって、それはページ遷移してURLが変わったという変更がちゃんとTEA(The Elm Architecture)のなかに収まることです。ReactとReduxの組み合わせで同様のことをやろうとすると、またそれ用のredux middlewareを設定してあげたりする必要がありそうなので、Elmではこういうことが標準の機能として用意されているというのが安心感があります。

詳しくは:https://package.elm-lang.org/packages/elm/browser/latest/Browser#application をご覧ください。

Port

Studyplus for Schoolで実装したアプリケーションではカメラやオーディオを扱う必要があったため、Elmだけでは実装が完結しませんでした。そこで、Portという機能を使う必要がありました。

Portとは、Elmの世界とJavaScriptの世界をきれいに分けて実装する仕組みです。公式のガイドには、localStorageを扱う例が書かれています。

JavaScript側

var app = Elm.Main.init({
  node: document.getElementById('elm')
});
app.ports.cache.subscribe(function(data) {
  localStorage.setItem('cache', JSON.stringify(data));
});

Elm側

port module Main exposing (..)

import Json.Encode as E

port cache : E.Value -> Cmd msg

上記のコードは、Elm側で起こったイベントを、JavaScript側で処理しています。これを使うには、Elm側のupdate関数のなかで以下のようにCmd.batchという関数にこのcache関数の返り値を指定します。

update msg model = 
    case msg of
        Cache value ->
            (model, Cmd.batch [cache value])

実装がElmだけで完結せず、JavaScriptの依存(package.jsonのことです)がたくさん入ってしまうのは残念なところですが、作りとしてはElmとJavaScriptがしっかり分けられるのは良いことだと思います。

テスト

Elmには、https://github.com/elm オーガニゼーションで管理されている標準のライブラリのほかに、https://github.com/elm-explorations で管理されている準標準ライブラリのようなものがあります3。そこにテストライブラリもあるので、基本的にはそれが使えます。

まとめると?

ごちゃごちゃと書きましたが、まとめるとどういうところが良いのでしょう。

  • SPAをつくるために必要なものがちゃんと標準ライブラリ(あるいは準標準ライブラリ)に入っている
  • 実行時エラーが起きにくい

逆に良くないところは、以下の2つです。

  • localStorageやAudioなどブラウザのAPIを直接触れないところが多く、そういうのが必要なところはJSのコードを書かないといけない
  • 学習コスト。特に型の読み方に慣れるまで大変。

アプリケーション実装にElmを使わなくても、Elmを学ぶことで恩恵はあると思います。たとえば、Elmを学ぶ前は、サーバーから来たデータをJSON.parseして型をanyにしてしまうことに、疑問を持ったことなどありませんでした。もしこの記事を読んで興味を持たれたら、仕事で使う予定がなくても、堅牢なフロントエンドを作る仕組みを知るためにElmに触れてもらえればと思います。


  1. これはElmの世界ではMsgと呼びます。ReduxではActionと呼んだりします。ライブラリによっていろんな呼び方があってややこしいですね。

  2. ElmからJavaScriptの関数を呼び出すようなこともできるため、絶対に起きないわけではありません。

  3. オーガニゼーションの説明にはPackages that may one day be in @elm-langと書かれています