Cassandra中的Key有如下三种类型

  • Primary Key
  • Partitioning Key
  • Clustering Key

Primary Key 主键

每张表都需要有主键。主键可以是一个字段或者多个字段的组合。每条记录的主键必须唯一。举个例子

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE player (
name text,
club text,
league text,
nationality text,
kit_number text,
position text,
goals int,
assists int,
appearences int,
PRIMARY KEY (club, league, name, kit_number, position, goals)
)

这个数据表的主键有多个字段,称做复合主键。

分区键

Cassandra根据分区键,使用一致性哈希算法,把数据分配到集群的各个机器上。一个机器可以包含多个分区。Cassandra保证同一分区键的数据都在一台机器上。通过合理的设置分区键,可以让你的查询让尽量少的机器处理,提升查询的效率

对于单主键字段来说,分区键和主键是同一个字段。

对于复合主键字段来说,默认情况下,分区键是复合主键的第一个字段。如上例中,分区键是club字段

可以通过括号来将分区键指定为多个字段,如将上面CQL的11行修改为

1
PRIMARY KEY ((name, club), league, kit_number, position, goals)

Clustering Key

Clustering Keys决定了分区内数据的排序。让我们再看一下最初的例子

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE player (
name text,
club text,
league text,
nationality text,
kit_number text,
position text,
goals int,
assists int,
appearences int,
PRIMARY KEY (club, league, name, kit_number, position, goals)
)

在主键中的字段,除了分区键外都是clustering key。既然club是主键,那么league name kit_number position goals是Clustering key。你可以定义clustering key中每个字段的升降序。可以将kit_number降序、goals升序

排序顺序与主键中字段的顺序相同。因此,在上面的例子中,数据是按照如下布局的

  • 所有相同club的运动员都将分在同一个分区
  • 在分区内,按照leauge排序
  • 然后按照name排序
  • 然后按照kit_number排序

定义不同字段升降序的语法如下(默认为升序)

1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE TABLE player (
name text,
club text,
league text,
nationality text,
kit_number text,
position text,
goals int,
assists int,
appearances int,
PRIMARY KEY (club, league, name, kit_number, position, goals)
)
WITH CLUSTERING ORDER BY (league ASC, name DESC, kit_number ASC, position DESC );

准备被测程序

通过-g编译程序来携带debug信息,这样子输出的错误信息就可以包含精确的行号。如果你可以承受程序运行缓慢,那么我们可以使用-O0来编译程序。如果使用-O1,那么输出的行号可能会不准确。不推荐使用-O2及以上,因为 valgrind memcheck 偶尔会报告不存在的未初始化值错误。

运行程序

如果平时这么运行

1
myprog arg1 arg2

就使用这个命令

1
valgrind --leak-check=yes myprog arg1 arg2

Memcheck是默认的valgrind工具,--leak-check打开了内存泄漏检测开关。

通过这个命令运行,大约会比平时运行慢20到30倍,并且使用更大的内存。Memcheck 将发出它检测到的内存错误和泄漏的信息。

解释memcheck的输出

使用样例的C程序,包含一个内存分配错误和内存溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdlib.h>

void f(void)
{
int* x = malloc(10 * sizeof(int));
x[10] = 0; // problem 1: heap block overrun
} // problem 2: memory leak -- x not freed

int main(void)
{
f();
return 0;
}

执行内存检测

1
2
gcc -g demo.c
valgrind ./a.out

输出信息如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
==145== Memcheck, a memory error detector
==145== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==145== Using Valgrind-3.17.0 and LibVEX; rerun with -h for copyright info
==145== Command: ./a.out
==145==
==145== Invalid write of size 4
==145== at 0x401144: f (demo.c:6)
==145== by 0x401155: main (demo.c:11)
==145== Address 0x4a27068 is 0 bytes after a block of size 40 alloc'd
==145== at 0x484086F: malloc (vg_replace_malloc.c:380)
==145== by 0x401137: f (demo.c:5)
==145== by 0x401155: main (demo.c:11)
==145==
==145==
==145== HEAP SUMMARY:
==145== in use at exit: 40 bytes in 1 blocks
==145== total heap usage: 1 allocs, 0 frees, 40 bytes allocated
==145==
==145== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==145== at 0x484086F: malloc (vg_replace_malloc.c:380)
==145== by 0x401137: f (demo.c:5)
==145== by 0x401155: main (demo.c:11)
==145==
==145== LEAK SUMMARY:
==145== definitely lost: 40 bytes in 1 blocks
==145== indirectly lost: 0 bytes in 0 blocks
==145== possibly lost: 0 bytes in 0 blocks
==145== still reachable: 0 bytes in 0 blocks
==145== suppressed: 0 bytes in 0 blocks
==145==
==145== For lists of detected and suppressed errors, rerun with: -s
==145== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)
  • 145 是进程号
  • 输出的第一行,即上方的第六行(Invalid write)表明了错误的类型。这里程序往不归它拥有的内存中写入了信息
  • 下方是错误的堆栈信息
  • 代码地址如0x401155通常并不重要,但有时候定位奇怪的问题可能会很关键
  • 一些错误信息会有第二段,用来描述所涉及的内存地址。上例指出写入的内存刚好超过 example.c 的第 5 行使用 malloc() 分配的块的末尾。

内存泄漏信息

1
2
3
4
==145== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==145== at 0x484086F: malloc (vg_replace_malloc.c:380)
==145== by 0x401137: f (demo.c:5)
==145== by 0x401155: main (demo.c:11)

堆栈可以表明什么内存泄露了,但memcheck并不能告诉你内存泄漏的原因

valgrind会提示两种内存泄漏

  • “definitely lost”: 绝对泄漏了内存,必须修复
  • “probably lost”: 程序可能泄漏了内存,也有可能是一些特定的指针操作(如:指针放到了堆中)

参考

https://www.valgrind.org/docs/manual/quick-start.html#quick-start.intro

https://www.valgrind.org/docs/manual/mc-manual.html#mc-manual.errormsgs

前提条件

添加配置

[mysqld]下面添加core-file

image-20211016232603862

ulimit打开core file限制

1
ulimit -c unlimited

如需要,修改core file路径(如在容器内,需要特权容器权限)

1
echo "/opt/sh/mysql/core/core" > /proc/sys/kernel/core_pattern

使得core file携带pid信息

1
echo 1 >/proc/sys/kernel/core_uses_pid

通过kill命令获取core file

1
kill -11 $pid

image-20211016233105059

将程序(以rust程序为例)托管为Systemd运行比较容易,步骤分为以下几步

  • 生成rust二进制文件
  • 创建专用用户和用户组(可省略)
  • 书写.service文件
  • 通过systemctl启动

接下来我们以ubuntu、简单的rust web程序为例,演示一个简单的rust程序如何托管给Systemd运行

生成rust二进制文件

添加依赖

1
2
tokio = { version = "1", features = ["full"] }
warp = "0.3"

书写简单代码

1
2
3
4
5
6
7
8
9
10
11
12
use warp::Filter;

#[tokio::main]
async fn main() {
// GET /hello/warp => 200 OK with body "Hello, warp!"
let hello = warp::path!("hello" / String)
.map(|name| format!("Hello, {}!", name));

warp::serve(hello)
.run(([127, 0, 0, 1], 3030))
.await;
}

编译生成二进制

cargo build --release

二进制就在target/release

创建专用用户和用户组

为了更细粒度地授权和文件权限控制,我们可以给守护程序创建专用的用户

1
sudo useradd vmproxy -s /sbin/nologin -M

书写.service文件

.service文件在ubuntu应该放到/lib/systemd/system/路径下。样例如下vm-proxy.service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[Unit]
Description=Vm Proxy
ConditionPathExists=/home/ubuntu/rust/vm-proxy/target/release/vm-proxy-rust
After=network.target

[Service]
Type=simple
User=vmproxy
Group=vmproxy
LimitNOFILE=1024

Restart=on-failure
RestartSec=10

WorkingDirectory=/home/ubuntu/rust/vm-proxy/target/release
ExecStart=/home/ubuntu/rust/vm-proxy/target/release/vm-proxy-rust

# make sure log directory exists and owned by syslog
PermissionsStartOnly=true
ExecStartPre=/bin/mkdir -p /var/log/vm-proxy
ExecStartPre=/bin/chown syslog:adm /var/log/vm-proxy
ExecStartPre=/bin/chmod 755 /var/log/vm-proxy
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=vm-proxy

[Install]
WantedBy=multi-user.target

ConditionPathExistsWorkingDirectoryExecStart 这三个参数需要根据自己的路径修改

通过Systemctl启动

1
2
sudo systemctl enable vm-proxy.service
sudo systemctl start vm-proxy.service

验证启动完成

image-20211012165631993

环境准备

需要有一个运行的java程序,如果你已经有了运行中的java程序,请跳过这一节,示例,我启动自制的kafka镜像

1
docker run ttbb/kafka:mate

找到java程序的pid

ps -ef或者jps均可,其中jps需要安装jdk

image-20210929143232887

安装arthas

1
2
3
wget https://github.com/alibaba/arthas/releases/download/arthas-all-3.5.4/arthas-bin.zip
mkdir -p arthas
unzip arthas-bin.zip -d arthas

使用arthas连接到目标程序

image-20210929143537663

开始profiler

1
profiler start

如果出现Perf events unavailable. See stderr of the target process.如图所示

image-20210929143621015

需要在docker所在虚拟机上执行如下命令

1
2
echo 1 > /proc/sys/kernel/perf_event_paranoid
echo 0 > /proc/sys/kernel/kptr_restrict

如果是mac用户

1
docker run -it --privileged --pid=host debian nsenter -t 1 -m -u -n -i sh

执行上述命令进入docker所在的虚拟机操作即可

注意,在部分docker版本中,有可能还无法进行profiler采集,您可能需要以特权方式启动容器,不过,为了定位性能问题,这总是值得付出的,不是吗?

image-20210929151106090

等待profiler一段时间

一般等待一分钟即可

结束profiler

1
profiler stop

image-20210929151209352

结束

Congratulations,完成了火焰图的输出,现在你可以使用火焰图来分析执行时间较长的方法啦

image-20210929151506122

今天看到了https://mp.weixin.qq.com/s/tTipcU8MKxTpFurWNEUIww 中热迁移网络句柄的功能,忍不住自己在机器上实验了一下,确实可以实现网络句柄的迁移。实验代码如下

old_server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"net"
"syscall"
)

func main() {
tcpAddr := net.TCPAddr{Port: 8001}
tcpLn, err := net.ListenTCP("tcp4", &tcpAddr)
if err != nil {
panic(err)
}
f, _ := tcpLn.File()
fdNum := f.Fd()
data := syscall.UnixRights(int(fdNum))
// 与新版本sidecar通过Unix Domain Socket建立链接
raddr, _ := net.ResolveUnixAddr("unix", "/dev/shm/migrate.sock")
uds, _ := net.DialUnix("unix", nil, raddr)
// 通过UDS,发送ListenFD到新版本sidecar容器
_, _, _ = uds.WriteMsgUnix(nil, data, nil)
// 停止接收新的request,并且开始排水阶段
tcpLn.Close()
}

new_server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

import (
"fmt"
"net"
"net/http"
"os"
"syscall"
"time"
)

func main() {
// 新版本sidecar 接收ListenFD,并且开始对外服务
// 监听UDS
addr, _ := net.ResolveUnixAddr("unix", "/dev/shm/migrate.sock")
unixLn, _ := net.ListenUnix("unix", addr)
conn, _ := unixLn.AcceptUnix()
buf := make([]byte, 32)
oob := make([]byte, 32)
time.Sleep(5 * time.Second)
// 接收 ListenFD
_, oobn, _, _, _ := conn.ReadMsgUnix(buf, oob)
scms, _ := syscall.ParseSocketControlMessage(oob[:oobn])
if len(scms) > 0 {
// 解析FD,并转化为 *net.TCPListener
fds, _ := syscall.ParseUnixRights(&scms[0])
f := os.NewFile(uintptr(fds[0]), "")
ln, _ := net.FileListener(f)
tcpLn, _ := ln.(*net.TCPListener)
http.Serve(tcpLn, &MyHttp{})
}
}

type MyHttp struct {
}

func (*MyHttp) ServeHTTP(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "hello world")
}

实验结果

  • 先启动new_server
  • 再启动old_server
  • 然后访问localhost:8001,返回hello world,这是new_server返回的结果

代码地址

https://github.com/hezhangjian/go_demo/tree/main/demo_base/fd_migrate

本篇文章探讨镜像仓库registry的高可用

镜像仓库高可用无单点故障涉及那些场景

镜像仓库对外提供访问无单点故障

镜像仓库对外提供的访问点保持高可用

镜像仓库的数据存储高可用

存储在镜像仓库中的数据都得是高可用的

镜像仓库无单点故障技术关键点

镜像仓库对外提供访问无单点故障

和上一篇文章一样,如果IaaS能提供ELB,我们最好是使用ELB,或者使用浮动IP的方式替换

镜像仓库的数据存储高可用

  • 配置镜像仓库使用IaaS的S3存储
  • 配置镜像仓库使用本地存储,通过共享文件路径存储来实现高可用,如Glusterfs
  • 配置镜像仓库使用S3存储,自建兼容S3 API的存储Server

通常会使用共享存储来做到镜像仓库存储的高可用

方案概述

那么其实镜像仓库的高可用方案就是对上面方案的组合,下面我们举几个例子

镜像仓库依赖组件部署方式

MinIoKeepAlivedregistry都推荐使用容器部署,方便运维管理,但是镜像推荐内置到虚拟机中,不依赖镜像仓库或其他组件,避免循环依赖

使用IaaS的S3存储 + 负载均衡组件

这是最简单的方案,得益于云厂商提供的S3存储和负载均衡组件,我们可以进行很简单的配置,并部署一台以上的registry,如下图所示

kubernetes-registry-ha-s3

自建兼容S3存储 + KeepAlived浮动Ip

我们可以自己搭建MinIo集群来作为兼容S3存储,由于MINIO最低部署4个节点,我们需要根据故障域机器来选择部署MINIO的数目,比如,故障域是三台物理机,我们部署4节点就不妥。原因是,4节点,总会有一台物理机上会部署2个minio节点,如果这台物理机挂掉,就会导致单点故障。所以,如果故障域为三台物理机,我们最好部署6节点,可容忍一台物理机宕机。其他的节点,读者也可以自行测算。

下图是假设三台物理机,minio6副本场景下的部署示意图

kubernetes-registry-ha-minio

注,为了图的美观,并未画出所有的连线

MINIO关键配置

  • 节点数目6个
  • EC2

k8s高可用无单点故障涉及那些场景

k8s 节点添加、pod添加等增删查改无单点故障

需要元数据的存储和处理能力高可用

k8s对外的apiServer(如worker)无单点故障

worker node和其他组件访问apiServer路径高可用

k8s无单点故障技术关键点

元数据存储

通过etcd存储元数据,etcd三节点集群保证高可用

元数据处理

通过多个kube-controllerkube-scheduler节点来保证高可用

worker节点请求数据通过多ip或负载均衡来保证

节点请求通信通过多Ip或负载均衡来保证高可用,这里也有几种方式

IaaS厂商可提供负载均衡的场景下

如下图所示,可将worker node的访问地址指向负载均衡的地址

kubernetes-ha-iaas-lb

私有化部署KeepAlived

私有化部署场景常用keepAlived提供浮动IP来给worker node或其他组件访问,如下图所示

kubernetes-ha-keepalived

私有化部署加上负载均衡组件

如果你觉得同一时刻只有单个apiServer工作会成瓶颈,也可以使用KeepAlivedNginxHaProxy来对ApiServer做负载均衡

kubernetes-ha-keepalived-nginx

为了简化图像,只画出了master1上的Nginx向后转发的场景。

至于Nginx和KeepAlived如何部署,推荐采用容器化的部署模式,方便进行监控和运维;但是镜像不从镜像仓库拉取,而是保存在master节点上,这样虽然升级复杂一点,但是这样子kubernetes的高可用就不依赖镜像仓库了,不会和镜像仓库形成循环依赖,更不会影响镜像仓库的高可用方案,大大简化了后续的技术方案。(因为镜像仓库可能会占据较大的存储空间,可能会和master节点分离部署,这时会作为worker节点连接master节点)。

前几天,我的同事碰到了一个问题,是关于GoFrame 框架中数据字段的更新问题,数据中有一个status字段,他本来不想更新,但是却更新成了0。

相信看到描述,已经有经验丰富的专家可以猜到是数据部分更新导致的问题。

没错,就是因为数据库部分更新,把0这个值当成了需要更新的值刷新到了数据库中。

中间是复现问题的流程及代码细节,不感兴趣的可以直接拉到最后

复现问题

创建一个数据表

1
2
3
4
5
6
7
8
CREATE TABLE health_check
(
id VARCHAR(50) PRIMARY KEY,
str_null VARCHAR(50) NULL,
str_not_null VARCHAR(50) NOT NULL,
int_null INT NULL,
int_not_null INT NOT NULL
)

插入一条数据

image-20210819104835758

数据库module的声明

1
2
3
4
5
6
7
type HealthCheck struct {
Id string `orm:"id"`
StrNull *string `orm:"str_null"`
StrNotNull string `orm:"str_not_null"`
IntNull *int `orm:"int_null"`
IntNotNull int `orm:"int_not_null"`
}

更新数据

这时,只想把IntNotNull字段更新成0

1
2
3
4
5
6
7
8
9
10
11
func UpdateHealth() error {
table := g.DB().Schema(healthDb).Table(healthTable)
healthCheck := &module.HealthCheck{}
healthCheck.Id = config.HealthCheckId
i := 6
healthCheck.IntNull = &i
table.Data(healthCheck).Where("id", config.HealthCheckId)
result, err := table.Update()
glog.Info(result)
return err
}

执行完毕之后

除了我要更新的int_not_null字段,其他值都被修改了

image-20210819111903460

这肯定不是我们想要的效果,我们只想更新一个字段。

这个时候GoFrame框架提供了OmitEmpty方法,可以忽略0值,也就是达到没传值不修改的效果,让我们加上OmitEmpty试试

1
2
3
4
5
6
7
8
9
table := g.DB().Schema(healthDb).Table(healthTable).OmitEmpty()
healthCheck := &module.HealthCheck{}
healthCheck.Id = config.HealthCheckId
i := 0
healthCheck.IntNotNull = i
table.Data(healthCheck).Where("id", config.HealthCheckId)
result, err := table.Update()
glog.Info(result)
return err

重新导入数据

image-20210819110235646

操作update

image-20210819111954946

可是什么都没有更新,原来OmitEmpty把数字0当做是0值,没有进行更新,那么要怎么写呢,答案是使用*int类型

让我们使用定位为*int类型的字段来更新一下int_not_null,代码如下

1
2
3
4
5
6
7
8
9
10
11
func UpdateHealth() error {
table := g.DB().Schema(healthDb).Table(healthTable).OmitEmpty()
healthCheck := &module.HealthCheck{}
healthCheck.Id = config.HealthCheckId
i := 0
healthCheck.IntNull = &i
table.Data(healthCheck).Where("id", config.HealthCheckId)
result, err := table.Update()
glog.Info(result)
return err
}

再次操作Update

image-20210819112254854

效果达成.

总结

  • GoFrame的框架默认会对全量字段进行更新,无论你的字段有没有复制
  • OmitEmpty可以让框架跳过空值,但是int类型的0,也会跳过
  • 如果你还是想用OmitEmpty跳过空值的情况下,写入0,请使用*int类型

当代码只在当地(example:中国)部署的时候,可能不需要太考虑时区问题,很大可能你的部署机器、容器都是在当地时间。
这种情况下,代码里面统一用当地时间做处理没有任何问题。

但如果代码需要多地域部署,或者是部署的机器时区不一,比如XX单位机器统一采用UTC时间,YY单位机器统一采用当地时间,这么做不利于数据的导入导出,也不利于开发人员的维护

一个良好的处理方式可以是这样子的

image-20210814094346086

  • 存储数据都使用UTC格式存储
  • 业务层跟不同的当地系统对接
0%