Docker实战(二十三)Docker安装Shadowsocks

最近DO服务器一直没续费到期了,想换个新的VPS服务器又发现原来的翻墙工具Shadowsocks又要重装好麻烦啊。所以想到了用Docker安装一个Shadowsocks镜像的一劳永逸的方式。

Dockerfile文件

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
############################################
# version : birdben/shadowsocks:v1
# desc : 当前版本安装的shadowsocks
############################################
# 设置继承自我们创建的 tools 镜像
FROM birdben/tools:v1
# 下面是一些创建者的基本信息
MAINTAINER birdben (191654006@163.com)
# 设置环境变量,所有操作都是非交互式的
ENV DEBIAN_FRONTEND noninteractive
ENV SSS_USER=shadowsocks
ENV SSS_SUPERVISOR_LOG_DIR=/var/log/supervisor
ENV SSS_SHADOWSOCKS_LOG_DIR=/var/log/shadowsocks
# Add a user and make dirs
RUN set -x \
&& useradd $SSS_USER \
&& mkdir -p $SSS_SUPERVISOR_LOG_DIR \
&& mkdir -p $SSS_SHADOWSOCKS_LOG_DIR \
&& chown $SSS_USER:$SSS_USER $SSS_SUPERVISOR_LOG_DIR \
&& chown $SSS_USER:$SSS_USER $SSS_SHADOWSOCKS_LOG_DIR \
&& chmod 777 /var/run
# 替换 sources.list 的配置文件,并复制配置文件到对应目录下面。
# 这里使用的AWS国内的源,也可以替换成其他的源(例如:阿里云的源)
COPY sources.list /etc/apt/sources.list
# 添加 supervisord 的配置文件,并复制配置文件到对应目录下面。(supervisord.conf文件和Dockerfile文件在同一路径)
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY shadowsocks.json /etc/shadowsocks.json
RUN sudo rm -rf /var/lib/apt/lists/*
RUN sudo apt-get update
RUN sudo apt-get -y install python-pip
RUN sudo pip install --upgrade pip
RUN sudo pip install shadowsocks
# /Users/yunyu/workspace_git/birdDocker/shadowsocks/logs/supervisor:/var/log/supervisor
# /Users/yunyu/workspace_git/birdDocker/shadowsocks/logs/shadowsocks:/var/log/shadowsocks
# 这里挂载的路径是birdTracker项目的目录
VOLUME ["/var/log/supervisor"]
VOLUME ["/var/log/shadowsocks"]
USER root
# 容器需要开放Shadowsocks的443端口
EXPOSE 443
# 执行run.sh文件
# CMD ["/run.sh"]
# 执行supervisord来同时执行多个命令,使用 supervisord 的可执行路径启动服务。
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]

Dockerfile源文件链接:

supervisord.conf

1
2
3
4
5
6
7
8
9
10
11
12
[program:shadowsocks]
command=ssserver -c /etc/shadowsocks.json
autostart=true
autorestart=true
stderr_logfile = /var/log/shadowsocks/shadowsocks.err
stdout_logfile = /var/log/shadowsocks/shadowsocks.out
user=root
stopsignal=INT
[supervisord]
nodaemon=true
user=root

shadowsocks.json配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"server":"0.0.0.0",
"server_port":443,
"local_address":"127.0.0.1",
"local_port":1080,
"password":"123456",
"timeout":500,
"method":"aes-256-cfb",
"fast_open":false
}
# 配置内容描述
server : 服务端监听的地址,服务端可填写 0.0.0.0
server_port : 服务端的端口
local_address : 本地端监听的地址
local_port : 本地端的端口
password : 用于加密的密码
timeout : 超时时间,单位秒
method : 默认"aes-256-cfb",建议chacha20或者rc4-md5,因为这两个速度快
fast_open : 是否使用 TCP_FASTOPEN, true / false(后面优化部分会打开系统的 TCP_FASTOPEN,所以这里填 true,否则填 false)

构建Docker镜像

1
docker build -t "birdben/shadowsocks:v1" .

运行Docker容器

1
2
CURRENT_UID=`whoami`
docker run -itd -p 443:443 -v /Users/yunyu/workspace_git/birdDocker/shadowsocks/logs/shadowsocks:/var/log/shadowsocks -v /Users/yunyu/workspace_git/birdDocker/shadowsocks/logs/supervisor:/var/log/supervisor --name shadowsocks_${CURRENT_UID} birdben/shadowsocks:v1

Shadowsocks客户端配置

1
2
3
4
地址:192.168.99.127
端口:443
加密:aes-256-cfb
密码:123456

也可以不使用配置文件方式启动,可以在启动的时候指定配置信息

1
2
3
4
5
6
7
8
9
ssserver -s 监听地址 -p 服务器端口 -k 密码 -m 加密方法
参数说明:
-s : 监听的服务器端地址
-p : 服务器监听端口
-k : 客户端连接密码
-m : 加密方式
--user : 指定启动进程的用户
--d : 指定运行方式,启动/关闭/重启

浏览器访问www.google.com就可以看到Shadowsocks的日志中会有访问记录出现

1
2
3
4
5
INFO: loading config from /etc/shadowsocks.json
2017-02-05 08:19:32 INFO loading libcrypto from libcrypto.so.1.0.0
2017-02-05 08:19:32 INFO starting server at 0.0.0.0:443
2017-02-05 08:21:46 INFO connecting www.google.com:443 from 172.17.0.1:54762
2017-02-05 08:21:46 INFO connecting www.facebook.com:443 from 172.17.0.1:54766

参考文章:

我的MacBook工具

我的MacBook工具

Mac内置快捷键

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
空格键: 预览
cmd + ,: 设置
cmd + -/=: 缩小/放大
ctrl + u: 删除到行首(与zsh冲突, zsh中是删除整行)
ctrl + k: 删除到行尾
ctrl + p/n: 上/下移动一行或者前/后一个命令
ctrl + b/f: 光标前/后移char
esc + b/f: 光标前/后移word(蛋疼不能连续work)
ctrl + a/e: 到行首/行尾
ctrl + h/d: 删前/后字符
ctrl + y: 粘贴
ctrl + w: 删除前一个单词
esc + d: 删后一个单词
ctrl + _: undo
ctrl + r: bck-i-search/reverse-i-search, 输入关键字搜索历史命令

WebSite类

  • 易集
  • 百度云
  • 百度脑图
  • 七牛
  • 微信公众号
  • DigitalOcean
  • Processon
  • GitHub
  • Toggl

开发环境类

  • Homebrew
  • VisualVM
  • Jenkins
  • JDK
  • Redis
  • MySQL
  • Nginx
  • Tomcat
  • Maven

开发工具类

文本编辑类

工作效率类

休闲娱乐

  • 网易云音乐
  • QQ
  • 微信

Go学习(六)Go对字符串的操作(转)

之前项目中有个功能是获取当前月份第一天的日期的需求,也就是如果今天的日期是”20170110”,我需要获取到的日期是”20170101”。

  • 方式一:通过time获取当月的第一天
1
2
3
4
//昨天的日期
yesterdayTime := time.Now().AddDate(0, 0, -1)
//上周的日期
lastweekTime := time.Now().AddDate(0, 0, -8)

缺点:无法确定今天与当月第一天间隔多少天,所以需要结合方式二一起使用

  • 方式二:通过time分别获取当前日期的年,月,日,然后重新拼接
1
2
3
year := time.Now().Year()
month := time.Now().Month()
day := time.Now().Day()

缺点:month获取出来是英文January,而不是01

  • 方式三:将日期转换成字符串,然后截取字符串,删除字符串的最后两位字符加上”01”,或者是直接替换最后两位字符

缺点:在Go官方strings包中并没有找到合适的截取或者替换方法,下面是转自别人总结的strings包的方法

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
# 获取当前时间戳
fmt.Println(time.Now().Unix())
# 1389058332
# 格式化当前时间
fmt.Println(time.Now().Format("2006-01-02 15:04:05"))
# 2014-01-07 09:42:20
# 时间戳转str格式化时间
str_time := time.Unix(1389058332, 0).Format("2006-01-02 15:04:05")
fmt.Println(str_time)
# 2014-01-07 09:32:12
# str格式化时间转时间戳
the_time := time.Date(2014, 1, 7, 5, 50, 4, 0, time.Local)
unix_time := the_time.Unix()
fmt.Println(unix_time)
# 389045004
# 使用time.Parse将str格式化时间转时间戳
the_time, err := time.Parse("2006-01-02 15:04:05", "2014-01-08 09:04:41")
if err == nil {
unix_time := the_time.Unix()
fmt.Println(unix_time)
}
# 1389171881

strings包方法

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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
// 转换
func ToUpper(s string) string
func ToLower(s string) string
func ToTitle(s string) string
func ToUpperSpecial(_case unicode.SpecialCase, s string) string
func ToLowerSpecial(_case unicode.SpecialCase, s string) string
func ToTitleSpecial(_case unicode.SpecialCase, s string) string
func Title(s string) string
------------------------------
// 比较
func Compare(a, b string) int
func EqualFold(s, t string) bool
------------------------------
// 清理
func Trim(s string, cutset string) string
func TrimLeft(s string, cutset string) string
func TrimRight(s string, cutset string) string
func TrimFunc(s string, f func(rune) bool) string
func TrimLeftFunc(s string, f func(rune) bool) string
func TrimRightFunc(s string, f func(rune) bool) string
func TrimSpace(s string) string
func TrimPrefix(s, prefix string) string
func TrimSuffix(s, suffix string) string
------------------------------
// 拆合
func Split(s, sep string) []string
func SplitN(s, sep string, n int) []string
func SplitAfter(s, sep string) []string
func SplitAfterN(s, sep string, n int) []string
func Fields(s string) []string
func FieldsFunc(s string, f func(rune) bool) []string
func Join(a []string, sep string) string
func Repeat(s string, count int) string
------------------------------
// 子串
func HasPrefix(s, prefix string) bool
func HasSuffix(s, suffix string) bool
func Contains(s, substr string) bool
func ContainsRune(s string, r rune) bool
func ContainsAny(s, chars string) bool
func Index(s, sep string) int
func IndexByte(s string, c byte) int
func IndexRune(s string, r rune) int
func IndexAny(s, chars string) int
func IndexFunc(s string, f func(rune) bool) int
func LastIndex(s, sep string) int
func LastIndexByte(s string, c byte) int
func LastIndexAny(s, chars string) int
func LastIndexFunc(s string, f func(rune) bool) int
func Count(s, sep string) int
------------------------------
// 替换
func Replace(s, old, new string, n int) string
func Map(mapping func(rune) rune, s string) string
------------------------------------------------------------
type Reader struct { ... }
func NewReader(s string) *Reader
func (r *Reader) Read(b []byte) (n int, err error)
func (r *Reader) ReadAt(b []byte, off int64) (n int, err error)
func (r *Reader) WriteTo(w io.Writer) (n int64, err error)
func (r *Reader) Seek(offset int64, whence int) (int64, error)
func (r *Reader) ReadByte() (byte, error)
func (r *Reader) UnreadByte() error
func (r *Reader) ReadRune() (ch rune, size int, err error)
func (r *Reader) UnreadRune() error
func (r *Reader) Len() int
func (r *Reader) Size() int64
func (r *Reader) Reset(s string)
------------------------------------------------------------
type Replacer struct { ... }
// 创建一个替换规则,参数为“查找内容”和“替换内容”的交替形式。
// 替换操作会依次将第 1 个字符串替换为第 2 个字符串,将第 3 个字符串
// 替换为第 4 个字符串,以此类推。
// 替换规则可以同时被多个例程使用。
func NewReplacer(oldnew ...string) *Replacer
// 使用替换规则对 s 进行替换并返回结果。
func (r *Replacer) Replace(s string) string
// 使用替换规则对 s 进行替换并将结果写入 w。
// 返回写入的字节数和遇到的错误。
func (r *Replacer) WriteString(w io.Writer, s string) (n int, err error)

上述的方式一和方式三都能实现此需求,但是方式三因为在strings包中没找到按照字符串索引位置截取的方法,所以在网上找到了一个自己实现的SubString截取字符串的方法。下面是具体的方法实现,更多字符串用法可以查看参考文章中的内容。

字符串的截取

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
//截取字符串 start 起点下标 length 需要截取的长度
func Substr(str string, start int, length int) string {
rs := []rune(str)
rl := len(rs)
end := 0
if start < 0 {
start = rl - 1 + start
}
end = start + length
if start > end {
start, end = end, start
}
if start < 0 {
start = 0
}
if start > rl {
start = rl
}
if end < 0 {
end = 0
}
if end > rl {
end = rl
}
return string(rs[start:end])
}
//截取字符串 start 起点下标 end 终点下标(不包括)
func Substr2(str string, start int, end int) string {
rs := []rune(str)
length := len(rs)
if start < 0 || start > length {
panic("start is wrong")
}
if end < 0 || end > length {
panic("end is wrong")
}
return string(rs[start:end])
}

参考文章:

Go学习(五)Go的特殊语法

Go语言=和:=有什么区别

  • = 符号的意思是给变量赋值,如果要使用必须先var声明
  • := 符号的意思是声明并赋值,并且系统自动推断类型,不需要使用var声明
1
2
3
4
5
6
7
8
9
10
// 使用var声明变量
var a = 100
var b int = 100
// 如果var已经声明过这个变量了,想赋值给这个变量使用的是=,而不是:=
var c
c = 100
// 声明并赋值
d := 100

Go语言指针符号的*和&

  • & 符号的意思是对变量取地址,如:变量a的地址是&a
    • 符号的意思是对指针取值,如:*&a,就是a变量所在地址的值,当然也就是a的值了

注意:

1
2
* 和 & 可以互相抵消,同时注意,*& 可以抵消掉,但 &* 是不可以抵消的
a 和 *&a 是一样的,都是a的值,值为1 (因为 *& 互相抵消掉了)

参考文章:

Go学习(四)Go复合数据类型总结

Map

map是一种key-value的关系,一般都会使用make来初始化内存,有助于减少后续新增操作的内存分配次数。假如一开始定义了话,但没有用make来初始化,会报错的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
import (
"fmt"
)
func main(){
var test = map[string]string{"姓名":"李四","性别":"男"}
name, ok := test["姓名"]
// 假如key存在,则name = 李四,ok = true,否则,ok = false
if ok {
fmt.Println(name)
}
delete(test,"姓名")//删除为姓名为key的值,不存在没关系
fmt.Println(test)
var a map[string]string
a["b"] = "c"//这样会报错的,要先初始化内存
a = make(map[string]string)
a["b"] = "c"//这样才不会错
}

interface

接口的转换遵循以下规则:

  • 普通类型向接口类型的转换是隐式的。
  • 接口类型向普通类型转换需要类型断言。
1
2
3
4
5
6
7
8
9
10
11
12
package main
import (
"fmt"
)
func main() {
var val interface{} = "hello"
fmt.Println(val)
val = []byte{'a', 'b', 'c'}
fmt.Println(val)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main
import "fmt"
func main() {
var i32 interface{}
i32 = int32(1)
var i int
//i = i32.(int) // won't work, you need to assert against the exact type in i32
i32_tmp := i32.(int32) // this is called a type assertion
i = int(i32_tmp)
fmt.Println(i)
}

正如您所预料的,”hello”作为string类型存储在interface{}类型的变量val中,[]byte{‘a’, ‘b’, ‘c’}作为slice存储在interface{}类型的变量val中。这个过程是隐式的,是编译期确定的。

接口类型向普通类型转换有两种方式:Comma-ok断言和switch测试。任何实现了接口I的类型都可以赋值给这个接口类型变量。由于interface{}包含了0个方法,所以任何类型都实现了interface{}接口,这就是为什么可以将任意类型值赋值给interface{}类型的变量,包括nil。还有一个要注意的就是接口的实现问题,T 包含了定义在 T 和 T 上的所有方法,而T只包含定义在T上的方法。我们来看一个例子:

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
package main
import (
"fmt"
)
// 演讲者接口
type Speaker interface {
// 说
Say(string)
// 听
Listen(string) string
// 打断、插嘴
Interrupt(string)
}
// 王兰讲师
type WangLan struct {
msg string
}
func (this *WangLan) Say(msg string) {
fmt.Printf("王兰说:%s\n", msg)
}
func (this *WangLan) Listen(msg string) string {
this.msg = msg
return msg
}
func (this *WangLan) Interrupt(msg string) {
this.Say(msg)
}
// 江娄讲师
type JiangLou struct {
msg string
}
func (this *JiangLou) Say(msg string) {
fmt.Printf("江娄说:%s\n", msg)
}
func (this *JiangLou) Listen(msg string) string {
this.msg = msg
return msg
}
func (this *JiangLou) Interrupt(msg string) {
this.Say(msg)
}
func main() {
wl := &WangLan{}
jl := &JiangLou{}
var person Speaker
person = wl
person.Say("Hello World!")
person = jl
person.Say("Good Luck!")
}

Speaker接口有两个实现WangLan类型和JiangLou类型。但是具体到实例来说,变量wl和变量jl只有是对应实例的指针类型才真正能被Speaker接口变量所持有。这是因为WangLan类型和JiangLou类型所有对Speaker接口的实现都是在*T上。这就是上例中person能够持有wl和jl的原因。

想象一下Java的泛型(很可惜golang不支持泛型),java在支持泛型之前需要手动装箱和拆箱。由于golang能将不同的类型存入到接口类型的变量中,使得问题变得更加复杂。所以有时候我们不得不面临这样一个问题:我们究竟往接口存入的是什么样的类型?有没有办法反向查询?答案是肯定的。

Comma-ok断言的语法是:value, ok := element.(T)。element必须是接口类型的变量,T是普通类型。如果断言失败,ok为false,否则ok为true并且value为变量的值。来看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main
import (
"fmt"
)
type Html []interface{}
func main() {
html := make(Html, 5)
html[0] = "div"
html[1] = "span"
html[2] = []byte("script")
html[3] = "style"
html[4] = "head"
for index, element := range html {
if value, ok := element.(string); ok {
fmt.Printf("html[%d] is a string and its value is %s\n", index, value)
} else if value, ok := element.([]byte); ok {
fmt.Printf("html[%d] is a []byte and its value is %s\n", index, string(value))
}
}
}

其实Comma-ok断言还支持另一种简化使用的方式:value := element.(T)。但这种方式不建议使用,因为一旦element.(T)断言失败,则会产生运行时错误。如:

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
package main
import (
"fmt"
)
func main() {
var val interface{} = "good"
fmt.Println(val.(string))
// fmt.Println(val.(int))
}
以上的代码中被注释的那一行会运行时错误。这是因为val实际存储的是string类型,因此断言失败。
还有一种转换方式是switch测试。既然称之为switch测试,也就是说这种转换方式只能出现在switch语句中。可以很轻松的将刚才用Comma-ok断言的例子换成由switch测试来实现:
package main
import (
"fmt"
)
type Html []interface{}
func main() {
html := make(Html, 5)
html[0] = "div"
html[1] = "span"
html[2] = []byte("script")
html[3] = "style"
html[4] = "head"
for index, element := range html {
switch value := element.(type) {
case string:
fmt.Printf("html[%d] is a string and its value is %s\n", index, value)
case []byte:
fmt.Printf("html[%d] is a []byte and its value is %s\n", index, string(value))
case int:
fmt.Printf("error type\n")
default:
fmt.Printf("unknown type\n")
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import (
"fmt"
"reflect"
)
func main() {
var val interface{} = int64(58)
fmt.Println(reflect.TypeOf(val))
val = 50
fmt.Println(reflect.TypeOf(val))
}

我们已经知道接口类型的变量底层是作为两个成员来实现,一个是type,一个是data。type用于存储变量的动态类型,data用于存储变量的具体数据。在上面的例子中,第一条打印语句输出的是:int64。这是因为已经显示的将类型为int64的数据58赋值给了interface类型的变量val,所以val的底层结构应该是:(int64, 58)。我们暂且用这种二元组的方式来描述,二元组的第一个成员为type,第二个成员为data。第二条打印语句输出的是:int。这是因为字面量的整数在golang中默认的类型是int,所以这个时候val的底层结构就变成了:(int, 50)。

理解上就是,感觉比静态的类型逼格高一点,总之我的interface就写在那里,你要是想继承我,使用我的方法,就把这些方法实现了就行,并不要求你显示的声明,继承自我,怎样怎样。有些动态类型的语言在运行期才能进行类型的语法检测,go语言在编译期间就可以检测。

参考文章:

编程规范

Go学习(三)Go基本数据类型总结

最近一直在写Go,但是一直都不是很明白Go的基础数据类型都有哪些,int,int32,int64都有,而且运算的时候还需要先转换,感觉使用起来很麻烦,所以特意看了一下官方文档深入了解一下Go相关的数据类型。

  • 基本类型:boolean,numeric,string类型的命名实例是预先声明的。
  • 复合类型:array,struct,指针,function,interface,slice,map,channel类型(可以使用type构造)。

Numeric types

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
A numeric type represents sets of integer or floating-point values. The predeclared architecture-independent numeric types are:
uint8 the set of all unsigned 8-bit integers (0 to 255)
uint16 the set of all unsigned 16-bit integers (0 to 65535)
uint32 the set of all unsigned 32-bit integers (0 to 4294967295)
uint64 the set of all unsigned 64-bit integers (0 to 18446744073709551615)
int8 the set of all signed 8-bit integers (-128 to 127)
int16 the set of all signed 16-bit integers (-32768 to 32767)
int32 the set of all signed 32-bit integers (-2147483648 to 2147483647)
int64 the set of all signed 64-bit integers (-9223372036854775808 to 9223372036854775807)
float32 the set of all IEEE-754 32-bit floating-point numbers
float64 the set of all IEEE-754 64-bit floating-point numbers
complex64 the set of all complex numbers with float32 real and imaginary parts
complex128 the set of all complex numbers with float64 real and imaginary parts
byte alias for uint8
rune alias for int32
The value of an n-bit integer is n bits wide and represented using two's complement arithmetic.
There is also a set of predeclared numeric types with implementation-specific sizes:
uint either 32 or 64 bits
int same size as uint
uintptr an unsigned integer large enough to store the uninterpreted bits of a pointer value
To avoid portability issues all numeric types are distinct except byte, which is an alias for uint8, and rune, which is an alias for int32. Conversions are required when different numeric types are mixed in an expression or assignment.
For instance, int32 and int are not the same type even though they may have the same size on a particular architecture.
  1. int类型中哪些支持负数

    • 有符号(负号):int8 int16 int32 int64
    • 无符号(负号):uint8 uint16 uint32 uint64
  2. 浮点类型的值有float32和float64(没有 float 类型)

  3. byte和rune特殊类型是别名

    • byte就是unit8的别名
    • rune就是int32的别名
  4. int和uint取决于操作系统(32位机器上就是32字节,64位机器上就是64字节)

    • uint是32字节或者64字节
    • int和uint是一样的大小
  5. 为了避免可移植性问题,除了byte(它是uint8的别名)和rune(它是int32的别名)之外,所有数字类型都是不同的。 在表达式或赋值中混合使用不同的数字类型时,需要转换。例如,int32和int不是相同的类型,即使它们可能在特定架构上具有相同的大小。

所以上面的文档解释了为什么int,int32,int64之间需要进行类型转换才能进行运算。

String types

1
2
3
A string type represents the set of string values. A string value is a (possibly empty) sequence of bytes. Strings are immutable: once created, it is impossible to change the contents of a string. The predeclared string type is string.
The length of a string s (its size in bytes) can be discovered using the built-in function len. The length is a compile-time constant if the string is a constant. A string's bytes can be accessed by integer indices 0 through len(s)-1. It is illegal to take the address of such an element; if s[i] is the i'th byte of a string, &s[i] is invalid.

字符串是不可变的:一旦创建,就不可能改变字符串的内容。 预先声明的字符串类型是字符串。

可以使用内置函数len来发现字符串s的长度(以字节为单位的大小)。 如果字符串是常量,则length是编译时常量。 字符串的字节可以通过整数索引0到len(s)-1来访问。 取这种元素的地址是非法的; 如果s [i]是字符串的第i个字节,则&s [i]无效。

Map Types

1
2
3
make(map[string]int)
make(map[string]int, 100)
The initial capacity does not bound its size: maps grow to accommodate the number of items stored in them, with the exception of nil maps. A nil map is equivalent to an empty map except that no elements may be added.

初始容量不限制其大小:map增长以适应存储在其中的项目数,除了nil map。 nil map等价于空map,不能添加元素。

int,int32,int64相互转换

int转换成int32,int64
1
2
3
4
5
// int转换成int32
i32 = int32(i)
// int转换成int64
i64 = int64(i)
int32,int64转换成int
1
2
3
4
5
// int32转换成int
i = int(int32)
// int64转换成int
i = int(int64)
实例测试

测试一:

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
package main
import (
"fmt"
)
func main() {
// 超出int32范围的值
var i64 int64 = 2147483647123
var i32 int32
var i int
// 转换不会损失精读(因为操作系统是64位,int就相当于int64)
i = int(i64)
// 转换会损失精读
i32 = int32(i64)
fmt.Println(i)
fmt.Println(i32)
fmt.Println(i64)
}
结果如下:
2147483647123
-877
2147483647123

测试二:

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
package main
import (
"fmt"
)
func main() {
var i64 int64
// 不超出int32范围的值
var i32 int32 = 21474836
var i int
// 转换不会损失精读
i = int(i32)
// 转换不会损失精读
i64 = int64(i32)
fmt.Println(i)
fmt.Println(i32)
fmt.Println(i64)
}
结果如下:
21474836
21474836
21474836

int和string相互转换

int,int32,int64转换成string
1
2
3
4
5
6
7
8
// 通过fmt.Sprintf方法转换(%d代表Integer,i可以是int,int32,int64类型)
str1 := fmt.Sprintf("%d", i)
// 通过strconv.Itoa方法转换(i是int类型)
str2 := strconv.Itoa(i)
// 通过strconv.FormatInt方法转换(i可以是int,int32,int64类型)
str3 := strconv.FormatInt(int64(i), 10)
  • fmt.Sprintf
1
2
3
4
5
6
7
8
9
// Sprint formats using the default formats for its operands and returns the resulting string.
// Spaces are added between operands when neither is a string.
func Sprint(a ...interface{}) string {
p := newPrinter()
p.doPrint(a)
s := string(p.buf)
p.free()
return s
}
  • strconv.Itoa实现
1
2
3
func Itoa(i int) string {
return FormatInt(int64(i), 10)
}
  • strconv.FormatInt实现
1
2
3
4
5
6
7
// FormatInt returns the string representation of i in the given base,
// for 2 <= base <= 36. The result uses the lower-case letters 'a' to 'z'
// for digit values >= 10.
func FormatInt(i int64, base int) string {
_, s := formatBits(nil, uint64(i), base, i < 0, false)
return s
}
string转换成int,int32,int64
1
2
3
4
5
6
7
8
9
10
11
// string转换成int64
strInt64, _ := strconv.ParseInt(str, 10, 64)
// string转换成int32
strInt32, _ := strconv.ParseInt(str, 10, 32)
// 这里strInt32实际上还是int64类型的,只是截取了32位,所以最终还是要强转一下变成int32类型,如果不强转成int32是会编译报错的
var realInt32 int32 = 0
realInt32 := int32(strInt32)
// string转换成int
strInt, err := strconv.Atoi(str)

参考文章:

Go学习(二)引用第三方包

上一篇开发环境搭建已经简单的引用了第三方glog包,这里我们回顾一下

引用第三方包

若你在包的导入路径中包含了代码仓库的URL,go get就会自动地获取、构建并安装它:

1
$ go get github.com/golang/glog

若指定的包不在工作空间中,go get就会将会将它放到GOPATH指定的第一个工作空间内。(若该包已存在,go get就会跳过远程获取,其行为与go install相同)

在执行完上面的go get命令后,工作空间的目录树看起来应该是这样的:

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
workspace_go
|
|--- TestGo
| |
| |--- src
| | |
| | |--- github.com
| | | |
| | | |--- golang
| | | |
| | | |--- glog
| | | |
| | | |--- LICENSE
| | | |
| | | |--- README
| | | |
| | | |--- glog.go
| | | |
| | | |--- glog_file.go
| | | |
| | | |--- glog_test.go
| | |
| | |--- hello
| | | |
| | | |--- hello.go
| | |
| | |--- main
| | |
| | |--- main.go
| |
| |--- bin
| | |
| | |--- main
| |
| |
| |--- pkg
| |
| |--- github.com
| | |
| | |--- golang
| | |
| | |--- glog.a
| |
| |--- hello.a
|
|-- GoDemo

hello.go

1
2
3
4
5
6
7
8
9
10
11
12
package hello
import "fmt"
import "flag"
import "github.com/golang/glog"
func Hello() {
flag.Parse()
fmt.Println("Hello World!")
glog.Info("glog Hello World!")
glog.Flush()
}

注意事项:

  1. 因为是使用时依参数配置,所以需在main()中加上flag.Parse()
  2. 需在结尾加上glog.Flush()
  3. 依级别生成不同的日志文件,但级别高的日志信息会同时在级别低的日志文件中输出
1
2
3
4
5
6
7
8
9
10
11
12
13
$ go run main.go -log_dir=./
Hello World!
$ ll
lrwxr-xr-x 1 yunyu staff 51 12 27 11:44 main.INFO -> main.localhost.yunyu.log.INFO.20161227-114425.19830
-rw-r--r-- 1 yunyu staff 72 12 22 14:39 main.go
-rw-r--r-- 1 yunyu staff 186 12 27 11:44 main.localhost.yunyu.log.INFO.20161227-114425.19830
$ vi main.INFO
Log file created at: 2016/12/27 11:48:08
Running on machine: localhost
Binary: Built with gc go1.7.3 for darwin/amd64
Log line format: [IWEF]mmdd hh:mm:ss.uuuuuu threadid file:line] msg
I1227 11:48:08.088596 19951 hello.go:10] glog Hello World!

import用法

1
2
3
import(
"fmt"
)

上面这个fmt是Go语言的标准库,他其实是去GOROOT下去加载该模块(先找GOROOT,如果GOROOT找不到在去GOPATH找),当然Go的import还支持如下两种方式来加载自己写的模块:

相对路径

1
import "./model"

当前文件同一目录的model目录,但是不建议这种方式来import

绝对路径

1
import "shorturl/model"

加载gopath/src/shorturl/model模块

点操作

1
import( . "fmt" )

这个点操作的含义就是这个包导入之后在你调用这个包的函数时,你可以省略前缀的包名,
也就是前面你调用的fmt.Println(“hello world”)可以省略的写成Println(“hello world”)

别名操作

别名操作顾名思义我们可以把包命名成另一个我们用起来容易记忆的名字

1
import( f "fmt" )

别名操作的话调用包函数时前缀变成了我们的前缀,即f.Println(“hello world”)

_操作

这个操作经常是让很多人费解的一个操作符,请看下面这个import

1
import ( "database/sql" _ "github.com/ziutek/mymysql/godrv" )

_操作其实是引入该包,而不直接使用包里面的函数,而是调用了该包里面的init函数。

要理解这个问题,需要看下面这个图,理解包是怎么按照顺序加载的:

程序的初始化和执行都起始于main包。如果main包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到fmt包,但它只会被导入一次,因为没有必要导入多次)。当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行init函数(如果有的话),依次类推。等所有被导入的包都加载完毕了,就会开始对main包中的包级常量和变量进行初始化,然后执行main包中的init函数(如果存在的话),最后执行main函数。下图详细地解释了整个执行过程:

通过上面的介绍我们了解了import的时候其实是执行了该包里面的init函数,初始化了里面的变量,_操作只是说该包引入了,我只初始化里面的 init函数和一些变量,但是往往这些init函数里面是注册自己包里面的引擎,让外部可以方便的使用,就很多实现database/sql的引起,在 init函数里面都是调用了sql.Register(name string, driver driver.Driver)注册自己,然后外部就可以使用了。

示例目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
GOPATH
|
|--bin
|
|--pkg
|
|--src
|
|--main.go
|
|--hello
|
|--init.go

main.go

1
2
3
4
5
6
7
package main
import _ "hello"
func main() {
// hello.Print() 编译报错,说:undefined: hello
}

init.go

1
2
3
4
5
6
7
8
9
10
11
package hello
import "fmt"
func init() {
fmt.Println("hello-init() come here.")
}
func Print() {
fmt.Println("Hello!")
}

输出结果:hello-init() come here.

总结

命令安装第三方包

安装第三方包命令,这条命令它会把第三方包源代码,下载解压到你的GOPATH路径里面去

1
go get github.com/golang/glog

注意:首先必须设置环境变量GOPATH的路径,而且要安装了git客户端,否则go get命令不起作用

在代码中导入下载的那个第三方包

1
2
3
import (
"github.com/golang/glog"
)

手动安装第三方包

如果是手动下载压缩包,就解压到$GOPATH/src里面的路径,然后执行go install github.com/golang/glog安装这个包。

参考文章:

Kafka学习(六)Kafka集群的迁移与扩容

Kafka的集群扩容实际上就是把Topic的Partition移动到新加的集群节点上。我们只需要copy一份Kafka的安装目录到新的节点机器上,修改一下相关配置文件(broker.id,logs.dir等等),但是新添加的Kafka节点机器是不会自动分配数据的,所以需要我们手动操作。

Kafka的扩容可以细分为以下三种:

  • 增加节点
  • 增加Topic副本
  • 增加分区

具体的步骤有两种方式:

  • 通过–topics-to-move-json-file和–broker-list批量生成新的Topic分区信息,然后根据该信息执行转移操作。
  • 手动写要移动的topic信息,更灵活,但是在大量Topic和Partition的情况下非常繁琐并且容易出错。
1
2
3
4
5
6
7
8
9
kafka-reassign-partitions.sh命令的三种模式
generate模式:给需要重新分配的Topic,自动生成reassign plan(执行计划),但并不执行
execute模式:根据指定的reassign plan(json文件)重新分配partition或者replication
verify模式:检查重新分配是否完成
$ kafka-reassign-partitions.sh --zookeeper localhost:2181 --topics-to-move-json-file json_file --broker-list "brokerIds" --generate
$ kafka-reassign-partitions.sh --zookeeper localhost:2181 --reassignment-json-file json_file --execute
$ kafka-reassign-partitions.sh --zookeeper localhost:2181 --reassignment-json-file json_file --verify

Kafka动态增加节点(Node)

新添加的Kafka节点机器是不会自动分配数据的,所以无法分担集群的负载,除非我们新建一个Topic,此时新的Topic会使用新添加的Kafka节点机器。如果我们想新添加的Kafka节点机器能够分担集群存储,需要手动将部分分区移动到新添加的Kafka节点机器上。

我们原来的Kafka节点分别是0,1,2,现在要加入3,4两个新节点

topicMove.json

1
2
3
4
5
6
{
"topics":[
{"topic":"logstash_test"}
],
"version":1
}

生成迁移的计划

1
2
3
4
5
6
7
$ kafka-reassign-partitions.sh --zookeeper localhost:2181 --topics-to-move-json-file topicMove.json --broker-list "0,1,2,3,4" --generate
Current partition replica assignment
{"version":1,"partitions":[{"topic":"logstash_test","partition":3,"replicas":[1,2,0]},{"topic":"logstash_test","partition":1,"replicas":[2,1,0]},{"topic":"logstash_test","partition":0,"replicas":[1,2,0]},{"topic":"logstash_test","partition":2,"replicas":[0,2,1]},{"topic":"logstash_test","partition":5,"replicas":[0,1,2]},{"topic":"logstash_test","partition":4,"replicas":[2,0,1]}]}
Proposed partition reassignment configuration
{"version":1,"partitions":[{"topic":"logstash_test","partition":3,"replicas":[2,0,1]},{"topic":"logstash_test","partition":1,"replicas":[0,3,4]},{"topic":"logstash_test","partition":0,"replicas":[4,2,3]},{"topic":"logstash_test","partition":2,"replicas":[1,4,0]},{"topic":"logstash_test","partition":5,"replicas":[4,3,0]},{"topic":"logstash_test","partition":4,"replicas":[3,1,2]}]}

上面是原来的所有partition在各个节点的分布情况,下面是加入4,5两个新节点之后所有partition在各个节点的分布情况

将生成的执行计划保存为add_node.json文件

重新分配partition(其实这里是添加新的节点)

1
2
3
4
5
6
7
$ kafka-reassign-partitions.sh --zookeeper localhost:2181 --reassignment-json-file ~/Downloads/add_node.json --execute
Current partition replica assignment
{"version":1,"partitions":[{"topic":"logstash_test","partition":3,"replicas":[1,2,0]},{"topic":"logstash_test","partition":1,"replicas":[2,1,0]},{"topic":"logstash_test","partition":0,"replicas":[1,2,0]},{"topic":"logstash_test","partition":2,"replicas":[0,2,1]},{"topic":"logstash_test","partition":5,"replicas":[0,1,2]},{"topic":"logstash_test","partition":4,"replicas":[2,0,1]}]}
Save this to use as the --reassignment-json-file option during rollback
Successfully started reassignment of partitions {"version":1,"partitions":[{"topic":"logstash_test","partition":0,"replicas":[4,2,3]},{"topic":"logstash_test","partition":4,"replicas":[3,1,2]},{"topic":"logstash_test","partition":5,"replicas":[4,3,0]},{"topic":"logstash_test","partition":2,"replicas":[1,4,0]},{"topic":"logstash_test","partition":3,"replicas":[2,0,1]},{"topic":"logstash_test","partition":1,"replicas":[0,3,4]}]}

查看执行的状态

1
$ kafka-reassign-partitions.sh --zookeeper localhost:2181 --reassignment-json-file ~/Downloads/add_node.json --verify
1
ERROR: Assigned replicas (X,X) don't match the list of replicas for reassignment (X,X) for partition [logstash_test,1]

假设出现类似这样的错误,他并不是真的出错,而是指目前仍在复制数据中。再过一段时间再运行verify命令,他就会消失(加入完成拷贝)

当然也可以不使用generate先生成执行计划,而是自己直接手动编辑生成add_node.json文件内容,然后直接execute执行,但是这样容易出错,Kafka节点比较少的时候推荐使用。

注意:其实通过generate的结果我们也可以看出,其实不管是扩容,减容,迁移,其实都是重新分配Topic或者Partition的过程。json文件的内容都是指定Topic下的Partition要移动到哪个Node上。

Kafka动态增加Topic副本(Replication)

查看当前node_log的Topic信息

1
2
3
$ kafka-topics.sh --describe --zookeeper localhost:2181 --topic node_log
Topic:node_log PartitionCount:1 ReplicationFactor:1 Configs:
Topic: node_log Partition: 0 Leader: 2 Replicas: 2 Isr: 2

add_replication.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"version":1
"partitions":[
{
"topic":"node_log",
"partition":0,
"replicas":[
0,
1,
2
]
}
]
}

重新分配partition(其实这里是添加副本replication)

1
2
3
4
5
6
7
$ kafka-reassign-partitions.sh --zookeeper localhost:2181 --reassignment-json-file ~/Downloads/add_replication.json --execute
Current partition replica assignment
{"version":1,"partitions":[{"topic":"node_log","partition":0,"replicas":[2]}]}
Save this to use as the --reassignment-json-file option during rollback
Successfully started reassignment of partitions {"version":1,"partitions":[{"topic":"node_log","partition":0,"replicas":[0,1,2]}]}

查看执行的状态

1
2
3
$ kafka-reassign-partitions.sh --zookeeper localhost:2181 --reassignment-json-file ~/Downloads/add_replication.json --verify
Status of partition reassignment:
Reassignment of partition [node_log,0] completed successfully

如果遇到下面的错误很有可能是json文件格式有错误,仔细检查修正重新运行即可

1
2
3
4
5
Partitions reassignment failed due to Partition reassignment data file add_replication.json is empty
kafka.common.AdminCommandFailedException: Partition reassignment data file add_replication.json is empty
at kafka.admin.ReassignPartitionsCommand$.executeAssignment(ReassignPartitionsCommand.scala:120)
at kafka.admin.ReassignPartitionsCommand$.main(ReassignPartitionsCommand.scala:52)
at kafka.admin.ReassignPartitionsCommand.main(ReassignPartitionsCommand.scala)

查看添加replication后的node_log的Topic信息

1
2
3
$ kafka-topics.sh --describe --zookeeper localhost:2181 --topic node_log
Topic:node_log PartitionCount:1 ReplicationFactor:2 Configs:
Topic: node_log Partition: 0 Leader: 0 Replicas: 0,1,2 Isr: 1,0,2

Kafka动态增加分区(Parition)

查看当前node_log的Topic信息

1
2
3
$ kafka-topics.sh --describe --zookeeper localhost:2181 --topic node_log
Topic:node_log PartitionCount:1 ReplicationFactor:2 Configs:
Topic: node_log Partition: 0 Leader: 0 Replicas: 0,1,2 Isr: 1,0,2

add_partition.json

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
{
"version":1,
"partitions":[
{
"topic":"node_log",
"partition":0,
"replicas":[
0,
1,
2
]
},
{
"topic":"node_log",
"partition":1,
"replicas":[
0,
1,
2
]
},
{
"topic":"node_log",
"partition":2,
"replicas":[
0,
1,
2
]
}
]
}

重新分配partition

1
2
3
4
5
6
7
8
9
$ kafka-reassign-partitions.sh --zookeeper localhost:2181 --reassignment-json-file ~/Downloads/add_partition.json --execute
Current partition replica assignment
{"version":1,"partitions":[{"topic":"node_log","partition":0,"replicas":[0,1,2]}]}
Save this to use as the --reassignment-json-file option during rollback
[2016-12-30 12:22:17,195] ERROR Skipping reassignment of partition [node_log,1] since it doesn't exist (kafka.admin.ReassignPartitionsCommand)
[2016-12-30 12:22:17,207] ERROR Skipping reassignment of partition [node_log,2] since it doesn't exist (kafka.admin.ReassignPartitionsCommand)
Successfully started reassignment of partitions {"version":1,"partitions":[{"topic":"node_log","partition":0,"replicas":[0,1,2]},{"topic":"node_log","partition":1,"replicas":[0,1,2]},{"topic":"node_log","partition":2,"replicas":[0,1,2]}]}

查看执行的状态

1
2
3
4
5
6
7
$ kafka-reassign-partitions.sh --zookeeper localhost:2181 --reassignment-json-file ~/Downloads/add_partition.json --verify
Status of partition reassignment:
ERROR: Assigned replicas (1,2,0) don't match the list of replicas for reassignment (0,1,2) for partition [node_log,1]
ERROR: Assigned replicas (2,0,1) don't match the list of replicas for reassignment (0,1,2) for partition [node_log,2]
Reassignment of partition [node_log,0] completed successfully
Reassignment of partition [node_log,1] failed
Reassignment of partition [node_log,2] failed

报错是因为我们只有一个partition0,没有partition1,partition2,所以跳过了重新分配partition的过程。

我们需要先增加partition的数量,我们把partition的数量变成6个

1
2
3
$ kafka-topics.sh --zookeeper localhost:2181 --alter --topic node_log --partitions 6
WARNING: If partitions are increased for a topic that has a key, the partition logic or ordering of the messages will be affected
Adding partitions succeeded!

查看添加partition后的node_log的Topic信息

1
2
3
4
5
6
7
8
$ kafka-topics.sh --describe --zookeeper localhost:2181 --topic node_log
Topic:node_log PartitionCount:6 ReplicationFactor:3 Configs:
Topic: node_log Partition: 0 Leader: 0 Replicas: 0,1,2 Isr: 1,0,2
Topic: node_log Partition: 1 Leader: 1 Replicas: 1,2,0 Isr: 1,2,0
Topic: node_log Partition: 2 Leader: 2 Replicas: 2,0,1 Isr: 2,0,1
Topic: node_log Partition: 3 Leader: 0 Replicas: 0,2,1 Isr: 0,2,1
Topic: node_log Partition: 4 Leader: 1 Replicas: 1,0,2 Isr: 1,0,2
Topic: node_log Partition: 5 Leader: 2 Replicas: 2,1,0 Isr: 2,1,0

重新执行reassign,然后查看执行状态

1
2
3
4
5
$ kafka-reassign-partitions.sh --zookeeper localhost:2181 --reassignment-json-file ~/Downloads/add_partition.json --verify
Status of partition reassignment:
Reassignment of partition [node_log,0] completed successfully
Reassignment of partition [node_log,1] completed successfully
Reassignment of partition [node_log,2] completed successfully

重新分配执行完成之后,再次查看node_log的Topic信息

1
2
3
4
5
6
7
8
9
$ kafka-topics.sh --describe --zookeeper localhost:2181 --topic node_log
Topic:node_log PartitionCount:6 ReplicationFactor:3 Configs:
Topic: node_log Partition: 0 Leader: 0 Replicas: 0,1,2 Isr: 1,2,0
Topic: node_log Partition: 1 Leader: 0 Replicas: 0,1,2 Isr: 1,2,0
Topic: node_log Partition: 2 Leader: 0 Replicas: 0,1,2 Isr: 1,2,0
Topic: node_log Partition: 3 Leader: 0 Replicas: 0,2,1 Isr: 1,2,0
Topic: node_log Partition: 4 Leader: 1 Replicas: 1,0,2 Isr: 1,2,0
Topic: node_log Partition: 5 Leader: 2 Replicas: 2,1,0 Isr: 1,2,0

执行reassign之后,我们发现0-3的partition的leader都是0,这是因为我们之前突然扩展partition到6个,而我们在reassign的之后只指定了0-2的partition的分配,我们修改一下add_partition.json文件如下,将6个partition均匀的分布在我们3个broker节点上,每个partition有2个replication。

add_partition.json

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
{
"version":1,
"partitions":[
{
"topic":"node_log",
"partition":0,
"replicas":[
0,
1
]
},
{
"topic":"node_log",
"partition":1,
"replicas":[
1,
2
]
},
{
"topic":"node_log",
"partition":2,
"replicas":[
2,
0
]
},
{
"topic":"node_log",
"partition":3,
"replicas":[
0,
1
]
},
{
"topic":"node_log",
"partition":4,
"replicas":[
1,
2
]
},
{
"topic":"node_log",
"partition":5,
"replicas":[
2,
0
]
}
]
}

再次查看reassign之后的Topic情况

1
2
3
4
5
6
7
8
$ kafka-topics.sh --describe --zookeeper localhost:2181 --topic node_log
Topic:node_log PartitionCount:6 ReplicationFactor:2 Configs:
Topic: node_log Partition: 0 Leader: 0 Replicas: 0,1 Isr: 0,1
Topic: node_log Partition: 1 Leader: 1 Replicas: 1,2 Isr: 1,2
Topic: node_log Partition: 2 Leader: 0 Replicas: 2,0 Isr: 0,2
Topic: node_log Partition: 3 Leader: 0 Replicas: 0,1 Isr: 0,1
Topic: node_log Partition: 4 Leader: 1 Replicas: 1,2 Isr: 1,2
Topic: node_log Partition: 5 Leader: 2 Replicas: 2,0 Isr: 0,2

我们发现还是有点小问题,partition2的leader仍然是0。这里先普及一下assigned replicas和preferred replica

assigned replicas和preferred replica

每个partitiion的所有replicas叫做”assigned replicas”,”assigned replicas”中的第一个replicas叫”preferred replica”,刚创建的topic一般”preferred replica”是leader。leader replica负责所有的读写。但随着时间推移,broker可能会停机,会导致leader迁移,导致机群的负载不均衡。

我们这里preferred replica已经是2了,但是leader却不是2,这样我们需要重新选举一下leader,需要使用kafka-preferred-replica-election.sh来调整。

两种操作方式:

  • 对所有Topics进行操作
1
$ kafka-preferred-replica-election.sh --zookeeper localhost:2181
  • 对某个Topic进行操作(json文件中指定要操作的Topic)
1
$ kafka-preferred-replica-election.sh --zookeeper localhost:2181 --path-to-json-file json_file

注意:这里我们只需要调整node_log的Topic的partition2的leader。

leaderTopic.json

1
2
3
4
5
6
{
"partitions":
[
{"topic":"node_log","partition":2}
]
}

重新选举leader

1
2
$ kafka-preferred-replica-election.sh --zookeeper localhost:2181 --path-to-json-file leaderTopic.json
Successfully started preferred replica election for partitions Set([node_log,2])

再次查看Topic情况,发现leader也是我们所期望的均匀分配了

1
2
3
4
5
6
7
8
$ kafka-topics.sh --describe --zookeeper localhost:2181 --topic node_log
Topic:node_log PartitionCount:6 ReplicationFactor:2 Configs:
Topic: node_log Partition: 0 Leader: 0 Replicas: 0,1 Isr: 0,1
Topic: node_log Partition: 1 Leader: 1 Replicas: 1,2 Isr: 2,1
Topic: node_log Partition: 2 Leader: 2 Replicas: 2,0 Isr: 2,0
Topic: node_log Partition: 3 Leader: 0 Replicas: 0,1 Isr: 0,1
Topic: node_log Partition: 4 Leader: 1 Replicas: 1,2 Isr: 2,1
Topic: node_log Partition: 5 Leader: 2 Replicas: 2,0 Isr: 0,2

数据迁移的步骤

  1. The tool updates the zookeeper path “/admin/reassign_partitions” with the list of topic partitions and (if specified in the Json file) the list of their new assigned replicas.

  2. The controller listens to the path above. When a data change update is triggered, the controller reads the list of topic partitions and their assigned replicas from zookeeper.

  3. For each topic partition, the controller does the following:

  • 3.1. Start new replicas in RAR – AR (RAR = Reassigned Replicas, AR = original list of Assigned Replicas)
  • 3.2. Wait until new replicas are in sync with the leader
  • 3.3. If the leader is not in RAR, elect a new leader from RAR
  • 3.4. Stop old replicas AR – RAR
  • 3.5. Write new AR
  • 3.6. Remove partition from the /admin/reassign_partitions path

参考文章:

Kafka学习(五)Kafka数据目录的迁移与扩容

之前我们只有一个40G的磁盘挂在到/data0目录,Kafka的数据目录和日志目录都是写在/data0这个磁盘上的。但是很快收集上来的日志就将40G的磁盘写满了,然后我们又为Kafka集群的机器加了200G的磁盘,当时的200G磁盘是分成了2个100G的分别挂在到/data1和/data2目录上,我们也将原来40G的/data0目录下的数据全部都移动到/data1目录下,然后修改Kakfa的log.dirs配置,指定了多个Kafka数据存储目录。如下:

1
log.dirs=/data1/kafka_data,/data2/kafka_data

重启Kafka集群之后一切都运行正常,但是/data2目录中却没有Topic的数据,当时也觉得有些奇怪,觉得可能是因为/data1磁盘没有写满就不会写入到/data2磁盘上,而且短时间内Kafka数据应该不会增长这么快,就没有深入研究。

悲剧来了,圣诞节放假期间公司线上Kafka集群突然磁盘占用已满,后来发现是日志量突然暴增,把Kafka磁盘写满了。SSH到线上Kafka服务器发现/data1磁盘已经写满了,但是仍然没有写入到/data2磁盘,说明我之前的猜想是错的(/data1写满了才会写入到/data2)。

开始以为是log.dirs配置没有生效,我又尝试在自己本机的Kafka集群尝试了修改配置,发现配置生效了,但是和线上服务器有些不同的是,在我本机的Kafka集群指定的/data2目录下是有文件的,也有Topic数据的。

1
2
3
4
$ ll /data/kafka_data1 total 28 drwxrwxr-x 4 yunyu yunyu 4096 Dec 26 16:57 ./ drwxr-xr-x 17 yunyu yunyu 4096 Dec 24 23:39 ../ drwxrwxr-x 2 yunyu yunyu 4096 Dec 24 23:56 go_log-0/ drwxrwxr-x 2 yunyu yunyu 4096 Dec 24 23:56 golog-0/ -rw-rw-r-- 1 yunyu yunyu 0 Dec 24 23:56 .lock -rw-rw-r-- 1 yunyu yunyu 54 Dec 24 23:56 meta.properties -rw-rw-r-- 1 yunyu yunyu 25 Dec 26 16:57 recovery-point-offset-checkpoint -rw-rw-r-- 1 yunyu yunyu 25 Dec 26 16:57 replication-offset-checkpoint
$ ll /data/kafka_data2 total 24 drwxrwxr-x 3 yunyu yunyu 4096 Dec 26 16:57 ./ drwxr-xr-x 17 yunyu yunyu 4096 Dec 24 23:39 ../ -rw-rw-r-- 1 yunyu yunyu 0 Dec 24 23:56 .lock drwxrwxr-x 2 yunyu yunyu 4096 Dec 24 23:56 logstash_test-0/ -rw-rw-r-- 1 yunyu yunyu 54 Dec 24 23:56 meta.properties -rw-rw-r-- 1 yunyu yunyu 22 Dec 26 16:57 recovery-point-offset-checkpoint -rw-rw-r-- 1 yunyu yunyu 22 Dec 26 16:57 replication-offset-checkpoint

注意:我们本地和线上的Kafka集群都是三台机器,Node1,Node2,Node3

于是我又查看了Kafka集群的其他几个节点,发现每个节点/data2都有数据,但是Topic数量却都不一样,golog这个Topic在Kafka的所有节点都有数据,但是node_log_test这个Topic却只在Node3有,在Node2和Node3就没有,这是啥情况啊???

后来我仔细想了一下,是我在创建Topic的时候没有指定副本个数,默认的副本个数是1个,所以就只会在Kafka集群的一个节点上有数据。为了验证我的想法,我又查询了一下这两个Topic的详细信息。

1
2
3
4
5
6
# golog这个Topic是有两个副本的,所以在三台机器上都有数据存储
$ kafka-topics.sh --describe --zookeeper localhost:2181 --topic golog Topic:golog PartitionCount:1 ReplicationFactor:3 Configs: Topic: golog Partition: 0 Leader: 2 Replicas: 2,0,1 Isr: 0,1,2
# node_log_test这个Topic是没有副本的,所以在三台机器上只有Node3一台机器上有数据存储
$ kafka-topics.sh --describe --zookeeper localhost:2181 --topic node_log_test Topic:node_log_test PartitionCount:1 ReplicationFactor:1 Configs: Topic: node_log_test Partition: 0 Leader: 2 Replicas: 2 Isr: 2

根据上面的信息,我猜想有可能创建Topic的时候就已经指定存储的磁盘了(因为我们创建Topic的时候使用的之前的/data0磁盘,后来扩容的时候将/data0的磁盘数据整体迁移到/data1磁盘的),即使后来我们修改了log.dirs也只会影响新创建的Topic,对以前已经创建好的Topic是没有影响的。带着这个猜想,又仔细观察了一下线上Kafka服务器的kafka_data目录,/data1和/data2下面会有一些相同的文件,其他的文件夹都是Topic数据(log_go-0, log_node-0, log_php-0这三个是我们的Topic数据)。这几个相同的文件应该是指定log.dirs配置Kafka启动的时候创建的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ ls /data1/kafka_data
.lock
cleaner-offset-checkpoint
log_go-0/
log_node-0/
log_php-0/
meta.properties
recovery-point-offset-checkpoint
replication-offset-checkpoint
$ ls /data2/kafka_data
.lock
cleaner-offset-checkpoint
meta.properties
recovery-point-offset-checkpoint
replication-offset-checkpoint

发现只有recovery-point-offset-checkpoint和replication-offset-checkpoint文件不同,/data1目录下的这两个文件都有记录对应Topic和offset,而/data2目录下的这两个文件都是空的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ vi /data1/kafka_data/recovery-point-offset-checkpoint
0
3
log_node 0 18213007
log_go 0 41049987
log_php 0 100203453
$ vi /data1/kafka_data/replication-offset-checkpoint
0
3
log_node 0 18623953
log_go 0 41248972
log_php 0 100727371

我大胆的猜测,Kafka就是靠这个来读取数据的,我只要把磁盘占用比较大的Topic数据移动到/data2/kafka_data目录下,并且把两个文件的内容修改正确,应该就可以做到安全迁移。

按照上面的思路,我将log_php这个占用47G的Topic迁移到/data2/kafka_data目录下,然后修改对应的recovery-point-offset-checkpoint和replication-offset-checkpoint文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ vi /data1/kafka_data/recovery-point-offset-checkpoint
0
2
log_node 0 18213007
log_go 0 41049987
$ vi /data1/kafka_data/replication-offset-checkpoint
0
1
log_php 0 100727371
$ vi /data2/kafka_data/recovery-point-offset-checkpoint
0
2
log_node 0 18213007
log_go 0 41049987
$ vi /data2/kafka_data/replication-offset-checkpoint
0
1
log_php 0 100727371

这里需要注意一下,第二行的数字是当前目录下的Topic数量,这一点是我根据本地Kafka集群和线上Kafka集群的配置的不同而推测出来的。

最后重启Kafka集群成功了~~

参考文章:

Linux常用命令(四)

kill命令

kill命令是通过向进程发送指定的信号来结束相应进程的。在默认情况下,采用编号为15的TERM信号。TERM信号将终止所有不能捕获该信号的进程。对于那些可以捕获该信号的进程就要用编号为9的kill信号,强行“杀掉”该进程。

  1. 命令格式:
    kill[参数][进程号]

  2. 命令功能:
    发送指定的信号到相应进程。不指定信号将发送SIGTERM(15)终止指定进程。如果任无法终止该程序可用“-KILL” 参数,其发送的信号为SIGKILL(9) ,将强制结束进程,使用ps命令或者jobs 命令可以查看进程号。root用户将影响用户的进程,非root用户只能影响自己的进程。

  3. 命令参数:

  • -l 信号,若果不加信号的编号参数,则使用“-l”参数会列出全部的信号名称
  • -a 当处理当前进程时,不限制命令名和进程号的对应关系
  • -p 指定kill 命令只打印相关进程的进程号,而不发送任何信号
  • -s 指定发送信号
  • -u 指定用户

注意:

  1. kill命令可以带信号号码选项,也可以不带。如果没有信号号码,kill命令就会发出终止信号(15),这个信号可以被进程捕获,使得进程在退出之前可以清理并释放资源。也可以用kill向进程发送特定的信号。例如:
    kill -2 123
    它的效果等同于在前台运行PID为123的进程时按下Ctrl+C键。但是,普通用户只能使用不带signal参数的kill命令或最多使用-9信号。

  2. -9信号使进程强行终止,这常会带来一些副作用,如数据丢失或者终端无法恢复到正常状态。发送信号时必须小心,只有在万不得已时,才用kill信号(9),因为进程不能首先捕获它。要撤销所有的后台作业,可以输入kill 0。因为有些在后台运行的命令会启动多个进程,跟踪并找到所有要杀掉的进程的PID是件很麻烦的事。这时,使用kill 0来终止所有由当前shell启动的进程,是个有效的方法。

  3. 特殊用法:kill -0 pid 不发送任何信号,但是系统会进行错误检查。所以经常用来检查一个进程是否存在,存在返回0;不存在返回1

1
2
3
4
5
6
7
8
9
10
# 以下的方式都是使用TERM方式终止进程
$ kill pid
$ kill -15 pid
$ kill -TERM pid
$ kill -SIGTERM pid
# 以下的方式都是使用KILL方式终止进程
$ kill -9 pid
$ kill -KILL pid
$ kill -SIGKILL pid

kill pid与kill -9 pid的区别

  • kill pid的作用是向进程号为pid的进程发送SIGTERM(这是kill默认发送的信号),该信号是一个结束进程的信号且可以被应用程序捕获。若应用程序没有捕获并响应该信号的逻辑代码,则该信号的默认动作是kill掉进程。这是终止指定进程的推荐做法。

一旦进程接收到了SIGTERM信号,会有下面几种情况发生

1
2
3
the process may stop immediately
the process may stop after a short delay after cleaning up resources
the process may keep running indefinitely

当接收到SIGTERM信号应用程序可以决定它自己如何处理,大多数的应用程序将会清理他们的资源并且停止运行,有一些则不是。当接收到SIGTERM信号一个应用程序可能会被配置做的事情完全不同。如果这个应用程序正处于一个不好的状态,例如等待磁盘IO,它可能无法相应我们发送的SIGTERM信号。

  • kill -9 pid则是向进程号为pid的进程发送SIGKILL(该信号的编号为9),从本文上面的说明可知,SIGKILL既不能被应用程序捕获,也不能被阻塞或忽略,其动作是立即结束指定进程。通俗地说,应用程序根本无法“感知”SIGKILL信号,它在完全无准备的情况下,就被收到SIGKILL信号的操作系统给干掉了,显然,在这种“暴力”情况下,应用程序完全没有释放当前占用资源的机会。事实上,SIGKILL信号是直接发给init进程的,它收到该信号后,负责终止pid指定的进程。在某些情况下(如进程已经hang死,无法响应正常信号),就可以使用kill -9来结束进程。

若通过kill结束的进程是一个创建过子进程的父进程,则其子进程就会成为孤儿进程(Orphan Process),这种情况下,子进程的退出状态就不能再被应用进程捕获(因为作为父进程的应用程序已经不存在了),不过应该不会对整个linux系统产生什么不利影响。

应用程序如何优雅退出

Linux Server端的应用程序经常会长时间运行,在运行过程中,可能申请了很多系统资源,也可能保存了很多状态,在这些场景下,我们希望进程在退出前,可以释放资源或将当前状态dump到磁盘上或打印一些重要的日志,也就是希望进程优雅退出(exit gracefully)。

从上面的介绍不难看出,优雅退出可以通过捕获SIGTERM来实现。具体来讲,通常只需要两步动作:

1)注册SIGTERM信号的处理函数并在处理函数中做一些进程退出的准备。信号处理函数的注册可以通过signal()或sigaction()来实现,其中,推荐使用后者来实现信号响应函数的设置。信号处理函数的逻辑越简单越好,通常的做法是在该函数中设置一个bool型的flag变量以表明进程收到了SIGTERM信号,准备退出。

2)在主进程的main()中,通过类似于while(!bQuit)的逻辑来检测那个flag变量,一旦bQuit在signal handler function中被置为true,则主进程退出while()循环,接下来就是一些释放资源或dump进程当前状态或记录日志的动作,完成这些后,主进程退出。

参考文章: