こんにちは。商品開発部 入社2年目の中村です。
私は主に、弊社が提供しているサービス「現場クラウドConne」(以下、Conne)の開発に携わっております。
今回は、半年かけて Conne のパフォーマンス改善に取り組んだ話をご紹介いたします。
現場クラウドConne とは
Conne とは、一言でいうと「建設産業向けのビジネスSNS」です。
機能の一つにメッセージをやり取りできるものがあり、Facebook や Twitter のようにスレッド形式でやり取りをおこなえます。
Conne では現在、一日におよそ1200件のやり取りがおこなわれています。
データがどんどん膨大になると、処理の仕組みや体制を見直す必要が出てきます。
今後の利用者数増加に向けて先手を打とうということで、パフォーマンス改善に取り組み始めました。
話を進めるうえでの前知識
改修前のデータの作りをご説明します。
Conne では階層的にメッセージのやり取りができ、それぞれのメッセージは一意のidや本文の内容、どのメッセージに対するメッセージなのかという情報などを保持しています。
本記事では「id:1 のメッセージは id:2 の親メッセージ」「id:3, 4 は id:2 の子メッセージ」「id:1 は id: 2, 3, 4, 5 の先祖メッセージ」のように、メッセージの関係性を親子関係を用いてお話しさせていただきます。
改修内容
データベースに新規データを追加して再帰処理を減らす
先輩社員に「再帰処理が重いので改善してみてください」とアドバイスをいただいたので、「再帰って重いんだー」と思いつつ『python 遅い』とGoogle で検索したり、関数の速度を調査したりして、どんな処理が遅いのかを調査しました。
結果、再帰はやはり遅いということが分かったので、実際に再帰処理を減らす改修を始めました。
Conne では、テキスト検索時、検索にヒットしたメッセージが存在するスレッドを検索結果として表示します。
そのため、ヒットしたメッセージから元投稿を取得し、取得した元投稿からスレッド内のメッセージを取得する必要があります。
データの構造上、検索に引っかかったメッセージからは親しかわからないため、最初の投稿を取得するために再帰処理を使って一番上の親(以後、先祖メッセージ)までたどっていました。
def get_ancestor_message(message): parent_id = parent_message.get("parent_id", None) if not parent_id: return message parent_message = db.messages.find_one({"id": message["parent_id"]}) return get_ancestor_message(parent_message)
ここで、それぞれのメッセージに「先祖メッセージのid」を持たせることで、都度再帰処理をせずとも一発で最初の投稿を取得することができるようになりました。
def get_ancestor_message(message): ancestor_message = db.messages.find_one({"id": message["ancestor_id"]}) return ancestor_message
検索に引っかかるメッセージの数は、キーワードによってはかなり多くなります。
余計な find の実行回数を減らすことで、最小の負荷で目的のデータを取得することができました。
マイグレーションをおこなう
新しい検索ロジックを過去のデータでも使えるように、データマイグレーションを行ないました。
新しく登録されるデータには先祖メッセージの id が登録されるようになりました。
しかし、今までのデータには登録されていないため、過去のメッセージにも先祖メッセージのid を登録する必要があります。
専用のスクリプトを書いて、メッセージデータに対してマイグレーションを実行しました。
データ量が非常に多いため、マイグレーション自体も完了までおよそ45分かかりました 汗
インデックスについて
新しく設定したフィールドにインデックスを設定しました。
db.collection.create_index(“new_field_name”, 1)
また、テキスト検索に有効なテキストインデックスも検討していましたが、MongoDB では日本語に対応しておらず見送りとなりました。
https://docs.mongodb.com/manual/reference/text-search-languages/#std-label-text-search-languages
結果どれくらい改善できたか
メッセージデータの総数がおよそ37万データある中から、ユーザーが閲覧可能なメッセージを取得にかかる時間を計測した結果、改修前が 2300 ms 、改修後は 1900 ms とおよそ 20% 高速化することができました。
今回の開発を通じて学んだこと
全体を見通した開発が大事
Conne に携わって半年時点での改修でしたので、まだ理解しきれていない部分も多かったです。
どこがどういうロジックでデータを処理するか、db構造はどうなっているのか、改めて製品理解に努めました。
最初は「メッセージ取得時の再帰処理をなくしてパフォーマンスをあげよう!」と張り切っていましたが、有効な手段はそれだけではありませんでした。メモリを節約したり、そもそものdb構造を工夫したり、アプローチの方法がさまざまあることを学べました。
製品全体を見通し、どこが大きな問題になっているのか、改修によってどれくらいの効果を得られるのかをよく考えることが大切だと感じました。
データを扱う上での基本的な概念
私は今まで、データベースに対して find や update など基本的なメソッドを実行することしかしていなかったので、RDBMS のように外部キーや型などのデータの整合性をとるためのバリデーションが必要なことを初めて知りました。
また、NoSQL だからこそ db構造を柔軟に変化させられたり、マイグレーションでデータを更新する必要があったりと、データベースの特性やデータを管理するうえで必要な処理をいろいろ知ることができました。
さいごに
今までは目的の動作ができるプログラムを書くことしかしておりませんでした。
負荷について考えるにあたり、「今までなんとなく使ってたもの」を深いところまで知ることができ、非常に良い経験ができました。
今後、より使いやすく便利な Conne にするために、日々新しいことにチャレンジしてゆきます!