現在我們簡單地來定制二個 ServiceInstanceListSupplier, 都是zone-preference的變種.
為了方便, 我重新調整了一下項目的結構, 把一些公用的類移動到hello-pubtool 模塊, 這樣網關項目和Feign項目就能復用一樣的類了.
A. main和beta互不相通, 絕對隔離 (資源相對充裕)
回到最開始的目的, 我們先實現這個A方案
package com.cnscud.betazone.pub.samezone;
import com.cnscud.betazone.pub.LogUtils;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.config.LoadBalancerZoneConfig;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 只返回統一區域的實例. (和網上的代碼略有不同)
* @see org.springframework.cloud.loadbalancer.core.ZonePreferenceServiceInstanceListSupplier
* @see org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplierBuilder
*
*/
public class SameZoneOnlyServiceInstanceListSupplier implements ServiceInstanceListSupplier {
private final String ZONE = "zone";
private final ServiceInstanceListSupplier delegate;
private final LoadBalancerZoneConfig zoneConfig;
private String zone;
public SameZoneOnlyServiceInstanceListSupplier(ServiceInstanceListSupplier delegate,
LoadBalancerZoneConfig zoneConfig) {
this.delegate = delegate;
this.zoneConfig = zoneConfig;
}
@Override
public String getServiceId() {
return delegate.getServiceId();
}
@Override
public Flux<List<ServiceInstance>> get() {
return delegate.get().map(this::filteredByZone);
}
private List<ServiceInstance> filteredByZone(List<ServiceInstance> serviceInstances) {
if (zone == null) {
zone = zoneConfig.getZone();
}
if (zone != null) {
List<ServiceInstance> filteredInstances = new ArrayList<>();
for (ServiceInstance serviceInstance : serviceInstances) {
String instanceZone = getZone(serviceInstance);
if (zone.equalsIgnoreCase(instanceZone)) {
filteredInstances.add(serviceInstance);
}
}
//如果沒找到就返回空列表,絕不返回其他集群的實例
LogUtils.warn("find instances size: " + filteredInstances.size());
return filteredInstances;
}
//如果沒有zone設置, 則返回所有實例
return serviceInstances;
}
private String getZone(ServiceInstance serviceInstance) {
Map<String, String> metadata = serviceInstance.getMetadata();
if (metadata != null) {
return metadata.get(ZONE);
}
return null;
}
}
很簡單, 不過要注意這個實現如果沒有zone設置, 則返回所有可用實例.
對應的配置聲明:
/**
* 自定義 Instance List Supplier: 根據默認Zone划分, 並且zone互相隔離.
*/
public class SameZoneOnlyCustomLoadBalancerConfiguration {
@Bean
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
DiscoveryClient discoveryClient, Environment env,
LoadBalancerZoneConfig zoneConfig,
ApplicationContext context) {
ServiceInstanceListSupplier delegate = new SameZoneOnlyServiceInstanceListSupplier(
new DiscoveryClientServiceInstanceListSupplier(discoveryClient, env),
zoneConfig
);
ObjectProvider<LoadBalancerCacheManager> cacheManagerProvider = context
.getBeanProvider(LoadBalancerCacheManager.class);
if (cacheManagerProvider.getIfAvailable() != null) {
return new CachingServiceInstanceListSupplier(
delegate,
cacheManagerProvider.getIfAvailable()
);
}
return delegate;
}
}
這里使用了緩存, 如果需要更多特性, 就去 LoadBalancerClientConfiguration 的源碼里去參悟吧, 還有什么 health-check, same-instance-preference很多東西可以參考.
C. beta絕對隔離, main在發布過程中可以切換到beta (main區域只有一台機器, 資源比較緊張)
這個也比較簡單, 僅有幾行代碼的差異
package com.cnscud.betazone.pub.samezone;
import com.cnscud.betazone.pub.LogUtils;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.config.LoadBalancerZoneConfig;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Beta只返回統一區域的實例, 其他區域如果為空則返回所有實例.
* @see org.springframework.cloud.loadbalancer.core.ZonePreferenceServiceInstanceListSupplier
* @see org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplierBuilder
*
*/
public class SameZoneSpecialBetaServiceInstanceListSupplier implements ServiceInstanceListSupplier {
private final String ZONE = "zone";
private String ZONE_BETA = "beta";
private final ServiceInstanceListSupplier delegate;
private final LoadBalancerZoneConfig zoneConfig;
private String zone;
public SameZoneSpecialBetaServiceInstanceListSupplier(ServiceInstanceListSupplier delegate,
LoadBalancerZoneConfig zoneConfig) {
this.delegate = delegate;
this.zoneConfig = zoneConfig;
}
@Override
public String getServiceId() {
return delegate.getServiceId();
}
@Override
public Flux<List<ServiceInstance>> get() {
return delegate.get().map(this::filteredByZone);
}
private List<ServiceInstance> filteredByZone(List<ServiceInstance> serviceInstances) {
if (zone == null) {
zone = zoneConfig.getZone();
}
if (zone != null) {
List<ServiceInstance> filteredInstances = new ArrayList<>();
for (ServiceInstance serviceInstance : serviceInstances) {
String instanceZone = getZone(serviceInstance);
if (zone.equalsIgnoreCase(instanceZone)) {
filteredInstances.add(serviceInstance);
}
}
//如果沒找到就返回空列表,絕不返回其他集群的實例
LogUtils.warn("find instances size: " + filteredInstances.size());
if(filteredInstances.size()>0) {
return filteredInstances;
}
else {
//如果是beta, 則返回空
if (zone.equalsIgnoreCase(ZONE_BETA)){
return filteredInstances;
}
}
}
//如果沒有zone設置, 則返回所有實例
return serviceInstances;
}
private String getZone(ServiceInstance serviceInstance) {
Map<String, String> metadata = serviceInstance.getMetadata();
if (metadata != null) {
return metadata.get(ZONE);
}
return null;
}
}
這個也沒啥特殊的.
D. 發布前標注一套區域為beta的服務, 測試通過后修改beta服務的區域為main, 多個實例都可上線服務 (資源非常緊張, 操作相對復雜)
這個就是個附加操作, 可以使用A,B,C任意一個方案, 搭配腳本就可以使用.
腳本已經准備好了:
本文源碼: https://github.com/cnscud/javaroom/tree/main/betazone2/hello-pubtool{:target="_blank"}
比較
灰度發布肯定還有很多方案, 但是對於作者來說, 根據zone來做分區灰度發布, 可能這是最簡單的一種方式了, 實現簡單, 通過Nginx做簡單的設置分流到2組網關上, 就可以實現2組實例了.
當然Nginx也可以根據header, cookie, URL來做分流,就看自己的需要了.
代碼簡單, 容易理解, 大道至簡!
后面還會實踐通過Header來分流的方法, 不過比較而言, zone-preference 還是最簡單的, 后面的實踐起來服務直接傳遞數據就是頭疼...