Tallate

该吃吃该喝喝 啥事别往心里搁

sar

Collect, report, or save system activity information.
根据要统计的信息类型的不同,输出格式也不同。

统计CPU利用率

1
2
3
4
# 查看全天
sar -p
# 每隔1秒统计一次,统计10次
sar -u 1 10

CPU统计信息输出格式:

  • CPU
    all表示统计信息为所有 CPU 的平均值。
  • %user
    显示在用户级别(application)运行使用 CPU 总时间的百分比。
  • %nice
    显示在用户级别,用于nice操作,所占用 CPU 总时间的百分比。
  • %system
    在核心级别(kernel)运行所使用 CPU 总时间的百分比。
  • %iowait
    显示用于等待I/O操作占用 CPU 总时间的百分比。
  • %steal
    管理程序(hypervisor)为另一个虚拟进程提供服务而等待虚拟 CPU 的百分比。
  • %idle
    显示 CPU 空闲时间占用 CPU 总时间的百分比。

内存利用率

1
2
3
4
# 查看全天
sar -r
# 每隔1秒统计一次,统计10次
sar -r 1 10

内存统计信息输出格式

  • kbmemfree
    这个值和free命令中的free值基本一致,所以它不包括buffer和cache的空间。
  • kbmemused
    这个值和free命令中的used值基本一致,所以它包括buffer和cache的空间。
  • %memused
    这个值是kbmemused和内存总量(不包括swap)的一个百分比。
  • kbbuffers和kbcached
    这两个值就是free命令中的buffer和cache。
  • kbcommit
    保证当前系统所需要的内存,即为了确保不溢出而需要的内存(RAM+swap)。
  • %commit
    这个值是kbcommit与内存总量(包括swap)的一个百分比。

磁盘IO

1
2
3
4
# 查看全天
sar -d
# 每隔1秒统计一次,统计10次
sar -d 1 10

IO信息输出格式

  • await
    表示平均每次设备I/O操作的等待时间(以毫秒为单位)。
  • svctm
    表示平均每次设备I/O操作的服务时间(以毫秒为单位)。
  • %util
    表示一秒中有百分之几的时间用于I/O操作。

网络流量

1
2
3
4
# 查看全天
sar -n DEV
# 每个1秒统计一次,统计10次
sar -n DEV 1 10

流量信息输出格式:

  • IFACE
    就是网络设备的名称。
  • rxpck/s
    每秒钟接收到的包数目。
  • txpck/s
    每秒钟发送出去的包数目。
  • rxkB/s
    每秒钟接收到的字节数。
  • txkB/s
    每秒钟发送出去的字节数。
  • rxcmp/s
    每秒钟接收到的压缩包数目。
  • txcmp/s
    每秒钟发送出去的压缩包数目。
  • rxmcst/s
    每秒钟接收到的多播包的包数目。

系统信息

  1. 操作系统版本
    1
    head -n 1 /etc/issue
  2. uname(系统信息)
    1
    uname -a
  3. hostname(计算机名称)
    1
    hostname
  4. lspci(PCI 设备信息)
    1
    lspci -tv
  5. lsusb(USB 设备信息)
    1
    lsusb -tv
  6. lsmod(系统加载的模块信息)
    1
    lsmod
  7. CPU 信息
    1
    cat /proc/cpuinfo

CPU & 进程

ps

查看进程状态。

参数 作用
-a 显示所有的进程(包括其他用户的)
-u 用户以及其他详细信息
-x 显示没有控制终端的进程
1
2
3
4
5
ps -ef
ps -aux
$ ps -e -o 'pid,comm,args,pcpu,rsz,vsz,stime,user,uid'
# 其中rsz为实际内存,实现按内存排序,由大到小
$ ps -e -o 'pid,comm,args,pcpu,rsz,vsz,stime,user,uid' | grep oracle | sort -nrk5

linux 系统中进程最常见的 5 种状态为:

  • R(运行):正在运行或在运行队列中等待。
  • S(中断):休眠中, 在等待某个条件的形成或接受到信号。
  • D(不可中断):收到信号不唤醒和不可运行, 进程必须等待直到有中断发生。
  • Z(僵死):进程已终止, 但进程描述符存在, 直到父进程调用 wait()系统调用后释放。
  • T(停止):进程收到 SIGSTOP, SIGSTP, SIGTIN, SIGTOU 信号后停止运行。

当执行”ps aux”命令后通常会看到下面格式的进程状态,表格中只是列举了部分输出值,而且正常的输出值中不包括中文注释部分:

USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
进程的所有者 进程 ID 号 运算器占用率 内存占用率 虚拟内存使用量(单位是 KB) 占用的固定内存量(单位是 KB) 所在终端 进程状态 被启动的时间 实际使用 CPU 的时间 命令名称与参数
root 1 0.0 0.4 53684 7628 ? Ss 07:22 0:02 /usr/lib/systemd/systemd
root 2 0.0 0.0 0 0 ? S 07:22 0:00 [kthreadd]
root 3 0.0 0.0 0 0 ? S 07:22 0:00 [ksoftirqd/0]
root 5 0.0 0.0 0 0 ? S< 07:22 0:00 [kworker/0:0H]
root 7 0.0 0.0 0 0 ? S 07:22 0:00 [migration/0]

pidof

查询某个指定服务的进程 PID 号码值,比如

1
pidof firefox

kill

终止某个指定 PID 号码的进程

1
2
3
$ kill <PID>
# 杀掉所有进程名里带idea的
$ ps -aux | grep idea | awk '{print $2}' | xargs kill

killall

终止某个指定名称的服务所对应的全部进程,因为一般大型软件的服务程序通常都会有数个进程协同为其提供服务,如果逐个去结束 PID 实在麻烦,所以可以使用 killall 命令来批量结束某个服务程序的全部进程,比如

1
$ killall httpd

top

top 命令是 Linux 下常用的性能分析工具,能够动态监视进程活动与系统负载等信息,类似于 Windows 的任务管理器
可以直接使用 top 命令后,查看%MEM 的内容。可以选择按进程查看或者按用户查看,如想查看 oracle 用户的进程内存使用情况的话可以使用如下的命令:

1
$ top -u oracle

示例输出如下:

1
2
3
4
5
6
7
8
9
10
11
top - 17:59:11 up 289 days,  5:57, 20 users,  load average: 0.01, 0.02, 0.05
Tasks: 201 total, 2 running, 199 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.1 us, 0.0 sy, 0.0 ni, 99.9 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 8010956 total, 465600 free, 886236 used, 6659120 buff/cache
KiB Swap: 8191996 total, 8170296 free, 21700 used. 6412852 avail Mem

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
751 root 20 0 555136 7428 3784 S 0.3 0.1 29:27.31 tuned
1176 root 20 0 91160 2032 1712 S 0.3 0.0 248:22.80 zabbix_agentd
2287 root 20 0 1033580 260536 3204 S 0.3 3.3 149:31.25 salt-minion
......

前面的五行为系统整体的统计信息,下面我们来逐行的讲解:

  • 第 1 行:系统时间,运行时间(上例中是 289 天 5 小时 57 分),登录用户数,系统负载(分别为 1 分钟、5 分钟、15 分钟的平均值)。
  • 第 2 行:进程总数,运行中的,睡眠中的,停止的,僵死的。
  • 第 3 行:用户态占用时间比例(us user),内核态占用时间比例(sy system),改变过优先级的进程占用时间比例(ni nice),空闲 CPU 时间比例(id idle),等待 IO 时间比例(wa iowait),处理硬中断时间比例(hi hard interrupt),处理软中断时间比例(si soft interrupt),当前系统运行在虚拟机中的时候、被其他虚拟机占用的 CPU 时间比例(st steal)。
    此行数据为百分比,比如11.2 id意味着有 11.2%的 CPU 资源是空闲的。
  • 第 4 行:物理内存总量,空闲量,使用量,作为内核缓存的内存量。
  • 第 5 行:虚拟内存总量,空闲量,使用量,已被提前加载的内存数据。

后面每行是进程的统计数据,其中:
PID:进程的 ID
USER:进程所有者
PR:进程的优先级别,越小越优先被执行
NInice:值
VIRT:进程占用的虚拟内存
RES:进程占用的物理内存
SHR:进程使用的共享内存
S:进程的状态。S 表示休眠,R 表示正在运行,Z 表示僵死状态,N 表示该进程优先值为负数
%CPU:进程占用 CPU 的使用率
%MEM:进程使用的物理内存和总内存的百分比
TIME+:该进程启动后占用的总的 CPU 时间,即占用 CPU 使用时间的累加值。
COMMAND:进程启动命令名称

常用的命令:
P:按%CPU 使用率排行
T:按 MITE+排行
M:按%MEM 排行

pmap

可以根据进程查看进程相关信息占用的内存情况,(进程号可以通过 ps 查看)如下所示:

1
$ pmap -d 14596

内存

/proc/meminfo

查看内存总量和空闲内存量

1
2
$ grep MemTotal /proc/meminfo
$ grep MemFree /proc/meminfo

free

内存和交换分区容量及使用情况

1
2
3
4
5
6
7
8
9
10
$ free
total used free shared buffers cached
Mem: 2902976 932640 1970336 9856 928 328196
-/+ buffers/cache: 603516 2299460
Swap: 2097148 0 2097148
$ free -h
total used free shared buffers cached
Mem: 2.8G 910M 1.9G 9.6M 928K 320M
-/+ buffers/cache: 589M 2.2G
Swap: 2.0G 0B 2.0G

total:总计内存量
used:已用量
free:可用量
shared:进程共享的内存量
buffers:磁盘缓存的内存量
cached:缓存的内存量

pmap

TODO

硬盘

df

硬盘分区使用情况

1
df

du

查看某个目录的大小

1
du -sh <目录>

网络

TODO

ifconfig

获取网卡配置与网络状态等信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ ifconfig

eno16777728: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
ether 00:0c:29:62:f3:d0 txqueuelen 1000 (Ethernet)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
inet6 ::1 prefixlen 128 scopeid 0x10<host>
loop txqueuelen 0 (Local Loopback)
RX packets 514 bytes 41612 (40.6 KiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 514 bytes 41612 (40.6 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

注意每段开头的网卡名称、inet 参数后面的 IP 地址、ether 参数后面的物理 mac 地址,以及 RX、TX 的接受与发送数据包的大小

nc

netstat

iostat

tcpdump

mtr

监控

Linux性能观测工具

syslog

TODO

dmesg

如果发现自己的 java 进程悄无声息的消失了,几乎没有留下任何线索,那么 dmesg 一发,很有可能有你想要的。

1
sudo dmesg|grep -i kill|less

去找关键字 oom_killer。找到的结果类似如下:

1
2
3
4
5
[6710782.021013] java invoked oom-killer: gfp_mask=0xd0, order=0, oom_adj=0, oom_scoe_adj=0
[6710782.070639] [<ffffffff81118898>] ? oom_kill_process+0x68/0x140
[6710782.257588] Task in /LXC011175068174 killed as a result of limit of /LXC011175068174
[6710784.698347] Memory cgroup out of memory: Kill process 215701 (java) score 854 or sacrifice child
[6710784.707978] Killed process 215701, UID 679, (java) total-vm:11017300kB, anon-rss:7152432kB, file-rss:1232kB

以上表明,对应的 java 进程被系统的 OOM Killer 给干掉了,得分为 854.
解释一下 OOM killer(Out-Of-Memory killer),该机制会监控机器的内存资源消耗。当机器内存耗尽前,该机制会扫描所有的进程(按照一定规则计算,内存占用,时间等),挑选出得分最高的进程,然后杀死,从而保护机器。
dmesg 日志时间转换公式:
log 实际时间=格林威治 1970-01-01+(当前时间秒数-系统启动至今的秒数+dmesg 打印的 log 时间)秒数:

1
date -d "1970-01-01 UTC `echo "$(date +%s)-$(cat /proc/uptime|cut -f 1 -d' ')+12288812.926194"|bc ` seconds"

剩下的,就是看看为什么内存这么大,触发了 OOM-Killer 了。

/proc/loadavg

系统负载

1
$ cat /proc/loadavg

uptime

包括当前系统时间、系统已运行时间、当前在线用户、平均负载值等。
平均负载值指的是最近 1/5/15 分钟的系统压力情况,负载值越低越好,尽量不要长期超过 1。

1
2
$ uptime
05:43:50 up 16 min, 2 users, load average: 0.00, 0.07, 0.13

配合 watch 命令来每秒刷新一次来获得当前系统负载情况:

1
watch -n 1 uptime

vmstat

TODO

dmidecode

TODO

参考

  1. 温度 sensors
  2. Linux 性能检测常用的 10 个基本命令

Eureka 的目标

原来:负载均衡器会根据配好的 IP 和主机名来进行负载均衡,但是对 AWS cloud 这样体量的系统来说,因为服务实例宕机恢复十分频繁,所以负载均衡器还会有一个更复杂的注册 / 注销服务的机制。
现在:Eureka 在中间层提供一种负载均衡的可能。

服务发现比较

Feature Consul zookeeper etcd euerka
服务健康检查 服务状态,内存,硬盘等 (弱)长连接,keepalive 连接心跳 可配支持
多数据中心 支持
kv存储服务 支持 支持 支持
一致性 raft paxos(zab) raft
cap ca cp cp ap
使用接口(多语言能力) 支持http和dns 客户端 http/grpc http(sidecar)
watch支持 全量/支持long polling 支持 支持 long polling 支持 long polling/大部分增量
自身监控 metrics metrics metrics
安全 acl /https acl https支持(弱)
spring cloud集成 已支持 已支持 已支持 已支持

纵向(功能)比较

服务的健康检查

Euraka 使用时需要显式配置健康检查支持;Zookeeper,Etcd 则在失去了和服务进程的连接情况下任务不健康,而 Consul 相对更为详细点,比如内存是否已使用了90%,文件系统的空间是不是快不足了。

多数据中心支持

Consul 通过 WAN 的 Gossip 协议,完成跨数据中心的同步;而且其他的产品则需要额外的开发工作来实现。

KV 存储服务

除了 Eureka ,其他几款都能够对外支持 k-v 的存储服务,所以后面会讲到这几款产品追求高一致性的重要原因。而提供存储服务,也能够较好的转化为动态配置服务哦。

产品设计中 CAP 理论的取舍(这一段感觉是瞎说的,博客下面评论产生很多争议)

Eureka 典型的 AP,作为分布式场景下的服务发现的产品较为合适,服务发现场景的可用性优先级较高,一致性并不是特别致命。其次 CA 类型的场景 Consul,也能提供较高的可用性,并能 k-v store 服务保证一致性。 而Zookeeper,Etcd则是CP类型 牺牲可用性,在服务发现场景并没太大优势。

多语言能力与对外提供服务的接入协议

Zookeeper的跨语言支持较弱,其他几款支持 http11 提供接入的可能。Euraka 一般通过 sidecar的方式提供多语言客户端的接入支持。Etcd 还提供了Grpc的支持。 Consul除了标准的Rest服务api,还提供了DNS的支持。

Watch的支持(客户端观察到服务提供者变化)

Zookeeper 支持服务器端推送变化,Eureka 2.0(正在开发中)也计划支持。 Eureka 1,Consul,Etcd则都通过长轮询的方式来实现变化的感知。

自身集群的监控

除了 Zookeeper ,其他几款都默认支持 metrics,运维者可以搜集并报警这些度量信息达到监控目的。

安全

Consul,Zookeeper 支持ACL,另外 Consul,Etcd 支持安全通道https。

Spring Cloud的集成

目前都有相对应的 boot starter,提供了集成能力。
总的来看,目前Consul 自身功能,和 spring cloud 对其集成的支持都相对较为完善,而且运维的复杂度较为简单(没有详细列出讨论),Eureka 设计上比较符合场景,但还需持续的完善。

Eureka VS ZooKeeper

  • Eureka 能提供 REST 接口来动态调整配置、renewals、expiration、cancel 等;
  • Eureka 倾向于高可用,而不是 ZooKeeper 的高一致性。
  • Eureka 可以集成到应用中,ZooKeeper 只能作为一个外部组件提供服务,这会增加复杂性、增加系统崩溃的几率。

组成部分

Eureka组件结构

  • 负载均衡:Eureka Client 提供最简单的轮询负载均衡策略,可以封装 Eureka 并根据更多的因素(流量、资源使用、异常发生频次等)来提供一种更好的弹性伸缩特性。
  • 分区:每个 Region 有一个 Eureka 集群用于处理该区域服务失败的情况,各 Region 之间是不会互相通信的。
  • 服务注册到 Eureka Server 后每 30 秒发送一次心跳(heartbeats)来刷新租约(lease),如果网络出现分区或者 Eureka 宕机了,这种心跳自然会停止,如果达到了Renews threshold(即 Server 期望在每分钟中收到的心跳次数,需要考虑是否禁用服务器的自注册、Server/Client 数量等,暂时取默认值 85%就好),Eureka Server 就会将其从服务注册表中移除。
  • 服务注册信息会自动同步到整个 Eureka Server 集群,这也意味着它们是对等的 P2P 集群。
  • 集成到业务服务中的 Eureka Client 可以查询服务注册信息(默认每 30 秒一次)来定位服务及进行远程调用。

服务状态机

Eureka实例状态机

  • STARTING:启动中的状态,应用可以在这个阶段做一些初始化工作
  • UP:可以正常进行通信;
  • DOWN:心跳停了,一般是宕机了或者网络出现了分区
  • OUT_OF_SERVICE:因为某些特殊原因无法提供服务,比如 Elasticsearch 因为没有达到最小可用分片数,或者由于蓝绿发布的需要,新版本如果发布后有问题可以直接将实例状态置为 OUT_OF_SERVICE 来达到回滚的目的。
  • UNKNOWN:WTF?

Client 与 Server 间的交互

Register

Eureka Client 将信息注册到 Eureka Server,注册过程发生在第一次心跳时(在 30 秒后)。

Unregister

正常情况下,Client 必须显式调用 Unregister 来释放自己的注册信息,除非是由于”unclean termination”而导致心跳丢失超过 3 次。

Renew

客户端每30秒通过发送一次心跳(heartbeats)来续约(renewal),心跳告知Eureka Server本实例仍然存活,如果Server在90秒内没有收到续约请求,它将从服务注册表中移除该实例。

Fetch Registry

Eureka clients fetches the registry information from the server and csort_bufferhes it locally. After that, the clients use that information to find other services. This information is updated periodically (every 30 seconds) by getting the delta updates between the last fetch cycle and the current one. The delta information is held longer (for about 3 mins) in the server, hence the delta fetches may return the same instances again. The Eureka client automatically handles the duplicate information.

After getting the deltas, Eureka client reconciles the information with the server by comparing the instance counts returned by the server and if the information does not match for some reason, the whole registry information is fetched again. Eureka server caches the compressed payload of the deltas, whole registry and also per application as well as the uncompressed information of the same. The payload also supports both JSON/XML formats. Eureka client gets the information in compressed JSON format using jersey apache client.

Cancel

Eureka client sends a cancel request to Eureka server on shutdown. This removes the instance from the server’s instance registry thereby effectively taking the instance out of traffic.

This is done when the Eureka client shuts down and the application should make sure to call the following during its shutdown.
DiscoveryManager.getInstance().shutdownComponent()

Time Lag

All operations from Eureka client may take some time to reflect in the Eureka servers and subsequently in other Eureka clients. This is because of the caching of the payload on the eureka server which is refreshed periodically to reflect new information. Eureka clients also fetch deltas periodically. Hence, it may take up to 2 mins for changes to propagate to all Eureka clients.

Communication mechanism

Eureka Client默认使用Jersey发送基于Jackson封装的JSON数据包给Eureka Server。

通信协议

Eureka 不限制通信协议,Thrift、HTTP(S)等均可。

高可用

Eureka Client 的高可用设计:

  • Client 中有服务注册表的缓存,即使所有 Server 都挂掉了,Client 还是能继续工作。
  • 刚开始,Eureka Client 会尝试与同一 zone(可视为同一局域网)中的 Eureka Server 交互,如果交互出现问题或同一 zone 中没有可用的 Eureka Server,则它将转向其他 zone。

Eureka Server 的高可用设计:

  • 启动 Server 时从邻居节点获取注册信息,一个不行换另一个,直到获取成功,如果从邻居节点均无法获取到注册信息,则它会等待几分钟(默认 5 分钟)让 Client 注册它们的信息
    Server 之间获取服务注册信息的机制和 Client 从 Server 获取的一样。
    获取成功后,Server 会设置Renewal Threshold并开始接收 Client 的心跳;
  • 保护模式:如果Renews(last min)(上一分钟内收到的心跳次数)达到了Renews threshold(Server 期望在每分钟中收到的心跳次数,一般是 3),或者过去 15 分钟内的统计数据小于eureka.server.renewalPercentThreshold(renews / renews threshold 的比值,默认为 0.85,当在 15 分钟内微服务心跳数低于 85%,则 Server 会进入自我保护状态,在这种情况下 Server 不会删除注册信息),则进入保护模式,自我保护状态其实是为了防止突发网络不稳定或断电时微服务心跳数剧减,导致微服务注册信息被大量删除的情况。
    在保护模式下,Client 可能从 Server 得到已经不可用的 IP(服务器已不存在或因某些原因无法响应),因此 Client 必须保证这种情况下的弹性高可用,比如快速地超时并重试其他服务器。
  • 退出保护模式:在保护模式下,Eureka Server 会停止移除服务注册信息,直到满足如下条件中的任意之一:
    1. 心跳Renews达到了Renews threshold
    2. 保护模式被禁用,设置eureka.server.enableSelfPreservation=false
  • 孤儿 Server:当发生网络分区,一些 Eureka Server 可能会成为orphaned server,一些 Client 会注册到这些 Server 上,导致一些 Client 能看到这些注册信息而其他的一些则不能。
    当网络恢复后,Server 的 P2P 集群能正常地交互,注册信息会被自动同步到所有 Server 上。

异常情况

比如在测试环境中出现:

1
EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.

解决办法:

  • 在生产上可以开自注册,部署两个 server
  • 在本机器上测试的时候,可以把比值调低,比如 0.49
  • 或者简单粗暴把自我保护模式关闭:eureka.server.enableSelfPreservation=false

Eureka 配置

配置:
Configuring Eureka
Eureka Server 开放的 REST 接口提供动态配置功能:
Eureka REST operations

添加自定义元数据

静态设置:

1
eureka.metadata.mykey=myvalue

设置后,相当于将mykey:myvalue添加到 eureka 的metadata map中。
动态设置:
需要提供一个自定义的

获取:

1
String myValue = instanceInfo.getMetadata().get("myKey");

源码

原生客户端的执行过程

EurekaClient

通过 DI(依赖注入)使用 EurekaClient

ExampleEurekaGovernatedService

配置

DefaultEurekaClientConfig extends EurekaClientConfig
EurekaServerConfig extends DefaultEurekaServerConfig
CloudInstanceConfig extends PropertiesInstanceConfig
MyDataCenterInstanceConfig extends PropertiesInstanceConfig

To dynamically do this, you will need to first provide your own custom implementation of the EurekaInstanceConfig interface. You can then overload the public Map<String, String> getMetadataMap() method to return a metadata map that contains the desired metadata values. See PropertiesInstanceConfig for an example implementation that provides the configuration based system above.

参考

  1. Netflix/eureka

网关 - Zuul

Filter

Zuul组件结构
Zuul 基于 Netty 开发,使用 filters 包含了核心业务逻辑,Filter 是使用 Groovy 写的,主要是为了提供动态编译加载的能力,filters 主要包含了三类:

  • Inbound Filters execute before routing to the origin and can be used for things like authentication, dynamic routing, rate limiting, DDoS protection, metrics and decorating the request.
  • Endpoint Filters can be used to return static responses, otherwise the built-in ProxyEndpoint filter will route the request to the origin.
  • Outbound Filters execute after getting the response from the origin and can be used for metrics, decorating the response to the user or adding custom headers.
  • Async
    Filter 可以被同步执行或异步执行。
    如果 Filter 没有做太重的工作,可以通过继承HttpInboundSyncFilterHttpOutboundSyncFilter来实现一种同步 Filter,例子见Zuul 源码中的Routes.groovy
    反之,如果需要从其他服务、缓存获取数据,或做一些复杂的计算工作,则最好继承HttpInboundFilterHttpOutboundFilter,例子见Zuul 源码中的SampleServiceFilter.groovy

Filter属性

Type: most often defines the stage during the routing flow when the Filter will be applied (although it can be any custom string)
Async: define if the filter is sync or async, generally meaning do you need to make an external call or just doing work on-box
Execution Order: applied within the Type, defines the order of execution across multiple Filters
Criteria: the conditions required in order for the Filter to be executed
Action: the action to be executed if the Criteria is met

其他的一些例子

这些例子是zuul-sample中的代码。

  • DebugRequest - look for a query param to add extra debug logging for a request
  • Healthcheck - simple static endpoint filter that returns 200, if everything is bootstrapped correctly
  • ZuulResponseFilter - add informational headers to provide extra details on routing, request execution, status and error cause
  • GZipResponseFilter - can be enabled to gzip outbound responses
  • SurgicalDebugFilter - can be enabled to route specific requests to different hosts for debugging

缓存请求体

默认情况下 Zuul 不会缓存请求体,因为 Filter 一般用到请求头就够了,但是如果需要在 inbound 中用到请求头或在 outbound 中用到响应头,则需要明确指定 Zuul 缓存,可以重写 Filter 的needsBodyBuffered()

1
2
3
4
@Override
boolean needsBodyBuffered(HttpResponseMessage input) {
return true
}

网络协议

Zuul 支持修改暴露服务时使用的协议,使用方法见 sample 项目中的SampleServerStartup

其他功能

Core Features

Push Messaging

Push Messaging 机制可以支持从 Server 端推送消息到 Client 端,支持两种协议:WebSocketsServer Sent Events (SSE)
Push Messaging
TODO

负载均衡 - Ribbon

使用

原生 API 如何使用见:Netflix / ribbon - Getting Started
如果是搭配 Spring Boot,可以参考 Spring Could 文档。

组件结构及实现

Rule

a logic component to determine which server to return from a list

  • RoundRobinRule
    简单的轮询策略
  • AvailabilityFilteringRule
    这个 Rule 会跳过那些疑似“电路跳闸”或并发连接数已经很高的服务器。
    比如客户端的最后 3 次连接失败,客户端会认为该服务实例已经出现了类似“电路跳闸”的问题而导致无法提供服务,于是在接下来的 30 秒内均保持这种状态,如果之后还是连接失败,这个等待时间会指数增长(1min、2min、4min…)。
  • WeightedResponseTimeRule
    每个 Server 会根据其平均响应时间计算出一个权重,响应时间越长、比重越小,该 Rule 选择 Server 时会根据该权重来计算概率。

Ping

a component running in background to ensure liveness of servers

ServerList

this can be static or dynamic. If it is dynamic (as used by DynamicServerListLoadBalancer), a background thread will refresh and filter the list at certain interval

  • 静态的 Server 列表
    可以在程序里写一个静态列表,将该列表设置到BaseLoadBalancer.setServerList()中。
  • ConfigurationBasedServerList
    默认的 ServerList 实现,可以通过 Archaius ConfigurationManager来设置 Server 列表。
  • DiscoveryEnabledNIWSServerList
    可以通过 Eureka Client 获取服务器列表,服务器集群必须通过 VipAddress 来定义:
    1
    2
    3
    myClient.ribbon.NIWSServerListClassName=com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList 
    # the server must register itself with Eureka server with VipAddress "myservice"
    myClient.ribbon.DeploymentContextBasedVipAddresses=myservice

ServerListFilter

ServerListFilter 是DynamicServerListLoadBalancer的组件,用于过滤从ServerList返回的服务器列表,现在有两种实现:

  • ZoneAffinityServerListFilter
    过滤掉不在同一个 zone 内的服务器,除非 zone 内没有可用的服务器,这个 Filter 可以通过设置如下属性来启用(假设客户端名为 myclient、客户端的属性空间为 ribbon):
    1
    myclient.ribbon.EnableZoneAffinity=true
  • ServerListSubsetFilter
    可以保证客户端只能看到ServerList返回的全体服务器的一个固定子集,如果有服务器可用性较弱,则可以定期用新服务器替换老服务器。可以通过设置以下属性启用该Filter:
    1
    2
    3
    4
    5
    6
    myClient.ribbon.NIWSServerListClassName=com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList 
    # the server must register itself with Eureka server with VipAddress "myservice"
    myClient.ribbon.DeploymentContextBasedVipAddresses=myservice
    myClient.ribbon.NIWSServerListFilterClassName=com.netflix.loadbalancer.ServerListSubsetFilter
    # only show client 5 servers. default is 20.
    myClient.ribbon.ServerListSubsetFilter.size=5

源码

com.netflix.loadbalancer.DynamicServerListLoadBalancer#updateListOfServers
com.netflix.loadbalancer.ServerList#getUpdatedListOfServers
com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList#obtainServersViaDiscovery
TODO

参考

常见实现

  1. Netflix/zuul
  2. Netflix/ribbon
  3. Netflix / Turbine

虚拟化技术将一台服务器虚拟出多个虚拟机来提供服务。
虚拟化技术包括计算虚拟化(服务器虚拟化)、存储虚拟化、网络虚拟化等。
在实际讲解虚拟化之前,我们需要先解释一下隔离技术,在隔离的基础上我们才能任意粒度、自由地分配资源。

阅读全文 »

kubectl 的安装

Install and Set Up kubectl

kubernetes 含义

Kubernetes Master

Kubernetes Master 主要负责管理集群,它会协调集群内的所有活动,包括:scheduling applications, maintaining applications’ desired state, scaling applications, and rolling out new updates。
Master 实际上是三个进程的集合,它们运行在集群的一个 Master Node 上,这三个进程包括:

  • kube-apiserver:提供REST-API来操作 Kubernetes Objects,包括 pods、services、replicationcontrollers 等,并可用于控制集群状态。
  • kube-controller-manager:通过 apiserver 监听集群状态并做状态转移操作。
  • kube-scheduler:scheduler 可以根据集群的拓扑结构、性能、容量等动态分配资源。

Node

Node 是一台虚拟机或物理电脑,在集群中作为 Worker。Master 管理 Cluster,而 Node 则管理应用。
每个 Node 都有一个 Kubelet,是管理 Node 的媒介,且负责与 Kubernetes Master 进行交互。
每个 Node 还需要有工具来处理容器操作,比如 Docker 或 rkt。
一个生产环境的 cluster 必须由至少 3 个 Node 组成。

kubelet

节点代理,负责和 Kubernetes Master 交互:

  • 向 apiserver 注册节点
  • 其他云计算指定逻辑。

kube-proxy

运行于每个 Node 上的一个网络代理,可以执行简单的 TCP、UDP、SCTP 流转发或提供多后端进程的负载均衡。
相当于在主机和 Cluster 之间创建了一个连接,让我们能直接访问 API。

Pod

一个或多个应用容器集合的抽象,并且包含一些共享资源,包括:

  • 共享存储,如 Volumes;
  • 网络连接,每个 Pod 具有 Cluster 中唯一的一个 IP 地址;
  • 运行每个容器所必须的信息,比如容器镜像的版本、端口等。

Pod 建模了一种“logical host”,可以同时运行多种不同的容器。
当 Pod 所处的 Node 挂了,ReplicaSet会动态地创建新 Pod 来使得 Cluster 回到原来的状态。

Service

Service 定义了 Pod 的逻辑集合及其访问规则,虽然每个 Pod 都有一个唯一的 IP 地址(Cluster 范围内),但是如果没有 Service 的话这些 IP 也是没法暴露到 Cluster 外的,可以通过指定 ServiceSpec 中的type来指定暴露服务的方式:

  • ClusterIP (default) - Exposes the Service on an internal IP in the cluster. This type makes the Service only reachable from within the cluster.
  • NodePort - Exposes the Service on the same port of each selected Node in the cluster using NAT. Makes a Service accessible from outside the cluster using <NodeIP>:<NodePort>. Superset of ClusterIP.
  • LoadBalancer - Creates an external load balancer in the current cloud (if supported) and assigns a fixed, external IP to the Service. Superset of NodePort.
  • ExternalName - Exposes the Service using an arbitrary name (specified by externalName in the spec) by returning a CNAME record with the name. No proxy is used. This type requires v1.7 or higher of kube-dns.

Volumes

Namespaces

Deployments

负责创建和更新应用实例,创建 Deployment 后,Master 会持续监听并在各 Node 中调度应用实例,一旦有实例挂掉或被删掉,Deployment controller 就会用 Cluster 中另一 Node 上的实例取代之。

  • searched for a suitable node where an instance of the application could be run (we have only 1 available node)
  • scheduled the application to run on that Node
  • configured the cluster to reschedule the instance on a new Node when needed

DaemonSet

StatefulSets

ReplicaSet

Jobs

kubernetes 使用

可以采用kubectl命令来和 Cluster 交互,kubectl最常用的操作只有如下 4 种:

  • kubectl get - list resources
  • kubectl describe - show detailed information about a resource
  • kubectl logs - print the logs from a container in a pod
  • kubectl exec - execute a command on a container in a pod

创建Cluster

例子中使用minikube创建 Cluster,然后使用kubectl来和创建的 Cluster 交互:

1
2
3
4
5
6
7
8
9
minikube version
# 启动一个Cluster
minikube start
# 查看客户端(kubectl的版本)和服务端(Kubernetes的版本)的版本
kubectl version
# Cluster的详细信息
kubectl cluster-info
# 获取Cluster内的Node列表
kubectl get nodes
  1. 创建部署单元(Deployment)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 创建Deploymnet
    kubectl create deployment kubernetes-bootcamp --image=gcr.io/google-samples/kubernetes-bootcamp:v1
    # 查看Deployment
    kubectl get deployments

    # 查看Pod
    kubectl get pods
    # 查看Pod内Container的属性,比如使用的是什么镜像
    kubectl describe pods

访问

  1. 使用 proxy 访问 Service 和 Pod
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 创建proxy实例
    kubectl proxy
    # 创建proxy后可以在另一个终端调用REST API来查看Cluster信息
    curl http://localhost:8001/version
    # 查看Pod的名字
    export POD_NAME=$(kubectl get pods -o go-template --template '{{range .items}}{{.metadata.name}}{{"\n"}}{{end}}')
    echo Name of the Pod: $POD_NAME
    # 可以通过proxy直接访问Pod
    curl http://localhost:8001/api/v1/namespaces/default/pods/$POD_NAME/proxy/
  2. 查看日志
    1
    2
    # 所有应用发送给STDOUT的信息都会成为Pod容器的日志
    kubectl logs $POD_NAME
  3. 使用 exec 命令访问 Pod
    1
    2
    3
    4
    # 查看Pod内的环境变量
    kubectl exec $POD_NAME env
    # 在Pod中一个容器内启动一个bash会话,相当于登入了该容器
    kubectl exec -ti $POD_NAME bash

Service

  1. 创建 Service
    1
    2
    3
    4
    5
    6
    # 查看Service
    kubectl get services
    # 创建一个新的Service并暴露端口(type=NodePort可以用于占用一个Node上的端口来暴露服务)
    kubectl expose deployment/kubernetes-bootcamp --type="NodePort" --port 8080
    # 查看Service信息
    kubectl describe services/kubernetes-bootcamp
  2. Deployment 与 Service
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 8080其实是集群内的一个逻辑端口,可以通过以下命令获取Node上暴露的端口
    export NODE_PORT=$(kubectl get services/kubernetes-bootcamp -o go-template='{{(index .spec.ports 0).nodePort}}')
    # 然后我们可以通过curl访问Node上的端口来确认Service已经被暴露到了Cluster外
    echo NODE_PORT=$NODE_PORT

    # Deployment会自动为Pod创建一个label,可以通过如下命令来查看
    kubectl describe deployment
    # 通过label查看Pod列表和Service列表
    kubectl get pods -l run=kubernetes-bootcamp
    kubectl get services -l run=kubernetes-bootcamp
  3. Service 中的 Pod
    1
    2
    3
    4
    5
    6
    7
    8
    # 先获取Pod的名字
    export POD_NAME=$(kubectl get pods -o go-template --template '{{range .items}}{{.metadata.name}}{{"\n"}}{{end}}')
    echo Name of the Pod: $POD_NAME
    # 通过label命令可以设置一个新的label
    kubectl label pod $POD_NAME app=v1
    # 之后可以使用describe 命令来查看这些label,也可以使用label来查询Pod
    kubectl describe pods $POD_NAME
    kubectl get pods -l app=v1
  4. 删除 Service
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 删除一个Service
    kubectl delete service -l run=kubernetes-bootcamp
    # 查看服务列表,该服务已经被删除
    kubectl get services
    # curl端口确认不再暴露服务
    curl $(minikube ip):$NODE_PORT
    # 我们可以在Pod中运行curl来确定应用仍运行于Pod中
    kubectl exec -ti $POD_NAME curl localhost:8080
    # 如果需要终止应用,则需要同时删除Deployment

app 扩展

scaling
上图来自官网教程,Scaling 即修改 Deployment 中的 Pod 数。

  1. 扩展 ReplicaSet(Scale Up)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 先看下部署了几个Pod
    kubectl get deployments
    # 查看Deployment创建的ReplicaSet,ReplicaSet名的格式为[DEPLOYMENT-NAME]-[RANDOM-STRING]
    kubectl get rs
    # 将Deployment扩展为4个复制
    kubectl scale deployments/kubernetes-bootcamp --replicas=4
    # 查看Deployment及其中的Pod
    kubectl get deployments
    kubectl get pods -o wide
    # 查看Deployment的时间日志
    kubectl describe deployments/kubernetes-bootcamp
  2. Load Balancing
    1
    2
    3
    4
    5
    # 使用一个NODE_PORT环境变量来保存Node暴露的端口
    export NODE_PORT=$(kubectl get services/kubernetes-bootcamp -o go-template='{{(index .spec.ports 0).nodePort}}')
    echo NODE_PORT=$NODE_PORT
    # curl访问多次,会发现请求被负载均衡到了不同的Pod上
    curl $(minikube ip):$NODE_PORT
  3. 缩小(Scale Down)
    1
    2
    3
    4
    5
    # 再运行一次scale命令来缩小ReplicaSet
    kubectl scale deployments/kubernetes-bootcamp --replicas=2
    # 查看伸缩结果
    kubectl get deployments
    kubectl get pods -o wide

滚动更新(Rolling Update)

  1. 更新应用版本
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 先看看Cluster里有啥
    kubectl get deployments
    kubectl get pods
    # 查看应用当前使用的镜像版本
    kubectl describe pods
    # 使用set image命令设置应用的镜像版本
    kubectl set image deployments/kubernetes-bootcamp kubernetes-bootcamp=jocatalin/kubernetes-bootcamp:v2
    # 可以通过Pod查看应用的更新过程
    kubectl get pods
  2. 验证更新
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 确认应用正在运行中
    kubectl describe services/kubernetes-bootcamp
    # 用一个NODE_PORT环境变量保存Node暴露的端口,然后用curl确认暴露的服务可以使用
    export NODE_PORT=$(kubectl get services/kubernetes-bootcamp -o go-template='{{(index .spec.ports 0).nodePort}}')
    echo NODE_PORT=$NODE_PORT
    curl $(minikube ip):$NODE_PORT
    # 更新同样可以使用rollout status命令来确认
    kubectl rollout status deployments/kubernetes-bootcamp
    # 查看应用当前使用的镜像的版本
    kubectl describe pods
  3. 回滚(Rollback)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # 设置镜像进行更新,但是这个镜像的v10版本是不存在的,因此会引起更新失败
    kubectl set image deployments/kubernetes-bootcamp kubernetes-bootcamp=gcr.io/google-samples/kubernetes-bootcamp:v10
    # 查看Deployment的当前状态,会发现某些Pod运行不正常
    kubectl get deployments
    # 查看Pod状态,describe能提供更多信息
    kubectl get pods
    kubectl describe pods
    # 发现问题,可以使用rollout undo命令来回滚,rollout命令会复原deployment到上一已知状态。实际上所有更新操作都是版本化的,并且每次更新都可以回滚到之前的任意版本
    kubectl rollout undo deployments/kubernetes-bootcamp
    # 接下来再查看Pod的状态可以发现所有的Pod都已经恢复运行了
    kubectl get pods
    kubectl describe pods

参考

  1. Tutorials
    Learn Kubernetes Basics
    Configuring Redis using a ConfigMap
    使用 k8s 搭建 Redis 集群
    Exposing an External IP Address to Access an Application in a Cluster
    如何使用 k8s 搭建一个无状态服务
    StatefulSet Basics
    如何使用 k8s 搭建一个有状态服务
    AppArmor
    Using Source IP
    CICD
    https://www.linux.com/tutorials/set-cicd-pipeline-jenkins-pod-kubernetes-part-2/
    https://www.linux.com/tutorials/run-and-scale-distributed-crossword-puzzle-app-cicd-kubernetes-part-3/
    https://www.linux.com/tutorials/set-cicd-distributed-crossword-puzzle-app-kubernetes-part-4/

在 awk、sed、cut 三个命令中,awk 是功能最强大的,基本能实现所有字符串操作,平时常用于较复杂的日志分析,不过比起别的命令来也会相对复杂一点。

阅读全文 »

  • TCP是面向连接
    所谓的建立连接,是为了在客户端和服务端维护连接,而建立一定的数据结构来维护双方交互的状态,用这样的数据结构来保证所谓的面向连接的特性
  • TCP 提供可靠交付
    通过 TCP 连接传输的数据,无差错、不丢失、不重复、并且按序到达。
  • TCP 是面向字节流
    发送的时候发的是一个流,没头没尾。
  • 拥塞控制
    TCP意识到包丢弃了或者网络的环境不好了,就会根据情况调整自己的行为,看看是不是发快了,要不要发慢点。

TCP 优点

  • 可靠,稳定
    TCP 的可靠体现在 TCP 在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制,在数据传完后,还会断开连接用来节约系统资源。

TCP 缺点

慢,效率低,占用系统资源高,易被攻击
TCP 在传递数据之前,要先建连接,这会消耗时间,而且在数据传递时,确认机制、重传机制、拥塞控制机制等都会消耗大量的时间,而且要在每台设备上维护所有的传输连接,事实上,每个连接都会占用系统的 CPU、内存等硬件资源。 而且,因为 TCP 有确认机制、三次握手机制,这些也导致 TCP 容易被人利用,实现 DOS、DDOS、CC 等攻击。

TCP的数据包格式

TCP数据包格式

  1. 源端口号和目标端口号,用于确定数据应该发给哪个应用;
  2. 包序号
    用于解决乱序问题;
  3. 确认序号
    解决丢包问题,如果没有收到就重新发送,直到送达。
  4. 状态位
    例如SYN是发起一个连接;
    ACK是回复;
    RST是重新连接;
    FIN是结束连接。
  5. 窗口大小
    TCP要做到流量控制,通信双方各声明一个窗口,标识自己当前的处理能力。

连接的建立 - 三次握手

TCP三次握手
一开始,客户端和服务端都处于 CLOSED 状态。先是服务端主动监听某个端口,处于 LISTEN 状态。然后客户端主动发起连接 SYN,之后处于 SYN-SENT 状态。服务端收到发起的连接,返回 SYN,并且 ACK 客户端的 SYN,之后处于 SYN-RCVD 状态。客户端收到服务端发送的 SYN 和 ACK 之后,发送 ACK 的 ACK,之后处于 ESTABLISHED 状态,因为它一发一收成功了。服务端收到 ACK 的 ACK 之后,处于 ESTABLISHED 状态,因为它也一发一收了。
从连接建立的流程中可见,三次握手除了双方建立连接外,还能确定TCP包的序号问题。

总而言之:

  1. 首先 Client 端发送连接请求报文,Server 段接受连接后回复 ACK 报文,并为这次连接分配资源。
  2. Client 端接收到 ACK 报文后也向 Server 段发生 ACK 报文,并分配资源,这样 TCP 连接就建立了。
    TCP三次握手
    最初两端的 TCP 进程都处于 CLOSED 关闭状态,A(Client)主动打开连接,而 B(Server)被动打开连接。(A、B 关闭状态 CLOSED——B 创建 TCB,进入 LISTEN 状态,等待 A 请求——A 同步已发送状态 SYN-SENT——B 同步收到状态 SYN-RCVD——A、B 连接已建立状态 ESTABLISHED)
  3. 第一次握手:起初两端都处于 CLOSED 关闭状态,A(Client)将标志位 SYN 置为 1,随机产生一个值 seq=x,并将该数据包发送给 B(Server),A(Client)进入 SYN-SENT 状态,等待 B(Server)确认;
  4. 第二次握手:B(Server)收到连接请求报文段后,如同意建立连接,则向 A(Client)发送确认,在确认报文段中(SYN=1,ACK=1,确认号 ack=x+1,初始序号 seq=y),B(Server)TCP 服务器进程进入 SYN-RCVD(同步收到)状态;
  5. 第三次握手:TCP 客户进程收到 B(Server)的确认后,要向 B(Server)给出确认报文段(ACK=1,确认号 ack=y+1,序号 seq=x+1)(初始为 seq=x,第二个报文段所以要+1),ACK 报文段可以携带数据,不携带数据则不消耗序号。TCP 连接已经建立,A 进入 ESTABLISHED(已建立连接)。
    当 B 收到 A 的确认后,也进入 ESTABLISHED 状态。

    TCB 传输控制块 Transmission Control Block,存储每一个连接中的重要信息,如 TCP 连接表,到发送和接收缓存的指针,到重传队列的指针,当前的发送和接收序号。

一些问题:

  1. 为什么 A 还要发送一次确认呢?可以二次握手吗?
    主要为了防止已失效的连接请求报文段突然又传送到了 B,因而产生错误。如 A 发出连接请求,但因连接请求报文丢失而未收到确认,于是 A 再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接,A 发出了两个连接请求报文段,其中第一个丢失,第二个到达了 B,但是第一个丢失的报文段只是在某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到达 B,此时 B 误认为 A 又发出一次新的连接请求,于是就向 A 发出确认报文段,同意建立连接,不采用三次握手,只要 B 发出确认,就建立新的连接了,此时 A 不理睬 B 的确认且不发送数据,则 B 一致等待 A 发送数据,浪费资源。
  2. Server 端易受到 SYN 攻击?
    服务器端的资源分配是在二次握手时分配的,而客户端的资源是在完成三次握手时分配的,所以服务器容易受到 SYN 洪泛攻击,SYN 攻击就是 Client 在短时间内伪造大量不存在的 IP 地址,并向 Server 不断地发送 SYN 包,Server 则回复确认包,并等待 Client 确认,由于源地址不存在,因此 Server 需要不断重发直至超时,这些伪造的 SYN 包将长时间占用未连接队列,导致正常的 SYN 请求因为队列满而被丢弃,从而引起网络拥塞甚至系统瘫痪。
    防范 SYN 攻击措施:降低主机的等待时间使主机尽快的释放半连接的占用,短时间受到某 IP 的重复 SYN 则丢弃后续请求。

连接的断开 - 四次挥手

TCP四次挥手
假设 Client 端发起中断连接请求,也就是发送 FIN 报文。Server 端接到 FIN 报文后,意思是说”我 Client 端没有数据要发给你了”,但是如果你还有数据没有发送完成,则不必急着关闭 Socket,可以继续发送数据。所以你先发送 ACK,”告诉 Client 端,你的请求我收到了,但是我还没准备好,请继续你等我的消息”。这个时候 Client 端就进入 FIN_WAIT 状态,继续等待 Server 端的 FIN 报文。当 Server 端确定数据已发送完成,则向 Client 端发送 FIN 报文,”告诉 Client 端,好了,我这边数据发完了,准备好关闭连接了”。Client 端收到 FIN 报文后,”就知道可以关闭连接了,但是他还是不相信网络,怕 Server 端不知道要关闭,所以发送 ACK 后进入 TIME_WAIT 状态,如果 Server 端没有收到 ACK 则可以重传。“,Server 端收到 ACK 后,”就知道可以断开连接了”。Client 端等待了 2MSL 后依然没有收到回复,则证明 Server 端已正常关闭,那好,我 Client 端也可以关闭连接了。Ok,TCP 连接就这样关闭了!

数据传输结束后,通信的双方都可释放连接,A 和 B 都处于 ESTABLISHED 状态。(A、B 连接建立状态 ESTABLISHED——A 进入等待 1 状态 FIN-WAIT-1——B 关闭等待状态 CLOSE-WAIT——A 进入等待 2 状态 FIN-WAIT-2——B 最后确认状态 LAST-ACK——A 时间等待状态 TIME-WAIT——B、A 关闭状态 CLOSED)

  1. A 的应用进程先向其 TCP 发出连接释放报文段(FIN=1,序号 seq=u),并停止再发送数据,主动关闭 TCP 连接,进入 FIN-WAIT-1(终止等待 1)状态,等待 B 的确认。
  2. B 收到连接释放报文段后即发出确认报文段,(ACK=1,确认号 ack=u+1,序号 seq=v),B 进入 CLOSE-WAIT(关闭等待)状态,此时的 TCP 处于半关闭状态,A 到 B 的连接释放。
  3. A 收到 B 的确认后,进入 FIN-WAIT-2(终止等待 2)状态,等待 B 发出的连接释放报文段。
  4. B 没有要向 A 发出的数据,B 发出连接释放报文段(FIN=1,ACK=1,序号 seq=w,确认号 ack=u+1),B 进入 LAST-ACK(最后确认)状态,等待 A 的确认。
  5. A 收到 B 的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),A 进入 TIME-WAIT(时间等待)状态。此时 TCP 未释放掉,需要经过时间等待计时器设置的时间 2MSL 后,A 才进入 CLOSED 状态。

总而言之:

  1. 刚开始A发送断开连接请求,进入FIN_WAIT_1状态;
  2. B接收请求进入CLOSED_WAIT状态,并发回响应;
  3. A接收B响应,进入FIN_WAIT_2状态;
  4. B发送断开连接请求,并进入LAST_ACK状态;
  5. A接收请求,进入TIME_WAIT状态,并发回响应;
    A会在发出响应2MSL后自动进入CLOSED状态。
    MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的,而 IP 头中有一个 TTL 域,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。协议规定 MSL 为 2 分钟,实际应用中常用的是 30 秒,1 分钟和 2 分钟等。
  6. B接收响应后进入CLOSED状态。

该流程中让人比较困惑的问题是:

  1. 为什么A断开连接后,第4步还要B在反向发起一次断开连接?
    不能直接断开,因为A单方面断开连接时,A不知道B是不是还有事情要处理。
  2. 如果第4步B没有重新发起断开连接(只有2次挥手),连接怎么断开?
    如果这期间B宕机了没有重新发起断开连接,A将永远停留在FIN_WAIT_2的状态,TCP 协议里面并没有对这个状态的处理,但是 Linux 有,可以调整 tcp_fin_timeout 这个参数,设置一个超时时间。
  3. 第5步如果A响应没有返回给B,B怎么断开?
    此时B会重新发起一次断开请求,因而TCP要求A最后等待一段时间TIME_WAIT,这个时间要足够长,长到如果B没有收到ACK,B可以重试且时间足够到达。
    不过,还有一个异常情况就是,B 超过了 2MSL 的时间,依然没有收到它发的 FIN 的 ACK,怎么办呢?按照 TCP 的原理,B 当然还会重发 FIN,这时A再收到这个包后,会直接返回RST,B就知道早已断开连接了。
  4. TIME_WAIT
    TIME_WAIT 状态容易被人误解,比如当使用三方系统的服务时,看到系统 TIME_WAIT 数量特别高,于是赖对方没有及时把连接释放掉,实际上TIME_WAIT 产生在主动断开连接的一方
  5. 为什么 A 在 TIME-WAIT 状态必须等待 2MSL 的时间?(MSL 最长报文段寿命 Maximum Segment Lifetime,MSL=2)
    原因有 2:
    保证 A 发送的最后一个 ACK 报文段能够到达 B。这个 ACK 报文段有可能丢失,使得处于 LAST-ACK 状态的 B 收不到对已发送的 FIN+ACK 报文段的确认,B 超时重传 FIN+ACK 报文段,而 A 能在 2MSL 时间内收到这个重传的 FIN+ACK 报文段,接着 A 重传一次确认,重新启动 2MSL 计时器,最后 A 和 B 都进入到 CLOSED 状态,若 A 在 TIME-WAIT 状态不等待一段时间,而是发送完 ACK 报文段后立即释放连接,则无法收到 B 重传的 FIN+ACK 报文段,所以不会再发送一次确认报文段,则 B 无法正常进入到 CLOSED 状态。
    防止“已失效的连接请求报文段”出现在本连接中。A 在发送完最后一个 ACK 报文段后,再经过 2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现这种旧的连接请求报文段。
  6. 为什么连接的时候是三次握手,关闭的时候却是四次握手?
    因为当 Server 端收到 Client 端的 SYN 连接请求报文后,可以直接发送 SYN+ACK 报文。其中 ACK 报文是用来应答的,SYN 报文是用来同步的。但是关闭连接时,当 Server 端收到 FIN 报文时,很可能并不会立即关闭 SOCKET,所以只能先回复一个 ACK 报文,告诉 Client 端,”你发的 FIN 报文我收到了”。只有等到我 Server 端所有的报文都发送完了,我才能发送 FIN 报文,因此不能一起发送。故需要四步握手。
  7. 为什么 TIME_WAIT 状态需要经过 2MSL(最大报文段生存时间)才能返回到 CLOSE 状态?
    虽然按道理,四个报文都发送完毕,我们可以直接进入 CLOSE 状态了,但是我们必须假象网络是不可靠的,有可能最后一个 ACK 丢失。所以 TIME_WAIT 状态就是用来重发可能丢失的 ACK 报文。
  8. 如何优化
    我们可以通过修改系统参数来优化服务器
    tcp_tw_reuse: 是否重用处于 TIME_WAIT 状态的 TCP 链接 (设为 true)
    tcp_max_tw_buckets: 处于 TIME_WAIT 状态的 SOCKET 最大数目 (调大,这个参数千万不要调小了)
    tcp_fin_timeout: 处于 FIN_WAIT_2 的时间 (调小)

TCP状态机

上面的握手和挥手流程汇总为状态机如下图所示:
TCP状态机

累计确认(cumulative acknowledgment,或称为累计应答)

TCP中并不是发1收1,而是使用一个缓冲区保存数据包,这个缓冲区称为窗口,发送端的发送窗口如下图所示:
TCP发送窗口

  1. 发送了并且已经确认的。
  2. 发送了并且尚未确认的。
  3. 没有发送,但是已经等待发送的。
  4. 没有发送,并且暂时还不会发送的。
  • LastByteAcked:第一部分和第二部分的分界线
  • LastByteSent:第二部分和第三部分的分界线
  • LastByteAcked + AdvertisedWindow:第三部分和第四部分的分界线

为什么会有第3和第4部分?难道这些不都是等待发送的吗?其实区分第3和第4部分的主要目的是流量控制,在 TCP 里,接收端会给发送端报一个窗口的大小,叫 Advertised window。这个窗口的大小应该等于上面的第2部分加上第3部分,超过了这个窗口的接收端处理不过来,就不发送了,作为第4部分。

接收端同样也有一个接收窗口:
TCP接收窗口

  1. 接受并且确认过的。
  2. 还没接收,但是马上就能接收的。
  3. 还没接收,也没法接收的。
  • MaxRcvBuffer:最大缓存的量;
  • LastByteRead 之后是已经接收了,但是还没被应用层读取的;
  • NextByteExpected 是第一部分和第二部分的分界线。

顺序问题与丢包问题

TCP中的重发有两种:

  1. 超时重试
    对每一个发送了,但是没有 ACK 的包,都有设一个定时器,超过了一定的时间,就重新尝试。
    这个时间不宜过短,时间必须大于往返时间 RTT,否则会引起不必要的重传。也不宜过长,这样超时时间变长,访问就变慢了。估计往返时间,需要 TCP 通过采样 RTT 的时间,然后进行加权平均,算出一个值,而且这个值还是要不断变化的,因为网络状况不断地变化。除了采样 RTT,还要采样 RTT 的波动范围,计算出一个估计的超时时间。由于重传时间是不断变化的,我们称为自适应重传算法(Adaptive Retransmission Algorithm)
    超时间隔加倍:每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。
  2. 快速重传
    超时重传存在的问题是超时周期可能会比较长,为了加快重传,TCP采用一种快速重传机制:
    当接收方收到一个序号大于下一个所期望的报文段时,就会检测到数据流中的一个间隔,于是它就会发送冗余的 ACK,仍然 ACK 的是期望接收的报文段。而当客户端收到三个冗余的 ACK 后,就会在定时器过期之前,重传丢失的报文段。
    例如,接收方发现 6 收到了,8 也收到了,但是 7 还没来,那肯定是丢了,于是发送 6 的 ACK,要求下一个是 7。接下来,收到后续的包,仍然发送 6 的 ACK,要求下一个是 7。当客户端收到 3 个重复 ACK,就会发现 7 的确丢了,不等超时,马上重发。
  3. Selective Acknowledgment (SACK)
    接收方可以将缓存的地图发送给发送方,例如发送ACK6、SACK8,SACK8表示已接收但未处理的,有了地图,发送方一下子就能看出来是 7 丢了。

流量控制

当发送未确认窗口中最早的一个包接收到了确认,则窗口将前移一格:
TCP接收窗口
上图中,5接收到了确认,此时窗口前移一格,第14个包可以发送了。
TCP接收窗口移动
如果接收方处理太慢,导致缓存中没有空间了,可以通过确认信息修改窗口的大小,甚至可以设置为 0,则发送方将暂时停止发送。
假设一个极端情况,接收端的应用一直不读取缓存中的数据,当数据包 6 确认后,窗口大小就不能再是 9 了,就要缩小一个变为 8。
那么缩小后什么时候恢复呢?发送方会定时发送窗口探测数据包,看是否有机会调整窗口的大小。

拥塞控制

拥塞控制也是通过控制窗口大小来实现的,前面的滑动窗口 rwnd 是怕发送方把接收方缓存塞满,而拥塞窗口 cwnd,是怕把网络塞满。

TCP的拥塞控制主要是为了避免丢包和超时重传问题:

  • 如果窗口不经控制——像UDP那样——很有可能发送的数据量超过中间设备的承载能力,多出来的包就会被丢弃,即发生了丢包,这是我们不希望看到的。

  • 如果在这些设备上加缓存,处理不过来的先保存在缓存队列里,这样虽然不会丢失,但是会增加时延,如果时延达到一定程度,就会导致超时重传

  • 慢启动(指数增长)
    刚开始不清楚网络情况,因此发送数据包时一次只能发1个,后来按2、4、8的指数性增长速度来增长;

  • 线性增长
    当超过一个阈值ssthresh=65535时,可能速度达到了网络性能,这时会慢下来,变成线程增长,每收到一个确认后,cwnd才会增加1/cwnd

  • 指数递减
    当发生了丢包,需要超时重传时,会设置ssthresh=cwnd/2,并将cwnd设置为1,重新开始慢启动,这种减速方式的问题是太过激进,从原来的高速马上减到1,会造成明显的网络卡顿,因为这个问题,一般会采用快速重传算法

  • 快速重传算法
    当接收端发现丢了一个中间包的时候,发送三次前一个包的 ACK,于是发送端就会快速地重传,不必等待超时再重传。
    TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,cwnd 减半为 cwnd/2,然后 sshthresh = cwnd,当三个包返回的时候,cwnd = sshthresh + 3,也就是说没有立刻减到1。

0%