There is a race between the listener thread and the server thread in the UDP accept.
Hi All,
A work around for this is to use -P 1 on the server. Can you try that? I believe UTF sets that by default. I also wonder if this is FC30 related though probably not.
There seems to be a race between the listener thread and the server thread on the recvfrom. I perf hangs a recvfrom in listener thread, connects the socket after a first packet arrives and then creates a new server thread to handle that traffic. The Listener thread then hangs another recvfrom. (This subsequent recvfrom won't happen if -P is 1) It's done this way so -P parallel threads will work for UDP.
I think the server thread startup is racing with the listener thread as both have a recvfrom. The Listener thread recvfom only has the port and optionally the -B IP addr. The server thread recvfrom has the full quintuple per the connect. My understanding is the OS will route packets properly per the connect. That's what I've seen but I need to verify this with the netdev kernel team.
The change was the multicast work. I tuned this path for very fast drops as that's the only way multicast can work well. I think that tuning caused the Listener to win the race for high pps initial loads.
From cloudflare post:
Established-over-unconnected technique
For some services we are using a technique which we call "established-over-unconnected". This comes from a realization that on Linux it's possible to create a connected socket over an unconnected one. Consider this code:
Does this look hacky? Well, it should. What we do here is:
— We start a UDP unconnected socket.
— We wait for a client to come in.
— As soon as we receive the first packet from the client, we immediately create a new fully connected socket, over the unconnected socket! It shares the same local port and local IP.
This is how it might look in ss:
see blog
Here you can see the two sockets managed in our python test server. Notice the established socket is sharing the unconnected socket port.
This trick is basically reproducing the 'accept()` behaviour in UDP, where each ingress connection gets its own dedicated socket descriptor.
While this trick is nice, it's not without drawbacks — it's racy in two places. First, it's possible that the client will send more than one packet to the unconnected socket before the connected socket is created. The application code should work around it — if a packet received from the server socket belongs to an already existing connected flow, it shall be handed over to the right place. Then, during the creation of the connected socket, in the short window after bind() before connect() we might receive unexpected packets belonging to the unconnected socket! We don't want these packets here. It is necessary to filter the source IP/port when receiving early packets on the connected socket.
Is this approach worth the extra complexity? It depends on the use case. For a relatively small number of long-lived flows, it might be ok. For a high number of short-lived flows (especially DNS or NTP) it's an overkill.
Keeping old flows stable during service restarts is particularly hard in UDP. The established-over-unconnected technique is just one of the simpler ways of handling it. We'll leave another technique, based on SO_REUSEPORT ebpf, for a future blog post.
One of the features merged in the 3.9 development cycle was TCP and UDP support for the SO_REUSEPORT socket option; that support was implemented in a series of patches by Tom Herbert. The new socket option allows multiple sockets on the same host to bind to the same port, and is intended to improve the performance of multithreaded network server applications running on top of multicore systems.
The basic concept of SO_REUSEPORT is simple enough. Multiple servers (processes or threads) can bind to the same port if they each set the option as follows:
So long as the first server sets this option before binding its socket, then any number of other servers can also bind to the same port if they also set the option beforehand. The requirement that the first server must specify this option prevents port hijacking—the possibility that a rogue application binds to a port already used by an existing server in order to capture (some of) its incoming connections or datagrams. To prevent unwanted processes from hijacking a port that has already been bound by a server using SO_REUSEPORT, all of the servers that later bind to that port must have an effective user ID that matches the effective user ID used to perform the first bind on the socket.
SO_REUSEPORT can be used with both TCP and UDP sockets. With TCP sockets, it allows multiple listening sockets—normally each in a different thread—to be bound to the same port. Each thread can then accept incoming connections on the port by calling accept(). This presents an alternative to the traditional approaches used by multithreaded servers that accept incoming connections on a single socket.
https://lwn.net/Articles/542629/