Linux中Apache+Tomcat+JK实现负载均衡和群集的完整过程


转载来源:http://blog.csdn.net/chaijunkun/article/details/6987443


最近在开发的项目需要承受很高的并发量。综合各种情况,决定使用Apache+Tomcat+JK的方式实现负载均衡,并且作为一个统一的服务还要实现群集(同步Session)。

在网上找了很多资料,都是零零散散的,没有一个完整的过程。通过几天的努力,完成了从编译、部署到配置的整个过程,期间也遇到了一些问题。在接下来的文字中将这些过程记录下来,做个笔记同时也分享给大家。

为了重新演示整个过程,我新搭建了一个服务器,各项参数如下:

CPU:Intel Xeon 5506*2

内存:DDR3 4G*4

主机型号:Dell PowerEdge R710

操作系统:CentOS release 5.7 x86_64版

内核版本:2.6.18

gcc版本:4.1.2

g++版本:4.1.2

Java版本:1.6.0_30

[plain] view plain copy print?[root@lxp2 ~]# cat /etc/redhat-release   CentOS release 5.7 (Final)  [root@lxp2 ~]# uname -a  Linux ku6 2.6.18-274.18.1.el5 #1 SMP Thu Feb 9 12:45:44 EST 2012 x86_64 x86_64 x86_64 GNU/Linux  [root@lxp2 ~]# gcc --version  gcc (GCC) 4.1.2 20080704 (Red Hat 4.1.2-54)  Copyright (C) 2006 Free Software Foundation, Inc.  This is free software; see the source for copying conditions.  There is NO  warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.    [root@lxp2 ~]# g++ --version  g++ (GCC) 4.1.2 20080704 (Red Hat 4.1.2-54)  Copyright (C) 2006 Free Software Foundation, Inc.  This is free software; see the source for copying conditions.  There is NO  warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.    [root@lxp2 ~]# java -version  java version "1.6.0_30"  Java(TM) SE Runtime Environment (build 1.6.0_30-b12)  Java HotSpot(TM) 64-Bit Server VM (build 20.5-b03, mixed mode)  [root@lxp2 ~]#   [root@lxp2 ~]# cat /etc/redhat-release 

CentOS release 5.7 (Final)

[root@lxp2 ~]# uname -a

Linux ku6 2.6.18-274.18.1.el5 #1 SMP Thu Feb 9 12:45:44 EST 2012 x86_64 x86_64 x86_64 GNU/Linux

[root@lxp2 ~]# gcc --version

gcc (GCC) 4.1.2 20080704 (Red Hat 4.1.2-54)

Copyright (C) 2006 Free Software Foundation, Inc.

This is free software; see the source for copying conditions.  There is NO

warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.


[root@lxp2 ~]# g++ --version

g++ (GCC) 4.1.2 20080704 (Red Hat 4.1.2-54)

Copyright (C) 2006 Free Software Foundation, Inc.

This is free software; see the source for copying conditions.  There is NO

warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.


[root@lxp2 ~]# java -version

java version "1.6.0_30"

Java(TM) SE Runtime Environment (build 1.6.0_30-b12)

Java HotSpot(TM) 64-Bit Server VM (build 20.5-b03, mixed mode)

[root@lxp2 ~]# 

gcc、g++和java是必须的,如果运行上述命令提示command not found,则需要安装。具体安装方法这里不做介绍,请参阅相关文档。

 

接下来要准备的是apache服务器、tomcat服务器和JK连接器

1.下载apache服务器源码包

apache服务器官方没有发布编译好的Linux二进制包,只能通过下载源代码,然后自己编译。因此需要先下载源码。

访问网址http://httpd.apache.org/download.cgi,可以看到apache服务器目前放出的版本信息,推荐使用稳定版的release。


然后选择Unix版源码:


2.下载tomcat服务器源码包

目前tomcat服务器个人还是觉得6.0比较稳定。7.0毕竟是新出的东西,需要一定的生产实践考验才能达到理想的状态。因此这里选择tomcat 6.0。

访问网址http://tomcat.apache.org/download-60.cgi,可以看到目前稳定的版本为6.0.33:


这里强烈建议下载tar.gz格式的压缩包。在linux下,文件访问有着严格的权限限制。一个文件是否允许以二进制或者脚本的形式执行,完全取决于其是否拥有执行缺陷,这与Windows识别文件后缀名(.exe、.bat)的方式不同。zip格式的压缩包中是不保留文件的权限信息的,而tar.gz格式的压缩包是保存有文件的权限信息的。

3.下载JK连接器源码包

作为apache与tomcat连接的桥梁,JK连接器使用C语言编写,与apache紧密结合,作为模块装载到apache服务器中,通过配置实现与特定的tomcat服务器进行通信,从而实现负载均衡的功能。

访问网址http://tomcat.apache.org/download-connectors.cgi,可以找到最新最稳定的JK连接器版本:


这里还是推荐下载tar.gz格式的源码。原因同上。

4.解压

apache服务器、tomcat服务器和JK连接器都已经下载好了,如下图所示:


然后将这三个包都解压出来:


5.编译apache服务器

首先编译apache服务器。在编译之前需要执行其自带的检测配置脚本。对于不同发行版本的Linux,默认安装的库都有所差别,即便是同一个发行版本,由于用户安装软件的软件不同,也会导致系统内包含的库有所区别。因此apache作为开源服务器,在编译前需要了解系统的库安装情况,某些模块需要依赖于特定的库,如果这些库不存在,配置脚本将自动忽略这些库的编译。经过检测时候会生成合适的MakeFile文件。这里特别提醒一句,如果直接执行配置脚本,是不会编译额外的模块的,我们希望使用额外模块时,需要在运行配置脚本命令后加入参数,让其尽最大可能编译可用的库。关于这方面的介绍可以参阅我的另外一篇文章“Linux下编译apache服务器modules文件夹缺少模块(.so)的问题”(http://blog.csdn.net/chaijunkun/article/details/6977466)。下面进入apache服务器源码目录并执行配置脚本:

 

[plain] view plain copy print?[root@lxp2 Downloads]# cd httpd-2.2.21  [root@lxp2 httpd-2.2.21]# ./configure --enable-so --enable-mods-shared=most --with-mpm=worker  [root@lxp2 Downloads]# cd httpd-2.2.21

[root@lxp2 httpd-2.2.21]# ./configure --enable-so --enable-mods-shared=most --with-mpm=worker

加入--with-mpm=worker是修改apache服务器的工作模式。默认模式是prefork。prefork采用预派生子进程方式,用单独的子进程来处理 不同的请求,进程之间彼此独立。相对于prefork,worker是全新的支持多线程和多进程混合模型的MPM(多路处理模块)。由于使用线程来处理,所以可以处理相对海量的请求,而系统资源的开销要小于基于进程的服务器。但是,worker也使用了多进程,每个进程又生成多个线程,以获得基于进程服务器的稳定性。

如果配置过程中出现

[plain] view plain copy print?configure: error: Cannot use an external APR with the bundled APR-util  configure: error: Cannot use an external APR with the bundled APR-util

这样的错误信息,说明本机没有安装apr运行库,需要下载并安装。访问网址:http://apr.apache.org/download.cgi,下载apr和apr-util:


解压apr和apr-util

[plain] view plain copy print?[root@lxp2 Downloads]# tar -xf apr-1.4.5.tar.gz   [root@lxp2 Downloads]# tar -xf apr-util-1.3.12.tar.gz   [root@lxp2 Downloads]# tar -xf apr-1.4.5.tar.gz 

[root@lxp2 Downloads]# tar -xf apr-util-1.3.12.tar.gz 

进入apr,并编译

[plain] view plain copy print?[root@lxp2 Downloads]# cd apr-1.4.5  [root@lxp2 apr-1.4.5]# ls  apr-config.in  build.conf        helpers       memory         shmem  apr.dep        build-outputs.mk  include       misc           strings  apr.dsp        CHANGES           libapr.dep    mmap           support  apr.dsw        config.layout     libapr.dsp    network_io     tables  apr.mak        configure         libapr.mak    NOTICE         test  apr.pc.in      configure.in      libapr.rc     NWGNUmakefile  threadproc  apr.spec       docs              LICENSE       passwd         time  atomic         dso               locks         poll           user  build          emacs-mode        Makefile.in   random  buildconf      file_io           Makefile.win  README  [root@lxp2 apr-1.4.5]# ./configure  [root@lxp2 Downloads]# cd apr-1.4.5

[root@lxp2 apr-1.4.5]# ls

apr-config.in  build.conf        helpers       memory         shmem

apr.dep        build-outputs.mk  include       misc           strings

apr.dsp        CHANGES           libapr.dep    mmap           support

apr.dsw        config.layout     libapr.dsp    network_io     tables

apr.mak        configure         libapr.mak    NOTICE         test

apr.pc.in      configure.in      libapr.rc     NWGNUmakefile  threadproc

apr.spec       docs              LICENSE       passwd         time

atomic         dso               locks         poll           user

build          emacs-mode        Makefile.in   random

buildconf      file_io           Makefile.win  README

[root@lxp2 apr-1.4.5]# ./configure

生成了MakeFile后直接编译

[plain] view plain copy print?[root@lxp2 apr-1.4.5]# ls  apr-1-config   buildconf         dso         locks          poll  apr-config.in  build.conf        emacs-mode  Makefile       random  apr.dep        build-outputs.mk  file_io     Makefile.in    README  apr.dsp        CHANGES           helpers     Makefile.win   shmem  apr.dsw        config.layout     include     memory         strings  apr.mak        config.log        libapr.dep  misc           support  apr.pc         config.nice       libapr.dsp  mmap           tables  apr.pc.in      config.status     libapr.mak  network_io     test  apr.spec       configure         libapr.rc   NOTICE         threadproc  atomic         configure.in      libtool     NWGNUmakefile  time  build          docs              LICENSE     passwd         user  [root@lxp2 apr-1.4.5]# make  [root@lxp2 apr-1.4.5]# ls

apr-1-config   buildconf         dso         locks          poll

apr-config.in  build.conf        emacs-mode  Makefile       random

apr.dep        build-outputs.mk  file_io     Makefile.in    README

apr.dsp        CHANGES           helpers     Makefile.win   shmem

apr.dsw        config.layout     include     memory         strings

apr.mak        config.log        libapr.dep  misc           support

apr.pc         config.nice       libapr.dsp  mmap           tables

apr.pc.in      config.status     libapr.mak  network_io     test

apr.spec       configure         libapr.rc   NOTICE         threadproc

atomic         configure.in      libtool     NWGNUmakefile  time

build          docs              LICENSE     passwd         user

[root@lxp2 apr-1.4.5]# make

编译好之后使用root权限安装:

[plain] view plain copy print?[root@lxp2 apr-1.4.5]# sudo make install  [root@lxp2 apr-1.4.5]# sudo make install

然后使用类似的方法配置apr-util:

[plain] view plain copy print?[root@lxp2 Downloads]# cd apr-util-1.3.12  [root@lxp2 apr-util-1.3.12]# ./configure --with-apr=/usr/local/apr  [root@lxp2 Downloads]# cd apr-util-1.3.12

[root@lxp2 apr-util-1.3.12]# ./configure --with-apr=/usr/local/apr编译apr-util:[plain] view plain copy print?[root@lxp2 apr-util-1.3.12]# make  [root@lxp2 apr-util-1.3.12]# make

编译好之后使用root权限安装:

[plain] view plain copy print?[root@lxp2 apr-util-1.3.12]# sudo make install  [root@lxp2 apr-util-1.3.12]# sudo make install

当然如果你在配置apache服务器编译的时候没有提示缺少“APR”,请忽略上面关于APR编译的几步。

回到apache服务器源码所在目录,开始编译:

[plain] view plain copy print?[root@lxp2 httpd-2.2.21]# make  [root@lxp2 httpd-2.2.21]# make

编译过程大概不到十分钟,完成之后使用root权限进行安装

[plain] view plain copy print?[root@lxp2 httpd-2.2.21]# sudo make install  [root@lxp2 httpd-2.2.21]# sudo make install

如果不出意外,至此apache就安装成功了。来测试一下:

进入apache服务器的bin目录,并启动服务器:

[plain] view plain copy print?[root@lxp2 httpd-2.2.21]# cd /usr/local/apache2/bin/  [root@lxp2 bin]# sudo ./apachectl start  httpd: Could not reliably determine the server's fully qualified domain name, using 127.0.0.1 for ServerName  [root@lxp2 httpd-2.2.21]# cd /usr/local/apache2/bin/

[root@lxp2 bin]# sudo ./apachectl start

httpd: Could not reliably determine the server's fully qualified domain name, using 127.0.0.1 for ServerName

在本地打开浏览器,访问http://127.0.0.1

如果出现“It Works!”则表示启动成功了


这里要注意一点就是Linux的防火墙问题。如果你的Linux服务器启动了防火墙,本地访问上面的网址是没有问题的,但如果其它计算机访问你的服务器有可能会连接失败。

出现这种情况的原因是防火墙将入站80端口封锁了。解决方法是将80端口加入到允许列表中:


进入防火墙设置后,如果发现Firewall状态为Enabled,表示防火墙已启用,需要将WWW(HTTP)服务标记为信任,如果需要使用hhtps协议,还要将Secure WWW(HTTPS)服务也标记为信任。如下图所示:


另外,此时如果有其他程序占用80端口也是会影响到apache服务器的,需要确保这个端口没有被占用。

还有我还要补充一点,在Mac OS中按照上述方法安装apache服务器是不行的。开始的时候我不想搭建Linux服务器,想到Mac OS也是类Unix的系统,操作命令什么的都一样,就先在Mac上实验了。结果安装上apache服务器后启动了,每次访问都提示505错误,service temporarily unavailable。经过查阅很多资料和尝试才发现,原来Mac系统中已经自带了apache服务器。具体应用是在“系统设置”中的“共享”功能。这个功能里有“Web共享”方式。其实现时使用的服务器就是apache。它采用的配置文件在/etc/httpd/目录中。这里的配置文件和自己安装的apache服务器配置文件冲突了,因此造成505错误。这一点需要注意。(注:我是用的Mac系统为Mac

 OS X Lion 10.7.2)

2011年11月23日补充:

如果你希望把apache服务器注册为系统服务,让它随着系统启动而启动,则需要在/etc/init.d/目录中建立服务管理脚本,我们将其命名为httpd:

[python] view plain copy print?#!/bin/bash  #chkconfig: 345 61 61  #description: This is apache http service  #processname: httpd  pidfile="/usr/local/apache2/logs/httpd.pid"  httpd_process_name="httpd"  httpd_path="/usr/local/apache2/bin/apachectl"    RETVAL=0  start(){    echo "Starting Apache Httpd Service..."    httpd_pid_list=`pidof $httpd_process_name`    if test -n "$httpd_pid_list"    then      echo "Fail To Launch Httpd, Since It Has Already Started"      RETVAL=1      else      echo "Launching Apache Httpd Server"      `$httpd_path "start"`      RETVAL=$?      echo "Launch Httpd Successfully"    fi;  }    stop(){    echo "Stopping Apache Httpd Service..."    httpd_pid_list=`pidof $httpd_process_name`    if test -n "$httpd_pid_list"    then      echo "Find Httpd Process, Start To End Them"      `$httpd_path "stop"`      if test "$?" = "0"      then        echo "Success to Terminate Httpd Service"        RETVAL=0      else        echo "Can Not Terminate Httpd Service"        RETVAL=1      fi;    else      echo "Can Not Find Any Httpd Process, Fail To Stop Service"      RETVAL=0    fi;  }    restart(){    stop    if test "$?"="0"    then      #sleep 3 seconds to wait for process exit      sleep 3      start      RETVAL= $?    else      RETVAL= $?    fi;  }    status(){    if test -f $pidfile    then      pid_list=`cat $pidfile`      echo "$httpd_process_name (pid:$pid_list) is running"    else      echo "$httpd_process_name is stopped"    fi;  }    case "$1" in    start)       start       RETVAL=$?       ;;    stop)       stop       RETVAL=$?       ;;    restart)       restart       RETVAL=$?       ;;    status)       status       ;;    *)       echo {1}quot;Usage:$0 {start|stop|restart} asdfasdfasdfasdf "       RETVAL=2  esac    exit $RETVAL  #!/bin/bash

#chkconfig: 345 61 61

#description: This is apache http service

#processname: httpd

pidfile="/usr/local/apache2/logs/httpd.pid"

httpd_process_name="httpd"

httpd_path="/usr/local/apache2/bin/apachectl"


RETVAL=0

start(){

  echo "Starting Apache Httpd Service..."

  httpd_pid_list=`pidof $httpd_process_name`

  if test -n "$httpd_pid_list"

  then

    echo "Fail To Launch Httpd, Since It Has Already Started"

    RETVAL=1

    else

    echo "Launching Apache Httpd Server"

    `$httpd_path "start"`

    RETVAL=$?

    echo "Launch Httpd Successfully"

  fi;

}


stop(){

  echo "Stopping Apache Httpd Service..."

  httpd_pid_list=`pidof $httpd_process_name`

  if test -n "$httpd_pid_list"

  then

    echo "Find Httpd Process, Start To End Them"

    `$httpd_path "stop"`

    if test "$?" = "0"

    then

      echo "Success to Terminate Httpd Service"

      RETVAL=0

    else

      echo "Can Not Terminate Httpd Service"

      RETVAL=1

    fi;

  else

    echo "Can Not Find Any Httpd Process, Fail To Stop Service"

    RETVAL=0

  fi;

}


restart(){

  stop

  if test "$?"="0"

  then

    #sleep 3 seconds to wait for process exit

    sleep 3

    start

    RETVAL= $?

  else

    RETVAL= $?

  fi;

}


status(){

  if test -f $pidfile

  then

    pid_list=`cat $pidfile`

    echo "$httpd_process_name (pid:$pid_list) is running"

  else

    echo "$httpd_process_name is stopped"

  fi;

}


case "$1" in

  start)

     start

     RETVAL=$?

     ;;

  stop)

     stop

     RETVAL=$?

     ;;

  restart)

     restart

     RETVAL=$?

     ;;

  status)

     status

     ;;

  *)

     echo {1}quot;Usage:$0 {start|stop|restart} asdfasdfasdfasdf "

     RETVAL=2

esac


exit $RETVAL

编写完成后保存并赋予755权限。然后在该目录下执行

[plain] view plain copy print?[root@lxp2 init.d]# chkconfig --add httpd  [root@lxp2 init.d]# chkconfig --add httpd

将服务添加到系统。脚本的具体解释请参阅我的另外一篇博文:

Linux中将memcached注册为系统服务(地址:http://blog.csdn.net/chaijunkun/article/details/7000600)。

 

6.编译JK连接器

刚刚完成了apache服务器的编译,接下来顺便把JK连接器也编译出来。

进入刚刚解压出来的tomcat-connector目录,再进入native目录。执行配置:

[plain] view plain copy print?[root@lxp2 Downloads]# cd tomcat-connectors-1.2.32-src  [root@lxp2 tomcat-connectors-1.2.32-src]# ls  BUILD.txt  conf  docs  jkstatus  LICENSE  native  NOTICE  support  tools  xdocs  [root@lxp2 tomcat-connectors-1.2.32-src]# cd native/  [root@lxp2 native]# ls  aclocal.m4    BUILDING.txt  configure.in  Makefile.am  nt_service  TODO.txt  apache-1.3    CHANGES       docs          Makefile.in  README.txt  apache-2.0    common        iis           netscape     scripts  buildconf.sh  configure     jni           NEWS         STATUS.txt  [root@lxp2 native]# ./configure --with-apxs=/usr/local/apache2/bin/apxs  [root@lxp2 Downloads]# cd tomcat-connectors-1.2.32-src

[root@lxp2 tomcat-connectors-1.2.32-src]# ls

BUILD.txt  conf  docs  jkstatus  LICENSE  native  NOTICE  support  tools  xdocs

[root@lxp2 tomcat-connectors-1.2.32-src]# cd native/

[root@lxp2 native]# ls

aclocal.m4    BUILDING.txt  configure.in  Makefile.am  nt_service  TODO.txt

apache-1.3    CHANGES       docs          Makefile.in  README.txt

apache-2.0    common        iis           netscape     scripts

buildconf.sh  configure     jni           NEWS         STATUS.txt

[root@lxp2 native]# ./configure --with-apxs=/usr/local/apache2/bin/apxs

这里需要注意的是配置脚本要添加一个apxs完整路径作为参数。apxs是一个为Apache HTTP服务器编译和安装扩展模块的工具,用于编译一个或多个源程序或目标代码文件为动态共享对象,使之可以用由mod_so提供的LoadModule指令在运行时加载到Apache服务器中。

另外,配置脚本运行时会检查g++所在的目录,如果没有安装g++,则会显示:

[html] view plain copy print?configure: error: C++ preprocessor "/lib/cpp" fails sanity check  configure: error: C++ preprocessor "/lib/cpp" fails sanity check

请检查是否已经正确安装了c++编译器。

因为实验用的服务器安装的是X86_64版的Red Hat Enterprise Linux Server ,因此要安装如下的包:

libstdc++-devel-4.1.2-46.el5.x86_64.rpm

gcc-c++-4.1.2-46.el5.x86_64.rpm

如果使用rpm命令无法安装,可以在http://szmov.net/centos5464/CentOS/里查找到相应的资源,下载下来安装也是一样的。

配置无误后就可以编译了,执行make命令:

[plain] view plain copy print?[root@lxp2 native]# make  [root@lxp2 native]# make

7.JK连接器模块的部署

编译完成后使用ls命令来列出native目录下的所有目录和文件。注意有apache-1.3和apache-2.0两个目录。由于在配置编译的时候指定了apxs工具的位置。配置脚本会根据apxs的反馈结果自动识别目标apache服务器为2.x版本,因此本次编译生成的mod_jk.so模块会放在apache-2.0目录中,apache-1.3目录中是没有mod_jk.so的,这一点请注意。如下所示:

[plain] view plain copy print?[root@lxp2 native]# ls  aclocal.m4    CHANGES        configure     libtool      NEWS        TODO.txt  apache-1.3    common         configure.in  Makefile     nt_service  apache-2.0    config.log     docs          Makefile.am  README.txt  buildconf.sh  config.nice    iis           Makefile.in  scripts  BUILDING.txt  config.status  jni           netscape     STATUS.txt  [root@lxp2 native]# cd apache-2.0/  [root@lxp2 apache-2.0]# ls  bldjk54.qclsrc  Makefile.apxs     mod_jk.a    mod_jk.lo  bldjk.qclsrc    Makefile.apxs.in  mod_jk.c    mod_jk.o  config.m4       Makefile.in       mod_jk.dsp  mod_jk.so  Makefile        Makefile.vc       mod_jk.la   NWGNUmakefile  [root@lxp2 apache-2.0]#   [root@lxp2 native]# ls

aclocal.m4    CHANGES        configure     libtool      NEWS        TODO.txt

apache-1.3    common         configure.in  Makefile     nt_service

apache-2.0    config.log     docs          Makefile.am  README.txt

buildconf.sh  config.nice    iis           Makefile.in  scripts

BUILDING.txt  config.status  jni           netscape     STATUS.txt

[root@lxp2 native]# cd apache-2.0/

[root@lxp2 apache-2.0]# ls

bldjk54.qclsrc  Makefile.apxs     mod_jk.a    mod_jk.lo

bldjk.qclsrc    Makefile.apxs.in  mod_jk.c    mod_jk.o

config.m4       Makefile.in       mod_jk.dsp  mod_jk.so

Makefile        Makefile.vc       mod_jk.la   NWGNUmakefile

[root@lxp2 apache-2.0]# 

我们现在将编译好的mod_jk.so拷贝到apache服务器的modules目录中,这个目录是专门用来存放扩展模块的:

[plain] view plain copy print?[root@lxp2 apache-2.0]# sudo cp ./mod_jk.so /usr/local/apache2/modules/  [root@lxp2 apache-2.0]# cd /usr/local/apache2/modules/  [root@lxp2 modules]# ls  httpd.exp               mod_authz_user.so   mod_include.so  mod_actions.so          mod_autoindex.so    mod_info.so  mod_alias.so            mod_cgi.so          mod_jk.so  mod_asis.so             mod_dav_fs.so       mod_log_config.so  mod_auth_basic.so       mod_dav.so          mod_logio.so  mod_auth_digest.so      mod_dbd.so          mod_mime.so  mod_authn_anon.so       mod_deflate.so      mod_negotiation.so  mod_authn_dbd.so        mod_dir.so          mod_reqtimeout.so  mod_authn_dbm.so        mod_dumpio.so       mod_rewrite.so  mod_authn_default.so    mod_env.so          mod_setenvif.so  mod_authn_file.so       mod_expires.so      mod_speling.so  mod_authz_dbm.so        mod_ext_filter.so   mod_status.so  mod_authz_default.so    mod_filter.so       mod_substitute.so  mod_authz_groupfile.so  mod_headers.so      mod_userdir.so  mod_authz_host.so       mod_ident.so        mod_version.so  mod_authz_owner.so      mod_imagemap.so     mod_vhost_alias.so  [root@lxp2 apache-2.0]# sudo cp ./mod_jk.so /usr/local/apache2/modules/

[root@lxp2 apache-2.0]# cd /usr/local/apache2/modules/

[root@lxp2 modules]# ls

httpd.exp               mod_authz_user.so   mod_include.so

mod_actions.so          mod_autoindex.so    mod_info.so

mod_alias.so            mod_cgi.so          mod_jk.so

mod_asis.so             mod_dav_fs.so       mod_log_config.so

mod_auth_basic.so       mod_dav.so          mod_logio.so

mod_auth_digest.so      mod_dbd.so          mod_mime.so

mod_authn_anon.so       mod_deflate.so      mod_negotiation.so

mod_authn_dbd.so        mod_dir.so          mod_reqtimeout.so

mod_authn_dbm.so        mod_dumpio.so       mod_rewrite.so

mod_authn_default.so    mod_env.so          mod_setenvif.so

mod_authn_file.so       mod_expires.so      mod_speling.so

mod_authz_dbm.so        mod_ext_filter.so   mod_status.so

mod_authz_default.so    mod_filter.so       mod_substitute.so

mod_authz_groupfile.so  mod_headers.so      mod_userdir.so

mod_authz_host.so       mod_ident.so        mod_version.so

mod_authz_owner.so      mod_imagemap.so     mod_vhost_alias.so


至此JK连接器模块就部署完成了,但是还需要配置,具体配置将在下文中详细描述。

8.部署tomcat服务器

由于要在本地开启两个tomcat服务器实例以模拟负载均衡+群集的效果,因此我们需要将之前解压出来的tomcat复制成两份,进入解压时的目录,重命名解压出来的原始目录为tomcat_server_1,然后复制此目录,副本目录名称为tomcat_server_2:

[plain] view plain copy print?[root@lxp2 ~]# cd Downloads/  [root@lxp2 Downloads]# ls  apache-tomcat-6.0.33      cpp  apache-tomcat-6.0.33.tar  httpd-2.2.21.tar  apr-1.4.5                 tomcat-connectors-1.2.32-src  apr-1.4.5.tar.gz          tomcat-connectors-1.2.32-src.tar  apr-util-1.3.12  apr-util-1.3.12.tar.gz  [root@lxp2 Downloads]# mv apache-tomcat-6.0.33 tomcat_server_1  [root@lxp2 Downloads]# cp -r tomcat_server_1 tomcat_server_2  [root@lxp2 Downloads]# ls  apache-tomcat-6.0.33.tar  httpd-2.2.21.tar  apr-1.4.5                 tomcat-connectors-1.2.32-src  apr-1.4.5.tar.gz          tomcat-connectors-1.2.32-src.tar  apr-util-1.3.12           tomcat_server_1  apr-util-1.3.12.tar.gz    tomcat_server_2  httpd-2.2.21  [root@lxp2 Downloads]#   [root@lxp2 ~]# cd Downloads/

[root@lxp2 Downloads]# ls

apache-tomcat-6.0.33      cpp

apache-tomcat-6.0.33.tar  httpd-2.2.21.tar

apr-1.4.5                 tomcat-connectors-1.2.32-src

apr-1.4.5.tar.gz          tomcat-connectors-1.2.32-src.tar

apr-util-1.3.12

apr-util-1.3.12.tar.gz

[root@lxp2 Downloads]# mv apache-tomcat-6.0.33 tomcat_server_1

[root@lxp2 Downloads]# cp -r tomcat_server_1 tomcat_server_2

[root@lxp2 Downloads]# ls

apache-tomcat-6.0.33.tar  httpd-2.2.21.tar

apr-1.4.5                 tomcat-connectors-1.2.32-src

apr-1.4.5.tar.gz          tomcat-connectors-1.2.32-src.tar

apr-util-1.3.12           tomcat_server_1

apr-util-1.3.12.tar.gz    tomcat_server_2

httpd-2.2.21

[root@lxp2 Downloads]# 


 现在测试tomcat_server_1是否能够正常工作。

将我实现写好的一个测试用例下载下来(测试用例基于spring 3.0编写,已经打成war包),下载地址:http://download.csdn.net/detail/chaijunkun/3815798。下载得到的文件是TestProject.war。将此压缩包放入tomcat_server_1的webapps目录下。然后切换到tomcat_server_1的bin目录下,启动tomcat_server_1:

[plain] view plain copy print?[root@lxp2 bin]# ./startup.sh   Using CATALINA_BASE:   /root/Downloads/tomcat_server_1  Using CATALINA_HOME:   /root/Downloads/tomcat_server_1  Using CATALINA_TMPDIR: /root/Downloads/tomcat_server_1/temp  Using JRE_HOME:        /usr/java/jdk1.6.0_27  Using CLASSPATH:       /root/Downloads/tomcat_server_1/bin/bootstrap.jar  [root@lxp2 bin]# ./startup.sh 

Using CATALINA_BASE:   /root/Downloads/tomcat_server_1

Using CATALINA_HOME:   /root/Downloads/tomcat_server_1

Using CATALINA_TMPDIR: /root/Downloads/tomcat_server_1/temp

Using JRE_HOME:        /usr/java/jdk1.6.0_27

Using CLASSPATH:       /root/Downloads/tomcat_server_1/bin/bootstrap.jar

然后在浏览器中访问http://127.0.0.1:8080/TestProject/showInfo.do,如果没什么意外会显示类似于下面的信息:

[plain] view plain copy print?This message is from Server, RealPath:  /root/Downloads/tomcat_server_1/webapps/TestProject/  Current Session Id:  471D55C942346EC7BB48D07D9437D57E  This message is from Server, RealPath:

/root/Downloads/tomcat_server_1/webapps/TestProject/

Current Session Id:

471D55C942346EC7BB48D07D9437D57E

信息中显示了当前测试用例所在的路径以及当前会话的SessionId。

此处要注意的地方同测试apache服务器是否正常工作时是一样的,需要注意防火墙是否阻塞了tomcat服务器默认采用的8080端口,是否有其他程序占用此端口。

看到没什么问题,我们先吧tomcat_server_1关闭

[plain] view plain copy print?[root@lxp2 bin]# ./shutdown.sh   Using CATALINA_BASE:   /root/Downloads/tomcat_server_1  Using CATALINA_HOME:   /root/Downloads/tomcat_server_1  Using CATALINA_TMPDIR: /root/Downloads/tomcat_server_1/temp  Using JRE_HOME:        /usr/java/jdk1.6.0_27  Using CLASSPATH:       /root/Downloads/tomcat_server_1/bin/bootstrap.jar  [root@lxp2 bin]# ./shutdown.sh 

Using CATALINA_BASE:   /root/Downloads/tomcat_server_1

Using CATALINA_HOME:   /root/Downloads/tomcat_server_1

Using CATALINA_TMPDIR: /root/Downloads/tomcat_server_1/temp

Using JRE_HOME:        /usr/java/jdk1.6.0_27

Using CLASSPATH:       /root/Downloads/tomcat_server_1/bin/bootstrap.jar

9.apache服务器的配置

apache服务器、tomcat服务器和JK连接器都部署完成并能正确执行后就可以开始配置了

用vi或者其它编辑器打开/usr/local/apache2/conf/httpd.conf文件(由于该文件权限属性为rw-r--r--,因此要想修改此文件需要root权限),这就是apache服务器的主配置文件了。

这里我推荐使用图形化的编辑器来编辑它。因为这个文件很多行,如果用文本模式的编辑器编辑个人感觉很繁琐。

在有很多LoadModule语句的地方,末尾追加一行

LoadModule jk_module modules/mod_jk.so

然后在写有<IfModule XXXX>的区域追加一行如下配置

<IfModule jk_module>

  JkWorkersFile conf/workers.properties

  JkMountFile conf/uriworkermap.properties

  JkLogFile logs/mod_jk.log

  JkLogLevel warn

</IfModule>

下面给出了一个我写的配置。注意配置中有注释的地方。“#”开头的行为注释行。已经去除了原有的配置中的多余注释。

[python] view plain copy print?ServerRoot "/usr/local/apache2"  Listen 80  ServerName 0.0.0.0  ServerAdmin [email protected]  DocumentRoot "/usr/local/apache2/htdocs"    LoadModule authn_file_module modules/mod_authn_file.so    LoadModule authn_dbm_module modules/mod_authn_dbm.so    LoadModule authn_anon_module modules/mod_authn_anon.so    LoadModule authn_dbd_module modules/mod_authn_dbd.so    LoadModule authn_default_module modules/mod_authn_default.so    LoadModule authz_host_module modules/mod_authz_host.so    LoadModule authz_groupfile_module modules/mod_authz_groupfile.so    LoadModule authz_user_module modules/mod_authz_user.so    LoadModule authz_dbm_module modules/mod_authz_dbm.so    LoadModule authz_owner_module modules/mod_authz_owner.so    LoadModule authz_default_module modules/mod_authz_default.so    LoadModule auth_basic_module modules/mod_auth_basic.so    LoadModule auth_digest_module modules/mod_auth_digest.so    LoadModule dbd_module modules/mod_dbd.so    LoadModule dumpio_module modules/mod_dumpio.so    LoadModule reqtimeout_module modules/mod_reqtimeout.so    LoadModule ext_filter_module modules/mod_ext_filter.so    LoadModule include_module modules/mod_include.so    LoadModule filter_module modules/mod_filter.so    LoadModule substitute_module modules/mod_substitute.so    LoadModule deflate_module modules/mod_deflate.so    LoadModule log_config_module modules/mod_log_config.so    LoadModule logio_module modules/mod_logio.so    LoadModule env_module modules/mod_env.so    LoadModule expires_module modules/mod_expires.so    LoadModule headers_module modules/mod_headers.so    LoadModule ident_module modules/mod_ident.so    LoadModule setenvif_module modules/mod_setenvif.so    LoadModule version_module modules/mod_version.so    LoadModule mime_module modules/mod_mime.so    LoadModule dav_module modules/mod_dav.so    LoadModule status_module modules/mod_status.so    LoadModule autoindex_module modules/mod_autoindex.so    LoadModule asis_module modules/mod_asis.so    LoadModule info_module modules/mod_info.so    LoadModule cgid_module modules/mod_cgid.so    LoadModule dav_fs_module modules/mod_dav_fs.so    LoadModule vhost_alias_module modules/mod_vhost_alias.so    LoadModule negotiation_module modules/mod_negotiation.so    LoadModule dir_module modules/mod_dir.so    LoadModule imagemap_module modules/mod_imagemap.so    LoadModule actions_module modules/mod_actions.so    LoadModule speling_module modules/mod_speling.so    LoadModule userdir_module modules/mod_userdir.so    LoadModule alias_module modules/mod_alias.so    LoadModule rewrite_module modules/mod_rewrite.so    # Load JK Connector Module    LoadModule jk_module modules/mod_jk.so  <IfModule !mpm_netware_module>    <IfModule !mpm_winnt_module>      User daemon      Group daemon    </IfModule>  </IfModule>  <IfModule dir_module>    DirectoryIndex index.html  </IfModule>  # Load Configure while Loading JK Connector Module  <IfModule jk_module>    JkWorkersFile conf/workers.properties    JkMountFile conf/uriworkermap.properties    JkLogFile logs/mod_jk.log    JkLogLevel warn  </IfModule>  # Apache Server is working in worker mode  <IfModule worker.c>   StartServers            5   ServerLimit            20   ThreadLimit           200   MaxClients           4000   MinSpareThreads        25   MaxSpareThreads       250   ThreadsPerChild       200   MaxRequestsPerChild  1000  </IfModule>  <FilesMatch "^\.ht">    Order allow,deny    Deny from all    Satisfy All  </FilesMatch>  ErrorLog "logs/error_log"  LogLevel warn  <IfModule log_config_module>    LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined    LogFormat "%h %l %u %t \"%r\" %>s %b" common    <IfModule logio_module>      LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio    </IfModule>    CustomLog "logs/access_log" common  </IfModule>  <IfModule alias_module>    ScriptAlias /cgi-bin/ "/usr/local/apache2/cgi-bin/"  </IfModule>  <IfModule cgid_module>  </IfModule>  DefaultType text/plain  <IfModule mime_module>    TypesConfig "conf/mime.types"    AddType application/x-compress .Z    AddType application/x-gzip .gz .tgz  </IfModule>  <IfModule ssl_module>    SSLRandomSeed startup builtin    SSLRandomSeed connect builtin  </IfModule>  <Directory "/">    Options FollowSymLinks    Deny from all    Order deny,allow    AllowOverride None  </Directory>  <Directory "/usr/local/apache2/htdocs">    Options FollowSymLinks Indexes    Allow from all    Order allow,deny    AllowOverride None  </Directory>  <Directory "/usr/local/apache2/cgi-bin">    Options None    Allow from all    Order allow,deny    AllowOverride None  </Directory>  ServerRoot "/usr/local/apache2"

Listen 80

ServerName 0.0.0.0

ServerAdmin [email protected]

DocumentRoot "/usr/local/apache2/htdocs"

  LoadModule authn_file_module modules/mod_authn_file.so

  LoadModule authn_dbm_module modules/mod_authn_dbm.so

  LoadModule authn_anon_module modules/mod_authn_anon.so

  LoadModule authn_dbd_module modules/mod_authn_dbd.so

  LoadModule authn_default_module modules/mod_authn_default.so

  LoadModule authz_host_module modules/mod_authz_host.so

  LoadModule authz_groupfile_module modules/mod_authz_groupfile.so

  LoadModule authz_user_module modules/mod_authz_user.so

  LoadModule authz_dbm_module modules/mod_authz_dbm.so

  LoadModule authz_owner_module modules/mod_authz_owner.so

  LoadModule authz_default_module modules/mod_authz_default.so

  LoadModule auth_basic_module modules/mod_auth_basic.so

  LoadModule auth_digest_module modules/mod_auth_digest.so

  LoadModule dbd_module modules/mod_dbd.so

  LoadModule dumpio_module modules/mod_dumpio.so

  LoadModule reqtimeout_module modules/mod_reqtimeout.so

  LoadModule ext_filter_module modules/mod_ext_filter.so

  LoadModule include_module modules/mod_include.so

  LoadModule filter_module modules/mod_filter.so

  LoadModule substitute_module modules/mod_substitute.so

  LoadModule deflate_module modules/mod_deflate.so

  LoadModule log_config_module modules/mod_log_config.so

  LoadModule logio_module modules/mod_logio.so

  LoadModule env_module modules/mod_env.so

  LoadModule expires_module modules/mod_expires.so

  LoadModule headers_module modules/mod_headers.so

  LoadModule ident_module modules/mod_ident.so

  LoadModule setenvif_module modules/mod_setenvif.so

  LoadModule version_module modules/mod_version.so

  LoadModule mime_module modules/mod_mime.so

  LoadModule dav_module modules/mod_dav.so

  LoadModule status_module modules/mod_status.so

  LoadModule autoindex_module modules/mod_autoindex.so

  LoadModule asis_module modules/mod_asis.so

  LoadModule info_module modules/mod_info.so

  LoadModule cgid_module modules/mod_cgid.so

  LoadModule dav_fs_module modules/mod_dav_fs.so

  LoadModule vhost_alias_module modules/mod_vhost_alias.so

  LoadModule negotiation_module modules/mod_negotiation.so

  LoadModule dir_module modules/mod_dir.so

  LoadModule imagemap_module modules/mod_imagemap.so

  LoadModule actions_module modules/mod_actions.so

  LoadModule speling_module modules/mod_speling.so

  LoadModule userdir_module modules/mod_userdir.so

  LoadModule alias_module modules/mod_alias.so

  LoadModule rewrite_module modules/mod_rewrite.so

  # Load JK Connector Module

  LoadModule jk_module modules/mod_jk.so

<IfModule !mpm_netware_module>

  <IfModule !mpm_winnt_module>

    User daemon

    Group daemon

  </IfModule>

</IfModule>

<IfModule dir_module>

  DirectoryIndex index.html

</IfModule>

# Load Configure while Loading JK Connector Module

<IfModule jk_module>

  JkWorkersFile conf/workers.properties

  JkMountFile conf/uriworkermap.properties

  JkLogFile logs/mod_jk.log

  JkLogLevel warn

</IfModule>

# Apache Server is working in worker mode

<IfModule worker.c>

 StartServers            5

 ServerLimit            20

 ThreadLimit           200

 MaxClients           4000

 MinSpareThreads        25

 MaxSpareThreads       250

 ThreadsPerChild       200

 MaxRequestsPerChild  1000

</IfModule>

<FilesMatch "^\.ht">

  Order allow,deny

  Deny from all

  Satisfy All

</FilesMatch>

ErrorLog "logs/error_log"

LogLevel warn

<IfModule log_config_module>

  LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined

  LogFormat "%h %l %u %t \"%r\" %>s %b" common

  <IfModule logio_module>

    LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio

  </IfModule>

  CustomLog "logs/access_log" common

</IfModule>

<IfModule alias_module>

  ScriptAlias /cgi-bin/ "/usr/local/apache2/cgi-bin/"

</IfModule>

<IfModule cgid_module>

</IfModule>

DefaultType text/plain

<IfModule mime_module>

  TypesConfig "conf/mime.types"

  AddType application/x-compress .Z

  AddType application/x-gzip .gz .tgz

</IfModule>

<IfModule ssl_module>

  SSLRandomSeed startup builtin

  SSLRandomSeed connect builtin

</IfModule>

<Directory "/">

  Options FollowSymLinks

  Deny from all

  Order deny,allow

  AllowOverride None

</Directory>

<Directory "/usr/local/apache2/htdocs">

  Options FollowSymLinks Indexes

  Allow from all

  Order allow,deny

  AllowOverride None

</Directory>

<Directory "/usr/local/apache2/cgi-bin">

  Options None

  Allow from all

  Order allow,deny

  AllowOverride None

</Directory>

LoadModule表示当apache服务启动时要加载模块 jk_module为模块的别名,后面跟的modules/mod_jk.so就是相对于apache服务器所在目录(/usr/local/apache2/)的模块文件名。

<IfModule jk_module>区域表示当apache服务器加载jk_module(在LoadModule指令中指定的模块别名)模块时所做的配置。

其中:

JkWorkersFile 指定负载均衡服务器的配置文件,文件名为相对于apache服务器所在目录的conf/workers.properties文件

JkMountFile 指定那些请求交由负载均衡服务器来处理,那些由apache服务器来处理,配置文件为相对于apache服务器所在目录的conf/uriworkermap.properties文件

JkLogFile 指定JK连接器的日志输出文件,文件为相对于apache服务器所在目录的logs/mod_jk.log文件

JkLogLevel 指定JK连接器输出日志的级别,级别为warn以上的日志将被输出到日志文件中,可选的值级别由低到高分别为:TRACE DEBUG INFO WARN ERROR FATAL

------------------------------------------------------------------------------------------------------------------------------------------------

<IfModule worker.c>区域表示当apache服务器以worker模式工作时使用的配置。

指令说明:

StartServers:设置服务器启动时建立的子进程数量。因为子进程数量动态的取决于负载的轻重,所有一般没有必要调整这个参数。

ServerLimit:服务器允许配置的进程数上限。只有在你需要将MaxClients和ThreadsPerChild设置成需要超过默认值16个子进程的时候才需要使用这个指令。不要将该指令的值设置的比MaxClients 和ThreadsPerChild需要的子进程数量高。修改此指令的值必须完全停止服务后再启动才能生效,以restart方式重启动将不会生效。

ThreadLimit:设置每个子进程可配置的线程数ThreadsPerChild上限,该指令的值应当和ThreadsPerChild可能达到的最大值保持一致。修改此指令的值必须完全停止服务后再启动才能生效,以restart方式重启动将不会生效。

MaxClients:用于伺服客户端请求的最大接入请求数量(最大线程数)。任何超过MaxClients限制的请求都将进入等候队列。默认值是"400",16 (ServerLimit)乘以25(ThreadsPerChild)的结果。因此要增加MaxClients的时候,你必须同时增加 ServerLimit的值。笔者建议将初始值设为(以Mb为单位的最大物理内存/2),然后根据负载情况进行动态调整。比如一台4G内存的机器,那么初始值就是4000/2=2000。

MinSpareThreads:最小空闲线程数,默认值是"75"。这个MPM将基于整个服务器监视空闲线程数。如果服务器中总的空闲线程数太少,子进程将产生新的空闲线程。

MaxSpareThreads:设置最大空闲线程数。默认值是"250"。这个MPM将基于整个服务器监视空闲线程数。如果服务器中总的空闲线程数太多,子进程将杀死多余的空闲线程。MaxSpareThreads的取值范围是有限制的。Apache将按照如下限制自动修正你设置的值:worker要求其大于等于 MinSpareThreads加上ThreadsPerChild的和。

ThreadsPerChild:每个子进程建立的线程数。默认值是25。子进程在启动时建立这些线程后就不再建立新的线程了。每个子进程所拥有的所有线程的总数要足够大,以便可以处理可能的请求高峰。

MaxRequestsPerChild:设置每个子进程在其生存期内允许伺服的最大请求数量。到达MaxRequestsPerChild的限制后,子进程将会结束。如果MaxRequestsPerChild为"0",子进程将永远不会结束。将MaxRequestsPerChild设置成非零值有两个好处:可以防止(偶然的)内存泄漏无限进行而耗尽内存;

给进程一个有限寿命,从而有助于当服务器负载减轻的时候减少活动进程的数量。

如果设置为非零值,笔者建议设为10000-30000之间的一个值。

公式:

ThreadLimit >= ThreadsPerChild

MaxClients <= ServerLimit * ThreadsPerChild,并且MaxClients必须是ThreadsPerChild的倍数

MaxSpareThreads >= MinSpareThreads+ThreadsPerChild

------------------------------------------------------------------------------------------------------------------------------------------------

接下来配置上面提到的conf/workers.properties文件和conf/uriworkermap.properties文件:

进入apache服务器的conf目录

[plain] view plain copy print?[root@lxp2 ~]# cd /usr/local/apache2/conf/  [root@lxp2 ~]# cd /usr/local/apache2/conf/

建立workers.properties和uriworkermap.properties文件

下面给出我已经配置好的两个文件

[python] view plain copy print?#  # workers.properties  #    # list the workers by name  worker.list=loadBalanceServers, jk_watcher    # localhost server 1  # ------------------------  worker.s1.port=8109  worker.s1.host=localhost  worker.s1.type=ajp13  worker.s1.lbfactor=10  worker.s1.cachesize=5    # localhost server 2  # ------------------------  worker.s2.port=8209  worker.s2.host=localhost  worker.s2.type=ajp13  worker.s2.lbfactor=10  worker.s2.cachesize=5    worker.loadBalanceServers.type=lb  worker.loadBalanceServers.balanced_workers=s1,s2  worker.loadBalanceServers.sticky_session=false  worker.jk_watcher.type=status  # worker.jk_watcher.read_only=True  worker.jk_watcher.mount=/admin/jk  worker.retries=3  #

# workers.properties

#


# list the workers by name

worker.list=loadBalanceServers, jk_watcher


# localhost server 1

# ------------------------

worker.s1.port=8109

worker.s1.host=localhost

worker.s1.type=ajp13

worker.s1.lbfactor=10

worker.s1.cachesize=5


# localhost server 2

# ------------------------

worker.s2.port=8209

worker.s2.host=localhost

worker.s2.type=ajp13

worker.s2.lbfactor=10

worker.s2.cachesize=5


worker.loadBalanceServers.type=lb

worker.loadBalanceServers.balanced_workers=s1,s2

worker.loadBalanceServers.sticky_session=false

worker.jk_watcher.type=status

# worker.jk_watcher.read_only=True

worker.jk_watcher.mount=/admin/jk

worker.retries=3

worker.list 首先配置了两个worker,一个用于负载均衡,一个用于监视负载均衡状态。别名分别为loadBalanceServers和jk_watcher

然后分别配置位于本机的两个负载均衡服务器

worker.s1.port:第一台负载均衡服务器AJP协议连接器的连接端口,这里配置为8109

worker.s1.host:第一台负载均衡服务器的主机名、域名或者IP地址,这里配置为本机localhost

worker.s1.type:JK模块实现负载均衡采用的是AJP协议1.3版本,因此第一台负载均衡服务器的类型配置为ajp13

worker.s1.lbfactor:第一台负载均衡服务器在整个负载均衡系统中所占的权重,这里配置为10,权重越大,越有可能处理更多的请求,建议给性能好的机器配置更高的权重。

worker.s1.cachesize:apache服务器是多线程的,tomcat能够利用这一优势来维持一定数量的连接作为缓存。根据用户的多少来配置一个合适缓存连接数量有助于提高性能。这里配置为5

2013年7月8日补充:最近配置的这台单击集群出现了问题,在高并发量的情况下经常会报HTTP 503错误,这里我在每个worker上配置了如下参数:


[python] view plain copy print?worker.s1.connection_pool_size=800  worker.s1.connection_pool_minsize=25  worker.s1.connection_pool_timeout=600  worker.s1.connection_pool_size=800

worker.s1.connection_pool_minsize=25

worker.s1.connection_pool_timeout=600同样的配置也为s2增加了一份。这样JK组件和tomcat之间的连接池数量就增加了。另外为了应付大并发量下linux文件句柄不够用的情况,还需要配置ulimit -n


我这里配置的是65535。

s1是第一台负载均衡服务器的别名,这个别名要牢记,因为在接下来的配置中还会用到。

s2作为第二台负载均衡服务器,配置与s1大致相同。区别是AJP协议连接器的连接端口与s1的不同,这是因为要在同一台物理机上部署两个tomcat服务器的缘故。如果是两台物理机,则可以配置相同的端口,那么host属性就应该不一样了。两个tomcat服务器的权重都是10,则两个tomcat服务器将会有相同的处理请求的机会。

worker.loadBalanceServers.type:设置名称为“loadBalanceServers”的worker类型,这里配置为lb,也就是Load Balance负载均衡

worker.loadBalanceServers.balanced_workers:设置名称为“loadBalanceServers”的worker拥有哪些负责负载均衡的服务器实例,这里配置为s1和s2

worker.loadBalanceServers.sticky_session:设置负载均衡是否采用粘性会话。如果该属性设置为true,假设一个请求被s1处理了,下次来源于同一个客户端的请求也将被s1处理。直到s1已经达到最大连接数,JK才会将会话切换到其他服务器上。但是如果恰巧一直负责处理该会话的服务器down掉了,则会话将会丢失,明显的故障现象就是关于session的操作会出现莫名其妙的错误(例如你所运行的应用中用户可能已经登录了,但突然在一次访问后莫名其妙地提示没有登录)。这里配置为false,不启用粘性会话,让服务器都有机会处理请求,提高了系统的稳定性。

worker.jk_watcher.type:设置名称为“jk_watcher”的worker类型,这里配置为status,用于监视各个负载均衡服务器实例的运行状态

# worker.jk_watcher.read_only:设置名称为“jk_watcher”的worker是否为只读。上面已经将这个worker设置为了监控worker,如果设置为只读,就不能对负载均衡服务器参数进行配置了,这里先将这条配置注释掉,默认值为false,表示可以配置参数。

worker.jk_watcher.mount:设置名称为“jk_watcher”的worker(负载均衡服务器实例监视器)的挂载路径,这里配置为/admin/jk。这样就可以通过http://127.0.0.1/admin/jk来访问监视工具了,可以很方便地看到各个负载均衡服务器的工作情况。

worker.retries:这是worker全局的重试次数。在apache服务器启动后,会最多尝试若干次去连接这些负载均衡服务器,若连接不上就认为是down掉了,这里配置为3

下面给出配置,其作用是告诉apache服务器哪些请求由负载均衡服务器处理:

[python] view plain copy print?#  # uriworkermap.properties  #    #define all requests will be submitted to load balance servers  #if the condition is satisfied, the filter will validate the next statement until it's not.  #notice the order of the following statements  /*=loadBalanceServers  /jkstatus=jk_watcher  !/*.gif=loadBalanceServers  !/*.jpg=loadBalanceServers  !/*.tif=loadBalanceServers  !/*.png=loadBalanceServers  #

# uriworkermap.properties

#


#define all requests will be submitted to load balance servers

#if the condition is satisfied, the filter will validate the next statement until it's not.

#notice the order of the following statements

/*=loadBalanceServers

/jkstatus=jk_watcher

!/*.gif=loadBalanceServers

!/*.jpg=loadBalanceServers

!/*.tif=loadBalanceServers

!/*.png=loadBalanceServers

在配置文件中,以“!”开头的条件表示“不要”,“=”表示交给。

因此条件“/*=loadBalanceServers”表示将任何请求交给负载均衡服务器。

条件“!/*.jpg=loadBalanceServers”表示不要将.jpg结尾的请求交给负载均衡服务器

apache服务器接收到一个请求后会按照配置文件中的约束条件一个一个地检查,然后按照最后满足的匹配条件来决定由哪个worker来处理请求。

我的测试用例中需要输入http://127.0.0.1/TestProject/showInfo.do来查看信息。那么接下来就将这个请求作为示例来解释上面配置文件的工作过程:

经过上面的条件筛选,最符合条件的就是“/*=loadBalanceServers”。因此将请求转给了负载均衡服务器。

试想一下,如果在apache主目录下放置了一个名为a.jpg的图片,访问路径为http://127.0.0.1/a.jpg,请求经过该配置的检查,最后满足的条件就是“!/*.jpg=loadBalanceServers”,不要将.jpg结尾的请求交给负载均衡服务器,因此apache服务自己处理了该请求。

.jpg是静态数据,apache由c语言实现,直接针对系统底层进行IO操作,因此静态性能优良。而tomcat作为Servlet容器,擅长的是J2EE相关业务的解析。因此通过这样配置可以实现应用的“动静态分离”,相互取长补短,优化了性能。类似地也可以将.js、.css和.html等等静态文件按照上述格式填写到uriworkermap.properties配置文件中。

10.tomcat服务器的配置

由于在同一台物理机中部署了两个tomcat服务器实例,因此需要对端口相关的设置特别小心。tomcat服务器的主配置文件server.xml位于conf目录内。为了配置简单,我将最原始server.xml配置文件中的所有注释删除,然后配置好了一个模板,该模板是s1((即tomcat_server_1)的配置文件,如下所示:

[html] view plain copy print?<?xml version='1.0' encoding='utf-8'?>  <!--指定tomcat服务器的Telnet登录端口 并设置关闭服务器指令为"SHUTDOWN" 此处需注意服务器安全 -->  <Server port="8005" shutdown="SHUTDOWN">      <Listener className="org.apache.catalina.core.AprLifecycleListener"          SSLEngine="on" />      <Listener className="org.apache.catalina.core.JasperListener" />      <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />      <Listener className="org.apache.catalina.mbeans.ServerLifecycleListener" />      <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />      <GlobalNamingResources>          <Resource name="UserDatabase" auth="Container"              type="org.apache.catalina.UserDatabase" description="User database that can be updated and saved"              factory="org.apache.catalina.users.MemoryUserDatabaseFactory"              pathname="conf/tomcat-users.xml" />      </GlobalNamingResources>      <Service name="Catalina">          <!-- HTTP服务端口 若在同一台机器上运行两个Tomcat实例 需要修改这个端口 具体数值无要求 不重复即可 -->          <Connector port="8081" protocol="HTTP/1.1"              connectionTimeout="20000" redirectPort="8443" />          <!-- 此处的端口要与workers.properties文件中s1负载均衡服务器一致 -->          <Connector port="8109" protocol="AJP/1.3" redirectPort="8443" />          <!-- jvmRoute属性用来负载均衡的转发对象 值一定要与在workers.properties文件中指定的另外一台一样 -->          <!-- 本实验中有两个tomcat服务器实例,因此s1服务器指向的转发对象为s2,s2服务器的这个地方要设置为s1 -->          <!-- 如果有三个tomcat服务器实例做负载均衡,需要设置为s1->s2, s2->s3, s3->s1 -->                 <Engine name="Catalina" defaultHost="localhost" jvmRoute="s2">              <Realm className="org.apache.catalina.realm.UserDatabaseRealm"                  resourceName="UserDatabase" />              <Host name="localhost" appBase="webapps" unpackWARs="true"                  autoDeploy="true" xmlValidation="false" xmlNamespaceAware="false">                  <!-- 开始配置集群 -->                  <!-- 如果仅配置负载均衡,只需要将Cluster标签注释掉 -->                  <!-- 如果使用群集,应用的web.xml要加上distributable标签 -->                  <Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"                      channelSendOptions="8">                      <Manager className="org.apache.catalina.ha.session.DeltaManager"                          expireSessionsOnShutdown="false" notifyListenersOnReplication="true" />                      <Channel className="org.apache.catalina.tribes.group.GroupChannel">                          <!-- 配置群集系统中各个节点之间的伙伴关系 同一个群集系统中的各节点此处配置要相同 -->                          <Membership className="org.apache.catalina.tribes.membership.McastService"                              address="228.0.0.4" port="45564" frequency="500" dropTime="3000" />                          <!-- 在群集中 本服务器用于接收来自其他服务器分发的信息 例如同步会话 -->                          <!-- 若在同一台机器上运行两个Tomcat实例 需要修改这个端口 具体数值无要求 不重复即可 -->                          <Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"                              address="auto" port="4001" autoBind="100" selectorTimeout="5000"                              maxThreads="6" />                          <Sender                              className="org.apache.catalina.tribes.transport.ReplicationTransmitter">                              <Transport                                  className="org.apache.catalina.tribes.transport.nio.PooledParallelSender" />                          </Sender>                          <Interceptor                              className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector" />                          <Interceptor                              className="org.apache.catalina.tribes.group.interceptors.MessageDispatch15Interceptor" />                      </Channel>                      <Valve className="org.apache.catalina.ha.tcp.ReplicationValve"                          filter="" />                      <Valve className="org.apache.catalina.ha.session.JvmRouteBinderValve" />                      <Deployer className="org.apache.catalina.ha.deploy.FarmWarDeployer"                          tempDir="/tmp/war-temp/" deployDir="/tmp/war-deploy/" watchDir="/tmp/war-listen/"                          watchEnabled="false" />                      <ClusterListener                          className="org.apache.catalina.ha.session.JvmRouteSessionIDBinderListener" />                      <ClusterListener                          className="org.apache.catalina.ha.session.ClusterSessionListener" />                  </Cluster>                  <!-- 结束配置群集 -->              </Host>          </Engine>      </Service>  </Server>  <?xml version='1.0' encoding='utf-8'?>

<!--指定tomcat服务器的Telnet登录端口 并设置关闭服务器指令为"SHUTDOWN" 此处需注意服务器安全 -->

<Server port="8005" shutdown="SHUTDOWN">

<Listener className="org.apache.catalina.core.AprLifecycleListener"

SSLEngine="on" />

<Listener className="org.apache.catalina.core.JasperListener" />

<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />

<Listener className="org.apache.catalina.mbeans.ServerLifecycleListener" />

<Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />

<GlobalNamingResources>

<Resource name="UserDatabase" auth="Container"

type="org.apache.catalina.UserDatabase" description="User database that can be updated and saved"

factory="org.apache.catalina.users.MemoryUserDatabaseFactory"

pathname="conf/tomcat-users.xml" />

</GlobalNamingResources>

<Service name="Catalina">

<!-- HTTP服务端口 若在同一台机器上运行两个Tomcat实例 需要修改这个端口 具体数值无要求 不重复即可 -->

<Connector port="8081" protocol="HTTP/1.1"

connectionTimeout="20000" redirectPort="8443" />

<!-- 此处的端口要与workers.properties文件中s1负载均衡服务器一致 -->

<Connector port="8109" protocol="AJP/1.3" redirectPort="8443" />

<!-- jvmRoute属性用来负载均衡的转发对象 值一定要与在workers.properties文件中指定的另外一台一样 -->

<!-- 本实验中有两个tomcat服务器实例,因此s1服务器指向的转发对象为s2,s2服务器的这个地方要设置为s1 -->

<!-- 如果有三个tomcat服务器实例做负载均衡,需要设置为s1->s2, s2->s3, s3->s1 -->

<Engine name="Catalina" defaultHost="localhost" jvmRoute="s2">

<Realm className="org.apache.catalina.realm.UserDatabaseRealm"

resourceName="UserDatabase" />

<Host name="localhost" appBase="webapps" unpackWARs="true"

autoDeploy="true" xmlValidation="false" xmlNamespaceAware="false">

<!-- 开始配置集群 -->

<!-- 如果仅配置负载均衡,只需要将Cluster标签注释掉 -->

<!-- 如果使用群集,应用的web.xml要加上distributable标签 -->

<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"

channelSendOptions="8">

<Manager className="org.apache.catalina.ha.session.DeltaManager"

expireSessionsOnShutdown="false" notifyListenersOnReplication="true" />

<Channel className="org.apache.catalina.tribes.group.GroupChannel">

<!-- 配置群集系统中各个节点之间的伙伴关系 同一个群集系统中的各节点此处配置要相同 -->

<Membership className="org.apache.catalina.tribes.membership.McastService"

address="228.0.0.4" port="45564" frequency="500" dropTime="3000" />

<!-- 在群集中 本服务器用于接收来自其他服务器分发的信息 例如同步会话 -->

<!-- 若在同一台机器上运行两个Tomcat实例 需要修改这个端口 具体数值无要求 不重复即可 -->

<Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"

address="auto" port="4001" autoBind="100" selectorTimeout="5000"

maxThreads="6" />

<Sender

className="org.apache.catalina.tribes.transport.ReplicationTransmitter">

<Transport

className="org.apache.catalina.tribes.transport.nio.PooledParallelSender" />

</Sender>

<Interceptor

className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector" />

<Interceptor

className="org.apache.catalina.tribes.group.interceptors.MessageDispatch15Interceptor" />

</Channel>

<Valve className="org.apache.catalina.ha.tcp.ReplicationValve"

filter="" />

<Valve className="org.apache.catalina.ha.session.JvmRouteBinderValve" />

<Deployer className="org.apache.catalina.ha.deploy.FarmWarDeployer"

tempDir="/tmp/war-temp/" deployDir="/tmp/war-deploy/" watchDir="/tmp/war-listen/"

watchEnabled="false" />

<ClusterListener

className="org.apache.catalina.ha.session.JvmRouteSessionIDBinderListener" />

<ClusterListener

className="org.apache.catalina.ha.session.ClusterSessionListener" />

</Cluster>

<!-- 结束配置群集 -->

</Host>

</Engine>

</Service>

</Server>


 配置好s1服务器后再配置s2服务器。按照上面模板中的注释要求,修改相应的端口就可以了。PS:<Cluster ...></Cluster>节点之间部分不必自己动手敲进去,在tomcat服务器目录的/webapps/docs/cluster-howto.html文件中有这一段文字,拷贝出来贴到server.xml文件中即可。



2013年7月8日补充:

当大并发量存在时,即便Apache的JK组件与tomcat保持足够多数量的连接,也不能有效避免HTTP 503错误,因为在大量的请求被转发到tomcat时,tomcat无法相应那么多请求,于是抛出了503。需要在Connector的地方配置maxThreads参数,将最大线程数调大。如下所示:


[html] view plain copy print?<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" maxThreads="1000" URIEncoding="UTF-8" />   <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" maxThreads="1000" URIEncoding="UTF-8" />


另外,如果业务代码中有频繁的SQL操作,还需要增加连接池的最大连接数,并控制好连接空闲释放连接的时间,因为如果时间太长,连接得不到释放,数据库连接会变得越来越多越来越多,直至连管理都登不上去。因为这个不在本文讨论范围之内,所以调整数据库连接池大小请参阅其他文档。



2012年1月16日补充:

tomcat6中,单机开发时为了保证GET请求参数采用UTF8编码(用于支持中文参数),在server.xml中会进行了如下设置:

<Connector port="8080" maxThreads="150" minSpareThreads="25"

maxSpareThreads="75" enableLookups="false" redirectPort="8443" 

acceptCount="100" debug="99" connectionTimeout="20000" 

disableUploadTimeout="true" URIEncoding="UTF-8" />

但是,当使用apache + tomcat 组成群集与负载均衡系统时,apache会将servlet/jsp请求转发给Tomcat。此时是通过AJP协议来转发的,因此对应的请求实际上是被转发到Tomcat监听的AJP端口上的,所以这里针对8080的设置自然就无效了。正确的方法是进行下面的设置:

<Connector port="8009" enableLookups="false" redirectPort="8443"

debug="0" protocol="AJP/1.3" 

URIEncoding="UTF-8" />

需要在群集环境中修改默认URL编码的朋友们在这里需要注意一下。

 

11.部署测试用例

将我提供的TestProject.war文件复制到两个tomcat服务器实例的webapps目录下,然后分别启动tomcat服务器,TestProject.war会自动部署上:

[plain] view plain copy print?[root@lxp2 Downloads]# cd tomcat_server_1/bin/  [root@lxp2 bin]# ./startup.sh   Using CATALINA_BASE:   /root/Downloads/tomcat_server_1  Using CATALINA_HOME:   /root/Downloads/tomcat_server_1  Using CATALINA_TMPDIR: /root/Downloads/tomcat_server_1/temp  Using JRE_HOME:        /usr/java/jdk1.6.0_27  Using CLASSPATH:       /root/Downloads/tomcat_server_1/bin/bootstrap.jar  [root@lxp2 bin]# cd ../../  [root@lxp2 Downloads]# cd tomcat_server_2/bin/  [root@lxp2 bin]# ./startup.sh   Using CATALINA_BASE:   /root/Downloads/tomcat_server_2  Using CATALINA_HOME:   /root/Downloads/tomcat_server_2  Using CATALINA_TMPDIR: /root/Downloads/tomcat_server_2/temp  Using JRE_HOME:        /usr/java/jdk1.6.0_27  Using CLASSPATH:       /root/Downloads/tomcat_server_2/bin/bootstrap.jar  [root@lxp2 bin]#   [root@lxp2 Downloads]# cd tomcat_server_1/bin/

[root@lxp2 bin]# ./startup.sh 

Using CATALINA_BASE:   /root/Downloads/tomcat_server_1

Using CATALINA_HOME:   /root/Downloads/tomcat_server_1

Using CATALINA_TMPDIR: /root/Downloads/tomcat_server_1/temp

Using JRE_HOME:        /usr/java/jdk1.6.0_27

Using CLASSPATH:       /root/Downloads/tomcat_server_1/bin/bootstrap.jar

[root@lxp2 bin]# cd ../../

[root@lxp2 Downloads]# cd tomcat_server_2/bin/

[root@lxp2 bin]# ./startup.sh 

Using CATALINA_BASE:   /root/Downloads/tomcat_server_2

Using CATALINA_HOME:   /root/Downloads/tomcat_server_2

Using CATALINA_TMPDIR: /root/Downloads/tomcat_server_2/temp

Using JRE_HOME:        /usr/java/jdk1.6.0_27

Using CLASSPATH:       /root/Downloads/tomcat_server_2/bin/bootstrap.jar

[root@lxp2 bin]# 

这里需要注意的是两个tomcat服务实例的配置文件server.xml的访问权限。我这里使用的是root账户,所以不用太关心,但是如果用非root账户,一定要看看当前账户是否有server.xml的读写权限。如果没有,则tomcat服务器将不能成功启动。这种情况下要么修改server.xml的访问权限,要么使用root权限启动tomcat服务器。

[plain] view plain copy print?[root@lxp2 bin]# sudo ./startup.sh  [root@lxp2 bin]# sudo ./startup.sh

 

2011年12月8日补充:最近刚刚发现tomcat的关闭脚本shutdown.sh有问题,经常不能完全回收资源。tomcat是基于Java编写的,当然其运行也就脱离不了java的JVM。当执行完shutdown.sh脚本后,tomcat服务器表面上是关闭了,然而JVM并没有完全退出,还在清理并回收资源,如果这个时候立即使用startup.sh进行启动,很容易导致再启动一个新的JVM实例,如果维护次数增多就会导致系统内存耗尽,我今天就经历了如下的错误:

registered the JDBC driver [com.MySQL.jdbc.Driver] but failed to unregister it when the web application was stopped.

这条错误发现于logs目录下的catalina.日期.log文件中。无论再怎么启动tomcat都启动不了。后来重启了服务器居然可以启动了,后来同事说shutdown.sh脚本有问题。于是我经过实验,果然是这样。因此在这里奉劝读者,如果需要重新启动tomcat服务器,除了先执行shutdown.sh脚本外,还应该运行ps ax | grep java 来看看有没有残留的java进程,如果有,使用kill命令将其杀死,这才是正确的关闭tomcat服务器方法。

tomcat 6.0.25以后引入了内存泄露侦测,对于垃圾回收不能处理的对像,它就会做日志。

在tomcat的server.xml文件中,如下配置就是用来做内存泄露侦测的

<!-- Prevent memory leaks due to use of particular java/javax APIs-->

<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener"/>;

也幸亏有了这个东西才提醒了我正在运行着多个JVM实例。

 

开启了tomcat服务器之后,就可以开启apache服务器了(注意,顺序很重要!一定要先开tomcat)

[plain] view plain copy print?[root@lxp2 ~]# cd /usr/local/apache2/bin/  [root@lxp2 bin]# sudo ./apachectl start  httpd: Could not reliably determine the server's fully qualified domain name, using 127.0.0.1 for ServerName  [root@lxp2 bin]#   [root@lxp2 ~]# cd /usr/local/apache2/bin/

[root@lxp2 bin]# sudo ./apachectl start

httpd: Could not reliably determine the server's fully qualified domain name, using 127.0.0.1 for ServerName

[root@lxp2 bin]# 

这时候如果没问题,在本机浏览器访问http://127.0.0.1会出现如下页面:


然后输入网址访问http://127.0.0.1/TestProject/showInfo.do来看看效果:


然后再来刷新一下看看:


我们看到,访问URL没有改变,但是apache把两次访问的负载平均分配给了两个tomcat服务器实例,并且SessionId是相同的。

当再次刷新页面的时候,请求又再一次交给了s1服务器。这也正好验证了之前workers.properties文件中关于两服务器的权重设置。


至此,apache服务器+tomcat服务器+JK连接器实现负载均衡与群集的操作结束。 



2012年7月2日补充:

有网友留言询问apache的httpd.conf文件是否要严格按照文中我的示例来写,另外还提出了关于session方面的疑问,我在这里一并写上。

首先关于httpd.conf配置文件,我提供的只是一个范本,其中一些用不到的选项是可以去掉的,比方说模块加载部分。如果有些模块根本用不到,那去除了之后会使得apache工作得更高效。具体模块对应的功能还要再参看其他资料。至于session的问题,其实涉及到开发技巧了。既然使用负载均衡和动静态分离,一般都是按照大网站的架构了,因此对于会话状态的管理也就需要做出变化。我建议通过加入memcache来实现。用户登录之后,服务器端生成唯一识别号(建议再用XXTEA算法加密一下)作为用户的Token和memcache里面的key,然后把用户对象作为value存放到memcache中,利用memcache自有的失效机制来实现session功能,再加上memcache本身支持集群,多少session都能管理得过来。凡是用到session的地方就改成凭借用户Token从memcache中取出用户对象(这套东西还是写成Util类比较好)。至于用户Token你怎么得到,随你喜好。可以写在Cookie中,也可以写在页面的每一个链接中(你可以看一下网易163、126的邮箱,登进去看访问地址就明白了),后者这样做的好处是浏览器可以禁用Cookie,对于这样的用户,在每一个URL中嵌入token=XXXXXX这样的参数就不会出现问题。


 

参考文献:

httpd.conf (4) 详解,http://hi.baidu.com/prince_zyb/blog/item/685b0901939db70f1c958307.html

Pro Apache Tomcat 6, Matthew Moodie, Kunal Mittal著,Apress公司出品。

ISBN-13 (pbk): 978-1-59059-785-9

ISBN-10 (pbk): 1-59059-785-0


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章