Websocket: Увеличение использования памяти при включенном сжатии

Созданный на 11 янв. 2017  ·  50Комментарии  ·  Источник: gorilla/websocket

Привет,

Я начал использовать Centrifugo на прошлой неделе.
Я использую исходную конечную точку Websocket, которая использует эту библиотеку под капотом.

Я столкнулся с ситуацией, когда включена функция дефляции для каждого сообщения, объем памяти резко увеличился до такой степени, что контейнер докеров аварийно завершил работу из-за использования слишком большого объема памяти.

Я работаю внутри контейнера докеров со средним числом одновременных пользователей от 150 до 200 000, моя средняя скорость сообщений составляет от 30 до 50 000 сообщений в секунду при среднем размере сообщения 600 байт.

Без deflate для каждого сообщения память вообще не растет, а производительность потрясающая, но скорость передачи данных очень высока.

Может ли кто-нибудь помочь мне разобраться в этом?

Спасибо.

bug help wanted question waiting on new maintainer

Самый полезный комментарий

Я подал golang / go # 32371

Все 50 Комментарий

Первым шагом, вероятно, будет получение профиля кучи и распределения вашего приложения с помощью pprof.

Пожалуйста, создайте профили с последней версией пакета. Недавнее изменение пула считывающих и записывающих устройств должно помочь.

Спасибо ребята,

Я жду следующую версию Centrifugo, которая включает вашу последнюю версию, а затем профилирует приложение и загружает сюда.

Вы можете закрыть эту проблему до тех пор, но я надеюсь получить новую версию сегодня или завтра, так что решать вам.

Вот дамп pprof

Это профиль кучи

@kisielk @garyburd Я обновил суть, проверьте сейчас

https://gist.github.com/joshdvir/091229e3d3e4ade8d73b8cffe86c602b

Я попросил @joshdvir отправить профили процессора и памяти с

ПРОЦЕССОР:

(pprof) top 20 --cum
28.99s of 62.03s total (46.74%)
Dropped 523 nodes (cum <= 0.31s)
Showing top 20 nodes out of 155 (cum >= 8.07s)
      flat  flat%   sum%        cum   cum%
         0     0%     0%     58.68s 94.60%  runtime.goexit
     0.05s 0.081% 0.081%     45.44s 73.25%  github.com/centrifugal/centrifugo/libcentrifugo/conns/clientconn.(*client).sendMessages
     0.16s  0.26%  0.34%     44.23s 71.30%  github.com/centrifugal/centrifugo/libcentrifugo/conns/clientconn.(*client).sendMessage
     0.16s  0.26%   0.6%     44.07s 71.05%  github.com/centrifugal/centrifugo/libcentrifugo/server/httpserver.(*wsSession).Send
     0.05s 0.081%  0.68%     43.82s 70.64%  github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket.(*Conn).WriteMessage
     0.01s 0.016%  0.69%     21.67s 34.93%  github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket.(*flateWriteWrapper).Close
     0.03s 0.048%  0.74%     20.19s 32.55%  github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket.(*Conn).NextWriter
     0.07s  0.11%  0.85%     19.79s 31.90%  github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket.compressNoContextTakeover
    17.56s 28.31% 29.16%     17.56s 28.31%  runtime.memclr
     0.04s 0.064% 29.23%     15.46s 24.92%  compress/flate.(*Writer).Reset
     0.03s 0.048% 29.28%     15.42s 24.86%  compress/flate.(*compressor).reset
         0     0% 29.28%     14.40s 23.21%  compress/flate.(*Writer).Flush
         0     0% 29.28%     14.40s 23.21%  compress/flate.(*compressor).syncFlush
     2.62s  4.22% 33.50%     14.01s 22.59%  compress/flate.(*compressor).deflate
     0.01s 0.016% 33.52%     11.05s 17.81%  compress/flate.(*compressor).writeBlock
     0.15s  0.24% 33.76%     11.04s 17.80%  compress/flate.(*huffmanBitWriter).writeBlock
     0.21s  0.34% 34.10%      9.05s 14.59%  runtime.systemstack
     0.06s 0.097% 34.19%      8.87s 14.30%  github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket.(*messageWriter).flushFrame
     0.07s  0.11% 34.31%      8.81s 14.20%  github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket.(*Conn).write

Большая часть времени процессора, проводимого в WriteMessage:

(pprof) list WriteMessage
Total: 1.03mins
ROUTINE ======================== github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket.(*Conn).WriteMessage in /Users/fz/go/src/github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket/conn.go
      50ms     43.82s (flat, cum) 70.64% of Total
         .          .    659:
         .          .    660:// WriteMessage is a helper method for getting a writer using NextWriter,
         .          .    661:// writing the message and closing the writer.
         .          .    662:func (c *Conn) WriteMessage(messageType int, data []byte) error {
         .          .    663:
      50ms       50ms    664:   if c.isServer && (c.newCompressionWriter == nil || !c.enableWriteCompression) {
         .          .    665:
         .          .    666:       // Fast path with no allocations and single frame.
         .          .    667:
         .       20ms    668:       if err := c.prepWrite(messageType); err != nil {
         .          .    669:           return err
         .          .    670:       }
         .          .    671:       mw := messageWriter{c: c, frameType: messageType, pos: maxFrameHeaderSize}
         .       10ms    672:       n := copy(c.writeBuf[mw.pos:], data)
         .          .    673:       mw.pos += n
         .          .    674:       data = data[n:]
         .      1.69s    675:       return mw.flushFrame(true, data)
         .          .    676:   }
         .          .    677:
         .     20.19s    678:   w, err := c.NextWriter(messageType)
         .          .    679:   if err != nil {
         .          .    680:       return err
         .          .    681:   }
         .      190ms    682:   if _, err = w.Write(data); err != nil {
         .          .    683:       return err
         .          .    684:   }
         .     21.67s    685:   return w.Close()
         .          .    686:}

NextWriter:

(pprof) list NextWriter
Total: 1.03mins
ROUTINE ======================== github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket.(*Conn).NextWriter in /Users/fz/go/src/github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket/conn.go
      30ms     20.19s (flat, cum) 32.55% of Total
         .          .    437:// method flushes the complete message to the network.
         .          .    438://
         .          .    439:// There can be at most one open writer on a connection. NextWriter closes the
         .          .    440:// previous writer if the application has not already done so.
         .          .    441:func (c *Conn) NextWriter(messageType int) (io.WriteCloser, error) {
         .       90ms    442:   if err := c.prepWrite(messageType); err != nil {
         .          .    443:       return nil, err
         .          .    444:   }
         .          .    445:
         .          .    446:   mw := &messageWriter{
         .          .    447:       c:         c,
         .          .    448:       frameType: messageType,
      10ms      280ms    449:       pos:       maxFrameHeaderSize,
         .          .    450:   }
         .       10ms    451:   c.writer = mw
         .          .    452:   if c.newCompressionWriter != nil && c.enableWriteCompression && isData(messageType) {
      10ms     19.80s    453:       w := c.newCompressionWriter(c.writer)
         .          .    454:       mw.compress = true
      10ms       10ms    455:       c.writer = w
         .          .    456:   }
         .          .    457:   return c.writer, nil
         .          .    458:}
         .          .    459:
         .          .    460:type messageWriter struct {

compressNoContextTakeover:

(pprof) list compressNoContextTakeover
Total: 1.03mins
ROUTINE ======================== github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket.compressNoContextTakeover in /Users/fz/go/src/github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket/compression.go
      70ms     19.79s (flat, cum) 31.90% of Total
         .          .     33:   fr.(flate.Resetter).Reset(io.MultiReader(r, strings.NewReader(tail)), nil)
         .          .     34:   return &flateReadWrapper{fr}
         .          .     35:}
         .          .     36:
         .          .     37:func compressNoContextTakeover(w io.WriteCloser) io.WriteCloser {
         .      130ms     38:   tw := &truncWriter{w: w}
      40ms      3.93s     39:   fw, _ := flateWriterPool.Get().(*flate.Writer)
      10ms     15.47s     40:   fw.Reset(tw)
      20ms      260ms     41:   return &flateWriteWrapper{fw: fw, tw: tw}
         .          .     42:}

А теперь профиль кучи:

(pprof) top 30 --cum
4794.23MB of 5414.45MB total (88.55%)
Dropped 238 nodes (cum <= 27.07MB)
Showing top 30 nodes out of 46 (cum >= 113.64MB)
      flat  flat%   sum%        cum   cum%
         0     0%     0%  5385.39MB 99.46%  runtime.goexit
         0     0%     0%  4277.82MB 79.01%  sync.(*Pool).Get
         0     0%     0%  4277.82MB 79.01%  sync.(*Pool).getSlow
         0     0%     0%  4182.80MB 77.25%  github.com/centrifugal/centrifugo/libcentrifugo/conns/clientconn.(*client).sendMessages
         0     0%     0%  4181.80MB 77.23%  github.com/centrifugal/centrifugo/libcentrifugo/conns/clientconn.(*client).sendMessage
         0     0%     0%  4181.80MB 77.23%  github.com/centrifugal/centrifugo/libcentrifugo/server/httpserver.(*wsSession).Send
         0     0%     0%  4181.80MB 77.23%  github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket.(*Conn).WriteMessage
       8MB  0.15%  0.15%  4168.27MB 76.98%  github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket.(*Conn).NextWriter
      12MB  0.22%  0.37%  4160.27MB 76.84%  github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket.compressNoContextTakeover
 3792.80MB 70.05% 70.42%  4148.27MB 76.61%  compress/flate.NewWriter
         0     0% 70.42%  4148.27MB 76.61%  github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket.glob..func1
    0.50MB 0.0092% 70.43%  1156.29MB 21.36%  net/http.(*conn).serve
         0     0% 70.43%   873.42MB 16.13%  github.com/centrifugal/centrifugo/libcentrifugo/server/httpserver.(*HTTPServer).Logged.func1
         0     0% 70.43%   873.42MB 16.13%  net/http.HandlerFunc.ServeHTTP
         0     0% 70.43%   872.92MB 16.12%  github.com/centrifugal/centrifugo/libcentrifugo/server/httpserver.(*HTTPServer).WrapShutdown.func1
         0     0% 70.43%   872.92MB 16.12%  net/http.(*ServeMux).ServeHTTP
         0     0% 70.43%   872.92MB 16.12%  net/http.serverHandler.ServeHTTP
         0     0% 70.43%   866.91MB 16.01%  github.com/centrifugal/centrifugo/libcentrifugo/server/httpserver.(*HTTPServer).RawWebsocketHandler
         0     0% 70.43%   866.91MB 16.01%  github.com/centrifugal/centrifugo/libcentrifugo/server/httpserver.(*HTTPServer).RawWebsocketHandler-fm
         0     0% 70.43%   404.78MB  7.48%  github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket.(*Conn).ReadMessage
  355.47MB  6.57% 76.99%   355.47MB  6.57%  compress/flate.(*compressor).init
         0     0% 76.99%   320.19MB  5.91%  github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket.(*Upgrader).Upgrade
    0.50MB 0.0092% 77.00%   292.64MB  5.40%  github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket.(*Conn).NextReader
    1.50MB 0.028% 77.03%   291.64MB  5.39%  github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket.decompressNoContextTakeover
  215.85MB  3.99% 81.02%   216.35MB  4.00%  github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket.newConn
  159.10MB  2.94% 83.96%   159.10MB  2.94%  compress/flate.(*decompressor).Reset
  129.04MB  2.38% 86.34%   129.04MB  2.38%  compress/flate.NewReader
         0     0% 86.34%   129.04MB  2.38%  github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket.glob..func2
  119.46MB  2.21% 88.55%   119.46MB  2.21%  net/http.newBufioWriterSize
         0     0% 88.55%   113.64MB  2.10%  io/ioutil.ReadAll

NextWriter:

(pprof) list WriteMessage
Total: 5.29GB
ROUTINE ======================== github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket.(*Conn).WriteMessage in /Users/fz/go/src/github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket/conn.go
         0     4.08GB (flat, cum) 77.23% of Total
         .          .    673:       mw.pos += n
         .          .    674:       data = data[n:]
         .          .    675:       return mw.flushFrame(true, data)
         .          .    676:   }
         .          .    677:
         .     4.07GB    678:   w, err := c.NextWriter(messageType)
         .          .    679:   if err != nil {
         .          .    680:       return err
         .          .    681:   }
         .          .    682:   if _, err = w.Write(data); err != nil {
         .          .    683:       return err
         .          .    684:   }
         .    13.53MB    685:   return w.Close()
         .          .    686:}

compressNoContextTakeover:

(pprof) list compressNoContextTakeover
Total: 5.29GB
ROUTINE ======================== github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket.compressNoContextTakeover in /Users/fz/go/src/github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket/compression.go
      12MB     4.06GB (flat, cum) 76.84% of Total
         .          .     33:   fr.(flate.Resetter).Reset(io.MultiReader(r, strings.NewReader(tail)), nil)
         .          .     34:   return &flateReadWrapper{fr}
         .          .     35:}
         .          .     36:
         .          .     37:func compressNoContextTakeover(w io.WriteCloser) io.WriteCloser {
      10MB       10MB     38:   tw := &truncWriter{w: w}
         .     4.05GB     39:   fw, _ := flateWriterPool.Get().(*flate.Writer)
         .          .     40:   fw.Reset(tw)
       2MB        2MB     41:   return &flateWriteWrapper{fw: fw, tw: tw}
         .          .     42:}

@FZambia Спасибо за размещение информации профиля.

Счетчики должны быть добавлены сюда и сюда, чтобы определить, насколько эффективен пул. Возможно, существует кодовый путь, по которому устройство записи flate не возвращается в пул.

Возможно, связано: https://github.com/golang/go/issues/18625

@ y3llowcake благодарит за указание на эту проблему.

Я написал тестовый пример для Gorilla Websocket:

type testConn struct {
    conn     *Conn
    messages chan []byte
}

func newTestConn(c *Conn, bufferSize int) *testConn {
    return &testConn{
        conn:     c,
        messages: make(chan []byte, bufferSize),
    }
}

func printss() {
    m := runtime.MemStats{}
    runtime.ReadMemStats(&m)
    fmt.Printf("inuse: %d sys: %d\n", m.StackInuse, m.StackSys)
}

func TestWriteWithCompression(t *testing.T) {
    w := ioutil.Discard
    done := make(chan struct{})
    numConns := 1000
    numMessages := 1000
    conns := make([]*testConn, numConns)
    var wg sync.WaitGroup
    for i := 0; i < numConns; i++ {
        c := newConn(fakeNetConn{Reader: nil, Writer: w}, false, 1024, 1024)
        c.enableWriteCompression = true
        c.newCompressionWriter = compressNoContextTakeover
        conns[i] = newTestConn(c, 256)
        wg.Add(1)
        go func(c *testConn) {
            defer wg.Done()
            i := 0
            for i < numMessages {
                select {
                case <-done:
                    return
                case msg := <-c.messages:
                    c.conn.WriteMessage(TextMessage, msg)
                    i++
                }
            }
        }(conns[i])
    }
    messages := textMessages(100)
    for i := 0; i < numMessages; i++ {
        if i%100 == 0 {
            printss()
        }
        msg := messages[i%len(messages)]
        for _, c := range conns {
            c.messages <- msg
        }
    }
    wg.Wait()
}

func textMessages(num int) [][]byte {
    messages := make([][]byte, num)
    for i := 0; i < num; i++ {
        msg := fmt.Sprintf("planet: %d, country: %d, city: %d, street: %d", i, i, i, i)
        messages[i] = []byte(msg)
    }
    return messages
}

Он создает 1000 соединений с включенным сжатием, каждое с буферизованным каналом сообщений. Затем в цикле мы записываем сообщение в каждое соединение.

Вот как это ведет себя с go1.7.4

fz<strong i="7">@websocket</strong>: go test -test.run=TestWriteWithCompression
inuse: 4259840 sys: 4259840
inuse: 27394048 sys: 27394048
inuse: 246251520 sys: 246251520
inuse: 1048510464 sys: 1048510464
inuse: 1048510464 sys: 1048510464
inuse: 1049034752 sys: 1049034752
inuse: 1049034752 sys: 1049034752
inuse: 1049034752 sys: 1049034752
inuse: 1049034752 sys: 1049034752
inuse: 1049034752 sys: 1049034752
PASS
ok      github.com/gorilla/websocket    11.053s

Использование Go с фиксацией https://github.com/golang/go/commit/9c3630f578db1d4331b367c3c7d284db299be3a6

fz<strong i="12">@websocket</strong>: go1.8 test -test.run=TestWriteWithCompression
inuse: 4521984 sys: 4521984
inuse: 4521984 sys: 4521984
inuse: 4521984 sys: 4521984
inuse: 4521984 sys: 4521984
inuse: 4521984 sys: 4521984
inuse: 4521984 sys: 4521984
inuse: 4521984 sys: 4521984
inuse: 4521984 sys: 4521984
inuse: 4521984 sys: 4521984
inuse: 4521984 sys: 4521984
PASS
ok      github.com/gorilla/websocket    12.023s

Хотя пока трудно сказать, решит ли это исправление исходную проблему в данном выпуске или нет.

Я также пробовал то же самое с flate из https://github.com/klauspost/compress by @klauspost, который уже содержит исправление копии массива в master:

fz<strong i="20">@websocket</strong>: go test -test.run=TestWriteWithCompression
inuse: 4358144 sys: 4358144
inuse: 4587520 sys: 4587520
inuse: 4587520 sys: 4587520
inuse: 4587520 sys: 4587520
inuse: 4587520 sys: 4587520
inuse: 4587520 sys: 4587520
inuse: 4587520 sys: 4587520
inuse: 4587520 sys: 4587520
inuse: 4587520 sys: 4587520
inuse: 4587520 sys: 4587520
PASS
ok      github.com/gorilla/websocket    3.426s

Но на самом деле даже без этого исправления библиотека https://github.com/klauspost/compress ведет себя без увеличения памяти ... Я не могу этого объяснить.

Также вот результат теста с использованием библиотеки https://github.com/klauspost/compress :

BenchmarkWriteWithCompression-4       200000          5676 ns/op         149 B/op          3 allocs/op

Это 4-кратное ускорение по сравнению со стандартными результатами lib compress/flate :

BenchmarkWriteWithCompression-4        50000         25362 ns/op         128 B/op          3 allocs/op

@garyburd Я понимаю, что наличие нестандартного пакета lib в ядре может быть неправильным шагом, но, может быть, мы можем рассмотреть механизм, позволяющий каким-либо образом подключить его с помощью кода пользователя?

даже без этого исправления [он] ведет себя без увеличения памяти ... Я не могу этого объяснить.

AFAICT, этот пакет использует сжатие "уровня 3" (что является хорошим выбором). В моем пакете уровни 1-4 являются специализированными и не используют «общий» код, в котором есть проблема.

В Go 1.7 уровень 1 (Лучшая скорость) выполняет аналогичную специализированную функцию. Я думаю, что если вы воспользуетесь этим, у вас не возникнет проблемы. Это может быть решение, которое вы можете использовать, поэтому вам не нужно импортировать специализированный пакет (даже если бы я не возражал предоставить пользователям такую ​​возможность). Производительность для уровня 1 должна быть очень близка к моему пакету.

@klauspost спасибо за объяснение, просто попробовал то, что вы сказали - да, с уровнем сжатия 1 производительность сопоставима с вашей библиотекой и не имеет проблем с памятью в go1.7 (в тестовом примере выше)

@garyburd что ты об этом думаешь? Я вижу два решения, которые могут нам помочь - сделать экспортируемую переменную уровня сжатия или разрешить подключать пользовательскую реализацию flate. Конечно, мы также можем дождаться go1.8, но способ повышения производительности сжатия все еще очень важен. Вы хотите, чтобы мы попробовали создать индивидуальную сборку с указанными вами советами и исправили ошибку копирования массива и посмотреть, как она ведет себя в производственной среде?

@FZambia Как насчет установки уровня сжатия на единицу сейчас? Это, вероятно, лучший вариант для большинства приложений в настоящее время, позволяющий избежать увеличения площади поверхности API.

@garyburd Я согласен с @FZambia , возможность установить сжатие очень поможет, для моего конкретного случая использования у меня есть вентилятор из 80K сообщений в секунду для 200K пользователей, которые вызывают большой трафик, если я смогу чтобы установить уровень сжатия, я смог найти золотую середину между количеством серверов и исходящим трафиком.
Обычно серверы стоят намного меньше, чем трафик, когда трафик настолько высок, поэтому возможность настройки этой опции будет отличной.
Спасибо

Хорошо, добавим следующее:

type CompressionOptions {
    // Level specifies the compression level for the flate compressor.  Valid levels range from
    // -2 to 9.  Level -2 will use Huffman compression only. Level -1 uses the default compression 
    // level.  Level 0 does not attempt any compression.  Levels 1 through 9 range from best
    // speed to best compression. 
    // 
    // Applications should set this field.  The default value of 0 does not attempt any compression.
    Level int
}

type Dialer struct {
   // CompressionOptions specifies options the client should use for 
   // per message compression (RFC 7692).  If CompressionOptions is nil and
   // EnableCompression is nil, then the client does not attempt to negotiate
   // compression with the server.
   CompressionOptions *CompressionOptions

  // EnableCompression specifies if the client should attempt to negotiate
  // per message compression (RFC 7692). Setting this value to true does not
  // guarantee that compression will be supported. Currently only "no context
  // takeover" modes are supported.
  //
  // Deprecated: Set CompressionOptions to non-nil value to enable compression
  // negotiation.
  EnableCompression bool
}

Измените Upgrader в соответствии с Dialer.

@garyburd Я согласен с тем, что на данный момент уровень 1 лучше для значения по умолчанию, потому что он исправляет рост памяти на Go1.7 и сжатие настолько дорого. Но похоже, что на уровне разветвления в таких больших приложениях, как @joshdvir , экономия полосы пропускания экономит много денег, поэтому имеет смысл настраивать уровень сжатия.

Мы сделали кастомную сборку с уровнем сжатия 1 и счетчиками, которые вы предложили, и запустили ее в производство. Значения счетчика:

Node 1:
"gorilla_websocket_flate_writer_from_pool": 1453147,
"gorilla_websocket_new_flate_writer": 6702

Node 2:
"gorilla_websocket_flate_writer_from_pool": 1820919,
"gorilla_websocket_new_flate_writer": 3676,

Node 3:
"gorilla_websocket_flate_writer_from_pool": 574187,
"gorilla_websocket_new_flate_writer": 321

...

Агрегация за 1 минуту. Так что пул выглядит довольно эффектно, но ...

... Сжатие по-прежнему является лидером по аллокам и профилям ЦП, а получение из sync.Pool по какой-то причине является самой затратной операцией.

Вот теперь профиль процессора:

(pprof) top 30 --cum
27.28s of 52.42s total (52.04%)
Dropped 414 nodes (cum <= 0.26s)
Showing top 30 nodes out of 137 (cum >= 1.89s)
      flat  flat%   sum%        cum   cum%
         0     0%     0%     50.21s 95.78%  runtime.goexit
     0.16s  0.31%  0.31%     43.93s 83.80%  github.com/centrifugal/centrifugo/libcentrifugo/conns/clientconn.(*client).sendMessages
     0.21s   0.4%  0.71%     42.52s 81.11%  github.com/centrifugal/centrifugo/libcentrifugo/conns/clientconn.(*client).sendMessage
     0.19s  0.36%  1.07%     42.31s 80.71%  github.com/centrifugal/centrifugo/libcentrifugo/server/httpserver.(*wsSession).Send
     0.21s   0.4%  1.47%     41.87s 79.87%  github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket.(*Conn).WriteMessage
     0.01s 0.019%  1.49%     35.43s 67.59%  github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket.(*flateWriteWrapper).Close
     0.03s 0.057%  1.55%     24.69s 47.10%  compress/flate.(*Writer).Flush
         0     0%  1.55%     24.66s 47.04%  compress/flate.(*compressor).syncFlush
     0.04s 0.076%  1.62%     24.03s 45.84%  compress/flate.(*compressor).encSpeed
     0.08s  0.15%  1.77%     18.16s 34.64%  compress/flate.(*huffmanBitWriter).writeBlockDynamic
     0.12s  0.23%  2.00%     15.03s 28.67%  github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket.(*messageWriter).flushFrame
     0.11s  0.21%  2.21%     14.90s 28.42%  github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket.(*Conn).write
     0.91s  1.74%  3.95%     13.21s 25.20%  compress/flate.(*huffmanEncoder).generate
         0     0%  3.95%     12.72s 24.27%  net.(*conn).Write
     0.06s  0.11%  4.06%     12.72s 24.27%  net.(*netFD).Write
    11.78s 22.47% 26.54%     12.16s 23.20%  syscall.Syscall
     0.05s 0.095% 26.63%     12.09s 23.06%  syscall.Write
     0.02s 0.038% 26.67%     12.04s 22.97%  syscall.write
     0.61s  1.16% 27.83%     11.98s 22.85%  compress/flate.(*huffmanBitWriter).indexTokens
         0     0% 27.83%     10.61s 20.24%  github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket.(*messageWriter).Close
     5.20s  9.92% 37.75%      6.62s 12.63%  compress/flate.(*huffmanEncoder).bitCounts
     4.77s  9.10% 46.85%      5.44s 10.38%  compress/flate.encodeBestSpeed

list WriteMessage

(pprof) list WriteMessage
Total: 52.42s
ROUTINE ======================== github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket.(*Conn).WriteMessage in /Users/fz/go/src/github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket/conn.go
     210ms     41.87s (flat, cum) 79.87% of Total
         .          .    659:
         .          .    660:// WriteMessage is a helper method for getting a writer using NextWriter,
         .          .    661:// writing the message and closing the writer.
         .          .    662:func (c *Conn) WriteMessage(messageType int, data []byte) error {
         .          .    663:
     160ms      160ms    664:   if c.isServer && (c.newCompressionWriter == nil || !c.enableWriteCompression) {
         .          .    665:
         .          .    666:       // Fast path with no allocations and single frame.
         .          .    667:
      10ms       40ms    668:       if err := c.prepWrite(messageType); err != nil {
         .          .    669:           return err
         .          .    670:       }
         .          .    671:       mw := messageWriter{c: c, frameType: messageType, pos: maxFrameHeaderSize}
         .       80ms    672:       n := copy(c.writeBuf[mw.pos:], data)
         .          .    673:       mw.pos += n
         .          .    674:       data = data[n:]
      10ms      4.43s    675:       return mw.flushFrame(true, data)
         .          .    676:   }
         .          .    677:
         .      1.63s    678:   w, err := c.NextWriter(messageType)
         .          .    679:   if err != nil {
         .          .    680:       return err
         .          .    681:   }
      30ms      100ms    682:   if _, err = w.Write(data); err != nil {
         .          .    683:       return err
         .          .    684:   }
         .     35.43s    685:   return w.Close()

список Закрыть

         .          .    104:func (w *flateWriteWrapper) Close() error {
         .          .    105:   if w.fw == nil {
         .          .    106:       return errWriteClosed
         .          .    107:   }
         .     24.69s    108:   err1 := w.fw.Flush()
      10ms      130ms    109:   flateWriterPool.Put(w.fw)
         .          .    110:   w.fw = nil
         .          .    111:   if w.tw.p != [4]byte{0, 0, 0xff, 0xff} {
         .          .    112:       return errors.New("websocket: internal error, unexpected bytes at end of flate stream")
         .          .    113:   }
         .     10.61s    114:   err2 := w.tw.w.Close()
         .          .    115:   if err1 != nil {
         .          .    116:       return err1
         .          .    117:   }
         .          .    118:   return err2
         .          .    119:}

list Flush

         .          .    711:// In the terminology of the zlib library, Flush is equivalent to Z_SYNC_FLUSH.
         .          .    712:func (w *Writer) Flush() error {
         .          .    713:   // For more about flushing:
         .          .    714:   // http://www.bolet.org/~pornin/deflate-flush.html
      30ms     24.69s    715:   return w.d.syncFlush()
         .          .    716:}
         .          .    717:
         .          .    718:// Close flushes and closes the writer.
         .          .    719:func (w *Writer) Close() error {
         .          .    720:   return w.d.close()
ROUTINE ======================== compress/flate.(*compressor).syncFlush in /Users/fz/go1.7/src/compress/flate/deflate.go
         0     24.66s (flat, cum) 47.04% of Total
         .          .    555:func (d *compressor) syncFlush() error {
         .          .    556:   if d.err != nil {
         .          .    557:       return d.err
         .          .    558:   }
         .          .    559:   d.sync = true
         .     24.03s    560:   d.step(d)
         .          .    561:   if d.err == nil {
         .      490ms    562:       d.w.writeStoredHeader(0, false)
         .      140ms    563:       d.w.flush()
         .          .    564:       d.err = d.w.err
         .          .    565:   }
         .          .    566:   d.sync = false
         .          .    567:   return d.err
         .          .    568:}

Использование памяти сейчас намного лучше, но все еще очень высокое, сжатие выделяет много:

fz<strong i="9">@centrifugo</strong>: go tool pprof --alloc_space centrifugo heap_profile_extra 
Entering interactive mode (type "help" for commands)
(pprof) top 30 --cum
518.97GB of 541.65GB total (95.81%)
Dropped 314 nodes (cum <= 2.71GB)
Showing top 30 nodes out of 35 (cum >= 3.33GB)
      flat  flat%   sum%        cum   cum%
         0     0%     0%   541.53GB   100%  runtime.goexit
         0     0%     0%   505.54GB 93.33%  github.com/centrifugal/centrifugo/libcentrifugo/conns/clientconn.(*client).sendMessages
         0     0%     0%   504.45GB 93.13%  github.com/centrifugal/centrifugo/libcentrifugo/conns/clientconn.(*client).sendMessage
         0     0%     0%   504.45GB 93.13%  github.com/centrifugal/centrifugo/libcentrifugo/server/httpserver.(*wsSession).Send
         0     0%     0%   504.45GB 93.13%  github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket.(*Conn).WriteMessage
    6.63GB  1.22%  1.22%   501.75GB 92.63%  github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket.(*Conn).NextWriter
    6.56GB  1.21%  2.44%   495.11GB 91.41%  github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket.compressNoContextTakeover
         0     0%  2.44%   491.89GB 90.81%  sync.(*Pool).Get
         0     0%  2.44%   491.89GB 90.81%  sync.(*Pool).getSlow
  359.74GB 66.42% 68.85%   488.55GB 90.20%  compress/flate.NewWriter
         0     0% 68.85%   488.55GB 90.20%  github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket.glob..func1
  128.81GB 23.78% 92.63%   128.81GB 23.78%  compress/flate.(*compressor).init
    0.01GB 0.0019% 92.64%    28.97GB  5.35%  net/http.(*conn).serve
         0     0% 92.64%    25.18GB  4.65%  github.com/centrifugal/centrifugo/libcentrifugo/server/httpserver.(*HTTPServer).Logged.func1
         0     0% 92.64%    25.18GB  4.65%  net/http.(*ServeMux).ServeHTTP
         0     0% 92.64%    25.18GB  4.65%  net/http.HandlerFunc.ServeHTTP
         0     0% 92.64%    25.18GB  4.65%  net/http.serverHandler.ServeHTTP

list NextWriter

(pprof) list NextWriter
Total: 541.65GB
ROUTINE ======================== github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket.(*Conn).NextWriter in /Users/fz/go/src/github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket/conn.go
    6.63GB   501.75GB (flat, cum) 92.63% of Total
         .          .    444:   }
         .          .    445:
         .          .    446:   mw := &messageWriter{
         .          .    447:       c:         c,
         .          .    448:       frameType: messageType,
    6.63GB     6.63GB    449:       pos:       maxFrameHeaderSize,
         .          .    450:   }
         .          .    451:   c.writer = mw
         .          .    452:   if c.newCompressionWriter != nil && c.enableWriteCompression && isData(messageType) {
         .   495.11GB    453:       w := c.newCompressionWriter(c.writer)
         .          .    454:       mw.compress = true
         .          .    455:       c.writer = w
         .          .    456:   }
         .          .    457:   return c.writer, nil
         .          .    458:}

список compressNoContextTakeover

(pprof) list compressNoContextTakeover
Total: 541.65GB
ROUTINE ======================== github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket.compressNoContextTakeover in /Users/fz/go/src/github.com/centrifugal/centrifugo/vendor/github.com/gorilla/websocket/compression.go
    6.56GB   495.11GB (flat, cum) 91.41% of Total
         .          .     44:   fr.(flate.Resetter).Reset(io.MultiReader(r, strings.NewReader(tail)), nil)
         .          .     45:   return &flateReadWrapper{fr}
         .          .     46:}
         .          .     47:
         .          .     48:func compressNoContextTakeover(w io.WriteCloser) io.WriteCloser {
    4.41GB     4.41GB     49:   tw := &truncWriter{w: w}
         .   488.55GB     50:   fw, _ := flateWriterPool.Get().(*flate.Writer)
         .          .     51:   plugin.Metrics.Counters.Inc("gorilla_websocket_flate_writer_from_pool")
         .          .     52:   fw.Reset(tw)
    2.15GB     2.15GB     53:   return &flateWriteWrapper{fw: fw, tw: tw}
         .          .     54:}

--inuse_space показывает аналогичную картину, только значения на два порядка меньше (2,69 ГБ для строки flateWriterPool.Get (). (* Flate.Writer), которая является лидером).

Трудно сказать, что мы можем сделать с такими большими накладными расходами на сжатие ...

@garyburd вы предлагаете добавить CompressionOptions - я могу сделать с ним запрос на вытягивание, но, может быть, только глобальная экспортированная переменная, такая как DefaultFlateCompressionLevel которую мы можем установить при запуске приложения или метод установки, сделает работу? Нам не нужен уровень сжатия для каждого соединения - и в конечном итоге мы сможем сделать то, что вы предложили, если в дальнейшем возникнет необходимость. И на данный момент такой способ устаревания не требуется.

@FZambia Спасибо за тестирование эффективности пула и сообщение о профилях.

Я не хочу добавлять API сейчас, который позже может быть заменен другим API.

По мере того, как я думаю об этом больше, лучше добавить один метод:

// SetCompressionLevel sets the flate compression level for the next message.
// Valid levels range from -2 to 9.  Level -2 will use Huffman compression only. 
// Level -1 uses the default compression  level.  Level 0 does not attempt any
// compression.  Levels 1 through 9 range from best speed to best compression. 
func (c *Conn) SetCompressionLevel(n int) error {
}

Это более гибкий вариант, чем другие варианты. Реализация заменит flateWriterPool на flatWriterPools [12]. FlateWriterWrapper должен будет сохранить уровень, чтобы писатель мог быть возвращен в правильный пул.

Я думаю, что наличие одного метода, изменяющего уровень сжатия по умолчанию, все еще имеет смысл:

var defaultCompressionLevel int = 1

// SetDefaultCompressionLevel sets the flate compression level which will be used by 
// default to compress messages when compression negotiated. This function must be 
// called once before application starts.
//
// Valid levels range from -2 to 9.  Level -2 will use Huffman compression only. 
// Level -1 uses the default compression  level.  Level 0 does not attempt any
// compression.  Levels 1 through 9 range from best speed to best compression. 
func (c *Conn) SetDefaultCompressionLevel(n int) error {
    defaultCompressionLevel = n
}

В большинстве случаев я полагаю, что пользователям, которым требуется индивидуальное сжатие, необходимо один раз установить это значение по умолчанию.

Затем, если кому-то нужен уровень сжатия для каждого соединения / сообщения, мы можем добавить SetCompressionLevel и [12] flateWriterPool:

// SetCompressionLevel sets the flate compression level for the next message.
// Valid levels range from -2 to 9.  Level -2 will use Huffman compression only. 
// Level -1 uses the default compression  level.  Level 0 does not attempt any
// compression.  Levels 1 through 9 range from best speed to best compression.
// If not set default compression level will be used.
func (c *Conn) SetCompressionLevel(n int) error {
}

Если он не вызван, но сжатие согласовано, будет использоваться defaultCompressionLevel . Единственное предостережение, которое я вижу, заключается в том, что SetDefaultCompressionLevel следует вызывать один раз перед запуском приложения в текущей реализации, но это кажется довольно непрозрачным.

Просто посмотрел размер каждого экземпляра flate.Writer :

package main

import "unsafe"
import "compress/flate"

func main() {
    var w flate.Writer
    println(unsafe.Sizeof(w))
}

600Кб! Этот размер меня удивляет - я даже не предполагал, что он такой большой :)

Да, существует множество таблиц, необходимых для поддержания состояния, создания таблиц Хаффмана и вывода буфера. С этим мало что можно сделать, кроме уровня -2 (HuffmanOnly), 0 (без сжатия) и 1 (максимальная скорость) в стандартной библиотеке.

Для моей собственной библиотеки я искал возможность уменьшить требования к памяти для параметров кодирования, которые не требуют такого количества буферов, см. Https://github.com/klauspost/compress/pull/70 (как видно, у него все еще есть проблемы по сбою зашел в проблему)

@FZambia Я не хочу использовать переменные уровня пакета для настроек. Этот параметр должен быть включен в другие параметры в Dialer и Upgrader или должен быть частью соединения, чтобы его можно было изменять от сообщения к сообщению.

См. B0dc45572b148296ddf01989da00a2923d213a56.

@garyburd большое спасибо за то, что вы помогли нам с этим. Глядя на все эти профили, видите ли вы какой-либо способ уменьшить использование и выделение памяти для сжатия?

Если приложение отправляет одно и то же сообщение нескольким клиентам, то следующим шагом будет # 182.

@garyburd # 182 мог бы творить чудеса в моем случае использования, где скорость разветвления составляет от 50 до 90 тысяч в секунду, где все сообщения одинаковы.

Спасибо за помощь.

Исправлена ​​ли эта проблема с помощью b0dc45572b148296ddf01989da00a2923d213a56 и 804cb600d06b10672f2fbc0a336a7bee507a428e?

@garyburd куча до изменений:
screen shot 2017-03-06 at 10 09 57
После изменений (уровень сжатия 1 и использование PreparedMessage ):
screen shot 2017-03-06 at 10 10 35

Такая же картина для CPU. Таким образом, мы больше не видим сжатие в профилях кучи и процессора на первом месте.

Хотя использование памяти уменьшилось, оно все еще довольно велико при использовании сжатия - но это может быть проблемой приложения, мы постараемся исследовать

@garyburd только что проанализировал inuse_space профиль, который мне дал

Лидируют gorilla/websocket.newConnBRW (36%), http.newBufioWriterSize (17%), http.newBufioReader (16%). Я думал, что мы использовали недавнее улучшение, которое вы добавили в # 223, но, глядя на профили, я заметил, что буферы чтения и записи из hijack по какой-то причине не используются повторно. Затем я обнаружил ошибку в коде Centrifugo - ReadBufferSize и WriteBufferSize были установлены на SockJS-go значения по умолчанию (4096) в случае, если эти размеры установлены на 0 в конфигурации. Построим с исправлением и вернемся с результатами.

В профилях есть еще кое-что, что может вас заинтересовать - я попытаюсь загрузить визуализации svg-графиков из сборки с исправлением.

Вот несколько графиков с использованием Centrifugo с последней версией Gorilla Websocket. Это из узла Centrifugo, запущенного @joshdvir с большим количеством подключений (около 100k) и включенным сжатием веб-сокетов (уровень 1), go1.7.5

ПРОЦЕССОР:

screen shot 2017-03-31 at 23 54 19

Здесь мы видим, что большая часть процессора тратится на системные вызовы - чтение, запись - я не думаю, что мы можем здесь много сделать, потому что мы отправляем много сообщений разветвления в разные сокеты. Так что это нормально.

Выделить пространство:

screen shot 2017-03-31 at 23 52 49

Здесь мы видим большое влияние compress/flate.(*decompressor).Reset - я немного подумал, как это улучшить, но не нашел способа ..

Используемое пространство:

screen shot 2017-04-01 at 00 00 31

@garyburd, видишь ли ты способ еще больше улучшить ситуацию? Если нет, то, полагаю, мы сможем с этим жить.

PS Я собираюсь создать много ws-соединений на локальном компьютере с включенным / отключенным сжатием и сравнить профили.

Я нашел некоторые настройки, которые можно было бы внести в декодер, чтобы избежать выделения памяти и отложить выделение около 4 КБ до того момента, когда это действительно необходимо.

https://github.com/klauspost/compress/pull/76

Я не вижу ничего очевидного, что нужно улучшить в пакете websocket.

Просто попробовал на localhost - похоже, что потребление памяти происходит из разных мест в более или менее равных пропорциях, поэтому трудно диагностировать узкое место, над которым нужно работать. Я думаю, что на данный момент мы закончили.

@klauspost Я также пробовал вашу https: //github.com/klauspost/compress/pull/76 - Я не могу сказать, как это может повлиять на производственный экземпляр (поскольку я только что создал много подключений, без широковещательных сообщений, без сообщений, исходящих от клиентов, за исключением необходимости подключения и подписки на channel) - возможно, в реальном сценарии выигрыш в памяти будет более значительным (тем более, что мы видели, что много памяти выделяется при чтении сжатым на графе alloc_space).

Обновление до Go1.8.1 с Go 1.7.5 улучшило использование памяти примерно на 15% (90 МБ -> 75 МБ) при моей искусственной локальной настройке. Но я не могу точно сказать, откуда взялся этот выигрыш.

На производственном экземпляре с включенным сжатием использование памяти также упало на те же 15-20% с Go 1.8.

@joshdvir @FZambia (и другие), чувствуем ли мы, что эта проблема решена с помощью исправлений в стандартной библиотеке Go или здесь есть что отследить?

@theckman привет, у меня нет идей, как мы можем улучшить производительность сжатия. Я не думаю, что проблема решена полностью, потому что сжатие по-прежнему сильно влияет на производительность. Но если вы планируете закрыть эту проблему - смело делайте это, поскольку очевидного решения на горизонте нет. В зависимости от ситуации PreparedMessage может спасти жизнь, но лично мне не очень нравится его концепция, потому что она слишком специфична и нарушает семантику записи байтов в соединение.

Возможно, если бы вы написали довольно автономный тест, то есть используя только stdlib и deflate, которые представляли реальные сценарии, я мог бы использовать его в качестве эталона для тестирования, действительно ли поможет более компактный кодировщик .

Сам PR придется переписать, но отправной точкой будет хороший тест.

Мы также могли бы добавить []byte -> []byte API, где предоставляется полный фрейм, что избавит от необходимости иметь внутренний буфер (и штраф за копирование в него).

Стоит ли включать сжатие с учетом этих проблем с производительностью?

Мы также могли бы добавить API-интерфейс [] byte -> [] byte, в котором предоставляется полный кадр, что устраняет необходимость во внутреннем буфере (и штрафы за копирование в него).

Как будет работать этот API? Разрешим ли мы писать прямо в net.Conn?

На сервере без сжатия API WriteMessage в основном копирует данные непосредственно из предоставленного приложением байта [] в базовое сетевое соединение. Подготовленный API сообщения также копирует байт [] непосредственно в базовое сетевое соединение.

Я считаю, что эта проблема связана с тем, что сжатие / выравнивание не позволяет настроить скользящее окно. См. Https://golang.org/issue/3155.

Таким образом, каждый плоский модуль записи выделяет много памяти, и даже если они повторно используются через пул, это приводит к резкому увеличению использования памяти при включенном сжатии.

Не похоже, что эта библиотека может что-то делать дальше, пока проблема не будет закрыта.

Фактически, согласно этому тесту


func BenchmarkFlate(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
        flate.NewWriter(nil, flate.BestSpeed)
    }
}
$ go test -bench=BenchmarkFlate -run=^$
goos: darwin
goarch: amd64
pkg: scratch
BenchmarkFlate-8       10000        131504 ns/op     1200008 B/op         15 allocs/op
PASS
ok      scratch 1.345s

flate.NewWriter выделяет 1,2 МБ! Это много памяти, которую нужно выделить для сжатия, когда вы пишете 600-байтовые сообщения WebSocket.

При использовании BestCompression или DefaultCompress объем памяти сокращается на 800 КБ, но это все еще огромно.

# DefaultCompression
$ go test -bench=BenchmarkFlate -run=^$
goos: darwin
goarch: amd64
pkg: nhooyr.io/websocket
BenchmarkFlate-8       20000         93332 ns/op      806784 B/op         13 allocs/op
PASS
ok      nhooyr.io/websocket 2.848s
# BestCompression
$ go test -bench=BenchmarkFlate -run=^$
goos: darwin
goarch: amd64
pkg: nhooyr.io/websocket
BenchmarkFlate-8       20000         95197 ns/op      806784 B/op         13 allocs/op
PASS
ok      nhooyr.io/websocket 2.879s

Я подал golang / go # 32371

@klauspost , это действительно круто. Какие накладные расходы на память будут связаны с каждой записью?

Для этого потребуется несколько сотен килобайт, но, в отличие от других альтернатив, после сжатия память будет освобождена.

Для веб-сокетов это то, что вам нужно. Это означает, что для неактивных сокетов нет выделенных ресурсов в реальном времени. Просто убедитесь, что все написано за один раз.

@klauspost спасибо, попробую проверить, как только будет время. У меня есть суть, которая позволяет использовать настраиваемый flate с библиотекой Gorilla WebSocket - нужно вернуться к нему и поэкспериментировать с ветвью сжатия без сохранения состояния.

https://github.com/klauspost/compress/pull/185 уменьшает выделение кучи до нескольких сотен байтов во время записи, имея внутренний sync.Pool .

Мое тестирование библиотеки @klauspost со сжатием без сохранения состояния показало отличные результаты.

См. Https://github.com/klauspost/compress/pull/216#issuecomment -586660128

Около 9,5 КБ выделяется на каждое сообщение, написанное для простого 512-байтового сообщения []byte(strings.Repeat("1234", 128)) .

изменить: теперь он доступен для тестирования в мастере в моей библиотеке.

Чтобы уточнить, что со словарем в режиме подмены контекста, это 50 B сообщение, выделенное, если нет словаря.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги