第 9-1 章 Linux haproxy 服务基础配置

作者: Brinnatt 分类: ARM64 Linux 进阶架构 发布时间: 2022-01-21 14:59

官方站点:http://www.haproxy.org/

HAProxy 是一个使用 C 语言编写的自由及开放源代码软件,其提供高可用性、负载均衡,以及基于 TCP 和 HTTP 的应用程序代理。

HAProxy 特别适用于那些负载特大的 web 站点,这些站点通常又需要会话保持或七层处理。HAProxy 运行在当前的硬件上,完全可以支持数以万计的并发连接。并且它的运行模式使得它可以很简单安全的整合进您当前的架构中, 同时可以保护你的 web 服务器不被暴露到网络上。

HAProxy 实现了一种事件驱动,单一进程模型,此模型支持非常大的并发连接数。多进程或多线程模型受内存限制 、系统调度器限制以及无处不在的锁限制,很少能处理数千并发连接。

事件驱动模型因为在有更好的资源和时间管理的用户空间(User-Space) 实现所有这些任务,所以没有这些问题。此模型的弊端是,在多核系统上,这些程序通常扩展性较差。这就是为什么他们必须进行优化以使每个 CPU 时间片(Cycle)做更多的工作。

包括 GitHubBitbucket、Stack Overflow、RedditTumblrTwitterTuenti 在内的知名网站,及亚马逊网络服务系统都使用了 HAProxy。

注意:本篇及后续对 haproxy 的讲解融合了 haproxy v1.7 到 haproxy v2.5 版本,因为生产环境上我都在用。单一针对某个版本没有必要,因为大版本之间的变化对于使用者来说,不过是废弃一些选项,新增一些选项而已。

9.1、安装 haproxy

CentOS 操作系统的官方源中都自带 haproxy,但是版本都比较老。

[root@armv8_1 ~]# cat /etc/redhat-release 
CentOS Linux release 7.9.2009 (AltArch)
[root@armv8_1 ~]# uname -a
Linux armv8_1 4.18.0-193.28.1.el7.aarch64 #1 SMP Wed Oct 21 16:25:35 UTC 2020 aarch64 aarch64 aarch64 GNU/Linux
[root@armv8_1 ~]# 
[root@armv8_1 ~]# yum info haproxy
Loaded plugins: fastestmirror
Loading mirror speeds from cached hostfile
Available Packages
Name        : haproxy
Arch        : aarch64
Version     : 1.5.18
Release     : 9.el7_9.1
Size        : 809 k
Repo        : updates/7/aarch64
Summary     : TCP/HTTP proxy and load balancer for high availability environments
URL         : http://www.haproxy.org/
License     : GPLv2+
Description : HAProxy is a TCP/HTTP reverse proxy which is particularly suited for high
            : availability environments. Indeed, it can:
            :  - route HTTP requests depending on statically assigned cookies
            :  - spread load among several servers while assuring server persistence
            :    through the use of HTTP cookies
            :  - switch to backup servers in the event a main server fails
            :  - accept connections to special ports dedicated to service monitoring
            :  - stop accepting connections without breaking existing ones
            :  - add, modify, and delete HTTP headers in both directions
            :  - block requests matching particular patterns
            :  - report detailed status to authenticated users from a URI
            :    intercepted by the application

[root@armv8_1 ~]#

当前最新的稳定版本为 haproxy-2.5.0,下面进行编译安装。

编译安装 haproxy 时,可以借助于 pcre 环境,该环境下编译时借助正则表达式分析编译速度会快很多,但是没有该环境也可以安装。

[root@armv8_1 ~]# yum install pcre pcre-devel -y
[root@armv8_1 ~]# yum groups install "Development Tools" -y
[root@armv8_1 ~]# tar xf haproxy-2.5.0.tar.gz 
[root@armv8_1 ~]# cd haproxy-2.5.0/
[root@armv8_1 haproxy-2.5.0]# make TARGET=linux-glibc PREFIX=/usr/local/haproxy USE_PCRE=1
[root@armv8_1 haproxy-2.5.0]# make install PREFIX=/usr/local/haproxy

使用 make help 查看编译设置,TARGET 设置操作系统和版本,然后自动开启相关特性。haproxy v2.0 以前通过 linux22、linux24、linux26、linux2628 进行设置,haproxy v2.0 以后的版本废弃了这种设置,只提供以下选项:

TARGET not set, you may pass 'TARGET=xxx' to set one among :
  linux-glibc, linux-glibc-legacy, solaris, freebsd, dragonfly, netbsd,
  osx, openbsd, aix51, aix52, aix72-gcc, cygwin, haiku, generic,
  custom

区别可以使用如下方式查看:

[root@armv8_1 haproxy-2.5.0]# make help TARGET=generic
......
Current TARGET: generic

Enabled features for TARGET 'generic' (disable with 'USE_xxx=') :
  POLL TPROXY SLZ
......

[root@armv8_1 haproxy-2.5.0]# make help TARGET=linux-glibc
......
Current TARGET: linux-glibc

Enabled features for TARGET 'linux-glibc' (disable with 'USE_xxx=') :
  EPOLL NETFILTER POLL THREAD BACKTRACE TPROXY LINUX_TPROXY LINUX_SPLICE
  LIBCRYPT CRYPT_H GETADDRINFO ACCEPT4 SLZ CPU_AFFINITY TFO NS DL RT
  PRCTL THREAD_DUMP
......
  • 使用 USE_PCRE=1 表示使用 PCRE 环境编译,加快编译速度。

编译安装完成后,只有 3 个目录:doc、share 和 sbin,sbin 里面只有一个 haproxy 的主程序 haproxy,所以要手动创建几个文件才行。

  1. 创建 haproxy.cfg 配置文件
[root@armv8_1 ~]# vim /etc/haproxy/haproxy.cfg 
global
    log         127.0.0.1 local2
    chroot      /var/lib/haproxy
    pidfile     /var/run/haproxy.pid
    maxconn     20000
    user        haproxy
    group       haproxy
    daemon
    stats socket /var/lib/haproxy/stats
    spread-checks 2
defaults
    mode                    http
    log                     global
    option                  httplog
    option                  dontlognull
    option http-server-close
    option forwardfor       except 127.0.0.0/8
    option                  redispatch
    timeout http-request    2s
    timeout queue           3s
    timeout connect         1s
    timeout client          10s
    timeout server          2s
    timeout http-keep-alive 10s
    timeout check           2s
    maxconn                 18000 

frontend http-in
    bind             *:80
    mode             http
    log              global
    capture request  header Host len 20
    capture request  header Referer len 60
    acl url_static   path_beg  -i /static /images /stylesheets
    acl url_static   path_end  -i .jpg .jpeg .gif .png .ico .bmp .css .js
    acl url_static   path_end  -i .html .htm .shtml .shtm .pdf .mp3 .mp4 .rm .rmvb .txt
    acl url_static   path_end  -i .zip .rar .gz .tgz .bz2 .tgz

    use_backend      StaticGroup   if url_static
    default_backend  DynamicGroup

backend StaticGroup
    balance            roundrobin
    option             http-keep-alive
    http-reuse         safe
    option httpchk     GET /index.html
    http-check expect  status 200
    server staticsrv1  192.168.122.14:80 check rise 1 maxconn 5000
    server staticsrv2  192.168.122.15:80 check rise 1 maxconn 5000

backend DynamicGroup
    cookie appsrv insert nocache
    balance roundrobin
    option http-server-close
    option httpchk     GET /index.php
    http-check expect  status 200
    server appsrv1 192.168.122.12:80  check rise 1 maxconn 3000 cookie appsrv1
    server appsrv2 192.168.122.13:80  check rise 1 maxconn 3000 cookie appsrv2

listen report_stats
        bind *:8081
        stats enable
        stats hide-version
        stats uri    /hastats
        stats realm  "pls enter your name"
        stats auth   admin:admin
        stats admin  if TRUE
[root@armv8_1 ~]#
  1. 创建 systemd 管理 haproxy 的配置文件 haproxy.service
[root@armv8_1 ~]# vim /usr/lib/systemd/system/haproxy.service 
[Unit]
Description=Haproxy Service
After=syslog.target network.target

[Service]
Type=forking
ExecStart=/usr/local/haproxy/sbin/haproxy -f /etc/haproxy/haproxy.cfg
ExecStop=cd&&/usr/local/haproxy/sbin/&&pkill haproxy
[Install]
WantedBy=multi-user.target
[root@armv8_1 ~]#
  1. haproxy.cfg 配置文件中的 stats socket /var/lib/haproxy/stats 指令要提前创建文件
[root@armv8_1 ~]# mkdir /var/lib/haproxy/
[root@armv8_1 ~]# touch /var/lib/haproxy/stats
  1. 启动 haproxy 服务
[root@armv8_1 ~]# systemctl start haproxy

9.2、haproxy 命令

详细内容参见:http://cbonte.github.io/haproxy-dconv/

介绍几个常用的:

# 检查配置文件语法
haproxy -c -f /etc/haproxy/haproxy.cfg

# 以daemon模式启动,以systemd管理的daemon模式启动
haproxy -D -f /etc/haproxy/haproxy.cfg [-p /var/run/haproxy.pid]
haproxy -Ds -f /etc/haproxy/haproxy.cfg [-p /var/run/haproxy.pid]

# 启动调试功能,将显示所有连接和处理信息在屏幕
haproxy -d -f /etc/haproxy/haproxy.cfg

# restart。需要使用st选项指定pid列表
haproxy -f /etc/haproxy.cfg [-p /var/run/haproxy.pid] -st `cat /var/run/haproxy.pid`

# graceful restart,即reload。需要使用sf选项指定pid列表
haproxy -f /etc/haproxy.cfg [-p /var/run/haproxy.pid] -sf `cat /var/run/haproxy.pid`

# 显示haproxy编译和启动信息
haproxy -vv

需要注意的是,restart 会直接关掉旧进程并建立新进程,所以会丢弃大量已建立的连接,而 reload 会启动新进程,但旧进程会先处理完当前已建立连接然后再关闭。

但是,reload 仍然会丢弃极少量的连接,虽然大多数情况下这足够完美了,但是在极度严格的环境下,这是不允许的。

haproxy 1.8 版本后,提供了完全不丢弃连接的无损重启,要求 haproxy 启动命令中加入 -x 选项,同时要求 haproxy 配置文件的 "stats socket" 配置中加入 expose-fd listeners,比如:

stats socket /var/run/haproxy.sock mode 600 expose-fd listeners level user

使用 -x 选项以及 expose-fd listeners 之后,reload haproxy 的时候,会将已建立 TCP 连接(TCP 套接字)转移到 Unix Domain 状态套接字中进行处理。

9.3、haproxy 丰富特性

haproxy 是一款负载均衡软件,它工作在 7 层模型上,可以分析数据包中的应用层协议,并按规则进行负载。通常这类 7 层负载工具也称为反向代理软件,nginx 是另一款著名的反向代理软件。

haproxy 支持使用 splice() 系统调用,它可以将数据在两个套接字之间在内核空间直接使用管道进行传递,无需再在 kernel buffer --> app buffer --> kernel 之间来回复制数据,实现零复制转发(Zero-copy forwarding),还可以实现零复制启动(zero-starting)。

haproxy 默认对客户端的请求和对服务端的响应数据都开启了 splice 功能,它自身对数据状态进行判断,决定此数据是否启用 splice() 进行管道传递,这能极大提高性能。

9.3.1、haproxy 的特性(1):连接保持和连接关闭

先说明 HTTP 协议事务模型。

http 协议是事务驱动的,意味着每个 request 产生且仅产生一个 response。客户端发送请求时,将建立一个从客户端到服务端的 TCP 连接,客户端发送的每一个 request 都经过此连接传送给服务端,然后服务端发出 response 报文。随后这个 TCP 连接将关闭,下一个 request 将重新打开一个 tcp 连接进行传送。

[conn1][req1]......[resp1][close1][conn2][req2]......[resp2][close2]......
  • 这种模式称为 "http close" 模式。这种模式下,有多少个 http 事务就有多少个连接,且每发出一个 response 就关闭一次 tcp 连接。这种情况下,客户端不知道 response 中 body 的长度。
  • 如果 "http close" 可以避免 "tcp 连接随 response 而关闭",那么它的性能就可以得到一定程度的提升,因为频繁建立和关闭 tcp 连接消耗的资源和时间是较大的。

那么如何进行提升?在 server 端发送 response 时给出 content-length 的标记,让客户端知道还有多少内容没有接收到,没有接收完则 tcp 连接不关闭。这种模式称为 "keep-alive"

[conn][req1]...[resp1][req2]...[resp2][close]

另一种提升 "http close" 的方式是 "pipelining" 模式。它仍然使用 "keep-alive" 模式,但是客户端不需要等待收到服务端的 response 后才发送后续的 request。这在请求一个含有大量图片的页面时很有用。这种模式类似于累积报文数量成一批或完成后才一次性发送,能很好的提升性能。

[conn][req1][req2]...[resp1][resp2][close]...

很多 http 代理不支持 pipelining,因为它们无法将 response 和相应的 request 在一个 http 协议中联系起来,而 haproxy 可以在 pipelinign 模式下对报文进行重组。

默认 haproxy 操作在 keep-alive 模式:对于每一个 tcp 连接,它处理每一个 request 和 response,并且在发送 response 后连接两端都处于空闲状态一段时间,如果该连接的客户端发起新的 request,则继续使用此连接。

haproxy 支持 5 种连接模式:

  1. keep alive:分析并处理所有的 request 和 response(默认),后端为静态或缓存服务器建议使用此模式
  2. tunnel:仅分析处理第一个 request 和 response,剩余所有内容不进行任何分析直接转发。1.5 版本之前此为默认,现在不建议设置为此模式。
  3. passive close:在请求和响应首部加上 "connection:close" 标记的 tunnel,在处理完第一个 request 和 response 后尝试关闭两端连接。
  4. server close:处理完第一个 response 后关闭和 server 端的连接,但和客户端的连接仍然保持,后端为动态应用程序服务器组建议使用此模式
  5. forced close:传输完一个 response 后客户端和服务端都关闭连接。

9.3.2、haproxy 的特性(2):会话保持

任何一个反向代理软件,都必须具备这个基本的功能,因为 http 是 "无状态" 的。这主要针对后端是应用服务器的情况,如果后端是静态服务器或缓存服务器,无需实现会话保持。

如果反向代理的后端提供的是 "有状态" 的服务或协议时,必须保证请求过一次的客户端能被引导到同一服务端上。只有这样,服务端才能知道这个客户端是它曾经处理过的,能查到并获取到和该客户端对应的上下文环境(session 上下文),有了这个 session 环境才能继续为该客户端提供后续的服务。

  • 简单举个例子,客户端 A 向服务端 B 请求将 C 商品加入它的账户购物车,加入成功后,服务端 B 会在某个缓存中记录下客户端 A 和它的商品 C,这个缓存的内容就是 session 上下文环境。
    • 而识别客户端的方式一般是设置 session ID(如 PHPSESSID、JSESSIONID),并将其作为 cookie 的内容交给客户端。
    • 客户端 A 再次请求的时候(比如将购物车中的商品下订单)只要携带这个 cookie,服务端 B 就可以从中获取到 session ID 并找到属于客户端 A 的缓存内容,也就可以继续执行下订单部分的代码。
  • 假如这时使用负载均衡软件对客户端的请求进行负载,如果这个负载软件只是简单地进行负载转发,就无法保证将客户端 A 引导到服务端 B,可能会引导到服务端 X、服务端 Y,但是 X、Y 上并没有缓存和客户端 A 对应的 session 内容,当然也无法为客户端 A 下订单。

因此,反向代理软件必须具备将客户端和服务端 "绑定" 的功能,也就是所谓的提供会话保持,让客户端 A 后续的请求一定转发到服务端 B 上。

9.3.2.1、源地址 hash 算法实现会话保持

作为负载均衡软件,一般都会提供一种称为源地址 hash 的调度算法,将客户端的 IP 地址结合后端服务器数量和权重做散列计算,每次客户端请求时都会进行同样的 hash 计算,这样同一客户端总能得到相同的 hash 值,也就能调度到同一个服务端上。

一般来说,除非无路可选,都不应该选择类似源地址 hash 这样的算法。因为只要后端服务器的权重发生任何一点改变,所有源 IP 地址的 hash 值几乎都会改变,这是非常大的动荡。

9.3.2.2、cookie 实现会话保持

作为反向代理软件,一般还提供一种 cookie 绑定的功能实现会话保持。反向代理软件为客户端 A 单独生成一个 cookie1,或者直接修改应用服务器为客户端设置的 cookie2,最后将 cookie 通过在响应报文中设置 "Set-Cookie" 字段响应给客户端。

与此同时,反向代理软件会在内存中维持一张 cookie 表,这张表记录了 cookie1 或修改后的 cookie2 对应的服务端。只要客户端请求报文中的 "Cookie" 字段中携带了 cookie1 或 cookie2 属性的请求到达反向代理软件时,反向代理软件根据 cookie 表就能检索到对应的服务端是谁。

需要注意的是,客户端收到的 cookie 可能来源有两类:一类是反向代理软件增加的,这时客户端收到的响应报文中将至少有两个 "Set-Cookie" 字段,其中一个是反代软件的,其他是应用服务器设置的;一类是反向代理软件在应用服务器设置的 Cookie 基础上修改或增加属性。

例如,当配置 haproxy 插入 cookie 时,客户端从第一次请求到第二次请求被后端应用程序处理的过程大致如下图所示:

haproxy cookie

9.3.2.3、stick table 实现会话粘性

haproxy 还提供另一种 stick table 功能实现会话粘性(stickiness)

  • 这张 stick-table 表非常强大,它可以根据提取客户端请求报文中的内容或者源 IP 地址或者提取响应报文中的内容(例如应用服务器设置的 Session ID 这个 cookie)作为这张表的 key,将后端服务器的标识符 ID 作为 key 对应的 value。
  • 只要客户端再次请求,haproxy 就能对请求进行匹配(match),无论是源 IP 还是 cookie 亦或是其它字符串作为 key,总能匹配到对应的记录,而且匹配速度极快,再根据 value 转发给对应的后端服务器。

例如,下图是一张最简单的 stick table 示意图:

haproxy stick-table

该 stick-table 存储的 key 是客户端的源 IP 地址,当客户端第一次请求到达 haproxy 后,haproxy 根据调度算法为其分配一个后端 appserver,当请求转发到达后端后,haproxy 立即在 stick table 表中建立一条 ip 和 appserver 的粘性(stickiness)记录。之后,无论是否使用 cookie,haproxy 都能为该客户端找到它对应的后端服务器。

  • stick table 的强大远不止会话粘性。还可以根据需要定制要记录的计数器和速率统计器,例如在一个时间段内总共流入了多少个连接、平均每秒流入多少个连接、流入流出的字节数和平均速率、建立会话的数量和平均速率等。

  • 更强大的是,stick table 可以在 "主主模型" 下进行 stick 记录复制(replication),它不像 session 复制(copy),节点一多,无论是节点还是网络带宽的压力都会暴增。

    • haproxy 每次推送的是 stick table 中的一条记录,而不是整表整表地复制,而且每条记录占用的空间很小(最小时每条记录 50 字节),使得即使在非常繁忙的情况下,在几十台 haproxy 节点之间复制都不会占用太多网络带宽。
    • 借助 stick table 的复制,可以完完整整地实现 haproxy "主主模型",保证所有粘性信息都不会丢失,从而保证 haproxy 节点 down 掉也不会让客户端和对应的服务端失去联系。

9.3.2.4、session 共享

无论反向代理软件实现的会话保持能力有多强,功能有多多,只要后端是应用服务器,就一定是 "有状态" 的。有状态对于某些业务逻辑来说是必不可少的,但对架构的伸缩和高可用带来了不便。我们无法在架构中随意添加新的代理节点,甚至无法随意添加新的应用服务器,高可用的时候还必须考虑状态或者某些缓存内容是否会丢失。

如果将所有应用服务器的 session 信息全部存储到一台服务器上(一般放在 redis 或数据库中)进行共享,每台应用服务器在需要获取上下文的时候从这台服务器上取,那么应用服务器在取 session 消息之前就是 "无状态" 的。

例如,下面是一个后端使用 session 共享的示意图:

session share

使用 session share 后,调度器无论将请求调度到哪个后端上,这个后端都能从 session share 服务器上获取到对应的 session 上下文。这样无状态的请求完全可以被任意负载,负载软件无需记住后端服务器,从而达到四层负载的效果。如果没有特殊需求(如处理 7 层协议),这时可以使用 LVS 替代 haproxy,因为在负载性能上,LVS 比 haproxy 高好几个级别。

session 共享给架构带来的好处非常多,正如上面所说的,可以使用 LVS 进行极其高效的负载(前提是没有 LVS 无法实现的需求),无论是负载节点还是应用服务器节点都可以随意增删服务器。而唯一需要保证的就是 session 共享服务器的高可用。

9.3.3、haproxy 的特性(3):后端健康状况检查和被检查

任何一个负载均衡软件,都应该提供后端服务器健康状况检查的功能,即使自身没有,也必须能够借助其他第三方工具来实现。只有具备后端健康检查的功能,在后端某服务器 down 掉的时候,调度器才能将它从后端服务器组中踢出去,保证客户端的请求不会被调度到这台 down 掉的服务器上。

haproxy 为多种协议类型提供了健康状况检查的功能,除了最基本的基于 tcp 的检查,还有以下几种协议提供健康检查:

  1. HTTP
  2. ldap
  3. mysql
  4. pgsql
  5. redis
  6. SPOP
  7. smtp

如果 haproxy 没有指定基于哪种协议进行检查,默认会使用 tcp 协议进行检查,这种检查的健康判断方式就是能否连上后端。例如:

backend StaticGroup
    server staticsrv1 192.168.122.12:80 check rise 1
    server staticsrv2 192.168.122.13:80 check rise 1
  • 在 server 指令中的 check 设置的是是否开启健康检查功能,以及检查的时间间隔、判断多少次不健康后就认为后端下线了以及成功多少次后认为后端重新上线了。

如果要基于其它协议检查,需要使用协议对应的 option 指令显式指定要检查的对象。且前提是 server 中必须指定 check,这是控制检查与否的开关。例如,基于 http 协议检查:

backend DynamicGroup
    option httpchk     GET /index.php
    server appsrv1 192.168.122.12:80  check
    server appsrv2 192.168.122.13:80  check

对于基于 http 协议的检查,haproxy 提供了多种判断健康与否的方式,可以通过返回状态码或拿状态码来进行正则匹配、通过判断响应体是否包含某个字符串或者对响应体进行正则匹配。例如:

backend DynamicGroup1
    option httpchk     GET /index.php
    http-check expect  status 200
    server appsrv1 192.168.122.12:80  check
    server appsrv2 192.168.122.13:80  check
backend DynamicGroup2
    option httpchk     GET /index.php
    http-check expect  ! string error
    server appsrv1 192.168.122.12:80  check
    server appsrv2 192.168.122.13:80  check
  • 上面两个后端组都指定了使用 http 协议进行检查,并分别使用 http-check expect 指定了要检查到状态码 200、响应体中不包含字符串 "error" 才认为健康。
  • 如果不指定 http-check expect 指令,那么基于 http 协议检查的时候,只要状态码为 2xx 或 3xx 都认为是健康的。

haproxy 除了具备检查后端的能力,还支持被检查,只需要使用 monitor 类的指令即可。所谓被检查,指的是 haproxy 可以指定一个检查自己的指标,自己获取检查结果,并将检查状态上报给它的前端或高可用软件,让它们很容易根据上报的结果(200 或 503 状态码)判断 haproxy 是否健在。

以下是两个被检查的示例:

frontend www
    mode http
    monitor-uri /haproxy_test

frontend www
   mode http
   acl site_dead nbsrv(dynamic) lt 2
   acl site_dead nbsrv(static)  lt 2
   monitor-uri   /site_alive
   monitor fail  if site_dead
  • 第一个示例中,"/haproxy_test" 是它的前端指定要检查的路径,此处 haproxy 对该 uri 路径进行监控,当该路径正常时,haproxy 会告诉前端 "HTTP/1.0 200 OK",当不正常时,将 "HTTP/1.0 503 Service unavailable"。
  • 第二个示例中,不仅监控了 "/site_alive",还监控了后端健康节点的数量。当 dynamic 或 static 后端组的健康节点数量少于 2 时,haproxy 立即主动告诉前端 "HTTP/1.0 503 Service unavailable",否则返回给前端 "HTTP/1.0 200 OK"。

9.3.4、haproxy 的特性(4):处理请求和响应报文

一个合格的反向代理软件,必须能够处理流入的请求报文和流出的响应报文。具备这些能力后,不仅可以按照需求改造报文,还能筛选报文,防止被恶意攻击。

haproxy 提供了很多处理请求、响应报文的功能性指令,还有一些所谓 "函数",对 4、5、6、7 层协议进行抓包处理。

大多数处理请求报文的函数都以 "req" 或 "capture.req." 开头,处理响应报文的函数都以 "res." 或 "capture.res."开头,这样的函数非常多,几乎可以实现任何想达到的功能。完整的指令集见官方手册:https://cbonte.github.io/haproxy-dconv/2.4/configuration.html#7.3

9.3.5、haproxy 的特性(5):状态查看

作为反向代理,必须具备查看自身和后端服务器的状态信息。

haproxy 提供了多种获取状态信息的方法:

  1. 使用 stats enable 指令启用状态报告功能,这样就可以在浏览器中输入特定的 url 访问状态信息。

  2. 提供了很多对前端状态和后端节点状态取样调查的函数。例如某指定后端或所有后端有多少个节点存活、某后端或所有后端已建立多少连接、后端还有多少连接槽位可以继续提供连接、前端建立了多少连接等等。完整的指令集见官方手册:https://cbonte.github.io/haproxy-dconv/2.4/configuration.html#7.3

  3. 提供了套接字状态查看、管理功能。也许很多人都不知道,默认配置文件中的 stat socket 指令是干吗用的,其实这就是为系统管理员提供的接口。

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

我们安装 "socat" 包(socket cat,在 epel 源提供该包)后,就可以通过 socat 命令来查看 /var/lib/haproxy/stats 这个状态套接字。例如,执行下面的命令可以获取到所有可执行的命令。

echo "help" | socat unix:/var/lib/haproxy/stats -

例如,其中一条命令是 "show backend" 用来列出所有的 backend,可以这样使用:

echo "show backend" | socat unix:/var/lib/haproxy/stats -

或者也可以进入交互式操作模式:

socat readline unix:/var/lib/haproxy/stats

9.3.6、haproxy 的特性(6):ACL

ACL 本意是 access control list(访问控制列表),用来定义一组黑名单或白名单。但显然,它绝不仅仅是为了黑白名单而存在的,有了 ACL,可以随意按条件定制一组或多组列表。ACL 存在的意义,就像是正则表达式存在的意义一样,极大程度上简化了软件在管理上的复杂度。

在 haproxy 中,只要能在逻辑意义上进行分组的,几乎都可以使用 ACL 来定制。比如哪些 IP 属于 A 组,后端哪些节点是静态组,后端节点少于几个时属于 dead 状态等等。

9.3.7、haproxy 的特性(7):连接重用功能

haproxy 支持后端连接重用的功能。

在默认情况下(不使用连接重用),当某客户端的请求到来后,haproxy 为了将请求转发给后端,会和后端某服务器建立一个 TCP 连接,并将请求调度到该服务器上,该客户端后续的请求也会通过该 TCP 连接转发给后端(假设没有采用关闭后端连接的 http 事务模型)。但在响应后和该客户端的下一个请求到来前,这个连接是空闲的。

其实仔细想想,和后端建立的 TCP 连接仅仅只是为了调度转发,免去后续再次建立 tcp 连接的消耗。完全可以为其它客户端的请求调度也使用这个 TCP 连接,保证 TCP 连接资源不浪费。可以使用 http-reuse strategy_name 指令设置连接重用的策略,而默认策略禁用连接重用。

该指令有 4 个值:

  1. never:这是默认设置。表示禁用连接重用,因为老版本的 haproxy 认为来源不同的请求不应该共享同一个后端连接。
  2. safe:这是建议使用的策略。"安全"策略下,haproxy 为客户端的每个第一个请求都单独建立一个和后端的 TCP 连接,但是后续的请求则会重用和该后端的空闲 TCP 连接。这样的转发不仅提高了资源使用率,还保持了 keep-alive 的功能。因此,safe 策略配合 http-keep-alive 事务模式比 http-server-close 事务模式更高效,无论后端是静态、缓存还是动态应用服务器。
  3. aggressive:一种激进的策略,该策略的 haproxy 会重用空闲 TCP 连接来转发大多数客户端的第一次请求。之所以是大多数而不是所有,是因为 haproxy 会挑选那些已经被重用过至少一次的连接(即从建立开始转发过至少两次,不管源是否是同一客户端)进行重用,因为 haproxy 认为只有这样的连接才具有重用能力。
  4. always:它将总是为第一个请求重用空闲连接。当后端是缓存服务器时,这种策略比 safe 策略的性能要高许多,因为这样的请求行为都是一样的,且可以共享同一连接来获取资源。不过不建议使用这种策略,因为大多数情况下,它和 aggressive 的性能是一样的,但是却带来了很多风险。

因此,为了性能的提升,将它设置为 safe 或 aggressive 吧,同时再将 http 事务模型设置为 http-keep-alive,以免后端连接在响应后立即被关闭。

9.4、配置 haproxy

9.4.1、配置 haproxy 前

尽管 haproxy 大多数配置选项都可以采用默认配置,但有些选项,特别是关于实际需求、连接数和超时时间相关的选项,必须独立配置。

大致总结了下以下几点需要考虑的问题:

  1. haproxy 支持 5 种 http 事务模型。一般只会选择其中两种:

    • 当后端为静态 web 或静态缓存服务器时,由于响应速度快,频繁建立 tcp 连接的代价比较大;建议使用 http-keep-alive 模型
    • 当后端为动态应用程序服务器或者静态但传输的资源对象体积较大时,由于响应速度相对较慢,占用空闲连接的资源比建立 tcp 连接的代价更大,建议使用 http-server-close 模型
  2. haproxy 反向代理的调度算法优先级是低于 cookie 的,因此当一个连接已经保持了会话,调度算法对该连接就无效。只有新的连接请求或者长连接已经失效时,才会使用调度算法进行调度。在调度算法的选择上,如果不考虑服务器性能差距的话:

    • 如果后端会话时间比较长(mysql),建议使用 leastconn,因为调度过程中,后端释放连接时动荡不大,比较稳定。
    • 如果后端是静态 web,建议使用 roundrobin 算法。
    • 如果后端需要保持会话信息,但又不使用 cookie 时,可以使用源地址 hash 算法 source,保证将同一客户端引导到同一后端服务器上。如果使用 cookie,则可以使用 roundrobinleastconn 算法。源地址 hash 算法,一般只在没有办法的时候但又要调度到同一后端服务器时,才作为最后手段。
    • 如果配置了 session 共享,则对于 haproxy 来说,动态资源的请求是 "无状态" 的,可以使用 roundrobin 算法或 leastconn
    • 如果后端是缓存服务器,为了保证命中率,建议使用 uri 算法,同时将 hash-type 设置为 consistent 方法(一致性 hash),保证后端缓存服务器 down 掉后对客户端的影响足够小。
  3. haproxy 是单进程、事件驱动模型的软件,单进程下工作效率已经非常好,不建议开启的多进程/多实例。

  4. maxconn 指令控制最大并发连接数,可以在多处设置,设置位置不同,代表意义不同:

    • 设置在 global 段或 frontend/listen/defaults 段的 maxconn 代表的是和客户端(即 frontend)的最大连接并发数;其中 global 段的值是硬限制,frontend/listen/defaults 段的 maxconn 值不能超过 global 段的值。

    • 设置在 server 指令中时,代表的是 haproxy 和某台后端服务器维持的最大并发连接数。

    • 前端的最大并发数(即 global 段的 maxconn)可以根据内存来估算,haproxy 为每个连接维持两个缓存区,每个大致 16K 左右,加上一些额外数据,共约 33-34K 左右,因此理论上 1G 的空闲内存能维持 2W-2.5W 个纯 HTTP 的并发连接(只是理论上),如果代理的是 https,则允许的最大并发数量要小的多。前端 maxconn 默认值为 2000,非常有必要将其增加几倍

      一般代理纯 http 服务时,如果后端能处理及时,这里设置 20000 以上都不会有什么问题。以上只是大致估算代理能力,实际设置时必须根据后端处理能力以及 haproxy 自身能力设置前端 maxconn,否则将前端接进来后端也无法立即处理。

    • 后端所有服务器的 maxconn 值之和应接近前端的 maxconn 值,计算两者差距时,还需要考虑后端的等待队列长度 maxqueue。其中和静态 web 服务器的 maxconn 可以设置大一些。

  5. 参见本章 9.3.7 设置连接重用功能。

  6. 对于 haproxy 是否开启 cookie 以及 stick table 相关功能的设置必须严加考虑,它直接影响调度算法的选择和负载均衡的性能。不过如果后端应用程序服务器共享了 session,haproxy 可以不用设置会话粘性相关的选项。

  7. haproxy 的默认配置文件中关于超时时间的设置应该修改,不少项设置都很不合理。

  8. 建议开启 haproxy 的 X-Forwarded-For 选项,使得后端服务器能够记录客户端的真实源 IP 地址。

  9. 建议开启 haproxy 的状态页面,并设置访问权限。

为了实现 Haproxy 完善的功能,上面几个问题是远远不够的,但可以在边使用 haproxy 过程中边增加功能使其不断完美。

9.4.2、配置 haproxy 反向代理

实验环境如下图:

haproxy proxy

haproxy 编译安装参见 9.1 小节,由于默认配置文件中和超时时间相关的设置比较不合理,所以建议修改这些时间。另外还有些建议开启或关闭的项也尽量开启或关闭。

默认配置如下:

global
    log         127.0.0.1 local2            # 需要设置/etc/rsyslog.conf加上local2设备的日志记录级别和日志路径
    chroot      /var/lib/haproxy
    pidfile     /var/run/haproxy.pid
    maxconn     4000                        # 这是前段对外的最大连接数。代理http时,1G空闲内存承载20000以上没大问题
    user        haproxy
    group       haproxy
    daemon
    stats socket /var/lib/haproxy/stats     # 开启动态查看、管理haproxy的状态文件
                                            # 另外建议设置spread-checks全局项,且百分比建议为2-5之间
defaults
    mode                    http            # 7层http代理,另有4层tcp代理
    log                     global
    option                  httplog         # 在日志中记录http请求、session信息等
    option                  dontlognull     # 不要在日志中记录空连接
    option http-server-close                # 后端为动态应用程序建议使用http-server-close,后端为静态建议使用http-keep-alive
    option forwardfor       except 127.0.0.0/8  # haproxy将在发往后端的请求中加上"X-Forwarded-For"首部字段
    option                  redispatch          # 当某后端down掉使得haproxy无法转发携带cookie的请求到该后端时,将其转发到别的后端上
    timeout http-request    10s     # 此为等待客户端发送完整请求的最大时长,应该设置较短些防止洪水攻击,如设置为2-3秒,
                                    # haproxy总是要求一次请求或响应全部发送完成后才会处理、转发。

    timeout queue           1m      # 请求在队列中的最大时长,1分钟太长了。设置为10秒都有点长,10秒请求不到资源客户端会失去耐心
    timeout connect         10s     # haproxy和服务端建立连接的最大时长,设置为1秒就足够了。局域网内建立连接一般都是瞬间的
    timeout client          1m      # 和客户端保持空闲连接的超时时长,在高并发下可稍微短一点,可设置为10秒以尽快释放连接
    timeout server          1m      # 和服务端保持空闲连接的超时时长,局域网内建立连接很快,所以尽量设置短一些,特别是并发时,如设置为1-3秒
    timeout http-keep-alive 10s     # 和客户端保持长连接的最大时长。优先级高于timeout http-request高于timeout client
    timeout check           10s     # 和后端服务器成功建立连接后到最终完成检查的时长(不包括建立连接的时间,只是读取到检查结果的时长),
                                    # 可设置短一点,如1-2秒

    maxconn                 3000    # 默认和前段的最大连接数,但不能超过global中的maxconn硬限制数

所以修改后建议配置如下:

global
    log         127.0.0.1 local2
    chroot      /var/lib/haproxy
    pidfile     /var/run/haproxy.pid
    maxconn     20000
    user        haproxy
    group       haproxy
    daemon
    stats socket /var/lib/haproxy/stats
    spread-checks 2
defaults
    mode                    http
    log                     global
    option                  httplog
    option                  dontlognull
    option http-server-close
    option forwardfor       except 127.0.0.0/8
    option                  redispatch
    timeout http-request    2s
    timeout queue           3s
    timeout connect         1s
    timeout client          10s
    timeout server          2s
    timeout http-keep-alive 10s
    timeout check           2s
    maxconn                 18000 

frontend http-in
    bind             *:80
    mode             http
    log              global
    capture request  header Host len 20
    capture request  header Referer len 60
    acl url_static   path_beg  -i /static /images /stylesheets
    acl url_static   path_end  -i .jpg .jpeg .gif .png .ico .bmp .css .js
    acl url_static   path_end  -i .html .htm .shtml .shtm .pdf .mp3 .mp4 .rm .rmvb .txt
    acl url_static   path_end  -i .zip .rar .gz .tgz .bz2 .tgz

    use_backend      StaticGroup   if url_static
    default_backend  DynamicGroup

backend StaticGroup
    balance            roundrobin
    option             http-keep-alive
    http-reuse         safe
    option httpchk     GET /index.html
    http-check expect  status 200
    server staticsrv1  192.168.122.14:80 check rise 1 maxconn 5000
    server staticsrv2  192.168.122.15:80 check rise 1 maxconn 5000

backend DynamicGroup
    cookie appsrv insert nocache
    balance roundrobin
    option http-server-close
    option httpchk     GET /index.php
    http-check expect  status 200
    server appsrv1 192.168.122.12:80  check rise 1 maxconn 3000 cookie appsrv1
    server appsrv2 192.168.122.13:80  check rise 1 maxconn 3000 cookie appsrv2

listen report_stats
        bind *:8081
        stats enable
        stats hide-version
        stats uri    /hastats
        stats realm  "pls enter your name"
        stats auth   admin:admin
        stats admin  if TRUE

上面的配置中:

  1. 静态请求将分配给 StaticGroup 并进行 roundrobin 调度,同时通过获取 index.html 来做健康状况检查,此外还设置了 haproxy 和后端连接重用的功能。
  2. 动态请求将分配给 DynamicGroup 并进行 roundrobin 调度,但是向响应报文中插入了一个 cookie,保证被调度过的服务端和客户端能保持会话。此外还设置了通过获取 index.php 来做健康状况检查。

如何配置 nginx + php 请参见 nginx 配置 LNMP

为了以示区分,在 DynamicGroup 和 StaticGroup 主机中的响应文件里面加入 IP 地址:

  • 比如 index.html 文件如下:
# 192.168.122.14 主机 index.html
<h1>This is static page on 192.168.122.14 !!!</h1>

# 192.168.122.15 主机 index.html
<h1>This is static page on 192.168.122.15 !!!</h1>
  • 比如 index.php 文件如下:
# 192.168.122.12 主机 index.php
<h1>response from webapp 192.168.122.12</h1>
<?php
    session_start();
    echo "Server IP: "."<font color=red>".$_SERVER['SERVER_ADDR']."</font>"."<br>";
    echo "Server Name: "."<font color=red>".$_SERVER['SERVER_NAME']."</font>"."<br>";
    echo "SESSIONNAME: "."<font color=red>".session_name()."</font>"."<br>";
    echo "SESSIONID: "."<font color=red>".session_id()."</font>"."<br>";
?>

# 192.168.122.13 主机 index.php
<h1>response from webapp 192.168.122.13</h1>
<?php
    session_start();
    echo "Server IP: "."<font color=red>".$_SERVER['SERVER_ADDR']."</font>"."<br>";
    echo "Server Name: "."<font color=red>".$_SERVER['SERVER_NAME']."</font>"."<br>";
    echo "SESSIONNAME: "."<font color=red>".session_name()."</font>"."<br>";
    echo "SESSIONID: "."<font color=red>".session_id()."</font>"."<br>";
?>

测试:

haproxy test

标签云