需求
这个项目是刚开始做Web开发的时候,做的第一个项目,之前都在写爬虫.
项目大概是这个样子.
项目本身就是当收到客户查询请求之后,去供应商接口查询数据,解析结果并返回给客户,哪怕在我当时没什么经验,也知道这个蛮好做的.
当时定下的是使用Django+MySQL来做.
架构设计
这是第一版设计的架构图,然后一直用到了现在.(不过并不是因为设计的多么合理,而是重构不如重写,后来我用Golang重新开发了一套基于同一个数据库的系统,来接入新产品.)
设计架构的时候,主要是考虑了一下几点.
- 可分布式部署,来应对调用量的增加
- 保存业务日志,整理二手数据降低成本
- 业务日志与业务逻辑分离,尽量减少数据库操作来提高响应速度
- 预充值,实时计费
对应这些需求,我首先确定了使用Celery+Redis作为消息队列,处理保存日志的任务,同时将Redis作为Django的缓存,提高用户鉴权速度.
鉴权主要包含两个过程:
- 验证身份
- 验证余额
为此,我在缓存中缓存了用户密码相关内容,余额,以及对应调用API的价格,保证每个用户24小时内只从数据库读取一次信息.
当用户一次查询完成之后,将需要记录的内容通过celery发送到worker进行数据库写入操作.降低业务阻塞时间.
(事实证明,这一顿花式操作,没太大用,后期业务不是特别多,而且我们作为中转方,时间开销99%都在等待远程服务器返回结果,做这些只是保证没有出现低级错误而已)
选用的工具及理由
- Django:相比来说使用Django更多是因为认为开发会更快一些,一是自己了解一些,学习时间更短,二是ORM和ADMIN都是现成的,我一个人做这个很重要.
- Celery:..应该没什么其他选择了.
- Redis:同时作为Celery的Broker和Django缓存,肯定比RabbitMQ+Memcached强,尽量降低系统的复杂度了.
- MySQL:这个位置上,其实有更好的选项Postgresql,因为不熟悉,我选择了自己更熟悉的Mysql,而且一开始就有避免频繁数据库操作,选用哪个完全不会构成瓶颈,SQLite都行,就是有点low.
- 阿里云SLB:设计之初并没有决定使用哪个云平台,不过确立了Nginx+Gunicorn的结构,后来迁移到阿里云之后,阿里云的SLB的确很方便,因为他可以通过调整权重来停用\启用服务器,实现热更新重启
- Gunicorn:打败uWsgi的原因是这个部署更简单,方便.
- Supervisor:可以说很好用,无脑,用了它,夜里也很安心.不过有一个问题是,这玩意不能关掉Celery的进程,每次重启Celery都需要ps + xargs + kill来关,具体是什么原因,一直没有深究.
遇到的问题
网络请求效率低下
前期我采用了requests来做网络请求,可以说我快哭了,慢的令人发指...主要体现在无法和Gunicorn+Gevent配合,请求数量稍微提高一些,效率蹭蹭往下掉.定位这个问题我花了好几个小时.也没有什么好的解决思路,尝试把requests替换成urllib3,居然就解决了.
不过时至今日,我也没有解决不同worker之间共享一个连接的问题,urllib3的连接池只能做到在一个worker里复用,好在这也算不得瓶颈,就搁置了.
业务层设计不合理
这一段实际上是我最想说的,因为这个是我设计的第一套系统,让我吃了挺多堑,涨了不少智.
数据表设计的问题
我拿到项目需求的时候,实际上只有一个数据供应商,三个数据接口,我所需要做的就是把这三个接口加一层壳而已,我开开心心的在数据库建了两张表,日志表就不说了:
class User(models.Model):
class Meta:
db_table = u"用户信息"
account = models.CharField(max_length=32, unique=True)
password = models.CharField(max_length=32)
price1 = models.FloatField(default=0.5)
price2 = models.FloatField(default=0.5)
price3 = models.FloatField(default=0.5)
total = models.FloatField(default=0)
cost = models.FloatField(default=0)
def __unicode__(self):
return self.account
美滋滋,开搞!感觉自己萌萌哒,稚嫩的我甚至都没有考虑api的访问权限,就这么把系统给开发完了.
后来他变成了这个样子:
class User(models.Model):
# todo
# 此处由于初期端口较少,在定义价格时采用
# 了一个用户的价格和账户信息写在一条里的方式.
# 后期端口增加,拓展性较差,较难维护.
# 合理的设计结构为[用户+接口+价格+是否开放...]来储存价格
# 此处应择期重构.
class Meta:
db_table = u"用户信息"
account = models.CharField(max_length=32, unique=True)
password = models.CharField(max_length=32)
price1 = models.FloatField(default=0.5)
price2 = models.FloatField(default=0.5)
price3 = models.FloatField(default=0.5)
price4 = models.FloatField(default=0.5)
price5 = models.FloatField(default=0.5)
price6 = models.FloatField(default=0.5)
price11 = models.FloatField(default=0.5)
price12 = models.FloatField(default=0.5)
price13 = models.FloatField(default=0.5)
price14 = models.FloatField(default=0.5)
total = models.FloatField(default=0)
cost = models.FloatField(default=0)
def __unicode__(self):
return self.account
事情搞成这样,大家都不想的,谁能想到一开始的三个接口,搞到后来十几个呢(可能只有我没想到...)?
商务每次有新的接口谈进来,我就不想干活.
- 写逻辑
- 数据库里添加字段设置价格(这个表居然是横着长的,卧槽太蠢了)
- 同步数据库(数据库加了字段要保证线上系统不停机,每次小心翼翼)
- 数据库写入Worker里边的原生语句修改,能够读取和修改新字段(这个看上边的model看不出来,事情其实更复杂一些)
- 重启worker(celery不好关,...ps -ef|..xargs...|kill)
- 负载均衡调权重为0,重启服务,恢复权重.
这一套想想我就觉得麻烦,每次都觉得当初脑子一定是进水了,为什么设计成这个样子?
这也让我认识到, 初期的架构设计真的非常重要,不管是什么样的精英团队,将来都会遇到无法预料的需求,微服务,低耦合这些,只有真的体会过切肤之痛之后,才会甘之如饴.如果一开始就瞎搞,后边心态比系统先崩.
终于有一天我忍不了了.重写了一套系统,下文再谈.
RSA加密
本来这个不值得拿出来一谈,但是这一点我折腾了挺久,权当凑字数吧.
加密这块之前接触的不多,供应商那里的系统是Java,我这边是Python,RSA搞起来比较麻烦.
对方提供的公钥是pkcs8的,而python支持的是pkcs1,我这边转来转去的挺痛苦的,具体当时是怎么折腾的都忘记了.
提这个主要是想说,Rsa这个东西,python做起来还是挺慢的,在web开发过程中,这种情况还是比较少见的.如果条件允许的话,还是用C写个.so来调用更好一些.
Restful
给出去的接口如果明显不符合restful规范,挺掉价的,大家都是搞技术的,你自己写的代码别人是看不到的,但是你设计的Url和Response别人一眼就能看到,无论是uri还是参数命名,都是一个公司在技术上的门面.
有些公司管理比较到位,丢人在公司内部丢丢就完了,这个业务我自己就做主了,导致后来看到之前设计的uri都有点脸红,不提了.
重写
后来这个项目由于写的太烂,我选择了重构.没有继续使用Python,而选择了Golang来写,就当练手了,避免自己怕麻烦推进不下去.
自己做了一段时间开发,虽说没人教没人带,但是也算摸索除了点方法.
这次采用了链式的请求处理,把常规操作全部交给中间件来负责,只关心业务逻辑.
有Golang的天生异步,连消息队列都省了,请求返回之后异步处理日志问题.
现在新增一个接口,数据库不需要改动,只需要新增记录即可.
INSERT INTO `USERINFO` (`用户名`,`接口ID`,`是否可用`,`价格`)
VALUES ("user",16,1,0.5);
在经过权衡之后我并没有把更多的配置放在数据库中,比如供应商接口地址,己方定义的url等,而是写在了代码中的setting文件中.由于golang的性能优势,我也暂时没有引入缓存,鉴权操作需要操作2次数据库的Select操作读取密码和连接权限.我认为目前这不是瓶颈,没有必要增加复杂度.后期如果需要,也只需要在鉴权中间件上进行修改就好,仅仅是1个小时不到的工作量.
后记
这个项目从开发到现在大概有半年时间了,断断续续的修改,到最后的重写,其实并没有什么特别的困难,也没有什么高端的操作,但是作为第一个web项目,让我思考了很多,有不小的进步.写下这个权当警醒自己不要停止学习吧.