前沿
最近工作中在優(yōu)化redis訪問(wèn)性能昧捷,這里總結(jié)一下優(yōu)化過(guò)程中redis使用方法的一些心得體會(huì),以及在spring-data-redis開(kāi)源項(xiàng)目提交代碼的一些經(jīng)驗(yàn)姨谷。具體來(lái)說(shuō)赐劣,包括如下三部分:
連接池參數(shù)配置對(duì)性能的影響,包括testOnBorrow鳍悠、testOnReturn税娜、testWhileIdle等Apache Pool庫(kù)的參數(shù)設(shè)置。該部分最后會(huì)給出連接池參數(shù)設(shè)置的best practice贼涩,對(duì)于性能比較敏感的應(yīng)用直接使用該最優(yōu)設(shè)置即可巧涧,合理的連接池設(shè)置可以提升性能2-10倍。
redis集群模式如何提高訪問(wèn)效率遥倦,相對(duì)sentinal和單機(jī)模式,集群模式數(shù)據(jù)分散在不同的節(jié)點(diǎn)上占锯,因此性能調(diào)優(yōu)更為重要袒哥,合理性能調(diào)優(yōu)甚至可以提升訪問(wèn)性能10倍以上。這部分主要討論了redis cluster模式下使用過(guò)程中遇到的一些問(wèn)題消略,比如數(shù)據(jù)分片堡称、mGet的使用性能、redis cluster節(jié)點(diǎn)的負(fù)載均衡等艺演。
優(yōu)化過(guò)程中還查到spring-data-redis庫(kù)兩個(gè)bug(或者可以改進(jìn)的improvement)却紧,已經(jīng)向開(kāi)源庫(kù)提交code fix改進(jìn)性能桐臊。這部分主要討論下如何在開(kāi)源項(xiàng)目提交代碼,開(kāi)源項(xiàng)目的代碼風(fēng)格晓殊,單元測(cè)試断凶、集成測(cè)試、CI等代碼質(zhì)量控制等幾個(gè)方面
Reids連接池參數(shù)設(shè)置對(duì)性能的影響
前不久在梳理項(xiàng)目性能的過(guò)程中巫俺,通過(guò)哨兵監(jiān)控發(fā)現(xiàn)了一個(gè)很詭異的現(xiàn)象认烁。如下圖所示,mergeData方法只包含redis的setex和get兩個(gè)操作介汹,redis的監(jiān)控顯示兩個(gè)操作的rt大概加起來(lái)是1.5ms却嗡,然而該方法的平均rt占了3.74ms左右,多出了2ms多的時(shí)間嘹承,覺(jué)得非常奇怪窗价,排查了一下多出的2ms的時(shí)間到底去哪兒了。
剛開(kāi)始懷疑是不是多線程情況下io同步阻塞叹卷,導(dǎo)致CPU輪換時(shí)間增大舌镶,即線程較多導(dǎo)致線程上下文切換時(shí)間變大。實(shí)際上可以分析哨兵上redis客戶端命令的監(jiān)控原理豪娜,哨兵是對(duì)客戶端jedis中BinaryJedis.get方法攔截餐胀,生成新的方法get$sentryProxy,統(tǒng)計(jì)該方法的執(zhí)行時(shí)間瘤载,而該代理方法包含了從發(fā)送命令到同步讀數(shù)據(jù)完成否灾,因此該設(shè)想不成立。
使用visualvm模擬線上環(huán)境排查問(wèn)題鸣奔,發(fā)現(xiàn)該方法除了get setex兩個(gè)操作墨技,還消耗大量時(shí)間去getConnecton和releaseConnection,問(wèn)題已經(jīng)很明顯了挎狸,客戶端從連接池獲取扣汪、返還連接時(shí)耗費(fèi)了大量時(shí)間。
優(yōu)化方案:
修改redis的連接池配置锨匆,比如預(yù)先分配連接崭别,并設(shè)置minIdel = 1,保證至少有一個(gè)可用的連接恐锣。
同時(shí)設(shè)置testOnBorrow和testOnReturn兩個(gè)值為false茅主,減少不必要IO請(qǐng)求。
連接可用性主要有testWhileIdle = true來(lái)保證
小結(jié):優(yōu)化后性能基本得到一倍以上的提升土榴,方法執(zhí)行時(shí)間減少為原來(lái)的一半诀姚。雖然改動(dòng)很小,但是很小的改動(dòng)帶來(lái)的性能提升實(shí)在令人吃驚玷禽。哨兵的方法級(jí)監(jiān)控和redis的客戶端監(jiān)控幫助我們排查問(wèn)題赫段,經(jīng)常查看監(jiān)控還是有很大的好處呀打,可以主動(dòng)發(fā)現(xiàn)一些問(wèn)題并提前解決問(wèn)題。
Redis連接池參數(shù)性能優(yōu)化在cluster模式下更明顯
上面寫(xiě)到優(yōu)化redis連接池的testOnBorrow,testOnReturn 等參數(shù)糯笙,Jedis連接池使用了apache的連接池Common Pool贬丛,JedisFactory的代碼可以看出,testOnBorrow,testOnReturn兩個(gè)參數(shù)如果設(shè)為true,每次獲取connection時(shí)候都要發(fā)送Ping指令到redis集群炬丸,浪費(fèi)大量IO時(shí)間瘫寝。如果每次獲取和返還時(shí)不檢測(cè)連接的可用性,怎么保證每次拿到的接連是可用的呢稠炬,一般來(lái)說(shuō)可以設(shè)置參數(shù)testWhenIdle=true來(lái)保證連接的可用性焕阿,改參數(shù)會(huì)定期檢查空閑連接的狀態(tài)。
class JedisFactory implements PooledObjectFactory {
@Override
public boolean validateObject(PooledObject pooledJedis) {
final BinaryJedis jedis = pooledJedis.getObject();
try {
HostAndPort hostAndPort = this.hostAndPort.get();
String connectionHost = jedis.getClient().getHost();
int connectionPort = jedis.getClient().getPort();
return hostAndPort.getHost().equals(connectionHost)
&& hostAndPort.getPort() == connectionPort && jedis.isConnected()
&& jedis.ping().equals("PONG");
} catch (final Exception e) {
return false;
}
}
在另一個(gè)系統(tǒng)中Redis采用Cluster模式部署首启,如下圖所示參數(shù)優(yōu)化上線后rt下降更加明顯暮屡,從原來(lái)的耗時(shí)600ms下降到60ms,性能提升甚至達(dá)到10倍左右毅桃。原因在于:cluster模式下使用了mGet命令褒纲,而緩存在redis中數(shù)據(jù)沒(méi)有使用hashtag,因此需要從各節(jié)點(diǎn)讀取數(shù)據(jù)钥飞。那么客戶端就需要維護(hù)一個(gè)Redis集群中各節(jié)點(diǎn)信息莺掠,包括有哪些節(jié)點(diǎn),每個(gè)節(jié)點(diǎn)被分配的slots等读宙。相比NCR的單點(diǎn)連接彻秆,cluster模式除了簡(jiǎn)單的執(zhí)行g(shù)et set等命令外,還需要與Redis服務(wù)端進(jìn)行額外的通信结闸,會(huì)多發(fā)送很多PING命令到Redis集群唇兑。
Redis Cluter模式下的性能問(wèn)題分析
為什么Cluster Node命令總是發(fā)送到集群中的同一臺(tái)機(jī)器
在redis集群中有一個(gè)節(jié)點(diǎn)CPU經(jīng)常被撐爆,而且Cluster Nodes命中非常多桦锄,占用較多的CPU資源扎附,嚴(yán)重影響了Redis集群的整體性能。Nodes命令主要用來(lái)獲取集群的節(jié)點(diǎn)信息结耀,包括有哪些活躍的節(jié)點(diǎn)留夜,該節(jié)點(diǎn)的ip和port,master還是slave饼记,包含哪些slots等信息香伴。由于數(shù)據(jù)路由是在客戶端做的,客戶端需要知道redis服務(wù)器的節(jié)點(diǎn)信息具则,并根據(jù)此信息對(duì)數(shù)據(jù)做路由。下面是Nodes命令返回的結(jié)果:
d1861060fe6a534d42d8a19aeb36600e18785e04 127.0.0.1:6379 myself - 0 1318428930 1 connected 0-1364
3886e65cc906bfd9b1f7e7bde468726a052d1dae 127.0.0.1:6380 master - 1318428930 1318428931 2 connected 1365-2729
d289c575dcbc4bdd2931585fd4339089e461a27d 127.0.0.1:6381 master - 1318428931 1318428931 3 connected 2730-4095
那么為什么獲取cluster信息的命中總是發(fā)送到同一臺(tái)節(jié)點(diǎn)呢具帮,原因出在下面Spring-data-redis的代碼上:
@Override
public ClusterTopology getTopology() {
if (cached != null && time + 100 > System.currentTimeMillis()) {
return cached;
}
Map errors = new LinkedHashMap();
for (Entry entry : cluster.getClusterNodes().entrySet()) {
Jedis jedis = null;
try {
jedis = entry.getValue().getResource();
time = System.currentTimeMillis();
Set nodes = Converters.toSetOfRedisClusterNodes(jedis.clusterNodes());
synchronized (lock) {
cached = new ClusterTopology(nodes);
}
return cached;
} catch (Exception ex) {
errors.put(entry.getKey(), ex);
} finally {
if (jedis != null) {
entry.getValue().returnResource(jedis);
}
}
for循環(huán)遍歷所有的節(jié)點(diǎn)博肋,從第一個(gè)開(kāi)始嘗試發(fā)送Nodes命令低斋,在獲取到信息后返回。我們看到這段代碼匪凡,第一反應(yīng)是map中數(shù)據(jù)的存儲(chǔ)是無(wú)序的膊畴,那么遍歷應(yīng)該也是無(wú)序的,那么請(qǐng)求應(yīng)該是隨機(jī)發(fā)送到集群中的任意節(jié)點(diǎn)才對(duì)病游。仔細(xì)想下就能發(fā)現(xiàn)問(wèn)題了唇跨,由于Entry<String, JedisPool>中的key是節(jié)點(diǎn)名稱name:port,集群中的節(jié)點(diǎn)名稱和port相對(duì)固定衬衬,因此很容易就往第一個(gè)獲取到的連接去發(fā)送cluster命令买猖。
hashmap.entrySet的順序性,主要有hashcode和插入前后保證滋尉,由于節(jié)點(diǎn)較少不存在hash沖突玉控,因此基本有hashcode來(lái)確定前后順序,就是說(shuō)map中數(shù)據(jù)看似無(wú)序?qū)崉t有序狮惜。由于集群中節(jié)點(diǎn)的名稱和port基本不會(huì)改變高诺,因此entrySet的順序也不會(huì)改變,Cluster Nodes命令就會(huì)一直發(fā)送到同一個(gè)節(jié)點(diǎn)上碾篡。 該問(wèn)題向spring-data官方反饋:https://jira.spring.io/browse/DATAREDIS-890厢蒜,目前該bug的code fix已經(jīng)merge到spring官方master分支,在2.1.3(Lovalace SR3) Release發(fā)布鸿秆。
為什么NODES命令在Redis服務(wù)端如此消耗CPU
經(jīng)過(guò)線上監(jiān)控可知叉橱,redis服務(wù)端NODES命令達(dá)到1400+ qps時(shí),該節(jié)點(diǎn)CPU飆升到100%眼姐,嚴(yán)重影響redis集群的問(wèn)題性诅迷。經(jīng)過(guò)上文分析可知,redis客戶端最快每100ms拉一個(gè)NODE信息众旗,我們客戶端節(jié)點(diǎn)數(shù)大概150左右罢杉,1400 qps數(shù)據(jù)能夠?qū)Φ纳稀?strong>可以看出,該問(wèn)題的暴露與客戶端節(jié)點(diǎn)數(shù)也密切相關(guān)贡歧。
那么問(wèn)題來(lái)了滩租,為何NODES命令才1400qps就把cpu打滿了,redis服務(wù)端在接受到該命令后究竟做了什么操作利朵?答案在下圖律想,CLUSTER_SLOTS常量等于16384,因此redis每次都要循環(huán)很多次去組裝每個(gè)節(jié)點(diǎn)的slot信息绍弟。CPU至少需要循環(huán)16384乘以N次技即,N為redis集群master的個(gè)數(shù)。因此樟遣,隨著redis集群規(guī)模的擴(kuò)大而叼、以及客戶端節(jié)點(diǎn)數(shù)的增加身笤,NODES命令打滿CPU的問(wèn)題會(huì)越來(lái)越嚴(yán)重。
/* Slots served by this instance */
start = -1;
for (j = 0; j < CLUSTER_SLOTS; j++) {
int bit;
if ((bit = clusterNodeGetSlotBit(node,j)) != 0) {
if (start == -1) start = j;
}
if (start != -1 && (!bit || j == CLUSTER_SLOTS-1)) {
if (bit && j == CLUSTER_SLOTS-1) j++;
if (start == j-1) {
ci = sdscatprintf(ci," %d",start);
} else {
ci = sdscatprintf(ci," %d-%d",start,j-1);
}
start = -1;
}
}
其實(shí)redis系統(tǒng)命令NODES的性能問(wèn)題葵陵,在2018年已經(jīng)有反饋給redis官方:Slow performance of CLUSTER SLOTS液荸,有計(jì)劃對(duì)該命令做性能優(yōu)化,比如可以采用在內(nèi)存維護(hù)加一個(gè)cache脱篙,動(dòng)態(tài)更新每個(gè)節(jié)點(diǎn)的slot信息等方案娇钱。
mGet為什么特別慢
項(xiàng)目設(shè)計(jì)初時(shí)沒(méi)有對(duì)數(shù)據(jù)進(jìn)行合理的hash tag分片,因此需要從多Redis節(jié)點(diǎn)獲取數(shù)據(jù)绊困,使用spring的mGet方法從redis cluster多節(jié)點(diǎn)獲取數(shù)據(jù)文搂。調(diào)整了三個(gè)參數(shù)后,mGet還是比較預(yù)期慢不少考抄,通過(guò)排查mGet命令發(fā)現(xiàn)细疚,其中70%的時(shí)間用來(lái)sleep,而且是程序主動(dòng)去sleep川梅。
while (!done) {
done = true;
for (Map.Entry>> entry : futures.entrySet()) {
if (!entry.getValue().isDone() && !entry.getValue().isCancelled()) {
done = false;
} else {
try {
String futureId = ObjectUtils.getIdentityHexString(entry.getValue());
if (!saveGuard.contains(futureId)) {
result.add(entry.getValue().get());
saveGuard.add(futureId);
}
} catch (ExecutionException e) {
RuntimeException ex = convertToDataAccessExeption((Exception) e.getCause());
exceptions.put(entry.getKey().getNode(), ex != null ? ex : e.getCause());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
RuntimeException ex = convertToDataAccessExeption((Exception) e.getCause());
exceptions.put(entry.getKey().getNode(), ex != null ? ex : e.getCause());
break;
}
}
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
done = true;
Thread.currentThread().interrupt();
}
}
上面代碼可以看出疯兼,在等待各節(jié)點(diǎn)返回中休眠10ms后再次檢查數(shù)據(jù)有沒(méi)有到達(dá),spring的實(shí)現(xiàn)看起來(lái)有點(diǎn)不太能理解贫途。對(duì)于該問(wèn)題吧彪,向spring官方反饋:https://jira.spring.io/projects/DATAREDIS/issues/DATAREDIS-889,收到如下答復(fù)丢早。
The code of clustered mget sends multiple commands, needs to await completion and needs to merge results. 50ms sounds as if synchronization looped about five times over the synchronization code until all responses arrived.
There are various things to consider:
1. Cross-slot requests need to be split up into multiple requests. This also means that a single mget call can be split up into a number of requests that hit the same server just to server the purpose of having a single slot per command.
2. With version 1.8.x, we're required to use Java 6 APIs and we can't use synchronization utilities such as composing CompletableFuture.
3. Without sleep, we basically burn CPU cycles in that loop. So when a mget command awaits synchronization, we would basically use 100% of your CPU (core) within a busy spin that cannot be interrupted (e.g. shutdown). To avoid busy spin, allow interruption and allow the CPU core to do other things, we added a sleep timeout.
Have you tried switching clients? Lettuce does mget routing itself and uses Java 8 API for that purpose which eliminates the need to sleep.
總體上就是受限于JDK6姨裸,不想忙等待占有大量CPU,采用sleep的方式減少對(duì)CPU的占用怨酝。同時(shí)建議我們使用Lettuce庫(kù)傀缩,該庫(kù)采用Netty作為底層通信框架,使用異步IO性能有很大提升农猬。
這里有的同學(xué)可能會(huì)問(wèn)赡艰,直接循環(huán)調(diào)用get()方法同步阻塞不就好了,其實(shí)不然斤葱。由于mGet需要從多個(gè)節(jié)點(diǎn)獲取數(shù)據(jù)慷垮,有些節(jié)點(diǎn)任務(wù)執(zhí)行可能會(huì)拋異常,這里需要提前獲取這些已經(jīng)完成的任務(wù)揍堕,如果有異常就直接返回了料身,不用再等待其他的數(shù)據(jù)返回。采用這種實(shí)現(xiàn)方案衩茸,實(shí)際是在CPU消耗和總耗時(shí)之間的折中芹血。JDK8中引入CompletableFuture支持按照順序返回結(jié)果,先完成的先返回可以提高并發(fā)執(zhí)行的效率。
由于臨時(shí)切換客戶端祟牲,無(wú)論是開(kāi)發(fā)還是測(cè)試成本都比較高隙畜,目前的解決辦法是把mGet轉(zhuǎn)化成多個(gè)get請(qǐng)求抖部,在線程池中并發(fā)執(zhí)行说贝,RT時(shí)間從60ms下降到6ms左右,如下圖所示性能提升10倍慎颗。后期有時(shí)間可以好好研究一下Lettuce客戶端的使用方法和性能指標(biāo)乡恕,貌似Jedis客戶端API目前很少更新,處于不太活躍的狀態(tài)俯萎,Spring目前已經(jīng)把開(kāi)發(fā)重心從Jedis轉(zhuǎn)移到Lettuce客戶端了傲宜,Spring-Session中使用的默認(rèn)Redis訪問(wèn)API就是Lettuce。
前文說(shuō)過(guò)mGet命令中70%的時(shí)間用來(lái)sleep夫啊,那么按道理RT應(yīng)該從60ms降到21ms左右函卒,為什么改用自己的線程池執(zhí)行能降到6ms左右呢。原因在于撇眯,Spring內(nèi)部實(shí)際上也是把多個(gè)mGet拆開(kāi)多個(gè)get并在線程池里面執(zhí)行报嵌,該線程池為spring封裝類ThreadPoolTaskExecutor,參數(shù)coreSize為1熊榛,嚴(yán)重影響了并發(fā)的執(zhí)行效率锚国。我們使用自己的線程池,把coreSize設(shè)置為Runtime.getRuntime().availableProcessors() * 4, 可以進(jìn)一步提高并發(fā)數(shù)玄坦,提高訪問(wèn)性能血筑。可能有的同學(xué)又要問(wèn)煎楣,Redis服務(wù)端是單線程的豺总,為什么提高客戶端并發(fā)數(shù)可以提高性能呢,Redis服務(wù)端不是一條一條地執(zhí)行命令的嗎择懂?
原因主要有兩點(diǎn):
并發(fā)提交請(qǐng)求可以節(jié)約一定IO通信時(shí)間喻喳,Redis服務(wù)端接收請(qǐng)求是并發(fā)的,這部分實(shí)際節(jié)約應(yīng)該比較有限
Redis服務(wù)端部署是集群模式休蟹,讀請(qǐng)求是并發(fā)分散到各節(jié)點(diǎn)的
這里有一點(diǎn)需要特別說(shuō)明的沸枯,Redis Cluster部署模式下,盡量把相關(guān)聯(lián)的數(shù)據(jù)通過(guò)hashtag強(qiáng)制存儲(chǔ)到一個(gè)節(jié)點(diǎn)上赂弓,可以大大提高使用效率绑榴。由于歷史原因,數(shù)據(jù)已經(jīng)存儲(chǔ)在不同節(jié)點(diǎn)上盈魁,或者有強(qiáng)烈需要從多節(jié)點(diǎn)獲取數(shù)據(jù)的翔怎,應(yīng)該盡量避免直接使用Spring的Jedis庫(kù)中的mGet方法。可以自己封裝線程池單獨(dú)執(zhí)行多個(gè)get操作赤套,也可以使用Lettuce客戶端訪問(wèn)數(shù)據(jù)飘痛。
Spring開(kāi)源項(xiàng)目提交代碼經(jīng)驗(yàn)分享
由于筆者開(kāi)源提交代碼經(jīng)驗(yàn)不多,簡(jiǎn)單分享下:
- 給Spring創(chuàng)建Jira任務(wù)容握,經(jīng)過(guò)討論方案可行后宣脉,可以提交pull request。
- Git中CI檢查必須通過(guò)剔氏,開(kāi)源項(xiàng)目中的單元測(cè)試塑猖、集成測(cè)試比較嚴(yán)格,新增的代碼需要新的測(cè)試用例證明代碼正確性谈跛,git commit后會(huì)自動(dòng)跑CI羊苟。spring-data-redis采用travis作為CI平臺(tái),和Git結(jié)合比較好感憾,可以看到每次代碼提交后測(cè)試用例的執(zhí)行情況蜡励。
- pull request提交到master分支,spring一般在發(fā)布版本時(shí)單獨(dú)從master拉出分支阻桅。這樣的好處是可以比較靈活凉倚,可以單獨(dú)對(duì)不同的發(fā)布分支加Patch,同時(shí)頻繁發(fā)布時(shí)也比較靈活鳍刷。
回到之前的那個(gè)Nodes命令不均衡的問(wèn)題占遥,查詢Redis文檔可以知道Cluster中每個(gè)節(jié)點(diǎn)都可以外對(duì)提供Nodes查詢節(jié)點(diǎn)信息,不管是master還是slave:
The CLUSTER NODES command can be sent to any node in the cluster and provides the state of the cluster and the information for each node according to the local view the queried node has of the cluster.
因此输瓜,只要在遍歷map之前做一下隨機(jī)就可以了瓦胎,解決辦法非常簡(jiǎn)單直接。好處是可以把Nodes命令分散到Cluster的各個(gè)節(jié)點(diǎn)上尤揣,由于默認(rèn)每隔100ms每個(gè)客戶端節(jié)點(diǎn)就會(huì)發(fā)送該命令搔啊,如果客戶端數(shù)量比較多,該命令都落到同一個(gè)服務(wù)端節(jié)點(diǎn)上北戏,對(duì)性能肯定會(huì)造成比較大的影響负芋。