比较常见pprof 可以把调用栈可视化成调用图,embedded-struct-visualizer 可以把Go 的项目的代码分层结构和依赖都可视化成流程图。

安装 embedded-struct-visualizer

 go install github.com/davidschlachter/embedded-struct-visualizer@latest

查看命令选项参数

embedded-struct-visualizer -h
Usage: [OPTIONS] DirToScan
If the directory to scan is not provided, it defaults to './'
OPTIONS:
  -out <file>  path to output file (default: write to stdout)
  -v           verbose logging

Example

以官方给的例子 main.go

package main

import (
	"time"
)

type A struct {
	B
	C map[string]D
}

type B struct {
	E, F  string
	G     string
	Timer H
}

type D struct {
	I uint64
}

type H struct {
	Timer time.Ticker
	J     chan D
}

生成结构关系:

> embedded-struct-visualizer
digraph {
"main.A" -> { "main.B" "main.D" };
"main.B" -> { "main.H" };
"main.H" -> { "time.Ticker" "main.D" };
}

生成结构关系 并 输出到.gv 文件:

>embedded-struct-visualizer -out .\example.gv
> ls

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----        2022/11/27     10:54            112 example.gv
-a----        2022/11/26     22:06             31 go.mod
-a----        2022/11/26     22:07            208 main.go

项目目录下多出了example.gv ,具体内容如下

cat .\example.gv
digraph {
"main.A" -> { "main.B" "main.D" };
"main.B" -> { "main.H" };
"main.H" -> { "time.Ticker" "main.D" };

安装 graphviz 

graphviz 是一种将结构信息表示为抽象图和网络的图的工具。

接着,把生成的gv文件利用graphviz绘制的更美观。

首先下载安装:

https://graphviz.org/download/ 查看是否安装成功(Win):

 dot -v
dot - graphviz version 7.0.2 (20221119.0110)
... <为了不篇幅过长,省略一些细节>

利用graphviz绘图

dot -Tformat[:renderer[:formatter]] -o 将输出语言设置为支持的格式之一。默认情况下,生成带属性的点。

接着把上一步生成的gv文件,生成PNG输出

dot -Tpng example.gv -o example.png

在项目目录下多出example.png

接着,实用于一个相对复杂的Gin Engine的gv

  1. 源码链接: https://github.com/gin-gonic/gin/blob/master/gin.go

  2. 生成的gv:

embedded-struct-visualizer -out .\gin.gv

gin.gv 详细如下:

digraph {"gin.mockWriter" -> { "http.Header" };"binding.QueryTest" -> { "binding.appkey" };"binding.FooBarStruct" -> { "binding.FooStruct" };"binding.FooBarFileStruct" -> { "binding.FooBarStruct" 
"multipart.FileHeader" };"binding.FooBarFileFailStruct" -> { "binding.FooBarStruct" 
"multipart.FileHeader" };"binding.FooDefaultBarStruct" -> { "binding.FooStruct" };"binding.FooStructUseNumber" -> { "binding.any" };"binding.FooStructDisallowUnknownFields" -> { "binding.any" 
};"binding.FooBarStructForTimeType" -> { "time.Time" };"binding.FooStructForTimeTypeNotUnixFormat" -> { "time.Time" 
};"binding.FooStructForTimeTypeNotFormat" -> { "time.Time" };"binding.FooStructForTimeTypeFailFormat" -> { "time.Time" };"binding.FooStructForTimeTypeFailLocation" -> { "time.Time" 
};"binding.FooStructForMapType" -> { "binding.any" };"binding.FooStructForIgnoreFormTag" -> { "binding.string" };"binding.defaultValidator" -> { "sync.Once" 
"validator.Validate" };"binding.structFull" -> { "binding." "time.Time" 
"binding.string" };"binding.S" -> { "binding.S" };"binding.testFile" -> { "binding.byte" };"binding.structNoValidationValues" -> { 
"binding.substructNoValidation" "binding.int16" 
"binding.uint16" "time.Time" "binding.mapNoValidationSub" 
"binding." };"binding.structNoValidationPointer" -> { 
"binding.substructNoValidation" "binding.uint32" 
"binding.mapNoValidationSub" "binding.int8" "binding.int32" 
"binding.uint8" "binding.uint16" "binding.float64" 
"binding.map" "binding.int" "binding.int16" "binding.int64" 
"binding.float32" "time.Time" "binding.testInterface" 
"binding.uint" "binding.uint64" "binding.string" };"gin.Context" -> { "url.Values" "gin.responseWriter" 
"gin.HandlersChain" "gin.any" "gin.errorMsgs" 
"gin.skippedNode" "sync.RWMutex" "gin.string" 
"http.SameSite" "http.Request" "gin.ResponseWriter" 
"gin.Params" "gin.Engine" };"gin.interceptedWriter" -> { "gin.ResponseWriter" 
"bytes.Buffer" };"gin.Error" -> { "gin.error" "gin.ErrorType" "gin.any" };"gin.onlyFilesFS" -> { "http.FileSystem" };"gin.neuteredReaddirFile" -> { "http.File" };"gin.RouteInfo" -> { "gin.HandlerFunc" };"gin.Engine" -> { "gin.RouterGroup" "gin.string" 
"render.HTMLRender" "gin.HandlersChain" "sync.Pool" 
"gin.methodTrees" "render.Delims" "template.FuncMap" 
"gin.uint16" "net.IPNet" };"gin.LoggerConfig" -> { "gin.LogFormatter" "io.Writer" 
"gin.string" };"gin.LogFormatterParams" -> { "http.Request" "time.Time" 
"time.Duration" "gin.any" };"render.Data" -> { "render.byte" };"render.HTMLProduction" -> { "template.Template" 
"render.Delims" };"render.HTMLDebug" -> { "render.string" "render.Delims" 
"template.FuncMap" };"render.HTML" -> { "template.Template" "render.any" };"render.JSON" -> { "render.any" };"render.IndentedJSON" -> { "render.any" };"render.SecureJSON" -> { "render.any" };"render.JsonpJSON" -> { "render.any" };"render.AsciiJSON" -> { "render.any" };"render.PureJSON" -> { "render.any" };"render.MsgPack" -> { "render.any" };"render.ProtoBuf" -> { "render.any" };"render.Reader" -> { "io.Reader" };"render.Redirect" -> { "http.Request" };"render.String" -> { "render.any" };"render.TOML" -> { "render.any" };"render.XML" -> { "render.any" };"render.YAML" -> { "render.any" };"gin.responseWriter" -> { "http.ResponseWriter" };"gin.RouterGroup" -> { "gin.HandlersChain" "gin.Engine" };"protoexample.Test" -> { "protoexample.int32" 
"protoexample.int64" "protoexample.TestOptionalGroup" 
"protoimpl.MessageState" "protoimpl.SizeCache" 
"protoimpl.UnknownFields" "protoexample.string" };"protoexample.Test_OptionalGroup" -> { 
"protoimpl.MessageState" "protoimpl.SizeCache" 
"protoimpl.UnknownFields" "protoexample.string" };"gin.methodTree" -> { "gin.node" };"gin.node" -> { "gin.node" "gin.HandlersChain" 
"gin.nodeType" };"gin.nodeValue" -> { "gin.HandlersChain" "gin.Params" };"gin.skippedNode" -> { "gin.node" "gin.int16" };}


  1. 绘制png
 dot -Tpng gin.gv -o gin.png

<ps: 图片过大可下载预览>

参考

本文主要学习记录Kubernetes集群暴露服务的方式: Ingress。

简介

Ingress 是对集群中服务的外部访问进行管理的 API 对象,典型的访问方式是 HTTP。Ingress 可以提供负载均衡、SSL 终结和基于名称的虚拟托管。

IngressController 为了让 Ingress 资源工作,集群必须有一个正在运行的 Ingress 控制器。

上图清晰标识出了Ingress的流量走向,其中:

  • Ingress基于DNS名称(host)或URL路径把请求转发⾄指定的Service资源的规则。它仅是⼀组路由规则的集合。
  • Ingress控制器是真正实现“流量穿透”,可以由具有反向代理(HTTP/HTTPS)功能的服务程序实现 , 然后根据这些规则的匹配机制路由请求流量

Ingress 资源声明

Ingress是Kubernetes API的标准资源类型之⼀ ,一个最小Ingress例子:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: minimal-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx-example
  rules:
  - http:
      paths:
      - path: /testpath
        pathType: Prefix
        backend:
          service:
            name: test
            port:
              number: 80

其中:

  • Ingress 需要指定 apiVersion、kind、 metadata和 spec 字段。
  • Ingress 对象的命名必须是合法的 DNS 子域名名称。
  • Ingress annotations 来配置一些选项, ⽤于识别其所属的Ingress控制器的类别。
  • Ingress rules 提供了配置负载均衡器或者代理服务器所需的所有信息。 最重要的是,其中包含与所有传入请求匹配的规则列表。 Ingress 资源仅支持用于转发 HTTP(S) 流量的规则。

更多参考: https://kubernetes.io/zh-cn/docs/concepts/services-networking/ingress/

Ingress Controller

Ingress控制器可以由任何具有反向代理(HTTP/HTTPS)功能的服务程序实现,目前支持和维护 AWS、 GCE 和 Nginx Ingress 控制器。

  • Nginx Ingress 作为反向代理和负载均衡器。
  • Apache APISIX Ingress 控制器 是一个基于 Apache APISIX 网关 的 Ingress 控制器。

更多参考:https://kubernetes.io/zh-cn/docs/concepts/services-networking/ingress-controllers/

安装 Ingress Nginx

wget https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.0.0/deploy/static/provider/cloud/deploy.yaml  -O ingress-nginx-deploy.yaml

kubectl apply -f  ingress-nginx-deploy.yaml

观测是否成功:

kubectl get pods -n ingress-nginx --watch

如果成功的话,查看namespace下所有的资源信息

root@master:/home/master# kubectl get all -n ingress-nginx
NAME                                            READY   STATUS    RESTARTS   AGE
pod/ingress-nginx-controller-123456   1/1     Running   0          1h

NAME                                         TYPE           CLUSTER-IP       EXTERNAL-IP    PORT(S)                      AGE
service/ingress-nginx-controller             LoadBalancer   10.104.182.98    192.168.1.100   80:31666/TCP,443:31888/TCP   1d
service/ingress-nginx-controller-admission   ClusterIP      10.111.228.99   <none>         443/TCP                      1d

NAME                                       READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/ingress-nginx-controller   1/1     1            1           1d

NAME                                                  DESIRED   CURRENT   READY   AGE
replicaset.apps/ingress-nginx-controller-123456   1         1         1      1d

NAME                                       COMPLETIONS   DURATION   AGE
job.batch/ingress-nginx-admission-create   1/1           1s         1d
job.batch/ingress-nginx-admission-patch    1/1           2s         1d

使用Ingress 发布 Tomcat

部署Tomcat Service

  1. 创建deployment

        kubectl create deployment web --image=tomcat:8.0.50-jre8-alpine
    
  2. 将 Deployment 暴露出来

    kubectl expose deployment web --type=NodePort --port=8080
    
    
  3. 将 Deployment 暴露出来

    root@master:/home/master# kubectl get service web
        NAME   TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
        web    NodePort   10.100.183.22   <none>        8080:32562/TCP   30s
    
  4. 验证nodeport 是否正常访问 tomcat ,浏览器访问 http://matster_ip:32562

  5. 创建 ingress

 apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: ingressfoo
      annotations:
        kubernetes.io/ingress.class: "nginx"
      namespace: default
    spec:
      rules:
      - host: ingressfoo.io
        http:
          paths:
          - backend:
              service:
                name: web
                port:
                  number: 8080
            path: /bar
            pathType: Prefix

查看ingress是否创建成功

root@master:/home/master# kubectl get ingress
NAME         CLASS    HOSTS                     ADDRESS        PORTS   AGE
ingressfoo   <none>   ingressfoo.io   192.168.1.100   80      1h

说明Ingress创建成功,修改hosts :

ingressfoo.io 192.168.1.100

验证访问 ingressfoo.io/bar

success

参考

本文简要介绍Go网络抓包、引流工具GoReplay。

前言

在后端的实际开发中,会遇到以下一些场景:

  • 用户通过作弊手段绕过前端,利用抓包,进行破解,模拟,魔改交互数据进行伪装。后端程序需要对其操作记录进行重现和跟踪。
  • 某个bug在测试环境无法复现。
  • 服务压测数据和线上数据有偏差,压测数据希望能和线上接近 。

GoReplay是Go语言编写的流量回放工具,侦听器服务器捕获http流量并将其发送到重放服务器或保存到文件。重播服务器将流量转发给给定的地址。

安装

测试环境: Windows。 版本: Version:1.3.0 安装分2步骤:

  1. 前置条件: 安装npcap https://npcap.com

GoReplay可以在Windows机器上工作,但由于Windows堆栈的不同网络层的性质,它有一些细节。 默认情况下,Windows不像Unix系统那样有支持包捕获的网络驱动程序,如果您想捕获通信量,则必须单独安装它。其中一个选项是安装https://nmap.org/npcap/。

  1. 安装:这里测试使用的是 gor-1.3.3_windows.zip https://github.com/buger/goreplay/releases 下载解压即可。

捕获流量

启动两个http服务:

  1. httpServerA: http://localhost:8000 监听端口8000
  2. httpServerB: http://localhost:8001 监听端口8001

路由为 “get: “/helloworld/{name}”

监听端口8000,并输出到stdout

 ./gor --input-raw :8000 --output-stdout

打开浏览器请求接口 http://localhost:8000/helloworld/lilei

在gor的命令行窗口:

1 db0a1f4000000001c344a1d3 1668261985986369000 0
GET /helloworld/lilei HTTP/1.1
Host: localhost:8000
Connection: keep-alive
sec-ch-ua: " Not A;Brand";v=" 此处省略一些系数
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
此处省略一些参数

可见,gor已经把整个http请求的完整信息记录下来。 同时gor还支持输出到文件和ElasticSearch,进行回放和分析。记录到文件request.gor

 ./gor --input-raw :8000 --output-file request.gor --input-raw-track-response --input-raw-override-snaplen

打开浏览器请求接口 http://localhost:8000/helloworld/hanmeimei

写入到文件 request_0.gor

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ---- 
-a----        2022/11/12     21:36           1876 request_0.gor

request_0 内容如下:

1 d4571f4000000001b7adc1a6 1668260139796335000 0
GET /helloworld/lilei HTTP/1.1
Host: localhost:8000
Connection: keep-alive
sec-ch-ua: " Not A;Brand";v="99", 此处省略一些参数
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64), 此处省略一些参数
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
此处省略一些参数


🐵🙈🙉
2 d4571f4000000001b7adc1a6 1668260139796652000 0
HTTP/1.1 200 OK
Content-Type: application/json
Date: Sat, 12 Nov 2022 13:35:39 GMT
Content-Length: 25

{"message":"Hello lilei"}
🐵🙈🙉
1 d4571f4000000001b7adc22b 1668260148757949000 0
GET /helloworld/hanmeimei HTTP/1.1
Host: localhost:8000
Connection: keep-alive
sec-ch-ua: " Not A;Brand";v="99", 此处省略一些参数
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) , 此处省略一些参数
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
此处省略一些参数


🐵🙈🙉
2 d4571f4000000001b7adc22b 1668260148758268000 0
此处省略一些参数

{"message":"Hello hanmeimei"}
🐵🙈🙉

流量回放

接着将该文件request_0.gor 进行流量回放,并转发到另外一台服务器(HttpServerB)上:

./gor --input-file request_0.gor --output-http http://localhost:8001

观察HttpServerB服务器控制台输出:

INFO ts=2022-11-12T21:37:26+08:00 caller=greeter.go:44 service.id=LAPTOP-H3HBMV8A service.name= service.version= trace.id= span.id= msg=CreateGreeter: lilei
INFO ts=2022-11-12T21:37:34+08:00 caller=greeter.go:44 service.id=LAPTOP-H3HBMV8A service.name= service.version= trace.id= span.id= msg=CreateGreeter: hanmeimei

从日志中可以看到两个请求(http://localhost:8000/helloworld/ilei、http://localhost:8000/helloworld/hanmeimei)分别重放请求到了HttpServerB。

gor支持进行流量缩小、放大、倍速重放,实现真实流量的压测效果。 更多命令 ./gor -help 查看

参考

本文主要介绍Kubernetes集群资源监控机制和资源指标 前言 临近双11,对于码农,尤其是后端,尤其是某宝某东的后端,那是多么激动人心(心惊胆战)的一夜,为什么?怕宕机呀~。那么就让我们来构建护城河–监控与自动扩容,来抵挡千军万马–高并发场景。首先让我们学习一些基础:k8s集群资源监控机制和资源指标。 分析 从需求出发,我们自然需要收集一些数据(资源指标),再根据指标做一系列的操作(control),比如说预警警告、统计、自动扩容等。 首先,我们希望可以监控整个Kubernetes集群的健康状况,包括: 整个集群的资源利⽤率 集群中的所有⼯作节点是否运⾏正常、系统资源容量⼤⼩ 每个⼯作节点上运⾏的容器化应⽤的数量 k8s资源控制 我们看看k8s如何资源控制 限制节点 以购物平台为例,微服务广受推崇,比如双11当天,用户进首页是流畅的,但是进活动主页就卡顿,到了零点时,加入购物车的按钮都转圈了。设想你的设计可能是三个微服务(主页服务 、双十一活动服务、订单服务),可想而知,活动服务是压力最大的,我们希望,就算宕机,不要影响其他的服务。所以可以把活动服务限制运行在某节点。 创建一个会被调度到特定节点上的 Pod,你也可以通过设置 nodeName 将某个 Pod 调度到特定的节点 nodeName: foo-node # 调度 Pod 到特定的节点 限制内存 还是上面的例子,我们把活动服务调度到了 foo-node。那么剩下的服务没有去限制,但想想订单服务的压力也不小,这里我们希望限制它的资源上限。 要为容器指定内存请求,请在容器资源清单中包含 resources:requests 字段。 同理,要指定内存限制,请包含 resources:limits。 resources: requests: memory: "1000Gi" limits: memory: "1000Gi" 限制CPU 要为容器指定 CPU 请求,请在容器资源清单中包含 resources: requests 字段。 要指定 CPU 限制,请包含 resources:limits resources: limits: cpu: "100" requests: cpu: "100" Example 环境准备: k8s集群(master * 1,node * 2 ) kubectl 命令行工具( 笔者使用rancher) 创建一个namespace(stress)。 增加一个deployment, 修改内存上限为10M 1.

前言

游戏排行榜是一个常见需求,今天主要介绍go语言使用redis-sort-sets来实现排行榜常见功能。

Redis Sort Sets

官方介绍:

Redis排序集是按相关分数排序的惟一字符串(成员)的集合。当多个字符串具有相同的分数时,字符串将按字典顺序排列。排序集的一些用例包括:

游戏排行榜。例如,您可以使用排序集轻松地维护大型在线游戏中最高分数的有序列表。 速率限制器。特别是,您可以使用一个排序集来构建滑动窗口速率限制器,以防止过多的API请求。

常用于排行榜的命令:

  • ZRANGE 返回排序集的成员(升序)
  • ZREVANGE 返回排序集的成员(降序序)
  • ZADD 向排序集添加一个新成员和相关分数。如果成员已经存在,则更新评分。
  • ZREM 删除排序集一个成员
  • ZRANK 返回提供的成员的排名(升序)
  • ZREVRANK 返回提供的成员的排名(降序)

Go Redis

Go Redis为各种风格的Redis提供Go客户端,基础的使用例子如下:

package main

import (
	"context"
	"fmt"
	"github.com/go-redis/redis/v9"
)

func main() {
	ctx := context.Background()
	rdb := redis.NewClient(&redis.Options{
		Addr:     "localhost:6379", // use default Addr
		Password: "",               // no password set
		DB:       0,                // use default DB
	})
	pong, err := rdb.Ping(ctx).Result()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(pong, err)
	if err != nil {
		fmt.Println(err)
		return
	}
    defer rdb.Close()
}

Examples

这里我们用go-redis实现排行榜常见的功能,包括:

  • 获取排行榜成员和分数 (升序、降序)
  • 加入排行榜成员
  • 删除排行榜成员
  • 更新排行榜成员分数
  • 获取排行榜成员分数
  • 获取排行榜名次成员
  • 清空排行榜

完整代码如下:

package main

import (
	"context"
	"fmt"
	"github.com/go-redis/redis/v9"
	"math/rand"
	"strconv"
)

func main() {
	ctx := context.Background()
	rdb := redis.NewClient(&redis.Options{
		Addr:     "localhost:6379", // use default Addr
		Password: "",               // no password set
		DB:       0,                // use default DB
	}) 
	var err error
	defer rdb.Close()
	//增加玩家分数的变化
	for i := 1; i <= 5; i++ {
		err = rdb.ZAdd(ctx, "leaderboard", redis.Z{
			Score:  float64(rand.Intn(100)),
			Member: "user:" + strconv.Itoa(i),
		}).Err()
		if err != nil {
			fmt.Println(err)
			return
		}
	}
	if err != nil {
		fmt.Println(err)
		return
	}
	//查询排行榜成员-升序
	members, err := rdb.ZRange(ctx, "leaderboard", 0, 5).Result()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("查询排行榜成员-升序", members)
	//查询排行榜成员和分数-升序
	membersWithScore, err := rdb.ZRevRangeWithScores(ctx, "leaderboard", 0, 5).Result()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("查询排行榜成员和分数-升序", membersWithScore)

	//查询排行榜成员-降序
	membersRev, err := rdb.ZRevRange(ctx, "leaderboard", 0, 5).Result()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("查询排行榜成员-降序", membersRev)
	//查询排行榜成员和分数-升序
	membersRevWithScore, err := rdb.ZRangeWithScores(ctx, "leaderboard", 0, 5).Result()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("查询排行榜成员和分数-升序", membersRevWithScore)

	//获取某玩家的分数
	user2Score, err := rdb.ZScore(ctx, "leaderboard", "user:2").Result()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("获取某玩家 user:2 的分数", user2Score)
	//更新某玩家的分数
	err = rdb.ZAdd(ctx, "leaderboard", redis.Z{
		Score:  user2Score,
		Member: "user:1",
	}).Err()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("更新某玩家 user:1 的分数", user2Score)
	//查询排行榜成员和分数-升序
	membersRevWithScore, err = rdb.ZRangeWithScores(ctx, "leaderboard", 0, 5).Result()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("查询排行榜变化后的排行榜成员和分数-升序", membersRevWithScore)

	//查询排行榜某玩家的分数-升序
	memberRank, err := rdb.ZRank(ctx, "leaderboard", "user:2").Result()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("查询排行榜某玩家  user:2 的排名", memberRank)

	//查询排行榜某玩家的分数-降序
	memberRevRank, err := rdb.ZRevRank(ctx, "leaderboard", "user:2").Result()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("查询排行榜某玩家 user:2 的排名-降序", memberRevRank)

	//删除排序集一个成员
	zrem, err := rdb.ZRem(ctx, "leaderboard", "user:1").Result()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("删除排序集一个成员", zrem)

	//删除后查询排行榜成员-升序
	members, err = rdb.ZRange(ctx, "leaderboard", 0, 5).Result()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("删除后查询排行榜成员-升序", members)

	//清空排行榜
	clear, err := rdb.ZRemRangeByRank(ctx, "leaderboard", 0, 4).Result()
	if err != nil {
		fmt.Println("err zrem", err)
		return
	}
	fmt.Println("清空排行榜", clear)
	//清空后查询排行榜成员-升序
	members, err = rdb.ZRange(ctx, "leaderboard", 0, 5).Result()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("清空后查询排行榜成员-升序", members) 

OutPut:

查询排行榜成员-升序 [user:3 user:4 user:1 user:5 user:2]
查询排行榜成员和分数-升序 [{87 user:2} {81 user:5} {81 user:1} {59 user:4} {47 user:3}]
查询排行榜成员-降序 [user:2 user:5 user:1 user:4 user:3]
查询排行榜成员和分数-升序 [{47 user:3} {59 user:4} {81 user:1} {81 user:5} {87 user:2}]
获取某玩家 user:2 的分数 87
更新某玩家 user:1 的分数 87
查询排行榜变化后的排行榜成员和分数-升序 [{47 user:3} {59 user:4} {81 user:5} {87 user:1} {87 user:2}]
查询排行榜某玩家  user:2 的排名 4
查询排行榜某玩家 user:2 的排名-降序 0
删除排序集一个成员 1
删除后查询排行榜成员-升序 [user:3 user:4 user:5 user:2]
清空排行榜 4
清空后查询排行榜成员-升序 []

参考

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 查看详细信息.

参考

jefffff

Stay hungry. Stay Foolish COOL

Go backend developer

China Amoy