1. 一篇講透自研的前端錯誤監(jiān)控

        共 27853字,需瀏覽 56分鐘

         ·

        2021-07-30 00:27

        本文轉(zhuǎn)自 https://juejin.cn/post/6987681953424080926

        作者: 羽飛

        關(guān)注并將「趣談前端」設(shè)為星標(biāo)

        每早08:30按時推送技術(shù)干貨/優(yōu)秀開源/技術(shù)思維

        一、背景

        痛點

        某?天產(chǎn)品:xxx?告主反饋我們的??注冊不了!??天運營:這個活動在xxx媒體上掛掉了!

        在我司線上運行的是近億級別的廣告頁面,這樣線上如果裸奔,出現(xiàn)了什么問題不知道,后置在業(yè)務(wù)端發(fā)現(xiàn),被業(yè)務(wù)方詢問,這種場景很尷尬。

        選擇

        公司存在四個事業(yè)部,而每個事業(yè)部不下于3個項目,這里至少12個項目,這里作為伏筆,業(yè)務(wù)線多。

        我們是選擇自己做呢,還是選第三方的呢。我們比較一幾款常見第三方。

        • Fundebug:付費版 159元/月起,數(shù)據(jù)存在第三方,而數(shù)據(jù)自我保存需要 30 萬/年。還是很貴的。
        • FrontJS,F(xiàn)rontJS 高級版 899/月,專業(yè)版是 2999/月。
        • Sentry,80 美金/月。

        以Sentry為計費,對這12個項目計算一下。12個項目一年將近10萬。而大致估算過需要2人1.5月即90人日,能完成MVP版本,按每人1.5萬工資/月計算,總共花費4.5萬,而且是一勞永逸的。

        因此從成本角度我們會選擇自研,但除了成本外,還有其他原因。例如我們會基于這套系統(tǒng)做一些自定義功能,與公司權(quán)限用戶系統(tǒng)打通,再針對用戶進行Todo管理,對用戶進行錯誤排行等。

        還有基于業(yè)務(wù)數(shù)據(jù)的安全,我們希望自我搭建一個系統(tǒng)。

        所以從成本、安全、擴展性角度,我們選擇了自己研發(fā)。

        二、產(chǎn)品設(shè)計

        我們要什么樣的一個產(chǎn)品呢,根據(jù)第一性原理,解決關(guān)鍵問題“怎么定位問題”。通過5W1H法我們來分析,我們想要知道些什么信息呢?

        錯誤信息

        其實錯誤監(jiān)控說簡單就一句話可以描述,搜集頁面錯誤,進行上報,然后對癥分析。

        按照5W1H法則進行分析這句話,可以發(fā)現(xiàn)有幾項需要我們關(guān)注。

        1. What,發(fā)?了什么錯誤:邏輯錯誤、數(shù)據(jù)錯誤、?絡(luò)錯誤、語法錯誤等。
        2. When,出現(xiàn)的時間段,如時間戳。
        3. Who,影響了多少用戶,包括報錯事件數(shù)、IP、設(shè)備信息。
        4. Where,出現(xiàn)的頁面是哪些,包括頁面、廣告位(我司)、媒體(我司)。
        5. Why,錯誤的原因是為什么,包括錯誤堆棧、?列、SourceMap。
        6. How,怎么定位解決問題,我們還需要收集系統(tǒng)等信息。

        架構(gòu)層次

        首先我們需要梳理下,我們需要一些哪些功能。

        那我們怎么得到上面的信息進行最終錯誤的定位呢。

        首先我們肯定需要對錯誤進行搜集,然后用戶設(shè)備頁面端的錯誤我們怎么才能感知到呢,這就需要進行上報。那么第一層就展現(xiàn)出來了,我們需要一個搜集上報端。

        那怎么才能進行上報呢,和后端協(xié)作那么久,肯定知道的吧?? ,你需要一個接口。那就需要一個服務(wù)器來進行對于上報的錯誤進行采集,對于錯誤進行篩選聚合。那么第二層也知道了啊,我們需要一個采集聚合端。

        我們搜集到了我們足夠的物料信息了,那接下來要怎么用起來呢,我們需要把它們按照我們的規(guī)則進行整理。如果每次又是通過寫類SQL進行整理查詢效率會很低,因此我們需要一個可視化的平臺進行展示。因此有了第三層,可視化分析端。

        感覺好像做完啦,想必大家都這么想,一個錯誤監(jiān)控平臺做完了,?? 。如果是這樣你會發(fā)現(xiàn)一個現(xiàn)象,每次上線和上線后一段時間,開發(fā)同學(xué)都一直盯著屏幕看,這是在干嘛,人形眼動觀察者模式嗎。因此我們需要通過代碼去解決,自然而然,第四層,監(jiān)控告警端應(yīng)運而生。

        所以請大聲說出來我們需要什么?? ,搜集上報端,采集聚合端,可視分析端,監(jiān)控告警端。

        三、系統(tǒng)設(shè)計

        如函數(shù)一樣,定義好每個環(huán)節(jié)的輸入和輸出,且核心需要處理的功能。

        下面我們看看上述所說的四個端怎么去實現(xiàn)呢。

        搜集上報端(SDK)

        這個環(huán)節(jié)主要輸入是所有錯誤,輸出是捕獲上報錯誤。核心是處理不同類型錯誤的搜集工作。其他是一些非核心但必要的工作。

        錯誤類型


        先看看我們需要處理哪些錯誤類型。

        常見JS執(zhí)行錯誤

        1. SyntaxError

        解析時發(fā)生語法錯誤

        // 控制臺運行
        const xx, 

        window.onerror捕獲不到SyntxError,一般SyntaxError在構(gòu)建階段,甚至本地開發(fā)階段就會被發(fā)現(xiàn)。

        1. TypeError

        值不是所期待的類型

        // 控制臺運行
        const person = void 0
        person.name 
        1. ReferenceError

        引用未聲明的變量

        // 控制臺運行
        nodefined 
        1. RangeError

        當(dāng)一個值不在其所允許的范圍或者集合中

        (function fn ( ) { fn() })() 

        網(wǎng)絡(luò)錯誤

        1. ResourceError

        資源加載錯誤

        new Image().src = '/remote/image/notdeinfed.png' 
        1. HttpError

        Http請求錯誤

        // 控制臺運行
        fetch('/remote/notdefined', {}) 

        搜集錯誤

        所有起因來源于錯誤,那我們?nèi)绾芜M行錯誤捕獲。

        try/catch

        能捕獲常規(guī)運行時錯誤,語法錯誤和異步錯誤不行

        // 常規(guī)運行時錯誤,可以捕獲 ?
        try {
          console.log(notdefined);
        } catch(e) {
          console.log('捕獲到異常:', e);
        }

        // 語法錯誤,不能捕獲 ?
        try {
          const notdefined,
        } catch(e) {
          console.log('捕獲到異常:', e);
        }

        // 異步錯誤,不能捕獲 ?
        try {
          setTimeout(() => {
            console.log(notdefined);
          }, 0)
        } catch(e) {
          console.log('捕獲到異常:',e);

        try/catch有它細致處理的優(yōu)勢,但缺點也比較明顯。

        window.onerror

        pure js錯誤收集,window.onerror,當(dāng) JS 運行時錯誤發(fā)生時,window 會觸發(fā)一個 ErrorEvent 接口的 error 事件。

        /**
        * @param {String} message    錯誤信息
        * @param {String} source    出錯文件
        * @param {Number} lineno    行號
        * @param {Number} colno    列號
        * @param {Object} error  Error對象
        */

        window.onerror = function(message, source, lineno, colno, error) {
           console.log('捕獲到異常:', {message, source, lineno, colno, error});

        先驗證下幾個錯誤是否可以捕獲。

        // 常規(guī)運行時錯誤,可以捕獲 ?

        window.onerror = function(message, source, lineno, colno, error) {
          console.log('捕獲到異常:',{message, source, lineno, colno, error});
        }
        console.log(notdefined);

        // 語法錯誤,不能捕獲 ?
        window.onerror = function(message, source, lineno, colno, error) {
          console.log('捕獲到異常:',{message, source, lineno, colno, error});
        }
        const notdefined,
              
        // 異步錯誤,可以捕獲 ?
        window.onerror = function(message, source, lineno, colno, error) {
          console.log('捕獲到異常:',{message, source, lineno, colno, error});
        }
        setTimeout(() => {
          console.log(notdefined);
        }, 0)

        // 資源錯誤,不能捕獲 ?
        <script>
          window.onerror = function(message, source, lineno, colno, error) {
          console.log('捕獲到異常:',{message, source, lineno, colno, error});
          return true;
        }
        </script>
        <img src="https://yun.tuia.cn/image/kkk.png"

        window.onerror 不能捕獲資源錯誤怎么辦?

        window.addEventListener

        當(dāng)一項資源(如圖片或腳本)加載失敗,加載資源的元素會觸發(fā)一個 Event 接口的 error 事件,這些 error 事件不會向上冒泡到 window,但能被捕獲。而window.onerror不能監(jiān)測捕獲。

        // 圖片、script、css加載錯誤,都能被捕獲 ?
        <script> window.addEventListener('error', (error) => {
           console.log('捕獲到異常:', error);
         }, true) </script>
        <img src="https://yun.tuia.cn/image/kkk.png">
        <script src="https://yun.tuia.cn/foundnull.js"></script>
        <link href="https://yun.tuia.cn/foundnull.css" rel="stylesheet"/>
          
        // new Image錯誤,不能捕獲 ?
        <script> window.addEventListener('error', (error) => {
            console.log('捕獲到異常:', error);
          }, true) </script>
        <script> new Image().src = 'https://yun.tuia.cn/image/lll.png' </script>

        // fetch錯誤,不能捕獲 ?
        <script> window.addEventListener('error', (error) => {
            console.log('捕獲到異常:', error);
          }, true) </script>
        <script> fetch('https://tuia.cn/test') </script> 

        new Image運用的比較少,可以單獨自己處理自己的錯誤。

        但通用的fetch怎么辦呢,fetch返回Promise,但Promise的錯誤不能被捕獲,怎么辦呢?

        Promise錯誤

        1. 普通Promise錯誤

        try/catch不能捕獲Promise中的錯誤

        // try/catch 不能處理 JSON.parse 的錯誤,因為它在 Promise 中
        try {
          new Promise((resolve,reject) => { 
            JSON.parse('')
            resolve();
          })
        } catch(err) {
          console.error('in try catch', err)
        }

        // 需要使用catch方法
        new Promise((resolve,reject) => { 
          JSON.parse('')
          resolve();
        }).catch(err => {
          console.log('in catch fn', err)
        }) 
        1. async錯誤

        try/catch不能捕獲async包裹的錯誤

        const getJSON = async () => {
          throw new Error('inner error')
        }

        // 通過try/catch處理
        const makeRequest = async () => {
            try {
                // 捕獲不到
                JSON.parse(getJSON());
            } catch (err) {
                console.log('outer', err);
            }
        };

        try {
            // try/catch不到
            makeRequest()
        } catch(err) {
            console.error('in try catch', err)
        }

        try {
            // 需要await,才能捕獲到
            await makeRequest()
        } catch(err) {
            console.error('in try catch', err)

        1. import chunk錯誤

        import其實返回的也是一個promise,因此使用如下兩種方式捕獲錯誤

        // Promise catch方法
        import(/* webpackChunkName: "incentive" */'./index').then(module => {
            module.default()
        }).catch((err) => {
            console.error('in catch fn', err)
        })

        // await 方法,try catch
        try {
            const module = await import(/* webpackChunkName: "incentive" */'./index');
            module.default()
        } catch(err) {
            console.error('in try catch', err)

        小結(jié):全局捕獲Promise中的錯誤

        以上三種其實歸結(jié)為Promise類型錯誤,可以通過unhandledrejection捕獲

        // 全局統(tǒng)一處理Promise
        window.addEventListener("unhandledrejection"function(e){
          console.log('捕獲到異常:', e);
        });
        fetch('https://tuia.cn/test'

        為了防止有漏掉的 Promise 異常,可通過unhandledrejection用來全局監(jiān)聽Uncaught Promise Error。

        Vue錯誤

        由于Vue會捕獲所有Vue單文件組件或者Vue.extend繼承的代碼,所以在Vue里面出現(xiàn)的錯誤,并不會直接被window.onerror捕獲,而是會拋給Vue.config.errorHandler。

        /**
         * 全局捕獲Vue錯誤,直接扔出給onerror處理
         */
        Vue.config.errorHandler = function (err) {
          setTimeout(() => {
            throw err
          })

        React錯誤

        react 通過componentDidCatch,聲明一個錯誤邊界的組件

        class ErrorBoundary extends React.Component {
          constructor(props) {
            super(props);
            this.state = { hasError: false };
          }

          static getDerivedStateFromError(error) {
            // 更新 state 使下一次渲染能夠顯示降級后的 UI
            return { hasError: true };
          }

          componentDidCatch(error, errorInfo) {
            // 你同樣可以將錯誤日志上報給服務(wù)器
            logErrorToMyService(error, errorInfo);
          }

          render() {
            if (this.state.hasError) {
              // 你可以自定義降級后的 UI 并渲染
              return <h1>Something went wrong.</h1>;
            }

            return this.props.children; 
          }
        }

        class App extends React.Component {
           
          render() {
            return (
            <ErrorBoundary>
              <MyWidget />
            </ErrorBoundary>  
            )
          }

        但error boundaries并不會捕捉以下錯誤:React事件處理,異步代碼,error boundaries自己拋出的錯誤。

        跨域問題

        一般情況,如果出現(xiàn) Script error 這樣的錯誤,基本上可以確定是出現(xiàn)了跨域問題。

        如果當(dāng)前投放頁面和云端JS所在不同域名,如果云端JS出現(xiàn)錯誤,window.onerror會出現(xiàn)Script Error。通過以下兩種方法能給予解決。

        • 后端配置Access-Control-Allow-Origin、前端script加crossorigin。
        <script src="http://yun.tuia.cn/test.js" crossorigin></script>

        const script = document.createElement('script');
        script.crossOrigin = 'anonymous';
        script.src = 'http://yun.tuia.cn/test.js';
        document.body.appendChild(script); 
        • 如果不能修改服務(wù)端的請求頭,可以考慮通過使用 try/catch 繞過,將錯誤拋出。
        <!doctype html>
        <html>
        <head>
          <title>Test page in http://test.com</title>
        </head>
        <body>
          <script src="https://yun.dui88.com/tuia/cdn/remote/testerror.js"></script>
          <script> window.onerror = function (message, url, line, column, error) {
            console.log(message, url, line, column, error);
          }

          try {
            foo(); // 調(diào)用testerror.js中定義的foo方法
          } catch (e) {
            throw e;
          } </script>
        </body>
        </html> 

        會發(fā)現(xiàn)如果不加try catch,console.log就會打印script error。加上try catch就能捕獲到。

        我們捋一下場景,一般調(diào)用遠端js,有下列三種常見情況。

        • 調(diào)用遠端JS的方法出錯
        • 遠端JS內(nèi)部的事件出問題
        • 要么在setTimeout等回調(diào)內(nèi)出錯


        調(diào)用方法場景 

        可以通過封裝一個函數(shù),能裝飾原方法,使得其能被try/catch。

         <!doctype html>
        <html>
        <head>
          <title>Test page in http://test.com</title>
        </head>
        <body>
          <script src="https://yun.dui88.com/tuia/cdn/remote/testerror.js"></script>
          <script> window.onerror = function (message, url, line, column, error) {
            console.log(message, url, line, column, error);
          }

          function wrapErrors(fn) {
            // don't wrap function more than once
            if (!fn.__wrapped__) {
              fn.__wrapped__ = function () {
                try {
                  return fn.apply(this, arguments);
                } catch (e) {
                  throw e; // re-throw the error
                }
              };
            }

            return fn.__wrapped__;
          }

          wrapErrors(foo)() </script>
        </body>
        </html> 

        大家可以嘗試去掉wrapErrors感受下。

        事件場景 

        可以劫持原生方法。

         <!doctype html>
        <html>
        <head>
          <title>Test page in http://test.com</title>
        </head>
        <body>
          <script> const originAddEventListener = EventTarget.prototype.addEventListener;
            EventTarget.prototype.addEventListener = function (type, listener, options) {
              const wrappedListener = function (...args) {
                try {
                  return listener.apply(this, args);
                }
                catch (err) {
                  throw err;
                }
              }
              return originAddEventListener.call(this, type, wrappedListener, options);
            } </script>
          <div style="height: 9999px;">http://test.com</div>
          <script src="https://yun.dui88.com/tuia/cdn/remote/error_scroll.js"></script>
          <script> window.onerror = function (message, url, line, column, error) {
            console.log(message, url, line, column, error);
          } </script>
        </body>
        </html> 

        大家可以嘗試去掉封裝EventTarget.prototype.addEventListener的那段代碼,感受下。

        上報接口

        為什么不能直接用GET/POST/HEAD請求接口進行上報?

        這個比較容易想到原因。一般而言,打點域名都不是當(dāng)前域名,所以所有的接口請求都會構(gòu)成跨域。

        為什么不能用請求其他的文件資源(js/css/ttf)的方式進行上報?

        創(chuàng)建資源節(jié)點后只有將對象注入到瀏覽器DOM樹后,瀏覽器才會實際發(fā)送資源請求。而且載入js/css資源還會阻塞頁面渲染,影響用戶體驗。

        構(gòu)造圖片打點不僅不用插入DOM,只要在js中new出Image對象就能發(fā)起請求,而且還沒有阻塞問題,在沒有js的瀏覽器環(huán)境中也能通過img標(biāo)簽正常打點。

        使用new Image進行接口上報。最后一個問題,同樣都是圖片,上報時選用了1x1的透明GIF,而不是其他的PNG/JEPG/BMP文件。

        首先,1x1像素是最小的合法圖片。而且,因為是通過圖片打點,所以圖片最好是透明的,這樣一來不會影響頁面本身展示效果,二者表示圖片透明只要使用一個二進制位標(biāo)記圖片是透明色即可,不用存儲色彩空間數(shù)據(jù),可以節(jié)約體積。因為需要透明色,所以可以直接排除JEPG。

        同樣的響應(yīng),GIF可以比BMP節(jié)約41%的流量,比PNG節(jié)約35%的流量。GIF才是最佳選擇。

        • 可以進行跨域
        • 不會攜帶cookie
        • 不需要等待服務(wù)器返回數(shù)據(jù)

        使用1\*1的gif[1]

        非阻塞加載

        盡量避免SDK的js資源加載影響。

        通過先把window.onerror的錯誤記錄進行緩存,然后異步進行SDK的加載,再在SDK里面處理錯誤上報。

        <!DOCTYPE html>
        <html lang="en">
        <head>
            <script> (function(w) {
                    w._error_storage_ = [];
                    function errorhandler(){
                        // 用于記錄當(dāng)前的錯誤 
                        w._error_storage_&&w._error_storage_.push([].slice.call(arguments));
                    } 
                    w.addEventListener && w.addEventListener("error", errorhandler, true);
                    var times = 3,
                    appendScript = function appendScript() {
                        var sc = document.createElement("script");
                        sc.async = !0,
                        sc.src = './build/skyeye.js',  // 取決于你存放的位置
                        sc.crossOrigin = "anonymous",
                        sc.onerror = function() {
                            times--,
                            times > 0 && setTimeout(appendScript, 1500)
                        },
                        document.head && document.head.appendChild(sc);
                    };
                    setTimeout(appendScript, 1500);
                })(window); </script>
        </head>
        <body>
            <h1>這是一個測試頁面(new)</h1>
        </body>
        </html> 

        采集聚合端(日志服務(wù)器)

        這個環(huán)節(jié),輸入是接口接收到的錯誤記錄,輸出是有效的數(shù)據(jù)入庫。核心功能需要對數(shù)據(jù)進行清洗,順帶解決了過多的服務(wù)壓力。另一個核心功能是對數(shù)據(jù)進行入庫。

        總體流程可以看為錯誤標(biāo)識 -> 錯誤過濾 -> 錯誤接收 -> 錯誤存儲。

        錯誤標(biāo)識(SDK配合)

        聚合之前,我們需要有不同維度標(biāo)識錯誤的能力,可以理解為定位單個錯誤條目,單個錯誤事件的能力。

        單個錯誤條目 

        通過date和隨機值生成一條對應(yīng)的錯誤條目id。

        const errorKey = `${+new Date()}@${randomString(8)}`

        function randomString(len) {  
            len = len || 32;
            let chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';
            let maxPos = chars.length;
            let pwd = '';  
            for (let i = 0; i < len; i++) {    
                pwd += chars.charAt(Math.floor(Math.random() * maxPos));  
            }  
            return pwd;


        單個錯誤事件 

        首先需要有定位同個錯誤事件(不同用戶,發(fā)生相同錯誤類型、錯誤信息)的能力。

        通過message、colno與lineno進行相加計算阿斯克碼值,可以生成錯誤的errorKey。

        const eventKey = compressString(String(e.message), String(e.colno) + String(e.lineno))

        function compressString(str, key) {
            let chars = 'ABCDEFGHJKMNPQRSTWXYZ';
            if (!str || !key) {
                return 'null';
            }
            let n = 0,
                m = 0;
            for (let i = 0; i < str.length; i++) {
                n += str[i].charCodeAt();
            }
            for (let j = 0; j < key.length; j++) {
                m += key[j].charCodeAt();
            }
            let num = n + '' + key[key.length - 1].charCodeAt() + m + str[str.length - 1].charCodeAt();
            if(num) {
                num = num + chars[num[num.length - 1]];
            }
            return num;

        如下圖,一個錯誤事件(事件列表),下屬每條即為實際的錯誤條目。

        錯誤過濾(SDK配合)

        域名過濾 

        過濾本頁面script error,可能被webview插入其他js。 

        我們只關(guān)心自己的遠端JS問題,因此做了根據(jù)本公司域名進行過濾。

        // 偽代碼
        if(!e.filename || !e.filename.match(/^(http|https):\/\/yun./)) return true 

        重復(fù)上報 

        怎么避免重復(fù)的數(shù)據(jù)上報?根據(jù)errorKey來進行緩存,重復(fù)的錯誤避免上報的次數(shù)超過閾值。

        // 偽代碼

        const localStorage = window.localStorage;
        const TIMES = 6; // 緩存條數(shù)

        export function setItem(key, repeat) {
            if(!key) {
                key = 'unknow';
            }
          
            if (has(key)) {
                const value = getItem(key);
                
               // 核心代碼,超過條數(shù),跳出
                if (value >= repeat) {
                    return true;
                }
                storeStorage[key] = {
                    value: value + 1,
                    time: Date.now()
                }
            } else {
                storeStorage[key] = {
                    value: 1,
                    time: Date.now()
                }
            }
            return false;


        錯誤接收

        在處理接收接口的時候,注意流量的控制,這也是后端開發(fā)需要投入最多精力的地方,處理高并發(fā)的流量。

        錯誤記錄

        接收端使用Koa,簡單的實現(xiàn)了接收及打印到磁盤。

        // 偽代碼

        module.exports = async ctx => {
          const { query } = ctx.request;
          
          // 對于字段進行簡單check 
          check([ 'mobile''network''ip''system''ua', ......], query);

          ctx.type = 'application/json';
          ctx.body = { code: '1', msg: '數(shù)據(jù)上報成功' };

          // 進行日志記錄到磁盤的代碼,根據(jù)自己的日志庫選擇
        }; 

        削峰機制 

        比如每秒設(shè)置2000的閾值,然后根據(jù)請求量減少上限,定時重置上限。

        // 偽代碼

        // 1000ms
        const TICK = 1000;
        // 1秒上限為2000
        const MAX_LIMIT = 2000;
        // 每臺服務(wù)器請求上限值
        let maxLimit = MAX_LIMIT;

        /**
         * 啟動重置函數(shù)
         */
        const task = () => {
          setTimeout(() => {
            maxLimit = MAX_LIMIT;
            task();
          }, TICK);
        };
        task();

        const check = () => {
          if (maxLimit <= 0) {
            throw new Error('超過上報次數(shù)');
          }
          maxLimit--;
          // 執(zhí)行業(yè)務(wù)代碼。。。
        }; 

        采樣處理 

        超過閾值,還可以進行采樣收集。

        // 只采集 20%
        if(Math.random() < 0.2) {
          collect(data)      // 記錄錯誤信息

        錯誤存儲

        對于打印在了磁盤的日志,我們怎么樣才能對于其進行聚合呢,這里得考慮使用存儲方案。

        一般選擇了存儲方案后,設(shè)置好配置,存儲方案就可以通過磁盤定時周期性的獲取數(shù)據(jù)。因此我們需要選擇一款存儲方案。

        對于存儲方案,我們對比了日常常見方案,阿里云日志服務(wù) - Log Service(SLS)、ELK(Elastic、Logstash、Kibana)、Hadoop/Hive(將數(shù)據(jù)存儲在 Hadoop,利用 Hive 進行查詢) 類方案的對比。

        從以下方面進行了對比,最終選擇了Log Service,主要考慮為無需搭建,成本低,查詢功能滿足。

        功能項ELK 類系統(tǒng)Hadoop + Hive日志服務(wù)
        日志延時1~60 秒幾分鐘~數(shù)小時實時
        查詢延時小于 1 秒分鐘級小于 1 秒
        查詢能力
        擴展性提前預(yù)備機器提前預(yù)備機器秒級 10 倍擴容
        成本較高較低很低

        日志延時:日志產(chǎn)生后,多久可查詢。查詢延時:單位時間掃描數(shù)據(jù)量。查詢能力:關(guān)鍵詞查詢、條件組合查詢、模糊查詢、數(shù)值比較、上下文查詢。擴展性:快速應(yīng)對百倍流量上漲。成本:每 GB 費用。

        具體API使用,可查看日志服務(wù)[2]

        可視分析端(可視化平臺)

        這個環(huán)節(jié),輸入是接口接收到的錯誤記錄,輸出是有效的數(shù)據(jù)入庫。核心功能需要對數(shù)據(jù)進行清洗,順帶解決了過多的服務(wù)壓力。另一個核心功能是對數(shù)據(jù)進行入庫。

        主功能

        這部分主要是產(chǎn)品功能的合理設(shè)計,做到小而美,具體的怎么聚合,參考阿里云SLS就可以。

        1. 首頁圖表,可選1天、4小時、1小時等等,聚合錯誤數(shù),根據(jù)1天切分24份來聚合。
        2. 首頁列表,聚合選中時間內(nèi)的數(shù)據(jù),展示錯誤文件、錯誤key、事件數(shù)、錯誤類型、時間、錯誤信息。
        3. 錯誤詳情,事件列表、基本信息、設(shè)備信息、設(shè)備占比圖表(見上面事件列表的圖)。


        image.png

        排行榜

        剛開始做了待處理錯誤列表、我的錯誤列表、已解決列表,錯誤與人沒有綁定關(guān)系,過于依賴人為主動,需要每個人主動到平臺上處理,效果不佳。

        后面通過錯誤作者排行榜,通過釘釘日報來提醒對應(yīng)人員處理。緊急錯誤,通過實時告警來責(zé)任到人,后面告警會說。

        具體原理:

        • webpack打包通過git命令把作者和作者郵箱、時間打包在頭部。
        • 在可視化服務(wù)中,去請求對應(yīng)的報錯url匹配到對應(yīng)作者,返回給展示端。
        image.png

        SourceMap

        利用webpack的hidden-source-map構(gòu)建。與 source-map 相比少了末尾的注釋,但 output 目錄下的 index.js.map 沒有少。線上環(huán)境避免source-map泄露。

        webpackJsonp([1],[
          function(e,t,i){...},
          function(e,t,i){...},
          function(e,t,i){...},
          function(e,t,i){...},
          ...
        ])
        // 這里沒有生成source-map的鏈接地址 

        根據(jù)報錯文件的url,根據(jù)團隊內(nèi)部約定好的目錄和規(guī)則,定位之前打包上傳的sourceMap地址。

        const sourcemapUrl = ('xxxfolder/' + url + 'xxxHash' +'.map'


        獲取上報的line、column、source,利用第三方庫sourceMap進行定位。

        const sourceMap = require('source-map')

        // 根據(jù)行數(shù)獲取源文件行數(shù)
        const getPosition = async(map, rolno, colno) => {
          const consumer = await new sourceMap.SourceMapConsumer(map)

          const position = consumer.originalPositionFor({
            line: rolno,
            column: colno
          })

          position.content = consumer.sourceContentFor(position.source)

          return position

        感興趣SourceMap原理的,可以繼續(xù)深入,SourceMap 與前端異常監(jiān)控[3]。

        錯誤報警

        報警設(shè)置

        1. 每條業(yè)務(wù)線設(shè)置自己的閾值、錯誤時間跨度,報警輪詢間隔
        2. 通過釘釘hook報警到對應(yīng)的群
        3. 通過日報形式報出錯誤作者排行榜
        image.png

        四、擴展

        行為搜集

        通過搜集用戶的操作,可以明顯發(fā)現(xiàn)錯誤為什么產(chǎn)生。

        分類

        • UI行為:點擊、滾動、聚焦/失焦、長按
        • 瀏覽器行為:請求、前進/后退、跳轉(zhuǎn)、新開頁面、關(guān)閉
        • 控制臺行為:log、warn、error

        搜集方式

        1. 點擊行為

        使用addEventListener監(jiān)聽全局上的click事件,將事件和DOM元素名字收集。與錯誤信息一起上報。

        1. 發(fā)送請求

        監(jiān)聽XMLHttpRequest的onreadystatechange回調(diào)函數(shù)

        1. 頁面跳轉(zhuǎn)

        監(jiān)聽window.onpopstate,頁面進行跳轉(zhuǎn)時會觸發(fā)。

        1. 控制臺行為

        重寫console對象的info等方法。

        有興趣可以參考行為監(jiān)控[4]。

        遇到的問題

        由于涉及到一些隱私,下述會做脫敏處理。

        空日志問題

        上線灰度運行后,我們發(fā)現(xiàn)SLS日志存在一些空日志?? ,??,這是發(fā)生了啥?

        首先我們回憶下這個鏈路上有哪些環(huán)節(jié)可能存在問題。

        image.png

        排查鏈路,SLS采集環(huán)節(jié)之前有磁盤日志收集,服務(wù)端接收,SDK上報,那我們依次排查。

        往前一步,發(fā)現(xiàn)磁盤日志就已經(jīng)存在空日志,那剩下就得看一下接收端、SDK端。

        開始利用控制變量法,先在SDK端進行空判斷,防止空日志上報。結(jié)果:發(fā)現(xiàn)無效??。

        再繼續(xù)對Node接收端處理,對接收到的數(shù)據(jù)進行判空,如果為空不進行日志打印,結(jié)果:依然無效??。

        所以開始定位是不是日志打印本身出了什么問題?研究了下日志第三方日志庫的API,進行了各種嘗試,發(fā)現(xiàn)依舊沒用,我臉黑了??。

        什么情況,“遇事不決”看源碼。排查下日志庫源碼存在什么問題。對于源碼的主調(diào)用流程走了一遍,并沒有發(fā)現(xiàn)什么問題,一頭霧水??。

        整個代碼邏輯很正常,這讓我們開始懷疑難道是數(shù)據(jù)的問題,于是開始縮減上報的字段,最終定義為了一個字段。發(fā)現(xiàn)上線后沒有問題了??。

        難道是有些字段存儲的數(shù)據(jù)過長導(dǎo)致的?但從代碼邏輯、流程日志中并沒有反應(yīng)這個錯誤的可能性。

        因此我們利用二分法,二分地增加字段,最終定位到了某個字段。如果存在某個字段上報就會出現(xiàn)問題。這很出乎人的意料。

        我們再想了下鏈路,除了日志庫,其他代碼基本都是我們自己的邏輯,所以對日志庫進行了排查,懷疑其對某個字段做了什么處理。

        于是通過搜索,定位到了日志庫在仆從模式(可以了解下Node的主從模式)下會使用某個字段來表意,導(dǎo)致和我們上報的字段沖突,因此丟失了??。

        日志丟失問題

        解決了上個問題,開心了,一股成就感涌上心頭。但馬上就被當(dāng)頭一棒,我發(fā)現(xiàn)我高興的太早了??。

        團隊的某同學(xué)在本地測試的時候,由于玩的很開心,一直去刷新頁面去上報當(dāng)前頁面的錯誤。但他發(fā)現(xiàn)本地上報的條數(shù)和實際日志服務(wù)里的條數(shù)對不上,日志服務(wù)里的少了很多。

        由于之前自身剛畢業(yè)時候做過2年多后端開發(fā),對于IO操作丟失數(shù)據(jù)還是有點敏感。直覺上就感覺可能是多進程方向的問題。懷疑是多進程導(dǎo)致的文件死鎖問題。

        那我們?nèi)サ舳嗑€程,通過單線程,我們?nèi)ブ貜?fù)原先復(fù)現(xiàn)問題的步驟。發(fā)現(xiàn)沒有遺漏??。

        我們發(fā)現(xiàn)能進行配置Cluster(主從模式)的地方有兩處,日志庫和部署工具。

        觀察日志庫默認(rèn)使用的主從進程模式,而部署工具沒有主從模式的概念,勢必會導(dǎo)致寫入IO的死鎖問題,導(dǎo)致日志丟失。于是在想社區(qū)有沒有可以有解決此問題的第三方支持。

        然后通過谷歌搜索,很快就找到了對應(yīng)的第三方庫,它能提供主人進程和仆從進程之間的消息溝通。原理是主人進程負(fù)責(zé)所有消息寫入log,而仆從進程通過消息傳遞給主人進程。

        五、推薦閱讀及引用

        處理異常

        如何優(yōu)雅處理前端異常?[5]

        source-map

        SourceMap 與前端異常監(jiān)控[6]

        React錯誤

        React,優(yōu)雅的捕獲異常[7]

        Script Error

        Capture and report JavaScript errors with window.onerror | Product Blog ? Sentry[8] What the heck is "Script error"? | Product Blog ? Sentry[9]

        整體

        前端搞監(jiān)控|Allan - 如何實現(xiàn)一套多端錯誤監(jiān)控平臺[10] 一步一步搭建前端監(jiān)控系統(tǒng):JS錯誤監(jiān)控篇[11] 擼一個前端監(jiān)控系統(tǒng)[12]

        ?? 看完三件事

        如果你覺得這篇內(nèi)容對你挺有啟發(fā),我想邀請你幫我三個小忙:

        • 點個【在看】,或者分享轉(zhuǎn)發(fā),讓更多的人也能看到這篇內(nèi)容
        • 關(guān)注公眾號【趣談前端】,定期分享 工程化 可視化 / 低代碼 / 優(yōu)秀開源。




        從零設(shè)計可視化大屏搭建引擎

        Dooring可視化搭建平臺數(shù)據(jù)源設(shè)計剖析

        可視化搭建的一些思考和實踐

        基于Koa + React + TS從零開發(fā)全棧文檔編輯器(進階實戰(zhàn))


        點個在看你最好看

        瀏覽 59
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
          
          

            1. www.色色色com | 日本wwwwxxxx泡妞下课 | 国产精品美女www爽爽爽视频 | 淫淫视频 | 亚洲一级A片毛毛aA片18 日韩无码中文字幕电影 |