营销型企业网站建设哪家好,cetos做网站,网页设计师需要掌握的领域,彩票网站怎么做ip管理参考资料
https://blog.csdn.net/TZ845195485/article/details/119745735
Redis单线程和多线程问题的背景
Redis里程碑版本迭代 Redis的单线程 主要是指Redis的网络IO和键值对读写是由一个线程来完成的#xff0c;Redis在处理客户端的请求时包括获取#xff08;socket读Redis在处理客户端的请求时包括获取socket读、解析、执行、内容返回socket写等都由一个顺序串行的主线程处理这就是所谓的“单线程”。这也是Redis对外提供键值存储服务的主要流程。 但Redis的其他功能比如持久化RDB、AOF、异步删除、集群数据同步等等其实是由额外的线程执行的。 Redis命令工作线程是单线程的但是整个Redis来说是多线程的 Redis为什么选择单线程
基于内存操作Redis的所有数据都存在内存中因此所有的运算都是内存级别的性能比较高数据结构简单Rdis的数据结构是专门设计的而这些简单的数据结构的查找和操作的时间大部分复杂度都是O(1)因此性能比较高多路复用和非阻塞I/ORedis使用I/O多路复用功能来监听多个socket连接客户端这样就可以使用一个线程连接来处理多个请求减少线程切换带来的开销避免了IO阻塞操作避免上下文功换因为是单线程模型因此就避免了不必要的上下文切换和多线程竞争这就省去了多线程切换带来的时间和性能上的消耗
Redis为什么开始利用多核 https://redis.io/docs/getting-started/faq/#how-can-redis-use-multiple-cpus-or-cores It’s not very frequent that CPU becomes your bottleneck with Redis, as usually Redis is either memory or network bound. As of version 4.0, Redis has started implementing threaded actions. For now this is limited to deleting objects in the background and blocking commands implemented via Redis modules. For subsequent releases, the plan is to make Redis more and more threaded. 使用多核的必要性 Redis4.0引入异步删除在删除大key时单线程会出现阻塞类似synchronized锁 unlink key
flushdb async
flushall async利用惰性删除避免阻塞。因为Redis是单个主线程处理而lazy free的本质就是把某些cost主要时间复制度占用主线程cpu时间片较高删除操作从redis主线程剥离让bio子线程来处理极大地减少主线阻塞时间。从而减少删除导致性能和稳定性问题。
Redis当前的性能瓶颈 redis主要的性能瓶颈是内存或者网络IO而不太可能是cpu 虽然有些命令操作可以用后台线程或子进程执行比如数据删除、快照生成、AOF重写。但是从网络IO处理到实际的读写命令处理都是由单个线程完成的。 单个主线程处理网络请求的速度跟不上底层网络硬件的速度为了应对这个问题采用多个IO线程来处理网络请求提高网络请求处理的并行度Rdis6/7就是采用的这种方法。 但是Redis的多IO线程只是用来处理网络请求的对于读写操作命令Redis仍然使用单线程来处理。这是因为Redis处理请求时网络处理经常是瓶颈通过多个IO线程并行处理网络操作可以提升实例的整体处理性能。而继续使用单线程执行命令操作就不用为了保证Lua脚本、事务的原子性额外开发多线程互斥加锁机制这样一来Redis线程模型实现就简单了
Redis的主线程如何和IO线程协同
既然瓶颈是网络IORedis的主线程如何和IO线程协同
服务端和客户端建立Socket连接并分配处理线程。首先主线程负责接收建立连接请求。当有客户端请求和实例建立Socket连接时主线程会创健和客户的连接并把Socket放入全局等待队列中。紧接看主线程通过轮询方法把Socket连接分配给IO线程IO线程读取并解折请求。主线程一旦把Socket分配给IO线程就会进入阻塞状态等待IO线程完成客户端请求读取和解析。因为有多个IO线程在并行处理所似这个过程很快完成主线程执行请求操作。等IO线程解析完请求主线程以单线程的方式执行这些命令操作。IO线程回写Socket和主当主线程清空全局队列。主线程执行完请求操作后会把需要返回的结果写入缓冲区然后主线程会阻塞等待IO线程把这些结果回写到Socket中多个线程在并发执行并返回给客户端。等到lO线程回写Socket完毕主线程会清空全局队列等待客户端的后续请求。 IO多路复用的理解
linux的5种IO模型的理解 当用户进程调用了select那么整个进程会被block而同时kernel会“监视”所有select负责的socket当任何一个socket中的数据准备好了select就会返回。这个时候用户进程再调用read操作将数据从kernel拷贝到用户进程。 IO多路复用的解释即使用一个或一组线程处理多个tcp连接
IO指代网络IO需要进行模态转换的读写操作多路多个客户端连接socket复用复用一个或多个线程、
一个服务端进程可以同时处理多个socketIO多路复用模型包括selectpollepoll
在单个线程通过记录跟踪每一个SockekI/O流的状态来同时管理多个I/O流。一个服务端进程可以同时处理多个套接字描述符。目的是尽量多的提高服务器的吞吐能力。 将用户socket对应的文件描述符FileDescriptor注册进epoll然后epoll监听哪些socket上有消息到达这样就避免了大量的无用操作。此时的socket应该采用非阻塞模式。这样整个过程只在调用select.、poll、epoll这些调用的时候才会阻塞收发客户消息是不会阻塞的整个进程或者线程就被充分利用起来这就是事件驱动所谓的reactor模式。 因此IO多路复用epoll也是Redis速度快的根本保证。redis的多线程即
单个工作线程保证线程安全和高性能多个IO线程解决网络IO的问题 在redis6/7中IO多路复用默认是关闭的有2个相关的配置
################################ THREADED I/O ################################## Redis is mostly single threaded, however there are certain threaded
# operations such as UNLINK, slow I/O accesses and other things that are
# performed on side threads.
#
# Now it is also possible to handle Redis clients socket reads and writes
# in different I/O threads. Since especially writing is so slow, normally
# Redis users use pipelining in order to speed up the Redis performances per
# core, and spawn multiple instances in order to scale more. Using I/O
# threads it is possible to easily speedup two times Redis without resorting
# to pipelining nor sharding of the instance.
#
# By default threading is disabled, we suggest enabling it only in machines
# that have at least 4 or more cores, leaving at least one spare core.
# Using more than 8 threads is unlikely to help much. We also recommend using
# threaded I/O only if you actually have performance problems, with Redis
# instances being able to use a quite big percentage of CPU time, otherwise
# there is no point in using this feature.
#
# So for instance if you have a four cores boxes, try to use 2 or 3 I/O
# threads, if you have a 8 cores, try to use 6 threads. In order to
# enable I/O threads use the following configuration directive:
#
# io-threads 4
#
# Setting io-threads to 1 will just use the main thread as usual.
# When I/O threads are enabled, we only use threads for writes, that is
# to thread the write(2) syscall and transfer the client buffers to the
# socket. However it is also possible to enable threading of reads and
# protocol parsing using the following configuration directive, by setting
# it to yes:
#
# io-threads-do-reads no
#
# Usually threading reads doesnt help much.
#
# NOTE 1: This configuration directive cannot be changed at runtime via
# CONFIG SET. Also, this feature currently does not work when SSL is
# enabled.
#
# NOTE 2: If you want to test the Redis speedup using redis-benchmark, make
# sure you also run the benchmark itself in threaded mode, using the
# --threads option to match the number of Redis threads, otherwise youll not
# be able to notice the improvements.线程数不要超过机器的核心数
深入理解Redis的IO多路复用
Redis利用epoll来实现IO多路复用将连接信息和事件放到队列中依次放到文件事件分派器事件分派器将事件分发给事件处理器。 Rdis是跑在单线程中的所有的操作都是按照顺序线性执行的但是由于读写操作等待用户输入或输出都是阻塞的所以I/O操作在一般情况下往往不能直接返回这会导致某一文件的I/O阻塞导致整个进程无法对其它客户提供服务而I/O多路复用就是为了解决这个问题而出现
所谓I/O多路复用机制就是说通过一种机制可以监视多个描述符一旦某个描述符就绪一般是读就绪或写就绪)能够通知程序进行相应的读写操作。这种机制的使用需要select、pol川、epol来配合。多个连接共用一个阻塞对象应用程序只需要在一个阻塞对象上等待无需阻塞等待所有连接。当某条连接有新的数据可以处理时操作系统通知应用程序线程从阻塞状态返回开始进行业务处理。
Redis服务采用Reactor的方式来实现文件事件处理器每一个网络连接其实都对应一个文件描述符
Redis基于Reactor模式开发了网络事件处理器这个处理器被称为文件事件处理器。它的组成结构为4部分
多个套接字IO多路复用程序文件事件分派器事件处理器。
因为文件事件分派器队列的消费是单线程的所汉Reds才叫单线程模型
解释为什么从bionio到io多路复用
在bio场景下当client1请求并和server建立连接后server在accept和read处发生阻塞此时新的client2建立连接的请求无法被server处理处于阻塞中
注意这里的应用程序指代的是server端
如何解决服务器主线程阻塞的问题
考虑使用子线程来处理新的客户端连接和请求。在连接socket后创建线程来处理read操作
client代码
Socket socket new Scoket(127.0.0.1,6379);
OutputStream outputStream socket.getOUtputStream();
// 获取标准输入到msg
outputStream.write(msg.getBytes());server代码
ServerSocket serverSocket new ServerSocket(6379);
while(true){Socket socket serverSocket.accept(); // 阻塞等待连接new Thread(() - {try{InputStream inputStream socket.getInputStream();// 从input中获取输入阻塞在读请求socket.close();}},Thread.currentThread().getName()).start();
}如何解决多线程资源浪费的问题
多线程模型要求每到来一个请求就切换到内核态创建对应的的进程。新的进程和上下文切换都会产生性能的损耗
池化技术但是实际的线程数量未知NIO使read方法不阻塞
NIO的特点是使用轮询替代阻塞 在客户端如果kernal中的数据没有准备好则会返回error
在NIO中一切都是非阻塞的
accept方法非阻塞如果没有客户端连接则返回无连接标识read方法非阻塞如果read读不到数据返回空间中标识有数据则只阻塞read读取数据的时间nio中服务端只有一个线程对socker数组进行遍历
客户端代码和bio下相同
Socket socket new Scoket(127.0.0.1,6379);
OutputStream outputStream socket.getOUtputStream();
// 获取标准输入到msg
outputStream.write(msg.getBytes());服务端代码
通过ServerSocketChannel的configureBlocking(false)方法将获得连接设置为非阻塞的。此时若没有连接accept会返回null通过SocketChannel的configureBlocking(false)方法将从通道中读取数据设置为非阻塞的。若此时通道中没有数据可读read会返回-1
static ArrayListSocketChannel socketList new ArrayList(); // 容纳socket数组
static ByteBuffer byteBuffer ByteBuffer.allocate(1024); // 输入缓冲ServerSocketChannel serverSocket new ServerSocketChannel.open();
serverSocket.bind(new InetSocketAddress(127.0.0.1,6379));
serverSocket.configureBlocking(false); // jdk1.4开始兼容阻塞模式此处设置为非阻塞while(true){for (SocketChannel element : socketList){int read element.read(byteBuffer); //不会阻塞if(read 0){byteBuffer.flip();byte[] bytes new byte[read];byteBuffer.get(byte);byteBuffer.clear();}}SocketChannel socketChannel serverSocket.accpet(); // 不会阻塞if(socketChannel ! null){sockerChannel.configureBlocking(false);socketList.add(socketChannel) // 连接成功}
}NIO模型存在的问题
socketlist遍历存在性能瓶颈空socket仍旧会轮询cpu不友好对socket状态的判断仍旧需要切换模态查询socket仍旧是通过read syscall开销较大、轮询会不断询问内核占用了大量cpu时间系统资源利用率较低
如何解决NIO模型cpu不友好的问题
如果通过linux内核完成上述工作遍历socket不需要模态切换内核本身是非阻塞的。
多路复用就是使用一个信道传输多个信号多个网络连接使用一个线程多个socket使用一个通道是在内核和驱动层实现的
IO多路复用模型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aRXsM3LX-1687616915979)(null)]
基于I/O复用模型多个连接共用一个阻塞对象应用程序只需要在一个阻塞对象上等待无需阻塞等待所有连接。当某条连接有新的数据可以处理时操作系统通知应用程序线程从阻塞状态返回开始进行业务处理。
Reactor模式是指通过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式。服务端程序处理传入多路请求并将它们同步分派给请求对应的处理线程Reactor模式也叫Dispatcher模式。即I/O多了复用统一监听事件收到事件后分发Dispatch给某进程是编写高性能网络服务器的必备技术。Reactor模式中有2个关键组成
ReactorReactor在一个单独的线程中运行负贵监听和分发事件分发给适当的处理程序来对IO事件做出反应。它就像公司的电话接线员它接听来自客户的电话并将线路转移到适当的联系人Handlers处理程序执行I/O事件要完成的实际事件类似于客户想要与之交谈的公司中的实际办理人。Reactor通过调度适当的处理程序来响应I/O事件处理程序执行非阻塞操作 IO多路复用的具体实现
select方法 select函数监视的文件描述符分3类分别是readfds、writefds和exceptfds将用户传入的数组拷贝到内核空间 调用后select函数会阻塞直到有描述符就绪有数据可读、可写、或者有except或超时timeout指定等待时间如果立即返回设为null即可函数返回 当select函数返回后可以通过遍历fdset来找到就绪的描述符。
分析select函数的执行流程
select是一个阻塞函数当没有数据时会一直阻塞在select调用当有数据时会将rset中对应的位置为1selecti函数返回不再阻塞遍历文件描述符数组判断哪个fd被置位读取数据然后处理
下面的示例代码 实际是将之前java示例的代码从用户态搬到内核态运行 将fd拷贝到内核态避免模态切换
sockfd socket(AF_INET,SOCK_STREAM,0);
memse(addr, 0, sizeof(addr));
addr.sin_family AF_INET;
addr.sin_port htons(2000);
addr.sin_addr.s_addr INADDR_ANY;
bind(sockfd,(struct sockaddr*)addr, sizeof(addr));
listen (sockfd, 5);
for(i0;i5;i){// 模拟5个客户端连接memset(client,0,sizeof (client)); addrlen sizeof(client);fds[i] accept(sockfd, (struct sockaddr*)client, addrlen);if(fds[i]max)max fds[i]; //找到一个最大的文件描述符在下面select中限制范围
}while(1){FD_ZERO(rset); // 重置rset为0for(i0;i5;i){FD_SET(fds[i],rset); // rset是个bitmap.如果5个文件描述符分别是1,2,4,5,7,那么这个bitmap为01101101}puts(round again); select(max1,rset,NULL,NULL,NULL); // select是一个系统调用,它会堵塞直到有数据发送到socket,select会把rset相应的位置置位,但并不会返回哪个socket有数据for(i0;i5;i){// 用户态只要遍历rst,看哪一位被置位了,不需要每次调用系统调用来判断了,效率有很大提升,遍历到被置位的文件描述符就进行读取if (FD_ISSET(fds[i],rset)){memset(buffer, 0, MAXBUF);read(fds[i], buffer, MAXBUF);puts(buffer);}
}select函数的缺点
bitmap默认大小为1024代码实现较早1983年虽然可以调整但还是有限度的rset每次循环都必须重新置位为0不可重复使用尽管将rset从用户态拷贝到内核态由内核态判断是否有数据但是还是有拷贝的开销当有数据时select就会返回但是selecti函数并不知道哪个文件描述符有数据了仅仅置位。后面还需要再次对文件描述符数组进行遍历效率比较低
poll方法
函数原型
int poll(struct pollfd *fds,nfds_t nfds,int timeout);
struct pollfdint fd; //file descriptor short events; // 读或者写事件short revents; // 文件描述符有事件则置1
};poll的执行流程:
将五个fd从用户态拷贝到内核态poll为阻塞方法执行poll方法如果有数据会将fd对应的revents置为POLLINpoll方法返回循环遍历查找哪个fd被置位为POLLIN了将revents重置为0便于复用对置位的fd进行读取和处理
解决的问题
不在使用bitmap消除了client的大小限制1024rset不可复用的问题每次置0
代码实现
for(10;15;1) //模拟5个客户端连接
{memset(client,0,sizeof (client));addrlen sizeof(client);pollfds[i].fd accept(sockfd,(struct sockaddr)client,addr);pollfds[i].events POLLIN; //这5个socket只读事件
}c
sleep(1);
while(1){puts(round again);poll(po11fds,5,50000); // poll中传入pollfds数组,交给内核判断是否有事件发生,如果哪个发生事件则revents置1for(10; 15; 1){ //遍历数组,找到哪个pollfd有事件发生if (pollfds[i].revents POLLIN){pollfds[i].reventsO; // 找到后revents置0memset(buffer,0,MAXBUF);read(pollfds[i].fd,buffer,MAXBUF); //读取数据puts(buffer);}
}缺点
本质上还是select方法拷贝fd到内核态仍旧存在开销仍旧需要遍历socketO(n)复杂度
epoll方法
函数原型
// 创建一个epoll的句柄size表示内核监听的数目。并非最大限制而是内核初始分配数据结构的建议值
int epoll_create(int size); // 向内核添加修改或删除要监控的fd
int epoll_ctl(int epfd, // epfd是epoll_create的返回值1nt op, // 表示op操作,用三个宏来表示:添加EPOLL.CTL_ADD,除EPOLL.CTL_DEL,修改EPOLL.CTL_MODint fd, //fd:是需要监听的fd(文件描述符)struct epoll_event *event // 告诉内核要监听什么事
);
struct epoll_event// events可以是以下几个宏的熊合:// EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭);// EPOLLOUT:表示对应的文件描述符可以写;__uint32_t events;epoll_data_t data; // User data variable
}// 类似select调用
int epoll_wait(int epfd,struct epoll event *events,int maxevents, // 最多返回的事件数int timeout
);具体实现
socket生命周期只会从用户态到内核态拷贝一次使用event通知级值每次socket有数据会主动通知内核加入就绪列表不需要遍历所有socket
struct epoll_event events[5];
int epfd epoll_create(10); // epoll_create在内核开辟一块内存空间,用来存放epolle中fd的数据结构for(i0;i5;i) { //模拟S个客户端连接static struct epoll_event ev; // epoll中fd的数据结构和polI中的差不多,只是没有了reventsmemset(client,0,sizeof (client));addrlen sizeof(client); ev.data.fd accept(sockfd,(struct sockaddrclient,addrlen);ev.events EPOLLIN;epoll_ctl(epfd,EPOLL_CTL_ADD,ev.data.fd,ev); // epoll ctl把每-个socket的fd数据结构放到epoll_create创建的内存空间中
}while(1){puts(round again);nfds epoll_wait(epfd,events,5,1O0O0); // epoll_wait阻塞,只有当epoll_create中创建的内存空间中的fd有事件发生,才会把这些fd放入就绪链表中,返回就绪fd的个数for(i0;infds;i){ //遍历就绪链表,读取数据memset(buffer,0,MAXBUF);read(events[i].data.fd, buffer, MAXBUF);puts(buffer);}
}三个方法的对比
不同操作系统选择的方式不同windows没有epoll默认为select
selectpollepoll操作方式遍历遍历回调数据结构bitmap数组红黑树最大连接数1024或2048x64无限无限最大支持fd数无限6553565535fd跨态拷贝每次select调用都需要拷贝每次poll调用都需要拷贝首次调用epoll_ctl拷贝工作效率线性遍历O(n)线性遍历O(n)事件通知O(1)
elasticache7的增强IO Amazon ElastiCache for Redis 7 新增功能通过增强型 I/O 多路复用将吞吐量提升高达 72% 使用 ElastiCache for Redis 7 时将会免费自动提供增强型 I/O 多路复用功能 采用增强型 I/O 后引擎将专注于处理命令的运行而非处理网络连接。不再为每个客户端打开一个通道而是如下图所示由每个增强型 I/O 线程使用 ElastiCache for Redis 引擎将命令组合在单个通道中