kernel - Remove bottlenecks related to mass pipe closures
authorMatthew Dillon <dillon@apollo.backplane.com>
Mon, 14 Aug 2017 02:48:28 +0000 (19:48 -0700)
committerMatthew Dillon <dillon@apollo.backplane.com>
Mon, 14 Aug 2017 03:03:14 +0000 (20:03 -0700)
* These changes fix teardown latencies involved with destroying a large
  number of pipe()s at the same time, for example when a large number
  of processes are killed simultaneously.  Testing with 900,000 processes
  chaining 900,000 pipe()s, the system could become unusable for upwards
  of an hour when the test is killed with ^C prior to these changes.

  After these changes the system balks for less than 30 seconds in the
  900,000 process case.

* Wrap pipe teardowns in a pcpu mutex.  Such teardowns only occur when the
  normal pcpu pipe cache / hysteresis gets blown out due to a large number
  of closures occurring simultaneously and are not typically in the
  critical path.

  The pcpu mutex allows good concurrency while also limiting the number of
  processes which can be tearing down pipe KVM at the same time.  If
  we have say 900,000 processes all exiting, this mechanism limits the
  teardown to (ncpus) concurrent processes.

  The mutex uses a queued lock (whereas the lockmgr spams wakeup()s),
  so we use a mutex to avoid a degenerate O(N^2) wakeup situation.
  This removes an enormous amount of overhead during the teardown.

* When removing areas from the kernel_map, issue a pmap_remove() call
  on the kernel_pmap before cleaning up the underlying VM object just as
  we do for other types of maps.  This was originally not done because the
  kernel_pmap can be extremely sparse and iteration could be inefficient.

  However, the current pmap implementation handles sparse pmaps just
  fine.  Removing the pages in bulk reduces the number of global IPIs
  that wind up being issued during the removal, greatly improving
  performance when a large volume of kernel resource is being destroyed
  all at once.

* Increase the zalloc burst from 4 pages to 32 pages.  When adding pages,
  use more relaxed vm_page_alloc() flags after the 4th page to try to
  avoid blowing up the emergency free page cache.

sys/kern/sys_pipe.c
sys/vm/vm_map.c
sys/vm/vm_zone.c
sys/vm/vm_zone.h

index 03a22d5..ac2c0c7 100644 (file)
 #include <sys/sysctl.h>
 #include <sys/socket.h>
 #include <sys/kern_syscall.h>
+#include <sys/lock.h>
+#include <sys/mutex.h>
 
 #include <vm/vm.h>
 #include <vm/vm_param.h>
-#include <sys/lock.h>
 #include <vm/vm_object.h>
 #include <vm/vm_kern.h>
 #include <vm/vm_extern.h>
@@ -61,6 +62,7 @@
 
 #include <sys/file2.h>
 #include <sys/signal2.h>
+#include <sys/mutex2.h>
 
 #include <machine/cpufunc.h>
 
@@ -122,6 +124,7 @@ static int pipe_bcache_alloc;
 static int pipe_bkmem_alloc;
 static int pipe_rblocked_count;
 static int pipe_wblocked_count;
+static struct mtx *pipe_gdlocks;
 
 SYSCTL_NODE(_kern, OID_AUTO, pipe, CTLFLAG_RW, 0, "Pipe operation");
 SYSCTL_INT(_kern_pipe, OID_AUTO, nbig,
@@ -154,6 +157,7 @@ void
 pipeinit(void *dummy)
 {
        size_t mbytes = kmem_lim_size();
+       int n;
 
        if (pipe_maxbig == LIMITBIGPIPES) {
                if (mbytes >= 7 * 1024)
@@ -167,6 +171,10 @@ pipeinit(void *dummy)
                if (mbytes >= 15 * 1024)
                        pipe_maxcache *= 2;
        }
+       pipe_gdlocks = kmalloc(sizeof(*pipe_gdlocks) * ncpus,
+                            M_PIPE, M_WAITOK | M_ZERO);
+       for (n = 0; n < ncpus; ++n)
+               mtx_init(&pipe_gdlocks[n], "pipekm");
 }
 SYSINIT(kmem, SI_BOOT2_MACHDEP, SI_ORDER_ANY, pipeinit, NULL);
 
@@ -1130,6 +1138,9 @@ pipe_shutdown(struct file *fp, int how)
        return (error);
 }
 
+/*
+ * Destroy the pipe buffer.
+ */
 static void
 pipe_free_kmem(struct pipe *cpipe)
 {
@@ -1216,7 +1227,22 @@ pipeclose(struct pipe *cpipe)
                lockmgr(cpipe->pipe_slock, LK_RELEASE);
 
        /*
-        * If we disassociated from our peer we can free resources
+        * If we disassociated from our peer we can free resources.  We
+        * maintain a pcpu cache to improve performance, so the actual
+        * tear-down case is limited to bulk situations.
+        *
+        * However, the bulk tear-down case can cause intense contention
+        * on the kernel_map when, e.g. hundreds to hundreds of thousands
+        * of processes are killed at the same time.  To deal with this we
+        * use a pcpu mutex to maintain concurrency but also limit the
+        * number of threads banging on the map and pmap.
+        *
+        * We use the mtx mechanism instead of the lockmgr mechanism because
+        * the mtx mechanism utilizes a queued design which will not break
+        * down in the face of thousands to hundreds of thousands of
+        * processes trying to free pipes simultaneously.  The lockmgr
+        * mechanism will wind up waking them all up each time a lock
+        * cycles.
         */
        if (ppipe == NULL) {
                gd = mycpu;
@@ -1227,7 +1253,9 @@ pipeclose(struct pipe *cpipe)
                if (gd->gd_pipeqcount >= pipe_maxcache ||
                    cpipe->pipe_buffer.size != PIPE_SIZE
                ) {
+                       mtx_lock(&pipe_gdlocks[gd->gd_cpuid]);
                        pipe_free_kmem(cpipe);
+                       mtx_unlock(&pipe_gdlocks[gd->gd_cpuid]);
                        kfree(cpipe, M_PIPE);
                } else {
                        cpipe->pipe_state = 0;
index 0f271d0..187eb0e 100644 (file)
@@ -2965,6 +2965,12 @@ again:
                /*
                 * Unwire before removing addresses from the pmap; otherwise,
                 * unwiring will put the entries back in the pmap.
+                *
+                * Generally speaking, doing a bulk pmap_remove() before
+                * removing the pages from the VM object is better at
+                * reducing unnecessary IPIs.  The pmap code is now optimized
+                * to not blindly iterate the range when pt and pd pages
+                * are missing.
                 */
                if (entry->wired_count != 0)
                        vm_map_entry_unwire(map, entry);
@@ -2972,6 +2978,7 @@ again:
                offidxend = offidxstart + count;
 
                if (object == &kernel_object) {
+                       pmap_remove(map->pmap, s, e);
                        vm_object_hold(object);
                        vm_object_page_remove(object, offidxstart,
                                              offidxend, FALSE);
index f495082..7d803af 100644 (file)
@@ -332,9 +332,9 @@ zinitna(vm_zone_t z, char *name, size_t size, long nentries, uint32_t flags)
        z->zpagecount = 0;
 
        /*
-        * Reduce kernel_map spam by allocating in chunks of 4 pages.
+        * Reduce kernel_map spam by allocating in chunks.
         */
-       z->zalloc = 4;
+       z->zalloc = ZONE_MAXPGLOAD;
 
        /*
         * Populate the interrrupt zone at creation time rather than
@@ -498,12 +498,21 @@ zget(vm_zone_t z)
                 * simply populate an existing mapping.
                 *
                 * First allocate as many pages as we can, stopping at
-                * our limit or if the page allocation fails.
+                * our limit or if the page allocation fails.  Try to
+                * avoid exhausting the interrupt free minimum by backing
+                * off to normal page allocations after a certain point.
                 */
                for (i = 0; i < ZONE_MAXPGLOAD && i < z->zalloc; ++i) {
-                       m = vm_page_alloc(NULL,
-                                         mycpu->gd_rand_incr++,
-                                         z->zallocflag);
+                       if (i < 4) {
+                               m = vm_page_alloc(NULL,
+                                                 mycpu->gd_rand_incr++,
+                                                 z->zallocflag);
+                       } else {
+                               m = vm_page_alloc(NULL,
+                                                 mycpu->gd_rand_incr++,
+                                                 VM_ALLOC_NORMAL |
+                                                 VM_ALLOC_SYSTEM);
+                       }
                        if (m == NULL)
                                break;
                        pgs[i] = m;
index 1d40255..111c1d0 100644 (file)
@@ -25,7 +25,7 @@
 #define ZONE_USE_RESERVE 0x0020        /* use reserve memory if necessary */
 #define ZONE_DESTROYABLE 0x0040 /* can be zdestroy()'ed */
 
-#define ZONE_MAXPGLOAD       /* max VM pages burst in zget() */
+#define ZONE_MAXPGLOAD 32      /* max VM pages burst in zget() */
 
 #include <sys/spinlock.h>
 #include <sys/thread.h>