くらしのマーケット開発ブログ

「くらしのマーケット」を運営する、みんなのマーケット株式会社のテックブログです

Redisのコネクション数を10分の1にした話

はじめに

こんにちは、バックエンドエンジニアのtakayukiです。
私のあるリリースをきっかけとしてRedisのコネクション数が増加しました。 この記事にはRedisのコネクション数が増加した際の調査内容と修正の備忘録を書きます。

起きたこと

くらしのマーケットでは、非同期にタスクを実行する仕組みとしてCeleryというタスクキューを使用し、メッセージングキュー(メッセージブローカー)にはRedisを採用しています。

先日、このCeleryを利用した機能をリリースしたのですが、リリースの翌日にRedisのコネクション数が一定の基準値を超え、障害検知のアラートが鳴りました。
※くらしのマーケットではAWSのElastiCacheのRedisを使用しており最大接続数は65000ですがアラートは余裕をもって設定しています。

Redisのkeyとサイトへのリクエストを調査すると、私のリリースでCeleryのtaskを呼ぶ回数が増加し、それに伴ってコネクション数も増加していました。

調査

くらしのマーケットではバックエンドにPython製webフレームワークのDjangoを使用しており、そこからCeleryを経由してRedisにエンキューしています。 今回の私が作成したエンドポイントや他のエンドポイントでは、以下のようなCeleryのラッパークラスをインスタンス化してエンキューしていました。

from datetime import datetime
from celery import Celery

class CeleryClass(object):
    def __init__(self):
        self.app = Celery()
        self.app.config_from_object("config")

    def send_task(self, task_name: str, args: any, queue: str, eta: datetime=None):
        self.app.send_task(task_name, args=args, queue=queue, eta=eta)

# 使用するとき
Celery = CeleryClass()
Celery.send_task("SampleTask", args=[""], queue="user")

(一部名前など改変しています)

擬似コードなどを作って調べたところ、CeleryCelery()インスタンス化される時にコネクションを接続しているようです。しかし、このクラスを見ると共通クラスがインスタンス化される度にCelery()を呼んでいます。
どうやらこれが原因のようです。

解決策

解決策としては以下の2つが考えられました。

  1. Celeryにコネクションを明示的に閉じるコマンドが存在するので、コネクションが貼られる度に閉じる
  2. 生成するインスタンスを1つに制限するSingletonパターンを参考にしてCelery()インスタンスを使い回す

Celeryにはコネクションプール機能があり、明示的に閉じてしまうとその機能を生かせないため、2を選択し以下のように修正しました。

from datetime import datetime
from celery import Celery

class CeleryClass(object):
    __instance = None
    @staticmethod
    def get_instance():
        if CeleryClass.__instance is None:
            CeleryClass()
        return CeleryClass.__instance

    def __init__(self):
        if CeleryClass.__instance is not None:
            raise Exception("error")
        else:
            self.app = Celery()
            self.app.config_from_object("config")
            CeleryClass.__instance = self

    def send_task(self, task_name: str, args: any, queue: str, eta: datetime=None):
        self.app.send_task(task_name, args=args, queue=queue, eta=eta)

# 使用するとき
Celery = CeleryClass.get_instance()
Celery.send_task("SampleTask", args=[""], queue="user")

get_instanceインスタンスがあれば使い回し、なければ__instanceに保存するようにして、既存コードの修正を最小限にしました。

結果

結果として、コネクション数10分の1に少なくなりました。
一度下がった後にコネクション数が上がることなく推移したため、今回の修正で解決だと考えられます。

感想

今回の修正で無事Redisのコネクション数を下げることができました!
知識で知ってたことを実際に修正+体験して勉強になりました!

私たちテックチームでは「くらしのマーケット」を一緒に作る仲間を募集しています。 一緒にくらしのマーケットを作っていきたい!という方はコーポレートサイト https://www.minma.jp/ までお気軽にご連絡ください!