overseer执行流程和源码分析

2,576 阅读7分钟

前言

overseer作为一个用于热更新,平滑重启的监控服务,在实现上有很多我们需要了解和学习的地方。本文旨在从 overseer 的启动流程出发,了解overseer 是怎样完成服务监控,fork子进程,以及对于信号的处理。

由于本人水平有限,分析过程中可能有遗漏和错误,希望大家可以直接指出来,一起学习,一起进步。

overseer 执行流程

master

overseerRun方法中传入指定的配置数据,它的相关配置如下所示:

type Config struct {
   // Required 为 true 时,如果 overseer 运行返回错误就直接退出程序
   Required bool
   // Program 为主进程
   Program func(state State)
   // Program 零停机套接字监听地址
   Address string
   // Program 零停机套接字监听地址组
   Addresses []string
   // 指定触发重启的信号,默认为 SIGUSR2
   RestartSignal os.Signal
   // TerminateTimeout指定当程序中断自己多长时间后发出 SIGKILL 信号
   TerminateTimeout time.Duration
   // MinFetchInterval定义了Fetch之间的最小时间间隔,防止频繁地请求数据
   MinFetchInterval time.Duration
   // 在检索到二进制文件后,会在这里进行预升级的一些操作,
   // 比如检查用户自定义的检查,如果停止升级,那么就会返回一个错误
   PreUpgrade func(tempBinaryPath string) error
   // Debug表示开启debug日志
   Debug bool
   // NoWarn表示关闭警告日志
   NoWarn bool
   // NoRestart表示禁用服务重启功能,该选项本质上将RestartSignal转换为ShutdownSignal
   NoRestart bool
   // NoRestartAfterFetch表示当获取到更新的二进制文件后也不进行重启
   // 不过也可以通过RestartSignal设置重启信号
   NoRestartAfterFetch bool
   // 指定远程获取二进制文件的地址
   Fetcher fetcher.Interface
}

Run方法中,再调用runErr(&c)执行主进程,如果该方法返回了错误,那么就根据c.Required判断是否直接退出程序,否则的话就尝试直接运行被监听的服务。

func Run(c Config) {
   err := runErr(&c)
   if err != nil {
      // Fatalf 和 Printf 方法唯一的区别在于,Fatalf 里最后会执行 os.Exit(1) 来退出程序
      if c.Required {
         log.Fatalf("[overseer] %s", err)
      } else if c.Debug || !c.NoWarn {
         log.Printf("[overseer] disabled. run failed: %s", err)
      }
      // DisabledState 是一个占位符状态,在 overseer 无法启动但服务能手动启动时使用 
      c.Program(DisabledState)
      return
   }
   os.Exit(0)
}

我们进入overseer.gorunErr方法看看:

func runErr(c *Config) error {
   // 操作系统环境不支持 overseer 开启
   if !supported {
      return fmt.Errorf("os (%s) not supported", runtime.GOOS)
   }
   // 检查必要配置是否存在
   if err := validate(c); err != nil {
      return err
   }
   // sanityCheck:快速评估计算结果或分析结论是否合理,是否根本没有正确的可能
   if sanityCheck() {
      return nil
   }
   // 判断运行模式:slave/master
   if os.Getenv(envIsSlave) == "1" {
      currentProcess = &slave{Config: c}
   } else {
      currentProcess = &master{Config: c}
   }
   // 开启执行 overseer
   return currentProcess.run()
}

currentProcess是一个接口,overseer分别对于masterslave进行了实现,我们先来看看对于主进程的实现:

func (mp *master) run() error {
   if err := mp.checkBinary(); err != nil {
      return err
   }
   if mp.Config.Fetcher != nil {
      // 远程获取器的初始化 
      if err := mp.Config.Fetcher.Init(); err != nil {
         mp.Config.Fetcher = nil
      }
   }
   mp.setupSignalling()
   // 获得 Config.Addresses 的文件描述
   if err := mp.retreiveFileDescriptors(); err != nil {
      return err
   }
   // 如果要用 fetch 方式获取二进制文件
   if mp.Config.Fetcher != nil {
      mp.printCheckUpdate = true
      mp.fetch()
      go mp.fetchLoop()
   }
   // 子进程执行程序
   return mp.forkLoop()
}

func (mp *master) forkLoop() error {
	// fork 子进程执行需要监听的服务
	for {
		if err := mp.fork(); err != nil {
			return err
		}
	}
}

mp.setupSignalling方法的作用是创建信号的监听器,并开启协程,对接收到的信号调用对应的mp.handleSignal()

func (mp *master) setupSignalling() {
   mp.restarted = make(chan bool)
   mp.descriptorsReleased = make(chan bool)
   // 读取主进程的信号
   signals := make(chan os.Signal)
   signal.Notify(signals)
   go func() {
      // 等待操作系统发出的信号,对不同的信号进行不同的处理
      for s := range signals {
         mp.handleSignal(s)
      }
   }()
}

func (mp *master) handleSignal(s os.Signal) {
	if s == mp.RestartSignal {
		// 重启服务
		go mp.triggerRestart()
	} else if s.String() == "child exited" {
		// will occur on every restart, ignore it
	} else
	// 如果在服务重启的时候,主进程又收到了一个 SIGUSR1 信号,那么就释放当前文件描述符
	if mp.awaitingUSR1 && s == SIGUSR1 {
		mp.awaitingUSR1 = false
		mp.descriptorsReleased <- true
	} else if mp.slaveCmd != nil && mp.slaveCmd.Process != nil {
		// 如果从进程开启了,那么代理会通过所有的信号
		mp.sendSignal(s)
	} else if s == os.Interrupt {
		// CTRL+c 是可以强行停止程序的
		os.Exit(1)
	} else {
		mp.debugf("signal discarded (%s), no slave process", s)
	}
}

// 触发重启
func (mp *master) triggerRestart() {
	if mp.restarting {
		return //skip
	} else if mp.slaveCmd == nil || mp.restarting {
		return //skip
	}
	mp.restarting = true
	mp.awaitingUSR1 = true
	mp.signalledAt = time.Now()
    // 向 slave 发出一个 RestartSignal 信号
	mp.sendSignal(mp.Config.RestartSignal)
    // 超时处理
	select {
	case <-mp.restarted:
		mp.debugf("restart success")
	case <-time.After(mp.TerminateTimeout):
		mp.sendSignal(os.Kill)
	}
}

看到这里,可以分析一下 master 对于信号的处理逻辑:

Run方法启动之后,调用setupSignalling方法初始化信号处理逻辑,其中会开启一个协程调用handleSignal方法处理接收到的信号。

  1. 如果是RestartSignal信号,那么就开启一个协程调用triggerRestart方法,即执行重启逻辑。triggerRestart方法向slave发送一个RestartSignal信号。
  2. 如果是其它信号,并且overseerslave进程已在运行,那么就发送这个信号给slave进程。
  3. 如果slave进程没有开启,并且是Ctrl+c操作,那么就退出overseer服务。
  4. 如果slave进程没有打开,并且是非Crtl+c操作发出的信号,那么就打印日志,不做其它处理。

理清了master重启逻辑的一部分,接下来我们看看它是怎么fork出从进程的:

func (mp *master) fork() error {
   cmd := exec.Command(mp.binPath)
   // 将这个新进程标记为"活跃的salve进程",假定这个进程持有socket文件
   mp.slaveCmd = cmd
   mp.slaveID++
   // 给slave进程提供一些状态属性
   e := os.Environ()
   e = append(e, envBinID+"="+hex.EncodeToString(mp.binHash))
   e = append(e, envBinPath+"="+mp.binPath)
   e = append(e, envSlaveID+"="+strconv.Itoa(mp.slaveID))
   e = append(e, envIsSlave+"=1")
   e = append(e, envNumFDs+"="+strconv.Itoa(len(mp.slaveExtraFiles)))
   cmd.Env = e
    // 继承父进程(或者说是主进程)的相关信息
   cmd.Args = os.Args
   cmd.Stdin = os.Stdin
   cmd.Stdout = os.Stdout
   cmd.Stderr = os.Stderr
   //include socket files
   cmd.ExtraFiles = mp.slaveExtraFiles
   if err := cmd.Start(); err != nil {
      return fmt.Errorf("Failed to start slave process: %s", err)
   }
   // 是否开启定时重启功能
   if mp.restarting {
      mp.restartedAt = time.Now()
      mp.restarting = false
      mp.restarted <- true
   }
   // 将命令行执行等待通过 channel 执行
   cmdwait := make(chan error)
   go func() {
      cmdwait <- cmd.Wait()
   }()
   
   select {
   case err := <-cmdwait:
      code := 0
      if err != nil {
         code = 1
         if exiterr, ok := err.(*exec.ExitError); ok {
            if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
               code = status.ExitStatus()
            }
         }
      }
      
      // 如果禁用了重新启动,或者发生了意外的崩溃,则将此退出代理回到主进程
      if mp.NoRestart || !mp.restarting {
         os.Exit(code)
      }
   case <-mp.descriptorsReleased:
      // 如果文件描述符被释放,程序将获得socket的控制权,这样就可以安全
      // 启动程序的并行实例 
   }
   return nil
}

想到之前的forkLoop方法,发现它是一个阻塞等待fork程序返回然后执行循环的for

for {
		if err := mp.fork(); err != nil {
			return err
		}
	}

而在fork方法中,最后面是通过select阻塞等待通道数据:

select {
   case err := <-cmdwait:
      // ......
   case <-mp.descriptorsReleased:
   }

结合前面的master处理信号的逻辑中,有向slave发送RestartSignal信号的步骤。我们可以大胆猜测,当slave接收到RestartSignal信号后,就将服务进行关闭,然后向mp.descriptorsReleased传输数据,由此master.fork方法返回nil,然后forkLoop方法接着执行fork方法创建slave进程。

接下来,我们就来分析slave部分的处理逻辑,看看是否跟我们猜想的一样。

slave

分析slave的执行流程,自然要从它的创建开始。在fork方法里,我们看到master通过exec.Command方法创建了一个命令行执行句柄,并且给它设置了很多属性。在其中,需要注意的是这几处:

cmd := exec.Command(mp.binPath) // binPath 是你的可执行文件的绝对路径
e = append(e, envIsSlave+"=1") // 从 key 可以看出,它是用于判断 master/slave 的开启
cmd.ExtraFiles = mp.slaveExtraFiles // ExtraFiles指定新进程继承其其它打开的文件

判断master/slave模式的实现,在runErr方法中,就是从命令的环境变量中获取OVERSEER_IS_SLAVE,如果为1,表示是slave启动,否则是master启动。

cmd.ExtraFiles = mp.slaveExtraFiles,这行代码可以理解为fork出的slave进程继承了master进程开启的服务的文件(比如监听HTTP服务,那么master就会有一个socket文件,这时就会继承到slave。根据这个可以看出,master类似一个守护进程,slave才是真正运行服务的进程)。

再次执行runErr方法,然后后面就会调用实现了currentProcess接口的salve下的run方法:

func (sp *slave) run() error {
   sp.id = os.Getenv(envSlaveID)
   sp.state.Enabled = true
   sp.state.ID = os.Getenv(envBinID)
   sp.state.StartedAt = time.Now()
   sp.state.Address = sp.Config.Address
   sp.state.Addresses = sp.Config.Addresses
   sp.state.GracefulShutdown = make(chan bool, 1)
   sp.state.BinPath = os.Getenv(envBinPath)
   if err := sp.watchParent(); err != nil {
      return err
   }
   if err := sp.initFileDescriptors(); err != nil {
      return err
   }
   // 开始监听信号 
   sp.watchSignal()
   sp.Config.Program(sp.state)
   return nil
}

这里是slave处理信号的地方

func (sp *slave) watchSignal() {
   signals := make(chan os.Signal)
   signal.Notify(signals, sp.Config.RestartSignal)
   // 如果服务器产生了服务关闭的信号,那么执行下面协程
   go func() {
      <-signals
      signal.Stop(signals)
      //master wants to restart,
      close(sp.state.GracefulShutdown)
      // 释放 socket,并且通知 master 
      if len(sp.listeners) > 0 {
         for _, l := range sp.listeners {
            // 资源清理操作
            l.release(sp.Config.TerminateTimeout)
         }
         // 发出信号,通知 master 创建一个新的 slave 进程
         if !sp.NoRestart {
            sp.masterProc.Signal(SIGUSR1)
         }
      }
      // 超时处理 
      go func() {
         time.Sleep(sp.Config.TerminateTimeout)
         os.Exit(1)
      }()
   }()
}

由此我们可以看出,确实是slave接收到RestartSignal信号后,将服务进行关闭,释放相关资源,然后给master发送SIGUSR1信号。master接收到SIGUSR1信号后,在handleSignal中执行以下步骤:

if mp.awaitingUSR1 && s == SIGUSR1 {
   mp.debugf("signaled, sockets ready")
   mp.awaitingUSR1 = false
   mp.descriptorsReleased <- true
}

因为在masterfork方法里,一直在阻塞等到descriptorsReleased数据写入,所以当接收到该数据后就结束掉了fork方法,并返回nill。在forkLoop的死循环中,接收到了fork方法返回的nil,因为不是err错误,所以继续执行循环,master执行fork方法。