NodeJS链路追踪与性能优化,首杀性能提升50%

2020年12月17日 342点热度 0人点赞 0条评论
作者:覃志强,腾讯CSIG研发工程师。

|语 微服务开发利器,网络调用链遥测,性能遥测。开发、测试、生产多套环境的链路与性能全在掌控之中,告别打日志定位性能问题的苦逼日子。首次优化,网络性能提升50%,后端接口请求量减少3/4。




01



前端系统架构



图片

前端使用 Egg + React + SSR 框架,仅用户导航时首屏使用服务端渲染(SSR),之后使用客户端渲染(CSR),可确保用户在首屏与其它页面均有极致的用户体验。Node层,也负责一些Web安全处理,比如:CSRF、CSP、缓存控制等。



02

面临的问题


加入Node层,在开发、测试与生产阶段,我们面临浏览器端不存在的问题:

  1. 网络请求调用链遥测,发现与解决调用链过长、多余接口调用的问题。

  2. 性能遥测,发现性能问题点并优化。


03

集成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.ts

import { 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.ts

import { 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.ts

import {

  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,

    ),

  };

}


04

首屏优化

4.1. 首屏优化前


通过Jaeger UI,发现网络请求有4大段依次执行,不能并发,网络延时较高。使用Jaeger UI查看首屏的链路追踪详情,内容如下:

图片

4.2. 首屏优化方案


  1. sessionSelf中间件只获取Session的基本信息,其他接口请求统一移到页面渲染entry.tsx中。

  2. 去除接口次序依赖,原先顺序执行的接口,全部变成并发请求。

  3. 区分企业账号和普通账号,去除多余的接口请求。


在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,减轻服务器压力。

图片

05

API请求优化

5.1. API请求优化前


通过Jaeger UI,观察到API请求的转发也有类似的问题:网络接口依次执行、请求多余的接口。

图片

5.2. API请求优化方案


经分析,发现API请求均不需要DescribeUsers与DescribeOrg…接口,大部分接口也不需要DescribeSession接口(后端服务自己完成Session校验)。


因此,去除了中间两个网络请求,仅需要填写Uin的接口,才先调用DescribeSession接口。


5.3. API请求优化后


优化后,网络性能提升50%,请求个数减少3/4,减轻服务器压力。

图片

06

总结



使用链路追踪,我们可以直接观察到调用链过长问题、性能问题,比原始的打印日志方式要方便、高效。


链路追踪,不仅能够解决服务边界的问题,在服务内部我们也可以新建多个span来观测代码段的性能,比如,上文中“首屏优化后”的pageBeginSSR与dva18nInit。在项目实现中,我们通过它来优化第一个服务请求异常缓慢的问题:通过预先加载SSR JS文件的方式来解决。

图片
近期热文

图片

信支付万亿日志在Hermes中的实践 

图片

如何做有说服力的PPT ——从胡乱堆积到有理有据 


图片

区块链赋能下的数据治理新思路 

图片

让我知道你在看

图片
49330NodeJS链路追踪与性能优化,首杀性能提升50%

这个人很懒,什么都没留下

文章评论