forked from XiaoMi/soar
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathenv.go
587 lines (514 loc) · 18.6 KB
/
env.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
/*
* 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
}