go语言提供非常方便的创建轻量级的协程goroutine来并发处理任务,但是 协程过多影响程序性能,所以,这时goroutine池就登场了。本文简要介绍ants goroutine 池的基本的使用方法。

简介

ants是一个高性能的 goroutine 池,实现了对大规模 goroutine 的调度管理、goroutine 复用,允许使用者在开发并发程序的时候限制 goroutine 数量,复用资源,达到更高效执行任务的效果。 ants就是一个很多大厂广泛使用的goroute池。

官网地址

https://github.com/panjf2000/ants

功能

  • 自动调度海量的 goroutines,复用 goroutines
  • 定期清理过期的 goroutines,进一步节省资源
  • 提供了大量有用的接口:任务提交、获取运行中的 goroutine 数量、动态调整 Pool 大小、释放 Pool、重启 Pool
  • 优雅处理 panic,防止程序崩溃
  • 资源复用,极大节省内存使用量;在大规模批量并发任务场景下比原生 goroutine 并发具有更高的性能
  • 非阻塞机制

quick start

使用 ants v1 版本:

go get -u github.com/panjf2000/ants

使用 ants v2 版本 (开启 GO111MODULE=on):

go get -u github.com/panjf2000/ants/v2

接下来看一下官方demo:实现一个计算大量整数和的程序

NewPool

NewPool生成ants池实例。

package main

import (
	"fmt"
	"sync"
	"time"

	"github.com/panjf2000/ants/v2"
)

var sum int32

func demoFunc() {
	time.Sleep(10 * time.Millisecond)
	fmt.Println("Hello World!")
}

func main() {
	defer ants.Release()

	runTimes := 1000

	//使用公共池。
	var wg sync.WaitGroup
	syncCalculateSum := func() {
		demoFunc()
		wg.Done()
	}
	for i := 0; i < runTimes; i++ {
		wg.Add(1)
		_ = ants.Submit(syncCalculateSum)
	}
	wg.Wait()
	fmt.Printf("running goroutines: %d\n", ants.Running())
    fmt.Printf("finish all tasks.\n")
 }

其中:

生成一个具有特定函数的ants池实例

  • NewPool(size int, options …Option) (*PoolWithFunc, error)

args.size 即池容量,即池中最多有 10 个 goroutine。 arg.Option 定制化 goroutine pool.

  • p.Submit向此池提交任务。
  • ants.Release关闭此池并释放工作队列。
  • defaultAntsPool 导入ants时初始化实例池。

NewPoolWithFunc

package main

import (
	"fmt"
	"github.com/panjf2000/ants/v2"
	"sync"
	"sync/atomic"
)

var sum int32

func myFunc(i interface{}) {
	n := i.(int32)
	atomic.AddInt32(&sum, n)
	fmt.Printf("run with %d\n", n)
}

func main() {
	defer ants.Release()
	runTimes := 1000
	var wg sync.WaitGroup
	//使用带有函数的池
//设置goroutine池的容量为10,过期时间为1秒。
	p, _ := ants.NewPoolWithFunc(10, func(i interface{}) {
		myFunc(i)
		wg.Done()
	})
	defer p.Release()
	//逐个提交任务。
	for i := 0; i < runTimes; i++ {
		wg.Add(1)
		_ = p.Invoke(int32(i))
	}
	wg.Wait()
	fmt.Printf("running goroutines: %d\n", p.Running())
	fmt.Printf("finish all tasks, result is %d\n", sum)
}

OutPut:

run with 1
run with 5
run with 11
run with 12
run with 13
run with 14
run with 7
...
run with 789
run with 809
running goroutines: 10
finish all tasks, result is 499500

其中:

生成一个具有特定函数的ants池实例

  • NewPoolWithFunc(size int, pf func(interface{}), options …Option) (*PoolWithFunc, error)

args.pf 即为执行任务的函数

优雅处理 panic

测试一些当任务触发panic的情况

func myFunc(i interface{}) {
	n := i.(int32)
	atomic.AddInt32(&sum, n)
	if n%2 == 0 {
		panic(any(fmt.Sprintf("panic from task:%d", n)))
	}
	fmt.Printf("run with %d\n", n)
}

output:

run with 3
run with 13
...
run with 999
2022/10/21 21:41:05 worker with func exits from a panic: panic from task:6
2022/10/21 21:41:05 worker with func exits from a panic: panic from task:0
2022/10/21 21:41:07 worker with func exits from panic: goroutine 14 [running]:

可以看到,main routine 没有因此受影响。

Options

// Options包含实例化ants池时将应用的所有选项。
type Options struct {
	// ExpiryDuration is a period for the scavenger goroutine to clean up those expired workers,
	// the scavenger scans all workers every `ExpiryDuration` and clean up those workers that haven't been
	// used for more than `ExpiryDuration`.
	ExpiryDuration time.Duration

	// PreAlloc indicates whether to make memory pre-allocation when initializing Pool.
	PreAlloc bool

	// Max number of goroutine blocking on pool.Submit.
	// 0 (default value) means no such limit.
	MaxBlockingTasks int

	// When Nonblocking is true, Pool.Submit will never be blocked.
	// ErrPoolOverload will be returned when Pool.Submit cannot be done at once.
	// When Nonblocking is true, MaxBlockingTasks is inoperative.
	Nonblocking bool

	// PanicHandler is used to handle panics from each worker goroutine.
	// if nil, panics will be thrown out again from worker goroutines.
	PanicHandler func(interface{})

	// Logger is the customized logger for logging info, if it is not set,
	// default standard logger from log package is used.
	Logger Logger
}

比如 PanicHandler 遇到 panic会调用这里设置的处理函数,以上例中,我们遇到偶数会触发panic,修改NewPool函数

p, _ := ants.NewPoolWithFunc(10, func(i interface{}) {
		myFunc(i)
		wg.Done()
	}, ants.WithPanicHandler(func(i interface{}) {
		fmt.Printf("panic recover %v", i)
	}))

out:

panic recover panic from i:992run with 880
panic recover panic from i:880run with 972
panic recover panic from i:972run with 994
run with 991

还有更多关于 Benchmarks 性能报告,可参考https://github.com/panjf2000/ants

参考:

定义

将一个复杂对象的构建与它的表示分离,使得同样的构建 过程可以创建不同的表示。

复杂的构造函数

创建一个对象最常用的方式是,使用 new 关键字调用类的构造函数来完成。举个常用资源连接池的例子

type ResourcePool struct {
	name     string
	maxTotal int
	maxIdle  int
	minIdle  int
}
resourcePool := NewResourcePool("dbconnectionpool", 16, 8, 
10)

在可配置项不多的时候没什么问题,但是,如果可配置项逐渐增多,以gorm的配置文件为例

// Config GORM config
type Config struct {
	// GORM perform single create, update, delete operations in transactions by default to ensure database data integrity
	// You can disable it by setting `SkipDefaultTransaction` to true
	SkipDefaultTransaction bool
	// NamingStrategy tables, columns naming strategy
	NamingStrategy schema.Namer
	// FullSaveAssociations full save associations
	FullSaveAssociations bool
	// Logger
	Logger logger.Interface
	// NowFunc the function to be used when creating a new timestamp
	NowFunc func() time.Time
	// DryRun generate sql without execute
	DryRun bool
	// PrepareStmt executes the given query in cached statement
	PrepareStmt bool
	// DisableAutomaticPing
	DisableAutomaticPing bool
	// DisableForeignKeyConstraintWhenMigrating
	DisableForeignKeyConstraintWhenMigrating bool
	// DisableNestedTransaction disable nested transaction
	DisableNestedTransaction bool
	// AllowGlobalUpdate allow global update
	AllowGlobalUpdate bool
	// QueryFields executes the SQL query with all fields of the table
	QueryFields bool
	// CreateBatchSize default create batch size
	CreateBatchSize int

	// ClauseBuilders clause builder
	ClauseBuilders map[string]clause.ClauseBuilder
	// ConnPool db conn pool
	ConnPool ConnPool
	// Dialector database dialector
	Dialector
	// Plugins registered plugins
	Plugins map[string]Plugin
    // ... 省略一些参数
}

这时,这个构造函数就变得复杂了。构造函数参数列表会变得很长,容易搞错各参数的顺序,造成隐蔽的bug。

resourcePool := NewResourcePool("dbconnectionpool",   16, null, 8, null, false , true, 10, 20false true )

开始重构,这里使用 set() 函数来给成员变量赋值,以替代冗长的构造函数,并在set()函数里做参数校验。

func NewResourcePool(name string) *ResourcePool {
	this := &ResourcePool{}
	this.SetName(name)
	return this
}
func (this *ResourcePool) SetName(name string) *ResourcePool {
	if len(name) <= 0 {
		panic(any("name should not be empty."))
	}
	this.name = name
	return this
}
func (this *ResourcePool) SetMaxTotal(maxTotal int) *ResourcePool {
	if maxTotal <= 0 {
		panic(any("maxTotal should be positive."))
	}
	this.maxTotal = maxTotal
	return this
}
func (this *ResourcePool) SetMinIdle(minIdle int) *ResourcePool {
	if minIdle < 0 {
		panic(any("minIdle should not be negative."))
	}
	this.minIdle = minIdle
	return this
}

重构后的构造函数:

resourcePool := NewResourcePool("dbconnectionpool").SetMaxTotal(16).SetMinIdle(8)

但是,这里还是有几个问题:

  1. 如果必填的配置项有很多,那构造函数就又会出现参数列表很长的问题。
  2. 如果配置项之间有一定的依赖关系,并校验参数的合法性。
  3. 如果希望ResourcePool的配置项不对外提供修改方法。

使用构造者重构

  1. 首先,创建一个构造者类Builder,并把参数改为私有,提供set()函数修改。
  2. 其次build() 方法真正创建对象之前,做集中的校验,校验通过之后才会创建对象。

func NewResourcePool(builder *ResourcePoolBuilder) *ResourcePool {
	this := &ResourcePool{}
	this.name = builder.name
	this.maxTotal = builder.maxTotal
	this.maxIdle = builder.maxIdle
	this.minIdle = builder.minIdle
	return this
}

type ResourcePoolBuilder struct {
	name     string
	maxTotal int
	maxIdle  int
	minIdle  int
}

func Builder() *ResourcePoolBuilder {
	builder := &ResourcePoolBuilder{}
	builder.maxTotal = 16
	builder.minIdle = 5
	return builder
}

func (b *ResourcePoolBuilder) Build() *ResourcePool {
	// 校验逻辑放到这里来做,包括必填项校验、依赖关系校验、约束条件校验等
	if len(b.name) <= 0 {
		panic(any("name should not be empty."))
	}
	if b.maxIdle > b.maxTotal {
		panic(any("maxIdle should bigger than maxTotal."))
	}
	return NewResourcePool(b)
}


func (this *ResourcePoolBuilder) SetName(name string) *ResourcePoolBuilder {
	if len(name) <= 0 {
		panic(any("name should not be empty."))
	}
	this.name = name
	return this
}
func (this *ResourcePoolBuilder) SetMaxTotal(maxTotal int) *ResourcePoolBuilder {
	if maxTotal <= 0 {
		panic(any("maxTotal should be positive."))
	}
	this.maxTotal = maxTotal
	return this
}
func (this *ResourcePoolBuilder) SetMinIdle(minIdle int) *ResourcePoolBuilder {
	if minIdle < 0 {
		panic(any("minIdle should not be negative."))
	}
	this.minIdle = minIdle
	return this
}

重构后的构造函数

resourcePool := Builder().SetName("test").SetMaxTotal(0).SetMinIdle(5).Build()

如此,

  • 这样我们就只能通过建造者Builder 来创建 ResourcePool 类对象。
  • 要修改resourcePool只能通过 Builder提供的 set()函数,配置项不对外提供修改方法。
  • 参数校验或者提供默认参数可以放在build()函数内。

小结

构造者模式原理并不复杂,主要适当的场景中灵活使用。通过构造者类Builder、提供的set()函数设置必选项,最终调用build()处理构造类之前的一些逻辑。如此,可以以通过设置不同的可选参数,“定制化”地创建不同的复杂对象.

参考

前后端数据常用传输格式有:json、xml 和 proto等,不管是mvc还是ddd,都会在表现层的对不同的格式进行转化下层依赖所需的类、Object、 aggregate等。

Kratos 表现层

在实际开发场景中,前后端传输采用json。 但kratos使用proto定义Api, 那么我们以一个Http请求为例,来看看kratos的表现层是如何处理的。

以下是一个Kratos-Example,由编写好的proto文件,proto-go 等自动生成表现层的代码。 大概步骤:

  1. 编写proto,包括Service,Req,Reply
  2. 生成表现层代码
  3. 实现下层逻辑等

Example完整代码如下

func _Greeter_SayHello0_HTTP_Handler(srv GreeterHTTPServer) func(ctx http.Context) error {
	return func(ctx http.Context) error {
		var in HelloRequest
		if err := ctx.BindQuery(&in); err != nil {
			return err
		}
		if err := ctx.BindVars(&in); err != nil {
			return err
		}
		http.SetOperation(ctx, "/helloworld.v1.Greeter/SayHello")
		h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
			return srv.SayHello(ctx, req.(*HelloRequest))
		})
		out, err := h(ctx, &in)
		if err != nil {
			return err
		}
		reply := out.(*HelloReply)
		return ctx.Result(200, reply)
	}
}

可以看到:

  1. 这里对具体的格式无感,输入In还是输出Out都是一个proto生成的类。
  2. 具体的实现交给了ctx 上下文去实现 查看 ctx.Result的源码:
func (c *wrapper) Result(code int, v interface{}) error {
	c.w.WriteHeader(code)
	return c.router.srv.enc(&c.w, c.req, v)
}

这里的enc,没有特殊设置,使用默认的DefaultResponseEncoder,部分源码如下

func DefaultResponseEncoder(w http.ResponseWriter, r *http.Request, v interface{}) error {
	...
	codec, _ := CodecForRequest(r, "Accept")
	data, err := codec.Marshal(v)
	if err != nil {
		return err
	}
	w.Header().Set("Content-Type", httputil.ContentType(codec.Name()))
	_, err = w.Write(data)
    ...
}

通过codc接口隔离具体实现,调用接口的 codec.Marshal序列化。

codec包

Codec定义Transport用于编码和解码消息的接口。它的实现包括 json,xml,proto,yaml等

type Codec interface {
	// Marshal returns the wire format of v.
	Marshal(v interface{}) ([]byte, error)
	// Unmarshal parses the wire format into v.
	Unmarshal(data []byte, v interface{}) error
	// Name returns the name of the Codec implementation. The returned string
	// will be used as part of content type in transmission.  The result must be
	// static; the result cannot change between calls.
	Name() string
}

这里我们聚焦正在使用的Json

type codec struct{}
func (codec) Marshal(v interface{}) ([]byte, error) {
	switch m := v.(type) {
	case json.Marshaler:
		return m.MarshalJSON()
	case proto.Message:
		return MarshalOptions.Marshal(m)
	default:
		return json.Marshal(m)
	}
}

这里Marshal()使用第三方库““google.golang.org/protobuf/encoding/protojson”

实现proto转json。

protojson第三方包

protojson是Google提供的proto和json的转化

package api_test

import (
	"google.golang.org/protobuf/encoding/protojson"
	"testing"

	v1 "helloworld/api/helloworld/v1"
)

func TestToJson(t *testing.T) {
	reply := &v1.HelloReply{
		Message: "Jobs",
	}
	replyJson, err := MarshalOptions.Marshal(reply.ProtoReflect().Interface())
	if err != nil {
		t.Errorf("Marshal Error: %v", err)
	}
	t.Logf("replyJson:  %v", string(replyJson))
}

更多的接口请参考“https://google.golang.org/protobuf/encoding/protojson”

小结

本文主要以kratos的表现层的组织方式,来介绍常用的框架表现层处理方式。 其中:

  • kratos通过proto来生成表现层代码的类,包括http和grapc,内部对具体实现无感。
  • kratos通过解码器codec实现不同传输格式的解耦。
  • 第三方包protojson实现proto和json转换。

框架的主要功能之一就是标准化处理一下公共的问题场景,从源码中可以学习:通过接口隔离具体的实现,而使框架与具体实现解耦,且具备更好的拓展性。

参考

基础概念

来自Head First设计模式一书的定义

观察者模式 Observer 观察者模式 定义了一系列对象之间的一对多的关系,当一个对象的状态改变, 其他依赖者都会收到到通知。

经常观察者模式也称发布订阅模式,一般观察者模式有以下组成:

  • Subject-被观察者,亦或是发布者-Publisher
  • Observer-观察者,亦或是订阅者-Subscribe

经典的实现方式

以下是golang的具体实现:observer.go

package design_mode

import (
	"fmt"
)

type Subject interface {
	RegisterObserver(Observer)
	NotifyObservers(message interface{})
}

type Observer interface {
	Update(message interface{})
}

type ConcreteSubject struct {
	observerList []Observer
}

func NewConcreteSubject() Subject {
	return &ConcreteSubject{}
}

func (o *ConcreteSubject) RegisterObserver(observer Observer) {
	o.observerList = append(o.observerList, observer)
}

func (o *ConcreteSubject) NotifyObservers(message interface{}) {
	for _, observer := range o.observerList {
		observer.Update(message)
	}
}

type ConcreteObserverOne struct {
}

func (b *ConcreteObserverOne) Update(message interface{}) {
	fmt.Printf("ConcreteObserverOne is notified.%v \n", message)
}

type ConcreteObserverTwo struct {
}

func (b *ConcreteObserverTwo) Update(message interface{}) {
	fmt.Printf("ConcreteObserverOne is notified.%v \n", message)
}

observer_test.go

package design_mode

import "testing"

func TestObserver(t *testing.T) {
	concreteSubject := NewConcreteSubject()
	concreteSubject.RegisterObserver(&ConcreteObserverOne{})
	concreteSubject.RegisterObserver(&ConcreteObserverTwo{})
	concreteSubject.NotifyObservers("hello every one")
}

OutPut:

ConcreteObserverOne is notified.hello every one 
ConcreteObserverOne is notified.hello every one 

可以看到例子里包含了以下接口和实现:

  • Subject 主题接口: 注册实现、通知所有观察者
  • Observer 观察者接口: 接收主体通知的接口。
  • ConcreteSubject 主题的具体实现
  • ConcreteObserverOne/ConcreteObserverTwo 不同的观察者实现

当你需要增加一个观察者时,只需要实现 Update()接口和注册Register到subject即可。

应用场景

那么观察者模式在什么场景下适用呢?接着我们以一个游戏用户注册的例子来套用一下。

场景: 玩家在注册成功后,将在“地图服务”创建一个出生点位,同时“邮件服务”会发送一份新手礼包。

在没有使用观察者模式时,可能是这么写的。

package main

import "fmt"

type WorldMapService struct {
}
func NewWorldMapService() WorldMapService {
	return WorldMapService{}
}
func (w WorldMapService) Join(u User) {
	fmt.Println(fmt.Sprintf("欢迎%v来到新手村", u.Name))
}

type MailService struct {
}
func NewMailService() MailService {
	return MailService{}
}
func (w MailService) Send(u User) {
	fmt.Println(fmt.Sprintf("恭喜勇士%v,获得金币999", u.Name))
}

type UserService struct {
}
func NewUserService() UserService {
	return UserService{}
}
type User struct {
	Name string
}
func (u UserService) Register(name string) User {
	return User{Name: name}
}
func (m User) Start() {
	fmt.Printf("欢迎来到元宇宙")
}

type UserController struct {
	User     UserService
	WorldMap WorldMapService
	Mail     MailService
}

func NewUserController() UserController {
	user := NewUserService()
	worldMap := NewWorldMapService()
	mail := NewMailService()
	return UserController{
		User:     user,
		WorldMap: worldMap,
		Mail:     mail,
	}
}

func (c UserController) Do() {
	user := c.User.Register("好奇的小明")
	c.WorldMap.Join(user)
	c.Mail.Send(user)
	user.Start()
}

func main() {
	controller := NewUserController()
	controller.Do()
}

OutPut
MapService: 欢迎好奇的小明来到新手村
MailService: 恭喜勇士好奇的小明,获得金币999
UserService: 让我们开始愉快的旅程吧

UserController.Do 注册、增加出生点位、发送邮件,违反单一职责原则。如果模块越来越多,比如增加一个任务系统(登入过游戏赠送金币),坐骑模块(登入赠送初始坐骑)之类,那么这里的代码就会变得越来越长,不好拓展等。这时候,使用观察者模式,进行解耦。

package main

import "fmt"

type WorldMapService struct {
}

func NewWorldMapService() WorldMapService {
	return WorldMapService{}
}
func (w WorldMapService) HandleRegSuccess(u User) {
	fmt.Println(fmt.Sprintf("MapService: 欢迎%v来到新手村", u.Name))
}

type MailService struct {
}

func NewMailService() *MailService {
	return &MailService{}
}
func (w MailService) HandleRegSuccess(u User) {
	fmt.Println(fmt.Sprintf("MailService: 恭喜勇士%v,获得金币999", u.Name))
}

type UserService struct {
}

func NewUserService() UserService {
	return UserService{}
}

type User struct {
	Name string
}

func (u UserService) Register(name string) User {
	return User{Name: name}
}

func (m User) Start() {
	fmt.Printf("UserService: 让我们开始愉快的旅程吧")
}

type UserController struct {
	obsrvers []RegObserver
}

func NewUserController(obsrvers ...RegObserver) UserController {
	return UserController{obsrvers: obsrvers}
}

func (c UserController) Do() {
	userSvc := NewUserService()
	user := userSvc.Register("好奇的小明")
	for _, ob := range c.obsrvers {
		ob.HandleRegSuccess(user)
	}
	user.Start()
}

type RegObserver interface {
	HandleRegSuccess(User)
}

func main() {
	controller := NewUserController(NewWorldMapService(), NewMailService())
	controller.Do()
}

这里

  1. 定义了一个RegObserver接口,不同的服务都实现了这个接口。并注册到了userController,控制器保存了所有的观察者,在登入userController的执行函数Do里,会去遍历所有的观察者,执行HandleRegSuccess。

  2. 当拓展需求时,只需要再添加一个实现了RegObserver接口的类并注册到控制器即可。

如此,各模块间耦合性就降低了。

小结

本文主要介绍观察者模式和使用场景,观察者模式和发布订阅模式的思路是差不多的,主要是为了解耦。应用场景还是非常广泛的,在同一进程的编码上, 在进程间的消息队列,还是在产品的订阅模式都有异曲同工之处。

参考

最近在日常运维过程,发现挖矿病毒利用GitLab的CVE-2021-22205漏洞,消耗服务器的资源。为了彻底解决问题,决定对GitLab进行迁移和版本升级。

前提

服务器 OS GitLabVersion 备注
原始服务器A Ubuntu 13.7.4 下文简称ServerA
迁移目标服务器B Ubuntu 15.3.3 下文简称原ServerB

大致步骤:

  1. ServerA:备份GitLab
  2. ServerB:恢复GitLab
  3. ServerB:更新GitLab版本

备份

数据

这里我们是用DockerCompose运行的GitLab-13.7.4,从主机运行备份: GitLab 12.2 或更高版本:

$docker exec -t <container name> gitlab-backup create

GitLab 12.1 及更早版本:

$docker exec -t <container name> gitlab-rake gitlab:backup:create

开始备份:

$ docker exec -t 985506cf361c gitlab-rake gitlab:backup:create
2022-09-16 02:56:34 +0000 -- Dumping database ...
Dumping PostgreSQL database gitlabhq_production ... [DONE]
...
Backup task is done.

查看备份文件: 默认的备份路径为 /var/opt/gitlab/backups,如果不知道保存路径,可以从容器的 /etc/gitlab/gitlab.rb 文件,查找 gitlab_rails[‘backup_path’] = “/var/opt/gitlab/backups” 此为备份目录。

$ docker exec -it 985506cf361c bash
root@985506cf361c:/# cd /var/opt/gitlab/backups
root@985506cf361c:/var/opt/gitlab/backups# ls
123456_2022_09_16_13.7.4—_gitlab_backup.tar

将容器数据备份拷贝到主机的当前目录

docker cp gitlab:/var/opt/gitlab/backups/123456_2022_09_16_13.7.4_gitlab_backup.tar  ~/

配置

GitLab 提供的备份 Rake 任务不存储您的配置文件,故而这里需要收到备份

/etc/gitlab/gitlab-secrets.json
/etc/gitlab/gitlab.rb

再将配置备份拷贝到主机的当前目录

docker cp gitlab:/etc/gitlab  ~/

迁移

相同版本启动

docker-compose.yaml 示例如下

version: '3.3'
services:
  web:
    image: gitlab/gitlab-ce:13.7.4-ce.0
    restart: always
    hostname: 'gitlab.example.com'
    environment:
      GITLAB_OMNIBUS_CONFIG: |
        external_url 'https://gitlab.example.com'
        # Add any other gitlab.rb configuration here, each on its own line        
    ports:
      - '80:80'
      - '443:443'
      - '22:22'
    volumes:
      - './volumes/gitlab/config:/etc/gitlab'
      - './volumes/gitlab/logs:/var/log/gitlab'
      - './volumes/gitlab/data:/var/opt/gitlab'
    shm_size: '256m'

配置

数据

将ServerA的备份拷贝到ServerB

docker cp 1123456_2022_09_16_13.7.4_gitlab_backup.tar gitlab:/var/opt/gitlab/backups/

进入容器

停止连接到数据库的进程。让 GitLab 的其余部分继续运行

root@23b5e49fc9ea:/var/opt/gitlab/backups# gitlab-ctl stop unicorn
root@23b5e49fc9ea:/var/opt/gitlab/backups# gitlab-ctl stop sidekiq
ok: down: sidekiq: 0s, normally up
root@23b5e49fc9ea:/var/opt/gitlab/backups# gitlab-ctl status

备份文件必须是git用户所有者(root下导入才需要)

# chown -R git:git /var/opt/gitlab/backups/备份.tar

开始还原备份

root@23b5e49fc9ea:/var/opt/gitlab/backups# gitlab-rake gitlab:backup:restore BACKUP=123456_2022_09_16_13.7.4
Unpacking backup ... done

Do you want to continue (yes/no)? yes
...
Do you want to continue (yes/no)? yes

Warning: Your gitlab.rb and gitlab-secrets.json files contain sensitive data
and are not included in this backup. You will need to restore these files manually.
Restore task is done.

启动GitLab

root@23b5e49fc9ea:/var/opt/gitlab/backups# gitlab-ctl start

登入http://ServerB,检查GitLab正常运行。

更新GitLab版本

接着开始更新版本,这里采用的停机升级方案,耗时较久(大概3~4小时),所以需要选择适当时机更新。更新路线: 13.7.4-> 13.8.8->13.12.15-> 14.0.12-> 14.3.6=>14.6.2-> 14.9.5-> 14.10.5-> 15.0.2->15.1.0

services:
   gitlab:
    image: gitlab/gitlab-ce:15.3.3-ce.0
#    image: gitlab/gitlab-ce:15.1.0-ce.0
#    image: gitlab/gitlab-ce:15.0.2-ce.0
#    image: gitlab/gitlab-ce:14.10.5-ce.0
#    image: gitlab/gitlab-ce:14.9.5-ce.0
#    image: gitlab/gitlab-ce:14.6.2-ce.0
#    image: gitlab/gitlab-ce:14.3.6-ce.0
#    image: gitlab/gitlab-ce:14.0.12-ce.0
#    image: gitlab/gitlab-ce:13.12.15-ce.0
#    image: gitlab/gitlab-ce:13.8.8-ce.0
#    image: gitlab/gitlab-ce:13.7.4-ce.0

遇到的问题

  1. 在使用备份恢复GitLab时卡住
$ docker exec -it 7ddbcc0a6eb2 gitlab-rake gitlab:backup:restore BACKUP=1123456_2022_09_16_13.7.4
Unpacking backup ... done
Do you want to continue (yes/no)? yes

处理方式:进入容器,授权,执行恢复

# chown -R git:git /var/opt/gitlab/backups/备份.tar
root@23b5e49fc9ea:/var/opt/gitlab/backups# gitlab-rake gitlab:backup:restore 

备注:增加 gitlab-rake gitlab:backup:restore –trace 查看详细信息.

参考

前两篇(1.volume2.pv&pvc)通过部署redis学习实战了k8s的来Volume、PV和PVC。但是,应⽤程序存在“有状态”和“⽆状态”两种类别,显然redis属于读写磁盘需求的有状态应⽤程序,如⽀持事务功能的RDBMS存储系统,所以,本文学习实战k8s有状态应用的部署。

Stateful基础

StatefulSet是Pod资源控制器的⼀种实现,⽤于部署和扩展有状态应⽤的Pod资源,确保它们的运⾏顺序及每个Pod资源的唯⼀性。适用以下需求的应用:

  • 稳定且唯⼀的⽹络标识符。
  • 稳定且持久的存储。
  • 有序、优雅地部署和扩展。
  • 有序、优雅地删除和终⽌。
  • 有序⽽⾃动地滚动更新。

部署

接着,这里把之前的redis存储修改为stateful的方式,修改后的步骤:

  1. 创建 ConfigMap (参考第一篇)
  2. 修改 Deployment 为StatefulSets

修改部署StatefulSets

mkdir my-redis-statefulsets.yaml

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis
  namespace: my-ns
spec:
  replicas: 1
  serviceName: redis
  selector:
    matchLabels:
      name: redis
  template:
    metadata:
      labels:
        name: redis
    spec:
      containers:
        - name: redis
          image: redis
          resources:
            requests:
              cpu: 100m
              memory: 100Mi
#          command: ["redis-server","/etc/redis/redis.conf"]
          command:
            - redis-server
            - /etc/redis/redis.conf
          ports:
            - containerPort: 6379
          volumeMounts:
            - name: my-redis-config
              mountPath: /etc/redis/redis.conf
              subPath: redis.conf
            - name: my-redis-storage
              mountPath: /data
      volumes:
        - name: my-redis-storage
          emptyDir: {}
        - name: my-redis-config
          configMap:
            name: my-redis-config
            items:
              - key: redis.conf
                path: redis.conf
 
---
kind: Service
apiVersion: v1
metadata:
  labels:
    name: redis
  name: redis
  namespace: my-ns
spec:
  type: NodePort
  ports:
  - name: redis
    port: 6379
    targetPort: 6379
    nodePort: 30379
  selector:
    name: redis          

其中:

  1. Headless Service:⽤于为Pod资源标识符⽣成可解析的DNS资源记录
  2. StatefulSet ⽤于管控Pod资源
  3. volumeClaimTemplate则基于静态或动态的PV供给⽅式为Pod资源提供 专有且固定的存储(这里我们直接使用了第二篇创建的pv)

测试

redis-client 连接 NodeId:NodePort

# redis-cli -h YourNodeIp-p 30379 -a 123456
YourNodeIp:30379> info
# Serverredis_version:7.0.4

连接成功!

参考

之前学习实践使用熟悉卷(Volume)来存储利用k8s储存卷来部署redis,本文接着学习实践k8s的存储,主要通过redis存储例子学习实战PV和PVC。

PV & PVC

Kubernetes为例⽤户和开发隐藏底层架构的⽬标,在用户和存储服务之间添加了一个中间层,也就是PersistentVolume和PersistentVolumeClaim。

RedisVolume修改为PV&PVC

接着,这里把之前的redis存储修改为pv-pvc的方式,修改后的步骤:

  1. 创建 ConfigMap (参考第一篇)
  2. 增加声明PV和PVC (新增)
  3. 增加 Deployment (把Volume修改为 步骤2 的PVC)
  4. 暴露 Service (参考第一篇)

步骤2,增加声明PV和PVC

mkdir my-redis-pv-pvc.yaml

apiVersion: v1
kind: PersistentVolume
metadata:
  name: my-redis-pv
  labels:
     app: my-redis
spec:
  capacity:
    storage: 1Gi
  accessModes:
  - ReadWriteOnce
  hostPath:
    path: "/mnt/data/my-redis"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: redis-pvc
  namespace: my-ns
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi

执行创建

master# kubectl apply -f my-redis-pv-pvc.yaml

查看pv状态

master# kubectl get pv 
NAME    CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM     STORAGECLASS    REASON   AGE
my-redis-pv     1Gi        RWO            Retain           Bound    my-ns/redis-pvc                                                 50m

查看pvc状态

master# kubectl get pvc -n my-ns
NAME               STATUS   VOLUME         CAPACITY   ACCESS MODES   STORAGECLASS   AGE
redis-pvc          Bound    my-redis-pv    1Gi             RWO                                          54m

pv和pvc已经Bound成功。

步骤3,增加 Deployment (把Volume修改为 步骤2 的PVC)

mkdir my-redis-deployment-pvc.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-redis # Unique name for the deployment
  namespace: myns
  labels:
    app: my-redis       # Labels to be applied to this deployment
spec:
  selector:
    matchLabels:     # This deployment applies to the Pods matching these labels
      app: my-redis
      role: master
      tier: backend
  replicas: 1        # Run a single pod in the deployment
  template:          # Template for the pods that will be created by this deployment
    metadata:
      labels:        # Labels to be applied to the Pods in this deployment
        app: my-redis
        role: master
        tier: backend
    spec:            # Spec for the container which will be run inside the Pod.
      containers:
        - name: my-redis
          image: redis
          resources:
            requests:
              cpu: 100m
              memory: 100Mi
#          command: ["redis-server","/etc/redis/redis.conf"]
          command:
            - redis-server
            - /etc/redis/redis.conf
          ports:
            - containerPort: 6379
          volumeMounts:
            - name: my-redis-config
              mountPath: /etc/redis/redis.conf
              subPath: redis.conf
            - name: my-redis-storage
              mountPath: /data
      volumes:
        - name: my-redis-persistent-storage 
          persistentVolumeClaim:
          claimName: redis-pvc # 这里修改为步骤2声明的pvc
        - name: my-redis-config
          configMap:
            name: my-redis-config
            items:
              - key: redis.conf
                path: redis.conf

执行创建deployment

master# kubectl apply -f my-redis-deployment-pvc.yaml

检查状态

master# kubectl get pod -n my-ns
NAME                                   READY   STATUS    RESTARTS   AGE 
my-redis-6565459689-mbptf              1/1     Running   0          53m

测试

redis-client 连接 NodeId:NodePort

# redis-cli -h YourNodeIp-p 30379 -a 123456
YourNodeIp:30379> info
# Server
redis_version:7.0.4

连接成功。

参考

本文主要通过利用k8s如何部署Redis,来学习使用k8s的存储卷Volume。

Pod本⾝具有⽣命周期,故其内部运⾏的容器及其相关数据⾃⾝均⽆法持久存在。Kubernetes也⽀持类似Docker的存储卷功能,不过,其存储卷Volume是与Pod资源绑定⽽⾮容器。

Pod Volume

如何要在一个Pod里声明 Volume

  1. ⼀是通过.spec.volumes字段定义在Pod之上的存储卷列表,其⽀持使⽤多种不同类型的存储卷且配置参数差别很⼤;
spec:

volumes:
* name: logdata
emptyDir: {}
* name: example
gitRepo:
repository: https://github.com/iKubernetes/k8s_book.git
revision: master
directory: .
  1. 另⼀个是通过.spec.containers.volumeMounts字段在容器上定义的存储卷挂载列表,它只能挂载当前Pod资源中定义的具体存储卷,当然,也可以不挂载任何存储卷
spec:

containers:
* name: <String>

volumeMounts:
* name <string> -required-
mountPath <string> -required-

在之前(声明式对象配置)有介绍过nginx的部署,接着来部署Redis,和Nginx有所不同的是,这里多了一个 ConfigMap 和Volume ,用来配置管理redis和储存。

环境

  • 一个k8s 集群(mater* 1,node* 1)
  • 一个正常连接k8smater的主机的终端

创建 Config-Map

使用 ConfigMap 来配置 Redis ,包含了Redis配置文件里需要的配置项,在创建Pod时会作为配置文件挂载到应用所在的容器中。 my-config-map.yaml 具体如下:

apiVersion: v1
kind: ConfigMap
metadata:
  name: my-redis-config
  namespace: my-ns
data:
  redis.conf: |
    requirepass 123456
    protected-mode no
    port 6379
    tcp-backlog 511
    timeout 0
    tcp-keepalive 300
    daemonize no
    supervised no
    pidfile /var/run/redis_6379.pid
    loglevel notice
    logfile ""
    databases 16
    always-show-logo yes
    save 900 1
    save 300 10
    save 60 10000
    stop-writes-on-bgsave-error yes
    rdbcompression yes
    rdbchecksum yes
    dbfilename dump.rdb
    dir /data
    slave-serve-stale-data yes
    slave-read-only yes
    repl-diskless-sync no
    repl-diskless-sync-delay 5
    repl-disable-tcp-nodelay no
    slave-priority 100
    lazyfree-lazy-eviction no
    lazyfree-lazy-expire no
    lazyfree-lazy-server-del no
    slave-lazy-flush no
    appendonly no
    appendfilename "appendonly.aof"
    appendfsync everysec
    no-appendfsync-on-rewrite no
    auto-aof-rewrite-percentage 100
    auto-aof-rewrite-min-size 64mb
    aof-load-truncated yes
    aof-use-rdb-preamble no
    lua-time-limit 5000
    slowlog-log-slower-than 10000
    slowlog-max-len 128
    latency-monitor-threshold 0
    notify-keyspace-events Ex
    hash-max-ziplist-entries 512
    hash-max-ziplist-value 64
    list-max-ziplist-size -2
    list-compress-depth 0
    set-max-intset-entries 512
    zset-max-ziplist-entries 128
    zset-max-ziplist-value 64
    hll-sparse-max-bytes 3000
    activerehashing yes
    client-output-buffer-limit normal 0 0 0
    client-output-buffer-limit slave 256mb 64mb 60
    client-output-buffer-limit pubsub 32mb 8mb 60
    hz 10
    aof-rewrite-incremental-fsync yes    

执行命令

# kubectl apply -f my-redis-config.yaml

创建 Deployment

创建Deployment 作为调度Pod运行 Redis 的载体。my-redis-deployment.yaml具体如下

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-redis # Unique name for the deployment
  namespace: my-ns
  labels:
    app: my-redis       # Labels to be applied to this deployment
spec:
  selector:
    matchLabels:     # This deployment applies to the Pods matching these labels
      app: my-redis
      role: master
      tier: backend
  replicas: 1        # Run a single pod in the deployment
  template:          # Template for the pods that will be created by this deployment
    metadata:
      labels:        # Labels to be applied to the Pods in this deployment
        app: my-redis
        role: master
        tier: backend
    spec:            # Spec for the container which will be run inside the Pod.
      containers:
        - name: my-redis
          image: redis
          resources:
            requests:
              cpu: 100m
              memory: 100Mi
#          command: ["redis-server","/etc/redis/redis.conf"]
          command:
            - redis-server
            - /etc/redis/redis.conf
          ports:
            - containerPort: 6379
          volumeMounts:
            - name: my-redis-config
              mountPath: /etc/redis/redis.conf
              subPath: redis.conf
            - name: my-redis-storage
              mountPath: /data
      volumes:
        - name: my-redis-storage
          emptyDir: {}
        - name: my-redis-config
          configMap:
            name: my-redis-config
            items:
              - key: redis.conf
                path: redis.conf

执行

# kubectl apply -f my-redis-deployment.yaml

创建 service

NodePort 方式向外暴露服务。my-redis-service.yaml 具体如下

apiVersion: v1
kind: Service        # Type of Kubernetes resource
metadata:
  name: my-redis-svc # Name of the Kubernetes resource
  namespace: my-ns
  labels:            # Labels that will be applied to this resource
    app: my-redis
    role: master
    tier: backend
spec:
  type: NodePort
  ports:
    - port: 6379       # Map incoming connections on port 6379 to the target port 6379 of the Pod
      targetPort: 6379
      nodePort: 30379
  selector:          # Map any Pod with the specified labels to this service
    app: my-redis
    role: master
    tier: backend

执行

# kubectl apply -f my-redis-service.yaml

测试

redis-client 测试NodeId:NodePort

redis-cli -h YourNodeIp -p 30379 -a 123456
YourNodeIp:30379> info
# Server
redis_version:7.0.4
...

连接成功。

参考

本文主要介绍go如何读取和写入csv文件,以及使用第三方库gocsv转换为Struct。

读取csv

go 标准库 “encoding/csv” 用于读写csv文件。

Reader从csv编码的文件中读取记录

  • type Reader
    • func NewReader(r io.Reader) *Reader
    • func (r *Reader) Read() (record []string, err error) 。
    • func (r *Reader) ReadAll() (records [][]string, err error)

创建一个csv文件servers.csv,具体如下

world-svc,1/1,Running,0,44m
battle-svc,1/1,Running,0,7d

ReadAll从r中读取所有剩余的记录,每个记录都是字段的切片,成功的调用返回值err为nil而不是EOF。因为ReadAll方法定义为读取直到文件结尾,因此它不会将文件结尾视为应该报告的错误。

package main

import (
	"encoding/csv"
	"fmt"
	"io"
	"os"
)
func main() {
	readeCsvAll() 
}
func readerCsvReadAll() {
	file, err := os.Open("servers.csv")
	defer file.Close()
	if err != nil {
		fmt.Println(err)
	}
	reader := csv.NewReader(file)
	servers, _ := reader.ReadAll()
	fmt.Println("ReadAll:", servers)
}

OutPut:

ReadAll:  [world-svc 1/1 Running 0 44m] [battle-svc 1/1 Running 0 7d]]

Read从r读取一条记录,返回值record是字符串的切片,每个字符串代表一个字段。

reader := csv.NewReader(file)
for {
    servers, err := reader.Read()
    if err == io.EOF {
        break
    } else if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println(servers)
}

输出和ReadAll是一样的。

csv读写逗号分隔值(csv)的文件,亦或可以定义数据的分隔符

reader.Comma = '|' 

亦或可以定义忽略的的行

reader.Comment = '#'

写入csv

Writer类型的值将记录写入一个csv编码的文件

  • type Writer
    • func NewWriter(w io.Writer) *Writer
    • func (w *Writer) Write(record []string) (err error)
    • func (w *Writer) WriteAll(records [][]string) (err error)
    • func (w *Writer) Flush()
    • func (w *Writer) Error() error

向w中写入一条记录,会自行添加必需的引号。记录是字符串切片,每个字符串代表一个字段。

func writerOne() {
	servers := []Server{
		{"world-svc", "1/1", "Running", 0, "44m"},
		{"battle-svc", "1/1", "Running", 0, "7d"},
	}
	file, err := os.Create("serversA.csv")
	defer file.Close()
	if err != nil {
		log.Fatalln("failed to open file", err)
	}
	w := csv.NewWriter(file)
   //将缓存中的数据写入底层的io.Writer。
	defer w.Flush()
	// 使用 Write
	for _, server := range servers {
		row := []string{server.Name, server.Ready, server.State, strconv.Itoa(server.Restarts), server.Age}
		if err := w.Write(row); err != nil {
			log.Fatalln("error writing server to file", err)
		}
	}

执行,在当前目录生成 serversA.csv,内容如下:

world-svc,1/1,Running,0,44m
battle-svc,1/1,Running,0,7d

WriteAll方法使用Write方法向w写入多条记录,并在最后调用Flush方法清空缓存

	var data [][]string
	for _, server := range servers {
		row := []string{server.Name, server.Ready, server.State, strconv.Itoa(server.Restarts), server.Age}
		data = append(data, row)
	}
	w.WriteAll(data)

To Struct

GoCSV包旨在提供 CSV 和 Go (golang) 值之间的快速和惯用的映射。 已有servers.csv 内容如下

Name,Ready,Status,Restart,Age
world-svc,1/1,Running,0,44m
battle-svc,1/1,Running,0,7d

UnmarshalFile(in *os.File, out interface{}) error UnmarshalFile从接口中的文件解析CSV。

func ToStruct() {
	clientsFile, err := os.OpenFile("servers.csv", os.O_RDWR|os.O_CREATE, os.ModePerm)
	if err != nil {
		panic(err)
	}
	defer clientsFile.Close()

	var servers []*Server
    //转换成 Server Object
	if err := gocsv.UnmarshalFile(clientsFile, &servers); err != nil {  
		panic(err)
	}
	for _, server := range servers {
		fmt.Println("Server", server.Name)
	}
}

OuPut:

Server world-svc
Server battle-svc

更多api使用方式可参考:https ://github.com/gocarina/gocsv

参考

访问Rancher后台提示“证书无效”。

排查原因是由于挂载的ssl证书失败,rancher启用了生成的默认 CA 证书。这里打算 更新为公认的 CA 签名证书,其中

  1. Rancher版本 v2.6.6
  2. Racher为Docker单节点安装

解决步骤

考虑到单机版是没有Volume的,为了保证数据不丢失。处理大致分为以下几个步骤

  1. 从你的 Rancher 服务器容器创建数据的副本
  2. 创建一个备份压缩包
  3. 使用新的证书 启动新的Rancher服务器容器

1. 从你的 Rancher 服务器容器创建数据的副本

1.1 停止当前运行 Rancher 服务器的容器。替换<RANCHER_CONTAINER_NAME>为您的 Rancher 容器的名称

运行停止RancherDocker

docker stop <RANCHER_CONTAINER_NAME>
1.2 使用以下命令替换每个占位符,从您刚刚停止的 Rancher 容器创建一个数据容器。
docker create --volumes-from <RANCHER_CONTAINER_NAME> --name rancher-data rancher/rancher:<RANCHER_CONTAINER_TAG>

2. 创建一个备份压缩包

从您刚刚创建的数据容器 ( rancher-data),创建一个备份 tarball ( rancher-data-backup-<RANCHER_VERSION>-.tar.gz)。如果升级期间出现问题,此 tarball 将用作回滚点。使用以下命令,替换每个占位符。

docker run --volumes-from rancher-data -v "$PWD:/backup" --rm busybox tar zcvf /backup/rancher-data-backup-<RANCHER_VERSION>-<DATE>.tar.gz /var/lib/rancher

3. 启动新的Rancher服务器容器

如果您选择使用由公认的 CA 签名的证书,您将添加–volumes-from rancher-data到您启动原始 Rancher 服务器容器的命令中,并且需要访问您最初安装时使用的相同证书。请记住将–no-cacerts作为参数包含在容器中以禁用 Rancher 生成的默认 CA 证书。

docker run -d --volumes-from rancher-data \
  --restart=unless-stopped \
  -p 80:80 -p 443:443 \
  -v /[your-workspace]/certs:/etc/rancher/ssl \ 
  --privileged \
  rancher/rancher:v2.6.6 \
  --no-cacerts

这里的 [your-workspace]替换为你的证书存放路径。其中包括 cert.pem 和 key.pem

再访问rancher dashboard,提示“有效证书”。

参考

jefffff

Stay hungry. Stay Foolish COOL

Go backend developer

China Amoy