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