从零开始写一个web服务到底有多难?(五)——生命周期管理

Application Lifecycle

对于应用的服务的管理,一般会抽象一个application lifecycle的管理,方便服务的启动/停止等。

生命周期管理需要包含以下几个基本功能:

1.应用的信息。

2.服务的start/stop。

3.信号处理。

4.服务注册。

服务的start

找到我们之前启动服务的代码,这样一个简单的服务就启动起来了。

go 复制代码
func main() {
	server := server.NewHttpServer("demo")
	server.Route(http.MethodGet, "/hello", hello)
	server.Route(http.MethodGet, "/", notfound)
	server.Route(http.MethodGet, "/greet", greet)
	server.Start(":80")
}

但是我们实际业务往往不会这么简单,实际上我们经常会同时监听多个端口,比如我们常常会区分业务面和管理面。这种时候,由于原来的Start方法会导致阻塞,我们需要引入Goroutines来处理。

go 复制代码
func (s *httpServer) Start(address string) error {
	return http.ListenAndServe(address, nil)
}

Goroutines

定义我们搜索一下CSDN。

进行一些简单的修改,这样我们就可以同时监听两个端口,并且注册了不同的路由。需要注意的是,如果我们把监听的操作放在一个goroutine中,main函数会继续往下执行,如果main函数执行完并且退出,所有goroutine也会停止。因此我们可以在尾部加入一个空的select,这样main函数会一直pending在select的地方。这样我们的goroutine也可以正常监听。

go 复制代码
func main() {
	serverbiz := server.NewHttpServer("serverbiz")
	serverbiz.Route(http.MethodGet, "/hello", hello)

	servermgt := server.NewHttpServer("servermgt")
	servermgt.Route(http.MethodGet, "/greet", greet)

	go serverbiz.Start(":80")
	go servermgt.Start(":81")

	select {}
}
go 复制代码
type httpServer struct {
	Name string
	Mux  *http.ServeMux
}

func (s *httpServer) Route(method string, pattern string, handlerFunc func(ctx *Context)) {
	s.Mux.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
		if r.Method != method {
			w.Write([]byte("error"))
			return
		}
		ctx := NewContext(w, r)
		handlerFunc(ctx)

		go s.Tracker("method:" + method + ",pattern:" + pattern)
	})
}

func (s *httpServer) Start(address string) error {
	svr := &http.Server{Addr: address, Handler: s.Mux}
	return svr.ListenAndServe()
}

func NewHttpServer(name string) Server {
	return &httpServer{
		Name: name,
		Mux:  http.NewServeMux(),
	}
}

如果采用这样方式会有什么问题?

对于go的并发编程,有三条建议:

1.Keep yourself busy or do the work yourself(让自己忙碌起来或自己做工作)

2.Leave concurrency to the caller(将并发留给调用者)

3.Never start a goroutine without knowning when it will stop(永远不要在不知道何时停止的情况下启动 goroutine)

第一条是说我们应该在自己忙碌的时候,再另外启动一个goroutine执行其他的任务。不然就应该自己执行这个任务。

作者举了一个反面的例子。在这个例子中为了阻塞main goroutine不要退出,最后写了一个for的死循环,这样的话main这个goroutine就是啥事没干。作者在这个例子中给出的建议是,既然只有一个任务要做,main goroutine就可以自己去完成,没有必要另外启动一个goroutine任务,而让main goroutine等待。

完蛋!我们好像随手就写了一个大佬不推荐的反面例子。但是因为我们有2个任务,所以也不能简单的把任务放到main当中执行。

当然可能有人会说,那我们是不是可以把一个start放进goroutine,一个放在main当中执行?我的建议是不要这样做,因为监听端口的任务其实是并列的两个任务。如果我们将其中一个移入main当中执行,那么其实暗示了main当中的监听任务更重要,如果出现了异常,可能会导致整个服务的退出。而在go启动的另一个goroutine中的监听如果出现了异常,我们是无法感知的。

我们没有理由这样做。

第二条是说一个对象提供了启动goroutine的方法,那么就必须提供关闭goroutine的方法,一般原则是谁调用谁关闭。

看看我们的代码,goroutine启动了之后,我们外部就没有再去控制它的方式了。这里需要补上一个关闭的方法。goroutine之间可以通过channel通信,那么我们可以通过channel来控制goroutine。

第三条是一种非常常见的情况。

go 复制代码
func (s *httpServer) Route(method string, pattern string, handlerFunc func(ctx *Context)) {
	s.Mux.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
		if r.Method != method {
			w.Write([]byte("error"))
			return
		}
		ctx := NewContext(w, r)
		handlerFunc(ctx)

		go s.Tracker("method:" + method + ",pattern:" + pattern)
	})
}

func (s *httpServer) Tracker(data string) {
	time.Sleep(time.Millisecond)
	log.Println(data)
}

这边我们模拟了一个服务埋点记录日志的操作。相信很多同学开发的时候也会这样做,因为记录日志是一个旁路逻辑,并不在业务流程当中,因此我们另外开启一个goroutine去处理。但是这样做会有一个问题,无法保证创建的goroutine生命周期管理,我们不知道这个Tracker的执行情况。会导致最常见的问题就是服务关闭的时候,一些goroutine还没执行完成,这样就会导致一些事件的丢失。我们需要对正在执行的goroutine进行一个统一的管理。

Tracker优化

我们以包内部的Tracker为例。

首先定义Track的接口和数据结构。

Track需要实现三个方法,Event输入需要打印的日志。Run开始服务启动时,开始打印日志。Shutdown停止服务时,终止打印日志。包含两个channel,ch用来存放Event准备打印的日志,stop用来在shutdown时传输ch处理完毕的信号。

go 复制代码
type Track interface {
	Event(ctx context.Context, data string) error
	Run()
	Shutdown(ctx context.Context, name string)
}

type Tracker struct {
	ch   chan string
	stop chan struct{}
}

func NewTracker() Track {
	return &Tracker{
		ch:   make(chan string, 10),
		stop: make(chan struct{}, 1),
	}
}

Event将data传入ch。Run方法开始遍历ch处理数据。这里模拟每2秒处理一条信息。Shutdown的时候,首先关闭ch,不再允许进的Event进入。然后等待Run执行完毕触发stop。触发stop后我们shutdown方法会答应所有事件均已处理完毕。

当然有时候Run一直没有结束 ,我们也不能无限制的等待下去,提供一个ctx.Done()的超时机制。当等待时间超过我们的设定值时,也可强制Shutdown。

go 复制代码
func (t *Tracker) Event(ctx context.Context, data string) error {
	select {
	case t.ch <- data:
		return nil
	case <-ctx.Done():
		return ctx.Err()
	}
}

func (t *Tracker) Run() {
	for data := range t.ch {
		time.Sleep(2 * time.Second)
		fmt.Println(data)
	}
	t.stop <- struct{}{}
}

func (t *Tracker) Shutdown(ctx context.Context, name string) {
	close(t.ch)
	select {
	case <-t.stop:
		fmt.Println("All event run,Server:" + name + ",time:" + time.Now().Format("2006-01-02 15:04:05"))
	case <-ctx.Done():
		fmt.Println("Shutdown: time out,Server:" + name + ",time:" + time.Now().Format("2006-01-02 15:04:05"))
	}
}

然后看看在我们的服务包中如何使用

首先更新我们的Server定义,在接口中新增Shutdown方法,在数据结构中新增Track。

go 复制代码
type Server interface {
	Route(method string, pattern string, handlerFunc handlerFunc)
	Start(address string) error
	Shutdown()
}

type httpServer struct {
	Name string
	Mux  *http.ServeMux
	Tr   Track
}

在服务Start时启一个goroutine s.Tr.Run()。

go 复制代码
func (s *httpServer) Start(address string) error {
	svr := &http.Server{Addr: address, Handler: s.Mux}
	go s.Tr.Run()
	return svr.ListenAndServe()
}

在服务Shutdown时,调用Track的Shutdown方法关闭Tracker。此处设定超时时间3秒。如果超过3秒就不再等待强制关闭。最后调用log.Fatal关闭服务。

go 复制代码
func (s *httpServer) Shutdown() {
	defer log.Fatal("Shutdown the server,Server:" + s.Name)
	ctxR, cancel := context.WithDeadline(context.Background(), time.Now().Add(3*time.Second))
	go s.Tr.Shutdown(ctxR, s.Name)
	time.Sleep(3 * time.Second)
	defer cancel()
}

在Route时加入埋点。

go 复制代码
func (s *httpServer) Route(method string, pattern string, handler handlerFunc) {
	s.Mux.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
		if r.Method != method {
			w.Write([]byte("error"))
			return
		}
		ctx := NewContext(w, r)
		handler(ctx)
		s.Tr.Event(ctx.R.Context(), "method:"+method+",pattern:"+pattern+",time:"+time.Now().Format("2006-01-02 15:04:05"))
	})
}

在我们的业务代码中

启动两个服务,模拟在5秒后关闭其中一个服务。

go 复制代码
func main() {
	serverbiz := server.NewHttpServer("serverbiz")
	serverbiz.Route(http.MethodGet, "/hello", hello)

	servermgt := server.NewHttpServer("servermgt")
	servermgt.Route(http.MethodGet, "/greet", greet)

	go serverbiz.Start(":80")
	go servermgt.Start(":81")

	go ShutDown(serverbiz)

	select {}
}

func ShutDown(server server.Server) {
	time.Sleep(10 * time.Second)
	go server.Shutdown()
}

开始测试!

我们启动服务后,立刻使用postman调用greet接口多次,观察打印日志。我们设置的打印时间间隔是2秒,超时时间是3秒。也就是说在10秒时累计剩余的事件>=2个则会超时,小于2个则可以全部完成。

控制下调用接口的时间点。果然得到了2种预期的效果。

总结

这一章写了生命周期管理,和并发编程的一些建议。并且对包内部的Tracker给出了一个代码实例。其实业务代码,即main函数内还存在不足。没有将业务代码也优化成符合建议的代码。生命周期管理其实是一个嵌套的过程,外部一层一层的管理内部的实例。只要理解了一层的样例。继续扩展并非难事。

相关推荐
许野平1 小时前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
齐 飞3 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
童先生3 小时前
Go 项目中实现类似 Java Shiro 的权限控制中间件?
开发语言·go
LunarCod3 小时前
WorkFlow源码剖析——Communicator之TCPServer(中)
后端·workflow·c/c++·网络框架·源码剖析·高性能高并发
码农派大星。4 小时前
Spring Boot 配置文件
java·spring boot·后端
杜杜的man4 小时前
【go从零单排】go中的结构体struct和method
开发语言·后端·golang
幼儿园老大*4 小时前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go
llllinuuu4 小时前
Go语言结构体、方法与接口
开发语言·后端·golang
cookies_s_s4 小时前
Golang--协程和管道
开发语言·后端·golang
为什么这亚子4 小时前
九、Go语言快速入门之map
运维·开发语言·后端·算法·云原生·golang·云计算