记一次接口服务器网络请求的优化过程

最近做了一个提供给中学生使用的背单词的小型WEB项目部署在一个1G CPU 2G内存 2M宽带的 腾讯云容器服务上接口所在的服务,分了两个实例,各分配了0.1个CPU,196M的内存人数不多的情况下,一切都正常,CPU内存并没有吃紧的情况,相反还很宽裕但是同时上百个人在线的时候,发现部分学生出现加载卡顿的情况,初步分析瓶颈卡在网络带宽上 目前项目中用到的一些静态图片,比如单词配图,单词发音等文件,分离放到了对象存储服务器COS中.主域名通过腾讯的CDN做了加速.因为前端工程中还有相当多的静态文件(css,js,html等)预期的策略是,全部不缓存,但静态文件做30天缓存. 通过CDN的日志分析,发现大量的静态文件也没有HIT,回过头查看发现,CDN的缓存优先级弄反了根据CDN文档说明匹配是从上一条到最后一条,下层覆盖上层,而不是匹配即中止的模式所以我们应该把全部不缓存这一大条件先设到最上,然后是针对静态文件做30天的缓存值得注意的是,这里的静态文件,即前端工程的JS,CSS文件等,在前端工程的编译设置里,务必处理成hash结尾的,如果不带hash区别内容的变化就加CDN缓存,很容易就陷入缓存陷阱而无法正常刷新变更了的内容。 通过这一优化,回源的数量大量减少 通过上边的优化,把静态文件从源服务器上松绑了。很大程度上解决了2MB小水管带来的宽带瓶颈问题。通过CDN日志的进一步分析发现,很多接口在重复请求的时候,依然从源服务器上处理并返回了,即使那些接口的结果内容并没有什么改变,这里就要引入ETag缓存机制了,来专门处理这种内容没发生变化的请求的优化。 项目的接口服务框架采用的是基于 Express的nest.js(也可以基于 Fastify ),而Express默认有开启弱类型的etag,这样根据每次的返回内容,会用md5方式计算一个字段,通过响应头返回来 虽然说服务端因为nestjs基于express,express又通过fresh实现了ETag的if-not-match请求头的比较功能但是使用axios的前端,并没有天然的实现etag的数据缓存,以及if-not-match的请求头的自动添加因此有必要扩展一下axios,使其能够读取服务端返回的ETag头,并缓存返回的内容,然后请求的时候,根据请求的地址,查找ETag值,并添加到请求头if-not-match中。 其中保存数据用到了localforage这个库,这是一个很强大的客户端缓存解决方案 这里的原理就是为axio做一个请求和响应的拦截器,并根据页面地址(含参数)为索引,将ETag以及返回的数据,存在本地缓存中后续请求的时候,就先查找一下本地有没有缓存,如果有,就带上if-not-match请求头字段,服务端用来比较字段和内容生成的etag是否一致,如果一致就直接304返回。虽然依然需要请求服务端,但是通过ETag比对的机制,可以有效的减少没有必要的数据传输,节省带宽进而提升用户体验。 通过ETag实现了单个client与server之间的缓存问题,但如果出现大量用户的访问,服务器还是会一个一个地返回数据针对这种情况,对于一些公用的变动频度不高的接口数据(实时性要求不强的排名数据,可能会更新的单词教材),可以考虑把缓存前移到CDN级,让CDN去缓存接口请求的结果,就可以避免每个用户都去回源问服务器要数据的问题但是想借由CDN的路径匹配来建立缓存策略,却发现腾讯云CDN的全路径匹配必需要有一个明确的文件后缀,以匹配静态文件 因此我计划将现有的各个接口统一都加上一个/data.json这样的后缀,用来模拟静态文件,以符合腾讯云CDN的规则。比如 /user/login » /user/login/data.json要通篇逐个去在数百个接口后添加/data.json是很不方便的一件事情,因此,考虑用点hack的办法来解决这个问题在nestjs中,通过@Get这样的装饰器,顺藤摸瓜,找到了关键词PATH_METADATA,再通过PATH_METADATA进一步找到了最后应用路由的关键方法@nestjs\core\router\router-explorer.js中的 applyCallbackToRouter(router, pathProperties, instanceWrapper, moduleKey, basePath)再进一步在构造函数上打断点,查找route-explorer实例是通过何种方式建立的,最后发现一个改写applyCallbackToRouter方法的路径在main.ts中 先从router-explorer.js中把原本的方法COPY过来,并做一点点小修改,为每个路由加上一个“小尾巴” 测试接口,发现一切OK。即可以用原本的接口地址访问,也可以用加个小尾巴的地址来访问。 返回CDN的缓存设置处,加上新的缓存规则。 因为很多接口是伴随着请求参数来的,比如说某些分页参数的接口。 list?page=1&size=10和list?page=2&size=10 应该被设为不同的缓存对象,因此,在访问过滤处,将过滤参数关闭,好让这种参数不同的路径,能被分别缓存 做了上边一些优化操作后,2MB的小水管也一点无压力了。 再之后的排查中,发现存放在COS上的一些静态文件cache-control设成了no-cache。然而并没有一个方便的办法直接批量去把某个文件夹下的内容都加个统一的头,搜索了一圈,发现了一个同步工具它的操作是拿本地的文件夹里的内容和服务端文件夹做比较,有文件才会去设置服务端的,所以我只好先从COS把文件拉到本地来。如此操作了一番后,cache-control被设成了no-cache这个问题得以解决。