5c04c9652e48ee10d5466faf7878d2b14f56de87
\345\210\206\345\270\203\345\274\217\347\263\273\347\273\237\346\236\266\346\236\204\346\226\271\346\241\210\350\256\276\350\256\241\345\210\235\347\250\277.md
... | ... | @@ -286,61 +286,141 @@ public class ServiceRegistry { |
286 | 286 | |
287 | 287 | ``` |
288 | 288 | |
289 | -#### 2.2 MySQL 维护app和容器服务的关系 |
|
289 | +#### 2.2 在service的yaml文件中维护app和容器的关系 |
|
290 | 290 | |
291 | -在 MySQL 中维护app和其所在容器服务的关系。每个 Pod 中可在进程内缓存这些关系,并设置有效期(如 5 分钟)。如果缓存未过期但访问失败,则强制从 MySQL 读取,以确保数据的正确性。 |
|
291 | +在service的yaml文件中维护app和容器的关系。每个 Pod 中可在进程内缓存这些关系,并通过k8s提供的watch api实时监听和更新本地路由缓存。 |
|
292 | 292 | |
293 | 293 | 用途: |
294 | 294 | - 在访问时,直接读取app与容器的路由信息,进行访问,没有网络转发。 |
295 | -- 在安装和重启时,可以直接从 MySQL 获取完整的需要安装的app信息,没有各做一半的情况,无需协调和回调,独立可完成全部的安装和重启。 |
|
296 | -- 使用k8s CoreDNS 实现透明通信,支持 stream 模式,体验类似于单机版。 |
|
295 | +- 在安装和重启时,可以直接从 service yaml 文件中 获取完整的需要安装的app信息,没有各做一半的情况,无需协调和回调,独立可完成全部的安装和重启。 |
|
296 | +- 使用pod ip直接通信,实现透明通信,支持 stream 模式,体验类似于单机版。 |
|
297 | 297 | |
298 | 298 | |
299 | 299 | 此外,本地缓存可通过 watch 功能主动更新,防止无意义、尝试性的操作。可以参考以下示例代码实现 watch 功能: |
300 | 300 | ```java |
301 | -public class WatchExample { |
|
302 | - public static void main(String[] args) throws IOException, ApiException { |
|
303 | - ApiClient client = Config.defaultClient(); |
|
304 | - // infinite timeout |
|
305 | - OkHttpClient httpClient = |
|
306 | - client.getHttpClient().newBuilder().readTimeout(0, TimeUnit.SECONDS).build(); |
|
307 | - client.setHttpClient(httpClient); |
|
308 | - Configuration.setDefaultApiClient(client); |
|
309 | - |
|
310 | - CoreV1Api api = new CoreV1Api(); |
|
311 | - |
|
312 | - Watch<V1Namespace> watch = |
|
313 | - Watch.createWatch( |
|
314 | - client, |
|
315 | -// api.listNamespaceCall( |
|
316 | -// null, null, null, null, null, 5, null, null, null, Boolean.TRUE, null), |
|
317 | -// new TypeToken<Watch.Response<V1Namespace>>() {}.getType()); |
|
318 | - |
|
319 | - api.listServiceForAllNamespacesCall( |
|
320 | - null, null, null, |
|
321 | - null, null, null, null, |
|
322 | - null, null, Boolean.TRUE, null), |
|
323 | - new TypeToken<Watch.Response<V1Namespace>>() {}.getType()); |
|
324 | - |
|
325 | - try { |
|
326 | - for (Watch.Response<V1Namespace> item : watch) { |
|
327 | - // 打印service的操作类型、名称 |
|
328 | - System.out.printf("%s : %s%n", item.type, item.object.getMetadata().getName()); |
|
329 | - } |
|
330 | - } finally { |
|
331 | - watch.close(); |
|
301 | + |
|
302 | + public static void watchServicesInNamespace(BiConsumer<AppRoute.EventType, Map<String, String>> updateRouteCache) { |
|
303 | + SharedInformerFactory informerFactory = new SharedInformerFactory(apiClient); |
|
304 | + |
|
305 | + SharedIndexInformer<V1Service> serviceInformer = informerFactory.sharedIndexInformerFor( |
|
306 | + (callGeneratorParams) -> coreApi.listNamespacedServiceCall( |
|
307 | + NAMESPACE, |
|
308 | + null, |
|
309 | + null, |
|
310 | + null, |
|
311 | + null, |
|
312 | + K8sConstants.LABEL_SELECTOR_IIDP, |
|
313 | + null, |
|
314 | + "", |
|
315 | + null, |
|
316 | + callGeneratorParams.timeoutSeconds, |
|
317 | + callGeneratorParams.watch, |
|
318 | + null), |
|
319 | + V1Service.class, |
|
320 | + V1ServiceList.class); |
|
321 | + serviceLister = new Lister<>(serviceInformer.getIndexer(), NAMESPACE); |
|
322 | + |
|
323 | + SharedIndexInformer<V1Endpoints> endpointsInformer = informerFactory.sharedIndexInformerFor( |
|
324 | + (callGeneratorParams) -> coreApi.listNamespacedEndpointsCall( |
|
325 | + NAMESPACE, |
|
326 | + null, |
|
327 | + null, |
|
328 | + null, |
|
329 | + null, |
|
330 | + K8sConstants.LABEL_SELECTOR_IIDP, |
|
331 | + null, |
|
332 | + "", |
|
333 | + null, |
|
334 | + callGeneratorParams.timeoutSeconds, |
|
335 | + callGeneratorParams.watch, |
|
336 | + null), |
|
337 | + V1Endpoints.class, |
|
338 | + V1EndpointsList.class); |
|
339 | + endpointsLister = new Lister<>(endpointsInformer.getIndexer(), NAMESPACE); |
|
340 | + |
|
341 | + serviceInformer.addEventHandler( |
|
342 | + new ResourceEventHandler<V1Service>() { |
|
343 | + @Override |
|
344 | + public void onAdd(V1Service obj) { |
|
345 | + V1ObjectMeta metadata = obj.getMetadata(); |
|
346 | + if (metadata == null) { |
|
347 | + return; |
|
348 | + } |
|
349 | + logger.info("\n\n ==================== Service added: {} \n\n", metadata.getName()); |
|
350 | + // 更新路由缓存 |
|
351 | + updateRouteCache.accept(AppRoute.EventType.ADDED, getRouteMap(metadata)); |
|
352 | + } |
|
353 | + |
|
354 | + @Override |
|
355 | + public void onUpdate(V1Service oldObj, V1Service newObj) { |
|
356 | + logger.info("\n\n ==================== Service updated: {}\n\n", newObj.getMetadata().getName()); |
|
357 | + // 有新的app加入则会更新service,需要同步到路由缓存 |
|
358 | + updateRouteCache.accept(AppRoute.EventType.UPDATED, getRouteMap(newObj.getMetadata())); |
|
359 | + } |
|
360 | + |
|
361 | + @Override |
|
362 | + public void onDelete(V1Service obj, boolean deletedFinalStateUnknown) { |
|
363 | + V1ObjectMeta metadata = obj.getMetadata(); |
|
364 | + if (metadata == null) { |
|
365 | + return; |
|
366 | + } |
|
367 | + |
|
368 | + logger.info("\n\n ===================== Service deleted: {} \n\n", metadata.getName()); |
|
369 | + |
|
370 | + updateRouteCache.accept(AppRoute.EventType.DELETED, getRouteMap(metadata)); |
|
371 | + } |
|
372 | + |
|
373 | + private Map<String, String> getRouteMap(V1ObjectMeta metadata) { |
|
374 | + Map<String, String> annotations = metadata.getAnnotations(); |
|
375 | + if (annotations == null) { |
|
376 | + return null; |
|
377 | + } |
|
378 | + String installedApps = annotations.get(K8sConstants.ANNOTATION_INSTALLED_APPS); |
|
379 | + if (installedApps == null) { |
|
380 | + return null; |
|
381 | + } |
|
382 | + |
|
383 | + Map<String, String> routeMap = new HashMap<>(); |
|
384 | + String serviceName = metadata.getName(); |
|
385 | + // 按逗号切割,并去掉两边的空格 |
|
386 | + String[] apps = installedApps.split(","); |
|
387 | + for (String app : apps) { |
|
388 | + routeMap.put(app.trim(), serviceName); |
|
389 | + } |
|
390 | + |
|
391 | + String dependencies = annotations.get(K8sConstants.ANNOTATION_DEPENDENCIES); |
|
392 | + if (dependencies != null && !dependencies.isEmpty()) { |
|
393 | + String[] deps = dependencies.split(","); |
|
394 | + for (String dep : deps) { |
|
395 | + routeMap.put(dep.trim(), metadata.getName()); |
|
396 | + } |
|
397 | + } |
|
398 | + |
|
399 | + return routeMap; |
|
400 | + } |
|
401 | + }); |
|
402 | + |
|
403 | + informerFactory.startAllRegisteredInformers(); |
|
404 | + |
|
405 | + // 使用 Wait.poll 方法等待 Informer 同步完成 |
|
406 | + boolean synced = Wait.poll(Duration.ofMillis(100), Duration.ofSeconds(10), serviceInformer::hasSynced); |
|
407 | + if (synced) { |
|
408 | + logger.info("Cache fully loaded (total {} services), discovery client is now available", serviceLister.list().size()); |
|
409 | + } else { |
|
410 | + logger.error("Failed to sync informer within the timeout period."); |
|
411 | + throw new RuntimeException("Failed to sync informer within the timeout period."); |
|
412 | + } |
|
332 | 413 | } |
333 | - } |
|
334 | -} |
|
414 | + |
|
335 | 415 | ``` |
336 | -通过 watch 服务变化,及时主动更新内存中的 service 和应用对应关系,确保数据的一致性和实时性。 |
|
416 | +通过 watch 服务变化,及时主动更新内存中的 service 和app对应关系,确保数据的一致性和实时性。 |
|
337 | 417 | |
338 | 418 | |
339 | 419 | |
340 | 420 | #### 2.3 安装和卸载应用的一致性 |
341 | 421 | |
342 | 422 | |
343 | -安装和卸载功能可以由一个独立的内置app管理和创建容器。为了确保一致性,可以采用以下策略: |
|
423 | +安装和卸载功能都由master节点进行管理和创建容器。为了确保一致性,可以采用以下策略: |
|
344 | 424 | |
345 | 425 | 1. 先写入 MySQL,如果出现错误则直接报错给用户,提示重试。 |
346 | 426 | 2. 成功写入 MySQL 后,再创建容器。 |
... | ... | @@ -434,10 +514,10 @@ k8s已经给出了答案,通过operaotr即可做到 [[https://kubernetes.io/zh |
434 | 514 | |
435 | 515 | 通过上述改进方案,可以解决当前架构中的多个问题: |
436 | 516 | |
437 | -- 减少不必要的网络转发,提升系统性能。 |
|
517 | +- 减少不必要的网络转发,直接使用pod ip进行通信,提升系统性能。 |
|
438 | 518 | - 实现确定性的路由,避免死循环。 |
439 | 519 | - 简化应用安装和卸载过程,减少协调和回调,提高效率。 |
440 | -- 使用网关和 MySQL 维护应用和容器服务关系,确保数据一致性和高可用性。 |
|
520 | +- 使用service yaml文件维护应用和容器服务关系,确保数据一致性,只要容器在就能保证需要安装的app。 |
|
441 | 521 | - 不需要依赖hazelcast组网来进行内存同步,简化同步流程,确保元模型数据一致性和正确性。 |
442 | 522 | - 引入 Operator 模式,实现自动化管理和一致性操作k8s资源。 |
443 | 523 |