zero's Blog

持续迭代

转载自:https://blog.csdn.net/zhousenshan/article/details/78660017

  
  目前支持I/O多路复用的系统调用有 select,poll,epoll,I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
  但select,poll,epoll本质上都是同步I/O,因为它们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

1、select

  基本原理:select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、except)或者超时,函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。

  select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:
  1、select最大的缺陷就是 单个进程所打开的fd是有一定限制的,它由FD_SETSIZE设置,默认值是1024。具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048。
  2、对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低。当套接字比较多的时候,每次select()都要通过遍历 FD_SETSIZE 个Socket来完成调度,不管Socket是否活跃,都遍历一遍,这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当它们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。
  3、需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。

Read more »

对 VIRT 和 RES 两个区域做下解释:

  • VIRT(virtual memory usage):进程“需要的”虚拟内存大小,包括进程使用的库、代码、数据,以及malloc、new分配的堆空间和分配的栈空间等,另外 VIRT = SWAP + RES,其中 swap 区域是需要内存置换的区域(可参考操作系统虚拟内存原理)。
  • RES(resident memory usage):又称为 RSS,是进程在 RAM 中真正占用的内存大小。RES 包含了它所链接的动态库被加载到物理内存中的内存、栈内存和堆内存。动态链接库占用的内存会被多个进程共享,所以RSS并不能准确反映单进程的内存占用情况。
    Read more »

转载自:http://blog.csdn.net/kobejayandy/article/details/47128349

查看Linux系统默认的最大文件句柄数,系统默认是1024

$ ulimit -n

查看Linux系统某个进程打开的文件句柄数量

$ lsof -n | grep 5950 -c

修改Linux系统的最大文件句柄数限制的方法:

1)$ulimit -n 65535

针对当前session有效,用户退出或者系统重新后恢复默认值

2)修改profile文件:在profile文件中添加:ulimit -n 65535

只对单个用户有效

3)修改文件:/etc/security/limits.conf,在文件中添加:(立即生效-当前session中运行ulimit -a命令无法显示)

soft nofile 32768 #限制单个进程最大文件句柄数(到达此限制时系统报警)

hard nofile 65536 #限制单个进程最大文件句柄数(到达此限制时系统报错)

4)修改文件:/etc/sysctl.conf。在文件中添加:

fs.file-max=655350 #限制整个系统最大文件句柄数

运行命令:/sbin/sysctl -p 使配置生效

[toc]

1. 惊群效应

1.1 简介

惊群问题又名惊群效应。简单来说就是多个进程或者线程在等待同一个事件,当事件发生时,所有线程和进程都会被内核唤醒。唤醒后通常只有一个进程获得了该事件并进行处理,其他进程发现获取事件失败后又继续进入了等待状态,在一定程度上降低了系统性能。

打个比方就是:当你往一群鸽子中间扔一块食物,虽然最终只有一个鸽子抢到食物,但所有鸽子都会被惊动来争夺,没有抢到食物的鸽子只好回去继续睡觉, 等待下一块食物到来。这样,每扔一块食物,都会惊动所有的鸽子,即为惊群。

1.2 引发的问题

惊群效应会占用系统资源,降低系统性能。

多进程/线程的唤醒,涉及到的一个问题是上下文切换问题。频繁的上下文切换带来的一个问题是数据将频繁的在寄存器与运行队列中流转。极端情况下,时间更多的消耗在进程/线程的调度上,而不是执行。

2. 常见的惊群效应

在 Linux 下,我们常见的惊群效应发生于我们使用 accept 以及我们 select 、poll 或 epoll 等系统提供的 API 来处理我们的网络链接。

2.1 accept 惊群

以多进程为例,在主进程创建监听描述符listenfd后,fork()多个子进程,多个进程共享listenfd,accept是在每个子进程中,当一个新连接来的时候,会发生惊群。

由上图所示:

  1. 主线程创建了监听描述符listenfd = 3
  2. 主线程fork 三个子进程共享listenfd=3
  3. 当有新连接进来时,内核进行处理

在内核2.6之前,所有进程accept都会惊醒,但只有一个可以accept成功,其他返回EGAIN。

在内核2.6及之后,解决了惊群,在内核中增加了一个互斥等待变量。一个互斥等待的行为与睡眠基本类似,主要的不同点在于:

1.当一个等待队列入口有 WQ_FLAG_EXCLUSEVE 标志置位, 它被添加到等待队列的尾部. 没有这个标志的入口项, 相反, 添加到开始.

2.当 wake_up 被在一个等待队列上调用时, 它在唤醒第一个有 WQ_FLAG_EXCLUSIVE 标志的进程后停止。(内核只会唤醒一个用户进程 task 就会退出唤醒过程,从而不存在了”惊群”现象)
3.对于互斥等待的行为,比如如对一个listen后的socket描述符,多线程阻塞accept时,系统内核只会唤醒所有正在等待此事件的队列 的第一个,队列中的其他人则继续等待下一次事件的发生,这样就避免的多个线程同时监听同一个socket描述符时的惊群问题。

2.2 epoll惊群

epoll惊群分两种:

  • 在fork之前创建epollfd,所有进程共用一个epoll;
  • 在fork之后创建epollfd,每个进程独用一个epoll.

2.2.1 fork之前创建epollfd(内核2.6已解决)

  1. 主进程创建listenfd, 创建epollfd
  2. 主进程fork多个子进程
  3. 每个子进程把listenfd,加到epollfd中
  4. 当一个连接进来时,会触发epoll惊群,多个子进程的epoll同时会触发

分析:这里的epoll惊群跟accept惊群是类似的,共享一个epollfd, 加锁或标记解决。在新版本的epoll中已解决。但在内核2.6及之前是存在的。

2.2.2 fork之后创建epollfd(内核未解决)

  1. 主进程创建listendfd
  2. 主进程创建多个子进程
  3. 每个子进程创建自已的epollfd
  4. 每个子进程把listenfd加入到epollfd中
  5. 当一个连接进来时,会触发epoll惊群,多个子进程epoll同时会触发

分析:因为每个子进程的epoll是不同的epoll, 虽然listenfd是同一个,但新连接过来时, accept会触发惊群,但内核不知道该发给哪个监听进程,因为不是同一个epoll。所以这种惊群内核并没有处理。惊群还是会出现。

3. 内核解决惊群问题详解

首先如前面所说,Accept 的惊群问题在 Linux Kernel 2.6 之后就被从内核的层面上解决了。但是 EPOLL 怎么办?在 2016 年一月,Linux 之父 Linus 向内核提交了一个补丁,其中的关键代码是

1
2
3
4
if (epi->event.events & EPOLLEXCLUSIVE)
add_wait_queue_exclusive(whead, &pwq->wait);
else
add_wait_queue(whead, &pwq->wait);

简而言之,通过增加一个 EPOLLEXCLUSIVE 标志位作为辅助。如果用户开启了 EPOLLEXCLUSIVE ,那么在加入内核等待队列时,使用 add_wait_queue_exclusive 否则则使用 add_wait_queue。 EPOLLEXCLUSIVE 只保证唤醒的进程数小于等于我们开启的进程数,而不是直接唤醒所有进程,也不是只保证唤醒一个进程。

4. Nginx解决惊群效应

目前而言,应用解决惊群有两种策略

  1. 这是可以接受的代价,那么我们暂时不管。这是我们大多数的时候的策略
  2. 通过加锁或其余的手段来解决这个问题,最典型的例子是 Nginx

我们来看看 Nginx 怎么解决这样的问题的:
Nginx通过 控制争抢处理socket的进程数量抢占ngx_accept_mutex锁 解决惊群现象。只有一个ngx_accept_mutex锁,谁拿到锁,谁处理该socket的请求。

同时:如果当前进程的连接数>最大连接数*7/8,则该进程不参与本轮竞争。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
//nginx的每个worker进程在函数ngx_process_events_and_timers中处理事件。下面代码是ngx_process_events_and_timers()函数的核心部分。
void ngx_process_events_and_timers(ngx_cycle_t *cycle)
{
//ngx_use_accept_mutex表示是否需要通过对accept加锁来解决惊群问题。当nginx worker进程数>1时且配置文件中打开accept_mutex时,这个标志置为1
if (ngx_use_accept_mutex)
{
//ngx_accept_disabled表示此时满负荷,没必要再处理新连接了,nginx.conf配置了每一个nginx worker进程能够处理的最大连接数,当达到最大数的7/8时,ngx_accept_disabled为正,说明本nginx worker进程非常繁忙,将不再去处理新连接,这也是个简单的负载均衡
if (ngx_accept_disabled > 0)
{
ngx_accept_disabled--;
}
else
{
//工作进程抢占锁,抢占成功的进程将ngx_accept_mutex_held变量置为1。拿到锁,意味着socket被放到本进程的epoll中了,如果没有拿到锁,则socket会被从epoll中取出。
//此处trylock是非阻塞锁,如果没有抢占到锁,进程会立刻返回,处理自己监听的描述符上的读写事件。
if(pthread_mutex_trylock(&ngx_accept_mutex))
{
ngx_accept_mutex_held = 1;
}
else
{
//设置time时间,500ms后就去争抢锁,使得没有拿到锁的worker进程,去拿锁的频繁更高,确保每个进程可以处理几乎相同数量的fd的读写。
timer = 500;
ngx_accept_mutex_held = 0;
}

//拿到锁的话,置flag为NGX_POST_EVENTS,这意味着ngx_process_events函数中,任何事件都将延后处理,会把accept事件都放到ngx_posted_accept_events链表中,epollin|epollout事件都放到ngx_posted_events链表中
if (ngx_accept_mutex_held)
{
flags |= NGX_POST_EVENTS;
}
}
//继续epoll_wait等待处理事件
int num = epoll_wait(epollfd, events, length, timer);
for(int i=0; i<num; ++i)
{
......
//如果是读事件
if (revents & EPOLLIN)
{
//有NGX_POST_EVENTS标志的话,就把accept事件放到ngx_posted_accept_events队列中,把正常的事件放到ngx_posted_events队列中延迟处理
//新连接事件队列ngx_posted_accept_events
//用户读写事件队列ngx_posted_events
if (flags & NGX_POST_EVENTS)
{
queue = rev->accept ?
&ngx_posted_accept_events:
&ngx_posted_events;

ngx_post_event(rev, queue);
}
else//处理
{
rev->handler(rev);
}
}

//如果是写事件
if (revents & EPOLLOUT)
{
//同理,有NGX_POST_EVENTS标志的话,写事件延迟处理,放到ngx_posted_events队列中
if (flags & NGX_POST_EVENTS)
{
ngx_post_event(rev, &ngx_posted_events);
}
else//处理
{
rev->handler(rev);
}
}
}

//先处理新用户的连接事件
ngx_event_process_posted(cycle, &ngx_posted_accept_events);

//释放处理新连接的锁
if(ngx_accept_mutex_held)
{
pthread_mutex_unlock(&ngx_accept_mutex);
}

//再处理已建立连接的用户读写事件
ngx_event_process_posted(cycle, &ngx_posted_events);
}

nginx从抢锁、释放锁到处理事件的整个过程,我已经结合代码做了注释,相信大家对整个过程应该已经不陌生了。至于pthread_mutex_trylock()中进程是如何抢占锁的,这就有赖于实现抢占的算法了,此处只是解释处理过程,并不关心抢占实现原理。感兴趣的同学可以自己搜索相关资料。

  1. 先处理新用户的连接事件,再释放处理新连接的锁:如果刚释放锁,就有新连接,刚获得锁的进程要给等待队列中添加sockfd时,此时原获得锁的进程也要从等待队列中删除sockfd,TCP的三次握手的连接是非线程安全的。为了避免产生错误,使得将sockfd从等待队列中删除后,再让新的进程抢占锁,处理新连接。
  2. 拿到锁,将任务放在任务队列中,不是立刻去处理:每个进程要处理新连接事件,必须拿到锁,当前进程将新连接事件的sokect添加到任务队列中,立即释放锁,让其他进程尽快获得锁,处理用户的连接。

你可能有个疑问,如果没有加锁,有新事件连接时,所有的进程都会被唤醒执行accept,有且仅有一个进程会accept返回成功,其他进程都重新进入睡眠状态。现在有了锁,在发生accept之前,进程们要去抢占锁,也是有且仅有一个进程会抢到锁,其他进程也是重新进入睡眠状态。即:不论是否有accept锁,都会有很多进程被唤醒再重新进入睡眠状态的过程,那惊群现象如何解释?

其实,锁不能解决惊群现象,惊群现象是没办法解决的,很多进程被同时唤醒是一个必然的过程。Nginx中通过检查当前进程的连接数是否>最大连接数*7/8来判断当前进程是否能处理新连接,减少被唤醒的进程数量,也实现了简单的负载均衡。锁只能保证不让所有的进程去调用accept函数,解决了很多进程调用accept返回错误,锁解决的是惊群现象的错误,并不是解决了惊群现象!


转载自:
https://blog.csdn.net/fedorafrog/article/details/114068524?spm=1001.2014.3001.5502

基本概念

同步与异步

​ 同步和异步关注的是消息通信机制。
​ 所谓同步,就是在发出一个“调用”时,在没有得到结果之前,该“调用”就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由“调用者”主动等待这个“调用”的结果。
​ 而异步则是相反,“调用”在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在“调用”发出后,“被调用者”通过状态、通知来通知调用者,或通过回调函数处理这个调用。
​ 举个通俗的例子:你打电话问书店老板有没有《分布式系统》这本书,如果是同步通信机制,书店老板会说,你稍等,”我查一下”,然后开始查啊查,等查好了(可能是5秒,也可能是一天)告诉你结果(返回结果)。而异步通信机制,书店老板直接告诉你我查一下啊,查好了打电话给你,然后直接挂电话了(不返回结果)。然后查好了,他会主动打电话给你。在这里老板通过“回电”这种方式来回调。

阻塞与非阻塞

​ 阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。
​ 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
​ 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
​ 还是上面的例子,你打电话问书店老板有没有《分布式系统》这本书,你如果是阻塞式调用,你会一直把自己“挂起”,直到得到这本书有没有的结果,如果是非阻塞式调用,你不管老板有没有告诉你,你自己先一边去玩了, 当然你也要偶尔过几分钟check一下老板有没有返回结果。在这里阻塞与非阻塞与是否同步异步无关。跟老板通过什么方式回答你结果无关。

Read more »

通常使用下面的JVM选项在GC日志中打印”stop-the-world”(STW)暂停时间。

-XX:+PrintGCApplicationStoppedTime

但是在GC日志中看,会发现有许多这样的暂停时间,时间很短,肯定不是垃圾回收引起的。

1
2
3
4
Total time for which application threads were stopped: 0.0013102 seconds, Stopping threads took: 0.0001792 seconds
Total time for which application threads were stopped: 0.0012521 seconds, Stopping threads took: 0.0001591 seconds
Total time for which application threads were stopped: 0.0008090 seconds, Stopping threads took: 0.0001030 seconds
Total time for which application threads were stopped: 0.0010226 seconds, Stopping threads took: 0.0001058 seconds

​ 实际上,触发STW暂停的原因除了垃圾回收外,还有一些其他的操作会触发STW,例如一些JIT活动、偏向锁擦除、特定的JVMTI操作,以及许多场景也可能会导致应用程序暂停。

上述例子中application threads stopped的时间表示应用暂停时间,Stopping threads took 的时间表示等待所有的应用线程都到达安全点花费的时间。只有应用程序线程到达安全点后,JVM才会做些特殊处理,比如垃圾收集、偏向锁擦除等等。

jps是可以查看当前Java进程。

命令格式:jps [options ] [ hostid ]

[options]选项 :

-q:仅输出java进程号

-m:输出传递给Java进程(主函数)的参数

-l:输出主函数的完整路径

-v:输出jvm启动参数

Read more »

一般查看Java程序堆栈信息时,会使用jstack工具来获得java stack和native stack的信息,如果现在运行的java程序呈现hung的状态,jstack是非常有用的。
如果是在64位机器上,需要指定选项 -J-d64,Windows的jstack使用方式只支持jstack [-l] pid。
命令格式:

1
2
jstack [ option ] pid
jstack [ option ] executable core

常用参数说明:

1
2
3
4
-F   当’jstack [-l] pid’没有相应的时候强制打印栈信息
-l 长列表. 打印关于锁的附加信息,例如属于java.util.concurrent的ownable synchronizers列表.
-m 打印java和native c/c++框架的所有栈信息.
-h | -help 打印帮助信息
Read more »

[toc]

一、遇到的问题


Total time for which application threads were stop 超级长时间,这行日志代表什么,以及为什么时间会这么长 ?

Read more »

java.lang.OutOfMemoryError共有8种类型,其中java.lang.OutOfMemoryError: unable to create new native thread是很常见的一种,这类错误通常发生在应用试图创建新线程时。

可能原因

  1. 系统内存耗尽,无法为新线程分配内存
  2. 创建线程数超过了操作系统的限制
Read more »
0%