基于docker部署的微服务架构(三): 服务网关

前言

在微服务架构中,后端服务往往不直接开放给调用端,而是通过一个公共网关根据请求的url,路由到相应的服务。
在网关中可以做一些服务调用的前置处理,比如权限验证。也可以通过动态路由,提供多个版本的api接口。
spring cloud 提供的技术栈中,使用netflix zuul来作为服务网关。

创建服务网关

新建一个maven工程,修改pom.xml引入 spring cloud 依赖:


    org.springframework.boot
    spring-boot-starter-parent
    1.4.2.RELEASE



    
        org.springframework.cloud
        spring-cloud-starter-zuul
    
    
        org.springframework.cloud
        spring-cloud-starter-eureka
    



    
        
            org.springframework.cloud
            spring-cloud-dependencies
            Camden.SR2
            pom
            import
        
    

resources 目录中创建 application.yml 配置文件,在配置文件内容:

spring:
  application:
    name: @project.artifactId@

server:
  port: 80

eureka:
  client: 
    serviceUrl:
      defaultZone: http://localhost:8000/eureka/

zuul:
  routes:
    add-service-demo:
      path: /add-service/**
      serviceId: add-service-demo

这里主要关注 zuul 相关的配置,path 定义了需要路由的url,serviceId 和注册中心中的application 相对应,定义了路由到哪个服务。(这里 serviceId 应该换行和 path 同级,oschina 的markdown格式显示有问题)
java 目录中创建一个包 demo ,在包中创建启动入口 ServiceGatewayApplication.java

@EnableDiscoveryClient
@SpringBootApplication
@EnableZuulProxy
public class ServiceGatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(ServiceGatewayApplication.class, args);
    }

}

到这里一个简单的服务网关就配置好了,先启动注册中心 service-registry-demo,然后启动两个 add-service-demo 工程,分别映射到 81008101 端口,然后再启动刚刚配置好的服务网关 service-gateway-demo
启动完成后访问注册中心页面 http://localhost:8000,可以看到注册了两个 add-service-demo 和一个 service-gateway-demo

基于docker部署的微服务架构(三): 服务网关_第1张图片
服务注册中心

在浏览器中访问 http://localhost/add-service/add?a=1&b=2,可以看到返回结果:

{
    msg: "操作成功",
    result: 3,
    code: 200
}

多次访问,查看 add-service-demo 的控制台输出,可以看到服务网关对请求分发做了负载均衡。

使用corsFilter解决前端跨域问题

在对外提供rest接口时,经常会遇到跨域问题,尤其是使用前后端分离架构时。
可以在服务端使用cors技术,解决前端的跨域问题。这里我们在网关层解决跨域问题。
修改 ServiceGatewayApplication.java ,增加一个CorsFilter,代码如下:

[@Bean](https://my.oschina.net/bean)
public CorsFilter corsFilter() {
    final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    final CorsConfiguration config = new CorsConfiguration();
    config.setAllowCredentials(true);
    config.addAllowedOrigin("*");
    config.addAllowedHeader("*");
    config.addAllowedMethod("OPTIONS");
    config.addAllowedMethod("HEAD");
    config.addAllowedMethod("GET");
    config.addAllowedMethod("PUT");
    config.addAllowedMethod("POST");
    config.addAllowedMethod("DELETE");
    config.addAllowedMethod("PATCH");
    source.registerCorsConfiguration("/**", config);
    return new CorsFilter(source);
}

这样服务端的接口就可以支持跨域访问了。

使用自定义filter过滤请求

自定义filter也很简单,只需要继承 ZuulFilter 就可以了。
在demo包下新建一个filter的子包,用来存放自定义的filter类。新建一个filter类 MyFilter 继承 ZuulFilter

@Component
public class MyFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();

        String ip = getIpAddr(request);

        System.out.println("收到来自IP为: '" + ip + "'的请求");
        return null;
    }

    private String getIpAddr(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

这个filter的逻辑很简单,就是从 HttpServletRequest 对象中获取请求IP,并打印出来。
然后在 ServiceGatewayApplication.java 中增加 MyFilter 的配置:

@Bean
public MyFilter myFilter() {
    return new MyFilter();
}

重新启动 service-gateway-demo,再次发起请求,会看到控制台打印的IP。

我们回过头看一下,MyFilter 这个类从 ZuulFilter 继承之后做了哪些处理。
首先覆写 ZuulFilter 中的4个方法,分别为 filterTypefilterOrdershouldFilterrun。这4个方法看名字就知道有什么作用:

  • filterType 定义了filter的类型
    • pre 表示在请求被路由之前调用
    • route 请求被路由时调用,时机比pre
    • post 在路由完成后调用
    • error 发生错误时调用
  • filterOrder 定义过滤器的执行顺序,值小的先执行
  • shouldFilter 是否需要过滤
  • run 过滤器的具体执行逻辑

demo源码 spring-cloud-1.0/service-gateway-demo

使用docker-maven-plugin打包并生成docker镜像

复制 application.yml,重命名为 application-docker.yml,修改 defaultZone为:

eureka:
  client:
    serviceUrl:
      defaultZone: http://service-registry:8000/eureka/

这里修改了 defaultZone 的访问url,如何修改取决于部署docker容器时的 --link 参数, --link 可以让两个容器之间互相通信。

修改 application.yml 中的 spring 节点为:

spring:
  application:
    name: @project.artifactId@
  profiles:
    active: @activatedProperties@

这里增加了 profiles 的配置,在maven打包时选择不同的profile,加载不同的配置文件。

在pom.xml文件中增加:


    1.8 
    
    
        registry.cn-hangzhou.aliyuncs.com/ztecs
    
    
    demo

    
    



    
    
        docker

        
            docker
            docker-demo-${project.version}
        
    



    install
    ${project.artifactId}

    
        
            src/main/resources
            true
        
    

    
        
        
            org.springframework.boot
            spring-boot-maven-plugin
            
                true
            
        

        
        
            org.apache.maven.plugins
            maven-surefire-plugin
            
                true
            
        

        
        
            com.spotify
            docker-maven-plugin
            0.4.13
            
                
                    install
                    
                        build
                        tag
                    
                
            
            
                
                http://docker节点ip:2375
                ${docker.image.prefix}/${project.build.finalName}
                java
                
                
                    ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/${project.build.finalName}.jar"]
                
                
                    
                        /
                        ${project.build.directory}
                        ${project.build.finalName}.jar
                    
                
                ${docker.image.prefix}/${project.build.finalName}
                ${docker.image.prefix}/${project.build.finalName}:${docker.tag}
                true
                
                false
            
        
    

选择 docker profile,运行 mvn install -P docker ,打包项目并生成docker镜像,注意docker-maven-plugin中的 标签里的内容不能换行,否则在生成docker镜像的时候会报错
运行成功后,登录docker节点,运行 docker images 应该可以看到刚才打包生成的镜像了。

启动docker容器并注册服务

在前一篇中,已经启动了 service-registry-demoadd-service-demo,并且在两个容器之间建立了连接。这里启动 service-gateway-demo

docker run -d --name service-gateway-demo --publish 80:80 --link service-registry-demo:service-registry \
 --link add-service-demo --volume /etc/localtime:/etc/localtime \
 registry.cn-hangzhou.aliyuncs.com/ztecs/service-gateway-demo:docker-demo-1.0

这里比起上一篇启动 add-service-demo 容器的命令,有两个 --link ,分别连接了 service-registry-demoadd-service-demo,因为服务网关不仅需要注册到服务注册中心,还需要和后端提供的服务进行连接。
启动完成之后,访问注册中心的页面 http://宿主机IP:8000 查看服务注册信息,可以发现 service-gateway-demo 也注册成功了。
这时候就可以通过网关访问 add-service-demo 提供的服务了。
注意:在前一篇启动 add-service-demo 时使用了 --publish把端口映射到了宿主机,在部署服务网关的情况下,后端服务就不需要映射到宿主机了,所有对服务的访问都通过网关进行路由,避免透过网关直接访问。
可以把3条启动命令封装到一个shell里:

docker run -d --name service-registry-demo --publish 8000:8000 \
 --volume /etc/localtime:/etc/localtime \
 registry.cn-hangzhou.aliyuncs.com/ztecs/service-registry-demo:docker-demo-1.0

echo 'sleep 30s to next step...'
sleep 30s

docker run -d --name add-service-demo --link service-registry-demo:service-registry \
 --volume /etc/localtime:/etc/localtime \
 registry.cn-hangzhou.aliyuncs.com/ztecs/add-service-demo:docker-demo-1.0

docker run -d --name service-gateway-demo --publish 80:80 --link service-registry-demo:service-registry \
 --link add-service-demo --volume /etc/localtime:/etc/localtime \
 registry.cn-hangzhou.aliyuncs.com/ztecs/service-gateway-demo:docker-demo-1.0

这里的 sleep 30s 是为了让 service-registry-demo 启动完成之后再启动 add-service-demoservice-gateway-demo
在启动完成之后,通过网关访问接口时,可能会报错
Load balancer does not have available server for client: add-service-demo
这是因为 service-gateway-demoadd-service-demo 同时启动,service-gateway-demo 在向注册中心注册时,add-service-demo 可能还没有来得及注册,导致 service-gateway-demo 获取不到 add-service-demo 的注册信息,过个几十秒再访问就可以了。

最后

目前我们已经成功搭建了 服务注册中心服务网关后端服务,也创建了两个服务调用者 ribbonfeign
配置中心断路器 还没有涉及,配置中心 由于 spring cloud bus 需要用到消息队列 rabbitmqkafka,在进行配置中心的开发之前,需要先部署消息队列。
断路器 的功能会和 hystrix-dashboard 断路监控一起放出,包括 turbinezipkinspring cloud sleuth 服务调用追踪,这些都属于服务端异常监控范畴。
由于配置中心和服务追踪都涉及到消息队列,下一篇先脱离 spring cloud,介绍一下 docker 环境下的 rabbitmq 部署、AMQP 协议、以及使用 spring AMQP 进行消息收发。

你可能感兴趣的:(基于docker部署的微服务架构(三): 服务网关)