作者 | 金色旭光
在过去的一个半月里我第一次作为后端开发组长角色参与公司项目从0到1的开发,记录这一次开发的经历。
1、背景介绍
首先说明一下背景。我所在的公司是做智慧社区相关业务,开发的项目是系统升级工具,方便公司实施同事安装和升级系统。
参与后端开发一共四个人,包括我在内。其他三个同事有一个是应届生、两个做大数据的。按照公司的技术规划,对内项目开发节奏要短平快,用Python语言完成,对外的项目一律用Java语言完成。
项目经过正常开发生命周期,包括需求采集、产品设计、系统设计、详细设计、编码、测试等过程。其中详细设计就是针对接口做的详细设计,一共用时3天完成,设计了45个功能点,包括40个接口和5个初始化准备工作。编码用时计划为3周。最终在时间点之内完成了相关的开发。
我在这次开发过程中担任的是组长的角色,主要的任务包括:
(1)项目框架的搭建。本次开发是一个从0到1的过程,在此之前并没有Python项目的框架。
(2)关键技术的实现。包括通用接口,复杂的技术点。
(3)任务分配。所有接口根据任务量分配给指定的成员,完成最多的接口开发。
2、项目框架搭建
Python做web开发常用的项目框架其实并不是很多,我的候选项有三个:
Django 前后端不分离框架、Flask 最容易上手的框架、FastAPI 异步高性能框架。
对比这三个框架,我从业务逻辑、公司技术栈、复杂度等三个角度出发,选择了Flask。
业务逻辑
业务逻辑对性能并没有特别要求,就是通过接口调用运维的ansible脚本,没有高并发,计算密集等任务,所以三个都能满足。
技术栈
公司技术栈是前后端分离,所以Django这种前后端不分离的框架并不适合,虽然Django也可以做纯后端开发(防杠)。
复杂程度
复杂度来说肯定是Flask最简单。Django号称大而全,配置复杂。FastAPI是异步框架,需要学习异步编程,虽然用来做同步框架也很丝滑,但是学习成本需要增加很多。其他三个同事都没有做过Python项目,所以尽量减少学习成本。
经过这三个方向的衡量,最终确定了Flask框架,搭配peewee orm数据库框架。核心的技术包括:
(1)web框架 Flask
(2)数据库ORM框架 peewee
(3)数据库 sqlite
(4)运维脚本执行模块 subprocess
(5)WSGI服务 Gunicorn
(6)代码检查工具pre-commit
在编码前我已经准备好完整的项目框架,写好了数据库CRUD接口的demo,后续开发过程同事模仿相关接口,一定程度上提高了开发效率。
3、关键技术实现
带团队开发并且是带领成员第一次做Python项目,自然要将有挑战的任务安排给自己。在关键技术的实现上挑选三个有代表性的讲解。三个分别是:系统命令执行通用接口、流式日志、sqlite 多线程写问题解决。
系统命令通用接口
项目主要用于公司开发的其他系统安装和升级,因此需要调用运维人员用ansible编写的相关脚本。调用的ansible脚本格式如下:
ansible-playbook 03.mysql.yml
ansible-playbook 08.zk.yml
需要到指定的路径下执行如上的命令。在详细设计阶段就知道需要使用Python调用系统命令的工具,所以就让应届生同事调研了subprocess模块,输出相关文档。一来是给新人一个学习方向,再则借这个机会熟悉项目需要的技术。
在开发阶段根据对相关模块的理解,完成了通用接口的开发。写通用接口切忌朝令夕改,依赖它的代码也要随之变化。一两次还能接受,次数多估计要被问候祖宗了。所以该接口实现程度不仅仅是写完,而且是自己亲自调用,确认没有问题才宣告完成。
在没有完成之前耐着性子调试,直到没有任何问题才在群里告诉其他开发人员。整个系统中需要大量的调用该命令执行脚本,最终也都比较顺利的完成,没有因为接口造成的bug。
流式日志
按照产品的设计,当一个组件在安装时需要在web页面上展示日志,并且日志的格式要和终端中安装日志一样,也就是一行一行的滚动打印。产品对日志的要求是全量滚动展示,刷新页面要能够再次全量展示出来。为了实现该功能,调研了三个方案:
一、定时刷新。缺点:日志有几万行,每一次读取全部日志给前端,前端会卡顿,而且打印也不连续,体验不好。
二、websocket。可以完成后端向前端的主动推送,但是刷新页面并不会从头开始推送。
三、流式响应。可以将大块文件切分成小块分批传给前端,刷新页面时会再次从头开始推送,符合要求。
经过对比最终使用了流式响应,也就是ChatGPT那种响应的方式。但是需要解决一个问题:什么时候结束推送?因为安装一边向文件中写入日志,流式日志一边读出日志,如果日志已经读完了安装还没结束,那这个时候肯定需要等待而不是停止响应。
解决办法是在安装完成之后插入标记字符,当流式日志读取到标记字符时就表明结束了,没有读取到标记字符则等待。核心代码如下:
def log_flow():
query = request.values
log_path = query["log_path"]
def generate():
with open(log_path, "r") as f:
while True:
chunk = f.read(800)
if not chunk or chunk.isspace():
time.sleep(0.1)
if chunk == 800 * "-":
break
content = json.dumps({"content": chunk})
yield f"event: message\ndata: {content}\n\n"
time.sleep(0.05)
return Flask_response(generate(), mimetype="text/event-stream"
效果:
sqlite3 多线程写问题
在数据库存储这一块,领导钦定用sqlite3,咱也据理力争过用MySQL,but无效。领导说该项目只需要一个轻量的数据库即可,sqlite3轻量,所以就很合适。而且其他项目中已经使用的非常成熟了。好吧,既然领导坚持,我也只能硬着头皮上了。
开始还没问题,到了项目开发中后期就发现问题了,接口经常报错:
File "/home/ljk/.virtualenvs/idt_dev/lib/Python3.8/site-packages/peewee.py", line 3246, in execute_sql
cursor.execute(sql, params or ())
File "/home/ljk/.virtualenvs/idt_dev/lib/Python3.8/site-packages/peewee.py", line 3014, in __exit__
reraise(new_type, new_type(exc_value, *exc_args), traceback)
File "/home/ljk/.virtualenvs/idt_dev/lib/Python3.8/site-packages/peewee.py", line 192, in reraise
raise value.with_traceback(tb)
File "/home/ljk/.virtualenvs/idt_dev/lib/Python3.8/site-packages/peewee.py", line 3246, in execute_sql
cursor.execute(sql, params or ())
peewee.OperationalError: database is locked
查询之后发现是sqlite3不支持多线程写。sqlite3支持事务,是用库锁来完成的。当一个写入开始后,整个数据库都加锁了,再次有写操作就会报错。
这个问题首先从技术上是不好解决的,sqlite3的架构设计就是如此,还能让它支持多线程写吗?只能通过业务逻辑解决。经过一次会议讨论之后,得出几个解决方法:
- 分库。将写操作分到不同的库里完成。既然写操作会锁库,那就分出不同的库,就能避免锁库的问题。
- 全局写队列。将所有的写放到一个消息队列里面,将多线程的写变成串行的写。
- 全局写标识。所有的写操作开始前判断是否有可写标识,能写入就写入,否则接口返回,告诉前端数据库繁忙。
经过投票,大家一致决定用第三种方式实现,技术难度最小,代码侵入性最少。因为第三种方案是我提出来的,所以最终由我来完成。具体的过程可参见另一篇文档《peewee操作sqlite锁表问题分析》。
最终是解决了该问题,虽然不是优雅,但是在目前的时间成本和风险控制上局部是最优解了。后续将调研peewee这个ORM框架提供的sqliteQueueDatabase实现写队列。
摘录于peewee扩展插件playhouse:
SqliteQueueDatabase可作为常规SqliteDatabase。如果你想简单点 read and write 从访问sqlite数据库多线程.
SQLite在任何给定的时间只允许一个连接写入数据库。因此,如果您有一个多线程应用程序(例如Web服务器)需要写入数据库,当一个或多个尝试写入的线程无法获取锁时,您可能会偶尔看到错误。
SqliteQueueDatabase 旨在通过一个长期存在的连接发送所有写查询,从而简化操作。好处是,您可以看到多个线程在向数据库写入时没有冲突或超时。但是,缺点是您不能发出包含多个查询的写事务——本质上,所有写操作都在自动提交模式下运行。
4、个人感受
第一次带团队开发,才明白很多事情。
做项目的主人公
第一真正理解什么叫主人公意识。各种文章都说要对项目有主人公意识才能成长更快。我感觉只有站在这样一个位置上才能感受到这种意识。
项目进度的第一责任人就是你,项目中出现了大大小小的问题都是找你。领导会问题项目进度如何,有没有阻塞,能不能按期完成?队员会问这个校验框架是否ok?这个语法有没有问题?测试会找你说这个bug该给谁的?所以你必须对这个项目了如指掌,小到代码的一个配置项,大到工程进度的把控。开发过程中有任何问题都得及时顶上,组长就是一块砖,哪里需要哪里搬~
团队和谐
再说说团队的和谐。以前做一个小开发,只要完成自己的任务就可以了,团队的氛围影响我写代码的速度吗?带团队开发就不一样。团队中有各种特点的同事,有埋头苦干不汇报进度的、有能力强脾气差的、有脾气好进度慢的。总之各种性格的人都会存在。
本次开发中就遇到了一个能力强脾气差的,看到技术上小问题就直接群里开怼,谁不是热血小青年?第一次遇到这种情况可想而知。领导不得不为此找我们谈话一两次,甚至惊动了大部门领导。那段时间团队氛围特别差,没有人说话。我也不敢多说什么,害怕气氛更差,项目不能如期完成,到那个时候不是我的问题也变成我的问题了。所以只能选择忍一忍,尽量回避分歧。好在领导的谈话起到很大的作用,该安抚的安抚,该批评的批评,后来也没有发生语言的冲突,顺利按期完成项目。
总结
第一次带团队做项目对我来说是一次挑战和提高。从技术层面讲让我以后面对技术选型时能以更高的角度看待问题;从个人角度讲这是一次难得的机会让我负责开发团队,对接测试团队、前端团队、运维团队等。这对我的沟通交流都是一次锻炼。
最后,希望下一次做的更好,让所有组员都能有一些进步。