前文介绍了byteman的基本语法以及流量回放平台,今天一起看下如何使用byteman如何对 Redis 相关命令进行数据记录和回放.
这里记录和回放的难点是找到redis命令执行的处理方法.
1
Redis数据切入点
本例中, 基于spring boot框架中redis使用三方依赖包的lettuce.jar
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>5.0.5.RELEASE</version>
<scope>compile</scope>
</dependency>
lettuce中集成处理所有命令的核心处理类是AbstractInvocationHandler. 而redis的单机模式, 哨兵或者cluster集群模式, 都是实现抽象方法handleInvocation()完成命令执行的.
public abstract class AbstractInvocationHandler implements InvocationHandler {
public final Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// ...
return handleInvocation(proxy, method, args);
}
protected abstract Object handleInvocation(Object proxy, Method method, Object[] args) throws Throwable;
}
2
请求记录
前文已经提过, 请求录制一般都是在生产环境中, 所以Redis处理请求记录的时机, 应该是invoke()方法执行结束后, 将参数和结果通过日志打印出来. 通过Debug可以发现参数和返回值都是byte[]或者byte[][],这并不利于收集和回放时数据的还原. 所以需要对数据进行格式化, 这里选择base64编码.这时就需要helper了. byteman配置文件及注入方式,参考前文byteman.
redis-record.btm
####### record #####
RULE redis execution recorder InvocationHandler.exit
CLASS io.lettuce.core.internal.AbstractInvocationHandler
METHOD invoke
AT EXIT
BIND handler = $this;
method = $2;
methodName = method.getName();
HELPER com.in.rt.helper.RedisReplayTraceHelper
IF !method.getName().equals("getConnection")
&& !method.getName().equals("readonly")
&& !method.getName().equals("nodes")
&& !method.getName().equals("command")
&& !method.getName().equals("hashcode")
&& !method.getName().equals("equals")
&& !method.getName().equals("toString")
DO log(methodName,$3,$!);
ENDRULE
byteman辅助类: RedisReplayTraceHelper
public void log(String methodName, Object[] args, Object result) {
record("CACHE", methodName, args, result);
}
private void record(String prefix, String methodName, Object[] args, Object result) {
String re = toStr(result);
String arg = toStr(args);
log.error(marker, "{}-{}-{}-{}-{}", prefix, methodName, System.nanoTime(), arg, re);
}
public String toStr(Object result) {
try (final ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(result);
String logs = Base64.getEncoder().encodeToString(baos.toByteArray());
log.debug("logs:{}", logs);
return logs;
} catch (Exception e) {
log.error("toStr error:", e);
throw new RuntimeException("toStr exception:", e);
}
}
3
请求回放
请求回放是为了验证服务的新增功能是否对预想外的接口或逻辑产生影响, 这个过程一般会在测试环境中进行. 在具体实现时, 并不需要真正去执行原有命令, 只需要根据执行方法, 参数等信息从流量银行中取就可以. 同样也是利用byteman拦截执行字节码实现.
redis-replay.btm
####### replay #####
RULE redis execution recorder InvocationHandler.entry
CLASS io.lettuce.core.internal.AbstractInvocationHandler
METHOD invoke
AT ENTRY
BIND handler = $this;
method = $2;
methodName = method.getName();
HELPER com.in.rt.helper.RedisReplayTraceHelper
IF !method.getName().equals("getConnection")
&& !method.getName().equals("readonly")
&& !method.getName().equals("nodes")
&& !method.getName().equals("command")
&& is(methodName, $3)
DO return replay(methodName)
ENDRULE
byteman辅助类: RedisReplayTraceHelper
public Object toObject(String str) {
byte[] decode = Base64.getDecoder().decode(str.getBytes());
try (final ByteArrayInputStream bain = new ByteArrayInputStream(decode);
ObjectInputStream oin = new ObjectInputStream(bain)) {
Object inObj = oin.readObject();
log.debug("inObj:{}", inObj);
return inObj;
} catch (Exception e) {
log.error("toObject error:{}", e);
throw new RuntimeException("inObj exception:", e);
}
}
数据获取类 DataFetcher
public class DataFetcher {
private static final Logger log = LoggerFactory.getLogger("RedisCommandTraceHelper");
public String getData(String host, String traceId, String methodName, String nanoTime,
String args) {
HttpURLConnection con = null;
try {
log.debug("replay fetch data:{},{},{},{}", traceId, methodName, nanoTime, args);
String spec = host + "/api/data?traceId=" + traceId
+ "&methodName=" + methodName + "&args=" + args;
if (nanoTime != null) {
spec = spec + "&nanoTime=" + nanoTime;
}
java.net.URL url = new java.net.URL(spec);
con = (HttpURLConnection) url.openConnection();
int status = con.getResponseCode();
if (status != 200) {
return null;
}
try (java.io.BufferedReader in = new java.io.BufferedReader(
new java.io.InputStreamReader(con.getInputStream()));) {
return in.readLine();
}
} catch (IOException e) {
log.error("replay fetch data error:{},{},{},{}",
traceId, methodName, nanoTime, args, e);
} finally {
if (con != null) {
con.disconnect();
}
}
return null;
}
}
小结
文中的实现方式都是尽量避开依赖其他三方jar, 在实际项目中, 需要根据框架的不同做调整, 但整体思路是一样的. 另外需要说明的一点是, 这里因为数据并没有真实写入到中间件和服务中, 如果在此之上又添加其他逻辑, 是无法通过测试的. 这个需要在回归测试的时候做好判断.