智者一切求自己,愚者一切求他人 [登录·注册]

吕滔博客

首页 开发 运维 工具 摄影

一步步打造每秒百万请求的高性能Web集群

运维 memory 发布于June 17, 2015 标签: Nginx, LVS

如何生成每秒百万级别的 HTTP 请求?

负载生成工具(Load-Generating Tools)

在进行负责测试时要牢记一件重要的事:你能在 Linux 上建立多少个 socket 连接。这个限制是硬编码在内核里的,最典型的就是临时 W 端口的限制。(在某种程度上)你可以在 /etc/sysctl.conf 里扩展它。但是基本上,一台 Linux 机器只能同时打开大约 64,000 个 socket 。因此在负载测试时,我们不得不通过在单一的连接上尽可能多地发出请求来充分利用 socket 。 除此之外,我们还需要不止一台的机器来产生负载。否则,负载生成器会把可用的 socket 占用导致不能产生足够的负载。

我一开始用的是‘ab’,Apache Bench 。它是我所知道的 http 基准测试工具中最简单、最通用的。并且它是 Apache 附带的产品,因此它可能已经存在于你的系统中。不幸的是,我在使用它的时候每秒大约只能生成 900 个请求。虽然我见过其他人使用它每秒能达到 2,000 个请求,但我可以立即告诉你,‘ab’并不适合我们的基准测试。

Httperf

接着,我尝试了 ‘httperf’。这个工具更强大,但是它依然相对简单并且功能有限。要算出每秒生产了多少个请求并不是仅传递参数那么简单。经过我的多次尝试,获取了每秒超过几百请求的结果。例如:

它以每秒 1,000 个的速率创建了 100,000 个会话(session)。每次会话发起 5 次请求,时间间隔为 2 秒。

httperf --hog --server=192.168.122.10 --wsess=100000,5,2 --rate 1000 --timeout 5
Total: connections 117557 requests 219121 replies 116697 test-duration 111.423 s
 
Connection rate: 1055.0 conn/s (0.9 ms/conn, <=1022 concurrent connections)
Connection time [ms]: min 0.3 avg 865.9 max 7912.5 median 459.5 stddev 993.1
Connection time [ms]: connect 31.1
Connection length [replies/conn]: 1.000
 
Request rate: 1966.6 req/s (0.5 ms/req)
Request size [B]: 91.0
 
Reply rate [replies/s]: min 59.4 avg 1060.3 max 1639.7 stddev 475.2 (22 samples)
Reply time [ms]: response 56.3 transfer 0.0
Reply size [B]: header 267.0 content 18.0 footer 0.0 (total 285.0)
Reply status: 1xx=0 2xx=116697 3xx=0 4xx=0 5xx=0
 
CPU time [s]: user 9.68 system 101.72 (user 8.7% system 91.3% total 100.0%)
Net I/O: 467.5 KB/s (3.8*10^6 bps)

最终,我使用这些设置达到了每秒 6,622 个连接:

httperf --hog --server 192.168.122.10 --num-conn 100000 --ra 20000 --timeout 5

(总共创建了 100,000 个连接,并且以每秒 20,000 个连接的固定速率创建)
它还有一些潜在的优势,并且拥有比‘ab‘更多的特性。但它不是我要用在这个项目里的重量级工具。我需要的是能够支持分布式多负载测试节点的工具。因此,我的下一个尝试是:Jmeter。

Apache Jmeter

这是一个功能齐全的 web 应用测试套件,它可以模拟真实用户的所有行为。你可以使用 Jmeter 的代理去访问你的网站,进行点击、登陆、模仿用户可以做的所有行为。Jemeter 会把这些行为记录下来作为测试用例。然后 Jmeter 会反复执行这些动作来模拟你想要的用户数量。尽管配置 Jmeter 比 ‘ab‘ 和 ’httperf‘ 复杂得多,但它是一个很有趣的工具!

根据我的测试,它每秒可以产生 14,000 个请求!这绝对是一个好的进展。

我使用了 Googlle Code project 上的一些插件,并且使用它们的“Stepping Threads”和“HTTP RAW”请求,最终每秒大约可以产生 30,000 个请求!但这已经达到极限了,所以还要寻找另一个工具。这里有一个我之前的 Jmeter 配置,希望可以帮助到其他人。虽然这个配置离完美相差甚远,但有时它可以满足你的要求。

Tsung: 重型的(heavy-duty)、分布式的、多协议测试工具
它每秒基本可以产生 40,000 个请求,这绝对是我们想要的工具。类似于 Jmeter,你可以把一些行为记录下来在测试时运行,并且可以测试大多数的协议。比如 SSL、HHTP、WebDAV、SOAP、PostgreSQLMySQL、LDAP 和 Jabber/XMPP。与 Jmeter 不同的是,它没有让人感到迷茫的 GUI 设置,它仅有一个 XML 配置文件,和一些你选择的分布式节点的 SSH 密钥。它的简洁和效率对我的吸引力,完全不亚于它的健壮性和可扩展性。我发现它是一个很强大的工具,在正确的配置下它可以每秒产生百万级的 HTTP 请求。

除此之外,Tsung 还可以在 html 上产生图表以及输入你的测试的详细报告。测试的结果通俗易懂,并且你甚至可以把这些图片展示给你的 boss 看!

在这个系列文章的剩余部分,我还会讲解这个工具。现在你可以继续浏览下面的配置说明,或者直接跳到下一页。

CentOS 6.2 上安装 Tsung

首先,你要安装(Erlang 需要的) EPEL 源。因此,在进行下一步之前要把它安装好。安装完后,继续安装你用来产生负载的每个节点需要的包。如果你还没有在节点之间建立无密码 SSH 密钥(passwordless SSH key),那么请建立之。

yum -y install erlang perl perl-RRD-Simple.noarch perl-Log-Log4perl-RRDs.noarch gnuplot perl-Template-Toolkit firefox

Github 或者 Tsung 的官网上下载最新的 Tsung。

wget http://tsung.erlang-projects.org/dist/tsung-1.4.2.tar.gz

解压并且编译。

tar zxfv  tsung-1.4.2.tar.gz
cd tsung-1.4.2
./configure && make && make install

把示例配置复制到 ~/.tsung 目录里。这是 Tsung 的配置文件和日志文件的存放地方。

cp  /usr/share/doc/tsung/examples/http_simple.xml /root/.tsung/tsung.xml

你可以根据你的需求去编辑这个配置文件,或者使用我的配置文件。经过大量的尝试以及失败后,我目前的配置文件在使用 7 个分布式节点时可以每秒产生 5 百万个 HTTP 请求。

<?xml version="1.0"?>
<!DOCTYPE tsung SYSTEM "/usr/share/tsung/tsung-1.0.dtd">
<tsung loglevel="notice" version="1.0">
 
<clients>
<client host="localhost" weight="1" cpu="10" maxusers="40000">
<ip value="192.168.122.2"/>
</client>
<client host="loadnode1" weight="1" cpu="9" maxusers="40000">
<ip value="192.168.122.2"/>
</client>
<client host="loadnode2" weight="1" maxusers="40000" cpu="8">
<ip value="192.168.122.3"/>
</client>
<client host="loadnode3" weight="1" maxusers="40000" cpu="9">
<ip value="192.168.122.21"/>
</client>
<client host="loadnode4" weight="1" maxusers="40000" cpu="9">
<ip value="192.168.122.11"/>
</client>
<client host="loadnode5" weight="1" maxusers="40000" cpu="9">
<ip value="192.168.122.12"/>
</client>
<client host="loadnode6" weight="1" maxusers="40000" cpu="9">
<ip value="192.168.122.13"/>
</client>
<client host="loadnode7" weight="1" maxusers="40000" cpu="9">
<ip value="192.168.122.14"/>
</client>
</clients>
 
<servers>
<server host="192.168.122.10" port="80" type="tcp"/>
</servers>
 
<load>
<arrivalphase phase="1" duration="10" unit="minute">
<users maxnumber="15000" arrivalrate="8" unit="second"/>
</arrivalphase>
 
<arrivalphase phase="2" duration="10" unit="minute">
<users maxnumber="15000" arrivalrate="8" unit="second"/>
</arrivalphase>
 
<arrivalphase phase="3" duration="30" unit="minute">
<users maxnumber="20000" arrivalrate="3" unit="second"/>
</arrivalphase>
 
</load>
 
<sessions>
<session probability="100" name="ab" type="ts_http">
<for from="1" to="10000000" var="i">
<request> <http url="/test.txt" method="GET" version="1.1"/> </request>
</for>
</session>
</sessions>
</tsung>

刚开始的时候有很多东西要理解,但你一旦理解了它们后就会变得很简单。

一旦你已经很好地理解了它们,你就可以创建一个便利的别名,去快速观察 Tsung 报告。

vim ~/.bashrc
alias treport="/usr/lib/tsung/bin/tsung_stats.pl; firefox report.html"
source ~/.bashrc

然后启动 Tsung

[root@loadnode1 ~] tsung start
Starting Tsung
"Log directory is: /root/.tsung/log/20120421-1004"

结束后观察报告

cd /root/.tsung/log/20120421-1004
treport

1.jpg
使用 Tsung 去规划你的集群构造
现在我们拥有了一个足够强大的负载测试工具,我们可以规划余下的集群构造了:

  1. 使用 Tsung 去测试一个单一的 HTTP 服务器。获取一个基本的基准。
  2. 对 web 服务器进行调优,定期使用 Tsung 进行测试提高性能。
  3. 对这些系统的 TCP 套接字进行调优,获取最佳的网络性能。再来一次,测试,测试,不停地测试。
  4. 构造 LVS 集群,它包含了这些充分调优过的 web 服务器。
  5. 使用 Tsung IP 集群对 LVS 进行压力测试。

为最佳性能调优 Nginx

通常来说,一个优化良好的 Nginx Linux 服务器可以达到 500,000 – 600,000 次/秒 的请求处理性能,然而我的 Nginx 服务器可以稳定地达到 904,000 次/秒 的处理性能,并且我以此高负载测试超过 12 小时,服务器工作稳定。

这里需要特别说明的是,本文中所有列出来的配置都是在我的测试环境验证的,而你需要根据你服务器的情况进行配置:

从 EPEL 源安装 Nginx:

yum -y install nginx

备份配置文件,然后根据你的需要进行配置:

cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.orig
vim /etc/nginx/nginx.conf
# This number should be, at maximum, the number of CPU cores on your system.
# (since nginx doesn't benefit from more than one worker per CPU.)
# 这里的数值不能超过 CPU 的总核数,因为在单个核上部署超过 1 个 Nginx 服务进程并不起到提高性能的作用。
worker_processes 24;
 
# Number of file descriptors used for Nginx. This is set in the OS with 'ulimit -n 200000'
# or using /etc/security/limits.conf
# Nginx 最大可用文件描述符数量,同时需要配置操作系统的 "ulimit -n 200000",或者在 /etc/security/limits.conf 中配置。 
worker_rlimit_nofile 200000;
 
# only log critical errors
# 只记录 critical 级别的错误日志
error_log /var/log/nginx/error.log crit
 
# Determines how many clients will be served by each worker process.
# (Max clients = worker_connections * worker_processes)
# "Max clients" is also limited by the number of socket connections available on the system (~64k)
# 配置单个 Nginx 单个进程可服务的客户端数量,(最大值客户端数 = 单进程连接数 * 进程数 )
# 最大客户端数同时也受操作系统 socket 连接数的影响(最大 64K )
worker_connections 4000;
 
# essential for linux, optmized to serve many clients with each thread
# Linux 关键配置,允许单个线程处理多个客户端请求。
use epoll;
 
# Accept as many connections as possible, after nginx gets notification about a new connection.
# May flood worker_connections, if that option is set too low.
# 允许尽可能地处理更多的连接数,如果 worker_connections 配置太低,会产生大量的无效连接请求。
multi_accept on;
 
# Caches information about open FDs, freqently accessed files.
# Changing this setting, in my environment, brought performance up from 560k req/sec, to 904k req/sec.
# I recommend using some varient of these options, though not the specific values listed below.
# 缓存高频操作文件的FDs(文件描述符/文件句柄)
# 在我的设备环境中,通过修改以下配置,性能从 560k 请求/秒 提升到 904k 请求/秒。
# 我建议你对以下配置尝试不同的组合,而不是直接使用这几个数据。
open_file_cache max=200000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
 
# Buffer log writes to speed up IO, or disable them altogether
# 将日志写入高速 IO 存储设备,或者直接关闭日志。
# access_log /var/log/nginx/access.log main buffer=16k;
access_log off;
 
# Sendfile copies data between one FD and other from within the kernel.
# More efficient than read() + write(), since the requires transferring data to and from the user space.
# 开启 sendfile 选项,使用内核的 FD 文件传输功能,这个比在用户态用 read() + write() 的方式更加高效。
sendfile on;
 
# Tcp_nopush causes nginx to attempt to send its HTTP response head in one packet,
# instead of using partial frames. This is useful for prepending headers before calling sendfile,
# or for throughput optimization.
# 打开 tcp_nopush 选项,Nginux 允许将 HTTP 应答首部与数据内容在同一个报文中发出。
# 这个选项使服务器在 sendfile 时可以提前准备 HTTP 首部,能够达到优化吞吐的效果。
tcp_nopush on;
 
# don't buffer data-sends (disable Nagle algorithm). Good for sending frequent small bursts of data in real time.
# 不要缓存 data-sends (关闭 Nagle 算法),这个能够提高高频发送小数据报文的实时性。
tcp_nodelay on;
 
# Timeout for keep-alive connections. Server will close connections after this time.
# 配置连接 keep-alive 超时时间,服务器将在超时之后关闭相应的连接。
keepalive_timeout 30;
 
# Number of requests a client can make over the keep-alive connection. This is set high for testing.
# 单个客户端在 keep-alive 连接上可以发送的请求数量,在测试环境中,需要配置个比较大的值。
keepalive_requests 100000;
 
# allow the server to close the connection after a client stops responding. Frees up socket-associated memory.
# 允许服务器在客户端停止发送应答之后关闭连接,以便释放连接相应的 socket 内存开销。
reset_timedout_connection on;
 
# send the client a "request timed out" if the body is not loaded by this time. Default 60.
# 配置客户端数据请求超时时间,默认是 60 秒。
client_body_timeout 10;
 
# If the client stops reading data, free up the stale client connection after this much time. Default 60.
# 客户端数据读超时配置,客户端停止读取数据,超时时间后断开相应连接,默认是 60 秒。
send_timeout 2;
 
# Compression. Reduces the amount of data that needs to be transferred over the network
# 压缩参数配置,减少在网络上所传输的数据量。
gzip on;
gzip_min_length 10240;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml;
gzip_disable "MSIE [1-6].";

启动 Nginx 并配置起机自动加载。

service nginx start
chkconfig nginx on

配置 Tsung 并启动测试,测试差不多 10 分钟左右就能测试到服务器的峰值能力,具体的时间与你的 Tsung 配置相关。

vim ~/.tsung/tsung.xml
<server host="YOURWEBSERVER" port="80" type="tcp"/>

tsung start

你觉得测试结果已经够了的情况下,通过 ctrl+c 退出,之后使用我们之前配置的别名命令 treport 查看测试报告。

WEB 服务器调优,第二部分:TCP 协议栈调优

这个部分不只是对 Ngiinx 适用,还可以在任何 WEB 服务器上使用。通过对内核 TCP 配置的优化可以提高服务器网络带宽。

以下配置在我的 10-Gbase-T 服务器上工作得非常完美,服务器从默认配置下的 8Gbps 带宽提升到 9.3Gbps。

当然,你的服务器上的结论可能不尽相同。

下面的配置项,我建议每次只修订其中一项,之后用网络性能测试工具 netperf、iperf 或是用我类似的测试脚本 cluster-netbench.pl 对服务器进行多次测试。
cluster-netbench.pl:

#!/usr/bin/perl
#
# My hideous first perl script!
# Stefanie Edgar
# Feb 2012
#
# netbench.pl
# A script to run network benchmarks on remote hosts and return the results.
# Useful for testing out new network settings across cluster nodes.

use strict;
use warnings;
use IO::Handle;

# remote hosts to run network benchmarks on
my @hosts = qw(192.168.12.1 192.168.12.2 192.168.12.3 192.168.12.4 192.168.12.5 192.168.12.6 192.168.12.7 192.168.12.8 192.168.12.9);

# configuration
# choose between one or more network benchmark tests to run
my $conf={
        debug        =>        0,
        iperf        =>        1,
        netperf      =>        1,
        netpipe      =>        0,
        path         =>        {
                               iperf        =>        "/usr/bin/iperf",
                               netpipe      =>        "/usr/bin/NPtcp",
                               netperf      =>        "/usr/bin/netperf",
                               netserver    =>        "/usr/bin/netserver",
                               iperf        =>        "/usr/bin/iperf",
                               ssh          =>        "/usr/bin/ssh",
                               },
};

# For each host, create a child processes to start the background daemons remotely.
# Then locally run the client benchmark program for that host.
foreach my $host (@hosts)
{
        # Store the PIDs of the child processes in this hash as they're spawned.
        my %pids;

        print "============ Network Benchmarks for $host ============ \n";

        # -- fork for netpipe -- #
        defined(my $pid=fork) or die "Can't fork, error: [$!].\n";
        if ($pid)
        {
                # record child pid in the hash. 
                $pids{$pid}=1;
        }
        else
        {
                # Start up the background daemon on the remote hosts.
                print "netpipe fork: started. \n" if $conf->{debug};
                print "netpipe fork: calling function call_netpipe_on_remote($host) \n" if $conf->{debug};
                call_netpipe_on_remote($host);

                # When the function returns, this child fork exits. 
                # Though, netpipe won't exit without being killed,
                # so I'll have to make sure to kill netpipe after
                # the test has run. Until then, this will stay running.

                print "fork: exiting\n" if $conf->{debug};
                exit;
        }


        # -- fork for netperf -- #
        defined(my $pid=fork) or die "Can't fork, error: [$!].\n";
        if ($pid)
        {
                $pids{$pid}=1;
        } 
        else
        {
               call_netperf_on_remote($host);
                exit;
        }
        
        # -- fork for iperf -- #
        defined(my $pid=fork) or die "Can't fork, error: [$!].\n";
        if ($pid)
        {
               $pids{$pid}=1;
        } 
        else
        {
                print "iperf fork: started. \n" if $conf->{debug};
                print "iperf fork: calling function call_iperf_on_remote($host) \n" if $conf->{debug};
                call_iperf_on_remote($host);
                print "iperf fork: exiting\n" if $conf->{debug};
                exit;
        }

        # wait for daemons to get set up, then run client-side benchmarks on the local machine
        sleep 3;
        run_local_netpipe($host) if $conf->{netpipe};
        run_local_iperf($host) if $conf->{iperf};
        run_local_netperf($host) if $conf->{netperf};
}

#############
# Functions #
#############

sub call_netpipe_on_remote
{
    print "call_netpipe_on_remote: function started. \n" if $conf->{debug};

    # param passed to function, tells where to run NetPIPE
    my $host = shift;

    print "call_netpipe_on_remote: calling kill_remote_process($host, $conf->{path}{netpipe}) \n" if $conf->{debug};
    kill_remote_process($host, $conf->{path}{netpipe});
    sleep 1; # wait for process to die before proceeding

    print "call_netpipe_on_remote: attempting to start NetPIPE on $host\n" if $conf->{debug};

    # create file handle, then specify a shell command to start the load generator
    my $fh=IO::Handle->new();
    my $sc="$conf->{path}{ssh} root\@$host \"$conf->{path}{netpipe} 2>&1\"";

    # open the file handle, using the command and catching the output
    open ($fh, "$sc 2>&1 |") or die "Failed to call: [$sc], error was: $!\n";

    while(<$fh>)
    {
        my $line=$_;
        print "$line\n" if $conf->{debug};
    }
    $fh->close();

    print "call_netpipe_on_remote: exiting. \n" if $conf->{debug};
}

sub call_netperf_on_remote 
{
    print "call_netperf_on_remote: function started. \n" if $conf->{debug};
    my $host = shift;

    print "call_netperf_on_remote: calling kill_remote_process($host, $conf->{path}{netserver}) \n" if $conf->{debug};
    kill_remote_process($host, $conf->{path}{netserver});

    sleep 1; # wait for process to die before proceeding

    print "call_netperf_on_remote: attempting to start NetPerf on $host\n" if $conf->{debug};
    my $fh=IO::Handle->new();
    my $sc="$conf->{path}{ssh} root\@$host \"$conf->{path}{netserver} \"";
    open ($fh, "$sc 2>&1 |") or die "Failed to call: [$sc], error was: $!\n";

    while(<$fh>)
    {
        my $line=$_;
        print "$line\n" if $conf->{debug};
    }
    $fh->close();
    print "call_netperf_on_remote: Netperf\'s netserver runs as a daemon, so this fork doesnt need to stay open. Exiting. \n" if $conf->{debug};
}


sub call_iperf_on_remote
{
    print "call_iperf_on_remote: function started\n" if $conf->{debug};

    my $host = shift;
    print "call_iperf_on_remote: calling kill_remote_process($host, iperf) \n" if $conf->{debug};
    kill_remote_process($host, "iperf");
 
    print "call_iperf_on_remote: attempting to start iperf on $host\n" if $conf->{debug};
    my $fh=IO::Handle->new();
    my $sc="$conf->{path}{ssh} root\@$host \"$conf->{path}{iperf} -s --bind $host\"";
    open ($fh, "$sc 2>&1 |") or die "Failed to call: [$sc], error was: $!\n";
    print "call_iperf_on_remote: iperf daemon started on $host\n" if $conf->{debug};
 
    while(<$fh>)
    {
        chomp;
        my $line=$_;
        print "$line\n" if  $conf->{debug};
    }
    $fh->close();
    print "call_iperf_on_remote: exiting. \n" if $conf->{debug};
}

sub kill_remote_process
{
        print "kill_remote_process: function started.\n" if $conf->{debug};
        # params
        my ($host, $process) = @_;

        print "kill_remote_process: killing all $process on $host\n" if $conf->{debug};
        my $fh=IO::Handle->new();
        my $sc="$conf->{path}{ssh} root\@$host killall $process";
    open ($fh, "$sc 2>&1 |") or die "Failed to call: [$sc], error was: $!\n";

    while(<$fh>)
    {
            my $line=$_;
            print "$line\n" if $conf->{debug};
    }
    $fh->close();
        print "kill_remote_process: exiting.\n" if $conf->{debug};

}

sub run_local_netpipe
{
    print "run_local_netpipe: function started.\n" if $conf->{debug};
    my $host=shift;
    my $fh=IO::Handle->new();
    my $sc="$conf->{path}{netpipe} -h $host 2>&1 | tail -n 10";
    open ($fh, "$sc 2>&1 |") or die "Failed to call: [$sc], error was: $!\n";
    while(<$fh>)
    {
            chomp;
            my $line=$_;
            print "$line\n";
    }
    $fh->close();
    print "run_local_netpipe: post-run. calling kill_remote_process($host, $conf->{path}{netpipe}) \n" if $conf->{debug};
    kill_remote_process($host, $conf->{path}{netpipe});
}

sub run_local_iperf
{
    print "run_local_iperf: function started.\n" if $conf->{debug};
    my $host=shift;
    my $fh=IO::Handle->new();
    my $sc="$conf->{path}{iperf} -c $host |tail -n 1";
    open ($fh, "$sc 2>&1 |") or die "Failed to call: [$sc], error was: $!\n";
    while(<$fh>)
    {
        chomp;
        my $line=$_;
        print "$line\n";
    }
    $fh->close();
        print "run_local_iperf: FH closed. killing off remaining iperf process.\n" if $conf->{debug};
        kill_remote_process($host, "iperf"); 
}

sub run_local_netperf
{
    print "run_local_netperf: function started. \n" if $conf->{debug};
    my $host=shift;
    my $fh=IO::Handle->new();
    my $sc="$conf->{path}{netperf} -l 30 -H $host 2>&1 | tail -n 6";
    open ($fh, "$sc 2>&1 |") or die "Failed to call: [$sc], error was: $!\n";
        while(<$fh>)
        {
            my $line=$_;
            print "$line"; # missing \n here intentially. Netperf has its own newlines.
        }
    $fh->close();
    print "run_local_netperf: post-run cleanup. Calling kill_remote_process($host, $conf->{path}{netserver}) \n" if $conf->{debug};
    kill_remote_process($host, $conf->{path}{netserver}); 
    print "run_local_netperf: Exiting. \n" if $conf->{debug};
}

安装相关软件

yum -y install netperf iperf
vim /etc/sysctl.conf
# Increase system IP port limits to allow for more connections
# 调高系统的 IP 以及端口数据限制,从可以接受更多的连接
net.ipv4.ip_local_port_range = 2000 65000
 
net.ipv4.tcp_window_scaling = 1
 
# number of packets to keep in backlog before the kernel starts dropping them
# 设置协议栈可以缓存的报文数阀值,超过阀值的报文将被内核丢弃
net.ipv4.tcp_max_syn_backlog = 3240000
 
# increase socket listen backlog
# 调高 socket 侦听数阀值
net.core.somaxconn = 3240000
net.ipv4.tcp_max_tw_buckets = 1440000
 
# Increase TCP buffer sizes
# 调大 TCP 存储大小
net.core.rmem_default = 8388608
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
net.ipv4.tcp_congestion_control = cubic

每次修订配置之后都需要执行以下命令使之生效.

sysctl -p /etc/sysctl.conf

别忘了在配置修订之后务必要进行网络 benchmark 测试,这样可以观测到具体是哪个配置修订的优化效果最明显。通过这种有效测试方法可以为你节省大量时间。


用 LVS 搭建一个负载均衡集群

本文基于你已经优化好服务器以及网络协议栈的基础之上,并使用 iperf 与 netperf 工具测试将服务器已优化到支持 500,000 次/秒的静态WEB页面的性能。

现在你已经做好足够准备进行安装服务器集群。

Redhat 官网已经有一些不错的文章,所以我建议你在遇到不明白的问题时查看一下这些文章。不过你先别担心,我接下会一步步地讲解群集搭建的所有操作。

LVS 路由器配置

这里需要一台设备作为路由器,它负责将 TCP 流量均衡到 LVS 集群中的每一台服务器。因此你需要拿出一台设备按以下操作进行配置。如果你的 IP 路由的流量非常小的话,你可拿一台性能最比较弱服务器做为路由器。

1.在 LVS 路由器上安装 LVS 软件

yum groupinstall "Load Balancer"
chkconfig piranha-gui on
chkconfig pulse on

2.配置 WEB 管理的密码

/usr/sbin/piranha-passwd

3.在 iptables 中配置放行端口

vim /etc/sysconfig/iptables
-A INPUT -m state --state NEW -m tcp -p tcp --dport 3636 -j ACCEPT

4.启动 WEB 管理

service piranha-gui start

一定要等到 Piranha 配置结束之后再开启 pulse 。
5.打开报文转发

vim /etc/sysctl.conf
net.ipv4.ip_forward = 1
 
sysctl -p /etc/sysctl.conf

6.启动 WEB 服务器

service nginx start

Direct Routing 模式配置

1.在 LVS 路由器上登录 Piranha WEB 管理界面进行配置。
2.jpg
2.选择 VIRTUAL SERVERS 标签页,创建虚拟 WEB 服务器, 这里的服务器就是你的 WEB 服务器集群。通过这个配置可以让你有多台服务器对外像是一台服务器,因此又被称作虚拟服务器(virtual server)。
点击 ADD,然后点 EDIT。
3.jpg
3.编辑虚拟服务器,首先选择一个 IP 地址作为 Virtual IP(IP 不作为真实服务器使用),然后选择一个设备接口(Device)进行绑定。

点击 ACCEPT 完成配置,这个时候 WEB 页面并不会刷新,不过此时配置已经保存完毕。

点击 REAL SERVER 进行下一步真实服务器配置。
4.jpg

4.配置真实服务器,REAL SERVER 页面用于配置 WEB 集群所对应的真实服务器。
用 ADD 将所有的 HTTP 服务器添加进来,然后用 EDIT 进行服务器的详细配置,之后点 ACCEPT 进行保存。

如果需要重新配置集群,先点 VIRTUAL SERVER 之后重新配置 REAL SERVER。

在 REAL SERVER 页配置完所有的真实服务器之后,依次选择每一行后点击 (DE)ACTIVATE 进行激活。
5.jpg
5.至此,所有的真实服务器配置并激活完毕,下接下来回到 VIRTUAL SERVERS 页

点 (DE)ACTIVATE 激活虚拟服务器。
6.jpg
到此为止路由器配置完毕,现在你可以关闭并退出浏览器,接下来要打开 pulse 对每台服务器进行配置。

service pulse start

输入 ipvsadm 可以看到集群已经正常启动。

# ipvsadm
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 192.168.122.10:http wlc
-> 192.168.122.1:http Route 1   0 0
-> 192.168.122.2:http Route 1   0 0
-> 192.168.122.3:http Route 1   0 0

Direct Routing – 配置每台真实服务器节点

在集群中的每台服务器上按以下步骤进行配置。
1.为真实服务器配置虚拟 IP 地址。

ip addr add 192.168.12.10 dev eth0:1

由于我们希望 IP 地址配置在服务器重启之后也能生效,因此需要将配置写入 /etc/rc.local 文件中。

vim /etc/rc.local
ip addr add 192.168.12.10 dev eth0:1

2.在真实服务器上为虚拟 IP 配置 ARP 表项。
这里要关闭所有真实服务器对虚拟 IP 地址的 ARP 请求的响应,这些服务器只响应物理 IP 地址的ARP请求,在整个集群系统中,仅有 LVS 路由器才能响应虚拟 IP 地址的ARP请求。

yum -y install arptables_jf
arptables -A IN -d <cluster-ip-address> -j DROP
arptables -A OUT -s <cluster-ip-address> -j mangle --mangle-ip-s <realserver-ip-address>

3.在真实服务器上配置完毕之后,保存 ARP 表项配置。

service arptables_jf save
chkconfig --level 2345 arptables_jf on

4.测试

如果 arptables 命令配置正确,只有 LVS 路由器才会应答 Ping 请求。首先确保 pulse 已关闭,之后从群集的任一真实服务器上 ping 虚拟 IP 地址,如果有真实服务器回应这个请求,你可以通过查看 ARP 表项看到它。

ping 192.168.122.10
arp | grep 192.168.122.10

这里可以看到解析到服务器的 mac 地址,然后在这台服务器上关闭 ARP 响应。

还有一个简单而有效测试方法就是使用 curl 向集群请求 WEB 页面,你可以在 LVS 路由器上通过命令 ipvsadm 查看到数据流量。

[root@lvsrouter ~]# watch ipvsadm
[user@outside ~]$ curl http://192.168.122.10/test.txt

使用 Tsung 对集群进行性能测试
到此为此集群服务器已经配置完毕并工作正常,这时你可以通过压力测试看到它的性能是多么的强大。

[root@loadnode1 ~] tsung start
Starting Tsung
"Log directory is: /root/.tsung/log/20120421-1004"

建议测试至少进行 2 个小时以上,因为测试需要经过比较长的时间才能看到 HTTP 的峰值请求速率。在整个测试过程中你可以在集群服务器上通过 htop 命令看到每个 CPU 核的率用率。

这里假设你已经安装好了 EPEL 和 RPMforge 源。

yum -y install htop cluster-ssh
cssh node1 node2 node3 ...
htop

7.jpg
你可以看到 HTTP 服务器在高速地接收并回应 WEB 请求,整个过程 LVS 路由器实际没有多少负载。

在实际使用中请确保服务器的 CPU 占所有核的总负责的平均值小于 CPU 的总核数(比如:我的 24 核系统中,我始终保持负载小于等于 23 个核的能力。),这样所有 CPU 即能够充分发挥能力,同时系统又能够具备单一失效时的冗余能力。

在 Tsung 执行结束之后,可以查看集群服务器压力测试的详细测试报告。

cd /root/.tsung/log/20120421-1004
/usr/lib/tsung/bin/tsung_stats.pl
firefox report.html

本文英文出处:http://dak1n1.com/blog/13-load-balancing-lvs

相关推荐

添加新评论

全部评论:仅有一条评论

  1. yet handsome

    我看了你的这个发现历史真的是惊人的相似,我们已经做到redis集群这一步了,但是可能是由于持久化的策略问题导致redis 集群经常MOVED,并发大的时候流量突然激增,平常4~50M达到2~300M,对于redis集群有没有什么好的建议,是否需要代码级优化

网站状态

  • 栏目分类:49个
  • 发布文章:1324篇
  • 用户评论:697条
  • 开博至今:3998天

正则速查

[abc] 匹配中括号中的单个字符,如a或b或c
[^abc] 匹配除了a、b、c等字符的其他单个字符
[a-z] 匹配一个字符范围,如a到z
[a-zA-Z] 匹配一个字符范围,如a-z 或 A-Z
^ 匹配行的开始
$ 匹配行的结束
\A 匹配一个字符串的开始
\z 匹配一个字符串的结束
. 匹配任意单个字符
\s 匹配空白字符,如空格,TAB
\S 匹配非空白字符
\d 匹配一个数字
\D 匹配非数字
\w 匹配一个字母
\W 匹配非字母
\b 匹配字符边界
(...) 引用所有括号中的内容
(a|b) a或者b
a? 零个或1个a
a* 零个或多个a
a+ 1个或多个a
a{3} 3次重复的a
a{3,} 3次或3次以上重复的a
a{3,6} 3到6次重复的a

修正符

/g 查找所有可能的匹配
/i 不区分大小写
/m 多行匹配
/s 单行匹配
/x 忽略空白模式
/e 可执行模式,PHP专有
/A 强制从目标字符串开头匹配
/D 使用$限制结尾字符,则不允许结尾有换行
/U 只匹配最近的一个字符串;不重复匹配

最新回复

  • tomxuetao: 能个实例吗?
  • memory: 哈哈哈。。。话说也没毛病。
  • 硫酸亚铁: 看了文章觉得自己已经是松鼠癌了 东西总是先占着 反正我有 什么...
  • xl: 支持一下
  • iHerb海淘攻略: 感谢分享
  • memory: POINT
  • sunyunlin: 数据库应该建些什么字段?
  • pengcheng: 已解决
  • pengcheng: Oops! It seems that sphinx was b...
  • 券都有: 感谢分享
  • memory: 这是我3年前玩过一次,当时因为安装完后,发现日常办公还好,但是好...
  • sdf: 想问一下,激活工具激活后,后面会出现提示要再次重新激活吗?
  • memory: 你要用碗装我吗?还是要我把扔的扔进你碗里???!!! &^_^&
  • 薛才杰: 同感,快扔到我的碗里来。。。
  • memory: 要说明一下,在大陆及香港的云主机或VPS,都是不支持的。原因都懂...
  • 历史趣谈LishiQtan: 很好的文章,值得收藏
  • memory: 现在回头看,,,这么烂的代码,也是醉了.