source: doc/theses/thierry_delisle_PhD/thesis/text/eval_macro.tex @ 9d67a6d

ADTast-experimentalpthread-emulation
Last change on this file since 9d67a6d was 9d67a6d, checked in by Thierry Delisle <tdelisle@…>, 20 months ago

Merge branch 'master' of plg.uwaterloo.ca:software/cfa/cfa-cc

  • Property mode set to 100644
File size: 28.7 KB
Line 
1\chapter{Macro-Benchmarks}\label{macrobench}
2The previous chapter demonstrated the \CFA scheduler achieves its equivalent performance goal in small and controlled \at-scheduling scenarios.
3The next step is to demonstrate performance stays true in more realistic and complete scenarios.
4Therefore, this chapter exercises both \at and I/O scheduling using two flavours of webservers that demonstrate \CFA performs competitively with production environments.
5
6Webservers are chosen because they offer fairly simple applications that perform complex I/O, both network and disk, and are useful as standalone products.
7Furthermore, webservers are generally amenable to parallelization since their workloads are mostly homogeneous.
8Therefore, webservers offer a stringent performance benchmark for \CFA.
9Indeed, existing webservers have close to optimal performance, while the homogeneity of the workload means fairness may not be a problem.
10As such, these experiments should highlight the overhead due to any \CFA fairness cost in realistic scenarios.
11
12\section{Memcached}
13Memcached~\cite{memcached} is an in-memory key-value store used in many production environments, \eg \cite{atikoglu2012workload}.
14In fact, the Memcached server is so popular there exists a full-featured front-end for performance testing, called @mutilate@~\cite{GITHUB:mutilate}.
15Experimenting on Memcached allows for a simple test of the \CFA runtime as a whole, exercising the scheduler, the idle-sleep mechanism, as well the \io subsystem for sockets.
16Note, this experiment does not exercise the \io subsystem with regards to disk operations because Memcached is an in-memory server.
17
18\subsection{Benchmark Environment}
19The Memcached experiments are run on a cluster of homogeneous Supermicro SYS-6017R-TDF compute nodes with the following characteristics.
20\begin{itemize}
21\item
22The server runs Ubuntu 20.04.3 LTS on top of Linux Kernel 5.11.0-34.
23\item
24Each node has 2 Intel(R) Xeon(R) CPU E5-2620 v2 running at 2.10GHz.
25\item
26Each CPU has 6 cores and 2 \glspl{hthrd} per core, for a total of 24 \glspl{hthrd}.
27\item
28A CPU has 384 KB, 3 MB and 30 MB of L1, L2 and L3 caches, respectively.
29\item
30The compute nodes are connected to the network through a Mellanox 10 Gigabit Ethernet port.
31\item
32Network routing is performed by a Mellanox SX1012 10/40 Gigabit Ethernet switch.
33\end{itemize}
34
35\subsection{Memcached threading}\label{memcd:thrd}
36Memcached can be built to use multiple threads in addition to its @libevent@ subsystem to handle requests.
37When enabled, the threading implementation operates as follows~\cite[\S~16.2.2.8]{MemcachedThreading}:
38\begin{itemize}
39\item
40Threading is handled by wrapping functions within the code to provide basic protection from updating the same global structures at the same time.
41\item
42Each thread uses its own instance of the @libevent@ to help improve performance.
43\item
44TCP/IP connections are handled with a single thread listening on the TCP/IP socket.
45Each connection is then distributed to one of the active threads on a simple round-robin basis.
46Each connection then operates solely within this thread while the connection remains open.
47\item
48For UDP connections, all the threads listen to a single UDP socket for incoming requests.
49Threads that are not currently dealing with another request ignore the incoming packet.
50One of the remaining, nonbusy, threads reads the request and sends the response.
51This implementation can lead to increased CPU load as threads wake from sleep to potentially process the request.
52\end{itemize}
53Here, Memcached is based on an event-based webserver architecture~\cite{Pai99Flash}, using \gls{kthrd}ing to run multiple largely independent event engines, and if needed, spinning up additional kernel threads to handle blocking I/O.
54Alternative webserver architecture are:
55\begin{itemize}
56\item
57pipeline~\cite{Welsh01}, where the event engine is subdivided into multiple stages and the stages are connected with asynchronous buffers, where the final stage has multiple threads to handle blocking I/O.
58\item
59thread-per-connection~\cite{apache,Behren03}, where each incoming connection is served by a single \at in a strict 1-to-1 pairing, using the thread stack to hold the event state and folding the event engine implicitly into the threading runtime with its nonblocking I/O mechanism.
60\end{itemize}
61Both pipelining and thread-per-connection add flexibility to the implementation, as the serving logic can now block without halting the event engine~\cite{Harji12}.
62
63However, \gls{kthrd}ing in Memcached is not amenable to this work, which is based on \gls{uthrding}.
64While it is feasible to layer one user thread per kernel thread, it is not meaningful as it fails to exercise the user runtime;
65it simply adds extra scheduling overhead over the kernel threading.
66Hence, there is no direct way to compare Memcached using a kernel-level runtime with a user-level runtime.
67
68Fortunately, there exists a recent port of Memcached to \gls{uthrding} based on the libfibre~\cite{DBLP:journals/pomacs/KarstenB20} \gls{uthrding} library.
69This port did all of the heavy-lifting, making it straightforward to replace the libfibre user-threading with the \gls{uthrding} in \CFA.
70It is now possible to compare the original kernel-threading Memcached with both user-threading runtimes in libfibre and \CFA.
71
72As such, this Memcached experiment compares 3 different variations of Memcached:
73\begin{itemize}
74 \item \emph{vanilla}: the official release of Memcached, version~1.6.9.
75 \item \emph{fibre}: a modification of vanilla using the thread-per-connection model on top of the libfibre runtime.
76 \item \emph{cfa}: a modification of the fibre webserver that replaces the libfibre runtime with \CFA.
77\end{itemize}
78
79\subsection{Throughput} \label{memcd:tput}
80This experiment is done by having the clients establish 15,360 total connections, which persist for the duration of the experiment.
81The clients then send read and write queries with only 3\% writes (updates), attempting to follow a desired query rate, and the server responds to the desired rate as best as possible.
82Figure~\ref{fig:memcd:rate:qps} shows the 3 server versions at different client rates, ``Target \underline{Q}ueries \underline{P}er \underline{S}econd'', and the actual rate, ``Actual QPS'', for all three webservers.
83
84Like the experimental setup in Chapter~\ref{microbench}, each experiment is run 15 times, and for each client rate, the measured webserver rate is plotted.
85The solid line represents the median while the dashed and dotted lines represent the maximum and minimum respectively.
86For rates below 500K queries per seconds, all three webservers match the client rate.
87Beyond 500K, the webservers cannot match the client rate.
88During this interval, vanilla Memcached achieves the highest webserver throughput, with libfibre and \CFA slightly lower but very similar throughput.
89Overall the performance of all three webservers is very similar, especially considering that at 500K the servers have reached saturation, which is discussed more in the next section.
90
91\begin{figure}
92        \centering
93        \resizebox{0.83\linewidth}{!}{\input{result.memcd.rate.qps.pstex_t}}
94        \caption[Memcached Benchmark: Throughput]{Memcached Benchmark: Throughput\smallskip\newline Desired vs Actual query rate for 15,360 connections. Target QPS is the query rate that the clients are attempting to maintain and Actual QPS is the rate at which the server is able to respond.}
95        \label{fig:memcd:rate:qps}
96%\end{figure}
97\bigskip
98%\begin{figure}
99        \centering
100        \resizebox{0.83\linewidth}{!}{\input{result.memcd.rate.99th.pstex_t}}
101        \caption[Memcached Benchmark : 99th Percentile Latency]{Memcached Benchmark : 99th Percentile Latency\smallskip\newline 99th Percentile of the response latency as a function of \emph{desired} query rate for 15,360 connections. }
102        \label{fig:memcd:rate:tail}
103\end{figure}
104
105\subsection{Tail Latency}
106Another popular performance metric is \newterm{tail} latency, which indicates some notion of fairness among requests across the experiment, \ie do some requests wait longer than other requests for service.
107Since many web applications rely on a combination of different queries made in parallel, the latency of the slowest response, \ie tail latency, can dictate a performance perception.
108Figure~\ref{fig:memcd:rate:tail} shows the 99th percentile latency results for the same Memcached experiment.
109
110Again, each experiment is run 15 times with the median, maximum and minimum plotted with different lines.
111As expected, the latency starts low and increases as the server gets close to saturation, at which point, the latency increases dramatically because the webservers cannot keep up with the connection rate so client requests are disproportionally delayed.
112Because of this dramatic increase, the Y axis is presented using log scale.
113Note that the graph shows \emph{target} query rate, the actual response rate is given in Figure~\ref{fig:memcd:rate:qps} as this is the same underlying experiment.
114
115For all three servers, the saturation point is reached before 500K queries per second, which is when throughput starts to decline among the webservers.
116In this experiment, all three webservers are much more distinguishable than the throughput experiment.
117Vanilla Memcached achieves the lowest latency until 600K, after which all the webservers are struggling to respond to client requests.
118\CFA begins to decline at 600K, indicating some bottleneck after saturation.
119Overall, all three webservers achieve micro-second latencies and the increases in latency mostly follow each other.
120
121\subsection{Update rate}
122Since Memcached is effectively a simple database, the information that is cached can be written to concurrently by multiple queries.
123And since writes can significantly affect performance, it is interesting to see how varying the update rate affects performance.
124Figure~\ref{fig:memcd:updt} shows the results for the same experiment as the throughput and latency experiment but increasing the update percentage to 5\%, 10\% and 50\%, respectively, versus the original 3\% update percentage.
125
126\begin{figure}
127        \subfloat[][\CFA: Throughput]{
128                \resizebox{0.5\linewidth}{!}{
129                        \input{result.memcd.forall.qps.pstex_t}
130                }
131                \label{fig:memcd:updt:forall:qps}
132        }
133        \subfloat[][\CFA: Latency]{
134                \resizebox{0.5\linewidth}{!}{
135                        \input{result.memcd.forall.lat.pstex_t}
136                }
137                \label{fig:memcd:updt:forall:lat}
138        }
139
140        \subfloat[][LibFibre: Throughput]{
141                \resizebox{0.5\linewidth}{!}{
142                        \input{result.memcd.fibre.qps.pstex_t}
143                }
144                \label{fig:memcd:updt:fibre:qps}
145        }
146        \subfloat[][LibFibre: Latency]{
147                \resizebox{0.5\linewidth}{!}{
148                        \input{result.memcd.fibre.lat.pstex_t}
149                }
150                \label{fig:memcd:updt:fibre:lat}
151        }
152
153        \subfloat[][Vanilla: Throughput]{
154                \resizebox{0.5\linewidth}{!}{
155                        \input{result.memcd.vanilla.qps.pstex_t}
156                }
157                \label{fig:memcd:updt:vanilla:qps}
158        }
159        \subfloat[][Vanilla: Latency]{
160                \resizebox{0.5\linewidth}{!}{
161                        \input{result.memcd.vanilla.lat.pstex_t}
162                }
163                \label{fig:memcd:updt:vanilla:lat}
164        }
165        \caption[Throughput and Latency results at different update rates (percentage of writes).]{Throughput and Latency results at different update rates (percentage of writes).\smallskip\newline Description}
166        \label{fig:memcd:updt}
167\end{figure}
168
169In the end, this experiment mostly demonstrates that the performance of Memcached is affected very little by the update rate.
170Indeed, since values read/written can be bigger than what can be read/written atomically, a lock must be acquired while the value is read.
171Hence, I believe the underlying locking pattern for reads and writes is fairly similar, if not the same.
172These results suggest Memcached does not attempt to optimize reads/writes using a readers-writer lock to protect each value and instead just relies on having a sufficient number of keys to limit contention.
173In the end, the update experiment shows that \CFA is achieving equivalent performance.
174
175\section{Static Web-Server}
176The Memcached experiment does not exercise two key aspects of the \io subsystem: accept\-ing new connections and interacting with disks.
177On the other hand, a webserver servicing static web-pages does stress both accepting connections and disk \io by accepting tens of thousands of client requests per second where these requests return static data serviced from the file-system cache or disk.\footnote{
178Webservers servicing dynamic requests, which read from multiple locations and construct a response, are not as interesting since creating the response takes more time and does not exercise the runtime in a meaningfully different way.}
179The static webserver experiment compares NGINX~\cite{nginx} with a custom \CFA-based webserver developed for this experiment.
180
181\subsection{NGINX threading}
182NGINX is an high-performance, \emph{full-service}, event-driven webserver.
183It can handle both static and dynamic web content, as well as serve as a reverse proxy and a load balancer~\cite{reese2008nginx}.
184This wealth of features comes with a variety of potential configuration, dictating both available features and performance.
185The NGINX server runs a master process that performs operations such as reading configuration files, binding to ports, and controlling worker processes.
186When running as a static webserver, uses an event driven architecture to server incoming requests.
187Incoming connections are assigned a \emph{statckless} HTTP state-machine and worker processes can potentially handle thousands of these state machines.
188For the following experiment, NGINX was configured to use epoll to listen for events on these state machines and have each worker process independently accept new connections.
189Because of the realities of Linux, see Subsection~\ref{ononblock}, NGINX also maintains a pool of auxilary threads to handle block \io.
190The configuration can be used to set the number of worker processes desired, as well as the size of the auxilary pool.
191However, for the following experiments NGINX was configured to let the master process decided the appropriate number of threads.
192
193
194% Like memcached, NGINX can be made to use multiple \glspl{kthrd}.
195% It has a very similar architecture to the memcached architecture described in Section~\ref{memcd:thrd}, where multiple \glspl{kthrd} each run a mostly independent network logic.
196% While it does not necessarily use a dedicated listening thread, each connection is arbitrarily assigned to one of the \newterm{worker} threads.
197% Each worker thread handles multiple connections exclusively, effectively dividing the connections into distinct sets.
198% Again, this is effectively the \emph{event-based server} approach.
199%
200% \cit{https://www.nginx.com/blog/inside-nginx-how-we-designed-for-performance-scale/}
201
202% NGINX 1.13.7 is an high-performance, \emph{full-service}, event-driven, with multiple operating-system processes or multiple kernel-threads within a process to handle blocking I/O.
203% It can also .
204% Concurrent connections are handled using a complex event-driven architecture.
205% The NGINX server runs a master process that performs operations such as reading configuration files, binding to ports, and controlling worker processes.
206% NGINX uses a disk-based cache for performance, and assigns a dedicated process to manage the cache.
207% This process, known as the \textit{cache manager}, is spun-off by the master process.
208% Additionally, there can be many \textit{worker processes}, each handling network connections, reading and writing disk files, and communicating with upstream servers, such as reverse proxies or databases.
209
210% A worker is a single-threaded process, running independently of other workers.
211% The worker process handles new incoming connections and processes them.
212% Workers communicate using shared memory for shared cache and session data, and other shared resources.
213% Each worker assigns
214% As in a typical event-driven architecture, the worker listens for events from the clients, and responds immediately without blocking.
215% Memory use in NGINX is very conservative, because it does not spin up a new process or thread per connection, like Apache~\cite{apache} or \CFA.
216% All operations are asynchronous -- implemented using event notifications, callback functions and fine-tuned timers.
217
218\subsection{\CFA webserver}
219The \CFA webserver is a straightforward thread-per-connection webserver, where a fixed number of \ats are created upfront.
220Each \at calls @accept@, through @io_uring@, on the listening port and handles the incoming connection once accepted.
221Most of the implementation is fairly straightforward;
222however, the inclusion of file \io found an @io_uring@ problem that required an unfortunate workaround.
223
224Normally, webservers use @sendfile@~\cite{MAN:sendfile} to send files over a socket because it performs a direct move in the kernel from the file-system cache to the NIC, eliminating reading/writing the file into the webserver.
225While @io_uring@ does not support @sendfile@, it does supports @splice@~\cite{MAN:splice}, which is strictly more powerful.
226However, because of how Linux implements file \io, see Subsection~\ref{ononblock}, @io_uring@ must delegate splice calls to worker threads \emph{inside} the kernel.
227As of Linux 5.13, @io_uring@ had no mechanism to restrict the number of worker threads, and therefore, when tens of thousands of splice requests are made, it correspondingly creates tens of thousands of internal \glspl{kthrd}.
228Such a high number of \glspl{kthrd} slows Linux significantly.
229Rather than abandon the experiment, the \CFA webserver was switched to @sendfile@.
230
231Starting with \emph{blocking} @sendfile@, \CFA achieves acceptable performance until saturation is reached.
232At saturation, latency increases and client connections begin to timeout.
233As these clients close their connection, the server must close its corresponding side without delay so the OS can reclaim the resources used by these connections.
234Indeed, until the server connection is closed, the connection lingers in the CLOSE-WAIT TCP state~\cite{rfc:tcp} and the TCP buffers are preserved.
235However, this poses a problem using blocking @sendfile@ calls:
236when @sendfile@ blocks, the \proc rather than the \at blocks, preventing other connections from closing their sockets.
237The call can block if there is insufficient memory, which can be caused by having too many connections in the CLOSE-WAIT state.\footnote{
238\lstinline{sendfile} can always block even in nonblocking mode if the file to be sent is not in the file-system cache, because Linux does not provide nonblocking disk I/O.}
239This effect results in a negative feedback where more timeouts lead to more @sendfile@ calls running out of resources.
240
241Normally, this problem is addressed by using @select@/@epoll@ to wait for sockets to have sufficient resources.
242However, since @io_uring@ does not support @sendfile@ but does respects non\-blocking semantics, marking all sockets as non-blocking effectively circumvents the @io_uring@ subsystem entirely:
243all calls simply immediately return @EAGAIN@ and all asynchronicity is lost.
244
245Switching all of the \CFA runtime to @epoll@ for this experiment is unrealistic and does not help in the evaluation of the \CFA runtime.
246For this reason, the \CFA webserver sets and resets the @O_NONBLOCK@ flag before and after any calls to @sendfile@.
247However, when the nonblocking @sendfile@ returns @EAGAIN@, the \CFA server cannot block the \at because its I/O subsystem uses @io_uring@.
248Therefore, the \at spins performing the @sendfile@, yields if the call returns @EAGAIN@, and retries in these cases.
249
250Interestingly, Linux 5.15 @io_uring@ introduces the ability to limit the number of worker threads that are created through the @IORING_REGISTER_IOWQ_MAX_WORKERS@ option.
251Presumably, this limit would prevent the explosion of \glspl{kthrd}, which justified using @sendfile@ over @io_uring@ and @splice@.
252However, recall from Section~\ref{iouring} that @io_uring@ maintains two pools of workers: bounded workers and unbounded workers.
253For a webserver, the unbounded workers should handle accepts and reads on socket, and the bounded workers should handle reading files from disk.
254This setup allows fine-grained control over the number of workers needed for each operation type and presumably lead to good performance.
255
256However, @io_uring@ must contend with another reality of Linux: the versatility of @splice@.
257Indeed, @splice@ can be used both for reading and writing to or from any type of file descriptor.
258This generality makes it ambiguous which pool @io_uring@ should delegate @splice@ calls to.
259In the case of splicing from a socket to pipe, @splice@ behaves like an unbounded operation, but when splicing from a regular file to a pipe, @splice@ becomes a bounded operation.
260To make things more complicated, @splice@ can read from a pipe and write to a regular file.
261In this case, the read is an unbounded operation but the write is a bounded one.
262This leaves @io_uring@ in a difficult situation where it can be very difficult to delegate splice operations to the appropriate type of worker.
263Since there is little or no context available to @io_uring@, it seems to always delegate @splice@ operations to the unbounded workers.
264This decision is unfortunate for this specific experiment, since it prevents the webserver from limiting the number of parallel calls to @splice@ without affecting the performance of @read@ or @accept@.
265For this reason, the @sendfile@ approach described above is still the most performant solution in Linux 5.15.
266
267One possible workaround is to create more @io_uring@ instances so @splice@ operations can be issued to a different instance than the @read@ and @accept@ operations.
268However, I do not believe this solution is appropriate in general;
269it simply replaces my current webserver hack with a different, equivalent hack.
270
271\subsection{Benchmark Environment}
272Unlike the Memcached experiment, the webserver experiment is run on a heterogeneous environment.
273\begin{itemize}
274\item
275The server runs Ubuntu 20.04.4 LTS on top of Linux Kernel 5.13.0-52.
276\item
277The server computer has four AMD Opteron(tm) Processor 6380 with 16 cores running at 2.5GHz, for a total of 64 \glspl{hthrd}.
278\item
279The computer is booted with only 8 CPUs enabled, which is sufficient to achieve line rate.
280\item
281Each CPU has 64 KB, 256 KiB and 8 MB of L1, L2 and L3 caches respectively.
282\item
283The computer is booted with only 25GB of memory to restrict the file-system cache.
284\end{itemize}
285There are 8 client machines.
286\begin{itemize}
287\item
288A client runs a 2.6.11-1 SMP Linux kernel, which permits each client load-generator to run on a separate CPU.
289\item
290It has two 2.8 GHz Xeon CPUs, and four one-gigabit Ethernet cards.
291\item
292\todo{switch}
293\item
294A client machine runs two copies of the workload generator.
295\end{itemize}
296The clients and network are sufficiently provisioned to drive the server to saturation and beyond.
297Hence, any server effects are attributable solely to the runtime system and webserver.
298Finally, without restricting the server hardware resources, it is impossible to determine if a runtime system or the webserver using it has any specific design restrictions, \eg using space to reduce time.
299Trying to determine these restriction with large numbers of processors or memory simply means running equally large experiments, which takes longer and are harder to set up.
300
301\subsection{Throughput}
302To measure webserver throughput, the server computer is loaded with 21,600 files, sharded across 650 directories, occupying about 2.2GB of disk, distributed over the server's RAID-5 4-drives to achieve high throughput for disk I/O.
303The clients run httperf~\cite{httperf} to request a set of static files.
304The httperf load-generator is used with session files to simulate a large number of users and to implement a partially open-loop system.
305This permits httperf to produce overload conditions, generate multiple requests from persistent HTTP/1.1 connections, and include both active and inactive off periods to model browser processing times and user think times~\cite{Barford98}.
306
307The experiments are run with 16 clients, each running a copy of httperf (one copy per CPU), requiring a set of 16 log files with requests conforming to a Zipf distribution.
308This distribution is representative of users accessing static data through a web browser.
309Each request reads a file name from its trace, establishes a connection, performs an HTTP get-request for the file name, receive the file data, close the connection, and repeat the process.
310Some trace elements have multiple file names that are read across a persistent connection.
311A client times-out if the server does not complete a request within 10 seconds.
312
313An experiment consists of running a server with request rates ranging from 10,000 to 70,000 requests per second;
314each rate takes about 5 minutes to complete.
315There is 20 seconds idle time between rates and between experiments to allow connections in the TIME-WAIT state to clear.
316Server throughput is measured both at peak and after saturation (\ie after peak).
317Peak indicates the level of client requests the server can handle and after peak indicates if a server degrades gracefully.
318Throughput is measured by aggregating the results from httperf for all the clients.
319
320This experiment can be done for two workload scenarios by reconfiguring the server with different amounts of memory: 25 GB and 2.5 GB.
321The two workloads correspond to in-memory and disk-I/O respectively.
322Due to the Zipf distribution, only a small amount of memory is needed to service a significant percentage of requests.
323Table~\ref{t:CumulativeMemory} shows the cumulative memory required to satisfy the specified percentage of requests; e.g., 95\% of the requests come from 126.5 MB of the file set and 95\% of the requests are for files less than or equal to 51,200 bytes.
324Interestingly, with 2.5 GB of memory, significant disk-I/O occurs.
325
326\begin{table}
327\caption{Cumulative memory for requests by file size}
328\label{t:CumulativeMemory}
329\begin{tabular}{r|rrrrrrrr}
330\% Requests   & 10 & 30 & 50 & 70 & 80 & 90 & \textbf{95} & 100 \\
331Memory (MB)   & 0.5 & 1.5 & 8.4 & 12.2 & 20.1 & 94.3 & \textbf{126.5} & 2,291.6 \\
332File Size (B) & 409 & 716 & 4,096 & 5,120 & 7,168 & 40,960 & \textbf{51,200} & 921,600
333\end{tabular}
334\end{table}
335
336\begin{figure}
337        \centering
338        \subfloat[][Throughput]{
339                \resizebox{0.85\linewidth}{!}{\input{result.swbsrv.25gb.pstex_t}}
340                \label{fig:swbsrv:ops}
341        }
342
343        \subfloat[][Rate of Errors]{
344                \resizebox{0.85\linewidth}{!}{\input{result.swbsrv.25gb.err.pstex_t}}
345                \label{fig:swbsrv:err}
346        }
347        \caption[Static Webserver Benchmark : Throughput]{Static Webserver Benchmark : Throughput\smallskip\newline Throughput vs request rate for short lived connections connections.}
348        \label{fig:swbsrv}
349\end{figure}
350
351Figure~\ref{fig:swbsrv} shows the results comparing \CFA to NGINX in terms of throughput.
352These results are fairly straightforward.
353Both servers achieve the same throughput until around 57,500 requests per seconds.
354Since the clients are asking for the same files, the fact that the throughput matches exactly is expected as long as both servers are able to serve the request rate.
355Once the saturation point is reached, both servers are still very close.
356NGINX achieves slightly better throughput.
357However, Figure~\ref{fig:swbsrv:err} shows the rate of errors, a gross approximation of tail latency, where \CFA achieves notably fewer errors once the servers reach saturation.
358This suggests \CFA is slightly fairer with less throughput, while NGINX sacrifice fairness for more throughput.
359This experiment demonstrate that the \CFA webserver is able to match the performance of NGINX up-to and beyond the saturation point of the machine.
360
361\subsection{Disk Operations}
362With 25GB of memory, the entire experimental file-set plus the webserver and OS fit in memory.
363If memory is constrained, the OS must evict files from the file cache, which causes @sendfile@ to read from disk.\footnote{
364For the in-memory experiments, the file-system cache was warmed by running an experiment three times before measuring started to ensure all files are in the file-system cache.}
365Webservers can behave very differently once file I/O begins and increases.
366Hence, prior work~\cite{Harji10} suggests running both kinds of experiments to test overall webserver performance.
367
368However, after reducing memory to 2.5GB, the problem with @splice@ and @io_uring@ rears its ugly head again.
369Indeed, in the in-memory configuration, replacing @splice@ with calls to @sendfile@ works because the bounded side basically never blocks.
370Like @splice@, @sendfile@ is in a situation where the read side requires bounded blocking, \eg reading from a regular file, while the write side requires unbounded blocking, \eg blocking until the socket is available for writing.
371The unbounded side can be handled by yielding when it returns @EAGAIN@, as mentioned above, but this trick does not work for the bounded side.
372The only solution for the bounded side is to spawn more threads and let these handle the blocking.
373
374Supporting this case in the webserver would require creating more \procs or creating a dedicated thread-pool.
375However, I felt this kind of modification moves too far away from my goal of evaluating the \CFA runtime, \ie it begins writing another runtime system;
376hence, I decided to forgo experiments on low-memory performance.
Note: See TracBrowser for help on using the repository browser.