2024年1月

一、简介

Socket.D 是一种二进制字节流传输协议,位于 OSI 模型中的5~6层,底层可以依赖 TCP、UDP、KCP、WebSocket 等传输层协议。由 Noear 开发。支持异步流处理。其开发背后的动机是用开销更少的协议取代超文本传输协议(HTTP),HTTP 协议对于许多任务(如微服务通信)来说效率低下。

二、设计目标

  • 协议接口丰富,包括 Send, SendAsRequest, SendAndSubscribe,Reply,ReplyEnd
  • 支持应用层流量控制
  • 支持单连接双向、多次复用
  • 支持断连后自动重连
  • 可以更好的使用 WebSocket 协议

三、消息驱动

网络通信是异步的,Socket.D 协议包含这一点,并将所有通信建模为在单个网络连接(TCP)上的、多路复用的消息流,在等待响应时从不同步阻塞。

响应式宣言指出:

反应式系统依赖异步的消息传递,从而确保了松耦合、隔离、位置透明的组件之间有着明确边界。 这一边界还提供了将失败作为消息委托出去的手段。 使用显式的消息传递,可以通过在系统中塑造并监视消息流队列, 并在必要时应用回压, 从而实现负载管理、 弹性以及流量控制。 使用位置透明的消息传递作为通信的手段, 使得跨集群或者在单个主机中使用相同的结构成分和语义来管理失败成为了可能。 非阻塞的通信使得接收者可以只在活动时才消耗资源, 从而减少系统开销。

此外,HTTP/2 FAQ 很好地解释了在持久连接上采用多路复用的面向消息的协议的动机:

  • HTTP/1.x 有一个叫做 “head-of-line blocking(队头阻塞)” 的问题,在这种情况下,即在一个连接上一次只能有一个未完成请求。

  • HTTP/1.1 试图通过流水线(Pipelining)来解决这个问题,但它并没有完全解决这个问题(大的或慢的响应仍然会阻塞后面的其他响应)。此外,人们发现流水线很难部署,因为许多代理和服务器不能正确地处理它。
    在HTTP/1中使用并发连接和域名分片来缓解HOL问题

  • 并发连接,浏览器针对每个源(域名)可以打开4-8个TCP连接,提升并发度。

  • 域名分片,浏览器和HTTP/1限制了并发连接的数量,那么就把多个域名指向一台服务器,从而提升连接数量。
    这迫使客户端使用一些启发式方法(通常是猜测)来确定什么请求在什么时候放在与源站的哪个连接上;由于加载一个页面的次数通常是可用连接数量的10倍或者更多。这会导致被阻塞的请求“瀑布式”的增长,从而严重的影响性能。

多路复用通过允许多个请求和响应消息在一个连接上同时传输来解决这些问题;甚至可以将一条消息的部分与另一条消息的部分混合在一起。

使用HTTP/1,浏览器为每个源打开4-8个连接,由于许多站点使用多个源,这可能意味着打开单个页面要加载30多个TCP连接。

一个应用程序同时打开如此多的连接,打破了TCP所建立的许多假设;由于每个连接都会在响应中传输大量的数据,因此TCP缓冲区很大可能会溢出,从而导致拥塞事件和超时重传。

一个应用程序同时打开如此多的连接,此外,使用如此多的连接不公平地垄断了网络资源,“窃取”了其他性能更好的应用程序(如VoIP)的资源。

四、协议交互模型

不合适的协议会增加系统开发的成本。它可能是一个不匹配的抽象,但是我们必须将系统设计强加到他允许的交互模型中。这迫使开发人员花费额外的时间来解决它的缺点,以处理错误并获得可接受的性能。在多语言环境中,这个问题被放大了,因为不同的语言将使用不同的方法来解决这个问题,这需要团队之间的额外协调。到目前为止,通信协议事实上的标准是HTTP,它只支持请求/响应的交互模式。在某些情况下,这可能不是最理想通信模型。

  • Send(只管发送,不要结果)

发完就不管是请求/响应的优化,在不需要响应时很有用。它允许显着的性能优化,不仅仅是通过跳过响应来节省网络使用,而且还可以减少客户端和服务器的处理时间,因为客户端不需要记录和等待请求关联的响应和取消请求。

此交互模型对于支持有损的用例非常有用,例如非关键事件日志记录、或者设备信息上报。

像这样使用:

clientSession.send("/demo", entity);
  • SendAsRquest(发送一个请求,并要求一个响应)

仍然支持标准请求/响应语义,并且仍有望代表 Socket.D 连接上的大多数请求。这些请求/响应交互可以被认为是优化的“只有 1 个响应的流”,并且是在单个连接上多路复用的异步消息。

消费者“等待”响应消息,所以它看起来像一个典型的请求/响应,但它从不同步阻塞。

就像 http 一样使用:

//同步等待
let response = clientSession.sendAndRequest("/demo", entity).await();

//异步回调
clientSession.sendAndRequest("/demo", entity).thenReply(response => {

});
  • SendAndSubscribe(发送一个订阅,可以接收多个答复)

从一个请求一个响应那儿延伸出来的是多个响应,它允许多条消息流回。将此视为“集合”或“列表”响应,但不是将所有数据作为单个响应返回,而是按顺序流回每个元素。

用例可能包括以下内容:获取视频列表,在目录中获取产品,逐行检索文件。

可能有点像 mq,还可能通过 range 指定数据区间(或者不指定):

//异步回调
clientSession.sendAndSubscribe("/demo", entity.range(0,5)).thenReply(reply => {
      if(reply.isEnd()){
          //如果需要识别最后一个?
      }else{
          //
      }
});

五、协议形式

  • 连接上传输的数据可称之为流,每个消息都会有一个 sid(流Id)
  • 流(Stream)由帧(Frame)组成
  • 帧(Frame)包含了流Id(Sid)、事件(Event)、元数据字符串(MetaString)及数据(Data)

帧码结构为:

[len:int][flag:int][sid:str(<64)][\n][event:str(<512)][\n][metaString:str(<4k)][\n][data:byte(<16m)]

Socket.D 是一个二进制协议,也就是说在一个 Socket.D 连接上传输的消息体对数据格式没有任何要求,应用程序可以为所欲为的压缩数据量的大小。

这样的二进制协议通常来说能给性能带来极大的提升,但是产生的代价是,网络中间件也会因为无法解读消息体中的数据,丧失了在对具体应用流量进行监控,日志和路由的能力。Socket.D 通过把每个消息体分成 metaString 和 data 的方式,在保证高效传输的前提下,提供了暴露少量元数据给网络中间件的能力。

  • data 一般作为应用本身需要传递的业务数据,采取自定义的高效序列化方式,且对网络基础设施不可见。
  • metaString 采用标准的 urlQueryString 格式。在分布式传输的过程中,这些中间件可以按需求对 metaString 进行读写,然后监控应用健康状况或者调整路由。

Socket.D 有哪些适用的场景?

  • 移动设备与服务器的连接
  • 数据双向传输,且支持流量控制。
  • 支持连接修复,比如手机进地铁之后,网络断开一段时间,其他协议需要重新建立连接,Socket.D 会自动修复连接。
  • 微服务场景

本章主要演示以下 cgroups 下各个 subsystem 的作用。

根据难易程度,依次演示了 pids 、cpu 和 memory 3 个 subsystem 的使用。

注:本文所有操作在 Ubuntu20.04 下进行。


如果你对云原生技术充满好奇,想要深入了解更多相关的文章和资讯,欢迎关注微信公众号。

搜索公众号【
探索云原生
】即可订阅


1. pids

pids subsystem 功能是限制 cgroup 及其所有子孙 cgroup 里面能创建的总的 task 数量

注意:这里的 task 指通过 fork 和 clone 函数创建的进程,由于 clone 函数也能创建线程(在 Linux 里面,线程是一种特殊的进程),所以这里的 task 也包含线程。

本文统一以进程来代表 task,即本文中的进程代表了进程和线程>

创建子 cgroup

创建子 cgroup,取名为 test

#进入目录/sys/fs/cgroup/pids/并新建一个目录,即创建了一个子cgroup
lixd  /home/lixd $ cd /sys/fs/cgroup/pids
lixd  /sys/fs/cgroup/pids $ sudo mkdir test

再来看看 test 目录下的文件

lixd  /sys/fs/cgroup/pids $ cd test
#除了上一篇中介绍的那些文件外,多了两个文件
 lixd  /sys/fs/cgroup/pids/test $ ls
cgroup.clone_children  cgroup.procs  notify_on_release  pids.current  pids.events  pids.max  tasks

下面是这两个文件的含义:

  • pids.current: 表示当前 cgroup 及其所有子孙 cgroup 中现有的总的进程数量
  • pids.max: 当前 cgroup 及其所有子孙 cgroup 中所允许创建的总的最大进程数量

限制进程数

首先是将当前 bash 加入到 cgroup 中,并修改
pids.max
的值,为了便于测试,这里就限制为 1:

#--------------------------第一个shell窗口----------------------
# 将当前bash进程加入到该cgroup
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/pids/test# echo $$ > cgroup.procs
#将pids.max设置为1,即当前cgroup只允许有一个进程
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/pids/test# echo 1 > pids.max

由于 bash 已经占用了一个进程,所以此时 bash 中已经无法创建新的进程了:

root@DESKTOP-9K4GB6E:/sys/fs/cgroup/pids/test# ls
bash: fork: retry: Resource temporarily unavailable
bash: fork: retry: Resource temporarily unavailable
bash: fork: retry: Resource temporarily unavailable

创建新进程失败,于是命令运行失败,说明限制生效。

打开另一个 shell 查看

 lixd  /mnt/c/Users/意琦行 $ cd /sys/fs/cgroup/pids/test
 lixd  /sys/fs/cgroup/pids/test $ ls
cgroup.clone_children  cgroup.procs  notify_on_release  pids.current  pids.events  pids.max  tasks
 lixd  /sys/fs/cgroup/pids/test $ cat pids.current
1

果然,pids.current 为 1,已经到 pids.max 的限制了。

当前 cgroup 和子 cgroup 之间的关系

当前 cgroup 中的
pids.current

pids.max
代表了当前 cgroup 及所有子孙 cgroup 的所有进程,所以子孙 cgroup 中的 pids.max 大小不能超过父 cgroup。

如果子 cgroup 中的 pids.max 设置的大于父 cgroup 里的值,会怎么样?

答案是子 cgroup 中的进程不光受子 cgroup 限制,还要受其父 cgroup 的限制。

#继续使用上面的两个窗口
#--------------------------第二个shell窗口----------------------
#将pids.max设置成2
dev@dev:/sys/fs/cgroup/pids/test$ echo 2 > pids.max
#在test下面创建一个子cgroup
dev@dev:/sys/fs/cgroup/pids/test$ mkdir subtest
dev@dev:/sys/fs/cgroup/pids/test$ cd subtest/
#将subtest的pids.max设置为5
dev@dev:/sys/fs/cgroup/pids/test/subtest$ echo 5 > pids.max
#将当前bash进程加入到subtest中
dev@dev:/sys/fs/cgroup/pids/test/subtest$ echo $$ > cgroup.procs
#--------------------------第三个shell窗口----------------------
#重新打开一个bash窗口,看一下test和subtest里面的数据
#test里面的数据如下:
dev@dev:~$ cd /sys/fs/cgroup/pids/test
dev@dev:/sys/fs/cgroup/pids/test$ cat pids.max
2
#这里为2表示目前test和subtest里面总的进程数为2
dev@dev:/sys/fs/cgroup/pids/test$ cat pids.current
2
dev@dev:/sys/fs/cgroup/pids/test$ cat cgroup.procs
3083

#subtest里面的数据如下:
dev@dev:/sys/fs/cgroup/pids/test$ cat subtest/pids.max
5
dev@dev:/sys/fs/cgroup/pids/test$ cat subtest/pids.current
1
dev@dev:/sys/fs/cgroup/pids/test$ cat subtest/cgroup.procs
3185
#--------------------------第一个shell窗口----------------------
#回到第一个窗口,随便运行一个命令,由于test里面的pids.current已经等于pids.max了,
#所以创建新进程失败,于是命令运行失败,说明限制生效
dev@dev:/sys/fs/cgroup/pids/test$ ls
-bash: fork: retry: No child processes
-bash: fork: retry: No child processes
-bash: fork: retry: No child processes
-bash: fork: retry: No child processes
-bash: fork: Resource temporarily unavailable
#--------------------------第二个shell窗口----------------------
#回到第二个窗口,随便运行一个命令,虽然subtest里面的pids.max还大于pids.current,
#但由于其父cgroup “test”里面的pids.current已经等于pids.max了,
#所以创建新进程失败,于是命令运行失败,说明子cgroup中的进程数不仅受自己的pids.max的限制,还受祖先cgroup的限制
dev@dev:/sys/fs/cgroup/pids/test/subtest$ ls
-bash: fork: retry: No child processes
-bash: fork: retry: No child processes
-bash: fork: retry: No child processes
-bash: fork: retry: No child processes
-bash: fork: Resource temporarily unavailable

pids.current > pids.max 的情况

并不是所有情况下都是 pids.max >= pids.current,在下面两种情况下,会出现 pids.max < pids.current 的情况:

  • 设置 pids.max 时,将其值设置的比 pids.current 小
  • 将其他进程加入到当前 cgroup 有可能会导致 pids.current > pids.max
    • 因为 pids.max 只会在当前 cgroup 中的进程 fork、clone 的时候生效,将其他进程加入到当前 cgroup 时,不会检测 pids.max,所以可能触发这种情况

小结

作用
:pids subsystem 用于限制 cgroups 下能够创建的 task(进程和线程)数。

原理
:在调用 fork 和 clone 时对比 subsystem 中配置的 pids.max 和 pids.current 值来判断当前是否能够继续创建 task。

用法
:配置 pids.max 防止容器消耗完 pid。

2. cpu

在 cgroup 里面,跟 CPU 相关的子系统有
cpusets

cpuacct

cpu

  • 其中 cpuset 主要用于设置 CPU 的亲和性,可以限制 cgroup 中的进程只能在指定的 CPU 上运行,或者不能在指定的 CPU 上运行,同时 cpuset 还能设置内存的亲和性。设置亲和性一般只在比较特殊的情况才用得着,所以这里不做介绍。

  • cpuacct 包含当前 cgroup 所使用的 CPU 的统计信息,信息量较少,有兴趣可以去看看它的文档,这里不做介绍。

本节只介绍 cpu 子系统,包括怎么限制 cgroup 的 CPU 使用上限及相对于其它 cgroup 的相对值。

创建子 cgroup

通用是创建子目录即可。

#进入/sys/fs/cgroup/cpu并创建子cgroup
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/cpu# cd /sys/fs/cgroup/cpu
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/cpu# mkdir test
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/cpu# cd test/
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/cpu/test# ls
cgroup.clone_children  cpu.cfs_period_us  cpu.rt_period_us   cpu.shares  notify_on_release
cgroup.procs           cpu.cfs_quota_us   cpu.rt_runtime_us  cpu.stat    tasks

看起来文件比 memory subsystem 还是少一些。

cpu.cfs_period_us & cpu.cfs_quota_us
:两个文件配合起来设置 CPU 的使用上限,两个文件的单位都是微秒(us)。

  • cfs_period_us:用来配置时间周期长度
    • 取值范围为 1 毫秒(ms)到 1 秒(s)
  • cfs_quota_us:用来配置当前 cgroup 在设置的周期长度内所能使用的 CPU 时间数
    • 取值大于 1ms 即可
    • 默认值为 -1,表示不受 cpu 时间的限制。

cpu.shares
用来设置 CPU 的相对值(比例),并且是针对所有的 CPU(内核),默认值是 1024。

假如系统中有两个 cgroup,分别是 A 和 B,A 的 shares 值是 1024,B 的 shares 值是 512,那么 A 将获得 1024/(1204+512)=66% 的 CPU 资源,而 B 将获得 33% 的 CPU 资源。

shares 有两个特点:

  • 如果 A 不忙,没有使用到 66% 的 CPU 时间,那么剩余的 CPU 时间将会被系统分配给 B,即 B 的 CPU 使用率可以超过 33%
  • 如果添加了一个新的 cgroup C,且它的 shares 值是 1024,那么 A 的限额变成了 1024/(1204+512+1024)=40%,B 的变成了 20%

从上面两个特点可以看出:

  • 在闲的时候,shares 基本上不起作用,只有在 CPU 忙的时候起作用,这是一个优点。
  • 由于 shares 是一个绝对值,需要和其它 cgroup 的值进行比较才能得到自己的相对限额,而在一个部署很多容器的机器上,cgroup 的数量是变化的,所以这个限额也是变化的,自己设置了一个高的值,但别人可能设置了一个更高的值,所以这个功能没法精确的控制 CPU 使用率。

cpu.stat
包含了下面三项统计结果:

  • nr_periods: 表示过去了多少个 cpu.cfs_period_us 里面配置的时间周期
  • nr_throttled: 在上面的这些周期中,有多少次是受到了限制(即 cgroup 中的进程在指定的时间周期中用光了它的配额)
  • throttled_time: cgroup 中的进程被限制使用 CPU 持续了多长时间(纳秒)

原理

前面配置的参数都是
cfs_xxx
,这里的 cfs 是 Completely Fair Scheduler 的缩写。

CFS 是 Linux 内核中的调度器,它负责决定哪个进程在给定时间片内运行
。CFS 使用 CFS 配额(
cpu.cfs_quota_us
)和 CFS 周期(
cpu.cfs_period_us
)来限制每个 cgroup 中的 CPU 使用。

CFS 的实现与 cgroups 协同工作,它负责追踪每个 cgroup 中的进程消耗的 CPU 时间,并在每个调度周期结束时根据 cgroup 的 CPU 配额调整进程的运行时间。

如果一个 cgroup 中的进程在调度周期内超过了它的 CPU 配额,它将被调度器限制,从而实现了 CPU 的使用限制。

即:cgroups 中的 subsystem 负责提供配置,cfs 负责记录进程使用的 cpu 时间,达到阈值后就从调度层面进行限制,避免该进程继续使用 cpu。

演示

#继续使用上面创建的子cgroup: test
#设置只能使用1个cpu的20%的时间
dev@ubuntu:/sys/fs/cgroup/cpu,cpuacct/test$ sudo sh -c "echo 50000 > cpu.cfs_period_us"
dev@ubuntu:/sys/fs/cgroup/cpu,cpuacct/test$ sudo sh -c "echo 10000 > cpu.cfs_quota_us"

#将当前bash加入到该cgroup
dev@ubuntu:/sys/fs/cgroup/cpu,cpuacct/test$ echo $$
5456
dev@ubuntu:/sys/fs/cgroup/cpu,cpuacct/test$ sudo sh -c "echo 5456 > cgroup.procs"

#在bash中启动一个死循环来消耗cpu,正常情况下应该使用100%的cpu(即消耗一个内核)
dev@ubuntu:/sys/fs/cgroup/cpu,cpuacct/test$ while :; do echo test > /dev/null; done

#--------------------------重新打开一个shell窗口----------------------
#通过top命令可以看到5456的CPU使用率为20%左右,说明被限制住了
#不过这时系统的%us+%sy在10%左右,那是因为我测试的机器上cpu是双核的,
#所以系统整体的cpu使用率为10%左右
dev@ubuntu:~$ top
Tasks: 139 total,   2 running, 137 sleeping,   0 stopped,   0 zombie
%Cpu(s):  5.6 us,  6.2 sy,  0.0 ni, 88.2 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :   499984 total,    15472 free,    81488 used,   403024 buff/cache
KiB Swap:        0 total,        0 free,        0 used.   383332 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 5456 dev       20   0   22640   5472   3524 R  20.3  1.1   0:04.62 bash

#这时可以看到被限制的统计结果
dev@ubuntu:~$ cat /sys/fs/cgroup/cpu,cpuacct/test/cpu.stat
nr_periods 1436
nr_throttled 1304
throttled_time 51542291833
# cfs_period_us 值为 10W
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/cpu/test# cat cpu.cfs_period_us
100000
# 往 cfs_quota_us 写入 20000,即限制只能使用20%cpu
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/cpu/test# echo 20000 > cpu.cfs_quota_us

# 新开一个窗口,运行一个死循环
$ while : ; do : ; done &
[1] 519
# top 看一下 cpu 占用率,果然是100%了

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
  519 lixd      25   5   13444   2912      0 R 100.0   0.0   0:05.66 zsh


# 回到第一个shell窗口,限制当前进程的cpu使用率
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/cpu/test# echo 519 >> cgroup.procs

# 再切回第二个窗口,发现519进程的cpu已经降到20%了,说明限制生效了
  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
  519 lixd      25   5   13444   2912      0 R  20.0   0.0   0:31.86 zsh

# 查看被限制的统计结果
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/cpu/test# cat cpu.stat
nr_periods 2090
nr_throttled 2088
throttled_time 166752684900

小结

作用:cpu subsystem 用于限制 cgroups 下进程可以使用的 cpu 上限。

原理:cgroups 中的 subsystem 负责提供配置,cfs 负责记录进程使用的 cpu 时间,达到阈值后就从调度层面进行限制,避免该进程继续使用 cpu。

用法:

  • 1)限制为具体值:用 cfs_period_us & cfs_quota_us 两个配置可以严格限制进程 cpu 使用量。
  • 2)按比例分配:用 shares 配置,可以使得多个 cgroups 之间按比例分配所有 cpu。

3. memory

memory subsystem 顾名思义,限制 cgroups 中进程的内存使用。

为什么需要内存控制

  • 站在一个普通开发者的角度,如果能控制一个或者一组进程所能使用的内存数,那么就算代码有 bug,内存泄漏也不会对系统造成影响,因为可以设置内存使用量的上限,当到达这个值之后可以将进程重启。
  • 站在一个系统管理者的角度,如果能限制每组进程所能使用的内存量,那么不管程序的质量如何,都能将它们对系统的影响降到最低,从而保证整个系统的稳定性。

内存控制能控制些什么?

  • 限 制 cgroup 中所有进程所能使用的物理内存总量
  • 限制 cgroup 中所有进程所能使用的物理内存+交换空间总量(CONFIG_MEMCG_SWAP): 一般在 server 上,不太会用到 swap 空间,所以不在这里介绍这部分内容。
  • 限制 cgroup 中所有进程所能使用的内核内存总量及其它一些内核资源(CONFIG_MEMCG_KMEM): 限制内核内存有什么用呢?其实限制内核内存就是限制当前 cgroup 所能使用的内核资源,比如进程的内核栈空间,socket 所占用的内存空间等,通过限制内核内存,当内存吃紧时,可以阻止当前 cgroup 继续创建进程以及向内核申请分配更多的内核资源。由于这块功能被使用的较少,本篇中也不对它做介绍。

创建子 cgroup

在 /sys/fs/cgroup/memory 下创建一个子目录就算是创建了一个子 cgroup

root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory# cd /sys/fs/cgroup/memory
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory# mkdir test
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory# ls test/
cgroup.clone_children           memory.kmem.tcp.max_usage_in_bytes  memory.oom_control
cgroup.event_control            memory.kmem.tcp.usage_in_bytes      memory.pressure_level
cgroup.procs                    memory.kmem.usage_in_bytes          memory.soft_limit_in_bytes
memory.failcnt                  memory.limit_in_bytes               memory.stat
memory.force_empty              memory.max_usage_in_bytes           memory.swappiness
memory.kmem.failcnt             memory.memsw.failcnt                memory.usage_in_bytes
memory.kmem.limit_in_bytes      memory.memsw.limit_in_bytes         memory.use_hierarchy
memory.kmem.max_usage_in_bytes  memory.memsw.max_usage_in_bytes     notify_on_release
memory.kmem.tcp.failcnt         memory.memsw.usage_in_bytes         tasks
memory.kmem.tcp.limit_in_bytes  memory.move_charge_at_immigrate

从上面 ls 的输出可以看出,除了每个 cgroup 都有的那几个文件外,和 memory 相关的文件还不少,这里先做个大概介绍(kernel 相关的文件除外),后面会详细介绍每个文件的作用:

 cgroup.event_control       #用于eventfd的接口
 memory.usage_in_bytes      #显示当前已用的内存
 memory.limit_in_bytes      #设置/显示当前限制的内存额度
 memory.failcnt             #显示内存使用量达到限制值的次数
 memory.max_usage_in_bytes  #历史内存最大使用量
 memory.soft_limit_in_bytes #设置/显示当前限制的内存软额度
 memory.stat                #显示当前cgroup的内存使用情况
 memory.use_hierarchy       #设置/显示是否将子cgroup的内存使用情况统计到当前cgroup里面
 memory.force_empty         #触发系统立即尽可能的回收当前cgroup中可以回收的内存
 memory.pressure_level      #设置内存压力的通知事件,配合cgroup.event_control一起使用
 memory.swappiness          #设置和显示当前的swappiness
 memory.move_charge_at_immigrate #设置当进程移动到其他cgroup中时,它所占用的内存是否也随着移动过去
 memory.oom_control         #设置/显示oom controls相关的配置
 memory.numa_stat           #显示numa相关的内存

添加进程

也是往 cgroup 中添加进程只要将进程号写入 cgroup.procs 就可以了。

#重新打开一个shell窗口,避免相互影响
root@DESKTOP-9K4GB6E:~# cd /sys/fs/cgroup/memory/test/
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# echo $$ >> cgroup.procs
#运行top命令,这样这个cgroup消耗的内存会多点,便于观察
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# top
# 后续操作不再在这个窗口进行,避免在这个bash中运行进程影响cgropu里面的进程数及相关统计

设置限额

设置限额很简单,将阈值写入 memory.limit_in_bytes 文件就可以了,例如:

  • echo 1M > memory.limit_in_bytes:限制只能用 1M 内存
  • echo -1 > memory.limit_in_bytes:-1 则是不限制
#回到第一个shell窗口
#开始设置之前,看看当前使用的内存数量,这里的单位是字节
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# cat memory.usage_in_bytes
2379776
#设置1M的限额
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# echo 1M > memory.limit_in_bytes
#设置完之后记得要查看一下这个文件,因为内核要考虑页对齐, 所以生效的数量不一定完全等于设置的数量
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# cat memory.usage_in_bytes
950272
#如果不再需要限制这个cgroup,写-1到文件memory.limit_in_bytes即可
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# echo -1 > memory.limit_in_bytes
#这时可以看到limit被设置成了一个很大的数字
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# cat memory.limit_in_bytes
9223372036854771712

如果设置的限额比当前已经使用的内存少呢?

root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# free -h
              total        used        free      shared  buff/cache   available
Mem:          7.7Gi       253Mi       7.4Gi       0.0Ki        95Mi       7.3Gi
Swap:         2.0Gi       0.0Ki       2.0Gi
# 此时用了 1232K
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# cat memory.usage_in_bytes
1232896
# 限制成500K
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# echo 500k > memory.limit_in_bytes
# 再次查看发现现在只用了401K
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# cat memory.usage_in_bytes
401408
# 发现swap多了1M,说明另外的数据被转移到swap上了
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# free -h
              total        used        free      shared  buff/cache   available
Mem:          7.7Gi       254Mi       7.4Gi       0.0Ki        94Mi       7.3Gi
Swap:         2.0Gi       1.0Mi       2.0Gi
#这个时候再来看failcnt,发现有381次之多(隔几秒再看这个文件,发现次数在增长)
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# cat memory.failcnt
381
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# cat memory.failcnt
385

#再看看memory.stat(这里只显示部分内容),发现物理内存用了400K,
#但有很多pgmajfault以及pgpgin和pgpgout,说明发生了很多的swap in和swap out
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# cat memory.stat
swap 946176 # 946K 差不多刚好是内存中少的量
pgpgin 30492
pgpgout 30443
pgfault 23859
pgmajfault 12507

从上面的结果可以看出,当物理内存不够时,就会触发 memory.failcnt 里面的数量加 1,但进程不会被 kill 掉,那是因为内核会尝试将物理内存中的数据移动到 swap 空间中,从而让内存分配成功。

如果设置的限额过小,就算 swap out 部分内存后还是不够会怎么样?

#--------------------------第一个shell窗口----------------------
# 限制到100k
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# echo 100K > memory.limit_in_bytes

#--------------------------第二个shell窗口----------------------
# 尝试执行 top 发现刚运行就被Kill了
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# top
Killed

从上面的这些测试可以看出,一旦设置了内存限制,将立即生效,并且当物理内存使用量达到 limit 的时候,memory.failcnt 的内容会加 1,但这时进程不一定就会

被 kill 掉,内核会尽量将物理内存中的数据移到 swap 空间上去,如果实在是没办法移动了(设置的 limit 过小,或者 swap 空间不足),默认情况下,就会 kill 掉 cgroup 里面继续申请内存的进程。

行为控制

通过修改
memory.oom_control
文件,可以控制 subsystem 在物理内存达到上限时的行为。文件中包含以下 3 个参数:

  • oom_kill_disable
    :是否启用 oom kill
    • 0:关闭
    • 1:开启
  • under_oom
    :表示当前是否已经进入 oom 状态,也即是否有进程被暂停了。
  • oom_kill
    :oom 后是否执行 kill
    • 1:启动,oom 后直接 kill 掉对应进程
    • 2:关闭:当内核无法给进程分配足够的内存时,将会暂停该进程直到有空余的内存之后再继续运行。同时会更新 under_oom 状态
    • 注意:root cgroup 的 oom killer 是不能被禁用的

为了演示 OOM-killer 的功能,创建了下面这样一个程序,用来向系统申请内存,它会每秒消耗 1M 的内存。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define MB (1024 * 1024)

int main(int argc, char *argv[])
{
    char *p;
    int i = 0;
    while(1) {
        p = (char *)malloc(MB);
        memset(p, 0, MB);
        printf("%dM memory allocated\n", ++i);
        sleep(1);
    }

    return 0;
}

保存上面的程序到文件
~/mem-allocate.c
,然后编译并测试

#--------------------------第一个shell窗口----------------------
#编译上面的文件
dev@dev:/sys/fs/cgroup/memory/test$ gcc ~/mem-allocate.c -o ~/mem-allocate
#设置内存限额为5M
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 5M > memory.limit_in_bytes"
#将当前bash加入到test中,这样这个bash创建的所有进程都会自动加入到test中
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo $$ >> cgroup.procs"
#默认情况下,memory.oom_control的值为0,即默认启用oom killer
dev@dev:/sys/fs/cgroup/memory/test$ cat memory.oom_control
oom_kill_disable 0
under_oom 0
#为了避免受swap空间的影响,设置swappiness为0来禁止当前cgroup使用swap
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 0 > memory.swappiness"
#当分配第5M内存时,由于总内存量超过了5M,所以进程被kill了
dev@dev:/sys/fs/cgroup/memory/test$ ~/mem-allocate
1M memory allocated
2M memory allocated
3M memory allocated
4M memory allocated
Killed

#设置oom_control为1,这样内存达到限额的时候会暂停
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 1 >> memory.oom_control"
#跟预期的一样,程序被暂停了
dev@dev:/sys/fs/cgroup/memory/test$ ~/mem-allocate
1M memory allocated
2M memory allocated
3M memory allocated
4M memory allocated

#--------------------------第二个shell窗口----------------------
#再打开一个窗口
dev@dev:~$ cd /sys/fs/cgroup/memory/test/
#这时候可以看到memory.oom_control里面under_oom的值为1,表示当前已经oom了
dev@dev:/sys/fs/cgroup/memory/test$ cat memory.oom_control
oom_kill_disable 1
under_oom 1
#修改test的额度为7M
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 7M > memory.limit_in_bytes"

#--------------------------第一个shell窗口----------------------
#再回到第一个窗口,会发现进程mem-allocate继续执行了两步,然后暂停在6M那里了
dev@dev:/sys/fs/cgroup/memory/test$ ~/mem-allocate
1M memory allocated
2M memory allocated
3M memory allocated
4M memory allocated
5M memory allocated
6M memory allocated

# 创建上面的文件并编译
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# vim ~/mem-allocate.c
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# gcc ~/mem-allocate.c -o ~/mem-allocate
# 限制5M的上限
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# echo 5M > memory.limit_in_bytes
#将当前bash加入到test中,这样这个bash创建的所有进程都会自动加入到test中
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# echo $$ >> cgroup.procs
#默认情况下,会启用oom killer
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# cat memory.oom_control
oom_kill_disable 0
under_oom 0
oom_kill 1
#为了避免受swap空间的影响,设置swappiness为0来禁止当前cgroup使用swap
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# echo 0 > memory.swappiness
#当分配第5M内存时,由于总内存量超过了5M,所以进程被kill了
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# ~/mem-allocate
1M memory allocated
2M memory allocated
3M memory allocated
4M memory allocated
Killed
#设置oom_control为1,这样内存达到限额的时候会暂停
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# echo 1 >> memory.oom_control
#跟预期的一样,程序被暂停了
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# ~/mem-allocate
1M memory allocated
2M memory allocated
3M memory allocated
4M memory allocated

#--------------------------第二个shell窗口----------------------
#再打开一个窗口
dev@dev:~$ cd /sys/fs/cgroup/memory/test/
#这时候可以看到memory.oom_control里面under_oom的值为1,表示当前已经oom了
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# cat memory.oom_control
oom_kill_disable 1
under_oom 1
oom_kill 2
#修改test的额度为7M
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# echo 7M > memory.limit_in_bytes

# 切换会第一个窗口,发送程序又跑了两步,停在了6M
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# ~/mem-allocate
1M memory allocated
2M memory allocated
3M memory allocated
4M memory allocated
5M memory allocated
6M memory allocated

其他

进程迁移(migration)

当一个进程从一个 cgroup 移动到另一个 cgroup 时,默认情况下,该进程已经占用的内存还是统计在原来的 cgroup 里面,不会占用新 cgroup 的配额,但新分配的内存会统计到新的 cgroup 中(包括 swap out 到交换空间后再 swap in 到物理内存中的部分)。

我们可以通过设置 memory.move_charge_at_immigrate 让进程所占用的内存随着进程的迁移一起迁移到新的 cgroup 中。

enable: echo 1 > memory.move_charge_at_immigrate
disable:echo 0 > memory.move_charge_at_immigrate

注意: 就算设置为 1,但如果不是 thread group 的 leader,这个 task 占用的内存也不能被迁移过去。

换句话说,如果以线程为单位进行迁移,必须是进程的第一个线程,如果以进程为单位进行迁移,就没有这个问题。

当 memory.move_charge_at_immigrate 被设置成 1 之后,进程占用的内存将会被统计到目的 cgroup 中,如果目的 cgroup 没有足够的内存,系统将尝试回收目的 cgroup 的部分内存(和系统内存紧张时的机制一样,删除不常用的 file backed 的内存或者 swap out 到交换空间上,请参考
Linux 内存管理
),如果回收不成功,那么进程迁移将失败。

注意:迁移内存占用数据是比较耗时的操作。

移除 cgroup

当 memory.move_charge_at_immigrate 为 0 时,就算当前 cgroup 中里面的进程都已经移动到其它 cgropu 中去了,由于进程已经占用的内存没有被统计过去,当前 cgroup 有可能还占用很多内存,当移除该 cgroup 时,占用的内存需要统计到谁头上呢?

答案是依赖 memory.use_hierarchy 的值,

  • 如果该值为 0,将会统计到 root cgroup 里;
  • 如果值为 1,将统计到它的父 cgroup 里面。

force_empty

当向 memory.force_empty 文件写入 0 时(echo 0 > memory.force_empty),将会立即触发系统尽可能的回收该 cgroup 占用的内存。该功能主要使用场景是移除 cgroup 前(cgroup 中没有进程),先执行该命令,可以尽可能的回收该 cgropu 占用的内存,这样迁移内存的占用数据到父 cgroup 或者 root cgroup 时会快些。

memory.swappiness

该文件的值默认和全局的 swappiness(/proc/sys/vm/swappiness)一样,修改该文件只对当前 cgroup 生效,其功能和全局的 swappiness 一样,请参考
Linux 交换空间
中关于 swappiness 的介绍。

注意:有一点和全局的 swappiness 不同,那就是如果这个文件被设置成 0,就算系统配置的有交换空间,当前 cgroup 也不会使用交换空间。

memory.use_hierarchy

该文件内容为 0 时,表示不使用继承,即父子 cgroup 之间没有关系;当该文件内容为 1 时,子 cgroup 所占用的内存会统计到所有祖先 cgroup 中。

如果该文件内容为 1,当一个 cgroup 内存吃紧时,会触发系统回收它以及它所有子孙 cgroup 的内存。

注意: 当该 cgroup 下面有子 cgroup 或者父 cgroup 已经将该文件设置成了 1,那么当前 cgroup 中的该文件就不能被修改。

#当前cgroup和父cgroup里都是1
dev@dev:/sys/fs/cgroup/memory/test$ cat memory.use_hierarchy
1
dev@dev:/sys/fs/cgroup/memory/test$ cat ../memory.use_hierarchy
1

#由于父cgroup里面的值为1,所以修改当前cgroup的值失败
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 0 > ./memory.use_hierarchy"
sh: echo: I/O error

#由于父cgroup里面有子cgroup(至少有当前cgroup这么一个子cgroup),
#修改父cgroup里面的值也失败
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 0 > ../memory.use_hierarchy"
sh: echo: I/O error

memory.soft_limit_in_bytes

有了 hard limit(memory.limit_in_bytes),为什么还要 soft limit 呢?hard limit 是一个硬性标准,绝对不能超过这个值。

而 soft limit 可以被超越,既然能被超越,要这个配置还有啥用?先看看它的特点

  • 1)当系统内存充裕时,soft limit 不起任何作用
  • 2)当系统内存吃紧时,系统会尽量的将 cgroup 的内存限制在 soft limit 值之下(内核会尽量,但不 100% 保证)

从它的特点可以看出,它的作用主要发生在系统内存吃紧时,如果没有 soft limit,那么所有的 cgroup 一起竞争内存资源,占用内存多的 cgroup 不会让着内存占用少的 cgroup,这样就会出现某些 cgroup 内存饥饿的情况。如果配置了 soft limit,那么当系统内存吃紧时,系统会让超过 soft limit 的 cgroup 释放出超过 soft limit 的那部分内存(有可能更多),这样其它 cgroup 就有了更多的机会分配到内存。

从上面的分析看出,这其实是系统内存不足时的一种妥协机制,给次等重要的进程设置 soft limit,当系统内存吃紧时,把机会让给其它重要的进程。

注意: 当系统内存吃紧且 cgroup 达到 soft limit 时,系统为了把当前 cgroup 的内存使用量控制在 soft limit 下,在收到当前 cgroup 新的内存分配请求时,就会触发回收内存操作,所以一旦到达这个状态,就会频繁的触发对当前 cgroup 的内存回收操作,会严重影响当前 cgroup 的性能。

memory.pressure_level

这个文件主要用来监控当前 cgroup 的内存压力,当内存压力大时(即已使用内存快达到设置的限额),在分配内存之前需要先回收部分内存,从而影响内存分配速度,影响性能,而通过监控当前 cgroup 的内存压力,可以在有压力的时候采取一定的行动来改善当前 cgroup 的性能,比如关闭当前 cgroup 中不重要的服务等。目前有三种压力水平:

  • low


    • 意味着系统在开始为当前 cgroup 分配内存之前,需要先回收内存中的数据了,这时候回收的是在磁盘上有对应文件的内存数据。
  • medium


    • 意味着系统已经开始频繁为当前 cgroup 使用交换空间了。
  • critical


    • 快撑不住了,系统随时有可能 kill 掉 cgroup 中的进程。

如何配置相关的监听事件呢?和 memory.oom_control 类似,大概步骤如下:

  1. 利用函数 eventfd(2) 创建一个 event_fd
  2. 打开文件 memory.pressure_level,得到 pressure_level_fd
  3. 往 cgroup.event_control 中写入这么一串:
    <event_fd> <pressure_level_fd> <level>
  4. 然后通过读 event_fd 得到通知

注意: 多个 level 可能要创建多个 event_fd,好像没有办法共用一个

Memory thresholds

我们可以通过 cgroup 的事件通知机制来实现对内存的监控,当内存使用量穿过(变得高于或者低于)我们设置的值时,就会收到通知。使用方法和 memory.oom_control 类似,大概步骤如下:

  1. 利用函数 eventfd(2) 创建一个 event_fd
  2. 打开文件 memory.usage_in_bytes,得到 usage_in_bytes_fd
  3. 往 cgroup.event_control 中写入这么一串:
    <event_fd> <usage_in_bytes_fd> <threshold>
  4. 然后通过读 event_fd 得到通知

stat file

这个文件包含的统计项比较细,需要一些内核的内存管理知识才能看懂,这里就不介绍了(怕说错)。详细信息可以参考
Memory Resource Controller
中的“5.2 stat file”。这里有几个需要注意的地方:

  • 里面 total 开头的统计项包含了子 cgroup 的数据(前提条件是 memory.use_hierarchy 等于 1)。
  • 里面的 'rss + file_mapped" 才约等于是我们常说的 RSS(ps aux 命令看到的 RSS)
  • 文件(动态库和可执行文件)及共享内存可以在多个进程之间共享,不过它们只会统计到他们的 owner cgroup 中的 file_mapped 去。(不确定是怎么定义 owner 的,但如果看到当前 cgroup 的 file_mapped 值很小,说明共享的数据没有算到它头上,而是其它的 cgroup)

小结

作用:限制 cgroups 中的进程占用的内存上限

用法:

  • 1)
    memory.limit_in_bytes
    配置进程可以使用的内存上限(hard limit),当超过该阈值时,一般是尝试使用 swap,如果不行则直接 kill 掉。
  • 2)
    memory.soft_limit_in_bytes
    配置进程可以使用的内存上行(soft limit),当系统内存不足时,cgroups 会优先将使用量超过 soft limit 的进程进行内存回收,腾出内存。
  • 3)
    memory.oom_control
    参数配置内存使用量到达阈值时内核的处理行为,默认为 oom_kill。

原理:当进程使用内存超过
memory.limit_in_bytes
之后,系统会根据
memory.oom_control
配置的行为进行处理,一般是尝试使用 swap,如果不行则直接 kill 掉。

本节没有介绍 swap 和 kernel 相关的内容,不过在实际使用过程中一定要留意 swap 空间,如果系统使用了交换空间,那么设置限额时一定要注意一点,那就是当 cgroup 的物理空间不够时,内核会将不常用的内存 swap out 到交换空间上,从而导致一直不触发 oom killer,而是不停的 swap out/in,导致 cgroup 中的进程运行速度很慢。

如果一定要用交换空间,最好的办法是限制 swap+物理内存 的额度,虽然我们在这篇中没有介绍这部分内容,但其使用方法和限制物理内存是一样的,只是换做写文件 memory.memsw.limit_in_bytes 罢了。

4. 小结

本文主要简单介绍了 pid、cpu、memory 这三个 subsystem 的作用和基本使用,具体如下:

subsystem 功能 用法 原理 备注
pid 限制 cgroups 中进程使用的 pid 数 配置 subsystem 中的 pids.max 即可 当 cgroups 中的进程调用 fork 或者 clone 系统调用时会判断,subsystem 中配置的 pids.max 和当前 pids.current 的值,来确定是否能够创建新的进程(或线程) linux 中的 pid 是有限的,通过该 subsystem 可以有效防止 fork 炸弹之类的恶意进程
cpu 限制 cgroups 中进程使用的 cpu 上限 1)限制为具体值:用 cfs_period_us & cfs_quota_us 两个配置可以严格限制进程 cpu 使用量。 2)按比例分配:用 shares 配置,可以使得多个 cgroups 之间按比例分配所有 cpu。 subsystem 负责提供配置,cfs 负责记录进程使用的 cpu 时间,达到阈值后就从调度层面进行限制,避免该进程继续使用 cpu。 一般使用 cfs_period_us & cfs_quota_us 方式限制具体值用得比较多。
memory 限制 cgroups 中进程使用的 memory 上限 1)
memory.limit_in_bytes
配置进程可以使用的内存上限(hard limit),当超过该阈值时,一般是尝试使用 swap,如果不行则直接 kill 掉。 2)
memory.soft_limit_in_bytes
配置进程可以使用的内存上行(soft limit),当系统内存不足时,cgroups 会优先将使用量超过 soft limit 的进程进行内存回收,腾出内存。 3)
memory.oom_control
参数配置内存使用量到达阈值时内核的处理行为,默认为 oom_kill。
当进程使用内存超过
memory.limit_in_bytes
之后,系统会根据
memory.oom_control
配置的行为进行处理,一般是尝试使用 swap,如果不行则直接 kill 掉。
如果系统使用了交换空间,那么设置限额时一定要注意一点,那就是当 cgroup 的物理空间不够时,内核会将不常用的内存 swap out 到交换空间上,从而导致一直不触发 oom killer,而是不停的 swap out/in,导致 cgroup 中的进程运行速度很慢。


如果你对云原生技术充满好奇,想要深入了解更多相关的文章和资讯,欢迎关注微信公众号。

搜索公众号【
探索云原生
】即可订阅


5. 参考

cgroups(7) — Linux manual page

美团技术团队---Linux 资源管理之 cgroups 简介

Red Hat---资源管理指南

目录:

  1. OpenID 与 OAuth2 基础知识
  2. Blazor wasm Google 登录
  3. Blazor wasm Gitee 码云登录
  4. Blazor wasm GitHub 登录
  5. Blazor wasm Facebook 登录
  6. Blazor IdentityServer 部署到服务器
  7. Blazor wasm OIDC 单点登录 (IDS)

源码

b21_OAuth_Gitee

相关基础知识

OpenID 与 OAuth2

参考阅读
https://auth0.com/docs/authenticate/protocols/openid-connect-protocol#what-is-openid-connect-oidc-

OpenID 连接协议

什么是 OpenID Connect (OIDC)?

OpenID Connect (OIDC) 是构建在OAuth 2.0框架之上的身份层。它允许第三方应用程序验证最终用户的身份并获取基本的用户配置文件信息。OIDC 使用JSON Web 令牌(JWT),您可以使用符合 OAuth 2.0 规范的流获取该令牌。看看我们的
OIDC 手册
更多细节。

OpenID 与 OAuth2

OAuth 2.0 涉及资源访问和共享,而 OIDC 涉及用户身份验证。其目的是让您一次登录多个站点。每次您需要使用 OIDC 登录网站时,您都会被重定向到您登录的 OpenID 站点,然后返回到该网站。例如,如果您选择使用 Google 帐户登录 Auth0,则您使用的是 OIDC。一旦您成功通过 Google 进行身份验证并授权 Auth0 访问您的信息,Google 就会将有关用户和所执行的身份验证的信息发送回 Auth0。此信息以 JWT 返回。您将收到一个访问令牌,如果需要,还会收到一个ID 令牌。

OpenID 和 JWT

JWT 包含索赔,它们是有关实体(通常是用户)和其他元数据的语句(例如姓名或电子邮件地址)。这
OpenID 连接规范定义了一组标准声明。该组标准声明包括姓名、电子邮件、性别、出生日期等。但是,如果您想要捕获有关用户的信息,并且当前没有最能反映此信息的标准声明,您可以创建自定义声明并将其添加到您的令牌中。

使用 OIDC 和 OAuth2 配置应用程序

您可以自动使用 OIDC 发现配置您的应用程序。

Auth0 中的应用程序

Auth0 中的术语“
应用程序
”或“app”并不暗示任何特定的实现特征。例如,它可以是在移动设备上执行的本机应用程序、在浏览器上执行的单页应用程序或在服务器上执行的常规 Web 应用程序。

Auth0 根据以下特征对应用程序进行分类:

应用程序类型
:要向您的应用程序添加身份验证,您必须在Auth0 仪表板中注册它并从以下应用程序类型之一中进行选择:

  • 常规 Web 应用程序
    :在服务器上执行大部分应用程序逻辑的传统 Web 应用程序(例如 Express.js 或 ASP.NET)。

  • 单页 Web 应用程序 (SPA)
    :在 Web 浏览器中执行大部分用户界面逻辑的 JavaScript 应用程序,主要使用 API(例如 AngularJS + Node.js 或 React)与 Web 服务器通信。

  • 本机应用程序
    :在设备(例如 iOS 或 Android)上本机运行的移动或桌面应用程序。

  • 机器对机器 (M2M) 应用程序
    :非交互式应用程序,例如命令行工具、守护程序、物联网设备或在后端运行的服务。通常,如果您有需要访问 API 的服务,则可以使用此选项。

凭证安全
:根据OAuth 2.0 规范,应用程序可以分为公共或机密;机密应用程序可以安全地保存凭据,而公共应用程序则不能。

所有权
:应用程序是否被归类为第一方或第三方取决于应用程序的所有权和控制权。第一方应用程序由拥有 Auth0 域的同一组织或个人控制。第三方应用程序使外部各方或合作伙伴能够安全地访问 API 背后的受保护资源。

单点登录

当用户登录到一个应用程序,然后自动登录到其他应用程序时,就会发生单点登录(SSO),无论用户使用的平台、技术或域如何。用户仅登录一次,因此该功能被称为“单点登录”。

例如,如果您登录 Gmail 等 Google 服务,您将自动通过 YouTube、AdSense、Google Analytics 和其他 Google 应用程序的身份验证。同样,如果您退出 Gmail 或其他 Google 应用程序,您也会自动退出所有应用程序;这称为单点注销。

SSO 在使用您的应用程序和服务时为用户提供无缝体验。用户无需记住每个应用程序或服务的单独凭据集,只需登录一次即可访问全套应用程序。

每当用户访问需要身份验证的域时,他们都会被重定向到可能要求登录的身份验证域。如果用户已经在该身份验证域登录,则可以立即重定向到原始域,而无需登录再次。

首次登录时 SSO

对于使用 Auth0 的 SSO,中央服务是 Auth0授权服务器。

让我们看一下用户首次登录时的 SSO 流程示例:

  1. 您的应用程序将用户重定向到登录页面。

  2. Auth0 检查是否存在现有的 SSO cookie。

  3. 由于这是用户第一次访问登录页面并且不存在 SSO cookie,因此系统将要求用户使用您已配置的连接之一进行登录。

  4. 用户登录后,Auth0 将设置一个 SSO cookie 并将用户重定向到您的应用程序,返回一个包含用户身份信息的 ID 令牌。

后续登录时进行 SSO

让我们看一下用户再次访问您的网站时的 SSO 流程示例:

  1. 您的应用程序将用户重定向到登录页面。

  2. Auth0 检查是否存在现有的 SSO cookie。

  3. Auth0 找到 SSO cookie,并在必要时更新它。不显示登录屏幕。

  4. Auth0 将用户重定向到您的应用程序,返回包含用户身份信息的 ID 令牌。

OpenID 连接

OpenID Connect (OIDC) 是一种常用于面向消费者的 SSO 实现的身份验证协议。OIDC 协议通过JSON Web 令牌和中央身份提供商处理身份验证。

与 OIDC 合作:

  1. 用户请求访问应用程序。

  2. 应用程序将用户重定向到身份提供商以进行身份​​验证。

  3. 身份提供者验证用户,如果成功,则提示用户授予对应用程序的数据访问权限。

  4. 如果授予访问权限,身份提供者将生成一个 ID 令牌,其中包含应用程序可以使用的用户身份信息。

  5. 身份提供者将用户返回到应用程序。

下一篇直接开始快速通关CV大法

互联网系统架构的演进

在互联网系统发展之初,系统比较简单,消耗资源小,用户访问量也比较少,我们只部署一个Tomcat应用就可以满足需求。系统架构图如下:

一个Tomcat可以看作是一个JVM进程,当大量的请求并发到达系统时,所有的请求都落在这唯一的一个Tomcat上,如果某些请求方法是需要加锁的,比如:秒杀扣减库存,是可以满足需求的,这和我们前面章节所讲的内容是一样的。但是随着访问量的增加,导致一个Tomcat难以支撑,这时我们就要集群部署Tomcat,使用多个Tomcat共同支撑整个系统。系统架构图如下:

上图中,我们部署了两个Tomcat,共同支撑系统。当一个请求到达系统时,首先会经过Nginx,Nginx主要是做负载转发的,它会根据自己配置的负载均衡策略将请求转发到其中的一个Tomcat中。当大量的请求并发访问时,两个Tomcat共同承担所有的访问量,这时,我们同样在秒杀扣减库存的场景中,使用单体应用锁,还能够满足要求吗?

单体应用锁的局限性

如上图所示,在整个系统架构中,存在两个Tomcat,每个Tomcat是 -个JVM。在进行秒杀业务的时候,由于大家都在抢购秒杀商品,大量的请求同时到达系统,通过Nginx分 发到两个Tomcat上。我们通过一个极端的案例场景, 可以更好地理解单体应用锁的局限性。假如,秒杀商品的数量只有1个,这时,这些大量的请求当中,只有一个请求可以成功的抢到这个商品,这就需要在扣减库存的方法.上加锁,扣减库存的动作只能一个一个去执行,而不能同时去执行如果同时执行,这1个商品可能同时被多个人抢到,从而产生超卖现象。加锁之后,扣减库存的动作一个一个去执行,凡是将库存扣减为负数的,都抛出异常,提示该用户没有抢到商品。通过加锁看似解决了秒杀的问题,但是事实,上真的是这样吗?

我们看到系统中存在两个Tomcat,我们加的锁是JDK提供的锁,这种锁只能在一个JVM下起作用,也就是在一个Tomcat内是没有问题的。当存在两个或两个以上的Tomcat时,大量的并发请求分散到不同的Tomcat_上,在每一一个Tomcat中都可以防止并发的产生,但是在多个Tomcat之间,每个Tomcat中获得锁的这个请求,又产生了并发从而产生超卖现象。这也就是单体应用锁的局限性,它只能在一个JVM内加锁,而不能从这个应用层面去加锁。

那么这个问题如何解决呢?这就需要使用分布式锁了,在整个应用层面去加锁。什么是分布式锁呢?我们怎么去使用分布式锁呢?

什么是分布式锁

在说分布式锁之前,我们看一看单体应用锁的特点,单体应用锁是在一个JVM进程内有效,无法跨JVM、跨进程。那么分布式锁的定义就出来了,分布式锁就是可以跨越多个JVM、跨越多个进程的锁,这种锁就叫做分布式锁。

分布式锁的设计思路

在上图中,由于Tomcat是由Java启动的,所以每个Tomcat可以看成一个JVM,JVM内部的锁是无法跨越多个进程的。所以,我们要实现分布式锁,我们只能在这些JVM之外去寻找,通过其他的组件来实现分布式锁。系统的架构如图所示:

两个Tomcat通过第三方的组件实现跨JVM、跨进程的分布式锁。这就是分布式锁的解决思路,找到所有JVM可以共同访问的第三方组件,通过第三方组件实现分布式锁。

目前存在的分布式的方案

分布式锁都是通过第三方组件来实现的,目前比较流行的分布式锁的解决方案有:

  • 数据库,通过数据库可以实现分布式锁,但是在高并发的情况下对数据库压力较大,所以很少使用。
  • Redis,借助Redis也可以实现分布式锁,而且Redis的Java客户端种类很多,使用的方法也不尽相同。
  • Zookeeper,Zookeeper也可以实现分布式锁,同样Zookeeper 也存在多个Java客户端,使用方法也不相同。

本人最近搭建了程序员专属的编程资料个人网站:
程序员波特
,主要记录Java相关技术系列教程,面试题的收集和整理等,让上班摸鱼的你,不再无聊。

分享是最有效的学习方式。

背景

这是张小帅失业之后的第三场面试。

面试官:“实际开发中用过多线程吧,那聊聊线程池吧”。

“有CachedThreadPool:可缓存线程池,FixedThreadPool:定长线程池.......balabala”。小帅暗暗窃喜,还好把这几种线程池背下来了,看来这次可以上岸了。

面试官点点头,继续问到“那线程池底层是如何实现复用的?”

“额,这个....”

寒风中,那个男人的背影在暮色中显得孤寂而凄凉,仿佛与世隔绝,独自面对着无尽的寂寞......

概要

如果问到线程池的话,不好好剖析过底层代码,恐怕真的会像小帅那样被问翻吧。

那么在此我们就来好好剖析一下线程池的底层吧。我们大概从如下几个方面着手:

概览图

什么是线程池

说到线程池,其实我们要先聊到池化技术。

池化技术:我们将资源或者任务放入池子,使用时从池中取,用完之后交给池子管理。通过优化资源分配的效率,达到性能的调优。

池化技术优点:

  1. 资源被重复使用,减少了资源在分配销毁过程中的系统的调度消耗。比如,在IO密集型的服务器上,并发处理过程中的子线程或子进程的创建和销毁过程,带来的系统开销将是难以接受的。所以在业务实现上,通常把一些资源预先分配好,如线程池,数据库连接池,Redis连接池,HTTP连接池等,来减少系统消耗,提升系统性能。

  2. 池化技术分配资源,会集中分配,这样有效避免了碎片化的问题。

  3. 可以对资源的整体使用做限制,相关资源预分配且只在预分配是生成,后续不再动态添加,从而限制了整个系统对资源的使用上限。

所以我们说线程池是提升线程可重复利用率、可控性的池化技术的一种。

线程池的使用

多线程发送邮件案例

现在我们有这样一个场景,上层有业务系统批量调用底层进行发送邮件,废话不多,直接上代码:

demo

最终运行输出结果为:

由线程:pool-1-thread-1 发送第:0封邮件
由线程:pool-1-thread-2 发送第:1封邮件
由线程:pool-1-thread-1 发送第:2封邮件
由线程:pool-1-thread-2 发送第:3封邮件
由线程:pool-1-thread-1 发送第:4封邮件
由线程:pool-1-thread-1 发送第:6封邮件
由线程:pool-1-thread-2 发送第:5封邮件
由线程:pool-1-thread-1 发送第:7封邮件
由线程:pool-1-thread-2 发送第:8封邮件
由线程:pool-1-thread-1 发送第:9封邮件

上面的例子中从结果来看是10封邮件分别由两条线程发送出去了,上图可见,我们给ThreadPoolExecutor这个执行器分别指定了七个参数。那么参数的含义到底是什么呢?接下来咱们层层抽丝剥茧。

构造函数说明

大家估计会有疑问,线程池的种类那么多,案例中为什么要用TheadPoolExecutor类呢,其他的种类是由TheadPoolExecutor通过不同的入参定义出来的,所以我们直接拿ThreadPoolExecutor来看。

我们先来看一下ThreadPoolExecutor的继承关系,有个宏观印象:

宏观继承

我们再来看一下ThreadPoolExecutor的构造方法:

构造方法

下面我们来解释一下几个参数的含义:

  1. corePoolSize: 核心线程数。

  2. maximumPoolSize: 最大线程数。

  3. keepAliveTime: 线程池中线程的最大闲置生命周期。

  4. unit: 针对keepAliveTime的时间单位。

  5. workQueue: 阻塞队列。

  6. threadFactory: 创建线程的线程工厂。

  7. handler: 拒绝策略。

大家对上述的含义初步有个概念。

工作流程概述

看了上面的构造函数字段大家估计也还是优点懵的,尤其是从来没有接触过商品池的小伙伴。所以老猫又撸了一张商品池的大概的工作流程图,方便大家把这些概念串起来。

大概流程

上图中老猫标记了四条线,简单介绍一下(当然上图若有问题,也希望大家能够指出来)。

  1. 当发起任务时候,会计算线程池中存在的线程数量与核心线程数量(corePoolSize)进行比较,如果小于,则在线程池中创建线程,否则,进行下一步判断。
  2. 如果不满足条件1,则会将任务添加到阻塞队列中。等待线程池中的线程空闲下来后,获取队列中的任务进行执行。
  3. 但是条件2中如果阻塞队列满了之后,此时又会重新获取当前线程的数量和最大线程数(maximumPoolSize)进行比较,如果发现小于最大线程数,那么继续添加到线程池中即可。
  4. 如果都不满足上述条件,那么此时会放到拒绝策略中。

execute核心流程剖析

接下来我们来看一下执行theadPoolExecutor.execute()的时候到底发生了什么。先来看一下源码:

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }
ctl变量

进入执行源码之后我们首先看到的是ctl,只知道ctl中拿到了一个int数据至于这个数值有什么用,目前不知道,接着看涉及的相关代码,老猫将相关的代码解读放到源码中进行注释。

    //通过ctl获取线程池的状态以及包含的线程数量
    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    private static final int COUNT_BITS = Integer.SIZE - 3;   // COUNT_BITS = 32-3 = 29
    /**001左移29位
     * 00100000 00000000 00000000 00000000
     * 操作减1
     * 00066666611 66666666666611 66666666666611 66666666666611(表示初始化的时候线程情况,1表示均有空闲线程)
     * 换成十进制:COUNT_MASK = 536870911
     */
    private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;
    /**
     * 运行中状态
     * 1的原码
     * 00000000 00000000 00000000 00000001
     * 取反+1
     * 66666666666611 66666666666611 66666666666611 66666666666611
     * 左移29位
     * 66666600000 00000000 00000000 00000000
     **/
    // runState is stored in the high-order bits
    private static final int RUNNING    = -1 << COUNT_BITS; //运行中状态  66666600000 00000000 00000000 00000000
    private static final int SHUTDOWN   =  0 << COUNT_BITS; //终止状态    00000000 00000000 00000000 00000000
    private static final int STOP       =  1 << COUNT_BITS; //停止       00100000 00000000 00000000 00000000
    private static final int TIDYING    =  2 << COUNT_BITS; //           01000000 00000000 00000000 00000000
    private static final int TERMINATED =  3 << COUNT_BITS; //           01100000 00000000 00000000 00000000
    
    //取高3位表示获取运行状态
    private static int runStateOf(int c)     { return c & ~COUNT_MASK; }  //~COUNT_MASK表示取反码:66666600000 00000000 00000000 00000000
    //取出低位29位的值,当前活跃的线程数
    private static int workerCountOf(int c)  { return c & COUNT_MASK; } //COUNT_MASK:00066666611 66666666666611 66666666666611 66666666666611
    //计算ctl的值,ctl=[3位]线程池状态 + [29位]线程池中线程数量。
    private static int ctlOf(int rs, int wc) { return rs | wc; } //进行或运算

上面我们针对各个状态以及那么多的二级制表示符有点懵,当然如果不会二进制运算的,大家可以先自己去了解一下二进制的运算逻辑。
通过源码中的英文,我们知道CTL的值其实分成两部分组成,高三位是状态,其余均为当先线程数。如下的图:

线程池状态

上面的图的描述解释,其实也都是英文注释版的翻译,我们再来看一下有了这些状态,这些状态是怎么流转的,英文注释是这样的:

/*** RUNNING -> SHUTDOWN
     *    On invocation of shutdown()
     * (RUNNING or SHUTDOWN) -> STOP
     *    On invocation of shutdownNow()
     * SHUTDOWN -> TIDYING
     *    When both queue and pool are empty
     * STOP -> TIDYING
     *    When pool is empty
     * TIDYING -> TERMINATED
     *    When the terminated() hook method has completed
     * /

上面的描述不太直观,老猫将流程串了起来,得到了下面的状态机流转图。如下图:

状态机流程

写到这里,其实ctl已经很清楚了,ctl说白了就是状态位和活跃线程数的表示方式。通过ctl咱们可以知道当前是什么状态以及活跃线程数量是多少
(设计很巧妙,如果此处还有问题,欢迎大家私聊老猫)。

线程池中的线程数小于核心线程数

读完ctl之后,我们来看一下接下来的代码。

if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true)) return; //添加新的线程
            c = ctl.get(); //重新获取当前的状态以及线程数量
}

继上述的workerCountOf,我们知道这个方法可以获取当前活跃的线程数。如果当前线程数小于配置的核心线程数,则会调用addWorker进行添加新的线程。
如果添加失败了,则重新获取ctl的值。

任务添加到队列的相关逻辑
if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            //再次check一下,当前线程池是否是运行状态,如果不是运行时状态,则把刚刚添加到workQueue中的command移除掉
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }

上述我们知道当添加线程池失败的时候,我们会重新获取ctl的值。
此时咱们的第一步就很清楚了:

  1. 通过isRunning方法来判断线程池状态是不是运行中状态,如果是,则将command任务放到阻塞队列workQueue中。
  2. 再次check一下,当前线程池是否是运行状态,如果不是运行时状态,则把刚刚添加到workQueue中的command移除掉,并调用拒绝策略。否则,判断如果当前活动的线程数如果为0,则表明只去创建线程,而此处,并不执行任务(因为,任务已经在上面的offer方法中被添加到了workQueue中了,等待线程池中的线程去消费队列中的任务)
线程池中的线程数量小于最大线程数代码逻辑以及拒绝策略的代码逻辑

接下来,我们看一下最后的一个步骤

/**
 * 进入第三步骤前提:
 * 1.线程池不是运行状态,所以isRunning(c)为false
 * 2.workCount >= corePoolSize的时候于此同时并且添加到queue失败的时候执行
 */
else if (!addWorker(command, false))
            reject(command);
    }

由于调用addWorker的第二个参数是false,则表示对比的是最大线程数,那么如果往线程池中创建线程依然失败,即addWorker返回false,那么则进入if语句中,直接调用reject方法调用拒绝策略了。

写到这里大家估计会对这个第二个参数是false为什么比较的是最大线程数有疑问。其实这个是addWorker中的方法。我们可以大概看一下:

private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (int c = ctl.get();;) {
            // Check if queue empty only if necessary.
            if (runStateAtLeast(c, SHUTDOWN)
                && (runStateAtLeast(c, STOP)
                    || firstTask != null
                    || workQueue.isEmpty()))
                return false;

            for (;;) {
                if (workerCountOf(c)
                    >= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))
                    return false;
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                c = ctl.get();  // Re-read ctl
                if (runStateAtLeast(c, SHUTDOWN))
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }
}

我们很明显地看到当core为flase的时候咱们获取的是maximumPoolSize,也就是最大线程数。

写到这里,其实咱们的核心主流程大概就已经结束了。这里其实老猫也只是写了一个算是比较入门的开头。当然我们还可以在深入去理addWorker的源码。这个其实就交给大家去细看了,篇幅过长,相信大家也会失去阅读的兴趣了,感兴趣的可以自己研究一下,如果说还是有问题的,可以找老猫一起探讨,老猫的公众号:"程序员老猫"。
老猫觉得在上述的源码中比较重要的其实就是ctl值的流转顺序以及计算方式,读懂这个的话,后面一切的源码只要树藤摸瓜即可理解。

Executors线程池模板

我们上述主要和大家分享了比较核心的theadPoolExecutor。除此之外,线程池Executors里面包含了很多其他的线程池模板。
当然这也是小猫直接面试的时候说的那些,其实小猫也就仅仅只是背了线程池模板而已,并不知晓其工作原理。
如下几种:

  1. newCachedThreadPool
    创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

  2. newFixedThreadPool
    创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

  3. newScheduledThreadPool
    创建一个定长线程池,支持定时及周期性任务执行。

  4. newSingleThreadScheduleExecutor
    创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期地执行。(注意,如果因为在关闭前的执行期间出现失败而终止了此单个线程,那么如果需要,一个新线程会代替它执行后续的任务)。可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。与其他等效的 newScheduledThreadPool(1) 不同,可保证无需重新配置此方法所返回的执行程序即可使用其他的线程。

  5. newSingleThreadExecutor
    创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

多样化的blockingQueue

  1. PriorityBlockingQueue
    它是一个无界的并发队列。无法向这个队列中插入null值。所有插入到这个队列中的元素必须实现Comparable接口。因此该队列中元素的排序就取决于你自己的Comparable实现。

  2. SynchronousQueue
    它是一个特殊的队列,它的内部同时只能够容纳单个元素。如果该队列已有一个元素的话,那么试图向队列中插入一个新元素的线程将会阻塞,直到另一个新线程将该元素从队列中抽走。同样的,如果队列为空,试图向队列中抽取一个元素的线程将会被阻塞,直到另一个线程向队列中插入了一条新的元素。因此,它其实不太像是一个队列,而更像是一个汇合点。

  3. ArrayBlockingQueue
    它是一个有界的阻塞队列,其内部实现是将对象放到一个数组里。一但初始化,大小就无法修改

  4. LinkedBlockingQueue
    它内部以一个链式结构(链接节点)对其元素进行存储。可以指定元素上限,否则,上限则为Integer.MAX_VALUE。

  5. DelayQueue
    它对元素进行持有直到一个特定的延迟到期。注意:进入其中的元素必须实现Delayed接口。

上述针对这些罗列了一下,其实很多官网上也有相关的介绍,当然感兴趣的小伙伴也可以再去刨一刨里面的源码实现。

拒绝策略

  1. AbortPolicy
    丢弃任务并抛出RejectedExecutionException异常。

  2. DiscardPolicy
    丢弃任务,但是不抛出异常。

  3. DiscardOldestPolicy
    丢弃队列中最前面的任务,然后重新尝试执行任务。

  4. CallerRunsPolicy
    由调用线程处理该任务。

总结

很多小伙伴在用一些线程池或者第三方中间件的时候可能只停留在如何使用上,一旦出了问题或者被人深入问到其实现原理的时候就比较头大。
所以在日常开发的过程中,我们不仅仅需要知道如何去用,其实更应该知道底层的原理是什么。这样才能长立于不败之地。老猫后续也计划出一些关于spring源码阅读系列的连载文章。希望和大家一起进步感兴趣的小伙伴可以加个关注。
公众号:“程序员老猫”

::: block-2
我是老猫,10Year+资深研发老鸟,让我们一起聊聊技术,聊聊人生。
如果有帮到你,求个点赞、关注、分享三连击,谢谢。
:::