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 变量对应的链表上,一旦锁被释放,查找锁上挂起的任务并唤醒

操作系统 - 进程、线程以及协程

简述

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

协程 (coroutine)是可暂停和恢复执行的过程,是一种编程思想。

协程

子程序,或者称为函数,在所有语言中都是层级调用。子程序调用是通过栈实现的,一个线程同一时间就是执行一个子程序。协程本质上也是子程序,协程作为子程序最大的特征是可中断可恢复

关联

Java 中的线程

green threads VS native threads

green threads 是一种由运行环境或虚拟机(VM)调度,而不是由本地底层操作系统调度的线程。绿色线程并不依赖底层的系统功能,模拟实现了多线程的运行,这种线程的管理调配发生在用户空间而不是内核空间,所以它们可以在没有原生线程支持的环境中工作。
在Java 1.1中,绿色线程(至少在 Solaris 上)是JVM 中使用的唯一一种线程模型。 由于绿色线程和原生线程比起来在使用时有一些限制,随后的 Java 版本中放弃了绿色线程,转而使用native threads。[2]

在 Java1.2 之后. Linux中的JVM是基于pthread实现的,即 现在的Java中线程的本质,其实就是操作系统中的线程

参考

c++ debug

addr2line

1
addr2line -f -e path/to/binary <address>

nm

列出对象文件中的符号

1
2
nm libssl.a 
nm -an libssl.a
1
2
# 安装
$ yum install binutils

ldd

1
2
3
4
5
6
7
8
$ ldd main
linux-vdso.so.1 (0x00007ffe5458c000)
libresolv.so.2 => /lib/x86_64-linux-gnu/libresolv.so.2 (0x00007f74d9d1a000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f74d9c33000)
libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f74d9a07000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f74d99e7000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f74d97be000)
/lib64/ld-linux-x86-64.so.2 (0x00007f74dc649000)

strings

strings 程序的主要功能是找出文件(包括文本文件、二进制文件等)内容中的可打印字符串。

1
strings /usr/lib/libstdc++.so.6 | grep GLIBCXX