Why does Nginx perform so much better than Apache

  • 2020-05-15 02:43:20
  • OfStack

Why does Nginx perform so much better than Apache? This is due to Nginx's use of the latest epoll (Linux 2.6 kernel) and kqueue (freebsd) network I/O models, while Apache USES the traditional select model.

Currently, Squid and Memcached, which can withstand high concurrent access under Linux, adopt epoll network I/O model.

The select network I/O model used by Apache is very inefficient for handling large Numbers of connection reads and writes.

The following metaphor is used to explain the difference between the select model used by Apache and the epoll model used by Nginx:

Let's say you're in college, and your dorm building has a lot of rooms, and your friends are coming to see you.

Your aunt will take your friends from room to room until she finds you.

The dorm administrator's mother would write down the room number of each student,

When your friends come, just tell them which room you live in, and don't have to walk your friends around the building.

If 10,000 people come, all want to find their own students living in this building, select version or epoll version of the dormitory administrator aunt, who is more efficient, it is self-evident.

Similarly, in high concurrency servers, polling I/O is one of the most time-consuming operations, and it is equally clear which of select's or epoll's performance is higher.


epoll - I/O event notification facility

In the network programming of linux, select has been used for event triggering for a long time.

In the new linux kernel, there is a mechanism to replace it, epoll.

The biggest advantage of epoll over select is that it does not become less efficient as the number of fd is increased.

Because in the select implementation in the kernel, it is handled by polling, the more the number of fd polled, the more time it naturally takes.

In addition, the linux/ posix_types.h header file has the following declaration:

#define __FD_SETSIZE 1024

Means that select is listening to a maximum of 1024 fd at the same time. Of course, you can expand this number by modifying the header file and recompiling the kernel, but this does not seem to be a cure.

The interface of epoll is very simple. There are 3 functions for 1:

1. int epoll_create(int size);

Create a handle to epoll, which is used to tell the kernel how many 1's there are.

This parameter is different from the first parameter in select() and gives the value of the maximum listened fd+1.

Note that when the epoll handle is created, it will occupy 1 fd value. If you view /proc/ process id/fd/ under linux,

You can see this fd, so after using epoll, you must call close() to turn it off, or you may run out of fd.

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

epoll's event registration function, unlike select(), tells the kernel what type of event to listen for when listening for an event,

Instead, you register the type of event you want to listen to. The first parameter is the return value of epoll_create(),

The second parameter represents the action, which is represented by three macros:

EPOLL_CTL_ADD: register new fd to epfd;

EPOLL_CTL_MOD: modify the registered fd listening event;

EPOLL_CTL_DEL: delete 1 fd from epfd;

The third parameter is fd to be listened to, and the fourth parameter is to tell the kernel what to listen to. The structure of struct epoll_event is as follows:


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 */
};

events can be a collection of the following macros:
EPOLLIN: means that the corresponding file descriptor can be read (including the opposite end SOCKET normally closed);
EPOLLOUT: means that the corresponding file descriptor can be written;
EPOLLPRI: indicates that the corresponding file descriptor has urgent data to read.
EPOLLERR: indicates that the corresponding file descriptor has an error;
EPOLLHUP: means that the corresponding file descriptor is hung up;
EPOLLET: set EPOLL to the edge trigger (Edge Triggered) mode, as opposed to the horizontal trigger (Level Triggered).
EPOLLONESHOT: listen for only one event. After listening for this event,
If you want to continue listening for socket, you need to add socket to the EPOLL queue again

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

Wait for an event to occur, similar to the select() call.
The parameter events is used to get the set of events from the kernel. maxevents tells the kernel how big the events is. The value of maxevents cannot be greater than size when epoll_create() was created.
This function returns the number of events that need to be processed, such as 0 for timeout.

4. About the two working modes of ET and LT:

It can be concluded that:
ET mode only notified when, when a state change of the so-called state changes here does not include buffer and unprocessed data, that is to say, if you want to adopt the ET mode, need 1 straight read/write until error, a lot of people reflect why ET model received only 1 part of the data is no longer inform, mostly because of this; The LT mode notifies whenever there is data not processed.

So how do you use epoll? It's very simple.
By including 1 header file #include < sys/epoll.h > And a few simple API will greatly increase the number of people your web server supports.
First create a handle to epoll by create_epoll(int maxfds), where maxfds is the maximum number of handles supported by your epoll.
This function returns a new epoll handle through which all subsequent operations will be performed.
Remember to close the created epoll handle with close() after using it.
Then in your network main loop, each frame of the call epoll_wait(int epfd, epoll_event events, int max events, int timeout) queries all the network interfaces to see which one can be read and which one can be written. The basic grammar is:
nfds = epoll_wait(kdpfd, events, maxevents, -1);
Where kdpfd is the handle created with epoll_create, events is a pointer to epoll_event*, when epoll_wait is successfully operated, epoll_events will store all read and write events.
max_events is the number of all socket handles you currently need to listen to. The last one, timeout, is the timeout for epoll_wait, which means to return immediately when it is 0, which means to wait until there is an event range when it is -1, which means to wait until there is an event range when it is any positive integer, which means to wait that long if there is no event at 1, then the range. If the network main loop is a separate thread, can use -1 to wait, so you can ensure 1 some efficiency, if it is and the main logic in the same thread, you can use 0 to ensure the efficiency of the main loop.

The epoll_wait range should be followed by a loop, traversing all events.
Almost all epoll programs use the following framework:


for( ; ; )
 {
  nfds = epoll_wait(epfd,events,20,500);
  for(i=0;i<nfds;++i)
  {
   if(events[i].data.fd==listenfd) // There's a new connection 
   {
    connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); //accept This connection 
    ev.data.fd=connfd;
    ev.events=EPOLLIN|EPOLLET;
    epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); // Will the new fd Added to the epoll In the listening queue 
   }
   else if( events[i].events&EPOLLIN ) // Received data, read socket
   {
    n = read(sockfd, line, MAXLINE)) < 0 // read 
    ev.data.ptr = md;  //md For custom types, add data 
    ev.events=EPOLLOUT|EPOLLET;
    epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);// Modify the identifier and wait 1 Sending data in a loop, the essence of asynchronous processing 
   }
   else if(events[i].events&EPOLLOUT) // There's data to send, write socket
   {
    struct myepoll_data* md = (myepoll_data*)events[i].data.ptr; // Take the data 
    sockfd = md->fd;
    send( sockfd, md->ptr, strlen((char*)md->ptr), 0 );  // To send data 
    ev.data.fd=sockfd;
    ev.events=EPOLLIN|EPOLLET;
    epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); // Modify the identifier and wait 1 Receive data in four cycles 
   }
   else
   {
    // Other processing 
   }
  }
 }


Related articles: