使用Spring Cloud/Netflix堆栈在PWS上进行蓝绿部署的规范方法是什么?

13

我正在尝试一种与此图像详细描述的设置非常相似的设置:https://raw.githubusercontent.com/Oreste-Luci/netflix-oss-example/master/netflix-oss-example.png

(注:该链接可能需要翻墙才能访问)

enter image description here

在我的设置中,我使用了一个客户端应用程序(https://www.joedog.org/siege-home/),一个代理(Zuul),一个发现服务(Eureka)和一个简单的微服务。所有内容都部署在PWS上。
我想从一个版本的简单微服务迁移到下一个版本而不会有任何停机时间。最初,我采用了这里描述的技术:https://docs.cloudfoundry.org/devguide/deploy-apps/blue-green.html 在我看来,这种方法与像Eureka这样的发现服务不“兼容”。事实上,在我重新映射所有路由(CF Router)之前,我的服务的新版本已经在Eureka中注册并接收到流量。
这导致我采取了另一种方法,依赖于Spring Cloud / Netflix中的故障转移机制:
我启动了一个新的(向后兼容的)版本的服务。
当Zuul / Eureka选中此版本时,它开始获得50%的流量。
一旦我验证了新版本的正确性,就会关闭“旧”实例。(我只需在PWS中单击“停止”按钮)
据我所知,Zuul在底层使用Ribbon(负载平衡),因此在旧实例仍然在Eureka中但实际上正在关闭的那一刻,在新实例上进行重试而不影响客户端。
然而,我的假设是错误的。 我在客户端中收到了一些502错误:
Lifting the server siege...      done.

Transactions:               5305 hits
Availability:              99.96 %
Elapsed time:              59.61 secs
Data transferred:          26.06 MB
Response time:              0.17 secs
Transaction rate:          89.00 trans/sec
Throughput:             0.44 MB/sec
Concurrency:               14.96
Successful transactions:        5305
Failed transactions:               2
Longest transaction:            3.17
Shortest transaction:           0.14

我的application.yml的一部分

server:
  port: ${PORT:8765}

info:
  component: proxy

ribbon:
  MaxAutoRetries: 2   # Max number of retries on the same server (excluding the first try)
  MaxAutoRetriesNextServer: 2 # Max number of next servers to retry (excluding the first server)
  OkToRetryOnAllOperations: true # Whether all operations can be retried for this client
  ServerListRefreshInterval: 2000 # Interval to refresh the server list from the source
  ConnectTimeout: 3000 # Connect timeout used by Apache HttpClient
  ReadTimeout: 3000 # Read timeout used by Apache HttpClient

hystrix:
  threadpool:
      default:
        coreSize: 50
        maxQueueSize: 100
        queueSizeRejectionThreshold: 50
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 10000

我不确定出了什么问题。

这是技术问题吗?

还是我做出了错误的假设(我确实在某个地方读到过POST请求无法重试,但我并不完全理解)?

我很想听听你是如何处理的。

谢谢,Andy

1个回答

2

我也曾对此感到困惑。我不会声称已经在使用Spring Cloud“生产环境”中,只是一直在试验它。

假设:我们假设所有实例状态的真实来源都存储在Eureka中,那么Eureka应该是我们操作控制的机制。我们可以使用Eureka通过将实例状态设置为OUT_OF_SERVICE来使实例停止服务。当Ribbon刷新其服务器列表时,它不会使用这些停用的实例。Eureka提供了用于查询实例和设置实例状态的REST API。太好了。

问题是:如何确定哪些实例属于蓝色组,哪些实例属于绿色组?

我在想... Eureka为每个实例提供了一个元数据映射。在我们的构建/制作步骤中,我们在元数据映射中设置版本ID怎么样?我们可以使用Git提交ID或某种语义化版本方案或其他内容。现在,我可以查看Eureka元数据并根据版本值识别蓝色与绿色实例。我们可以在每个服务中使用属性设置元数据值。

例如:eureka.instance.metadataMap.version=8675309

现在,如果我们可以告诉Eureka:“停用FUBAR服务的所有8675309版本实例。”那就太好了。我认为这不是开箱即用的。Spring Cloud的酷之处在于,所有这些服务(包括Eureka Server)都只是我们可以根据自己的需求进行修改的Spring应用程序。下面的代码公开了一个端点,根据应用名称和版本将实例设置为“停用”。只需将此控制器添加到您的Eureka Server中即可。它不适用于生产环境,只是一个想法而已。

现在,一旦Eureka停用了这些实例并且Ribbon刷新了其服务器列表,就可以安全地杀死或路由这些实例。

POST至:

http://[eurekahost:port]/takeInstancesOutOfService?applicationName=FOOBAR&version=8675309

希望这有所帮助?
import java.util.Collection;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.netflix.appinfo.InstanceInfo;
import com.netflix.appinfo.InstanceInfo.InstanceStatus;
import com.netflix.discovery.shared.Application;
import com.netflix.eureka.EurekaServerContextHolder;
import com.netflix.eureka.registry.PeerAwareInstanceRegistry;

@RestController
public class EurekaInstanceStateController {

    @RequestMapping(value="/instancesQuery", method=RequestMethod.POST)
    public Collection<String> queryInstancesByMetaData(
            @RequestParam("applicationName") String applicationNameCriteria,
            @RequestParam("version") String versionCriteria)
    {
        return getRegistry().getSortedApplications()
                .stream()
                .filter(hasApplication(applicationNameCriteria))
                .flatMap(app -> app.getInstances().stream())
                .filter(hasVersion(versionCriteria))
                .map(info -> info.getAppName() + " - " + info.getId() + " - " + info.getStatus() + " - " + info.getMetadata().get("version"))
                .collect(Collectors.toList());
    }

    @RequestMapping(value="/takeInstancesOutOfService", method=RequestMethod.POST)
    public Collection<String> takeInstancesOutOfService(
            @RequestParam("applicationName") String applicationNameCriteria,
            @RequestParam("version") String versionCriteria)
    {
        return getRegistry().getSortedApplications()
                .stream()
                .filter(hasApplication(applicationNameCriteria))
                .flatMap(app -> app.getInstances().stream())
                .filter(hasVersion(versionCriteria))
                .map(instance -> updateInstanceStatus(instance, InstanceStatus.OUT_OF_SERVICE) )
                .collect(Collectors.toList());
    }

    /**
     * @param instance
     * @return
     */
    private String updateInstanceStatus(InstanceInfo instance, InstanceStatus status)
    {
        boolean isSuccess = getRegistry().statusUpdate(instance.getAppName(), instance.getId(),
        status, String.valueOf(System.currentTimeMillis()),
        true);

        return (instance.getAppName() + " - " + instance.getId() + " result: " + isSuccess);
    }

    /**
     * Application Name Predicate
     * @param applicationNameCriteria
     * @return
     */
    private Predicate<Application> hasApplication(final String applicationNameCriteria)
    {
        return application -> applicationNameCriteria.toUpperCase().equals(application.getName());
    }

    /**
     * Instance Version Predicate.  Uses Eureka Instance Metadata value name "version".</br>
     * 
     * Set / Bake the instance metadata map to contain a version value.</br>  
     * e.g. eureka.instance.metadataMap.version=85839c2
     * 
     * @param versionCriteria
     * @return
     */
    private Predicate<InstanceInfo> hasVersion(final String versionCriteria)
    {
        return info -> versionCriteria.equals(info.getMetadata().get("version"));
    }

    private PeerAwareInstanceRegistry getRegistry() {
        return EurekaServerContextHolder.getInstance().getServerContext().getRegistry();
    }
}

好主意。我也在研究这个问题。但是我不确定是否应该在Eureka端进行操作——如果服务发送了新的心跳,那么它的状态不会再次变为UP吗?Spring Cloud带有/pause和/resume端点,我认为这会将客户端状态更改为OUT_OF_SERVICE或DOWN。我正在考虑编写一个部署脚本,在部署之前提交到/pause。仍然可以从Eureka中获取实例列表,并按版本或其他方式进行过滤。 - nedenom
我也在研究OUT_OF_SERVICE状态。据我所知,Asgard采用了类似的方法:https://github.com/Netflix/asgard/wiki/Eureka-Integration我的结论是,为了在PWS上实现滚动更新,我们需要一个自定义的、自制的仪表板(例如Asgard),以便于实现此功能。PWS视图过于有限,无法完成此操作。据我所知,没有Spring库可以做到这一点。我之前并不知道我可以像你那样开发自己的REST端点,所以我一开始使用了Eureka本身的REST API。我会看看它的 - 谢谢! - Andy Verbunt
@nedenom 如果您将状态设置为DOWN,它将在30秒后自动设置为UP。如果您将状态设置为OUT_OF_SERVICE,则会一直保持该状态,直到您手动(通过REST api)将其设置回UP / DOWN。 - Andy Verbunt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接