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)

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

с гитхаба, с мастера, качать зависимости так это вообще трешак

Зависимости на уровне исходников — это неплохо, ящетаю. Целый пласт проблем, проистекающих из возни с артефактами и их версиями, просто исчезает. Зачем паковать исходники в архив с наклейкой ручной работы и где-то его отдельно хостить только для того, чтобы юзер его сразу распаковал обратно? Не проще ли просто выдать ему охапку исходников с нужной полки? Если, конечно, ты не проприетараст.

И чем хэш коммита принципиально хуже версии артефакта? К нему можно точно так же привязаться для воспроизводимости, как и к версии. Разве что хэши не упорядочены последовательно и нельзя задать диапазон версий… но как раз именно в этом месте начинает отчётливо пованивать говном %)

Опять же теги есть.

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

лисп из 50-60 и современные реализации CL это две большие разницы.

И построены лиспы по другому принципу, не по worse is better(херак херак и в продакшн), и перекладыванию и парсингу говна из пайпов, а по MIT принципу «нормально делай, нормально будет».

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

Вот только пайпы остались актуальны, а LISP нет :D

Даже если завтра лисп окончательно умрёт (не дождётесь), здравые и полезные идеи из него никуда из наших недоязычков уже не денутся %)

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

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

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

Вангую не очищается переменная которая объявляется в цикле, классика же.

Нет, тут другое. Есть два буфера, один побольше, другой поменьше. Из-за логической ошибки строка пишется в меньший и переполняет. Другая сишная классика :)

cumvillain
()