• 基于CSE的微服务工程实践-多微服务框架演进


    【摘要】 本文介绍了在微服务架构持续演进过程中,如何解决多种REST开发框架并存的问题。重点描述了网关在将请求转发给不同框架的微服务实例时,治理能力的差异;描述了CSE在提供第三方微服务访问方面的治理能力增强和开发。

    开发团队选择同样的开发框架能够更好的进行经验积累和知识共享,从而提高开发效率。在实际项目中,这个过程经常被打破。团队需要根据用户需求的变化,选择更好的开发框架来解决面对的新问题。每个开发团队都不得不采取“持续迭代演进”的方法,来改造旧系统,开发新系统。

    在[单体应用微服务改造实践]( https://bbs.huaweicloud.com/blogs/17ad483f325f11e9bd5a7ca23e93a891)中,分享了一种单体应用“持续迭代”改造微服务的方法。本文结合在改造过程中多微服务框架并存问题,描述一下使用CSE的开发实践。

    本文假设应用已经采用网关搭建可持续演进的架构,如下图。这个架构由多种开发框架构建的基于REST的微服务组成。

    image.png

    本文提供的示例项目代码托管在[github](https://github.com/huaweicse/cse-java-chassis-samples/tree/master/multi-framework) 。

    1      网关转发规则开发

    CSE提供的edge service默认提供的转发规则对于采用CSE开发的微服务最简单,功能也最强大。根据配置的URL规则,将请求转发到对应的微服务,只是网关核心功能之一。当edge service将请求转发到CSE的微服务的时候,还具备如下功能:

    ·实例发现和动态更新。网关可以通过微服务名称从服务中心发现实例列表,并在实例列表变化的时候,自动更新。

    ·实例隔离和重试。当微服务存在多个实例的时候,如果一个实例出现故障,那么可以进行一次重试,将请求转发到其他实例;如果一个实例多次出现故障,需要在后续请求中将该实例隔离一段时间,避免频繁失败。

    ·支持灰度发布。网关可以结合请求参数规则,或者版本规则,将请求转发到符合规则的实例上面去。比如将请求参数count>20的请求转发给v2版本的实例,count<=20的请求转发给v1版本的实例。

    ·支持AZ亲和。网关需要结合微服务部署的数据中心信息,将请求转发给就近的数据中心,以提高效率。

    ·支持流量控制。可以针对微服务、微服务的某个接口进行流量控制。

    ·自动按照接口兼容性转发。这个场景指的是如果微服务存在多个版本,比如v1存在接口/a/b/c,v2新增了接口/x/y/z,那么当用户请求/x/y/z的时候,能够自动将请求转发给v2,而不需要做任何配置。

    ·支持故障注入。故障注入用于模拟故障,这个在开发测试阶段比较有用。通过模拟延时等情况,能够测试网关在异常情况下是否正常工作。

    上面列的功能是edge service的部分功能,这些功能都是开箱即用,无需用户开发的。此外,CSE提供handler的机制,还实现了其他大量的开箱即用功能,在上述列表中没有给出。下面的例子实现了一个edge service转发给CSE服务的Dispatcher,CSE也默认提供了几个Dispatcher,本例子参考了默认的Dispatcher代码,定制了转发的URL。

    public class DefaultCseDispatcher extends AbstractEdgeDispatcher {
      @Override
      public void init(Router router) {
        // Dispatcher patterns. This dispatcher only forward requests to store.
        router.routeWithRegex("/store/(.*)").handler(CookieHandler.create());
        router.routeWithRegex("/store/(.*)").handler(createBodyHandler());
        router.routeWithRegex("/store/(.*)").failureHandler(this::onFailure).handler(this::onRequest);
      }
     
      protected void onRequest(RoutingContext context) {
        Map<String, String> pathParams = context.pathParams();
        String microserviceName = "store";
        String path = "/" + pathParams.get("param0");
     
        EdgeInvocation edgeInvocation = new EdgeInvocation();
        edgeInvocation.init(microserviceName, context, path, httpServerFilters);
        edgeInvocation.edgeInvoke();
      }
     
      @Override
      public int getOrder() {
        return 10000;
      }
    }

    Edge service可以通过Dispatcher扩展,将请求转发给非CSE开发的REST服务。并且提供了一些扩展API供开发者使用,使用这些扩展API,能够使用CSE提供的部分功能。

    其中:

    ·DiscoveryTree 集成了实例发现和动态更新、实例隔离、AZ亲和等功能。

    ·LoadbalanceHandler 集成了重试、灰度发布等功能。

    下面例子展示了将请求转发给auth服务。

    public class GenericHttpWithDiscoveryDispatcher extends AbstractEdgeDispatcher {
      private static Logger LOGGER = LoggerFactory.getLogger(GenericHttpWithDiscoveryDispatcher.class);
     
      private static Vertx vertx = VertxUtils.getOrCreateVertxByName("transport", null);
     
      private static HttpClient httpClient = vertx.createHttpClient(new HttpClientOptions());
     
      private DiscoveryTree discoveryTree = new DiscoveryTree();
     
      private LoadbalanceHandler loadbalanceHandler;
     
      class RetriableHandler implements Handler {
        private RoutingContext context;
     
        private String path;
     
        private Buffer data;
     
        private boolean isRetry = false;
     
        public RetriableHandler(RoutingContext context, String path) {
          this.context = context;
          this.path = path;
          this.context.response().setChunked(true);
        }
     
        @Override
        public void handle(Invocation invocation, AsyncResponse asyncResponse) throws Exception {
          URIEndpointObject endpoint = (URIEndpointObject) invocation.getEndpoint().getAddress();
          HttpClientRequest clietRequest =
              httpClient.request(context.request().method(),
                  endpoint.getPort(),
                  endpoint.getHostOrIp(),
                  "/" + path + "?" + context.request().query(),
                  clientResponse -> {
                    context.response().setStatusCode(clientResponse.statusCode());
                    VertxHttpHeaders headers = new VertxHttpHeaders();
                    clientResponse.headers().forEach(entry -> {
                      headers.add(entry.getKey(), entry.getValue());
                    });
                    context.response().headers().setAll(headers);
                    clientResponse.handler(data -> {
                      context.response().write(data);
                    });
                    clientResponse.endHandler((v) -> {
                      context.response().end();
                      asyncResponse.success("OK");
                    });
                  });
          clietRequest.headers().setAll(context.request().headers());
          clietRequest.exceptionHandler(e -> {
            asyncResponse.consumerFail(e);
          });
     
          if (!isRetry) {
            // data can not be read twice, so cache it in retry 
            this.context.request().handler(d -> {
              clietRequest.write(d);
              data = d;
            });
     
            context.request().endHandler((v) -> {
              clietRequest.end();
            });
     
            isRetry = true;
          } else {
            if (data != null) {
              clietRequest.write(data);
            }
            clietRequest.end();
          }
        }
      }
     
      public GenericHttpWithDiscoveryDispatcher() {
        discoveryTree.addFilter(new IsolationDiscoveryFilter());
        discoveryTree.addFilter(new ServerDiscoveryFilter());
        discoveryTree.sort();
        loadbalanceHandler = new LoadbalanceHandler(discoveryTree);
      }
     
      @Override
      public int getOrder() {
        return 10001;
      }
     
      @Override
      public void init(Router router) {
        // Dispatcher patterns. This dispatcher only forward requests to auth.
        String regex = "/auth/(.*)";
        router.routeWithRegex(regex).failureHandler(this::onFailure).handler(this::onRequest);
      }
     
      protected void onRequest(RoutingContext context) {
        Map<String, String> pathParams = context.pathParams();
     
        String microserviceName = "auth";
        String path = pathParams.get("param0");
     
        Invocation invocation =
            new NonSwaggerInvocation(RegistryUtils.getAppId(), microserviceName, "0+", new RetriableHandler(context, path));
        try {
          loadbalanceHandler.handle(invocation, resp -> {
            if (resp.isFailed()) {
              context.response().setStatusCode(resp.getStatusCode());
              context.response().write(((Exception) resp.getResult()).getMessage());
              context.response().end();
            }
          });
        } catch (Exception e) {
          LOGGER.error("", e);
        }
      }
     
    }

    由于auth服务采用Spring MVC开发,该服务在注册的时候,不会生成契约信息。因此上面列出的几个功能是不具备的:

    ·不支持流量控制。

    ·不支持自动按照接口兼容性转发。

    ·不支持故障注入。

    此外,CSE通过Handler扩展提供的其他开箱即用功能,也是不具备的。

    最后一个服务就是将请求转发给user了。user服务没有向服务中心注册,因此没有微服务信息,也没有契约信息。需要在代码里面写上实例列表和自己定义负载均衡。

    public class GenericHttpDispatcher extends AbstractEdgeDispatcher {
      private static Logger LOGGER = LoggerFactory.getLogger(GenericHttpDispatcher.class);
     
      private static Vertx vertx = VertxUtils.getOrCreateVertxByName("transport", null);
     
      private static HttpClient httpClient = vertx.createHttpClient(new HttpClientOptions());
     
      public GenericHttpDispatcher() {
      }
     
      @Override
      public int getOrder() {
        return 10001;
      }
     
      @Override
      public void init(Router router) {
        // Dispatcher patterns. This dispatcher only forward requests to user.
        String regex = "/user/(.*)";
        router.routeWithRegex(regex).failureHandler(this::onFailure).handler(this::onRequest);
      }
     
      protected void onRequest(RoutingContext context) {
        Map<String, String> pathParams = context.pathParams();
     
        String path = pathParams.get("param0");
     
        HttpClientRequest clietRequest =
            httpClient.request(context.request().method(),
                // hard coded ip/port here. can use configurations. 
                9093,
                "localhost",
                "/" + path + "?" + context.request().query(),
                clientResponse -> {
                  context.response().setStatusCode(clientResponse.statusCode());
                  VertxHttpHeaders headers = new VertxHttpHeaders();
                  clientResponse.headers().forEach(entry -> {
                    headers.add(entry.getKey(), entry.getValue());
                  });
                  context.response().headers().setAll(headers);
                  clientResponse.handler(data -> {
                    context.response().write(data);
                  });
                  clientResponse.endHandler((v) -> {
                    context.response().end();
                  });
                });
        
     
        clietRequest.headers().setAll(context.request().headers());
        clietRequest.exceptionHandler(e -> {
          LOGGER.error("", e);
        });
        context.request().handler(data -> {
          clietRequest.write(data);
        });
        context.request().endHandler((v) -> {
          clietRequest.end(); 
        });
      }
    }

    网关给user服务转发的过程中,没有上面列举的所有服务治理能力,只是一个单纯的HTTP转发器。

    2      CSE访问第三方服务

    在edge service转发给CSE服务的过程中,介绍了一组治理能力。这组治理能力在CSE服务调用其他CSE的服务的时候,也是具备的,而且这组能力不仅体现在调用者(consumer),提供者(provider)处理请求的时候,也会走handler链,从而具备流控、隔离等能力。 CSE之间的微服务调用非常简单,包括RestTemplate和RPC两种开发模式,这里不详细介绍,开发者可以参考开发指南进行学习。

    CSE也提供了调用第三方服务的能力,通过使用CSE提供的第三方能力,这些治理能力对于调用者(consumer)一端,也是全部具备的。因此在CSE服务中调用其他第三方服务的时候,应该尽可能使用CSE提供的第三方访问能力,而不是使用普通的HTTP client。

    使用CSE的http client的原理是首选通过接口的方式声明需要访问的服务端的契约。

    @RequestMapping(path = "/")
    public interface AuthService {
      @GetMapping(path = "/oauth/token")
      Token auth(@RequestParam(name = "username") String username, @RequestParam(name = "password") String password,
          @RequestParam(name = "grant_type") String grant_type,
          @RequestParam(name = "scope") String scope,
          @RequestParam(name = "client_id") String clientId,
          @RequestParam(name = "client_secret") String clientSecret);
    }

    然后本地模拟注册一个在服务中心注册的实例。

    @Component
    public class ThirdPartyRegistry implements BootListener {
     
      @Override
      public void onBootEvent(BootEvent event) {
        if (event.getEventType() == EventType.AFTER_REGISTRY) {
          DiscoveryTree discoveryTree = new DiscoveryTree();
          discoveryTree.addFilter(new IsolationDiscoveryFilter());
          discoveryTree.addFilter(new ServerDiscoveryFilter());
          discoveryTree.sort();
     
          DiscoveryContext context = new DiscoveryContext();
          Invocation invocation =
              new NonSwaggerInvocation("default", "auth", "0+", null);
          context.setInputParameters(invocation);
          VersionedCache serversVersionedCache = discoveryTree.discovery(context,
              "default",
              "auth",
              "0+");
          List<ServiceCombServer> servers = serversVersionedCache.data();
          List<MicroserviceInstance> instances = new ArrayList<>(servers.size());
          servers.forEach(item -> {
            MicroserviceInstance instance = new MicroserviceInstance();
            List<String> endpoints = item.getInstance().getEndpoints();
            List<String> endpointsWithoutPrefix = new ArrayList<>(endpoints.size());
            endpoints.forEach(e -> {
              System.out.println(e);
              endpointsWithoutPrefix.add(e.substring(0, e.indexOf("?")));
            });
            instance.setEndpoints(endpointsWithoutPrefix);
            instances.add(instance);
          });
          RegistryUtils.getServiceRegistry()
              .registerMicroserviceMapping(
                  "authStub",
                  "1.1.1",
                  instances,
                  AuthService.class);
        }
      }
    }

    最后就可以按照CSE访问CSE服务一样访问这个第三方服务。

      @RpcReference(microserviceName = "authStub", schemaId="authStub")
      private AuthService authService;
     
      @GetMapping("/auth")
      public Token auth() {
        return authService.auth("user_1", "123456", "password", "read", "client_2", "123456");
      }

    3      第三方服务访问CSE

    第三方访问CSE可以按照原生的第三方HTTP Client API来访问。 这个治理能力等同于第三方提供的功能,通常不包含治理能力。 这里不在详细描述。

    4      总结

    通过上面的例子,看到了CSE在处理多种REST开发框架并存情况下,网关转发和服务之间调用需要采用的技术。这些技术在微服务持续演进过程中会经常用到。但是微服务演进的最终目的是选择一套技术来解决问题,应该避免架构选型的多样化,这无疑会增加学习和维护的成本。同时可以看到CSE在服务治理能力方面的优势,这些优势对于微服务的可靠运行提供了非常强大的保障,这个才是CSE作为一个微服务框架核心重要的部分。

    来源:华为云社区原创  作者:liubao68

  • 相关阅读:
    SpringCloud分布式配置中心
    SpringCloud服务降级案列
    SpringCloud断路器(Hystrix)
    SpringCloud服务过滤filter
    SpringCloud路由网关Zuul
    SpringCloud微服务实现生产者消费者+ribbon负载均衡
    SpringCloud微服务的Eureka
    忘记MySQL密码以及无法登陆等解决办法
    MySQL备份
    实现两个MySQL数据库之间的主从同步
  • 原文地址:https://www.cnblogs.com/2020-zhy-jzoj/p/13165969.html
Copyright © 2020-2023  润新知