LINUX.ORG.RU

Утечки памяти в golang приложении - как найти?

 , ,


1

9

Изучаю сетевое программирование в golang, нашел в каком-то блоге вот такой пример websocket чат-сервера. Как он работает - вполне понятно - тупо бродкастит сообщения от клиентов. Но почему после нескольких коннектов ядро прибивает процесс из-за OOM - не могу разобраться. Вроде все ресурсы освобождаются, каналы закрываются. Пробовал проанализировать кучу с помощью pprof - не могу поймать момент, когда происходит утечка. Как вообще отлаживаются подобные ошибки в go?


package main

import (
  "encoding/json"
  "fmt"
  "net"
  "net/http"

  "github.com/gorilla/websocket"
  uuid "github.com/satori/go.uuid"
)

//Client management
type ClientManager struct {
  //The client map stores and manages all long connection clients, online is TRUE, and those who are not there are FALSE
  clients map[*Client]bool
  //Web side MESSAGE we use Broadcast to receive, and finally distribute it to all clients
  broadcast chan []byte
  //Newly created long connection client
  register chan *Client
  //Newly canceled long connection client
  unregister chan *Client
}

//Client
type Client struct {
	//User ID
	id string
	//Connected socket
	socket *websocket.Conn
	//Message
	send chan []byte
}

//Will formatting Message into JSON
type Message struct {
	//Message Struct
	Sender    string `json:"sender,omitempty"`
	Recipient string `json:"recipient,omitempty"`
	Content   string `json:"content,omitempty"`
	ServerIP  string `json:"serverIp,omitempty"`
	SenderIP  string `json:"senderIp,omitempty"` 
}

//Create a client manager
var manager = ClientManager{
	broadcast:  make(chan []byte),
	register:   make(chan *Client),
	unregister: make(chan *Client),
	clients:    make(map[*Client]bool),
}

func (manager *ClientManager) start() {
  for {
    select {
    //If there is a new connection access, pass the connection to conn through the channel
    case conn := <-manager.register:
      //Set the client connection to true
      manager.clients[conn] = true
      //Format the message of returning to the successful connection JSON
      jsonMessage, _ := json.Marshal(&Message{Content: "/A new socket has connected. ", ServerIP: LocalIp(), SenderIP: conn.socket.RemoteAddr().String()})
      //Call the client's send method and send messages
      manager.send(jsonMessage, conn)
      //If the connection is disconnected
    case conn := <-manager.unregister:
      //Determine the state of the connection, if it is true, turn off Send and delete the value of connecting client
      if _, ok := manager.clients[conn]; ok {
      	close(conn.send)
      	delete(manager.clients, conn)
      	jsonMessage, _ := json.Marshal(&Message{Content: "/A socket has disconnected. ", ServerIP: LocalIp(), SenderIP: conn.socket.RemoteAddr().String()})
      	manager.send(jsonMessage, conn)
      }
    	//broadcast
    case message := <-manager.broadcast:
    	//Traversing the client that has been connected, send the message to them
    	for conn := range manager.clients {
          select {
          case conn.send <- message:
      	  default:
      	    close(conn.send)
            delete(manager.clients, conn)
      	  }
    	}
    }
  }
}

//Define the send method of client management
func (manager *ClientManager) send(message []byte, ignore *Client) {
  for conn := range manager.clients {
    //Send messages not to the shielded connection
    if conn != ignore {
      conn.send <- message
    }
  }
}

//Define the read method of the client structure
func (c *Client) read() {
  defer func() {
    manager.unregister <- c
    _ = c.socket.Close()
  }()

  for {
    //Read message
    _, message, err := c.socket.ReadMessage()
    //If there is an error message, cancel this connection and then close it
    if err != nil {
    	manager.unregister <- c
    	_ = c.socket.Close()
    	break
    }
    //If there is no error message, put the information in Broadcast
    jsonMessage, _ := json.Marshal(&Message{Sender: c.id, Content: string(message), ServerIP: LocalIp(), SenderIP: c.socket.RemoteAddr().String()})
    manager.broadcast <- jsonMessage
  }
}

func (c *Client) write() {
  defer func() {
    _ = c.socket.Close()
  }()

  for {
    select {
    //Read the message from send
    case message, ok := <-c.send:
      //If there is no message
      if !ok {
        _ = c.socket.WriteMessage(websocket.CloseMessage, []byte{})
        return
      }
      //Write it if there is news and send it to the web side
      _ = c.socket.WriteMessage(websocket.TextMessage, message)
    }
  }
}

func main() {
	fmt.Println("Starting application...")

	//Open a goroutine execution start program
	go manager.start()
	//Register the default route to /ws, and use the wsHandler method
	http.HandleFunc("/ws", wsHandler)
	http.HandleFunc("/health", healthHandler)
	//Surveying the local 8011 port
	fmt.Println("chat server start.....")
	//Note that this must be 0.0.0.0 to deploy in the server to use
	_ = http.ListenAndServe("0.0.0.0:8448", nil)
}

var upgrader = websocket.Upgrader{
  ReadBufferSize:  1024 * 1024 * 1024,
  WriteBufferSize: 1024 * 1024 * 1024,
  //Solving cross-domain problems
  CheckOrigin: func(r *http.Request) bool {
    return true
  },
}

func wsHandler(res http.ResponseWriter, req *http.Request) {
  //Upgrade the HTTP protocol to the websocket protocol
  conn, err := upgrader.Upgrade(res, req, nil)
  if err != nil {
    http.NotFound(res, req)
    return
  }

  //Every connection will open a new client, client.id generates through UUID to ensure that each time it is different
  client := &Client{id: uuid.Must(uuid.NewV4(), nil).String(), socket: conn, send: make(chan []byte)}
  //Register a new link
  manager.register <- client

  //Start the message to collect the news from the web side
  go client.read()
  //Start the corporation to return the message to the web side
  go client.write()
}

func healthHandler(res http.ResponseWriter, _ *http.Request) {
  _, _ = res.Write([]byte("ok"))
}

func LocalIp() string {
  address, _ := net.InterfaceAddrs()
  var ip = "localhost"
  for _, address := range address {
    if ipAddress, ok := address.(*net.IPNet); ok && !ipAddress.IP.IsLoopback() {
      if ipAddress.IP.To4() != nil {
        ip = ipAddress.IP.String()
      }
    }
  }
  return ip
}



Последнее исправление: C (всего исправлений: 1)

Ответ на: комментарий от beastie

По ходу там горилла буянит. Выделяет по 2GB на каждый коннект.

(pprof) top
Showing nodes accounting for 4GB, 100% of 4GB total
      flat  flat%   sum%        cum   cum%
       2GB 50.00% 50.00%        4GB   100%  github.com/gorilla/websocket.newConn
       2GB 50.00%   100%        2GB 50.00%  bufio.NewReaderSize (inline)
         0     0%   100%        4GB   100%  github.com/gorilla/websocket.(*Upgrader).Upgrade
         0     0%   100%        4GB   100%  main.wsHandler
         0     0%   100%        4GB   100%  net/http.(*ServeMux).ServeHTTP
         0     0%   100%        4GB   100%  net/http.(*conn).serve
         0     0%   100%        4GB   100%  net/http.HandlerFunc.ServeHTTP
         0     0%   100%        4GB   100%  net/http.serverHandler.ServeHTTP
(pprof) list newConn
Total: 4GB
ROUTINE ======================== github.com/gorilla/websocket.newConn in /Users/dima/go/pkg/mod/github.com/gorilla/websocket@v1.5.0/conn.go
       2GB        4GB (flat, cum)   100% of Total
         .          .    285:func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int, writeBufferPool BufferPool, br *bufio.Reader, writeBuf []byte) *Conn {
         .          .    286:
         .          .    287:	if br == nil {
         .          .    288:		if readBufferSize == 0 {
         .          .    289:			readBufferSize = defaultReadBufferSize
         .          .    290:		} else if readBufferSize < maxControlFramePayloadSize {
         .          .    291:			// must be large enough for control frame
         .          .    292:			readBufferSize = maxControlFramePayloadSize
         .          .    293:		}
         .        2GB    294:		br = bufio.NewReaderSize(conn, readBufferSize)
         .          .    295:	}
         .          .    296:
         .          .    297:	if writeBufferSize <= 0 {
         .          .    298:		writeBufferSize = defaultWriteBufferSize
         .          .    299:	}
         .          .    300:	writeBufferSize += maxFrameHeaderSize
         .          .    301:
         .          .    302:	if writeBuf == nil && writeBufferPool == nil {
       2GB        2GB    303:		writeBuf = make([]byte, writeBufferSize)
         .          .    304:	}
         .          .    305:
         .          .    306:	mu := make(chan struct{}, 1)
         .          .    307:	mu <- struct{}{}
         .          .    308:	c := &Conn{
(pprof)
beastie ★★★★★
()
Последнее исправление: beastie (всего исправлений: 1)
Ответ на: комментарий от beastie

Вместо гориллы можно попробовать https://pkg.go.dev/golang.org/x/net/websocket и сравнить. Вообще гориллу очень не советую. Она лишняя и буйная по определению. Всё что нужно для счастья есть в stdlib.

И ещё https://pkg.go.dev/github.com/google/uuid более каконичный пакет. У satori были ЕМНИС большие проблемы с UUIDv4 и он давным давно deprecated.

beastie ★★★★★
()
Последнее исправление: beastie (всего исправлений: 2)
Ответ на: комментарий от beastie

По ходу там горилла буянит. Выделяет по 2GB на каждый коннект.

(pprof) top 
>>Showing nodes accounting for 4GB, 100% of 4GB total

Спасибо, так отлаживать я не умел.

Поискал в багтрекере гориллы на предмет утечек - и потом заметил, что буферы чтения и записи для websocket.Upgrader по 1Gb выделяются в коде :)

var upgrader = websocket.Upgrader{
  ReadBufferSize:  1024 * 1024 * 1024,
  WriteBufferSize: 1024 * 1024 * 1024,
  //Solving cross-domain problems
  CheckOrigin: func(r *http.Request) bool {
    return true
  },
}

Я почему-то думал, что это что-то с каналами или горутинами

C
() автор топика
Последнее исправление: C (всего исправлений: 2)
Ответ на: комментарий от C

Выделенная память – не занятая память. Но таки да, неприятно.

Там ещё опасненькие манипуляции с map с потенциалом к панике с «concurrent write to map».

Я бы не стал ровняться на этот «туториал» (не буду приводить тут ссылку, но гуглится легко по комментариям).

beastie ★★★★★
()
Последнее исправление: beastie (всего исправлений: 1)
Ответ на: комментарий от beastie

Там ещё опасненькие манипуляции с map с потенциалом к панике с «concurrent write to map»

Вроде только из горутины с именем start происходит доступ к map. А она в одном экземпляре запускается

C
() автор топика
Ответ на: комментарий от beastie

Я бы не стал ровняться на этот «туториал» (не буду приводить тут ссылку, но гуглится легко по комментариям).

А какие небольшие проекты с хорошим конкурентным кодом на go поизучать? (не больше пары тысяч строк)

C
() автор топика
Ответ на: комментарий от C

Пересмотри ранние выступления от Го-команды (Пайк, Жеранд,..), там было много хороших докладов про идиоматические приемы. Ну а по коду можно смотреть сам Го и его стандартную библиотеку.

urxvt ★★★★★
()
Ответ на: комментарий от C

Даже сложно назвать с ходу. Как и со всем, лучше не зло-употреблять. В общей практике оно относительно редко нужно. Можно посмотреть на net/http.

Тут вот ещё например использование со смыслом: Lexical Scanning in Go - Rob Pike; вот ещё Go Concurrency Patterns; но это всё все уже видели.

beastie ★★★★★
()
Последнее исправление: beastie (всего исправлений: 1)
Ответ на: комментарий от C

какой трешак

Все кто упарывается по Го - умственно отсталые, просто невменозники нахер. Полные. Потому что это трешак и дерьмо же, полнейшее, уровня JS, что экосистема, что сам язык

lovesan ★★
()
Последнее исправление: lovesan (всего исправлений: 1)
Ответ на: комментарий от lovesan

Не все так просто. У Го есть плюсы, которые всем нравятся - читаемость, горутины и быстрая компиляция. Хотелось бы более конкретных претензий к языку и экосистеме. Хотя я понимаю, хочется, чтобы все на LISP писали.

C
() автор топика
Последнее исправление: C (всего исправлений: 1)
Ответ на: комментарий от C

читаемость, горутины и быстрая компиляция

Есть примерно везде кроме C++/Rust(разные виды асинхронности вместо горутин). Для этого не нужно использовать гугловское поделие для умственно отсталых.

lovesan ★★
()
Ответ на: комментарий от lovesan

Хорошо. Я когда первый раз go увидел - было довольно отвратно смотреть на синтаксис. Но после прочтения пары книг я понял, что этот язык имеет право на жизнь.

Вот выше приведен код простейшего websocket сервера, с использованием библиотеки из экосистемы. В нем пожалуй только ping/pong обработчика не хватает. Сколько строк кода займет подобный проект на CL? И будет ли он настолько же прозрачным для восприятия?

C
() автор топика
Ответ на: комментарий от C

Что значит прозрачность для восприятия вообще? Если ты синтаксис не знаешь, и вообще не знаком с языком, тогда тебе нихрена не прозрачно. В том числе даже Go.

Сервер займет меньше кода.

lovesan ★★
()
Ответ на: комментарий от lovesan

Не имею ничего против функционального программирования - не зря map/filter/reduce и лямбды прижились почти во всех современных языках. Но задай себе вопрос - каким образом тот же python оказался наиболее востребованным языком? В первую очередь - это наличие огромного количества библиотек и легкость их использования. Go идет по его стопам в настоящий момент в сфере написания сетевых сервисов.

C
() автор топика
Ответ на: комментарий от C

Не имею ничего против функционального программирования - не зря map/filter/reduce и лямбды прижились почти во всех современных языках.

Common Lisp, если ты к этому - это не про функциональное программирование.

Но задай себе вопрос - каким образом тот же python оказался наиболее востребованным языком?

Продвижение крупными корпорациями типа гугла. Плюс сам царь гвидон тот еще хайпожор и нарцисс. Хомячки ведутся на любой хайп.

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

Говно там библиотеки. И всегда были. Включая стандартную. С тем же CPAN, или же с жабовской экосистемой, когда бидон еще только набирал популярность, оно вообще не было сравнимо. Да и вряд ли и щас сравнимо.

Go идет по его стопам в настоящий момент в сфере написания сетевых сервисов.

Go идет по тому же пути что и бидон - это проталкивание его хомячкам гуглом. Хомячки такие - уууу, это же гугол, гугол не может быть не прав. Несмотря на то что и библиотеки там говно, включая стандартную, и под асинхронное и распределенное программирование он подходит как корове пятая нога, и рантайм там говно(это я понял когда еще давно в свое время на эрланге писал).

lovesan ★★
()
Ответ на: комментарий от lovesan

Все кто упарывается по Го - умственно отсталые, просто невменозники нахер. Полные. Потому что это трешак и дерьмо же, полнейшее, уровня JS, что экосистема, что сам язык

Ты просто не осилил.

cumvillain
()
Ответ на: комментарий от cumvillain

нормальный пакетный менеджер

ЛОЛ. Да это говно из говен. А с гитхаба, с мастера, качать зависимости так это вообще трешак, до которого даже жопоскриптеры не додумались.

Остальное есть вообще примерно везде. Ну, в бидоне с пакетами жопа, разве что.

Java и C# уделывают это говно по всем примерно этим параметрам, в частности.

lovesan ★★
()
Ответ на: комментарий от lovesan

ЛОЛ. Да это говно из говен. А с гитхаба, с мастера, качать зависимости так это вообще трешак, до которого даже жопоскриптеры не додумались.

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

Остальное есть вообще примерно везде.

Есть. Только за голанг, в отличии от лиспа, ещё и и платят. Ну и библиотек больше.

Ну, в бидоне с пакетами жопа, разве что.

Зачем ты засунул жопу в бидон? Найди себе девушку и засовывай в неё.

Java и C# уделывают это говно по всем примерно этим параметрам, в частности.

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

cumvillain
()
Ответ на: комментарий от cumvillain

Если ты не понимаешь что не так с тем чтобы качать какие-то левые сорцы в гитхаба, и с тем что это интегрировано в язык, у тебя огромные проблемы с головой.

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

PS. консольные приложения можно писать вообще на чем угодно, и если конкретно Java иметь ввиду, то ее время старта компенсируется тем что внутри нее можно делать уже все что тебе надо, и парсинг входных данных и их обработку и все остальное, вместо того чтобы гонять говно по трубам пайпам между кучей процессов как в 70х, аля юнипс-ой-вей.

lovesan ★★
()
Последнее исправление: lovesan (всего исправлений: 1)
Ответ на: комментарий от lovesan

Если ты не понимаешь что не так с тем чтобы качать какие-то левые сорцы в гитхаба, и с тем что это интегрировано в язык, у тебя огромные проблемы с головой.

Буквально то же самое позволяет Paket для в C#. Ну и где твой бог теперь?

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

Я тебя и так не возьму, даже если ты придешь :D

cumvillain
()
Ответ на: комментарий от lovesan

PS. консольные приложения можно писать вообще на чем угодно, и если конкретно Java иметь ввиду, то ее время старта компенсируется тем что внутри нее можно делать уже все что тебе надо, и парсинг входных данных и их обработку и все остальное, вместо того чтобы гонять говно по трубам пайпам между кучей процессов как в 70х, аля юнипс-ой-вей.

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

cumvillain
()
Ответ на: комментарий от cumvillain

Когда у тебя будет 15 лет опыта промышленной разработки софта, возвращайся на ЛОР умничать. Про Go, и все остальное. А пока иди завари доширак и лабы доделывай.

lovesan ★★
()
Ответ на: комментарий от lovesan

Когда у тебя будет 15 лет опыта промышленной разработки софта, возвращайся на ЛОР умничать. Про Go, и все остальное. А пока иди завари доширак и лабы доделывай.

Судя по тому что ты пишешь, у тебя есть 15 лет опыта в промышленном быдлокодировании на сишарпе. К разработке софта это не имеет никакого отношения. Мы на разном уровне.

cumvillain
()