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

IaaS、PaaS和SaaS

云计算供应商都会通过不同的模型来为用户提供服务。美国国家标准技术研究所(National Institute of Standards and Technology, USDepartment 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 ComputingService,EC2)、Google公司的Compute Engine(计算引擎)以及Digital Ocean的Droplets。

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

使用SaaS模型的供应商会向用户提供应用服务。尽管消费者当今使用的绝大多数服务都可以看作是SaaS服务,但是在本书的语境中,我们只会把Heroku的Postgres数据库服务(Postgres database ser-vice,它提供的是基于云的Postgres服务)、AWS的RelationalDatabase Service(关系数据库服务,RDS)以及Google公司的CloudSQL(云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 limit10 5respawn设置了参数,它指示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 wssausheo+ 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——这是因为Upstartkill命令执行之后,察觉到了ws-s进程已被关闭,于是它重启了ws-s进程,从而导致ws-s进程的ID发生了变化。

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

results matching ""

    No results matching ""