一拳搞定 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