• 如来神掌第一式第十四招----HAPROXY详解


    ###############################################################################
    # Name : Mahavairocana                                                                                                                                           
    # Author : Mahavairocana                                                                                                                                         
    # QQ : 10353512                                                                                                                                                    
    # WeChat : shenlan-qianlan                                                                                                                                      
    # Blog : http://www.cnblogs.com/Mahavairocana/                                                                                                       
    # Description : You are welcome to reprint, or hyperlinks to indicate the                                                                        
    #                    source of the article, as well as author information.                                                                                ###############################################################################

    一、简介

    Haproxy:高可用性、负载均衡以及基于TCP(第四层)和HTTP(第七层)应用的代理软件

    HAProxy 支持两种主要的代理模式:"tcp"也即4 层(大多用于邮件服务器、内部协议通信服务器等),和7 层(HTTP)。在4 层模式下,HAproxy仅在客户端和服务器之间转发双向流量。7 层模式下,HAProxy 会分析协议,并且能通过允许、拒绝、交换、增加、修改或者删除请求(request)或者回应(response)里指定内容来控制协议,这种操作要基于特定规则。

    二、术语

    1、基于cookie 插入的简单HTTP 负载均衡

    配置 haproxy (LB1) :
    -------------------------
    
        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 接收clients的requests
     - 如果1个request不包含cookie,则把这个request前传到分配的一个有效的server
     - 作为回报, 1个拥有server名称的cookie "SERVERID"会被插入到response中
     - 当client带有cookie "SERVERID=A"再此访问时,LB1就会知道这个request必须被前传到server A. 同时删除这个cookie是的server不会看到它
     - 当server"webA"宕机时,request会被前传至另外一个有效的server,并且重新分配cookie

    2、带cookie前缀的HTTP负载均衡和高可用性

      现在你可以不用添加更多的cookie,而是使用已有的cookie。如果应用已经生成J足够跟踪session的SESSIONID cookie,我们会在看到该cookie时,在它前面加上server name前缀。由于load-balancer变得关键,因此,可以通过利用keepalived使得运行在VRRP模式下的第二台热备它。
    
          从网站上下载最新版本的keepalived,并且安装在load-balancer LB1和LB2上。
    
           在两个load-balancer(我们仍然使用原始IP)之间,我们使用一个共享IP。在任何时刻只有1个load-balancers处于活跃状态。为了允许proxy在Linux2.4下绑定一个共享IP,需要在/proc中启用如下配置:
    
    # echo 1 >/proc/sys/net/ipv4/ip_nonlocal_bind
    
        shared IP=192.168.1.1
      192.168.1.3  192.168.1.4    192.168.1.11-192.168.1.14   192.168.1.2
     -------+------------+-----------+-----+-----+-----+--------+----
            |            |           |     |     |     |       _|_db
         +--+--+      +--+--+      +-+-+ +-+-+ +-+-+ +-+-+    (___)
         | LB1 |      | LB2 |      | A | | B | | C | | D |    (___)
         +-----+      +-----+      +---+ +---+ +---+ +---+    (___)
         haproxy      haproxy        4 cheap web servers
         keepalived   keepalived
    
    proxies上的配置(LB1 and LB2):
    
     listen webfarm 192.168.1.1:80
    
           mode http
    
           balance roundrobin
    
           cookie JSESSIONID prefix
    
           option httpclose
    
           option forwardfor
    
           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
    
    提示:proxy会修改client和server发出的每个cookie,因此proxy能够访问每个session中的所有request的所有cookie是非常重要的。这意味着不是keepalive(HTTP/1.1),需要使用'httpclose'选项。只有你能确认clients不会使用keepalive,才能删除这个选项。
    
    LB1和LB2上keepalived的配置:
    
        vrrp_script chk_haproxy {           # Requires keepalived-1.1.13
    
            script "killall -0 haproxy"     # cheaper than pidof
    
            interval 2                      # check every 2 seconds
    
        weight 2                        # add 2 points of prio if OK
    
        }
    
    
        vrrp_instance VI_1 {
    
            interface eth0
    
            state MASTER
    
            virtual_router_id 51
    
            priority 101                    # 101 on master, 100 on backup
    
            virtual_ipaddress {
    
                192.168.1.1
    
            }
    
            track_script {
    
                chk_haproxy
    
            }
    
        }
    
    描述:
    
    —LB1是VRRP的主(keepalived),LB2是备。他们都监控haproxy进程,并且如果haproxy failed则降低他们的权重,迁移至另外的节点;
    
    —LB1将在IP:192.168.1.1上接收client request
    
    —两个LB用他们的内部IP发送健康检测
    
    —如果request不包含cookie,request会被前传至1个有效的server
    
    —在回复时,如果看到JESSIONID cookie,proxy会在cookie加上以('~')为分隔符的server name前缀;
    
    —如果client再次访问时带着"JSESSIONID=A~xxx" cookie,LB1会知道这个request必须前传至 server A。在把cookie发送给server之前,cookie中的server name会先被剔除出来。
    
    —如果server "webA"宕机,requests会被前传到另外的有效服务器,并且会重新分配cookie;
    
    数据流:
    
    (client)                           (haproxy)                         (server A)
    
      >-- GET /URI1 HTTP/1.0 ------------> |
    
                   ( no cookie, haproxy forwards in load-balancing mode. )
    
                                           | >-- GET /URI1 HTTP/1.0 ---------->
    
                                           |     X-Forwarded-For: 10.1.2.3
    
                                           | <-- HTTP/1.0 200 OK -------------<
    
                            ( no cookie, nothing changed )
    
      <-- HTTP/1.0 200 OK ---------------< |
    
      >-- GET /URI2 HTTP/1.0 ------------> |
    
        ( no cookie, haproxy forwards in lb mode, possibly to another server. )
    
                                           | >-- GET /URI2 HTTP/1.0 ---------->
    
                                           |     X-Forwarded-For: 10.1.2.3
    
                                           | <-- HTTP/1.0 200 OK -------------<
    
                                           |     Set-Cookie: JSESSIONID=123
    
        ( the cookie is identified, it will be prefixed with the server name )
    
      <-- HTTP/1.0 200 OK ---------------< |
    
          Set-Cookie: JSESSIONID=A~123     |
    
      >-- GET /URI3 HTTP/1.0 ------------> |
    
          Cookie: JSESSIONID=A~123         |
    
           ( the proxy sees the cookie, removes the server name and forwards
    
              to server A which sees the same cookie as it previously sent )
    
                                           | >-- GET /URI3 HTTP/1.0 ---------->
    
                                           |     Cookie: JSESSIONID=123
    
                                           |     X-Forwarded-For: 10.1.2.3
    
                                           | <-- HTTP/1.0 200 OK -------------<
    
                            ( no cookie, nothing changed )
    
      <-- HTTP/1.0 200 OK ---------------< |
    
                                        ( ... )
    
    提示:
    
    有时,在一个集群中有一些性能强劲的server,也有一些性能差一些的server。在这种情况下,很有必要告诉haproxy这些server在性能上的差异。假设WebA和WebB是两台老的P3-1.2 GHz,WebC和WebD是最新的Opteron-2.6 GHz。如果你的application关注CPU,则你可以假设这两台服务器之间的性能比为2.6/1.2。你可以通过使用1-256之间数值标记的"weight"关键字,告诉haproxy这些性能差异。它可以用这些比例来平滑load:
    
     server webA 192.168.1.11:80 cookie A weight 12 check
    
    server webB 192.168.1.12:80 cookie B weight 12 check
    
    server webC 192.168.1.13:80 cookie C weight 26 check
    
    server webD 192.168.1.14:80 cookie D weight 26 check
    
     2.1 包含外部L4 load-balancers的变化
    
    作为基于VRRP主备方案的替代方案,他们也可以使用L4 load-balancer(如:Alteon)进行负载,这些L4 load-balancer也可以检查这些运行在proxy上的服务:
    
                  | VIP=192.168.1.1
    
             +----+----+
    
             | Alteon  |
    
             +----+----+
    
                  |
    
     192.168.1.3  |  192.168.1.4  192.168.1.11-192.168.1.14   192.168.1.2
    
     -------+-----+------+-----------+-----+-----+-----+--------+----
    
            |            |           |     |     |     |       _|_db
    
         +--+--+      +--+--+      +-+-+ +-+-+ +-+-+ +-+-+    (___)
    
         | LB1 |      | LB2 |      | A | | B | | C | | D |    (___)
    
         +-----+      +-----+      +---+ +---+ +---+ +---+    (___)
    
         haproxy      haproxy        4 cheap web servers
    
    proxy(LB1和LB2)上的配置:
    
        listen webfarm 0.0.0.0:80
    
           mode http
    
           balance roundrobin
    
           cookie JSESSIONID prefix
    
           option httpclose
    
           option forwardfor
    
           option httplog
    
           option dontlognull
    
           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
    
    "dontlognull"选项用来防止记录Alteon发出的健康检测。如果一个session交互没有数据,这个session就不会被记录。
    
    Alteon上的配置:
    
        /c/slb/real  11
    
               ena
    
               name "LB1"
    
               rip 192.168.1.3
    
        /c/slb/real  12
    
               ena
    
               name "LB2"
    
               rip 192.168.1.4
    
        /c/slb/group 10
    
               name "LB1-2"
    
               metric roundrobin
    
               health tcp
    
               add 11
    
               add 12
    
        /c/slb/virt 10
    
               ena
    
               vip 192.168.1.1
    
        /c/slb/virt 10/service http
    
               group 10
    
    提示:Alteon上的健康检测被设置为'tcp',用来防止proxy把连接前传。Alteon也可以设置为'http',但是这样proxy必须把配置Alteon的地址为"monitor-net",那样Alteon可以真实的以http会话的方式检测proxy而不会把连接前传到后端的servers。检查下一节可以看到怎样使用monitor-net的例子。
    2.2 通用的TCP中继和外部L4 load-balancers
    
    有时,能够中继通用的TCP协议(如:SMTP, TSE, VNC等)是非常有用的,如访问内部稀有网络。当你在使用外部 load-balancers需要发送周期行的健康检测至proxy时问题就会出现,这是因为健康检测会被前传至后端servers。对这个问题的1个解决方案是通过使用"monitor-net"用一个专用的网络来监控系统,并且必须不会前传连接和记录。
    
    提示:这个特性只有在1.1.32或者1.2.6版本以上才支持。
    
    
                    |  VIP=172.16.1.1   |
    
               +----+----+         +----+----+
    
               | Alteon1 |         | Alteon2 |
    
               +----+----+         +----+----+
    
     192.168.1.252  |  GW=192.168.1.254 |  192.168.1.253
    
                    |                   |
    
              ------+---+------------+--+-----------------> TSE farm : 192.168.1.10
    
           192.168.1.1  |            | 192.168.1.2
    
                     +--+--+      +--+--+
    
                     | LB1 |      | LB2 |
    
                     +-----+      +-----+
    
                     haproxy      haproxy
    
    在LB1和LB2上的配置:
    
        listen tse-proxy
    
           bind :3389,:1494,:5900  # TSE, ICA and VNC at once.
    
           mode tcp
    
           balance roundrobin
    
           server tse-farm 192.168.1.10
    
           monitor-net 192.168.1.252/31
    
    "monitor-net"选项指示haproxy对来自192.168.1.252和192.168.1.253的请求不记录、不前传并且马上关闭连接。Alteon load-balancers将可以检测到proxy是活跃的而不会扰动正常服务。
    
    Alteon上的配置:
    
    ----------------------
    
    
        /c/l3/if 1
    
               ena
    
               addr 192.168.1.252
    
               mask 255.255.255.0
    
        /c/slb/real  11
    
               ena
    
               name "LB1"
    
               rip 192.168.1.1
    
        /c/slb/real  12
    
               ena
    
               name "LB2"
    
               rip 192.168.1.2
    
        /c/slb/group 10
    
               name "LB1-2"
    
               metric roundrobin
    
               health tcp
    
               add 11
    
               add 12
    
        /c/slb/virt 10
    
               ena
    
               vip 172.16.1.1
    
        /c/slb/virt 10/service 1494
    
               group 10
    
        /c/slb/virt 10/service 3389
    
               group 10
    
        /c/slb/virt 10/service 5900
    
               group 10
    
    SSL的特殊处理:
    
    有时,即使在TCP模式下,你需要给远程系统发送健康检测以便能够在server发生故障时能够切换到备份服务器。当然,你可以简单地启用TCP健康检查,但是在proxy和远程server之间的firewalls会自己确认TCP连接,从而显示为1个始终活跃的server。由于这是在使用SSL的长途通信中普遍遇到的问题,一个SSL健康检查已实现来要解决这个问题。
    
    它发出的SSL messages 消息到远程server,server回复Hello messages。可以很容易的进行配置:
    
        listen tcp-syslog-proxy
    
           bind :1514      # listen to TCP syslog traffic on this port (SSL)
    
           mode tcp
    
           balance roundrobin
    
           option ssl-hello-chk
    
           server syslog-prod-site 192.168.1.10 check
    
           server syslog-back-site 192.168.2.10 check backup

    3、带cookie插入的简单HTTP/HTTPS负载均衡

    这是跟示例1相同的情况下,但是Web server使用的是https。
                    +-------+
                    |clients|  clients
                    +---+---+
                        |
                       -+-----+--------+----
                              |       _|_db
                           +--+--+   (___)
                           | SSL |   (___)
                           | web |   (___)
                           +-----+
                       192.168.1.1   192.168.1.2
    由于haproxy不能处理SSL,SSL这一部分将不得不从server中提取出来(腾出更多的资源),而安装在load-balancer上。在一个box上安装haproxy和apache+mod_ssl,把该box的负载分散至新的box上。Apache运行在SSL的reverse-proxy-cache模式下。如果应用程序设计的合理,甚至可能会降低其负载。不过,由于现在是一个与客户端和缓存的haproxy,一些安全必须采取措施,以确保插入的cookies不被缓存。
    
      192.168.1.1    192.168.1.11-192.168.1.14   192.168.1.2
     -------+-----------+-----+-----+-----+--------+----
            |           |     |     |     |       _|_db
         +--+--+      +-+-+ +-+-+ +-+-+ +-+-+    (___)
         | LB1 |      | A | | B | | C | | D |    (___)
         +-----+      +---+ +---+ +---+ +---+    (___)
         apache         4 cheap web servers
         mod_ssl
         haproxy 
    
    LB1上的配置:
    
    -------------------------
    
    listen 127.0.0.1:8000
    
           mode http
    
           balance roundrobin
    
           cookie SERVERID insert indirect nocache
    
           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上的apache在443端口上接收clients的请求
    
    - apache前传请求至绑定在127.0.0.1:8000上的haproxy
    
    - 如果request不带cookie,则被前传至一台有效的server
    
    - 在回复时,haproxy会在回复中插入一个包含server名称(如:A)的"SERVERID" cookie,和一个"Cache-control: private" header。那样apache就不会cache带这样cookie的page;
    
    - 如果client再次访问时带了"SERVERID=A" cookie,则LB1会知道必须把该request前传至server A。haproxy会删除该cookie,而server不会看到它;
    
    - 如果server "webA"宕机,request会被前传至另外一个有效的server,而cookie会被重置;
    
    
    提示:
    
    -------------------------
    
    - 如果cookie工作在"prefix"模式下,haproxy就不需要配置"nocache"选项。因为,这个application cookie会被修改,并且application flags会被保留;
    
    - 如果haproxy的前段使用了apache1.3,则它会一直禁用后端HTTP keep-alive,因此可以不必在haproxy上配置 "httpclose"- 如果application需要知道client's IP,则在apache上配置X-Forwarded-For header,而不要在haproxy上配置;
    
    
    数据流:
    
    -------------------------
    
    (apache)                           (haproxy)                         (server A)
    
      >-- GET /URI1 HTTP/1.0 ------------> |
    
                   ( no cookie, haproxy forwards in load-balancing mode. )
    
                                           | >-- GET /URI1 HTTP/1.0 ---------->
    
                                           | <-- HTTP/1.0 200 OK -------------<
    
                   ( the proxy now adds the server cookie in return )
    
      <-- HTTP/1.0 200 OK ---------------< |
    
          Set-Cookie: SERVERID=A           |
    
          Cache-Control: private           |
    
      >-- GET /URI2 HTTP/1.0 ------------> |
    
          Cookie: SERVERID=A               |
    
          ( the proxy sees the cookie. it forwards to server A and deletes it )
    
                                           | >-- GET /URI2 HTTP/1.0 ---------->
    
                                           | <-- HTTP/1.0 200 OK -------------<
    
       ( the proxy does not add the cookie in return because the client knows it )
    
      <-- HTTP/1.0 200 OK ---------------< |
    
      >-- GET /URI3 HTTP/1.0 ------------> |
    
          Cookie: SERVERID=A               |
    
                                        ( ... )
    
    3.1 使用Stunnel的替代方案
    
    如果只需要SSL而不需要cache,则stunnel是一个比Apache+mod_ssl更廉价的解决方案。stunnel默认不出HTTP并且不能增加X-Forwarded-For header,但是在haproxy的官方网站上有针对最新stunnel versions版本支持该特性的patch。这时,stunnel只是处理HTTPS而不处理HTTP。这也意味着haproxy会接收所有的HTTP访问。因此,haproxy需要在HTTP访问中增加X-Forwarded-For header,而不用对HTTPS访问做处理。因为,stunnel已经做过处理了。我们可以使用"except"关键字告诉haproxy,来自本地的连接已经有有效的header了。
    
    
      192.168.1.1    192.168.1.11-192.168.1.14   192.168.1.2
    
     -------+-----------+-----+-----+-----+--------+----
    
            |           |     |     |     |       _|_db
    
         +--+--+      +-+-+ +-+-+ +-+-+ +-+-+    (___)
    
         | LB1 |      | A | | B | | C | | D |    (___)
    
         +-----+      +---+ +---+ +---+ +---+    (___)
    
         stunnel        4 cheap web servers
    
         haproxy 
    
    stunnel(LB1)上的配置:
    
        cert=/etc/stunnel/stunnel.pem
    
        setuid=stunnel
    
        setgid=proxy
    
        socket=l:TCP_NODELAY=1
    
        socket=r:TCP_NODELAY=1
    
        [https]
    
        accept=192.168.1.1:443
    
        connect=192.168.1.1:80
    
        xforwardedfor=yes
    
    haproxy(LB1)上的配置:
    
        listen 192.168.1.1:80
    
           mode http
    
           balance roundrobin
    
           option forwardfor except 192.168.1.1
    
           cookie SERVERID insert indirect nocache
    
           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上的stunnel会在443上接收client的requests;
    
    - stunnel前传request至绑定在80的haproxy;
    
    - haproxy会在80端口接收HTTP client request,在同一个端口(80)上解析来自stunnel的SSL requests;
    
    - stunnel增加X-Forwarded-For header(SSL 请求)
    
    - haproxy在除了来自本地的地址(stunnel)的每个request中增加X-Forwarded-For header;

    4、内存管理机制

     haproxy的内存管理采用了pool机制,即通过pool提高内存的重用,减少内存频繁的申请和释放。
    Pool结构及逻辑
    
        Pool数据结构
    
    struct list {
    
    struct list *n; /* next */
    
    struct list *p; /* prev */
    
    };
    
    
    struct pool_head {
    
    void **free_list;
    
    struct list list; /* list of all known pools */
    
    unsigned int used; /* how many chunks are currently in use */
    
    unsigned int allocated; /* how many chunks have been allocated */
    
    unsigned int limit; /* hard limit on the number of chunks */
    
    unsigned int minavail; /* how many chunks are expected to be used */
    
    unsigned int size; /* chunk size */
    
    unsigned int flags; /* MEM_F_* */
    
    unsigned int users; /* number of pools sharing this zone */
    
    char name[12]; /* name of the pool */
    
    };
    
    其中未说明的分量含义解释如下:
    
    free_list:保存空闲的内存链表
    
        Pool的逻辑结构
    
    
    
    static struct list pools:全局变量,所有不同尺寸pool的链表head;
    
    pool2_session:全局变量,session大小尺寸的pool;
    
    pool2_buffer:全局变量,buffer大小尺寸的pool;
    Pool的初始化
    
    函数原型:
    
    struct pool_head *create_pool(char *name, unsigned int size, unsigned int flags)
    
    函数流程:
    
    list_for_each_entry(entry, &pools, list) {
    
    if (entry->size == size && entry->flags & MEM_F_SHARED)
    
    return entry;
    
    else
    
    calloc a pool;
    
    insert pool to poos;
    
    }
    内存的申请
    
    函数原型:
    
    static inline void *pool_alloc2(struct pool_head *pool)
    
    函数流程:
    
    if (pool->free_list)
    
    get first node from free_list;
    
    else
    
    malloc a buf;
    
    内存的释放
    
    函数原型:
    
    static inline void pool_free2(struct pool_head *pool, void *ptr)
    
    函数流程:
    
    在相应的pool中,把ptr指向的buf插入至free_list头。
    Pool的销毁
    
    函数原型:
    
    void *pool_destroy2(struct pool_head *pool);
    
    函数流程:
    
    free对应pool free_list中的buf,并且从pools中删除。
    Haproxy内存管理机制的缺陷
    
    分析Haproxy内存管理机制,可以看出该机制存在如下缺陷:
    
        缺少pool收缩机制,即pool中分配的buf数目只会增长不会减少;也真是这个缺陷导致haproxy占用内存不会随着请求量的下降而下降,而只会把不用的buf存放至free_list中。

    5、健康检查机制

        option httpchk
    
    启用七层健康检测
    
        http-check disable-on-404
    
    如果backend返回404,则除了长连接之外的后续请求将不被分配至该backend
    
        http-check send-state
    
    增加一个header,同步HAProxy中看到的backend状态。该header为server可见。 X-Haproxy-Server-State: UP 2/3; name=bck/srv2; node=lb1; weight=1/2; scur=13/22; qcur=0
    
        server option
    
    check:启用健康检测
    
    inter:健康检测间隔
    
    rise:检测服务可用的连续次数
    
    fall:检测服务不可用的连续次数
    
    error-limit:往server写数据连续失败的次数上限,执行on-error的设定
    
    observe :把正常服务过程作为健康检测请求,即实时检测
    
    on-error :满足error-limit后执行的操作(fastinter、fail-check、sudden-death、mark-down) 。其中fastinter表示立即按照fastinter的检测延时进行。fail-check表示改次error作为一次检测;sudden-death表示模仿一次fatal,如果紧接着一次fail则置server为down;mark-down表示直接把server置为down状态。
    
        其它
    
    retries:连接失败重试的次数,如果重试该次数后还不能正常服务,则断开连接。
    3 检测机制
    3.1 相关数据结构
    
    struct server {
    
    ......
    
    int health; /* 0->rise-1 = bad; rise->rise+fall-1 = good */
    
    int consecutive_errors; /* current number of consecutive errors */
    
    int rise, fall; /* time in iterations */
    
    int consecutive_errors_limit; /* number of consecutive errors that triggers an event */
    
    short observe, onerror; /* observing mode: one of HANA_OBS_*; what to do on error: on of ANA_ONERR_* */
    
    int inter, fastinter, downinter; /* checks: time in milliseconds */
    
    ......
    
    }
    3.2 check流程
    
    3.3 server状态切换条件
    
        UP-->DOWN
    
        初始为s->health=s->rise;
    
        if (s->health < s->rise + s->fall – 1) then s->health = s->rise + s->fall – 1;
    
        check失败:s->health--
    
        if (s->health <= s->rise) then set_server_down(), s->health = 0;
    
        DOWN-->UP
    
    初始为s->health=0;
    
        check成功:s->health++
    
    if (s->health == s->rise) then set_server_up(), s->health = s->rise + s->fall – 1;
    3.4 observe机制
    
    observe机制是分析请求服务过程中发生错误的时候调用heath_adjust函数来实时更新check机制中的相关计数。其跟check机制的区别在于,check机制只通过定时检测。observe机制基于check机制。在不同的on-error(mode)情况下对s->health的影响如下:
    
    备注:执行on-error(mode)的前提是
    
    s->consecutive_errors < s->consecutive_errors_limit(连接失败的次数超过了上限)
    
        fastinter
    
    不修改s->health值,但是会调整check出发的时间,时间为间隔fastinter后的数字。
    
        fail-check
    
    把本次连接的失败作为1次check,s->health--
    
        sudden-death
    
    把本次连接作为1次致命的失败,s->health = s->rise + 1,如下次还失败则置为DOWN
    
        mark-down
    
    本次连接失败后,直接把后端server置为DOWN

    6、HAProxy负载均衡器算法与使用技巧

    HAProxy支持的负载均衡算法
    
    (1)、roundrobin,表示简单的轮询,负载均衡基础算法
    
    (2)、static-rr,表示根据权重
    
    (3)、leastconn,表示最少连接者先处理
    
    (4)、source,表示根据请求源IP
    
    (5)、uri,表示根据请求的URI;
    
    (6)、url_param,表示根据请求的URl参数来进行调度
    
    (7)、hdr(name),表示根据HTTP请求头来锁定每一次HTTP请求;
    
    (8)、rdp-cookie(name),表示根据据cookie(name)来锁定并哈希每一次TCP请求。
    
    
    常用的负载均衡算法
    
    
    (1)轮询算法:roundrobin   
    
    (2)根据请求源IP算法:source
    
    (3)最少连接者先处理算法:lestconn

    三、配置

    HAproxy 配置中分成五部分内容,当然这些组件不是必选的,可以根据需要选择部分作为配置
        global:参数是进程级别的,通常和操作系统(OS)有关。这些参数一般只设置一次,如果配置无误,就不需要再次配置进行修改。
        defaults:配置默认参数的,这些参数可以被利用配置frontend,backend。Listen 组件
        frontend:接收请求的前虚拟节点,frontend 可以根据规则直接指定具体使用后端的
        backend :后端服务集群的配置,是真实的服务器,一个backend 对应一个或者多个实体服务器
        listen:frontend 和backend 的组合体1、主配置文件

    #---------------------------------------------------------------------
    # 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/syslog    #修改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        #日志配置,所有的日志都记录本地,通过local2输出
     
        chroot      /var/lib/haproxy        #改变haproxy的工作目录
        pidfile     /var/run/haproxy.pid    #指定pid文件的路径
        maxconn     4000                    #最大连接数的设定
        user        haproxy                 #指定运行服务的用户
        group       haproxy                 #指定运行服务的用户组
        daemon
     
        # turn on stats unix socket
        stats socket /var/lib/haproxy/stats
     
    #---------------------------------------------------------------------
    # common defaults that all the 'listen' and 'backend' sections will
    # use if not designated in their block
    #---------------------------------------------------------------------
    defaults
         
        mode                    http                  #默认使用协议,可以为{http|tcp|health} http:是七层协议 tcp:是四层 health:只返回OK
        log                     global                #全局日志记录
        option                  httplog               #详细记录http日志
        option                  dontlognull           #不记录空日志
        option http-server-close                      #启用http-server-close
        option forwardfor       except 127.0.0.0/8    #来自这些信息的都不forwardfor
        option                  redispatch            #重新分发,ServerID对应的服务器宕机后,强制定向到其他运行正常的服务器
        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规则以如".html"结尾的文件;-i:忽略大小写
        acl url_static       path_beg       -i /static /images /javascript /stylesheets
        acl url_static       path_end       -i .jpg .gif .png .css .js
     
        use_backend static          if url_static    #调用后端服务器并检查ACL规则是否被匹配
        default_backend             app              #客户端访问时默认调用后端服务器地址池
     
    #---------------------------------------------------------------------
    # static backend for serving up images, stylesheets and such
    #---------------------------------------------------------------------
    backend static                    #定义后端服务器
        balance     roundrobin        #定义算法;基于权重进行轮询
        server      static 127.0.0.1:4331 check    check:启动对后端server的健康状态检测
     
    #---------------------------------------------------------------------
    # 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
    
    状态监控    
    listen stats                            #关联前端和后端定义一个完整的代理
        mode http                           #设置代理协议
        bind 0.0.0.0:1080                   #绑定相应的端口
        stats enable                        #开启Haproxy统计状态
        stats refresh 3s             #统计页面自动刷新时间间隔
        stats hide-version                  #隐藏代理服务器版本
        stats uri     /haproxyadmin?stats   #访问的url
        stats realm   Haproxy Statistics   #统计页面认证时提示内容信息
        stats auth    admin:123456          #设置登录用户和密码     
        stats admin if TRUE                 #如果认证通过,则就可以打开stats
        
    网站检测listen 定义
    listen site_status
        bind 0.0.0.0:1081
        mode http
        log 127.0.0.1 local0 err #[err warning info debug]
        monitor-uri /site_status 网站健康检测url,用来检测haproxy 管理的网站是否可以用,正常返回200,不正常返回500
        acl site_dead nbsrv(denali_server) lt 1 当挂载在负载均衡上的指定backend 的中有效机器数小于1 台时返回true
        acl site_dead nbsrv(tm_server) lt 1
        acl site_dead nbsrv(mms_server) lt 1
        monitor fail if site_dead 当满足策略的时候返回500
        monitor-net 192.168.0.252/31 如果192.168.0.252 或者192.168.0.31 这两台机器挂了,就认为网站挂了,这时候返回500,判断标准是如果mode 是http 返回200 认为是正常的,如果
        mode 是tcp 认为端口畅通是好的    
        
        
    
        
    动静分离    
    frontend http-in
        bind *:80
        mode http
        log global
        option httpclose
        option logasap
        option dontlognull
        capture request  header Host len 20
        capture request  header Referer len 60
        acl url_static  path_end -i .html .jpg .gif
        acl url_dynamic path_end -i .php
        default_backend servers
        use_backend lnmmp if url_dynamic
     
    backend servers
        balance roundrobin
        server websrv1 192.168.0.102:80 check rise 2 fall 1 weight 2 maxconn 2000
        server websrv2 192.168.0.106:80 check rise 2 fall 1 weight 2 maxconn 2000
     
    backend lnmmp
        balance source
        server websrv3 192.168.0.107:80 check rise 2 fall 1 maxconn 2000
        
        
    https 的配置方法
    listen loging_https_server
        bind 0.0.0.0:443 绑定https 的443 端口
        mode tcp https 必须使用tcp 模式
        log global
        balance roundrobin
        option httpchk GET /member/login.html HTTP/1.1
    Host:login.daily.taobao.net
        server vm94f.sqa 192.168.212.94:443 check port 80 inter 6000 rise 3 fall 3
        server v21520.sqa 192.168.215.120:443 check port 80 inter 6000 rise 3 fall 3
    
    Frontedn 配置
    frontend http_80_in
        bind 0.0.0.0:80 监听端口
        mode http http 的7 层模式
        log global 应用全局的日志配置
        option httplog 启用http 的log
        option httpclose 每次请求完毕后主动关闭http 通道,http-proxy 不支持keep-alive 模式
        option forwardfor 如果后端服务器需要获得客户端的真实ip 需要配置此参数,可以从http Header 中获得客户端ip
        
    HAProxy 错误页面设置
        errorfile 400 /home/admin/haproxy/errorfiles/400.http
        errorfile 403 /home/admin/haproxy/errorfiles/403.http
        errorfile 408 /home/admin/haproxy/errorfiles/408.http
        errorfile 500 /home/admin/haproxy/errorfiles/500.http
        errorfile 502 /home/admin/haproxy/errorfiles/502.http
        errorfile 503 /home/admin/haproxy/errorfiles/503.http
        errorfile 504 /home/admin/haproxy/errorfiles/504.http
    
    
    Backend 的设置
    Backend mmm_server
        Mode http http 的7 层模式
        Balance roudrobin 负载均衡的方式,roundrobin 为平均方式
        Cookies SERVERID 允许插入serverid 到cookie 中,serverid 后面可以定义
        Server mms1 10.1.5.134:80 cookie 1 check inter 1500 rise 3 fall 3 weight 1
        Server mms2 10..1.6.134:80 cookie 2 check inter 1500 rise 3 fall 3 weight 2    服务器定义,cookie1 标示serverid 为1,check inter 1500 是检测心跳频率,rise 3 是3 次正确认为服务器可用。Fall 3 是3 次失败认为服务器不可用,weight 代表权重
    
    Backend denali_server
        Mode http
        Balance source 负载均衡的方式,source 根据客户端ip 进行哈希的方式
        Option allbackups 但设置了backup 的时候,默认第一个backup 会优先,设置option allbackups后,所有备份服务器权重都一样
        Option httpchk GET /myfile/home/check.html HTTP/1.1
    Host:my.gemini.taobao.net
        Server denlai1 10.1.5.114:80 minconn 4 maxconn 12 check inter 1500 rise 3 fall 3
        Server denlai2 10.1.6.104:80 minconn 10 maxconn 20 check inter 1500 rise 3 fall 3    可以根据机器的性能不同,不使用默认的连接数配置而使用自己的特殊的连接数配置
        Server dnali-back1 10.1.7.114:80 check backup inter 1500 rise 3 fall 3
        Server dnali-back2 10.1.7.114:80 check backup inter 1500 rise 3 fall 3备份机器配置,正常情况备用机不会使用,当主机的全部服务器都down 的时候备份机会启
        用
        
    backend tm_server
        mode http#负载均衡的方式,leastconn 根据服务器当前的请求数,取当前请求数最少的服务器
        balance leastconn
        option httpchk GET /trade/itemlist/prepayCard.htm HTTP/
        server tm1 10.1.5.115:80 check inter 1500 rise 3 fall 3
        server tm2 10.1.6.105:80 check inter 1500 rise 3 fall 3
        ######reqisetbe 自定义关键字匹配backend 部分#######################
    backend dynamic
        mode http
        balance source
        option httpchk GET /welcome.html HTTP/1.1
    Host:www.taobao.net
        server denlai1 10.3.5.114:80 check inter 1500 rise 3 fall 3
        server denlai2 10.4.6.104:80 check inter 1500 rise 3 fall 3
    backend stats
        mode http
    balance source
        option httpchk GET /welcome.html HTTP/1.1
    Host:www.taobao.net
        server denlai1 10.5.5.114:80 check inter 1500 rise 3 fall 3
        server denlai2 10.6.6.104:80 check inter 1500 rise 3 fall 3
        listen www-balancer
        bind 192.168.16.21:80
        mode http
    balance roundrobin
        maxconn 32768
        timeout connect 5000ms #连接超时
        timeout client 50000ms #客户端超时
        timeout server 50000ms #服务端超时
        retries 3
        server www1 192.168.16.2:80 cookie A check inter 1500 rise 3 fall 3 weight 1
        server www2 192.168.16.3:80 cookie B check inter 1500 rise 3 fall 3 weight 1
        server www3 192.168.16.4:80 cookie C check inter 1500 rise 3 fall 3 weight 1
        option httpchk GET /index.html
        option httplog
        option forwardfor 获取clietn ip 地址的“X-Forwarder-For" header,需注
        意,此选项要在apache 或者nginx 里面配置日志格式,见附录
        option httpclose
        option redispatch
        option originalto 获取原始目的地ip 的"X-Original-To" header

    2、日志配置文件

    配置Haproxy的日志    
    默认情况下,Haproxy没有启用日志文件,但是我们可以根据haproxy的配置文件做修改。
    (1)修改系统日志的配置文件
    # vim /etc/sysconfig/rsyslog
    SYSLOGD_OPTIONS="-c 2 -r"
    (2)增加日志设备
    # vim /etc/rsyslog.conf 
    local2.*                                                /var/log/haproxy.log
    (3)重启一下日志服务
    # /etc/init.d/rsyslog restart
    关闭系统日志记录器:                                       [确定]
    启动系统日志记录器:                                       [确定]
    (4)查看日志记录信息
    # tail -f /var/log/haproxy.log

    四、连接处理

    1. 关键数据结构 session
    haproxy 负责处理请求的核心数据结构是 struct session,
    
    从业务的处理的角度,简单介绍一下对 session 的理解:
    
        haproxy 每接收到 client 的一个连接,便会创建一个 session 结构,该结构一直伴随着连接的处理,直至连接被关闭,session 才会被释放
        haproxy 其他的数据结构,大多会通过引用的方式和 session 进行关联
        一个业务 session 上会存在两个 TCP 连接,一个是 client 到 haproxy,一个是 haproxy 到后端 server。
    
    此外,一个 session,通常还要对应一个 task,haproxy 最终用来做调度的是通过 task。
    2. 相关初始化
    
    在 haproxy 正式处理请求之前,会有一系列初始化动作。这里介绍和请求处理相关的一些初始化。
    2.1. 初始化处理 TCP 连接的方法
    
    初始化处理 TCP 协议的相关数据结构,主要是和 socket 相关的方法的声明。详细见下面 proto_tcpv4 (proto_tcp.c)的初始化:
    
    static struct protocol proto_tcpv4 = {
        .name = "tcpv4",
        .sock_domain = AF_INET,
        .sock_type = SOCK_STREAM,
        .sock_prot = IPPROTO_TCP,
        .sock_family = AF_INET,
        .sock_addrlen = sizeof(struct sockaddr_in),
        .l3_addrlen = 32/8,
        .accept = &stream_sock_accept,
        .read = &stream_sock_read,
        .write = &stream_sock_write,
        .bind = tcp_bind_listener,
        .bind_all = tcp_bind_listeners,
        .unbind_all = unbind_all_listeners,
        .enable_all = enable_all_listeners,
        .listeners = LIST_HEAD_INIT(proto_tcpv4.listeners),
        .nb_listeners = 0,
    };
    
    2.2. 初始化 listener
    
    listener,顾名思义,就是用于负责处理监听相关的逻辑。
    
    在 haproxy 解析 bind 配置的时候赋值给 listener 的 proto 成员。函数调用流程如下:
    
    cfgparse.c
    -> cfg_parse_listen
    -> str2listener
    -> tcpv4_add_listener
    -> listener->proto = &proto_tcpv4;
    
    由于这里初始化的是 listener 处理 socket 的一些方法。可以推断, haproxy 接收 client 新建连接的入口函数应该是 protocol 结构体中的 accpet 方法。对于tcpv4 来说,就是 stream_sock_accept() 函数。该函数到 1.5-dev19 中改名为 listener_accept()。这是后话,暂且不表。
    
    listener 的其他初始化
    
    cfgparse.c
    -> check_config_validity
    -> listener->accept = session_accept;
       listener->frontend = curproxy; (解析 frontend 时,会执行赋值: curproxy->accept = frontend_accept)
       listener->handler = process_session;
    
    整个 haproxy 配置文件解析完毕,listener 也已初始化完毕。可以简单梳理一下几个 accept 方法的设计逻辑:
    
        stream_sock_accept(): 负责接收新建 TCP 连接,并触发 listener 自己的 accept 方法 session_accept()
        session_accept(): 负责创建 session,并作 session 成员的初步初始化,并调用 frontend 的 accept 方法 front_accetp()
        frontend_accept(): 该函数主要负责 session 前端的 TCP 连接的初始化,包括 socket 设置,log 设置,以及 session 部分成员的初始化
    
    下文分析 TCP 新建连接处理过程,基本上就是这三个函数的分析。
    2.3. 绑定所有已注册协议上的 listeners
    
    haproxy.c 
    -> protocol_bind_all 
    -> all registered protocol bind_all
    -> tcp_bind_listeners (TCP)
    -> tcp_bind_listener 
    -> [ fdtab[fd].cb[DIR_RD].f = listener->proto->accept ]
    
    该函数指针指向 proto_tcpv4 结构体的 accept 成员,即函数 stream_sock_accept
    2.4. 启用所有已注册协议上的 listeners
    
    把所有 listeners 的 fd 加到 polling lists 中 haproxy.c -> protocol_enable_all -> all registered protocol enable_all -> enable_all_listeners (TCP) -> enable_listener 函数会将处于 LI_LISTEN 的 listener 的状态修改为 LI_READY,并调用 cur poller 的 set 方法, 比如使用 sepoll,就会调用 __fd_set
    3. TCP 连接的处理流程
    3.1. 接受新建连接
    
    前面几个方面的分析,主要是为了搞清楚当请求到来时,处理过程中实际的函数调用关系。以下分析 TCP 建连过程。
    
    haproxy.c 
    -> run_poll_loop 
    -> cur_poller.poll 
    -> __do_poll (如果配置使用的是 sepoll,则调用 ev_sepoll.c 中的 poll 方法) 
    -> fdtab[fd].cb[DIR_RD].f(fd) (TCP 协议的该函数指针指向 stream_sock_accept )
    -> stream_sock_accept
    -> 按照 global.tune.maxaccept 的设置尽量可能多执行系统调用 accept,然后再调用 l->accept(),即 listener 的 accept 方法 session_accept
    -> session_accept
    
    session_accept 主要完成以下功能
    
        调用 pool_alloc2 分配一个 session 结构
        调用 task_new 分配一个新任务
        将新分配的 session 加入全局 sessions 链表中
        session 和 task 的初始化,若干重要成员的初始化如下
            t->process = l->handler: 即 t->process 指向 process_session
            t->context = s: 任务的上下文指向 session
            s->listener = l: session 的 listener 成员指向当前的 listener
            s->si[] 的初始化,记录 accept 系统调用返回的 cfd 等
            初始化 s->txn
            为 s->req 和 s->rep 分别分配内存,并作对应的初始化
                s->req = pool_alloc2(pool2_buffer)
                s->rep = pool_alloc2(pool2_buffer)
                从代码上来看,应该是各自独立分配 tune.bufsize + sizeof struct buffer 大小的内存
            新建连接 cfd 的一些初始化
                cfd 设置为非阻塞
                将 cfd 加入 fdtab[] 中,并注册新建连接 cfg 的 read 和 write 的方法
                fdtab[cfd].cb[DIR_RD].f = l->proto->read,设置 cfd 的 read 的函数 l->proto->read,对应 TCP 为 stream_sock_read,读缓存指向 s->req,
                fdtab[cfd].cb[DIR_WR].f = l->proto->write,设置 cfd 的 write 函数 l->proto->write,对应 TCP 为 stream_sock_write,写缓冲指向 s->rep
        p->accept 执行 proxy 的 accept 方法即 frontend_accept
            设置 session 结构体的 log 成员
            根据配置的情况,分别设置新建连接套接字的选项,包括 TCP_NODELAY/KEEPALIVE/LINGER/SNDBUF/RCVBUF 等等
            如果 mode 是 http 的话,将 session 的 txn 成员做相关的设置和初始化
    
    3.2. TCP 连接上的接收事件
    
    haproxy.c 
    -> run_poll_loop 
    -> cur_poller.poll 
    -> __do_poll (如果配置使用的是 sepoll,则调用 ev_sepoll.c 中的 poll 方法) 
    -> fdtab[fd].cb[DIR_RD].f(fd) (该函数在建连阶段被初始化为四层协议的 read 方法,对于 TCP 协议,为 stream_sock_read )
    -> stream_sock_read
    
    stream_sock_read 主要完成以下功能
    
        找到当前连接的读缓冲,即当前 session 的 req buffer:
    
            struct buffer *b = si->ib
    
        根据配置,调用 splice 或者 recv 读取套接字上的数据,并填充到读缓冲中,即填充到从 b->r(初始位置应该就是 b->data)开始的内存中
        如果读取到 0 字节,则意味着接收到对端的关闭请求,调用 stream_sock_shutr 进行处理
            读缓冲标记 si->ib->flags 的 BF_SHUTR 置位,清除当前 fd 的 epoll 读事件,不再从该 fd 读取
            如果写缓冲 si->ob->flags 的 BF_SHUTW 已经置位,说明应该是由本地首先发起的关闭连接动作
                将 fd 从 fdset[] 中清除,从 epoll 中移除 fd,执行系统调用 close(fd), fd.state 置位 FD_STCLOSE
                stream interface 的状态修改 si->state = SI_ST_DIS
        唤醒任务 task_wakeup,把当前任务加入到 run queue 中。随后检测 runnable tasks 时,就会处理该任务
    
    3.3. TCP 连接上的发送事件
    
    haproxy.c 
    -> run_poll_loop 
    -> cur_poller.poll 
    -> __do_poll (如果配置使用的是 sepoll,则调用 ev_sepoll.c 中的 poll 方法) 
    -> fdtab[fd].cb[DIR_WR].f(fd) (该函数在建连阶段被初始化为四层协议的 write 方法,对于 TCP 协议,为 stream_sock_write )
    -> stream_sock_write
    
    stream_sock_write主要完成以下功能
    
        找到当前连接的写缓冲,即当前 session 的 rep buffer:
    
            struct buffer *b = si->ob
    
        将待发送的数据调用 send 系统调用发送出去
        或者数据已经发送完毕,需要发送关闭连接的动作 stream_sock_shutw-> 系统调用 shutdown
        唤醒任务 task_wakeup,把当前任务加入到 run queue 中。随后检测 runnable tasks 时,就会处理该任务
    
    3.4. http 请求的处理
    
    haproxy.c 
    -> run_poll_loop 
    -> process_runnable_tasks,查找当前待处理的任务所有 tasks, 然后调用 task->process(大多时候就是 process_session) 进行处理
    -> process_session
    
    process_session 主要完成以下功能
    
        处理连接需要关闭的情形,分支 resync_stream_interface
        处理请求,分支 resync_request (read event)
            根据 s->req->analysers 的标记位,调用不同的 analyser 进行处理请求
            ana_list & AN_REQ_WAIT_HTTP: http_wait_for_request
            ana_list & AN_REQ_HTTP_PROCESS_FE: http_process_req_common
            ana_list & AN_REQ_SWITCHING_RULES:process_switching_rules
        处理应答,分支 resync_response (write event)
            根据 s->rep->analysers 的标记位,调用不同的 analyser 进行处理请求
            ana_list & AN_RES_WAIT_HTTP: http_wait_for_response
            ana_list & AN_RES_HTTP_PROCESS_BE:http_process_res_common
        处理 forward buffer 的相关动作
        关闭 req 和 rep 的 buffer,调用 pool2_free 释放 session 及其申请的相关内存,包括读写缓冲 (read 0 bytes)
            pool_free2(pool2_buffer, s->req);
            pool_free2(pool2_buffer, s->rep);
            pool_free2(pool2_session, s);
        task 从运行任务队列中清除,调用 pool2_free 释放 task 申请的内存: task_delete(); task_free();
  • 相关阅读:
    JS 时间格式化函数
    jQuery 输入框 在光标位置插入内容, 并选中
    js Html结构转字符串形式显示
    .aspx 页面引用命名空间
    sql随机实现,sql GUID
    一个清华女大学生与一个普通二本男大学生的QQ聊天记录
    asp.net inc 的使用
    JS编码,解码. asp.net(C#)对应解码,编码
    SQL的小常识, 备忘之用, 慢慢补充.
    Js 时间间隔计算(间隔天数)
  • 原文地址:https://www.cnblogs.com/Mahavairocana/p/8166058.html
Copyright © 2020-2023  润新知