如何在Tomcat中部署应用的多个版本

你是否听说过在一个Tomcat中部署两个应用,使用相同的请求路径?

你是否了解,对于Tomcat中的应用,可以部署同时部署多个版本?

其实,在Tomcat的Context组件中,包含一项名叫Parallel Deployment的功能,就支持我们上面提到的这几点。

也许,你会问,我为什么要部署两个同名的应用呢?

一些历史文章和关联内容,请关注公众号查看。同时包含一些常见问题的原理与解决方式。

试想下面一种场景:

你的应用已经部署到线上,正在源源不断的接收到用户的请求,你忽然发现有一个功能需要马上修改一下、升级一下,甚至说你的产品发了新版本。此时为了上线新功能、新版本,你采用什么方式实现,又不影响用户使用呢?

不少同学会想到集群部署的应用,可以先把一部分的实例停止,部署后再启动起来再部署另外一部分。

那对于单实例的应用,该怎么办呢?

憋着急,我们有Tomcat的Parallel Deployment特性,可以同时部署应用的多个版本,而且请求的path保持一致。这真是个好消息。对于部署新版本之后到达的请求,默认会使用新版本来处理,对于旧的请求,由于session中已经包含请求数据,所以会继续处理,直到完成。

下图是manager应用是显示的当前虚拟主机中部署的多个版本的应用。


那对于要以多版本部署的应用,应该如何配置呢?

对于应用的版本部署那是相~当~简单,只需要新版本应用的应用名称后加两个#,再加上版本号即可。例如

foo##1.0.war

在部署时,1.0被做为应用的版本号使用。

或者应用以目录形式部署时也以这种格式命名即可。然后采用熟悉的形式,无论 是直接在webapps目录中部署还是通过manager应用远程部署,都可以。

我们来看源码中对于多版本是如何处理的。

关于应用部署的整体流程,参见旧文两篇:

WEB应用是怎么被部署的?

Tomcat多虚拟主机配置及原理

整体流程上前面的文章里已经讲过,应用在HostConfig中进行部署时,我们重点看一下下面几行代码:

context.setName(cn.getName());
context.setPath(cn.getPath());
context.setWebappVersion(cn.getVersion());
context.setDocBase(cn.getBaseName() + ".war" );
host.addChild(context);

在添加到host之前设置的Context对象信息里,包含了WebappVersion。这里的version信息,就是通过应用的名称cn(ContextName对象)获取的。

在MapperListener里的registerContext过程中,也会使用到这里的version信息。这里会创建一个ContextVersion的列表进行维护

ContextVersion newContextVersion = new ContextVersion(version,
                     path, slashCount, context, resources, welcomeResources);
    if (wrappers != null ) {
          addWrappers(newContextVersion, wrappers);
   }
     ContextList contextList = mappedHost.contextList;
  MappedContext mappedContext = exactFind(contextList.contexts, path);
       if (mappedContext == null ) {
      mappedContext = new MappedContext(path, newContextVersion);
      ContextList newContextList = contextList.addContext(
         mappedContext, slashCount);
    if (newContextList != null ) {
      updateContextList(mappedHost, newContextList);
      contextObjectToContextVersionMap.put(context, newContextVersion);
    }
} else {
     ContextVersion[] contextVersions = mappedContext.versions;
  ContextVersion [] newContextVersions = new ContextVersion[contextVersions.length + 1 ];
     if (insertMap(contextVersions, newContextVersions,
        newContextVersion)) {
        mappedContext.versions = newContextVersions;
    contextObjectToContextVersionMap.put(context, newContextVersion);
  } else {
        int pos = find(contextVersions, version);
      if (pos >= 0 && contextVersions[pos].name.equals(version)) {
           contextVersions[pos] = newContextVersion;
          contextObjectToContextVersionMap.put(context, newContextVersion);
         }
         }
     }

Mapper中对于应用多个版本数据组织形式如下:


在请求处理的时候,重点在CoyoteAdapter.postParseRequest方法内。其中关键在于,第一次请求时,version为空,所以根据请求的path去获取对应的应用。
向后请求时,获取当前应用下包含多少个版本信息。如果只有一个,就直接使用其进行请求的处理。

如果有多个版本,此时先默认按照最新版本来。之后再判断当前SessionManager中是否存在SessionId,如果有的话,判断其对应的应用版本是多少,以此做为version,再次执行代码内的逻辑,去获取对应的Context。

while (mapRequired) {
       // This will map the the latest version by default
       connector.getService().getMapper().map(serverName, decodedURI,
               version, request.getMappingData());
// Look for session ID in cookies and SSL session
       parseSessionCookiesId(request);
       parseSessionSslId(request);
 
       sessionID = request.getRequestedSessionId();
// 这里是在一个while循环内,根据条件设置mapRequired,获取真实的Context进行请求处理
 

}

在循环体内,如果判断当前Context对应的sessionManager还持有对应sessionId的信息,处理逻辑是这样的:

Context[] contexts = request.getMappingData().contexts;
           // Single contextVersion means no need to remap
           // No session ID means no possibility of remap
    if (contexts != null && sessionID != null ) {
      // Find the context associated with the session
     for ( int i = (contexts.length); i > 0 ; i--) {
        Context ctxt = contexts[i - 1 ];
        if (ctxt.getManager().findSession(sessionID) != null ) {
         // We found a context. Is it the one that has
        // already been mapped?
          if (!ctxt.equals(request.getMappingData().context)) {
          // Set version so second time through mapping
          // the correct context is found
             version = ctxt.getWebappVersion();
            versionContext = ctxt;
               // Reset mapping
             request.getMappingData().recycle();
             mapRequired = true ;
              // Recycle cookies in case correct context is
             // configured with different settings
              req.getCookies().recycle();
                       }
                       break ;
                   }

从代码里我们看到,在判断sessionId之前,其实已经提取过Context和version的信息,但在此处如果了解到session中还包含数据,就进行recycle操作,继续回来循环顶部,此时version不再为空,而是使用session中提取中原请求对应的version来进行使用。这样才能保证原请求还沿用老版本应用,新请求使用新版本应用。

至于拿到请求的Context之后的处理流程,请参见这篇旧文:

和Tomcat学设计模式 | Facade模式与请求处理

总结一下,应用的多版本部署,实质上和多个不同的应用部署原理一样,应用的多版本的应用docBase和name都也不会重复,唯一相同的是应用的ContextRoot,即应用的请求路径。而这一切靠Mapper在定位Context时进行选择。

常见问题中增加了一些内容,在公众号会话窗口选择常见问题菜单了解。

关注Tomcat那些事儿,发现更多精彩文章!了解各种常见问题背后的原理与答案。深入源码,分析细节,内容原创,欢迎关注。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值