让我们从最简单的部署方法开始——创建一个可执行的二进制文件,并将它放到互联网的某个服务器上运行,这个服务器可以是物理存在的,也可以是由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 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 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——这是因为Upstart
在kill
命令执行之后,察觉到了ws-s
进程已被关闭,于是它重启了ws-s
进程,从而导致ws-s
进程的ID发生了变化。
最后,因为大部分Web应用都部署在标准HTTP端口(即80端口)之上,所以读者在实际部署时,应该将简单Web服务代码中的端口号从现在的8080改为80,或者通过某种机制将8080端口的流量代理或者重定向到80端口。