Zero Downtime Reload Menggunakan Socketmaster

Zero downtime reload sudah menjadi keharusan pada kebanyakan sistem, terutama sistem yang diakses user sepanjang waktu. User menginginkan sistem yang memilik high availability. Jadi merupakan hal buruk apabila sistem memerlukan downtime untuk reload walaupun hanya dalam hitungan milisecond. Socketmaster hadir untuk membantu kita membuat sistem dengan zero downtime reload.

Apa itu socketmaster

Socketmaster adalah sebuah aplikasi yang memungkinkan kita untuk me-reload aplikasi tanpa downtime. Socketmaster melakukannya dengan menjalankan aplikasi kita sebagai child nya. Pada saat reload, socketmaster akan membuat proses baru untuk menjalankan aplikasi dan mengirim sinyal ke proses yang lama untuk berhenti. Sehingga kita tetap dapat menghandle request-request baru yang masuk sambil menunggu proses yang sedang berjalan selesai. Dengan cara ini semua request dapat di handle walaupun sedang reload. Zero downtime reload is achieved.

Untuk menginstal socketmaster, buka https://github.com/zimbatm/socketmaster kemudian download binary atau download source code dan compile sendiri.
Seperti yang tertulis pada Readme, ada beberapa hal yang perlu kita lakukan untuk mengintegrasikan service socketmaster dengan service kita.

Your server is responsible for:

  • opening the socket passed on fd 3
  • not crashing
  • gracefully shutdown on SIGTERM (close the listener, wait for the child connections to close)

Kita akan membahasnya satu per satu.

Untuk bereksperimen dengan socketmaster, saya membuat web server sederhana dengan Go. Kemudian mengirim request secara terus menerus ke server tersebut dan mereload service nya. Yang diharapkan adalah semua request dapat dihandle walaupun server sedang reload. Perlu diketahui bahwa walaupun socketmaster ditulis dengan bahasa pemrograman Go, dia juga dapat digunakan dengan bahasa pemrograman lainnya.

My web server in Go

Code di bawah ini yang digunakan untuk membuat web server. Kemudian kita akan mengintegrasikannya dengan socketmaster untuk membuat zero downtime reload.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func main() {
    // create handler
    r := mux.NewRouter()
    r.HandleFunc("/{path}", myHandler)

    // create listener
    f := os.NewFile(uintptr(3), "listener")
    listener, err := net.FileListener(f)
    if err != nil {
        log.Fatalln("error create listener", err)
    }

    srv := http.Server{
        Handler: r,
    }
    go func() {
        fmt.Println("listening...")
        srv.Serve(listener)
    }()

    // accept sigterm 
    term := make(chan os.Signal)
    signal.Notify(term, syscall.SIGTERM)

    // shutdown when sigterm received
    fmt.Println("got signal", <-term)
    fmt.Println("shutting down..")
    srv.Shutdown(context.Background())
}

Pada line 7 sampai 11, dapat dilihat kalau web server akan listen fd 3, seperti yang diterangkan pada requirement point 1 sebelumnya, yaitu opening the socket passed on fd 3.

Untuk requirement berikutnya yaitu not crashing, by default Go HTTP server sudah memiliki panic recovery. Tapi jika ingin menggunakan panic recovery kita sendiri, bisa cek post saya sebelumnya di sini.

Kemudian untuk requirement terakhir yaitu gracefully shutdown on SIGTERM (close the listener, wait for the child connections to close), lihat pada line 22 - 23 pada code diatas. Server me-listen sinyal SIGTERM dan menampungnya ke dalam sebuah channel. Pada line 26, server menunggu sampai SIGTERM diterima. Code di bawahnya tidak akan dieksekusi sebelum sinyal tersebut diterima. Ketika SIGTERM diterima, eksekusi code akan dilanjutkan sampai line 28 di mana server akan di shutdown secara graceful, yaitu menunggu sampai semua proses selesai, baru server benar-benar mati. Ketika service yang lama dishutdown, socketmaster sudah membuat proses baru untuk menghandle request-request baru yang masuk.

Saya menggunakan handler berikut untuk testing. Dia akan sleep selama 3 detik, lalu memberikan response berupa pid dari proses saat ini.

1
2
3
4
5
6
func myHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Println("handle", r.URL.RequestURI())
    time.Sleep(3 * time.Second)
    pid := os.Getpid()
    w.Write([]byte(fmt.Sprintf(`{"status": "success", "pid": %d}`, pid)))
}

Jalankan socketmaster & test it

Build go app kemudian gunakan command berikut untuk menjalankan web server dengan socketmaster.

socketmaster -listen tcp://:7008 -command=./mygoapp

-listen tcp://:7008 socket master akan listen ke tcp port 7008.
-command=./mygoapp ini adalah command untuk menjalankan service. Socketmaster akan menjalankan command di sini ketika reload, kemudian mengirim sinyal ke proses yang lama untuk berhenti.

Ketika dijalankan, akan muncul log seperti ini

socketmaster[2969] 2021/02/10 15:39:25 Listening on tcp://:7008
socketmaster[2969] 2021/02/10 15:39:25 Starting ./mygoapp [./mygoapp]
socketmaster[2969] 2021/02/10 15:39:26 [2970] listening...

Untuk reload, kita mengirim sinya HUP ke pid dari socketmaster, dalam contoh di sini pid nya adalah 2969.

kill -HUP <pid>

Untuk mengetest nya, saya mengirim request secara terus menerus ke server kemudian reload server tersebut. Berikut hasilnya.

1
2
3
4
5
6
7
8
203504 :: ~ » curl localhost:7008/abc
{"status": "success", "pid": 332}
203504 :: ~ » curl localhost:7008/abc
{"status": "success", "pid": 332}
203504 :: ~ » curl localhost:7008/abc
{"status": "success", "pid": 368}
203504 :: ~ » curl localhost:7008/abc
{"status": "success", "pid": 368}

Server direload ketika menghandle request pada line 3.
Yang perlu dipastikan adalah semua request mendapatkan response dari server. Bisa dilihat bahwa pid pada response di line 6 berubah karena di handle oleh service pada process yang baru di buat oleh socketmaster.

Jalankan dengan systemd

Biasanya, seperti pada aplikasi server lainnya, socketmaster dijalankan sebagai daemon. Yang berarti proses nya jalan di background. Di Linux, ada systemd untuk menjalankan aplikasi di background. Berikut ini adalah contoh file configurasi untuk menjalankan socketmaster sebagai daemon. File ini disimpan di /etc/systemd/system/myservice.service.

[Unit]
Description=<description about this service>

[Service]
ExecStart=socketmaster -listen tcp://:7008 -command=./mygoapp
ExecReload=/bin/kill -HUP $MAINPID

[Install]
WantedBy=multi-user.target

Untuk menjalankannya

systemctl start myservice  

Untuk reload

systemctl reload myservice

Untuk mendapatkan status

systemctl status myservice
Akan muncul status seperti ini
root@d6bc11757efd:/# systemctl status myservice
* myservice.service - <description about this service>
     Loaded: loaded (/etc/systemd/system/myservice.service; disabled; vendor preset: enabled)
     Active: active (running) since Wed 2021-02-10 04:21:27 UTC; 1min 7s ago
    Process: 378 ExecReload=/bin/kill -HUP $MAINPID (code=exited, status=0/SUCCESS)
   Main PID: 352 (socketmaster)
      Tasks: 14 (limit: 2227)
     Memory: 2.7M
     CGroup: /docker/d6bc11757efdb63c8a359353764b29e72e752d39b2c6a8a2ca7514d56b6bcb86/system.slice/myservice.service
             |-352 /usr/local/bin/socketmaster -listen tcp://:7008 -command=./mygoapp
             `-379 ./mygoapp

Kesimpulan

Dengan socketmaster kita dapat mereload aplikasi kita tanpa downtime. Ada beberapa hal yang perlu dilakukan untuk mengintegrasikan aplikasi kita dengan socketmaster. Tapi hal-hal tersebut simple dan tidak memerlukan banyak perubahan code. Socketmaster dapat digunakan di production. If you have any questions, leave a comment below.


See also