标签 Go 下的文章

源创会2017沈阳站讲稿:基于Harbor的高可用企业级私有容器镜像仓库部署实践

上周六开源中国源创会在沈阳举办了一次技术活动,很荣幸以本地讲师的身份和大家交流了一个topic: “基于Harbor的高可用企业级私有容器镜像仓库部署实践”。之所以选择这个topic,是因为这是我们团队的项目实践心得。很多企业和组织在深入使用Docker之后,都会有类似的高可用私有容器仓库搭建的需求,于是我就把我们摸索的实践和填坑过程拿出来,用30分钟与大家分享一下。另外这算是一个入门级的分享,并未深入过多原理。以下就是本次分享的内容讲稿整理。如有不妥或不正确的地方,欢迎交流指正。

img{512x368}

大家下午好,欢迎各位来到源创会沈阳站。在这里我也代表沈阳的IT人欢迎源创会来到沈阳,希望能有更多的像源创会这样的组织到沈阳举办技术活动。非常高兴能有这个机会在源创会这个平台上做分享, 今天和大家一起探讨的题目是:“基于Harbor的高可用企业级私有容器镜像仓库部署实践”。题目有些长,简单来说就是如何搭建一个好用的镜像仓库。

img{512x368}

首先做个简单的自我介绍。我叫白明,东软(注:源创会这次活动的会场在东软沈阳园区)是我的主场,在这里工作很多年,目前就职东软云科技;Gopher一枚,近两年主要使用Go语言开发;技术译者,曾参与翻译过《七周七语言》一书;并且参与过智慧城市架构系列丛书的编著工作;GopherChina大会讲师,这里顺便说一下GopherChina大会,它是目前中国地区规模最大、水平最高的Go语言技术大会,一般每年4月份在北京或上海举行。希望有志于Go语言开发的开发者积极参与;Blogger,写博10多年,依旧笔耕不倦;目前主要从事Docker&kubernetes的研究和实践。

当今,IT技术发展飞快。五年前, IT从业者口中谈论最多的技术是Virtual Machine,即虚拟化技术,人们经常争论的是到底是vmware的技术好,还是原生kvm技术稳定,又或是xen的技术完美。转眼间五年过去了,大家口中经常讨论的技术词汇发生了变化,越来越多的技术人在谈论Docker,谈论容器。

Docker是什么? Docker这门技术非常热,但我们要透过现象看其本质:

Docker技术并不是新技术,而是将已有技术进行了更好的整合和包装

内核容器技术以一种完整形态最早出现在Sun公司Solaris操作系统上,Solaris是当时最先进的服务器操作系统。2005年Solaris发布Solaris Container技术,从此开启了内核容器之门。

IT技术发展的趋势就是这样:商业有的,开源也要有。三年后,即2008年,以Google公司开发人员为主导的Linux Container,LXC功能在被merge到Linux内核。LXC是一种内核级虚拟化技术,主要基于namespacescgroup技术,实现共享一个os kernel前提下的进程资源隔离,为进程提供独立的虚拟执行环境,这样的一个虚拟的执行环境就是一个容器。本质上说,LXC容器与现在的Docker所提供容器是一样的。但是,当时LXC处于早期阶段,开发人员可能更为关注LXC的技术实现,而对开发体验方面有所忽略,导致LXC技术使用门槛较高,普通应用开发者学习、理解和使用它的心智负担较高,因此应用并不广泛。

这一情况一直持续到2013年,当时美国一家名不见经传的公司dotCloud发布了一款平台工具Docker,对外宣称可以实现:“build,ship and run any app and anywhere”。Docker实质上也是基于namespaces和cgroup技术的,Docker的创新之处在于其基于union fs技术定义了一套应用打包规范,真正将应用及其运行的所有依赖都封装到一个特定格式的文件中,这种文件就被称为image,即镜像文件。同时,Docker还提供了一套抽象层次更高的工具集,这套工具对dev十分友好,具有良好的开发体验(Developer eXperience),开发者无需关心namespace, cgroups之类底层技术,即可很easy的启动一个承载着其应用的容器:

Docker run ubuntu echo hello

因此, 从2013发布以来,Docker项目就像坐上了火箭,发展迅猛,目前已经是github上最火爆的开源项目之一。这里还要提一点就是:Docker项目是使用go语言开发的,Docker项目的成功,也或多或少得益于Go优异的开发效率和执行效率。

Docker技术的出现究竟给我们带来了哪些好处呢,个人觉得至少有以下三点:

  • 交付标准化:Docker使得应用程序和依赖的运行环境真正绑定结合为一体,得之即用。这让开发人员、测试和运维实现了围绕同一交付物,保持开发交付上下文同步的能力,即“test what you write, ship what you test”;
  • 执行高效化:应用的启动速度从原先虚拟机的分钟级缩短到容器的秒级甚至ms级,使得应用可以支持快速scaling伸缩;
  • 资源集约化:与vm不同的是,Container共享一个内核,这使得一个container的资源消耗仅为进程级别或进程组级别。同时,容器的镜像也因为如此,其size可以实现的很小,最小可能不足1k,平均几十M。与vm动辄几百兆的庞大身段相比,具有较大优势。

有了image文件后,自然而言我们就有了对image进行存取和管理的需求,即我们需要一个镜像仓库,于是Docker推出了Docker registry这个项目。Docker Registry就是Docker image的仓库,用来存储、管理和分发image的;Docker registry由Docker公司实现,项目名为distribution,其实现了Docker Registr 2.0协议,与早前的Registry 1.x协议版本相比,Distribution采用Go语言替换了Python,在安全性和性能方面都有了大幅提升;Docker官方运行着一个世界最大的公共镜像仓库:hub.docker.com,最常用的image都在hub上,比如反向代理nginx、redis、ubuntu等。鉴于国内访问hub网速不佳,多使用国内容器服务厂商提供的加速器。Docker官方还将Registry本身打入到了一个image中,方便开发人员快速以容器形式启动一个Registry:

docker run -d -p 5000:5000 --restart=always --name registry registry:2

不过,这样启动的Registry更多仅仅是一个Demo级别或满足个体开发者自身需要的,离满足企业内部开发流程或生产需求还差了许多。

既然Docker官方运行着免费的镜像仓库,那我们还需要自己搭建吗?实际情况是,对Docker的使用越深入,对私有仓库的需求可能就越迫切。我们先来看一组Docker 2016官方的调查数据,看看Docker都应用在哪些场合。 从Docker 2016官方调查来看,Docker 更多用于dev、ciDevOps等环节,这三个场合下的应用占据了半壁江山。而相比于公共仓库,私有镜像仓库能更好的满足开发人员在这些场合对镜像仓库的需求。理由至少有四点:

  • 便于集成到内部CI/Cd
    以我司内部为例,由于公司内部办公需要使用正向代理访问外部网络,要想将Public Registry集成到你的内部CI中,技术上就会有很多坎儿,整个搭建过程可能是非常痛苦的;

  • 对镜像可以更全面掌控
    一般来说,外部Public Registry提供的管理功能相对单一,往往无法满足企业内部的开发和交付需求;

  • 内部网络,网络传输性能更好
    内部开发运维流水线很多环节是有一定的时间敏感性的,比如:一次CI如果因为network问题导致image pull总是timeout,会让dev非常闹心,甚至影响整体的开发和交付效率。

  • 出于安全考虑
    总是有企业不想将自己开发的软件或数据放到公网上,因此在企业内部选择搭建一个private registry更会让这些企业得到满足;另外企业对仓库的身份验证可能还有LDAP支持的需求,这是外部registry无法满足的。

一旦企业决定搭建自己的private仓库,那么就得做一个private仓库的技术选型。商业版不在我们讨论范围内,我们从开源软件中挑选。不过开源的可选的不多,Docker 官方的Registry更聚焦通用功能,没有针对企业客户需求定制,开源领域我们大致有两个主要候选者:SUSEPortus和Vmware的Harbor。针对开源项目的技术选型,我个人的挑选原则最简单的就是看社区生态,落实到具体的指标上包括:

  • 项目关注度(即star数量)
  • 社区对issue的反馈数量和积极性
  • 项目维护者对issue fix的积极程度以及是否有远大的roadmap

对比后,我发现在这三个指标上,目前Harbor都暂时领先portus一段距离,于是我们选择Harbor。

Harbor是VMware中国团队开源的企业级镜像仓库项目,聚焦镜像仓库的企业级需求,这里从其官网摘录一些特性,大家一起来看一下:

– 支持基于角色的访问控制RBAC;
– 支持镜像复制策略(PUSH);
– 支持无用镜像数据的自动回收和删除; – 支持LDAP/AD认证;
– Web UI;
– 提供审计日志功能;
– 提供RESTful API,便于扩展;
– 支持中文&部署Easy。

不过,Harbor默认安装的是单实例仓库,并非是高可用的。对于接纳和使用Docker的企业来说,镜像仓库已经企业内部开发、交付和运维流水线的核心,一旦仓库停掉,流水线将被迫暂停,对开发交付的效率会产生重要影响;对于一些中大型企业组织,单实例的仓库性能也无法满足需求,为此高可用的Harbor势在必行。在设计Harbor HA方案之前,我们简单了解一下Harbor组成架构。

一个Harbor实例就是一组由docker-compose工具启动的容器服务,主要包括四个主要组件:

  • proxy
    实质就是一个反向代理nginx,负责流量路由分担到ui和registry上;

  • registry
    这里的registry就是原生的docker官方的registry镜像仓库,Harbor在内部内置了一个仓库,所有仓库的核心功能均是由registry完成的;

  • core service
    包含了ui、token和webhook服务;

  • job service
    主要用于镜像复制供。

同时,每个Harbor实例还启动了一个MySQL数据库容器,用于保存自身的配置和镜像管理相关的关系数据。

高可用系统一般考虑三方面:计算高可用、存储高可用和网络高可用。在这里我们不考虑网络高可用。基于Harbor的高可用仓库方案,这里列出两个。

img{512x368}

两个方案的共同点是计算高可用,都是通过lb实现的多主热运行,保证无单点;存储高可用则各有各的方案。一个使用了分布式共享存储,数据可靠性由共享存储provider提供;另外一个则需要harbor自身逻辑参与,通过镜像相互复制的方式保持数据的多副本。

两种方案各有优缺点,就看哪种更适合你的组织以及你手里的资源是否能满足方案的搭建要求。

方案1是Harbor开发团队推荐的标准方案,由于基于分布式共享存储,因此其scaling非常好;同样,由于多Harbor实例共享存储,因此可以保持数据是实时一致的。方案1的不足也是很明显的,第一:门槛高,需要具备共享存储provider;第二搭建难度要高于第二个基于镜像复制的方案。

方案2的优点就是首次搭建简单。不足也很多:scaling差,甚至是不能,一旦有三个或三个以上节点,可能就会出现“环形复制”;镜像复制需要时间,因此存在多节点上数据周期性不一致的情况;Harbor的镜像复制规则以Project为单位配置,因此一旦新增Project,需要在每个节点上手工维护复制规则,非常繁琐。因此,我们选择方案1。

我们来看一下方案1的细节: 这是一幅示意图。

  • 每个安放harbor实例的node都mount cephfs。ceph是目前最流行的分布式共享存储方案之一;
  • 每个node上的harbor实例(包含组件:ui、registry等)都volume mount node上的cephfs mount路径;
  • 通过Load Balance将request流量负载到各个harbor实例上;
  • 使用外部MySQL cluster替代每个Harbor实例内部自维护的那个MySQL容器;对于MySQL cluster,可以使用mysql galera cluster或MySQL5.7以上版本自带的Group Replication (MGR) 集群。
  • 通过外部Redis实现访问Harbor ui的session共享,这个功能是Harbor UI底层MVC框架-beego提供的。

接下来,我们就来看具体的部署步骤和细节。

环境和先决条件:

  • 三台VM(Ubuntu 16.04及以上版本);
  • CephFS、MySQL、Redis已就绪;
  • Harbor v1.1.0及以上版本;
  • 一个域名:hub.tonybai.com:8070。我们通过该域名和服务端口访问Harbor,我们可以通过dns解析多ip轮询实现最简单的Load balance,虽然不完美。

第一步:挂载cephfs

每个安装Harbor instance的节点都要mount cephfs的相关路径,步骤包括:

#安装cephfs内核驱动
apt install ceph-fs-common

# 修改/etc/fstab,添加挂载指令,保证节点重启依旧可以自动挂载cephfs
xx.xx.xx.xx:6789:/apps/harbor /mnt/cephfs/harbor ceph name=harbor,secretfile=/etc/ceph/a dmin.secret,noatime,_netdev 0 2

这里涉及一个密钥文件admin.secret,这个secret文件可以在ceph集群机器上使用ceph auth tool生成。

img{512x368}

前面提到过每个Harbor实例都是一组容器服务,这组容器启动所需的配置文件是在Harbor正式启动前由prepare脚本生成的,Prepare脚本生成过程的输入包括:harbor.cfg、docker-compose.yml和common/templates下的配置模板文件。这也是部署高可用Harbor的核心步骤,我们逐一来看。

第二步:修改harbor.cfg

我们使用域名访问Harbor,因此我们需要修改hostname配置项。注意如果要用域名访问,这里一定填写域名,否则如果这里使用的是Harbor node的IP,那么在后续会存在client端和server端仓库地址不一致的情况;

custom_crt=false 关闭 crt生成功能。注意:三个node关闭其中两个,留一个生成一套数字证书和私钥。

第三步:修改docker-compose.yml

docker-compose.yml是docker-compose工具标准配置文件,用于配置docker-compose即将启动的容器服务。针对该配置文件,我们主要做三点修改:

  • 修改volumes路径
    由/data/xxx 改为:/mnt/cephfs/harbor/data/xxx
  • 由于使用外部Mysql,因此需要删除mysql service以及其他 service对mysql service的依赖 (depends_on)
  • 修改对proxy外服务端口 ports: 8070:80

第四步:配置访问external mysql和redis

external mysql的配置在common/templates/adminserver/env中,我们用external Mysql的访问方式覆盖下面四项配置:

MYSQL_HOST=harbor_host
MYSQL_PORT=3306
MYSQL_USR=harbor
MYSQL_PWD=harbor_password

还有一个关键配置,那就是将RESET由false改为true。只有改为true,adminserver启动时,才能读取更新后的配置

RESET=true

Redis连接的配置在common/templates/ui/env中,我们需要新增一行:

_REDIS_URL=redis_ip:6379,100,password,0

第五步:prepare并启动harbor

执行prepare脚本生成harbor各容器服务的配置;在每个Harbor node上通过下面命令启动harbor实例:

docker-compose up -d

启动后,可以通过docker-compose ps命令查看harbor实例中各容器的启动状态。如果启动顺利,都是”Up”状态,那么我们可以在浏览器里输入:http://hub.tonybai.com:8070,不出意外的话,我们就可以看到Harbor ui的登录页面了。

至此,我们的高可用Harbor cluster搭建过程就告一段落了。

Troubleshooting

不过,对Harbor的认知还未结束,我们在后续使用Harbor的过程中遇到了一些问题,这里举两个例子。

问题1: docker login hub.tonybai.com:8070 failed

现象日志:

Error response from daemon: Get https://hub.tonybai.com:8070/v1/users/: http: server gave HTTP response to HTTPS client

通过错误日志分析应该是docker daemon与镜像仓库所用协议不一致导致。docker engine默认采用https协议访问仓库,但之前我们搭建的Harbor采用的是http协议提供服务,两者不一致。

解决方法有两种,这里列出第一种:让docker引擎通过http方式访问harbor仓库:

在/etc/docker/daemon.json中添加insecure-registry:

{
    "insecure-registries": ["hub.tonybai.com:8070"]
}

重启docker service生效

第二种方法就是让Harbor支持https,需要为harbor的proxy配置私钥和证书,位置:harbor.cfg中

#The path of cert and key files for nginx, they are applied only the protocol is set to https
ssl_cert = /data/cert/server.crt
ssl_cert_key = /data/cert/server.key

这里就不细说了。

问题2:docker login hub.tonybai.com:8070 有时成功,有时failed

现象日志:

第一次登录成功:
# docker login -u user -p passwd http://hub.tonybai.com:8070 Login Succeeded

第二次登录失败:
# docker login -u user -p passwd http://hub.tonybai.com:8070
Error response from daemon: login attempt to http://hub.tonybai.com:8070/v2/ failed with status: 401 Unauthorized

这个问题的原因在于对docker registry v2协议登录过程理解不够透彻。docker registry v2是一个两阶段登录的过程:

  • 首先:docker client会到registry去尝试登录,registry发现request中没有携带token,则返回失败应答401,并告诉客户端到哪里去获取token;
  • 客户端收到应答后,获取应答中携带的token service地址,然后到harbor的core services中的token service那里获取token(使用user, password进行校验)。一旦token service校验ok,则会使用private_key.pem生成一个token;
  • 客户端拿到token后,再次到registry那里去登录,这次registry用root.crt去校验客户端携带的token,校验通过,则login成功。

由于我们是一个harbor cluster,如果docker client访问的token service和registry是在一个harbor实例中的,那么login就会ok;否则docker client就会用harbor node1上token service生成的token到harbor node2上的registry去登录,由于harbor node2上root.crt与harbor node1上private_key.pem并非一对,因此登录失败

解决方法:将所有节点上使用同一套root.crt和private_key.pem。即将一个harbor node(harbor.cfg中custom_crt=true的那个)上的 common/config/ui/private_key.pem和 common/config/registry/root.crt复制到其他harbor node;然后重建各harbor实例中的容器。

至此,我们的高可用Harbor仓库部署完了。针对上面的配置过程,我还做了几个录屏文件,由于时间关系,这里不能播放了,大家可以在下面这个连接下载并自行播放收看。

Harbor install 录屏: https://pan.baidu.com/s/1o8JYKEe

谢谢大家!

讲稿slide可以在这里获取到。

微博:@tonybai_cn
微信公众号:iamtonybai
github.com: https://github.com/bigwhite

Go语言:成长的十年

Go语言之父,Google大神Rob Pike代表Go语言的另外两位缔造者Robert GriesemerKen Thompson在自己的博客上发表了一篇名为《Go: Ten years and climbing》的文章,用以纪念Go语言从最初的设计idea起到目前的十年发展。笔者读完后,也是深有感触,因此在这里粗略翻译一下全文,希望能有更多的程序员加入到Gopher行列中来。

译文全文如下:

img{512x368}
Drawing Copyright ©2017 Renee French

本周是创建Go语言十周年的纪念日。

记得第一次关于这门语言设计的讨论是在2007年9月20日,一个周四的下午。进而在第二天的下午两点,我、Robert Griesemer以及Ken Thompson在谷歌山景城总部43#楼的一间名为Yaounde的会议室里又组织进行了一场有关这门语言设计的会议。这门语言的名字诞生于9月25日,在第一封有关语言设计的mail中可以看到一些关于命名的设计考量:

    Subject: Re: prog lang discussion
    From: Rob 'Commander' Pike
    Date: Tue, Sep 25, 2007 at 3:12 PM
    To: Robert Griesemer, Ken Thompson

    i had a couple of thoughts on the drive home.

    1. name

    'go'. you can invent reasons for this name but it has nice properties.
    it's short, easy to type. tools: goc, gol, goa. if there's an interactive
    debugger/interpreter it could just be called 'go'. the suffix is .go
    ...

(将语言命名为Go这事儿值得一提;“golang”来自于这门语言的web站点地址(因为go.com当时已经是迪斯尼的一个web站点了),但却不是语言的恰当名字。)

Go项目将2009年11月10日,即Go项目正式开源的那天作为其官方生日。最初Go项目托管在code.google.com上,几年后迁移至GitHub。不过,现在我们要回到最初的语言概念构建阶段,即那之前的两年,这可以让我们做更进一步地回顾,以更久远的视角,见证一些语言早期的历史事件。

Go开发过程中的第一个惊喜是收到下面这封mail信息:

    Subject: A gcc frontend for Go
    From: Ian Lance Taylor
    Date: Sat, Jun 7, 2008 at 7:06 PM
    To: Robert Griesemer, Rob Pike, Ken Thompson

    One of my office-mates pointed me at http://.../go_lang.html .  It
    seems like an interesting language, and I threw together a gcc
    frontend for it.  It's missing a lot of features, of course, but it
    does compile the prime sieve code on the web page.

Ian Lance Taylor的加入以及第二个编译器实现(gccgo)在带来震惊的同时,也伴随着喜悦。这对Go项目来说不仅仅是鼓励,更是一种对可行性的证明。有了语言的第二个实现对确定语言规范和标准库的过程是至关重要的,同时也有助于Go保证其高可移植性的承诺

虽然Ian的办公室离我们不远,但在看到这封mail之前我们从未谋面。不过,从那之后,Ian Lance Taylor便成为了Go语言及工具设计和实现的核心人物。

Russ Cox也是在2008年加入到刚成立不久的Go语言开发团队的。随着他的加入,他的一些天赋也随即在语言设计和实现中展现出来。Russ发现Go method的通用性意味着一个函数也可以拥有自己的方法,这直接导致了http.HandlerFunc的出现,这是一个我们所有人都未曾想到的结果。Russ还在当时设计的基础上提出了一些更泛化的想法,比如io.Readerio.Writer接口,奠定了所有I/O库的整体结构。

Jini Kim是我们最初的产品经理,他招来了安全专家Adam Langley来帮助我们将Go推向Google外面的世界。Adam为我们做了许多不为外人所知的事情,包括创建最初golang.org站点的web页面以及build dashboard。不过他最大的贡献当然要属cryptographic库了。起先,对于我们中的一部分人来说,这个库无论是规模还是复杂度,和其他库比起来都不成比例。但是就是这个库在后期成为了很多重要的网络和安全软件的基础,并且成为了Go语言开发历史的关键组成部分。像Cloudflare这样的网络基础设施提供商就重度依赖Adam在Go项目中的工作,Internet也因此变得更好。因此,我们由衷感谢他的工作。

事实上,许多公司在早期使用Go进行开发,尤其是初创公司。其中一些公司成为了云计算的巨头,其中就有一家这样的公司,它现在叫Docker。这家公司使用Go语言,并催化出计算领域的容器行业,进而导致了像Kubernetes这样的项目出现。今天我们可以说Go是容器语言,这是另一个我们完全没有预料到的结果。

不过,Go语言在云计算领域起到作用更大。2015年3月,Donnie Berkholz在为RedMonk撰写的一篇文章中宣称:Go是“云计算基础设施新兴语言”。几乎与此同时,Apcera的Derek Collison说:Go已经是云计算语言了。在那个时候,这也许还不是事实。但Berkholz所使用的“新兴”一词却恰如其分的表明了Go在当时的地位。

今天,Go已经成为云计算语言。想象一下:一个只有10岁的年轻编程语言已经成为这样一个规模庞大且不断发展的行业的主导者,这样的成功以前只是存在于在想象中。如果你觉得“主导”这个词太过强势的话,让我们来看看中国互联网行业。一段时间以来,Go在中国地区大量使用的数据一度让我们误认为Google趋势图出现了某些错误,但是凡是去过中国,参加过中国区Go语言大会的人都可以证实:Google趋势图的数据是真的,Go在中国的使用非常火爆!

简而言之,Go语言的十年发展为我们带来了许多里程碑。 最令人惊讶的是我们现在的位置:保守估计表明至少有50万Go程序员。 当前面那封为Go命名的邮件发送时,憧憬能有有五十万gopher的想法听起来会感觉很荒唐。 但就在此时此刻这里,我们不仅有了50w gopher,并且数量还在持续增长。

说到gophers,很高兴看到来自Renee French想法的吉祥物Go Gopher(地鼠),不仅成为了一个非常受人喜爱的作品,而且也是世界各地Go程序员的象征。许多各个地区顶级的Go大会都被称为GopherCons,因为他们聚集了来自世界各地的gophers。

Gopher大会正在迅速发展。第一次大会的举办只不过是三年前的事情,但今天在全世界各地有很多这样的Go大会。并且还有无数小的本地“聚会(meetups)”。在任何某一天,世界上某个地方都会有不止一个gopher群体在进行有关Go的分享。

回顾过去十年的Go设计和开发,Go社区的发展是惊人的。会议和聚会的数量、长长的且不断增加的Go项目贡献者名单、大量用Go实现的开放源代码存储库、使用Go的公司数量等等,细思恐(吃惊)极!

对于我们三个人,Robert, Rob和Ken,当初只是想让我们的编程生活更轻松一些,而如今,我们难以置信地、欣慰地看到我们的工作已经开始起作用了。

未来十年会带来什么呢?

- Rob Pike, with Robert Griesemer and Ken Thompson


微博:@tonybai_cn
微信公众号:iamtonybai
github.com: https://github.com/bigwhite

Go 1.9中值得关注的几个变化

Go语言在2016年当选tiobe index的年度编程语言。

img{512x368}

转眼间6个月过去了,Go在tiobe index排行榜上继续强势攀升,在最新公布的TIBOE INDEX 7月份的排行榜上,Go挺进Top10:

img{512x368}

还有不到一个月,Go 1.9版本也要正式Release了(计划8月份发布),当前Go 1.9的最新版本是go1.9beta2,本篇的实验环境也是基于该版本的,估计与final go 1.9版本不会有太大差异了。在今年的GopherChina大会上,我曾提到:Go已经演进到1.9,接下来是Go 1.10还是Go 2? 现在答案已经揭晓:Go 1.10。估计Go core team认为Go 1还有很多待改善和优化的地方,或者说Go2的大改时机依旧未到。Go team的tech lead Russ Cox将在今年的GopherCon大会上做一个题为”The Future of Go”的主题演讲,期待从Russ的口中能够得到一些关于Go未来的信息。

言归正传,我们还是来看看Go 1.9究竟有哪些值得我们关注的变化,虽然我个人觉得Go1.9的变动的幅度并不是很大^0^。

一、Type alias

Go 1.9依然属于Go1系,因此继续遵守Go1兼容性承诺。这一点在我的“值得关注的几个变化”系列文章中几乎每次都要提到。

不过Go 1.9在语言语法层面上新增了一个“颇具争议”的语法: Type Alias。关于type alias的proposal最初由Go语言之父之一的Robert Griesemer提出,并计划于Go 1.8加入Go语言。但由于Go 1.8的type alias实现过于匆忙,测试不够充分,在临近Go 1.8发布的时候发现了无法短时间解决的问题,因此Go team决定将type alias的实现从Go 1.8中回退

Go 1.9 dev cycle伊始,type alias就重新被纳入。这次Russ Cox亲自撰写文章《Codebase Refactoring (with help from Go)》为type alias的加入做铺垫,并开启新的discussion对之前Go 1.8的general alias语法形式做进一步优化,最终1.9仅仅选择了type alias,而不需要像Go 1.8中general alias那样引入新的操作符(=>)。这样,结合Go已实现的interchangeable constant、function、variable,外加type alias,Go终于在语言层面实现了对“Gradual code repair(渐进式代码重构)”理念的初步支持。

注:由于type alias的加入,在做Go 1.9相关的代码试验之前,最好先升级一下你本地编辑器/IDE插件(比如:vim-govscode-go)以及各种tools的版本。

官方对type alias的定义非常简单:

An alias declaration binds an identifier to the given type.

我们怎么来理解新增的type alias和传统的type definition的区别呢?

type T1 T2  // 传统的type defintion

vs.

type T1 = T2 //新增的type alias

把握住一点:传统的type definition创造了一个“新类型”,而type alias并没有创造出“新类型”。如果我们有一个名为“孙悟空”的类型,那么我们可以写出如下有意思的代码:

type  超级赛亚人  孙悟空
type  卡卡罗特 = 孙悟空

这时,我们拥有了两个类型:孙悟空超级赛亚人。我们以孙悟空这个类型为蓝本定义一个超级赛亚人类型;而当我们用到卡卡罗特这个alias时,实际用的就是孙悟空这个类型,因为卡卡罗特就是孙悟空,孙悟空就是卡卡罗特。

我们用几个小例子再来仔细对比一下:

1、赋值

Go强调“显式类型转换”,因此采用传统type definition定义的新类型在其变量被赋值时需对右侧变量进行显式转型,否则编译器就会报错。

//github.com/bigwhite/experiments/go19-examples/typealias/typedefinitions-assignment.go
package main

// type definitions
type MyInt int
type MyInt1 MyInt

func main() {
    var i int = 5
    var mi MyInt = 6
    var mi1 MyInt1 = 7

    mi = MyInt(i)  // ok
    mi1 = MyInt1(i) // ok
    mi1 = MyInt1(mi) // ok

    mi = i   //Error: cannot use i (type int) as type MyInt in assignment
    mi1 = i  //Error: cannot use i (type int) as type MyInt1 in assignment
    mi1 = mi //Error: cannot use mi (type MyInt) as type MyInt1 in assignment
}

而type alias并未创造新类型,只是源类型的“别名”,在类型信息上与源类型一致,因此可以直接赋值:

//github.com/bigwhite/experiments/go19-examples/typealias/typealias-assignment.go
package main

import "fmt"

// type alias
type MyInt = int
type MyInt1 = MyInt

func main() {
    var i int = 5
    var mi MyInt = 6
    var mi1 MyInt1 = 7

    mi = i // ok
    mi1 = i // ok
    mi1 = mi // ok

    fmt.Println(i, mi, mi1)
}

2、类型方法

Go1中通过type definition定义的新类型,新类型不会“继承”源类型的method set

// github.com/bigwhite/experiments/go19-examples/typealias/typedefinition-method.go
package main

// type definitions
type MyInt int
type MyInt1 MyInt

func (i *MyInt) Increase(a int) {
    *i = *i + MyInt(a)
}

func main() {
    var mi MyInt = 6
    var mi1 MyInt1 = 7
    mi.Increase(5)
    mi1.Increase(5) // Error: mi1.Increase undefined (type MyInt1 has no field or method Increase)
}

但是通过type alias方式得到的类型别名却拥有着源类型的method set(因为本就是一个类型),并且通过alias type定义的method也会反映到源类型当中:

// github.com/bigwhite/experiments/go19-examples/typealias/typealias-method1.go
package main

type Foo struct{}
type Bar = Foo

func (f *Foo) Method1() {
}

func (b *Bar) Method2() {
}

func main() {
    var b Bar
    b.Method1() // ok

    var f Foo
    f.Method2() // ok
}

同样对于源类型为非本地类型的,我们也无法通过type alias为其增加新method:

//github.com/bigwhite/experiments/go19-examples/typealias/typealias-method.go
package main

type MyInt = int

func (i *MyInt) Increase(a int) { // Error: cannot define new methods on non-local type int
    *i = *i + MyInt(a)
}

func main() {
    var mi MyInt = 6
    mi.Increase(5)
}

3、类型embedding

有了上面关于类型方法的结果,其实我们也可以直接知道在类型embedding中type definition和type alias的差异。

// github.com/bigwhite/experiments/go19-examples/typealias/typedefinition-embedding.go
package main

type Foo struct{}
type Bar Foo

type SuperFoo struct {
    Bar
}

func (f *Foo) Method1() {
}

func main() {
    var s SuperFoo
    s.Method1() //Error: s.Method1 undefined (type SuperFoo has no field or method Method1)
}

vs.

// github.com/bigwhite/experiments/go19-examples/typealias/typealias-embedding.go

package main

type Foo struct{}
type Bar = Foo

type SuperFoo struct {
    Bar
}

func (f *Foo) Method1() {
}

func main() {
    var s SuperFoo
    s.Method1() // ok
}

通过type alias得到的alias Bar在被嵌入到其他类型中,其依然携带着源类型Foo的method set

4、接口类型

接口类型的identical的定义决定了无论采用哪种方法,下面的赋值都成立:

// github.com/bigwhite/experiments/go19-examples/typealias/typealias-interface.go
package main

type MyInterface interface{
    Foo()
}

type MyInterface1 MyInterface
type MyInterface2 = MyInterface

type MyInt int

func (i *MyInt)Foo() {

}

func main() {
    var i MyInterface = new(MyInt)
    var i1 MyInterface1 = i // ok
    var i2 MyInterface2 = i1 // ok

    print(i, i1, i2)
}

5、exported type alias

前面说过type alias和源类型几乎是一样的,type alias有一个特性:可以通过声明exported type alias将package内的unexported type导出:

//github.com/bigwhite/experiments/go19-examples/typealias/typealias-export.go
package main

import (
    "fmt"

    "github.com/bigwhite/experiments/go19-examples/typealias/mylib"
)

func main() {
    f := &mylib.Foo{5, "Hello"}
    f.String()            // ok
    fmt.Println(f.A, f.B) // ok

    // Error:  f.anotherMethod undefined (cannot refer to unexported field
    // or method mylib.(*foo).anotherMethod)
    f.anotherMethod()
}

而mylib包的代码如下:

package mylib

import "fmt"

type foo struct {
    A int
    B string
}

type Foo = foo

func (f *foo) String() {
    fmt.Println(f.A, f.B)
}

func (f *foo) anotherMethod() {
}

二、Parallel Complication(并行编译)

Go 1.8版本的gc compiler的编译性能虽然照比Go 1.5刚自举时已经提升了一大截儿,但依然有提升的空间,虽然Go team没有再像Go 1.6时对改进compiler性能那么关注。

在Go 1.9中,在原先的支持包级别的并行编译的基础上又实现了包函数级别的并行编译,以更为充分地利用多核资源。默认情况下并行编译是enabled,可以通过GO19CONCURRENTCOMPILATION=0关闭。

在aliyun ECS一个4核的vm上,我们对比了一下并行编译和关闭并行的差别:

# time GO19CONCURRENTCOMPILATION=0 go1.9beta2 build -a std

real    0m16.762s
user    0m28.856s
sys    0m4.960s

# time go1.9beta2 build -a std

real    0m13.335s
user    0m29.272s
sys    0m4.812s

可以看到开启并行编译后,gc的编译性能约提升20%(realtime)。

在我的Mac 两核pc上的对比结果如下:

$time GO19CONCURRENTCOMPILATION=0 go build -a std

real    0m16.631s
user    0m36.401s
sys    0m8.607s

$time  go build -a std

real    0m14.445s
user    0m36.366s
sys    0m7.601s

提升大约13%。

三、”./…”不再匹配vendor目录

自从Go 1.5引入vendor机制以来,Go的包依赖问题有所改善,但在vendor机制的细节方面依然有很多提供的空间。

比如:我们在go test ./…时,我们期望仅执行我们自己代码的test,但Go 1.9之前的版本会匹配repo下的vendor目录,并将vendor目录下的所有包的test全部执行一遍,以下面的repo结构为例:

$tree vendor-matching/
vendor-matching/
├── foo.go
├── foo_test.go
└── vendor
    └── mylib
        ├── mylib.go
        └── mylib_test.go

如果我们使用go 1.8版本,则go test ./…输出如下:

$go test ./...
ok      github.com/bigwhite/experiments/go19-examples/vendor-matching    0.008s
ok      github.com/bigwhite/experiments/go19-examples/vendor-matching/vendor/mylib    0.009s

我们看到,go test将vendor下的包的test一并执行了。关于这点,gophers们在go repo上提了很多issue,但go team最初并没有理会这个问题,只是告知用下面的解决方法:

$go test $(go list ./... | grep -v /vendor/)

不过在社区的强烈要求下,Go team终于妥协了,并承诺在Go 1.9中fix该issue。这样在Go 1.9中,你会看到如下结果:

$go test ./...
ok      github.com/bigwhite/experiments/go19-examples/vendor-matching    0.008s

这种不再匹配vendor目录的行为不仅仅局限于go test,而是适用于所有官方的go tools。

四、GC性能

GC在Go 1.9中依旧继续优化和改善,大多数程序使用1.9编译后都能得到一定程度的性能提升。1.9 release note中尤其提到了大内存对象分配性能的显著提升。

在”go runtime metrics“搭建一文中曾经对比过几个版本的GC,从我的这个个例的图中来看,Go 1.9与Go 1.8在GC延迟方面的指标性能相差不大:

img{512x368}

五、其他

下面是Go 1.9的一些零零碎碎的改进,这里也挑我个人感兴趣的说说。

1、Go 1.9的新安装方式

go 1.9的安装增加了一种新方式,至少beta版支持,即通过go get&download安装:

# go get golang.org/x/build/version/go1.9beta2

# which go1.9beta2
/root/.bin/go18/bin/go1.9beta2
# go1.9beta2 version
go1.9beta2: not downloaded. Run 'go1.9beta2 download' to install to /root/sdk/go1.9beta2

# go1.9beta2 download
Downloaded 0.0% (15208 / 94833343 bytes) ...
Downloaded 4.6% (4356956 / 94833343 bytes) ...
Downloaded 34.7% (32897884 / 94833343 bytes) ...
Downloaded 62.6% (59407196 / 94833343 bytes) ...
Downloaded 84.6% (80182108 / 94833343 bytes) ...
Downloaded 100.0% (94833343 / 94833343 bytes)
Unpacking /root/sdk/go1.9beta2/go1.9beta2.linux-amd64.tar.gz ...
Success. You may now run 'go1.9beta2'

# go1.9beta2 version
go version go1.9beta2 linux/amd64

# go1.9beta2 env GOROOT
/root/sdk/go1.9beta2

go1.9 env输出支持json格式:

# go1.9beta2 env -json
{
    "CC": "gcc",
    "CGO_CFLAGS": "-g -O2",
    "CGO_CPPFLAGS": "",
    "CGO_CXXFLAGS": "-g -O2",
    "CGO_ENABLED": "1",
    "CGO_FFLAGS": "-g -O2",
    "CGO_LDFLAGS": "-g -O2",
    "CXX": "g++",
    "GCCGO": "gccgo",
    "GOARCH": "amd64",
    "GOBIN": "/root/.bin/go18/bin",
    "GOEXE": "",
    "GOGCCFLAGS": "-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build750457963=/tmp/go-build -gno-record-gcc-switches",
    "GOHOSTARCH": "amd64",
    "GOHOSTOS": "linux",
    "GOOS": "linux",
    "GOPATH": "/root/go",
    "GORACE": "",
    "GOROOT": "/root/sdk/go1.9beta2",
    "GOTOOLDIR": "/root/sdk/go1.9beta2/pkg/tool/linux_amd64",
    "PKG_CONFIG": "pkg-config"
}

2、go doc支持查看struct field的doc了

我们使用Go 1.8查看net/http包中struct Response的某个字段Status:

# go doc net/http.Response.Status
doc: no method Response.Status in package net/http
exit status 1

Go 1.8的go doc会报错! 我们再来看看Go 1.9:

# go1.9beta2 doc net/http.Response.Status
struct Response {
    Status string  // e.g. "200 OK"
}

# go1.9beta2 doc net/http.Request.Method
struct Request {
    // Method specifies the HTTP method (GET, POST, PUT, etc.).
    // For client requests an empty string means GET.
    Method string
}

3、核心库的变化

a) 增加monotonic clock支持

在2017年new year之夜,欧美知名CDN服务商CloudflareDNS出现大规模故障,导致欧美很多网站无法正常被访问。之后,Cloudflare工程师分析了问题原因,罪魁祸首就在于golang time.Now().Sub对时间的度量仅使用了wall clock,而没有使用monotonic clock,导致返回负值。而引发异常的事件则是新年夜际授时组织在全时间范围内添加的那个闰秒(leap second)。一般来说,wall clock仅用来告知时间,mnontonic clock才是用来度量时间流逝的。为了从根本上解决问题,Go 1.9在time包中实现了用monotonic clock来度量time流逝,这以后不会出现时间的“负流逝”问题了。这个改动不会影响到gopher对timer包的方法层面上的使用。

b) 增加math/bits包

在一些算法编程中,经常涉及到对bit位的操作。Go 1.9提供了高性能math/bits package应对这个问题。关于bits操作以及算法,可以看看经典著作《Hacker’s Delight》。这里就不举例了。

c) 提供了一个支持并发的Map类型

Go原生的map不是goroutine-safe的,尽管在之前的版本中陆续加入了对map并发的检测和提醒,但gopher一旦需要并发map时,还需要自行去实现。在Go 1.9中,标准库提供了一个支持并发的Map类型:sync.Map。sync.Map的用法比较简单,这里简单对比一下builtin map和sync.Map在并发环境下的性能:

我们自定义一个简陋的支持并发的类型:MyMap,来与sync.Map做对比:

// github.com/bigwhite/experiments/go19-examples/benchmark-for-map/map_benchmark.go
package mapbench

import "sync"

type MyMap struct {
    sync.Mutex
    m map[int]int
}

var myMap *MyMap
var syncMap *sync.Map

func init() {
    myMap = &MyMap{
        m: make(map[int]int, 100),
    }

    syncMap = &sync.Map{}
}

func builtinMapStore(k, v int) {
    myMap.Lock()
    defer myMap.Unlock()
    myMap.m[k] = v
}

func builtinMapLookup(k int) int {
    myMap.Lock()
    defer myMap.Unlock()
    if v, ok := myMap.m[k]; !ok {
        return -1
    } else {
        return v
    }
}

func builtinMapDelete(k int) {
    myMap.Lock()
    defer myMap.Unlock()
    if _, ok := myMap.m[k]; !ok {
        return
    } else {
        delete(myMap.m, k)
    }
}

func syncMapStore(k, v int) {
    syncMap.Store(k, v)
}

func syncMapLookup(k int) int {
    v, ok := syncMap.Load(k)
    if !ok {
        return -1
    }

    return v.(int)
}

func syncMapDelete(k int) {
    syncMap.Delete(k)
}

针对上面代码,我们写一些并发的benchmark test,用伪随机数作为key:

// github.com/bigwhite/experiments/go19-examples/benchmark-for-map/map_benchmark_test.go
package mapbench

import "testing"

func BenchmarkBuiltinMapStoreParalell(b *testing.B) {
    b.RunParallel(func(pb *testing.PB) {
        r := rand.New(rand.NewSource(time.Now().Unix()))
        for pb.Next() {
            // The loop body is executed b.N times total across all goroutines.
            k := r.Intn(100000000)
            builtinMapStore(k, k)
        }
    })
}

func BenchmarkSyncMapStoreParalell(b *testing.B) {
    b.RunParallel(func(pb *testing.PB) {
        r := rand.New(rand.NewSource(time.Now().Unix()))
        for pb.Next() {
            // The loop body is executed b.N times total across all goroutines.
            k := r.Intn(100000000)
            syncMapStore(k, k)
        }
    })
}
... ...

我们执行一下benchmark:

$go test -bench=.
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/experiments/go19-examples/benchmark-for-map
BenchmarkBuiltinMapStoreParalell-4         3000000           515 ns/op
BenchmarkSyncMapStoreParalell-4            2000000           754 ns/op
BenchmarkBuiltinMapLookupParalell-4        5000000           396 ns/op
BenchmarkSyncMapLookupParalell-4          20000000            60.5 ns/op
BenchmarkBuiltinMapDeleteParalell-4        5000000           392 ns/op
BenchmarkSyncMapDeleteParalell-4          30000000            59.9 ns/op
PASS
ok      github.com/bigwhite/experiments/go19-examples/benchmark-for-map    20.550s

可以看出,除了store,lookup和delete两个操作,sync.Map都比我自定义的粗糙的MyMap要快好多倍,似乎sync.Map对read做了特殊的优化(粗略看了一下代码:在map read这块,sync.Map使用了无锁机制,这应该就是快的原因了)。

d) 支持profiler labels

通用的profiler有时并不能完全满足需求,我们时常需要沿着“业务相关”的执行路径去Profile。Go 1.9在runtime/pprof包、go tool pprof工具增加了对label的支持。Go team成员rakyll有一篇文章“Profiler labels in go”详细介绍了profiler labels的用法,可以参考,这里不赘述了。

六、后记

正在写这篇文章之际,Russ Cox已经在GopherCon 2017大会上做了”The Future of Go”的演讲,并announce Go2大幕的开启,虽然只是号召全世界的gopher们一起help and plan go2的设计和开发。同时,该演讲的文字版已经在Go官网发布了,文章名为《Toward Go 2》,显然这又是Go语言演化史上的一个里程碑的时刻,值得每个gopher为之庆贺。不过Go2这枚靴子真正落地还需要一段时间,甚至很长时间。当下,我们还是要继续使用和改善Go1,就让我们从Go 1.9开始吧^0^。

本文涉及的demo代码可以在这里下载。


微博:@tonybai_cn
微信公众号:iamtonybai
github.com: https://github.com/bigwhite




这里是Tony Bai的个人Blog,欢迎访问、订阅和留言!订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:


以太币:


如果您喜欢通过微信App浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:



本站Powered by Digital Ocean VPS。

选择Digital Ocean VPS主机,即可获得10美元现金充值,可免费使用两个月哟!

著名主机提供商Linode 10$优惠码:linode10,在这里注册即可免费获得。

阿里云推荐码:1WFZ0V立享9折!

View Tony Bai's profile on LinkedIn


文章

评论

  • 正在加载...

分类

标签

归档











更多