フロントエンドキャッシング:Flask、Nginx + Memcached + SSI

長い間、このトピックに関する次の記事に出会いました。



私はPHPの友達なので、例を試してみて、動作することを確認しました。 しかし、これにはすべて「致命的な欠陥」がありました:)-私はPythonファンであり、主にバックエンドで働いています。 真剣に言えば、それを実践することは不可能でした。



しかし、年の初めに、1つの野心的なプロジェクトに参加するという提案が寄せられました。当初は、このオペラのHiLoadやその他のパンを意味していました。 事業計画が策定されている間、投資家はそのようなものを探していたので、私は、私の意見では、キャッシュの問題を含むこの作業に役立つ問題を研究することにしました。



まず、ドラフトソリューションが、キャッシングにVarnish + ESIスタックを使用して、お気に入りのFlaskフレームワークに実装されました。 それは機能し、良い結果さえ示しました。 後にVarnishが「特別なプレーヤー」であり、 Nginx + Memcached + SSIバンドルを使用すると、すべて同じでさらに柔軟性が高いことが理解できるようになりました。 このオプションも作成され、パフォーマンスに特別な違いはありませんでしたが、後者はより柔軟で管理しやすいように見えました。



そのプロジェクトは滑走路までもタクシーもタクシーもしませんでした。 考えた後、私は「コードをとかす」ことを決め、それをオープンソースに入れ、一般の人が判断するようにしました。



ページフラグメントのキャッシュの原理については詳しく説明しません。 上記の記事では、それについて詳しく説明されており、GoogleとYandexがさらに多くの情報を見つけるのに役立ちます。 特定の実装にもっと焦点を合わせようとします。 私の場合、これらはNginx + Memcached + SSIと、私が書いた拡張機能を使用したFlaskです。



簡単に言えば、この原則はいくつかの文章で説明されています。 Webページのフラグメントを生成する関数の結果は、通常このフラグメントに一意に対応するURIの形式で表されるキーとともにmemcachedに配置され、 <!-#include virtual = "<URI>"->という形式の行がページ自体に表示されます。 <URI>-実際のコンテンツがキャッシュに入れられるキー値。 さらに、プロキシ中にこの命令に遭遇した「特別に訓練された」Nginxは、memcachedサーバーから直接受信した実際のコンテンツに置き換えます。



典型的なサイトの例を考えてみましょう。各ページには、ユーザーへの挨拶と、ユーザーが作成した投稿とコメントの数が表示されるブロックがあります。 ユーザーメッセージの数を数えるのはかなりコストのかかる操作であり、フレンドグラフもそこに表示すると、データベースを大幅に消耗するフラグメントが1つだけになり、ページ全体の読み込み速度が大幅に低下します。 しかし、方法があります! 上記のようにこのブロックのコンテンツをキャッシュすることができ、ユーザーがアルバムの新しい写真を開くたびにデータベースへのクエリが作成されることはありません。 Nginxはこのブロックにバックエンドに「負担をかけない」ようにします。 ユーザーが新しい投稿を作成したかコメントを書き込んだ場合、アプリケーションはキャッシュ内のコンテンツを更新します。



このアプローチは、アプリケーション自体がキャッシュからデータを選択し、Nginxがページのデータを表示するという典型的なアプローチとは異なり、Nginxがその役割を果たします。 これは、私が知っているフレームワークのどれにでもコンテンツを返す速度において比類のないものです。



実用部



拡張コードは本当に哲学的ではありません。私はFlask-Fragmentと呼ばれ、MITライセンスの下でGithubに公開されました 。 テストやドキュメントはありませんが、ブログの「ライト」バージョンを表すかなり機能的なデモアプリケーションがあります。 私以外の誰かに興味があれば、APIの拡張、Varnish + ESIオプションのサポート、そしてもちろんテストとドキュメントを作成する予定です。



キャッシュを有効にする


フラグメントとその後のキャッシュを選択するには、ページの必要な部分のみを生成する関数を作成する必要があります。 フラグメントデコレータによるフラグメント生成の原因としてマークします。 Flask-Fragment拡張機能はその機能を担当し、接続する必要があります。 このような関数は、 以下フラグメントビューと呼び、必要なパラメーターを取得できます。出力では、Webページへの挿入に適したコンテンツを提供する必要があります。

 from flask import Flask from flask.ext.fragment import Fragment app = Flask(__name__) fragment = Fragment(app) @fragment(app, cache=300) def posts_list(page): page = int(page) page_size = POSTS_ON_PAGE pagination = Post.query.filter_by().paginate(page, page_size) posts = Post.query.filter_by().offset((page-1)*page_size).limit(page_size).all() return render_template('fragments/posts_list.html', pagination=pagination, posts=posts)
      
      





メインページのテンプレートでは、フラグメントコールは次の形式で発行されます。

 <div class="content"> {% block content %} {{ fragment('posts_list', page) }} {% endblock %} </div>
      
      





これで、 page=2



フラグメントが最初に呼び出されたときに、 posts_list



関数の結果がfragment:/_inc/posts_list/2



とともにmemcachedキャッシュに入れられ、Nginxの命令がページに挿入されます。 次のようになります。

 <div class="content"> <!--# include virtual="/_inc/posts_list/2" --> </div>
      
      





さらに、 fragment:fresh:/_inc/posts_list/2



キーfragment:fresh:/_inc/posts_list/2



、値1がmemcachedに配置されますposts_list



関数の呼び出しをインターセプトする拡張機能は、このキーがキャッシュにあり、値が0より大きい間、コンテンツの生成を開始しません。



キーfragment:/_inc/posts_list/2



TTL fragment:/_inc/posts_list/2



は300( fragment



デコレータのcache



パラメータで定義)+設定で指定された値FRAGMENT_LOCK_TIMEOUT、デフォルトでは180に設定されます。キーfragment:fresh:/_inc/posts_list/2



TTL fragment:fresh:/_inc/posts_list/2



のみ設定値は300です。その後、Nginxは、命令で<!--# include virtual="/_inc/posts_list/2" –>



命令を満たし、アプリケーションに480秒間アクセスすることなく、memcachedキャッシュからこのフラグメントのコンテンツを取得します。 原則として、NginxはTTLの有効期限を待たず、 fragment:fresh:/_inc/posts_list/2



キーが存在しなくなると、アプリケーションは300秒後にコンテンツを更新します。



フラッシュキャッシュ


そのため、フラグメントはキャッシュされます。 ちなみに、上記の例はFlask-Fragmentパッケージに付属しているデモアプリケーションから取得したもので、各コメントの数を含む投稿のリストを生成します。 したがって、ユーザーが投稿またはコメントを追加したとき、キャッシュ内のリストの内容は関係ありません。 更新する必要があります。 以下は、投稿が追加されたときに呼び出されるフラスコビューの例です。

 @app.route('/new/post', methods=['GET', 'POST']) @login_required def new_post(): form = PostForm() if form.validate_on_submit(): form.post.author_id = current_user.id db.session.add(form.post) db.session.commit() fragment.reset(posts_list) fragment.reset(user_info, current_user.id) flash('Your post has saved successfully.', 'info') return redirect(url_for('index')) return render_template('newpost.html', form=form)
      
      





ここでは、 fragment.reset



メソッドの呼び出しが2つあります。 最初のfragment.reset(posts_list)



はフラグメントビューposts_list



キャッシュをフラッシュし、2番目のfragment.reset(user_info, current_user.id)



表示するため、記事の冒頭で例として挙げたユーザーグリーティングでそのブロックのキャッシュをフラッシュします投稿とユーザーのコメントの数。 このフラグメントは、URI / _inc / user_info / 21によって一意にアドレス指定されます。最後の桁はユーザーのユーザーuserid



。 拡張機能は、独自にキーリセットを編成し、 fragment.reset



渡されたパラメーターに基づいてそれを形成します。



最初のケースでは事態はさらに悪く、そこではページネーションが使用され、投稿のリスト用に現在形成されているページと同じ数のリセット可能なキーがあります。 たとえば、 fragment:fresh:/_inc/posts_list/2



、これは2番目のページをリセットするための唯一のキーです。 ここでは、より高い精神の介入なしで行うことはできません。 以下は、特定のキャッシュリセットフラグメントview posts_list



を実行する関数コードです。

 @fragment.resethandler(posts_list) def reset_posts_list(): page_size = POSTS_ON_PAGE pagination = Post.query.filter_by().paginate(1, page_size) for N in range(pagination.pages): fragment.reset_url(url_for('posts_list', page=N+1))
      
      





ここでは、 fragment.reset_url



メソッドを使用して投稿リストの各ページのキャッシュがフラッシュされる「カスタム」ハンドラーを定義するfragment.resethandler



デコレーターを使用します。



結論として、コードの別のブロックを紹介します。これらは、フラスコ拡張自体のメソッドであり、フラグメントのコンテンツの形成とキャッシュへの書き込みに関連する機能の重要な部分を示しています。

 def _render(self, url, timeout, deferred_view): if self.memcache and timeout: if not self._cache_valid(url): self._cache_prepare(url, timeout, deferred_view) return jinja2.Markup('<!--# include virtual="{0}" -->'.format(url)) else: return jinja2.Markup(deferred_view()) def _cache_valid(self, url): return bool(self.memcache.get(self.fresh_prefix+url) or False) def _cache_prepare(self, url, timeout, deferred_view): successed_lock = self.memcache.add(self.lock_prefix+url, 1, self.lock_timeout) if successed_lock: result = Compressor.unless_prefix+(deferred_view()).encode('utf-8') self.memcache.set(self.body_prefix+url, result, timeout+self.lock_timeout) self.memcache.set(self.fresh_prefix+url, 1, timeout) self.memcache.delete(self.lock_prefix+url)
      
      





ご覧のとおり、ロックキーを作成しようとします。 これにより、競合状態が防止されます。 キャッシュ内の情報の更新にはロックを設定することができたスレッドが1つだけで、残りはサイレントスクリプトを実行し、古いデータをクライアントに返します。



おわりに


何を得たの? そして、フロントエンドとデータベースの深刻なアンロードが行われました。これは、デモアプリケーションがDebugToolbarで実行されているときにはっきりと表示されます。 後で、ブログユーザーが投稿またはコメントを追加するためのリクエストの5%のみを生成し、残りは表示しているという仮定に基づいて、負荷テストをリポジトリに配置する予定です。 ただし、2〜3ダースの投稿をそれぞれ2〜3ダースのコメントで埋めると、脆弱な仮想マシンではすでに違いが顕著になっています。



configのFRAGMENT_CACHING



パラメーターの値をFalse



設定すると、キャッシュをオフにできます。 この場合、アプリケーションはNginxを介してプロキシせずに動作でき、拡張機能はフラグメントの実際のコンテンツを独自に挿入します。



ご清聴ありがとうございました。この記事が、Pythonが好きなWebプログラマーだけでなく、Webアプリケーションのパフォーマンスの改善に関心があるすべての人にとっても興味深いものであったと思います。 また、すばらしいFlaskフレームワークの普及に貢献したことを願っています。



All Articles