你是否听说过在一个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那些事儿 ,发现更多精彩文章!了解各种常见问题背后的原理与答案。深入源码,分析细节,内容原创,欢迎关注。