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

时间:2016-05-19 10:03:10

标签: django postgresql nginx ubuntu-14.04 cpu-usage

我会尝试尽可能多地提供相关信息。虽然解决方案很棒,但我只想获得如何解决问题的指导。如何查看更多有用的日志文件等。因为我是服务器维护的新手。欢迎任何建议。

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

  • 我正在运行2个数字海洋飞沫(Ubuntu 14.04 VPS)
  • Droplet#1正在运行django,nginx,gunicorn
  • 运行postgres的Droplet#2
  • 一切都运行良好一个月,然后突然发出postgres droplet CPU使用率飙升至100%
  • 发生这种情况时,您可以看到htop log。我已附上截图
  • 另一个截图是nginx error.log,你可以看到那个问题 从15:56:14开始,我用红框突出显示
  • sudo poweroff Postgres droplet并重启它并没有解决问题 问题
  • 将postgres droplet恢复到我上次的备份(20小时前)解决了问题,但它仍然继续发生。这是2天内的第7次

我会继续做研究并提供更多信息。同时欢迎任何意见。

谢谢。

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(以及gunjorn,django服务器上的nginx)执行 NOT 修复 冻结(我认为这是一个非常有趣的点
  • 但是,将Postgres服务器恢复到之前的备份(现在为2天)确定修复冻结
  • 罪魁祸首 Postgres日志消息无法向客户端发送数据:已损坏 管
  • 罪魁祸首 Nginx日志消息是一个简单的django-rest-framework
    api调用只返回20个项目(每个都有一些外键数据 查询)

2016年5月20日更新#2 当冻结发生时,我尝试按时间顺序执行以下操作(关闭所有内容并逐个转回)

  • 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%并保持不变 有信息

那么这不是关于服务器负载或长查询时间吗?

这是非常令人困惑的,因为如果我将数据库恢复到我的最新备份(2天前),即使没有触及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 这是您需要的信息:

你可以认为我的网站有点像Udemy,一个在线课程市场。有"产品"(课程)。每个产品都包含许多视频。用户可以对产品页面本身和每个视频进行评论。

在许多情况下,我需要查询产品列表,按照其获得的总评论数量(产品评论和对该产品的每个视频的评论的总和)

与上面的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]

我只是注意到我(或我团队中的其他开发人员)使用这两条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

注意:这是photoshopped并包含其他一些模型。 Indexes of Product and Video models

--- 2016年6月8日更新#3 --- @Jerzyk 如你所料。在我再次检查了所有代码之后,我发现我确实做了一个“内存切换”:我尝试通过这样做来调整前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)

2 个答案:

答案 0 :(得分:4)

我们可以得出结论,您的问题是由有问题的查询引起的。单独查询的每次运行似乎都不足以导致超时。但是,这些查询中的一些可能会同时执行,并可能导致崩溃。你可以做两件事来加快速度。

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

此查询现在来自memcache(或用于缓存的任何内容),这意味着如果您对连续快速连续使用此页面的页面进行两次连续点击,则第二次点击将对数据库没有影响。您可以控制对象在内存中缓存的时间。

2)优化查询

从解释中向您跳出的第一件事是您正在对products_productsproduct_videos表进行顺序扫描。通常,顺序扫描不如索引扫描所需。但是,由于您拥有COUNT()HAVING COUNT()条款以及大量{{1},因此可能无法在此查询中使用索引扫描 在它上面的条款。

更新

您的查询有一个LEFT OUTER JOIN,INNER JOIN或子查询可能更快,为了做到这一点,我们需要识别GROUP BYVideo表上的分组可以为我们提供至少包含8种产品的视频集。

product_id

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

3)改善指数

虽然在inner = RawSQL('SELECT id from product_videos GROUP BY product_id HAVING COUNT(product_id) > 1',params=[]) Product.objects.filter(id__in=b) draft列上创建索引可能很诱人,但由于这些列没有足够的基数来成为索引的良好候选者,因此这将是徒劳的。但是,仍然可以创建条件索引。只有看到你的表后才能得出结论。

答案 1 :(得分:2)

*** 2016年6月7日更新:问题再次发生。 CPU达到100%并保持不变。这个答案对性能有帮助,但遗憾的是不能解决这个问题。

感谢DigitalOcean支持团队的推荐。我尝试了这个工具建议的配置:

http://pgtune.leopard.in.ua/

我建议使用1个CPU核心和1GB RAM的小滴的以下值:

postgresql.conf中的

max_connections = 200
shared_buffers = 256MB
effective_cache_size = 768MB
work_mem = 1310kB
maintenance_work_mem = 64MB
checkpoint_segments = 32
checkpoint_completion_target = 0.7
wal_buffers = 7864kB
default_statistics_target = 100

/etc/sysctl.conf中

kernel.shmmax=536870912
kernel.shmall=131072

到目前为止,我的postgres服务器已经运行了3-4天。所以我认为这是解决方案。谢谢大家!