优雅管理Go Project生命周期

目录
  • 写在前面
  • 一、什么时候要注意管理应用的生命周期?
  • 二、我们是如何做的
    • (1)利用面向对象的方式来管理应用的生命周期
    • (2)处理start
    • (3)处理stop
      • 1、什么时候才去Stop?
      • 2、Dousheng的应用场景
      • 3、代码实现
  • 三、什么是优雅关闭
    • (1)没有优雅关闭
    • (2)有了优雅关闭

写在前面

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

虽然昨天已经提交了项目,但有很多地方值得总结一下,比如这一篇,来看看我们是如何管理应用的生命周期的。

一、什么时候要注意管理应用的生命周期?

先来看一段代码:(假设无 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服务这个帮手意外出事了。既然是帮我么你做事,那我们肯定得对别人负责是吧。但是我们也不知道它出不出意外啊,怎么办呢?这时候你想了两个方法:

  • 跟你的帮手HTTP,发了如下消息

这就需要HTTP自己告诉我们,按理来说,应该是可以的。但是如果HTTP遇到了重大问题,根本来不及告诉我们呢?咱们又是一个负责的男人。为了避免这种情况发生,又请一个人,专门给我们看HTTP有没有遇到重大问题。于是有了第二种方式:

  • 在请一个帮手signal.Notify,帮助我们监听HTTP可能会遇到的重大问题

当我们收到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服务器正常运行的情况下,多半是没问题的。

请求已发出,若提供的HTTP服务突然异常关闭了呢?我们继续来把HTTP服务比作一个人。看看它是否优雅呢?

(1)没有优雅关闭

如果HTTP这个人不太优雅,是一个做事不怎么负责的渣男。当自己异常over了之后,也不解决完自己的事情,就让别人(request),找不到资源了。真的很不负责啊。

大致用一幅图表示:

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

(2)有了优雅关闭

看完了那个渣男HTTP(没有优雅关闭),我们简直想骂它了。那我们来看,当一个优雅的谦谦君子(有优雅关闭),又是如何看待这个问题的。

这是一个负责人的人,为什么说他负责人、说它优雅呢?因为当它自己接收到异常关闭的信号后。它不会只顾自己关闭。它大概还会做两件事:

  • 关闭建立连接的请求通道,防止还会接收到新的请求
  • 处理完以请求的,但是还未响应的请求。保证资源得到响应,哪怕是错误的response

正是因为它主要做了这两件事,我们才说此时的HTTP服务,是一个优雅的谦谦君子。

而当有很多个请求到时候,我们怎么知道是否会不会突然异常关闭呢?如果遇到了这种情况,我们应该处理完未完成的响应,拒绝新的请求建立连接,因为我们是一个优雅的人。

以上就是优雅管理Go Project生命周期的详细内容,更多关于Go Project生命周期的资料请关注我们其它相关文章!

(0)

相关推荐

  • 详解pycharm的newproject左侧没有出现项目选项的情况下创建Django项目的解决方法/社区版pycharm创建django项目的方法

    首先,我当时出现的问题是newproject创建的时候没有django的选项,查了半天发现我安装的pycharm是社区版本.所以需要用终端命令行的方式创建django项目. 首先,随便打开一个项目,然后在pycharm界面的左下角有Terminal终端的图标,点开. cd返回根目录 在终端输入你PycharmProjects的目录,由于我是mac 端,我输入的是:cd /Users/apple/PycharmProjects 进入目录后,输入:django-admin startproject

  • Mango Cache缓存管理库TinyLFU源码解析

    目录 介绍 整体架构 初始化流程 读流程 写流程 事件处理机制 主流程 write 清理工作 缓存管理 什么是LRU? 什么是SLRU? 什么是TinyLFU? mango Cache中的TinyLFU counter counter的初始化 counter的使用 lruCache slruCache filter TinyLFU的初始化 TinyLFU写入 TinyLFU访问 增加entry的访问次数 估计entry访问次数 总结 介绍 据官方所述,mango Cache是对Guava Cac

  • Go语言包和包管理详解

    目录 1 包简介 1.1 工作空间 1.2 源文件 1.3 包命名 1.4 main 包 2导包 2.1 两种方式 2.2 包的别名 2.3 简洁模式 2.4非导入模式(匿名导入) 2.5 导包的路径 2.6 远程导入 3 初始化 init 3.1 init总结 4 包管理 4.1 演变过程 4.2 Go Model优点 4.3 启用go module 4.4 GOPROXY 5 go mod详解 5.1 go mod命令 5.2 go.mod说明 5.2.1 依赖的版本 5.2.2 repla

  • Go中的应用配置管理详解

    目录 问题 解决 命令行参数 系统环境变量 打包进二进制文件 配置热更新 开源的fsnotify (1)安装 (2)案例 使用viper开源库实现热更新 问题 Go语言在编译时不会将配置文件这类第三方文件打包进二进制文件中 它既受当前路径的影响,也会因所填写的不同而改变,并非是绝对可靠的 解决 命令行参数 在Go语言中,可以直接通过flag标准库来实现该功能.实现逻辑为,如果存在命令行参数,则优先使用命令行参数,否则使用配置文件中的配置参数. 如下: var ( port string runM

  • goland 设置project gopath的操作

    用goland打开别人的go项目.可能碰到下面的问题goland cannot find package "server/common/config" in any of: 这是因为没有设置项目的gopath 设置方法 补充:Goland创建Go project 配置当前project GOPATH 1. new project 取消勾选index entire GOPATH GOPATH是项目部署和构建目录,默认是c:\user\xxx\go文件夹,go get命令下载的第三方包都会

  • Golang 内存管理简单技巧详解

    目录 引言 预先分配切片 结构中的顺序字段 使用 map[string]struct{} 而不是 map[string]bool 引言 除非您正在对服务进行原型设计,否则您可能会关心应用程序的内存使用情况.内存占用更小,基础设施成本降低,扩展变得更容易/延迟. 尽管 Go 以不消耗大量内存而闻名,但仍有一些方法可以进一步减少消耗.其中一些需要大量重构,但很多都很容易做到. 预先分配切片 数组是具有连续内存的相同类型的集合.数组类型定义指定长度和元素类型.数组的主要问题是它们的大小是固定的——它们

  • 优雅管理Go Project生命周期

    目录 写在前面 一.什么时候要注意管理应用的生命周期? 二.我们是如何做的 (1)利用面向对象的方式来管理应用的生命周期 (2)处理start (3)处理stop 1.什么时候才去Stop? 2.Dousheng的应用场景 3.代码实现 三.什么是优雅关闭 (1)没有优雅关闭 (2)有了优雅关闭 写在前面 最近和几个小伙伴们在写字节跳动第五届青训营后端组的大作业. 虽然昨天已经提交了项目,但有很多地方值得总结一下,比如这一篇,来看看我们是如何管理应用的生命周期的. 项目地址 项目文档 一.什么时

  • Microsoft .Net Remoting系列教程之二:Marshal、Disconnect与生命周期以及跟踪服务

    一.远程对象的激活 在Remoting中有三种激活方式,一般的实现是通过RemotingServices类的静态方法来完成.工作过程事实上是将该远程对象注册到通道中.由于Remoting没有提供与之对应的Unregister方法来注销远程对象,所以如果需要注册/注销指定对象,微软推荐使用Marshal(一般译为编组)和Disconnect配对使用.在<Net Remoting基础篇>中我已经谈到:Marshal()方法是将MarshalByRefObject类对象转化为ObjRef类对象,这个

  • 实例探究Android应用编写时Fragment的生命周期问题

    管理fragment的生命周期有些像管理activity的生命周期.Fragment可以生存在三种状态: Resumed: Fragment在一个运行中的activity中并且可见. Paused: 另一个activity处于最顶层,但是fragment所在的activity并没有被完全覆盖(顶层的activity是半透明的或不占据整个屏幕). Stoped: Fragment不可见.可能是它所在的activity处于stoped状态或是fragment被删除并添加到后退栈中了.此状态的frag

  • 谈谈我对Spring Bean 生命周期的理解

    前言 Spring的ioc容器功能非常强大,负责Spring的Bean的创建和管理等功能.而Spring 的bean是整个Spring应用中很重要的一部分,了解Spring Bean的生命周期对我们了解整个spring框架会有很大的帮助. BeanFactory和ApplicationContext是Spring两种很重要的容器,前者提供了最基本的依赖注入的支持,而后者在继承前者的基础进行了功能的拓展,例如增加了事件传播,资源访问和国际化的消息访问等功能.本文主要介绍了ApplicationCo

  • Asp.Net Core中服务的生命周期选项区别与用法详解

    前言 最近在做一个小的Demo中,在一个界面上两次调用视图组件,并且在视图组件中都调用了数据库查询,结果发现,一直报错,将两个视图组件的调用分离,单独进行,却又是正常的,寻找一番,发现是配置依赖注入服务时,对于服务的生命周期没有配置得当导致,特此做一次实验来认识三者之间(甚至是四者之间的用法及区别). 本文demo地址(具体见WebApi控制器中):https://gitee.com/530521314/koInstance.git (本地下载)  一.服务的生命周期 在Asp.Net Core

  • spring中bean的生命周期详解

    1.Spring IOC容器可以管理bean的生命周期,Spring允许在bean生命周期内特定的时间点执行指定的任务. 2.Spring IOC容器对bean的生命周期进行管理的过程: ① 通过构造器或工厂方法创建bean实例 ② 为bean的属性设置值和对其他bean的引用 ③ 调用bean的初始化方法 ④ bean可以使用了 ⑤ 当容器关闭时,调用bean的销毁方法 3.在配置bean时,通过init-method和destroy-method 属性为bean指定初始化和销毁方法 4.be

  • 通过实例解析spring对象生命周期

    1.生命周期-@Bean指定初始化和销毁方法 配置时指定初始化及销毁方法: Bean中提供对应的初始化及销毁方法: package com.atguigu.bean; import org.springframework.stereotype.Component; @Component public class Car { public Car(){ System.out.println("car constructor..."); } public void init(){ Syst

  • 详解Spring 中 Bean 的生命周期

    前言 这其实是一道面试题,是我在面试百度的时候被问到的,当时没有答出来(因为自己真的很菜),后来在网上寻找答案,看到也是一头雾水,直到看到了<Spring in action>这本书,书上有对Bean声明周期的大致解释,但是没有代码分析,所以就自己上网寻找资料,一定要把这个Bean生命周期弄明白! ​ 网上大部分都是验证的Bean 在面试问的生命周期,其实查阅JDK还有一个完整的Bean生命周期,这同时也验证了书是具有片面性的,最fresh 的资料还是查阅原始JDK!!! 一.Bean 的完整

  • Spring中Bean的作用域与生命周期详解

    目录 一.Bean的作用域 1.单实例Bean声明 2.多实例Bean声明 二.Bean的生命周期 1.bean的初始和销毁 2.bean的后置处理器 总结 一.Bean的作用域 首先我们来讲一下有关于bean的作用域, 一般情况下,我们书写在IOC容器中的配置信息,会在我们的IOC容器运行时被创建,这就导致我们通过IOC容器获取到bean对象的时候,往往都是获取到了单实例的Bean对象, 这样就意味着无论我们使用多少个getBean()方法,获取到的同一个JavaBean都是同一个对象,这就是

  • ASP.NET Core服务生命周期

    1.前言 在ConfigureServices方法中的容器注册每个应用程序的服务,Asp.Core都可以为每个应用程序提供三种服务生命周期: Transient(暂时):每次请求都会创建一个新的实例.这种生命周期最适合轻量级,无状态服务. Scoped(作用域):在同一个作用域内只初始化一个实例 ,可以理解为每一个请求只创建一个实例,同一个请求会在一个作用域内. Singleton(单例):整个应用程序生命周期以内只创建一个实例,后续每个请求都使用相同的实例.如果应用程序需要单例行为,建议让服务

随机推荐