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

前言

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

创建服务网关

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

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.4.2.RELEASE</version>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-zuul</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-eureka</artifactId>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Camden.SR2</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

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

服务注册中心
服务注册中心

在浏览器中访问 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文件中增加:

<properties>
    <java.version>1.8</java.version> <!-- 指定java版本 -->
    <!-- 镜像前缀,推送镜像到远程库时需要,这里配置了一个阿里云的私有库 -->
    <docker.image.prefix>
        registry.cn-hangzhou.aliyuncs.com/ztecs
    </docker.image.prefix>
    <!-- docker镜像的tag -->
    <docker.tag>demo</docker.tag>

    <!-- 激活的profile -->
    <activatedProperties></activatedProperties>
</properties>

<profiles>
    <!-- docker环境 -->
    <profile>
        <id>docker</id>

        <properties>
            <activatedProperties>docker</activatedProperties>
            <docker.tag>docker-demo-${project.version}</docker.tag>
        </properties>
    </profile>
</profiles>

<build>
    <defaultGoal>install</defaultGoal>
    <finalName>${project.artifactId}</finalName>

    <resources>
        <resource>
            <directory>src/main/resources</directory>
            <filtering>true</filtering>
        </resource>
    </resources>

    <plugins>
        <!-- 配置spring boot maven插件,把项目打包成可运行的jar包 -->
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <executable>true</executable>
            </configuration>
        </plugin>

        <!-- 打包时跳过单元测试 -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <configuration>
                <skipTests>true</skipTests>
            </configuration>
        </plugin>

        <!-- 配置docker maven插件,绑定install生命周期,在运行maven install时生成docker镜像 -->
        <plugin>
            <groupId>com.spotify</groupId>
            <artifactId>docker-maven-plugin</artifactId>
            <version>0.4.13</version>
            <executions>
                <execution>
                    <phase>install</phase>
                    <goals>
                        <goal>build</goal>
                        <goal>tag</goal>
                    </goals>
                </execution>
            </executions>
            <configuration>
                <!-- 修改这里的docker节点ip,需要打开docker节点的远程管理端口2375,
                具体如何配置可以参照之前的docker安装和配置的文章 -->
                <dockerHost>http://docker节点ip:2375</dockerHost>
                <imageName>${docker.image.prefix}/${project.build.finalName}</imageName>
                <baseImage>java</baseImage>
                <!-- 这里的entryPoint定义了容器启动时的运行命令,容器启动时运行
                java -jar 包名 , -Djava.security.egd这个配置解决tomcat8启动时,
因为需要收集环境噪声来生成安全随机数导致启动过慢的问题-->
                <entryPoint>
                    ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/${project.build.finalName}.jar"]
                </entryPoint>
                <resources>
                    <resource>
                        <targetPath>/</targetPath>
                        <directory>${project.build.directory}</directory>
                        <include>${project.build.finalName}.jar</include>
                    </resource>
                </resources>
                <image>${docker.image.prefix}/${project.build.finalName}</image>
                <newName>${docker.image.prefix}/${project.build.finalName}:${docker.tag}</newName>
                <forceTags>true</forceTags>
                <!-- 如果需要在生成镜像时推送到远程库,pushImage设为true -->
                <pushImage>false</pushImage>
            </configuration>
        </plugin>
    </plugins>
</build>

选择 docker profile,运行 mvn install -P docker ,打包项目并生成docker镜像,注意docker-maven-plugin中的 <entryPoint> 标签里的内容不能换行,否则在生成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 进行消息收发。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容