goss: https://github.com/eleven26/goss

背景

一开始写这个东西只是为了写一个命令行工具,拿来将阿里云的 OSS 作为自己的一个临时的云盘,可以通过命令行上传下载一些文件。 当然可能有人会问那么多云盘为什么不用,简单来说就是觉得这些云盘用起来有点麻烦,你要打开网页或者打开客户端,我是大概是一个懒惰的人,只想省去这些繁琐的操作。 另外,这些云盘普遍有个特点就是上传下载速度往往比较慢,但用 OSS 来存储可以有一个较快的下载速度,只是会产生费用。

Goss 的优势

  • 支持常见的对象存储:阿里云 OSS、腾讯云 COS、七牛云 kodo、华为云 OBS。
  • 完善的测试:有完善的单元测试、集成测试。
  • 容易使用的接口:通过 storage.Put 等方式就可以上传下载文件,隐藏了繁琐的细节。
  • 易扩展:实现了 Storage 接口即可。

Goss 提供的接口

goss 目前提供了常用的一些接口:

这些接口覆盖了我的需要了,所以目前就只提供了这些接口;

Put

上传文件到云存储。第一个参数是 key,第二个参数是 io.Reader

1
2
3
data := []byte("this is some data stored as a byte slice in Go Lang!")
r := bytes.NewReader(data)
err := storage.Put("test/test.txt", r)

PutFromFile

上传文件到云存储。第一个参数是 key,第二个参数是本地文件路径。

1
err := storage.PutFromFile("test/test.txt", "/path/to/test.txt")

Get

从云存储获取文件,返回字符串。参数是 key。返回值是 io.ReadClosererror

1
2
3
4
5
6
// rc 是 `io.ReadCloser`
rc, err := storage.Get("test/test.txt")
defer rc.Close()

bs, err := ioutil.ReadAll(rc)
fmt.Println(string(bs))

GetString

从云存储获取文件,返回字符串。参数是 key。返回值是 stringerror

1
2
content, err := storage.GetString("test/test.txt")
fmt.Println(content)

GetBytes

从云存储获取文件,返回字符串。参数是 key。返回值是 []byteerror

1
2
bs, err := storage.Get("test/test.txt")
fmt.Println(string(bs))

GetToFile

下载云存储文件到本地。第一个参数是 key,第二个参数是本地路径。

1
2
// 第一个参数是云端路径,第二个参数是本地路径
err := storage.GetToFile("test/test.txt", "/path/to/local")

Delete

删除云存储文件。

1
err := storage.Delete("test/test.txt")

Exists

判断云存储文件是否存在。

1
exists, err := storage.Exists("test/test.txt")

Files

根据前缀获取文件列表。

1
exists, err := storage.Files("test/")

Size

获取云存储文件大小。

1
size, err := storage.Size("test/test.txt")

goss-cli

正如本文一开始说的,其实一开始是想做一个命令行工具的,最后写成了一个类库。

但是我的命令行工具也是存在的,从 goss 里面拆分出去了,成了 goss-cli

当然功能也是比较简单,但是也足够了,下面是一些示例:

help:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
➜ goss -h
Usage:
[command]

Available Commands:
completion Generate the autocompletion script for the specified shell
debug 调试命令
get 获取指定文件
help Help about any command
list 列出指定目录下的文件
put 上传文件

Flags:
-h, --help help for this command

Use " [command] --help" for more information about a command.

list:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
➜ goss list mac/
Size LastModified Key
0B 2018-06-18 00:13:24 mac/
452.2MB 2020-11-28 06:09:25 mac/Command_Line_Tools_for_Xcode_12.2.dmg
2.0MB 2018-06-18 01:04:39 mac/Go2Shell.dmg
135.3MB 2018-06-18 00:59:00 mac/Navicat Premium 12.0.23.dmg
155.0MB 2020-11-05 00:22:48 mac/Navicat Premium_15.0.20.dmg
1.2GB 2018-06-18 00:34:39 mac/Office 2016 for Mac 多国语言大客户版.dmg
31.0MB 2018-06-18 01:28:06 mac/Sketch49.dmg
16.0MB 2018-06-18 01:28:31 mac/Tunnelblick_3.7.6beta03_build_5031.dmg
69.5MB 2018-06-18 00:55:53 mac/googlechrome-67.0.3396.87.dmg
6.7MB 2018-06-18 01:04:45 mac/iTerm2-3_1_6.zip
13.0MB 2018-06-18 01:04:57 mac/mindnodexx.zip
43.5MB 2018-06-18 01:27:09 mac/privatetunnel-mac-2.8.dmg
459.2MB 2018-12-28 01:03:48 mac/vmware fushion.zip

get:

1
2
3
4
5
➜ goss get mac/Go2Shell.dmg
下载成功!保存路径:"Go2Shell.dmg"

➜ ls -lh Go2Shell.dmg
-rw-r--r-- 1 ruby staff 1.9M Aug 25 09:10 Go2Shell.dmg

注意:下面结果针对5.7版本,无论大表在前还是小表在前,in 的查询效率都要远远高于exists。但在8.0版本中,得益于版本的改善,in 的查询效率 与 exists 几乎没有明显差别。

MySQL 8 里面的优化可参考:Optimizing Subqueries with Materialization

MySQL 中的 in 语句是把外表和内表做 hash 连接,而 exists 语句是对外表做 loop 循环,每次 loop 循环再对内表进行查询。一直大家都认为 existsin 语句的效率要高,这种说法其实是不准确的。需要分不同情况讨论。

exists 对外表用 loop 逐条查询,每次查询都会查看 exists 的条件语句,当 exists 里的条件语句能够返回记录行时(无论记录行有多少,只要能返回),条件就为真,返回当前 loop 到的这条记录,反之如果 exists 里的条件语句不能返回记录行,则当前 loop 到的这条记录被丢弃,exists 的条件就像一个 bool 条件,当能返回结果集则为 true,不能返回结果集则为 false

实例一:exists 子句永远为 true

1
select * from user where exists (select 1);

user 表的记录逐条取出,由于子条件中的 select 1 永远能返回记录行,那么 user 表的所有记录都将被加入结果集,所以与 select * from user; 是一样的。

实例二:exists 子句永远为 false

这里假设 user 表的 userId 都是大于 0 的

1
select * from user where exists (select * from user where userId = 0);

可以知道对 user 表进行 loop 时,检查条件语句 select * from user where userId = 0,由于 userId 永远不为 0,所以条件语句永远返回空集,条件永远为 false,那么 user 表的所有记录都将被丢弃。

not existsexists 相反,也就是当子查询有结果返回时,loop 到的记录将被丢弃,否则将 loop 到的记录加入结果集。

exists 的执行过程

总的来说,如果 A 表有 n 条记录,那么 exists 查询就是将这 n 条记录逐条取出,然后判断 nexists 条件。

in 的实际效果

in 查询相当于多个 or 条件的叠加,这个比较好理解,比如下面的查询:

1
select * from user where userId in (1, 2, 3);

等效于:

1
select * from user where userId = 1 or userId = 2 or userId = 3;

not inin 相反,如下:

1
select * from user where userId not in (1, 2, 3);

等效于:

1
select * from user where userId != 1 and userId != 2 and userId != 3;

in 的执行过程

总的来说,in 查询就是先将子查询结果集拿出来,假设结果集为 B,共有 m 条记录,然后在将子查询条件的结果集分解成 m 个,再进行 m 次查询。

值得一提的是,in 查询的子条件返回结果必须只有一个字段,例如:

1
select * from user where userId in (select id from B);

而不能是:

1
select * from user where userId in (select id, age from B);

而 exists 就没有这个限制。

exists 和 in 的性能

考虑如下SQL语句

  1. select * from A where exists (select * from B where B.id = A.id);

  2. select * from A where A.id in (select id from B);

查询 1 可以转化为以下伪代码,便于理解:

1
2
3
4
5
6
7
8
9
10
$result = [];

for ($i = 0; $i < count(A); $i++) {
  $a = get_record(A, $i); // 从A表逐条获取记录

  if (B.id = $a[id]) // 如果子条件成立
    $result[] = $a;
}

return $result;

查询 1 主要是用到了 B 表的索引,A 表如何对查询的效率的影响应该不大。

假设B表的所有id为1,2,3,查询2可以转换为:

1
select * from A where A.id = 1 or A.id = 2 or A.id = 3;

这个就好理解了,这里主要是用到了 A 的索引,B 表如何对查询影响不大。

not exists 和 not in

  1. select * from A where not exists (select * from B where B.id = A.id);

  2. select * from A where A.id not in (select id from B);

查询 1 还是和上面一样,用了 B 的索引。

而对于查询 2,可以转化为如下语句:

1
select * from A where A.id != 1 and A.id != 2 and A.id != 3;

可以知道 not in 是个范围查询,这种 != 的范围查询无法使用任何索引,等于说 A 表的每条记录,都要在 B 表里遍历一次,查看 B 表里是否存在这条记录。

not existsnot in 效率高。

mysql 中的 in 语句是把外表和内表作 hash 连接,而 exists 语句是对外表作 loop 循环,每次 loop 循环再对内表进行查询。一直大家都认为 exists 比 in 语句的效率要高,这种说法其实是不准确的。这个是要区分环境的。

如果查询的两个表大小相当,那么用 in 和 exists 差别不大

如果两个表中一个较小,一个是大表,则子查询表大的用 exists,子查询表小的用 in:

例如,表 A(小表),表 B(大表)

  1. select * from A where cc in (select cc from B) 效率低,用到了 A 表上 cc 列的索引;

select * from A where exists(select cc from B where cc=A.cc) 效率高,用到了 B 表上 cc 列的索引。

相反的

  1. select * from B where cc in (select cc from A) 效率高,用到了 B 表上 cc 列的索引;

select * from B where exists(select cc from A where cc=B.cc) 效率低,用到了 A 表上 cc 列的索引。

not innot exists 如果查询语句使用了 not in 那么内外表都进行全表扫描,没有用到索引;而 not exists 的子查询依然能用到表上的索引。

所以无论那个表大,用 not exists 都比 not in 要快。

copy

otiai10/copy 是一个可以让你递归地复制目录的库。

安装

1
go get github.com/otiai10/copy

示例

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

import (
"fmt"
cp "github.com/otiai10/copy"
)

func main() {
err := cp.Copy("your/src", "your/dest")
fmt.Println(err) // nil
}

我们可以通过 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 设置的值也会被忽略。

0%