Skip to content

Latest commit

 

History

History
1054 lines (719 loc) · 49.4 KB

11.md

File metadata and controls

1054 lines (719 loc) · 49.4 KB

十一、部署同构 Go Web 应用

通过我们在上一章中实现的自动化端到端测试,IGWEB 演示网站现在满足了预期功能的基线集。是时候将我们的同构 Go web 应用释放到 web 上了。现在是将 IGWEB 部署到生产环境的时候了。

我们对同构 Go 生产部署的探索将包括将 IGWEB 作为静态二进制可执行文件以及静态资产部署到独立服务器(真实或虚拟)以及将 IGWEB 部署为多 docker 容器应用。

部署 web 应用是一个浩瀚的主题,是一片海洋,值得许多专门讨论这一主题的书籍阅读。真实世界的 web 应用部署可能包括持续集成、配置管理、自动化测试、部署自动化工具和敏捷团队管理。这些部署还可能包括多个团队成员,在部署过程中扮演各种角色。

本章的重点将仅仅是由单个人员部署同构的 Go web 应用。为了便于说明,将手动执行展开程序。

要成功地为生产使用准备同构的 Go web 应用,需要考虑一些因素,例如缩小 GopherJS 生成的 JavaScript 源文件,确保静态资产在启用 GZIP 压缩的情况下传输到 web 客户端。通过将本章中介绍的材料重点放在同构 Go 上,读者可以根据自己的特殊部署需要调整本章中介绍的概念和技术。

在本章中,我们将介绍以下主题:

  • IGWEB 如何在生产模式下运行
  • 将同构 Go web 应用部署到独立服务器
  • 使用 Docker 部署同构 Go web 应用

IGWEB 如何在生产模式下运行

在继续进行生产部署之前,我们需要了解服务器端 web 应用igweb在进入生产模式时是如何运行的。在启动igweb服务器端应用之前,可以通过将IGWEB_MODE环境变量的值设置为"production"来打开生产模式,如下所示:

$ export IGWEB_MODE=production

IGWEB 在生产模式下运行时将发生三种重要行为:

  1. 包含客户端应用的 JavaScript 外部<script>标记(位于头部分模板内)将请求位于$IGWEB_APP_ROOT/static/js/client.min.js的缩小 JavaScript 源文件。
  2. 当 web 服务器实例启动时,COG(cogimport.csscogimport.js的静态资产不会自动生成。相反,包含 CSS 和 JavaScript 捆绑静态资产的缩小源文件将分别位于$IGWEB_APP_ROOT/static/css/cogimports.min.css$IGWEB_APP_ROOT/static/js/cogimports.min.js
  3. 与其依赖于在$IGWEB_APP_ROOT/shared/templates文件夹中找到的模板,模板将从保存在磁盘上的单个 gob 编码的模板包文件中读取。

我们将考虑服务器端 Web 应用如何响应这些行为中的每一个。

GopherJS 生成的 JavaScript 源文件

在定义模板函数的funcs.go源文件中,我们引入了一个名为IsProduction的新函数:

func IsProduction() bool {
  if isokit.OperatingEnvironment() == isokit.ServerEnvironment {
    return os.Getenv("IGWEB_MODE") == "production"
  } else {
    return false
  }
}

此函数用于服务器端,如果当前操作模式为生产,则返回值true,如果不是,则返回值false。我们可以在模板中使用此自定义函数来确定从何处获取客户端 JavaScript 应用。

在非生产模式下运行时,client.js源文件将从/js/client.js的服务器相对路径获取。在生产模式下,将从/static/js/client.min.js的服务器相对路径获取缩小的 JavaScript 源文件。

在 header 部分模板中,我们调用productionmode自定义函数来确定从哪个路径为客户端 JavaScript 源文件提供服务,如下所示:

<head>
  <meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
  <title>{{.PageTitle}}</title> 
  <link rel="icon" type="image/png" href="/statimg/isomorphic_go_icon.png">
  <link rel="stylesheet" href="/static/css/pure.min.css">
 {{if productionmode}}
  <link rel="stylesheet" type="text/css" href="/static/css/cogimports.min.css">
  <link rel="stylesheet" type="text/css" href="/static/css/igweb.min.css">
  <script type="text/javascript" src="/static/js/client.min.js" async></script>
  <script src="/static/js/cogimports.min.js" type="text/javascript" async></script>
 {{else}}
  <link rel="stylesheet" type="text/css" href="/static/css/cogimports.css">
  <link rel="stylesheet" type="text/css" href="/static/css/igweb.css">
  <script src="/static/js/cogimports.js" type="text/javascript" async></script>
  <script type="text/javascript" src="/js/client.js" async></script>
  {{end}}
</head>

您可能想知道为什么我们在非生产模式和生产模式之间包含不同的 JavaScript 源文件(client.jsclient.min.js)。回想一下,在运行了kick的开发环境中,client.jsclient.js.map源文件在$IGWEB_APP_ROOT/client文件夹中生成。在igweb.go中,我们注册了路由的处理函数,这些路由将/js/client.js路径和/js/client.js.map路径映射到$IGWEB_APP_ROOT/client文件夹中相应的源文件:

  // Register Handlers for Client-Side JavaScript Application
  if WebAppMode != "production" {
    r.Handle("/js/client.js", isokit.GopherjsScriptHandler(WebAppRoot)).Methods("GET")
    r.Handle("/js/client.js.map", isokit.GopherjsScriptMapHandler(WebAppRoot)).Methods("GET")
  }

这为我们提供了方便,我们可以在对应用代码进行更改的瞬间kick自动为我们传输 JavaScript 代码。在非生产模式下,我们不希望缩小 JavaScript 源文件,这样我们可以通过 web 控制台获得更详细的调试信息,例如死机堆栈跟踪(见附录调试同构 Go中所述)。

在生产模式下,不需要使用kick。如果您检查client.js源文件的文件大小,您会注意到它大约有 8.1MB 大!这确实是一个严重的标签冲击!在下一节中,我们将学习如何将笨重的文件占用空间缩小到合适的大小。

驯服 GopherJS 产生的 JavaScript 文件大小

在生产部署过程中,我们必须发出gopherjs build命令,指定缩小生成的 JavaScript 源文件并将 JavaScript 源文件的输出保存到指定目标位置的选项。

我们必须缩小生成的 JavaScript 代码以减小其文件大小。如前所述,未缩小的 JavaScript 源文件是 8.1MB!我们可以通过缩小源文件的大小,使用-m选项运行gopherjs build命令,并使用clientonly值指定--tags选项,将源文件的大小进一步减小到 2.9 MB,如下所示:

$ gopherjs build -m --verbose --tags clientonly -o $IGWEB_APP_ROOT/static/js/client.min.js

[T0]标记告诉 isokit 避免传输客户端应用未使用的源文件。-o选项将把生成的输出 JavaScript 源文件放在指定的目标位置。

在运行gopherjs build命令之前,最好先执行$IGWEB_APP_ROOT/scripts目录中的clear_gopherjs_cache.shbash 脚本。它将清除从以前的gopherjs build运行中缓存的项目工件。

为一个将近 3MB 的 JavaScript 源文件提供服务对于生产需求来说仍然是站不住脚的。通过启用 GZIP 压缩,我们可以进一步减少传输文件的大小。一旦使用 GZIP 压缩发送源文件,传输文件大小将约为 510 KB。我们将在启用 GZIP 压缩部分学习如何在 web 服务器上启用 GZIP 压缩。

生成静态资产

在部署服务器端 Go web 应用时,通常不仅推出 web 服务器实例的二进制可执行文件,还推出静态资产文件(CSS、JavaScript、模板文件、图像、字体等)和模板文件。在传统的 Go web 应用中,我们必须将各个模板文件推送到生产系统,因为传统的 Go web 应用依赖于每个单独的文件,以便在服务器端呈现给定的模板。

由于我们利用了通过运行的应用在内存中持久化模板集的概念,因此不需要将单个模板文件带到生产环境中。这是因为我们只需要生成内存中的模板集,即一个gob编码的模板包文件,该文件保存在$IGWEB_APP_ROOT/static/templates文件夹中的磁盘上。

通过在isokit包中设置导出的StaticTemplateBundleFilePath变量,我们指示 isokit 在我们提供的文件路径上生成静态模板包文件。下面是igweb.go源文件中initializeTemplateSet函数的一行,我们在其中设置变量:

 isokit.StaticTemplateBundleFilePath = StaticAssetsPath + "/templates/igweb.tmplbundle"

第 9 章Cogs–可重用组件中,我们了解到当igweb应用首次启动时,isokit 将所有 Cogs 中的所有 JavaScript 源文件捆绑到一个cogimports.js源文件中。以类似的方式,来自所有 COG 的所有 CSS 样式表被捆绑到一个cogimports.css源文件中。在非生产模式下运行 IGWEB 时,通过调用igweb.go源文件中的initailizeCogs函数中的isokit.BundleStaticAssets函数(粗体显示),自动绑定静态资产:

func initializeCogs(ts *isokit.TemplateSet) {
  timeago.NewTimeAgo().CogInit(ts)
  liveclock.NewLiveClock().CogInit(ts)
  datepicker.NewDatePicker().CogInit(ts)
  carousel.NewCarousel().CogInit(ts)
  notify.NewNotify().CogInit(ts)
  isokit.BundleStaticAssets()
}

不应在生产环境中使用自动静态资产绑定,因为绑定 JavaScript 和 CSS 的动态功能依赖于具有已安装 Go 发行版和已配置 Go 工作区的服务器,并且必须在该 Go 工作区中存在对 COG 源文件的访问。

这立即消除了 Go 开箱即用的优点之一。由于 Go 生成静态链接的二进制可执行文件,所以我们不需要在生产服务器上安装 Go 运行时来部署应用。

当我们在生产模式下运行 IGWEB 时,我们可以通过在igweb.go源文件中找到的initializeTemplateSet函数中引入以下代码来防止自动静态资产绑定:

  if WebAppMode == "production" && oneTimeStaticAssetsGeneration == false {
    isokit.UseStaticTemplateBundleFile = true
    isokit.ShouldBundleStaticAssets = false
  }

我们指示 isokit 使用静态模板捆绑文件,并且我们指示 isokit 不要自动捆绑静态资产。

为了生成同构 Go web 应用所需的静态资产(CSS、JavaScript 和模板包),我们可以在非生产系统上使用--generate-static-assets flag运行igweb

$ igweb --generate-static-assets

此命令将生成必要的静态资产,然后退出igweb程序。此功能的实现可以在igweb.go源文件中定义的generateStaticAssetsAndExit函数中找到:

func generateStaticAssetsAndExit(env *common.Env) {
  fmt.Print("Generating static assets...")
  isokit.ShouldMinifyStaticAssets = true
  isokit.ShouldBundleStaticAssets = true
  initializeTemplateSet(env, true)
  initializeCogs(env.TemplateSet)
  fmt.Println("Done")
  os.Exit(0)
}

指令igweb生成静态资产时,将创建三个文件:

  • $IGWEB_APP_ROOT/static/templates/igweb.tmplbundle(模板包)
  • $IGWEB_APP_ROOT/static/css/cogimports.min.css(小型 CSS 包)
  • $IGWEB_APP_ROOT/static/js/cogimports.min.js(缩小的 JavaScript 包)

在执行生产部署时,可以将整个$IGWEB_APP_ROOT/static文件夹复制到生产系统,确保上述三个静态资产在生产系统上可用。

至此,我们已经确定了 IGWEB 将如何在生产模式下运行。现在,是时候执行最简单的部署了,将同构的 Go web 应用部署到独立服务器上。

将同构 Go web 应用部署到独立服务器

为了演示独立的同构 Go 部署,我们将使用托管在 Linode(上的虚拟专用服务器(VPS)http://www.linode.com )。本文介绍的过程适用于任何其他云提供商,也适用于独立服务器恰好是驻留在服务器机房中的真实服务器的场景。我们将概述的独立部署过程是手动执行的,以说明该过程的每个步骤

配置服务器

本演示中的服务器以及本章后续演示中提到的服务器将在 Linode 上运行 Ubuntu Linux 版本 16.04 LTS,Linode 是**虚拟专用服务器(VPS)**实例的提供商。我们将运行 Linode 默认的 Ubuntu16.04 库存映像,而不做任何内核修改。

当我们发出本章中以sudo开头的任何命令时,我们假设您的用户帐户是 sudoers 组的一部分。如果您使用的是服务器的根帐户,则不需要在命令前面加上[T1]。

我们将通过发出以下命令来创建一个名为igweb的权限较低的用户:

$ sudo adduser igweb

运行adduser命令后,系统将提示您输入igweb用户的其他信息和密码。如果未提示您输入用户密码,您可以通过发出以下命令来设置密码:

$ sudo passwd igweb

igweb应用依赖于两个组件才能正常运行。首先,我们需要安装 Redis 数据库。第二,我们需要安装nginx。我们将使用nginx作为反向代理服务器,这将允许我们在向 web 客户端提供静态资产时启用 GZIP 压缩。正如您将看到的,当涉及到 GopherJS 生成的 JavaScript 源文件的文件大小时,这会产生巨大的差异(510KB 与 3MB)。图 11.1描述了 Linode VPS 实例,包括三个关键组件igwebnginxredis-server

图 11.1:运行 igweb、nginx 和 redis 服务器的 Linode VPS 实例

设置 Redis 数据库实例

您可以按照第 2 章同构 Go 工具链中演示的相同步骤安装 Redis 数据库。执行此操作之前,应发出以下命令以安装基本构建工具:

$ sudo apt-get install build-essential tcl

安装 Redis 数据库后,应通过发出以下命令启动 Redis 服务器:

$ sudo redis-server --daemonize yes

--daemonize命令行参数允许我们在后台运行 Redis 服务器。即使会话结束,服务器仍将继续运行。

您应该通过添加足够的防火墙规则来保护 Redis 安装,以防止外部流量访问端口 6379(Redis 服务器实例的默认端口)。

设置 NGINX 反向代理

尽管作为 Go 应用的igwebweb 服务器实例可以单独满足服务 IGWEB 的主要需求,但将igwebweb 服务器实例置于反向代理之后更为有利。

反向代理服务器是一种代理服务器,它将通过将请求发送到指定的目标服务器(在本例中为[T0])来服务客户端请求,从[T1]服务器实例获取响应,并将响应发送回客户端。

反向代理之所以派上用场有几个原因。发布 IGWEB 带来直接好处的最重要原因是,我们可以在出站静态资产上启用 GZIP 压缩。除此之外,反向代理还允许我们根据需要轻松添加重定向规则来控制流量。

NGINX 是一种流行的高性能 web 服务器。我们将使用nginx作为位于igwebweb 服务器实例前面的反向代理。图 11.2描述了一个典型的反向代理配置,其中 web 客户端将通过端口 80 发出 HTTP 请求,nginx将通过端口 8080 向igweb服务器实例发送 HTTP 请求来服务请求,并从igweb检索响应服务器并通过端口 80 将响应发送回 web 客户端:

图 11.2:反向代理配置

下面是nginx配置文件nginx.conf的列表,我们将使用它作为反向代理运行nginx

user igweb;
worker_processes 1;

error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';

    sendfile on;
    keepalive_timeout 65;

 gzip on;
 gzip_min_length 1100;
 gzip_buffers 16 8k;
 gzip_types text/plain application/javascript text/css;
 gzip_vary on;
 gzip_comp_level 9;

    server_tokens off;

    server {
        listen 80;
        access_log /var/log/nginx/access.log main;
        location / {
 proxy_pass http://192.168.1.207:8080/;
 proxy_set_header X-Forwarded-For $remote_addr;
 proxy_http_version 1.1;
 proxy_set_header Upgrade $http_upgrade;
 proxy_set_header Connection "upgrade";
 proxy_set_header Host $host;
        }
    }
}

我们特别感兴趣的设置有两部分,一部分用于启用 GZIP 压缩,另一部分用于代理设置。

启用 GZIP 压缩

让我们检查与启用 GZIP 压缩相关的nginx配置设置。

我们将gzip指令设置为on以启用服务器响应的 gzip。

gzip_min_length指令允许我们指定 gzip 响应的最小长度。

gzip_buffers指令设置用于压缩响应的缓冲区的数量和大小。我们已经指定使用 16 个缓冲区,内存页大小为 8K。

gzip_types指令允许我们指定除了text/HTML之外还应该启用 GZIP 压缩的 MIME 类型。我们已经为纯文本文件、JavaScript 源文件和 CSS 源文件指定了 MIME 类型。

gzip_vary指令用于启用或禁用变量:接受编码响应头。Vary:Accept Encoding响应标头指示缓存在标头发生变化时存储不同版本的网页。对于不支持 GZIP 编码的 web 浏览器,此设置对于正确接收文件的未压缩版本尤为重要。

[T0]指令指定将使用的 GZIP 压缩级别。我们指定了一个值 9,这是 GZIP 压缩的最大级别。

代理设置

nginx配置设置中重要的第二部分是反向代理设置。

我们在location块中包含proxy_pass指令,该指令的值为 web 服务器的地址和端口。这指定所有请求都应发送到位于http://192.168.1.207:8080的指定代理服务器(igweb

记住用运行igweb实例的机器的 IP 地址替换本例中显示的 IP 地址 192.168.1.207。

反向代理将从igweb服务器实例获取响应并将其发送回 web 客户端。

proxy_set_header指令允许我们重新定义(或附加)传递到代理服务器的请求头字段。我们已经包含了X-Forwaded-For头,以便代理服务器能够识别发起请求的 web 客户端的原始 IP 地址。

为了支持 WebSocket 的正常功能(实时聊天功能依赖于 WebSocket),我们包括以下代理设置。首先,我们使用proxy_http_version指令指定服务器将使用 HTTP 版本 1.1。默认情况下,"Upgrade""Connection"头不会传递给代理服务器。因此,我们必须使用proxy_set_header指令将这些头发送到代理服务器。

我们可以通过发出以下命令来安装nginx

$ sudo apt-get install nginx

安装nginx后,web 服务器通常默认启动。但是如果没有,我们可以通过发出以下命令启动nginx

$ sudo systemctl start nginx

$IGWEB_APP_ROOT/deployments-config/standalone-setup文件夹中找到的nginx.conf文件可以放在生产服务器的/etc/nginx文件夹中。

图 11.3描述了我们尝试访问igweb.kamesh.comURL 时遇到的 502 坏网关错误:

图 11.3:502 坏网关错误

我们收到此服务器错误,因为我们尚未启动igweb。为了启动并运行igweb,我们首先需要在服务器上设置一个位置,在那里存放igweb二进制可执行文件和静态资产。

设置 IGWEB 根文件夹

IGWEB 根文件夹是igweb可执行文件和静态资产将驻留在生产服务器上的位置。我们使用以下命令成为生产服务器上的igweb用户:

$ su - igweb

我们在igweb用户的主目录中创建一个igweb文件夹,如下所示:

mkdir ~/igweb

这是包含[T0]web 服务器实例的二进制可执行文件和 IGWEB 演示网站所需的静态资产的目录。请注意,静态资产将驻留在~/igweb/static文件夹中。

交叉编译 IGWEB

使用go build命令,我们实际上可以为不同的目标操作系统构建二进制文件,这种技术称为交叉编译。例如,在我的 macOS 机器上,我可以构建一个 64 位 Linux 二进制文件,我们可以将它推送到运行 Ubuntu Linux 的独立生产服务器上。在构建二进制文件之前,我们通过设置GOOS环境变量指定要构建的目标操作系统:

$ export GOOS=linux

通过将GOOS环境变量设置为linux,我们已经指定希望为 Linux 生成一个二进制文件。

为了指定我们想要的二进制是 64 位二进制,我们设置了GOARCH环境变量来指定目标架构:

$ export GOARCH=amd64

通过将GOARCH变量设置为amd64,我们已经指定需要一个 64 位二进制文件。

让我们通过发出mkdir命令在igweb文件夹中创建一个builds目录:

$ mkdir $IGWEB/builds

该目录将作为包含各种操作系统的igweb二进制可执行文件的仓库。为了本章的目的,我们只考虑构建一个 64 位的 Linux 二进制文件,但是将来我们可以在这个目录中适应其他操作系统的构建,比如 Windows。

我们发出go build命令并提供-o参数来指定生成的二进制文件应该驻留在哪里:

$ go build -o $IGWEB_APP_ROOT/builds/igweb-linux64

我们已指示应在$IGWEB_APP_ROOT/builds文件夹中创建生成的 64 位 Linux 二进制文件,可执行文件的名称为igweb-linux64

您可以通过发出file命令来验证生成的二进制文件是否为 Linux 二进制文件:

$ file builds/igweb-linux64
builds/igweb-linux64: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped

从结果可以看出,go build命令已经生成了一个64-bit LSB(Linux 标准库)可执行文件。

如果您有兴趣为 Linux 以外的其他操作系统构建 Go 二进制文件,此链接将为您提供所有可能的[T0]和[T1]值的完整列表:https://golang.org/doc/install/source#environment

准备部署包

除了发送igweb可执行文件外,我们还需要发送静态文件夹的内容,该文件夹保存了 IGWEB 的所有静态资产。

准备部署捆绑包的静态资产包括以下步骤:

  1. 传输客户端应用
  2. 生成静态资产包(模板包、CSS 和 JavaScript)
  3. 缩小 IGWEB CSS 样式表

首先,我们传输客户端应用:

$ cd $IGWEB_APP_ROOT/client
$ $IGWEB_APP_ROOT/scripts/clear_gopherjs_cache.sh
$ gopherjs build --verbose -m --tags clientonly -o  $IGWEB_APP_ROOT/static/js/client.min.js

其次,我们必须生成静态资产包:

$ $IGWEB_APP_ROOT/igweb --generate-static-assets
Generating static assets...Done

准备部署包的第三步,也是最后一步,包括缩小 CSS 样式表。

首先,我们需要通过发出以下命令来安装基于 Go 的 minifier:

$ go get -u github.com/tdewolff/minify/cmd/minify
$ go install github.com/tdewolff/minify

现在,我们可以缩小 IGWEB 的 CSS 样式表:

$ minify --mime="text/css" $IGWEB_APP_ROOT/static/css/igweb.css > $IGWEB_APP_ROOT/static/css/igweb.min.css

有了这些项目,我们现在准备创建一个部署包,一个 tarball,它包括igwebLinux 二进制文件和static文件夹。我们通过发出以下命令来创建 tarball:

$ cd $IGWEB_APP_ROOT
$ tar zcvf /tmp/bundle.tgz builds/igweb-linux64 static

我们将使用scp命令将捆绑包发送到远程服务器:

$ scp /tmp/bundle.tgz igweb@targetserver:/tmp/.

scp命令将 tarballbundle.tgz复制到主机名为targetserver的服务器上的/tmp目录。现在将部署包放在服务器上,是时候启动并运行igweb

部署捆绑包并启动 IGWEB

我们将已安全复制到/tmp文件夹的模板包移动到~/igweb文件夹中,并提取 tarball 的内容:

 $ cd ~/igweb
 $ mv /tmp/bundle.tgz .
 $ tar zxvf bundle.tgz

提取完bundle.tgztarball 的内容后,通过发出rm命令删除 tarball 文件。

$ rm bundle.tgz

我们可以使用mv命令将二进制文件重命名回igweb

$ mv igweb-linux64 igweb

我们在本地机器中的二进制文件名上加上了[T0],以便我们能够将其与其他操作系统/架构组合的构建区分开来。

此时,我们已将捆绑包部署到生产服务器。现在是运行igweb的时候了。

运行 IGWEB

在运行igweb可执行文件之前,我们必须在生产服务器上设置$IGWEB_APP_ROOT$IGWEB_MODE环境变量:

 $ export IGWEB_APP_ROOT=/home/igweb/igweb
 $ export IGWEB_MODE=production

设置$IGWEB_APP_ROOT环境变量允许igweb应用知道指定的igweb目录,该目录将包含依赖资源,例如静态资产。

$IGWEB_MODE环境变量设置为production允许我们在生产模式下运行igweb应用。

您应该在igweb用户的.bashrc配置文件中为这两个环境变量添加条目:

export IGWEB_APP_ROOT=/home/igweb/igweb
export IGWEB_MODE=production

您可以注销并重新登录生产服务器,以使对.bashrc所做的更改生效。

在前台运行 IGWEB

让我们启动igwebweb 服务器实例:

$ cd $IGWEB_APP_ROOT
$ ./igweb

图 11.4显示了 IGWEB 在地址的独立服务器实例上运行的屏幕截图 http://igweb.kamesh.com

图 11.4:IGWEB 在独立服务器实例上运行

当我们点击Ctrl+C组合键退出igweb程序时,我们的 web 服务器实例由于一直在前台运行而陷入了一个艰难的停顿。对于任何客户端请求,NGINX 都将返回 502 坏网关服务器错误。我们需要一种方法来后台监控igweb以便它在后台运行。

在后台运行 IGWEB

igwebweb 服务器实例可以使用nohup命令在后台运行:

$ nohup ./igweb 2>&1 &

nohup命令用于在当前会话终止后继续运行igweb程序。在类 Unix 系统上,2>&1构造意味着将标准错误(stderr重定向到与标准输出(stdout相同的位置。通过跟踪/var/log/syslog文件,可以查看igweb程序的日志消息。最后,命令中的最后一个符号[T7]表示在后台运行程序。

我们可以先获取PID(进程 ID)停止igweb进程:

$ ps -ef | grep igweb | grep -v grep

在运行此命令返回的输出中,PID 值将恰好位于可执行文件名称igweb的旁边。一旦我们确定了进程的 PID,我们可以通过使用kill命令终止igweb进程并指定 PID 值来停止igweb进程:

$ kill PID

请注意,我们在前面的kill命令中放置了名称PID,仅用于说明目的。您必须向kill命令提供运行ps命令返回的 PID 数值。

用 systemd 运行 IGWEB

这种运行igweb的方法暂时有效,但如果服务器重新启动怎么办?我们需要一种使igweb计划更具弹性的方法。一旦服务器恢复在线,它必须能够再次启动,而nohup不是实现这一目标的合适选择。

我们真正需要的是一种将igweb转化为系统服务的方法。我们可以通过sysytemd来实现这一点,这是一个 init 系统,可用于 Ubuntu 16.04 LTS。通过systemd,我们可以初始化、管理和跟踪系统服务。它可以在系统启动或运行时使用。

您需要以root用户的身份运行以下命令,因为您需要root才能添加新的系统服务。

为了将igweb转化为服务,我们创建了一个名为igweb.service的单元文件,并将其放在/etc/systemd/system目录中。以下是单位文件的内容:

[Unit]
Description=IGWEB

[Service]
USER=igweb
GROUP=igweb
Environment=IGWEB_APP_ROOT=/home/igweb/igweb
Environment=IGWEB_MODE=production
WorkingDirectory=/home/igweb/igweb
ExecStart=/home/igweb/igweb/igweb
Restart=always

[Install]
WantedBy=multi-user.target

指定文件扩展名.service表示我们正在创建一个服务单元,描述如何管理服务器上的应用。这包括执行操作,例如启动或停止服务,以及是否应在系统启动时启动服务。

单元文件分为多个部分,每个部分的开头用一对方括号*【表示,括号中包含部分名称。*

*单元文件中的节名称区分大小写!

第一部分是[Unit]部分。这用于定义单元的元数据以及该单元与其他单元的关系。在[Unit]部分中,我们为Description指定了一个值,用于描述机组名称。例如,我们运行以下命令:

$ systemctl status nginx

当我们运行它时,我们看到的nginx描述是使用Description指令指定的描述。

[Service]部分用于指定服务的配置。USERGROUP指令指定命令应该作为什么用户和组运行。我们使用Environment指令设置$IGWEB_APP_ROOT环境变量,然后再次使用它设置$IGWEB_MODE环境变量。

WorkingDirectory指令为执行的命令设置工作目录。ExecStart指令指定要执行的命令的完整路径;在本例中,我们提供了igweb可执行文件的完整路径。

Restart指令用于指定systemd将尝试重新启动服务的情况。通过提供一个值always,我们已经指定服务应该一直在运行,如果由于某种原因停止,它应该重新启动。

我们定义的最后一部分是[Install]部分。本节允许我们指定单元启用或禁用时的行为。

本节中声明的WantedBy指令告诉systemd应该如何启用一个单元,也就是说,启用时服务应该在哪个系统运行级别运行。通过将此指令的值设置为multi-user.target,我们指定此服务的系统运行级别为 3(多用户模式)。

每当我们引入新的systemd服务脚本或对现有脚本进行更改时,我们都必须重新加载systemd守护进程。我们可以通过发出以下命令来执行此操作:

$ systemctl daemon-reload

我们可以通过发出以下命令指定,igweb服务在引导时自动启动:

$ systemctl enable igweb

如果我们不希望igweb服务在引导时自动启动,我们可以发出以下命令:

$ systemctl disable igweb

我们可以通过发出以下命令来启动igweb服务:

$ systemctl start igweb

我们可以通过发出以下命令来停止igweb服务:

$ systemctl stop igweb

我们现在已经完成了igweb的独立部署。令人惊讶的是,我们可以运行igweb应用,而不必在目标生产系统上安装 Go。

然而,对于负责保持 IGWEB 正常运行的 DevOps 团队来说,这种方法相当不透明。我所说的不透明的意思是,DevOps 工程师无法通过检查静态二进制可执行文件和一堆静态资产来确定多少。

我们需要的是一种更加精简的方式来部署 IGWEB,这个过程向我们展示了从头开始启动igweb实例所需的所有依赖关系。为了实现这一目标,我们需要对 IGWEB 进行 dockerize。

使用 Docker 部署同构 Go web 应用

本节概述了如何将igweb部署为 Linode 云上的多容器 Docker 应用。Docker 是一种技术和平台,允许我们在一台机器上运行和管理多个 Docker 容器。您可以将 Docker 容器视为一个模块化的轻量级虚拟机。我们可以将igweb等应用打包为 Docker 容器,使其立即可移植。应用保证在容器中以相同的方式运行,而不管它在哪个环境中运行。

您可以通过以下链接了解有关 Docker 的更多信息:https://www.docker.com

大多数云提供商都支持 Docker,使其成为基于云部署的非常方便的工具。正如您将在本章后面看到的,在 Linode 云上部署多容器 Docker 应用相对容易。

安装 Docker

在生产系统上安装 Docker 之前,我们首先需要安装一些先决条件:

$ sudo apt-get install dmsetup && dmsetup mknodes

现在,我们可以发出以下命令来安装 Docker:

$ sudo apt-get install docker-ce

要验证 Docker 是否已正确安装在生产系统上,可以发出以下命令:

$ docker --version
Docker version 17.09.0-ce, build afdb6d4

运行该命令后,您应该会看到安装的 Docker 版本。

对接 IGWEB

dockerizeigweb的过程首先涉及创建一个Dockerfile,该文件指定如何创建 Docker 映像的说明。Docker 映像将用于创建 Docker 容器。

创建 Dockerfile 后,我们将使用docker-compose工具定义并运行 IGWEB 网站所需的多个容器。

igweb部署为多容器 Docker 应用需要三个步骤:

  1. 创建一个可以创建 IGWEB docker 映像的Dockerfile
  2. docker-compose.yml文件中定义组成 IGWEB 的服务
  3. 运行docker-compose up启动多容器应用

码头工人

Dockerfile描述了igwebdocker 图像应该由什么组成。该文件位于deployments-config/docker-single-setup文件夹中。让我们来看看Dockerfile是如何工作的。

FROM指令指定从中派生当前图像的基本父图像:

FROM golang

在这里,我们已经指定将使用基本[T0]docker 映像。

有关golangdocker 图像的更多信息,请访问https://hub.docker.com/_/golang/

MAINTAINER说明指定了Dockerfile维护人员的姓名及其电子邮件地址:

MAINTAINER Kamesh Balasubramanian kamesh@kamesh.com

我们指定了一组ENV指令,允许我们定义和设置所有必需的环境变量:

ENV IGWEB_APP_ROOT=/go/src/github.com/EngineerKamesh/igb/igweb
ENV IGWEB_DB_CONNECTION_STRING="database:6379"
ENV IGWEB_MODE=production
ENV GOPATH=/go

为了正确操作igweb应用,我们设置了$IGWEB_APP_ROOT$IGWEB_DB_CONNECTION$IGWEB_MODE$GOPATH环境变量。

在此块中,我们使用RUN指令获取igweb应用所需的 Go 包:

RUN go get -u github.com/gopherjs/gopherjs
RUN go get -u honnef.co/go/js/dom
RUN go get -u -d -tags=js github.com/gopherjs/jsbuiltin
RUN go get -u honnef.co/go/js/xhr
RUN go get -u github.com/gopherjs/websocket
RUN go get -u github.com/tdewolff/minify/cmd/minify
RUN go get -u github.com/isomorphicgo/isokit 
RUN go get -u github.com/uxtoolkit/cog
RUN go get -u github.com/EngineerKamesh/igb

这基本上是启动和运行igweb所需的 Go 包列表

以下RUN命令安装基于 Go 的 CSS/JavaScript 迷你程序:

RUN go install github.com/tdewolff/minify

我们使用另一条RUN指令传输客户端 Go 程序:

RUN cd $IGWEB_APP_ROOT/client; go get ./..; /go/bin/gopherjs build -m --verbose --tags clientonly -o $IGWEB_APP_ROOT/static/js/client.min.js

此命令实际上是三个连续命令的组合,其中每个命令用分号分隔。

第一个命令将目录更改为$IGWEB_APP_ROOT/client目录。在第二个命令中,我们获取当前目录和所有子目录中剩余的所有必需 Go 包。第三个命令将 Go 代码传输到一个简化的 JavaScript 源文件client.min.js,该文件位于$IGWEB_APP_ROOT/static/js目录中。

下一条RUN指令构建并安装服务器端 Go 程序:

>RUN go install github.com/EngineerKamesh/igb/igweb

注意,go install命令不仅会通过执行构建操作生成igweb二进制可执行文件,还会将生成的可执行文件移动到$GOPATH/bin

我们发布以下RUN指令生成静态资产:

RUN /go/bin/igweb --generate-static-assets

RUN说明缩小了 IGWEB 的 CSS 样式表:

RUN /go/bin/minify --mime="text/css" $IGWEB_APP_ROOT/static/css/igweb.css > $IGWEB_APP_ROOT/static/css/igweb.min.css

ENTRYPOINT指令允许我们设置容器的主命令:

# Specify the entrypoint
ENTRYPOINT /go/bin/igweb

这使我们能够像运行命令一样运行映像。我们已经将ENTRYPOINT设置为igweb可执行文件的路径:/go/bin/igweb

我们使用EXPOSE指令通知 Docker 容器在运行时应监听的网络端口:

EXPOSE 8080

我们已经暴露了集装箱的港口8080

除了能够使用Dockerfile构建 docker 图像外,此文件最重要的优点之一是它传达了意义和意图。可以将其视为一流的项目配置工件,以准确理解构建由服务器端igweb应用和客户端应用client.min.js组成的同构 web 应用的过程。通过查看Dockerfile,DevOps 工程师可以轻松确定从头成功构建整个同构 web 应用的过程。

封闭源项目的 Dockerfile

我们介绍的Dockerfile对于开源项目非常有用,但是如果您的同构 Go 项目是封闭源代码的,您会怎么做?您如何仍然能够利用在云中运行 Docker 的优势,同时确保源代码的安全性?我们需要对Dockerfile做一些细微的修改,以考虑封闭源代码项目。

让我们考虑一个场景,即 Type T0.代码分发是封闭源代码。让我们假设我们无法使用go get命令获得它。

我们还假设您已经创建了封闭源代码igweb项目的 tarball 包,包括项目目录根目录下的封闭源代码友好Dockerfile。您已将 tarball 从本地计算机安全复制到目标计算机,并已提取 tarball。

以下是我们需要对Dockerfile进行的更改。首先,我们使用go get命令注释掉获取igb分布的相应RUN指令:

# Get the required Go packages
RUN go get -u github.com/gopherjs/gopherjs
RUN go get -u honnef.co/go/js/dom
RUN go get -u -d -tags=js github.com/gopherjs/jsbuiltin
RUN go get -u honnef.co/go/js/xhr
RUN go get -u github.com/gopherjs/websocket
RUN go get -u github.com/tdewolff/minify/cmd/minify
RUN go get -u github.com/isomorphicgo/isokit 
RUN go get -u github.com/uxtoolkit/cog
# RUN go get -u github.com/EngineerKamesh/igb

RUN指令集之后,我们立即引入COPY指令:

COPY . $IGWEB_APP_ROOT/.

COPY指令将递归地将当前目录中的所有文件和文件夹复制到$IGWEB_APP_ROOT/.指定的目标。就这些。

现在,我们已经深入了解了 IGWEB 的Dockerfile解剖结构,我们必须承认igwebweb 服务器实例本身无法为 IGWEB 网站提供服务。它具有某些我们必须考虑的服务依赖性,例如用于数据持久性需求的 Redis 数据库和用于以合理的 Gzip 方式服务大量静态资产的 NGINX 反向代理。

我们需要的是 Redis 的 Docker 容器和 NGINX 的另一个 Docker 容器。igweb被证明是一个多容器 Docker 应用。是时候把我们的重点转向docker-compose,这是一个定义和运行多容器应用的便捷工具。

Docker compose

docker-compose工具允许我们定义一个多容器 Docker 应用,并使用单个命令docker-compose up运行它。

docker-compose的工作原理是读取一个docker-compose.yml文件,该文件包含特定的指令,这些指令不仅描述了应用中的容器,还描述了它们各自的依赖关系。让我们检查多容器igweb应用的docker-compose.yml文件的每个部分。

在文件的第一行中,我们指出我们将使用 Docker Compose 配置文件格式的版本 2:

version: '2'

我们在services部分声明应用的服务。每个服务(以粗体显示)都有一个名称,以指示其在多容器应用中的角色:

services:
  database:
    image: "redis"
  webapp:
    depends_on:
        - database 
    build: .
    ports:
        - "8080:8080"
  reverseproxy:
    depends_on:
        - webapp
    image: "nginx"
    volumes:
   - ./deployments-config/docker-single setup/nginx.conf:/etc/nginx/nginx.conf
    ports:
        - "80:80"

我们已经定义了一个名为database的服务,它将是 Redis 数据库实例的容器。我们将 image 选项设置为redis以告知docker-compose基于 Redis 映像运行容器。

紧接着,我们定义了一个名为webapp的服务,它将是igweb应用的容器。我们使用depends_on选项明确说明webapp服务需要database服务才能运行。如果不启动database服务,webapp服务将无法启动。

我们指定build选项,告诉docker-compose在指定路径中基于Dockerfile构建图像。通过指定.的相对路径,我们指出当前目录中存在的Dockerfile应该用于构建基础映像。

我们为ports部分指定一个8080:8080(主机:容器)值,以表示我们希望在主机上打开端口8080,并将连接转发到 Docker 容器的端口8080

我们已经定义了名为reverseproxy的服务,它将是nginx反向代理服务器的容器。我们将depends_on选项设置为webapp,表示在webapp服务未启动的情况下无法启动reverseproxy服务。我们已经将 image 选项设置为nginx以告知docker-compose基于nginx图像运行容器。

volumes部分中,我们可以以 HOST:CONTAINER 的形式定义装载路径。我们已经定义了一个装载路径,在该路径中,我们将位于./deployments-config/docker-single-setup目录中的nginx配置文件nginx.conf装载到容器内的/etc/nginx/nginx.conf路径。

由于reverseproxy服务将为 HTTP 客户端请求提供服务,因此我们为ports部分指定了一个80:80值,表示我们希望在主机上打开端口80(默认 HTTP 端口),并将连接转发到 Docker 容器的端口80

现在我们已经浏览了 Docker Compose 配置文件,是时候使用docker-compose up命令启动igweb作为多容器 Docker 应用了。

运行 Docker compose

我们发出以下命令来构建服务:

$ docker-compose build

以下是运行docker-compose build命令的输出(为了简洁起见,部分输出被省略):

database uses an image, skipping
Building webapp
Step 1/22 : FROM golang
 ---> 99e596fc807e
Step 2/22 : MAINTAINER Kamesh Balasubramanian kamesh@kamesh.com
 ---> Running in 107a99d5c4ee
 ---> 6facac83509e
Removing intermediate container 107a99d5c4ee
Step 3/22 : ENV IGWEB_APP_ROOT /go/src/github.com/EngineerKamesh/igb/igweb
 ---> Running in f009d8391fc4
 ---> ec1b1d15c6c3
Removing intermediate container f009d8391fc4
Step 4/22 : ENV IGWEB_DB_CONNECTION_STRING "database:6379"
 ---> Running in 2af5e98c71e2
 ---> 6748f0f5bc4d
Removing intermediate container 2af5e98c71e2
Step 5/22 : ENV IGWEB_MODE production
 ---> Running in 1a87b871f761
 ---> 9871fc511e80
Removing intermediate container 1a87b871f761
Step 6/22 : ENV GOPATH /go
 ---> Running in c6c2eff0ded2
 ---> 4dc456357dc9
Removing intermediate container c6c2eff0ded2
Step 7/22 : RUN go get -u github.com/gopherjs/gopherjs
 ---> Running in c8996108bd96
 ---> 6ae68fb84178
Removing intermediate container c8996108bd96
Step 8/22 : RUN go get -u honnef.co/go/js/dom
 ---> Running in a1ad103c4c10
 ---> abd1f7f3b8b7
Removing intermediate container a1ad103c4c10
Step 9/22 : RUN go get -u -d -tags=js github.com/gopherjs/jsbuiltin
 ---> Running in d7dc4ec21ee1
 ---> cd5829fb609f
Removing intermediate container d7dc4ec21ee1
Step 10/22 : RUN go get -u honnef.co/go/js/xhr
 ---> Running in b4e88d0233fb
 ---> 3fe4d470799e
Removing intermediate container b4e88d0233fb
Step 11/22 : RUN go get -u github.com/gopherjs/websocket
 ---> Running in 9cebc021cb34
 ---> 20cd1c09d6cd
Removing intermediate container 9cebc021cb34
Step 12/22 : RUN go get -u github.com/tdewolff/minify/cmd/minify
 ---> Running in 9875889cc267
 ---> 3c60c2de51b0
Removing intermediate container 9875889cc267
Step 13/22 : RUN go get -u github.com/isomorphicgo/isokit
 ---> Running in eb839d91588e
 ---> e952d6e6cbe2
Removing intermediate container eb839d91588e
Step 14/22 : RUN go get -u github.com/uxtoolkit/cog
 ---> Running in 3e6853ff7196
 ---> 3b00f78e5acf
Removing intermediate container 3e6853ff7196
Step 15/22 : RUN go get -u github.com/EngineerKamesh/igb
 ---> Running in f5082861ca8a
 ---> 93506a92526c
Removing intermediate container f5082861ca8a
Step 16/22 : RUN go install github.com/tdewolff/minify
 ---> Running in b0a72d9e9807
 ---> e3e49d9c2898
Removing intermediate container b0a72d9e9807
Step 17/22 : RUN cd $IGWEB_APP_ROOT/client; go get ./..; /go/bin/gopherjs build -m --verbose --tags clientonly -o $IGWEB_APP_ROOT/static/js/client.min.js
 ---> Running in 6f6684209cfd
Step 18/22 : RUN go install github.com/EngineerKamesh/igb/igweb
 ---> Running in 17ed6a871db7
 ---> 103f12e38c04
Removing intermediate container 17ed6a871db7
Step 19/22 : RUN /go/bin/igweb --generate-static-assets
 ---> Running in d6fb5ff48a08
Generating static assets...Done
 ---> cc7434fbb94d
Removing intermediate container d6fb5ff48a08
Step 20/22 : RUN /go/bin/minify --mime="text/css" $IGWEB_APP_ROOT/static/css/igweb.css > $IGWEB_APP_ROOT/static/css/igweb.min.css
 ---> Running in e1920eb49cc2
 ---> adbf78450b9c
Removing intermediate container e1920eb49cc2
Step 21/22 : ENTRYPOINT /go/bin/igweb
 ---> Running in 20246e214462
 ---> a5f1d978060d
Removing intermediate container 20246e214462
Step 22/22 : EXPOSE 8080
 ---> Running in 6e12e970dfe2
 ---> 4c7f474b2704
Removing intermediate container 6e12e970dfe2
Successfully built 4c7f474b2704
reverseproxy uses an image, skipping

构建完成后,我们可以通过发出以下命令继续运行多容器igweb应用:

$ docker-compose up

图 11.5是 IGWEB 作为多容器应用运行的屏幕截图:

图 11.5:IGWEB 作为多容器应用运行

当我们运行docker-compose up命令时,该命令为我们提供所有运行容器中活动的实时输出。要退出程序,您可以使用Ctrl+C组合键。请注意,这将终止docker-compose程序,该程序将以优雅的方式关闭正在运行的容器。

或者,在启动多容器igweb应用时,您可以指定-d选项以分离模式运行,这将在后台运行容器,如下所示:

$ docker-compose up -d

如果希望关闭多容器应用,可以发出以下命令:

$ docker-compose down

如果您对Dockerfiledocker-compose.yml文件进行进一步更改,则必须再次运行docker-compose build命令以重建服务:

$ docker-compose build

拥有docker-compose up -d功能在后台运行集装箱是很方便的,但现在,我们知道最好将我们的多集装箱 Docker 应用转变为systemd服务。

设置停靠的 IGWEB 服务

为停靠的igweb设置systemd服务非常简单。以下是igweb-docker.service文件的内容,应该放在生产系统的/etc/systemd/system目录中:

[Unit]
Description=Dockerized IGWEB
After=docker.service
Requires=docker.service

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/igb/igweb
ExecStart=/usr/bin/docker-compose -f /opt/igb/igweb/docker-compose.yml up -d
ExecStop=/usr/bin/docker-compose -f /opt/igb/igweb/docker-compose.yml down

[Install]
WantedBy=multi-user.target

[Unit]部分中,我们将After指令的值设置为docker.service。这表明docker单元必须在igweb-docker单元之前启动。Requires指令也设置了值docker.service。这表明igweb-docker单元依赖docker单元成功运行。未能启动docker装置将导致无法启动igweb-docker装置。

[Service]部分,我们将Type指令设置为oneshot。这表明我们正在启动的可执行文件是短期的。使用它是有意义的,因为我们将使用指定的-d标志(分离模式)运行docker-compose up,以便容器在后台运行。

我们已将RemainAfterExit指令与Type指令一起指定。通过将RemainAfterExit指令设置为yes,我们表明即使在docker-compose进程退出之后,igweb-docker服务也应被视为处于活动状态。

使用ExecStart指令,我们以分离模式启动docker-compose进程。我们已经指定了ExecStop指令来指示停止服务所需的命令。

[Install]部分中,通过将WantedBy指令的值设置为multi-user.target,我们指定此服务的系统运行级别为 3(多用户模式)。

回想一下,将igweb-docker.service文件放入/etc/systemd/system目录后,我们必须重新加载systemd守护进程,如下所示:

$ systemctl daemon-reload

现在,我们可以启动停靠的igweb应用:

$ systemctl start igweb-docker

您可以使用systemctl enable命令指定在系统启动时启动igweb-docker

我们可以通过发出以下命令来关闭服务:

$ systemctl stop igweb-docker

现在,我们已经演示了如何将igweb应用作为托管在 Linode 云上的多容器 Docker 应用运行。同样,尽管我们使用的是 Linode,但我们已经演示的过程可以在您选择的首选云提供商上复制。

总结

在本章中,我们学习了如何将同构 web 应用部署到云中。我们介绍了igweb服务器端应用如何在生产模式下运行,并向您展示了应用如何包含外部 CSS 和 JavaScript 源文件。我们还向您展示了如何控制 GopherJS 生成的 JavaScript 程序的文件大小。我们向您展示了如何为应用的模板包生成静态资产,以及将由部署的 COG 使用的 JavaScript 和 CSS。

我们首先考虑将同构 web 应用部署到独立服务器。这包括向服务器添加一个igweb用户,设置redis-server实例,将nginx设置为启用 GZIP 压缩的反向代理,并设置igweb根文件夹。我们还向您展示了如何将 Go 代码从开发系统(64 位 macOS)交叉编译到生产系统(64 位 Linux)上运行的操作系统。我们指导您完成了准备部署捆绑包的过程,然后将捆绑包部署到生产系统。最后,我们向您展示了如何将igweb设置为systemd服务,以便在系统启动时轻松启动、停止、重新启动和自动启动。

然后,我们将注意力集中在同构 web 应用作为多容器 Docker 应用的部署上。我们向您展示了如何在生产系统上安装 Docker。我们向您介绍了停靠igweb的过程,包括创建Dockerfile,在docker-compose.yml文件中定义组成 IGWEB 的服务,以及运行docker-compose up命令以启动 IGWEB 作为多容器停靠应用。最后,我们向您展示了如何设置igweb-docker systemd脚本以将igweb作为系统服务进行管理。*