01
前端系统架构
前端使用 Egg + React + SSR 框架,仅用户导航时首屏使用服务端渲染(SSR),之后使用客户端渲染(CSR),可确保用户在首屏与其它页面均有极致的用户体验。Node层,也负责一些Web安全处理,比如:CSRF、CSP、缓存控制等。
02
面临的问题
加入Node层,在开发、测试与生产阶段,我们面临浏览器端不存在的问题:
-
网络请求调用链遥测,发现与解决调用链过长、多余接口调用的问题。
-
性能遥测,发现性能问题点并优化。
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. 首屏优化方案
-
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,减轻服务器压力。
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文件的方式来解决。
区块链赋能下的数据治理新思路
让我知道你在看
文章评论