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
- 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:指令執行失敗的時候會發出這個事件。
Lettuce 的監控是基於事件分發與監聽機制的設計,其核心接口是 EventBus
:編程
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 即時間統計,包含:
- 收到 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,右鍵使用事件建立新頁:
在建立的事件頁中,按照 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: