go netpooler-网络io的实现细节:

首先,client连接server的时候,listener会通过accept函数接受到一个新的connection,每一个connection会启动一个goroutine来处理读写, accept会将该connection的fd连带goroutine信息封装注册到epoll的监听列表中,当G调用conn.read或者conn.write等需要阻塞等待的函数时, 会调用gopark将当前G挂起休眠,让P执行下一个G。往后会由Go scheduler在循环调度runtime.schedule()函数或者sysmon监控线程中调用runtime.netpoll 以获取已就绪的G列表并通过inhectglist把G放入全局调度队列或者当前P队列中去执行。

当IO事件发生之后,netpoller是通过runtime.netpoll函数唤醒那些I/O wait状态的G。它的主要逻辑是:

  • 根据调用方的入参delay,设置对应的epollwait的timeout值
  • 调用epollwait等待发生了可读可写的事件fd
  • 循环遍历epollwait返回的事件列表,处理对应的读写事件类型,组装可运行的G链表并返回

runtime.netpoll在很多场景下都会被调用。它会调用epoll_wait系统函数从epoll的eventpoll.rdllist就绪双向列表返回,从而得到了就绪的socket fd列表, 并取出最初调用epoll_ctl时保存的上下文信息恢复G。 所以执行完netpoll之后,会返回一个就绪的G链表(包含对应的就绪fd)接下来将就绪的G通过injectglist加入到全局调度队列或者P的本地队列去执行。

go内存逃逸

1、堆上动态分配内存比栈上静态分配内存,开销大很多。

2、变量分配在栈上需要能在编译期确定它的作用域,否则会分配到堆上。

3、Go编译器会在编译期对考察变量的作用域,并作一系列检查,如果它的作用域在运行期间对编译器一直是可知的,那么就会分配到栈上。简单来说,编译器会根据变量是否被外部引用来决定是否逃逸。

4、对于Go程序员来说,编译器的这些逃逸分析规则不需要掌握,我们只需通过go build -gcflags ‘-m’命令来观察变量逃逸情况就行了。

5、不要盲目使用变量的指针作为函数参数,虽然它会减少复制操作。但其实当参数为变量自身的时候,复制是在栈上完成的操作,开销远比变量逃逸后动态地在堆上分配内存少的多。

6、逃逸分析在编译阶段完成的。

go空struct有什么用

  1. 空结构体也是结构体,只是 size 为 0 的类型而已;
  2. 所有的空结构体都有一个共同的地址:zerobase 的地址;
  3. 空结构体可以作为 receiver ,receiver 是空结构体作为值的时候,编译器其实直接忽略了第一个参数的传递,编译器在编译期间就能确认生成对应的代码;
  4. map 和 struct{} 结合使用常常用来节省一点点内存,使用的场景一般用来判断 key 存在于 map;
  5. chan 和 struct{} 结合使用是一般用于信号同步的场景,用意并不是节省内存,而是我们真的并不关心 chan 元素的值;
  6. slice 和 struct{} 结合好像真的没啥用。。。

new和malloc的区别

来源:https://www.cnblogs.com/33debug/p/12068699.html

原理

make 在编译期的类型检查阶段,Go语言其实就将代表 make 关键字的 OMAKE 节点根据参数类型的不同转换成了 OMAKESLICE、OMAKEMAP 和 OMAKECHAN 三种不同类型的节点, 这些节点最终也会调用不同的运行时函数来初始化数据结构。 new 内置函数 new 会在编译期的 SSA 代码生成阶段经过 callnew 函数的处理,如果请求创建的类型大小是 0,那么就会返回一个表示空指针的 zerobase 变量, 在遇到其他情况时会将关键字转换成 newobject。原理如上所述。 主要区别如下:

  1. make 只能用来分配及初始化类型为 slice、map、chan 的数据。new 可以分配任意类型的数据;
  2. new 分配返回的是指针,即类型 *Type。make 返回引用,即 Type;
  3. new 分配的空间被清零。make 分配空间后,会进行初始化;

总结

  1. 判定对象大小:
  2. 若是微小对象: 从 mcache 的 alloc 找到对应 classsize 的 mspan; 当前mspan有足够的空间时,分配并修改mspan的相关属性(nextFreeFast函数中实现); 若当前mspan没有足够的空间,从 mcentral 重新获取一块对应 classsize的 mspan,替换原先的mspan,然后分配并修改mspan的相关属性; 若 mcentral 没有足够的对应的classsize的span,则去向mheap申请; 若对应classsize的span没有了,则找一个相近的classsize的span,切割并分配; 若找不到相近的classsize的span,则去向系统申请,并补充到mheap中;
  3. 若是小对象,内存分配逻辑大致同小对象: 查表以确定需要分配内存的对象的 sizeclass,找到 对应classsize的 mspan; mspan有足够的空间时,分配并修改mspan的相关属性(nextFreeFast函数中实现); 若当前mspan没有足够的空间,从 mcentral重新获取一块对应 classsize的 mspan,替换原先的mspan,然后分配并修改mspan的相关属性; 若mcentral没有足够的对应的classsize的span,则去向mheap申请; 若对应classsize的span没有了,则找一个相近的classsize的span,切割并分配 若找不到相近的classsize的span,则去向系统申请,并补充到mheap中
  4. 若是大对象,直接从mheap进行分配 若对应classsize的span没有了,则找一个相近的classsize的span,切割并分配; 若找不到相近的classsize的span,则去向系统申请,并补充到mheap中;

Mysql exist in 执行效率

1、exists是对外表做loop循环,每次loop循环再对内表(子查询)进行查询,那么因为对内表的查询使用的索引(内表效率高,故可用大表),而外表有多大都需要遍历,不可避免(尽量用小表),故内表大的使用exists,可加快效率,包括内表是分组筛选后的结果比外表小的情况;

2、in是把外表和内表做join连接,先查询内表,再把内表结果与外表匹配,对外表使用索引(外表效率高,可用大表),而内表多大都需要查询,不可避免,故外表大的使用in,可加快效率。

3、如果用not in ,则是内外表都全表扫描,无索引,效率低,可考虑使用not exists,也可使用A left join B on A.id=B.id where B.id is null 进行优化。

4、结论: 1.当内外表的数据量差不多时,in和exist的效率是差不多的 2.当外表大,内表小时,in执行效率高 3.外表小,内表大时,exist的执行效率高 4.mysql高性能编程里面讲查询优化会讲in转化为exist执行。

TCP的time_wait状态的知识点

  1. time wait存在的意义有两个: a. 作为全双工连接的tcp,需要保证连接关闭的准确状态。当主动关闭的一端接收到FIN报文时,主动端发出ACK后会处于time wait状态。此时会保持状态2MSL时间(RFC默认时间是60s), 如果对方未收到最后的ack时,会重发FIN包,此时就可以重新处理这个包,保证连接的正常结束,双方都不要有丢包的问题。 b. tcp连接有keep-alive的特性,避免重新建立,关闭连接带来的性能开销。如果网络包因为DNS等问题,在网络中丢失方向,TCP发送端就会因为确认超时,重发这个包,这些包最终也会被送到目的地。 此时如果重新使用了当前的四元组,建立了新链接。如果不等待的话,可能会收到这些"迷途知返"包。为了避免这个情况,TCP不允许处于TIME_WAIT状态的连接启动一个新连接。等待2MSL时间,可以确保当成功建立一个新连接时, 来自旧链接的数据包在网络中已经消逝。
  2. time_wait状态带来的影响 在高并发的短连接场景下,time_wait状态会大量占用port信息(16bit,65535个),影响新连接的建立。具体的原因是:数据请求返回时间+业务处理时间<time_wait时间,此时机器的性能 主要集中在等待资源释放上,
如何尽量处理TIMEWAIT过多?

编辑内核文件/etc/sysctl.conf,加入以下内容:

1
2
3
4
net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;
net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;
net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。
net.ipv4.tcp_fin_timeout 修改系默认的 TIMEOUT 时间 

然后执行 /sbin/sysctl -p 让参数生效.

/etc/sysctl.conf是一个允许改变正在运行中的Linux系统的接口,它包含一些TCP/IP堆栈和虚拟内存系统的高级选项,修改内核参数永久生效。 简单来说,就是打开系统的TIME_WAIT重用和快速回收。

如果以上配置调优后性能还不理想,可继续修改一下配置: 复制代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
vi /etc/sysctl.conf
net.ipv4.tcp_keepalive_time = 1200
#表示当keepalive起用的时候,TCP发送keepalive消息的频度。缺省是2小时,改为20分钟。
net.ipv4.ip_local_port_range = 1024 65000
#表示用于向外连接的端口范围。缺省情况下很小:32768到61000,改为1024到65000。
net.ipv4.tcp_max_syn_backlog = 8192
#表示SYN队列的长度,默认为1024,加大队列长度为8192,可以容纳更多等待连接的网络连接数。
net.ipv4.tcp_max_tw_buckets = 5000
#表示系统同时保持TIME_WAIT套接字的最大数量,如果超过这个数字,TIME_WAIT套接字将立刻被清除并打印警告信息。
默认为180000,改为5000。对于Apache、Nginx等服务器,上几行的参数可以很好地减少TIME_WAIT套接字数量

go slice的扩容

GO1.17版本及之前 当新切片需要的容量cap大于两倍扩容的容量,则直接按照新切片需要的容量扩容; 当原 slice 容量 < 1024 的时候,新 slice 容量变成原来的 2 倍; 当原 slice 容量 > 1024,进入一个循环,每次容量变成原来的1.25倍,直到大于期望容量。

GO1.18之后 当新切片需要的容量cap大于两倍扩容的容量,则直接按照新切片需要的容量扩容; threshold=256 当原 slice 容量 < threshold 的时候,新 slice 容量变成原来的 2 倍; 当原 slice 容量 > threshold,进入一个循环,每次容量增加(旧容量+3*threshold)/4。

17中的扩容存在问题,当容量刚超过1024时,其新增容量回比之前有回落。18中新的方案有两个点,一个是1024–》256,另一个是扩容公式(旧容量+3*threshold)/4, 从commit的实验数据来看,当容量越来越大时,容量的增长比率会接近1.25倍。这次的更改让扩容的整体增长曲线更加平滑。

go的GC

go刚开始的标记清除策略是需要STW维护对象的状态的,演变到三色标记时,主要是想降低STW对用户进程的影响,希望用户进程和标记可以并发执行。但是这样的话 对象就有可能被用户进程修改占用。比如已经标记的A对象(黑色),指向了白色对象。清扫阶段会清理掉白色,A的对象引用就丢失了,称为悬挂指针。针对这种情况, 在标记阶段,对象指针发生变更时,需要满足下面情况,提出强弱三色不变性: 1.黑色对象不能指向白色对象,只能指向黑色和灰色 2.黑色对象指向的白色对象,必须有一条路径,从灰色对象出发经历多个白色对象后,可达这个白色对象 遵循上述两个不变性中的任意一个,我们都能保证垃圾收集算法的正确性,而屏障技术就是在并发或者增量标记过程中保证三色不变性的重要技术。 所谓的屏障技术,简单来说,就是控制对内存操作的顺序性,屏障之前的代码比之后的代码先执行,这主要是防止多核cpu架构下,对内存访问的优化操作。在GC中具体的做法 就是,在gc和用户程序并发的情况下,当用户程序对对象进行增删改时,执行屏障逻辑,改变操作的对象的着色标记。

Golang使用的是三色标记法方案,并且支持并行GC,即用户代码可以和GC代码同时运行。具体来讲,Golang GC分为几个阶段:

  • Mark阶段该阶段又分为两个部分:
    • Mark Prepare:初始化GC任务,包括开启写屏障(write barrier)和辅助GC(mutator assist),统计root对象的任务数量等,这个过程需要STW。
    • GC Drains: 扫描所有root对象,包括全局指针和goroutine(G)栈上的指针(扫描对应G栈时需停止该G),将其加入标记队列(灰色队列),并循环处理灰色队列的对象,直到灰色队列为空。该过程后台并行执行。
  • Mark Termination阶段:该阶段主要是完成标记工作,重新扫描(re-scan)全局指针和栈。因为Mark和用户程序是并行的,所以在Mark过程中可能会有新的对象分配和指针赋值,这个时候就需要通过写屏障(write barrier)记录下来,re-scan再检查一下,这个过程也是会STW的。
  • Sweep: 按照标记结果回收所有的白色对象,该过程后台并行执行。
  • Sweep Termination: 对未清扫的span进行清扫, 只有上一轮的GC的清扫工作完成才可以开始新一轮的GC。

总结一下,Golang的GC过程有两次STW:第一次STW会准备根对象的扫描, 启动写屏障(Write Barrier)和辅助GC(mutator assist).第二次STW会rescan新标记的灰色对象, 禁用写屏障(Write Barrier)和辅助GC(mutator assist).

在 Go 语言 v1.7 版本之前,运行时会使用 Dijkstra 插入写屏障保证强三色不变性,但是运行时并没有在所有的垃圾收集根对象上开启插入写屏障。因为应用程序可能包含成百上千的 Goroutine, 而垃圾收集的根对象一般包括全局变量和栈对象,如果运行时需要在几百个 Goroutine 的栈上都开启写屏障,会带来巨大的额外开销,所以 Go 团队在实现上选择了在标记阶段完成时暂停程序、将所有栈对象标记为灰色并重新扫描, 在活跃 Goroutine 非常多的程序中,重新扫描的过程需要占用 10 ~ 100ms 的时间。

Go 语言在 v1.8 组合 Dijkstra 插入写屏障和 Yuasa 删除写屏障构成了如下所示的混合写屏障,该写屏障会将被覆盖的对象标记成灰色并在当前栈没有扫描时将新对象也标记成灰色:

1
2
3
4
5
writePointer(slot, ptr):
    shade(*slot)
    if current stack is grey:
        shade(ptr)
    *slot = ptr

为了移除栈的重扫描过程,除了引入混合写屏障之外,在垃圾收集的标记阶段,我们还需要将创建的所有新对象都标记成黑色,防止新分配的栈内存和堆内存中的对象被错误地回收, 因为栈内存在标记阶段最终都会变为黑色,所以不再需要重新扫描栈空间。

总结一下:1.7版本之前没有混合写屏障时,使用插入写屏障来保证三色不变性,但是为了性能,并不会在所有的G上开启写屏障。所以会在标记阶段完成后,开启STW,栈对象全部置灰, 重新扫描re-scan。而Yuasa 删除写屏障,标记结束不需要STW,但是回收精度低,会有已删除的引用,在下次时才清理。为了优化stw的这个过程和提高效率, 1.8引入混合写屏障,大大缩减了stw的时间,具体如下:

  • GC 开始将栈上的对象全部扫描并标记为黑色;

  • GC 期间,任何在栈上创建的新对象,均为黑色(不用re-scan栈);

  • 被删除的堆对象标记为灰色;

  • 被添加的堆对象标记为灰色;

    阶段 说明 赋值器状态 SweepTermination 清扫终止阶段,为下一阶段的并发标记做准备工作,启动写屏障 STW Mark 扫描标记阶段,与赋值器并发执行,写屏障开启 并发 MarkTermination 标记终止阶段,保证一个周期内标记任务完成,停止写屏障 STW GCoff 内存清扫阶段,将需要回收的内存归还到堆中,写屏障关闭 并发 GCoff 内存归还阶段,将需要回收的内存归还给操作系统,写屏障关闭 并发

知识点: 删除写屏障:也叫做基于其实快照的解决方案(snapshot-at-the-begining)。顾名思义,就是在开始 gc 之前,必须 STW ,对整个根做一次起始快照。 当赋值器(业务线程)从灰色或者白色对象中删除白色指针时候,写屏障会捕捉这一行为,将这一行为通知给回收器。这样,基于起始快照的解决方案保守地将其目标对象当作存活的对象, 这样就绝对不会有被误回收的对象,但是有扫描工作量浮动放大的风险。术语叫做追踪波面的回退。

删除写屏障(基于起始快照的写屏障)有一个前提条件,就是起始的时候,把整个根部扫描一遍,让所有的可达对象全都在灰色保护下(根黑,下一级在堆上的全灰), 之后利用删除写屏障捕捉内存写操作,确保弱三色不变式不被破坏,就可以保证垃圾回收的正确性。