flow.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. package nebula
  2. import (
  3. "fmt"
  4. "log/slog"
  5. "runtime"
  6. "sync"
  7. "time"
  8. "github.com/rcrowley/go-metrics"
  9. "github.com/slackhq/nebula/config"
  10. "github.com/slackhq/nebula/noiseutil"
  11. )
  12. // Staged processing (tun.staged.workers) moves crypto off each tun reader and udp listener onto worker goroutines,
  13. // behind which an ordered stage sends or writes every batch in read order. It reuses the inline packet handlers:
  14. // staging changes ownership and scheduling, not policy. Each routine has its own workers and ordered stages, so order
  15. // is per routine. Full queues block the reader. A read error drains the batches in flight, while shutdown waits for
  16. // the crypto and then drops packets whose destination has closed. A TX slot holds one tun read and its ciphertext,
  17. // about 1.1 MiB; there are eight, or two more than the workers. An RX slot holds one recvmmsg batch, listen.batch
  18. // times 64 KiB with GRO, and the count keeps up to 384 entries in flight in four to six slots: six and 24 MiB at
  19. // listen.batch=64.
  20. // tun.staged.tx_slots and rx_slots override the counts. Small-MSS TX arenas can grow.
  21. //
  22. // Metrics are registered only for active pipelines. flow.{tx,rx}.batches counts reads handed to the ordered stage and
  23. // flow.{tx,rx}.inline those the reader handled itself (TX inline also counts oversized reads); their rates show
  24. // whether staging is used. flow.tx.gate_switches counts transmit switches between inline and fanout mode.
  25. // flow.stage.<stage>.q<queue>.busy_ns locates the limiting stage: tx_read, tx_encrypt, tx_send, rx_read, rx_decrypt or
  26. // rx_write. Take rates; 1e9 ns/s fills a serial stage. These are elapsed times that exclude read syscalls and
  27. // downstream waits; descheduling inflates them and worker times add up across the pool.
  28. // Inline work counts at the reader; small RX batches sent to the writer include decryption in writer time.
  29. // stagedConfig is tun.staged. workers is per routine and direction; zero keeps processing inline.
  30. type stagedConfig struct {
  31. workers int
  32. tx bool
  33. rx bool
  34. // inlineBelow is the TX segment or RX packet count below which a read may skip the worker handoff, 0 never: a TX
  35. // read in inline mode, an RX read with nothing in flight
  36. inlineBelow int
  37. // txSlots and rxSlots are how many batches each routine can have in flight per direction
  38. txSlots, rxSlots int
  39. // gateHold is how many TX reads in a row must want the other mode before a routine switches between inline and
  40. // fanout transmit
  41. gateHold int
  42. }
  43. // stagedGateHold is the default tun.staged.gate_hold
  44. const stagedGateHold = 64
  45. const (
  46. // stagedTxSlots is the default TX slot count, unless the workers need more
  47. stagedTxSlots = 8
  48. // stagedRxEntries is how many recvmmsg entries the RX slots hold in flight by default
  49. stagedRxEntries = 6 * 64
  50. // stagedMinSlots lets the reader fill one batch while another is in flight
  51. stagedMinSlots = 2
  52. // stagedMinRxSlots keeps the default RX pipeline deep enough for large listen.batch values
  53. stagedMinRxSlots = 4
  54. // stagedMaxRxSlots is the measured default, a smaller listen.batch does not take more slots
  55. stagedMaxRxSlots = 6
  56. )
  57. // stagedSlots sizes the TX and RX slots. TX reads are bounded by the tun queue, so one slot per worker plus the one
  58. // being read and the one being sent, and at least the eight measured. RX slots keep stagedRxEntries in flight, four
  59. // to six, so a larger listen.batch means fewer slots and RX memory never exceeds the six measured. A positive override replaces either, down to stagedMinSlots.
  60. func stagedSlots(workers, batch, txOverride, rxOverride int) (tx, rx int) {
  61. tx = max(stagedTxSlots, workers+2)
  62. rx = min(max(stagedMinRxSlots, (stagedRxEntries+batch-1)/max(batch, 1)), stagedMaxRxSlots)
  63. if txOverride > 0 {
  64. tx = max(txOverride, stagedMinSlots)
  65. }
  66. if rxOverride > 0 {
  67. rx = max(rxOverride, stagedMinSlots)
  68. }
  69. return tx, rx
  70. }
  71. // parseStagedConfig enables staging on Linux only; queues and conns that cannot hand off batches also stay inline.
  72. func parseStagedConfig(c *config.C, l *slog.Logger) stagedConfig {
  73. sc := stagedConfig{workers: c.GetInt("tun.staged.workers", 0)}
  74. if sc.workers <= 0 {
  75. return stagedConfig{}
  76. }
  77. if runtime.GOOS != "linux" {
  78. l.Warn("tun.staged is only supported on Linux; packet processing stays inline")
  79. return stagedConfig{}
  80. }
  81. sc.tx, sc.rx = c.GetBool("tun.staged.tx", true), true
  82. // 256 is the measured crossover, 0 hands every batch to the workers
  83. sc.inlineBelow = max(c.GetInt("tun.staged.inline_below", 256), 0)
  84. if sc.tx && noiseutil.EncryptLockNeeded {
  85. l.Warn("tun.staged cannot encrypt in parallel with boringcrypto or fips140; transmit stays inline")
  86. sc.tx = false
  87. }
  88. sc.gateHold = max(c.GetInt("tun.staged.gate_hold", stagedGateHold), 1)
  89. sc.txSlots, sc.rxSlots = stagedSlots(sc.workers, c.GetInt("listen.batch", 64),
  90. c.GetInt("tun.staged.tx_slots", 0), c.GetInt("tun.staged.rx_slots", 0))
  91. l.Info("Staged packet processing enabled", "workers", sc.workers, "tx", sc.tx, "rx", sc.rx,
  92. "inlineBelow", sc.inlineBelow, "gateHold", sc.gateHold,
  93. "txSlots", sc.txSlots, "rxSlots", sc.rxSlots)
  94. return sc
  95. }
  96. // flowJob is one batch of crypto work. It owns its packet buffers until run returns; jobs and queues are reused.
  97. type flowJob interface{ run(nb []byte) }
  98. // flowPool runs one reader's crypto jobs. The reader is the only producer and closes the pool once its ordered stage
  99. // has drained.
  100. type flowPool struct {
  101. jobs chan flowJob
  102. wg sync.WaitGroup
  103. }
  104. func newFlowPool(workers, queueLen int, busy metrics.Counter) *flowPool {
  105. p := &flowPool{jobs: make(chan flowJob, queueLen)}
  106. for range workers {
  107. p.wg.Go(func() {
  108. nb := make([]byte, 12)
  109. for job := range p.jobs {
  110. // One pair of clock reads covers a run of jobs that are queued back to back
  111. t := time.Now()
  112. for job != nil {
  113. job.run(nb)
  114. job = p.next()
  115. }
  116. busy.Inc(int64(time.Since(t)))
  117. }
  118. })
  119. }
  120. return p
  121. }
  122. // next returns the next queued job without waiting, or nil.
  123. func (p *flowPool) next() flowJob {
  124. select {
  125. case job := <-p.jobs:
  126. return job
  127. default:
  128. return nil
  129. }
  130. }
  131. func (p *flowPool) close() { close(p.jobs); p.wg.Wait() }
  132. // stageBusy is the busy time counter of one stage of routine q.
  133. func stageBusy(stage string, q int) metrics.Counter {
  134. return metrics.GetOrRegisterCounter(fmt.Sprintf("flow.stage.%s.q%d.busy_ns", stage, q), nil)
  135. }