GoSuda

Konfiguration eines eingebetteten NATS-Servers in Go-Anwendungen und Struktur der prozessinternen Kommunikation

By prravda views ...

Erste Schritte

Über NATS

Softwareanwendungen und Dienste mΓΌssen Daten austauschen. NATS ist eine Infrastruktur, die einen solchen Datenaustausch ermΓΆglicht, der in Form von Nachrichten segmentiert wird. Wir bezeichnen dies als "message oriented middleware".

Mit NATS kΓΆnnen Anwendungsentwickler:

  • Verteilte und skalierbare Client-Server-Anwendungen mΓΌhelos erstellen.
  • Daten in Echtzeit auf allgemeine Weise speichern und verteilen. Dies lΓ€sst sich flexibel ΓΌber verschiedene Umgebungen, Sprachen, Cloud-Anbieter und On-Premises-Systeme hinweg realisieren.

What is NATS, NATS docs

  • NATS λŠ” Go 둜 κ΅¬μ„±λœ λ©”μ‹œμ§€ λΈŒλ‘œμ»€μ΄λ‹€.

Eingebettetes NATS

Wenn Ihre Anwendung in Go geschrieben ist und dies zu Ihrem Anwendungsfall und Ihren Bereitstellungsszenarien passt, kΓΆnnen Sie sogar einen NATS-Server in Ihre Anwendung einbetten.

Embedding NATS, NATS docs

  • 그리고 NATS 의 νŠΉμ΄μ‚¬ν•­μ΄ μžˆλŠ”λ°, Go 둜 κ΅¬μ„±λœ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ 경우 embedded mode λ₯Ό μ§€μ›ν•œλ‹€λŠ” 것이닀.
  • 즉, λ©”μ‹œμ§€ 브둜컀의 일반적인 방식인 λ³„λ„μ˜ 브둜컀 μ„œλ²„ ꡬ동 ν›„ ν•΄λ‹Ή μ„œλ²„μ™€ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ ν΄λΌμ΄μ–ΈνŠΈλ₯Ό 톡해 ν†΅μ‹ ν•˜λŠ” ꡬ쑰가 μ•„λ‹Œ, 브둜컀 자체λ₯Ό Go 둜 λ§Œλ“  μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ— λ‚΄μž₯(embed)ν•  수 μžˆλ‹€λŠ” 이야기이닀.

Vorteile und AnwendungsfΓ€lle von eingebettetem NATS

  • 잘 μ„€λͺ…λœ Youtube μ˜μƒμ΄ μžˆμ–΄μ„œ μ˜μƒμ˜ 링크둜 κ°ˆμŒν•œλ‹€.
  • λ³„λ„μ˜ λ©”μ‹œμ§€ 브둜컀 μ„œλ²„λ₯Ό λ°°ν¬ν•˜μ§€ μ•Šλ”λΌλ„ modular monolith applictaion 을 λ§Œλ“€μ–΄μ„œ separate of concern 을 λ‹¬μ„±ν•˜κΈ°λ„ ν•˜λ©΄μ„œ nats λ₯Ό embedded 둜 심을 수 μžˆλŠ” μž₯점을 μ·¨ν•  μˆ˜κ°€ μžˆλ‹€. λ”ν•˜μ—¬ single binary deployment 도 κ°€λŠ₯ν•΄μ§„λ‹€.
  • platform with no network(wasm) 뿐만 μ•„λ‹ˆλΌ, offline-first application μ—μ„œ μœ μš©ν•˜κ²Œ μ‚¬μš©ν•  수 μžˆλ‹€.

Beispiel aus der offiziellen Dokumentation

 1package main
 2
 3import (
 4    "fmt"
 5    "time"
 6
 7    "github.com/nats-io/nats-server/v2/server"
 8    "github.com/nats-io/nats.go"
 9)
10
11func main() {
12    opts := &server.Options{}
13
14    // Initialize new server with options
15    ns, err := server.NewServer(opts)
16
17    if err != nil {
18        panic(err)
19    }
20
21    // Start the server via goroutine
22    go ns.Start()
23
24    // Wait for server to be ready for connections
25    if !ns.ReadyForConnections(4 * time.Second) {
26        panic("not ready for connection")
27    }
28
29    // Connect to server
30    nc, err := nats.Connect(ns.ClientURL())
31
32    if err != nil {
33        panic(err)
34    }
35
36    subject := "my-subject"
37
38    // Subscribe to the subject
39    nc.Subscribe(subject, func(msg *nats.Msg) {
40        // Print message data
41        data := string(msg.Data)
42        fmt.Println(data)
43
44        // Shutdown the server (optional)
45        ns.Shutdown()
46    })
47
48    // Publish data to the subject
49    nc.Publish(subject, []byte("Hello embedded NATS!"))
50
51    // Wait for server shutdown
52    ns.WaitForShutdown()
53}
  • NATS 곡식 λ¬Έμ„œκ°€ 걸어놓은 Embedded NATS 의 μ˜ˆμ‹œμΈλ°, ν•΄λ‹Ή μ˜ˆμ‹œ μ½”λ“œλŒ€λ‘œ μ§„ν–‰ν•˜κ²Œ 되면 embedding mode 둜 communication 이 이뀄지지 μ•ŠλŠ”λ‹€.
1Every 2.0s: netstat -an | grep 127.0.0.1         pravdalaptop-home.local: 02:34:20
2                                                                     in 0.017s (0)
3...
4tcp4       0      0  127.0.0.1.4222         127.0.0.1.63769        TIME_WAIT
  • watch 'netstat -an | grep 127.0.0.1' command λ₯Ό 톡해 localhost(127.0.0.1) 둜 μ˜€κ°€λŠ” network λ₯Ό ν™•μΈν•˜λ©΄μ„œ go run . 둜 ν•΄λ‹Ή go file 을 μ‹€ν–‰μ‹œν‚€λ©΄ NATS 의 default port 인 4222 μ—μ„œ μΆœλ°œν•˜λŠ” μƒˆλ‘œμš΄ λ„€νŠΈμ›Œν¬ μš”μ²­λ“€μ΄ μΆ”κ°€λ˜λŠ” κ±Έ λ³Ό 수 μžˆλ‹€.

Richtige Konfigurationen fΓΌr den Embedding-Modus

  • μ˜λ„ν•˜λŠ” λŒ€λ‘œ embedded mode 둜 톡신을 ν•˜κΈ° μœ„ν•΄μ„  λ‹€μŒκ³Ό 같은 두 κ°€μ§€ μ˜΅μ…˜μ΄ ν•„μš”ν•˜λ‹€.

    • Client: InProcessServer option 을 λ„£μ–΄μ£Όμ–΄μ•Ό ν•œλ‹€.
    • Server: Server.Options 에 DontListen μ΄λΌλŠ” flag λ₯Ό true 둜 λͺ…μ‹œν•΄μ•Ό ν•œλ‹€.
  • ν•΄λ‹Ή 뢀뢄듀은 κ³΅μ‹μ μœΌλ‘œ λ¬Έμ„œν™”κ°€ λ˜μ–΄μžˆμ§„ μ•Šμ•˜κ³ , 이 κΈ°λŠ₯의 μ‹œμž‘μ€ ν•΄λ‹Ή PR 을 톡해 νŒŒμ•…ν•  수 μžˆλ‹€.

    This PR adds three things:

    1. InProcessConn() function to Server which builds a net.Pipe to get a connection to the NATS server without using TCP sockets
    2. DontListen option which tells the NATS server not to listen on the usual TCP listener
    3. startupComplete channel, which is closed right before we start AcceptLoop, and readyForConnections will wait for it

    The main motivation for this is that we have an application that can run either in a monolith (single-process) mode or a polylith (multi-process) mode. We'd like to be able to use NATS for both modes for simplicity, but the monolith mode has to be able to cater for a variety of platforms where opening socket connections either doesn't make sense (mobile) or just isn't possible (WASM). These changes will allow us to use NATS entirely in-process instead.

    An accompanying PR nats-io/nats.go#774 adds support to the client side.

    This is my first PR to this project so apologies in advance if I've missed anything obvious anywhere.

    /cc @nats-io/core

Funktionierendes Beispiel fΓΌr den Embedded-Modus

 1package main
 2
 3import (
 4	"fmt"
 5	"time"
 6
 7	"github.com/nats-io/nats-server/v2/server"
 8	"github.com/nats-io/nats.go"
 9)
10
11func main() {
12	opts := &server.Options{
13		// for configuring the embeded NATS server
14		// set DonListen as true
15		DontListen: true,
16	}
17
18	// Initialize new server with options
19	ns, err := server.NewServer(opts)
20
21	if err != nil {
22		panic(err)
23	}
24
25	// Start the server via goroutine
26	go ns.Start()
27
28	// Wait for server to be ready for connections
29	if !ns.ReadyForConnections(10 * time.Second) {
30		panic("not ready for connection")
31	}
32
33	// Connect to server via in-process connection
34	nc, err := nats.Connect(ns.ClientURL(), nats.InProcessServer(ns))
35
36	if err != nil {
37		panic(err)
38	}
39
40	subject := "my-subject"
41
42	// Subscribe to the subject
43	nc.Subscribe(subject, func(msg *nats.Msg) {
44		// Print message data
45		data := string(msg.Data)
46		fmt.Println(data)
47
48		// Shutdown the server (optional)
49		ns.Shutdown()
50	})
51
52	// Publish data to the subject
53	nc.Publish(subject, []byte("Hello embedded NATS!"))
54
55	// Wait for server shutdown
56	ns.WaitForShutdown()
57}
58
1Every 2.0s: netstat -an | grep 127.0.0.1         pravdalaptop-home.local: 02:37:50
2                                                                     in 0.023s (0)
3...no additional logs
4
  • 이제 μ˜λ„ν•œ λŒ€λ‘œ 좔가적인 network hop 이 λ°œμƒν•˜μ§€ μ•ŠλŠ” 것을 λ³Ό 수 μžˆλ‹€.

Unter der Haube

TL;DR

diagram1

  • ν•΄λ‹Ή μ½”λ“œλ₯Ό main.go μ—μ„œ μ‹€ν–‰μ‹œμΌ°μ„ λ•Œ, λ‚΄λΆ€μ μœΌλ‘œ μ–΄λ– ν•œ ν•¨μˆ˜λ“€μ΄ μ–΄λ–»κ²Œ μž‘λ™λ˜λŠ”μ§€λ₯Ό λ‚˜νƒ€λ‚Έ sequence diagram 이며, 골자λ₯Ό μ„€λͺ…ν•˜μžλ©΄ μ•„λž˜μ™€ κ°™λ‹€.
    • DontListen: true λ₯Ό 톡해 μ„œλ²„λŠ” AcceptLoop λΌλŠ” client listening phase λ₯Ό μƒλž΅ν•œλ‹€.
    • client 의 Connect option 쀑 InProcessServer κ°€ ν™œμ„±ν™” λœλ‹€λ©΄ in-memory connection 을 μƒμ„±ν•˜κ³  net.Pipe λ₯Ό 톡해 pipe λ₯Ό λ§Œλ“  λ’€ end of pipe λ₯Ό client μ—κ²Œ net.Conn type 으둜 λ°˜ν™˜ν•œλ‹€.
    • client 와 server κ°€ ν•΄λ‹Ή connection 을 톡해 in-process communication 을 μ§„ν–‰ν•œλ‹€.

Server

AcceptLoop

1// nats-server/server/server.go
2
3// Wait for clients.
4if !opts.DontListen {
5	s.AcceptLoop(clientListenReady)
6}
  • λ¨Όμ € DontListen 이 true 인 경우, AcceptLoop λΌλŠ” client listening phase λ₯Ό μƒλž΅ν•œλ‹€.
 1// nats-server/server/server.go
 2
 3// AcceptLoop is exported for easier testing.
 4func (s *Server) AcceptLoop(clr chan struct{}) {
 5	// If we were to exit before the listener is setup properly,
 6	// make sure we close the channel.
 7	defer func() {
 8		if clr != nil {
 9			close(clr)
10		}
11	}()
12
13	if s.isShuttingDown() {
14		return
15	}
16
17	// Snapshot server options.
18	opts := s.getOpts()
19
20	// Setup state that can enable shutdown
21	s.mu.Lock()
22	hp := net.JoinHostPort(opts.Host, strconv.Itoa(opts.Port))
23	l, e := natsListen("tcp", hp)
24	s.listenerErr = e
25	if e != nil {
26		s.mu.Unlock()
27		s.Fatalf("Error listening on port: %s, %q", hp, e)
28		return
29	}
30	s.Noticef("Listening for client connections on %s",
31		net.JoinHostPort(opts.Host, strconv.Itoa(l.Addr().(*net.TCPAddr).Port)))
32
33	// Alert of TLS enabled.
34	if opts.TLSConfig != nil {
35		s.Noticef("TLS required for client connections")
36		if opts.TLSHandshakeFirst && opts.TLSHandshakeFirstFallback == 0 {
37			s.Warnf("Clients that are not using \"TLS Handshake First\" option will fail to connect")
38		}
39	}
40
41	// If server was started with RANDOM_PORT (-1), opts.Port would be equal
42	// to 0 at the beginning this function. So we need to get the actual port
43	if opts.Port == 0 {
44		// Write resolved port back to options.
45		opts.Port = l.Addr().(*net.TCPAddr).Port
46	}
47
48	// Now that port has been set (if it was set to RANDOM), set the
49	// server's info Host/Port with either values from Options or
50	// ClientAdvertise.
51	if err := s.setInfoHostPort(); err != nil {
52		s.Fatalf("Error setting server INFO with ClientAdvertise value of %s, err=%v", opts.ClientAdvertise, err)
53		l.Close()
54		s.mu.Unlock()
55		return
56	}
57	// Keep track of client connect URLs. We may need them later.
58	s.clientConnectURLs = s.getClientConnectURLs()
59	s.listener = l
60
61	go s.acceptConnections(l, "Client", func(conn net.Conn) { s.createClient(conn) },
62		func(_ error) bool {
63			if s.isLameDuckMode() {
64				// Signal that we are not accepting new clients
65				s.ldmCh <- true
66				// Now wait for the Shutdown...
67				<-s.quitCh
68				return true
69			}
70			return false
71		})
72	s.mu.Unlock()
73
74	// Let the caller know that we are ready
75	close(clr)
76	clr = nil
77}
  • 참고둜 AcceptLoop ν•¨μˆ˜λŠ” λ‹€μŒκ³Ό 같은 과정듀을 μ§„ν–‰ν•œλ‹€. TLS λ‚˜ hostPort 와 같이 network 톡신과 κ΄€λ ¨λœ λΆ€λΆ„μœΌλ‘œ, in-process communication 을 ν•˜κ²Œ 되면 ν•„μš” μ—†λŠ” λΆ€λΆ„λ“€μ΄λ‹ˆ μƒλž΅ν•΄λ„ λ¬΄λ°©ν•œ 뢀뢄듀이닀.

Client

InProcessServer

 1
 2// nats-go/nats.go
 3
 4// Connect will attempt to connect to the NATS system.
 5// The url can contain username/password semantics. e.g. nats://derek:pass@localhost:4222
 6// Comma separated arrays are also supported, e.g. urlA, urlB.
 7// Options start with the defaults but can be overridden.
 8// To connect to a NATS Server's websocket port, use the `ws` or `wss` scheme, such as
 9// `ws://localhost:8080`. Note that websocket schemes cannot be mixed with others (nats/tls).
10func Connect(url string, options ...Option) (*Conn, error) {
11	opts := GetDefaultOptions()
12	opts.Servers = processUrlString(url)
13	for _, opt := range options {
14		if opt != nil {
15			if err := opt(&opts); err != nil {
16				return nil, err
17			}
18		}
19	}
20	return opts.Connect()
21}
 1// nats-go/nats.go
 2
 3// Options can be used to create a customized connection.
 4type Options struct {
 5	// Url represents a single NATS server url to which the client
 6	// will be connecting. If the Servers option is also set, it
 7	// then becomes the first server in the Servers array.
 8	Url string
 9
10	// InProcessServer represents a NATS server running within the
11	// same process. If this is set then we will attempt to connect
12	// to the server directly rather than using external TCP conns.
13	InProcessServer InProcessConnProvider
14
15	//...
16}
1// nats-go/nats.go
2
3type InProcessConnProvider interface {
4	InProcessConn() (net.Conn, error)
5}
  • nats server 와 nats client 의 연결을 μ§„ν–‰ν•˜λŠ” Connect ν•¨μˆ˜λŠ” client URL κ³Ό connect Option 을 μ„€μ •ν•  수 있고, ν•΄λ‹Ή Option 듀을 λͺ¨μ•„놓은 Options struct μ—” InProcesConnProvider interface type 의 InProcessServer λΌλŠ” field κ°€ μ‘΄μž¬ν•œλ‹€.
1// main.go of example code
2
3// Initialize new server with options
4ns, err := server.NewServer(opts)
5
6//...
7
8// Connect to server via in-process connection
9nc, err := nats.Connect(ns.ClientURL(), nats.InProcessServer(ns))
  • nats client μ—μ„œ Connect λ₯Ό μ§„ν–‰ν•  λ•Œ, InProcessServer field 둜 nats.InProcessServer(ns) λ₯Ό λ„˜κ²¨μ£Όκ²Œ 되면
 1// nats-go/nats.go
 2
 3// InProcessServer is an Option that will try to establish a direction to a NATS server
 4// running within the process instead of dialing via TCP.
 5func InProcessServer(server InProcessConnProvider) Option {
 6	return func(o *Options) error {
 7		o.InProcessServer = server
 8		return nil
 9	}
10}
  • option 의 InProcessServer κ°€ embedded nats server 둜 λŒ€μ²΄λ˜κ³ 
 1// nats-go/nats.go
 2
 3// createConn will connect to the server and wrap the appropriate
 4// bufio structures. It will do the right thing when an existing
 5// connection is in place.
 6func (nc *Conn) createConn() (err error) {
 7	if nc.Opts.Timeout < 0 {
 8		return ErrBadTimeout
 9	}
10	if _, cur := nc.currentServer(); cur == nil {
11		return ErrNoServers
12	}
13
14	// If we have a reference to an in-process server then establish a
15	// connection using that.
16	if nc.Opts.InProcessServer != nil {
17		conn, err := nc.Opts.InProcessServer.InProcessConn()
18		if err != nil {
19			return fmt.Errorf("failed to get in-process connection: %w", err)
20		}
21		nc.conn = conn
22		nc.bindToNewConn()
23		return nil
24	}
25
26	//...
27}
  • ν•΄λ‹Ή interface λŠ” connection 을 λ§Œλ“€μ–΄μ£ΌλŠ” createConn ν•¨μˆ˜μ—μ„œ InProcessServer option 이 nil 이 μ•„λ‹Œ(valid ν•œ)경우 option 에 μžˆλŠ” InProcessServer 의 InProcesConn 을 μ‹€ν–‰ν•˜λ©΄μ„œ
 1// nats-server/server/server.go
 2
 3// InProcessConn returns an in-process connection to the server,
 4// avoiding the need to use a TCP listener for local connectivity
 5// within the same process. This can be used regardless of the
 6// state of the DontListen option.
 7func (s *Server) InProcessConn() (net.Conn, error) {
 8	pl, pr := net.Pipe()
 9	if !s.startGoRoutine(func() {
10		s.createClientInProcess(pl)
11		s.grWG.Done()
12	}) {
13		pl.Close()
14		pr.Close()
15		return nil, fmt.Errorf("failed to create connection")
16	}
17	return pr, nil
18}
  • server 에 κ΅¬ν˜„λœ InProcessConn 을 ν˜ΈμΆœν•΄ μ‹€ν–‰ν•œλ‹€.
  • ν•΄λ‹Ή function 은 nats 의 go client 인 nats.go μ—μ„œ nc(nats connection) 의 InProcessServer κ°€ nil 이 아닐 경우 호좜이 λ˜μ–΄ connection(net.Conn) 을 λ§Œλ“€κ³ , 이λ₯Ό server 의 connection 에 bind μ‹œν‚¨λ‹€.

Consumer-gesteuerte Schnittstelle in Go

A type implements an interface by implementing its methods. There is no explicit declaration of intent, no "implements" keyword. Implicit interfaces decouple the definition of an interface from its implementation, which could then appear in any package without prearrangement.

Interfaces are implemented implicitly, A Tour of Go

If a type exists only to implement an interface and will never have exported methods beyond that interface, there is no need to export the type itself.

Generality, Effective Go

  • ν•΄λ‹Ή interface design 은 Go μ—μ„œ ν”νžˆ 말 ν•˜λŠ” consumer defined interface 와 structural typing(duck typing) 을 잘 λ‹΄κ³  μžˆμ–΄μ„œ ν•΄λ‹Ή μ£Όμ œλ„ 같이 μ†Œκ°œν•΄ 보렀고 ν•œλ‹€.
 1// nats-go/nats.go
 2
 3// Options can be used to create a customized connection.
 4type Options struct {
 5	// Url represents a single NATS server url to which the client
 6	// will be connecting. If the Servers option is also set, it
 7	// then becomes the first server in the Servers array.
 8	Url string
 9
10	// InProcessServer represents a NATS server running within the
11	// same process. If this is set then we will attempt to connect
12	// to the server directly rather than using external TCP conns.
13	InProcessServer InProcessConnProvider
14
15	//...
16}
1// nats-go/nats.go
2
3type InProcessConnProvider interface {
4	InProcessConn() (net.Conn, error)
5}
  • λ‹€μ‹œ μ½”λ“œλ‘œ λ„˜μ–΄κ°€λ³΄μž. nats.go client μ—μ„œ InProcessServer option struct field λŠ” InProcessConn λ§Œμ„ μˆ˜ν–‰ν•˜λŠ” InProcessConnProvider interface 둜 μ •μ˜λ˜μ—ˆλ‹€.
 1// nats-server/server/server.go
 2
 3// InProcessConn returns an in-process connection to the server,
 4// avoiding the need to use a TCP listener for local connectivity
 5// within the same process. This can be used regardless of the
 6// state of the DontListen option.
 7func (s *Server) InProcessConn() (net.Conn, error) {
 8	pl, pr := net.Pipe()
 9	if !s.startGoRoutine(func() {
10		s.createClientInProcess(pl)
11		s.grWG.Done()
12	}) {
13		pl.Close()
14		pr.Close()
15		return nil, fmt.Errorf("failed to create connection")
16	}
17	return pr, nil
18}
  • κ·ΈλŸ¬λ‚˜ 그것에 λ“€μ–΄κ°€λŠ” type 은 nats-server 의 Server 둜, InProcessConn 뿐만 μ•„λ‹ˆλΌ λ‹€μ–‘ν•œ κΈ°λŠ₯듀을 μˆ˜ν–‰ν•˜κ³  μžˆλ‹€.
  • μ™œλƒλ©΄ ν•΄λ‹Ή μƒν™©μ—μ„œμ˜ client 의 κ΄€μ‹¬μ‚¬λŠ” InProcessConn μ΄λΌλŠ” interface λ₯Ό μ œκ³΅ν–ˆλŠλƒ μ•„λ‹ˆλƒ 뿐이지, λ‹€λ₯Έ 것듀은 크게 μ€‘μš”ν•˜μ§€ μ•ŠκΈ° λ•Œλ¬Έμ΄λ‹€.
  • λ”°λΌμ„œ nats.go client λŠ” InProcessConn() (net.Conn, error) μ΄λΌλŠ” κΈ°λŠ₯λ§Œμ„ μ •μ˜ν•œ InProcessConnProvider λΌλŠ” consumer defined interface λ§Œμ„ λ§Œλ“€μ–΄μ„œ μ‚¬μš©ν•˜κ³  μžˆλ‹€.

Fazit

  • NATS 의 embedded mode 와 κ·Έ μž‘λ™λ°©μ‹, 그리고 NATS 의 code λ₯Ό 톡해 확인할 수 μžˆλŠ” Go 의 consumer defined interface 에 λŒ€ν•΄ κ°„λž΅ν•˜κ²Œ 닀루어 λ³΄μ•˜λ‹€.
  • ν•΄λ‹Ή 정보가 μœ„μ™€ 같은 λͺ©μ μœΌλ‘œ NATS λ₯Ό μ‚¬μš©ν•˜λŠ” μ‚¬λžŒλ“€μ—κ²Œ 도움이 되길 바라며 이 글을 마치고자 ν•œλ‹€.