django 性能调优手记

最近在做一项课程作业,是基于 django 框架的一个抢票系统。项目的特点是逻辑比较简单、并发要求高。要求支持 1000 个用户并发访问,响应时间小于 5 秒,错误率 0。

    1. 首先我们知道,python manage.py runserver 是肯定不行的,测试表明支持并发数小于 10。
    2. 使用 uWSGI + nginx。基本配置下,支持并发数达到 120。
    3. 调大系统 net.core.somaxconn 值。
    4. 调整 uWSGI 配置参数,此时支持并发数达到 380。
        • 以 IPC socket 取代 TCP socket,节约协议栈开支:socket chmod-socket,对应修改 nginx 配置文件中 uwsgi_pass
        • socket 队列大小:listen
        • 进程数:processes,大致和 CPU 核心数匹配
        • 线程数:enable-threads threads
        • 关闭日志:disable-logging
        • Python 解释器优化级别:optimize

      至此,我们的 nginx 配置大致是

      server {
          listen 80;
          server_name wx.nullspace.cn;
          location ~ ^/(wechat|api|admin|accounts){
              include uwsgi_params;
              uwsgi_pass unix:///tmp/wechatticket.sock;
          }
          location / {
              root /home/justin/WeChatTicket-1611/static/;
          }
      }

      uWSGI 配置大致是

      [uwsgi]
      chdir=/home/justin/WeChatTicket-1611/
      module=WeChatTicket.wsgi:application
      master=True
      max-requests=65535
      socket=/tmp/wechatticket.sock
      chmod-socket=666
      optimize=2
      processes=2
      threads=2
      listen=65535
      disable-logging=True
      enable-threads=True
    5. JMeter 显示并发数更高时会出现很多错误,尽管响应时间是比较低的。查看 nginx 的错误日志发现全部是 socket() failed (24: Too many open files)。后来又出现了worker_connections are not enough while connecting to upstream,为此魔改了一批参数:
      1. nginx 配置 worker_rlimit_nofile
      2. nginx 配置 worker_connections
      3. 修改 /etc/security/limits.conf
      4. 编辑 /etc/sysctl.conf 修改 fs.file-max 值
    6.  优化数据库锁操作
      • 检查余量、减库存的操作是需要加锁的,代码片段原先是这样的:
        # 减库存
        with transaction.atomic():
            activity = Activity.objects.select_for_update().get(id=actId)
            remainTickets = activity.remain_tickets
            if remainTickets <= 0:
                return self.reply_text("没票了,你来晚啦 :(")
            activity.remain_tickets = remainTickets - 1
            activity.save()

        reply_text 是阻塞的 I/O 操作,将其挪到 transaction.atomic() 作用域外:

        # 减库存
        with transaction.atomic():
            activity = Activity.objects.select_for_update().get(id=actId)
            remainTickets = activity.remain_tickets
            if remainTickets > 0:
                activity.remain_tickets = remainTickets - 1
                activity.save()
        if remainTickets <= 0:
            return self.reply_text("没票了,你来晚啦 :(")

        由此可以减少数据库锁的持续时长。测试表明:此处仅稍稍调整了语句的顺序,便使平均响应时间下降了 20%。

      • 顺便指出,如果去掉锁,可以再下降 40% ( ̄(工) ̄)
      • 可考虑使用 django.db.models.F 表达式代替数据库存取和锁
    7. 优化数据库索引
      • 给“票”表增加了一个字段的索引,去掉了一个字段的索引
    8. 避免 SELECT *,减少数据库查询字段数
      • django.db.models.QuerySet.only 方法

 

文中的测试数据是在一台单核 CPU、 768MB 内存、运行 Ubuntu 16.04 系统的 VPS 上,利用 JMeter 取得的。

“结对编程”初体验

本学期选修了清华大学软件学院刘强老师主讲的《软件工程》课程。近期完成了该课程的一项作业——体验结对编程(Pair programming)。本文谈谈个人体验。

我们实现了该课程的作业“紫荆之声——基于微信公众平台的票务管理系统”的一个功能单元:一个事件处理器 Handler,当用户在微信公众号点击“抢啥”菜单项时,返回近期可以抢票的活动列表。这是我们在该项目开发过程中编写的第一个 Handler。至于为什么这个看似简单的小任务花了30分钟之久,大概是因为我们事先毫无准备,两台笔记本搬过来,装上录屏软件就开始了,什么都还没配置呢( ̄(工) ̄)

总的来说,我扮演了驾驶员的角色,刘斌同学扮演了导航员的角色。我的手放在键盘上实际编写代码。同伴则负责进行审查、解答我提出的各种问题、跟我讨论实现上的细节,比如我们应该用请求体的什么参数来识别这是“抢啥”事件?这个地方写得对不对?是否有库函数完成这个操作?这个异常是怎么回事?返回给用户的活动列表应该怎么过滤、怎么排序?他利用另一台笔记本电脑,有时去查阅文档、有时搜索爆栈网、有时打个断点跑起来试试看。有时充当小黄鸭的角色。

相比两人分工各写各的,我感受到的好处主要有几点:不再有冲突合并的麻烦;一个人写另一个人就同时检查了,省去了一些调试查错的时间,也省去了代码审查的时间;沟通成本降低。缺点也有,比如总的工时数似乎并不会下降,而且约时间、找地点有额外的成本。