2023年6月

work_pool 协程池, 工作池


在工作中我们通常会使用可以指定启动的goroutine数量worker pool模式,控制goroutine的数量,防止goroutine泄漏和暴涨。

一个简易的work pool示例代码如下:

package main

import (
    "fmt"
    "time"
)

//var g sync.WaitGroup

func worker(id int, project chan int, result chan int) {

    for j := range project {
        fmt.Printf("worker:%d,start job:,%d\n", id, j)
        time.Sleep(time.Second)
        result <- j * 2
    }
}

func res(res chan int) {
    for {
        fmt.Printf("任务执行完毕,结果%d\n", <-res)
    }
}

func main() {

    project := make(chan int, 100)
    result := make(chan int)

    // 携程池 启动三个携程
    for i := 0; i <= 3; i++ {
        go worker(i, project, result)
    }

    // 生产五个任务 先执行完的协程会自动过取任务
    for j := 0; j <= 5; j++ {
        project <- j
    }

    // 取出结果
    go res(result)

    time.Sleep(time.Second * 3)

}

}

select多路复用

在阅读此篇文章之前,我希望你已经掌握了 并发编程 里的知识。

在某些场景下我们可能需要同时从多个通道接收数据。通道在接收数据时,如果没有数据可以被接收那么当前 goroutine 将会发生阻塞。你也许会写出如下代码尝试使用遍历的方式来实现从多个通道中接收值。

for{
    // 尝试从ch1接收值
    data, ok := <-ch1
    // 尝试从ch2接收值
    data, ok := <-ch2
}

这种方式虽然可以实现从多个通道接收值的需求,但是程序的运行性能会差很多。Go 语言内置了select关键字,使用它可以同时响应多个通道的操作。

Select 的使用方式类似于之前学到的 switch 语句,它也有一系列 case 分支和一个默认的分支。每个 case 分支会对应一个通道的通信(接收或发送)过程。select 会一直等待,直到其中的某个 case 的通信操作完成时,就会执行该 case 分支对应的语句。具体格式如下:


select {
case <-ch1:
    //...
case data := <-ch2:
    //...
case ch3 <- 10:
    //...
default:
    //默认操作
}

Select 语句具有以下特点。

  • 可处理一个或多个 channel 的发送/接收操作。
  • 如果多个 case 同时满足,select 会随机选择一个执行。
  • 对于没有 case 的 select 会一直阻塞,可用于阻塞 main 函数,防止退出。

下面的示例代码能够在终端打印出10以内的奇数,我们借助这个代码片段来看一下 select 的具体使用。


package main

import "fmt"

func main() {
    ch := make(chan int, 1)
    for i := 1; i <= 10; i++ {
        select {
        case x := <-ch:
            fmt.Println(x)
        case ch <- i:
        }
    }
}

// 输出如下 
1
3
5
7
9

示例中的代码首先是创建了一个缓冲区大小为1的通道 ch,进入 for 循环后:

  • 第一次循环时 i = 1,select 语句中包含两个 case 分支,此时由于通道中没有值可以接收,所以x := <-ch 这个 case 分支不满足,而ch <- i这个分支可以执行,会把1发送到通道中,结束本次 for 循环;
  • 第二次 for 循环时,i = 2,由于通道缓冲区已满,所以ch <- i这个分支不满足,而x := <-ch这个分支可以执行,从通道接收值1并赋值给变量 x ,所以会在终端打印出 1;
  • 后续的 for 循环以此类推会依次打印出3、5、7、9。

Dockerfile篇

Dockerfile是用来构建Docker镜像的文本文件, 由一条条构建镜像所需的指令和参数构成的脚本。

  • 文件名首字母D大写
  • 构建三部曲

    • 编写 Dockerfile文件
    • 构建 docker build -t 新镜像名字:TAG .

      • (.)代表当前目录
    • 运行 docker run -it 新镜像名字:TAG

使用 Dockerfile 定制镜像

这里仅讲解如何运行 Dockerfile 文件来定制一个镜像,具体 Dockerfile 文件内指令详解,将在下一节中介绍,这里你只要知道构建的流程即可。

1、下面以定制一个 nginx 镜像(构建好的镜像内会有一个 /usr/share/nginx/html/index.html 文件)

在一个空目录下,新建一个名为 Dockerfile 文件,并在文件内添加以下内容:

FROM nginx
RUN echo '这是一个本地构建的nginx镜像' > /usr/share/nginx/html/index.html

2、FROM 和 RUN 指令的作用

FROM:定制的镜像都是基于 FROM 的镜像,这里的 nginx 就是定制需要的基础镜像。后续的操作都是基于 nginx。

RUN:用于执行后面跟着的命令行命令。有以下俩种格式:

shell 格式:

RUN <命令行命令>
# <命令行命令> 等同于,在终端操作的 shell 命令。

exec 格式:

RUN ["可执行文件", "参数1", "参数2"]
# 例如:
# RUN ["./test.php", "dev", "offline"] 等价于 RUN ./test.php dev offline

注意:Dockerfile 的指令每执行一次都会在 docker 上新建一层。所以过多无意义的层,会造成镜像膨胀过大。例如:

FROM centos
RUN yum -y install wget
RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz"
RUN tar -xvf redis.tar.gz

以上执行会创建 3 层镜像。可简化为以下格式:

FROM centos
RUN yum -y install wget \
    && wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \
    && tar -xvf redis.tar.gz

如上,以 && 符号连接命令,这样执行后,只会创建 1 层镜像。

Dockerfile 保留关键字

  当前容器对外暴露出的端口
  EXPOSE

  容器启动命令
  Dockerfile 中可以有多个CMD指令, 但只有最后一个生效,CMD会被docker run 之后的参数替换
  CMD ["可执行文件", "参数", "参数2"]

  用来在构建镜像过程中设置环境变量
  ENV MY_PATH /user/test
  
  容器数据卷, 用于数据保持
  VOLUME

  当指定ENTRYPOINT后,CMD含义就发生了变化, 不再是直接运行其命令,二十将CMD的内容作为参数传递给ENTRYPOINT指令,他两个组合会变成<ENTRYPOINT>"<CMD>".
  例子:
  FROM nginx
  ENTRYPOINT ["nginx", "-c"] # 定参
  CMD ["/etc/nginx/nginx.conf"] # 变参

  指定一个已经存在的镜像作为模板:
  From

  作者/维护者
  MAINTAINER

  容器构建时执行的命令 两种格式 shell格式 exec格式 RUN是在docker build时运行
  RUN 命令

  将宿主机目录下的文件拷贝进镜像,且会自动处理URL和解压tar压缩包
  ADD

  复制文件 宿主机文件 复制进目标文件夹
  COPY

  指定创建容器后,终端默认登录的进来工作目录,一个落脚点, 可以配合ENV使用
  WORKDIR 

compose篇

前言

Dockerfile可以让用户管理一个单独的应用容器,而compose则允许用户在一个模板(yaml格式)中定义一组相关联的应用容器(被称为一个project,即项目)
例如一个web服务再加上后端的数据库服务容器等。

docker-compose是docker官方的开源项目
负责实现对docker容器集群的快速编排

docker建议我们每一个容器中只运行一个服务,因为docker容器本身占用资源极少,所以最好是将每个服务单独分隔开,
但是这样我们又面临了一个问题

如果我需要同事部署好多个服务,难道要每个服务单独写Dockerfile然后在构建镜像,构建容器,这样累都累死了,所以docker官方给我们提供了docker-compose多服务部署工具

例如要实现要给web微服务项目,除了Web服务容器本身,往往还需要再加上后端的数据库mysql服务容器,redis服务器,注册中心qureka,甚至还包括负载均衡容器等等。。


compose 使用的三个步骤

  1. 编写Dockerfile定义各个微服务应用并构建出对应的镜像
  2. 使用docker-compose 定义一个完整的业务单元,安排好整体应用中的各个容器
  3. 最后, 执行docker-compose up命令 来启动并运行整个应用程序,完成一键部署上线

一、Docker-compose简介

1、docker-compose基础概念

docker-compose项目是docker官方的开源项目, 负责实现对docker容器集群的快速编排。

docker-compose将所管理的容器分为三层, 分别是工程(project),服务(service)以及容器(containner)

docker-compose运行目录下的所有文件(docker-compose.yml文件、extends文件或环境变量等)组成一个工程,如无特殊指定,工程名即为当前目录名。
一个工程当中,可以包含多个服务,每个服务中定义了容器运行的镜像、参数、依赖。
一个服务中可以包括多个容器实例,docker-compose并没有解决负载均衡的问题。因此需要借助其他工具实现服务发现及负载均衡,比如consul。
docker-compose的工程配置文件默认为docker-compose.yml。可以通过环境变量COMPOSE_FILE -f 参数自定义配置文件,其自定义多个有依赖关系的服务及每个人服务运行的容器。

2、为什么要使用docker-compose

使用一个Dockerfile模板文件,可以让用户很方便的定义一个单独应用容器。在工作中,经常会碰到需要多个容器相互配合来完成某项任务的情况,例如要实现一个web项目,除了web服务容器本身,往往还需要再加上后端的数据库服务容器,甚至还包括负载均衡容器等。
compose允许用户通过一个单独docker-compose.yml模板文件(YAML格式)来定义一组相关联的应用容器为一个项目(project)

docker-compose项目由pypthon编写,调用docker服务提供的API来对容器进行管理,因此, 只要所操作的平台支持docker-API,就可以在其上利用conpose来进行编排管理。
简单来说:就是来管理多个容器的,定义启动顺序的,合理编排,方便管理。

二、YAML文件格式及编写注意事项

1、YAML文件格式

YAML是一种标记性语言,它可以很直观的展示数据序列化格式,可读性高。
类似于json数据描述语言,但是语法要比json简单很多。
YAML数据结构通过缩进来表示,连续的项目通过减号来表示,键值对用冒号分隔,数组用中括号[ ] 括起来,bash用花括号{ } 括起来。

2、YAML格式的注意事项

不支持制表符tab键缩进,只能使用空格缩进
通常开头缩进2个空格
字符后缩进1个空格,如冒号【:】、逗号【,】、横杠【-】
用#号表示注释
如果包含特殊字符用单引号【’ '】引起来作为普通字符,如果用双引号【“ ”】表示特殊字符本身的意思,
布尔值必须用【“ ”】括起来
YAML区分大小写
3、YAML数据结构案例

version: '3'
services:
  # mysql
  mysql:
    container_name: daoda-mysql //容器名字
    image: mysql:5.7  //使用mysql:5.7镜像
    ports:
      - "60133:3306"  //端口映射 不能低于60
    environment:
      - "MYSQL_ROOT_PASSWORD=root"
      - "TZ=Asia/Shanghai"
    command:  // 执行命令,会覆盖容器启动后默认执行的命令(会覆盖dockefile中的CMD指令)
      --character-set-server=utf8mb4
      --collation-server=utf8mb4_unicode_ci
      --lower-case-table-names=1
      --sql-mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'
    restart: always //重启策略,定义是否重启容器1、no,默认策略,在容器退出时不重启容器2、on-failure,在容器非正常退出时(退出状态非0),才会重启容器3、on-failure:3 在容器非正常退出时,重启容器,最多重启3次4、always,在容器退出时总是重启容器,5、unless-stopped,在容器退出时总是重启容器,但是不考虑在Docker守护进程启动时就已经停止了的容器。
    privileged: true // 用来给容器root权限,注意是不安全的,true
    networks: //加入网络,引用顶级networks下条目
      bridge:
        ipv4_address: 172.60.0.1

  # 报告模块:lly
  php:
    container_name: php
    build: // 编译指定内容
      context: ../../Code_lly //设置编译文件夹
      dockerfile: daoda-report-Dockerfile // 编译Dockerfile文件
    restart: always
    privileged: true
    networks:
      daoda-bridge:
        ipv4_address: 172.60.0.2
networks:
  daoda-bridge:
    driver: bridge
    ipam:
      config:
        - subnet: 172.60.0.0/16
        
volumes: 定义挂在宿主机目录
  log:
    driver_opts:
      type: nfs
      o: bind
      device: "/home/data/log"



compose下载:

自动安装:

  sudo yum update
  sudo yum install docker-compose-plugin


  docker compose version

手动安装:

第一步:
要下载并安装撰写 CLI 插件,请运行:

    DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}
    mkdir -p $DOCKER_CONFIG/cli-plugins
    curl -SL https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-linux-x86_64 -o $DOCKER_CONFIG/cli-plugins/docker-compose

第二步:
将可执行权限应用于二进制文件:

  
  chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose

或者,如果您选择为所有用户安装 Compose:

  sudo chmod +x /usr/local/lib/docker/cli-plugins/docker-compose

第三步:
测试安装:

  docker compose version
  Docker Compose version v2.16.0

compose 常用命令

最好在yml同级目录下执行

docker-compose -h 查看帮助
docker-compose up 启动所有docker-compose服务
docker-compose up -d 启动所有docker-compose服务并后台运行
docker-compose down 停止并删除容器,网络,卷,镜像
docker-compose exec yml的服务id /bin/bash 进入容器实例内部
docker-compose ps 列出当前所有compose编排过运行的所有容器
docker-compose top 展示当前compose编排过的容器进程

docker-compose config 检查配置
docker-compose config -q 检查配置,有问题才有输出
docker-compose restart 重启
docker-compose start 启动
docker-compose stro 停止