1. ホーム
  2. パイソン

django querysetのキャッシュ最適化を記憶する

2022-03-02 02:28:18
<パス

原点

私たちの単一のテーブルのレコードを見てほぼ7000万行、画期的な億行は、角を曲がったところにされているので、データベーステーブルの意図は、アプリケーション層は比較的透明なアプローチは、データベースミドルウェアを導入することです、アリ、360などのいくつかのプログラムがありますが、それらのほとんどは、JavaやC、および最後に見ています。 https://github.com/flike/kingshard これは、golangによって書かれている、コードの品質は、私の好みに合わせて、まだ非常に高いので、使い切るためにプロキシとして最初のキングハードは、前に発見されていなかった問題を発見した。

django はデータベース接続プールさえ持っていません。

kingshardをプロキシとして使ってみたところ、kingshardに接続しているソケットのステータスがtime_waitで、kingshard to mysqlだけが常にestablishになっていたので、気になってdjangoがデータベース接続プーリングを使っていないのではないかと疑っています(もちろん、djangoは1.4という古いバージョンを使っているのですが)。CONN_MAX_AGE というパラメータは、バージョン 1.6 以降でのみ利用できるようです。詳しくは、以下を参照してください。 http://www.huochai.mobi/p/d/752667/?share_tid=85a118bfb04b&fmid=0

django get(filter) を queryset の中で再度実行すると、SQL クエリが再度起動します。

for register_day_obj in self.data['UserRegisterDayObj']:
    xxxxxx
    pay_day_obj = self.data['UserPayDayObj'].get(gameid=register_day_obj.gameid)
    xxxxxx

self.data はすべて django queryset 内にあり、sql クエリの数だけ for ループが送られることになり、これは許容できません。

なぜ、私たちのプロジェクトでは、このようなことが発見されなかったのでしょうか?主に、私たちのシステムはバイシステムで、すべてのクエリが一度に多くのデータを取得し、すべてインデックス付きのクエリであるため、1つの大きなSQL文と100の分割SQL文はクエリにほぼ同じ時間がかかり、クエリが非常に高速であるため、遅いデータベースクエリも誘発されていないためです。これはまた、orm が発行された sql 文をチェックする必要があることを示しています、django はデバッグの設定も提供しています。

修正した場合はどうでしょうか?つまり、最初の for は sql リクエストを発行し、それ以降の for ループは要求されたキャッシュを使用します。
これは簡単で、カスタム cache_filter,cache_get メソッドを使用します。

def cache_filter(queryset, **option):
    """queryset cachefilter"""
    return [query for query in queryset if all(getattr(query, k) == v for k, v in option.iteritems())]


def cache_get(queryset, **option):
    """queryset cache get"""
    result = cache_filter(queryset, **option)
    if len(result) ! = 1:
        raise ValueError("get should return one record, but now:%s" % len(result))
    return result[0]

特に、辞書を使って最適化できる外側のforループがあるため、forループで実行すると、時間計算がO(n)になります。

def fast_cache_filter(queryset, *keys):
    """Use dictionary to speed up cache filter"""
    cache = defaultdict(list)

    for query in queryset:
        v = [getattr(query, key) for key in keys]
        cache[tuples(v)].append(query)

    def query_filter(**option):
        assert len(keys)==len(option), "filter keys and option must same"
        return cache.get([option[key] for key in keys], [])

    return query_filter


def fast_cache_get(queryset, *keys):
    """Use dictionary to speed up cache get"""
    query_filter = fast_cache_filter(queryset, *keys)

    def query_get(**option):
        result = query_filter(**option)
        if len(result) ! = 1:
            raise ValueError("get should return one record, but now:%s" % len(result))
        return result[0]

    return query_get

もちろん、2番目のオプションを使用すると、最初のものより侵襲的です。まず、fast_cache_get関数にクエリセットと取得キーを伝える必要があり、fast_cache_getは、典型的なクロージャであるcache_getの使用と同様の関数を返すことになります。

比較した結果、高速版の方が10%~20%ほど若干速いのですが、侵襲性が高いので、結局、高速版をやめてfor loop版を使っています。なぜ10%~20%しか速くならないのかについては、一言で説明できるものではなく、主観で判断せず、データで語ることが重要であることがわかります。