问题描述:

在Rancher导入外部k8s集群 ,导入的集群一直处于“pending”状态。

这里简要记录一下排查过程和临时解决办法。

问题环境

  • rancher v2.6.6
  • kubernetes v1.24
  • vm1 ubuntu (rancer)
  • vm2-3-4 k8s集群

导入步骤:“集群”–“添加”–“集群名称”–创建–

排查&解决

Rancher 通过 CusterAgent 与集群进行通信(通过cattle-cluster-agent调用 Kubernetes API 与集群通讯),并通过cattle-node-agent与节点进行通信。如果 cattle-cluster-agent 无法连接到已有的 Rancher Server 也就是server-url,集群将保留在 Pending 状态。

1 . 在k8smaster节点上面,查看ClusterAgent的pod状态是否正常,默认的namespace为“cattle-system”

kubectl get pods -n cattle-system

root@k8s-master:~# kubectl get pods -n cattle-system
NAME                             READY   STATUS    RESTARTS   AGE
cattle-cluster-agent-79749...   1/0     Pending   0              8m52s
cattle-cluster-agent-79749...   1/0     Pending   0              5m21s

看到pod未正常启动

2. 进一步查看pod错误日志

kubectl logs -f [contariner-id] -n cattle-system

root@k8s-master:~# kubectl logs -f cattle-cluster-agent-79749... -n cattle-system
...
no secret assigned to service account cattle-system/rancher
...
3. 查找error处理办法

通过查阅官方issures有提到

2.6.6版本说明建议:

Rancher v2.6.6是v2.6.5的镜像版本,只做了一个修改,以解决以下问题:

-当Rancher试图控制来自下游集群的大量流量时,出现了一个主要的性能问题。这种机制没有正确地处理断开,并将导致无限锁定。看到# 37250。

所以我猜他们没有在这个版本中添加K8S 1.24兼容性,因为v2.6.6中的兼容版本是:

  • v1.18.20-rancher1-3
  • v1.19.16-rancher1-5
  • v1.20.15-rancher1-3
  • v1.21.12-rancher1-1
  • v1.22.9-rancher1-1
  • v1.23.6-rancher1-1

Rancher 2.6.6 不支持 1.24。根据在此问题上设置的里程碑,计划为 2.6.7 提供支持。

4.尝试回滚版本

回滚k8s版本v1.23,rancher重新导入集群,再查看pods状态

kubectl get pods -n cattle-system
NAME                                    READY   STATUS    RESTARTS   AGE
cattle-cluster-agent-79749...   1/1     Running   0          8m52s
cattle-cluster-agent-79749...   1/1     Running   0          5m21s

回到rancher查看导入的k8s状态为 “Active”

至此,问题已暂时解决。持续关注Rancher2.6.7版本。

参考

本文主要记录Kubernetes的资源管理,包括资源配置,声明式管理。

Kubernetes提供了RESTful风格的API,它将各类组件均抽象为“资源”,并通过属性赋值完成实例化 。API主要由资源类型和控制器两部分组成,资源通常以json、yaml格式并写入集群的对象,控制器则在集群资源存储完成后自动创建并启动。

常用的K8s资源有

  • Pod
  • Deployment
  • Service
  • Ingress

资源分类

依据资源的主要功能作为分类标准,Kubernetes的API对象⼤体可分为

  • ⼯作负载 (Workload)

    • ReplicationController
    • ReplicaSet
    • Deploymen
    • StatefulSet
    • DaemonSet
    • Job
  • 发现和负载均衡 (Discovery&LB)

  • 配置和存储 (Config&Storage

  • 集群 (Cluster)

    • Namespace
    • Node
    • Role
    • ClusterRole
  • 元数据 (Metadata)

它们基本上都是围绕⼀个核⼼⽬的⽽设计:如何更好地运⾏和丰富Pod资源,从⽽为容器化应⽤提供更灵活、更完善的操作与管理组件。

资源配置

标准格式一般包括一级字段

  • kind
  • apiVersion
  • metadata (对象元数据)
  • spec(描述所期望的对象应该具有的状态)
  • status(字段在对象创建后由系统⾃⾏维护)

以一个nginx的pod.yaml为例


apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod
  labels:
    app: nginx-pod
spec:
  containers:
    - name: nginx
      image: nginx:1.7.9
      ports:
          - containerPort: 80
  • metadata:
    • name 当前的对象名称
    • labels: 当前对象的标签(键值对)
  • spec
    • containers,它的值是⼀个容器对象列表,⽀持嵌套创建⼀到 多个容器。

声明式对象配置

提供配置清单文件给k8s系统,并委托系统来跟踪活动对象的状态变动。管理操作的命令通过apply

  • 创建
$kubectl apply -f <directory>/
  • 更新
kubectl apply -f <directory>/
  • 删除
kubectl apply -f <directory>/ --prune -l your-label 

建议使用命令式的方法

kubectl delete -f  <filename>

实战声明式部署nginx

定义的deployment文件: nginx_dp.yaml


apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx-dp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx-pod
  template:
    metadata:
      labels:
        app: nginx-pod
    spec:
      containers:
        - name: nginx
          image: nginx:1.7.9
          ports:
            - containerPort: 80

暴露服务,定义service文件nginx_svc.yaml

apiVersion: v1
kind: Service
metadata:
  name: nginx-svc
spec:
  type: NodePort
  selector:
    app: nginx-pod
  ports:
    - name: default
      protocol: TCP
      port: 80
      targetPort: 80

访问nginx,output

参考

原子操作

原子操作即执行过程不能被中断的操作。在针对某个值的原子操作执行过程当值,cpu绝不会再去执行其他针对该值的操作,无论这些其他操作是否为原子操作。

go-atomic

查看Mutex、RWMutex的源码,底层是通过atomic包中的一些原子操作来实现。

Go标准库 sync/atomic 提供了对基础类型 int32、int64、uint32、uint64、uintptr、Pointer(Add 方法不支持) 的原子级内存操作。其中包括:

  • Add (给第一个参数地址中的值增加一个 delta 值)
  • CompareAndSwap(判断相等即替换))
  • Swap(不需要比较旧值,直接替换,返回旧值)
  • Load(方法会取出 addr 地址中的值)
  • Store( 方法会把一个值存入到指定的 addr 地址中)

我们通过一个例子可以快速了解atomic封装的这些api

package main

import (
	"fmt"
	"sync/atomic"
)

func main() {
	var x int32 = 0

	//func AddInt32(addr *int32, delta int32) (new int32)
	y := atomic.AddInt32(&x, int32(1))
	fmt.Printf("x=%d,y=%d \n", x, y)
	//OutPut: x=1,y=1

	// func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
	// addr 地址里的值是不是 old,如果不等于 old,就返回 false;如果等于 old,就把此地址的值替换成 new 值,返回 true
	isCompare := atomic.CompareAndSwapInt32(&x, int32(0), int32(2))
	fmt.Printf("isCompare=%v,x=%d  \n", isCompare, x)
	//OutPut: isCompare=false,x=1
	//不相等,故x还是1
	isCompare2 := atomic.CompareAndSwapInt32(&x, int32(1), int32(2))
	fmt.Printf("isCompare=%v,x=%d  \n", isCompare2, x)
	//OutPut: isCompare=true,x=2
	//相等,故x还是2

	//func SwapInt32(addr *int32, new int32) (old int32)
	xOld := atomic.SwapInt32(&x, int32(3))
	fmt.Printf("xOld=%d,x=%d  \n", xOld, x)
	//OutPut: xOld=2,x=3
	//不比较,故x替换3

	//func LoadInt32(addr *int32) (val int32)
	vValue := atomic.LoadInt32(&x)
	fmt.Printf("vValue=%d \n", vValue)
	//OutPut: xOld=2,x=3
	//获取x的值3

	//func StoreInt32(addr *int32, val int32)
	atomic.StoreInt32(&x, 8)
	vValue2 := atomic.LoadInt32(&x)
	fmt.Printf("vValue2=%d \n", vValue2)
	//OutPut:vValue2=8 
}

小试牛刀

atomic 比较常见的类型 还提供了一个特殊的类型:Value,但是只支持 load、store。 这里模拟一个场景:当配置变更后,期待其他goroutine可以收到通知和变更。

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
	"time"
)

type Config struct {
	Network      string
	Addr         string
	ReadTimeout  int32
	WriteTimeout int32
}

func loadConfig() Config {
	return Config{
		Network:      "redis",
		Addr:         "127.0.0.1:6379",
		ReadTimeout:  60,
		WriteTimeout: 60,
	}
}

var (
	done   bool
	config atomic.Value
)

func main() {
	config.Store(loadConfig())
	var cond = sync.NewCond(&sync.Mutex{})
	go waitForLoad(1, cond)
	go waitForLoad(2, cond)
	go beginLoad(cond)
	select {}
}

func beginLoad(cond *sync.Cond) {
	for {
		time.Sleep(3 * time.Second)
		config.Store(loadConfig())
		cond.Broadcast()
	}
}

func waitForLoad(node int, cond *sync.Cond) {
	cond.L.Lock()
	for {
		if !done {
			cond.Wait()
		}
		c := config.Load().(Config)
		fmt.Printf("node: %d - redis config: %+v\n", node, c)

	}
	cond.L.Unlock()
}

OutPut:

node: 2 - redis config: {Network:redis Addr:127.0.0.1:6379 ReadTimeout:60 WriteTimeout:60}
node: 1 - redis config: {Network:redis Addr:127.0.0.1:6379 ReadTimeout:60 WriteTimeout:60}

uber-go/atomic

uber-go/atomic对标准库进行进一步封装,采用面向对象的使用方式。 这些类型包括 Bool、Duration、Error、Float64、Int32、Int64、String、Uint32、Uint64 等 举个例子uint32的减法你可能是这么写的

atomic.AddUint32(&x, ^(delta - 1))

利用计算机补码的规则,把减法变成加法 。uber-go对它进行了封装

var atom atomic.Uint32
atom.Store(10)
atom.Sub(2)
atom.CAS(20, 1)

小结

本文简要介绍原子操作和go-atomic,使用场景,以及第三库uber-go/atomic。

参考

本文简要介绍golang 使用crontab实现定时任务。

linux crontab

Linux crontab 是用来定期执行程序的命令。当安装完成操作系统之后,默认便会启动此任务调度命令

命令语法:

crontab [ -u user ] file

crontab [ -u user ] { -l | -r | -e }

*    *    *    *    *
-    -    -    -    -
|    |    |    |    |
|    |    |    |    +----- 星期中星期几 (0 - 6) (星期天 为0)
|    |    |    +---------- 月份 (1 - 12) 
|    |    +--------------- 一个月中的第几天 (1 - 31)
|    +-------------------- 小时 (0 - 23)
+------------------------- 分钟 (0 - 59)

go cron

第三方包“github.com/robfig/cron”来创建 crontab,以实现定时任务

package main

import (
	"fmt"
	"github.com/robfig/cron"
)

func main() {
	var (
		cronS = cron.New()
		spec  = "*/2 * * * * "
		count = 0
	)

	entityID, err := cronS.AddFunc(spec, func() {
		count++
		fmt.Println("count: ", count,"now:", time.Now().Unix())
	})
	if err != nil {
		fmt.Errorf("error : %v", err)
		return
	}
	cronS.Start()

	fmt.Println(entityID)
	defer cronS.Stop()
	select {}
}

go run main.go

输出:

count:  1 now: 1658053860
count:  2 now: 1658053920
count:  3 now: 1658053980
count:  4 now: 1658054040
count:  5 now: 1658054100

可以看到每隔1分钟,执行一次Func,count++

默认情况下标准 cron 规范解析(第一个字段是“分钟”) 可以轻松选择进入秒字段。

cronS = cron.New(cron.WithSeconds())
//注意这里多了一个参数
spec  = "*/2 * * * * * "

执行输出

count:  1 now: 1658053640
count:  2 now: 1658053642
count:  3 now: 1658053644
count:  4 now: 1658053646
count:  5 now: 1658053648 

可以看到每隔两秒执行一次

上篇传送门介绍和搭建 GrafanaLoki,今天学习实战收集dockers容器的日志。

Grafana Loki 支持以下官方客户端发送日志:

  • Promtail
  • Docker Driver
  • Fluentd
  • Fluent Bit
  • Logstash
  • Lambda Promtail

其中我们关注比较多是:

  • Promtail 是运行 Kubernetes 时的首选客户端,因为您可以将其配置为自动从运行 Promtail 的同一节点上运行的 pod 中抓取日志
  • Docker Logging Driver 当使用 Docker 而不是 Kubernetes 时,应该使用 Loki 的 Docker 日志记录驱动程序,因为它会自动添加适合正在运行的容器的标签。

Docker Driver

Grafana Loki 官方支持一个 Docker 插件,该插件将从 Docker 容器中读取日志并将它们发送到 Loki

安装

在每台 Docker 主机安装插件

docker plugin install grafana/loki-docker-driver:latest –alias loki –grant-all-permissions

检查是否安装成功:

docker plugin ls

$ docker plugin ls
ID             NAME          DESCRIPTION           ENABLED
2aa05FooBar   loki:latest   Loki Logging Driver   true

插件的使用可以有两种方式:

  • 配置全局 daemon.json,收集此后创建的所有容器的日志。
  • 运行容器时指定logging类型为loki

这里我们采用第一种方式

配置全局 daemon.json

sudo vi /etc/docker/daemon.json

{
    "log-driver": "loki",
    "log-opts": {
        "loki-url": "http://127.0.0.1:3100/loki/api/v1/push"
    }
}

重启docker服务

sudo systemctl restart docker

查看是否生效

docker info

$docker info
...
Logging Driver: loki

说明配置生效。

测试

启动测试服务,打开grafana查看,数据源选择 “Loki”。 搜索选择 compose-service ,由于这边是使用docker-compose启动的服务。 选择 “你的测试服务”,显示如下。

至此,已经成功收集docker的日志。

小结

本篇主要介绍如何利用 Docker Driver插件和配置本地环境来收集dockers日志到loki。

reference

为了快速定位、排查问题的,那么需要一个日志聚合、搜索的工具或者系统。

简介

Loki是一个受普罗米修斯启发的水平可伸缩、高可用性、多租户日志聚合系统。它的设计是非常有效的成本和易于操作。它不索引日志的内容,而是为每个日志流设置一组标签。

相关组件

  • Loki是主服务器,负责存储日志和处理查询。
  • Promtail是代理,负责收集日志并将其发送给 loki 。
  • Grafana用于 UI 展示。

部署

这里在Linux环境下,使用docker-compose的方式部署, 采集os的日志,主要分以下几个步骤:

  1. 启动 loki 、promtail、grafana
  2. 配置日志收集方式
  3. 观测日志是否采集、聚合

启动 loki 、promtail、grafana

在工作目录底下创建 docker-compose.yaml

version: "3"

networks:
  loki:

services:
  loki:
    image: grafana/loki:latest
    ports:
      - "3100:3100"
    command: -config.file=/etc/loki/local-config.yaml
    networks:
      - loki

  promtail:
    image: grafana/promtail:latest
    volumes:
      - /var/log:/var/log
    command: -config.file=/etc/promtail/config.yml
    networks:
      - loki

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    networks:
      - loki

查看是否启动:

docker-compose ls

输出:

smartgo$ docker-compose ls
NAME                STATUS              CONFIG FILES
xloki               running(3)          /home/smartgo/xloki/docker-compose.yaml

配置数据源

访问Grafana: yournode:3000 ,选择数据源:Explore–DataSource 可以看到Grafana支持的数据源,点击选择 Loki 设置地址为:

http://loki:3100

保存即可,点击 SaveAndTest

观测日志是否采集、聚合

搜索日志,选择数据源Loki,选择文件类型和文件路径,具体如下

小结

本文主要初步介绍日志聚合、搜索系统Loki,以及如何docker部署,Grafana配置,搜索日志。

ToDo

reference

上一篇(传送门)介绍了测试平台 locust + boomer 的环境搭建,以及运行http压测用例,观测性能指数、图表。这篇接上篇,继续讲go boomer如何实现。

setup

Install the master branch

$ go get github.com/myzhan/boomer

Install a tagged version that works with locust 1.6.0

$ go get github.com/myzhan/boomer@v1.6.0

install gomq

$ go get -u github.com/zeromq/gomq

quick start

run master

创建python文件 workspace/dummy.py

from locust import Locust, TaskSet, task
class MyTaskSet(TaskSet):
    @task(20)
    def hello(self):
        pass
class Dummy(Locust):
    task_set = MyTaskSet

运行:

$ locust –master -f dummy.py output:

$locust.main: Starting web interface at http://0.0.0.0:8089 (accepting connections from all network interfaces)
$locust.main: Starting Locust 2.9.1.dev23
run slave

创建go文件 workspace/main.go

package main

import(
	"fmt"
	"io/ioutil"
	"net/http"
	"time"
	"github.com/myzhan/boomer"
)

func helloTask() {
	start := time.Now()
	err := HttpGet("hello")
	elapsed := time.Since(start)
	if err != nil {
		boomer.RecordFailure("http", "world", elapsed.Nanoseconds()/int64(time.Millisecond), err.Error())
		return
	}
    
/*    Report your test result as a success, if you write it in locust, it will looks like this    events.request_success.fire(request_type="http", name="world", response_time=100, response_length=10)    */
	boomer.RecordSuccess("http", "world", elapsed.Nanoseconds()/int64(time.Millisecond), int64(10))
}

func worldTask() {
	start := time.Now()
	err := HttpGet("world")
	elapsed := time.Since(start)
	if err != nil {
		boomer.RecordFailure("udp", "world", elapsed.Nanoseconds()/int64(time.Millisecond), err.Error())
		return
	} 
/*  Report your test result as a failure, if you write it in locust, it will looks like this    events.request_failure.fire(request_type="udp", name="hello", response_time=100, exception=Exception("udp error"))    */
	boomer.RecordSuccess("udp", "world", elapsed.Nanoseconds()/int64(time.Millisecond), int64(10))
}

func main() {
	task1 := &boomer.Task{
		// 同时跑多个 tasks 的时候,Weight 字段用于分配 goroutines
		Weight: 10,
		Fn:     helloTask,
	}

	task2 := &boomer.Task{
		Weight: 10,
		Fn:     worldTask,
	}

	// 连接到 master,等待页面上下发指令,支持多个 Task
	boomer.Run(task1, task2)
}


func HttpGet(path string) error {
	url := fmt.Sprintf("http://localhost:8090/%s", path)
	method := "GET"

	client := &http.Client{}
	req, err := http.NewRequest(method, url, nil)

	if err != nil {
		fmt.Println(err)
		return err
	}
	res, err := client.Do(req)
	if err != nil {
		fmt.Println(err)
		return err
	}
	defer res.Body.Close()

	body, err := ioutil.ReadAll(res.Body)
	if err != nil {
		fmt.Println(err)
		return err
	}
	fmt.Println(string(body))
	return nil
}


go run main.go

output

$ Boomer is built with gomq support.
$ Boomer is connected to master(tcp://127.0.0.1:5557) press Ctrl+c to quit.

说明启动slave成功,查看是否连接上master

$ locust.runners: Client 'crazyMac.local_axxbyy123456' reported as ready. Currently 1 clients ready to swarm.

说明已经连接上master 。

testing

启动测试,output

succeed

小结

本文主要介绍了如何利用go boomer 实现locust的通讯协议,以及使用boomer实现一个上一篇的http压测例子。

reference

最近公司打算对后端服务进行压力测试,考虑后端的主要使用golang实现,因此作者准备使用 locust + boomer 实现一个性能测试平台,mark一下实现过程。

what is locust

Locust 是一种易于使用、可编写脚本且可扩展的性能测试工具。 您可以在常规 Python 代码中定义用户的行为,而不是停留在 UI 或限制性特定领域的语言中。

what is boomer

boomer 完整地实现了 locust 的通讯协议,运行在 slave 模式下,用 goroutine 来执行用户提供的测试函数,然后将测试结果上报给运行在 master 模式下的 locust。

与 locust 原生的实现相比,解决了两个问题。 一是单台施压机上,能充分利用多个 CPU 核心来施压, 二是再也不用提防阻塞 IO 操作导致 gevent 阻塞。

环境

  • 服务器
    • Ubuntu (2核4G300G)
  • 压测机
    • Mac
    • Python 版本 Python 3.10.2
    • Go 版本 go version go1.17.1 darwin/arm64

压测机

安装 locust

  1. 安装python3.7或者版本大于3.7 (mac 自带python2.X版本)
brew install python

查看安装版本

# python3 -V
Python 3.10.2
  1. Install Locust
# pip3 install locust
  1. 检查安装是否成功
# locust -V
locust 2.9.1.dev23

运行 locust: hello-world

要把大象放冰箱一共分三步:第一步打开冰箱–,不不不,第一步:先试试把小象(hello-world)看看能不能放的进去

在当前目录 workspace/ 底下创建 locustfile.py

from locust import HttpUser, task
class HelloWorldUser(HttpUser):
    @task
    def hello_world(self):
        self.client.get("/hello")
        self.client.get("/world")
启动 locust
 # locust
 locust 
$: Starting web interface at http://0.0.0.0:8089 (accepting connections from all network interfaces)
$: Starting Locust 2.9.1.dev23

访问 http://localhost:8089/ 可以看到

接着,这边使用golang启动一个http服务 localhost:80(path:/hello & /world)

locust - HelloWorld

进行一个简单测试 50 个并发用户,加速速度为 1个用户/秒,将其指向响应/hello和的服务器/world

点击 “start swarming”

切换标签页 “Charts” 可以查看:显示每秒请求数 (RPS)

查看:响应时间(以毫秒为单位)

查看: 用户数量

小结

本文主要介绍性能测试平台 locust + boomer 的环境搭建,以及运行http 测试用例helloworld,使用locust观测性能指数、图表等。

参考

问题

在mac m1 构建的docker镜像(image:v1.0),在ubuntu上面运行报错:

smart-service | standard_init_linux.go:228: exec user process caused: exec format error

$ uname -m
 arm64

由于m1是arm64架构,而ubuntu的是x86_64架构,那有没有可以在mac上面构建x86_64的镜像的方法呢?

解决步骤

在 Docker 19.03+ 版本中可以使用

$ docker buildx build

命令使用 BuildKit 构建镜像。该命令支持 –platform 参数可以同时构建支持多种系统架构的 Docker 镜像。 更多参考:

https://docs.docker.com/engine/reference/commandline/buildx/

Sep1 启用experimental features

Docker 的 buildx 还是实验性功能,需要在 Docker Desktop 设置中开启。

Preferences > Experimental Features > Enable CLI experimental features

Sep2 创建builder

由于 Docker 默认的 builder 实例不支持同时指定多个 –platform,我们必须首先创建一个新的 builder 实例。

$ docker buildx create --use --name my_builder

国内环境使用:

  • 网易云镜像
$ docker buildx create --use --name=mybuilder --driver docker-container --driver-opt image=dockerpracticesig/buildkit:master
  • 百度云镜像
$ docker buildx create --use --name=mybuilder --driver docker-container --driver-opt image=dockerpracticesig/buildkit:master-baidu

查看实例详情:

docker buildx inspect --bootstrap

output:

$ docker buildx inspect --bootstrap
[+] Building 4.9s (1/1) FINISHED
 => [internal] booting buildkit                           
 => => starting container buildx_buildkit_my_builder0     
Name:   my_builder
Driver: docker-container

Nodes:
Name:      my_builder0
Endpoint:  unix:///var/run/docker.sock
Status:    running
Platforms: linux/arm64, linux/amd64, linux/amd64/v2, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/mips64le, linux/mips64, linux/arm/v7, linux/arm/v6

Sep3 构建

使用 $ docker buildx build 命令构建镜像

     docker buildx build
      –platform linux/amd64,linux/arm64  –push -t myusername:v2.0 .

  • –platfrom 不同的架构
  • –push 将构建好的镜像推送到 Docker 仓库

构建完 push 上去以后,可以查看远程仓库的 manifest:

 docker buildx imagetools inspect myusername:v2.0 

output:

$ docker buildx imagetools inspect myusername:v2.0 
Name:      myusername:v2.0 
MediaType: application/vnd.docker.distribution.manifest.list.v2+json
Digest:    sha256:c234123456XXXyyyy3a7123456XXXyyyyd28de6c376d2f123456XXXyyyy9edf2

Manifests:
  Name:      myusername:v2.0 @sha123456XXXyyyy777122
  MediaType: application/vnd.docker.distribution.manifest.v2+json
  Platform:  linux/amd64

  Name:      myusername:v2.0 @sha123456XXXyyyy777123
  MediaType: application/vnd.docker.distribution.manifest.v2+json
  Platform:  linux/arm64

显示已经支持不同架构

Sep4 测试

在Ubuntu下(v2.0镜像)运行

Status: Downloaded newer image for 
myusername:v0.2
Recreating my-service ... done 
my-service | INFO msg=[HTTP] server listening on: [::]:8000

正常运行 Succeed

总结

本文主要介绍 使用 buildx 构建多种系统架构支持的 Docker 镜像

参考:

场景

在项目开发中,需要用到缓存和对一个列表数据分页查询,但由于redis是key-value的存储方式,我们期望的使用类似postgresql的offset和limit,不至于需要一个个key遍历过去。

设计

分析一下我们需求,那么需求需要实现的接口大概是:

FindByPage(ctx context.Context, page, size int) ([]Object, error)

大致我们需要解决两个问题:

  • 存储对象
  • 列表分页快速查找

对象存储

Redis hash 是一个 string 类型的 field(字段) 和 value(值) 的映射表,hash 特别适合用于存储对象。

  • HDEL key field1 [field2]删除一个或多个哈希表字段
  • HEXISTS key field查看哈希表 key 中,指定的字段是否存在。
  • HGET key field获取存储在哈希表中指定字段的值。
  • HGETALL key获取在哈希表中指定 key 的所有字段和值

如此,我们可以 redis hash 和 json.Encoding 来存储。 获得一个Object

//【ObjectID】= "{"foo": "123", "bar":"456"}"

import  "github.com/go-redis/redis/v8"

result, err := rdb.HGet(ctx, "key", id.String()).Result()
if err != nil {
     return
}
var obj = &YourObject{}
err = json.Unmarshal([]byte(result), obj)
if err != nil {
     return
}

增加一个Object:

ob, err := json.Marshal(Object{}) 
	err = repo.data.rdb.HSet(ctx, "key", ob.ID, ob).Err()
	if err != nil {
		return err
	}

分页

我们的需求中,ObjectID是一个唯一的int,那么可以使用 zset

ZXyy redis 有序集合的基本命令。

  • ZADD key score1 member1 [score2 member2]向有序集合添加一个或多个成员,或者更新已存在成员的分数
  • ZCARD key获取有序集合的成员数
  • ZRANGE key start stop [WITHSCORES]通过索引区间返回有序集合指定区间内的成员

那么可以把 zset 的key 和 value 都设置为 ObjectID 增加一个Object的代码:

if err := rdb.ZAdd(ctx, "key", &redis.Z{
     Score:  float64(Object.ID),
     Member: strconv.Itoa(int(Object.ID)),
}).Err(); err != nil {
     return err
}

获取X分页的代码:

var (
     start = int64((page - 1) * size)
     end   = start + int64(size)
)
result, err := rdb.ZRange(ctx, "key", start, end).Result()
if err != nil {
     return
}

测试

我们增加N个Object:

[1 a]
[2 b]
[3 c]
[11 aa]
[12 bb]
[13 cc]
[21 aaaa]

每页的长度为4,获取第一页

[1 2 3 11]

第二页:

[12 13 21]

第三页:

[ ]

参考

jefffff

Stay hungry. Stay Foolish COOL

Go backend developer

China Amoy