clickhouse单分片三副本高可用搭建

3台机器,分别起3个 clickhosue 和 zookeeper 搭建一分片三副本高可用集群。

用到镜像yandex/clickhouse-server:19.15.3.6zookeeper

zookeeper 配置

zoo.cfg 增加server列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@node-a002 zookeeper]# cat zoo.cfg
dataDir=/data
dataLogDir=/datalog
tickTime=2000
initLimit=5
syncLimit=2
autopurge.snapRetainCount=3
autopurge.purgeInterval=0
maxClientCnxns=60
standaloneEnabled=true
admin.enableServer=true
server.1=10.0.0.14:2888:3888;2181
server.2=10.0.0.15:2888:3888;2181
server.3=10.0.0.16:2888:3888;2181

同时 data 目录下 myid 内容对应配置里的id编号

1
2
[root@node-a002 zookeeper]# cat data/myid
1

启动 zookeeper

1
2
3
4
5
6
7
8
[root@node-a002 ~]# cat sh/d_zookeeper.sh
docker run -d \
--network host \
-v /root/zookeeper/data:/data \
-v /root/zookeeper/zoo.cfg:/conf/zoo.cfg \
--name zookeeper \
--restart="always" \
zookeeper

clickhouse 配置

添加 metrika.xml 文件: 配置3个zookeeper,replica 处表示01集群、01分片、a001备份

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[root@node-a002 clickhouse]# cat conf/metrika.xml
<yandex>
<zookeeper-servers>
<node index="1">
<host>10.0.0.14</host>
<port>2181</port>
</node>
<node index="2">
<host>10.0.0.15</host>
<port>2181</port>
</node>
<node index="3">
<host>10.0.0.16</host>
<port>2181</port>
</node>
</zookeeper-servers>

<macros>
<layer>01</layer>
<shard>01</shard>
<replica>cluster01-01-a002</replica>
</macros>
</yandex>

config.xml 引入 metrika.xml

1
2
[root@node-a002 clickhouse]# cat conf/config.xml -n | grep 'metrika.xml'
461 <include_from>/etc/clickhouse-server/metrika.xml</include_from>

启动 clickhouse

1
2
3
4
5
6
7
8
9
10
[root@node-a002 sh]# cat d_clickhouse.sh
docker run -d \
--network=host \
-v /root/clickhouse/conf/config.xml:/etc/clickhouse-server/config.xml \
-v /root/clickhouse/conf/metrika.xml:/etc/clickhouse-server/metrika.xml \
-v /root/clickhouse/conf/users.xml:/etc/clickhouse-server/users.xml \
-v /root/clickhouse/data:/var/lib/clickhouse \
-v /root/clickhouse/log:/var/log/clickhouse-server/ \
--name clickhouse \
yandex/clickhouse-server:19.14

写数据

三分片分别创建ReplicatedMergeTree引擎表

1
2
3
4
5
6
7
8
9
CREATE TABLE test
(
`id` Int64,
`created_date` DateTime
)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{layer}-{shard}/test', '{replica}')
PARTITION BY toYYYYMMDD(created_date)
ORDER BY id
SETTINGS index_granularity = 8192

插入数据:

1
insert into test values(1,'2020 06 28 14:00:00') ;

到其他两台备份上分别查看数据是否同步 ✓

停掉某一台 zookeeper 数据库是否能正常访问 ✓

其他表引擎

自动数据备份,是表的行为,ReplicatedXXX的表支持自动同步。

Replicated前缀只用于MergeTree系列(MergeTree是最常用的引擎),即clickhouse支持以下几种自动备份的引擎:

1
2
3
4
5
6
ReplicatedMergeTree
ReplicatedSummingMergeTree
ReplicatedReplacingMergeTree
ReplicatedAggregatingMergeTree
ReplicatedCollapsingMergeTree
ReplicatedGraphiteMergeTree

参考:

clickhouse入门操作

zookeeper集群启动报错

Clickhouse集群应用、分片、复制

添加帐号密码

xorm结合logrus记录traceId

通用 traceId 写日志,来查看一次请求的处理过程,是我们常用的排错方式。日志一般包括我们自定义的日志,和数据库日志。

比较了常用的 go orm 包,发现 xorm 可以自定义上下文 ,满足我们的需求。通过engine.SetDefaultContext(ctx),我们可以把一次请求产生的 traceId 通过 context.WithValue 的方式传递,在sql 执行结束时打印出来。

xorm 包本身的日志是不输出上下文变量的,但是 engine.SetLogger() 方法支持自定义 log ,我们通过重写log来实现自定义日志打印。需要实现如下 ContextLogger 接口,主要重写了 AfterSQL 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ContextLogger represents a logger interface with context
type ContextLogger interface {
SQLLogger

Debugf(format string, v ...interface{})
Errorf(format string, v ...interface{})
Infof(format string, v ...interface{})
Warnf(format string, v ...interface{})

Level() LogLevel
SetLevel(l LogLevel)

ShowSQL(show ...bool)
IsShowSQL() bool
}

我们通过自定义结构体,使用 logrus 日志包来实现这个接口

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
40
41
42
43
44
45
46
47
48
49
50
51
type LoggerAdapter struct {
logger *logrus.Logger
}

func NewLoggerAdapter() *LoggerAdapter {
return &LoggerAdapter{
logger: logrus.New(),
}
}

func (l *LoggerAdapter) BeforeSQL(ctx xlog.LogContext) {}

func (l *LoggerAdapter) AfterSQL(ctx xlog.LogContext) {
#在这里打印了上下文环境里的traceId
traceId := ctx.Ctx.Value("traceId")
if ctx.ExecuteTime > 0 {
l.logger.Infof("[SQL] %v %v - %v %v", ctx.SQL, ctx.Args, ctx.ExecuteTime, traceId)
} else {
l.logger.Infof("[SQL] %v %v", ctx.SQL, ctx.Args, traceId)
}
}

func (l *LoggerAdapter) Debugf(format string, v ...interface{}) {
l.logger.Debugf(format, v...)
}

func (l *LoggerAdapter) Errorf(format string, v ...interface{}) {
l.logger.Errorf(format, v...)
}

func (l *LoggerAdapter) Infof(format string, v ...interface{}) {
l.logger.Infof(format, v...)
}

func (l *LoggerAdapter) Warnf(format string, v ...interface{}) {
l.logger.Warnf(format, v...)
}

func (l *LoggerAdapter) Level() xlog.LogLevel {
return xlog.LogLevel(l.logger.GetLevel())
}

func (l *LoggerAdapter) SetLevel(lv xlog.LogLevel) {
l.logger.SetLevel(logrus.Level(lv))
}

func (l *LoggerAdapter) ShowSQL(show ...bool) {}

func (l *LoggerAdapter) IsShowSQL() bool {
return true
}

测试一下

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
engine, _ = xorm.NewEngine("mysql", "dev_w:*****@(172.16.200.40:3306)/api_monitor?charset=utf8")
dblog := NewLoggerAdapter()

engine.SetLogger(dblog)

type Group struct {
Id int `xorm:"not null pk autoincr INT(11)"`
Name string `xorm:"not null default '' comment('分组名称') VARCHAR(50)"`
Maintainer string `xorm:"not null default '' comment('维护者') VARCHAR(50)"`
CreatedAt time.Time `xorm:"not null TIMESTAMP"`
UpdatedAt time.Time `xorm:"not null TIMESTAMP"`
DeletedAt time.Time `xorm:"TIMESTAMP"`
}

groups := []Group{}

go func() {
ctx := context.WithValue(context.Background(), "traceId", "11111111111111")
engine.SetDefaultContext(ctx)
engine.Where("name = ? or name = ?", "cms", "biw").Find(&groups)
}()

go func() {
time.Sleep(1 * time.Second)
ctx := context.WithValue(context.Background(), "traceId", "2222222222222")
engine.SetDefaultContext(ctx)

engine.Where("name = ? or name = ?", "cms", "tt").Find(&groups)

}()
for {

}

查看日志,可以看到末尾记录了我们本次执行的 traceId

1
2
3
4
INFO[0000] [SQL] SELECT `id`, `name`, `maintainer`, `created_at`, `updated_at`, `deleted_at` FROM `group` WHERE (name = ? or name = ?) [cms biw] - 187.773683ms 1111111
1111111
INFO[0001] [SQL] SELECT `id`, `name`, `maintainer`, `created_at`, `updated_at`, `deleted_at` FROM `group` WHERE (name = ? or name = ?) [cms tt] - 92.988533ms 222222222
2222

参考资料:

logrus日志使用详解

xorm中文文档

快速掌握 Golang context 包,简单示例

Docker web shell 实现二

通过 docke exec start 实现 webshell

调用 exec 接口 传入参数

1
{"AttachStdin":true,"AttachStdout":true,"AttachStderr":true,"Tty":true,"Cmd":["/bin/sh"]}

用返回的 id 请求 start 接口, 附加 post 参数

1
'{"Detach":false,"Tty":true}'

我们使用 go 代码来完成上面的操作

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
package main

import (
"fmt"
_ "io"
"log"
"net"
"os"
"regexp"
"time"
)

//发送信息
func sender(conn net.Conn) {
buffer := make([]byte, 1024)

exec_req := fmt.Sprintf(`POST /containers/%s/exec HTTP/1.1
Host: 0.0.0.0:2375
Content-Type: application/json
Cache-Control: no-cache
Content-Length: 91

{"AttachStdin":true,"AttachStdout":true,"AttachStderr":true,"Tty":true,"Cmd":["/bin/sh"]}

`, "alpine")
conn.Write([]byte(exec_req))

n, err := conn.Read(buffer)
if err != nil {
//
}
rep := string(buffer[:n])

Log(rep)
reg1 := regexp.MustCompile(`.*?{"Id":"(.*?)"`)
res := reg1.FindAllStringSubmatch(rep, -1)
s_id := res[0][1]

start_req := fmt.Sprintf(`POST /exec/%s/start HTTP/1.1
Host: 0.0.0.0:2375
Content-Type: application/json
Cache-Control: no-cache
Content-Length: 27

{"Detach":false,"Tty":true}

`, s_id)

conn.Write([]byte(start_req))
time.Sleep(time.Second)
conn.Write([]byte("ls -l\n"))
Log("send over")

for {
//接收服务端反馈
n, err := conn.Read(buffer)
if err != nil {
Log(conn.RemoteAddr().String(), "waiting server back msg error: ", err)
return
}
Log(conn.RemoteAddr().String(), "receive server back msg: ", string(buffer[:n]), n)
}

}

//日志
func Log(v ...interface{}) {
log.Println(v...)
}

func main() {
server := "0.0.0.0:2375"
tcpAddr, err := net.ResolveTCPAddr("tcp4", server)
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
os.Exit(1)
}
conn, err := net.DialTCP("tcp", nil, tcpAddr)
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
os.Exit(1)
}

fmt.Println("connection success")
sender(conn)
}

Read More

Docker web shell 实现一

通过 docker 原生 接口 Attach to a container via a websocket 实现

首先开启 Remote API 访问 2375 端口

1
2
3
4
5
6
7
8
9
# sudo vim /lib/systemd/system/docker.service
[Service]
ExecStart=/usr/bin/dockerd -H fd:// -H tcp://0.0.0.0:2375

# sudo systemctl daemon-reload
# sudo service docker restart

# curl 0.0.0.0:2375
{"message":"page not found"}

启动容器

1
docker run -itd --name alpine alpine /bin/sh

前端网页代码

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<html>

<head>
<meta charset="utf-8">
<title>Docker Web Shell</title>
<link rel="stylesheet" href="https://cdn.bootcss.com/xterm/3.14.5/xterm.css" />
<script src="https://cdn.bootcss.com/xterm/3.14.5/xterm.js"></script>
</head>

<body>
<div id="container-terminal"></div>
<style type="text/css">
body {
width: 100%;
height: 100%;
}

#container-terminal {
width: 100%;
height: 100%;
}

.terminal {
height: 100%;
width: 100%;
font-size: 18px;
}
</style>
<script type="text/javascript">
var term;
var url = "ws://0.0.0.0:2375/containers/alpine/attach/ws?stream=1&stdout=1";
xterm = new Terminal({
rows: 38,
cursorBlink: true
});
ws = new WebSocket(url);
ws.binaryType = 'arraybuffer';
xterm.on('data', function (data) {
ws.send(data);
});
xterm.open(document.getElementById("container-terminal"), true);
ws.onopen = function () {
console.log('ws connected');
ws.send("\n");
};
ws.onerror = function () {
console.log('ws error');
};
ws.onclose = function () {
xterm.writeln('socket已断开连接,请重连')
console.log('ws closed');
};
ws.onmessage = function (evt) {
console.log(evt.data);
var decoder = new TextDecoder('utf-8');
var data = decoder.decode(evt.data);
xterm.write(data);
};
</script>
</body>

</html>

效果图:

上面的方式是通过 attach 方式连接到容器内,优点是原生接口、简单,缺点是1号进程必须是 shell 命令,无权限验证功能,后面介绍另外一种方式,满足多用户同时操作,可以添加权限验证。

angular js基本使用

基本使用

  • 引入 angular.js
1
<script src="https://cdn.bootcss.com/angular.js/1.7.8/angular.min.js"></script>
  • 数据绑定
1
2
3
4
5
6
7
8
<div ng-app="myApp"  ng-controller="myCtrl" ng-cloak>
<input type="text" ng-model="name"/>
</div>

var app = angular.module('myApp', []);
app.controller('myCtrl', function ($scope, $http) {
$scope.name = "runner"
})
  • 样式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <style>
    [ng\:cloak],
    [ng-cloak],
    [data-ng-cloak],
    [x-ng-cloak],
    .ng-cloak,
    .x-ng-cloak {
    display: none !important;
    </style>
  • 下拉列表框、单选框数据绑定 ng-model、触发事件ng-change(ng-chnge必须要有ng-model绑定)、是否可用ng-disabled

1
2
3
4
5
6
7
8
9
<select class="form-control" ng-model="list.filter.group_id"
ng-options="k as v for (k,v) in data_source.list_group_list" ng-change="list.refresh()" required>
</select>

<label class="radio-inline" ng-repeat="data_format in data_source.data_format_list">
<input type="radio" name="data_format[]" value="{{data_format}}" ng-model="edit.data_format"
ng-click="edit.clear_data_body()" ng-disabled="edit.mode=='view'" ng-change="edit.form_change()">
{{data_format}}
</label>
  • Button 按钮是否显示 ng-show
1
<button type="submit" class="btn btn-primary" ng-show="edit.mode == 'insert' || edit.mode=='update'" ng-disabled="edit.form_validate()">提交</button>

Read More

使用awk处理csv文件

上周有个处理对账单的需求,从微信、A下载每天的对账单,然后把订单号转成数据库里对应的B单号,需求比较简单,打算用 shell 命令来写。

首先考虑了 join 命令,实现左连接、内连接都是没有问题的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ cat 1.csv 
No,Con
1,aaa
2,bbb
5,ccc

$ cat 2.csv
No,S_id
1,000555
5,000111
3,000333
8,000111

# 对1.csv、2.csv排序,使用逗号作为分隔符,用两个文件的第一列,跳过header,-a左连接。
$ join -t, -1 1 -2 1 -a 1 --header --nocheck-order <(sort -k 1 -n 1.csv) <(sort -k 1 -n 2.csv)
No,Con,S_id
1,aaa,000555
2,bbb
5,ccc,000111

awk 实现 join

Read More

ClickHouse 入门笔记

ClickHouse是一个用于联机分析(OLAP)的列式数据库管理系统(DBMS)

版本

文章使用版本 docker pull yandex/clickhouse-server:19.15.3.6

SQL支持

支持的查询包括 GROUP BY,ORDER BY,IN,JOIN以及非相关子查询。 不支持窗口函数和相关子查询。

支持近似计算

用于近似计算的各类聚合函数,如:distinct values, medians(中位数), quantiles(分位数)

吞吐量

ClickHouse可以在单个服务器上每秒处理数百个查询(在最佳的情况下最多可以处理数千个),建议每秒最多查询100次。有一种流行的观点认为,想要有效的计算统计数据,必须要聚合数据,因为聚合将降低数据量。

访问控制

ClickHouse包含访问控制配置,它们位于users.xml文件中(与’config.xml’同目录)。 默认情况下,允许从任何地方使用默认的‘default’用户无密码的访问ClickHouse。

默认情况下它使用‘default’用户无密码的与localhost:9000服务建立连接。 客户端也可以用于连接远程服务,例如:clickhouse-client --host=example.com

时区设置

1
sed "144 i<timezone>Asia/Shanghai</timezone>" -i /etc/clickhouse-server/config.xml

Read More

迁移gogs用户到openldap

公司之前使用 gogs 作为 git 服务器,要改成 gitlab,现在解决帐号迁移问题

搭建 openldap 服务器

查看 gogs 代码,使用的是 pbkdf2 加密方式,SALT_SIZE为10 ,迭代次数为10000 ,DK_SIZE为50 ,如下:

1
2
3
4
5
// EncodePasswd encodes password to safe format.
func (u *User) EncodePasswd() {
newPasswd := pbkdf2.Key([]byte(u.Passwd), []byte(u.Salt), 10000, 50, sha256.New)
u.Passwd = fmt.Sprintf("%x", newPasswd)
}

openldap 默认是不支持 pbkdf2 加密的,好在有人贡献了这部分代码,现在已经作为 openldap 的一个模块在项目源码里了.

按照下面的 Dockerfile 生成镜像,需要注意的是 SALT_SIZE、DK_SIZE 作为环境变量参与编译,和 gogs 的保持一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
FROM ubuntu:18.04
WORKDIR /root
ENV SALT_SIZE=10 DK_SIZE=50
RUN apt-get -y update \
&& apt-get -y install git gcc libltdl-dev make groff groff-base libssl-dev \
&& git clone https://github.com/openldap/openldap.git \
&& cd openldap \
&& ./configure --enable-modules \
&& make \
&& make install \
&& cd ./contrib/slapd-modules/passwd/pbkdf2 \
&& sed -i "s/PBKDF2_SALT_SIZE 16/PBKDF2_SALT_SIZE ${SALT_SIZE}/g" pw-pbkdf2.c \
&& sed -i "s/PBKDF2_SHA256_DK_SIZE 32/PBKDF2_SHA256_DK_SIZE ${DK_SIZE}/g" pw-pbkdf2.c \
&& sed '19 a#define HAVE_OPENSSL' -i pw-pbkdf2.c \
&& make \
&& mv slapo-pw-pbkdf2.5 slapd-pw-pbkdf2.5 \
&& make install \
&& sed '19 a# moduleload\tpw-pbkdf2.la' -i /usr/local/etc/openldap/slapd.conf
CMD ["sh", "-c", "/usr/local/libexec/slapd -f /usr/local/etc/openldap/slapd.conf; tail -f /dev/null;"]

Read More

ubuntu18.04安装

镜像制作

工具:https://rufus.ie/

分区:

1
2
3
4
5
/ 50G 根分区(一般分配30G就可以)
/boot 500MB 引导分区
efi 500MB
swap 4G 交换分区
/home 个人数据分区

安装完成重启

修改163源并更新

1
2
3
4
5
6
7
8
9
10
11
12
13
$ cat /etc/apt/sources.list
deb http://mirrors.163.com/ubuntu/ bionic main restricted universe multiverse
deb http://mirrors.163.com/ubuntu/ bionic-security main restricted universe multiverse
deb http://mirrors.163.com/ubuntu/ bionic-updates main restricted universe multiverse
deb http://mirrors.163.com/ubuntu/ bionic-proposed main restricted universe multiverse
deb http://mirrors.163.com/ubuntu/ bionic-backports main restricted universe multiverse
deb-src http://mirrors.163.com/ubuntu/ bionic main restricted universe multiverse
deb-src http://mirrors.163.com/ubuntu/ bionic-security main restricted universe multiverse
deb-src http://mirrors.163.com/ubuntu/ bionic-updates main restricted universe multiverse
deb-src http://mirrors.163.com/ubuntu/ bionic-proposed main restricted universe multiverse
deb-src http://mirrors.163.com/ubuntu/ bionic-backports main restricted universe multiverse
$ sudo apt update
$ sudo apt upgrade

安装网卡驱动 :(

1
https://github.com/tomaspinho/rtl8821ce

常用软件

1.png

总结:装个系统还是比较容易的,数据备份很重要,坚果网盘真的好用。