陈泽韦
前言
之前我们介绍了古茗前端数据中心的大纲: 古茗前端数据中心,现在我们将其中接口分析的内容单独拿出来讲讲
为什么要做接口分析
- 经常有一些后端接口治理的情况,需要改造接口,通过接口分析可以了解哪些应用哪些页面使用了这些接口
- 接口发生错误的时候可以及时告知
- 提供指标分析,接口日志分析、错误分析
为什么从前端收集数据
- 前端上报接口数据可以捕获一些超时、网络错误等服务端无法接收的情况下的接口访问
- 前端可以收集hash路由等详细信息
- 一般报错都是从前端感知的,我们以前端为起点排查问题更加方便
怎么采集数据
在不同的端侧使用不同的hook方式
Web 侧
通过 hack XMLHttpRequest和 Fetch 的方式,以fetch举例:
typescript
const originalFetch = window.fetch;
window.fetch = function (input: RequestInfo | URL, init?: RequestInit) {
/// - 获取一些请求信息
return originalFetch.apply(window, [input, init]).then((res: Response) => {
/// - 获取一些响应信息
return res;
})
}
小程序侧
- 微信小程序: developers.weixin.qq.com/miniprogram...
- 支付宝小程序: opendocs.alipay.com/mini/api/ow...
- 字节小程序: microapp.bytedance.com/docs/zh-CN/...
- 钉钉小程序: open.dingtalk.com/document/or...
- qq 小程序: q.qq.com/wiki/develo...
- 百度小程序: smartprogram.baidu.com/docs/develo...=
通过hack请求参数options的complete回调:
typescript
const originRequest = global[method];
Object.defineProperty(global, method, {
writable: true,
enumerable: true,
configurable: true,
value(...args: any[]) {
const options: RequestOption = args[0];
const originComplete = options.complete;
/// - 获取一些请求信息
options.complete = function (res: any) {
/// - 这里获取响应信息
originComplete && originComplete(res);
}
}
}
Flutter 侧
通过覆盖 HttpOverrides 来实现拦截, 实现自己的 CustomHttpOverrides
然后实现一个 CustomHttpClient 重写 open 方法
dart
HttpOverrides? origin = HttpOverrides.current;
HttpOverrides.global = CustomHttpOverrides(origin: origin);
/// 重写 HttpOverrides
class CustomHttpOverrides extends HttpOverrides {
/// 原始的 HttpOverrides
final HttpOverrides? origin;
CustomHttpOverrides({this.origin});
/// 覆写 createHttpClient
/// 原有 HttpOverrides存在,直接创建 _httpClient对象
/// HttpOverrides 不存在,置空 HttpOVerrides.global 创建默认 _httpClient; 用自己实现的 HttpClient持有
@override
HttpClient createHttpClient(SecurityContext? context) {
if (origin != null) {
return origin!.createHttpClient(context);
}
HttpOverrides.global = null;
final httpClient = CustomHttpClient(HttpClient(context: context));
HttpOverrides.global = this;
return httpClient;
}
}
/// 实现自己的 HttpClient
class CustomHttpClient implements HttpClient {
/// - 实现其他方法
@override
Future<HttpClientRequest> open(String method, String host, int port, String path) {
const int hashMark = 0x23;
const int questionMark = 0x3f;
int fragmentStart = path.length;
int queryStart = path.length;
for (int i = path.length - 1; i >= 0; i--) {
var char = path.codeUnitAt(i);
if (char == hashMark) {
fragmentStart = i;
queryStart = i;
} else if (char == questionMark) {
queryStart = i;
}
}
String? query;
if (queryStart < fragmentStart) {
query = path.substring(queryStart + 1, fragmentStart);
path = path.substring(0, queryStart);
}
Uri uri =
Uri(scheme: "http", host: host, port: port, path: path, query: query);
return _openUrl(method, uri);
}
@override
Future<HttpClientRequest> openUrl(String method, Uri url) {
return _openUrl(method, url);
}
/// 自己实现 openUrl 方法
/// 用来发送请求
Future<HttpClientRequest> _openUrl(String method, Uri url) async {
HttpClientRequest request;
/// 生成唯一标识
String key = uuid.v1();
try {
request = await origin.openUrl(method, url);
request = HttpRequest(this, request, key);
/// - 获取请求信息
} catch (e) {
/// - 获取错误信息
rethrow;
}
return request;
}
}
数据如何处理
指标处理
我们使用 nodejs+redis+influxdb+mysql来处理指标数据(不要问什么不用 flink、ck这些,主要便宜、便宜、便宜,大数据设施太贵了)
统计到 Redis
将每条数据按 url 和当天标识作为 key 存储到 redis 中
typescript
const date = new Date();
const [yy, mm, dd] = [date.getFullYear(), date.getMonth(), date.getDate()]
/// - 按天来存储数据
await redis().incrby(`${url}.${yy}${mm}${dd}`, 1)
/// - 使用 set 存储当天有哪些 url
await redis().sadd(`full-urls.${yy}${mm}${dd}`, value)
定时任务
通过定时任务将列表型统计数据写入到 mysql 中
typescript
/// - 通过 sscan 来分批处理 url 列表,自行分页
const [, members] = await redis().sscan(skey, (page - 1) * 300, 'COUNT', 300)
/// - 然后通过拿出来的 url 加上时间参数,获取 pv
/// - 如果需要uv等数据,在上面统计的时候写入,这里获取就行了
/// - 这边就举例 pv 的案例
const pv = await redis().get(pvKey) || 0
/// - 然后将 pv 写入 mysql
统计趋势
这块略复杂,这边做个简单的介绍 首先在内存中保存按分钟维度作为key的缓存,我们将url分类写入缓存,通过统计算法统计数据 然后每分钟,从内存中取出缓存,写入 influxdb 并清空缓存 缓存结构大概长这样, 实际有些指标需要计算p99、p95等数据,这里就不展开这些统计算法了
typescript
const cache = {
[`${minute}`]: {
[`${url}`]: {
pv: 0,
rt: 0,
/// ... 等指标
}
}
}
然后就可以从查询数据直接输出给前端展示趋势了,例如 可以根据url查询具体趋势,也可以展示全部url的趋势,非常好用
接口日志
具体日志查询嘛,没有什么好介绍了,就是常规写入 ES 集群,然后进行查询,主要就是进行一些 ES 优化(有机会专门出一篇ES优化的文章)的操作 不过可以简单介绍下我们是如何使用 Nodejs 进行消费 Kafka 服务来写入到ES中并实现并发控制、自动流控的
并发控制
typescript
/// - 并发控制服务
export class ConcurrencyService {
/// - 队列缓存
private queue: Function[] = [];
/// - 当前并发数
public currentConcurrency = 0;
/// - 最大并发数
public maxConcurrency = 2
/// - 执行任务
public exec(fn: any) {
this.queue.push(fn)
this.next()
}
private next() {
const maxConcurrency = this.maxConcurrency;
if (this.currentConcurrency < maxConcurrency && this.queue.length > 0) {
const fn = this.queue.shift();
this.currentConcurrency++;
fn()
.catch((error: any) => {
console.error(error);
})
.finally(() => {
this.currentConcurrency--;
this.next();
});
}
}
}
自动流控
我们可以根据 ConcurrencyService 的 queue 缓存队列的长度来控制是否暂停/恢复 Kafka 的消费
typescript
if (this.queue.length <= 3 && this.paused) {
this.paused = false;
console.warn(`负载恢复, 开启消费`)
this.kafka.resumeTopics(topics)
}
if (this.queue.length > 6 && !this.paused) {
this.paused = true;
console.warn(`负载过高, 暂停消费`)
this.kafka.pauseTopics(topics)
}
这样我们就可以根据写入速度来控制消费速度(这可能会使数据查询的时候有延迟,因为流控可能导致最新数据还未被消费),我们可以根据 ES 集群的写入性能来调整并发数和批量写入的数量
全链路追踪
排查问题还有个最重要的(找到锅在哪里)流程,收到反馈后我们定位出问题的接口,然该接口对应的后端服务(服务负责人),可以有效降低沟通成本,那么我们是如何实现求全链路追踪的呢 首先需要后端服务都接入 arms 服务(自研/阿里云都可以),这样所有接口响应头都会包含类似 traceid 的字段,我们上报的时候需要携带上它 然后我们就可以通过 traceid 与后端日志/arms系统对接,直接查询他们数据展示到平台上,就可以直观的看到整个请求链路了
总结
总的来说,监控平台接口分析是一个复杂而关键的系统模块,在做接口分析的过程中,也碰到了一些难题,例如数据量级(数十TB)、性能瓶颈(计算指标的 cpu 密集型)等等,该模块也是在一边使用一边迭代,也在业务上确实解决了一些痛点,但是还是有很多地方需要改进,下次再给大家分享 ^_^