背景
項(xiàng)目中使用了 OpenStack4j github鏈接 來調(diào)用 OpenStack 接口震捣,最近負(fù)責(zé)實(shí)現(xiàn)一個(gè)“自動(dòng)化構(gòu)建上百個(gè)虛擬節(jié)點(diǎn)”功能笋颤,覺得單線程模式下一個(gè)個(gè)創(chuàng)虛擬機(jī)太慢了祸憋,考慮使用多線程的方式創(chuàng)建虛擬機(jī)虐急,創(chuàng)建過程中遇到了如下異常:
# org.openstack4j.api.exceptions.OS4JException:
# Unable to retrieve current session.
# Please verify thread has a current session available.
從字面上比庄,是說我當(dāng)前的線程沒有對(duì)應(yīng)的會(huì)話(session)唉工,那么這個(gè)會(huì)話到底是怎么回事呢研乒?如何用多線程實(shí)現(xiàn)對(duì) OpenStack 的并發(fā)請求呢?下面就來說一說
錯(cuò)誤還原
使用 OpenStack4j 創(chuàng)建虛擬機(jī)的代碼很簡單淋硝,分兩步走雹熬,第一步宽菜,建立一個(gè) client
,第二步竿报,用 client
獲取 compute
服務(wù)并創(chuàng)建虛擬機(jī)铅乡,代碼示范如下:
第一步,建立一個(gè) client
:
OSClientV3 os = OSFactory.builderV3()
.endpoint("http://<fqdn>:5000/v3")
.credentials("admin", "secret", Identifier.byId("user domain id"))
.scopeToProject(Identifier.byId("project id"))
.authenticate());
第二步烈菌,獲取 compute
服務(wù)并創(chuàng)建虛擬機(jī)阵幸,這里的server就是創(chuàng)建的虛擬機(jī):
ServerCreate serverCreate = Builders.server()
.name(name)
.addSecurityGroup(securityGroup)
.flavor(flavorId)
.image(imageId)
.networks(networks)
.keypairName(keypairName)
.build();
Server server = os.compute().servers().boot(serverCreate);
那么用多線程創(chuàng)建虛擬機(jī)的代碼結(jié)構(gòu)就大致如下,當(dāng)然芽世,只是簡單的示范代碼:
OSClientV3 os = OSFactory.builderV3()
.endpoint("http://<fqdn>:5000/v3")
.credentials("admin", "secret", Identifier.byId("user domain id"))
.scopeToProject(Identifier.byId("project id"))
.authenticate());
//用線程池管理線程
ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT);
for(int i = 0; i < n; i++) {
executorService.submit(new createMission(os));
}
executorService.shutdown();
//創(chuàng)建一個(gè)任務(wù)類挚赊,繼承線程
class CreateMission extends Thread {
OSClientV3 os;
public CreateMission(OSClientV3 os) {
this.os = os;
}
@Override
public void run() {
//創(chuàng)建serverCreate對(duì)象,這里省略代碼
Server server = os.compute().servers().boot(serverCreate);
}
}
如果這么寫的話济瓢,就報(bào)錯(cuò)啦荠割,遇到上文所述的 session
異常:
# org.openstack4j.api.exceptions.OS4JException:
# Unable to retrieve current session.
# Please verify thread has a current session available.
為了解決這個(gè)錯(cuò)誤,我們就得知道 session
是什么旺矾,為什么在多線程下會(huì)出錯(cuò)
Session
下面結(jié)合 OpenStack4j 源碼來分析一下這個(gè) session
說簡單也簡單涨共,從 client
的創(chuàng)建說起:
OSClientV3 os = OSFactory.builderV3()
.endpoint("http://<fqdn>:5000/v3")
.credentials("admin", "secret", Identifier.byId("user domain id"))
.scopeToProject(Identifier.byId("project id"))
.authenticate());
如上所示,client
是用 OSFactory
的 builderV3
實(shí)現(xiàn)的宠漩,返回了一個(gè)ClientV3
對(duì)象:
package org.openstack4j.openstack;
public abstract class OSFactory<T extends OSFactory<T>> {
……
public static V3 builderV3() {
return new ClientV3();
}
}
在創(chuàng)建 client
的一系列調(diào)用中举反, clientV3
對(duì)象最后調(diào)用了一個(gè)方法 authenticate()
:
package org.openstack4j.openstack.client;
public abstract class OSClientBuilder {
……
public static class ClientV3 extends OSClientBuilder {
……
public OSClientV3 authenticate() throws AuthenticationException {
if (this.tokenId != null && this.tokenId.length() > 0) {
return this.scope != null ? (OSClientV3)OSAuthenticator.invoke(new KeystoneAuth(this.tokenId, this.scope), this.endpoint, this.perspective, this.config, this.provider) : (OSClientV3)OSAuthenticator.invoke(new KeystoneAuth(this.tokenId), this.endpoint, this.perspective, this.config, this.provider);
} else {
return this.user != null && this.user.length() > 0 ? (OSClientV3)OSAuthenticator.invoke(new KeystoneAuth(this.user, this.password, this.domain, this.scope), this.endpoint, this.perspective, this.config, this.provider) : (OSClientV3)OSAuthenticator.invoke(new KeystoneAuth(this.scope, Type.TOKENLESS), this.endpoint, this.perspective, this.config, this.provider);
}
}
}
}
要注意 return
語句里調(diào)用了 OSAuthenticator.invoke
方法,這里面的實(shí)現(xiàn)如下:
package org.openstack4j.openstack.internal;
public class OSAuthenticator {
……
public static OSClient invoke(KeystoneAuth auth, String endpoint, Facing perspective, Config config,
CloudProvider provider) {
SessionInfo info = new SessionInfo(endpoint, perspective, false, provider);
return authenticateV3(auth, info, config);
}
private static OSClientV3 authenticateV3(KeystoneAuth auth, SessionInfo info, Config config) {
……
if (!info.reLinkToExistingSession) {
OSClientSessionV3 v3 = OSClientSessionV3.createSession(token, info.perspective, info.provider, config);
v3.reqId = reqId;
return v3;
}
OSClientSessionV3 current = (OSClientSessionV3) OSClientSessionV3.getCurrent();
current.token = token;
current.reqId = reqId;
return current;
}
}
注意看最后扒吁,這個(gè)方法返回了一個(gè) OSClientSessionV3
的對(duì)象火鼻!所以說我們用的 client
,其實(shí)就可以視為一個(gè) session
以上介紹了那么多看似繁瑣的調(diào)用雕崩,就是為了讓我們形成這樣一個(gè)理解:
在 OpenStack4j
里魁索,創(chuàng)建的 client
可以被理解成創(chuàng)建了一次會(huì)話(session),以后的調(diào)用盼铁,都是基于此次會(huì)話的
最后看一下session類:
/**
* A client which has been identified. Any calls spawned from this session will
* automatically utilize the original authentication that was successfully
* validated and authorized
*
* @author Jeremy Unruh
*/
public abstract class OSClientSession {
private static final ThreadLocal<OSClientSession> sessions = new ThreadLocal<OSClientSession>();
……
public static class OSClientSessionV3 extends OSClientSession<OSClientSessionV3, OSClientV3> {
……
}
}
注意看一開始定義的 ThreadLocal<OSClientSession> sessions
謎底揭曉了粗蔚,OSClientSession
用 ThreadLocal<OSClientSession>
為每個(gè)線程維護(hù)了一個(gè) session
(也就是client)
每次使用client調(diào)用某服務(wù)的時(shí)候,最后會(huì)調(diào)用到這里:
package org.openstack4j.openstack.internal;
public class BaseOpenStackService {
……
@SuppressWarnings("rawtypes")
private <R> Invocation<R> builder(Class<R> returnType, String path, HttpMethod method) {
OSClientSession ses = OSClientSession.getCurrent();
if (ses == null) {
throw new OS4JException(
"Unable to retrieve current session. Please verify thread has a current session available.");
}
RequestBuilder<R> req = HttpRequest.builder(returnType).endpointTokenProvider(ses).config(ses.getConfig())
.method(method).path(path);
Map headers = ses.getHeaders();
if (headers != null && headers.size() > 0){
return new Invocation<R>(req, serviceType, endpointFunc).headers(headers);
}else{
return new Invocation<R>(req, serviceType, endpointFunc);
}
}
}
上圖里饶火,著重看這段代碼:
OSClientSession ses = OSClientSession.getCurrent();
if (ses == null) {
throw new OS4JException(
"Unable to retrieve current session. Please verify thread has a current session available.");
}
回顧一下之前多線程創(chuàng)建虛擬機(jī)的代碼鹏控,我們在主線程里創(chuàng)建了 client
(也可理解為會(huì)話),在另外的線程里試圖用這個(gè)會(huì)話調(diào)用 OpenStack
的服務(wù)肤寝,但這個(gè)會(huì)話是用 ThreadLocal
維護(hù)的当辐,只屬于主線程,另外的線程當(dāng)然取不到這個(gè)會(huì)話鲤看,于是就報(bào)錯(cuò)啦
于是要使用多線程并發(fā)調(diào)用 OpenStack 的話缘揪,需要在每個(gè)線程里都創(chuàng)建一個(gè)會(huì)話,再進(jìn)行調(diào)用,代碼框架大概如下:
//用線程池管理線程
ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT);
for(int i = 0; i < n; i++) {
executorService.submit(new createMission());
}
executorService.shutdown();
//創(chuàng)建一個(gè)任務(wù)類找筝,繼承線程
class CreateMission extends Thread {
@Override
public void run() {
OSClientV3 os = OSFactory.builderV3()
.endpoint("http://<fqdn>:5000/v3")
.credentials("admin", "secret", Identifier.byId("user domain id"))
.scopeToProject(Identifier.byId("project id"))
.authenticate());
//創(chuàng)建serverCreate對(duì)象蹈垢,這里省略代碼
Server server = os.compute().servers().boot(serverCreate);
}
}
但是!讓我們假設(shè)一個(gè)場景袖裕,如果我要?jiǎng)?chuàng)建500個(gè)虛擬機(jī)耘婚,我用一個(gè)線程池,維護(hù)了若干個(gè)線程陆赋。每次,一個(gè)線程執(zhí)行一次創(chuàng)建虛擬機(jī)的任務(wù)嚷闭,就要?jiǎng)?chuàng)建一次會(huì)話攒岛,那么創(chuàng)建500個(gè)虛擬機(jī),就需要?jiǎng)?chuàng)建500個(gè)會(huì)話胞锰!
這樣對(duì)OpenStack服務(wù)器的壓力是不是太大了灾锯?如果要?jiǎng)?chuàng)建1000個(gè),1萬個(gè)虛擬機(jī)呢嗅榕?
既然我們用了線程池顺饮,復(fù)用了線程,我們是不是應(yīng)該想辦法把會(huì)話也復(fù)用一下凌那?
這時(shí)候兼雄,就想到了利用 ThreadLocal
,至于怎么用帽蝶,下一篇再說了赦肋。