使用HAProxy实现RabbitMq集群负载均衡

一、目的

​ 在使用 RabbitMq集群时可能会遇到集群中某个节点出现异常或者连接数过多的情况,这个时候与该节点连接的Consumer将会断开,Publisher也会无法将消息发送至集群。为了解决这些问题,本文中将使用 HAProxy来代理集群,实现多个节点的负载均衡以及在某个节点异常时自动将连接切换至其他正常节点等功能。

二、HAProxy安装配置(Centos 7)

  1. 安装 HAProxy
    1. 下载最新稳定版2.0.8并解压
      // PWD:/opt/ci123/www/html/rabbitMq/
      wget https://www.haproxy.org/download/2.0/src/haproxy-2.0.8.tar.gz
      tar xf haproxy-2.0.8.tar.gz
      
    2. 查看系统内核版本来指定编译版本
      uname -r
      3.10.0-862.6.3.el7.x86_64
      

      版本参考:

      ```

    3. </ol>

      <ul>
      <li>linux22 for Linux 2.2</li>
      <li>linux24 for Linux 2.4 and above (default)</li>
      <li>linux24e for Linux 2.4 with support for a working epoll (> 0.21)</li>
      <li>linux26 for Linux 2.6 and above</li>
      <li>linux2628 for Linux 2.6.28, 3.x, and above (enables splice and tproxy)</li>
      <li>solaris for Solaris 8 or 10 (others untested)</li>
      <li>freebsd for FreeBSD 5 to 10 (others untested)</li>
      <li>netbsd for NetBSD</li>
      <li>osx for Mac OS/X</li>
      <li>openbsd for OpenBSD 5.7 and above</li>
      <li>aix51 for AIX 5.1</li>
      <li>aix52 for AIX 5.2</li>
      <li>cygwin for Cygwin</li>
      <li>haiku for Haiku</li>
      <li>generic for any other OS or version.</li>
      <li><p>custom to manually adjust every setting
      ```

      根据版本参考,这里我们选择linux2628版本进行编译。

      1. 编译到指定目录

        cd haproxy-2.0.8
        make TARGET=linux2628 PREFIX=/opt/ci123/haproxy
        
        // 这里出现报错,从2.0版本开始linux2628已被废弃
        Target 'linux2628' was removed from HAProxy 2.0 due to being irrelevant and
        often wrong. Please use 'linux-glibc' instead or define your custom target
        by checking available options using 'make help TARGET=<your-target>'.
        
        // 根据提示修改参数后编译
        make TARGET=linux-glibc PREFIX=/opt/ci123/haproxy
        make install PREFIX=/opt/ci123/haproxy
        
    4. 配置
      1. 复制 haproxy命令至全局变量
        cp /opt/ci123/haproxy/sbin/haproxy /usr/bin/
        
      2. 创建系统用户
        useradd -r haproxy
        
      3. 添加haproxy配置文件
        1. haproxy配置文件由五部分组成:
        • global: 参数是进程级的,通常和操作系统相关。这些参数一般只设置一次,如果配置无误,就不需要再次配置进行修改。
      • default:默认参数。
        • frontend:用于接收客户端请求的前端节点,可以设置相应的转发规则来指定使用哪个backend
        • backend:后端服务器代理配置,可实现代理多台服务器实现负载均衡、为请求添加额外报文数据等功能。
        • listen:是frontendbackend的结合,通常只对tcp流量有用。
      1. 添加配置文件/opt/ci123/haproxy/conf/haproxy.cfg

        ```
        # 全局配置
        global
        log 127.0.0.1 local3 # 设置日志
        pidfile /opt/ci123/haproxy/logs/haproxy.pid
        maxconn 4000 # 最大连接数
        user haproxy
        group haproxy
        daemon # 守护进程运行

        # 默认配置
        defaults
        log global
        mode tcp # 默认的模式mode { tcp|http|health },tcp是4层,http是7层,health只会返回OK
        option httplog # http 日志格式,仅在http模式下可用
        option dontlognull # 不记录健康检查日志信息;
        option redispatch # serverId对应的服务器挂掉后,强制定向到其他健康的服务器
        option http-server-close
        #option abortonclose # 当服务器负载很高的时候,自动结束掉当前队列处理比较久的链接;
        #option forwardfor # 如果后端服务器需要获得客户端真实ip需要配置的参数,可以从Http Header中获得客户端ip;
        #option httpclose # 主动关闭http通道,每次请求完毕后主动关闭http通道,ha-proxy不支持keep-alive,只能模拟这种模式的实现;<br />
        balance roundrobin # 负载均衡算法,轮询;
        retries 3 # 重试次数;

        <pre class="prism-highlight line-numbers" data-start="1"><code class="language-null"> timeout http-request 10s # 客户端建立连接但不请求数据时,关闭客户端连接;
        timeout queue 1m # 高负载响应haproxy时,会把haproxy发送来的请求放进一个队列中,timeout queue定义放入这个队列的超时时间;
        timeout connect 10s # 定义haproxy将客户端请求转发至后端服务器所等待的超时时间;
        timeout client 1m # 客户端非活动状态的超长时间(默认毫秒)
        timeout server 1m # 服务端与客户端非活动状态连接的超时时间。(默认毫秒)
        timeout http-keep-alive 10s # 定义保持连接的超时时长;
        timeout check 10s # 心跳检测超时;
        maxconn 3000 # 每个server最大的连接数;
        </code></pre>

        #前端配置
        frontend rabbitmq_cluster_front
        bind 0.0.0.0:10000 # http请求的端口,会被转发到设置的ip及端口
        default_backend rabbitmq_cluster_back

        # 后端配置
        backend rabbitmq_cluster_back
        #roundrobin 轮询方式
        balance roundrobin # 负载均衡的方式,轮询方式

        <pre class="prism-highlight line-numbers" data-start="1"><code class="language-null"> # 配置Rabbitmq连接负载均衡
        # 需要转发的ip及端口
        # inter 2000 健康检查时间间隔2秒
        # rise 3 检测多少次才认为是正常的
        # fall 3 失败多少次才认为是不可用的
        # weight 30 权重
        server clusterRabbit1 192.168.3.14:5672 check inter 2000 rise 3 fall 3 weight 30
        server clusterRabbit2 192.168.3.14:5673 check inter 2000 rise 3 fall 3 weight 30
        </code></pre>

        # 统计页面配置
        listen admin_stats<br />
        bind 0.0.0.0:10080 # 监听IP和端口,为了安全可以设置本机的局域网IP及端口;
        mode http
        option httplog # 采用http日志格式<br />
        stats refresh 30s # 统计页面自动刷新时间<br />
        stats uri /haproxy # 状态管理页面,通过/haproxy来访问
        stats realm Haproxy Manager # 统计页面密码框上提示文本<br />
        stats auth duomai:shijiemori@2012 # 统计页面用户名和密码设置<br />
        #stats hide-version # 隐藏统计页面上HAProxy的版本信息
        ```

        1. 配置日志 rsyslog
        vim /etc/rsyslog.conf
        # 取消如下2行注释
        $ModLoad imudp
        $UDPServerRun 51
        
        # 新增配置(自定义的日志设备)
        local3.*  /opt/ci123/haproxy/logs/haproxy.log
        
        # 重启rsyslog服务
        systemctl restart rsyslog
        
        1. 启动 haproxy
        haproxy -f /opt/ci123/haproxy/conf/haproxy.cfg
        

        访问统计页面出现如下界面:

        haproxy管理

    三、集群测试

    沿用上一篇【RabbitMq 镜像队列集群搭建】中的集群测试环境,在测试中将PubliserConsumer的连接替换为HAProxy的地址192.168.3.14:10000

    1. 测试环境
      1. 节点:
        1. 节点一:
        • clusterRabbit1
        • 端口:192.168.3.14:5672
          1. 节点二:
        • clusterRabbit2
        • 端口:192.168.3.14:5673
      2. 队列:
        1. 节点一 rabbit@clusterRabbit1
          1. 队列一:
          • nameclusterRabbit1Queue1
          • routing_keyclusterRabbit1key
          1. 队列二:
          • nameclusterRabbit1Queue2
          • routing_keyclusterRabbitCommonKey
        2. 节点二 rabbit@clusterRabbit2
          1. 队列三:
          • nameclusterRabbit2Queue1
          • routing_keyclusterRabbit2key
          1. 队列四:
          • nameclusterRabbit2Queue2
          • routing_keyclusterRabbitCommonKey(与队列二 相同)
    2. 启动消费者
      1. 消费者
        1. 消费者一:
        • 连接节点:节点一 rabbit@clusterRabbit1
        • 消费队列:队列一 clusterRabbit1Queue1
          1. 消费者二:
        • 连接节点:节点一 rabbit@clusterRabbit1
        • 消费队列:队列二 clusterRabbit1Queue2
          1. 消费者三:
        • 连接节点:节点二 rabbit@clusterRabbit2
        • 消费队列:队列三 clusterRabbit2Queue1
          1. 消费者四:
        • 连接节点:节点二 rabbit@clusterRabbit2
        • 消费队列:队列四 clusterRabbit2Queue2
      2. 启动结果:

        启动成功,但是在一分钟后客户端异常退出,原因是HAProxy设置了timeout client 1m 和 timeout server 1m,消费者在一分钟内都没有接收到消息导致被判定为不活跃连接从而被删除。

        由于HAProxy默认不支持长连接,上述问题可以使用pm2管理消费者的方法来解决,消费者进程在不活跃退出后pm2将自动重启此进程。

    3. 发布消息

      1. 发布clusterRabbit1key消息
      • 消费者一 成功收到消息。
        1. 发布clusterRabbitCommonKey消息
      • 消费者二/四 成功收到消息。
    4. 关闭节点二rabbit@clusterRabbit2后再次发布消息

    • 连接节点二的消费者先退出后重新使用HAProxy成功连接。
    • 发布的消息均能被成功消费。

    四、使用说明

    1. HAProxy默认不支持tcp 长连接,需要使用PM2之类的守护进程管理工具或者长连接技术来实现rabbitMq客户端持续连接。
    2. HAProxy通过活跃检测机制来判定负载均衡中的节点是否可用, 当rabbitMq集群中某个节点不可用时,在经过一段时间的活跃检测之后,HAProxy将弃用该节点直至节点恢复。在这种情况下,rabbitMq消费者将断开连接后选择剩余可用的节点再次启动,客户端发布时也会自动选择剩余可用的节点。
    3. rabbitMq集群中某个节点宕机之后,HAProxy会自动使用可用的集群节点,所以不会出现在HAProxy活跃检测期间发布消息出现一半成功一半失败的情况,所有的消息都将通过可用的集群节点发布至集群。

RabbitMq 使用docker搭建集群

建议食用本文前请先阅读【RabbitMq 普通集群搭建】

一、RabbitMq镜像

  1. 镜像: rabbitmq:3.8-management
  2. 启动参数:
    • --name: 容器名称
    • -h / --hostnamerabbitMq默认节点的 host
    • -v:文件挂载映射
    • -p:端口映射
      • 5672:容器内默认的rabbitMq 启动端口
      • 15672:容器内默认的rabbitMq 管理插件启动端口
  3. 环境变量:(支持所有rabbitMq环境变量)
    • RABBITMQ_NODENAME:节点名称,缺省为 rabbit@[hostname]
    • RABBITMQ_DEFAULT_USER:默认用户名,缺省为guset
    • RABBITMQ_DEFAULT_PASS:默认密码,缺省为 guest
    • RABBITMQ_DEFAULT_VHOST:默认虚拟主机,缺省为/
    • RABBITMQ_ERLANG_COOKIEerlang.cookie

二、搭建集群

  1. 启动多份rabbitMq容器
    # 启动节点一
    docker run -d --hostname clusterRabbit1 --name clusterRabbit1 -p 15672:15672 -p 5672:5672 -e RABBITMQ_ERLANG_COOKIE='rabbitcookie' -v /opt/ci123/www/html/rabbitMq/clusterRabbit1:/var/lib/rabbitmq rabbitmq:3.8-management
    
    # 启动节点二
    docker run -d --hostname clusterRabbit2 --name clusterRabbit2 -p 5673:5672 --link clusterRabbit1:clusterRabbit1 -e RABBITMQ_ERLANG_COOKIE='rabbitcookie' -v /opt/ci123/www/html/rabbitMq/clusterRabbit2:/var/lib/rabbitmq rabbitmq:3.8-management
    
  • /var/lib/rabbitmq 是容器内部文件数据存放目录,使用-v进行文件目录挂载。
  • 多个容器之间使用--link <name or id>:alias连接,否则需要自行在各个容器添加供rabbitMq 相互访问的host
  • Erlang Cookie值必须相同,rabbitMQ是通过Erlang实现的,Erlang Cookie相当于不同节点之间相互通讯的秘钥,Erlang节点通过交换Erlang Cookie获得认证。
  1. 启动集群
    1. 设置节点二 rabbit@clusterRabbit2,加入节点一 rabbit@clusterRabbit1的集群
      rabbitmqctl stop_app
      rabbitmqctl reset
      rabbitmqctl join_cluster rabbit@clusterRabbit1
      rabbitmqctl start_app
      
    2. 查看rabbitMq管理后台集群数据

      rabbitmq管理后台

  2. 一键化启动脚本

    #!bin/sh
    # 启动节点一
    docker run -d --hostname clusterRabbit1 --name clusterRabbit1 -p 15672:15672 -p 5672:5672 -e RABBITMQ_ERLANG_COOKIE='rabbitcookie' -v /opt/ci123/www/html/rabbitMq/clusterRabbit1:/var/lib/rabbitmq rabbitmq:3.8-management
    
    # 启动节点二
    docker run -d --hostname clusterRabbit2 --name clusterRabbit2 -p 5673:5672 --link clusterRabbit1:clusterRabbit1 -e RABBITMQ_ERLANG_COOKIE='rabbitcookie' -v /opt/ci123/www/html/rabbitMq/clusterRabbit2:/var/lib/rabbitmq rabbitmq:3.8-management
    
    echo "即将开始初始化"
    sleep 5
    # 加入集群
    docker exec -it clusterRabbit2 sh -c 'rabbitmqctl stop_app && rabbitmqctl reset && rabbitmqctl join_cluster rabbit@clusterRabbit1 && rabbitmqctl start_app'
    
    # 添加虚拟主机
    docker exec -it clusterRabbit1 sh -c 'rabbitmqctl add_vhost cluster'
    
    # 添加用户
    docker exec -it clusterRabbit1 sh -c 'rabbitmqctl add_user api_management shijiemori2012'
    
    # 添加用户权限
    docker exec -it clusterRabbit1 sh -c 'rabbitmqctl set_permissions -p cluster api_management ".*" ".*" ".*"'
    
    # 设置用户标签
    docker exec -it clusterRabbit1 sh -c 'rabbitmqctl set_user_tags api_management administrator'
    

RabbitMq 普通集群搭建

一、集群概念

RabbitMQ集群是一个或多个节点的逻辑分组,每个节点共享用户、虚拟主机、队列、交换、绑定路由、运行时参数和其他分布式状态。集群中的节点可以动态地添加/删除,RabbitMQ代理一开始都运行在单个节点上,可以将这些节点连接到集群中,然后再将其转换回各个代理。

  1. 默认情况下,RabbitMq将复制除消息队列外的所有数据至集群中的每一个节点。而消息队列的完整数据只会存放于创建该队列的节点上,其余节点仅保存该队列的元数据和指针(类似于索引)。如果需要复制队列,则需要启用镜像队列集群。

  2. 集群中每个节点是平等的,不存在主从和特殊的节点。

  3. 集群中的节点通过Erlang Cookie相互通信,每个节点必须具有相同的cookie

  4. 节点分为磁盘节点和RAM节点,RAM节点只在RAM中存储内部数据库表,并不存储包括消息、消息存储索引、队列索引和其他节点状态等数据,RAM节点的性能更加高效,但是由于数据是非持久化的,一旦宕机将无法恢复。默认创建的都是磁盘节点。

  5. 单节点拓扑图如下,集群的拓扑是基于多个Node节点的扩展。

    单节点拓扑图

二、配置需求

  1. 配置方式
    • config 文件配置
    • rabbitmqctl命令配置(下文中使用此方法配置)
  2. 集群中的节点名必须是唯一的
    • 可以在启动时使用环境变量RABBITMQ_NODENAME设置
    • 节点名由[节点名称]@[host]组成
  3. 各个节点的启动端口可以被成功连接
  4. 节点之间通过节点名相互访问,要求各个节点之间的host可以相互进行DNS解析
  5. 每个节点之间必须配置相同的Erlang Cookie(多机环境需要额外配置)

三、集群配置

  1. 启动多个独立的节点

    RABBITMQ_NODE_PORT=5674 RABBITMQ_NODE_IPDDRESS=192.168.0.235  RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port,15673}]" RABBITMQ_NODENAME="clusterRabbit1@VM235"  /usr/local/rabbitmq/3.1.3/sbin/rabbitmq-server -detached
    RABBITMQ_NODE_PORT=5675 RABBITMQ_NODE_IPDDRESS=192.168.0.235  RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port,15674}]" RABBITMQ_NODENAME="clusterRabbit2@VM235" /usr/local/rabbitmq/3.1.3/sbin/rabbitmq-server -detached
    

    执行完成后,分别创建了名称为clusterRabbit1@VM235,clusterRabbit2@VM235的两个节点。

    VM235需要提前配置host文件,确保各个节点之间的host可以相互进行DNS解析。

  2. 创建集群

    1. 关闭节点clusterRabbit1@VM235
      ./rabbitmqctl -n clusterRabbit1@VM235 stop_app
      
    2. 重置节点clusterRabbit1@VM235
      ./rabbitmqctl -n clusterRabbit1@VM235 reset
      

      必须重置节点才能加入现有集群,重置节点将删除该节点上以前存在的所有资源和数据。这意味着节点不能在成为集群的成员时保留其现有数据,节点中的数据需要进行额外的备份和恢复。

    3. 将节点clusterRabbit1@VM235加入clusterRabbit2@VM235的集群

      ./rabbitmqctl -n clusterRabbit1@VM235 join_cluster clusterRabbit2@VM235
      
    4. 启动节点clusterRabbit1@VM235
      ./rabbitmqctl -n clusterRabbit1@VM235 start_app
      
    5. 查看集群信息
      ./rabbitmqctl -n clusterRabbit1@VM235 cluster_status
      结果:
      Cluster status of node clusterRabbit1@VM235 ...
      [{nodes,[{disc,[clusterRabbit1@VM235,clusterRabbit2@VM235]}]},
      {running_nodes,[clusterRabbit2@VM235,clusterRabbit1@VM235]},
      {partitions,[]}]
      ...done.
      
  3. 集群中节点的关闭与重启
    1. 节点关闭并重启之后会选择一个在线的集群成员(只考虑磁盘节点)进行同步。在重新启动节点时,默认情况下将尝试与该成员联系10次,并有30秒的响应超时。如果该成员在时间间隔内可用则节点将成功启动,并与该成员同步所需内容后继续运行。如果该成员无法响应,则重新启动的节点将放弃同步数据并启动。
    2. 以下情况将导致节点无法重新加入集群:
    • 修改节点名/主机名,节点的数据目录路径会因此更改。
    • 重置节点数据/更换节点数据目录
  4. 移除集群中的节点
    1. 关闭该节点
    2. 重置该节点
    3. 再次启动该节点

四、集群测试

  1. 创建用户、vhostexchangequeue,并启动消费者。【测试基础】
    1. 用户:api_managementmanagement标签,开放虚拟主机cluster所有权限)
    2. vhostcluster
    3. exchangecluster(直连交换机)
    4. queue
      1. 节点一 clusterRabbit1
        1. 队列一:
        • nameclusterRabbit1Queue1
        • routing_keyclusterRabbit1key
        1. 队列二:
        • nameclusterRabbit1Queue2
        • routing_keyclusterRabbitCommonKey
      2. 节点二 clusterRabbit2
        1. 队列三:
        • nameclusterRabbit2Queue1
        • routing_keyclusterRabbit2key
        1. 队列四:
        • nameclusterRabbit2Queue2
        • routing_keyclusterRabbitCommonKey(与队列二 相同)
    5. consumer
      1. 消费者一:
      • 连接节点:节点一 clusterRabbit1
      • 消费队列:队列一 clusterRabbit1Queue1
        1. 消费者二:
      • 连接节点:节点一 clusterRabbit1
      • 消费队列:队列二 clusterRabbit1Queue2
        1. 消费者三(非此节点的队列):
      • 连接节点:节点一 clusterRabbit1
      • 消费队列:队列三 clusterRabbit2Queue1
        1. 消费者四:
      • 连接节点:节点二 clusterRabbit2
      • 消费队列:队列四 clusterRabbit2Queue2
  2. 连接节点二 clusterRabbit2,再次声明队列名和队列一同名的队列clusterRabbit1Queue1。【同名队列再次声明】
    • 结果:声明不成功,节点二中没有生成新的clusterRabbit1Queue1队列,而节点一中clusterRabbit1Queue1队列多出了新绑定的routing_key,这意味着在同一集群中不同节点之间的队列名是唯一的,在一个节点中可以操作另一个节点的队列数据。
  3. 生产者连接节点一 clusterRabbit1,发布clusterRabbit1key消息。【发布此节点队列消息】
    • 消费者一 成功接收到消息
  4. 生产者连接节点二 clusterRabbit2,发布clusterRabbit1key消息。【发布非此节点队列消息】
    • 消费者一 成功接收到消息
  5. 生产者连接节点一 clusterRabbit1,发布clusterRabbitCommonKey消息。【多个节点队列绑定相同消息】
    • 消费者二 成功接收到消息
    • 消费者四 成功接收到消息
  6. 生产者连接节点一 clusterRabbit1,发布clusterRabbit2key消息。【消费非此节点队列消息】
    • 消费三 成功接收到消息
  7. 关闭节点二 clusterRabbit2,连接节点一,发布clusterRabbit2key消息【投递消息给集群中意外退出的节点】
    • 关闭节点二 clusterRabbit2之后,消费者三、四异常退出
    • 投递消息至集群成功
    • 再次启动节点二 clusterRabbit2以及消费者三、四,之前投递的消息没有成功接收

五、使用总结

  1. 同一集群中不同节点之间的队列名是唯一的,在一个节点中可以操作另一个节点的队列数据。
  2. 生产者可以发布集群中任一节点队列绑定的消息,集群将自动匹配出符合条件的节点队列,并投递给消费者进行消费。
  3. 集群中不同节点的队列如果绑定了相同的routing_key,消息将投递到集群中所有符合路由匹配条件的节点队列中。
  4. 消费者可以订阅集群中任意节点的队列。
  5. 集群中某个节点异常退出后,生产者投递到集群中的消息将无法送达至该节点,但是不影响其他节点的接收。(解决这个问题需要使用镜像队列集群)

六、错误记录

  1. 启动新的节点报错:could_not_start,rabbitmq_management

    解决:

    rabbitmq_management插件默认使用的是15672端口,这个端口已被之前启动的节点占用,修改启动命名为 rabbitmq_management插件指定一个新的端口即可。

    RABBITMQ_NODE_PORT=5674 RABBITMQ_NODE_IPDDRESS=192.168.0.235  RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port,15673}]" RABBITMQ_NODENAME="clusterRabbit1@VM235"  /usr/local/rabbitmq/3.1.3/sbin/rabbitmq-server -detached
    
  2. 【认知错误】rabbitmq_management管理后台Overview中节点的Memory表示使用的内存,Disk space表示剩余可用的磁盘空间,不是已使用的磁盘空间。

  3. 修改节点的host后启动失败:``ERROR: epmd error for host "VM235": nxdomain (non-existing domain)`

    解决:

    VM235需要预先添加到 HOST文件,确保可以被正确解析。

  4. 节点一 clusterRabbit1 退出集群后重新加入集群,再次声明之前的队列提示routing_key绑定不成功

    NOT_FOUND - no binding clusterRabbitCommonKey between exchange 'cluster' in vhost 'cluster' and queue 'clusterRabbit1Queue2' in vhost 'cluster'
    

    此问题是rabbitMq集群本身的问题,且未得到官方明确的解决方案。

    一些临时解放方案:

php的几种运行模式CLI、CGI、FastCGI、mod_php

常用运行模式

CLI

CLI:命令行,可以在控制台或者shell中输入命令获取输出,没有header头信息

CGI

公共网关接口(Common Gateway Interface/CGI):是一种重要的互联网技术,可以让一个客户端,从网页浏览器向执行在网络服务器上的程序请求数据。CGI描述了服务器和请求处理程序之间传输数据的一种标准。即web server将用户请求以消息的方式转交给PHP独立进程,PHP与web服务之间无从属关系。

FastCGI

快速通用网关接口(Fast Common Gateway Interface/FastCGI):是一种让交互程序与Web服务器通信的协议。FastCGI是早期通用网关接口(CGI)的增强版本。

CGI 和 FastCGI 是一种通信协议规范,不是一个实体

CGI 程序和FastCGI程序,是指实现这两个协议的程序,可以是任何语言实现这个协议的。(PHP-CGI 和 PHP-FPM就是实现FastCGI的程序)

FastCGI和CGI的区别

  • CGI每接收一个请求就要fork一个进程处理,只能接收一个请求作出一个响应。请求结束后该进程就会结束。
  • FastCGI会事先启动起来,作为一个cgi的管理服务器存在,预先启动一系列的子进程来等待处理,然后等待web服务器发过来的请求,一旦接受到请求就交由子进程处理,这样由于不需要在接受到请求后启动cgi,会快很多。
  • FastCGI使用进程/线程池来处理一连串的请求。这些进程/线程由FastCGI服务器管理,而不是Web服务器。 当进来一个请求时,Web服务器把环境变量和这个页面请求通过一个Socket长连接传递给FastCGI进程。FastCGI像是一个常驻型的CGI,它可以一直执行,在请求到达时不会花费时间去fork一个进程来处理(这是CGI对位人诟病的fork-and-execute模式)。正是因为它只是一个通信协议,它还支持分布式的运算,即FastCGI程序可以在网站服务器以外的主机上执行并且接受来自其他网站服务器的请求。

FastCGI整个流程

  1. Web server启动时载入FastCGI进程管理器
  2. FastCGI自身初始化,启动多个CGI解释器进程(可见多个php-cgi)并等待来自Web server的请求
  3. 当请求Web server时,Web server通过socket请求FastCGI进程管理器,FastCGI进程管理器选择并连接到一个CGI解释器,Web server将CGI环境变量和标准输入发送到FastCGI子进程php-cgi
  4. FastCGI子进程处理请求完成后将标准输出和错误从同一连接返回给Web server,当FastCGI子进程结束后请求便结束。FastCGI子进程接着等待处理来自FastCGI进程管理器的下一个连接,在CGI模式中,php-cgi在此便退出了。

PHP-FPM:PHP的FastCGI进程管理器

PHP-CGI 和 PHP-FPM的区别

php-cgi与php-fpm一样,也是一个fastcgi进程管理器

php-cgi的问题在于:
- php-cgi变更php.ini配置后需重启php-cgi才能让新的php-ini生效,不可以平滑重启
- 直接杀死php-cgi进程,php就不能运行了。

PHP-FPM和Spawn-FCGI就没有这个问题,守护进程会平滑从新生成新的子进程。针对php-cgi的不足,php-fpm应运而生。

PHP-FPM 的管理对象是php-cgi。使用PHP-FPM来控制PHP-CGI的FastCGI进程

mod_php(传统模式)

apache的php模块,将PHP做为web-server的子进程控制,两者之间有从属关系。

最明显的例子就是在CGI模式下,如果修改了PHP.INI的配置文件,不用重启web服务便可生效,而模块模式下则需要重启web服务。

以mod_php模式运行PHP,意味着php是作为apache的一个模块来启动的,因此只有在apache启动的时候会读取php.ini配置文件并加载扩展模块,在apache运行期间是不会再去读取和加载扩展模块的。如果修改php的配置,需要重启apache服务

Apache的工作模式 prefork的工作原理

一个单独的控制进程(父进程)负责产生子进程,这些子进程用于监听请求并作出应答。

Apache总是试图保持一些备用的 (spare)或是空闲的子进程用于迎接即将到来的请求。这样客户端就无需在得到服务前等候子进程的产生。

在Unix系统中,父进程通常以root身份运行以便邦定80端口,而 Apache产生的子进程通常以一个低特权的用户运行。User和Group指令用于配置子进程的低特权用户。运行子进程的用户必须要对他所服务的内容有读取的权限,但是对服务内容之外的其他资源必须拥有尽可能少的权限。

Apache的工作模式 worker的工作原理

每个进程能够拥有的线程数量是固定的。服务器会根据负载情况增加或减少进程数量。

一个单独的控制进程(父进程)负责子进程的建立。每个子进程能够建立ThreadsPerChild数量的服务线程和一个监听线程,该监听线程监听接入请求并将其传递给服务线程处理和应答。

nginx默认是使用的fastcgi模式,可以配合fpm使用

js和php闭包的使用和区别

匿名函数

如果只是省去函数名,单纯的当作一个函数式方法返回,只能称为匿名函数(闭包需要将匿名函数当作结果返回),比如:

// js
var foo = function(x, y) {
    return x + y ;
};
console.log(foo(1, 2));  // 3
<?php
// php
$foo = function($a, $b) {
    return $a + $b;
}; // 一定要加分号
echo $foo(1, 2); // 3

闭包

闭包通常是用来创建内部变量,使得这些变量不得被外部随意修改,而只能通过指定的函数接口去修改

js闭包

这里举一个阮老师博客里的例子,阮老师博客:学习Javascript闭包(Closure)

js基础

参考链接:深入理解JS中声明提升、作用域(链)和this关键字

  • js比较特殊的一点是:函数内部可以直接读取到全局变量(对于阮老师的这句话不是很能理解,大概是想表达的意思:父作用域的变量可以在子作用域直接访问,而不需要去声明访问真正的全局变量?)
    • 大部分语言,变量都是先声明在使用,而对于js,具有声明提升的特性(不管在哪里声明,都会在代码执行前处理)
    • 函数和变量的声明总是会隐式地被移动到当前作用域的顶部,函数的声明优先级高于变量的声明
    • var 会在当前作用域声明一个变量,而未声明的变量,会隐式地创建一个全局变量
// 声明提升
console.log(a);  // 1, 未报错
var a = 1;
// 上文链接中的例子
function testOrder(arg) {
    console.log(arg); // arg是形参,不会被重新定义
    console.log(a); // 因为函数声明比变量声明优先级高,所以这里a是函数
    var arg = 'hello'; // var arg;变量声明被忽略, arg = 'hello'被执行
    var a = 10; // var a;被忽视; a = 10被执行,a变成number
    function a() {
        console.log('fun');
    } // 被提升到作用域顶部
    console.log(a); // 输出10
    console.log(arg); // 输出hello
}; 
testOrder('hi');
/* 输出:
hi 
function a() {
        console.log('fun');
    }
10 
hello 
*/
// 全局作用域
var foo = 42;
function test() {
    // 局部作用域
    foo = 21;
}
test();
foo; // 21
// 全局作用域
foo = 42;
function test() {
    // 局部作用域
    var foo = 21;
}
test();
foo; // 42
  • js变量的查找是从里往外的,直到最顶层(全局作用域),并且一旦找到,即停止向上查找。所有内部函数可以访问函数外部的变量,反之无效
function foo(a) {
    var b = a * 2;
    function bar(c) {
        console.log(a, b, c);
    }
    bar(b * 3);
}
foo(2);
function foo() {
    var a = 1;
}
console.log(a);  //a is not defined
function foo1() {
    var num = 0;
    addNum = function() {  // 这里未通过var去声明,默认是全局变量
        num += 1;
    };
    function foo2() {
        console.log(num);
    }
    return foo2;
}
var tmp = foo1();
tmp();  // 0

addNum();
tmp(); // 1

这里第二次调用foo2函数,foo1函数的局部变量num并没有被初始化为0,说明打印的是内存中的num。正常函数在每次调用结束后都会销毁局部变量,在重新调用的时候会再次声明变量;而这边没有重新声明的原因是:把foo2函数赋值给了一个全局变量tmp,导致foo2函数一直存在内存中,而foo2函数依赖于foo1函数存在,所以foo1函数也存在内存中,并没有被销毁,所以foo1的局部变量也是存在内存中。

  • this的上下文基于函数调用的情况。和函数在哪定义无关,而和函数怎么调用有关。
    • 在全局上下文(任何函数以外),this指向全局对象(windows)
    • 在函数内部时,this由函数怎么调用来确定
      • 当函数作为对象方法调用时,this指向该对象

下面是阮老师博客里的两个思考题:

var name = "The Window";
var object = {
  name : "My Object",
  getNameFunc : function(){
    return function(){
      return this.name;
    };
  }
};
alert(object.getNameFunc()()); // The Window
var name = "The Window";
var object = {
    name : "My Object",
  getNameFunc : function(){
      var that = this;
    return function(){
        return that.name;
    };
  }
};
alert(object.getNameFunc()());// My Object

this的作用域好像一直是个比较奇怪的东西,对于上面两个例子,我的理解是:第一个例子,是在方法里调用的this,而这个this并没有声明,会隐式地创建一个全局变量,所以调用的全局的name;第二个,调用的that的时候,会向顶级链式查找是否声明that,而这个that有this赋值,这里的this又是通过对象方法调用,则该this指向这个object对象,所有最终调用的是object作用域内的name。不知道这么理解是不是有问题,还望大神指正。

那其实js闭包的主要目的:访问函数内部的局部变量,即延长作用域链
参考链接:js闭包MDN文档

php闭包

php回调函数

mixed call_user_func ( callable $callback [, mixed $parameter [, mixed $... ]] )
mixed call_user_func_array ( callable $callback , array $param_arr )

这两个函数都是把第一个参数作为回调函数d调用,后面接收参数,区别就是第二个函数第二参数接收数组;在使用上唯一的区别就是,call_user_func函数无法接收引用传递; 个人觉得同样是使用,call_user_func 相比call_user_func_array完全可以被替代,不知道是不是有一些性能上的优势。具体使用样例,请参考官方文档。

<?php
// 引用传递
function foo(&$a, &$b) {
    $a ++;
    $b --;
};
$a = $b = 10;
call_user_func_array('foo', [&$a, &$b]);
echo $a."\n", $b; // 11, 9
基本用法

基本用法,跟js的闭包类似
- 普通调用

<?php
global $tmp = 'hello world';
function foo() {
    var_dump(global $tmp);
}
foo(); // null, 函数内部无法直接调用上级作用域的变量,除非声明为全局变量
<?php
$foo1 = function() {
    $a = 10;
    $foo2 = function() {
        var_dump($a);
    };
    return $foo2;
};
$tmp = $foo1();
$tmp();  // null,原因同上 
  • php想要能够获取上级作用域的变量,需要通过use传递
<?php
$foo1 = function () {
    $a = 10;
    $foo2 = function () use ($a) {
        var_dump($a);
        $a ++;
    };
    $foo2();
    return $foo2;
};
$tmp = $foo1();
$tmp();  // 10, 10,  use并不能实际改变变量的值,只是值传递
<?php
$foo1 = function () {
    $a = 10;
    $foo2 = function () use (&$a) {
        var_dump($a);
        $a ++;
    };
    $foo2();
    return $foo2;
};
$tmp = $foo1();
$tmp();  // 10, 11,  通过值传递改变变量的值
  • 下面两段代码的区别,不是很明白,望大佬指点,为什么后一个值传递就可以获取到已经改变后变量的值。好像都是在调用方法之前,已经执行过变量的递增了吧?
<?php
// 值传递
$foo1 = function () {
    $a = 10;
    $foo2 = function () use ($a) {
        var_dump($a);
    };
    $a ++;
    return $foo2;
};
$tmp = $foo1();
$tmp();  // 10
<?php
// 引用传递
$foo1 = function () {
    $a = 10;
    $foo2 = function () use (&$a) {
        var_dump($a);
    };
    $a ++;
    return $foo2;
};
$tmp = $foo1();
$tmp();  // 11
  • 正确使用
<?php
// 值传递
$foo = function () {
    $a = 10;
    $foo2 = function ($num) use ($a) {
        var_dump($num + $a);
    };
    return $foo2;
};
$tmp = $foo();
$tmp(100); // 110
<?php
// 引用传递
$foo = function () {
    $a = 10;
    $foo2 = function ($num) use (&$a) {
        var_dump($num + $a);
        $a ++;
    };
    return $foo2;
};
$tmp = $foo();
$tmp(100); // 110
$tmp(100); // 111
$tmp(100); // 112  跟js类似,保证变量常驻内存
php Closure 类

共同点

都是为了扩展作用域,获取内部变量

区别

js能够在方法内部直接获取到父级作用域的变量,而php需要通过use声明,并且默认是值传递

应用场景

  • 不是很能理解应用场景,搜索了一下,很多只是写了一个闭包实现的购物车,感觉并不是那么的实用。
  • 如果只是单纯的使用匿名函数,感觉还不如封装成一个私有方法
    >这些只是个人粗鄙的理解,望指正.

phpstorm或vscode使用psr2规范

  • 安装composer
  • 全局安装phpcs
composer global require squizlabs/php_codesniffer

vscode直接插件搜索phpcs安装

phpstorm

全局安装phpcs后,会在C:\Users{user name}\AppData\Roaming\Composer\vendor\bin下生成一个phpcs.bat,后面会用到
- phpstorm -> setting
- languages & Frameworks->PHP->Code Sniffer点击Configuration右侧的按钮
- 找到刚才的phpcs.bat,点击Validate,确认
- Editor->Inspection->PHP
- 双击PHP Code Sniffer validation,点击Coding standard右侧的刷新按钮,然后选择psr2,确定

参考链接: 如何优雅地使用phpstorm?

移动端列表页操作优化

详情见ppt
移动端列表页操作优化
旧项目代码
code

大纲如下
方案一:PHP + ajax方式
tab切换使用PHP判断
第一页数据PHP获取,输出
当前页面滚动到底部,ajax加载后续数据

方案二:纯ajax方式
tab切换也使用加载
默认加载第一页数据
滚动到屏幕底部,加载下一页

方案三:本地存储缓存列表
同纯ajax加载方式
点击详情页的时候,保存当前状态
屏幕滚动位置
当前列表页数据
当前页码
初始化时,如果本地存储有数据
恢复相应数据(列表页、滚动位置、页码等)
删除本地存储数据

对比总结

优化方案
纯ajax处理
每次ajax请求的时候,将请求参数附加到当前URL上
window.location.replaceState
点击内页,本地存储中存储当前滚动条滚动位置
初始化的时候,读取url中的请求参数进行请求
page如果不是第一页,则合并请求,修改limit
读取本地存储中的滚动位置,js滚动

常见JS写法问题
PHP和JS代码混合
列表页数据统一,不要一部分PHP,一部分JS
在JS中,到处有PHP输出的内容
污染全局,各种变量,函数定义
到处有监听事件的代码,同一个事件可能都有几个地方
JS使用样式定义的class或者ID
代码耦合严重,全都堆积在一起
JS文件未压缩

如何使用CasperJs抓取商品数据

标签: 爬虫 casperjs phantomjs


编 写:袁 亮
时 间:2016-08-30
说 明:如何使用CasperJs抓取商品数据

一、目的

抓取天猫、淘宝、京东的相应商品数据
抓取商品名、价格、轮播图、详情、产品属性等数据

二、方案选择

1、情况分析
    1.1 对应的页面数据很多都是后加载的
    1.2 页面请求很多,想找到其对应的ajax请求比较麻烦
    1.3 ajax请求的地址是服务端生成的,不能通过简单的拼接来生成
    1.4 对正则水平要求较高,很多请求地址是写在js里的

2、可选方案
    2.1 php等语言,直接发钱http请求,然后通过正则匹配等方式,找到响应数据源
        curl、phpquery、Snopy等等 
    2.2 使用浏览器实际访问,然后获取浏览器的最终结果数据
        phantomJS
        casperJS + phantomJS
        python + selenium + phantomJS
3、选用方案 casperJS + phantomJS
    3.1 纯JS,方便前后端使用
    3.2 Casper封装的比较友好、文档齐全,比较好用
        特别是针对后加载的元素,之间waitForSelector的方法非常方便
        可以引入jquery等自己的js文件,方便dom操作
    3.3 获取数据,只需要在浏览器打开的时候,找到对应的节点选择器即可
    3.4 安装部署方便

三、简单例子

var casper = require('casper').create();
casper.start('http://casperjs.org/', function() {
    this.echo(this.getTitle());
});

casper.thenOpen('http://phantomjs.org', function() {
    this.echo(this.getTitle());
});
casper.run();

四、环境部署 (非源码安装)

1、安装phantomjs
    1.1 下载安装
        wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2
    1.2 解压
        tar jxvf phantomjs-2.1.1-linux-x86_64.tar.bz2
    1.3 做软连接,放在环境变量里,比如(注意权限问题,如果需要apache调用,更要注意)
        ln -s /opt/ci123/phantomjs/bin/phantomjs /usr/local/sbin/
    1.4 测试运行
        phantomjs --version

2、安装casperjs
    2.1 选择相应版本下载
        wget https://github.com/casperjs/casperjs/archive/1.1.3.zip
    2.2 解压
        unzip 1.1.3.zip 
    2.3 做软连接,放在环境变量里,比如(注意权限问题,如果需要apache调用,更要注意)
        ln -s /opt/ci123/casper/bin/casperjs /usr/bin/casperjs
    2.4 测试是否正常
        casperjs --version

五、实际使用

1、apache调用php
2、php根据url调用不同的CasperJS,传入要抓取的url
3、Casper输出json数据
4、php使用exec调用,并获取casper的输出
5、php将相应的json输出返回apache或者存入数据库等等
演示代码:
    192.168.0.249 
    /opt/ci123/www/html/yuanliang/casper/fetch

六、踩过的坑

1、抓取https的时候,249上一直失败
    原因:
        249上的ssl配置应该问题,导致抓取部分https网站的时候,一直报错
    解决办法:
        a:CasperJS需要较高版本才能用
            casperjs --ignore-ssl-errors=yes --ssl-protocol=any xxx.js
            phantomjs --ssl-protocol=any xxx.js
        b:升级249上的ca证书或者openssl版本

2、抓取速度太慢
    原因:
        这个工具本质是一个无界面的浏览器,因此里面的所有资源都跟浏览器一样,会去加载,图片等资源较多的时候会卡
    解决办法:
        设置不加载图片 loadImages:false
3、clientScripts 注入远程jquery文件无效
    原因:
        只能加载本地的js文件,不能加载远程的js文件
    解决办法:
        可以下载到本地,或者使用remoteScripts来加载,这个可以使用远程文件
        不过使用本地文件比较好,少一次http请求,速度会快很多
4、淘宝的后加载数据一直获取不到
    原因:
        之前https的出问题的时候,以为是ua有问题,所以设置了一个chrome的ua,但是实际上内核不一样
        导致淘宝本身的js执行失败,数据加载不出来
    解决办法:
        去掉ua设置即可
5、天猫的后加载数据(详情)一直加载不出来,报语法错误
    原因:
        天猫的代码中,如何屏幕大小超过1260,会执行一段JS,这段JS会出错
    解决办法:
        设置屏幕宽度小于1260即可
6、waitForSelector获取某个数据,超时,导致后面都不执行了
    原因:
        默认的timeout处理函数,会直接将整个进程中止,后面的所有代码都不执行
    解决办法:
        如果可以接受部分字段抓取不到,则可以在waitForSelector中自行设置timeout处理函数,不中止
        如果需要全部抓取才算结束,可以在timeout的时候触发一个报警等来通知
7、根据浏览器的元素定位,返回不了相应的数据
    原因:
        浏览器最终呈现的元素,有些跟phantomJs返回的不一样
        比如图片后加载,这是在页面滚动到一定位置之后才会触发的
    解决办法:
        如图片后加载这种,数据其实已经返回了,只是存在其他地方,可以返回对应的html,然后看数据存储在哪个字段上
        如果有些是点击、滚动等之后,从服务端获取的数据,那就需要模拟浏览器的点击滚动等事件,然后获取,比较少见
8、执行获取页面数据的时候,失败报错
    原因:
        页面代码执行在不同的沙箱中,在CasperJS本身是读取不到的
    解决办法:
        均在this.evaluate函数中执行,在内部可用操作所有的DOM,跟页面执行JS效果一样
9、天猫商品详情图片一直抓取不对
    原因:
        图片本身是后加载的,之前数据是放在data-ks-lazyload中,后加载执行之后,这个data被干掉了
        直接取src也会有问题,因为有的时候取数据的时候,后加载js还没执行到
        所以导致一会好一会坏
    解决办法:
        先取data-ks-lazyload,如果没有,则去src数据

七、注意事项

1、debug的时候,可用通过将当前页面截图出来,方便查看加载到什么情况了
    也可以将完整的html输出到文件里查看
2、打印json数据,方便跟php等交互
    打印数据
    casper.then(function(){
        require('utils').dump(data);
    });
3、注意很多异步操作,很多时候,执行效果会跟预期不一样
4、当Casper运行结果跟预期不一样的时候,可以考虑直接写相应的phantomJs做对应小块功能来调试
5、淘宝url等在命令行传参会出错,需要用单引号包含起来

八、参考资料

1、CasperJS 官方文档
    http://docs.casperjs.org/en/latest/index.html
2、phantomJS 文档
    http://phantomjs.org/documentation/
3、PhantomJS基础及示例
    http://imweb.io/topic/560b402ac2317a8c3e08621c
4、casperjs模拟登陆https页面获取当前面地址后发现为about:blank问题排查
    http://www.bubuko.com/infodetail-1018663.html
5、 Linux使用curl访问https站点时报错汇总
    http://www.ipcpu.com/2014/12/curl-https-error/

关于ip的那些事


编	写:袁	亮
时	间:2014-05-20
说	明:关于ip的那些事

一、ip传递过程
	1、【真实客户端】 ==> [多级代理服务器] ==> [CDN加速] ==> [前端代理nginx|squid] ==> 【apache】==> 【PHP】
	2、中文括号的是必然会经过的,英文括号是可能经过的
	3、标准的ip传递是REMOTE_ADDR和HTTP_X_FORWARDED_FOR,前一个跟当前服务连接的真实ip,后一个是请求到前一个ip之前,经过了哪些代理
	4、REMOTE_ADDR是不可伪造的,HTTP_X_FORWARDED_FOR是可以任意修改的
	5、按标准,每传递到下一层,都会将上一层的实际ip地址加入到HTTP_X_FORWARDED_FOR中,继续传递
	6、对每一层来说,只有上一层的时间地址是可信的(REMOTE_ADDR),HTTP_X_FORWARDED_FOR均有风险
	7、真实情况中,到了cdn或者前端代理之后,ip传递都是可信的(我们自己可控制),之前的都有篡改的危险

二、各服务的真实ip传递情况
	1、CDN 快网的cdn会将用户的实际地址或者代理服务器地址传递到后面的服务中
		$_SERVER["HTTP_USER_IP"]	【不用快网的时候可伪造】
		$_SERVER["HTTP_FW_ADDR"]	【不用快网的时候可伪造】
		测试了一个新的cdn测试,没有传递真实ip过来

	2、nginx代理的情况下,可以使用x_real_ip来获取真实ip(有cdn的时候,该值获取的是cdn的ip地址)
		$_SERVER["HTTP_X_REAL_IP"]	【不用nginx的时候可伪造】

	3、$_SERVER["HTTP_CLIENT_IP"] :代理服务器发送的客户端真实ip【可伪造】

三、现在使用的获取ip函数
	a、如果有HTTP_CLIENT_IP,则该ip为用户ip(可被伪造)
	b、如果有HTTP_X_FORWARDED_FOR,则将HTTP_CLIENT_IP也加入到HTTP_X_FORWARDED_FOR,判断HTTP_X_FORWARDED_FOR中的ip是否是内网的,取第一个非内网的ip为客户端真实ip
	c、经过以上两步还没有取到ip的话,则根据REMOTE_ADDR取用户的ip
	ps:该函数的问题在于,前面两个的ip都是可以被任意伪造改写,从而导致获取不到用户的真实ip情况

四、附:(线上使用的获取ip函数)
function getIp(){//获取IP函数
        $ip = false;
        if(!empty($_SERVER["HTTP_CLIENT_IP"])){
                $ip = $_SERVER["HTTP_CLIENT_IP"];
        }
        if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
                $ips = explode (", ", $_SERVER['HTTP_X_FORWARDED_FOR']);
                if ($ip) {
                        array_unshift($ips, $ip);
                        $ip = FALSE;
                }
                for ($i = 0; $i < count($ips); $i++) {
                        if (!preg_match("/^(10|172\.16|192\.168)\./", $ips[$i])) { // 判断是否内网的IP
                                $ip = $ips[$i];
                                break;
                        }
                }
        }
        return ($ip ? $ip : $_SERVER['REMOTE_ADDR']);
}



http 1.1 和 http1.0 主要区别


编	写:袁	亮
时	间:2016-01-12
说	明:http 1.1 和 http1.0 主要区别

一、持久链接keep-alive
	1、标准的1.0版本中,每次请求都必须重新建立连接、传输数据、关闭连接
		但有些http服务和浏览器也实现了Connection: Keep-Alive的功能
	2、在1.1版本,默认就是Connection: Keep-Alive
		可以在同一次TCP连接中,多次传输数据
		减少了重新建立连接的开销,特别是当一个网页中有很多图片、js、css等的时候会非常有用
		但这也有可能会导致TCP一直不释放,从而影响性能,需要权衡设置
		
二、增加了HOST
	1、在1.0版本中,不支持HOST,同一ip同一端口,只能供一个服务使用
	2、1.1版本中,支持HOST来创建虚拟主机

三、带宽优化
	1、1.1版本中,增加了RANGE头来实现断点续传,从而防止下载中断之后又要全部重新上传
	2、增加压缩,通过Accept-Encoding头来实现,减少数据传输

四、其他
	1、缓存策略
	2、新增http状态码
	3、身份认证、状态管理
	4、我们服务器上有些apache还是返回http 1.0,感觉不大正常
	
		
附:参考文档
1、http://blog.csdn.net/elifefly/article/details/3964766
2、http://www.cnblogs.com/qqzy168/p/3141849.html
3、http://www.cnblogs.com/huangfox/archive/2012/03/31/2426341.html
4、http://www.360doc.com/content/14/0730/09/1073512_398058058.shtml