apeescape2.com
  • メイン
  • Kpiと分析
  • ツールとチュートリアル
  • Uiデザイン
  • 収益性と効率性
バックエンド

PythonとDjangoを使用したパフォーマンステストと最適化のガイド

ドナルド・クヌースは言った 「時期尚早の最適化はすべての悪の根源です。」 しかし、通常、負荷の高い成熟したプロジェクトでは、最適化が避けられない時期があります。この記事では、私はについて話したいと思います Webプロジェクトのコードを最適化するための5つの一般的な方法。 Djangoを使用しますが、原則は他のフレームワークや言語でも同様である必要があります。この記事では、これらのメソッドを使用して、クエリの応答時間を77秒から3.7秒に短縮します。

PythonとDjangoを使用したパフォーマンスの最適化とパフォーマンステストのガイド

サンプルコードは、私が取り組んだ実際のプロジェクトを基にしたものであり、パフォーマンス最適化手法を示しています。自分で結果を確認したい場合は、コードを初期状態で取得できます。 GitHub 記事に沿ってフォローしながら、対応する変更を加えます。一部のサードパーティパッケージはまだPython3で利用できないため、Python2を使用します。



アプリケーションの紹介

私たちのウェブプロジェクトは、国ごとの不動産オファーを追跡するだけです。したがって、モデルは2つだけです。

# houses/models.py from utils.hash import Hasher class HashableModel(models.Model): '''Provide a hash property for models.''' class Meta: abstract = True @property def hash(self): return Hasher.from_model(self) class Country(HashableModel): '''Represent a country in which the house is positioned.''' name = models.CharField(max_length=30) def __unicode__(self): return self.name class House(HashableModel): '''Represent a house with its characteristics.''' # Relations country = models.ForeignKey(Country, related_name='houses') # Attributes address = models.CharField(max_length=255) sq_meters = models.PositiveIntegerField() kitchen_sq_meters = models.PositiveSmallIntegerField() nr_bedrooms = models.PositiveSmallIntegerField() nr_bathrooms = models.PositiveSmallIntegerField() nr_floors = models.PositiveSmallIntegerField(default=1) year_built = models.PositiveIntegerField(null=True, blank=True) house_color_outside = models.CharField(max_length=20) distance_to_nearest_kindergarten = models.PositiveIntegerField(null=True, blank=True) distance_to_nearest_school = models.PositiveIntegerField(null=True, blank=True) distance_to_nearest_hospital = models.PositiveIntegerField(null=True, blank=True) has_cellar = models.BooleanField(default=False) has_pool = models.BooleanField(default=False) has_garage = models.BooleanField(default=False) price = models.PositiveIntegerField() def __unicode__(self): return '{} {}'.format(self.country, self.address)

要約HashableModelそれを継承するモデルを提供しますhashインスタンスの主キーとモデルのコンテンツタイプを含むプロパティ。これにより、インスタンスIDなどの機密データがハッシュに置き換えられて非表示になります。また、プロジェクトに複数のモデルがあり、さまざまなクラスのさまざまなモデルインスタンスを処理するために、ハッシュを解除して決定する一元化された場所が必要な場合にも役立ちます。私たちの小さなプロジェクトでは、ハッシュなしで処理できるため、ハッシュは実際には必要ありませんが、いくつかの最適化手法を示すのに役立つので、そのままにしておきます。

これがHasherですクラス:

# utils/hash.py import basehash class Hasher(object): @classmethod def from_model(cls, obj, klass=None): if obj.pk is None: return None return cls.make_hash(obj.pk, klass if klass is not None else obj) @classmethod def make_hash(cls, object_pk, klass): base36 = basehash.base36() content_type = ContentType.objects.get_for_model(klass, for_concrete_model=False) return base36.hash('%(contenttype_pk)03d%(object_pk)06d' % { 'contenttype_pk': content_type.pk, 'object_pk': object_pk }) @classmethod def parse_hash(cls, obj_hash): base36 = basehash.base36() unhashed = '%09d' % base36.unhash(obj_hash) contenttype_pk = int(unhashed[:-6]) object_pk = int(unhashed[-6:]) return contenttype_pk, object_pk @classmethod def to_object_pk(cls, obj_hash): return cls.parse_hash(obj_hash)[1]

APIエンドポイントを介してこのデータを提供したいので、Django RESTフレームワークをインストールし、次のシリアライザーとビューを定義します。

# houses/serializers.py class HouseSerializer(serializers.ModelSerializer): '''Serialize a `houses.House` instance.''' id = serializers.ReadOnlyField(source='hash') country = serializers.ReadOnlyField(source='country.hash') class Meta: model = House fields = ( 'id', 'address', 'country', 'sq_meters', 'price' ) # houses/views.py class HouseListAPIView(ListAPIView): model = House serializer_class = HouseSerializer country = None def get_queryset(self): country = get_object_or_404(Country, pk=self.country) queryset = self.model.objects.filter(country=country) return queryset def list(self, request, *args, **kwargs): # Skipping validation code for brevity country = self.request.GET.get('country') self.country = Hasher.to_object_pk(country) queryset = self.get_queryset() serializer = self.serializer_class(queryset, many=True) return Response(serializer.data)

これで、データベースにいくつかのデータ(factory-boyを使用して生成された合計100,000の家のインスタンス:ある国で50,000、別の国で40,000、第3の国で10,000)を入力し、アプリのパフォーマンスをテストする準備が整いました。 。

パフォーマンスの最適化は測定がすべてです

プロジェクトで測定できることがいくつかあります。

  • 実行時間
  • コードの行数
  • 関数呼び出しの数
  • 割り当てられたメモリ
  • 等。

しかし、それらのすべてが、私たちのプロジェクトのパフォーマンスの測定に関連しているわけではありません。一般的に、最も重要な2つの主要なメトリックがあります。それは、実行時間と必要なメモリ量です。

Webプロジェクトでは、 反応時間 (サーバーがユーザーのアクションによって生成された要求を受信し、それを処理して結果を送り返すのに必要な時間)は通常、最も重要なメトリックです。これは、ユーザーが応答を待っている間に退屈して別のユーザーに切り替えることができないためです。ブラウザのタブ。

プログラミングでは、プロジェクトのパフォーマンスの分析は プロファイリング 。 APIエンドポイントのパフォーマンスをプロファイルするために、 シルク パッケージ。インストールして/api/v1/houses/?country=5T22RIを作成した後呼び出し(50,000の家のエントリがある国に対応するハッシュ)、次のようになります。

200 GET
/ api / v1 / houses /

77292ms全体
15854msクエリについて
50004クエリ

全体の応答時間は77秒で、そのうち16秒がデータベース内のクエリに費やされ、合計50,000回のクエリが実行されました。このように膨大な数で、改善の余地は十分にあるので、始めましょう。

1.データベースクエリの最適化

パフォーマンスの最適化に関する最も頻繁なヒントの1つは、データベースクエリが最適化されていることを確認することです。この場合も例外ではありません。さらに、応答時間を最適化するために、クエリについていくつかのことを行うことができます。

1.1すべてのデータを一度に提供する

これらの50,000クエリが何であるかを詳しく見ると、これらはすべてhouses_countryに対する冗長なクエリであることがわかります。テーブル:

200 GET
/ api / v1 / houses /

77292ms全体
15854msクエリについて
50004クエリ

で テーブル 参加する 実行時間(ミリ秒)
+0:01:15.874374 'houses_country' 0 0.176
+0:01:15.873304 'houses_country' 0 0.218
+0:01:15.872225 'houses_country' 0 0.218
+0:01:15.871155 'houses_country' 0 0.198
+0:01:15.870099 'houses_country' 0 0.173
+0:01:15.869050 'houses_country' 0 0.197
+0:01:15.867877 'houses_country' 0 0.221
+0:01:15.866807 'houses_country' 0 0.203
+0:01:15.865646 'houses_country' 0 0.211
+0:01:15.864562 'houses_country' 0 0.209
+0:01:15.863511 'houses_country' 0 0.181
+0:01:15.862435 'houses_country' 0 0.228
+0:01:15.861413 'houses_country' 0 0.174

この問題の原因は、Djangoではクエリセットが 怠惰 。これは、クエリセットが評価されず、実際にデータを取得する必要があるまでデータベースにヒットしないことを意味します。同時に、指示されたデータのみを取得し、追加のデータが必要な場合は後続のリクエストを行います。

それがまさに私たちの場合に起こったことです。 House.objects.filter(country=country)を介してクエリセットを取得すると、Djangoは指定された国のすべての家のリストを取得します。ただし、houseをシリアル化する場合インスタンス、HouseSerializer countryが必要ですシリアライザーのcountryを計算するための家のインスタンスフィールド。国のデータがクエリセットに存在しないため、djangoはそのデータを取得するために追加のリクエストを行います。そして、クエリセット内のすべての家に対してこれを行います。これは合計で50,000回です。

ただし、解決策は非常に簡単です。シリアル化に必要なすべてのデータを抽出するには、 select_related() クエリセットのメソッド。したがって、get_queryset次のようになります:

def get_queryset(self): country = get_object_or_404(Country, pk=self.country) queryset = self.model.objects.filter(country=country).select_related('country') return queryset

これがパフォーマンスにどのように影響したかを見てみましょう。

javascriptは関数ではありません

200 GET
/ api / v1 / houses /

35979ms全体
102msクエリについて
4クエリ

全体的な応答時間は36秒に短縮され、データベースで費やされる時間はわずか4クエリで約100ミリ秒です。それは素晴らしいニュースですが、もっと多くのことができます。

1.2関連データのみを提供する

デフォルトでは、Djangoはデータベースからすべてのフィールドを抽出します。ただし、列と行が多い巨大なテーブルがある場合は、抽出する特定のフィールドをDjangoに指示するのが理にかなっているため、まったく使用されない情報を取得するために時間を費やすことはありません。この場合、シリアル化に必要なフィールドは5つだけですが、17のフィールドがあります。データベースから抽出するフィールドを正確に指定することは理にかなっているため、応答時間をさらに短縮できます。

Djangoには defer() そしてその only() これを正確に行うためのクエリセットメソッド。最初のものはどのフィールドを指定します ない ロードするフィールドと2番目のフィールドはロードするフィールドを指定します のみ 。

def get_queryset(self): country = get_object_or_404(Country, pk=self.country) queryset = self.model.objects.filter(country=country) .select_related('country') .only('id', 'address', 'country', 'sq_meters', 'price') return queryset

これにより、クエリに費やされる時間が半分に短縮されました。これは良いことですが、50ミリ秒はそれほど多くありません。全体の時間もわずかに減少しましたが、それをカットするためのより多くのスペースがあります。

200 GET
/ api / v1 / houses /

33111ms全体
52msクエリについて
4クエリ

2.コードの最適化

データベースクエリを無限に最適化することはできません。最後の結果はそれを示しています。仮にクエリに費やす時間を0に減らしたとしても、応答を得るまで30分待つという現実に直面することになります。別のレベルの最適化に切り替えるときが来ました。 ビジネスの論理 。

2.1コードを簡素化する

サードパーティのパッケージには、単純なタスクのために多くのオーバーヘッドが伴う場合があります。そのような例の1つは、シリアル化されたハウスインスタンスを返すタスクです。

Django RESTフレームワークは素晴らしく、すぐに使える便利な機能がたくさんあります。ただし、現在の主な目標は応答時間を短縮することであるため、特にシリアル化されたオブジェクトが非常に単純であるという点で、最適化の優れた候補です。

この目的のためにカスタムシリアライザーを作成しましょう。簡単にするために、ジョブを実行する単一の静的メソッドを用意します。実際には、シリアライザーを交換可能に使用できるように、同じクラスとメソッドのシグネチャが必要になる場合があります。

# houses/serializers.py class HousePlainSerializer(object): ''' Serializes a House queryset consisting of dicts with the following keys: 'id', 'address', 'country', 'sq_meters', 'price'. ''' @staticmethod def serialize_data(queryset): ''' Return a list of hashed objects from the given queryset. ''' return [ { 'id': Hasher.from_pk_and_class(entry['id'], House), 'address': entry['address'], 'country': Hasher.from_pk_and_class(entry['country'], Country), 'sq_meters': entry['sq_meters'], 'price': entry['price'] } for entry in queryset ] # houses/views.py class HouseListAPIView(ListAPIView): model = House serializer_class = HouseSerializer plain_serializer_class = HousePlainSerializer # <-- added custom serializer country = None def get_queryset(self): country = get_object_or_404(Country, pk=self.country) queryset = self.model.objects.filter(country=country) return queryset def list(self, request, *args, **kwargs): # Skipping validation code for brevity country = self.request.GET.get('country') self.country = Hasher.to_object_pk(country) queryset = self.get_queryset() data = self.plain_serializer_class.serialize_data(queryset) # <-- serialize return Response(data)

200 GET
/ api / v1 / houses /

17312ms全体
38msクエリについて
4クエリ

これは今良く見えます。 DRFシリアライザーコードを採用しなかったため、応答時間はほぼ半分になりました。

もう1つの測定可能な結果、つまり要求/応答サイクル中に行われた関数呼び出しの総数は、15,859,427呼び出し(上記のセクション1.2で行われた要求から)から9,257,469呼び出しに減少しました。これは、すべての関数呼び出しの約3分の1がDjango RESTFrameworkによって行われたことを意味します。

2.2サードパーティパッケージの更新/置換

上記の最適化手法は最も一般的であり、徹底的な分析と思考なしで実行できる手法です。ただし、17秒はまだかなり長く感じます。この数を減らすには、コードをさらに深く掘り下げて、内部で何が起こっているかを分析する必要があります。つまり、コードのプロファイルを作成する必要があります。

組み込みのPythonプロファイラーを使用して自分でプロファイリングを行うことも、(組み込みのPythonプロファイラーを使用している)サードパーティのパッケージを使用することもできます。すでにsilkを使用しているので、コードをプロファイリングしてバイナリプロファイルファイルを生成し、さらに視覚化することができます。バイナリプロファイルを洞察に満ちた視覚化に変換する視覚化パッケージがいくつかあります。私は使用します snakeviz パッケージ。

これは、ビューのディスパッチメソッドにフックされた、上からの最後のリクエストのバイナリプロファイルの視覚化です。

ビューの画像

上から順にコールスタックが表示され、ファイル名、メソッド/関数名とその行番号、およびそのメソッドで費やされた対応する累積時間が表示されます。これで、時間の大部分がハッシュ(紫色の__init__.pyおよびprimes.py長方形)の計算に費やされていることがわかりやすくなりました。

現在、これはコードの主なパフォーマンスのボトルネックですが、同時に実際にはそうではありません 私たちの コード-サードパーティのパッケージです。

このような状況では、私たちができることは限られています。

  • パッケージの新しいバージョンを確認します(パフォーマンスが向上することを願っています)。
  • 必要なタスクでより優れたパフォーマンスを発揮する別のパッケージを見つけてください。
  • 現在使用しているパッケージのパフォーマンスを上回る独自の実装を作成します。

私にとって幸運なことに、basehashの新しいバージョンがありますハッシュを担当するパッケージ。コードはv.2.1.0を使用していますが、v.3.0.4があります。このような状況は、パッケージの新しいバージョンに更新できる場合、既存のプロジェクトで作業しているときに発生する可能性が高くなります。

v.3のリリースノートを確認すると、非常に有望に聞こえる次の特定の文があります。

素数アルゴリズムを使用して大規模なオーバーホールが行われました。 (原文のまま)のサポートを含む gmpy2 それがシステム上で利用可能であれば(原文のまま)、それ以上の増加が可能です。

これを見つけましょう!

pip install -U basehash gmpy2

200 GET
/ api / v1 / houses /

検索は機能ではありません

7738ms全体
59msクエリについて
4クエリ

応答時間を17秒から8秒未満に短縮しました。素晴らしい結果ですが、もう1つ確認する必要があります。

2.3独自のコードをリファクタリングする

これまでのところ、クエリを改善し、サードパーティの複雑で汎用的なコードを独自の非常に特殊な関数に置き換え、サードパーティのパッケージを更新しましたが、既存のコードはそのままにしました。ただし、既存のコードを少しリファクタリングすると、印象的な結果が得られる場合があります。ただし、このためには、プロファイリングの結果を再度分析する必要があります。

プロファイリング結果の画像

よく見ると、ハッシュはまだ問題であることがわかります(当然のことながら、データを使用して行うのはそれだけです)。ただし、その方向で改善しました。ただし、__init__.pyという緑がかった長方形灰色がかった__init__.py:54(hash)とともに、2.14秒を消費します。それはその直後に行きます。これは、初期化に時間がかかることを意味します。

basehashのソースコードを見てみましょう。パッケージ。

# basehash/__init__.py # Initialization of `base36` class initializes the parent, `base` class. class base36(base): def __init__(self, length=HASH_LENGTH, generator=GENERATOR): super(base36, self).__init__(BASE36, length, generator) class base(object): def __init__(self, alphabet, length=HASH_LENGTH, generator=GENERATOR): if len(set(alphabet)) != len(alphabet): raise ValueError('Supplied alphabet cannot contain duplicates.') self.alphabet = tuple(alphabet) self.base = len(alphabet) self.length = length self.generator = generator self.maximum = self.base ** self.length - 1 self.prime = next_prime(int((self.maximum + 1) * self.generator)) # `next_prime` call on each initialized instance

ご覧のとおり、baseの初期化インスタンスにはnext_primeの呼び出しが必要です関数;上の視覚化の左下の長方形でわかるように、これはかなり重いです。

私のHashを見てみましょう再びクラス:

class Hasher(object): @classmethod def from_model(cls, obj, klass=None): if obj.pk is None: return None return cls.make_hash(obj.pk, klass if klass is not None else obj) @classmethod def make_hash(cls, object_pk, klass): base36 = basehash.base36() # <-- initializing on each method call content_type = ContentType.objects.get_for_model(klass, for_concrete_model=False) return base36.hash('%(contenttype_pk)03d%(object_pk)06d' % { 'contenttype_pk': content_type.pk, 'object_pk': object_pk }) @classmethod def parse_hash(cls, obj_hash): base36 = basehash.base36() # <-- initializing on each method call unhashed = '%09d' % base36.unhash(obj_hash) contenttype_pk = int(unhashed[:-6]) object_pk = int(unhashed[-6:]) return contenttype_pk, object_pk @classmethod def to_object_pk(cls, obj_hash): return cls.parse_hash(obj_hash)[1]

ご覧のとおり、base36を初期化する2つのメソッドにラベルを付けました。各メソッド呼び出しのインスタンス。これは実際には必要ありません。

ハッシュは決定論的な手順であるため、特定の入力値に対して常に同じハッシュ値を生成する必要があるため、何かが壊れることを恐れずにクラス属性にすることができます。パフォーマンスを確認しましょう。

class Hasher(object): base36 = basehash.base36() # <-- initialize hasher only once @classmethod def from_model(cls, obj, klass=None): if obj.pk is None: return None return cls.make_hash(obj.pk, klass if klass is not None else obj) @classmethod def make_hash(cls, object_pk, klass): content_type = ContentType.objects.get_for_model(klass, for_concrete_model=False) return cls.base36.hash('%(contenttype_pk)03d%(object_pk)06d' % { 'contenttype_pk': content_type.pk, 'object_pk': object_pk }) @classmethod def parse_hash(cls, obj_hash): unhashed = '%09d' % cls.base36.unhash(obj_hash) contenttype_pk = int(unhashed[:-6]) object_pk = int(unhashed[-6:]) return contenttype_pk, object_pk @classmethod def to_object_pk(cls, obj_hash): return cls.parse_hash(obj_hash)[1]

200 GET
/ api / v1 / houses /

3766ms全体
38msクエリについて
4クエリ

最終結果は4秒未満で、最初の結果よりもはるかに小さくなっています。キャッシュを使用すると、応答時間をさらに最適化できますが、この記事では取り上げません。

結論

パフォーマンスの最適化は、分析と発見のプロセスです。各プロジェクトには独自のフローとボトルネックがあるため、すべてのケースに適用される厳格なルールはありません。ただし、最初にすべきことは、コードのプロファイルを作成することです。そして、そのような短い例で、応答時間を77秒から3.7秒に短縮できれば、巨大なプロジェクトにはさらに最適化の可能性があります。

Django関連の記事をもっと読むことに興味がある場合は、チェックしてください Django開発者が犯す間違いトップ10 仲間のApeeScapeによる Django開発者 アレクサンドルシュリギン。

多言語アプリを構築する方法:PHPとGettextを使用したデモ

バックエンド

多言語アプリを構築する方法:PHPとGettextを使用したデモ
Angular 6SPAでJWT認証を行う方法

Angular 6SPAでJWT認証を行う方法

バックエンド

人気の投稿
BEM方法論の紹介
BEM方法論の紹介
Ember.js開発者が犯す8つの最も一般的な間違い
Ember.js開発者が犯す8つの最も一般的な間違い
ソフトウェア設計ドキュメントを書くことが重要な理由
ソフトウェア設計ドキュメントを書くことが重要な理由
初期市場参入の課題
初期市場参入の課題
AngularMaterialを使用して最新のWebアプリを構築する
AngularMaterialを使用して最新のWebアプリを構築する
 
モバイルエクスペリエンスのためのeコマースUX
モバイルエクスペリエンスのためのeコマースUX
コンサルタントツールボックス:あらゆるものを解決するためのフレームワーク
コンサルタントツールボックス:あらゆるものを解決するためのフレームワーク
モバイルWebアプリケーションの開発:いつ、なぜ、そしてどのように
モバイルWebアプリケーションの開発:いつ、なぜ、そしてどのように
トップピッチデッキの間違い
トップピッチデッキの間違い
GraphQLとREST-GraphQLチュートリアル
GraphQLとREST-GraphQLチュートリアル
人気の投稿
  • モノのインターネットホームデバイス
  • ゼロからangularjsを学ぶ方法
  • グラス・スティーガル法は景気後退を引き起こしましたか
  • ベンチャーキャピタルタームシートサンプル
  • ガラススティーガル行為の廃止はどのように崩壊を引き起こしたのか
  • SpringBootセキュリティトークンベースの認証例
カテゴリー
Webフロントエンド プロセスとツール Uxデザイン データサイエンスとデータベース ライフスタイル プロジェクト管理 技術 Uiデザイン エンジニアリング管理 Kpiと分析

© 2021 | 全著作権所有

apeescape2.com