Hello Coder


  • 首页

  • 归档

  • 标签

  • 搜索
close

Java NIO:Selector详解

发表于 2018-12-04

Selector事件分发器(单线程选择就绪的事件)作为Java NIO的核心组件,这里详细了解内部实现

服务端代码

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
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.socket().bind(new InetSocketAddress(port));
Selector selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while(true){
selector.select();
Iterator iter = this.selector.selectedKeys().iterator();
while(iter.hasNext()){
SelectionKey key = (SelectionKey)iter.next();
if (key.isAcceptable()){
SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
clientChannel.configureBlocking(false);
// 监听客户端socket可读就绪事件
client.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()){
handleRead(key);
}
if (key.isWritable() && key.isValid()){
handleWrite(key);
}
iter.remove();
}
}
  1. 如果有客户端A连接服务,执行select方法时,可以通过serverSocketChannel获取客户端A的socketChannel,并在selector上注册socketChannel的OP_READ事件。
  2. 如果客户端A发送数据,会触发OP_READ事件,这样下次轮询调用select方法时,就能通过socketChannel读取数据,同时在selector上注册该socketChannel的OP_WRITE事件,实现服务器往客户端写数据。

Selector.open()实现原理

注意:以下源代码皆来源于openjdk8

  • Selector.open()可以得到一个Selector实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static Selector open() throws IOException {
// 首先找到provider,然后再打开Selector
return SelectorProvider.provider().openSelector();
}
// java.nio.channels.spi.SelectorProvider
public static SelectorProvider provider() {
synchronized (lock) {
if (provider != null)
return provider;
return AccessController.doPrivileged(
new PrivilegedAction<SelectorProvider>() {
public SelectorProvider run() {
if (loadProviderFromProperty())
return provider;
if (loadProviderAsService())
return provider;
// 实际创建SelectorProvider的方法
provider = sun.nio.ch.DefaultSelectorProvider.create();
return provider;
}
});
}
}
  • sun.nio.ch.DefaultSelectorProvider

不同系统对应着不同的sun.nio.ch.DefaultSelectorProvider,以Linux为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Returns the default SelectorProvider.
*/
public static SelectorProvider create() {
// 获取OS名称
String osname = AccessController
.doPrivileged(new GetPropertyAction("os.name"));
// 根据名称来创建不同的Selctor
if (osname.equals("SunOS"))
return createProvider("sun.nio.ch.DevPollSelectorProvider");
if (osname.equals("Linux"))
return createProvider("sun.nio.ch.EPollSelectorProvider");
return new sun.nio.ch.PollSelectorProvider();
}

如果系统名称是Linux的话,真正创建的是sun.nio.ch.EPollSelectorProvider

1
2
3
4
// EPollSelectorProvider.openSelector()
public AbstractSelector openSelector() throws IOException {
return new EPollSelectorImpl(this);
}

Linux最终的Selector实现:sun.nio.ch.EPollSelectorImpl

EPollSelectorImpl.select()实现原理

epoll系统调用主要分为3个函数: epoll_create, epoll_ctl, epoll_wait

epoll_create:创建一个epoll fd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// class EPollSelectorImpl extends SelectorImpl
EPollSelectorImpl(SelectorProvider sp) throws IOException {
super(sp);
// makePipe返回管道的2个文件描述符,编码在一个long类型的变量中
// 高32位代表读 低32位代表写
// 使用pipe为了实现Selector的wakeup逻辑
long pipeFds = IOUtil.makePipe(false);
fd0 = (int) (pipeFds >>> 32);
fd1 = (int) pipeFds;
// 创建一个EPollArrayWrapper
pollWrapper = new EPollArrayWrapper();
pollWrapper.initInterrupt(fd0, fd1);
fdToKey = new HashMap<>();
}

创建一个EPollArrayWrapper 初始化

1
2
3
4
5
6
EPollArrayWrapper() throws IOException {
// 创建epoll fd
epfd = epollCreate();
}
private native int epollCreate();

在初始化过程中调用了native epollCreate方法。

1
2
3
4
5
6
7
8
9
Java_sun_nio_ch_EPollArrayWrapper_epollCreate(JNIEnv *env, jobject this)
{
//从Linux2.6.8之后,改用了红黑树结构,指定了大小也没用
int epfd = epoll_create(256);
if (epfd < 0) {
JNU_ThrowIOExceptionWithLastError(env, "epoll_create failed");
}
return epfd;
}

epoll_create: 内核系统调用,创建一个epoll fd, 并且开辟epoll自己的内核高速cache区,建立红黑树分配初始size的内存对象,同时建立一个list链表,用于存储准备就绪的事件

Epoll wait:等待内核IO事件

调用Selector.select(),最后会委托给EPollSelectorImpl的doSelect()方法

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
protected int doSelect(long timeout) throws IOException {
if (closed)
throw new ClosedSelectorException();
processDeregisterQueue();
try {
begin();
// 等待事件到来,收集事件到来的fd并用来处理
pollWrapper.poll(timeout);
} finally {
end();
}
processDeregisterQueue();
int numKeysUpdated = updateSelectedKeys();
if (pollWrapper.interrupted()) {
// Clear the wakeup pipe
pollWrapper.putEventOps(pollWrapper.interruptedIndex(), 0);
synchronized (interruptLock) {
pollWrapper.clearInterrupted();
IOUtil.drain(fd0);
interruptTriggered = false;
}
}
return numKeysUpdated;
}

实际执行EPollArrayWrapper.poll(timeout);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int poll(long timeout) throws IOException {
// 看下文
updateRegistrations();
// 调用native方法,发起系统内核调用
updated = epollWait(pollArrayAddress, NUM_EPOLLEVENTS, timeout, epfd);
for (int i=0; i<updated; i++) {
if (getDescriptor(i) == incomingInterruptFD) {
interruptedIndex = i;
interrupted = true;
break;
}
}
return updated;
}
private native int epollWait(long pollAddress, int numfds, long timeout,
int epfd) throws IOException;

epollWait也是个native方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Java_sun_nio_ch_EPollArrayWrapper_epollWait(JNIEnv *env, jobject this,
jlong address, jint numfds,
jlong timeout, jint epfd)
{
struct epoll_event *events = jlong_to_ptr(address);
int res;
if (timeout <= 0) {
// 发起epoll_wait系统调用等待内核事件
RESTARTABLE(epoll_wait(epfd, events, numfds, timeout), res);
} else {
res = iepoll(epfd, events, numfds, timeout);
}
if (res < 0) {
JNU_ThrowIOExceptionWithLastError(env, "epoll_wait failed");
}
return res;
}

epoll_wait: 内核系统调用, 等待内核返回IO事件

epoll_ctl: IO事件管理

注册到Selector上的IO事件是使用SelectionKey来表示,代表了Channel感兴趣的事件,如Read,Write,Connect,Accept.

调用Selector.register()完成IO事件注册,实际执行EPollSelectorImpl.implRegister()方法

1
2
3
4
5
6
7
8
9
protected void implRegister(SelectionKeyImpl ski) {
if (closed)
throw new ClosedSelectorException();
SelChImpl ch = ski.channel;
int fd = Integer.valueOf(ch.getFDVal());
fdToKey.put(fd, ski);
pollWrapper.add(fd);
keys.add(ski);
}

调用Selector.register()时均会将事件存储到EpollArrayWrapper的成员变量eventsLow和eventsHigh中

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
// 使用数组保存事件变更, 数组的最大长度是MAX_UPDATE_ARRAY_SIZE, 最大64*1024
private final byte[] eventsLow = new byte[MAX_UPDATE_ARRAY_SIZE];
// 超过数组长度的事件会缓存到这个map中,等待下次处理
private Map<Integer,Byte> eventsHigh;
// 添加文件描述符fd
void add(int fd) {
// force the initial update events to 0 as it may be KILLED by a
// previous registration.
synchronized (updateLock) {
assert !registered.get(fd);
setUpdateEvents(fd, (byte)0, true);
}
}
private void setUpdateEvents(int fd, byte events, boolean force) {
// 判断fd和数组长度
if (fd < MAX_UPDATE_ARRAY_SIZE) {
if ((eventsLow[fd] != KILLED) || force) {
eventsLow[fd] = events;
}
} else {
Integer key = Integer.valueOf(fd);
if (!isEventsHighKilled(key) || force) {
eventsHigh.put(key, Byte.valueOf(events));
}
}
}

执行EpollArrayWrapper.poll()的时候, 首先会调用updateRegistrations()

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
/**
* Returns the pending update events for the given file descriptor.
*/
private byte getUpdateEvents(int fd) {
if (fd < MAX_UPDATE_ARRAY_SIZE) {
return eventsLow[fd];
} else {
Byte result = eventsHigh.get(Integer.valueOf(fd));
// result should never be null
return result.byteValue();
}
}
private void updateRegistrations() {
synchronized (updateLock) {
int j = 0;
while (j < updateCount) {
int fd = updateDescriptors[j];
// 从保存的eventsLow和eventsHigh里取出事件
short events = getUpdateEvents(fd);
boolean isRegistered = registered.get(fd);
int opcode = 0;
if (events != KILLED) {
// 判断操作类型以传给epoll_ctl
// 没有指定EPOLLET事件类型
if (isRegistered) {
// 如果已经注册过,不需要调用epollCtl去内核红黑树新增节点
opcode = (events != 0) ? EPOLL_CTL_MOD : EPOLL_CTL_DEL;
} else {
opcode = (events != 0) ? EPOLL_CTL_ADD : 0;
}
if (opcode != 0) {
// 熟悉的epoll_ctl
epollCtl(epfd, opcode, fd, events);
if (opcode == EPOLL_CTL_ADD) {
registered.set(fd);
} else if (opcode == EPOLL_CTL_DEL) {
registered.clear(fd);
}
}
}
j++;
}
updateCount = 0;
}
}
private native void epollCtl(int epfd, int opcode, int fd, int events);

在获取到事件之后将操作委托给了epollCtl,这又是个native方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Java_sun_nio_ch_EPollArrayWrapper_epollCtl(JNIEnv *env, jobject this, jint epfd,
jint opcode, jint fd, jint events)
{
struct epoll_event event;
int res;
event.events = events;
event.data.fd = fd;
// 发起epoll_ctl调用来进行IO事件的管理
RESTARTABLE(epoll_ctl(epfd, (int)opcode, (int)fd, &event), res);
if (res < 0 && errno != EBADF && errno != ENOENT && errno != EPERM) {
JNU_ThrowIOExceptionWithLastError(env, "epoll_ctl failed");
}
}

注意:jdk nio没有指定ET(边缘触发)还是LT(水平触发), 所以默认会用LT, 而Netty epoll transport使用ET触发

通过channel就能不断的获取客户端socket数据,实现后端业务处理

参考

Java NIO分析(8): 高并发核心Selector详解

java NIO 运行原理介绍

Introduction to the Java NIO Selector

深入浅出NIO之Selector实现原理

谈一谈 Java IO 模型

IO多路复用机制

发表于 2018-11-29
1
One basic concept of Linux (actually Unix) is the rule that everything in Unix/Linux is a file. Each process has a table of file descriptors that point to files, sockets, devices and other operating system objects

用户空间与内核空间

对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中(通过DMA,不需要CPU),然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间(需要CPU)。所以说,当一个read操作发生时,它会经历两个阶段:

1
2
1. 等待数据准备 (Waiting for the data to be ready);对于一个socket接口上的操作,这一步骤关系到数据从网络到达,并将其复制到内核的缓冲区
2. 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)

Blocking IO

传统的阻塞 I/O 模型工作方式:当线程使用 read 或者 write 对某一个文件描述符(File Descriptor 以下简称 FD)进行读写时,如果当前 FD 不可读或不可写,当前线程会被CPU挂起阻塞,一直等待数据准备完毕。

例如:tomcat服务器BIO模式,利用多线程 + 线程池 处理;即每一个socket连接创建一个独立的线程处理

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
{
ExecutorService executor = Excutors.newFixedThreadPollExecutor(100);//线程池
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(8080);
while(true){ //死循环等待新连接到来
Socket socket = serverSocket.accept();
executor.submit(new ConnectIOHandler(socket));//为新的连接创建新的线程
}
}
class ConnectIOHandler extends Thread{
private Socket socket;
public ConnectIOnHandler(Socket socket){
this.socket = socket;
}
public void run(){
while(!socket.isClosed()){ // 死循环处理读写事件
String someThing = socket.read()....//读取数据
if(someThing!=null){
......//处理数据
socket.write()....//写数据
}
}
}
}

缺点

  • 线程本身占用较大内存,像Java的线程栈,一般至少分配512K~1M的空间,如果系统中的线程数过千,恐怕整个JVM的内存都会被吃掉一半
  • 线程的切换成本是很高的。操作系统发生线程切换的时候,需要保留线程的上下文,然后执行系统调用。如果线程数过高,可能执行线程切换的时间甚至会大于线程执行的时间

IO多路复用(IO mutiplexing)

IO多路复用就通过一种机制,单个线程通过监视多个I/O流的状态来同时管理多个I/O流,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作

当应用进程通过select读取文件(socket),应用进程会被block,于此同时内核会“监视”所有通过select请求的文件读取(socket),任何一个文件(socket)的数据被准备好,select就会返回,应用进程再调用read操作,把数据从内核中拷贝到应用进程。

优缺点

  • IO复用技术的优势在于,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,很大程度上减少了资源占用;适合于连接数多的场景(nginx,rpc,redis等)。
  • 如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接

select,poll,epoll

select,poll,epoll都是IO多路复用的实现机制。select,poll,epoll本质上都是同步I/O,因为它们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的

注意: IO多路复用经常被称为异步非阻塞,这里的异步只是相对于以前同步阻塞而起的名称,并非实际情况下的unix异步模型,如果从Unix IO模型角度只能将IO多路复用称为非阻塞IO

对于IO多路复用,有两件事是必须要做的(对于监控可读事件而言):

1
2
1. 准备好需要监控的fds集合
2. 探测并返回fds集合中哪些fd可读

select

基本原理
1
单个进程就可以同时处理多个网络连接的IO请求(同时阻塞多个IO操作)。基本原理就是程序调用select(),然后整个程序就阻塞了,这时候,select会将需要监控的readfds集合拷贝到内核空间(假设监控的仅仅是socket可读),kernel就会轮询检查所有select负责的fd,当找到一个client中的数据准备就绪了,select就会返回,这个时候程序就会系统调用,将数据从kernel复制到进程缓冲区。
优缺点
  • select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点
  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  • 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  • select支持的文件描述符数量太小了,默认是1024

poll

基本原理
1
2
3
poll的原理与select非常相似,差别如下:
1) 描述fd集合的方式不同,poll使用 pollfd 结构而不是select结构fd_set结构,所以poll是链式的,没有最大连接数的限制
2) poll有一个特点是水平触发,也就是通知程序fd就绪后,这次没有被处理,那么下次poll的时候会再次通知该fd已经就绪。
优缺点

poll机制虽然改进了select的监控大小1024的限制,但以下两个性能问题还没有解决 :

  • fds集合整体仍然需要从用户空间拷贝到内核空间的问题,而不管这样的复制是不是有意义
  • 当被监控的fds中某些有数据可读的时候,我们希望通知更加精细一点,就是我们希望能够从通知中得到有可读事件的fds列表,而不是需要遍历整个fds来收集。

epoll

它是由Linux内核2.6推出的可伸缩的IO多路复用实现,目的是为了替代select()与poll()

事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,select()与poll()的效率也会线性下降。

基本原理

select和poll都只提供了一个函数-select或者poll函数。而epoll提供了三个函数,分别如下:

  • int epoll_create(int size):// 创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大
  • int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event):// 注册描述符fd要监听的事件类型;epfd:是epoll_create()的返回值; fd:需要监听的文件描述符; epoll_event:告诉内核需要监听什么事
  • int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout): // 等待epfd上的io事件,返回在events中发生的事件, 最多返回maxevents个事件
1
2
3
4
5
epoll执行过程:
1) 首先执行epoll_create创建一个epoll句柄;并开辟epoll自己的内核高速cache区,在该缓冲区建立红黑树和就绪链表
2) epoll_ctl执行add动作时除了将要监听的文件句柄放到红黑树上之外,还向内核注册了该文件句柄的回调函数(内核中断处理程序注册一个回调函数),内核在检测到某句柄可读可写时(内核针对读缓冲区和写缓冲区来判断是否可读可写)则调用该回调函数,回调函数将文件句柄放到就绪链表(当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了)。
3) epoll_wait只监控就绪链表就可以(利用schedule_timeout()实现睡一会,判断一会的效果),如果就绪链表有文件句柄不为空,则表示该文件句柄可读可写,并返回到用户态(少量的拷贝)
4) 由于内核不修改文件句柄的位置,因此只需要在第一次传入就可以重复监控,直到使用epoll_ctl删除,否则不需要重新传入,因此无多次拷贝

epoll_ctl通过(EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL)三个操作来分散对需要监控的fds集合的修改,做到了有变化才变更,将select/poll高频、大块内存拷贝(集中处理)变成epoll_ctl的低频、小块内存的拷贝(分散处理),避免了大量的内存拷贝

优缺点

相比select/poll,epoll的优点如下:

  • 没有最大并发连接的限制,能打开的FD的上限远大于1024 (在1GB内存的机器上大约是10万左右)
  • 效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,epoll的效率就会远远高于select和poll。
  • epoll内部使用了mmap共享了用户和内核的部分空间,避免了数据的来回拷贝
  • epoll 有个致命的缺点,只有linux支持
epoll的两种工作模式

支持边缘触发ET(edge trigger)与水平触发LT(level trigger)两种模式(poll()只支持水平触发)

  • LT模式:缺省的工作方式,并且同时支持block和no-block socket;当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件(清理就绪列表后,重新把句柄放回刚刚清空的就绪列表)
  • ET模式:高速工作方式,只支持no-block socket;当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件(only once)。所以在ET模式下,一般是通过while循环,一次性读完全部数据.epoll默认使用的是LT.

    ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

参考

LINUX – IO MULTIPLEXING – SELECT VS POLL VS EPOLL

JAVA 中原生的 socket 通信机制

通俗讲解 异步,非阻塞和 IO 复用

Redis 和 I/O 多路复用

select、poll、epoll之间的区别总结

Linux IO模式及 select、poll、epoll详解

Java NIO和多路复用(I/O multiplexing)

Java NIO浅析

《Netty 权威指南》—4种IO的对比

epoll 或者 kqueue 的原理是什么?

innoDb快照读

发表于 2018-10-22

快照读(Snapshot Read)

MySQL数据库,InnoDB存储引擎,为了提高并发,使用MVCC(多版本并发控制)机制,在并发事务时,通过读取数据行的历史数据版本,不加锁,来提高并发的一种不加锁一致性读(Consistent Nonlocking Read)。一致性读,又称为快照读

1
A consistent read means that InnoDB uses multi-versioning to present to a query a snapshot of the database at a point in time. The query sees the changes made by transactions that committed before that point of time, and no changes made by later or uncommitted transactions. The exception to this rule is that the query sees the changes made by earlier statements within the same transaction.

注意:本事务中修改的数据,即使未提交的数据也可以被本事务的后面部分读取到

快照实现

  • undo log :记录事务变更前的状态。操作数据之前,先将数据备份到undo log,然后进行数据修改(COW:写时备份),如果出现错误或用户执行了rollback语句,则系统就可以利用undo log中的备份数据恢复到事务开始之前的状态。
  • redo log: 记录事务变更后的状态。在事务提交前,只要将redo log持久化即可,数据在内存中变更。当系统崩溃时,虽然数据没有落盘,但是redo log已持久化,系统可以根据redo Log的内容,将所有数据恢复到最新的状态。

参考

一致性读

nginx负载均衡策略

发表于 2018-09-17

负载均衡可以将前端的请求分担到后端多个节点上,提升系统的响应和处理能力。

负载均衡策略

负载均衡的策略可以分为两大类:内置策略 和扩展策略

  • 内置策略:一般会直接编译进Nginx内核,常用的有轮询、ip hash
  • 扩展策略:fair、url hash等

普通轮询方式

默认选项,当weight不指定时,各服务器权重相同, 每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。

1
2
3
4
5
upstream bakend {
server 10.11.0.1;
server 10.11.0.2;
server 10.11.0.3;
}

加权轮询方式

指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。

1
2
3
4
upstream backend {
server 10.11.0.1 weight=3;
server 10.11.0.2 weight=7;
}

权重越高,在被访问的概率越大,如上例,分别是30%,70%。

轮询流程图

首先将请求都分给高权重的机器,直到该机器的权值降到了比其他机器低,才开始将请求分给下一个高权重的机器;当所有后端机器都down掉时,nginx会立即将所有机器的标志位清成初始状态,以避免造成所有的机器都处在timeout的状态

ip hash方式

默认情况下,Nginx 会为你提供轮询作为负载均衡策略。

采用ip_hash策略解决登录信息丢失,如果客户已经访问了某个服务器,当用户再次访问时,会将该请求通过哈希算法,自动定位到同一个服务器,当然,如果所 hash 到的 server 当前不可用,则请求会被转移到其他server。
每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题。

1
2
3
4
5
upstream backend {
ip_hash;
server 10.11.0.1;
server 10.11.0.2;
}

fair

根据后端服务器的响应时间判断负载情况,从中选出负载最轻的机器进行优先分配。

1
2
3
4
5
upstream backend {
server 10.11.0.1;
server 10.11.0.2;
fair;
}

缺点:需要「负载均衡器」不停的去统计每一台后端服务器对请求的处理速度,比如一分钟统计一次,生成一个后端服务器处理速度的排行榜,然后「负载均衡器」根据这个排行榜去转发服务。

jvm内存参数

发表于 2018-05-10

线上jvm参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager,
-XX:MetaspaceSize=256M, // 分配给类元数据空间的初始大小
-XX:MaxMetaspaceSize=256M, // 分配给类元数据空间的最大值
-Xms4g, // 初始堆大小
-Xmx4g, // 最大堆大小
-Xmn1g, // 年轻代大小
-Xss256k, // 每个线程的堆栈大小
-XX:SurvivorRatio=8, // Eden区与Survivor区的大小比值
-XX:MaxTenuringThreshold=8, // 垃圾最大年龄, 该参数只有在串行GC时才有效.
-XX:ParallelGCThreads=8, // 并行收集器的线程数
-XX:+UseConcMarkSweepGC, // 使用CMS内存收集
-XX:+UseParNewGC, // 设置年轻代为并行收集
-XX:+DisableExplicitGC, // 关闭System.gc()
-XX:+CMSParallelRemarkEnabled, // 降低标记停顿
-XX:+CMSClassUnloadingEnabled, // 在CMS中清理过期的Class而不等到Full GC
-XX:CMSInitiatingOccupancyFraction=70, // 使用cms作为垃圾回收,使用70%后开始CMS收集
-XX:CMSFullGCsBeforeCompaction=5, // 多少次GC后进行内存压缩
-XX:+UseCMSCompactAtFullCollection, // 在FULLGC的时候,对年老代的压缩
-XX:+CMSScavengeBeforeRemark, // 在CMS remark前,先执行一次minor GC将新生代清掉
-XX:+HeapDumpOnOutOfMemoryError, // 当OutOfMemoryError发生时,将heap内存dump到文件
......

-XX:MetaspaceSize

Java 8彻底将永久代 (PermGen) 移除出了 HotSpot JVM,将其原有的数据迁移至 Java Heap 或 Metaspace

1234…31
David

David

Develop Notes

155 日志
37 标签
GitHub Weibo
© 2016 - 2020 David
由 Hexo 强力驱动
主题 - NexT.Pisces