django -- 一次性能问题的排查

Posted bao9687426

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了django -- 一次性能问题的排查相关的知识,希望对你有一定的参考价值。

在最近的工作中,由于服务中数据量较大,在前端请求的时候,延迟比较严重,(我们服务中web界面的数据都是从数据库中取出通过一定计算得到的,则就会有大量的数据库操作),在前期的时候,我们都是无脑的将数据返回较慢的接口给加了一层缓存,而我们的缓存的更新方式也很简单粗暴,就是间隔一定时间后,后端自己去更新所有的缓存,随着功能的扩展,添加缓存的接口也越来越多,终于,我们服务占用的cpu大部分时间都在100%以上,得不偿失,而且缓存更新的间隔时间也被拉长。

1. 由于我们服务中有很多的线程,而我们也不能直接断定是哪一个线程占用cpu较高,那么这种我们该怎么去知道是哪一个线程占用cpu较高呢??

答: 当时我是这样做的,在我们服务中预测占用cpu较高的线程中加入了一个方法,可以记录这个线程在linux中的原生线程号,这样我们就可以知道了每一个线程对应的linux中原生线程ID,python中获取线程原生线程ID是有方法的:

import cytypes
linux_thread_id = ctypes.CDLL(libc.so.6).syscall(186)

       而后我们使用top -H -p pid 就可以知道服务进程中线程占用cpu的情况,从而找到我们占用cpu较高的线程。

 

2. 这个时候我们只是找到了那个线程占用cpu较高,但是也并不能明确到底是哪儿慢了,当然我们可以猜测是数据库相关的操作慢了,但是并不能明确,那么我们怎么知道是执行那一句的时候慢了呢?

答: 为了知道是那些语句执行较慢,我们就找到了python自带的line_profiler进行代码的分析,这个模块可以打印出每一句语句执行所耗费的时间,次数,以及占比等信息。

from line_profiler import LineProfiler
def test():
    print(test)

def __name__ == "__main__":
    profile = LineProfiler(test)  # 把函数传递到性能分析器
    profile.enable()  # 开始分析
    test()
    profile.disable()  # 停止分析
    profile.dump_stats(test.stats.format(more_msg, fn_name))
 

      这样就可以把函数中每一句执行的情况给打印出来,也可以放到一个文件中,而后进行查看分析。

3. 这个时候我们就知道了到底是那一句太慢了,果不其然,是数据库相关的操作导致,尤其在将数据库中的数据反序列化相关操作较慢,因此当时我们就是不需要反序列化就尽量不要反序列化,当然,还有一个特别大的原因是django中orm惰性执行导致的?

     1. 在创建查询集的时候,其实是不会去访问数据库的·,知道调用数据时,才会真正的访问数据库,而调用数据库的情况包括(迭代,序列化,同if语句使用等)

     2. 当存在外键和多对多的关系时,也是不会去检索关联对象的数据,只有在调用关联对象时才会再次去查询数据库,这也就导致了多次的数据库连接,而连接数据库又是一个特别耗时的过程!!

4. 那么我们该怎么解决这种问题呢?

     1. 这个时候我们就想到了django中orm的预加载。

     2. 预加载单个关联对象--》可以使用select_related

     3. 预加载多个关联对象--》可以使用prefetch_related

     4. 一个Queryset中可以同时使用select_related和prefetch_related,这样可以减少数据库连接次数。

     5. 需要注意的是只有在prefetch_related之前的select_related才是有效的,之后的都是无效的。

hosts=hosts.filter(type=xdata.PROXY_AGENT).exclude(ext_info__contains=r"is_deleted":true).select_related(user__first_name,user__username)
.prefetch_related(backup_task_schedules, cluster_backup_schedules,remote_backup_schedules)

# 数据库表

class Host(models.Model):
    # 主机唯一编码
    ident = models.CharField(max_length=32, unique=True)
    # 用于显示的别名,用户可自定义
    display_name = models.CharField(max_length=256, blank=True)
    # 主机最近的连接时间
    login_datetime = models.DateTimeField(blank=True, null=True)
    # 从属用户
    user = models.ForeignKey(User, related_name=hosts, blank=True, null=True)

class User(AbstractUser):
    """
    Users within the Django authentication system are represented by this
    model.

    Username and password are required. Other fields are optional.
    """
    class Meta(AbstractUser.Meta):
        swappable = AUTH_USER_MODEL

class ClusterBackupSchedule(models.Model):
    # 使能/禁用
    enabled = models.BooleanField(blank=True, default=True)
    # 删除(不会在界面显示)
    deleted = models.BooleanField(blank=True, default=False)
    # 计划名称,用户可自定义
    name = models.CharField(max_length=256)
    # 计划周期类型
    cycle_type = models.IntegerField(choices=xdata.CYCLE_CHOICES)
    # 计划创建时间
    created = models.DateTimeField(auto_now_add=True)
    # 计划开始时间
    plan_start_date = models.DateTimeField(blank=True, null=True, default=None)
    # 关联的主机
    hosts = models.ManyToManyField(Host, related_name=cluster_backup_schedules)

通过预加载,可以较高的提升速度。

5. 由于项目紧急,并没有所有的线程这样做,而后只是把更新缓存的线程单独以进程外挂在服务主进程中,也就是踢到了另一个进程中来更新我们的缓存,这样就可以给我们服务的进程减压,cpu占用减低。

 

 

def _start_update_workload_cache():
    t = Process(target=update_workload_cache, args=())
    t.start()

def update_workload_cache():
    cmd = r‘python /sbin/aio/box_dashboard/update_workload_cache.py‘
    os.system(cmd)

_start_update_workload_cache()

 以这样的方式外挂在服务主进程上,可以较好的解决cpu占用较高的问题

 

以上是关于django -- 一次性能问题的排查的主要内容,如果未能解决你的问题,请参考以下文章

一次线上kafka消费问题排查

一个C++工程CPU占用100%问题的排查

一个C++工程CPU占用100%问题的排查

如何在 Django Summernote 中显示编程片段的代码块?

Java 问题排查技术分享

记一次Django响应超慢的解决过程