<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>服务端高性能架构之道 on 子恒的博客</title>
    <link>http://chestnutheng.github.io/tags/%E6%9C%8D%E5%8A%A1%E7%AB%AF%E9%AB%98%E6%80%A7%E8%83%BD%E6%9E%B6%E6%9E%84%E4%B9%8B%E9%81%93/</link>
    <description>Recent content in 服务端高性能架构之道 on 子恒的博客</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Tue, 28 Sep 2021 21:13:12 +0800</lastBuildDate>
    <atom:link href="http://chestnutheng.github.io/tags/%E6%9C%8D%E5%8A%A1%E7%AB%AF%E9%AB%98%E6%80%A7%E8%83%BD%E6%9E%B6%E6%9E%84%E4%B9%8B%E9%81%93/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>[后台]服务端高性能架构之道（系统和服务篇）</title>
      <link>http://chestnutheng.github.io/high_perf_1/</link>
      <pubDate>Tue, 28 Sep 2021 21:13:08 +0800</pubDate>
      <guid>http://chestnutheng.github.io/high_perf_1/</guid>
      <description>&lt;p&gt;如果你在服务端的工区，常常会听到同学们激烈的讨论，包括能不能扛得住xx流量？能不能P99达到x毫秒？某操作能不能立即生效？某服务CPU飙升了，某服务OOM了，某服务超时率暴涨了？&lt;br&gt;&#xA;这些灵魂的质问，其实就是在保障服务端的高并发、高性能、高可用、高一致性等等，是我们服务端同学必备的扎实基本功。&lt;/p&gt;&#xA;&lt;h1 id=&#34;克服系统瓶颈&#34;&gt;克服系统瓶颈&lt;/h1&gt;&#xA;&lt;p&gt;服务端的代码都跑在各种版本的Linux之上，所以高性能的第一步要和操作系统打交道。我们的服务需要通过操作系统进行I/O、CPU、内存等等设备的使用，同时在使用各种系统调用时避免各种资源的开销过大。&lt;/p&gt;&#xA;&lt;h2 id=&#34;零拷贝&#34;&gt;零拷贝&lt;/h2&gt;&#xA;&lt;p&gt;认识零拷贝之前，我们先要对Linux系统I/O机制有一定的了解。当我们执行一个&lt;a href=&#34;https://man7.org/linux/man-pages/man2/write.2.html&#34;&gt;write(2)&lt;/a&gt;或者&lt;a href=&#34;https://man7.org/linux/man-pages/man2/read.2.html&#34;&gt;read(2)&lt;/a&gt;的时候（或者recv和send），什么时候操作系统会执行读写操作？什么时候又最终会落到磁盘上？&lt;br&gt;&#xA;以一个简单的echo服务器为例，我们模拟下每天都在发生的请求和回包：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;&#xA;&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;3&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;4&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;5&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;6&#xA;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#xA;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;sockfd&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;socket&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(...);&lt;/span&gt; &#x9;&#x9;&#x9;&#x9;&#x9;&lt;span class=&#34;c1&#34;&gt;//打开socket&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;buffer&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;new&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;buffer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(...);&lt;/span&gt; &#x9;&#x9;&#x9;&#x9;&lt;span class=&#34;c1&#34;&gt;//创建buffer &#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;while&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;((&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;clientfd&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;accept&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;socketfd&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;...)){&lt;/span&gt;&#x9;&lt;span class=&#34;c1&#34;&gt;// 接收一个请求&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&#x9;&lt;span class=&#34;nf&#34;&gt;read&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;clientfd&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;buffer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;...);&lt;/span&gt;        &lt;span class=&#34;c1&#34;&gt;//从文件内容读到buffer中 &#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&#x9;&lt;span class=&#34;nf&#34;&gt;write&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;clientfd&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;buffer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;...);&lt;/span&gt;       &lt;span class=&#34;c1&#34;&gt;//将buffer中的内容发送到网络&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#x9;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;/div&gt;&lt;p&gt;看一下这段代码的拷贝流程（下图）：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;数据包到达网卡，网卡进行DMA操作，把网卡寄存器的数据拷贝到内核缓冲区&lt;/li&gt;&#xA;&lt;li&gt;CPU把内核缓冲区的数据拷贝到用户空间的缓冲区&lt;/li&gt;&#xA;&lt;li&gt;用户空间处理buffer中的数据（此处不处理）&lt;/li&gt;&#xA;&lt;li&gt;CPU把用户空间的缓冲区的数据拷贝到内核缓冲区&lt;/li&gt;&#xA;&lt;li&gt;网卡进行DMA操作，把内核缓冲区的数据拷贝到网卡寄存器，发送出去&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;整个过程触发了4次拷贝（2次CPU，2次DMA），2次系统调用（对应4次上下文切换）&lt;br&gt;&#xA;（注：DMA(Direct Memory Access)， I/O 设备直接访问内存的一个通道，可以完成数据拷贝，使得CPU 不再参与任何拷贝相关的事情，现在几乎所有的设备都有DMA）&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://chestnutheng-blog-1254282572.cos.ap-chengdu.myqcloud.com/2021/zerocopy1.jpg&#34; alt=&#34;norm copy&#34;&gt;&lt;/p&gt;&#xA;&lt;h3 id=&#34;使用mmap&#34;&gt;使用mmap&lt;/h3&gt;&#xA;&lt;p&gt;mmap可以把用户空间的内存地址映射到内核空间，这样对用户空间的数据操作可以反映到内核空间，省去了用户空间的一次拷贝：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;应用调用mmap，和内核共享缓冲区（只需一次）&lt;/li&gt;&#xA;&lt;li&gt;数据包到达网卡，网卡进行DMA操作，把网卡寄存器的数据拷贝到内核缓冲区&lt;/li&gt;&#xA;&lt;li&gt;CPU把接收到的内核缓冲区的数据拷贝到发送的内核缓冲区&lt;/li&gt;&#xA;&lt;li&gt;网卡进行DMA操作，把内核缓冲区的数据拷贝到网卡寄存器，发送出去&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;整个过程触发了&lt;strong&gt;3次拷贝&lt;/strong&gt;（1次CPU，2次DMA），2次系统调用（对应4次上下文切换）&lt;br&gt;&#xA;&lt;img src=&#34;https://chestnutheng-blog-1254282572.cos.ap-chengdu.myqcloud.com/2021/zerocopy2.jpg&#34; alt=&#34;norm copy&#34;&gt;&lt;/p&gt;&#xA;&lt;h3 id=&#34;使用sendfilesplice&#34;&gt;使用sendfile/splice&lt;/h3&gt;&#xA;&lt;p&gt;Linux 内核版本 2.1 中实现了一个函数&lt;code&gt;sendfile(2)&lt;/code&gt;：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;他把&lt;code&gt;read(2)&lt;/code&gt;和&lt;code&gt;write(2)&lt;/code&gt;合二为一，成为一次系统调用，实现了把一个文件读取并写到另一个文件的语义&lt;/li&gt;&#xA;&lt;li&gt;系统调用中不再切换回用户态，而是在内核空间中直接把数据拷贝过去（2.4 之后这一步支持了DMA拷贝，实现了CPU零拷贝）&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;我门看下使用sendfile之后的流程：&lt;br&gt;&#xA;&lt;img src=&#34;https://chestnutheng-blog-1254282572.cos.ap-chengdu.myqcloud.com/2021/zerocopy3_2.jpg&#34; alt=&#34;norm copy&#34;&gt;&lt;br&gt;&#xA;整个过程触发了&lt;strong&gt;3次拷贝&lt;/strong&gt;（0次CPU，3次DMA），&lt;strong&gt;1次系统调用&lt;/strong&gt;（对应2次上下文切换）&lt;/p&gt;&#xA;&lt;p&gt;Linux 内核版本 2.6 中实现了一个函数&lt;code&gt;splice(2)&lt;/code&gt;，类似sendfile，但是接收/发送方必须有一个文件是管道，通过管道的方式连接发送方和接收方的内核缓冲区，不再需要拷贝（0次CPU，2次DMA，1次系统调用）&lt;/p&gt;&#xA;&lt;p&gt;transferTo（内部调用sendfile）的性能对比：&lt;br&gt;&#xA;&lt;img src=&#34;https://chestnutheng-blog-1254282572.cos.ap-chengdu.myqcloud.com/2021/zerocopy_prof.jpeg&#34; alt=&#34;copy&#34;&gt;&lt;/p&gt;&#xA;&lt;h3 id=&#34;对于我们的启发&#34;&gt;对于我们的启发&lt;/h3&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;零拷贝能带来显著的性能提升，目前kafka，nginx默认都开启了零拷贝（大文件传输可以提升60%以上）&lt;/li&gt;&#xA;&lt;li&gt;部分场景对时效性或者拷贝次数有严格的要求时（比如数据库、消息队列的实现），可以考虑用mmap或者直接I/O，配合自己实现的缓存替代操作系统的缓存方案&lt;/li&gt;&#xA;&lt;li&gt;拷贝很可能是CPU消耗的主要原因，比如业务代码中的大结构体复制，所以我们要谨慎控制复制操作，尽量使用指针或者引用类型&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 id=&#34;无锁&#34;&gt;无锁&lt;/h2&gt;&#xA;&lt;p&gt;多线程、多协程、多机器、多地部署是我们服务端实现高并发和强容灾的必备解决方案，这些方案都有一个共性，把数据或者过程分而治之。问题在于，几乎所有的并发场景都会涉及到数据竞争，涉及到共享数据的地方就会涉及到锁，协程有锁，线程有锁，多机部署的服务有分布式锁。&lt;br&gt;&#xA;服务中的锁会带来很多问题，随着并发数量的加大，会带来更大的上下文切换、用户态切换的开销，出现CPU飙升且都在做一些无用功的现象，也会导致性能快速下降，甚至还不如单线程模型的效率高。除此以外，各种锁还会带来很高的复杂度，和并发的复杂度相叠加，非常容易出现死锁和各种并发问题。&lt;/p&gt;&#xA;&lt;p&gt;因此，我们使用锁一定是去解决某种问题而去用的，能无锁就无锁，能轻量级就轻量级。&lt;/p&gt;&#xA;&lt;h3 id=&#34;无锁的替代方案&#34;&gt;无锁的替代方案&lt;/h3&gt;&#xA;&lt;h4 id=&#34;单线程&#34;&gt;单线程&lt;/h4&gt;&#xA;&lt;p&gt;最简单的方案就是单线程reactor模式，redis、nginx都用了这种方式来避免加锁带来的损耗和复杂性，适用于功能简单的场景。&lt;br&gt;&#xA;&lt;img src=&#34;https://chestnutheng-blog-1254282572.cos.ap-chengdu.myqcloud.com/2021/redissingle.png&#34; alt=&#34;redis &#34;&gt;&lt;br&gt;&#xA;如图，redis的单线程模型有这么几个部分：&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
