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

陈泽韦

前言

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

为什么要做接口分析

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

为什么从前端收集数据

  • 前端上报接口数据可以捕获一些超时、网络错误等服务端无法接收的情况下的接口访问
  • 前端可以收集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 密集型)等等,该模块也是在一边使用一边迭代,也在业务上确实解决了一些痛点,但是还是有很多地方需要改进,下次再给大家分享 ^_^

相关推荐
小小小妮子~36 分钟前
深入理解 MySQL 架构
数据库·mysql·架构
雪球不会消失了2 小时前
MVC架构模式
架构·mvc
云云3214 小时前
云手机:Facebook多账号管理的创新解决方案
服务器·线性代数·安全·智能手机·架构·facebook
窗外的寒风4 小时前
java工作流模式、背包模式、适配器工厂模式整合架构,让服务任务编排更便捷
架构·适配器模式
绝无仅有6 小时前
PHP语言laravel框架中基于Redis的异步队列使用实践与原理
后端·面试·架构
AI人H哥会Java6 小时前
【Spring】基于XML的Spring容器配置——<bean>标签与属性解析
java·开发语言·spring boot·后端·架构
time_silence6 小时前
微服务——技术选型与框架
微服务·架构
dbcat官方6 小时前
1.微服务灰度发布(方案设计)
java·数据库·分布式·微服务·中间件·架构
S-X-S8 小时前
【微服务】整合Nacos注册中心和动态配置
微服务·rpc·架构
嵌入(师)9 小时前
嵌入式驱动开发详解20(IIO驱动架构)
驱动开发·架构