一、问题提出及解决

某试点现场,测试人员反馈,连接数据库失败,日志出现too many connections。由于数据库是我用docker部署的,且时间紧急,需立即响应,经询问,版本、配置与其它试点无差别,只是新试点多了一些连接的微服务。

经查,可以在容器启动时添加连接参数解决,如下:

command: ['mysqld', '--max-connections=1024', ‘其它参数’]

为不影响上线,当时提供修改后的配置给测试人员,修改后测试正常。由于这块暂时没有研究到,所以有了本文。

二、服务器设置

2.1 数据库服务

docker-compose.yml 文件:

version: '2'

services:
  ttmysql:
    image: mysql:8.0
    container_name: llmysql
    restart: always
    command: ['mysqld', '--character-set-server=utf8mb4', '--collation-server=utf8mb4_unicode_ci', '--explicit_defaults_for_timestamp=false', '--lower_case_table_names=1', '--default_authentication_plugin=mysql_native_password']  ## , '--skip-ssl'
    volumes:
        - ./mysqldata:/var/lib/mysql
    environment:
        - TZ=Asia/Shanghai
        - MYSQL_ROOT_PASSWORD=123456
        - MYSQL_USER=latelee
        - MYSQL_PASSWORD=123456
    ports:
        - "43306:3306"
    networks:
      - mysql-net

networks:
  mysql-net:
    driver: bridge
~

为简单起见,数据库的root用户密码为123456

启动:

docker-compose up -d

2.2 配置文件分析

进入容器:

docker exec -it llmysql bash

查看默认配置文件:

# cat /etc/mysql/my.cnf

[mysqld]
pid-file        = /var/run/mysqld/mysqld.pid
socket          = /var/run/mysqld/mysqld.sock
datadir         = /var/lib/mysql
secure-file-priv= NULL

# Custom config should go here
!includedir /etc/mysql/conf.d/

可以看到,自定义的配置文件应放置于/etc/mysql/conf.d/目录。查看之:

# ls /etc/mysql/conf.d/
docker.cnf  mysql.cnf

其中,/etc/mysql/conf.d/mysql.cnf 文件无实质内容。

对于容器部署的情形,可以将自定义配置文件挂载到上述目录,示例如下:

volumes:
    - ./mysqldata:/var/lib/mysql
    - ./my.cnf:/etc/mysql/conf.d/my.cnf

2.3 参数配置说明

由官方文档知,对于系统变量(server-system-variables),既可以通过命令行参数的形式传递,也可以通过配置文件的形式。从笔者经验看,如大部分使用默认配置,只改动个别参数,则前者较好,但后者定制能力强,适用于高阶应用。

如设置最大连接数,可在my.cnf文件做如下修改:

[mysqld]
max_connections=1000

也可用命令行方式,参数如下:

command: ['mysqld', '--max-connections=1024', ‘其它参数’]

注意,两者同时存在时,经测试,命令行方式优先级最高。

三、客户端测试

3.1 连接数据库

使用如下命令连接数据库:

 mysql -h 127.0.0.1 -P 43306 -uroot -p123456 --ssl-mode=DISABLED
 
临时 创建test数据库:
 create database test;

3.2 查询状态

最大连接数

使用如下命令查询mysql服务器的最大连接数:

show variables like '%max_connections%';

连接及查询输出日志:

$ mysql -h 127.0.0.1 -P 43306 -uroot -p123456 --ssl-mode=DISABLED
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 216676
Server version: 8.0.21 MySQL Community Server - GPL

Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> show variables like '%max_connections%';
+------------------------+-------+
| Variable_name          | Value |
+------------------------+-------+
| max_connections        | 151   |
| mysqlx_max_connections | 100   |
+------------------------+-------+
2 rows in set (3.08 sec)

可以看到,默认最大连接数为151。据官方文档,max_connections值范围为1~100000。

服务器响应的连接数

max_used_connections

show global status like 'max_used_connections';

输出示例:

mysql> show global status like 'max_used_connections';
+----------------------+-------+
| Variable_name        | Value |
+----------------------+-------+
| Max_used_connections | 1     |
+----------------------+-------+
1 row in set (0.00 sec)
当前连接数
SHOW STATUS LIKE 'Threads%';

输出示例:

mysql> SHOW STATUS LIKE 'Threads%';
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| Threads_cached    | 0     |
| Threads_connected | 1     |
| Threads_created   | 1     |
| Threads_running   | 2     |
+-------------------+-------+
4 rows in set (0.00 sec)
当前连接信息
SHOW PROCESSLIST;

输出示例:

SHOW PROCESSLIST;
+----+-----------------+---------------------+------+---------+------+------------------------+------------------+
| Id | User            | Host                | db   | Command | Time | State                  | Info             |
+----+-----------------+---------------------+------+---------+------+------------------------+------------------+
|  5 | event_scheduler | localhost           | NULL | Daemon  |   80 | Waiting on empty queue | NULL             |
|  8 | root            | 192.168.144.1:44052 | NULL | Query   |    0 | init                   | SHOW PROCESSLIST |
+----+-----------------+---------------------+------+---------+------+------------------------+------------------+
2 rows in set (0.00 sec)

说明:因无其它连接,所以实际连接数为1,执行的命令为SHOW PROCESSLIST

max_used_connections / max_connections * 100%(理想值≈ 85%)
如果max_used_connections跟max_connections相同,那么就是max_connections设置过低或者超过服务器负载上限了,低于10%则设置过大。

3.3 自测程序连接

本节使用golang编写程序进行连接测试,再查询服务端的连接信息。

数据库相关封闭代码如下:

package main

import (
	"context"
	"database/sql"
	"fmt"
	"strings"
	"time"

	"errors"
	"os"

	// 导入mysql驱动
	_ "github.com/denisenkom/go-mssqldb"
	_ "github.com/go-sql-driver/mysql"

	// _ "github.com/mattn/go-oci8"
	_ "github.com/mattn/go-sqlite3"
)

const (
	SQL_TIMEOUT_S = 10 // 连接超时时间 10 秒
)

// 根据传入参数解析
func CreateSQLDb(dbstr string) (SQLDB *sql.DB, DBType string) {
	fmt.Println("connecting db...")
	var err error

	if len(dbstr) == 0 {
		fmt.Println("sql string is empty")
		os.Exit(0)
	}
	if strings.Contains(dbstr, "encrypt=disable") == true {
		SQLDB, err = CreateSqlServer(dbstr)
		DBType = "sqlserver"
	} else if strings.Contains(dbstr, "@tcp") == true {
		SQLDB, err = CreateMysql(dbstr)
		DBType = "mysql"
	} else if strings.Contains(strings.ToUpper(dbstr), "DB3") == true {
		SQLDB, err = CreateSqlite3(dbstr)
		DBType = "sqlite"
	} else {
		fmt.Printf("dbstr %v not support\n", dbstr)
		os.Exit(0)
	}

	/*
		else if strings.Contains(dbstr, "latelee_pdb") == true ||
			strings.Contains(dbstr, "ORCLCDB") == true {
			SQLDB, err = db.CreateOracle(dbstr)
			DBType = "oracle"
		}
	*/
	if err != nil {
		fmt.Println("connect db error: ", err)
	}

	return
}

/

func IsExist(path string) bool {
	_, err := os.Stat(path)
	return err == nil || os.IsExist(err)
}

func CreateSqlServer(dbstr string) (sqldb *sql.DB, err error) {
	connCh := make(chan int)
	timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second*SQL_TIMEOUT_S)
	defer cancel()

	go func() {
		sqldb, err = sql.Open("mssql", dbstr)
		if err != nil {
			err = errors.New("open database failed: " + err.Error())
			connCh <- -1
			return
		}
		// Open不一定会连接数据库,Ping可能会连接
		err = sqldb.Ping()
		if err != nil {
			err = errors.New("connect database failed: " + err.Error())
			connCh <- -1
			return
		}
		connCh <- 0
	}()

	select {
	case <-timeoutCtx.Done():
		sqldb = nil
		err = errors.New("connect to sqlserver timeout")
	case ret := <-connCh:
		if ret == 0 {
			fmt.Println("connect to sqlserver ok")
			err = nil
		}
	}
	return
}

func CreateMysql(dbstr string) (sqldb *sql.DB, err error) {
	connCh := make(chan int)
	timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second*SQL_TIMEOUT_S)
	defer cancel()

	go func() {
		sqldb, err = sql.Open("mysql", dbstr)
		if err != nil {
			err = errors.New("open database failed: " + err.Error())
			connCh <- -1
			return
		}
		err = sqldb.Ping()
		if err != nil {
			err = errors.New("connect database failed: " + err.Error())
			connCh <- -1
			return
		}
		connCh <- 0
	}()

	select {
	case <-timeoutCtx.Done():
		sqldb = nil
		err = errors.New("connect to mysql timeout")
	case ret := <-connCh:
		if ret == 0 {
			fmt.Println("connect to mysql ok")
			err = nil
		}
	}

	return
}

func CreateSqlite3(dbname string) (sqldb *sql.DB, err error) {
	if !IsExist(dbname) {
		return nil, errors.New("open database failed: " + dbname + " not found")
	}
	sqldb, err = sql.Open("sqlite3", dbname)
	if err != nil {
		return nil, errors.New("open database failed: " + err.Error())
	}
	err = sqldb.Ping()
	if err != nil {
		return nil, errors.New("connect database failed: " + err.Error())
	}
	fmt.Println("connect to ", dbname, "ok")

	return
}

// note:暂时不使用oracle
func CreateOracle(dbstr string) (sqldb *sql.DB, err error) {
	// connCh := make(chan int)
	// timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second*SQL_TIMEOUT_S)
	// defer cancel()

	// go func() {
	// 	sqldb, err = sql.Open("oci8", dbstr)
	// 	if err != nil {
	// 		err = errors.New("open database failed: " + err.Error())
	// 		connCh <- -1
	// 		return
	// 	}
	// 	err = sqldb.Ping()
	// 	if err != nil {
	// 		err = errors.New("connect database failed: " + err.Error())
	// 		connCh <- -1
	// 		return
	// 	}
	// 	connCh <- 0
	// }()
	// // log.Println("connect to ", dbParam.server, dbParam.database, "ok")

	// select {
	// case <-timeoutCtx.Done():
	// 	sqldb = nil
	// 	err = errors.New("connect to oracle timeout")
	// case ret := <-connCh:
	// 	if ret == 0 {
	// 		fmt.Println("connect to oracle ok")
	// 		err = nil
	// 	}
	// }

	return
}

主程序如下:

package main

import (
	"database/sql"
	"fmt"
	"os"
)



var SQLDB *sql.DB // 默认以此为准
var DBType string

var ConfDBServer string = "root:123456@tcp(10.0.153.12:43306)/test?charset=utf8&interpolateParams=true&parseTime=true&loc=Local"

func initDB(dbfile string) {
	SQLDB, DBType = CreateSQLDb(ConfDBServer)
	if SQLDB == nil {
		os.Exit(0)
	}
}

var user_table_sql string = `CREATE TABLE user (
	id bigint(20) NOT NULL,
	email varchar(128) DEFAULT NULL,
	first_name varchar(128) DEFAULT NULL,
	last_name varchar(128) DEFAULT NULL,
	username varchar(128) DEFAULT NULL,
	PRIMARY KEY (id)
   ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
   `

var user_table_sql_1 string = `CREATE TABLE user (
	id bigint(20) NOT NULL,
	email varchar(128) DEFAULT NULL,
	first_name varchar(128) DEFAULT NULL,
	last_name varchar(128) DEFAULT NULL,
	username varchar(128) DEFAULT NULL,
   `
var user_table_sql_3 string = `
	PRIMARY KEY (id)
   ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
   `

/
func createDBTable(sqldb *sql.DB) {
	_, err := sqldb.Exec(user_table_sql)
	if err != nil {
		fmt.Printf("Exec sql failed: [%v] [%v] \n", err, user_table_sql)
	}
}

func createDBTableLong(sqldb *sql.DB) {

	count := 200
	mysql := ""

	for i := 0; i < count; i++ {
		mysql += fmt.Sprintf("username%v int DEFAULT NULL,", i)
	}
	realsql := user_table_sql_1 + mysql + user_table_sql_3

	// fmt.Println("dddd ", realsql)
	// return

	_, err := sqldb.Exec(realsql)
	if err != nil {
		fmt.Printf("Exec sql failed: [%v] [%v] \n", err, realsql)
	}

	for {
	// 死循环查询连接数
	}
}

func main() {
	fmt.Println("db test...")

	initDB(ConfDBServer)

	createDBTableLong(SQLDB)
}

在服务库中查询当前连接数:

mysql> SHOW PROCESSLIST;
+----+-----------------+---------------------+------+---------+------+------------------------+------------------+
| Id | User            | Host                | db   | Command | Time | State                  | Info             |
+----+-----------------+---------------------+------+---------+------+------------------------+------------------+
|  5 | event_scheduler | localhost           | NULL | Daemon  | 1529 | Waiting on empty queue | NULL             |
| 11 | root            | 192.168.208.1:49212 | test | Query   |    0 | init                   | SHOW PROCESSLIST |
| 21 | root            | 10.0.11.23:60858    | test | Sleep   |   23 |                        | NULL             |
+----+-----------------+---------------------+------+---------+------+------------------------+------------------+
3 rows in set (0.00 sec)

小结

限于时间,本文仅是开头,后续基于测试程序按需进行各种测试。计划将其作为数据库测试工具,可以用于国产化适配数据库选项,如:客户端库友好性(如上述代码,可直接用于MySQL、TiDB的连接);数据表支持最大列数量;一列的存储大小;等等。

参考

mysql官方配置说明:https://dev.mysql.com/doc/refman/8.1/en/server-system-variables.html

Logo

数据库是今天社会发展不可缺少的重要技术,它可以把大量的信息进行有序的存储和管理,为企业的数据处理提供了强大的保障。

更多推荐