`
listen1984
  • 浏览: 1109 次
  • 性别: Icon_minigender_1
  • 来自: 大连
最近访客 更多访客>>
文章分类
社区版块
存档分类
最新评论

mod_jk2引起Apache+Tomcat重复提交问题

阅读更多

以前项目中遇到的一个很诡异的问题,记录下来分享一下。

 

一个很老的项目的生产环境是采用Apache httpd + Tomcat ,使用mod_jk2的插件进行整合,其实这个插件早已停止更新了,反而是mod_jk(1.x)插件的生命周期还在维持。

 

具体问题的现象是,项目中有一些耗时较多的处理页面,例如一个创建新项目的业务画面,前台页面submit之后,后台要处理一系列的文件,还要登录数据库等,其实在设计阶段已经考虑到了重复提交的问题,所以画面上做了控制(提交之后画面按钮禁用,直到后台操作完成)。开发测试阶段没有出现过问题,然而到了客户的生产环境,在运行了一段时间之后,出现了几次数据重复提交的问题,客户提交给我们解决。

 

搭设测试环境后,尝试后发现提交按钮按下后,如果关闭浏览器或者通过地址栏输入其他URL并跳转后就会发生服务器端重复提交。经过反复的调查,首先确定了浏览器端没有任何问题,提交的请求只有一次;但是Tomcat中部署的服务程序确实在个别情况下接收到两次请求,与程序代码也没有关系;最后把注意力集中到了Apache的httpd上面。

 

先是怀疑httpd存在问题,但是google之后没有发现有类似的反馈,考虑到如果真的有问题应该会有很多用户发现的,所以最后怀疑到了mod_jk2上面,更换成了mod_jk(1.x)插件之后就没有问题了。

 

照理说事情到此就可以解决了,但是不幸的是项目的客户非常较真,认为更换mod_jk插件的话整个系统就需要重新测试才能上线,所以不同意,要求调查清楚原先为什么有问题,被逼无奈只好去查mod_jk2的源代码。

mod_jk2的代码可以从这里下载:http://archive.apache.org/dist/tomcat/tomcat-connectors/jk2/source/jakarta-tomcat-connectors-jk2-2.0.4-src.zip

 

因为对C不是很熟悉,所以从httpd的log入手,在出现重复提交问题的时候,log中能查到一条对应的记录:“ajp13.service() ajpGetReply recoverable error 3”,在代码中搜索可以找到是jk_worker_ajp13.c的line547输出的,分析其所在的函数jk2_worker_ajp13_forwardStream之后,发现问题的大致原理是:服务端处理完请求之后,发回响应消息,但是因为浏览器端已经被关闭或者迁移到其他页面,所以试图发回响应时会得到失败的消息,但是mod_jk2插件会试图恢复这一错误(但是这种情况显然无法恢复了),试图恢复的方式是再次向Tomcat发送一开始的请求,这就构成了第二次提交,而在浏览器和Tomcat服务器来看确实是没有任何问题的。(这个仅仅是个人的理解,因为对这方面不是很了解,所以可能理解的不对,不过对于解决问题影响不大)

 

出问题的函数代码片段如下,可以看到一开始定义了JK_RETRIES变量值为2,如果把这个值改为1就不会出现上面的问题了,但是显然客户是不会接受这一方案的,于是继续调查其他解决方法。

 

/** There is no point of trying multiple times - each channel may
    have built-in recovery mechanisms
*/
#define JK_RETRIES 2


static int JK_METHOD
jk2_worker_ajp13_forwardStream(jk_env_t *env, jk_worker_t *worker,
                               jk_ws_service_t *s, jk_endpoint_t *e)
{
    int err = JK_OK;
    int attempt;
    int has_post_body = JK_FALSE;

    e->recoverable = JK_TRUE;
    s->is_recoverable_error = JK_TRUE;

    /*
     * Try to send the request on a valid endpoint. If one endpoint
     * fails, close the channel and try again ( maybe tomcat was restarted )
     * 
     * XXX JK_RETRIES could be replaced by the number of workers in
     * a load-balancing configuration 
     */
    for (attempt = 0; attempt < JK_RETRIES; attempt++) {

        if (e->sd == -1) {
            err = jk2_worker_ajp13_connect(env, e);
            if (err != JK_OK) {
                env->l->jkLog(env, env->l, JK_LOG_ERROR,
                              "ajp13.service() failed to connect endpoint errno=%d %s\n",
                              errno, strerror(errno));
                e->worker->in_error_state = JK_TRUE;
                return err;
            }
            if (worker->mbean->debug > 0)
                env->l->jkLog(env, env->l, JK_LOG_DEBUG,
                              "ajp13.service() connecting to endpoint \n");
        }

        err = e->worker->channel->send(env, e->worker->channel, e,
                                       e->request);

        if (e->worker->mbean->debug > 10)
            e->request->dump(env, e->request, "Sent");

        if (err != JK_OK) {
            /* Can't send - bad endpoint, try again */
            env->l->jkLog(env, env->l, JK_LOG_ERROR,
                          "ajp13.service() error sending, reconnect %s %d %d %s\n",
                          e->worker->channelName, err, errno,
                          strerror(errno));
            jk2_close_endpoint(env, e);
            continue;
        }

        /* We should have a channel now, send the post data */

        /* Prepare to send some post data ( ajp13 proto ). We do that after the
           request was sent ( we're receiving data from client, can be slow, no
           need to delay - we can do that in paralel. ( not very sure this is
           very usefull, and it brakes the protocol ) ! */

        /* || s->is_chunked - this can't be done here. The original protocol sends the first
           chunk of post data ( based on Content-Length ), and that's what the java side expects.
           Sending this data for chunked would break other ajp13 serers.

           Note that chunking will continue to work - using the normal read.
         */
        if (has_post_body || s->left_bytes_to_send > 0
            || s->reco_status == RECO_FILLED) {
            /* We never sent any POST data and we check it we have to send at
             * least of block of data (max 8k). These data will be kept in reply
             * for resend if the remote Tomcat is down, a fact we will learn only
             * doing a read (not yet) 
             */

            /* If we have the service recovery buffer FILLED and we're in first attempt */
            /* recopy the recovery buffer in post instead of reading it from client */
            if (s->reco_status == RECO_FILLED && (attempt == 0)) {
                /* Get in post buf the previously saved POST */

                if (s->reco_buf->copy(env, s->reco_buf, e->post) < 0) {
                    s->is_recoverable_error = JK_FALSE;
                    env->l->jkLog(env, env->l, JK_LOG_ERROR,
                                  "ajp13.service() can't use the LB recovery buffer, aborting\n");
                    return JK_ERR;
                }

                env->l->jkLog(env, env->l, JK_LOG_DEBUG,
                              "ajp13.service() using the LB recovery buffer\n");
            }
            else {
                if (attempt == 0)
                    err = jk2_serialize_postHead(env, e->post, s, e);
                else
                    err = JK_OK;        /* We already have the initial body chunk */

                if (e->worker->mbean->debug > 10)
                    e->request->dump(env, e->request, "Post head");

                if (err != JK_OK) {
                    /* the browser stop sending data, no need to recover */
                    /* e->recoverable = JK_FALSE; */
                    s->is_recoverable_error = JK_FALSE;
                    env->l->jkLog(env, env->l, JK_LOG_ERROR,
                                  "ajp13.service() Error receiving initial post %d %d %d\n",
                                  err, errno, attempt);

                    /* BR #27281 : Should we return HTTP 500 since its the user who stop the sending ? */
                    /* may be not, so return another HTTP code -> use PARTIAL CONTENT, 206 instead */
                    s->status = 206;
                    return JK_ERR;
                }

                /* If a recovery buffer exist (LB mode), save here the post buf */
                if (s->reco_status == RECO_INITED) {
                    /* Save the post for recovery if needed */
                    if (e->post->copy(env, e->post, s->reco_buf) < 0) {
                        s->is_recoverable_error = JK_FALSE;
                        env->l->jkLog(env, env->l, JK_LOG_ERROR,
                                      "ajp13.service() can't save the LB recovery buffer, aborting\n");
                        return JK_ERR;
                    }
                    else
                        s->reco_status = RECO_FILLED;
                }
            }

            has_post_body = JK_TRUE;
            err = e->worker->channel->send(env, e->worker->channel, e,
                                           e->post);
            if (err != JK_OK) {
                /* e->recoverable = JK_FALSE; */
                /*                 s->is_recoverable_error = JK_FALSE; */
                env->l->jkLog(env, env->l, JK_LOG_ERROR,
                              "ajp13.service() Error sending initial post %d %d %d\n",
                              err, errno, attempt);
                jk2_close_endpoint(env, e);
                continue;
                /*  return JK_ERR; */
            }
        }

        err =
            e->worker->workerEnv->processCallbacks(env, e->worker->workerEnv,
                                                   e, s);

        /* if we can't get reply, check if no recover flag was set 
         * if is_recoverable_error is cleared, we have started received 
         * upload data and we must consider that operation is no more recoverable
         */
        if (err != JK_OK && !e->recoverable) {
            s->is_recoverable_error = JK_FALSE;
            env->l->jkLog(env, env->l, JK_LOG_ERROR,
                          "ajp13.service() ajpGetReply unrecoverable error %d\n",
                          err);
            /* The connection is compromised, need to close it ! */
            e->worker->in_error_state = 1;
            return JK_ERR;
        }

        if (err != JK_OK) {
            env->l->jkLog(env, env->l, JK_LOG_ERROR,
                          "ajp13.service() ajpGetReply recoverable error %d\n",
                          err);
            jk2_close_endpoint(env, e);
        }

        if (err == JK_OK)
            return err;
    }
    return err;
}

 

 

从代码if (err != JK_OK && !e->recoverable) 可以看出,当出现发送响应失败时,如果e->recoverable是false,则不会继续整个的loop从而推出整个函数,但是从结果来看显然这个值默认情况下不是false,否则就不会出现问题了。具体查找给e->recoverable赋值的过程忘了是怎样的了,如果借助开发工具(例如VS等)好像容易些,写这篇文章的时候手头恰巧没有C的开发工具,所以用文本编辑器花了点儿事件才找到,这里直接给出,节省各位的时间。

 

给这个变量赋值是在jk_workerEnv.c的line550~563,大致的逻辑是如果配置文件中指定了相关处理方式,则recoverable的值是false,否则默认设定为true(曾经对比了mod_jk 1.x的对应代码,默认设定值就是false),看来问题就是出在这里,最可气的是在设定默认为true的代码旁边还有一行作者留的注释“/* Should we do this ? not sure */”,啥意思就不用解释了,费了我这么多力气,真是f**k!

 

case JK_HANDLER_ERROR:
    /* Normal error ( for example writing to the client failed ).
     * The ajp connection is still in a stable state but if we ask in configuration
     * to abort when header has been send to client, mark as unrecoverable.
     */
    if (wEnv->options & JK_OPT_RECO_ABORTIFTCSENDHEADER) {
        req->is_recoverable_error = JK_FALSE;
        env->l->jkLog(env, env->l, JK_LOG_ERROR,
                      "workerEnv.processCallbacks() by configuration, avoid recovery when tomcat has started to send headers to client\n");
    }
    else
        ep->recoverable = JK_TRUE;      /* Should we do this ? not sure */

    return rc;

 

剩下的事儿就简单了,顺蔓摸瓜,决定wEnv->options & JK_OPT_RECO_ABORTIFTCSENDHEADER的值的代码在同一个文件的line98,代码从设定文件中读取了一个名字是“noRecoveryIfHeaderSent”的变量,Google之,这个属性可以设定在workers2.properties中,具体例子如下(这个例子是从网络搜索来的,不是我的项目中实际使用的,仅仅是为了各位参考“noRecoveryIfHeaderSent”的使用方式)。

 

[workerEnv]
logger=logger.apache2
sslEnable=1
timing=1
forwardURICompatUnparsed
noRecoveryIfRequestSent
noRecoveryIfHeaderSent
disabled=0
debug=5
 
[logger.apache2]
level=DEBUG
 
[shm]
file=${serverRoot}/logs/shm.file
size=1048576
disabled=0
debug=5
 
[channel.socket:192.168.13.4:8009]
tomcatId=server2
keepalive=0
timeout=0
disabled=0
debug=5
#---LB---
lb_factor=1
……

 

 

 注意:只需要写上noRecoveryIfHeaderSent就可以了,如不写这个属性,那么就是默认值。

 

以上的都经过了测试,如果有哪位朋友遇到类似的问题,请随意参考~

分享到:
评论

相关推荐

    mod_jk2.so apache+tomcat 连接器

    apache+tomcat的连接器。 整合Tomcat5.0和Apache2.0的连接器、中间件。

    mod_jk2.so 整合Tomcat5.0和Apache2.0

    **整合Apache2.0与Tomcat5.0的步骤** 在Windows 2000或XP环境下,将...以上就是通过`mod_jk2.so`在Windows 2000或XP下整合Apache2.0和Tomcat5.0的详细过程。正确配置后,你可以享受到更高效的Web应用部署和管理。

    如何利用Apache+Tomcat配置JSP开发环境.doc

    【Apache+Tomcat 配置JSP开发环境】 在Java Web开发中,Apache和Tomcat是常见的服务器组合,用于处理动态网页,特别是JSP(JavaServer Pages)的应用。Apache主要作为一个HTTP服务器,而Tomcat是一个Java Servlet...

    Apache+Tomcat5.0实现集群

    - 需要Tomcat Connector(JK2)来连接Apache和Tomcat,这里使用的是jk-1.2.30版本,需要与Apache版本相匹配。 3. **Tomcat Connector的安装**: - 将下载的JK2模块(mod_jk2.so)拷贝到Apache的modules目录下。 ...

    Apache+Tomcat集群和负载均衡的资料

    ### Apache+Tomcat集群与负载均衡配置详解 #### 一、环境说明 为了实现Apache与Tomcat集群的负载均衡,我们需要准备以下环境: 1. **服务器配置**:四台服务器,其中一台安装Apache,三台安装Tomcat。 - Apache...

    Apache+Tomcat集群和负载均衡及Session绑定

    - 安装JK2模块(mod_jk),作为Apache和Tomcat间的通信桥梁 #### 3.2 配置负载均衡器 配置Apache的httpd.conf文件,添加JK2模块配置,包括定义每个Tomcat服务器的worker,设置负载均衡策略(例如轮询、最少连接数...

    jboss_apache_jk2

    本文主要探讨了在Linux环境下如何将Apache 2.0与JBoss 3.2.x进行集成,具体而言是通过JK2(Java Connector)来实现Apache与嵌入式Servlet容器(如Tomcat或Jetty)之间的通信。这种集成方式可以显著提高Web应用的性能...

    Tomcat5027_Apache2048_IMS9_win.zip

    mod_jk2.so 这是保证Apache和Tomcat成功运行的关键一步,将它解压到本地硬盘中。从解压文件夹中将mod_jk2.so拷贝到Apache安装目录的modules目录下(C:\ Apache2\modules)。 用文本编辑工具打开Apache安装...

    tomcat+apache+jk集群和负载均衡

    以下是对标题“tomcat+apache+jk集群和负载均衡”以及描述“tomcat+apache+jk集群和负载均衡”的深入解析,涵盖其原理、配置步骤以及关键参数调整。 ### 1. 理论基础 #### Tomcat Tomcat是Apache软件基金会下的一...

    mod_jk2-2.0.43.dll

    在我们生产的环境中,往往需要Apache做前端服务器,Tomcat做后端服务器。此时我们就需要一个连接器,这个连接器的作用就是把所有Servlet/JSP的请求转给Tomcat来处理。在Apache2.2之前,一般有两个...mod_jk和mod_jk2。

    安装Jdk+tomcat+apache+PHP+mysql(linux).pdf

    4. 配置Apache与Tomcat集成:使用mod_jk2.so作为连接器,需要编译并安装。配置Apache的`workers.properties`和`httpd.conf`文件,指定Tomcat的位置和工作线程。 5. PHP安装:解压`php-4.3.11.tar.gz`,编译并安装。...

    Apache2.4服务器+mod_jk.so

    mod_jk.so是Apache的负载均衡模块,它通过Apache与Tomcat之间的通信协议(Jk或JK2)来实现这种协同工作。 首先,我们需要理解Apache2.4和mod_jk.so的关系。Apache2.4是Apache HTTP服务器的第2.4版本,提供了许多...

    win2K+JDK1.4.1+Apache+2.0.44+Tomcat4.1.18完全解决方案

    ### Win2K+JDK1.4.1+Apache+2.0.44+Tomcat4.1.18 完全解决方案 #### 一、背景与目标 本方案旨在为开发人员提供一个在Windows 2000环境下集成JDK 1.4.1、Apache 2.0.44以及Tomcat 4.1.18的一站式解决方案。通过本...

    整合apache和tomcat构建Web服务器.docx

    继承jk2模块的是mod_jk模块,mod_jk模块支持Apache1.x和2.X系列版本,现在一般都使用mod_jk做Apache和Tomcat的连接器。 在Apache2.2版本以后,又出现了两种连接器可供选择,那就是http-proxy和proxy-ajp模块。...

    Apache+Jboss(Tomcat)集群配置

    7. **负载均衡策略**:在`mod_jk2.conf`中,可以通过设置`JkMount`和`JkUnMount`指令控制哪些请求由Apache处理,哪些由Jboss/Tomcat处理。例如,静态图片可以由Apache直接服务,以减轻应用服务器的负担。 8. **日志...

    Apache和Tomcat的集成

    下面以Red Hat Enterprise Linux Server release 5操作系统为例,详细介绍Apache+Tomcat+JK的安装过程。 1.安装Apache 首先安装Apache,Apache的安装可以通过yum命令来实现,命令如下: ``` yum install httpd `...

Global site tag (gtag.js) - Google Analytics