跳转至

第36章:微服务,第一部分

在前面的章节中,你已经建立了一个单一的Vapor应用程序来运行你的服务器代码。对于大型应用来说,单一的单体变得难以维护和扩展。在本章中,你将学习如何利用微服务将你的代码分割成不同的应用。你将学习微服务的好处和坏处,以及如何与它们互动。最后,你将学习认证和关系如何在微服务架构中工作。

微服务

微服务是近几年来流行的一种设计模式。微服务的目的是提供小型的、独立的模块,它们之间相互作用。这与大型单体应用不同。这种方法使单个服务更容易开发和测试,因为它们更小。因为它们是独立的,你可以单独开发它们。这就不需要为整个应用程序使用和构建所有的依赖关系。

微服务还允许你更好地扩展你的应用程序。在一个单体应用中,当处于重载时,你必须对整个应用进行扩展。这包括应用程序中流量较少的部分。在微服务中,你只需要扩展那些繁忙的服务。

最后,微服务使构建和部署你的应用程序更容易。部署非常大的应用程序是复杂的,而且容易出错。在大型应用中,你必须与每个开发团队协调,以确保应用准备好部署。将一个单体应用分解成更小的服务,使每个服务的部署更容易。

每个微服务应该是一个完全包含的应用程序。每个服务都有自己的数据库、自己的缓存,必要时还有自己的前端。唯一共享的部分应该是公共API,允许其他服务与该微服务互动。通常,它们提供HTTP REST API,尽管你可以使用其他技术,如protobuf或远程过程性调用(RPC)。由于每个微服务只通过公共API与其他服务进行交互,所以每个服务可以使用不同的技术栈。例如,你可以在一个需要PostgreSQL的服务中使用它,但在主要用户服务中使用MySQL。你甚至可以混合语言。这允许不同的团队使用他们喜欢的语言。

Swift是微服务的绝佳选择。Swift应用程序的内存占用率低,可以处理大量的连接。这使得Swift微服务可以轻松地融入现有的应用程序,而不需要大量的资源。

TIL微服务

在本书的前几节,你开发了一个单一的TIL应用。你本可以使用一个微服务架构来代替。例如,你可以有一个处理用户的服务,另一个处理类别的服务,还有一个处理缩写的服务。在本章中,你将开始了解如何做到这一点。

下载并打开本章的启动项目。里面有两个Vapor应用程序。

  • TILAppUsers:一个运行在8081端口的用户微服务。这个服务使用PostgreSQL数据库来保存用户的信息。
  • TILAppAcronyms:运行在8082端口的缩略语的微服务。该服务使用MySQL数据库来存储首字母缩写。

用户微服务

在终端中导航到TILAppUsers目录。输入以下内容,启动数据库:

docker run --name postgres -e POSTGRES_DB=vapor_database \
  -e POSTGRES_USER=vapor_username \
  -e POSTGRES_PASSWORD=vapor_password \
  -p 5432:5432 -d postgres

以下是这样做的:

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

接下来在Xcode中生成并打开该项目:

open Package.swift

一旦Xcode完成了下载的依赖,打开User.swift。这个服务的User模型是主TIL应用程序的一个简化版本。

接下来,打开UsersController.swift。同样,像TIL应用程序一样,它包含创建用户、检索用户和检索所有用户的路由。

构建并运行该应用程序,并启动RESTed。配置一个请求,如下所示:

  • URL: http://localhost:8081/users
  • method: POST
  • Parameter encoding: JSON-encoded

添加三个带有名称和值的参数:

  • username: a username of your choice
  • name: a name of your choice
  • password: a password of your choice

点击Send Request。这将在应用程序中创建一个用户。

img

配置一个新的请求,如下所示:

  • URL: http://localhost:8081/users
  • method: GET

点击Send Request。你会看到你创建的用户:

img

就目前而言,用户服务需要多复杂就有多复杂!

缩写的微服务

保持用户服务运行,并在终端中导航到TILAppAcronyms目录。输入以下内容,启动数据库:

docker run --name mysql -e MYSQL_USER=vapor_username \
  -e MYSQL_PASSWORD=vapor_password \
  -e MYSQL_DATABASE=vapor_database \
  -e MYSQL_RANDOM_ROOT_PASSWORD=yes \
  -p 3306:3306 -d mysql

下面是这个的作用:

  • 运行一个名为mysql的新容器。
  • 通过环境变量指定数据库名称、用户名和密码。
  • 设置MYSQL_RANDOM_ROOT_PASSWORD,将所需的根密码设置为一个随机值。
  • 允许应用程序在其默认端口连接到MySQL服务器:3306.
  • 在后台作为一个守护程序运行服务器。
  • 为这个容器使用名为mysqlDocker镜像。如果该镜像在你的机器上不存在,Docker会自动下载它。

接下来在终端输入以下内容,在Xcode中打开该项目:

open Package.swift

这个服务包含与TIL主程序完全相同的Acronym模型。打开AcronymsController.swift。你会看到对Acronym进行CRUD操作的路由。当Xcode完成下载依赖项时,构建并运行该服务,并在RESTed中配置一个新的请求,如下所示:

  • URL: http://localhost:8082/
  • method: POST
  • Parameter encoding: JSON-encoded

添加三个带有名称和值的参数:

  • short: OMG
  • long: Oh My God
  • userID: The ID of the user created earlier

点击Send Request。这将在服务中创建一个首字母缩写:

img

配置一个新的请求,如下所示:

  • URL: http://localhost:8082/
  • method: GET

点击Send Request。你会看到你创建的首字母缩写:

img

处理好关系

在这一点上,你可以在各自的微服务中创建用户和缩略语。然而,处理不同服务之间的关系则更为复杂。在本书的第1节中,你学到了如何使用Fluent使你能够查询模型之间的不同关系。对于微服务,由于模型是在不同的数据库中,你必须手动完成这些。

获取用户的缩写词

TILAppAcronymsXcode项目中,打开AcronymsController.swift。在updateHandler(_:)下面,添加一个新的路由处理程序,以获得一个特定用户的首字母缩写:

func getUsersAcronyms(_ req: Request) 
  throws -> EventLoopFuture<[Acronym]> {
    // 1
    let userID = 
      try req.parameters.require("userID", as: UUID.self)
    // 2
    return Acronym.query(on: req.db)
      .filter(\.$userID == userID)
      .all()
}

以下是路由处理程序的工作:

  1. 从请求的参数中获取用户的ID作为UUID
  2. Acronym表进行查询,以获得所有userID与传入的ID匹配的缩写。

由于Acronym表包含用户ID,你不需要请求任何外部信息来执行查询。在boot(routes:)的末尾添加以下内容来注册路由:

routes.get("user", ":userID", use: getUsersAcronyms)

这将/user/<USER_ID>GET请求路由到getUsersAcronyms(_:)。建立并运行TILAppAcronyms服务,在RESTed中配置一个新的请求,如下所示:

  • URL: http://localhost:8082/user/
  • method: GET

点击Send request,你会看到该用户创建的所有首字母缩写:

img

获得一个首字母缩写的用户

你已经可以通过当前的项目获得一个首字母缩写的用户。你提出一个请求来获取缩写,从中提取用户的ID,然后提出请求从用户服务中获取该用户。第37章,"微服务,第二部分"讨论了如何为客户简化这一点。

微服务中的认证

目前,用户可以创建、编辑和删除缩略语,无需认证。像TIL应用一样,你应该在必要时为微服务添加认证。在本章中,你将为TILAppAcronyms微服务添加认证。然而,你将把这个认证委托给TILAppUsers微服务。

在实践中,它是这样工作的:

  • 一个用户登录到TILAppUsers微服务并获得一个令牌。
  • 当创建一个缩写时,用户向TILAppAcronyms服务提供该令牌。
  • TILAppAcronyms服务通过TILAppUsers服务验证该令牌。
  • 如果令牌是有效的,TILAppAcronyms将继续进行请求,否则将拒绝请求。

登录

Xcode中打开TILAppUsers项目。这个启动项目已经包含了一个Token类型和一个空的AuthContoller。你可以将令牌存储在与用户相同的数据库中。由于每个验证请求都需要查询,而且你有多个服务,所以你希望这个过程尽可能快。一个解决方案是将它们存储在内存中。然而,如果你想扩展你的微服务,这并不可行。你需要使用像Redis这样的东西。Redis是一个快速的键值数据库,它是存储会话令牌的理想选择。你可以在不同的服务器上共享数据库,这允许你在没有任何性能损失的情况下进行扩展。

在终端中,键入以下内容来启动Redis数据库服务器:

docker run --name redis -p 6379:6379 -d redis

下面是这个的作用:

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

回到Xcode中,打开TILAppUsers项目的configure.swift。在文件的顶部,在import Vapor下面添加以下内容:

import Redis

这允许你在你的应用程序中使用Redis。该项目已经将Redis配置为一个依赖项。接下来,找到下面内容:

app.migrations.add(CreateUser())

在它之后添加下面的内容:

// 1
let redisHostname: String
if let redisEnvironmentHostname = 
  Environment.get("REDIS_HOSTNAME") {
    redisHostname = redisEnvironmentHostname
} else {
  redisHostname = "localhost"
}
// 2
app.redis.configuration = 
  try RedisConfiguration(hostname: redisHostname)

以下是代码的作用:

  1. 使用REDIS_HOSTNAME环境变量作为Redis服务器的主机名,如果它被设置了。否则,使用localhost
  2. 配置应用程序的Redis设置以使用RedisConfiguration

现在你已经将TILAppUsers项目配置为使用Redis。注意该项目现在使用两个数据库--PostgreSQLRedis。接下来,打开AuthController.swift,在boot(routes:)下面创建一个新的路由处理程序来处理用户的登录:

func loginHandler(_ req: Request) 
  throws -> EventLoopFuture<Token> {
    // 1
    let user = try req.auth.require(User.self)
    // 2
    let token = try Token.generate(for: user)
    // 3
    return req.redis
      .set(RedisKey(token.tokenString), toJSON: token)
      .transform(to: token)
}

以下是新代码的作用:

  1. 从请求中获取认证的用户。路由将使用基本HTTP认证来检索用户。
  2. 为用户生成一个Token
  3. 将令牌保存在Redis中,作为一个JSON字符串的值。使用token字符串创建一个RedisKey。使用transform(to:)返回Token作为响应。

最后,在boot(routes:)中注册路由:

// 1
let authGroup = routes.grouped("auth")
// 2
let basicMiddleware = User.authenticator()
// 3
let basicAuthGroup = authGroup.grouped(basicMiddleware)
// 4
basicAuthGroup.post("login", use: loginHandler)

以下是路由代码的作用:

  1. /auth下创建一个新的路由组,用于处理所有认证路由。
  2. 使用authenticator()User创建HTTP基本认证中间件。
  3. 使用该中间件创建一个新的路由组。
  4. /auth/loginPOST请求路由到loginHandler(_:)

关于HTTP基本认证的更多信息,请参见第18章,"API认证,第一部分"。

验证令牌

现在,用户可以登录并获得一个令牌,你需要一种方法让其他微服务验证该令牌并检索与之相关的用户信息。

首先,创建一个新的类型来表示在令牌验证请求中发送的数据。在AuthController.swift的底部,添加以下内容:

struct AuthenticateData: Content {
  let token: String
}

该请求只需要令牌来验证该请求。接下来,在loginHandler(_:)下面创建一个新的路由,以处理来自其他微服务的带有这些数据的请求:

func authenticate(_ req: Request) 
  throws -> EventLoopFuture<User.Public> {
    // 1
    let data = try req.content.decode(AuthenticateData.self)
    // 2
    return req.redis
      .get(RedisKey(data.token), asJSON: Token.self)
      .flatMap { token in
      // 3
      guard let token = token else {
        return req.eventLoop.future(error: Abort(.unauthorized))
      }
      // 4
      return User.query(on: req.db)
        .filter(\.$id == token.userID)
        .first()
        .unwrap(or: Abort(.internalServerError))
        .convertToPublic()
    }
}

以下是路线处理程序的工作:

  1. 将请求主体解码为AuthenticateData
  2. 使用请求中发送的令牌作为密钥,在Redis中检索数据。将数据解码为Token
  3. 确保token存在,否则返回401 Unauthorized响应。
  4. 查询用户数据库,从Token中获取有ID的用户。确保该用户存在,否则会产生一个内部服务器错误。应用程序不应该用一个不存在的用户的ID在数据库中存储一个令牌。返回用户的公共表示,以避免在响应中发送用户的密码。

最后,在boot(routes:)的末尾添加以下内容来注册路由:

authGroup.post("authenticate", use: authenticate)

这将一个POST请求路由到/auth/authenticateauthenticate(_:data:)。建立并运行应用程序,在RESTed中配置一个新的请求,如下所示;

  • URL: http://localhost:8081/auth/login
  • method: POST

点击Authorization按钮,将UsernamePassword设为你先前创建的用户的值。确保你勾选Present Before Authentication Challenge,然后点击OK。点击Send Request,你会看到响应中返回的令牌:

img

再次点击Authorization,取消复选框。这样就可以确保在下一个请求中不发送HTTP基本认证标头。配置一个新的请求,如下所示:

  • URL: http://localhost:8081/auth/authenticate
  • method: POST

添加一个名称为token的单一参数,以及之前请求中返回的token的值。点击Send request,你会看到响应中返回的用户:

img

与其他微服务进行认证

回到Xcode中的TILAppAcronyms项目,停止该应用。打开User.swift,在文件的底部添加以下内容:

extension User: Authenticatable {}

这允许你使用Vapor的认证逻辑向请求添加认证用户。接下来,在Sources/App/Middlewares/中创建一个新文件,称为UserAuthMiddleware.swift。你将创建一个中间件来与其他微服务对话。打开这个新文件,插入以下内容:

import Vapor

struct AuthenticateData: Content {
  let token: String
}

这表示发送到TILAppUsers微服务的数据,以验证令牌。

Note

这与该微服务中使用的代码完全相同。接下来,在AuthenticateData上面,添加中间件来验证TILAppUsers微服务的令牌:

struct UserAuthMiddleware: Middleware {
  // 1
  func respond(to request: Request, chainingTo next: Responder) 
    -> EventLoopFuture<Response> {
      // 2
      guard let token = 
        request.headers.bearerAuthorization else {
          return request.eventLoop
            .future(error: Abort(.unauthorized))
      }
      // 3
      return request.client.post(
        "http://localhost:8081/auth/authenticate", 
        beforeSend: { authRequest in
          // 4
          try authRequest.content
            .encode(AuthenticateData(token: token.token))
      // 5
      }).flatMapThrowing { response in
        // 6
        guard response.status == .ok else {
          if response.status == .unauthorized {
            throw Abort(.unauthorized)
          } else {
            throw Abort(.internalServerError)
          }
        }
        // 7
        let user = try response.content.decode(User.self)
        // 8
        request.auth.login(user)
      // 9
      }.flatMap {
        // 10
        return next.respond(to: request)
      }
    }
}

以下是新的中间件的作用:

  1. 按照Middleware的要求实现respond(to:chainingTo:)
  2. 确保请求在Authorization头中包含一个承载令牌。否则,返回401 Unauthorized响应。
  3. TILAppUsers微服务发送一个请求,以验证该令牌。
  4. 使用post(_:headers:beforeSend)beforeSend参数将令牌编码到请求字符串中。
  5. 使用flatMapThrowing(_:)解决未来。这允许你在闭包内抛出错误。
  6. 确保响应代码是200 OK。如果不是,如果服务返回该状态,则返回401 Unauthorized,否则返回500 Internal Server Error
  7. 将响应体解码为一个User
  8. 用从TILAppUsers服务返回的用户来验证请求。
  9. 使用flatMap(_:)来连锁flatMapThrowing(_:)的结果,并允许你返回一个future
  10. 调用链中的下一个中间件。

关于中间件的更多信息,请参见第29章,"中间件"。

使用新的中间件来保护突变数据库的路由。打开AcronymsController.swift,并在boot(routes:)的末尾添加以下内容:

let authGroup = routes.grouped(UserAuthMiddleware())
authGroup.post(use: createHandler)
authGroup.delete(":acronymID", use: deleteHandler)
authGroup.put(":acronymID", use: updateHandler)

这将使用UserAuthMiddleware创建一个新的路由组,并保护创建、更新和删除路由。删除现在重复的下列路由:

routes.post(use: createHandler)
routes.delete(":acronymID", use: deleteHandler)
routes.put(":acronymID", use: updateHandler)

现在,这些路由包含一个认证的用户,改变路由处理程序以代替使用该用户。在文件的底部,为创建首字母缩写所需的数据添加一个新类型:

struct AcronymData: Content {
  let short: String
  let long: String
}

由于用户来自于请求,你只需要短和长的属性。接下来,将createHandler(_:)的主体替换为以下内容:

// 1
let data = try req.content.decode(AcronymData.self)
// 2
let user = try req.auth.require(User.self)
// 3
let acronym = Acronym(
  short: data.short, 
  long: data.long, 
  userID: user.id)
return acronym.save(on: req.db).map { acronym }

以下是新代码的作用:

  1. 使用上面创建的新类型从请求体中获取首字母缩写数据。
  2. 从请求中获取认证的用户。
  3. 从用户和数据中创建一个Acronym,并保存它。

接下来,在updateHandler(_:)中,替换从请求中解码的类型:

let updateData = try req.content.decode(AcronymData.self)

这使用了AcronymData而不是Acronym。在修改后的一行下面,添加:

let user = try req.auth.require(User.self)

这将从请求中获得认证的用户。你在这里做这个,因为你可以在这一层抛出错误。最后,将acronym.userID = updateData.userID替换为以下内容:

acronym.userID = user.id

这使用了请求的认证用户的ID。建立并运行应用程序,在RESTed中配置一个新的请求,如下所示:

  • URL: http://localhost:8082/
  • method: POST
  • Parameter encoding: JSON-encoded

添加两个带有名称和值的参数:

  • short: IKR
  • long: I Know Right

Authorization添加一个头,其值为Bearer。点击Send Request。你会看到响应中返回的新首字母缩写:

img

对于跨微服务的请求认证,有很多选择。对于大型应用,你可以将认证分割到另一个微服务中。你也可能希望在微服务之间进行认证,即使用户的原始请求不需要它。最后,另一个选择是使用JWT(JSON Web Tokens)。这些是JSON令牌,其中包含编码的信息和签名。它们很有用,因为签名确保你可以信任该令牌,而不需要访问另一个微服务。

接下来去哪?

在本章中,你学到了如何将TIL应用拆分为不同的微服务,用于用户和缩写。你已经看到了如何处理不同服务间的认证和关系。

在下一章中,你将构建另一个微服务,作为客户端访问不同服务的网关。你还将学习如何使用DockerLinux上轻松构建和运行不同的服务。