开发效率提升50%以上,爱奇艺官网主站的Nuxt实践

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":22}},{"type":"strong","attrs":{}}],"text":"01 背景","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"让每一个用户获取到","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"稳定、及时","attrs":{}},{"type":"text","text":"的页面体验,是前端工程师们一直以来努力的方向。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"作为一个拥有丰富内容资源的视频网站,爱奇艺官网主站需要频繁进行节目上线或者下线、各种活动配置等操作调整,对于","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"页面SSR服务的可用性及稳定性","attrs":{}},{"type":"text","text":",都有着极高的要求。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"2019年之前","attrs":{}},{"type":"text","text":",爱奇艺官网主站页面的SSR采用的是在","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"CMS平台中书写Velocity模板","attrs":{}},{"type":"text","text":",由Java编译,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"优点是渲染速度快","attrs":{}},{"type":"text","text":",","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"但缺点也非常明显","attrs":{}},{"type":"text","text":":","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(1)","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"在CMS平台中开发体验不好:","attrs":{}},{"type":"text","text":"没有传统IDE方便,不能配置快捷键、不能安装插件等等,导致开发效率低下。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(2)","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"前后端代码不同构:","attrs":{}},{"type":"text","text":"由于后端使用Velocity模板,而前端需要使用Vue,导致前后端代码不同构。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(3)","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"破坏Vue组件封装性:","attrs":{}},{"type":"text","text":"由于Java无法编译Vue组件,所有的Vue组件都需要用Slot的方式在CMS平台中书写以达到SEO和SSR的目的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"基于以上所有原因,我们决定使用","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"Node","attrs":{}},{"type":"text","text":"来进行SSR。因为我们的","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"前端框架是Vue","attrs":{}},{"type":"text","text":",因此我们选择了","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"配套的Nuxt框架进行SSR","attrs":{}},{"type":"text","text":"。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用Nuxt进行SSR,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"难点并不在于如何使用Nuxt,而在于如何维护这个服务,保证其性能、稳定性等","attrs":{}},{"type":"text","text":",因此,本文将","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"不会介绍Nuxt的使用","attrs":{}},{"type":"text","text":",其语法可以参考官网,这里将主要从","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"性能、缓存、限流、灾备、日志","attrs":{}},{"type":"text","text":"等几个方面来介绍我们是如何","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"保证Nuxt服务的可用性及稳定性的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":22}},{"type":"strong","attrs":{}}],"text":"02 Nuxt 稳定性提升之路","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":16}},{"type":"strong","attrs":{}}],"text":"2.1 页面配置","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先介绍一个很重要的","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"配置文件","attrs":{}},{"type":"text","text":"。在我们的","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"项目根目录","attrs":{}},{"type":"text","text":"下,创建了一个页面配置文件,用来存放每个页面的","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"通用配置","attrs":{}},{"type":"text","text":",例如页面的","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"缓存配置、Purge信息、主题色配置、广告信息配置","attrs":{}},{"type":"text","text":"等等,该文件导出一个Object, 键值为页面的Router Name,Value值为页面的配置信息:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"// configs/pageinfo.js\nexport default {\n 'dianshiju-id': {...},\n 'zongyi': {\n theme: 'dark', // 页面主题色配置\n },\n 'home2020': {...},\n 'rank-hot': {...}\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然后我们在Nuxt插件中根据请求的","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"路由信息","attrs":{}},{"type":"text","text":",读取","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"对应的页面配","attrs":{}},{"type":"text","text":"置,并将其注入到所有的","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"组件实例中","attrs":{}},{"type":"text","text":",方便随时取用:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"// plugins/pageinfo.js\nimport config from 'configs/pageinfo.js'\nexport default ({ route }, inject) => {\n inject('pageInfo', config[route.name]) // 注入页面配置信息\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因此你可以在组件的","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"任何地方","attrs":{}},{"type":"text","text":"获取到页面配置信息而不需要通过Props一层层传递,页面通用配置也","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"不会散落在项目各个地方","attrs":{}},{"type":"text","text":",方便统一管理。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"
我是综艺页面
","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":16}},{"type":"strong","attrs":{}}],"text":"2.2 浏览器兼容性","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"虽然Nuxt理论上可以支持IE9,但IE9在很多方面都","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"需要添加Polyfill","attrs":{}},{"type":"text","text":",例如对History API的支持等,为了保持","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"代码的简洁性","attrs":{}},{"type":"text","text":",我们放弃了支持IE9-,但我们依然在框架中","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"保留了一套机制来支持jQuery","attrs":{}},{"type":"text","text":",使得高低版本可以","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"共用HTML","attrs":{}},{"type":"text","text":",而不需要单独为低版本写模板,从而最大程度的减少兼容低版本浏览器的成本。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大致思路为,Nuxt提供了一个","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"’render.route’","attrs":{}},{"type":"text","text":"的钩子函数,该钩子函数的执行时机在生成HTML后,返回给用户之前。在这个钩子函数中,我们可以根据用户请求的UA信息判断用户版本,如果是低版本浏览器用户则移除HTML中高版本JS并注入低版本打包后的入口文件即可。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"// nuxt.config.js\n 'render:route': (url, result, { req }) => {\n if (isLowBrowser(req)) { // 根据用户ua信息判断是否是低版本\n const $ = cheerio.load(result.html)\n $('body script[src*=\\'pcw/ssr\\']').remove() // 移除高版本js\n $('body').append(' { // 定义purge接口,支持传递pageName\n ctx.body = await purgePage(ctx) // purge nginx缓存和cdn缓存\n})\napp.use(router.routes()) // 插入我们需要的api\napp.use(ctx => { // nuxt 进行 ssr\n nuxt.render(ctx.req, ctx.res)\n})","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那我们如何知道每个pageName要Purge哪些URL呢?这里我们需要在之前提到的","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"页面配置文件","attrs":{}},{"type":"text","text":"中进行配置来","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"将pageName和Purge URL关联起来","attrs":{}},{"type":"text","text":":","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"js"},"content":[{"type":"text","text":"// configs/pageinfo.js\nzongyi: {\n purge: {\n purgeUrl: [\n 'https://zongyi.iqiyi.com/', \n 'https://www.iqiyi.com/zongyi'\n ],\n },\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下来我们只需要Purge所有服务上的这些URL,服务部署在公司的应用平台,一共有","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"4个集群","attrs":{}},{"type":"text","text":",上百个","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"Docker容器","attrs":{}},{"type":"text","text":",我们需要Purge所有宿主机上的Nginx缓存,具体操作如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先我们需要在Nginx中配置让其支持Purge:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"nginx"},"content":[{"type":"text","text":"location / {\n proxy_cache_purge PURGE from all;\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这样就可以通过调用","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"http://{宿主机的域名}:{宿主机的端口}/purge/{uri}","attrs":{}},{"type":"text","text":"来Purge该宿主机上uri对应的缓存了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下来我们只需要","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"逐个调用","attrs":{}},{"type":"text","text":"所有宿主机上的Purge接口就可以Purge所有的宿主机上的页面缓存了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":16}},{"type":"strong","attrs":{}}],"text":"2.6 限流","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"对于无缓存页面,为了谨防恶意刷量行为,要进行限流。我们从","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"WAF,  单IP限流,  IP黑名单","attrs":{}},{"type":"text","text":"进行了三方面的限制。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":14}},{"type":"strong","attrs":{}}],"text":"2.6.1 WAF(Web Application Firewall)","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先我们接入了公司的防火墙平台,通过智能识别以过滤掉一些恶意请求。其次,对于一些动态路由的页面,我们对请求的URL进行了正则匹配,不符合正则的请求全部拒绝访问并返回403。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":14}},{"type":"strong","attrs":{}}],"text":"2.6.2  单IP限流","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"为了防止","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"单IP脚本刷量","attrs":{}},{"type":"text","text":",我们在Nginx反向代理使用limit_req模块进行单IP限流。对于普通用户和爬虫,我们设置了不同的访问频次,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"超过频次的请求拒绝访问并返回503。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":14}},{"type":"strong","attrs":{}}],"text":"2.6.3 IP黑名单","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除此之外,我们通过日志分析会发现一些很明显的刷量IP,对于这样的IP,我们希望直接封禁。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果直接在Nginx配置中添加Deny语句,会发现","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"Deny并不会生效","attrs":{}},{"type":"text","text":",是因为请求经过了网关,到我们的Nginx服务时,Remote Address变成了网关的IP,而我们Deny的是真实用户的IP,所以我们需要想办法让Nginx知道用户的真实IP是什么。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通常用户的真实IP存储在","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"x-forwarded-for字段","attrs":{}},{"type":"text","text":"中,为了拿到用户的真实IP,我们需要在Nginx中做以下配置:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"nginx"},"content":[{"type":"text","text":"# nginx.conf\n\nserver {\n real_ip_header X-Forwarded-For; # 告诉Nginx,用户的真实IP存储在x-forwarded-for字段中\n real_ip_recursive on;\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但光有以上配置还不够,因为x-forwarded-for字段为一个字符串,每经过一个节点,这个节点就会","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"向里面追加一个IP","attrs":{}},{"type":"text","text":",所以到达我们的Nginx时,该字段的值为","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"x-forwarded-for: {用户的真实IP},{网关的IP}","attrs":{}},{"type":"text","text":",而Nginx读取IP时,会默认从后往前读取IP, 如果这个IP是受信任的IP,则会继续往前读取,直到不被信任的IP就会当做是用户的真实IP,因此,如果没有额外配置,Nginx读取到的IP依然是网关的IP,因此,我们还需要将","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"所有网关IP添加到信任IP的列表中","attrs":{}},{"type":"text","text":",Nginx才能继续往前读取到用户的真实IP。我们可以","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"将整个内网网段都设置成信任IP:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"nginx"},"content":[{"type":"text","text":"# nginx.conf\n\nserver {\n set_real_ip_from xxx.0.0.0/8; # 设置内网网段为信任IP\n real_ip_header X-Forwarded-For; # 告诉Nginx,用户的真实IP存储在x-forwarded-for字段中\n real_ip_recursive on;\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"现在Nginx可以读取到用户的真实IP了,这时候我们只需要创建一个IP黑名单即可:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"# nginx.conf\n\nserver {\n set_real_ip_from xxx.0.0.0/8; # 设置内网网段为信任IP\n real_ip_header X-Forwarded-For; # 告诉Nginx,用户的真实IP存储在x-forwarded-for字段中\n real_ip_recursive on;\n\n include ip-blacklist.conf # 导入IP黑名单\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"nginx"},"content":[{"type":"text","text":"# ip-blacklist.conf\n\ndeny xx.xx.xx.xx;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":16}},{"type":"strong","attrs":{}}],"text":"2.7 灾备","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/1d/1dbdc299ffb52b10ebbbf4a1dac69285.png","alt":null,"title":"","style":[{"key":"width","value":"25%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"对于无缓存的页面,除了限流以外,我们还需要有","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"灾备方案","attrs":{}},{"type":"text","text":",否则一旦服务出错返回非200,用户将看到错误页面。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们部署了一套","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"独立的灾备服务","attrs":{}},{"type":"text","text":",使用Node脚本","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"每隔三分钟","attrs":{}},{"type":"text","text":"从线上服务拉取所有重要页面,如果页面返回200,则将其存储为HTML文件,否则抛弃该页面,然后使用Nginx做","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"反向代理","attrs":{}},{"type":"text","text":"来Serve灾备页面。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"CDN先从","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"线上服务","attrs":{}},{"type":"text","text":"拉取页面,若返回非200,则从灾备服务拉取对应的页面返回给用户,以此保证","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"用户永远不会看见出错的页面。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":16}},{"type":"strong","attrs":{}}],"text":"2.8 服务端日志","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"服务端日志主要用来记录Nuxt渲染页面的记录、错误信息等,它们对于","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"排查问题、统计流量","attrs":{}},{"type":"text","text":"来说是非常重要的,我们的服务端日志分为","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"两大部分:页面渲染日志、接口请求日志。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"页面渲染日志即每一次来一个页面请求,则写一条日志,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"记录页面的URL、Referer、用户Cookie、用户IP等信息","attrs":{}},{"type":"text","text":",若页面渲染","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"未出错","attrs":{}},{"type":"text","text":"则","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"写入到logs/page/info.log","attrs":{}},{"type":"text","text":"中,若页面","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"渲染出错","attrs":{}},{"type":"text","text":",","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"则写一条日志到logs/page/error.log","attrs":{}},{"type":"text","text":"中。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接口日志是每一次页面渲染中发出的请求日志,封装在底层发送请求的","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"HTTP函数","attrs":{}},{"type":"text","text":"中,记录了调用该接口的页面URL、接口URL、接口参数等信息,若请求成功,则写一条日志到","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"logs/api/info.log","attrs":{}},{"type":"text","text":", 若请求失败,则写一条日志到","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"logs/api/error.log","attrs":{}},{"type":"text","text":"中。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"js"},"content":[{"type":"text","text":"// nuxt.config.js\nhooks: {\n 'render:setupMiddleware': app => { // 在nuxt初始化时插入一个中间件,每次请求都生成一个logParams对象\n app.use(async (req, res, next) => {\n req.logParams = { \n requestId: generateRandomString(), // 生成requestId随机串\n pageUrl: req.url\n }\n next()\n })\n },\n 'render:routeDone': (url, result, { req, res }) => { // 渲染完毕\n logger.page.info({ type: 'render', ...req.logParams}, req) // 写日志时带上requestId\n },\n 'render:errorMiddleware': app => app.use(async (error, req, res, next) => { // 渲染错误\n logger.page.error({ type: 'render', error, ...req.logParams }, req) // 错误日志带上requestId\n\n next(error)\n }),\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"为了让页面渲染日志、这一次渲染的接口日志关联起来,我们会在渲染前生成一个唯一的","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"RequestId","attrs":{}},{"type":"text","text":", 然后在","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"该次渲染的所有日志","attrs":{}},{"type":"text","text":"中都带上这个RequestId,就可以通过一个RequestId查询到页面渲染日志,以及这个页面发出去的所有请求日志了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"js"},"content":[{"type":"text","text":"//http.js\nclass Resource {\n async http (opts)\n let data\n try {\n data = await axios(opts)\n process.server && logger.api.info(opts, this.req.logParams) // api日志带上requestid\n } catch (error) {\n process.server && logger.api.error(opts, error, this.req.logParams) // api错误日志带上requestid\n }\n return data\n }\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":16}},{"type":"strong","attrs":{}}],"text":"2.9 日志采集","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们采用了","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"Filebeat + Elasticsearch + Kibana进行日志管理","attrs":{}},{"type":"text","text":",","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"首先","attrs":{}},{"type":"text","text":"通过Filebeat进行实时日志采集,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"然后","attrs":{}},{"type":"text","text":"上报至指定 kafka 集群,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"然后","attrs":{}},{"type":"text","text":"对日志进行分析并建立索引,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"最终","attrs":{}},{"type":"text","text":"生成一个可视化的日志查询页面,这样我们就可以查看一段时间内符合查询条件的日志了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/99/991194877fcbbae780f3e15f47fbd846.png","alt":"图片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":16}},{"type":"strong","attrs":{}}],"text":"2.10 流量监控","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"基于服务端日志,就可以据此统计流量经由了","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"CDN的缓存、WAF的拦截、Nginx反向代理的缓存","attrs":{}},{"type":"text","text":",最后计算出到达我们的Nuxt服务的实际流量到底有多少。我们可以根据日志的time字段筛选出指定时间段且type= 'render'的日志,就是该时间段内Nuxt服务承受的总流量了,如果想看各个页面的流量,还可以进一步对日志中的pageUrl字段进行筛选。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/25/25ce438a09c59b8751aff599c36dca0b.png","alt":"图片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":22}},{"type":"strong","attrs":{}}],"text":"03 总结","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Nuxt从根本上解决了之前在CMS平台使用Velocity开发遇到的","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"所有问题","attrs":{}},{"type":"text","text":",但同时也带来了一些","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"别的问题","attrs":{}},{"type":"text","text":",例如","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"域名冲突的问题、服务端变量共享的问题、渲染性能问题等","attrs":{}},{"type":"text","text":"。不过总体来说,瑕不掩瑜,开发体验得到了","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"质的提升","attrs":{}},{"type":"text","text":",","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"开发效率提升了50%以上","attrs":{}},{"type":"text","text":";","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"组件复用率","attrs":{}},{"type":"text","text":"更高、","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"组件封装性","attrs":{}},{"type":"text","text":"更好,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"代码可读性","attrs":{}},{"type":"text","text":"、","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"可维护性","attrs":{}},{"type":"text","text":"都得到了","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"飞跃性","attrs":{}},{"type":"text","text":"的提升;在CDN缓存、Nginx反向代理缓存、组件缓存的强力加持下,页面的渲染性能也并没有下降;由于移除了一些由于","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"前后端代码","attrs":{}},{"type":"text","text":"不一致、大量使用Slot等一些复杂逻辑后,首屏渲染性能反而提高了许多,服务器响应时间维持在","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"平均0.5s","attrs":{}},{"type":"text","text":"左右,错误率维持在","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"0.2%","attrs":{}},{"type":"text","text":"左右,而在有灾备服务兜底的情况下,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"可访问性也几乎达到100%","attrs":{}},{"type":"text","text":"。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最后,期待","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"Nuxt3","attrs":{}},{"type":"text","text":"的到来以及性能和开发体验上的进一步提升。","attrs":{}}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章