【问题标题】:Server Response Time is Much Longer Than Expected服务器响应时间比预期长得多
【发布时间】:2021-09-07 04:39:51
【问题描述】:

我有一个 Django 应用程序,目前在我的本地主机上运行。有些页面需要更长的时间,因此出于调试原因,我使用以下简单代码计算了在一个基于类的视图中花费的时间:

class DiscoverView(LoginRequiredMixin, ListView):
    model = MyModel
    template_name = "app1/discover.html"
    context_object_name = 'influencers'  # not necessary, default is object_list

    def get_context_data(self, **kwargs):

        start_time = time.time()
        # ...
        # Some tasks which I tought they are the reasons of long response time
        # ...
        print(time.time() - start_time, "seconds took to complete...")
        return context

经过实验,我发现这个视图需要 0.01 秒 才能完成,而页面加载大约需要 5 秒。

为了进一步调试,我深入研究了 Chrome 开发工具。在网络选项卡中,页面的“完成”时间为 5.86 秒。我还运行了 Lighthouse 测试,它表明服务器的初始响应时间约为 2 秒。

我不明白为什么这个 0.01 秒变成了 2 秒,我还使用 django-debug-toolbar 来检查数据库查询时间,这并不长。我的应用程序也在生产环境中(Heroku),加载时间当然要差得多,但我觉得我需要先在本地解决这个问题。

感谢任何形式的帮助或调试建议,如果您需要有关应用程序/系统的更多信息,请告诉我。

编辑:这是get_context_data()的内部:

def get_context_data(self, **kwargs):
        start_time = time.time()

        context = super().get_context_data(**kwargs)
        platform = self.kwargs['platform']
        page_no = self.request.GET.get('page') # make it int to multiply when slicing
        if page_no is not None: page_no = int(page_no)
        ITEMS_PER_PAGE = 10
        parents = None
        
        if platform == 'instagram':
            # Will implement this later
            pass
        elif platform == 'tiktok':
            tiktok_influencers = TiktokInfluencer.objects.all()
            context['platform_counter'] = tiktok_influencers.count()
            parents = InfluencerParent.objects.filter(tiktok__id__in = tiktok_influencers)
        elif platform == 'youtube':
            youtube_influencers = YoutubeInfluencer.objects.all()
            context['platform_counter'] = youtube_influencers.count()
            parents = InfluencerParent.objects.filter(youtube__id__in = youtube_influencers)
        elif platform == 'twitch':
            twitch_influencers = TwitchInfluencer.objects.all()
            context['platform_counter'] = twitch_influencers.count()
            parents = InfluencerParent.objects.filter(twitch__id__in = twitch_influencers)
        elif platform == 'clubhouse':
            clubhouse_influencers = ClubhouseInfluencer.objects.all()
            context['platform_counter'] = clubhouse_influencers.count()
            parents = InfluencerParent.objects.filter(clubhouse__id__in = clubhouse_influencers)
        else:
            # unknown platform, redirect to 404
            pass
        
        # Pagination stuff
        if page_no == None or page_no == 1:
            parents = parents[:ITEMS_PER_PAGE]
        else:
            parents = parents[(page_no-1) * ITEMS_PER_PAGE : (page_no) * ITEMS_PER_PAGE]

        context['influencers'] = parents
        context['platform'] = platform

        print(time.time() - start_time, "seconds took DiscoverView to complete...")
        return context

编辑 2:我需要进一步的帮助,所以我要添加模型和模板信息:

# models.py
class InfluencerParent(models.Model):
'''
Parent class which points to influencers' IG, YT & other social media platform accounts
'''
    
def __str__(self):
    if self.instagram.first():
        return self.instagram.first().fullname
    else:
        return "None"

class InstagramInfluencer(models.Model):
    # some fields...
    influencer_parent = models.ForeignKey(InfluencerParent, on_delete=models.SET_NULL, related_name='instagram', default=None, blank=True, null=True)

class YoutubeInfluencer(models.Model):
    # some fields...
    influencer_parent = models.ForeignKey(InfluencerParent, on_delete=models.SET_NULL, related_name='instagram', default=None, blank=True, null=True)

class TiktokInfluencer(models.Model):
    # some fields...
    influencer_parent = models.ForeignKey(InfluencerParent, on_delete=models.SET_NULL, related_name='instagram', default=None, blank=True, null=True)

class TwitchInfluencer(models.Model):
    # some fields...
    influencer_parent = models.ForeignKey(InfluencerParent, on_delete=models.SET_NULL, related_name='instagram', default=None, blank=True, null=True)

class ClubhouseInfluencer(models.Model):
    # some fields...
    influencer_parent = models.ForeignKey(InfluencerParent, on_delete=models.SET_NULL, related_name='instagram', default=None, blank=True, null=True)

模板示例:

# template (not the all for the sake of clarity)
<!-- discover.html

A single template to list all influencers (IG, YT, Tiktok...)
Takes platform param from URL
In view we filtered parents according to the platform
For example if platform param is 'tiktok' I use this updated version:   
parents = InfluencerParent.objects.filter(tiktok__isnull=False).prefetch_related('instagram', 'youtube', 'tiktok', 'twitch', 'clubhouse')

There parents influencers are who have tiktok accounts, in template I show all the social media cards in a tabbed design
So I need to send parent and acces parent.instagram.first.username
-->

{% for influencer in influencers %}

<div class="instagram" {% influencer.instagram.first == None %} disabled {% endif %}>
    {{influencer.instagram.first.username}}
</div>

<div class="tiktok" {% influencer.tiktok.first == None %} disabled {% endif %}>
    {{influencer.tiktok.first.username}}
</div>

<!-- 
. 
. 
OTHER SOCIAL MEDDIA ACCOUNTS OF THAT PARENT
. 
. 
-->

<div class="clubhouse" {% influencer.clubhouse.first == None %} disabled {% endif %}>
    {{influencer.clubhouse.first.username}}
</div>

这是我如何使用 prefetch_related (select_related 给出错误):

elif platform == 'tiktok':
    context['platform_counter'] = TiktokInfluencer.objects.count()
    parents = InfluencerParent.objects.filter(tiktok__isnull=False).prefetch_related('instagram', 'youtube', 'tiktok', 'twitch', 'clubhouse')
    # This is how I use prefetch_lated(), I need to send parent objecjts and their all (if exists) platform related influencer objects
    # I acces Parent's all existing reverse foreign key fields in one template, for example:
    # {{parent.influencer.username}}
    # {{parent.youtube.follower_count}}
    # {{parent.tiktok.fullname}}
    # ...

    # The row below gives error: 'Invalid field name(s) given in select_related: 'tiktok'. Choices are: (none)'
    # Even tho I need  to give all social platforms as params (select_related('instagram', 'youtube', 'tiktok', 'twitch', 'clubhouse'))
    parents = InfluencerParent.objects.select_related('tiktok')

【问题讨论】:

  • 你应该安装 Django 调试工具栏,这将有助于调试一些加载时间:pypi.org/project/django-debug-toolbar
  • 你能添加任何模板或查看代码来访问你的模型中的任何内容吗?您可能有一个 for 循环,它在每次迭代中都会访问数据库。
  • 计时 get_context_data 不是衡量时间的好方法......例如查询集是惰性的,因此很可能不会通过计时注意到昂贵的查询,而是大部分时间会是在渲染循环查询集的模板时拍摄(例如,您的问题可能是 N + 1 个查询等)。另一个可能的问题是,这可能是一段时间后的请求,因此 Django 需要与数据库建立新连接,因为没有旧连接可以重用,这可能需要一些时间。
  • 我已经安装并使用 django-debug-toolbar 进行了检查,当我启用“SQL”(1355 次查询在 973 毫秒内)时确实需要花费大量时间。我编辑了我的帖子并添加到 get_context_data() 中,但我无法在此处添加 1300 行长的模板,有没有办法可以在此处正确共享它(通过上传某处并提供链接?)
  • 学习 Prefetch 和 .prefecth_related() 将使您受益匪浅。 docs.djangoproject.com/en/3.2/ref/models/querysets 你要做的是从第一个重复查询问题开始。抛出一个 select 或 prefetch_related 调用,您的查询计数将随着您优化数据库命中而迅速减少。没有看到您的模型和一些重复的查询,我无法提供更多帮助。但似乎您实际上可以删除您的 .all() 调用并运行 Q 类型查询以获取您要查找的内容

标签: python html django web loading


【解决方案1】:

https://docs.djangoproject.com/en/3.2/ref/models/querysets/ 为了帮助建立基础,select_related 适用于具有外键关系的模型,并且通常 prefetch_related 适用于具有 M2M 的模型。 在这里,我将提供一种“简单”的方式来实现 prefetch_related。请记住,连接到数据库的调用不仅在 views.py 中,而且在您的模板中,访问属性可能对数据库造成额外的打击(请参阅上面的链接以获得进一步的解释,即 blog.author.hometown)。因此,为了减轻我们访问数据库的次数,我建议实施类似的方法。现在看不到模型、模板和实际 SQL 查询,这将是我最好的猜测。

        
        ...
        if platform == 'instagram':
            # Will implement this later
            pass
        elif platform == 'tiktok':
            context['platform_counter'] = TiktokInfluencer.objects.count()
            parents = InfluencerParent.objects.prefetch_related('tiktok')
        elif platform == 'youtube':
            context['platform_counter'] = YoutubeInfluencer.objects.count()
            parents = InfluencerParent.objects.prefetch_related('youtube')

         ... repeat this pattern for others
        else:

我认为仅添加我所做的编辑就会大大改变查询量。

如果你想看看它会如何变化。取一部分视图,在 shell 中做一些测试

from django.db import reset_queries
from django.db import connection

>>> parents = list(InfluencerParent.objects.prefetch_related('tiktok'))
>>> connections.queries
>>> reset_queries()
>>> parents = list(InfluencerParent.objects.select_related('tiktok')) 
>>> connections.queries
# If both work, then your using a Forgeign Key but more importantly
# notice how many SQL queries are made. If using select_related breaks 
# its because your using an M2M. But again, the importants is getting that list of "connections" as low as possible

现在,为了进一步完善您的代码,我将看看 Prefetch,因为您可以使 缓存查询 类似于您之前实现的。

from django.db.models import Prefetch

>>> qs = MyModel.objects.filter(some_lookup)
>>> results = MyDesiredResultObjects.objects.prefetch_related(Prefetch('some_attr'), queryset=qs, to_attr='my_new_result')
>>> my_new_result

============================
由于您提供了模型
select_related

...
elif platform == 'tiktok':
            context['platform_counter'] = TiktokInfluencer.objects.count()
            parents = InfluencerParent.objects.select_related('tiktok')

【讨论】:

  • 我从来没有讨论过模板如何查询数据库,这对我来说是一个启示。我正在使用外键关系,但我会同时尝试这两种“预取”和“相关”,看看哪个查询较少。我将比较 django-debug-toolbar 的结果并在这里分享结果。有很多要读的,感谢您的帮助!
  • 没问题。我知道最后一部分似乎很神秘,但是一旦您阅读它,它就会变得更有意义。在问题中发布您的模型,我可能会提供帮助。如果你觉得这个答案对你有帮助,请不要忘记接受它。
  • 我添加了 'prefetch_related()' 并且查询计数从 1397 变为 1317,仍然不确定瓶颈在哪里,添加了有关我的模型和模板使用的更多信息。我查看了“预取”,但不确定如何使用它来预取父对象的所有社交媒体(向后外键),即使我可以仍然有近一千个查询我不知道它们来自哪里,会花时间使用调试工具栏。
  • 这是一个开始!哈哈。查看复制了哪些 SQL quieres。如果您从调试工具栏中提供一些数据库事务信息,我们可以提供帮助。
  • 使用 select_related() 会出现错误:'select_related 中给出的字段名称无效:'tiktok'。选择是:(无)',我设法将查询计数增加到 70,问题是我在很多地方(180 次)的模板中使用了 '.first' 标记,它不使用缓存的查询集并产生额外的查询db,谢谢你的帮助
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-03-08
  • 1970-01-01
  • 1970-01-01
  • 2021-05-19
  • 2023-04-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多