现象
通过http访问springboot服务时,时而正常,时而超时
背景
springboot webflux 开发
centos7
原因分析
1、排除网络问题
1 | tcpdump -n -i eth0 '((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0' and host 172.16.160.12 and host 172.16.247.230 and port 18092 -XX |
使用tcpdump抓包,发现 12:18:14.286806
收到请求,直到 12:40:33.668631
才将结果返回。证明网络链路是通的,但在近20分钟的时间里,服务究竟在干嘛?
1 | 12:18:14.286806 IP 172.16.160.12.50966 > 172.16.247.230.18092: Flags [P.], seq 200172037:200172579, ack 1033672370, win 16652, options [nop,nop,TS val 15683831 ecr 93358138], length 542 |
2、排除ipv4和ipv6双栈问题
只抓ipv4包
1 | tcpdump -nv -i eth0 '((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0' and host 172.16.160.12 and host 172.16.247.230 and port 18092 -XX |
只抓ipv6包
2、 strace查看系统调用
1 | jps -ml |
正常访问如下:
1 | 275 [pid 23135] 13:02:24 futex(0x7f87240f0854, FUTEX_WAIT_BITSET_PRIVATE, 1, {tv_sec=96308, tv_nsec=293387689}, 0xffffffff <unfinished ...> |
访问超时调用如下:
1 | 817 [pid 23147] 13:02:32 <... epoll_wait resumed>[{EPOLLIN, {u32=34, u64=34}}], 4096, -1) = 1 |
涉及线程
23135、23147、23167
1 | printf '%x %x %x \n' 23135 23147 23167 |
EAGAIN (Resource temporarily unavailable)
1 | accept4(34, 0x7f8714bfa220, [128], SOCK_CLOEXEC|SOCK_NONBLOCK) = -1 EAGAIN (Resource temporarily unavailable) |
在linux进行非阻塞的socket接收数据时经常出现Resource temporarily unavailable,errno代码为11(EAGAIN),这是什么意思?
这表明你在非阻塞模式下调用了阻塞操作,在该操作没有完成就返回这个错误,这个错误不会破坏socket的同步,不用管它,下次循环接着recv就可以。对非阻塞socket而言,EAGAIN不是一种错误。在VxWorks和Windows上,EAGAIN的名字叫做EWOULDBLOCK。
另外,如果出现EINTR即errno为4,错误描述Interrupted system call,操作也应该继续。
最后,如果recv的返回值为0,那表明连接已经断开,我们的接收操作也应该结束。
1 | strace -t -fp 23126 2>&1 | egrep 'accept|poll|read|write|connect|send|recv|ctl|sock' |
正常
1 | [root@appweb-docker-002 ~]# strace -t -fp 23126 2>&1 | grep -v 'futex' |
基础知识扫盲
strace命令
strace命令是一个集诊断、调试、统计与一体的工具,我们可以使用strace对应用的系统调用和信号传递的跟踪结果来对应用进行分析,以达到解决问题或者是了解应用工作过程的目的。当然strace与专业的调试工具比如说gdb之类的是没法相比的,因为它不是一个专业的调试器。
strace的最简单的用法就是执行一个指定的命令,在指定的命令结束之后它也就退出了。在命令执行的过程中,strace会记录和解析命令进程的所有系统调用以及这个进程所接收到的所有的信号值。
语法
1 | strace [ -dffhiqrtttTvxx ] [ -acolumn ] [ -eexpr ] ... |
选项
1 | -c 统计每一系统调用的所执行的时间,次数和出错的次数等. |
epoll使用详解
EPOLL 的API用来执行类似poll()的任务。能够用于检测在多个文件描述符中任何IO可用的情况。Epoll API可以用于边缘触发(edge-triggered)和水平触发(level-triggered), 同时epoll可以检测更多的文件描述符。以下的系统调用函数提供了创建和管理epoll实例:
- epoll_create() 可以创建一个epoll实例并返回相应的文件描述符(epoll_create1() 扩展了epoll_create() 的功能)。
- 注册相关的文件描述符使用epoll_ctl()
- epoll_wait() 可以用于等待IO事件。如果当前没有可用的事件,这个函数会阻塞调用线程。
epoll_create 创建epoll
1 | int epoll_create(int size); |
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
epoll_ctl 设置epoll事件
1 |
|
这个系统调用能够控制给定的文件描述符epfd\指向的epoll实例,op\是添加事件的类型,fd\是目标文件描述符。
有效的op值有以下几种:
- EPOLL_CTL_ADD 在epfd\中注册指定的fd文件描述符并能把event\和fd\关联起来。
- EPOLL_CTL_MOD 改变* fd*和*evetn*之间的联系。
- EPOLL_CTL_DEL 从指定的epfd\中删除fd\文件描述符。在这种模式中event\是被忽略的,并且为可以等于NULL。
event\这个参数是用于关联制定的fd\文件描述符的。它的定义如下:
1 | typedef union epoll_data { |
events\这个参数是一个字节的掩码构成的。下面是可以用的事件:
- EPOLLIN - 当关联的文件可以执行 read ()操作时。
- EPOLLOUT - 当关联的文件可以执行 write ()操作时。
- EPOLLRDHUP - (从 linux 2.6.17 开始)当socket关闭的时候,或者半关闭写段的(当使用边缘触发的时候,这个标识在写一些测试代码去检测关闭的时候特别好用)
- EPOLLPRI - 当 read ()能够读取紧急数据的时候。
- EPOLLERR - 当关联的文件发生错误的时候,epoll_wait() 总是会等待这个事件,并不是需要必须设置的标识。
- EPOLLHUP - 当指定的文件描述符被挂起的时候。epoll_wait() 总是会等待这个事件,并不是需要必须设置的标识。当socket从某一个地方读取数据的时候(管道或者socket),这个事件只是标识出这个已经读取到最后了(EOF)。所有的有效数据已经被读取完毕了,之后任何的读取都会返回0(EOF)。
- EPOLLET - 设置指定的文件描述符模式为边缘触发,默认的模式是水平触发。
- EPOLLONESHOT - (从 linux 2.6.17 开始)设置指定文件描述符为单次模式。这意味着,在设置后只会有一次从epoll_wait() 中捕获到事件,之后你必须要重新调用 epoll_ctl() 重新设置。
返回值:\如果成功,返回0。如果失败,会返回-1, errno\将会被设置
有以下几种错误:
- EBADF - epfd\ 或者 fd\ 是无效的文件描述符。
- EEXIST - op\是EPOLL_CTL_ADD,同时 fd\ 在之前,已经被注册到epoll中了。
- EINVAL - epfd\不是一个epoll描述符。或者fd\和epfd\相同,或者op\参数非法。
- ENOENT - op\是EPOLL_CTL_MOD或者EPOLL_CTL_DEL,但是fd\还没有被注册到epoll上。
- ENOMEM - 内存不足。
- EPERM - 目标的fd\不支持epoll。
epoll_wait 等待epoll事件
1 |
|
epoll_wait 这个系统调用是用来等待epfd\中的事件。events\指向调用者可以使用的事件的内存区域。maxevents\告知内核有多少个events,必须要大于0.
timeout\这个参数是用来制定epoll_wait 会阻塞多少毫秒,会一直阻塞到下面几种情况:
- 一个文件描述符触发了事件。
- 被一个信号处理函数打断,或者timeout超时。
当timeout\等于-1的时候这个函数会无限期的阻塞下去,当timeout\等于0的时候,就算没有任何事件,也会立刻返回。
struct epoll_event 如下定义:
1 | typedef union epoll_data { |
每次epoll_wait() 返回的时候,会包含用户在epoll_ctl中设置的events。
还有一个系统调用epoll_pwait ()。epoll_pwait()和epoll_wait ()的关系就像select()和 pselect()的关系。和pselect()一样,epoll_pwait()可以让应用程序安全的等待知道某一个文件描述符就绪或者捕捉到信号。
下面的 epoll_pwait () 调用:
1 | ready = epoll_pwait(epfd, &events, maxevents, timeout, &sigmask); |
在内部等同于:
1 | pthread_sigmask(SIG_SETMASK, &sigmask, &origmask); |
如果 sigmask\为NULL, epoll_pwait()等同于epoll_wait()。
返回值:\有多少个IO事件已经准备就绪。如果返回0说明没有IO事件就绪,而是timeout超时。遇到错误的时候,会返回-1,并设置 errno。
有以下几种错误:
- EBADF - epfd\是无效的文件描述符
- EFAULT - 指针events\指向的内存没有访问权限
- EINTR - 这个调用被信号打断。
- EINVAL - epfd\不是一个epoll的文件描述符,或者maxevents\小于等于0
关于ET、LT两种工作模式
边缘触发(edge-triggered 简称ET)和水平触发(level-triggered 简称LT):
epoll的事件派发接口可以运行在两种模式下:边缘触发(edge-triggered)和水平触发(level-triggered),两种模式的区别请看下面,我们先假设下面的情况:
- 一个代表管道读取的文件描述符已经注册到epoll实例上了。
- 在管道的写入端写入了2kb的数据。
- epoll_wait 返回一个可用的rfd文件描述符。
- 从管道读取了1kb的数据。
- 调用epoll_wait 完成。
如果rfd被设置了ET,在调用完第五步的epool_wait 后会被挂起,尽管在缓冲区还有可以读取的数据,同时另外一段的管道还在等待发送完毕的反馈。这是因为ET模式下只有文件描述符发生改变的时候,才会派发事件。所以第五步操作,可能会去等待已经存在缓冲区的数据。在上面的例子中,一个事件在第二步被创建,再第三步中被消耗,由于第四步中没有读取完缓冲区,第五步中的epoll_wait可能会一直被阻塞下去。
下面情况下推荐使用ET模式:
- 使用非阻塞的IO。
- epoll_wait() 只需要在read或者write返回的时候。
相比之下,当我们使用LT的时候(默认),epoll会比poll更简单更快速,而且我们可以使用在任何一个地方。