我正在参与「启航方案」
办理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服务这个辅佐意外出事了。已然是帮我么你干事,那咱们必定得对别人担任是吧。可是咱们也不知道它出不出意外啊,怎样办呢?这时分你想了两个办法:
- 跟你的辅佐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服务,是一个高雅的谦谦君子。
而当有许多个恳求到时分,咱们怎样知道是否会不会忽然反常封闭呢?假如遇到了这种状况,咱们应该处理完未完成的呼应,拒绝新的恳求建立连接,由于咱们是一个高雅的人。