老树新花——旧项目改善之浙师60周年庆,点亮全球送祝福

小柊 发表于 2017年02月28日 23时02分20秒

人总在不停地进步,可能若干年后回首看看自己多年以前开发的项目,可能或多或少会笑话自己当年是多么的年轻。最近无聊的时候翻看了一下自己以前的几个项目,发现有很多可以改进的地方。所以决定开一个新系列出来专门讲一下。

这次要讲的项目是去年浙师大六十年校庆的时候行知求知社委托我们计算机协会制作的一个“浙师60周年庆,点亮全球送祝福”的项目。

 

这个项目说白了就是一个点亮地图的项目,很简单。但期间也遇到了很多问题:

 

一、页面加载缓慢

当时一开始学校给的预算极其的过分,好像就只有一百,当时就震惊的没话说。一百就一百吧,琢磨着这活动估计没啥人,就在阿里云上买了一个虚拟主机(虚拟主机独享高级版RMB¥67.00/月,具体配置没记错的话大概就20G硬盘空间+1G数据库空间+1G内存+单核CPU+1M带宽)

结果当天晚上22时左右求知就有人找过来说页面访问特别慢。特别是页面中间的地图半天加载不出来。

 

当时解决方案:

当时就上阿里云云虚拟主机控制台上看了一下虚拟主机的运行情况。其他指标都还算正常,但主机带宽持续满载,判断是网站访问人数过多,虚拟主机的带宽已经不够了。

最后权衡之下一边将整个网站移动到自己的ECS服务器(双核/4G内存)上,然后将自己的服务器带宽临时升级到100Mbps,另外一边修改域名解析,将新用户引导到我的ECS服务器上。由于DNS解析缓存的原因,从23时虚拟主机的带宽才慢慢下降直至次日1时回落到零点。

切换服务器后经初步测试网站加载缓慢现象略有好转,但地图加载还是要花上1~2秒的时间。当时有几个学长找过来,说是我们网站里地图用的ECharts太大(当时直接用的ECharts-all.js,一个文件有949 KB大),由于完美主义作祟,决定启用阿里云CDN加速,但后发现阿里云CDN加速仅支持国内,国外用户访问完全访问不了,后于第二天次日取消CDN加速。

 

现在提出的解决方案

其实我当时只注意到了带宽对整个系统的影响,并没有注意到一些其他地方带来的系统瓶颈,如果是现在的我,我会再除了切换服务器的基础上补上以下解救措施:

 

1.优化ECharts.js

其实百度提供了ECharts在线构造工具,利用这个工具可以重新构造ECharts.js。

构造时仅添加地图图标同时启用代码压缩。

 

最后生成出来的ECharts.js只有734 KB,比ECharts-all.js整整小了215KB。

 

2.优化前台JS

地图加载缓慢的原因除了浏览器需要加载EChart.js,其实还有一个非常重要的性能瓶颈在于Js代码不合理。

在之前的设计里,前台同学写的Js代码在执行页面加载完成时,需要先根据我定义的接口去请求当前所有被记录在数据库中的坐标点。在得到回复后再解析数据绘制地图。

那么,如果服务器没有即时的回复所有坐标点信息呢?

根据前台同学写的Js代码,在没得到服务器返回的坐标点集合前,是不会初始化ECharts地图的。

也就是说,地图的加载需要以下两步完成之后才能进行:

1.需要加载一个949KB的JS脚本;

2.需要等待后台服务器返回坐标点信息。

 

在当时,由行知求知社公众号导过来的用户流量非常大,后台的数据库并没有做缓存缓冲,数据库在当时需要频繁的处理新建坐标、计算表记录总数和拉取整个列表的工作,继带宽瓶颈之后数据库便成为了整个系统的性能瓶颈。而由于迟迟得不到后台返回的坐标点集合,前台JS并没有去初始化ECharts地图,导致页面中间出现一个非常难看的空白,对于外行的用户来说,就是:你这网页怎么这么卡啊

 

解决方案很简单,修改前台小哥写的Js代码,在loadData之前,抢先初始化ECharts地图,就算没有数据,先把地图展示给用户,也能提高那么点用户体验。

 

【后日谈】

这篇博客写了一半的时候无聊去百度ECharts 3的官网晃荡了一圈,在这个《微博签到数据点亮中国》示例项目中ECharts有个载入动画的,仔细研究了一下代码,发现只需要调用一下对应的函数showLoading()/hideLoading()就好。日常怀疑自己智商哈哈哈。

 

3.数据库做Cache缓存

刚刚也说了,在解决掉服务器带宽瓶颈后,数据库的性能才是制约整个系统性能的瓶颈。要解决数据库瓶颈怎么办?当然是Cache啊!

在后期,数据库表内有1K+的记录,密密麻麻的画在地图上,并不会有用户吃饱了撑着没事干去一个一个数它,用户最后只会在意自己的送祝福排名。

全表拉取数据,性能自然不会高,不如在用户送祝福添加坐标点记录时,复制一份:一份存到SQL Server里做持久化,一份放到Redis或者MemCached做Cache。我在这里选择用Redis的List做Cache,使用StackExchange.Redis连接Redis。在StackExchange.Redis里,向列表Push项目的时候是返回项目的位置,也就是用户点赞的名次。在此优化后,项目运行时SQL Server只需要负责数据持久化就可以,大大减轻了数据库的负担。

下图为实际优化效果(由于Entity Framework是冷启动,数据略有夸张,EF冷启动完毕后单次单线程扫表耗时大概200ms+):

 

二、架构优化

在当时开发这个项目时,也算是赶工完成的,首要目标是跑起来,而架构优化什么的就没有特别的放在心上。我们来看一下整个项目的Web层,我当时创建了两个Controller:HomeController和BackgroundController。

HomeController里就只有一个Index的Action,用于展示网页,而BackgroundController则负责前台发回的Ajax请求,也就是取得所有坐标点和添加坐标点两个功能。这两个Action返回的都是JsonAction。

反正返回的都是JsonAction干嘛不直接设计成WebAPI啊!

为什么不用Controller而改用ApiController,原因并不是什么性能提升,而是WebAPI更专注于API接口的设计,更抽象且舍去了View的设计。

 

现在提出的解决方案

撤销BackgroundController,同时创建一个PointController的ApiController,并规定GET方法请求时返回所有点坐标,POST方法请求时,创建新点。

修改完毕后,修改相应前台Ajax代码即可。

 

三、恶意数据提交

由于没有做恶意提交拦截,导致活动次日凌晨出现了非常壮观的赤道点景象:

 

一开始以为是程序错误,结果用管理工具登录到数据库一看,发现是被人攻击了。

 

估计是攻击者以为这边是SQL注入点,结果发现注入不了(因为用的Entity Framework)之后一气之下刷了一遍吧。

 

当时解决方案:

当时正好处于快睡了的状态,然后从床上爬起来做过滤代码。

1.用户界面层:在BackgroundController中AddNewPoint Action里测试代码,如果传入参数lng或lat两者任意一项无法转换为double类型就视为攻击直接丢弃数据并返回错误。

2.业务层:在添加点的时候进行校验,如果User-Agent中包含Python,则视为攻击直接丢弃数据并返回错误;如果lng和lat同时为整数,则视为攻击直接丢弃数据并返回错误。

 

次日醒来,发现晚上解决方案中红色部分与前端约定有冲突(前端如果无法请求到地理位置则lng和lat返回负整数),导致众多用户无法正常点赞,遂另开白名单,通过前端同学返回的-333值。

 

现在提出的解决方案

现在看来当时还是太嫩了。对付机器提交,最好的方法就是验证码。

但如果是在用户点赞的时候要求输入验证码,未免就有些太不人性了。

现在的解决方案如下:前端同学在点赞完成后会将点赞完成的信息存在Cookies中,如果服务器没有检查到Cookies,则会在页面中埋藏一个随机序列串,用于在点赞时同时提交这个随机序列串,如果用户提交的随机序列串与服务器上保存的随机序列串一致,则通过验证,另外检查User-Agent中是否存在“Mozilla/5.0”字符串,如果不存在,则直接判定不是浏览器访问,丢弃数据并返回错误。

虽然这样的方法对还是会存在一定的问题。但就目前情况来说,已经可以足够防御大部份扫描器的扫描注入了。不过需要注意的是Asp.net WebAPI默认不启用Session,解决方法可以点击这里查看

【关于这个问题,我会在以后继续关注有没有更优解。】

 

四、后记

至此,对这个浙师60周年校庆点赞项目的第一次反思就结束了,虽然可能还有别的问题或缺陷,不过这些嘛,就交给以后的自己慢慢反省啦。

 

 

小柊

2017年2月28日 22:55:32

相关文章

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注