Go is designed as a background language and is often used in back-end programs. The server-side program is the most common software product in the Go language. The question I'm going to address here is how to cleanly upgrade a running server-side program.
Goal:
- Do not close existing connections: for example, we do not want to turn off programs that have been deployed in operation. But want to upgrade the service without any restrictions.
- Socket connection to respond to user requests at any time: Any moment the closure of the socket may cause the user to return the ' connection denied ' message, which is undesirable.
- The new process should be able to start and replace the old.
Principle
In a unix-based operating system, signal (signal) is a common method of interacting with long-running processes.
- Sigterm: gracefully Stop the process
- Sighup: Reboot/Reload Process (for example: Nginx, sshd, Apache)
If you receive a sighup signal, gracefully restarting the process requires the following steps:
- The server wants to deny new connection requests, but to maintain existing connections.
- To enable a new version of a process
- "Give" the socket "to" the new process, and the new process begins to accept the new connection request
- Stop the old process as soon as it has finished processing.
Stop accepting connection requests
What server programs have in common: hold a dead loop to accept a connection request:
Copy Code code as follows:
for {
Conn, err: = Listener. Accept ()
Handle Connection
}
The easiest way to jump out of this loop is to set a timeout on the socket listener when calling listener. SetTimeout (time. Now ()) after the listener. Accept () will immediately return a timeout err, which you can capture and handle:
Copy Code code as follows:
for {
Conn, err: = Listener. Accept ()
If Err!= nil {
If nerr, OK: = Err. (NET. ERR); OK && nerr. Timeout () {
Fmt. Println ("Stop Accepting connections")
Return
}
}
}
Note that this operation differs from the closing listener. This process still listens on the server port, but the connection requests are queued by the operating system's network stack, waiting for a process to accept them.
start a new process
Go provides an original type forkexec to produce a new process. You can share some messages with this new process, such as file descriptors or environment parameters.
Copy Code code as follows:
Execspec: = &syscall. procattr{
Env:os. Environ (),
Files: []uintptr{os. STDIN.FD (), OS. STDOUT.FD (), OS. STDERR.FD ()},
}
Fork, err: = Syscall. Forkexec (OS. ARGS[0], OS. Args, Execspec)
[...]
You will find that the process uses exactly the same parameters as the OS. Args started a new process.
send socket to child process and restore it
As you saw earlier, you can pass the file descriptor to the new process, which requires some Unix magic (everything is a file) and we can send the socket to the new process so that the new process can use it and receive and wait for the new connection.
But the fork-execed process needs to know that it has to get the socket from the file instead of creating a new one (some might already be in use because we haven't disconnected the existing listening). You can do it any way you want, most commonly through environment variables or command-line flags.
Copy Code code as follows:
Listenerfile, err: = Listener. File ()
If Err!= nil {
Log. Fatalln ("Fail to get Socket file descriptor:", err)
}
LISTENERFD: = LISTENERFILE.FD ()
Set a flag for the new process START process
Os. Setenv ("_graceful_restart", "true")
Execspec: = &syscall. procattr{
Env:os. Environ (),
Files: []uintptr{os. STDIN.FD (), OS. STDOUT.FD (), OS. STDERR.FD (), LISTENERFD},
}
Fork exec The new version of your server
Fork, err: = Syscall. Forkexec (OS. ARGS[0], OS. Args, Execspec)
Then at the beginning of the program:
Copy Code code as follows:
var listener *net. TcpListener
If OS. Getenv ("_graceful_restart") = = "true" {
The second argument should is the filename of the file descriptor
However, a socker is isn't a named file but we should fit the interface
of the OS. NewFile function.
FILE: = os. NewFile (3, "")
Listener, err: = Net. Filelistener (file)
If Err!= nil {
Handle
}
var bool ok
Listener, OK = listener. (*net. TcpListener)
If!ok {
Handle
}
} else {
Listener, err = Newlistenerwithport (12345)
}
The file description is not randomly selected as 3, because the UIntPtr slice has been sent fork, listening for the index 3. Note the implicit declaration problem.
last step, wait for the old service connection to stop
So far, we have passed it to another process that is running correctly, and the last operation for the old server is to wait for its connection to close. Sync is provided in the standard library. Waitgroup structure, using go to implement this function is simple.
Each time you receive a connection, add 1 to the Waitgroup, and then we reduce the counter by one when it completes:
Copy Code code as follows:
for {
Conn, err: = Listener. Accept ()
Wg. ADD (1)
Go func () {
HANDLE (conn)
Wg. Done ()
}()
}
As for waiting for the end of the connection, you only need the WG. Wait (), because there is no new connection, we are waiting for the WG. Done () has been invoked by all running handler.
Bonus: Do not wait indefinitely, given a limited amount of time
Copy Code code as follows:
Timeout: = time. Newtimer (time. Minute)
Wait: = Make (chan struct{})
Go func () {
Wg. Wait ()
Wait <-struct{}{}
}()
Select {
Case <-timeout. C:
Return Waittimeouterror
Case <-WAIT:
return Nil
}
Complete example
The code snippets in this article are all extracted from this complete example: https://github.com/Scalingo/go-graceful-restart-example
Conclusions
Socket delivery with Forkexec is indeed an effective way to do without interfering with the update process, and at the maximum time, the new connection waits a few milliseconds-for the service's startup and recovery socket, but this is a short time.