概念簡介
簡單介紹,不做過多描述,網上很多詳細介紹
- TCP/IP
屬于網絡協議 - SOCKET 為內核向用戶提供的可以實現各種網絡通信協議的api
- FD Linux 一切皆文件,進程可以打開成百上千個文件,為了表示和區分已經打開的文件,Linux 會給每個文件分配一個編號(一個 ID),這個編號就是一個整數,被稱為文件描述符(File Descriptor)
IO模型(Unix 網絡編程P124)
首先一個IO操作其實分成了兩個步驟:發起IO請求和實際的IO操作,
同步IO和異步IO的區別就在于第二個步驟是否阻塞,如果實際的IO讀寫阻塞請求進程,那么就是同步IO,因此阻塞IO、非阻塞IO、IO復用、信號驅動IO都是同步IO,如果不阻塞,而是操作系統幫你做完IO操作再將結果返回給你,那么就是異步IO。
阻塞IO和非阻塞IO的區別在于第一步,發起IO請求是否會被阻塞,如果阻塞直到完成那么就是傳統的阻塞IO,如果不阻塞,那么就是非阻塞IO。
同步:所謂同步,就是在發出一個功能調用時,在沒有得到結果之前,該調用就不返回。
異步:異步的概念和同步相對。當一個異步過程調用發出后,調用者不能立刻得到結果。實際處理這個調用的部件在完成后,通過狀態、通知和回調來通知調用者。
-
阻塞blocking I/O
可讀事件和實際數據讀全過程全阻塞
blocking-io.jpg
-非阻塞 nonblocking I/O
可讀事件由用戶態輪詢,數據讀取過程阻塞
-
IO復用 I/O multiplexing (select and poll)
multiplexing-io.jpg
1.2 IO多路復用的歷史
select, poll, epoll 都是I/O多路復用的具體的實現,之所以有這三個鬼存在,其實是他們出現是有先后順序的。
I/O多路復用這個概念被提出來以后, select是第一個實現 (1983 左右在BSD里面實現的)。
select
select 被實現以后,很快就暴露出了很多問題。
select 會修改傳入的參數數組,這個對于一個需要調用很多次的函數,是非常不友好的。
select 如果任何一個sock(I/O stream)出現了數據,select 僅僅會返回,但是并不會告訴你是那個sock上有數據,于是你只能自己一個一個的找,10幾個sock可能還好,要是幾萬的sock每次都找一遍,這個無謂的開銷就頗有海天盛筵的豪氣了。
select 只能監視1024個鏈接, 這個跟草榴沒啥關系哦,linux 定義在頭文件中的,參見FD_SETSIZE。
select 不是線程安全的,如果你把一個sock加入到select, 然后突然另外一個線程發現,尼瑪,這個sock不用,要收回。對不起,這個select 不支持的,如果你喪心病狂的竟然關掉這個sock, select的標準行為是。。呃。。不可預測的, 這個可是寫在文檔中的哦.
“If a file descriptor being monitored by select() is closed in another thread, the result is unspecified”
霸不霸氣
poll
于是14年以后(1997年)一幫人又實現了poll, poll 修復了select的很多問題,比如
poll 去掉了1024個鏈接的限制,于是要多少鏈接呢, 主人你開心就好。
poll 從設計上來說,不再修改傳入數組,不過這個要看你的平臺了,所以行走江湖,還是小心為妙。
其實拖14年那么久也不是效率問題, 而是那個時代的硬件實在太弱,一臺服務器處理1千多個鏈接簡直就是神一樣的存在了,select很長段時間已經滿足需求。
但是poll仍然不是線程安全的, 這就意味著,不管服務器有多強悍,你也只能在一個線程里面處理一組I/O流。你當然可以那多進程來配合了,不過然后你就有了多進程的各種問題。
select/poll的幾大缺點
1、每次調用select/poll,都需要把fd集合用戶態拷貝到內核態,這個開銷在fd很多時會很大
2、同時每次調用select/poll都需要在內核遍歷傳遞進來的所有fd,這個開銷在fd很多時也很大
3、針對select支持的文件描述符數量太小了,默認是1024
4.select返回的是含有整個句柄的數組,應用程序需要遍歷整個數組才能發現哪些句柄發生了事件;
5.select的觸發方式是水平觸發。(個人理解:如交易系統每筆交易會觸發一次,一次就是把所有fd集合從用戶態拷貝到內核態,所有表示select觸發頻率也很高)
epoll
于是5年以后, 在2002, 大神 Davide Libenzi 實現了epoll.
epoll 可以說是I/O 多路復用最新的一個實現,epoll 修復了poll 和select絕大部分問題, 比如:
epoll 現在是線程安全的。
epoll 現在不僅告訴你sock組里面數據,還會告訴你具體哪個sock有數據,你不用自己去找了。
select的本質是采用32個整數的32位,即32*32= 1024來標識,fd值為1->1024。當fd的值超過1024限制時,就必須修改FD_SETSIZE的大小。這個時候就可以標識32*max值范圍的fd。
這種設計遍歷的過程非常快,因為用位的邏輯操作就可以
缺點:
a. fd超過1024時,性能無法滿足,在linux 早期并發沒有那么大,還可以大范圍支持。
b. 網絡一般分兩步操作,一步是獲取io事件,另一步是數據讀取,從內核態讀到用戶態.select兩步都是阻塞的poll
沒有1024限制,但依然是兩步IO都是阻塞的epoll
沒有1024 限制,但第二步依賴阻塞,這也是java nio 所謂的同步非阻塞IO
-
signal driven I/O (SIGIO)
signal-driven-io.jpg
- 異步 IO
兩步都不阻塞
-
asynchronous I/O (the POSIX aio_functions)
asynchronous-io.jpg -
比較
compare-io.gif
Synchronous I/O versus Asynchronous I/O
POSIX defines these two terms as follows:
- A synchronous I/O operation causes the
requesting process
to be blocked until that I/O operation completes. - An asynchronous I/O operation does not cause the
requesting process
to be blocked.
Using these definitions, the first four I/O models blocking, nonblocking, I/O multiplexing, and signal-driven I/O are all synchronous because the actual I/O operation (recvfrom)
blocks the process. Only the asynchronous I/O model matches the asynchronous I/O definition.
requesting process 可以理解為用戶態當前進程
長鏈接VS短鏈接
TCP協議中有長連接和短連接之分。短連接在數據包發送完成后就會自己斷開,長連接在發包完畢后,會在一定的時間內保持連接,即我們通常所說的Keepalive(存活定時器)功能。
默認的Keepalive超時需要7,200,000 milliseconds,即2小時,探測次數為5次。它的功效和用戶自己實現的心跳機制是一樣的。開啟Keepalive功能需要消耗額外的寬帶和流量,盡管這微不足道,但在按流量計費的環境下增加了費用,另一方面,Keepalive設置不合理時可能會因為短暫的網絡波動而斷開健康的TCP連接。
- ?;?/li>
keepalive并不是TCP規范的一部分。在Host Requirements RFC羅列有不使用它的三個理由:
(1)在短暫的故障期間,它們可能引起一個良好連接(good connection)被釋放(dropped),(2)它們消費了不必要的寬帶,
(3)在以數據包計費的互聯網上它們(額外)花費金錢。
然而,需要在應用層實現心跳檢測與鏈接重連
網絡單通
連接被防火墻Hand 住
長時間GC或者通信線程發生非預期異常
會導致連接不可用,而又無法及時發現。
應用場景
- 長鏈接
- 鏈接固定且頻繁
- 長連接多用于操作頻繁,點對點的通訊,而且連接數不能太多情況。每個TCP連接都需要三步握手,這需要時間,如果每個操作都是先連接,再操作的話那么處理速度會降低很多,所以每個操作完后都不斷開,每次處理時直接發送數據包就OK了,不用建立TCP連接。例如:數據庫的連接用長連接, 如果用短連接頻繁的通信會造成socket錯誤,而且頻繁的socket 創建也是對資源的浪費。
- 推送業務
-
短鏈接
a. 隨機訪問
而像WEB網站的http服務一般都用短鏈接,因為長連接對于服務端來說會耗費一定的資源,而像WEB網站這么頻繁的成千上萬甚至上億客戶端的連接用短連接會更省一些資源,如果用長連接,而且同時有成千上萬的用戶,如果每個用戶都占用一個連接的話,資源占用太大。所以并發量大,但每個用戶無需頻繁操作情況下需用短連好。
實際場景分析(lettuce 客戶端)
redis 的lettuce客戶端
-
驗證客戶端的connection與tcp鏈接的關系,即與fd的關系,這里tcp物理連接與fd一一對應.
TCP協議
由于tcp是網絡層協議,主要關系圖中的第三層,主要幾個字段source-->ip:port到destination ip:port
- 驗證代碼
引自 lettuce官方example
/*
* Copyright 2011-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.lambdaworks.examples;
import com.lambdaworks.redis.RedisClient;
import com.lambdaworks.redis.RedisURI;
import com.lambdaworks.redis.api.StatefulRedisConnection;
import com.lambdaworks.redis.api.sync.RedisCommands;
/**
* @author Mark Paluch
*/
public class ReadWriteExample {
public static void main(String[] args) {
// Syntax: redis://[password@]host[:port][/databaseNumber]
RedisClient redisClient = RedisClient.create(RedisURI.create("redis://localhost:6379/0"));
StatefulRedisConnection<String, String> connection = redisClient.connect();
//創建10個connection
for (int i=0;i<10;i++) {
redisClient.connect();
}
System.out.println("Connected to Redis");
RedisCommands<String, String> sync = connection.sync();
sync.set("foo", "bar");
String value = sync.get("foo");
System.out.println(value);
connection.close();
redisClient.shutdown();
}
}
redis-client 監控結果
10個tcp鏈接對于10個fd
localhost:0>client list
"id=220 addr=127.0.0.1:62381 fd=13 name= age=42 idle=42 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=NULL
id=221 addr=127.0.0.1:62382 fd=12 name= age=42 idle=42 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=NULL
id=222 addr=127.0.0.1:62383 fd=11 name= age=42 idle=42 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=NULL
id=223 addr=127.0.0.1:62384 fd=18 name= age=42 idle=42 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=NULL
id=224 addr=127.0.0.1:62385 fd=14 name= age=42 idle=42 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=NULL
id=225 addr=127.0.0.1:62386 fd=8 name= age=42 idle=42 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=NULL
id=226 addr=127.0.0.1:62387 fd=15 name= age=42 idle=42 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=NULL
id=227 addr=127.0.0.1:62388 fd=9 name= age=42 idle=42 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=NULL
id=36 addr=127.0.0.1:57381 fd=16 name= age=18865 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client
id=217 addr=127.0.0.1:62378 fd=20 name= age=42 idle=42 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=NULL
id=218 addr=127.0.0.1:62379 fd=10 name= age=42 idle=42 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=NULL
id=219 addr=127.0.0.1:62380 fd=19 name= age=42 idle=42 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=NULL
"
localhost:0>
AbstractRedisClient相關netty代碼
{
Bootstrap redisBootstrap = new Bootstrap();
redisBootstrap.option(ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK, 32 * 1024);
redisBootstrap.option(ChannelOption.WRITE_BUFFER_LOW_WATER_MARK, 8 * 1024);
redisBootstrap.option(ChannelOption.ALLOCATOR, BUF_ALLOCATOR);
SocketOptions socketOptions = getOptions().getSocketOptions();
redisBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,
(int) socketOptions.getConnectTimeoutUnit().toMillis(socketOptions.getConnectTimeout()));
if (LettuceStrings.isEmpty(redisURI.getSocket())) {
redisBootstrap.option(ChannelOption.SO_KEEPALIVE, socketOptions.isKeepAlive());
redisBootstrap.option(ChannelOption.TCP_NODELAY, socketOptions.isTcpNoDelay());
}
connectionBuilder.timeout(redisURI.getTimeout(), redisURI.getUnit());
connectionBuilder.password(redisURI.getPassword());
connectionBuilder.bootstrap(redisBootstrap);
connectionBuilder.channelGroup(channels).connectionEvents(connectionEvents).timer(timer);
connectionBuilder.commandHandler(handler).socketAddressSupplier(socketAddressSupplier).connection(connection);
connectionBuilder.workerPool(genericWorkerPool);
}
netty 客戶端的chnnel 創建
/**
* Connect and initialize a channel from {@link ConnectionBuilder}.
*
* @param connectionBuilder must not be {@literal null}.
* @return the {@link ConnectionFuture} to synchronize the connection process.
* @since 4.4
*/
@SuppressWarnings("unchecked")
protected <K, V, T extends RedisChannelHandler<K, V>> ConnectionFuture<T> initializeChannelAsync(
ConnectionBuilder connectionBuilder) {
SocketAddress redisAddress = connectionBuilder.socketAddress();
if (clientResources.eventExecutorGroup().isShuttingDown()) {
throw new IllegalStateException("Cannot connect, Event executor group is terminated.");
}
logger.debug("Connecting to Redis at {}", redisAddress);
CompletableFuture<Channel> channelReadyFuture = new CompletableFuture<>();
Bootstrap redisBootstrap = connectionBuilder.bootstrap();
RedisChannelInitializer initializer = connectionBuilder.build();
redisBootstrap.handler(initializer);
clientResources.nettyCustomizer().afterBootstrapInitialized(redisBootstrap);
CompletableFuture<Boolean> initFuture = initializer.channelInitialized();
//物理TCP鏈接創建
ChannelFuture connectFuture = redisBootstrap.connect(redisAddress);
connectFuture.addListener(future -> {
if (!future.isSuccess()) {
logger.debug("Connecting to Redis at {}: {}", redisAddress, future.cause());
connectionBuilder.commandHandler().initialState();
channelReadyFuture.completeExceptionally(future.cause());
return;
}
initFuture.whenComplete((success, throwable) -> {
if (throwable == null) {
logger.debug("Connecting to Redis at {}: Success", redisAddress);
RedisChannelHandler<?, ?> connection = connectionBuilder.connection();
connection.registerCloseables(closeableResources, connection);
channelReadyFuture.complete(connectFuture.channel());
return;
}
logger.debug("Connecting to Redis at {}, initialization: {}", redisAddress, throwable);
connectionBuilder.commandHandler().initialState();
Throwable failure;
if (throwable instanceof RedisConnectionException) {
failure = throwable;
} else if (throwable instanceof TimeoutException) {
failure = new RedisConnectionException("Could not initialize channel within "
+ connectionBuilder.getTimeout() + " " + connectionBuilder.getTimeUnit(), throwable);
} else {
failure = throwable;
}
channelReadyFuture.completeExceptionally(failure);
CompletableFuture<Boolean> response = new CompletableFuture<>();
response.completeExceptionally(failure);
});
});
return new DefaultConnectionFuture<>(redisAddress, channelReadyFuture.thenApply(channel -> (T) connectionBuilder
.connection()));
}
由代碼和監控可確認,luttuce redis 的connection對netty的channel一一對應,同時服務器會一一創建tcp鏈接!(這里強調一下是lettuce的redis 客戶端,而spring的封裝并非如此
)
Spring Boot RedisTemplate
由于本地redis 這里的demo未使用集群模式,不影響測試效果
- 配置文件
#spring.redis.cluster.nodes= 192.168.2.10:9000,192.168.2.14:9001,192.168.2.13:9000
#spring.redis.cluster.max-redirects=3
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.timeout=5000ms
# Lettuce
# 連接池最大連接數(使用負值表示沒有限制)
spring.redis.lettuce.pool.max-active=8
# 連接池最大阻塞等待時間(使用負值表示沒有限制)
spring.redis.lettuce.pool.max-wait=1ms
# 連接池中的最大空閑連接
spring.redis.lettuce.pool.max-idle=10
# 連接池中的最小空閑連接
spring.redis.lettuce.pool.min-idle=8
# 關閉超時時間
spring.redis.lettuce.shutdown-timeout=100ms
#驅逐時間 初始化延遲時間 默認-1
#if (delay > 0L) 必須>時才初始化
(這句很重要,這個參數默認為-1,不>0L則驅逐任務不會生成,池中的idel max 等參數等于沒配置,不會生效。第二,如果是默認配置,即使生效也不會讀池中鏈接,更浪費資源!??!代碼見下邊引用)
#spring.redis.lettuce.pool.time-between-eviction-runs=1s
引用驅逐器代碼
BaseGenericObjectPool
final void startEvictor(long delay) {
Object var3 = this.evictionLock;
synchronized(this.evictionLock) {
EvictionTimer.cancel(this.evictor, this.evictorShutdownTimeoutMillis, TimeUnit.MILLISECONDS);
this.evictor = null;
this.evictionIterator = null;
if (delay > 0L) {
this.evictor = new BaseGenericObjectPool.Evictor();
EvictionTimer.schedule(this.evictor, delay, delay);
}
}
}
- 測試代碼
package com.sparrow.spring.boot;
import com.sparrow.spring.Application;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.types.RedisClientInfo;
import org.springframework.test.context.junit4.SpringRunner;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {Application.class})
public class RedisConnectionTest {
@Autowired
private RedisTemplate redisTemplate;
@Test
public void test() throws IOException {
List<RedisClientInfo> redisClientInfos = redisTemplate.getClientList();
List<RedisConnection> redisConnections = new ArrayList<>();
//模擬多線程訪問場景,如果按lettuce 的邏輯,這里應該每個鏈接都會對應一個socket 鏈接?
for (int i = 0; i < 200; i++) {
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
redisTemplate.opsForValue().get("a" + System.currentTimeMillis());
}
}
}).start();
}
System.in.read();
}
}
- 執行后的redis client list結果
只有兩條鏈接(fd),其中一條為當前client
localhost:0>client list
"id=36 addr=127.0.0.1:57381 fd=16 name= age=20394 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client
id=250 addr=127.0.0.1:63325 fd=14 name= age=18 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=get
"
localhost:0>
- 可見結果,spring boot redistemplate 不管開多少個線程,都只有一個鏈接,為什么?
摘自LettuceConnectionFactory官方代碼注釋
This factory creates a new @link LettuceConnection on each call to @link #getConnection(). Multiple @link LettuceConnection's share a single thread-safe native connection by default.
當我們調用getConnection()方法獲取鏈接時,多個LettuceConnection會共享一個線程安全的 native connection(默認情況,意思就是說可以重寫,可以改)
/**
* Connection factory creating <a >Lettuce</a>-based connections.
* <p>
* This factory creates a new {@link LettuceConnection} on each call to {@link #getConnection()}. Multiple
* {@link LettuceConnection}s share a single thread-safe native connection by default.
當我們調用getConnection()方法獲取鏈接時,多個LettuceConnection會共享一個線程安全的`native connection`
* <p>
* The shared native connection is never closed by {@link LettuceConnection}, therefore it is not validated by default
* on {@link #getConnection()}. Use {@link #setValidateConnection(boolean)} to change this behavior if necessary. Inject
* a {@link Pool} to pool dedicated connections. If shareNativeConnection is true, the pool will be used to select a
* connection for blocking and tx operations only, which should not share a connection. If native connection sharing is
* disabled, the selected connection will be used for all operations.
* <p>
* ....
*/
public class LettuceConnectionFactory
implements InitializingBean, DisposableBean, RedisConnectionFactory, ReactiveRedisConnectionFactory {
總結
- redis 長鏈接情況下,物理鏈接非常少,甚至可以共享一個,而并不影響并發效果,線上壓測結果 qps 40000左右,CPU不到2%,當然測試效果與實際key有關,但5W左右是沒有問題的
- redis 的client list 命令和info clients 命令可以查看當前服務器的tcp鏈接數
- tcp 物理鏈接與netty的channel一一對應,可以簡單理解netty的channel 是對tcp鏈接的封裝,里邊實現了很多事。具體可以參考,李林峰老師的《netty權威指南》
- channel可以被多線程共享
- tcp鏈接與fd一一對應,受內核的最大fd數限制,但非1024限制,該值可以修改,與內存有關,理論上不受限。