ITPub博客

首页 > > > Golang中的自动伸缩和自防御设计-1

Golang中的自动伸缩和自防御设计-1

作者:zhmw 时间:2019-12-24 10:08:23 0 删除 编辑

aygun服务由许多活动组件构成,每个组件用于特定的任务。其中一个模块是用Golang编写的,负责对iOS崩溃报告进行处理。简而言之,它接受本机iOS崩溃报告,查找相关的dSYM文件,并生成开发者可以阅读并理解的堆栈跟踪信息。

 

dSYM-worker进程的操作非常简单,它通过Redis队列接收作业并执行,然后不断重复。这个dSYN-worker进程在一台机器上运行,作业处理的速率及负载相对合理,但仍有一些情况需要运维人员随时待命维护:

负载飙升。每隔一段时间,通常是在周末,用户更多地使用iOS设备,iOS崩溃报告的数量可能会远远超过预设的数量。这种场景下,需要手动启动更多dSYM-worker进程来处理任务并降低平均负载。每个新启动的进程作为消费者消费任务队列,它使用Golang     Redis队列库分布式并发地完成多个作业。

不响应(进程假死)。也就是说,进程仍然在运行,但是没有执行任何操作。大多数情况下,可能是由于死锁造成的。糟糕的是,从监控上看它仍在运行(但没有消费队列中的任务),因此只有当队列达到阈值时才会发出警报。这种情况下,则需要手动终止假死进程,并启动一个新的进程。(也可能需要启动更多的进程,为了加速消费队列中的任务)。

意外终止。进程崩溃并完全关闭。这种情况从未发生在dSYM-worker上,但在更新代码并发布时还是有可能发生。一旦发生,监控会发出进程已死的警报,需要再次手动启动该进程。

在深夜或凌晨处理这些问题非常痛苦,不论是对运维还是相关的负责人。以上这些人工操作理论上都应当是自动化的,因此有了以下的这些实践。

 

原理(Theory)

我们需要某种自动伸缩的能力来处理变化的、突增的负载,以某种方式检测并重启无响应的、假死的进程。是时候制定一个计划来解决问题了。

 

最初想法非常简单。我们使用Golang Redis队列库在单个进程中将多个Consumer关联到队列。通过添加多个Consumer,可以一次性完成更多的工作,这有助于实现自动伸缩。此外,如果每个Consumer都跟踪它们上一次完成工作的时间,那么就可以定期检查其是否已经长时间没有运行。这可以用来实现对无响应的Consumer的简单检测。于是开始着手研究这个计划的可行性。

 

 

没过多久就发现这种策略不会奏效——至少对Golang不起作用。每个Consumer都在Golang Redis队列库中的goroutine中进行管理。如果检测到一个反应迟钝的Consumer,那么就需要关闭它;但事实证明,这并不是简单地关闭一个goroutine。为了结束goroutine,通常要等到它完成工作,或者使用Channels或其他一些机制来打破循环。但如果Consumer陷入死循环,很难控制goroutine关闭。即使有,也意味着需要修改Golang Redis队列库。因此这个策略变得越来越复杂,于是不得不思考其他的解决方案。

 

 

紧接着的下一个想法是编写一个新的程序,生成并管理几个Worker进程。每个Worker进程仍然可以将单个Consumer关联到队列上,但是运行的进程越多,意味着一次完成的工作就越多。Golang具有启动和关闭子进程的能力,因此这对自动伸缩有很大帮助。不同的进程之间有不同的通信方式,因此Worker进程可以告诉Master进程它们上一次完成任务是什么时候。如果Master进程发现某个Worker已经很长时间没有反馈了,那么就会触发响应迟钝和死亡检测——稍后将对此进行更多介绍。 

 

 

另一种方法是建立hivemind。单个Worker进程可以同时处理作业,并且生成和管理其他Worker进程。如果检测到一个无响应或假死进程,则另一个正在运行的进程可以负责启动一个新进程。总的来说,它们可以确保始终有大量的进程在运行以处理队列中的任务。最终采用了第一种方法——Master-Worker方法。

 

 

自动伸缩(Autoscaling)

Master进程首先针对一个goroutine自旋,该goroutine定时检查Worker进程的数量。然后这个goroutine根据实际的进程数量来启动或停止Worker进程,确保负载均匀。所需Worker进程的计算非常简单,一般可以根据队列的当前长度,队列的生产/消费速率来计算。队列越长,或生产作业的速度越快,对应的Worker就越多。以下是对主要goroutine的简单介绍:

funcwatch() {

procs:= make(map[int]*Worker)

for {

// 检查每个Worker的健康状况

checkProcesses(&procs)

queueLength, rate:=queueStats()

// 计算所需的Worker进程数,根据实际情况增减

desiredWorkerCount:=calculateDesiredWorkerCount(queueLength, rate, len(procs))

if len(procs) !=desiredWorkerCount {

manageWorkers(&procs, desiredWorkerCount)

}

time.Sleep(30000*time.Millisecond)

}

}

 

Master进程需要跟踪它启动的子进程。这有助于自动伸缩,并定期检查每个子进程的健康状态。最简单的方法是使用一个Map,以整型键作为键值(keys),以及一个Worker结构(Worker struct)的实例作为值。这个Map的长度用于确定在添加Worker时使用的下一个键(keys),以及在删除Worker时删除哪个键。

 

funcmanageWorkers(procs*map[int]*Worker, desiredWorkerCountint) {

currentCount:= len(*procs)

ifcurrentCount < desiredWorkerCount {

// 添加Workers:

forcurrentCount < desiredWorkerCount {

StartWorker(procs, currentCount)

currentCount++

}

} elseifcurrentCount > desiredWorkerCount {

// 移除Workers:

forcurrentCount > desiredWorkerCount {

StopWorker(procs, currentCount-1)

currentCount--

}

}

}

 

Golang为进程管理提供了一个os/exec包。Master进程使用这个包生成新的Worker进程。Master进程和Worker进程被部署到相同的文件夹中,因此”./dSYM-worker"可以用来启动Worker。但是,Master进程作为守护进程运行时将不起作用。下面StartWorker函数中的第一行是如何获得正在运行的进程的工作目录。这样就可以创建Worker可执行文件的完整路径来可靠地运行它。一旦进程运行,将从Worker结构创建一个对象并将其存储在Map中。

 

funcStartWorker(procs*map[int]*Worker, indexint) {

dir, err:=filepath.Abs(filepath.Dir(os.Args[0]))

iferr!=nil {

// 错误及异常处理逻辑

}

cmd:=exec.Command(dir+"/dSYM-worker")

// 此处执行实际业务逻辑并将结果存入Worker对象

// 例如检索标准的输入和输出管道等,后面将对此进行详细解释

cmd.Start()

worker:=NewWorker(cmd, index)

(*procs)[index] = worker

}

 

确定作业负载所需的Worker数量,然后启动/停止Worker以满足,这是在本例中自动伸缩的实现。接下来将介绍如何阻止Worker进程假死。

来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69901620/viewspace-3130851/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2018-12-04

  • 博文量
    295
  • 访问量
    9532