一次 Jedis 引數異常引發服務雪崩
Redis 作為網際網路業務的遠端快取工具而被大面積使用,作為訪問客戶端的 Jedis 同樣被大面積使用。本文主要分析 Redis3.x 版本叢集模式發生主從切換場景下 Jedis 的引數設定不合理引發服務雪崩的過程。
一、背景介紹
二、故障現場記錄
-
訊息堆積告警
【MQ-訊息堆積告警】
告警時間:2022-11-29 23:50:21
檢測規則: 訊息堆積閾值:-》異常( > 100000)
告警服務:xxx-anti-addiction
告警叢集:xx公共
告警物件:xxx-login-event-exchange/xxx-login-event-queue
異常物件(當前值): 159412說明:
2022-11-29 23:50:21收到一條RMQ訊息堆積的告警,正常情況下服務是不會有這類異常告警,出於警覺性開始進入系統排查過程。
排查的思路基本圍繞系統相關的指標:系統的請求量,響應時間,下游服務的響應時間,執行緒數等指標。
說明:
排查系統監控之後發現在故障發生時段服務整體的請求量有大幅下跌,響應的介面的平均耗時接近1分鐘。
服務整體出於雪崩狀態,請求耗時暴漲導致服務不可用,進而導致請求量下跌。
說明:
排查系統對應的執行緒數,發現在故障期間處於wait的執行緒數大量增加。
說明:
事後運維同學反饋在故障時間點Redis叢集主從切換,整體時間和故障時間較吻合。
三、故障過程分析
在進行故障的過程分析之前,首先需要對目前的現象進行分析,需要回答下面幾個問題:
-
介面響應耗時增加為何會引起請求量的陡增?
-
Redis主從切換期間大部分的耗時為啥是2s?
-
介面的平均響應時間為啥接近60s?
3.1 流量陡降
說明:
透過nginx的日誌可以看出存在大量的connection timed out的報錯,可以歸因為由於後端服務的響應時間過程導致nginx層和下游服務之間的讀取超時。
由於大量的讀取超時導致nginx判斷為後端的服務不可用,進而觸發了no live upstreams的報錯,ng無法轉發到合適的後端服務。
透過nginx的日誌可以將問題歸因到後端服務異常導致整體請求量下跌。
3.2 耗時問題
說明:
透過報錯日誌定位到Jedis在獲取連線的過程中丟擲了connect timed out的異常。
透過定位Jedis的原始碼發現預設的設定連線超時時間 DEFAULT_TIMEOUT = 2000。
<redis-cluster name="redisCluster" timeout="3000" maxRedirections="6"> // 最大重試次數為6 <properties> <property name="maxTotal" value="20" /> <property name="maxIdle" value="20" /> <property name="minIdle" value="2" /> </properties></redis-cluster>
說明:
透過報錯日誌定位Jedis執行了6次重試,每次重試耗時參考設定連線超時預設時長2s,單次請求約耗時12s。
排查部分對外介面,發現一次請求內部總共訪問的Redis次數有5次,那麼整體的響應時間會達到1m=60s。
結合報錯日誌和監控指標,判定服務的雪崩和Jedis的連線重試機制有關,需要從Jedis的原始碼進一步進行分析。
四、Jedis 執行流程
4.1 流程解析
說明:
Jedis處理Redis的命令請求如上圖所示,整體在初始化連線的基礎上根據計算的slot槽位獲取連線後傳送命令進行執行。
在獲取連線失敗或命令傳送失敗的場景下觸發異常重試,重新執行一次命令。
異常重試流程中省略了重新獲取Redis叢集分佈的邏輯,避免複雜化整體流程。
4.2 原始碼解析
(1)整體流程
public class JedisCluster extends BinaryJedisCluster implements JedisCommands,
MultiKeyJedisClusterCommands, JedisClusterScriptingCommands {
@Override
public String set(final String key, final String value, final String nxxx, final String expx,
final long time) {
return new JedisClusterCommand<String>(connectionHandler, maxAttempts) {
@Override
public String execute(Jedis connection) {
// 真正傳送命令的邏輯
return connection.set(key, value, nxxx, expx, time);
}
}.run(key); // 透過run觸發命令的執行
}
}
public abstract class JedisClusterCommand<T> {
public abstract T execute(Jedis connection);
public T run(String key) {
// 執行帶有重試機制的方法
return runWithRetries(SafeEncoder.encode(key), this.maxAttempts, false, false);
}
}
public abstract class JedisClusterCommand<T> {
private T runWithRetries(byte[] key, int attempts, boolean tryRandomNode, boolean asking) {
Jedis connection = null;
try {
if (asking) {
// 省略相關的程式碼邏輯
} else {
if (tryRandomNode) {
connection = connectionHandler.getConnection();
} else {
// 1、嘗試獲取連線
connection = connectionHandler.getConnectionFromSlot(JedisClusterCRC16.getSlot(key));
}
}
// 2、執行JedisClusterCommand封裝的execute命令
return execute(connection);
} catch (JedisNoReachableClusterNodeException jnrcne) {
throw jnrcne;
} catch (JedisConnectionException jce) {
// 省略程式碼
} finally {
releaseConnection(connection);
}
}
}
說明:
以
JedisCluster
執行
set
命令為例,封裝成
JedisClusterCommand
物件透過run觸發
runWithRetries
進而執行set命令的
execute
方法。
runWithRetries
方法封裝了具體的重試邏輯,內部透過
connectionHandler.getConnectionFromSlot
獲取對應的Redis節點的連線。
(2)計算槽位
public final class JedisClusterCRC16 {
public static int getSlot(byte[] key) {
int s = -1;
int e = -1;
boolean sFound = false;
for (int i = 0; i < key.length; i++) {
if (key[i] == '{' && !sFound) {
s = i;
sFound = true;
}
if (key[i] == '}' && sFound) {
e = i;
break;
}
}
if (s > -1 && e > -1 && e != s + 1) {
return getCRC16(key, s + 1, e) & (16384 - 1);
}
return getCRC16(key) & (16384 - 1);
}
}
(3)連線獲取
public class JedisSlotBasedConnectionHandler extends JedisClusterConnectionHandler {
@Override
public Jedis getConnectionFromSlot(int slot) {
JedisPool connectionPool = cache.getSlotPool(slot);
if (connectionPool != null) {
// 嘗試獲取連線
return connectionPool.getResource();
} else {
renewSlotCache();
connectionPool = cache.getSlotPool(slot);
if (connectionPool != null) {
return connectionPool.getResource();
} else {
return getConnection();
}
}
}
}
class JedisFactory implements PooledObjectFactory<Jedis> {
@Override
public PooledObject<Jedis> makeObject() throws Exception {
// 1、建立Jedis連線
final HostAndPort hostAndPort = this.hostAndPort.get();
final Jedis jedis = new Jedis(hostAndPort.getHost(), hostAndPort.getPort(), connectionTimeout,
soTimeout, ssl, sslSocketFactory, sslParameters, hostnameVerifier);
try {
// 2、嘗試進行連線
jedis.connect();
} catch (JedisException je) {
jedis.close();
throw je;
}
return new DefaultPooledObject<Jedis>(jedis);
}
}
public class Connection implements Closeable {
public void connect() {
if (!isConnected()) {
try {
socket = new Socket();
socket.setReuseAddress(true);
socket.setKeepAlive(true); // Will monitor the TCP connection is
socket.setTcpNoDelay(true); // Socket buffer Whetherclosed, to
socket.setSoLinger(true, 0); // Control calls close () method,
// 1、設定連線超時時間 DEFAULT_TIMEOUT = 2000;
socket.connect(new InetSocketAddress(host, port), connectionTimeout);
// 2、設定讀取超時時間
socket.setSoTimeout(soTimeout);
outputStream = new RedisOutputStream(socket.getOutputStream());
inputStream = new RedisInputStream(socket.getInputStream());
} catch (IOException ex) {
broken = true;
throw new JedisConnectionException(ex);
}
}
}
}
說明:
Jedis透過connectionPool維護和Redis的連線資訊,在可複用的連線不夠的場景下會觸發連線的建立和獲取。建立連線物件透過封裝成Jedis物件並透過connect進行連線,在Connection的connect的過程中設定連線超時connectionTimeout和讀取超時soTimeout。
建立連線過程中如果異常會丟擲JedisConnectionException異常,注意這個異常會在後續的分析中多次出現。
(4)傳送命令
public class Connection implements Closeable {
protected Connection sendCommand(final Command cmd, final byte[]... args) {
try {
// 1、必要時嘗試連線
connect();
// 2、傳送命令
Protocol.sendCommand(outputStream, cmd, args);
pipelinedCommands++;
return this;
} catch (JedisConnectionException ex) {
broken = true;
throw ex;
}
}
private static void sendCommand(final RedisOutputStream os, final byte[] command,
final byte[]... args) {
try {
// 按照redis的命令格式傳送資料
os.write(ASTERISK_BYTE);
os.writeIntCrLf(args.length + 1);
os.write(DOLLAR_BYTE);
os.writeIntCrLf(command.length);
os.write(command);
os.writeCrLf();
for (final byte[] arg : args) {
os.write(DOLLAR_BYTE);
os.writeIntCrLf(arg.length);
os.write(arg);
os.writeCrLf();
}
} catch (IOException e) {
throw new JedisConnectionException(e);
}
}
}
說明:
Jedis透過sendCommand向Redis傳送Redis格式的命令。
傳送過程中會執行connect連線動作,邏輯和獲取連線時的connect過程一致。
傳送命令異常會丟擲JedisConnectionException 的異常資訊。
(5)重試機制
public abstract class JedisClusterCommand<T> {
private T runWithRetries(byte[] key, int attempts, boolean tryRandomNode, boolean asking) {
Jedis connection = null;
try {
if (asking) {
} else {
if (tryRandomNode) {
connection = connectionHandler.getConnection();
} else {
// 1、嘗試獲取連線
connection = connectionHandler.getConnectionFromSlot(JedisClusterCRC16.getSlot(key));
}
}
// 2、透過連線執行命令
return execute(connection);
} catch (JedisNoReachableClusterNodeException jnrcne) {
throw jnrcne;
} catch (JedisConnectionException jce) {
releaseConnection(connection);
connection = null;
// 4、重試到最後一次丟擲異常
if (attempts <= 1) {
this.connectionHandler.renewSlotCache();
throw jce;
}
// 3、進行第一輪重試
return runWithRetries(key, attempts - 1, tryRandomNode, asking);
} finally {
releaseConnection(connection);
}
}
}
說明:
Jedis執行Redis的命令時按照先獲取connection後透過connection執行命令的順序。 在獲取connection和透過connection執行命令的過程中如果發生異常會進行重試且在達到最大重試次數後丟擲異常。 以 attempts=5
為例,如果在獲取connection
過程中發生異常,那麼最多重試5次後丟擲異常。
-
maxAttempts: 出現異常最大重試次數。
-
connectionTimeout: 表示連線超時時間。
-
soTimeout: 讀取資料超時時間。
五、總結
本文轉自公眾號 vivo網際網路技術
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70013542/viewspace-2990613/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- JVM實戰調優(空格引發的服務異常)JVM
- 一次訊號量引發的tomcat異常退出Tomcat
- python自定義異常,使用raise引發異常PythonAI
- MongoDB 異常當機與引數cacheSizeGBMongoDB
- 一次線上Redis類轉換異常排查引發的思考Redis
- Kotlin藝術探索之引數和異常Kotlin
- 【高併發】由InterruptedException異常引發的思考Exception
- 一場 Kafka CRC 異常引發的血案Kafka
- python生成器呼叫方法引發異常Python
- 線上ES叢集引數配置引起的業務異常案例分析
- 深度覆盤-重啟 etcd 引發的異常
- Spring-RestTemplate之urlencode引數解析異常全程分析SpringREST
- nginx 常見引數以及重定向引數配置Nginx
- Bulk 異常引發的 Elasticsearch 記憶體洩漏Elasticsearch記憶體
- 【PARANETERS】Oracle異常恢復相關的隱含引數Oracle
- Spring Boot統一異常處理以及引數校驗Spring Boot
- jedis異常:Could not get a resource from the pool
- linux sshd服務異常Linux
- DNS伺服器故障引發流量異常問題-VeCloudDNS伺服器Cloud
- MySQL:MGR修改max_binlog_cache_size引數導致異常MySql
- Dubbo服務如何優雅的校驗引數
- JVM常見引數設定JVM
- [譯] Ruby 2.6 Kernel 的system 方法增加是否丟擲異常引數。
- [譯] Ruby 2.6 增加了 Integer 和 Float 方法的異常引數
- 記錄一次事務異常
- Android開發:系統程式中使用Webview引發異常的處理AndroidWebView
- MySQL服務端innodb_buffer_pool_size配置引數MySql服務端
- Loadrunner+引數化檔案編碼格式+獲取請求報文發生異常
- SpringBoot 實戰 (十五) | 服務端引數校驗之一Spring Boot服務端
- 一次定時任務配置錯誤引發的思考
- 引導過程與服務控制
- 記一次FreeBSD系統中mysql服務異常的排查過程MySql
- 一次fork引發的慘案!
- 一次Toast元件引發的思考AST元件
- 拼多多物流服務異常率多少正常?怎麼降低異常?
- 什麼是請求引數、表單引數、url引數、header引數、Cookie引數?一文講懂HeaderCookie
- MySQL常見的配置引數概覽MySql
- Weblogic接收SIGQUIT資訊號引發服務中止問題WebUI