# 3.5 小集群部署.md

## 5.小集群部署.md

欢迎访问我的个人网站O(∩\_∩)O哈哈\~希望大佬们能给个star，个人网站网址：<http://www.wenzhihuai.com>，个人网站代码地址：<https://github.com/Zephery/newblog>。\
洋洋洒洒的买了两个服务器，用来学习分布式、集群之类的东西，整来整去，感觉分布式这种东西没人指导一下真的是太抽象了，先从网站的分布式部署一步一步学起来吧，虽然网站本身的访问量不大==。

## nginx负载均衡

一般情况下，当单实例无法支撑起用户的请求时，就需要就行扩容，部署的服务器可以分机房、分地域。而分地域会导致请求分配到太远的地区，比如：深圳的用户却访问到了北京的节点，然后还得从北京返回处理之后的数据，光是来回就至少得30ms。这部分可以通过智能DNS（就近访问）解决。而分机房，需要将请求合理的分配到不同的服务器，这部分就是我们所需要处理的。 通常，负载均衡分为硬件和软件两种，硬件层的比较牛逼,将4-7层负载均衡功能做到一个硬件里面,如F5,梭子鱼等。目前主流的软件负载均衡分为四层和七层，LVS属于四层负载均衡,工作在tcp/ip协议栈上,通过修改网络包的ip地址和端口来转发, 由于效率比七层高,一般放在架构的前端。七层的负载均衡有nginx, haproxy, apache等,虽然nginx自1.9.0版本后也开始支持四层的负载均衡，但是暂不讨论（我木有硬件条件）。下图来自[张开涛](http://jinnianshilongnian.iteye.com/)的《亿级流量网站架构核心技术》

![](https://github-images.wenzhihuai.com/images/20171018044732.png)

本站并没有那么多的服务器，目前只有两台，搭建不了那么大型的架构，就简陋的用两台服务器来模拟一下负载均衡的搭建。下图是本站的简单架构：

![](https://github-images.wenzhihuai.com/images/20171018051437.png)

其中服务器A（119.23.46.71）为深圳节点，服务器B（47.95.10.139）为北京节点，搭建Nginx之后流量是这么走的：user->A->B-A->user或者user->A->user，第一条中A将请求转发给B，然后B返回的是其运行结果的静态资源。因为这里仅仅是用来学习，所以请不要考虑因为地域导致延时的问题。。。。下面是过程。

### 1.1 Nginx的安装

可以选择tar.gz、yum、rpm安装等，这里，由于编译、nginx配置比较复杂，要是没有把握还是使用rpm来安装吧，比较简单。从<https://pkgs.org/download/nginx>可以找到最新的rpm包，然后rpm -ivh 文件，然后在命令行中输入nginx即可启动，可以使用netstat检查一下端口。

![](https://github-images.wenzhihuai.com/images/20171103062512.png)

启动后页面如下：

![](https://github-images.wenzhihuai.com/images/20171103063022.png)

记一下常用命令

```html
启动nginx，由于是采用rpm方式，所以环境变量什么的都配置好了。
[root@beijingali ~]# nginx          #启动nginx
[root@beijingali ~]# nginx -s reload         #重启nginx
[root@beijingali ~]# nginx -t           #校验nginx配置文件
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
```

### 1.2 Nginx的配置

**1.2.1 负载均衡算法**

Nginx常用的算法有： （1）round-robin：轮询，nginx默认的算法，从词语上可以看出，轮流访问服务器，也可以通过weight来控制访问次数。 （2）ip\_hash：根据访客的ip，一个ip地址对应一个服务器。 （3）hash算法：hash算法常用的方式有根据uri、动态指定的consistent\_key两种。 使用hash算法的缺点是当添加服务器的时候，只有少部分的uri能够被重新分配到新的服务器。这里，本站使用的是hash uri的算法，将不同的uri分配到不同的服务器，但是由于是不同的服务器，tomcat中的session是不一致，解决办法是[tomcat session](http://blog.csdn.net/qh_java/article/details/45955923)的共享。额。。。可惜本站目前没有什么能够涉及到登陆什么session的问题。

```html
http{
    ...
    upstream backend {
        hash $uri;
        # 北京节点
        server 47.95.10.139:8080;
        # 深圳节点
        server 119.23.46.71:8080;
    }

    server {
        ...
        location / {
            root   html;
            index  index.html index.htm;
            proxy_pass http://backend;
            ...
        }
    ...
```

**1.2.2 日志格式**

之前有使用过ELK来跟踪日志，所以将日志格式化成了json的格式，这里贴一下吧

```html
    ...
    log_format main '{"@timestamp":"$time_iso8601",'
                    '"host":"$server_addr",'
                    '"clientip":"$remote_addr",'
                    '"size":$body_bytes_sent,'
                    '"responsetime":$request_time,'
                    '"upstreamtime":"$upstream_response_time",'
                    '"upstreamhost":"$upstream_addr",'
                    '"http_host":"$host",'
                    '"url":"$uri",'
                    '"xff":"$http_x_forwarded_for",'
                    '"referer":"$http_referer",'
                    '"agent":"$http_user_agent",'
                    '"status":"$status"}';
    access_log  logs/access.log  main;
    ...
```

**1.2.3 HTTP反向代理**

配置完上流服务器之后，需要配置Http的代理，将请求的端口转发到proxy\_pass设定的上流服务器，即当我们访问<http://wwww.wenzhihuai.com的时候，请求会被转发到backend中配置的服务器，此处为http://47.95.10.139:8080或者http://119.23.46.71:8080。但是，仔细注意之后，我们会发现，tomcat中的访问日志ip来源都是127.0.0.1，相当于本地访问自己的资源。由于后台中有处理ip的代码，对客户端的ip、访问uri等记录下来，所以需要设置nginx来获取用户的实际ip，参考[nginx> 配置]\(<http://blog.csdn.net/bao19901210/article/details/52537279)。参考文中的一句话：经过反向代理后，由于在客户端和web服务器之间增加了中间层，因此web服务器无法直接拿到客户端的ip，通过$remote\\_addr变量拿到的将是反向代理服务器的ip地址”。nginx是可以获得用户的真实ip的，也就是说nginx使用$remote\\_addr变量时获得的是用户的真实ip，如果我们想要在web端获得用户的真实ip，就必须在nginx这里作一个赋值操作，如下：>

```html
        location / {
            root   html;
            index  index.html index.htm;
            proxy_pass http://backend;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $host;
            proxy_set_header REMOTE-HOST $remote_addr;
        }
```

**（1）proxy\_set\_header X-real-ip $remote\_addr;**\
其中这个X-real-ip是一个自定义的变量名，名字可以随意取，这样做完之后，用户的真实ip就被放在X-real-ip这个变量里了，然后，在web端可以这样获取： request.getAttribute("X-real-ip")

**（2）proxy\_set\_header X-Forwarded-For $proxy\_add\_x\_forwarded\_for;**

X-Forwarded-For：squid开发的，用于识别通过HTTP代理或负载平衡器原始IP一个连接到Web服务器的客户机地址的非rfc标准，这个不是默认有的，其经过代理转发之后，格式为client1, proxy1, proxy2，如果想通过这个变量来获取用户的ip，那么需要和$proxy\_add\_x\_forwarded\_for一起使用。\
$proxy\_add\_x\_forwarded\_for：现在的$proxy\_add\_x\_forwarded\_for变量，X-Forwarded-For部分包含的是用户的真实ip，$remote\_addr部分的值是上一台nginx的ip地址，于是通过这个赋值以后现在的X-Forwarded-For的值就变成了“用户的真实ip，第一台nginx的ip”。

**1.2.4 HTTPS**

[HTTPS](https://baike.baidu.com/item/https/285356?fr=aladdin)（全称：Hyper Text Transfer Protocol over Secure Socket Layer），是以安全为目标的HTTP通道，简单讲是HTTP的安全版。即HTTP下加入SSL层，HTTPS的安全基础是SSL，因此加密的详细内容就需要SSL。一般情况下，能通过服务器的ssh来生成ssl证书，但是如果使用是自己的，一般浏览器（谷歌、360等）都会报证书不安全的错误，正常用户都不敢访问吧==，所以现在使用的是腾讯跟别的机构颁发的：

![](https://github-images.wenzhihuai.com/images/20171105024159.png)

首先需要下载证书，放在nginx.conf相同目录下，nginx上的配置也需要有所改变，在nginx.conf中设置listen 443 ssl;开启https。然后配置证书和私钥：

```html
        ssl_certificate 1_www.wenzhihuai.com_bundle.crt;    #主要文件路径
        ssl_certificate_key 2_www.wenzhihuai.com.key;
        ssl_session_timeout 5m;         # 超时时间
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2; #按照这个协议配置
        ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;#按照这个套件配置
        ssl_prefer_server_ciphers on;
```

至此，可以使用https来访问了。https带来的安全性（保证信息安全、识别钓鱼网站等）是http远远不能比拟的，目前大部分网站都是实现全站https，还能将http自动重定向为https，此处，需要在server中添加rewrite ^(.\*) https\://$server\_name$1 permanent;即可

**1.2.5 失败重试**

配置好了负载均衡之后，如果有一台服务器挂了怎么办？nginx中提供了可配置的服务器存活的识别，主要是通过max\_fails失败请求次数，fail\_timeout超时时间，weight为权重，下面的配置的意思是当服务器超时10秒，并失败了两次的时候，nginx将认为上游服务器不可用，将会摘掉上游服务器，fail\_timeout时间后会再次将该服务器加入到存活上游服务器列表进行重试

```html
upstream backend_server {
    server 10.23.46.71:8080 max_fails=2 fail_timeout=10s weight=1;
    server 47.95.10.139:8080 max_fails=2 fail_timeout=10s weight=1;
}
```

## session共享

分布式情况下难免会要解决session共享的问题，目前推荐的方法基本上都是使用redis，网上查找的方法目前流行的有下面四种，参考自[tomcat 集群中 session 共](http://blog.csdn.net/qh_java/article/details/45955923)： 1.使用 filter 方法存储。（推荐，因为它的服务器使用范围比较多，不仅限于tomcat ，而且实现的原理比较简单容易控制。） 2.使用 tomcat sessionmanager 方法存储。（直接配置即可） 3.使用 terracotta 服务器共享。（不知道，不了解） 4.使用spring-session。（spring的一个小项目，其原理也和第一种基本一致）

本站使用spring-session，毕竟是spring下的子项目，学习下还是挺好的。参考[Spring-Session官网](https://docs.spring.io/spring-session/docs/2.0.0.BUILD-SNAPSHOT/reference/html5/)。官方文档提供了spring-boot、spring等例子，可以参考参考。目前最新版本是2.0.0，不同版本使用方式不同，建议看官网的文档吧。

首先，添加相关依赖

```xml
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
            <version>1.3.1.RELEASE</version>
            <type>pom</type>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>${jedis.version}</version>
        </dependency>
```

新建一个session.xml，然后在spring的配置文件中添加该文件，然后在session.xml中添加：

```xml
    <!-- redis -->
    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
    </bean>

    <bean id="jedisConnectionFactory"
          class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <property name="hostName" value="${host}" />
        <property name="port" value="${port}" />
        <property name="password" value="${password}" />
        <property name="timeout" value="${timeout}" />
        <property name="poolConfig" ref="jedisPoolConfig" />
        <property name="usePool" value="true" />
    </bean>

    <bean id="redisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
        <property name="connectionFactory" ref="jedisConnectionFactory" />
    </bean>

    <!-- 将session放入redis -->
    <bean id="redisHttpSessionConfiguration"
          class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
        <property name="maxInactiveIntervalInSeconds" value="1800" />
    </bean>
```

然后我们需要保证servlet容器（tomcat）针对每一个请求都使用springSessionRepositoryFilter来拦截

```html
<filter>
    <filter-name>springSessionRepositoryFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>springSessionRepositoryFilter</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>ERROR</dispatcher>
</filter-mapping>
```

配置完成，使用RedisDesktopManager查看结果：

![](https://github-images.wenzhihuai.com/images/20171105045003.png)

### 测试：

访问<http://www.wenzhihuai.com\\>
tail -f localhost\_access\_log.2017-11-05.txt查看日志，然后清空一下当前记录

![](https://github-images.wenzhihuai.com/images/20171105050714.png)

访问技术杂谈页面，此时nginx将请求转发到119.23.46.71服务器，session为28424f91-5bc5-4bba-99ec-f725401d7318。

![](https://github-images.wenzhihuai.com/images/20171105050757.png)

点击生活笔记页面，转发到的服务器为47.95.10.139，session为28424f91-5bc5-4bba-99ec-f725401d7318，与上面相同。session已保持一致。

![](https://github-images.wenzhihuai.com/images/20171105050849.png)

**值得注意的是：同一个浏览器，在没有关闭的情况下，即使通过域名访问和ip访问得到的session是不同的。** 欢迎访问我的个人网站O(∩\_∩)O哈哈\~希望能给个star 个人网站网址：<http://www.wenzhihuai.com>\
个人网站代码地址：<https://github.com/Zephery/newblog>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://gitbook.wenzhihuai.com/ge-ren-wang-zhan/5.-xiao-ji-qun-bu-shu.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
