BIO

有了Block的定义,就可以讨论BIO和NIO了。BIO是Blocking IO的意思。在类似于网络中进行read, write, connect一类的系统调用时会被卡住。

举个例子,当用read去读取网络的数据时,是无法预知对方是否已经发送数据的。因此在收到数据之前,能做的只有等待,直到对方把数据发过来,或者等到网络超时。

对于单线程的网络服务,这样做就会有卡死的问题。因为当等待时,整个线程会被挂起,无法执行,也无法做其他的工作。

顺便说一句,这种Block是不会影响同时运行的其他程序(进程)的,因为现代操作系统都是多任务的,任务之间的切换是抢占式的。这里Block只是指Block当前的进程。

于是,网络服务为了同时响应多个并发的网络请求,必须实现为多线程的。每个线程处理一个网络请求。线程数随着并发连接数线性增长。这的确能奏效。实际上2000年之前很多网络服务器就是这么实现的。但这带来两个问题:

  • 线程越多,Context Switch就越多,而Context Switch是一个比较重的操作,会无谓浪费大量的CPU。
  • 每个线程会占用一定的内存作为线程的栈。比如有1000个线程同时运行,每个占用1MB内存,就占用了1个G的内存。

也许现在看来1GB内存不算什么,现在服务器上百G内存的配置现在司空见惯了。但是倒退20年,1G内存是很金贵的。并且,尽管现在通过使用大内存,可以轻易实现并发1万甚至10万的连接。但是水涨船高,如果是要单机撑1千万的连接呢?

问题的关键在于,当调用read接受网络请求时,有数据到了就用,没数据到时,实际上是可以干别的。使用大量线程,仅仅是因为Block发生,没有其他办法。

当然你可能会说,是不是可以弄个线程池呢?这样既能并发的处理请求,又不会产生大量线程。但这样会限制最大并发的连接数。比如你弄4个线程,那么最大4个线程都Block了就没法响应更多请求了。

要是操作IO接口时,操作系统能够总是直接告诉有没有数据,而不是Block去等就好了。于是,NIO登场。

NIO

NIO是指将IO模式设为“Non-Blocking”模式。在Linux下,一般是这样:

void setnonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

再强调一下,以上操作只对socket对应的文件描述符有意义;对磁盘文件的文件描述符做此设置总会成功,但是会直接被忽略。

这时,BIO和NIO的区别是什么呢?

在BIO模式下,调用read,如果发现没数据已经到达,就会Block住。

在NIO模式下,调用read,如果发现没数据已经到达,就会立刻返回-1, 并且errno被设为EAGAIN

在有些文档中写的是会返回EWOULDBLOCK。实际上,在Linux下EAGAINEWOULDBLOCK是一样的,即#define EWOULDBLOCK EAGAIN

于是,一段NIO的代码,大概就可以写成这个样子。

struct timespec sleep_interval{.tv_sec = 0, .tv_nsec = 1000};
ssize_t nbytes;
while (1) {
/* 尝试读取 */
if ((nbytes = read(fd, buf, sizeof(buf))) < 0) {
if (errno == EAGAIN) { // 没数据到
perror("nothing can be read");
} else {
perror("fatal error");
exit(EXIT_FAILURE);
}
} else { // 有数据
process_data(buf, nbytes);
}
// 处理其他事情,做完了就等一会,再尝试
nanosleep(sleep_interval, NULL);
}

这段代码很容易理解,就是轮询,不断的尝试有没有数据到达,有了就处理,没有(得到EWOULDBLOCK或者EAGAIN)就等一小会再试。这比之前BIO好多了,起码程序不会被卡死了。

但这样会带来两个新问题:

  • 如果有大量文件描述符都要等,那么就得一个一个的read。这会带来大量的Context Switch(read是系统调用,每调用一次就得在用户态和核心态切换一次)
  • 休息一会的时间不好把握。这里是要猜多久之后数据才能到。等待时间设的太长,程序响应延迟就过大;设的太短,就会造成过于频繁的重试,干耗CPU而已。

要是操作系统能一口气告诉程序,哪些数据到了就好了。

于是IO多路复用被搞出来解决这个问题。

IO多路复用

IO多路复用(IO Multiplexing) 是这么一种机制:程序注册一组socket文件描述符给操作系统,表示“我要监视这些fd是否有IO事件发生,有了就告诉程序处理”。

IO多路复用是要和NIO一起使用的。尽管在操作系统级别,NIO和IO多路复用是两个相对独立的事情。NIO仅仅是指IO API总是能立刻返回,不会被Blocking;而IO多路复用仅仅是操作系统提供的一种便利的通知机制。操作系统并不会强制这俩必须得一起用——你可以用NIO,但不用IO多路复用,就像上一节中的代码;也可以只用IO多路复用 + BIO,这时效果还是当前线程被卡住。但是,IO多路复用和NIO是要配合一起使用才有实际意义。因此,在使用IO多路复用之前,请总是先把fd设为O_NONBLOCK

对IO多路复用,还存在一些常见的误解,比如:

  • ❌IO多路复用是指多个数据流共享同一个Socket。其实IO多路复用说的是多个Socket,只不过操作系统是一起监听他们的事件而已。

    多个数据流共享同一个TCP连接的场景的确是有,比如Http2 Multiplexing就是指Http2通讯中中多个逻辑的数据流共享同一个TCP连接。但这与IO多路复用是完全不同的问题。

  • ❌IO多路复用是NIO,所以总是不Block的。其实IO多路复用的关键API调用(selectpollepoll_wait)总是Block的,正如下文的例子所讲。

  • IO多路复用和NIO一起减少了IO。实际上,IO本身(网络数据的收发)无论用不用IO多路复用和NIO,都没有变化。请求的数据该是多少还是多少;网络上该传输多少数据还是多少数据。IO多路复用和NIO一起仅仅是解决了调度的问题,避免CPU在这个过程中的浪费,使系统的瓶颈更容易触达到网络带宽,而非CPU或者内存。要提高IO吞吐,还是提高硬件的容量(例如,用支持更大带宽的网线、网卡和交换机)和依靠并发传输(例如HDFS的数据多副本并发传输)。

操作系统级别提供了一些接口来支持IO多路复用,最老掉牙的是selectpoll

select

select长这样:

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

它接受3个文件描述符的数组,分别监听读取(readfds),写入(writefds)和异常(expectfds)事件。那么一个 IO多路复用的代码大概是这样:

struct timeval tv = {.tv_sec = 1, .tv_usec = 0};

ssize_t nbytes;
while(1) {
FD_ZERO(&read_fds);
setnonblocking(fd1);
setnonblocking(fd2);
FD_SET(fd1, &read_fds);
FD_SET(fd2, &read_fds);
// 把要监听的fd拼到一个数组里,而且每次循环都得重来一次...
if (select(FD_SETSIZE, &read_fds, NULL, NULL, &tv) < 0) { // block住,直到有事件到达
perror("select出错了");
exit(EXIT_FAILURE);
}
for (int i = 0; i < FD_SETSIZE; i++) {
if (FD_ISSET(i, &read_fds)) {
/* 检测到第[i]个读取fd已经收到了,这里假设buf总是大于到达的数据,所以可以一次read完 */
if ((nbytes = read(i, buf, sizeof(buf))) >= 0) {
process_data(nbytes, buf);
} else {
perror("读取出错了");
exit(EXIT_FAILURE);
}
}
}
}

首先,为了select需要构造一个fd数组(socket文件描述符,表示“我要监视这些fd是否有IO事件发生,有了就告诉程序处理”。这里为了简化,没有构造要监听写入和异常事件的fd数组)。之后,用select监听了read_fds中的多个socket的读取时间。调用select后,程序会Block住,直到一个事件发生了,或者等到最大1秒钟(tv定义了这个时间长度)就返回。之后,需要遍历所有注册的fd,挨个检查哪个fd有事件到达(FD_ISSET返回true)。如果是,就说明数据已经到达了,可以读取fd了。读取后就可以进行数据的处理。

select有一些发指的缺点:

  • select能够支持的最大的fd数组的长度是1024。这对要处理高并发的web服务器是不可接受的。
  • fd数组按照监听的事件分为了3个数组,为了这3个数组要分配3段内存去构造,而且每次调用select前都要重设它们(因为select会改这3个数组);调用select后,这3数组要从用户态复制一份到内核态;事件到达后,要遍历这3数组。很不爽。
  • select返回后要挨个遍历fd,找到被“SET”的那些进行处理。这样比较低效。
  • select是无状态的,即每次调用select,内核都要重新检查所有被注册的fd的状态。select返回后,这些状态就被返回了,内核不会记住它们;到了下一次调用,内核依然要重新检查一遍。于是查询的效率很低。

poll

pollselect类似于。它大概长这样:

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

poll的代码例子和select差不多,因此也就不赘述了。有意思的是poll这个单词的意思是“轮询”,所以很多中文资料都会提到对IO进行“轮询”。

上面说的select和下文说的epoll本质上都是轮询。

poll优化了select的一些问题。比如不再有3个数组,而是1个polldfd结构的数组了,并且也不需要每次重设了。数组的个数也没有了1024的限制。但其他的问题依旧:

  • 依然是无状态的,性能的问题与select差不多一样;
  • 应用程序仍然无法很方便的拿到那些“有事件发生的fd“,还是需要遍历所有注册的fd。

目前来看,高性能的web服务器都不会使用selectpoll。他们俩存在的意义仅仅是“兼容性”,因为很多操作系统都实现了这两个系统调用。

如果是追求性能的话,在BSD/macOS上提供了kqueue api;在Salorias中提供了/dev/poll(可惜该操作系统已经凉凉);而在Linux上提供了epoll api。它们的出现彻底解决了selectpoll的问题。Java NIO,nginx等在对应的平台的上都是使用这些api实现。

因为大部分情况下我会用Linux做服务器,所以下文以Linux epoll为例子来解释多路复用是怎么工作的。

用epoll实现的IO多路复用

epoll是Linux下的IO多路复用的实现。这里单开一章是因为它非常有代表性,并且Linux也是目前最广泛被作为服务器的操作系统。细致的了解epoll对整个IO多路复用的工作原理非常有帮助。

selectpoll不同,要使用epoll是需要先创建一下的。

int epfd = epoll_create(10);

epoll_create在内核层创建了一个数据表,记录要注册的fd。接口会返回一个“epoll的文件描述符”指向这个表。注意,接口参数是一个表达要监听事件列表的长度的数值。但不用太在意,因为epoll内部随后会根据事件注册和事件注销动态调整epoll中表格的大小。

 

为什么epoll要创建一个用文件描述符来指向的表呢?这里有两个好处:

  • epoll是有状态的,不像selectpoll那样每次都要重新传入所有要监听的fd,这避免了很多无谓的数据复制。epoll的数据是用接口epoll_ctl来管理的(增、删、改)。
  • epoll文件描述符在进程被fork时,子进程是可以继承的。这可以给对多进程共享一份epoll数据,实现并行监听网络请求带来便利。但这超过了本文的讨论范围,就此打住。

epoll创建后,第二步是使用epoll_ctl接口来注册要监听的事件。

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

其中第一个参数就是上面创建的表epfd。第二个参数op表示如何对文件名进行操作,共有3种。

  • EPOLL_CTL_ADD - 注册一个事件
  • EPOLL_CTL_DEL - 取消一个事件的注册
  • EPOLL_CTL_MOD - 修改一个事件的注册

第三个参数是要操作的fd,这里必须是支持NIO的fd(比如socket)。

第四个参数是一个epoll_event的类型的数据,表达了注册的事件的具体信息。

typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t; struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};

比方说,想关注一个fd1的读取事件事件,并采用边缘触发(下文会解释什么是边缘触发),大概要这么写:

struct epoll_data ev;
ev.events = EPOLLIN | EPOLLET; // EPOLLIN表示读事件;EPOLLET表示边缘触发
ev.data.fd = fd1;

通过epoll_ctl就可以灵活的注册/取消注册/修改注册某个fd的某些事件。

 

第三步,使用epoll_wait来等待事件的发生。

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

特别留意,这一步是"block"的。只有当注册的事件至少有一个发生,或者timeout达到时,该调用才会返回。这与selectpoll几乎一致。但不一样的地方是evlist,它是epoll_wait的返回数组,里面只包含那些被触发的事件对应的fd,而不是像selectpoll那样返回所有注册的fd。

 

综合起来,一段比较完整的epoll代码大概是这样的。

#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];
int nfds, epfd, fd1, fd2; // 假设这里有两个socket,fd1和fd2,被初始化好。
// 设置为non blocking
setnonblocking(fd1);
setnonblocking(fd2); // 创建epoll
epfd = epoll_create(MAX_EVENTS);
if (epollfd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
} //注册事件
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = fd1;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, fd1, &ev) == -1) {
perror("epoll_ctl: error register fd1");
exit(EXIT_FAILURE);
}
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, fd2, &ev) == -1) {
perror("epoll_ctl: error register fd2");
exit(EXIT_FAILURE);
} // 监听事件
for (;;) {
nfds = epoll_wait(epdf, events, MAX_EVENTS, -1);
if (nfds == -1) {
perror("epoll_wait");
exit(EXIT_FAILURE);
} for (n = 0; n < nfds; ++n) { // 处理所有发生IO事件的fd
process_event(events[n].data.fd);
// 如果有必要,可以利用epoll_ctl继续对本fd注册下一次监听,然后重新epoll_wait
}
}

此外,epoll的手册 中也有一个简单的例子。

所有的基于IO多路复用的代码都会遵循这样的写法:注册——监听事件——处理——再注册,无限循环下去。

epoll的优势

为什么epoll的性能比selectpoll要强呢? selectpoll每次都需要把完成的fd列表传入到内核,迫使内核每次必须从头扫描到尾。而epoll完全是反过来的。epoll在内核的数据被建立好了之后,每次某个被监听的fd一旦有事件发生,内核就直接标记之。epoll_wait调用时,会尝试直接读取到当时已经标记好的fd列表,如果没有就会进入等待状态。

同时,epoll_wait直接只返回了被触发的fd列表,这样上层应用写起来也轻松愉快,再也不用从大量注册的fd中筛选出有事件的fd了。

简单说就是selectpoll的代价是"O(所有注册事件fd的数量)",而epoll的代价是"O(发生事件fd的数量)"。于是,高性能网络服务器的场景特别适合用epoll来实现——因为大多数网络服务器都有这样的模式:同时要监听大量(几千,几万,几十万甚至更多)的网络连接,但是短时间内发生的事件非常少。

但是,假设发生事件的fd的数量接近所有注册事件fd的数量,那么epoll的优势就没有了,其性能表现会和pollselect差不多。

epoll除了性能优势,还有一个优点——同时支持水平触发(Level Trigger)和边沿触发(Edge Trigger)。

AIO

AIO希望的是,你select,poll,epoll都需要用一个函数去监控一大堆fd,那么我AIO不需要了,你把fd告诉内核,你应用程序无需等待,内核会通过信号等软中断告诉应用程序,数据来了,你直接读了,所以,用了AIO可以废弃select,poll,epoll。

但linux的AIO的实现方式是内核和应用共享一片内存区域,应用通过检测这个内存区域(避免调用nonblocking的read、write函数来测试是否来数据,因为即便调用nonblocking的read和write由于进程要切换用户态和内核态,仍旧效率不高)来得知fd是否有数据,可是检测内存区域毕竟不是实时的,你需要在线程里构造一个监控内存的循环,设置sleep,总的效率不如epoll这样的实时通知。所以,AIO是渣,适合低并发的IO操作。所以java7引入的NIO.2引入的AIO对高并发的网络IO设计程序来说,也是渣,只有Netty的epoll+edge-triggered notification最牛,能在linux让应用和OS取得最高效率的沟通。

水平触发和边沿触发

默认情况下,epoll使用水平触发,这与selectpoll的行为完全一致。在水平触发下,epoll顶多算是一个“跑得更快的poll”。

而一旦在注册事件时使用了EPOLLET标记(如上文中的例子),那么将其视为边沿触发(或者有地方叫边缘触发,一个意思)。那么到底什么水平触发和边沿触发呢?

考虑下图中的例子。有两个socket的fd——fd1和fd2。我们设定监听f1的“水平触发读事件“,监听fd2的”边沿触发读事件“。我们使用在时刻t1,使用epoll_wait监听他们的事件。在时刻t2时,两个fd都到了100bytes数据,于是在时刻t3, epoll_wait返回了两个fd进行处理。在t4,我们故意不读取所有的数据出来,只各自读50bytes。然后在t5重新注册两个事件并监听。在t6时,只有fd1会返回,因为fd1里的数据没有读完,仍然处于“被触发”状态;而fd2不会被返回,因为没有新数据到达。

 

这个例子很明确的显示了水平触发和边沿触发的区别。

  • 水平触发只关心文件描述符中是否还有没完成处理的数据,如果有,不管怎样epoll_wait,总是会被返回。简单说——水平触发代表了一种“状态”。

  • 边沿触发只关心文件描述符是否有的事件产生,如果有,则返回;如果返回过一次,不管程序是否处理了,只要没有新的事件产生,epoll_wait不会再认为这个fd被“触发”了。简单说——边沿触发代表了一个“事件”。

    那么边沿触发怎么才能迫使新事件产生呢?一般需要反复调用read/write这样的IO接口,直到得到了EAGAIN错误码,再去尝试epoll_wait才有可能得到下次事件。

那么为什么需要边沿触发呢?

边沿触发把如何处理数据的控制权完全交给了开发者,提供了巨大的灵活性。比如,读取一个http的请求,开发者可以决定只读取http中的headers数据就停下来,然后根据业务逻辑判断是否要继续读(比如需要调用另外一个服务来决定是否继续读)。而不是次次被socket尚有数据的状态烦扰;写入数据时也是如此。比如希望将一个资源A写入到socket。当socket的buffer充足时,epoll_wait会返回这个fd是准备好的。但是资源A此时不一定准备好。如果使用水平触发,每次经过epoll_wait也总会被打扰。在边沿触发下,开发者有机会更精细的定制这里的控制逻辑。

但不好的一面时,边沿触发也大大的提高了编程的难度。一不留神,可能就会miss掉处理部分socket数据的机会。如果没有很好的根据EAGAIN来“重置”一个fd,就会造成此fd永远没有新事件产生,进而导致饿死相关的处理代码。

再来思考一下什么是“Block”

上面的所有介绍都在围绕如何让网络IO不会被Block。但是网络IO处理仅仅是整个数据处理中的一部分。如果你留意到上文例子中的“处理事件”代码,就会发现这里可能是有问题的。

  • 处理代码有可能需要读写文件,可能会很慢,从而干扰整个程序的效率;
  • 处理代码有可能是一段复杂的数据计算,计算量很大的话,就会卡住整个执行流程;
  • 处理代码有bug,可能直接进入了一段死循环……

这时你会发现,这里的Block和本文之初讲的O_NONBLOCK是不同的事情。在一个网络服务中,如果处理程序的延迟远远小于网络IO,那么这完全不成问题。但是如果处理程序的延迟已经大到无法忽略了,就会对整个程序产生很大的影响。这时IO多路复用已经不是问题的关键。

试分析和比较下面两个场景:

  • web proxy。程序通过IO多路复用接收到了请求之后,直接转发给另外一个网络服务。
  • web server。程序通过IO多路复用接收到了请求之后,需要读取一个文件,并返回其内容。

它们有什么不同?它们的瓶颈可能出在哪里?

总结

小结一下本文:

  • 对于socket的文件描述符才有所谓BIO和NIO。
  • 多线程+BIO模式会带来大量的资源浪费,而NIO+IO多路复用可以解决这个问题。
  • 在Linux下,基于epoll的IO多路复用是解决这个问题的最佳方案;epoll相比selectpoll有很大的性能优势和功能优势,适合实现高性能网络服务。

操作系统层面聊聊BIO,NIO和AIO (epoll)的更多相关文章

  1. Java BIO NIO 与 AIO

    回顾 上一章我们介绍了操作系统层面的 IO 模型. 阻塞 IO 模型. 非阻塞 IO 模型. IO 复用模型. 信号驱动 IO 模型(用的不多,知道个概念就行). 异步 IO 模型. 并且介绍了 IO ...

  2. Java的BIO,NIO和AIO的区别于演进

    作者:公众号:我是攻城师 前言 Java里面的IO模型种类较多,主要包括BIO,NIO和AIO,每个IO模型都有不一样的地方,那么这些IO模型是如何演变呢,底层的原理又是怎样的呢? 本文我们就来聊聊. ...

  3. BIO,NIO与AIO的区别

    Java NIO : 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理.Java AIO(NIO.2 ...

  4. 关于BIO NIO和AIO的理解

    转载自 :http://blog.csdn.net/anxpp/article/details/51512200 1.BIO编程 1.1.传统的BIO编程 网络编程的基本模型是C/S模型,即两个进程间 ...

  5. 从Socket入门到BIO,NIO,multiplexing,AIO

    Socket入门 最简单的Server端读取Client端内容的demo public class Server { public static void main(String [] args) t ...

  6. 以Java的视角来聊聊BIO、NIO与AIO的区别?

    转: 以Java的视角来聊聊BIO.NIO与AIO的区别? 飞丫玲丫 17-07-2623:10 题目:说一下BIO/AIO/NIO 有什么区别?及异步模式的用途和意义? BIO(Blocking I ...

  7. 聊聊BIO、NIO与AIO的区别

    题目:说一下BIO/AIO/NIO 有什么区别?及异步模式的用途和意义? 1F 说一说I/O首先来说一下什么是I/O? 在计算机系统中I/O就是输入(Input)和输出(Output)的意思,针对不同 ...

  8. 五种I/O 模式,select、epoll方法的理解,BIO、NIO、AIO理解 相关文章

    一.io方式 Linux网络编程 五种I/O 模式及select.epoll方法的理解 web优化必须了解的原理之I/o的五种模型和web的三种工作模式 五种I/O 模式——阻塞(默认IO模式),非阻 ...

  9. bio、nio、aio及select、poll、epoll

    BIO与NIO.AIO的区别http://blog.csdn.net/skiof007/article/details/52873421 select.poll.epoll之间的区别总结http:// ...

随机推荐

  1. canvas动态图标

    前言 canvas 强大的功能让它成为了 HTML5 中非常重要的部分,至于它是什么,这里就不需要我多作介绍了.而可视化图表,则是 canvas 强大功能的表现之一. 现在已经有了很多成熟的图表插件都 ...

  2. Linux如何查找文件的创建时间

    Linux的文件能否找到文件的创建时间取决于文件系统类型,在ext4之前的早期文件系统中(ext.ext2.ext3),文件的元数据不会记录文件的创建时间,它只会记录访问时间.修改时间.更改时间(状态 ...

  3. ASP.NET Zero--基础设施

    基础设施 启动类 ASP.NET Core从应用程序中的Startup类初始化.我们在这个类中配置所有库(包括ABP).我们建议您先检查此课程.它也被集成到 OWIN.所以,你可以在这里添加OWIN中 ...

  4. SpringMVC的相关知识

    前几天学习了SpringMVC 感觉比Servlet好用得多具体如下: 首先SpringMVC的概念: SpringMVC是一个前端控制框架,主要用来负责与页面的交互.SpringMVC是Spring ...

  5. WPF软件开发系统之四——医疗病人信息管理系统

    仿360悬浮窗的方式,始终有个工具栏浮在桌面的最顶层,方便任何时候操作. 主要功能包括:病人信息的添加.修改.查询.包括别人基本信息.诊断结果.接待医生.手术多张图片等. 系统特点:简洁.易操作.美观 ...

  6. 如何使用java validation api进行参数校验----Hibernate-Validation

    在日常开发中,Hibernate Validator经常用来验证bean的字段,基于注解,方便快捷高效. 1. Bean Validation 中内置的 constraint 注解           ...

  7. kubeadm快速搭建k8s集群

    环境 master01:192.168.1.110 (最少2核CPU) node01:192.168.1.100 规划 services网络:10.96.0.0/12 pod网络:10.244.0.0 ...

  8. mysql 分组内 排序

    mysql 分组内 排序 类似于 sqlserver over partition by   因为mysql中木有sqlserver over partition by这个函数,要从sqlserver ...

  9. Ubuntu server 16.04的安装 以及配置(服务器版)

    1.在电脑上下载最新版本的Ubuntu服务器ISO映像,刻录到CD或创建一个可启动的USB盘. 2.视情况而定,进入到服务器的boot界面,把cd或者usb设置为启动第一项 3.开始安装   1)Ub ...

  10. 面试中被问Spring循环依赖的三种方式!!!

    什么是循环依赖? 循环依赖其实就是循环引用,也就是两个或则两个以上的 Bean 互相持有对方,最终形成闭环.比如A依赖于B,B依赖于C,C又依赖于A.如下图: 如果在日常开发中我们用new 对象的方式 ...