前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从零开始写一个web服务到底有多难?(五)——生命周期管理

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

原创
作者头像
4cos90
发布2024-01-15 01:39:36
1400
发布2024-01-15 01:39:36
举报

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函数内还存在不足。没有将业务代码也优化成符合建议的代码。生命周期管理其实是一个嵌套的过程,外部一层一层的管理内部的实例。只要理解了一层的样例。继续扩展并非难事。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Application Lifecycle
  • 服务的start
  • Goroutines
  • 如果采用这样方式会有什么问题?
  • Tracker优化
  • 开始测试!
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档