エブリーエンジニアブログ エブリーエンジニアブログ

DELISH KITCHEN のデータベースの現状と Aurora を導入した話

f:id:nanakookada:20210614135834p:plain

はじめに

DELISH KITCHEN のデータベースについて紹介します。

サービスやバックエンドシステムの全体像については DELISH KITCHEN のサービスとバックエンドシステムのお話 - every Engineering Blog で紹介しています。よろしければご覧ください。

概観

DELISH KITCHEN ではサービスの大半のデータの保存に Amazon RDS を使用しており、データベースエンジンとしては主に MySQL を使用しています。サーバーがいくつかに分かれておりデータベースもそれぞれにありますが、今回はレシピやユーザーの情報の入ったメインのデータベースの話をします。

DELISH KITCHEN は規模としては月間総利用者数 5200 万人のサービスです。

※ DELISH KITCHEN のアプリ、Web、SNS、サイネージなど全ての提供内容における総利用者数のこと。

オンメモリキャッシュ

レシピなどのマスタ系のデータについては、更新頻度が低いことからサーバーアプリ内にオンメモリキャッシュを持ち、定期的に更新するというアプローチを取っています。管理画面外からのアクセスにはそのキャッシュを利用しており、トランザクション系のデータの読み書きについてはキャッシュは行わずサーバーから RDB へ読み書きを行っています。

RDB 負荷分散

MySQL レプリケーションを利用して複数のインスタンスへ負荷を分散しています。また、マスタ系とトランザクション系の DB インスタンスを分けており、トランザクション系の方の RDS インスタンスタイプを大きめにしています。

Amazon Aurora の導入

最近読み込み頻度が高く、レコード量が将来的に数千〜億単位になることが見込まれ、かつ読み書きのレイテンシがアプリの初期描画速度に影響するために相応に低いことが求められるデータの保存方法を考える機会がありました。 書き込みは素直なテーブル構造にするとランダムインサートとなり、通常の RDS ではレコード量増加に伴って書き込みのレイテンシは増えていくことが想定されました。

そこで、レコード量が増加しても I/O が低下しにくい Amazon Aurora MySQL を導入しました。導入にあたり簡単な負荷試験をして RDS (MySQL) との比較を行いましたのでその概要を紹介します。

Aurora と非 Aurora RDS の負荷試験による比較

ランダムインサートを行う下記のような簡素な Go のコードを実行し、レコード量増加に伴った INSERT のスループットの変化を確認しました。

func main() {
    insertedCount := int64(0)
    insertStart := time.Now()
    countMutex := sync.RWMutex{}
    wg := &sync.WaitGroup{}

    for i := 0; i < 8; i++ {
        wg.Add(1)
        go func() {
            sess := getSession() // DB セッションを作成
            for insertedCount < 10000000 {
                // 主キーをランダムに発行した適当なレコードを INSERT
                record := genNewRecord()
                if _, err := sess.InsertInto("test_table").Columns("col_a", "col_b").Record(record).Exec(); err != nil {
                    panic(err)
                }
                countMutex.Lock()
                insertedCount++
                if insertedCount%100000 == 0 { // 100000 レコードごとに経過時間を出力
                    fmt.Println(insertedCount, time.Now().Sub(insertStart))
                }
                countMutex.Unlock()
            }
            wg.Done()
        }()
    }

    wg.Wait()
    os.Exit(0)
}

結果的に、レコード量が増えても Aurora ではまるでスループットは一定でしたが、非 Aurora では対数的ではありますが顕著な増加が見られました。(グラフはイメージです)

f:id:bnpb:20210609191845p:plain
Aurora はレコードが増えても INSERT のパフォーマンスが変わらない!

導入決定〜使用開始まで

Aurora RDS インスタンスの作成、サーバーアプリからの接続までについては非 Aurora のそれとあまり変わりませんでした。DB インスタンスがたとえ 1 つのみでも Aurora クラスターが作成され、Web コンソールの DB 一覧を見るとクラスタ配下にインスタンスがあると表示されます。

f:id:bnpb:20210609191720p:plain
DB 一覧画面。クラスタ配下に DB インスタンスが表示される

MySQL 互換なので、接続後は通常の MySQL エンジンと(完全ではないようですが)同じように使用できます。導入後のパフォーマンスについてはまだレコード数が多くないのであまり大きなことは言えませんが、期待通り動作しています。

Aurora を監視する

Aurora は非 Aurora の RDS と比べて取れるメトリクスが変わってきます。How to Collect Aurora Metrics | Datadog に非常にわかりやすくまとまっていたので、参考にして以下の項目に合致するメトリクスをモニタリングしています。

  • Query throughput
  • Query performance
  • Resource utilization
  • Connections
  • Read replica metrics

最後に

DELISH KITCHEN のデータベースについての紹介でした。DELISH KITCHEN ではレシピ動画サービスの安定稼働に向けて改善を続けています。お読みいただきありがとうございました。

参考

今すぐできるレビュワーに優しいPull Requestをつくる7つのポイント

f:id:nanakookada:20210608121120p:plain

はじめに

はじめまして。DELISH KITCHEN開発部の桝村です。DELISH KITCHENのWEBフロントやAPIサーバーの開発等に携わっています。

突然ですが、みなさんは本日もPull Requestを使ってレビュー依頼しましたか?もしくは、誰かからレビュー依頼を受けましたか?

チーム開発におけるコードレビューというものは、プロダクトの品質向上やチーム内での知見共有に貢献しているものの、チームがコードレビューに対して相当な時間や労力をかけているのも事実かと思います。

加えて、レビュー対象の実体でもあるPull Requestの品質は、作り手である実装者に大きく依存しており、コミットから説明文まで自由に作れる反面、レビューしやすいPull Requestを作成しないと、より一層自身やチームに大きな負担がかかる可能性があります。

そこで、今回はレビュワー目線に焦点を当てて、レビューしやすいPull Requestをつくるために自分が心がけていることを紹介させて頂きます。 簡単かつすぐに改善できるポイントをまとめたので、ぜひ参考にして頂けると幸いです。

今すぐできるレビュワーに優しいPull Requestをつくる7つのポイント

1. WhyとWhatをそれぞれ記載する

WhyとWhatが不十分な場合、レビュワーはそれらをコードから想像せざるを得なかったり、実装者へ直接確認する手間が生じて、大きな負担になる可能性があります。また、WhyとWhatが区別されず混合している場合も、実装内容の難易度や複雑性により、実装者とレビュワーの認識に齟齬が生じ得ます。

WhyとWhatをそれぞれきちんと記載することで、レビュワーは、本来のレビュー内容である、仕様通りかどうか、改善の余地はあるか等の確認作業に集中でき、よりコードレビューをしやすくなります。

また、Pul Request自体が履歴的な情報としてリポジトリ内に残り続ける点で、実装に関するドキュメントとしての役割も担います。よって、WhyとWhatをきちんと記載することは、長期的に見てもチームにとって非常に貴重な財産になります。

加えて、以下のような情報があると、実装内容の正当性を容易に検証できたり、アウトプットがひと目で分かる点で、よりレビュワーが実装概要を理解しやすくなります。

  • ローカルでの動作確認の手順
  • 関連するPRやタスク管理システムのチケットへの参照リンク
  • フロントエンド実装の場合、デバイス別でスクリーンショットを添付
  • バックエンド実装の場合、レスポンスや必要なパラメータを記載

2. 説明文は構造化する

説明文が各項目について整理されず文章のみで構成されている場合、読み手であるレビュワーは実装内容の要点を理解するのに時間がかかり、大きな負担になる可能性があります。

説明文では以下のようにマークダウン記法を使用できるので、見出しや箇条書き、コード埋め込み等のスタイルを利用することで、レビュワーは、実装概要をひと目で理解でき、よりコードレビューをしやすくなります。

### Why
- 実装背景

### What
- 実装内容
  - 実装内容詳細(その1)
  - 実装内容詳細(その2)

### Ref
- 関連PRへの参照リンク

### Check
- [ ] レビュー依頼前に必ず実施すること(その1)
- [ ] レビュー依頼前に必ず実施すること(その2)

Pull Request 説明文の構造化例
Pull Request 説明文の構造化例

参考: Basic writing and formatting syntax

また、Pull Request TemplatesというPull Requestの説明文に対して開発者に含めて欲しい情報をカスタマイズし、標準化できる機能があり、これを使用すると、レビュワーにとって見やすい説明文になるだけでなく、実装者にとっても構造化する手間がなくなったり、何を記載すれば良いか明確になる点で導入するメリットが非常に大きいです。

参考: Creating a pull request template for your repository

3. コミットは課題を解決した単位で行う

コミットの粒度がバラバラであったり複数の変更が入った曖昧なコミットである場合、レビュワーはどんな変更をしているのか把握しづらく、大きな負担がかかる可能性があります。

機能実装やバグ修正、リファクタ等、まずは単一の課題や目的を単位としてコミットすることで、レビュワーは、変更概要や意図を正確かつ容易に理解でき、よりコードレビューをしやすくなります。

加えて、以下のようにコミットメッセージにPrefix (テキストの先頭につける文字) をつけると、どのカテゴリの修正をしたのか、プロダクションコードに影響があるコードかがひと目でわかるようになり、よりコードレビューをしやすくなると思います。

  • feat: (new feature for the user, not a new feature for build script)
  • fix: (bug fix for the user, not a fix to a build script)
  • docs: (changes to the documentation)
  • style: (formatting, missing semi colons, etc; no production code change)
  • refactor: (refactoring production code, eg. renaming a variable)
  • test: (adding missing tests, refactoring tests; no production code change)
  • chore: (updating grunt tasks etc; no production code change)

参考: Semantic Commit Messages

4. Pull Requestは適切な大きさに分割する

Pull Requestが大きすぎる場合、レビュワーは単純に時間や労力がかかるだけでなく、既存のコードへの影響範囲が大きくなるゆえに問題点の発見も困難になり、大きな負担がかかる可能性があります。

適切な大きさに分割すると、レビュワーは、影響範囲もより限定的になるため、レビューが楽になったり、その精度も上がり、よりコードレビューをしやすくなります。Pull Requestの粒度としては、自分の場合、スコープ、つまり機能セットを絞り込み、1つのPull Requestで解決するタスクを減らすことを意識しています。

5. 個別説明が必要な箇所は積極的にコメントをつける

特定のコードについて説明が必要な場合があると思います。例えば、実装したものの自信がなく注意深くレビューをお願いしたい時やコードのみで実装の意図が伝わりにくい時、知見を共有したい時などです。そういった場合、インラインコメントを記載すると、レビュワーは、自ずとコメント周りのコードを注意深く確認したり、早期に問題提起・解決策の話し合いができ、よりコードレビューをしやすくなります。

Pull Requestへのインラインコメント例  (Githubの場合)
Pull Requestへのインラインコメント例 (Githubの場合)

参考: Adding line comments to a pull request

また、実装者が躊躇せず積極的に発信することが、有意義な議論やコミュニケーションが生み、結果的にチームの成長や開発効率の向上に繋がると思います。

6. テストを書く

テストがない場合、レビュワーは実装内容の仕様をソースコードのみから読み取る必要があり、大きな負担がかかる可能性があります。

テストがきちんと書かれていると、ただソフトウェアの品質を向上させるだけでなく、ソースコードの仕様(期待する処理結果)に関するドキュメントとしての役割も担うため、レビュワーは、その仕様や振る舞いを容易に読み取ることができ、よりコードレビューをしやすくなります。

7. Pull Requestでのコメントを Slack に通知させる

コードレビューにて実装者のレスポンスが遅い場合、レビュワーは返信が無くて気になったり、コメント内容を忘れる等により、レビューの効率を下げ、大きな負担がかかる可能性があります。

そこで、実装者が簡単かつ最初にできることは、レビュワーによるコメントにいち早く気づくことです。Slackとの連携機能を使用することで、レビュワーによるコメントやレビューを任意のチャンネルへ通知させることができ、少しでも早く返信できるようになります。

SlackへのPull Request コメント通知例  (Githubの場合)
SlackへのPull Request コメント通知例 (Githubの場合)

参考: GitHub と Slack を連携させる

さいごに

今回は、チーム開発において、レビュワーに優しいPull Requestをつくるポイントをまとめてみました。 冒頭でお話ししたとおり、チーム開発ではコードレビューは結構な時間と労力がかかります。裏を返せば、メンバー一人一人がレビューしやすいPull Requestの作成を心がけることで、チームの開発速度が大きく改善する可能性があると思います。

今回紹介させて頂いたポイントを実際の開発現場で試して頂けると嬉しいです。

ここまでお読みいただき、ありがとうございました。

In-App Review APIの導入について

f:id:nanakookada:20210527184322p:plain

はじめに

はじめまして。普段はMAMADAYSでiOSエンジニアをしている國吉です。

iOSエンジニアではありますが、アプリのストアレビュー改善企画も兼務で行っているため、時にはAndroidの実装を担当することもあります。

そこで今回は2020年8月頃にGoogleから提供されたIn-App Review APIをMAMADAYSのAndroidアプリに導入し、実際レビュー評価にどのような変化を及ぼしたのかをお話していきます。

In-App Review APIとは

アプリから離脱することなく、アプリのレビューを行うことができるAPIです。

APIレベルは21以上(Android5.0)がサポートされています。

developer.android.com

これまでの課題

ストアのレビュー評価はアプリのインストールに大きく関わってきます。

ただ、MAMADAYSではこれまで”独自ポップアップを実装し、Google Play Storeに遷移させレビューを行ってもらう”というフローだったため、レビューまで手間がかかってしまいユーザーの離脱が多かったです。

そのためレビュー評価4.0から全く上がらないのが課題でした。

導入してみた結果

In-App Review APIを導入し約8ヶ月程経過しましたが、徐々にレビュー評価の件数が上がってきています。

もちろんその8ヶ月間でアプリ自体に様々な機能を追加しており、利便性が向上していることも影響してると思いますが、アプリ内でレビュー完結できることがレビューという行為のハードルを下げています。

導入前と比較すると0.3程度上がり、レビュー評価は4.3~4.4程度にまで成長しました。 f:id:K-ryosuke:20210525153733p:plain

レビュー要求を表示するタイミング

レビュー要求は表示するタイミングが重要です。

MAMADAYSでは、ユーザーが何かを達成した時(例えば記事をお気に入りした。育児記録を登録した。など)に下記の公式リファレンスに記載されている事項を考慮しレビュー要求を表示しています。

  1. ユーザーがアプリやゲームを十分体験してから、アプリ内レビューのフローを開始してください
  2. ユーザーに過度にレビューを求めないでください
  3. 評価ボタンや評価カードを表示する前または表示中に質問をしないでください(「アプリを気に入りましたか?」といったユーザーの意見に関する質問など)

実装方法

In-App Review APIはPlay Core SDKの一部なので、gradleファイルにCoreライブラリ1.8.0以上を追加します。

dependencies {
    implementation 'com.google.android.play:core:1.8.2'
}

様々な画面でレビュー要求を表示したいので、In-App Review APIをリクエストして表示する処理を共通関数として作成していきます。

ただし、公式リファレンスにも注意書きされていますが勝手に表示されるViewのデザインやサイズ、背景色等のカスタマイズは一切行わないようにしてください。

import com.google.android.play.core.review.ReviewManagerFactory
...
fun showInAppReview(activity: Activity) {
    val manager = ReviewManagerFactory.create(activity)
    val request = manager.requestReviewFlow()
    request.addOnCompleteListener {
        if (it.isSuccessful) {
            manager.launchReviewFlow(activity, it.result)
        } else {
            // error
        }
    }
}

f:id:K-ryosuke:20210525153528p:plain

最後に

Googleから提供されているAPIのため実装が比較的簡単であり、レビュー評価の向上にも繋がるためまだ導入されていない方は是非導入してはいかがでしょうか。

ただ、In-App Review APIをリクエストした際、必ずレビュー要求が表示されるわけではないので、絶対にレビュー要求を表示したい画面は”独自のポップアップ”を表示するように切り分けて使っていくのもいいかもしれません。 

ここまで読んでいただき、ありがとうございました。

UITableViewDiffableDataSourceを使ってクラッシュ率を改善しました

f:id:nanakookada:20210521173407p:plain

はじめに

iOSでTableviewやCollectionViewを扱っていると、UIとデータとの間で不整合が起きた際に NSInternalInconsistencyException というエラーを吐いてアプリが落ちるというのはよくある話だと思います。

TableViewに関してはiOS13から UITableViewDiffableDataSource が追加され、Apple曰くこの問題を回避できるらしいので、DELISH KITCHENのiOSアプリで採用してみました。

導入方法

Hashable化

セクションやアイテムに対応するオブジェクトがHashableに適合している必要があります。 今回は対象となるオブジェクトがユニークなIDを既に持っていたので簡単でした。

/// TableViewの各セルに対応するオブジェクト
struct Item: Hashable {

    let id: Int
    ...

    /// 追加1
    static func == (lhs: MessageDetailRowItem, rhs: MessageDetailRowItem) -> Bool {
        return lhs.id == rhs.message.id
    }

    /// 追加2
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}

UITableViewDataSourceをUITableViewDiffableDataSourceに変更する

struct Section {
    let items: [Item]
}

このようなセクションがあると仮定して

class SomeClass: UITableViewDataSource {

    let sections: [Section]

    func numberOfSections(in tableView: UITableView) -> Int {
        return sections.count
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return sections[section].items.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        /// cellをデキューして加工して返す処理
    }
}

というUITableViewDataSourceの実装があった場合の変更点を示します。

まず、Section をHashableに適合させます。

次に、UITableViewDataSourceの代わりにUITableViewDiffableDataSourceを使うように変更します。

class SomeClass {
    private var dataSource: UITableViewDiffableDataSource<Section, Item>?

    func setupDataSource(tableView: UITableView) {
        dataSource = UITableViewDiffableDataSource<Section, Item>(tableView: tableView, cellProvider: { [weak self] (tableView, indexPath, item) -> UITableViewCell? in
            /// func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCellと一緒の内容
        }
    }

    func setupSnapshot() {
        var snapShot = NSDiffableDataSourceSnapshot<Section, Item>()
        let sections: [Section] = /// 省略
        snapShot.appendSections(sections)
        sections.forEach { snapShot.appendItems($0.items, toSection: $0) }
        dataSource.apply(snapShot)
    }
}

setupDataSourceはViewControllerのViewDidLoad、setupSnapshotsetupDataSourceより後でデータが取得できたタイミングで実行すれば良いと思います。

また、変更がある場合は現在のスナップショットをdataSourceから取得できるので、それに対して変更を加えて再度applyするだけで良いです。

performBatchUpdateなどの処理

dataSourceにapplyしたらTableViewにも反映されるので不要になります。

結果

日に数件エラーが出ていたのですが、0件になりました。

今回はTableViewに対しての改善でしたがCollectionViewにも同様のAPIが存在するので、そちらも改善していきたいと考えています。

参考

https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/implementing_modern_collection_views

Appleが提供しているサンプルプロジェクトです。 2021年3月18日時点では WiFiSettingsViewControllerTableViewEditingViewController がUITableViewDiffableDataSourceを使っているので参考になると思います!

API Serverの新規開発時に導入してみて良かった事

f:id:fkymev:20210511174549p:plain

はじめに

DELISH KITCHEN開発部の福山です。 社内向けシステムとしてAPI Serverを新規に構築する機会がありました。新規開発にあたり導入してみて良かった事をいくつかご紹介したいと思います。

前提技術スタック

新規GitHubリポジトリを用意してREST APIをGoで実装する。Webフレームワークはechoを採用。インフラはAWS ECS、RDSを利用。ログ情報はfluentd経由でS3やTreasuredataに格納しています。SentryやDatadogによる監視も行っています。

pre-commit、CIでのLintチェック、パッケージをクリーンアーキテクチャ構成にする

pre-commit

『DELISH KITCHEN』のメインAPI Serverの方で途中から採用された内容とほぼ同じ内容となります。今回は実装初期段階でpre-commit を導入しました。 pre-commitを使えばローカルでのGit commit時に任意のスクリプトを実行出来ます。

以下の処理が行われる様に設定されています。

- go generate
- go vet
- gofmt
- goimports
- golint
- wire

主に実装内容の静的解析を行っておりcommit前にルールから外れたコードを発見し修正を促します。 その他go generate契機でmockファイルの作成や wire でDIコードの生成処理(*後述)を実行しています。

良かった事

コードレビュー時にレビュアーがコードフォーマットや生成ファイルの有無等の指摘をする必要が無くなり、仕様やバグのチェックに集中出来る様になりました。

CIでのLintチェック

自動テストは導入しているのですが、更に実装初期段階からGithub Actionsにて reviewdog/golangci-lint を導入しました。 golangci-lintには 様々なLinter が用意されておりプロジェクト状況に合わせて任意のLinterを利用出来ます。

今回の開発では以下のLintチェックを有効化しています。

- bodyclose
- deadcode
- depguard
- errcheck
- goconst
- gocritic
- gofmt
- goimports
- gosec
- gosimple
- govet
- ineffassign
- interfacer
- misspell
- nakedret
- noctx
- prealloc
- scopelint
- staticcheck
- structcheck
- typecheck
- unconvert
- unparam
- unused
- varcheck

pushした内容に指摘対象のコードが存在する時にGithub上で以下の様に表示してくれます。

f:id:fkymev:20210511162002p:plain
reviewdog/action-golangci-lint

良かった事

pre-commitと同じなのですがレビュアーに指摘される前に自動チェックが行われる為、コードレビュー時に仕様やバグのチェックに集中出来る様になりました。 具体的にはgosecでセキュリティ面での指定が有ったり、gosimpleでシンプルなコードの書き方に気付かされたりとコードの品質向上のきっかけとなります。 特に実装初期段階から導入した事によりほぼ全てのコードが随時チェックされている事になるので途中から採用するよりオススメです。

パッケージをクリーンアーキテクチャ構成にする

既存のシステムでMVC的な構成で苦労する場面が有りました。Goでdomain/model的なパッケージとデータの永続化処理は切り離したいと考え今回はクリーンアーキテクチャ構成で実装する事にしました。
(色々なクリーンアーキテクチャの詳細解釈が存在するのであくまで一例とお考え下さい。)

ざっくり過ぎる図解なのですが以下の様なパッケージ構成となっております。(→は依存方向)

f:id:fkymev:20210511162214p:plain
パッケージレイヤー構成の一例

最低限下記は意識しつつ、ルールに拘り過ぎて工数が掛かり過ぎない様に状況に応じて詳細実装を進めました。

- 依存方向を守る(domainは他に依存しない)
- 抽象に依存する(interfaceに依存する)

handlerパッケージから wire で生成されたコードでDependency Injectionされる構成となっております。 動作のポイントとしてはdomain/repositoryで定義されたinterfaceの詳細実装はinfrastructureに存在しており、注入された内容として実行される様になっています。 他にはWebフレームワークの固有処理もhandler内に閉じておりusecase以降には影響しない様になっています。

良かった事

  • テストの容易性

一般的に言われている事ですがテスタブルになりました。 MVC構成だと要件ロジックのユニットテストを書く時も依存するデータベース情報等を実際に用意する必要が有りました。しかし抽象に依存しつつパッケージレイヤーを切った事により、例えばusecaseでのユニットテスト時にパッケージ内で扱う永続化処理に対してmockを利用し任意の結果が返却出来る事になります。 従ってテスト対象パッケージ以外の状態に悩まされる事無く確認したい要件ロジックに集中してユニットテストを行う事が出来る様になりました。

  • ドメイン情報の認識が深まる

単一パッケージにほぼ全てのロジックを詰める様な形だと肥大したり処理の責務等は考えなくなってしまう可能性が高いのですが、 要件ロジックをどのパッケージレイヤーに書くべきかを個人的にもチーム内でも意識する様になりました。 実際にPRレビューの時にチームメンバーと議論する事が有り、結果として当初より見通しの良いコードになる事が有りました。 この部分は個々人の解釈の違いも有るので工数が膨らみ過ぎないバランスで進める様にしています。

まとめ

新規開発を行うタイミングで導入して良かった事としてpre-commitCIでのLintチェックパッケージをクリーンアーキテクチャ構成にするをご紹介しました。
工数面のバランスや未経験な技術要素を導入するリスクも有りますが開発工程初期に開発効率を向上させる仕組みを用意するメリットは大きいと考えています。 まだ開発は続きますので良い仕組みを活かして効率良くアウトプットしていきたいと思います。