这个 Redis 链接池的新监控方式针不戳~我再加一点佐料

image

Lettuce 是一个 Redis 链接池,和 Jedis 不同的是,Lettuce 是主要基于 Netty 以及 ProjectReactor 实现的异步链接池。因为基于 ProjectReactor,因此能够直接用于 spring-webflux 的异步项目,固然,也提供了同步接口。java

在咱们的微服务项目中,使用了 Spring Boot 以及 Spring Cloud。而且使用了 spring-data-redis 做为链接 Redis 的库。而且链接池使用的是 Lettuce。同时,咱们线上的 JDK 是 OpenJDK 11 LTS 版本,而且每一个进程都打开了 JFR 记录。关于 JFR,能够参考这个系列:JFR 全解git

在 Lettuce 6.1 以后,Lettuce 也引入了基于 JFR 的监控事件。参考:events.flight-recordergithub

image

1. Redis 链接相关事件web

  • ConnectEvent:当尝试与 Redis 创建链接以前,就会发出这个事件。
  • ConnectedEvent链接创建的时候会发出的事件,包含创建链接的远程 IP 与端口以及使用的 Redis URI 等信息,对应 Netty 其实就是 ChannelHandler 中的 channelActive 回调一开始就会发出的事件。
  • ConnectionActivatedEvent:在完成 Redis 链接一系列初始化操做以后(例如 SSL 握手,发送 PING 心跳命令等等),这个链接能够用于执行 Redis 命令时发出的事件
  • ConnectionDeactivatedEvent:在没有任何正在处理的命令而且 isOpen() 是 false 的状况下,链接就不是活跃的了,准备要被关闭。这个时候就会发出这个事件。
  • DisconnectedEvent链接真正关闭或者重置时,会发出这个事件。
  • ReconnectAttemptEvent:Lettuce 中的 Redis 链接会被维护为长链接,当链接丢失,会自动重连,须要重连的时候,会发出这个事件。
  • ReconnectFailedEvent:当重连而且失败的时候的时候,会发出这个事件。

2. Redis 集群相关事件redis

  • AskRedirectionEvent:针对 Redis slot 处于迁移状态时会返回 ASK,这时候会发出这个事件。
  • MovedRedirectionEvent:针对 Redis slot 不在当前节点上时会返回 MOVED,这时候会发出这个事件。
  • TopologyRefreshEvent:若是启用了集群拓补刷新的定时任务,在查询集群拓补的时候,就会发出这个事件。可是,这个须要在配置中开启定时检查集群拓补的任务,参考 cluster-topology-refresh
  • ClusterTopologyChangedEvent:当 Lettuce 发现 Redis 集群拓补发生变化的时候,就会发出这个事件。

3. Redis 命令相关事件spring

  • CommandLatencyEvent:Lettuce 会统计每一个命令的响应时间,并定时发出这个事件。这个也是须要手动配置开启的,后面会提到如何开启。
  • CommandStartedEvent开始执行某一指令的时候会发出这个事件。
  • CommandSucceededEvent指令执行成功的时候会发出这个事件。
  • CommandFailedEvent指令执行失败的时候会发出这个事件。

image

Lettuce 的监控是基于事件分发与监听机制的设计,其核心接口是 EventBus:编程

EventBus.java浏览器

public interface EventBus {
    // 获取 Flux,经过 Flux 订阅,能够容许多个订阅者
    Flux<Event> get();
    // 发布事件
    void publish(Event event);
}

其默认实现为 DefaultEventBus缓存

public class DefaultEventBus implements EventBus {
    private final DirectProcessor<Event> bus;
    private final FluxSink<Event> sink;
    private final Scheduler scheduler;
    private final EventRecorder recorder = EventRecorder.getInstance();

    public DefaultEventBus(Scheduler scheduler) {
        this.bus = DirectProcessor.create();
        this.sink = bus.sink();
        this.scheduler = scheduler;
    }

    @Override
    public Flux<Event> get() {
        //若是消费不过来直接丢弃
        return bus.onBackpressureDrop().publishOn(scheduler);
    }

    @Override
    public void publish(Event event) {
        //调用 recorder 记录
        recorder.record(event);
        //调用 recorder 记录以后,再发布事件
        sink.next(event);
    }
}

在默认实现中,咱们发现发布一个事件首先要调用 recorder 记录,以后再放入 FluxSink 中进行事件发布。目前 recorder 有实际做用的实现即基于 JFR 的 JfrEventRecorder.查看源码:服务器

JfrEventRecorder

public void record(Event event) {
    LettuceAssert.notNull(event, "Event must not be null");
    //使用 Event 建立对应的 JFR Event,以后直接 commit,即提交这个 JFR 事件到 JVM 的 JFR 记录中
    jdk.jfr.Event jfrEvent = createEvent(event);
    if (jfrEvent != null) {
        jfrEvent.commit();
    }
}

private jdk.jfr.Event createEvent(Event event) {
    try {
        //获取构造器,若是构造器是 Object 的构造器,表明没有找到这个 Event 对应的 JFR Event 的构造器
        Constructor<?> constructor = getEventConstructor(event);
        if (constructor.getDeclaringClass() == Object.class) {
            return null;
        }
        //使用构造器建立 JFR Event
        return (jdk.jfr.Event) constructor.newInstance(event);
    } catch (ReflectiveOperationException e) {
        throw new IllegalStateException(e);
    }
}

//Event 对应的 JFR Event 构造器缓存
private final Map<Class<?>, Constructor<?>> constructorMap = new HashMap<>();

private Constructor<?> getEventConstructor(Event event) throws NoSuchMethodException {
    Constructor<?> constructor;
    //简而言之,就是查看缓存 Map 中是否存在这个 class 对应的 JFR Event 构造器,有则返回,没有则尝试发现
    synchronized (constructorMap) {
        constructor = constructorMap.get(event.getClass());
    }
    if (constructor == null) {
    
        //这个发现的方式比较粗暴,直接寻找与当前 Event 的同包路径下的以 Jfr 开头,后面跟着当前 Event 名称的类是否存在
        //若是存在就获取他的第一个构造器(无参构造器),不存在就返回 Object 的构造器
        String jfrClassName = event.getClass().getPackage().getName() + ".Jfr" + event.getClass().getSimpleName();

        Class<?> eventClass = LettuceClassUtils.findClass(jfrClassName);

        if (eventClass == null) {
            constructor = Object.class.getConstructor();
        } else {
            constructor = eventClass.getDeclaredConstructors()[0];
            constructor.setAccessible(true);
        }

        synchronized (constructorMap) {
            constructorMap.put(event.getClass(), constructor);
        }
    }

    return constructor;
}

发现这块代码并非很好,每次读都要获取锁,因此我作了点修改并提了一个 Pull Request:reformat getEventConstructor for JfrEventRecorder not to synchronize for each read

由此咱们能够知道,一个 Event 是否有对应的 JFR Event 经过查看是否有同路径的以 Jfr 开头后面跟着本身名字的类便可。目前能够发现:

  • io.lettuce.core.event.connection 包:
    • ConnectedEvent -> JfrConnectedEvent
    • ConnectEvent -> JfrConnectedEvent
    • ConnectionActivatedEvent -> JfrConnectionActivatedEvent
    • ConnectionCreatedEvent -> JfrConnectionCreatedEvent
    • ConnectionDeactivatedEvent -> JfrConnectionDeactivatedEvent
    • DisconnectedEvent -> JfrDisconnectedEvent
    • ReconnectAttemptEvent -> JfrReconnectAttemptEvent
    • ReconnectFailedEvent -> JfrReconnectFailedEvent
  • io.lettuce.core.cluster.event 包:
    • AskRedirectionEvent -> JfrAskRedirectionEvent
    • ClusterTopologyChangedEvent -> JfrClusterTopologyChangedEvent
    • MovedRedirectionEvent -> JfrMovedRedirectionEvent
    • AskRedirectionEvent -> JfrTopologyRefreshEvent
  • io.lettuce.core.event.command 包:
    • CommandStartedEvent -> 无
    • CommandSucceededEvent -> 无
    • CommandFailedEvent -> 无
  • io.lettuce.core.event.metrics 包:、
    • CommandLatencyEvent -> 无

咱们能够看到,当前针对指令,并无 JFR 监控,可是对于咱们来讲,指令监控反而是最重要的。咱们考虑针对指令相关事件添加 JFR 对应事件

image

若是对 io.lettuce.core.event.command 包下的指令事件生成对应的 JFR,那么这个事件数量有点太多了(咱们一个应用实例可能每秒执行好几十万个 Redis 指令)。因此咱们倾向于针对 CommandLatencyEvent 添加 JFR 事件。

CommandLatencyEvent 包含一个 Map:

private Map<CommandLatencyId, CommandMetrics> latencies;

其中 CommandLatencyId 包含 Redis 链接信息,以及执行的命令。CommandMetrics 即时间统计,包含:

  • 收到 Redis 服务器响应的时间指标,经过这个判断是不是 Redis 服务器响应慢。
  • 处理完 Redis 服务器响应的时间指标,可能因为应用实例过忙致使响应一直没有处理完,经过这个与收到 Redis 服务器响应的时间指标对比判断应用处理花的时间。

这两个指标都包含以下信息:

  • 最短期
  • 最长时间
  • 百分位时间,默认是前 50%,前 90%,前 95%,前 99%,前 99.9%,对应源码:MicrometerOptions: public static final double[] DEFAULT_TARGET_PERCENTILES = new double[] { 0.50, 0.90, 0.95, 0.99, 0.999 };

咱们想要实现针对每一个不一样 Redis 服务器每一个命令都能经过 JFR 查看一段时间内响应时间指标的统计,能够这样实现:

package io.lettuce.core.event.metrics;

import jdk.jfr.Category;
import jdk.jfr.Event;
import jdk.jfr.Label;
import jdk.jfr.StackTrace;

@Category({ "Lettuce", "Command Events" })
@Label("Command Latency Trigger")
@StackTrace(false)
public class JfrCommandLatencyEvent extends Event {
    private final int size;

    public JfrCommandLatencyEvent(CommandLatencyEvent commandLatencyEvent) {
        this.size = commandLatencyEvent.getLatencies().size();
        commandLatencyEvent.getLatencies().forEach((commandLatencyId, commandMetrics) -> {
            JfrCommandLatency jfrCommandLatency = new JfrCommandLatency(commandLatencyId, commandMetrics);
            jfrCommandLatency.commit();
        });
    }
}
package io.lettuce.core.event.metrics;

import io.lettuce.core.metrics.CommandLatencyId;
import io.lettuce.core.metrics.CommandMetrics;
import jdk.jfr.Category;
import jdk.jfr.Event;
import jdk.jfr.Label;
import jdk.jfr.StackTrace;

import java.util.concurrent.TimeUnit;

@Category({ "Lettuce", "Command Events" })
@Label("Command Latency")
@StackTrace(false)
public class JfrCommandLatency extends Event {
    private final String remoteAddress;
    private final String commandType;
    private final long count;
    private final TimeUnit timeUnit;
    private final long firstResponseMin;
    private final long firstResponseMax;
    private final String firstResponsePercentiles;
    private final long completionResponseMin;
    private final long completionResponseMax;
    private final String completionResponsePercentiles;

    public JfrCommandLatency(CommandLatencyId commandLatencyId, CommandMetrics commandMetrics) {
        this.remoteAddress = commandLatencyId.remoteAddress().toString();
        this.commandType = commandLatencyId.commandType().toString();
        this.count = commandMetrics.getCount();
        this.timeUnit = commandMetrics.getTimeUnit();
        this.firstResponseMin = commandMetrics.getFirstResponse().getMin();
        this.firstResponseMax = commandMetrics.getFirstResponse().getMax();
        this.firstResponsePercentiles = commandMetrics.getFirstResponse().getPercentiles().toString();
        this.completionResponseMin = commandMetrics.getCompletion().getMin();
        this.completionResponseMax = commandMetrics.getCompletion().getMax();
        this.completionResponsePercentiles = commandMetrics.getCompletion().getPercentiles().toString();
    }
}

这样,咱们就能够这样分析这些事件:

首先在事件浏览器中,选择 Lettuce -> Command Events -> Command Latency,右键使用事件建立新页:

image

在建立的事件页中,按照 commandType 分组,而且将感兴趣的指标显示到图表中:

image

image

针对这些修改,我也向社区提了一个 Pull Requestfix #1820 add JFR Event for Command Latency

image

在 Spring Boot 中(即增长了 spring-boot-starter-redis 依赖),咱们须要手动打开 CommandLatencyEvent 的采集:

@Configuration(proxyBeanMethods = false)
@Import({LettuceConfiguration.class})
//须要强制在 RedisAutoConfiguration 进行自动装载
@AutoConfigureBefore(RedisAutoConfiguration.class)
public class LettuceAutoConfiguration {
}
import io.lettuce.core.event.DefaultEventPublisherOptions;
import io.lettuce.core.metrics.DefaultCommandLatencyCollector;
import io.lettuce.core.metrics.DefaultCommandLatencyCollectorOptions;
import io.lettuce.core.resource.DefaultClientResources;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.Duration;

@Configuration(proxyBeanMethods = false)
public class LettuceConfiguration {
    /**
     * 每 10s 采集一次命令统计
     * @return
     */
    @Bean
    public DefaultClientResources getDefaultClientResources() {
        DefaultClientResources build = DefaultClientResources.builder()
                .commandLatencyRecorder(
                        new DefaultCommandLatencyCollector(
                                //开启 CommandLatency 事件采集,而且配置每次采集后都清空数据
                                DefaultCommandLatencyCollectorOptions.builder().enable().resetLatenciesAfterEvent(true).build()
                        )
                )
                .commandLatencyPublisherOptions(
                        //每 10s 采集一次命令统计
                        DefaultEventPublisherOptions.builder().eventEmitInterval(Duration.ofSeconds(10)).build()
                ).build();
        return build;
    }
}

微信搜索“个人编程喵”关注公众号,每日一刷,轻松提高技术,斩获各类offer

相关文章
相关标签/搜索