一 应用场景描述

我们将要上线的一款手游后端游戏代码采用HAProxy+Keepalived+后端服务器的方式来部署。HAProxy根据游戏域名进行负载均衡,Keepalived为HAProxy提供高可用方案。最近有空就深入研究了一下HAProxy的官方架构文档。这里提一下,很多游戏公司在部署游戏服的时候,没有采用负载均衡的方式,而是采用一台服务器上部署很多个区服这种模式,实际上根本不能达到节省系统资源的目的,因为一台服务器能够承载的游戏区服是不好控制的,这种模式也不好对后端游戏服务器进行容量规划和动态增减。主要还是运维同事没有和开发同事关于游戏代码部署没有沟通好,如果开发同事在开发期间没有考虑部署的方便性,那后期游戏上线后,运维同事就会一直做些没有技术含量的重复劳作。


二 HAProxy简单介绍

HAProxy是一个为基于TCP和HTTP应用提供免费的,快速的,可靠的高可用功能,负载均衡均衡功能和反向代理功能的方案。它特别适合应用在那些需要高并发访问并需要作七层处理和会话保持的高负载网站。

haproxy-pmode.png


目前主要使用的两个版本是1.3和1.4.


三 HAProxy配置案例

HAProxy作者在http://haproxy.1wt.eu/download/1.3/doc/architecture.txt 文档中详细描述了HAProxy相关的配置案例。以下内容主要来自这个文档的翻译。

在以下各个案例中将省略全局配置参数和默认配置参数。使用以下gloabl和defaults配置。

以下案例都是在CentOS 6.4下测试完成。可以使用yum install haproxy 安装。

global
    log         127.0.0.1 local2
    chroot      /var/lib/haproxy
    pidfile     /var/run/haproxy.pid
    maxconn     4000
    user        haproxy
    group       haproxy
    daemon
    stats socket /var/lib/haproxy/stats

defaults
    mode                    http
    log                     global
    option                  httplog
    option                  dontlognull
    retries                 3
    timeout http-request    10s
    timeout queue           1m
    timeout connect         10s
    timeout client          1m
    timeout server          1m
    timeout http-keep-alive 10s
    timeout check           10s                设置对服务器进行健康检查周期
    maxconn                 3000               设置最大并发连接数


案例1.带cookie插入的简单HTTP负载均衡

spacer.gifwKioL1OEZbiSlj6dAAx9SKgPl0s096.bmp

如上图所示,一台WEB服务器上面运行Nginx和PHP-FPM服务,后端是一台MySQL数据库。由于用户访问量增加,这台WEB服务器的CPU已经趋于饱和状态。MySQL数据库服务器的压力不大。如果将这台WEB服务器替换成一台处理能力更强的高配置服务器,可能比增加同样的几台低配置服务器的成本更高。所以考虑增加几台低配置服务器并部署相同的服务。但是由于一些用户数据如用户会话等没有存储在后端MySQL数据库中,而是存储在WEB服务器上,所以,简单的利用IP或TCP负载均衡调度增加几台WEB服务器将不能满足需求。将改用以下的架构

wKioL1OFxMSgtQTkABHcnBGY_7o043.bmp

在192.168.1.1这台WEB服务器上安装HAProxy,通过HAProxy将用户请求转发到后端新增加的4台WEB服务器达到增加用户请求处理能力的目的。

在LB1上配置HAProxy如下内容:

listen webfarm 192.168.1.1:80
       mode http
       balance roundrobin
       cookie SERVERID insert indirect
       option httpchk HEAD /index.html HTTP/1.0
       server webA 192.168.1.11:80 cookie A check
       server webB 192.168.1.12:80 cookie B check
       server webC 192.168.1.13:80 cookie C check
       server webD 192.168.1.14:80 cookie D check

在LB1上配置好HAProxy后,LB1将接受用户的所有请求。如果一个用户请求不包含任何cookie,那这个请求将被HAProxy转发到一台可用的WEB服务器。可能是webA,webB,webC或webD。然后HAProxy将把处理这个请求的WEB服务器的cookie值插入到请求响应中。如SERVERID=A。当这个客户端再次访问并在HTTP请求头中带有SERVERID=A,HAProxy将会把它的请求直接转发给webA处理。在请求到达webA之前,cookie将被移除,webA将不会看到这个cookie。如果webA不可用,对应的请求将被转发到其他可用的WEB服务器,相应的cookie值也将被重新设置。

可以使用curl -I 192.168.1.1查看cookie设置如:

HTTP/1.1 200 OK
Server: nginx/1.6.0
Date: Thu, 29 May 2014 04:12:09 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Thu, 24 Apr 2014 13:33:03 GMT
Connection: keep-alive
ETag: "5359128f-264"
Accept-Ranges: bytes
Set-Cookie: SERVERID=A; path=/

使用以上配置需要注意以下几点:

1)如果客户端使用HTTP1.1(keep-alive),只有第一个响应才会被插入cookie,并且HAProxy只会分析每个session的第一个请求。在使用cookie的插入模式下,可能不会造成什么问题,因为HAProxy会立即在第一个响应中插入cookie,同一个session中的所有请求将被转发到同一台服务器上。但是,到达服务器端的请求中的cookie不会被删除,所以这要求后端服务器对来历不明的cookie不作敏感处理。如果不想引起其他的问题,可以使用以下参数关掉keep-alive

option httpclose

2)由于一些原因,一些客户端不能识别多个cookie,并且后端应用已经设置了cookie,如果HAProxy再对响应插入cookie后,这些客户端可能不能识别cookie。这种情况下,可以使用cookie的prefix模式。

3)LB1成为整个架构中的瓶颈,如果LB1不可用,那么客户端的所有请求都得不到响应。可以使用Keepalived配置HAProxy解决LB1单点故障的问题。

4)在以上配置中后端服务器是看不到客户端的真实IP地址的。可以使用 forwardfor 参数,它能够在客户端的请求头中增加 X-Forwarded-For 字段。同时需要配置使用 httpclose 参数,确保每个请求都被重写,而不只是第一个请求被重写。

option httpclose
option forwardfor

后端WEB服务器上的Nginx的日志格式需要增加$http_x_forwarded_for 变量

 log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';


在有些情况下,一些用户会将他们的浏览器的cookie功能关闭。这时候可能没法访问WEB服务器的内容。这种情况下,可以使用HAProxy的 source 负载均衡算法替代  roundroubin 算法。source 算法可以确保来自同一个IP的所有请求都到达同一台WEB服务器,但是前提是,可用的服务器的数量保持不变。

balance source

不要在一个小型网络和代理服务器后面使用source 算法,因为请求分发会不太公平,但是在大型内部网络或互联网上使用source 算法,转发效率很好。对于那些具有动态IP地址的客户端,只要它们能够接收cookie,那么请求转发就不会受到影响,因为HAProxy对cookie的处理具有较高优先级。

 listen webfarm 192.168.1.1:80
       mode http
       balance source
       option httpclose
       option forwardfor
       cookie SERVERID insert indirect
       option httpchk HEAD /index.html HTTP/1.0
       server webA 192.168.1.11:80 cookie A check
       server webB 192.168.1.12:80 cookie B check
       server webC 192.168.1.13:80 cookie C check
       server webD 192.168.1.14:80 cookie D check


现在在同一台客户端访问192.168.1.1,可以看到每次HTTP响应中设置的cookie值都相同。

$ curl -I 192.168.1.1
HTTP/1.1 200 OK
Server: nginx/1.6.0
Date: Fri, 30 May 2014 02:12:13 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Thu, 24 Apr 2014 13:33:03 GMT
Connection: close
ETag: "5359128f-264"
Accept-Ranges: bytes
Set-Cookie: SERVERID=A; path=/

$ curl -I 192.168.1.1
HTTP/1.1 200 OK
Server: nginx/1.6.0
Date: Fri, 30 May 2014 02:12:15 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Thu, 24 Apr 2014 13:33:03 GMT
Connection: close
ETag: "5359128f-264"
Accept-Ranges: bytes
Set-Cookie: SERVERID=A; path=/

将负载均衡算法设置成 source 后,连续两次访问192.168.1.1,HTTP响应中设置的cookie都是SERVERID=A 。


现在让webA上的nginx停掉,然后再使用curl进行探测

$ curl -I 192.168.1.1
HTTP/1.1 200 OK
Server: nginx/1.6.0
Date: Fri, 30 May 2014 02:17:33 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Thu, 24 Apr 2014 13:33:03 GMT
Connection: close
ETag: "5359128f-264"
Accept-Ranges: bytes
Set-Cookie: SERVERID=B; path=/

$ curl -I 192.168.1.1
HTTP/1.1 200 OK
Server: nginx/1.6.0
Date: Fri, 30 May 2014 02:17:35 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Thu, 24 Apr 2014 13:33:03 GMT
Connection: close
ETag: "5359128f-264"
Accept-Ranges: bytes
Set-Cookie: SERVERID=B; path=/


可以看到HTTP响应中 cookie 被设置成了 SERVERID=B


现在将webB,webC,webD上的nginx都停掉,再通过curl -I 192.168.1.1探测HTTP 响应头

$ curl -I 192.168.1.1
HTTP/1.0 503 Service Unavailable
Cache-Control: no-cache
Connection: close
Content-Type: text/html

$ curl -I 192.168.1.1
HTTP/1.0 503 Service Unavailable
Cache-Control: no-cache
Connection: close
Content-Type: text/html


可以看到当所有的WEB服务器都不可用时,将返回HAProxy的503错误。