/* * Copyright 2018 Xiaomi, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package env import ( "fmt" "strings" "time" "github.com/XiaoMi/soar/ast" "github.com/XiaoMi/soar/common" "github.com/XiaoMi/soar/database" "github.com/dchest/uniuri" "vitess.io/vitess/go/vt/sqlparser" ) // VirtualEnv SQL优化评审 测试环境 // DB使用的信息从配置文件中获取 type VirtualEnv struct { *database.Connector // 保存 DB 测试环境映射关系,防止 vEnv 环境冲突。 DBRef map[string]string // db -> optimizer_xxx Hash2DB map[string]string // optimizer_xxx -> db // 保存 Table 创建关系,防止重复创建表 TableMap map[string]map[string]string // 错误 Error error } // NewVirtualEnv 初始化一个新的测试环境 func NewVirtualEnv(vEnv *database.Connector) *VirtualEnv { return &VirtualEnv{ Connector: vEnv, DBRef: make(map[string]string), Hash2DB: make(map[string]string), TableMap: make(map[string]map[string]string), } } // BuildEnv 测试环境初始化&连接线上环境检查 // @output *VirtualEnv 测试环境 // @output *database.Connector 线上环境连接句柄 func BuildEnv() (*VirtualEnv, *database.Connector) { connTest, err := database.NewConnector(common.Config.TestDSN) common.LogIfError(err, "") // 生成测试环境 vEnv := NewVirtualEnv(connTest) // 检查测试环境可用性,并记录数据库版本 vEnvVersion, err := vEnv.Version() common.Config.TestDSN.Version = vEnvVersion if err != nil { common.Log.Warn("BuildEnv TestDSN: %s:********@%s/%s not available , Error: %s", vEnv.User, vEnv.Addr, vEnv.Database, err.Error()) common.Config.TestDSN.Disable = true } // 连接线上环境 // 如果未配置线上环境线测试环境配置为线上环境 if common.Config.OnlineDSN.User == "" { common.Log.Warn("BuildEnv AllowOnlineAsTest: OnlineDSN not config, use TestDSN: %s:********@%s/%s as OnlineDSN", vEnv.User, vEnv.Addr, vEnv.Database) common.Config.OnlineDSN = common.Config.TestDSN } connOnline, err := database.NewConnector(common.Config.OnlineDSN) common.LogIfError(err, "") // 检查线上环境可用性版本 rEnvVersion, err := connOnline.Version() common.Config.OnlineDSN.Version = rEnvVersion if err != nil { common.Log.Warn("BuildEnv OnlineDSN: %s:********@%s/%s not available , Error: %s", connOnline.User, connOnline.Addr, connOnline.Database, err.Error()) common.Config.TestDSN.Disable = true } // 检查是否允许 Online 和 Test 一致,防止误操作 if common.FormatDSN(common.Config.OnlineDSN) == common.FormatDSN(common.Config.TestDSN) && !common.Config.AllowOnlineAsTest { common.Log.Warn("BuildEnv AllowOnlineAsTest: %s:********@%s/%s OnlineDSN can't config as TestDSN", vEnv.User, vEnv.Addr, vEnv.Database) common.Config.TestDSN.Disable = true common.Config.OnlineDSN.Disable = true } // 是否禁用版本检测,禁用后,不再对比测试环境和线上环境的版本大小 if !common.Config.DisableVersionCheck { // 判断测试环境与线上环境版本是否一致,要求测试环境版本不低于线上环境 if vEnvVersion < rEnvVersion { common.Log.Warning("TestDSN MySQL version older than OnlineDSN(%d), TestDSN(%d) will not be used", rEnvVersion, vEnvVersion) common.Config.TestDSN.Disable = true } } return vEnv, connOnline } // RealDB 从测试环境中获取通过 hash 后的 DB func (vEnv *VirtualEnv) RealDB(hash string) string { if _, ok := vEnv.Hash2DB[hash]; ok { return vEnv.Hash2DB[hash] } // hash may be real database name not hash if strings.HasPrefix(hash, "optimizer_") { common.Log.Warning("RealDB, Hash2DB missing hash map: %s", hash) } return hash } // DBHash 从测试环境中根据 DB 找到对应的 hash 值 func (vEnv *VirtualEnv) DBHash(db string) string { if _, ok := vEnv.DBRef[db]; ok { return vEnv.DBRef[db] } return db } // CleanUp 环境清理 func (vEnv *VirtualEnv) CleanUp() bool { if !common.Config.TestDSN.Disable && common.Config.DropTestTemporary { common.Log.Debug("CleanUp ...") for db := range vEnv.Hash2DB { _, err := vEnv.Query(fmt.Sprintf("drop database %s", db)) if err != nil { common.Log.Error("CleanUp failed Error: %s", err) return false } } // cleanup hash map vEnv.DBRef = make(map[string]string) vEnv.Hash2DB = make(map[string]string) vEnv.TableMap = make(map[string]map[string]string) common.Log.Debug("CleanUp, done") } return true } // CleanupTestDatabase 清除一小时前的环境 func (vEnv *VirtualEnv) CleanupTestDatabase() { common.Log.Debug("CleanupTestDatabase ...") dbs, err := vEnv.Query("show databases like 'optimizer%%'") if err != nil { common.Log.Error("CleanupTestDatabase failed Error:%s", err.Error()) return } // TODO: 1 hour should be config-able minHour := 1 for dbs.Rows.Next() { var testDatabase string err = dbs.Rows.Scan(&testDatabase) if err != nil { break } // test temporary database format `optimizer_YYMMDDHHmmss_randomString(16)` if len(testDatabase) != 39 { common.Log.Debug("CleanupTestDatabase by pass %s", testDatabase) continue } s := strings.Split(testDatabase, "_") pastTime, err := time.ParseInLocation("060102150405", s[1], time.Local) if err != nil { common.Log.Error("CleanupTestDatabase compute pastTime Error: %s", err.Error()) continue } subHour := time.Since(pastTime).Hours() if subHour > float64(minHour) { if _, err := vEnv.Query(fmt.Sprintf("drop database %s", testDatabase)); err != nil { common.Log.Error("CleanupTestDatabase failed Error: %s", err.Error()) continue } common.Log.Debug("CleanupTestDatabase drop database %s success", testDatabase) continue } common.Log.Debug("CleanupTestDatabase by pass database %s, %.2f less than %d hours", testDatabase, subHour, minHour) } err = dbs.Rows.Close() common.LogIfError(err, "") common.Log.Debug("CleanupTestDatabase done") } // ChangeDB use db change dsn Database func ChangeDB(env *database.Connector, sql string) { stmt, err := sqlparser.Parse(sql) if err != nil { return } switch stmt := stmt.(type) { case *sqlparser.Use: if stmt.DBName.String() != "" { env.Database = stmt.DBName.String() } } } func CurrentDB(sql, db string) string { stmt, err := sqlparser.Parse(sql) if err != nil { return common.Config.TestDSN.Schema } switch stmt := stmt.(type) { case *sqlparser.Use: if stmt.DBName.String() != "" { db = stmt.DBName.String() } } if db == "" { db = common.Config.TestDSN.Schema } return db } // BuildVirtualEnv rEnv 为 SQL 源环境,DB 使用的信息从接口获取 // 注意:如果是 USE, DDL 等语句,执行完第一条就会返回,后面的 SQL 不会执行 func (vEnv *VirtualEnv) BuildVirtualEnv(rEnv *database.Connector, SQLs ...string) bool { var stmt sqlparser.Statement var err error // 置空错误信息 vEnv.Error = nil // 检测是否已经创建初始数据库,如果未创建则创建一个名称 hash 过的映射数据库 err = vEnv.createDatabase(rEnv) common.LogIfWarn(err, "") // 测试环境检测 if common.Config.TestDSN.Disable { common.Log.Info("BuildVirtualEnv TestDSN not config") return true } // 判断 rEnv 中是否指定了 DB if rEnv.Database == "" { common.Log.Error("BuildVirtualEnv no database specified, TestDSN init failed") return false } // 库表提取 meta := make(map[string]*common.DB) for _, sql := range SQLs { common.Log.Debug("BuildVirtualEnv Database&TableName Mapping, SQL: %s", sql) stmt, err = sqlparser.Parse(sql) if err != nil { common.Log.Error("BuildVirtualEnv Error : %v", err) return false } // 语句类型判断 switch stmt := stmt.(type) { case *sqlparser.Use: // 如果是use语句,则更改基础环配置 if _, ok := meta[stmt.DBName.String()]; !ok { // 如果USE了一个线上环境不存在的数据库,将创建该数据库 meta[stmt.DBName.String()] = common.NewDB(stmt.DBName.String()) rEnv.Database = stmt.DBName.String() // use DB 后检查 DB是否已经创建,如果没有创建则创建DB err = vEnv.createDatabase(rEnv) common.LogIfWarn(err, "") } return true case *sqlparser.DDL: // 如果是DDL,则先获取DDL对应的表结构,然后直接在测试环境接执行SQL // 为不影响其他SQL操作,复制一个Connector对象,将数据库切换到对应的DB上直接执行 vEnv.Database = vEnv.DBRef[rEnv.Database] // 为了支持并发,需要将DB进行映射,但 db.table 这种形式无法保证 DB 的映射是正确的 // TODO:暂不支持 create db.tableName (id int) 形式的建表语句 if stmt.Table.Qualifier.String() != "" { common.Log.Error("BuildVirtualEnv DDL Not support db.tb format") return false } for _, tb := range stmt.FromTables { if tb.Qualifier.String() != "" { common.Log.Error("BuildVirtualEnv DDL Not support db.tb format") return false } } for _, tb := range stmt.ToTables { if tb.Qualifier.String() != "" { common.Log.Error("BuildVirtualEnv DDL Not support db.tb format") return false } } // 拉取表结构 table := stmt.Table.Name.String() if table != "" { err = vEnv.createTable(rEnv, table) // 这里如果报错可能有两种可能: // 1. SQL 是 Create 语句,线上环境并没有相关的库表结构 // 2. 在测试环境中执行 SQL 报错 // 如果是因为 Create 语句报错,后续会在测试环境中直接执行 create 语句,不会对程序有负面影响 // 如果是因为执行 SQL 报错,那么其他地方执行 SQL 的时候也一定会报错 // 所以这里不需要 `return false`,可以继续执行 if err != nil { common.Log.Warning("BuildVirtualEnv Error : %v", err) } } _, err = vEnv.Query(sql) if err != nil { switch stmt.Action { case "create", "alter": // 如果是创建或者修改语句,且报错信息为如重复建表、重复索引等信息,将错误反馈到上一次层输出建议 vEnv.Error = err default: common.Log.Error("BuildVirtualEnv DDL Execute Error : %v", err) } } return true } meta := ast.GetMeta(stmt, nil) // 由于 DB 环境可能是变的,所以需要每一次都单独的提取库表结构,整体随着 rEnv 的变动而发生变化 for db, table := range meta { if db == "" { db = rEnv.Database } rEnv.Database = db // 创建数据库环境 for _, tb := range table.Table { if tb.TableName == "" { continue } // 视图检查 common.Log.Debug("BuildVirtualEnv Checking view -- %s.%s", rEnv.Database, tb.TableName) tbStatus, err := rEnv.ShowTableStatus(tb.TableName) if err != nil { common.Log.Error("BuildVirtualEnv ShowTableStatus Error : %v", err) return false } // 如果是视图,解析语句 if len(tbStatus.Rows) > 0 && string(tbStatus.Rows[0].Comment) == "VIEW" { var viewDDL string viewDDL, err = rEnv.ShowCreateTable(tb.TableName) if err != nil { common.Log.Error("BuildVirtualEnv create view failed: %v", err) return false } startIdx := strings.Index(viewDDL, "AS") if startIdx < 0 || viewDDL == "" { common.Log.Error("BuildVirtualEnv '%s' got '%s', Index: %d", tb.TableName, viewDDL, startIdx) return false } viewDDL = viewDDL[startIdx+2:] if !vEnv.BuildVirtualEnv(rEnv, viewDDL) { return false } } err = vEnv.createTable(rEnv, tb.TableName) if err != nil { common.Log.Error("BuildVirtualEnv %s.%s Error : %v", rEnv.Database, tb.TableName, err) return false } } } } return true } func (vEnv *VirtualEnv) createDatabase(rEnv *database.Connector) error { // 生成映射关系 if _, ok := vEnv.DBRef[rEnv.Database]; ok { common.Log.Debug("createDatabase, Database `%s` has created, mapping from `%s`", vEnv.DBRef[rEnv.Database], rEnv.Database) return nil } // optimizer_YYMMDDHHmmss_xxxx dbHash := fmt.Sprintf("optimizer_%s_%s", // Total 39 bytes time.Now().Format("060102150405"), // 12 Bytes 180102030405 strings.ToLower(uniuri.New())) // 16 Bytes random string common.Log.Debug("createDatabase, mapping `%s` :`%s`-->`%s`", rEnv.Database, rEnv.Database, dbHash) ddl, err := rEnv.ShowCreateDatabase(rEnv.Database) if err != nil { common.Log.Warning("createDatabase, rEnv.ShowCreateDatabase Error : %v", err) ddl = fmt.Sprintf("create database `%s` character set %s", rEnv.Database, rEnv.Charset) } ddl = strings.Replace(ddl, rEnv.Database, dbHash, -1) if ddl == "" { return fmt.Errorf("dbName: '%s' get create info error", rEnv.Database) } res, err := vEnv.Query(ddl) if err != nil { common.Log.Warning("createDatabase, Error : %v", err) return err } err = res.Rows.Close() common.LogIfWarn(err, "") // 创建成功,添加映射记录 vEnv.DBRef[rEnv.Database] = dbHash vEnv.Hash2DB[dbHash] = rEnv.Database return nil } /* @input: database.Connector 为一个线上环境数据库连接句柄的复制,因为在处理SQL时需要对上下文进行关联处理, 所以存在修改DB连接参数(主要是数据库名称变更)的可能性,为了不影响整体上下文的环境,所以需要一个镜像句柄来做当前环境的操作。 dbName, tbName: 需要在环境中操作的库表名称, @output: return 执行过程中的错误 NOTE: 该函数会将线上环境中使用到的库表结构复制到测试环境中,为后续操作提供基础环境。 传入的库表名称均来自于对AST的解析,库表名称的获取遵循以下原则: 如果未在SQL中指定数据库名称,则数据库一定是配置文件(或命令行参数传入DSN)中指定的数据库 如果一个SQL中存在多个数据库,则只能有一个数据库是没有在SQL中被显示指定的(即DSN中指定的数据库) TODO: 在一些可能的情况下,由于数据库配置的不一致(如SQL_MODE不同)导致remote环境的库表无法正确的在测试环境进行同步, soar 能够做出判断并进行 session 级别的修改,但是这一阶段可用性保证应该是由用户提供两个完全相同(或测试环境兼容线上环境) 的数据库环境来实现的。 */ func (vEnv *VirtualEnv) createTable(rEnv *database.Connector, tbName string) error { // 判断数据库是否已经创建 if vEnv.DBRef[rEnv.Database] == "" { // 若没创建,则创建数据库 err := vEnv.createDatabase(rEnv) if err != nil { return err } } if vEnv.TableMap[rEnv.Database] == nil { vEnv.TableMap[rEnv.Database] = make(map[string]string) } if strings.ToLower(tbName) == "dual" { common.Log.Debug("createTable, %s no need create", tbName) return nil } if vEnv.TableMap[rEnv.Database][tbName] != "" { common.Log.Debug("createTable, `%s`.`%s` has created, mapping from `%s`.`%s`", vEnv.DBRef[rEnv.Database], tbName, rEnv.Database, tbName) return nil } common.Log.Debug("createTable, Database: %s, TableName: %s", vEnv.DBRef[rEnv.Database], tbName) // TODO:查看是否有外键关联(done),对外键的支持 (未解决循环依赖的问题) // 记录Table创建信息 vEnv.TableMap[rEnv.Database][tbName] = tbName // 生成建表语句 common.Log.Debug("createTable DSN(%s/%s): generate ddl", rEnv.Addr, rEnv.Database) ddl, err := rEnv.ShowCreateTable(tbName) if err != nil { // 有可能是用户新建表,因此线上环境查不到 common.Log.Error("createTable, %s DDL Error : %v", tbName, err) return err } // 改变数据环境 vEnv.Database = vEnv.DBRef[rEnv.Database] res, err := vEnv.Query(ddl) if err != nil { // 有可能是用户新建表,因此线上环境查不到 common.Log.Error("createTable: %s Error : %v", tbName, err) return err } err = res.Rows.Close() common.LogIfWarn(err, "") // 泵取数据 if common.Config.Sampling { common.Log.Debug("createTable, Start Sampling data from %s.%s to %s.%s ...", rEnv.Database, tbName, vEnv.DBRef[rEnv.Database], tbName) err = vEnv.SamplingData(rEnv, tbName) } return err } // GenTableColumns 为 Rewrite 提供的结构体初始化 func (vEnv *VirtualEnv) GenTableColumns(meta common.Meta) common.TableColumns { tableColumns := make(common.TableColumns) for dbName, db := range meta { for _, tb := range db.Table { // 防止传入非预期值 if tb == nil { break } td, err := vEnv.Connector.ShowColumns(tb.TableName) if err != nil { common.Log.Warn("GenTableColumns, ShowColumns Error: " + err.Error()) break } // tableColumns 初始化 if dbName == "" { dbName = vEnv.RealDB(vEnv.Connector.Database) } if _, ok := tableColumns[dbName]; !ok { tableColumns[dbName] = make(map[string][]*common.Column) } if _, ok := tableColumns[dbName][tb.TableName]; !ok { tableColumns[dbName][tb.TableName] = make([]*common.Column, 0) } if len(tb.Column) == 0 { // tb.column 为空说明 SQL 里这个表是用的*来查询 if err != nil { common.Log.Error("ast.Rewrite ShowColumns, Error: %v", err) break } for _, colInfo := range td.DescValues { tableColumns[dbName][tb.TableName] = append(tableColumns[dbName][tb.TableName], &common.Column{ Name: colInfo.Field, DB: dbName, Table: tb.TableName, DataType: colInfo.Type, Character: string(colInfo.Collation), Key: colInfo.Key, Default: string(colInfo.Default), Extra: colInfo.Extra, Comment: colInfo.Comment, Privileges: colInfo.Privileges, Null: colInfo.Null, }) } } else { // tb.column如果不为空则需要把使用到的列填写进去 var columns []*common.Column for _, col := range tb.Column { for _, colInfo := range td.DescValues { if col.Name == colInfo.Field { // 根据获取的信息将列的信息补全 col.DB = dbName col.Table = tb.TableName col.DataType = colInfo.Type col.Character = string(colInfo.Collation) col.Key = colInfo.Key col.Default = string(colInfo.Default) col.Extra = colInfo.Extra col.Comment = colInfo.Comment col.Privileges = colInfo.Privileges col.Null = colInfo.Null columns = append(columns, col) break } } } tableColumns[dbName][tb.TableName] = columns } } } return tableColumns }