Android性能优化系列-腾讯matrix-流量监控之TrafficPlugin源码分析

这是性能优化系列之matrix框架的第17篇文章,我将在性能优化专栏中对matrix apm框架做一个全面的代码分析,性能优化是Android高级工程师必知必会的点,也是面试过程中的高频题目,对性能优化感兴趣的读者可以去我主页查看所有关于matrix的分享。

前言

本篇进行matrix框架的网络流量监控模块的代码分析。你可能想,为什么需要对流量进行监控呢?我们平常进行的网络接口请求都是一些必要的操作,监控它的意义何在?首先我们要明确流量监控的对象是什么,是上行(发请求消耗的流量)和下行(接收到服务器返回的数据流量)这两块消耗的用户流量。通过这个监控,我们可以清晰的看到每个接口在每次调用时所消耗的流量的具体值,有了这个数据之后,我们可以从两个维度来分析流量问题。第一,明确单次接口请求是否存在过多消耗流量的情况,从而促进网络数据包的体积优化;第二,从多次请求的维度来看,也能帮助我们定位是否存在单个接口请求数量异常的问题,从而定位代码存在的业务逻辑问题。笔者在万能钥匙的时候,就曾遇到过类似的情况,某接口在应用启动阶段频繁调用,导致用户流量消耗过多被用户投诉的问题。试想假如当时做了流量监控,那么对开发团队来说,就可以更高效的定位到问题所在。

言归正传,我们进入今天的代码分析,分析的对象时matrix中的TrafficPlugin,我们从它的几个关键方法入手。

  • 静态代码块
  • start
  • stop

静态代码块

arduino 复制代码
static {
    System.loadLibrary("matrix-traffic");
}

根据加载的名称matrix-traffic找到MatrixTraffic.cc这个c++的class,loadLibrary方法执行的时候会进入到JNI_OnLoad方法,JNI_OnLoad方法通常用来做一些准备性的工作,用于后边c++层和Java层的一个互调,下面是一部分关键代码,可以看到,这里将Java层的TrafficPlugin保存为全局引用,并获取了它的setStackTrace方法备用,并动态注册了一些Java层到native层方法的映射关系。

kotlin 复制代码
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
    jclass trafficCollectorCls = env->FindClass("com/tencent/matrix/traffic/TrafficPlugin");
    if (!trafficCollectorCls)
        return -1;
    //保存TrafficPlugin的jclass对象为全局引用
    gJ.TrafficPlugin = static_cast<jclass>(env->NewGlobalRef(trafficCollectorCls));
    //保存TrafficPlugin的setStackTrace方法为全局引用
    gJ.TrafficPlugin_setFdStackTrace =
            env->GetStaticMethodID(trafficCollectorCls, "setStackTrace", "(Ljava/lang/String;Ljava/lang/String;)V");
    //动态注册一些Java层方法到native方法的映射关系
    if (env->RegisterNatives(
            trafficCollectorCls, TRAFFIC_METHODS, static_cast<jint>(NELEM(TRAFFIC_METHODS))) != 0)
        return -1;
    return JNI_VERSION_1_6;
} 

start

通过nativeInitMatrixTraffic方法调用进入native层,根据上边JNI_OnLoad动态注册的映射关系找到MatrixTraffic.cc中的nativeInitMatrixTraffic方法。

scss 复制代码
@Override
public void start() {
    //这里可以设置需要过滤的so
    String[] ignoreSoFiles = trafficConfig.getIgnoreSoFiles();
    //进入native层
    nativeInitMatrixTraffic(trafficConfig.isRxCollectorEnable(), trafficConfig.isTxCollectorEnable(), trafficConfig.willDumpStackTrace(), trafficConfig.willDumpNativeBackTrace(), trafficConfig.willLookupIpAddress(), ignoreSoFiles);
}

MatrixTraffic.cc中的nativeInitMatrixTraffic方法。

ini 复制代码
static void nativeInitMatrixTraffic(JNIEnv *env, jclass, jboolean rxEnable, jboolean txEnable, jboolean dumpStackTrace, jboolean dumpNativeBackTrace, jboolean lookupIpAddress, jobjectArray ignoreSoFiles) {
    //启动loop循环线程
    TrafficCollector::startLoop(dumpStackTrace == JNI_TRUE, lookupIpAddress == JNI_TRUE);
    //是否dump native堆栈
    sDumpNativeBackTrace = (dumpNativeBackTrace == JNI_TRUE);
    //需要过滤的so
    ignoreSo(env, ignoreSoFiles);
    //通过hook socket实现对网络请求的拦截
    hookSocket(rxEnable == JNI_TRUE, txEnable == JNI_TRUE);
}

startLoop

首先startLoop启动循环线程

arduino 复制代码
void TrafficCollector::startLoop(bool dumpStackTrace, bool lookupIpAddress) {
    thread loopThread(loop);
    loopThread.detach();
}

loop循环线程是作为一个消费者线程出现的,我们先跳过这个方法的具体实现,先把生产者生产数据的过程看一下。

csharp 复制代码
void loop() {
    while (loopRunning) {
        if (msgQueue.empty()) {
            queueMutex.lock();
        } else {
            ...
        }
    }
}

hookSocket

网络请求最终都是通过底层socket进行发起的,所以通过hook socket的方式可以拦截到所有的网络请求,这里是用了plt hook的方式,什么是plt hook可以参考爱奇艺的xhook框架介绍。为了使代码更简洁,用...省略了部分代码。

scss 复制代码
static void hookSocket(bool rxHook, bool txHook) {
    //连接和关闭
    xhook_grouped_register(..., ".*\.so$", "connect",(void *) my_connect, (void **) (&original_connect));
    xhook_grouped_register(..., ".*\.so$", "close",(void *) my_close, (void **) (&original_close));
    //接收的数据监控
    if (rxHook) {
        xhook_grouped_register(..., ".*\.so$", "read",(void *) my_read, (void **) (&original_read));
        xhook_grouped_register(..., ".*\.so$", "recv",(void *) my_recv, (void **) (&original_recv));
        xhook_grouped_register(..., ".*\.so$", "recvfrom",(void *) my_recvfrom, (void **) (&original_recvfrom));
        xhook_grouped_register(..., ".*\.so$", "recvmsg",(void *) my_recvmsg, (void **) (&original_recvmsg));
    }
    //上传的数据监控
    if (txHook) {
        xhook_grouped_register(..., ".*\.so$", "write",(void *) my_write, (void **) (&original_write));
        xhook_grouped_register(..., ".*\.so$", "send",(void *) my_send, (void **) (&original_send));
        xhook_grouped_register(.., ".*\.so$", "sendto",(void *) my_sendto, (void **) (&original_sendto));
        xhook_grouped_register(.., ".*\.so$", "sendmsg",(void *) my_sendmsg, (void **) (&original_sendmsg));
    }
}

可以看到这里hook了socket的一些关键方法,这些方法被hook之后,当方法再次被调用的时候,我们就可以拦截到它的执行,从而做一些额外的处理。

  • connect
  • close
  • read
  • recv
  • recvfrom
  • recvmsg
  • write
  • send
  • sendto
  • sendmsg

connect

arduino 复制代码
int my_connect(int fd, sockaddr *addr, socklen_t addr_length) {
    TrafficCollector::enQueueConnect(fd, addr, addr_length);
    return original_connect(fd, addr, addr_length);
}

通过调用TrafficCollector的enQueueConnect方法记录本次socket连接的信息,将MSG_TYPE_CONNECT类型,文件描述符,socket地址,调用栈等信息封装成TrafficMsg存入msgQueue队列。

arduino 复制代码
void TrafficCollector::enQueueConnect(int fd, sockaddr *addr, socklen_t addr_length) {
    //将MSG_TYPE_CONNECT类型,文件描述符,socket地址,调用栈等信息封装成TrafficMsg存入msgQueue队列
    shared_ptr<TrafficMsg> msg = make_shared<TrafficMsg>(MSG_TYPE_CONNECT, fd, addr->sa_family, getKeyAndSaveStack(fd), 0);
    msgQueue.push(msg);
    queueMutex.unlock();
}

close

arduino 复制代码
int my_close(int fd) {
    TrafficCollector::enQueueClose(fd);
    return original_close(fd);
}

通过调用TrafficCollector的enQueueClose方法记录本次socket关闭的信息,将MSG_TYPE_CLOSE类型,文件描述符封装成TrafficMsg存入msgQueue队列。

arduino 复制代码
void TrafficCollector::enQueueClose(int fd) {
    shared_ptr<TrafficMsg> msg = make_shared<TrafficMsg>(MSG_TYPE_CLOSE, fd, 0, "", 0);
    msgQueue.push(msg);
    queueMutex.unlock();
}

read

arduino 复制代码
ssize_t my_read(int fd, void *buf, size_t count) {
    ssize_t ret = original_read(fd, buf, count);
    TrafficCollector::enQueueRx(MSG_TYPE_READ, fd, ret);
    return ret;
}

type为MSG_TYPE_READ,调用TrafficCollector的enQueueRx方法。

arduino 复制代码
void enQueueMsg(int type, int fd, size_t len) {
    shared_ptr<TrafficMsg> msg = make_shared<TrafficMsg>(type, fd, 0, getKeyAndSaveStack(fd), len);
    msgQueue.push(msg);
    queueMutex.unlock();
}

recv

arduino 复制代码
ssize_t my_recv(int sockfd, void *buf, size_t len, int flags) {
    ssize_t ret = original_recv(sockfd, buf, len, flags);
    TrafficCollector::enQueueRx(MSG_TYPE_RECV, sockfd, ret);
    return ret;
}

type为MSG_TYPE_RECV,调用TrafficCollector的enQueueRx方法。

arduino 复制代码
void enQueueMsg(int type, int fd, size_t len) {
    shared_ptr<TrafficMsg> msg = make_shared<TrafficMsg>(type, fd, 0, getKeyAndSaveStack(fd), len);
    msgQueue.push(msg);
    queueMutex.unlock();
}

recvfrom

arduino 复制代码
ssize_t my_recvfrom(int sockfd, void *buf, size_t len, int flags,
                    struct sockaddr *src_addr, socklen_t *addrlen) {
    ssize_t ret = original_recvfrom(sockfd, buf, len, flags, src_addr, addrlen);
    TrafficCollector::enQueueRx(MSG_TYPE_RECVFROM, sockfd, ret);
    return ret;
}

type为MSG_TYPE_RECVFROM,调用TrafficCollector的enQueueRx方法。

recvmsg

arduino 复制代码
ssize_t my_recvmsg(int sockfd, struct msghdr *msg, int flags) {
    ssize_t ret = original_recvmsg(sockfd, msg, flags);
    TrafficCollector::enQueueRx(MSG_TYPE_RECVMSG, sockfd, ret);
    return ret;
}

type为MSG_TYPE_RECVMSG,调用TrafficCollector的enQueueRx方法。

write

arduino 复制代码
ssize_t my_write(int fd, const void *buf, size_t count) {
    ssize_t ret = original_write(fd, buf, count);
    TrafficCollector::enQueueTx(MSG_TYPE_WRITE, fd, ret);
    return ret;
}

type为MSG_TYPE_WRITE,调用TrafficCollector的enQueueRx方法。

send

arduino 复制代码
ssize_t my_send(int sockfd, const void *buf, size_t len, int flags) {
    ssize_t ret = original_send(sockfd, buf, len, flags);
    TrafficCollector::enQueueTx(MSG_TYPE_SEND, sockfd, ret);
    return ret;
}

type为MSG_TYPE_SEND,调用TrafficCollector的enQueueRx方法。

sendto

arduino 复制代码
ssize_t my_sendto(int sockfd, const void *buf, size_t len, int flags,
                  const struct sockaddr *dest_addr, socklen_t addrlen) {
    ssize_t ret = original_sendto(sockfd, buf, len, flags, dest_addr, addrlen);
    TrafficCollector::enQueueTx(MSG_TYPE_SENDTO, sockfd, ret);
    return ret;
}

type为MSG_TYPE_SENDTO,调用TrafficCollector的enQueueRx方法。

sendmsg

arduino 复制代码
ssize_t my_sendmsg(int sockfd, const struct msghdr *msg, int flags) {
    ssize_t ret = original_sendmsg(sockfd, msg, flags);
    TrafficCollector::enQueueTx(MSG_TYPE_SENDMSG, sockfd, ret);
    return ret;
}

type为MSG_TYPE_SENDMSG,调用TrafficCollector的enQueueRx方法。

enQueueRx

可以看到上边除了connect和close方法外,其他都调用到了TrafficCollector的enQueueRx方法,我们看看这个方法做了什么。

go 复制代码
void TrafficCollector::enQueueTx(int type, int fd, size_t len) {
    enQueueMsg(type, fd, len);
}
arduino 复制代码
void enQueueMsg(int type, int fd, size_t len) {
    shared_ptr<TrafficMsg> msg = make_shared<TrafficMsg>(type, fd, 0, getKeyAndSaveStack(fd), len);
    msgQueue.push(msg);
    queueMutex.unlock();
}

enQueueRx方法在不断的封装TrafficMsg对象并存入队列msgQueue,每个方法间区别在于type的定义不同。所以到这里我们可以有一个简单的结论了: 被hook的这些方法,执行时会不断的获取socket此时的信息,封装成TrafficMsg对象存入队列供消费者线程进行消费,所以这里扮演的角色就是生产者线程。此时我们回头再去看loop线程。

loop

loop线程就是上边提到的消费者线程,消费线程不断的循环,当msgQueue中有数据时,就开始做进一步的处理。

rust 复制代码
void loop() {
    while (loopRunning) {
        if (msgQueue.empty()) {
            queueMutex.lock();
        } else {
            shared_ptr<TrafficMsg> msg = msgQueue.front();
            if (msg->type == MSG_TYPE_CONNECT) {
                //socket开始连接,以文件描述符为key, 地址为value存入fdFamilyMap中
                fdFamilyMap[msg->fd] = msg->sa_family;
            } else if (msg->type == MSG_TYPE_READ) {
                //接收数据,开始read,假如fdFamilyMap存在,这个fd,说明已连接过
                if (fdFamilyMap.count(msg->fd) > 0) {
                    appendRxTraffic(msg->threadName, msg->len);
                }
            } else if (msg->type >= MSG_TYPE_RECV && msg->type <= MSG_TYPE_RECVMSG) {
                //接收数据
                if (fdFamilyMap[msg->fd] != AF_LOCAL) {
                    appendRxTraffic(msg->threadName, msg->len);
                }
            } else if (msg->type == MSG_TYPE_WRITE) {
                //写入数据
                if (fdFamilyMap.count(msg->fd) > 0) {
                    appendTxTraffic(msg->threadName, msg->len);
                }
            } else if (msg->type >= MSG_TYPE_SEND && msg->type <= MSG_TYPE_SENDMSG) {
                //写入数据
                if (fdFamilyMap[msg->fd] != AF_LOCAL) {
                    appendTxTraffic(msg->threadName, msg->len);
                }
            } else if (msg->type == MSG_TYPE_CLOSE) {
                //关闭
                fdThreadNameMapLock.lock();
                fdThreadNameMap.erase(msg->fd);
                fdThreadNameMapLock.unlock();
                fdFamilyMap.erase(msg->fd);
            }
            msgQueue.pop();
        }
    }
}

从上边代码的注释可以看到,关键的两个方法是上传数据时调用的appendTxTraffic用来记录上行的数据流量,接收数据时调用的appendRxTraffic方法用来记录下行的数据流量,所以读或写的过程也就是不断的实时记录流量的过程。

appendTxTraffic

以线程名为key, 流量值为value存入txTrafficInfoMap中。

csharp 复制代码
void appendTxTraffic(const string& threadName, long len) {
    txTrafficInfoMapLock.lock();
    txTrafficInfoMap[threadName] += len;
    txTrafficInfoMapLock.unlock();
}

appendRxTraffic

以线程名为key, 流量值为value存入rxTrafficInfoMap中。

csharp 复制代码
void appendRxTraffic(const string& threadName, long len) {
    rxTrafficInfoMapLock.lock();
    rxTrafficInfoMap[threadName] += len;
    rxTrafficInfoMapLock.unlock();
}

看到这里感觉有点奇怪了,怎么只是将数据记录到对应的map中,什么时候取的数据?在TrafficPlugin.java中我们可以找到这个方法getTrafficInfoMap,它的返回值是HashMap,其实就是上边提到的存储起来的流量信息。

getTrafficInfoMap

typescript 复制代码
public HashMap<String, String> getTrafficInfoMap(int type) {
    //进入native层的方法
    return nativeGetTrafficInfoMap(type);
}

来到TrafficCollector的getTrafficInfoMap方法。

typescript 复制代码
static jobject nativeGetTrafficInfoMap(JNIEnv *env, jclass, jint type) {
    return TrafficCollector::getTrafficInfoMap(type);
}

可以看到,下面的逻辑很清晰,通过构造一个Java层的HashMap对象,并将指定类型的信息从c++层的map对象中转移到HashMap中,这样一来,就实时的拿到了当前流量消耗的数据,数据包含线程名和流量值,拿到线程名后可以通过getStackTraceMap拿到线程名和堆栈信息的映射关系,从而获取到实时的调用堆栈信息。

scss 复制代码
jobject TrafficCollector::getTrafficInfoMap(int type) {
    ...
    if (type == TYPE_GET_TRAFFIC_RX) {
        //接收的数据
        for (auto & it : rxTrafficInfoMap) {
            //线程名
            jstring threadName = env->NewStringUTF(it.first.c_str());
            //流量长度,是一个数值型
            jstring traffic = env->NewStringUTF(to_string(it.second).c_str());
            env->CallObjectMethod(jHashMap, mapPut, threadName, traffic);
        }
    } else if (type == TYPE_GET_TRAFFIC_TX) {
        //上传的数据
        for (auto & it : txTrafficInfoMap) {
            jstring threadName = env->NewStringUTF(it.first.c_str());
            jstring traffic = env->NewStringUTF(to_string(it.second).c_str());
            env->CallObjectMethod(jHashMap, mapPut, threadName, traffic);
        }
    }
    return jHashMap;
}

看一个效果图

stop

stop方法做的就是清理资源的工作了,因为核心功能都在native层,所以清理的工作还是会进入native层做处理,第一跳出循环线程,第二清理内存中的map映射表,至此,流量监控的代码分析完成。

typescript 复制代码
@Override
public void stop() {
    nativeReleaseMatrixTraffic();
}
arduino 复制代码
static void nativeReleaseMatrixTraffic(JNIEnv *env, jclass) {
    //停止循环线程
    TrafficCollector::stopLoop();
    //清理所有的映射表
    TrafficCollector::clearTrafficInfo();
}

总结

流量监控的实现方式是通过hook c++层socket的发起和接收相关的方法,拦截到对应方法从而对过程中涉及到的流量信息进行采集,采集到的数据就可以实时的获取到,以做进一步的分析。你会发现本次流量监控的实现和我们在Android性能优化系列-腾讯matrix IO优化之IOCanaryPlugin源码分析中分析的IO监控的实现原理是一致的,都用到了plt hook技术,其实这一技术的应用场景非常广,也非常的实用,应用层因为权限有限,要实现一些黑科技的操作,基本离不开plt hook,对此感兴趣可以读一下爱奇艺的xhook框架介绍

相关推荐
Estar.Lee1 小时前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
找藉口是失败者的习惯2 小时前
从传统到未来:Android XML布局 与 Jetpack Compose的全面对比
android·xml
uzong3 小时前
7 年 Java 后端,面试过程踩过的坑,我就不藏着了
java·后端·面试
Jinkey3 小时前
FlutterBasic - GetBuilder、Obx、GetX<Controller>、GetxController 有啥区别
android·flutter·ios
大白要努力!4 小时前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
天空中的野鸟5 小时前
Android音频采集
android·音视频
小白也想学C6 小时前
Android 功耗分析(底层篇)
android·功耗
曙曙学编程7 小时前
初级数据结构——树
android·java·数据结构
J老熊7 小时前
JavaFX:简介、使用场景、常见问题及对比其他框架分析
java·开发语言·后端·面试·系统架构·软件工程
猿java7 小时前
什么是 Hystrix?它的工作原理是什么?
java·微服务·面试