跳转至

第35章:生产关注点和Redis

编程中最令人兴奋的部分之一是与世界分享你所创造的东西。对于网络应用,这通常意味着将你的项目部署到可以通过互联网访问的服务器上。

网络服务器可以是数据中心的专用机器,也可以是云端的容器,甚至可以是放在衣柜里的Raspberry Pi。只要你的服务器能够运行Swift,并且能够连接到互联网,你就可以用它来部署Vapor应用程序。

在本章中,你将了解Vapor的一些常见部署方法的优缺点。你还将学习如何正确优化、配置和监控你的应用程序,以提高效率和正常运行时间。

使用环境

每个Application的实例都有一个相关的Environment。每个环境都有一个String名称。常见的环境包括:production, development, 和testing. 你可以从Applicationenvironment属性中获取当前环境。

print(req.application.environment) // "production"

在大多数情况下,环境是供你在配置你的应用程序时随意使用的。

然而,当在发布环境中运行时,Vapor的某些部分会有不同的表现。一些区别包括在500错误中隐藏调试信息和减少错误日志的冗长性。

正因为如此,当你在生产中运行你的应用程序时,请确保你使用production环境。

选择一个环境

大多数模板包括代码,用于在应用程序运行时检测当前环境。如果你在项目的Run模块中打开main.swift,你会看到与下面类似的内容:

import App
import Vapor

var env = try Environment.detect()
try LoggingSystem.bootstrap(from: &env)
let app = Application(env)
defer { app.shutdown() }
try configure(app)
try app.run()

这段代码调用Environment.detect(),它解析传递给你的应用程序的命令行参数并返回指定的环境。如果你没有指定环境,Vapor默认使用development。你可以使用--env标志和环境名称来指定环境。

你可以在使用swift run从命令行运行你的应用程序的可执行文件时这样做。

swift run Run serve --env development

你也可以在Xcode中使用方案编辑器运行你的应用程序时指定环境。

img

Vapor支持prod表示productiondev表示development的快捷键。它还支持-e的缩写,即--env的缩写。

$ swift run Run serve -e prod

用优化的方式进行编译

在开发你的应用程序时,你通常会使用Swift的调试构建模式编译代码。调试构建模式速度快,并且在生成的二进制文件中包含有用的调试信息。Xcode可以在以后使用这些信息来提供更多关于致命错误和断点调试的信息。

对于生产部署,你应该使用Swift的发布构建模式。在发布模式下构建时,Swift会花更多时间分析和优化您的程序。虽然这增加了整体的构建时间,但它在运行时的性能改进是非常值得的。Swift还从生成的二进制文件中删除了调试信息,使其更小。

VaporSwift NIO在发布构建模式下的行为也可能略有不同。这些软件包的一个常见模式是,在调试模式下,将可恢复的开发者错误转换成致命错误。这有助于开发人员在开发过程中快速追踪常见的错误,而不影响生产中的稳定性。

本节向您展示了如何在Xcode和直接使用SwiftPM启用发布构建模式。它还告诉你如何在发布模式下运行你的测试。这对依赖运行时性能的测试很有用。

Xcode中构建发布模式

您可以使用方案编辑器在Xcode中启用发布构建模式。要在发布模式下构建,请为您的应用程序的可执行目标编辑方案。然后,在Build Configuration下选择Release

img

要在发布模式下进行测试,再次为你的应用程序的可执行目标编辑方案。然后,从方案编辑器的左侧选择Test,将Build Configuration模式改为Release

使用SwiftPM构建发布版

当部署到Linux时,由于Xcode不可用,你需要使用SwiftPM来编译发布可执行文件。默认情况下,SwiftPM以调试构建模式进行编译。要指定发布模式,请在您的构建命令中添加-c release

swift build -c release

当构建完成后,编译器会将生成的可执行文件的路径打印到终端。你可以复制并粘贴该路径来运行你的应用程序。

如果你访问构建文件夹,你可能会注意到与你的可执行二进制文件一起存在的其他文件。这些文件中包括由构建过程产生的任何共享库(macOS上的.dylibLinux上的.so)。这些共享库是你的可执行文件运行所需要的。

你也可以用SwiftPM在发布模式下运行你的测试。

swift test -c release

Note

有些功能,如@testable import,在发布模式下测试时可能无法使用。

关于测试的说明

在类似生产环境中定期构建和测试你的代码,对于尽早发现问题非常重要。你将使用的一些模块,如Foundation,根据平台的不同有不同的实现。实现上的细微差别会导致你的代码出现错误。有时,一个API的实现可能在一个平台上还不存在。像Docker这样的容器环境可以帮助你解决这个问题,使你可以很容易地在不同于主机的平台上测试你的代码,比如在Linux上测试,而在macOS上开发。

使用Docker

Docker是一个测试和部署Vapor应用程序的伟大工具。部署步骤被编码为Dockerfile,你可以将其与你的项目一起提交到源代码控制。你可以执行这个Dockerfile来构建和运行你的应用程序的实例,在本地进行测试,或在你的部署服务器上进行生产。这样做的好处是,可以很容易地测试部署,创建新的部署,并跟踪你的代码部署方式的变化。

参见第33章"使用Docker进行部署",了解更多信息。

进程监控

要运行一个Vapor应用程序,你只需要启动SwiftPM生成的可执行文件。

swift build -c release
.build/release/Run serve -e prod

虽然这对测试很有效,但它有一个主要问题:如果你的应用程序崩溃了怎么办?在这种情况下,你将需要登录到你的服务器并手动重启它。幸运的是,进程监控器可以帮助解决这个问题。

Supervisor

Supervisor,也叫supervisord,是一个流行的Linux进程监视器。这个程序允许你注册你想启动和停止的进程。如果其中一个进程崩溃了,Supervisor会自动为你重新启动它。它还可以方便地将进程的stdoutstderr存储在/var/log中,以便于访问。

Supervisor通常在Ubuntu上使用APT安装,但可能因你的部署方式不同而不同。

apt-get install supervisor

一旦安装完毕,Supervisor就可以通过Ubuntusystemctl命令来启动。

systemctl restart supervisor

Supervisor的配置文件存储在/etc/supervisor/conf.d。在那里创建一个新的文件来管理你的Vapor应用程序,称为my-app.conf

// 1
[program:my-app]
command=/path/to/my-app/.build/release/Run serve -e prod
// 2
autostart=true
autorestart=true
// 3
stderr_logfile=/var/log/my-app.err.log
stdout_logfile=/var/log/my-app.out.log

下面是这个配置文件的具体内容:

  1. 声明一个新的Supervisor程序,使用serve命令和生产环境启动你的应用程序的Run可执行程序。
  2. 启用自动启动和自动重启,确保你的应用程序在服务器开启时始终运行。
  3. 配置Supervisor,使你的应用程序的stderrstdout指向日志文件。

现在你已经添加了配置文件,运行以下命令来更新Supervisor

supervisorctl reread
supervisorctl update

你的应用程序现在应该正在运行。如果应用程序崩溃了,Supervisor会注意到这一点,并立即尝试重新启动它。

Systemd

另一个不需要你安装额外软件的选择叫做systemd。它是Swift支持的Linux版本中的一个标准部分。关于如何使用systemd配置你的应用程序,请参见第34章,"使用AWS部署"。

反向代理

无论你在哪里或如何部署你的Vapor应用程序,在nginx这样的反向代理后面托管它通常是一个好主意。nginx是一个非常快的、经过战斗检验的、易于配置的HTTP服务器和代理。虽然Vapor支持直接提供HTTP请求,但在nginx后面的代理可以提供更高的性能、安全性和易用性。

安装Nginx

nginx通常在Ubuntu上使用APT安装,但根据你的部署方式,可能会有所不同。

apt-get update
apt-get install nginx

安装完毕后,可以使用Ubuntusystemctl命令来启动nginx

systemctl start nginx
systemctl restart nginx
systemctl stop nginx

启动后,可以在/etc/nginx/sites-enabled创建一个新的站点配置。看一下下面的nginx配置文件的例子:

server {
  ## 1
  server_name hello.com;

  ## 2
  listen 80;

  ## 3
  root /home/vapor/Hello/Public/;
  try_files $uri @proxy;

  ## 4
  location @proxy {
    ## 5
    proxy_pass http://127.0.0.1:8080;

    ## 6
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    ## 7
    proxy_connect_timeout 3s;
    proxy_read_timeout 10s;
  }
}

下面是nginx配置的每一行的作用:

  1. 指定此配置用于对hello.com的请求。你可以在这里列出多个服务器名称。
  2. 指定此配置用于对80端口的请求,这是默认的HTTP端口。
  3. 为这个服务器指定一个文件根。任何对hello.com/*的请求,如果与这个文件夹中的文件名匹配,将由nginx直接提供服务,绕过你的Vapor应用程序。
  4. 指定该服务器应该是一个反向代理。
  5. 将所有请求传递给Vapor程序,该程序绑定在127.0.0.1端口8080
  6. 指定特殊的头文件,添加到传入的请求中。这些头信息有助于Vapor维护有关连接的客户端的信息。
  7. 为你的服务器指定连接和读取超时。

保存好配置文件后,重启nginx以启用新的网站。接下来,确保你的Vapor服务器在配置中指定的主机名和端口运行。现在你应该能够通过nginx访问你的Vapor服务器。

记录

在开发过程中使用Swiftprint方法来记录日志是非常好的,甚至可以成为一些生产用例的合适选择。像Supervisor这样的程序有助于将你的应用程序的打印输出汇总到服务器上的文件中,你可以根据需要访问。

然而,在某些情况下,你可能希望以不同的方式收集你的日志。例如,也许你更喜欢收集日志,并将它们发送到一个远程API进行存储。你可能还想指定每条日志的重要性,这样你就知道如何对待它。Vapor使用SwiftLog为你和你用来构建的所有包提供了一个一致的API

使用日志很容易;只需import Vapor并从你的RequestApplication中访问一个Logger

app.get("log-test") { req -> HTTPStatus in
    req.logger.info("The route was called")
    return .ok
}

记录器有几种可用的日志级别方法:

  • trace:记录任何和所有信息。用于追踪特定的问题。
  • debug:用于调试问题。
  • info:表示发生了一个不常见的事件。
  • notice:用于通知应该注意的特定事件或状态,但不作为错误处理。
  • warning:表明有些事情应该被修复。
  • error:表明有什么地方出了问题。
  • critical:致命的错误。必须取消执行。

默认情况下,访问一个Logger会产生一个ConsoleLogger,它将你的日志输出到控制台,使用终端颜色来指定日志级别。

不过,SwiftLog还有其他几种实现方式供你选择,可以在https://github.com/apple/swift-log#selecting-a-logging-backend-implementation-applications-only

横向可扩展性

最后,在设计一个生产就绪的应用程序时,最重要的问题之一是可扩展性。随着你的应用程序的用户群增长和流量增加,你将如何跟上需求?你的瓶颈会是什么?刚开始的时候,一个合理的解决方案是随着流量的增加而增加服务器的资源--增加内存、更好的CPU、更多的磁盘空间等等。这通常被称为scaling vertically

当你的应用程序的要求开始超过单台服务器的能力时,垂直扩展就会失败。最终,如果你的应用程序增长得足够大,你可能需要扩展到多个服务器。这就是所谓的horizontal scaling。然而,水平扩展不仅在你用尽了垂直扩展的能力时才有用。扩展到多个便宜的服务器可能比单个昂贵的服务器更有成本效益。

负载平衡

现在你了解了横向扩展的一些好处,你可能想知道它实际上是如何工作的。这个概念的关键是负载平衡器。负载平衡器是轻量级的、快速的程序,坐在你的应用程序的服务器前面。当一个新的请求进来时,负载平衡器选择你的一个服务器来发送这个请求。

如果其中一个服务器不健康--响应缓慢或返回错误--负载平衡器可以暂时停止向该服务器发送请求。

img

在上图中,负载均衡器收到来自客户端的消息,并决定将请求转发给应用程序#3。该应用为该请求生成一个响应,而负载均衡器将该响应送回给客户端。

虽然横向扩展的基础知识很简单,但你应该了解一些常见的陷阱,这些陷阱会阻止你的应用程序以这种方式进行扩展。最常见的是,这些问题与在服务器上本地存储信息有关。

为了更好地理解这一点,以下面这个将图片保存到磁盘的个人资料图片上传端点为例:

img

当客户AAPI上传其资料图片时,负载均衡器将请求引导到应用程序#2。这个应用程序处理请求并将图像保存到服务器的磁盘上。后来,当客户B试图获取该图像时,负载平衡器将请求引导到应用程序#3。运行应用程序#3的服务器不知道该图像,所以它返回一个错误。客户端B有可能被引导到应用程序#2,成功获取图像,但那纯粹是运气。

这个问题的其他常见例子是内存会话缓存和SQLite数据库。解决这个问题的一般方法是为你的应用程序的common数据使用共享存储。这意味着你的应用程序的任何实例可能需要访问的数据。如果这些数据对服务器来说是私有的--例如,一个API响应缓存--那么将其存储在本地是没有问题的。

有大量的工具供你使用,以使你的应用程序可扩展。对于文件上传,有像亚马逊网络服务的S3桶这样的API,让你从一个单一的远程来源存储和获取文件。你也可以将你的服务器配置成一个共享驱动器来存储文件,如下图所示:

img

在上面的例子中,客户B对图像的请求成功了,因为App #2App #3都可以访问同一个共享驱动器。对于数据库和会话,你可以使用非基于文件的数据库,如RedisMySQLPostgreSQLMongoDB等。这些数据库在一个单独的服务器上运行,你的所有应用程序实例都可以访问。

如果你认为你的应用程序需要处理大量的流量,或者它有可能快速增长,那么在你设计和编写代码时要记住水平扩展性。

使用Redis的会话

为了演示如何在一个应用程序中工作,请下载本章的启动项目。该项目是基于本书第一节中的TIL应用。在Xcode中打开该项目并构建应用程序。

Note

和前几章一样,你需要为该项目设置自定义工作目录。

当用户登录到网站时,应用程序将用户的ID存储在一个相关的会话中。目前,该应用程序将会话存储在内存中。这带来了一些问题。

  • 当你重新启动应用程序时,你会失去所有的会话。任何已登录的用户将不得不重新登录。
  • 如果你横向扩展你的应用程序,会话是不共享的。如果一个用户登录到服务器#1,而该用户的下一个请求到了服务器#2,它不知道这个会话,所以该用户不能访问任何受保护的路由。登录到2号服务器会覆盖1号服务器的会话信息,从而失去该会话。随着你的水平扩展,这导致问题的机会也会增加。

你可以通过将会话转移到数据库中来解决这个问题。Redis是一个快速的内存数据库,有很多用途,它是这个用例的最佳选择。如果应用程序的所有实例都使用Redis,它们可以共享会话。

Xcode中,打开configure.swift。启动项目已经将Redis配置为Package.swift中的一个依赖项。在import Leaf下面,添加以下内容:

import Redis

这允许你看到Redis的功能和类型。接下来,在你的应用程序中配置Redis数据库。在app.databases.use(..)下面添加以下内容:

// 1
let redisHostname = Environment
  .get("REDIS_HOSTNAME") ?? "localhost"
// 2
let redisConfig = 
  try RedisConfiguration(hostname: redisHostname)
// 3
app.redis.configuration = redisConfig

下面是这个的作用:

  1. 将主机名设置为REDIS_HOSTNAME环境变量,如果它存在的话。否则,默认为localhost。这允许你为托管方案注入主机名。
  2. 使用该主机名创建一个RedisConfiguration
  3. 在应用程序的Redis服务上设置RedisConfiguration

接下来,在app.views.use(.leaf)后面添加以下内容:

app.sessions.use(.redis)

这告诉应用程序在存储会话数据时要使用Redis

最后,将app.middleware.use(app.session.middleware)移到app.session.use(.redis)下面。这可以确保会话中间件使用Redis会话配置。

这就是在会话中使用Redis所需要的全部内容!

在终端,输入以下内容来启动数据库:

# 1
docker run --name postgres \
  -e POSTGRES_DB=vapor_database \
  -e POSTGRES_USER=vapor_username \
  -e POSTGRES_PASSWORD=vapor_password \
  -p 5432:5432 -d postgres
# 2
docker run --name redis -p 6379:6379 -d redis

下面是这个的作用:

  1. 启动PostgreSQL数据库。

  2. 运行一个名为postgres的新容器。

  3. 通过环境变量指定数据库的名称、用户名和密码。
  4. 允许应用程序连接到PostgreSQL服务器的默认端口:5432.
  5. 在后台作为一个守护程序运行服务器。
  6. 为这个容器使用名为postgresDocker镜像。如果你的机器上没有这个镜像,Docker会自动下载它。

  7. 启动Redis数据库:

  8. 运行一个名为redis的新容器。

  9. 允许应用程序连接到Redis服务器的默认端口:6379.
  10. 在后台作为一个守护程序运行服务器。
  11. 为这个容器使用名为redisDocker镜像。如果你的机器上没有这个镜像,Docker会自动下载它。

Xcode中构建并运行该应用程序。在你的浏览器中,导航到http://localhost:8080/。点击Create An Acronym,该应用程序会将你重定向到登录页面。用用户名admin和密码password登录。点击Create An Acronym,你就可以查看该页面:

img

Xcode中,停止并启动应用程序,在浏览器中刷新页面。应用程序知道你仍然在登录,因为它将会话存储在Redis而不是内存中。

接下来去哪?

你现在明白了将你的Swift网络应用程序转移到生产中时要避免的常见陷阱。现在是时候把这里列出的最佳实践和有用的工具用起来了。这里有一些额外的资源,在你继续磨练你的技能时,它们应该被证明是无价的: