代码插桩化

image

from pixiv

理论

代码插桩化

插桩所需的计时器

软件和硬件定时器

  • 软件定时器:系统范围的高分辨率定时器如C++中的std::chrono
  • 硬件定时器:时间戳计数器(TSC):这是一种硬件定时器,实现为硬件寄存器。
    • 不同架构下(x86,arch64,riscv)获取时间戳计数器的方式可能不同
    • 比如在x86下可以使用rdtsc指令指令,在鲲鹏(arch64架构)下使用cntvct_el0
    //x86
    static uint64_t Rdtsc() 
    { 
    uint32_t lo, hi; 
    __asm__ __volatile__ ("rdtsc" : "=a" (lo), "=d" (hi)); 
    return (uint64_t)hi << 32 | lo; 
    }
    // arch64
    static uint64_t Rdtsc() 
    { 
    uint64_t count_num; 
    Current_Speed = 2400; // Current Speed =2400MHz 
    External_Clock = 100; // External Clock = 100MHz 
    __asm__ __volatile__ ("mrs %0, cntvct_el0" : "=r" (count_num)); 
    return count_num *(Current_Speed / External_Clock);
    }
    

实践

程序/变量的生命周期

若对于一个大型项目我们进行代码插桩,然后进行基准测试,将一系列想要通过插桩获得的信息保存在我们编写的插桩数据结构中,但是最终如何输出这些信息呢?

因为面对一个大型项目,在对其进行基准测试时,我们常常是找不到其main函数的,即我们不知道我们要插桩的程序何时不再被调用,从而我们可以不用再收集信息。那么有如下解决方法:

在程序的生命周期管理中,C语言的GCC构造/析构属性、C++的析构函数和Java的Hook机制确实存在关联,但它们的实现方式和作用层级有所不同。以下是它们的核心关系与区别分析:


1. C语言中的GCC构造/析构属性

  • 功能
    • __attribute__((constructor)):标记的函数会在main()前自动执行,用于全局初始化(如加载资源)。
    • __attribute__((destructor)):标记的函数在main()结束后或exit()时执行,用于全局清理(如释放资源)。
  • 作用层级全局程序级别,与程序启动/终止直接相关。
  • 示例
    void __attribute__((constructor)) init() { 
        printf("Global init\n");
    }
    void __attribute__((destructor)) cleanup() { 
        printf("Global cleanup\n");
    }
    

__attribute__((destructor))用于标记一个函数,使其在以下两种场景中自动执行:

  • 程序正常终止时(例如 main 函数返回后或exit()退出)。
  • 共享库(动态库)被卸载时(例如程序退出或动态库被显式卸载)。

在Linux通过kill -TERM $pid可使得触发,需要注意kill -9 $pid并不会触发__attribute__((destructor))


2. C++中的析构函数

  • 功能
    • 类的成员函数(~ClassName()),在对象生命周期结束时自动调用,用于释放对象占用的资源(如内存、文件句柄)。
  • 作用层级对象级别,依赖对象的创建和销毁(如离开作用域、delete操作)。
  • 确定性:析构时机明确,与对象生命周期严格绑定。
  • 示例
    class MyClass {
    public:
        MyClass() { std::cout << "Constructor\n"; }
        ~MyClass() { std::cout << "Destructor\n"; } // 对象销毁时调用
    };
    

3. Java中的Hook

  • 常见形式
    • Shutdown Hook:通过Runtime.addShutdownHook(Thread)注册线程,在JVM关闭前执行清理(如关闭数据库连接)。
    • 广义Hook:通过反射或AOP(如Spring的@PreDestroy)拦截特定事件(如类加载、方法调用)。
  • 作用层级应用级别(如JVM关闭事件)或代码执行流程级别
  • 非确定性:Shutdown Hook的触发依赖JVM正常终止,无法处理强制终止(如kill -9)。
  • 示例
    Runtime.getRuntime().addShutdownHook(new Thread(() -> {
        System.out.println("JVM cleanup");
    }));
    

触发条件:

  • 程序正常终止(如 System.exit(0) 或 main 结束)。
  • 用户强制终止(如 Ctrl+C 或命令行 kill 命令(等同于kill -TERM))。
  • 不触发的情况:kill -9 或 JVM 崩溃。

总结

特性 GCC构造/析构属性 C++析构函数 Java Hook
作用层级 全局程序级别 对象级别 应用级别/代码流程级别
触发时机 main()前后或exit() 对象生命周期结束时 JVM关闭或事件发生时
确定性 确定(程序启动/终止) 确定(对象销毁时) 不确定(依赖外部事件)
典型用途 全局资源管理 对象资源回收 应用级清理或流程拦截

三者均用于管理程序生命周期中的资源,但设计哲学不同:

  • C/C++ 通过编译器或语言特性提供确定性控制。
  • Java 依赖运行时和事件机制,更灵活但确定性较弱。

Redis插桩

Redis是用C的,如果想要测试Redis某个函数的执行时间的话:

static inline uint64_t read_cyclecount(void) {
    uint64_t cnt;
    __asm__ volatile("mrs %0, cntvct_el0": "=r"(cnt));
    return cnt;
}
static inline uint64_t read_counter_freq(void) {
    uint64_t freq;
    __asm__ volatile("mrs %0, cntfrq_el0": "=r"(freq));
    return freq;
}
void init_instrumentationInfo(void) __attribute__((constructor));
void final_instrumentationInfo(void) __attribute__((destructor));
void init_instrumentationInfo(void) {
    printf("init_instrumentationInfo\n");
	// 初始化
	xxx
}
void final_instrumentationInfo(void) {
    printf("final_instrumentationInfo\n");
	// 输出最终信息等
	xxx
}
#define INSTR_START     \
   uint64_t start = read_cyclecount();

#define INSTR_END(category)       \
   uint64_t end = read_cyclecount();    \
   uint64_t cycles = end - start;       \
   uint64_t freq = read_counter_freq(); \
   uint64_t nsec = (uint64_t)((double)cycles * 1e9 / (double)freq);

#ifdef 和 #define使用

1. 宏中嵌入预处理指令

C 标准规定:宏展开后的令牌序列不会再被当作预处理指令处理(即使它看起来像 #ifdef…#endif)
如果宏体里写了 #ifdef,预处理器只会把它视作普通文本插入输出,最终编译时就出现“# is not followed by a macro parameter”

类似地,任何宏展开后生成的 #if、#endif 都不会被再解析,也就是说我们不能写类型如下内容:

#define
#ifdef xxx
#else
#endif

我们可以做些改变写成如下:

#ifdef xxx
  #define xxx	\
    xxx		\
	xxx
else 
  #define xxx	\
    xxx		\
	xxx
#endif

需要注意#ifdef/#endif 缩进导致无法识别
在 C/C++ 预处理器中,条件编译指令(如 #ifdef、#endif)必须位于行首(第 1 列),否则编译器会将 # 视为普通字符

2. 续行反斜杠后有空格问题

在宏定义中每行末尾 \ 必须紧贴最后一个字符、且直接接换行符才能起到续行作用,否则会报“warning: backslash and newline separated by space”, 正确写法为:

#define xxx 	\
   xxx		\
   xxx		\
   xxx

\后确保无其他字符,空格也不行。

基于Java agent和ASM机制进行插桩

具体而言我想要实现对Hive的某个函数进行插桩,如:

对Hive函数Lorg/apache/hadoop/hive/ql/exec/CommonJoinOperator;::genUniqueJoinObject进行插桩,测量其每次调用时的运行时间,记录在文件中。

在Java中,以 L 开头的类型表示法(如 Ljava/lang/String;)是 JVM内部使用的类型描述符(Type Descriptor)的一部分,用于标识引用类型(类或接口):

  • 例如:Ljava/lang/String; 对应 java.lang.String 类。
  • Lcom/example/Log则对应 com.example.Log 类。
  • 那么Lorg/apache/hadoop/hive/ql/exec/CommonJoinOperator对应于org.apache.hadoop.hive.ql.exec.CommonJoinOperator

Lorg/apache/hadoop/hive/ql/exec/CommonJoinOperator;::genUniqueJoinObject可以在Hive Github仓库中搜索CommonJoinOperator.java然后找到genUniqueJoinObject

参考博客:javaagent+ASM获取方法执行时间和打印参数

Can t Pass Java Agent in Hive


即使用java agent的方法,一般步骤为:

1.编写java agent + ASM的java插桩代码
2.将代码编译打包成jar包
3.运行插桩代码的方法为加上javaopt: -javaagent:/path/to/yourjar

对于Hive,需要再Hadoop集群, tez(如果使用的话)的启动命令参数处加上-javaagent:

# 位于$HADOOP_HOME/etc/hadoop/hadoop-env.sh
export HDFS_NAMENODE_OPTS="-javaagent:"
export HDFS_SECONDARYNAMENODE_OPTS="-javaagent:"
export HDFS_DATANODE_OPTS="-javaagent:"

# 位于$HADOOP_HOME/etc/hadoop/yarn-env.sh
export YARN_NODEMANAGER_OPTS="-javaagent:"
export YARN_RESOURCEMANAGER_OPTS="-javaagent:"

# 在 Hive 会话中设置
set hive.tez.java.opts="-javaagent:"
set tez.am.launch.cmd-opts="-javaagent:"
set tez.task.launch.cmd-opts="-javaagent:"

# 位于${HIVE_CONF_DIR}/hive-env.sh
export HADOOP_OPTS="-javaagent:"
export HIVE_METASTORE_HADOOP_OPTS="-javaagent:"

Java Agent 项目结构:

your-agent-project/
├── src/
│   └── main/
│       ├── java/
│       │   └── com/
│       │       └── example/
│       │           ├── agent/
│       │           │   ├── TimingAgent.java
│       │           │   └── CommonJoinTransformer.java
│       │           └── util/
│       │               └── TimeRecorder.java
│       └── resources/
│           └── META-INF/
│               └── MANIFEST.MF
└── pom.xml

添加依赖 pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <!-- 项目基本信息 -->
    <groupId>com.example</groupId>
    <artifactId>hive-join-timing-agent</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm</artifactId>
            <version>9.2</version>
        </dependency>
        <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm-commons</artifactId>
            <version>9.2</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.2.4</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                    </execution>
                </executions>
	    </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-jar-plugin</artifactId>
            <version>3.3.0</version>
            <configuration>
                <archive>
                    <manifestEntries>
                        <Premain-Class>com.example.agent.TimingAgent</Premain-Class>
                        <Can-Redefine-Classes>true</Can-Redefine-Classes>
                        <Can-Retransform-Classes>true</Can-Retransform-Classes>
                    </manifestEntries>
                </archive>
            </configuration>
        </plugin>
        </plugins>
    </build>
</project>

配置 MANIFEST.MF:

Manifest-Version: 1.0
Premain-Class: com.example.agent.TimingAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true

实现 Java Agent (TimingAgent.java):

package com.example.agent;

import java.lang.instrument.Instrumentation;

public class TimingAgent {
    public static void premain(String args, Instrumentation inst) {
	    System.out.println("-----------------启动开始---------------------");
        inst.addTransformer(new CommonJoinTransformer(), true);
	    System.out.println("-----------------启动结束---------------------");
    }
}

实现 Class Transformer (CommonJoinTransformer.java):

package com.example.agent;

import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AdviceAdapter;
import org.objectweb.asm.commons.Method;

class CommonJoinTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(
            ClassLoader loader,
            String className,
            Class<?> classBeingRedefined,
            ProtectionDomain protectionDomain,
            byte[] classfileBuffer) {

        if (!"org/apache/hadoop/hive/ql/exec/CommonJoinOperator".equals(className)) {
            return null;
        }
        try {
            ClassReader reader = new ClassReader(classfileBuffer);
            ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
            ClassVisitor cv = new CommonJoinClassVisitor(writer);
            reader.accept(cv, ClassReader.EXPAND_FRAMES);
            return writer.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

class CommonJoinClassVisitor extends ClassVisitor {
    public CommonJoinClassVisitor(ClassVisitor cv) {
        super(Opcodes.ASM5, cv);
    }

    @Override
    public MethodVisitor visitMethod(
            int access, String name, String desc,
            String signature, String[] exceptions) {

        MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
        // 精确匹配方法名和 descriptor "(II)V"
        if ("genUniqueJoinObject".equals(name)) {
            // 使用 AdviceAdapter 简化插桩逻辑
            return new TimingAdviceAdapter(api, mv, access, name, desc);
        }
        return mv;
    }
}

class TimingAdviceAdapter extends AdviceAdapter {
    private int startTimeVar;

    protected TimingAdviceAdapter(int api, MethodVisitor mv, int access, String name, String desc) {
        super(api, mv, access, name, desc);
    }

    @Override
    protected void onMethodEnter() {
        // long start = System.nanoTime();
        invokeStatic(Type.getType("Ljava/lang/System;"), new Method("nanoTime", "()J"));
        startTimeVar = newLocal(Type.LONG_TYPE);
        storeLocal(startTimeVar, Type.LONG_TYPE);
    }

    @Override
    protected void onMethodExit(int opcode) {
        if (opcode == RETURN) {
            // 1. 计算 elapsed = System.nanoTime() - startTimeVar
            invokeStatic(
                Type.getType("Ljava/lang/System;"),
                new Method("nanoTime", "()J")
            );
            loadLocal(startTimeVar, Type.LONG_TYPE);
            math(SUB, Type.LONG_TYPE);

            // 2. 将 elapsed 存到一个新局部变量
            int elapsedVar = newLocal(Type.LONG_TYPE);
            storeLocal(elapsedVar, Type.LONG_TYPE);

            // 3. 先加载 TimeRecorder.INSTANCE,再加载 elapsed
            getStatic(
                Type.getType("Lcom/example/util/TimeRecorder;"),
                "INSTANCE",
                Type.getType("Lcom/example/util/TimeRecorder;")
            );
            loadLocal(elapsedVar, Type.LONG_TYPE);

            // 4. 调用 recordTime(long)
            invokeVirtual(
                Type.getType("Lcom/example/util/TimeRecorder;"),
                new Method("recordTime", "(J)V")
            );
        }
    }
}

实现时间记录工具类 (TimeRecorder.java):

package com.example.util;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class TimeRecorder {
    public static final TimeRecorder INSTANCE = new TimeRecorder();
    private static final String FILE_PATH = "/home/yxlin/redis/output/hive.csv";

    private BufferedWriter writer;

    private TimeRecorder() {
        try {
            Path path = Paths.get(FILE_PATH);
            Path parentDir = path.getParent();
            if (parentDir != null) {
                Files.createDirectories(parentDir);
            }

            boolean fileExists = Files.exists(path);
            
            writer = new BufferedWriter(new FileWriter(FILE_PATH, true));
            
            if (!fileExists) {
                writer.write("RunTime\r\n");
                writer.flush();
            }

            Runtime.getRuntime().addShutdownHook(new Thread(() -> {
                try {
                    if (writer != null) {
                        writer.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }));
        } catch (IOException e) {
            e.printStackTrace();
            writer = null; // 明确标记初始化失败
        }
    }

    public synchronized void recordTime(long duration) {
        if (writer == null) {
            System.err.println("TimeRecorder未正确初始化!");
            return; // 防止NPE
        }
        try {
            writer.write(duration + "\r\n");
            writer.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

构建并打包 Agent

mvn clean package

BUG

在添加-javaagent:path/to/your-agent.jar后会出现ERROR: Cannot set priority of namenode process 1166577之类的报错,导致Hadoop的集群启动不了,日志中也没有显示错误

同时hive-testbench-hdp3中的测试也运行不起来了...目前解决不了。

这个问题主要是因为java agent程序出错,导致集群线程启动不起来,也就无法设置优先级了。
所以建议可以先简单写个java程序,测试一下java agent程序,彻底排除java agent的错误后再将其使用到组件中

源码手动插桩

在 CommonJoinOperator.java 中添加代码:

public Object genUniqueJoinObject() {
    long startTime = System.nanoTime(); // 记录开始时间
    try {
        // 原方法逻辑
        // ...
        return result;
    } finally {
        long duration = System.nanoTime() - startTime;
        TimeRecorder.recordTime(duration); // 记录耗时
    }
}

// TimeRecorder.java(需添加到 Hive 源码中)
package org.apache.hadoop.hive.ql.exec;
import java.util.concurrent.atomic.LongAdder;

public class TimeRecorder {
    private static final LongAdder totalTime = new LongAdder();

    public static void recordTime(long duration) {
        totalTime.add(duration);
    }

    static {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println("[Manual] Total time of genUniqueJoinObject: " + totalTime.sum() + " ns");
        }));
    }
}

然后重新编译Hive并部署。

posted @ 2025-04-26 13:14  次林梦叶  阅读(105)  评论(0)    收藏  举报