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

陈泽韦

前言

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

为什么要做接口分析

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

为什么从前端收集数据

  • 前端上报接口数据可以捕获一些超时、网络错误等服务端无法接收的情况下的接口访问
  • 前端可以收集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;
  })
}

小程序侧

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

相关推荐
.生产的驴12 分钟前
SpringCloud OpenFeign用户转发在请求头中添加用户信息 微服务内部调用
spring boot·后端·spring·spring cloud·微服务·架构
丁总学Java39 分钟前
ARM 架构(Advanced RISC Machine)精简指令集计算机(Reduced Instruction Set Computer)
arm开发·架构
ZOMI酱2 小时前
【AI系统】GPU 架构与 CUDA 关系
人工智能·架构
天天扭码9 小时前
五天SpringCloud计划——DAY2之单体架构和微服务架构的选择和转换原则
java·spring cloud·微服务·架构
余生H10 小时前
transformer.js(三):底层架构及性能优化指南
javascript·深度学习·架构·transformer
凡人的AI工具箱10 小时前
15分钟学 Go 第 60 天 :综合项目展示 - 构建微服务电商平台(完整示例25000字)
开发语言·后端·微服务·架构·golang
运维&陈同学11 小时前
【zookeeper01】消息队列与微服务之zookeeper工作原理
运维·分布式·微服务·zookeeper·云原生·架构·消息队列