集群管理、失败转移和负载均衡的实践(下)

容忍失败(Fault Tolerance)

    构建类似微服务架构这样的复杂分布式系统,需要在心中有一个重要的假设:没有什么不会坏的(things will fail)。我们能花大量的精力来防止失败,但是就算这样,我们也不能预防所有的案例,因此面对这个必然的假设唯一的解法就是:我们面向失败进行设计,换一句话说就是,如何做到在一个充满变数的不稳定环境中幸存。

figure out how to survive in an environment where there are failures

集群自愈

    如果一个服务开始变得有问题,我们怎样发现它呢?理想情况下我们的集群管理系统能够探测到它,并且通过一切可能的方式通知到我们,这种方式是传统环境惯用的手段。当把环境切换到一定规模的微服务环境下,我们有一大堆类似的微服务应用,我们真的需要停下来逐个分析到底是哪里导致了一个服务的不正常吗?长时间运行的服务可能处于不健康的状态。一个简单的途径就是把我们的微服务设计成为一种能够在任意时刻被杀死,特别是能够当它们出现问题时被杀死的架构体系。

    Kubernetes提供一组开箱即用的健康探测仪(Probe),我们能用它们来实现集群对实例的管理,使之能够做到自愈。第一个需要介绍的是readiness探测仪,Kubernetes通过它来判断当前的Pod是否能够被服务发现或者挂载到负载均衡上,需要readiness探测仪的原因是应用在容器中启动之后,仍需要一段时间启动应用的进程,这时需要等待应用已经完全启动完成之后才能够让流量进入。

    如果我们在刚启动的阶段放入流量,该Pod并不一定在那一刻处于正常状态,用户就会获得失败的结果或者不一致的状态。使用readiness探测仪,我们能够让Kubernetes查询一个HTTP端点,当且仅当该端点返回200或者其他结果时才会放入流量。如果Kubernetes探测到一个Pod在一段时间内readiness探测仪一直返回失败,这个Pod将会被杀死并重启。

    另一个健康探测仪被称为liveness探测仪,它和readiness探测仪很像,它被检测的时间段是在Pod到达ready状态后,当时在流量放入前(可以理解为早于readiness探测仪)。如果liveness探测仪(可能也是一个简单的HTTP端点)返回了一个不健康的状态(比如:HTTP 500),Kubernetes就会自动的杀死该Pod并重启。

    当我们使用jboss-forge工具以及fabric8-setup命令进行工程创建时,一个readiness探测仪就被默认创建了,可以观察一下hola-springbootpom.xml,如果没有创建,可以使用fabric8-readiness-probe或者fabric8-liveness-probe两个maven命令进行创建。

<fabric8.readinessProbe.httpGet.path>/health</fabric8.readinessProbe.httpGet.path>
<fabric8.readinessProbe.httpGet.port>8080</fabric8.readinessProbe.httpGet.port>
<fabric8.readinessProbe.initialDelaySeconds>5</fabric8.readinessProbe.initialDelaySeconds>
<fabric8.readinessProbe.timeoutSeconds>30</fabric8.readinessProbe.timeoutSeconds>

    可以看到在pom.xml中对于readiness探测仪的配置,而生成的Kubernetes描述文件中的内容如下:

readinessProbe: #pod内容器健康检查的设置
  httpGet: #通过httpget检查健康,返回200-399之间,则认为容器正常
    path: "/health"
    port: 8080
  initialDelaySeconds: 5
  timeoutSeconds: 30

    这意味着readiness探测仪在hola-springboot中已经进行了配置,而Kubernetes会周期性检查Pod的/heath端点。当我们给hola-springboot添加了actuator时,就默认添加了一个/heath端点,这点在Dropwizard和WildFly Swarm也可以按照一样模式处理!

断路器(Circuit Breaker)

    作为服务提供者,你的职责是提供给消费者稳定的服务,根据《Java环境下的微服务》章节提到的承诺设计,服务提供者也会依赖其他的服务或者下游系统,但是这种依赖不能是强依赖。一个服务提供者对其消费者服务承诺有完全的责任,因为分布式系统中总会存在失败,而这些失败将会导致承诺无法被兑现。在我们之前的例子中,hola-spring会调用到hola-backend,如果hola-backend不可用,将会发生什么?在这个场景下我们如何能够信守服务承诺?

    我们需要处理这些分布式系统中常见的错误,一个服务可能会不可用,底层网络可能会间歇性的不稳定,后端服务可能因为负载升高而导致它的响应变慢,一个后端服务的bug可能会造成服务调用时收到异常。如果我们不显式的处理这些场景,我们就会面临自己提供的服务降级的风险,可能会使线程处于阻塞状态,不仅如此可能会造成获取的资源(如:数据库锁或者其他资源)没有被释放,进而导致整个分布式系统出现雪崩。为了解决这个问题,我们将使用NetflixOSS工具栈下的Hystrix

    Hystrix是一个支持容错的Java类库,它能够支持微服务兑现服务的承诺,主要在以下几个方面:

  • 当依赖不可用时提供保护
  • 支持监控并提供调用依赖时的超时机制,用于调用依赖服务时的延迟问题
  • 负载隔离和自愈
  • 优雅降级
  • 实时的监控失败状态
  • 支持处理失败时的业务逻辑

    使用Hystrix的方式,就是使用命令模式,将调用外部依赖的逻辑放置在HystrixCommand的实现中,也就是将调用外部可能失败的代码放置在run()方法中。为了帮助我们开始了解这个过程,我们从hola-wildflyswarm项目入手,我们使用Netflix的最佳实践,显式的进行调用,为什么这样做?因为调试分布式系统是一件非常困难的工作,调用栈分布在不同的机器上,而越少的干预使用者,使调试过程变得简单就显得很重要了。

虽然Hystrix提供了注解的使用方式,但是我们仍然使用最基本的方式去用

    在hola-wildflyswarm项目中,增加依赖:

<dependency>
    <groupId>com.netflix.hystrix</groupId>
    <artifactId>hystrix-core</artifactId>
    <version>${hystrix.version}</version>
</dependency>

    但实际上只需要增加<hystrix.version>1.5.12</hystrix.version>到pom属性即可。 ** 原书中的示例在新的docker ce下理论已经无法跑起来,支离破碎的实例代码让读者无法串联起整个过程,因此译者认为首先需要将hola-wildflyswarm跑起来,然后添加断路器的相关功能。 **

这个过程中,译者会介绍一下如何将镜像推送到aliyun,避免中美交互的网速问题。

    首先在hola-wildflyswarm工程下,启动jboss-forge,然后运行fabric8-setup。然后修改POM,以下针对关键点做说明,具体的内容可以GitHub上对应的pom。对docker-maven-plugin需要增加一些配置,用来禁用fabric8提供的jolokia,这个组件目前和WildFly有兼容性问题。

<plugin>
    <groupId>io.fabric8</groupId>
    <artifactId>docker-maven-plugin</artifactId>
    <version>0.14.2</version>
    <configuration>
        <images>
            <image>
                <name>${docker.image}</name>
                <build>
                    <from>${docker.from}</from>
                    <assembly>
                        <basedir>/app</basedir>
                        <inline>
                            <id>${project.artifactId}</id>
                            <files>
                                <file>
                                    <source>
                                        ${project.build.directory}/${project.build.finalName}-swarm.jar
                                    </source>
                                    <outputDirectory>/</outputDirectory>
                                </file>
                            </files>
                        </inline>
                    </assembly>
                    <env>
                        <JAVA_APP_JAR>${project.build.finalName}-swarm.jar</JAVA_APP_JAR>
                        <AB_JOLOKIA_OFF>true</AB_JOLOKIA_OFF>
                        <AB_OFF>true</AB_OFF>
                        <JOLOKIA_OFF>true</JOLOKIA_OFF>
                    </env>
                </build>
            </image>
        </images>
    </configuration>
</plugin>

    maven配置属性需要调整,镜像from的位置需要调整,不能使用fabric8的tomcat,否则跑不起来,因为hola-wildflyswarm到底还是一个基于JavaEE的项目。Kubernetes的readiness地址需要调整一下,这里放置了一个rest接口。

<properties>
    <failOnMissingWebXml>false</failOnMissingWebXml>
    <docker.from>docker.io/fabric8/java-jboss-openjdk8-jdk:1.0.10</docker.from>
    <docker.image>weipeng2k/${project.artifactId}:${project.version}</docker.image>
    <fabric8.readinessProbe.httpGet.path>/api/holaV1</fabric8.readinessProbe.httpGet.path>
    <fabric8.readinessProbe.httpGet.port>8080</fabric8.readinessProbe.httpGet.port>
    <fabric8.readinessProbe.initialDelaySeconds>5</fabric8.readinessProbe.initialDelaySeconds>
    <fabric8.readinessProbe.timeoutSeconds>30</fabric8.readinessProbe.timeoutSeconds>
    <fabric8.env.GREETING_BACKEND_SERVICE_HOST>hola-backend</fabric8.env.GREETING_BACKEND_SERVICE_HOST>
    <fabric8.env.GREETING_BACKEND_SERVICE_PORT>80</fabric8.env.GREETING_BACKEND_SERVICE_PORT>
    <fabric8.env.AB_JOLOKIA_OFF>true</fabric8.env.AB_JOLOKIA_OFF>
    <version.wildfly-swarm>2017.6.1</version.wildfly-swarm>
    <hystrix.version>1.5.12</hystrix.version>
</properties>

    可以看到hola-wildflyswarm通过fabric8.env的配置形式,将系统变量进行了设置,使得容器能够在运行时感知到底层hola-backend提供的服务所在域名和端口,而这个hola-backend域名对应的真实地址,只有在Kubernetes环境中,由Kubernetes提供给所有的Pod。

    在hola-wildflyswarm下,可以看到存在rc.ymlsvc.yml,同时我们在项目中进行REST调用后,不再使用Map来作为返回值,而是定义了Book这个类型用来承接返回,接下来我们将它们运行起来。

$ kubectl create -f rc.yml
replicationcontroller "hola-wildflyswarm" created
$ kubectl create -f svc.yml
service "hola-wildflyswarm" created

    运行kubectl port-forward hola-backend-t06sg 9001:8080 &,我们做一下数据的添加,将hola-backend的一个Pod端口映射到本机的9001上。

$ curl -H "Content-Type: application/json" -X POST -d '{"name":"Java编程的艺术","authorName":"魏鹏","publishDate":"2015-07-01"}' http://localhost:9001/hola-backend/rest/books/
Handling connection for 9001

    添加数据之后,我们通过kubectl get pods查询一下创建的Pod。

$ kubectl get pods
NAME                      READY     STATUS    RESTARTS   AGE
hola-backend-t06sg        1/1       Running   4          24d
hola-springboot-mdx1v     1/1       Running   3          21d
hola-wildflyswarm-9gv39   1/1       Running   0          1h

    接着创建hola-wildflyswarm-9gv39的代理,kubectl port-forward hola-wildflyswarm-9gv39 9002:8080 &,然后再请求一下接口,看是否能够获得数据。

$ curl http://localhost:9002/api/books/1
Handling connection for 9002
Book{id=2, name='Java编程的艺术', version=0, authorName='魏鹏', publishDate=Wed Jul 01 00:00:00 UTC 2015}

    从调用接口的返回可以看到,hola-wildflyswarm通过REST调用到了hola-backend,但是如果hola-backend应用出现问题,我们再请求hola-wildflyswarm会发生什么呢?

$ kubectl scale rc hola-backend --replicas=0
replicationcontroller "hola-backend" scaled
$ kubectl get pods
NAME                      READY     STATUS    RESTARTS   AGE
hola-springboot-mdx1v     1/1       Running   3          21d
hola-wildflyswarm-9gv39   1/1       Running   0          1h
$ curl http://localhost:9002/api/books/1
Handling connection for 9002
^C

    我们先将hola-backend缩容到0个,然后再请求原有的hola-wildflyswarm接口,结果卡在那里了,也就是hola-wildflyswarm提供的服务无法兑现承诺,我们需要使用Hystrix改造后,就可以在hola-backend后端服务出现问题时,依旧在一定程度上兑现服务承诺。

public class BookCommand extends HystrixCommand<Book> {

    private final String host;
    private final int port;
    private final Long bookId;

    public BookCommand(String host, int port, Long bookId) {
        super(Setter.withGroupKey(
                HystrixCommandGroupKey.Factory
                        .asKey("wildflyswarm.backend"))
                .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                        .withCircuitBreakerEnabled(true)
                        .withCircuitBreakerRequestVolumeThreshold(5)
                        .withMetricsRollingStatisticalWindowInMilliseconds(5000)
                ));

        this.host = host;
        this.port = port;
        this.bookId = bookId;
    }

    @Override
    protected Book run() throws Exception {
        String backendServiceUrl = String.format("http://%s:%d",
                host, port);
        System.out.println("Sending to: " + backendServiceUrl);
        Client client = ClientBuilder.newClient();
        Book book = client.target(backendServiceUrl).path("hola-backend").path("rest").path("books").path(
                bookId.toString()).request().accept("application/json").get(Book.class);

        return book;
    }

    @Override
    protected Book getFallback() {
        Book book = new Book();
        book.setAuthorName("老中医");
        book.setId(999L);
        book.setPublishDate(new Date());
        book.setVersion(1);
        book.setName("颈椎病康复指南");

        return book;
    }
}

    我们可以看到,通过继承HystrixCommand然后返回Book,首先看构造函数,它进行了Hystrix配置。最关键的点是将有可能失败的代码(远程调用逻辑)放置在run()方法中,在run()方法中对远程hola-backend服务进行调用。

    如果后端服务在一段时间内不可用,断路器就会激活,请求将会被拦截,以快速失败的形式体现出来,断路器行为将会在后端服务恢复后关闭,这样就相当于在后端服务出现问题时,进行了服务降级。在BookCommand示例中,可以看到对hola-backend后端请求出现5次问题后将会激活断路器,同时配置了5秒的窗口,用于检查后端服务是否恢复。

可以通过搜索Hystrix文档来了解更多的配置项和相关功能,这些配置功能可以通过外部配置或者运行时配置加以运用

    如果后端服务变得不可用或者延迟较高,Hystrix的断路器将会介入,随着断路器的介入,我们hola-wildflyswarm服务的承诺如何兑现?答案是和场景相关。举个例子:如果我们的服务是给用户一个专属的书籍推荐列表,我们会调用后台的图书推荐服务,如果图书推荐服务变得很慢或者不可用时该怎么办?我们将会降级图书推荐服务,可能返回一个通用的推荐图书列表。为了达到这个效果,我们需要使用Hystrixfallback方法。在BookCommand示例中,通过实现getFallback()方法,在这个方法中,我们返回了一本默认的书。

    接下来看一下使用了BookCommand的新接口:

@Path("/api")
public class BookResource2 {

    @Inject
    @ConfigProperty(name = "GREETING_BACKEND_SERVICE_HOST",
            defaultValue = "localhost")
    private String backendServiceHost;
    @Inject
    @ConfigProperty(name = "GREETING_BACKEND_SERVICE_PORT",
            defaultValue = "8080")
    private int backendServicePort;

    @Path("/books2/{bookId}")
    @GET
    public String greeting(@PathParam("bookId") Long bookId) {
        return new BookCommand(backendServiceHost, backendServicePort, bookId).execute().toString();
    }
}

    我们先创建一个脚本,interval.sh,它用来每隔1秒钟调用一下hola-wildflyswarm的服务。

#!/bin/sh
for i in $(seq 1000); do
  curl http://localhost:9002/api/books2/1
  echo ""
  sleep 1
done;

    原有的hola-wildflyswarm服务不支持断路器,我们将其停止,可以通过kubectl delete all -l project=hola-wildflyswarm,然后使用svc2.ymlrc2.yml创建新的hola-wildflyswarm。我们先将interval脚本运行起来,这时hola-backend服务并没有启动,然后在一段时间后,将hola-backend服务通过kubectl scale rc hola-wildflyswarm --replicas=1扩容1个实例,然后可以看到,对hola-wildflyswarm服务的请求返回出现了变化。

需要往新生成的hola-backend中添加Book数据,因为重启后数据丢失

Book{id=999, name='颈椎病康复指南', version=1, authorName='老中医', publishDate=Sun Sep 24 11:18:45 UTC 2017}
Book{id=999, name='颈椎病康复指南', version=1, authorName='老中医', publishDate=Sun Sep 24 11:18:46 UTC 2017}
Book{id=999, name='颈椎病康复指南', version=1, authorName='老中医', publishDate=Sun Sep 24 11:18:47 UTC 2017}
Book{id=1, name='Java编程的艺术', version=0, authorName='魏鹏', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=1, name='Java编程的艺术', version=0, authorName='魏鹏', publishDate=Wed Jul 01 00:00:00 UTC 2015}

    当后端服务恢复时,调用链路转而正常。这个例子比原书中显得更加通用,但是应用对于fallback或者优雅降级并不是一直优于无法兑现服务承诺,原因是这些选择是基于场景的。例如,如果设计一个资金转换服务,如果后端服务挂掉了,你可能更希望拒绝转账,而不是fallback,你可能更希望的解决方式是当后端服务恢复后,转账才能继续。因此,这里不存在 银弹 ,但是理解fallback和降级却可以这样来:是否使用fallback取决于用户体验,是否选择优雅降级取决于业务场景。

隔水舱(Bulkhead)

    我们看到Hystrix提供了许多开箱即可用的功能,服务的一两次失败并不会由于服务的延迟导致断路器的开启。而这种情况就是分布式环境下最糟糕的,它很容易导致所有的工作线程瞬间都被卡主,从而导致级联错误,由此导致服务不可用。我们希望能够减轻由依赖服务的延迟导致所有资源不可用的困境,为了达到目的,我们引入了一项技术,叫做隔水舱 -- Bulkhead。隔水舱是将资源切分成不同的部分,一个部分耗尽,不会影响到其他,你可以从飞机或者船只的设计中看到这个模式,当出现撞击或意外时,只有会部分受损,而不是全部。

    Hystrix通过线程池技术来实现Bulkhead模式,每一个下游依赖都会被分配一个线程池,在这个线程池中会和外部系统通信。Netflix针对这些线程池进行了调优以确保它们的上下文切换对用户的影响降到最低,但是如果你非常关注这个点,也可以做一些测试或者调优。如果一个下游依赖延迟变高,为这个下游依赖分配的线程池将会耗尽,进而导致对这个依赖发起的新请求被拒绝,这时就可以采用服务降级策略而不用等着错误的级联。

    如果不想使用线程池来完成Bulkhead模式,Hystrix也支持调用线程上的semaphores来实现这个模式,可以访问Hystrix Documentation来获得更多信息。

    Hystrix的默认实现是为下游依赖分配了一个核心线程数为10的线程池,该线程池不是采用阻塞队列作为任务的传递装置,而是使用了一个SynchronousQueue。如果你需要调整它,可以通过线程池配置加以调整,下面我们看一个例子:

public class BookCommandBuckhead extends HystrixCommand<Book> {

    private final String host;
    private final int port;
    private final Long bookId;

    public BookCommandBuckhead(String host, int port, Long bookId) {
        super(Setter.withGroupKey(
                HystrixCommandGroupKey.Factory
                        .asKey("wildflyswarm.backend.buckhead"))
                .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
                        .withMaxQueueSize(-1)
                        .withCoreSize(5)
                )
                .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                        .withCircuitBreakerEnabled(true)
                        .withCircuitBreakerRequestVolumeThreshold(5)
                        .withMetricsRollingStatisticalWindowInMilliseconds(5000)
                ));

        this.host = host;
        this.port = port;
        this.bookId = bookId;
    }

    @Override
    protected Book run() throws Exception {
        String backendServiceUrl = String.format("http://%s:%d",
                host, port);
        System.out.println("Sending to: " + backendServiceUrl);
        Client client = ClientBuilder.newClient();
        Book book = client.target(backendServiceUrl).path("hola-backend").path("rest").path("books").path(
                bookId.toString()).request().accept("application/json").get(Book.class);
        Random random = new Random();
        int i = random.nextInt(1000) + 1;
        Thread.sleep(i);

        return book;
    }

    @Override
    protected Book getFallback() {
        Book book = new Book();
        book.setAuthorName("老中医");
        book.setId(999L);
        book.setPublishDate(new Date());
        book.setVersion(1);
        book.setName("颈椎病康复指南");

        return book;
    }
}

    上面例子中,将线程池的核心线程数设置为5个,也就是同一时刻,只能接受5个并发请求,在run()方法中,可以看到我们选择随机的睡眠一段时间,同时创建第三个rest接口:

@Path("/api")
public class BookResource3 {

    @Inject
    @ConfigProperty(name = "GREETING_BACKEND_SERVICE_HOST",
            defaultValue = "localhost")
    private String backendServiceHost;
    @Inject
    @ConfigProperty(name = "GREETING_BACKEND_SERVICE_PORT",
            defaultValue = "8080")
    private int backendServicePort;

    @Path("/books3/{bookId}")
    @GET
    public String greeting(@PathParam("bookId") Long bookId) {
        return new BookCommandBuckhead(backendServiceHost, backendServicePort, bookId).execute().toString();
    }
}

    原有的hola-wildflyswarm服务不支持Bulkhead,我们将其停止,可以通过kubectl delete all -l project=hola-wildflyswarm,然后使用svc3.ymlrc3.yml创建新的hola-wildflyswarm。我们使用quick脚本,快速的发起调用,如果超过5个线程同时访问hola-backend,将会触发fallback。

#!/bin/sh
sleep 5
for i in $(seq 20); do
  curl http://localhost:9002/api/books3/1
  echo ""
done;

    运行上面的脚本,观察输出,可以看到在正常的返回中出现了fallback数据,也就是下游依赖资源耗尽时,不会拖住当前服务的线程,不会造成级联错误。

$ ./quick.sh
Book{id=1, name='Java编程的艺术', version=0, authorName='魏鹏', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=1, name='Java编程的艺术', version=0, authorName='魏鹏', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=1, name='Java编程的艺术', version=0, authorName='魏鹏', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=1, name='Java编程的艺术', version=0, authorName='魏鹏', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=1, name='Java编程的艺术', version=0, authorName='魏鹏', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=1, name='Java编程的艺术', version=0, authorName='魏鹏', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=1, name='Java编程的艺术', version=0, authorName='魏鹏', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=1, name='Java编程的艺术', version=0, authorName='魏鹏', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=999, name='颈椎病康复指南', version=1, authorName='老中医', publishDate=Tue Sep 26 14:34:44 UTC 2017}
Book{id=1, name='Java编程的艺术', version=0, authorName='魏鹏', publishDate=Wed Jul 01 00:00:00 UTC 2015}

负载均衡

    在一个具备高度伸缩能力的分布式系统中,我们需要一个方式能否发现服务并在服务集群上做到负载均衡。就像我们之前看到的例子,微服务应用必须能够处理失败,我们依赖的服务实例时刻都在加入或者离开集群,因此我们需要通过负载均衡来应对可能的调用失败。不太成熟的途径做到负载均衡的方式是使用round-robin域名解析,但是这个往往不足以应对负载均衡的场景,因此我们也需要会话粘连,自动伸缩等负载的负载均衡策略。让我们看一下在微服务环境下做负载均衡与传统的方式有何不同吧。

Kubernetes的负载均衡

    Kubernetes最好的一点就是提供了许多开箱即用的分布式特性,而不需要添加额外的服务端组件或者客户端依赖。Kubernetes服务提供了微服务的发现机制的同时也提供了服务端的负载均衡功能,事实上,一个Kubernetes服务就是一组Pod的抽象层,它们(Pods)是依靠label selector选择出来的,而针对选出来的这些Pods,Kubernetes将会把请求按照一定策略发送给它们,默认的策略是round-robin,但是可以改变这个策略,比如会话粘连。需要注意的是,客户端不需要将Pod主动添加到Service,而是让Service通过label selector来选择Pods。客户端使用CLUSTER-IP或者Kubernetes提供的DNS来访问服务,而这个DNS并非传统的DNS,在传统的DNS实现中,TTL问题是DNS作为负载均衡的难题,但是在Kubernetes中却不存在这个问题,同时Kubernetes也不依赖硬件做到负载均衡,这些功能完全是内置的。

    为了演示Kubernetes的负载均衡,我们尝试将hola-backend扩容到2个,然后循环请求前端的hola-wildflyswarm接口,观察返回的结果。

由于hola-backend实例使用的是内存数据库,我们将2个实例中的数据初始化成不一样的,这样就可以通过返回的数据来推测出请求到了哪个实例

    首先我们进行扩容hola-backend

$ kubectl scale rc hola-backend --replicas=2
replicationcontroller "hola-backend" scaled
$ kubectl get pods
NAME                      READY     STATUS    RESTARTS   AGE
hola-backend-crrt8        0/1       Running   0          7s
hola-backend-n3sg5        1/1       Running   2          13d
hola-wildflyswarm-bk18g   1/1       Running   1          11d

    扩容完成后,我们在将Pod端口进行映射,因为需要数据初始化操作。

$ kubectl port-forward hola-backend-n3sg5 9001:8080 > backend1.log &
[1] 3471
$ kubectl port-forward hola-backend-crrt8 9002:8080 > backend2.log &
[2] 3487
$ kubectl port-forward hola-wildflyswarm-bk18g 9000:8080 > wildfly.log &
[3] 3514

    对hola-backend实例分别进行数据初始化。

$ curl -H "Content-Type: application/json" -X POST -d '{"name":"Java编程的艺术","authorName":"魏鹏","publishDate":"2015-07-01"}' http://localhost:9001/hola-backend/rest/books/
$ curl -H "Content-Type: application/json" -X POST -d '{"name":"颈椎病康复指南","authorName":"老中医","publishDate":"2015-07-01"}' http://localhost:9002/hola-backend/rest/books/

    由于hola-wildflyswarm实例由DNS负载均衡来请求到后端,我们运行脚本loadbalance.sh

#!/bin/sh
for i in $(seq 10); do
  curl http://localhost:9000/api/books/1
  echo ""
  sleep 1
done;

    以下是输出,读者运行时可能与此有所不同,但是理论上出现不同的输出。

Book{id=1, name='颈椎病康复指南', version=0, authorName='老中医', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=1, name='颈椎病康复指南', version=0, authorName='老中医', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=1, name='Java编程的艺术', version=0, authorName='魏鹏', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=1, name='Java编程的艺术', version=0, authorName='魏鹏', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=1, name='颈椎病康复指南', version=0, authorName='老中医', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=1, name='Java编程的艺术', version=0, authorName='魏鹏', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=1, name='Java编程的艺术', version=0, authorName='魏鹏', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=1, name='颈椎病康复指南', version=0, authorName='老中医', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=1, name='颈椎病康复指南', version=0, authorName='老中医', publishDate=Wed Jul 01 00:00:00 UTC 2015}
Book{id=1, name='颈椎病康复指南', version=0, authorName='老中医', publishDate=Wed Jul 01 00:00:00 UTC 2015}

我们需要客户端负载均衡吗?

    如果你需要更加精细化的负载均衡控制,那么也可以在Kubernetes中使用客户端负载均衡,使用特定的算法来决定调用到服务背后的哪个Pod。你升职可以实现类似权重的负载均衡,跳过哪些看起来失败率较高的Pod,而这种客户端负载均衡技术往往都是语言相关的。在大多数情况下,还是推荐使用语言或者技术无关的方案,因为Kubernetes内置的服务端负载均衡技术已经足够了,但是如果你想使用客户端负载均衡,那么你可以尝试类似:SmartStackbakerstreet.io或者NetflixOSS Ribbon

    在接下来的例子中,我们使用NetflixOSS Ribbon来做客户端负载均衡,有许多不同的方式使用Ribbon,并且可以选择一些不同的注册和发现客户端进行适配,比如:EurekaConsul,但是当运行在Kubernetes中时,我们可以选择使用Kubernetes内置的API来完成服务发现。要启动这个特性,需要使用Kubeflix中的ribbon-discovery,我们首先需要新增依赖:

<dependency>
    <groupId>org.wildfly.swarm</groupId>
    <artifactId>ribbon</artifactId>
</dependency>
<dependency>
    <groupId>io.fabric8.kubeflix</groupId>
    <artifactId>ribbon-discovery</artifactId>
    <version>${kubeflix.version}</version>
</dependency>

其中${kubeflix.version}1.0.15

    在Spring Boot应用中,我们可以使用Spring Cloud,它也提供了对Ribbon的整合,我们可以这样依赖:

<dependency>
    <groupId>com.netflix.ribbon</groupId>
    <artifactId>ribbon-core</artifactId>
    <version>${ribbon.version}</version>
</dependency>
<dependency>
    <groupId>com.netflix.ribbon</groupId>
    <artifactId>ribbon-loadbalancer</artifactId>
    <version>${ribbon.version}</version>
</dependency>

    在pom中,需要增加环境变量配置。

<fabric8.env.USE_KUBERNETES_DISCOVERY>true</fabric8.env.USE_KUBERNETES_DISCOVERY>

    接着看如何将Ribbon整合入我们的应用,我们新建了一个rest接口。

@Path("/api")
public class BookResource4 {

    @Inject
    @ConfigProperty(name = "GREETING_BACKEND_SERVICE_HOST",
            defaultValue = "localhost")
    private String backendServiceHost;
    @Inject
    @ConfigProperty(name = "GREETING_BACKEND_SERVICE_PORT",
            defaultValue = "8080")
    private int backendServicePort;

    private String useKubernetesDiscovery;

    private ILoadBalancer loadBalancer;
    private IClientConfig config;

    public BookResource4() {
        this.config = new KubernetesClientConfig();
        this.config.loadProperties("hola-backend");

        this.useKubernetesDiscovery = System.getenv("USE_KUBERNETES_DISCOVERY");
        System.out.println("Value of USE_KUBERNETES_DISCOVERY: " + useKubernetesDiscovery);

        if ("true".equalsIgnoreCase(useKubernetesDiscovery)) {
            System.out.println("Using Kubernetes discovery for ribbon...");
            loadBalancer = LoadBalancerBuilder.newBuilder()
                    .withDynamicServerList(new KubernetesServerList(config))
                    .buildDynamicServerListLoadBalancer();
        }
    }

    @Path("/books4/{bookId}")
    @GET
    public String greeting(@PathParam("bookId") Long bookId) {
        if (loadBalancer == null) {
            System.out.println("Using a static list for ribbon");
            Server server = new Server(backendServiceHost, backendServicePort);
            loadBalancer = LoadBalancerBuilder.newBuilder()
                    .buildFixedServerListLoadBalancer(Arrays.asList(server));
        }

        Book book = LoadBalancerCommand.<Book>builder()
                .withLoadBalancer(loadBalancer)
                .build()
                .submit(server -> {
                    String backendServiceUrl = String.format("http://%s:%d", server.getHost(),
                            server.getPort());
                    System.out.println("Sending to: " + backendServiceUrl);

                    Client client = ClientBuilder.newClient();
                    return Observable.just(client.target(backendServiceUrl)
                            .path("hola-backend")
                            .path("rest")
                            .path("books")
                            .path(bookId.toString())
                            .request()
                            .accept("application/json")
                            .get(Book.class)
                    );
                }).toBlocking().first();
        return book.toString();
    }
}

    可以看到我们首先使用KubernetesServerList构建了ILoadBalancer,然后在接下来的调用过程中使用com.netflix.loadbalancer.reactive.LoadBalancerCommand,通过负载均衡来进行调用地址的选择。

镜像重新构建为weipeng2k/hola-wildflyswarm:1.3,使用rc4.ymlsvc4.yml进行重新部署

小结

    在本章中,我们学习了一些如何在Linux容器帮助下完成未付的部署、管理和扩缩容。我们能够使用不可变递交带来的好处,通过Linux容器带来服务之间的隔离、快速部署以及移植。我们学习使用了Kubernetes中的服务发现、failover,健康检查等有用的容器管理技术,而这些在Kubernetes中都是开箱即用的,如果想了解更多,可以参考下面的链接:

推荐阅读更多精彩内容