網站首頁 編程語言 正文
背景
接手了一個系統,該使用了HAProxy + Sentinel +redis方案,該方案在redis發生主從切換后,因為應用層用的是redis的連接池,老連接仍然是連接的redis的“老主節點”。進行寫操作的時候,會拋出異常。
org.springframework.data.redis.RedisSystemException: Error in execution; nested exception is io.lettuce.core.RedisCommandExecutionException: READONLY You can’t write against a read only replica.
該方案如下
redis使用使用哨兵模式進行組網,哨兵負責主節點的故障轉移。
HAProxy作為Redis集群的代理(HAProxy工作臺TCP層),屏蔽底層redis的組網細節,對上層應用來看就是單節點的redis。
從下面的配置可以看到,每次應用層創建新連接時,HAproxy會輪訓所有節點,如果探測到節點為主節點,則代應用層向其發起連接。
defaults
mode tcp
frontend redis
bind *:16382 name redis
default_backend redis
backend redis
option tcp-check
tcp-check connect
tcp-check send AUTH\ xxxxxxxxx\r\n
tcp-check expect string +OK
tcp-check send PING\r\n
tcp-check expect string +PONG
tcp-check send info\ replication\r\n
tcp-check expect string role:master
tcp-check send QUIT\r\n
tcp-check expect string +OK
server redis_6382_1 xx.xx.xx.xx:6382 check inter 1s
server redis_6382_2 xx.xx.xx.xx:6382 check inter 1s
server redis_6382_3 xx.xx.xx.xx:6382 check inter 1s
server redis_6382_4 xx.xx.xx.xx:6382 check inter 1s
該方案的問題
不像http協議連接被設計成無狀態。redis的協議較簡單,且是有狀態的。在執行操作前得先認證(認證是可選的),認證完了后,可以長期保持連接,進行交互,除非客戶端主動斷開連接(不深究,可能服務端設置了tcp參數,長期無響應的連接會被服務端關閉)。
所以jedis設計了連接池,創建出來的連接只要能夠正常訪問,連接每次用完后會被回收到連接池中進行復用。
哨兵模式通過心跳檢測redis主節點是否發生故障,然后進行故障轉移(主從切換)。發生主從切換時,并不一定是redis掛了,有時候可能redis的負債過重,無法及時響應哨兵的PING命令。
這個時候雖然哨兵認為redis掛了,但是應用層的之前建立的連接還是能夠正常訪問的,所以老的連接不會被銷毀。但發生主從切換后,使用老連接進行寫操作時就會導致異常,因為此時老連接連的是“老主節點”,發生主從切換后,“老主節點”變成從節點且只讀。
解釋:應用層的之前建立的連接還是能夠正常訪問
第一,應用層查詢頻率不一定有哨兵的心跳檢測頻率高,所以redis負載過重的時候,部分老連接并沒有發起訪問。
第二,應用層查詢的超時時間設置的比哨兵的心跳檢測超時時間長。即使響應很慢,應用層仍然認為連接是健康的。
問題復現
人工模擬哨兵的主從切換。
- 去掉Sentinel的監控(kill掉Sentinel進程)
- 找一個從節點,執行slaveof no one
- 剩余的節點執行,slaveof 新節點
方案選型
Jedis目前支持哨兵模式了,不過我們系統無法容忍長時間的主備變更信息延時通知到應用層,因此需要審視jedis的源碼,確認集群發生主從切換后,是否能夠快速通知到應用層并清理老連接。
測試代碼如下:
public static void main(String[] args) throws JsonProcessingException {
// SpringApplication.run(DemoApplication.class, args);
RedisTemplate<String,String> redisTemplate = new RedisTemplate();
Set<String> setRedisNode = new HashSet<>();
# 哨兵的IP:Port
setRedisNode.add("xx.xx.xx.xx:26379");
setRedisNode.add("xx.xx.xx.xx:26381");
setRedisNode.add("xx.xx.xx.xx:26382");
setRedisNode.add("xx.xx.xx.xx:26383");
# "mymaster" 對應sentinel.conf中"sentinel monitor mymaster xx.xx.xx.xx 6383 2"
RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration("mymaster", setRedisNode);
JedisPoolConfig config = new JedisPoolConfig();
JedisConnectionFactory connectionFactory = new JedisConnectionFactory(redisSentinelConfiguration,config);
connectionFactory.afterPropertiesSet();
redisTemplate.setConnectionFactory(connectionFactory);
redisTemplate.afterPropertiesSet();
redisTemplate.opsForValue().set("aaa","bbb");
while(true){}
}
跟蹤調用鏈,發現JedisConnectionFactory (afterPropertiesSet方法)在初始化的過程中會做以下動作:
- 向哨兵進程查詢當前主節點
- 使用異步線程監聽哨兵發過來的事件,如果是主從切換事件,則立馬更新主節點,并清理連接池。
# 代碼有刪減
public JedisSentinelPool(String masterName, Set<HostAndPort> sentinels,
final GenericObjectPoolConfig<Jedis> poolConfig, final JedisFactory factory,
final JedisClientConfig sentinelClientConfig) {
# 查詢當前主節點
HostAndPort master = initSentinels(sentinels, masterName);
# 設置當前主節點
initMaster(master);
}
JedisSentinelPool#initSentinels方法詳解,代碼有刪減
private HostAndPort initSentinels(Set<HostAndPort> sentinels, final String masterName) {
HostAndPort master = null;
boolean sentinelAvailable = false;
# 防止部分哨兵不可用
for (HostAndPort sentinel : sentinels) {
Jedis jedis = new Jedis(sentinel, sentinelClientConfig))
# 連接哨兵進程并通過"SENTINEL get-master-addr-by-name mymaster"命令查詢當前主節點
List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);
sentinelAvailable = true;
if (masterAddr == null || masterAddr.size() != 2) {
continue;
}
master = toHostAndPort(masterAddr);
break;
}
# MasterListener是Thread的子類
# 是通過異步線程去檢測主從切換事件,然后及時更新主節點,清理連接池
for (HostAndPort sentinel : sentinels) {
MasterListener masterListener = new MasterListener(masterName, sentinel.getHost(), sentinel.getPort());
masterListener.setDaemon(true);
masterListeners.add(masterListener);
masterListener.start();
}
return master;
}
MasterListener 的run方法,代碼有刪減
MasterListener是JedisSentinelPool內部類,因此能夠操作JedisSentinelPool的變量(masterName)和方法(initMaster)
感興趣的同學可以繼續深入JedisPubSub代碼,可以發現如下:
- 主從切換事件是哨兵進程主動推送過來的,所以能夠保證實時性
因為redis協議是雙工的,所以服務端可以主動推數據給客戶端。
監聽事件的過程就是,創建socket連接,然后讀取數據進行解析。
不發生主從切換事件時,沒有數據推送過來,線程會阻塞在read操作上面。所以雖然run方法是死循環,但是并不會占用cpu時間。
public void run() {
while (running.get()) {
final HostAndPort hostPort = new HostAndPort(host, port);
# 連接本MasterListener關注的哨兵進程
j = new Jedis(hostPort, sentinelClientConfig);
# 監聽+switch-master事件
j.subscribe(new JedisPubSub() {
@Override
public void onMessage(String channel, String message) {
String[] switchMasterMsg = message.split(" ");
if (masterName.equals(switchMasterMsg[0])) {
initMaster(toHostAndPort(Arrays.asList(switchMasterMsg[3], switchMasterMsg[4])));
}
}
}, "+switch-master");
}
JedisSentinelPool#initMaster
如果和當前主節點信息不一致,這更新主節點信息,并清理連接池
private void initMaster(HostAndPort master) {
synchronized (initPoolLock) {
if (!master.equals(currentHostMaster)) {
currentHostMaster = master;
factory.setHostAndPort(currentHostMaster);
clearInternalPool();
}
}
}
jedis的哨兵模式并沒有實現讀寫分離,僅僅只與主節點建立連接。所以沒辦法完全挖掘集群的性能。
可以通過redisTemplate.opsForValue().set(“aaa”,“bbb”);去跟蹤新連接的創建過程(非復用連接池中的連接)。
最終會追蹤到JedisConnectionFactory#fetchJedisConnector方法
protected Jedis fetchJedisConnector() {
# jedis會開啟連接池
if (getUsePool() && pool != null) {
return pool.getResource();
}
Jedis jedis = createJedis();
jedis.connect();
return jedis;
}
JedisSentinelPool負責連接的創建,JedisSentinelPool持有主節點信息
public Jedis getResource() {
while (true) {
Jedis jedis = super.getResource();
jedis.setDataSource(this);
final HostAndPort master = currentHostMaster;
final HostAndPort connection = new HostAndPort(jedis.getClient().getHost(), jedis.getClient()
.getPort());
if (master.equals(connection)) {
return jedis;
} else {
returnBrokenResource(jedis);
}
}
}
原文鏈接:https://blog.csdn.net/shuxiaohua/article/details/119045627
相關推薦
- 2022-04-07 使用swift實現計算器功能_Swift
- 2022-07-10 輸入兩個正整數 m 和 n,求最大公約數
- 2022-05-14 pytorch中with?torch.no_grad():的用法實例_python
- 2022-06-19 教你用vbs實現微信自動發送消息功能_vbs
- 2022-12-11 React高級特性Context萬字詳細解讀_React
- 2022-08-15 Android開發gradle拉取依賴的加速配置_Android
- 2023-07-26 vscode中配置代碼片段
- 2022-05-31 關于k8s?使用?Service?控制器對外暴露服務的問題_云其它
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支