Elegantly manage multiple services in one process

kevwan

Kevin Wan

Posted on April 21, 2022

Elegantly manage multiple services in one process

Image description

Background

I was often asked if it is possible to put API gateway and RPC service in the same process, and how to do it? There are also devs who put the external service and the message queue in the same process. Let's see how to solve such a problem more elegantly.

Problem

Let's use two mocking services as an example. We have two services that need to be started on two different ports in one process. The code is as following.

package main

import (
  "fmt"
  "net/http"
)

func morning(w http.ResponseWriter, req *http.Request) {
  fmt.Fprintln(w, "morning!")
}

func evening(w http.ResponseWriter, req *http.Request) {
  fmt.Fprintln(w, "evening!")
}

type Morning struct{}

func (m Morning) Start() {
  http.HandleFunc("/morning", morning)
  http.ListenAndServe("localhost:8080", nil)
}

func (m Morning) Stop() {
  fmt.Println("Stop morning service...")
}

type Evening struct{}

func (e Evening) Start() {
  http.HandleFunc("/evening", evening)
  http.ListenAndServe("localhost:8081", nil)
}

func (e Evening) Stop() {
  fmt.Println("Stop evening service...")
}

func main() {
  // todo: start both services here
}
Enter fullscreen mode Exit fullscreen mode

The code is simple enough to have a request for the morning service and the service returns morning! and a request for the evening service and the service returns evening. Let's try to implement it~

First try

Is it possible to start both services in main? Let's try.

func main() {
  var morning Morning
  morning.Start()
  defer morning.Stop()

  var evening Evening
  Start()
  Stop()
}
Enter fullscreen mode Exit fullscreen mode

After starting, let's verify with curl.

$ curl -i http://localhost:8080/morning
HTTP/1.1 200 OK
Date: Mon, 18 Apr 2022 02:10:34 GMT
Content-Length: 9
Content-Type: text/plain; charset=utf-8

morning!
$ curl -i http://localhost:8081/evening
curl: (7) Failed to connect to localhost port 8081 after 4 ms: Connection refused
Enter fullscreen mode Exit fullscreen mode

Why only morning succeeds, but evening cannot be requested?

Let's add a print statement to main and try

func main() {
  fmt.Println("Start morning service...")
  var morning Morning
  morning.Start()
  defer morning.Stop()

  fmt.Println("Start evening service...")
  var evening Evening
  Start()
  Stop() defer evening.
}
Enter fullscreen mode Exit fullscreen mode

Re-run and see.

$ go run main.go
Start morning service...
Enter fullscreen mode Exit fullscreen mode

It only prints Start morning service..., so the evening service is not started at all. The reason for this is that morning.Start() blocks the current goroutine, so the subsequent code is not executed.

Second try

This is where WaitGroup come in handy. As the name implies, WaitGroup is used to wait a group of operations and wait for them to be done so that the waiting goroutine can continue. Let's try it out.

func main() {
  var wg sync.WaitGroup
  wg.Add(2)

  go func() {
    defer wg.Done()
    fmt.Println("Start morning service...")
    var morning Morning
    defer morning.Stop()
    morning.Start()
  Start() }()

  go func() {
    defer wg.Done()
    fmt.Println("Start evening service...")
    var evening Evening
    defer evening.Stop()
    evening.Start()
  Start() }()

  wg.Wait()
}
Enter fullscreen mode Exit fullscreen mode

Try it.

$ go run main.go
Start evening service...
Start morning service...
Enter fullscreen mode Exit fullscreen mode

Okay, both services are up, so let's verify with curl.

$ curl -i http://localhost:8080/morning
HTTP/1.1 200 OK
Date: Mon, 18 Apr 2022 02:28:33 GMT
Content-Length: 9
Content-Type: text/plain; charset=utf-8

morning!
$ curl -i http://localhost:8081/evening
HTTP/1.1 200 OK
Date: Mon, 18 Apr 2022 02:28:36 GMT
Content-Length: 9
Content-Type: text/plain; charset=utf-8

evening!
Enter fullscreen mode Exit fullscreen mode

It works, and we see that our process with WaitGroup is

  1. remember that we have several services that need wait
  2. add the services one by one
  3. wait for all services to finish

Let's see how go-zero handle it~

Third try

In go-zero, we provide a ServiceGroup to easily manage the start and stop of multiple services. Let's see how it is done by using it in our scenario.

import "github.com/zeromicro/go-zero/core/service"

// more code here

func main() {
  group := service.NewServiceGroup()
  defer group.Stop()
  group.Add(Morning{})
  group.Add(Evening{})
  group.Start()
}
Enter fullscreen mode Exit fullscreen mode

As you can see, the code is much more readable and we can't accidentally miscalculate how many to add to WaitGroup. And ServiceGroup also makes sure that services that Start later Stop first, which is the same as defer, and makes it easier to clean up resources.

The ServiceGroup not only manages the Start/Stop of each service, but also provides graceful shutdown, which actively calls the Stop method of each service when a SIGTERM signal is received, and for HTTP services, you can exit gracefully with server.Shutdown for HTTP services and server.GracefulStop() for gRPC services.

Summary

The implementation of ServiceGroup is simple enough, with 82 lines of code.

$ cloc core/service/servicegroup.go
------------------------------------------------------------------
Language files blank comment code
------------------------------------------------------------------
Go           1    22      14   82
------------------------------------------------------------------
Enter fullscreen mode Exit fullscreen mode

The code is short and concise, every service (Restful, RPC, MQ) is basically managed by ServiceGroup in go-zero, which is very convenient and worth reading.

Project address

https://github.com/zeromicro/go-zero

Welcome to use go-zero and star to support us!

💖 💪 🙅 🚩
kevwan
Kevin Wan

Posted on April 21, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related