分布式Session研究(一):Docker + spring boot +Nginx构建分布式应用

由于自己一直痴迷于大型分布式系统的设计原理与实践。奈何条件有限,毕竟还在读书,根本无法接触到真正的分布式,真正的大数据。便只能在自己电脑上通过docker这种虚拟化技术来自己搭建”分布式系统”来玩玩,体验一下分布式Session,分布式事物等等。

这篇文章将搭建出一个”分布式”系统,并先体验分布式系统中Session管理的问题,并通过集中Session管理方案解决

构建两个SpringBoot镜像,提供Session服务。

先使用SpringBoot搭建构建一个Restful 接口,并做一些很简单的Session操作。
Maven:

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

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.5</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

然后写一个比较简单的Controller:

@SpringBootApplication
@RestController
public class App{

    @RequestMapping("/setSession")
    public String home(HttpServletRequest request) {
        request.getSession().setAttribute("user","jack");
        return "I am One .You session has k-v user-wang!!"+request.getSession().getAttribute("user");
    }

    @RequestMapping("/getSession")
    public String session(HttpServletRequest request){
        return "I am One .You session is :"+request.getSession().getAttribute("user");
    }

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

}

现在一个SpringBoot项目就写完了。它只提供了两个Rest接口。一个设置Session值,一个获取Session值。注意一点,上面代码中返回值中有个One的标识。现在我们将其Docker化:

  • 先通过mvn package将其打成jar包,命名为dockerOne.jar
  • 编写Dockerfile
  • 生成镜像并运行

Dockerfile:

FROM registry.cn-hangzhou.aliyuncs.com/alirobot/oraclejdk8-nscd #基础源
VOLUME /tmp #挂载
add dockerOne.jar app.jar #传递jar包
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
#运行命令

然后运行 sudo docker build -t dockerone/jdk8 . 构建第一个镜像。
按上面步骤构建第二个镜像:代码中one改为two。镜像名称也改为dockertwo/jdk8。

启动上面两个镜像,通过inspect查看ip为192.168.110.2 192.168.110.3 (docker网桥为192.168.110.1)

构建Nginx并搭建负载均衡

docker pull 下载一个nginx后。我们在本机上写一个nginx的配置文件,然后通过 -v 已数据卷的形式挂载到docker镜像中。

#使用轮询法做负载均衡,权重都是1,这样请求可以随机分配。
upstream webservers{
 server 192.168.110.3:8080 weight=1;
 server 192.168.110.2:8080 weight=1;
}

server {
    listen       80;
    server_name  localhost;

    #charset koi8-r;
    #access_log /var/log/nginx/log/host.access.log main;

    location =/index.html{
       root /data;
    }
    location / {
       proxy_pass  http://webservers;
    }
}

启动Nginx并查看ip为 192.168.110.4

访问192.168.110.4并观察浏览器携带的cookie及Session的返回值

我们知道,第一次访问网站的时候(也就是该用户还没有创建Session的时候),服务器的response会携带一个set-cookie的值随机值。用来表示该用户的Session。当该用户下次访问网站则会在request中携带一个cookie的头信息,带入该cookie。这样服务器就能在内存中找到该用户的唯一Session。
首先请求/setSession

1.请求分配到two服务器: 没有携带cookie值,服务器符合 E5DD28A3B255AEB9B95C2C5304342685
2.请求分配到one服务器: 携带cookie E5DD28A3B255AEB9B95C2C5304342685 却返回set-cookie 75965AEBC91A57B509BDE095602B9F97
3.请求到two 发送 75965AEBC91A57B509BDE095602B9F97 却返回60BC939E156DBB21676BCC02EC0C7E2A
4.请求到one 发送 60BC939E156DBB21676BCC02EC0C7E2A 却返回E090DA9B2ABA7B7D08915DF78EDFA31E
5.请求到two 发送 E090DA9B2ABA7B7D08915DF78EDFA31E 却返回96BA8FCD5305A75ECC0684F9D541EBDE

现在请求 /getSession

1.请求分配到one 携带cookie 96BA8FCD5305A75ECC0684F9D541EBDE
返回值为 I am One .You session is :null
返回set cookie值为95833D8765EB2818B220CA9AC53DA72C
2.请求分配到two 携带cookie 95833D8765EB2818B220CA9AC53DA72C
返回值为I am Two .You session is : null
返回set cookie值 6BFB1D62A6B3925A1774CC51C3B21893

现在我们应该清楚了分布式系统中Session的问题了吧,就是不清楚用户的请求会落地在那一台服务器上。而对于Session是默认仅保存在你请求落地的那台服务器上的,对于下一次的请求,如果落地在非上一个请求落地的服务器上,则没有Session信息。

分布式Session的解决方法之Session集中式管理

Session集中式管理就是将所以用户的Session统一到一台中央服务器上(也有可能这个中央服务器也搭建成分布式缓存集群)。

对于我们上述项目,现在已经有三台服务器了。一台Nginx负载均衡服务器。两台提供Rest接口的服务器。现在再加一台Session缓存服务器。让两台Rest接口服务器把Session全部交给Session缓存服务器来保持。

具体的实现我们使用Spring-Session,Session缓存保存于Redis中

首先启动一个redis的Dcoker镜像。查看ip为192.168.110.5

Maven中添加到Redis的依赖:

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

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.5</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session</artifactId>
            <version>1.2.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
            <version>1.2.2.RELEASE</version>
            <type>pom</type>
        </dependency>
    </dependencies>

SpringBoot中的配置:

@SpringBootApplication
@RestController
@EnableRedisHttpSession
public class App{

    @RequestMapping("/")
    public String home(HttpServletRequest request) {
        request.getSession().setAttribute("user","xi");
        return "I am Two .You session has k-v user-wang!!"+request.getSession().getAttribute("user");
    }

    @RequestMapping("/getSession")
    public String session(HttpServletRequest request){
        return "I am Two .You session is :"+request.getSession().getAttribute("user");
    }

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

    #缓存服务器的配置
    @Bean
    public JedisConnectionFactory connectionFactory() {
        JedisConnectionFactory redisConnectionFactory = new JedisConnectionFactory();
        redisConnectionFactory.setUsePool(false);
        redisConnectionFactory.setHostName("192.168.110.2");
        redisConnectionFactory.setPort(6379);
        return redisConnectionFactory;
    }

}

其实可以在properties配置文件中加入如下配置;

spring.redis.host=192.168.110.5
spring.redis.port=6379

@EnableRedisHttpSession这个注解就是最重要的东西,加了它之后,spring生产一个新的拦截器,用来实现Session共享的操作,具体实现这里暂不展开。而配置的这个JedisConnectionFactory,则是让Spring根据配置文件中的配置连到Redis。
现在继续之前的步骤,发生Session问题即可解决。

因为无论最后请求落地到哪一台服务器,根据Session共享服务器只返回一个Session给浏览器,而浏览器也永远只携带了一个cookie值

此时我们查看Redis数据库:

dev@dev:~$ redis-cli  -h 192.168.110.5
192.168.110.5:6379> keys *
1) "spring:session:sessions:96b04dd3-4045-435f-ac2b-935c2af5eb52"
3) "spring:session:expirations:1484127300000"
4) "spring:session:sessions:expires:96b04dd3-4045-435f-ac2b-935c2af5eb52"
xxxx

其中一个为Session失效时间,一个则是前面我们说的cookie值。

你可能感兴趣的:(spring,nginx,session,分布式,docker)