[源码分析] OpenTracing之跟踪Redis

0x00 摘要

本文将通过研究OpenTracing的Redis埋点插件来进一步深入理解OpenTracing。

0x01 总体逻辑

1.1 相关概念

Tracer是用来管理Span的统筹者,负责创建span和传播span。它表示从头到尾的一个请求的调用链,是一次完整的跟踪,从请求到服务器开始,服务器返回response结束,跟踪每次rpc调用的耗时。它的标识符是“traceID”。

Span是一个更小的单位,表示一个RPC调用过程。一个“trace”包含有许多跨度(span),每个跨度捕获调用链内的一个工作单元(系统或服务节点),并由“spanId”标识。 每个跨度具有一个父跨度,并且一个“trace”的所有跨度形成有向无环图(DAG)。

1.2 埋点插件

对一个应用的跟踪要关注的无非就是 客户端——>web 层——>rpc 服务——>dao 后端存储、cache 缓存、消息队列 mq 等这些基础组件。OpenTracing 插件的作用实际上也就是对不同组件进行埋点,以便基于这些组件采集应用的链路数据。

不同组件有不同的应用场景和扩展点,因此针对不同的框架,需要开发对应的OpenTracing API 插件用来实现自动埋点。

对于Redis来说各种插件更是层出不穷,所以OpenTracing 对与 Redis 各种插件也做了不同处理,比如 Jedis,Redisson,Spring Data Redis 2.x。本文主要是以 Redisson 为例说明,最后用spring-cloud-redis进行补充对照**。

1.3 总体逻辑

总体思路是使用代理模式。因为 Redis 并没有提供像 Servlet 那样的过滤器或者拦截器,所以 Redis OpenTracing 插件没有进行常规埋点,而是通过组合的方式自定义若干代理类,比如 TracingRedissonClient 和 TracingRList .....。

  • TracingRedissonClient 代理了 Redis Client。
  • TracingRList 代理了Redis List 数据结构。
  • 还有其他类代理其他Redis数据结构,比如TracingRMap。

这些代理类将具体完成Tracing 功能。比如代理类 TracingRedissonClient 包含了两个成员变量:

  • private final RedissonClient redissonClient; 是真正的 Redis Client。
  • private final TracingRedissonHelper tracingRedissonHelper; 是具体针对 Redission 的Tracing 功能类,比如构建Span。

最后各种代理对 Redis 进行拦截:

  • 在执行具体的连接操作之前创建相关的 Span。
  • 在操作结束之后结束 Span,并进行上报。

具体可以见下图

+--------------------------+ +-------------------------+ +-------------------------+
| TracingRedissonClient | | TracingRMap | | TracingRList |
| +----------------------+ | | +---------------------+ | | +---------------------+ |
| | RedissonClient | | | | RMap | | | | RList | | ....
| | | | | | | | | | | |
| | TracingRedissonHelper| | | |TracingRedissonHelper| | | |TracingRedissonHelper| |
| +----------------------+ | | +---------------------+ | | +---------------------+ |
+--------------------------+ +-------------------------+ +-------------------------+
| | |
| | |
| | |
| | |
| v |
| +---------------+-----------------+ |
+-----------> | TracingRedissonHelper | <--------+
| +-----------------------------+ |
| | Tracer +-----+
| +-----------------------------+ | |
+---------------------------------+ |
|
+---------------------------------+ |
| TracingConfiguration | |
| +----------------------------+ | |
| | Tracer <-------+
| +----------------------------| |
+---------------------------------+

下图是为了手机观看。

0x02 示例代码

我们使用代码自带的test来做说明。我们可以看到有两个代理类 TracingRedissonClientTracingRList

  • beforeClass 起到了系统启动的作用。

    • 首先定义了一个tracer(这里是MockTracer,真正使用时候会用到其他Tracer)。
    • 然后使用这个Tracer来构建一个代理类 TracingRedissonClient
  • 后续各种测试操作都是使用这个client在进行Redis操作。
    • 会通过代理类 TracingRedissonClient 得到一个 org.redisson.api.RList 以备后续操作。这个 RList 实际是OpenTracing 进行修改的另一个代理类 TracingRList
    • 会对这个 TracingRList 进行操作 :list.add("key");
    • 针对 Redisson 的异步操作,也进行了操作测试。

具体代码如下:

public class TracingRedissonTest {
private static final MockTracer tracer = new MockTracer();
private static RedisServer redisServer;
private static RedissonClient client; @BeforeClass
public static void beforeClass() {
redisServer = RedisServer.builder().setting("bind 127.0.0.1").build();
redisServer.start(); Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379"); client = new TracingRedissonClient(Redisson.create(config),
new TracingConfiguration.Builder(tracer).build());
} @Test
public void test_list() {
RList<Object> list = client.getList("list"); list.add("key");
assertTrue(list.contains("key")); List<MockSpan> spans = tracer.finishedSpans();
assertEquals(2, spans.size());
checkSpans(spans);
assertNull(tracer.activeSpan());
} @Test
public void test_config_span_name() throws Exception {
...... final MockSpan parent = tracer.buildSpan("test").start();
try (Scope ignore = tracer.activateSpan(parent)) {
RMap<String, String> map = customClient.getMap("map_config_span_name");
map.getAsync("key").get(15, TimeUnit.SECONDS);
}
parent.finish(); ......
}
}

0x03 Redis代理

前面我们提到了对于Redis是使用了代理来完成功能,下面我们具体来讲解。

3.1 Client 代理类

TracingRedissonClient 实现了 Redis Client 代理功能,其包含两个成员变量。

  • RedissonClient redissonClient; 是真正的Redis Client,代理类最终是通过此Client进行Redis操作。
  • TracingRedissonHelper tracingRedissonHelper; 完成了Tracing 功能。

具体在使用中,比如在测试代码会通过 Client 代理类得到一个 TracingRList 以备后续操作(这是另一个代理类)。

  • TracingRList 实现了 org.redisson.api.RList 接口。
  • 在构建 TracingRList 会把 TracingRedissonHelper 作为参数传递进去。
RList<Object> list = client.getList("list");

具体代码如下:

public class TracingRedissonClient implements RedissonClient {
private final RedissonClient redissonClient;
private final TracingRedissonHelper tracingRedissonHelper; public TracingRedissonClient(RedissonClient redissonClient, TracingConfiguration configuration) {
this.redissonClient = redissonClient;
this.tracingRedissonHelper = new TracingRedissonHelper(configuration);
} @Override
public <V> RList<V> getList(String name) {
// 通过代理生成
return new TracingRList<>(redissonClient.getList(name), tracingRedissonHelper);
} // 其他操作
......
}

3.2 List 代理类

TracingRList 是Redis List代理类(Redis插件还有其他代理类,代理其他Redis数据结构)。里面也是两个变量:

  • RList
  • TracingRedissonHelper 完成 Tracing 功能。

在具体 add 函数中:

  • 在执行具体的命令前先通过 tracingRedissonHelper.buildSpan 构建 Span 进行埋点操作。
  • 然后添加 Tag。
  • 最后通过 tracingRedissonHelper.decorate 进行实际 Redis 操作。

具体代码如下:

public class TracingRList<V> extends TracingRExpirable implements RList<V> {
private final RList<V> list;
private final TracingRedissonHelper tracingRedissonHelper; @Override
public boolean add(V element) {
Span span = tracingRedissonHelper.buildSpan("add", list);
span.setTag("element", nullable(element));
return tracingRedissonHelper.decorate(span, () -> list.add(element));
} // 其他操作
.....
}

0x04 Tracing功能类

前面一直在提TracingRedissonHelper是Tracing功能类,下面我们就深入研究下 tracingRedissonHelper.decorate(span, () -> list.add(element)); 做了什么。

4.1 配置类

在初始化 Redis Client时候,生成了 TracingConfiguration。

client = new TracingRedissonClient(Redisson.create(config),
new TracingConfiguration.Builder(tracer).build());

TracingConfiguration 之中就定义了io.opentracing.Tracer以及其他配置项。

具体类定义如下:

public class TracingConfiguration {
static final int DEFAULT_KEYS_MAX_LENGTH = 100;
private final Tracer tracer;
private final boolean traceWithActiveSpanOnly;
private final int keysMaxLength;
private final Function<String, String> spanNameProvider;
private final Map<String, String> extensionTags; // 其他操作
......
}

4.2 Tracing基础功能类

io.opentracing.contrib.redis.common.TracingHelperOpenTracing 通用的 Redis Tracing 功能类,我们看到里面有 Tracer 变量(就是TracingConfiguration之中的Tracer),也有 SpanBuilder 这样的helper 函数。

业务逻辑具体在 decorate 函数中有体现。参数 Supplier

return tracingRedissonHelper.decorate(span, () -> list.add(object));

Supplier 是 JAVA8 提供的接口,这个接口是一个提供者的意思,只有一个get的抽象类,没有默认的方法以及静态的方法。get方法返回一个泛型T,这就是一个创建对象的工厂。

所以decorate的作用在我们这里就是:

  • tracer.scopeManager().activate(span) 来激活当前span。
  • 返回对象,执行Redis操作,我们例子就是 () -> list.add(element)
  • 调用 span.finish(); 完成了结束操作,如果采样就会上报。

测试代码 执行流程图如下:

TracingRList                  TracingHelper
+ +
+---+--+ |
| add | begin |
+---+--+ |
| |
| invoke |
| v
| ----------------> +-------+------+
| | buildSpan |
| <---------------+ +-------+------+
| Return |
| |
+---+-------+ |
|span.setTag| |
+---+-------+ |
| |
| |
| |
| invoke +-------------v-------------------------+
| -----------> |decorate(span, () -> list.add(element))|
| +-------------+-------------------------+
| |
| |
| |
| v begin tracing
| +-------------+----------------------+
| |tracer.scopeManager().activate(span)|
| +-------------+----------------------+
| |
| |
| |
| v Real Redis action
+-----+------------+ <----+ +-----+--------+
| list.add(element)| |supplier.get()|
+-----+------------+ +----> +-----+--------+
| |
| |
| v end tracing
| decorate Return +-----+-------+
| <----------------+ |span.finish()|
| +-------------+
+--+---+
| add | end
+--+---+
|
|
|
v

具体 TracingHelper 代码如下:

public class TracingHelper {

  public static final String COMPONENT_NAME = "java-redis";
public static final String DB_TYPE = "redis";
protected final Tracer tracer;
private final boolean traceWithActiveSpanOnly;
private final Function<String, String> spanNameProvider;
private final int maxKeysLength;
private final Map<String, String> extensionTags; public Span buildSpan(String operationName) {
if (traceWithActiveSpanOnly && tracer.activeSpan() == null) {
return NoopSpan.INSTANCE;
} else {
return builder(operationName).start();
}
} private SpanBuilder builder(String operationName) {
SpanBuilder sb = tracer.buildSpan(spanNameProvider.apply(operationName))
.withTag(Tags.COMPONENT.getKey(), COMPONENT_NAME)
.withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT)
.withTag(Tags.DB_TYPE.getKey(), DB_TYPE);
extensionTags.forEach(sb::withTag);
return sb;
} public <T> T decorate(Span span, Supplier<T> supplier) {
try (Scope ignore = tracer.scopeManager().activate(span)) { // 激活span
return supplier.get(); // 执行Redis操作
} catch (Exception e) {
onError(e, span);
throw e;
} finally {
span.finish();// 完成了结束操作,如果采样就会上报。
}
} // 其他操作
.....
}

4.3 Redission专用Tracing功能类

TracingRedissonHelper 是具体实现了Redission 的 Tracing 功能,主要是针对异步操作。

4.3.1 测试代码

官方测试代码如下

final MockSpan parent = tracer.buildSpan("test").start();
try (Scope ignore = tracer.activateSpan(parent)) {
RMap<String, String> map = customClient.getMap("map_config_span_name");
map.getAsync("key").get(15, TimeUnit.SECONDS); // Redis异步操作
}
parent.finish();

能看到,测试的思路是:

  • 生成一个parent Span
  • 然后使用redis map进行异步操作,getAsync这里会生成一个 client span。
  • parent span结束

具体下面我们会讲解。

4.3.2 TracingRedissonHelper

TracingRedissonHelper 需要针对 Redisson 来进行特殊设置,就是因为Redisson同时还为分布式锁提供了异步执行的相关方法

所以需要对异步操作进行处理。其中:

  • RFuture 是 org.redisson.api 包下面的类,
  • CompletableRFuture 是 io.opentracing.contrib.redis.redisson 包下面的类,针对 RFuture 做了特殊处理。

prepareRFuture函数是执行Redis具体操作的函数,其作用如下:

  • 通过 futureSupplier.get(); 获取redisFuture( prepareRFuture的参数span是之前 getAsync 生成的child span )。
  • 设置 redisFuture 的 whenComplete函数,在whenComplete函数中会对传入的Span进行 finish操作 ,这个span 其实是child span。这样异步的Tracing通过Client Span完成
  • 继续操作,恢复parent span,在redisFuture基础上生成CompletableRFuture,然后继续设置redisFuture.whenComplete,如果redisFuture完成,则调用 customRedisFuture.complete。
  • 返回,外界测试函数会finish parent span

针对官方测试代码,执行流程图如下:

+------------+
| Parent Span|
+-----+------+
|
v
TracingRMap TracingRedissonHelper
+ +
| |
| v
+----+-----+ Invoke +----+------+
| getAsync | +-----------------------> | buildSpan |create Child Span
+----+-----+ +----+------+
| |
| v
| +-----+--------+
| |prepareRFuture|
| +-----+--------+
| |
| Real redis action v
+-----+----------------+ <--------+ +-------+-------------+
|() -> map.getAsync(key| | futureSupplier.get()|
+-----+----------------+ +--------> +-------+-------------+
| Future |
| |
| +-------v---------+
| |setCompleteAction|
| +-------+---------+
| |
| |
| +------v-------+
| | whenComplete |
| +------+-------+
| |
| v
| +------+-------+
| | span.finish()| Child Span
| +------+-------+
| |
| v
| +--------+----------+
| | continueScopeSpan |
| +--------+----------+
| |
| v
| +---------+----------+
| | tracer.activeSpan()|
| +---------+----------+
| | Parent Span
| |
| v
| +-----+--------+
| |activate(span)|
| +-----+--------+
| |
| |
| v
| return +------------+-----------------+
| <-------------------- | customRedisFuture.complete(v)|
| +------------------------------+
|
+----v----------+
|parent.finish()|
+---------------+

具体代码如下:

class TracingRedissonHelper extends TracingHelper {

  TracingRedissonHelper(TracingConfiguration tracingConfiguration) {
super(tracingConfiguration);
} Span buildSpan(String operationName, RObject rObject) {
return buildSpan(operationName).setTag("name", rObject.getName());
} private <T> RFuture<T> continueScopeSpan(RFuture<T> redisFuture) {
Span span = tracer.activeSpan();
CompletableRFuture<T> customRedisFuture = new CompletableRFuture<>(redisFuture);
redisFuture.whenComplete((v, throwable) -> {
try (Scope ignored = tracer.scopeManager().activate(span)) {
if (throwable != null) {
customRedisFuture.completeExceptionally(throwable);
} else {
customRedisFuture.complete(v);
}
}
});
return customRedisFuture;
} private <V> RFuture<V> setCompleteAction(RFuture<V> future, Span span) {
future.whenComplete((v, throwable) -> {
if (throwable != null) {
onError(throwable, span);
}
span.finish();
}); return future;
} <V> RFuture<V> prepareRFuture(Span span, Supplier<RFuture<V>> futureSupplier) {
RFuture<V> future;
try {
future = futureSupplier.get();
} catch (Exception e) {
onError(e, span);
span.finish();
throw e;
} return continueScopeSpan(setCompleteAction(future, span));
}
}

4.4 TracingRMap代理类的异步处理

TracingRMap 实现了 org.redisson.api.RMap。这里就使用了上述的异步相关的功能,比如 getAsync。

所以调用了 prepareRFuture 的功能。

public class TracingRMap<K, V> extends TracingRExpirable implements RMap<K, V> {
private final RMap<K, V> map;
private final TracingRedissonHelper tracingRedissonHelper; @Override
public RFuture<V> getAsync(K key) {
Span span = tracingRedissonHelper.buildSpan("getAsync", map);
span.setTag("key", nullable(key));
return tracingRedissonHelper.prepareRFuture(span, () -> map.getAsync(key));
} // 其他操作
......
}

0x05 spring-cloud-redis

opentracing-spring-cloud-redis-starter 实现了对 spring-cloud-redis 的Tracing功能。

Spring Cloud 埋点实现主要实现原理是利用Spring AOP切片技术抽象埋点行为,比如TraceAsyncAspect 切面类,使用@Around 声明拦截规则,后面的逻辑与手动埋点类似,创建一个span,将业务逻辑包围起来即可。

5.1 Bean

首先,利用注解生成一些Bean,比如。

@Configuration
@AutoConfigureAfter({TracerRegisterAutoConfiguration.class, org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration.class})
@ConditionalOnBean(RedisConnectionFactory.class)
@ConditionalOnProperty(name = "opentracing.spring.cloud.redis.enabled", havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(RedisTracingProperties.class)
public class RedisAutoConfiguration {
@Bean
public RedisAspect openTracingRedisAspect(Tracer tracer, RedisTracingProperties properties) {
return new RedisAspect(tracer, properties);
}
}

5.2 拦截规则

其次,使用 @Around 和 @Pointcut 声明拦截规则。具体是通过一层代理来是实现的拦截。对所有的 Redis connection 都通过 TracingRedisConnection 进行了一层包装

@Aspect
public class RedisAspect { private final Tracer tracer; private final RedisTracingProperties properties; RedisAspect(Tracer tracer, RedisTracingProperties properties) {
this.tracer = tracer;
this.properties = properties;
} @Pointcut("target(org.springframework.data.redis.connection.RedisConnectionFactory)")
public void connectionFactory() {} @Pointcut("execution(org.springframework.data.redis.connection.RedisConnection *.getConnection(..))")
public void getConnection() {} @Pointcut("execution(org.springframework.data.redis.connection.RedisClusterConnection *.getClusterConnection(..))")
public void getClusterConnection() {} @Around("getConnection() && connectionFactory()")
public Object aroundGetConnection(final ProceedingJoinPoint pjp) throws Throwable {
final RedisConnection connection = (RedisConnection) pjp.proceed(); final String prefixOperationName = this.properties.getPrefixOperationName();
final TracingConfiguration tracingConfiguration = new TracingConfiguration.Builder(tracer)
.withSpanNameProvider(RedisSpanNameProvider.PREFIX_OPERATION_NAME(prefixOperationName))
.build(); return new TracingRedisConnection(connection, tracingConfiguration);
} @Around("getClusterConnection() && connectionFactory()")
public Object aroundGetClusterConnection(final ProceedingJoinPoint pjp) throws Throwable {
final RedisClusterConnection clusterConnection = (RedisClusterConnection) pjp.proceed(); final String prefixOperationName = this.properties.getPrefixOperationName();
final TracingConfiguration tracingConfiguration = new TracingConfiguration.Builder(tracer)
.withSpanNameProvider(RedisSpanNameProvider.PREFIX_OPERATION_NAME(prefixOperationName))
.build(); return new TracingRedisClusterConnection(clusterConnection, tracingConfiguration);
}
}

5.2 埋点

在执行具体的命令前后通过自己提供的 API 进行埋点操作,基本上就是:redisTemplate的操作会在每个操作中调用connect做操作,比如 set操作中调用 connection.set(rawKey, rawValue) ,所以就通过 TracingRedisConnection来做一个封装,在做真正connection操作前后进行tracing

流程图如下:

        redisTemplate                             TracingRedisConnection
+ +
| |
| |
v |
+--------------+-----------------+ |
|redisTemplate.opsForValue().set | |
+--------------+-----------------+ |
| |
| |
| |
| |
v |
+-----------+-----------+ |
| RedisTemplate.execute | |
+-----------+-----------+ |
| |
| |
v v
+-------------+-------------+ invoke +-+---+
| DefaultValueOperations.set| +----------------------> | set |
+-------------+-------------+ +-+---+
| |
| |
| v begin tracing
| +---------+--------------+
| | TracingHelper.doInScope|
| +---------+--------------+
| |
| v
| +---+-----+
| |buildSpan|
| +---+-----+
| |
| v
| +---------+-----------+
| |activateAndCloseSpan |
| +---------+-----------+
| |
| |
v Real redis action |
+--------------+-------------------+ <----------------- |
| () -> connection.set(key, value) | |
+--------------+-------------------+ +-----------------> |
| |
| | end tracing
| return +------v--------+
| <--------------------------------+ |span.finish(); |
| +---------------+
|
|
v

代码如下:

public class TracingRedisConnection implements RedisConnection {
private final RedisConnection connection;
private final TracingConfiguration tracingConfiguration;
private final TracingHelper helper; public TracingRedisConnection(RedisConnection connection,
TracingConfiguration tracingConfiguration) {
this.connection = connection;
this.tracingConfiguration = tracingConfiguration;
this.helper = new TracingHelper(tracingConfiguration);
} // 在 span 的生命周期内执行具体命令
@Override
public Object execute(String command, byte[]... args) {
// 执行命令
return helper.doInScope(command, () -> connection.execute(command, args));
} // 其他操作
.....
}

具体Span是在TracingHelper中完成。

public class TracingHelper {
public static final String COMPONENT_NAME = "java-redis";
public static final String DB_TYPE = "redis";
protected final Tracer tracer;
private final boolean traceWithActiveSpanOnly;
private final Function<String, String> spanNameProvider;
private final int maxKeysLength;
private final Map<String, String> extensionTags; public <T> T doInScope(String command, Supplier<T> supplier) {
Span span = this.buildSpan(command);
return this.activateAndCloseSpan(span, supplier);
} // 其他操作
.....
}

0xFF 参考

分布式链路组件 SOFATracer 埋点机制解析

蚂蚁金服开源分布式链路跟踪组件 SOFATracer 埋点机制剖析

https://github.com/opentracing/opentracing-java

https://github.com/opentracing-contrib/java-redis-client

opentracing-spring-cloud-redis-starter

[源码分析] OpenTracing之跟踪Redis的更多相关文章

  1. Redis 内存管理 源码分析

    要想了解redis底层的内存管理是如何进行的,直接看源码绝对是一个很好的选择 下面是我添加了详细注释的源码,需要注意的是,为了便于源码分析,我把redis为了弥补平台差异的那部分代码删了,只需要知道有 ...

  2. Redis 专栏(使用介绍、源码分析、常见问题...)

    一.介绍相关 说Redis : 介绍Redis特性,使用场景,使用Jedis操作Redis等. 二.源码分析 1. 数据结构 Redis源码分析(sds):Redis自己封装的C语言字符串类型. Re ...

  3. redis源码分析之事务Transaction(上)

    这周学习了一下redis事务功能的实现原理,本来是想用一篇文章进行总结的,写完以后发现这块内容比较多,而且多个命令之间又互相依赖,放在一篇文章里一方面篇幅会比较大,另一方面文章组织结构会比较乱,不容易 ...

  4. Redis学习——ae事件处理源码分析

    0. 前言 Redis在封装事件的处理采用了Reactor模式,添加了定时事件的处理.Redis处理事件是单进程单线程的,而经典Reator模式对事件是串行处理的.即如果有一个事件阻塞过久的话会导致整 ...

  5. 手机自动化测试:Appium源码分析之跟踪代码分析九

    手机自动化测试:Appium源码分析之跟踪代码分析九   poptest是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化测试,性能测试,测试工具开发等工作为目标.如果对课程感兴趣,请大家 ...

  6. 手机自动化测试:Appium源码分析之跟踪代码分析八

    手机自动化测试:Appium源码分析之跟踪代码分析八   poptest是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化测试,性能测试,测试工具开发等工作为目标.如果对课程感兴趣,请大家 ...

  7. 手机自动化测试:Appium源码分析之跟踪代码分析七

    手机自动化测试:Appium源码分析之跟踪代码分析七   poptest是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化测试,性能测试,测试工具开发等工作为目标.poptest推出手机自 ...

  8. 手机自动化测试:Appium源码分析之跟踪代码分析六

    手机自动化测试:Appium源码分析之跟踪代码分析六   poptest是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化测试,性能测试,测试工具开发等工作为目标.poptest推出手机自 ...

  9. 手机自动化测试:Appium源码分析之跟踪代码分析五

    手机自动化测试:Appium源码分析之跟踪代码分析五   手机自动化测试是未来很重要的测试技术,作为一名测试人员应该熟练掌握,POPTEST举行手机自动化测试的课程,希望可以训练出优秀的手机测试开发工 ...

随机推荐

  1. 关于 JavaScript 字符串的一个小知识

    说起字符串,我们再熟悉不过了.接触编程的第一个经典任务就是输出字符串:Hello, world.但是你知道 JavaScript 字符串在计算机里是怎么表示的吗? 最简单直观但不太准确的的理解就是,字 ...

  2. Pytorch_第十篇_卷积神经网络(CNN)概述

    卷积神经网络(CNN)概述 Introduce 卷积神经网络(convolutional neural networks),简称CNN.卷积神经网络相比于人工神经网络而言更适合于图像识别.语音识别等任 ...

  3. Ng-Matero V10 正式发布!

    Angular v10 在六月下旬就悄无声息的发布了,虽然 v9 的发布延期了两个月,但是 v10 并没有受影响,仍然如期而至. 相比 v9 的重量级发布,v10 并没有颠覆性的变化,主要还是 bug ...

  4. 1.MongoDB 2.7主从复制(master &ndash;> slave)环境基于时间点的恢复

    (一)MongoDB恢复概述 对于任何类型的数据库,如果要将数据库恢复到过去的任意时间点,否需要有过去某个时间点的全备+全备之后的重做日志,MongoDB也不例外.使用全备将数据库恢复到固定时刻,然后 ...

  5. Android studio 运行程序出现两个apk的解决方案

    保留主项目中的 intent-filter,将其他module中 AndroidManifest.xml文件的intent-filter的内容删除掉即可. 感恩:https://blog.csdn.n ...

  6. Jmeter系列(46)- Jmeter 中有哪些常用的函数

    如果你想从头学习Jmeter,可以看看这个系列的文章哦 https://www.cnblogs.com/poloyy/category/1746599.html 前言 Jmeter 提供了很多函数 但 ...

  7. 测量定位实践-:C#采集控制

    用Halcon已经完成采集,在在C#中不行. 其实还是比较简单,主要是Halcon封装的太好了. 主要是以下几个算子: *打开 Image Acquisitionopen_framegrabber ( ...

  8. 10.oracle分页

    oracle的分页一共有三种方式 方法一 根据rowid来分 SELECT * FROM EMP WHERE ROWID IN (SELECT RID FROM (SELECT ROWNUM RN, ...

  9. Invalid credentials for 'https://repo.magento.com/packages.json'

    Use your public key as username and private key as password from your magento connect account You ca ...

  10. Linux下安装mysql时报错:FATAL ERROR: please install the following Perl modules before executing ./scripts/mysql_install_db:Data::Dumper

    如题,安装mysql过程中,执行scripts/mysql_install_db --user=mysql命令时报错: FATAL ERROR: please install the followin ...