目录

一拳搞定 Casbin | 不合理使用导致数据量连接池太多

本文介绍 casbin 使用过程中出现 too Many Connections 的情况

一、前言

在新的项目中采用casbin来对角色权限进行验证,角色和权限的关系保存在数据库,因为对 casbin 的代码不熟悉,直接使用了它官方提供的其他人的示例代码来跑的功能,导致每次都去创建casbin.Enforcer(会有数据库连接),所幸在本地开发过程中发现了这个问题及时纠正。

二、事情经过

2.1 问题描述

开发环境运行,每次页面刷新都会请求后端服务查询用户信息(没有加缓存,每次查询数据库),突然开始数据库报错

too Many Connections

2.2 排查

数据库连接池满,首先去查看数据库的连接池配置和项目中数据库连接池的配置。

2.2.1 数据库信息

数据库配置的连接池数量

1show variables like '%max_connections%';
2
3max_connections	128

如果要设置数据库连接的最大值呢?

1// 设置最大连接数 1000,但是如果数据库重启,就会失效,要永久有效需要配置到 MySQL 的配置文件
2set GLOBAL max_connections=1000;

查看连接进程

 1show processlist;
 2
 32313	root	localhost:50629		Sleep	53111
 42313	root	localhost:60621		Sleep	33149
 52313	root	localhost:50893		Sleep	22447
 6...
 72313	root	localhost:50609		Sleep	13143
 82314	root	localhost:50610	casbin	Sleep	13143
 9...
102315	root	localhost:56672	gdcloud	Query	0	starting	show processlist

可以看到连接有一大波的进程是处于 Sleep 状态,MySQL 被建立了很多连接但是没有使用

2.2.2 项目 Gorm 数据库配置

 1func gormSetting(db *gorm.DB) {
 2	// 不需要给表加S
 3	db.SingularTable(true)
 4	// 连接池配置
 5	db.DB().SetMaxIdleConns(10)
 6	db.DB().SetMaxOpenConns(100)
 7	db.DB().SetConnMaxLifetime(time.Hour)
 8	// 打开日志
 9	db.LogMode(true)
10	db.Callback().Create().Replace("gorm:update_time_stamp", updateTimeStampForCreateCallback)
11	// 设置表名前缀
12	gorm.DefaultTableNameHandler = func(db *gorm.DB, defaultTableName string) string {
13		return "gdcloud_" + defaultTableName
14	}
15}
  • 最大打开连接数 100
  • 最大闲置连接数 10
  • 连接最大保持时间 1小时

但是 MySQL 的 process 是大于 100的,并且观察请求日志因为涉及到权限,每次请求都会走 casbin 的代码访问,因为项目中除了 gorm 外就 casbin 和数据库有交互。

2.2.3 Casbin 持久化逻辑

代码从其它列子抄来的,未曾琢磨

连接

 1// Persisten to db.
 2// %s:%s@(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local
 3func Casbin() (c *casbin.Enforcer, err error) {
 4	if a, err := gormadapter.NewAdapter("mysql", "root:root@(127.0.0.1:3306)/"); err != nil {
 5		return nil, err
 6	} else {
 7		if e, err := casbin.NewEnforcer("pkg/config/rbac_model.conf", a, true); err != nil {
 8			return nil, err
 9		} else {
10			e.LoadPolicy()
11			return e, nil
12		}
13	}
14}

增加策略

1// Add casbin policy.
2func AddPolicy(p permissionModel) (bool, error) {
3	if e, err := Casbin(); err != nil {
4		return false, err
5	} else {
6		return e.AddPolicy(p.V0, p.V1, p.V2)
7	}
8}

验证权限

 1func AuthCheckRole(c *gin.Context) {
 2	//根据上下文获取 claims 从 claims 获得 roles
 3	claims := c.MustGet("claims").(*jwt.Customclaims)
 4	role := claims.Roles
 5	if e, err := casbin.Casbin(); err != nil {
 6	......
 7	}
 8
 9	......
10}

看了下代码,就猜到是 Casbin 的问题了,因为每次页面请求都会验证角色权限,而每次都会调用casbin.Casbin(),它又是调用到了 gormadapter.NewAdapter(),每一次都会New的话就很可能占用了一次连接。继续跟进去:

 1func NewAdapter(driverName string, dataSourceName string, dbSpecified ...bool) (*Adapter, error) {
 2	a := &Adapter{}
 3	a.driverName = driverName
 4	a.dataSourceName = dataSourceName
 5
 6	......
 7
 8	// Open the DB, create it if not existed.
 9	err := a.open()
10
11	......
12
13	return a, nil
14}

可以看到这里有 a.open() 操作,写着打开一个 DB,点进去看:

 1func (a *Adapter) open() error {
 2	var err error
 3	var db *gorm.DB
 4
 5	if a.dbSpecified {
 6		db, err = openDBConnection(a.driverName, a.dataSourceName)
 7		if err != nil {
 8			return err
 9		}
10	} else {
11		if err = a.createDatabase(); err != nil {
12			return err
13		}
14		if a.driverName == "postgres" {
15			db, err = openDBConnection(a.driverName, a.dataSourceName+" dbname=casbin")
16		} else if a.driverName == "sqlite3" {
17			db, err = openDBConnection(a.driverName, a.dataSourceName)
18		} else {
19			db, err = openDBConnection(a.driverName, a.dataSourceName+"casbin")
20		}
21		if err != nil {
22			return err
23		}
24	}
25	a.db = db
26	return a.createTable()
27}

看到这里的openDBConnection()就彻底明白了,*gorm.DB对象每一次都会创建,每一次创建都会建立连接,最终导致 MySQL 连接满。

2.2.4 改进

每次连接改成创建的对象的时候创建一次连接,后面共用 *gorm.DB。

 1type Permission struct {
 2	adapter    *gormadapter.Adapter
 3	enforcer   *casbin.Enforcer
 4	configPath string
 5}
 6
 7func NewPermission(dbModel config.DbConfig, path string) *Permission {
 8	p := &Permission{
 9		configPath: path, // "pkg/config/rbac_model.conf"
10	}
11	connArgs := fmt.Sprintf("%s:%s@(%s:%s)/", dbModel.Username, dbModel.Password, dbModel.Host, dbModel.Port)
12
13	if a, err := gormadapter.NewAdapter("mysql", connArgs); err != nil {
14		panic(err)
15	} else {
16		p.adapter = a
17
18		if e, err := casbin.NewEnforcer(p.configPath, p.adapter, true); err != nil {
19			panic(err)
20		} else {
21			p.enforcer = e
22		}
23	}
24
25	return p
26}
27
28func GetPermission() *Permission {
29	onceNew.Do(func() {
30		PermissionService = NewPermission(config.GetMustConfig().Orgrimmar.DbConfig, config.GetMustConfig().Orgrimmar.CasbinPath)
31	})
32
33	return PermissionService
34}
35
36// 增加权限
37func (p *Permission) AddPermissionParams(params ...string) (bool, error) {
38	var pm PermissionModel
39	l := len(params)
40	switch {
41	case l == 4 && params[0] == "p":
42		pm = p.make3PermissionModel("p", params[0], params[1], params[2])
43	case l == 3 && params[0] == "g":
44		pm = p.make2PermissionModel("g", params[0], params[1])
45	default:
46		return false, nil
47	}
48
49	return p.AddPermission(pm)
50}
51
52// Check 验证权限
53func (p *Permission) Check(p1, p2, p3 string) (bool, error) {
54	return p.enforcer.Enforce(p1, p2, p3)
55}
56
57// RemovePolicy 删除策略 传入 p 对应的 v0 v1 v2 参数
58func (p *Permission) RemovePolicy(params ...string) (bool, error) {
59	return p.enforcer.RemovePolicy(params)
60}
61
62......

因为暂时配置目前不需要动态刷新,所以直接共享了下面两个变量:

1adapter    *gormadapter.Adapter
2enforcer   *casbin.Enforcer

后面对casbin的操作都基于了enforcer即可。

本文中的代码存在 if-else 嵌套的存在不符合 go 的编码规范,仅供参考。

三、参考

https://github.com/casbin/casbin

https://darjun.github.io/2020/06/12/godailylib/casbin/

https://casbin.org/zh-CN/

https://www.kancloud.cn/oldlei/casbin/1289450