HAProxy 简介

2014-11-28 linux

HAProxy 是一个免费的负载均衡软件,可以运行于大部分主流的 Linux 操作系统上,提供了 L4 和 L7 两种负载均衡能力,可媲美商用负载均衡器的性能和稳定性,使其不仅仅是免费负载均衡软件的首选,更几乎成为了唯一选择。

这里简单介绍下 HAProxy 的使用方式。

简介

相比 Nginx 来说,HAProxy 支持自定义 URL 健康监测;会话保持除了可以使用 IP_HASH 外,还可以使用 URL_HASH 算法;支持多种负载均衡等等,如下是 HAProxy 提供的主要功能:

  • 负载均衡。提供 L4 和 L7 两种模式,支持 RoundRobinStatic RoundRobinLeastConnectionSource IP HashURI HashURL_PARAM HashHTTP_HEADER Hash 等丰富的负载均衡算法;
  • 健康检查。支持 TCPHTTPSSLMySQLRedis 等多种健康检查模式;
  • 会话保持。对于未实现会话共享的应用集群,可通过 Insert CookieRewrite CookiePrefix Cookie 以及上述的多种 Hash 方式实现会话保持;
  • SSL 支持。可以解析 HTTPS 协议,并能够将请求解密为 HTTP 后向后端传输;
  • HTTP。可以对请求进行重写与重定向,支持多种 HTTP 模式;
  • 监控与统计。提供了基于 Web 的统计页面,展现健康状态和流量数据。

在配置文件中可以通过 balance 参数指定算法,如下仅介绍一些常用的算法:

roundrobin:
  轮询,适用于短链接,会根据权重比选择服务器,支持4096个后端服务器;注意,这里的权重信息可以动态修改。
static-rr:
  与上类似,只是这里的服务器权重不能动态修改,而且对于服务器的数量没有限制,消耗CPU相对也较少。
leastconn:
  选择链接数最少的服务器建立链接,通常在使用长连接时,例如MySQL、LDAP等,对于短链接HTTP不建议使用;
  这里的服务器权重可以动态调整。
first:
  忽略权重信息,选择第一个可用服务器,需要设置服务器的最大连接数,适用于长连接;常用于动态扩容的场景,
  一般有工具用于动态监测,提供动态伸缩服务。
source
  源地址算法,通过客户端的原IP地址进行hash,只要没有服务器宕机,那么一个客户端的请求都会由同一台服务器处理。
uri
  算法,需要使用HTTP服务,通过 HTTP URI 地址来选择对应服务器。
hdr
  通过 HTTP eder 内容来选择对应服务器。

在源码中,其实现在 lb_XXX.c 文件中,关于 HAProxy 的健康检查可以参考 Health checking

配置文件

详细可以查看官方的文档 doc/configuration.txt ,这里简单介绍常见的概念。

如下是常见的运维操作。

----- 测试配置文件是否有语法错误
# haproxy -c -f /etc/haproxy/haproxy.cfg
----- 没有问题则尝试启动
# haproxy -f /etc/haproxy/haproxy.cfg
----- 重新加载
# haproxy -f /etc/haproxy/haproxy.cfg -sf `cat /var/run/haproxy.pid`

在通过 systemctl status haproxy 启动时,也就是使用 systemd 时,实际会有一个 wrap 程序启动,所以会看到有多个进程启动。

HAProxy 配置文件由全局配置+代理配置两部分组成,又分为五段:global、defaults、frontend、backend、listen。

global:
  全局配置内容,用于定义全局参数,属于进程级的配置,通常和操作系统配置有关。
default:
  作为frontend、backend、listen的默认配置参数。
frontend:
  接收请求的前端虚拟节点,在1.3版本引入,用于简化haproxy配置文件复杂度,可以通过ACL规则指定要使用的后端backend。
backend:
  后端服务器配置。
listen:
  frontend+backend结合体,1.3版本之前使用,主要为了保持兼容性。

日志配置

global 中设置日志的级别,然后通过 syslog 进行保存。

$ cat /etc/haproxy/haproxy.conf
global
  log 127.0.0.1 local2 info

使用 rsyslog 保存。

----- 确认添加了include子目录,因为如下的HAProxy配置会放置到子目录下
cat /etc/rsyslog.conf | grep "IncludeConfig"
----- 修改rsyslog的主配置文件,开启远程日志,查看是否开启UDP端口
cat /etc/sysconfig/rsyslog | grep "SYSLOGD_OPTIONS"
SYSLOGD_OPTIONS="-c 2 -r -m 0"
#-c 2 使用兼容模式,默认是 -c 5
#-r 开启远程日志
#-m 0 标记时间戳。单位是分钟,为0时,表示禁用该功能

----- 添加HAProxy配置文件,需要注意最后一行,否则会同时写入haproxy.log和message文件
cat /etc/rsyslog.d/haproxy.conf
$ModLoad imudp
$UDPServerRun 514
local2.*     /var/log/haproxy.log
&~

配置文件

#---------------------------------------------------------------------
# Global settings
#---------------------------------------------------------------------
global
    # to have these messages end up in /var/log/haproxy.log you will
    # need to:
    # 1) configure syslog to accept network log events.  This is done
    #    by adding the '-r' option to the SYSLOGD_OPTIONS in
    #    /etc/sysconfig/[r]syslog
    # 2) configure local2 events to go to the /var/log/haproxy.log
    #   file. A line like the following can be added to
    #   /etc/sysconfig/syslog
    #    local2.*                       /var/log/haproxy.log
    log         127.0.0.1 local2 debug


    chroot      /var/lib/haproxy
    pidfile     /var/run/haproxy.pid
    maxconn     4000
    user        haproxy
    group       haproxy
    daemon

    # turn on stats unix socket
    stats socket /var/run/haproxy.sock level admin process 1
    stats timeout 5m                   # 设置等待输入超时时间为5min

#---------------------------------------------------------------------
# common defaults that all the 'listen' and 'backend' sections will
# use if not designated in their block
#---------------------------------------------------------------------
defaults
    mode                    http
    log                     global
    option                  tcplog                 # 默认只打印很少的信息,指定打印详细信息
    option                  httplog
    option                  dontlognull            # 默认会将保活等信息记录到日志,可通过该选项关闭
    option http-server-close
    option                  redispatch             # 使用cookies后会发送到指定服务器,如果服务器不可用则重新调度
    retries                 3                      # 3次连接失败则认为服务不可用
    timeout http-request    10s                    # 默认http请求超时时间
    timeout queue           1m                     # 默认队列超时时间
    timeout connect         10s                    # 默认连接超时时间
    timeout client          1m                     # 默认客户端超时时间
    timeout server          1m                     # 默认服务器超时时间
    timeout http-keep-alive 10s                    # 默认持久连接超时时间
    timeout check           10s                    # 默认检查时间间隔
    maxconn                 3000                   # 最大连接数

#---------------------------------------------------------------------
# main frontend which proxys to the backends
#---------------------------------------------------------------------
frontend  main *:5000
    # 新建acl策略path_beg以/static /images等开头的访问路径,-i忽略大小写 
    acl url_static       path_beg       -i /static /images /javascript /stylesheets
    # 新建acl策略path_end以.jpg .gif等结尾的访问路径,-i忽略大小写 
    acl url_static       path_end       -i .jpg .gif .png .css .js

    # 如果匹配url_static这个acl策略,则使用static这个后端 
    use_backend static          if url_static
    # 没有任何配置的情况下,使用默认的后端app 
    default_backend             app

#---------------------------------------------------------------------
# static backend for serving up images, stylesheets and such
#---------------------------------------------------------------------
backend static
    balance     roundrobin
    server      static 127.0.0.1:4331 check

#---------------------------------------------------------------------
# round robin balancing between the various backends
#---------------------------------------------------------------------
backend app
    balance     roundrobin
    server  app1 127.0.0.1:5001 check
    server  app2 127.0.0.1:5002 check
    server  app3 127.0.0.1:5003 check
    server  app4 127.0.0.1:5004 check

#---------------------------------------------------------------------
# 1.3之前使用,为了保持向后兼容,建议使用frontend+backend代替
# 关于server的详细配置选项可以参考Server and default-server options
#  * maxconn NUM 每个后端服务器的最大链接数,超过该值则保存在队列中
#  * maxqueue NUM 超过该队列后会重新选择后端服务器
#---------------------------------------------------------------------
listen http-proxy 192.168.1.201:80
    mode http
    option httpchk GET /test
    balance roundrobin
    timeout server 30s
    timeout client 30s
    server server-01 192.168.1.101:80 check inter 2s
    server server-02 192.168.1.102:80 check inter 2s
    server server-03 192.168.1.103:80 check inter 2s

源码解析

HAProxy is a single-threaded, event-driven, non-blocking daemon.

启动非常简单,只需要一个可执行文件+配置文件即可,启动之后基本处理如下的三个操作:

1. 处理链接;
   1.1 从frontend指定的监听端口创建链接;
   1.2 应用frontend指定的规则,包括了阻塞、修改头部信息、统计等;
   1.3 将链接传递给backend指定的服务器;
   1.4 应用backend指定的规则;
   1.5 根据策略决定将链接发送给那个服务器;
   1.6 对响应数据应用backend指定的规则;
   1.7 对响应数据应用frontend指定的规则;
   1.8 发送日志;
   1.9 如果是HTTP那么会等待新请求,否则就关闭链接;
2. 周期性检查服务器的状态;
3. 与其它HAProxy节点交换数据;

然后,直接看下源码解析过程。

main()
 |-init()                                   所有的初始化操作,包括各个模块初始化、命令行解析等
 | |-cfgfiles_expand_directories()          处理配置文件,参数解析会将配置文件保存在cfg_cfgfiles
 | |-init_default_instance()                初始化默认配置
 | |-readcfgfile()                          读取配置文件,并调用sections->section_parser()对应的函数
 | | |-cfg_parse_listen()                   对于frontendbackendlisten段的参数解析验证
 | |   |-str2listener()
 | |     |-l->frontend=curproxy
 | |     |-tcpv4_add_listener()             添加到proto_tcpv4对象中的链表,真正监听会在proto_XXX.c文件中
 | |       |-listener->proto=&proto_tcpv4   会设置该变量,后续的接收链接也就对应了accept变量
 | |-check_config_validity()                配置文件格式校验
 | | |-listener->frontend=curproxy          在上面解析,实际上curporxy->accept=frontend_accept
 | | |-listener->accept=session_accept_fd
 | | |-listener->handler=process_stream
 | | |-根据不同的后端服务器选择算法选择
 | |
 | | <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<解析完配置文件>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
 | |
 | |-start_checks()                         执行后端检查任务
 | | |-start_check_task()
 | |   |-process_chk()
 | |     |-process_chk_conn()
 | |       |-connect_conn_chk()
 | |         |-event_srv_chk_r()            健康检查,在check_conn_cb变量中定义,通过conn_attach()绑定
 | |           |-tcpcheck_main()            如果是TCP检查PR_O2_TCPCHK_CHK
 | |
 | |-init_pollers()                         选择多路复用方法,也就是设置cur_poller
 |   |-calloc()                             分配资源fd_cache+fd_updt,其大小是global.maxsock
 |   |-bp->init()                           调用各个可用poll方法的初始化函数,选择第一个可用方法
 |   |-memcpy()                             复制到cur_poller全局变量中
 |
 |-start_proxies()                          开始启动,调用各协议bind接口,对TCPv4就是tcp_bind_listener()
 |-protocol_bind_all()
 |-protocol_enable_all()                    启动各个协议,例如ipv4/ipv6/unix等
 |
 | <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<主循环处理流程>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
 |
 |-run_poll_loop()
 | |-tv_update_date()
 | | ###WHILE###BEGIN
 | |-process_runnable_tasks()               调用可运行任务
 | | |-process_stream()                     一般是调用该函数,也可执行struct task.process中自定义函数
 | |   |
 | |   | <<PHASE:解析请求>>                 会根据定义的各种规则选择后端
 | |   |-process_switching_rules()          除了默认使用的后端服务器之外,会根据规则再次选择<<RULES>>
 | |   | |-stream_set_backend()             选择该后端,<<STAT:be->beconn>>
 | |   |   |-proxy_inc_be_ctr()             后端统计值的更新
 | |   |-http_process_tarpit()
 | |   |
 | |   | <<PHASE:解析响应>>
 | |   |-process_store_rules()              处理规则
 | |   |
 | |   | <<PAHSE:正式处理请求>>
 | |   |-sess_prepare_conn_req()            选择后端的服务器
 | |     |-srv_redispatch_connect()
 | |       |-assign_server_and_queue()      选择后端的服务器,并添加到队列中
 | |         |-assign_server()              根据负载均衡配置选择后端服务器
 | |         | |-get_server_sh()
 | |         |   |-chash_get_server_hash()  使用一致性hash算法
 | |         |-sess_change_server()
 | |         |-pendconn_add()               如果后端服务器已满,则添加到proxy队列中<<STAT:px->nbpend++>>
 | |
 | |-signal_process_queue()                 信号队列,如果捕获了信号则处理
 | |-wake_expired_tasks()                   超时任务
 | |-cur_poller.poll()                      不同平台的多路复用技术
 | | |-_do_poll()                           ev_epoll.c中的epoll为例
 | |   |-epoll_wait()
 | |   |-fd_may_recv()
 | |   | |-fd_update_cache()                在处理函数中只添加到cache中,真正的处理过程在后面
 | |   |-fd_may_send()
 | |     |-fd_update_cache()
 | |
 | |-fd_process_cached_events()             真正处理epoll()中触发的事件
 | | |-fdtab[fd].iocb(fd)                   调用注册的回调函数,一般是conn_fd_handler()
 | |   |-conn->data->recv(conn)             实际调用si_conn_recv_cb()函数,也就是负责接收的函数
 | |   | |-conn->xprt->rcv_pipe()           如果不启用SSL则调用raw_sock_to_pipe(),否则调用下面的buff函数
 | |   | |-conn->xprt->rcv_buf()            不启用SSL则调用raw_sock_to_buf(),否则调用ssl_sock_to_buf()
 | |   |-conn->data->recv(conn)             实际调用si_conn_send_cb()函数,也就是负责发送的函数
 | |
 | |-applet_run_active()
 | | ###WHILE###END
 |-deinit() 清理操作

参考