redis,memcached,nginx网路组件

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: redis,memcached,nginx网路组件

⽹络编程关注的问题

连接建⽴

分为两种:服务端处理接收客户端的连接;服务端作为客户端 连接第三方服务;

int clientfd = accept(listenfd, addr, sz);
// 举例为非阻塞io,阻塞io成功直接返回0;
int connectfd = socket(AF_INET, SOCK_STREAM, 0);
int ret = connect(connectfd, (struct sockaddr
*)&addr, sizeof(addr));
// ret == -1 && errno == EINPROGRESS 正在建立连接
// ret == -1 && errno = EISCONN 连接建立成功

连接断开

分为两种:主动断开和被动断开;

// 主动关闭
close(fd);
shutdown(fd, SHUT_RDWR);
// 主动关闭本地读端,对端写端关闭
shutdown(fd, SHUT_RD);
// 主动关闭本地写端,对端读端关闭
shutdown(fd, SHUT_WR);
// 被动:读端关闭
// 有的网络编程需要支持半关闭状态
int n = read(fd, buf, sz);
if (n == 0) {
    close_read(fd);
    // write()
    // close(fd);
}
// 被动:写端关闭
int n = write(fd, buf, sz);
if (n == -1 && errno == EPIPE) {
    close_write(fd);
    // close(fd);
}

消息到达

从读缓冲区中读取数据;

int n = read(fd, buf, sz);
if (n < 0) { // n == -1
    if (errno == EINTR || errno == EWOULDBLOCK)
        break;
    close(fd);
} else if (n == 0) {
    close(fd);
} else {
    // 处理 buf
}

消息发送完毕

往写缓冲区中写数据;

int n = write(fd, buf, dz);
if (n == -1) {
    if (errno == EINTR || errno == EWOULDBLOCK) {
        return;
   }
    close(fd);
}

网络 IO 职责

检测 IO

io 函数本身可以检测 io 的状态;但是只能检测一个 fd 对应的 状态;io 多路复用可以同时检测多个 io 的状态;区别是:io函 数可以检测具体状态;io 多路复用只能检测出可读、可写、错 误、断开等笼统的事件;

操作 IO

只能使用 io 函数来进行操作;分为两种操作方式:阻塞 io 和非 阻塞 io;

readbuffer里面有数据就read出去 , writebuffer里面没有数据就write进去。

⽹络编程流程

阻塞io模型和⾮阻塞io模型

阻塞在哪⾥? 阻塞在⽹络线程

什么来决定阻塞还是⾮阻塞?

连接的fd阻塞属性决定了 io 函数是否阻塞;

fcntl(c->fd, F_SETFL, O_NONBLOCK);

// 默认情况下,fd 是阻塞的,设置非阻塞的方法如下;

int flag = fcntl(fd, F_GETFL, 0);

fcntl(fd, F_SETFL, flag | O_NONBLOCK);

io函数  read,wrirte ,recv , send

具体的差异?

io函数在数据未就绪时是否⽴刻返回

非阻塞IO处理方式

连接建立

connect               第一次返回-1          errno= EINPROGRESS(正在建立)

                            多次调用                                  EISCONN (已经建立)

listen(fd , backlog)

accept                                          int fd =  accept(listenfd , addr , len);

                                                  客户端fd                          客户端ip和端口

accept两个功能: 1  检测全连接队列里面是否有未处理的连接信息。(检测IO)

                             2  如果全连接里面有节点就会返回connfd和客户端五元组信息(操作IO)

连接断开

主动       1          close   关闭读端和写端

             2           shutdown                 shutdown(fd, SHUT_RDWR);

                                                           // 主动关闭本地读端,对端写端关闭

                                                           shutdown(fd, SHUT_RD);

                                                            // 主动关闭本地写端,对端读端关闭

                                                            shutdown(fd, SHUT_WR);

被动

                   read=0     连接断开  (服务端的读端关闭了)

                   write=-1  && errno = EPIPE (写端关闭了)

数据到达

read = -1 && EWOULDBLOCK     // 没有数据

                     EINTR                    // 被中断打断了

消息发送

write = -1 && EWOULDBLOCK   // 写缓冲区满

                     EINTR                    // 被中断打断

io函数只能检测一条连接的就绪状态以及操作一条连接的io数据

阻塞io模型 + 多线程

每⼀个线程处理⼀个 fd 连接 bio

优点:处理及时

缺点:线程利⽤率很低,线程的数量是有限的(如果是非阻塞io,cpu利用率低)

io多路复⽤(⽹络线程)

⽤⼀个线程来检测多个io的就绪状态,不会去操作具体io

⽔平触发的时候,io函数既可以是阻塞的也可以是⾮阻塞的。

边缘触发的时候,io函数只能是⾮阻塞的。

epoll基础

重要数据结构

struct eventpoll {
 // ...
 struct rb_root rbr; // 管理 epoll 监听的事件
 struct list_head rdllist; // 保存着 epoll_wait 返回满⾜条件的事件
 // ...
};
struct epitem {
 // ...
 struct rb_node rbn; // 红⿊树节点
 struct list_head rdllist; // 双向链表节点
 struct epoll_filefd ffd; // 事件句柄信息
 struct eventpoll *ep; // 指向所属的eventpoll对象
 struct epoll_event event; // 注册的事件类型
 // ...
};
struct epoll_event {
 __uint32_t events;
 epoll_data_t data; // 保存 关联数据
};
typedef union epoll_data {
 void *ptr;
 int fd;
 uint32_t u32;
 uint64_t u64;
}epoll_data_t;

主要函数

epoll_create系统调⽤

int epoll_create(int size);

size参数告诉内核这个epoll对象会处理的事件⼤致数量,⽽不是能够处理的事件的最⼤数。 在现在linux版本中,这个size参数已经没有意义了; 返回:epoll对象句柄;之后针对该epoll的操作需要通过该句柄来标识该epoll对象;

epoll_ctl系统调⽤

int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);

epoll_ctl向epoll对象添加、修改或删除事件; 返回:0表示成功,-1表示错误,根据errno错误码判断错误类型。

op类型: EPOLL_CTL_ADD 添加新的事件到epoll中 、EPOLL_CTL_MOD 修改epoll中的事件 EPOLL_CTL_DEL 删除epoll中的事件。

event.events 取值:

EPOLLIN 表示该连接上有数据可读(tcp连接远端主动关闭连接,也是可读事件,因为需要处理发送来的FIN包;FIN包就是read 返回 0)

EPOLLOUT 表示该连接上可写发送(主动向上游服务器发起⾮阻塞tcp连接,连接建⽴成功事件相当于可写事件)

EPOLLRDHUP 表示tcp连接的远端关闭或半关闭连接 (服务器的读端已经关闭了)

EPOLLPRI 表示连接上有紧急数据需要读

EPOLLERR 表示连接发⽣错误

EPOLLHUP 表示连接被挂起    (读写端全部关闭)

EPOLLET 将触发⽅式设置为边缘触发,系统默认为⽔平触发

EPOLLONESHOT 表示该事件只处理⼀次,下次需要处理时需重新加⼊epoll

epoll_wait系统调⽤

int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);

收集 epoll 监控的事件中已经发⽣的事件,如果 epoll 中没有任何⼀个事件发⽣,则最多等待 timeout 毫秒后返回。 返回:表示当前发⽣的事件个数,返回0表示本次没有事件发⽣; 返回-1表示出现错误,需要检查errno错误码判断错误类型。

注意:

events 这个数组必须在⽤户态分配内存,内核负责把就绪事件复制到该数组中; maxevents 表示本次可以返回的最⼤事件数⽬,⼀般设置为 events 数组的⻓度; timeout表示在没有检测到事件发⽣时最多等待的时间;如果设置为0,检测到rdllist为空⽴ 刻返回;如果设置为-1,⼀直等待

原理图

要点

所有添加到epoll中的事件都会与⽹卡驱动程序建⽴回调关系,相应的事件发⽣时会调⽤这⾥的回 调⽅法(ep_poll_callback),它会把这样的事件放在rdllist双向链表中。调用 epoll_wait 将会把 rdlist 中就绪事件拷贝到 用户态中;

reactor模型

组成:⾮阻塞的io + io多路复⽤;

io多路复用负责检测IO  

IO函数操作IO

为什么搭配非阻塞IO:

1  多线程环境 : 将一个listenfd交给多个epoll去处理  (如果是阻塞的IO那么其他的线程就会被阻塞住) (惊群)--->(用户层不需要处理枷锁问题,内核协议栈已经处理好了)

2  边缘触发下必须使用非阻塞IO:  读事件触发时read要再一次事件循环中把readbuffer读空

  如果此时readbuffer已经为空了,阻塞IO就会被阻塞住

3  select bug :  当某个socket接收缓冲区中有新数据分节到达,然后select报告这个socket描述符可读,但随后,协议栈检查到这个新分节检验和错误,然后丢弃这个分节,这时候调用read则无数据可读,如果socket没有被设置nonblocking ,此时read将阻塞当前线程。

是不是io多路复用一定要搭配非阻塞IO:

MYSQL:   select 接收数据,每条连接一个线程。

libevent:  可以加一个系统调用看看缓冲区多少数据,效率低,

                       int n = EVBUFFER_MAX_READ_DEFAULT;

                       if( ioctl(fd , FIONREAD , &n) < 0)  return -1;

                       return n;

                       取出读缓冲区字节数

特征:基于事件循环,以事件驱动或者事件回调的⽅式来实现业务逻辑;

表述:将连接的io处理转化为事件处理;

单reactor模型

原理图:

代表:redis 内存数据库    数据结构太复杂,锁的粒度不好选择,所以使用单线程

          redis 6.0 多线程

环境: 命令处理是单线程的      只能使用单reactor

redis为什么要使用单reactor :  1单线程业务逻辑   2 操作具体命令时间复杂度比较低

redis 针对reactor做了哪些优化:          read/decode 记录日志

                                                             write/encode 获取排行榜记录

因为解协议的时间特别长,占用单线程。

networking.c    IO多线程操作

单reactor模型 + 任务队列 + 线程池

原理图

代表:skynet

多reactor多线程

连接reactor处理connect然后将connfd通过pip负载均衡给不同线程的epoll管理

环境:  key/value  内存数据库   ,  命令处理多线程。

为什么使用: 数据结构简单,多线程处理效率高。 kv数据操作简单,更高程度并发处理业务

多进程

用户层进行处理:  master进程创建的时候会创建一个共享内存,fork多个worker进程共享同一个共享内存,把这个锁放到共享内存当中,多个worker进程去争夺这把锁,谁拿到这把锁,谁就可以处理listenfd读事件

负载均很做法:  每一个进程处理  7/8 * n 个连接,当连接数大于 7/8 * n 的时候这个worker就不会再加入连接了,交给其他wroker去处理。

多reactor + 消息队列 + 线程池

业务场景中⽐较多 ⽹络密集型 + 业务密集型

TCP状态转换图

epoll处理细节图

redis连接上游服务器时要设置EPOLLOUT事件。当上游服务器回第一个ACK的时候会触发EPOLLOUT

服务端主动断开的情况:服务端主动调用close()或者shutdown(SHUT_WR)都会主动发送一个fin给客户端。当write()==-1 && errno = EPIPE此时写通道关闭,但此时仍然可能收到数据(如果客户端支持版关闭状态的话)。

系统调用的时候线程不会切换,但是中断的优先级 > 系统调用,线程会被切换出去。当read() == -1 && errno == EWOULDBLOCK 或者 errno == EINTR时我们需要重试。

三次握⼿

四次挥⼿

主动断开方在收到第三次握手数据包的时候会进入time_wait状态并等待2个数据包的时间,为什么需要等待呢,是因为我们不确定我们的ack包是否发送到对端去了,如果这个包发送失败了。那么在2个数据包的时间内又会收到对端发送来的一个fin包。然后再发送ack。

客户端主动发送fin包之后关闭自身写端,服务端收到fin之后关闭读端,但是不关闭写端,此时服务器可能还需要发送一些数据包给客户端。发送完之后服务端再把自己的写端关闭。客户端收到fin包之后把自己的读端关闭并且等待两个数据包的时间。并发送一个ack包。

这个ack包对epoll来说是EPOLLHUP , server端收到之后就可以调用close将fd关闭。close(fd)只是将这个fd的引用-1了。内核中检测到没有数据引用这个fd了,内核才会将它释放掉。

半关闭状态

背景

客户端关闭写通道,此时服务端想要推送完所有数据后再关闭; 推送系统中 close_read() close_write() close() 服务端 close_read send send ... close 客户端 close()、

实现

需要实现半关闭状态;close-wait阶段;必须要收到ack包,才能知道客户端收到了我们推送的所 有数据;

细节

发送端: shutdown(SHUT_WR) 发送⼀个 FIN 包,并且标记该 socket 为 SEND_SHUTDOWN; shutdown(SHUT_RD) 不发送任何包,但是标记该 socket 为 RCV_SHUTDOWN;

接收端: 收到 FIN 包标记该 socket 为RCV_SHUTDOWN; 对于epoll⽽⾔,如果⼀个socket同时标记为 SEND_SHUTDOWN 和 RCV_SHUTDOWN;那么poll 会返回 EPOLLHUP; 如果⼀个socket被标记为 RCV_SHUTDOWN;poll会返回 EPOLLRDHUP;

应⽤:skynet ⽀持半关闭状态

tcp-keepalive

背景

tcp是⾯向连接的,⼀般情况下,两端应⽤可以通过发送接收数据得知对端的存活;当两端都没有 数据的时候,如何判断连接是否正常?系统默认keepalive是关闭的,当keepalive开启时,可以保 持连接检测对⽅主机是否崩溃;

属性

1. tcp_keepalive_time 两端多久没有数据交换,开始发送 keepalive syn 探活包;

2. tcp_keepalive_probes 发送多少次探活包  (如果发送三次都没有收到ack则说明断开了)

3. tcp_keepalive_intvl 探活包发送间隔

这是在传输层,应用层无法感知,只有当read() == -1 && errno == ETIMEOUT的时候,我们就可以close了。

如何开启

linux全局

/etc/sysctl.conf

net.ipv4.tcp_keepalive_time=7200

net.ipv4.tcp_keepalive_intvl=75

net.ipv4.tcp_keepalive_probes=9

sysctl -p 使其⽣效

没有配置开启

单个连接

可以使⽤三个属性设置:TCP_KEEPCNT、TCP_KEEPIDLE、TCP_KEEPINTVL; 开启:SO_KEEPALIVE

redis源码

// 开启tcp-keepalive
int val = 1;
if (setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &val, sizeof(val)) == -1)
{
 anetSetError(err, "setsockopt SO_KEEPALIVE: %s", strerror(errno));
 return ANET_ERR;
}
// 设置
val = interval;
if (setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &val, sizeof(val)) < 0) {
 anetSetError(err, "setsockopt TCP_KEEPIDLE: %s\n", strerror(errno));
 return ANET_ERR;
}
/* Send next probes after the specified interval. Note that we set the
 * delay as interval / 3, as we send three probes before detecting
 * an error (see the next setsockopt call). */
val = interval/3;
if (val == 0) val = 1;
if (setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &val, sizeof(val)) < 0) {
 anetSetError(err, "setsockopt TCP_KEEPINTVL: %s\n", strerror(errno));
 return ANET_ERR;
}
/* Consider the socket in error state after three we send three ACK
 * probes without getting a reply. */
val = 3;
if (setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &val, sizeof(val)) < 0) {
 anetSetError(err, "setsockopt TCP_KEEPCNT: %s\n", strerror(errno));
 return ANET_ERR;
}

为什么应⽤层需要开启⼼跳检测?

因为传输层探活检测,⽆法判断进程阻塞或者死锁的情况;

⼼跳检测: 每隔10秒发送⼀次⼼跳包 3次没有收到 close

应⽤

1. 数据库间,主从复制,使⽤⼼跳检测;

2. 客户端与服务器,使⽤⼼跳检测;

3. 客户端->反向代理->上游服务器;反向代理与上游服务器使⽤探活检测;

4. 服务端->数据库,使⽤探活检测;对于数据库⽽⾔,服务端是否阻塞跟它⽆关;

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
3月前
|
缓存 NoSQL 应用服务中间件
2.2.2 redis,memcached,nginx网络组件
2.2.2 redis,memcached,nginx网络组件
|
1月前
|
NoSQL 关系型数据库 MySQL
Docker安装详细步骤及相关环境安装配置(mysql、jdk、redis、自己的私有仓库Gitlab 、C和C++环境以及Nginx服务代理)
Docker安装详细步骤及相关环境安装配置(mysql、jdk、redis、自己的私有仓库Gitlab 、C和C++环境以及Nginx服务代理)
224 0
|
1月前
|
NoSQL Java 应用服务中间件
使用innoSetup将mysql+nginx+redis+jar包打包成windows安装包
使用innoSetup将mysql+nginx+redis+jar包打包成windows安装包
使用innoSetup将mysql+nginx+redis+jar包打包成windows安装包
|
2月前
|
缓存 NoSQL Redis
如何在Python中使用Redis或Memcached进行缓存?
如何在Python中使用Redis或Memcached进行缓存?
28 2
|
3月前
|
NoSQL Go Redis
Golang实现redis系列-(1)日志组件的封装
Golang实现redis系列-(1)日志组件的封装
31 0
|
3月前
|
缓存 NoSQL Java
flea-cache使用之整合Memcached和Redis接入
【1月更文挑战第3天】本篇博文介绍 flea框架下的 flea-cache 模块中 整合接入 Memcached 和 Redis
49 1
flea-cache使用之整合Memcached和Redis接入
|
26天前
|
运维 前端开发 应用服务中间件
LNMP详解(八)——Nginx动静分离实战配置
LNMP详解(八)——Nginx动静分离实战配置
28 0
|
25天前
|
前端开发 应用服务中间件 nginx
Nginx配置详解Docker部署Nginx使用Nginx部署vue前端项目
Nginx配置详解Docker部署Nginx使用Nginx部署vue前端项目
101 0