Go golang-nsq Series--nsqlookupd Source Parsing

Posted by Lee on Sat, 23 Nov 2019 14:05:58 +0100

Previous Introduces the code logic and flow chart of nsqd. This paper parses another module nsqlookupd in nsq, which is responsible for maintaining the topology information of nsqd nodes, and achieves the service registration and discovery of de-centralization.

1. nsqlookupd Execution Entry

Execution entry files can be found in nsq/apps/nsqlookupd/main.go as follows:


2. nsqlookupd executes main logic

The main process is similar to the nsqd execution logic described in the previous section, except that the specific tasks run are different.

2.1 Elegant background process management through third-party SVC packages, svc.Run () -> svc.Init () -> svc.Start (), start nsqlookupd instance;

func main() {
  prg := &program{}
  if err := svc.Run(prg, syscall.SIGINT, syscall.SIGTERM); err != nil {
    logFatal("%s", err)
  }
}

func (p *program) Init(env svc.Environment) error {
  if env.IsWindowsService() {
    dir := filepath.Dir(os.Args[0])
    return os.Chdir(dir)
  }
  return nil
}

func (p *program) Start() error {
  opts := nsqlookupd.NewOptions()

  flagSet := nsqlookupdFlagSet(opts)
  ...
}


2.2 Initialize the configuration parameters (priority: flagSet-command-line parameters > cfg-configuration File > opts-default), open the protocol, and enter the nsqlookupd.Main() main function;

options.Resolve(opts, flagSet, cfg)
  nsqlookupd, err := nsqlookupd.New(opts)
  if err != nil {
    logFatal("failed to instantiate nsqlookupd", err)
  }
  p.nsqlookupd = nsqlookupd

  go func() {
    err := p.nsqlookupd.Main()
    if err != nil {
      p.Stop()
      os.Exit(1)
    }
  }()


2.3 Open goroutine to execute tcpServer, httpServer to listen for client requests from nsqd and nsqadmin, respectively;

func (l *NSQLookupd) Main() error {
  ctx := &Context{l}

  exitCh := make(chan error)
  var once sync.Once
  exitFunc := func(err error) {
    once.Do(func() {
      if err != nil {
        l.logf(LOG_FATAL, "%s", err)
      }
      exitCh <- err
    })
  }

  tcpServer := &tcpServer{ctx: ctx}
  l.waitGroup.Wrap(func() {
    exitFunc(protocol.TCPServer(l.tcpListener, tcpServer, l.logf))
  })
  httpServer := newHTTPServer(ctx)
  l.waitGroup.Wrap(func() {
    exitFunc(http_api.Serve(l.httpListener, httpServer, "HTTP", l.logf))
  })

  err := <-exitCh
  return err
}


2.4 TCPServer loops to listen for client requests, establish long connections for communication, and turn on handler to process each client conn;

func TCPServer(listener net.Listener, handler TCPHandler, logf lg.AppLogFunc) error {
  logf(lg.INFO, "TCP: listening on %s", listener.Addr())

  for {
    clientConn, err := listener.Accept()
    if err != nil {
      if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
        logf(lg.WARN, "temporary Accept() failure - %s", err)
        runtime.Gosched()
        continue
      }
      // theres no direct way to detect this error because it is not exposed
      if !strings.Contains(err.Error(), "use of closed network connection") {
        return fmt.Errorf("listener.Accept() error - %s", err)
      }
      break
    }
    go handler.Handle(clientConn)
  }

  logf(lg.INFO, "TCP: closing %s", listener.Addr())

  return nil
}


2.5 httpServer uses http_api.Decorate decorator to decorate each HTTP route with handler, such as log log log, uniform format output of V1 protocol version number, etc.

func newHTTPServer(ctx *Context) *httpServer {
  log := http_api.Log(ctx.nsqlookupd.logf)

  router := httprouter.New()
  router.HandleMethodNotAllowed = true
  router.PanicHandler = http_api.LogPanicHandler(ctx.nsqlookupd.logf)
  router.NotFound = http_api.LogNotFoundHandler(ctx.nsqlookupd.logf)
  router.MethodNotAllowed = http_api.LogMethodNotAllowedHandler(ctx.nsqlookupd.logf)
  s := &httpServer{
    ctx:    ctx,
    router: router,
  }

  router.Handle("GET", "/ping", http_api.Decorate(s.pingHandler, log, http_api.PlainText))
  router.Handle("GET", "/info", http_api.Decorate(s.doInfo, log, http_api.V1))

  // v1 negotiate
  router.Handle("GET", "/debug", http_api.Decorate(s.doDebug, log, http_api.V1))
  router.Handle("GET", "/lookup", http_api.Decorate(s.doLookup, log, http_api.V1))
  router.Handle("GET", "/topics", http_api.Decorate(s.doTopics, log, http_api.V1))
  router.Handle("GET", "/channels", http_api.Decorate(s.doChannels, log, http_api.V1))
}


2.6 tcp resolves the V1 protocol and loops the client commands using the prot.IOLoop(conn) encapsulated by the internal protocol until all resolving of the client commands is completed.

var prot protocol.Protocol
  switch protocolMagic {
  case "  V1":
    prot = &LookupProtocolV1{ctx: p.ctx}
  default:
    protocol.SendResponse(clientConn, []byte("E_BAD_PROTOCOL"))
    clientConn.Close()
    p.ctx.nsqlookupd.logf(LOG_ERROR, "client(%s) bad protocol magic '%s'",
      clientConn.RemoteAddr(), protocolMagic)
    return
  }

  err = prot.IOLoop(clientConn)


2.7 Perform p.Exec (Execute Command), p.SendResponse (Return Result) through the internal protocol to ensure that each nsqd node can register and unregister correctly, and to Ping the availability of the node to ensure that the list of nsqd nodes fetched by the client is up to date.

for {
    line, err = reader.ReadString('\n')
    if err != nil {
      break
    }

    line = strings.TrimSpace(line)
    params := strings.Split(line, " ")

    var response []byte
    response, err = p.Exec(client, reader, params)
    if err != nil {
      ctx := ""
      if parentErr := err.(protocol.ChildErr).Parent(); parentErr != nil {
        ctx = " - " + parentErr.Error()
      }
      _, sendErr := protocol.SendResponse(client, []byte(err.Error()))
      if sendErr != nil {
        p.ctx.nsqlookupd.logf(LOG_ERROR, "[%s] - %s%s", client, sendErr, ctx)
        break
      }
      continue
    }

    if response != nil {
      _, err = protocol.SendResponse(client, response)
      if err != nil {
        break
      }
    }
  }

  conn.Close()


3. Summary of nsqlookupd flowchart

The above process summary diagram is as follows:


[Summary] By reading and parsing the source code, we can see that the function of nsqlookupd is to manage the authentication, registration, logoff, heartbeat detection of nsqd nodes, and dynamically maintain the latest available list of nsqd nodes in the distributed cluster for clients to use.

The source code uses many RWMutex read-write locks, the public interface of the interface protocol, and concurrent communication between goroutine/channel protocols, thus ensuring high availability and high throughput.


Topics: Go network