软件开发中的决策:权衡与取舍
上QQ阅读APP看书,第一时间看更新

2.3 抽取代码为一个独立的微服务

以库的方式共享代码是一个好的开端,不过,正如我们在2.2.1节中所介绍的,它也有不足,包含多个问题。例如,使用库的开发者需要考虑兼容性及相关的问题。他们将无法自由地使用第三方库。与此同时,导入库的代码意味着你的代码与库代码之间存在着依赖上的紧耦合。这并不是说微服务架构就没有紧耦合,它可能也存在各种耦合,譬如在API级别、以请求格式等进行耦合。采用库与微服务架构,在耦合这一点上,二者的区别主要在于发生的场所。

如果耦合的代码逻辑可以抽取为独立的业务领域,我们就可以创建一个新的微服务,以HTTP API的方式提供相关功能。譬如,我们可以将之前抽取出来、单独提供的功能定义为一个新的业务领域。之前讨论的授权组件是说明这一问题的好例子,它提供的令牌验证功能相对独立,有自己的业务领域。我们可以找到新服务能处理的业务实体,譬如授权服务可以处理user实体的用户名和密码。

注意 我们对例子做了高度的简化,然而现实中,授权服务通常需要访问其他系统的信息(譬如,数据库中的信息)。如果权限信息存储在数据库中,那么将授权逻辑抽取为一个独立的微服务就更合理了。出于简化的目的,在我们设计的例子中,授权服务并没有对外部服务进行访问。

添加新的服务会带来一系列不可忽略的开销。这些开销并不局限于开发,还有进行维护所需的人力。很明显,授权服务有自己的业务领域,有独立的业务模型。因此,授权服务与现有平台是独立的。无论是Person服务还是Payment服务都与授权服务没有太大的关系。基于这些考量,我们看看如何实现授权服务。图2.6展示了这3个服务之间的关系。

图2.6 授权服务与Person服务和Payment服务之间的关系

如图2.6所示,新的架构由3个独立的服务组成,服务之间使用HTTP API通信连接。这意味着无论是Person服务还是Payment服务,都需要多执行一次HTTP调用请求,对它们的令牌进行验证。如果你的应用对高性能没有要求,多进行一次HTTP调用请求应该不会有什么大问题(我们假设该请求发生于集群内部,或者一个封闭网络内,请求的服务器并非随机选择的某个服务器)。

在新架构中,之前重复的或者以库方式提取出的授权逻辑会被抽象为授权服务,以HTTP API的方式,经由/auth端点提供访问。我们的客户端会向授权服务发送验证令牌的请求,如果验证失败,授权服务会返回值为401的HTTP返回码。如果令牌验证通过,HTTP API会返回200 OK的状态码。代码清单2.4展示了如何构建新的验证服务。

代码清单2.4 使用HTTP端点提供的授权服务

@Path("/auth")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class AuthResource {
 
  private final AuthService authService = new AuthService();
  
  @GET
  @Path("/validate/{token}")
  public Response getAllPayments(@PathParam("token") String token) {
    if (authService.isTokenValid(token)) {
      return Response.ok().build();
    } else {
      return Response.status(Status.UNAUTHORIZED).build();
    }
  }
}

由于AuthService已经封装了令牌验证的逻辑,授权的执行将通过HTTP请求的方式实现,不再使用库函数调用的方式。授权的代码将存放在单独的授权微服务库中。Payment和Person服务不再需要以直接导入授权库,或者在自己的代码库中实现授权逻辑的方式来执行授权相关的操作,现在只需要使用一个HTTP客户端向/auth端点发送HTTP请求即可完成验证令牌的工作。代码清单2.5展示了发送HTTP请求的逻辑。

代码清单2.5 向授权服务发送HTTP请求

// 向独立的服务发送请求
public boolean isTokenValid(String token) throws IOException {
  CloseableHttpClient client = HttpClients.createDefault(); 
  HttpGet httpGet = new HttpGet("http://auth-service/auth/validate/" + 
   token);
  CloseableHttpResponse response = client.execute(httpGet);  ◁--- 向独立的授权服务发送HTTP请求
  return response.getStatusLine().getStatusCode() == HttpStatus.SC_OK;
 }

在代码清单2.5中,我们创建了一个HTTP客户端执行HTTP请求。在实际生产系统中,客户端会通过在调用组件之间共享以减少打开的连接数来节约资源。

HTTP客户端发起一次HTTP GET请求,验证令牌的合法性。如果返回的状态码是OK,就意味着令牌是合法的。否则,令牌就是非法的。

注意 授权服务既可以使用auth-service的域名系统(domain name system,DNS)向外提供,也可以使用别的服务发现机制,譬如Eureka、Consul等。auth-service也可以使用静态IP地址的方式直接暴露给外部服务。