转载

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

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

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

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

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

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

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

试想下面一种场景:

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

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

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

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

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

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

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

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

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中对于应用多个版本数据组织形式如下:

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

在请求处理的时候,重点在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那些事儿 ,发现更多精彩文章!了解各种常见问题背后的原理与答案。深入源码,分析细节,内容原创,欢迎关注。

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

原文  http://mp.weixin.qq.com/s/9QEK5jqKHJ-1Q0UF8SuwEQ
正文到此结束
Loading...