|
1 | | -## 灰度发布 |
2 | | -灰度发布是在多版本控制的基础上进一步扩展实现出来的项目 -> fm-cloud-graybunny,抽象出灰度服务、灰度服务实例、灰度策略、灰度决策等。支持A/B test, 金丝雀 test。 灰度策略可以从request ip, request patameter, request header等方面进行去创建,也可以根据bamboo的LoadBalanceRequestTrigger结合graybuanny的接口去扩展灰度策略和灰度决策。 |
3 | | - |
4 | | -##### 场景 |
5 | | - |
6 | | -有两个服务,共四个服务实例,分别是ServiceA-1, ServiceA-2, ServiceB-1。其中ServiceA-2是灰度实例。 |
7 | | -场景1:所有请求头usertype:old,ip:10.217.***.***的请求或者请求头usertype:test, url 参数action:create的请求,都会被转发到的灰度服务ServiceA-2 。 |
8 | | -场景2:ServiceA-2通过一段时间的观察,判定运行稳定,开始ServiceA-2删除灰度标记,开始和ServiceA-1一样会加入正常的负载均衡规则当中。 |
9 | | -场景3:服务ServiceB发布新版本,ServiceB-2需要灰度注册,注册成功后所有的请求不能转发到ServiceB-2, 在为ServiceB-2设置灰度策略后,符合策略的请求才会被转发到ServiceB-2上。 |
10 | | - |
11 | | - |
12 | | -##### 思路 |
13 | | - |
14 | | -从上面的场景分析,可以归纳出两个对象:服务实例和调用请求;服务实例的灰度管理是基础,调用请求时如何决策路由,都是根据服务实例的灰度策略去判断的。既然有灰度管理这个概念,那么从功能上分,就会有client-server之分,所以又可以从gray-client和gray-server去分析。接下来将一步一步去分析这四个方面。 |
15 | | - |
16 | | -* 灰度实例 |
17 | | - |
18 | | - |
19 | | - |
20 | | -实例注册:服务实例添加到灰度管理中。 |
21 | | -实例下线:服务实例下线,从灰度管理中删除。 |
22 | | -灰度开关:调整服务实例的灰度状态,有启用、禁用两个状态,禁用的实例不纳入灰度列表中。 |
23 | | -灰度策略:请求是否可以被转发到该服务实例的条件,只有通过,请求才有可能会被转发到该实例上。 |
24 | | - |
25 | | -* 调用请求 |
26 | | - |
27 | | - |
28 | | - |
29 | | -灰度决策:根据请求的信息去匹配灰度服务实例的灰度策略,如果匹配上,会将服务实例加入到通过列表中。如果都没有匹配上,就按bamboo的路由规则去筛选非灰度的服务实例进行转发。 |
30 | | - |
31 | | - |
32 | | -* 灰度客户端 |
33 | | - |
34 | | -调用请求的服务消费者,和提供服务的服务提供者都可以是灰度客户端,因为微服务中,大多服务实例既是服务提供者,同时也是服务消费者。 |
35 | | - |
36 | | - |
37 | | - |
38 | | -灰度服务注册:服务实例在启动时,就会向灰度服务端发起请求,将实例自身的灰度开关打开。 |
39 | | -灰度服务下线:在服务实例下线前,会触发钩子,向灰度服务端发起请求将实例自身从灰度列表中删除。 |
40 | | -接收灰度实例调整消息:接收由灰度服务端推送过来的灰度列表更新消息比如新增灰度实例,删除灰度实例等,维护缓存在实例上的灰度列表。 |
41 | | -定时拉取灰度列表:定时从灰度服务端拉取最新的灰度列表,维护实例自身缓存的灰度列表。 |
42 | | - |
43 | | -* 灰度服务端 |
44 | | - |
45 | | -灰度服务端负表维护灰度列表,可以新增、删除、编辑灰度信息。 |
46 | | - |
47 | | - |
48 | | - |
49 | | -编辑灰度实例:新增灰度实例,删除灰度实例,修改实例灰度状态。 |
50 | | -编辑灰度策略:新增实例灰度策略,删除实例灰度策略,修改灰度策略状态。 |
51 | | -推送灰度服务调整消息:向灰度客户端推送灰度列表变动消息,比如新增灰度实例,删除灰度实例,修改实例灰度状态等。 |
52 | | -定时检查服务实例是否下线:定时检查灰度服务实例是否下线,下线的的实例将从灰度列表中删除。 |
53 | | - |
54 | | - |
55 | | - |
56 | | -##### 代码设计 |
57 | | -根据上面的思路,设计以下对象和接口。共6个接口,4个模型对象。 |
58 | | - |
59 | | - |
60 | | - |
61 | | -对象: |
62 | | -* GrayService: 灰度服务 |
63 | | -* GrayInstance: 灰度实例,有状态属性 |
64 | | -* GrayPolicyGroup: 灰度策略组,有状态属性 |
65 | | -* GrayPolicy: 灰度策略 |
66 | | - |
67 | | -接口: |
68 | | -* GrayManager: |
69 | | -灰度客户端管理器,维护灰度列表,维护自身灰度状态,创建灰度决策对象。抽象实现类AbstractGrayManager实现了基础的获取灰度列表, 创建灰度决策对象的能力。BaseGrayManger在期基础上进行了扩展,将灰度列表缓存起来,定时从灰度服务端更新灰度列表。 |
70 | | - |
71 | | -* InformationClient: |
72 | | -该接口主要是负责和灰度服务端进行通信,获取灰度列表,编辑灰度实例等能力。其实现类HttpInformationClient默认使用http方式访问灰度服务端。 |
73 | | -子类InformationClientDecorator是一个适配器类,RetryableInformationClient继承了InformationClientDecorator类,实现了重试的功能。 |
74 | | - |
75 | | -* GrayDecision: |
76 | | -该接口是灰度决策,用来判断请求是否匹配灰度策略。实现了ip匹配、request parameter匹配、request header匹配、BambooRequestContext中的参数匹配器以及合并匹配等多个匹配能力。 |
77 | | - |
78 | | -* GrayDecisionFactory: |
79 | | -灰度决策的工厂类,其默认实现类支持上述几种灰度决策的创建。 |
80 | | - |
81 | | -* GrayServiceManager: |
82 | | -灰度服务管理类,属于服务端的类。主要是编辑服务实例,编辑灰度策略,以及维护最新的灰度列表。 |
83 | | - |
84 | | -* GrayBunnyServerEvictor: |
85 | | -如果灰度服务实例下线后, 由于意外情况,没有向灰度服务端发送删除请求, 服务端会每隔一段时间调用该接口的方法,检查灰度列表中的实例是否下线,如果实例已下线,就将其从灰度列表中删除。 |
86 | | - |
87 | | - |
88 | | -##### 代码实现 |
89 | | -将模型抽象成接口和对象设计出来之后,实现思路就清晰了。 |
90 | | - |
91 | | -* 灰度路由: |
92 | | -灰度路由是客户端必须要实现的能力,gray是在bamboo的基础上扩展的,所以gray的路由规则对象GrayLoadBalanceRule继承了BambooZoneAvoidanceRule, |
93 | | -逻辑是这样的: |
94 | | -1、 判断目标服务是否有灰度实例。 |
95 | | -2.1、 如果没有, 执行父类逻辑。结束。 |
96 | | -2.2、 有灰度实例,先将灰度实例和非灰度实例筛选出来。 |
97 | | -3、 挑选灰度实例, 筛选调用请求匹配上灰度实例的策略。 |
98 | | -4.1、 如果没有匹配的灰度实例, 将非灰度实例列表传递过去执行父类的筛选逻辑。结束。 |
99 | | -4.2、 如果有匹配的灰度实例, 从其中按轮询的方式挑选出一个实例。结束。 |
100 | | -```java |
101 | | -/** |
102 | | - * 灰度发布的负载规则 |
103 | | - */ |
104 | | -public class GrayLoadBalanceRule extends BambooZoneAvoidanceRule { |
105 | | - |
106 | | - protected CompositePredicate grayCompositePredicate; |
107 | | - |
108 | | - public GrayLoadBalanceRule() { |
109 | | - super(); |
110 | | - GrayDecisionPredicate apiVersionPredicate = new GrayDecisionPredicate(this); |
111 | | - grayCompositePredicate = CompositePredicate.withPredicates(super.getPredicate(), |
112 | | - apiVersionPredicate).build(); |
113 | | - } |
114 | | - |
115 | | - |
116 | | - @Override |
117 | | - public Server choose(Object key) { |
118 | | - ILoadBalancer lb = getLoadBalancer(); |
119 | | - BambooRequestContext requestContext = BambooRequestContext.currentRequestCentxt(); |
120 | | - if (requestContext != null && getGrayManager().isOpen(requestContext.getServiceId())) { |
121 | | - GrayService grayService = getGrayManager().grayService(requestContext.getServiceId()); |
122 | | - List<Server> servers = lb.getAllServers(); |
123 | | - List<Server> grayServers = new ArrayList<>(grayService.getGrayInstances().size()); |
124 | | - List<Server> normalServers = new ArrayList<>(servers.size() - grayService.getGrayInstances().size()); |
125 | | - for (Server server : servers) { |
126 | | - DiscoveryEnabledServer disServer = (DiscoveryEnabledServer) server; |
127 | | - if (grayService.getGrayInstance(disServer.getInstanceInfo().getInstanceId()) != null) { |
128 | | - grayServers.add(server); |
129 | | - } else { |
130 | | - normalServers.add(server); |
131 | | - } |
132 | | - } |
133 | | - |
134 | | - Optional<Server> server = grayCompositePredicate.chooseRoundRobinAfterFiltering(grayServers, key); |
135 | | - if (server.isPresent()) { |
136 | | - return server.get(); |
137 | | - } else { |
138 | | - return choose(super.getPredicate(), normalServers, key); |
139 | | - } |
140 | | - } |
141 | | - return super.choose(key); |
142 | | - } |
143 | | - |
144 | | - |
145 | | - private Server choose(AbstractServerPredicate serverPredicate, List<Server> servers, Object key) { |
146 | | - Optional<Server> server = serverPredicate.chooseRoundRobinAfterFiltering(servers, key); |
147 | | - if (server.isPresent()) { |
148 | | - return server.get(); |
149 | | - } else { |
150 | | - return null; |
151 | | - } |
152 | | - } |
153 | | - |
154 | | - |
155 | | - public GrayManager getGrayManager() { |
156 | | - return GrayClientAppContext.getGrayManager(); |
157 | | - } |
158 | | -} |
159 | | - |
160 | | -``` |
161 | | - |
162 | | -灰度决策的执行代码在GrayDecisionPredicate中 |
163 | | - |
164 | | -```java |
165 | | -public class GrayDecisionPredicate extends AbstractServerPredicate { |
166 | | - |
167 | | - public GrayDecisionPredicate(GrayLoadBalanceRule rule) { |
168 | | - super(rule); |
169 | | - } |
170 | | - |
171 | | - @Override |
172 | | - public boolean apply(PredicateKey input) { |
173 | | - BambooRequestContext bambooRequestContext = BambooRequestContext.currentRequestCentxt(); |
174 | | - if (bambooRequestContext == null || bambooRequestContext.getBambooRequest() == null) { |
175 | | - return false; |
176 | | - } |
177 | | - DiscoveryEnabledServer server = (DiscoveryEnabledServer) input.getServer(); |
178 | | - BambooRequest bambooRequest = bambooRequestContext.getBambooRequest(); |
179 | | - List<GrayDecision> grayDecisions = |
180 | | - getIRule().getGrayManager().grayDecision(bambooRequest.getServiceId(), server.getInstanceInfo().getInstanceId()); |
181 | | - for (GrayDecision grayDecision : grayDecisions) { |
182 | | - if (grayDecision.test(bambooRequest)) { |
183 | | - return true; |
184 | | - } |
185 | | - } |
186 | | - return false; |
187 | | - } |
188 | | - |
189 | | - |
190 | | - protected GrayLoadBalanceRule getIRule() { |
191 | | - return (GrayLoadBalanceRule) this.rule; |
192 | | - } |
193 | | -} |
194 | | -``` |
195 | | - |
196 | | -* 灰度管理: |
197 | | -灰度管理是灰度服务端的功能,主要是维护灰度列表。其实现类DefaultGrayServiceManager有一个Map, 用来维护GrayService,key是service id。并且每隔一段时间就调用EurekaGrayServerEvictor,检查列表中的实例是否下线,将下线的服务从灰度列表中删除。 |
198 | | - |
199 | | -```java |
200 | | -public class DefaultGrayServiceManager implements GrayServiceManager { |
201 | | - |
202 | | - |
203 | | - private Map<String, GrayService> grayServiceMap = new ConcurrentHashMap<>(); |
204 | | - private Timer evictionTimer = new Timer("Gray-EvictionTimer", true); |
205 | | - |
206 | | - //... |
207 | | - |
208 | | - @Override |
209 | | - public void openForWork() { |
210 | | - evictionTimer.schedule(new EvictionTask(), |
211 | | - serverConfig.getEvictionIntervalTimerInMs(), |
212 | | - serverConfig.getEvictionIntervalTimerInMs()); |
213 | | - } |
214 | | - |
215 | | - @Override |
216 | | - public void shutdown() { |
217 | | - evictionTimer.cancel(); |
218 | | - } |
219 | | - |
220 | | - |
221 | | - protected void evict() { |
222 | | - GrayServerContext.getGrayServerEvictor().evict(this); |
223 | | - } |
224 | | - |
225 | | - |
226 | | - class EvictionTask extends TimerTask { |
227 | | - |
228 | | - @Override |
229 | | - public void run() { |
230 | | - evict(); |
231 | | - } |
232 | | - } |
233 | | - |
234 | | -} |
235 | | - |
236 | | -``` |
237 | | - |
238 | | -EurekaGrayServerEvictor是依赖EurekaClient来检查服务实例是否下线。 |
239 | | - |
240 | | -```java |
241 | | -public class EurekaGrayServerEvictor implements GrayServerEvictor { |
242 | | - |
243 | | - private EurekaClient eurekaClient; |
244 | | - |
245 | | - |
246 | | - public EurekaGrayServerEvictor(EurekaClient eurekaClient) { |
247 | | - this.eurekaClient = eurekaClient; |
248 | | - } |
249 | | - |
250 | | - @Override |
251 | | - public void evict(GrayServiceManager serviceManager) { |
252 | | - Collection<GrayService> grayServices = serviceManager.allGrayService(); |
253 | | - grayServices.forEach(grayService -> { |
254 | | - grayService.getGrayInstances().forEach(grayInstance -> { |
255 | | - evict(serviceManager, grayInstance); |
256 | | - }); |
257 | | - }); |
258 | | - |
259 | | - } |
260 | | - |
261 | | - |
262 | | - private void evict(GrayServiceManager serviceManager, GrayInstance grayInstance) { |
263 | | - if (isDownline(grayInstance)) { |
264 | | - serviceManager.deleteGrayInstance(grayInstance.getServiceId(), grayInstance.getInstanceId()); |
265 | | - } |
266 | | - } |
267 | | - |
268 | | - |
269 | | - private boolean isDownline(GrayInstance grayInstance) { |
270 | | - Application app = eurekaClient.getApplication(grayInstance.getServiceId()); |
271 | | - return app == null || app.getByInstanceId(grayInstance.getInstanceId()) == null; |
272 | | - } |
273 | | - |
274 | | -} |
275 | | -``` |
276 | | - |
277 | | - |
278 | | - |
279 | | -##### 使用说明 |
280 | | -灰度发布 --> [spring-cloud-gray-samples](../spring-cloud-gray-samples/README.md) |
0 commit comments