django - 多个Postgres SELECT进程(Django GET请求)卡住,导致100%CPU使用率

标签 django postgresql nginx ubuntu-14.04 cpu-usage

我会尽力在这里提供尽可能多的信息。尽管解决方案很好,但我只想提供有关如何解决该问题的指导。如何查看更有用的日志文件等。我是服务器维护的新手。欢迎任何建议。

这是按时间顺序发生的事情:

  • 我正在运行2个digitalocean小滴(Ubuntu 14.04 VPS)
  • 运行django,nginx,gunicorn的Droplet#1
  • 运行Postgres的Droplet#2
  • 一切正常运行了一个月,然后突然出现了postgres滴
    CPU使用率飙升至100%
  • 发生这种情况时,您可以看到htop日志。我已附上截图
  • 另一个屏幕截图是nginx error.log,您可以看到该问题
    从15:56:14开始,我用红色框
  • 突出显示
  • sudo关闭Postgres小滴并重新启动它并不能解决问题
    问题
  • 将postgres Droplet恢复到我的上一个备份(20小时前)解决了该问题,但仍会再次发生。这是2天内的第七次

  • 我将继续进行研究并提供更多信息。同时,欢迎任何意见。

    谢谢你。

    Postgres server htop log when problem occurs
    nginx error.log when problem occurs

    更新2016年5月20日
  • 按照e4c5
  • 的建议在Postgres服务器上启用慢速查询日志记录
  • 6小时后,服务器在上午8:07再次卡住(100%CPU使用率)。我已经附上了所有相关的屏幕截图
  • 如果在卡住
  • 期间尝试访问站点,则浏览器显示502错误
  • sudo service restart postgresql(以及Django服务器上的gunicorn,nginx)不会对进行修复
    卡住(我认为这是一个非常有趣的点)
  • 但是,将Postgres服务器还原到我以前的备份(现在已经2天了) 是否修复了卡住
  • 罪魁祸首 Postgres日志消息是无法将数据发送到客户端:损坏
    管道
  • 罪魁祸首 Nginx日志消息是一个简单的django-rest-framework
    api调用仅返回20个项目(每个项目都带有一些外键数据
    查询)

  • 更新#2 2016年5月20日
    发生卡住时,我尝试按时间顺序执行以下操作(关闭所有内容,然后将它们重新打开一次)
  • sudo service stop postgresql-> cpu使用率降至0-10%
  • sudo service stop gunicorn-> cpu使用率保持在0-10%
  • sudo service stop nginx-> cpu使用率保持在0-10%
  • sudo service restart postgresql-> cpu使用率保持在0-10%
  • sudo service restart gunicorn-> cpu使用率保持在0-10%
  • sudo service restart nginx-> cpu使用率上升至100%并保持不变

  • 因此,这与服务器负载或查询时间长有关吗?

    这非常令人困惑,因为如果我将数据库还原到最新的备份(两天前),即使没有触摸nginx/gunicorn/django服务器,一切也会恢复在线状态...

    2016年6月8日更新
    我打开了慢查询日志记录。将其设置为记录耗时超过1000毫秒的查询。

    我得到这个查询多次出现在日志中。
    SELECT
         "products_product"."id",
         "products_product"."seller_id",
         "products_product"."priority",
         "products_product"."media",
         "products_product"."active",
         "products_product"."title",
         "products_product"."slug",
         "products_product"."description",
         "products_product"."price",
         "products_product"."sale_active",
         "products_product"."sale_price",
         "products_product"."timestamp",
         "products_product"."updated",
         "products_product"."draft",
         "products_product"."hitcount",
         "products_product"."finished",
         "products_product"."is_marang_offline",
         "products_product"."is_seller_beta_program",
         COUNT("products_video"."id") AS "num_video"
     FROM "products_product"
     LEFT OUTER JOIN "products_video" ON ( "products_product"."id" = "products_video"."product_id" )
     WHERE ("products_product"."draft" = false AND "products_product"."finished" = true)
     GROUP BY
         "products_product"."id",
         "products_product"."seller_id",
         "products_product"."priority",
         "products_product"."media",
         "products_product"."active",
         "products_product"."title",
         "products_product"."slug",
         "products_product"."description",
         "products_product"."price",
         "products_product"."sale_active",
         "products_product"."sale_price",
         "products_product"."timestamp",
         "products_product"."updated",
         "products_product"."draft",
         "products_product"."hitcount",
         "products_product"."finished",
         "products_product"."is_marang_offline",
         "products_product"."is_seller_beta_program"
     HAVING COUNT("products_video"."id") >= 8
     ORDER BY "products_product"."priority" DESC, "products_product"."hitcount" DESC
     LIMIT 100
    

    我知道这是一个丑陋的查询(由django聚合生成)。用英语来说,此查询仅表示“给我一个列表,其中包含8个以上的视频”。

    这是该查询的EXPLAIN输出:
                      QUERY PLAN                                                                                                                                                                                                                 
    ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
     Limit  (cost=351.90..358.40 rows=100 width=933)
       ->  GroupAggregate  (cost=351.90..364.06 rows=187 width=933)
             Filter: (count(products_video.id) >= 8)
             ->  Sort  (cost=351.90..352.37 rows=187 width=933)
                   Sort Key: products_product.priority, products_product.hitcount, products_product.id, products_product.seller_id, products_product.media, products_product.active, products_product.title, products_product.slug, products_product.description, products_product.price, products_product.sale_active, products_product.sale_price, products_product."timestamp", products_product.updated, products_product.draft, products_product.finished, products_product.is_marang_offline, products_product.is_seller_beta_program
                   ->  Hash Right Join  (cost=88.79..344.84 rows=187 width=933)
                         Hash Cond: (products_video.product_id = products_product.id)
                         ->  Seq Scan on products_video  (cost=0.00..245.41 rows=2341 width=8)
                         ->  Hash  (cost=88.26..88.26 rows=42 width=929)
                               ->  Seq Scan on products_product  (cost=0.00..88.26 rows=42 width=929)
                                     Filter: ((NOT draft) AND finished)
    

    (11列)

    ---更新2016年6月8日#2 ---
    既然有很多人提出很多建议。因此,我将尝试一个接一个地应用这些修复程序,并定期向其报告。

    @ e4c5
    这是您需要的信息:

    您可以想到我的网站,就像在线类(class)市场Udemy。有“产品”(类(class))。每个产品都包含许多视频。用户可以在“产品”页面本身和每个视频上发表评论。

    在很多情况下,我需要根据收到的评论总数(产品评论和该产品的每个视频上评论的总数)来查询产品订单列表

    与上面的EXPLAIN输出相对应的django查询:
    all_products_exclude_draft = Product.objects.all().filter(draft=False)
    products_that_contain_more_than_8_videos =  all_products_exclude_draft.annotate(num_video=Count('video')).filter(finished=True, num_video__gte=8).order_by('timestamp')[:30]
    

    我只是注意到我(或团队中的其他一些开发人员)使用这2条python行两次命中数据库。

    这是产品和视频的django模型:
    from django_model_changes import ChangesMixin
    
    class Product(ChangesMixin, models.Model):
        class Meta:
            ordering = ['-priority', '-hitcount']
        seller = models.ForeignKey(SellerAccount)
        priority = models.PositiveSmallIntegerField(default=1)
        media = models.ImageField(blank=True, 
                null=True, 
                upload_to=download_media_location,
                default=settings.MEDIA_ROOT + '/images/default_icon.png',
                storage=FileSystemStorage(location=settings.MEDIA_ROOT))
        active = models.BooleanField(default=True)
        title = models.CharField(max_length=500)
        slug = models.SlugField(max_length=200, blank=True, unique=True)
        description = models.TextField()
        product_coin_price = models.IntegerField(default=0)
        sale_active = models.BooleanField(default=False)
        sale_price = models.IntegerField(default=0, null=True, blank=True) #100.00
        timestamp = models.DateTimeField(auto_now_add=True, auto_now=False, null=True)
        updated = models.DateTimeField(auto_now_add=False, auto_now=True, null=True)
        draft = models.BooleanField(default=True)
        hitcount = models.IntegerField(default=0)
        finished = models.BooleanField(default=False)
        is_marang_offline = models.BooleanField(default=False)
        is_seller_beta_program = models.BooleanField(default=False)
    
        def __unicode__(self):
            return self.title
    
        def get_avg_rating(self):
            rating_avg = self.productrating_set.aggregate(Avg("rating"), Count("rating"))
            return rating_avg
    
        def get_total_comment_count(self):
            comment_count = self.video_set.aggregate(Count("comment"))
            comment_count['comment__count'] += self.comment_set.count()
            return comment_count
    
        def get_total_hitcount(self):
            amount = self.hitcount
            for video in self.video_set.all():
                amount += video.hitcount
            return amount
    
        def get_absolute_url(self):
            view_name = "products:detail_slug"
            return reverse(view_name, kwargs={"slug": self.slug})
    
        def get_product_share_link(self):
            full_url = "%s%s" %(settings.FULL_DOMAIN_NAME, self.get_absolute_url())
            return full_url
    
        def get_edit_url(self):
            view_name = "sellers:product_edit"
            return reverse(view_name, kwargs={"pk": self.id})
    
        def get_video_list_url(self):
            view_name = "sellers:video_list"
            return reverse(view_name, kwargs={"pk": self.id})
    
        def get_product_delete_url(self):
            view_name = "products:product_delete"
            return reverse(view_name, kwargs={"pk": self.id})
    
        @property
        def get_price(self):
            if self.sale_price and self.sale_active:
                return self.sale_price
            return self.product_coin_price
    
        @property
        def video_count(self):
            videoCount = self.video_set.count()
            return videoCount
    
    class Video(models.Model):
        seller = models.ForeignKey(SellerAccount)
        title = models.CharField(max_length=500)
        slug = models.SlugField(max_length=200, null=True, blank=True)
        story = models.TextField(default=" ")
        chapter_number = models.PositiveSmallIntegerField(default=1)
        active = models.BooleanField(default=True)
        featured = models.BooleanField(default=False)
        product = models.ForeignKey(Product, null=True)
        timestamp = models.DateTimeField(auto_now_add=True, auto_now=False, null=True)
        updated = models.DateTimeField(auto_now_add=False, auto_now=True, null=True)
        draft = models.BooleanField(default=True)
        hitcount = models.IntegerField(default=0)
        objects = VideoManager()
    
        class Meta:
            unique_together = ('slug', 'product')
            ordering = ['chapter_number', 'timestamp']
    
        def __unicode__(self):
            return self.title
    
        def get_comment_count(self):
            comment_count = self.comment_set.all_jing_jing().count()
            return comment_count
    
        def get_create_chapter_url(self):
            return reverse("sellers:video_create", kwargs={"pk": self.id})
    
        def get_edit_url(self):
            view_name = "sellers:video_update"
            return reverse(view_name, kwargs={"pk": self.id})
    
        def get_video_delete_url(self):
            view_name = "products:video_delete"
            return reverse(view_name, kwargs={"pk": self.id})
    
        def get_absolute_url(self):
            try:
                return reverse("products:video_detail", kwargs={"product_slug": self.product.slug, "pk": self.id})
            except:
                return "/"
    
        def get_video_share_link(self):
            full_url = "%s%s" %(settings.FULL_DOMAIN_NAME, self.get_absolute_url())
            return full_url      
    
        def get_next_url(self):
            current_product = self.product
            videos = current_product.video_set.all().filter(chapter_number__gt=self.chapter_number)
            next_vid = None
            if len(videos) >= 1:
                try:
                    next_vid = videos[0].get_absolute_url()
                except IndexError:
                    next_vid = None
            return next_vid
    
        def get_previous_url(self):
            current_product = self.product
            videos = current_product.video_set.all().filter(chapter_number__lt=self.chapter_number).reverse()
            next_vid = None
            if len(videos) >= 1:
                try:
                    next_vid = videos[0].get_absolute_url()
                except IndexError:
                    next_vid = None
            return next_vid
    

    这是我从命令中获得的“产品和视频”表的索引:
    my_database_name=# \di
    

    注意:这是photoshop处理的,并且还包括其他一些模型。
    Indexes of Product and Video models

    ---更新2016年6月8日#3 ---
    @杰兹克
    如您所怀疑。再次检查所有代码后,我发现我确实做了一个“内存切片”:我试图通过这样做来改组前10个结果:
    def get_queryset(self):
            all_product_list = Product.objects.all().filter(draft=False).annotate(
            num_video=Count(
                    Case(
                        When(
                            video__draft=False,
                            then=1,
                        )
                    )
                )
            ).order_by('-priority', '-num_video', '-hitcount')
            the_first_10_products = list(all_product_list[:10])
            the_11th_product_onwards = list(all_product_list[10:])
            random.shuffle(copy)
            finalList = the_first_10_products + the_11th_product_onwards
    

    注意:在上面的代码中,我需要计算未处于草稿状态的视频的数量。

    因此,这也是我需要解决的问题之一。谢谢。 > _ <

    ---这是相关的屏幕截图---

    发生卡住时的Postgres日志(log_min_duration = 500毫秒)
    Postgres log 20 May 2016

    Postgres日志(来自上述屏幕截图)
    Postgres log 20 May 2016 (page2)

    Nginx error.log在同一时间段
    Nginx log 20 May 2016

    卡住之前的DigitalOcean CPU使用率图
    DigitalOcean graph 20 May 2016 (1)

    卡住后的DigitalOcean CPU使用率图
    DigitalOcean graph 20 May 2016 (2)

    最佳答案

    我们可以得出结论,您的问题是由相关查询缓慢引起的。就其本身而言,每次查询运行似乎都不足够慢以致导致超时。但是,有可能同时执行这些查询中的几个,这可能导致崩溃。您可以执行两项操作来加快速度。

    1)缓存结果

    长时间运行的查询结果可以被缓存。

    from django.core.cache import cache
    
    def get_8x_videos():
        cache_key = 'products_videos_join'
        result = cache.get(cache_key, None)
        if not result:
            all_products_exclude_draft = Product.objects.all().filter(draft=False)
            result =  all_products_exclude_draft.annotate(num_video=Count('video')).filter(finished=True, num_video__gte=8).order_by('timestamp')[:30]
    
            result = Product.objects.annotate('YOUR LONG QUERY HERE')
            cache.set(cache_key, result)
    
        return result
    

    现在,此查询来自内存缓存(或用于缓存的任何内容),这意味着如果连续连续两次点击使用此缓存的页面,则第二次匹配不会对数据库产生影响。您可以控制对象在内存中缓存多长时间。

    2)优化查询

    从说明中脱颖而出的第一件事是,您正在对products_productsproduct_videos表进行顺序扫描。通常,顺序扫描不如索引扫描可取。但是,由于您上面有COUNT()HAVING COUNT()子句以及上面有大量的GROUP BY子句,因此可能无法在此查询上使用索引扫描。

    更新:

    您的查询具有LEFT OUTER JOIN,有可能INNER JOIN或子查询可能会更快,为此,我们需要认识到Videoproduct_id表上的分组可以为我们提供一组视频,这些视频在至少8个产品。
    inner = RawSQL('SELECT id from product_videos GROUP BY product_id HAVING COUNT(product_id) > 1',params=[])
    
    Product.objects.filter(id__in=b)
    

    上面的代码消除了LEFT OUTER JOIN并引入了一个子查询。但是,这不能轻松访问每种产品的实际视频数量,因此,以当前形式存在的此查询可能无法完全使用。

    3)改善指标

    虽然可能很想在draftfinished列上创建索引,但这将是徒劳的,因为这些列没有足够的基数来适合用作索引。但是,仍然可以创建条件索引。同样,只有在查看表格后才能得出结论。

    关于django - 多个Postgres SELECT进程(Django GET请求)卡住,导致100%CPU使用率,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/37319970/

    相关文章:

    php - NGINX/PHP - 无法通过 php 脚本提供 jpeg

    Nginx Plus 不流式传输 HLS

    Django:is_valid 和 form_valid 之间的区别

    django - 玛雅 edms 与 s3

    postgresql - SailsJS 通过关联 : how to create association?

    ruby-on-rails - 显示错误的引用列不为空 - rails

    python - 将 Django 项目从开发迁移到生产时模型丢失错误

    django - django sessions 是否可以安全地用于关键任务注册表单?

    postgresql - 在 PostgreSQL 8.4 中排序重音字符时出现问题

    php - php yii2 的访问日志系统?