開發效率提升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":{}}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章