学习构建之法(2):课程总结

 

 

年底、期末,在繁忙之中,我和我的团队完成了“微学堂”这一产品。伴随着它的交付,《软件工程》结课了。我现在是本科三年级,两年半以来修了五十多门课,应该说这一门是挑战度最高的。最后几周,不时有通知“下周的课不上了,各个小组自己做团队项目”:相对于课堂听讲,课外需要付出的精力之多,由此可见。

即使以代码行数衡量并不是,我感觉这个团队项目是我经历过的规模最大的项目,大到另我有无法驾驭的恐慌。不过这样说也不公平,这种“out of hand”的感受不仅来自这个项目本身,也有来自其他课程、辅修专业、社会工作等等带来的压力。然而话说回来,我直观上的感受就是,作为组长我不清楚每一个组员都具体做了什么、用什么方法做的。不可能有一人 review 每一段代码;不可能有一人了解项目中的全部实现细节;更不可能凭一人之力完成。

与此相比,此前的和本学期其他的“大作业”都只是小打小闹。不点名提及这学期的某些课程的某些 Project:一人花几个小时就做完,好意思说它是 Project?顶多算个 Assignment 好吧。如果把这些都算上,那么我这学期可能做了 20 个大作业,恩,说出来挺唬人(划掉)。

 

Donald Knuth 有一句名言:过早优化是万恶之源。项目中还存在一些过度设计的问题。其实这些词我都不是很懂,通过这个团队项目,只是感觉对此有了一些浅薄的理解。

中途出现的需求变更对我们造成了不利影响——来自教师的一条非功能性需求:我们被要求使用另一小组提供的 API 服务,而非自己编写相关模块。为了保证进度,我没有选择重构,而是把这个 API 服务做了一层包装,以保持接口不变而替换原有模块。这带来了架构上的冗余和“不优雅”。

这里所说的模块是一组爬虫。爬虫总有个运行频率的限度,而用户总是希望得到最新的信息,而且希望在最短时间内得到最新的信息。针对这一矛盾,我们设计了后台定时触发的“自动刷新”和用户手动触发的“强制刷新”这两种不同的刷新,同一个接口设计同步和异步两种调用方法。结果,因为他们 API 服务没能支持,全都白费功夫啦。

值得一提的是,做 API 服务的那组同学似乎并没有考虑运行频率的限度的问题。他们对爬虫的目标,也就是我校网络学堂和综合教务系统,发起高频次、高并发的请求,形成 DoS 攻击,造成网络学堂压力过大、反复宕机。运行着他们 API 服务的那台服务器,其 IP 被学校封禁。不巧的是,该台服务器是由我管理的。简言之,我因为他们被查水表了……这导致我不得不花大量的精力来与上级管理人员沟通,撰写需要同时抄送给三四个单位的整改报告——这影响了我们的进度。

我们在最初设计时,在安全性上花了很多精力。我们使用 OAuth,我们使用 https,避免用户信息的泄露。实现时,微信公众平台 OAuth 那一套牵扯了我们更多的精力:除代码编写外,开发环境搭建、调试、测试等方方面面都有复杂性的增加。结果呢,直到最后,我们的上游,也就是上文提及的 API 服务,也没有实现任何对调用者身份的验证。数据全部明文传输。他们的 mongodb 数据库甚至不设密码,直接暴露在外(这也是上文提及我被查水表的原因之一,原话是“存在高危安全隐患”)。

只要有一环出了问题,整个安全链条就会断掉。API 组糟糕的安全性意味着我们的用户的一切信息都会被公开在国际互联网上,也就意味着我们在安全性上的努力全部白费——除了在文档中写上一笔,期盼因此获得作业成绩的加分。好气啊!

 

花了这么多篇幅吐槽 API 组。我们自己得到的教训是:应该循序渐进。特别是在敏捷的语境下,不能一开始就想着搞出完美的设计“一步到位”,不能指望一开始就把所有细节都考虑周全。一开始最好是设计得简单些,keep it simple and stupid,在反复迭代中进行改进,边写边重构。

通过这个项目,团队管理上的经验也积累了不少。把一个团队带好,这是非常困难的,我一直觉得我在这方面没有缺少天赋,适合埋头干活,而不是做一个管理者。这回做组长,让我在这方面增加了一些经验,也增加了一些自信(笑)。具体的经验可以列一些,比如重要的事情不能只口头说——在微信群里说也算“口头”——要形成文档,形成书面的东西。

 

最后说回这个课程本身吧。除了做项目的锻炼之外,通过课堂讲授,我的理论水平也得到了提高——SC、CC、MCC、CDC 等度量测试覆盖的一堆词儿;系统的 actor、stakeholder,UML 状态图、顺序图、活动图,用户故事、用例建模,需求工程的那一套理论;接触了几种设计模式;关于团队、流程一些成熟经验……作为一门工科专业课,竟也有了一分管理学的味道。

建议就没什么了,各方面都挺好的。只有一条,那就是一组来搞 API、其他组使用的这种玩法,估计明年不会再搞了吧(逃)

 

这是《软件工程》课程系列博客的最后一篇。
相关文章:
学习构建之法(1):写个微信抢票
django 性能调优手记
“结对编程”初体验

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分钟之久,大概是因为我们事先毫无准备,两台笔记本搬过来,装上录屏软件就开始了,什么都还没配置呢( ̄(工) ̄)

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

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