Golang中select用法导致CPU占用100%的问题分析

上一节(golang中有关select的几个知识点)中介绍了一些对于select{}的一些用法,今天介绍一下有关select在for语句中由于使用不当引起的CPU占用100% 的案例。

先看代码

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan int, 10)

	// 读取chan
	go func() {
		for {
			select {
			case i := <-ch:
				// 只读取15次chan
				fmt.Println(i)
			default:
				// 读取15次chan以后的操作一直在这个空语句无任何IO操作的default条件里死循环,无法出让P,以保证一个GPM关系。
				// 而如果无default条件的话,则系统当读取完15次chan后,当前goroutine会发会chan IO阻塞, Go调度器根据GPM的调度关系,会将当前执行关系中的G切换出去,再从LRQ队列中取一个新的G,重新组成一个GPM继续执行,以实现合理利用计算机资源,提高GO的高并发性能
			}
		}
	}()

	// 写入10个值到chan
	for i := 0; i < 15; i++ {
		ch <- i
	}

	// 模拟程序效果使用
	time.Sleep(time.Minute)
}

实现功能

通过操作chan来实现消费者逻辑。

问题现象

但在运行的时候,即发现CPU占用率100%,下面我们分析一下什么原因引起的。

问题分析

程序运行时,先使用go关键字创建一个 goroutine,里面是一个for循环语句。for语句里面通过select{}来监听是否有chan的IO操作,当ch中有可以读取的数据时,则将值打印出来。没有的话则执行default语句,而这里default语句为空,所以继续下一次for语句,for{}是一个死循环语句。

当读取15次ch后,由于ch会永远处于阻塞状态,所以会一直执行default条件,然后再执行for循环。此时这段逻辑基本演变成了一个空的 for{} 语句,所以会导致CPU占用100%。

解决办法

既然我们知道这个goroutine会一直在死循环的执行空的语句,导致一直占用着cpu不放,我们只需要让当前goroutine出让CPU控制权给其它goroutine即可。根据GPM调度原理,这里我们只需要让操作chan的IO语句进行阻塞即可,这样Go Seched 就会发现goroutine发生阻塞,于是将当前G切换出去,重新调度一个新的G过来,开始一个新的GPM关系继续运行。
这里最简单的实现方法就注释掉 default 语句即可。将当前goroutine死循环状态变成阻塞状态。

注意这里的阻塞和执行空的 default是两码事,阻塞是执行到这里主停止不再继续执行了,而这里有空的default, 表示的是无执行代码,但本次循环是可以结束的,继续下一次循环,就是一个死循环而已。

对于Go中的阻塞需要了解一下有哪些场景会发生,可以参考上面提到的GPM文章。
常见的阻塞一般发生在像网络请求、系统调用进行磁盘IO操作、执行Sleep函数等,而针对每一种阻塞的处理方式也不一样。
如果是网络导致的阻塞的话,则直接将G切换到网络轮询器NetPoller继续执行, 为PM重新调度过来一个新的G继续执行,等网络请求完成后,再将G追加到LRQ的队列尾部等待再次执行。
而对于系统调用产生的IO阻塞这种情况,则Go Seched则直接将M和G同时切换出去,并保持MG继续执行IO操作,出让出来的P再分配一个新的MG继续执行。等原来IO操作完成后,将原来的G放入LRQ队列,等待P的再次执行,而原来的M放在一旁等待被重复调用使用。可以看到原来的G和M关系到此结束了,下次与G执行的M不一定是原来的M了, 所以G再次执行就需要知道运行状态,堆栈之类的信息,而这些正是存储在G的结构体中了。

对于GPM关系中的几个要点

Go 调度器模型我们通常叫做G-P-M 模型,他包括 4 个重要结构,分别是G、P、M、Sched

  • G 并非执行体,每个 G 需要绑定到 P 才能被调度执行。
  • P 的数量决定了系统内最大可并行的 G 的数量(前提:物理 CPU 核数  >= P 的数量)。
  • M 并不保留 G 状态,这是 G 可以跨 M 调度的基础。M的数量是由Go Runtime来调整,目前默认最大限制为10000个。
  • Sched:Go 调度器,它维护有存储 M 和 G 的队列以及调度器的一些状态信息等。
  • Go 调度器中有两个不同的运行队列:全局运行队列(GRQ)和本地运行队列(LRQ)。
  • 多个 Goroutine 通过用户级别(用户态)的上下文切换来共享内核线程 M 的计算资源(内核态),但对于操作系统来说并没有线程上下文切换产生的性能损耗。

调度器循环的机制大致是从各种队列、P 的本地队列中获取 G,切换到 G 的执行栈上并执行 G 的函数,调用 Goexit 做清理工作并回到 M,如此反复。

推荐 https://mp.weixin.qq.com/s/ihJFa5Wir4ohhZUXVSBvMQ

基于 GitHub Actions 实现 Golang 项目的自动构建部署

前几天 GitHub官网宣布 GitHub 的所有核心功能对所有人都免费开放,不得不说自从微软收购了GitHub后,确实带来了一些很大的改变。

以前有些项目考虑到协作关系的原因,虽然放在github上面,但对于一些项目的持续构建和部署一般是通过自行抢建Travis CI、jenkins等系统来实现。虽然去年推出了Actions用来代替它类三方系统,但感觉着还是不方便,必须有些核心功能无法使用,此消息的发布很有可能将这种格局打破。

本篇教程将介绍使用github的系列产品来实现项目的发布,构建,测试和部署,当然这仅仅是一个非常小的示例,有些地方后期可能会有更好的瞿恩方案。

GitHub Actions 是一款持续集成工具,包括clone代码,代码构建,程序测试和项目发布等一系列操作。更多内容参考:http://www.ruanyifeng.com/blog/2019/09/getting-started-with-github-actions.html

如果你对CI/CD不了解的话,建议先找些文档看看。

项目源文件见 https://github.com/cfanbo/github-actions-demo

GitHub Actions 术语

GitHub Actions 相关的术语。

(1)workflow (工作流程):持续集成一次运行的过程,就是一个 workflow。

(2)job (任务):一个 workflow 由一个或多个 jobs 构成,含义是一次持续集成的运行,可以完成多个任务。

(3)step(步骤):每个 job 由多个 step 构成,一步步完成。

(4)action (动作):每个 step 可以依次执行一个或多个命令(action)。

一、创建workflow 文件

在项目里创建一个workflow文件,文件格式为yaml类型。文件名可以随意起,文件后缀可以为yml 或 .yaml, 这里我们创建文件 .github/workflows/deploy.yaml,注意这里的路径。

如果你的仓库中有项目文件的话,当你点击“Actions”时,系统会自动根据你的开发语言推荐一个常用的actions,在页面的右侧也会推荐一些相应的actions.

二、在部署服务器上生成部署用户密钥

部署时需要用到用户的私钥,所以先登录到部署服务器获取私钥,这里为了方便,单独创建了一对公钥和公钥,

$ cd ~/.ssh
$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa): /root/.ssh/id_rsa_actions
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /root/.ssh/id_rsa_actions.****
Your public key has been saved in /root/.ssh/id_rsa_actions.pub.
The key fingerprint is:
SHA256:QPg26V17/pnRdHZrDqysG6jFgdTEysbz+aCuXusyO/Q root@iZbp1acq02ar70gdvppgh6Z
The key’s randomart image is:
+—[RSA 2048]—-+
| .o. |
| ..o. |****
| oooo |
| .**. . |
| .+o+S. . =|
| . o++ . o ++|
| . …+o. o o.o.|
| +.E+ .o o ++ |
| .+O= ooo .+. |
+—-[SHA256]—–+

这里为部署单独生成了一对公钥和私钥,私钥路径为 /root/.ssh/id_rsa_actions

将公钥保存到 authorized_keys 文件中
$ cat id_rsa_actions.pub >> authorized_keys

查看私钥内容,后面需要用到,先将内容保存起来
$ cat id_rsa_actions

三、准备工作

对于服务的部署所以这里选择了 ssh-deploy 这一个actioins,官方网址 https://github.com/marketplace/actions/ssh-deploy。

下面开始添加deploy.yaml文件中用到了一些变量。

在项目首页右上角点击 Seetings->Secets, 找到 Add a new secret,分别添加以下变量

SERVER_SSH_KEY 登录私钥,就是上面保存的私钥内容
REMOTE_HOST 服务器地址,如202.102.224.68
REMOTE_PORT (可选项)服务器ssh端口,一般默认为22
REMOTE_USER 登录服务器用户名,这时指密钥所属的用户
SOURCE (可选项),默认为‘’, 构建服务器路径,这里为相应 $GITHUB_WORKSPACE 根目录而言的相对路径, 例如 dist/
REMOTE_TARGET 服务器部署路径,如 /data/ghactions
ARGS (可选项)默认值为 -rltgoDzvO

四、上传代码到github远程仓库

我们这里定义了当master分支发生push操作时就触发一系列workflow操作。
$git push origin master
这时我们可以在 https://github.com/cfanbo/github-actions-demo/actions 页面看到当前项目的构建情况。

四、测试

这里我们登录到远程服务器,可以发现一个server可执行文件,表示已经成功部署到生产服务器了

五、其它

有关 workflow 的一些环境变量可参考 https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables

如果你有过CI/CD的经历,会发现本教程虽然实现了最为初级的功能,离真正线上使用还有一定的距离。主要存在以下问题:
1. 教程里只用了一个job,如果你的项目允许的话,完全可以利用多个job来同时异步构建。
2. 这里构建、测试和部署同时放在了一个job里,也不是太优雅
3. 这里的部署只是将最终生成的二进制文件上传到了生产服务器,并没有对服务进行启动操作或者说没有进行服务的热更新,这在一般场景下是不允许的
由于本篇文章只是一个简单介绍actions基本用法的教程,所以如果想真正用到工作中的话,可能还需要对李篇内容再完善完善才行。

参考

  • https://help.github.com/en/articles/workflow-syntax-for-github-actions
  • https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables
  • https://github.com/marketplace/actions/checkout
  • https://github.com/marketplace/actions/setup-go-for-use-with-actions
  • https://github.com/marketplace/actions/ssh-deploy
  • http://www.ruanyifeng.com/blog/2019/09/getting-started-with-github-actions.html