第10章 Go的部署

本章主要内容

在学习了如何使用Go开发Web应用之后,接下来要考虑的自然就是如何部署这些应用了。Web应用跟其他类型的应用在部署方式上存在着非常大的不同。比如,桌面应用和移动应用就是部署在智能手机、平板电脑、手提电脑等终端用户的设备上,而Web应用则是部署在服务器上,然后通过终端用户设备上的浏览器等客户端对其进行访问。

因为Go的可执行程序都会被编译为单独的二进制文件,所以部署Go Web应用程序在某种程度上可以说是非常简单的。除此之外,Go还可以编译出不需要引用任何外部库的静态链接二进制文件,这种文件可以作为独立的可执行文件存在。不过一个完整的Web应用通常不会只包含一个可执行文件,它一般还会包含一些模板文件,以及诸如JavaScript、图片、样式表(style sheet)这样的静态文件。本章将会介绍几种把Go Web应用部署到互联网的方法,其中大部分方法都是通过云供应商(cloud provider)实现的。本章将要介绍的部署方法包括:

云计算

 

云计算,简称“云”,是一种获取网络和计算机使用权限的模型,这种模型可以提供一个由服务器、存储空间、网络以及其他可共享资源组成的共享资源池,从而使这些资源的用户可以避免不必要的前期投入,也可以让这些资源的供应商更加高效地利用这些资源为更多的用户提供服务。云计算在最近这些年吸引了非常多的关注,时至今日,包括Amazon、Google和Facebook在内的绝大部分基础设施以及服务供应商都使用这种模型作为他们的标准收费模型。

需要注意的是,部署一个Web应用通常会有很多种不同的方法可选,比如,本章介绍的几种部署方法之间就存在着非常多的不同之处。还有一点要说明的是,本章介绍的部署方法关注的是如何部署个人的Web应用,真正生产环境下的部署通常会包含运行测试套件、实施持续集成以及调整服务器等一系列额外的任务,具体过程会比这里介绍的要复杂得多。

本章虽然介绍了很多概念和工具,但由于这些概念和工具每个都值得用整整一本书的篇幅去介绍,所以本章并没有试图全面讲解这些技术和服务。相反,本章只会关注这些技术的一部分知识,读者可以把这些知识看作是学习相关技术的起点。

本章展示的部署例子将会用到7.6节介绍过的简单Web服务,并在条件允许的情况下使用PostgreSQL(因为GAE不支持PostgreSQL,所以在介绍GAE的部署方法时,本章将使用基于MySQL的Google Cloud SQL)。与此同时,本章还会假设独立的数据库服务器上已经预先设置好了数据库的相关设置,所以本章将不会介绍具体的数据库设置方法,有需要的读者可以通过复习2.6节来获得一个简短的设置介绍。

让我们从最简单的部署方法开始——创建一个可执行的二进制文件,并将它放到互联网的某个服务器上运行,这个服务器可以是物理存在的,也可以是由Amazon Web Services(AWS)或者Digital Ocean等供应商创建的虚拟机(VM)。在本节中,我们将要学习如何在运行着Ubuntu Server 14.04系统的服务器上部署Go Web应用。

IaaS、PaaS和SaaS

 

云计算供应商都会通过不同的模型来为用户提供服务。美国国家标准技术研究所(National Institute of Standards and Technology, US Department of Commerce,NIST)定义了当今广为使用的3种服务模型,分别是基础设施即服务(Infrastructure-as-a-Service,IaaS),平台即服务(Platform-as-a- Service,PaaS)和软件即服务(Software-as-a-Service,SaaS)。

IaaS是这3种模型中最为基本的一种,使用这种模型的供应商将向他们的用户提供包括计算、存储以及网络在内的基本计算能力。提供IaaS服务的例子有AWS的弹性云计算服务(Elastic Cloud Computing Service,EC2)、Google公司的Compute Engine(计算引擎)以及Digital Ocean的Droplets。

使用PaaS模型的供应商会让用户通过他们提供的工具,将应用部署到云端的基础设施之上。提供PaaS服务的例子有Heroku、AWS的Elastic Beanstalk以及Google公司的App Engine。

使用SaaS模型的供应商会向用户提供应用服务。尽管消费者当今使用的绝大多数服务都可以看作是SaaS服务,但是在本书的语境中,我们只会把Heroku的Postgres 数据库服务(Postgres database service,它提供的是基于云的Postgres服务)、AWS的Relational Database Service(关系数据库服务,RDS)以及Google公司的Cloud SQL(云SQL)这样的服务看作是SaaS服务。

在本章中,我们将学习如何利用IaaS和PaaS供应商来部署GoWeb应用。

本书第7章介绍过的简单Web服务由代码清单10-1中的data.go 和代码清单10-2中的server.go 这两个文件组成,前者包含了所有指向数据库的连接和所有对数据库进行读写的函数,而后者则包含了main 函数和Web服务的所有处理逻辑。

代码清单10-1 使用data.go 访问数据库

package main

import (
 "database/sql"
 _ "github.com/lib/pq"
)

var Db *sql.DB

func init() {
 var err error
 Db, err = sql.Open("postgres", "user=gwp dbname=gwp password=gwp
 ➥sslmode=disable")
 if err != nil {
  panic(err)
 }
}

func retrieve(id int) (post Post, err error) {
 post = Post{}
 err = Db.QueryRow("select id, content, author from posts where id =
 ➥$1", id).Scan(&post.Id, &post.Content, &post.Author)
 return
}

func (post *Post) create() (err error) {
 statement := "insert into posts (content, author) values ($1, $2)
 ➥returning id"
 stmt, err := Db.Prepare(statement)
 if err != nil {
  return
 }
 defer stmt.Close()
 err = stmt.QueryRow(post.Content, post.Author).Scan(&post.Id)
 return
}

func (post *Post) update() (err error) {
 _, err = Db.Exec("update posts set content = $2, author = $3 where id =
 ➥$1", post.Id, post.Content, post.Author)
 return
}
func (post *Post) delete() (err error) {
 _, err = Db.Exec("delete from posts where id = $1", post.Id)
 return
}

代码清单10-2 定义了Go Web服务的server.go

package main

import (
 "encoding/json"
 "net/http"
 "path"
 "strconv"
)

type Post struct {
 Id   int  `json:"id"`
 Content string `json:"content"`
 Author string `json:"author"`
}

func main() {
 server := http.Server{
  Addr: "127.0.0.1:8080",
 }
 http.HandleFunc("/post/", handleRequest)
 server.ListenAndServe()
}

func handleRequest(w http.ResponseWriter, r *http.Request) {
 var err error
 switch r.Method {
 case "GET":
  err = handleGet(w, r)
 case "POST":
  err = handlePost(w, r)
 case "PUT":
  err = handlePut(w, r)
 case "DELETE":
  err = handleDelete(w, r)
 }
 if err != nil {
  http.Error(w, err.Error(), http.StatusInternalServerError)
  return
 }
}

func handleGet(w http.ResponseWriter, r *http.Request) (err error) {
 id, err := strconv.Atoi(path.Base(r.URL.Path))
 if err != nil {
  return
 }
 post, err := retrieve(id)
 if err != nil {
  return
 }
 output, err := json.MarshalIndent(&post, "", "\t\t")
 if err != nil {
  return
 }
 w.Header().Set("Content-Type", "application/json")
 w.Write(output)
 return
}

func handlePost(w http.ResponseWriter, r *http.Request) (err error) {
 len := r.ContentLength
 body := make([]byte, len)
 r.Body.Read(body)
 var post Post
 json.Unmarshal(body, &post)
 err = post.create()
 if err != nil {
  return
 }
 w.WriteHeader(200)
 return
}

func handlePut(w http.ResponseWriter, r *http.Request) (err error) {
 id, err := strconv.Atoi(path.Base(r.URL.Path))
 if err != nil {
  return
 }
 post, err := retrieve(id)
 if err != nil {
  return
 }
 len := r.ContentLength
 body := make([]byte, len)
 r.Body.Read(body)
 json.Unmarshal(body, &post)
 err = post.update()
 if err != nil {
  return
 }
 w.WriteHeader(200)
 return
}

func handleDelete(w http.ResponseWriter, r *http.Request) (err error) {
 id, err := strconv.Atoi(path.Base(r.URL.Path))
 if err != nil {
  return
 }
 post, err := retrieve(id)
 if err != nil {
  return
 }
 err = post.delete()
 if err != nil {
  return
 }
 w.WriteHeader(200)
 return
}

首先,我们需要使用以下命令编译这段代码:

go build

如果我们把简单Web服务的代码放到一个名为ws-s 的目录里,那么这个编译命令将产生一个同名的可执行二进制文件。为了部署Web服务ws-s ,我们需要把ws-s文件复制到服务器里,并将其放置到一个可以通过外部访问的地方。

接着我们只需要登录服务器,并在终端里执行以下命令,就可以运行ws-s 这个Web服务了:

./ws-s

需要注意的是,因为Web服务现在是在前台运行,所以在服务运行期间,我们将无法执行其他操作。与此同时,我们也无法简单地通过& 命令或者bg 命令在后台运行这个服务,因为这样做的话,一旦用户登出,Web服务就会被杀死。

避免上述问题的一种方法就是使用nohup 命令,让操作系统在用户注销时,把发送至Web服务的HUP (hangup,挂起)信号忽略掉:

nohup ./ws-s &

执行上述命令将导致Web服务被放到后台运行,并且不用担心因为HUP 信号而被杀死。以这种方式启动的Web服务仍会如常地与客户端进行连接,但现在的Web服务将忽略所有挂起或者退出信号。因为这种状态下运行的Web服务在崩溃时将不会有任何提醒,所以在服务崩溃或者服务器重启之后,用户必须重新登入系统并重启服务。

除nohup 之外,持续运行Web服务的另一种方法是使用Upstart或者systemd这样的init 守护进程:init 进程是类Unix系统在启动时运行的第一个进程,该进程由内核负责启动,它会一直运行直到系统关闭为止,并且它还是其他所有进程直接或间接的祖先。

Upstart是由Ubuntu创建的一个基于事件的init 替代品,尽管现在systemd也越来越受到大家的青睐,但考虑到这两个工具都能够完成本节介绍的工作,并且Upstart的使用方法相对来说要更为简单一些,所以我们接下来将要学习如何使用Upstart来持续地运行Web服务。

为了使用Upstart,用户首先需要创建一个对应的Upstart任务配置文件,并将该文件放到etc/init 目录里面。对简单Web服务来说,我们将创建代码清单10-3所示的ws.conf 文件,并将它放到etc/init 目录里面。

代码清单10-3 简单Web服务的Upstart任务配置文件

respawn
respawn limit 10 5

setuid sausheong
setgid sausheong

exec /go/src/github.com/sausheong/ws-s/ws-s

这个Upstart任务配置文件非常简单和直接。文件中的每个Upstart任务都由一个或任意多个称为节 (stanzas)的命令块组成。第一节respawn 指示当任务失效(fail)时,Upstart将对其实施重新派生(respawn)或者重新启动。第二节respawn limit 10 5 为respawn 设置了参数,它指示Upstart最多只会尝试重新派生该任务10次,并且每次尝试之间会有5 s的间隔;在用完了10次重新派生的机会之后,Upstart将不再尝试重新派生该任务,并将该任务视为已失效。第三节和第四节负责设置运行进程的用户以及用户组,而最后一节则是Upstart在启动任务时需要运行的可执行文件。

为了启动上述Upstart任务,我们需要在终端里面执行以下命令:

sudo start ws
ws start/running, process 2011

这个命令将触发Upstart读取/etc/init/ws.conf 任务配置文件并启动任务。本节以管中窥豹的方式,快速地了解了如何使用简单的Upstart任务运行一个Go Web应用,但是除这里介绍的内容之外,Upstart的任务配置文件还有其他不同的节可供使用,并且Upstart的任务也拥有多种不同的配置方式可以使用,不过这些内容不在本书的介绍范围之内,有兴趣的读者可以自行通过互联网进行了解。

为了验证Upstart是否能够正确地运行和管理ws-s 服务,我们可以尝试在Upstart任务启动之后,杀死正在运行的ws-s 服务:

ps -ef | grep ws
sausheo+ 2011 1 0 17:23 ? 00:00:00 /go/src/github.com/sausheong/ws-s/ws-s

sudo kill -0 2011

ps -ef | grep ws
sausheo+ 2030 1 0 17:23 ? 00:00:00 /go/src/github.com/sausheong/ws-s/ws-s

注意看,在kill 命令执行之前,ws-s 进程的ID为2011 ,但是在kill 命令执行之后,ws-s 进程的ID变成了2030 ——这是因为Upstart在kill 命令执行之后,察觉到了ws-s 进程已被关闭,于是它重启了ws-s 进程,从而导致ws-s 进程的ID发生了变化。

最后,因为大部分Web应用都部署在标准HTTP端口(即80端口)之上,所以读者在实际部署时,应该将简单Web服务代码中的端口号从现在的8080改为80,或者通过某种机制将8080端口的流量代理或者重定向到80端口。

在上一节中,我们学习了如何将一个简单的Go Web服务部署到独立的服务器上面,以及如何通过init 守护进程管理Web服务。在本节中,我们将要学习如何将同样的Web服务部署到PaaS供应商Heroku上面,这种部署方式跟上一节介绍的部署方式一样简单。

Heroku允许用户部署、运行和管理使用包括Go在内的几种编程语言开发的应用。根据Heroku的定义,一个应用 就是由Heroku支持的某一种编程语言编写的一系列源代码,以及与这些源代码相关联的依赖关系。

Heroku的预设条件非常简单,它只要求用户提供以下几样东西:

Heroku大量地使用命令行,并因此提供了一个名为toolbelt的命令行工具,用于部署、运行和管理应用。此外,Heroku还需要通过Git将被部署的源码推送至服务器。当Heroku平台接收到Git推送的代码时,它会构建代码并获取指定的依赖关系,然后将构建的结果以及相应的依赖关系组装到一个slug 里面,最后在Heroku的dynos上运行这个slug(dynos是Heroku对隔离式、轻量级、虚拟化的Unix容器的称呼)。

尽管某些管理和配置工作可以在之后通过Web界面来完成,但Heroku最主要的操作界面还是它的命令行工具toolbelt,因此我们在注册完Heroku之后的第一件事,就是访问https:// toolbelt.heroku.com下载toolbelt。

Heroku是一个非典型的PaaS供应商,人们想要使用PaaS来部署Web应用的原因有很多,对Web应用的开发者来说,最主要的原因莫过于PaaS可以让基础设施和系统层变得抽象,并且不再需要人工的管理和干预。尽管PaaS在企业级IT基础设施这样的大规模生产环境中并不少见,但它们对小型公司和创业公司来说却能够提供极大的方便,并且能够有效地减少这些公司在基础设施方面的前期投入。

在下载完toolbelt之后,用户需要使用注册账号时获得的凭据登入Heroku:

heroku login
Enter your Heroku credentials.
Email: <your email>
Password (typing will be hidden):
Authentication successful.

图10-1展示了将简单Web服务部署到Heroku的具体步骤。

为了将简单Web应用部署到Heroku,我们需要对这个应用的代码做一些细微的修改:在当前的代码中,应用使用的是8080端口,但是在把应用部署到Heroku的时候,用户是无法控制应用使用哪个端口的,程序必须通过读取环境变量PORT 来获知自己能够使用的端口号。为此,我们需要将server.go文件 中main 函数的代码从现在的:

func main() {
 server := http.Server{
  Addr: ":8080",
 }
 http.HandleFunc("/post/", handlePost)
 server.ListenAndServe()
}

修改为:

func main() {
 server := http.Server{
  Addr: ":" + os.Getenv("PORT"),// ❶
 }
 http.HandleFunc("/post/", handlePost)
 server.ListenAndServe()
}

❶ 从环境变量中获取端口号

10-01

图10-1 将Web应用部署到Heroku的具体步骤

以上就是将简单Web应用部署到Heroku所需要做的全部代码修改,其他代码只要保留原样即可。在修改完代码之后,我们接下来要做的就是将简单Web应用所需的依赖关系告知Heroku。Heroku使用godep(https://github.com/tools/godep)来管理Go的依赖关系,godep可以通过执行以下命令来安装:

go get github.com/tools/godep

在godep安装完毕之后,我们需要使用它来引入简单Web服务的依赖关系。为此,我们需要在简单Web服务的根目录中执行以下命令:

godep save

这条命令不仅会创建一个名为Godeps 的目录,它还会获取代码中的全部依赖关系,并将这些依赖关系的源代码复制到Godeps/_workspace 目录中。除此之外,这个命令还会创建一个名为Godeps.json 的文件,并在该文件中列出代码中的全部依赖关系。作为例子,代码清单10-4展示了godep为简单Web服务创建的Godeps.json 文件。

代码清单10-4 Godeps.json 文件

{
 "ImportPath": "github.com/sausheong/ws-h",
 "GoVersion": "go1.4.2",
 "Deps": [
  {
   "ImportPath": "github.com/lib/pq",
   "Comment": "go1.0-cutoff-31-ga33d605",
   "Rev": "a33d6053e025943d5dc89dfa1f35fe5500618df7"
  }
 ]
}

因为我们的简单Web服务只需要依赖Postgres数据库驱动,所以文件中只出现了关于该驱动的依赖信息。

在Heroku上实施部署需要做的最后一件事,就是定义一个Procfile 文件,并使用该文件去描述需要被执行的可执行文件或者主函数。代码清单10-5展示了简单Web服务的Procfile 文件。

代码清单10-5 Procfile 文件

web: ws-h

整个文件非常简单,只有短短的一行。这个文件定义了Web进程与ws-h 可执行二进制文件之间的关联,Heroku在完成应用的构建工作之后,就会执行ws-h 文件。

准备工作一切就绪之后,我们接下来要做的就是将简单Web服务的代码推送至Heroku。Heroku允许用户通过GitHub集成、Dropbox同步、Heroku官方提供的API以及标准的Git操作等多种不同的手段来推送代码。作为例子,本节接下来将展示如何使用标准的Git操作将简单Web服务推送至Heroku。

在推送代码之前,用户首先需要创建一个Heroku应用:

heroku create ws-h

这条命令将创建一个名为ws-h 的Heroku应用,该应用最终将在地址https://ws-h.herokuapp.com上展示。需要注意的是,因为本书在这里已经使用了ws-h 作为应用的名字,所以读者将无法创建相同名字的应用。为此,读者在创建应用的时候可以使用其他名字,或者在创建应用时去掉名字参数,让Heroku为应用自动生成一个随机的名字:

heroku create

heroku create 命令将为我们的简单Web服务创建一个本地的Git代码库(repository),并在代码库中添加远程Heroku代码库的地址。因此,用户在创建完Heroku应用之后,就可以通过以下命令使用Git将应用代码推送至Heroku:

git push heroku master

因为Heroku在接收到用户推送的代码之后就会自动触发相应的构建以及部署操作,所以将应用部署到Heroku的工作到此就可以告一段落了。除上面提到的工具之外,Heroku还提供了一系列非常棒的应用管理工具,这些工具可以对应用进行性能扩展以及版本管理,并且在需要时,用户也可以使用Heroku提供的配置工具添加新的服务,有兴趣的读者可以自行查阅Heroku提供的相关文档。

Google App Engine(GAE)是另一个流行的Go Web应用PaaS部署平台。Google公司在它的云平台产品套件中包含了App Engine(应用引擎)和Compute Engine(计算引擎)等多种服务,其中App Engine为PaaS服务,而Compute Engine则跟AWS的EC2和Digital Ocean的Droplets一样,是一个IaaS服务。使用EC2和Droplets这样的服务跟使用自有的虚拟机或者服务器并没有太大区别,并且我们已经在上一节学习过如何在类似的平台上进行部署,因此在这一节,我们要学习如何使用GAE这款由Google公司提供的强大的PaaS服务。

人们选择使用GAE而不是包括Heroku在内的其他PaaS服务的原因通常会有好几种,但其中最主要的原因还是跟性能和可扩展性有关。GAE能够让用户构建出可以根据负载自动进行性能扩展和负载平衡的应用,并且Google公司除为GAE提供了大量的工具之外,还在GAE内部构建了大量的功能。比如,GAE允许用户的应用通过身份验证功能登录Google账号,并且GAE还提供了发送邮件、创建日志、发布和管理图片等多种服务。除此之外,GAE用户还可以非常简单直接地在自己的应用中集成其他Google API。

虽然GAE拥有如此多的优点,但天下并没有免费的午餐——GAE在拥有众多优点的同时,也拥有不少限制和缺点,其中包括:用户只拥有对文件系统的读权限,请求时长不能超过60 s(否则GAE将强制杀死该请求),无法进行直接的网络访问,无法创建其他类型的系统调用,等等。这些限制意味着用户将不能(至少是无法轻易地)访问Google应用环境沙箱(sandbox)之外的其他大量服务。

图10-2展示了在GAE上部署Web应用的大致步骤。

10-02

图10-2 在GAE上部署应用的大致步骤

跟其他所有Google服务一样,使用GAE也需要一个Google账号。跟Heroku大量使用命令行界面的做法不同,在GAE上,对Web应用的大部分管理和维护操作都是通过名为Google Developer Console(开发者控制台)的Web界面完成的。虽然GAE也拥有与开发者控制台具有同等功能的命令行接口,但Google公司的命令行工具并没有像Heroku那样集成这些接口。图10-3展示了Google开发者控制台的使用界面。

除了注册账号之外,使用GAE需要做的另一件事就是访问https://cloud.google.com/appengine/ downloads下载相应的SDK(Software Development Kit,软件开发工具包)。在这个例子中,我们将下载GAE为Go语言提供的SDK。

10-03

图10-3 使用Google开发者控制台创建GAE Web应用

GAE与其他Google服务

 

GAE和Google Cloud SQL这样的Google服务并不是 免费的。Google公司会为新注册的用户提供60天的试用期以及价值300美元的试用额度,因此读者应该可以免费实践本节介绍的内容,但是当试用期到期或者试用额度耗尽时,读者就需要付费才能继续使用这些服务了。

在安装完SDK之后,我们接下来要做的是对GAE的数据存储(datastore)进行配置。正如前所说,Google公司对直接的网络访问有严格的限制,用户是无法直接连接外部的PostgreSQL服务器的。为此,在这一节中,我们将使用Google公司提供的Google Cloud SQL服务来代替PostgreSQL。Google Cloud SQL是一个基于MySQL的云端数据库服务,用户可以通过cloudsql 包直接在GAE中使用该服务。

为了使用Google Cloud SQL,我们需要先通过开发者控制台创建一个数据库实例,具体步骤如图10-4所示。用户首先需要在控制台上点击自己创建的项目(在这个例子中,我创建的项目名为ws-g-1234 ),接着在左侧的导航面板中点击“Storage”(存储),然后再选择其中的“Cloud SQL”,从而进入Cloud SQL的设置页面。在点击“New Instance”(新实例)按钮之后,用户将会看到一些与创建数据库实例有关的选项。这些选项中的大部分已经预先设置好了,需要改动的地方不多,我们唯一要做的就是将“Preferred location”(首选位置)选项设置为“Follow App Engine App”(与App Engine的应用保持一致),并让项目的应用ID保持默认值不变。在进行了上述设置之后,我们的GAE应用就能够正常访问数据库实例了。

需要注意的是,因为Google公司默认会为用户的数据库实例提供一个免费的IPv6地址,但是却不会提供IPv4地址,所以如果你的桌面计算机、移动电脑、服务器或者你正在使用的网络供应商并没有使用IPv6连接,那么你还需要花一点额外的钱去获取一个IPv4地址,并将这个地址添加到设置页面。

10-04

图10-4 通过开发者控制台创建一个Google Cloud SQL数据库实例

除以上提到的少数几个选项之外,其他选项只需要保留默认即可。在最后,用户只需要为自己的实例设置一个名字,一切就大功告成了。

也许你已经预料到了,因为GAE平台是如此地别具一格,所以为了将Web应用部署到这个平台上,对代码的修改自然也变得无法避免了。下面从高层次的角度列出了将简单 Web 服务部署到GAE所需要做的一些事情:

因为GAE将接管被部署的整个应用,所以用户将无法控制应用何时被启动或者运行在哪个端口之上。实际上,用户编写的将不再是一个独立的应用,而是一个部署在GAE上的包。这样导致的结果是,用户将不能再使用main 这个为独立的Go程序预留的包名,而是要将包名修改为main 以外的其他名字。

接下来,用户还需要移除程序中的main 函数,并将该函数中的代码移到init 函数。对简单Web服务来说,我们需要将原来的main 函数:

func main() {
 server := http.Server{
  Addr: ":8080",
 }
 http.HandleFunc("/post/", handlePost)
 server.ListenAndServe()
}

修改为以下init 函数:

func init() {
 http.HandleFunc("/post/", handlePost)
}

注意,在新的init 函数里,原本用于指定服务器地址以及端口号的代码已经消失,同样消失的还有用于启动Web服务器的相关代码。

考虑到我们还需要将简单Web服务使用的数据库驱动从PostgreSQL切换至MySQL,因此我们需要在data.go 中导入MySQL数据库驱动,并设置正确的数据连接字符串:

import (
 "database/sql"
 _ "github.com/ziutek/mymysql/godrv"
)
func init() {
 var err error
 Db, err = sql.Open("mymysql", "cloudsql:<app ID>:<instance name>*<database
 ➥name>/<user name>/<password>")
 if err != nil {
  panic(err) 
 }
}

除了上述修改之外,我们还需要将相应的SQL查询修改为MySQL格式。尽管这两种数据库使用的语法非常相似,但并不完全相同,所以程序是无法在不做修改的情况下直接运行的。比如,对于以下代码中加粗显示的SQL查询语句:

func retrieve(id int) (post Post, err error) {
 post = Post{}
 err = Db.QueryRow("select id, content, author from posts where id =


 ➥$1

", id).Scan(&post.Id, &post.Content, &post.Author)
 return
}

我们将不再使用诸如$1 、$2 这样的标识,而是使用? 来表示被替换的变量,就像这样:

func retrieve(id int) (post Post, err error) {
 post = Post{}
 err = Db.QueryRow("select id, content, author from posts where id = ?",


 ➥id).Scan(&post.Id, &post.Content, &post.Author) ❶
 return
}

❶ 根据MySQL 的查询格式,将原来的$n 标识修改为?标识

在对代码做完必要的修改之后,我们接下来还要创建一个对应用进行描述的app.yaml 文件,如代码清单10-6所示。

代码清单10-6 用于GAE部署的app.yaml文件

application: ws-g-1234
version: 1
runtime: go
api_version: go1

handlers:
- url: /.*

 script: _go_app

这个文件非常简单,一目了然,读者在进行测试时,唯一需要做的就是在这个文件中修改应用的名字,然后一切就大功告成了!以上就是将简单Web服务部署到GAE上所需要做的全部工作,接下来,是时候对这个将要运行在GAE之上的简单Web服务做一些测试了!

因为我们在前面对应用做了大量的修改,所以可能会有读者觉得自己已经无法在本地的机器上运行这个应用了,不过这种担心是不必要的——开发者只需要使用Google公司提供的GAE SDK,就可以在本地运行自己的GAE应用了。

在按照下载页面提供的指示安装了GAE SDK之后,我们只需要在应用的根目录下使用终端执行以下命令,就可以运行自己的GAE Web应用了:

goapp serve

GAE SDK提供了在本地运行GAE应用所需的环境,从而使用户可以在本地测试自己的应用。除此之外,GAE SDK还提供了一个本地运行的管理网站(admin site),用户只需访问http://localhost:8000/,就可以通过该网站检视自己编写的代码。遗憾的是,在撰写本书的时候,开发环境还不支持Cloud SQL,所以我们还无法直接在本地测试简单Web服务。解决这个问题的一种方法是在本地使用MySQL服务器进行测试,然后在生产环境中继续使用Cloud SQL数据库。

在确保应用一切正常之后,用户就可以通过执行以下命令,将应用部署到Google公司的服务器上了:

goapp deploy

在执行以上命令之后,SDK将把应用的代码推送到Google公司的服务器,然后由服务器对其进行编译和部署。如果一切正常,被推送的应用将如期地出现在互联网上。比如,我们可以通过http://ws-g-1234.appspot.com/访问名为ws-g-1234 的应用。

为了测试这个刚刚部署完毕的简单Web服务,我们可以使用以下命令,让curl向服务器发送一个创建数据库记录的POST 请求:

curl -i -X POST -H "Content-Type: application/json" -d '{"content":"My first 
   post","author":"Sau Sheong"}' http://ws-g-1234.appspot.com/post/
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Date: Sat, 01 Aug 2015 06:46:59 GMT
Server: Google Frontend
Content-Length: 0
Alternate-Protocol: 80:quic,p=0

现在再次使用curl去获取刚刚创建的数据库记录:

curl -i -X GET http://ws-g-1234.appspot.com/post/1
HTTP/1.1 200 OK
Content-Type: application/json
Date: Sat, 01 Aug 2015 06:44:29 GMT
Server: Google Frontend
Content-Length: 69
Alternate-Protocol: 80:quic,p=0
{
  "id": 1,
  "content": "My first post",
  "author": "Sau Sheong"
}

GAE非常强大,它拥有许许多多的功能,这些功能可以帮助开发者在互联网上创建和部署可扩展的Web应用,但因为GAE是Google公司开发的平台,所以如果用户想要使用这个平台,就必须遵守这个平台的规则。

前一节简单地介绍过Docker,讨论了如何将Go Web应用封装为Docker容器并将其推送至可用的Docker托管服务上,而在本节中,我们将会更加完整地学习Docker的部署方法,并研究如何将简单Go Web服务部署到本地Docker宿主机以及云端的Docker宿主机之上。

Docker是一个非常了不起的项目,PaaS公司dotCloud最初在2013年发布了这个开源项目,之后无论是大型公司还是小型公司,都被这一项目震撼了。Google、AWS以及微软这样的技术公司都在拥抱Docker,AWS拥有EC2 Container Service(容器服务),Google提供了Google Container Engine(容器引擎),Digital Ocean、Rackspace甚至IBM等众多云供应商也纷纷加入了支持Docker的行列当中。除此之外,像BBC、ING这样的银行以及高盛这样的传统公司也开始在内部尝试使用Docker。

一言以蔽之,Docker就是在容器中构建、发布和运行应用的一个开放平台。容器并不是一项新技术——它在Unix初期就已经出现,Docker最初基于Linux的容器就是在2008年引入的。Heroku的dynos同样也是一种容器。

如图10-5所示,容器与虚拟机的不同之处在于,虚拟机模拟的是包括操作系统在内的整个计算机系统,而容器只提供操作系统级别的虚拟化,并将计算机资源划分给多个独立的用户空间实例使用。这两种虚拟方式的差异导致容器对资源的需求比虚拟机要少得多,并且容器的启动速度和部署速度也比虚拟机快得多。

10-05

图10-5 容器与虚拟机的不同之处在于,容器提供的是操作系统级别的虚拟化,并且容器可以将资源划分给多个独立的用户空间实例

Docker实质上就是一种管理容器的软件,它的存在使开发者可以更为简单地使用容器。除Docker之外,市面上还存在着chroot、Linux containers(LXC)、Solaris Zones、CoreOS和lmctfy等一系列同类软件,但Docker是其中名声最显赫的一款。

Docker目前只能在基于Linux的系统上工作,但它也提供了一些变通的方法,使OS X用户和Windows用户也能够在自己的系统上使用Docker的开发工具。为了安装Docker,用户需要访问https://docs.docker.com/engine/installation,然后根据自己的系统以及想要使用的Docker版本,按照说明安装。对于Ubuntu Server 14.04,我们可以通过执行以下这个简单的命令来安装Docker:

wget -qO- https://get.docker.com/ | sh

在安装Docker之后,我们可以通过执行以下这条命令来确认Docker是否已经安装成功:

sudo docker run hello-world

这条命令会从远程代码库中拉取hello-world 镜像,并作为本地容器运行这个镜像。

如图10-6所示,Docker引擎 (简称Docker)包含多个组件。刚才在测试Docker安装是否成功时,我们就用到了第一个组件Docker客户端 ,它就是用户在与Docker守护进程交互时所使用的命令行接口。

10-06

图10-6 Docker引擎由Docker客户端、Docker 守护进程以及不同的Docker容器组成,这些容器为Docker镜像的实例。Docker镜像可以通过Dockerfile创建,并且镜像还能够存储在Docker注册中心(registy)中

Docker守护进程 运行在宿主操作系统(host OS)之上,该进程会对客户端发送的服务请求进行应答,并对容器进行管理。

Docker容器 (简称容器)是对运行特定应用所需的全部程序(包括操作系统在内)的一种轻量级虚拟化。轻量级容器会让应用以及与之相关联的其他程序认为自己独占了整个操作系统以及所有硬件,但是实际上并非如此,多个应用共享同一宿主操作系统。

Docker容器都基于Docker镜像 构建,后者是辅助容器进行启动的只读模板,所有容器都需要通过镜像启动。有好几种不同的方法可以创建Docker镜像,其中一种就是在一个名为Dockerfile的文件里包含一系列指令。

Docker镜像既可以以本地形式存储在运行着Docker守护进程的机器上(也就是Docker的宿主机之上),也可以被托管至名为Docker注册中心的Docker镜像资源库里面。用户既可以使用自己的私有Docker注册中心,也可以使用Docker Hub(https://hub.docker.com/)作为自己的registy。Docker Hub同时提供公开和私有的Docker镜像,但私有的Docker镜像需要付费才能使用。

如果用户是在类似Ubuntu这样的Linux系统上安装Docker,那么Docker守护进程和Docker客户端将被安装到同一机器里面。但如果用户是在OS X和Windows这样的系统上安装Docker,那么Docker只会把客户端安装在操作系统里面,而守护进程则会被安装到其他地方,通常会是一个运行在该系统之上的虚拟机里面。这种情况的一个例子是,在OS X上安装Docker时,Docker客户端将被安装到OS X里面,而Docker守护进程则会被安装到VirtualBox(一款基于x86架构的虚拟机监视器)的一个虚拟机里面。

在此之后,用户只需要通过Docker镜像来运行Docker容器,并将其运行在Docker宿主之上就可以了。

在对Docker有了一个大体的了解之后,我们是时候来学习如何将Web应用部署到Docker里面了。接下来的一节将继续使用前面展示过的简单Web服务作为例子,演示如何将Web应用部署到Docker容器。

尽管Docker使用了那么多的技术,但Docker化一个Go Web应用却一点也不困难。因为Web服务拥有对整个容器的完整访问权限,所以我们不需要对服务的代码做任何修改,只要使用Docker并进行相应的配置就可以了。作为例子,图10-7从高层次的角度展示了将一个Web应用Docker化并部署到本地以及云端的具体步骤。

在本节中,我们将使用ws-d 作为Web服务的名字。部署的第一步是在应用程序的根目录中创建一个代码清单10-7所示的Dockerfile文件。

10-07

图10-7 将Go Web应用Docker化并部署到本地以及云端的具体步骤

代码清单10-7 简单Web服务的Dockerfile文件

FROM golang ❶

ADD . /go/src/github.com/sausheong/ws-d ❷
WORKDIR /go/src/github.com/sausheong/ws-d
RUN go get github.com/lib/pq ❸
RUN go install github.com/sausheong/ws-d

ENTRYPOINT /go/bin/ws-d ❹

EXPOSE 8080 ❺

❶ 使用一个安装了Go 并且将GOPATH 设置为/go 的Debian 镜像作为容器的起点

❷ 把本地的包文件复制到容器的工作空间里面

❸ 在容器内部构建ws-d 命令

❹ 把ws-d 命令设置为随容器启动

❺ 注明该服务监听的端口号为8080

这个Dockerfile文件的第一行告诉Docker使用golang 镜像启动,这是一个安装了最新版Go并将工作空间设置为/go 的Debian镜像。之后的两行会将当前目录中的本地代码复制到容器中,并设置相应的工作目录。在此之后,文件使用RUN 命令指示Docker获取PostgreSQL驱动并构建Web服务的代码,然后将可执行的二进制文件放置到/go/bin 目录中。在此之后,文件使用ENTRYPOINT 命令指示Docker将/go/bin/ws-d 设置为随容器启动。最后,文件使用EXPOSE 命令指示容器将8080端口暴露给其他容器。需要注意的是,这个EXPOSE 命令只会对同一宿主内的其他容器打开8080端口,但它并不会对外开放8080端口。

在编写好Dockerfile文件之后,我们就可以使用以下命令来构建镜像了:

docker build –t ws-d .

这条命令将执行Dockerfile文件,并根据文件中的指示构建一个本地镜像。如果一切顺利,那么在这条命令执行完毕之后,用户应该可以通过docker images 命令看到新鲜出炉的镜像文件:

REPOSITORY   TAG     IMAGE ID     CREATED     VIRTUAL SIZE
ws-d      latest    65e8437fce6b   10 minutes ago  534.7 MB

在成功创建镜像之后,我们就可以通过运行镜像来创建和启动容器了:

docker run --publish 80:8080 --name simple_web_service --rm ws-d

这条命令会通过ws-d 镜像创建出一个名为simple_web_service 的容器。--publish80:8080标志 打开HTTP端口80并将其映射至前面通过EXPOSE 命令暴露的8080端口,而—rm 标志则指示Docker在容器已经存在的情况下,先移除已有的容器,然后再创建并启动新容器。如果不设置--rm 标志,那么Docker在容器已经存在的情况下将保留已有的容器,并直接启动该容器,而不是创建并启动新容器。为了确认容器是否已经启动,我们可以执行以下命令:

docker ps

如果一切正常,你的容器应该会作为其中一员,出现在已激活容器列表当中:

CONTAINER ID IMAGE ... PORTS         NAMES
eeb674e289a4 ws-d ... 0.0.0.0:80->8080/tcp  simple_web_service

因为页面宽度的限制,这里忽略了docker ps 命令输出的某些列,但这里展示的信息已经足以表明我们的容器现在已经正常地运行在本地的Docker宿主之上了。跟之前一样,我们可以通过curl 命令向服务器发送一个POST 请求,创建一条记录:

curl -i -X POST -H "Content-Type: application/json" -d '{"content":"My first 
➥post","author":"Sau Sheong"}' http://127.0.0.1/post/
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Date: Sat, 01 Aug 2015 06:46:59 GMT
Server: Google Frontend
Content-Length: 0
Alternate-Protocol: 80:quic,p=0

现在,通过curl命令获取之前创建的记录:

curl -i -X GET http://127.0.0.1/post/1
HTTP/1.1 200 OK
Content-Type: application/json
Date: Sat, 01 Aug 2015 06:44:29 GMT
Server: Google Frontend
Content-Length: 69
Alternate-Protocol: 80:quic,p=0
{
  "id": 1,
  "content": "My first post",
  "author": "Sau Sheong"
}

把简单Web服务Docker化为容器听起来是一件非常棒的事情,但这个容器现在还只是运行在本地宿主上,而我们真正想要做的是把容器放到互联网上运行。有几种不同的方法可以把Docker容器部署到远程宿主上运行,目前来说,最简单的一种方法就是使用Docker机器了。

Docker机器 (machine)是一个命令行接口,它允许用户在本地以及云端创建公开或者私有的Docker宿主。在编写本书时,Docker机器支持包括AWS、Digital Ocean、Google Compute Engine、IBM Softlayer、Microsoft Azure、Rackspace、Exoscale和VMWare vCloud Air在内的公有云供应商;与此同时,Docker机器也支持在私有云供应商以及运行着OpenStack、VMWare或者Microsoft Hyper-V的云供应商上创建宿主。

Docker机器并不会与Docker本身一同被安装,而需要单独安装。用户可以通过克隆代码库https://github.com/docker/machine或者从https://docs.docker.com/machine/install-machine/下载相应平台的二进制包来安装Docker机器。比如,使用Linux的用户就可以通过以下命令来获得Docker机器的二进制包:

curl -L https://github.com/docker/machine/releases/download/v0.3.0/docker-
➥machine_linux-amd64 /usr/local/bin/docker-machine

在下载完二进制包之后,用户还需要执行以下命令将二进制包变成可执行文件:

chmod +x /usr/local/bin/docker-machine

在下载完Docker机器并将它变成可执行文件之后,用户就可以在Docker机器支持的云端上创建Docker宿主了。要做到这一点,其中最为轻松的一种办法就是使用Digital Ocean。Digital Ocean是一个虚拟专用服务器(virtual private server,VPS)供应商,它的服务以易于使用以及价格实惠而著称(VPS是供应商以服务形式销售的虚拟机)。Digital Ocean在2015年5月成为了仅次于AWS的世界第二大Web服务器托管公司。

为了在Digital Ocean上创建Docker宿主,我们需要先申请一个Digital Ocean账号,并在拥有账号之后,访问Digital Ocean的“Applications & API”(应用与API)页面https://cloud.digitalocean.com/ settings/applications。

图10-8展示了“Applications & API”页面的样子,该页面中包含了一个“Generate new token” (生成新令牌)按钮,我们可以通过点击这个按钮来生成一个新的令牌。生成令牌时首先要做的就是输入一个名字,并勾选其中的“Write”(写入)复选框,然后点击“Generate new token”(生成令牌)按钮。这样一来,你就会拥有一个由用户名和密码混合而成的个人访问令牌,这个令牌可以用于进行API身份验证。需要注意的是,令牌只会在生成时出现一次,之后便不再出现,因此用户需要把这个令牌存储到安全的地方。

10-08

图10-8 在Digital Ocean上生成个人访问令牌非常简单,只需要点击“Generate new token”即可

为了使用Docker机器在Digital Ocean上创建Docker宿主,我们需要在控制台执行以下命令:

docker-machine create --driver digitalocean --digitalocean-access-token <tokenwsd
Creating CA: /home/sausheong/.docker/machine/certs/ca.pem
Creating client certificate: /home/sausheong/.docker/machine/certs/cert.pem
Creating SSH key...
Creating Digital Ocean droplet...
To see how to connect Docker to this machine, run: docker-machine env wsd

在成功创建远程Docker宿主之后,接下来要做的就是与之进行连接。注意,因为我们的Docker客户端目前还连接着本地Docker宿主,所以我们需要对它进行调整,让它改为连接Digital Ocean上的Docker宿主。Docker机器返回的结果提示我们应该如何做到这一点。简单来说,我们需要执行以下命令:

docker-machine env wsd
export DOCKER_TLS_VERIFY="1"
export DOCKER_HOST="tcp://104.236.0.57:2376"
export DOCKER_CERT_PATH="/home/sausheong/.docker/machine/machines/wsd" 
export DOCKER_MACHINE_NAME="wsd"
# Run this command to configure your shell:
# eval "$(docker-machine env wsd)"

这条命令展示了云上的Docker宿主的环境设置,而我们要做的就是修改现有的环境设置,让客户端指向这个Docker宿主而不是本地Docker宿主,这一点可以通过执行以下命令来完成:

eval "$(docker-machine env wsd)"

这条简单的命令会让Docker客户端连接到Digital Ocean的Docker宿主之上。为了确认这一点,我们可以执行以下命令:

docker images

如果一切正常,应该不会看见任何镜像。回想一下,之前我们在连接本地Docker宿主的时候,曾经在本地创建过一个镜像,如果客户端还在连接本地宿主,那么至少会看到之前创建的镜像,而没有看见任何镜像则表示客户端已经没有再连接到本地Docker宿主了。

因为新的Docker宿主还没有任何镜像可用,所以我们接下来要做的就是在新宿主上重新创建镜像,为此,我们需要再次执行之前提到过的docker build 命令:

docker build –t ws-d .

在这条命令执行完毕之后,用户使用docker images 至少会看到两个镜像,其中一个是golang 基础镜像,而另一个则是新创建的ws-d 镜像。现在,一切都已就绪,我们最后要做的就是跟之前一样,通过镜像运行容器:

docker run --publish 80:8080 --name simple_web_service --rm ws-d

这条命令将在远程Docker宿主上面创建并启动一个容器。为了验证这一点,我们可以跟之前一样,通过curl 创建并获取一条数据库记录。跟之前不一样的是,这次curl 将不再是向本地服务器发送 POST 请求,而是向远程服务器发送 POST 请求,而这个远程服务器的 IP 地址就是docker-machine env wsd 命令返回的IP地址:

curl -i -X GET http://104.236.0.57/post/1
HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 03 Aug 2015 11:35:46 GMT
Content-Length: 69
{
  "id": 2,
  "content": "My first post",
  "author": "Sau Sheong"
}

大功告成!以上就是通过Docker容器将一个简单的Go Web服务部署到互联网所需的全部步骤。Docker并不是部署Go Web应用最简单的方式,但这种部署方式正在变得越来越流行。与此同时,通过使用Docker,用户只需要在本地成功部署过一次,就可以毫不费力地在多个私有或者公有的云供应商上重复进行部署,而这一点正是Docker真正的威力所在。幸运的是,现在你已经知道该如何通过Docker来获得这一优势了。

为了保证本章以及本节的内容足够简短并且目标足够明确,这里介绍的内容省略了大量的细节。如果你对Docker感兴趣(这是一件好事,因为它是一个非常有趣的新工具),那么可以花些时间阅读Docker的在线文档(https://docs.docker.com/)以及其他关于Docker的文章。

在结束本章之前,让我们通过表 10-1 来回顾一下本章介绍的几种部署方法,不过别忘了,这些方法只是许许多多Web应用部署方法中的几种而已。

表10-1 几种Go Web应用部署方法的对比

独立服务器

Heroku

GAE

Docker

类型

公有或私有

公有

公有

公有或私有

是否需要修改代码

不需要

少量

中等

不需要

是否需要配置系统

大量

不需要

不需要

中等

是否需要维护

大量

不需要

不需要

中等

部署的难度

低

高

中等

低

平台对应用的支持程度

无

低

高

低

应用与平台的紧密程度

无

低

高

低

可扩展性

无

中等

高

高

评注

对于这种自力更新式的部署方式,使用者需要自己完成几乎所有事情

Heroku是一个公有PaaS平台,除了少数几项限制之外,使用者几乎可以做所有事情

GAE是一个严格受限的PaaS平台,使用者需要与平台密切绑定

Docker是一项非常有前景的技术,无论是公有的部署还是私有的部署,都有很多供应商可供选择