Golang

Golang

【腾讯云】#容器团队#高级容器研发#招聘

Shirleyee 发表了文章 • 1 个评论 • 222 次浏览 • 2019-05-24 10:33 • 来自相关话题

高级容器研发工程师 工作职责 负责公有云/私有云中 Kubernetes/Devops 等产品技术方案设计与研发工作;负责 Kubernetes 相关前沿技术规划和调研工作,从技术上保证产品的竞争力;负责与产品及客户沟通,判定需求的合理 ...查看全部
高级容器研发工程师
工作职责
  1. 负责公有云/私有云中 Kubernetes/Devops 等产品技术方案设计与研发工作;
  2. 负责 Kubernetes 相关前沿技术规划和调研工作,从技术上保证产品的竞争力;
  3. 负责与产品及客户沟通,判定需求的合理性,找出最合适的方式解决客户问题。
工作要求
  1. 3 年以上后端开发经验,Coding、Debug 能力强, 有丰富的架构设计经验;
  2. 熟悉 C/C++/Go/Java/Python/Ruby 等至少二种编程语言;
  3. 熟悉 Docker/Kubernetes/Swarm/Mesos 等技术;
  4. 熟悉 Jenkins/ELK/Prometheus 等技术优先;
  5. 熟悉 AWS/Google Cloud 等云计算厂商产品优先。


有意请戳:
Wechat:13723737494
Email:Shirleyeee@foxmail.com

知乎社区核心业务 Golang 化实践

大卫 发表了文章 • 0 个评论 • 1081 次浏览 • 2018-12-13 18:40 • 来自相关话题

#背景 众所周知,知乎社区后端的主力编程语言是 Python。 随着知乎用户的迅速增长和业务复杂度的持续增加,核心业务的流量在过去一年内增长了好几倍,对应的服务端的压力也越来越大。随着业务发展,我们发现 Python 作为 ...查看全部
#背景
众所周知,知乎社区后端的主力编程语言是 Python。

随着知乎用户的迅速增长和业务复杂度的持续增加,核心业务的流量在过去一年内增长了好几倍,对应的服务端的压力也越来越大。随着业务发展,我们发现 Python 作为动态解释型语言,较低的运行效率和较高的后期维护成本带来的问题逐渐暴露出来:

  1. 运行效率较低。知乎目前机房机柜空间已经不足,按照目前的用户和流量增长速度,可预见将在短期内服务器资源告急(针对这一点,知乎正在由单机房架构升级为异地多活架构);
  2. Python 过于灵活的语言特性,导致多人协作和项目维护成本较高。
受益于近些年开源社区的发展和容器等关键技术的普及,知乎的基础平台技术选型一直较为开放。在开放的标准之上,各个语言都有成熟的开源的中间件可供选择。这使得业务做选型时可以根据问题场景选择更合适的工具,语言也是一样。

基于此,为了解决资源占用问题和动态语言的维护成本问题,我们决定尝试使用静态语言对资源占用极高的核心业务进行重构。
#为什么选择 Golang
如上所述,知乎在后端技术选型上比较开放。在过去几年里,除了 Python 作为主力语言开发,知乎内部也不乏 Java、Golang、NodeJS 和 Rust 等语言开发的项目。
1.png

通过 ZAE(Zhihu App Engine)新建一个应用时,提供了多门语言的支持

Golang 是目前知乎内部讨论交流最活跃的编程语言之一,考虑到以下几点,我们决定尝试用 Golang 重构内部高并发量的核心业务:

* 天然的并发优势,特别适合 IO 密集应用
* 知乎内部基础组件的 Golang 版生态比较完善
* 静态类型,多人协作开发和维护更加安全可靠
* 构建好后只需一个可执行文件即可,方便部署
* 学习成本低,且开发效率较 Python 没有明显降低

相比另一门也很优秀的待选语言—— Java,Golang 在知乎内部生态环境、部署的方便程度和工程师的兴趣上都更胜一筹,最终我们决定,选择 Golang 作为开发语言。
#改造成果
截至目前,知乎社区 member(RPC,高峰数十万 QPS)、评论(RPC + HTTP)、问答(RPC + HTTP)服务已经全部通过 Golang 重写。同时因为在 Golang 化过程中我们对 Golang 基础组件的进一步完善,目前一些新的业务在开发之初就直接选择了 Golang 来实现,Golang 已经成为知乎内部新项目技术选型的推荐语言之一。

相比改造前,目前得到改进的点有以下:

  1. 节约了超过 80% 的服务器资源。由于我们的部署系统采用蓝绿部署,所以之前占用服务器资源最高的几个业务会因为容器资源原因无法同时部署,需要排队依次部署。重构后,服务器资源得到优化,服务器资源问题得到了有效解决。
  2. 多人开发和项目维护成本大幅下降。想必大家维护大型 Python 项目都有经常需要里三层、外三层确认一个函数的参数类型和返回值。而 Golang 里,大家都面向接口定义,然后根据接口来实现,这使得编码过程更加安全,很多 Python 代码运行时才能发现的问题可以在编译时即可发现。
  3. 完善了内部 Golang 基础组件。前面提到,知乎内部基础组件的 Golang 版比较完善,这是我们选择 Golang 的前提之一。不过,在重构的过程中,我们发现仍有部分基础组件不够完善甚至缺少。所以,我们也完善和提供了不少基础组件,为之后其它项目的 Golang 化改造提供了便利。

2.png

过去 10 个月问答服务的 CPU 核数占用变化趋势
#实施过程
得益于知乎微服务化比较彻底,每个独立的微服务想要更换语言非常方便,我们可以方便地对单个业务进行改造,且几乎可以做到外部依赖方无感知。

知乎内部,每个独立的微服务有自己独立的各种资源,服务间是没有资源依赖的,全部通过 RPC 请求交互,每个对外提供服务(HTTP or RPC)的容器组,都通过独立的 HAProxy 地址代理对外提供服务。一个典型的微服务结构如下:
3.png

知乎内部一个典型的微服务组成,服务间没有资源依赖

所以,我们的 Golang 化改造分为了以下几步:
##Step 1. 用 Golang 重构逻辑
首先,我们会新起一个微服务,通过 Golang 来重构业务逻辑,但是:

  1. 新服务对外暴露的协议(HTTP 、RPC 接口定义和返回数据)与之前保持一致(保持协议一致很重要,之后迁移依赖方会更方便)
  2. 新的服务没有自己的资源,使用待重构服务的资源:

4.png

新服务(下)使用待重构服务(上)的资源,短期内资源混用

##Step 2. 验证新逻辑正确性
当代码重构完成后,在将流量切换到新逻辑之前,我们会先验证新服务的正确性。

针对读接口,由于其是幂等的,多次调用没有副作用,所以当新版接口实现完成后,我们会在老服务收到请求的同时,起一个协程请求新服务,并对比新老服务的数据是否一致:

  1. 当请求到达老服务后,会立即启一个协程请求新的服务,与此同时老服务的主逻辑会正常执行。
  2. 当请求返回后,会比较老服务与新实现的服务返回数据是否相同,如果不同,会打点记录 + 日志记录。
  3. 工程师根据打点指标和日志,发现新实现逻辑的错误,改正后继续验证(其实这一步,我们也发现了不少原本 Python 实现的错误)。

5.png

服务请求两边数据,并对比结果,但返回老服务的结果

而对于写接口,大部分并不是幂等的,所以针对写接口不能像上面这样验证。对于写接口,我们主要会通过以下手段保证新旧逻辑等价:

  1. 单元测试保证
  2. 开发者验证
  3. QA 验证

##Step 3. 灰度放量
当一切验证通过之后,我们会开始按照百分比转发流量。

此时,请求依然会被代理到老的服务的容器组,但是老服务不再处理请求,而是转发请求到新服务中,并将新服务返回的数据直接返回。

之所以不直接从流量入口切换,是为了保证稳定性,在出现问题时可以迅速回滚。
6.png

服务请求 Golang 实现
##Step 4. 切流量入口
当上一步的放量达到 100% 后,请求虽然依然会被代理到老的容器组,但返回的数据已经全部是新服务产生的。此时,我们可以把流量入口直接切换到新服务了。
7.png

请求直接打到新的服务,旧服务没有流量了
##Step 5. 下线老服务
到这里重构已经基本接近尾声了。不过新服务的资源还在老服务中,以及老的没有流量的服务其实还没有下线。

到这里,直接把老服务的资源归属调整为新服务,并下线老服务即可。
8.png

Goodbye,Python

至此,重构完成。
#Golang 项目实践
在重构的过程中,我们踩了不少坑,这里摘其中一些与大家分享一下。如果大家有类似重构需求,可简单参考。
##换语言重构的前提是了解业务
不要无脑翻译原来的代码,也不要无脑修复原本看似有问题的实现。在重构的初期,我们发现一些看似可以做得更好的点,闷头一顿修改之后,却产生了一些奇怪的问题。后面的经验是,在重构前一定要了解业务,了解原本的实现。最好整个重构的过程有对应业务的工程师也参与其中。
##项目结构
关于合适的项目结构,其实我们也走过不少弯路。

一开始,我们根据在 Python 中的实践经验,层与层之间直接通过函数提供交互接口。但是,迅速发现 Golang 很难像 Python 一样,方便地通过 monkey patch 完成测试。

经过逐渐演进和参考各种开源项目,目前,我们的代码结构大致是这样:
.
├── bin --> 构建生成的可执行文件
├── cmd --> 各种服务的 main 函数入口( RPC、Web 等)
│ ├── service
│ │ └── main.go
│ ├── web
│ └── worker
├── gen-go --> 根据 RPC thrift 接口自动生成
├── pkg --> 真正的实现部分(下面详细介绍)
│ ├── controller
│ ├── dao
│ ├── rpc
│ ├── service
│ └── web
│ ├── controller
│ ├── handler
│ ├── model
│ └── router
├── thrift_files --> thrift 接口定义
│ └── interface.thrift
├── vendor --> 依赖的第三方库( dep ensure 自动拉取)
├── Gopkg.lock --> 第三方依赖版本控制
├── Gopkg.toml
├── joker.yml --> 应用构建配置
├── Makefile --> 本项目下常用的构建命令
└── README.md

分别是:

* bin:构建生成的可执行文件,一般线上启动就是 `bin/xxxx-service`
* cmd:各种服务(RPC、Web、离线任务等)的 main 函数入口,一般从这里开始执行
* gen-go:thrift 编译自动生成的代码,一般会配置 Makefile,直接 `make thrift` 即可生成(这种方式有一个弊端:很难升级 thrift 版本)
* pkg:真正的业务实现(下面详细介绍)
* thrift_files:定义 RPC 接口协议
* vendor:依赖的第三方库

其中,pkg 下放置着项目的真正逻辑实现,其结构为:
pkg/
├── controller
│ ├── ctl.go --> 接口
│ ├── impl --> 接口的业务实现
│ │ └── ctl.go
│ └── mock --> 接口的 mock 实现
│ └── mock_ctl.go
├── dao
│ ├── impl
│ └── mock
├── rpc
│ ├── impl
│ └── mock
├── service --> 本项目 RPC 服务接口入口
│ ├── impl
│ └── mock
└── web --> Web 层(提供 HTTP 服务)
├── controller --> Web 层 controller 逻辑
│ ├── impl
│ └── mock
├── handler --> 各种 HTTP 接口实现
├── model -->
├── formatter --> 把 model 转换成输出给外部的格式
└── router --> 路由

如上结构,值得关注的是我们在每一层之间一般都有 impl、mock 两个包。
9.png

这样做是因为 Golang 中不能像 Python 那样方便地动态 mock 掉一个实现,不能方便地测试。我们很看重测试,Golang 实现的测试覆盖率也保持在 85% 以上。所以我们将层与层之间先抽象出接口(如上 ctl.go),上层对下层的调用通过接口约定。在执行的时候,通过依赖注入绑定 impl 中对接口的实现来运行真正的业务逻辑,而测试的时候,绑定 mock 中对接口的实现来达到 mock 下层实现的目的。

同时,为了方便业务开发,我们也实现了一个 Golang 项目的脚手架,通过脚手架可以更方便地直接生成一个包含 HTTP & RPC 入口的 Golang 服务。这个脚手架已经集成到 ZAE(Zhihu App Engine),在创建出 Golang 项目后,默认的模板代码就生成好了。对于使用 Golang 开发的新项目,创建好就有了一个开箱即用的框架结构。
##静态代码检查,越早越好
我们在开发的后期才意识到引入静态代码检查,其实最好的做法是在项目开始时就及时使用,并以较严格的标准保证主分支的代码质量。

在开发后期才引入的问题是,已经有太多代码不符合标准。所以我们不得不短期内忽略了很多检查项。

很多非常基础甚至愚蠢的错误,人总是无法 100% 避免的,这正是 linter 存在的价值。

实际实践中,我们使用 gometalinter。gometalinter 本身不做代码检查,而是集成了各种 linter,提供统一的配置和输出。我们集成了 vet、golint 和 errcheck 三种检查。
##降级
降级的粒度究竟是什么?这个问题一些工程师的观点是 RPC 调用,而我们的答案是「功能」。

在重构过程中,我们按照「如果这个功能不可用,对用户的影响该是什么」的角度,将所有可降级的功能点都做了降级,并对所有降级加上对应的指标点和报警。最终的效果是,如果问答所有的外部 RPC 依赖全部挂了(包括 member 和鉴权这样的基础服务),问答本身仍然可以正常浏览问题和回答。

我们的降级是在 circuit 的基础上,封装指标收集和日志输出等功能。Twitch 也在生产环境中使用了这个库,且我们超过半年的使用中,还没有遇到什么问题。
##anti-pattern: panic - recover
大部分人开始使用 Golang 开发后,一个非常不习惯的点就是它的错误处理。一个简单的 HTTP 接口实现可能是这样:
func (h [i]AnswerHandler) Get(w http.ResponseWriter, r [/i]http.Request) {
ctx := r.Context()

loginId, err := auth.GetLoginID(ctx)
if err != nil {
zapi.RenderError(err)
---> return
}

answer, err := h.PrepareAnswer(ctx, r, loginId)
if err != nil {
zapi.RenderError(err)
---> return
}

formattedAnswer, err := h.ctl.FormatAnswer(ctx, loginId, answer)
if err != nil {
zapi.RenderError(err)
---> return
}
zapi.RenderJSON(w, formattedAnswer)
}

如上,每行代码后有紧跟着一个错误判断。繁琐只是其次,主要问题在于,如果错误处理后面的 return 语句忘写,那么逻辑并不会被阻断,代码会继续向下执行。在实际开发过程中,我们也确实犯过类似的错误。

为此,我们通过一层 middleware,在框架外层将 panic 捕获,如果 recover 住的是框架定义的错误则转换为对应的 HTTP Error 渲染出去,反之继续向上层抛出去。改造后的代码成了这样:
func (h [i]AnswerHandler) Get(w http.ResponseWriter, r [/i]http.Request) {
ctx := r.Context()

loginId := auth.MustGetLoginID(ctx)

answer := h.MustPrepareAnswer(ctx, r, loginId)

formattedAnswer := h.ctl.MustFormatAnswer(ctx, loginId, answer)

zapi.RenderJSON(w, formattedAnswer)
}

如上,业务逻辑中以前 RenderError 并直接紧接着返回的地方,现在再遇到 error 的时候,会直接 panic。这个 panic 会在 HTTP 框架层被捕获,如果是项目内定义的 HTTPError,则转换成对应的接口 4xx JSON 格式返回给前端,否则继续向上抛出,最终变成一个 5xx 返回前端。

这里提到这个实现并不是推荐大家这样做,Golang 官方明确不推荐这样使用。不过,这确实有效地解决了一些问题,这里提出来供大家多一种参考。
##Goroutine 的启动
在构建 model 的时候,很多逻辑其实相互之间没有依赖是可以并发执行的。这时候,启动多个 goroutine 并发获取数据可以极大降低响应时间。

不过,刚使用 Golang 的人很容易踩到的一个 goroutine 坑点是,一个 goroutine 如果 panic 了,在它的父 goroutine 是无法 recover 的——严格来讲,并没有父子 goroutine 的概念,一旦启动,就是一个独立的 goroutine 了。

所以这里一定要非常注意,如果你新启动的 goroutine 可能 panic,一定需要本 goroutine 内 recover。当然,更好的方式是做一层封装,而不是在业务代码裸启动 goroutine。

因此我们参考了 Java 里面的 Future 功能,做了简单的封装。在需要启动 goroutine 的地方,通过封装的 Future 来启动,Future 来处理 panic 等各种状况。
##http.Response Body 没有 close 导致 goroutine 泄露
一段时间内,我们发现服务 goroutine 数量随着时间不断上涨,并会随着重启容器立刻掉下来。因此我们猜测代码存在 goroutine 泄露。
10.png

Goroutine 数量随运行时间逐渐增长,并在重启后掉下来

通过 goroutine stack 和在依赖库打印日志,最终定位到的问题是某个内部的基础库使用了 http.Client,但是没有 `resp.Body.Close()`,导致发生 goroutine 泄露。

这里的一个经验教训是生产环境不要直接用 http.Get,自己生成一个 http client 的实例并设置 timeout 会更好。

修复这个问题后就正常了:
11.png

resp.Body.Close()

虽然简单几句话介绍了这个问题,但实际定位问题的步骤耗费了我们不少时间,后面可以新起一篇文章专门介绍下 goroutine 泄露的排查过程。
#最后
核心业务的 Golang 化重构是由社区业务架构团队与社区内容技术团队的同学一起,经过 2018 年 Q2/Q3 的努力达成的目标。

社区业务架构团队负责解决知乎社区后端的业务复杂度和并发规模快速提升带来的问题和挑战。随着知乎业务规模和用户的快速增长,以及业务复杂度的持续增加,我们团队面临的技术挑战也越来越大。目前我们正在实施知乎社区的多机房异地多活架构,同时也在努力保障和提升知乎后端的质量和稳定性。

Golang基于GitLab CI/CD部署方案

大卫 发表了文章 • 0 个评论 • 1886 次浏览 • 2018-10-14 12:01 • 来自相关话题

持续集成(Continuous Integration)是一种软件开发实践,即团队开发成员经常集成它们的工作,通过每个成员每天至少集成一次,也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽早地发现集成错误 ...查看全部
持续集成(Continuous Integration)是一种软件开发实践,即团队开发成员经常集成它们的工作,通过每个成员每天至少集成一次,也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽早地发现集成错误。

持续部署(Continuous Deployment)是通过自动化的构建、测试和部署循环来快速交付高质量的产品。某种程度上代表了一个开发团队工程化的程度,毕竟快速运转的互联网公司人力成本会高于机器,投资机器优化开发流程化相对也提高了人的效率,让 engineering productivity 最大化。
#1. 环境准备
本次试验是基于CentOS 7.3,Docker 17.03.2-ce环境下的。Docker的安装这里就不赘述了,提供官方链接:Get Docker CE for CentOS
##1.1. Docker启动GitLab
启动命令如下:
docker run --detach \
--hostname gitlab.chain.cn \
--publish 8443:443 --publish 8080:80 --publish 2222:22 \
--name gitlab \
--restart always \
--volume /Users/zhangzc/gitlab/config:/etc/gitlab \
--volume /Users/zhangzc/gitlab/logs:/var/log/gitlab \
--volume /Users/zhangzc/gitlab/data:/var/opt/gitlab \
gitlab/gitlab-ce

port,hostname,volume根据具体情况具体设置。
##1.2. Docker启动gitlab-runner
启动命令如下:
sudo docker run -d /
--name gitlab-runner /
--restart always /
-v /Users/zhangzc/gitlab-runner/config:/etc/gitlab-runner /
-v /Users/zhangzc/gitlab-runner/run/docker.sock:/var/run/docker.sock /
gitlab/gitlab-runner:latest

volume根据具体情况具体设置。
##1.3. 用于集成部署的镜像制作
我们的集成和部署都需要放在一个容器里面进行,所以,需要制作一个镜像并安装一些必要的工具,用于集成和部署相关操作。目前我们的项目都是基于Golang 1.9.2的,这里也就基于Golang 1.9.2的镜像制定一个特定的镜像。

Dockerfile内容如下:
# Base image: https://hub.docker.com/_/golang/
FROM golang:1.9.2
USER root
# Install golint
ENV GOPATH /go
ENV PATH ${GOPATH}/bin:$PATH
RUN mkdir -p /go/src/golang.org/x
RUN mkdir -p /go/src/github.com/golang
COPY source/golang.org /go/src/golang.org/x/
COPY source/github.com /go/src/github.com/golang/
RUN go install github.com/golang/lint/golint
# install docker
RUN curl -O https://get.docker.com/builds/Linux/x86_64/docker-latest.tgz \
&& tar zxvf docker-latest.tgz \
&& cp docker/docker /usr/local/bin/ \
&& rm -rf docker docker-latest.tgz
# install expect
RUN apt-get update
RUN apt-get -y install tcl tk expect

其中Golint是用于Golang代码风格检查的工具。

Docker是由于需要在容器里面使用宿主的Docker命令,这里就需要安装一个Docker的可执行文件,然后在启动容器的时候,将宿主的 /var/run/docker.sock 文件挂载到容器内的同样位置。

expect是用于SSH自动登录远程服务器的工具,这里安装改工具是为了可以实现远程服务器端部署应用。

另外,在安装Golint的时候,是需要去golang.org下载源码的,由于墙的关系,go get命令是执行不了的。为了处理这个问题,首先通过其他渠道先下载好相关源码,放到指定的路径下,然后copy到镜像里,并执行安装即可。

下面有段脚本是用于生成镜像的:
#!/bin/bash
echo "提取构建镜像时需要的文件"
source_path="source"
mkdir -p $source_path/golang.org
mkdir -p $source_path/github.com
cp -rf $GOPATH/src/golang.org/x/lint $source_path/golang.org/
cp -rf $GOPATH/src/golang.org/x/tools $source_path/golang.org/
cp -rf $GOPATH/src/github.com/golang/lint $source_path/github.com
echo "构建镜像"
docker build -t go-tools:1.9.2 .
echo "删除构建镜像时需要的文件"
rm -rf $source_path

生成镜像后,推送到镜像仓库,并在gitlab-runner的服务器上拉取该镜像。

本次试验的GitLab和gitlab-runner是运行在同一服务器的Docker下的
#2. Runner注册及配置
##2.1. 注册
环境准备好后,在服务器上执行以下命令,注册Runner:
docker exec -it gitlab-runner gitlab-ci-multi-runner register

按照提示输入相关信息:
Please enter the gitlab-ci coordinator URL:
# gitlab的url, 如:https://gitlab.chain.cn/
Please enter the gitlab-ci token for this runner:
# gitlab->你的项目->settings -> CI/CD ->Runners settings
Please enter the gitlab-ci description for this runner:
# 示例:demo-test
Please enter the gitlab-ci tags for this runner (comma separated):
# 示例:demo
Whether to run untagged builds [true/false]:
# true
Please enter the executor: docker, parallels, shell, kubernetes, docker-ssh, ssh, virtualbox, docker+machine, docker-ssh+machine:
# docker
Please enter the default Docker image (e.g. ruby:2.1):
# go-tools:1.9.2(之前自己制作的镜像)

1.png

成功后,可以看到GitL ab->你的项目->Settings -> CI/CD ->Runners settings页面下面有以下内容:
2.png

##2.2. 配置
注册成功之后,还需要在原有的配置上做一些特定的配置,如下:
[[runners]]
name = "demo-test"
url = "https://gitlab.chain.cn/"
token = "c771fc5feb1734a9d4df4c8108cd4e"
executor = "docker"
[runners.docker]
tls_verify = false
image = "go-tools:1.9.2"
privileged = false
disable_cache = false
volumes = ["/var/run/docker.sock:/var/run/docker.sock"]
extra_hosts = ["gitlab.chain.cn:127.0.0.1"]
network_mode = "host"
pull_policy = "if-not-present"
shm_size = 0
[runners.cache]

这里先解释下gitlab-runner的流程吧,gitlab-runner在执行的时候,会根据上面的配置启动一个容器,即配置中的go-tools:1.9.2,其中所有的启动参数都会在[runners.docker]节点下配置好,包括挂载啊,网络啊之类的。容器启动成功之后,会使用这个容器去GitLab上pull代码,然后根据自己定义的规则进行检验,全部检测成功之后便是部署了。

volumes:是为了在容器中可以执行宿主机的Docker命令。

extra_hosts:给GitLab添加个host映射,映射到127.0.0.1

network_mode:令容器的网络与宿主机一致,只有这样才能通过127.0.0.1访问到GitLab。

pull_policy:当指定的镜像不存在的话,则通过docker pull拉取。
#3. 定义规则
在GitLab项目根目录创建.gitlab-ci.yml文件,填写Runner规则,具体语法课参考官方文档:https://docs.gitlab.com/ee/ci/yaml/。
##3.1. Go集成命令
下面介绍几个Golang常见的集成命令。

包列表,正如在官方文档中所描述的那样,Go项目是包的集合。下面介绍的大多数工具都将使用这些包,因此我们需要的第一个命令是列出包的方法。我们可以用go list子命令来完成:
go list ./...

请注意,如果我们要避免将我们的工具应用于外部资源,并将其限制在我们的代码中。 那么我们需要去除vendor目录,命令如下:
go list ./... | grep -v /vendor/

单元测试,这些是您可以在代码中运行的最常见的测试。每个.go文件需要一个能支持单元测试的_test.go文件。可以使用以下命令运行所有包的测试:
go test -short $(go list ./... | grep -v /vendor/)

数据竞争,这通常是一个难以逃避解决的问题,Go工具默认具有(但只能在Linux/amd64、FreeBSD/amd64、Darwin/amd64和Windows/amd64上使用):
go test -race -short $(go list . /…| grep - v /vendor/)

代码覆盖,这是评估代码的质量的必备工具,并能显示哪部分代码进行了单元测试,哪部分没有。

要计算代码覆盖率,需要运行以下脚本:
PKG_LIST=$(go list ./... | grep -v /vendor/)
for package in ${PKG_LIST}; do
go test -covermode=count -coverprofile "cover/${package[size=16]*/}.cov" "$package" ;[/size]
done
tail -q -n +2 cover/*.cov >> cover/coverage.cov
go tool cover -func=cover/coverage.cov

如果我们想要获得HTML格式的覆盖率报告,我们需要添加以下命令:
go tool cover -html=cover/coverage.cov -o coverage.html

构建,最后一旦代码经过了完全测试,我们要对代码进行编译,从而构建可以执行的二进制文件。
go build .

linter,这是我们在代码中使用的第一个工具:linter。它的作用是检查代码风格/错误。这听起来像是一个可选的工具,或者至少是一个“不错”的工具,但它确实有助于在项目上保持一致的代码风格。

linter并不是Go本身的一部分,所以如果要使用,你需要手动安装它(之前的go-tools镜像我们已经安装过了)。

使用方法相当简单:只需在代码包上运行它(也可以指向. go文件):
$ golint -set_exit_status $(go list ./... | grep -v /vendor/)

注意-set_exit_status选项。 默认情况下,Golint仅输出样式问题,并带有返回值(带有0返回码),所以CI不认为是出错。 如果指定了-set_exit_status,则在遇到任何样式问题时,Golint的返回码将不为0
##3.2. Makefile
如果我们不想在.gitlab-ci.yml文件中写的太复杂,那么我们可以把持续集成环境中使用的所有工具,全部打包在Makefile中,并用统一的方式调用它们。

这样的话,.gitlab-ci.yml文件就会更加简洁了。当然了,Makefile同样也可以调用*.sh脚本文件。
##3.3. 配置示例
3.3.1. .gitlab-ci.yml
image: go-tools:1.9.2
stages:
- build
- test
- deploy
before_script:
- mkdir -p /go/src/gitlab.chain.cn/ZhangZhongcheng /go/src/_/builds
- cp -r $CI_PROJECT_DIR /go/src/gitlab.chain.cn/ZhangZhongcheng/demo
- ln -s /go/src/gitlab.chain.cn/ZhangZhongcheng /go/src/_/builds/ZhangZhongcheng
- cd /go/src/_/builds/ZhangZhongcheng/demo
unit_tests:
stage: test
script:
- make test
tags:
- demo
race_detector:
stage: test
script:
- make race
tags:
- demo
code_coverage:
stage: test
script:
- make coverage
tags:
- demo
code_coverage_report:
stage: test
script:
- make coverhtml
only:
- master
tags:
- demo
lint_code:
stage: test
script:
- make lint
build:
stage: build
script:
- pwd
- go build .
tags:
- demo
build_image:
stage: deploy
script:
- make build_image
tags:
- demo

3.3.2. Makefile
PROJECT_NAME := "demo"
PKG := "gitlab.chain.cn/ZhangZhongcheng/$(PROJECT_NAME)"
PKG_LIST := $(shell go list ./... | grep -v /vendor/)
GO_FILES := $(shell find . -name '*.go' | grep -v /vendor/ | grep -v _test.go)
test: [size=16] Run unittests[/size]
@go test -v ${PKG_LIST}
lint: [size=16] Lint the files[/size]
@golint ${PKG_LIST}
race: [size=16] Run data race detector[/size]
@go test -race -short ${PKG_LIST}
coverage: [size=16] Generate global code coverage report[/size]
./scripts/coverage.sh;
coverhtml: [size=16] Generate global code coverage report in HTML[/size]
./scripts/coverage.sh html;
build_image:
./scripts/buildDockerImage.sh

3.3.3. coverage.sh
#!/bin/bash
#
# Code coverage generation
COVERAGE_DIR="${COVERAGE_DIR:-coverage}"
PKG_LIST=$(go list ./... | grep -v /vendor/)
# Create the coverage files directory
mkdir -p "$COVERAGE_DIR";
# Create a coverage file for each package
for package in ${PKG_LIST}; do
go test -covermode=count -coverprofile "${COVERAGE_DIR}/${package[size=16]*/}.cov" "$package" ;[/size]
done ;
# Merge the coverage profile files
echo 'mode: count' > "${COVERAGE_DIR}"/coverage.cov ;
tail -q -n +2 "${COVERAGE_DIR}"/*.cov >> "${COVERAGE_DIR}"/coverage.cov ;
# Display the global code coverage
go tool cover -func="${COVERAGE_DIR}"/coverage.cov ;
# If needed, generate HTML report
if [ "$1" == "html" ]; then
go tool cover -html="${COVERAGE_DIR}"/coverage.cov -o coverage.html ;
fi
# Remove the coverage files directory
rm -rf "$COVERAGE_DIR";

3.3.4. buildDockerImage.sh
#!/bin/bash
#检测GOPATH
echo "检测GOPATH"
if [ -z "$GOPATH" ];then
echo "GOPATH 未设定"
exit 1
else
echo "GOPATH=$GOPATH"
fi
#初始化数据
echo "初始化数据"
new_version="1.0.0"
old_version="1.0.0"
golang_version="1.9.2"
app_name="application"
projust_root="demo"
DOCKER_IMAGE_NAME="demo"
REGISTRY_HOST="xxx.xxx.xxx.xxx:5000"
path="/go/src/_/builds/ZhangZhongcheng/demo"
#当前容器更换为旧标签
echo "当前容器更换为旧标签"
docker rmi $REGISTRY_HOST/$DOCKER_IMAGE_NAME:$old_version
# 基于golang:1.9.2镜像启动的容器实例,编译本项目的二进制可执行程序
echo "基于golang:1.9.2镜像启动的容器实例,编译本项目的二进制可执行程序"
cd $path
go build -o $app_name
echo "检测 $app_name 应用"
FILE="$path/$app_name"
if [ -f "$FILE" ];then
echo "$FILE 已就绪"
else
echo "$FILE 应用不存在"
exit 1
fi
#docker构建镜像 禁止在构建上下文之外的路径 添加复制文件
#所以在此可以用命令把需要的文件cp到 dockerfile 同目录内 ,构建完成后再用命令删除
cd $path/scripts
echo "提取构建时需要的文件"
cp ../$app_name $app_name
# 基于当前目录下的Dockerfile构建镜像
echo "基于当前目录下的Dockerfile构建镜像"
echo "docker build -t $REGISTRY_HOST/$DOCKER_IMAGE_NAME:$new_version ."
docker build -t $REGISTRY_HOST/$DOCKER_IMAGE_NAME:$new_version .
# 删除本次生成的可执行文件 以及构建所需要的文件
echo "删除本次生成的可执行文件 以及构建所需要的文件"
rm -rf $app_name
rm -rf ../$app_name
#查看镜像
echo "查看镜像"
docker images | grep $DOCKER_IMAGE_NAME
#推送镜像
echo "推送镜像"
echo "docker push $REGISTRY_HOST/$DOCKER_IMAGE_NAME:$new_version"
docker push $REGISTRY_HOST/$DOCKER_IMAGE_NAME:$new_version
echo "auto deploy"
./automationDeployment.sh $new_version $old_version

3.3.5. automationDeployment.sh
#!/usr/bin/expect
#指定shebang
#设定超时时间为3秒
set ip xxx.xxx.xxx.xxx
set password "xxxxxxx"
set new_version [lindex $argv 0]
set old_version [lindex $argv 1]
spawn ssh root@$ip
expect {
"*yes/no" { send "yes\r"; exp_continue}
"*password:" { send "$password\r" }
}
expect "#*"
send "cd /root/demo/\r"
send "./docker_run_demo.sh $new_version $old_version\r"
expect eof

3.3.6. Dockerfile
FROM golang:1.9.2
#定义环境变量 alpine专用
#ENV TIME_ZONE Asia/Shanghai
ADD application /go/src/demo/
WORKDIR /go/src/demo
ADD run_application.sh /root/
RUN chmod 755 /root/run_application.sh
CMD sh /root/run_application.sh
EXPOSE 8080

3.3.7. run_application.sh
#!/bin/bash
#映射ip
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
cd /go/src/demo/
./application

#4. 结果
以下为部署成功后的截图:
3.png


原文链接:http://www.chairis.cn/blog/article/96

Containerd Server学习 Part 3

田浩浩 发表了文章 • 0 个评论 • 1209 次浏览 • 2018-02-06 17:37 • 来自相关话题

【编者的话】containerd v1.0.0的源码分析,以 docker-containerd --config /var/run/docker/containerd/containerd.toml为入口 ...查看全部
【编者的话】containerd v1.0.0的源码分析,以 docker-containerd --config /var/run/docker/containerd/containerd.toml为入口



### 初始化Snapshot插件

* 位置:`cmd/containerd/builtins_xxx.go`


package main

import (
_ "github.com/llitfkitfk/containerd/snapshots/naive"
)

# `snapshots/naive/naive.go`
func init() {
plugin.Register(&plugin.Registration{
Type: plugin.SnapshotPlugin,
ID: "naive",
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
ic.Meta.Platforms = append(ic.Meta.Platforms, platforms.DefaultSpec())
return NewSnapshotter(ic.Root)
},
})
}


### 初始化Content插件

* 位置:`server/server.go`


plugin.Register(&plugin.Registration{
Type: plugin.ContentPlugin,
ID: "content",
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
ic.Meta.Exports["root"] = ic.Root
return local.NewStore(ic.Root)
},
})



### 初始化Metadata插件

* 位置:`server/server.go`
* 依赖:github.com/boltdb/bolt



plugin.Register(&plugin.Registration{
Type: plugin.MetadataPlugin,
ID: "bolt",
Requires: []plugin.Type{
plugin.ContentPlugin,
plugin.SnapshotPlugin,
},



### grpc服务拦截器

* 位置:`server/server.go`


func interceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
ctx = log.WithModule(ctx, "containerd")
switch info.Server.(type) {
case tasks.TasksServer:
ctx = log.WithModule(ctx, "tasks")
case containers.ContainersServer:
ctx = log.WithModule(ctx, "containers")
case contentapi.ContentServer:
ctx = log.WithModule(ctx, "content")
case images.ImagesServer:
ctx = log.WithModule(ctx, "images")
case grpc_health_v1.HealthServer:
// No need to change the context
case version.VersionServer:
ctx = log.WithModule(ctx, "version")
case snapshotsapi.SnapshotsServer:
ctx = log.WithModule(ctx, "snapshot")
case diff.DiffServer:
ctx = log.WithModule(ctx, "diff")
case namespaces.NamespacesServer:
ctx = log.WithModule(ctx, "namespaces")
case eventsapi.EventsServer:
ctx = log.WithModule(ctx, "events")
case introspection.IntrospectionServer:
ctx = log.WithModule(ctx, "introspection")
case leasesapi.LeasesServer:
ctx = log.WithModule(ctx, "leases")
default:
log.G(ctx).Warnf("unknown GRPC server type: %#v\n", info.Server)
}
return grpc_prometheus.UnaryServerInterceptor(ctx, req, info, handler)
}


# 整理:
github.com/llitfkitfk/containerd/tree/part-3

Containerd Server学习 Part 2

田浩浩 发表了文章 • 0 个评论 • 1397 次浏览 • 2018-02-02 16:54 • 来自相关话题

【编者的话】containerd v1.0.0的源码分析,以 docker-containerd --config /var/run/docker/containerd/containerd.toml为入口,看大神们如何组织Go语言代码 ...查看全部
【编者的话】containerd v1.0.0的源码分析,以 docker-containerd --config /var/run/docker/containerd/containerd.toml为入口,看大神们如何组织Go语言代码

# 分析

### 程序信号处理
* 位置:`cmd/containerd/main.go`


...
var (
signals = make(chan os.Signal, 2048)
ctx = log.WithModule(gocontext.Background(), "containerd")
)
done := handleSignals(ctx, signals, serverC)
...
signal.Notify(signals, handledSignals...)

...

<-done
...


### 初始化服务
* 位置:`cmd/containerd/main.go`


...
server, err := server.New(ctx, config)
if err != nil {
return err
}
...



### 开启debug / metrics / grpc服务
* 位置:`cmd/containerd/main.go`
* 依赖:
github.com/docker/go-metrics
github.com/grpc-ecosystem/go-grpc-prometheus


...
if config.Debug.Address != "" {
l, err := sys.GetLocalListener(config.Debug.Address, config.Debug.UID, config.Debug.GID)
if err != nil {
return errors.Wrapf(err, "failed to get listener for debug endpoint")
}
serve(log.WithModule(ctx, "debug"), l, server.ServeDebug)
}

if config.Metrics.Address != "" {
l, err := net.Listen("tcp", config.Metrics.Address)
if err != nil {
return errors.Wrapf(err, "failed to get listener for metrics endpoint")
}
serve(log.WithModule(ctx, "metrics"), l, server.ServeMetrics)
}
l, err := sys.GetLocalListener(address, config.GRPC.UID, config.GRPC.GID)
if err != nil {
return errors.Wrapf(err, "failed to get listener for main endpoint")
}
serve(log.WithModule(ctx, "grpc"), l, server.ServeGRPC)
...




# 初始化服务解析

### 载入插件
* 位置:`server/server.go`

plugins, err := loadPlugins(config)
if err != nil {
return nil, err
}


### 初始化 grpc服务 和 传输服务
* 位置:`server/server.go`


type Server struct {
rpc *grpc.Server
events *exchange.Exchange
}



### grpc服务
* 位置:`server/server.go`
* 依赖:
google.golang.org/grpc
github.com/grpc-ecosystem/go-grpc-prometheus


rpc := grpc.NewServer(
grpc.UnaryInterceptor(interceptor),
grpc.StreamInterceptor(grpc_prometheus.StreamServerInterceptor),
)



### 传输服务
* 位置:`events/exchange/exchange.go`
* 作用:负责传播事件
* 依赖:github.com/docker/go-events


// Exchange broadcasts events
type Exchange struct {
broadcaster *goevents.Broadcaster
}


# 整理:
github.com/llitfkitfk/containerd/tree/part-2

Containerd Server学习 Part 1

田浩浩 发表了文章 • 0 个评论 • 1426 次浏览 • 2018-01-31 21:56 • 来自相关话题

【编者的话】containerd v1.0.0的源码分析,以` docker-containerd --config /var/run/docker/containerd/containerd.toml`为入口,看大神们如何组织Go语言代码 ...查看全部
【编者的话】containerd v1.0.0的源码分析,以` docker-containerd --config /var/run/docker/containerd/containerd.toml`为入口,看大神们如何组织Go语言代码

containerd.io
github: containerd v1.0.0

# 准备:
```
go get -v github.com/containerd/containerd
cd $GOPATH/src/github.com/containerd/containerd
git checkout v1.0.0 -b v1.0.0
```
# 分析

### 命令行
* 位置:`cmd/containerd/main.go`
* 作用:定义containerd 命令行相关参数与配置文件
* 依赖:github.com/urfave/cli


package main

import (
"fmt"
"os"

"github.com/containerd/containerd/version"
"github.com/urfave/cli"
)

const usage = `
__ _ __
_________ ____ / /_____ _(_)___ ___ _________/ /
/ ___/ __ \/ __ \/ __/ __ ` + "`" + `/ / __ \/ _ \/ ___/ __ /
/ /__/ /_/ / / / / /_/ /_/ / / / / / __/ / / /_/ /
\___/\____/_/ /_/\__/\__,_/_/_/ /_/\___/_/ \__,_/

high performance container runtime
`

func main() {
app := cli.NewApp()
app.Name = "containerd"
app.Version = version.Version
app.Usage = usage
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "config,c",
Usage: "path to the configuration file",
Value: defaultConfigPath,
},
}
app.Commands = []cli.Command{}
app.Action = func(context *cli.Context) error {
return nil
}
if err := app.Run(os.Args); err != nil {
fmt.Fprintf(os.Stderr, "containerd: %s\n", err)
os.Exit(1)
}
}


### 配置文件解析
* 位置:`server/config.go`
* 作用:解析配置文件相关参数
* 依赖:
github.com/BurntSushi/toml
github.com/pkg/errors


// LoadConfig loads the containerd server config from the provided path
func LoadConfig(path string, v *Config) error {
if v == nil {
return errors.Wrapf(errdefs.ErrInvalidArgument, "argument v must not be nil")
}
md, err := toml.DecodeFile(path, v)
if err != nil {
return err
}
v.md = md
return nil
}



#### 添加参数
* 位置:`cmd/containerd/main.go`
* 作用:应用参数到相关配置


func applyFlags(context [i]cli.Context, config [/i]server.Config) error {
// the order for config vs flag values is that flags will always override
// the config values if they are set
if err := setLevel(context, config); err != nil {
return err
}
for _, v := range []struct {
name string
d *string
}{
{
name: "root",
d: &config.Root,
},
{
name: "state",
d: &config.State,
},
{
name: "address",
d: &config.GRPC.Address,
},
} {
if s := context.GlobalString(v.name); s != "" {
*v.d = s
}
}
return nil
}


#### 日志
* 位置:`log/context.go`
* 作用:应用日志
* 依赖: github.com/sirupsen/logrus


...
var ctx = log.WithModule(gocontext.Background(), "containerd")
...
log.G(ctx).WithFields(logrus.Fields{
"version": version.Version,
"revision": version.Revision,
}).Info("starting containerd")
...

# log/context.go
func WithModule(ctx context.Context, module string) context.Context {
parent := GetModulePath(ctx)
if parent != "" {
if path.Base(parent) == module {
return ctx
}
module = path.Join(parent, module)
}

ctx = WithLogger(ctx, GetLogger(ctx).WithField("module", module))
return context.WithValue(ctx, moduleKey{}, module)
}


# 整理:
* github.com/llitfkitfk/containerd/tree/part-1
* jianshu

想请问下golang的编辑工具用的是什么

小泽巴比 回复了问题 • 4 人关注 • 3 个回复 • 2545 次浏览 • 2017-07-31 16:05 • 来自相关话题

扒一扒Rancher社区中的小工具

Rancher 发表了文章 • 1 个评论 • 2647 次浏览 • 2017-01-19 09:27 • 来自相关话题

与Linux、OpenStack等成熟的技术社区相比,Rancher社区还是处于初级发展阶段,一个技术社区的成败并不是单纯的代码贡献,而学习文档的数量和代码管理作业流程也是非常重要的。如何让怀揣不同需求的工程师都能在社区中快速找到相应的解决方案,这就需要大家协 ...查看全部
与Linux、OpenStack等成熟的技术社区相比,Rancher社区还是处于初级发展阶段,一个技术社区的成败并不是单纯的代码贡献,而学习文档的数量和代码管理作业流程也是非常重要的。如何让怀揣不同需求的工程师都能在社区中快速找到相应的解决方案,这就需要大家协同合作共同促进社区发展与完善。除了我们所熟知的Rancher & RancherOS,Rancher Labs的开发团队在实践中提炼了很多实用的小工具,这些小工具虽然并不会左右Rancher发展的大局,但是在项目标准化和开发效率上给团队带来巨大的便捷。这次主要是带着大家一起来认识一下这些小工具。

Golang包管理工具-Trash

项目地址:https://github.com/rancher/trash

目前主流的编程语言 Python、Ruby、Java、Php 等已经把包管理的流程设计的犹如行云流水般流畅,一般情况下开发者是不需要操心类库包依赖管理以及升级、备份、团队协作的。Golang在1.5版本开始,官方开始引入包管理的设计,加了 vendor目录来支持本地包管理依赖,但是需要特殊设置GO15VENDOREXPERIMENT=1,在1.6时代这个特性已经是默认的了。可是vendor并没有统一的版本号管理功能,只是额外提供了project内包的依赖路径。于是Trash这个工具就应运而生了,Trash的使用非常简单,只需要有一份依赖包的描述文件即可。

描述文件 trash.conf 支持两种格式,普通方式和YAML方式,可以直接在其中描述依赖库的远程地址、版本号等,一个简单的例子(我这里使用普通格式):



然后在根目录执行trash,即可获得相关版本的依赖包:



非常轻量级,非常简洁。

Golang编译工具-Dapper

项目地址:https://github.com/rancher/dapper

我们在编译golang执行程序的时候,因为涉及到协作开发,这就会碰到一个问题,就是如何保证编译环境的一致性。环境一致性最好的办法就是使用容器技术,Dapper就是利用Docker build镜像的过程中可以执行各种命令生成容器的原理。只需在项目的根目录下创建Dockerfile.dapper 文件,这是一个参考Dockerfile标准的文件,执行 dapper 命令即可按照约定规则生成最终的执行程序,通过这种方式统一的编译环境。

几乎所有的Rancher项目都是基于Dapper来编译的,随意打开一个项目比如rancher-dns就可以看到 Dockerfile.dapper 文件:



DAPPER_SOURCE 指定了容器内的源码路径
DAPPER_OUTPUT 指定了编译后的输出路径,bin dist 目录会自动在项目根目录下创建
DAPPER_DOCKER_SOCKET 设置为True相当于docker run -v /var/run/docker.sock:/var/run/docker.sock ...
DAPPER_ENV 相当于docker run -e TAG -e REPO ...

有一点需要注意的是,目前Dapper装载源码有两种方式bind和cp,bind就是直接mount本地的源码路径,但是如果使用remote docker daemon方式那就得使用cp模式了。

Golang项目标准化工具 go-skel

项目地址:https://github.com/rancher/go-skel

介绍了包管理工具和打包编译工具,如果我们在创建一个golang项目时能把这两个工具整合起来一起使用,那就太赞了。go-skel就是提供了这样一个便利,我们直接来demo一下。

clone一份go-skel的源码,创建一个rancher-tour(./skel.sh rancher-tour)的项目:



执行完毕后,会创建一个标准的项目,包含了dapper和trash这两个工具,同时定义了一份Makefile,我们可以通过make命令来简化操作:



比如我们要执行一个ci操作,那就可以直接运行 make ci,自动运行单元测试,并在bin目录下生成最终的可执行程序:



标准项目已经创建了一些初始化代码,集成了 github.com/urfave/cli ,所以我们可以执行执行rancher-tour:



微服务辅助小工具 Giddyup

项目地址:https://github.com/cloudnautique/giddyup

一个传统的服务在容器化的过程中,通常我们会将其拆分成多个微服务,充分展现每个容器只做好一件事情这样的哲学。那么就会出现我们在Rancher中看到的sidekick容器,数据卷容器,专门负责更新配置信息的容器等等。

实际应用中我们会遇到一些问题,比如这些微服务容器的启动是无序的,无序启动会导致微服务之间可能出现连接失败,进而导致整个服务不可用;再比如我们如何来判定依赖的微服务已经正常启动,这可能需要一个health check的服务端口。giddyup就是简化这种微服务检查工作的利器,它都能做些什么呢:
Get connection strings from DNS or Rancher Metadata.
Determine if your container is the leader in the service.
Proxy traffic to the leader
Wait for service to have the desired scale.
Get the scale of the service
Get Managed-IP of the container (/self/container/primary_ip)

我们还是创建stack并在其中创建service来体验一下giddyup的部分功能,service中包含两个容器分别为主容器main和sidekick容器conf,他们共享网络栈:



我们可以在conf内启动giddyup health,会启动一个监听1620端口的http服务,服务路径是/ping:



此时我们可以在其他服务的容器内查看这个服务 health port的健康状态,通过giddyup ip stringify获取服务地址,使用giddyup probe可以查看相关状态:



可以看到probe返回了OK的信息,说明giddy/main的health port是正常的,如果我们把giddyup health的端口停掉,giddyup probe会如何?



除了开启health port功能外,还可以sleep等待service内所有容器都启动,比如刚才的service,我在main上做wait操作,同时在UI上对service扩容,可以看到为了等待完成扩容差不多sleep了3s:



更多功能可以去看一看giddyup项目的README文档,另外也可以看看catalog中的一些项目是怎么使用giddyup的,参考:https://github.com/rancher/catalog-dockerfiles

Rancher CLI 工具

项目地址: https://github.com/rancher/cli

现在我们管理Rancher的方式包括UI、API方式,为了能够和其他工具更好的融合Rancher开发了CLI工具,可以非常轻量级的嵌入到其他工具中。CLI将会在Rancher 1.2-pre2版本中正式放出,兼容性上目前来看没有支持老版本的计划,所以在老版本上出现各种问题也是正常的。

直接在 https://github.com/rancher/cli/releases 下载最新版本即可试用,将rancher cli放到PATH中,正式使用前需要初始化:



然后便可以执行各种犀利的操作,比如针对某个service进行scale操作,这比之前需要用rancher-compose cli要简洁的多:



再比如可以实时监听rancher events,可以即时查看Rancher中正在发生的各种任务,对协助排查问题,分析系统运行情况都非常有用:



后语

小工具中内含着大智慧,工具文化是工程师Team高效协作的重要标志,高质量的工具能让项目开发有事半功倍之效,其背后也蕴藏着深厚的团队文化理念,就是不计项目KPI利用个人业余时间为团队做贡献的和谐氛围。其实国内很多互联网公司都是有专门设立工具开发工程师的岗位,对工具带来的生产效率提升,其重视程度不言而喻!

原文来源:Rancher Labs

开源第二弹!数人云Mesos调度器Swan来啦

Dataman数人科技 发表了文章 • 0 个评论 • 1669 次浏览 • 2016-11-10 15:17 • 来自相关话题

继数人云容器管理面板Crane开源之后,小数有一个好消息告诉大家,数人云Mesos调度器Swan也加入了开源的大家庭!从此Crane不再寂寞,和Swan相亲相爱。开源的脚步从不停歇,下一个会是谁呢? ...查看全部

数人云容器管理面板Crane开源之后,小数有一个好消息告诉大家,数人云Mesos调度器Swan也加入了开源的大家庭!从此Crane不再寂寞,和Swan相亲相爱。开源的脚步从不停歇,下一个会是谁呢?


微信截图_20161110151714.png


Swan,为天鹅,优雅的代名词。

刚刚诞生于数人云工程师的手中的Swan,尚处于开源项目的早期,还是一只跌跌撞撞的丑小鸭,未能完全展现天鹅优雅的模样。比它早出生两个月的哥哥Crane已经在开源社区的帮助下成长许多,小数相信在大家的关怀下,Swan也会很快成长为一只真正的天鹅^ v ^

Swan基于Mesos Restful API编写的应用调度框架,可以帮助用户轻松发布应用,实现应用的滚动更新,并根据用户指定的策略做应用的健康检测和故障转移。

未来,数人云Swan团队还将努力实现调度策略、高可用服务发现、网络管理、编排,以及任务抢占等功能。


主要功能

  • 应用发布:发布应用支持应用实例名称固定,便于监控和做服务发现。
  • 应用扩缩:支持手动扩缩,扩缩过程中保持实例ID连续。
  • 滚动更新:可以指定更新的实例数目分步更新,也可以全量更新。滚动更新策略可配。
  • 版本回滚:支持更新过程中手动回滚,任意一个实例更新失败后自动回滚。
  • 版本管理:支持应用多版本管理。
  • 健康检查:支持实例级别的健康检测,可配置健康检测策略。
  • 自动容错:可配置自动容错策略,根据策略自动恢复失败的实例。
  • 优雅终止:扩缩和滚动更新过程中,支持优雅终止实例。


不多说啦,快跟随小数的步伐来一看究竟吧!
Fork me on GitHub!
https://github.com/Dataman-Cloud/swan


数人云现有企业版产品数人云操作系统和多个开源项目。数人云操作系统针对企业客户,帮助传统企业实现IT业务转型,更好地应对业务变化;开源项目容器管理面板Crane针对开发者,强调简单易用,Mesos调度器Swan用于Mesos环境应用管理。

Docker与Golang的巧妙结合

cyeaaa 发表了文章 • 0 个评论 • 25334 次浏览 • 2016-09-22 20:07 • 来自相关话题

【编者的话】这是一个展示在使用Go语言时如何让Docker更有用的提示与技巧的简辑。例如,如何使用不同版本的Go工具链来编译Go代码,如何交叉编译到不同的平台(并且测试结果!),或者如何制作真正小的容器镜像。 下面的文章假定你已经安装 ...查看全部
【编者的话】这是一个展示在使用Go语言时如何让Docker更有用的提示与技巧的简辑。例如,如何使用不同版本的Go工具链来编译Go代码,如何交叉编译到不同的平台(并且测试结果!),或者如何制作真正小的容器镜像。

下面的文章假定你已经安装了Docker。不必是最新版本(这篇文章不会使用Docker任何花哨的功能)。
# 没有go的Go
...意思是:“不用安装`go`就能使用Go”

如果你写Go代码,或者你对Go语言有一点点兴趣,你肯定要安装了Go编译器和Go工具链,所以你可能想知道:“重点是什么?”;但有些情况下,你想不安装`Go`就来编译Go。

  • 机器上依旧有老版本Go 1.2(你不能或不想更新),不得不使用这个代码库,需要一个高版本的工具链。
  • 想使用Go1.5的交叉编译功能(例如,确保能从一个Linux系统创建操作系统X的二进制文件)。
  • 想拥有多版本的Go,但不想完全弄乱系统。
  • 想100%确定项目和它所有的依赖,下载,建立和运行在一个纯净的系统上。
如果遇到上述情况,找Docker来解决!## 在容器里编译一个程序当你安装了Go,你可以执行`go get -v github.com/user/repo`来下载,创建和安装一个库。(`-v`只是信息显示,如果你喜欢工具链快速和静默地运行,可以将它移除!)你也可以执行`go get github.com/user/repo/...`来下载,创建和安装那个repo(包括库和二进制文件)里面所有的东西。我们可以在一个容器里面这样做!试试这个:
docker run golang go get -v github.com/golang/example/hello/...
这将拉取golang镜像(除非你已经有了,那它会马上启动),并且创建一个基于它的容器。在那个容器里,go会下载一个“hello world”的例子,创建它,安装它。但它会把它安装到这个容器里……我们现在怎么运行那个程序呢?##在容器里运行程序一个办法是提交我们刚刚创建的容器,即,打包它到一个新的镜像:
docker commit $(docker ps -lq) awesomeness
注意:`docker ps –lq`输出最后一个执行的容器的ID(只有ID!)。如果你是机器的唯一用户,并且你从上一个命令开始没有创建另一个容器,那这个容器就是你刚刚创建的“hello world”的例子。现在,可以用刚刚构建的镜像创建容器来运行程序:
docker run awesomeness hello
输出会是`Hello, Go examples!`。## 闪光点当用`docker commit`构建镜像时,可以用`--change`标识指定任意Dockerfile命令。例如,可以使用一个`CMD`或者`ENTRYPOINT`命令以便`docker run awesomeness`自动执行hello。##在一次性容器上运行如果不想创建额外的镜像只想运行这个Go程序呢?使用:
docker run --rm golang sh -c \"go get github.com/golang/example/hello/... && exec hello"
等等,那些花哨的东西是什么?
  • `--rm` 告诉Docker CLI一旦容器退出,就自动发起一个`docker rm`命令。那样,不会留下任何东西。
  • 使用shell逻辑运算符`&&`把创建步骤(`go get`)和执行步骤(`exec hello`)联接在一起。如果不喜欢shell,`&&`意思是“与”。它允许第一部分`go get...`,并且如果(而且仅仅是如果!)那部分运行成功,它将执行第二部分(`exec hello`)。如果你想知道为什么这样:它像一个懒惰的`and`计算器,只有当左边的值是`true`才计算右边的。
  • 传递命令到`sh –c`,因为如果是简单的做`docker run golang "go get ... && hello"`,Docker将试着执行名为`go SPACE get SPACE etc`的程序。并且那不会起作用。因此,我们启动一个shell,并让shell执行命令序列。
  • 使用`exec hello`而不是`hello`:这将使用hello程序替代当前的进程(我们刚才启动的shell)。这确保`hello`在容器里是PID 1。而不是shell的是PID 1而`hello`作为一个子进程。这对这个微小的例子毫无用处,但是当运行更有用的程序,这将允许它们正确地接收外部信号,因为外部信号是发送给容器里的PID 1。你可能会想,什么信号啊?好的例子是`docker stop`,发送`SIGTERM`给容器的PID 1。
##使用不同版本的Go当使用`golang`镜像,Docker扩展为`golang:latest,`将(像你所猜的)映射到Docker Hub上的最新可用版本。如果想用一个特定的Go版本,很容易:在镜像名字后面用那个版本做标签指定它。例如,想用Go 1.5,修改上面的例子,用`golang:1.5`替换`golang`:
docker run --rm golang:1.5 sh -c \ "go get github.com/golang/example/hello/... && exec hello"
你能在Docker Hub的Golang镜像页面上看到所有可用的版本(和变量)。##在系统上安装好了,如果想在系统上运行编译好的程序,而不是一个容器呢?我们将复制这个编译了的二进制文件到容器外面。注意,仅当容器架构和主机架构匹配的时候,才会起作用;换言之,如果在Linux上运行Docker。(我排除的可能是运行Windows容器的人!)最容易在容器外获得二进制文件的方法是映射`$GOPATH/bin`目录到一个本地目录,在`golang`容器里,`$GOPATH`是`/go.`所以我们可以如下操作:
docker run -v /tmp/bin:/go/bin \ golang go get github.com/golang/example/hello/... /tmp/bin/hello
如果在Linux上,将看到`Hello, Go examples!`消息。但如果是,例如在Mac上,可能会看到:
-bash:/tmp/test/hello: cannot execute binary file
我们又能做什么呢?##交叉编译Go 1.5具备优秀的开箱即用交叉编译能力,所以如果你的容器操作系统和/或架构和你的系统不匹配,根本不是问题!开启交叉编译,需要设置`GOOS`和/或`GOARCH`。例如,假设在64位的Mac上:
docker run -e GOOS=darwin -e GOARCH=amd64 -v /tmp/crosstest:/go/bin \ golang go get github.com/golang/example/hello/...
交叉编译的输出不是直接在`$GOPATH/bin`,而是在`$GOPATH/bin/$GOOS_$GOARCH.`。换言之,想运行程序,得执行`/tmp/crosstest/darwin_amd64/hello.`。##直接安装到$PATH如果在Linux上,甚至可以直接安装到系统bin目录:
docker run -v /usr/local/bin:/go/bin \ golang get github.com/golang/example/hello/...
然而,在Mac上,尝试用`/usr`作为一个卷将不能挂载Mac的文件系统到容器。会挂载Moby VM(小Linux VM藏在工具栏Docker图标的后面)的`/usr`目录。(译注:目前Docker for Mac版本可以自定义设置挂载路径)但可以使用`/tmp`或者在你的home目录下的什么其它目录,然后从这里复制。# 创建依赖镜像我们用这种技术产生的Go二进制文件是静态链接的。这意味着所有需要运行的代码包括所有依赖都被嵌入了。动态链接的程序与之相反,不包含一些基本的库(像“libc”)并且使用系统范围的复制,是在运行时确定的。这意味着可以在容器里放弃Go编译好的程序,没有别的,并且它会运行。我们试试!##scratch镜像Docker生态系统有一个特殊的镜像:`scratch.`这是一个空镜像。它不需要被创建或者下载,因为定义的就是空的。给新的Go依赖镜像创建一个新的空目录。在这个新目录,创建下面的Dockerfile:
FROM scratch COPY ./hello /hello ENTRYPOINT ["/hello"]
这意味着:从scratch开始(一个空镜像),增加`hello`文件到镜像的根目录,*定义`hello`程序为启动这个容器后默认运行的程序。然后,产生`hello`二进制文件如下:
docker run -v $(pwd):/go/bin --rm \ golang go get github.com/golang/example/hello/...
注意:不需要设置`GOOS`和`GOARCH`,正因为,想要一个运行在Docker容器里的二进制文件,不是在主机上。所以不用设置这些变量!然后,创建镜像:
docker build -t hello .
测试它:
docker run hello
(将显示“Hello, Go examples!”)最后但不重要,检查镜像的大小:
docker images hello
如果一切做得正确,这个镜像大约2M。相当好!##构建东西而不推送到Github当然,如果不得不推送到GitHub,每次编译都会浪费很多时间。想在一个代码段上工作并在容器中创建它时,可以在`golang`容器里挂载一个本地目录到`/go`。所以`$GOPATH`是持久调用:`docker run -v $HOME/go:/go golang ....`但也可以挂载本地目录到特定的路径上,来“重载”一些包(那些在本地编辑的)。这是一个完整的例子:
# Adapt the two following environment variables if you are not running on a Mac export GOOS=darwin GOARCH=amd64 mkdir go-and-docker-is-love cd go-and-docker-is-love git clone git://github.com/golang/example cat example/hello/hello.go sed -i .bak s/olleH/eyB/ example/hello/hello.go docker run --rm \ -v $(pwd)/example:/go/src/github.com/golang/example \ -v $(pwd):/go/bin/${GOOS}_${GOARCH} \ -e GOOS -e GOARCH \golang go get github.com/golang/example/hello/... ./hello # Should display "Bye, Go examples!" 
# 网络包和CGo的特殊情况进入真实的Go代码世界前,必须承认的是:在二进制文件上有一点点偏差。如果在使用CGo,或如果在使用`net`包,Go链接器将生成一个动态库。这种情况下,`net`包(里面确实有许多有用的Go程序!),罪魁祸首是DNS解析。大多数系统都有一个花哨的,模块化的名称解析系统(像名称服务切换),它依赖于插件,技术上,是动态库。默认地,Go将尝试使用它;这样,它将产生动态库。我们怎么解决?##重用另一个版本的libc一个解决方法是用一个基础镜像,有那些程序功能所必需的库。几乎任何“正规”基于GNU libc的Linux发行版都能做到。所以,例如,使用`FROM debian`或`FROM fedora`,替代`FROM scratch`。现在结果镜像会比原来大一些;但至少,大出来的这一点将和系统里其它镜像共享。注意:这种情况不能使用Alpine,因为Alpine是使用musl库而不是GNU libc。##使用自己的libc另一个解决方案是像做手术般地提取需要的文件,用`COPY`替换容器里的。结果容器会小。然而,这个提取过程困难又繁琐,太多更深的细节要处理。如果想自己看,看看前面提到的`ldd`和名称服务切换插件。##用netgo生成静态二进制文件我们也可以指示Go不用系统的libc,用本地DNS解析代替Go的`netgo`。要使用它,只需在`go get`选项加入`-tags netgo -installsuffix netgo`。
  • `-tags netgo`指示工具链使用`netgo`。
  • `-installsuffix netgo`确保结果库(任何)被一个不同的,非默认的目录所替代。如果做多重`go get`(或`go build`)调用,这将避免代码创建和用不用netgo之间的冲突。如果像目前我们讲到的这样,在容器里创建,是完全没有必要的。因为这个容器里面永远没有其他Go代码要编译。但它是个好主意,习惯它,或至少知道这个标识存在。
# SSL证书的特殊情况还有一件事,你会担心,你的代码必须验证SSL证书;例如,通过HTTPS联接外部API。这种情况,需要将根证书也放入容器里,因为Go不会捆绑它们到二进制文件里。##安装SSL证书再次,有很多可用的选择,但最简单的是使用一个已经存在的发布里面的包。Alpine是一个好的选择,因为它非常小。下面的`Dockerfile`将给你一个小的基础镜像,但捆绑了一个过期的跟证书:
FROM alpine:3.4RUN apk add --no-cache ca-certificates apache2-utils
来看看吧,结果镜像只有6MB!注意:`--no-cache`选项告诉`apk`(Alpine包管理器)从Alpine的镜像发布上获取可用包的列表,不保存在磁盘上。你可能会看到Dockerfiles做这样的事`apt-get update && apt-get install ... && rm -rf /var/cache/apt/*`;这实现了(即在最终镜像中不保留包缓存)与一个单一标志相当的东西。一个附加的回报:把你的应用程序放入基于Alpine镜像的容器,让你获得了一堆有用的工具。如果需要,现在你可以吧shell放入容器并在它运行时做点什么。# 打包 我们看到Docker如何帮助我们在干净独立的环境里编译Go代码;如何使用不同版本的Go工具链;以及如何在不同的操作系统和平台之间交叉编译。我们还看到Go如何帮我们给Docker创建小的,容器依赖镜像,并且描述了一些静态库和网络依赖相关的微妙联系(没别的意思)。除了Go是真的适合Docker项目这个事实,我们希望展示给你的是,Go和Docker如何相互借鉴并且一起工作得很好!##致谢这最初是在2016年GopherCon骇客日提出的。我要感谢所有的校对材料、提出建议和意见让它更好的人,包括但不局限于:
所有的错误和拼写错误都是我自己的;所有的好东西都是他们的!

原文链接:Docker + Golang = <3(翻译:陈晏娥 审校:田浩浩

Golang基于GitLab CI/CD部署方案

大卫 发表了文章 • 0 个评论 • 1886 次浏览 • 2018-10-14 12:01 • 来自相关话题

持续集成(Continuous Integration)是一种软件开发实践,即团队开发成员经常集成它们的工作,通过每个成员每天至少集成一次,也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽早地发现集成错误 ...查看全部
持续集成(Continuous Integration)是一种软件开发实践,即团队开发成员经常集成它们的工作,通过每个成员每天至少集成一次,也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽早地发现集成错误。

持续部署(Continuous Deployment)是通过自动化的构建、测试和部署循环来快速交付高质量的产品。某种程度上代表了一个开发团队工程化的程度,毕竟快速运转的互联网公司人力成本会高于机器,投资机器优化开发流程化相对也提高了人的效率,让 engineering productivity 最大化。
#1. 环境准备
本次试验是基于CentOS 7.3,Docker 17.03.2-ce环境下的。Docker的安装这里就不赘述了,提供官方链接:Get Docker CE for CentOS
##1.1. Docker启动GitLab
启动命令如下:
docker run --detach \
--hostname gitlab.chain.cn \
--publish 8443:443 --publish 8080:80 --publish 2222:22 \
--name gitlab \
--restart always \
--volume /Users/zhangzc/gitlab/config:/etc/gitlab \
--volume /Users/zhangzc/gitlab/logs:/var/log/gitlab \
--volume /Users/zhangzc/gitlab/data:/var/opt/gitlab \
gitlab/gitlab-ce

port,hostname,volume根据具体情况具体设置。
##1.2. Docker启动gitlab-runner
启动命令如下:
sudo docker run -d /
--name gitlab-runner /
--restart always /
-v /Users/zhangzc/gitlab-runner/config:/etc/gitlab-runner /
-v /Users/zhangzc/gitlab-runner/run/docker.sock:/var/run/docker.sock /
gitlab/gitlab-runner:latest

volume根据具体情况具体设置。
##1.3. 用于集成部署的镜像制作
我们的集成和部署都需要放在一个容器里面进行,所以,需要制作一个镜像并安装一些必要的工具,用于集成和部署相关操作。目前我们的项目都是基于Golang 1.9.2的,这里也就基于Golang 1.9.2的镜像制定一个特定的镜像。

Dockerfile内容如下:
# Base image: https://hub.docker.com/_/golang/
FROM golang:1.9.2
USER root
# Install golint
ENV GOPATH /go
ENV PATH ${GOPATH}/bin:$PATH
RUN mkdir -p /go/src/golang.org/x
RUN mkdir -p /go/src/github.com/golang
COPY source/golang.org /go/src/golang.org/x/
COPY source/github.com /go/src/github.com/golang/
RUN go install github.com/golang/lint/golint
# install docker
RUN curl -O https://get.docker.com/builds/Linux/x86_64/docker-latest.tgz \
&& tar zxvf docker-latest.tgz \
&& cp docker/docker /usr/local/bin/ \
&& rm -rf docker docker-latest.tgz
# install expect
RUN apt-get update
RUN apt-get -y install tcl tk expect

其中Golint是用于Golang代码风格检查的工具。

Docker是由于需要在容器里面使用宿主的Docker命令,这里就需要安装一个Docker的可执行文件,然后在启动容器的时候,将宿主的 /var/run/docker.sock 文件挂载到容器内的同样位置。

expect是用于SSH自动登录远程服务器的工具,这里安装改工具是为了可以实现远程服务器端部署应用。

另外,在安装Golint的时候,是需要去golang.org下载源码的,由于墙的关系,go get命令是执行不了的。为了处理这个问题,首先通过其他渠道先下载好相关源码,放到指定的路径下,然后copy到镜像里,并执行安装即可。

下面有段脚本是用于生成镜像的:
#!/bin/bash
echo "提取构建镜像时需要的文件"
source_path="source"
mkdir -p $source_path/golang.org
mkdir -p $source_path/github.com
cp -rf $GOPATH/src/golang.org/x/lint $source_path/golang.org/
cp -rf $GOPATH/src/golang.org/x/tools $source_path/golang.org/
cp -rf $GOPATH/src/github.com/golang/lint $source_path/github.com
echo "构建镜像"
docker build -t go-tools:1.9.2 .
echo "删除构建镜像时需要的文件"
rm -rf $source_path

生成镜像后,推送到镜像仓库,并在gitlab-runner的服务器上拉取该镜像。

本次试验的GitLab和gitlab-runner是运行在同一服务器的Docker下的
#2. Runner注册及配置
##2.1. 注册
环境准备好后,在服务器上执行以下命令,注册Runner:
docker exec -it gitlab-runner gitlab-ci-multi-runner register

按照提示输入相关信息:
Please enter the gitlab-ci coordinator URL:
# gitlab的url, 如:https://gitlab.chain.cn/
Please enter the gitlab-ci token for this runner:
# gitlab->你的项目->settings -> CI/CD ->Runners settings
Please enter the gitlab-ci description for this runner:
# 示例:demo-test
Please enter the gitlab-ci tags for this runner (comma separated):
# 示例:demo
Whether to run untagged builds [true/false]:
# true
Please enter the executor: docker, parallels, shell, kubernetes, docker-ssh, ssh, virtualbox, docker+machine, docker-ssh+machine:
# docker
Please enter the default Docker image (e.g. ruby:2.1):
# go-tools:1.9.2(之前自己制作的镜像)

1.png

成功后,可以看到GitL ab->你的项目->Settings -> CI/CD ->Runners settings页面下面有以下内容:
2.png

##2.2. 配置
注册成功之后,还需要在原有的配置上做一些特定的配置,如下:
[[runners]]
name = "demo-test"
url = "https://gitlab.chain.cn/"
token = "c771fc5feb1734a9d4df4c8108cd4e"
executor = "docker"
[runners.docker]
tls_verify = false
image = "go-tools:1.9.2"
privileged = false
disable_cache = false
volumes = ["/var/run/docker.sock:/var/run/docker.sock"]
extra_hosts = ["gitlab.chain.cn:127.0.0.1"]
network_mode = "host"
pull_policy = "if-not-present"
shm_size = 0
[runners.cache]

这里先解释下gitlab-runner的流程吧,gitlab-runner在执行的时候,会根据上面的配置启动一个容器,即配置中的go-tools:1.9.2,其中所有的启动参数都会在[runners.docker]节点下配置好,包括挂载啊,网络啊之类的。容器启动成功之后,会使用这个容器去GitLab上pull代码,然后根据自己定义的规则进行检验,全部检测成功之后便是部署了。

volumes:是为了在容器中可以执行宿主机的Docker命令。

extra_hosts:给GitLab添加个host映射,映射到127.0.0.1

network_mode:令容器的网络与宿主机一致,只有这样才能通过127.0.0.1访问到GitLab。

pull_policy:当指定的镜像不存在的话,则通过docker pull拉取。
#3. 定义规则
在GitLab项目根目录创建.gitlab-ci.yml文件,填写Runner规则,具体语法课参考官方文档:https://docs.gitlab.com/ee/ci/yaml/。
##3.1. Go集成命令
下面介绍几个Golang常见的集成命令。

包列表,正如在官方文档中所描述的那样,Go项目是包的集合。下面介绍的大多数工具都将使用这些包,因此我们需要的第一个命令是列出包的方法。我们可以用go list子命令来完成:
go list ./...

请注意,如果我们要避免将我们的工具应用于外部资源,并将其限制在我们的代码中。 那么我们需要去除vendor目录,命令如下:
go list ./... | grep -v /vendor/

单元测试,这些是您可以在代码中运行的最常见的测试。每个.go文件需要一个能支持单元测试的_test.go文件。可以使用以下命令运行所有包的测试:
go test -short $(go list ./... | grep -v /vendor/)

数据竞争,这通常是一个难以逃避解决的问题,Go工具默认具有(但只能在Linux/amd64、FreeBSD/amd64、Darwin/amd64和Windows/amd64上使用):
go test -race -short $(go list . /…| grep - v /vendor/)

代码覆盖,这是评估代码的质量的必备工具,并能显示哪部分代码进行了单元测试,哪部分没有。

要计算代码覆盖率,需要运行以下脚本:
PKG_LIST=$(go list ./... | grep -v /vendor/)
for package in ${PKG_LIST}; do
go test -covermode=count -coverprofile "cover/${package[size=16]*/}.cov" "$package" ;[/size]
done
tail -q -n +2 cover/*.cov >> cover/coverage.cov
go tool cover -func=cover/coverage.cov

如果我们想要获得HTML格式的覆盖率报告,我们需要添加以下命令:
go tool cover -html=cover/coverage.cov -o coverage.html

构建,最后一旦代码经过了完全测试,我们要对代码进行编译,从而构建可以执行的二进制文件。
go build .

linter,这是我们在代码中使用的第一个工具:linter。它的作用是检查代码风格/错误。这听起来像是一个可选的工具,或者至少是一个“不错”的工具,但它确实有助于在项目上保持一致的代码风格。

linter并不是Go本身的一部分,所以如果要使用,你需要手动安装它(之前的go-tools镜像我们已经安装过了)。

使用方法相当简单:只需在代码包上运行它(也可以指向. go文件):
$ golint -set_exit_status $(go list ./... | grep -v /vendor/)

注意-set_exit_status选项。 默认情况下,Golint仅输出样式问题,并带有返回值(带有0返回码),所以CI不认为是出错。 如果指定了-set_exit_status,则在遇到任何样式问题时,Golint的返回码将不为0
##3.2. Makefile
如果我们不想在.gitlab-ci.yml文件中写的太复杂,那么我们可以把持续集成环境中使用的所有工具,全部打包在Makefile中,并用统一的方式调用它们。

这样的话,.gitlab-ci.yml文件就会更加简洁了。当然了,Makefile同样也可以调用*.sh脚本文件。
##3.3. 配置示例
3.3.1. .gitlab-ci.yml
image: go-tools:1.9.2
stages:
- build
- test
- deploy
before_script:
- mkdir -p /go/src/gitlab.chain.cn/ZhangZhongcheng /go/src/_/builds
- cp -r $CI_PROJECT_DIR /go/src/gitlab.chain.cn/ZhangZhongcheng/demo
- ln -s /go/src/gitlab.chain.cn/ZhangZhongcheng /go/src/_/builds/ZhangZhongcheng
- cd /go/src/_/builds/ZhangZhongcheng/demo
unit_tests:
stage: test
script:
- make test
tags:
- demo
race_detector:
stage: test
script:
- make race
tags:
- demo
code_coverage:
stage: test
script:
- make coverage
tags:
- demo
code_coverage_report:
stage: test
script:
- make coverhtml
only:
- master
tags:
- demo
lint_code:
stage: test
script:
- make lint
build:
stage: build
script:
- pwd
- go build .
tags:
- demo
build_image:
stage: deploy
script:
- make build_image
tags:
- demo

3.3.2. Makefile
PROJECT_NAME := "demo"
PKG := "gitlab.chain.cn/ZhangZhongcheng/$(PROJECT_NAME)"
PKG_LIST := $(shell go list ./... | grep -v /vendor/)
GO_FILES := $(shell find . -name '*.go' | grep -v /vendor/ | grep -v _test.go)
test: [size=16] Run unittests[/size]
@go test -v ${PKG_LIST}
lint: [size=16] Lint the files[/size]
@golint ${PKG_LIST}
race: [size=16] Run data race detector[/size]
@go test -race -short ${PKG_LIST}
coverage: [size=16] Generate global code coverage report[/size]
./scripts/coverage.sh;
coverhtml: [size=16] Generate global code coverage report in HTML[/size]
./scripts/coverage.sh html;
build_image:
./scripts/buildDockerImage.sh

3.3.3. coverage.sh
#!/bin/bash
#
# Code coverage generation
COVERAGE_DIR="${COVERAGE_DIR:-coverage}"
PKG_LIST=$(go list ./... | grep -v /vendor/)
# Create the coverage files directory
mkdir -p "$COVERAGE_DIR";
# Create a coverage file for each package
for package in ${PKG_LIST}; do
go test -covermode=count -coverprofile "${COVERAGE_DIR}/${package[size=16]*/}.cov" "$package" ;[/size]
done ;
# Merge the coverage profile files
echo 'mode: count' > "${COVERAGE_DIR}"/coverage.cov ;
tail -q -n +2 "${COVERAGE_DIR}"/*.cov >> "${COVERAGE_DIR}"/coverage.cov ;
# Display the global code coverage
go tool cover -func="${COVERAGE_DIR}"/coverage.cov ;
# If needed, generate HTML report
if [ "$1" == "html" ]; then
go tool cover -html="${COVERAGE_DIR}"/coverage.cov -o coverage.html ;
fi
# Remove the coverage files directory
rm -rf "$COVERAGE_DIR";

3.3.4. buildDockerImage.sh
#!/bin/bash
#检测GOPATH
echo "检测GOPATH"
if [ -z "$GOPATH" ];then
echo "GOPATH 未设定"
exit 1
else
echo "GOPATH=$GOPATH"
fi
#初始化数据
echo "初始化数据"
new_version="1.0.0"
old_version="1.0.0"
golang_version="1.9.2"
app_name="application"
projust_root="demo"
DOCKER_IMAGE_NAME="demo"
REGISTRY_HOST="xxx.xxx.xxx.xxx:5000"
path="/go/src/_/builds/ZhangZhongcheng/demo"
#当前容器更换为旧标签
echo "当前容器更换为旧标签"
docker rmi $REGISTRY_HOST/$DOCKER_IMAGE_NAME:$old_version
# 基于golang:1.9.2镜像启动的容器实例,编译本项目的二进制可执行程序
echo "基于golang:1.9.2镜像启动的容器实例,编译本项目的二进制可执行程序"
cd $path
go build -o $app_name
echo "检测 $app_name 应用"
FILE="$path/$app_name"
if [ -f "$FILE" ];then
echo "$FILE 已就绪"
else
echo "$FILE 应用不存在"
exit 1
fi
#docker构建镜像 禁止在构建上下文之外的路径 添加复制文件
#所以在此可以用命令把需要的文件cp到 dockerfile 同目录内 ,构建完成后再用命令删除
cd $path/scripts
echo "提取构建时需要的文件"
cp ../$app_name $app_name
# 基于当前目录下的Dockerfile构建镜像
echo "基于当前目录下的Dockerfile构建镜像"
echo "docker build -t $REGISTRY_HOST/$DOCKER_IMAGE_NAME:$new_version ."
docker build -t $REGISTRY_HOST/$DOCKER_IMAGE_NAME:$new_version .
# 删除本次生成的可执行文件 以及构建所需要的文件
echo "删除本次生成的可执行文件 以及构建所需要的文件"
rm -rf $app_name
rm -rf ../$app_name
#查看镜像
echo "查看镜像"
docker images | grep $DOCKER_IMAGE_NAME
#推送镜像
echo "推送镜像"
echo "docker push $REGISTRY_HOST/$DOCKER_IMAGE_NAME:$new_version"
docker push $REGISTRY_HOST/$DOCKER_IMAGE_NAME:$new_version
echo "auto deploy"
./automationDeployment.sh $new_version $old_version

3.3.5. automationDeployment.sh
#!/usr/bin/expect
#指定shebang
#设定超时时间为3秒
set ip xxx.xxx.xxx.xxx
set password "xxxxxxx"
set new_version [lindex $argv 0]
set old_version [lindex $argv 1]
spawn ssh root@$ip
expect {
"*yes/no" { send "yes\r"; exp_continue}
"*password:" { send "$password\r" }
}
expect "#*"
send "cd /root/demo/\r"
send "./docker_run_demo.sh $new_version $old_version\r"
expect eof

3.3.6. Dockerfile
FROM golang:1.9.2
#定义环境变量 alpine专用
#ENV TIME_ZONE Asia/Shanghai
ADD application /go/src/demo/
WORKDIR /go/src/demo
ADD run_application.sh /root/
RUN chmod 755 /root/run_application.sh
CMD sh /root/run_application.sh
EXPOSE 8080

3.3.7. run_application.sh
#!/bin/bash
#映射ip
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
cd /go/src/demo/
./application

#4. 结果
以下为部署成功后的截图:
3.png


原文链接:http://www.chairis.cn/blog/article/96

Docker与Golang的巧妙结合

cyeaaa 发表了文章 • 0 个评论 • 25334 次浏览 • 2016-09-22 20:07 • 来自相关话题

【编者的话】这是一个展示在使用Go语言时如何让Docker更有用的提示与技巧的简辑。例如,如何使用不同版本的Go工具链来编译Go代码,如何交叉编译到不同的平台(并且测试结果!),或者如何制作真正小的容器镜像。 下面的文章假定你已经安装 ...查看全部
【编者的话】这是一个展示在使用Go语言时如何让Docker更有用的提示与技巧的简辑。例如,如何使用不同版本的Go工具链来编译Go代码,如何交叉编译到不同的平台(并且测试结果!),或者如何制作真正小的容器镜像。

下面的文章假定你已经安装了Docker。不必是最新版本(这篇文章不会使用Docker任何花哨的功能)。
# 没有go的Go
...意思是:“不用安装`go`就能使用Go”

如果你写Go代码,或者你对Go语言有一点点兴趣,你肯定要安装了Go编译器和Go工具链,所以你可能想知道:“重点是什么?”;但有些情况下,你想不安装`Go`就来编译Go。

  • 机器上依旧有老版本Go 1.2(你不能或不想更新),不得不使用这个代码库,需要一个高版本的工具链。
  • 想使用Go1.5的交叉编译功能(例如,确保能从一个Linux系统创建操作系统X的二进制文件)。
  • 想拥有多版本的Go,但不想完全弄乱系统。
  • 想100%确定项目和它所有的依赖,下载,建立和运行在一个纯净的系统上。
如果遇到上述情况,找Docker来解决!## 在容器里编译一个程序当你安装了Go,你可以执行`go get -v github.com/user/repo`来下载,创建和安装一个库。(`-v`只是信息显示,如果你喜欢工具链快速和静默地运行,可以将它移除!)你也可以执行`go get github.com/user/repo/...`来下载,创建和安装那个repo(包括库和二进制文件)里面所有的东西。我们可以在一个容器里面这样做!试试这个:
docker run golang go get -v github.com/golang/example/hello/...
这将拉取golang镜像(除非你已经有了,那它会马上启动),并且创建一个基于它的容器。在那个容器里,go会下载一个“hello world”的例子,创建它,安装它。但它会把它安装到这个容器里……我们现在怎么运行那个程序呢?##在容器里运行程序一个办法是提交我们刚刚创建的容器,即,打包它到一个新的镜像:
docker commit $(docker ps -lq) awesomeness
注意:`docker ps –lq`输出最后一个执行的容器的ID(只有ID!)。如果你是机器的唯一用户,并且你从上一个命令开始没有创建另一个容器,那这个容器就是你刚刚创建的“hello world”的例子。现在,可以用刚刚构建的镜像创建容器来运行程序:
docker run awesomeness hello
输出会是`Hello, Go examples!`。## 闪光点当用`docker commit`构建镜像时,可以用`--change`标识指定任意Dockerfile命令。例如,可以使用一个`CMD`或者`ENTRYPOINT`命令以便`docker run awesomeness`自动执行hello。##在一次性容器上运行如果不想创建额外的镜像只想运行这个Go程序呢?使用:
docker run --rm golang sh -c \"go get github.com/golang/example/hello/... && exec hello"
等等,那些花哨的东西是什么?
  • `--rm` 告诉Docker CLI一旦容器退出,就自动发起一个`docker rm`命令。那样,不会留下任何东西。
  • 使用shell逻辑运算符`&&`把创建步骤(`go get`)和执行步骤(`exec hello`)联接在一起。如果不喜欢shell,`&&`意思是“与”。它允许第一部分`go get...`,并且如果(而且仅仅是如果!)那部分运行成功,它将执行第二部分(`exec hello`)。如果你想知道为什么这样:它像一个懒惰的`and`计算器,只有当左边的值是`true`才计算右边的。
  • 传递命令到`sh –c`,因为如果是简单的做`docker run golang "go get ... && hello"`,Docker将试着执行名为`go SPACE get SPACE etc`的程序。并且那不会起作用。因此,我们启动一个shell,并让shell执行命令序列。
  • 使用`exec hello`而不是`hello`:这将使用hello程序替代当前的进程(我们刚才启动的shell)。这确保`hello`在容器里是PID 1。而不是shell的是PID 1而`hello`作为一个子进程。这对这个微小的例子毫无用处,但是当运行更有用的程序,这将允许它们正确地接收外部信号,因为外部信号是发送给容器里的PID 1。你可能会想,什么信号啊?好的例子是`docker stop`,发送`SIGTERM`给容器的PID 1。
##使用不同版本的Go当使用`golang`镜像,Docker扩展为`golang:latest,`将(像你所猜的)映射到Docker Hub上的最新可用版本。如果想用一个特定的Go版本,很容易:在镜像名字后面用那个版本做标签指定它。例如,想用Go 1.5,修改上面的例子,用`golang:1.5`替换`golang`:
docker run --rm golang:1.5 sh -c \ "go get github.com/golang/example/hello/... && exec hello"
你能在Docker Hub的Golang镜像页面上看到所有可用的版本(和变量)。##在系统上安装好了,如果想在系统上运行编译好的程序,而不是一个容器呢?我们将复制这个编译了的二进制文件到容器外面。注意,仅当容器架构和主机架构匹配的时候,才会起作用;换言之,如果在Linux上运行Docker。(我排除的可能是运行Windows容器的人!)最容易在容器外获得二进制文件的方法是映射`$GOPATH/bin`目录到一个本地目录,在`golang`容器里,`$GOPATH`是`/go.`所以我们可以如下操作:
docker run -v /tmp/bin:/go/bin \ golang go get github.com/golang/example/hello/... /tmp/bin/hello
如果在Linux上,将看到`Hello, Go examples!`消息。但如果是,例如在Mac上,可能会看到:
-bash:/tmp/test/hello: cannot execute binary file
我们又能做什么呢?##交叉编译Go 1.5具备优秀的开箱即用交叉编译能力,所以如果你的容器操作系统和/或架构和你的系统不匹配,根本不是问题!开启交叉编译,需要设置`GOOS`和/或`GOARCH`。例如,假设在64位的Mac上:
docker run -e GOOS=darwin -e GOARCH=amd64 -v /tmp/crosstest:/go/bin \ golang go get github.com/golang/example/hello/...
交叉编译的输出不是直接在`$GOPATH/bin`,而是在`$GOPATH/bin/$GOOS_$GOARCH.`。换言之,想运行程序,得执行`/tmp/crosstest/darwin_amd64/hello.`。##直接安装到$PATH如果在Linux上,甚至可以直接安装到系统bin目录:
docker run -v /usr/local/bin:/go/bin \ golang get github.com/golang/example/hello/...
然而,在Mac上,尝试用`/usr`作为一个卷将不能挂载Mac的文件系统到容器。会挂载Moby VM(小Linux VM藏在工具栏Docker图标的后面)的`/usr`目录。(译注:目前Docker for Mac版本可以自定义设置挂载路径)但可以使用`/tmp`或者在你的home目录下的什么其它目录,然后从这里复制。# 创建依赖镜像我们用这种技术产生的Go二进制文件是静态链接的。这意味着所有需要运行的代码包括所有依赖都被嵌入了。动态链接的程序与之相反,不包含一些基本的库(像“libc”)并且使用系统范围的复制,是在运行时确定的。这意味着可以在容器里放弃Go编译好的程序,没有别的,并且它会运行。我们试试!##scratch镜像Docker生态系统有一个特殊的镜像:`scratch.`这是一个空镜像。它不需要被创建或者下载,因为定义的就是空的。给新的Go依赖镜像创建一个新的空目录。在这个新目录,创建下面的Dockerfile:
FROM scratch COPY ./hello /hello ENTRYPOINT ["/hello"]
这意味着:从scratch开始(一个空镜像),增加`hello`文件到镜像的根目录,*定义`hello`程序为启动这个容器后默认运行的程序。然后,产生`hello`二进制文件如下:
docker run -v $(pwd):/go/bin --rm \ golang go get github.com/golang/example/hello/...
注意:不需要设置`GOOS`和`GOARCH`,正因为,想要一个运行在Docker容器里的二进制文件,不是在主机上。所以不用设置这些变量!然后,创建镜像:
docker build -t hello .
测试它:
docker run hello
(将显示“Hello, Go examples!”)最后但不重要,检查镜像的大小:
docker images hello
如果一切做得正确,这个镜像大约2M。相当好!##构建东西而不推送到Github当然,如果不得不推送到GitHub,每次编译都会浪费很多时间。想在一个代码段上工作并在容器中创建它时,可以在`golang`容器里挂载一个本地目录到`/go`。所以`$GOPATH`是持久调用:`docker run -v $HOME/go:/go golang ....`但也可以挂载本地目录到特定的路径上,来“重载”一些包(那些在本地编辑的)。这是一个完整的例子:
# Adapt the two following environment variables if you are not running on a Mac export GOOS=darwin GOARCH=amd64 mkdir go-and-docker-is-love cd go-and-docker-is-love git clone git://github.com/golang/example cat example/hello/hello.go sed -i .bak s/olleH/eyB/ example/hello/hello.go docker run --rm \ -v $(pwd)/example:/go/src/github.com/golang/example \ -v $(pwd):/go/bin/${GOOS}_${GOARCH} \ -e GOOS -e GOARCH \golang go get github.com/golang/example/hello/... ./hello # Should display "Bye, Go examples!" 
# 网络包和CGo的特殊情况进入真实的Go代码世界前,必须承认的是:在二进制文件上有一点点偏差。如果在使用CGo,或如果在使用`net`包,Go链接器将生成一个动态库。这种情况下,`net`包(里面确实有许多有用的Go程序!),罪魁祸首是DNS解析。大多数系统都有一个花哨的,模块化的名称解析系统(像名称服务切换),它依赖于插件,技术上,是动态库。默认地,Go将尝试使用它;这样,它将产生动态库。我们怎么解决?##重用另一个版本的libc一个解决方法是用一个基础镜像,有那些程序功能所必需的库。几乎任何“正规”基于GNU libc的Linux发行版都能做到。所以,例如,使用`FROM debian`或`FROM fedora`,替代`FROM scratch`。现在结果镜像会比原来大一些;但至少,大出来的这一点将和系统里其它镜像共享。注意:这种情况不能使用Alpine,因为Alpine是使用musl库而不是GNU libc。##使用自己的libc另一个解决方案是像做手术般地提取需要的文件,用`COPY`替换容器里的。结果容器会小。然而,这个提取过程困难又繁琐,太多更深的细节要处理。如果想自己看,看看前面提到的`ldd`和名称服务切换插件。##用netgo生成静态二进制文件我们也可以指示Go不用系统的libc,用本地DNS解析代替Go的`netgo`。要使用它,只需在`go get`选项加入`-tags netgo -installsuffix netgo`。
  • `-tags netgo`指示工具链使用`netgo`。
  • `-installsuffix netgo`确保结果库(任何)被一个不同的,非默认的目录所替代。如果做多重`go get`(或`go build`)调用,这将避免代码创建和用不用netgo之间的冲突。如果像目前我们讲到的这样,在容器里创建,是完全没有必要的。因为这个容器里面永远没有其他Go代码要编译。但它是个好主意,习惯它,或至少知道这个标识存在。
# SSL证书的特殊情况还有一件事,你会担心,你的代码必须验证SSL证书;例如,通过HTTPS联接外部API。这种情况,需要将根证书也放入容器里,因为Go不会捆绑它们到二进制文件里。##安装SSL证书再次,有很多可用的选择,但最简单的是使用一个已经存在的发布里面的包。Alpine是一个好的选择,因为它非常小。下面的`Dockerfile`将给你一个小的基础镜像,但捆绑了一个过期的跟证书:
FROM alpine:3.4RUN apk add --no-cache ca-certificates apache2-utils
来看看吧,结果镜像只有6MB!注意:`--no-cache`选项告诉`apk`(Alpine包管理器)从Alpine的镜像发布上获取可用包的列表,不保存在磁盘上。你可能会看到Dockerfiles做这样的事`apt-get update && apt-get install ... && rm -rf /var/cache/apt/*`;这实现了(即在最终镜像中不保留包缓存)与一个单一标志相当的东西。一个附加的回报:把你的应用程序放入基于Alpine镜像的容器,让你获得了一堆有用的工具。如果需要,现在你可以吧shell放入容器并在它运行时做点什么。# 打包 我们看到Docker如何帮助我们在干净独立的环境里编译Go代码;如何使用不同版本的Go工具链;以及如何在不同的操作系统和平台之间交叉编译。我们还看到Go如何帮我们给Docker创建小的,容器依赖镜像,并且描述了一些静态库和网络依赖相关的微妙联系(没别的意思)。除了Go是真的适合Docker项目这个事实,我们希望展示给你的是,Go和Docker如何相互借鉴并且一起工作得很好!##致谢这最初是在2016年GopherCon骇客日提出的。我要感谢所有的校对材料、提出建议和意见让它更好的人,包括但不局限于:
所有的错误和拼写错误都是我自己的;所有的好东西都是他们的!

原文链接:Docker + Golang = <3(翻译:陈晏娥 审校:田浩浩

想请问下golang的编辑工具用的是什么

回复

小泽巴比 回复了问题 • 4 人关注 • 3 个回复 • 2545 次浏览 • 2017-07-31 16:05 • 来自相关话题

最新版的etcd使用代理模式还是有问题?

回复

颦_ 发起了问题 • 1 人关注 • 0 个回复 • 3093 次浏览 • 2016-07-26 20:22 • 来自相关话题

【腾讯云】#容器团队#高级容器研发#招聘

Shirleyee 发表了文章 • 1 个评论 • 222 次浏览 • 2019-05-24 10:33 • 来自相关话题

高级容器研发工程师 工作职责 负责公有云/私有云中 Kubernetes/Devops 等产品技术方案设计与研发工作;负责 Kubernetes 相关前沿技术规划和调研工作,从技术上保证产品的竞争力;负责与产品及客户沟通,判定需求的合理 ...查看全部
高级容器研发工程师
工作职责
  1. 负责公有云/私有云中 Kubernetes/Devops 等产品技术方案设计与研发工作;
  2. 负责 Kubernetes 相关前沿技术规划和调研工作,从技术上保证产品的竞争力;
  3. 负责与产品及客户沟通,判定需求的合理性,找出最合适的方式解决客户问题。
工作要求
  1. 3 年以上后端开发经验,Coding、Debug 能力强, 有丰富的架构设计经验;
  2. 熟悉 C/C++/Go/Java/Python/Ruby 等至少二种编程语言;
  3. 熟悉 Docker/Kubernetes/Swarm/Mesos 等技术;
  4. 熟悉 Jenkins/ELK/Prometheus 等技术优先;
  5. 熟悉 AWS/Google Cloud 等云计算厂商产品优先。


有意请戳:
Wechat:13723737494
Email:Shirleyeee@foxmail.com

知乎社区核心业务 Golang 化实践

大卫 发表了文章 • 0 个评论 • 1081 次浏览 • 2018-12-13 18:40 • 来自相关话题

#背景 众所周知,知乎社区后端的主力编程语言是 Python。 随着知乎用户的迅速增长和业务复杂度的持续增加,核心业务的流量在过去一年内增长了好几倍,对应的服务端的压力也越来越大。随着业务发展,我们发现 Python 作为 ...查看全部
#背景
众所周知,知乎社区后端的主力编程语言是 Python。

随着知乎用户的迅速增长和业务复杂度的持续增加,核心业务的流量在过去一年内增长了好几倍,对应的服务端的压力也越来越大。随着业务发展,我们发现 Python 作为动态解释型语言,较低的运行效率和较高的后期维护成本带来的问题逐渐暴露出来:

  1. 运行效率较低。知乎目前机房机柜空间已经不足,按照目前的用户和流量增长速度,可预见将在短期内服务器资源告急(针对这一点,知乎正在由单机房架构升级为异地多活架构);
  2. Python 过于灵活的语言特性,导致多人协作和项目维护成本较高。
受益于近些年开源社区的发展和容器等关键技术的普及,知乎的基础平台技术选型一直较为开放。在开放的标准之上,各个语言都有成熟的开源的中间件可供选择。这使得业务做选型时可以根据问题场景选择更合适的工具,语言也是一样。

基于此,为了解决资源占用问题和动态语言的维护成本问题,我们决定尝试使用静态语言对资源占用极高的核心业务进行重构。
#为什么选择 Golang
如上所述,知乎在后端技术选型上比较开放。在过去几年里,除了 Python 作为主力语言开发,知乎内部也不乏 Java、Golang、NodeJS 和 Rust 等语言开发的项目。
1.png

通过 ZAE(Zhihu App Engine)新建一个应用时,提供了多门语言的支持

Golang 是目前知乎内部讨论交流最活跃的编程语言之一,考虑到以下几点,我们决定尝试用 Golang 重构内部高并发量的核心业务:

* 天然的并发优势,特别适合 IO 密集应用
* 知乎内部基础组件的 Golang 版生态比较完善
* 静态类型,多人协作开发和维护更加安全可靠
* 构建好后只需一个可执行文件即可,方便部署
* 学习成本低,且开发效率较 Python 没有明显降低

相比另一门也很优秀的待选语言—— Java,Golang 在知乎内部生态环境、部署的方便程度和工程师的兴趣上都更胜一筹,最终我们决定,选择 Golang 作为开发语言。
#改造成果
截至目前,知乎社区 member(RPC,高峰数十万 QPS)、评论(RPC + HTTP)、问答(RPC + HTTP)服务已经全部通过 Golang 重写。同时因为在 Golang 化过程中我们对 Golang 基础组件的进一步完善,目前一些新的业务在开发之初就直接选择了 Golang 来实现,Golang 已经成为知乎内部新项目技术选型的推荐语言之一。

相比改造前,目前得到改进的点有以下:

  1. 节约了超过 80% 的服务器资源。由于我们的部署系统采用蓝绿部署,所以之前占用服务器资源最高的几个业务会因为容器资源原因无法同时部署,需要排队依次部署。重构后,服务器资源得到优化,服务器资源问题得到了有效解决。
  2. 多人开发和项目维护成本大幅下降。想必大家维护大型 Python 项目都有经常需要里三层、外三层确认一个函数的参数类型和返回值。而 Golang 里,大家都面向接口定义,然后根据接口来实现,这使得编码过程更加安全,很多 Python 代码运行时才能发现的问题可以在编译时即可发现。
  3. 完善了内部 Golang 基础组件。前面提到,知乎内部基础组件的 Golang 版比较完善,这是我们选择 Golang 的前提之一。不过,在重构的过程中,我们发现仍有部分基础组件不够完善甚至缺少。所以,我们也完善和提供了不少基础组件,为之后其它项目的 Golang 化改造提供了便利。

2.png

过去 10 个月问答服务的 CPU 核数占用变化趋势
#实施过程
得益于知乎微服务化比较彻底,每个独立的微服务想要更换语言非常方便,我们可以方便地对单个业务进行改造,且几乎可以做到外部依赖方无感知。

知乎内部,每个独立的微服务有自己独立的各种资源,服务间是没有资源依赖的,全部通过 RPC 请求交互,每个对外提供服务(HTTP or RPC)的容器组,都通过独立的 HAProxy 地址代理对外提供服务。一个典型的微服务结构如下:
3.png

知乎内部一个典型的微服务组成,服务间没有资源依赖

所以,我们的 Golang 化改造分为了以下几步:
##Step 1. 用 Golang 重构逻辑
首先,我们会新起一个微服务,通过 Golang 来重构业务逻辑,但是:

  1. 新服务对外暴露的协议(HTTP 、RPC 接口定义和返回数据)与之前保持一致(保持协议一致很重要,之后迁移依赖方会更方便)
  2. 新的服务没有自己的资源,使用待重构服务的资源:

4.png

新服务(下)使用待重构服务(上)的资源,短期内资源混用

##Step 2. 验证新逻辑正确性
当代码重构完成后,在将流量切换到新逻辑之前,我们会先验证新服务的正确性。

针对读接口,由于其是幂等的,多次调用没有副作用,所以当新版接口实现完成后,我们会在老服务收到请求的同时,起一个协程请求新服务,并对比新老服务的数据是否一致:

  1. 当请求到达老服务后,会立即启一个协程请求新的服务,与此同时老服务的主逻辑会正常执行。
  2. 当请求返回后,会比较老服务与新实现的服务返回数据是否相同,如果不同,会打点记录 + 日志记录。
  3. 工程师根据打点指标和日志,发现新实现逻辑的错误,改正后继续验证(其实这一步,我们也发现了不少原本 Python 实现的错误)。

5.png

服务请求两边数据,并对比结果,但返回老服务的结果

而对于写接口,大部分并不是幂等的,所以针对写接口不能像上面这样验证。对于写接口,我们主要会通过以下手段保证新旧逻辑等价:

  1. 单元测试保证
  2. 开发者验证
  3. QA 验证

##Step 3. 灰度放量
当一切验证通过之后,我们会开始按照百分比转发流量。

此时,请求依然会被代理到老的服务的容器组,但是老服务不再处理请求,而是转发请求到新服务中,并将新服务返回的数据直接返回。

之所以不直接从流量入口切换,是为了保证稳定性,在出现问题时可以迅速回滚。
6.png

服务请求 Golang 实现
##Step 4. 切流量入口
当上一步的放量达到 100% 后,请求虽然依然会被代理到老的容器组,但返回的数据已经全部是新服务产生的。此时,我们可以把流量入口直接切换到新服务了。
7.png

请求直接打到新的服务,旧服务没有流量了
##Step 5. 下线老服务
到这里重构已经基本接近尾声了。不过新服务的资源还在老服务中,以及老的没有流量的服务其实还没有下线。

到这里,直接把老服务的资源归属调整为新服务,并下线老服务即可。
8.png

Goodbye,Python

至此,重构完成。
#Golang 项目实践
在重构的过程中,我们踩了不少坑,这里摘其中一些与大家分享一下。如果大家有类似重构需求,可简单参考。
##换语言重构的前提是了解业务
不要无脑翻译原来的代码,也不要无脑修复原本看似有问题的实现。在重构的初期,我们发现一些看似可以做得更好的点,闷头一顿修改之后,却产生了一些奇怪的问题。后面的经验是,在重构前一定要了解业务,了解原本的实现。最好整个重构的过程有对应业务的工程师也参与其中。
##项目结构
关于合适的项目结构,其实我们也走过不少弯路。

一开始,我们根据在 Python 中的实践经验,层与层之间直接通过函数提供交互接口。但是,迅速发现 Golang 很难像 Python 一样,方便地通过 monkey patch 完成测试。

经过逐渐演进和参考各种开源项目,目前,我们的代码结构大致是这样:
.
├── bin --> 构建生成的可执行文件
├── cmd --> 各种服务的 main 函数入口( RPC、Web 等)
│ ├── service
│ │ └── main.go
│ ├── web
│ └── worker
├── gen-go --> 根据 RPC thrift 接口自动生成
├── pkg --> 真正的实现部分(下面详细介绍)
│ ├── controller
│ ├── dao
│ ├── rpc
│ ├── service
│ └── web
│ ├── controller
│ ├── handler
│ ├── model
│ └── router
├── thrift_files --> thrift 接口定义
│ └── interface.thrift
├── vendor --> 依赖的第三方库( dep ensure 自动拉取)
├── Gopkg.lock --> 第三方依赖版本控制
├── Gopkg.toml
├── joker.yml --> 应用构建配置
├── Makefile --> 本项目下常用的构建命令
└── README.md

分别是:

* bin:构建生成的可执行文件,一般线上启动就是 `bin/xxxx-service`
* cmd:各种服务(RPC、Web、离线任务等)的 main 函数入口,一般从这里开始执行
* gen-go:thrift 编译自动生成的代码,一般会配置 Makefile,直接 `make thrift` 即可生成(这种方式有一个弊端:很难升级 thrift 版本)
* pkg:真正的业务实现(下面详细介绍)
* thrift_files:定义 RPC 接口协议
* vendor:依赖的第三方库

其中,pkg 下放置着项目的真正逻辑实现,其结构为:
pkg/
├── controller
│ ├── ctl.go --> 接口
│ ├── impl --> 接口的业务实现
│ │ └── ctl.go
│ └── mock --> 接口的 mock 实现
│ └── mock_ctl.go
├── dao
│ ├── impl
│ └── mock
├── rpc
│ ├── impl
│ └── mock
├── service --> 本项目 RPC 服务接口入口
│ ├── impl
│ └── mock
└── web --> Web 层(提供 HTTP 服务)
├── controller --> Web 层 controller 逻辑
│ ├── impl
│ └── mock
├── handler --> 各种 HTTP 接口实现
├── model -->
├── formatter --> 把 model 转换成输出给外部的格式
└── router --> 路由

如上结构,值得关注的是我们在每一层之间一般都有 impl、mock 两个包。
9.png

这样做是因为 Golang 中不能像 Python 那样方便地动态 mock 掉一个实现,不能方便地测试。我们很看重测试,Golang 实现的测试覆盖率也保持在 85% 以上。所以我们将层与层之间先抽象出接口(如上 ctl.go),上层对下层的调用通过接口约定。在执行的时候,通过依赖注入绑定 impl 中对接口的实现来运行真正的业务逻辑,而测试的时候,绑定 mock 中对接口的实现来达到 mock 下层实现的目的。

同时,为了方便业务开发,我们也实现了一个 Golang 项目的脚手架,通过脚手架可以更方便地直接生成一个包含 HTTP & RPC 入口的 Golang 服务。这个脚手架已经集成到 ZAE(Zhihu App Engine),在创建出 Golang 项目后,默认的模板代码就生成好了。对于使用 Golang 开发的新项目,创建好就有了一个开箱即用的框架结构。
##静态代码检查,越早越好
我们在开发的后期才意识到引入静态代码检查,其实最好的做法是在项目开始时就及时使用,并以较严格的标准保证主分支的代码质量。

在开发后期才引入的问题是,已经有太多代码不符合标准。所以我们不得不短期内忽略了很多检查项。

很多非常基础甚至愚蠢的错误,人总是无法 100% 避免的,这正是 linter 存在的价值。

实际实践中,我们使用 gometalinter。gometalinter 本身不做代码检查,而是集成了各种 linter,提供统一的配置和输出。我们集成了 vet、golint 和 errcheck 三种检查。
##降级
降级的粒度究竟是什么?这个问题一些工程师的观点是 RPC 调用,而我们的答案是「功能」。

在重构过程中,我们按照「如果这个功能不可用,对用户的影响该是什么」的角度,将所有可降级的功能点都做了降级,并对所有降级加上对应的指标点和报警。最终的效果是,如果问答所有的外部 RPC 依赖全部挂了(包括 member 和鉴权这样的基础服务),问答本身仍然可以正常浏览问题和回答。

我们的降级是在 circuit 的基础上,封装指标收集和日志输出等功能。Twitch 也在生产环境中使用了这个库,且我们超过半年的使用中,还没有遇到什么问题。
##anti-pattern: panic - recover
大部分人开始使用 Golang 开发后,一个非常不习惯的点就是它的错误处理。一个简单的 HTTP 接口实现可能是这样:
func (h [i]AnswerHandler) Get(w http.ResponseWriter, r [/i]http.Request) {
ctx := r.Context()

loginId, err := auth.GetLoginID(ctx)
if err != nil {
zapi.RenderError(err)
---> return
}

answer, err := h.PrepareAnswer(ctx, r, loginId)
if err != nil {
zapi.RenderError(err)
---> return
}

formattedAnswer, err := h.ctl.FormatAnswer(ctx, loginId, answer)
if err != nil {
zapi.RenderError(err)
---> return
}
zapi.RenderJSON(w, formattedAnswer)
}

如上,每行代码后有紧跟着一个错误判断。繁琐只是其次,主要问题在于,如果错误处理后面的 return 语句忘写,那么逻辑并不会被阻断,代码会继续向下执行。在实际开发过程中,我们也确实犯过类似的错误。

为此,我们通过一层 middleware,在框架外层将 panic 捕获,如果 recover 住的是框架定义的错误则转换为对应的 HTTP Error 渲染出去,反之继续向上层抛出去。改造后的代码成了这样:
func (h [i]AnswerHandler) Get(w http.ResponseWriter, r [/i]http.Request) {
ctx := r.Context()

loginId := auth.MustGetLoginID(ctx)

answer := h.MustPrepareAnswer(ctx, r, loginId)

formattedAnswer := h.ctl.MustFormatAnswer(ctx, loginId, answer)

zapi.RenderJSON(w, formattedAnswer)
}

如上,业务逻辑中以前 RenderError 并直接紧接着返回的地方,现在再遇到 error 的时候,会直接 panic。这个 panic 会在 HTTP 框架层被捕获,如果是项目内定义的 HTTPError,则转换成对应的接口 4xx JSON 格式返回给前端,否则继续向上抛出,最终变成一个 5xx 返回前端。

这里提到这个实现并不是推荐大家这样做,Golang 官方明确不推荐这样使用。不过,这确实有效地解决了一些问题,这里提出来供大家多一种参考。
##Goroutine 的启动
在构建 model 的时候,很多逻辑其实相互之间没有依赖是可以并发执行的。这时候,启动多个 goroutine 并发获取数据可以极大降低响应时间。

不过,刚使用 Golang 的人很容易踩到的一个 goroutine 坑点是,一个 goroutine 如果 panic 了,在它的父 goroutine 是无法 recover 的——严格来讲,并没有父子 goroutine 的概念,一旦启动,就是一个独立的 goroutine 了。

所以这里一定要非常注意,如果你新启动的 goroutine 可能 panic,一定需要本 goroutine 内 recover。当然,更好的方式是做一层封装,而不是在业务代码裸启动 goroutine。

因此我们参考了 Java 里面的 Future 功能,做了简单的封装。在需要启动 goroutine 的地方,通过封装的 Future 来启动,Future 来处理 panic 等各种状况。
##http.Response Body 没有 close 导致 goroutine 泄露
一段时间内,我们发现服务 goroutine 数量随着时间不断上涨,并会随着重启容器立刻掉下来。因此我们猜测代码存在 goroutine 泄露。
10.png

Goroutine 数量随运行时间逐渐增长,并在重启后掉下来

通过 goroutine stack 和在依赖库打印日志,最终定位到的问题是某个内部的基础库使用了 http.Client,但是没有 `resp.Body.Close()`,导致发生 goroutine 泄露。

这里的一个经验教训是生产环境不要直接用 http.Get,自己生成一个 http client 的实例并设置 timeout 会更好。

修复这个问题后就正常了:
11.png

resp.Body.Close()

虽然简单几句话介绍了这个问题,但实际定位问题的步骤耗费了我们不少时间,后面可以新起一篇文章专门介绍下 goroutine 泄露的排查过程。
#最后
核心业务的 Golang 化重构是由社区业务架构团队与社区内容技术团队的同学一起,经过 2018 年 Q2/Q3 的努力达成的目标。

社区业务架构团队负责解决知乎社区后端的业务复杂度和并发规模快速提升带来的问题和挑战。随着知乎业务规模和用户的快速增长,以及业务复杂度的持续增加,我们团队面临的技术挑战也越来越大。目前我们正在实施知乎社区的多机房异地多活架构,同时也在努力保障和提升知乎后端的质量和稳定性。

Golang基于GitLab CI/CD部署方案

大卫 发表了文章 • 0 个评论 • 1886 次浏览 • 2018-10-14 12:01 • 来自相关话题

持续集成(Continuous Integration)是一种软件开发实践,即团队开发成员经常集成它们的工作,通过每个成员每天至少集成一次,也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽早地发现集成错误 ...查看全部
持续集成(Continuous Integration)是一种软件开发实践,即团队开发成员经常集成它们的工作,通过每个成员每天至少集成一次,也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽早地发现集成错误。

持续部署(Continuous Deployment)是通过自动化的构建、测试和部署循环来快速交付高质量的产品。某种程度上代表了一个开发团队工程化的程度,毕竟快速运转的互联网公司人力成本会高于机器,投资机器优化开发流程化相对也提高了人的效率,让 engineering productivity 最大化。
#1. 环境准备
本次试验是基于CentOS 7.3,Docker 17.03.2-ce环境下的。Docker的安装这里就不赘述了,提供官方链接:Get Docker CE for CentOS
##1.1. Docker启动GitLab
启动命令如下:
docker run --detach \
--hostname gitlab.chain.cn \
--publish 8443:443 --publish 8080:80 --publish 2222:22 \
--name gitlab \
--restart always \
--volume /Users/zhangzc/gitlab/config:/etc/gitlab \
--volume /Users/zhangzc/gitlab/logs:/var/log/gitlab \
--volume /Users/zhangzc/gitlab/data:/var/opt/gitlab \
gitlab/gitlab-ce

port,hostname,volume根据具体情况具体设置。
##1.2. Docker启动gitlab-runner
启动命令如下:
sudo docker run -d /
--name gitlab-runner /
--restart always /
-v /Users/zhangzc/gitlab-runner/config:/etc/gitlab-runner /
-v /Users/zhangzc/gitlab-runner/run/docker.sock:/var/run/docker.sock /
gitlab/gitlab-runner:latest

volume根据具体情况具体设置。
##1.3. 用于集成部署的镜像制作
我们的集成和部署都需要放在一个容器里面进行,所以,需要制作一个镜像并安装一些必要的工具,用于集成和部署相关操作。目前我们的项目都是基于Golang 1.9.2的,这里也就基于Golang 1.9.2的镜像制定一个特定的镜像。

Dockerfile内容如下:
# Base image: https://hub.docker.com/_/golang/
FROM golang:1.9.2
USER root
# Install golint
ENV GOPATH /go
ENV PATH ${GOPATH}/bin:$PATH
RUN mkdir -p /go/src/golang.org/x
RUN mkdir -p /go/src/github.com/golang
COPY source/golang.org /go/src/golang.org/x/
COPY source/github.com /go/src/github.com/golang/
RUN go install github.com/golang/lint/golint
# install docker
RUN curl -O https://get.docker.com/builds/Linux/x86_64/docker-latest.tgz \
&& tar zxvf docker-latest.tgz \
&& cp docker/docker /usr/local/bin/ \
&& rm -rf docker docker-latest.tgz
# install expect
RUN apt-get update
RUN apt-get -y install tcl tk expect

其中Golint是用于Golang代码风格检查的工具。

Docker是由于需要在容器里面使用宿主的Docker命令,这里就需要安装一个Docker的可执行文件,然后在启动容器的时候,将宿主的 /var/run/docker.sock 文件挂载到容器内的同样位置。

expect是用于SSH自动登录远程服务器的工具,这里安装改工具是为了可以实现远程服务器端部署应用。

另外,在安装Golint的时候,是需要去golang.org下载源码的,由于墙的关系,go get命令是执行不了的。为了处理这个问题,首先通过其他渠道先下载好相关源码,放到指定的路径下,然后copy到镜像里,并执行安装即可。

下面有段脚本是用于生成镜像的:
#!/bin/bash
echo "提取构建镜像时需要的文件"
source_path="source"
mkdir -p $source_path/golang.org
mkdir -p $source_path/github.com
cp -rf $GOPATH/src/golang.org/x/lint $source_path/golang.org/
cp -rf $GOPATH/src/golang.org/x/tools $source_path/golang.org/
cp -rf $GOPATH/src/github.com/golang/lint $source_path/github.com
echo "构建镜像"
docker build -t go-tools:1.9.2 .
echo "删除构建镜像时需要的文件"
rm -rf $source_path

生成镜像后,推送到镜像仓库,并在gitlab-runner的服务器上拉取该镜像。

本次试验的GitLab和gitlab-runner是运行在同一服务器的Docker下的
#2. Runner注册及配置
##2.1. 注册
环境准备好后,在服务器上执行以下命令,注册Runner:
docker exec -it gitlab-runner gitlab-ci-multi-runner register

按照提示输入相关信息:
Please enter the gitlab-ci coordinator URL:
# gitlab的url, 如:https://gitlab.chain.cn/
Please enter the gitlab-ci token for this runner:
# gitlab->你的项目->settings -> CI/CD ->Runners settings
Please enter the gitlab-ci description for this runner:
# 示例:demo-test
Please enter the gitlab-ci tags for this runner (comma separated):
# 示例:demo
Whether to run untagged builds [true/false]:
# true
Please enter the executor: docker, parallels, shell, kubernetes, docker-ssh, ssh, virtualbox, docker+machine, docker-ssh+machine:
# docker
Please enter the default Docker image (e.g. ruby:2.1):
# go-tools:1.9.2(之前自己制作的镜像)

1.png

成功后,可以看到GitL ab->你的项目->Settings -> CI/CD ->Runners settings页面下面有以下内容:
2.png

##2.2. 配置
注册成功之后,还需要在原有的配置上做一些特定的配置,如下:
[[runners]]
name = "demo-test"
url = "https://gitlab.chain.cn/"
token = "c771fc5feb1734a9d4df4c8108cd4e"
executor = "docker"
[runners.docker]
tls_verify = false
image = "go-tools:1.9.2"
privileged = false
disable_cache = false
volumes = ["/var/run/docker.sock:/var/run/docker.sock"]
extra_hosts = ["gitlab.chain.cn:127.0.0.1"]
network_mode = "host"
pull_policy = "if-not-present"
shm_size = 0
[runners.cache]

这里先解释下gitlab-runner的流程吧,gitlab-runner在执行的时候,会根据上面的配置启动一个容器,即配置中的go-tools:1.9.2,其中所有的启动参数都会在[runners.docker]节点下配置好,包括挂载啊,网络啊之类的。容器启动成功之后,会使用这个容器去GitLab上pull代码,然后根据自己定义的规则进行检验,全部检测成功之后便是部署了。

volumes:是为了在容器中可以执行宿主机的Docker命令。

extra_hosts:给GitLab添加个host映射,映射到127.0.0.1

network_mode:令容器的网络与宿主机一致,只有这样才能通过127.0.0.1访问到GitLab。

pull_policy:当指定的镜像不存在的话,则通过docker pull拉取。
#3. 定义规则
在GitLab项目根目录创建.gitlab-ci.yml文件,填写Runner规则,具体语法课参考官方文档:https://docs.gitlab.com/ee/ci/yaml/。
##3.1. Go集成命令
下面介绍几个Golang常见的集成命令。

包列表,正如在官方文档中所描述的那样,Go项目是包的集合。下面介绍的大多数工具都将使用这些包,因此我们需要的第一个命令是列出包的方法。我们可以用go list子命令来完成:
go list ./...

请注意,如果我们要避免将我们的工具应用于外部资源,并将其限制在我们的代码中。 那么我们需要去除vendor目录,命令如下:
go list ./... | grep -v /vendor/

单元测试,这些是您可以在代码中运行的最常见的测试。每个.go文件需要一个能支持单元测试的_test.go文件。可以使用以下命令运行所有包的测试:
go test -short $(go list ./... | grep -v /vendor/)

数据竞争,这通常是一个难以逃避解决的问题,Go工具默认具有(但只能在Linux/amd64、FreeBSD/amd64、Darwin/amd64和Windows/amd64上使用):
go test -race -short $(go list . /…| grep - v /vendor/)

代码覆盖,这是评估代码的质量的必备工具,并能显示哪部分代码进行了单元测试,哪部分没有。

要计算代码覆盖率,需要运行以下脚本:
PKG_LIST=$(go list ./... | grep -v /vendor/)
for package in ${PKG_LIST}; do
go test -covermode=count -coverprofile "cover/${package[size=16]*/}.cov" "$package" ;[/size]
done
tail -q -n +2 cover/*.cov >> cover/coverage.cov
go tool cover -func=cover/coverage.cov

如果我们想要获得HTML格式的覆盖率报告,我们需要添加以下命令:
go tool cover -html=cover/coverage.cov -o coverage.html

构建,最后一旦代码经过了完全测试,我们要对代码进行编译,从而构建可以执行的二进制文件。
go build .

linter,这是我们在代码中使用的第一个工具:linter。它的作用是检查代码风格/错误。这听起来像是一个可选的工具,或者至少是一个“不错”的工具,但它确实有助于在项目上保持一致的代码风格。

linter并不是Go本身的一部分,所以如果要使用,你需要手动安装它(之前的go-tools镜像我们已经安装过了)。

使用方法相当简单:只需在代码包上运行它(也可以指向. go文件):
$ golint -set_exit_status $(go list ./... | grep -v /vendor/)

注意-set_exit_status选项。 默认情况下,Golint仅输出样式问题,并带有返回值(带有0返回码),所以CI不认为是出错。 如果指定了-set_exit_status,则在遇到任何样式问题时,Golint的返回码将不为0
##3.2. Makefile
如果我们不想在.gitlab-ci.yml文件中写的太复杂,那么我们可以把持续集成环境中使用的所有工具,全部打包在Makefile中,并用统一的方式调用它们。

这样的话,.gitlab-ci.yml文件就会更加简洁了。当然了,Makefile同样也可以调用*.sh脚本文件。
##3.3. 配置示例
3.3.1. .gitlab-ci.yml
image: go-tools:1.9.2
stages:
- build
- test
- deploy
before_script:
- mkdir -p /go/src/gitlab.chain.cn/ZhangZhongcheng /go/src/_/builds
- cp -r $CI_PROJECT_DIR /go/src/gitlab.chain.cn/ZhangZhongcheng/demo
- ln -s /go/src/gitlab.chain.cn/ZhangZhongcheng /go/src/_/builds/ZhangZhongcheng
- cd /go/src/_/builds/ZhangZhongcheng/demo
unit_tests:
stage: test
script:
- make test
tags:
- demo
race_detector:
stage: test
script:
- make race
tags:
- demo
code_coverage:
stage: test
script:
- make coverage
tags:
- demo
code_coverage_report:
stage: test
script:
- make coverhtml
only:
- master
tags:
- demo
lint_code:
stage: test
script:
- make lint
build:
stage: build
script:
- pwd
- go build .
tags:
- demo
build_image:
stage: deploy
script:
- make build_image
tags:
- demo

3.3.2. Makefile
PROJECT_NAME := "demo"
PKG := "gitlab.chain.cn/ZhangZhongcheng/$(PROJECT_NAME)"
PKG_LIST := $(shell go list ./... | grep -v /vendor/)
GO_FILES := $(shell find . -name '*.go' | grep -v /vendor/ | grep -v _test.go)
test: [size=16] Run unittests[/size]
@go test -v ${PKG_LIST}
lint: [size=16] Lint the files[/size]
@golint ${PKG_LIST}
race: [size=16] Run data race detector[/size]
@go test -race -short ${PKG_LIST}
coverage: [size=16] Generate global code coverage report[/size]
./scripts/coverage.sh;
coverhtml: [size=16] Generate global code coverage report in HTML[/size]
./scripts/coverage.sh html;
build_image:
./scripts/buildDockerImage.sh

3.3.3. coverage.sh
#!/bin/bash
#
# Code coverage generation
COVERAGE_DIR="${COVERAGE_DIR:-coverage}"
PKG_LIST=$(go list ./... | grep -v /vendor/)
# Create the coverage files directory
mkdir -p "$COVERAGE_DIR";
# Create a coverage file for each package
for package in ${PKG_LIST}; do
go test -covermode=count -coverprofile "${COVERAGE_DIR}/${package[size=16]*/}.cov" "$package" ;[/size]
done ;
# Merge the coverage profile files
echo 'mode: count' > "${COVERAGE_DIR}"/coverage.cov ;
tail -q -n +2 "${COVERAGE_DIR}"/*.cov >> "${COVERAGE_DIR}"/coverage.cov ;
# Display the global code coverage
go tool cover -func="${COVERAGE_DIR}"/coverage.cov ;
# If needed, generate HTML report
if [ "$1" == "html" ]; then
go tool cover -html="${COVERAGE_DIR}"/coverage.cov -o coverage.html ;
fi
# Remove the coverage files directory
rm -rf "$COVERAGE_DIR";

3.3.4. buildDockerImage.sh
#!/bin/bash
#检测GOPATH
echo "检测GOPATH"
if [ -z "$GOPATH" ];then
echo "GOPATH 未设定"
exit 1
else
echo "GOPATH=$GOPATH"
fi
#初始化数据
echo "初始化数据"
new_version="1.0.0"
old_version="1.0.0"
golang_version="1.9.2"
app_name="application"
projust_root="demo"
DOCKER_IMAGE_NAME="demo"
REGISTRY_HOST="xxx.xxx.xxx.xxx:5000"
path="/go/src/_/builds/ZhangZhongcheng/demo"
#当前容器更换为旧标签
echo "当前容器更换为旧标签"
docker rmi $REGISTRY_HOST/$DOCKER_IMAGE_NAME:$old_version
# 基于golang:1.9.2镜像启动的容器实例,编译本项目的二进制可执行程序
echo "基于golang:1.9.2镜像启动的容器实例,编译本项目的二进制可执行程序"
cd $path
go build -o $app_name
echo "检测 $app_name 应用"
FILE="$path/$app_name"
if [ -f "$FILE" ];then
echo "$FILE 已就绪"
else
echo "$FILE 应用不存在"
exit 1
fi
#docker构建镜像 禁止在构建上下文之外的路径 添加复制文件
#所以在此可以用命令把需要的文件cp到 dockerfile 同目录内 ,构建完成后再用命令删除
cd $path/scripts
echo "提取构建时需要的文件"
cp ../$app_name $app_name
# 基于当前目录下的Dockerfile构建镜像
echo "基于当前目录下的Dockerfile构建镜像"
echo "docker build -t $REGISTRY_HOST/$DOCKER_IMAGE_NAME:$new_version ."
docker build -t $REGISTRY_HOST/$DOCKER_IMAGE_NAME:$new_version .
# 删除本次生成的可执行文件 以及构建所需要的文件
echo "删除本次生成的可执行文件 以及构建所需要的文件"
rm -rf $app_name
rm -rf ../$app_name
#查看镜像
echo "查看镜像"
docker images | grep $DOCKER_IMAGE_NAME
#推送镜像
echo "推送镜像"
echo "docker push $REGISTRY_HOST/$DOCKER_IMAGE_NAME:$new_version"
docker push $REGISTRY_HOST/$DOCKER_IMAGE_NAME:$new_version
echo "auto deploy"
./automationDeployment.sh $new_version $old_version

3.3.5. automationDeployment.sh
#!/usr/bin/expect
#指定shebang
#设定超时时间为3秒
set ip xxx.xxx.xxx.xxx
set password "xxxxxxx"
set new_version [lindex $argv 0]
set old_version [lindex $argv 1]
spawn ssh root@$ip
expect {
"*yes/no" { send "yes\r"; exp_continue}
"*password:" { send "$password\r" }
}
expect "#*"
send "cd /root/demo/\r"
send "./docker_run_demo.sh $new_version $old_version\r"
expect eof

3.3.6. Dockerfile
FROM golang:1.9.2
#定义环境变量 alpine专用
#ENV TIME_ZONE Asia/Shanghai
ADD application /go/src/demo/
WORKDIR /go/src/demo
ADD run_application.sh /root/
RUN chmod 755 /root/run_application.sh
CMD sh /root/run_application.sh
EXPOSE 8080

3.3.7. run_application.sh
#!/bin/bash
#映射ip
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
cd /go/src/demo/
./application

#4. 结果
以下为部署成功后的截图:
3.png


原文链接:http://www.chairis.cn/blog/article/96

Containerd Server学习 Part 3

田浩浩 发表了文章 • 0 个评论 • 1209 次浏览 • 2018-02-06 17:37 • 来自相关话题

【编者的话】containerd v1.0.0的源码分析,以 docker-containerd --config /var/run/docker/containerd/containerd.toml为入口 ...查看全部
【编者的话】containerd v1.0.0的源码分析,以 docker-containerd --config /var/run/docker/containerd/containerd.toml为入口



### 初始化Snapshot插件

* 位置:`cmd/containerd/builtins_xxx.go`


package main

import (
_ "github.com/llitfkitfk/containerd/snapshots/naive"
)

# `snapshots/naive/naive.go`
func init() {
plugin.Register(&plugin.Registration{
Type: plugin.SnapshotPlugin,
ID: "naive",
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
ic.Meta.Platforms = append(ic.Meta.Platforms, platforms.DefaultSpec())
return NewSnapshotter(ic.Root)
},
})
}


### 初始化Content插件

* 位置:`server/server.go`


plugin.Register(&plugin.Registration{
Type: plugin.ContentPlugin,
ID: "content",
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
ic.Meta.Exports["root"] = ic.Root
return local.NewStore(ic.Root)
},
})



### 初始化Metadata插件

* 位置:`server/server.go`
* 依赖:github.com/boltdb/bolt



plugin.Register(&plugin.Registration{
Type: plugin.MetadataPlugin,
ID: "bolt",
Requires: []plugin.Type{
plugin.ContentPlugin,
plugin.SnapshotPlugin,
},



### grpc服务拦截器

* 位置:`server/server.go`


func interceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
ctx = log.WithModule(ctx, "containerd")
switch info.Server.(type) {
case tasks.TasksServer:
ctx = log.WithModule(ctx, "tasks")
case containers.ContainersServer:
ctx = log.WithModule(ctx, "containers")
case contentapi.ContentServer:
ctx = log.WithModule(ctx, "content")
case images.ImagesServer:
ctx = log.WithModule(ctx, "images")
case grpc_health_v1.HealthServer:
// No need to change the context
case version.VersionServer:
ctx = log.WithModule(ctx, "version")
case snapshotsapi.SnapshotsServer:
ctx = log.WithModule(ctx, "snapshot")
case diff.DiffServer:
ctx = log.WithModule(ctx, "diff")
case namespaces.NamespacesServer:
ctx = log.WithModule(ctx, "namespaces")
case eventsapi.EventsServer:
ctx = log.WithModule(ctx, "events")
case introspection.IntrospectionServer:
ctx = log.WithModule(ctx, "introspection")
case leasesapi.LeasesServer:
ctx = log.WithModule(ctx, "leases")
default:
log.G(ctx).Warnf("unknown GRPC server type: %#v\n", info.Server)
}
return grpc_prometheus.UnaryServerInterceptor(ctx, req, info, handler)
}


# 整理:
github.com/llitfkitfk/containerd/tree/part-3

Containerd Server学习 Part 2

田浩浩 发表了文章 • 0 个评论 • 1397 次浏览 • 2018-02-02 16:54 • 来自相关话题

【编者的话】containerd v1.0.0的源码分析,以 docker-containerd --config /var/run/docker/containerd/containerd.toml为入口,看大神们如何组织Go语言代码 ...查看全部
【编者的话】containerd v1.0.0的源码分析,以 docker-containerd --config /var/run/docker/containerd/containerd.toml为入口,看大神们如何组织Go语言代码

# 分析

### 程序信号处理
* 位置:`cmd/containerd/main.go`


...
var (
signals = make(chan os.Signal, 2048)
ctx = log.WithModule(gocontext.Background(), "containerd")
)
done := handleSignals(ctx, signals, serverC)
...
signal.Notify(signals, handledSignals...)

...

<-done
...


### 初始化服务
* 位置:`cmd/containerd/main.go`


...
server, err := server.New(ctx, config)
if err != nil {
return err
}
...



### 开启debug / metrics / grpc服务
* 位置:`cmd/containerd/main.go`
* 依赖:
github.com/docker/go-metrics
github.com/grpc-ecosystem/go-grpc-prometheus


...
if config.Debug.Address != "" {
l, err := sys.GetLocalListener(config.Debug.Address, config.Debug.UID, config.Debug.GID)
if err != nil {
return errors.Wrapf(err, "failed to get listener for debug endpoint")
}
serve(log.WithModule(ctx, "debug"), l, server.ServeDebug)
}

if config.Metrics.Address != "" {
l, err := net.Listen("tcp", config.Metrics.Address)
if err != nil {
return errors.Wrapf(err, "failed to get listener for metrics endpoint")
}
serve(log.WithModule(ctx, "metrics"), l, server.ServeMetrics)
}
l, err := sys.GetLocalListener(address, config.GRPC.UID, config.GRPC.GID)
if err != nil {
return errors.Wrapf(err, "failed to get listener for main endpoint")
}
serve(log.WithModule(ctx, "grpc"), l, server.ServeGRPC)
...




# 初始化服务解析

### 载入插件
* 位置:`server/server.go`

plugins, err := loadPlugins(config)
if err != nil {
return nil, err
}


### 初始化 grpc服务 和 传输服务
* 位置:`server/server.go`


type Server struct {
rpc *grpc.Server
events *exchange.Exchange
}



### grpc服务
* 位置:`server/server.go`
* 依赖:
google.golang.org/grpc
github.com/grpc-ecosystem/go-grpc-prometheus


rpc := grpc.NewServer(
grpc.UnaryInterceptor(interceptor),
grpc.StreamInterceptor(grpc_prometheus.StreamServerInterceptor),
)



### 传输服务
* 位置:`events/exchange/exchange.go`
* 作用:负责传播事件
* 依赖:github.com/docker/go-events


// Exchange broadcasts events
type Exchange struct {
broadcaster *goevents.Broadcaster
}


# 整理:
github.com/llitfkitfk/containerd/tree/part-2

Containerd Server学习 Part 1

田浩浩 发表了文章 • 0 个评论 • 1426 次浏览 • 2018-01-31 21:56 • 来自相关话题

【编者的话】containerd v1.0.0的源码分析,以` docker-containerd --config /var/run/docker/containerd/containerd.toml`为入口,看大神们如何组织Go语言代码 ...查看全部
【编者的话】containerd v1.0.0的源码分析,以` docker-containerd --config /var/run/docker/containerd/containerd.toml`为入口,看大神们如何组织Go语言代码

containerd.io
github: containerd v1.0.0

# 准备:
```
go get -v github.com/containerd/containerd
cd $GOPATH/src/github.com/containerd/containerd
git checkout v1.0.0 -b v1.0.0
```
# 分析

### 命令行
* 位置:`cmd/containerd/main.go`
* 作用:定义containerd 命令行相关参数与配置文件
* 依赖:github.com/urfave/cli


package main

import (
"fmt"
"os"

"github.com/containerd/containerd/version"
"github.com/urfave/cli"
)

const usage = `
__ _ __
_________ ____ / /_____ _(_)___ ___ _________/ /
/ ___/ __ \/ __ \/ __/ __ ` + "`" + `/ / __ \/ _ \/ ___/ __ /
/ /__/ /_/ / / / / /_/ /_/ / / / / / __/ / / /_/ /
\___/\____/_/ /_/\__/\__,_/_/_/ /_/\___/_/ \__,_/

high performance container runtime
`

func main() {
app := cli.NewApp()
app.Name = "containerd"
app.Version = version.Version
app.Usage = usage
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "config,c",
Usage: "path to the configuration file",
Value: defaultConfigPath,
},
}
app.Commands = []cli.Command{}
app.Action = func(context *cli.Context) error {
return nil
}
if err := app.Run(os.Args); err != nil {
fmt.Fprintf(os.Stderr, "containerd: %s\n", err)
os.Exit(1)
}
}


### 配置文件解析
* 位置:`server/config.go`
* 作用:解析配置文件相关参数
* 依赖:
github.com/BurntSushi/toml
github.com/pkg/errors


// LoadConfig loads the containerd server config from the provided path
func LoadConfig(path string, v *Config) error {
if v == nil {
return errors.Wrapf(errdefs.ErrInvalidArgument, "argument v must not be nil")
}
md, err := toml.DecodeFile(path, v)
if err != nil {
return err
}
v.md = md
return nil
}



#### 添加参数
* 位置:`cmd/containerd/main.go`
* 作用:应用参数到相关配置


func applyFlags(context [i]cli.Context, config [/i]server.Config) error {
// the order for config vs flag values is that flags will always override
// the config values if they are set
if err := setLevel(context, config); err != nil {
return err
}
for _, v := range []struct {
name string
d *string
}{
{
name: "root",
d: &config.Root,
},
{
name: "state",
d: &config.State,
},
{
name: "address",
d: &config.GRPC.Address,
},
} {
if s := context.GlobalString(v.name); s != "" {
*v.d = s
}
}
return nil
}


#### 日志
* 位置:`log/context.go`
* 作用:应用日志
* 依赖: github.com/sirupsen/logrus


...
var ctx = log.WithModule(gocontext.Background(), "containerd")
...
log.G(ctx).WithFields(logrus.Fields{
"version": version.Version,
"revision": version.Revision,
}).Info("starting containerd")
...

# log/context.go
func WithModule(ctx context.Context, module string) context.Context {
parent := GetModulePath(ctx)
if parent != "" {
if path.Base(parent) == module {
return ctx
}
module = path.Join(parent, module)
}

ctx = WithLogger(ctx, GetLogger(ctx).WithField("module", module))
return context.WithValue(ctx, moduleKey{}, module)
}


# 整理:
* github.com/llitfkitfk/containerd/tree/part-1
* jianshu

扒一扒Rancher社区中的小工具

Rancher 发表了文章 • 1 个评论 • 2647 次浏览 • 2017-01-19 09:27 • 来自相关话题

与Linux、OpenStack等成熟的技术社区相比,Rancher社区还是处于初级发展阶段,一个技术社区的成败并不是单纯的代码贡献,而学习文档的数量和代码管理作业流程也是非常重要的。如何让怀揣不同需求的工程师都能在社区中快速找到相应的解决方案,这就需要大家协 ...查看全部
与Linux、OpenStack等成熟的技术社区相比,Rancher社区还是处于初级发展阶段,一个技术社区的成败并不是单纯的代码贡献,而学习文档的数量和代码管理作业流程也是非常重要的。如何让怀揣不同需求的工程师都能在社区中快速找到相应的解决方案,这就需要大家协同合作共同促进社区发展与完善。除了我们所熟知的Rancher & RancherOS,Rancher Labs的开发团队在实践中提炼了很多实用的小工具,这些小工具虽然并不会左右Rancher发展的大局,但是在项目标准化和开发效率上给团队带来巨大的便捷。这次主要是带着大家一起来认识一下这些小工具。

Golang包管理工具-Trash

项目地址:https://github.com/rancher/trash

目前主流的编程语言 Python、Ruby、Java、Php 等已经把包管理的流程设计的犹如行云流水般流畅,一般情况下开发者是不需要操心类库包依赖管理以及升级、备份、团队协作的。Golang在1.5版本开始,官方开始引入包管理的设计,加了 vendor目录来支持本地包管理依赖,但是需要特殊设置GO15VENDOREXPERIMENT=1,在1.6时代这个特性已经是默认的了。可是vendor并没有统一的版本号管理功能,只是额外提供了project内包的依赖路径。于是Trash这个工具就应运而生了,Trash的使用非常简单,只需要有一份依赖包的描述文件即可。

描述文件 trash.conf 支持两种格式,普通方式和YAML方式,可以直接在其中描述依赖库的远程地址、版本号等,一个简单的例子(我这里使用普通格式):



然后在根目录执行trash,即可获得相关版本的依赖包:



非常轻量级,非常简洁。

Golang编译工具-Dapper

项目地址:https://github.com/rancher/dapper

我们在编译golang执行程序的时候,因为涉及到协作开发,这就会碰到一个问题,就是如何保证编译环境的一致性。环境一致性最好的办法就是使用容器技术,Dapper就是利用Docker build镜像的过程中可以执行各种命令生成容器的原理。只需在项目的根目录下创建Dockerfile.dapper 文件,这是一个参考Dockerfile标准的文件,执行 dapper 命令即可按照约定规则生成最终的执行程序,通过这种方式统一的编译环境。

几乎所有的Rancher项目都是基于Dapper来编译的,随意打开一个项目比如rancher-dns就可以看到 Dockerfile.dapper 文件:



DAPPER_SOURCE 指定了容器内的源码路径
DAPPER_OUTPUT 指定了编译后的输出路径,bin dist 目录会自动在项目根目录下创建
DAPPER_DOCKER_SOCKET 设置为True相当于docker run -v /var/run/docker.sock:/var/run/docker.sock ...
DAPPER_ENV 相当于docker run -e TAG -e REPO ...

有一点需要注意的是,目前Dapper装载源码有两种方式bind和cp,bind就是直接mount本地的源码路径,但是如果使用remote docker daemon方式那就得使用cp模式了。

Golang项目标准化工具 go-skel

项目地址:https://github.com/rancher/go-skel

介绍了包管理工具和打包编译工具,如果我们在创建一个golang项目时能把这两个工具整合起来一起使用,那就太赞了。go-skel就是提供了这样一个便利,我们直接来demo一下。

clone一份go-skel的源码,创建一个rancher-tour(./skel.sh rancher-tour)的项目:



执行完毕后,会创建一个标准的项目,包含了dapper和trash这两个工具,同时定义了一份Makefile,我们可以通过make命令来简化操作:



比如我们要执行一个ci操作,那就可以直接运行 make ci,自动运行单元测试,并在bin目录下生成最终的可执行程序:



标准项目已经创建了一些初始化代码,集成了 github.com/urfave/cli ,所以我们可以执行执行rancher-tour:



微服务辅助小工具 Giddyup

项目地址:https://github.com/cloudnautique/giddyup

一个传统的服务在容器化的过程中,通常我们会将其拆分成多个微服务,充分展现每个容器只做好一件事情这样的哲学。那么就会出现我们在Rancher中看到的sidekick容器,数据卷容器,专门负责更新配置信息的容器等等。

实际应用中我们会遇到一些问题,比如这些微服务容器的启动是无序的,无序启动会导致微服务之间可能出现连接失败,进而导致整个服务不可用;再比如我们如何来判定依赖的微服务已经正常启动,这可能需要一个health check的服务端口。giddyup就是简化这种微服务检查工作的利器,它都能做些什么呢:
Get connection strings from DNS or Rancher Metadata.
Determine if your container is the leader in the service.
Proxy traffic to the leader
Wait for service to have the desired scale.
Get the scale of the service
Get Managed-IP of the container (/self/container/primary_ip)

我们还是创建stack并在其中创建service来体验一下giddyup的部分功能,service中包含两个容器分别为主容器main和sidekick容器conf,他们共享网络栈:



我们可以在conf内启动giddyup health,会启动一个监听1620端口的http服务,服务路径是/ping:



此时我们可以在其他服务的容器内查看这个服务 health port的健康状态,通过giddyup ip stringify获取服务地址,使用giddyup probe可以查看相关状态:



可以看到probe返回了OK的信息,说明giddy/main的health port是正常的,如果我们把giddyup health的端口停掉,giddyup probe会如何?



除了开启health port功能外,还可以sleep等待service内所有容器都启动,比如刚才的service,我在main上做wait操作,同时在UI上对service扩容,可以看到为了等待完成扩容差不多sleep了3s:



更多功能可以去看一看giddyup项目的README文档,另外也可以看看catalog中的一些项目是怎么使用giddyup的,参考:https://github.com/rancher/catalog-dockerfiles

Rancher CLI 工具

项目地址: https://github.com/rancher/cli

现在我们管理Rancher的方式包括UI、API方式,为了能够和其他工具更好的融合Rancher开发了CLI工具,可以非常轻量级的嵌入到其他工具中。CLI将会在Rancher 1.2-pre2版本中正式放出,兼容性上目前来看没有支持老版本的计划,所以在老版本上出现各种问题也是正常的。

直接在 https://github.com/rancher/cli/releases 下载最新版本即可试用,将rancher cli放到PATH中,正式使用前需要初始化:



然后便可以执行各种犀利的操作,比如针对某个service进行scale操作,这比之前需要用rancher-compose cli要简洁的多:



再比如可以实时监听rancher events,可以即时查看Rancher中正在发生的各种任务,对协助排查问题,分析系统运行情况都非常有用:



后语

小工具中内含着大智慧,工具文化是工程师Team高效协作的重要标志,高质量的工具能让项目开发有事半功倍之效,其背后也蕴藏着深厚的团队文化理念,就是不计项目KPI利用个人业余时间为团队做贡献的和谐氛围。其实国内很多互联网公司都是有专门设立工具开发工程师的岗位,对工具带来的生产效率提升,其重视程度不言而喻!

原文来源:Rancher Labs

开源第二弹!数人云Mesos调度器Swan来啦

Dataman数人科技 发表了文章 • 0 个评论 • 1669 次浏览 • 2016-11-10 15:17 • 来自相关话题

继数人云容器管理面板Crane开源之后,小数有一个好消息告诉大家,数人云Mesos调度器Swan也加入了开源的大家庭!从此Crane不再寂寞,和Swan相亲相爱。开源的脚步从不停歇,下一个会是谁呢? ...查看全部

数人云容器管理面板Crane开源之后,小数有一个好消息告诉大家,数人云Mesos调度器Swan也加入了开源的大家庭!从此Crane不再寂寞,和Swan相亲相爱。开源的脚步从不停歇,下一个会是谁呢?


微信截图_20161110151714.png


Swan,为天鹅,优雅的代名词。

刚刚诞生于数人云工程师的手中的Swan,尚处于开源项目的早期,还是一只跌跌撞撞的丑小鸭,未能完全展现天鹅优雅的模样。比它早出生两个月的哥哥Crane已经在开源社区的帮助下成长许多,小数相信在大家的关怀下,Swan也会很快成长为一只真正的天鹅^ v ^

Swan基于Mesos Restful API编写的应用调度框架,可以帮助用户轻松发布应用,实现应用的滚动更新,并根据用户指定的策略做应用的健康检测和故障转移。

未来,数人云Swan团队还将努力实现调度策略、高可用服务发现、网络管理、编排,以及任务抢占等功能。


主要功能

  • 应用发布:发布应用支持应用实例名称固定,便于监控和做服务发现。
  • 应用扩缩:支持手动扩缩,扩缩过程中保持实例ID连续。
  • 滚动更新:可以指定更新的实例数目分步更新,也可以全量更新。滚动更新策略可配。
  • 版本回滚:支持更新过程中手动回滚,任意一个实例更新失败后自动回滚。
  • 版本管理:支持应用多版本管理。
  • 健康检查:支持实例级别的健康检测,可配置健康检测策略。
  • 自动容错:可配置自动容错策略,根据策略自动恢复失败的实例。
  • 优雅终止:扩缩和滚动更新过程中,支持优雅终止实例。


不多说啦,快跟随小数的步伐来一看究竟吧!
Fork me on GitHub!
https://github.com/Dataman-Cloud/swan


数人云现有企业版产品数人云操作系统和多个开源项目。数人云操作系统针对企业客户,帮助传统企业实现IT业务转型,更好地应对业务变化;开源项目容器管理面板Crane针对开发者,强调简单易用,Mesos调度器Swan用于Mesos环境应用管理。

Docker与Golang的巧妙结合

cyeaaa 发表了文章 • 0 个评论 • 25334 次浏览 • 2016-09-22 20:07 • 来自相关话题

【编者的话】这是一个展示在使用Go语言时如何让Docker更有用的提示与技巧的简辑。例如,如何使用不同版本的Go工具链来编译Go代码,如何交叉编译到不同的平台(并且测试结果!),或者如何制作真正小的容器镜像。 下面的文章假定你已经安装 ...查看全部
【编者的话】这是一个展示在使用Go语言时如何让Docker更有用的提示与技巧的简辑。例如,如何使用不同版本的Go工具链来编译Go代码,如何交叉编译到不同的平台(并且测试结果!),或者如何制作真正小的容器镜像。

下面的文章假定你已经安装了Docker。不必是最新版本(这篇文章不会使用Docker任何花哨的功能)。
# 没有go的Go
...意思是:“不用安装`go`就能使用Go”

如果你写Go代码,或者你对Go语言有一点点兴趣,你肯定要安装了Go编译器和Go工具链,所以你可能想知道:“重点是什么?”;但有些情况下,你想不安装`Go`就来编译Go。

  • 机器上依旧有老版本Go 1.2(你不能或不想更新),不得不使用这个代码库,需要一个高版本的工具链。
  • 想使用Go1.5的交叉编译功能(例如,确保能从一个Linux系统创建操作系统X的二进制文件)。
  • 想拥有多版本的Go,但不想完全弄乱系统。
  • 想100%确定项目和它所有的依赖,下载,建立和运行在一个纯净的系统上。
如果遇到上述情况,找Docker来解决!## 在容器里编译一个程序当你安装了Go,你可以执行`go get -v github.com/user/repo`来下载,创建和安装一个库。(`-v`只是信息显示,如果你喜欢工具链快速和静默地运行,可以将它移除!)你也可以执行`go get github.com/user/repo/...`来下载,创建和安装那个repo(包括库和二进制文件)里面所有的东西。我们可以在一个容器里面这样做!试试这个:
docker run golang go get -v github.com/golang/example/hello/...
这将拉取golang镜像(除非你已经有了,那它会马上启动),并且创建一个基于它的容器。在那个容器里,go会下载一个“hello world”的例子,创建它,安装它。但它会把它安装到这个容器里……我们现在怎么运行那个程序呢?##在容器里运行程序一个办法是提交我们刚刚创建的容器,即,打包它到一个新的镜像:
docker commit $(docker ps -lq) awesomeness
注意:`docker ps –lq`输出最后一个执行的容器的ID(只有ID!)。如果你是机器的唯一用户,并且你从上一个命令开始没有创建另一个容器,那这个容器就是你刚刚创建的“hello world”的例子。现在,可以用刚刚构建的镜像创建容器来运行程序:
docker run awesomeness hello
输出会是`Hello, Go examples!`。## 闪光点当用`docker commit`构建镜像时,可以用`--change`标识指定任意Dockerfile命令。例如,可以使用一个`CMD`或者`ENTRYPOINT`命令以便`docker run awesomeness`自动执行hello。##在一次性容器上运行如果不想创建额外的镜像只想运行这个Go程序呢?使用:
docker run --rm golang sh -c \"go get github.com/golang/example/hello/... && exec hello"
等等,那些花哨的东西是什么?
  • `--rm` 告诉Docker CLI一旦容器退出,就自动发起一个`docker rm`命令。那样,不会留下任何东西。
  • 使用shell逻辑运算符`&&`把创建步骤(`go get`)和执行步骤(`exec hello`)联接在一起。如果不喜欢shell,`&&`意思是“与”。它允许第一部分`go get...`,并且如果(而且仅仅是如果!)那部分运行成功,它将执行第二部分(`exec hello`)。如果你想知道为什么这样:它像一个懒惰的`and`计算器,只有当左边的值是`true`才计算右边的。
  • 传递命令到`sh –c`,因为如果是简单的做`docker run golang "go get ... && hello"`,Docker将试着执行名为`go SPACE get SPACE etc`的程序。并且那不会起作用。因此,我们启动一个shell,并让shell执行命令序列。
  • 使用`exec hello`而不是`hello`:这将使用hello程序替代当前的进程(我们刚才启动的shell)。这确保`hello`在容器里是PID 1。而不是shell的是PID 1而`hello`作为一个子进程。这对这个微小的例子毫无用处,但是当运行更有用的程序,这将允许它们正确地接收外部信号,因为外部信号是发送给容器里的PID 1。你可能会想,什么信号啊?好的例子是`docker stop`,发送`SIGTERM`给容器的PID 1。
##使用不同版本的Go当使用`golang`镜像,Docker扩展为`golang:latest,`将(像你所猜的)映射到Docker Hub上的最新可用版本。如果想用一个特定的Go版本,很容易:在镜像名字后面用那个版本做标签指定它。例如,想用Go 1.5,修改上面的例子,用`golang:1.5`替换`golang`:
docker run --rm golang:1.5 sh -c \ "go get github.com/golang/example/hello/... && exec hello"
你能在Docker Hub的Golang镜像页面上看到所有可用的版本(和变量)。##在系统上安装好了,如果想在系统上运行编译好的程序,而不是一个容器呢?我们将复制这个编译了的二进制文件到容器外面。注意,仅当容器架构和主机架构匹配的时候,才会起作用;换言之,如果在Linux上运行Docker。(我排除的可能是运行Windows容器的人!)最容易在容器外获得二进制文件的方法是映射`$GOPATH/bin`目录到一个本地目录,在`golang`容器里,`$GOPATH`是`/go.`所以我们可以如下操作:
docker run -v /tmp/bin:/go/bin \ golang go get github.com/golang/example/hello/... /tmp/bin/hello
如果在Linux上,将看到`Hello, Go examples!`消息。但如果是,例如在Mac上,可能会看到:
-bash:/tmp/test/hello: cannot execute binary file
我们又能做什么呢?##交叉编译Go 1.5具备优秀的开箱即用交叉编译能力,所以如果你的容器操作系统和/或架构和你的系统不匹配,根本不是问题!开启交叉编译,需要设置`GOOS`和/或`GOARCH`。例如,假设在64位的Mac上:
docker run -e GOOS=darwin -e GOARCH=amd64 -v /tmp/crosstest:/go/bin \ golang go get github.com/golang/example/hello/...
交叉编译的输出不是直接在`$GOPATH/bin`,而是在`$GOPATH/bin/$GOOS_$GOARCH.`。换言之,想运行程序,得执行`/tmp/crosstest/darwin_amd64/hello.`。##直接安装到$PATH如果在Linux上,甚至可以直接安装到系统bin目录:
docker run -v /usr/local/bin:/go/bin \ golang get github.com/golang/example/hello/...
然而,在Mac上,尝试用`/usr`作为一个卷将不能挂载Mac的文件系统到容器。会挂载Moby VM(小Linux VM藏在工具栏Docker图标的后面)的`/usr`目录。(译注:目前Docker for Mac版本可以自定义设置挂载路径)但可以使用`/tmp`或者在你的home目录下的什么其它目录,然后从这里复制。# 创建依赖镜像我们用这种技术产生的Go二进制文件是静态链接的。这意味着所有需要运行的代码包括所有依赖都被嵌入了。动态链接的程序与之相反,不包含一些基本的库(像“libc”)并且使用系统范围的复制,是在运行时确定的。这意味着可以在容器里放弃Go编译好的程序,没有别的,并且它会运行。我们试试!##scratch镜像Docker生态系统有一个特殊的镜像:`scratch.`这是一个空镜像。它不需要被创建或者下载,因为定义的就是空的。给新的Go依赖镜像创建一个新的空目录。在这个新目录,创建下面的Dockerfile:
FROM scratch COPY ./hello /hello ENTRYPOINT ["/hello"]
这意味着:从scratch开始(一个空镜像),增加`hello`文件到镜像的根目录,*定义`hello`程序为启动这个容器后默认运行的程序。然后,产生`hello`二进制文件如下:
docker run -v $(pwd):/go/bin --rm \ golang go get github.com/golang/example/hello/...
注意:不需要设置`GOOS`和`GOARCH`,正因为,想要一个运行在Docker容器里的二进制文件,不是在主机上。所以不用设置这些变量!然后,创建镜像:
docker build -t hello .
测试它:
docker run hello
(将显示“Hello, Go examples!”)最后但不重要,检查镜像的大小:
docker images hello
如果一切做得正确,这个镜像大约2M。相当好!##构建东西而不推送到Github当然,如果不得不推送到GitHub,每次编译都会浪费很多时间。想在一个代码段上工作并在容器中创建它时,可以在`golang`容器里挂载一个本地目录到`/go`。所以`$GOPATH`是持久调用:`docker run -v $HOME/go:/go golang ....`但也可以挂载本地目录到特定的路径上,来“重载”一些包(那些在本地编辑的)。这是一个完整的例子:
# Adapt the two following environment variables if you are not running on a Mac export GOOS=darwin GOARCH=amd64 mkdir go-and-docker-is-love cd go-and-docker-is-love git clone git://github.com/golang/example cat example/hello/hello.go sed -i .bak s/olleH/eyB/ example/hello/hello.go docker run --rm \ -v $(pwd)/example:/go/src/github.com/golang/example \ -v $(pwd):/go/bin/${GOOS}_${GOARCH} \ -e GOOS -e GOARCH \golang go get github.com/golang/example/hello/... ./hello # Should display "Bye, Go examples!" 
# 网络包和CGo的特殊情况进入真实的Go代码世界前,必须承认的是:在二进制文件上有一点点偏差。如果在使用CGo,或如果在使用`net`包,Go链接器将生成一个动态库。这种情况下,`net`包(里面确实有许多有用的Go程序!),罪魁祸首是DNS解析。大多数系统都有一个花哨的,模块化的名称解析系统(像名称服务切换),它依赖于插件,技术上,是动态库。默认地,Go将尝试使用它;这样,它将产生动态库。我们怎么解决?##重用另一个版本的libc一个解决方法是用一个基础镜像,有那些程序功能所必需的库。几乎任何“正规”基于GNU libc的Linux发行版都能做到。所以,例如,使用`FROM debian`或`FROM fedora`,替代`FROM scratch`。现在结果镜像会比原来大一些;但至少,大出来的这一点将和系统里其它镜像共享。注意:这种情况不能使用Alpine,因为Alpine是使用musl库而不是GNU libc。##使用自己的libc另一个解决方案是像做手术般地提取需要的文件,用`COPY`替换容器里的。结果容器会小。然而,这个提取过程困难又繁琐,太多更深的细节要处理。如果想自己看,看看前面提到的`ldd`和名称服务切换插件。##用netgo生成静态二进制文件我们也可以指示Go不用系统的libc,用本地DNS解析代替Go的`netgo`。要使用它,只需在`go get`选项加入`-tags netgo -installsuffix netgo`。
  • `-tags netgo`指示工具链使用`netgo`。
  • `-installsuffix netgo`确保结果库(任何)被一个不同的,非默认的目录所替代。如果做多重`go get`(或`go build`)调用,这将避免代码创建和用不用netgo之间的冲突。如果像目前我们讲到的这样,在容器里创建,是完全没有必要的。因为这个容器里面永远没有其他Go代码要编译。但它是个好主意,习惯它,或至少知道这个标识存在。
# SSL证书的特殊情况还有一件事,你会担心,你的代码必须验证SSL证书;例如,通过HTTPS联接外部API。这种情况,需要将根证书也放入容器里,因为Go不会捆绑它们到二进制文件里。##安装SSL证书再次,有很多可用的选择,但最简单的是使用一个已经存在的发布里面的包。Alpine是一个好的选择,因为它非常小。下面的`Dockerfile`将给你一个小的基础镜像,但捆绑了一个过期的跟证书:
FROM alpine:3.4RUN apk add --no-cache ca-certificates apache2-utils
来看看吧,结果镜像只有6MB!注意:`--no-cache`选项告诉`apk`(Alpine包管理器)从Alpine的镜像发布上获取可用包的列表,不保存在磁盘上。你可能会看到Dockerfiles做这样的事`apt-get update && apt-get install ... && rm -rf /var/cache/apt/*`;这实现了(即在最终镜像中不保留包缓存)与一个单一标志相当的东西。一个附加的回报:把你的应用程序放入基于Alpine镜像的容器,让你获得了一堆有用的工具。如果需要,现在你可以吧shell放入容器并在它运行时做点什么。# 打包 我们看到Docker如何帮助我们在干净独立的环境里编译Go代码;如何使用不同版本的Go工具链;以及如何在不同的操作系统和平台之间交叉编译。我们还看到Go如何帮我们给Docker创建小的,容器依赖镜像,并且描述了一些静态库和网络依赖相关的微妙联系(没别的意思)。除了Go是真的适合Docker项目这个事实,我们希望展示给你的是,Go和Docker如何相互借鉴并且一起工作得很好!##致谢这最初是在2016年GopherCon骇客日提出的。我要感谢所有的校对材料、提出建议和意见让它更好的人,包括但不局限于:
所有的错误和拼写错误都是我自己的;所有的好东西都是他们的!

原文链接:Docker + Golang = <3(翻译:陈晏娥 审校:田浩浩

使用Docker和Golang进行便捷的MongoDB测试

吴锦晟 发表了文章 • 1 个评论 • 8192 次浏览 • 2015-01-14 17:07 • 来自相关话题

【编者的话】Docker的使用场景之一就是测试,在测试中,我们有时候会由于超时或者仅仅因为两个开发版本使用相同的数据库在同时运行而导致测试出错。本文以Golang和MongoDB为例,介绍了如何使用Docker来简化和改进单元测试。 ...查看全部
【编者的话】Docker的使用场景之一就是测试,在测试中,我们有时候会由于超时或者仅仅因为两个开发版本使用相同的数据库在同时运行而导致测试出错。本文以Golang和MongoDB为例,介绍了如何使用Docker来简化和改进单元测试。

背景
我们正在不断寻找新技术来解决开发中遇到的问题。我们一直在使用Java+Spring,然而Java 8和Spring Boot为我们带来了新的生机,并改变了单一的Java应用为微服务模式(译者注:monolithic Java applications)。而当你有API的时候,你只需一个合适的前端框架就可以替代jsp和jQuery:在我们的案例中我们选择AngularJS。两年前我们第一次使用了Angular,现在所有的项目都引入了AngularJS。

超过10年的Java 在你的灵魂深处留下了深刻的印记
这两三年来,我一直在寻找更好的东西。在这个行业中,最好的事情是你会有各种选择。我们曾经使用NodeJS构建了几个项目,并学习了Ruby的服务配置管理框架Chef。当然我们也有一些Scala项目,并了解过Clojure、Haskell和Rust语言,后来我们发现了Go。虽然我们只使用Go语言编写了几个小服务,但是却对与Go相关的语言、标准库、工具和社区而震惊。有大量的博客文章解释了为什么不同的公司选择了Go,本文不再赘述。同时如果你想学习如何编写Go,可以查阅A tour of Go,如果你喜欢阅读请查看Effective Go或观看A tour of Go视频。

负载测试
可能我需要相当长的篇幅来介绍负载测试,所有的编程语言都需要编写单元测试代码,另外还有一些需要使用TDD方法和达到100%测试覆盖率的目标的方法。动态语言需要安排更多类型的测试,可能当你经过了上百次的测试,你的应用才能达到一个稳定的状态。痛苦的是,由于有不同的开发语言,所以你的测试需要很多的准备工作:曾经几秒钟就可以完成的事情,那现在可能会需要几分钟,甚至是几十分钟才能完成。因此,你要开始仓库(数据库)的调用,并建立集成测试的数据库开发的预载和清除方法。有时候集成测试可能会失败,而这可能是由于超时或者仅仅因为两个开发版本使用相同的数据库在同时运行。

使用Golang和Docker进行的测试
Golang不会有类似的问题,有了Golang的快速构建、测试周期和一些Docker魔法的支持,你能够在几秒内启动MongoDB Docker容器并运行所有的测试。这个真的是从开始到结束只需要几秒的时间,但是第一次运行除外,因为第一次运行的时候需要下载和提供MongoDB Docker容器。

我从这里得到真正的灵感,即使一直在寻找借口来确认这是否是正确的:

3AF91CF4-74A0-4577-98C9-ECE3FFE7C014.jpg


让我们做一些可以进行Docker实验的nice的事情
我已经研究Golang+AngularJS一段时间了,而且现在是最佳时间来证明Docker是否如宣传的一样神奇。对于OS X用户来说,涉及到Docker时会有个小烦恼:它只在Linux上面运行。是的,你可以运用Boot2Docker来安装到OS X上,而Boot2Docker将在虚拟化的Linux上运行Docker。我已经通过Ubuntu来使用Vagrant作为开发环境,因此我刚刚在这上面安装了Docker。

首先,我要熟悉Camlistore的实施环境并且复制它。特别感谢Brad Fitzpartick,你通过Camlistore和Golang标准程序库来完成了出色的工作。Thanks!

可以通过story_test.go来找到实际测试。对于那些看不懂Golang的用户,我已经在最重要的代码部分添加了额外的注释。

Setup test environment
func TestStoryCreateAndGet(t *testing.T) {

// Start MongoDB Docker container
//
// One of the most powerful features in Golang
// is the ability to return multiple values from functions.
// In this we get:
// - containerID (type=ContainerID struct)
// - ip (type=string)
containerID, ip := dockertest.SetupMongoContainer(t)

// defer schedules KillRemove(t) function call to run immediatelly
// when TestStoryCreateAndGet(t) function is done,
// so you can place resource clenup code close to resource allocation
defer containerID.KillRemove(t)

app := AppContext{}

// Connect to Dockerized MongoDB
mongoSession, err := mgo.Dial(ip)

// Golang favors visible first hand error handling.
// Main idea is that Errors are not exceptional so you should handle them
if err != nil {
Error.Printf("MongoDB connection failed, with address '%s'.", Configuration.MongoUrl)
}

// close MongoDB connections when we're finished
defer mongoSession.Close()

app.mongoSession = mongoSession

// create test http server with applications route configuration
ts := httptest.NewServer(app.createRoutes())
defer ts.Close()

storyId := testCreate(ts, t) // run create test
testGet(ts, storyId, t) // run get test for created story
}



Post json document to http handler
func testCreate(ts [i]httptest.Server, t [/i]testing.T) string {

postData := strings.NewReader("{\"text\":\"tekstiä\",\"subjectId\":\"k2j34\",\"subjectUrl\":\"www.fi/k2j34\"}")

// create http POST with postData JSON
res, err := http.Post(ts.URL+"/story", applicationJSON, postData)

// read http response body data
data, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
t.Error(err)
}

id := string(data)

// verify that we got correct http status code
if res.StatusCode != http.StatusCreated {
t.Fatalf("Non-expected status code: %v\n\tbody: %v, data:%s\n", http.StatusCreated, res.StatusCode, id)
}

// verify that we got valid lenght response data
if res.ContentLength != 5 {
t.Fatalf("Non-expected content length: %v != %v\n", res.ContentLength, 5)
}
return id
}



Test that previously created story exists
func testGet(ts [i]httptest.Server, storyId string, t [/i]testing.T) {

// create http GET request with correct path
res, err := http.Get(ts.URL + "/story/" + storyId)
data, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
t.Error(err)
}

body := string(data)

// validate status code
if res.StatusCode != http.StatusOK {
t.Fatalf("Non-expected status code: %v\n\tbody: %v, data:%s\n", http.StatusCreated, res.StatusCode, body)
}

// validate that response has correct storyId
if !strings.Contains(body, "{\"storyId\":\""+storyId+"\",") {
t.Fatalf("Non-expected body content: %v", body)
}

// validate that content leght is what is should be
if res.ContentLength < 163 && res.ContentLength > 165 {
t.Fatalf("Non-expected content length: %v < %v, content:\n%v\n", res.ContentLength, 160, body)
}

}


眼见为实
因此,启动MongoDB Docker容器,将它配置到应用程序,然后用内置的测试直至创建HTTP服务器。然后,我们设置同样的路由给服务器,并对测试服务器运行两个请求,第一个来创建故事评论,另外一个来获取它。所有的数据都被存储了,并且从MongoDB中获取。那么所有这一切需要多久时间呢?


docker-testing-0d333fd6d1b21887e978d4d3110dc715.png

仅两秒以下!

docker-testing-with-race-detection-567c060af6235504c42111c3c5d33446.png

即使你运行一些条件选择器它仍只需要不到3秒 \o/

Docker是针对所有的用户,而不仅仅是Golang用户

对于那些可以使用Golang的用户,Docker也可以帮助你。它当然没有Golang那么快速,但是和使用外部的MongoDB服务器一样的快,而且没有额外的清理麻烦。毫无疑问,Docker是虚拟化业务中的游戏变化者,并且这些炒作也得到了很好的回报。这样就没有借口来针对MongoDB功能编写任何模拟测试。

原文链接:Painless MongoDB testing with Docker and Golang(翻译:吴锦晟 校对:李颖杰)

===============================================
译者介绍
吴锦晟,大连理工大学硕士研究生,就职于上海金桥信息股份有限公司技术中心。目前负责云计算、虚拟化、大数据及其信息可视化等方向的研究和应用。希望通过翻译技术文章于Dockone社区为Docker的步道做出微薄贡献。