共计 22905 个字符,预计需要花费 58 分钟才能阅读完成。
作者:路路,热爱技术、乐于分享的技术人,目前主要从事数据库相关技术的研究。
前言
对于数据库中间件来说,后端连接模块是重中之重,也是难点。连接出现问题通常很难排查,需要具备网络抓包以及 MySQL 协议的知识才能进行定位。
本文将从源码层面,力求详尽的分析后端连接模块。包括连接模块的设计、连接池的初始化、连接的创建以及获取等内容,希望能够帮助到大家。
将内容划分为:后端连接的方案设计、源码解读,两个方面来进行叙述。
一、后端连接模块方案设计
设计思想
DBLE 源于 Mycat,我们先来看下 Mycat 的连接池设计思想。
Mycat 为了最高效的利用后端的 MySQL 连接,采取了不同于 Cobar 也不同于传统 JDBC 连接池做法。传统的做法是基于 Database 的连接池,即一个 MySQL 服务器上有 5 个 Database。如果数据库最大连接为 1000,则每个 Database 独占 200 个连接。这种模式的最大问题在于,将一个数据库所具备的最大 1000 个连接,隔离成了更新小的连接池,于是可能产生某个应用的连接不够,但其他应用的连接却很空闲,此类资源浪费的情况。而对于分片这种场景,这个缺陷则几乎是致命的。因为每个分片所对应的 Database 的连接数量被限制在了一个很小的范围内,从而导致系统并发能力的大幅降低。而 Mycat 则采用了基于 MySQL 实例的连接池模式,每个 Database 都可以用现有的 1000 个连接中的空闲连接(通过后续的源码解读,大家能够更清楚这里的设计)。
连接池的结构设计
DBLE 的后端连接池基本分为两个部分,一个是在空闲的部分,一个是等待响应的部分。在空闲的部分基本结构符合在 schema.xml 配置文件里面的配置结构,如下为 DBLE 后端连接池的模块结构图(来源于 DBLE 官方文档):
对照后端连接池模块的结构图,我们再来看下 schema.xml 配置文件:
上图中将配置文件中的主要结构与相关的类对应起来了,后续源码阅读也将主要从这几个类展开。继续看下连接池相关类的关系:
从上图可以看出 PhysicalDBPool 类关联了多个 PhysicalDatasource 类,PhysicalDatasource 类中也存在 PhysicalDBPool 类的引用,这与 schema.xml 配置文件中所展示的一样。
连接池模块还涉及到其他的一些类,我们这里一并看下:
关于上图的说明如下:
- PhysicalDatasource 类引用了 ConMap 类来存储后端连接
- ConMap 类中又引用了 ConQueue 类,ConQueue 类中才是真正存储了后端连接(ConQueue 类关联了多个 BackendConnection 类,BackendConnection 类为后端连接的抽象接口)
上述类图关系也可以从代码中看出:
在 DBLE 的连接池里,当前可用的 MySQL 连接是放到一个 HashMap 的数据结构里(对应 ConMap 类中的 items 变量),Key 为当前连接对应的 Database,另外还有二级分类,即按照连接是自动提交还是手动提交模式进行区分(对应 ConQueue 类中的 autoCommitCons 变量和 manCommitCons 变量),这个设计是为了高效的查询匹配可用的连接,具体逻辑如下:
当某个用户会话需要一个自动提交的,到分片 dn1 (对应 db1)的 SQL 连接的时候,连接池首先找是否有 db1 上的可用连接,如果有,看是否有自动提交模式的连接,找到就返回,否则返回 db1 上的手动提交模式的连接,若没有 db1 的可用连接,则随机返回一个其他 db 对应的可用连接,若没有可用连接,并且连接池还没达到上限,则创建一个新连接并返回,这个逻辑过程,我们会发现,用户会话得到的连接可能不是他原先想要的,比如 Database 不对应,或者事务模式不匹配,因此在执行具体的 SQL 之前,还有一个自动同步数据库连接的过程,包括事务隔离级别、事务模式、字符集、Database 等四个指标,同步完成以后,才会执行具体的 SQL 指令,这里就是为什么 Mycat 能够使用基于 MySQL 实例的连接池的原因。
上述逻辑对应的代码在 ConMap#tryTakeCon 中:
public BackendConnection tryTakeCon(final String schema, boolean autoCommit) {final ConQueue queue = items.get(schema == null ? KEY_STRING_FOR_NULL_DATABASE : schema);
BackendConnection con = null;
if (queue != null) {con = tryTakeCon(queue, autoCommit);
}
if (con != null) {return con;} else {for (ConQueue queue2 : items.values()) {if (queue != queue2) {con = tryTakeCon(queue2, autoCommit);
if (con != null) {return con;}
}
}
}
return null;
}
private BackendConnection tryTakeCon(ConQueue queue, boolean autoCommit) {
BackendConnection con;
if (queue != null && ((con = queue.takeIdleCon(autoCommit)) != null)) {return con;} else {return null;}
}
该方法是获取一个可用连接,代码的逻辑中,首先看对应的 Database 上是否有可用连接,如果有就立即返回,否则从其他的 Database 上找一个可用连接返回。
MySQLConnection 类为具体的 MySQL 连接对象,synAndDoExecute 方法则判断获取到的连接是否符合要求,若不符合要求,先同步状态,然后执行具体的 SQL。
通过共享一个 MySQL 上的所有物理连接,并结合连接状态同步的特性,Mycat 的连接池做到了最佳的吞吐量,也在一定程度上提升了整个系统的并发支撑能力。
二、源码解读
了解了 Dble 后端连接池的相关设计思想,下面我们就从源码层面走读整个后端连接池的初始化、连接创建与获取的过程。
数据库连接池的数据结构的创建在启动时候配置加载阶段完成,DbleServer#startup 方法中相关代码如下:
public void startup() throws IOException {this.config = new ServerConfig();
……
}
继续看 ServerConfig 类的构造方法:public ServerConfig() {
// 创建连接池的逻辑在这一行代码
ConfigInitializer confInit = new ConfigInitializer(true, false);
this.system = confInit.getSystem();
this.users = confInit.getUsers();
this.schemas = confInit.getSchemas();
/*
初始化后的 datahost 数据结构存储在 ServerConfig 类的 dataHosts:Map<String, PhysicalDBPool> 变量中,其中 key 值为 schema.xml 中配置的 dataHost 的 name 值
*/
this.dataHosts = confInit.getDataHosts();
/*
初始化后的 datanode 数据结构存储在 ServerConfig 类的 dataNodes:Map<String, PhysicalDBNode> 变量中,其中 key 值为 schema.xml 中配置的 datanode 的 name 值
*/
this.dataNodes = confInit.getDataNodes();
this.erRelations = confInit.getErRelations();
this.dataHostWithoutWR = confInit.isDataHostWithoutWH();
ConfigUtil.setSchemasForPool(dataHosts, dataNodes);
……
}
继续走读 ConfigInitializer 类的构造方法:public ConfigInitializer(boolean loadDataHost, boolean lowerCaseNames) {
//load server.xml
XMLServerLoader serverLoader = new XMLServerLoader(this);
//load rule.xml and schema.xml
SchemaLoader schemaLoader = new XMLSchemaLoader(lowerCaseNames, this);
// 下面这两个方法分别初始化了 dataHosts:Map<String, PhysicalDBPool> 和 dataNodes:Map<String, PhysicalDBNode> 变量
this.dataHosts = initDataHosts(schemaLoader);
this.dataNodes = initDataNodes(schemaLoader);
}
继续查看 ConfigInitializer#initDataHosts 和 ConfigInitializer#initDataNodes 这两个方法:// 这里即根据配置文件生成相应的 Map 结构
private Map<String, PhysicalDBPool> initDataHosts(SchemaLoader schemaLoader) {Map<String, DataHostConfig> nodeConf = schemaLoader.getDataHosts();
//create PhysicalDBPool according to DataHost
Map<String, PhysicalDBPool> nodes = new HashMap<>(nodeConf.size());
for (DataHostConfig conf : nodeConf.values()) {
//create PhysicalDBPool
PhysicalDBPool pool = getPhysicalDBPool(conf);
nodes.put(pool.getHostName(), pool);
}
return nodes;
}
// 这里也是根据配置文件生成相应的 Map 结构
private Map<String, PhysicalDBNode> initDataNodes(SchemaLoader schemaLoader) {Map<String, DataNodeConfig> nodeConf = schemaLoader.getDataNodes();
Map<String, PhysicalDBNode> nodes = new HashMap<>(nodeConf.size());
for (DataNodeConfig conf : nodeConf.values()) {
//PhysicalDBNode 类里存放了 PhysicalDBPool 类的引用
PhysicalDBPool pool = this.dataHosts.get(conf.getDataHost());
if (pool == null) {throw new ConfigException("dataHost not exists" + conf.getDataHost());
}
PhysicalDBNode dataNode = new PhysicalDBNode(conf.getName(), conf.getDatabase(), pool);
nodes.put(dataNode.getName(), dataNode);
}
return nodes;
}
到这里,后端连接相关的数据结构都已经初始化完成了(注意这里只是创建了相应的数据结构,里面还没有存放实际的后端 MySQL 连接),我们看到有 PhysicalDBPool 类和 PhysicalDBNode 类,PhysicalDBNode 是 DBLE 分片(Datanode)的对应,引用一个连接池对象 PhysicalDBPool,PhysicalDBPool 里面引用了真正的连接池对象 PhysicalDatasource,并且按照读节点和写节点分开引用,实现读写分类和节点切换的功能。
这两个类的关系可以看一下类图:
可以看到 PhysicalDBNode 类关联了 PhysicalDBPool 类,即对应每个 datanode 关联一个 datahost 连接池。
后端连接相关的数据结构都已经初始化完成了,但我们并没有看到创建连接放到对应的连接池里的代码逻辑,那创建连接的代码逻辑在哪里呢?
答案还是在 DbleServer#startup 方法中:
public void startup() throws IOException {
……
// 关键代码在这一行(光看方法名真是不知道这个创建后端连接的方法)pullVarAndMeta();
……
来看一下 DbleServer#pullVarAndMeta 方法:
private void pullVarAndMeta() throws IOException {
……
// 直接看关键代码,是下面这个方法进行初始化连接池的操作
initDataHost();
……
继续走读 DbleServer#initDataHost 方法:
private void initDataHost() {
// init datahost
Map<String, PhysicalDBPool> dataHosts = this.getConfig().getDataHosts();
LOGGER.info("Initialize dataHost ...");
// 下面的代码便是根据配置文件里的 datahosts 开始遍历初始化相应的连接池了
for (PhysicalDBPool node : dataHosts.values()) {
// 这里的 index 为配置多个 writehost 的时候,默认初始化哪一个,默认是 0,即第一个配置的 writehost
String index = dnIndexProperties.getProperty(node.getHostName(), "0");
if (!"0".equals(index)) {LOGGER.info("init datahost:" + node.getHostName() + "to use datasource index:" + index);
}
// 初始化连接池的方法
node.init(Integer.parseInt(index));
node.startHeartbeat();}
}
现在跟着代码走到 PhysicalDBPool#init 方法:
public int init(int index) {
if (!checkIndex(index)) {index = 0;}
for (int i = 0; i < writeSources.length; i++) {int j = loop(i + index);
//initSource() 方法中初始化了连接池
if (initSource(j, writeSources[j])) {
//activedIndex 表明了当前是哪个写节点的数据源在生效
activeIndex = j;
initSuccess = true;
LOGGER.info(getMessage(j, "init success"));
return activeIndex;
}
}
initSuccess = false;
LOGGER.warn(hostName + "init failure");
return -1;
}
下面我们就来详细分析下 PhysicalDBPool#initSource 方法:
private boolean initSource(int index, PhysicalDatasource ds) {
if (ds.getConfig().isDisabled()) {LOGGER.info(ds.getConfig().getHostName() + "is disabled, skipped");
return true;
}
// 获取 datasource 的最小连接数,从这里也可以看出来,datahost 里配置的最小连接数范围针对下面配置的写节点、读节点
int initSize = ds.getConfig().getMinCon();
// 最小连接数不能小于对应数据源的数据库 schema 数 +1
if (initSize < this.schemas.length + 1) {
initSize = this.schemas.length + 1;
LOGGER.warn("minCon size is less than (the count of schema +1), so dble will create at least 1 conn for every schema and an empty schema conn");
}
// 最大连接数不能小于最小连接数的判断
if (ds.getConfig().getMaxCon() < initSize) {ds.getConfig().setMaxCon(initSize);
ds.setSize(initSize);
LOGGER.warn("maxCon is less than the initSize of dataHost:" + initSize + "change the maxCon into" + initSize);
}
LOGGER.info("init backend mysql source ,create connections total" + initSize + "for" + ds.getName() +
"index :" + index);
// 存放创建好的连接的数据结构
CopyOnWriteArrayList<BackendConnection> list = new CopyOnWriteArrayList<>();
// 连接池初始化完成后的回调函数,后面会详细分析
GetConnectionHandler getConHandler = new GetConnectionHandler(list, initSize);
boolean hasConnectionInPool = false;
try {
// 这里是初始化 schema 为 null 的一条连接
if (ds.getTotalConCount() <= 0) {ds.initMinConnection(null, true, getConHandler, null);
} else {LOGGER.info("connection with null schema do not create,because testConnection in pool");
getConHandler.initIncrement();
hasConnectionInPool = true;
}
} catch (Exception e) {LOGGER.warn("init connection with schema null error", e);
}
// 根据初始化连接池大小,正式初始化连接池中的连接
for (int i = 0; i < initSize - 1; i++) {
try {
//PhysicalDataSource#initMinConnection 方法为初始化单条后端连接的方法
ds.initMinConnection(this.schemas[i % schemas.length], true, getConHandler, null);
} catch (Exception e) {LOGGER.warn(getMessage(index, "init connection error."), e);
}
}
long timeOut = System.currentTimeMillis() + 60 * 1000;
// 等待所有初始化连接创建完成
while (!getConHandler.finished() && (System.currentTimeMillis() < timeOut)) {
try {Thread.sleep(100);
} catch (InterruptedException e) {
/*
* hardly triggered no error is needed
*/
LOGGER.info("initError", e);
}
}
LOGGER.info("init result :" + getConHandler.getStatusInfo());
return !list.isEmpty() || hasConnectionInPool;}
通过查看代码,我们知道 PhysicalDataSource#initMinConnection 方法为初始化单条后端连接的方法,继续查看该代码:
public void initMinConnection(String schema, boolean autocommit, final ResponseHandler handler,
final Object attachment) throws IOException {
LOGGER.info("create new connection for" +
this.name + "of schema" + schema);
// 先判断是否还需创建新连接,防止创建连接过多
if (this.createNewCount()) {
// 继续调用 createNewConnection 方法
createNewConnection(handler, attachment, schema);
}
}
跟着代码继续查看 PhysicalDataSource#createNewConnection 方法:
private void createNewConnection(final ResponseHandler handler, final Object attachment,
final String schema) throws IOException {
// 这里是异步创建连接
DbleServer.getInstance().getComplexQueryExecutor().execute(new Runnable() {public void run() {
try {
// 继续调用了 createNewConnection 方法,并且这里定义了内部类 DelegateResponseHandler,注意这里的 handler 变量代表的是哪个类还记得吗?提醒一下大家这里是 GetConnectionHandler 类,这么多层调用很容易被搞迷糊了。createNewConnection(new DelegateResponseHandler(handler) {
@Override
public void connectionError(Throwable e, BackendConnection conn) {Map<String, String> labels = AlertUtil.genSingleLabel("data_host", hostConfig.getName() + "-" + config.getHostName());
AlertUtil.alert(AlarmCode.CREATE_CONN_FAIL, Alert.AlertLevel.WARN, "createNewConn Error" + e.getMessage(), "mysql", config.getId(), labels);
ToResolveContainer.CREATE_CONN_FAIL.add(hostConfig.getName() + "-" + config.getHostName());
handler.connectionError(e, conn);
}
@Override
public void connectionAcquired(BackendConnection conn) {takeCon(conn, handler, attachment, schema);
}
}, schema);
} catch (IOException e) {handler.connectionError(e, null);
}
}
});
}
上述方法异步提交了创建连接的任务,我们继续看下创建连接的任务里做了什么,PhysicalDatasource#createNewConnection 方法是个抽象方法:
public abstract void createNewConnection(ResponseHandler handler, String schema) throws IOException;
具体实现看 MySQLDataSource#createNewConnection 方法:
// 这里调用了 MySQLConnectionFactory#make 方法创建连接
public void createNewConnection(ResponseHandler handler, String schema) throws IOException {
// 注意这里的 handler 为 DelegateResponseHandler,即在 PhysicalDataSource#createNewConnection 方法里定义的匿名内部类
factory.make(this, handler, schema);
}
MySQLConnectionFactory#make 我们也来看一下,这个方法涉及到网络方面了,我们都知道 Dble 的连接用了 reactor 设计模式,这里网络 io 层面我们就不再深入了,我们只需要知道连接通过网络建立之后,回调了哪个方法就行:
public MySQLConnection make(MySQLDataSource pool, ResponseHandler handler,
String schema) throws IOException {DBHostConfig dsc = pool.getConfig();
NetworkChannel channel = openSocketChannel(DbleServer.getInstance().isAIO());
MySQLConnection c = new MySQLConnection(channel, pool.isReadNode(), schema == null);
c.setSocketParams(false);
c.setHost(dsc.getIp());
c.setPort(dsc.getPort());
c.setUser(dsc.getUser());
c.setPassword(dsc.getPassword());
c.setSchema(schema);
// 这里是连接建立后的回调类 MySQLConnectionAuthenticator,注意这里的 handler 变量为 PhysicalDataSource#createNewConnection 方法里定义的匿名内部类 DelegateResponseHandler
c.setHandler(new MySQLConnectionAuthenticator(c, handler));
c.setPool(pool);
c.setIdleTimeout(pool.getConfig().getIdleTimeout());
if (channel instanceof AsynchronousSocketChannel) {((AsynchronousSocketChannel) channel).connect(new InetSocketAddress(dsc.getIp(), dsc.getPort()), c,
(CompletionHandler) DbleServer.getInstance().getConnector());
} else {((NIOConnector) DbleServer.getInstance().getConnector()).postConnect(c);
}
return c;
}
我们直接来看通过网络建立连接后的回调逻辑,进入 MySQLConnectionAuthenticator#handle 方法:
public void handle(byte[] data) {
……
// 省略不相关的代码,在后端连接建立成功后,会直接调用 PhysicalDataSource#createNewConnection 方法里定义的匿名内部类的 connectionAcquired 方法
listener.connectionAcquired(source);
……
}
转了好大的一圈,终于回来了,继续看 PhysicalDataSource#createNewConnection 方法:
private void createNewConnection(final ResponseHandler handler, final Object attachment,
final String schema) throws IOException {
// aysn create connection
DbleServer.getInstance().getComplexQueryExecutor().execute(new Runnable() {public void run() {
try {createNewConnection(new DelegateResponseHandler(handler) {
@Override
public void connectionError(Throwable e, BackendConnection conn) {Map<String, String> labels = AlertUtil.genSingleLabel("data_host", hostConfig.getName() + "-" + config.getHostName());
AlertUtil.alert(AlarmCode.CREATE_CONN_FAIL, Alert.AlertLevel.WARN, "createNewConn Error" + e.getMessage(), "mysql", config.getId(), labels);
ToResolveContainer.CREATE_CONN_FAIL.add(hostConfig.getName() + "-" + config.getHostName());
handler.connectionError(e, conn);
}
// 回调的是这里的方法,这里的 handler 还记得吗?别忘了是 GetConnectionHandler 类
@Override
public void connectionAcquired(BackendConnection conn) {takeCon(conn, handler, attachment, schema);
}
}, schema);
} catch (IOException e) {handler.connectionError(e, null);
}
}
});
}
继续看 PhysicalDatasource#takeCon 方法:
private void takeCon(BackendConnection conn,
final ResponseHandler handler, final Object attachment,
String schema) {if (ToResolveContainer.CREATE_CONN_FAIL.contains(this.getHostConfig().getName() + "-" + this.getConfig().getHostName())) {Map<String, String> labels = AlertUtil.genSingleLabel("data_host", this.getHostConfig().getName() + "-" + this.getConfig().getHostName());
AlertUtil.alertResolve(AlarmCode.CREATE_CONN_FAIL, Alert.AlertLevel.WARN, "mysql", this.getConfig().getId(), labels,
ToResolveContainer.CREATE_CONN_FAIL, this.getHostConfig().getName() + "-" + this.getConfig().getHostName());
}
// 这个方法是创建连接所属 schema 的连接队列
takeCon(conn, schema);
conn.setAttachment(attachment);
// 调用了 GetConnectionHandler 类的 connectionAcquired 方法
handler.connectionAcquired(conn);
}
继续让我们看一下 GetConnectionHandler#connectionAcquired 方法(这个变量从传入到现在,终于用上了):
public void connectionAcquired(BackendConnection conn) {
// 将成功创建的连接放入成功连接列表
successCons.add(conn);
finishedCount.addAndGet(1);
LOGGER.info("connected successfully" + conn);
// 重点!这里便是将新建的连接放入连接池的方法
conn.release();}
继续看 MySQLConnection#release 方法:
public void release() {
……
complexQuery = false;
metaDataSynced = true;
attachment = null;
statusSync = null;
modifiedSQLExecuted = false;
isDDL = false;
testing = false;
setResponseHandler(null);
setSession(null);
logResponse.set(false);
// 这里真正将连接放入连接池,该方法为 `PhysicalDatasource#releaseChannel` 方法
pool.releaseChannel(this);
}
让我们来到 PhysicalDatasource#releaseChannel 方法:
public void releaseChannel(BackendConnection c) {
if (LOGGER.isDebugEnabled()) {LOGGER.debug("release channel" + c);
}
// 将连接放入连接池
returnCon(c);
}
继续看 PhysicalDatasource#returnCon 方法:
private void returnCon(BackendConnection c) {
if (c.isClosedOrQuit()) {return;}
c.setAttachment(null);
c.setBorrowed(false);
c.setLastTime(TimeUtil.currentTimeMillis());
String errMsg = null;
boolean ok;
// 根据连接的 schema 获取到 ConQueue 连接队列
ConQueue queue = this.conMap.createAndGetSchemaConQueue(c.getSchema());
// 判断连接是自动提交还是手动提交,将连接放入相应的队列中
if (c.isAutocommit()) {ok = queue.getAutoCommitCons().offer(c);
} else {ok = queue.getManCommitCons().offer(c);
}
if (!ok) {errMsg = "can't return to pool ,so close con " + c;}
if (errMsg != null) {LOGGER.info(errMsg);
c.close(errMsg);
}
}
到这里,一个连接从创建到放入连接池的逻辑都走通了,不得不感慨一句,DBLE 的调用真的是有点多,有点复杂呀!特别是异步回调,很容易把人搞蒙。
一不做二不休,我们再来看看连接获取的逻辑。
我们都知道 DBLE 是先对发送过来的 SQL 进行路由,然后根据路由结果到相应的节点执行 SQL 的。执行 SQL 当然要用连接了,我们下面便通过单节点下如何执行 SQL,来看连接的获取逻辑。
连接获取有两种情况:一是从上述创建的连接池中获取;二是新建连接(当连接池中的连接被用完的时候),下面分别说明。
单节点 SQL 的执行代码在 SingleNodeHandler#execute 方法中:
public void execute() throws Exception {
connClosed = false;
this.packetId = (byte) session.getPacketId().get();
// 这里是先判断会话有没有取过连接,如果有的话就不再取
if (session.getTargetCount() > 0) {BackendConnection conn = session.getTarget(node);
if (conn == null && rrs.isGlobalTable() && rrs.getGlobalBackupNodes() != null) {for (String dataNode : rrs.getGlobalBackupNodes()) {RouteResultsetNode tmpNode = new RouteResultsetNode(dataNode, rrs.getSqlType(), rrs.getStatement());
conn = session.getTarget(tmpNode);
if (conn != null) {break;}
}
}
node.setRunOnSlave(rrs.getRunOnSlave());
if (session.tryExistsCon(conn, node)) {execute(conn);
return;
}
}
// 我们重点看下获取新连接的逻辑
node.setRunOnSlave(rrs.getRunOnSlave());
ServerConfig conf = DbleServer.getInstance().getConfig();
PhysicalDBNode dn = conf.getDataNodes().get(node.getName());
//PhysicalDBNode 有相应 PhysicalDBPool 类的引用,这里调用了 PhysicalDBNode#getConnection 方法获取连接
dn.getConnection(dn.getDatabase(), session.getSource().isTxStart(), session.getSource().isAutocommit(), node, this, node);
}
查看 PhysicalDBNode#getConnection 方法:
public void getConnection(String schema, boolean isMustWrite, boolean autoCommit, RouteResultsetNode rrs,
ResponseHandler handler, Object attachment) throws Exception {
// 判断是否必须走写节点
if (isMustWrite) {
// 获取写节点的连接
getWriteNodeConnection(schema, autoCommit, handler, attachment);
return;
}
if (rrs.getRunOnSlave() == null) {if (rrs.canRunINReadDB(autoCommit)) {dbPool.getRWBalanceCon(schema, autoCommit, handler, attachment);
} else {getWriteNodeConnection(schema, autoCommit, handler, attachment);
}
} else {if (rrs.getRunOnSlave()) {if (!dbPool.getReadCon(schema, autoCommit, handler, attachment)) {LOGGER.info("Do not have slave connection to use, use master connection instead.");
rrs.setRunOnSlave(false);
rrs.setCanRunInReadDB(false);
getWriteNodeConnection(schema, autoCommit, handler, attachment);
}
} else {rrs.setCanRunInReadDB(false);
getWriteNodeConnection(schema, autoCommit, handler, attachment);
}
}
}
我们看下如何获取写节点的连接的,也就是对应配置文件中 writehost 中相应的连接池中的连接,查看 PhysicalDBNode#getWriteNodeConnection 方法:
private void getWriteNodeConnection(String schema, boolean autoCommit, ResponseHandler handler, Object attachment) throws IOException {
checkRequest(schema);
if (dbPool.isInitSuccess()) {PhysicalDatasource writeSource = dbPool.getSource();
if (writeSource.getConfig().isDisabled()) {throw new IllegalArgumentException("[" + writeSource.getHostConfig().getName() + "." + writeSource.getConfig().getHostName() + "] is disabled");
}
writeSource.setWriteCount();
// 调用了 PhysicalDatasource 类的 getConnection 方法获取连接
writeSource.getConnection(schema, autoCommit, handler, attachment);
} else {throw new IllegalArgumentException("Invalid DataSource:" + dbPool.getActiveIndex());
}
}
跟着代码进入 PhysicalDatasource#getConnection 方法:
public void getConnection(String schema, boolean autocommit, final ResponseHandler handler,
final Object attachment) throws IOException {
/*
这里获取连接有两种情况:1. 从上述创建的连接池中获取;2. 新建连接。*/
//1. 从连接池中获取连接
BackendConnection con = this.conMap.tryTakeCon(schema, autocommit);
if (con != null) {takeCon(con, handler, attachment, schema);
} else {if (!this.createNewCount()) {String maxConError = "the max active Connections size can not be max than maxCon for data host[" + this.getHostConfig().getName() + "." + this.getName() + "]";
LOGGER.warn(maxConError);
Map<String, String> labels = AlertUtil.genSingleLabel("data_host", this.getHostConfig().getName() + "-" + this.getConfig().getHostName());
AlertUtil.alert(AlarmCode.REACH_MAX_CON, Alert.AlertLevel.WARN, maxConError, "dble", this.getConfig().getId(), labels);
ToResolveContainer.REACH_MAX_CON.add(this.getHostConfig().getName() + "-" + this.getConfig().getHostName());
throw new IOException(maxConError);
} else { // 2. 新建连接
if (ToResolveContainer.REACH_MAX_CON.contains(this.getHostConfig().getName() + "-" + this.getConfig().getHostName())) {Map<String, String> labels = AlertUtil.genSingleLabel("data_host", this.getHostConfig().getName() + "-" + this.getConfig().getHostName());
AlertUtil.alertResolve(AlarmCode.REACH_MAX_CON, Alert.AlertLevel.WARN, "dble", this.getConfig().getId(), labels,
ToResolveContainer.REACH_MAX_CON, this.getHostConfig().getName() + "-" + this.getConfig().getHostName());
}
LOGGER.info("no idle connection in pool,create new connection for" + this.name + "of schema" + schema);
createNewConnection(handler, attachment, schema);
}
}
}
我们先来看下从连接池中获取连接的方法,ConMap#tryTakeCon 方法,这个方法的代码我们在连接池方案设计小节已经讲过了,这里在看一下吧:
public BackendConnection tryTakeCon(final String schema, boolean autoCommit) {
final ConQueue queue = items.get(schema == null ? KEY_STRING_FOR_NULL_DATABASE : schema);
BackendConnection con = null;
if (queue != null) {con = tryTakeCon(queue, autoCommit);
}
if (con != null) {return con;} else {for (ConQueue queue2 : items.values()) {if (queue != queue2) {con = tryTakeCon(queue2, autoCommit);
if (con != null) {return con;}
}
}
}
return null;
}
private BackendConnection tryTakeCon(ConQueue queue, boolean autoCommit) {
BackendConnection con;
if (queue != null && ((con = queue.takeIdleCon(autoCommit)) != null)) {return con;} else {return null;}
}
tryTakeCon 方法是获取一个可用连接,代码的逻辑中,首先看对应的 Database 上是否有可用连接,如果有就立即返回,否则从其他的 Database 上找一个可用连接返回。
MySQLConnection 类的 synAndDoExecute 方法会判断获取到的连接是否符合要求,若不符合要求,会先同步状态,然后执行具体的 SQL,所以这里可以用其他 database 上的连接。
成功获取连接后,看下后续的执行逻辑,调用了 PhysicalDatasource#takeCon 方法:
private void takeCon(BackendConnection conn,
final ResponseHandler handler, final Object attachment,
String schema) {if (ToResolveContainer.CREATE_CONN_FAIL.contains(this.getHostConfig().getName() + "-" + this.getConfig().getHostName())) {Map<String, String> labels = AlertUtil.genSingleLabel("data_host", this.getHostConfig().getName() + "-" + this.getConfig().getHostName());
AlertUtil.alertResolve(AlarmCode.CREATE_CONN_FAIL, Alert.AlertLevel.WARN, "mysql", this.getConfig().getId(), labels,
ToResolveContainer.CREATE_CONN_FAIL, this.getHostConfig().getName() + "-" + this.getConfig().getHostName());
}
// 这里仅做连接的一些状态记录
takeCon(conn, schema);
conn.setAttachment(attachment);
// 这里的 handler 为 SingleNodeHandler 类(别忘了我们是一路从 SingleNodeHandler 类走过来的)handler.connectionAcquired(conn);
}
继续看下 SingleNodeHandler#connectionAcquired 方法:
public void connectionAcquired(final BackendConnection conn) {
// 将后端连接与会话绑定,后续如果再用连接的话,则可以直接取出
session.bindConnection(node, conn);
// 获取连接后,就是执行具体的 sql 了
execute(conn);
}
从连接池中获取连接的过程如上所述,流程还是比较清晰的。
那么当连接池中没有连接可用了(连接被用完)怎么办呢?我们此时便需要通过创建新的连接来执行 SQL,新建连接的操作与连接池初始化过程中新建连接的流程一样(忘记的话可以回过头再看一遍),唯一的不同是回调类不同,针对本例回调类是 SingleNodeHandler 类:
// 新建连接主要方法
private void createNewConnection(final ResponseHandler handler, final Object attachment,
final String schema) throws IOException {
// aysn create connection
DbleServer.getInstance().getComplexQueryExecutor().execute(new Runnable() {public void run() {
try {createNewConnection(new DelegateResponseHandler(handler) {
@Override
public void connectionError(Throwable e, BackendConnection conn) {Map<String, String> labels = AlertUtil.genSingleLabel("data_host", hostConfig.getName() + "-" + config.getHostName());
AlertUtil.alert(AlarmCode.CREATE_CONN_FAIL, Alert.AlertLevel.WARN, "createNewConn Error" + e.getMessage(), "mysql", config.getId(), labels);
ToResolveContainer.CREATE_CONN_FAIL.add(hostConfig.getName() + "-" + config.getHostName());
handler.connectionError(e, conn);
}
@Override
public void connectionAcquired(BackendConnection conn) {
// 这里的 handler 为 SingleNodeHandler,新建连接成功后,将回调此处代码
takeCon(conn, handler, attachment, schema);
}
}, schema);
} catch (IOException e) {handler.connectionError(e, null);
}
}
});
}
PhysicalDataSource#takecon 方法中,在此处为调用了 SingleNodeHandler#connectionAcquired 方法:
private void takeCon(BackendConnection conn,
final ResponseHandler handler, final Object attachment,
String schema) {
……
// 调用了 SingleNodeHandler#connectionAcquired 方法
handler.connectionAcquired(conn);
}
SingleNodeHandler#connectionAcquired 方法:
// 与从连接池中获取连接的操作一样,先是绑定连接到会话,然后具体执行 SQL
public void connectionAcquired(final BackendConnection conn) {
session.bindConnection(node, conn);
execute(conn);
}
连接的获取流程到这里应该也已经清楚了。
但还有一个疑问,没有从连接池中获取到而新建的连接使用完之后怎么处理呢?我们还是以单节点的处理为例,看下 SingleNodeHandler#okResponse 方法,为什么看这个方法呢?因为 SQL 执行成功后,会返回 OK 包到 DBLE,这里便会回调此方法(回调确实多呢):
public void okResponse(byte[] data, BackendConnection conn) {
……
// 删除掉多余代码,重点看下面这一行代码,这一行代码便是将新建的连接放回连接池的代码了
session.releaseConnectionIfSafe(conn, false);
……
}
}
将新建连接放入连接池的具体代码在 NonBlockingSession#releaseConnectionIfSafe 方法中,其实还是调用的上述初始化连接池过程中将连接放入连接池中的代码,所以这里不再赘述了。
总结
本文先介绍了 DBLE 数据库中间件的后端连接的设计思想、后端连接的数据结构,然后从源码角度详细走读了连接的创建以及获取过程,代码中涉及到很多的异步调用,不小心就会把人搞晕。连接模块对于理解数据库中间件很重要,而且连接出现问题通常都是很难排查的,希望通过本文能帮助大家理解 DBLE 的后端连接模块。后续如果忘记相应的逻辑,还可以回过头翻阅一下。