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
1. Redis 链接相关事件:web
ChannelHandler
中的 channelActive
回调一开始就会发出的事件。isOpen()
是 false 的状况下,链接就不是活跃的了,准备要被关闭。这个时候就会发出这个事件。2. Redis 集群相关事件:redis
3. Redis 命令相关事件:spring
Lettuce 的监控是基于事件分发与监听机制的设计,其核心接口是 EventBus
:编程
EventBus.java
segmentfault
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
.查看源码:缓存
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 对应事件
若是对 io.lettuce.core.event.command
包下的指令事件生成对应的 JFR,那么这个事件数量有点太多了(咱们一个应用实例可能每秒执行好几十万个 Redis 指令)。因此咱们倾向于针对 CommandLatencyEvent 添加 JFR 事件。
CommandLatencyEvent 包含一个 Map:
private Map<CommandLatencyId, CommandMetrics> latencies;
其中 CommandLatencyId 包含 Redis 链接信息,以及执行的命令。CommandMetrics 即时间统计,包含:
这两个指标都包含以下信息:
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,右键使用事件建立新页:
在建立的事件页中,按照 commandType 分组,而且将感兴趣的指标显示到图表中:
针对这些修改,我也向社区提了一个 Pull Request:fix #1820 add JFR Event for Command Latency
在 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: