首先介绍一下什么是高可用。
高可用是分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计减少系统不能提供服务的时间。假设系统一直能够提供服务,我们说系统的可用性是100%。高可用是通过冗余+自动故障转移来实现的。本文数据层的高可用采用DM数据守护实时主备集群来实现。
一、DM数据守护主备集群简介
DM 数据守护 (Data Watch) 是一种集成化的高可用、高性能数据库解决方案,是数据库异地容灾的首选方案。通过部署 DM 数据守护,可以在硬件故障(如磁盘损坏)、自然灾害(地震、火灾)等极端情况下,避免数据损坏、丢失,保障数据安全,并且可以快速恢复数据库服务,满足用户不间断提供数据库服务的要求。数据守护基本不受数据规模的影响,只需数秒时间就可以将备库切换为主库对外提供数据库服务。
实时主备由一个主库以及一个或者多个配置了实时 (Realtime) 归档的备库组成,其主要目的是保障数据库可用性,提高数据安全性。实时主备系统中,主库提供完整的数据库功能,备库提供只读服务。主库修改数据产生的 Redo 日志,通过实时归档机制,在写入联机 Redo 日志文件之前发送到备库,实时备库通过重演 Redo 日志与主库保持数据同步。当主库出现故障时,备库在将所有 Redo 日志重演结束后,就可以切换为主库对外提供数据库服务。
二、配置详解
1.配置DM_svc.conf文件
使用DM数据守护,要求编辑DM_svc.conf文件配置连接服务名,以实现故障自动重连。
#Windows平台下位于%SystemRoot%\system32目录
#Linux平台下位于/etc目录
#编辑DM_svc.conf文件
TIME_ZONE=(+480)
LANGUAGE=(en)
DM_DW=(192.168.10.101:15236,192.168.10.102:15236,192.168.10.103:15236) #服务名,若多服务连接一套环境,配多个服务名,名称唯一
[DM_DW]
LOGIN_MODE=1 #1:只连接主库
SWITCH_TIME=20000 #在服务器之间切换的次数
SWITCH_INTERVAL=200 #在服务器之间切换的时间间隔,单位为毫秒
DB_ALIVE_CHECK_FREQ=10000 #检测数据库是否存活的频率,单位为毫秒。0表示不检测,默认为0。
2.修改TCP参数
/proc/sys/net/ipv4/tcp_retries2 设置为1
在丢弃激活(已建立通讯状况)的TCP连接之前﹐需要进行多少次重试。默认值为15,根据RTO的值来决定,相当于13-30分钟(RFC1122规定,必须大于100秒).(这个值根据目前的网络设置,可以适当地改小)
3.配置连接池参数
本文以两款数据库连接池为例,C3P0和Druid。其中C3P0属于一代连接池,Druid属于二代连接池。
C3P0,版本c3p0-0.9.2.1
package com.framework.admin.test;
import java.sql.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
import com.mchange.v2.c3p0.ComboPooledDataSource;
public class TestC3P0 {
//c3p0-0.9.2.1
public static void main(String[] args) throws SQLException {
int threadNum = 3;//线程数
long excuteMCounts = 1000000000;//执行次数
ExecutorService pool = Executors.newFixedThreadPool(threadNum);
int c = 0;//计数器
Connection conn = null;
Statement stat = null;
ComboPooledDataSource dataSource = new ComboPooledDataSource();
try {
dataSource.setJdbcUrl("jdbc:dm://DM_DW?dbAliveCheckFreq=10000");
dataSource.setDriverClass("dm.jdbc.driver.DmDriver");
dataSource.setUser("GFW_EASYUI");
dataSource.setPassword("GFW_EASYUI");
dataSource.setInitialPoolSize(50); //连接池初始化时创建的连接数
dataSource.setMaxPoolSize(100); //连接池中拥有的最大连接数,如果获得新连接时会使连接总数超过这个值则不会再获取新连接,而是等待其他连接释放
dataSource.setMaxIdleTime(1800); //连接的最大空闲时间,如果超过这个时间,某个数据库连接还没有被使用,则会断开掉这个连接。如果为0,则永远不会断开连接
dataSource.setAcquireIncrement(5); //连接池在无空闲连接可用时一次性创建的新数据库连接数
dataSource.setIdleConnectionTestPeriod(0); //每个几秒检查所有连接池中的空闲连接,0为取消检查
dataSource.setTestConnectionOnCheckin(false); //连接的同时将校验连接的有效性,false为取消检查
dataSource.setTestConnectionOnCheckout(false); //连接释放的同时将校验连接的有效性,false为取消检查
dataSource.setBreakAfterAcquireFailure(false); //true表示pool向数据库请求连接失败后标记整个pool为block并close,就算后端数据库恢复正常也不进行重连,客户端对pool的请求都拒绝掉。false表示不会标记 pool为block,新的请求都会尝试去数据库请求connection。默认为false。因此,如果想让数据库和网络故障恢复之后,pool能继续请求正常资源必须把此项配置设为false
} catch (Exception e) {
}
while (c < excuteMCounts) {
conn = dataSource.getConnection();
stat = conn.createStatement();
pool.execute(new MyWork(conn, stat));
c++;
}
pool.shutdown();
}
}
class MyWork implements Runnable {
Connection conn;
Statement stat;
ResultSet rs;
AtomicLong totalNum;
AtomicLong totalTime;
MyWork (Connection conn, Statement stat) {
this.conn = conn;
this.stat = stat;
}
@Override
public void run() {
long start = System.currentTimeMillis();
try {
stat = conn.createStatement();
//模拟多读多写环境
stat.executeUpdate("INSERT INTO LOGIN_LOGS VALUES(SYS_GUID,'admin')");
rs = stat.executeQuery("SELECT A.* FROM FW_B_USER A LEFT JOIN FW_B_USER_ROLE B ON A.USER_ID = B.USER_ID");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
rs.close();
stat.close();
conn.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
long end = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + "执行" + (end - start) + "毫秒");
}
}
Druid,版本druid-1.1.22
package com.framework.admin.test;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;
import com.alibaba.druid.pool.DruidDataSource;
public class TestDruid {
//注意此版本的druid(druid-1.1.22)不支持jdk1.6,需要更换为jdk1.8
public static void main(String[] args) throws SQLException {
int threadNum = 3;//线程数
long excuteMCounts = 1000000000;//执行次数
ExecutorService pool = Executors.newFixedThreadPool(threadNum);
int c = 0;//计数器
Connection conn = null;
Statement stat = null;
DruidDataSource dataSource = new DruidDataSource();
try {
//2.设置连接数据库需要的配置信息
dataSource.setDriverClassName("dm.jdbc.driver.DmDriver");
dataSource.setUrl("jdbc:dm://DM_DW?dbAliveCheckFreq=10000");
dataSource.setUsername("GFW_EASYUI");
dataSource.setPassword("GFW_EASYUI");
dataSource.setMaxActive(200);//最大连接数量
dataSource.setInitialSize(50);//初始化连接
dataSource.setRemoveAbandoned(true);//是否自动回收超时连接
dataSource.setRemoveAbandonedTimeout(180);//超时时间(以秒数为单位)
dataSource.setMaxWait(120000);//超时等待时间以毫秒为单位
dataSource.setTimeBetweenEvictionRunsMillis(60000); //检查空闲连接的频率,单位毫秒, 非正整数时表示不进行检查
dataSource.setMinEvictableIdleTimeMillis(180000); //池中某个连接的空闲时长达到 N 毫秒后, 连接池在下次检查空闲连接时,回收该连接,要小于防火墙超时设置
dataSource.setTestWhileIdle(true); //当程序请求连接,池在分配连接时,是否先检查该连接是否有效
dataSource.setTestOnBorrow(false); //程序 申请 连接时,进行连接有效性检查(
dataSource.setTestOnReturn(false); //程序 返还 连接时,进行连接有效性检查
dataSource.setValidationQuery("SELECT 1 FROM DUAL;"); //检查池中的连接是否仍可用的 SQL语句
dataSource.setPoolPreparedStatements(true); //是否缓存preparedStatement
dataSource.setMaxPoolPreparedStatementPerConnectionSize(20); //每个连接最多缓存多少个SQL
} catch (Exception e) {
}
while (c < excuteMCounts) {
conn = dataSource.getConnection();
stat = conn.createStatement();
pool.execute(new MyWork3(conn, stat));
c++;
}
pool.shutdown();
}
}
class MyWork3 implements Runnable {
Connection conn;
Statement stat;
ResultSet rs;
AtomicLong totalNum;
AtomicLong totalTime;
MyWork3 (Connection conn, Statement stat) {
this.conn = conn;
this.stat = stat;
}
@Override
public void run() {
long start = System.currentTimeMillis();
//此处调用业务方法
try {
stat = conn.createStatement();
stat.executeUpdate("INSERT INTO LOGIN_LOGS VALUES(SYS_GUID,'admin')");
rs = stat.executeQuery("SELECT A.* FROM FW_B_USER A LEFT JOIN FW_B_USER_ROLE B ON A.USER_ID = B.USER_ID");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
rs.close();
stat.close();
conn.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
long end = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + "执行" + (end - start) + "毫秒");
}
}
三、测试过程
程序模拟100并发,当程序运行时,断网(拔掉主库网线或者断开网卡)后10秒内数据库集群进行切换。数据库切换成功后,配置Druid的应用基本秒切换到备库,配置C3P0的应用1分钟左右切换到备库。
测试中对比发现,对于多线程的并发来说,Druid的故障重连速度是要高于C3P0的。
以上便是高可用环境下,使用连接池连接数据库的相关配置。在实际应用中,影响故障重连速度的原因还有很多,例如网络速度、应用压力、读写比等等。根据不同情况选择不同的数据库集群以及连接池,也是我们需要重点考虑的内容。