数据库连接池性能调优实战:HikariCP配置详解与监控告警体系建设

数字化生活设计师
数字化生活设计师 2026-01-07T01:01:01+08:00
0 0 0

引言

在现代Web应用开发中,数据库连接池作为连接数据库的核心组件,其性能直接影响着整个系统的响应速度和吞吐量。随着业务规模的不断扩大,如何构建高性能、高可用的数据库访问层成为每个开发者必须面对的挑战。

HikariCP作为一个轻量级、高性能的JDBC连接池,凭借其卓越的性能表现和丰富的配置选项,已经成为众多企业级应用的首选。然而,仅仅使用HikariCP并不意味着系统就能获得最佳性能,合理的配置调优和完善的监控告警体系同样重要。

本文将深入探讨数据库连接池的性能优化策略,详细介绍HikariCP的各项配置参数调优方法、连接池监控指标体系建设、异常检测与告警机制,帮助开发者构建高性能、高可用的数据库访问层。

HikariCP核心概念与优势

什么是数据库连接池

数据库连接池是一种复用数据库连接的技术,通过维护一组预先创建的数据库连接,避免了频繁创建和销毁连接带来的性能开销。当应用程序需要访问数据库时,从连接池中获取一个空闲连接;使用完毕后,将连接返回给连接池,而不是直接关闭。

HikariCP的核心优势

HikariCP作为当前最流行的JDBC连接池之一,具有以下显著优势:

  1. 卓越的性能表现:HikariCP在性能上远超其他连接池实现,其设计目标是提供最佳的吞吐量和最低的延迟
  2. 轻量级架构:代码简洁,依赖少,内存占用小
  3. 自动化的连接管理:内置连接泄漏检测、连接验证等功能
  4. 丰富的配置选项:提供了大量可调优参数,满足不同场景需求
  5. 完善的监控支持:提供详细的运行时指标和统计信息

HikariCP核心配置参数详解

基础配置参数

1. dataSourceClassName

HikariConfig config = new HikariConfig();
config.setDataSourceClassName("com.mysql.cj.jdbc.MysqlDataSource");

这个参数指定了数据源的实现类,HikariCP会使用反射机制创建相应的数据源实例。对于MySQL数据库,通常使用com.mysql.cj.jdbc.MysqlDataSource

2. jdbcUrl

config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC");

JDBC URL用于指定数据库连接的地址、端口和数据库名称等信息。需要注意的是,应该在URL中添加适当的参数来优化连接性能。

3. username 和 password

config.setUsername("myuser");
config.setPassword("mypassword");

数据库用户名和密码,建议使用环境变量或配置文件管理敏感信息。

连接池大小配置

1. maximumPoolSize

// 设置最大连接池大小
config.setMaximumPoolSize(20);

maximumPoolSize定义了连接池中最大连接数。这个值应该根据应用的并发访问量和数据库的最大连接数来设置。

调优建议

  • 对于CPU密集型应用,可以设置较小的连接池大小
  • 对于IO密集型应用,可以适当增加连接池大小
  • 通常设置为数据库最大连接数的50%-80%

2. minimumIdle

// 设置最小空闲连接数
config.setMinimumIdle(5);

minimumIdle参数定义了连接池中保持的最小空闲连接数。这个值可以确保在高并发时有足够的连接可用。

调优建议

  • 通常设置为最大连接池大小的20%-30%
  • 对于高并发应用,可以适当增加此值

3. poolName

// 设置连接池名称
config.setPoolName("MyHikariCP");

连接池名称用于在日志和监控中标识不同的连接池实例。

连接管理配置

1. connectionTimeout

// 设置连接超时时间(毫秒)
config.setConnectionTimeout(30000);

connectionTimeout定义了从连接池获取连接的最大等待时间。如果超过此时间仍未获取到连接,将抛出异常。

调优建议

  • 一般设置为30秒到60秒
  • 过小可能导致连接获取失败
  • 过大可能影响应用响应速度

2. idleTimeout

// 设置连接空闲超时时间(毫秒)
config.setIdleTimeout(600000);

idleTimeout定义了连接在池中空闲的最大时间。超过此时间的空闲连接将被回收。

调优建议

  • 通常设置为5-10分钟
  • 需要平衡内存占用和连接创建开销

3. maxLifetime

// 设置连接最大生命周期(毫秒)
config.setMaxLifetime(1800000);

maxLifetime定义了连接在池中的最大生命周期。超过此时间的连接将被强制关闭并重新创建。

调优建议

  • 通常设置为30分钟到1小时
  • 需要考虑数据库的连接超时设置

连接验证配置

1. validationTimeout

// 设置连接验证超时时间(毫秒)
config.setValidationTimeout(5000);

validationTimeout定义了连接验证的最大等待时间。

2. connectionTestQuery

// 设置连接测试查询语句
config.setConnectionTestQuery("SELECT 1");

connectionTestQuery用于验证连接是否有效的SQL语句。这是一个轻量级的查询,通常使用简单的SELECT 1

高级配置参数

1. leakDetectionThreshold

// 设置连接泄漏检测阈值(毫秒)
config.setLeakDetectionThreshold(60000);

leakDetectionThreshold用于检测连接泄漏的阈值。如果连接使用时间超过此阈值,HikariCP会记录警告信息。

2. autoCommit

// 设置自动提交
config.setAutoCommit(false);

autoCommit控制是否启用自动提交功能。通常建议关闭自动提交以提高性能。

3. transactionIsolation

// 设置事务隔离级别
config.setTransactionIsolation("TRANSACTION_READ_COMMITTED");

transactionIsolation用于设置默认的事务隔离级别。

实际配置示例

完整的HikariCP配置示例

@Configuration
public class DatabaseConfig {
    
    @Bean
    public DataSource dataSource() {
        HikariConfig config = new HikariConfig();
        
        // 基础配置
        config.setDataSourceClassName("com.mysql.cj.jdbc.MysqlDataSource");
        config.setJdbcUrl("jdbc:mysql://localhost:3306/myapp?useSSL=false&serverTimezone=UTC&characterEncoding=utf8");
        config.setUsername("myuser");
        config.setPassword("mypassword");
        
        // 连接池配置
        config.setMaximumPoolSize(20);
        config.setMinimumIdle(5);
        config.setPoolName("MyAppHikariCP");
        
        // 连接管理
        config.setConnectionTimeout(30000);
        config.setIdleTimeout(600000);
        config.setMaxLifetime(1800000);
        
        // 验证配置
        config.setValidationTimeout(5000);
        config.setConnectionTestQuery("SELECT 1");
        
        // 高级配置
        config.setLeakDetectionThreshold(60000);
        config.setAutoCommit(false);
        config.setTransactionIsolation("TRANSACTION_READ_COMMITTED");
        
        // 连接池监控
        config.setRegisterMbeans(true);
        
        return new HikariDataSource(config);
    }
}

针对不同场景的配置优化

高并发场景配置

HikariConfig highConcurrencyConfig = new HikariConfig();
highConcurrencyConfig.setMaximumPoolSize(50);
highConcurrencyConfig.setMinimumIdle(10);
highConcurrencyConfig.setConnectionTimeout(15000);
highConcurrencyConfig.setIdleTimeout(300000);
highConcurrencyConfig.setMaxLifetime(1200000);

低并发场景配置

HikariConfig lowConcurrencyConfig = new HikariConfig();
lowConcurrencyConfig.setMaximumPoolSize(10);
lowConcurrencyConfig.setMinimumIdle(2);
highConcurrencyConfig.setConnectionTimeout(30000);
highConcurrencyConfig.setIdleTimeout(600000);
highConcurrencyConfig.setMaxLifetime(1800000);

连接池监控指标体系建设

监控指标分类

1. 基础运行指标

public class ConnectionPoolMetrics {
    private final HikariDataSource dataSource;
    
    public ConnectionPoolMetrics(HikariDataSource dataSource) {
        this.dataSource = dataSource;
    }
    
    // 获取活跃连接数
    public int getActiveConnections() {
        return dataSource.getHikariPoolMXBean().getActiveConnections();
    }
    
    // 获取空闲连接数
    public int getIdleConnections() {
        return dataSource.getHikariPoolMXBean().getIdleConnections();
    }
    
    // 获取总连接数
    public int getTotalConnections() {
        return dataSource.getHikariPoolMXBean().getTotalConnections();
    }
    
    // 获取等待连接数
    public int getThreadsAwaitingConnection() {
        return dataSource.getHikariPoolMXBean().getThreadsAwaitingConnection();
    }
}

2. 性能指标

// 连接获取时间统计
public class ConnectionAcquisitionStats {
    private final AtomicLong totalAcquisitionTime = new AtomicLong(0);
    private final AtomicLong acquisitionCount = new AtomicLong(0);
    
    public void recordAcquisitionTime(long time) {
        totalAcquisitionTime.addAndGet(time);
        acquisitionCount.incrementAndGet();
    }
    
    public double getAverageAcquisitionTime() {
        if (acquisitionCount.get() == 0) return 0;
        return (double) totalAcquisitionTime.get() / acquisitionCount.get();
    }
}

Prometheus监控集成

@Component
public class HikariMetricsCollector implements MeterRegistryCustomizer<SimpleMeterRegistry> {
    
    @Autowired
    private HikariDataSource dataSource;
    
    @Override
    public void customize(SimpleMeterRegistry registry) {
        Gauge.builder("hikari.active.connections")
            .description("Active connections in the pool")
            .register(registry, dataSource, ds -> ds.getHikariPoolMXBean().getActiveConnections());
            
        Gauge.builder("hikari.idle.connections")
            .description("Idle connections in the pool")
            .register(registry, dataSource, ds -> ds.getHikariPoolMXBean().getIdleConnections());
            
        Gauge.builder("hikari.total.connections")
            .description("Total connections in the pool")
            .register(registry, dataSource, ds -> ds.getHikariPoolMXBean().getTotalConnections());
            
        Gauge.builder("hikari.waiting.connections")
            .description("Threads waiting for connection")
            .register(registry, dataSource, ds -> ds.getHikariPoolMXBean().getThreadsAwaitingConnection());
    }
}

日志监控配置

@Configuration
public class LoggingConfig {
    
    @Bean
    public HikariConfig hikariConfig() {
        HikariConfig config = new HikariConfig();
        // ... 其他配置
        
        // 启用详细的日志记录
        config.setLeakDetectionThreshold(60000);
        config.setRegisterMbeans(true);
        
        return config;
    }
}

异常检测与告警机制

连接池异常类型识别

1. 连接泄漏检测

public class ConnectionLeakDetector {
    
    private final HikariDataSource dataSource;
    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
    
    public ConnectionLeakDetector(HikariDataSource dataSource) {
        this.dataSource = dataSource;
        startMonitoring();
    }
    
    private void startMonitoring() {
        scheduler.scheduleAtFixedRate(() -> {
            try {
                HikariPoolMXBean poolBean = dataSource.getHikariPoolMXBean();
                int activeConnections = poolBean.getActiveConnections();
                int totalConnections = poolBean.getTotalConnections();
                
                // 如果活跃连接数接近最大连接数,可能存在连接泄漏
                if (activeConnections > totalConnections * 0.8) {
                    logger.warn("Potential connection leak detected: {}/{} connections in use",
                               activeConnections, totalConnections);
                    // 触发告警
                    triggerAlert("Connection leak warning", 
                               "Active connections exceed 80% of total connections");
                }
            } catch (Exception e) {
                logger.error("Error monitoring connection pool", e);
            }
        }, 1, 5, TimeUnit.MINUTES);
    }
    
    private void triggerAlert(String title, String message) {
        // 实现告警逻辑
        System.out.println("ALERT: " + title + " - " + message);
    }
}

2. 连接超时检测

public class ConnectionTimeoutMonitor {
    
    private final HikariDataSource dataSource;
    private final AtomicLong timeoutCount = new AtomicLong(0);
    private final AtomicLong totalWaitTime = new AtomicLong(0);
    
    public ConnectionTimeoutMonitor(HikariDataSource dataSource) {
        this.dataSource = dataSource;
        setupMonitoring();
    }
    
    private void setupMonitoring() {
        // 监控连接获取超时情况
        HikariPoolMXBean poolBean = dataSource.getHikariPoolMXBean();
        
        // 可以通过JMX或自定义拦截器实现
        // 这里提供一个简单的实现思路
    }
    
    public void recordTimeout() {
        timeoutCount.incrementAndGet();
    }
    
    public double getAverageWaitTime() {
        if (timeoutCount.get() == 0) return 0;
        return (double) totalWaitTime.get() / timeoutCount.get();
    }
}

告警策略设计

1. 基础告警阈值

@Component
public class ConnectionPoolAlertService {
    
    private static final double HIGH_UTILIZATION_THRESHOLD = 0.8;
    private static final double LOW_IDLE_THRESHOLD = 0.2;
    private static final int CONNECTION_TIMEOUT_THRESHOLD = 5000; // 5秒
    
    @Autowired
    private HikariDataSource dataSource;
    
    public void checkAndAlert() {
        HikariPoolMXBean poolBean = dataSource.getHikariPoolMXBean();
        
        double utilization = (double) poolBean.getActiveConnections() / poolBean.getTotalConnections();
        
        // 高利用率告警
        if (utilization > HIGH_UTILIZATION_THRESHOLD) {
            sendAlert("High Connection Utilization", 
                     String.format("Connection pool utilization is %.2f%%", utilization * 100));
        }
        
        // 低空闲连接告警
        double idleRatio = (double) poolBean.getIdleConnections() / poolBean.getTotalConnections();
        if (idleRatio < LOW_IDLE_THRESHOLD) {
            sendAlert("Low Idle Connections", 
                     String.format("Idle connections ratio is %.2f%%", idleRatio * 100));
        }
        
        // 等待连接告警
        if (poolBean.getThreadsAwaitingConnection() > 0) {
            sendAlert("Connection Wait", 
                     String.format("%d threads are waiting for database connections",
                                poolBean.getThreadsAwaitingConnection()));
        }
    }
    
    private void sendAlert(String title, String message) {
        // 实现具体的告警发送逻辑
        System.out.println("ALERT - " + title + ": " + message);
        
        // 可以集成邮件、短信、微信等告警方式
        // 这里简化为控制台输出
    }
}

2. 告警聚合与抑制

@Component
public class AlertAggregator {
    
    private final Map<String, AlertRecord> alertRecords = new ConcurrentHashMap<>();
    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
    
    @PostConstruct
    public void startAlertMonitoring() {
        scheduler.scheduleAtFixedRate(this::cleanupOldAlerts, 1, 1, TimeUnit.HOURS);
    }
    
    public void registerAlert(String alertKey, String message) {
        AlertRecord record = alertRecords.computeIfAbsent(alertKey, k -> new AlertRecord());
        record.update(message);
        
        // 如果是新的告警或者间隔时间足够长,发送告警
        if (record.isNew() || record.shouldSendAlert()) {
            sendAlert(alertKey, message);
            record.reset();
        }
    }
    
    private void cleanupOldAlerts() {
        long now = System.currentTimeMillis();
        alertRecords.entrySet().removeIf(entry -> 
            now - entry.getValue().getLastTriggerTime() > 24 * 60 * 60 * 1000); // 24小时
    }
    
    private void sendAlert(String alertKey, String message) {
        // 实现告警发送逻辑
        System.out.println("Sending alert: " + alertKey + " - " + message);
    }
    
    private static class AlertRecord {
        private String lastMessage;
        private long lastTriggerTime = 0;
        private int triggerCount = 0;
        
        public void update(String message) {
            this.lastMessage = message;
            this.lastTriggerTime = System.currentTimeMillis();
            this.triggerCount++;
        }
        
        public boolean isNew() {
            return lastTriggerTime == 0;
        }
        
        public boolean shouldSendAlert() {
            // 每5分钟发送一次重复告警
            return System.currentTimeMillis() - lastTriggerTime > 5 * 60 * 1000;
        }
        
        public void reset() {
            triggerCount = 0;
        }
        
        public long getLastTriggerTime() {
            return lastTriggerTime;
        }
    }
}

性能调优最佳实践

配置调优策略

1. 基于负载的动态调优

@Component
public class DynamicConnectionPoolConfig {
    
    private final HikariDataSource dataSource;
    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
    
    public DynamicConnectionPoolConfig(HikariDataSource dataSource) {
        this.dataSource = dataSource;
        startDynamicTuning();
    }
    
    private void startDynamicTuning() {
        scheduler.scheduleAtFixedRate(() -> {
            try {
                HikariPoolMXBean poolBean = dataSource.getHikariPoolMXBean();
                
                // 根据系统负载动态调整连接池大小
                int currentActive = poolBean.getActiveConnections();
                int total = poolBean.getTotalConnections();
                double utilization = (double) currentActive / total;
                
                if (utilization > 0.9) {
                    // 高负载,增加连接池大小
                    adjustPoolSize(1.2);
                } else if (utilization < 0.3 && total > 10) {
                    // 低负载,减少连接池大小
                    adjustPoolSize(0.8);
                }
                
            } catch (Exception e) {
                logger.error("Error in dynamic tuning", e);
            }
        }, 30, 30, TimeUnit.SECONDS);
    }
    
    private void adjustPoolSize(double factor) {
        try {
            HikariConfig config = dataSource.getHikariConfigMXBean();
            int currentMaxSize = config.getMaximumPoolSize();
            int newMaxSize = (int) (currentMaxSize * factor);
            
            if (newMaxSize > 0 && newMaxSize != currentMaxSize) {
                config.setMaximumPoolSize(newMaxSize);
                logger.info("Adjusted pool size from {} to {}", currentMaxSize, newMaxSize);
            }
        } catch (Exception e) {
            logger.error("Error adjusting pool size", e);
        }
    }
}

2. 数据库连接优化

public class DatabaseConnectionOptimizer {
    
    public static HikariConfig optimizeForDatabase(String databaseType, int concurrentUsers) {
        HikariConfig config = new HikariConfig();
        
        switch (databaseType.toLowerCase()) {
            case "mysql":
                // MySQL优化配置
                config.setMaximumPoolSize(Math.min(50, concurrentUsers * 2));
                config.setMinimumIdle(Math.max(1, concurrentUsers / 4));
                config.setConnectionTimeout(30000);
                config.setIdleTimeout(600000);
                config.setMaxLifetime(1800000);
                config.setLeakDetectionThreshold(60000);
                break;
                
            case "postgresql":
                // PostgreSQL优化配置
                config.setMaximumPoolSize(Math.min(30, concurrentUsers * 2));
                config.setMinimumIdle(Math.max(1, concurrentUsers / 4));
                config.setConnectionTimeout(30000);
                config.setIdleTimeout(600000);
                config.setMaxLifetime(1800000);
                config.setLeakDetectionThreshold(60000);
                break;
                
            default:
                // 默认配置
                config.setMaximumPoolSize(Math.min(20, concurrentUsers * 2));
                config.setMinimumIdle(Math.max(1, concurrentUsers / 4));
                config.setConnectionTimeout(30000);
                config.setIdleTimeout(600000);
                config.setMaxLifetime(1800000);
                config.setLeakDetectionThreshold(60000);
        }
        
        return config;
    }
}

监控工具集成

1. Spring Boot Actuator集成

@Configuration
public class HikariActuatorConfig {
    
    @Bean
    public HikariPoolMXBean hikariPoolMXBean(HikariDataSource dataSource) {
        return dataSource.getHikariPoolMXBean();
    }
    
    @Bean
    public MeterRegistryCustomizer<SimpleMeterRegistry> hikariMetrics() {
        return registry -> {
            // 注册HikariCP指标到Spring Boot Actuator
            Gauge.builder("hikaricp.connections.active")
                .description("Active connections")
                .register(registry, dataSource, ds -> ds.getHikariPoolMXBean().getActiveConnections());
                
            Gauge.builder("hikaricp.connections.idle")
                .description("Idle connections")
                .register(registry, dataSource, ds -> ds.getHikariPoolMXBean().getIdleConnections());
        };
    }
}

2. 自定义监控端点

@RestController
@RequestMapping("/monitoring")
public class ConnectionPoolMonitorController {
    
    @Autowired
    private HikariDataSource dataSource;
    
    @GetMapping("/pool-status")
    public ResponseEntity<Map<String, Object>> getPoolStatus() {
        Map<String, Object> status = new HashMap<>();
        HikariPoolMXBean poolBean = dataSource.getHikariPoolMXBean();
        
        status.put("activeConnections", poolBean.getActiveConnections());
        status.put("idleConnections", poolBean.getIdleConnections());
        status.put("totalConnections", poolBean.getTotalConnections());
        status.put("threadsAwaitingConnection", poolBean.getThreadsAwaitingConnection());
        status.put("connectionTimeout", dataSource.getHikariConfigMXBean().getConnectionTimeout());
        
        return ResponseEntity.ok(status);
    }
    
    @GetMapping("/pool-stats")
    public ResponseEntity<Map<String, Object>> getPoolStats() {
        Map<String, Object> stats = new HashMap<>();
        HikariPoolMXBean poolBean = dataSource.getHikariPoolMXBean();
        
        stats.put("activeConnections", poolBean.getActiveConnections());
        stats.put("idleConnections", poolBean.getIdleConnections());
        stats.put("totalConnections", poolBean.getTotalConnections());
        stats.put("threadsAwaitingConnection", poolBean.getThreadsAwaitingConnection());
        stats.put("maxLifetime", dataSource.getHikariConfigMXBean().getMaxLifetime());
        stats.put("connectionTimeout", dataSource.getHikariConfigMXBean().getConnectionTimeout());
        
        return ResponseEntity.ok(stats);
    }
}

故障诊断与问题排查

常见问题分析

1. 连接池耗尽问题

public class ConnectionPoolDrainageDetector {
    
    private final HikariDataSource dataSource;
    private final AtomicLong connectionTimeoutCount = new AtomicLong(0);
    private final Queue<Long> timeoutTimestamps = new ConcurrentLinkedQueue<>();
    
    public ConnectionPoolDrainageDetector(HikariDataSource dataSource) {
        this.dataSource = dataSource;
        startMonitoring();
    }
    
    private void startMonitoring() {
        // 每分钟检查一次连接池状态
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
        scheduler.scheduleAtFixedRate(() -> {
            checkPoolStatus();
        }, 1, 1, TimeUnit.MINUTES);
    }
    
    private void checkPoolStatus() {
        HikariPoolMXBean poolBean = dataSource.getHikariPoolMXBean();
        
        // 如果等待连接数大于0,说明可能存在连接池耗尽
        if (poolBean.getThreadsAwaitingConnection() > 0) {
            logger.warn("Connection pool may be drained. Threads waiting: {}",
                       poolBean.getThreadsAwaitingConnection());
            
            // 记录超时事件
            connectionTimeoutCount.incrementAndGet();
            timeoutTimestamps.offer(System.currentTimeMillis());
            
            // 清理旧的超时记录
            cleanupOldTimestamps();
        }
    }
    
    private void cleanupOldTimestamps() {
        long cutoffTime = System.currentTimeMillis() - 24 * 60 * 60 * 1000; // 24小时
        while (!timeoutTimestamps.isEmpty() && timeoutTimestamps.peek() < cutoffTime) {
            timeoutTimestamps.poll();
        }
    }
    
    public int getRecentTimeoutCount() {
        return timeoutTimestamps.size();
    }
}

2. 连接泄漏检测

@Component
public class ConnectionLeakAnalyzer {
    
    private final HikariDataSource dataSource;
    private final AtomicLong leakDetectionCount = new AtomicLong(0);
    
    public ConnectionLeakAnalyzer(HikariDataSource dataSource) {
        this.dataSource = dataSource;
        setupLeakDetection();
    }
    
    private void setupLeakDetection() {
        // 启用连接泄漏检测
        HikariConfig config = dataSource.getHikariConfigMXBean();
        if (config.getLeakDetectionThreshold() == 0) {
            config.setLeakDetectionThreshold(60000); // 1分钟
        }
    }
    
    public void analyzeLeaks() {
        try {
            HikariPoolMXBean poolBean = dataSource.getHikariPoolMXBean();
            
            // 获取详细的连接池信息
            int active = poolBean.getActiveConnections();
            int idle = poolBean.getIdleConnections();
            int total = poolBean.getTotalConnections();
            
            logger.info("Pool Status - Active: {}, Idle: {}, Total: {}", active, idle, total);
            
            if (active >= total * 0.9) {
                logger.warn("High pool utilization detected");
            }
            
        } catch (Exception e) {
            logger.error("Error analyzing connection pool",
相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000