最近工作总结(49)

2021/05/07 Work

###Feed流设计简记

存储

我们先来看中间黑色框中的部分,这部分是使用TableStore的数据,从左往右分别是:

  • 个人页Timeline:这个是每个用户的发件箱,也就是自己的个人页页面。
  • 关注页Timeline:这个是每个用户的收件箱,也就是自己的关注页页面,内容都是自己关注人发布的消息。
  • 关注列表:保存账号关系,比如朋友圈中的好友关系;微博中的关注列表等。
  • 虚拟关注列表:这个主要用来个性化和广告

发布Feed流程

当你发布一条Feed消息的时候,流程是这样的:

  1. Feed消息先进入一个队列服务。
  2. 先从关注列表中读取到自己的粉丝列表,以及判断自己是否是大V。
  3. 将自己的Feed消息写入个人页Timeline(发件箱)。如果是大V,写入流程到此就结束了。
  4. 如果是普通用户,还需要将自己的Feed消息写给自己的粉丝,如果有100个粉丝,那么就要写给100个用户,包括Feed内容和Feed ID。
  5. 第三步和第四步可以合并在一起,使用BatchWriteRow接口一次性将多行数据写入TableStore。
  6. 发布Feed的流程到此结束。
  • 对大V采用拉模式,普通用户使用推模式,这种模式有个缺点,后面会有分析。
  • 对活跃粉丝采用推模式,非活跃粉丝采用拉模式(这种方式可以较好的避免大流量对平台的冲击)

读取Feed流流程

当刷新自己的Feed流的时候,流程是这样的:

  1. 先去读取自己关注的大V列表
  2. 去读取自己的收件箱,只需要一个GetRange读取一个范围即可,范围起始位置是上次读取到的最新Feed的ID,结束位置可以使当前时间,也可以是MAX,建议是MAX值。由于之前使用了主键自增功能,所以这里可以使用GetRange读取。
  3. 如果有关注的大V,则再次并发读取每一个大V的发件箱,如果关注了10个大V,那么则需要10次访问。
  4. 合并2和3步的结果,然后按时间排序,返回给用户。

至此,使用推拉结合方式的发布,读取Feed流的流程都结束了

https://developer.aliyun.com/article/224132

正则表达式的性能优化简记

正则表达式的原理是有穷自动机(确定性有穷自动机(DFA) 非确定性有穷自动机(NFA))

在匹配过程中经常会引起回溯问题。大量的回溯会长时间地占用 CPU,从而带来系统性能开销。

如何优化性能:

  • 少用贪婪模式:多用贪婪模式会引起回溯问题,可以使用独占模式来避免回溯

  • 减少分支选择:分支选择类型 “(X Y Z)” 的正则表达式会降低性能,在开发的时候要尽量减少使用。如果一定要用,可以通过以下几种方式来优化:
    1. 考虑选择的顺序,将比较常用的选择项放在前面,使他们可以较快地被匹配;

    2. 可以尝试提取共用模式,例如,将 “(abcd abef)” 替换为 “ab(cd ef)” ,后者匹配速度较快,因为 NFA 自动机会尝试匹配 ab,如果没有找到,就不会再尝试任何选项;
    3. 如果是简单的分支选择类型,可以用三次 index 代替 “(X Y Z)” ,如果测试话,你就会发现三次 index 的效率要比 “(X Y Z)” 高一些
  • 减少捕获嵌套 :

    1. 捕获组是指把正则表达式中,子表达式匹配的内容保存到以数字编号或显式命名的数组中,方便后面引用。一般一个()就是一个捕获组,捕获组可以进行嵌套。

    2. 非捕获组则是指参与匹配却不进行分组编号的捕获组,其表达式一般由(?:exp)组成

https://www.cnblogs.com/huangrenhui/p/13893903.html

https://www.cnblogs.com/he1m4n6a/p/10256163.html

Rabbitmq死信队列存在的问题

如果使用在消息属性上设置TTL的方式,消息可能并不会按时“死亡“,因为RabbitMQ只会检查第一个消息是否过期,如果过期则丢到死信队列,索引如果第一个消息的延时时长很长,而第二个消息的延时时长很短,则第二个消息并不会优先得到执行

利用RabbitMQ插件实现延迟队列 上文中提到的问题,确实是一个硬伤,如果不能实现在消息粒度上添加TTL,并使其在设置的TTL时间及时死亡,就无法设计成一个通用的延时队列。 那如何解决这个问题呢?不要慌,安装一个插件即可:https://www.rabbitmq.com/community-plugins.html ,下载rabbitmq_delayed_message_exchange插件,然后解压放置到RabbitMQ的插件目录。 接下来,进入RabbitMQ的安装目录下的sbin目录,执行下面命令让该插件生效,然后重启RabbitMQ

JWT相对session的优势

session依赖缓存和数据库的存储,如果缓存和数据库挂了,很有可能导致用户无法登入

而JWT没有这种存储依赖,缓存和数据库挂了,在登入方面理论上依然可以正常进行

但jwt的缺点是:由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。

所以,要实现登出功能,还是需要存储JWT滴

time.Duration 作为timeout参数类型,别传整数

在一些方法需要传入超时参数值,超时参数的类型是 time.Duration,当你传比如30,这不是表示30s超时,而是30纳秒,这样你的client请求可能还没进行第一次握手就超时了,这时会收到

net/http: request canceled (Client.Timeout exceeded while awaiting headers)

的报错

正确使用方式参数值为: 30 * time.Second

Time Stamp Counter时间戳计数器

The Time Stamp Counter (TSC) 时间戳计数器TSC)是一个64位寄存器存在于所有86自处理器奔腾。它计算自复位以来的CPU周期数。该指令RDTSC以EDX:EAX返回TSC。在x86-64模式下,RDTSC还清除RAXRDX的高32位。是一种更高性能的基于CPU获取时间戳的方式。它记录了 CPU 供电重设后到当前时刻所经过的 CPU 时钟周期数。在 CPU 时钟周期速率相同的条件下,经过测量和换算即可用于高精度计时。对于需要大量获取时间戳的操作,比如分布式链式追踪(opentracing),日志打印等,如果能够使用这种方式获取时间戳信息,那么能够大大提升性能

https://en.wikipedia.org/wiki/Time_Stamp_Counter

https://github.com/dterei/gotsc

https://github.com/tikv/minitrace-go

https://www.jianshu.com/p/d57b12d18c98

###HTTPS实现原理 SSL建立连接过程

  1. client向server发送请求https://baidu.com,然后连接到server的443端口,发送的信息主要是随机值1和客户端支持的加密算法。
  2. server接收到信息之后给予client响应握手信息,包括随机值2和匹配好的协商加密算法,这个加密算法一定是client发送给server加密算法的子集。
  3. 随即server给client发送第二个响应报文是数字证书。服务端必须要有一套数字证书,可以自己制作,也可以向组织申请。区别就是自己颁发的证书需要客户端验证通过,才可以继续访问,而使用受信任的公司申请的证书则不会弹出提示页面,这套证书其实就是一对公钥和私钥。传送证书,这个证书其实就是公钥,只是包含了很多信息,如证书的颁发机构,过期时间、服务端的公钥,第三方证书认证机构(CA)的签名,服务端的域名信息等内容。
  4. 客户端解析证书,这部分工作是由客户端的TLS来完成的,首先会验证公钥是否有效,比如颁发机构,过期时间等等,如果发现异常,则会弹出一个警告框,提示证书存在问题。如果证书没有问题,那么就生成一个随即值(预主秘钥)。
  5. 客户端认证证书通过之后,接下来是通过随机值1、随机值2和预主秘钥组装会话秘钥。然后通过证书的公钥加密会话秘钥。
  6. 传送加密信息,这部分传送的是用证书加密后的会话秘钥,目的就是让服务端使用秘钥解密得到随机值1、随机值2和预主秘钥。
  7. 服务端解密得到随机值1、随机值2和预主秘钥,然后组装会话秘钥,跟客户端会话秘钥相同。
  8. 客户端通过会话秘钥加密一条消息发送给服务端,主要验证服务端是否正常接受客户端加密的消息。
  9. 同样服务端也会通过会话秘钥加密一条消息回传给客户端,如果客户端能够正常接受的话表明SSL层连接建立完成了

最终的会话密钥,就是用「客户端随机数 + 服务端随机数 + x(ECDHE 算法算出的共享密钥) 」三个材料生成的

https://www.zhihu.com/column/c_1185131592262148096

https://blog.csdn.net/xinyuan_java/article/details/109742049

###进程间通信及优缺点分析(转)

img


正文

每个进程的用户地址空间都是独立的,一般而言是不能互相访问的,但内核空间是每个进程都共享的,所以进程之间要通信必须通过内核。

img

Linux 内核提供了不少进程间通信的机制,我们来一起瞧瞧有哪些?

管道

如果你学过 Linux 命令,那你肯定很熟悉「|」这个竖线。

$ ps auxf | grep mysql

上面命令行里的「|」竖线就是一个管道,它的功能是将前一个命令(ps auxf)的输出,作为后一个命令(grep mysql)的输入,从这功能描述,可以看出管道传输数据是单向的,如果想相互通信,我们需要创建两个管道才行。

同时,我们得知上面这种管道是没有名字,所以「|」表示的管道称为匿名管道,用完了就销毁。

管道还有另外一个类型是命名管道,也被叫做 FIFO,因为数据是先进先出的传输方式。

在使用命名管道前,先需要通过 mkfifo 命令来创建,并且指定管道名字:

$ mkfifo myPipe

myPipe 就是这个管道的名称,基于 Linux 一切皆文件的理念,所以管道也是以文件的方式存在,我们可以用 ls 看一下,这个文件的类型是 p,也就是 pipe(管道) 的意思:

$ ls -l
prw-r--r--. 1 root    root         0 Jul 17 02:45 myPipe

接下来,我们往 myPipe 这个管道写入数据:

$ echo "hello" > myPipe  // 将数据写进管道
                         // 停住了 ...

你操作了后,你会发现命令执行后就停在这了,这是因为管道里的内容没有被读取,只有当管道里的数据被读完后,命令才可以正常退出。

于是,我们执行另外一个命令来读取这个管道里的数据:

$ cat < myPipe  // 读取管道里的数据
hello

可以看到,管道里的内容被读取出来了,并打印在了终端上,另外一方面,echo 那个命令也正常退出了。

我们可以看出,管道这种通信方式效率低,不适合进程间频繁地交换数据。当然,它的好处,自然就是简单,同时也我们很容易得知管道里的数据已经被另一个进程读取了。

那管道如何创建呢,背后原理是什么?

匿名管道的创建,需要通过下面这个系统调用:

int pipe(int fd[2])

这里表示创建一个匿名管道,并返回了两个描述符,一个是管道的读取端描述符 fd[0],另一个是管道的写入端描述符 fd[1]。注意,这个匿名管道是特殊的文件,只存在于内存,不存于文件系统中。

img

其实,所谓的管道,就是内核里面的一串缓存。从管道的一段写入的数据,实际上是缓存在内核中的,另一端读取,也就是从内核中读取这段数据。另外,管道传输的数据是无格式的流且大小受限。

看到这,你可能会有疑问了,这两个描述符都是在一个进程里面,并没有起到进程间通信的作用,怎么样才能使得管道是跨过两个进程的呢?

我们可以使用 fork 创建子进程,创建的子进程会复制父进程的文件描述符,这样就做到了两个进程各有两个「 fd[0]fd[1]」,两个进程就可以通过各自的 fd 写入和读取同一个管道文件实现跨进程通信了。

img

管道只能一端写入,另一端读出,所以上面这种模式容易造成混乱,因为父进程和子进程都可以同时写入,也都可以读出。那么,为了避免这种情况,通常的做法是:

  • 父进程关闭读取的 fd[0],只保留写入的 fd[1];
  • 子进程关闭写入的 fd[1],只保留读取的 fd[0];

img

所以说如果需要双向通信,则应该创建两个管道。

到这里,我们仅仅解析了使用管道进行父进程与子进程之间的通信,但是在我们 shell 里面并不是这样的。

在 shell 里面执行 A | B命令的时候,A 进程和 B 进程都是 shell 创建出来的子进程,A 和 B 之间不存在父子关系,它俩的父进程都是 shell。

img

所以说,在 shell 里通过「|」匿名管道将多个命令连接在一起,实际上也就是创建了多个子进程,那么在我们编写 shell 脚本时,能使用一个管道搞定的事情,就不要多用一个管道,这样可以减少创建子进程的系统开销。

我们可以得知,对于匿名管道,它的通信范围是存在父子关系的进程。因为管道没有实体,也就是没有管道文件,只能通过 fork 来复制父进程 fd 文件描述符,来达到通信的目的。

另外,对于命名管道,它可以在不相关的进程间也能相互通信。因为命令管道,提前创建了一个类型为管道的设备文件,在进程里只要使用这个设备文件,就可以相互通信。

不管是匿名管道还是命名管道,进程写入的数据都是缓存在内核中,另一个进程读取数据时候自然也是从内核中获取,同时通信数据都遵循先进先出原则,不支持 lseek 之类的文件定位操作。


消息队列

前面说到管道的通信方式是效率低的,因此管道不适合进程间频繁地交换数据。

对于这个问题,消息队列的通信模式就可以解决。比如,A 进程要给 B 进程发送消息,A 进程把数据放在对应的消息队列后就可以正常返回了,B 进程需要的时候再去读取数据就可以了。同理,B 进程要给 A 进程发送消息也是如此。

再来,消息队列是保存在内核中的消息链表,在发送数据时,会分成一个一个独立的数据单元,也就是消息体(数据块),消息体是用户自定义的数据类型,消息的发送方和接收方要约定好消息体的数据类型,所以每个消息体都是固定大小的存储块,不像管道是无格式的字节流数据。如果进程从消息队列中读取了消息体,内核就会把这个消息体删除。

消息队列生命周期随内核,如果没有释放消息队列或者没有关闭操作系统,消息队列会一直存在,而前面提到的匿名管道的生命周期,是随进程的创建而建立,随进程的结束而销毁。

消息这种模型,两个进程之间的通信就像平时发邮件一样,你来一封,我回一封,可以频繁沟通了。

但邮件的通信方式存在不足的地方有两点,一是通信不及时,二是附件也有大小限制,这同样也是消息队列通信不足的点。

消息队列不适合比较大数据的传输,因为在内核中每个消息体都有一个最大长度的限制,同时所有队列所包含的全部消息体的总长度也是有上限。在 Linux 内核中,会有两个宏定义 MSGMAXMSGMNB,它们以字节为单位,分别定义了一条消息的最大长度和一个队列的最大长度。

消息队列通信过程中,存在用户态与内核态之间的数据拷贝开销,因为进程写入数据到内核中的消息队列时,会发生从用户态拷贝数据到内核态的过程,同理另一进程读取内核中的消息数据时,会发生从内核态拷贝数据到用户态的过程。


共享内存

消息队列的读取和写入的过程,都会有发生用户态与内核态之间的消息拷贝过程。那共享内存的方式,就很好的解决了这一问题。

现代操作系统,对于内存管理,采用的是虚拟内存技术,也就是每个进程都有自己独立的虚拟内存空间,不同进程的虚拟内存映射到不同的物理内存中。所以,即使进程 A 和 进程 B 的虚拟地址是一样的,其实访问的是不同的物理内存地址,对于数据的增删查改互不影响。

共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,传来传去,大大提高了进程间通信的速度。

img


信号量

用了共享内存通信方式,带来新的问题,那就是如果多个进程同时修改同一个共享内存,很有可能就冲突了。例如两个进程都同时写一个地址,那先写的那个进程会发现内容被别人覆盖了。

为了防止多进程竞争共享资源,而造成的数据错乱,所以需要保护机制,使得共享的资源,在任意时刻只能被一个进程访问。正好,信号量就实现了这一保护机制。

信号量其实是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据

信号量表示资源的数量,控制信号量的方式有两种原子操作:

  • 一个是 P 操作,这个操作会把信号量减去 -1,相减后如果信号量 < 0,则表明资源已被占用,进程需阻塞等待;相减后如果信号量 >= 0,则表明还有资源可使用,进程可正常继续执行。
  • 另一个是 V 操作,这个操作会把信号量加上 1,相加后如果信号量 <= 0,则表明当前有阻塞中的进程,于是会将该进程唤醒运行;相加后如果信号量 > 0,则表明当前没有阻塞中的进程;

P 操作是用在进入共享资源之前,V 操作是用在离开共享资源之后,这两个操作是必须成对出现的。

接下来,举个例子,如果要使得两个进程互斥访问共享内存,我们可以初始化信号量为 1

img

具体的过程如下:

  • 进程 A 在访问共享内存前,先执行了 P 操作,由于信号量的初始值为 1,故在进程 A 执行 P 操作后信号量变为 0,表示共享资源可用,于是进程 A 就可以访问共享内存。
  • 若此时,进程 B 也想访问共享内存,执行了 P 操作,结果信号量变为了 -1,这就意味着临界资源已被占用,因此进程 B 被阻塞。
  • 直到进程 A 访问完共享内存,才会执行 V 操作,使得信号量恢复为 0,接着就会唤醒阻塞中的线程 B,使得进程 B 可以访问共享内存,最后完成共享内存的访问后,执行 V 操作,使信号量恢复到初始值 1。

可以发现,信号初始化为 1,就代表着是互斥信号量,它可以保证共享内存在任何时刻只有一个进程在访问,这就很好的保护了共享内存。

另外,在多进程里,每个进程并不一定是顺序执行的,它们基本是以各自独立的、不可预知的速度向前推进,但有时候我们又希望多个进程能密切合作,以实现一个共同的任务。

例如,进程 A 是负责生产数据,而进程 B 是负责读取数据,这两个进程是相互合作、相互依赖的,进程 A 必须先生产了数据,进程 B 才能读取到数据,所以执行是有前后顺序的。

那么这时候,就可以用信号量来实现多进程同步的方式,我们可以初始化信号量为 0

img

具体过程:

  • 如果进程 B 比进程 A 先执行了,那么执行到 P 操作时,由于信号量初始值为 0,故信号量会变为 -1,表示进程 A 还没生产数据,于是进程 B 就阻塞等待;
  • 接着,当进程 A 生产完数据后,执行了 V 操作,就会使得信号量变为 0,于是就会唤醒阻塞在 P 操作的进程 B;
  • 最后,进程 B 被唤醒后,意味着进程 A 已经生产了数据,于是进程 B 就可以正常读取数据了。

可以发现,信号初始化为 0,就代表着是同步信号量,它可以保证进程 A 应在进程 B 之前执行。


信号

上面说的进程间通信,都是常规状态下的工作模式。对于异常情况下的工作模式,就需要用「信号」的方式来通知进程。

信号跟信号量虽然名字相似度 66.66%,但两者用途完全不一样,就好像 Java 和 JavaScript 的区别。

在 Linux 操作系统中, 为了响应各种各样的事件,提供了几十种信号,分别代表不同的意义。我们可以通过 kill -l 命令,查看所有的信号:

$ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

运行在 shell 终端的进程,我们可以通过键盘输入某些组合键的时候,给进程发送信号。例如

  • Ctrl+C 产生 SIGINT 信号,表示终止该进程;
  • Ctrl+Z 产生 SIGTSTP 信号,表示停止该进程,但还未结束;

如果进程在后台运行,可以通过 kill 命令的方式给进程发送信号,但前提需要知道运行中的进程 PID 号,例如:

  • kill -9 1050 ,表示给 PID 为 1050 的进程发送 SIGKILL 信号,用来立即结束该进程;

所以,信号事件的来源主要有硬件来源(如键盘 Cltr+C )和软件来源(如 kill 命令)。

信号是进程间通信机制中唯一的异步通信机制,因为可以在任何时候发送信号给某一进程,一旦有信号产生,我们就有下面这几种,用户进程对信号的处理方式。

1.执行默认操作。Linux 对每种信号都规定了默认操作,例如,上面列表中的 SIGTERM 信号,就是终止进程的意思。Core 的意思是 Core Dump,也即终止进程后,通过 Core Dump 将当前进程的运行状态保存在文件里面,方便程序员事后进行分析问题在哪里。

2.捕捉信号。我们可以为信号定义一个信号处理函数。当信号发生时,我们就执行相应的信号处理函数。

3.忽略信号。当我们不希望处理某些信号的时候,就可以忽略该信号,不做任何处理。有两个信号是应用进程无法捕捉和忽略的,即 SIGKILLSEGSTOP,它们用于在任何时候中断或结束某一进程。


Socket

前面提到的管道、消息队列、共享内存、信号量和信号都是在同一台主机上进行进程间通信,那要想跨网络与不同主机上的进程之间通信,就需要 Socket 通信了。

实际上,Socket 通信不仅可以跨网络与不同主机的进程间通信,还可以在同主机上进程间通信。

我们来看看创建 socket 的系统调用:

int socket(int domain, int type, int protocal)

三个参数分别代表:

  • domain 参数用来指定协议族,比如 AF_INET 用于 IPV4、AF_INET6 用于 IPV6、AF_LOCAL/AF_UNIX 用于本机;
  • type 参数用来指定通信特性,比如 SOCK_STREAM 表示的是字节流,对应 TCP、SOCK_DGRAM 表示的是数据报,对应 UDP、SOCK_RAW 表示的是原始套接字;
  • protocal 参数原本是用来指定通信协议的,但现在基本废弃。因为协议已经通过前面两个参数指定完成,protocol 目前一般写成 0 即可;

根据创建 socket 类型的不同,通信的方式也就不同:

  • 实现 TCP 字节流通信: socket 类型是 AF_INET 和 SOCK_STREAM;
  • 实现 UDP 数据报通信:socket 类型是 AF_INET 和 SOCK_DGRAM;
  • 实现本地进程间通信: 「本地字节流 socket 」类型是 AF_LOCAL 和 SOCK_STREAM,「本地数据报 socket 」类型是 AF_LOCAL 和 SOCK_DGRAM。另外,AF_UNIX 和 AF_LOCAL 是等价的,所以 AF_UNIX 也属于本地 socket;

接下来,简单说一下这三种通信的编程模式。

针对 TCP 协议通信的 socket 编程模型

img

  • 服务端和客户端初始化 socket,得到文件描述符;
  • 服务端调用 bind,将绑定在 IP 地址和端口;
  • 服务端调用 listen,进行监听;
  • 服务端调用 accept,等待客户端连接;
  • 客户端调用 connect,向服务器端的地址和端口发起连接请求;
  • 服务端 accept 返回用于传输的 socket 的文件描述符;
  • 客户端调用 write 写入数据;服务端调用 read 读取数据;
  • 客户端断开连接时,会调用 close,那么服务端 read 读取数据的时候,就会读取到了 EOF,待处理完数据后,服务端调用 close,表示连接关闭。

这里需要注意的是,服务端调用 accept 时,连接成功了会返回一个已完成连接的 socket,后续用来传输数据。

所以,监听的 socket 和真正用来传送数据的 socket,是「两个」 socket,一个叫作监听 socket,一个叫作已完成连接 socket

成功连接建立之后,双方开始通过 read 和 write 函数来读写数据,就像往一个文件流里面写东西一样。

针对 UDP 协议通信的 socket 编程模型

img

UDP 是没有连接的,所以不需要三次握手,也就不需要像 TCP 调用 listen 和 connect,但是 UDP 的交互仍然需要 IP 地址和端口号,因此也需要 bind。

对于 UDP 来说,不需要要维护连接,那么也就没有所谓的发送方和接收方,甚至都不存在客户端和服务端的概念,只要有一个 socket 多台机器就可以任意通信,因此每一个 UDP 的 socket 都需要 bind。

另外,每次通信时,调用 sendto 和 recvfrom,都要传入目标主机的 IP 地址和端口。

针对本地进程间通信的 socket 编程模型

本地 socket 被用于在同一台主机上进程间通信的场景:

  • 本地 socket 的编程接口和 IPv4 、IPv6 套接字编程接口是一致的,可以支持「字节流」和「数据报」两种协议;
  • 本地 socket 的实现效率大大高于 IPv4 和 IPv6 的字节流、数据报 socket 实现;

对于本地字节流 socket,其 socket 类型是 AF_LOCAL 和 SOCK_STREAM。

对于本地数据报 socket,其 socket 类型是 AF_LOCAL 和 SOCK_DGRAM。

本地字节流 socket 和 本地数据报 socket 在 bind 的时候,不像 TCP 和 UDP 要绑定 IP 地址和端口,而是绑定一个本地文件,这也就是它们之间的最大区别。


总结

由于每个进程的用户空间都是独立的,不能相互访问,这时就需要借助内核空间来实现进程间通信,原因很简单,每个进程都是共享一个内核空间。

Linux 内核提供了不少进程间通信的方式,其中最简单的方式就是管道,管道分为「匿名管道」和「命名管道」。

匿名管道顾名思义,它没有名字标识,匿名管道是特殊文件只存在于内存,没有存在于文件系统中,shell 命令中的「|」竖线就是匿名管道,通信的数据是无格式的流并且大小受限,通信的方式是单向的,数据只能在一个方向上流动,如果要双向通信,需要创建两个管道,再来匿名管道是只能用于存在父子关系的进程间通信,匿名管道的生命周期随着进程创建而建立,随着进程终止而消失。

命名管道突破了匿名管道只能在亲缘关系进程间的通信限制,因为使用命名管道的前提,需要在文件系统创建一个类型为 p 的设备文件,那么毫无关系的进程就可以通过这个设备文件进行通信。另外,不管是匿名管道还是命名管道,进程写入的数据都是缓存在内核中,另一个进程读取数据时候自然也是从内核中获取,同时通信数据都遵循先进先出原则,不支持 lseek 之类的文件定位操作。

消息队列克服了管道通信的数据是无格式的字节流的问题,消息队列实际上是保存在内核的「消息链表」,消息队列的消息体是可以用户自定义的数据类型,发送数据时,会被分成一个一个独立的消息体,当然接收数据时,也要与发送方发送的消息体的数据类型保持一致,这样才能保证读取的数据是正确的。消息队列通信的速度不是最及时的,毕竟每次数据的写入和读取都需要经过用户态与内核态之间的拷贝过程。

共享内存可以解决消息队列通信中用户态与内核态之间数据拷贝过程带来的开销,它直接分配一个共享空间,每个进程都可以直接访问,就像访问进程自己的空间一样快捷方便,不需要陷入内核态或者系统调用,大大提高了通信的速度,享有最快的进程间通信方式之名。但是便捷高效的共享内存通信,带来新的问题,多进程竞争同个共享资源会造成数据的错乱。

那么,就需要信号量来保护共享资源,以确保任何时刻只能有一个进程访问共享资源,这种方式就是互斥访问。信号量不仅可以实现访问的互斥性,还可以实现进程间的同步,信号量其实是一个计数器,表示的是资源个数,其值可以通过两个原子操作来控制,分别是 P 操作和 V 操作

与信号量名字很相似的叫信号,它俩名字虽然相似,但功能一点儿都不一样。信号是进程间通信机制中唯一的异步通信机制,信号可以在应用进程和内核之间直接交互,内核也可以利用信号来通知用户空间的进程发生了哪些系统事件,信号事件的来源主要有硬件来源(如键盘 Cltr+C )和软件来源(如 kill 命令),一旦有信号发生,进程有三种方式响应信号 1. 执行默认操作、2. 捕捉信号、3. 忽略信号。有两个信号是应用进程无法捕捉和忽略的,即 SIGKILLSEGSTOP,这是为了方便我们能在任何时候结束或停止某个进程。

前面说到的通信机制,都是工作于同一台主机,如果要与不同主机的进程间通信,那么就需要 Socket 通信了。Socket 实际上不仅用于不同的主机进程间通信,还可以用于本地主机进程间通信,可根据创建 Socket 的类型不同,分为三种常见的通信方式,一个是基于 TCP 协议的通信方式,一个是基于 UDP 协议的通信方式,一个是本地进程间通信方式。

以上,就是进程间通信的主要机制了。你可能会问了,那线程通信间的方式呢?

同个进程下的线程之间都是共享进程的资源,只要是共享变量都可以做到线程间通信,比如全局变量,所以对于线程间关注的不是通信方式,而是关注多线程竞争共享资源的问题,信号量也同样可以在线程间实现互斥与同步:

  • 互斥的方式,可保证任意时刻只有一个线程访问共享资源;
  • 同步的方式,可保证线程 A 应在线程 B 之前执行;

https://www.zhihu.com/column/c_1185131592262148096

###自旋线程

为什么要让m3和m4自旋,自旋本质是在运行,线程在运行却没有执行g,就变成了浪费CPU?销毁线程不是更好吗?可以节约CPU资源。创建和销毁CPU都是浪费时间的,我们希望当有新goroutine创建时,立刻能有m运行它,如果销毁再新建就增加了时延,降低了效率。当然也考虑了过多的自旋线程是浪费CPU,所以系统中最多有GOMAXPROCS个自旋的线程,多余的没事做线程会让他们休眠

###HTTP/2的特性

https://zhuanlan.zhihu.com/p/26559480

流量控制与拥塞控制的区别

Redis cluster master挂了,从节点通过投票选举升级为主

1.slave发现自己的master变为FAIL

2.发起选举前,slave先给自己的epoch(即currentEpoch)增一,然后请求其它master给自己投票。slave是通过广播FAILOVER_AUTH_REQUEST包给集中的每一个masters。

3.slave发起投票后,会等待至少两倍NODE_TIMEOUT时长接收投票结果,不管NODE_TIMEOUT何值,也至少会等待2秒。

4.master接收投票后给slave响应FAILOVER_AUTH_ACK,并且在(NODE_TIMEOUT*2)时间内不会给同一master的其它slave投票。

5.如果slave收到FAILOVER_AUTH_ACK响应的epoch值小于自己的epoch,则会直接丢弃。一旦slave收到多数master的FAILOVER_AUTH_ACK,则声明自己赢得了选举。

6.如果slave在两倍的NODE_TIMEOUT时间内(至少2秒)未赢得选举,则放弃本次选举,然后在四倍NODE_TIMEOUT时间(至少4秒)后重新发起选举。

只所以强制延迟至少0.5秒选举,是为确保master的fail状态在整个集群内传开,否则可能只有小部分master知晓,而master只会给处于fail状态的master的slaves投票。如果一个slave的master状态不是fail,则其它master不会给它投票,Redis通过八卦协议(即Gossip协议,也叫谣言协议)传播fail。而在固定延迟上再加一个随机延迟,是为了避免多个slaves同时发起选举。

延迟计算公式:

DELAY = 500ms + random(0 ~ 500ms) + SLAVE_RANK * 1000ms

SLAVE_RANK表示此slave已经从master复制数据的总量的rank。Rank越小代表已复制的数据越新。这种方式下,持有最新数据的slave将会首先发起选举(理论上)

goroutine调度对for阻塞和channel阻塞CPU不同的表现

package main

import (
	"fmt"
	"runtime"
)

func main() {
	// ch := make(chan int)
	for i := 0; i < runtime.NumCPU()-1; i++ {
		go func() {
			for {
			}
			// ch <- 1
			fmt.Println("here")
		}()
	}
	go func() {
		fmt.Print("A")
	}()
	fmt.Print("B")
	for {
	}
}

// go version go1.14 会打印出A B 说明,在goroutine调度上,对for这种阻塞goroutine会切换出来,避免阻塞CPU 避免阻塞M。 但是,CPU会因为for循环任务的执行,而导致达到100%  而如果是channel的阻塞,是不会让CPU达到100%。因为在goroutine调度中,如果有channel的阻塞,会将该goroutine切换出去,放到goroutine的末尾,继续执行其他goroutine

// 如果是下面这样的for+select+no default 的情况,CPU不会被打满
go func() {
  for {
    select {
      case <-ch:
      	fmt.Println(1)
    }
  }
  fmt.Println("here")
}()

// 如果是下面这样的for+select+default 的情况,CPU会被打满
go func() {
  for {
    select {
      case <-ch:
      	fmt.Println(1)
      default:
    }
  }
  fmt.Println("here")
}()

###git 批量删除tag

####本地

  git tag | grep "v" |xargs git tag -d  

其中grep “v”应该是你自己想要的匹配

远程

  git show-ref --tag | grep "v1.0"| awk '{print $2}'|xargs git push origin --delete  

其中,grep “v1.0”应该是你想要的匹配

理解Service Mesh

https://zhuanlan.zhihu.com/p/61901608

###Redis 6.0 多线程

Redis 6.0将处理过程中最耗时的Socket的读取、请求解析、写入单独外包出去,剩下的命令执行仍然由单线程来完成和内存的数据交互。

这样一来,网络IO操作就变成多线程化了,其他核心部分仍然是线程安全的,确实是个不错的折中办法

IO 线程要么同时在读 socket,要么同时在写,不会同时读或写; IO 线程只负责读写 socket 解析命令,不负责执行命令,由主线程串行执行命令; IO 线程数可配置,默认为 1; 上面的过程是完全无锁的,因为在 IO 线程处理的时主线程会等待全部的 IO 线程完成,所以不会出现 data race 的场景

Redis 抽象了一套 AE 事件模型,将 IO 事件和时间事件融入一起,同时借助 epoll 的回调特性,使得 IO 读写都是非阻塞的,实现高性能的网络处理能力。加上 Redis 基于内存的数据处理,这便是 “单线程,但却高性能” 的核心原因。

但 IO 数据的读写依然是阻塞的,这也是 Redis 目前的主要性能瓶颈之一,特别是在数据吞吐量特别大的时候,具体情况如下:

img

上图的下半部分,当 socket 中有数据时,Redis 会通过系统调用将数据从内核态拷贝到用户态,供 Redis 解析用。这个拷贝过程是阻塞的,术语称作 “同步 IO”,数据量越大拷贝的延迟越高,时间消耗也越大,糟糕的是这些操作都是单线程处理的。(写 reponse 时也是一样)

这是 Redis 目前的瓶颈之一,Redis6.0 引入的 “多线程” 机制就是对于上诉瓶颈的优化。

核心思路是,将主线程的 IO 读写任务拆分出来给一组独立的线程执行,使得多个 socket 的读写可以并行化。(命令的执行依然是主线程串行执行)

核心流程大概如下:

img

流程简述如下:

  • 主线程获取 socket 放入等待列表
  • 将 socket 分配给各个 IO 线程(并不会等列表满)
  • 主线程阻塞等待 IO 线程读取 socket 完毕
  • 主线程执行命令 - 单线程(如果命令没有接收完毕,会等 IO 下次继续)
  • 主线程阻塞等待 IO 线程将数据回写 socket 完毕(一次没写完,会等下次再写)
  • 解除绑定,清空等待队列

Go Channel发送和接收具体逻辑简记

我们在这里可以简单梳理和总结一下使用 ch <- i 表达式向 Channel 发送数据时遇到的几种情况:

  1. 如果当前 Channel 的 recvq 上存在已经被阻塞的 Goroutine,那么会直接将数据发送给当前 Goroutine 并将其设置成下一个运行的 Goroutine;
  2. 如果 Channel 存在缓冲区并且其中还有空闲的容量,我们会直接将数据存储到缓冲区 sendx 所在的位置上;
  3. 如果不满足上面的两种情况,会创建一个 runtime.sudog 结构并将其加入 Channel 的 sendq 队列中,当前 Goroutine 也会陷入阻塞等待其他的协程从 Channel 接收数据;

发送数据的过程中包含几个会触发 Goroutine 调度的时机:

  1. 发送数据时发现 Channel 上存在等待接收数据的 Goroutine,立刻设置处理器的 runnext 属性,但是并不会立刻触发调度;
  2. 发送数据时并没有找到接收方并且缓冲区已经满了,这时会将自己加入 Channel 的 sendq 队列并调用 runtime.goparkunlock 触发 Goroutine 的调度让出处理器的使用权

我们梳理一下从 Channel 中接收数据时可能会发生的五种情况:

  1. 如果 Channel 为空,那么会直接调用 runtime.gopark 挂起当前 Goroutine;
  2. 如果 Channel 已经关闭并且缓冲区没有任何数据,runtime.chanrecv 会直接返回;
  3. 如果 Channel 的 sendq 队列中存在挂起的 Goroutine,会将 recvx 索引所在的数据拷贝到接收变量所在的内存空间上并将 sendq 队列中 Goroutine 的数据拷贝到缓冲区;
  4. 如果 Channel 的缓冲区中包含数据,那么直接读取 recvx 索引对应的数据;
  5. 在默认情况下会挂起当前的 Goroutine,将 runtime.sudog 结构加入 recvq 队列并陷入休眠等待调度器的唤醒;

我们总结一下从 Channel 接收数据时,会触发 Goroutine 调度的两个时机:

  1. 当 Channel 为空时;
  2. 当缓冲区中不存在数据并且也不存在数据的发送者时

https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-channel/

LSM树的缺点

当查找数据库中不存在的 键时,LSM树算法可能会很慢:您必须检查内存表,然后将这些段一直回到最老的(可能必 须从磁盘读取每一个),然后才能确定键不存在。为了优化这种访问,存储引擎通常使用额 外的Bloom过滤器。(布隆过滤器是用于近似集合内容的内存高效数据结构,它可以 告诉您数据库中是否出现键,从而为不存在的键节省许多不必要的磁盘读取操作

LSM树上的读取通常比较慢,因 为它们必须在压缩的不同阶段检查几个不同的数据结构和SSTables

日志结构存储的缺点是压缩过程有时会干扰正在进行的读写操作。尽管存储引擎尝试逐步执 行压缩而不影响并发访问,但是磁盘资源有限,所以很容易发生请求需要等待而磁盘完成昂 贵的压缩操作。对吞吐量和平均响应时间的影响通常很小,但是在更高百分比的情况下(参 阅“描述性能”),对日志结构化存储引擎的查询响应时间有时会相当长,而B树的行为则相对 更具可预测性【28】。

压缩的另一个问题出现在高写入吞吐量:磁盘的有限写入带宽需要在初始写入(记录和刷新 内存表到磁盘)和在后台运行的压缩线程之间共享。写入空数据库时,可以使用全磁盘带宽 进行初始写入,但数据库越大,压缩所需的磁盘带宽就越多。

如果写入吞吐量很高,并且压缩没有仔细配置,压缩跟不上写入速率。在这种情况下,磁盘 上未合并段的数量不断增加,直到磁盘空间用完,读取速度也会减慢,因为它们需要检查更 多段文件。通常情况下,即使压缩无法跟上,基于SSTable的存储引擎也不会限制传入写入的 速率,所以您需要进行明确的监控来检测这种情况

流量控制与拥塞控制区别

流量控制是发送数据一方根据接收数据一方接收数据的能力,包括接收缓存、处理速度等,调整数据发送速率和数据量,以避免接收方被数据淹没;拥塞控制则是根据网络的通过能力或网络拥挤程度,来调整数据发送速率和数据量。也就是说,拥塞控制主要考虑端系统之间的网络环境,目的是使网络负载不超过网络的传送能力;而流量控制主要考虑接收端的数据接收与处理能力,目的是使发送端的发送速率不超过接收端的接收能力。另外,拥塞控制的任务是确保网络能够承载所达到的流量;而流量控制只与特定的发送方和特定的接收方之间的点到点流量有关。

做一个类比,假设A地向B地放行一列车队。如果为了适应B地停车场的停车能力,来调整或约束A地车辆放行速率,这就是流量控制;如果为了避免A地到B地经过的路网不出现塞车,或者为了消除已发生的塞车现象,而约束A地放行车辆的速率,这就是拥塞控制。显然,虽然流量控制和拥塞都可能需要约束或调整端系统发送数据的速率或者数量,但两者的目标和解决的问题不同,调整速率的依据也不同,因此,二者不可能彼此互相取代

NGAC

NGAC,即下一代访问控制,采用将访问决定数据建模为图形的方法。NGAC 可以实现系统化、策略一致的访问控制方法,以高精细度授予或拒绝用户管理能力。NGAC 由 NIST(美国国家标准与技术研究所)开发,目前用于 Tetrate QTetrate Service Bridge

有几种类型的实体;它们代表了您要保护的资源、它们之间的关系以及与系统互动的行为者。这些实体是:

  • 用户
  • 对象
  • 用户属性,如组织单位
  • 对象属性,如文件夹
  • 策略类,如文件系统访问、位置和时间

NIST 的 David Ferraiolo 和 Tetrate 的 Ignasi Barrera 在旧金山举行的 2019 年服务网格日(Service Mesh Day 2019)上发表了关于下一代访问控制的 演讲,分享了 NGAC 的工作原理。

NGAC 是基于这样一个假设:你可以用一个图来表示你要保护的系统,这个图代表了你要保护的资源和你的组织结构,这个图对你有意义,并且符合你的组织语义。在这个对你的组织非常特殊的模型之上,你可以叠加策略。在资源模型和用户模型之间,定义了权限。这样 NGAC 提供了一种优雅的方式来表示你要保护的资源,系统中的不同角色,以及如何用权限把这两个世界联系在一起。

NGAC 模型中的 DAG

图片来自于 Linear Time Algorithms to Restrict Insider Access using Multi-Policy Access Control Systems

NGAC 示例

下面的例子展示了一个简单的 NGAC 图,其中有一个代表组织结构的用户 DAG,一个代表文件系统中的文件和文件夹的对象 DAG,一个文件的分类,以及两个不同的策略 —— 文件系统和范围,可以结合起来做出访问决策。两个 DAG 之间的关联边定义了行为者对目标资源的权限。

NGAC 示例图

在这张图中,我们可以看到 /hr-docs 文件夹中的两个文件 resumecontract 的表示,每个文件都链接到一个类别(public/confidential)。还有两个策略类,File SystemScope,图中的对象被连接在这里 —— 需要满足这些条件才能获得对每个文件的访问权。

在例子中,用户 Allice 对两个文件都有读写访问权限,因为有一个路径将 Allice 链接到每个文件,而且路径授予了两个策略类的权限。但是,用户 Bob 只有对 resume 文件的访问权,因为虽然存在一个从 Bob 到 contract 文件的路径,该路径满足 File System 策略类的 “读 “ 权限,但没有授予 Scope 策略类权限的路径。所以,Bob 对 contract 文件的访问被拒绝。

为什么选择 NGAC?

在 ABAC 的情况下,需要跟踪所有对象的属性,这造成了可管理性的负担。RBAC 减少了负担,因为我们提取了所有角色的访问信息,但是这种模式存在角色爆炸的问题,也会变得不可管理。有了 NGAC,我们在图中就有了我们所需要的一切 —— 以一种紧凑、集中的方式。

当访问决策很复杂时,ABAC 的处理时间会成倍上升。RBAC 在规模上变得特别难以管理,而 NGAC 则可以线性扩展。

NGAC 真正出彩的地方在于灵活性。它可以被配置为允许或不允许访问,不仅基于对象属性,而且基于其他条件 —— 时间、位置、月相等。

NGAC 的其他关键优势包括能够一致地设置策略(以满足合规性要求)和设置历时性策略的能力。例如,NGAC 可以在中断期间授予开发人员一次性的资源访问权,而不会留下不必要的权限,以免日后导致安全漏洞。NGAC 可以在一个访问决策中评估和组合多个策略,同时保持其线性时间的复杂度。

总结

下表从几个方面对 ABAC、RBAC 和 NGAC 进行了比较。

权限模型 优点 缺点
ABAC 灵活 性能和审计问题
RBAC 简单 角色爆炸、固定的访问权限、合规需求挑战
NGAC 细粒度、利于审计、灵活、组合权限策略 复杂

总而言之:

  • RBAC 比较简单,性能好,但在规模上会受到影响。
  • ABAC 很灵活,但性能和可审计性是个问题。
  • NGAC 通过使用一种新颖、优雅的革命性方法来修复这些差距:在用户提供的现有世界表示之上叠加访问策略。你也可以对 RBAC 和 ABAC 策略进行建模

https://jimmysong.io/blog/why-you-should-choose-ngac-as-your-access-control-model/

Search

    Table of Contents