流批一体数据平台及一站式机器学习平台

参考

go usage

[TOC]

说明

  • 如果一个对象的字段使用指针的方式引用了另外一个对象的字段,那么备用的变量会增加引用次数,且从栈迁移至堆,不会出现垃圾回收导致内存非法访问问题

time.Duration

1
2
3
4
d := time.Duration(10)
d := 10 * time.Nanosecond
// d := rand.Intn(100) * time.Nanosecond ERROR
d := time.Duration(rand.Intn(100)) * time.Nanosecond

打印

打印数组

1
fmt.Printf("%v", []float64{0.1, 0.5, 0.99, 0.999})

network

发送请求

1
2
3
4
urlBaiduFanyiSug := "https://fanyi.baidu.com/sug"
data := map[string]string{"kw": "hi"}
js, _ := json.Marshal(data)
res, err := http.Post(urlBaiduFanyiSug, "application/json", bytes.NewBuffer(js))

[]byte 和 string

1
2
3
4
5
// []byte -> string; bys: []byte
s := string(bys)

// string -> []byte
bys := []byte(s)

依赖

1
2
3
4
5
6
$ go mod tidy # 整理依赖, 下载没有下载的, 移除没有的
$ go mod download # 下载依赖
$ go get # 更新依赖

# 添加依赖到 go.mod
$ go get -v -t ./...

git 仓库使用 ssh 认证

1
2
3
4
5
6
git config --global --add url."git@your-repo.com:".insteadOf "https://your-repo.com/"
# 通过 ~/.gitconfig 查看配置,如果还是有问题可检查下配置

go env -w GO111MODULE=on
go env -w GOPROXY=direct
go env -w GOSUMDB=off

查看依赖版本

1
go list -m -versions git.*.com/org/repo

添加依赖

1
2
3
go get git.*.com/org/repo

go mod edit -require git.*.com/org/repo@version

GC

阶段

阶段 STW(STOP THE WORLD)
STW sweep termination YES
concurrent mark and scan NO
STW mark termination YES

gc log

1
2
3
4
5
6
GODEBUG=gctrace=1 ./<program> <parameters>
# GODEBUG 多值
GODEBUG=gctrace=1,schedtrace=1000./<program> <parameters>

gctrace=1 : 打印 gc 日志
schedtrace=1000 : 每 1000 ms 打印一次调度器的摘要信息

格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
gctrace: setting gctrace=1 causes the garbage collector to emit a single line to standard
error at each collection, summarizing the amount of memory collected and the
length of the pause. Setting gctrace=2 emits the same summary but also
repeats each collection. The format of this line is subject to change.
Currently, it is:
gc # @#s #%: #+#+# ms clock, #+#/#/#+# ms cpu, #->#-># MB, # MB goal, # P
where the fields are as follows:
gc # the GC number, incremented at each GC
@#s time in seconds since program start
#% percentage of time spent in GC since program start
#+...+# wall-clock/CPU times for the phases of the GC
#->#-># MB heap size at GC start, at GC end, and live heap
# MB goal goal heap size
# P number of processors used
The phases are stop-the-world (STW) sweep termination, concurrent
mark and scan, and STW mark termination. The CPU times
for mark/scan are broken down in to assist time (GC performed in
line with allocation), background GC time, and idle GC time.
If the line ends with "(forced)", this GC was forced by a
runtime.GC() call and all phases are STW.
1
gc # @#s #%: #+#+# ms clock, #+#/#/#+# ms cpu, #->#-># MB, # MB goal, # P
  • gc # :编号
  • @#s :自程序启动到打印 gc 日志的时间
  • #%:自程序启动,gc 花费的时间占比
  • #+#+# ms clock:垃圾回收时间,(sweep)+(mark & scan)+(termination)
  • #+#/#/#+# ms cpu:垃圾回收占用的 CPU 时间 (sweep)+(mark & scan (辅助时间/后台gc时间/空闲时间))+(termination)
  • #->#-># MB:gc 开始时堆大小、gc 结束时堆大小、存活堆大小
  • #MB goal :全局堆大小
  • #P :使用的处理器数量

ms cpu 约等于 cpu_num * ms clock

示例

1
2
3
gc 35 @1130.489s 1%: 0.71+3290+0.12 ms clock, 5.7+5932/26084/4619+1.0 ms cpu, 35956->37042->8445 MB, 37411 MB goal, 32 P

1. 第 35 次 gc,距离程序启动 1130s,自程序启动 gc 时间占比 1%

参考

Profile

1
2
3
4
5
curl http://127.0.0.1:12067/debug/pprof/profile > profile.dat
go tool pprof -http=:8081 ~/profile.dat

curl http://127.0.0.1:12067/debug/pprof/heap > heap.dat
go tool pprof -alloc_space -http=:8082 ~/heap.at

类型转换

String 转数字

1
2
3
4
5
6
7
# string -> int32
if tr, err := strconv.ParseInt("9207", 10, 32); err != nil {
res = int32(tr)
}

# string -> int64
res, err := strconv.ParseInt("9207", 10, 64)

问题

GLIBC_2.32' not found

1
2
3
4
5
# 错误
./main: /lib64/libc.so.6: version `GLIBC_2.32' not found (required by ./main)

# 解决: 编译的时候 CGO_ENABLED 设置为 0
CGO_ENABLED=0 go build

redis benchmark

工具

针对 redis 的 benchmark,可使用 redis 自带的 redis-benchmark 工具。

redis-benchmark

命令及参数

1
2
# 命令
$ redis-benchmark [option] [option value]

参数

参数 说明 默认值 示例
-h 主机 127.0.0.1
-p 端口 6379
-a 密码
-s 指定服务器socket
-c 客户端并发连接数 50
-n 请求数 10000
-d 以字节的形式指定 SET/GET 值的数据大小 2
-k 连接保持,1=keep alive 0=reconnect 1
-r SET/GET/INCR 使用随机 key, SADD 使用随机值
-P 通过管道传输 <numreq> 请求 1
-q 强制退出 redis,仅显示 query/sec 值
–csv 以 CSV 格式输出
-l 生成循环,永久执行测试
-t 仅运行以逗号分隔的测试命令列表 set,lpush
-l Idle 模式,仅打开 N 个 idle 连接并等待

示例

1
$ redis-benchmark -h <host> -p <port> -t set -n 100000 -d 1024 -a <password>

consul trouble shooting

一次服务无法注册的故障

现象

前端

我们的 consul dashboard(基于 consul ui 源码,新增了权限控制,独立部署)突然出现 504 异常。

image-20210315222804326

打开浏览器调试器的 network,查看异常的请求。

image-20210315223014936

/v1/* 接口,向部署的node应用发送请求,node 应用再将请求通过插件( express-http-proxy )代理转发至 consul server。

很奇怪,其他所有机房都没有问题。而且,多刷几次,偶尔可以正常响应。

服务

这一台机器的 Java 服务报了奇怪的 \n not found: limit=0 content=�~@� 异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[ERROR 2021-03-15 20:21:39.114] com...init(ConsulBase.java:58) [[consul] init consul base failed, e=[com.orbitz.consul.ConsulException: Error connecting to Consul
at com.orbitz.consul.AgentClient.ping(AgentClient.java:69)
at com.orbitz.consul.Consul$Builder.build(Consul.java:708)
...
Caused by: java.io.IOException: unexpected end of stream on http://127.0.0.1:8500/...
at okhttp3.internal.http1.Http1ExchangeCodec.readResponseHeaders(Http1ExchangeCodec.kt:205)
at okhttp3.internal.connection.Exchange.readResponseHeaders(Exchange.kt:105)
...
at okhttp3.RealCall.getResponseWithInterceptorChain(RealCall.kt:184)
at okhttp3.RealCall.execute(RealCall.kt:66)
at retrofit2.OkHttpCall.execute(OkHttpCall.java:186)
at com.orbitz.consul.AgentClient.ping(AgentClient.java:62)
... 14 more
Caused by: java.io.EOFException: \n not found: limit=0 content=�~@�
at okio.RealBufferedSource.readUtf8LineStrict(RealBufferedSource.kt:231)
at okhttp3.internal.http1.Http1ExchangeCodec.readHeaderLine(Http1ExchangeCodec.kt:210)
at okhttp3.internal.http1.Http1ExchangeCodec.readResponseHeaders(Http1ExchangeCodec.kt:181)
... 35 more

看样子,是无法连接consul,接口异常?此外,由于无法连接consol,报了一堆无法找到服务端的RPC异常,这也正常,注册中心连不上,肯定也就拿不到服务列表。

我们的 C++ 服务( 使用库ppconsul ),直接报了无法注册的异常。

1
E0315 19:28:54.129444 11661 ...cpp:456] [consul] register local consul agent failed!

解决

首先,尝试重启 consul server。不重启还好,一重启,这台机器之前注册的服务全没了,怎么重启服务都注册不上。

image-20210315224053342

随后,查看consul日志,伴随着不太明了的异常。

image-20210315224235590

于是,想通过consul程序,看下members,确认加入了集群,但是…

image-20210315224420906

他,出错了,很奇怪的错。

1
Error retrieving members: Get "http://127.0.0.1:8500/v1/agent/members?segment=_all": EOF

到这里,可以想到的是,是consul接口出问题。开始的时候前端请求异常,应该也是因为代理请求consul接口,出错导致的。凡事都有个但是,排查端口发现,有正常的连接建立啊。

image-20210315225135881

而且呢,还很多。

image-20210315225258268

等等… 难道… 是因为… 太多了?赶紧搜下。

image-20210315225438513

好吧,找到了这里。

image-20210315225530866

单个 IP 限制的最大连接数,是 200。行吧。改大试试吧。

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
{
"client_addr": "0.0.0.0",
"datacenter": "...",
"data_dir": "...",
"domain": "...",
"dns_config": {
"enable_truncate": true,
"only_passing": true
},
"limits": {
"http_max_conns_per_client": 500
},
"enable_syslog": true,
"encrypt": "...",
"leave_on_terminate": true,
"log_level": "INFO",
"rejoin_after_leave": true,
"server": true,
"bootstrap": false,
"bootstrap_expect": 3,
"retry_join": [
"...",
"...",
"...",
"...",
"...",
],
"ui": false
}

至此,好了。

启动失败

Failed to send gossip … invalid argument

错误信息

1
2
2023/11/10 14:48:23 [ERR] memberlist: Failed to send gossip to 172.31.153.158:9301: write udp 127.0.0.1:8301->192.168.6.8:9301: sendto: invalid argument
...

解决

修改绑定地址 -bind=192.168.6.5

1
$ consul agent -config-dir=/etc/consul.d/ -data-dir=/var/lib/consul/ -bind=192.168.6.5

计算机网络

计算机网络的层次结构

OSI 七层模型

功能 常见协议
应用层(Application Layer) 应用层提供网络服务和用户应用程序的接口。它定义了各种应用协议,如HTTP、FTP、SMTP等,以满足特定应用需求 HTTP、HTTPS、FTP、SMTP、POP、IMAP、DNS、DHCP、Telnet
表示层(Presentation Layer) 表示层负责处理数据的表示和格式转换,以确保在不同系统间的互操作性。它处理数据的压缩、加密、解密和格式转换等任务
会话层(Session Layer) 会话层管理不同应用程序之间的通信会话,建立、维护和终止会话连接。它提供会话控制和同步功能,支持多个会话和数据交换
传输层(Transport Layer) 传输层提供可靠的端到端数据传输服务,确保数据的完整性和顺序。它定义了传输协议(如TCP和UDP),并处理数据分段、流量控制和错误恢复 TCP、UDP
网络层(Network Layer) 网络层负责在不同网络之间进行数据包的路由和转发。它为数据包选择最佳路径,并处理跨网络的寻址拥塞控制 IP、ICMP
数据链路层(Data Link Layer) 数据链路层管理通过物理层建立的点对点链接或局域网,确保可靠的数据传输。它处理帧的组装错误检测校正,并提供对物理层的透明访问 Ethernet、ARP
物理层(Physical Layer) 物理层处理物理连接以及数据传输的物理介质,如电缆、光纤等。它负责为传输比特流提供传输媒介和基本的电气和物理特性

TCP/IP 四层模型

功能
应用层(Application Layer) 应用层是用户与网络进行交互的接口。它包含众多的协议和应用程序,用于实现各种网络服务和应用需求。常见的应用层协议有HTTP(超文本传输协议)、FTP(文件传输协议)、SMTP(简单邮件传输协议)等
传输层(Transport Layer) 传输层提供端到端的数据传输服务。它定义了两个主要的传输协议:TCP(Transmission Control Protocol)和UDP(User Datagram Protocol)。TCP提供可靠的、面向连接的数据传输,而UDP提供无连接的、不可靠的数据传输。该层还处理数据分段、流量控制和错误恢复等任务
网络层(Internet Layer) 网络层实现了IP协议,负责在不同网络之间进行数据包的路由和转发。它使用IP地址标识主机和网络,并通过路由选择算法确定最佳路径。主要协议包括IP(Internet Protocol)、ICMP(Internet Control Message Protocol)和ARP(Address Resolution Protocol)
网络接口层(Network Interface Layer) 也称为数据链路层或物理层。该层处理与物理网络介质的直接交互,例如以太网、Wi-Fi等。它负责将数据包封装为帧,并进行物理传输

常见概念

万维网、互联网和以太网的区别

互联网(Internet)是一个概念,是广域网的一种(WAN,Wide Area Network),在全世界被广泛使用。互联网通过 TCP/IP 协议,把全球范围内的局域网连接在一起,可以实现点对点的网络通讯。

以太网(Ethernet)是一种常见的局域网(LAN,Local Area Network)技术,工作在物理层和数据链路层。在物理层,它定义了电气特性、信号传输速率和距离限制等规范。在数据链路层,以太网定义了帧格式、帧起始和结束标记、帧同步和帧检验序列等。

从网络模型来看,互联网是在网络层及之上构建的互联网络,而以太网则是工作在数据链路层及之下的一种技术。互联网是被人普遍认为的网络,而以太网则是被广泛使用的构建局域网的技术。

万维网(World Wide Web)是一种基于英特网的信息系统,通过超文本链接的方式来组织、检索和共享文档和资源。在万维网上,不同的文档和资源通过统一的标识符(URL,统一资源定位符)进行唯一标识。万维网的核心技术是HTTP(Hypertext Transfer Protocol),它定义了在网络上传输超文本资源的规范。

MAC 地址

MAC地址(Media Access Control Address)是网络设备(如网卡、无线适配器)在出厂时固定分配的唯一标识符。它是一个由12个十六进制数(0-9,A-F)组成的字符串,通常以冒号或短横线分隔。MAC地址由两部分组成:前6个十六进制数表示厂商代码(OUI,Organizationally Unique Identifier),用于标识设备制造商;后面的6个十六进制数是设备的序列号,由制造商分配。

MAC地址是在数据链路层上使用的,用于局域网内设备之间的直接通信。当设备在局域网上发送数据时,目标设备的MAC地址用于将数据帧传送到正确的设备。

需要注意的是,MAC地址只在局域网内部有效,不会跨越路由器传输。在互联网上进行通信时,数据包会根据目标IP地址进行路由,而不是MAC地址。

常见网络工具

ping

用于判断网络联通状态。

1
2
3
4
# ipv4 地址
ping 10.0.10.1
# ipv6 地址
ping6 2001:4860:4860::8888

注意:

  • 没有收到消息回复,不一定是网络不通,也有可能是目标设备关闭了 ping 请求相应

telnet

可以远程连接开启 telnet 服务器的目标设备,也可以判断目标设备的某个端口的连通性。

1
telnet 10.0.10.1 8080

TCP/IP

建立/断开连接

TCP状态转换图

建立连接(三次握手)

  • 客户端发送一个带SYN标志的TCP报文到服务器(报文1)
  • 服务器端回应客户端(报文2), 这个报文同时带ACK标志和SYN标志. 因此, 它表示对刚才客户端SYN报文的回应, 同时又发送标志SYN给客户端, 询问客户端是否准备好进行数据通讯
  • 客户端必须再次回应服务端一个ACK报文(报文3)

FLAGS 含义
SYN 建立连接
FIN 关闭连接
ACK 响应
PSH 有数据传输
RST 重置连接
首部字段 说明
seq 序列号
ack 对方需要发送的下一个报文序号

再述三次握手。

  • 客户端想要建立连接,SYN 标志置为 1,序列号每次都有,所以将首部的 seq 设置为一个随机值 i
  • 服务端确认建立连接,SYN 标志置为 1,作为响应报文,ACK 标志置为 1,同样设置首部的 seq 为一个随机值 j,同时设置 ack 指明对方下一次需要发送的报文序列为 i+1
  • 客户端接收到服务端的连接请求,发送确认消息,作为响应报文,ACK 标志置为 1,同时将报文序列seq置为 i+1,请求对方下一次发送序列号为 j+1 的报文,ack 置为 j+1

终止连接(四次握手)

由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。当一方完成它的数据发送后发送一个FIN来终止这个方向的连接,另一端收到FIN后,仍可以发送数据。首先进行关闭的一方执行主动关闭,另一方执行被动关闭。

  • TCP客户端发送一个FIN, 用来关闭客户端到服务端的数据传送
  • 服务端收到FIN, 发回一个ACK, 确认序号为收到的序号加一. 和SYN一样, 一个FIN占用一个序号
  • 服务端关闭客户端的连接, 发送一个FIN给客户端
  • 客户端发回ACK报文确认, 并将确认序号设置为收到的序号加一

再述四次握手,终止连接。

终止连接,需要双方都发送终止请求,并回复对方。

  • 客户端主动关闭,置 FIN 为 1,序列号seq置为u
  • 服务端被动关闭,回复客户端的关闭请求,因为是响应报文,置ACK为 1,指明对方下一个报文序列,ack 置为 u+1
  • 服务端开始主动关闭,置 FIN 为 1,序列号 seq 置为 w,指明对方下一个报文序列,ack 置为 u + 1
  • 客户端被动关闭,恢复服务端的关闭请求,因为是响应报文,置ACK为1,返回对方想要的报文序列,置 seq 为 u+1,指明对方下一个报文序列,ack 置为 w+1

状态

状态 说明 备注
CLOSED 初始状态
LISTEN 监听状态, 可以接受连接
SYN_RCVD 接受到SYN报文 正常情况下, 这个状态是服务端的SOCKET在建立TCP连接时的三次握手会话过程中的一个中间状态. 在这种状态时, 当收到客户端的ACK报文后, 会进入ESTABLISHED状态
SYN_SEND 客户端已发送SYN报文 SYN_RCVD呼应, 当客户端SOCKET执行CONNECT连接时, 首先发送SYN报文, 随即进入SYN_SEND状态, 并等待服务端发送三次握手中的第二个报文
ESTABLISHED 连接已建立
FIN_WAIT_1 等待对方FIN报文 FIN_WAIT_1和FIN_WAIT_2都表示等待FIN报文. FIN_WAIT_1状态实际上是当SOCKET在ESTABLISHED状态时,它想主动关闭连接,向对方发送了FIN报文,此时该SOCKET进入到FIN_WAIT_1状态, 而当对方回应ACK报文后,则进入到FIN_WAIT_2状态.
FIN_WAIT_2 等待对方FIN报文 FIN_WAIT_2状态下的SOCKET,表示半连接,也即有一方要求close连接,但另外还告诉对方,我暂时还有点数据需要传送给你,稍后再关闭连接
TIME_WAIT 收到对方FIN报文, 并发送了ACK报文 等2MSL后即可回到CLOSED可用状态了。如果FIN_WAIT_1状态下,收到了对方同时带 FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态
CLOSING 表示发送FIN报文后, 没有收到对方的ACK报文, 却收到了对方FIN报文 如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报 文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接
CLOSE_WAIT 等待关闭 收到对方FIN报文, 并回应了ACK报文, 进入到CLOSE_WAIT状态. 在CLOSE_WAIT状态下,需要完成的事情是等待关闭连接.
LAST_ACK 被动关闭一方在发送FIN报文后, 等待对方的ACK报文 当收到ACK报文后, 进入CLOSED状态

同时关闭/打开连接

问题

为什么建立连接是三次握手, 关闭连接是四次握手?

这是因为,服务端的LISTEN状态下的SOCKET当收到SYN报文的连接请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。

TIME_WAIT后还需要SMSL

  • TCP协议在关闭连接的四次握手过程中,最终的ACK是由主动关闭连接的一端(后面统称A端)发出的,如果这个ACK丢失,对方(后面统称B端)将重发出最终的FIN,因此A端必须维护状态信息(TIME_WAIT)允许它重发最终的ACK。如果A端不维持TIME_WAIT状态,而是处于CLOSED 状态,那么A端将响应RST分节,B端收到后将此分节解释成一个错误(在java中会抛出connection reset的SocketException)。

    因而,要实现TCP全双工连接的正常终止,必须处理终止过程中四个分节任何一个分节的丢失情况,主动关闭连接的A端必须维持TIME_WAIT状态 。

  • 允许老的重复分节在网络中消逝(实际也就是避免同一端口对应多个套接字). TCP分节可能由于路由器异常而“迷途”,在迷途期间,TCP发送端可能因确认超时而重发这个分节,迷途的分节在路由器修复后也会被送到最终目的地,这个迟到的迷途分节到达时可能会引起问题。在关闭“前一个连接”之后,马上又重新建立起一个相同的IP和端口之间的“新连接”,“前一个连接”的迷途重复分组在“前一个连接”终止后到达,而被“新连接”收到了。为了避免这个情况,TCP协议不允许处于TIME_WAIT状态的连接启动一个新的可用连接,因为TIME_WAIT状态持续2MSL,就可以保证当成功建立一个新TCP连接的时候,来自旧连接重复分组已经在网络中消逝。

关闭TCP连接一定需要四次握手吗

不一定,4次挥手关闭TCP连接是最安全的做法。但在有些时候,我们不喜欢TIME_WAIT状态(如当MSL数值设置过大导致服务器端有太多TIME_WAIT状态的TCP连接,减少这些条目数可以更快地关闭连接,为新连接释放更多资源),这时我们可以通过设置SOCKET变量的SO_LINGER标志来避免SOCKET在close()之后进入TIME_WAIT状态,这时将通过发送RST强制终止TCP连接(取代正常的TCP四次握手的终止方式)。但这并不是一个很好的主意,TIME_WAIT 对于我们来说往往是有利的。

TCP如何保证可靠传输

  • 序列号、确认和重传:每条报文指定一个序列号,接收方收到报文会发送确认,如果发送方迟迟未收到确认,就重传
  • 数据校验
  • 连接管理
  • 窗口控制
  • 流量控制
  • 拥塞控制

参考

Nagle算法

规则

  • 如果包长度达到MSS, 则允许发送
  • 如果包还有FIN, 则允许发送
  • 设置了TCP_NODELAY选项, 则允许发送
  • 未设置TCP_CORK选项时, 若所有发出去的小数据包(包长度小于MSS)均被确认, 则允许发送;
  • 上述条件都未满足, 但发生了超时(一般为200ms), 则立即发送

其他说明

  • Nagle算法只允许一个未被ACK的包存在于网络,它并不管包的大小,因此它事实上就是一个扩展的停-等协议,只不过它是基于包停-等的,而不是基于字节停-等的
  • Nagle算法完全由TCP协议的ACK机制决定,这会带来一些问题,比如如果对端ACK回复很快的话,Nagle事实上不会拼接太多的数据包,虽然避免了网络拥塞,网络总体的利用率依然很低
  • Nagle算法是silly window syndrome(SWS)预防算法的一个半集。SWS算法预防发送少量的数据,Nagle算法是其在发送方的实现,而接收方要做的是不要通告缓冲空间的很小增长,不通知小窗口,除非缓冲区空间有显著的增长。这里显著的增长定义为完全大小的段(MSS)或增长到大于最大窗口的一半。
  • 注意:BSD的实现是允许在空闲链接上发送大的写操作剩下的最后的小段,也就是说,当超过1个MSS数据发送时,内核先依次发送完n个MSS的数据包,然后再发送尾部的小数据包,其间不再延时等待。(假设网络不阻塞且接收窗口足够大)
  • 当有一个TCP数据段不足MSS,比如要发送700Byte数据,MSS为1460Byte的情况。nagle算法会延迟这个数据段的发送,等待,直到有足够的数据填充成一个完整数据段
  • 为了解决大量的小报文对通信造成的影响,提高传输效率

参考

其他

  • ACK: 应答作用
  • SYN: 同步作用

I/O

阻塞 I/O

通常I/O操作都是 阻塞I/O,以一次读操作为例,数据先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间,对应数据准备、数据拷贝两个阶段。在数据准备阶段,线程/进程会被挂起,当数据拷贝至应用程序内存空间后,线程/进程被唤醒。

非阻塞 I/O

如果使用阻塞I/O,同时处理1000个请求,则需要1000个线程,虽然大多数线程可能被挂起,但不可避免的是频繁的线程上下文切换,非常占用系统资源,每个线程的时间槽也会非常短,导致系统整体有效负载降低,此外每个线程还会占用 512KB / 1MB 的内存。

此时,引入了 非阻塞I/O 的概念,通过调用 fcntl(POSIX)/ ioctl(Unix)设为非阻塞模式。在此模式下,如果有数据收到,就会返回数据,否则立即返回一个错误。这样虽然不会阻塞线程,但需要线程不断轮询来读取或写入。

I/O 多路复用

为了解决单线程轮询单个文件描述符效率低下的问题,有了 I/O多路复用 的概念,多路是指多个文件描述符(socket连接),复用指的是复用一个线程。

所以,I/O多路复用是指,使用一个线程来检查多个文件描述符的就绪状态,如调用 select / poll 函数,传入多个文件描述符,如果有一个就绪,则返回,否则阻塞至超时。

这样在处理1000个连接时,只需要1个线程监控就绪状态,对就绪的每个连接开一个线程处理就可以了,这样需要的线程数大大减少,减少了内存开销和上下文切换的CPU开销。

I/O多路复用(多路是指多个文件描述符(fd,file descriptor,socket 是fd的一种),复用指的是复用一个线程),主要技术有 select、poll、epoll。

- select poll epoll
时间处理复杂度 O(n) O(n) O(1)
连接数 需要通过 FD_SETSIZE 设置最大连接数 无限制
事件触发机制 轮询 轮询 回调
工作模式 LT LT LT / ET
fd 拷贝 每次调用,每次拷贝 每次调用,每次拷贝 通过 mmap 内存映射,避免内存拷贝

select

select 会将全量fd_set从用户空间拷贝到内核空间,并注册回调函数, 在内核态空间来判断每个请求是否准备好数据 。select在没有查询到有文件描述符就绪的情况下,将一直阻塞(select是一个阻塞函数)。如果有一个或者多个描述符就绪,那么select将就绪的文件描述符置位,然后select返回。返回后,由程序遍历查看哪个请求有数据。

缺陷

  • 牵扯到两次内存拷贝,第一次将 fd_set 从用户空间拷贝至内核空间;第二次是将 fd_set 从内核空间拷贝至用户空间
  • 牵扯到两次集合遍历,都是对 fd_set 遍历,第一次发生在内核态,第二次发生在用户态
  • select 文件描述符有上限,及 fd_set 的容量,虽然可以通过宏 FD_SETSIZE 来设置

poll

poll的实现和select非常相似,只是描述fd集合的方式不同,poll使用pollfd结构代替select的fd_set(网上讲:类似于位图)结构,其他的本质上都差不多。所以Poll机制突破了Select机制中的文件描述符数量最大为1024的限制

缺陷

  • 同样会有两次遍历和内存拷贝

epoll

epoll 对 select 的三个缺陷进行了修复。

解决内存拷贝:epoll 方式下,fd 是通过mmap共享内存的方式,避免在用户空间和内核空间的拷贝。

解决多次遍历:epoll不像select或poll一样每次都把当前线程轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把当前线程挂一遍(这一遍必不可少),并为每个fd指定一个回调函数。当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表。那么当我们调用epoll_wait时,epoll_wait只需要检查链表中是否有存在就绪的fd即可,效率非常可观

解决fd 数量限制:pollfd 结构代替 fd_set 解决数量限制, 使用双向链表保存就绪状态的fd。

工作模式

epoll 分为两种工作模式,LT(level trigger,水平触发) 和 ET(edge trigger,边缘触发),两种模式的区别是在于对就绪fd的处理。

  • LT模式: 默认的工作模式,即当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序可以不立即处理该事件;事件会被放回到就绪链表中,下次调用epoll_wait时,会再次通知此事件。
  • ET模式: 当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应并通知此事件。

异步 I/O

进程 / 线程在发起IO操作之后,内核会立刻返回,不会阻塞用户线程/进程。当内核把数据准备完毕后,向用户进程发送一个signal通知。

参考

netty

简介

简单的说,netty 提供了一个NIO(异步非阻塞IO)网络框架,基于此可以轻松地开发高性能的网络应用。

优点

  • 支持多种协议,且有统一的API
  • 强大的线程模型
  • 高性能

常见问题

reactor

reactor 模式是事件驱动的一种实现,将服务端接受请求和事件处理分离,从而提升系统处理并发的能力,java的NIO的reactor模式是基于系统内核的多路复用技术实现的。

reactor模式有三个组件,Handler、Demultiplexer、Reactor,核心是 IO复用 + 非阻塞编程 + bind/function。

reactor 和 I/O多路复用

reactor 是一种网络编程模式,其底层多使用系统实现的I/O多路复用能力。

高性能原因

  • 基于Reactor模式实现的 NIO(异步阻塞IO),IO多路复用
  • TCP接受和发送缓冲区使用直接内存代替堆内存,避免了内存复制
  • 使用内存池的方式,循环利用 ByteBuf
  • 环形数组缓冲区实现无锁化并发编程
  • 关键资源的处理使用单线程串行化,避免使用锁

线程模型

在 Netty 主要靠 NioEventLoopGroup 线程池来实现具体的线程模型的 。我们实现服务端的时候,一般会初始化两个线程组,Boss Group 和 Worker Group,前者用于接收连接,后者负责具体的处理,交由对应的 Handler 处理。

Netty 可以实现多种线程模型,单线程、多线程、主从多线程模型。

启动流程

  • 创建两组线程组,Boss 和 Worker
  • 创建服务端启动引导/辅助类 ServerBootstrap
  • 为引导类设置线程组
  • 绑定端口,得到 ChannelFuture 对象
  • 阻塞等待 Future 对象,知道服务器 Channel 关闭
  • 关闭线程组

默认线程数量

CPU核心数 * 2。

处理流程

BIO / NIO / AIO

  • BIO:阻塞IO
  • NIO:非阻塞IO
  • AIO:异步非阻塞IO

netty 中的 selector

Selector 能够检测多个注册的通道上是否有事件发生(多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。

netty 的核心组件

  • Channel
    • Netty 的网络操作抽象类,包含了基本的I/O操作,bind、connect、read、write 等,降低了直接使用 Socket 的复杂度
    • 与 EventLoop 一起处理I / O
  • ChannelHandler
    • 用于处理各种事件,以支持各种协议和处理数据的方式
    • 比如,连接、数据接收、异常、数据转换等
  • ChannelPipeline
    • 提供了 ChannelHandler 链的容器
  • EventLoop
    • EventLoop 定义了Netty的核心抽象,用于处理连接的生命周期中所发生的事件
    • 负责监听网络事件,并调用事件处理器进行相关的 I/O 操作的处理

Channel 和 EventLoop 联系

Channel 是为 Netty 网络操作的抽象类,EventLoop 负责处理注册到其上的Channel,处理I/O操作,两者配合完成I/O操作

EventloopGroup 和 EventLoop 关系

EventLoopGroup 是一组 EventLoop 的抽象,提供了 next 接口,从一组 EventLoop 中按规则选择一个 EventLoop 来处理任务。

BootStrap 和 ServerBootStrap

BootStrap 通过 connect 方法连接远程主机,作为TCP协议的客户端,或通过bind方法绑定本地端口,作为UDP协议的一端。只需配置一组 EventLoopGroup。

ServerBootStrap 通过 bind 方法绑定本地端口,等待客户端连接。需配置两组 EventLoopGroup ,一个用于接收,一个用于处理,即 Boss EventLoopGroup 和 Worker EventLoopGroup。

应用场景

  • RPC框架的网络通讯工具
  • 搭建网络应用,http 或其他

参考

redis

[toc]

简述

架构

redis的部署模式分为集群模式和哨兵模式,哨兵模式下的角色有主 + 从 + sentinel。无论哪种模式,主从模式都是存在的,起到读写分离、数据备份的作用。

哨兵模式

哨兵是一个独立与数据节点的进程,具备集群监控、消息通知、故障自动转移、配置中心的功能,用于解决高可用的问题。

这里了解两个概念,主观下线和客观下线。单个哨兵判断一个主节点下线,为主观下线;该哨兵向其他哨兵确认主节点是否下线,当一半以上哨兵确定主节点下线,则判定该主节点为客观下线。主节点被判定为客观下线后,哨兵会启动Raft选主程序,最终被投为领导者的哨兵节点完成主从自动切换的过程。

集群模式

官方提供的分布式方案,包含槽指派、重新分片、故障转移。

哨兵模式和集群模式的区别

  • 监控:哨兵模式下,监控权交给了哨兵系统;集群模式下,工作节点自己监控
  • 故障转移:哨兵模式下,哨兵发起选举,得到一个leader来处理故障转移;集群模式下,是在从节点中选举一个新的主节点,来处理故障的转移

数据安全

数据丢失

  • 主从复制
    • 由于复制是异步的,如果在未复制前主节点宕机,数据丢失
  • 脑裂
    • 网络分区间不可访问导致某个master节点和集群失联,但是客户端依然在写入数据,这部分数据会丢失

数据类型

string

string 类型是二进制安全的,一个键最大能存储512MB数据。存储结构分位整数、EmbeddingString、SDS,整数存储字符串长度小于21且能转化为整数的字符串;EmbeddingString 存储字符串长度小于 39 的字符串;SDS 存储不满足上述条件的字符串。

  • 只对长度小于或等于 21 字节,并且可以被解释为整数的字符串进行编码,使用整数存储

  • 尝试将 RAW 编码的字符串编码为 EMBSTR 编码,使用EMBSTR 编码

  • 这个对象没办法进行编码,尝试从 SDS 中移除所有空余空间,使用SDS编码

SDS

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
/*
* 保存字符串对象的结构
*/
struct sdshdr {

// buf 中已占用空间的长度
int len;

// buf 中剩余可用空间的长度
int free;

// 数据空间
char buf[];
};

/*
* 返回 sds 实际保存的字符串的长度
*
* T = O(1)
*/
static inline size_t sdslen(const sds s) {
struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
return sh->len;
}

/*
* 返回 sds 可用空间的长度
*
* T = O(1)
*/
static inline size_t sdsavail(const sds s) {
struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
return sh->free;
}

SDS扩容

当字符串长度小于SDS_MAX_PREALLOC (1024*1024),那么就以2倍的速度扩容,当字符串长度大于SDS_MAX_PREALLOC,那么就以+SDS_MAX_PREALLOC的速度扩容。

SDS缩容

释放内存的过程中修改len和free字段,并不释放实际占用内存。

hash / 字典

底层使用哈希表实现,结构为数组+链表。redis 字典包含两个哈希表,用于在 redis 扩缩容时的渐进式rehash。

线程

redis 4.0 之后,新增了后台线程处理重操作,比如清理脏数据、无用连接的释放、大 key 的删除,但是工作线程仍然是单线程。基于内存的redis,性能瓶颈不在处理器,主要受限于内存和网络。redis 6.0 新增 I/O 线程,用于处理处理 I/O 的读写,使多个socket的读写可以并行,为工作线程减压。

其他

rehash

在集群模式下,一个 redis 实例对应一个 RedisDB,一个RedisDB对应一个Dict,一个Dict对应两个Dictht(dict hash table),正常情况下只用到 ht[0];ht[1] 在 rehash 时使用。

rehash 是redis在字典扩缩容时进行的操作,rehash 过程中,字典同时持有两个哈希表,删除、查找、更新等操作都会在两个哈希表上进行,新增操作只在新的哈希表进行,原有的数据的搬迁通过后续的客户端指令(hset、hdel)及定时任务完成。

触发条件

扩容条件:数据量大于哈希表数组长度时触发,如果数据量大于数组长度5倍,则进行强制扩容;缩容条件:数据量小于数组长度的10%。

问题

  • 满容驱逐,由于 rehash 过程中会为新的哈希表申请空间,导致满容状态下大范围淘汰key

lpush

epoll

redis中使用IO多路复用(多路是指多个socket连接,复用指的是复用一个线程)的epoll方式。

问题

为什么吞吐量高?

  • IO多路复用
  • 基于内存的数据操作

过期机制?

可从过期策略和淘汰策略来分析。过期策略分位被动删除和主动删除两类,被动删除主要是读取过期key,惰性删除;主动删除,包括定期清理和内存超过阈值触发主动清理。

淘汰策略有多种。

淘汰策略 说明
volatile-lru 仅对设置了过期时间的key采取lru淘汰
volatile-lfu 对设置了过期时间的key执行最近最少使用淘汰,redis 4.0开始支持
volatile-ttl 仅对设置了过期时间的key生效,淘汰最早写入的key
volatile-random 随机回收设置过期时间的key
allkeys-lru 对所有key都采用lru淘汰
allkeys-random 随机回收所有key
allkeys-lfu 对所有key执行最近最少使用淘汰,redis 4.0开始支持
no-enviction 默认策略,向客户端返回错误响应

注:

  • LRU:Least Recently Used,即最近最久未使用。如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。通常使用map+链表的方式实现,map判断key是否存在,链表保存顺序。
  • LFU:Least Frequently Used,最近最少使用算法。与LRU算法的不同之处,LRU的淘汰规则是基于访问时间,而LFU是基于访问次数的。
  • FIFO:First in First out,先进先出。

参考

操作系统 - 锁

mutex lock

所谓锁,在计算机中,本质是内存中的一块空间,其值被赋予了加锁/未加锁的含义,其底层实现,是基于计算机系统中的原子操作。

mutex lock 核心包含两点。

  • Compare And Set(CAS)
    • 互斥锁是对临界区的保护,能否进入临界区即能否获取到锁,思路都是判断并置位一个标志位,这个标志位本身可以是一个普通的内存变量,关键在于,对于这个变量的"判断并置位"操作需要是原子的。
    • 计算机实现原子操作,对于单核机器,关中断防止调度即可;多核理论上可使用 spinlock 保护,实际一般通过原子汇编语言实现,比如x86的tsl指令(test and set),可以用一条“无法继续分割的”汇编指令实现判断变量值并根据是否为0进行置位,具体这个指令实现原子性一般通过锁总线实现,也就是我执行这条指令时,其它核都不能访问这个地址了。
  • 根据所是否获得,决定执行策略
    • 如果锁持有,则继续运行
    • 如果所没有获取到,常规做法是将当前任务挂起,并附在 mutex 变量对应的链表上,一旦锁被释放,查找锁上挂起的任务并唤醒