出處:https://mp.weixin.qq.com/s?__biz=MTEwNTM0ODI0MQ==&mid=2653453935&idx=1&sn=dd37f76919c29185a3f8939fd68ccbab
| 導 語 微服務開發利器,網路呼叫鏈遙測,效能遙測。開發、測試、生產多套環境的鏈路與效能全在掌控之中,告別打日誌定位效能問題的苦逼日子。首次最佳化,網路效能提升50%,後端介面請求量減少3/4。
前端系統架構前端使用 Egg + React + SSR 框架,僅使用者導航時首屏使用服務端渲染(SSR),之後使用客戶端渲染(CSR),可確保使用者在首屏與其它頁面均有極致的使用者體驗。Node層,也負責一些Web安全處理,比如:CSRF、CSP、快取控制等。
面臨的問題加入Node層,在開發、測試與生產階段,我們面臨瀏覽器端不存在的問題:
網路請求呼叫鏈遙測,發現與解決呼叫鏈過長、多餘介面呼叫的問題。效能遙測,發現效能問題點並最佳化。整合Jaeger鏈路追蹤為了解決上述問題,我們引入微服務常用的鏈路追蹤,選用的實現是Jaeger。Jaeger架構,請參考:https://www.jaegertracing.io/docs/1.21/architecture/
在NodeJS中,引入 jaeger-client-node 。我們的服務框架是Egg,新建一個jaeger中介軟體,專門處理鏈路追蹤。對Node層外發的網路請求,統一使用Axios,新建一個fetch-tracing攔截器。對每個外發的網路請求,新建一個span。
以下程式碼為NodeJS整合Jaeger的關鍵程式碼:
3.1. 建立Jaeger Tracer// src/app.tsimport { Application } from 'egg';import { initTracer } from 'jaeger-client';class AppBootHook {app: Application;constructor(app) {this.app = app;}async willReady() {this.app.tracer = this.createTracer();}createTracer() {const config = this.app.config.jaeger;const options = {tags: {'egg-jaeger-version': '1.0.0',},};return initTracer(config, options);}}module.exports = AppBootHook;
3.2. Egg鏈路追蹤中介軟體給每個Egg接入的請求,建立一個rootSpan。
// src/app/middleware/jaeger.tsimport { FORMAT_HTTP_HEADERS, Tags, SpanOptions } from 'opentracing';module.exports = (options, app: Application) => async (ctx: Context, next: () => Promise<any>) => {const { tracer } = app;const spanOptions: SpanOptions = {tags: {[Tags.HTTP_METHOD]: ctx.method,[Tags.HTTP_URL]: ctx.href,},};const parentSpan = tracer.extract(FORMAT_HTTP_HEADERS, ctx.headers);if (parentSpan) {spanOptions.childOf = parentSpan;}let spanName = `${ctx.method} ${ctx.url}`;const span = tracer.startSpan(spanName, spanOptions);span.setTag('span.kind', 'server');// span.setTag ...ctx.rootSpan = span;try {await next();// span.setTag ...span.finish();} catch (error) {// span.setTag ...span.setTag(Tags.ERROR, true);span.log({event: Tags.ERROR,message: error.message,stack: error.stack,});span.finish();throw error;}};
3.3. Axios鏈路追蹤攔截器在傳送網路請求時,需給Axios Config傳入eggCtx,攔截器就能夠根據eggCtx建立子span。
// src/lib/fetch-tracing.tsimport {AxiosError,AxiosInstance,AxiosRequestConfig,AxiosResponse,} from 'axios';import {FORMAT_HTTP_HEADERS, SpanOptions, Tags,} from 'opentracing';function requestTracingInterceptor(config: AxiosRequestConfig) {const urlId = `${config.method} ${config.baseURL || ''}${config.url}`;const spanOptions: SpanOptions = {childOf: config.eggCtx.rootSpan,};const span = config.eggCtx.app.tracer.startSpan(config.traceSpanName || urlId,spanOptions,);// span.setTag ...config.traceSpan = span;return config;}function requestErrorTracingInterceptor(error: AxiosError) {const { traceSpan } = error.config;traceSpan.setTag(Tags.ERROR, true);traceSpan.setTag('reason', 'error in request');traceSpan.finish();return Promise.reject(error);}function responseSuccessTracingInterceptor(response: AxiosResponse) {const { traceSpan } = response.config;traceSpan.setTag(Tags.HTTP_STATUS_CODE, response.status);traceSpan.setTag('http.response_type', response.headers?.['content-type']);// span.setTag ...traceSpan.finish();return response;}function responseErrorTracingInterceptor(error: AxiosError) {const { traceSpan } = error.config;traceSpan.setTag(Tags.ERROR, true);traceSpan.setTag(Tags.HTTP_STATUS_CODE, error.code);traceSpan.finish();return Promise.reject(error);}export function applyTracingInterceptors(axiosInstance: AxiosInstance) {return {request: axiosInstance.interceptors.request.use(requestTracingInterceptor,requestErrorTracingInterceptor,),response: axiosInstance.interceptors.response.use(responseSuccessTracingInterceptor,responseErrorTracingInterceptor,),};}
首屏最佳化4.1. 首屏最佳化前透過Jaeger UI,發現網路請求有4大段依次執行,不能併發,網路延時較高。使用Jaeger UI檢視首屏的鏈路追蹤詳情,內容如下:
4.2. 首屏最佳化方案sessionSelf中介軟體只獲取Session的基本資訊,其他介面請求統一移到頁面渲染entry.tsx中。去除介面次序依賴,原先順序執行的介面,全部變成併發請求。區分企業賬號和普通賬號,去除多餘的介面請求。在NodeJS中,比較典型的處理方式是把原先多次await改成一次await Promise.all():
// 具體 Component 需要初始化的狀態; 未登入的使用者導航到登入頁面,不需要請求資料if (isLogin) {await Promise.all([fetchPageCommonData(traceContext),Layout?.getInitialProps?.(ctx),ActiveComponent?.getInitialProps?.(ctx),needAuthCheck && UnAuthCheck?.getInitialProps?.(ctx),].filter(Boolean));}
4.3. 首屏最佳化後
最佳化後,網路效能提升50%,請求個數減少3/8,減輕伺服器壓力。
API請求最佳化5.1. API請求最佳化前透過Jaeger UI,觀察到API請求的轉發也有類似的問題:網路介面依次執行、請求多餘的介面。
5.2. API請求最佳化方案經分析,發現API請求均不需要DescribeUsers與DescribeOrg…介面,大部分介面也不需要DescribeSession介面(後端服務自己完成Session校驗)。
因此,去除了中間兩個網路請求,僅需要填寫Uin的介面,才先呼叫DescribeSession介面。
5.3. API請求最佳化後最佳化後,網路效能提升50%,請求個數減少3/4,減輕伺服器壓力。
總結使用鏈路追蹤,我們可以直接觀察到呼叫鏈過長問題、效能問題,比原始的列印日誌方式要方便、高效。
鏈路追蹤,不僅能夠解決服務邊界的問題,在服務內部我們也可以新建多個span來觀測程式碼段的效能,比如,上文中“首屏最佳化後”的pageBeginSSR與dva18nInit。在專案實現中,我們透過它來最佳化第一個服務請求異常緩慢的問題:透過預先載入SSR JS檔案的方式來解決。
出處:https://mp.weixin.qq.com/s?__biz=MTEwNTM0ODI0MQ==&mid=2653453935&idx=1&sn=dd37f76919c29185a3f8939fd68ccbab