我正在参与「启航方案」

办理Go项目的生命周期

写在前面

最近和几个小伙伴们在写字节跳动第五届青训营后端组的大作业。

虽然昨天现已提交了项目,但有许多当地值得总结一下,比方这一篇,来看看咱们是怎样办理运用的生命周期的。

  • 项目地址
  • 项目文档

一、什么时分要注意办理运用的生命周期?

先来看一段代码:(假设无 err 值)

func main() {
    // 1、发动HTTP服务
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Hello, World!")
	})
	http.ListenAndServe(":8080", nil)
    // 2、发动GRPC服务
    server := grpc.NewServer()
    listener, _ := net.Listen("tcp", ":1234")
	server.Serve(listener)    
}

这一段代码,相信你一眼就能看出问题,由于在发动HTTP后,进程会堵塞住,下面发动GRPC服务的代码,压根就不会履行。

可是,假如想要同时发动GRPC服务呢?该怎样做呢?

自己没有时间,那么就请一个辅佐咯,让它来为咱们发动GRPC服务,而这个辅佐,便是go的携程。

  • 来看一段伪代码,也便是调整成这样,
func main() {
    // 1、将HTTP服务放在后台发动
    go start http
    // 2、将GRPC服务放在前台发动
    start grpc  
}

可是调整成这样之后,理想的状况便是,HTTP成功发动后、GRPC也要发动成功。HTTP意外退出后,GRPC也需求退出服务,他们俩需求共存亡

但若出现了 HTTP 意外退出、GRPC还未退出,那么就会浪费资源。还或许出现其他的问题。比方接口反常。这样会很危险。那咱们该利用什么办法,让同一服务内,发动多个线程。并且让他们共同存亡的呢?

了解了上面的问题,咱们再来从头描述总结一下出现的问题。

一个服务,或许会发动多个进程,比方说 HTTP API、GRPC API、服务的注册,这些模块都是独立的,都是需求在程序发动的时分进行发动。

并且假如需求封闭掉这个运用,还需求处理许多封闭的问题。比方说

  • HTTP、GRPC 的高雅封闭
  • 封闭数据库链接
  • 完成注册中心的注销操作

并且,发动的多个进程间,该怎样通讯呢? 某些服务意外退出了,按理来说要封闭整个运用,该怎样监听到呢?

二、咱们是怎样做的

(1)利用面向目标的办法来办理运用的生命周期

定义一个办理者目标,来办理咱们运用所需求发动的一切服务,比方这儿需求被咱们发动的服务有:HTTP、GRPC

这个办理者中心有两个办法:start、stop

// 用于办理服务的开启、和封闭
type manager struct {
	http *protocol.HttpService // HTTP生命周期的结构体[自定义]
	grpc *protocol.GRPCService // GRPC生命周期的结构体[自定义]
	l    logger.Logger		   // 日志目标
}

不用关怀这儿依靠的 http、grpc结构体是什么,咱们在后面的章节,会具体解说。只需求知道,咱们用manager这个结构体,用于办理http、grpc服务即可。

(2)处理start

start这个函数,中心只做了两件事,别离发动HTTP、GRPC服务。

func (m *manager) start() error {
	// 打印加载好的服务
	m.l.Infof("已加载的 [Internal] 服务: %s", ioc.ExistingInternalDependencies())
	m.l.Infof("已加载的 [GRPC] 服务: %s", ioc.ExistingGrpcDependencies())
	m.l.Infof("已加载的 [HTTP] 服务: %s", ioc.ExistingGinDependencies())
	// 假如不需求发动HTTP服务,需求才发动HTTP服务
	if m.http != nil {
		// 将HTTP放在后台跑
		go func() {
			// 注:这属于正常封闭:"http: Server closed"
			if err := m.http.Start(); err != nil && err.Error() != "http: Server closed" {
				return
			}
		}()
	}
    // 将GRPC放入前台发动
	m.grpc.Start()
	return nil
}

又由于开头说过了,发动这两任一服务,都会将进程堵塞住。

所以咱们找了一个辅佐(携程)来发动HTTP服务,然后将GRPC服务放在前台运转。

那为什么我要将GRPC服务放在前台运转呢?其实理论上放谁都行,但由于咱们的架构原因。咱们有的服务不需求发动HTTP服务,而每一个服务都会发动GRPC服务。所以,将GRPC放置在前台,会更合适。

至于里边怎样运用HTTP、GRPC的服务目标发动它们的服务。在这一节就不多赘述了。在之后的章节会有具体的介绍~

看完了统一办理发动的start办法,那咱们来看看怎样停止服务吧

(3)处理stop

1、什么时分才去Stop?

咱们开启了多个服务,并且有的仍是放在后台运转的。这就涉及到了多个携程的间通讯的问题了

用什么来通讯吶?我怎样知道HTTP服务挂没挂?是意外挂的仍是自动挂的?咱们怎样能够高雅的统一封闭一切服务呢?

其实这一切的问题,Go都为咱们想好了:那便是运用Channels。一个channel是一个通讯机制,它能够让一个携程通过它给另一个携程发送值信息。每个channel都有一个特别的类型,也便是channels可发送数据的类型。

咱们把一个go程当作一个人的化,那么main 办法发动的主go程便是你自己。在你的程序中运用到的其他go程,都是你的好辅佐,你的好朋友,它们有给你去处理耗时逻辑的、有给你去履行业务无关的切面逻辑的。并且是你的好辅佐,按理来说最好是由你自己去决议,要不要请一个好辅佐。

当你请来了一个好辅佐后,它们会在你的背后为你做你让他们做的工作。那么多个人之间的通讯,比较现代的办法,那能够是:打个电话?发个音讯?所以用到了一个沟通的信道:Channel

好了,当你了解了这些后,也便是接纳到一些电话后,咱们才需求去stop。咱们再回到Dousheng运用的情形:

2、Dousheng的运用场景

主携程是GRPC服务这个人,咱们请了一个辅佐,给我发动HTTP服务。这个时分,假如HTTP服务这个辅佐意外出事了。已然是帮我么你干事,那咱们必定得对别人担任是吧。可是咱们也不知道它出不出意外啊,怎样办呢?这时分你想了两个办法:

  1. 跟你的辅佐HTTP,发了如下音讯

如何优雅的管理Go Project的生命周期?我是这样做的

这就需求HTTP自己告知咱们,按理来说,应该是能够的。可是假如HTTP遇到了重大问题,根本来不及告知咱们呢?咱们又是一个担任的男人。为了避免这种状况产生,又请一个人,专门给咱们看HTTP有没有遇到重大问题。于是有了第二种办法:

  1. 在请一个辅佐signal.Notify,协助咱们监听HTTP或许会遇到的重大问题

如何优雅的管理Go Project的生命周期?我是这样做的

当咱们收到HTTP出事的信号后,那咱们就能够统一的去高雅封闭服务了。就这样,咱们做了一个担任的人~

相信你现已了解了中心的思想,咱们来看看,用代码该怎样实现

3、代码实现

  • 发动signal.Notify,用于监听系统信号

咱们现已剖析过了,咱们需求再请一个辅佐,来给咱们处理HTTP或许会遇到的重大事故:(syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGHUP, syscall.SIGINT)

// WaitSign 等候退出的信号,实现高雅退出
func (m *manager) waitSign() {
   // 用于接纳信号的信道
   ch := make(chan os.Signal, 1)
   // 接纳这几种信号
   signal.Notify(ch, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGHUP, syscall.SIGINT)
   // 需求在后台等候封闭
   go m.waitStop(ch)
}

signal.Notify收到上面所列举的信号后,那么就能够去做封闭的工作了,那怎样封闭呢?

  • 读取信号,履行高雅封闭逻辑
// WaitStop 中止信号,比方Terminal [封闭服务的办法]
func (m *manager) waitStop(ch <-chan os.Signal) {
   // 等候信号,若收到了,咱们进行服务统一的封闭
   for v := range ch {
      switch v {
      default:
         m.l.Infof("接受到信号:%s", v)
         // 高雅封闭HTTP服务
         if m.http != nil {
            if err := m.http.Stop(); err != nil {
               m.l.Errorf("高雅封闭 [HTTP] 服务犯错:%s", err.Error())
            }
         }
		// 高雅封闭GRPC服务
         if err := m.grpc.Stop(); err != nil {
            m.l.Errorf("高雅封闭 [GRPC] 服务犯错:%s", err.Error())
         }
      }
   }
}

这儿的逻辑比较简单,便是当接纳到信号的时分,对HTTP、GRPC做高雅封闭的逻辑。至于为什么要进行高雅封闭,而不是直接os.Exit()?咱们鄙人一节讲~

这儿值得一提的是,咱们从chanel里获取数据,由于咱们这儿只和单个携程间进行通讯了,运用的是 for range,并没有运用for select

好了,这样咱们运用的生命周期算是被咱们高雅的拿捏了。咱们一直在讲高雅封闭这个词,咱们来解说一下什么是高雅封闭?为什么需求高雅封闭?

三、什么是高雅封闭

已然HTTP服务和GRPC服务都需求高雅封闭,咱们这儿用HTTP服务来举例。

先来看这张图,假设有三个并行的恳求至咱们的HTTP服务。它们都希望得到服务器的response。HTTP服务器正常运转的状况下,多半是没问题的。

如何优雅的管理Go Project的生命周期?我是这样做的

恳求已宣布,若供给的HTTP服务忽然反常封闭了呢?咱们持续来把HTTP服务比作一个人。看看它是否高雅呢?

(1)没有高雅封闭

假如HTTP这个人不太高雅,是一个干事不怎样担任的渣男。当自己反常over了之后,也不处理完自己的工作,就让别人(request),找不到资源了。真的很不担任啊。

大致用一幅图表示:

如何优雅的管理Go Project的生命周期?我是这样做的

这个不高雅的HTTP服务,当有还未处理的恳求时,自己就反常封闭了,那么它根本不会理会原先的恳求是否完成了。它只管自己退出程序。

(2)有了高雅封闭

看完了那个渣男HTTP(没有高雅封闭),咱们几乎想骂它了。那咱们来看,当一个高雅的谦谦君子(有高雅封闭),又是怎样看待这个问题的。

如何优雅的管理Go Project的生命周期?我是这样做的

这是一个担任人的人,为什么说他担任人、说它高雅呢?由于当它自己接纳到反常封闭的信号后。它不会只顾自己封闭。它大概还会做两件事:

  1. 封闭建立连接的恳求通道,避免还会接纳到新的恳求
  2. 处理完以恳求的,可是还未呼应的恳求。确保资源得到呼应,哪怕是过错的response

正是由于它首要做了这两件事,咱们才说此时的HTTP服务,是一个高雅的谦谦君子。

而当有许多个恳求到时分,咱们怎样知道是否会不会忽然反常封闭呢?假如遇到了这种状况,咱们应该处理完未完成的呼应,拒绝新的恳求建立连接,由于咱们是一个高雅的人。