Привет,
Я начал использовать Centrifugo на прошлой неделе.
Я использую исходную конечную точку Websocket, которая использует эту библиотеку под капотом.
Я столкнулся с ситуацией, когда включена функция дефляции для каждого сообщения, объем памяти резко увеличился до такой степени, что контейнер докеров аварийно завершил работу из-за использования слишком большого объема памяти.
Я работаю внутри контейнера докеров со средним числом одновременных пользователей от 150 до 200 000, моя средняя скорость сообщений составляет от 30 до 50 000 сообщений в секунду при среднем размере сообщения 600 байт.
Без deflate для каждого сообщения память вообще не растет, а производительность потрясающая, но скорость передачи данных очень высока.
Может ли кто-нибудь помочь мне разобраться в этом?
Спасибо.
Первым шагом, вероятно, будет получение профиля кучи и распределения вашего приложения с помощью 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:}
Возможно, связано: 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 куча до изменений:
После изменений (уровень сжатия 1 и использование PreparedMessage
):
Такая же картина для 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
ПРОЦЕССОР:
Здесь мы видим, что большая часть процессора тратится на системные вызовы - чтение, запись - я не думаю, что мы можем здесь много сделать, потому что мы отправляем много сообщений разветвления в разные сокеты. Так что это нормально.
Выделить пространство:
Здесь мы видим большое влияние compress/flate.(*decompressor).Reset
- я немного подумал, как это улучшить, но не нашел способа ..
Используемое пространство:
@garyburd, видишь ли ты способ еще больше улучшить ситуацию? Если нет, то, полагаю, мы сможем с этим жить.
PS Я собираюсь создать много ws-соединений на локальном компьютере с включенным / отключенным сжатием и сравнить профили.
Я нашел некоторые настройки, которые можно было бы внести в декодер, чтобы избежать выделения памяти и отложить выделение около 4 КБ до того момента, когда это действительно необходимо.
Я не вижу ничего очевидного, что нужно улучшить в пакете 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
Взгляните на https://github.com/klauspost/compress/pull/176
@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 сообщение, выделенное, если нет словаря.
Самый полезный комментарий
Я подал golang / go # 32371