Django ORM、geventおよび緑の熊手

多くの人がそのシンプルさのためにDjangoを選択しています。 Djangoのコードはシンプルで簡潔です。松葉杖についてはあまり考えず、ビジネスロジックについてはもっと考えます。



Geventは、シンプルで非常にスマートで、コールバックヘルルを持たないという事実からも選択されています。



私の頭には、2つのシンプルで便利なものを組み合わせるという素晴らしいアイデアがあります。 Djangoにパッチを適用し、シンプルさ、簡潔さ、パフォーマンスを喜ばせ、他のサイトに多くのリクエストを行い、サブプロセスを作成します。一般に、新しい非同期Djangoを最大限に使用します。

しかし、それらを組み合わせて、私たちは静かにいくつかの熊手を設定しました。





Django ORMおよびDB接続プール





Djangoは、同期アプリケーションのフレームワークとして作成されました。 各プロセスはユーザーからリクエストを受信し、それを完全に処理し、結果を送信します。その後のみ、別のリクエストの処理を開始できます。 蒸しカブのように、操作の原理は簡単です。 ブログやニュースサイトを作成するのに最適なアーキテクチャですが、もっと欲しいものがあります。



HTTP要求で多忙なアクティビティをシミュレートする簡単な例を見てみましょう。 リンクを短縮するためのサービスにしましょう:



# testproject/__init__.py __import__('gevent.monkey').monkey.patch_all() # testproject/main/models.py from django.db import models class LinkModel(models.Model): url = models.URLField(max_length=256, db_index=True, unique=True) # testproject/main/views.py import urllib2, httplib from django.core.urlresolvers import reverse from django.http import HttpResponse, HttpResponseRedirect from .models import LinkModel def check_url(url): request = urllib2.Request(url) request.get_method = lambda: 'HEAD' try: response = urllib2.urlopen(request) except (urllib2.URLError, httplib.HTTPException): return False response.close() return True def remember(request): url = request.GET['url'] try: link = LinkModel.objects.get(url=url) except LinkModel.DoesNotExist: if not check_url(url): return HttpResponse('Oops :(') link = LinkModel.objects.create(url=url) return HttpResponse('http://localhost:8000' + reverse( go_to, args=(str(link.id).encode('base64').strip(), ))) def go_to(request, code): obj = LinkModel.objects.get(id=code.decode('base64')) return HttpResponseRedirect(obj.url)
      
      







geventがなければ、このコードは非常に遅く動作し、2つまたは3つの同時リクエストをほとんど処理しませんが、geventを使用するとすべてが飛ぶようになります。



uwsgiを介してプロジェクトを開始します(Pythonサイトを展開する際に事実上標準になりました:



 uwsgi --http-socket 0.0.0.0:8000 --gevent 1000 -M -p 2 -w testproject.wsgi
      
      







リンクを短縮するための10個のリクエストを同時にテストして喜ばせようとします。すべてのリクエストは、最短時間でエラーなく実行されます。



新しいサービスを開始し、着席してその成功した開発を確認します。 負荷は10から75の同時要求に増加し、彼はそのような負荷を気にしません。



突然、次の内容を含む数千通の手紙の1つがメールで届きます。



トレースバック:
    ...
    > link = LinkModel.objects.get(url = url)
 OperationalError:FATAL:残りの接続スロットは、レプリケーション以外のスーパーユーザー接続用に予約されています




これは、デフォルトのUbuntu / Debian構成を使用した場合、次のようなメッセージを含む1000通のメールを受け取るため、 en_US.UTF-8ロケールをpostgresql.confに設定すると便利です。



 OperationalError:?????:??????????  ?????  ????????????  ????????????????  ???  ????????????  ?????????????????????  (?? ??? ???????????)




アプリケーションが作成したデータベース接続が多すぎるため(デフォルトでは最大100接続)、処罰されました。



最初の落とし穴は次のとおりです。Djangoにはデータベース接続プールがありません 。同期コードでは単純に必要ないからです。 単一の同期Djangoプロセスはリクエストを並行して処理できません;一度に1つのリクエストのみを処理するため、複数のデータベース接続を作成する必要はありません。

実際に...
実際、Djangoは1つのプロセスが複数のリクエストを処理できるマルチスレッドモードで動作できます。 このようなサーバーでは、 manage.py runserverコマンドが起動しますが、ドキュメントでは、このモードは戦闘での使用にはまったく不適切であると記載されています。




唯一の解決策があります。データベース接続のプールが緊急に必要です。



Djangoのプール実装は比較的少なく、たとえばdjango-db-pooldjango-psycopg2-poolです。 最初のプールはpsycopg2.TreadedConnectionPoolに基づいており 、空のプールから接続を取得しようとすると例外がスローされます。 アプリケーションは以前と同じように動作しますが、他のアプリケーションはデータベースへの接続を作成できます。 2番目のプールはgevent.Queueに基づいています 。空のプールから接続を取得しようとすると、別のグリーンレットが接続をプールに入れるまで、グリーンレットはブロックされます。

ほとんどの場合、2番目のソリューションをより論理的に選択します。



Greenlets内のデータベースクエリ



既にgeventを使用してアプリケーションにパッチを適用しており、同期呼び出しはほとんどありません。グリーンレットを最大限に活用してみませんか。 複数のHTTPリクエストを並行して作成したり、サブプロセスを作成したりできます。 グリーンレットでデータベースを使用したい場合があります。



 def some_view(request): greenlets = [gevent.spawn(handler, i) for i in xrange(5)] gevent.joinall(greenlets) return HttpResponse("Done") def handler(number): obj = MyModel.objects.get(id=number) obj.response = send_http_request_somewhere(obj.request) obj.save(update_fields=['response'])
      
      







数時間経過し、突然アプリケーションの動作が完全に停止しました。リクエストに対して504 Gateway Timeoutが発生します。 今回は何が起こったのですか? 明確にするために、いくつかのDjangoコードを読む必要があります。



すべての接続はdjango.db.connectionsによって保存されます。これはdjango.db.utils.ConnectionHandlerクラスのインスタンスです。 ORMは、リクエストを作成する準備ができたら、 connections ['default']を呼び出してデータベースへの接続をリクエストします。 ConnectionHandler .__ getattr__は、 ConnectionHandler._connectionsの接続を順番にチェックし、空の場合、新しい接続を作成します。



開いた接続はすべて、使用後に閉じる必要があります。 django.http.HttpResponseBase.closeで実行されるrequest_finishedシグナルがこれを行います。 Djangoは、誰も接続しない最後の瞬間にデータベース接続を閉じますが、これは非常に論理的です。



キャッチは、ConnectionHandlerがデータベース接続を保存する方法です。 これを行うために、彼はthreading.localを使用します 。これは、マンキング後にgevent.local.localに変わります。 宣言されると、このデータ構造はすべてのグリーンレットで一意であるかのように機能します。 Some_viewコントローラーが1つのグリーンレットで処理され始め、 ConnectionHandler._connectionsにはすでにデータベース接続があります。 ConnectionHandlers._connectionsが空であることが判明したいくつかの新しいグリーンレットを作成し、これらのグリーンレットに対してプールからの接続がさらに取得されました。 新しいグリーンレットが消え、 ローカル()のコンテンツが消え、データベースへの接続が回復不能に失われ、誰もプールに戻せなくなりました。 時間が経つにつれて、プールは完全に空になります。



Django + geventで開発するときは、常にこのニュアンスを覚えて、 django.db.close_connectionを呼び出して各グリーンレットの最後でデータベース接続を閉じる必要があります。 例外が発生した場合でも呼び出す必要があります。例外に対しては、小さなdecorator-contextmanagerを使用できます。



そのようなデコレータの例
 class autoclose(object): def __init__(self, f=None): self.f = f def __call__(self, *args, **kwargs): with self: return self.f(*args, **kwargs) def __enter__(self): pass def __exit__(self, exc_type, exc_info, tb): from django.db import close_connection close_connection() return exc_type is None
      
      









このラッパーを賢明に使用する必要があります:greenletsの各スイッチの前(たとえば、urllib2.urlopenの前)にすべての接続を閉じ、 Model.objects.all()のようなイテレーターを介して接続が不完全なトランザクションまたはループ内で閉じられないことも確認してください



Django ORMはDjangoとは別に使用します



cronまたはCeleryの類似物を作成すると、同じ問題を理解できます。cronまたはCeleryは、時々データベースにクエリを実行します。 gevent.WSGIServerを使用してDjangoをリフティングし 、Django ORMが使用する異なるプロトコルのサービスを同時に起動する場合も、同じことが待っています。 主なことは、データベースプールへの接続を時間通りに戻すことです。そうすれば、アプリケーションは安定して動作し、喜びがもたらされます。



結論



この投稿では、データベース接続プールを使用する必要があり、使用後すぐに接続をプールに戻す必要があるという基本的なルールについて説明しました。 geventとpsycopg2のみを使用している場合は、これを必ず検討してください。 しかし、Django ORMは、開発者がデータベース接続を処理しないような高レベルの抽象化で動作し、時間が経つにつれて、これらのルールは忘れられ、再発見される可能性があります。



All Articles