真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

golang服務(wù)平滑重啟小結(jié)

golang 服務(wù)平滑重啟小結(jié)


  • 背景
  • golang 程序平滑重啟框架
  • supervisor 出現(xiàn) defunct 原因
  • 使用 master/worker 模式

背景

在業(yè)務(wù)快速增長中,前期只是驗(yàn)證模式是否可行,初期忽略程序發(fā)布重啟帶來的暫短停機(jī)影響。當(dāng)模式實(shí)驗(yàn)成熟之后會(huì)逐漸放量,此時(shí)我們的發(fā)布停機(jī)帶來的影響就會(huì)大很多。我們整個(gè)服務(wù)都是基于云,請求流量從 四層->七層->機(jī)器。

目前創(chuàng)新互聯(lián)公司已為1000多家的企業(yè)提供了網(wǎng)站建設(shè)、域名、網(wǎng)站空間成都網(wǎng)站托管、企業(yè)網(wǎng)站設(shè)計(jì)、彝良網(wǎng)站維護(hù)等服務(wù),公司將堅(jiān)持客戶導(dǎo)向、應(yīng)用為本的策略,正道將秉承"和諧、參與、激情"的文化,與客戶和合作伙伴齊心協(xié)力一起成長,共同發(fā)展。

要想實(shí)現(xiàn)平滑重啟大致有三種方案,一種是在流量調(diào)度的入口處理,一般的做法是 ApiGateway + CD,發(fā)布的時(shí)候自動(dòng)摘除機(jī)器,等待程序處理完現(xiàn)有請求再做發(fā)布處理,這樣的好處就是程序不需要關(guān)心如何做平滑重啟。

第二種就是程序自己完成平滑重啟,保證在重啟的時(shí)候 listen socket FD(文件描述符) 依然可以接受請求進(jìn)來,只不過切換新老進(jìn)程,但是這個(gè)方案需要程序自己去完成,有些技術(shù)??赡軐?shí)現(xiàn)起來不是很簡單,有些語言無法控制到操作系統(tǒng)級(jí)別,實(shí)現(xiàn)起來會(huì)很麻煩。

第三種方案就是完全 docker,所有的東西交給 k8s統(tǒng)一管理,我們正在小規(guī)模接入中。

golang 程序平滑重啟框架

java、net等基于虛擬機(jī)的語言不同,golang天然支持系統(tǒng)級(jí)別的調(diào)用,平滑重啟處理起來很容易。從原理上講,基于 linux fork子進(jìn)程的方式,啟動(dòng)新的代碼,再切換 listen socket FD,原理固然不難,但是完全自己實(shí)現(xiàn)還是會(huì)有很多細(xì)節(jié)問題的。好在有比較成熟的開源庫幫我們實(shí)現(xiàn)了。

graceful https://github.com/tylerb/graceful
endless https://github.com/fvbock/endless

上面兩個(gè)是 github排名靠前的 web host框架,都是支持平滑重啟的,只不過接受的進(jìn)程信號(hào)有點(diǎn)區(qū)別 endless接受 signal HUP,graceful接受 signal USR2。graceful比較純粹的 web host,endless支持一些 routing的能力。

我們看下 endless處理信號(hào)。(如果對 srv.fork()內(nèi)部感興趣可以品讀品讀。)

func (srv *endlessServer) handleSignals() {
    var sig os.Signal

    signal.Notify(
        srv.sigChan,
        hookableSignals...,
    )

    pid := syscall.Getpid()
    for {
        sig = <-srv.sigChan
        srv.signalHooks(PRE_SIGNAL, sig)
        switch sig {
        case syscall.SIGHUP:
            log.Println(pid, "Received SIGHUP. forking.")
            err := srv.fork()
            if err != nil {
                log.Println("Fork err:", err)
            }
        case syscall.SIGUSR1:
            log.Println(pid, "Received SIGUSR1.")
        case syscall.SIGUSR2:
            log.Println(pid, "Received SIGUSR2.")
            srv.hammerTime(0 * time.Second)
        case syscall.SIGINT:
            log.Println(pid, "Received SIGINT.")
            srv.shutdown()
        case syscall.SIGTERM:
            log.Println(pid, "Received SIGTERM.")
            srv.shutdown()
        case syscall.SIGTSTP:
            log.Println(pid, "Received SIGTSTP.")
        default:
            log.Printf("Received %v: nothing i care about...\n", sig)
        }
        srv.signalHooks(POST_SIGNAL, sig)
    }
}

supervisor 出現(xiàn) defunct 原因

使用 supervisor管理的進(jìn)程,中間需要加一層代理,原因就是 supervisor可以管理自己啟動(dòng)的進(jìn)程,意思就是 supervisor可以拿到自己啟動(dòng)的進(jìn)程id(PID),可以檢測進(jìn)程是否還存活,carsh后做自動(dòng)拉起,退出時(shí)能接收到進(jìn)程退出信號(hào)。

但是如果我們用了平滑重啟框架,原來被 supervisor啟動(dòng)的進(jìn)程發(fā)布重啟 fork子進(jìn)程之后正常退出,當(dāng)再次發(fā)布重啟 fork子進(jìn)程后就會(huì)變成無主進(jìn)程就會(huì)出現(xiàn) defunct(僵尸進(jìn)程)的問題,原因就是此子進(jìn)程無法完成退出,沒有主進(jìn)程來接受它退出的信號(hào),退出進(jìn)程本身的少量數(shù)據(jù)結(jié)構(gòu)無法銷毀。

使用 master/worker 模式

supervisor本身提供了 pidproxy程序,我們在配置 supervisor command時(shí)候使用 pidproxy來做一層代理。由于進(jìn)程的id會(huì)隨著不停的發(fā)布 fork子進(jìn)程而變化,所以需要將程序的每次啟動(dòng) PID保存在一個(gè)文件中,一般大型分布式軟件都需要這樣的一個(gè)文件,MySQL、zookeeper等,目的就是為了拿到目標(biāo)進(jìn)程id。

這其實(shí)是一種 master/worker模式,master進(jìn)程交給 supervisor管理,supervisor啟動(dòng) master進(jìn)程,也就是 pidproxy程序,再由 pidproxy來啟動(dòng)我們目標(biāo)程序,隨便我們目標(biāo)程序 fork多少次子進(jìn)程都不會(huì)影響 pidproxy master進(jìn)程。

pidproxy依賴 PID文件,我們需要保證程序每次啟動(dòng)的時(shí)候都要寫入當(dāng)前進(jìn)程 id進(jìn) PID文件,這樣 pidproxy才能工作。
supervisor默認(rèn)的 pidproxy文件是不能直接使用的,我們需要適當(dāng)?shù)男薷摹?/p>

https://github.com/Supervisor/supervisor/blob/master/supervisor/pidproxy.py

#!/usr/bin/env python

""" An executable which proxies for a subprocess; upon a signal, it sends that
signal to the process identified by a pidfile. """

import os
import sys
import signal
import time

class PidProxy:
    pid = None
    def __init__(self, args):
        self.setsignals()
        try:
            self.pidfile, cmdargs = args[1], args[2:]
            self.command = os.path.abspath(cmdargs[0])
            self.cmdargs = cmdargs
        except (ValueError, IndexError):
            self.usage()
            sys.exit(1)

    def go(self):
        self.pid = os.spawnv(os.P_NOWAIT, self.command, self.cmdargs)
        while 1:
            time.sleep(5)
            try:
                pid = os.waitpid(-1, os.WNOHANG)[0]
            except OSError:
                pid = None
            if pid:
                break

    def usage(self):
        print("pidproxy.py   [ ...]")

    def setsignals(self):
        signal.signal(signal.SIGTERM, self.passtochild)
        signal.signal(signal.SIGHUP, self.passtochild)
        signal.signal(signal.SIGINT, self.passtochild)
        signal.signal(signal.SIGUSR1, self.passtochild)
        signal.signal(signal.SIGUSR2, self.passtochild)
        signal.signal(signal.SIGQUIT, self.passtochild)
        signal.signal(signal.SIGCHLD, self.reap)

    def reap(self, sig, frame):
        # do nothing, we reap our child synchronously
        pass

    def passtochild(self, sig, frame):
        try:
            with open(self.pidfile, 'r') as f:
                pid = int(f.read().strip())
        except:
            print("Can't read child pidfile %s!" % self.pidfile)
            return
        os.kill(pid, sig)
        if sig in [signal.SIGTERM, signal.SIGINT, signal.SIGQUIT]:
            sys.exit(0)

def main():
    pp = PidProxy(sys.argv)
    pp.go()

if __name__ == '__main__':
    main()

我們重點(diǎn)看下這個(gè)方法:

def go(self):
        self.pid = os.spawnv(os.P_NOWAIT, self.command, self.cmdargs)
        while 1:
            time.sleep(5)
            try:
                pid = os.waitpid(-1, os.WNOHANG)[0]
            except OSError:
                pid = None
            if pid:
                break

go 方法是守護(hù)方法,會(huì)拿到啟動(dòng)進(jìn)程的id,然后做 waitpid,但是當(dāng)我們 fork進(jìn)程的時(shí)候主進(jìn)程會(huì)退出,os.waitpid會(huì)收到退出信號(hào),然后就退出了,但是這是個(gè)正常的切換邏輯。

可以兩個(gè)辦法解決,第一個(gè)就是讓 go方法純粹是個(gè)守護(hù)進(jìn)程,去掉退出邏輯,在信號(hào)處理方法中處理:

    def passtochild(self, sig, frame):
        pid = self.getPid()
        os.kill(pid, sig)
        time.sleep(5)
        try:
            pid = os.waitpid(self.pid, os.WNOHANG)[0]
        except OSError:
            print("wait pid null pid %s", self.pid)
        print("pid shutdown.%s", pid)
        self.pid = self.getPid()

        if self.pid == 0:
            sys.exit(0)

        if sig in [signal.SIGTERM, signal.SIGINT, signal.SIGQUIT]:
            print("exit:%s", sig)
            sys.exit(0)

還有一個(gè)方法就是修改原有g(shù)o方法:

    def go(self):
        self.pid = os.spawnv(os.P_NOWAIT, self.command, self.cmdargs)
        while 1:
            time.sleep(5)
            try:
                pid = os.waitpid(-1, os.WNOHANG)[0]
            except OSError:
                pid = None
            try:
                with open(self.pidfile, 'r') as f:
                    pid = int(f.read().strip())
            except:
                print("Can't read child pidfile %s!" % self.pidfile)
            try:
                os.kill(pid, 0)
            except OSError:
                sys.exit(0)

當(dāng)然還可以用其他方法或者思路,這里只是拋出問題。如果你想知道真正問題在哪里,可以直接在本地 debug pidproxy腳本文件,還是比較有意思的,知道真正問題在哪里如何修改,就完全由你來發(fā)揮了。

作者:王清培 (趣頭條 Tech Leader)


網(wǎng)頁名稱:golang服務(wù)平滑重啟小結(jié)
轉(zhuǎn)載來源:http://weahome.cn/article/ghhpgj.html

其他資訊

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部