4年前 (2021-05-08)  微服务 |   抢沙发  310 
文章评分 0 次,平均分 0.0

在使用Spring Boot构建微服务的这一篇文章中,我们将引入一些新概念,使我们的服务更具可伸缩性和弹性。

内部服务间通信

与传统的、单一的方法不同的是,应用程序的所有部分通常都可以使用单一的数据库,而微服务作为不同的进程运行,它们有自己的私有数据存储,必须相互通信才能实现它们的目标。这种根本的差异需要不同的思维方式,这可能是开发人员在过渡到构建微服务时面临的最大挑战之一。

服务间通信有许多不同的方法,每种方法都适用于不同的场景。不过,在本教程中,我们将重点介绍同步HTTP。

HTTP客户端

用Java和Spring发出HTTP请求并不难。Java9引入了一个酝酿中的HttpClient,它在Java11中是标准化的,还有许多众所周知的开源HTTP客户端可用,比如OkHttp、ApacheHttpClient和Spring的RestTemplate。

我们可以选择这些选项中的任何一个来管理服务之间的通信,但是在我们这样做之前还有其他问题需要解决。由于服务地址是动态的,而且是预先未知的,所以我们需要与本地服务注册中心集成,以便在发出请求之前查找它们。我们还需要投入额外的时间来实现诸如(反)序列化、故障检测和负载分配等功能。

Feign是一个项目,最初由Netflix发起,旨在允许开发人员通过创建带注释的接口以声明方式构建HTTP客户机。它是高度可定制的,并且支持大量现成的集成。springcloudopenfeign通过配置Feign并将其绑定到Spring环境,集成springmvc风格的注释来构建HTTP客户机,并将其连接到springcloud生态系统的其余部分,从而增加了对Feign的支持。

假设我们希望通过向/customers/X/orders发出GET请求来列出属于某个客户的所有订单。

我们将首先修改OrderControllerOrder服务中的getAllOrders方法,以添加通过查询字符串中的可选客户ID筛选订单的功能:

@GetMapping
public List<Order> getAllOrders(@RequestParam(required = false) Integer customerId) {
    if (customerId != null) {
        return orders.stream()
                     .filter(order -> customerId.equals(order.getCustomerId()))
                     .collect(Collectors.toList());
    }

    return orders;
}

启动发现和订购服务,然后访问http://localhost:3002/?customerId=2查看Jane Doe的所有订单。

现在,通过将以下内容添加到其build.gradle文件的dependencies块,将Spring Cloud OpenFeign依赖项添加到客户服务中:

implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'

然后,将@EnableFeignClients注释添加到CustomerServiceApplication类:

package com.github.jrhenderson1988.customerservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class CustomerServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(CustomerServiceApplication.class, args);
    }
}

我们将创建一个与Order服务通信的假客户机,用Spring MVC样式的注释声明一个名为getOrdersForCustomer的方法来控制其行为:

package com.github.jrhenderson1988.customerservice;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;

@FeignClient(name = "order-service")
public interface OrderClient {
    @GetMapping("/")
    Object getOrdersForCustomer(@RequestParam int customerId);
}
  • @FeignClient(name=“order service”)允许发现客户机定义,并将order service指定为要与之通信的服务的名称。
  • @GetMapping(“/”)声明调用getOrdersForCustomer应该对/path执行GET请求。
  • @RequestParam int customerId指示Feign获取customerId参数,并在发出请求时将其插入查询字符串中。
  • 对象返回类型指示Feign应该将远程服务的响应反序列化为返回给调用者的对象。

在实际应用程序中,我们通常会指定一个返回类型,如Iterable<Order>。我们通常也会以不同的方式构造应用程序,以促进代码重用,并避免域实体的重复。不过,为了简洁起见,我决定保持简单,只返回一个对象,特别是因为这个响应的结构将在下一节中改变。

最后,让我们将新客户机注入CustomerController

@RestController
public class CustomerController {
    private List<Customer> customers = Arrays.asList(
            new Customer(1, "Joe Bloggs"),
            new Customer(2, "Jane Doe"));

    private OrderClient orderClient;

    public CustomerController(OrderClient orderClient) {
        this.orderClient = orderClient;
    }

并创建一个新方法来处理GET/{id}/orders,该方法使用外部客户端调用我们声明的方法:

@GetMapping("/{id}/orders")
    public Object getOrdersForCustomer(@PathVariable int id) {
        return orderClient.getOrdersForCustomer(id);
    }

确保所有服务都已启动并正在运行(一如既往,首先是发现服务),然后访问http://localhost/customers/2/orders. 你应该看到属于无名氏的同一份订单清单,就像我们访问时看到的一样http://localhost:3002/?customerId=2,更早。

扩容缩容

在微服务体系结构中,单个服务可以使用以下一种或多种组合进行独立扩展:

  • 垂直缩放:通过增加现有机器的功率(CPU、内存)来实现。这种方法受到机器最大容量的限制,并且经常涉及停机。由于增加处理能力的成本增加以及需求波动的事实,扩大规模可能成本高昂且效率低下。
  • 水平扩展:包括向现有集群添加更多机器。这种方法实际上是无限的,而且非常灵活,因为可以动态地添加或删除节点,几乎没有停机时间。由于这种灵活性和廉价商品硬件的可用性,向外扩展可能具有极高的成本效益。

通常,一旦为给定服务制定了最佳配置,水平扩展往往是首选选项。但是,这会带来一些额外的复杂性—我们需要能够在给定服务的多个实例之间有效地分配负载。

Ribbon的客户端负载平衡

Ribbon是Netflix编写的进程间通信库。它的主要目的是提供客户端负载平衡,但也与其他云服务(如Eureka for service discovery和Hystrix)很好地集成,以提供弹性。

我们已经在项目中使用Ribbon进行客户端负载平衡,因为SpringCloudOpenFeign使用Ribbon作为其默认负载平衡器。

让我们修改Order服务,并将服务当前运行的端口号添加到响应主体中,以证明负载平衡已启用。

创建ResponseWrapper类:

package com.github.jrhenderson1988.orderservice;

import org.springframework.core.env.Environment;

public class ResponseWrapper<T> {
    private final Integer port;
    private final T data;

    public ResponseWrapper(final Environment environment, final T data) {
        String serverPort = environment.getProperty("server.port");
        this.port = serverPort != null ? Integer.parseInt(serverPort) : null;
        this.data = data;
    }

    public Integer getPort() {
        return port;
    }

    public T getData() {
        return data;
    }
}

然后将Spring环境注入到OrderController中:

@RestController
public class OrderController {
    private final List<Order> orders = Arrays.asList(
            new Order(1, 1, "Product A"),
            new Order(2, 1, "Product B"),
            new Order(3, 2, "Product C"),
            new Order(4, 1, "Product D"),
            new Order(5, 2, "Product E"));

    private final Environment environment;

    @Autowired
    public OrderController(final Environment environment) {
        this.environment = environment;
    }

并更改getAllOrders方法以返回封装在ResponseWrapper中的列表List<Order>

@GetMapping
public ResponseWrapper<List<Order>> getAllOrders(@RequestParam(required = false) Integer customerId) {
    if (customerId != null) {
        return new ResponseWrapper<>(
                environment,
                orders.stream()
                        .filter(order -> customerId.equals(order.getCustomerId()))
                        .collect(Collectors.toList()));
    }

    return new ResponseWrapper<>(environment, orders);
}

正常启动发现、客户和网关服务。然后,我们将使用以下命令在不同的端口上启动Order服务的多个实例(每次用不同的数字替换<port>):

./gradlew bootRun --args='--server.port=<port>'

等待几分钟,等待所有内容向发现服务注册并传播更新的注册表。查看Eureka仪表板-在当前已向Eureka注册的实例下,您应该会看到Order Service的多个实例:

使用Spring Boot构建微服务

发出一些GET请求http://localhost/customers/2/orders 并注意响应中的端口。每次都应该改变。

Feign客户端使用Ribbon来平衡订单服务实例之间的请求。Ribbon使用本地服务注册表来查找Order服务的所有物理地址,并使用可插入的负载平衡算法来确定哪个实例应该接收下一个请求。

默认情况下,我们的网关服务(设置为Zuul代理)也使用Ribbon来平衡服务之间的请求。发出GET请求http://localhost/orders,再次注意到port

弹性

像所有的软件一样,微服务也会因为各种各样的原因而失败——bug、过载和硬件/网络故障等等。

分布式体系结构依赖于许多互连服务通过网络相互通信的能力。因此,优雅地处理不可避免的故障是至关重要的——否则会导致级联故障,其中一个小问题可能会引发其他服务中的问题,从而产生连锁反应,最终导致整个系统瘫痪。

Hystrix的延迟和容错

Hystrix是一个容错和延迟库,由Netflix编写,它为开发人员提供对服务间(或第三方)通信的细粒度控制。它有助于提高系统的整体弹性,防止级联故障,并允许工程师添加回退机制,以促进优雅的降级。

对外部系统的调用封装在命令中,这些命令通常在单独的线程中运行。Hystrix监督执行,对超过配置阈值的调用进行超时,并检测错误/异常。监测故障率,当超过规定的限值时,断路器跳闸,停止交通流,使过载或故障服务有机会恢复。

使用Spring Boot构建微服务

Feign还包括对Hystrix的支持,尽管它在默认情况下没有启用。当Hystrix位于类路径上并在配置中显式启用时,springcloudopenfeign会自动配置它并将其与外部客户机集成。

让我们添加Hystrix支持,并为客户服务的OrderClient创建一个回退,以允许getOrdersForCustomer方法在Order服务失败时正常降级。

首先,我们将添加springcloudhystrix作为依赖项。在客户服务的build.gradle文件的dependencies块中插入以下内容:

implementation 'org.springframework.cloud:spring-cloud-starter-netflix-hystrix'

然后,我们将配置springcloudopenfeign以启用Hystrix支持。在application.properties中,添加:

feign.hystrix.enabled=true

此时,Hystrix将由外部客户机使用。默认情况下,它允许远程订单服务在介入并取消请求之前最多1秒做出响应。

让我们向OrderClient添加一个回退机制,当请求失败时Hystrix将调用该机制。首先,创建一个@Component类,它实现我们的OrderClient接口并为getOrdersForCustomer方法提供回退行为。在这种情况下,我们只返回一个空列表:

package com.github.jrhenderson1988.customerservice;

import org.springframework.stereotype.Component;

import java.util.Collections;

@Component
public class OrderClientFallback implements OrderClient {
    @Override
    public Object getOrdersForCustomer(int customerId) {
        return Collections.emptyList();
    }
}

现在,我们需要配置我们的外部客户机来指向回退实现。在OrderClient类中,修改@FeignClient注释以添加指向OrderClientFallback类的回退:

@FeignClient(name = "order-service", fallback = OrderClientFallback.class)
public interface OrderClient {

网关服务还默认使用Hystrix,并在1秒后取消请求,这导致它超时并在客户服务有机会执行其回退机制和响应之前返回错误响应。为了避免这种情况,我们将增加网关的超时设置,以允许下游客户服务有足够的时间在其1秒超时过期后进行响应。

将以下行添加到网关服务的application.properties文件中:

hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=5000
ribbon.ConnectTimeout=4000
ribbon.ReadTimeout=4000

启动发现服务,然后启动客户、订单和网关服务(./gradlew bootRun)。在等待一切启动和传播之后,向发送请求http://localhost/customers/2/orders 确认一切正常,你看到了正确的反应。

现在,让我们通过终止Order服务实例并再次尝试相同的端点来模拟失败。这一次,您应该看到一个空列表,而不是看到所有的无名氏命令,这是我们在回退中定义的。

小结

  • 在收到对路径/customers/{id}/ordersGET请求后,我们的网关服务将其代理给客户服务。
  • 客户服务使用外部客户机向订单服务发送请求以处理请求。
  • 外部客户机在其本地注册表中查找可用订单服务实例的物理地址。
  • 它使用Ribbon来决定哪个实例应该接收请求。
  • 使用Hystrix命令将请求发送到所选实例以处理超时或失败。
  • 如果Hystrix断路器断开,请求将立即取消,并调用我们的回退方法。
  • 如果在调用Order服务时检测到问题(或超时),Hystrix会报告失败并调用我们配置的回退方法。
  • 收到请求后,所选的Order服务实例执行其工作并发回响应。
  • 客户服务接收响应(或调用fallback方法的结果),并将其自己的响应发送回网关(以及客户端)。

完整代码地址:https://github.com/jrhenderson1988/building-microservices-with-spring-boot/tree/part-2

 

除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/1899.html

关于

发表评论

表情 格式

暂无评论

登录

忘记密码 ?

切换登录

注册