0%

我们可以通过 kubernetes/dashboard 来对 k8s 进行一些可视化的管理。

参考文档:

creating-sample-user

web-ui-dashboard

创建服务账户

account.yml:

1
2
3
4
5
apiVersion: v1
kind: ServiceAccount
metadata:
name: admin-user # 账户名称,可以自定义
namespace: kubernetes-dashboard
1
kubectl apply -f account.yml

创建 ClusterRoleBinding

binding.yml:

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: admin-user
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: admin-user # 账户名称,可以自定义(跟上面的定义要一致)
namespace: kubernetes-dashboard
1
kubectl apply -f binding.yml

创建 token

dashboard 是通过 token 来登录的,所以这里需要生成 token。

1
kubectl -n kubernetes-dashboard create token admin-user

启动 proxy

1
kubectl proxy

然后打开 http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/ 即可访问

打开这个链接后,输入上一步获取的 token 即可登录。

效果:

清理

如果我们不需要 dashboard 了,可以通过下面的命令来做一些清理工作:

1
2
kubectl -n kubernetes-dashboard delete serviceaccount admin-user
kubectl -n kubernetes-dashboard delete clusterrolebinding admin-user

全连接队列大小和半连接队列大小如何设置?

全连接大小:全连接大小取决于 backlogsomaxconn 的最小值,也就是 min(backlog, somaxconn)

  • somaxconn 是 Linux 内核参数,默认 128,可通过 /proc/sys/net/core/somaxconn 进行配置
  • backloglisten(int sockfd, int backlog) 函数中的参数 backlog

半连接队列大小:通过 /proc/sys/net/ipv4/tcp_max_syn_backlog 来设置

如何查看当前全连接队列和半连接队列的大小?

  1. 查看半连接队列:
1
2
netstat -antp | grep SYN_RECV | wc -l
233 # 表示半连接状态的 TCP 连接有 233 个
  1. 查看全连接队列
1
2
3
4
ss -lnt | grep 6080

State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 100 :::6080 :::*
  • -l 显示正在 Listen 的 socket
  • -n 不解析服务名称
  • -t 只显示 tcp
  • Recv-Q 完成三次握手并等待服务端 accept() 的 TCP 全连接总数
  • Send-Q 全连接队列大小

如何查看全连接半连接溢出?

netstat -s | grep SYN 如果开启了 tcp_syncookies,那么当半队列溢出的时候, 95045 SYN cookies sent 这一行会一直变大。

netstat -s | grep overflowed 直接查看全链接队列是否有溢出,数字一直变大就是有溢出。

netstat -s | egrep "listen|LISTEN"

1
2
3
4
# 半连接队列溢出次数
104625 SYNs to LISTEN sockets dropped
# 全连接队列溢出的次数
7102 times the listen queue of a socket overflowed

全连接半连接溢出后如何处理?

全连接队列满

当全连接队列已满就会根据 tcp_abort_on_overflow 策略进行处理。Linux 可通过 /proc/sys/net/ipv4/tcp_abort_on_overflow 进行配置。

  • tcp_abort_on_overflow=0,服务端 accept 队列满了的时候,客户端发来 ack,服务端直接丢弃该 ACK,此时服务端处于 SYN_RCVD 状态,客户端处于 ESTABLISHED 状态。在该状态下服务端会重传 SYN-ACK 报文。超过 tcp_synack_retries 次后,服务端不再重传,后续也不会有任何动作。此时如果客户端再发送数据过来,服务端会返回 RST

  • tcp_abort_on_overflow=1,服务端 accept 队列满了,客户端发来 ack,服务端直接返回 RST 通知 client,表示废掉这个握手过程和这个连接,client 会报 connection reset by peer

半连接队列满

如果半连接队列满了,并且没有开启 tcp_syncookies,则会丢弃;

net.ipv4.tcp_syncookies=1 表示开启 tcp_syncookies。开启后,服务端会生成一个 cookie,通过 SYN+ACK 报文返回给客户端,然后收到客户端下一个 ACK 报文后会校验 cookie,如果是服务端生成的,则直接放入 accept 队列。

开启 tcp_syncookies 可防范 SYN 攻击。

syn flood

Linux 下默认会进行 5 次重发 SYN-ACK 包,总共耗时大概一分钟。由于 SYN-ACK 超时需要 63 秒,那么就给攻击者一个攻击服务器的机会,攻击者在短时间内发送大量的 SYN 包给服务端,用于耗尽服务端的 SYN 队列。为了应对 SYN 攻击的问题,linux 提供了几个 TCP 参数:tcp_syncookiestcp_synack_retriestcp_max_syn_backlogtcp_abort_on_overflow

Linux 实现了一种称为 SYN cookie 的机制,通过 net.ipv4.tcp_syncookies 控制,设置为 1 表示开启。简单说 SYNcookie 就是将连接信息编码在ISN(initialsequencenumber)中返回给客户端,这时服务端不需要将半连接保存在队列中,而是利用客户端随后发送的 ACK 带回的 ISN 还原连接信息,以完成连接的建立,避免了半连接队列被攻击 SYN 包填满。

SYN 半连接队列的大小是由 /proc/sys/net/ipv4/tcp_max_syn_backlog 这个内核参数控制的,有些内核似乎也受 listen 的 backlog 参数影响,取的是两个值的最小值。当这个队列满了,不开启 syncookies 的时候,服务端会丢弃新来的 SYN 包,而客户端在多次重发 SYN 包得不到响应而返回(connection timeout)错误。但是,当服务端开启了 syncookies=1,那么 SYN 半连接队列就没有逻辑上的最大值了,并且 /proc/sys/net/ipv4/tcp_max_syn_backlog 设置的值也会被忽略。

先来看一个简单的例子:

1
2
3
4
5
6
7
8
9
10
package main

import (
"fmt"
"time"
)

func main() {
fmt.Println(time.Now().Format("2006"))
}

我们会觉得这会输出什么呢?输出的是 2022,也就是当前年份。

另一个例子:

1
2
3
4
5
6
7
8
9
10
package main

import (
"fmt"
"time"
)

func main() {
fmt.Println(time.Now().Format("2006-01-02 15:04:05"))
}

输出:2022-08-04 09:43:56

如果之前写其他语言的,看到这个结果估计和我一样觉得百思不得其解,为什么不是 Y-m-d H:i:s 这种格式呢?而且那个参数看起来就是一个时间戳。

那个参数真的是时间戳吗?

答案我们可以点开 Format 的源码看看就知道了:

1
2
3
4
5
func (t Time) Format(layout string) string {
// ... 省略,下面这个调用才是关键
b = t.AppendFormat(b, layout)
// ... 省略
}

我们看到 Format 里面具体处理逻辑在 AppendFormat 函数里面,再点开 AppendFormat 看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
switch std & stdMask {
case stdYear:
y := year
if y < 0 {
y = -y
}
b = appendInt(b, y%100, 2)
case stdLongYear:
b = appendInt(b, year, 4)
case stdMonth:
b = append(b, month.String()[:3]...)
case stdLongMonth:
m := month.String()
b = append(b, m...)
// ... 省略其他 case
}

我们可以看到里面的 stdYearstdLongYear 之类的常量实际上就是我们传入到 Format 的参数里面的数字。

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
const (
_ = iota
stdLongMonth = iota + stdNeedDate // "January"
stdMonth // "Jan"
stdNumMonth // "1"
stdZeroMonth // "01"
stdLongWeekDay // "Monday"
stdWeekDay // "Mon"
stdDay // "2"
stdUnderDay // "_2"
stdZeroDay // "02"
stdUnderYearDay // "__2"
stdZeroYearDay // "002"
stdHour = iota + stdNeedClock // "15"
stdHour12 // "3"
stdZeroHour12 // "03"
stdMinute // "4"
stdZeroMinute // "04"
stdSecond // "5"
stdZeroSecond // "05"
stdLongYear = iota + stdNeedDate // "2006"
stdYear // "06"
stdPM = iota + stdNeedClock // "PM"
stdpm // "pm"
stdTZ = iota // "MST"
stdISO8601TZ // "Z0700" // prints Z for UTC
stdISO8601SecondsTZ // "Z070000"
stdISO8601ShortTZ // "Z07"
stdISO8601ColonTZ // "Z07:00" // prints Z for UTC
stdISO8601ColonSecondsTZ // "Z07:00:00"
stdNumTZ // "-0700" // always numeric
stdNumSecondsTz // "-070000"
stdNumShortTZ // "-07" // always numeric
stdNumColonTZ // "-07:00" // always numeric
stdNumColonSecondsTZ // "-07:00:00"
stdFracSecond0 // ".0", ".00", ... , trailing zeros included
stdFracSecond9 // ".9", ".99", ..., trailing zeros omitted

stdNeedDate = 1 << 8 // need month, day, year
stdNeedClock = 2 << 8 // need hour, minute, second
stdArgShift = 16 // extra argument in high bits, above low stdArgShift
stdSeparatorShift = 28 // extra argument in high 4 bits for fractional second separators
stdMask = 1<<stdArgShift - 1 // mask out argument
)

所以,其实答案已经有了,我们对照一下我们的参数 2006-01-02 15:04:05,可以很简单在上述常量里面找到对应的常量:

  • 2006 => stdLongYear
  • 01 => stdZeroMonth
  • 02 => stdZeroDay
  • 15 => stdHour
  • 04 => stdZeroMinute
  • 05 => stdZeroSecond

layout 参数里面的 - 以及 : 都会原样输出。

根据给出的这些常量,我们就可以将时间格式化为我们想要的格式了。

cobra 是什么

cobra 的主要功能是创建强大的现代 cli 应用程序。目前市面上许多的著名的 Go 语言开源项目都是使用 Cobra 来构建的,例如:K8s、Hugo、etcd、Docker 等,是非常可靠的一个开源项目。

没有 cobra 之前用什么

如果不用 cobra,我们也可以使用 go 自带的 flag 标准库

flag 的基本用法

下面代码中,我们调用标准库 flagStringVar 方法实现了对命令行参数 name 的解析和绑定,其各个形参的含义分别为命令行标识位的名称、默认值、帮助信息。

命令行参数支持如下三种命令行标志语法:

  • -flag 仅支持布尔类型
  • -flag x 仅支持非布尔类型
  • -flag=x 均支持
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"flag"
"fmt"
)

func main() {
var name string
// 名称为 name、默认值为 "Go go go!"
flag.StringVar(&name, "name", "Go go go!", "帮助信息")
flag.StringVar(&name, "n", "Go go go!", "帮助信息")
flag.Parse()

fmt.Printf("name: %s\n", name)
}

执行:

1
2
3
// 均输出 abc
go run main.go -name=abc
go run main.go -n=abc

子命令实现

在我们日常使用的 CLI 应用中,另一个最常见的功能就是子命令的使用,一个工具它可能包含大量相关联的功能命令以此形成工具集,可以说是刚需,那么这个功能在标准库 flag 中可以如何实现呢,如下述示例:

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
package main

import (
"flag"
"log"
)

var name string

func main() {
flag.Parse()

args := flag.Args()
if len(args) == 0 {
return
}

switch args[0] {
case "go":
goCmd := flag.NewFlagSet("go", flag.ExitOnError)
goCmd.StringVar(&name, "name", "Go 语言", "帮助信息")
_ = goCmd.Parse(args[1:])
case "php":
phpCmd := flag.NewFlagSet("php", flag.ExitOnError)
phpCmd.StringVar(&name, "n", "PHP 语言", "帮助信息")
_ = phpCmd.Parse(args[1:])
}

log.Printf("name: %s", name)
}

在上述代码中,我们首先调用了 flag.Parse 方法,将命令行解析为定义的标志,便于我们后续的参数使用。

另外由于我们需要处理子命令的情况,因此我们调用了 flag.NewFlagSet 方法,该方法会返回带有指定名称和错误处理属性的空命令集给我们去使用,相当于就是创建了一个新的命令集去支持子命令了。

这里需要特别注意的是 flag.NewFlagSet 方法的第二个参数是 ErrorHandling,用于指定处理异常错误,其内置提供以下三种模式:

1
2
3
4
5
6
7
8
const (
// 返回错误描述
ContinueOnError ErrorHandling = iota
// 调用 os.Exit(2) 退出程序
ExitOnError
// 调用 panic 语句抛出错误异常
PanicOnError
)
1
2
3
4
5
6
➜ go run main.go go    
2022/08/04 08:59:21 name: Go 语言
➜ go run main.go php
2022/08/04 09:00:54 name: PHP 语言
➜ go run main.go php -n abc
2022/08/04 09:01:05 name: abc

使用 cobra

安装:

1
go get -u github.com/spf13/cobra

示例:

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
package main

import (
"fmt"
"github.com/spf13/cobra"
"log"
)

func main() {
var wordCmd = &cobra.Command{
Use: "test", // 子命令的命令标识
Short: "测试", // 简短说明
Long: "测试 cobra 子命令", // 完整说明
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(args)
},
}

var rootCmd = &cobra.Command{}
rootCmd.AddCommand(wordCmd)
err := rootCmd.Execute()
if err != nil {
log.Fatalf("cmd.Execute err: %v", err)
}
}

在上面的例子中,我们添加了一个 test 子命令,使用:

1
2
➜  go run main.go test abc
[abc]

这里把 wordCmdrootCmd 都写在一块不是一个好的实践,如果命令多的情况下,这个文件会非常大。

Viper 是适用于 Go 应用程序的完整配置解决方案。

安装

1
go get github.com/spf13/viper

什么是 Viper?

Viper 是适用于 Go 应用程序的完整配置解决方案。它被设计用于在应用程序中工作,并且可以处理所有类型的配置需求和格式。它支持以下特性:

  • 设置默认值
  • 从 JSON、TOML、YAML、HCL、envfile 和 Java properties 格式的配置文件读取配置信息
  • 实时监控和重新读取配置文件(可选)
  • 从环境变量中读取
  • 从远程配置系统(etcd 或 Consul)读取并监控配置变化
  • 从命令行参数读取配置
  • 从 buffer 读取配置
  • 显式配置值

Viper 配置优先级

Viper 会按照下面的优先级。每个项目的优先级都高于它下面的项目:

  • 显式调用 Set 设置值
  • 命令行参数(flag)
  • 环境变量
  • 配置文件
  • key/value 存储
  • 默认值

重要:目前 Viper 配置的键(Key)是大小写不敏感的。

把值存入 Viper

设置默认值

1
viper.SetDefault("ContentDir", "content")

读取配置文件

Viper 可以搜索多个路径,但目前单个 Viper 实例只支持单个配置文件。Viper 不默认任何配置搜索路径,将默认决策留给应用程序。

下面是一个如何使用 Viper 搜索和读取配置文件的示例。不需要任何特定的路径,但是至少应该提供一个配置文件预期出现的路径。

1
2
3
4
5
6
7
8
9
10
viper.SetConfigFile("./config.yaml") // 指定配置文件路径
viper.SetConfigName("config") // 配置文件名称(无扩展名)
viper.SetConfigType("yaml") // 如果配置文件的名称中没有扩展名,则需要配置此项
viper.AddConfigPath("/etc/appname/") // 查找配置文件所在的路径
viper.AddConfigPath("$HOME/.appname") // 多次调用以添加多个搜索路径
viper.AddConfigPath(".") // 还可以在工作目录中查找配置
err := viper.ReadInConfig() // 查找并读取配置文件
if err != nil { // 处理读取配置文件的错误
panic(fmt.Errorf("Fatal error config file: %s \n", err))
}

在加载配置文件出错时,你可以像下面这样处理找不到配置文件的特定情况:

1
2
3
4
5
6
7
8
9
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
// 配置文件未找到错误;如果需要可以忽略
} else {
// 配置文件被找到,但产生了另外的错误
}
}

// 配置文件找到并成功解析

写入配置文件

从配置文件中读取配置是有用的,但是有时候你想要在运行时所做的所有修改。为此,可以使用下面一组命令,每个命令都有自己的用途:

  • WriteConfig - 将当前 的 viper 配置写入预定义的路径并覆盖(如果存在的话)。如果没有预定义的路径,则报错。
  • SafeWriteConfig - 跟 WriteConfig 一样,不过如果文件已经存在则不覆盖。
  • WriteConfigAs - 将当前的 viper 配置写入给定的文件路径。将覆盖给定的文件(如果它存在的话)
  • SafeWriteCofnigAs - 将当前的 viper 配置写入给定的文件路径。不会覆盖已有的文件。
1
2
3
4
5
viper.WriteConfig() // 将当前配置写入“viper.AddConfigPath()”和“viper.SetConfigName”设置的预定义路径
viper.SafeWriteConfig()
viper.WriteConfigAs("/path/to/my/.config")
viper.SafeWriteConfigAs("/path/to/my/.config") // 因为该配置文件写入过,所以会报错
viper.SafeWriteConfigAs("/path/to/my/.other_config")

监控并重新读取配置文件

Viper 支持在运行时实时读取配置文件的功能。

确保在调用 WatchConfig() 之前添加了所有的配置路径。

1
2
3
4
5
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
// 配置文件发生变更之后会调用的回调函数
fmt.Println("Config file changed:", e.Name)
})

从 io.Reader 读取配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
viper.SetConfigType("yaml") // 或者 viper.SetConfigType("YAML")

// 任何需要将此配置添加到程序中的方法。
var yamlExample = []byte(`
Hacker: true
name: steve
hobbies:
- skateboarding
- snowboarding
- go
clothing:
jacket: leather
trousers: denim
age: 35
eyes : brown
beard: true
`)

viper.ReadConfig(bytes.NewBuffer(yamlExample))

viper.Get("name") // 这里会得到 "steve"

覆盖配置

这些可能来自命令行的配置,也可能来自你自己的应用程序逻辑。

1
2
viper.Set("Verbose", true)
viper.Set("LogFile", LogFile)

注册和使用别名

别名允许多个键引用单个值:

1
2
3
4
5
6
7
viper.RegisterAlias("loud", "Verbose")  // 注册别名(此处loud和Verbose建立了别名)

viper.Set("verbose", true) // 结果与下一行相同
viper.Set("loud", true) // 结果与前一行相同

viper.GetBool("loud") // true
viper.GetBool("verbose") // true

使用环境变量

Viper 完全支持环境变量。有五种方法可以使用 ENV 相关特性:

  • AutomaticEnv()
  • BuildEnv(string...): error
  • SetEnvPrefix(string)
  • SetEnvKeyReplacer(string...) *strings.Replacer
  • AllowEmptyEnv(bool)

使用 ENV 变量时,务必意识到 Viper 将 ENV 变量视为区分大小写。

Viper 提供了一种机制来确保 ENV 变量是唯一的。通过使用 SetEnvPrefix,你可以告诉 Viper 在读取环境变量时使用前缀。BindEnvAutomaticEnv 都将使用这个前缀。

1
2
3
4
5
6
SetEnvPrefix("spf") // 将自动转为大写
BindEnv("id")

os.Setenv("SPF_ID", "13") // 通常是在应用程序之外完成的

id := Get("id") // 13

使用 Flags

Viper 具有绑定到标志的能力。

BindEnv 类似,该值不是在调用绑定方法时设置的,而是在访问该方法时设置的。这意味着你可以根据需要尽早进行绑定,即使在 init() 函数中也是如此。

对于单个标志,BindPFlag() 方法提供此功能。

1
2
serverCmd.Flags().Int("port", 1138, "Port to run Application server on")
viper.BindPFlag("port", serverCmd.Flags().Lookup("port"))

从 Viper 获取值

在 Viper 中,有几种方法可以根据值的类型获取值。存在以下功能和方法:

  • Get(key string) : interface{}
  • GetBool(key string) : bool
  • GetFloat64(key string) : float64
  • GetInt(key string) : int
  • GetIntSlice(key string) : []int
  • GetString(key string) : string
  • GetStringMap(key string) : map[string]interface{}
  • GetStringMapString(key string) : map[string]string
  • GetStringSlice(key string) : []string
  • GetTime(key string) : time.Time
  • GetDuration(key string) : time.Duration
  • IsSet(key string) : bool
  • AllSettings() : map[string]interface{}

需要认识到一件重要事情是,每一个 Get 方法在找不到值的时候都会返回零值。为了检查给定的键是否存在,提供了 IsSet() 方法。

访问嵌套的键

访问器方法也接受深度嵌套键的格式化路径。例如,如果加载下面的 JSON 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"host": {
"address": "localhost",
"port": 5799
},
"datastore": {
"metric": {
"host": "127.0.0.1",
"port": 3099
},
"warehouse": {
"host": "198.0.0.1",
"port": 2112
}
}
}

Viper 可以通过传入 . 分隔的路径来访问嵌套字段:

1
GetString("database.metric.host") // 127.0.0.1

提取子树

1
viper.Sub("app.cache")

反序列化

你可以选择将所有或特定的值解析道结构体、map 等。

有两种方法可以做到这一点:

  • Unmarshal(rawVal interface{}) : error
  • UnmarshalKey(key string, rawVal interface{}) : error

示例:

1
2
3
4
5
6
7
8
9
10
11
12
type config struct {
Port int
Name string
PathMap string `mapstructure:"path_map"`
}

var C config

err := viper.Unmarshal(&C)
if err != nil {
t.Fatalf("unable to decode into struct, %v", err)
}

序列化成字符串

你可能需要将 viper 中保存的所有设置序列化到一个字符串中,而不是将它们写入到一个文件中。你可以将自己喜欢的格式的序列化器与 AllSettings() 返回的配置一起使用。