背景
让每一个用户获得稳定及时的页面体验,是前端工程师一直努力的方向。
爱奇艺官网主站作为一个内容资源丰富的视频网站,需要经常进行线上或线下节目、各种活动配置等运营调整。,对页面SSR服务的可用性和稳定性有极高的要求。
2019年之前,爱奇艺官网主网站的SSR使用的是CMS平台编写的Velocity模板,用Java编译。优点是渲染速度快,但缺点也很明显:
在CMS平台的开发体验并不好:没有传统IDE那么方便,无法配置快捷键和安装插件,导致开发效率低。
前后端代码结构不同:因为后端使用Velocity模板,前端需要Vue,所以前后端代码结构不同。
破坏Vue组件的封装:由于Java无法编译Vue组件,所以所有的Vue组件都需要在CMS平台中按槽编写,以达到SEO和SSR的目的。
基于以上原因,我们决定使用Node进行SSR。因为我们的前端框架是Vue,所以选择了支持SSR的Nuxt框架。
使用Nuxt进行SSR的难点不在于如何使用Nuxt,而在于如何维护这项服务,保证其性能和稳定性。所以本文就不介绍Nuxt的用法了,其语法可以参考官网。这里主要从性能、缓存、限流、容灾、日志等方面介绍如何保证Nuxt服务的可用性和稳定性。
提高0nuxt稳定性的途径
2.1页面配置
首先介绍一个非常重要的配置文件。在我们项目的根目录下,会创建一个页面配置文件来存储每个页面的一般配置,比如缓存配置、清除信息、主题颜色配置、广告信息配置等。文件导出一个对象,键值是页面的路由器名称,Value值是页面的配置信息:
代码块 1然后,根据请求的路由信息,我们在Nuxt插件中读取相应的页面配置,并将其注入到所有组件实例中,以便随时访问:
代码块2因此,您可以在组件中的任何位置获取页面配置信息,而无需通过Props逐层传递,并且一般的页面配置也不会分散在整个项目中,便于统一管理。
代码块32.2浏览器兼容性
虽然Nuxt理论上可以支持IE9,但是IE9在很多方面都需要添加Polyfill,比如对历史API的支持等。为了保持代码简单,我们放弃了对IE9-的支持,但还是在框架中保留了一套支持jQuery的机制,让高低版本可以共享HTML,而不需要单独为低版本编写模板,从而将兼容低版本浏览器的成本降到最低。
大意是Nuxt提供了一个‘render . route’的钩子函数。这个钩子函数的执行时间是在HTML生成之后,返回给用户之前。在这个钩子函数中,我们可以根据用户请求的UA信息来判断用户版本。如果是低版本浏览器用户,删除HTML中的高版本JS,注入低版本打包导入文件。
代码块42.3性能优化
数据过滤
Nuxt有一个很重要的机制,就是会把asyncData函数返回的所有数据挂在` window上。_ _ NUXT _ _ `并通过HTML返回给客户端,从而防止客户端再次请求这些数据。所以asyncData函数返回的数据量对性能的影响变得更加重要,不仅影响接口数据的传输时间,还影响HTML的体积。因此,我们需要压缩这些数据。
在异步数据中进行数据过滤
GraphQL
数据过滤平台
asyncData中的数据过滤只是减少了HTML的体积,并不能减少冗余数据的传输。
GraqhQL虽然解决了数据传输冗余的问题,但是代码不利于维护,因为它需要编写大量的查询参数,查询参数过长时需要POST。
代码块5最后,我们搭建了一个数据过滤平台,以可视化的方式配置界面数据源和数据的字段过滤和映射,最终生成一个界面。接口从配置好的数据源获取数据,经过字段映射和字段过滤后,只返回我们需要的字段。这样在不维护GraphQL的查询参数的情况下过滤冗余数据,但是GraphQL的查询字符串被可视化为配置。
平台示例布局
Nuxt提供了布局配置项,看起来很方便。然而,通过分析。Nuxt/app.js导入Nuxt生成的文件,我们发现所有的布局都会被打包,不管用不用。例如,如果页面A使用Layouta,页面B使用Layoutb,页面C使用LayoutC,那么页面A、B和C的JS将具有LayoutA、LayoutB和LayoutC。
代码块6所以如果布局逻辑复杂,代码量大,那么所有页面的JS量都会变得大很多。基于以上原因,我们放弃了使用Nuxt的布局,而是封装了一个I71Layout组件,为所有页面提供通用功能,以减少冗余代码。
2.4缓存
因为Vue SSR是基于虚拟DOM,而Java是基于字符串,所以性能会比以前慢,所以我们从页面和组件两个粒度做缓存策略。
我们使用Nginx反向代理来控制页面级缓存。默认情况下,每个页面缓存5分钟。当Nuxt返回一个不是200的值时,Nginx返回一个过期的缓存。
组件缓存我们使用官方的@nuxtjs/component-cache模块,它提供了一个serverCacheKey配置项,Nuxt会使用这个配置项的值作为缓存键。因此,我们为每个需要缓存的组件定义了一个cache-key Props。传递后会根据传递的值进行缓存,如果没有传递,则没有缓存。这样,在为所有未缓存的页面调用组件时,可以传递一个cache-key来缓存组件,从而加快页面的SSR。
2.5吹扫
对于缓存的页面,我们需要相应的清除接口来清除页面缓存。页面清理分为两个部分,一个是我们Nginx反向代理的缓存清理,一个是CDN的缓存清理。它们的清除原理是一样的,这里只说Nuxt服务的Nginx反向代理的缓存清除。
我们希望提供一个清除接口,通过传递页面名称参数来清除指定的页面。我们的Nuxt框架本身是基于Koa的,所以我们只需要在SSR之前插入koa-router来提供我们的Purge接口。
代码块7那么,我们如何知道每个页面名称要清除哪些URL呢?这里,我们需要在前面提到的页面配置文件中对其进行配置,以将pageName与Purge URL相关联:
代码块8接下来,我们只需要所有清除服务上的这些URL,这些服务部署在公司的应用程序平台上。有四个集群和数百个Docker容器。我们在所有的Purge主机上都需要Nginx缓存,具体操作如下:
首先我们需要在Nginx中配置它来支持Purge:
代码块9这样就可以通过调用http://{主机域名}:{主机端口}/purge/{uri}来清除主机上uri对应的缓存。
接下来,我们只需要逐个调用所有主机上的Purge接口,就可以清除所有主机上的页面缓存。
2.6电流限制
对于未缓存的页面,为了防范恶意刷行为,应该限制current。我们从WAF做了三个限制,单IP限流,IP黑名单。
2.6.1晶圆
首先,我们连接到公司的防火墙平台,通过智能识别过滤掉一些恶意请求。其次,对于一些动态路由的页面,我们会定期匹配请求的URL,所有不符合规则的请求都会被拒绝访问并返回403。
2.6.2单IP电流限制
为了防止单IP脚本刷机,我们使用limit_req模块来限制Nginx反向代理中的单IP电流。对于普通用户和爬虫,我们设置不同的访问频率,超过频率的请求拒绝访问并返回503。
知识产权黑名单
另外,我们会通过日志分析发现一些明显的IP,希望直接封禁这样的IP。
如果直接在Nginx配置中添加Deny语句,会发现Deny并不生效,因为请求是经过网关的,当到达我们的Nginx服务时,远程地址就变成了网关的IP,而我们的是真实用户的IP,所以我们需要想办法让Nginx知道用户的真实IP是什么。
通常,用户的真实IP存储在x-forwarded-for字段中。为了获取用户的真实IP,我们需要在Nginx中做如下配置:
代码块10但是上面的配置还不够,因为x-forwarded-for字段是一个字符串,每次经过一个节点,这个节点都会给它附加一个IP,所以当它到达我们Nginx的时候,这个字段的值就是x-forwarded-for:{用户的真实IP}、{网关的IP },Nginx读取IP的时候,{默认会从后向前读取IP。如果这个IP是可信IP,那么它将继续读取,直到不可信IP将被视为用户的真实IP。所以,如果没有额外的配置,Nginx读取的IP还是会是网关的IP。因此,我们需要将所有网关IP添加到可信IP列表中,然后Nginx才能继续读取用户的真实IP。我们可以将整个intranet网段设置为信任IP:
代码块11现在Nginx可以读取用户的真实IP。这时,我们只需要创建一个IP黑名单:
代码块12代码块132.7灾难恢复
灾备模型对于未缓存的页面,除了限流,我们还需要一个容灾计划,否则一旦服务错误返回非200,用户就会看到错误页面。
我们部署了独立的灾难恢复服务,使用节点脚本每三分钟从在线服务中提取所有重要页面。如果页面返回200,则存储为HTML文件,否则丢弃该页面,然后使用Nginx作为反向代理服务容灾页面。
首先,CDN从在线服务中提取页面。如果它返回200以外的值,它将从灾难恢复服务中提取相应的页面并返回给用户,以确保用户永远不会看到错误的页面。
2.8服务器日志
服务器的日志主要用来记录Nuxt渲染的页面的记录和错误信息,对于排查问题和统计流量非常重要。我们的服务器日志分为两部分:页面渲染日志和界面请求日志。
页面渲染日志是指每次页面请求时,都会写一个日志来记录页面的URL、Referer、用户cookie、用户IP等信息。如果页面渲染没有错误,则写入logs/page/info.log,如果页面渲染有错误,则写入logs/page/error.log。
接口日志是每个页面渲染中发出的请求日志,封装在底层发送请求的HTTP函数中,记录页面URL、接口URL、接口参数等调用接口的信息。如果请求成功,将日志写入logs/api/info.log,如果请求失败,将日志写入logs/api/error.log。
代码块14为了将页面渲染日志和本次渲染的接口日志关联起来,我们会在渲染之前生成一个唯一的RequestId,然后将这个RequestId带到本次渲染的所有日志中,这样我们就可以通过一个RequestId查询页面渲染日志和本次页面发送的所有请求日志。
代码块152.9日志收集
我们使用Filebeat+Elasticsearch+Kibana进行日志管理。首先我们通过Filebeat收集实时日志,然后上报给指定的kafka集群。然后,我们对日志进行分析和索引,最终生成可视化的日志查询页面,这样我们就可以查看一段时间内符合查询条件的日志。
2.10流量监控
基于服务器日志,我们可以统计已经通过CDN的缓存、WAF的拦截、Nginx反向代理的缓存的流量,最终计算出到达我们Nuxt服务的实际流量。我们可以根据日志的时间域过滤出指定时间段内type= 'render '的日志,也就是该时间段内Nuxt服务的总流量。如果您想查看每个页面的流量,您可以进一步过滤日志中的pageUrl字段。
流量监控页面示例03摘要
Nuxt从根本上解决了使用Velocity开发CMS平台遇到的所有问题,但也带来了一些其他问题,比如域名冲突、服务器变量共享、渲染性能等等。但总体来说,开发体验有了质的提升,开发效率提升了50%以上。组件的复用率更高,组件的封装性更好,代码的可读性和可维护性有了很大的提高。在CDN缓存、Nginx反向代理缓存、组件缓存的强力支持下,页面的渲染性能并没有下降。去掉前后码不一致、大量使用Slot等一些复杂的逻辑后,首屏的渲染性能提升了不少。服务器的响应时间平均维持在0.5s左右,错误率维持在0.2%左右。在灾难恢复服务的情况下,可访问性几乎是100%。
最后,期待Nuxt3的到来,以及性能和开发体验的进一步提升。