高性能验证码图片设计
介绍
在涉及敏感信息输入或者用户信息获取、校验,经常会使用图形验证码提高安全性。在保障安全性的同时,验证码图片的生成也会给服务带来一定的性能损耗。如果验证码图片获取频率大或者被人恶意攻击,没做优化的话,会让服务器性能瞬间拉低,导致响应变长、拒绝服务。
验证码图片由什么组成呢,直观上看就是文字和背景图。文字并非无中生有的,而是来源于我们提供的字体库,有了字体,就需要将字体绘制到图片上,计算字体的偏移位置,同时为了增加干扰,也会增加一些不同颜色的线条。
验证码图片生成对服务性能的主要影响:
- 加载字体文件(消耗磁盘IO)
- 绘画(消耗CPU)
优化思路
我们的目的无非就是获取到图片足够的快,首先想到的就是绘图足够的快,那性能就得上去,如果是使用PHP开发的话,那就得使用好的硬盘,比如固态硬盘,把io速度提高。 还要加CPU,提高算力。虽然达到目的了,但是服务器成本也会上去,后期达到瓶颈还得持续投入,这明显不是我们能接受的,一个非核心的东西却要消耗大量的成本,当然你有钱,请随意。也可以从代码优化入手,但这一般都是现成的开源库,会有维护者持续迭代优化,优化空间不大,费时费力,带来的效益低。
从验证码这个场景来看,其实并不需要一次请求就要产生一个图片,我们更多的是想起到一个验证的作用,把门槛稍微提高一些。只要用户看到的不会一直都是同个图片就行,在一定时间内可能会看到一样的图片是可以接受的。
从上可以得到一个优化思路:资源复用(缓存)。
设计方案
对于加载字体文件而言,其实就是一个不会变的资源,只要加载一次到内存里,重复使用不进行释放就能减少磁盘IO。
对于绘图,只要初始化时候,提前把图片绘制出来,保存一批次在内存里就行,当请求过来,只要从中挑取一个。当然图片要有淘汰机制,不能一直不变,不然容易被黑产枚举攻破,所以需要定时生成新的图片进行替换,保证图片是动态变化的。可以看出CPU只是在初始时候和定时会消耗,可以减少很多。跟之前的情况简单对比的话,如果qps是100来说,1min就会有6k的cpu使用消耗,而如果定时5s生成新图片,那1min只会有12次的CPU使用消耗。
功能开发
语言的选择会影响具体方案的选择,
比如PHP,作为一个解释型语言,每一次请求处理都是重新执行,包括内存也是执行完就释放,所以无法做到提供服务的同时只加载一次字体,减少绘画。退而求其次,可以选择将功能分离:
-
额外开发脚本,脚本常驻运行,只加载一次字体,然后定时进行绘画,然后将图片资源放在数据组件里(比如redid、mysql)。当获取验证码图片时候,接口按一定策略从数据组件获取,这时候就只消耗网络IO。
-
也可以将图片资源不放数据组件,直接生成本地图片,然后与服务共享图片,接口直接从本地读取图片,这时候就只消耗本地磁盘IO。
而使用Go做接口时,是可以常驻提供服务,这样就可以将字体加载进内存进行复用;并且Go提供了协程的能力,就可以异步去执行绘画,当获取图片时候,就可以自己在进程内存内获取,相比PHP就可以把网络IO或者磁盘IO也给省掉。
接下来使用Go介绍 github项目
-
初始化两个切片,用于指向图片内存。一个切片作为实际使用,另外一个作为备用进行切换,相当于主从。
-
初始化资源池,存放图片的内存块,淘汰图片时,把内存块重置后放回资源池,避免频繁申请内存
-
初始化指向偏移量,通过切片偏移量获取对应图片资源
-
协程A定时修改切片的指向偏移量,让获取到的图片保持变化
- 协程B定时绘画,将指针放入备用切片,当备用切片补充完后,就会进行切换(主从)
对于接口而言,它只要去读取偏移量指定的图片就可以,复杂度O(1)
口说无凭,进行下压测对比优化前后的效果。
运行环境为k8s,限制CPU核心为( request:1, limit:2 ) 并发4 总数2000
- 优化前
─────┬───────┬───────┬───────┬────────┬────────┬────────┬────────┬────────┬────────┬────────
耗时│ 并发数│ 成功数│ 失败数│ qps │最长耗时│最短耗时│平均耗时│下载字节│字节每秒│ 错误码
─────┼───────┼───────┼───────┼────────┼────────┼────────┼────────┼────────┼────────┼────────
1s│ 4│ 43│ 0│ 44.49│ 191.49│ 43.51│ 89.92│ │ │200:43
2s│ 4│ 94│ 0│ 47.75│ 191.49│ 43.51│ 83.76│ │ │200:94
3s│ 4│ 141│ 0│ 47.47│ 191.49│ 31.42│ 84.26│ │ │200:141
4s│ 4│ 190│ 0│ 47.87│ 191.49│ 31.42│ 83.56│ │ │200:190
5s│ 4│ 236│ 0│ 47.62│ 191.49│ 31.42│ 83.99│ │ │200:236
6s│ 4│ 281│ 0│ 47.24│ 191.49│ 31.42│ 84.67│ │ │200:281
7s│ 4│ 328│ 0│ 47.15│ 191.49│ 31.42│ 84.83│ │ │200:328
8s│ 4│ 376│ 0│ 47.25│ 191.49│ 31.42│ 84.67│ │ │200:376
省略N行...
************************* 结果 stat ****************************
处理协程数量: 4
请求总数(并发数*请求数 -c * -n): 2000 总请求时间: 43.185 秒 successNum: 2000 failureNum: 0
************************* 结果 end ****************************
─────┬───────┬───────┬───────┬────────┬────────┬────────┬────────┬────────┬────────┬────────
耗时│ 并发数│ 成功数│ 失败数│ qps │最长耗时│最短耗时│平均耗时│下载字节│字节每秒│ 错误码
─────┼───────┼───────┼───────┼────────┼────────┼────────┼────────┼────────┼────────┼────────
1s│ 4│ 547│ 0│ 552.61│ 16.71│ 3.64│ 7.24│ │ │200:547
2s│ 4│ 1068│ 0│ 538.69│ 16.71│ 3.64│ 7.43│ │ │200:1068
3s│ 4│ 1548│ 0│ 519.89│ 18.24│ 3.64│ 7.69│ │ │200:1548
4s│ 4│ 2000│ 0│ 510.69│ 18.24│ 3.64│ 7.83│ │ │200:2000
************************* 结果 stat ****************************
处理协程数量: 4
请求总数(并发数*请求数 -c * -n): 2000 总请求时间: 3.965 秒 successNum: 2000 failureNum: 0
************************* 结果 end ****************************
从上可以看出优化前的qps只有不到50,CPU就被限制了。优化后可到达550,CPU几乎无影响,稳定的运行。