专业的编程技术博客社区

网站首页 > 博客文章 正文

古茗是怎么做前端数据中心的之接口分析篇

baijin 2024-08-12 14:00:28 博客文章 6 ℃ 0 评论

前言

之前我们介绍了古茗前端数据中心的大纲:古茗前端数据中心,现在我们将其中接口分析的内容单独拿出来讲讲

为什么要做接口分析

  • 经常有一些后端接口治理的情况,需要改造接口,通过接口分析可以了解哪些应用哪些页面使用了这些接口
  • 接口发生错误的时候可以及时告知
  • 提供指标分析,接口日志分析、错误分析

为什么从前端收集数据

  • 前端上报接口数据可以捕获一些超时、网络错误等服务端无法接收的情况下的接口访问
  • 前端可以收集hash路由等详细信息
  • 一般报错都是从前端感知的,我们以前端为起点排查问题更加方便

怎么采集数据

在不同的端侧使用不同的hook方式

Web 侧

通过 hack XMLHttpRequest和 Fetch 的方式,以fetch举例:

const originalFetch = window.fetch;
window.fetch = function (input: RequestInfo | URL, init?: RequestInit) {
  /// - 获取一些请求信息
  return originalFetch.apply(window, [input, init]).then((res: Response) => {
    /// - 获取一些响应信息
    return res;
  })
}


小程序侧

  • 微信小程序: https://developers.weixin.qq.com/miniprogram/dev/api/network/request/wx.request.html
  • 支付宝小程序: https://opendocs.alipay.com/mini/api/owycmh
  • 字节小程序: https://microapp.bytedance.com/docs/zh-CN/mini-app/develop/api/network/http/tt-request
  • 钉钉小程序: https://open.dingtalk.com/document/orgapp/send-network-requests
  • qq 小程序: https://q.qq.com/wiki/develop/miniprogram/API/network/network_request.html
  • 百度小程序: https://smartprogram.baidu.com/docs/develop/api/net/request/=

通过hack请求参数options的complete回调:

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 方法

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这些,主要便宜、便宜、便宜,大数据设施太贵了)

image.png

统计到 Redis

将每条数据按 url 和当天标识作为 key 存储到 redis 中

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 中

/// - 通过 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等数据,这里就不展开这些统计算法了

const cache = {
  [`${minute}`]: {
    [`${url}`]: {
      pv: 0,
      rt: 0,
      /// ... 等指标
    }
  }
}

然后就可以从查询数据直接输出给前端展示趋势了,例如

可以根据url查询具体趋势,也可以展示全部url的趋势,非常好用

接口日志

具体日志查询嘛,没有什么好介绍了,就是常规写入 ES 集群,然后进行查询,主要就是进行一些 ES 优化(有机会专门出一篇ES优化的文章)的操作 不过可以简单介绍下我们是如何使用 Nodejs 进行消费 Kafka 服务来写入到ES中并实现并发控制、自动流控的

并发控制

/// - 并发控制服务
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 的消费

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 密集型)等等,该模块也是在一边使用一边迭代,也在业务上确实解决了一些痛点,但是还是有很多地方需要改进,下次再给大家分享 ^_^


作者:陈泽韦

来源-微信公众号:Goodme前端团队

出处:https://mp.weixin.qq.com/s/nnF8P9FqOCkNUAubrGxVxg

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表