跳转至

第37章:微服务,第二部分

在上一章中,你了解了微服务的基础知识,以及如何将该架构应用于TIL应用。在这一章中,你将学习API网关,以及如何让客户访问微服务。最后,你将学习如何使用DockerDocker Compose来启动整个应用。

API网关

上一章介绍了TIL应用程序的两个微服务,一个用于缩写,一个用于用户。在一个真实的应用中,你可能会有更多的服务用于你的应用的各个不同方面。客户端很难与一个由如此多的微服务组成的应用程序集成。每个客户端都需要知道每个微服务做什么,以及每个服务的URL。客户端甚至可能要为每个服务使用不同的认证方法。微服务架构让人很难把一个服务拆成独立的服务。例如,将认证从TIL应用中的用户服务中移出,就需要对所有客户端进行更新。

这个问题的一个解决方案是API网关。一个API网关可以聚合来自客户的请求,并将其分配给所有需要的服务。此外,API网关可以从多个服务中检索结果,并将其合并为一个响应。

大多数云供应商提供API网关解决方案来管理大量的微服务,但你也可以轻松地创建自己的。在本章中,你就可以做到这一点。

下载本章的启动项目。TILAppUsersTILAppAcronyms项目与前一章的最终项目相同。还有一个新的TILAppAPI项目,包含API网关的骨架。

启动服务

在终端,打开三个独立的标签。确保MySQLPostgreSQLRedis Docker容器在前一章中运行。在终端中,输入以下内容:

docker ps

这个命令显示了当前正在运行的容器。你应该看到三个正在运行的容器:

img

接下来,在第一个选项卡中,导航到TILAppUsers目录并运行以下命令:

swift run

这将启动TILAppUsers服务。在第二个选项卡中,导航到TILAppAcronyms并运行以下命令:

swift run

这将启动TILAppAcronyms服务。最后,在第三个标签中,导航到TILAppAPI目录,并输入这个命令:

open Package.swift

这将打开Xcode项目并开始下载依赖项。

转发请求

TILAppAPIXcode项目中,打开UsersController.swift。在boot(route:)下面输入以下内容:

// 1
func getAllHandler(_ req: Request) 
  -> EventLoopFuture<ClientResponse> {
    return req.client.get("\(userServiceURL)/users")
}

// 2
func getHandler(_ req: Request) throws 
  -> EventLoopFuture<ClientResponse> {
    let id = try req.parameters.require("userID", as: UUID.self)
    return req.client.get("\(userServiceURL)/users/\(id)")
}

// 3
func createHandler(_ req: Request) 
  -> EventLoopFuture<ClientResponse> {
    return req.client.post("\(userServiceURL)/users") {
      createRequest in
      // 4
      try createRequest.content.encode(
        req.content.decode(CreateUserData.self))
  }
}

以下是新代码中的情况:

  1. 创建一个路由处理程序来获取所有的用户。简单地返回来自TILAppUsers微服务的/users路由的响应。
  2. 创建一个路由处理程序来获取单个用户。从请求的参数中获取用户的UUID,并从TILAppUsers微服务中返回该用户的响应。
  3. 创建一个路由处理程序来创建一个用户。向TILAppUsers服务的users路由发送一个POST请求并返回响应。
  4. 在你发送请求之前,将请求到API网关的数据编码到TILAppUsers服务的请求中。这是创建一个用户所需的数据。

要注册新的路由,请在boot(routes:)的末尾添加以下内容:

// 1
routeGroup.get(use: getAllHandler)
// 2
routeGroup.get(":userID", use: getHandler)
// 3
routeGroup.post(use: createHandler)

下面是这个的作用:

  1. 将一个到/api/users/GET请求路由到`getAllHandler(_:)'。
  2. /api/users/<USER_ID>GET请求路由到getHandler(_:),使用userID作为动态参数名。
  3. /api/users/POST请求路由到`createHandler(_:)'。

这些请求不需要任何认证或多重服务。你可以把它们直接转发到TILAppUsers微服务上。

打开AcronymsController.swift,为GET请求做同样的事情。在boot(routes:)下面添加以下内容:

// 1
func getAllHandler(_ req: Request) 
  -> EventLoopFuture<ClientResponse> {
    return req.client.get("\(acronymsServiceURL)/")
}

// 2
func getHandler(_ req: Request) throws 
  -> EventLoopFuture<ClientResponse> {
    let id = 
      try req.parameters.require("acronymID", as: UUID.self)
    return req.client.get("\(acronymsServiceURL)/\(id)")
}

以下是新代码的作用:

  1. 创建一个路由处理程序来获取所有的首字母缩写。简单地从TILAppAcronyms微服务的/路由返回响应。
  2. 创建一个路由处理程序来获取一个首字母缩写。从请求的参数中获取首字母缩写的UUID。从TILAppronyms微服务中返回该首字母缩写的响应。

要注册新的路由,请在boot(routes:)的末尾添加以下内容:

// 1
acronymsGroup.get(use: getAllHandler)
// 2
acronymsGroup.get(":acronymID", use: getHandler)

下面是这个的作用:

  1. 将一个GET请求路由到/api/acronyms/getAllHandler(_:)
  2. GET请求路由到/api/acronyms/<ACRONYM_ID>getHandler(_:)

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

  • URL: http://localhost:8080/api/users
  • method: GET

点击Send Request,你会看到TILAppUsers微服务中的所有用户:

img

API认证

登录

API网关的认证方式与微服务的认证方式完全相同。首先,你必须允许一个用户登录。

Xcode中,打开UsersController.swift。在createHandler(_:)下面,添加一个新的路由处理程序来处理登录:

func loginHandler(_ req: Request) 
  -> EventLoopFuture<ClientResponse> {
    // 1
    return req.client.post("\(userServiceURL)/auth/login") {
      loginRequest in
        // 2
        guard let authHeader =
          req.headers[.authorization].first else {
            throw Abort(.unauthorized)
        }
        // 3
        loginRequest.headers.add(
          name: .authorization,
          value: authHeader)
    }
}

以下是新的路径处理程序的作用:

  1. 发送一个POST请求给TILAppUsers微服务,以登录用户。
  2. 确保传入的请求包含一个Authorization头。否则,返回一个401 Unauthorized的响应。
  3. 用传入请求中的授权头对传出请求进行编码。这个头包含用户的HTTP基本认证信息。

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

routeGroup.post("login", use: loginHandler)

这将一个POST请求发送到/api/users/login的路由到loginHandler(_:)。建立并运行应用程序,并启动RESTed。配置一个新的请求,如下所示:

  • URL: http://localhost:8080/api/users/login
  • method: POST

点击Authorization,输入上一章创建的用户的用户名和密码。勾选Present Before Authentication Challenge,然后点击OK

点击Send Request,你会收到该用户的令牌。复制令牌的值:

img

访问受保护的路由

回到Xcode中,打开AcronymsController.swift。在getHandler(_:)下面,创建一个新的路由处理程序来创建一个首字母缩写:

func createHandler(_ req: Request) 
  -> EventLoopFuture<ClientResponse> {
    // 1
    return req.client.post("\(acronymsServiceURL)/") {
      createRequest in
        // 2
        guard let authHeader =
          req.headers[.authorization].first else {
            throw Abort(.unauthorized)
        }
        // 3
        createRequest.headers.add(
          name: .authorization,
          value: authHeader)
        // 4
        try createRequest.content.encode(
          req.content.decode(CreateAcronymData.self))
    }
}

以下是代码的作用:

  1. TILAppAcronyms微服务发送一个POST请求,创建一个新的缩写。
  2. 确保传入的请求包含一个Authorization头,否则返回一个401 Unauthorized的响应。
  3. Authorization头添加到传出的请求中,到TILAppAcronyms微服务。
  4. 用数据对传出请求的主体进行编码,以创建一个首字母缩写。数据来自于传入的请求。

boot(routes:)下面的acronymsGroup.get(":acronymID", use: getHandler)中注册路由处理器:

acronymsGroup.post(use: createHandler)

这将一个POST请求路由到/api/acronymscreateHandler(_:)。建立并运行应用程序,并返回到RESTed。点击Authorization,取消Present Before Authentication Challenge,以阻止RESTed在头中发送HTTP Basic Authentication凭证。

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

  • URL: http://localhost:8080/api/acronyms/
  • method: POST
  • Parameter encoding: JSON-encoded

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

  • short: IRL
  • long: In Real Life

Authorization创建一个新的头域,其值为Bearer <TOKEN STRING>,使用你之前复制的令牌字符串。

点击Send Request,你会看到通过API网关在TILAppAcronyms微服务中创建的缩写:

img

回到Xcode中,创建用于更新和删除首字母缩写的路由处理程序。在createHandler(_:)下面,添加以下内容:

func updateHandler(_ req: Request) throws 
  -> EventLoopFuture<ClientResponse> {
    // 1
    let acronymID = 
      try req.parameters.require("acronymID", as: UUID.self)
    // 2
    return req.client
      .put("\(acronymsServiceURL)/\(acronymID)") {
        updateRequest in
          // 3
          guard let authHeader =
            req.headers[.authorization].first else {
              throw Abort(.unauthorized)
          }
          // 4
          updateRequest.headers.add(
            name: .authorization,
            value: authHeader)
          // 5
          try updateRequest.content.encode(
            req.content.decode(CreateAcronymData.self))
    }
}

func deleteHandler(_ req: Request) throws 
  -> EventLoopFuture<ClientResponse> {
    // 6
    let acronymID = 
      try req.parameters.require("acronymID", as: UUID.self)
    // 7
    return req.client
      .delete("\(acronymsServiceURL)/\(acronymID)") {
        deleteRequest in
          // 8
          guard let authHeader =
            req.headers[.authorization].first else {
              throw Abort(.unauthorized)
          }
          // 9
          deleteRequest.headers.add(
            name: .authorization,
            value: authHeader)
    }
}

以下是新代码的作用:

  1. 从请求的参数中获取首字母缩写的ID
  2. TILAppAcronyms微服务发送请求以更新该首字母缩写。返回响应。
  3. 在发送请求之前,确保传入的请求包含一个Authorization头。如果没有,则返回一个401 Unauthorized的响应。
  4. 在发出的请求中添加Authorization头。
  5. 用数据对发出的请求的正文进行编码,以更新缩写。这些数据来自传入的请求。
  6. 从请求的参数中获取首字母缩写的ID
  7. TILAppAcronyms微服务发送请求,删除该首字母缩写。返回响应。
  8. 在发送请求之前,确保传入的请求包含一个Authorization头。如果没有,则返回一个401 Unauthorized的响应。
  9. 在发出的请求中添加Authorization头。

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

// 1
acronymsGroup.put(":acronymID", use: updateHandler)
// 2
acronymsGroup.delete(":acronymID", use: deleteHandler)

下面是这个的作用:

  1. 将一个PUT请求路由到/api/acronyms/<ID>updateHandler(_:)
  2. 将一个DELETE请求路由到/api/acronyms/<ID>deleteHandler(_:)

处理关系

在上一章中,你看到了关系如何在微服务中发挥作用。对于微服务架构中的客户端来说,获取不同模型的关系是很困难的。你可以使用API网关来帮助简化这个问题。

获取用户的首字母缩写

Xcode中,打开UsersController.swift。在loginHandler(_:)下面,添加一个新的路由处理程序来获取用户的首字母缩写:

func getAcronyms(_ req: Request) throws 
  -> EventLoopFuture<ClientResponse> {
    // 1
    let userID = 
      try req.parameters.require("userID", as: UUID.self)
    // 2
    return req.client
      .get("\(acronymsServiceURL)/user/\(userID)")
}

以下是发生的情况:

  1. 从请求的参数中获取用户的ID
  2. TILAppAcronyms微服务发送请求,以获得该用户的所有首字母缩写,并返回响应。

要注册新的路由,请在boot(routes:)的末尾添加以下内容:

routeGroup.get(":userID", "acronyms", use: getAcronyms)

这将一个GET请求路由到/api/users/<user_ID>/acronymsgetAcronyms(_:)

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

获取一个用户的首字母缩写词看起来和微服务中的其他请求一样,因为客户端知道用户的ID。获取某个特定首字母缩写的用户则比较复杂。打开AcronymsController.swift,在deleteHandler(_:)下面添加一个新的路由处理程序来完成这个任务:

func getUserHandler(_ req: Request) throws 
  -> EventLoopFuture<ClientResponse> {
    // 1
    let acronymID = 
      try req.parameters.require("acronymID", as: UUID.self)
    // 2
    return req
      .client
      .get("\(acronymsServiceURL)/\(acronymID)")
      .flatMapThrowing { response in
        // 3
        return try response.content.decode(Acronym.self)
      // 4
      }.flatMap { acronym in
        // 5
        return req
          .client
          .get("\(userServiceURL)/users/\(acronym.userID)")
    }
}

以下是新的路径处理程序的作用:

  1. 从请求的参数中获取首字母缩写的ID
  2. TILAppAcronyms发出请求,以获得该首字母缩写的详细信息。
  3. 将响应解码为一个Acronym并返回结果。
  4. 使用flatMap(_:)从前一个期货链中获得Acronym并将其传递到另一个链中。通过期货链,你可以避免在catch语句中包裹任何try
  5. 使用首字母缩写的用户IDTILAppUsers发出请求。

这个路由处理程序需要对两个微服务进行请求。API网关使客户的请求变得简单,就像单体的TIL应用一样。在boot(routes:)下面的acronymsGroup.delete(":acronymID", use: deleteHandler)中注册该路由:

acronymsGroup.get(":acronymID", "user", use: getUserHandler)

这就把对/api/acronyms/<ACRONYM_ID>/userGET请求路由到getUserHandler(_:)。建立并运行应用程序,并启动RESTed。配置一个新的请求,如下所示:

  • URL: http://localhost:8080/api/acronyms//user
  • method: GET

点击Send RequestAPI网关向所有的微服务发出必要的请求,以获得该ID的缩写的用户。你会看到返回的用户信息:

img

最后,在Xcode中停止TILAppAPI应用程序。

Docker中运行一切

现在你有三个微服务,组成了你的TIL应用程序。这些微服务还需要另外三个数据库来工作。如果你正在开发一个客户端应用程序,或另一个微服务,有很多东西需要运行才能开始。你可能还想在Linux中运行一切,以检查你的服务部署是否正确。就像在第11章"测试"中,你将使用Docker Compose来运行一切。

注入服务URLs

目前,应用程序将不同微服务的URL硬编码为localhost。你必须改变这一点以在Docker Compose中运行它们。回到XcodeTILAppAPI,打开AcronymsController.swift。将userServiceURLacronymsServiceURL的定义替换为以下内容:

let acronymsServiceURL: String
let userServiceURL: String

init(
  acronymsServiceHostname: String,
  userServiceHostname: String) {
    acronymsServiceURL =
      "http://\(acronymsServiceHostname):8082"
    userServiceURL = "http://\(userServiceHostname):8081"
}

这样你就可以为不同的服务注入主机名。打开UsersController.swift,再次将userServiceURLacronymsServiceURL的定义替换为以下内容:

let userServiceURL: String
let acronymsServiceURL: String

init(
  userServiceHostname: String,
  acronymsServiceHostname: String) {
    userServiceURL = "http://\(userServiceHostname):8081"
    acronymsServiceURL =
      "http://\(acronymsServiceHostname):8082"
}

最后,打开routes.swift,用以下内容替换routes(_:)的主体:

let usersHostname: String
let acronymsHostname: String

// 1
if let users = Environment.get("USERS_HOSTNAME") {
  usersHostname = users
} else {
  usersHostname = "localhost"
}

// 2
if let acronyms = Environment.get("ACRONYMS_HOSTNAME") {
  acronymsHostname = acronyms
} else {
  acronymsHostname = "localhost"
}

// 3
try app.register(collection: UsersController(
  userServiceHostname: usersHostname,
  acronymsServiceHostname: acronymsHostname))
try app.register(collection: AcronymsController(
  acronymsServiceHostname: acronymsHostname,
  userServiceHostname: usersHostname))

以下是变化的内容:

  1. 如果环境变量存在,使用USERS_HOSTNAME作为用户微服务的主机名。否则,默认为localhost
  2. 如果环境变量存在,使用ACRONYMS_HOSTNAME作为缩略语微服务的主机名。否则,默认为`localhost'。
  3. UsersControllerAcronymsController注册为RouteCollection,注入主机名。

构建项目以确保所有东西都能编译并关闭Xcode。现在,用TILAppUsers打开标签,用Control-C停止应用程序,因为你不再需要一个独立的实例运行。

接下来,打开TILAppAcronyms的标签,用Control-C停止该应用程序。在Xcode中打开该项目,并打开UserAuthMiddleware.swift。在respond(to:chainingTo:)之前添加以下内容:

let authHostname: String

init(authHostname: String) {
  self.authHostname = authHostname
}

这允许你传入TILAppUsers微服务的主机名。接下来,将中间件发出请求的URL--http://localhost:8081/auth/authenticate--替换为以下内容:

"http://\(authHostname):8081/auth/authenticate"

这使用了传入的主机名来进行请求。最后,打开AcronymsController.swift,在boot(routes:)中,将let authGroup = routes.grouped(UserAuthMiddleware())改为以下内容:

let authHostname: String
// 1
if let host = Environment.get("AUTH_HOSTNAME") {
  authHostname = host
} else {
  authHostname = "localhost"
}
// 2
let authGroup = routes.grouped(
  UserAuthMiddleware(authHostname: authHostname))

以下是新代码的作用:

  1. 检查是否有AUTH_HOSTNAME环境变量,并使用authHostname的值。如果环境变量不存在,则默认为localhost
  2. 使用UserAuthMiddleware创建一个路由组,并传入authHostname

构建项目以确保代码的编译。

Docker Compose文件

在包含所有三个项目的根目录下,创建一个名为docker-compose.yml的新文件,并在你选择的编辑器中打开它。添加以下内容来定义版本和数据库服务:

# 1
version: '3'
services:
  # 2
  postgres:
    image: "postgres"
    environment:
      - POSTGRES_DB=vapor_database
      - POSTGRES_USER=vapor_username
      - POSTGRES_PASSWORD=vapor_password
  # 3
  mysql:
    image: "mysql"
    environment:
      - MYSQL_USER=vapor_username
      - MYSQL_PASSWORD=vapor_password
      - MYSQL_DATABASE=vapor_database
      - MYSQL_RANDOM_ROOT_PASSWORD=yes
  # 4
  redis:
    image: "redis"

以下是正在发生的事情:

  1. 设置Docker Compose文件的版本号。
  2. PostgreSQL数据库定义一个服务。使用postgres图像和与你的本地Docker容器相同的环境变量。
  3. MySQL数据库定义一个服务。使用mysql图像和与你的本地Docker容器相同的环境变量。
  4. Redis数据库定义一个服务。使用redis镜像。

在文件的最后,为TILAppUsers微服务添加以下内容:

  # 1
  til-users:
    # 2
    depends_on:
      - postgres
      - redis
    # 3
    build:
      context: ./TILAppUsers
      dockerfile: Dockerfile
    # 4
    environment:
      - DATABASE_HOST=postgres
      - REDIS_HOSTNAME=redis
      - PORT=8081
      - ENVIRONMENT=production

Note

缩进必须与定义的其他服务相匹配。

下面是新代码的作用:

  1. TILAppUsers定义一个服务。
  2. 告诉Docker Compose这个服务依赖于postgresredis容器。Docker Compose将在TILAppUsers之前启动这些服务。
  3. 告诉Docker Compose该服务的工作目录和要使用的Docker文件。默认的Vapor模板包含一个兼容的Dockerfile
  4. 为服务设置必要的环境变量。这些定义了数据库以及环境和端口所需的变量。

你可能注意到这个服务没有在Docker Compose之外暴露任何端口。因为你通过API网关路由一切,没有必要暴露其他微服务。

在文件的最后,添加TILAppAcronyms的规范:

  # 1
  til-acronyms:
    # 2
    depends_on:
      - mysql
      - til-users
    # 3
    build:
      context: ./TILAppAcronyms
      dockerfile: Dockerfile
    # 4
    environment:
      - DATABASE_HOST=mysql
      - PORT=8082
      - ENVIRONMENT=production
      - AUTH_HOSTNAME=til-users

以下是新规范的作用:

  1. TILAppAcronyms定义一个服务。
  2. 告诉Docker Compose这个服务依赖于mysqltil-users容器。Docker Compose会在TILAppAcronyms之前启动这些服务。
  3. 告诉Docker Compose该服务的工作目录和要使用的Docker文件。默认的Vapor模板包含一个兼容的Dockerfile
  4. 为服务设置必要的环境变量。这些定义了数据库所需的变量以及环境和端口。这也设置了AUTH_HOSTNAME环境变量,所以这个服务可以向TILAppUsers发送请求。

最后,在文件的最后,添加TILAppAPI的规范:

  # 1
  til-api:
    # 2
    depends_on:
      - til-users
      - til-acronyms
    # 3
    ports:
      - "8080:8080"
    # 4
    build:
      context: ./TILAppAPI
      dockerfile: Dockerfile
    # 5
    environment:
      - USERS_HOSTNAME=til-users
      - ACRONYMS_HOSTNAME=til-acronyms
      - PORT=8080
      - ENVIRONMENT=production

以下是新规范的作用:

  1. TILAppAPI定义一个服务。
  2. 告诉Docker Compose这个服务依赖于til-userstil-acronyms容器。Docker Compose会在TILAppAcronyms之前启动这些服务。
  3. 将容器的8080端口暴露给你的本地机器,端口为8080。这允许你连接到该容器。
  4. 告诉Docker Compose服务的工作目录和要使用的Docker文件。默认的Vapor模板包含一个兼容的Dockerfile
  5. 为服务设置必要的环境变量。这定义了环境和端口。这也设置了USERS_HOSTNAMEACRONYMS_HOSTNAME环境变量,因此这个服务可以向TILAppUsersTILAppAcronyms发送请求。

修改Dockerfiles

在你可以运行一切之前,你必须修改DockerfilesDocker Compose按要求的顺序启动不同的容器,但不会等待它们准备好接受连接。如果你的Vapor应用程序试图在数据库准备好之前连接到数据库,这将导致问题。在TILAppAcronyms中,打开Dockerfile并替换:

ENTRYPOINT ["./Run"]
CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"]

为下面内容:

ENTRYPOINT sleep 20 && \
  ./Run serve --env $ENVIRONMENT --hostname 0.0.0.0 --port $PORT

这告诉容器在启动Vapor应用程序之前要等待20秒。这应该给数据库足够的时间来启动。在实际应用中,你可能要考虑把这个放在一个脚本中,在启动Vapor应用之前测试数据库。你也可以参考第33章,"用Docker部署",以获得更强大的解决方案。

TILAppUsers中,打开Dockerfile,做与上面相同的修改。

运行一切

现在你已经准备好在Docker Compose中启动你的应用程序了。在Terminal中,在包含docker-compose.yml的目录中,输入以下内容:

docker-compose up

这将下载和构建docker-compose.yml中指定的所有容器,并启动它们。请注意,构建所有的微服务可能需要一些时间。

当一切都启动和运行时,你会看到类似的东西:

img

然后你可以像以前一样打开RESTed并提出请求。

接下来去哪?

在本章中,你学到了如何使用Vapor来创建一个API网关。这使得客户与你的不同微服务的交互变得简单。你学会了如何在不同的微服务之间发送请求并返回单一的响应。你还学会了如何使用Docker Compose来构建和启动所有的微服务,并将它们连接起来。

你现在已经掌握了编写强大的微服务所需的基本知识。你可以用消息队列、协议缓冲区和远程程序性调用来进一步加强。你现在可以构建的应用是没有限制的!