作者:覃志强,腾讯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) => {

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 ——从胡乱堆积到有理有据

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

让我知道你在看

文章来源于腾讯云开发者社区,点击查看原文