2024年4月

一、引入

在Go语言的项目开发中,为了提高代码的可测试性和可维护性,我们通常会采用依赖注入(
Dependency Injection
,简称DI)的设计模式。依赖注入可以让高层模块不依赖底层模块的具体实现,而是通过抽象来互相依赖,从而使得模块之间的耦合度降低,系统的灵活性和可扩展性增强。

二、控制反转与依赖注入

控制反转(
Inversion of Control
,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(
Dependency Injection
,简称DI)。依赖注入是生成灵活和松散耦合代码的标准技术,通过明确地向组件提供它们所需要的所有依赖关系。在 Go 中通常采用将依赖项作为参数传递给构造函数的形式:

构造函数
NewUserRepository
在创建
UserRepository
时需要从外部将依赖项
db
作为参数传入,我们在
UserRepository
中无需关注
db
的创建逻辑,实现了代码解耦。

// NewUserRepository 创建BookRepo的构造函数
func NewUserRepository(db *gorm.DB) *UserRepository {
	return &UserRepository{db: db}
}

区别于控制反转,如果在
NewUserRepository
函数中自行创建相关依赖,这将导致代码高度耦合并且难以维护和调试。

// NewUserRepository 创建UserRepository的构造函数
func NewUserRepository() *UserRepository {
  db, _ := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{})
	return &UserRepository{db: db}
}

三、为什么需要依赖注入工具

3.1 示例

如果上面示例代码不够清晰的话,我们来看这两段代码:

// NewUserRepositoryV1非依赖注入的写法
func NewUserRepositoryV1(dbCfg DBConfig, c CacheConfig)*UserRepository{
    db, err := gorm.Open(mysql.Open(dbcfg.DSN))
    if err != nil {
        panic(err)
    }
    ud = dao.NewUserDAO(db)
    uc = cache.NewUserCache(redis.NewClient(&redis.Options{
            Addr: c.Addr,
    }))
    return &UserRepository{
        dao: ud,
        cache: uc,
    }
}

// NewUserRepository 依赖注入的写法
func NewUserRepository(d *dao.UserDAO, c *cache.UserCache)*UserRepository{
    return &UserRepository{
        dao: d,
        cache: c,
    }
}

可以清楚地看到,这两段代码展示了在Go语言中实现依赖注入的两种不同方式。
第一段代码
NewUserRepositoryV1
是非依赖注入的写法。在这个函数中,
UserRepository
的依赖(
db

cache
)是在函数内部创建的。这种方式的问题在于,它违反了单一职责原则,因为
NewUserRepositoryV1
不仅负责创建
UserRepository
实例,还负责创建其依赖的数据库和缓存客户端。这样做会导致代码耦合度较高,难以测试和维护。
第二段代码
NewUserRepository
是依赖注入的写法。这个函数接受
UserRepository
的依赖(
*dao.UserDAO

*cache.UserCache
)作为参数,而不是在函数内部创建它们。这种方式使得
UserRepository
的创建与它的依赖解耦,更容易测试,因为你可以轻松地为
UserRepository
提供模拟的依赖项。此外,这种写法也更符合依赖注入的原则,因为它将控制反转给了调用者,由调用者来决定
UserRepository
实例化时使用哪些依赖项。

3.2 依赖注入写法与非依赖注入写法

依赖注入写法
:不关心依赖是如何构造的。

非依赖注入写法
:必须自己初始化依赖,比如说
Repository
需要知道如何初始化
DAO

Cache
。由此带来的缺点是:

  • 深度耦合依赖的初始化过程。
  • 往往需要定义额外的
    Config
    类型来传递依赖所需的配置信息。
  • 一旦依赖增加新的配置,或者更改了初始化过程,都要跟着修改。
  • 缺乏扩展性。
  • 测试不友好。
  • 难以复用公共组件,例如 DB 或 Redis 之类的客户端。

四、wire 工具介绍与安装

4.1 wire 基本介绍

  • Wire
    是一个的 Google 开源专为依赖注入(
    Dependency Injection
    )设计的代码生成工具,通过自动生成代码的方式在
    初始编译过程中
    完成依赖注入。它可以自动生成用于化各种依赖关系的代码,从而帮助我们更轻松地管理和注入依赖关系。

  • Wire
    分成两部分,一个是在项目中使用的依赖, 一个是命令行工具。

4.2 安装

go install github.com/google/wire/cmd/wire@latest

五、Wire 的基本使用

5.1 前置代码准备

目录结构如下:

wire
├── db.go                          # 数据库相关代码
├── go.mod                         # Go模块依赖配置文件
├── go.sum                         # Go模块依赖校验文件
├── main.go                        # 程序入口文件
├── repository                     # 存放数据访问层代码的目录
│   ├── dao                        # 数据访问对象(DAO)目录
│   │   └── user.go                # 用户相关的DAO实现
│   └── user.go                    # 用户仓库实现
├── wire.go                        # Wire依赖注入配置文件

repository/dao/user.go
文件:

// repository/dao/user.go
package dao

import "gorm.io/gorm"

type UserDAO struct {
	db *gorm.DB
}

func NewUserDAO(db *gorm.DB) *UserDAO {
	return &UserDAO{
		db: db,
	}
}

repository/user.go
文件:

// repository/user.go
package repository

import "wire/repository/dao"

type UserRepository struct {
	dao *dao.UserDAO
}

func NewUserRepository(dao *dao.UserDAO) *UserRepository {
	return &UserRepository{
		dao: dao,
	}
}

db.go
文件:

// db.go
package wire

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

func InitDB() *gorm.DB {
	db, err := gorm.Open(mysql.Open("dsn"))
	if err != nil {
		panic(err)
	}
	return db
}

main.go
文件:

package wire

import (
	"fmt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"wire/repository"
	"wire/repository/dao"
)

func main() {
	// 非依赖注入
	db, err := gorm.Open(mysql.Open("dsn"))
	if err != nil {
		panic(err)
	}
	ud := dao.NewUserDAO(db)
	repo := repository.NewUserRepository(ud)
	fmt.Println(repo)
}

5.2 使用 Wire 工具生成代码

现在我们已经有了基本的代码结构,接下来我们将使用
wire
工具来生成依赖注入的代码。

首先,确保你已经安装了
wire
工具。如果没有安装,可以使用以下命令安装:

go get github.com/google/wire/cmd/wire

接下来,我们需要创建一个
wire
的配置文件,通常命名为
wire.go
。在这个文件中,我们将使用
wire
的语法来指定如何构建
UserRepository
实例。

wire.go
文件:

//go:build wireinject

// 让 wire 来注入这里的代码
package wire

import (
	"github.com/google/wire"
	"wire/repository"
	"wire/repository/dao"
)

func InitRepository() *repository.UserRepository {
	// 我只在这里声明我要用的各种东西,但是具体怎么构造,怎么编排顺序
	// 这个方法里面传入各个组件的初始化方法
	wire.Build(InitDB, repository.NewUserRepository, dao.NewUserDAO)
	return new(repository.UserRepository)
}

这段代码是使用
wire
工具进行依赖注入的配置文件。在这个文件中,我们定义了一个函数
InitRepository
,这个函数的目的是为了生成一个
*repository.UserRepository
的实例。但是,这个函数本身并不包含具体的实现代码,而是依赖于
wire
工具来注入依赖。
让我们逐步解释这段代码:

  1. 构建约束指令
    :

    //go:build wireinject
    

    这行注释是一个构建约束,它告诉
    go build
    只有在满足条件
    wireinject
    的情况下才应该构建这个文件。
    wireinject
    是一个特殊的标签,用于指示
    wire
    工具处理这个文件。

  2. 导入包
    :

    import (
        "github.com/google/wire"
        "wire/repository"
        "wire/repository/dao"
    )
    

    这部分导入了必要的包,包括
    wire
    工具库,以及项目中的
    repository

    dao
    包,这些包包含了我们需要注入的依赖。

  3. InitRepository 函数
    :

    func InitRepository() *repository.UserRepository {
        // 我只在这里声明我要用的各种东西,但是具体怎么构造,怎么编排顺序
        // 这个方法里面传入各个组件的初始化方法
        wire.Build(InitDB, repository.NewUserRepository, dao.NewUserDAO)
        return new(repository.UserRepository)
    }
    

    这个函数是
    wire
    注入的目标。它声明了一个返回
    *repository.UserRepository
    的函数,但是函数体内部没有具体的实现代码。
    wire.Build
    函数调用是关键, 主要是连接或绑定我们之前定义的所有初始化函数。当我们运行
    wire
    工具来生成代码时,它就会根据这些依赖关系来自动创建和注入所需的实例。

    ,这些函数按照依赖关系被调用,以正确地构造和注入
    UserRepository
    实例所需的依赖。


    • InitDB
      是初始化数据库连接的函数。
    • repository.NewUserRepository
      是创建
      UserRepository
      实例的函数。
    • dao.NewUserDAO
      是创建
      UserDAO
      实例的函数。
      wire
      工具会自动生成这些函数调用的代码,并确保依赖关系得到满足。
  4. 返回语句
    :

    return new(repository.UserRepository)
    

    这个返回语句是必须的,尽管它实际上并不会被执行。
    wire
    工具会生成一个替换这个函数体的代码,其中包括所有必要的依赖注入逻辑。
    在编写完
    wire.go
    文件后,你需要运行
    wire
    命令来生成实际的依赖注入代码。生成的代码将被放在一个名为
    wire_gen.go
    的文件中,这个文件应该被提交到你的版本控制系统中。

现在,我们可以运行
wire
命令来生成依赖注入的代码:

wire

这个命令会扫描
wire.go
文件,并生成一个新的 Go 文件
wire_gen.go
,其中包含了
InitializeUserRepository
函数的实现,这个函数会创建并返回一个
UserRepository
实例,其依赖项已经自动注入。

生成
wire_gen.go
文件,内容如下所示:

// Code generated by Wire. DO NOT EDIT.

//go:generate go run -mod=mod github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package wire

import (
	"wire/repository"
	"wire/repository/dao"
)

// Injectors from wire.go:

func InitRepository() *repository.UserRepository {
	db := InitDB()
	userDAO := dao.NewUserDAO(db)
	userRepository := repository.NewUserRepository(userDAO)
	return userRepository
}

最后,我们需要修改
main.go
文件,使用
wire
生成的代码来获取
UserRepository
实例:

package wire

func main() {
	InitRepository()
}

现在,当我们运行
main.go
时,它将使用
wire
工具生成的代码来初始化
UserRepository
,包括其依赖的
UserDAO
和数据库连接。这样,我们就实现了依赖注入,并且代码更加简洁、易于维护。

六、Wire 核心技术

5.1 抽象语法树分析

wire
工具的工作原理是基于对Go代码的抽象语法树(Abstract Syntax Tree,简称AST)的分析。AST是源代码的抽象语法结构的树状表示,它以树的形式表现编程语言的语法结构。
wire
工具通过分析AST来理解代码中的依赖关系。
在Go中,
go/ast
包提供了解析Go源文件并构建AST的功能。
wire
工具利用这个包来遍历和分析项目的Go代码,识别出所有的依赖项,并构建出依赖关系图。这个依赖关系图随后被用来生成注入依赖的代码。

5.2 模板编程

wire
工具生成代码的过程也涉及到模板编程。模板编程是一种编程范式,它允许开发者定义一个模板,然后使用具体的数据来填充这个模板,生成最终的代码或文本。

wire
中,虽然不直接使用Go语言的模板引擎(如
text/template

html/template
),但它的工作原理与模板编程类似。
wire
定义了一套自己的语法来描述依赖关系,然后根据这些描述生成具体的Go代码。
wire
的语法主要包括以下几个部分:

  • wire.NewSet
    :定义一组相关的依赖,通常包括一个或多个构造函数。
  • wire.Build
    :指定生成代码时应该使用哪些依赖集合。
  • bind
    函数:用于绑定接口和实现,告诉
    wire
    如何创建接口的实例。
    wire
    工具通过这些语法来构建一个依赖图,然后根据这个图生成一个函数,该函数负责创建并返回所有必要的组件实例,同时处理它们之间的依赖关系。
    通过结合抽象语法树分析和模板编程,
    wire
    工具能够提供一种声明式的依赖注入方法,让开发者能够专注于定义依赖关系,而不是手动编写依赖注入的代码。这不仅减少了重复劳动,还提高了代码的可维护性和降低了出错的可能性。

七、Wire 的核心概念

7.1 两个核心概念


wire
中,有两个核心概念:提供者(providers)和注入器(injectors)。

7.2 Wire 提供者(providers)

提供者
是一个普通有返回值的 Go 函数,它负责创建一个对象或者提供依赖。在
wire
的上下文中,提供者可以是任何返回一个或多个值的函数。这些返回值将成为注入器函数的参数。提供者函数通常负责初始化组件,比如数据库连接、服务实例等。并且提供者的返回值不仅限于一个,如果有需要的话,可以额外添加一个
error
的返回值。
例如,一个提供者函数可能会创建并返回一个数据库连接:

func NewDBConnection(dsn string) (*gorm.DB, error) {
    db, err := gorm.Open(mysql.Open(dsn))
    if err != nil {
        return nil, err
    }
    return db, nil
}

提供者函数可以分组为提供者函数集(
provider set
)。使用
wire.NewSet
函数可以将多个提供者函数添加到一个集合中。举个例子,例如将
user
相关的
handler

service
进行组合:

package web

var UserSet = wire.NewSet(NewUserHandler, service.NewUserService)

使用
wire.NewSet
函数将提供者进行分组,该函数返回一个
ProviderSet
结构体。不仅如此,
wire.NewSet
还能对多个
ProviderSet
进行分组
wire.NewSet(UserSet, XxxSet)

package demo

import (
    // ...
    "example.com/some/other/pkg"
)

// ...

var MegaSet = wire.NewSet(UserSet, pkg.OtherSet)

7.3 Wire 注入器(injectors)

注入器(
injectors
)的作用是将所有的提供者(
providers
)连接起来,要声明一个注入器函数只需要在函数体中调用
wire.Build()
。这个函数的返回值也无关紧要,只要它们的类型正确即可。这些值在生成的代码中将被忽略。回顾一下我们之前的代码:

//go:build wireinject

// 让 wire 来注入这里的代码
package wire

import (
	"github.com/google/wire"
	"wire/repository"
	"wire/repository/dao"
)

func InitRepository() *repository.UserRepository {
	// 我只在这里声明我要用的各种东西,但是具体怎么构造,怎么编排顺序
	// 这个方法里面传入各个组件的初始化方法
	wire.Build(InitDB, repository.NewUserRepository, dao.NewUserDAO)
	return new(repository.UserRepository)
}

在这个例子中,
InitRepository
是一个注入器,它依赖
InitDB

repository.NewUserRepository
这两个提供者。

与提供者一样,注入器也可以输入参数(然后将其发送给提供者),并且可以返回错误。
wire.Build
的参数和
wire.NewSet
一样:都是提供者集合。这些就在该注入器的代码生成期间使用的提供者集。

八、Wire 的高级用法

8.1 绑定接口

依赖项注入通常用于绑定接口的具体实现。
wire
通过类型标识将输入与输出匹配,因此倾向于创建一个返回接口类型的提供者。然而,这也不是习惯写法,因为Go的最佳实践是返回具体类型。你可以在提供者集中声明接口绑定.

我们对之前的代码进行改造:

首先,我们在
UserRepository
接口中定义一些方法。例如,我们可以定义一个
GetUser
方法,该方法接收一个用户ID,并返回相应的用户。 在
repository/user.go
文件中:

package repository

import (
    "wire/repository/dao"
    "gorm.io/gorm"
)

type UserRepository interface {
    GetUser(id uint) (*User, error)
}

type UserRepositoryImpl struct {
    dao *dao.UserDAO
}

func (r *UserRepositoryImpl) GetUser(id uint) (*User, error) {
    return r.dao.GetUser(id)
}

func NewUserRepository(dao *dao.UserDAO) UserRepository {
    return &UserRepositoryImpl{
        dao: dao,
    }
}

然后,我们在
UserDAO
中实现这个
GetUser
方法。在
repository/dao/user.go
文件中:

package dao

import (
    "gorm.io/gorm"
)

type User struct {
    ID uint
    // other fields...
}

type UserDAO struct {
    db *gorm.DB
}

func (dao *UserDAO) GetUser(id uint) (*User, error) {
    var user User
    result := dao.db.First(&user, id)
    if result.Error != nil {
        return nil, result.Error
    }
    return &user, nil
}

func NewUserDAO(db *gorm.DB) *UserDAO {
    return &UserDAO{
        db: db,
    }
}

最后,我们需要更新
wire.go
文件中的
InitRepository
函数,以返回
UserRepository
接口,而不是具体的实现。 在
wire.go
文件中:

//go:build wireinject

package wire

import (
    "github.com/google/wire"
    "wire/repository"
    "wire/repository/dao"
)

func InitRepository() repository.UserRepository {
    wire.Build(InitDB, repository.NewUserRepository, dao.NewUserDAO)
    return &repository.UserRepositoryImpl{}
}

使用
wire.Bind
来建立接口类型和具体的实现类型之间的绑定关系,这样
Wire
工具就可以根据这个绑定关系进行类型匹配并生成代码。

wire.Bind
函数的第一个参数是指向所需接口类型值的指针,第二个实参是指向实现该接口的类型值的指针。

8.2 结构体提供者(Struct Providers)

Wire
库有一个函数是
wire.Struct
,它能根据现有的类型进行构造结构体,我们来看看下面的例子:

package main

import "github.com/google/wire"

type Name string

func NewName() Name {
	return "小米SU7"
}

type PublicAccount string

func NewPublicAccount() PublicAccount {
	return "新一代车神"
}

type User struct {
	MyName          Name
	MyPublicAccount PublicAccount
}

func InitializeUser() *User {
	wire.Build(
		NewName,
		NewPublicAccount,
		wire.Struct(new(User), "MyName", "MyPublicAccount"),
	)
	return &User{}
}

上述代码中,首先定义了自定义类型
Name

PublicAccount
以及结构体类型
User
,并分别提供了
Name

PublicAccount
的初始化函数(
providers
)。然后定义一个注入器(
injectors

InitializeUser
,用于构造连接提供者并构造
*User
实例。

使用
wire.Struct
函数需要传递两个参数,第一个参数是结构体类型的指针值,另一个参数是一个可变参数,表示需要注入的结构体字段的名称集。

根据上述代码,使用
Wire
工具生成的代码如下所示:

func InitializeUser() *User {
    name := NewName()
    publicAccount := NewPublicAccount()
    user := &User{
       MyName:          name,
       MyPublicAccount: publicAccount,
    }
    return user
}

如果我们不想返回指针类型,只需要修改
InitializeUser
函数的返回值为非指针即可。

8.3 绑定值

有时,将基本值(通常为nil)绑定到类型是有用的。你可以向提供程序集添加一个
值表达式
,而不是让注入器依赖于一次性函数提供者(
providers
)。

func InjectUser() User {
    wire.Build(wire.Value(User{MyName: "小米SU7"}))
    return User{}
}

在上述代码中,使用
wire.Value
函数通过表达式直接指定
MyName
的值,生成的代码如下所示:

func InjectUser() User {
    user := _wireUserValue
    return user
}

var (
    _wireUserValue = User{MyName: "小米SU7"}
)

需要注意的是,值表达式将被复制到生成的代码文件中。

对于接口类型,可以使用
InterfaceValue

func InjectPostService() service.IPostService {
    wire.Build(wire.InterfaceValue(new(service.IPostService), &service.PostService{}))
    return nil
}

8.4 使用结构体字段作为提供者(providers)

有些时候,你可以使用结构体的某个字段作为提供者,从而生成一个类似
GetXXX
的函数。

func GetUserName() Name {
    wire.Build(
       NewUser,
       wire.FieldsOf(new(User), "MyName"),
    )
    return ""
}

你可以使用
wire.FieldsOf
函数添加任意字段,生成的代码如下所示:

func GetUserName() Name {
    user := NewUser()
    name := user.MyName
    return name
}

func NewUser() User {
    return User{MyName: Name("小米SU7"), MyPublicAccount: PublicAccount("新一代车神!")}
}

8.5 清理函数

如果一个提供者创建了一个需要清理的值(例如关闭一个文件),那么它可以返回一个闭包来清理资源。注入器会用它来给调用者返回一个聚合的清理函数,或者在注入器实现中稍后调用的提供商返回错误时清理资源。

func provideFile(log Logger, path Path) (*os.File, func(), error) {
    f, err := os.Open(string(path))
    if err != nil {
        return nil, nil, err
    }
    cleanup := func() {
        if err := f.Close(); err != nil {
            log.Log(err)
        }
    }
    return f, cleanup, nil
}

8.6 备用注入器语法

如果你不喜欢在注入器函数声明的末尾编写类似
return Foo{}, nil
的语句,那么你可以简单粗暴地使用
panic

func InitializeGin() *gin.Engine {
    panic(wire.Build(/* ... */))
}

九、参考文档

本文分享自华为云社区《
GaussDB(DWS)性能调优系列实战篇七:十八般武艺之GUC参数调优
》,作者: 黎明的风。

1. 前言

  • 适用版本:【8.1.1及以上】

GaussDB(DWS)性能调优系列专题文章,介绍了数据库性能调优的思路和总体策略。在系统级调优中数据库全局的GUC参数对整体性能的提升至关重要,而在语句级调优中GUC参数可以调整估算模型,选择查询计划中算子的类型,或者选择不同的执行计划。因此在SQL调优过程中合理的设置GUC参数十分重要。

2. 优化器GUC参数调优

在GaussDB(DWS)中,SQL语句的执行所需要经历的步骤如下图所示,其中红色部分为DBA可以介入实施调优的环节。

查询计划的生成是基于一定的模型和统计信息进行代码估算,在某些场景由于统计信息不准确或者代价估算有偏差时,就需要通过GUC参数设置的的方式选择更优的查询计划。

在GaussDB(DWS)中,和SQL执行性能相关的GUC参数主要有以下几个:

  • best_agg_plan: 进行聚集计算模型的设置
  • enable_sort: 控制优化器是否使用的排序,主要用于让优化器选择使用HashAgg来实现聚集操作
  • enable_hashagg:控制优化器是否使用HashAgg来实现聚集操作
  • enable_force_vector_engine:开启参数后强制生成向量化的执行计划
  • query_dop:用户自定义的查询并行度

2.1 best_agg_plan参数

GaussDB(DWS)是分布式的数据库集群,数据计算尽量在各个DN上并行计算,可以得到最优的性能,在Stream框架下Agg操作可以分为两个场景。

Agg下层算子输出结果集的分布列是Group By列的子集。

这种场景,直接对下层结果集进行汇聚的结果就是正确的汇聚结果,生成算子直接使用即可。例如以下语句,lineitem的分布列是l_orderkey,它是Group By的列。

selectl_orderkey,
count(
*) ascount_orderfromlineitem
group by
l_orderkey;

查询计划如下:

Agg下层算子输出结果集的分布列不是Group By列的子集。

对于这种场景Stream下的聚集(Agg)操作,优化器可以生成以下三种形态的查询计划:

  • hashagg+gather(redistribute)+hashagg
  • redistribute+hashagg(+gather)
  • hashagg+redistribute+hashagg(+gather)

通常优化器总会选择最优的执行计划,但是众所周知代价估算,尤其是中间结果集的代价估算有时会有比较大的偏差。这种比较大的偏差就可能会导致聚集(agg)的计算方式出现比较大的偏差,这时候就需要通过best_agg_plan参数进行聚集计算模型的干预。

以下通过TPC-H Q1语句分析三种形态的查询计划:

-- TPC-H Q1selectl_returnflag,
l_linestatus,
sum(l_quantity)
assum_qty,
sum(l_extendedprice)
assum_base_price,
sum(l_extendedprice
* (1 - l_discount)) assum_disc_price,
sum(l_extendedprice
* (1 - l_discount) * (1 + l_tax)) assum_charge,
avg(l_quantity)
asavg_qty,
avg(l_extendedprice)
asavg_price,
avg(l_discount)
asavg_disc,
count(
*) ascount_orderfromlineitemwherel_shipdate<= date '1998-12-01' - interval '90' day (3)
group by
l_returnflag,
l_linestatus
order by
l_returnflag,
l_linestatus;
}

当best_agg_plan=1时,在DN上进行了一次聚集,然后结果通过GATHER算子汇总到CN上进行了二次聚集,对应的查询计划如下:

该方法适用于DN第一次聚集后结果集较少并且DN数较少的场景,在CN上进行第二次聚集时的结果集小,CN不会成为计算瓶颈。

当best_agg_plan=2时,在DN上先按照Group By的列进行数据重分布,然后在DN上进行聚集操作,将汇总的结果返回给CN,对应的查询计划如下:

该方法适用于DN第一次聚集后结果集缩减不明显的场景,因为这样可以省略DN上的第一次聚集操作。

当best_agg_plan=3时,在DN上进行一次聚集,然后将聚集结果按照Group By的列进行数据重分布,之后在DN上进行二次聚集得到结果,对应的查询计划如下:

该方法使用于DN第一次聚集后中间结果缩减明显,但最终结果行数比较大的场景。

GaussDB(DWS)中,以上三种方法的选择是根据代价来自动选择。在实际的SQL调优时,如果遇到有聚集方式不合理的场景,可以通过尝试设置best_agg_plan参数,选择最优的聚集方式。

2.2 enable_sort参数

GaussDB(DWS)中实现分组聚集操作有两种方法:

  • HashAgg:使用Hash表对数据进行去重,并同时进行聚集操作,适用于聚集后行数缩减较多的场景。
  • Sort + GroupAgg:首先对数据进行排序,然后遍历排序后的数据,完成去重和聚集操作,适用于聚集后行数缩减较少的场景。

以下面的SQL为例:

selectl_orderkey,
count(
*) ascount_orderfromlineitem
group by
l_orderkey;

如果使用Sort + GroupAgg的方式,在Sort排序算子里执行时间比较长,因为需要对大量数据进行排序操作。

以上这种场景,可以关闭enable_sort参数,选择使用HashAgg的方式来实现聚集操作,可以获得较好的执行性能。

2.3 enable_hashagg参数

GaussDB(DWS)中通过count distinct来统计多个列的数据时,通常会使用HashAgg来实现每一个列的统计聚集操作,然后将结果通过Join方式关联起来得到最终结果。

以下面的SQL为例:

selectl_orderkey,
count(distinct l_partkey)
ascount_partkey,
count(distinct l_suppkey)
ascount_suppkey,
count(distinct l_linenumber)
ascount_linenumber,
count(distinct l_returnflag)
ascount_returnflag,
count(distinct l_linestatus)
ascount_linestatus,
count(distinct l_shipmode)
ascount_shipmodefromlineitem
group by
l_orderkey;

从查询计划来看,通过count distinct统计了lineitem表中的6列数据,是通过6个HashAgg操作来实现的,该SQL执行时消耗的资源相对较高。

如果关闭enable_hashagg参数,优化器会选择Sort + GroupAgg的方式,该SQL执行时消耗的资源相对较少。

在应用开发时,可以根据SQL并发和资源使用情况,通过设置enable_hashagg参数来选择合适的执行计划。

2.4 enable_force_vector_engine参数

GaussDB(DWS)支持行存储和列存储两种存储模型,用户可以根据应用场景,建表的时候选择行存储还是列存储表。向量化执行将传统的执行模式由一次一元组的模型修改为一次一批元组,配合列存特性,可以带来巨大的性能提升。

如果使用行存表或者是行列混存的场景,由于行存表默认走的是行存执行引擎,最终查询无法走向量化执行引擎。

以下面的SQL为例:

selectl_orderkey,
sum(l_extendedprice
* (1 - l_discount)) asrevenue,
o_orderdate,
o_shippriority
fromcustomer_row,
orders,
lineitem
wherec_mktsegment= 'BUILDING'and c_custkey=o_custkey
and l_orderkey
=o_orderkey
and o_orderdate
< date '1995-03-15'and l_shipdate> date '1995-03-15'group by
l_orderkey,
o_orderdate,
o_shippriority
order by
revenue desc,
o_orderdate
limit
10;

SQL语句中的customer_row表为行存表,orders和lineitem为列存表,该场景在默认参数的情况下无法走向量化引擎,Row Adapter算子表示将列存数据转为行存数据,对应的查询计划为:

这种场景,可以选择开启enable_force_vector_engine参数,通过向量化执行引擎来执行,Vector Adapter算子表示将行存数据转换为列存数据,每个算子前面的Vector表示改算子为向量化引擎的执行器算子,对应的查询计划为:

从上述计划可以看出,向量化引擎相比行执行引擎,执行性能有数倍的提升效果。

2.5 query_dop参数

GaussDB(DWS)支持并行计算技术,当系统的CPU、内存、I/O和网络带宽等资源充足时,可以充分利用富余硬件资源,提升语句的执行速度。在GaussDB(DWS)中,通过query_dop参数,来控制语句的并行度,取值如下:

  • query_dop=1,串行执行
  • query_dop=[2…N],指定并行执行并行度
  • query_dop=0,自适应调优,根据系统资源和语句复杂度情况自适应选择并行度

query_dop参数设置的一些原则:

  • 对于短查询为主的TP类业务中,如果不能通过CN轻量化或下发语句进行业务的调优,则生成SMP计划的时间较长,建议设置query_dop=1。
  • 对于AP类复杂语句的场景,建议设置query_dop=0。
  • 计划并行执行之后必定会引起资源消耗的增加,当资源成为瓶颈的情况下,SMP无法提升性能,反而可能导致性能的劣化。出现资源瓶颈的情况下,建议关闭SMP,即设置query_dop=1。

设置query_dop=0可以实现自适应调优,在部分场景下语句执行的并行度没有达到最优,这种情况可以考虑通过query_dop参数设置并行度。

例如下面的SQL:

select count(*) from(selectl_orderkey,
count(
*) ascount_orderfromlineitem
group by
l_orderkey
);

在query_dop=0时使用的并行度为2。

设置query_dop=4时使用的并行度为4,执行时间相比并行度为2时有明显的提升。

3. 数据库全局GUC参数

在使用GaussDB(DWS)时,全局的GUC参数对集群整体性能影响很大,这里介绍一些常用参数以及推荐的配置。

3.1 数据内存参数

影响数据库性能的五大内存参数有:max_process_memory、shared_buffers、cstore_buffers、work_mem和maintenance_work_mem。

max_process_memory

max_process_memory是逻辑内存管理参数,主要功能是控制单个CN/DN上可用内存的最大峰值。

计算公式:max_process_memory=物理内存*0.665/(1+主DN个数)。

shared_buffers

设置DWS使用的共享内存大小。增加此参数的值会使DWS比系统默认设置需要更多的System V共享内存。

建议设置shared_buffers值为内存的40%以内。主要用于行存表scan。计算公式:shared_buffers=(单服务器内存/单服务器DN个数)0.40.25

cstore_buffers

设置列存和OBS、HDFS外表列存格式(orc、parquet、carbondata)所使用的共享缓冲区的大小。

计算公式可参考shared_buffers。

work_mem

设置内部排序操作和Hash表在开始写入临时磁盘文件之前使用的内存大小。

ORDER BY,DISTINCT和merge joins都要用到排序操作。Hash表在散列连接、散列为基础的聚集、散列为基础的IN子查询处理中都要用到。

对于复杂的查询,可能会同时并发运行好几个排序或者散列操作,每个都可以使用此参数所声明的内存量,不足时会使用临时文件。同样,好几个正在运行的会话可能会同时进行排序操作。因此使用的总内存可能是work_mem的好几倍。

计算公式:

对于串行无并发的复杂查询场景,平均每个查询有5-10关联操作,建议work_mem=50%内存/10。

对于串行无并发的简单查询场景,平均每个查询有2-5个关联操作,建议work_mem=50%内存/5。

对于并发场景,建议work_mem=串行下的work_mem/物理并发数。

maintenance_work_mem

maintenance_work_mem用来设置维护性操作(比如VACUUM、CREATE INDEX、ALTER TABLE ADD FOREIGN KEY等)中可使用的最大的内存。

当自动清理进程运行时,autovacuum_max_workers倍数的内存将会被分配,所以此时设置maintenance_work_mem的值应该不小于work_mem。

3.2 连接相关GUC参数

连接相关的参数有两个:max_connections和max_prepared_transactions

max_connections

允许和数据库连接的最大并发连接数。此参数会影响集群的并发能力。

设置建议:
CN中此参数建议保持默认值。DN中此参数建议设置为CN的个数乘以CN中此参数的值。

增大这个参数可能导致GaussDB(DWS)要求更多的System V共享内存或者信号量,可能超过操作系统缺省配置的最大值。这种情况下,请酌情对数值加以调整。

max_prepared_transactions

设置可以同时处于"预备"状态的事务的最大数目。增加此参数的值会使GaussDB(DWS)比系统默认设置需要更多的System V共享内存。

NOTICE:

max_connections取值的设置受max_prepared_transactions的影响,在设

max_connections之前,应确保max_prepared_transactions的值大于或等

max_connections的值,这样可确保每个会话都有一个等待中的预备事务。

3.3 并发控制GUC参数

max_active_statements

设置全局的最大并发数量。此参数只应用到CN,且针对一个CN上的执行作业。

需根据系统资源(如CPU资源、IO资源和内存资源)情况,调整此数值大小,使得系统支持最大限度的并发作业,且防止并发执行作业过多,引起系统崩溃。

当取值-1或者0时,不限制全局并发数。

在点查询的场景下,参数建议设置为100。

在分析类查询的场景下,参数的值设置为CPU的核数除以DN个数,一般可以设置5~8个。

3.4 其他GUC参数

bulk_write_ring_size

数据并行导入使用的环形缓冲区大小。

该参数主要影响入库性能,建议导入压力大的场景增加DN上的该参数配置。

checkpoint_completion_target

指定检查点完成的目标。

含义是每个checkpoint需要在checkpoints间隔时间的50%内完成。

默认值为0.5,为提高性能可改成0.9。

data_replicate_buffer_size

发送端与接收端传递数据页时,队列占用内存的大小。此参数会影响主备之间复制的缓冲大小。

默认值为128MB,若服务器内存为256G,可适当增大到512MB。

wal_receiver_buffer_size

备机与从备接收Xlog存放到内存缓冲区的大小。

默认值为64MB,若服务器内存为256G,可适当增大到128MB

4. 总结

本篇文章主要介绍了GaussDB(DWS)性能调优涉及到的优化器和系统级GUC参数,通过合理配置这些GUC参数,能够充分利用好CPU、内存、磁盘IO和网络IO等资源,提升语句的执行性能和GaussDB(DWS)集群的整体性能。

5. 参考文档

  1. GaussDB(DWS) SQL进阶之SQL操作之聚集函数
    https://bbs.huaweicloud.com/blogs/293963
  2. PB级数仓GaussDB(DWS)性能黑科技之并行计算技术解密
    https://bbs.huaweicloud.com/blogs/203426
  3. 常见性能参数调优设计
    https://support.huaweicloud.com/performance-dws/dws_10_0068.html

点击关注,第一时间了解华为云新鲜技术~

这次继续研究无边框窗体需要的功能。其实就是把有边框的默认窗体的一些功能进行实现而已。不过不同的人不一定相同的代码,所以笔者尽量用最简单有效的方法例子让读者能够直接对代码进行复用,以节省时间和人力。这次解决的是无边框窗体Sizeabled的能改变大小的方案。

因为是改变无边框窗体的大小,这个时候就涉及到窗体的防闪烁的问题,这个见上次的那个博文:
https://www.cnblogs.com/lzhdim/p/18077345

1、
项目目录;

2、
源码介绍;

源码很简单,已经封装到无边框窗体的基类里面了。

3、
运行界面;

因为只是改变无边框窗体的大小,所以应用的运行界面不提供,请读者自己下载运行看效果。

4、
使用介绍;

源码的使用挺简单,直接复制窗体上的Panel控件到新的窗体,并添加代码即可。

5、
源码下载;

此例子提供源码下载:
https://download.csdn.net/download/lzhdim/89080232

6、
其它建议;

这个例子已经很简单明了了,请读者自己根据自己的要求去进行修改。

上面介绍了无边框窗体改变大小的一个例子,请需要的读者自己下载源码进行代码复用即可。

本文分享自华为云社区《
kube-apiserver限流机制原理
》,作者:可以交个朋友。

背景

apiserver是kubernetes中最重要的组件,一旦遇到恶意刷接口或请求量超过承载范围,apiserver服务可能会崩溃,导致整个kubernetes集群不可用。所以我们需要对apiserver做限流处理来提升kubernetes的健壮性。

k8s-apiserver限流能力发展过程

apiserver限流能力的发展分为两个阶段:

kubernetes 1.18版本之前kube-apiserver只是将请求分成了变更类型(create、update、delete、patch)和非变更类型(get、list、watch),并通过启动参数设置了两种类型的最大并发数。

--max-requests-inflight          ## 限制同时运行的非变更类型请求的个数上限,0表示无限制。--max-mutating-requests-inflight   ## 限制同时运行的变更类型请求的个数上限。0 表示无限制。

此时的apiserver限流能力较弱,若某个客户端错误的向kube-apiserver发起大量的请求时,必然会阻塞kube-apiserver,影响其他客户端的请求,因此高阶的限流APF就诞生了。

kubernetes1.18版本之后APF( APIPriorityAndFairness )成为kubernetes的默认限流方式。 APF以更细粒度的方式对请求进行分类和隔离,根据优先级和公平性进行处理。

--enable-priority-and-fairness   ##  该值作为APF特性开关,默认为true--max-requests-inflight、--max-mutating-requests-inflight    ## 当开启APF时,俩值相加确定kube-apiserver的总并发上限

两个阶段限流能力对比

限流能力 1.18版本前 1.18版本后(APF)
颗粒度 仅根据是否变更做分类 可以根据请求对象、请求者身份、命名空间等做分类
隔离性 一个坏用户可能堵塞整个系统 为请求分配固定队列,坏请求只能撑爆其使用的队列
公平性 会出现饿死 用公平性算法从队列中取出请求
优先级 有特权级别,可让重要请求不被限制

APF关键资源介绍

APF通过FlowSchema 和 PriorityLevelConfiguration两个资源配置限流策略。

FlowSchema:解决老版本分类颗粒度粗的问题。根据rules字段匹配请求,匹配规则包含:请求对象、执行操作、请求者身份和命名空间

apiVersion: flowcontrol.apiserver.k8s.io/v1beta2 
kind: FlowSchema # 一个kubernetes集群中可以定义多个FlowSchema
metadata:
name: myfl
spec:
distinguisherMethod: # 可选值为:ByNamespace或ByUser,用于把请求分组。属于同组的请求会分配到固定的queue中,如果省略该参数,则该FlowSchema匹配的所有请求都将视为同一个分组。
type: ByUser
matchingPrecedence:
90 # 数字越小代表FlowSchema的匹配顺序越在前,取值范围:1~10000
priorityLevelConfiguration: # FlowSchema关联的priorityLevelConfiguration
name: mypl
rules:
-nonResourceRules: # 匹配非资源型:匹配接口URL-nonResourceURLs:- '*'resourceRules: # 匹配资源型:匹配apigroup、namespace、resources、verbs-apiGroups:- '*'namespaces:- '*'resources:- '*'verbs:- get -create-list-update
subjects: # 匹配请求者主体:可选Group、User、ServiceAccount
-group:
name:
'*'kind: Group-kind: User
user:
name:
'*' -kind: ServiceAccount
serviceAccount:
name: myserviceaccount
namespace: demo

PriorityLevelConfiguration:解决老版本隔离性差的问题和优先级问题,并定义了限流细节(总队列数、队列长度、是否可排队)。当请求与某个FlowSchema匹配后,该请求会关联FlowSchema中指定的PriorityLevelConfiguration资源,每个PriorityLevelConfiguration相互隔离,且能承受的并发请求数也不一样

apiVersion: flowcontrol.apiserver.k8s.io/v1beta2 
kind: PriorityLevelConfiguration ## 每个PriorityLevelConfiguration有自己独立的限流配置, PriorityLevelConfiguration之间是完全隔离的。
metadata:
name: mypl
spec:
type: Limited # 设置是否为特权级别,如果为Exempt则不进行限流,如果为Limited则进行限流
limited:
assuredConcurrencyShares:
2# 值越大,PriorityLevelConfiguration的并发上限越高。若当前并发执行数未达到并发上限,则PL处于空闲状态。
limitResponse: # 定义如何处理当前无法被处理的请求
type: Queue # 类型,Queue或者Reject,Reject直接返回429并拒绝,Queue将请求加入队列
queuing:
handSize:
1# 根据ByNamespace或ByUser对请求分组,每个分组对应queues的数量,
queueLengthLimit:
20# 此PriorityLevelConfiguration中每个队列的长度
queues:
2 # 此PriorityLevelConfiguration中的队列数

一个FlowSchema只能关联一个priorityLevelConfiguration,多个FlowSchema可以关联同一个priorityLevelConfiguration

PriorityLevelConfiguration并发上限 = assuredConcurrencyShares / 所有assuredConcurrencyShares之和 * apiserver总并发数

APF处理过程

image.png

请求与集群中的FlowSchema列表按照顺序依次匹配,每个FlowSchema的matchingPrecedence字段决定其在列表中的顺序,matchingPrecedence字段值越小,越靠前,越先进行匹配请求。

根据FlowSchema资源中的rules规则进行匹配,匹配方式可以是 “请求的资源类型”、“请求的动作类型”、“请求者的身份”、“请求的命名空间” 等多个维度。

若请求与某个FlowSchema成功匹配,匹配就会结束。FlowSchema关联着一个PriorityLevelConfiguration,每个PriorityLevelConfiguration中包含许多queue,根据FlowSchema.spec.Distinguisher字段将请求进行"分组",根据分组来分配queue,分配queue数量由PriorityLevelConfiguration资源的handSize字段决定,如果省略该参数,则该FlowSchema匹配的所有请求都将视为同一个"分组"。

每个PriorityLevelConfiguration资源都有独立的并发上限,assuredConcurrencyShares字段为apiserver总并发数的权重占比,值越大分配的并发上限就越高,当PriorityLevelConfiguration达到并发上限后,请求会根据所属的"分组"写入固定的queue中,请求被阻塞等待。请求与queue的固定关联可以让恶意用户只影响其使用的queue,而不会影响同PriorityLevelConfiguration中的其他queue。

当PriorityLevelConfiguration未达到并发上限时,fair queuing算法从所有queue中选择一个合适的queue取出请求,解除请求的阻塞,执行这个请求。fair queuing算法能保证同一个 PriorityLevelConfiguration 中的所有queue被处理机会平等。

APF实战

kubernetes原生自带了一些FlowSchema和PriorityLevelConfiguration规则,我们选择一个查看,如下图:

image.png

下面我们创建新的APF规则:当请求对象是apf命名空间中的deployment,则进行"apfpl"限流规则。

apiVersion: flowcontrol.apiserver.k8s.io/v1beta2 
kind: FlowSchema
metadata:
name: apffl
spec:
matchingPrecedence:
150priorityLevelConfiguration:
name: apfpl ## 关联名为apfpl的PriorityLevelConfiguration
rules:
-resourceRules:-apiGroups:-apps
clusterScope:
truenamespaces:-apf ## 匹配apf命名空间
resources:
-deployments ## 匹配操作deployment的请求
verbs:
- '*'## 匹配任意操作类型
subjects:
-kind: Group
group:
name:
'*'## 匹配任意组身份---apiVersion: flowcontrol.apiserver.k8s.io/v1beta2
kind: PriorityLevelConfiguration
metadata:
name: apfpl
spec:
limited:
assuredConcurrencyShares:
2limitResponse: ## 设置限流处理细节
queuing:
handSize:
1queueLengthLimit:20queues:2type: Queue
type: Limited ## 对请求做限流处理

接着在apf命名空间和default命名空间分别创建deployment进行测试。apf_fs为请求被分类到的 FlowSchema 的名称,apf_pl为该请求的优先级名称。查看apiserver日志信息,见下图:

image.png

循环操作deployment,我们可以使用命令查看是否触发限流等待

kubectl get --raw /debug/api_priority_and_fairness/dump_priority_levels

image.png
返回waitingRequests非0,则代表触发最大并发数,有请求被限流进入等待队列。PriorityLevelConfiguration资源不为空闲表示已达到并发上限

点击关注,第一时间了解华为云新鲜技术~