golang系列(7)-flag, context, docker

marvin

flag
go 语言内置的 flag 包实现了命令行参数的解析, flag 包使得开发命令行工具更为简单
os.Args
如果你只是简单的想要获取命令行的参数, 可以像下面的例子一样使用 os.Args 来获取:
package main
import (
"fmt"
"os"
)
func main() {
if len(os.Args) > 0 {
for index, arg := range os.Args {
fmt.Printf("args[%d]=%v\n", index, arg)
}
}
}
os.Args 中的内容是一个字符串数组, 保存的是命令行参数的具体内容, 我们可以在执行上面的代码的时候指定参数例如:
go run main.go --a=haha
这样输出的时候第一个值指向 mainl.go 的路径, 第二个值是 —a=haha
使用 flag
上面虽然可以读取这些命令行参数, 但是我们还要手动解析, 很不方便, 这个时候就需要使用 flag 来解决, flag 的本意是标志位, 它支持的命令行参数类型有 bool, int, int64, uint, uint64, float, float64, string, duration.
下面是一个使用 flag 的例子:
// flag.Type(flag名称, 默认值, 帮助信息)*Type
name := flag.String("name", "张三", "姓名")
age := flag.Int("age", 18, "年龄")
married := flag.Bool("married", false, "婚否")
// 解析
flag.Parse()
我们这里定义了姓名, 年龄, 婚否三个命令行参数, 需要注意的是此时的 name, age, married 均为对应类型的指针, 且定义好参数之后还需要调用 Parse 方法进行解析.
上面我们是直接声明了变量, 如果我们有已经声明好的变量, 还可以通过带有Var后缀的方法来定义命令行参数, 例如:
var name string
flag.StringVar(&name, "name", "张三", "请输入姓名")
除了第一个参数是一个变量的指针之外, 其余参数和 String 是一致的.
Context
Context 用于解决子 goroutine 的退出问题, 为了说明下面来看一个例子:
import (
"fmt"
"time"
"sync"
)
var wg sync.WaitGroup
func f() {
defer wg.Done()
for {
fmt.Println("周公")
time.Sleep(time.Millisecond * 500)
}
}
func main() {
wg.Add(1)
go f()
wg.Wait()
}
我们这里的 wg.Wait 会一直阻塞, 因为 wg.Done 不会执行, 如果要通知 f 退出, 我们可以通过一个全局变量或一个通道去控制.
虽然全局变量或通道可以解决问题, 但是没有一个通用的解决方案, 特别是在 http 的 server 中, 每一个请求都有一个对应的 goroutine 去处理, 请求处理函数通常会启动额外的 goroutine 来访问后端服务, 比如数据库和 RPC 服务. 用来处理一个请求的 goroutine 通常需要访问一些请求特定的数据, 比如终端用户的身份认证信息, 验证相关的 token, 请求的截止时间, 当一个请求被取消或超时的时候, 所有用来处理该请求的 goroutine 都应该迅速退出, 然后系统才释放这些 goroutine 占用的资源
下面是一个使用 context 的例子:
import (
"fmt"
"time"
"sync"
"context"
)
var wg sync.WaitGroup
func f(ctx context.Context) {
defer wg.Done()
FORLOOP:
for {
fmt.Println("周公")
time.Sleep(time.Millisecond * 500)
select {
case <-ctx.Done():
break FORLOOP
defalt:
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
wg.Add(1)
go f(ctx)
time.Sleep(time.Second * 5)
cancel()
wg.Wait()
}
我们这里使用 context.WithCancel 生成了一个带有 Done 通道的父context的拷贝, 同时会返回一个取消函数, 当这个返回的取消函数被调用的时候或当父context的 Done 通道被关闭的时候, 这个返回的context 的 Done 通道也会被关闭. 因此这里的 cancel 在调用的时候, f 中的 break FORLOOP 会执行, 从而实现了 f 这个子 goroutine 的退出.
context 是在 go1.7 之后加入的, 它定义了 Context 类型, 专门用来简化对于处理单个请求的多个 goroutine 之间与请求域的数据, 取消信号, 截止时间等相关操作, 这些操作可能涉及多个 API 调用, 对服务器传入的请求应该创建上下文, 而对服务器的传出调用应该接受上下文, 它们之间的函数调用链必须传递上下文, 或者可以使用 WithCancel, WithDeadline, WithTimeout, WithValue 创建的派生上下文. 当一个上下文被取消时, 它派生的所有上下文也被取消.
Context 接口
context.Context 是一个接口, 该接口定义了四个需要实现的方法, 其签名如下:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
其中:
- Deadline 方法需要返回当前 Context 被取消的时间, 也就是完成工作的截止时间
- Done 方法需要返回一个 Channel, 这个 Channel 会在当前工作完成或者上下文被取消之后关闭, 多次调用 Done 方法会返回同一个 Channel
- Err 方法会返回当前 Context 结束的原因, 它只会在 Done 返回的 Channel 被关闭时才会返回非空的值, 如果当前的 Context 被取消就会返回 Canceled 错误, 如果当前 Context 超时就会返回 DeadlineExceeded 错误
- Value 方法会从 Context 中返回 key 对应的值, 对于同一个上下文来说, 多次调用 Value 并传入相同的 key 会返回相同的结果, 该方法仅用于传递跨 API 和进程的跟请求域相关的数据
Background 和 TODO
go 内置了两个函数, 一个 Background 一个 TODO, 其中 Background 实际上我们在上面已经使用了. 这两个函数分别返回一个实现了 Context 接口的 background 和 todo. 我们代码中最开始都是以background 这个内置的上下文对象作为顶层的 parent context, 衍生出更多的子上下文.
Background 主要用于 main 函数, 初始化以及测试代码中, 作为 Context 这个树状结构的最顶层的 Contextg, 也就是根 Context.
TODO 目前还不知道具体的使用场景, 如果我们不知道该使用什么 Context 的时候, 可以使用这个. background 和 todo 本质上都是 emptyCtx 结构体类型, 是一个不可取消的, 没有设置截止时间, 没有携带任何值的 Context.
With 系列函数
context 中定义了四个 With 系列函数, 其中 withCancel 我们已经有所了解, 接下来我们看看另外3个
WithDeadline
WithDeadline 的函数签名如下:
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
其返回父上下文的副本, 并将 deadline 调整为不迟于 d. 如果父上下文的 deadline 已经早于 d, 则 WithDeadline(parent, d) 在语义上等同于父上下文. 当调用返回的 cancel 函数时, 或当父上下文的 Done 通道关闭时, 返回上下文的 Done 通道将被关闭, 以最先发生的情况为准.
取消此上下文将释放与其关联的资源, 因此代码应该在此上下文中运行的操作完成后立即调用 cancel, 下面是一个使用 WithDeadline 的例子:
func main() {
d := time.Now().Add(50 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), d)
// 尽管 ctx 会过期, 但在任何情况下调用它的 cancel 函数都是很好的实践
// 如果不这样做, 可能会使上下文及其父上下文存活的时间超过必要的时间
defer cancel()
select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err())
}
}
上面的代码中, 定义了一个 50 毫秒之后过期的 deadline, 然后我们调用 context.WithDeadline(context.Background(), d) 得到一个上下文和一个取消函数, 然后使用一个 select 让主程序陷入等待, 等待一秒后打印 overslept 退出或等待 ctx 过期后退出. 因为 ctx 50 秒后就过期了, 所以 ctx.Done 会先接收到值, 上面的代码会打印 ctx.Err() 的结果, 表示取消的原因.
WithTimeout
WithTimeout 与 WithDeadline 类似, 只不过其指定的是 time.Duration 类型, 是一个相对时间.
WithValue
WithValue 函数能够将请求作用域的数据与 Context 建立关系, 其声明如下:
func WithValue(parent Context, key, value interface{}) Context
WithValue 返回父节点的副本, 其中与 key 关联的值为 value.
仅对 API 和进程间传递请求域数据使用该上下文值, 而不是用它来传递可选参数给函数. 而且这里所提供的值必须是可比较的, 而不应该是 string 类型或其他内置类型, 以避免使用上下文在包之间发生冲突. WithValue 的用户应该为 key 定义自己的类型, 为了避免在分配给 interface 时进行分配, 上下文键通常具有具体类型结构. 或者导出的上下文关键变量的静态类型应该是指针或接口.
docker
部署演变
- 在一台物理机上部署 application
- 虚拟化技术
容器的必要性
- 开发人员开发一个 application 需要各种环境, 各种依赖
- 运维人员部署 application 时也需要搭建各种环境
容器解决的问题
- 解决了开发和运维之间的矛盾
容器是什么
- 对软件及其依赖的标准化打包
- 应用之间相互隔离
- 共享一个 OS kernel
docker 能做什么
- 简化配置
- 提高效率
docker 和 kubernates
- docker 可以被 k8s 管理
- kubernates 简称 k8s
DevOps
- DevOps 是解决开发和运维之间合作和沟通的问题
- 不仅仅依赖 docker, 还需要版本管理, 持续集成等
docker 的应用
- 在 2015 年的 618 大促中, 京东大胆启用了基于 Docker 的容器技术来承载大促的关键业务, 当时 Docker 容器的弹性云项目已经有近万个 Docker 容器在线上运行, 并且经受住了大量流量的考验
- 2016 年的 618 中, 弹性云项目更是担当重任, 全部应用系统和大部分的 DB 服务都已经跑在 Docker 上, 包括商品页面,用户订单, 用户搜索, 缓存, 数据库, 京东线上启用15万个 Docker 容器
- 京东弹性计算云通过软件定义数据中心与大规模容器集群调度, 实现海量计算资源的统一管理, 并满足性能与效率方面的需求, 提升业务自助上线效率, 应用部署密度大幅提升, 资源使用率提升, 节约大量的硬件资源.
docker 的安装
docker 提供了两个版本, 一个是社区版本一个是企业版本, 其官网地址为: https://docs.docker.com
在安装之前我们可以通过以下命令删除之前的版本:
sudo yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine
接下来设置仓库
sudo yum install -y yum-utils \
device-mapper-persistent-data \
lvm2
sudo yum-config-manager \
--add-repo \
https://download.docker.com/linux/centos/docker-ce.repo
列出当前可装的版本
yum list docker-ce --showduplicates | sort -r
选择一个版本安装
yum -y install docker-ce-18.06.1.ce-3.e17
启动 docker, 设置开机自启动
systemctl enable docker
systemctl start docker
如果安装出现问题, 可以尝试使用以下命令:
curl -sSL https://get.daocloud.io/docker | sh
curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun
启动 docker 运行hello-world
docker run hello-world
docker 的镜像和容器
docker 提供了打包运行 app 的平台, 它能将 app 与底层基础设施隔离. docker engine 是核心, 里面有后台的 dockerd 进程, 其提供了 REST API 接口, 还提供了 CLI 接口, 另外 docker 也是一种 C/S 架构
当 docker 没有运行的时候, 都是以镜像的形式存在的, 其本质就是文件和元数据的集合, 当跑起来的时候我们就称之为容器. 也就是说容器是镜像的实例. 而在 docker 的底层, 则有 Namespaces(网络隔离), Control groups(资源限制), Union file systems(image 和 container 分层) 提供基本的技术支持.
创建镜像的两种方式
镜像可以通过编写 Dockerfile 然后 build 之后生成, 也可以在创建 container 之后, 通过 commit 生成一个新的 image
查看本地的 image 列表
docker image ls
查看 image 详细信息
docker history IMAGE_ID
通过 history 命令指定镜像的 id 就能查看这个镜像的详细信息.
删除镜像
docker rmi IMAGE_NAME
这里通过 rmi 命令指定镜像名称就能删除本地存储的镜像
查看运行当中的 container
docker container ls
如果添加 -a 参数则可以显示所有的 container, 包括之前运行过的实例, 如果想清除所有历史可以使用下面的 rm 命令:
docker rm $(docker ps -aq)
运行一个交互式的 container
docker run -it centos
交互式启动会进入这个容器环境, 需要注意的是, 当退出容器环境我们的容器也会被销毁
启动一个后台容器
docker run --name=test -itd IMAGE_NAME
除了直接运行和交互式运行之外我们还可以通过 -itd 参数让镜像在后台运行, 通过指定 name 参数我们可以对这个容器进行命名, 方便使用.
停止运行后台的容器
当我们测试完了之后, 需要停掉后台的容器, 可以通过以下命令实现:
docker stop 容器id
与已经运行的后台容器交互
我们可以通过 exec 命令进入到后台的 container 容器里, 下面是一个进入 bash 的例子:
docker exec -it 容器id /bin/bash
Dockerfile 详解
dockerfile 只有10个关键字
- FROM 代表文件的开始, scratch 表示从头开始制作一个最简的, centos 表示使用 centos 作为系统, 没有就拉取, 支持指定版本号
- LABEL 注释或说明信息
- RUN 执行命令, 每执行一条命令, image 就多一层
- WORKDIR 进入或创建目录
- ADD/COPY ADD 和 COPY 一样是将本地文件添加到镜像里面, 区别是 ADD 可以会对文件进行解压缩, 使用方式: ADD/COPY 文件名 镜像中的路径
- ENV 设置环境变量
- CMD/ENTRYPOINT 指定启动容器时运行的命令, 如果指定了多个, 只会执行最后一个, 如果 CMD 后面跟 [] 则表示等待接收参数
- EXPOSE 向外部暴露的端口
搭建私有 docker 仓库
我们可以通过以下命令运行一个本地的仓库:
docker run -d -p 5000:5000 --restart always --name registry registry:2
这里通过 -d 指定后台运行, -p 指定端口映射, 第一个端口对应的是我们本地的进程端口, 第二个端口对应的是docker内部的端口. 这样一来我们就在本地启动了一个仓库容器, 接下来我们可以通过下面的命令 push 一个 ubuntu 镜像到这个仓库中.
docker pull ubuntu
docker tag ubuntu localhost:5000/ubuntu
docker push localhost:5000/ubuntu
查看私有仓库中拥有的镜像
curl localhost:5000/v2/_catlog
通过 http 接口我们会看到一个 repositories 的数组, 其中只有一个 ubuntu.
从私有仓库中拉取镜像
我们可以通过以下命令从私有仓库中拉取镜像到本地, 下面是一个拉取的例子:
docker pull localhost:5000/ubuntu
这里和 push 类似指定的都是同一个仓库地址下的镜像名
对容器资源进行限制
我们可以在运行一个镜像的时候指定其所能拥有的最大内存和 CPU用量, 从而限制其资源使用, 下面是一个例子:
docker run --memory=内存大小 --cpu-shares=cpu数量 ...
docker 的网络
docker 的网络分类有4种类, 主要分成单机和多机:
- 单机
- Bridge Network
- Host Network
- None Network
- 多机
- Overlay Network
namespace
namespace 也叫命名空间, 它是 docker 底层非常重要的概念, 通过 namespace 实现了容器之间网络隔离和通信的能力, 为了说明我们来看一个例子, 首先我们启动两个容器:
docker run -d --name=test1 busybox /bin/sh -c "while true;do sleep 3600;done"
docker run -d --name=test2 busybox /bin/sh -c "while true;do sleep 3600;done"
这里我们启动了两个容器, 一个是 test1, 一个是 test2, 因为本地是没有 busybox 镜像的, 因此会到网上下载运行, 在容器运行之后我们启动了一个循环.
接下来我们分别进入 test1, test2 的交互环境:
docker exec -it test1 /bin/sh
docker exec -it test2 /bin/sh
然后我们通过 ifconfig 或 ip a 命令查看 test1, test2 的 ip 地址, 然后在 test1 和 test2 中相互 ping, 可以发现是能通的.
这里先退出 test1, test2, 通过 stop 命令停止容器的运行:
docker stop $(docker container ls -aq)
这里的 docker container ls -aq 会列出所有的容器 id 列表, 方便我们停止所有运行中的容器.
test1, test2 两个容器之所以能互通是因为docker在创建一个容器的时候, 会给这个容器分配一个命名空间, 这个容器有自己的 ip 地址, 并且容器通过一个 Veth peer 与 docker 进行连接, 容器之间通过 docker 进行通信, docker 通过本机的网卡与外网进行通信, 从而也实现了容器访问外部网络的能力.
容器端口映射
在单机情况下 docker 有3种网络, 我们可以通过下面的命令查看:
docker network ls
如果要查看具体网络, 例如桥接网络, 可以通过下面的命令指定网络 id 查看:
docker network inspect NETWORK_ID
在输出的 Containers 字段我们可以看到具体有哪些容器是通过桥接网络连接的. 通过桥接方式连接的容器可以被其他容器访问, 也可以访问外部网络.
我们知道容器之间是可以通信的, 但容器本身也是有名称的, 如果一个容器能够通过另一个容器的名称去访问而不是其ip, 这会非常方便, 如果要实现这种需求, 可以在启动一个容器的时候指定它要连接的另一个容器, 下面是一个容器连接的例子:
docker run -d --name=test4 --link=test3 busybox /bin/sh -c "while true;do sleep 3600;done"
这里我们在启动容器 test4 的时候将其与容器 test3 连接起来了, 这个时候在 test4 中我们就可以通过 test3 去引用 test3 的 ip 地址了.
容器现在能够访问外部了, 那如何从外部访问容器呢, 这是一个问题, 这个时候就需要将容器的端口映射出来, 为了说明我们来看一个例子:
docker run --name=web -d nginx
这里启动了一个 nginx 容器, 然后我们可以通过以下命令查看当前桥接网络的状态:
docker network inspect bridge
可以看到 name 为 web 的容器的 ip 地址是: 172.17.0.4, 本机通过 curl 也是可以访问到容器内的 nginx 的, 但是我们并不能通过 localhost 访问到容器内的环境, 因此这就需要用端口映射来解决, 端口映射只需要指定 -p 参数即可, 其语法如下:
docker run -p 外部端口:内部端口